늘
AOP에 대한 사실과 오해 그런데 트랜잭션을 사알짝 곁들인.. 본문
해당 글은 조금 각색해서 우아한테크코스 블로그(테코블)에 있습니다!
트랜잭션을 사용할 때, 아래의 사진처럼 private 메서드에 걸면 컴파일 에러가 나오는 것을 확인할 수가 있다.
인텔리제이가 알려주는 메시지를 보면 private 만 사용하지 않으면 된다고 한다. 이 이유에 대해서는 spring 2.5버전 이후부터는 default로 CGLIB을 사용하므로 상속을 통해 프록시를 구현한다. 하지만 private메서드는 상속이 불가능하기 때문에 프록시를 만들 수 없기 때문이다. 하지만 스프링 공식문서를 보면 public 이외의 모든 메서드는 트랜잭션이 적용되지 않는다고 한다.
실제 protected로 하면 컴파일단에선 예외가 잡히지 않는다. 왜냐하면 프록시는 만들어지기 때문이다. 하지만 스프링 공식문서를 보면 트랜잭션이 적용이 되지 않는다고 한다. 적용하려면 aspectJ의 compile-time or load-time weaving을 적용하면 된다고 한다.
주의할 점
- public이외의 메서드는 AOP가 걸리지 않는다.
- 동일한 클래스(빈) 내에서 @Transanctional이 선언되지 않은 메서드에서 @Transactional이 선언된 메서드를 호출해도 트랜잭션이 적용되지 않는다.
- @Transactional(propagation=Propagation.REQUIRES_NEW)사용할 때, 동일한 클래스(빈)의 메서드끼리 호출하면 새로 생성되지 않음. 반드시 다른 클래스 메서드를 호출해야 함.
그런데 왜?? 안되는 걸까?
1번 이유
스택 오버플로우에서 참고하였다. 이유는 JDK와 CGLIB의 구분 없이 일관되게 적용하기 위해서라고 한다! 👍👍왜냐하면 JDK 동적 프록시는 인터페이스를 기반으로 한 프록시를 만든다. 따라서 protected를 사용하면 프록시를 만들 수가 없기 때문에 JDK 동적 프록시까지 고려하여 public이 아닌 메서드에서는 aop가 걸리지 않는다.
2, 3번 이유
이번에는 공식문서에 나와있다.
In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that 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 even if the invoked method is marked with @Transactional . Also, the proxy must be fully initialized to provide the expected behavior, so you should not rely on this feature in your initialization code — for example, in a @PostConstruct method.
트랜잭션은 프록시를 통해 들어오는 외부 메서드 호출만 인터셉트된다. 즉, Spring AOP 는 외부 메서드의 호출만 인터셉트한다는 것이다!!!
왜냐하면 프록시의 내부 빈에서 프록시를 호출했기 때문이다. 사진으로 보면 이해가 도움이 될 것 같다.
progress()가 일반 메서드일 때, init()을 호출해도 프록시는 이미 프록시 객체 안에 있기 때문에 프록시를 인터셉트할 수 없는 것이다.
하지만 위의 3가지 경우는 AspectJ를 사용하면 가능하다는데 왜 그럴까?
→ 공식문서를 잘 보자..
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…
타겟 내에서 타겟의 다른 메서드를 호출할 때, 런타임에 실제 트랜잭션이 작동하지 않는다. 즉, 런타임 시점에 작동은 안 하지만 이것을 컴파일 시점에 적용하면 된다는 것이다. 이미 프록시로 감싸졌으므로, 따라서 AspectJ를 사용한 바이트코드 조작은 프록시로 감싸기 전에 적용되므로 같은 빈 내부에서도 호출이 가능한 것 같다!
즉, springAOP는 동적프록시를 사용하여 런타임 시점에 위빙을 하지만 AspectJ는 바이트코드를 조작하여 컴파일 시점에 위빙을 하기 때문에 가능하다.
AOP 용어
- Target : 어떤 대상에게 부가할것인지
- Advice: 부가기능을 언제 부여할 것인가. (Before, After..)
- JoinPoint: 어디에 적용할 것인가(적용 가능 대상): 메서드 , 필드 등 (스프링AOP는 메서드만 가능)
- pointCut: 실제 advice가 적용되는 곳 (joinPoint 대상들 중에 적용)
예시
@Around ← Advice
빨간색이 ← pointCut
Weaving(위빙)
Aspect 모듈을 통해 실제 프록시 객체를 생성하기 위한 과정. 즉 공통 코드를 핵심 로직 코드에 삽입하는 것을 weaving이라고 한다
AspectJ
- 컴파일 시점에 위빙한다. 위빙 방식은 3가지가 있다.
- 성능이 스프링 AOP보다 빠르다.
- 스프링에 종속되지 않고 자바 전역에 적용이 가능하다. ex) 메서드, 필드 등
Spring AOP
- 런타임 시점에 동적으로 위빙한다. (JDK 또는 CGLIB 사용)
- AspectJ보다 성능이 안 좋다.
- 스프링이 관리하는 빈에서만 적용이 가능하다.
- 설정하기 편하다. 또한 AspectJ와 다르게 컴파일 시점에 건드리는 게 없어서 각종 라이브러리(Lombok)과 호환성이 뛰어나다.
추가
트랜잭션은 인터페이스, 인터페이스의 메서드 그리고 클래스, 클래스의 메서드에 정의할 수 있다. 하지만 모두 정의만 한다고 실행되는 것은 아니다. 공식문서에서는 클래스에 트랜잭션을 거는 것을 추천한다고 한다.
JDK기반 프록시를 실행시켰을 때, 인터페이스를 적용하는 게 좋다고 한다, 왜냐하면 앞선 테코톡에서도 말했지만 스프링에서 CGLIB을 디폴트로 하는 이유도 인터페이스 기반 프록시는 ClassCast Exception 추적이 어렵다. 그래서인지 Aop도 인터페이스에 걸지 말라고 하는 것 같다.
마무리
트랜잭션에 대해서 좀 더 알아간 것 같아서 뿌듯하다. 테코톡을 준비하면서 CGLIB과 JDK를 공부했던게 이렇게 도움이 될줄은 몰랐다. 테코톡을 준비했던 덕분에 쉽게 이해할 수 있었던 것 같다!!!
Reference
'우아한테크코스 4기' 카테고리의 다른 글
서블릿의 요청 처리 과정 (0) | 2022.09.27 |
---|---|
SQL Injection 그리고 PreparedStatement (2) | 2022.09.17 |
[JPA] hibernate.ddl-auto 설정 (0) | 2022.07.14 |
[JPA]EnableJpaAuditing을 Application 위에 쓰면 안되는 이유 (0) | 2022.07.12 |
[h2]DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE; (0) | 2022.07.10 |