모르지 않다는 것은 아는것과 다르다.

객체지향

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

채마스 2022. 8. 6. 23:31

개요

  • 프록시를 이용하면 기존로직에 변화를 주지 않고도 새로운 기능을 추가할 수 있다.
  • 스프링은 이러한 방법을 이용해서 유연하게 기능을 확장했다.
  • 그래서 기존코드를 건드리지 않고 새로운 기능을 추가할 수 있는 프록시 패턴과 데코레이터 패턴을 학습하려고 한다.

 

클라이언트, 서버, 프록시

  • 보통 Client와 Server를 생각하면 컴퓨터 네트워크의 Client, Server 를 많이 떠올리지만, 이 개념을 객체에 도입하면 요청하는 객체는 클라이언트가 되고, 요청을 처리하는 객체는 서버가 된다.
  • 하지만 여기서 Client가 요청한 결과를 서버에 직접 요청하는 것이 아니라 어떤 대리자를 통해서 간접적으로 서버에 요청할 수 있다.
  • 여기서 그 대리자가 바로 프록시다.
  • 프록시는 객체안에서의 개념도 있고, 웹 서버에서의 개념도 있지만 규모의 차이가 있을 뿐 근본적인 역할은 같다.

 

프록시가 되기 위한 조건

  • Client는 Server에게 요청을 한 것인지, Proxy에게 요청을 한 것인지 몰라야 한다. -> 서버객체를 프록시로 변경해도 클라이언트의 코드는 변하지 않아야 한다.
  • 그러기 위해서 Server와 Proxy는 같은 인터페이스를 구현해야한다.

 

프록시의 주요기능

  • 프록시를 통해서 할 수 있는 일은 크게 2가지로 구분할 수 있다.
    • 접근 제어
      • 권한에 따른 접근 차단
      • 캐싱
      • 지연 로딩
    • 부가 기능 추가

 

프록시 패턴

  • 프록시 패턴은 위의 프록시 기능중 접근 제어를 하는 기능을 구현하는데 사용되는 패턴이다.
  • 이번에는 그 중에서 캐싱에 해당되는 기능을 프록시 패턴으로 구현해 보려고 한다.
  • 프록시 패턴에서 말하는 프록시는 위에서 안급했던 프록시의 개념과는 다르다.

 

프록시 패턴 적용 전

public interface Subject {
      String operation();
}
@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();
        }
    } 

}
  • RealSubject는 Server에 해당한다.
  • 위와 같이 "data"를 반환하기 전에 1초를 쉰다. -> 데이터를 받아오는 시간이라고 생각하면 된다.
public class ProxyPatternClient {
    private Subject subject;

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

    public void execute() {

        subject.operation();
    }
}
  • ProxyPatternClient는 Client에 해당한다.
public class ProxyPatternTest {
    @Test
    void noProxyTest() {
        RealSubject realSubject = new RealSubject();
        ProxyPatternClient client = new ProxyPatternClient(realSubject);
        client.execute();
        client.execute();
        client.execute();
    } 
}
  • 위와 같이 Client는 Server를 직접 주입 받는다.
  • 위의 코드를 실행시키면 총 3초의 시간이 소요된다.
  • 이제 프록시패턴을 사용해서 캐싱기능을 구현해 3초에서 1초로 성능을 높여보도록 한다.

 

프록시 적용 후

@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;
    }
}
  • 위와 같이 Server에 해당하는 RealSubject와 같은 인터페이스(Subject)를 구현해야한다.
  • 그리고 Proxy가 Server에 접근할 수 있어야 하기때문에 상위 인터페이스(Subject)를 참조하고 있어야한다.
  • operation을 보면, 값이 없으면 Server에 접근하고 있으면 Proxy에 저장된 값을 반환한다. -> 캐싱 기능
@Test
void cacheProxyTest() {
    Subject realSubject = new RealSubject();
    Subject cacheProxy = new CacheProxy(realSubject);
    ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
    client.execute();
    client.execute();
    client.execute();
}
  • 위와 같이 Proxy에 Server를 주입한다.
  • 그 다음 클라이언트는 Server가 아닌 Proxy를 주입 받는다.
  • 실행 결과는 처음 execute만 Proxy가 Server에 접근하고 나머지는 Proxy에 저장된 값이 반환되기 때문에 약 1초의 시간만 소요된다.
  • 이렇듯 RealSubject 코드와 클라이언트 코드를 전혀 변경하지 않고, 프록시를 도입해서 접근 제어할 수 있다.
  • 또한, 라이언트 코드의 변경 없이 자유롭게 프록시를 넣고 뺄 수 있다. 실제 클라이언트 입장에서는 프록시 객체가 주입되었는지, 실제 객체가 주입되었는지 알지 못한다.

 

