[Spring] 프록시(Proxy) 패턴, 데코레이터(Decorater) 패턴

Donghoon Jeong·2024년 5월 30일
0

Spring

목록 보기
6/15
post-thumbnail

이번 포스팅에서는 스프링의 핵심 디자인 패턴 중 프록시 패턴과 데코레이터 패턴에 대해서 알아보겠습니다.

프록시 패턴과 데코레이터 두 패턴 모두 프록시라는 개념을 사용하는 패턴이기 때문에, 프록시에 대해서 먼저 알아보고 두 패턴의 특징에 대해서 설명하겠습니다.


프록시 (Proxy)

우리가 일반적으로 알고 있는 클라이언트와 서버와의 관계에서 클라이언트가 서버를 직접 호출하고, 처리 결과를 직접 받습니다. 이것을 직접 호출이라고 합니다.

직접 호출 방식과는 다르게, 클라이언트가 요청한 결과를 서버에 직접 요청하는 것이 아니라 어떤 대리자를 통해서 간접적으로 서버에 요청을 할 수도 있습니다.

여기서 간접 호출하는 대상을 프록시(Proxy)라고 합니다.

프록시는 Client와 Server 사이에서 대리자의 역할을 수행하기 때문에 여러 가지 일을 수행할 수 있습니다.

  • 권한에 따른 접근제어, 캐싱

  • 부가 기능 추가 → 로그 기록

  • 프록시가 또 다른 프록시를 호출하는 형태의 프록시 체인

어떤 객체가 프록시 객체가 될 수 있을까?

프록시 객체가 되기 위해서는 클라이언트는 서버에게 요청을 한 것인지, 프록시에게 요청을 한 것인지조차 몰라야 합니다.

즉, 서버와 프록시는 같은 인터페이스를 사용해야 하며, 클라이언트 코드를 변경하지 않고 프록시 객체와 실제 객체를 런타임 시에 의존 관계 주입만 변경해서 사용할 수 있어야 합니다.

예시로 위 클래스 의존관계를 보면 클라이언트는 인터페이스인 ServerInterface에만 의존하고 있습니다. 그리고 서버와 프록시는 같은 인터페이스를 사용하고 있기 때문에 DI를 사용해서 대체 가능합니다.

런타임 시점에 의존관계를 그림으로 나타내면 다음과 같습니다. 런타임 시점에 클라이언트 객체에 DI를 사용해서 Client → Server에서 Client → Proxy로 객체 의존관계를 변경하더라도 클라이언트 코드를 전혀 변경하지 않아도 됩니다.

DI를 사용하여 프록시 객체를 주입받았기 때문에, 클라이언트 입장에서는 변경 사실조차 모릅니다.

이러한 프록시 개념을 사용하는 두 패턴이 프록시 패턴데코레이터 패턴입니다.

각각의 패턴에 대해서 예제 코드를 통해 자세하게 알아보겠습니다.


프록시 패턴 (Proxy Pattern)

public interface Subject {
	String operation();
}

Subject 인터페이스는 추후에 실제 객체와 프록시 객체가 동일한 메서드를 구현하도록 강제하기 위해 생성하였습니다.

@Slf4j
public class RealSubject implements Subject {

