[응용] 4. 프록시 패턴과 데코레이터 패턴

kiwonkim·2021년 11월 30일
0

[ 이전 포스팅 ]

템플릿 메서드 패턴이나 템플릿 콜백 패턴의 도입으로 공통 부가기능을 템플릿 메서드로 별도 관리할 수 있게 되었다. 하지만 여전히 공통 부가기능을 추가하려면 원래 클래스를 수정해야한다. 100 개의 클래스에 부가기능을 추가하면 100 번의 수정이 필요하다는 뜻이다. 원본 코드를 수정하지 않으면서 공통 부가기능을 적용할 수는 없을까? 이는 프록시의 도입으로 가능하다.

원본 코드를 수정하지 않고 공통 부가기능을 적용할 수 있게 해주는 프록시에 대해 알아보자.

[ 프록시 ]

프록시란

기존 방식

요청객체가 필요객체를 직접 호출해서 사용한다.
ex) Service 가 Repository 의 기능을 쓸 때 Repository 객체에 접근해 메서드를 호출한다.

프록시 방식

요청객체는 프록시객체를 호출하고 프록시 객체가 대신 실제 객체를 호출해서 사용해준다.
ex) Service 가 proxy_repo 객체에 접근해 사용하고, proxy_repo 가 Repository 객체에 접근해 실제 필요로직을 호출한다.

즉 직접 요청하는게 아닌 어떤 대리자를 통해 간접적으로 요청하는 방식을 프록시 방식이라고 한다.

프록시객체의 조건

  • 프록시객체의 추가로 요청객체와 실제객체의 코드가 수정되어서는 안된다.
  • 요청객체는 프록시객체의 존재를 모르고 실제 객체에 호출한 것 처럼 사용할 수 있어야한다.

따라서 프록시 객체는 실제객체와 같은 인터페이스를 구현했거나, 실제 객체의 자식클래스의 객체이어야만 한다.

프록시의 장점

프록시를 통해 간접 호출을 하게되면 중간에서 여러 처리를 해줄 수 있다.

  • 접근제어, 캐싱
    요청한 사용자를 판별해서 보안상으로 요청을 취소시키거나, 이전 호출 데이터를 저장해두어 캐싱 데이터를 제공할 수 있다.

  • 부가기능 추가
    요청한 기본기능 외에 프록시객체가 부가기능을 추가하여 처리할 수 있다.

위와 같이 프록시를 도입하여 중간처리를 활용하는 디자인 패턴 중, 접근제어 기능을 활용하는 디자인 패턴을 프록시 패턴. 부가기능을 추가하는 디자인 패턴을 데코레이터 패턴이라고 한다.


[ 프록시 패턴 - 예제 ]

공통 인터페이스

실제객체와 프록시객체의 공통 인터페이스이다. operation 메서드를 갖는다.

클라이언트 객체

클라이언트 객체이다. Subject 인터페이스에 의존한다.

실제 객체

실제 객체이다. 원하는 로직을 수행한다.

프록시 객체 - 캐시 수행

프록시 객체이다. 실제 객체를 필드로 갖게될 것이며, 클라이언트의 요청에 따라 실제 객체를 호출하여 사용한다. 이 때 필드인 캐시에 값이 존재한다면, 실제 객체를 호출하지 않고 캐시값을 반환한다.

의존객체 주입 및 실행

프록시객체는 실제객체를 주입받고
클라이언트객체는 프록시객체를 주입받는다.

클라이언트객체의 execute 메서드를 호출하면, 주입된 프록시객체의 operation 메서드가 호출되고, 캐시를 수행하며 필요에 따라 실제객체의 operation 메서드를 호출하게 된다. 프록시객체와 실제객체는 같은 인터페이스를 구현한 구현체이므로, 사용메서드의 이름은 같다.

결론

중간에 프록시 객체의 도입으로, 이미 결과값이 있다면 캐싱한 값을 돌려줄 수 있게 되었다. 이처럼 프록시의 도입을 통해 접근제어나 캐싱을 수행하는 디자인패턴을 프록시 패턴 이라고 한다.


[ 데코레이터 패턴 - 예제 ]

공통 인터페이스

실제객체와 프록시 객체의 공통인터페이스이다.

클라이언트 객체

클라이언트 객체이다. 인터페이스인 Component 에 의존한다.

실제 객체

실제 객체이다. Component 를 상속함을 알 수 있다.

프록시 객체1 - 메시지 데코레이터

프록시 객체1 이다. Component 를 상속하며, Component 인터페이스를 필드로 갖는다. 필드의 메서드를 호출하며, 앞 뒤로 *를 추가한다.

프록시 객체2 - 타임 데코레이터

프록시 객체2 이다. Component 를 상속하며, Component 인터페이스를 필드로 갖는다. 필드의 메서드를 호출하며 소요시간을 측정한다.

의존객체 주입 및 실행

실제 객체를 생성한다. 메시지 데코레이터는 실제 객체를 주입받는다. 타임 데코레이터는 메시지 데코레이터를 주입받는다. 클라이언트 객체는 타임 데코레이터를 주입받는다.
클라이언트 메서드 호출 -> 타임 데코레이터 메서드 호출 -> 메시지 데코레이터 메서드 호출 -> 실제객체 메서드 호출의 순서로 진행된다.

결론

데코레이터 패턴을 도입하였다. 실제객체와 클라이언트객체의 수정없이, 의존객체의 변경만으로 부가적인 기능을 수행할 수 있게 되었다. 인터페이스형 참조변수를 사용하고, 프록시객체와 실제객체가 같은 인터페이스를 구현했기 때문에 가능했다.


