프록시 패턴

yanju·2023년 1월 13일
0
post-thumbnail

프록시 패턴

프록시는 원래 객체에 대한 접근을 제어하므로, 당신의 요청이 원래 객체에 전달되기 전 또는 후에 무언가를 수행할 수 있도록 한다.

신용 카드는 은행 계좌의 프록시이다.

은행 계좌는 현금의 프록시이다.

신용 카드, 은행 계좌는 같은 인터페이스를 구현하며 둘 다 결제에 사용할 수 있다.

필요성

객체에 대한 접근을 제한하는 이유는 다음과 같다.

방대한 양의 시스템 자원을 소비하는 거대한 객체가 있다고 가정하자.

이 객체는 필요할 때가 있지만, 항상 필요하진 않다.

우리는 이 객체를 필요할 때만 만들어서 지연된 초기화를 구현할 수 있다.

하지만 이러면 객체의 모든 클라이언트들은 지연된 초기화 코드를 실행해야한다는 단점이 있다.

이것은 많은 코드 중복을 초래한다.

해결책

프록시 패턴은 원래 서비스 객체와 같은 인터페이스로 새 프록시 클래스를 생성한다.

그 다음 프록시 객체를 모든 클라이언트들에게 전달하도록 앱을 바꾼다.

클라이언트로부터 요청을 받으면 이 프록시는 실제 서비스 객체를 생성하고 모든 작업을 실제 서비스 객체에 위임한다.

이렇게하면 클래스의 메인 로직 이전이나 이후에 새로운 로직을 실행해야 하는 경우 프록시는 해당 클래스를 변경하지 않고 새로운 로직을 수행할 수 있다. (@Transactional)

구조

프록시는 원래 클래스와 같은 인터페이스를 구현하므로 실제 서비스 객체를 호출하는 모든 클라이언트가 사용할 수 있다.

ServiceInterface

서비스의 인터페이스를 선언한다.

프록시가 서비스 객체로 위장할 수 있으려면 이 인터페이스를 따라야 한다.

Sevice

서비스는 비즈니스 로직을 제공한다.

Proxy

프록시는 서비스 객체를 가리키는 참조 필드가 있다.

프록시가 요청의 처리(초기화 지연, 로깅, 액세스 제어, 캐싱 등)를 완료하면, 그 후 처리된 요청을 서비스 객체에 전달한다.

일반적으로 프록시들은 서비스 객체들의 전체 수명 주기를 관리한다.

Client

클라이언트는 같은 인터페이스를 통해 서비스들 및 프록시들과 함께 작동해야 한다.

그러면 서비스 객체를 기대하는 모든 코드에 프록시를 전달할 수 있다.

적용

프록시 패턴을 활용하는 방법은 수십 가지가 있다.

대표적으로는 다음이 있다.

  • 가상 프록시
  • 보호 프록시
  • 원격 프록시

보호 프록시

자바의 java.lang.reflect 패키지 안에 프록시 기능이 내장돼 있다.

이 패키지를 사용하면 동적 프록시(dynamic proxy)를 만들 수 있다.

reflect 패키지를 사용해 즉석에서 하나 이상의 인터페이스를 구현하고, 지정한 클래스에 메소드 호출을 전달하는 프록시 클래스를 만든다.

진짜 프록시는 실행 중에 생성된다.

  • 프록시는 Proxy와 InvocationHandlerClass 두 개의 클래스로 구성된다.
  • 자바에서 Proxy 클래스는 생성해주며 Subject 인터페이스 전체를 구현한다.
  • InvocationHandler 클래스는 Proxy 객체에 대한 모든 메소드 호출을 전달받는 InvocationHandler를 제공한다.
  • InvocationHandler 에서 RealSubject 객체에 있는 메소드에 대한 접근을 제어한다.

자바에서 Proxy 클래스를 알아서 생성해준다.

따라서 Proxy 클래스에게 무슨 일을 해야 하는지 알려줘야한다.

InvocationHandler 를 이용해 프록시의 일을 작성한다.

InvocationHandler 는 프록시에 호출되는 모든 메소드에 응답한다.

Proxy에서 메소드 호출을 받으면 항상 InvocationHandler에 진짜 작업을 부탁한다.

public interface PersonBean {
    String getName();
    String getGender();
    String getInterests();
    int getHotOrNotRating();

    void setName(String name);
    void setGender(String gender);
    void setInterests(String interests);
    void setHotOrNotRating(int rating);
}
// 위 인터페이스를 구현하면, 자기의 점수 뿐만 아니라 다른 고객의 정보를 수정할 수 있게 된다.
// 이런 경우 보호 프록시를 사용한다.
// 본인 접근용 proxy, 다른 유저 접근용 proxy 필요

보호 프록시는 접근 권한을 바탕으로 객체로의 접근을 제어하는 프록시다.

  • 회사 직원을 나타내는 객체가 있다면 일반 직원 객체에서 호출할 수 있는 메소드가 정해진다.
  • 관리자 객체는 더 많은 메소드를 호출할 수 있다
  • 인사과 직원 객체는 모든 메소드를 호출할 수 있다.

PersonBean 용 동적 프록시 만들기

1. InvocationHandler 만들기

InvocationHandler에서 프록시의 행동을 구현한다.

프록시 클래스 및 객체를 만드는 일은 자바에서 알아서 해주기 때문에, 프록시의 메소드가 호출되었을 때 할 일을 지정해주는 핸들러만 만들면 된다.

호출 핸들러는 두개 만들어야 한다.

  • 하나는 자기 자신을 위한 핸들러
  • 다른 하나는 타인을 위한 핸들러.

호출 핸들러(Invocation Handler)는 다음과 같이 생각한다.

프록시의 메소드가 호출되면 프록시에서는 그 호출을 호출 핸들러에게 넘긴다.

하지만 호출 핸들러는 호출된 메소드가 무엇이든 무조건 핸들러의 invoke() 메소드가 호출된다.

import java.lang.reflect.*;

public class OwnerInvocationHandler implements InvocationHandler { 
    PersonBean person;

		// 생성자
    public OwnerInvocationHandler(PersonBean person) { // PersonBean 주입
        this.person = person;
    }

		// 여기서 행동 정의, 주제 기준
    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException {

    try {
        if (method.getName().startsWith("get")) { // 주제에게 허용
          return method.invoke(person, args);
        } else if (method.getName().equals("setHotOrNotRating")) { // 허용 x
          throw new IllegalAccessException();
        } else if (method.getName().startsWith("set")) { // 나머지 set 허용 O
          return method.invoke(person, args);
        } 
    } catch (InvocationTargetException e) {
    	e.printStackTrace();
    } 
        return null;
    }
}
  • InvocationHandler는 java.lang.reflect 패키지에 들어있다. 호출 핸들러는 반드시 InvocationHandler 인터페이스를 구현해야 한다.
  • 생성자를 통해 전달받은 주 객체에 대한 레퍼런스를 저장한다.
  • 호출하는 주 객체가 무엇인지, 호출하는 메소드가 무엇인지에 따라 주 객체의 메소드를 호출할지, 예외를 던질지 정한다.

2. 동적 프록시를 생성하는 코드 작성

Proxy 클래스를 생성하고 Proxy 객체 인스턴스를 만들어야 한다.

OwnerInvocationHandler 혹은 NonOwnerInvocationHandler한테 넘겨주는 프록시를 만든다.

PersonBean getOwnerProxy(PersonBean person) {
    return (PersonBean) Proxy.newProxyInstance( 
        person.getClass().getClassLoader(),
        person.getClass().getInterfaces(),
        new OwnerInvocationHandler(person)); // 프록시 생성
}

PersonBean getNonOwnerProxy(PersonBean person) {
    return (PersonBean) Proxy.newProxyInstance(
        person.getClass().getClassLoader(),
        person.getClass().getInterfaces(),
        new NonOwnerInvocationHandler(person)); // 프록시 생성
}

Person 객체(실제 주 객체)를 인자로 받아오고 프록시를 리턴한다.

프록시의 인터페이는 주 객체의 인터페이스와 같기 때문에 리턴 타입은 PersonBean 이다.

리턴 문에서 프록시를 생성한다. 코드 설명은 다음과 같다.

  • line 2: Proxy 클래스에 있는 newProxyInstance라는 정적 메소드를 써서 프록시를 생성한다.
  • line 3: person의 클래스로더를 인자로 전달한다.
  • line 4: 프록시에서 구현해야 하는 인터페이스도 인자로 전달해야 한다..
  • line 5: 호출 핸들러의 인자로 person을 전달하며, 호출 핸들러인 OwnerInvocationHandler도 인자로 전달해야 한다.

3. PersonBean 객체를 적절한 프록시로 감싼다

PersonBean 객체를 사용하는 객체는 고객 자신의 객체거나, 다른 고객의 객체 중 하나이다.

어떤 경우든 해당 PersonBean에 따라 적절한 프록시를 생성해야 한다.

  • OwnerInvocationHandler: 자기 자신 빈에 직접 접근하는 경우
  • NonOwnerInvocationHandler: 다른 사람의 빈에 접근하는 경우

스프링과 프록시 패턴

Spring AOP는 프록시 기반으로 JDK Dynamic ProxyCGLIB을 활용하여 AOP 제공하고 있다.

두 가지 AOP Proxy의 차이

Spring은 AOP Proxy를 생성하는 과정에서 자체 검증 로직을 통해 타깃의 인터페이스 유무를 판단합니다.

타깃이 하나 이상의 인터페이스를 구현하고 있는 클래스라면 JDK Dynamic Proxy의 방식으로 생성한다.

인터페이스를 구현하지 않은 클래스라면 CGLIB의 방식으로 AOP 프록시를 생성한다..

0개의 댓글