리뷰 모듈 기획·설계·구현 정리서 (review × bandroom-info 메시징 & 캐싱 중심)
1. 배경과 목표
1.1 배경
밴드룸 플랫폼은 사용자들이 합주실(밴드룸)을 선택할 때 객관적인 평판 지표(평균 평점, 리뷰 수)를 빠르게 확인할 수 있어야 한다. 리뷰 본문(텍스트, 사진 등)은 review 도메인에서 보관/조회하고, 대량 트래픽이 걸리는 리스트/상세 뷰에서 필요한 핵심 지표(평균 평점, 리뷰 수)는 bandroom-info에 캐싱하여 고속으로 제공하는 전략을 택했다.
1.2 목표
- 리뷰 생성/수정/삭제 시점의 이벤트를 통해 bandroom-info의 캐시(리뷰 수/평점)를 일관되게 갱신한다.
- 트래픽 많은 조회 경로에서 bandroom-info의 캐시만으로도 빠르고 비용 효율적인 응답을 제공한다.
- 실패/지연 상황에서도 재처리와 멱등성을 통해 최종적 일관성을 보장한다.
- 메시징 인프라(RabbitMQ) 상의 Ack/Nack 정책을 명확히 하여 “중복 처리/유실” 위험을 최소화한다.
2. 전체 아키텍처 개요
- review 서비스: 리뷰 CRUD의 소스 오브 트루스(Source of Truth). DB에 원장 데이터를 보관하며, 리뷰 생성 시 ReviewCreatedEvent를 발행한다. (향후 Update/Delete 이벤트도 확장)
- bandroom-info 서비스: 합주실 메타 정보 제공. 리뷰 수/평균 평점을 자신의 데이터 모델(Review 엔티티)로 캐싱하여 고속 조회 제공. review 서비스가 발행한 이벤트를 구독해 캐시를 갱신한다.
- 메시징: RabbitMQ(Direct Exchange). 현재 이벤트: review.created.exchange → review.created.queue. routing key: review.created.routingkey.
- 캐싱: bandroom-info의 RDB 내 Review 테이블(1:1, @MapsId)로 영속 캐싱. 필요 시 Redis 등 2차 캐시 확장 고려.
논리적 데이터 흐름(간략):
1) 클라이언트가 리뷰 생성 → review 서비스가 DB 트랜잭션 커밋 후 ReviewCreatedEvent 발행
2) bandroom-info의 ReviewEvent 컨슈머가 이벤트 수신 → ReviewService.createReview(...)로 캐시 갱신
3) 리스트/상세 조회에서 bandroom-info는 자신의 Review 캐시를 사용해 즉시 응답
3. 메시징 설계 상세
3.1 교환기/큐/라우팅 키
- Exchange: review.created.exchange (Direct)
- Queue: review.created.queue
- Routing key: review.created.routingkey
두 서비스 모두에서 해당 리소스를 선언하여 idempotent하게 존재 보장. 선언부는 다음 위치에 존재:
- review 서비스: review/config/RabbitMQConfig.java
- bandroom-info 서비스: bandroominfo/config/RabbitMQConfig.java
설정상 동일한 이름으로 DirectExchange/Queue/Binding을 구성한다. 운영 환경에서는 IaC 혹은 중앙화된 인프라 레이어에서 선 선언을 권장하지만, 서비스 부트스트랩 시 선언은 안전한 중복 선언(멱등) 동작을 한다.
3.2 컨슈머 ACK 전략
- 컨슈머: bandroom-info/consumer/rabbitMQ/ReviewEvent
- 컨테이너 팩토리: manualAckListenerContainerFactory (AcknowledgeMode.MANUAL)
- 처리 성공: channel.basicAck(deliveryTag, false)
- 처리 실패: channel.basicNack(deliveryTag, false, false) → 기본적으로 재큐잉하지 않음. (Poison message 방지)
운영 중 관측된 이슈에서, 컨테이너가 AUTO ACK인데 리스너에서 수동 ack/nack을 수행하여 “unknown delivery tag” 오류가 발생했다. 이를 MANUAL ACK로 정정함으로써 중복 ack 문제를 해소했다. 해당 변경은 bandroominfo/config/RabbitMQConfig.java에 manualAckListenerContainerFactory를 추가하고, ReviewEvent의 @RabbitListener에 containerFactory 속성으로 지정하는 방식으로 반영되었다.
3.3 이벤트 스키마
ReviewCreatedEvent (event.MQevents.review.ReviewCreatedEvent) 예시 페이로드:
- bandRoomId: Long
- reviewRate: Double
- brokerType: String (메타 정보)
- topic: String (메타 정보)
현재 bandroom-info는 bandRoomId와 reviewRate만을 이용하여 집계를 수행한다. event 버전 및 스키마 관리 방안은 9장(버전 관리)에서 추가 설명한다.
4. bandroom-info의 캐싱 모델
4.1 데이터 모델
- BandRoom (bandroominfo/entity/BandRoom.java)
- Review (bandroominfo/entity/Review.java)
- 1:1 관계, @MapsId 사용. Review.id = BandRoom.roomId와 동일.
- 필드: reviewCount(Long), reviewAvg(Double), bandRoom(BandRoom)
@MapsId를 통해 Review의 PK가 BandRoom의 PK와 동일하여 조인 비용 최소화 및 참조 일관성 확보. BandRoom.review로 즉시 접근 가능한 캐시를 제공한다.
4.2 집계 수식
새 평점 r가 추가될 때:
- 새 평균 = (기존 평균 × 기존 개수 + r) / (기존 개수 + 1)
- 새 개수 = 기존 개수 + 1
정밀도 문제를 고려해 Double 사용 시 반올림 정책 또는 BigDecimal 전환이 향후 과제로 있다. 현재 구현은 Double 기반이며, UI 노출 시 소수점 자리수 포맷 기준을 API 레벨에서 통일할 필요가 있다.
4.3 캐시 생성/갱신 플로우
- Entry 생성 케이스: 해당 BandRoom에 Review 엔티티가 아직 없을 때, reviewAvg=reviewRate, reviewCount=1로 신규 생성하여 BandRoom에 양방향으로 연결(setReview) 후 영속화.
- 기존 캐시 갱신 케이스: 위 수식으로 평균/개수를 갱신 후 저장.
구현: bandroominfo/service/ReviewService.java
주요 포인트:
- BandRoom 조회 실패 시 BandRoomException(BAND_ROOM_NOT_FOUND_BY_ID) 발생
- Review 엔티티가 없을 경우 builder로 생성 후 bandRoom.setReview(review)로 양방향 할당
- 저장 순서: reviewRepository.save(bandRoom.getReview()) → bandRoomRepository.save(bandRoom)
- @MapsId 구조에서 Review 측 식별자 일치가 핵심이므로, 명시적 저장 순서를 통해 null 문제를 예방
운영 이슈: 초기 코드에서는 Review 객체를 생성만 하고 bandRoom.setReview(...) 하지 않아 bandRoom.getReview()가 null이 되어 save 호출에서 InvalidDataAccessApiUsageException이 발생. 이를 수정해 null 저장 시도를 제거했다.
5. 구현 상세 (코드 기준)
5.1 RabbitMQ 리소스 선언 (양쪽 서비스 공통)
- DirectExchange reviewCreatedExchange()
- Queue reviewCreatedQueue()
- Binding bindingReviewCreated(...)
각 서비스의 RabbitMQConfig.java에 동일한 선언이 존재하여, 서비스 부팅 시 존재하지 않으면 생성된다.
5.2 bandroom-info 컨슈머
파일: bandroominfo/consumer/rabbitMQ/ReviewEvent.java
핵심 구현:
- @RabbitListener(queues = "review.created.queue", containerFactory = "manualAckListenerContainerFactory")
- ObjectMapper로 JSON 페이로드를 ReviewCreatedEvent로 역직렬화
- ReviewService.createReview(bandRoomId, reviewRate) 호출
- 성공 시 basicAck, 실패 시 basicNack(requeue=false)
로그 전략:
- 수신 로그: "[결제 완료 이벤트 수신]"라는 기존 문구가 있으나, 실제로는 "리뷰 생성 이벤트"임. 레이블 정비가 향후 과제로 남아 있다.
- 처리 결과 로그: 성공/실패 메시지 출력
5.3 bandroom-info 서비스 로직
파일: bandroominfo/service/ReviewService.java
핵심 구현:
- @Transactional 경계에서 캐시 생성/갱신
- 신규 Review 생성 시 @Builder로 생성 후 bandRoom.setReview(review)로 역참조 세팅
- 누적 평균 공식 적용, reviewCount 증가
- reviewRepository.save(...) 후 bandRoomRepository.save(...)
예외 케이스:
- 밴드룸 미존재 시 BandRoomException으로 구체적 에러 코드 반환
5.4 엔티티 모델
BandRoom.java
- @OneToOne(mappedBy="bandRoom", cascade=ALL, orphanRemoval=true) Review 연관
- 추후 lazy 로딩 시 N+1 방지 위해 fetch 전략과 조회 API 설계 검토 필요
Review.java
- @Entity(name="review"), @OneToOne(fetch=LAZY) @MapsId, @JoinColumn(name="room_id")
- reviewCount(Long), reviewAvg(Double) 기본 필드
- 정확한 소수점 정책 미정(향후 개선)
6. 운영 이슈와 해결
6.1 “unknown delivery tag” 오류
- 증상: RabbitMQ 소비자 스레드에서 PRECONDITION_FAILED - unknown delivery tag 1
- 원인: 컨테이너의 acknowledgeMode=AUTO인데, 리스너에서 channel.basicAck/basicNack를 직접 호출 → 이미 auto-ack된 메시지에 대해 중복 ack 시도
- 해결: manualAckListenerContainerFactory를 도입하여 AcknowledgeMode.MANUAL로 전환하고, 리스너에 containerFactory 지정. defaultRequeueRejected=false 설정으로 실패 시 재큐잉 방지
6.2 InvalidDataAccessApiUsageException: Entity must not be null
- 증상: 리뷰 생성 이벤트 처리 중 reviewRepository.save(bandRoom.getReview())에서 null 전달
- 원인: Review 객체 생성 후 bandRoom.setReview(review)를 호출하지 않아, 양방향 관계가 완결되지 않음
- 해결: ReviewService에서 신규 생성 시 setReview 호출, 저장 순서 정리(Review → BandRoom)
7. 에러 처리·재처리·멱등성 전략
7.1 현재 정책
- 컨슈머 실패 시 basicNack(requeue=false)로 죽은 편지(혹은 폐기)에 가까운 동작. 운영 편의상 DLQ를 명시적으로 구성하는 것이 권장된다.
- 멱등성: 같은 리뷰 생성 이벤트가 중복 도착할 가능성을 고려해야 한다. 현재는 단순 누적 계산으로 중복 처리 시 지표가 왜곡될 수 있다.
7.2 개선안
- DLX/DLQ 구성: review.created.queue에 dead-letter-exchange/queue를 설정, 실패 이벤트를 DLQ로 보내고 알람·재처리 툴을 연결
- 재처리 UI/잡: DLQ에서 이벤트를 분석 후 재투입(republish)하는 운영 툴 마련
- 멱등성 키: 이벤트에 reviewId(리뷰 PK) 또는 eventId(글로벌 유니크 ID)를 포함시켜, bandroom-info 측에서 최근 처리 키를 Redis/DB에 저장 후 중복 방지
- 일괄 재계산 잡: 주기적으로 review 원장(Review 서비스)을 스캔해 bandroom-info 캐시를 보정하는 백그라운드 잡 운영(궁극적 정합성 보조)
8. 성능·확장성 고려
8.1 처리량 가정
- 리뷰 생성 TPS가 스파이크 시 수백~수천까지 증가할 수 있음
- 이벤트 컨슈머는 수평 확장 가능해야 함(concurrency 조정, 파티셔닝 고려)
8.2 컨슈머 튜닝 포인트
- SimpleRabbitListenerContainerFactory에서 prefetchCount 설정(예: 50~300)로 네트워크 왕복 감소
- concurrency, maxConcurrency 조절로 파이프라인 병렬 처리
- 비즈니스 로직 트랜잭션 경계 최소화, DB 인덱스 최적화 (BandRoom PK, Review PK)
8.3 캐싱 계층 확장
- 현재는 RDB 캐시만 존재. 리스트 조회 트래픽 급증 시 Redis 레이어 추가 고려:
- Key: review:room:{roomId} → {count, avg}
- 쓰기 경로: 이벤트 처리 시 RDB 갱신 후 Redis upsert (Write-through)
- 읽기 경로: bandroom-info API → Redis hit; 미스 시 RDB 조회 + Redis 채움
- TTL 정책: 0(상시), 혹은 짧은 TTL로 탄력적 일관성
9. 이벤트 스키마·버전화 전략
9.1 스키마 버전 관리
- ReviewCreatedEvent에 version 필드를 추가하여 소비자 호환성 관리
- JSON Schema(or Avro/Protobuf)로 포맷을 명시하고 CI에서 호환성 체크
9.2 이벤트 확장 계획
- ReviewUpdatedEvent: 평점 수정/신고/블라인드 처리 등으로 평균/개수 조정 필요 시
- ReviewDeletedEvent: 리뷰 삭제로 인한 평균/개수 재계산(주의: 평균 공식 역산 필요. 합계/개수 보관이 유리함)
평균 재계산을 정확하고 빠르게 하려면, 평균만 보관하는 대신 합계(sumOfRatings)와 개수(count)를 보관하는 설계가 더 안정적이다. 향후 필드 전환 계획을 검토한다.
10. API 설계와 사용성
10.1 bandroom-info 조회 API
- 리스트/상세 공통: reviewAvg, reviewCount를 함께 제공
- 포맷: 소수점 자리수 정책(예: 소수점 1자리 반올림) 통일
- 정렬: 평균 평점 내림차순 정렬, 리뷰 수 2차 정렬 등 인덱스 전략
10.2 review 서비스 API
- 리뷰 생성 POST /reviews
- 리뷰 수정/삭제 향후 추가. 이벤트 발행은 트랜잭션 아웃박스 패턴 도입 권장
11. 일관성 모델
- 최종적 일관성(Eventual Consistency): 리뷰 생성 직후 bandroom-info 캐시 반영까지 수 ms~수 초 지연 가능
- UX 고려: 생성 직후 사용자 개인 화면에서 서버 사이드 강제 동기화(예: 리뷰 작성 응답에 최신 값 포함) 또는 폴링/소켓을 통한 보정
- 운영 도구: 지연 통계, 누락 탐지(원장 대비 캐시) 알람
12. 보안·권한
- 메시지 브로커 접근 제어: 애플리케이션 단위 계정/권한
- 데이터 무결성: 리뷰 이벤트에 서명/검증은 과도하나, 내부망 보호와 TLS 적용 고려
- PII: 리뷰 본문에 개인정보 포함 금지. 현재 캐시에는 평점/개수만 존재하여 민감도 낮음
13. 관측성(Observability)
- 로그: 수신/성공/실패 로그 표준화(태그, correlation id)
- 메트릭: 처리량, 실패율, 처리 지연, DLQ 규모, 재처리 성공률
- 트레이싱: 리뷰 생성 요청 → 이벤트 발행 → 컨슘 → 캐시 갱신까지의 분산 트레이싱 연계
14. 배포·롤백 전략
- 안전 배포: 컨슈머를 먼저 배포(스키마 호환), 이후 프로듀서 확장 변경 배포
- 롤백: 새로운 이벤트 필드만 소비하지 않도록 방어 코드, 미인식 필드는 무시(Forward compatibility)
- 재처리: 롤백 시 생긴 누락은 DLQ/원장 재생(Replay)으로 보정
15. 테스트 전략
- 유닛 테스트: 평균 계산, 신규 생성/갱신 분기, 예외 처리
- 통합 테스트: 임베디드 브로커(or Testcontainers RabbitMQ)로 이벤트 발행 → 컨슘 → 캐시 확인
- 회귀 테스트: ack/nack 설정 변경 시 "unknown delivery tag" 재발 방지 케이스 포함
16. 마이그레이션/레거시 정리
- 레이블 정정: 로그 문구 "[결제 완료 이벤트 수신]" → "[리뷰 생성 이벤트 수신]"
- Review 엔티티 필드 개선: reviewAvg만 보관하는 구조에서 sumOfRatings + count 구조로 전환 고려(정확한 삭제/수정 반영)
- Double → BigDecimal 전환 및 반올림 모드 표준화
17. 향후 과제(로드맵)
1) 멱등성 고도화
- 이벤트에 eventId 또는 reviewId 포함, bandroom-info에서 처리 이력 저장(예: Redis set, TTL)
- 정확히 한 번 처리처럼 보이게 구현(At-least-once 위에서의 논리적 exactly-once)
2) DLQ 및 운영 툴링
- review.created.queue에 DLX/DLQ 구성
- 실패 이벤트 알람 및 원클릭 재처리 UI/잡
3) 캐시 이중화
- Redis 레이어 추가, RDB와 동기화 전략 수립(Write-through/Write-behind 선택)
- 대규모 리스트 조회에서 초저지연 응답 확보
4) 평균 계산 정밀도/정책
- BigDecimal 및 RoundingMode, 표시 자리수 표준
- 삭제/수정 이벤트에 대한 역산을 위해 sumOfRatings + count로 스키마 변경
5) 스키마 버전화/계약 테스트
- 이벤트 스키마에 version 추가
- 계약 테스트로 소비자/프로듀서 호환성 보장
6) 트랜잭션 아웃박스 패턴 도입
- 리뷰 생성 DB 트랜잭션과 이벤트 발행의 원자성 보장
- Outbox 테이블 + Debezium/폴링 발행기
7) 컨슈머 성능 튜닝
- prefetch, concurrency, 배치 ack 평가
- 핫 파티션 완화 전략(라우팅 키 설계 확장)
8) 관측성 강화
- 처리 지연 SLA, 알람 기준 정의
- 분산 트레이싱 전 구간 연결
18. FAQ
Q1. 왜 bandroom-info에 캐시를 두나요?
- 조회 트래픽이 압도적으로 많고, 리뷰 본문과 별개로 평점/개수만 빠르게 제공하면 UX가 크게 개선됩니다. review 서비스에 매 조회마다 의존하면 지연/부하가 증가합니다.
Q2. 최종적 일관성에 따른 사용자 혼란은 없나요?
- 짧은 지연이 허용되는 UX로 설계하되, 리뷰 작성 직후 화면에서는 최신 값으로 보정해 혼선을 줄입니다. 또한 DLQ/재처리로 누락을 방지합니다.
Q3. 메시지 중복/유실은 어떻게 방지하나요?
- 멱등 키 저장으로 중복을 막고, DLQ와 재처리로 유실을 보정합니다. Ack 정책은 수동 ACK로 안전하게 관리합니다.
19. 부록: 현재 코드 스냅샷의 핵심 포인트
bandroominfo/config/RabbitMQConfig.java
- manualAckListenerContainerFactory: AcknowledgeMode.MANUAL, defaultRequeueRejected=false
- reviewCreatedExchange/Queue/Binding 선언
bandroominfo/consumer/rabbitMQ/ReviewEvent.java
- @RabbitListener(..., containerFactory = "manualAckListenerContainerFactory")
- 성공 ack, 실패 nack(requeue=false)
bandroominfo/service/ReviewService.java
- 신규 Review 생성 시 setReview 호출로 null 저장 방지
- 평균/개수 누적 수식 적용
bandroominfo/entity/Review.java
- @MapsId로 BandRoom PK 공유, 조인 비용 절감
20. 결론
리뷰 모듈과 bandroom-info 간의 메시징·캐싱 아키텍처는, 조회 성능을 극대화하면서 서비스 간 결합을 낮추는 균형 잡힌 선택이다. 운영 과정에서 드러난 ack 설정 오류와 엔티티 연결 누락 문제를 해결해 안정성을 높였고, 이제는 멱등성, DLQ, 정밀도, 관측성, 스키마 버전화 등의 체계를 강화할 단계다. 본 문서의 기획·설계·구현·운영 정리를 바탕으로, 점진적인 고도화를 통해 대규모 트래픽과 장애 시나리오에도 견고한 리뷰 경험을 제공할 수 있을 것이다.
'BindProject' 카테고리의 다른 글
| 유저 활동 데이터 수집 및 밴드룸(합주실) 방문자 통계 서비스 기획/분석 (11) | 2025.08.17 |
|---|---|
| 리뷰 정렬부터 이메일 미인증 유저 정리까지 (3) | 2025.08.17 |
| 지금까지 프로젝트 리팩토링 시즌 도입 (7) | 2025.08.12 |
| 메시징 플레이그라운드 만들기 (4) | 2025.08.12 |
| 구현된 모든 서비스를 컨테이너화 시키는 여정기 (7) | 2025.08.05 |