본문 바로가기
개발/대규모 시스템 설계

[대규모 시스템 설계] 캐시 설계 전략

by 경험의 가치 2024. 5. 20.

캐싱 설계 전략 왜 필요한가?

캐싱은 서비스의 응답시간을 대폭 줄일 수 있는 아주 좋은 기술이다.

 

우리가 특정 사이트로부터 공지사항을 크롤링 해오고, 이를 목록으로 보여주는 기능을 구상했다고 해보자. 그리고 이 공지사항은 24시간에 한번만 크롤링을 해와 자주 바뀌지 않는 정보라고 가정해보자. 그런데 자주 바뀌지도 않는데 매번 RDB에서 정보를 읽어오면 어떨까? 가뜩이나 리스트 형태여서 많은 정보를 읽어와야하는데, 자주 바뀌지도 않는데도 매요청마다 읽어오면 매우 비효율적이고 성능에 치명적일 것이다.

 

따라서, 자주 바뀌지 않는 정보는 메모리에 캐싱을 해두면 빠르게 사용자의 요청에 응답할 수 있게 된다.

 

얼마나 빨리지는지 결론이 궁금한 사람들이 있을 것 같에서, 실제 우리 프로젝트에서 적용한 캐싱 결과를 공개하겠다.

AOP를 이용해서 메서드별 시간 측정을 하도록 만들었는데, 캐싱 전에는 공지사항 목록을 읽어오는데 414ms가 걸리는 모습이다.

캐싱된 데이터를 읽어오면 12ms만에 메서드가 실행된다. 거의 1/40 가까이 실행시간이 줄어든 모습을 볼 수 있다. 이렇듯, 캐싱을 하면 아주 효과적이다.

 

메모리가 무한하다면 모든 데이터를 캐싱해두면 확실히 좋긴 할 것이다. 하지만, 우리의 메모리는 무한하지 않다. 모든 데이터를 메모리에 캐싱해두면 용량이 부족해서져서 서버가 다운될 수 있다. 뿐만 아니라, 실제 데이터와 캐시 데이터가 서로 같지 않는 정합성 문제도 발생할 수 있다. 따라서, 우리는 어떤 데이터를 캐싱 해야하는지, 그리고 어떤 방식으로 캐싱을 해야하는지 캐싱 설계 전략을 사전에 짤 필요가 있다.

 

캐싱 전략은 읽기 전략과 쓰기 전략이 있다. 이번 포스팅에서는 이에 대해 다뤄보고자 한다. 참고로 캐시 서버는 Redis말고도 다른걸 사용할 수 있으나, 일반적으로 Redis를 가장 많이 사용하므로, 이를 바탕으로 다룰 것이다. 


캐시 읽기 전략

캐시 읽기 전략은 크게 두가지가 있다.

 

Look Aside 전략

 

Look Aside 전략은 다음과 같은 순서로 진행된다.

 

1. 우선적으로 Cache에 데이터를 받아온다. Cache Hit이 발생하면, 그 데이터를 전달한다. Cache Miss가 발생하면 2번으로 넘어간다.

2. 실제 RDB에서 데이터를 받아오고 전달한다.

3. RDB에서 읽어온 데이터를 Cache에 저장한다.

  • 가장 많이 사용되는 읽기 전략
  • Cache Aszie, Lazy Loading 전략이라고도 불림
  • 읽기가 많은 경우에 적합함
  • Cache가 Database에 직접적으로 연결되지 않아서, 캐시에 장애가 발생하더라도 서비스에는 문제가 없음
  • 하지만, Cache에 Connection이 많다면, 캐시가 다운되는 순간 많은 요청이 DB로 몰려서 부하가 발생
  • 데이터의 정합성 문제가 발생할 수 있음

위 전략은 좋지만 한가지 문제점이 있다. 서비스 초기에는 데이터 조회시에는 캐시에 데이터가 저장되있지 않아서 지속적으로 Cache miss가 발생하여 DB 성능 저하가 올 수 있다. 따라서, DB에 미리 캐시로 데이터를 넣어두는 작업인 Cache Warming 과정이 필요하다.  

 

 

Read Through 전략

 

Read Through 전략은 다음과 같다.

 

1. 우선적으로 Cache에 데이터를 받아온다. Cache Hit이 발생하면, 그 데이터를 전달한다. Cache Miss가 발생하면 2번으로 넘어간다.

2.  "캐시"에서 실제 RDB에서 데이터를 조회하고 캐시 데이터를 업데이트한다. 

3.  캐시에서 읽어온 데이터를 전달한다.

  • Look Aside와 가장 큰 차이는 캐시에 데이터를 저장하는 주체이다. Look Aside는 서버가 저장하지만, Read Through는 캐시가 직접 저장한다.
  • 데이터 조회를 전적으로 캐시에 의지하므로, 캐시가 다운되면 서비스가 망가져버림. 따라서, 캐시 서버를 Replication 하거나 Cluster로 구성하여 운영하는 것이 거의 필수적임. (Look Aside도 물론 이렇게 운영하는 것이 더 좋음)
  • 캐시가 데이터를 읽어오기 때문에, 정합성 문제에서 벗어날 수 있음

캐시 쓰기 전략

캐시 쓰기 전략으로는 크게 3가지가 있다.

 

Write Back 전략

 

1. 우선적으로 Cache에 데이터를 저장한다.

2. 일정 시간마다 실제 RDB에 데이터를 반영한다.

  • 데이터를 캐시에 일정 주기 모아서 Batch를 통해 실제 DB에 반영하는 형태 (Batch에 대하여)
  • 데이터의 정합성이 확보됨
  • 재사용되지 않는 데이터도 무조건 캐시에 넣어버리기 때문에 TTL 설정을 잘 해야한다.
  • Scheduler를 통해 모았다가 실제 DB에 반영하기 때문에 쓰기 비용을 대폭 줄일 수가 있음
  • 캐시에서 오류가 발생시, 데이터가 영구 소실됨

 

