Proxy Pattern

종원유·2022년 3월 13일
1

Java

목록 보기
9/11
post-thumbnail

프록시 패턴

프록시

  • 클라이언트의 요청과 서버의 응답 사이에서 요청과 응답에 대한 대리자로써 역할을 하는 것.
  • 요청과 응답 사이에서 대리인으로써 서로 직접 요청, 응답하지 않고 프록시를 통하여 요청, 응답한다.

GoF 디자인 패턴
GoF 디자인 패턴에서 프록시 패턴과 데코레이터 패턴은 모두 프록시를 사용하는 방법이다.
GoF 디자인 패턴에서는 이 둘을 의도(Intent)에 따라서 프록시 패턴과 데코레이터 패턴으로 구분한다.

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

둘다 프록시를 사용하지만, 의도가 다르다는 점이 핵심이다. 용어가 프록시 패턴이라고 해서 이 패턴만 프록시를 사용하는 것은 아니다.

직접 호출과 간접 호출

직접 호출

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

간접 호출

  • 간접 호출
    클라이언트가 요청한 결과를 서버에 직접 요청하는 것이 아니라 어떤 대리자(proxy)를 통해서 대신 간접적으로 서버에 요청할 수 있다.
    ex> 직접 장을 볼 수 있지만, 누군가에게 대신 장을 봐달라고 부탁할 수 있다. 여기서 대신 장을 보는 대리자를 proxy라 할 수 있다.

직접 호출과 다르게 간접 호출을 하면 대리자가 중간에서 여려가지 일을 할 수 있다.

프록시의 여러 역할

  • 접근 제어, 캐싱
    • 권한에 따른 접근 차단
      • 권한이 있는 지 확인하여 서버로 요청
    • 캐싱
      • 프록시에 데이터가 있을 경우 서버로 가지 않고 반환
    • 지연 로딩
      • 클라이언트가 프록시를 사용하고 있다가 실제 데이터 사용 요청이 있을 때, 서버에 요청해서 가져온다.
  • 부가 기능 추가
    • 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
    • 예 ) 요청 값이나, 응답 값을 중간에 변형한다.
    • 예 ) 실행 시간을 측정해서 추가로 로그를 남긴다.
  • 프록시 체인
    • 프록시에서 또 다른 프록시를 호출

요약하면 프록시 객체로 중간에서 접근 제어와 부가 기능 추가를 수행할 수 있다.

프록시 동작 구조

  • 객체에서 프록시가 되려면, 클라이언트는 서버에 요청한 것인지, 프록시에 요청한 것인지 몰라야한다.
  • 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야 한다.
  • 이처럼 서버 객체를 대체하기 위해 프록시는 서버와 같은 인터페이스를 사용해야 한다.

위 사진처럼 Client는 ServerInterface만 의존하여, 구현 클래스와는 의존성을 분리하여 사용한다.
또, 서버와 프록시는 같은 인터페이스를 사용하기 때문에, DI를 사용하여 대체 가능하다.

//ServerInterface
public interface Subject {
    String operation();
}

//Server
@Slf4j
public class RealSubject implements Subject{


    @Override
    public String operation() {
        log.info("실제 객체 호출");
        sleep(1000);    //데이터 처리 1초 가정
        return "data";
    }

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

//Client
public class ProxyPatternClient {

    private Subject subject;

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

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

//Proxy
@Slf4j
public class CacheProxy implements Subject{

    private Subject target;     //프록시가 호출되는 대상
    private String cacheValue;

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

    /**
     * 클라이언트가 프록시를 호출하면 프록시가 최종적으로 실제 객체를 호출해야 한다.
     * 따라서 내부에 실제 객체의 참조를 가지고 있어야 한다.
     * 이렇게 프록시가 호출하는 대상을 target이라 한다.
     *
     * 처음 호출 이후에 매우 빠르게 호출할 수 있음.
     * @return
     */
    @Override
    public String operation() {
        log.info("프록시 호출");
        if ( cacheValue == null )   //cacheValue에 값이 없으면 실제 객체 호출
            cacheValue = target.operation();
        return cacheValue;
    }
}

위 코드에서 Subject라는 인터페이스를 구현하여 서버 객체, 프록시 객체를 구현하여 사용하고 있다.
클라이언트는 Subject만 의존하기 때문에, 서버 객체인지 프록시 객체인지 알 필요가 없기에

  • 클라이언트 -> 서버
  • 클라이언트 -> 프록시 -> 서버

런타임 시에 DI를 통해 Server에서 Proxy로 변경되어도 클라이언트에서는 변경이 전혀 일어나지 않는다.

캐시

위의 GoF 디자인 패턴 설명에서 얘기했듯이,
프록시 패턴의 주요 기능은 접근 제어이다.
캐시 도 접근 자체를 제어하는 기능 중 하나이다.
ex > 접근 시 데이터가 있을 경우 캐시 데이터 반환, 없을 시 서버 호출 후 반환

테스트

public class ProxyPattrernTest {

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

        //client 3번 호출, 3초 소요
        client.execute();
        client.execute();
        client.execute();

    }

    /**
     * Cache Proxy 사용
     * CacheProxy에서 데이터가 없을 경우 RealSubject를 사용하고,
     * 데이터가 있을 경우 Proxy에서 바로 반환
     */
    @Test
    void cacheProxyTest(){
        RealSubject realSubject = new RealSubject();
        CacheProxy cacheProxy = new CacheProxy(realSubject);
        ProxyPatternClient client = new ProxyPatternClient(cacheProxy);

        //client 3번 호출, 1초 소모
        client.execute();
        client.execute();
        client.execute();
    }
}

noProxyTest()

realSubject와 ProxyPatternClient를 생성하고 둘을 연결한다.
client -> Server 런타임 객체 의존 관계가 완성된다.


noProxyTest()의 경우 실제 서버를 3번 호출한다.
Server에서는 데이터 처리 1회당 1초의 시간을 소요하므로 3초의 시간이 소요된다.

cacheProxtTest()

  1. realSubject와 cacheProxy를 생성하고 둘을 연결한다.
    • cacheProxy가 realSubject를 참조하는 런타임 의존관계가 완성된다.
  2. client에 realSubject를 참조하는 cacheProxy를 주입한다.

이 과정을 통해서
client -> cacheProxy -> realSubject 런타임 객체 의존 관계가 완성된다.

cacheProxtTest()는 client.execute()를 총 3번 호출한다.
하지만 실제 realSubject를 호출하지 않고 cacheProxy를 호출한다.

처리
1. client의 cacheProxy 호출 -> cacheProxy에 데이터 없음 -> realSubject 호출
2. client의 cacheProxy 호출 -> cacheProxy에 데이터 있음 -> cacheProxy에서 즉시 반환
3. client의 cacheProxy 호출 -> cacheProxy에 데이터 있음 -> cacheProxy에서 즉시 반환
결과적으로 캐시 프록시를 도입함으로써 최초 1번만 실제 서버에 요청하고, 이 후에는 프록시에서 캐싱을 통해 데이터를 반환하므로 성능상 효율이 상당히 개선된다.

정리

프록시 패턴의 핵심

RealSubject 코드와 클라이언트 코드를 전혀 변경하지 않고, 프록시를 도입해서 접근을 제어한다.

  • 클라이언트 코드의 변경 없이 자유롭게 프록시를 넣고 뺄 수 있다.
  • 실제 클라이언트 입장에서는 인터페이스에 의존하여 프록시 객체가 주입되었는지 실제 객체가 주입되었는지 알지 못한다.
profile
개발자 호소인

0개의 댓글