Connection Pool

Connection pool(연결 풀)은 데이터베이스 연결을 효율적으로 관리하기 위한 기술이다.
이 기술은 애플리케이션의 성능을 향상시키고 리소스 사용을 최적화하는 데 중요한 역할을 한다.

Connection pool은 데이터베이스 연결을 재사용 가능한 형태로 캐시하는 메커니즘이다.
이는 애플리케이션이 데이터베이스에 연결할 때마다 새로운 연결을 생성하는 대신, 미리 생성된 연결을 사용할 수 있게 해준다.

Connection pool은 현대 데이터베이스 애플리케이션에서 필수적인 기술로, 적절히 구현 및 설정될 경우 애플리케이션의 성능과 안정성을 크게 향상시킬 수 있다.

Connection Pool
https://medium.com/@sujoy.swe/database-connection-pool-647843dd250b

Connection Pool의 작동 원리

  1. 초기화: 애플리케이션 시작 시 미리 정해진 수의 데이터베이스 연결을 생성하여 풀에 저장한다.
  2. 연결 요청: 클라이언트가 데이터베이스 작업을 요청하면, 풀에서 사용 가능한 연결을 가져온다.
  3. 연결 사용: 클라이언트는 가져온 연결을 사용하여 데이터베이스 작업을 수행한다.
  4. 연결 반환: 작업이 완료되면 연결은 풀로 다시 반환된다.
  5. 연결 관리: 풀은 연결의 수명주기를 관리하며, 필요에 따라 새로운 연결을 생성하거나 오래된 연결을 제거한다.

Connection Pool의 주요 설정 파라미터

  1. 초기 연결 수 (initialSize):

    • pool 생성 시 초기에 만들어두는 연결의 개수
    • 애플리케이션 시작 시간과 초기 메모리 사용량에 영향
  2. 최대 연결 수 (maxActive/maxTotal):

    • pool이 관리할 수 있는 최대 연결 개수
    • 서버의 리소스 상황을 고려하여 설정
  3. 최소 유휴 연결 수 (minIdle):

    • pool에서 유지할 최소한의 유휴 연결 개수
    • 갑작스러운 요청 증가에 대비
  4. 최대 대기 시간 (maxWait):

    • 사용 가능한 연결이 없을 때 대기할 최대 시간
    • 타임아웃 설정으로 무한 대기 방지

Connection Pool의 장점

  1. 성능 향상: 연결 생성 및 해제에 따른 오버헤드를 줄여 애플리케이션의 응답 시간을 개선한다.
  2. 리소스 효율성: 제한된 수의 연결을 재사용함으로써 데이터베이스 서버의 리소스 사용을 최적화한다.
  3. 확장성: 동시에 처리할 수 있는 요청의 수를 증가시켜 애플리케이션의 확장성을 향상시킨다.
  4. 연결 관리: 연결의 수명주기를 자동으로 관리하여 개발자의 부담을 줄인다.

Connection Pool 설정 시 고려사항

  1. 풀 크기: 동시에 유지할 연결의 최소 및 최대 수를 적절히 설정해야 한다.
  2. 연결 수명: 연결의 최대 유지 시간을 설정하여 오래된 연결을 관리한다.
  3. 검증 쿼리: 주기적으로 연결의 유효성을 검사하는 쿼리를 설정한다.
  4. 대기 시간: 모든 연결이 사용 중일 때 새로운 연결 요청의 최대 대기 시간을 설정한다.

Connection Pool의 구현

Connection pool은 다양한 방식으로 구현될 수 있다.
주로 사용되는 방식은 내부 풀링, 외부 풀링, 컨테이너 관리 세 가지가 있다.

각 방식은 고유한 장단점을 가지고 있으며, 애플리케이션의 요구사항과 운영 환경에 따라 적절한 방식을 선택해야 한다.
특히 애플리케이션의 규모가 커질수록 더 강력한 관리 기능과 확장성을 제공하는 방식을 고려해야 한다.

