---
title: "connection pool 운영 기준: MySQL과 애플리케이션 사이의 backpressure 설계"
description: "MySQL connection pool 크기, 대기열, timeout, backpressure를 어떻게 설계해야 데이터베이스 과부하와 애플리케이션 장애 전파를 막을 수 있는지 운영 관점에서 정리한다."
tags: [ MySQL, 운영, 성능최적화, 아키텍처, DBA ]
image: "mysql-report-bg.png"
published: "2026-06-25"
updated: "2026-06-25"
author: "MySQL 기술 노트"
source_url: ""
---

## 1. 왜 connection pool 문제를 단순한 성능 튜닝으로 보면 안 되는가

대부분의 서비스는 MySQL 앞단에 애플리케이션 connection pool을 둔다. 이유는 분명하다. 매 요청마다 TCP connect, SSL 협상, authentication, 세션 초기화를 반복하는 비용을 줄이고, 이미 열린 연결을 재사용해 응답 시간을 안정화하기 위해서다. 그러나 운영 현장에서 실제로 더 중요한 질문은 "풀을 쓰는가"가 아니라 **풀을 어디까지 열어 두고, 언제 대기시키고, 언제 실패시키며, 어느 계층에서 압력을 되돌려 보낼 것인가**다.

서비스 장애는 흔히 다음 순서로 진행된다.

1. 애플리케이션 처리량이 증가한다.
2. pool이 더 많은 요청을 동시에 DB로 밀어 넣는다.
3. MySQL에서 `Threads_running`, row lock wait, I/O 대기, commit 지연이 증가한다.
4. 응답 시간이 늘어나면서 애플리케이션의 요청 체류 시간이 길어진다.
5. 같은 처리량을 처리하기 위해 더 많은 worker와 더 많은 pool checkout이 필요해진다.
6. 결국 pool 대기열과 DB 동시성이 함께 폭증한다.

즉, connection pool은 단순한 성능 최적화 장치가 아니라 **애플리케이션과 데이터베이스 사이의 동시성 조절기**다. 이 조절기가 없다면 DB는 짧은 시간 안에 과부하로 무너질 수 있고, 반대로 조절기가 너무 느슨하면 애플리케이션이 DB 병목을 숨긴 채 더 큰 장애를 증폭시킨다.

connection pool 운영의 핵심은 다음 한 문장으로 요약할 수 있다.

> 좋은 pool은 요청을 많이 통과시키는 장치가 아니라, DB가 감당 가능한 동시성까지만 통과시키고 그 이상은 대기·거절·축소하도록 만드는 장치다.

이 문장이 곧 backpressure 설계의 출발점이다.

## 2. connection pool을 backpressure 장치로 이해해야 하는 이유

backpressure는 하위 계층이 처리하지 못하는 속도를 상위 계층에 되돌려 보내는 메커니즘이다. MySQL 운영에서 이 말은 결국 다음 뜻이다.

- DB가 이미 바쁘다면 애플리케이션은 더 많은 동시 요청을 즉시 실행하지 말아야 한다.
- pool checkout이 즉시 되지 않는다면 애플리케이션은 대기 시간을 감수하거나, 빠르게 실패하거나, 상위 호출자에게 재시도를 위임해야 한다.
- timeout과 queue length가 없으면 과부하가 숨겨진 채 누적되고, 결국 더 큰 장애로 터진다.

이 구조를 그림으로 단순화하면 다음과 같다.

```mermaid
flowchart LR
    A[사용자 요청] --> B[애플리케이션 worker / thread / coroutine]
    B --> C{connection pool에 여유 연결이 있는가}
    C -- 예 --> D[MySQL 세션 할당]
    D --> E[SQL 실행]
    E --> F[응답 반환]
    F --> G[연결 반납]
    C -- 아니오 --> H[pool 대기열 진입]
    H --> I{checkout timeout 이전에 연결 확보 성공?}
    I -- 예 --> D
    I -- 아니오 --> J[빠른 실패 / 상위 재시도 / fallback]
    E --> K{DB 응답 지연 증가}
    K --> H
```

