김영한님의 스프링 핵심원리 고급편 강의를 들으면서 학습한 내용을 정리한 글입니다.

 

 

 

프록시(Proxy)란 어떤 것을 직접 호출하지 않고 대리자를 통해 간접적으로 호출할 때 대리자에 해당하는 개념입니다. 네트워크에서는 보안상의 문제를 해결하기 위해 클라이언트가 직접 서버에 접근하지 않고 프록시 서버를 거쳐 접근하곤 합니다.

 

직접 호출
클라이언트와 서버 개념에서 클라이언트가 서버를 직접 호출하고, 처리 결과를 직접 받는 것

 

간접 호출
클라이언트가 요청을 서버에 직접 하는 것이 아니라, 대리자를 통해서 간접적으로 서버에 요청하는 것. 이때 대리자를 프록시라 합니다.

 

프록시의 주요 기능

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

프록시 패턴


https://javadevcentral.com/proxy-pattern-vs-decorator-pattern

프록시 패턴은 실제객체(Service)를 Proxy 객체가 감싸고 있는 형태로 실제 객체에 대한 접근제어를 프록시가 진행하게 됩니다.

 

프록시 캐시 적용 전

프록시를 적용하기 전에는 클라이언트가 실제 객체(RealSubject)를 직접 호출 하여 데이터("data")를 가져오는 형태로 작성하였습니다. 매번 데이터를 직접 접근하여 가져오게 되기 때문에 접근횟수만큼 지연시간이 길어진다는 단점이 있습니다.

 

> 접근 과정 : Client -> ChacheProxy -> RealSubject

 

RealSubject.java

@Slf4j
public class RealSubject implements Subject {

  @Override
  public String operation() {
    log.info("실제 객체 호출");
    sleep(1000); //DB 접근 지연시간
    return "data";
  }

  private void sleep(int millis) {
    try {
      Thread.sleep(millis);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

}
16:55:44.350 [Test worker] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출
16:55:45.358 [Test worker] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출
16:55:46.365 [Test worker] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출

 

프록시 캐시 적용 후

프록시 패턴을 통해 캐시를 적용하였습니다. 한번 DB에 접근한 후에는 프록시 객체의 메모리 캐시에 데이터를 저장해 두고 이후 호출에서는 DB를 호출하지 않고 캐시의 데이터를 반환하도록 설계하였습니다


> 접근 과정 : Client -> ChacheProxy -> RealSubject

 

ChachProxy.java

@Slf4j
public class CacheProxy implements Subject {

  private Subject subject;
  private String cache; //메모리 캐시

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

  @Override
  public String operation() {
    log.info(“프록시 객체 호출”);
    if(cache == null) {
      return cache = subject.operation();
    }
    return cache;
  }

}
16:55:43.312 [Test worker] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 객체 호출
16:55:43.319 [Test worker] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출
16:55:44.324 [Test worker] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 객체 호출
16:55:44.325 [Test worker] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 객체 호출

실제 객체의 호출은 한번만 이루어지고 이후에는 프록시 객체만을 호출하는 형태로 DB의 호출 횟수를 줄일 수 있었습니다. 실제로 JPA에서는 영속성 컨텍스트를 통해 1차캐시기능을 제공하여 DB접근 병목을 해결합니다. 

 

 

데코레이터 패턴


https://refactoring.guru/design-patterns/decorator

데코레이터 패턴은 상황과 용도에 다라 객체에 부가기능을 동적으로 부여하기 위해 사용됩니다.

 

데코레이터 패턴 적용

데코레이터 패턴을 적용하여 실제 객체(RealComponent)에 부가기능을 적용해 보았습니다.

 

> 접근 과정 : Client -> TimeDecorator -> MessageDecorator -> RealComponent

 

RealComponent.java

@Slf4j
public class RealComponent implements Component {

  @Override
  public String operation() {
    log.info(“RealComponent 실행”);
    sleep(1000);
    return “data”;
  }

  private void sleep(int millis) {
    try {
      Thread.sleep(millis);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }
}

MssageDecorator.java

@Slf4j
public class MessageDecorator implements Component {

  private Component component;

  public MessageDecorator(Component component) {
    this.component = component;
  }

  @Override
  public String operation() {
    log.info("MessageDecorator 실행");
    return "****" + component.operation() + "****";
  }
}

 

프록시 패턴과 데코레이터 패턴의 차이


둘 다 프록시를 사용하는 방식이지만, 의도에 따라 프록시패턴과 데코레이터 패턴으로 구분합니다.

  • 프록시 패턴
    • 접근 제어
    • 구성 관계(Composition)
    • 프록시 객체와 실제 객체의 관계가 컴파일 타임에 정해진다.
  • 데코레이터 패턴
    • 부가기능 추가
    • 집합 관계(Aggregation)
    • 프록시 객체와 실제 객체의 관계가 런타임에 정해진다.

 

구체 클래스 기반 프록시(상속)


@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("TimeProxy 실행");

    long start = System.currentTimeMillis();
    var result = concreteLogic.operation();
    long end = System.currentTimeMillis();
    long resultTime = end - start;

    log.info("TimeProxy 종료, Running time={}", resultTime);
    return result;
  }

}

 

위의 예제에 부모 클래스에 final 키워드가 붙은 필드가 추가된다면 부모 클래스의 생성자를 호출해야 하기 때문에 파라미터를 넣어서 super(..) 호출이 강제되게 됩니다. 또한 클래스나 메서드에 final 키워드가 붙는 경우 상속이 제한되기 때문에 클래스 기반의 프록시를 사용할 수 없습니다.

 

인터페이스 기반 프록시(합성)


public interface Subject {

  String operation();

}
@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) {
      throw new RuntimeException(e);
    }
  }

}
@Slf4j
public class CacheProxy implements Subject{

  private Subject subject;
  private String cache;

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

  @Override
  public String operation() {
    log.info("프록시 객체 호출");
    if(cache == null) {
      return cache = subject.operation();
    }
    return cache;
  }

}

인터페이스 기반의 프록시는 상속이라는 제약에서 자유롭고, 역할과 구현을 명확하게 나눌 수 있다는 점에서 장점이 있습니다.

 

하지만 인터페이스가 필요하다는 단점이 있습니다. 인터페이스의 도입은 구현이 변경될 가능성이 있을 때 효과적인데, 구현이 변경될 가능성이 없는 코드에 항상 인터페이스를 도입하는 것은 번거로울 수 있습니다.

 

Reference


'CS' 카테고리의 다른 글

[Network] 웹소켓(Websocket)  (0) 2022.10.09
[OS] 데드락의 탐지와 해결방법  (1) 2022.10.03
[Design Pattern] Facade 패턴을 통한 SRP원칙 준수  (0) 2022.08.22
[Design Pattern] Singleton Pattern  (0) 2022.08.22
[OS] Scheduler  (0) 2022.08.22

+ Recent posts