카테고리 : MySQL/기술노트

prepared statement와 statement cache: 애플리케이션 연결 풀과의 관계

MySQL prepared statement와 드라이버 statement cache가 연결 풀과 결합될 때 어떤 메모리·세션·장애 특성이 생기는지 운영 관점에서 정리한다.

저자: MySQL 기술 노트 작성: 2026.06.23 약 15분 8,899자
다운로드

1. 왜 prepared statement와 statement cache를 연결 풀과 함께 봐야 하는가

많은 팀이 prepared statement를 이야기할 때 다음 두 문장을 거의 자동으로 붙인다.

  • prepared statement를 쓰면 SQL 파싱 비용이 줄어든다.
  • statement cache를 켜면 애플리케이션 성능이 좋아진다.

이 말은 방향으로는 맞지만, 운영 해석으로는 절반만 맞다. 실제 서비스에서는 prepared statement가 단독으로 존재하지 않고, 거의 항상 드라이버의 statement cache애플리케이션 connection pool 위에서 동작하기 때문이다. 이 세 요소가 결합되면 단순히 “한 쿼리가 빨라진다” 수준을 넘어, 서버 메모리 사용량, 세션 수명, failover 뒤 재연결 패턴, max_prepared_stmt_count 고갈 위험까지 함께 바뀐다.

핵심은 다음과 같다.

prepared statement는 세션 단위 자원이고, connection pool은 세션을 오래 살려 두며, statement cache는 그 세션 안에 prepared statement를 계속 남겨 둘 수 있다.

즉, 풀링이 없는 짧은 연결 환경에서는 금방 사라질 자원이, 풀링 환경에서는 수시간 또는 수일 동안 유지될 수 있다. 그래서 prepared statement 문제는 SQL 최적화 문제이면서 동시에 세션 수명 관리 문제이기도 하다.

운영 현장에서 자주 나타나는 오해는 다음과 같다.

  1. prepared statement는 애플리케이션 메모리 안에서만 캐시된다고 생각한다.
  2. connection pool이 재사용하는 것은 TCP 연결뿐이고, 서버 쪽 상태는 매번 깨끗하다고 가정한다.
  3. statement cache hit가 높으면 무조건 좋은 일이라고 본다.
  4. Prepared_stmt_count 상승을 단순 트래픽 증가로만 해석한다.

실제로는 그렇지 않다. server-side prepared statement를 쓰는 드라이버라면, 물리 연결 하나마다 MySQL 서버 안에 별도의 prepared statement 객체가 생길 수 있다. 그리고 그 연결이 pool 안에서 오래 살아 있으면 prepared statement도 같이 남는다. 여기에 statement cache가 크고 pool 크기도 크면, 애플리케이션이 의도하지 않아도 서버에 상당한 수의 prepared statement가 장기간 유지될 수 있다.

2. 먼저 용어를 분리해야 한다: prepared statement와 statement cache는 같은 것이 아니다

이 주제를 혼동 없이 이해하려면 먼저 세 가지 층을 분리해야 한다.

2.1 server-side prepared statement

MySQL 서버에 PREPARE가 등록되거나, 드라이버가 binary protocol 기반 prepared statement를 생성하면, 서버는 해당 세션에 속한 prepared statement를 보관한다. 이 객체는 대략 다음 정보를 가진다.

  • 원본 SQL 텍스트
  • 파라미터 메타데이터
  • 실행에 필요한 내부 구조 일부
  • 소유 세션 정보
  • 실행 횟수와 관련 관측 정보

중요한 점은 prepared statement가 전역 공유 객체가 아니라 세션 소유 객체라는 사실이다. 같은 SQL이라도 연결이 100개면 prepared statement도 최대 100개까지 따로 생길 수 있다.

2.2 client-side statement cache

드라이버나 ORM은 이미 준비한 statement handle을 애플리케이션 쪽에서 재사용하기 위해 캐시를 둔다. 여기에는 두 가지 방향이 섞일 수 있다.

  1. SQL 텍스트를 기준으로 driver 객체만 캐시하고, 실제 서버 prepared statement는 매번 다시 만들거나 필요 시에만 유지하는 방식
  2. 아예 server-side prepared statement 핸들을 연결별로 유지해 두고 재사용하는 방식

