[Java] 자바 동적 프록시(Dynamic Proxy) 구현하기

아두치·2021년 3월 15일
2

What is that?


프록시(Proxy)의 사전적 의미는 대리인을 의미한다.
프록시라는 말은 IT분야에서만 사용하는 IT용어가 아니라 이미 실생활에서 흔히 사용되고 있는 개념이다.
언제 어디서 어떻게 사용되고 있는지 한번 알아보도록 하자.

CASE 1

나는 원래 부산에 살고 있었다.
취업을 하기 위해 지원서를 여기저기 넣었는데 개발 회사가 수도권에 밀집되어 있다보니 가장 먼저 면접통보가 온 곳은 서울이였다.
면접결과는 합격이였고, 첫 출근까지 나에게 남은 시간은 단 일주일이였다.
일주일 안에 집을 구해야하는 상황이라 우선 월세부터 얻고 1년뒤 계약이 끝나면 전세로 전환하기로 했다.
KTX를 타고 서울로 와서 회사가 있는 동네의 부동산을 찾아갔다.
나는 내가 원하는 조건을 부동산에 제시했고, 부동산은 내 조건에 맞는 집을 보여주었다.
가장 처음 본 집이 위치도 괜찮았고 혼자살기에 나쁘지 않는 크기라 망설임 없이 계약하기로 했다.
계약 과정은 먼저 부동산에서 집에 대한 설명(융자 유무 등)과 주의사항을 전달해주었고, 확인 후 계약서에 서명하고 부동산의 계좌에 총 계약금을 입금했다.
부동산은 내가 서명한 계약서를 집주인에게 들고가서 집주인의 서명도 받고 내가 입금한 계약금에서 수수료를 뺀 남은 금액을 집주인에게 주었다.
그리고 부동산은 집주인의 서명을 받은 계약서를 다시 내게 건내주면서 최종 계약이 완료되었다.

여기서 부동산이 프록시라고 할 수 있다.
그리고 주목해야할 점은 다음과 같다.

  1. 나는 집주인(Target)이 소유한 집(Method)을 구매(Call)했지만, 나의 구매 요청(Call)은 부동산(Proxy)이 받았다.

  2. 부동산(Proxy)은 내가 건네 준 계약금,서명(Arguments)을 적절히 처리한 뒤 집주인(Target)에게 넘겨주었다(Call).

  3. 집주인(Target)은 자신이 서명한 계약서를 부동산(Proxy)에게 돌려주고(Return) 부동산(Proxy)은 그 계약서를 나에게 돌려주었다(Return).

이처럼 프록시는 두 객체 사이에서 이루어지는 커뮤니케이션의 중개자 역할을 하는 일종의 행위 및 주체를 뜻한다.

CASE 2

요즘 코로나로 인해 배달앱의 이용량이 급증했다고 한다.
수요가 많아지니 배달비도 많이 올라가서 어떤 가게는 배달비가 6000원인곳도 봤다.
스마트폰이 없던 시절에는 가게 전화번호가 적힌 템플릿을 보고 가게에게 직접 전화해서 주문했었는데 이젠 그런 문화는 없어진 듯 하다.
사실 문화 자체가 없어진건 아니고 그럴 필요가 없어진다고 하는게 더 정확할 것 같다.
이제는 가게의 전화번호와 메뉴정보를 템플릿에서 일일이 찾아서 볼 필요가 없어졌다.
어차피 배달앱에 다 내장되어 있으니까.
이제는 가게에 직접 전화해서 음식을 요청할 필요가 없어졌다.
어차피 배달앱에서 대신 요청해줄테니까.

여기서 배달앱을 프록시라고 할 수 있다.
이 케이스에선 다음과 같은 사실에 주목해야한다.

  1. 가게(Target)와 배달앱(Proxy)는 연동되어있다(Remote).

  2. 우리는 배달앱(Proxy)에게 음식(Method)을 요청(Call)하지만, 배달앱(Proxy)는 외부(Remoted)에 있는 가게(Target)에게 음식(Method)을 요청한다.

CASE 1 과 사실상 동일한 케이스이긴 하지만, 여기서는 외부 객체와 프록시를 연결했다는 가정이 다른점이다.
따라서 타겟이 반환하는 값이 어떻게 제어되는지는 CASE 1 과 동일하기 때문에 생략했다.

이처럼 프록시는 이미 우리 생활 곳곳에 적용되어 제 역할을 해나가고 있다.
그렇다면 자바에서는 이러한 프록시를 어떻게 구현할 수 있을까?


How to use it?


자바에서 프록시를 구현하는 가장 직관적이고 단순한 방법은 프로시 클래스를 직접 작성해서 사용하는 방법이다.

다음 코드들은 CASE 1 상황을 자바로 구현한 것이다.

  • 이 글에서는 CASE 1 에 대한 예제만 작성하기로 하겠다. (CASE 2 와 원격 서버로 요청한다는 점 말고는 100% 동일하다.)

임대차 계약서

public class Contract{
	private String signOfLessor;
	private String signOfTenant;
	
	public Contract(String signOfLessor,String signOfTenant) {
		this.signOfLessor = signOfLessor;
		this.signOfTenant = signOfTenant;
	}
	
	public String getSignOfLessor() {
		return signOfLessor;
	}
	
	public String getSignOfTenant() {
		return signOfTenant;
	}
	
	public void setSignOfLessor(String signOfLessor) {
		this.signOfLessor = signOfLessor;
	}
	