이 흐름에서 운영자가 제어할 수 있는 지점은 세 가지다.

1. **동시 실행 상한**: pool size, worker 수, query fan-out 수
2. **대기 정책**: pool checkout queue, queue 길이, queue wait timeout
3. **실패 정책**: fast fail, circuit breaker, retry budget, degraded mode

많은 팀이 pool size만 보고 튜닝을 끝낸다. 그러나 실제 장애는 pool size보다 **대기열과 timeout이 없는 구조**에서 더 자주 난다. 풀 크기만 키우면 단기적으로는 timeout이 줄어들 수 있지만, DB 입장에서는 동시에 처리해야 하는 세션과 작업이 더 늘어나므로 평균 응답 시간과 tail latency가 오히려 나빠질 수 있다.

## 3. MySQL 관점에서 connection pool은 무엇을 바꾸는가

connection pool이 있으면 MySQL 내부에서는 최소한 다음 네 가지가 달라진다.

### 3.1 세션 수명

짧은 연결 모델에서는 요청이 끝나면 세션도 빠르게 사라진다. 반대로 pool 모델에서는 세션이 수분, 수시간, 심하면 수일 이상 유지된다. 이 말은 곧 다음 상태가 오래 남을 수 있다는 뜻이다.

- session variable 변경
- temporary table
- prepared statement
- transaction 미정리 상태
- metadata lock을 유발할 수 있는 장기 세션
- idle in transaction과 유사한 애플리케이션 논리 오류

### 3.2 동시성 형태

pool은 DB에 들어가는 요청 수를 제한할 수도 있지만, 반대로 너무 크게 잡히면 **DB에 허용되는 동시성을 인위적으로 증폭**할 수도 있다. `max_connections`가 1000이라고 해서 1000개 세션이 동시에 유익하게 일하는 것은 아니다. 실제로는 CPU 코어 수, I/O 대기, row lock 경합, redo flush, buffer pool miss, log contention 때문에 효율적인 `Threads_running` 범위가 훨씬 낮을 수 있다.

### 3.3 장애 전파 방식

DB 응답이 느려지면 pool이 이를 흡수하는 대신 장애를 확산시킬 수 있다. 예를 들어 worker 200개, pool 100개, HTTP timeout 30초, pool checkout timeout 30초 구조라면, DB가 느려진 순간 worker 대부분이 DB 대기 상태에 묶인다. 그러면 애플리케이션은 새 요청을 처리하지 못하고 thread starvation 또는 event loop 지연으로 이어질 수 있다.

### 3.4 재연결 폭발 가능성

failover, restart, network flap, Aurora writer 전환 뒤에는 pool이 한꺼번에 broken connection을 버리고 새 연결을 만들려 한다. 이때 pool이 크고 애플리케이션 인스턴스도 많으면 **reconnect storm**가 발생한다. 이 상황에서는 DB CPU보다 handshake/authentication, DNS, TLS, endpoint 전환 감지, prepared statement 재생성 비용이 먼저 문제를 만들기도 한다.

## 4. pool size는 DB 한계가 아니라 서비스 동시성 예산으로 정해야 한다

실무에서 가장 흔한 실수는 다음 공식이다.

> `max_connections`가 600이니 애플리케이션 6대에서 pool 100씩 두면 안전하다.

이 공식은 운영적으로 거의 항상 위험하다. 이유는 간단하다.

- `max_connections`는 최후의 상한이지 권장 동시성 수치가 아니다.
- 여러 애플리케이션, 배치, 관리 세션, 복제/모니터링 세션이 같은 서버를 공유할 수 있다.
- connection이 열린 수와 실제로 동시에 일하는 query 수는 다르지만, pool이 크면 피크 시 둘이 빠르게 가까워질 수 있다.
- 세션 수가 많아지면 메모리, 스케줄링, context switching, metadata 관리 오버헤드가 같이 증가한다.

따라서 pool size는 다음 순서로 정하는 편이 안전하다.

