[디자인패턴] 프록시 패턴 (Proxy Pattern)

koline·2023년 9월 6일
0

디자인패턴

목록 보기
13/24

프록시 패턴


프록시는 '대리인'이라는 의미로 실체 객체에 대한 대리객체실체 객체에 대한 접근 이전에 필요한 행동을 취할 수 있게 만들며, 이 점을 이용해서 미리 할당하지 않아도 상관없는 것들을 실제 이용할 때 할당하게 하여 메모리 용량을 아낄 수 있으며, 실체 객체를 드러나지 않게 하여 정보은닉의 역할도 수행하는 패턴이다.

즉, 대상 클래스가 민감한 정보를 가지고 있거나 인스턴스화 하기에 무겁거나 추가 기능을 가미하고 싶은데, 원본 객체를 수정할수 없는 상황일 때를 극복하기 위해서 사용한다.

프록시 패턴을 사용하면 다음과 같은 효과를 얻을 수 있다.

  1. 보안(Security) : 프록시는 클라이언트가 작업을 수행할 수 있는 권한이 있는지 확인하고 검사 결과가 긍정적인 경우에만 요청을 대상으로 전달한다.
  2. 캐싱(Caching) : 프록시가 내부 캐시를 유지하여 데이터가 캐시에 아직 존재하지 않는 경우에만 대상에서 작업이 실행되도록 한다.
  3. 데이터 유효성 검사(Data Validation) : 프록시가 입력을 대상으로 전달하기 전에 유효성을 검사한다.
  4. 지연 초기화(Lazy Initialization) : 대상의 생성 비용이 비싸다면 프록시는 그것을 필요로 할때까지 연기할 수 있다.
  5. 로깅(Logging) : 프록시는 메소드 호출과 상대 매개 변수를 인터셉트하고 이를 기록한다.
  6. 원격 객체(Remote Objects) : 프록시는 원격 위치에 있는 객체를 가져와서 로컬처럼 보이게 할 수 있다.



구조


  1. Subject : Proxy와 RealSubject를 하나로 묶는 인터페이스 (다형성)
    대상 객체와 프록시 역할을 동일하게 하는 추상 메소드 operation() 를 정의한다.
    인터페이스가 있기 때문에 클라이언트는 Proxy 역할과 RealSubject 역할의 차이를 의식할 필요가 없다.
  2. RealSubject : 원본 대상 객체
  3. Proxy : 대상 객체(RealSubject)를 중계할 대리자 역할
    프록시는 대상 객체를 합성(composition)한다.
    프록시는 대상 객체와 같은 이름의 메서드를 호출하며, 별도의 로직을 수행 할수 있다 (인터페이스 구현 메소드)
    프록시는 흐름제어만 할 뿐 결과값을 조작하거나 변경시키면 안 된다.
  4. Client : Subject 인터페이스를 이용하여 프록시 객체를 생성해 이용.
    클라이언트는 프록시를 중간에 두고 프록시를 통해서 RealSubject와 데이터를 주고 받는다.

Proxy 패턴은 단순하면서도 자주 쓰이는 패턴이며, 그 활용 방식도 다양하다. 같은 프록시 객체라도 어떠한 로직을 짜느냐에 따라 그 활용도는 천차만별이 된다. Proxy 패턴의 기본형을 어떤 방식으로 변형하느냐에 따라 프록시 종류가 나뉘어지게 된다.



종류


  1. 기본형 프록시 (Normal Proxy)
// ISubject.java (Subject)
public interface ISubject {
    void operation();
}

// RealSubject.java (RealSubject)
public class RealSubject implements ISubject {
    @Override
    public void operation() {
        System.out.println("[RealSubject] operating...");
    }
}

// Proxy.java (Proxy)
public class Proxy implements ISubject {
    private RealSubject subject;

    public Proxy(RealSubject subject) {
        this.subject = subject;
    }

    @Override
    public void operation() {
		// subject의 action을 위임받음
        subject.operation();

        // do something...

        System.out.println("[Proxy] operating...");
    }
}

// Client.java (Client)
public class Client {
    public static void main(String[] args) {
        ISubject subject = new Proxy(new RealSubject());

        subject.operation();
    }
}

// 실행 결과
[RealSubject] operating...
[Proxy] operating...

기본형 프록시 패턴은 구조 파트에서 본 그림처럼 RealSubject의 operation을 Proxy가 위임 받아 실행한 뒤 추가적으로 필요한 동작을 수행하는 구조로 이뤄져있다.

  1. 가상 프록시 (Virtual Proxy)
// Proxy.java (Proxy)
public class Proxy implements ISubject {
    private RealSubject subject;