비교 기준내부 풀링외부 풀링컨테이너 관리
구현 복잡도낮음중간높음
설정 용이성매우 쉬움중간복잡함
확장성제한적높음매우 높음
모니터링 기능기본적풍부함매우 풍부함
리소스 관리개별 관리독립적 관리중앙 집중식
유지보수쉬움중간복잡함
성능 최적화중간높음매우 높음
장애 복구제한적우수함매우 우수함
트랜잭션 지원기본적풍부함완벽한 지원
보안 통합제한적중간강력함
배포 복잡도낮음중간높음
리소스 사용량낮음중간높음
다중 데이터소스 지원제한적우수함매우 우수함
커스터마이징제한적높음매우 높음

각 방식의 선택 기준:

  1. 내부 풀링 선택 시기:

    • 작은 규모의 애플리케이션
    • 단순한 데이터베이스 연결 요구사항
    • 빠른 개발이 필요한 경우
  2. 외부 풀링 선택 시기:

    • 중간 규모의 애플리케이션
    • 세밀한 풀링 제어가 필요한 경우
    • 다양한 환경에서의 재사용성이 중요한 경우
  3. 컨테이너 관리 선택 시기:

    • 대규모 엔터프라이즈 애플리케이션
    • 고가용성이 요구되는 환경
    • 중앙 집중식 관리가 필요한 경우

내부 풀링(Internal Pooling)

커넥션 풀의 “내부 풀링(Internal Pooling)“은 애플리케이션 내부에서 직접 연결 풀을 관리하는 방식으로, 주로 데이터베이스 드라이버나 ORM(Object-Relational Mapping) 프레임워크에 내장된 기능을 통해 구현된다.
이 방식은 외부 의존성 없이 애플리케이션 자체에서 연결 재사용을 최적화한다.

내부 풀링은 간편성과 통합성을 중시하는 환경에 적합하다.
ORM이나 데이터베이스 드라이버에 내장된 기능을 활용해 빠르게 구현할 수 있으나, 대규모 분산 시스템에서는 연결 관리의 비효율성이 발생할 수 있다.
Hibernate의 C3P0 통합이나 MySQL Connector/J의 자체 풀링이 대표적 예시이며, minSizemaxSize를 트래픽 패턴에 맞게 조정하는 것이 성능 개선의 핵심이다.

내부 풀링의 핵심 동작 원리
  1. 풀 초기화

    • 애플리케이션 시작 시 minSize 설정값에 따라 초기 연결을 생성한다.
    • 예: Hibernate는 기본적으로 5개의 연결을 풀에 미리 생성한다.
  2. 연결 할당

    • 애플리케이션 요청 시 풀에서 사용 가능한 연결을 제공한다.
    • 모든 연결이 사용 중이면 maxWait 시간 동안 대기 후 예외 발생.
  3. 연결 반환

    • Connection.close() 호출 시 실제로 연결을 닫지 않고 풀로 반환한다.
  4. 상태 검증

    • validationQuery(예: SELECT 1)로 주기적 연결 검증 수행.
    • 손상된 연결은 자동 제거 후 새 연결로 교체된다.
주요 특징
  1. 프레임워크 통합

    • ORM(Hibernate, EclipseLink)이나 데이터베이스 드라이버(MySQL Connector/J)에 내장.
    • 별도 라이브러리 없이 간편하게 설정 가능.
  2. 설정 간소화

    • 연결 문자열에 파라미터 추가만으로 활성화.
    • 예: JDBC URL에 useUnicode=true&pooling=true 추가.
  3. 애플리케이션 레벨 관리

    • 각 애플리케이션 인스턴스가 독립적인 풀을 유지.
    • 클라우드 환경에서 인스턴스 증가 시 연결 낭비 가능성 있음.
  4. 트랜잭션 지원

    • 트랜잭션 컨텍스트에 따라 연결을 분리 관리.
구현 예시

Hibernate 기준

1
2
3
4
5
6
7
8
// Hibernate 설정 파일 (hibernate.cfg.xml)
<property name="hibernate.connection.provider_class">
  org.hibernate.connection.C3P0ConnectionProvider