1. MySQL이 안정적으로 감당 가능한 **실효 동시 query 수**를 먼저 추정한다.
2. 그 동시성 예산을 전체 애플리케이션 fleet에 분배한다.
3. pool size는 그 예산보다 약간 보수적으로 잡는다.
4. 애플리케이션 worker 수는 pool보다 무한정 크게 두지 않는다.
5. query fan-out이 있는 API는 논리 요청 1개가 DB 몇 번을 치는지 함께 계산한다.

예를 들어 DB가 `Threads_running` 32 안팎에서 가장 안정적이고, 48을 넘기면 p99 latency가 급격히 악화된다면, pool 총합을 200까지 허용하는 것이 아니라 실제 동시 실행이 그 범위를 넘지 않도록 애플리케이션 구조를 설계해야 한다. 즉, **pool size는 연결 재사용 효율과 장애 완충을 위한 수단이지, DB를 끝까지 밀어붙이기 위한 레버가 아니다.**

## 5. 관측의 출발점: 연결 수와 실행 중 스레드를 분리해서 봐야 한다

connection pool 운영에서는 `Threads_connected`와 `Threads_running`을 혼동하면 안 된다.

- `Threads_connected`: 현재 연결되어 있는 세션 수
- `Threads_running`: 실제로 실행 중이거나 runnable 상태인 스레드 수

pool 환경에서는 `Threads_connected`가 높아도 상당수는 idle일 수 있다. 문제는 `Threads_running`이 지속적으로 높거나, 낮더라도 각 스레드의 응답 시간이 길어져 pool 대기열이 쌓이는 상황이다. 다음 SQL은 가장 기본적인 연결 상태 점검 출발점이다.

```sql
SELECT VERSION() AS mysql_version;

SHOW VARIABLES
WHERE Variable_name IN (
  'max_connections',
  'max_user_connections',
  'thread_cache_size',
  'wait_timeout',
  'interactive_timeout'
);

SHOW GLOBAL STATUS
WHERE Variable_name IN (
  'Threads_connected',
  'Threads_running',
  'Max_used_connections',
  'Connection_errors_max_connections',
  'Aborted_connects',
  'Connections'
);
```

실행 결과(MySQL 8.0.46):

```text
mysql> SELECT VERSION() AS mysql_version;

+---------------+
| mysql_version |
+---------------+
| 8.0.46        |
+---------------+
1 row in set (0.00 sec)

mysql> SHOW VARIABLES
    -> WHERE Variable_name IN (
    ->   'max_connections',
    ->   'max_user_connections',
    ->   'thread_cache_size',
    ->   'wait_timeout',
    ->   'interactive_timeout'
    -> );

+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| interactive_timeout  | 28800 |
| max_connections      | 30    |
| max_user_connections | 0     |
| thread_cache_size    | 8     |
| wait_timeout         | 28800 |
+----------------------+-------+
5 rows in set (0.00 sec)

mysql> SHOW GLOBAL STATUS
    -> WHERE Variable_name IN (
    ->   'Threads_connected',
    ->   'Threads_running',
    ->   'Max_used_connections',
    ->   'Connection_errors_max_connections',
    ->   'Aborted_connects',
    ->   'Connections'
    -> );

+-----------------------------------+-------+
| Variable_name                     | Value |
+-----------------------------------+-------+
| Aborted_connects                  | 0     |
| Connection_errors_max_connections | 0     |
| Connections                       | 11    |
| Max_used_connections              | 1     |
| Threads_connected                 | 1     |
| Threads_running                   | 2     |
+-----------------------------------+-------+
6 rows in set (0.00 sec)
```

이 조회를 읽을 때는 다음 순서를 권장한다.

1. `max_connections` 대비 `Max_used_connections`가 얼마나 가까운가
2. 피크 시 `Threads_running`이 어떤 범위에서 유지되는가
3. `Connection_errors_max_connections`가 실제로 증가하는가
4. `Connections` 증가 속도가 평소보다 빨라지는가
5. `Aborted_connects`가 failover 직후나 배포 직후 튀는가

