Proxy

초코칩·2024년 5월 23일
0

spring

목록 보기
14/16
post-thumbnail

프록시 패턴(Proxy Pattern)은 소프트웨어 설계에서 객체 지향 프로그래밍의 디자인 패턴 중 하나로, 다른 객체에 대한 접근을 제어하는 대리인 역할을 하는 객체를 제공하는 패턴이다.

프록시는 스프링 AOP가 동작하는 데 있어서 큰 부분을 차지하며 스프링 AOP를 제대로 활용하기 위해서 Proxy를 제대로 이해해야 한다. 스프링AOP에서 Proxy가 어떻게 동작하는지 알아보자.

Proxy

스프링은 런타임 시점에 ApplicationContext의 Bean에 정의된 횡단 관심사(AOP)를 분석하고 프록시 Bean(내부 대상 Bean을 Wrapping한 Bean)을 동적으로 생성한다. 그리고 호출자에 대상 Bean을 주입해 이를 직접 호출하게 하는 대신 프록시 Bean을 주입한다. 그런 다음 프록시 Bean은 실행 조건(JoinPoint, PointCut, Advice, ...)을 분석하고 이에 따라 적절한 Advice를 위빙한다.

위빙은 애플리케이션 코드의 적절한 위치에 Aspect를 삽입하는 과정을 말한다. 컴파일 시점 AOP 솔루션에서는 일반적으로 위빙은 빌드 시점에 수행된다.

스프링은 내부에 JDK 동적 프록시와 CGLIB 프록시의 두 가지 프록시 구현체를 가지고 있다.

기본적으로 스프링은 Advice를 적용할 대상 객체가 특정 인터페이스를 구현하면 JDK 동적 프록시를 사용해 대상 프록시 인스턴스를 생성한다. 하지만 Advice를 적용할 대상 객체가 인터페이스를 구현하지 않으면 CGLIB를 사용해 프록시 인스턴스를 생성한다. 이렇게 하는 이유는 JDK 동적 프록시가 인터페이스 프록시만 지원하기 때문이다.

ProxyFactory

ProxyFactory 클래스는 스프링 AOP의 위빙과 프록시 생성 과정을 제어한다. 프록시를 생성하기 전에 항상 Advice를 적용하는 객체나 대상 객체를 지정해야 한다.

setTarget() 메서드를 통해 객체를 지정할 수 있다.

ProxyFactory는 내부적으로 프록시 생성 프로세스를 DefaultAopProxyFactory의 인스턴스에 위임한다. 이 인스턴스는 애플리케이션 설정에 따라 Cglib2AopProxy 또는 JdkDynamicAopProxy에 프록시 생성을 다시 위임한다.

스프링에서 사용할 수 있는 프록시 타입은 JDK Proxy 클래스를 사용해 인터페이스 기반으로 생성되는 JDK 프록시와 CGLIB Enhancer 클래스를 사용해 상속하여 생성된 CGLIB 기반 프록시의 두 가지 타입이 있다.

스프링에서는 클라이언트가 메서드를 요청하면 ProxyFactoryBean에서 인터페이스 유무를 확인하여 인터페이스가 있으면 JDK Dynamic Proxy를 호출하고 없으면 CGLIB 방식으로 프록시를 생성한다.

Proxy의 목적

프록시의 핵심 목적은 메서드 호출을 인터셉트하고 필요한 경우 특정 메서드에 적용된 Advice chain을 실행하는 것이다. Advice의 관리와 호출은 프록시와 독립적이며 스프링 AOP 프레임워크가 관리한다. 하지만 프록시는 모든 메서드 호출을 인터셉트하고 필요에 따라 Advice를 적용할 수 있도록 AOP 프레임워크에 전달해야할 책임이 있다.

이러한 핵심 기능 외에도 프록시는 다양한 추가 기능을 지원해야 한다. 프록시는 추상 클래스인 AopContext 클래스로 자신을 외부로 노출하도록 구성해 프록시를 가져오고 대상 객체의 프록시를 사용해 어드바이스가 적용된 메서드를 호출할 수 있다. 프록시는 ProxyFactory.setExposeProxy()를 통해 이 옵션을 활성화하면 프록시 클래스가 적절하게 노출되게 한다.

