-
Notifications
You must be signed in to change notification settings - Fork 0
Feat: 요청글 조회수 관리 v1 #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat: 요청글 조회수 관리 v1 #178
Conversation
모델로 상태를 변경해도, 어차피 JPA 엔티티와 분리되어있기에 영속성 컨텍스트로 관리되지 않아 의미없음
@async 프록시 쓰려고 self-reference 했었는데, 순환 참조 문제가 생기니 비동기 서비스를 따로 분리함
|
|
아 그리고 스레드 풀 성능 튜닝에 대해서 톰캣 스레드풀이랑 우리가 사용하는 스레드 풀이랑 동작 과정이 좀 다릅니다. [톰캣] 작업이 밀려옴 -> min-spare 스레드로 처리 -> 감당 불가시 바로 스레드 생성해서 max-size 까지 스레드를 늘림 -> 이래도 감당 불가시 큐에 작업쌓임 -> 감당 불가시 연결 거부 [커스텀 스레드풀] 작업이 밀려옴 -> min 스레드로 처리 -> 감당 불가시 큐에 작업 쌓임 -> 큐 사이즈 초과 시 max 까지 스레드 늘어남 -> 감당 불가시 거절 정책에 따라 처리 |
아, 이 녀석 안그래도 이번에 |




close #91
☑️ 완료 태스크
PR 리뷰 요청글 상세 조회API에 조회수 증가 로직 추가🔎 PR 내용
조회수 관리는 고려할 부분이 많아서 8월부터 계속 고민을 하고 있었는데요.
이제야 최적의 방식을 찾은 것 같습니다 (지금 뇌 수준에서는)
최적의 방식을 바로 구현하는 것보단 단계별로(v1~v4) 구현하면서 지표로 비교하는 게 좋겠다 판단했습니다.
요청글 조회 API는 조회(Query)와 조회수 증가(Command) 결합되어 있고,중복 방지, 동시성, DB 부하, 분산 환경까지 같이 고려해야 하기에 좀 까다로웠습니다.
그래서 아래 조건들을 만족하면서 점점 개선해나가는 방식으로 차례로 구현해보겠습니다.
1️⃣ v1:
회원-요청글브릿지 테이블 기반 조회수 기록v1은 가장 단순한 방식으로, 회원-요청글 브릿지 테이블(pr_review_hits)에 기록해 중복 증가를 막는 겁니다.
아주 단순한 구조라 문제도 많긴 하지만 우선 장단점을 생각해보죠.
장점
단점
SELECT -> INSERT/UPDATE가 발생해 DB에 부하가 커짐중복 판단 방식 (v1에서 선택)
브릿지 테이블을 두는 방식에서도 중복 판단은 여러 방법이 있죠.
👉 마지막 증가 시각(last_increased_at)을 저장하고, 현재와 비교해서 TTL(1시간) 내 중복 증가를 막는 2번을 선택했습니다.
v1 최종 ERD
이렇게 조회수 테이블을 두고는 있지만 중복 증가 방지 목적이 큽니다.
특히 "요청글별 조회수 수치"는 조회/정렬에서 자주 사용되고 있기 때문에,
브릿지 테이블에 매번 COUNT를 하지 않고 요청글 엔티티에 hitCount를 비정규화해서 구현했습니다.
2줄 요약
👉
마지막 조회수 증가 시각을 가진 조회수 브릿지 테이블로 중복 증가를 방지하고,요청글의 hitCount는 별도 컬럼으로 비정규화해서 증가 처리하는 방식으로 v1을 구현했다.
넘어가기 전에 동시성 문제 처리와 조회/조회수 증가 비동기 분리 작업 설명하고 갈게요.
🛠️ 조회수 증가 시각 기록 시 동시성 문제 해결
INSERT IGNORE INTO조회수 중복 증가 방지를 위해 (pr_review_id, user_id) 조합으로 복합 유니크 제약 조건을 뒀습니다.
하지만 단순히 SELECT → 없으면 INSERT 같은 흐름으로 구현할 경우엔 동시성 문제가 발생할 수 있습니다.
두 요청이 거의 동시에 기록이 없다고 판단해 INSERT를 시도하면
중복 row를 만드려고 하거나 DuplicateKeyException가 터지게 될 겁니다.
특히 이 문제는 아직 테이블에 존재하지 않는 row에 대한 경쟁이라
SELECT … FOR UPDATE같은 Exclusive lock을 선점하는 것도 불가능합니다.(존재하지 않는 row라서 잠글 대상 자체가 없기 때문이죠 💁♂️)
그래서 이 문제를 애플리케이션 레벨에서 재시도하거나 lock으로 해결하기보다는
앞에서 만든 (pr_review_id, user_id) UNIQUE 인덱스를 활용했습니다.
INSERT IGNORE INTO로 아직 row가 없는 첫 조회 요청이라면 INSERT가 정상적으로 수행하고,동시에 들어오거나 이미 row가 존재하면 UNIQUE 충돌이 발생하긴 해도 에러 없이 무시합니다.
동시에 들어온 요청이니
lastIncreasedAt의 시간도 의미 상 크게 문제되지 않죠.즉, 동시에 여러 INSERT 요청이 들어와도 DB 차원에서 하나의 row만 생성한다는 걸 보장하니까,
애플리케이션 레벨에서 race condition을 처리해야 할 필요가 없게 된 겁니다.
🛠️ 요청글 내용 조회와 조회수 증가 작업을 비동기로 분리
@Async요청글 조회 API는 조회(Query)와 조회수 증가(Command) 두 가지 성격의 작업이 섞여있습니다.이 두 작업을 한 트랜잭션이나 동기로 묶어버리면,
조회수 처리 로직때문에 조회 API의 성능에 직접적인 영향을 주게 되는 거죠.
해결 방법: 조회수 증가 작업은 비동기 분리
실시간성이 중요하긴 하지 조회수 증가는 응답 결과에 즉시 반영될 필요는 없기 때문에,
상세 조회 로직과 분리하고
@Async를 활용해 조회수 증가를 비동기로 처리하도록 구현했습니다.코드를 보면 AsyncService를 따로 만든 것을 확인할 수 있는데요.
처음에는 같은 Service 내부에 @async 메서드를 정의했지만,
Spring의
@Async는 프록시 기반으로 동작한다는 걸 알게됐습니다.동일 클래스 내부 호출은 프록시를 거치지 않아서 비동기가 아니라 동기 호출로 실행되는 거죠.
그래서 내부 호출 대신 self-reference 방식으로 해야 하나 했었는데,
순환 참조가 일어나는 구조를 만들기보다는 AsyncSerivce로 따로 클래스를 빼는 구조로 구성했습니다.
사실 CQRS 얘기해놓고
findOrCreate()가 Query/Command 역할을 동시에 수행하는 점에서엄밀하게 따지면 CQRS 규칙은 어긴 구조입니다.
다만 존재하지 않는 row에 락을 걸 수 없는 상황이었기 때문에,
DB 차원의 유니크 제약으로 동시성 문제를 해결하기 위해 실용적으로 접근하기로 했습니다.
작업 요약
INSERT IGNORE INTO로 해결🪄 앞으로의 리팩토링 계획
아래는 앞으로 개선해 나갈 작업 계획들입니다.
2️⃣ v2: Redis에 조회를 SET으로 기록해 중복 증가 방지
요청글별로 Redis SET(pr_review_id, user_id)에 유저를 기록해 중복 조회를 판단하는 겁니다.
장점
단점
3️⃣ v3: Redis에 조회 SET에 기록 중복 증가 방지 + 누적 조회수 저장/배치로 일괄 UPDATE
그대로 중복 증가 판단을 Redis SET으로 하는 것까진 v2랑 같은데,
조회수 누적을 Redis에서 하다가 일정 주기로 RDB에 배치로 일괄 반영하는 아이디어입니다.
배치로 일괄 UPDATE 하는 만큼, 여기서는 TTL 기준을 고정 시간 단위(ex. 1시간)로 잡아야 합니다.
장점
단점
4️⃣ v4: Redis hyperloglog 중복 증가 방지 + 누적 조회수 저장/배치로 일괄 UPDATE
v3에서 중복 증가 기록을 위해 쓰던 SET 대신 HyperLogLog로 근사 집계하는 아이디어입니다.
hyperloglog.. 이걸 알고 나니 그동안의 모든 근심과 스트레스, 걱정이 없어지고 건강해졌습니다 👍
이런 게 있었다니, 살~짝 눈물 흘릴 뻔 했습니다.
장점
-> 고트래픽 환경에서도 안정적인 조회수 처리
단점
-> 우리 서비스의 PR리뷰요청글 조회수에 이 정도 오차는 충분히 감수할 만함!
📷 성능 스크린샷
성능 측정은 알아보고 있는데 어떻게 하는지 조사하고 결과 후첨할게요.