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

Spring의 Transactional 동작 원리

Spring의 @Transactional의 동작원리에 대해 알아보자.

@Transactional 애노테이션은 AOP를 사용하여 구현되는데,

그 특징으로는 transaction의 begincommit을 메인 로직 앞 뒤로 수행해주는 기능을 담당한다.

아래는 필자의 토이프로젝트 코드이다.

@Service
@AllArgsConstructor
public class BoardServiceImpl implements BoardService{
    private final PostRepository postRepository;

    @Override
    @Transactional
    public Post writePost(PostDTO postDTO) {
        Post post = Post.builder()
                .title(postDTO.getTitle())
                .content(postDTO.getContent())
                .build();
        return postRepository.save(post);
    }
}

writePost 메소드에 @Transactional이 붙어있는 것을 확인할 수 있다.

@Transactional은 메서드가 실행되기 전 begin을 호출하고, 메서드가 종료한 후 commit을 호출한다.

이처럼 Spring AOP는 기본적으로 디자인 패턴 중 하나인 Proxy 패턴을 사용하여 구현되는데,

Spring에서 사용하는 두 가지 프록시 구현체가 있다.

하나는 JDK Proxy(Dynamic Proxy라고도 부름)와 CGLib이다.

둘의 차이는 아래 그림을 보면 한 눈에 파악할 수 있다.

스크린샷 2021-05-23 오후 9 33 47

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

구체적인 구현 코드는 아래와 같다.

public class BoardServiceProxy {
    private final BoardService boardService;
    private final TransactonManager manager = TransactionManager.getInstance();

    public BaordServiceProxy (BoardService boardService) {
        this.boardService = boardService;
    }

    public void writePost(PostDTO postDTO) {
        try {
            manager.begin();
            boardService.writePost(postDTO);
            manager.commit();
        } catch ( Exception e ) {
            manager.rollback();
        }
    }
}

JDK Proxy를 쓰면 되는거 아닌가? 왜 CGLib라는 것도 있는것이지?

Springboot의 경우 기본적으로 프록시 객체를 생성할 때 CGLib를 사용하고 있다.

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

리플렉션 자체가 비용이 비싼 API이기도 하고 Effective Java 책에서도 리플렉션을 가급적 사용하지 않는 것을 추천하고 있다.

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

우리가 의무적으로 서비스 계층에서 인터페이스 -> XXXXImpl 클래스를 작성하던 관례도 다 이러한 JDK Proxy의 특성 때문이기도 하다. (물론이 것이 항상 나쁜것은 아니다. 장점도 분명 있다. )

public class ExamDynamicHandler implements InvocationHandler {
    private ExamInterface target; // 타깃 객체에 대한 클래스를 직접 참조하는것이 아닌 Interface를 이용

    public ExamDynamicHandler(ExamInterface target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        // TODO Auto-generated method stub
        // 메소드에 대한 명세, 파라미터등을 가져오는 과정에서 Reflection 사용
        String ret = (String)method.invoke(target, args); //타입 Safe하지 않는 단점이 있다.
        return ret.toUpperCase(); //메소드 기능에 대한 확장
    }
}

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

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

JDK Proxy의 경우 자바에서 기본적으로 제공하고 있는 기능이다.

CGLib

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

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

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

// 1. Enhancer 객체를 생성
Enhancer enhancer = new Enhancer();
// 2. setSuperclass() 메소드에 프록시할 클래스 지정
enhancer.setSuperclass(BoardServiceImpl.class);
enhancer.setCallback(NoOp.INSTANCE);
// 3. enhancer.create()로 프록시 생성
Object obj = enhancer.create();
// 4. 프록시를 통해서 간접 접근
BoardServiceImpl boardService = (BoardServiceImpl)obj;
boardService.writePost(postDTO);

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

BoardServiceProxy.writePost(postDTO) -> BoardServiceImpl.writePost(postDTO)

enhancer.setCallback(NoOp.INSTANCE);라는 코드가 존재하는데 이는 Enhancer 프록시 객체가 직접 원본 객체에 접근하기 위한 옵션이다.

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

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

BoardServiceProxy -> BoardServiceInterceptor -> BoardServiceImpl

자바 리플렉션 방식보다 CGLib의 MethodProxy이 더 빠르고 예외를 발생시키지 않는다고 하여 Springboot에서는 CGLib를 기본 프록시 객체 생성 라이브러리로 채택하게 되었다.

Reference

https://stackoverflow.com/questions/29650355/why-in-spring-aop-the-object-are-wrapped-into-a-jdk-proxy-that-implements-interf

https://mommoo.tistory.com/92

https://jaehun2841.github.io/2018/07/21/2018-07-21-spring-aop3/#jdk-dynamic-proxy

https://javacan.tistory.com/entry/114



© 2022. by minkuk

Powered by minkuk