Spring Boot/AOP

JDK Dynamic Proxy, CGLIB 그리고 AOP

수수한개발자 2023. 5. 2.
728x90

프록시 패턴

프록시 패턴이라는 디자인 패턴이 있다.

프록시 패턴은 실제 기능을 수행하는 객체 대신 가상의 대리자(프록시)를 사용하여 로직의 흐름을 제어하는 디자인 패턴이다.

 

프록시 패턴의 활용 

  • 원래 하려던 기능을 수행하며 그 외의 부가적인 작업 (로깅, 캐싱, 인증, 트랜잭션 등)을 별도로 수행할 수 있다.
  • 비용이 많이 드는 연산 (DB 쿼리)를 실제로 필요한 시점까지 미룰 수 있다.

 

 

프록시 객체 만드는 법 

기존의 객체를 프록시를 만드는 법은 두가지가 있다. 

1. JDK Dynamic Proxy

2. CGLIB

 

JDK Dynamic Proxy

JDK Dynamic Proxy 는 Java의 리플렉션 패키지에 존재하는 Proxy라는 클래스를 통해 생성된 프록시 객체를 의미 한다.

타겟클래스를  리플렉션의 Proxy 클래스가  동적으로 프록시 객체를 생성해주므로 JDK Dynamic Proxy 라고 한다.

 

프록시 객체 생성 

Object proxy = Proxy.newProxyInstance(ClassLoader       // 클래스로더
                                    , Class<?>[]        // 타겟의 인터페이스
                                    , InvocationHandler // 타겟의 정보가 포함된 Handler
                                                        );

 

위 코드와 같이 단순히 리플렉션 Proxy.newProxyInstance() 메소드를 사용하면 된다. 그리고 전달 받은 파라미터를 가지고 프록시 객체를 생성해준다.

타겟의 인터페이스에 대해 자체 적인 검증 로직을 거치고, ProxyFactory에 의해 타겟에 인터페이스를 상속한 프록시 객체를 생성한다.

프록시 객체에 InvocationHandler를 포함하여 하나의 객체로 변환한다.

 

JDK Dynamic Proxy의 가장 큰 특징은 인터페이스를 기준으로 인터페이스만 프록시 객체를 만들 수 있다는 점이다.

따라서 구현체는 인터페이스를 상속받아야 하며 프록시 빈을 사용하기 위해서는 반드시 인터페이스 타입으로 지정해야 한다.

 

@RestController
public class OrderController {
	
    private final PaymentService paymentService; // < Runtime Excetpion 발생
}


@Service
public class PaymentService implements OrderService {
	...
}

예를 들어 스프링 AOP를 통해 OrderService 프록시 빈이 만들어졌다면 이때 PaymentService(구현체) 를 대상으로 DI를 받게 되면 예외가 발생한다.

 

 

내부 검증 로직

프록시 패턴은 접근 제어 목적 및 사용자의 요청이 기존의 타겟을 그대로 바라볼 수 있도록 타겟에 대한 위임 코드를 프록시 객체에 작성하기 위해 사용된다. 이러한 위임 코드는 InvocationHandler에 작성해야 한다.

 

사용자의 요청에 의해 Proxy 메소드가 호출되면, 내부적으로 invoke에 대한 내부 검증 로직이 일어난다.

public Object invoke(Object proxy, Method proxyMethod, Object[] args) throws Throwable {
  Method targetMethod = null;

  // 주입된 타겟 객체에 대한 검증 코드
  if (!cachedMethodMap.containsKey(proxyMethod)) {
    targetMethod = target.getClass().getMethod(proxyMethod.getName(), proxyMethod.getParameterTypes());
    cachedMethodMap.put(proxyMethod, targetMethod);
  } else {
    targetMethod = cachedMethodMap.get(proxyMethod);
  }

  // 타겟의 메소드 실행
  Ojbect retVal = targetMethod.invoke(target, args);
  return retVal;
}

JDK Dynamic Proxy는 인터페이스에 대한 Proxy만 생성하기 때문에, 개발자가 타겟에 대한 정보를 잘 못 주입할 경우를 대비하여 내부적으로 타겟 객체에 관한 검증 코드를 형성하고 있다.

 

장단점

  • 장점
    • 개발자가 직접 프록시 객체를 만들 필요가 없다.
  • 단점
    • 프록시하려는 클래스는 반드시 인터페이스의 구현체여야한다.
    • 리플렉션을 활용하므로 성능이 떨어진다.

 

예제 코드

@Slf4j
public class JdkDynamicProxyTest {

    static interface ProxyInterface {
        void call(String name);
    }

    static class Kimjisu implements ProxyInterface {
        @Override
        public void call(String name) {
            log.info(name);
        }
    }

