* 해당 블로그의 모든 내용과 사진은 글 제일 아래 참고자료를 작성하였습니다.
아래 글에 이어서 강의에서 배운 내용을 토대로 Proxy Pattern을 정리해보겠습니다.
[Design Pattern] 프록시 패턴과 이터레이터 패턴
이전 글에 이어 더 다양한 디자인 패턴을 소개해 볼게요. 이전 글을 안보신 분들은 보고 오시면 더 도움이 많이 될거에요! [Design Pattern] 디자인 패턴? 야 너두 이해할 수 있어(2) 이전 글에 이어 더
mirr-coding.tistory.com
#5 Proxy 패턴 (Proxy Pattern)
프록시를 다시 한번 정리하면 클라이언트가 서버를 직접 호출하는 게 아니라
아래 처럼 "대리자" 가 대신 호출 하는 것입니다.
여기서 "대리자" 는 또 다른 "대리자" 를 호출할 수도 있습니다. 이런것을 프록시 체인이라고 부릅니다.
중요한 점은 클라이언트는 호출 이후의 과정은 모른다는것 입니다.
대체 가능
여기서 그럼 아무 객체나 프록시를 할 수 있을 것 같지만, 그렇지않습니다.
객체에서 프록시가 되려면 클라이언트는 서버에게 요청을 보내는지, 프록시에게 요청을 보내는지 알수 없어야합니다.
다시 말하면 위에 그림처럼 서버와 프록시는 같은 인터페이스를 사용해야합니다. 따라서 DI를 사용해서 대체 가능합니다.
그리고 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야합니다.
여기서 프록시는 서버 인스턴스를 알고 있어야 합니다. (프록시가 서버를 알도록 참조를 넣아놔야합니다.)
프록시의 주요기능
- 접근제어 (권한에 따른 실제 서버에 접근할 수 있는지 여부에 따른 접근 제어)
- 캐싱 (데이터가 있다면 서버에 접근을 제어)
- 지연로딩 (클라이언트가 프록시를 가지고 작업을 수행하다 실제 요청이 있을 때, 그때 데이터를 조회
프록시의 부가기능
- 원래 서버가 제공하는 기능에 더해서 기능을 수행
- ex) 실행시간 측정, 추가적인 로그등
하나의 프록시 패턴이라 부를 수 있지만, 의도(intent)에 따라서 프록시 패턴과, 데코레이터 패턴으로 구분합니다.
프록시 패턴은 접근제어가 목적
데코레이터 패턴은 새로운 기능 추가가 목적
이제 프록시 패턴을 더 자세히 알아보겠습니다.
프록시 패턴 적용 전
프록시 패턴 적용 후
위와 같이 구성이 된다고 했을 때, 코드로 보면 이런 인터페이스를
아래와 같은 실제 서버가 구현하고 있을 것이고,
이런 프록시 객체 또한 제 역할에 맞춰 구성되고 있을 것입니다.
또한 해당 프록시는 실제 서버를 참조하며, 실제 서버가 필요할 시점에 사용을 할 수 있을 것입니다.
해당 예제는 프록시의 캐시기능을 구현한 예제입니다.
캐시의 기능처럼 만일 cacheValue가 있을경우 서버 객체를 전혀 호출하지 않습니다.
데코레이터 패턴 적용 전
데코레이터 패턴 적용 후
프록시 패턴과 마찬가지로 데코레이터 패턴을 코드로 적용해보면 Component라는 인터페이스를 만들고,
이런 실제 서버를 구성하고 있을 때,
데코레이터를 적용하는 프록시 객체 또한 구성할 수 있습니다.
데코레이터 패턴 시간 측정 프록시 적용 후
해당 그림은 프록시 체인 개념을 기반으로 설계 되었습니다.
이런 식으로 프록시를 하나 또 만들어 체인을 구성할 수 있습니다.
하지만 위의 코드에서 이질감을 느낄 수 있는데, 데코레이터 객체는 혼자 존재할 수 없으므로 항상 Component를 호출해야하는데 이 부분이 중복된다는 점입니다.
따라서 다음과 같이 Decorator 패턴에 중복이 되는부분을 추상화하야 사용할 수 있습니다.
이렇게 되면 실제 컴포넌트인지, 데코레이터 부분인지 명확하게 구분도 할 수 있습니다.
하지만 지금은 대상 클래스 수가 많다면 그만큼 프록시 클래스를 만들어야되므로 이를 하나로 줄이는 방법에 대해 알아야합니다.
리플랙션
해당 코드의 target.callA. callB의 공통로직을 만들고 싶습니다. 하지만 위아래로 코드가 있어 메서드로 공통로직을 만들기 쉽지않아보입니다.
이럴때 리플랙션을 사용할 수 있습니다.
다음과 같이 실행하고 싶은 메서드에대한 속해있는 위치, 실제 클래스 객체를 넘기면 로직이 실행될 수 있게 구성이 가능합니다. 이를 통해 클래스와 메서드의 메타정보를 사용해 동적으로 메서드를 사용할 수 있습니다.
하지만 리플랙션은 런타임 시 동작하기 때문에 컴파일시점에서 오류를 잡을 수 없다는 단점이 존재합니다.
JDK 동적 프록시
동적 프록시는 다음과 같이 구성됩니다.
Proxy 자체는 자바에서 지우너하는 Proxy.newProxyInstance 메서드를 사용하면 되는데, 해당 프록시가 위의 예제에서 dynamicCall() 처럼 수행해야할 로직이 담겨있는 InvocationHandler를 받아야합니다.
다음과같이 해당 핸들러 안에 수행되야할 실제객체 target을 주입하고, invoke() 메서드를 완성시키면 됩니다.
그 후, Proxy에 사용해야할 클래스로더, 인터페이스, InvocationHandler를 순서대로 인자로 전달하기만 하면 프록시가 완성됩니다. (프록시의 구현이 인자로 전달한 클래스로 구현되어 캐스팅이 가능합니다.)
그럼 위의 동적프록시 그림과 같이 프록시의 메서드가 호출되면, 메서드를 넘겨 invoke()를 수행하게 됩니다.
순서를 정리해보면,
- JDK 동적 프록시 call() 호출
- JDK 동적 프록시가 인자로 전달한 InvocationHandler의 invoke()를 호출
- InvocationHandler에 구성했던 로직 수행 후, method.invoke()을 호출해서 target인 실제 객체를 호출
- 실제 객체의 call() 호출
- call()의 리턴은 InvocationHandler로 하게됩니다.
CGLIB
바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리입니다.
인터페이스가 없는 대상에 대해 프록시를 적용하는데요.
CGLIB가 제공하는 MethodInterceptor를 implements 하여 intercept 메서드를 구현하시면 됩니다.
여기서 마찬가지로 프록시이기에 실제대상 객체를 주입받아 사용할 수 있어야합니다.
또한 사용은 Enhancer 라는 CGLIB 에서 제공하는 클래스에 방금 전 만들었던 MethodInterceptor를 적용하고, 실제 대상 객체를 적용하여 JDK 동적 프록시와 비슷하게 사용할 수 있습니다.
CGLIB 제약
- 자식 클래스를 동적으로 생성하기 때문에, 부모 클래스의 생성자를 신경써야합니다.
- 클래스에 final 키워드가 붙으면 상속이 불가능합니다.
- 메서드에 final 키워드가 붙으면 Override가 불가능합니다.
하지만 이렇게 인터페이스의 여부에 따라 프록시를 구성하게되면 하나의 프로젝트에 JDK 동적 프록시, CGLIB를 둘다 사용해야할겁니다. 따라서 스프링에서 제공하는 프록시 팩토리와 Advice를 통해 추상화 시켜 이를 구분하지 않고 사용할 수 있습니다.
PointCut : 어디에 부가기능을 적용할지 말지를 판단하는 필터링 기능
Advice : 프록시가 호출하는 부가기능, 프록시 로직
Advisor : PointCut 하나, Advice 하나를 가지고 있는 것
ProxyFactory는 Advisor가 무조건 필요합니다.
따라서 addAdvice 안에 Advisor를 만드는 편의 소스들이 존재합니다.
참고사항
AOP 적용 수 만큼 (Transaction, Time, What Method..) 프록시가 생성되지 않는다!
적용 수 만큼의 Advisor가 생성되어 하나의 프록시에 적용된다.
다음과 같이 Advisor를 생성합니다.
그 후 다음과 같이 실제 객체를 ProxyFactory에 넘기고, Advisor를 추가하여 프록시를 얻어낼 수 있습니다.
● 참고자료 : 스프링 핵심원리 - 고급편 (김영한 | 인프런)
'Design Pattern' 카테고리의 다른 글
[Design Pattern] Template Method Pattern (0) | 2023.05.31 |
---|---|
[Design Pattern] 디자인 패턴 (1) | 2023.01.02 |