프록시 패턴 알아보기 (+ Java의 InvocationHandler)

rvlwldev·2023년 2월 8일
0

CS

목록 보기
5/12

프록시패턴의 정의는 디자인 패턴 중 하나이며 일반적으로 다른 무언가와 이어지는 인터페이스의 역할을 하는 클래스이다. 프록시는 어떠한 것(이를테면 네트워크 연결, 메모리 안의 커다란 객체, 파일, 또 복제할 수 없거나 수요가 많은 리소스)과도 인터페이스의 역할을 수행할 수 있다.

쉽게 말해서 프록시는 대리자라는 뜻을 가지고 있는 만큼 인터페이스를 활용하여 클래스에 접근하지 않고 어떤 것을 대신 처리해준다고 생각할 수 있다. 때문에 기존 클래스의 코드를 변경하지 않고 흐름을 제어하거나 기능(ex. 로깅, 시간측정, 캐싱 등)을 추가할 수 있기 때문에 객체가 해야할 일만 할 수 있게, 또는 새로운 기능을 객체지향적으로 구현할 수 있다.

프록시 패턴의 종류

1. 용량이 큰 리소스가 로딩되기 전, 프록시를 통해 참조될 수 있다. (가상프록시)

용량이 크면 로딩 시간이 길어질 수 있다.
예시로 용량이 큰 여러개의 이미지들을 반환한다고 할 때, 프록시 객체를 활용해서 실제로 이미지를 보여주는 시점에서만 로딩을 시작하여 대기시간을 줄일 수 있다.

예시 소스코드

Image Interface

Public interface Image {
	BufferedImage getImage();
}

Image Implements Class

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;
    }
}

Proxy Class

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)이라고도 한다.


2. 어떤 리소스에 대해 접근을 제어할 때 사용될 수 있다. (보호 프록시)

기존 클래스의 코드변경 없이 전처리 등의 기능을 추가할 수 있다는 장점을 살려 클라이언트의 상태 또는, 자격이나 권한이 충족되는 경우에만 실제 객체에게 요청을 전달할 수 있게 구현할 수 있다.

Java에서는 InvocationHandler 인터페이스를 활용해서 구현한다면, 실행중에 프록시 객체가 생성되므로 동적프록시(Dynamic Proxy)라고도 불린다.

간단하게는 위 예시의 Proxy Class에서 getImage에 if문 등을 추가해서 ImageImpl객체에 접근을 제어할 수 있다.

2-1. Java의 InvocationHandler 인터페이스를 활용하는 방법

이 방법을 활용하면 예제처럼 프록시 패턴을 적용 시키기 위해 인터페이스를 직접 구현할 필요가 없어진다. 또한 인터페이스를 구현했기 때문에 인터페이스의 모든 메소드를 Override 해야하기 때문에 중복이 발생할 수 있다. InvocationHandler를 활용하면 동적으로 프록시 객체가 생성되므로 위 두가지 문제를 해결할 수 있다.

예시 소스코드

interface

public interface Greetings {
	String whenMorning(String sentence);
    String whenAfternoon(String sentence);
    String whenEvening(String sentence);
}

implementation

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 인터페이스를 구현한 객체 (예시에서는 toUpperCaseHandler)

    InvocationHandler란?
    invoke() 메소드 하나만 가지는 인터페이스
    이 메소드는 Proxy.newProxyInstance에 의해 동적으로 생성된 프록시 객체의 메소드가 호출되면 동작하는 메소드이다
    각각, 또는 전체의 메소드의 확장 기능을 구현할 수 있고, 호출된 메소드 정보와 파라미터를 파라미터로 받는다.
    즉, 프록시 대상이 되는 인터페이스 각각의 메소드의 사용될 확장기능 코드가 반복되지 않게 구현할 수 있다.

InvocationHandler는 아래처럼 구현할 수 있다.

toUpperCaseHandler

// 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;
    }
}

3. 데이터를 공유해야 하는 경우 (원격 프록시)

프록시 클래스를 구현함으로써 프록시 클래스를 로컬에 두고 실제 클래스를 Remote하게 구현할 경우 데이터를 효율적으로 공유할 수 있다.

가장 쉬운 예시중 하나로 온라인 게임을 예시로 들 수가 있다.
온라인 게임의 경우 게임을 이용하는 사용자(클라이언트) 각각의 캐릭터를 프록시 클래스로 구현하여 서버에 있는 실제 캐릭터 클래스의 특정메소드를 실행하는 방식으로 구현할 수 있다.

  • 더 쉽게 정리하자면
      1. A사용자(클라이언트)가 로컬환경에서 존재하는 A캐릭터 프록시객체를 앞으로 이동
      1. A사용자의 A프록시캐릭터객체는 서비스 보조객체를 통해 서버에 있는 실제 A캐릭터 객체의 앞으로 이동하는 함수를 호출 (Remote)
      1. 서버의 실제 A캐릭터의 위치 데이터가 변경됨으로
      1. 서버를 수신하고 있는 모든 사용자가 A의 캐릭터가 앞으로 이동하는 것을 볼 수 있음.

즉, 원격 프록시 패턴이란 클라이언트는 실제 서비스의 함수를 호출하고 프록시 객체에서 자신이 원하는 작업을 처리한다고 생각하지만,
실제로는 프록시객체가 서비스보조객체에게 요청을 보내고 서비스보조객체가 요청을 해석 후
실제 서비스 객체가 해석된 요청을 처리하는 패턴이다.

이 원격프록시 패턴을 사용하지 않는다면,
A사용자가 자신의 캐릭터를 움직이는 함수를 호출했을때
서버에서 A사용자 이외의 모든 사용자에게 함수를 호출해서 처리하라는 명령을 내려야 할 것이다.


프록시 패턴의 단점

1. 가독성

  • 프록시 패턴 적용 시 위에 예시들처럼 사용하지 않을 때 보다 중간에 하나의 로직이 더 추가되므로 코드가 길어지고 코드가 난해해질 가능성이 있다.

2. 성능저하

  • 객체 생성 시 한 단계의 더 거치게 되므로 빈번한 객체 생성이나, 반복적인 로직이 적용 된다면 프록시 패턴의 적용 전보다 성능의 저하를 불러올 수 있다.
  • 만약 프록시 객체에서 스레드 생성, 동기화가 구현되어야 하는 경우에도 성능이 저하될 수 있다.

0개의 댓글