1. Proxy
Java에서 프록시(Proxy)는 대리자를 의미한다. 프록시는 기존의 객체를 감싸서 그 객체의 기능을 확장하거나 변경할 수 있게 해준다. 예를 들어, 프록시 객체를 사용하면 객체에 대한 접근을 제어하거나, 객체의 메소드 호출 전후에 로깅 작업 등을 수행할 수 있다. 또한, 프록시 객체를 사용하여 원격으로 실행되는 객체를 호출할 수도 있다. 프록시는 주로 AOP(Aspect Oriented Programming)에서 사용된다.
- 프록시 생성은 크게 두 가지 방식이 제공된다.
- 1. JDK Dynamic Proxy 방식
- 리플렉션을 이용해서 proxy 클래스를 동적으로 생성해주는 방식으로, 타겟의 인터페이스를 기준으로 proxy를 생성해준다. 사용자의 요청이 타겟을 바라보고 실행될 수 있도록 타겟 자체에 대한 코드 수정이 아닌 리플렉션을 이용한 방식으로, 타겟의 위임 코드를InvocationHandler를 이용하여 작성하게 된다. 하지만 사용자가 타겟에 대한 정보를 잘못 주입하는 경우가 발생할 수 있기 때문에 내부적으로 주입된 타겟에 대한 검증 코드를 거친 후 invoke가 동작하게 된다.
- CGLib 방식
- 동적으로 Proxy를 생성하지만 바이트코드를 조작하여 프록시를 생성해주는 방식이다. 인터페이스 뿐 아니라 타겟의 클래스가 인터페이스를 구현하지 않아도 프록시를 생성해준다. CGLib(Code Generator Library)의 경우에는 처음 메소드가 호출된 당시 동적으로 타켓 클래스의 바이트 코드를 조작하게 되고, 그 이후 호출 시부터는 변경된 코드를 재사용한다. 따라서 매번 검증 코드를 거치는 1번 방식보다는 invoke시 더 빠르게 된다. 또한 리플렉션에 의한 것이 아닌 바이트코드를 조작하는 방식이기 때문에 성능면에서는 더 우수하다.
하지만 CGLib 방식은 스프링에서 기본적으로 제공되는 방식은 아니었기에 별도로 의존성을 추가하여 개발해야 했고, 파라미터가 없는 default 생성자가 반드시 필요했으며, 생성된 프록시의 메소드를 호출하면 타겟의 생성자가 2번 호출되는 등의 문제점들이 있었다.
스프링 4.3, 스프링부트 1.3 이후부터 CGLib의 문제가 된 부분이 개선되어 기본 core 패키지에 포함되게 되었고, 스프링에서 기본적으로 사용하는 프록시 방식이 CGLib 방식이 되었다.
1. 로직을 포함하는 코드 작성
- Student 인터페이스 작성
public interface Student {
void study(int hours);
}
- Student 클래스 작성 (Student인터페이스 구현)
public class Student implements Student {
@Override
public void study(int hours) {
System.out.println(hours + "시간 동안 열심히 공부합니다.");
}
}
2. dynamic
1. Handler 클래스 작성
java.lang.reflect.InvocationHandler를 구현한 클래스를 작성한다.
Student 클래스를 타겟 인스턴스로 설정하고 invoke 메소드를 정의한다.
public class Handler implements InvocationHandler {
/* 메소드 호출을 위해 타겟 인스턴스가 필요하다 */
private final Student student;
public Handler(Student student) {
this.student = student;
}
/* 생성된 proxy 인스턴스와 타겟 메소드, 전달받은 인자를 매개변수로 한다. */
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
System.out.println("============ 공부가 너무 하고 싶습니다. ==============");
System.out.println("호출 대상 메소드 : " + method);
for(Object arg : args) {
System.out.println("전달된 인자 : " + arg);
}
/* 타켓 메소드를 호출한다. 타겟 Object와 인자를 매개변수로 전달한다.
* 여기서 프록시를 전달하면 다시 타겟을 호출할 때 다시 프록시를 생성하고 다시 또 전달하는 무한 루프에 빠지게 된다.
* */
method.invoke(student, args);
System.out.println("============ 공부를 마치고 수면 학습을 시작합니다. ============");
return proxy;
}
}
2. Application 실행 클래스 작성
Student student = new Student();
Handler handler = new Handler(student);
/* 클래스로더, 프록시를 만들 클래스 메타 정보(인터페이스만 가능), 프록시 동작할 때 적용될 핸들러 */
Student proxy
= (Student) Proxy.newProxyInstance(Student.class.getClassLoader(), new Class[] {Student.class}, handler);
/* 프록시로 감싸진 인스턴스의 메소드를 호출하게 되면 핸들러에 정의한 메소드가 호출된다. */
proxy.study(16);
/*
============ 공부가 너무 하고 싶습니다. ==============
호출 대상 메소드 : public abstract void Student.study(int)
전달된 인자 : 16
16시간 동안 열심히 공부합니다.
============ 공부를 마치고 수면 학습을 시작합니다. ============
*/
- study 메소드 호출 시 proxy 객체의 동작을 확인할 수 있다.
3. cglib
1. Handler 클래스 작성
org.springframework.cglib.proxy.InvocationHandler를 구현한 클래스를 작성한다.
Student 클래스를 타겟 인스턴스로 설정하고 invoke 메소드를 정의한다.
public class Handler implements InvocationHandler {
private final Student student;
public Handler(Student student) {
this.student = student;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
System.out.println("============ 공부가 너무 하고 싶습니다. ==============");
System.out.println("호출 대상 메소드 : " + method);
for(Object arg : args) {
System.out.println("전달된 인자 : " + arg);
}
method.invoke(student, args);
System.out.println("============ 공부를 마치고 수면 학습을 시작합니다. ============");
return proxy;
}
}
2. Application 실행 클래스 작성
Student student = new Student();
Handler handler = new Handler(student);
/* Enhancer 클래스의 create static 메소드는 타겟 클래스의 메타정보와 핸들러를 전달하면 proxy를 생성해서 반환해준다. */
Student proxy
= (Student) Enhancer.create(Student.class, new Handler(new Student()));
proxy.study(20);
/*
============ 공부가 너무 하고 싶습니다. ==============
호출 대상 메소드 : public void Student.study(int)
전달된 인자 : 20
20시간 동안 열심히 공부합니다.
============ 공부를 마치고 수면 학습을 시작합니다. ============
*/
- study 메소드 호출 시 proxy 객체의 동작을 확인할 수 있다.