"락은 정말 필요했던 걸까?"
코드 리뷰 도중 백엔드 팀원은 한 마디가 시작이었다.
"이거... Redisson 분산락 쓰셨던데, 추천 수가 자주 바뀌고 조회도 많은 API잖아요? 락 계속 쓰면 오히려 병목 되지 않을까요?"
🔍 질문에서 시작된 새로운 고민
기존에 Redisson을 도입해 추천 기능에 분산락을 적용한 이유는 명확했다:
"추천은 사용자별 중복을 막아야 하고, 멀티 인스턴스에서도 정합성이 보장되어야 하니까."
그러나 새로 합류한 팀원의 질문은 단순히 도구 사용 여부가 아니라, 설계 자체의 적합성을 다시 돌아보게 만들었다.
“추천이면, 락보다 Redis에서만 처리하고 나중에 DB로 밀어넣는 방식이 더 좋지 않을까요?”
🧠 생각의 전환 – 도구보다 데이터 흐름이 먼저다
사실 처음엔 멀티 인스턴스 환경에서는 락이 정답처럼 느껴졌지만, 곱씹어볼수록 아래와 같은 생각이 들었다.
🔄 추천 기능은 어떤 흐름인가?
- 중복 추천 여부 확인 → 추천 수 증가 → 추천 기록 DB 저장
- 동시 요청이 많고, API 조회도 빈번
- 실시간 정합성은 필요하지만, 모든 요청마다 DB를 강제할 필요는 없다
❓ 질문에 담긴 핵심 고민
- 분산락은 트래픽이 많을수록 비용이 커지는 구조다
- Redis가 원자적 연산이 가능한데(싱글 스레드인데) 굳이 락을 써야 할까?
그래서 나는 실험을 먼저 해보기로 했다.
🔍 실험 – 정말 락이 병목인가?
기존 구조에서는 다음과 같이 Redisson으로 동시성 처리를 보장하고 있었다.
- Redis Hash로 추천수 증가
- 동시 추천 / 추천 중복 -> Redisson RLcok으로 보호
동시에 100명이 동시 추천하는 상황으로 한번 돌려보았다
ExecutorService executor = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(100);
for (Member m : members) {
executor.execute(() -> {
// 각 스레드마다 SecurityContext 설정 후
redisCountService.getCommonCount(ServiceType.RECOMMEND, DomainType.BOARD, boardId, null);
latch.countDown();
});
}
latch.await();
CommonCountDto result = redisCountService.getCommonCount(ServiceType.CHECK, DomainType.BOARD, boardId, null);
assertThat(result.getRecommendCount()).isEqualTo(100);
결과는 정합성은 보장되었고, 처리 시간은 1881ms가 나왔다. 내 예상보다 느렸다...
트래픽이 더 많으면 더 느릴 것으로 생각이 들었다.
그래서 나는 구조를 개선해서 한번 더 실험을 하기로했다.
🔁 구조 개선 – Redisson 제거, Redis Set 기반 제어로 변경
🔨 개선 방향
구분 | 변경 전 | 변경 후 |
추천 수 증가 | Redis Hash.increment() 사용 | ✅ 그대로 유지 |
추천 중복 체크 | DB에 직접 쿼리 & RLock 보호 | Redis의 Set으로 중복 추천 판단 |
추천 기록 저장 | DB에 insert (비관적락으로 보호) | DB에 insert, 단 DuplicateKeyException 무시 |
락 처리 방식 | Redisson tryLock() | ❌ 제거됨 |
여기서 DuplicateKeyException을 무시하는 이유는 다음과 같다.
이미 추천을 한 유저가 한 번 더 추천 요청을 보내면, 동일한 (user_id, post_id)로 PK 충돌이 발생한다.
이때 발생하는 예외가 바로 DuplicateKeyException이다.
그런데 왜 무시해도 괜찮을까?
이미 Redis Set에서 중복 여부 체크를 했기 때문이다.
Redis에서 이미 중복이면 추천을 막는다. 그런데 분산 환경이나 정말 드물게 race condition이 발생해 동시에 요청이 들어오면,
Set에서는 2개 다 허용된 것처럼 보일 수 있고, DB 저장 시 하나가 실패할 수 있다.
👉 즉, 이건 예상 가능한 중복 충돌 상황입니다. 그래서 나중에 분산 환경이 되거나 race condition이 발생해도 멀티 스레드 환경에선 자연스럽게 생길 수 있는 현상이다.
✍ 주요 코드 변경
add() → 0일 때 "이미 추천"
remove() → 0일 때 "아직 추천 안 함"
추천
// Redis Set은 중복을 허용하지 않음.
// 그래서 userId가 이미 Set안에 존재하면 add()는 아무것도 하지 않고 0을 반환한다.
// added == 0 -> userId가 이미 존재 (이미 추천함)
// added == null -> 비정상 상황(Redis 문제 등...)
// added == 1 -> userId가 처음 추가 됨 (추천 성공)
Long added = redisTemplate.opsForSet().add(recommendSetKey, userId);
if (added == null || added == 0) {
throw new PlayHiveException(ErrorCode.ALREADY_MEMBER_RECOMMEND);
}
redisTemplate.opsForHash().increment(redisKey, "recommend", 1);
추천 취소
// removed == 0 → 해당 유저가 이 게시물에 대해 추천한 적이 없으므로, 추천 취소는 허용되지 않음
// removed == null → Redis에서 응답이 아예 없거나 비정상
Long removed = redisTemplate.opsForSet().remove(recommendSetKey, userId);
if (removed == null || removed == 0) {
throw new PlayHiveException(ErrorCode.NOT_RECOMMENDED_YET);
}
🧪 Redisson 제거 후 테스트 결과
Redisson 제거후 100명의 서로 다른 유저가 동시에 추천을 시도했을 때,
처리시간이 733ms!! 무려 약 2.5배 줄었다!!
구조 | 추천 수 정합성 | 처리 시간 |
Redisson 적용 | ✅ 정확히 100 | ⏱ 1881ms |
Redisson 제거 | ✅ 정확히 100 | ⏱ 733ms |
정합성은 유지되었고, 약 61%의 성능 개선을 하였다.
락 경합 해소, Redis 자료구조 활용이 병목을 해결했다!
🧭 구조적 시야에서 본 설계 변화
질문 | 첫 리팩토링 | 두 번째 리팩토링 |
락이 필요한가? | Redisson으로 보장 | Redis 자체로 충분히 제어 |
중복 추천 제어? | DB 기반 + 락 | Redis Set으로 원자적 제어 |
조회량 대비 효율? | 락 경합 증가 | 락 제거로 병목 제거 |
🔄 다음 스텝 – 분산락 없는 구조의 안전장치
Redisson을 제거한 구조는 단일 인스턴스 + Redis 싱글 스레드 기반에서 이상적이다.
그러나 향후 다음과 같은 조건이 생긴다면 다시 락을 고려해야 할 수도 있다:
- 추천 수 증가 충돌이 실제로 발생하는 분산 환경
- Redis의 Set → DB flush 시점에서 race condition 발생
- 또는 특정 구간에 정밀한 트랜잭션 정합성이 요구되는 경우
✅ 회고 – 기술은 목적이 아니라 도구다
Redisson은 분명 강력한 도구지만, “도구에 맞춘 설계”가 아니라, “맥락에 맞는 도구 선택”이 더 중요했다.
이번 리팩토링을 통해 나는 아래와 같은 인사이트를 얻었다.
✨ 남긴 배움
- 락은 정합성 보장보다, "충돌이 실제로 발생하는가?"*를 먼저 따져야 한다
- Redis의 자료구조(Set, Hash)만 잘 활용해도 많은 문제를 해결할 수 있다
- 도구를 선택할 때는 “지금 필요한 수준인지”부터 고민해야 한다
- 팀원 간 코드 리뷰 질문이 기술 구조를 다시 점검할 기회가 될 수 있다
🔗 함께 보면 좋은 글
첫 리팩토링 글
2025.06.17 - [# Study/프로젝트] - [Playhive] Redis, Redisson 으로 조회수·댓글수·추천수 처리 성능 44% 향상시키기
'# Projects > Playhive' 카테고리의 다른 글
[PlayHive] Headless Chrome 크롤링 시 좀비 프로세스 정리 및 메모리 문제 해결 방법 (0) | 2025.06.18 |
---|---|
[PlayHive] S3 PresignedUrl 기반 파일 업로드/삭제 API 구현기 (with AWS & MinIO) (0) | 2025.06.18 |
[PlayHive] Redis, Redisson 으로 조회수·댓글수·추천수 처리 성능 44% 향상시키기 (0) | 2025.06.17 |
[PlayHive] 댓글/대댓글 구조 리팩토링하기 (1) | 2025.06.11 |