프록시와 디자인 패턴

JooHeon·2021년 12월 15일
0

🖊 프록시란?

요청자를 Client 요청을 처리해주는 자를 Server라고 할 때,
Client에서 Server에 처리를 요청하는 것은 직접 요청이고
Client에서 어떤 대리자를 거쳐서 Server에 처리를 요청하는 것은 간접 요청이다.
간접 요청에서 이 대리자를 프록시(Proxy)라고 한다.

🖊 프록시의 이점

간접요청을 하면 프록시를 통해 여러가지 일을 할 수 있다.
접근 제어, 부가 기능을 추가할 수 있고 대리자를 통해 또 다른 대리자를 호출할 수도 있다(프록시 체인).

  • 접근 제어 (프록시 패턴)
    권한에 따른 접근 차단
    캐싱
    지연 로딩

  • 부가 기능 추가(데코레이터 패턴)
    원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.


🖊 객체가 프록시가 되려면?

객체에서 프록시가 되려면, Client는 Server에 요청을 한 건지, 프록시에 요청을 한 것인지 조차 몰라야한다. (대체 가능)
-> 프록시와 실제 객체가 같은 인터페이스를 사용해야한다.
-> Client가 사용하는 Server 객체를 프록시 객체로 변경해도 Client 코드를 수정할 필요가 없어야 한다.

🖊 프록시 패턴 예제

// 실제 Server 객체
@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();
        }
    }
}

// 프록시 객체
@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;
    }
}


// Client 객체
public class ProxyPatternClient {

    private Subject subject;

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

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

// 테스트 
@Test
public void cacheProxyTest() throws Exception{
  RealSubject realSubject = new RealSubject();
  CacheProxy  cacheProxy = new CacheProxy(realSubject);
  ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
  client.execute();
  client.execute();
  client.execute();
}

// 결과
17:33:29.814 [main] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 호출
17:33:29.819 [main] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출
17:33:30.820 [main] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 호출
17:33:30.821 [main] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 호출

실제 객체에서 데이터를 가져오는데 1초가 걸린다고 가정하면 프록시가 없을 경우엔 3초가 걸렸을 것이다.
그러나 프록시 객체에서 캐싱을 통해 접근제어를 한다면 처음 1회 후 나머지 2회는 캐시에서 반환한다.
따라서 1초가 된다.

🖊 데코레이터 패턴 예제

//실제 Server 객체
@Slf4j
public class RealComponent implements Component{

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

//데코레이터 객체
@Slf4j
public class MessageDecorator implements Component{

    private RealComponent target;

    public MessageDecorator(RealComponent target) {
        this.target = target;
    }

    @Override
    public String operation() {
        log.info("MessageDecorator 실행");
        String result = target.operation();
        String decoResult = "*****" + result + "*****";
        log.info("적용 전 = {}, 적용 후 = {}", result, decoResult);
        return decoResult;
    }
}


//Client 객체
@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);
    }
}

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

// 결과
17:48:59.170 [main] INFO MessageDecorator - MessageDecorator 실행
17:48:59.173 [main] INFO RealComponent - RealComponent 실행
17:48:59.183 [main] INFO MessageDecorator - 적용 전 = data, 적용 후 = *****data*****
17:48:59.185 [main] INFO DecoratorPatternClient - result = *****data*****

프록시 패턴과 차이점은 목적이 접근 제어냐 부가 기능 추가냐의 차이가 있고 형태는 동일하다.

🖊 인터페이스는 필수인가?

상속을 사용해도 다형성에 의해 프록시 객체를 사용할 수 있다.
다만, 상속에 따른 제약조건이 따라온다

// 실제 Server 객체
@Slf4j
public class ConcreteLogic {

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

// 프록시 객체
@Slf4j
public class TimeProxy extends ConcreteLogic{

    private ConcreteLogic concreteLogic;

    public TimeProxy(ConcreteLogic concreteLogic) {
        this.concreteLogic = concreteLogic;
    }

    @Override
    public String operation() {
        log.info("TimeDecorator 실행");
        long startTime = System.currentTimeMillis();
        String result = concreteLogic.operation();
        long endTime = System.currentTimeMillis();
        log.info("TimeDecorator 종료 ResultTime = {}ms", endTime-startTime);
        return result;
    }
}

// Client 객체
@Slf4j
public class ConcreteClient {

    private ConcreteLogic concreteLogic;

    public ConcreteClient(ConcreteLogic concreteLogic) {
        this.concreteLogic = concreteLogic;
    }

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


// Test
@Test
public void addProxy() throws Exception{
    ConcreteLogic concreteLogic = new ConcreteLogic();
    TimeProxy timeProxy = new TimeProxy(concreteLogic);
    ConcreteClient concreteClient = new ConcreteClient(timeProxy);

    concreteClient.execute();
}

// 결과
18:55:27.366 [main] INFO TimeProxy - TimeDecorator 실행
18:55:27.370 [main] INFO ConcreteLogic - ConcreteLogic 실행
18:55:27.371 [main] INFO TimeProxy - TimeDecorator 종료 ResultTime = 0ms

🖊 문제점

기존 코드를 건드리지 않고 원하는 기능을 추가할 수 있는 것은 큰 장점이다.
그러나 프록시를 적용할 클래스를 하나하나 전부 만들어줘야 하는 것은 문제점이다.
이러한 점을 개선한 것으로 동적 프록시 기술이 있다.
다음 포스트에 소개하겠습니다.

1개의 댓글

comment-user-thumbnail
2021년 12월 15일

잘 보고갑니다!

답글 달기