즉, statement cache라는 말 하나만 보고 “서버 자원이 늘지 않는다” 또는 “늘어난다”라고 단정하면 안 된다. 드라이버가 client-side only cache를 하는지, server-side prepared statement를 함께 붙잡는지를 구분해야 한다.

2.3 connection pool

connection pool은 논리적 요청보다 적은 수의 물리 연결을 재사용해 connect/authenticate 비용을 줄인다. 여기서 중요한 운영 사실은 다음과 같다.

  • prepared statement는 요청 단위가 아니라 물리 연결 단위로 남는다.
  • 따라서 pool이 연결을 오래 유지할수록 prepared statement도 오래 남을 수 있다.
  • pool size가 곧 prepared statement 보관 용량의 곱셈 계수로 작동한다.

이 세 층을 합쳐 보면 prepared statement 문제는 다음 수식으로 생각하는 편이 좋다.

대략적인 prepared statement 상한 ≈ 물리 연결 수 × 연결당 유지되는 statement 수

물론 실제로는 모든 연결이 동일한 SQL 집합을 갖지 않지만, 운영 상한을 추정할 때는 이 보수적 사고가 안전하다.

3. 내부 메커니즘: 요청 하나가 prepared statement가 되기까지

아래 흐름은 애플리케이션이 parameterized query를 보낼 때 흔히 벌어지는 과정을 단순화한 것이다.

flowchart TD
    A[애플리케이션 SQL 요청] --> B[드라이버/ORM이 SQL 템플릿 식별]
    B --> C{연결별 statement cache hit 인가}
    C -- 예 --> D[기존 statement handle 재사용]
    C -- 아니오 --> E{server-side prepared statement 사용 설정인가}
    E -- 예 --> F[MySQL 세션에 prepared statement 생성]
    E -- 아니오 --> G[client-side 처리 후 일반 실행]
    F --> H[연결별 cache에 handle 보관 가능]
    D --> I[EXECUTE 또는 binary protocol 실행]
    H --> I
    G --> J[일반 text protocol 실행]
    I --> K[요청 종료]
    J --> K
    K --> L{물리 연결을 pool에 반납하는가}
    L -- 예 --> M[prepared statement가 세션과 함께 유지될 수 있음]
    L -- 아니오 --> N[연결 종료와 함께 statement 정리]

이 그림에서 운영자가 특히 주의해야 할 지점은 pool에 반납 이후다. 많은 애플리케이션 개발자는 요청이 끝났으니 statement도 사라졌다고 생각하지만, 실제로는 논리적 요청만 끝났을 뿐 물리 연결은 살아 있고, 따라서 prepared statement도 그대로 남아 있을 수 있다.

4. 왜 connection pool이 prepared statement 문제를 증폭시키는가

4.1 prepared statement는 연결 간 공유되지 않는다

MySQL prepared statement는 세션 로컬 객체다. 따라서 동일한 SQL을 서비스 전체에서 1만 번 사용하더라도, 그 실행이 100개의 물리 연결에 분산되면 연결별로 독립적인 prepared statement가 만들어질 수 있다.

이 특성 때문에 다음 현상이 발생한다.

  • pool size가 10일 때는 문제가 없던 서비스가, 100으로 늘어난 뒤 Prepared_stmt_count가 급격히 상승한다.
  • statement cache 크기를 25에서 200으로 늘렸더니 평균 latency는 소폭 줄었지만 서버 메모리 사용량과 세션 잔존 상태가 크게 늘어난다.
  • SQL 템플릿 종류가 많은 ORM 기반 서비스는 풀의 각 연결이 서로 다른 statement 집합을 들고 있게 된다.

즉, prepared statement는 CPU 절감 장치이기도 하지만, 동시에 연결별 상태 증식 장치이기도 하다.

4.2 pool이 연결을 오래 살리면 statement도 오래 산다