    static class MyInvocationHandler implements InvocationHandler {
        private final ProxyInterface target;

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

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            log.info("proxy 전");
            Object result = method.invoke(target, args);
            log.info("proxy 후");
            return result;
        }
    }

    @Test
    void jdkDynamicTest() {
        Kimjisu kimjisu = new Kimjisu();
        MyInvocationHandler handler = new MyInvocationHandler(kimjisu);
        ProxyInterface proxy = (ProxyInterface) Proxy.newProxyInstance(
                ProxyInterface.class.getClassLoader(),
                new Class[]{ProxyInterface.class},
                handler);

        proxy.call("JDK Dynamic Proxy");
        kimjisu.call("call");

    }
}

ProxyInterface 인터페이스의 구현체 Kimjisu를 만들고 위에서 설명한 핸들러를 작성한다.

핸들러는 원본 객체의 메소드를 호출 전, 후 로그를 찍고 리플렉션의 Proxy.newProxyInstance() 메소드를 사용 하여 ProxyInterface의 프록시 객체를 만들 수 있다. 

 

00:12:00.750 [Test worker] INFO hello.aop.JdkDynamicProxyTest -- proxy 전
00:12:00.753 [Test worker] INFO hello.aop.JdkDynamicProxyTest -- JDK Dynamic Proxy
00:12:00.753 [Test worker] INFO hello.aop.JdkDynamicProxyTest -- proxy 후
00:12:00.753 [Test worker] INFO hello.aop.JdkDynamicProxyTest -- call

프록시 객체는 프록시 전, 후 로그가 찍히고 일반 객체는 로그 없이 call 로그만 찍히게 된다.

 

 

 

CGLIB

 

CGLIB는 Code Generator Libray의 약자로, 클래스의 바이트 코드를 조작하여 프록시 객체를 생성해 주는 라이브러리다. CGLIB를 사용하면 인터페이스가 아닌 타겟 클래스에 대해서도 프록시 객체를 만들어 줄 수 있고, 이 과정에서 Enhancer라는 클래스를 활용한다.

 

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MemberService.class); // 타겟 클래스
enhancer.setCallback(MethodInterceptor); // Handler
Object proxy = enhancer.create(); // Proxy 생성

 

CGLIB는 인터페이스가 아닌 타겟 클래스를 상속 받아서 프록시를 생성해준다.

이 과정에서 CGLIB는 타겟 클래스에 포함된 메소드를 재 정의 하고, 타겟 클래스에 대한 바이트 코드를 조작하여 프록시를 생성한다. 따라서 CGLIB를 적용할 클래스는 final메소드가 들어 있거나 final 클래스면 재정의 하지 못하기 때문에 안된다.

접근 제한자 또한 private은 안된다.

 

장단점

  • 장점
    • 인터페이스 없이 단순 클래스만으로도 프록시 객체를 동적으로 생성해 줄 수 있다.
    • 리플렉션이 아닌 바이트 조작을 사용하며, 타겟에 대한 정보를 알고 있기 때문에 JDK Dynamic Proxy에 비해 성능이 좋다.
  • 단점
    • 의존성을 추가해야 한다. (Spring 3.2 이후 버전의 경우 Spring Core 패키지에 포함되어 있음)
    • default 생성자가 필요하다. (현재는 objenesis 라이브러리를 통해 해결)
    • 타겟의 생성자가 두 번 호출된다. (현재는 objenesis 라이브러리를 통해 해결)

 

CGLIB 예제 코드

@Slf4j
public class CGLIBTest {

    static interface ProxyClass {
        void call(String name);
    }

    static class Kimjisu implements ProxyClass {
        @Override
        public void call(String name) {
            log.info(name);
        }
    }
    static class MyMethodInterceptor implements MethodInterceptor {

        private final ProxyClass target;

        public MyMethodInterceptor(ProxyClass target) {
            this.target = target;
        }

        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            log.info("proxy 전");
            method.invoke(target, args);
            log.info("proxy 후");
            return null;
        }
    }

    @Test
    void CGLIBProxyTest() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ProxyClass.class);
        enhancer.setCallback(new MyMethodInterceptor(new Kimjisu()));
        ProxyClass proxy = (ProxyClass) enhancer.create();
        proxy.call("CGLIB");
    }
}

CGLIB 라이브러리를 사용하여 프록시 객체를 만들어 보자.

JDK Dynamic Proxy 예제에서 사용한 코드를 거의 그대로 가져오고, Handler만 수정하자. 기존 InvocationHandler 대신 CGLIB 라이브러리 소속인 MethodInterceptor를 사용하면 된다.

 

