제육's 휘발성 코딩
article thumbnail
반응형

프록시란?

프록시(Proxy)란 대리의 의미를 갖고 있으며 서버와 클라이언트 사이에서 통신을 대신 처리해주는 역할을 한다.

프록시는 객체안에서의 개념(디자인 패턴), 웹 서버에서의 개념 (포워드 프록시, 리버스 프록시) 등으로 사용되며 근본적인 역할은 같다.

객체안에서의 프록시에 대해서 알아보자.

 

프록시 객체

프록시 객체는 서버와 클라이언트 사이에서 크게 2가지의 역할을 수행하고 있다.

GoF 디자인 패턴에는 이 둘을 의도(intent)에 따라 프록시 패턴, 데코레이터 패턴으로 구분한다.

  • 접근 제어 (권한, 캐싱, 지연 로딩 등) -> 프록시 패턴
  • 부가기능 (추가 기능 수행, 값을 변형, 로그 등) -> 데코레이터 패턴

 

클라이언트와 서버 (직접 호출)

image

  • 네트워크에서 클라이언트와 서버는 관점에 따라 다양하게 사용된다. (브라우저, 서버 등)
  • 클라이언트는 서버에 필요한 것을 요청하고, 서버는 요청에 대한 응답을 해준다.

 

클라이언트와 서버 (간접 호출)

image

  • 클라이언트가 요청한 내용을 중간에 대리자가 있고, 대리자가 서버에 전달한다. 여기서의 대리자가 프록시라고 불린다.

 

프록시가 될 수 있는 필수 조건

내용만 보면 아무 객체나 프록시가 될 수 있는 것처럼 보이지만 사실은 조건이 있다.

객체에서 프록시가 되려면 클라이언트는 요청 대상(프록시, 서버)을 몰라야한다.

즉, 서버와 프록시가 같은 인터페이스를 사용해야 한다. 아래 그림을 살펴보자.

image

  • 클라이언트의 요청 흐름을 보면, 인터페이스에만 의존하고 있다. 즉, DI를 통해서 구현체 대체가 가능하다.

DI를 통해서 의존 관계를 변경한다는 의미는 클라이언트의 대상이 서버에서, 런타임 시에 프록시로 변경할 수 있다는 것이다.

이 때 클라이언트의 입장에서는 변경 사실을 모르고 유연하게 주입하는 것을 의미한다.

 

프록시 패턴 - 예제

 

프록시 패턴 적용 전

프록시 패턴을 이해하기 위해, 프록시 패턴 도입 전 코드를 간단하게 만들어보자.

 
Subject - 인터페이스
public interface Subject {
  String operation();
}
 
RealSubject - Subject 인터페이스 구현체
@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();
      }
  }
}
  • Subject 인터페이스를 구현하여, 데이터를 1초마다 조회하는 시뮬레이션을 만들어보자.
 
ProxyPatternClient - 클라이언트
public class ProxyPatternClient {
  private Subject subject;

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

  public void execute() {
      subject.operation();
  }
}
  • 클라이언트는 인터페이스에 의존하고, 인터페이스에 해당하는 구현체를 주입받아 데이터를 조회하는 로직을 만들자.
 
ProxyPatternTest - 실제 객체 호출 테스트
public class ProxyPatternTest {
  @Test
  void noProxyTest() {
        RealSubject realSubject = new RealSubject();
      ProxyPatternClient client = new ProxyPatternClient(realSubject);
      client.execute();
      client.execute();
      client.execute();
  }
}
  • 테스트 코드를 작성해서 실행해보면, 1초에 한 번씩 3번의 데이터를 조회하게 된다.
  • 만약 데이터가 변하지 않는 불변의 데이터라면, 캐시를 적용하는 것이 성능 상 좋을 것이다. 프록시 객체를 사용하여 캐시를 적용해보자.

 

프록시 패턴 적용 후

이전에 프록시 설명에서 프록시를 적용하는 것을 클라이언트는 몰라야 된다고 했었다.

그렇게 구현하기 위해선 프록시와 서버가 같은 인터페이스를 의존해야함을 떠올리자.

프록시 패턴은 접근 제어 (권한, 캐싱, 지연 로딩 등)의 기능을 담당하고 있으니, 캐시를 적용하는 예제를 만들어보자.

 
CacheProxy - 프록시 객체
@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;
  }
}
  • 클라이언트가 프록시를 호출하면, 최종적으로 실제 객체를 호출해야 한다.
  • 최초로 접근한 조회라면 실제 객체의 참조 값을 넣어주고, 아니면 캐시 값을 반환하도록 구현하자.
 
