프록시패턴의 정의는 디자인 패턴 중 하나이며 일반적으로 다른 무언가와 이어지는 인터페이스의 역할을 하는 클래스이다. 프록시는 어떠한 것(이를테면 네트워크 연결, 메모리 안의 커다란 객체, 파일, 또 복제할 수 없거나 수요가 많은 리소스)과도 인터페이스의 역할을 수행할 수 있다.
쉽게 말해서 프록시는 대리자라는 뜻을 가지고 있는 만큼 인터페이스를 활용하여 클래스에 접근하지 않고 어떤 것을 대신 처리해준다고 생각할 수 있다. 때문에 기존 클래스의 코드를 변경하지 않고 흐름을 제어하거나 기능(ex. 로깅, 시간측정, 캐싱 등)을 추가할 수 있기 때문에 객체가 해야할 일만 할 수 있게, 또는 새로운 기능을 객체지향적으로 구현할 수 있다.
용량이 크면 로딩 시간이 길어질 수 있다.
예시로 용량이 큰 여러개의 이미지들을 반환한다고 할 때, 프록시 객체를 활용해서 실제로 이미지를 보여주는 시점에서만 로딩을 시작하여 대기시간을 줄일 수 있다.
Public interface Image {
BufferedImage getImage();
}
public class ImageImpl implements Image {
private BufferedImage image;
ImageImpl(String filePath) {
File imageFile = new File(filePath);
this.image = ImageIO.read(imageFile);
}
@Override
public BufferedImage getImage() {
return this.image;
}
}
public class ProxyImage implements Image {
private String filePath;
private ImageImpl image;
ProxyImage(String filePath) {
this.filePath = filePath;
}
@Override
public BufferedImage getImage() {
if(this.image == null) this.image = new ImageImpl(filePath);
return this.image.getImage();
}
}
위 세개의 예시코드를 살펴보면 ProxyImage 클래스는 생성 시
이미지파일을 불러오기 위한 최소 정보인 filePath만 가지고 있고
ImageImpl 클래스는 생성과 동시에 실제 이미지를 불러온다.
ProxyImage 클래스를 활용하여 getImage 메소드를 실행할 때 마다 실제 이미지 파일을 로드할 수 있게 구현할 수 있다.
예시)
... (기타 코드) ...
ProxyImage image1 = new ProxyImage("./image1.png");
ProxyImage image2 = new ProxyImage("./image2.png");
ProxyImage image3 = new ProxyImage("./image3.png");
...
public BufferedImage userClick1 () {
return image1.getImage();
}
public BufferedImage userClick2 () {
return image2.getImage();
}
...
위 예시처럼 활용할 때, 사용자의 행동에 따라 보여지는 이미지가 다르게 보여야 된다면,
프록시 클래스를 통해서 사용자가 모든 행동에 따라 보여지는 모든 이미지를 모두 불러오는 대신 이미지가 보여야 될 때만 실제 이미지파일을 불러오는 방식으로 구현할 수 있다.
이러한 방식을 지연로딩(Lazy Loading)이라고도 한다.
기존 클래스의 코드변경 없이 전처리 등의 기능을 추가할 수 있다는 장점을 살려 클라이언트의 상태 또는, 자격이나 권한이 충족되는 경우에만 실제 객체에게 요청을 전달할 수 있게 구현할 수 있다.
Java에서는 InvocationHandler 인터페이스를 활용해서 구현한다면, 실행중에 프록시 객체가 생성되므로 동적프록시(Dynamic Proxy)라고도 불린다.
간단하게는 위 예시의 Proxy Class에서 getImage에 if문 등을 추가해서 ImageImpl객체에 접근을 제어할 수 있다.
이 방법을 활용하면 예제처럼 프록시 패턴을 적용 시키기 위해 인터페이스를 직접 구현할 필요가 없어진다. 또한 인터페이스를 구현했기 때문에 인터페이스의 모든 메소드를 Override 해야하기 때문에 중복이 발생할 수 있다. InvocationHandler를 활용하면 동적으로 프록시 객체가 생성되므로 위 두가지 문제를 해결할 수 있다.
public interface Greetings {
String whenMorning(String sentence);
String whenAfternoon(String sentence);
String whenEvening(String sentence);
}
public class GreetingsImpl implements Greetings {
private String name;
GreetingsImpl (String name) {
this.name = name;
}
@Override
public String whenMorning (String sentence) {
return sentence.toUpperCase() + ", " + this.name.toUpperCase();
}
@Override
public String whenAfternoon (String sentence) {
return sentence.toUpperCase() + ", " + this.name.toUpperCase();
}
@Override
public String whenEvening (String sentence) {
return sentence.toUpperCase() + ", " + this.name.toUpperCase();
}
}
위 예시코드에서 문장을 단순히 대문자로 바꿔주기 때문에 메소드를 분리하는 방법을 고려해볼 수 있겠으나, whenMorning메소드만 필요하다거나 Greetings인터페이스가 여러곳에서 구현이 필요하다면 중복된 코드를 줄이기는 곤란해진다.
이 경우 Java.lang.reflect.Proxy 클래스의 newProxyInstance 메소드를 활용을 고려해볼 수 있다.
// 선언 예시
Greetings proxyGreeting = (Greetings) Proxy.newProxyInstance(
Greetings.class.getClassLoader(),
new Class[] {Greetings.class},
new toUpperCaseHandler(new GreetingImpl("Karina")));
InvocationHandler란?
invoke() 메소드 하나만 가지는 인터페이스
이 메소드는 Proxy.newProxyInstance에 의해 동적으로 생성된 프록시 객체의 메소드가 호출되면 동작하는 메소드이다
각각, 또는 전체의 메소드의 확장 기능을 구현할 수 있고, 호출된 메소드 정보와 파라미터를 파라미터로 받는다.
즉, 프록시 대상이 되는 인터페이스 각각의 메소드의 사용될 확장기능 코드가 반복되지 않게 구현할 수 있다.
InvocationHandler는 아래처럼 구현할 수 있다.
// GreetingsImpl의 toUpperCase() 반복을 줄이는 활용 예시
public class toUpperCaseHandler implements InvocationHandler {
Greetings target;
toUpperCaseHandler(Greetings target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(target, args);
if (result instanceof String && method.getName().startsWith("when")) {
return result.toString().toUpperCase();
}
return null;
}
}
프록시 클래스를 구현함으로써 프록시 클래스를 로컬에 두고 실제 클래스를 Remote하게 구현할 경우 데이터를 효율적으로 공유할 수 있다.
가장 쉬운 예시중 하나로 온라인 게임을 예시로 들 수가 있다.
온라인 게임의 경우 게임을 이용하는 사용자(클라이언트) 각각의 캐릭터를 프록시 클래스로 구현하여 서버에 있는 실제 캐릭터 클래스의 특정메소드를 실행하는 방식으로 구현할 수 있다.
즉, 원격 프록시 패턴이란 클라이언트는 실제 서비스의 함수를 호출하고 프록시 객체에서 자신이 원하는 작업을 처리한다고 생각하지만,
실제로는 프록시객체가 서비스보조객체에게 요청을 보내고 서비스보조객체가 요청을 해석 후
실제 서비스 객체가 해석된 요청을 처리하는 패턴이다.
이 원격프록시 패턴을 사용하지 않는다면,
A사용자가 자신의 캐릭터를 움직이는 함수를 호출했을때
서버에서 A사용자 이외의 모든 사용자에게 함수를 호출해서 처리하라는 명령을 내려야 할 것이다.