Skip to content

sene03/java-preparedStatement-practice

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Java PreparedStatement Practice

참고 문서: 우리의 애플리케이션에서 PreparedStatement는 어떻게 동작하고 있는가 | 카카오페이 기술 블로그



개요

JDBC의 PreparedStatement가 실제로 어떻게 동작하는지 MySQL Connector/J 설정값(useServerPrepStmts, cachePrepStmts)을 바꿔가며 실습한 코드입니다.



핵심 설정값

JDBC URL에 아래 파라미터를 추가하여 동작을 제어합니다.

jdbc:mysql://host:port/schema?useServerPrepStmts=true&cachePrepStmts=true
설정 기본값 설명
useServerPrepStmts false true면 MySQL 서버에 실제로 COM_STMT_PREPARE 패킷을 전송하여 서버 측에서 쿼리를 파싱/저장. false면 MySQL에 PREPARE를 보내지 않고 JVM 레벨에서만 처리
cachePrepStmts false true면 Connector/J가 PreparedStatement 객체를 커넥션 단위로 캐시하여 재사용. false면 매번 새로 생성
prepStmtCacheSize 25 커넥션당 캐시할 PreparedStatement 최대 개수 (LRU 방식)
prepStmtCacheSqlLimit 256 캐시할 쿼리의 최대 바이트 수. 초과 시 캐시하지 않음

useServerPrepStmts에 따른 네트워크 통신

useServerPrepStmts=true이면 쿼리 실행 시 아래와 같이 MySQL 서버와 패킷을 주고받습니다. 각 줄이 네트워크 통신 1회입니다.

[Java App]                          [MySQL Server]
    |                                      |
    | --- COM_STMT_PREPARE (패킷) -------> |  쿼리 파싱 및 실행계획 수립
    | <-- OK + statement_id 반환 --------- |
    |                                      |
    | --- COM_STMT_EXECUTE (패킷) -------> |  파라미터만 바인딩하여 즉시 실행
    |      (statement_id + 파라미터 포함)  |
    | <-- ResultSet 반환 ----------------- |
    |                                      |
    | --- COM_STMT_CLOSE (패킷) ---------> |  DEALLOCATE (응답 없음, 단방향)
    |      (statement_id 포함)             |


실습 1. close()와 캐시의 관계


왜 close()를 해야 캐시를 사용할 수 있는가?

cachePrepStmts=true일 때, prepareStatement를 생성할 때 캐시에서 가져오고 해당 statement를 close할 때 다시 캐시에 객체를 반납합니다.

prepareStatement() 캐시에서 remove, IN_USE 상태
execute()         
close()            캐시에 다시 put → recache

내부 소스코드: recachePreparedStatement()

stmt.close() 호출 시 내부적으로 아래 메서드가 실행됩니다.

// ConnectionImpl.java (MySQL Connector/J)
@Override
public void recachePreparedStatement(JdbcPreparedStatement pstmt) throws SQLException {
    synchronized (getConnectionMutex()) {
        if (this.cachePrepStmts.getValue() && pstmt.isPoolable()) {
            synchronized (this.serverSideStatementCache) {
                Object oldServerPrepStmt = this.serverSideStatementCache.put(
                        new CompoundCacheKey(pstmt.getCurrentDatabase(), ((PreparedQuery) pstmt.getQuery()).getOriginalSql()),
                        (ServerPreparedStatement) pstmt);
                if (oldServerPrepStmt != null && oldServerPrepStmt != pstmt) {
                    ((ServerPreparedStatement) oldServerPrepStmt).isCached = false;
                    ((ServerPreparedStatement) oldServerPrepStmt).setClosed(false);
                    ((ServerPreparedStatement) oldServerPrepStmt).realClose(true, true);
                }
            }
        }
    }
}

이름이 recache인 이유는, 처음 캐시에 넣는 것이 아니라 꺼내서 쓰고 다시 돌려놓는 개념이기 때문입니다.

동작 흐름:

close() 호출
    ↓
recachePreparedStatement()
    ↓
LRUCache에 put()
    ↓
