소개
안녕하세요! D'art-gallery 프로젝트 백엔드 팀장 박창민입니다.
선착순 쿠폰 이벤트를 구현하면서 발생한 트러블슈팅을 포스트에 작성하겠습니다
본 글은 해당 게시글에 이어서 시작됩니다.
2024.06.11 - [구름톤 트레이닝 풀스택 회고] - [D'art-gallery] 선착순 쿠폰(1) - 대용량 트래픽, 동시성 이슈
문제점
이전 글에서 얘기했던 문제점 2가지입니다.
1) 대기열 큐 로직이 데이터베이스에 의존하여 대용량 트래픽 상황에서 과도한 부하가 발생합니다.
2) 쿠폰 발급 처리 스케줄러가 쿠폰 여부와 상관없이 1초마다 데이터베이스에 접근하여 부하를 유발합니다.
해결법
쿠폰 정보는 변경 가능성이 없으며, 동일한 요청에 대해 별도 연산 없이 동일한 응답을 반환하기 때문에 성능 개선을 위해 캐싱을 도입하기로 했습니다.
캐싱이란 ?
캐싱은 데이터나 연산 결과를 미리 저장해 두었다가 필요할 때 빠르게 제공함으로써 성능을 향상시키는 기술입니다.
주로 사용되거나 반복적으로 요청되는 데이터에 접근 속도를 높이기 위해 사용됩니다.
캐시 전략패턴
Look Aside vs Read Through
캐시의 읽기 전략으로 Look Aside와 Read Through가 있습니다.
저희 서비스는 Read Through 전략을 선택했습니다.
그 이유는 1초마다 데이터베이스에 접근하는 스케줄러 로직 때문입니다.
쿠폰이 없는 날에도 스케줄러가 1초마다 동작하는 상황에서 Look Aside 패턴을 사용하면 데이터베이스 접근 문제를 완전히 해결할 수 없다고 판단했습니다.
따라서 쿠폰 이벤트 유무와 상관없이 오직 캐시에만 접근하도록 설계했습니다.
고려해야 할 점
1. 캐시에 저장할 쿠폰 정보 데이터의 만료시간
Read Through 패턴을 사용함에 따라 만료 시간을 정확히 설정하지 않으면, 캐시에 데이터베이스에 없는 쿠폰 정보가 남아 있을 수 있으며, 반대로 데이터베이스에 쿠폰이 있어도 캐시에 정보가 없을 가능성이 있습니다.
2. Local Cache vs Global Cache
Local Cache는 메모리 또는 디스크를 사용해 각 서버에 캐시를 저장하는 전략입니다. 속도는 빠르지만 다른 서버의 캐시를 참조하기 어렵다는 단점이 있습니다.
Global Cache는 Redis나 Memcached와 같은 별도의 캐시 서버를 두어 데이터를 저장하는 전략입니다.
서버 간 데이터 공유가 용이하지만, 네트워크 트래픽으로 인해 로컬 캐시보다 속도가 느립니다.
고려사항 해결
캐시에 저장할 쿠폰 정보 데이터의 만료 시간
<선착순 쿠폰 이벤트를 요구사항>
1. 쿠폰은 미리 저장하여 사용자에게 몇 시간 뒤 이벤트가 시작하는지 알려줍니다.
2. 이벤트는 시간을 기준으로 하지 않고 날짜를 기준으로 매일 00시에 시작합니다.
이에 따라 최초 요청 시, 00시와 요청 시간의 차이를 계산해 만료 시간을 설정하기로 결정했습니다.
Local Cache vs Global Cache
현재 저희 서비스는 검색어 자동완성, 리프레시 토큰 등에 이미 Redis를 사용 중이며, 추후 서버가 2대 이상으로 확장될 경우 코드 변경이 크게 발생할 가능성을 고려해 Global Cache를 선택했습니다.
캐싱 구현 코드
<캐시 서비스 코드>
@Service
@RequiredArgsConstructor
@CacheConfig(cacheNames = "coupons")
public class PriorityCouponCacheService {
private final PriorityCouponRepository priorityCouponRepository;
@Cacheable(key = "#couponId + #now.toString()", cacheManager = "couponCacheManager")
public PriorityCoupon getByIdAndStartAt(Long couponId, LocalDate now) {
return priorityCouponRepository.findCouponByIdAndDateRange(couponId, now)
.orElseThrow(() -> new NotFoundException(ErrorCode.FAIL_COUPON_NOT_FOUND));
}
@Cacheable(key = "#now")
public Optional<PriorityCoupon> getByStartAt(LocalDate now) {
return priorityCouponRepository.findCouponByDateRange(now);
}
}
<캐시 조회 플로우>
<캐시 저장소를 사용하는 부분>
final Optional<PriorityCoupon> optionalCoupon = priorityCouponCacheService.getByStartAt(nowDate);
final PriorityCoupon priorityCoupon = priorityCouponCacheService.getByIdAndStartAt(dto.priorityCouponId(),
nowDate);
결론적으로, 쿠폰 데이터가 있든 없든 최초 요청 1회 시에만 데이터베이스를 조회하고 계속해서 캐시를 바라보기 때문에 데이터베이스의 부하를 줄여주었습니다.
다음 글은 레디스 캐시 대기열이 얼마나 버틸 수 있는지에 대한 부하테스트 글을 작성하겠습니다.
Reference
'구름톤 트레이닝 풀스택 회고' 카테고리의 다른 글
[D'art-gallery] 선착순 쿠폰(3) - 부하테스트 (12만 RPM) (0) | 2024.06.27 |
---|---|
[D'art-gallery] 선착순 쿠폰(1) - 대용량 트래픽, 동시성 이슈 (0) | 2024.06.11 |
[ThinkTank] 채점서버 (2) - N개의 테스트케이스를 실행시키기 (0) | 2024.05.06 |
[ThinkTank] 채점서버 (1) - 도커로 사용자 코드 실행시키기 (0) | 2024.04.28 |
⛅️[구름톤 트레이닝 풀스택 6회차] - 16주 차 회고⛅️ - [WEB IDE 프로젝트] (0) | 2024.04.20 |