Study/Java&Spring

@Transactional Annotation 과 AOP 그리고 CGLib 와 Dynamic Proxy(JDK Proxy)

kdhoooon 2022. 1. 3. 15:14

트랜잭션이란?

  • 데이터베이스의 상태를 변경하는 작업 또는 한번에 수행되어야 하는 연산들을 의미
  • 트랜잭션은 4가지의 성질을 가지고 있다.
    • 원자성(Atomicity)
      • 한 트랜잭션 내에서 실행한 작업들은 하나의 단위로 처리한다. 즉, 모두 성공 또는 모두 실패
    • 일관성(Consistency)
      • 트랜잭션은 일관성 있는 데이터베이스 상태를 유지한다.
    • 격리성(Isolation)
      • 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 격리해야한다.
    • 영속성(Durability)
      • 트랜잭션을 성공적으로 마치면 결과가 항상 저장되어야 한다.

 

Spring 에서 제공하는 @Transactional 의 기능

  • 트랜잭션 begin, commit 을 자동으로 수행해준다.
  • 예외 발생 시 rollback 처리를 자동으로 수행해준다.
  • AOP를 사용하여 구현

 

 

기능 제공 방식

JPA의 객체 변경감지는 transaction 이 commit 될 때, 작동합니다.

그렇기에 spring은 @Transactional 어노테이션을 선언한 메서드가 실행되기전, transaction begin 코드를 삽입하며

메서드가 실행된 후, transaction commit 코드를 삽입하여, 객체 변경감지를 수행하게 유도합니다.

 

Spring의 코드삽입방법

  1. 바이트 코드생성(CGLIB 사용) -> SpringBoot 에서 기본적으로 사용
  2. 프록시 객체 사용 -> Spring에서 기본적으로 사용

Spring AOP는 기본적으로 디자인 패턴 중 하나인 Proxy 패턴을 사용하여 구현되는데, Spring에서 사용하는 두 가지 프록시 구현체가 있다.

하나는 JDK Proxy(=Dynamic Proxy)와 CGLib이다.

 

둘의 차이는 다음 그림과 같다.

 

JDK Proxy의 경우 AOP를 적용하여 구현된 클래스의 인터페이스를 프록시 객체로 구현해서 코드를 끼워 넣는 방식

 

왜 두가지가 존재하는지?( JDK Proxy vs CGLib Proxy)

Stringboot의 경우 기본적으로 프록시 객체를 생성할 때 CGLib를 사용.

 

이유는, JDK Proxy가 프록시 객체를 생성할 때 내부적으로 Reflction을 사용하고 있기 때문이다.

Reflection는 자체가 비용이 비싼 API이기 때문에 가급적 사용하지 않는 것을 추천하고 있다.

 

또, JDK Proxy의 경우 AOP 적용을 위해서 반드시 인터페이스를 구현해야한다는 단점이 있다.

그동안 서비스 계층에서 인터페이스 -> XXXimpl 클래스를 작성하던 관례가 이러한 JDK Proxy의 특성 때문

 

Dynamic Proxy는 InvocationHandler 라는 인터페이스를 구현한다.

InvocationHandler의 invoke 메소드를 오버라이딩 하여 Proxy 위임 기능을 수행하는데, 이 때 메소드에 대한 명세와 파라미터를 가져오는 과정에서 리플렉션(Reflection)을 사용한다.

 

그럼 CGLib 이란?

CGLib의 경우 외부 3rd party Library이며 JDK Proxy와는 달리 리플렉션을 사용하지 않고 바이트코드 조작을 통해 프록시 객체생성을 하고 있다.

 

또, 인터페이스를 구현하지 않고도 해당 구현체를 상속받는 것으로 문제를 해결하기 때문에 성능상에 이점이있다.

 

CGLib은 Enhancer라는 클래스를 바탕으로 Proxy를 생성한다.

상속을 통해 프록시 객체가 생성되기 때문에 더욱 성능상에 이점을 누릴 수 있다.

 

기본적으로 프록시 객체들은 직접 원본 객체를 호출하기 보다는, 별도의 작업을 수행하는데 CGLib의 경우 Callback을 사용한다.

 

CGLib에서 가장 많이 사용하는 콜백은 net.sf.cglib.proxy.MethodInterceptor인데, 프록시와 원본 객체 사이에 인터셉터를 두어 메소드 호출을 조작하는 것을 도와줄 수 있게 된다.

 

정리

JDK Proxy(Dynamic Proxy)

  • 프록시 객체를 생성할 때 Reflection을 사용하기 때문에 비용이 많이 든다.
  • AOP를 적용하기 위해 반드시 인터페이스를 구현해야한다. 

CGLib

  • Stringboot 에서 기본적으로 채택한 프록시 객체 방식
  • JDK Proxy의 위 두가지 단점때문에 등장
    • Reflection 없이 바이트 코드 조작을 통해 객체생성
    • 인터페이스를 구현하지 않고도 해당 구현체를 상속받는 것으로 문제를 해결하기 때문에 성능상의 이점
  • 상속을 통해 프록시 객체가 생성

 

옵션

  • isolation
    • 트랜잭션에서 일관성없는 데이터 허용 수준을 설정한다.
  • propagation
    • 트랜잭션 동작 도중 다른 트랜잭션을 호출할 때, 어떻게 할 것인지 지정하는 옵션이다.
  • noRollbackFor
    • 특정 예외 발생시 rollback하지 않는다.
  • timeout
    • 지정한 시간 내에 메소드 수행이 완료되지 않으면 rollback 한다. (-1일 경우 timeout을 사용하지 않는다.)
  • readOnly
    • 트랜잭션을 읽기 전용으로 설정한다.

 

@Transactional이 권장 사용 방법

@Transactional 메서드는 내부적으로 사용하지 않는 것이 근본적인 해결책입니다.(클래스 내부적으로 사용할 경우 프록시 객체로 인해서 메서드가 정상 작동하지 않을 수 있다.)

하지만 굳이 사용해야 한다면, 의존성 주입을 이용하여 Proxy 인스턴스를 자체적으로 가져와 사용할 수 있다.