[Spring] AOP - 관점 지향 프로그래밍 이란? 글에서 AOP에 대해 설명을 했었죠~! 혹시 못보셨다면 먼저 보고오시기 바랍니다!
다시 한번 짧게 요약하면, AOP(관점지향 프로그래밍)란 OOP를 보완하는 개념으로, 흩어진 관심사(cross-cutting concern)를 모듈화시킨 관점(Aspect)를 생성하여, 프록시 패턴(Proxy pattern)을 통해 비즈니스 로직에 영향없이 관점을 적용할 수 있는 그런 기법이었죠.
** 너무 한문장에 우겨넣긴했네요 ㅠ 혹시 이 문장이 이해가 안되신다면 여기를 꼭 먼저 보고와주세요!
자 오늘은 여기서 저 프록시(Proxy)에 대해서 알아볼겁니다. 이미 저 글에서 프록시 패턴에 대해 설명해주지 않았나요?? 라고 하신다면 디자인 패턴의 관점에서 프록시 패턴의 개념에 대해서 설명은 했었지만, Spring에서는 어떤 방식의 프록시를 쓰고있는지 설명을 안해드렸었습니다 흑흑 ㅠ
자 그럼 지금부터 Proxy의 세계로 가볼까요?
프록시(Proxy)란?
먼저 프록시에 대해서 다시한번 짚고 넘어가보겠습니다. 프록시란 "대리(행위)나 대리권, 대리 투표, 대리인" 등등의 뜻을 가지고 있습니다. 큰 개념에서 무언가를 대신해주는 것이라고 알면 될 것 같습니다. 아주 좋은 예시가 바로 대변인이겠죠!
대통령의 대변인은 대통령의 말을 직접 전달하고, 자신이 전달받은 것을 대통령에게 전달해주는 역할을 할 수 있겠죠. 따라서 대통령은 직접적인 소통없이도 대변인을 통해 간접적인 소통이 가능합니다.
프로그래밍에서도 똑같은 방식이 적용될 수 있습니다.
전에도 들었던 예시였죠. 클라이언트가 RealSubject 객체의 request() 메소드를 호출하고 싶을때, 직접 RealSubject를 불러와서 호출하지 않고, 프록시 객체가 대신 RealSubject객체의 request()를 호출하고 응답을 해주는 방식입니다. 이처럼 프록시를 쓰면 한단계가 추가적으로 생기지만 큰 장점이 있습니다.
- 첫번째: 실제 RealSubject의 request() 코드를 변경하지않고, 메소드 앞뒤로 필요한 기능을 추가할 수 있습니다. 가장 대표적인 예시로 로그를 찍는 것이 있죠.
- 두번째: 캐시를 사용할 수 있습니다. 프록시가 똑같은 request() 메소드가 호출되도록 명령받았다면, 굳이 RealSubject에 새롭게 request()를 호출하지 않고도 응답을 줄 수 있겠죠.
근데.. 의문이 드는게 있죠. 그럼 저렇게 만드려면 내가 Proxy 객체라는 것을 만들어야하나..? 만약 Spring AOP가 알아서 해준다면 Proxy 객체는 언제 생성되고 언제 없어지는거지..? 이 궁금증을 해결하러 지금 바로 가봅시다!
Spring 컨테이너(IoC 컨테이너)와 AOP 프록시
**혹시 IoC 컨테이너에 대한 설명이 필요하시다면 여기를 먼저 봐주세요!
자 일단 위 질문을 다시한번 본다면, Proxy 객체는 저희가 직접 생성할 필요는 없습니다. 그럼 누가 생성해주나요? 바로 IoC 컨테이너입니다! IoC 컨테이너는 특정 객체 (주로 Controller, Service 등)에 대해 알아서 생명주기를 관리해주죠. 프록시 객체도 마찬가지입니다.
위 그림처럼 IoC 컨테이너가 특정 호출 시점에 프록시 객체(Proxy Bean)를 생성해줍니다. 이렇게 동적으로 생성된 프록시 객체는 타깃의 메소드가 호출되는 시점에 (위 예시로 든다면 request() 메소드가 호출되는 시점) 부가기능을 추가할 메소드를 자체적으로 판단하고 가로채어 부가기능을 주입해줍니다. 이처럼 호출 시점에 동적으로 위빙(weaving) 한다고 하여 런타임 위빙이라고 부릅니다.
결국 Spring AOP는 위처럼 런타임 위빙 방식으로 proxy 객체(Proxy bean)를 만들고 이용하고 있으며 프록시 객체가 생성되는 방식에는 바로 JDK Proxy와 CGLIB Proxy 방식이 존재합니다.
서론이 너무 길었죠?? ㅠㅡㅜ 자 그럼 지금부터 알아봅시다.
JDK Proxy (JDK Dynamic Proxy)
JDK Dynamic Proxy (이하 JDK Proxy)란 Java의 reflect 패키지 (java.lang.reflect) 의 Proxy 클래스를 통해 생성된 Proxy 객체를 뜻합니다! 이름에 dynamic이 들어가는 이유는 위에서 설명한대로 Proxy 객체가 동적으로 생성되기 때문이죠.
import java.lang.reflect.Proxy;
// Proxy 객체 생성
Object proxy = Proxy.newProxyInstance(ClassLoader // 클래스로더
, Class<?>[] // 타깃의 인터페이스
, InvocationHandler // 타깃의 정보가 포함된 Handler
);
여러 특징이 있지만 JDK Proxy의 핵심은 타겟 클래스가 아니라 타겟의 인터페이스를 기준으로 Proxy가 생성된다는 점입니다.
// 인터페이스
public interface MyService {
String findUserId();
}
// 구현체
@Service
public class MyServiceImpl implements MyService {
@Override
public String findUserId(String input){
// 로직..
return param;
}
}
위 코드로 예시를 한번 들어보겠습니다.
위 그림을 설명하면, JDK Proxy는 타겟의 인터페이스(MyService)를 자체검증하고, ProxyFactory가 해당 인터페이스를 상속한 Proxy 객체를 생성합니다. 그리고 이 Proxy 객체에 InvocationHandler를 포함하여 하나의 객체로 반환하게 됩니다.
따라서 JDK Proxy 방식으로 Proxy객체를 생성하기 위해서는 반드시 인터페이스가 존재해야하고, @Autowired 를 통해 생성된 Proxy 객체를 사용하기 위해서는 반드시 인터페이스를 지정해주어야 합니다. 이를 위배한다면 Runtime 에러가 발생할 수 있습니다.
@Controller
public class MyController{
@Autowired
private MyServiceImpl myServiceImpl; // <- Runtime Error 발생...
// 로직
}
위 예시는 인터페이스가 아닌 타겟 클래스(구현체)를 주입받기 때문에 JDK Proxy 방식을 사용한다면 runtime exception이 발생하게 됩니다.
CGLib (Code Generator Library Proxy)
CGLib는 Code Generator Library 의 약자로, 클래스의 바이트코드를 조작하여 Proxy 객체를 생성해주는 라이브러리입니다.
JDK Proxy의 특징이 인터페이스를 기준으로 Proxy 객체를 생성해주는 것이었다면, CGLib의 특징은 인터페이스가 아닌 타겟 클래스에 대해서도 Proxy 객체를 생성할 수 있다는 점입니다. CGLib는 Enhancer 라는 클래스를 통해 Proxy를 생성합니다.
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MemberService.class); // 타깃 클래스
enhancer.setCallback(MethodInterceptor); // Handler
Object proxy = enhancer.create(); // Proxy 생성
JDK Proxy와는 다르게 CGLib는 타겟 클래스를 상속받고 타겟 클래스에 포함된 모든 메소드를 재정의하여 Proxy를 생성해줍니다. 장점이라고 한다면 CGLib는 바이트코드를 조작하여 Proxy를 생성해주기 때문에 성능이 JDK 프록시보다 좋습니다.
JDK Proxy VS CGLib
자 두개 다 알겠는데 그럼 뭘써야돼요?
처음에는 JDK Proxy가 권장되었습니다. 그리고 JDK Proxy가 Default Proxy 생성 방식이었죠. 그 이유는 다음과 같습니다.
- Java에서 기본적으로 지원하는 Proxy 클래스를 사용하는 것이기 때문에 의존성이 따로 필요없다. (CGLib 같은 경우는 net.sf.cglib.proxy.Enhancer 의존성이 필요)
- CGLib를 구현하기 위해선 반드시 NoArgs 생성자가 필요하다
- 생성된 CGLib Proxy의 메소드를 호출하면 타겟 클래스의 생성자가 2번 호출된다.
하지만 시간이 지나면서 Spring4.3, Spring Boot 1.4부터는 CGLib 방식이 Default가 되었는데요. 다음과 같은 개선사항이 있었습니다.
- Spring 3.2부터 CGLib가 Spring core 패키지에 들어가면서 의존성 추가필요없어짐
- Spring 4.0부터 Objensis 라이브러리의 도움을 받아 NoArgs 생성자 없이도 Proxy이 가능해지고, 생성자가 2번 호출되던 이슈도 수정
오늘은 JDK Proxy 와 CGLib에 대해 알아보았습니다. 이 글에서 너무 정리가 잘돼있고 깊게 다루어져 있어서 각 방식의 Proxy생성과정에 대한 더 자세한 내용을 알고싶으면 꼭 방문해서 읽어봐 주세요.
정리하면 JDK Proxy는 인터페이스를 구현하여 Proxy를 생성해주고, CGLib는 클래스를 상속받아 Proxy를 생성해주는 방식입니다. 처음에는 CGLib가 권장되지 않았지만, 시간이 지나고 문제점들이 개선되면서 지금은 CGLib가 Default 방식인점 기억해주시면 될것같습니다!
오늘도 읽어주셔서 감사합니다!
출처:
- [Spring] Proxy (2) Spring with proxy
- [Spring] Spring의 AOP 프록시 구현 방법(JDK 동적 프록시, CGLib 프록시)과 @EnableAspectJAutoProxy의 proxyTargetClass - (3/3)
- JDK Dynamic Proxy와 CGLIB의 차이점은 무엇일까?
'Spring' 카테고리의 다른 글
[Spring] @Transactional 어노테이션 동작방식 (0) | 2023.02.21 |
---|---|
[JPA] 비관적 락(Pessimistic Lock)과 낙관적 락(Optimistic Lock) (0) | 2022.11.01 |
[JPA] @OneToMany 단방향을 사용하면 안되는 이유 (0) | 2022.10.27 |
[JPA] @ManyToOne, @OneToMany 뭘 써야할까? (0) | 2022.10.13 |
[Spring Boot] @Transactional 어노테이션 속성 (0) | 2022.10.12 |