본문 바로가기

Java

Statement VS PreparedStatement 차이점

Statement와 PreparedStatement에 대해 이야기 해보려고 한다.

흔히 둘의 가장 큰 차이점으로 Caching 여부를 언급한다.

 

과연 Caching이 되는 곳은 어디일까? Statement는 정말 Caching이 안 되는 것인가?

 

Caching 구역을 두 군데로 나누어 볼 수 있다.

DB에서의 Caching과 Application Layer에서의 Caching

 

Statement, PreparedStatement 둘 다 첫 실행시에 아래와 같은 과정을 거쳐 DB에 쿼리가 실행된다.

(1) Parsing (문장 분석)

(2) Compile

(3) Execute

 

Statement, PreparedStatement 둘 다 (2) Compile이 발생할 때 DB에 해당 Query로 접근 계획이 Caching 된다.

즉, Statement를 사용하더라도 첫 수행한 Query와 완전히 일치하는 Query를 요청한다면, (2) Compile을 건너뛰고 수행된다.

 

PreparedStatement와 똑같이 수행된다고 생각할 수도 있는데 그렇지 않다.

엄밀히 말해서 Statement는 첫 번째와 완전히 일치하는 두 번째 Query 요청시  (1), (2) 의 로직으로 접근은 하지만 새로 (2) Complie을 하지는 않는 것이다. (DB에 Caching된 접근 계획을 찾았기 때문)

 

PreparedStatement는 위와 같은 상황에서 첫 번째 Query에 의해 수행되어 Caching된 PreparedStatement 객체로 DB에 Query를 요청하기 때문에 바로 (3) Execute를 하게 된다. (Application Layer에 Caching된 객체를 찾았기 때문)

 

둘은 Application Layer에서의 Caching에 차이가 있다.

 

PreparedStatement로 Query를 실행할 때 JDBC API는 해당 Query를 Connection에 연결되도록 PreparedStatement 객체를 Caching한다. 만약 다른 Connection이 완전히 일치하는 Query로 PreparedStatement를 생성하려 한다면 JDBC API가 Caching된 PreparedStatement 객체를 재사용하게끔 기존의 객체를 할당한다. 그리고 Caching되어 있는 PreparedStatement 객체와 연결된 Connection이 하나도 없다면 GC는 이 객체를 회수한다.

 

위의 내용만 보면 PreparedStatement만 사용하는 것이 가장 효율적인 방법으로 보인다.

하지만, 모든 Query가 Complie되어 PreparedStatement 객체가 매우 많이 생성된다면 어떻게 될까?

Caching 영역의 한계로 정작 Caching되어야 할 Query가 그렇게 되지 못할 가능성이 커진다.

그러므로 성능상 Caching되길 원하는 Query에 적용하는 것이 좋다.

 

Statement를 사용할 때에도 주의할 점이 있다.

Statement는 PreparedStatement가 ? 방식으로 Parameter를 받아 처리하는 것과는 다르게 Parameter를 받지 않고 Dynamic Query로 작성될 수 있다. Dynamic Query로 작성될 수 있다는 것은 SQL Injection에 취약할 수 있으므로 필터나 방어 로직을 추가하는 것이 좋다.

 

 

public final class MyDb {

    private static final String DRIVER_NAME = "com.mysql.jdbc.Driver";
    public static final String URL = "jdbc:mysql://localhost:3306/yourdb";
    public static final String USER = "yourUserName";
    public static final String PASSWORD = "yourPassword";

    public static void loadDriver() {
        try {
            Class.forName(DRIVER_NAME);
        } catch (ClassNotFoundException classNotFoundException) {
            classNotFoundException.printStackTrace();
        }
    }

}

 

 

public class StatementExample {