데코레이터 패턴

  • 다음으로는 데코레이터 패턴을 적용해 보려고한다.
  • 위에서 배웠던 프록시 패턴과 마찬가지로 프록시를 사용한다는 공통점이 있다.
  • 하지만 프록시 패턴이 접근을 제어하는 것이 초점을 맞췄다면, 데코레이터 패턴은 객체에 추가 기능을 부여하는데 주로 사용된다.

 

데코레이터 패턴 적용 전

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();
        og.info("result={}", result);
    } 
}
  • Client 코드는 Component 인터페이스에 의존하며, component.operation()를 실행한다.
@Slf4j
public class DecoratorPatternTest {
    @Test
    void noDecorator() {
        Component realComponent = new RealComponent();
        DecoratorPatternClient client = new DecoratorPatternClient(realComponent);
        client.execute();
    }
}
  • 위와 같이 Client가 Component를 주입 받아 excute()하는 것을 확인할 수 있다.
  • 이제 데코레이터 패턴을 사용해서 부가기능(로그)을 코드의 변경없이 구현해 보도록 하겠다.

 

데코레이터 패턴 적용 후

@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를 변수로 가지고 있다.
  • 로그를 찍으면서 component.operation();를 실행시켜 실제 구현체를 호출하는 것을 확인할 수 있다.
@Test
void decorator1() {
    Component realComponent = new RealComponent();
    Component messageDecorator = new MessageDecorator(realComponent);
    DecoratorPatternClient client = new DecoratorPatternClient(messageDecorator);
    client.execute();
}
  • 위의 코드를 보면, 실제 구현체(realComponent)를 messageDecorator(프록시, 부가기능)에 주입해준다.
  • 또한 Client는 더이상 실제 구현체가 아닌 프록시에 의존한다.
  • 의존관계를 정리하면, client -> messageDecorator -> realComponent 가 되는 것이다.
  • client.execute 가 실행되면 MessageDecorator.operation()이 실행되며, MessageDecorator.operation() 내부에서 부가기능을 수행하고 realComponent.operation()을 실행하게 된다.
  • 즉 기존 코드를 수정하지 않고 부가기능을 추가할 수 있었다.
  • 만약 실행시간을 측정하는 부가기능을 하나 더 추가한다면?

@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;
    } 
}
  • 위와 같이 실행시간을 측정하는 부가기능을 추가한다.
  • 이 클래스 또한 Component를 구현하고 있으며, Component를 변수로 가지고 있는것을 확인할 수 있다.
@Test
void decorator2() {
    Component realComponent = new RealComponent();
    Component messageDecorator = new MessageDecorator(realComponent);
    Component timeDecorator = new TimeDecorator(messageDecorator);
    DecoratorPatternClient client = new DecoratorPatternClient(timeDecorator);
    client.execute();
}
  • 위의 코드도 마찬가지로 client가 실행되면, timeDecorator가 실행되고, 그 이후에 messageDecorator가 실행된다.
  • 그리고 마지막으로 실제 구현체인 realComponet가 실해된다.
  • 따라서 의존관계는 client -> timeDecorator -> messageDecorator -> realComponent 가 된다.

 

정리

  • 프록시 패턴과 데코레이터 패턴은 거의 비슷한 것을 확인할 수 있다.
  • 하지만, 의도에서 차이가 있다.
  • 접근을 제어하기 위한 의도라면 프록시 패턴을, 추가 기능을 동적으로 추가하고 싶다면 데코레이터 패턴을 사용하면 된다.
  • 이 둘 모두 코드의 변경없이 어떠한 역할을 추가하기 위해 생긴 디자인 패턴이며 때로는 둘이 완전히 같을 때도 있다.

 

References

  • 김영한님의 스프링 고급편

'객체지향' 카테고리의 다른 글

프록시 적용하기  (0) 2022.08.06
생성 패턴 (디자인 패턴)  (0) 2022.05.15
행위 패턴 (디자인 패턴)  (0) 2022.05.15
구조패턴 (디자인 패턴)  (0) 2022.05.15
클래스 다이어그램  (0) 2022.04.23