본문 바로가기

Java/@Transaction 구현

4. JDK Dynamic Proxy

@Transactional 구현 - 목차

2021.12.02 - [Java/@Transaction 구현] - 1. 배경설명, 구현 동기 및 목표

2021.12.02 - [Java/@Transaction 구현] - 2. Aspect Oriented Programming

2021.12.02 - [Java/@Transaction 구현] - 3. Proxy 패턴

2021.12.02 - [Java/@Transaction 구현] - 4. JDK Dynamic Proxy (현재 글)

2021.12.02 - [Java/@Transaction 구현] - 5. 애너테이션 적용

 

 

자바는 동적으로 프록시를 만들 수 있는 프록시 라이브러리인 JDK Dynamic Proxy 를 제공한다. JDK Dynamic Proxy 를 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 된다. JDK Dynamic Proxy 는 개발자 대신에 런타임에 프록시 객체를 동적으로 생성해준다.

JDK Dynamic Proxy 는 인터페이스를 기반으로 프록시 객체를 동적으로 만들어준다.

 

Proxy.newInstance()

 

Proxy.newInstance() 팩토리 메서드를 이용해 프록시 객체를 만들 수 있다.

public static Object newProxyInstance(
	ClassLoader loader, 
	Class<?>[] interfaces, 
	InvocationHandler handler
) throws IllegalArgumentException {}

 

Argument 설명

ClassLoader loader 프록시 클래스를 정의하는 클래스 로더
Class<?>[] interfaces 프록시 플래스를 생성할 때 필요한 인터페이스 배열
InvocationHandler handler InvocationHandler를 구현하는 클래스의 인스턴스

 

ClassLoader

동적 프록시 클래스를 정의한다. 클래스 로더는 동적 프록시가 생성되는 클래스 또는 인터페이스로 얻을 수 있다.

 

Class<?>[]

원본 클래스에서 구현하는 인터페이스들을 배열로 넣는다.

 

InvocationHandler

InvocationHandler 인터페이스를 구현한 객체를 handler로 사용한다. InvocationHandler는 프록시 객체에서 메소드의 동작을 부여하기 위해 작성한다.

앞서 살펴본 인증, 트랜잭션 진입 과 같은 부가기능을 InvocationHandler의 invoke() 메소드에 작성한다.

 

public interface InvocationHandler {
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

 

Argument 설명

 

Object proxy 프록시 객체 자기 자신
Method m 프록시 객체에서 호출할 메소드
m.invoke() 를 실행하면 원본 클래스의 메소드가 실행된다.  
Object[] args 메소드의 파라미터

 

CellInvocationHandler 구현

 

RealCell.java

public class RealCell implements Cell {
	...
	@Override
	public void setValue(Object value) {
		System.out.println("real setValue() : " + value);
	}
}

 

CellInvocationHandler.java

public class CellInvocationHandler implements InvocationHandler {
	private final Cell target;

	public CellInvocationHandler(Cell target) {
		this.target= target;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		return method.invoke(target, args);
	}
}

 

 

 

setValue() 메소드 실행을 요청하면 프록시 객체는 InvocationHandler 에 작성한 invoke() 메소드를 호출한다. invoke() 의 파라미터로 전달되는 method 는 RealCell 의 setValue() 메소드이다.

 

method.invoke(target, args)

Parameter 설명

target 생성자로 주입받은 RealCell 객체
args setValue() 메소드를 호출할 때 파라미터로 전달하는 값.

 

setValue(value) 일 경우, args는 value가 된다.

파라미터가 1개 일 경우 args[0], 2개일 경우 args[1] 로 접근할 수 있다. |

method.invoke(target, args) 를 호출하기 전 후에 부가기능을 작성할 수 있다.

 

CellInvocationHandler 구현

public class CellInvocationHandler implements InvocationHandler {
	private final Cell cell;

	public CellInvocationHandler(Cell cell) {
		this.cell = cell;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		System.out.println("=== Authentication ===");

		if(!"setValue".equals(method.getName())){
			return method.invoke(cell, args);
		}

		System.out.println("=== Transaction in ===");
		Object result = method.invoke(cell, args);
		System.out.println("=== Transaction out ===");

		return result;
	}
}

 

모든 메소드를 호출할 때, 프록시 객체를 통해서 호출하게 된다. 앞서 모든 메소드는 접근 인가를 받아야 했다. method.invoke() 를 실행하기 전에 인증을 진행한다. 메소드의 이름이 setValue 가 아니라면, 인가만 받고 method.invoke() 를 호출한다.

setValue() 는 쓰기 트랜잭션 안에서 수행해야 한다. 쓰기 트랜잭션에 진입한 후 method.invoke() 를 실행한다. setValue() 의 반환값이 result 에 저장되지만, 반환형이 void 이므로 Void 객체가 저장된다. 그리고 invoke() 메소드를 종료한다.

 

Proxy.newInstance() 로 프록시 객체 생성

InvocationHandler handler = new CellInvocationHandler(cell);

Cell proxy = (Cell) Proxy.newProxyInstance(
    Cell.class.getClassLoader(),
    new Class[] {Cell.class},
    handler
);

프록시 팩토리에 인터페이스의 클래스로더, Cell 을 구현하는데 사용하는 인터페이스 클래스, 그리고 앞서 작성한 InvocationHandler넣으면, Object 타입의 프록시 객체를 반환한다.

프록시 객체는 Cell 인터페이스를 기반으로 만들어졌기 때문에 Cell 타입으로 형변환해서 사용할 수 있다.

 

프록시 사용

@Test
void proxyTest() {
    Cell cell = new RealCell();

    InvocationHandler handler = new CellInvocationHandler(cell);

    Cell proxy = (Cell) Proxy.newProxyInstance(
        Cell.class.getClassLoader(),
        new Class[] {Cell.class},
        handler
    );

    Node node = new Node(proxy);

    node.execute();
}

Node 객체에 앞서 생성한 프록시 객체를 주입하고 execute() 메소드를 실행한다.

 

실행결과

 

 

핵심기능을 실행하기 전에 인증요청을 수행하고, setValue() 는 쓰기 트랜잭션 내에서 실행된 것을 확인할 수 있다.

 

한계

InvocationHandler의 invoke 를 구현할 때, 메소드의 이름을 그대로 사용해 트랜잭션에 진입할 메소드를 필터링했다.

setValue() 메소드 중 쓰기 트랜잭션안에서 수행할 필요가 없거나, 다른 메소드를 쓰기 트랜잭션 내에서 수행해야하는 경우가 발생할 때, 이런 요청사항을 동적으로 수정할 수 없다.

이런 한계를 극복하기위해 애너테이션을 이용해 필터링 하는 방법을 알아본다.

'Java > @Transaction 구현' 카테고리의 다른 글

5. 애너테이션 적용  (0) 2021.12.02
3. Proxy 패턴  (0) 2021.12.02
2. Aspect Oriented Programming  (0) 2021.12.02
1. 배경설명, 구현 동기 및 목표  (0) 2021.12.02