[디자인패턴] 11. the Proxy Pattern

StandingAsh·2024년 12월 2일
3

참고: Head First Design Patterns

개요


지난번에 구현하였던 껌볼 기계에 또 요구사항이 생겼다.

의뢰인이 상태인벤토리에 대한 모니터링을 원한다. 우린 이미 countstate에 대한 getter 메소드가 있으므로, 이를 이용하면 간단하게 구현할 수 있지 않을까?

public class GumballMachine {
    ... 생략
    String location;
 
    public GumballMachine(String location, int count) {
        ... 생략
        this.location = location;
    }
 
    public String getLocation() { return location; }
 
    ... 생략
 }

우선 껌볼 기계 코드에 location 정보를 추가하고, 이에 대한 getter를 만들자.

public class GumballMonitor {
    GumballMachine machine;
 
    public GumballMonitor(GumballMachine machine) {
        this.machine = machine;
    }
 
    public void report() {
        System.out.println(Gumball Machine:+ machine.getLocation());
        System.out.println(Current inventory:+ machine.getCount() + “ gumballs”);
        System.out.println(Current state:+ machine.getState());
    }
}

그리고 위와 같이 모니터링을 위한 클래스를 만들어 출력해주면 되겠다.

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);
        }
        count = Integer.parseInt(args[1]);
        GumballMachine gumballMachine = new GumballMachine(args[0], count);
        GumballMonitor monitor = new GumballMonitor(gumballMachine);
 
        monitor.report();
    }
}

자, 그러면 이제 report() 메소드만 잘 호출해준다면 의뢰인도 분명 만족하시겠지?

문제점

분명 기능과 출력 결과에는 문제가 없다. 그런데, 의뢰인이 맘에 들어하지 않는 모습이다. 의뢰인이 원한 것은 원격으로 모니터링 할 수 있는 코드였는데, 우리가 제공한 결과물은 껌볼 기계와 같은 JVM에서만 작동한다는 점이 문제다.

  • 원격 프록시(Remote Proxy)를 이용해보자!

Remote Proxy


proxy [명사]
1. 대리/위임(권)
2. 대리인

프록시의 사전적 정의이다. 원격 프록시는 원격 객체에게 원본 객체의 대리인 역할을 수행한다. 원격 객체란 다른 메모리공간에 존재하는 객체를 의미한다. Java에서는 다른 JVM의 힙(heap)에 존재하는 객체에 해당한다.

이론은 이해됐는데, 대체 이를 어떻게 구현해야 할까? 다행히도, Java는 자체적으로 RMI라는 원격 호출 기능을 지원한다. 우리는 이를 우리 코드에 잘 적용시키기만 하면 된다.

RMI(Remote Method Invocation)

RMI는 다른 JVM 위의 원격 객체를 찾아서 메소드를 호출할 수 있도록 해주는 기능이다. 우선 RMI의 작동 원리부터 알아보자.

클라이언트 객체가 원격의 서비스 객체의 메소드를 이용하기 위해서는 클라이언트와 서비스 각각 헬퍼(Helper) 객체가 필요하다. 이 헬퍼가 클라이언트와 서버의 소통을 담당해 줄 것이다.

소통의 과정은 아래와 같이 단계적으로 정리할 수 있다.

  1. 클라이언트는 클라이언트 헬퍼의 메소드를 실제 서비스의 메소드를 호출하는 것 처럼 호출한다. 클라이언트 헬퍼는 마치 실제 서비스인 것처럼 역할을 수행하지만, 실제로는 서비스의 그 어떤 로직도 가지고 있지 않다.

  2. 대신, 클라이언트 헬퍼는 서비스 측에 접근해 메소드 호출에 대한 정보를 교환하고, 서비스의 답변을 기다린다. 이 클라이언트 헬퍼가 곧 서비스의 대변인, 즉 프록시라고 할 수 있다.

  3. 한편 서버 측에서는 서비스 헬퍼가 클라이언트 헬퍼의 요청을 소켓 통신을 통해 수신한 뒤 진짜 서비스 객체의 메소드를 호출한다.

  4. 서비스 객체는 메소드를 실행하여 결과값을 서비스 헬퍼에게 전달한다.

  5. 서비스 헬퍼는 전달 받은 결과값을 소켓을 통해 클라이언트 헬퍼에게 전달한다.

  6. 클라이언트 헬퍼는 수신한 결과값을 최종적으로 클라이언트에게 전달한다.