또한, 해당 Write Back 전략을 응용하여 Race Condition을 해결하는데도 사용할 수 있다. 실제로, 조회수나 추천수 같이 Race Condition이 많이 발생하는 상황에서 우리 팀은 Redis를 이용하여 Race Condition을 해결했다. 관련 내용 링크는 아래 글을 참조 바란다.

 

 

레디스와 스케줄러를 통한 조회수 증가 (+ 동시성)

레디스와 스케줄러를 통해 조회수 증가 로직을 개선해보았습니다. ( + 동시성도 고려해 보았습니다..)

velog.io

 

 

Write Through 전략

 

1. Cache에 우선적으로 데이터를 저장

2. 그 다음 DB에 데이터를 저장

  • Cache는 항상 최신 정보를 가지고 있어서 정합성 문제가 없어진다.
  • 데이터가 유실될 위험이 적어진다.
  • 재사용되지 않는 데이터도 무조건 캐시에 넣어버리기 때문에 TTL 설정을 잘 해야한다.
  • 매 요청마다 Write 작업이 두번 일어나서 빈번하게 수정이 일어날 경우 성능 저하가 발생한다.
  • DB 성능이 느릴 경우, 대기시간이 많이 발생한다.

Write Around 전략

 

1. 반드시 데이터를 RDB에 기록한다.

2. 기존 Cache에 저장되있는 데이터를 삭제하거나 무효화한다.

  • 모든 데이터는 DB에 저장됨. 따라서, Write 전략 중에서 가장 빠름
  • 하지만, 데이터의 정합성 문제가 발생할 수 있음

어떤 전략 조합을 사용해야될까?

일반적으로 가장 많이 쓰이는 조합은 Look Aside + Write Around 조합이다.

 

해당 조합을 사용하면, 쓸때 마다 Cache를 무효화 하고 (그럼 자연스럽게 Cache Miss를 유도해서 Database에서 읽어올 수 있다), 읽어올 때 우선적으로 Cache로 부터 읽어온다. 이때, Cache Miss가 나면 Database에서 읽어와서 캐시에 저장이 된다.

 

Read Through + Write Around 조합도 쓰일 수 있다.

 

항상 DB에 쓰고, 읽어올 때는 캐시가 DB로 부터 끌어와서 읽어오므로 데이터 정합성 문제가 발생하지 않는 완벽하게 안전한 구성이다.

 

그렇다면 Spring에서 이를 어떻게 구현하는가? 추후 다룰 예정이지만, Spring에서는 @Cacheable이라는 어노테이션을 통해서 쉽게 캐싱이 가능하다. 추가적으로, 캐시 저장소를 Redis로 설정해기만 하면된다. 현재 글을 작성하지 않아 관련 링크로 대체한다.

 

 

Spring Boot caching with Redis

Implementing Caching in Spring Boot Application Using Redis

medium.com

 


어떤 데이터를 캐싱해야될까?

캐싱할 데이터는 자주 사용되면서 자주 바뀌지 않는 데이터를 우선적으로 선택해야된다. 두번째 고려해야 될 사항은, 캐시는 메모리에 저장하기 때문에 데이터의 휘발성을 기본으로 한다. 또한, 앞서 언급했다싶이 데이터의 정합성 문제가 발생할 수 있다. 따라서, 중요한 정보는 되도록 캐싱하지 않아야한다.

 

 

내가 소프트웨어 공학 강의를 들으면서, 들었던 얘기중 하나가 파레토의 법칙이다. 캐싱도 이 법칙을 따른다. 모든 데이터를 굳이 캐싱할 필요가 없다. 파레토 법칙에 따라서 일부 데이터만 캐싱해도 대부분 데이터가 커버가 되기 때문이다.


캐싱 유효기간을 얼마나 잡아야될까?

우리가 캐싱을 했는데, 오랫동안 그 데이터를 제거하지 않고 유지한다면 반드시 정합성 문제가 발생할 것이다. 뿐만 아니라, 데이터를 캐싱했는데, 업데이트 했음에도 불구하고 옛날 데이터를 그대로 보관하면 메모리 사용량에 문제도 생길 것이다. 따라서, TTL을 설정해서 캐싱의 유효기간을 설정할 필요가 있다.

 

이게 참 시간 설정하는 부분이 어려운 일이다. 분명하게 이 정도가 좋다고 말할 수 있는게 없다. 데이터의 특성에 따라서, 사용 빈도에 따라서 다 다르기 때문에 스스로 알잘딱하게 판단해서 정할 필요가 있다.

 

예를 들어, TTL을 너무 길게 설정하면, 정합성 문제가 발생하고, 쓸모없는 데이터들이 메모리를 차지하게 된다. 반대로, TTL을 너무 짧게 설정하면 데이터가 너무 빨리 제거되어서 캐싱을 하는 의미가 없어진다.

 

nhncloud에서 설명하는 이미지

 

또한, Cache Stampede 현상이라고 존재하는데, 우리가 Look Aside 전략에서 TTL을 너무 짧게 설정했다고 가정해보자. 그런데, 우연히도 수많은 트래픽이 딱 캐싱이 만료되는 시간에 접근했다고 해보자. 그러면, 그 수많은 트래픽이 동시에 DB에 접근할 것이고, duplicate read와 duplicate write가 발생하여 서비스 장애로 이어질 가능성이 커진다.

 

따라서, 적절하게 캐싱 TTL을 설정하는 것이 좋다. 이건 스스로 판단해서....


💡참고자료