`Threads_connected`만 보고 “연결이 많으니 문제”라고 결론 내리면 안 된다. pool 환경에서는 idle connection이 많아도 정상일 수 있다. 반대로 `Threads_connected`가 여유 있어 보여도 `Threads_running`과 lock wait이 악화되면 이미 DB는 처리 한계에 도달했을 수 있다.

## 6. Performance Schema로 foreground 세션을 본다는 것은 무엇인가

운영자가 실제 애플리케이션 세션의 모양을 보려면 Performance Schema의 foreground thread를 확인하는 편이 좋다. 다음 SQL은 현재 foreground 세션과 그 세션이 마지막으로 실행한 SQL 텍스트 일부를 함께 보는 기본 예제다.

```sql
SELECT t.PROCESSLIST_ID,
       t.PROCESSLIST_USER,
       t.PROCESSLIST_HOST,
       t.PROCESSLIST_DB,
       t.PROCESSLIST_COMMAND,
       t.PROCESSLIST_TIME,
       LEFT(es.SQL_TEXT, 120) AS current_sql
FROM performance_schema.threads AS t
LEFT JOIN performance_schema.events_statements_current AS es
  ON es.THREAD_ID = t.THREAD_ID
WHERE t.TYPE = 'FOREGROUND'
ORDER BY t.PROCESSLIST_TIME DESC, t.PROCESSLIST_ID DESC
LIMIT 10;
```

실행 결과(MySQL 8.0.46):

```text
mysql> SELECT t.PROCESSLIST_ID,
    ->        t.PROCESSLIST_USER,
    ->        t.PROCESSLIST_HOST,
    ->        t.PROCESSLIST_DB,
    ->        t.PROCESSLIST_COMMAND,
    ->        t.PROCESSLIST_TIME,
    ->        LEFT(es.SQL_TEXT, 120) AS current_sql
    -> FROM performance_schema.threads AS t
    -> LEFT JOIN performance_schema.events_statements_current AS es
    ->   ON es.THREAD_ID = t.THREAD_ID
    -> WHERE t.TYPE = 'FOREGROUND'
    -> ORDER BY t.PROCESSLIST_TIME DESC, t.PROCESSLIST_ID DESC
    -> LIMIT 10;

+----------------+------------------+------------------+-----------------+---------------------+------------------+--------------------------------------------------------------------------------------------------------------------------+
| PROCESSLIST_ID | PROCESSLIST_USER | PROCESSLIST_HOST | PROCESSLIST_DB  | PROCESSLIST_COMMAND | PROCESSLIST_TIME | current_sql                                                                                                              |
+----------------+------------------+------------------+-----------------+---------------------+------------------+--------------------------------------------------------------------------------------------------------------------------+
|              7 | NULL             | NULL             | NULL            | Daemon              |                5 | NULL                                                                                                                     |
|              5 | event_scheduler  | localhost        | NULL            | Daemon              |                5 | NULL                                                                                                                     |
|             12 | root             | localhost        | mysql_tech_note | Query               |                0 | SELECT t.PROCESSLIST_ID,        t.PROCESSLIST_USER,        t.PROCESSLIST_HOST,        t.PROCESSLIST_DB,        t.PROCESS |
+----------------+------------------+------------------+-----------------+---------------------+------------------+--------------------------------------------------------------------------------------------------------------------------+
3 rows in set (0.01 sec)
```

이 쿼리의 해석 포인트는 다음과 같다.

- 같은 애플리케이션 user/host 조합이 과도하게 많은지
- 오래 살아 있는 sleep 세션이 비정상적으로 많은지
- 현재 실행 중 SQL이 특정 API나 배치에 편중되는지
- 장애 시점에 특정 호스트 그룹이 reconnect storm를 일으키는지

단, 테스트 컨테이너처럼 부하가 없는 환경에서는 `root@localhost` 한두 개 세션만 보일 수 있다. 중요한 것은 행 수 자체가 아니라 **운영 환경에서 세션이 어떤 패턴으로 분포하는지**다.