    @Override
    public String operation() {
        log.info("실제 객체 호출");
        sleep(1000);
        return "data";
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Subject 인터페이스를 구현하는 실제 객체를 생성합니다.

sleep(1000) 메서드는 서버에서의 비즈니스 로직이 1초 걸린다는 것을 가정하기 위한 코드입니다.

public class ProxyPatternClient {

    private Subject subject;

    public ProxyPatternClient(Subject subject) {
        this.subject = subject;
    }

    public void execute(){
        subject.operation();
    }
}

DI 받은 서버를 통해 operation() 메서드를 호출하는 클라이언트 코드입니다.

프록시 패턴을 적용하기 전에, 클래스 의존 관계는 위 사진과 같습니다.

void noProxyTest() {
	RealSubject realSubject = new RealSubject();
	ProxyPatternClient client = new ProxyPatternClient(realSubject);
	client.execute();
	client.execute();
	client.execute();
}

// 실행 결과
RealSubject - 실제 객체 호출
RealSubject - 실제 객체 호출
RealSubject - 실제 객체 호출


런타임 의존관계는 다음과 같이 설정됩니다.

프록시 객체가 존재하지 않기 때문에 클라이언트가 실제 객체를 3번 요청한다면 3번의 실제 객체 호출이 일어나 비즈니스를 완료하는데 총 3초의 시간이 걸릴 것입니다.

그런데 이 데이터가 한번 조회하면 변하지 않는 데이터라면 어딘가에 보관해두고 이미 조회한 데이터를 사용하는 것이 성능상 좋습니다.

위 개념을 캐시라고 합니다.

프록시 패턴의 주요 기능은 접근 제어입니다. 캐시도 접근 자체를 제어하는 기능 중 하나이기 때문에, 프록시 패턴을 사용하여 캐시를 적용해 보겠습니다.

@Slf4j
public class CacheProxy implements Subject{

    private Subject target;
    private String cacheValue;

    public CacheProxy(Subject target) {
        this.target = target;
    }

    @Override
    public String operation() {
      log.info("프록시 호출");
      if(cacheValue==null){
          cacheValue = target.operation();
      }
      return cacheValue;
    }
}

CacheProxy 클래스도 Subject 인터페이스를 구현합니다.

프록시 객체는 실제 서버를 호출해야되기 때문에, 실제 객체에 대한 참조(target)를 가지고 있으며, operation() 메서드가 호출되면 캐싱 된 값을 반환하거나, 캐시가 없을 경우 실제 객체의 메서드를 호출하여 값을 저장하고 반환합니다.

위 코드를 추가할 경우, 클래스 의존 관계는 다음과 같습니다.

void cacheProxyTest() {
	Subject realSubject = new RealSubject();
	Subject cacheProxy = new CacheProxy(realSubject);
	ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
	client.execute();
	client.execute();
	client.execute();
}

// 실행 결과
CacheProxy - 프록시 호출
RealSubject - 실제 객체 호출
CacheProxy - 프록시 호출
CacheProxy - 프록시 호출

위 클라이언트 코드인 ProxyPatternClient 클래스를 하나도 수정하지 않고 프록시 패턴을 적용했습니다.

realSubjectcacheProxy를 생성하고 둘을 연결합니다.

결과적으로 cacheProxyrealSubject를 참조하는 런타임 객체 의존관계가 완성됩니다.

그리고 마지막으로 clientrealSubject가 아닌 cacheProxy를 주입합니다.

이 과정을 통해서 위 사진과 동일한 런타임 객체 의존 관계가 완성되었습니다.

첫 번째 호출에서는 프락시가 실제 객체를 호출하지만, 이후 호출에서는 캐싱 된 값을 반환하여 시간 소요를 줄입니다.

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

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


데코레이터 패턴 (Decorator Pattern)

public interface Component {
    String operation();
}
@Slf4j
public class RealComponent implements Component {

    @Override
    public String operation() {
		log.info("RealComponent 실행");
        return "data";
    }
}
@Slf4j
public class DecoratorPatternClient {

    private Component component;
     
    public DecoratorPatternClient(Component component) {
        this.component = component;
	}
    
    public void execute() {
        String result = component.operation();
        log.info("result = {}", result);
	} 
}

데코레이터 패턴을 적용하기 전, 클래스 의존 관계는 다음과 같습니다.

void noDecorator() {
	Component realComponent = new RealComponent();
    DecoratorPatternClient client = new DecoratorPatternClient(realComponent);
    client.execute();
}
     
     
// 실행 결과
RealComponent - RealComponent 실행 
DecoratorPatternClient - result = data

런타임 객체 의존 관계는 다음과 같습니다.

앞서 설명한 것처럼 프록시를 통해서 할 수 있는 기능은 크게 접근 제어부가 기능 추가라는 2가지가 있습니다.

앞서 프록시 패턴에서 캐시를 통한 접근 제어를 알아보았습니다. 이번에는 프록시를 활용해서 부가 기능을 추가해 보겠습니다. 이렇게 프록시로 부가 기능을 추가하는 것을 데코레이터 패턴이라고 합니다.

@Slf4j
public class MessageDecorator implements Component {

	private Component component;
    
    public MessageDecorator(Component component) {
        this.component = component;
	}
     
    @Override
    public String operation() {
		log.info("MessageDecorator 실행");
		String result = component.operation();
		String decoResult = "*****" + result + "*****"; 		
        log.info("MessageDecorator 꾸미기 적용 전 = {}, 적용 후 = {}", result, decoResult);
    	return decoResult;
	}
}

MessageDecorator 클래스는 Component 인터페이스를 구현합니다.

프록시가 호출해야 하는 대상을 component에 저장하고 operation() 메서드를 호출하면 프록시와 연결된 대상의 operation() 메서드를 실행하고 그 결과값을 꾸며 반환합니다.

데코레이터 패턴 적용 후, 클래스 의존 관계는 다음과 같습니다.

void decorator1() {
    Component realComponent = new RealComponent();
    Component messageDecorator = new MessageDecorator(realComponent);
	DecoratorPatternClient client = new DecoratorPatternClient(messageDecorator);
    client.execute();
}

// 실행 결과
MessageDecorator - MessageDecorator 실행
RealComponent - RealComponent 실행
MessageDecorator - MessageDecorator 꾸미기 적용 전 = data, 적용 후 = *****data*****
DecoratorPatternClient - result = *****data*****

위 코드를 실행하면 런타임 객체 의존 관계가 위와 같이 설정됩니다.
client → messageDecorator → realComponent의 객체 의존 관계를 만들고 client.execute()를 호출한다.

실행 결과를 보면 MessageDecoratorRealComponent를 호출하고 반환한 응답 메시지를 꾸며서 반환한 것을 확인할 수 있습니다.


정리

프록시 패턴과 데코레이터 패턴 두 패턴 모두 프록시를 사용하는 패턴이고 그 모양이 거의 비슷한 것 같은데 이 둘을 어떻게 구분할 수 있을까?

디자인 패턴에서 중요한 것은 해당 패턴의 겉모양이 아니라 그 패턴을 만든 의도가 더 중요합니다. 따라서 의도에 따라 패턴을 구분합니다.

  • 프록시 패턴의 의도

    다른 개체에 대한 접근을 제어하기 위해 대리자를 제공

  • 데코레이터 패턴의 의도

    객체에 추가 기능을 동적으로 추가하고, 기능 확장을 위한 유연한 대안 제공

정리하자면, 프록시를 사용하고 해당 프록시가 접근 제어가 목적이라면 프록시 패턴이고, 새로운 기능을 추가하는 것이 목적이라면 데코레이터 패턴이 됩니다.


Reference

스프링 핵심 원리 - 고급편

profile
정신 🍒 !

0개의 댓글