[Spring] 프록시 패턴과 데코레이터 패턴

프록시란 ?

우리가 일반적으로 클라이언트가 서버를 호출할 때는 아래와 같은 그림을 떠올립니다.

직접 호출의 경우에는 클라이언트가 서버를 직접 호출하고, 그 결과를 받습니다.

 

그런데 클라이언트가 요청한 결과를 서버에 직접 요청하는 것이 아닌 "대리자"를 통해 대신 간접적으로 서버에 요청을 할 수도 있습니다.

여기서  "대리자"의 역할을 하는 것은 프록시(Proxy)로, 클라이언트는 프록시를 통해 간접적으로 서버에 요청합니다.

 

이렇게만 보면 굳이 프록시를 중간에 둬서 요청을 해야하나 ? 라는 생각이 들 수 있는데

프록시를 중간에 둘 경우 여러 기능을 추가로 얻을 수 있습니다.


프록시의 주요 기능

접근 제어
  • 권한에 따른 접근 차단
  • 캐싱
  • 지연 로딩
- 접근 제어의 캐싱 예시
철수가 메로나가 먹고 싶어 형한테 올 때 메로나를 사와달라 했는데, 형이 이미 집 냉동실에 메로나가 있다고 합니다.
그러면 철수는 형이 메로나를 사오는 시간 보다 훨씬 빨리 집 냉동실에 있는 메로나를 꺼내 먹을 수 있습니다. 

 

부가 기능 추가
  • 원래 서버가 제공하는 기능에 더해 부가 기능 수행
- 부가 기능 추가의 예시
철수의 자동차가 너무 더러워서 철수가 동생에게 자동차 세차좀 하고 와달라고 부탁을 했는데,
동생이 이미 세차를 끝내고 주유까지 해왔다고 합니다.
그러면 철수는 세차만 하려했지만 주유라는 부가 기능 까지 얻게 되었습니다.

 


인터페이스 기반 프록시와 클래스 기반 프록시

프록시는 아무 객체나 프록시가 될 수 없습니다.

객체에서 프록시가 되려면, 클라이언트는 서버에게 요청을 한 것인지, 프록시에게 요청을 한 것인지 조차 몰라야 합니다. ("대리자" (3자)를 통해 요청해 그 이후 과정은 모르기 때문)

즉, 서버와 프록시는 같은 인터페이스를 사용하거나,  클래스를 상속받아 프록시를 사용해야 합니다.

그리고 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야 합니다.

 

인터페이스 기반 프록시와 클래스 기반 프록시의 차이
  • 인터페이스 기반 프록시 : 인터페이스만 같으면 모든 곳에 적용할 수 있다.
  • 클래스 기반 프록시 : 해당 클래스에만 적용할 수 있다.
  인터페이스 기반 프록시 클래스 기반 프록시
단점 인터페이스를 추가로 만들어야 한다. 1. 부모 클래스의 생성자를 호출해야 한다.
2. 클래스에 final 키워드가 붙으면 상속이 불가능하다.
3. 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.

 

이렇게만 보면 인터페이스 기반 프록시가 더 좋아보이는데, 맞습니다.

인터페이스 기반의 프록시는 상속이라는 제약에서 자유롭고, 프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 더 좋습니다.

 


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

둘다 프록시를 사용하는 방법이지만, 이 둘의 의도에 따라 프록시 패턴과 데코레이터 패턴으로 구분하는 것이 핵심입니다.

  • 프록시 패턴 : 접근 제어가 목적
  • 데코레이터 패턴 : 새로운 기능 추가가 목적
프록시 패턴과 데코레이터 패턴을 사용하는 이유?

기존 코드에 부가 기능을 추가하거나, 접근 제어를 하려면 기존 코드에 수정이 필요하고, 추가나 제어가 필요한 코드가 100개라면 그 100개의 코드를 전부다 까서 수정을 해야할 것입니다.

하지만 프록시 개념을 도입하면 다형성을 이용함으로써

클라이언트 코드의 변경 없이 자유롭게 부가 기능이나 접근 제어를 추가하거나 뺄 수 있고, 실제 클라이언트 입장에서는 프록시 객체가 주입되었는지, 실제 객체가 주입되었는지 알지 못합니다.

즉, 클라이언트 코드를 전혀 수정하지 않고 부가 기능 추가와 접근 제어가 가능합니다.

 

 

아래의 예제 코드를 통해 프록시 패턴과 데코레이터 패턴에 대해 간단하게 알아봅시다.


프록시 패턴 (캐싱)

클라이언트 코드

인터페이스와 구현체

위 코드들의 관계를 그리면 다음과 같습니다.

