프록시 패턴과 데코레이터 패턴

slee2·2022년 3월 9일
0

프로젝트 생성

proxy-startproxy로 변경

예제 프로젝트 만들기

예제는 크게 3가지 상황으로 만든다.

  • v1 - 인터페이스와 구현 클래스 - 스프링 빈으로 수정 등록
  • v2 - 인터페이스 없는 구체 클래스 - 스프링 빈으로 수동 등록
  • v3 - 컴포넌트 스캔으로 스프링 빈 자동 등록

실무에서는 스프링 빈으로 클래스를 등록할 때, 인터페이스가 있는 경우도 있고, 없는 경우도 있다. 또, 빈을 수동으롣 등록하는 경우도 있고, 컴포넌트 스캔으로 자동 등록하는 경우도 있다. 이러한 다양한 경우에 프록시를 어떻게 적용하는지 알아보는 것.

V1 - 인터페이스 + 수동 등록

Repository

Service

Controller

@RequestMapping : 스프링MVC는 @Controller 또는 @RequestMapping 어노테이션이 타입에 있어야 스프링 컨트롤러로 인식한다. 그리고 스프링 컨트롤러로 인식해야 HTTP URL이 매핑되고 동작한다. 이 어노테이션은 인터페이스에 사용해도 된다.
@ResponseBody: 인터페이스에 사용해도 된다.
request()LogTrace를 적용할 대상이고, noLog()는 적용하지 않을 대상이다.

AppV1Config

@Import(AppV1Config.class): 클래스를 스프링 빈으로 등록한다.
일반적으로 @Configuration같은 설정 파일을 등록할 때 사용하지만, 스프링 빈을 등록할 때도 사용할 수 있다.

@SpringBootApplication(scaBasePackages = "hello.proxy.app") : @ComponentScan의 기능과 같다. 이렇게 설정하면, hello.proxy.app 패키지를 스캔한다.

이렇게 설정한 이유는 나중에 Configv2, v3 버전들이 나올텐데 컴포넌트 스캔이 v1, v2, v3를 전부다 스캔하는 것을 방지하기 위해 app 패키지 내부만 스캔하고 config는 수동으로 등록하는 것이라고 생각하면 될 것 같다.

v2

Repository

Service

Controller

@Controller를 굳이 쓰지 않는 이유는 @Component를 통해 자동으로 등록하지 않도록 하기 위해서이다. 나중에 Config에서 등록하기 위함.

Config

v3

모두 @Component를 통해 자동으로 등록되기 때문에 따로 Config 를 만들 필요 없음

요구사항 추가

이전에 했던 로그 추적기를 보았을때, 문제들이 있었는데

요구사항을 만족하기 위해서 기존 코드를 많이 수정해야 했다. 코드 수정을 최소화 하기 위해 템플릿 메서드 패턴과 콜백 패턴도 사용했지만, 결과적으로 로그를 남기고 싶은 클래스가 수백개라면, 이 클래스를 모두 고쳐야 한다. 로그를 남길 때 기존 원본 코드를 변경해야 한다는 사실 그 자체가 개발자에게는 가장 큰 문제로 남는다.

요구사항 추가

  • 원본 코드를 전혀 수정하지 않고, 로그 추적기를 적용해라.
    되나보다 오오...
  • 특정 메서드는 로그를 출력하지 않는 기능
    • 보안상 일부는 출력X
  • 다음과 같은 다양한 케이스에 적용할 수 있어야 한다.
    • 인터페이스가 있는 구현 클래스
    • 인터페이스가 없는 구체 클래스
    • 컴포넌트 스캔 대상

이 요구사항을 만족하기 위해서는 프록시(Proxy)의 개념을 먼저 이해해야 한다.

프록시 소개

프록시에 대해 알아보자

클라이언트와 서버

클라이언트와 서버라 하면 개발자들은 보통 서버 컴퓨터를 생각하지만, 사실 클라이언트와 서버의 개념은 넓은 의미로 사용된다.

클라이언트는 의뢰인,
서버는 '서비스나 상품을 제공하는 사람이나 물건'을 뜻한다.

따라서 클라이언트와 서버의 기본 개념을 정의하면
클라이언트는 서버에 필요한 것을 요청하고, 서버는 클라이언트의 요청을 처리
하는 것이다.

직접 호출과 간점 호출

클라이언트가 서버를 직접 호출하고, 처리 결과를 직접 받는걸 직접 호출.

그런데 어떤 대리자를 통해서 대신 간접적으로 서버에 요청할 수도 있다.
이때 대리자를 영어로 프록시(Proxy)라고 한다.

예)

  • 엄마에게 라면을 사달라고 부탁했는데,
    엄마는 그 라면은 이미 집에 있다고 할 수도 있다.
    그러면 기대한 것보다 더 빨리 라면을 먹을 수 있다. (접근 제어, 캐싱)
  • 아버지께 자동차 주유를 부탁했는데, 아버지가 주유 뿐만 아니라 세차까지 하고 왔다.
    클라이언트가 기대한 것 외에 세차라는 부가 기능까지 얻게 되었다.(부가 기능 추가)
  • 그리고 대리자가 또 다른 대리자를 부를 수도 있다. 쉽게 말해 누구에게 시키던 시킨 일을 완료하기만 하면 된다는 뜻.(프록시 체인)

