본문 바로가기
아키텍쳐

프록시 & 프록시 패턴

by sangyunpark99 2025. 3. 5.
프록시 패턴이 뭘까요?

 

 

이번 글은 프록시와 프록시 패턴에 대해 정리한 글입니다.

참고 강의 : 김영한의 스프링 핵심 원리 - 고급편

 

프록시

 

프록시를 이해하기 위해 클라이언트와 서버를 먼저 보겠습니다.

보통 클라이언트와 서버는 아래 그림과 같은 흐름을 가집니다.

클라이언트와 서버

클라이언트와 서버의 관계는 클라이언트에서 필요한 부분을 요청하면, 서버는 클라이언트가 요청한 부분을 처리하는 것입니다..

이 개념은 단순히 '서버 컴퓨터'에만 국한되지 않습니다. 훨씬 더 넓은 범위에서 사용되는 개념입니다.

 

예를 들어, 웹 브라우저와 웹 서버의 관계도 클라이언트-서버 관계로 볼 수 있습니다.
이 개념을 객체 단위로 확장하면, 요청을 보내는 객체는 클라이언트, 그 요청을 처리하는 객체는 서버로 볼 수 있습니다.

 

위에 나온 그림처럼 호출하는 것을 직접적으로 호출한다고해서 직접 호출이라고 합니다.

직접 호출은 일반적으로 클라이언트가 서버를 호출하고, 처리 결과를 직접 받는 구조입니다.

 

그렇다면, 간접 호출은 어떻게 되는 걸까요?

 

간접 호출을 하기 위해서 Proxy 객체를 사용합니다. 그림으로 나타내면 다음과 같습니다.

Proxy를 사용한 간접 호출

클라이언트가 직접 서버를 호출하던 직접 호출과는 달리, Proxy(대리자)를 통해서 간접적으로 서버에 요청을 합니다.

 

프록시는 왜 사용할까요?

 

프록시 주요 기능

프록시를 통해서 할 수 있는 기능은 접근 제어, 부가 기능 추가로 크게 2가지로 존재합니다.

 

접근 제어의 세부 기능 3가지

  • 권한에 따른 접근을 차단할 수 있습니다.
  • 캐싱이 가능합니다.
  • 지연 로딩이 가능합니다.

캐싱과 지연 로딩 어디서 많이 들어보지 않았나요? JPA에서도 프록시를 사용해서 캐싱과 지연 로딩을 해줍니다.

 

부가 기능 추가

  • 원래 서버가 제공하는 기능에 추가로 부가 기능을 수행합니다.
  • ex) 요청 및 응답 값 변형, 실행 시간 측정해서 로그 남기기

 

그렇다면, 프록시 패턴은 무엇일까요?

 

프록시 패턴

GOF 디자인 패턴에서 프록시를 사용하는 패턴은 프록시 패턴과 데코레이터 패턴이 존재합니다.

이 두 패턴은 프록시를 사용한다는 점에선 동일하지만, 서로 목적이 다릅니다.

 

프록시 패턴은 접근을 제어하기 위한 목적으로 사용을 하고, 데코레이터 패턴은 새로운 부가 기능을 추가하려는 목적으로 사용하게 됩니다.

 

프록시 패턴을 코드로 어떻게 적용할 수 있을까요?

 

간단한 예제 코드를 통해서 프록시 패턴을 알아보겠습니다.

예제에 사용될 클래스 의존 관계는 다음과 같습니다.

Client 클래스는 Subject 인터페이스에 의존하고 있고, RealSubject는 Subject 인터페이스의 구현체 입니다.

 

런타임시 객체 의존 관계는 다음과 같습니다.

이 흐름을 코드로 보겠습니다.

 

Subject 인터페이스

public interface Subject {
    String operation();
}

 

Subject 인터페이스의 구현체인 realSubject

import lombok.extern.slf4j.Slf4j;

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

 

Subject 인터페이스에 의존하는 Client 클래스

 

public class Client {

    private Subject subject; // Subject 인터페이스에 의존

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

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

 

Client 클래스로 호출하는 RealSubject의 operation() 메서드

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

    client.execute();
    client.execute();
    client.execute();
}

 

RealSubject 클래스를 생성해서 Client의 Subject 인터페이스에 생성자 주입을 해줍니다.

RealSubject는 Subject의 구현체이므로 다형성으로 인해 생성자 주입이 가능합니다.

 

테스트 코드를 수행하면 Thread.sleep(1000)으로 인해 3초의 시간이 걸리게 됩니다.

 

실제 객체 호출이 3번 발생합니다.

프록시 패턴을 적용하면 어떻게 될까요?

 

프록시 패턴을 적용한 클래스 의존 관계는 다음과 같습니다.

프록시 패턴을 적용한 클래스 의존 관계

 

프록시 패턴 사용시 런타임 객체 의존 관계는 다음과 같습니다.