Client가 Subject 인터페이스를 의존하고, Proxy와 RealSubject는 Subject 인페이스를 구현하여 operation() 메서드를 오버라이딩 하였습니다.

 

테스트 코드
public class ProxyPatternTest {

    @Test
    void noProxyTest() {
        RealSubject realSubject = new RealSubject();
        ProxyPatternClient client = new ProxyPatternClient(realSubject);

        client.execute(); // 1초
        client.execute(); // 1초
        client.execute(); // 1초
    }

    @Test
    void cacheProxyTest() {
        Subject realSubject = new RealSubject();
        Subject cacheProxy = new CacheProxy(realSubject);
        ProxyPatternClient client = new ProxyPatternClient(cacheProxy);

        client.execute(); // 1초
        client.execute();
        client.execute();
    }
}

캐쉬 프록시 패턴 적용한 뒤의 객체 의존 관계를 보면 다음과 같습니다.

 

캐시 프록시를 도입하기 전에는 실제 객체를 호출할 때마다 1초가 걸린반면,

캐시 프록시 도입 이후에는 최초에 한번만 1초가 걸리고 그 이후에는 즉시 반환되는 것을 볼 수 있습니다.

 

  • 프록시 사용 X

 

  • 프록시 사용 O

 

프록시 패턴의 핵심

위 코드를 보면 기존의 RealSubject 코드에서 변경없이, RealSubject 와 같은 인터페이스를 구현하는 cacheProxy를 만들어 접근 제어를 했습니다.

RealSubject 와 cacheProxy 둘 다 Subject 인터페이스를 구현하기 때문에,

클라이언트 코드의 변경 없이 자유롭게 프록시를 넣고 뺄 수 있고, 실제 클라이언트 입장에서는 프록시 객체가 주입되었는지, 실제 객체가 주입되었는지 알지 못합니다.

 

이렇게 프록시 패턴을 사용하여 클라이언트 코드를 전혀 변경하지 않고, 프록시를 도입해서 접근 제어를 했다는 점이 프록시 패턴의 핵심입니다.


데코레이터 패턴 (부가 기능 추가)

클라이언트 코드

인터페이스와 구현체

위 코드들의 관계를 그리면 다음과 같습니다.

Client는 Componet 인터페이스를 의존하고, RealComponent와 TimeDecorator, MssageDecorator는 Component 인페이스를 구현하여 operation() 메서드를 오버라이딩 하였습니다.

 

테스트 코드
@Slf4j
public class DecoratorPatternTest {

    @Test
    void noDecorator() {
        Component realComponent = new RealComponent();
        DecoratorPatternClient client = new DecoratorPatternClient(realComponent);
        client.execute();
    }

    @Test
    void 부가_기능_추가() {
        Component realComponent = new RealComponent();
        Component messageDecorator = new MessageDecorator(realComponent);
        Component timeDecorator = new TimeDecorator(messageDecorator);
        DecoratorPatternClient client = new DecoratorPatternClient(timeDecorator);
        client.execute();
    }
}

데코레이터 패턴 적용한 뒤의 객체 의존 관계를 보면 다음과 같습니다.

 

  • 데코레이터 패턴 적용 전

 

  • 데코레이터 패턴 적용 후

 

데코레이터 패턴의 핵심

부가 기능은 하나 뿐 아니라 위 처럼 여러 부가 기능을 추가할 수 있는 것을 볼 수 있습니다. (프록시 체인)

그리고 여기서 핵심은 프록시인 데코레이터 패턴을 사용해서 기존 코드를 전혀 변경하지 않고 부가 기능을 추가한 것입니다.

 


이렇게만 보면 프록시 패턴, 데코레이터 패턴이 너무 나도 좋은거 같지만 한 가지 단점이 있습니다.

바로 프록시를 적용해야할 대상 클래스가 100개라면 프록시 클래스도 100개를 만들어야 하는 것입니다.

이러한 단점을 해결 해주는 것이 바로 "동적 프록시" 입니다.

다음 포스팅에서는 아주 편리하게 자동으로 동적 프록시를 적용해주는 "스프링 AOP"에 대해 다뤄보겠습니다.

 

[Spring] 스프링 AOP(Aspect Oriented Programming)란? - @Aspect

AOP를 사용하지 않는다면 ? AOP에 대해 설명하기 전에 AOP를 사용하는 이유에 대해 먼저 알아 봅시다. 애플리케이션 로직은 크게 핵심 기능과 부가 기능으로 나눌 수 있습니다. 핵심 기능 : 해당 객

hstory0208.tistory.com

 


참고자료 : 김영한의 스프링 핵심 원리 - 고급편