JDBC Statement와 PreparedStatement: SQL 실행 방식 선택

JDBC의 Statement와 PreparedStatement 처리 흐름, SQL 인젝션 위험, 반복 실행과 배치 처리 시 선택 기준을 정리한다.

2026-08-14 · 최초 발행 2025-08-10

SQL이 결과를 돌려주기까지

애플리케이션이 DBMS에 SQL을 전달하면 쿼리는 바로 실행되지 않는다. 구문을 해석하고, 테이블·컬럼·권한을 확인하고, 실행 계획을 세운 뒤 실행 가능한 형태로 변환하는 과정을 거친다.

SQL 쿼리 작성파싱정당성 검사최적화컴파일실행결과 반환

처리 순서는 다음과 같다.

  1. 파싱은 SQL 구문을 분석해 구문 트리(Parse Tree)를 만든다.
  2. 정당성 검사에서는 테이블과 컬럼의 존재 여부, 권한을 확인한다.
  3. 최적화 단계에서 쿼리 실행 계획을 수립한다.
  4. 컴파일은 SQL을 실행 가능한 형태로 변환한다.
  5. 실행 단계에서 데이터베이스 작업이 수행된다.
  6. 결과는 클라이언트로 반환된다.

JDBC(Java Database Connectivity)를 포함한 데이터베이스 연결 인터페이스는 이 SQL을 실행할 때 Statement와 PreparedStatement라는 두 방식을 제공한다.

완성된 SQL을 보내는 Statement

Statement는 완성된 SQL 문자열을 만들어 직접 실행하는 기본 방식이다. 쿼리 내용이 실행 시점에 정해지는 경우에는 유연하지만, 매 실행마다 SQL 처리 단계를 거치며 사용자 입력을 문자열로 연결할 때 보안 문제가 생길 수 있다.

데이터베이스JDBC 드라이버애플리케이션데이터베이스JDBC 드라이버애플리케이션Statement 객체 생성SQL 쿼리 전달SQL 쿼리 전송파싱, 컴파일, 최적화, 실행결과 반환결과 전달

다음은 department_id가 100인 직원 정보를 조회하는 예제다.

Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement();
String sql = "SELECT * FROM employees WHERE department_id = 100";
ResultSet rs = stmt.executeQuery(sql);

while (rs.next()) {
    System.out.println(rs.getString("employee_name"));
}

rs.close();
stmt.close();
conn.close();

Statement는 구현이 단순하고, 동적으로 완성한 쿼리를 실행할 수 있다. 반면 SQL 문자열을 매번 파싱·컴파일·최적화해야 하며, 입력값을 직접 결합하면 SQL 인젝션에 노출된다.

// 위험한 코드
String userInput = request.getParameter("userName");
String sql = "SELECT * FROM users WHERE username = '" + userInput + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);

사용자가 admin' OR '1'='1를 입력하면 실제 SQL은 SELECT * FROM users WHERE username = 'admin' OR '1'='1'가 된다. 조건이 항상 참이 되어 모든 사용자 정보가 노출될 수 있다.

SQL 템플릿을 재사용하는 PreparedStatement

PreparedStatement는 파라미터화된 SQL을 준비하고, 이후 값만 바꿔 실행하는 방식이다. SQL 문을 한 번 파싱하고 컴파일해 재사용할 수 있으며, 파라미터 바인딩으로 사용자 입력을 다룬다.

데이터베이스JDBC 드라이버애플리케이션데이터베이스JDBC 드라이버애플리케이션다른 파라미터로 재사용 가능PreparedStatement 객체 생성(파라미터화된 SQL)SQL 템플릿 전송파싱, 컴파일, 최적화(실행계획 생성)준비 완료 응답파라미터 값 설정파라미터 값만 전송캐시된 실행계획으로 실행결과 반환결과 전달

동일한 SQL 템플릿에 서로 다른 department_id를 바인딩하는 코드는 다음과 같다.

Connection conn = DriverManager.getConnection(url, user, password);
String sql = "SELECT * FROM employees WHERE department_id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);

// 첫 번째 실행
pstmt.setInt(1, 100);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
    System.out.println(rs.getString("employee_name"));
}
rs.close();

// 두 번째 실행 (재사용)
pstmt.setInt(1, 200);
rs = pstmt.executeQuery();
while (rs.next()) {
    System.out.println(rs.getString("employee_name"));
}

rs.close();
pstmt.close();
conn.close();

이 방식은 반복 SQL 처리 시간을 줄일 수 있고, SQL 인젝션 공격을 방지하며, 파라미터 바인딩 과정에서 데이터 타입을 검사한다. BLOB, CLOB 같은 대용량 데이터 처리에도 사용할 수 있다.

