Spring 핵심 원리 고급편 - 4. 프록시 패턴과 데코레이터 패턴
* 예제는 크게 3가지 상황 *
v1 - 인터페이스와 구현 클래스 - 스프링 빈으로 수동 등록
v2 - 인터페이스 없는 구체 클래스 - 스프링 빈으로 수동 등록
v3 - 컴포넌트 스캔으로 스프링 빈 자동 등록
- 실무에서는 스프링 빈으로 등록할 클래스는 인터페이스가 있는 경우도 있고 없는 경우도 있다. 그리고 스프링 빈을 수동으로 직접 등록하는 경우도 있고, 컴포넌트 스캔으로 자동으로 등록하는 경우도 있다.
- @RequestMapping : 스프링 MVC는 @Controller 또는 @RequestMapping 애노테이션이 타입에 있어야 스프링 컨트롤러로 인식한다. 그리고 스프링 컨트롤러로 인식해야, HTTP URL이 매핑되고 동작한다. 이 애노테이션은 인터페이스에 사용해도 된다.
- @ResponseBody : HTTP 메시지 컨버터를 사용해서 응답한다. 이 애노테이션은 인터페이스에 사용해도 된다.
- interface에는 @RequestParam 를 넣어줘야 한다
- @Import ( AppV1Config.class ) : 클래스를 스프링 빈으로 등록한다. 여기서는 AppV1Config.class를 스프링 빈으로 등록한다. 일반적으로 @Configuration 같은 설정 파일을 등록할 때 사용하지만, 스프링 빈을 등록할 때도 사용할 수 있다.
- @SpringBootApplication(scanBasePackages = "hello.proxy.app") : '@ComponentScan' 의 기능과 같다.컴포넌트 스캔을 시작할 위치를 지정한다. 이 값을 설정하면 해당 패키지와 그 하위 패키지를 컴포넌트 스캔한다. 이 값을 사용하지 않으면 ProxyApplication이 있는 패키지와 그 하위 패키지로 스캔한다.
* 참고 *
만약 스프링 빈을 수동으로 변경하고 싶을 경우에는
@Configuration에는 @Component를 포함하고 있어서 컴포넌트 스캔의 대상이 되므로
@SpringBootApplication(scanBasePackages = "hello.proxy.app") 처럼 스캔 범위를 지정해주고
@Import(AppV1Config.class) 처럼 수동으로 스프링 빈을 등록한다.
프록시, 프록시 패턴, 데코레이터 패턴
* 클라이언트와 서버 *
- 클라이언트는 서버에 필요한 것을 요청하고, 서버는 클라이언트의 요청을 처리
* 직접호출과 간접호출 *
- 직접 호출 : 클라이언트와 서버 개념에서 일반적으로 클라이언트가 서버를 직접 호출하고, 처리 결과를 직접 받는다.
- 간접 호출 : 클라이언트가 요청한 결과를 서버에 직접 요청하는 것이 아니라 어떤 대리자( 프록시 Proxy )를 통해서 대신 간접적으로 서버에 요청할 수 있다.
프록시의 역할
- 접근제어, 캐싱 / 부가 기능 추가 / 프록시 체인
* 대체 가능 *
- 객체에서 프록시가 되려면, 클라이언트는 서버에게 요청을 한것인지, 프록시에게 요청을 한 것인지 조차 몰라야 한다
- 쉽게 이야기해서 서버와 프록시는 같은 인터페이스를 사용해야 한다. 그리고 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야 한다.
- 클래스 의존관계를 보면 클라이언트 서버 인터페이스( 'ServerInterface' )에만 의존한다. 그리고 서버와 프록시가 같은 인터페이스를 사용한다. 따라서 DI를 사용해서 대체 가능하다.
- 런타임 객체 의존관계 ( 인스턴스 객체 의존관계 )
런타임( 애플리케이션 실행 시점 ) 에 클라이언트 객체 DI를 사용해서 Client → Server 에서 Client → Proxy로 객체 의존관계를 변경해도 클라이언트 코드를 전혀 변경하지 않아도 된다. 클라이언트 입장에서는 변경 사실 조차 모른다.
DI를 사용하면 클라이언트 코드의 변경 없이 유연하게 프록시를 주입할 수 있다.
* 프록시의 주요 기능 *
- 접근 제어
ㆍ권한에 따른 접근 차단
ㆍ캐싱 : 클라이언트가 프록시 요청시 데이터 있을 경우 데이터 반환, 서버접근X
ㆍ지연 로딩 : 실제 요청이 있을 때 데이터 조회
- 부가 기능 추가
ㆍ원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다
ㆍ예 ) 요청 값이나, 응답 값을 중간에 변형한다.
ㆍ예 ) 실행 시간을 측정해서 추가 로그를 남긴다.
* GOF 디자인 패턴 *
- 둘다 프록시를 사용하는 방법이지만 GOF 디자인 패턴에서는 이 둘을 의도(intent)에 따라서 프록시 패턴과 데코레이터 패턴으로 구분한다
- 프록시 패턴 : 접근 제어가 목적
- 데코레이터 패턴 : 새로운 기능 추가가 목적
- 둘다 프록시를 사용하지만, 의도가 다르다는 점이 핵심이다. 용어가 프록시 패턴이라고 해서 이 패턴만 프록시를 사용하는 것이 아니다. 데코레이터 패턴도 프록시를 사용한다.
* 참고 *
프록시라는 개념은 클라이언트 서버라는 큰 개념안에서 자연스럽게 발생할 수 있다. 프록시는 객체안에서의 개념도 있고, 웹 서버에서의 프록시도 있다. 객체 안에서 객체로 구현되어있는가, 웹 서버로 구현되어 있는가 처럼 규모의 차이가 있을 뿐 근본적인 역할 같다.
- 근본적으로 프록시는 클라이언트 서버 사이에서 접근제어와 부가기능을 추가한다.
* 캐시 : 데이터가 한번 조회하면 변하지 않는 데이터라면 어딘가에 보관해두고 이미 조회한 데이터를 사용하는 것
- 프록시 패턴의 주요 기능은 접근제어이다. 캐시도 접근 자체를 제어하는 기능 중 하나이다.
- 프록시 패턴의 핵심은 클라이언트 코드를 전혀 변경하지 않고, 프록시를 도입해서 접근 제어를 했다는 점이다. 자유롭게 프록시를 넣고 뺄 수 있고, 실제 클라이언트 입장에서는 프록시 객체가 주입되었는지, 실제 객체가 주입되었는지 알지 못한다.
- 데코레이터 패턴 : 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
ㆍ예) 요청 값이나, 응닶 값을 중간에 변형한다.
ㆍ예) 실행 시간을 측정해서 추가 로그를 남긴다.
프록시 패턴과 데코레이터 패턴 정리
- Decorator 들은 스스로 존재할 수 없다. 항상 꾸며줄 대상이 있어야 한다.
- Component를 속성으로 가지고 있는 Decorator라는 추상 클래스를 만드는 방법도 고민 가능
ㆍ클래스 다이어그램에서 어떤 것이 실제 컴포넌트 인지 데코레이터인지 명확하게 구분 가능!
프록시 패턴 vs 데코레이터 패턴
* 의도( intent ) *
- 사실 프록시 패턴과 데코레이터 패턴은 그 모양이 거의 같고, 상황에 따라 정말 똑같은 때도 있다. 그러면 둘을 어떻게 구분하는 것일까?
ㆍ디자인 패턴에서 중요한 것은 해당 패턴의 겉모양이 아니라 그 패턴을 만든 의도가 더 중요하다.
ㆍ따라서 의도에 따라 패턴을 구분한다.
- 프록시 패턴의 의도 : 다른 개체에 대한 * 접근을 제어 * 하기 위해 대리자를 제공
- 데코레이터 패턴의 의도 : * 객체에 추가 책임(기능)을 동적으로 추가 * 하고, 기능 확장을 위한 유연한 대안 제공
* 정리 *
- 프록시를 사용하고 해당 프록시가 접근 제어가 목적이라면 프록시 패턴이고, 새로운 기능을 추가하는 것이 목적이라면 데코레이터 패턴이 된다.
인터페이스 기반 프록시
* V1 프록시 런타임 객체 의존 관계 설정 *
- 프록시를 생성하고 * 프록시를 실제 스프링 빈 대신 등록한다. 실제 객체는 스프링 빈으로 등록하지 않는다. *
- 프록시는 내부에 실제 객체를 참조
ㆍproxy → target
ㆍorderServiceInterfaceProxy → orderServiceV1Impl
- 스프링 빈으로 실제 객체 대신에 프록시 객체를 등록했기 때문에 앞으로 스프링 빈을 주입 받으면 * 실제 객체 대신에 프록시 객체가 주입 * 된다.
- 실제 객체가 스프링 빈으로 등록되지 않는다고 해서 사라지는 것은 아니다. 프록시 객체가 실제 객체를 참조하기 때문에 프록시를 통해서 실제 객체를 호출할 수 있다. 쉽게 이야기해서 프록시 객체 안에 실제 객체가 있는 것이다.
- 프록시 객체는 스프링 컨테이너가 관리하고 자바 힙 메모리에도 올라간다. 반면에 실제 객체는 자바 힙 메모리에는 올라가지만 스프링 컨테이너가 관리하지는 않는다.
@Configuration
public class InterfaceProxyConfig {
@Bean
public OrderControllerV1 orderController(LogTrace logTrace) {
OrderControllerV1Impl controllerImpl = new OrderControllerV1Impl(orderService(logTrace));
return new OrderControllerInterfaceProxy(controllerImpl, logTrace);
}
@Bean
public OrderServiceV1 orderService(LogTrace logTrace) {
OrderServiceV1Impl serviceImpl = new OrderServiceV1Impl(orderRepository(logTrace));
return new OrderServiceInterfaceProxy(serviceImpl, logTrace);
}
@Bean
public OrderRepositoryV1 orderRepository(LogTrace logTrace) {
OrderRepositoryV1Impl repositoryImpl = new OrderRepositoryV1Impl();
return new OrderRepositoryInterfaceProxy(repositoryImpl, logTrace);
}
}
구체 클래스 기반 프록시
- 클래스 기반 프록시 도입
ㆍ자바의 다형성은 인터페이스를 구현하든, 아니면 클래스를 상속하든 상위 타입만 맞으면 다형성이 적용
ㆍ즉 인터페이스가 없어도 프록시를 만들 수 있다는 뜻이다.
* 참고 *
- 자바 언어에서 다형성은 인터페이스나 클래스를 구분하지 않고 모두 적용된다. 해당 타입과 그 타입의 하위 타입은 모두 다형성의 대상이 된다.
- 자바 언어의 너무 기본적인 이야기지만 인터페이스가 없어도 프록시가 가능하다
* 클래스 기반 프록시의 단점 *
- super(null) : 자바 기본 문법에 의해 자식 클래스를 생성할 때는 항상 ' super( ) '로 부모 클래스의 생성자를 호출해야 한다. 이 부분을 생략하면 기본 생성자가 호출된다. 그런데 부모 클래스인 ' OrderServiceV2 ' 는 기본 생성자가 없고, 생성자에서 파라미터 1개를 필수로 받는다. 따라서 파라미터를 넣어서 ' super( . . ) ' 를 호출해야 한다.
- 프록시는 부모 객체의 기능을 사용하지 않기 때문에 ' super(null) '을 입력해도 된다.
- 인터페이스 기반 프록시는 이런 고민을 하지 않아도 된다.
인터페이스 기반 프록시와 클래스 기반 프록시
* 인터페이스 기반 프록시 vs 클래스 기반 프록시 *
- 인터페이스가 없어도 클래스 기반으로 프록시를 생성할 수 있다
- 클래스 기반 프록시는 해당 클래스에만 적용할 수 있다. 인터페이스 기반 프록시는 인터페이스만 같으면 모든 곳에 적용할 수 있다.
- 클래스 기반 프로시는 상속을 사용하기 때문에 몇가지 제약이 있다.
ㆍ부모 클래스의 생성자를 호출해야한다
ㆍ클래스에 final 키워드가 붙으면 상속이 불가능하다
ㆍ메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.
- 인터페이스 기반의 프록시는 상속이라는 제약에서 자유롭다.
- 프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 더 좋다.
- 인터페이스 기반 프록시의 단점은 인터페이스가 필요하다는 그 자체이다.
- 인터페이스를 도입하는 것은 구현을 변경할 가능성이 있을 때 효과적인데, 구현을 변경할 가능성이 거의 없는 코드에 무작정 인터페이스를 사용하는 것은 번거롭고 그렇게 실용적이지 않다.
- 이런 곳에는 실용적인 관점에서 인터페이스를 사용하지 않고 구체 클래스를 바로 사용하는 것이 좋다
( 물론 인터페이스를 도입하는 다양한 이유가 있다. 핵심은 인터페이스가 항상 필요하지는 않다는 것이다 )
- 문제점 : 너무 많은 프록시 클래스 → 동적 프록시 기술!
출처 : 인프런 김영한님의 스프링 핵심원리 - 고급편