## 7. pool 대기열은 숨겨진 큐가 아니라 명시적으로 관리해야 하는 큐다

애플리케이션이 DB 연결을 즉시 얻지 못할 때 대기가 어디에서 발생하는지 분명해야 한다. 대기 위치는 보통 세 군데 중 하나다.

1. 애플리케이션 worker가 pool checkout에서 대기
2. MySQL 세션은 확보했지만 row lock, I/O, CPU에서 대기
3. 상위 로드밸런서나 메시지 큐에서 이미 대기

가장 위험한 구조는 **어디서 얼마나 대기하는지 모르는 상태**다. 예를 들어 pool 내부 대기열은 무제한인데 HTTP timeout도 길고 재시도도 공격적이라면, DB가 느려졌을 때 사용자 요청은 빠르게 실패하지 않고 시스템 전체에 쌓인다. 이때 보이는 현상은 다음과 비슷하다.

- 애플리케이션 CPU는 높지 않은데 응답 불능처럼 보임
- DB는 완전히 죽지 않았지만 tail latency가 급격히 증가
- worker thread 대부분이 socket read 또는 pool wait에서 묶임
- autoscaling을 해도 각 인스턴스가 더 많은 연결 경쟁만 유발함

따라서 pool queue는 다음 원칙으로 운영하는 편이 좋다.

- queue 길이는 사실상 무한대로 두지 않는다.
- checkout timeout은 상위 요청 timeout보다 충분히 짧게 둔다.
- 대기 후 실패한 요청을 무제한 재시도하지 않는다.
- 동일 요청이 여러 하위 쿼리 fan-out을 만들면 fan-out 자체를 제한한다.

핵심은 **DB가 느려졌을 때 애플리케이션이 스스로 더 느려지는 대신, 빠르게 압력을 인지하고 상위 계층에 신호를 돌려보내게 만드는 것**이다.

## 8. timeout 계층은 일관되게 설계해야 한다

pool 관련 장애에서 자주 보이는 안티패턴은 timeout이 계층마다 제각각인 경우다. 예를 들어 다음처럼 잡혀 있으면 문제가 커진다.

- HTTP timeout: 15초
- pool checkout timeout: 30초
- JDBC socket timeout: 0 또는 매우 김
- MySQL `wait_timeout`: 8시간
- lock wait timeout: 기본값 유지

이 구조에서는 사용자는 15초 뒤 응답을 포기했는데, 애플리케이션 내부 worker는 여전히 pool을 기다리거나 DB 응답을 기다릴 수 있다. 그 결과는 작업 누수와 동시성 고갈이다.

실무적으로는 다음 방향이 더 안전하다.

- 상위 요청 timeout > pool checkout timeout > DB statement timeout 또는 애플리케이션 query timeout
- checkout 실패는 빠르게 관측 가능해야 한다.
- 네트워크 장애 시 socket timeout이 무한대에 가깝지 않아야 한다.
- idle connection 수명과 MySQL `wait_timeout`의 관계를 명확히 이해해야 한다.

다음 SQL은 timeout 관련 기본 변수를 점검하는 가장 안전한 예제다.

```sql
SHOW VARIABLES
WHERE Variable_name IN (
  'wait_timeout',
  'interactive_timeout',
  'connect_timeout',
  'net_read_timeout',
  'net_write_timeout',
  'lock_wait_timeout'
);
```

실행 결과(MySQL 8.0.46):

```text
mysql> SHOW VARIABLES
    -> WHERE Variable_name IN (
    ->   'wait_timeout',
    ->   'interactive_timeout',
    ->   'connect_timeout',
    ->   'net_read_timeout',
    ->   'net_write_timeout',
    ->   'lock_wait_timeout'
    -> );

+---------------------+----------+
| Variable_name       | Value    |
+---------------------+----------+
| connect_timeout     | 10       |
| interactive_timeout | 28800    |
| lock_wait_timeout   | 31536000 |
| net_read_timeout    | 30       |
| net_write_timeout   | 60       |
| wait_timeout        | 28800    |
+---------------------+----------+
6 rows in set (0.00 sec)
```