</property>
<property name="hibernate.c3p0.min_size">5</property>
<property name="hibernate.c3p0.max_size">50</property>
<property name="hibernate.c3p0.timeout">300</property>
<property name="hibernate.c3p0.idle_test_period">150</property>

SQLAlchemy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# SQLAlchemy 내장 풀 구현 예시
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

class DatabaseManager:
    def __init__(self):
        # 엔진 생성 및 풀 설정
        self.engine = create_engine(
            'mysql://user:password@localhost/mydb',
            pool_size=5,               # 초기 풀 크기
            max_overflow=10,           # 추가 생성 가능한 최대 연결 수
            pool_timeout=30,           # 연결 대기 시간
            pool_recycle=3600,         # 연결 재사용 주기
            echo=True                  # SQL 로깅 활성화
        )
        
        # 세션 팩토리 생성
        self.Session = sessionmaker(bind=self.engine)
    
    def get_session(self):
        return self.Session()

Sequelize

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Sequelize 내장 풀 구현 예시
const { Sequelize } = require('sequelize');

class SequelizeManager {
    constructor() {
        this.sequelize = new Sequelize('mydb', 'user', 'password', {
            host: 'localhost',
            dialect: 'mysql',
            
            // 풀 설정
            pool: {
                max: 5,        // 최대 연결 수
                min: 0,        // 최소 연결 수
                acquire: 30000,// 연결 획득 타임아웃
                idle: 10000    // 유휴 시간
            }
        });
    }
    
    async testConnection() {
        try {
            await this.sequelize.authenticate();
            console.log('데이터베이스 연결 성공');
        } catch (error) {
            console.error('데이터베이스 연결 실패:', error);
        }
    }
}
장점과 단점
구분설명
장점- 설치/설정 간편: 별도 서비스 구성 불필요
- 낮은 지연 시간: 애플리케이션 내부에서 직접 관리
- ORM 최적화: 프레임워크 특성에 맞춘 튜닝 가능
단점- 확장성 제한: 인스턴스 증가 시 연결 중복 생성
- 기능 제한: 외부 풀링 대비 모니터링/튜닝 옵션 적음
- 리소스 경합: 고트래픽 시 단일 애플리케이션 풀 포화 가능성
주요 사용 사례
  1. 소규모 애플리케이션
    • 빠른 개발 및 간단한 설정이 우선인 경우.
  2. ORM 기반 프로젝트
    • Hibernate, EclipseLink 등 ORM의 내장 풀 활용 시.
  3. 임베디드 데이터베이스
    • H2, SQLite 등 경량 DB와의 통합 환경.

외부 풀링(External Pooling)

외부 풀링(External Pooling)은 데이터베이스 연결 관리를 위해 독립적인 라이브러리나 미들웨어를 활용하는 방식으로, 애플리케이션 코드나 서버 인프라와 분리되어 유연성과 성능을 균형 있게 제공한다.
내부 풀링이나 컨테이너 관리 방식과 달리 특정 환경에 종속되지 않으며, 다양한 애플리케이션 환경에 적용 가능하다.

외부 풀링은 복잡한 엔터프라이즈 환경에서 연결 관리의 효율성을 극대화하는 최적의 선택이다.
HikariCP와 같은 라이브러리는 검증된 성능으로 Spring Boot 2.0+에서 기본 채택되었으며, maxPoolSizeidleTimeout을 서버 부하에 맞게 동적으로 조절하는 것이 성능 향상의 핵심이다.
다만, 초기 설정과 모니터링을 통해 연결 누수나 과도한 풀 크기로 인한 자원 낭비를 방지해야 한다.

외부 풀링의 핵심 특징
  1. 독립적 라이브러리 기반

    • HikariCP, Apache DBCP, c3p0 등의 오픈소스 라이브러리를 사용한다.
    • 애플리케이션 코드와 분리되어 별도로 설정 및 관리된다.
  2. 고성능 최적화

    • 연결 생성/해제 오버헤드를 최소화하여 초당 수만 건의 쿼리 처리 가능 (예: HikariCP는 마이크로초 단위의 응답 시간을 지원).
    • 경량화된 아키텍처로 리소스 사용 효율성이 높다.
  3. 세밀한 설정 조절

    • 최대 연결 수(maxPoolSize), 유휴 연결 유지 시간(idleTimeout), 연결 검증 쿼리(validationQuery) 등을 세부적으로 설정 가능하다.
  4. 다중 환경 지원

    • 클라우드, 온프레미스, 멀티티어 아키텍처 등 다양한 환경에서 통합적으로 사용 가능하다.
