트러블슈팅

REQUIRES_NEW로 인한 DB 커넥션 리소스 고갈

용쓰개 2025. 1. 23. 01:27

현재 getOrCreateUser(소셜 로그인) 메서드는 DB 커넥션 두 개가 필요하다.

@Transactional
getOrCreateUser ( ) {

    1. id token 검증 - 공개키 생성 및 조회 

    2. user 조회
		가입된 유저 -> 조회 결과 반환 
        미가입 유저 -> createUser 메서드 호출 ( REQUIRES_NEW )

    3. access token 발급

}

 

max-thread-pool = 10인 상태에서, getOrCreateUser 메서드를 실행하면 10개의 스레드가 커넥션을 하나씩 점유하게 된다. 이때 모든 요청이 미가입된 유저였다면, 모든 스레드가 REQUIRES_NEW 메서드들 실행해야한다. 이렇게 되면, 모두가 서로가 가진 DB 커넥션을 기다리지만 획득할 수 있는 idle 상태의 커넥션이 없어서 리소스 고갈 문제가 발생한다.

더보기

확인해보자. max-thread-pool <= 20일 때, 다음 테스트는 DB 커넥션 타임 아웃으로 실패하게 된다. 

        int NUMBER_OF_THREADS = 20;
        ExecutorService executorService = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
        
        CountDownLatch startLatch = new CountDownLatch(1);
        CountDownLatch doneLatch = new CountDownLatch(NUMBER_OF_THREADS);
        
        for (int i = 0; i < NUMBER_OF_THREADS; i++) {
            executorService.submit(() -> {
                try {
                    startLatch.await();
                    authService.getOrCreateUser(requestDto); // 미가입 유저라고 가정
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    doneLatch.countDown();
                }
            });
        }
        
        startLatch.countDown(); // 20개 스레드가 authService.getOrCreateUser(requestDto) 실행 시작
        doneLatch.await(); // doneLatch count가 0이 될 때까지 대기

max-thread-pool = 21부터는 테스트가 통과한다.

spring :
  datasource:
    hikari:
      maximum-pool-size: 21

 

 

 

해결

불필요한 Transaction을 제거했다.

// @Transactional 제거
getOrCreateUser ( ) {

    1. id token 검증 - 공개키 생성 및 조회 
    
    2. user 조회
		가입된 유저 -> 조회 결과 반환 
        미가입 유저 -> createUser 메서드 호출 (  REQUIES_NEW -> REQUIRED로 변경 )

    3. access token 발급

}

 

 

여담

원래 getOrCreateUser 메서드와 createUser 메서드는 같은 트랜잭션 소속이었다.

모종의 이유로 코드 리팩토링 과정에서 createUser 메서드에 REQUIRES_NEW를 사용하여 트랜잭션을 분리하게 되었고, 테스트 코드 작성 중 커넥션 고갈 문제를 인지했다. 

 

이 문제를 해결하는 방법으로 커넥션 풀 사이즈를 조정하는 방법을 고민하다가, 뒤늦게 getOrCreateUser 메서드의  @Transactional의 필요성에 대해 생각하게 되었다.

 

✔️ 트래픽 기반 적절한 커넥션 풀 사이즈를 설정하는 방법

  • 실제 상용 서비스였다면 모니터링 데이터를 기반으로 최적의 커넥션 풀 크기를 조정할 수 있었겠다. 그러나 실제 서비스가 아니라서, 가능하면 다른 방법을 시도해보고 싶었다.

✔️ 트랜잭션을 분리하지 않는 방법

  • createUser 메서드는 닉네임 중복 시 예외를 던진다. 공개키를 생성하고 조회하는 작업의 비용은 비싼데, 닉네임을 재생성할 때마다 해당 작업을 반복하는 상황은 최대한 피하고 싶었다.

 

✔️minimum idle size= max-connection-pool size + 1 로 설정하는 방법

  • 성능 상의 이유로 권장되지 않는 방법이다.

 

 

구글링을 해보면 이와 관련된 문제를 다루는 포스트들이 꽤 보인다. REQUIRES_NEW의 사용은 신중해야겠다...

https://medium.com/@taesulee93/spring-transaction-requires-new-propagation-%EC%A7%80%EC%98%A5-with-mybatis-local-session-cache-cf71415889c8

 

Spring Transaction REQUIRES_NEW Propagation 지옥 (with Mybatis Local session cache)

무분별하게 설정 된 REQUIRES_NEW Propagation으로 인한 Connection Deadlock 현상

medium.com

https://techblog.woowahan.com/2663/

 

HikariCP Dead lock에서 벗어나기 (실전편) | 우아한형제들 기술블로그

1부 HikariCP Dead lock에서 벗어나기 (이론편)은 잘 보셨나요? 2부 HikariCP Dead lock에서 벗어나기 (실전편)에서는 실제 장애 사례를 기반으로 장애 원인을 설명하고 해결 사례를 공유하고자 합니다. 그

techblog.woowahan.com