대체 가능
객체에서 프록시가 되려면, 클라이언트는 서버에게 요청을 한 것인지, 프록시에게 요청을 한 것인지 조차 몰라야 한다.
쉽게 말해서 서버와 프록시는 같은 인터페이스를 사용해야 한다. 그리고 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야 한다.

서버와 프록시가 같은 인터페이스 사용

런타임(어플리케이션 실행 시점)에 클라이언트 객체에 DI를 사용해서 Client -> Server 에서 Client -> Proxy로 객체 의존관계를 변경해도 클라이언트 코드를 전혀 변경하지 않아도 된다.
DI를 사용하면 클라이언트 코드의 변경 없이 유연하게 프록시를 주입할 수 있다.

프록시의 주요 기능

  • 접근 제어
    • 권한에 따른 접근 차단
    • 캐싱(프록시에 데이터가 있다면 이 데이터를 반환)
    • 지연 로딩
  • 부가 기능 추가
    • 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
    • 예) 요청 값이나, 응답 값을 중간에 변형한다.
    • 예) 실행 시간을 측정해서 추가 로그를 남긴다.

GOF 디자인 패턴
의도(intent)에 따라서 프록시 패턴데코레이터 패턴으로 구분

  • 프록시 패턴: 접근 제어가 목적
  • 데코레이터 패턴: 새로운 기능 추가가 목적

참고
프록시라는 개념은 클라이언트 서버라는 큰 개념에서 자연스럽게 발생할 수 있다. 프록시는 객체 안에서의 개념도 있고, 웹 서버에서의 프록시도 있다. 객체 안에서 객체로 구현되어 있는가, 웹 서버로 구현되어 있는가처럼 규모의 차이가 있을 뿐, 근본적인 역할은 같다.

둘다 봐보자.

프록시 패턴

예제코드1

롬복을 사용하기 위해 테스트 의존성을 추가해야함.

코드를 단순하게 만들어보자.

테스트 코드

총 3초가 걸린다.

그런데 이 데이터가 한번 조회하면 변하지 않는 데이터라면 어딘가에 보관해두고 이미 조회한 데이터를 사용하는 것이 성능상 좋다. 이런 것을 캐시라고 한다.
프록시 패턴의 주요 기능은 접근제어이고, 캐시도 접근 제어 기능중 하나이다.

프록시 객체를 통해서 캐시를 적용해보자.

예제 코드 2

해당 객체는 Subject 인터페이스를 참고한다.

private Subject target: 클라이언트가 프록시를 호출하면 프록시가 최종적으로 실제 객체를 호출해야 한다. 따라서 내부에 실제 객체의 참조를 가지고 있어야 한다. 이렇게 프록시가 호출하는 대상을 target이라 한다.

operation(): cacheValue에 값이 없으면, operation()을 통해 값을 가져와 cacheValue에 저장하고 이 값을 반환한다. 다시 접근했을때, cacheValue가 있으면 바로 반환한다.

처음에는 1초가 걸리고 그 이후부터는 바로 프록시에서 반환되는 것을 확인할 수 있다.

client -> cacheProxy -> realSubject 런타임 객체 의존관계가 완성되었다.

정리
프록시 패턴의 핵심은 ProxyPatterClien가 프록시를 받고 있는지 모른다는 것이다.

데코레이터 패턴

예제 코드1

테스트

이전과 똑같다.

예제 코드2

부가 기능을 추가해볼 것이다.
이렇게 부가 기능을 추가하는 것을 데코레이터 패턴이라고 한다.

여기서 부가기능이란.

  • 요청 값이나, 응답 값을 중간에 변형한다.
  • 실행 시간을 측정해서 추가 로그를 남긴다.
    등등이 있다.

프록시에서 operation()에서 값을 가져와 앞 뒤를 *로 꾸며서 반환한 것을 확인할 수 있다.

이번에는 데코레이터에 데코레이터를 하나 더 넣어보자.

예제 코드3

기존 데코레이터에서 실행 시간 측정하는 기능까지 넣어보자.

Client -> TimeDecorator -> MessageDecorator -> RealComponent -> MessageDecorator -> TimeDecorator
와 같이 갔다가 돌아온 것을 확인할 수 있다.

프록시, 데코레이터 패턴 정리

꾸며주는 역할을 하는 Decorator들은 대상이 있어야 한다. 그것이 Component이다.

의도
프록시 패턴과 데코레이터 패턴은 거의 모양이 같다. 이 둘을 어떻게 구분하는건가?
의도에 따라 패턴을 구분한다.

  • 프록시 패턴: 다른 객체에 대한 접근을 제어
  • 데코레이터 패턴: 객체에 추가 책임(기능)을 동적으로 추가하고, 기능 확장을 위한 유연한 대안 제공

0개의 댓글