동작 원리
  1. 풀 초기화

    • 애플리케이션 시작 시 maxPoolSize에 지정된 수만큼 연결을 미리 생성한다.
    • 예: HikariCP는 기본적으로 10개의 연결을 생성한다.
  2. 연결 할당

    • 애플리케이션 요청 시 풀에서 사용 가능한 연결을 제공한다.
    • 모든 연결이 사용 중일 경우 connectionTimeout 동안 대기한다.
  3. 연결 반환

    • 작업 완료 후 connection.close() 호출 시 실제로 연결을 닫지 않고 풀로 반환한다.
  4. 상태 모니터링

    • leakDetectionThreshold를 통해 연결 누수 감지, healthCheck로 연결 상태 주기적으로 검증한다.
구현 예시 (HikariCP 기준)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// HikariCP 설정
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(20);      // 최대 연결 수
config.setMinimumIdle(5);           // 최소 유휴 연결 수
config.setIdleTimeout(30000);      // 유휴 연결 유지 시간(30초)
config.setConnectionTestQuery("SELECT 1"); // 검증 쿼리

HikariDataSource dataSource = new HikariDataSource(config);

// 연결 사용
try (Connection conn = dataSource.getConnection()) {
    // 데이터베이스 작업 수행
}
장점과 단점
구분설명
장점- 높은 성능: 최적화된 알고리즘으로 초고속 처리
- 유연한 설정: 세부 파라미터 조절 가능
- 다중 프레임워크 호환: Spring, Jakarta EE 등 다양한 환경 지원
- WAS 독립성: 서버 변경 시 재구성 불필요
단점- 설정 복잡성: 튜닝을 위한 전문 지식 필요
- 추가 의존성: 라이브러리 업데이트 및 보안 관리 필요
- 초기 학습 곡선: 내부 풀링 대비 구현 난이도 상승
주요 사용 사례
  1. 고트래픽 웹 서비스
    • 동시 접속자 수가 많은 환경에서 연결 재사용률을 극대화한다.
  2. 마이크로서비스 아키텍처
    • 분산된 서비스들이 독립적인 풀을 유지하며 자원 경합을 방지한다.
  3. 배치 처리 시스템
    • 대량의 데이터 처리 시 연결 생성 비용을 절감한다.

컨테이너 관리(Container-Managed Pooling)

커넥션 풀의 “컨테이너 관리(Container-Managed Pooling)” 방식은 웹 애플리케이션 서버(WAS)가 직접 연결 풀을 생성하고 관리하는 방식을 의미한다.
이 방식은 서버 인프라와 긴밀하게 통합되어 있어 개발자의 관리 부담을 줄여주는 특징을 가진다.

컨테이너 관리 커넥션 풀의 핵심 개념
  1. WAS 주도 관리

    • 웹 컨테이너(WAS: Tomcat, JBoss, WebLogic 등)가 애플리케이션 구동 시점에 DB 연결을 초기화하고 풀을 생성한다.
    • 연결의 라이프사이클을 WAS가 전적으로 관리한다.
  2. 중앙 집중식 설정

    • context.xml 또는 서버 전용 설정 파일(예: JEUSMain.xml)에서 풀의 속성을 정의한다.
    • 애플리케이션 코드와 분리되어 인프라 수준에서 관리된다.
  3. 서버 리소스 통합