짧은 연결을 매번 열고 닫는 구조라면 prepared statement는 세션 종료와 함께 빠르게 정리된다. 반대로 HikariCP, c3p0, DBCP, SQLAlchemy pool, Go database/sql 같은 풀링 구조에서는 연결이 오랫동안 재사용된다. 그러면 과거 특정 시점에 준비된 statement가 트래픽 패턴이 바뀐 뒤에도 계속 남을 수 있다.

이것은 다음과 같은 운영 증상으로 이어진다.

  • 배치 시간대에만 쓰던 statement가 낮 시간에도 풀 안에서 남아 있음
  • 특정 기능이 한 번 실행된 뒤 잘 사용되지 않아도 cache eviction 전까지 statement가 유지됨
  • 재배포 이후 새 SQL 템플릿이 추가되면서 연결별 statement 종류가 누적됨

따라서 statement cache는 단순 LRU 튜닝이 아니라, 애플리케이션의 SQL 다양성, 풀의 연결 생존 시간, traffic phase 변화를 함께 고려해야 한다.

4.3 failover나 reconnect storm에서는 반대로 statement가 한꺼번에 사라지고 다시 만들어진다

prepared statement는 세션 로컬 상태이므로, failover나 네트워크 단절로 물리 연결이 끊기면 모두 사라진다. 그 뒤 pool이 새 연결을 대량으로 만들면 애플리케이션은 statement를 다시 prepare해야 한다. 이때 나타나는 현상은 다음과 같다.

  • failover 직후 latency spike
  • Com_stmt_prepare 급증
  • warm-up이 끝나기 전까지 CPU 사용량 증가
  • connection storm와 statement reprepare가 함께 발생

즉, 평상시에는 “너무 많이 남는” 문제가 생기고, 장애 직후에는 “모두 사라져서 다시 만들어야 하는” 문제가 생긴다. prepared statement와 pool의 관계는 정상 시와 장애 시의 모습이 정반대로 바뀔 수 있다는 점이 중요하다.

5. 관측 포인트: MySQL에서 무엇을 봐야 하는가

prepared statement 관련 진단에서 가장 먼저 봐야 할 것은 다음 세 가지다.

  1. 서버가 허용하는 총 prepared statement 상한: max_prepared_stmt_count
  2. 현재 서버에 살아 있는 prepared statement 수: Prepared_stmt_count
  3. prepare/execute/close 활동량: Com_stmt_prepare, Com_stmt_execute, Com_stmt_close, Com_stmt_reprepare

다음 SQL은 가장 기본적인 상태 확인 예제다.

SELECT VERSION() AS mysql_version;

SHOW VARIABLES LIKE 'max_prepared_stmt_count';

SHOW GLOBAL STATUS
WHERE Variable_name IN (
  'Prepared_stmt_count',
  'Com_stmt_prepare',
  'Com_stmt_execute',
  'Com_stmt_close',
  'Com_stmt_reprepare'
);

실행 결과(MySQL 8.0.46):

mysql> SELECT VERSION() AS mysql_version;

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

mysql> SHOW VARIABLES LIKE 'max_prepared_stmt_count';

+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| max_prepared_stmt_count | 16382 |
+-------------------------+-------+
1 row in set (0.00 sec)

mysql> SHOW GLOBAL STATUS
    -> WHERE Variable_name IN (
    ->   'Prepared_stmt_count',
    ->   'Com_stmt_prepare',
    ->   'Com_stmt_execute',
    ->   'Com_stmt_close',
    ->   'Com_stmt_reprepare'
    -> );

+---------------------+-------+
| Variable_name       | Value |
+---------------------+-------+
| Com_stmt_close      | 0     |
| Com_stmt_execute    | 0     |
| Com_stmt_prepare    | 0     |
| Com_stmt_reprepare  | 0     |
| Prepared_stmt_count | 0     |
+---------------------+-------+
5 rows in set (0.00 sec)

