어느날, 내게 조용히 찾아온 그대. 커넥션 풀

퇴근길 지하철 안에서 고객사로부터 연락을 받고, 나는 고민에 빠졌다.
그들이 전한 내용은,
“그룹웨어 시스템에서 회원 데이터를 조회하는 과정에서 문제가 발생하고 있다.” 는 것이었다.

이 문제는 간헐적으로 발생하여 즉각적인 해결이 필수적이지 않아 보였으나,
원인을 파악하면 좋겠다는 내용이였다.

사실, 오류가 일관되게 발생한다면 문제 해결 방향을 설정하기가 비교적 수월하므로 부담이 적지만,
이처럼 간헐적으로 발생하는 문제는 마치 까다로운 퍼즐을 푸는 것과 유사하다.

이러한 도전 또한 유익한 경험으로 간주하여 긍정적인 태도로 임하게 되었다.

먼저, 커넥션 풀에 대한 이해를 돕기 위해, 주요 개념을 간략히 요약하여 설명한다.

  1. JDBC(Java Database Connectivity): Java 애플리케이션에서 데이터베이스를 사용할 수 있게 하는 API로, Connection(연결), 명령 실행(Statement), 결과 집합 처리(ResultSet)의 세 가지 주요 기능을 표준 인터페이스로 제공한다.
  2. Connection Pool: 데이터베이스 연결 객체(Connection) 객체를 사전에 생성하고 필요할 때 재사용함으로써, 연결 과정에서 발생할 수 있는 오버헤드를 줄이는 기술이다.
  • HikariCP: 고성능을 목표로 하는 JDBC Connection Pool의 일종으로, Spring Boot 2.x 이상에서 기본적으로 사용되며, 오버헤드를 최소화하는데 초점을 맞춘 커넥션 풀이다.

자세한 정보는 Connection-Pool 문서에서 확인할 수 있다.
이 개념을 기반으로, 향후 논의에서 자주 언급될 용어들을 정의한다.

그룹웨어 시스템을 GW, 청년인턴 포털을 OD로 칭하기로 하였다.
이 두 시스템의 구조는 다음과 같이 도식화하였다.

구성도

GW는 티맥스의 티베로를 사용하며 OD는 오로라(AWS-RDS)를 사용한다.
GW의 회원 정보는 OD에 VIEW 형태로 제공된다.

HikariCP DataSource는 그룹웨어의 DBMS URL로 연결하고,
50분마다 배치 작업을 통해 VIEW 데이터를 OD의 회원 테이블에 동기화한다.

간단한 설명을 끝으로 실제 운영 중 발생 하였던 로그 분석 결과,
특정 트랜잭션에서 반복적으로 발생하는 에러 메시지는 다음과 같았다.

1
2
Connection is not available, request timed out after 30000ms.
...

이 메시지는 트랜잭션 생성 실패, 유휴 커넥션 부재 및 HandOffQueue 대기 커넥션 없음으로 인해 요청 시간이 만료되어 예외 처리된 상황을 설명한다.

이는 교착상태(DeadLock) 를 의미한다.
HikariCP의 처리 과정을 자세히 들여다보자.

---
title: HikariCP 커넥션 처리 흐름도
---
    %%GET["hikari.getConnection()"] ---> BORROW["hikari.concurrentBag.borrow()"]
    %%BORROW ---> CONDITION{Thread의 이전 Connection 정보가 있는가?}
    %%CONDITION ---> |Yes| CONDITION2{이전에 사용했던 Connection List 중 현재 사용 가능한 Connection이 있는가?}
    %%CONDITION2 ---> |Yes| FINAL[return Connection]
    %%CONDITION3{Pool 전체 Connection 중 현재 사용 가능한 상태의 Connection이 있는가?} ---> |Yes| FINAL
    %%CONDITION2 ---> |No| CONDITION3
    %%CONDITION3 ---> |No| CONDITION4{HandOfQueue에 사용 가능한 Connection이 있는가?} ---> |Yes| FINAL
    %%CONDITION4 ---> |No| CONDITION5{connectionTimeOut이 지났는가?} ---> |Yes| RETURN[return null]
    %%CONDITION5 ---> |No| CONDITION4
    stateDiagram
        direction LR
        
        accTitle: This is the accessible title
        accDescr: This is an accessible description
        
        classDef notMoving fill:white
        classDef movement font-style:italic
        classDef badBadEvent fill:#f00,color:white,font-weight:bold,stroke-width:2px,stroke:yellow
        
        [*]--> Still
        Still --> [*]
        Still --> Moving
        Moving --> Still
        Moving --> Crash
        Crash --> [*]
        
        class Still notMoving
        class Moving, Crash movement
        class Crash badBadEvent
        class end badBadEvent
        
        state "1. Thread는 Hikari에게 커넥션을 요청한다.
               2. Hikari는 Thread의 커넥션 사용내역을 확인한다.
               3. Thread의 사용내역에 조건에 만족하는 커넥션 객체가 없다면 다른 커넥션을 요청한다.
               4. Hikari는 커넥션 풀 전체에서 사용가능한(유휴) 커넥션을 확인한다.
               5. 모든 커넥션이 사용중이라면, Thread에게 HandOfQueue의 커넥션 반환을 대기하라고 통보한다.
               6. Thread는 대기시간 30초(Default) 이후 커넥션을 얻지 못했다면 Exception 을 발생하고 종료한다."
              as s2

