Spring-Boot/스프링핵심원리 - 고급편

Spring 핵심 원리 고급편 - 4. 프록시 패턴과 데코레이터 패턴

PHM 2022. 6. 3. 09:55

* 예제는 크게 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 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.

 

- 인터페이스 기반의 프록시는 상속이라는 제약에서 자유롭다.

- 프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 더 좋다.

- 인터페이스 기반 프록시의 단점은 인터페이스가 필요하다는 그 자체이다.

 

- 인터페이스를 도입하는 것은 구현을 변경할 가능성이 있을 때 효과적인데, 구현을 변경할 가능성이 거의 없는 코드에 무작정 인터페이스를 사용하는 것은  번거롭고 그렇게 실용적이지 않다.

- 이런 곳에는 실용적인 관점에서 인터페이스를 사용하지 않고 구체 클래스를 바로 사용하는 것이 좋다

   ( 물론 인터페이스를 도입하는 다양한 이유가 있다. 핵심은 인터페이스가 항상 필요하지는 않다는 것이다 )

 

- 문제점 : 너무 많은 프록시 클래스 → 동적 프록시 기술!

 

 

 

출처 : 인프런 김영한님의 스프링 핵심원리 - 고급편

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