Prepared_stmt_count는 지금 살아 있는 prepared statement의 재고에 가깝고, Com_stmt_prepare/Com_stmt_execute는 누적 활동량에 가깝다. 따라서 다음처럼 읽는 편이 좋다.

  • Com_stmt_execute는 높은데 Com_stmt_prepare는 상대적으로 낮다 → 재사용이 잘 일어날 가능성
  • Com_stmt_prepare가 지나치게 빠르게 증가한다 → prepare churn이 큼, cache 효율이 낮거나 연결 재생성이 많을 가능성
  • Prepared_stmt_count가 계속 높은데 Com_stmt_close가 잘 늘지 않는다 → 연결 수명 또는 cache retention이 길 수 있음
  • Com_stmt_reprepare가 의미 있게 보인다 → metadata invalidation, DDL 영향, 테이블 정의 변경 뒤 재준비가 발생했을 가능성

6. 기본 진단 SQL을 어떻게 읽어야 하는가

위 기본 진단 SQL은 단순 조회처럼 보이지만, 연결 풀과 함께 해석할 때 의미가 달라진다.

  • Prepared_stmt_count는 지금 서버에 남아 있는 prepared statement 재고다.
  • Com_stmt_prepare는 얼마나 자주 새 statement를 준비하는지 보여 준다.
  • Com_stmt_execute는 준비된 statement가 실제로 얼마나 반복 실행되는지 보여 준다.
  • Com_stmt_close는 서버 쪽 statement 정리가 얼마나 일어나는지 짐작하게 해 준다.
  • Com_stmt_reprepare는 DDL이나 metadata invalidation 영향으로 재준비가 발생하는지 판단하는 힌트다.

운영 해석의 요점은 “숫자 하나의 절대값”보다 시간에 따른 변화율연결 수 변화와의 상관관계를 함께 보는 데 있다. 예를 들어 배포 직후, autoscaling 직후, failover 직후, 배치 시작 직후에 이 값들을 비교하면 prepared statement가 구조적으로 누적되는지, 아니면 단순 warm-up 후 안정화되는지 구분하기 쉽다.

7. prepared statement 생성과 해제를 직접 확인하는 예제

다음 예제는 MySQL 세션 안에서 prepared statement를 만들고 실행한 뒤, performance_schema.prepared_statements_instances에서 현재 살아 있는 객체를 확인하는 가장 직접적인 방법이다.

PREPARE stmt1 FROM 'SELECT ? + ? AS sum_value';
SET @a = 2, @b = 5;
EXECUTE stmt1 USING @a, @b;

SELECT STATEMENT_NAME, SQL_TEXT, COUNT_EXECUTE
FROM performance_schema.prepared_statements_instances;

DEALLOCATE PREPARE stmt1;

SELECT COUNT(*) AS remaining_prepared
FROM performance_schema.prepared_statements_instances;

실행 결과(MySQL 8.0.46):

mysql> PREPARE stmt1 FROM 'SELECT ? + ? AS sum_value';

Query OK, 0 rows affected (0.00 sec)
Statement prepared

mysql> SET @a = 2, @b = 5;

Query OK, 0 rows affected (0.00 sec)

mysql> EXECUTE stmt1 USING @a, @b;

+-----------+
| sum_value |
+-----------+
|         7 |
+-----------+
1 row in set (0.00 sec)

mysql> SELECT STATEMENT_NAME, SQL_TEXT, COUNT_EXECUTE
    -> FROM performance_schema.prepared_statements_instances;

+----------------+---------------------------+---------------+
| STATEMENT_NAME | SQL_TEXT                  | COUNT_EXECUTE |
+----------------+---------------------------+---------------+
| stmt1          | SELECT ? + ? AS sum_value |             1 |
+----------------+---------------------------+---------------+
1 row in set (0.00 sec)

mysql> DEALLOCATE PREPARE stmt1;

Query OK, 0 rows affected (0.00 sec)

mysql> SELECT COUNT(*) AS remaining_prepared
    -> FROM performance_schema.prepared_statements_instances;

+--------------------+
| remaining_prepared |
+--------------------+
|                  0 |
+--------------------+
1 row in set (0.01 sec)

이 결과가 중요한 이유는 prepared statement가 단순 개념이 아니라 실제로 세션 안에 살아 있는 객체라는 점을 보여 주기 때문이다. 실행 후 COUNT_EXECUTE가 증가하고, DEALLOCATE PREPARE 뒤에는 목록에서 사라진다. 드라이버가 server-side prepared statement를 사용한다면 애플리케이션 풀 연결 안에서도 같은 현상이 연결별로 벌어진다고 이해하면 된다.