동작 원리
  1. 풀 초기화

    • WAS 시작 시 maxActive, minIdle 등의 설정값을 기반으로 초기 연결을 생성한다.
    • 예: Tomcat은 org.apache.tomcat.jdbc.pool을 내장 풀로 사용한다.
  2. 연결 할당

    • 애플리케이션은 JNDI(Java Naming and Directory Interface)를 통해 DataSource 객체를 조회한다.
    • 풀에서 유휴 상태의 연결을 가져와 사용한다.
  3. 연결 반환

    • 작업 완료 후 Connection.close() 호출 시 실제로 연결이 종료되지 않고 풀로 반환된다.
  4. 상태 검증

    • validationQuery(예: SELECT 1)를 주기적으로 실행하여 연결의 유효성을 확인한다.
    • 손상된 연결은 자동으로 제거되고 새 연결로 대체된다.
구체적인 설정 예시

Tomcat 기준

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!-- context.xml -->
<Resource 
  name="jdbc/myDB"
  auth="Container"
  type="javax.sql.DataSource"
  driverClassName="com.mysql.cj.jdbc.Driver"
  url="jdbc:mysql://localhost:3306/mydb"
  username="user"
  password="password"
  maxTotal="100"          <!-- 최대 연결  -->
  maxIdle="20"            <!-- 유휴 연결 최대 수 -->
  minIdle="5"             <!-- 유휴 연결 최소 수 -->
  maxWaitMillis="30000"   <!-- 연결 대기 시간(30초) -->
  validationQuery="SELECT 1"
  testOnBorrow="true"     <!-- 사용 전 연결 검증 -->
/>
장점과 단점
구분설명
장점- 서버 통합 관리: WAS의 모니터링 도구와 연동해 실시간 상태 추적 가능
- 간편한 설정: XML/관리 콘솔에서 중앙 집중식 설정
- 안정성: WAS의 안정성 보장 메커니즘(예: 장애 시 자동 재연결) 적용
- 성능 최적화: 서버 레벨에서 연결 풀을 최적화하여 고성능 유지
단점- WAS 종속성: 서버 변경 시 설정 재구성 필요
- 유연성 부족: 외부 라이브러리(HikariCP 등)보다 고급 기능 제한적
- 확장성 제한: 클라우드 환경에서의 탄력적 확장에 어려움
사용 사례
  1. 전통적인 엔터프라이즈 환경
    • JBoss, WebLogic 등에서 JNDI 기반의 풀 관리가 필수적인 경우.
  2. 레거시 시스템 통합
    • 기존 인프라와의 호환성을 유지해야 하는 환경.
  3. 간편한 설정 우선
    • 소규모 애플리케이션에서 별도 라이브러리 도입 없이 빠르게 구성할 때.

구현 예시

Python

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import threading
import queue
import time
import mysql.connector
from typing import Optional, List
from dataclasses import dataclass
import logging

@dataclass
class PooledConnection:
    connection: mysql.connector.MySQLConnection
    last_used: float
    created_at: float
    is_busy: bool = False

