1. 개요
앞서 executeUpdate를 사용하면서 발생했던 문제에 대한 포스트를 올렸다. 이 문제 자체는 @Transactional 어노테이션을 붙여 트랜잭션을 적용하는 것으로 간단하게 해결했지만, 트랜잭션을 적용하지 않고도 잘 실행되던 SP 쿼리들이 있었기에 특정 쿼리에서만 에러가 발생하는 원인을 좀 더 찾아봤다. 그 원인은 executeUpdate의 내부에 있었다.
2. JDBC의 execute와 executeUpdate, 그리고 executeQuery
먼저 보편적으로 사용되는 JDBC의 execute, executeQuery, execueUpdate에 대해 간단하게 알아보자.
1) execute
- 모든 종류의 쿼리를 실행하는 데 사용된다
- Boolean 값을 반환한다
- SELECT 쿼리를 통해 ResultSet을 받으면 True를 반환한다
- INSERT/UPDATE/DELETE 쿼리를 실행하면 별도의 ResultSet이 없으므로 False를 반환한다
2) executeQuery
- SELECT 쿼리를 실행하는 데 사용된다
- ResultSet을 반환한다
- 이때, 반환 값이 없더라도 ResultSet은 null이 아니다
- INSERT/UPDATE/DELETE 쿼리를 실행하면 SQLException이 발생한다
3) executeUpdate
- 아무것도 반환하지 않는 DML(INSERT/UPDATE/DELETE)문 또는 DDL문을 실행하는 데 사용된다
- Int 값을 반환한다
- DML문을 실행할 땐 영향받은 행 수를 반환한다
- DDL문을 실행할 땐 0을 반환한다 (MSSQL 드라이버 기준)
3. StoredProcedureQuery에서의 execute와 executeUpdate
ProcedureCallImpl은 StoredProcedureQuery의 구현체이다.
ProcedureCallImpl.execute()는 ResultSet이나 null을 가져온다.
@Override
public boolean execute() {
try {
final Output rtn = outputs().getCurrent();
return rtn != null && ResultSetOutput.class.isInstance( rtn );
}
catch (NoMoreReturnsException e) {
return false;
}
catch (HibernateException he) {
throw getExceptionConverter().convert( he );
}
catch (RuntimeException e) {
getProducer().markForRollbackOnly();
throw e;
}
}
ProcedureCallImpl.executeUpdate()은 현재 트랜잭션이 진행중인지 확인하고, 아니라면 예외를 발생시킨다. getProducer()를 통해 SharedSessonContractImplemtor 싱글톤 객체를 얻고, 해당 객체(세션)에서 트랜잭션 여부를 체크한다.
@Override
public int executeUpdate() {
getProducer().checkTransactionNeededForUpdateOperation(
"javax.persistence.Query.executeUpdate requires active transaction" );
// the expectation is that there is just one Output, of type UpdateCountOutput
try {
execute();
return getUpdateCount();
}
finally {
outputs().release();
}
}
// SharedSessionContractImplementor
default void checkTransactionNeededForUpdateOperation(String exceptionMessage) {
if ( !isTransactionInProgress() ) {
throw new TransactionRequiredException( exceptionMessage );
}
}
MSSQL은 기본적으로 오토커밋 모드이므로 execute()로 SP를 실행하면 애플리케이션 레벨에서 별도로 트랜잭션을 적용하지 않아도, 쿼리가 정상적으로 실행된다. 하지만, executeUpdate()로 SP를 실행하면 반드시 애플리케이션 레벨에서 @Transactional 어노테이션 등을 사용해서 트랜잭션을 적용해줘야 한다.
위와 같은 이유로 몇몇 SP들은 트랜잭션 적용 없이 잘 실행되었던 것이다. 현재 레거시 코드엔 execute나 executeUpdate가 섞여있는데, 개인적으로는 executeUpdate를 사용해서 명시적으로 트랜잭션을 적용시켜주는 게 좋은 것 같다.
4. 참고
- https://learn.microsoft.com/ko-kr/sql/connect/jdbc/reference/executeupdate-method?view=sql-server-ver16
- https://www.digitalocean.com/community/tutorials/jdbc-interview-questions-and-answers#execute-executeQuery-executeUpdate
- https://www.tabnine.com/code/java/methods/org.hibernate.procedure.internal.ProcedureCallImpl/getProducer
'JPA' 카테고리의 다른 글
[JPA] 실무에서 만난 N+1 문제 해결하기 (feat. LazyInitializationException) (1) | 2023.10.25 |
---|---|
[JPA] 스프링 데이터 JPA의 JpaRepository 구현체 분석 (0) | 2022.07.26 |
[JPA] 스프링 데이터 JPA에서 조인 컬럼(FK)을 조건으로 쿼리 메서드 만들 때 주의사항 (0) | 2022.07.22 |