백엔드/Spring

[Spring] 동적 프록시(Dynamic Proxy)

2023. 5. 26. 01:34
목차
  1. 들어가며
  2. JDK 동적 프록시
  3. CGLIB
  4. CGLIB 제약
728x90

들어가며

기존에 정리했던 프록시 패턴(https://brightstarit.tistory.com/48)을 통해 기존 코드를 변경하지 않고 핵심 기능과 부가 기능을 나누어서 개발할 수 있었다.

하지만, 프록시 패턴을 사용하면 대상 클래스의 수만큼 프록시 클래스를 만들어야 한다는 단점이 있었다.

클래스를 계속 생성하면 관리 포인트가 늘어나서 유지보수하기 쉽지 않다. 예를 들어 적용 대상이 100개면 프록시 클래스도 100개를 만들어야 되는 것이다.

그런데 프록시 클래스의 기본 코드와 흐름은 거의 같고, 프록시를 어떤 대상에 적용하는가 정도만 차이가 있다.

 

이 문제를 해결하는 것이 바로 동적 프록시 기술이다. 동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 된다.

이름 그대로 프록시 객체를 동적으로 런타임에 개발자 대신 만들어준다.

이런 동적 프록시 기술로는 자바에서 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술이 있다.


JDK 동적 프록시

  • 자바에서 기본적으로 제공하는 동적 프록시 기술로 리플렉션(https://brightstarit.tistory.com/50)을 사용한다.
  • 리플렉션을 사용하기 때문에 성능상 이슈가 있다.
  • 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수다.

public interface AInterface {
    String call();
}

public interface BInterface {
    String call();
}

 

@Slf4j
public class AImpl implements AInterface{
    @Override
    public String call() {
        log.info("A 호출");
        return "a";
    }
}

@Slf4j
public class BImpl implements BInterface{
    @Override
    public String call() {
        log.info("B 호출");
        return "b";
    }
}

JDK 동적 프록시에 적용할 로직은 InvocationHandler 인터페이스를 구현해서 작성하면 된다.

public interface InvocationHandler {
 public Object invoke(Object proxy, Method method, Object[] args)
 throws Throwable;
}

제공되는 파라미터는 다음과 같다.

  • Object Proxy : 프록시 자신
  • Method method : 호출한 메서드
  • Object[] args : 메서드를 호출할 때 전달한 인수

TimeInvocationHandler.java

@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;
    }
}
  • TimeInvoationHander는 InvocationHandler인터페이스를 구현한다.
  • Object target : 동적 프록시가 호출할 대상
  • method.invoke(target, args) : 리플렉션을 사용해서 target 인스턴스의 메서드를 실행한다. args는 메서드 호출 시 넘겨줄 인수이다.

 JdkDynamicProxyTest.java

@Slf4j
public class JdkDynamicProxyTest {

    @Test
    void dynamicA() {
        AInterface target = new AImpl();
        TimeInvocationHandler handler = new TimeInvocationHandler(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());
    }


    @Test
    void dynamicB() {
        BInterface target = new BImpl();
        TimeInvocationHandler handler = new TimeInvocationHandler(target);

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

        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());
    }
}

DynamicA 실행 결과

21:18:49.153 [Test worker] INFO hello.proxy.jdkdynamic.code.TimeInvocationHandler - TimeProxy 실행
21:18:49.156 [Test worker] INFO hello.proxy.jdkdynamic.code.AImpl - A 호출
21:18:49.156 [Test worker] INFO hello.proxy.jdkdynamic.code.TimeInvocationHandler - TimeProxy 종료 resultTime=0
21:18:49.157 [Test worker] INFO hello.proxy.jdkdynamic.JdkDynamicProxyTest - targetClass=class hello.proxy.jdkdynamic.code.AImpl
21:18:49.157 [Test worker] INFO hello.proxy.jdkdynamic.JdkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy12
  • new TomeInvocationHandler(target) : 동적 프록시에 적용할 핸들러 로직이다.
  • 동적 프록시는 java.lang.reflect.Proxy를 통해서 생성할 수 있다.
  • 클래스 로더 정보, 인터페이스, 그리고 핸들러 로직을 넣어주면 된다. 그러면 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환한다.
  • proxyClass=class co m.sun.proxy.$Proxy12이 부분이 동적으로 생성된 프록시 클래스 정보이다.

실행 순서

  1. 클라이언트는 JDK 동적 프록시의 call()을 실행한다.
  2. JDK 동적 프록시는 InvocationHandler.invoke()를 호출한다. TimeInvocationHandler가 구현체로 있으므로 TimeInvocationHandler.invoke()가 호출된다.
  3. TimeInvocationHandler가 내부 로직을 수행하고, method.invoke(target, args)를 호출해서 target인 실제 객체(AImpl)를 호출한다.
  4. AImpl 인스턴스의 call()이 실행된다.
  5. AImpl 인스턴스의 call()의 실행이 끝나면 TimeInvocationHandler로 응답이 돌아온다. 시간 로그를 출력하고 결과를 반환한다.

CGLIB

  • 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
  • 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
  • 원래는 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다. 따라서 스프링을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있다.

구체 클래스 - ConcreteService.java

@Slf4j
public class ConcreteService {
    public void call() {
        log.info("ConcreteService 호출");
    }
}