ProxyPatternTest - 캐시 프록시 추가
@Test
void noProxyTest() {
    RealSubject realSubject = new RealSubject();
    ProxyPatternClient client = new ProxyPatternClient(realSubject); // 프록시 도입 전 비교
    client.execute();
    client.execute();
    client.execute();
}
@Test
void cacheProxyTest() {
    Subject cacheProxy = new CacheProxy(new RealSubject());
    ProxyPatternClient client = new ProxyPatternClient(cacheProxy); // 클라이언트 변경 없음!!
    client.execute();
    client.execute();
    client.execute();
}
  • 캐시프록시 객체를 생성하여 데이터 조회 로직을 실행해보자.
  • 클라이언트의 변경 없이 프록시를 주입했다는 것이 프록시 패턴의 핵심이다.

 

image

  • 최초 한번만 실제 객체를 호출하고 이후에는 캐싱된 값을 반환하는 것을 확인할 수 있다.

 

데코레이터 패턴 - 예제1

 

데코레이터 패턴 적용 전

Component 인터페이스
public interface Component {
  String operation();
}
  • 단순한 문자열 데이터를 가져오는 인터페이스
RealComponent - 인터페이스 구현체
@Slf4j
public class RealComponent implements Component{

  @Override
  public String operation() {
      log.info("RealComponent 실행");
      return "data";
  }
}
  • data 를 반환해주는 인터페이스 구현체
DecoratorPatternClient - 클라이언트
@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);
  }
}
  • String을 반환하는 인터페이스의 구현체를 주입받아, 구현체의 로직을 실행시키는 클라이언트
DecoratorPatternTest
public class DecoratorPatternTest {

  @Test
  void noDecorator() {
      Component realComponent = new RealComponent();
      DecoratorPatternClient client = new DecoratorPatternClient(realComponent);
      client.execute();
  }
}
  • 테스트를 실행시켜보면 별다른 문제 없이 data 를 출력한다. 이 예제를 기반으로 데코레이터 패턴을 적용해보자.

 

데코레이터 패턴 적용 후

image

  • 데코레이터 패턴은 프록시 기능에서 부가 기능을 수행한다. messageDecorator를 통해 응답 값을 꾸며주는 데코레이터 프록시를 만들어보자.
 
MessageDecorator - 데코레이터 프록시 역할
@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;
  }
}
 
DecoratorPatternTest - 데코레이터 프록시 추가
  @Test
  void decorator1() {
      Component realComponent = new RealComponent();
      Component messageDecorator = new MessageDecorator(realComponent);
      DecoratorPatternClient client = new DecoratorPatternClient(messageDecorator);
      client.execute();
  }
  • MessageDecorator 꾸미기 적용 전=data, 적용 후=*data* 처럼 응답 값을 꾸며서 반환해준다.
  • 이번에도 클라이언트의 수정없이 데코레이터 패턴이 적용되었음을 확인할 수 있다.

 

데코레이터 패턴 - 예제 2

image

  • 데코레이터 프록시(TimeDecorator)를 한 개 더 추가하여 시간 측정하는 프록시 기능도 만들어보자.
  • 데코레이터 프록시 (TimeDecorator, MessageDecorator)는 명백히 꾸밀 대상이 존재한다. 이 둘을 추상클래스로 분리하는 방법도 있다. 실제 컴포넌트인 RealComponent와 명확하게 구분도 지을 수 있다는 점을 참고하자.
 
TimeDecorator - 데코레이터 객체 생성
@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;
  }
}
  • 시작 시간과 종료 시간을 측정해주는 데코레이터 객체 생성
 
DecoratorPatternTest - 데코레이터 객체 추가
@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)를 주의해서 생성하자. 코드 상에선 역순으로 타고 간다.

image

  • 테스트를 실행해보면, 다음과 같이 정상적으로 실행되는 것을 확인할 수 있다.

 

정리

  • 프록시는 서버와 클라이언트 사이에서 통신을 대신 처리해주는 역할을 한다.
  • 프록시의 기능은 접근 제어, 부가 기능으로 의도를 구분하고 있으며 곧 프록시 패턴(접근 제어)과 데코레이터 패턴(부가 기능)으로 구분한다.

 


본 포스팅은 인프런 - 스프링 핵심 원리 -고급편을 참고하여 정리하였습니다.

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^