JDBC의 Statement
는 DB에 쿼리를 보내 실행시키는 객체이다.
PreparedStatement
는 Statement
를 상속받은 객체로, 확장된 기능을 가지고 있어 더 자주 사용되지만 Statement
를 완전히 대체할 수 있는 것은 아니다.
Statement
: java.sql
PreparedStatement
: java.sql
extends Statement
가장 표면적으로 보이는 차이점은 파라미터 여부이다.
Statement
는 받은 문자열을 통채로 쿼리문으로 인식하여 그대로 실행하기 때문에 파라미터 바인딩이 불가능하다.
stmt = con.createStatement(sql)
String sql = "select * from member;"
stmt.execute()
👉 받은 쿼리 그대로 실행한다.PreparedStatement
는 문자열을 전달받는 것은 같지만 문자열을 그대로 쿼리문으로 인식하지 않는다.
pstmt = con.prepareStatement(sql);
String sql = "insert into member(id, name) values(?, ?);"
pstmt.setInt(1, member.getId());
pstmt.setString(2, member.getName());
👉 외부에서 파라미터를 바인딩 할 수있다.pstmt.execute()
👉 파라미터 바인딩 후 쿼리를 완성하여 실행한다.PreparedStatement
는 플레이스홀더(?
)에 파라미터 바인딩이 가능하다. 런타임 중에 파라미터를 전달할 수 있으므로 동적인 쿼리문 생성에 유용하다.
또한 문자열만 다루는 Statement
와 달리 PreparedStatement
는 객체를 바인딩할 수 있기 때문에 이미지, 파일 등 바이너리 데이터를 다루는 것도 가능하다.
JDBC는 자바 애플리케이션과 데이터베이스 연결 방법을 표준화 한 인터페이스 API이고, JDBC의 구체적인 구현은 각 DB 벤더(제조사)가 자신의 DB에 맞게 제공하는 JDBC 드라이버마다 차이가 있다.
따라서 PreparedStatement
의 동작 방식도 드라이버마다 다르다. 자신이 사용하는 데이터베이스의 세부 동작을 알고 싶다면 JDBC API 문서가 아닌 JDBC 드라이버 문서를 참고해야 한다.
쿼리 문자열을 실행하기 위해서는 데이터베이스 엔진이 이해할 수 있는 형식으로 변환하는 컴파일 과정이 필요하다.
실행 시점에 쿼리문이 컴파일되는 Statement
와 달리 PreparedStatement
는 실행되기 전에 데이터베이스 엔진이 이해할 수 있는 형태로 미리 컴파일된다. 덕분에 데이터베이스는 실행 시점에는 컴파일 과정을 생략하고 알맞은 형식으로 준비된 쿼리를 실행만 하면 된다.
Statement
는 서로 독립적이다. Statement
는 매번 새로운 쿼리로 인식되므로 DB는 쿼리를 실행할 때마다 컴파일 작업을 수행한다.
반면 PreparedStatement
는 최초 1번만 컴파일을 시행하고 결과를 캐시에 저장해둔다. 미리 컴파일 해놓은 것을 재사용하기 때문에 처리 속도가 훨씬 빠르다.
사전 컴파일과 캐싱 덕분에 PreparedStatement
는 플레이스홀더(?
)에 들어가는 값만 달라지는 경우 쿼리 전체를 컴파일하는 것이 아니라 파라미터 부분만 처리한다.
Statement
에서는 두 개가 완전히 다른 쿼리이다 :select * from member where id = 1;
select * from member where id = 2;
PreparedStatement
에서는 다음 쿼리를 컴파일하고 캐싱해둔다 :select * from member where id = ?;
?
에 바인딩한 값만 처리한다.따라서 파라미터만 달라지는 동일 쿼리를 반복해서 사용하는 경우 PreparedStatement
를 사용하는 것이 훨씬 효율적이다.
SQL 인젝션은 애플리케이션이 클라이언트가 제공한 데이터를 SQL 문에 사용하는 것을 이용한 공격 방식이다. Statement
는 이 공격에 굉장히 취약하다.
String sql = "select "
+ "customer_id, name, balance "
+ "from Accounts where customer_id = '"
+ customerId
+ "'";
Statement
는 위의 예시처럼 변수(customer_id
)에 클라이언트로부터 받은 값을 담고 문자열을 조합하여 쿼리문으로 사용하는 경우가 많다.
그런데 클라이언트의 값 조작은 매우 쉽기 때문에 숫자로 보내져야 할 customer_id
값을 해커가 악의적으로 xxx' or '1' = '1
과 같이 보냈다고 하면, 최종적으로 애플리케이션은 다음과 같은 쿼리를 실행하게 된다 :
select customer_id, name, balance from Accounts
where customer_id = 'xxx' or '1' = '1'
'1' = '1'
은 무조건 참이고 OR 연산이기 때문에 WHERE 절 전체는 참이 된다. 앞의 조건은 무효화되어 해커는 모든 값을 조회할 수 있게 된다.
그러나 위 예시의 공격 방법은 PreparedStatement
에서는 무의미하다. 문자열을 그대로 쿼리문으로 사용하지 않기 때문이다.
예를 들어 클라이언트로부터 값을 받는 변수 customer_id
의 데이터타입이 int
라고 한다면 파라미터 바인딩을 할 때 abc' or '1' = '1'
와 같이 유효하지 않은 값은 바인딩이 되지 않는다.
pstmt.setInt(1, customer_id);
Statement | PreparedStatement | |
---|---|---|
파라미터 | 런타임 중 파라미터를 전달할 수 없음 | 런타임 중에 파라미터를 전달할 수 있음 |
컴파일 | 실행될 때마다 | 컴파일 후 캐싱 → 재사용 |
성능 | 매우 낮음 | 비교적 높음 |
바이너리 데이터 | 처리 불가 | 처리 가능 |
사용처 | 한 번만 실행되는 쿼리 | 여러번 사용되는 쿼리 |
DDL(CREATE, ALTER, DROP) | 동적 쿼리 |
Statement
는 JDBC 인터페이스에 정의되어 있으며, 문자열로 된 쿼리문을 DB에 보내 실행시킨다.
PreparedStatement
는 Statement
를 상속받은 객체로, 파라미터 바인딩이 가능하다. 사전 컴파일과 캐싱을 지원하기 때문에 파라미터만 달라지는 동일한 쿼리를 반복해서 사용하는 경우 효율성이 극대화된다. 문자열을 그대로 쿼리문으로 사용하지 않기 때문에 SQL 인젝션 공격을 방지해주기도 한다.
PreparedStatement
의 사용 빈도가 훨씬 높지만 DDL 등 한 번만 사용되는 쿼리의 경우 Statement
를 사용하는 것이 좋다. 또한 로직상 PreparedStatement
를 사용할 수 없는 경우도 있고 Statement
로 작성된 레거시 코드를 모두 바꾸는 것이 어려울 수도 있으므로 SQL 인젝션에 대한 대책으로 항상 PreparedStatement
가 답이 될 수 있는 것은 아니라고 한다.