개발놀이터

디자인패턴 (프록시 패턴, 데코레이터 패턴) 본문

Spring/Spring

디자인패턴 (프록시 패턴, 데코레이터 패턴)

마늘냄새폴폴 2022. 1. 17. 16:02

이 포스팅은 인프런 김영한 님의 스프링 핵심 원리 고급 편을 보고 각색한 포스팅입니다. 자세한 내용은 강의를 확인해주세요

 

저번 포스팅에서 우리는 템플릿 메서드 패턴, 전략 패턴에 대해서 알아보았다. https://coding-review.tistory.com/93

 

디자인패턴 (템플릿 메서드 패턴, 전략 패턴)

이 포스팅은 인프런 김영한 님의 스프링 핵심 원리 고급 편을 보고 각색한 포스팅입니다. 자세한 내용은 강의를 확인해주세요 오늘 알아볼 패턴들을 보기 전에 간단한 예시를 먼저 소개하겠다.

coding-review.tistory.com

 

앞서 배운 디자인 패턴들은 우리가 원했던 요구사항에 맞긴 하지만 코드를 직접 다 고쳐야 한다는 엄청난 부담감이 있다. 수백개면 수백개 수천개면 수천개의 코드를 일일히 다 고쳐야 한다는 것이다. 

 

기존 코드는 변경하지 않고 시간을 잴 수 있는 방법은 없을까?

 

 

방법이 있다. 바로 프록시를 사용하면 된다.

 

 

프럼 프록시에 대해서 알아보자

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

그런데 클라이언트가 요청한 결과를 서버에 직접 요청하는 것이 아니라 어떤 대리자를 통해서 대신 간접적으로 서버에 요청할 수 있다. 예를 들어서 내가 직접 마트에서 장을 볼 수도 있지만, 누군가에게 대신 장을 봐달라고 부탁할 수도 있는것처럼 말이다.

 

이해를 돕기위해 실생활속 예시를 몇개 들어 설명하겠다.

 

상황1) 엄마에게 라면을 사달라고 부탁했는데 엄마는 그 라면은 이미 집에 있다고 말했다. 그러면 기대한 것 보다 더 빨리 라면을 먹을 수 있을 것이다. (접근 제어, 캐싱)

 

상황2) 아버지께 자동차 주유를 부탁했는데 아버지가 주유 뿐만 아니라 세차까지 하고 왔다. 클라이언트가 기대한 것 외에 세차라는 부가 기능까지 얻게 되었다. (부가 기능 추가)

 

상황3) 대리자가 또다른 대리자를 부를 수도 있다. 예를 들어서 내가 동생에게 라면을 사달라고 했는데 동생이 엄마한테 다시 라면을 사달라고 요청할 수도 있다. 중요한 점은 클라이언트는 대리자를 통해서 요청했기 때문에 그 이후 과정은 모른다는 점이다. 동생을 통해서 라면이 나에게 도착하기만 하면 된다. (프록시 체인)

 

 

여기까지 듣고 보면 아무 객체나 프록시가 될 수 있는 것 같다.

 

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

 

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

 

이는 우리가 DI를 공부할 떄 중요하게 봤던 포인트 중 하나이다.

 

이제 프록시의 주요 기능에 대해 알아보자

 

프록시를 통해서 할 수 있는 일은 크게 두가지로 구분할 수 있다.

1. 접근제어

-권한에 따른 접근 차단

-캐싱

-지연 로딩

2. 부가 기능 추가

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

-예) 요청 값이나, 응답 값을 중간에 변형한다.

-예) 실행 시간을 측정해서 추가 로그를 남긴다.

 

GOF 디자인 패턴에서는 이 둘을 의도에 따라서 프록시 패턴과 데코레이터 패턴으로 구분한다. 

 

 

프록시 패턴부터 순서대로 구현해보자

 

Subject.java

public interface Subject {
    String operation();
}

이 예제의 근간이 되는 서버 인터페이스이다.

 

RealSubject.java

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

서버 인터페이스를 구현하는 구현체중 실제 객체이다. 실제 객체를 호출하는데 부하가 많이 걸리는 작업이라 가정하고 1초의 지연시간을 두었다. 

 

ProxyPatternClient.java

public class ProxyPatternClient {