8. 연결 하나에 statement를 여러 개 붙잡으면 어떤 일이 생기는가

다음 예제는 prepared statement가 연결 하나 안에서도 여러 개 누적될 수 있다는 점을 보여 준다.

SHOW VARIABLES LIKE 'max_prepared_stmt_count';
SHOW GLOBAL STATUS LIKE 'Prepared_stmt_count';

PREPARE stmt_a FROM 'SELECT 1';
PREPARE stmt_b FROM 'SELECT 2';

SHOW GLOBAL STATUS LIKE 'Prepared_stmt_count';

SELECT STATEMENT_NAME, SQL_TEXT
FROM performance_schema.prepared_statements_instances
ORDER BY STATEMENT_NAME;

DEALLOCATE PREPARE stmt_a;
DEALLOCATE PREPARE stmt_b;

SHOW GLOBAL STATUS LIKE 'Prepared_stmt_count';

실행 결과(MySQL 8.0.46):

mysql> SHOW VARIABLES LIKE 'max_prepared_stmt_count';

+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| max_prepared_stmt_count | 16382 |
+-------------------------+-------+
1 row in set (0.00 sec)

mysql> SHOW GLOBAL STATUS LIKE 'Prepared_stmt_count';

+---------------------+-------+
| Variable_name       | Value |
+---------------------+-------+
| Prepared_stmt_count | 0     |
+---------------------+-------+
1 row in set (0.01 sec)

mysql> PREPARE stmt_a FROM 'SELECT 1';

Query OK, 0 rows affected (0.00 sec)
Statement prepared

mysql> PREPARE stmt_b FROM 'SELECT 2';

Query OK, 0 rows affected (0.00 sec)
Statement prepared

mysql> SHOW GLOBAL STATUS LIKE 'Prepared_stmt_count';

+---------------------+-------+
| Variable_name       | Value |
+---------------------+-------+
| Prepared_stmt_count | 2     |
+---------------------+-------+
1 row in set (0.00 sec)

mysql> SELECT STATEMENT_NAME, SQL_TEXT
    -> FROM performance_schema.prepared_statements_instances
    -> ORDER BY STATEMENT_NAME;

+----------------+----------+
| STATEMENT_NAME | SQL_TEXT |
+----------------+----------+
| stmt_a         | SELECT 1 |
| stmt_b         | SELECT 2 |
+----------------+----------+
2 rows in set (0.00 sec)

mysql> DEALLOCATE PREPARE stmt_a;

Query OK, 0 rows affected (0.00 sec)

mysql> DEALLOCATE PREPARE stmt_b;

Query OK, 0 rows affected (0.00 sec)

mysql> SHOW GLOBAL STATUS LIKE 'Prepared_stmt_count';

+---------------------+-------+
| Variable_name       | Value |
+---------------------+-------+
| Prepared_stmt_count | 0     |
+---------------------+-------+
1 row in set (0.00 sec)

이 단일 세션 예제는 단순하지만 운영적으로 매우 중요하다. 애플리케이션 풀에 물리 연결이 50개 있고, 연결당 statement cache가 평균 40개를 유지한다면 이론상 2000개의 prepared statement가 동시에 살아 있을 수 있다. 실제 운영에서는 SQL 종류 편차와 eviction 때문에 정확히 맞지는 않지만, 상한 추정은 이런 식으로 계산해야 한다.

9. statement cache를 어떻게 해석해야 하는가

9.1 cache hit가 높다고 무조건 좋은 것은 아니다

statement cache hit가 높다는 것은 재사용 이득이 있다는 뜻일 수 있다. 그러나 다음 반대 해석도 함께 가능하다.

  • 오래된 SQL 템플릿이 cache를 점유하고 있다.
  • 거의 쓰지 않는 statement가 연결별로 잔존한다.
  • 메모리 사용량과 서버 prepared statement 재고를 늘리는 대가를 치르고 있다.

즉, hit ratio 하나만 보고 크기를 늘리는 것은 위험하다. latency 절감 폭서버 자원 점유 증가 폭을 같이 봐야 한다.

