728x90
반응형
오늘은 가게 평점 기능을 구현하는데, 어떤 문제가 있고, 어떤 전략들을 쓸 수 있는지 등을 정리해보았습니다.
1. 문제 정의 (Problem Definition)
가게(Store) 테이블에 average_rating 컬럼 하나만 있을 때 발생하는 성능 및 정합성 문제를 해결해야 했습니다.
- 성능 저하: 리뷰 작성 시마다 전체 리뷰를 COUNT 쿼리로 조회하여 평균을 내는 방식은 데이터 증가 시 N+1 문제와 DB 부하를 야기함.
- 동시성 충돌: 여러 사용자가 동시에 리뷰를 남길 때, 동일한 값을 읽고 수정하여 데이터가 덮어씌워지는(Lost Update) 현상 발생.
2. 설계 선택 및 아키텍처 결정 이유
① review_count 컬럼 추가 (역정규화)
매번 전체 리뷰를 조회하는 대신, 가게 테이블에 리뷰 개수 컬럼을 추가했습니다.
- 로직: 새 평균 = ((기존 평균 * 기존 개수) + 새 점수) / (기존 개수 + 1)
- 이유: Row 한 줄 업데이트만으로 실시간 갱신이 가능하여 성능이 비약적으로 향상됨.
② 비관적 락(Pessimistic Lock) 선택 이유
동시성 제어를 위해 낙관적 락 대신 비관적 락을 선택했습니다.
- 이유: 평점 업데이트는 인기 가게의 경우 충돌이 매우 빈번하게 발생합니다. 낙관적 락은 충돌 발생 시 어플리케이션에서 재시도 로직을 계속 돌려야 하므로, 충돌이 잦은 환경에서는 DB 수준에서 순차 처리를 보장하는 비관적 락이 더 효율적이라 판단했습니다.
- 보완: 락 타임아웃을 대비해 @EnableRetry로 최대 3번 재시도를 수행합니다.
<Retry 전략>
1). 지수 백오프(Exponential Backoff) 적용
단순히 일정한 간격으로 재시도하는 것이 아니라, 실패 횟수가 늘어날수록 대기 시간을 지수적으로(2배씩) 늘렸습니다.
- 의미: 충돌이 발생했다는 것은 현재 시스템(DB)에 부하가 걸렸다는 신호입니다. 이때 즉시 재시도하면 다시 충돌할 확률이 높으므로, 대기 시간을 늘려 시스템이 숨을 쉴 틈(Recovery Time)을 벌어주는 전략입니다.
- 설정: multiplier = 2.0을 통해 0.5초, 1초, 2초... 식으로 대기 시간을 점진적으로 확장했습니다.
2). 랜덤 지터(Random Jitter) 추가
모든 재시도 요청이 정확히 2배씩 늘어난 시점에 동시에 다시 몰리는 '황소 떼 현상(Thundering Herd Problem)'을 방지하기 위해 랜덤성을 부여했습니다.
- 의미: 동일한 시점에 실패한 여러 요청이 똑같은 대기 시간을 갖지 않도록 엇박자(Jitter)를 주는 것입니다.
- 효과: 각 요청의 재시도 타이밍을 미세하게 분산시킴으로써 DB 락 경합(Contention)을 획기적으로 낮추고 성공 확률을 높였습니다.
3). 최대 대기 시간(maxDelay) 및 최후 방어선(Recover)
무한정 대기 시간이 늘어나 유저 경험을 해치는 것을 방지하기 위해 상한선을 두고, 최종 실패 시의 대응까지 설계했습니다.
- 상한선: maxDelay = 5000(5초)를 설정하여 아무리 재시도가 반복되어도 유저가 수십 초간 무한 루프에 빠지지 않도록 제어했습니다.
- 복구: 5번의 재시도가 모두 실패할 경우 @Recover 메서드를 통해 시스템 로그를 남기고, 유저에게는 "현재 요청이 많아 지연되고 있다"는 안내와 함께 예외를 던져 데이터 정합성을 끝까지 보호했습니다.
3. 고도화 방안 검토 및 기술적 트레이드오프
설계 과정에서 시스템의 안정성을 극대화하기 위한 고도화 방안들을 검토했습니다.
리뷰 Status 관리 및 트랜잭션 분리
- 장점 (서버 셧다운 대응): 리뷰 저장과 평점 반영 트랜잭션을 분리하고 status 필드를 두면, 서버가 갑자기 셧다운(Shutdown) 되어도 어떤 리뷰가 평점에 반영되지 않았는지 명확히 파악할 수 있습니다. 서버 재시작 후 PENDING 상태인 데이터를 찾아 다시 처리할 수 있는 강력한 복구 능력을 가집니다.
- 단점: 구현 복잡도가 상승하고 트랜잭션 관리가 까다로워집니다.
주기적 배치 보정(Batch Processing)
- 장점: 연산 과정의 부동 소수점 오차나 외부 요인(DB 직접 조작 등)으로 인한 데이터 불일치를 원천적으로 해결할 수 있습니다.
현실적인 판단 (Decision)
- 리뷰 평점은 결제 데이터처럼 1원의 오차나 일시적 누락도 허용되지 않는 크리티컬한 정보는 아닙니다.
- 현재 단계에서 서버 셧다운 복구 로직이나 배치 시스템을 도입하는 것은 '오버스펙(Over-engineering)'이라 판단했습니다.
- 따라서 비관적 락 + Retry를 통한 실시간 방어에 집중하고, 복잡한 보정 로직은 과감히 제외했습니다.
4. 향후 확장 계획 (Roadmap)
현재 설계는 초기 서비스 단계에서 충분한 안정성을 제공하지만, 추후 트래픽이 급증하거나 정합성 요건이 강화될 경우 다음 단계를 고려하고 있습니다.
- 서버 안정성 강화: 트래픽 증가로 서버 부하가 커지고 셧다운 리스크가 유의미해질 때, status 관리 및 메시지 큐를 도입하여 복구 메커니즘 구축.
- 최종적 정합성 확보: 부동 소수점 누적 오차가 사용자 경험을 저해할 수준이 되면, 새벽 시간대 배치 보정 프로세스 도입.
[추가 고민] 확장성을 위한 Status 도입 여부
- 고민: 서버 셧다운 대비를 위해 미리 status 필드를 설계에 포함해야 할까?
- 결론: 나중에 도입해도 늦지 않다. 현재는 구조를 단순하게 가져가서 개발 속도를 높이는 것이 더 중요하다 판단함.
- 근거: status는 추후 컬럼 추가와 로직 분리만으로도 충분히 확장이 가능한 구조이며, 평점 데이터의 특성상 현재는 복잡한 복구 메커니즘보다 비관적 락을 통한 실시간 정합성만으로도 충분한 신뢰도를 제공할 수 있음.
💡 오늘의 결론 (Summary)
- 효율성: review_count 추가로 실시간 업데이트 성능 확보.
- 안정성: 충돌이 잦은 특성에 맞춰 비관적 락으로 동시성을 제어함.
- 유연성: 서버 셧다운 대응(Status)이나 배치 보정은 강력한 도구이지만, 현재 데이터의 중요도를 고려해 추후 트래픽 증가 시 도입하기로 결정하며 기술적 트레이드오프를 실천함.
반응형
'Study' 카테고리의 다른 글
| [내일배움캠프 TIL] 20일차 - 1차 프로젝트 마무리, 회고 (0) | 2026.05.01 |
|---|---|
| [내일배움캠프 TIL] 18일차 - aws ec2 배포 및 이슈 (0) | 2026.04.29 |
| [내일배움캠프 TIL] 15일차 - JAVA 프로그램 실행 방법(터미널에서), Clean Test 전략 (1) | 2026.04.24 |
| [내일배움캠프 TIL] 14일차 - 3. @LastModifiedBy가 동작을 안할때. (0) | 2026.04.23 |
| [내일배움캠프 TIL] 14일차 - 2. @NotNull vs @Column(nullable = false) 차이점. (0) | 2026.04.23 |