CS study/java

[디자인 패턴]프록시(Proxy) 패턴 설명과 예시

블랑v 2024. 2. 26. 23:08

https://refactoring.guru/ko/design-patterns/proxy

 

프록시 패턴

/ 디자인 패턴들 / 구조 패턴 프록시 패턴 다음 이름으로도 불립니다: Proxy 의도 프록시는 다른 객체에 대한 대체 또는 자리표시자를 제공할 수 있는 구조 디자인 패턴입니다. 프록시는 원래 객체

refactoring.guru

개요

 

프록시 패턴은 구조 패턴의 하나로, 어떤 객체에 대한 접근을 제어하는 대리인(프록시) 객체를 두어 직접적인 접근을 방지하는 패턴이다.

 

이 패턴은 실제 서비스 객체의 참조를 감싸는 프록시 객체를 생성하여, 클라이언트가 서비스 객체에 접근할 때 프록시 객체를 통해 간접적으로 접근하게 만든다. 프록시 패턴은 접근 제어, 비용이 많이 드는 연산의 지연 실행, 원격 객체의 접근 등 다양한 상황에서 유용하게 사용된다.

 

 

원래 로직 과정이 이렇게 진행되었다면, (최대한 단순하게 가정해보자.)

 

1. 원래 요청하고자 하는 객체의 Proxy 객체를 만든다.

2. 이 참조 객체는 원래 객체의 역할을 대신하거나, 추가 로직을 작성하거나, 이를 변경하여 수행할 수 있다.

 

구체적으로 이것을 왜 쓰는지 알아보자.

 

사용 의도 : 왜 '객체의 참조'를 만들어서 간접적으로 접근하는지? 

 

객체에 대한 접근을 제어하고, 객체와 클라이언트 사이에 추가적인 기능을 제공하기 위해서이다.

 

이것이 어떤 이점이 있을까? 다음과 같은 이점이 존재한다.

1. 접근 제어

객체에 대한 접근을 제한하거나 특정 조건에서만 접근을 허용해야 할 때 유용하다.

 

예를 들어, 민감한 정보를 다루는 개인정보의 로직을 생각해보자.

이 경우 사용자의 인증 여부를 확인한 후에만 접근을 허용하고 싶을 수 있다.

 

프록시 객체는 이러한 인증 절차를 수행하고, 조건을 만족하는 경우에만 실제 객체에 대한 접근을 허용한다.

 

물론 직접적으로 인증 절차를 구현할 수 있지만, 프록시를 사용하는 주 이유는 관심사의 분리(separation of concerns) 때문이다.

 

예를 들어, 비즈니스 로직을 처리하는 클래스가 있다고 하자.

이 클래스는 특정 작업(로그인, 글쓰기 등..)을 수행하는 데 집중해야 하며, 인증 같은 보안 로직으로 인해 복잡해지지 않아야 한다. 프록시를 사용하면, 이 프록시가 인증 같은 보안 로직을 처리하고, 실제 클래스는 비즈니스 로직에만 집중할 수 있다.

 

 

이렇게 함으로써 각 클래스가 하나의 책임만을 가지게 되어 소프트웨어의 유지보수성과 확장성이 향상된다.

 

요약

여기서 프록시 객체는 원래 객체(delegate)를 가져오면서, 추가적으로 확장적인 로직을 구현하는 것이다.

즉, '아이디 확인', 'PW 확인' 등 추가적인 관심사를 분리하지만, 이것들은 전부 프록시가 아닌 원래 객체를 참조의 형태로 가지고 있고, 이것에 더해서 부가적인 기능을 추가하는 것이다 --에 가깝다.

 

2. 비용이 많이 드는 연산의 지연 실행

객체의 생성이나 호출이 많은 시간과 비용이 걸릴 때, 이를 지연시킬 수 있다.
지연 실행(Lazy Loading)은 실제로 필요할 때까지 객체의 생성을 미루는 기법을 의미한다.

 

프록시 객체는 클라이언트의 요청을 받고, 실제로 필요한 순간까지 객체의 생성이나 연산의 실행을 지연시킨다. 이를 통해 애플리케이션의 성능을 최적화할 수 있다.

 

예시를 들어 확인해보자.

 

1. '실제로 연산이 많이 드는 객체' 가 있다고 가정하자. 즉, 이를 빌드하기 위해서는 많은 비용이 발생한다.

2. 서버 입장에서는 이를 실제로 사용하는지 유무를 알 수 없다. 사용하지도 않을 것인데 만들어놓는 건 자원의 낭비 아닌가?

 

 

3. 그래서 지연 로딩을 사용하는 것이다. 즉, 실제 사용 전까지는 그냥 Proxy의 형태로 가짜로 생성만 하는 것이다.

이 과정에서 프록시 객체는 실제 객체와 동일한 인터페이스를 구현한다. 하지만 실제로는 내부적으로 실제 객체의 인스턴스를 생성하지 않고, 가볍게 유지한다.

 

 

4. 그리고 런타임이나 실제 호출 시에(즉, 이걸 진짜로 사용할 때) 다음과 같은 절차가 이루어진다.

 

4-1. 클라이언트가 프록시 객체를 통해 특정 연산을 요청할 때, 프록시 객체는 내부적으로 실제 객체가 이미 생성되었는지를 확인한다.

 

 

4-2. 만약 실제 객체가 아직 생성되지 않았다면, 그 순간 실제 객체를 생성하고, 해당 요청을 실제 객체로 전달한다.

 

 

