[스프링 핵심 원리 - 고급편] #5. 프록시 패턴과 데코레이터 패턴 #676
Develop-KIM
started this conversation in
동환
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
예제 프로젝트 만들기 v1
예제는 크게 3가지 상황으로 만든다.
실무에서는 스프링 빈으로 등록할 클래스는 인터페이스가 있는 경우도 있고 없는 경우도 있다.
그리고 스프링 빈을 수동으로 직접 등록하는 경우도 있고, 컴포넌트 스캔으로 자동으로 등록하는 경우도 있음
v1 - 인터페이스와 구현 클래스 - 스프링 빈으로 수동 등록
OrderRepositoryV1
OrderRepositoryV1Impl
OrderServiceV1
OrderServiceV1Impl
OrderControllerV1
@RequestMapping: 스프링MVC는 타입에@Controller또는@RequestMapping애노테이션이 있어야 스프링 컨트롤러로 인식한다.그리고 스프링 컨트롤러로 인식해야, HTTP URL이 매핑되고 동작한다. 이 애노테이션은 인터페이스에 사용해도 된다.
@ResponseBody: HTTP 메시지 컨버터를 사용해서 응답한다. 이 애노테이션은 인터페이스에 사용해도 된다.@RequestParam("itemId") String itemId: 인터페이스에는@RequestParam("itemId")의 값을 생략하면itemId컴파일 이후 자바 버전에 따라 인식하지 못할 수 있어 인터페이스에서는 꼭 넣어주어야 한다. 클래스에는 생략해도 대부분 잘 지원된다.request(),noLog()두 가지 메서드가 있다.request()는LogTrace를 적용할 대상이고noLog()는 단순히LogTrace를 적용하지 않을 대상이다.OrderControllerV1Impl
OrderControllerV1인터페이스에 스프링MVC 관련 애노테이션이 정의되어 있다.AppV1Config
ProxyApplication - 코드 추가
@Import(AppV1Config.class): 클래스를 스프링 빈으로 등록한다. 여기서는AppV1Config.class를 스프링 빈으로 등록한다.일반적으로
@Configuration같은 설정 파일을 등록할 때 사용하지만, 스프링 빈을 등록할 때도 사용할 수 있다.@SpringBootApplication(scanBasePackages = "hello.proxy.app"):@ComponentScan의 기능과 같다.컴포넌트 스캔을 시작할 위치를 지정한다. 이 값을 설정하면 해당 패키지와 그 하위 패키지를 컴포넌트 스캔한다.
이 값을 사용하지 않으면
ProxyApplication이 있는 패키지와 그 하위 패키지를 스캔한다.예제 프로젝트 만들기 v2
v2 - 인터페이스 없는 구체 클래스 - 스프링 빈으로 수동 등록
OrderRepositoryV2
OrderControllerV2
@RequestMapping: 스프링MVC는 타입에@Controller또는@RequestMapping애노테이션이 있어야 스프링 컨트롤러로 인식한다.그리고 스프링 컨트롤러로 인식해야, HTTP URL이 매핑되고 동작한다.
그런데 여기서는
@Controller를 사용하지 않고,@RequestMapping애노테이션을 사용했다.그 이유는
@Controller를 사용하면 자동 컴포넌트 스캔의 대상이 되기 때문이다.여기서는 컴포넌트 스캔을 통한 자동 빈 등록이 아니라 수동 빈 등록을 하는 것이 목표다.
따라서 컴포넌트 스캔과 관계 없는
@RequestMapping를 타입에 사용했다.AppV2Config
ProxyApplication
변경 사항
@Import(AppV1Config.class)@Import({AppV1Config.class, AppV2Config.class})@Import안에 배열로 등록하고 싶은 설정파일을 다양하게 추가할 수 있다.예제 프로젝트 만들기 v3
v3 - 컴포넌트 스캔으로 스프링 빈 자동 등록
OrderRepositoryV3
OrderServiceV3
OrderControllerV3
에서@SpringBootApplication(scanBasePackages = "hello.proxy.app")를 사용했고 각각@RestController,@service,@Repository` 애노테이션을 가지고 있기 때문에 컴포넌트 스캔의 대상이 된다.요구사항 추가
기존 요구사항
예시
요구사항 추가
프록시, 프록시 패턴, 데코레이터 패턴 - 소개
클라이언트와 서버의 기본 개념을 정의
이 개념을 객체에 도입하면, 요청하는 객체는 클라이언트가 되고, 요청을 처리하는 객체는 서버가 된다.

직접 호출과 간접 호출
클라이언트와 서버 개념에서 일반적으로 클라이언트가 서버를 직접 호출하고, 처리 결과를 직접 받는다. 이것을 직접 호출이라 한다.

예시
(접근 제어, 캐싱)
(부가 기능 추가)
대체 가능
객체에서 프록시가 되려면, 클라이언트는 서버에게 요청을 한 것인지, 프록시에게 요청을 한 것인지 조차 몰라야 한다.
서버와 프록시가 같은 인터페이스 사용

클래스 의존관계를 보면 클라이언트는 서버 인터페이스(
ServerInterface)에만 의존한다. 그리고 서버와 프록시가 같은 인터페이스를 사용한다.따라서 DI를 사용해서 대체 가능하다.
런타임(애플리케이션 실행 시점)에 클라이언트 객체에 DI를 사용해서
Client -> Server에서Client -> Proxy로 객체 의존관계를 변경해도 클라이언트 코드를 전혀 변경하지 않아도 된다.DI를 사용하면 클라이언트 코드의 변경 없이 유연하게 프록시를 주입할 수 있다.
프록시의 주요 기능
프록시를 통해서 할 수 있는 일은 크게 2가지로 구분할 수 있다.
프록시 객체가 중간에 있으면 크게 접근 제어와 부가 기능 추가를 수행할 수 있다.
GOF 디자인 패턴
GOF 디자인 패턴에서는 의도(intent)에 따라서 프록시 패턴과 데코레이터 패턴으로 구분
프록시 패턴 - 예제 코드1
테스트 코드에 Lombok 적용하기
build.gradle에 추가
프록시 패턴 - 예제 코드 작성
Subject 인터페이스
RealSubject
operation(): 데이터 조회를 시뮬레이션 하기 위해 1초 쉬도록 했다.ProxyPatternClient
Subject인터페이스에 의존하고,Subject를 호출하는 클라이언트 코드execute():subject.operation()를 호출ProxyPatternTest
실행 결과
client.execute()을 3번 호출하면 다음과 같이 처리된다.
client -> realSubject를 호출해서 값을 조회한다. (1초)client -> realSubject를 호출해서 값을 조회한다. (1초)client -> realSubject를 호출해서 값을 조회한다. (1초)이 데이터가 한번 조회하면 변하지 않는 데이터라면 어딘가에 보관해두고 이미 조회한 데이터를 사용하는 것이 성능상 좋다. 이런 것을 캐시라고 한다.
프록시 패턴의 주요 기능은 접근 제어
프록시 패턴 - 예제 코드2
프록시 패턴을 적용

CacheProxy
프록시도 실제 객체와 그 모양이 같아야 하기 때문에
Subject인터페이스를 구현해야 함private Subject target: 클라이언트가 프록시를 호출하면 프록시가 최종적으로 실제 객체를 호출해야 한다.따라서 내부에 실제 객체의 참조를 가지고 있어야 한다. 이렇게 프록시가 호출하는 대상을
target이라 한다.operation(): 구현한 코드를 보면cacheValue에 값이 없으면 실제 객체(target)를 호출해서 값을 구한다.그리고 구한 값을
cacheValue에 저장하고 반환한다. 만약cacheValue에 값이 있으면 실제 객체를 전혀 호출하지 않고, 캐시 값을 그대로 반환한다.따라서 처음 조회 이후에는 캐시(
cacheValue)에서 매우 빠르게 데이터를 조회할 수 있다.ProxyPatternTest - cacheProxyTest() 추가
cacheProxyTest()
realSubject와cacheProxy를 생성하고 둘을 연결한다. 결과적으로cacheProxy가realSubject를 참조하는 런타임 객체 의존관계가 완성된다. 그리고 마지막으로client에realSubject가 아닌cacheProxy를 주입한다.이 과정을 통해서
client -> cacheProxy -> realSubject런타임 객체 의존 관계가 완성된다.cacheProxyTest()는client.execute()을 총 3번 호출한다.실행 결과
client.execute()을 3번 호출하면 다음과 같이 처리된다.
정리
프록시 패턴의 핵심은
RealSubject코드와 클라이언트 코드를 전혀 변경하지 않고, 프록시를 도입해서 접근 제어를 했다는 점이다.그리고 클라이언트 코드의 변경 없이 자유롭게 프록시를 넣고 뺄 수 있다.
실제 클라이언트 입장에서는 프록시 객체가 주입되었는지, 실제 객체가 주입되었는지 알지 못한다.
데코레이터 패턴 - 예제 코드1
Component 인터페이스
RealComponent
RealComponent는Component인터페이스를 구현한다.operation(): 단순히 로그를 남기고"data"문자를 반환한다.DecoratorPatternClient
Component인터페이스를 의존한다.execute()를 실행하면component.operation()을 호출하고, 그 결과를 출력한다.DecoratorPatternTest
client -> realComponent의 의존관계를 설정하고,client.execute()를 호출한다.실행 결과
데코레이터 패턴 - 예제 코드2
부가 기능 추가
프록시를 통해서 할 수 있는 기능은 크게 접근 제어와 부가 기능 추가라는 2가지로 구분한다.
데코레이터 패턴: 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
응답 값을 꾸며주는 데코레이터
MessageDecorator
MessageDecorator는Component인터페이스를 구현한다.프록시가 호출해야 하는 대상을
component에 저장한다.operation()을 호출하면 프록시와 연결된 대상을 호출(component.operation())하고, 그 응답 값에*****을 더해서 꾸며준 다음 반환한다.예를 들어서 응답 값이
data라면 다음과 같다.data*****data*****DecoratorPatternTest - 추가
client -> messageDecorator -> realComponent의 객체 의존 관계를 만들고client.execute()를 호출한다.실행 결과
데코레이터 패턴 - 예제 코드3
실행 시간을 측정하는 데코레이터
TimeDecorator
DecoratorPatternTest - 추가
실행 결과
프록시 패턴과 데코레이터 패턴 정리
GOF 데코레이터 패턴
GOF 디자인 패턴의 핵심
의도(intent)
사실 프록시 패턴과 데코레이터 패턴은 그 모양이 거의 같고, 상황에 따라 정말 똑같을 때도 있다. 그러면 둘을 어떻게구분하는 것일까?
디자인 패턴에서 중요한 것은 해당 패턴의 겉모양이 아니라 그 패턴을 만든 의도가 더 중요하다. 따라서 의도에 따라 패턴을 구분한다.
정리
인터페이스 기반 프록시 - 적용
프록시를 사용하면 기존 코드를 전혀 수정하지 않고 로그 추적 기능을 도입할 수 있다.
V1 기본 클래스 의존 관계

V1 런타임 객체 의존 관계

V1 프록시 의존 관계 추가

Controller,Service,Repository각각 인터페이스에 맞는 프록시 구현체를 추가V1 프록시 런타임 객체 의존 관계

그리고 애플리케이션 실행 시점에 프록시를 사용하도록 의존 관계를 설정해주어야 한다. 이 부분은 빈을 등록하는 설정 파일을 활용하면 된다.
OrderRepositoryInterfaceProxy
LogTrace를 사용하는 로직을 추가한다.지금까지는
OrderRepositoryImpl에 이런 로직을 모두 추가해야했다.프록시를 사용한 덕분에 이 부분을 프록시가 대신 처리해준다. 따라서
OrderRepositoryImpl코드를 변경하지 않아도 된다.OrderRepositoryV1 target: 프록시가 실제 호출할 원본 리포지토리의 참조를 가지고 있어야 한다.OrderServiceInterfaceProxy
OrderControllerInterfaceProxy
noLog()메서드는 로그를 남기지 않아야 한다. 따라서 별도의 로직 없이 단순히target을 호출하면 된다.InterfaceProxyConfig
V1 프록시 런타임 객체 의존 관계 설정
기존에는 스프링 빈이
orderControlerV1Impl,orderServiceV1Impl같은 실제 객체를 반환했다. 하지만 이제는 프록시를 사용해야한다.따라서 프록시를 생성하고 프록시를 실제 스프링 빈 대신 등록한다. 실제 객체는 스프링 빈으로 등록하지 않는다.
예시)
OrderServiceInterfaceProxy는 내부에 실제 대상 객체인OrderServiceV1Impl을 가지고 있다.proxy -> targetorderServiceInterfaceProxy -> orderServiceV1Impl프록시 객체가 실제 객체를 참조하기 때문에 프록시를 통해서 실제 객체를 호출할 수 있다.
AppV1Config를 통해 프록시를 적용하기 전@x0..라고 해둔 것은 인스턴스라는 뜻이다.InterfaceProxyConfig를 통해 프록시를 적용한 후반면에 실제 객체는 자바 힙 메모리에는 올라가지만 스프링 컨테이너가 관리하지는 않는다.
최종적으로 이런 런타임 객체 의존관계가 발생한다.
ProxyApplication
@Bean: 먼저LogTrace스프링 빈 추가를 먼저 해주어야 한다. 이것을 여기에 등록한 이유는 앞으로 사용할 모든 예제에서 함께 사용하기 위해서다.@Import(InterfaceProxyConfig.class): 프록시를 적용한 설정 파일을 사용실행 결과 - 로그
추가된 요구사항
원본 코드를 전혀 수정하지 않고, 로그 추적기를 적용해라.특정 메서드는 로그를 출력하지 않는 기능보안상 일부는 로그를 출력하면 안된다.v1 - 인터페이스가 있는 구현 클래스에 적용구체 클래스 기반 프록시 - 예제1
ConcreteLogic
ConcreteLogic은 인터페이스가 없고, 구체 클래스만 있다. 여기에 프록시를 도입해야 한다.ConcreteClient
ConcreteProxyTest
구체 클래스 기반 프록시 - 예제2
클래스 기반 프록시 도입
자바의 다형성은 인터페이스를 구현하거나 클래스를 상속하든 상위 타입만 맞으면 다형성이 적용된다.

TimeProxy
TimeProxy프록시는 시간을 측정하는 부가 기능을 제공한다. 그리고 인터페이스가 아니라 클래스인ConcreteLogic를 상속 받아서 만든다.ConcreteProxyTest - addProxy() 추가
여기서 핵심은
ConcreteClient의 생성자에concreteLogic이 아니라timeProxy를 주입하는 부분이다.ConcreteClient는ConcreteLogic을 의존하는데 다형성에 의해ConcreteLogic에concreteLogic도 들어갈 수 있고timeProxy도 들어갈 수 있다.ConcreteLogic에 할당할 수 있는 객체
ConcreteLogic = concreteLogic(본인과 같은 타입을 할당)ConcreteLogic = timeProxy(자식 타입을 할당)ConcreteClient 참고
실행 결과
구체 클래스 기반 프록시 - 적용
OrderRepositoryConcreteProxy
인터페이스가 아닌
OrderRepositoryV2클래스를 상속 받아서 프록시를 만든다.OrderServiceConcreteProxy
OrderServiceV2클래스를 상속 받아서 프록시를 만든다.클래스 기반 프록시의 단점
super(null):OrderServiceV2: 자바 기본 문법에 의해 자식 클래스를 생성할 때는 항상super()로 부모 클래스의 생성자를 호출해야 한다.이 부분을 생략하면 기본 생성자가 호출된다. 그런데 부모 클래스인
OrderServiceV2는 기본 생성자가 없고, 생성자에서 파라미터 1개를 필수로 받는다.따라서 파라미터를 넣어서
super(..)를 호출해야 한다.super(null)을 입력해도 된다.OrderServiceV2의 생성자 - 참고
OrderControllerConcreteProxy
ConcreteProxyConfig
ProxyApplication
인터페이스 기반 프록시와 클래스 기반 프록시
프록시
프록시를 사용한 덕분에 원본 코드를 전혀 변경하지 않고, V1, V2 애플리케이션에
LogTrace기능을 적용할 수 있었다.인터페이스 기반 프록시 vs 클래스 기반 프록시
이렇게 보면 인터페이스 기반의 프록시가 더 좋아보인다. 맞다. 인터페이스 기반의 프록시는 상속이라는 제약에서 자유롭다.
프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 더 좋다.
인터페이스 기반 프록시의 단점은 인터페이스가 필요하다는 그 자체이다. 인터페이스가 없으면 인터페이스 기반 프록시를 만들 수 없다.
결론
실무에서는 프록시를 적용할 때 V1처럼 인터페이스도 있고, V2처럼 구체 클래스도 있다. 따라서 2가지 상황을 모두 대응할 수 있어야 한다.
너무 많은 프록시 클래스
지금까지 프록시를 사용해서 기존 코드를 변경하지 않고, 로그 추적기라는 부가 기능을 적용할 수 있었다.
만약 적용해야 하는 대상 클래스가 100개라면 프록시 클래스도 100개를 만들어야한다.
프록시 클래스를 하나만 만들어서 모든 곳에 적용하는 방법은 없을까?
Beta Was this translation helpful? Give feedback.
All reactions