반응형
프록시 패턴이란?
- 프록시 패턴의 특정 객체로의 접근을 제어하는 대리인을 제공하는 디자인패턴입니다.
- 프록시 패턴을 사용하면 생성하기 힘든 객체 또는 보안이 중요한 객체와 같이 접근을 제어하는 대리인 객체를 만들 수 있습니다.
- 프록시 패턴을 사용하게 되면 가상 프록시를 사용해서 생성하기 힘든 자원으로의 접근을 제어할 수 있으며 보호 프록시를 사용해서 접근 권한이 필요한 자원의 접근을 제어할 수 있습니다.
프록시 패턴의 종류
1. 원격 프록시
- 원격 프록시는 원격 개체의 로컬 대변자 역할을 수행하는것을 의미합니다.
- 로컬 대변자란 어떤 메소드를 호출하면 다른 원격 객체에게 그 메소드 호출을 전달해주는 객체를 로컬 대변자라고 합니다.
- 클라이언트 객체는 원격 객체가 가지고 있는 메소드를 호출 하는것처럼 행동합니다. 하지만 실제로로 로컬 힙에 들어있는 ‘프록시’ 객체의 메소드를 호출합니다.
- 네트워크 통신과 관련된 저수준 작업은 위의 프록시 객체에서 처리해준다.
원격 프록시 구현
public interface GumballMachineRemote extends Remote {
public int getCount() throws RemoteException;
public String getLocation() throws RemoteException;
public State getState() throws RemoteException;
}
- 예시는 뽑기 기계를 원격으로 관찰하는 시스템을 만든다는 흐름으로 예시를 만들어보도록 하겠습니다.
- 먼저 GambleMachine을 관찰하기 위해 클라이언트가 전달할 행동들을 인터페이스화는 작업을 진행하겠습니다.
- 위의 인터페이스를 통해 GambleMachine을 호출하는 행동들을 정의한것입니다.
public interface State extends Serializable {
public void insertQuarter(); //동전 투입
public void ejectQuarter(); // 동전 반환
public void turnCrank(); //손잡이 회전
public void dispense(); //알맹이 반환
}
- GambleMachine의 상태값을 가지고 있는 State 인터페이스입니다.
- 원격 객체에게 데이터를 전달할 때 데이터가 직렬화되어야 하므로 Serializable을 상속받습니다.
package com.example.designpattern.proxy.gumball;
public class NoQuarterState implements State {
private static final long serialVersionUID = 2L;
//State 상태를 직렬화 하기위해 GumballMachine 자체를 직렬화하는것은 비효율적이기 때문에 Transient를 선언한다.
//Transient를 선언하게 되면 해당 필드는 직렬화하지 않는다.
//하지만 객체를 직렬화한 후에 해당 객체를 호춣하면 에러가 발생할 수 있다.
transient GumballMachine gumballMachine;
public NoQuarterState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
public void insertQuarter() {
System.out.println("You inserted a quarter");
gumballMachine.setState(gumballMachine.getHasQuarterState());
}
public void ejectQuarter() {
System.out.println("You haven't inserted a quarter");
}
public void turnCrank() {
System.out.println("You turned, but there's no quarter");
}
public void dispense() {
System.out.println("You need to pay first");
}
public String toString() {
return "waiting for quarter";
}
}
- 동전을 보유하지 않은 상태를 의미하는 NoQuarterState 객체이다.
- GumballMachine은 상태 패턴을 통해 구현되었기 때문에 객체의 상태에 맞게 등록된 행동이 수행된다.
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
//원격 통신을 위한 java의 unicastRemoteObject를 상속받는다.
public class GumballMachine
extends UnicastRemoteObject implements GumballMachineRemote
{
private static final long serialVersionUID = 2L;
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;
State winnerState;
State state = soldOutState;
int count = 0;
String location;
//슈퍼 클래스에서 RemoteException을 던질수 있으므로 해당 생성자에도 RemoteException을 던질수 있어야한다.
public GumballMachine(String location, int numberGumballs) throws RemoteException {
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
soldState = new SoldState(this);
winnerState = new WinnerState(this);
this.count = numberGumballs;
if (numberGumballs > 0) {
state = noQuarterState;
}
this.location = location;
}
public void insertQuarter() {
state.insertQuarter();
}
public void ejectQuarter() {
state.ejectQuarter();
}
public void turnCrank() {
state.turnCrank();
state.dispense();
}
void setState(State state) {
this.state = state;
}
void releaseBall() {
System.out.println("A gumball comes rolling out the slot...");
if (count != 0) {
count = count - 1;
}
}
public void refill(int count) {
this.count = count;
state = noQuarterState;
}
public int getCount() {
return count;
}
public State getState() {
return state;
}
public String getLocation() {
return location;
}
public State getSoldOutState() {
return soldOutState;
}
public State getNoQuarterState() {
return noQuarterState;
}
public State getHasQuarterState() {
return hasQuarterState;
}
public State getSoldState() {
return soldState;
}
public State getWinnerState() {
return winnerState;
}
public String toString() {
StringBuffer result = new StringBuffer();
result.append("\nMighty Gumball, Inc.");
result.append("\nJava-enabled Standing Gumball Model #2014");
result.append("\nInventory: " + count + " gumball");
if (count != 1) {
result.append("s");
}
result.append("\n");
result.append("Machine is " + state + "\n");
return result.toString();
}
}
- 원격 통신을 응답받는 GumbleMachine 객체 구현입니다.
import java.rmi.RemoteException;
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();
}
}
}
- GumballMachine 정보를 파악하기 위한 원격 모니터링 객체 구현입니다.
테스트
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();
}
}
}
//terminal
//rmiregistry를 먼저 실행
>> rmiregistry
//austin지역의 100개의 볼을 생성
>> java GuballMachineTestDrive austin.mightygumball.com 100
모니터링 테스트 코드
import java.rmi.Naming;
public class GumballMonitorTestDrive {
public static void main(String[] args) {
//모니터링할 위치
String[] location = {"rmi://santafe.mightygumball.com/gumballmachine",
"rmi://boulder.mightygumball.com/gumballmachine",
"rmi://austin.mightygumball.com/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();
}
}
}
2.가상 프록시(보호 프록시)
- 가상 프록시는 객체를 생성하는데 많은 비용이 드는 객체를 대신 생성하는 프록시 패턴입니다.
- 진짜 객체의 사용 호출을 호출 시점까지 최대한 미루는것을 통해 성능적인 부분에서 이점을 가져올수 있다.
가상 프록시 구현
가상 프록시 이해를 위한 간단한 문제 제안
- 뮤직 서비스에서 음악을 조회할때 앨범의 커버 이미지를 가져옵니다.
- 앨범을 조회할 때까지는 앨범 커버 이미지를 불러올 필요가 없습니다. 그러나 앨범 커머 이미지를 가져오는데 다른 이미지를 사용자에게 보여줘야 합니다.
- 가상 프록시가 앨범 이미지를 가져오는동안 가상 프록시를 활용하여 “앨범 커버 이미지를 불러오는중입니다” 라는 이미지를 보여주는 예제를 만들어보도록 하겠습니다
가상 프록시 설계하기
가상 프록시 구현
public interface Icon {
void paintIcon(Component var1, Graphics var2, int var3, int var4);
int getIconWidth();
int getIconHeight();
}
- java에서 제공하는 Icon인터페이스 입니다.
class ImageProxy implements Icon {
//ImageIcon은 이미지 로딩이 끝났을 때 실제 이미지를 화면에 표시하는 아이콘 객체입니다.
//volatile을 사용해서 멀티 스레드 작업시 읽기를 보호하는 작업을 수행함
volatile ImageIcon imageIcon;
final URL imageURL;
Thread retrievalThread;
boolean retrieving = false;
public ImageProxy(URL url) { imageURL = url; }
//ImageIcon 로딩이 끝나기 전까지는 기본 높이 800를 리턴합니다.
public int getIconWidth() {
if (imageIcon != null) {
return imageIcon.getIconWidth();
} else {
return 800;
}
}
//ImageIcon 로딩이 끝나기 전까지는 기본 너비 600을 리턴합니다.
public int getIconHeight() {
if (imageIcon != null) {
return imageIcon.getIconHeight();
} else {
return 600;
}
}
//ImageIcon은 2개의 서로 다른 스레드에서사용합니다. 따라선 ImageIcon 변수를 volatile로 선언합니다.
//volatile을 사용해서 읽기를 보호하고 쓰기를 보호하기 위해 Setter메소드에 synchronized를 선언합니다.
synchronized void setImageIcon(ImageIcon imageIcon) {
this.imageIcon = imageIcon;
}
public void paintIcon(final Component c, Graphics g, int x, int y) {
if (imageIcon != null) {
imageIcon.paintIcon(c, g, x, y);
} else {
g.drawString("Loading album cover, please wait...", x+300, y+190);
if (!retrieving) {
retrieving = true;
retrievalThread = new Thread(new Runnable() {
public void run() {
try {
setImageIcon(new ImageIcon(imageURL, "Album Cover"));
//비동기 방식으로 계속 화면을 재갱신하여 imageurl을 계속 찾는 작업 수행
c.repaint();
} catch (Exception e) {
e.printStackTrace();
}
}
});
retrievalThread.start();
}
}
}
}
- ImageProxy는 Icon 인터페이스를 구현합니다.
class ImageComponent extends JComponent {
private static final long serialVersionUID = 1L;
private Icon icon;
public ImageComponent(Icon icon) {
this.icon = icon;
}
public void setIcon(Icon icon) {
this.icon = icon;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
int w = icon.getIconWidth();
int h = icon.getIconHeight();
int x = (800 - w)/2;
int y = (600 - h)/2;
icon.paintIcon(this, g, x, y);
}
}
- Pannel 정보를 담고있는 ImageComponent 구현입니다
테스트
public class ImageProxyTestDrive {
ImageComponent imageComponent;
JFrame frame = new JFrame("Album Cover Viewer");
JMenuBar menuBar;
JMenu menu;
Hashtable<String, String> albums = new Hashtable<String, String>();
public static void main (String[] args) throws Exception {
ImageProxyTestDrive testDrive = new ImageProxyTestDrive();
}
public ImageProxyTestDrive() throws Exception {
albums.put("Buddha Bar","http://images.amazon.com/images/P/B00009XBYK.01.LZZZZZZZ.jpg");
albums.put("Ima","http://images.amazon.com/images/P/B000005IRM.01.LZZZZZZZ.jpg");
albums.put("Karma","http://images.amazon.com/images/P/B000005DCB.01.LZZZZZZZ.gif");
albums.put("MCMXC a.D.","http://images.amazon.com/images/P/B000002URV.01.LZZZZZZZ.jpg");
albums.put("Northern Exposure","http://images.amazon.com/images/P/B000003SFN.01.LZZZZZZZ.jpg");
albums.put("Selected Ambient Works, Vol. 2","http://images.amazon.com/images/P/B000002MNZ.01.LZZZZZZZ.jpg");
URL initialURL = new URL((String)albums.get("Selected Ambient Works, Vol. 2"));
menuBar = new JMenuBar();
menu = new JMenu("Favorite Albums");
menuBar.add(menu);
frame.setJMenuBar(menuBar);
for (Enumeration<String> e = albums.keys(); e.hasMoreElements();) {
String name = (String)e.nextElement();
JMenuItem menuItem = new JMenuItem(name);
menu.add(menuItem);
menuItem.addActionListener(event -> {
imageComponent.setIcon(new ImageProxy(getAlbumUrl(event.getActionCommand())));
frame.repaint();
});
}
// set up frame and menus
Icon icon = new ImageProxy(initialURL);
imageComponent = new ImageComponent(icon);
frame.getContentPane().add(imageComponent);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(800,600);
frame.setVisible(true);
}
URL getAlbumUrl(String name) {
try {
return new URL((String)albums.get(name));
} catch (MalformedURLException e) {
e.printStackTrace();
return null;
}
}
}
가상 프록시 패턴과 데코레이터 패턴의 차이점
- 가상 프록시는 수행 결과를 먼저 받는 데코레이터 아니냐는 의구심이 생길수있습니다.
- 하지만 가상 프록시 패턴과 데코레이터 패턴은 용도로 구분이 가능합니다.
- 데코레이터 패턴은 새로운 행동을 추가하는 용도로 사용되지만 가상 프록시패턴은 어떤 클래스로 접근을 하는지 제어하는 용도로 사용됩니다.
3.보호 프록시
- 자바 기술중 객체가 실행중에 생성되도록 구현해주는 기술인 동적 프록시가 존재합니다.
- 동적 프록시는 위와 같이 동작합니다.
- 이러한 동적프록시를 활용해서 보호프록시를 구현해보도록하겠습니다.
- 보호 프록시란 접근 권한을 바탕으로 객체로의 접근을 제어하는 프록시입니다.
보호 프록시 구현
보호 프록시 이해를 위한 간단한 문제 제안
- 데이팅 서비스를 개발하려고합니다.
- 데이팅 서비스는 서로 상대방의 괴짜 지수(좋은 쪽으로 얼마나 괴짜인지를 따지는 점수)를 매기는 기능을 사용하여 사용자들이 적극적으로 데이트 매칭이 이뤄질수 있도록 유도할려고합니다.
- 이때 사용자들이 서로 자신의 괴짜 지수를 함부로 바꾸지 못하게 보호 프록시를 사용해서 구현해보도록 하겠습니다.
public interface Person {
String getName();
String getGender();
String getInterests();
int getGeekRating();
void setName(String name);
void setGender(String gender);
void setInterests(String interests);
void setGeekRating(int rating);
}
- 사람에 대한 인터페이스 정의입니다.
- setGeekRating은 점수를 인자로 받아 해당 사람의 괴짜 지수를 이동 평균 방식으로 계산합니다.
public class PersonImpl implements Person {
String name;
String gender;
String interests;
int rating;
int ratingCount = 0;
public String getName() {
return name;
}
public String getGender() {
return gender;
}
public String getInterests() {
return interests;
}
public int getGeekRating() {
if (ratingCount == 0) return 0;
return (rating/ratingCount);
}
public void setName(String name) {
this.name = name;
}
public void setGender(String gender) {
this.gender = gender;
}
public void setInterests(String interests) {
this.interests = interests;
}
public void setGeekRating(int rating) {
this.rating += rating;
ratingCount++;
}
}
- Person 인터페이스에대한 구현입니다.
public class OwnerInvocationHandler implements InvocationHandler {
Person person;
public OwnerInvocationHandler(Person person) {
this.person = person;
}
//InvocationHandler가 제공하는 Invoke 메소드
//Proxy는 호출하는 Proxy 자체 오브젝트를 의미, Method는 사용되어지 Method를 의미, args는 넘어온 매개변수의 정보를 의미
public Object invoke(Object proxy, Method method, Object[] args)
throws IllegalAccessException {
try {
//메소드 이름별로 수행 분기 로직
if (method.getName().startsWith("get")) {
return method.invoke(person, args);
//자신 계정의 GeekRating은 커스터 마이징 하면안되므로 접근시 예외처리 작업 수행
} else if (method.getName().equals("setGeekRating")) {
throw new IllegalAccessException();
} else if (method.getName().startsWith("set")) {
return method.invoke(person, args);
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
- 자신 계정의 소유권을 의미하는 Proxy Handler인 OwnerInvoactionHandelr를 구현합니다.
public class NonOwnerInvocationHandler implements InvocationHandler {
Person person;
public NonOwnerInvocationHandler(Person 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("setGeekRating")) {
return method.invoke(person, args);
//소유권이 없으므로 객체의 set 작업은 모두 막아야함.
} else if (method.getName().startsWith("set")) {
throw new IllegalAccessException();
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
- 소유권이 없는 경우 InvocationHandelr을 구현합니다.
테스트
public class MatchMakingTestDrive {
HashMap<String, Person> datingDB = new HashMap<String, Person>();
public static void main(String[] args) {
MatchMakingTestDrive test = new MatchMakingTestDrive();
test.drive();
}
public MatchMakingTestDrive() {
initializeDatabase();
}
public void drive() {
Person joe = getPersonFromDatabase("Joe Javabean");
Person ownerProxy = getOwnerProxy(joe);
System.out.println("Name is " + ownerProxy.getName());
ownerProxy.setInterests("bowling, Go");
System.out.println("Interests set from owner proxy");
try {
ownerProxy.setGeekRating(10);
} catch (Exception e) {
System.out.println("Can't set rating from owner proxy");
}
System.out.println("Rating is " + ownerProxy.getGeekRating());
Person 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.setGeekRating(3);
System.out.println("Rating set from non owner proxy");
System.out.println("Rating is " + nonOwnerProxy.getGeekRating());
}
Person getOwnerProxy(Person person) {
//Proxy 클래스에 있는 newProxyInstance를 사용하여 Proxy를 생성한다. 사용할 객체의 클래스. 인터페이스, InvocationHandler 정보를 매개변수 주고 생성한다.
return (Person) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new OwnerInvocationHandler(person));
}
Person getNonOwnerProxy(Person person) {
return (Person) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new NonOwnerInvocationHandler(person));
}
Person getPersonFromDatabase(String name) {
return (Person)datingDB.get(name);
}
void initializeDatabase() {
Person joe = new PersonImpl();
joe.setName("Joe Javabean");
joe.setInterests("cars, computers, music");
joe.setGeekRating(7);
datingDB.put(joe.getName(), joe);
Person kelly = new PersonImpl();
kelly.setName("Kelly Klosure");
kelly.setInterests("ebay, movies, music");
kelly.setGeekRating(6);
datingDB.put(kelly.getName(), kelly);
}
}
프록시 패턴 UML
- UML은 크게 Subject, RealSubject, Proxy 3가지로 이루어집니다.
- RealSubject, Proxy 모두 Subject를 상속받아서 정의합니다.
- 진짜 작업은 RealSubject가 수행합니다. 그러나 Client는 Proxy를 사용해서 RealSubject를 호출합니다.
- Proxy는 객체의 대변인 역할을 하며 실제 객체를 수행해야할 때는 RealSubject의 레퍼런스를 사용해서 호출합니다.
- 이러한 형식을 통해 접근자가 RealSubject의 호출을 시도할 때 Proxy에서 검증하여 접근자를 차단하고 허용하는 작업을 수행할 수 있습니다.
정리
- 프록시 패턴은 특정 객체로의 접근을 제어하는 대리인을 제공합니다.
- 원격 프록시는 클라이언트와 원격 객체 사이의 데이터 전달을 관리합니다.
- 가상 프록시는 인스턴스를 만드는데 많은 비용이 드는 객체로의 접근을 제어합니다.
- 보호 프록시는 호출하는 쪽의 권한에 따라서 객체에 있는 메소드로의 접근을 제어합니다.
- 자바에 내장된 프록시 지원 기능을 사용하여 동적 프록시 클래스를 만들어서 원하는 핸들러에서 호출을 처리하도록 할 수 있습니다.
반응형
'디자인 패턴' 카테고리의 다른 글
CHAPTER 13.실전 디자인 패턴 (0) | 2023.05.06 |
---|---|
CHAPTER 12.복합 패턴 (0) | 2023.05.06 |
CHAPTER 10.상태 패턴 (0) | 2023.05.06 |
CHAPTER 09.반복자 패턴과 컴포지트 패턴 (0) | 2023.05.06 |
CHAPTER 08.템플릿 메소드 패턴 (0) | 2023.05.06 |