이 값만으로 애플리케이션 pool timeout을 알 수는 없다. 그러나 적어도 MySQL 측 기본 세션 생존 시간과 네트워크 read/write timeout 경계가 어디에 있는지는 확인할 수 있다. 특히 pool이 오래 idle connection을 재사용하는 구조라면, 애플리케이션의 idle eviction 정책과 MySQL `wait_timeout`이 엇갈려 **죽은 연결을 빌려주는 현상**이 없는지 점검해야 한다.

## 9. 운영 진단 예제: 연결 관련 상태를 묶어 보는 방법

connection pool 문제는 개별 지표 하나보다 여러 지표를 묶어 해석해야 한다. 다음 SQL은 연결, 스레드, 연결 오류, 세션 수명 설정을 한 번에 확인하는 예제다.

```sql
SELECT VERSION() AS mysql_version;

SHOW VARIABLES
WHERE Variable_name IN (
  'max_connections',
  'thread_cache_size',
  'wait_timeout',
  'interactive_timeout',
  'connect_timeout'
);

SHOW GLOBAL STATUS
WHERE Variable_name IN (
  'Threads_connected',
  'Threads_running',
  'Max_used_connections',
  'Connection_errors_max_connections',
  'Aborted_connects',
  'Connections',
  'Threads_created'
);

SELECT t.PROCESSLIST_COMMAND,
       COUNT(*) AS session_count
FROM performance_schema.threads AS t
WHERE t.TYPE = 'FOREGROUND'
GROUP BY t.PROCESSLIST_COMMAND
ORDER BY session_count DESC, t.PROCESSLIST_COMMAND;
```

실행 결과(MySQL 8.0.46):

```text
mysql> SELECT VERSION() AS mysql_version;

+---------------+
| mysql_version |
+---------------+
| 8.0.46        |
+---------------+
1 row in set (0.00 sec)

mysql> SHOW VARIABLES
    -> WHERE Variable_name IN (
    ->   'max_connections',
    ->   'thread_cache_size',
    ->   'wait_timeout',
    ->   'interactive_timeout',
    ->   'connect_timeout'
    -> );

+---------------------+-------+
| Variable_name       | Value |
+---------------------+-------+
| connect_timeout     | 10    |
| interactive_timeout | 28800 |
| max_connections     | 30    |
| thread_cache_size   | 8     |
| wait_timeout        | 28800 |
+---------------------+-------+
5 rows in set (0.00 sec)

mysql> SHOW GLOBAL STATUS
    -> WHERE Variable_name IN (
    ->   'Threads_connected',
    ->   'Threads_running',
    ->   'Max_used_connections',
    ->   'Connection_errors_max_connections',
    ->   'Aborted_connects',
    ->   'Connections',
    ->   'Threads_created'
    -> );

+-----------------------------------+-------+
| Variable_name                     | Value |
+-----------------------------------+-------+
| Aborted_connects                  | 0     |
| Connection_errors_max_connections | 0     |
| Connections                       | 14    |
| Max_used_connections              | 1     |
| Threads_connected                 | 1     |
| Threads_created                   | 1     |
| Threads_running                   | 2     |
+-----------------------------------+-------+
7 rows in set (0.00 sec)

mysql> SELECT t.PROCESSLIST_COMMAND,
    ->        COUNT(*) AS session_count
    -> FROM performance_schema.threads AS t
    -> WHERE t.TYPE = 'FOREGROUND'
    -> GROUP BY t.PROCESSLIST_COMMAND
    -> ORDER BY session_count DESC, t.PROCESSLIST_COMMAND;

+---------------------+---------------+
| PROCESSLIST_COMMAND | session_count |
+---------------------+---------------+
| Daemon              |             2 |
| Query               |             1 |
+---------------------+---------------+
2 rows in set (0.00 sec)
```

이 결과는 다음 질문에 답하기 위한 출발점이다.