class ConnectionPool:
    def __init__(
        self,
        host: str,
        user: str,
        password: str,
        database: str,
        min_connections: int = 5,
        max_connections: int = 10,
        connection_timeout: int = 30,
        idle_timeout: int = 300,
        validation_interval: int = 60
    ):
        # 기본 설정 초기화
        self.db_config = {
            "host": host,
            "user": user,
            "password": password,
            "database": database
        }
        self.min_connections = min_connections
        self.max_connections = max_connections
        self.connection_timeout = connection_timeout
        self.idle_timeout = idle_timeout
        self.validation_interval = validation_interval

        # 연결 풀 및 잠금 장치 초기화
        self.pool: queue.Queue = queue.Queue()
        self.active_connections: List[PooledConnection] = []
        self.lock = threading.Lock()
        self.last_validation = time.time()
        
        # 모니터링을 위한 메트릭 초기화
        self.metrics = {
            "total_connections": 0,
            "active_connections": 0,
            "waiting_requests": 0
        }

        # 로깅 설정
        self.logger = logging.getLogger(__name__)
        
        # 초기 연결 생성
        self._initialize_pool()

    def _initialize_pool(self):
        """초기 연결 풀 생성"""
        for _ in range(self.min_connections):
            self._add_connection()

    def _add_connection(self) -> Optional[PooledConnection]:
        """새로운 데이터베이스 연결 생성"""
        try:
            connection = mysql.connector.connect(**self.db_config)
            pooled_conn = PooledConnection(
                connection=connection,
                last_used=time.time(),
                created_at=time.time()
            )
            self.pool.put(pooled_conn)
            self.active_connections.append(pooled_conn)
            self.metrics["total_connections"] += 1
            return pooled_conn
        except Exception as e:
            self.logger.error(f"연결 생성 실패: {str(e)}")
            return None

    def _validate_connection(self, conn: PooledConnection) -> bool:
        """연결 유효성 검사"""
        try:
            conn.connection.ping(reconnect=True)
            return True
        except:
            return False

    def get_connection(self) -> Optional[PooledConnection]:
        """풀에서 연결 획득"""
        self.metrics["waiting_requests"] += 1
        start_time = time.time()

        # 연결 검증 주기 확인
        if time.time() - self.last_validation > self.validation_interval:
            self._validate_all_connections()

        while True:
            try:
                conn = self.pool.get(timeout=self.connection_timeout)
                
                # 연결 상태 검증
                if not self._validate_connection(conn):
                    self._remove_connection(conn)
                    if len(self.active_connections) < self.max_connections:
                        self._add_connection()
                    continue

                conn.is_busy = True
                conn.last_used = time.time()
                self.metrics["active_connections"] += 1
                self.metrics["waiting_requests"] -= 1
                return conn

            except queue.Empty:
                # 타임아웃 발생 시 새 연결 생성 시도
                if len(self.active_connections) < self.max_connections:
                    new_conn = self._add_connection()
                    if new_conn:
                        continue
                
                if time.time() - start_time > self.connection_timeout:
                    self.logger.error("연결 획득 타임아웃")
                    self.metrics["waiting_requests"] -= 1
                    return None

    def release_connection(self, conn: PooledConnection):
        """연결 반환"""
        conn.is_busy = False
        self.metrics["active_connections"] -= 1
        self.pool.put(conn)

    def _validate_all_connections(self):
        """모든 연결의 유효성 검사"""
        with self.lock:
            self.last_validation = time.time()
            for conn in self.active_connections[:]:
                if not self._validate_connection(conn):
                    self._remove_connection(conn)

    def _remove_connection(self, conn: PooledConnection):
        """손상된 연결 제거"""
        try:
            conn.connection.close()
        except:
            pass
        self.active_connections.remove(conn)
        self.metrics["total_connections"] -= 1

    def cleanup(self):
        """오래된 유휴 연결 정리"""
        current_time = time.time()
        with self.lock:
            for conn in self.active_connections[:]:
                if (not conn.is_busy and 
                    current_time - conn.last_used > self.idle_timeout and 
                    len(self.active_connections) > self.min_connections):
                    self._remove_connection(conn)

    def get_metrics(self):
        """풀 상태 메트릭 반환"""
        return {
            **self.metrics,
            "pool_size": len(self.active_connections),
            "available_connections": self.pool.qsize()
        }

Javascript

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
const mysql = require('mysql2/promise');
const EventEmitter = require('events');

class ConnectionPool extends EventEmitter {
    constructor(config) {
        super();
        // 기본 설정
        this.config = {
            host: config.host,
            user: config.user,
            password: config.password,
            database: config.database,
            minConnections: config.minConnections || 5,
            maxConnections: config.maxConnections || 10,
            connectionTimeout: config.connectionTimeout || 30000,
            idleTimeout: config.idleTimeout || 300000,
            validationInterval: config.validationInterval || 60000
        };

        // 풀 상태 관리
        this.pool = [];
        this.waiting = [];
        this.metrics = {
            totalConnections: 0,
            activeConnections: 0,
            waitingRequests: 0
        };

        // 초기화
        this.initialize();
        
        // 주기적인 작업 설정
        setInterval(() => this.validateConnections(), this.config.validationInterval);
        setInterval(() => this.cleanup(), this.config.idleTimeout);
    }