JDK 동적 프록시에서 실행 로직을 위해 InvocationHandler를 제공했듯이, CGLIB는 MethodInterceptor를 제공한다.

public interface MethodInterceptor extends Callback {
 Object intercept(Object obj, Method method, Object[] args, MethodProxy 
proxy) throws Throwable;
}
  • obj : CGLIB가 적용된 객체
  • method : 호출된 메서드
  • args : 메서드를 호출하면서 전달된 인수
  • proxy : 메서드 호출에 사용

TimeMethodInterceptor.java

@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {

    private final Object target;

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

    @Override
    public Object intercept(Object obj, 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;
    }
}
  • TimeMethodInterceptor는 MethodInterceptor인터페이스를 구현해서 CGLIB 프록시의 실행 로직을 정의한다.
  • proxy.invoke(target, args) : 실제 대상을 동적으로 호출한다.
    • method를 사용해도 좋지만, CGLIB은 성능상 MethodProxy proxy를 사용하는 것을 권장한다.

CglibTest.java

@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();
    }
}
01:23:34.871 [Test worker] INFO hello.proxy.cglib.CglibTest - targetClass=class hello.proxy.common.service.ConcreteService
01:23:34.876 [Test worker] INFO hello.proxy.cglib.CglibTest - proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerByCGLIB$$25d6b0e3
01:23:34.877 [Test worker] INFO hello.proxy.cglib.code.TimeMethodInterceptor - TimeProxy 실행
01:23:34.887 [Test worker] INFO hello.proxy.common.service.ConcreteService - ConcreteService 호출
01:23:34.887 [Test worker] INFO hello.proxy.cglib.code.TimeMethodInterceptor - TimeProxy 종료 resultTime=10
  • Enhancer : CGLIB는 Enhancer를 사용해서 프록시를 생성한다.
  • enhancer.setSuperclass(CocreteService.class) : CGLIB는 구체 클래스를 상속받아서 프록시를 생성할 수 있다. 어떤 구체 클래스를 상속받을지 지정한다.
  • enhancer.setCallback(new TimeMethodInterceptor(target)) : 프록시에 적용할 실행 로직을 할당한다.
  • enhancer.create() : 프록시를 생성한다. 앞서 설정한 enhancer.setSuperclass(CocreteService.class)에서 지정한 클래스를 상속받아서 프록시가 만들어진다.

JDK 동적 프록시는 인터페이스를 구현(implement)해서 프록시를 만든다. CGLIB은 구체 클래스를 상속(extends)해서 프록시를 만든다.

 

CGLIB가 동적으로 생성하는 클래스 이름은 다음과 같은 규칙으로 생성된다.

  • 대상클래스$$EnhancerByCGLIB$$임의코드

CGLIB 제약

클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.

  • 부모 클래스의 생성자를 체크해야 한다.
    • CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
  • 클래스에 final 키워드가 붙으면 상속이 불가능하다. 
    • CGLIB에서는 예외가 발생한다.
  • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.
    • CGLIB에서는 프록시 로직이 동작하지 않는다.

 

참고 자료 : 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8

 

스프링 핵심 원리 - 고급편 - 인프런 | 강의

스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

'백엔드 > Spring' 카테고리의 다른 글

[Spring Cloud] Spring Cloud OpenFeign  (0) 2023.05.22
[Spring] Spring Boot Actuator  (0) 2023.05.18
[Spring] 스프링(Spring)  (0) 2023.04.28
[Spring] POJO(Plain Old Java Object)  (0) 2023.04.27
  1. 들어가며
  2. JDK 동적 프록시
  3. CGLIB
  4. CGLIB 제약
'백엔드/Spring' 카테고리의 다른 글
  • [Spring Cloud] Spring Cloud OpenFeign
  • [Spring] Spring Boot Actuator
  • [Spring] 스프링(Spring)
  • [Spring] POJO(Plain Old Java Object)
밝은별 개발자
밝은별 개발자
호기심 가득한 밤을 하나씩 밝히는 밝은별입니다.
밝은별 개발 블로그호기심 가득한 밤을 하나씩 밝히는 밝은별입니다.
밝은별 개발자
밝은별 개발 블로그
밝은별 개발자
전체
오늘
어제
  • 전체 글 (66)
    • 자소서 (0)
    • 백엔드 (56)
      • Java (15)
      • 네트워크 (5)
      • JPA (11)
      • Spring (5)
      • 운영체제 (8)
      • MSA (4)
      • etc (4)
      • 데이터베이스 (2)
      • 분산추적 (2)
    • 프로젝트 (3)
      • Petogram (2)
    • 책 (6)
      • 오브젝트 - 조영호 (6)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 스트림
  • OS
  • pojo
  • Spring
  • HTTP
  • Redis
  • 오브젝트
  • db
  • 조영호
  • JPA
  • ORM
  • Service discovery
  • EUREKA
  • MSA
  • Petogram
  • 데이터독
  • java8
  • 객체지향
  • 책
  • 데이터베이스
  • 상속
  • 캐시
  • 분산추적
  • Java
  • 디자인 패턴
  • 운영체제
  • 메모리
  • 이펙티브자바
  • 네트워크
  • 영속성컨텍스트

최근 댓글

최근 글

hELLO · Designed By 정상우.
밝은별 개발자
[Spring] 동적 프록시(Dynamic Proxy)
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.