NAVER Business Platform DBMS개발랩 김시완
응용 프로그램에서 DBMS(database management system)에 연결하여 질의를 처리할 때 꼭 필요한 것이 각 벤더에서 제공하는 드라이버입니다. Java를 이용할 때는 표준화된 JDBC(Java Database Connectivity)를 이용하여 쉽게 개발할 수 있습니다. 또한, 응용 프로그램 개발 생산성을 향상시킬 수 있는 여러 뛰어난 도구와 플랫폼이 제공됩니다.
이 글에서는 이러한 JDBC의 커넥션을 CUBRID가 내부적으로 관리하는 방법을 알아보겠습니다.
CUBRID는 커넥션 관리 방식이 다른 DBMS와는 달라서 그에 따른 몇 가지 장점이 있고, 이는 응용 프로그램이 JDBC의 커넥션을 관리하기 위한 DBCP(database connection pool) 사용에 영향을 줍니다(DBCP는 Apache 프로젝트 중 하나로 개발되고 있으며, 다른 많은 상위 플랫폼에 기본적으로 포함되어 있습니다).
먼저, CUBRID의 커넥션 관리 방식을 알아보고 그에 따라 DBCP를 어떻게 설정하는지 알아보겠습니다.
드라이버 커넥션 검증
DBCP를 이용하여 DBMS와의 커넥션을 관리하다 보면 질의 처리 중 이미 끊어진 커넥션 객체를 응용 프로그램이 전달받을 수 있다. 예를 들어 DBMS는 자원을 효율적으로 사용하기 위해서 낭비되는 자원을 관리하는 설정을 제공하는데, 이 중 대표적인 것이 유휴 커넥션 최대 유지 시간이다(MySQL에서는 wait_timeout, 기본 값은 8시간). 이 설정으로 지정한 시간 동안 커넥션을 통해 아무 요청이 없으면 DBMS는 그 커넥션을 강제로 종료한다. 이처럼 응용 프로그램이 끊어진 커넥션 객체를 받을 수 있는 상황은 다음과 같다.
- DBMS가 자원을 효율적으로 사용하기 위해 유휴 커넥션 강제 종료
- DBMS 재시작으로 인한 커넥션의 종료
- 네트워크 문제로 인한 커넥션의 종료
그러나 이러한 경우에서 응용 프로그램은 커넥션이 종료되었는지 알 수 없으므로, 커넥션 검증 없이 그대로 요청을 보내면 커넥션이 종료되었다는 예외를 받게 될 것이다. 물론, 응용 프로그램에서 모든 예외를 잘 처리하고 있다면 이런 경우에는 요청이 성공할 때까지 기존 커넥션을 반납하고 새 커넥션을 받는 과정을 반복하여 결국 요청을 처리할 것이다. 그러나 응용 프로그램에서 모든 예외를 처리하기는 힘들다.
DBCP의 커넥션 검증이란 이러한 사태를 미연에 방지하기 위해 귀찮은 일을 응용 프로그램 대신 DBCP가 하는 것이다. DBCP는 여러 설정을 통해 커넥션 검증을 지원하는데, 그것은 먼저 CUBRID의 커넥션 관리 방식을 살펴본 후에 알아보겠다.
CUBRID의 커넥션 관리 방식
앞에서는 드라이버 커넥션의 유효성 검사가 왜 필요한지 알아보았다. 하지만 CUBRID를 사용하는 경우는 이러한 커넥션 유효성 검사가 필요 없는데 그 이유를 알아보자.
먼저 CUBRID는 3 tier 구조로 되어 있다. 드라이버와 서버 사이에 브로커라는 미들웨어가 있는 구조이다. 브로커는 크게 두 종류의 프로세스로 구분되는데 한 개의 브로커 프로세스(cub_broker)와 여러 개의 CAS 프로세스(cub_cas)이다.
cub_broker는 드라이버의 연결을 받아 요청을 처리하거나 SQL 질의 처리를 위해 드라이버와 유휴 cub_cas나 대기 cub_cas를 연결시켜 주는 역할을 한다. cub_cas는 드라이버와 연결된 후 응용 프로그램이 보내는 SQL을 서버에서 처리하고 결과를 받아서 드라이버로 전달한다.
드라이버의 요청을 처리하는 cub_cas는 다음과 같은 상태로 전환될 수 있다.
표 1 CAS 프로세스(cub_cas) 상태
cub_cas 프로세스 상태 | 설명 |
유휴(IDLE) | 연결된 드라이버 없이 새 커넥션을 기다리고 있는 상태 |
동작(BUSY, CLIENT_WAIT) | 연결된 드라이버에서 요청을 보내 처리 중인 상태(트랜잭션 진행 중) |
대기(CLOSE_WAIT) | 연결된 드라이버의 요청이 완료되어 다음 요청을 기다리는 상태. 기존 커넥션을 끊고 새 드라이버 커넥션을 받을 수 있다(트랜잭션 완료). |
이러한 브로커는 서버와 드라이버 사이의 커넥션에 풀을 제공하는 것과 비슷하게 동작한다. 그래서 드라이버는 CUBRID의 max_client 설정보다 더 많은 커넥션을 만들 수 있다. 물론, max_client에 설정된 수만큼의 커넥션만 동시에 사용할 수 있고 나머지는 브로커의 큐에서 대기한다.
그림 1 CUBRID의 3 tier 구조
<그림 1>과 같이 응용 프로그램에서 브로커로 연결한 커넥션의 수가 브로커에서 서버로 연결된 수보다 많게 유지될 수 있다. 이때, 드라이버 커넥션은 활성 상태에서 비활성 상태로, 또는 비활성 상태에서 활성 상태로 전환된다. 드라이버 커넥션의 활성 상태와 비활성 상태는 다음과 같다.
표 2 드라이버 커넥션
드라이버 커넥션 상태 | 설명 |
활성 | cub_cas와 소켓이 연결되어 있고 즉시 요청을 전송할 수 있는 상태 |
비활성 | cub_cas와 소켓이 끊어져 cub_broker를 통해 재접속해야 요청을 전송할 수 있는 상태. cub_broker를 통해 cub_cas와 재접속되면 활성 상태로 전환 된다. |
cub_broker 동작 방식
우선 cub_broker의 동작 방식을 살펴보자. cub_broker는 드라이버와 직접적으로 커넥션을 맺은 후 우선 드라이버의 요청을 파악한다. cub_broker가 처리할 수 있는 요청은 커넥트, 질의 수행 중단과 핑 세 가지이다.
- 커넥트(connect): 드라이버가 응용 프로그램의 SQL을 처리하기 위해 cub_cas와 연결을 요청
- 질의 수행 중단(query cancel): 특정 cub_cas에서 수행 중인 SQL 중단
- 핑(ping): 드라이버가 cub_cas와 연결이 정상인지 확인
질의 수행 중단 요청과 핑 요청은 cub_broker의 accept 스레드가 독자적으로 처리한다. 커넥트 요청을 받으면 accept 스레드는 드라이버의 커넥션을 큐(job queue)에 넣는다. 비활성 상태에서 활성 상태로 전환될 드라이버들이 우선 여기서 기다린다.
이후, dispatcher 스레드가 큐에서 드라이버의 커넥션을 꺼내, 유휴 cub_cas를 찾은 후 커넥션을 전달한다. 이때 유휴 cub_cas가 없다면 새로 cub_cas를 생성한 후 전달하거나 대기 cub_cas에 전달할 수 있다.
그림 2 브로커의 동작 방식
cub_cas 동작 방식
cub_cas는 전달받은 드라이버 커넥션으로부터 SQL 요청을 받아 처리를 시작한다. 이때 트랜잭션이 시작되며, 트랜잭션 종료(commit/rollback이 될 때)까지 이 트랜잭션을 유지한다.
트랜잭션이 종료되면 cub_cas는 대기 상태로 전환되고, 연결된 드라이버가 새로운 트랜잭션을 시작하거나 브로커로부터 새로운 커넥션을 받을 수 있도록 대기한다.
cub_broker가 새로운 드라이버 커넥션을 cub_cas에 전달하면, cub_cas는 기존의 드라이버 커넥션을 종료하고 새로운 드라이버 커넥션을 받는다. 비활성 상태로 전환된 드라이버 커넥션이 활성 상태로 전환되려면 브로커를 통해 다시 접속해야 한다.
드라이버 동작 방식
다음으로 드라이버의 동작을 살펴보자. 드라이버는 우선 응용 프로그램의 요청을 cub_cas로 보내려는 시도를 한다. 이때, cub_cas로부터 특정 시간 동안 응답이 없으면(timeout 발생) cub_broker에 핑 요청을 해 본다.
cub_broker가 핑 요청에 응답하면 다시 기다리고, 그렇지 않다면 에러가 발생하고 다음 과정으로 넘어간다. 이 과정은 네트워크의 장애로 인한 응답 지연을 파악하는 데 유용하다.
네트워크를 통해 요청을 cub_cas로 보내는 데 실패하거나 cub_cas로부터 받은 응답이 에러이면 그 에러가 재접속해야 하는 에러인지를 판단한다. 재접속해야 하는 에러인 경우 트랜잭션 진행 중이 아니라면 재접속 후 다시 요청을 보낸다.
드라이버는 트랜잭션이 진행 중이 아닐 때 소켓의 상태와 브로커가 보내주는 에러 코드를 종합적으로 분석하여 판단하고 재접속을 시도한다. 재접속해야 하는 상황은 다음과 같다.
- 소켓이 이미 끊어진 상태
- 드라이버와 cub_cas의 커넥션이 비활성 상태가 되었을 때
- 드라이버와 cub_cas의 커넥션이 네트워크 등의 문제로 단절되었을 때
- 특정 오류가 발생한 경우
- cub_cas와 서버의 커넥션이 네트워크 등의 문제로 단절되었을 때
- 서버가 비정상으로 종료되어 질의 처리가 불가능할 때
그림 3 드라이버의 동작 방식
이러한 브로커와 드라이버의 쌍방 메커니즘에 의해 드라이버 커넥션의 상태가 응용 프로그램의 관여 없이 비활성에서 활성 상태로 변경 가능하다. 이는 응용 프로그램의 관여 없이도 드라이버가 항상 연결을 유지한다는 것을 의미한다.
이 때문에 몇 가지 이점이 있는데, 그 중 하나가 앞서 이야기 한 커넥션 검증이 별도로 필요하지 않다는 것이다. 또한, 드라이버가 접속할 수 있는 브로커가 여러 개이면 우선 순위에 따라 모든 브로커에 접속을 시도하여 이중화를 지원한다.
DBCP의 커넥션 검증 설정
지금까지 CUBRID에 커넥션 검증이 필요 없는 이유를 알아보았다. DBCP는 어떻게 커넥션을 검증하는지 DBCP의 각 설정에 대해서 알아보고, 커넥션 검증을 사용하지 않도록 하는 권장 설정을 알아보자.
표 3 커넥션 검증 관련 DBCP 설정
설정 이름 | 기본 값 | 설명 |
validationQuery | 없음 | 커넥션의 검증에 사용할 SQL. 1개 이상의 row를 결과로 받는 SELECT 문이다. |
testOnBorrow | true | 커넥션 풀에서 커넥션을 하나 꺼내 올 때 validationQuery에 설정된 SQL을 수행, 검증에 성공한 커넥션 객체만 얻어 옴 |
testOnReturn | false | 사용한 커넥션을 풀로 반환할 때 validationQuery에 설정된 SQL을 수행, 검증에 성공한 커넥션 객체만 풀에 들어감 |
testWhileIdle | false | eviction 스레드가 동작할 때 유휴 상태에 있는 커넥션의 검증 여부 |
numTestsPerEvictionRun | 3 | eviction 스레드가 한 번 동작할 때 검사할 커넥션 객체의 수 |
timeBetweenEvictionRunsMillis | -1 | eviction 스레드가 동작하는 주기. 음수면 eviction 스레드가 동작하지 않는다. |
커넥션 검증에 관련된 위의 여러 설정 중에서 몇 가지만 자세히 살펴보자.
- validationQuery: DBMS와의 커넥션을 검증할 때 사용할 SQL 문을 설정한다. 결과로 한 개 이상의 ROW가 반환되는 SELECT 문이 필요하다. 설정해야 한다면 최대한 성능에 지장을 주지 않는 SQL 문으로 하는 것이 좋다. 설정을 잘못하면 실제 서비스 성능에 큰 영향을 줄 수 있다. 예를 들어, 트랜잭션이 매우 짧고 응답 속도가 중요한 응용 프로그램에서 커넥션 객체를 풀에서 얻어 올 때마다 수행시간이 긴 validationQuery를 수행한다면 성능이 크게 저하될 것이다.
- testOnBorrow: 풀에서 커넥션을 얻어 올 때 validationQuery를 이용해 검증한다. 트랜잭션을 시작하기 전에 미리 SQL을 한번 수행해 보는 것이다. SQL 처리에 실패한다면 그 커넥션 객체는 더 이상 사용하지 않고 SQL 처리에 성공한 커넥션 객체만 응용 프로그램에 전달된다. 실제 커넥션 검증의 핵심 설정이라 할 수 있다.
- testWhileIdle: DBCP는 설정에 따라 주기적으로 eviction 스레드(유휴 상태에 있는 커넥션 객체를 정리)를 동작시키는데, 이때 장시간 사용되지 않은 커넥션 객체를 validationQuery를 이용해 검증한다.
이처럼, DBCP는 커넥션 객체를 풀에 넣거나 뺄 때 또는 백그라운드 작업으로 검증을 하고 이는 SQL을 한번 수행해 보는 것으로 처리된다. 이러한 방식의 커넥션 검증은 성능을 감소시킨다. CUBRID에서는 드라이버와 브로커가 상호 연동하며 자동으로 재접속하여 커넥션을 유지하기 때문에 DBCP에서 커넥션 검증을 제거하여 성능의 이득을 볼 수 있다.
CUBRID JDBC 사용 시 DBCP 권장 설정
앞에서 이야기했듯이 CUBRID는 자동 재접속 메커니즘을 드라이버에서 제공하므로 DBCP와 함께 사용할 때는 커넥션을 검증하도록 설정하지 않아도 된다. 따라서 다음과 같이 DBCP를 설정하는 것이 성능상 유리하다.
testOnBorrow와 testOnReturn은 꼭 false로 설정하여 validationQuery를 수행하지 않도록 한다. eviction 스레드를 이용하여 커넥션의 수를 조절하는 것은 커넥션 검증과는 무관하다. 단, testWhileIdle을 꼭 false로 설정해야 한다. testWhileIdle은 유휴 상태로 있는 드라이버의 커넥션을 검증하는 것인데, CUBRID의 경우 사용자의 요청 없이 비활성 상태의 드라이버 커넥션이 활성 상태로 변경될 수 있으므로 환경에 따라 성능에 많은 영향을 줄 수 있다.
비활성 상태의 커넥션을 많이 유지하도록 설정했을 경우, testWhileIdle로 인해 validationQuery가 수행되어 활성 상태로 변경하려는 드라이버 커넥션이 많아진다. 이로 인해 브로커의 대기 큐가 길어지면 사용자 요청에 따라 활성 상태로 변경될 드라이버 커넥션의 반응 시간이 느려지는 현상이 나타날 것이다.
이는 질의 처리 시간에 재접속 시간이 포함되어 결국 응용 프로그램이 수행하는 질의의 응답 시간이 느려지는 결과를 초래한다. DBCP가 매우 많은 수의 장비에서 동작하는 경우 응답이 지연되는 현상은 더 심화될 수 있으며, 이러한 현상이 발생한 경우 응용 프로그램에서의 질의 처리는 오래 걸리지만 cub_cas의 로그에는 질의 수행 시간이 짧게 기록될 수 있다.
따라서 CUBRID를 사용하는 경우에는 필요하지 않은 DBCP의 커넥션 검증을 사용하지 않도록 다음 세 가지 설정을 변경하도록 하자.
- testOnBorrow=false
- testOnReturn=false
- testWhileIdle=false
마치며
지금까지 CUBRID의 JDBC 드라이버와 브로커의 동작 방식을 살펴보았고 드라이버 커넥션의 검증이 필요 없음을 확인하였다. 그리고 DBCP가 제공하는 커넥션 검증에 대해 몇 가지 설정을 살펴보고 CUBRID의 경우는 이 설정을 어떻게 해야 할지도 살펴보았다.
CUBRID를 사용할 때는 testOnBorrow, testOnReturn, testWhileIdle 이 세 가지를 꼭 false로 설정해야 하는 것을 기억하자.
참고 자료
- Apache Commons DBCP http://commons.apache.org/dbcp
- NAVER Business Platform DBMS개발랩 김시완
- 2007년 NHN 입사 이후 CUBRID 한길만 파고 있는 개발자입니다.
- CUBRID를 개발하면서 얕았던 지식이 점점 깊어지고 있어 매우 즐겁습니다.
- CUBRID가 세계 최고의 DBMS가 되는 그날까지 열심히 달리겠습니다.