프록시 패턴을 적용한 객체 의존 관계

 

 

프록시 객체가 하나 추가되었고, 이는 Subject 인터페이스의 구현체 입니다.

@Slf4j
public class Proxy implements Subject {

    private Subject target;
    private String cacheValue;

    public Proxy(Subject subject) {
        this.target = subject;
    }

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

        if(cacheValue == null) {  // 캐싱
            cacheValue = target.operation(); // cacheValue에 값이 없는 경우 실제 객체를 호출
        }

        return cacheValue;
    }
}

 

 

프록시 패턴 테스트 코드

@Test
void proxyTest() {
    RealSubject realSubject = new RealSubject();
    Proxy proxy = new Proxy(realSubject);
    Client client = new Client(proxy);

    client.execute();
    client.execute();
    client.execute();
}

 

이 흐름을 조금 더 코드적으로 접근 해보겠습니다.

 

client.execute();

 

client.execute()를 호출하면, subject.operation() 로직이 실행됩니다.

 

subject는 생성자 주입을 통해 초기화가 되고 있습니다.

Client 클래스의 인스턴스를 생성할때, 생성자로 주입되는 객체는 proxy 객체 입니다.

Proxy proxy = new Proxy(realSubject);

 

 

subject.operation()의 로직은 곧 proxy.operation()이 됩니다.

 

proxy.operation()은 target.operation()을 호출합니다.

Proxy 클래스에서 target 객체는 생성자 주입을 통해서 초기화 되고 있습니다.

RealSubject realSubject = new RealSubject();
Proxy proxy = new Proxy(realSubject);

 

Proxy 클래스의 인스턴스 생성시 생성자에 RealSubject의 인스턴스를 주입해주고 있습니다.

따라서, target 객체는 realSubject를 의미합니다. traget.operation()은 realsubject.operation()과 동일합니다.

 

 

테스트 코드를 실행하면 나오는 결과는 다음과 같습니다.

 

테스트 결과는 1초 30ms가 걸렸습니다.

 

프록시 호출이 3번 발생하지만, 실제 객체는 1번 밖에 호출되지 않습니다.

 

프록시 패턴을 사용하기 이전의 테스트 코드와 이후의 테스트 코드의 결과가 왜 다를까요?

 

프록시 패턴 사용전
프록시 패턴 사용전

 

 

프록시 패턴 사용후
프록시 패턴 사용후

 

이는 Proxy클래스의 코드로 인해 발생하는 차이점입니다.

Proxy 클래스에 정의된 operation 메서드는 다음과 같습니다.

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

    if(cacheValue == null) {  // 캐싱
        cacheValue = target.operation(); // cacheValue에 값이 없는 경우 실제 객체를 호출
    }

    return cacheValue;
}

 

operation 메서드는 아래와 같은 조건문을 사용해서, cacheValue가 null인경우 즉 한번도 target.operation()이 호출되지 않은 경우에만 target.operation()을 호출해 줍니다. 즉, 1번만 호출하고 캐싱을 해두는 것입니다.

if(cacheValue == null) {  // 캐싱
    cacheValue = target.operation(); // cacheValue에 값이 없는 경우 실제 객체를 호출
}

 

프록시 패턴 사용후


프록시 객체가 처음 호출 될 때는 cacheValue가 null이기 때문에 target.operation()을 호출한 뒤 cacheValue를 초기화 해줍니다.

그런 다음에 프록시 객체가 다시 호출 되는 경우 target.operation()을 호출하지 않고 cacheValue를 바로 return 해줍니다.

 

흐름을 간단하게 정리하면 다음과 같습니다.

  1.  client의 proxy 객체 호출하고 cacheValue의 값이 null이기 때문에, realSubject를 호출한 후, 결과를 cacheValue에 저장합니다.(1초)
  2. client의 proxy 객체 호출시, 이미 cacheValue의 값이 초기화 되었기 때문에, 즉시 값을 반환합니다. (0초)
  3. 2번 과정과 동일합니다.(0초)

이는 프록시 패턴에서 프록시를 사용해서 캐싱을 해주는 역할을 간단하게 구현한 코드입니다.

 

 

 

정리

  • 프록시 패턴은 클라이언트와 서버 사이에 대리 객체를 두어, 접근 제어와 부가 기능 추가를 가능하게 하는 디자인 패턴입니다.
  • 프록시 패턴은 접근을 제어하기 위한 목적으로 사용을 하고, 데코레이터 패턴은 새로운 부가 기능을 추가하려는 목적으로 사용하게 됩니다.

'아키텍쳐' 카테고리의 다른 글

Saga Pattern(사가 패턴)  (0) 2025.04.11
DDD(도메인 주도 설계)  (0) 2025.03.24
데코레이터 패턴  (0) 2025.03.06
API 멱등성  (0) 2025.02.02
VO(Value Obejct)  (0) 2025.01.31