TransactionRequiredException 예외 메시지로 두가지를 경험해보았습니다.
no transaction is in progress 와 Executing an update/delete query 입니다.
발생하는 이유에 대해서는 잘 정리된 글들이 많아서, 각 메시지별 해결방법 위주로 작성해보았습니다.
@Transactional 어노테이션 누락 확인
트랜잭션으로 관리할 메서드에 종종 어노테이션을 누락한 적이 있습니다.
//@Transactional
public void update() throws Exception { //... }
트랜잭션 메서드 접근제어자 / 불변 키워드 확인
@Transactional 은 AOP로 구현되는데, 스프링 부트 2.0 이후부터는 기본적으로 CGLib이란 라이브러리를 활용하여 AOP를 구현합니다.
CGLib로 AOP를 구현하려면, 해당 메서드는 외부 접근가능해야하고 상속 가능해야합니다. (final 클래스, final 메서드 안 됨)
IDE 에서 에러를 표기해줄 수는 있지만, 컴파일은 정상적으로 됩니다. 런타임에러 주의해야겠습니다.
//public 이어야 함, final 없어야 함
@Transactional
public /*final*/ void update(){ //... }
내부 메서드로 호출하고 있지는 않는지 확인
접근제어자가 public이어도 같은 클래스 내의 메서드로 호출하면 안됩니다.
IDE에 따라 아래와 같은 경고 메시지가 나오며 런타임에서 에러가 발생합니다.
@Transactional self-invocation (in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime
public void outer() {
inner();
}
@Transactional
public void inner() { //...정상 동작하지 않음 }
(참고) outer 메서드에 @Transactional를 선언한다면 정상 동작합니다. inner()의 @Transactional은 아무 의미 없습니다.
no transaction is in progress
JPA @Lock 어노테이션으로 명시적인 락을 획득하려는 경우, 호출 당시 활성 상태의 트랜잭션이 존재해야합니다. 이 메서드를 호출한 쪽에서 트랜잭션이 제대로 걸려있는지 확인해야합니다.
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.stereotype.Repository;
import javax.persistence.LockModeType;
@Repository
public interface MemberRepository extends JpaRepository<MemberEntity, String> {
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
Optional<MemberEntity> findBySearchKeyword(String searchKeyword);
}
Executing an update/delete query
@Modifying 과 같이 JPQL, native query 로 레코드 업데이트나 삭제를 할 경우, 호출 당시 활성 상태의 트랜잭션이 존재해야합니다.
이 메서드를 호출한 쪽에서 트랜잭션이 제대로 걸려있는지 확인해야합니다.
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface MemberRepository extends JpaRepository<MemberEntity, String> {
@Modifying
@Query(value = "update ... set ... where id = :id", nativeQuery = true)
void update(String id);
}
(참고1) 정확히는... 조건절에 파라미터가 있는 경우에만 해당 예외가 발생하였습니다. (그럴일은 없겠지만) 'update member set age = 29' 처럼 아무 조건없는 네이티브 쿼리 요청에서는 정상적으로 업데이트 되었습니다.
(참고2) 이 주제와 상관 없지만, dirty check를 활용한 엔티티 업데이트에서도 @Transaction 으로 관리되는지에 따라 동작이 달라집니다. 이부분도 같이 보면 좋을 것 같습니다.
회고
편리할수록 추상적인 것 같습니다. 여러 라이브러리들과 프레임워크는 편리한 개발을 도와주지만 역설적으로 (잘 모르고 사용할 경우) 개발자의 의도대로 동작하지 않을 가능성이 많습니다. 잘 모르고 사용하는 일이 없도록 주의하도록 하겠습니다.
'경험과 지식' 카테고리의 다른 글
제네릭, 와일드카드 처음부터 끝까지 이해하기[with PECS, <E extends Comparable<? super E>> E max(Collection<? extends E> c)] (0) | 2023.12.27 |
---|---|
에러 FOR UPDATE cannot be applied to the nullable side of an outer join(postgres) (0) | 2023.12.11 |
트랜잭션 격리수준 4단계 테스트 해보기(mysql innoDB) (2) | 2023.12.10 |
JPA N+1문제와 해결방안(2) (0) | 2023.04.10 |
JPA N+1문제와 해결방안(1) (0) | 2022.03.25 |