제육's 휘발성 코딩
article thumbnail
반응형

프록시란?

프록시는 서버와 클라이언트 사이에서 통신을 대신해주는 역할을 하는 객체이다.

자바에서는 이 프록시를 통해서 JDK 동적 프록시CGLIB 프록시를 지원해서 프록시를 직접 생성하는 것이 아닌, 프록시 객체를 런타임 환경에서 자동으로 만들 수 있다.

JDK 동적 프록시는 인터페이스의 구현체일 경우 생성되며, CGLIB 동적 프록시의 경우 인터페이스가 없는 구현체 클래스일 경우에 생성 된다.

스프링에서는 AOP 적용을 인터페이스 구현없이 할 수 있도록 기본 방식으로 CGLIB를 채택하여 사용하고 있다. (레거시는 JDK 동적 프록시 사용)

JDK 동적 프록시와 CGLIB 프록시에 대해 알아보자.

 

프록시에 대한 개념은 하단의 포스팅을 참고하자.

https://sasca37.tistory.com/278

 

[디자인패턴] 프록시 (프록시 패턴, 데코레이터 패턴) 정리

프록시란? 프록시(Proxy)란 대리의 의미를 갖고 있으며 서버와 클라이언트 사이에서 통신을 대신 처리해주는 역할을 한다. 프록시는 객체안에서의 개념(디자인 패턴), 웹 서버에서의 개념 (포워드

sasca37.tistory.com

 

JDK 동적 프록시

JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. (인터페이스가 필수)

JDK 프록시는 InvocationHandler 인터페이스 구현을 통해 만들 수 있다. 예제 코드를 통해 JDK 동적 프록시를 알아보자.

JDK 동적 프록시 - 예제

JDK 동적 프록시 - InvocationHandler 인터페이스
package java.lang.reflect;
public interface InvocationHandler {
     public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}
  • 자바 리플렉션 (java.lang.reflect)에서 인터페이스를 제공한다. Object는 프록시 자신, Method 는 호출한 메서드, args 는 메서드를 호출할 때 전달한 파라미터 정보를 갖고 있다.
TimeInvocationHandler 클래스 - InvocationHandler 구현체
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

@Slf4j
public class TimeInvocationHandler implements InvocationHandler {

  private final Object target;

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

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      log.info("TimeProxy 실행");
      long startTime = System.currentTimeMillis();

      Object result = method.invoke(target, args);

      long endTime = System.currentTimeMillis();
      long resultTime = endTime - startTime;

      log.info("TimeProxy 종료 resultTime={}", resultTime);
      return result;
  }
}
  • Object target 이 동적 프록시를 호출할 대상이며, 리플렉션을 사용하여 target 인스턴스의 메서드를 실행해보자.
AInterface 인터페이스, AImpl 구현체
public interface AInterface {
    String call();
}

@Slf4j
public class AImpl implements AInterface{

    @Override
    public String call() {
        log.info("A 호출");
        return "a";
    }
}
  • 테스트를 위한 인터페이스와 구현체를 생성하자.
JdkDynamicProxyTest
import hello.proxy.jdkdynamic.code.*;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Proxy;

@Slf4j
public class JdkDynamicProxyTest {

  @Test
  void dynamicA() {
      AInterface target = new AImpl();

      TimeInvocationHandler handler = new TimeInvocationHandler(target); // AImpl 이 Target

      AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);

      proxy.call();
      log.info("targetClass={}", target.getClass());
      log.info("proxyClass={}", proxy.getClass());
  }
}
  • java.lang.reflect.Proxy 를 통해 동적프록시를 생성한다. (클래스로더 정보, 대상 인터페이스, 핸들러 정보)
  • Proxy는 Object 반환이므로 형변환을 통해 인터페이스로 만든 후 선언한 call() 메서드를 호출할 수 있다.
JdkDynamicProxyTest 실행결과
11:23:52.923 [Test worker] INFO hello.proxy.jdkdynamic.code.TimeInvocationHandler - TimeProxy 실행
11:23:52.924 [Test worker] INFO hello.proxy.jdkdynamic.code.AImpl - A 호출
11:23:52.924 [Test worker] INFO hello.proxy.jdkdynamic.code.TimeInvocationHandler - TimeProxy 종료 resultTime=0
11:23:52.925 [Test worker] INFO hello.proxy.jdkdynamic.JdkDynamicProxyTest - targetClass=class hello.proxy.jdkdynamic.code.AImpl
11:23:52.925 [Test worker] INFO hello.proxy.jdkdynamic.JdkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy12
  • 실행 결과를 보면 프록시의 클래스가 com.sun.proxy.$ProxyX 로 프록시가 정상 수행된 것을 확인할 수 있다.