- 세션 수가 많은데 대부분 `Sleep`인가, 아니면 실제 작업 중인가
- `Threads_created`가 비정상적으로 빨리 증가하는가
- `Connection_errors_max_connections`가 발생할 만큼 상한에 닿고 있는가
- `wait_timeout`이 pool 설계보다 너무 길거나 짧지 않은가

테스트 컨테이너에서는 `Threads_running`과 `Threads_created`가 매우 낮고 `Sleep` 세션도 거의 없을 수 있다. 이것은 이상이 아니라, **운영 해석을 위한 쿼리의 구조 검증**이라는 점을 의미한다.

## 10. 실무에서 pool size를 키우기 전에 먼저 물어야 할 질문

장애가 발생하면 흔히 "timeout이 나니 pool을 키우자"는 제안이 나온다. 그러나 다음 질문에 답하기 전에는 pool 확장이 해법이 아닐 가능성이 높다.

1. 현재 timeout은 pool checkout 대기 때문인가, DB query 실행 지연 때문인가
2. `Threads_running`이 이미 높은데 pool만 키우면 더 나아질 근거가 있는가
3. 느린 API가 fan-out query를 과도하게 만들고 있지 않은가
4. transaction 범위가 지나치게 길어 연결 점유 시간이 늘고 있지 않은가
5. 애플리케이션 worker 수가 pool보다 지나치게 커서 DB 병목을 증폭하고 있지 않은가
6. failover 뒤 reconnect storm를 완화할 jitter, warm-up, rate limit가 있는가

대부분의 경우 pool 증설은 근본 원인을 해결하지 않는다. 오히려 다음 문제를 악화시킬 수 있다.

- lock wait 동시성 증가
- buffer pool miss 경쟁 증가
- redo flush와 commit 경쟁 증가
- 세션당 메모리 사용량 증가
- 장애 시 더 큰 reconnect burst

즉, pool 확대는 **DB가 여전히 여유가 있는데 애플리케이션이 보수적으로 막고 있는 상황**에서만 유효하다. DB 자체가 이미 포화라면 pool 확대는 혼잡을 더 깊게 만들 뿐이다.

## 11. Aurora MySQL에서는 무엇이 달라지는가

Aurora MySQL도 애플리케이션 입장에서는 MySQL protocol 세션을 사용하지만, 운영 해석에는 몇 가지 차이가 있다.

### 11.1 failover 체감 방식

Aurora는 스토리지 계층과 compute 계층이 분리되어 있어 restart와 failover의 모양이 Community MySQL 단일 인스턴스와 다를 수 있다. 그러나 애플리케이션 pool 입장에서는 여전히 다음이 중요하다.

- writer endpoint 전환을 얼마나 빨리 감지하는가
- DNS TTL과 connector 재해석 방식이 어떠한가
- broken connection을 얼마나 공격적으로 다시 여는가
- failover 직후 모든 인스턴스가 동시에 warm-up을 시작하는가

즉, Aurora라고 해서 pool/backpressure 설계가 덜 중요해지지 않는다. 오히려 **endpoint 전환과 재연결 폭발을 어떻게 제어하는가**가 더 중요해질 수 있다.

### 11.2 읽기 확장과 쓰기 병목의 분리

Aurora reader endpoint를 사용하면 read pool과 write pool을 분리할 수 있다. 이 경우에도 단순히 reader 수가 많다고 read-side pool을 무한정 키우면 안 된다. 각 reader의 실효 동시성 예산과 replica lag, 캐시 warm-up 상태를 함께 봐야 한다. write path는 결국 writer 인스턴스 한 곳에 집중되므로, write pool에서 backpressure를 제대로 걸지 못하면 애플리케이션 전체 장애로 이어질 수 있다.

### 11.3 프록시 계층의 영향

RDS Proxy, ProxySQL 같은 중간 계층을 두면 pool 운영 양상이 달라진다. 세션 재사용, multiplexing 가능 여부, transaction pinning, prepared statement와 session variable 유지 방식에 따라 애플리케이션 pool이 기대하는 동작과 실제 DB 세션 수가 달라질 수 있다. 따라서 "프록시가 있으니 pool을 크게 둬도 된다"라고 단정하면 위험하다. 프록시 역시 또 하나의 큐와 backpressure 지점이기 때문이다.