또한, 모든 프록시 클래스는 기본적으로 Advised 인터페이스를 구현한다. 이 인터페이스를 사용하면 프록시가 생성된 후 어드바이스 체인을 변경할 수 있다. 또한, 프록시는 프록시가 적용된 대상을 반환하는 모든 메서드가 실제로 대상이 아닌 프록시를 반환해야 한다.

이와 같이 일반적인 프록시는 많은 작업을 수행해야 하며, 이들 로직은 모두 JDK와 CGLIB 프록시에서 구현된다.

Proxy의 종류

JDK Dynamic Proxy

JDK 프록시는 스프링에서 사용할 수 있는 가장 기본적인 프록시 타입이다. CGLIB 프록시와 달리 JDK 프록시는 클래스가 아닌 인터페이스 프록시만을 생성할 수 있다. 이와 같이 프록시를 적용하는 모든 객체는 적어도 하나의 인터페이스를 구현해야 하고, 결과로 생성되는 프록시는 그 인터페이스를 구현하는 객체가 된다.

public interface MyService {
    void perform();
}

public class MyServiceImpl implements MyService {
    @Override
    public void perform() {
        System.out.println("Performing service");
    }
}

public class MyInvocationHandler implements InvocationHandler {
    private final Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before method call");
        Object result = method.invoke(target, args);
        System.out.println("After method call");
        return result;
    }
}

// Usage
MyService target = new MyServiceImpl();
MyService proxy = (MyService) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    new Class[]{MyService.class},
    new MyInvocationHandler(target)
);

proxy.perform();

CGLIB Proxy

CGLIB를 사용할 때는 CGLIB가 각 프록시에 대해 동적으로 새 클래스에 대한 바이트코드를 생성하고 이미 생성된 클래스를 사용할 수 있으면 재사용한다. 이때 생성되는 프록시 타입은 대상 객체 클래스를 상속 받은 하위 클래스가 된다.

public class MyService {
    public void perform() {
        System.out.println("Performing service");
    }
}

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Before method call");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("After method call");
        return result;
    }
}

// Usage
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class);
enhancer.setCallback(new MyMethodInterceptor());

MyService proxy = (MyService) enhancer.create();
proxy.perform();

Proxy 성능 비교

성능

  • CGLIB: 많은 호출을 수행할 때 CGLIB 프록시가 JDK 프록시보다 빠르다. 메서드가 처음 호출되었을 때 동적으로 타깃 클래스의 바이트 코드를 조작하고, 이후 호출 시엔 조작된 바이트 코드를 재사용한다.

CGIB는 타겟에 대한 정보를 직접적으로 제공 받고, 타겟 클래스에 대한 바이트 코드를 조작하여 프록시를 생성하므로 리플렉션을 사용하는 JDK Dynamic Proxy에 비해 성능이 좋다.

복잡성

  • CGLIB: CGLIB 프록시는 구현과 호출이 더 복잡하다. CGLIB 프록시는 클래스 경로에 CGLIB 라이브러리가 필요하다. CGLIB 프록시는 대상 클래스의 생성자와 인수를 알아야 한다.

  • JDK: JDK 프록시는 구현 및 호출이 더 간단하다. JDK 프록시는 외부 라이브러리가 필요하지 않다. JDK 프록시는 대상 클래스의 생성자와 인수를 신경 쓰지 않는다.

유연성

  • CGLIB: CGLIB 프록시는 최종 클래스가 아닌 모든 클래스를 프록시할 수 있다. CGLIB 프록시는 인터페이스에 선언되지 않은 메서드를 프록시할 수 있다. CGLIB 프록시는 필터를 사용하여 메서드를 선택적으로 프록시할 수 있다.

  • JDK: JDK 프록시는 하나 이상의 인터페이스를 구현하는 클래스만 프록시할 수 있다. JDK 프록시는 인터페이스에 선언된 메서드만 프록시할 수 있다. JDK 프록시는 모든 메소드에 대해 단일 호출 처리기를 사용한다.

profile
초코칩처럼 달콤한 코드를 짜자

0개의 댓글

관련 채용 정보