📌 프로젝트 다시 보기
내가 만들었던 미간지 프로젝트를 돌아보면서 개선할 부분이 없을까 하고 코드를 다시 들여다봤다.
그러다 발견한 문제:
멀티스레드 상황에서 게시물 조회 시 조회수가 정확히 증가하지 않음
🚨 동시성 문제 상황
📉 동시 요청이 들어오면 조회수가 꼬인다.
시간 | 스레드 1 | 스레드 2 |
T1 | findById → viewCount = 10 | |
T2 | findById → viewCount = 10 | |
T3 | viewCount++ → 11 | |
T4 | viewCount++ → 11 | |
T5 | save(viewCount = 11) | |
T6 | save(viewCount = 11) |
기대값은 12여야 하지만 실제로는 11
동시 요청이 많아지면 손실도 커진다
이런 문제가 재고 관리 시스템에서 일어난다면?
=> 데이터 정합성 완전 박살남.
🔧 개선 전: 단순 조회수 증가 구조
// 단순한 조회수 증가 로직
public void increasePostViewCount(Long id) {
UserPost userPost = boardRepository.findById(id).orElseThrow();
userPost.increase();
boardRepository.save(userPost);
}
Controller에서는 게시글을 조회하고, 바로 조회수를 증가시킨다.
@GetMapping("/{postId}")
public DetailPageDto getPost(@PathVariable Long postId) {
UserPost userPost = userPostService.getUserPost(postId);
userPostService.increasePostViewCount(postId);
...
}
문제는 여기서 동시에 여러 스레드가 접근하면 증가가 꼬인다는 점.
✅ 해결책: Redisson 분산 락 + AOP
📎 먼저 AOP로 어노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
String key();
}
🔄 AOP 적용 코드
@Around("@annotation(preventDuplicateSubmit)")
public Object around(ProceedingJoinPoint joinPoint, PreventDuplicateSubmit preventDuplicateSubmit) throws Throwable {
String key = generateKey(joinPoint, preventDuplicateSubmit);
RLock lock = redissonClient.getLock(key);
boolean isLocked = lock.tryLock(2100, 100, TimeUnit.MILLISECONDS); // 2.1초 기다림, 락 유지 0.1초
if (!isLocked) throw new RuntimeException("중복 요청이 감지되었습니다.");
try {
return joinPoint.proceed();
} finally {
lock.unlock();
}
}
tryLock(대기시간, 락 유지시간)을 적절히 설정해줘야 함
🔐 개선된 조회수 증가 함수
@PreventDuplicateSubmit(key = "#postId")
public void redissonIncreasePostViewCount(Long postId) {
UserPost userPost = boardRepository.findById(postId).orElseThrow();
userPost.increase();
boardRepository.save(userPost);
}
🧪 테스트 코드
❌ 기존 방식 테스트
@Test
@DisplayName("동시에 100명의 게시물 조회 : 동시성 이슈")
public void badViewTest() throws Exception {
request100Posts((_no) -> userPostService.increasePostViewCount(1L));
}
- 100개의 요청이 동시에 들어왔는데
- viewCount가 100만큼 증가하지 않음
- 예상된 문제 그대로 발생
✅ 분산 락 적용 후 테스트
@Test
@DisplayName("동시 100명 게시물 조회 : 분산락")
public void redissonViewingTest() throws Exception {
request100Posts((_no) -> userPostService.redissonIncreasePostViewCount(1L));
}
✅ 정확하게 조회수 100 증가
분산락 적용 성공!
🔍 분산 락 (tryLock) 설정 비교 실험
tryLock(대기시간, 유지시간) 값을 조절하며 테스트해봤다.
❗ 짧은 대기시간 → 실패 확률 높음
boolean isLocked = lock.tryLock(500, 500, TimeUnit.MILLISECONDS);

- 조회수 증가 안 된 요청 약 20~25%
- 락을 못 잡고 스킵됨
❗ 유지시간 짧게 설정했을 때
lock.tryLock(500, 100, TimeUnit.MILLISECONDS);

- 유지시간 줄였다고 성공률이 올라가는 것도 아님
- 오히려 성공률 더 떨어짐 (조회수 49)
✅ 대기시간 길게 설정하면 안정성 향상
lock.tryLock(2000, 100, TimeUnit.MILLISECONDS);

- 대기시간을 넉넉하게 주면 대부분 락 획득 가능
- 테스트 시간은 2.1초 정도 소요
🧠 분산 락 사용시 고려할 점
- Redisson 락을 사용하면 중복이 발생하면 안 되는 작업(조회수, 좋아요 등)에 효과적
- 하지만 tryLock 설정을 꼭 적절히 튜닝해야 함:
- 대기시간 짧으면 실패율 올라감
- 유지시간 짧으면 락이 쉽게 풀려서 실패 증가
💡 분산 락으로 배운 핵심 포인트
분산 락 적용보다 더 중요한 건 락 설정(tryLock) 튜닝
조회수 증가뿐만 아니라 댓글, 좋아요 등 다양한 기능에서 AOP로 확장 가능
단순 조회수 증가 기능도 동시성 이슈가 발생할 수 있다는 걸 이번에 제대로 체감함
'스프링부트' 카테고리의 다른 글
[스프링 부트] keycloak 사용 SSO(OIDC) 인증 서버 간단하게 구축해보기 (1) | 2024.11.25 |
---|---|
[Spring Boot] 이미지 업로드 비동기 처리로 글쓰기 속도 30초 → 1초 개선하기 (0) | 2024.06.19 |
[스프링] 프로젝트 N+1 해결하기 (fetch join (0) | 2023.09.01 |
[Spring boot] 스프링 부트 에러페이지 커스터마이징하기 (0) | 2022.04.06 |