최근에 회사에서 스크립트 작업을 하다,
Typeorm의 기본 Connection Pool이 10개가 된다는 사실을 알고 동료가 20개로 조정해서 동시성을 늘리는 건 어떻겠냐고 제안을 했다.
(시간이 오래 걸려도 상관이 없었지만) 스크립트 작업이니 짧은 시간내에 요청이 많은 건 사실이니 조금이라도 빠르게 처리하기 위해서 커넥션 풀을 조금 더 늘리는 건 괜찮은 아이디어라고 생각하고 작업을 진행하였다.
데이터베이스 연결 풀(Connection Pool)이란?
데이터베이스 연결 풀은 데이터베이스 연결을 캐시하여 필요할 때 재사용할 수 있도록 유지하는 메커니즘이다. 이는 애플리케이션이 데이터베이스와 상호작용할 때 성능을 개선하고 리소스 활용을 최적화하는 일반적인 최적화 기법이다. 매번 새로운 데이터베이스 연결을 열고 닫는 대신, 연결 풀은 일정 수의 연결을 유지하며 필요할 때 제공하는 방식으로 작동한다.
연결 풀의 동작 방식
1.
초기화(Initialization): 애플리케이션이 시작될 때 일정 수의 데이터베이스 연결이 생성되어 풀에 추가된다.
2.
연결 요청(Connection Request): 애플리케이션이 데이터베이스 작업을 수행하려고 할 때, 연결 풀에서 연결을 요청다.
3.
사용(Usage): 애플리케이션은 할당받은 연결을 사용하여 데이터베이스 작업을 수행한다.
4.
반환(Return): 작업이 완료되면 연결을 닫는 대신 풀에 반환하여 다른 요청에서 사용할 수 있도록 합니다.
5.
재사용(Reuse): 이후 데이터베이스 연결이 필요할 때, 풀에 사용 가능한 연결이 있으면 이를 재사용하며, 없을 경우 사용 가능한 연결이 생길 때까지 대기합니다.
연결 풀의 장점
1.
성능 향상(Improved Performance)
•
매번 새로운 연결을 생성하는 오버헤드를 줄여 쿼리 응답 속도를 개선하고, 애플리케이션의 전반적인 성능을 향상시킨다.
2.
리소스 효율성(Resource Efficiency)
•
데이터베이스 연결은 제한된 리소스이므로, 불필요한 연결 낭비를 줄이고 최대한 효율적으로 사용하여 더 많은 사용자가 동시에 데이터베이스에 접근할 수 있도록 한다.
3.
연결 재사용(Connection Reuse)
•
기존 연결을 재사용하면 데이터베이스 서버에서 매번 새로운 세션을 생성할 필요가 없어 부하를 줄이고, 특히 트래픽이 많은 애플리케이션에서 성능을 크게 개선할 수 있다.
4.
연결 관리(Connection Management)
•
연결 풀은 연결의 생성, 반환 및 유지 관리 등의 작업을 자동으로 처리하여 개발자가 직접 관리해야 하는 부담을 줄이고, 리소스 누수를 방지할 수 있다.
5.
동시성 제어(Concurrency Control)
•
연결 풀은 동시에 사용 가능한 최대 연결 수를 제한하여 과부하를 방지하고 데이터베이스의 성능 저하를 방지한다.
6.
장애 허용성(Fault Tolerance)
•
일부 연결 풀 구현에서는 연결 오류가 발생할 경우 자동으로 새로운 연결을 생성하거나 실패한 연결을 교체하는 기능을 제공하여 애플리케이션의 안정성을 높인다.
7.
사용자 정의 설정(Customization)
•
개발자는 최대 풀 크기, 연결 시간 제한 등 다양한 옵션을 설정하여 애플리케이션의 요구 사항에 맞게 최적화할 수 있다.
8.
연결 재활용(Connection Recycling)
•
오래된 연결을 주기적으로 갱신하여 오래된(stale) 연결 문제를 방지하고, 항상 건강한 상태의 연결을 유지한다.
9.
오버헤드 감소(Reduced Overhead)
•
데이터베이스 연결을 열고 닫을 때 발생하는 인증, 네트워크 설정, 연결 해제 등의 오버헤드를 줄여 시스템 리소스를 효율적으로 사용할 수 있다.
10.
호환성(Compatibility)
•
대부분의 데이터베이스 시스템과 프로그래밍 언어에서 연결 풀 기능을 지원하므로 다양한 데이터베이스 드라이버 및 라이브러리와 함께 사용할 수 있다.
11.
확장성(Scalability)
•
애플리케이션이 여러 인스턴스나 서버에서 실행될 경우에도 효율적으로 데이터베이스 연결을 관리할 수 있어 확장성이 뛰어나다.
연결 풀의 단점
1. 최대 연결 제한으로 인한 대기 시간 증가
•
문제점
◦
연결 풀의 크기(connectionLimit)가 가득 차면, 새로운 요청은 기존 연결이 해제될 때까지 대기해야 한다. 특히 고부하(High Traffic) 환경에서 풀 크기가 작으면 연결 대기 시간이 증가할 수 있다.
•
해결 방법
◦
예상 트래픽에 맞춰 connectionLimit을 적절히 설정
◦
쿼리 성능을 최적화하여 불필요한 연결 점유를 줄이기
◦
queueLimit을 조정하여 너무 많은 요청이 대기하지 않도록 설정
2.
연결 풀 관리 복잡성 증가
•
문제점
◦
연결 풀을 사용하면 커넥션을 명확하게 닫아야 하는 책임이 발생합니다. 잘못된 구현으로 인해 연결이 풀에서 해제되지 않고 누적되는 문제(Connection Leak)가 생길 수 있다. 이는 시간이 지남에 따라 시스템이 응답하지 않는 상태(Deadlock)로 이어질 수 있다.
•
해결 방법:
◦
모든 트랜잭션 처리 후 반드시 연결을 해제 (release(), end())
◦
finally 블록을 사용하여 예외가 발생해도 연결이 풀로 반환되도록 보장
◦
Connection Leak 탐지 도구를 사용하여 연결 누수를 모니터링
3.
풀 내 오랫동안 유지된 연결로 인해 세션 만료 가능
•
문제점
◦
연결이 오랫동안 사용되지 않으면 데이터베이스가 자동으로 연결을 종료할 수 있다. 하지만 애플리케이션은 여전히 해당 연결을 사용하려고 시도하면서 세션 만료(Session Timeout) 오류가 발생할 수 있다.
•
해결 방법
◦
idleTimeoutMillis 옵션을 사용하여 오래된 연결을 자동으로 해제
◦
keepAlive 설정을 활성화하여 주기적으로 유휴 연결을 유지
◦
retry 메커니즘을 추가하여 세션이 만료되었을 때 자동으로 재연결
4.
데이터베이스 성능 저하 가능성
•
문제점:
◦
너무 많은 연결을 동시에 유지하면 데이터베이스가 CPU 및 메모리를 과도하게 사용하게 된다. 또한 풀 크기를 너무 크게 설정하면 시스템 리소스를 불필요하게 차지할 수 있다. 이는 오히려 데이터베이스 응답 속도를 느리게 하거나 장애를 유발할 가능성이 있다.
•
해결 방법:
◦
connectionLimit을 적절한 수준으로 설정하여 과부하 방지
◦
애플리케이션의 실제 트래픽을 분석하여 최적의 풀 크기 결정
◦
로드 밸런싱 및 데이터베이스 샤딩을 고려하여 분산 처리
5.
병목 현상 가능성 (Pool Exhaustion)
•
문제점
◦
동시에 많은 요청이 발생하면 풀의 모든 연결이 사용되고, 새로운 요청은 대기 상태(Queue)로 밀려나는 문제가 발생할 수 있다. 이로 인해 요청 시간이 길어지고, 결국 애플리케이션 성능이 저하될 위험이 있다.
•
해결 방법
◦
queueLimit을 설정하여 너무 많은 요청이 대기하지 않도록 제한
◦
acquireTimeoutMillis 옵션을 활용하여 연결 획득이 너무 오래 걸리면 실패하도록 설정
6.
트랜잭션 관리 실수 가능성
•
문제점
◦
연결 풀을 사용할 때, 개발자가 트랜잭션을 명확히 종료하지 않으면 데이터베이스가 해당 트랜잭션을 계속 유지하고 있을 수 있다. 이는 데이터 무결성 문제를 일으킬 수 있으며, 결국 풀 내 연결을 고갈시킬 수도 있다.
•
해결 방법
◦
트랜잭션이 끝나면 반드시 commit() 또는 rollback()을 실행
◦
트랜잭션 후에는 반드시 release()를 호출하여 연결을 풀로 반환
◦
예외 처리(try-catch-finally)를 올바르게 구현
결론
그럼 언제 연결 풀을 사용해야 할까?
연결 풀링은 데이터베이스 성능을 크게 향상시키는 기술이지만, 적절한 관리 없이 사용하면 오히려 성능 문제를 일으킬 수도 있다.
•
연결 풀을 사용할 때 고려할 점
◦
적절한 풀 크기(connectionLimit)를 설정하여 데이터베이스 과부하를 방지
◦
트랜잭션 종료 후 반드시 release()를 호출하여 풀 내 연결이 해제되도록 관리
◦
쿼리 최적화를 수행하여 연결을 빠르게 반환하도록 개선
◦
유휴 연결을 주기적으로 정리하여 세션 만료 문제 방지
결국은, 당연한 이야기이지만
안정된 애플리케이션을 만들기 위해서는 때와 시기와 규모에 맞는 적절한 설정과 모니터링을 통해 안정적이게 운영해야 하는 것 같다.