Java 아키텍처·Spring 용어 사전
JDBC파라미터 바인딩 · Statement

PreparedStatement

SQL 뼈대를 먼저 보내고 값을 나중에 채우는 방식. 인젝션을 원천 차단하고 실행 계획을 재사용한다.

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(?))

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — JDBC — 자바가 DB와 말하는 가장 낮은 층