Spring 핵심원리 고급편 - Proxy 패턴

목차

Proxy 패턴 - 실제 객체에 대한 접근 제어를 위한 디자인 패턴

기능을 수행하는 실제 (Real) 객체 대신 가상의 (Proxy) 객체 를 이용해 실제 객체 에 대한 접근을 제어 하는 디자인 패턴

프록시 패턴은 객체 지향 프로그래밍에서 사용되는 디자인 패턴 중 하나로 실제 객체와 같은 인터페이스 를 제공하는 객체를 사용하여 실제 객체에 대한 접근을 제어하는 데 사용됩니다. 즉, 프록시 객체를 사용하여 실제 객체에 대한 액세스를 제어할 수 있습니다.

프록시 객체는 실제 객체에 대한 참조를 유지하고, 클라이언트가 실제 객체에 액세스하려고 할 때 대신 실제 객체에 대한 요청을 처리합니다. 프록시 객체는 실제 객체와 같은 인터페이스를 구현하므로, 클라이언트는 프록시 객체와 실제 객체를 동일한 방식으로 사용할 수 있습니다.

프록시 패턴의 사용 사례로는 다음과 같은 것들이 있습니다.

  • 원격 객체에 대한 액세스
    • 원격 객체에 대한 액세스를 제어하려면, 클라이언트는 원격 객체에 직접 액세스하지 않고, 원격 객체를 대신하여 프록시 객체를 사용합니다. 이때 프록시 객체는 원격 객체에 대한 액세스를 제어하고, 원격 객체에 대한 요청을 처리합니다.
  • 보안
    • 프록시 객체를 사용하여 실제 객체에 대한 액세스를 제어하면, 보안상 이점이 있습니다. 프록시 객체를 사용하면, 클라이언트는 실제 객체에 직접 액세스하지 않고, 프록시 객체를 통해 액세스하므로, 실제 객체에 대한 액세스를 제어하고 보안을 유지할 수 있습니다.
  • 비용
    • 실제 객체에 대한 액세스 비용이 높은 경우, 프록시 객체를 사용하여 액세스 비용을 줄일 수 있습니다. 예를 들어, 원격 객체에 대한 액세스는 네트워크 비용이 들기 때문에, 원격 객체에 직접 액세스하는 것보다 프록시 객체를 사용하여 원격 객체에 대한 액세스 비용을 줄일 수 있습니다.
  • 캐싱
    • 프록시 객체를 사용하여 실제 객체에 대한 액세스를 캐싱할 수 있습니다. 이때 프록시 객체는 실제 객체에 대한 요청을 처리하기 전에 캐시된 결과를 반환합니다. 이를 통해, 실제 객체에 대한 액세스 비용을 줄이고, 성능을 향상시킬 수 있습니다.

Proxy 패턴의 구성 요소

  • Subject
    • 실제 객체와 대리자 객체가 구현해야 하는 공통 인터페이스입니다. 이 인터페이스를 통해 실제 객체와 대리자 객체를 동일한 인터페이스를 갖도록 만들어줍니다.
  • Real Subject
    • 실제 객체를 나타냅니다. 이 객체는 Proxy 객체에서 대신하는 역할을 합니다.
  • Proxy
    • Real Subject를 대신해서 클라이언트로부터 요청을 받아 처리합니다. Real Subject에 대한 참조를 유지하고, 클라이언트에게 Real Subject와 동일한 인터페이스를 제공합니다. Proxy는 Real Subject에 대한 접근을 제어하고, 실제 객체를 생성하거나 초기화하는 역할을 합니다.

Proxy 의 주요 기능

Proxy 가 중간에 있으면 접근 제어부가 기능 추가 를 수행할 수 있다.

  • 객체의 접근 제어
    • 프로그램 로직 실행시 실제 객체 대신 Proxy 객체에 로직을 대신 맡기게 된다.
    • 권한에 따른 접근 차단
      • 권한별로 실제 객체에 접근을 제한하도록 한다.
    • Lazy-Loading
      • 실제 객체 생성을 해당 객체를 사용하기 전 시점까지 미룰 수 있는 장점이 있다.
    • Caching
      • DB 와 같이 요청이 길어지는 값을 미리 저장해 실제 객체에 대한 접근을 줄인다.
  • 부가 기능 추가
    • 요청 값, 응답 값을 중간에 변경
    • 추가 로그 남기기
  • Proxy Chain
    • Proxy 객체가 또 다른 Proxy 객체를 부를 수 있다.

