이번 포스트를 보기 앞서 이전 포스트를 보고 오시기 바랍니다.
https://jaystorage.tistory.com/25
Proxy 패턴
프록시 패턴은 대상 원본 객체를 대리하여 대신 처리하게 함으로써 로직의 흐름을 제어하는 행동 패턴이다. 프록시(Proxy)의 사전적인 의미는 '대리인'이라는 뜻이다. 즉, 누군가에게 어떤 일을 대신 시키는 것을 의미하는데, 이를 객체 지향 프로그래밍에 접목해보면 클라이언트가 대상 객체를 직접 쓰는게 아니라 중간에 프록시(대리인)을 거쳐서 쓰는 코드 패턴이라고 보면 된다. 따라서 대상 객체의 메소드를 직접 실행하는 것이 아닌, 대상 객체에 접근하기 전에 프록시(Proxy) 객체의 메서드를 접근한 후 추가적인 로직을 처리한 뒤 접근하게 된다.
예제) 클라이언트는 request() 메소드 호출 -> 프록시 객체가 대신 RealSubject객체의 메소드를 호출하고 그 반환 값을 클라이언트에 전달
[Proxy 패턴의 장점]
- 실제 메소드의 코드를 변경하지 않고 필요한 기능을 추가하는 것이 가능
- 프록시가 내부 캐시를 유지하여 데이터가 캐시에 아직 존재하지 않는 경우에만 대상에서 작업이 실행되도록 할 수 있다.
그런데 우리는 Spring AOP를 사용하면서 프록시 객체를 따로 만든 적이 없다. 그 얘기는 즉 Spring AOP가 자동으로 프록시 객체를 생성한다는 것을 의미한다.
Spring 컨테이너(IoC 컨테이너)와 AOP 프록시
Spring AOP에서 프록시 객체를 생성하는 주체는 IoC 컨테이너이다. IoC 컨테이너는 특정 객체 (주로 Controller, Service 등)에 대해 알아서 생명주기를 관리하는데 프록시 객체 또한 마찬가지다.
위 그림에서 볼 수 있듯이 IoC 컨테이너는 특정 호출 시점에 AOP를 할 수 있는 프록시 객체(Proxy Bean)을 생성한다. 이렇게 동적으로 생성된 프록시 객체는 타깃의 메소드가 호출되는 시점에 (위 예시로 든다면 request() 메소드가 호출되는 시점) 부가기능을 추가할 메소드를 자체적으로 판단하고 가로채어 부가기능을 주입한다. 이처럼 호출 시점에 동적으로 위빙(weaving) 한다고 하여 런타임 위빙 (Runtime Weaving)이라고 부른다.
Weaving : 정의된 관점(Aspect)을 타겟 객체에 적용하여 부가적인 기능을 주입하는 과정
Spring AOP Proxy의 두 가지 구현 방법(JDK Dynamic Proxy와 CGLib Proxy)
Spring은 AOP Proxy를 생성하는 과정에서 자체 검증 로직을 통해 타깃의 인터페이스 유무를 판단한다. 만약 이때 타겟 객체가 하나 이상의 인터페이스를 구현하고 있는 클래스라면 JDK Dynamic Proxy 방식으로, 인터페이스를 구현하지 않은 클래스라면 CGLib의 방식으로 AOP 프록시를 생성한다.
JDK Dynamic Proxy
JDK Dynamic Proxy (이하 JDK Proxy)란 Java의 reflect 패키지 (java.lang.reflect) 의 Proxy 클래스를 통해 생성된 Proxy 객체를 의미한다. 명칭에서 알 수 있듯이 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 발생
// (MyServiceImpl 이 아니라 인터페이스인 MyService를 DI 해야 함)
// 로직
}
CGLib Proxy (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 Dynamic Proxy VS CGLib Proxy
처음에는 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 Proxy 생성 방식이 되었는데 그 이유는 다음과 같은 개선사항이 있기 때문이었다.
- Spring 3.2부터 CGLib가 Spring core 패키지에 들어가면서 따로 의존성을 추가할 필요가 없어짐
- Spring 4.0부터 Objensis 라이브러리의 도움을 받아 NoArgs 생성자 없이도 Proxy이 가능해지고 생성자가 2번 호출되던 이슈도 수정됨
'Spring' 카테고리의 다른 글
Spring, Spring Boot 차이 (0) | 2024.07.14 |
---|---|
@Transactional 동작 방식 (0) | 2024.07.02 |
AOP와 Spring AOP (0) | 2024.06.26 |
SL4J와 Logback을 이용한 Logging (0) | 2024.03.03 |
Filter, Interceptor, AOP (0) | 2023.11.29 |