2월 2주차 스터디 발표 자료📖
2주차 기술면접 주제 중 세번 째 주제입니다!
프록시 개념에 대해 정리하고자 합니다.
참고(10분 테코톡): https://www.youtube.com/watch?v=MFckVKrJLRQ&t=345s
프록시란?
- 클라이언트로부터 타깃(Real Object)을 대신해서 요청을 받는 대리인
- 실제 오브젝트인 타깃은 프록시를 통해 요청을 받아 처리함
- 타깃은 자신의 기능에만 집중하고 프록시에게 부가기능을 위임한다.
- 프록시 패턴을 통해 Proxy를 직접 구현할 수 있다.
프록시 사용 목적
- 클라이언트가 타깃에 접근하는 방법을 제어하기위해
ex. 지연로딩, 접근 제어(권한) - 타깃에 부가적인 기능을 부여해줄 때
ex. 시간을 측정하는 로직을 추가한다, Transactional 기능을 부여한다.
프록시 패턴의 장점
- OCP(개방폐쇠의 원칙)
기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있다. - SRP(단일 책임의 원칙)
기존 코드가 해야 하는 일만 유지할 수 있다. 즉, 부가적인 기능은 프록시에게 맡기면 된다.
프록시 패턴의 단점
- 코드의 복잡도가 증가한다.
- 중복 코드가 발생한다.
➔ 이러한 문제를 해결해주는 것이 동적 프록시이다.
프록시 패턴의 구조(예시)
- 프록시 패턴의 흐름
- 인터페이스를 선언하고
- 해당 인터페이스를 구현한 proxy 객체와 타깃(Real Object)객체를 생성한 뒤
- Client의 요청을 HelloProxy가 대신 처리하도록 구현한다.
동적 프록시의 종류?
1. JDK Dynamic Proxy
- 프록시 클래스를 직접 구현하지 않아도 된다.
➔ 코드 복잡도 해소 - Invocation Handler
➔ Invocation Handler를 통해 중복 코드를 제거할 수 있다. - Invocation Handler 예시
밑의 코드를 보면 Proxy 객체에서 toUpperCase()라는 중복 코드가 발생하고 있다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
public class HelloProxy implements Hello { private Hello hello; public HelloProxy(Hello hello){ this.hello = hello; } @Override public String sayHello(String name){ // 1. 기존 sayHello 메서드에 대문자로 출력하는 부가 기능을 추가 return hello.sayHello(name).toUpperCase(); } @Override public String sayHi(String name){ // 2. toUpperCase() 라는 중복 코드 발생 return hello.sayHi(name).toUpperCase(); } @Override public String sayThankYou(String name){ // 3. toUpperCase() 라는 중복 코드 발생 return hello.sayThankYou(name).toUpperCase(); } }
위의 문제점은 InvocationHandler를 재정의하여 해결할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.Method;
public class UpperCaseHandler implements InvocationHandler {
// 부가 기능을 제공할 타킷(Real Object)
private final Object target;
// 다이나믹 프록시로부터 전달받은 요청을
// 다시 타깃 오브젝트로 위임해야하므로, 타깃 오브젝트 주입받는다.
public UpperCaseHandler(Object target)}{
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().startsWith("say")){
return ((String) method.invoke(target,args)).toUpperCase(); //타깃에게 위임
}
return method.invoke(target,args); //타깃에게 위임
}
}
JDK Dynamic Proxy의 동작과정
- 클라이언트가 메소드를 요청한다.
- JDK Dynamic Proxy는 Invocation Handler에 메소드 처리를 위임한다.
- Invocation Handler는 부가 기능을 수행 후 Target에게 메소드 처리를 위임한다.
JDK Dynamic Proxy의 특징
- JDK에서 지원하는 프록시 생성 방법
- Reflection API를 사용하기때문에 느리다.
- ⭐ 인터페이스가 반드시 있어야한다.(프록시 생성 시, 반드시 필요함)
⭐ Invocation Handler를 재정의한 invoke를 구현해줘야 부가기능이 추가된다.
- JDK Dynamic Proxy 구현 예시
1 2 3 4 5 6 7 8 9 10 11 12
class ProxyTest{ @Test void jdkProxy(){ OrderService orderService = (OrderService) Proxy.newProxyInstance( ProxyTest.class.getClassLoader(), // 프록시 로딩에 사용할 클래스로더 new Class[]{OrderService.class}, //타깃의 interface new LoggingHandler(new OrderServiceImpl()); // 부가 기능과 위임할 타깃(RealObject) ); orderService.getOne(1L); } }
2. CGLIB
- 스프링의 동적 프록시 생성 방법
- Client가 메소드를 요청하면
- Proxy Factory Bean에서 메서드에 대한 인터페이스 유무를 판단하고
- Interface가 존재하면 JDK Dynamic Proxy 방식으로 프록시 생성
- Interface가 없다면 CGLIB 방식으로 프록시 생성(클래스만 있어도 작동)
- 🤔 근데 Spring은 Interface가 존재해도 CGLIB 방식으로 작동한다. 그 이유는?
스프링에서 default로 proxy-target-class 옵션을 true로 설정해놨기때문.
false로 변경하면 위의 방법처럼 작동한다.
CGLIB의 특징
- ⭐ 상속을 통한 프록시 구현 ➔ 클래스에 final을 붙이면 정상동작하지 않는다.
- 바이트 코드를 조작해서 프록시 생성 - Reflection Api보다 빠르다.
- MethodInterceptor를 재정의한 Intercept를 구현해야 부가기능이 추가된다.
메소드에 final을 붙이면 오버라이딩이 불가능하다.
- MethodInterceptor 예시
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
public class LoggingMethodInterceptor implements MethodInterceptor{ // 부가 기능을 제공할 타킷(Real Object) private final Object target; private static final Logger log = LoggerFactory.getLogger(LoggingMethodInterceptor.class); public LoggingMethodInterceptor(Object target){ this.target = target; } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy){ if(method.getName().startsWith("get")){ log.info("Excuting {} method : {}",method.getName(),"조회 메서드 호출!!"); return methodProxy.invoke(target, args); } log.info("Executing {} method: {}",method.getName(),"조회가 아닌 메서드 호출!!"); return methodProxy.invoke(target,args); } }
- CGLIB 구현 예시 코드
1 2 3 4 5 6 7 8 9 10 11
public class CglibTest { @Test void cglibProxyWhenGet(){ ProductService productService = (ProductService)Enhancer.create( ProductService.class, new LoggingMEthodInterceptor(new ProductService()) // 부가기능과 위임할 타깃 ); productService.getOne(1L); } }
CGLIB의 동작과정
- 클라이언트가 메소드를 요청한다.
- CGLIB은 Method Interceptor에 메소드 처리를 위임한다.
- Method Interceptor는 부가 기능을 수행 후 Target에게 메소드 처리를 위임한다.
Spring의 프록시 구현?
- Spring에서는 프록시를 Bean으로 만들어주는 ProxyFactoryBean을 제공한다.
- ProxyFactoryBean을 통해 Proxy를 생성할 수 있음.
ProxyFactoryBean의 주요 특징
- 스프링에서 지원하는 프록시 생성 방법
- 프록시 빈을 생성해준다
- 프록시 생성 시, 타깃의 인터페이스 정보가 필요없다..
- 부가기능을 MethondInterceptor를 재정의하여 구현한다. ➔ CGLIB의 MethodInterceptor와는 다른 개념
ProxyFactoryBean의 프록시 생성 과정
- 프록시를 생성한다.
- 프록시가 Method Interceptor에게 메소드 처리를 위임한다.
- MethodInterceptor는 부가기능을 수행하고 Target에게 메소드 처리를 위임한다.
🤔스프링에선 왜 Invocation Handler가 아닌 MethodInterceptor를 사용할까?
- Invocation Handler?
타겟(실제 객체)을 필드로 직접 가지고있기때문에, 부가 기능을 독립적으로 유지할 수 없다(타겟에 의존적임)
➔ 타겟이 늘어난다면, 같은 기능을 사용하더라도 그 개수만큼 InvocationHandler를 빈으로 매번 등록하고 객체로 생성해야한다. - Method Interceptor?
타겟을 직접 가지고있지않고 Proxy가 가지고있다.
따라서 부가기능을 독립적으로 유지할 수 있다.(싱글톤으로 공유하여 사용이 가능함)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
@Test public void proxyFactoryBean() { ProxyFactoryBean pfBean = new ProxyFactoryBean(); pfBean.setTarget(new HelloTarget()); NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); pointcut.addMethodName("sayH*"); pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice())); Hello proxiedHello = (Hello)pfBean.getObject(); assertThat(proxiedHello.sayHello("Toby"), is("HELLO, TOBY")); assertThat(proxiedHello.sayHi("Mindy"), is("HI, MINDY")); assertThat(proxiedHello.sayThankyou("Jack"), is("Thank you, Jack")); } static class UppercaseAdvice implements MethodInterceptor { public Object invoke(MethodInvocation invocation) throws Throwable { String ret = (String)invocation.proceed(); return ret.toUpperCase(); } }