Proxy Chain

Proxy 패턴의 의존 관계

Proxy 패턴은 실제 객체와 같은 인터페이스 를 사용한다.

Client 객체 입장에서는 Server 객체 실행시 Server 객체를 실행하든 Proxy 객체를 실행하든 동일하게 실행이 되야 하기 때문에 같은 인터페이스를 이용해 구현해야 한다.

인터페이스

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 ex){
ex.printStackTrace();
}
}
}

Client 코드

public class ProxyPatternClient {

private Subject subject;

// 생성자 주입을 통해 Subject 객체를 주입한다.
public ProxyPatternClient(Subject subject){
this.subject = subject;
}

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

비즈니스 로직 실행 및 결과

client 객체내 execute 메소드를 호출하게 되면 execute 내부 로직을 반복해서 3번 실행하는 것을 확인할 수 있다.

public class ProxyPatternTest {

@Test
void noProxyTest(){
RealSubject realSubject = new RealSubject();
ProxyPatternClient client = new ProxyPatternClient(realSubject);
client.execute();
client.execute();
client.execute();
}
}
00:03:29.052 [Test worker] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출
00:03:30.059 [Test worker] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출
00:03:31.062 [Test worker] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출

Proxy 패턴 적용 - 프록시 클래스 생성

Proxy 패턴을 적용해 RealSubject 객체 접근을 제어한다. 우선, RealSubject 를 만들때 사용한 Subject 인터페이스를 이용해 CacheProxy 클래스를 생성합니다.

CacheProxy 는 저장된 값이 없을 경우 RealSubject 객체 대한 접근해 데이터를 가져오고 저장된 값이 있을 경우 RealSubject 객체 접근을 생략하고 저장된 값을 반환하도록 한다.

@Slf4j
public class CacheProxy implements Subject{

// Proxy 객체 내 실제 객체를 저장
private Subject target;

// 실제 객체 실행 결과를 저장하기 위한 객체
private String cacheValue;

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

@Override
public String operation() {
log.info("프록시 호출");

// 값을 캐싱해 놓음으로써 매번 객체를 생성 및 DB 로부터 데이터를 불러오는 작업을 생략 한다.
// 1. 캐싱된 값이 없을 경우 실제 객체에 대한 접근이 이뤄진다.
// 2. 캐싱된 값이 있을 경우 실제 객체에 대한 접근이 이뤄지지 않는다.
if(cacheValue == null){
// 실제 객체 실행
cacheValue = target.operation();
}

return cacheValue;
}
}

비즈니스 로직 실행 및 결과

RealSubject 코드와 클라이언트 코드를 변경하지 않고 Proxy 객체를 통해 접근 제어 를 했다.

  • 처음에는 실제 객체를 실행해 실행 결과를 받고 두번째 부터는 캐싱한 결과 를 받는다.
  • 실제 객체 대신 Proxy 객체 주입 을 통해 런타임시 client -> cacheProxy -> realSubject 로 의존 관계가 형성된다.
@Test
void cacheProxyTest(){
RealSubject realSubject = new RealSubject();
// 실제 객체를 이용해 Proxy 객체 생성
CacheProxy cacheProxy = new CacheProxy(realSubject);
// Client 에 실제 객체 대신 Proxy 객체를 주입 Client 가 Proxy 객체를 실행하도록 한다.
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);

client.execute(); // 처음에는 실제 객체를 실행해 실행 결과를 받는다.
client.execute(); // 두번째 부터는 캐싱한 결과를 받는다.
client.execute();
}

실행 결과를 통해 Proxy 객체 CacheProxy 를 호출 후 실제 객체 RealSubject 를 호출한 것을 확인할 수 있다. 또한, 한번 호출 후 값이 캐싱되면 더 이상 실제 객체에 대한 접근이 이뤄지지 않는 것을 확인할 수 있다.

11:03:11.638 [Test worker] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 호출
11:03:11.640 [Test worker] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출
11:03:12.645 [Test worker] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 호출
11:03:12.645 [Test worker] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 호출
Share