스프링 @Transactional 동작원리

이승우·2024년 5월 7일

프로젝트 마무리단계에서 갑자기 이런 오류가 떴다.

org.springframework.dao.InvalidDataAccessApiUsageException: No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call] with root cause

jakarta.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call
	at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:303) ~[spring-orm-6.1.2.jar:6.1.2]
	at jdk.proxy2/jdk.proxy2.$Proxy161.remove(Unknown Source) ~[na:na]
	at org.springframework.data.jpa.repository.query.JpaQueryExecution$DeleteExecution.doExecute(JpaQueryExecution.java:298) ~[spring-data-jpa-3.2.1.jar:3.2.1]
	at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:92) ~[spring-data-jpa-3.2.1.jar:3.2.1]
	at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:149) ~[spring-data-jpa-3.2.1.jar:3.2.1]
	at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:137) ~[spring-data-jpa-3.2.1.jar:3.2.1]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170) ~[spring-data-commons-3.2.1.jar:3.2.1]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158) ~[spring-data-commons-3.2.1.jar:3.2.1]
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:164) ~[spring-data-commons-3.2.1.jar:3.2.1]
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143) ~[spring-data-commons-3.2.1.jar:3.2.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.2.jar:6.1.2]
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:70) ~[spring-data-commons-3.2.1.jar:3.2.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.2.jar:6.1.2]

에러가 발생했는데 해당 트랜잭션을 담당하는 프록시 객체가 없다. -> Transaction을 요구하는 예외다
난 이렇게 결론을 내렸다.

에러가 발생한 service layer를 살펴보니,

    public Map<String, String> signIn(@Valid SignInDTO signInDTO) {
        Authentication authentication = authenticate(signInDTO);
        if (checkDuplicatedSignIn(signInDTO)) {
            removeExistingToken(signInDTO);
        }
        String accessToken = jwtTokenProvider.generateAccessToken(authentication);
        String refreshToken = jwtTokenProvider.generateRefreshToken(authentication);
        saveRefreshToken(signInDTO.getEmail(), refreshToken);

        return createTokenMap(accessToken, refreshToken);
    }

removeExistingToken에서 발생한다는 것을 알았고, Transactional 어노테이션이 붙어있지 않는걸 확인했다.
@Transactional을 붙여놓지 않으니, 서비스레이어를 Bean으로 등록할 때 프록시 객체를 생성하지 못했고 Transaction 작업을 하려는데 객체를 찾지 못해 이런 에러가 난 것이다.

구체적인 상황은, 서버를 다시 열고 원래 로그인한 기기가 아닌 다른 기기에서 로그인을 하면 에러가 발생했고 트랜잭션 작업을 수행하려는데 EntityManager를 찾을 수 없어서 난 에러다.

어노테이션을 붙이니 간단히 해결이 됐다.

이제까지 @Transactional에 무지했던 것 같아 자세히 공부하고 넘어가기로 했다.

1. @Transactional이란

만약 1,2,3의 CUD 작업을 할 때 3번에서 에러가 났을 때 1,2번만 db에 commit되는 것을 막고 1,2번 작업을 rollback해주는 행위.
A가 B에게 송금하려는데 A에게서 돈이 빠져나가고 와중에 에러가 나 B는 돈을 받지 못한다면? <- 이런 상황을 막을 수 있다.
따라서 DB에 쓰는 작업을 예외없이 한번에 묶어서 하는 행위라고 정의할 수 있을 것 같다.

2. Transaction 동작 과정

  1. 트랜잭션을 포함한 클래스가 생성되면 해당 클래스를 Proxy객체로 감싼다.
  2. 트랜잭션이 포함된 메서드가 호출되면 메서드안에 있는 작업들은 모두 하나로 묶인다.
  3. Proxy객체가 가로채 에러가 없다면 Commit, 있다면 Rollback을 한다.

3. 적용 위치

적용 위치는 우선순위와도 관련이 있다.

클래스 메소드 -> 클래스 -> 인터페이스 메소드 -> 인터페이스

  • 클래스 : 클래스에 @Transactional이 적용되면 해당 클래스의 모든 메서드 호출 시 트랜잭션을 실행한다.
    따라서 클래스에 선언할 때는 @Transactional(readOnly = true) 와 같이 읽기 전용으로 선언해놓고, CUD 작업을 하는 메서드에는 Transactional로 선언하여 사용할 수 있다.
    기본값으로 선언한다면 변경감지를 위해 스냅샷 인스턴스를 저장하기 때문에 메모리를 더 사용한다. 조회 작업만 하는 쿼리라면 readOnly옵션을 부여해서 메모리 사용량을 줄일 수 있다.
@Slf4j
@Service
@EnableWebSecurity
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder encoder;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;

Controller나 Repository가 아닌 Service layer에 적용을 시킨 이유도 있다.
Service단에서는 repository의 save, delete 등의 작업들을 호출하는데 이것을 한번에 관리하기 위해 service에서 선언한다고 한다.

그런데 만약 단순 조회를 하는 로직이라면??
service가 필요없는 controller-repository의 경우가 있을 수도 있다. 이런 특이한 경우엔 트랜잭션 없이 처리하거나, repository에 선언을 한다고 한다.
controller에는 절대 사용하지 않는다고 한다.

4. 적용 상황

Spring data jpa를 사용하는 경우에 단일 작업에 대해선 선언할 필요가 없다.
-> JPA 구현체의 모든 메서드에 선언이 되어있기 때문.
상위 메서드에서 트랜잭션이 시작되고 커밋, 롤백될 때까지 하위 메서드에서의 작업은 같은 트랜잭션 내에서 처리된다.
두개 이상의 작업을 진행할 때 상위 메서드에 선언하고 하위 메서드에 전파하는 방식으로 사용하면 된다.

0개의 댓글