9.2 ORM과 동적 SQL이 많으면 cache 품질이 급격히 나빠질 수 있다

같은 구조의 SQL을 parameterized template로 보내지 않고, 리터럴을 섞어 동적으로 조립하면 cache 효율이 급격히 떨어진다. 예를 들어 다음 두 경우는 운영 결과가 매우 다르다.

  • 좋은 패턴: SELECT * FROM orders WHERE id = ?
  • 나쁜 패턴: SELECT * FROM orders WHERE id = 1001, ...1002, ...1003를 매번 다른 SQL 텍스트로 보냄

후자의 경우는 prepared statement나 statement cache를 켜도 SQL 템플릿 수가 불필요하게 폭증한다. 결국 pool 연결마다 다양한 SQL 텍스트가 누적되어 캐시가 빨리 오염된다.

9.3 cache 크기는 “많을수록 좋다”가 아니라 “실제 hot set보다 조금 큰 정도”가 목표다

운영적으로 이상적인 cache는 모든 SQL을 보관하는 cache가 아니라, 짧은 시간 안에 반복 호출되는 hot statement 집합만 안정적으로 잡아 두는 cache다. 따라서 cache size를 정할 때는 다음을 함께 고려해야 한다.

  • 애플리케이션의 SQL 템플릿 종류 수
  • hot path SQL의 비중
  • 배치성 일회성 SQL의 비중
  • 물리 pool size
  • 서버 max_prepared_stmt_count 여유율

10. 장애와 오해: prepared statement가 문제일 때 흔히 나타나는 패턴

10.1 max_prepared_stmt_count 초과

가장 명확한 실패 모드는 서버 상한 초과다. 이때는 새 prepared statement 생성이 실패하고 애플리케이션 오류가 발생한다. 원인은 대부분 다음 조합 중 하나다.

  • pool size 확대
  • statement cache 확대
  • SQL 템플릿 수 증가
  • 연결 누수 또는 정리 누락
  • 장애 뒤 warm-up 과정에서 빠른 재준비 폭주

이 문제를 단순히 max_prepared_stmt_count 상향으로만 해결하면, 일시적으로는 버틸 수 있어도 더 큰 메모리 점유와 장애 반경을 만들 수 있다. 먼저 왜 연결당 statement 수가 커졌는지를 봐야 한다.

10.2 prepare churn이 큰데 cache가 없는 줄 착각함

드라이버는 cache를 켰더라도, pool recycle, connection max lifetime, 서버 disconnect, failover 때문에 prepared statement를 자주 다시 만들 수 있다. 이 경우 개발팀은 “우리는 cache를 켰는데 왜 Com_stmt_prepare가 계속 늘지?”라고 묻는다. 실제 원인은 cache 부재가 아니라 연결 생존 시간이 짧거나 자주 교체되고 있기 때문일 수 있다.

10.3 DDL이 잦은 환경에서는 reprepare 비용을 무시하면 안 된다

prepared statement는 참조 객체의 메타데이터가 바뀌면 재준비가 필요할 수 있다. 지속적 스키마 변경, 온라인 DDL, 임시 테이블 패턴이 많은 환경에서는 Com_stmt_reprepare가 의미 있는 신호가 된다. 실행 수가 많을수록 이 비용은 조용히 누적된다.

10.4 메모리 문제를 SQL 개수 문제로만 오판함

prepared statement 자체는 보통 행 데이터 캐시처럼 거대한 메모리를 쓰는 객체는 아니다. 그러나 다음 조합에서 총량이 문제로 바뀐다.

  • 풀 연결이 많음
  • SQL 템플릿 종류가 많음
  • statement cache retention이 김
  • 애플리케이션 인스턴스 수가 많음
  • writer/reader endpoint에 동시에 풀을 유지함

즉, 개별 객체는 작아 보여도 연결 수 × SQL 종류 수 × 인스턴스 수가 되면 서버 전체에서는 충분히 큰 상태 집합이 된다.

11. Aurora MySQL과 관리형 환경에서의 해석

Aurora MySQL에서도 prepared statement의 세션 로컬 성질은 동일하다. 다만 운영 해석에는 몇 가지 차이가 있다.