단발 실행과 반복 실행의 차이

단일 쿼리만 실행한다면 두 방식의 성능 차이는 크지 않을 수 있다. Statement는 별도의 준비 과정이 없으므로 초기 오버헤드가 적을 수 있다.

0000000000000000000001Statement 방식 SQL 파싱 및 컴파일 PreparedStatement 방식 SQL 파싱 및 컴파일 쿼리 실행 파라미터 바인딩 쿼리 실행 단일 쿼리 실행

같은 쿼리를 다른 파라미터와 함께 여러 번 실행하면 PreparedStatement의 재사용성이 드러난다. Statement는 각 요청에서 파싱과 실행을 반복하지만, PreparedStatement는 SQL 파싱 및 컴파일을 1회 수행한 뒤 바인딩과 실행을 반복한다.

0000000000010101010102Statement 방식 1회 쿼리 (파싱+실행) PreparedStatement 방식 SQL 파싱 및 컴파일 (1회) 1회 실행 (바인딩+실행) 2회 실행 (바인딩+실행) ... 10회 실행 (바인딩+실행) 2회 쿼리 (파싱+실행) ... 10회 쿼리 (파싱+실행) 10회 반복 쿼리 실행

쿼리 성격에 따른 선택

한 번만 실행하는 동적 쿼리, CREATE·ALTER·DROP 같은 DDL, 파라미터가 없는 간단한 SQL, 테이블명이나 컬럼명이 동적으로 달라지는 경우에는 Statement를 사용할 수 있다.

// 동적 테이블 쿼리 예시
String tableName = "sales_" + year + "_" + month;
String sql = "SELECT sum(amount) FROM " + tableName;
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);

반대로 동일한 SQL을 다른 파라미터로 반복 실행할 때, 사용자 입력이 포함된 쿼리, 대량 데이터 배치 처리, BLOB·CLOB 처리에는 PreparedStatement가 적합하다.

// 배치 처리 예시
String sql = "INSERT INTO products (name, price) VALUES (?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);

for (Product product : productList) {
    pstmt.setString(1, product.getName());
    pstmt.setDouble(2, product.getPrice());
    pstmt.addBatch();
}
int[] results = pstmt.executeBatch();

대형 금융 시스템 사례에서는 뱅킹 시스템이 수천만 건의 거래를 처리하며 PreparedStatement를 사용해 CPU 사용률을 30% 감소시켰고, 증권 거래 플랫폼은 주문 처리 시스템에 적용해 초당 처리량을 2배 증가시켰다. 보험 청구 시스템에서는 PreparedStatement 활용으로 SQL 인젝션 공격 위험을 제거했다.

재사용과 배치 처리

PreparedStatement는 연결을 가져온 뒤 한 번 생성해 여러 파라미터에 재사용할 수 있다. 다음 코드는 여러 category_id 조회에 하나의 PreparedStatement를 사용한다.

// Connection 풀에서 연결 가져오기
Connection conn = dataSource.getConnection();

// PreparedStatement 생성 및 재사용
PreparedStatement pstmt = null;
try {
    pstmt = conn.prepareStatement("SELECT * FROM products WHERE category_id = ?");

    // 여러 카테고리 조회
    for (int categoryId : categoryIds) {
        pstmt.setInt(1, categoryId);
        ResultSet rs = pstmt.executeQuery();
        // 결과 처리
        rs.close();
    }
} finally {
    if (pstmt != null) pstmt.close();
    conn.close(); // 연결 반환
}

대량 INSERT에서는 자동 커밋을 비활성화하고, 1000개마다 배치를 실행한 뒤 남은 항목을 처리할 수 있다.

PreparedStatement pstmt = conn.prepareStatement(
    "INSERT INTO orders (customer_id, product_id, quantity) VALUES (?, ?, ?)");

// 자동 커밋 비활성화
conn.setAutoCommit(false);

for (Order order : orderList) {
    pstmt.setInt(1, order.getCustomerId());
    pstmt.setInt(2, order.getProductId());
    pstmt.setInt(3, order.getQuantity());
    pstmt.addBatch();

    // 1000개마다 배치 실행
    if (++count % 1000 == 0) {
        pstmt.executeBatch();
    }
}
// 남은 배치 실행
pstmt.executeBatch();
conn.commit();

대부분의 애플리케이션에서는 PreparedStatement를 기본으로 두고, 완성된 동적 SQL이 꼭 필요한 경우에만 Statement를 선택한다. 이 구분은 SQL 처리 성능과 입력값 보안을 함께 다루는 기준이 된다.

JDBCSQLPreparedStatementStatementSQL 인젝션