## 12. 흔한 오해와 실패 패턴

### 12.1 `max_connections`까지 쓰지 않았으니 안전하다는 오해

실제로는 `Threads_running`, lock wait, I/O 지연, commit latency가 먼저 악화될 수 있다. 연결 상한에 아직 닿지 않았다는 사실은 과부하 부재를 의미하지 않는다.

### 12.2 pool이 크면 spike를 흡수한다는 오해

짧은 순간의 스파이크는 일부 흡수할 수 있다. 그러나 DB가 지속적으로 느려진 상태에서는 큰 pool이 더 많은 동시성을 밀어 넣어 tail latency를 악화시킬 수 있다.

### 12.3 timeout을 길게 두면 사용자 실패가 줄어든다는 오해

실패 시점을 늦출 뿐 시스템 전체 체류 시간을 늘리고, worker를 오래 붙잡아 더 큰 장애를 만든다. 운영적으로는 **적절히 짧은 timeout과 명시적 실패**가 더 안전하다.

### 12.4 재시도는 항상 복원력을 높인다는 오해

backpressure가 없는 재시도는 장애 중인 DB에 추가 부하를 주는 경우가 많다. retry budget, exponential backoff, jitter가 없으면 재시도는 복구가 아니라 증폭 장치다.

### 12.5 idle connection은 비용이 없다는 오해

idle 세션도 서버 메모리, 세션 상태, 관리 오버헤드를 가진다. 더 중요한 것은 idle 세션이 많다는 사실이 **애플리케이션이 필요 이상으로 큰 동시성 예산을 잡고 있다**는 신호일 수 있다는 점이다.

## 13. 운영 체크리스트

- [ ] DB가 안정적으로 감당 가능한 `Threads_running` 범위를 관측 기반으로 알고 있다.
- [ ] 애플리케이션 전체 인스턴스 수를 합친 pool 총합이 서비스 동시성 예산과 연결되어 있다.
- [ ] pool checkout timeout이 상위 요청 timeout보다 짧다.
- [ ] pool 대기열이 사실상 무한대가 아니며, queue saturation 시 실패 전략이 정의되어 있다.
- [ ] 재시도에는 횟수 제한, backoff, jitter가 있다.
- [ ] failover 뒤 reconnect storm를 완화할 warm-up 또는 rate limit 전략이 있다.
- [ ] `Threads_connected`, `Threads_running`, `Max_used_connections`, `Connections`, `Aborted_connects`를 함께 본다.
- [ ] `performance_schema.threads`로 foreground 세션 분포를 확인할 수 있다.
- [ ] 장기 세션, 미정리 transaction, prepared statement 누적이 pool 환경에서 어떻게 남는지 이해하고 있다.
- [ ] Aurora MySQL 사용 시 writer/reader endpoint, failover, 프록시 계층이 pool 설계에 미치는 영향을 점검했다.

## 14. 결론

connection pool은 단순한 연결 재사용 장치가 아니다. 그것은 애플리케이션과 MySQL 사이의 **동시성 제한기**, **대기열 제어기**, **장애 전파 억제기**, 그리고 잘못 설계하면 **장애 증폭기**다. 운영 관점에서 중요한 것은 pool을 크게 두는 기술이 아니라, DB가 감당 가능한 처리율을 기준으로 상위 계층의 속도를 조절하는 기술이다.

실무에서는 `max_connections`보다 `Threads_running`의 안정 범위, 평균 응답 시간보다 tail latency, 단일 timeout보다 timeout 계층의 정렬, 그리고 연결 수보다 **backpressure가 실제로 작동하는가**를 먼저 봐야 한다.

다음 연결 관리 주제에서는 transaction 길이, lock wait, prepared statement, session state가 connection pool 환경에서 어떻게 증폭되거나 숨겨지는지를 더 구체적으로 연결해 볼 수 있다.