00:27:27.471 [Test worker] INFO hello.aop.CGLIBTest -- proxy 전
00:27:27.473 [Test worker] INFO hello.aop.CGLIBTest -- CGLIB
00:27:27.473 [Test worker] INFO hello.aop.CGLIBTest -- proxy 후

정상적으로 프록시를 거쳐서 로직이 수행된 것을 확인할 수 있다.

CGLIB는 인터페이스가 없는 일반 클래스에 대해서 프록시를 만들어준다는 것이 큰 특징이다.

 

 

JDK Dynamic Proxy와 CGLIB의 성능 차이

CGIB는 타겟에 대한 정보를 직접적으로 제공 받고, 타겟 클래스에 대한 바이트 코드를 조작하여 프록시를 생성하므로 리플렉션을 사용하는 JDK Dynamic Proxy에 비해 성능이 좋다.
또한 CGLIB는 메소드가 처음 호출되었을 때 동적으로 타겟 클래스의 바이트 코드를 조작하고, 이후 호출 시엔 조작된 바이트 코드를 재사용한다.

 

 

AOP 란?

AOP는 Aspect Oriented Programing의 약자로 관점 지향 프로그래밍을 의미한다.

관점 지향은 어떤 로직을 수행할때 핵심적인 관점, 부가적인 관점으로 나누고 그 관점을 기준으로 각각 따로 분리 하겠다는 의미이다. 여기서 분리한다는 의미는 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.

예를 들어, 핵심적인 관점은 비즈니스 로직이 되고, 부가적인 관점은 로깅, 실행 시간 측정, 트랜잭션등이 될 수 있다.

 

JDK Dynamic Proxy, CGLIB 그리고 AOP - AOP 란?
https://www.javaguides.net/2019/05/understanding-spring-aop-concepts-and-terminology-with-example.html

 

위 그림과 같이 각 레이어에서 반복 되는 기능들을 흩어진 관심사라고 부르며, 이렇게 흩어진 관심사들을 Aspect로 모듈화 하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP 목적이라고 한다.

 

 

 

AOP 주요 개념 

Aspect
횡단 관심사의 깔끔한 모듈화 
어드바이스 + 포인트컷을 모듈화 한것 
@Aspect를 생각하면됨, 여러 어드바이스와 포인트 컷이 함께 존재
조인 포인트(Join point) 어드바이스가 적용될 수 있는 위치, 프로그램 실행 중 지점
ex) 메소드 실행, 필드 값 접근 등등..
어드바이스(Advice) 부가 기능, 특정 조인 포인트에서 Aspect에 의해 취해지는 조치
포인트컷(pointcut) 조인 포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능
타겟(Target) 어드바이스를 받는 객체, 포인트컷으로 결정
어드바이저(Advisor) 하나의 어드바이스와 하나의 포인트컷으로 구성
스프링 AOP에서만 사용되는 특별한 용어
위빙(Weaving)
포인트컷으로 결정한 타켓의 조인 포인트에 어드바이스를 적용하는 것
AOP 프록시
AOP 기능을 구현하기 위해 만든 프록시 객체
스프링에서 AOP 프록시는 JDK 동적 프록시 또는 CGLIB 프록시이다.

 

 

AOP 적용 방식

  • 컴파일 시점에 코드에 공통 기능을 삽입하는 방법
  • 클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입하는 방법
  • 런타임에 프록시 객체를 생성하여 공통 기능을 삽입하는 방법 (프록시 패턴)

 

AOP 동작 원리

스프링 AOP는 프록시 패턴을 사용한다.

JDK Dynamic Proxy, CGLIB 그리고 AOP - AOP 동작 원리 - 모든 영역

 

프록시는 타겟을 감싸서 타겟의 요청을 대신 받아주는 객체이다.

클라이언트에서 타겟을 호출하게 되면 타겟이 아닌 프록시가 호출된다.

이때 프록시는 타겟 전후로 부가 기능을 실행하도록 구성할 수 있다.

하지만 프록시 패턴을 타겟 하나하나 프록시 객체를 정의해주어야 하기 때문에 번거롭고 코드의 중복이 생기게 된다.

그래서 Spring AOP는 런타임 시점에서 JDK Dynamic Proxy, CGLIB를 활용하여 프록시를 생성해주는데 이를 

런타임 위빙(Runtime wraving)이라고 부르며, 빈 포스트 프로세서를 통해 타겟 객체를 새로운 프록시 객체로 적용하는 과정을 의미한다.

 

여기서 Spring AOP는 인터페이스의 유무에 따라서 인터페이스면 JDK Dynamic Proxy, 인터페이스가 아니라면 CGLIB 방식으로 프록시 객체를 생성해준다.

 

출처

https://www.inflearn.com/course/스프링-핵심-원리-고급편/dashboard

 

 

 

 

728x90

댓글