	public void setSignOfTenant(String signOfTenant) {
		this.signOfTenant = signOfTenant;
	}
}

임대인 인터페이스

public interface Lessor{
	public Contract contract(int money,Contract contract);
}

집주인 클래스

public class Landlord implements Lessor{
	private int account;
	private String sign;
	
	public Landlord(String sign) {
		this.account = 0;
		this.sign = sign;
	}
	
	@Override
	public Contract contract(int money, Contract contract) {
		account += money;
		contract.setSignOfLessor(sign);
	}
}

우리는 여기서 부동산의 역할을 하는 프록시를 만들어서 프록시가 계약요청을 받아서 중개수수료를 제외한 금액과 임차인의 서명이 들어간 계약서를 집주인(Landlord)에게 전달하도록 할 것이다.

부동산 프록시

public class Realtor implements Lessor{
	private Landlord landlord;
	
	public Realtor(Landlord landlord) {
		this.landlord = landlord;
	}
	
	@Override
	public Contract contract(int money, Contract contract) {
		money = money * 10 / 100;
		Contract contract = landlord.contract(money,contract);
		return contract;
	}
	
	public Contract newContract() {
		return new Contract();
	}
}

프로시 사용 예제

LandLord landlord = new Landlord("홍길동"); // 예제에서는 사용자 쪽에서 타겟 객체를 생성했지만 실제로는 프로시 내에서 생성 및 관리할 수 있음.
Realtor realtor = new Realtor(landlord);

Contract contract = realtor.newContract();
contract.setSignOfTenant("김대차");
contract = realtor.contract(100000,contract); // 프록시를 통해 계약 진행.

여기서 예제의 효율성이나 적합성보다는 프록시를 통해 계약이 진행된다는 점에만 주목하자.
자바는 이러한 방법 외에 동적 프록시(Dynamic Proxy) 생성을 위한 라이브러리(java.lang.reflect)를 제공하고 있다.
그리고 우리는 최종적으로 동적 프록시를 사용할 것이다.


Why use it?


위 예제처럼 프록시 클래스를 직접 생성해서 사용하는 방법은 두 가지 단점이 있다.

  1. 귀찮기도 하고 관리해야할 클래스가 하나 더 늘어난다.

  2. 만약 계약의 종류(메소드)가 무수히 많고 각 계약에서 프록시가 하는 일이라곤 수수료 10% 제외라면, 수수료 제외를 위해 무수히 많은 메소드를 오버라이딩해줘야한다.

자바의 동적 프록시는 이러한 문제를 한번에 해결해준다.
우선 동적 프록시는 다음 클래스의 객체를 생성하면 만들어진다.

java.lang.reflect.Proxy

하지만 new 연산자로 Proxy 의 객체를 생성할 순 없고 newProxyInstance 메소드를 통해서만 생성이 가능하다.

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

각 매개변수의 의미를 먼저 살펴보자.

  1. 첫번째 매개변수 - 프록시 클래스를 가상머신으로 로드할 클래스 로더. null 전달 시 기본 클래스 로더 사용.

  2. 두번째 매개변수 - 프록시가 구현해야하는 인터페이스의 Class 객체로 구성된 배열. 생성되는 프록시 객체는 여기에 전달되는 인터페이스들의 메소드와 Object 클래스의 메소드를 가지게 된다.

  3. 세번째 매개변수 - 호출 핸들러. 생성되는 프록시 객체를 대상으로 메소드 호출 시 호출정보(호출당한 프록시 객체,호출한 메소드명,호출 시 인자목록)를 호출 핸들러에게 전달한다.

이제 호출 핸들러를 생성하는 방법을 살펴보자.
호출 핸들러 InvocationHandler 는 invoke 메소드 하나만을 가지는 함수형 인터페이스이기 때문에 이를 구현하는 클래스를 작성하거나 람다 표현식 등을 사용해 익명 객체를 생성해야한다.

public Object invoke(Object proxy, Method method, Object[] args)

  1. 첫번째 매개변수 - 호출 핸들러를 호출한 프록시 객체.

  2. 두번째 매개변수 - 프록시를 통해 호출한 메소드의 Method 객체.

  3. 세번째 매개변수 - 프록시를 통해 호출한 메소드의 인자 배열.

프록시 클래스를 직접 작성해서 구현한 방법에서는 프록시 내부에 타겟(Landlord)을 소유하고 있어야 했기 때문에 이번에도 InvocationHandler 인터페이스를 구현한 클래스를 작성하는게 맞지만, 여기서는 동적 프록시의 사용법이 주 목적이기 때문에 람다 표현식으로 예제를 작성할것이다.

Lessor realtor = (Lessor)Proxy.newInstance(null,new Class[]{Lessor.class},(proxy,method,agrs)->{
	int money = (Integer)args[0];
	money = money * 10 / 100;
	return method.invoke(landload,money,contract);
});

realtor.contract(...);

앞으로 동적 프록시의 어떤 메소드를 호출하든 무조건 호출 핸들러가 실행을 제어하기 때문에, 계약의 종류가 얼마나 더 생기더라도 모든 계약을 진행하기 전에 수수료를 차감할 수 있다.

  • 동적 프록시는 리플렉션을 사용하기 때문에 예외처리는 필수적으로 해주어야한다.

So


자바의 동적 프록시를 사용하면 이러한 동작 뿐만 아니라 런타임에 동적으로 인터페이스를 구현하는 효과를 구현할 수 있다.

profile
HAVE YOU TRIED IT?

0개의 댓글