CannotGetJdbcConnectionException 에러 해결

2026. 1. 19. 13:19·TroubleShooting

1. 문제 상황

지난 포스팅에 이어 이번에도 운영 환경 모니터링 중 발생했던 에러와 해결과정을 다룰 예정이다. 이번엔 사용 중인 postgresSQL에 접근하는 쿼리 실행 중에 다음과 같은 에러가 간혈적으로 발생하였다. 

org.apache.ibatis.exceptions.exceptions.PersistenceException:
Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection;
nested exception is org.apache.commons.dbcp.SQLNestedException:
Cannot get a connection, pool error Timeout waiting for idle object

 

에러 로그를 분석한 결과, 데이터베이스 커넥션 풀에서 가용한 커넥션을 얻지 못해 타임아웃이 발생한 상황이라는 것을 확인할 수 있었다.

 


 

2. 해결 과정

2.1. 에러 원인 분석

본격적인 문제 해결에 앞서 현재 상황을 조금 더 명확히 파악하고자 어플리케이션의 커넥션 풀 설정과 DB의 연결 상태를 확인하기로 한다. 

 

2.1.1. 커넥션 풀 설정 확인 

먼저 기존의 커넥션 풀 설정을 살펴보기로 한다. 애플리케이션에서 사용되고 있는 DBCP(DataBase Connection Pool) 라이브러리는 Apache의 Commons DBCP 1.4 버전이며, 적용 중인 설정은 다음과 같았다.

<property name="maxActive" value="5" />
<property name="maxIdle" value="5" />
<property name="minIdle" value="5" />
<property name="maxWait" value="1000" /> <!-- 1초 -->

<property name="validationQuery" value="select 1" />
<property name="testWhileIdle" value="true" />
<property name="timeBetweenEvictionRunsMillis" value="7200000" /> <!-- 2시간 -->
  • maxActive: 최대 활성 커넥션 수
  • maxIdle: 최대 유휴 커넥션 수
  • minIdle: 최소 유휴 커넥션 수
  • maxWait: 풀에 가용한 커넥션이 없을 때, 몇 ms 동안 기다릴지 결정하는 대기 시간
  • validationQuery: 커넥션이 유효한지 체크하기 위한 검증용 쿼리 
  • testWhileIdle: 유휴 커넥션 검사 여부
  • timeBetweenEvictionRunsMillis: 유휴 커넥션 점검 주기

 

Apache Commons DBCP의 configuration은 다음의 링크에서 상세하게 확인할 수 있다. (버전 주의)

https://commons.apache.org/proper/commons-dbcp/configuration.html

 

2.1.2.  DB  연결 상태 확인

pg_stat_activity은 PostgreSQL의 시스템 카탈로그 뷰로 현재 DB 서버의 모든 활성 연결 정보를 확인할 수 있다.

SELECT
    pid,                              -- 프로세스 ID
    now() - query_start as duration,  -- 쿼리 실행 시간 (현재시각 - 시작시각)
    state,                            -- 연결 상태
    query                             -- 실행 중인 쿼리문
FROM 
    pg_stat_activity                 
ORDER BY
    query_start DESC

 

여기서 state는 다음의 상태값을 가질 수 있으며, idle을 제외한 상태는 어플리케이션 상에서 해당 커넥션을 사용 중인 것으로 간주하므로 다른 요청에서 재사용할 수 없다.

  • active - 쿼리 실행 중
  • idle - 쿼리 대기 중 (유휴 상태)
  • idle in transaction - 트랜잭션은 열려있지만 쿼리 실행 안 함
  • idle in transaction (aborted) - 트랜잭션 에러 후 대기 중

해당 쿼리를 통해 WAS 컨테이너와 맺어진 연결들의 state가 모두 idle in transaction나 active인 경우, 어플리케이션에서 사용할 수 있는 idle connection이 없으므로 CannotGetJdbcConnectionException 에러가 발생하는 것을 확인할 수 있었다.

 

2.1.3.  해결 방안

해당 에러의 일반적인 원인은 다음과 같이 정리할 수 있다. 

  • 커넥션 누수(Connection Leak): 사용한 커넥션을 제대로 반환하지 않아 풀이 고갈됨
  • 커넥션 풀 크기 부족: 동시 요청 대비 풀 크기가 너무 작음
  • 느린 쿼리: 오래 걸리는 쿼리가 커넥션을 장시간 점유
  • DB 서버 문제: DB 자체가 응답하지 않거나 최대 연결 수 초과

해당 에러의 전후로 특별히 오래 걸리는 쿼리가 있지 않았고, 웹 컨테이너 재시작 시 해당 에러가 일시적으로 사라졌으므로, 커넥션 누수와 커넥션 풀 설정을 중점으로 해결 방안을 찾아보기로 한다.

 