밀려난 oldStmt가 있는가?
    ├── null → 새로 추가됨, 완료
    ├── 자기 자신 → 중복 put, 무시
    └── 다른 객체 → LRU로 밀려난 것
              → isCached = false
              → setClosed(false)   ← realClose() 내부 가드 우회
              → realClose()        → COM_STMT_CLOSE 전송 → MySQL에서 삭제

실습 코드

PrepStmtWithClose.java — close()를 호출한 경우

PREPARE  1회
EXECUTE  1회 → close() → 캐시에 반납
EXECUTE  1회 → close() → 캐시에서 꺼냄 (캐시 히트)

stmt1 == stmt2: true

PrepStmtWithoutClose.java — close()를 호출하지 않은 경우

PREPARE  1회  → IN_USE 상태
EXECUTE  1회
PREPARE  1회  → 캐시에 IN_USE 객체가 있으므로 히트 실패, 새로 PREPARE
EXECUTE  1회

stmt1 == stmt2: false


실습 2. 복잡한 쿼리 캐시 테스트

ComplexQueryTest.java

복잡한 쿼리(조인, 서브쿼리 등)를 대상으로 캐시 설정에 따른 성능 차이를 측정했습니다.


결과

설정을 바꿔도 실행시간에 유의미한 차이가 없었습니다.


원인 추측: prepStmtCacheSqlLimit

prepStmtCacheSqlLimit의 기본값은 256바이트입니다. 복잡한 쿼리는 이 크기를 초과하는 경우가 많아 캐시 대상에서 제외됩니다.

// isPoolable() 내부 - 쿼리가 너무 길면 캐시 자격 박탈
if (sql.length() > this.prepStmtCacheSqlLimit.getValue()) {
    return false;  // 캐시 안 함
}

해결 방법으로는 prepStmtCacheSqlLimit 값을 늘리는 것이 있습니다.

?prepStmtCacheSqlLimit=2048


실습 3. max_prepared_stmt_count 초과 테스트

MaxPrepStmtCountTest.java

MySQL 서버의 max_prepared_stmt_count를 초과할 경우 1461 에러가 발생하는지 확인합니다.


사전 셋업

-- prepared statement 최대 개수를 50으로 축소
SET GLOBAL max_prepared_stmt_count = 50;
SHOW VARIABLES LIKE 'max_prepared_stmt_count';

-- 실행 전후 상태 확인용 쿼리
SHOW GLOBAL STATUS LIKE 'Com_stmt_prepare';   -- PREPARE된 누적 횟수
SHOW GLOBAL STATUS LIKE 'Com_stmt_close';     -- CLOSE된 누적 횟수
SHOW GLOBAL STATUS LIKE 'Prepared_stmt_count'; -- 현재 활성 상태인 개수

실행 결과

useServerPrepStmts=true, cachePrepStmts=false 설정으로 실행했습니다.

Com_stmt_prepare  : 440,412
Com_stmt_close    : 220,000
Prepared_stmt_count: 50
  • 애플리케이션 콘솔에 1461 에러는 찍히지 않았습니다.
  • Prepared_stmt_countmax_prepared_stmt_count(=50)와 동일한 값으로 나왔습니다.
  • Com_stmt_prepareCom_stmt_close의 차이가 큽니다.

⚠️ 이 결과에 대한 원인은 아직 분석 중입니다.

참고: Com_stmt_prepare, Com_stmt_close는 MySQL 서버가 재시작되기 전까지 누적되는 값입니다. 정확한 측정을 위해 실험 전 FLUSH STATUS로 카운터를 초기화하는 것을 권장합니다.


설정별 동작 요약

useServerPrepStmts cachePrepStmts MySQL PREPARE 객체 캐시 특이사항
true true ✅ 최초 1회 권장 설정
true false ✅ 매번 네트워크 3배, 최악의 성능
false true ✅ (QueryInfo 캐시) MySQL엔 항상 Statement로 전달
false false 기본값

About

JDBC PreparedStatement 캐시 동작과 MySQL Connector/J 내부 구현 이해를 위한 실습 코드

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages