BackEnd/Design Patterens

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

샤아이인 2022. 1. 13.

Proxy Pattern 이란?

 

Proxy Pattern - 어떤 객체에 대한 접근을 제어하기 위한 용도로
대리인이나 대변인에 해당하는 객체를 제공하는 패턴

위의 정의에서 접근을 제어하는 프록시는 어떤 것 일까요?

아래에서 배울 뽑기 기계의 경우 프록시가 원격 객체에 대한 접근을 제어하고 있다고 생각하면 됩니다. 원격 프록시가 접근을 제어해서 네트워크 관련사항들을 챙겨줬다 할 수 이는거죠.

 

대표적인 접근을 제어하는 방법을 알아봅시다.

▶ 원격 프록시를 써서 원격 객체에 대한 접근을 제어할 수 있습니다.

▶ 가상 프록시를 써서 생성하기 힘든 자원에 대한 접근을 제어할 수 있습니다.

▶ 보호 프록시를 써서 접근 권한이 필요한 자원에 대한 접근을 제어할 수 있습니다.

 

클래스 다이어그램도 한번 살펴볼까요?

출처 - 헤드퍼스트 디자인패턴

Proxy에는 RealSubject에 대한 레퍼런스가 들어있습니다. Proxy에서 RealSubject를 생성하거나 제거하는 역할을 책임지는 경우도 있습니다. 클라이언트는 항상 Proxy를 통해서 RealSubject하고 데이터를 주고받습니다.

 

또한 둘다 같은 인터페이스를 구현하기 때문에 RealSubject가 들어갈 자리라면 어디든 Proxy도 들어갈 수 있습니다.

Proxy는 RealSubject에 대한 접근을 제어하는 역할도 맡게됩니다.


패턴 소개

이전시간 State 패턴에서 뽑기기계를 만들어 달라 하신 CEO가 다시한번 일을 맡기려 합니다!

이번에는 "모든 뽑기 기계의 재고와 현재 상태를 알고싶다" 라고 하시는군요.

 

모니터링용 코드를 만들어 봅시다!

 

우선 GumballMachine 클래스에 현재 위치를 지원하기 위한 기능을 추가해 봅시다.

public class GumballMachine {
    String location;

    public GumballMachine(String location, int count){
        this.location = location;
    }

    public String getLocation(){
        return location;
    }
    // ... 기타 메소드들
}
 

위치 정보는 그냥 String으로 저장하면 충분할 것 입니다. 또한 이를 얻기위한 getter 메소드 또한 준비했습니다.

 

뽑기의 위치, 알맹이의 남은 재고량, 현 상태를 깔끔하게 출력해주는 GumballMonitor 클래스를 만들어 봅시다.

public class GumballMonitor {
    GumballMachine machine;

    public GumballMonitor(GumballMachine machine){
        this.machine = machine;
    }

    public void report() {
        System.out.println("뽑기 기계 위치: " + machine.getLocation());
        System.out.println("현재 재고: " + machine.getCount());
        System.out.println("현재 상태: " + machine.getState());
    }
}
 

 

이제 테스트 코드를 사용해서 확인해봐야 겠죠? 다음 테스트 코드를 사용해 봅시다.

package com.company.Proxy;

public class GumballMachineTestDrive {

