Spring

[중요!] @Transactional(AOP)가 동작하지 않는 이유 this.update();

백엔드 유성 2024. 10. 31. 00:01

이전부터 작성하고 싶었던 주제였습니다.

 

@Transactional 같은 AOP를 사용할 때 종종 발생하는 문제 중 하나는

"어노테이션을 붙였는데 값이 업데이트되지 않지? AOP가 작동하지 않지?" 같은 상황입니다.

코드상으로도 문제없어 보이고 에러도 나지 않지만, 기능이 제대로 작동하지 않을 때가 있죠.

 

다음 예제 코드에서 그 문제를 확인해 보겠습니다.

updateUserWithLogging() 메서드를 호출하면 로그는 정상적으로 출력되지만,

user 객체의 업데이트가 이루어지지 않는 문제가 있습니다.

UserService.class

 

여기서 @Transactional이 적용되어 있으므로 TransactionManager가 실행되어야 합니다. 그러나 UserController에서 updateUserWithLogging() 메서드를 호출할 경우 로그는 정상적으로 찍히지만, 데이터베이스 업데이트가 이루어지지 않습니다.

 

이 문제를 설명하기에 앞서 Spring에서 Bean을 어떻게 생성하는지 확인해보겠습니다.

빈 생성 방식에 따른 Proxy 객체

Spring에서 AOP를 적용할 때, 프록시 생성 방식은 다음과 같습니다:

  • 인터페이스가 있는 경우: JDK 동적 프록시를 사용해 인터페이스를 구현한 Proxy 객체가 생성됩니다.
  • 인터페이스가 없는 경우: CGLIB 같은 바이트 코드 조작 라이브러리를 통해 클래스를 상속하는 Proxy 객체를 만듭니다.

이 코드는 인터페이스가 없으므로, Spring은 CGLIB를 사용해 런타임 시점에 Proxy 객체를 생성하게 됩니다.

Proxy 객체의 동작을 이해하기 위해 UserServiceProxy 예제 코드를 보겠습니다.

UserServiceProxy.class

 

여기서는 스프링이 자동으로 생성해주는 프록시와 유사하게 빈을 직접 생성한 예시입니다.

(Spring 버전에 따라 프록시 생성 방식에 차이가 있을 수 있습니다.)

 

이제 UserServiceProxy 인스턴스에서 updateUserWithLogging() 메서드를 호출하는 상황을 예로 들어보겠습니다.

delegate.updateUserWithLogging(user) 호출

UserServiceProxy 인스턴스에서 delegate.updateUserWithLogging(user) 를 호출합니다.

this.updateUser() 메서드 호출

로그를 찍고 UserService 인스턴스에서 this.updateUser() 메서드를 호출합니다.

 

이 때 중요한 부분은 this입니다.

여기서 this는 Spring이 생성한 프록시 객체가 아닌 UserService 자체의 인스턴스를 가리킵니다.

 

그럼 한번 더 들어가보겠습니다.

마지막 실행 메서드

네. 보시면 PlatformTransactionManager 를 전혀 사용하고 있지 않습니다.

해당 코드는 단순히 껍대기 어노테이션만을 가지고 있고 Transaction Manager, commit, rollback에 대한 처리가 아무것도 없습니다.

(어노테이션 자체는 아무런 역할을 하지 않습니다.)

 

AOP가 동작하지 않는 이유: this.method() 호출 시 Proxy 미작동

Spring에서 AOP는 프록시 객체를 통해 메서드 호출을 가로채는 방식으로 작동합니다

Proxy 객체는 특정 조건에 맞는 메서드가 호출되면 AOP advice를 실행할 수 있도록 메서드 호출을 감쌉니다.

그러나 클래스 내부에서 this.method()로 메서드를 호출하면 Proxy 객체가 아닌 원본 객체의 인스턴스를 사용하게 되어, AOP advice가 적용되지 않습니다.

 

예시 코드

@Component
public class ExampleService {

    public void outerMethod() {
        this.innerMethod();
    }

    @Transactional
    public void innerMethod() {
        // 로직
    }
}

 

위 코드에서 outerMethod()는 같은 클래스의 innerMethod()를 호출합니다. 이 경우, 프록시를 통하지 않고 직접 호출되므로 @Transactional이 적용되지 않습니다.

 

이 부분은 Spring을 사용할 때 정말 중요한 내용이라고 생각하며, 이 글이 중요한 이유는 다음과 같습니다.

  • 만약 해당 코드가 작성되어 있으면, 런타임 시점에 버그가 나타납니다.
  • (컴파일 시점에 해당 버그를 확인할 수 없습니다.)
  • 중요한 로직이라면 서비스 장애까지 갈 수 있습니다.
  • 커밋 내역에 몇백줄이 커밋이 되어있으면, 해당 코드를 찾기가 정말 어렵습니다.
  • 개발팀이 붙어서 몇시간동안 버그만 찾을지도 모릅니다..