프록시

바그다드·2023년 8월 17일
0

지난 포스팅까지 템플릿 콜백 패턴을 통해 반복되는 코드를 줄이고, SRP를 지킬며 로그 추적기라는 부가 기능을 추가할 수 있게 되었다. 그 결과 인터페이스 + 람다식의 조합으로 한줄의 코드로 기존 로직과 부가 로직을 처리할 수 있었다.

하지만 아직 문제가 남아있는데, 아무리 코드를 줄였다고 해도 결국 기존의 코드를 수정해야 한다는 것에는 변함이 없다.
로그 추적기를 적용하는 클래스가 수십, 수백개라면 그 모든 클래스를 하나하나 수정해야 하는것이다.

이런 문제를 해결할 수 있는 방안이 바로 프록시다.

프록시

프록시는 대리자라는 뜻으로 클라이언트가 서버에 직접 요청을 하는 것이 아니라, 클라이언트는 대리자를 통해 서버로 간접적인 요청을 할 수 있다. 여기서 클라이언트와 서버를 보통 사용자와 웹 서버로 생각하는데, 이를 객체 관점에서 본다면, 클라이언트는 호출하는 객체, 서버는 호출에 응답하는 객체가 될 수 있다.
또 하나의 프록시뿐 아니라 필요한 다른 기능을 수행하는 프록시를 호출할 수도 있다. 이를 프록시 체인이라고 한다.

프록시는 2개의 주요 기능을 한다.

  1. 접근 제어
    • 권한에 따라 접근을 차단
    • 캐싱
    • 지연 로딩
  2. 부가기능 추가
    • 원래 서버에서 제공하는 기능에 더해 부가 기능을 수행
    • 요청 값이나, 응답 값을 중간에 변형하는 등
  • 프록시는 대체 가능해야 한다.

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

프록시 의존 관계


클라이언트는 서버를 요청한 것인지, 프록시를 요청한 것인지 몰라야 하므로, 인터페이스로 클래스를 묶고 있다.
덕분에 서버 객체를 프록시 객체로 변경하더라도 클라이언트 코드는 변경하지 않아도 된다.

여기서 주의해야할 점은 프록시는 부가기능이나 접근 제어를 하는 객체이므로 실제 기능을 처리하는 서버 객체를 프록시가 알고 있어야 한다. 따라서 런타임 의존 관계는 다음과 같다.

GOF 디자인 패턴에서는 프록시를 사용하는 패턴을 의도에 따라 2가지로 나눴는데, 다음과 같다.

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

둘 다 같은 프록시를 사용하지만, 의도에 따라 나뉜다는 것을 명심하자.
그럼 코드로 확인해보자.

1. 프록시 패턴

  • 프록시 패턴은 접근 제어를 처리할 목적으로 적용하는 패턴이다.

1. 인터페이스 생성

public interface Subject {
    String operation();
}

2. 프록시 생성

@Slf4j
public class CacheProxy implements Subject{
	
    // 다음 프록시 또는 타겟(실제 객체)
    private Subject subject;
    private String cacheValue;

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

    @Override
    public String operation() {
        log.info("프록시 호출");
        if (cacheValue == null) {
            cacheValue = subject.operation();
        }
        return null;
    }
}
  • Subject를 구현하고 있다.

3. 서버 코드 생성

@Slf4j
public class RealSubject implements Subject{
    @Override
    public String operation() {
        log.info("실제 객체 호출");
        sleep(1000);
        return "data";
    }

    private void sleep(int millis) {
        // 1초 지연 코드
    }
}
  • Subject를 구현하고 있다.

4. 클라이언트 코드 생성

public class ProxyPatternClient {

    private Subject subject;

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

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

5. 테스트 코드 생성

    @Test
    void cacheProxyTest() {
        RealSubject realSubject = new RealSubject();
        CacheProxy cacheProxy = new CacheProxy(realSubject);
        ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
        long start = System.currentTimeMillis();
        client.extecute();
        client.extecute();
        client.extecute();
        long end = System.currentTimeMillis() - start;
        long result = end - start;
        log.info("총 소요 시간 ={}",result);
    }
  • ProxyPatternClient -> cacheProxy -> realSubject
    위와 같은 의존 관계가 성립이 되고, 이는 위에서 봤던 그림과 같은 의존 관계를 가지는 것을 확인할 수 있다.

결과를 보면

실제 객체는 최초 한번만 실행이 되고, 그 이후에는 프록시만 호출되는 것을 확인할 수 있다.

2. 데코레이터 패턴과 프록시 체인

  • 데코레이터 패턴은 부가 기능을 수행하는데 목적을 둔다.

1. 인터페이스 생성

public interface Component {
    String operation();
}

2. 프록시1 생성

  • 메서드의 소요 시간을 측정
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={}",resultTime);
        return result;
    }
}

3. 프록시2 생성

  • 응답값을 꾸미는 기능
@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;
    }
}

4. 서버 코드 생성

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

5. 클라이언트 코드 생성

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

6. 테스트 코드 생성

    @Test
    void decorator2() {
        Component realComponent = new RealComponent();
        Component messageDecorator = new MessageDecorator(realComponent);
        TimeDecorator timeDecorator = new TimeDecorator(messageDecorator);
        DecoratorPatternClient client = new DecoratorPatternClient(timeDecorator);
        client.execute();
    }
  • DecoratorPatternClient -> TimeDecorator(프록시1) -> MessageDecorator(프록시2) -> RealComponent
    2개의 프록시가 각각의 부가 기능을 수행하는 프록시 체인이 형성되어 있다.
    결과를 보면

    위에서 형성된 의존 관계대로 프록시가 호출되고 각 부가기능이 수행된 것을 확인할 수 있다.

이번 포스팅에서는 프록시에 대해서 알아보았다.
프록시를 활용한 패턴에는 프록시 패턴, 데코레이터 패턴이 있으며, 이 두 패턴은 의도에 따라 나뉜다고 하였다.

  1. 프록시 객체가 타겟 객체를 주입 받은 상태에서 클라이언트가 서버를 호출한다.
  2. 서버에서는 타겟 객체를 반환하는게 아니라 프록시 객체를 호출한다.
  3. 프록시 객체는 부가 기능을 수행하고, 타겟 객체를 호출한다.
  4. 타겟 객체는 핵심 기능을 수행하고, 결과를 반환한다.
  5. 프록시 객체는 나머지 기능을 수행하고 결과를 반환한다.

여기서 중요한 부분은 클라이언트가 프록시 객체를 호출한 것인지, 서버 객체를 호출한 것인지 모른다는 것이다.
프록시와 타겟 모두 하나의 인터페이스를 구현하고 있기 때문에 언제는 프록시와 타겟은 변환이 가능하다.
또한 프록시의 강점은 핵심 로직은 전혀 변경하지 않고 부가 기능을 추가할 수 있다는 것이다.
그럼 이 프록시를 이용하여 로그 추적기를 적용한 코드가 어떻게 수정되는지, 다음 포스팅에서 확인해보자.

출처 : 김영한 - 스프링 핵심 원리 고급편

profile
꾸준히 하자!

0개의 댓글