[JDBC] Statement vs. PreparedStatement

dondonee·2023년 12월 7일
0
post-thumbnail

Statement vs. PreparedStatement

JDBC의 Statement는 DB에 쿼리를 보내 실행시키는 객체이다.

PreparedStatementStatement를 상속받은 객체로, 확장된 기능을 가지고 있어 더 자주 사용되지만 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 드라이버 문서를 참고해야 한다.


(1) 사전 컴파일(pre-compilataion)

쿼리 문자열을 실행하기 위해서는 데이터베이스 엔진이 이해할 수 있는 형식으로 변환하는 컴파일 과정이 필요하다.

실행 시점에 쿼리문이 컴파일되는 Statement와 달리 PreparedStatement는 실행되기 전에 데이터베이스 엔진이 이해할 수 있는 형태로 미리 컴파일된다. 덕분에 데이터베이스는 실행 시점에는 컴파일 과정을 생략하고 알맞은 형식으로 준비된 쿼리를 실행만 하면 된다.

  • 그러나 전처리를 위해서는 데이터베이스와의 통신이 사전 컴파일 시에 한 번,
    실행 시에 한 번, 두 번의 왕복이 필요하다. 대부분의 쿼리는 재사용 빈도가 낮아서 사전 컴파일을 하지 않고 한 번의 왕복으로 처리하는 것이 효율적이라 DB에 따라서 사전 컴파일을 지연시키는 경우도 있다고 한다. (🔗 참고: PostgreSQL JDBC Statement Caching)

(2) 캐싱

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 Injection 공격 방지

SQL 인젝션은 애플리케이션이 클라이언트가 제공한 데이터를 SQL 문에 사용하는 것을 이용한 공격 방식이다. Statement는 이 공격에 굉장히 취약하다.

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의 경우

그러나 위 예시의 공격 방법은 PreparedStatement에서는 무의미하다. 문자열을 그대로 쿼리문으로 사용하지 않기 때문이다.

예를 들어 클라이언트로부터 값을 받는 변수 customer_id의 데이터타입이 int라고 한다면 파라미터 바인딩을 할 때 abc' or '1' = '1'와 같이 유효하지 않은 값은 바인딩이 되지 않는다.

  • pstmt.setInt(1, customer_id);



정리

StatementPreparedStatement
파라미터런타임 중 파라미터를 전달할 수 없음런타임 중에 파라미터를 전달할 수 있음
컴파일실행될 때마다컴파일 후 캐싱 → 재사용
성능매우 낮음비교적 높음
바이너리 데이터처리 불가처리 가능
사용처한 번만 실행되는 쿼리여러번 사용되는 쿼리
DDL(CREATE, ALTER, DROP)동적 쿼리

Statement는 JDBC 인터페이스에 정의되어 있으며, 문자열로 된 쿼리문을 DB에 보내 실행시킨다.

PreparedStatementStatement를 상속받은 객체로, 파라미터 바인딩이 가능하다. 사전 컴파일과 캐싱을 지원하기 때문에 파라미터만 달라지는 동일한 쿼리를 반복해서 사용하는 경우 효율성이 극대화된다. 문자열을 그대로 쿼리문으로 사용하지 않기 때문에 SQL 인젝션 공격을 방지해주기도 한다.

PreparedStatement의 사용 빈도가 훨씬 높지만 DDL 등 한 번만 사용되는 쿼리의 경우 Statement를 사용하는 것이 좋다. 또한 로직상 PreparedStatement를 사용할 수 없는 경우도 있고 Statement로 작성된 레거시 코드를 모두 바꾸는 것이 어려울 수도 있으므로 SQL 인젝션에 대한 대책으로 항상 PreparedStatement가 답이 될 수 있는 것은 아니라고 한다.




🔗 References

연관 포스팅

0개의 댓글