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

ForLearn·2022년 9월 27일
0

프록시 패턴

이전에 템플릿 메서드 패턴과 콜백 패턴을 사용하여 부가기능을 추가해 보았다.
코드 수정을 최소화하기는 했지만, 부가기능을 적용해야 할 클래스가 증가와 수정사항이 발생할 경우 일일이 원본 코드를 변경하는 문제점이 남아있었다.

원본 코드를 수정하지 않고 부가기능을 적용해기 위해서는 프록시라는 개념을 알아야 한다.

프록시

클라이언트와 생각해 보면 클라이언트는 '의뢰인'이라는 뜻을 가지고 서버는 '서비스나 상품을 제공하는 사람이나 물건을 의미한다'. 이를 웹개발 측면에서 보면 클라이언트는 브라우저 서버는 웹 서버가 된다.

일반적으로 클라이언트가 서버를 직접 호출하면 서버는 결과를 반환해준다.

클라이언트가 요청한 결과를 서버에 직접 요청하지 않고 대리자를 통해 대신 간접적으로 요청할 수 있다. 이 때 대리자를 영어로 프록시(Proxy)라 한다.

대리자의 역할

이 경우 대리자는 다양한 역할을 수행할 수 있다.

  • 요청 받은 데이터를 이미 대리자가 가지고 있는 겨우 , 서버에 접근하지 않고 프록시에서 처리(접근 제어, 캐싱)
  • 대리자가 부가기능 추가
  • 대리자가 또 다른 대리자에게 요청 (프록시 체인)
    • 만약 내가 동생에게 라면을 사오라고 요청했는데, 동생이 다른 사람에게 라면을 사오라고 요청한 경우
    • ➡ 나는 라면을 동생에게 요청했기 때문에 그 이후의 과정은 모름, 동생을 통해 라면이 도착하기만 하면 된다.

대체 가능

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

즉, 서버와 프록시는 같은 인터페이스를 사용해야 한다. 그리고 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야 한다.

클래스 의존관계를 보면 클라이언트는 서버 인터페이스( ServerInterface )에만 의존한다. 그리고 서버와 프록시가 같은 인터페이스를 사용한다. 따라서 DI를 사용해서 대체 가능하다.

런타임 객체 의존관계를 보면, client ➡ server 을 DI를 이용해 client ➡ proxy로 변경하면 크라이언트 코드 변경없이 의존관계 변경이 가능하다.

GOF 디자인 패턴 - 프록시 패턴과 데코레이터 패턴

프록시를 사용하는 패턴은 프록시 패턴과 데코레이터 패턴이 있다. 이 두 패턴은 거의 비슷해서 사용하는 의도(intent)로 구분하자.

  • 프록시 패턴 (접근제어가 목적)

    • 권한에 따른 접근 차단
    • 캐싱
    • 지연 로딩
  • 데코레이터 패턴(부가기능 추가 목적)

    • 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
    • 예) 요청 값이나, 응답 값을 중간에 변형한다
    • 예) 실행 시간을 측정해서 추가 로그를 남긴다.

❗ 프록시 패턴만 프록시를 사용하는 게 아니다.의도로 구분하자!!

프록시 패턴 - 예제

Subject

public interface Subject {
    String operation();
}

RealSubject

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();
        }
    }
}
코드를 입력하세요
  • operation()재정의하여 실제 데이터 호출시 1초가 걸린다고 가정

ProxyPatternClient

public class ProxyPattenClient {

    private Subject subject;

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

    public void execute() {
        subject.operation();
    }
}
  • Subject인터페이스에 의존하여 실제 호출하는 클라이언트 코드이다.

ProxyPatternTest

    @Test
    void noProxyTest() {
        RealSubject realSubject = new RealSubject();
        ProxyPattenClient client = new ProxyPattenClient(realSubject);
     
        client.execute();
        client.execute();
        client.execute();
    }
  • 실행 시 client.execute() 를 3번 호출하여 총 3초가 걸렸다.
  • 이미 조회했던 데이터를 다시 사용하는 것이 성능상 좋다. 이런 것을 캐시라고 한다.

프록시 패턴을 통해 캐시를 적용해보자.

CacheProxy

@Slf4j
public class CacheProxy implements Subject {

    private Subject target; 
    private String cacheValue;  // cacheValue 값 존재하면 그 target 호출 않고 cacheValue 반환

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

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

        return cacheValue;
    }
}
  • private Subject target클라리언트가 프록를 호출하면 프록시는 실제 객체를 호출해야하기 때문에 실제 객체의 참조를 가져야 한다. 실제객체를 target이라 함
  • operation() 코드를 보면 cacheValuenull인 경우에만 원본 객체를 호출하고, 값이 존재하면 cacheValue를 반환한다.
  • 프록시 패턴을 사용하여 원본 객체에 접근은 제한하여 데이터를 빠르게 조회할 수 있다.