    public static void main(String[] args) {
        int count = 0;

        if (args.length < 2) {
            System.out.println("GumballMachine <name> <inventory>"); // 명령행에서 초기위치와 알맹이수를 입력
            System.exit(1);
        }

        try {
            count = Integer.parseInt(args[1]);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
        GumballMachine gumballMachine = new GumballMachine(args[0], count);

        GumballMonitor monitor = new GumballMonitor(gumballMachine); // 의존성 주입

        // 기타 실헙 코드들

        monitor.report();
    }
}
 

정말 잘만들어진 모니터링 시스템 이군요!

 

하지만 사장이 갑자기 말을 바꾸십니다... 자기는 원격으로 모니터링하는 기능을 원했다는군요? 이제와서?

이미 네트워크 연결또한 다 해놨다네요...

 

일단 모니터링용 GumballMonitor class는 만들어논 상태입니다. 모니터링하고싶은 기계의 레퍼런스만 GumballMonitor에 등록시켜주면 될것같군요.

 

문제는 GumballMonitor과 기계가 같은 JVM에서 돌아가야 하는데... CEO가 자기 컴퓨터에 있는 GumballMonitor 에서 멀리 떨어져 있는 뽑기 기계를 모니터링 하고 싶어한다는 점 입니다.

 

이럴때 GumballMonitor 클래스는 그대로 두고, 원격 객체에 대한 프록시만 넘겨주면 됩니다.

프록시가 진짜 객체를 대신하는 역할을 하는거죠. 실제로는 네트워크를 통해 GumballMachine 객체와 데이터를 주고받습니다

프록시가 진짜 객체인양 행동하지만 실제로는 네트워크를 통해 진짜와 소통하고 있는 가짜입니다.


원격 프록시의 역할

원격 프록시는 원격 객체에 대한 로컬대변자(local representative)역할을 수행합니다.

원격 객체란, 다른 자바 가상머신의 힙에서 존재하는 객체를 뜻합니다.

로컬대변자는 어떤 메소드를 호출하면 다른 원격객체한태 그 메소드 호출을 전달해주는 역할을 맡고있는 객체를 로컬대변자 라고 합니다.

 

그림으로 보면 다음과 같습니다.

프록시는 원격 객체(GumballMachine)인 것 같지만, 실상은 가짜입니다.

 

클라이언트에서는 이점을 모르기때문에 원격 객체의 메소드를 호출하는 것 처럼 느껴집니다.

하지만 실제로는 로컬 힙에 들어있는 '프록시' 가 원격 객체의 메소드를 대신 호출하고 있는거죠.

네트워크 통신과 관련된 저수준 작업은 이 프록시 객체에서 처리해 줍니다.

 

아! 추가적으로 프록시를 스터브라고도 부릅니다.

프록시로부터 전달된 명령을 이해하고 적합한 메소드를 호출해주는 역할을 하는 보조객체를 스켈레톤이라 합니다.


이론적으로 좋은 아이디어는 맞는것 같은데... 다른 JVM에 있는 객체의 메소드를 호출하는 프록시는 어떻게 만들수 있을까요?

바로 자바 원격 메소드 호출(RMI, Remote Method Invocation)을 사용하면 됩니다.

 

1) GumballMachine를 원격 서비스로 개조하기

우선 GumballMachine용 원격 인터페이스를 만듭니다. 인터페이스 에서는 원격 클라이언트가 호출할 메소드들을 정의해야 합니다.

import java.rmi.*;

public interface GumballMachineRemote extends Remote { // Remote 인터페이스를 상속합니다.
    public int getCount() throws RemoteException; // 모든 메소드들은 예외를 던질수 있습니다.
    public String getLocation() throws RemoteException;
    public State getState() throws RemoteException;
}
 

또한 인터페이스에서 모든 메소드들의 반환값이 직렬화 가능한 형식인지 꼭 확인해야 합니다.

 

State class는 사용자 정의 class이기 때문에 직렬화가 되지 않습니다. 따라서 다음과 같이 수정해주어야 합니다.

import java.io.Serializable;

public interface State extends Serializable { // Serializable interface를 상속받음

    public void insertQuarter();
    public void ejectQuarter();
    public void turnCrank();
    public void dispense();

}
 

이렇게 하면 State의 서브클래스들이 직렬화가 가능해집니다.

 

하지만 아직 문제점이 남아있습니다. 모든 State 객체에서는 뽑기 기계의 메소드를 호출하거나 상태를 변경하기 위한 용도로써 뽑기 기계에 대한 레퍼런스를 갖고있습니다. 그렇다고 State객체가 전송될 때 뽑기 기계도 전부 직렬화 시키기에는 효율적이 않은것 같습니다. 이는 다음과 같이 해결할 수 있습니다.

public class NoQuarterState implements State{
    transient GumballMachine gumballMachine; // transient 이용
    ...
}
 

State를 구현하는 모든 ConcreteState에서 인스턴스 변수 선언부에 trasient를 추가해줍니다.

 

 

GumballMachine은 이미 구현되어있지만, 서비스 역할을 할 수 있도록 약간 고쳐야 합니다.

import java.rmi.*;
import java.rmi.server.*;

public class GumballMachine extends UnicastRemoteObject implements GumballMachineRemote{
    // 인스턴스 변수

    public GumballMachine(String location, int numberGumballs) throws RemoteException {
        // 생성자 부분
    }

    public String getLocation(){
        return location;
    }

    public int getCount(){
        return count;
    }

