<Spring> 프록시 패턴, 데코레이터 패턴

라모스·2022년 6월 21일
0

Spring☘️

목록 보기
14/18
post-thumbnail

GoF 디자인 패턴

GoF 디자인 패턴에선 프록시를 사용하는 패턴인 프록시 패턴과 데코레이터 패턴을 의도에 따라 구분한다.

  • 프록시 패턴: 접근 제어가 목적
  • 데코레이터 패턴: 새로운 기능 추가가 목적

둘 다 프록시를 사용하지만, 의도가 다르다는 점이 핵심이다. 용어가 프록시 패턴이라 해서 이 패턴만 프록시를 사용하는 것은 아니다.

프록시 패턴

직접 호출과 간접 호출


클라이언트와 서버 개념에서 일반적으로 클라이언트가 서버를 직접 호출하고, 처리 결과를 직접 받는다. 이를 직접 호출이라 한다.


클라이언트가 요청한 결과를 서버에 직접 요청하는 것이 아니라 어떤 대리자(Proxy)를 통해 대신 간접적으로 서버에 요청할 수 있다. 이를 간접 호출이라 한다.

프록시의 주요 기능

  • 접근 제어
    • 권한에 따른 접근 차단
    • 캐싱
    • 지연 로딩
  • 부가 기능 추가
    • 원래 서버가 제공하는 기능에 더해 부가 기능을 수행한다.
    • ex) 요청 값이나, 응답 값을 중간에 변형
    • ex) 실행 시간을 측정해서 추가 로그를 남긴다.

프록시 객체가 중간에 있다면 크게 접근 제어와 부가 기능 추가를 수행할 수 있다.

프록시 동작 구조

  • 객체에서 프록시가 되려면, 클라이언트는 서버에게 요청을 한 것인지, 프록시에게 요청을 한 것인지 조차 몰라야 한다.
  • 서버와 프록시는 같은 인터페이스를 사용해야 한다.
  • 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야 한다.

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

런타임(애플리케이션 실행 시점)에 클라이언트 객체에 DI를 사용해서 객체 의존관계를 변경해도 클라이언트 코드를 전혀 변경하지 않아도 된다. 클라이언트 입장에선 변경 사실조차 모른다. DI를 사용하면 클라이언트 코드의 변경 없이 유연하게 프록시를 주입할 수 있다.

  • client -> server 에서 client-> proxy 로 의존관계 변경

핵심은 다음과 같다.

  • Server 코드와 클라이언트 코드를 전혀 변경하지 않고, 프록시를 도입해서 접근 제어를 했다는 점이다.
  • 클라이언트 코드의 변경 없이 자유롭게 프록시를 넣고 뺄 수 있다.
  • 실제 클라이언트 입장에선 프록시 객체가 주입되었는지, 실제 객체가 주입되었는지 알지 못한다.

예제

public interface Subject {
    String operation();
}

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

@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);
        } cache (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ProxyPatternClient {
    private Subject subject;
    
    public ProxyPatternClient(Subject subject) {
        this.subject = subject;
    }
    
    public void execute() {
        subject.operation();
    }
}
@Test
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 - 프록시 호출

캐시 프록시를 사용하면 최초 한번만 1초가 걸리고, 나머지는 거의 즉시 반환하게 된다.

데코레이터 패턴

원래 서버가 제공하는 기능에 더해 부가 기능을 수행한다.

Decorator 기능에 일부 중복이 있다. 꾸며주는 역할을 하는 Decorator들은 스스로 존재할 수 없다. 항상 꾸며줄 대상이 있어야 한다. 따라서 내부의 호출 대상인 component를 가지고 잇어야 한다. 또한 component를 항상 호출해야 한다. 이 부분이 중복이다. 이런 중복을 제거하기 위해 component를 속성으로 가지고 있는 Decorator 라는 추상 클래스를 만드는 방법도 고민할 수 있다. 이렇게 하면 추가로 클래스 다이어그램에서 어떤 것이 실제 컴포넌트인지, 데코레이터인지 명확하게 구분할 수 있다.

예제

public interface Component {
    String operation();
}

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

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

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

@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
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 -> realComponent의 객체 의존관계를 설정하고, 실행한다.

결과는 다음과 같다.

TimeDecorator 실행
MessageDecorator 실행
RealComponent 실행
MessageDecorator 꾸미기 적용 전=data, 적용 후=*****data*****
TimeDecorator 종료 resultTime=7ms
result=*****data*****

References

profile
Step by step goes a long way.

0개의 댓글