프로그래밍 Design Pattern 이해하기 - 11 프록시 패턴
오브젝트에 대한 접근을 컨트롤한다 - 프록시 패턴
프록시 패턴을 줄여 이야기하면 "어떤 객체에 대한 접근을 제어하기 위한 용도로 대리인이나 대변인에게 해당하는 객체를 제공하는 패턴"이다.
지난 시간에 검볼 머신에 대해 예제를 통해 구현을 했다. 이번엔, 검볼 회사 CEO 인 마이티 검볼이 우리에게 요청했다. 검볼 머신에 대한 프로그래밍은 했으니 검볼의 상태나 액션에 대한 모니터링을 하고 싶다고! 우리는 이미 검볼의 숫자를 가져오는 함수(getCount())와 현재 상태를 가져오는 함수(getState())를 구현해놓았다. 우리는 굉장히 빠르게 CEO의 요구에 대응할 수 있다!
"Monitor"를 코딩해보자
public class GumballMachine {
String location;
public GumballMachine(String location, int count){
this.location = location;
}
public String getLocation(){
return location;
}
}
로케이션이란 변수는 String 객체다. 검볼머신에 대해 생성을 할 때 이 로케이션을 같이 선언해준다. 이번엔 검볼머신 모니터 클래스를 만들어보자.
public class GumeballMonitor {
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 GumeballMachine(args[0], count);
GumeballMonitor monitor = new GumballMonitor(gumballMachine);
monitor.report();
}
}
위의 모니터 코드는 보기엔 깔끔해보이지만, 뭔가 석연찮은 느낌이다. CEO는 검볼머신을 Remotely 하게 모니터링하고 싶다!
여기서 remote기 어떤 의미인가?
검볼머신 CEO는 로컬에 찍히는 로그가 아닌, 자신의 사무실의 자신의 책상에서 검볼머신들을 모니터링하고 싶어한다는 의미이다. 그래서 우리는 프록시를 통해 remote object를 제어해보려고 한다.
"remote proxy"의 역활
remote proxy는 local representative(로컬의 대표)로 remote object를 행동한다. remote object는 무엇인가? remote object는 Java Virtual Machine의 로컬 Heap 메모리가 아닌 다른 Heap 메모리에 상주하는 것을 이야기한다. 그러면 local representative는 무엇인가? local representative는 로컬메서드를 통해 불린 객체로 remote Heap에 올라가는 object를 의미한다. 다음 그림을 보면 조금 더 이해가 쉽다.
CEO의 데스크탑의 local heap 과 검볼머신에 탑재된 JVM위에 올라간 Remote Heap이 있다. Remote heap 에 올라간 Remote object가 있다. 클라이언트 코드에서는 remote method를 호출하게 되며, Local의 Proxy 객체를 통해 네트워킹을 하게 된다.
Gumball Machine에 remote porxy를 넣어보자!
먼저 우리는 RMI를 사용해야 한다. RIMI는 자바 원격 함수 호출로, 자바프로그램에서 객체간, 컴퓨터간 메서드를 호출할 수 있는 술이다.
자바 원격 함수 호출
자바 원격 함수 호출(Java Remote Method Invocation, Java RMI)는 자바 프로그램에서 각 객체간, 컴퓨터간 메서드를 호출할 수 있게 해주는 기술이다.
개요[편집]
자바만을 위한 최초의 프로토콜은 JRMP (Java Remote Method Protocol) 이었다. 이후 공통적인 객체를 호출하기 위해 CORBA(Common Object Request Broker Architecture)가 개발되었다. 이후 CORBA의 IIOP를 받아들여 RMI가 개발되었다. 현재 RMI-IIOP는 JRMP 구현과 그 인터페이스는 동일하지 않다.
자바 원격 함수 호출 API(Java RMI)는 자바 응용프로그램을 짜는 인터페이스이다. 이것은 공통적인 객체를 호출하기 위해 사용된다. 이API는 보통 두가지 실시방법이 있다. 최초의 실행방법은 Java Virtual Machine (JVM) 클래스 표현 구조를 의지한다. 그러므로 이방식은 한JVM에서 다른 JVM에로의 호출만 지원한다. 이런 자바에서만 실행되는 프로토콜은 Java Remote Method Protocol (JRMP)로 알려져있다. 코드가 JVM환경 밖에서도 운행시키기 위해 CORBA (Common Object Request Broker Architecture)가 개발되었다.
다른 추천하는 RMI의 버전은 Jini이다. 이것은 앞의것과 비슷하지만 더욱많은 찾기능력과 분산 오브젝트 애플리케이션 기법을 지원한다.
그리고 난 뒤 우리는 검볼머신에 해당 코드를 넣고, remote Service(원격으로 함수를 부르는 서비스)를 구현해야 한다.
프록시를 만들어 remote 검볼머신과 RMI를 이용해 연결한다
위 단계를 그림으로 그려보자.
Client helper(Proxy)를 만들어 서버와 통신을 담당하도록 한다. client object는 remote servic에 올라가 있는 client helper의 메서드를 호출한다. client helper가 remote service는 아니다. 비록 클라이언트 헬퍼가 remote service의 역활을 하는 것처럼 보이지만, client helper는 실제로 함수들이 동작하는 로직이나 함수들은 가지고 있지 않다. 대신에 클라이언트 헬퍼는 서버와 접촉하고 함수가 호출될 때 정보를 서버로 보내게 된다.
서버 사이드에서는, Service helper가 존재하며, 이 service helper가 전송받은 정보를 받는다. 받은 정보를 분석하고 real service object에 있는 함수를 호출하게 된다. 서비스 오브젝트에서 처리한 내용을 다시 service helper를 통해 패키징해서 client helper로 전달하게 되고, client helper는 이 정보를 풀어 client object로 전달한다.
Java RIM을 적용해보면, RMI의 좋은 점으로는, client helper 와 service hleper간의 네트워킹이나 입출력 코드를 작성할 필요가 없다는 것이다. RMI는 또 모든 런타임 인프라를 제공하며, remote object를 찾아서 연결할 수 있다. RMI call과 로컬 메소드들을 호출하는 것의 차이점은 하나다. 로컬에 있는 함수가 아닌 네트워크를 통해 메소드를 호출한다는 점이다. 네트워크 입출력이 실패했을 때의 리스크가 있다! 이는 조금 더 있다 이야길 하자.
원격서비스를 만드는 과정은 다음과 같다.
- 원격 인터페이스 만들기 (클라이언트에서 호출가능한 메소드를 정의)
- 서비스 구현 클래스 만들기 (실제 작업을 처리하는 클래스)
- rmic를 이용해 stub과 skeleton 만들기
- rmiregistry 실행 (클라이언트와 서비스 보조 객체 생성 - rmic툴을 이용해서 자동생성)
- 원격 서비스 시작 (서비스 객체가 시작되며, 서비스의 인스턴스를 만들고 rmi registry에 등록)
GumballMachineRemote.java
import java.rmi.*; public interface GumballMachineRemote extends Remote { public int getCount() throws RemoteException; public String getLocation() throws RemoteException; public State getState() throws RemoteException; }
State.java
이전 코드
public interface State { public void insertQuarter(); public void ejectQuarter(); public void turnCrank(); public void dispense();
}
바뀐 코드
import java.io.*; public interface State extends Serializable { public void insertQuarter(); public void ejectQuarter(); public void turnCrank(); public void dispense(); }
NoQuarterState.java
이전 코드
public class NoQuarterState implements State { GumballMachine gumballMachine; }
public class NoQuarterState implements State { transient GumballMachine gumballMachine; }
GumballMachine.java
이전 코드
public class GumballMachine {
public GumballMachine(String locatrion, int numberGumballs) { } public int getCount() { return count; } public State getState(){ return state; } public String getLocation(){ return location; } }
바뀐 코드
import java.rmi.*; import java.rmi.server; public class GumballMachine extends UnicastRemoteObject implements GumballMachineRemote { public GumballMachine(String locatrion, int numberGumballs) throws RemoteException { // 생성자코드 } public int getCount() { return count; } public State getState(){ return state; } public String getLocation(){ return location; } }
GumballMachineTestDrive.java
이전 코드
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(); }
}
바뀐코드
import java.rmi.*; 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(); } } }
GumballMonitor.java
이전 코드
public calss 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()); } }
바뀐 코드
import java.rmi.*; public calss 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(); } } }
The proxy pattern provides a surrogate or placeholder for another object to control access to it
프록시 패턴은 다른 곳에서 접근하고 제어하는 어떤 객체를 위한 대리 또는 자리이다.
가상프록시
가상 프록시는 실제로 진짜 객체가 필요하게 되기 전까지 객체의 생성을 미루는 기능으로 사용되거나, 객체 생성 전, 또는 생성 도중에 객체를 대신하기도 한다. 다음 예를 통해 이해를 해보자.
CD커버를 보여주는 프로그램을 만드는데, CD타이틀의 목록을 선택하면 네트워크를 통해 이미지를 불러오는 프로그램이다.
네트워크의 속도에 따라 불러오는 시간이 걸리므로, 이미지를 불러오는 동안 화면이 멈추거나 어플리케이션의 전체작동이 멈추어서는 안된다.
따라서 가상 프록시 역활을 하는 ImageProxy를 만든다.
- ImageProxy는 ImageIcon을 생성하고 URL을 통해 이미지를 불러온다.
- 이미지를 로딩하는 동안 "loding CD image...' 라는 메세지를 표ㅕ시한다.
- 이미지로딩이 끝나면, paintIcon(), getWidth(), getHeight()를 비롯한 메소드호출을 이미지 아이콘 클래스에 넘긴다.
- 사용자가 새로운 이미지를 요청하면 이미지프록시를 만들고 위 과정을 반복한다.
보호 프록시
이번에는 보호 프록시에 대해 알아보자. 보호 프록시는 접근 권한을 통해 객체에 대한 접근을 제어하는 프록시다. 자바에는 프록시 기능이 내장되어 있으며 java.lang.reflect 패키지를 이용해서 만들 수 있다. 구조는 다음과 같다.
이 보호프록시의 예제로는 결혼정보 서비스 프로그램이다. 이 서비스는 가입한 고객에 대해 자신의 정보는 수정이 가능하나, 타인의 정보는 수정하지 못하며, 타인의 선호도는 평가할 수 있으나, 자신에 대한 평가는 하지 못하는 프로그램이다.
PersonBea.java
은, setter와 getter메소드를 가지고 있으며, 이름, 성별, 취미, 선호도에 대한 변수가 있다.
아까의 구조를 따라 invocationHandler 를 만들어보자.
자기 자신의 정보에 접근할 때에는 OwnerInvocationHandler를, 타인의 정보일 때는 NonOwnerInvocationHandler 을 사용한다.
OwnerInvocationHandler
import java.lang.reflect.*; public class OwnerInvocationHandler implements InvocationHandler { PersonBean person; public OwnerInvocationHandler(PersonBean person) { 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")) { throw new IllegalAccessException(); } else if (method.getName().startsWith("set")) { return method.invoke(person, args); } } catch (InvocationTargetException e) { e.printStackTrace(); } return null; } }
PersonBean을 넣으면, get이나 set에 대한 메서드는 그대로 실행하지만, 자기자신에 대한 선호도를 호출하는 메소드는 실행시키지 않는다.
NonOwnerInvocationHandler.java
mport java.lang.reflect.*; public class NonOwnerInvocationHandler implements InvocationHandler { PersonBean person; public NonOwnerInvocationHandler(PersonBean person) { 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")) { return method.invoke(person, args); } else if (method.getName().startsWith("set")) { throw new IllegalAccessException(); } } catch (InvocationTargetException e) { e.printStackTrace(); } return null; } }
반대로 내정보가 아닌 경우에는 set을 막아 수정을 막는다.
TestDrive
import java.lang.reflect.*; import java.util.*; public class MatchMakingTestDrive { Hashtable datingDB = new Hashtable(); public static void main(String[] args) { MatchMakingTestDrive test = new MatchMakingTestDrive(); test.drive(); } public MatchMakingTestDrive() { initializeDatabase(); } public void drive() { PersonBean joe = getPersonFromDatabase("Joe Javabean"); PersonBean ownerProxy = getOwnerProxy(joe); System.out.println("Name is " + ownerProxy.getName()); ownerProxy.setInterests("bowling, Go"); System.out.println("Interests set from owner proxy"); try { ownerProxy.setHotOrNotRating(10); } catch (Exception e) { System.out.println("Can't set rating from owner proxy"); } System.out.println("Rating is " + ownerProxy.getHotOrNotRating()); PersonBean nonOwnerProxy = getNonOwnerProxy(joe); System.out.println("Name is " + nonOwnerProxy.getName()); try { nonOwnerProxy.setInterests("bowling, Go"); } catch (Exception e) { System.out.println("Can't set interests from non owner proxy"); } nonOwnerProxy.setHotOrNotRating(3); System.out.println("Rating set from non owner proxy"); System.out.println("Rating is " + nonOwnerProxy.getHotOrNotRating()); } 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)); } PersonBean getPersonFromDatabase(String name) { return (PersonBean)datingDB.get(name); } void initializeDatabase() { PersonBean joe = new PersonBeanImpl(); joe.setName("Joe Javabean"); joe.setInterests("cars, computers, music"); joe.setHotOrNotRating(7); datingDB.put(joe.getName(), joe); PersonBean kelly = new PersonBeanImpl(); kelly.setName("Kelly Klosure"); kelly.setInterests("ebay, movies, music"); kelly.setHotOrNotRating(6); datingDB.put(kelly.getName(), kelly); } }
위와 같이 PersonBean을 만든 후, 맞는 프록시를 만들고 호출하면 된다.