[ 프로젝트 생성 ]

실제객체가 인터페이스를 구현한 경우일 때, 인터페이스 없이 클래스만 존재할 때, 컴포넌트 스캔을 사용할 때. 각각의 경우에 따라 프록시 적용방법이 다르기 때문에 프로젝트를 만들어보고 프록시를 적용해보겠다.

인터페이스 구현 설계

Repository, Service, Controller 가 각각 인터페이스와 구현클래스가 존재하도록 설계하였다.

그 중 컨트롤러의 인터페이스이다.

  • @RequestMapping
    스프링은 @Controller 나 @RequestMapping 이 클래스에 추가되어 있어야 컨트롤러로 인식한다. 수동빈 등록 방식을 사용할 것이기에 @Controller 대신 @RequestMapping을 사용하였다.
  • @GetMapping
    GetMapping 을 인터페이스 메서드에도 추가 가능하다.
  • @ReqeustParam
    인터페이스에서 @ReqeustParam 사용시 파라미터 명을 반드시 입력해야한다. 그렇지 않으면 자바 버전에 따라 인식하지 못할 수도 있다.

컨트롤러 구현체이다.

서비스와 리포지토리의 인터페이스+구현클래스는 기존에 많이 해왔으니 생략하겠다.

그 후 위와같이 @Configuration 에서 수동 빈 등록을 진행하고

@Configuration을 @Import 를 통해 빈으로 추가하였다. scanBasePackages 속성으로 컴포넌트 스캔 범위를 정할 수도 있다.

인터페이스 없이 클래스 설계

기존에 자주 사용하던 인터페이스 없이 클래스로만 Repository, Service, Controller 를 설계하는 방식이다. 위와 마찬가지로 수동 빈 등록을 진행하였다.

컴포넌트 스캔 설계

컴포넌트 스캔 방식인 @Controller @Service @Repsitory 를 사용하는 설계이다. 자동으로 빈 등록이 진행된다.


[ 인터페이스 기반 프록시 - 적용 ]

컨트롤러 프록시객체

Controller 의 프록시객체이다. 같은 인터페이스를 상속하고, 참조변수로 인터페이스를 갖는다. LogTrace 를 멤버변수로 가져, 로그추적기를 적용시키며 중간에서 실제 객체의 메서드를 호출하는 것을 볼 수 있다. Service 와 Repository 도 같은 방식으로 적용했다.

의존성 주입

Controller Service Repository 의 프록시객체 클래스를 각각 만든뒤 빈으로 등록한다. 이 때 의존성 주입되는 모든 객체에 로그추적기를 적용시킬 수 있도록 Proxy 클래스를 빈으로 등록해야한다.

[ 구체클래스 기반 프록시 - 적용 ]

컨트롤러 프록시객체

Controller 의 프록시객체이다. 다형성을 활용하기 위해 실제클래스를 상속한다. 참조변수로도 실제클래스를 갖는다. 단 실제객체는 Service 를 주입받는데, 자식은 반드시 부모의 생성자를 호출해야 하기 때문에, Service를 주입받게 된다. 그런데 실제객체를 호출해서 사용하여 Service가 필요없어 여기서 null을 넘겨준다. 결과적으로 로그추적기를 적용시키며 중간에서 실제 객체의 메서드를 호출하는 것을 볼 수 있다. Service 와 Repository 도 같은 방식으로 적용했다.

의존성 주입

의존성 주입은 인터페이스 기반 프록시와 동일하게 프록시객체를 빈으로 등록한다.

인터페이스 기반 프록시와 차이

실제객체가 인터페이스를 구현한 방식이 아니어도, 실제객체의 자식클래스로 프록시객체를 생성할 수 있다. 하지만 자식에서는 반드시 부모의 생성자를 호출하기 때문에, 부모 생성자의 파라미터로 null을 넘겨주는 부분이 추가된다.

한계

데코레이터 패턴의 도입으로 로그추적기를 적용할 Controller, Service, Repository 의 코드 수정없이 의존성주입 변경만으로 로그추적기의 적용이 가능해졌다. 그러나 로그추적기 코드는 동일한데, 적용할 대상인 실제객체가 다른 이유로 각각 하나의 프록시객체를 생성해야했다. 적용할 객체가 100개면 100개의 프록시객체를 만들어야한다. 프록시클래스를 하나만 만들어서 모든 곳에 적용할 수는 없을까?
동적 프록시 기술로 이를 해결할 수 있다.

[ 결론 ]

프록시객체는 클라이언트객체와 실제객체 사이에서 중간처리를 수행한다. 이 때 중간에 접근제어나 캐싱을 수행하면 프록시 패턴, 부가기능을 추가하면 데코레이터 패턴이다.
실제객체가 인터페이스+구현클래스로 구현되었다면 프록시객체는 실제객체와 같은 인터페이스를 상속하는 구현클래스이다. 실제객체가 구현클래스로만 구현되었다면 프록시객체는 실제객체의 자식클래스이다.
그래야 다형성을 통해 클라이언트객체와 실제객체를 변경하지 않고, 의존성주입의 변경만으로 프록시객체의 도입이 가능하다.
단 현재 적용할 실제객체마다 일일이 매핑되는 프록시객체를 만들어야하는 한계가 존재한다. 이는 동적프록시의 도입으로 해결할 수 있다.

0개의 댓글