JDK 동적 프록시 실행 순서

JDK 동적 프록시 실행 과정

  • client가 JDK 동적 프록시의 call() 메서드를 실행한다.
  • JDK 동적 프록시는 InvocationHandle.invoke() 를 호출하여, 구현체인 timeInvocationHandler.invoke() 를 실행한다.

CGLIB 동적 프록시

CGLIB는 Code Generator Library라는 의미로 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다. JDK 동적 프록시와 다르게 인터페이스 없이 구체 클래스만으로도 동적 프록시를 만들 수 있다.

CGLIB는 원래 외부 라이브러리지만, 스프링 프레임워크가 내부에 포함되어 있어서 스프링을 사용한다면, 별도의 설정 없이 사용할 수 있다.

MethodInterceptor - CGLIB 제공
package org.springframework.cglib.proxy;

import java.lang.reflect.Method;

public interface MethodInterceptor extends Callback {
  Object intercept(Object var1, Method var2, Object[] var3, MethodProxy var4) throws Throwable;
}
  • JDK 동적 프록시가 InvocationHandler를 제공하는 것처럼 CGLIB도 MethodInterceptor를 제공한다.
TimeMethodInterceptor - MethodInterceptor 구현체
import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {

  private final Object target;

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

  @Override
  public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
      log.info("TimeProxy 실행");
      long startTime = System.currentTimeMillis();

      Object result = methodProxy.invoke(target, args);

      long endTime = System.currentTimeMillis();
      long resultTime = endTime - startTime;

      log.info("TimeProxy 종료 resultTime={}", resultTime);
      return result;
  }
}
  • JDK 동적 프록시처럼 메서드를 invoke 하여 프록시가 실제 호출할 대상에 접근한다.
  • MethodMethodProxy 가 있는데 두 개 모두 동일한 기능을 하나 MethodProxy가 CGLIB 성능 상의 이점이 있어 권장한다고 한다.
CglibTest - CGLIB 적용 테스트
@Slf4j
public class CglibTest {
  @Test
  void cglib() {
      ConcreteService target = new ConcreteService();

      Enhancer enhancer = new Enhancer();
      enhancer.setSuperclass(ConcreteService.class);
      enhancer.setCallback(new TimeMethodInterceptor(target));
      ConcreteService proxy =  (ConcreteService) enhancer.create();
      log.info("targetClass={}", target.getClass());
      log.info("proxyClass={}", proxy.getClass());

      proxy.call();
  }

  static class ConcreteService { // 인터페이스가 없는 구체 클래스
    public void call() {
        log.info("ConcreteService 호출");
    }
    }
}
  • CGLIB는 Enhancer를 사용해서 구체 클래스 상속과 실행로직(콜백)을 할당 받아 프록시를 생성한다.
CglibTest - 실행결과
16:56:33.655 [Test worker] INFO hello.proxy.cglib.CglibTest - targetClass=class hello.proxy.common.service.ConcreteService
16:56:33.658 [Test worker] INFO hello.proxy.cglib.CglibTest - proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerByCGLIB$$25d6b0e3
16:56:33.658 [Test worker] INFO hello.proxy.cglib.code.TimeMethodInterceptor - TimeProxy 실행
16:56:33.663 [Test worker] INFO hello.proxy.common.service.ConcreteService - ConcreteService 호출
16:56:33.664 [Test worker] INFO hello.proxy.cglib.code.TimeMethodInterceptor - TimeProxy 종료 resultTime=6
  • CGLIB를 통해 동적 프록시가 생성되어 대상클래스$$EnhancerByCGLIB$$임의코드 형태로 생성된다.

CGLIB 주의 사항

CGLIB 동적 프록시 방식은 클래스 기반으로 상속 받아 사용하기 때문에 몇 가지 주의할 점이 있다.

  • 부모 클래스의 생성자 기본 생성자 체크 (자식 클래스를 동적으로 생성)
  • 클래스 또는 메서드에 final 키워드가 붙지 않았는 지 확인 (클래스는 상속 불가, 메서드는 오버라이딩 불가)

 


본 포스팅은 인프런 - 스프링 핵심 원리 - 고급편을 참고하여 작성하였습니다. 

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^