이렇게 함으로써, 실제 객체의 생성과 관련된 비용이 실제로 필요한 순간까지 지연된다.

 

3. 원격 객체의 접근

분산 시스템에서 원격 서버에 위치한 객체에 접근해야 할 경우, 직접적인 접근은 네트워크 지연 시간이나 다른 네트워크 문제로 복잡해질 수 있다. 프록시 객체는 클라이언트와 원격 객체 사이의 통신을 관리하고, 마치 로컬 객체처럼 작동하게 만들어 줌으로써 이러한 복잡성을 추상화한다.

 

만약 서버의 분리 과정에서, 다음과 같이 데이터를 주고받을 때 많은 오버헤드가 생길 여지가 있다.

 

 

만약 메인 서버의 DB나, 핵심 구조를 방문할 필요가 없다면, 이러한 로직의 프록시 객체를 Local Server에 생성하고, 이를 통해 로직을 작용한다면 오버헤드 문제를 방지할 수 있을 것이다.

 

4. 로깅 및 모니터링

클라이언트의 요청을 로깅하거나 모니터링하는 기능을 추가하고 싶을 때, 프록시 패턴을 활용할 수 있다. 프록시 객체는 실제 객체로의 모든 요청을 중간에서 가로채어 이를 기록하고, 필요한 추가 작업을 수행한 후 실제 객체에 요청을 전달한다.

 

 

프록시의 조건 및 정리 (디자인패턴 관점)

프록시 디자인 패턴을 "적용"한다고 하기 위해서는 몇 가지 핵심 요소가 충족되어야 한다.

단순히 어떤 클래스나 존재하는 객체를 생성자 등으로 가져오기만 해서는 프록시 패턴이라고 할 수 없다.

 

1. 인터페이스 일치: 프록시 객체는 원본 객체와 동일한 인터페이스를 구현해야 한다.

이를 통해 클라이언트는 프록시 객체를 원본 객체와 동일하게 사용할 수 있어야 한다.

 

2. 중개 역할: 프록시 객체는 원본 객체의 메서드 호출을 중개(intermediate)하는 역할을 해야 한다.

이는 프록시 객체가 메서드 호출을 가로채서 원본 객체로 전달하기 전후에 추가적인 작업을 수행할 수 있다는 의미이다.

 

3. 추가 기능 제공: 프록시 객체는 원본 객체의 기능을 확장하거나, 추가적인 기능(접근 제어, 로깅, 캐싱, 지연 로딩 등)을 제공해야 한다. 단순히 원본 객체의 메서드를 호출하는 것을 넘어서, 프록시 객체는 이러한 추가 기능을 통해 가치를 제공해야 진정한 프록시라고 할 수 있다.

 

구성 요소와 예시 코드

프록시 패턴의 주요 구성 요소

  1. Subject: 실제 객체와 프록시 객체가 구현해야 하는 인터페이스.
  2. RealSubject: 클라이언트가 사용하고자 하는 실제 객체.
  3. Proxy: RealSubject의 기능을 대리하여 수행하는 객체. RealSubject와 같은 Subject 인터페이스를 구현하며, RealSubject에 대한 참조를 내부에 가지고 있다.

프록시 패턴의 동작 과정

  1. 클라이언트는 Proxy 객체를 통해 어떤 작업을 요청한다.
  2. Proxy 객체는 필요한 경우 추가 작업을 수행하고, 실제 작업을 RealSubject 객체에 위임한다.
  3. RealSubject 객체는 실제 작업을 수행하고 결과를 Proxy 객체에 반환한다.
  4. Proxy 객체는 필요한 경우 결과를 수정하거나 추가 작업을 수행한 후 클라이언트에게 결과를 반환한다.

접근 제어 프록시

클라이언트가 특정 서비스의 메서드를 호출할 때, 프록시 패턴을 사용하여 접근 권한을 검사하는 예를 들어보겠다. 이 경우, 프록시 객체는 클라이언트의 요청을 받기 전에 사용자 인증을 수행한다.

 

interface Service {
    void performOperation();
}

// 실제 서비스 객체
class RealService implements Service {
    @Override
    public void performOperation() {
        System.out.println("Performing operation in RealService");
    }
}

// 프록시 객체
class ProxyService implements Service {
    private RealService realService;
    private boolean isAuthenticated;

    public ProxyService(boolean isAuthenticated) {
        this.isAuthenticated = isAuthenticated;
        this.realService = new RealService();
    }

    @Override
    public void performOperation() {
        if (isAuthenticated) {
            System.out.println("User is authenticated. Proceeding with operation.");
            realService.performOperation(); // 실제 서비스 메서드 호출
        } else {
            System.out.println("User is not authenticated. Access denied.");
        }
    }
}

public class ProxyPatternDemo {
    public static void main(String[] args) {
        Service service = new ProxyService(true);
        service.performOperation(); // 인증된 사용자

        Service service2 = new ProxyService(false);
        service2.performOperation(); // 인증되지 않은 사용자
    }
}

 

위 코드에서 ProxyServiceService 인터페이스를 구현하여 클라이언트가 performOperation 메서드를 호출할 때마다 사용자 인증 여부를 검사한다.

 

사용자가 인증된 경우에만 RealServiceperformOperation 메서드를 호출하고, 그렇지 않은 경우 접근 거부 메시지를 출력한다. 이 방식으로, 실제 서비스 객체에 대한 접근을 효과적으로 제어할 수 있다.