    private Subject subject;

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

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

클라이언트 객체이다. 클라이언트는 알맞는 서버 인터페이스를 주입받아 execute를 실행할 수 있게 준비했다.

 

CacheProxy.java

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

클라이언트가 프록시를 호출하면 프록시가 최종적으로 실제 객체를 호출해야 한다. 따라서 내부에 실제 객체의 참조를 가지고 있어야 한다. 이렇게 프록시가 호출하는 대상을 target이라 한다.

 

그 아래 구현한 코드를 보면 cacheValue에 값이 없으면 실제 객체를 호출해서 값을 구한다. 그리고 구한 값을 cacheValue에 저장하고 반환한다. 만약 cacheValue에 값이 있으면 실제 객체를 전혀 호출하지 않고 캐시값을 그대로 반환한다. 따라서 처음 조회 이후에는 캐시에서 매우 빠르게 데이터를 조회할 수 있다. 

 

public class ProxyPatternTest {

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

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

첫번째 테스트는 서버 인터페이스를 구현한 실제 객체를 클라이언트에 주입받아 세번 실행하는 모습이다. 세번 실행하면 각각1초씩 3초가 걸리게 된다. 

 

두번째 테스트는 서버 인터페이스를 구현한 실제 객체를 프록시에 주입받고 클라이언트는 프록시 객체를 주입받아서 실행하는 모습이다. 이렇게 되면 클라이언트는 자신이 실제 객체를 주입받았는지 프록시를 주입받았는지조차 알 수 없이 실행한다. 

 

두번째 테스트를 실행해보면 다음과 같은 결과를 얻을 수 있다. 

순서상으로 보면 이렇게 된다.

 

1. 클라이언트는 프록시 객체를 주입받았다.

2. 프록시 객체는 실제 객체를 주입 받았다.

3. 클라이언트가 프록시 객체를 이용해 execute를 실행한다.

4. 그럼 프록시 객체가 소환되고 프록시 객체가 호출된다.

5. 프록시 객체는 cacheValue에 값이 없기 때문에 실제 객체를 호출해서 값을 채워 넣는다.

6. 두번째 호출부터는 프록시 객체에 값이 있기 때문에 있는 값을 그대로 반환한다.

 

 

 

이제 데코레이터 패턴으로 넘어가보자 

 

Component.java

public interface Component {

    String operation();
}

RealComponent.java

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

프록시 패턴과 마찬가지로 서버 인터페이스를 구현한 실제 객체이다. 단순히 로그를 남기고 data를 반환한다.

 

DecoratorPatternClient.java

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

컴포넌트를 주입받아 operation을 실행하는 코드이다. 

 

MessageDecorator.java

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

operation을 실행하고 나온 결괏값에 별을 찍어 메시지를 꾸며주는 역할을 한다.

 

TimeDecorator.java

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

        log.info("걸린 시간 = {}ms", endTime - startTime);

        return result;
    }
}

컴포넌트를 주입받아 opration을 실행하는데 시작과 끝에 시간을 측정하여 걸린시간을 측정하는 로직이 들어가있다.

 

DecoratorPatternTest.java

@Slf4j
public class DecoratorPatternTest {

    @Test
    void noDecorator() {
        RealComponent realComponent = new RealComponent();
        DecoratorPatternClient client = new DecoratorPatternClient(realComponent);
        client.execute();
    }

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

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

 

 

런타임 객체간 프록시 체인으로 연결되어 실행되는 것을 확인할 수 있다. 

 

 

지금까지 프록시를 사용해서 기존 코드를 변경하지 않고 여러가지 기능을 추가해봤다. 그런데 문제는 프록시 클래스를 너무 많이 만들어야 한다는 점이다. 대상 클래스가 100개라면 프록시 클래스도 100개를 만들어야한다. 이것도 만만치않게 힘들다. 프록시 클래스를 하나만 만들어서 모든 곳에 적용하는 방법은 없을까?

 

다음 포스팅에선 이 문제를 해결하기 위해 동적 프록시라는 기술에 대해 알아보겠다. 

'Spring > Spring' 카테고리의 다른 글

프록시 팩토리  (0) 2022.01.19
동적 프록시  (0) 2022.01.18
디자인패턴 (템플릿 메서드 패턴, 전략 패턴)  (0) 2022.01.13
동시성문제와 스레드 로컬  (0) 2022.01.10
스프링 MVC 구조 파악하기  (0) 2022.01.07