RMI의 사용법


RMI를 이용하면 클라이언트와 서버의 소통을 위한 네트워크 혹은 I/O 코드를 직접 작성하지 않아도 된다. 이제 이를 적용하여 코드를 만들어보자.

  • 참고로, RMI에서 클라이언트 헬퍼스텁(Stub), 서비스 헬퍼스켈레톤(Skeleton)이라고 부른다.

본격적으로 원격 서비스를 만들기 위해서는 아래 5가지 단계가 필요하다.

  1. 원격 인터페이스를 만든다.
  2. 원격 인터페이스의 구현체를 만든다.
  3. rmic(툴)로 스텁과 스켈레톤을 생성한다.
  4. rmiregistry를 실행한다.
  5. 원격 서비스를 실행한다.

위 단계를 따르면서 차근차근 진행해보자.

1. 원격 인터페이스 만들기

java.rmi 패키지의 Remote를 상속받는 인터페이스를 만들고, 모든 메소드가 RemoteException을 던지도록 해야 한다.

import java.rmi.*;

public interface MyRemote extends Remote {
	public String sayHello() throws RemoteException;
}
  • 추가로, 메소드의 리턴 타입은 원시 타입이거나 직렬화 가능(Serializable)해야 한다.

사실 이는 당연한게, 서로 다른 JVM 위의 클라이언트와 서비스가 정보를 주고 받아야 하는데 서비스가 자체 제작한 클래스를 리턴 타입으로 사용한다면 클라이언트는 이를 알 방법이 없기 때문이다. 따라서, 직렬화를 통해 클라이언트에게 전달해 줄 수 있어야 한다.

2. 원격 인터페이스의 구현체

당연히 Remote 인터페이스를 implements 해야겠지만, UnicastRemoteObjectextends 해주어야 한다. 원격 객체가 되기 위해서는 필요한 기능이 있는데, 이를 구현하는 가장 쉬운 방법이 UnicastRemoteObject를 상속받아 알아서 이 수퍼클래스가 알아서 하도록 하는 것이기 때문이다.

public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {

	public MyRemoteImpl throws RemoteException {}
    
	public String sayHello() {
		returnServer says,Hey’”;
	}
	...
}

그런데, 이 객체의 생성자는 RemoteException을 던지도록 구현되어있다. 따라서, 우리의 MyRemoteImpl 역시 해당 예외를 던지도록 해야한다.

마지막으로, 이 구현체를 RMI Registry에 등록하는 코드를 추가해주어야 한다. java.rmi.Naming 클래스를 이용하면 된다. 이 구현체를 등록하면, RMI 시스템은 실제로는 스텁을 registry에 추가한다. 클라이언트에게 필요한 것은 원본 서비스가 아닌 스텁이기 때문이다.

try {
	MyRemote service = new MyRemoteImpl();
    Naming.rebind("RemoteHello", service);
} catch(Exception e) { ... }

3. 스텁과 스켈레톤의 생성

JDK에는 rmic라는 툴이 포함되어 있는데, 이 툴은 서비스 구현체를 바탕으로 스텁과 스켈레톤을 만들어준다. 네이밍 컨벤션으로 서비스 객체의 이름 뒤에 '_Stub' 혹은 '_Skel'을 붙여준다.

4. rmiregistry 실행

터미널에서 rmiregistry를 실행하기만 하면 된다. 다만, 이를 실행할 디렉토리 경로가 클래스 파일들에 접근 권한이 있어야 한다.

5. 서비스 실행

새로운 터미널에서 서비스 객체를 실행하면 된다. 서비스 객체의 최종 구현은 아래와 같은 모습일 것이다.

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

public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
    
    public MyRemoteImpl() throws RemoteException {}
    
    public String sayHello() {
    	returnServer says,Hey’”;
    }
    
    public static void main (String[] args) {
    	try {
        	MyRemote service = new MyRemoteImpl();
            Naming.rebind(RemoteHello, service);
        } catch(Exception ex) {
        	ex.printStackTrace();
        }
	}
}

그렇다면, 클라이언트는 어떻게 스텁 객체를 찾을까?

import java.rmi.*;

public class MyRemoteClient {

	public static void main (String[] args) {
    	new MyRemoteClient().go();
    }
    