11.1 failover 이후의 재준비 폭주를 더 주의해야 한다

Aurora failover가 발생하면 기존 writer 세션은 끊기고, pool은 새 writer에 대량 재연결을 시도한다. 이때 prepared statement cache도 모두 다시 채워야 한다. 따라서 장애 직후에는 다음을 함께 관찰해야 한다.

  • 연결 재수립 속도
  • Com_stmt_prepare 급증 여부
  • 애플리케이션 warm-up 지연
  • RDS Proxy 사용 시 세션 상태 보존 기대치와 실제 동작 차이

중요한 점은 prepared statement는 세션 상태이므로, 프록시가 있다고 해서 무조건 재사용이 완전히 보존되는 것은 아니라는 점이다. 프록시 계층의 연결 재사용 정책과 세션 pinning 여부를 별도로 확인해야 한다.

11.2 reader/writer 분리 구조에서는 cache 집합이 이중화될 수 있다

애플리케이션이 writer pool과 reader pool을 별도로 유지하면 prepared statement 집합도 풀별로 따로 쌓일 수 있다. SQL 종류가 비슷해 보여도 물리 연결 풀이 다르면 prepared statement 재고는 합산해서 봐야 한다.

11.3 parameter 변경보다 애플리케이션 템플릿 정리가 더 효과적인 경우가 많다

Aurora/RDS 환경에서는 서버 상한을 올리는 것보다, 다음 조치가 더 재현 가능하고 안전한 경우가 많다.

  • 동적 SQL을 parameterized template로 정리
  • statement cache를 hot set 중심으로 축소
  • pool max lifetime을 무작정 짧게 잡지 않기
  • 장애 복구 시 pool warm-up rate limiting 적용

12. 실무 체크리스트

다음 항목은 prepared statement와 statement cache 문제를 운영적으로 점검할 때 유용한 기준이다.

  • max_prepared_stmt_count와 현재 Prepared_stmt_count
  • Com_stmt_prepare 대비 Com_stmt_execute
  • failover 뒤 Com_stmt_prepare
  • 상한 초과 시 max_prepared_stmt_count

13. 운영 의사결정 기준

prepared statement와 statement cache를 튜닝할 때는 다음 순서가 안전하다.

  1. 실제 드라이버 동작을 확인한다.
    • server-side prepared statement 사용 여부
    • cache가 연결별인지 여부
    • cache eviction 정책
  2. 관측 지표를 본다.
    • Prepared_stmt_count
    • Com_stmt_prepare, Com_stmt_execute, Com_stmt_close, Com_stmt_reprepare
  3. 상한 계산을 한다.
    • 대략 pool size × 연결당 cache 수 × 앱 인스턴스 수
  4. 애플리케이션 SQL 템플릿을 정리한다.
    • 동적 리터럴 SQL 축소
    • 일회성 배치 SQL 분리
  5. 그 다음에야 cache 크기와 서버 상한을 조정한다.

이 순서를 거꾸로 하면, 즉 상한부터 크게 올리거나 cache부터 크게 키우면 문제는 잠시 숨을 수 있지만 구조는 더 악화되기 쉽다.

14. 결론

prepared statement는 단순한 마이크로 최적화 기능이 아니다. 그것은 세션 상태를 서버에 저장하는 메커니즘이며, connection pool과 statement cache가 결합되면 그 상태는 장기간 축적될 수 있다. 따라서 prepared statement를 잘 쓰는 방법은 단순히 “cache를 켠다”가 아니라 다음을 함께 만족하는 것이다.

  • SQL 템플릿 수를 통제한다.
  • 연결 수명을 이해한다.
  • 풀 크기와 cache 크기의 곱을 계산한다.
  • failover 뒤 재준비 비용을 관찰한다.
  • Prepared_stmt_countCom_stmt_prepare를 일상 지표로 본다.

다음 글에서는 이 흐름을 이어서, prepared statement와 직접 맞닿아 있는 parse/optimize/execute 경계 또는 metadata invalidation과 reprepare 관점으로 확장해 보면 운영 해석이 더 선명해진다.