    public Proxy() {
    }

    @Override
    public void operation() {

        if (subject == null) {
            subject = new RealSubject();
        }

        subject.operation();

        // do something...

        System.out.println("[Proxy] operating...");
    }
}

// Client.java (Client)
public class Client {
    public static void main(String[] args) {
        ISubject subject = new Proxy();

        subject.operation();
    }
}

// 실행 결과
[RealSubject] operating...
[Proxy] operating...

가끔 필요하지만 항상 메모리에 적재되어 있는 무거운 서비스 객체가 있는 경우 사용하는 지연 초기화 방식이다. 이 구현은 실제 객체의 생성에 많은 자원이 소모 되지만 사용 빈도는 낮을 때 쓰는 방식이다.

서비스가 시작될 때 객체를 생성하는 대신에 객체 초기화가 실제로 필요한 시점에 초기화될수 있도록 지연할 수 있다.

3.보호 프록시 (Protection Proxy)

// Proxy.java (Proxy)
public class Proxy implements ISubject {
    private RealSubject subject;
    private boolean access;

    public Proxy(RealSubject subject, boolean access) {
        this.subject = subject;
        this.access = access;
    }

    @Override
    public void operation() {
        if (access) {
            subject.operation();

            // do something...

            System.out.println("[Proxy] operating...");
        }
    }
}

// Client.java (Client)
public class Client {
    public static void main(String[] args) {
        ISubject subject1 = new Proxy(new RealSubject(), false);
        System.out.println("======== subject 1 ========");
        subject1.operation();

        ISubject subject2 = new Proxy(new RealSubject(), true);
        System.out.println("======== subject 2 ========");
        subject2.operation();
    }
}

// 실행 결과
======== subject 1 ========
======== subject 2 ========
[RealSubject] operating...
[Proxy] operating...

프록시가 대상 객체에 대한 자원으로의 엑세스를 제어한다 (접근 권한에 따라). 특정 클라이언트만 서비스 객체를 사용할 수 있도록 하는 경우 사용한다. 프록시 객체를 통해 클라이언트의 자격 증명이 기준과 일치하는 경우에만 서비스 객체에 요청을 전달할 수 있게 한다.

  1. 로깅 프록시 (Logging Proxy)
// Proxy.java (Proxy)
public class Proxy implements ISubject {
    private RealSubject subject;

    public Proxy(RealSubject subject) {
        this.subject = subject;
    }

    @Override
    public void operation() {
        System.out.println("======= Logging =======");

        subject.operation();

        // do something...

        System.out.println("[Proxy] operating...");
        
        System.out.println("======= Logging =======");
    }
}

// Client.java (Client)
public class Client {
    public static void main(String[] args) {
        ISubject subject = new Proxy(new RealSubject());
        subject.operation();
    }
}

// 실행 결과
======= Logging =======
[RealSubject] operating...
[Proxy] operating...
======= Logging =======

대상 객체에 대한 로깅을 추가하려는 경우 사용한다. 프록시는 서비스 메서드를 실행하기 전달하기 전에 로깅을 하는 기능을 추가하여 재정의한다.

  1. 원격 프록시 (Remote Proxy)

프록시 클래스는 로컬에 있고, 대상 객체는 원격 서버에 존재하는 경우 사용한다. 프록시 객체는 네트워크를 통해 클라이언트의 요청을 전달하여 네트워크와 관련된 불필요한 작업들을 처리하고 결과값만 반환한다. 클라이언트 입장에선 프록시를 통해 객체를 이용하는 것이니 원격이든 로컬이든 신경 쓸 필요가 없으며, 프록시는 진짜 객체와 통신을 대리하게 된다.

  1. 캐싱 프록시 (Caching Proxy)

출처: https://docs.oracle.com/cd/E19438-01/819-3161/images/doc-retrieval.gif

데이터가 큰 경우 캐싱하여 재사용을 유도하는 패턴이다.
클라이언트 요청의 결과를 캐시하고 이 캐시의 수명 주기를 관리한다.



목적

  1. 접근을 제어하거가 기능을 추가하고 싶은데, 기존의 특정 객체를 수정할 수 없는 상황일때
  2. 초기화 지연, 접근 제어, 로깅, 캐싱 등, 기존 객체 동작에 수정 없이 가미하고 싶을 때