    public State getState() {
        return state;
    }
    //... 등등등
}
 

UnicastRemoteObject를 상속받고 있으며, GumballMachineRemote을 구현합니다.

또한 UnicastRemoteObject의 생성자에서 RemoteException을 throw하기 때문에 이를 상속한 GumballMachine 에서도 생성자에서 예외를 던지도록 해주어야 합니다.

 

2) RMI 레지스트리 등록

서비스 부분은 전부 완료하였습니다. 이제 서비스 요청을 받아서 처리할 수 있도록, 즉 클라이언트에서 찾을 수 있도록 RMI 레지스트리에 등록해야 합니다.

import java.rmi.Naming;
import java.rmi.RemoteException;

public class GumballMachineTestDrive {

    public static void main(String[] args) throws RemoteException { // 예외를 던질수 있습니다.
        GumballMachineRemote gumballMachineRemote = null;
        int count = 0;

        if (args.length < 2) {
            System.out.println("GumballMachine <name> <inventory>");
            System.exit(1);
        }

        try {
            count = Integer.parseInt(args[1]);
            gumballMachineRemote = new GumballMachine(args[0], count);
            Naming.rebind("//" + args[0] + "/gumballmachine", gumballMachineRemote); // 스터브 등록
        } catch (Exception e) {
            e.printStackTrace();
        }
}
 

이후 rmiregistry를 실행켜 RMI 레지스트리 서비스를 실행한 후, 뽑기 기계를 구동시킨후 레지스트리에 등록합니다.

 

3) GumballMonitor 수정

import java.rmi.*;

public class GumballMonitor {
    GumballMachineRemote machine; // 원격 인터페이스 사용

    public GumballMonitor(GumballMachineRemote machine){
        this.machine = machine;
    }

    public void report() {
        try { // 메소드들이 예외를 던질수 있기 때문에 예외처리
            System.out.println("뽑기 기계 위치: " + machine.getLocation());
            System.out.println("현재 재고: " + machine.getCount());
            System.out.println("현재 상태: " + machine.getState());
        }catch (RemoteException e){
            e.printStackTrace();
        }
    }
}
 

드디어 준비가 다 끝난것 같습니다. 모니터링용 테스트 클래스를 작성하여 돌려봅시다.

import java.rmi.*;

public class GumballMonitorTestDrive {

    public static void main(String[] args) {
        String[] location = {"rmi://127.0.0.1/gumballmachine", // 모니터링할 위치
                "rmi://127.0.0.1/gumballmachine",
                "rmi://127.0.0.1/gumballmachine"};

        if (args.length >= 0){
            location = new String[1];
            location[0] = "rmi://" + args[0] + "/gumballmachine";
        }

        GumballMonitor[] monitor = new GumballMonitor[location.length]; // 모니터 배열

        for (int i=0;i < location.length; i++) {
            try {
                GumballMachineRemote machine = (GumballMachineRemote) Naming.lookup(location[i]); // 프록시 반환
                monitor[i] = new GumballMonitor(machine);
                System.out.println(monitor[i]);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        for(int i=0; i < monitor.length; i++) {
            monitor[i].report();
        }
    }
}
 

lookup을 통하여 레지스트리에 등록되어 있는 프록시를 반환받게 됩니다. 이렇게 반환받은 프록시는 GumballMonitor를 생성할때 인자로 넘겨줍니다.

 

좀 복잡하셨죠? 죄송합니다 RMI 기술에 대한 설명부분은 싹다 빼버리고 글을 작성해서 그러니 양해 부탁드려요 ㅠ.ㅠ

 

어떤 작업이 일어난건지 정리해봅시다.

 

1) CEO가 모니터링을 시작하면 GumballMonitor에서 RMI 레지스트리 로 부터 뽑기기계 원격객체에 대한 프록시를 갖어온 후,

getState(), getCount(), getLocation()을 반환합니다.

 

2) 프록시의 getState()가 요청되면 프록시는 그 요청을 원격 서비스로 전달합니다. 스캘레톤에서 그 요청을 받아 뽑기 기계에게 전달하게 됩니다.

 

3) GumballMachine에서 스켈레톤에 상태를 반환합니다. 그러면 스켈레톤에서 리턴받은 상태를 직렬화 한 다음 네트워크를 통해 프록시 한태 전달합니다. 프록시에서는 리턴받은 값을 역직렬화 해서 GumballMonitor에게 전달합니다.

댓글