개요
배경
우리가 만든 콘서트 예약 시스템이 흥행하여, 더욱 많은 사람들에게 콘서트 티켓을 장려하기 위해 일간 인기 콘서트를 배너로 보여주어야 하는 상황을 가정해보자. 이를 위해서는 일간 인기 콘서트를 위해 하루동안 예약된 콘서트 내용을 집계하여 사용자에게 보여주는 기능을 만들어야 한다.
우리는 하루동안 어떤 콘서트가 예약되어있는지 집계를 하지 않았기 때문에, 일간 인기 콘서트를 보여주기 위해서는 별도로 집계를 수행하는 기능을 구현해야한다. 또한 이렇게 집계된 데이터를 어떻게 처리할지에 대해서도 결정해야한다. 이러한 상황에서 어떤 방법이 좋을지 고민해보자.
문제 상황 및 해결 방법 제안
현재의 우리 시스템에서 일간 인기 콘서트 데이터 요청을 구현할 수 있는 가장 쉬운 방법은 요청을 받을 때마다 서비스 상에서 집계하여 매번 새로 계산하는 방법이다. 매 요청마다 전날 있었던 모든 예약을 조회해 콘서트 별로 집계하기 때문에, 당연하게도 성능이 좋지 않을 것을 예상할 수 있다.
또 다른 구현 방법으로는 데이터베이스의 View를 사용하는 방법도 있을 것 같다. 하지만 View를 사용하는 것도 부담을 DB에 넘길 뿐, 매 요청 시마다 집계를 위한 계산을 수행하는 것은 동일하다. 물론 SQL의 여러 집계함수가 존재하고 이와 쿼리 캐시를 사용하면 큰 부담이 되지 않고도 해결할 수 있지만, 학습 차원에서 분산환경에서 부하를 크게 줄일 수 있게 캐시를 적용하는 방안을 생각해보자.
일간 인기 콘서트를 미리 계산해 캐싱해두고, 요청이 들어온다면 캐시에서 데이터를 꺼내 반환하는 것이다. 전 주차에 Redis를 도입해 분산 락을 구현했으니, 이왕이면 분산환경에 맞춰 글로벌 캐시를 구현해보도록 하자.
As-Is
캐시를 적용하지 않은 채로 일간 인기 콘서트 기능을 구현하면 아래와 같이 될 것이다.
1.
일간 인기 콘서트 요청 인입
2.
ConcertFacadeService : 일간 인기 콘서트 조회 기능 호출
3.
ReservationService : 전날의 모든 예약 내역 조회 및 concertId 별로 횟수 집계
4.
일간 인기 콘서트 Top 20 데이터 응답
To-Be
어떻게 구현할 지를 고민하기 전에 먼저 캐시에 대해 학습하고 글로 정리해보았다.
정리 내용 : 캐시와 캐시 전략, 캐시 문제캐시와 캐시 전략, 캐시 문제
위 내용을 고려해 우리의 시스템에 적합한 캐시 전략을 고민해보자.
읽기 전략
Look Aside
•
장점
◦
캐시 서버의 장애가 서비스 장애로 이어지지 않는다.
◦
일간 인기 콘서트는 매일 업데이트 되기 때문에, 캐시 미스가 날 확률이 적다.
◦
데이터가 하루에 한 번 바뀌는 일간 인기 콘서트 데이터 특성상, 정합성이 맞지 않을 수 있다는 점은 단점이 되지 않을 수 있다.
◦
캐시를 직접 다루어볼 수 있다.(학습적 측면)
•
단점
◦
캐시 갱신 없이 데이터베이스 업데이트만 발생한다면 데이터 정합성이 맞지 않는 상태가 된다.
Read Through
•
장점
◦
일간 인기 콘서트는 매일 업데이트 되기 때문에, 캐시 미스가 날 확률이 적다.
◦
데이터 정합성이 거의 깨지지 않는다.
•
단점
◦
데이터 조회를 전적으로 캐시에 의존하기 때문에, 캐시 서버가 다운되는 경우 서비스 장애로 이어진다.
◦
캐시와 일치하는 내용을 데이터베이스에 저장할 필요가 있다.
◦
라이브러리 및 reference의 미흡(부재일수도…?)
Read-Through에 대해 직접 찾아보았을 때, 명확하게 특정 라이브러리에서 캐시가 직접 동기화를 수행하도록 지원한다는 내용은 찾아볼 수 없었다. 멘토링을 청강했을 때도 실무에서 Read-Trough 전략을 사용했다는 이야기를 들어본 적이 없다고 하셨다.
쓰기 전략
Write Back
•
장점
◦
데이터베이스에 장애가 발생하더라도 캐시를 통해 지속적인 서비스 제공이 가능하다.
•
단점
◦
캐시에 자주 사용되지 않는 불필요한 리소스를 저장하게 된다.
◦
캐시 서버가 장애가 발생하면, 그 사이의 저장되지 않은 데이터가 유실될 수 있다.
◦
캐시 업데이트가 자주 발생하지 않는 상황이라, 기존의 장점인 쓰기 작업 부하를 줄이고 정합성을 잘 지킨다는 장점의 메리트가 없다.
Write Through
•
장점
◦
DB와 캐시가 항상 동기화 되어있어 캐시의 데이터가 항상 최신의 상태로 유지된다.
•
단점
◦
캐시에 자주 사용되지 않는 불필요한 리소스를 저장하게 된다.
◦
라이브러리 및 reference의 미흡(부재일수도…?)
Write Around
•
장점
◦
데이터베이스에 직접 저장하므로, 캐시 서버 장애가 발생하더라도 데이터 유실이 없으며 서비스 장애로 이어지지 않는다.
•
단점
◦
쓰기 작업 시 캐시를 업데이트하지 않고 캐시 미스 시에 업데이트하기 때문에, 해당 캐시가 만료되기까지 데이터 정합성이 맞지 않는 상태가 된다.
Write Invalidate
•
장점
◦
write-around의 장점을 그대로 가져가면서도, 다음 읽기 요청 시 캐시 미스를 강제로 유발시켜 최신 데이터를 읽을 수 있도록 유지한다.
•
단점
◦
쓰기 작업 이후에는 해당 캐시 무효화 되기 때문에, 쓰기 작업이 자주 발생하는 경우 캐시 미스가 자주 발생할 수 있다.
검토
위에서 캐시에 대한 읽기 전략과 쓰기 전략을 여러 조건에서 비교해보았는데, 지금의 서비스 상황에 딱 맞는 전략이 있는지는 잘 모르겠다. 다만 최적의 전략을 고민 해본 결과는 다음과 같다.
•
읽기 전략
◦
Look Aside 전략을 살짝 변형하여, 캐시에 조회해보고 없다면 DB에서 데이터를 만들어 캐시에 저장 후 그 값을 반환한다.
◦
이와 같이 하는 이유는 일간 인기 콘서트의 경우 데이터베이스에서 데이터를 꺼내 가공해야하는 부분이 있기 때문에, 캐시 미스 시 응답을 나가기 위해 데이터를 가공하면서 동시에 캐시를 업데이트하는 것이 최적이라고 생각했다.
•
쓰기 전략
◦
특히 쓰기 전략이 위 내용 중에 적합하다고 느껴지는 것이 없었다.
◦
인기 콘서트 캐싱에 대해서는 스케줄러가 호출하거나 캐시 미스가 발생한 경우 업데이트가 필요하고, 원본이 되는 데이터(일마다 TOP 20의 콘서트)가 DB에 이쁘게 저장되어 있는게 아닌 상황이다. 그렇다보니 갱신 작업은 자주 불리지 않는 것이 좋으며, 한 번 불릴 때 이전의 데이터를 무효화하고 모든 데이터를 갱신하도록 구현하는게 좋아보인다.
◦
또한 콘서트의 정보가 수정되는 경우에도 캐시 중 일부를 수정할 필요가 있다는 생각이 들었으나, 전날 인기 예약 콘서트(수정되기 전의 정보로 많이 예약된 콘서트)라는 점과 캐시 스탬피드 가능성의 증가, 얻는 효용 대비 구현의 복잡성의 이유로 적용하지 않는 것이 나을 것 같다.
결론을 내보자면,
1.
스케줄러가 매일 00:00:00 ~ 00:05:00 쯤 API를 호출하여 이전 캐시를 무효화하고 새로운 캐시 데이터를 갱신한다.
2.
인기 콘서트 조회 시에는 일단 캐시를 확인하고 없으면 갱신 로직을 호출한다.
이 정도로 요약할 수 있을 것 같다.
캐시 적용하기
요구사항 분석
•
일간 인기 콘서트 시간 범위
◦
전일 00:00:00 ~ 당일 00:00:00
•
캐시 만료
◦
스케줄러가 도는 시간과 딱 맞추게되면, 데이터를 가공하는 동안 캐시 스템피드 현상이 발생할 수 있기 때문에 TTL은 다음날 10분에 만료하도록 설정한다.
◦
데이터 가공을 끝내고, 캐시 갱신을 하는 시점에 삭제 후 바로 갱신하도록 구현한다.
•
캐시 저장 데이터
◦
인기 콘서트에 대한 등수는 제공하지 않는다.
◦
Hash 자료구조를 사용해 인기 콘서트 id별 콘서트 정보를 각각 캐싱한다.
◦
캐시의 키는 data class의 해시 코드로 한다.(엔티티 변경 시 캐시 미스를 유도하여 갱신되도록 한다)
•
캐시 갱신
◦
캐시 갱신은 외부 스케줄러가 API를 호출하거나, 캐시 미스 시 내부 로직을 호출하여 수행한다.
◦
캐시 갱신 시 스템피드 현상을 최대한 피하기 위해, 데이터 조회 및 가공을 마치고 나서 캐시에 업로드 직전에 이전 캐시를 무효화하고 새로운 캐시를 업데이트한다.
기능 설계하기
기능별로 구현해야하는 내용을 의사코드로 작성해보자.
•
일간 인기 콘서트 갱신 기능
◦
스케줄링 설정
◦
전날 00:00:00 ~ 오늘 00:00:00 예약 목록 조회
◦
예약이 들고 있는 콘서트 id 기준으로 top 20 집계 및 해당 콘서트 정보 조회
◦
캐시 업데이트 및 만료 시간 설정
•
일간 인기 콘서트 조회 API
◦
캐시 조회
◦
캐시가 있다면 반환 / 캐시가 없다면 캐시 갱신 로직 호출