    public void go() {
    	try {
    		MyRemote service = (MyRemote) Naming.lookup(“rmi://127.0.0.1/RemoteHello);
        	String s = service.sayHello(); 
        	System.out.println(s);
    	} catch(Exception ex) {
    		ex.printStackTrace();
   		}
 	}
}

클라이언트는 java.rmi.Naminglookup 메소드를 이용한다. 메소드에 인자로 IP 주소 혹은 호스트명을 서비스 바인딩에 사용할 이름과 함께 전달한다.

lookup()의 과정을 그림으로 나타내면 위와 같다.

적용


이제 껌볼머신에 RMI를 적용할 차례이다.

서비스

우선 인터페이스부터 만들어보자.

import java.rmi.*;

public interface GumballMachineRemote extends Remote {
	public int getCount() throws RemoteException;
	public String getLocation() throws RemoteException;
	public State getState() throws RemoteException;
}

껌볼 기계 클래스가 Remote의 구현체가 되도록 인터페이스를 만들어주었다.

import java.io.*;

public interface State extends Serializable {
	public void insertQuarter();
	public void ejectQuarter();
	public void turnCrank();
	public void dispense();
}

다음으로, State 인터페이스를 Serializable하게 만들어주었다. 이제 State 구현체들을 문제 없이 네트워크를 통해 전달할 수 있게 되었다.

그런데, 한가지 문제가 있다. 상태 객체들은 모두 GumballMachine을 멤버로 갖는데, 우린 껌볼 기계가 통채로 네트워크를 통해서 전달되는 상황을 원하지 않는다!

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

이를 해결하기 위해 transient 키워드를 사용하여 JVM이 해당 필드를 직렬화(Serialize)하지 못하게 하자.

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

public class GumballMachine extends UnicastRemoteObject implements GumballMachineRemote {
	... 생략
    
	public GumballMachine(String location, int numberGumballs) throws RemoteException { 
    	... 
    }
    
	public int getCount() { return count; }
	public State getState() { return state; }
	public String getLocation() { return location; }
    
	... 생략
}

껌볼 머신 코드가 UnicastRemoteObject를 상속, GumballMachineRemote를 구현하도록 하고 생성자가 RemoteException을 던지도록 하는 점 외에는 변경사항은 없다.

public class GumballMachineTestDrive {

	public static void main(String[] args) {
    
		GumballMachineRemote gumballMachine = null;
		int count;
		if (args.length < 2) {
			System.out.println(GumballMachine <name> <inventory>);
			System.exit(1);
		}
		try {
			count = Integer.parseInt(args[1]);
            gumballMachine = new GumballMachine(args[0], count);
			Naming.rebind(//” + args[0] + “/gumballmachine”, gumballMachine);
		} catch (Exception e) {
			e.printStackTrace();
        }
	}
}

마지막으로 main 메소드에 껌볼 기계를 RMI에 등록하는 코드를 추가해주면 서비스측 수정이 모두 완료된다.

클라이언트

import java.rmi.*;

public class GumballMonitor {
	GumballMachineRemote machine;
    
	public GumballMonitor(GumballMachineRemote machine) {
		this.machine = machine;
	}
    
	public void report() {
		try {
			System.out.println(Gumball Machine:+ machine.getLocation());
			System.out.println(Current inventory:+ machine.getCount() + “ gumballs”);
			System.out.println(Current state:+ machine.getState());
		} catch (RemoteException e) {
			e.printStackTrace();
		}
    }
}

클라이언트에 해당하는 모니터 객체의 코드이다. 모니터의 report() 로직은 달라지지 않는다. 앞서 말한대로 헬퍼를 마치 실제 서비스 객체처럼 사용하기 때문이다. 다만, 그러기 위해 껌볼 머신 필드의 인스턴스 타입을 GumballMachineRemote 인터페이스로 바꿔주고, 예외처리를 위한 try-catch문을 추가해준다.

정리


프록시 패턴은 아래와 같이 정의한다

다른 객체에 대한 접근을 제어할 수 있는 대리자 혹은 플레이스 홀더를 제공하는 디자인 패턴

원격 프록시에서는 원격 객체에 대한 클라이언트의 접근을 제어하였는데, 어떤 객체에 대한 접근을 제어하는지에 따라 프록시는 여러 종류로 나뉜다.

대표적으로 생성 비용이 큰 리소스에 대한 접근을 제어하는 가상 프록시(Virtual Proxy), 접근 권한을 바탕으로 한 리소스에 대한 접근을 제어하는 보호 프록시(Protection Proxy) 등이 있다.

profile
우당탕탕 백엔드 생존기

0개의 댓글