스프링부트

스프링 부트 Redis 분산 락 활용 동시성 제어

컴공코딩러 2024. 7. 4. 16:32

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

📌 프로젝트 다시 보기

내가 만들었던 미간지 프로젝트를 돌아보면서 개선할 부분이 없을까 하고 코드를 다시 들여다봤다.

그러다 발견한 문제:

멀티스레드 상황에서 게시물 조회 시 조회수가 정확히 증가하지 않음

 


🚨 동시성 문제 상황

📉 동시 요청이 들어오면 조회수가 꼬인다.


시간 스레드 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로 확장 가능
단순 조회수 증가 기능도 동시성 이슈가 발생할 수 있다는 걸 이번에 제대로 체감함