    async initialize() {
        try {
            // 초기 연결 생성
            for (let i = 0; i < this.config.minConnections; i++) {
                await this.createConnection();
            }
        } catch (error) {
            console.error('풀 초기화 실패:', error);
            throw error;
        }
    }

    async createConnection() {
        try {
            const connection = await mysql.createConnection(this.config);
            
            // 연결 객체 래핑
            const pooledConnection = {
                connection,
                lastUsed: Date.now(),
                createdAt: Date.now(),
                isBusy: false
            };

            this.pool.push(pooledConnection);
            this.metrics.totalConnections++;
            
            // 연결 이벤트 처리
            connection.on('error', async (error) => {
                console.error('연결 오류:', error);
                await this.removeConnection(pooledConnection);
                await this.createConnection();
            });

            return pooledConnection;
        } catch (error) {
            console.error('연결 생성 실패:', error);
            throw error;
        }
    }

    async getConnection() {
        // 대기 요청 추가
        this.metrics.waitingRequests++;
        
        try {
            // 사용 가능한 연결 찾기
            const connection = this.pool.find(conn => !conn.isBusy);
            
            if (connection) {
                return this.prepareConnection(connection);
            }

            // 새 연결 생성 가능 여부 확인
            if (this.pool.length < this.config.maxConnections) {
                const newConnection = await this.createConnection();
                return this.prepareConnection(newConnection);
            }

            // 연결 대기
            return new Promise((resolve, reject) => {
                const timeout = setTimeout(() => {
                    this.waiting = this.waiting.filter(w => w.resolve !== resolve);
                    this.metrics.waitingRequests--;
                    reject(new Error('연결 타임아웃'));
                }, this.config.connectionTimeout);

                this.waiting.push({ resolve, reject, timeout });
            });
        } finally {
            this.metrics.waitingRequests--;
        }
    }

    async prepareConnection(pooledConnection) {
        try {
            // 연결 유효성 검사
            await this.validateConnection(pooledConnection);
            
            pooledConnection.isBusy = true;
            pooledConnection.lastUsed = Date.now();
            this.metrics.activeConnections++;
            
            return pooledConnection;
        } catch (error) {
            await this.removeConnection(pooledConnection);
            throw error;
        }
    }

    async releaseConnection(pooledConnection) {
        pooledConnection.isBusy = false;
        pooledConnection.lastUsed = Date.now();
        this.metrics.activeConnections--;

        // 대기 중인 요청 처리
        if (this.waiting.length > 0) {
            const { resolve, timeout } = this.waiting.shift();
            clearTimeout(timeout);
            resolve(await this.prepareConnection(pooledConnection));
        }
    }

    async validateConnection(pooledConnection) {
        try {
            await pooledConnection.connection.query('SELECT 1');
            return true;
        } catch (error) {
            throw new Error('연결 검증 실패');
        }
    }

    async validateConnections() {
        console.log('연결 유효성 검사 시작');
        for (const connection of this.pool) {
            if (!connection.isBusy) {
                try {
                    await this.validateConnection(connection);
                } catch (error) {
                    await this.removeConnection(connection);
                }
            }
        }
    }

    async removeConnection(pooledConnection) {
        try {
            await pooledConnection.connection.end();
        } catch (error) {
            console.error('연결 종료 실패:', error);
        }
        
        this.pool = this.pool.filter(conn => conn !== pooledConnection);
        this.metrics.totalConnections--;
        
        // 최소 연결 수 유지
        if (this.pool.length < this.config.minConnections) {
            await this.createConnection();
        }
    }

    async cleanup() {
        const now = Date.now();
        const idleConnections = this.pool.filter(
            conn => !conn.isBusy && 
            (now - conn.lastUsed > this.config.idleTimeout) &&
            this.pool.length > this.config.minConnections
        );

        for (const connection of idleConnections) {
            await this.removeConnection(connection);
        }
    }

    getMetrics() {
        return {
            this.metrics,
            poolSize: this.pool.length,
            availableConnections: this.pool.filter(conn => !conn.isBusy).length
        };
    }
}

참고 및 출처