원인을 분석한 결과, 다음 단계를 통해 문제를 해결하려고 시도하였다.

  1. 문제의 근원 파악: ODGW 중 어디에서 문제가 발생하는지 파악하는 것이 중요하였다. GW 운영 담당자에게 해당 이슈를 공유하였으나, 예상치 못한 방어적인 태도로 답변을 받았다.
    이로 인해, 상황을 더욱 신중하게 분석할 필요가 있었다.
  2. 오류 발생 시점 파악: 오류가 발생하는 정확한 시점을 파악하기 위해, 회원 정보 조회 과정에서 사용되는 Tibero DataSource와 관련된 VIEW 테이블의 사용 패턴을 면밀히 검토하였다.
    이를 통해 GW의 커넥션을 생성하는 과정에서 문제가 발생한다는 것을 확인했다. 이는 OD에서 발생하는 이벤트가 GW의 트랜잭션을 통해 오류를 유발한다는 가설을 세우게 되었다.
  3. 커넥션 풀 조정 고려: 이상적으로, 커넥션 풀의 조정을 통해 문제를 해결할 수 있다면 그것이 가장 바람직한 해결책이었다. 그러나 GW 담당자는 현재 커넥션 풀에 충분한 여유가 있다고 언급하였고, 이는 요청 시간 만료의 원인이 커넥션 풀의 부족이 아니라는 점을 시사하였다.
    따라서 문제의 원인을 다른 측면에서 찾아야 할 필요가 있었다.
  4. OD측의 문제 검토: OD측의 해당 상황은 매우 이례적이며, 추가적인 조사가 필요함을 의미하였다.

특이사항으로, 웹 어플리케이션 서버를 재시작하면 문제가 일시적으로 해결되는 현상이 관찰되었다.
이는 재시작 과정에서 쓰레드와 커넥션이 초기화되어 일시적으로 문제가 해소되는 것처럼 보이지만,
근본적인 해결책은 아니다.

따라서 OD WAS에서 GW로의 커넥션 요청 패턴을 재검토하고,
필요한 경우 여러 쓰레드에서 다수의 커넥션을 효율적으로 관리할 수 있는 방안을 모색해야 한다.

이러한 복합적인 문제 해결 과정을 통해,
우리는 시스템 간의 상호작용이 복잡하게 얽혀 있음을 인식하게 되었다.

이 문제를 해결하기 위해서는 다양한 시스템 구성 요소와 상호 의존성을 면밀히 이해하고,
시스템 간의 통신 과정에서 발생할 수 있는 잠재적인 문제점들을 사전에 식별하고 대비하는 것이 중요하다.

또한, 이러한 문제에 대응하기 위해 다학제적인 접근 방식과 팀 간의 긴밀한 협력이 필수이다.

아쉬운점

결국, 원인 파악을 분석하던 과정 중 명확한 해결방안을 얻진 못했다.
이를 계기로 커넥션 누수(leak)을 방지하는 중요한 과제를 얻게 되었다.
분명, 자원 반환을 방해하는 곳이 있을 것이다. (로그인?, 로그아웃?)

커넥션 누수(Leak)에 대한 대비

  1. leakDetectionThreshold 임계값 설정
  2. Prometheus, Grafana와 같은 도구를 사용하여 커넥션 풀 매트릭스 시각화
  3. Spring Boot Actuator 서비스 활성화

참고: HikariCP DBCP 최소 사이즈 공식

$T_n$ 은 WAS의 전체 쓰레드 개수,
$C_m$ 은 쓰레드가 작업을 수행하기 위해 동시에 필요한 Connection의 갯수이다.
$$T_n * \left( C_m - 1 \right) + 1$$