-
@Transactional을 final class에 붙이면 안되는 이유(AOP의 동작원리)개발/Spring(boot) 2025. 1. 12. 23:06
업무를 하던 도중 for문을 돌며 데이터를 DB에 넣어주는 코드를 발견했다.
그리고 데이터를 저장하는 연산이라 for문을 호출하는 메서드에 @Transactional을 붙여야 할 것 같아 해당 내용을 검토를 받고 어노테이션을 붙여주고자 했다.
하지만 그 코드에 @Transactional을 붙이자 스프링부트 앱이 실행이 안되는 현상이 발생했다.
결론적으로 원인은 final class에 @Transactional을 붙여서 생긴 문제였고,
final class에 @Transactional을 붙이면 안되는 이유는 아래와 같았다.
1. @Transactional은 Spring AOP를 기반으로 동작하고,
2. AOP는 CGLIB Proxy를 생성하는 것 기반으로 동작한다.
3. 추가적으로 @Transactional을 붙였을 때 클래스에 대한 정보를 가져오기 위해서는 super class로부터 정보를 가져와야 했다.
4. 그래서 final class는 CGLIB Proxy 객체를 생성할 수 없으므로 해당 내용 수정이 필요했다.
그리고 이번 글은 Spring docs를 찾아보며 해당 내용에 대해 작성해보고자 한다.
1. @Transactional은 Spring AOP를 기반으로 동작
Understanding the Spring Framework’s Declarative Transaction Implementation
Spring Framework의 선언적 트랜잭션 지원(declarative transaction support)에서 이해해야 할 가장 중요한 개념은 이 지원이 AOP 프록시를 통해 활성화된다는 것과 트랜잭션 관련 조언(advice)이 메타데이터(XML 또는 어노테이션 기반)에 의해 구동된다는 점입니다. AOP와 트랜잭션 메타데이터의 결합은 메서드 호출 주위에서 트랜잭션을 처리하기 위해 TransactionInterceptor와 적절한 TransactionManager 구현체를 사용하는 AOP 프록시를 생성합니다.
다음 이미지는 트랜잭션 프록시에서 메서드를 호출하는 개념적인 모습을 보여줍니다.
@Transactional은 AOP를 기반으로 동작한다고 한다.
2. AOP는 CGLIB Proxy를 생성하는 것 기반으로 동작
Spring AOP는 주어진 타겟 객체에 대해 JDK 동적 프록시 또는 CGLIB를 사용하여 프록시를 생성합니다. JDK 동적 프록시는 JDK에 내장되어 있는 반면, CGLIB는 일반적인 오픈 소스 클래스 정의 라이브러리로 (spring-core에 재패키징되어) 제공됩니다.
타겟 객체가 하나 이상의 인터페이스를 구현하는 경우 JDK 동적 프록시가 사용되며, 타겟 타입이 구현한 모든 인터페이스가 프록시됩니다. 타겟 객체가 어떤 인터페이스도 구현하지 않으면, CGLIB 프록시가 생성되며 이는 타겟 타입의 런타임에서 생성된 서브클래스입니다.
AOP는 JDK 동적 프록시 또는 CGLIB(Code Generator Library)를 통해 프록시를 생성한다고 한다.
그리고 타겟 객체가 어떤 인터페이스도 구현하지 않으면, CGLIB 프록시가 생성되며 이는 타겟 타입의 런타임에서 생성된 서브클래스 라고 하는데, 이 내용이 이해가 안가서 조금 더 찾아보았다.
그림으로 봐도 이해가 힘들었고 해당 내용을 좀 더 찾아보았다.
예를들어 아래와 같은 코드가 있다고 가정하자.
public class MyClass{ public void foo() { bar(); } public void bar() { // some logic... } }
그렇다면 프록시 객체는아 래와 같이 서브 클래스로 생성된다
public class MyProxyClass extends MyClass{ public void proxyFoo() { this.foo(); } }
즉 아래와 같은 방식으로 동작한다고 이해했다.
일반 호출의 경우
프록시 객체의 경우
이렇게 이해했다.
Spring AOP의 경우 호출 전(@Before), 호출 이후(@After), 호출 전체(@Around) 등으로 관리할 수 있으므로 이와같이 프록시 객체가 생성된다고 생각하면 이해가 되었다.
3. @Transactional을 붙였을 때 클래스에 대한 정보를 가져오기 위해서는 super class로부터 정보를 가져와야 함
그렇다면 동일한 맥락으로 이 내용도 이해가 간다.
public class MyClass{ public void foo() { bar(); } public void bar() { // some logic... } }
@Transaction을 붙여 Spring AOP에 의해 아래와 같이 Proxy 객체가 생성되었다면
public class ProxyClass extends MyClass{ public void proxyFoo() { this.foo(); } }
ProxyClass의 super class(MyClass)를 호출해야 foo의 정보를 가져올 수 있게 된다.
즉 아래와 같이 @Transactional을 붙이기 전과 후에 동일하게 동작하기 위해서는 아래와 같이 코드 수정이 필요하다.
public class InitApplication{ public void init() { MyClass.class.getSimpleName(); // @Transactional이 있기 전 MyClass.class.getSuperclass().getSimpleName(); // @Transactional을 붙인 후 } }
4. final class는 CGLIB Proxy 객체 생성 불가
2번에서 작성한 내용의 docs에 final class 관련 내용도 작성돼있다.
CGLIB 프록시를 강제로 사용하고 싶다면(예: 타겟 객체의 인터페이스에 정의된 메서드뿐만 아니라 모든 메서드를 프록시로 만들기 위해) 그렇게 할 수 있습니다. 하지만 다음과 같은 문제를 고려해야 합니다:
- final 클래스는 프록시화할 수 없습니다. 왜냐하면 이를 상속할 수 없기 때문입니다.
- final 메서드는 프록시할 수 없습니다. 왜냐하면 이를 오버라이드할 수 없기 때문입니다.
- private 메서드는 프록시할 수 없습니다. 왜냐하면 이를 오버라이드할 수 없기 때문입니다.
- 보이지 않는 메서드(예: 다른 패키지의 부모 클래스에서 정의된 패키지-프라이빗 메서드)는 프록시할 수 없습니다. 이는 사실상 private 메서드와 같기 때문입니다.
CGLIB는 클래스를 상속하여 프록시를 생성하므로 상속이 불가능하게 만들어주는 키워드(final, private ..)등이 붙은 클래스는 프록시화할 수 없는게 당연하다.
유사한 내용으로 JDK Dynamic Proxy가 있는데 이는 인터페이스를 기반으로 프록시를 생성한다.
그래서 final 클래스에 대해 프록시화가 가능하지만 인터페이스가 있어야만 사용 가능하고, 메서드는 오직 인터페이스에서 정의된 것만 프록시화할 수 있다.
결론적으로 @Transactional을 final class에 붙이면 안되는 이유는
@Transactional은 AOP를 통해 동작하고 AOP는 CGLIB는 클래스를 상속하여 프록시를 생성하므로 상속이 불가능하게 만들어주는 키워드(final, private ..)가 붙어서는 안된다.
특정 메서드에 @Transactional을 붙이려고 시도했으나 붙였을 때 오류가 있었고 이에 대해 찾아보니 AOP, Proxy와 같은 내용이 나와 이에 대해 작성해보았다.
메서드가 호출하는 클래스의 final 키워드를 제거해서 해결했고 기존 클래스 정보를 가져오던 코드를 super class로부터 클래스 정보를 가져오도록 변경해서 해결했다.
AOP의 동작 원리에 대해 이해했고 얕게나마 설명 가능한 수준이 된거 같아 유익했다.
출처
반응형'개발 > Spring(boot)' 카테고리의 다른 글
Custom Annotation을 만들고 AOP로 활용하는 법 (0) 2025.01.19 스프링의 DB 관련 예외 추상화 (0) 2025.01.05 ApplicationEventPublisher와 EventListener (0) 2024.12.01