장점

  1. 개방 폐쇄 원칙(OCP) 준수
    1-1. 기존 대상 객체의 코드를 변경하지 않고 새로운 기능을 추가할 수 있다.
  2. 단일 책임 원칙(SRP) 준수
    2-1. 대상 객체는 자신의 기능에만 집중 하고, 그 이외 부가 기능을 제공하는 역할을 프록시 객체에 위임하여 다중 책임을 회피 할 수 있다.
  3. 원래 하려던 기능을 수행하며 그외의 부가적인 작업(로깅, 인증, 네트워크 통신 등)을 수행하는데 유용하다
  4. 클라이언트는 객체를 신경쓰지 않고, 서비스 객체를 제어하거나 생명 주기를 관리할 수 있다
  5. 사용자 입장에서는 프록시 객체나 실제 객체나 사용법은 유사하므로 사용성에 문제 되지 않는다

단점

  1. 많은 프록시 클래스를 도입해야 하므로 코드의 복잡도가 증가한다.
    1-1. 예를들어 여러 클래스에 로깅 기능을 가미 시키고 싶다면, 동일한 코드를 적용함에도 각각의 클래스에 해당되는 프록시 클래스를 만들어서 적용해야 되기 때문에 코드량이 많아지고 중복이 발생 된다.
    1-2. 자바에서는 리플렉션에서 제공하는 동적 프록시(Dynamic Proxy) 기법을 이용해서 해결할 수 있다. (후술)
  2. 프록시 클래스 자체에 들어가는 자원이 많다면 서비스로부터의 응답이 늦어질 수 있다.



Dynamic Proxy

개발자가 직접 디자인 패턴으로서 프록시 패턴을 구현해도 되지만, 자바 JDK에서는 별도로 프록시 객체 구현 기능을 지원한다. 이를 동적 프록시(Dynamic Proxy) 기법이라고 불리운다.

동적 프록시는 개발자가 직접 일일히 프록시 객체를 생성하는 것이 아닌, 애플리케이션 실행 도중 java.lang.reflect.Proxy 패키지에서 제공해주는 API를 이용하여 동적으로 프록시 인스턴스를 만들어 등록하는 방법으로서, 자바의 Reflection APIVisit Website 기법을 응용한 연장선의 개념이다. 그래서 별도의 프록시 클래스 정의없이 런타임으로 프록시 객체를 동적으로 생성해 이용할 수 있다는 장점이 있다.

// 대상 객체와 프록시를 묶는 인터페이스
interface Animal {
    void eat();
}

// 프록시를 적용할 타겟 객체
class Tiger implements Animal{
    @Override
    public void eat() {
        System.out.println("호랑이가 음식을 먹습니다.");
    }
}

// Client.java
public class Client {
    public static void main(String[] arguments) {
		
        // newProxyInstance() 메서드로 동적으로 프록시 객체를 생성할 수 있다.
        Animal tigerProxy = (Animal) Proxy.newProxyInstance(
                Animal.class.getClassLoader(), // 대상 객체의 인터페이스의 클래스로더
                new Class[]{Animal.class}, // 대상 객체의 인터페이스
                new InvocationHandler() { // 프록시 핸들러
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object target = new Tiger();

                        System.out.println("----eat 메서드 호출 전----");

                        Object result = method.invoke(target, args); // 타겟 메서드 호출

                        System.out.println("----eat 메서드 호출 후----");

                        return result;
                    }
                }
        );

        tigerProxy.eat();
    }
}

// 실행 결과
----eat 메서드 호출 전----
호랑이가 음식을 먹습니다.
----eat 메서드 호출 후----


Spring AOP

스프링 프레임워크에서는 내부적으로 프록시 기술을 정말 많이 사용하고 있다. (AOP, JPA 등)

스프링에서는 Bean을 등록할 때 SingletonVisit Website을 유지하기 위해 Dynamic Proxy 기법을 이용해 프록시 객체를 Bean으로 등록한다. 또한 Bean으로 등록하려는 기본적으로 객체가 Interface를 하나라도 구현하고 있으면 JDK를 이용하고 Interface를 구현하고 있지 않으면 내장된 CGLIB 라이브러리를 이용한다.

@Service
public class GameService {
	public void startDame() {
    	System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");
    }
}

@Aspect
@Comonent
public class PerfAspect {
	@Around("bean(gameService)")
	public void timestamp(ProceedingJoinPoint point) throws Throwable {
    	System.out.println("프록시 실행 1");
        
        point.proceed(); // 대상 객체의 원본 메서드를 실행
        
        System.out.println("프록시 실행 2");
    }
}


참고


[디자인패턴] 디자인패턴이란? - 생성패턴, 구조패턴, 행위패턴

프록시(Proxy) 패턴 - 완벽 마스터하기

[Design Patterns] Proxy Pattern : 프록시 패턴

profile
개발공부를해보자

0개의 댓글