2.2. 커넥션 누수 확인

2.2.1. removeAbandoned 설정

기존의 설정으로는 애플리케이션에서 실제로 커넥션 누수가 발생하는지 확인하기 어려웠기 때문에 관련 설정을 찾게 되었고, 검색을 통해 다음과 같이 누수된 커넥션을 강제로 회수하고 로깅하는 설정을 찾을 수 있었다. 

<property name="removeAbandoned" value="true"/> 
<property name="removeAbandonedTimeout" value="600"/> <!-- 10분 -->
<property name="logAbandoned" value="true"/>
  • removeAbandoned: 회수되지 않고 방치된 Connection을 DBCP가 자동으로 회수
  • removeAbandonedTimeout: Connection을 빌린 후 일정 시간 동안 반환 안 하면 버려진 것으로 간주
  • logAbandoned: 방치된 Connection을 회수할 때 로그를 남기는 옵션

 

해당 설정을 적용하면 커넥션을 빌린 시점부터 10분 이상 풀에 반환하지 않은 커넥션을 방치된(abandoned) 것으로 판단하여 강제로 회수한다. 회수된 커넥션은 close() 되어 폐기된다. 그리고 logAbandoned 설정에 따라 커넥션 누수(Leak)가 발생했을 때 해당 커넥션을 생성했던 시점의 애플리케이션 소스 코드 위치를 Stack Trace 형태로 확인할 수 있어 원인 파악에 용이하다.

 

2.2.1. 추가 로깅 설정

추가로, 컨넥션 관리가 잘되고 있는지 확인하기 위한 로그 설정을 해주었다.

<Logger name="org.mybatis.spring.SqlSessionUtils" level="DEBUG" additivity="false">
	<AppenderRef ref="console"/>
</Logger>

해당 로그 설정을 통해 다음과 같이 Mybatis가 DB 커넥션을 어떻게 관리하는지 확인할 수 있다. 

  • 커넥션 획득: "Creating a new SqlSession", "Registering transaction synchronization"
    • 새로운 SqlSession(DB 연결 세션)을 생성하거나 기존 트랜잭션에 참여하는 과정을 보여줌
  • 커넥션 재사용: "Fetched SqlSession from current transaction"
    • 이미 열려 있는 트랜잭션 내에서 동일한 커넥션을 다시 사용할 때 출력됨
  • 커넥션 반납/종료: "Closing non-transactional SqlSession", "Releasing transactional SqlSession"
    • 쿼리 실행이 끝나고 커넥션을 다시 커넥션 풀(DBCP)로 돌려보내는 시점을 알려줌

 

<Logger name="org.springframework.jdbc" level="DEBUG" additivity="false">
	<AppenderRef ref="console"/>
</Logger>

해당 로그 설정을 통해 다음과 같이 Spring의 트랜잭션과 JDBC 커넥션 관리 과정을 확인할 수 있다.

  • 트랜잭션 시작/종료: "Creating new transaction", "Initiating transaction commit", "Initiating transaction rollback"
    • @Transactional이 적용된 메서드가 시작될 때 트랜잭션이 생성되고, 정상 종료 시 commit, 예외 발생 시 rollback되는 과정을 보여줌
  • 커넥션 획득/반납: "Acquired Connection", "Returning JDBC Connection to DataSource"
    • 트랜잭션이 시작될 때 DataSource(커넥션 풀)로부터 커넥션을 가져오고, 트랜잭션이 끝나면 다시 반납하는 시점을 알려줌
  • 트랜잭션 전파: "Participating in existing transaction", "Suspending current transaction"
    • 여러 @Transactional 메서드가 중첩 호출될 때, 기존 트랜잭션에 참여하거나 새로운 트랜잭션을 시작하는 전파(propagation) 동작을 확인할 수 있음
  • Auto-commit 설정: "Switching JDBC Connection to manual commit"
    • 트랜잭션 시작 시 auto-commit을 끄고 수동 커밋 모드로 전환하는 과정을 보여줌

 

2.2.2. 커넥션 누수 확인

해당 옵션들 적용 후 모니터링을 통해 다음과 같은 abandoned trace log가 발생하고 사용한 커넥션을 정상적으로 반환하는 로그가 남지 않음을 확인하였다.

org.apache.commons.dbcp.AbandonedTrace$AbandonedObjectException: DBCP object created 2026-01-14 08:51:19 by the following code was never closed

 

결국 현재 어플리케이션에서 사용한 커넥션이 제대로 반환되지 않는 누수가 발생하고 있으며, 누수의 근본적인 원인을 찾아 해결해야함을 깨닫게 되었다.

 