    public static void main(String[] args) {
        MyDb.loadDriver();
        try (Connection connection = DriverManager.getConnection(MyDb.URL, MyDb.USER, MyDb.PASSWORD)) {
            /**
             * statement.executeQuery(sql); 실행 시 다음과 같음.
             * (1) Parsing (문장 분석)
             * (2) Compile
             * (3) Execute
             * Caching 이 지원되지 않고, 미리 Compile 된 Query 가 아니기 때문에
             * 호출될 때 항상 위의 세 단계를 수행할 수 밖에 없는 구조.
             * 엄밀히 말하자면, Application Layer 에서만 Caching 이 안 되는 것이다.
             * DB 는 SQL 접근 계획 에 대해 Caching 을 하고 있다.
             * 그러므로 DB Cache 에 있는 SQL 접근 계획과 완전히 일치하는 SQL 이 요청으로 들어온다면
             * DB 는 SQL 접근 계획을 다시 실행하지 않고 Caching 되어 있는 접근 계획을 재사용한다.
             * 즉, 아래 로직은 항상 같은 SQL 이 만들어지므로 
             * 첫 (2) 시도 이후에는 DB 에 Caching 된 접근 계획을 재사용한다.
             */
            for (int i = 0; i < 100; i++) {
                String sql = findByNameQuery("장화평");
                Statement statement = connection.createStatement(); // 매 번 새로 생성됨.
                ResultSet resultSet = statement.executeQuery(sql);
                printResultSet(resultSet);
            }

            /**
             * i 에 의해서 SQL 이 변경되므로
             * DB 는 Cache 에서 SQL 접근 계획을 찾지 못해 매번 접근 계획을 다시 수행한다.
             * 즉, 아래 로직은 DB 에 Caching 된 접근 계획을 재사용할 수 없다.
             */
            for (int i = 0; i < 100; i++) {
                String sql = findByNameQuery("장화평" + i);
                Statement statement = connection.createStatement();
                ResultSet resultSet = statement.executeQuery(sql);
                printResultSet(resultSet);
            }
        } catch (SQLException sqlException) {
            sqlException.printStackTrace();
        }
    }

    private static String findByNameQuery(String name) {
        return "SELECT id, name FROM user WHERE name = " + name;
    }

    private static void printResultSet(ResultSet resultSet) throws SQLException {
        if (resultSet.next()) {
            do {
                System.out.println(resultSet.getInt("id"));
                System.out.println(resultSet.getString("name"));
            } while (resultSet.next());
        }
    }

}

 

 


public class PreparedStatementExample {

    public static void main(String[] args) {
        MyDb.loadDriver();
        try (Connection connection = DriverManager.getConnection(MyDb.URL, MyDb.USER, MyDb.PASSWORD)) {
            /**
             * connection.prepareStatement(sql); 실행 시 다음과 같음.
             * (1) Parsing (문장 분석)
             * (2) Compile
             * 할당 받은 Connection 에 대해서 Application Layer 에 Compile 된 SQL 이 Caching 된다.
             * 또한 Statement 와 마찬가지로 DB 에도 SQL 에 대한 접근 계획이 Caching 된다.
             * 사실 DB 에 SQL 접근 계획이 Caching 되는 것은 Statement, PreparedStatement 와는 무관하다.
             * (Connection 이 끊기면 GC 에 의해서 Caching 된 SQL 이 사라진다.)
             * (DB 에 Caching 되어 있는 접근 계획은 유효하다.)
             * (Application Layer 에서 SQL Caching 은 Connection 에 연결되어 있으나
             * 다른 Connection 에서 완전히 일치하는 SQL 로 PreparedStatement 객체를 생성할 경우
             * Caching 된 객체를 가져온다.)
             *
             * preparedStatement.executeQuery(); 실행 시 다음과 같음.
             * (3) Execute
             * Caching 된 SQL 이 있기 때문에 바로 실행된다.
             * i 에 의해서 Parameter 값이 바뀌더라도 ? 가 포함된 SQL 로 Caching 되어 있기 때문에
             * Caching 된 SQL 을 재사용할 수 있고, (2)에 의해서 DB 에서도 ? 가 포함된 SQL 로 접근 계획이
             * Caching 되어 있으므로 DB 에서도 Caching 되어 있는 SQL 접근 계획을 사용한다.
             */
            for (int i = 0; i < 100; i++) {
                String sql = findByNameQuery();
                // 매 번 새로 생성되지 않음. 메모리에 존재하는 경우 재사용함.
                PreparedStatement preparedStatement = connection.prepareStatement(sql);
                preparedStatement.setString(1, "장화평" + i);
                ResultSet resultSet = preparedStatement.executeQuery();
                printResultSet(resultSet);
            }

        } catch (SQLException sqlException) {
            sqlException.printStackTrace();
        }
    }

    private static String findByNameQuery() {
        return "SELECT id, name FROM user WHERE name = ?";
    }

    private static void printResultSet(ResultSet resultSet) throws SQLException {
        if (resultSet.next()) {
            do {
                System.out.println(resultSet.getInt("id"));
                System.out.println(resultSet.getString("name"));
            } while (resultSet.next());
        }
    }

}

 

참고

https://globalhost.interdol.com/190