SQL 뼈대를 먼저 DB에 보내 파싱시켜 두고, 값은 나중에 별도로 채우는 방식.
Statement와의 차이
// ❌ Statement — 문자열을 이어 붙인다
stmt.executeQuery("SELECT * FROM member WHERE name = '" + input + "'");
// ✅ PreparedStatement — 뼈대와 값을 분리한다
PreparedStatement ps = con.prepareStatement("SELECT * FROM member WHERE name = ?");
ps.setString(1, input);
ps.executeQuery();
두 가지 이득
-
① 보안 — SQL 인젝션 차단
- SQL 구조가 파싱 시점에 확정된다
- 입력에 따옴표나 세미콜론이 들어와도 '값' 으로만 취급된다
- 이스케이프가 아니라 구조적 분리라서 빠뜨릴 수가 없다
-
② 성능 — 실행 계획 재사용
- 같은 뼈대를 반복 실행하면 DB 가 파싱·계획 수립을 건너뛴다
- 같은 쿼리를 대량으로 돌릴 때 차이가 크다
바인딩할 수 없는 자리
// 테이블명 · 칼럼명 · ORDER BY · ASC/DESC 는 파라미터가 될 수 없다
String sql = "SELECT * FROM member ORDER BY " + sortColumn; // ❌
// 화이트리스트로 검증한다
Set<String> ALLOWED = Set.of("created_at", "name");
if (!ALLOWED.contains(sortColumn)) throw new IllegalArgumentException();
정렬 칼럼을 파라미터로 받는 목록 API가 가장 자주 뚫리는 지점이다.
try-with-resources로 닫는다
try (Connection con = dataSource.getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) { ... }
}
} // 예외가 나도 반드시 닫힌다 → 커넥션 누수 방지
대량 처리
// 조회: fetch size 를 키워 왕복을 줄인다 (기본값이 작다)
ps.setFetchSize(1000);
// 쓰기: 배치로 묶는다
for (var u : users) { ps.setString(1, u.name()); ps.addBatch(); }
ps.executeBatch(); // 왕복 10,000번 → 1번
프레임워크를 써도 원리는 같다
jdbcTemplate.query("SELECT * FROM member WHERE name = ?", rowMapper, input);
// JPA·MyBatis·jOOQ 도 내부적으로 PreparedStatement 를 쓴다
면접 함정
- ❌ "ORM을 쓰니 인젝션은 신경 안 써도 된다" → 네이티브 쿼리나 문자열 결합에서는 그대로 뚫린다.
- ❌ "PreparedStatement는 항상 빠르다" → 한 번만 실행하는 쿼리는 파싱 왕복이 추가돼 손해일 수 있다.
실행 계획 재사용의 실제 효과
-
같은 뼈대를 반복 실행하면 DB 가 파싱·계획 수립을 건너뛴다
-
PostgreSQL — 같은 PreparedStatement 를 5회 이상 실행하면
- generic plan 으로 전환할지 판단한다 (plan_cache_mode 로 제어)
-
MySQL — 서버 사이드 prepare 여부가 드라이버 설정에 달렸다
- useServerPrepStmts=true · cachePrepStmtsSize
# HikariCP + MySQL 권장 설정
spring.datasource.hikari.data-source-properties:
cachePrepStmts: true
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
useServerPrepStmts: true
IN 절의 함정
// IN 목록 크기가 매번 다르면 SQL 뼈대가 매번 달라진다 → 캐시가 무의미해진다
"... WHERE id IN (?, ?, ?)" // 3개
"... WHERE id IN (?, ?, ?, ?)" // 4개 → 다른 쿼리로 취급된다
// 대응: 개수를 2의 거듭제곱 등으로 반올림해 패딩하거나(Hibernate 의 in_clause_parameter_padding)
// 배열 파라미터를 지원하는 문법을 쓴다 (PostgreSQL 의 = ANY(?))