trace log를 따라 커넥션 객체가 최초로 사용된 시점에 실행된 코드를 살펴보니 TransactionManager 를 통해 트랜잭션을 관리하고 있었고, 일부 case에 따라 commit하는 코드가 누락된 것을 확인하였다. 즉, 해당 트랜잭션이 열려 있는 상태로 지속적으로 커넥션이 방치되고 있었으며, 어플리케이션에서 사용할 수 있는 커넥션의 개수가 줄어들게 된 것이었다...

 

이후, 해당 코드를 수정하고 재배포하여 커넥션 누수에 의한 커넥션 부족 상황을 해결할 수 있었다.

 

2.3. 커넥션 풀 크기 설정 확인

이번엔 커넥션 풀 크기를 살펴볼 것인데..

<property name="maxActive" value="5" />
<property name="maxIdle" value="5" />
<property name="minIdle" value="5" />
<property name="maxWait" value="1000" /> <!-- 1초 -->

 

가장 먼저 눈에 띄는 점은 maxActive 값이 상당히 보수적으로 설정되어 있다는 것이었다. 이 경우, 동시 요청이 5개 이상 들어올 경우 이후의 요청은 사용 가능한 커넥션을 얻지 못해 실패하게 된다. 실제로 모니터링을 위해 현재 사용중인 커넥션의 개수를 로깅하는 코드를 추가한 결과, 5개의 커넥션 모두 active 상태가 되어 커넥션 획득에 실패하는 경우가 빈번히 발생하였다.

 

따라서, maxActive를 비롯한 설정을 10으로 올려주었다. 

<!-- 커넥션풀 크기 설정 -->
<property name="maxActive" value="10" />
<property name="maxIdle" value="10" />
<property name="minIdle" value="10" />

 

커넥션 획득 대기 시간 또한 이전보다 여유있게 조정해주었다.

<!-- 커넥션 획득 대기시간 -->
<property name="maxWait" value="5000"/>  <!-- 5초 -->

 


 

3. 후기

해당 설정들을 적용한지 한 달여간 지속적인 모니터링을 한 결과, 다행히도 더 이상 CannotGetJdbcConnectionException 에러가 발생하지 않았다. 아무래도 트래픽이 많은 시스템이 아니라  커넥션 풀 크기를 10으로 잡아도 충분히 운영이 가능한 것으로 보인다. 이번 기회에 dbcp 관련 자료를 많이 접할 수 있었는데, 일반적으로 커넥션 풀의 크기를 운영 환경에 따라 점차 늘려가며 사용한다고 한다. (물론, 스레드 풀 크기보단 작게) 

 

사실 해당 에러를 접한 처음부터 사용할 수 있는 커넥션의 개수가 부족한 것이 원인이지 않을까란 생각을 하고 접근하였는데, dbcp 관련 공부를 하다 보니 커넥션 누수에 의한 커넥션 부족 현상 또한 발생할 수 있다는 것을 인지할 수 있었다. 덕분에, 기존에 알지 못했던 문제를 식별하고 조치할 수 있었다.

 

마지막으로, 이번 모니터링을 통해 애플리케이션에서 불필요한 DB 호출이 많이 발생한다는 것을 알게 되었는데, 몇 가지 개선점을 보고하고 리팩토링을 진행하여 개선할 수 있었다. 개인적으로 나에게 이번 트러블슈팅은 에러 해결부터 DB 호출 최적화까지 많은 것들을 경험할 수 있는 의미있는 시간이였다.

 


 

Reference

https://d2.naver.com/helloworld/5102792

 

 

 

 

'TroubleShooting' 카테고리의 다른 글

ORA-01555: snapshot too old 에러 해결  (0) 2025.12.26
쿼리 튜닝을 통해 WMS 레이어 응답 속도 80% 개선하기  (0) 2025.09.16
'TroubleShooting' 카테고리의 다른 글
  • ORA-01555: snapshot too old 에러 해결
  • 쿼리 튜닝을 통해 WMS 레이어 응답 속도 80% 개선하기
nicky777
nicky777
  • nicky777
    Nicky Dev
    nicky777
  • 전체
    오늘
    어제
    • 분류 전체보기 (19)
      • Project (9)
        • 티켓핑 (9)
      • TroubleShooting (3)
      • Programming (0)
        • Java (0)
        • Spring (0)
      • CS (7)
        • 데이터베이스 (6)
        • 네트워크 (1)
        • 운영체제 (0)
        • 자료구조 (0)
      • 회고 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Contact
  • 인기 글

  • 태그

    유일키 생성 전략
    HTTP
    리플리케이션
    샤딩
    materialized view
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
nicky777
CannotGetJdbcConnectionException 에러 해결
상단으로

티스토리툴바