cacheProxyTest()

    @Test
    void cacheProxyTest() {
        RealSubject realSubject = new RealSubject();
        CacheProxy cacheProxy = new CacheProxy(realSubject); 
        
        ProxyPattenClient client = new ProxyPattenClient(cacheProxy); 
        client.execute();
        client.execute();
        client.execute();
    }
  • client -> cacheProxy -> realSubject 의존관계 형성
  • 클라이언트는 인터페이스만 참조하기 때문에 프록시 객체가 주입되었는지 , 실제 객체가 주입되었는지 모른다.

데코레이터 패턴

예제1

Component

public interface Component {
    String operation();
}

RealComponent

@Slf4j
public class RealComponent implements Component {

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

DecoratorPatternClient

@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);

    }
}
  • 클라이언트 코드는 Component 인터페이스에만 의존한다.

noDecorator

@Test
 void noDecorator() {
 Component realComponent = new RealComponent();
 DecoratorPatternClient client = new
DecoratorPatternClient(realComponent);
 client.execute();
 }
  • 위의 코드는 client ➡ realComponent의 의존관계를 가지고 있다.

예제2 - 부가 기능 추가

프록시로 부가기능을 추가하는 것을 데코레이터 패턴이라 한다.
대표적으로 로그를 추가하거나 요청값, 응답값을 중간에 변형하는 등의 기능이 있다.

MessageDecorator

@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;
    }
}
  • Component 인터페이스를 구현한다.
  • 프록시가 호출해야 하는 대상을 component에 저장한다.
  • operation()호출시 프록시와 연결된 대상을 호출하고, 그 응답 값에 ****을 더해 꾸민 후 반환한다.

Test

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

    }
  • client -> messageDecorator -> realComponent의 객체 의존 관계 생성

예제3 - 데코레이터 패턴 체인

실행 시간 측정하는 기능을 추가해 보자

TimeDecorator

@Slf4j
public class TimeDecorator implements Component {

    private Component component;

    public TimeDecorator(Component component) {
        this.component = component;
    }

    @Override
    public String operation() {
        log.info("TimeDecorator 실행");
        long startTime = System.currentTimeMillis();

        String result = component.operation();

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("TimeDecorator 종료 resultTime ={} ms",resultTime);
        return result;
    }
}

Test

   @Test
    void decorator2() {
        RealComponent realComponent = new RealComponent();
        MessageDecorator messageDecorator = new MessageDecorator(realComponent);
        TimeDecorator timeDecorator = new TimeDecorator(messageDecorator);
        DecoratorPatternClient client = new DecoratorPatternClient(timeDecorator);

        client.execute();
    }

실행 결과

구체 클래스 프록시 적용

지금까지 인터페이스를 통해 프록시를 적용했다.
만약 인터페이스가 없는 구체 클래스를 프록시로 적용하려면 어떻게 해야 할까 ?

➡ 자바의 다형성은 인터페이스를 구현하든 , 클래스를 상속하든 상위 타입만 맞으면 다형성이 적용된다. 즉, 클래스의 상속을 이용한 프록시가 가능하다.

ConcreteLogic - 구체 클래스

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

TimeProxy - 구체클래스를 상속한 프록시

@Slf4j
public class TimeProxy extends ConcreteLogic {

 private ConcreteLogic realLogic;
 
 public TimeProxy(ConcreteLogic realLogic) {
	 this.realLogic = realLogic;
 }
 
 @Override
 public String operation() {
 
	 log.info("TimeDecorator 실행");
 	long startTime = System.currentTimeMillis();
   	String result = realLogic.operation();
   	long endTime = System.currentTimeMillis();
    
   	long resultTime = endTime - startTime;
   	log.info("TimeDecorator 종료 resultTime={}", resultTime);

return result;
 	}
}

Test

@Test
void addProxy() {
 ConcreteLogic concreteLogic = new ConcreteLogic();
 TimeProxy timeProxy = new TimeProxy(concreteLogic);
 ConcreteClient client = new ConcreteClient(timeProxy);
 client.execute();
}
  • 핵심은 ConcreteClient 는 ConcreteLogic 을 의존하는데, 다형성에 의해 ConcreteLogic 에
    concreteLogic 도 들어갈 수 있고, timeProxy 도 들어갈 수 있다는 것이다.

단점

  • 클래스 기반의 프록시는 상속의 제약을 가진다.
  • 부모 클래스의 생성자를 호출해야 한다.
  • 클래스에 final 키워드가 붙으면 상속이 불가능하다.
  • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.

정리

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

Reference

0개의 댓글