프록시는 원래 객체에 대한 접근을 제어하므로, 당신의 요청이 원래 객체에 전달되기 전 또는 후에 무언가를 수행할 수 있도록 한다.
신용 카드는 은행 계좌의 프록시이다.
은행 계좌는 현금의 프록시이다.
신용 카드, 은행 계좌는 같은 인터페이스를 구현하며 둘 다 결제에 사용할 수 있다.
객체에 대한 접근을 제한하는 이유는 다음과 같다.
방대한 양의 시스템 자원을 소비하는 거대한 객체가 있다고 가정하자.
이 객체는 필요할 때가 있지만, 항상 필요하진 않다.
우리는 이 객체를 필요할 때만 만들어서 지연된 초기화를 구현할 수 있다.
하지만 이러면 객체의 모든 클라이언트들은 지연된 초기화 코드를 실행해야한다는 단점이 있다.
이것은 많은 코드 중복을 초래한다.
프록시 패턴은 원래 서비스 객체와 같은 인터페이스로 새 프록시 클래스를 생성한다.
그 다음 프록시 객체를 모든 클라이언트들에게 전달하도록 앱을 바꾼다.
클라이언트로부터 요청을 받으면 이 프록시는 실제 서비스 객체를 생성하고 모든 작업을 실제 서비스 객체에 위임한다.
이렇게하면 클래스의 메인 로직 이전이나 이후에 새로운 로직을 실행해야 하는 경우 프록시는 해당 클래스를 변경하지 않고 새로운 로직을 수행할 수 있다. (@Transactional
)
프록시는 원래 클래스와 같은 인터페이스를 구현하므로 실제 서비스 객체를 호출하는 모든 클라이언트가 사용할 수 있다.
서비스의 인터페이스를 선언한다.
프록시가 서비스 객체로 위장할 수 있으려면 이 인터페이스를 따라야 한다.
서비스는 비즈니스 로직을 제공한다.
프록시는 서비스 객체를 가리키는 참조 필드가 있다.
프록시가 요청의 처리(초기화 지연, 로깅, 액세스 제어, 캐싱 등)를 완료하면, 그 후 처리된 요청을 서비스 객체에 전달한다.
일반적으로 프록시들은 서비스 객체들의 전체 수명 주기를 관리한다.
클라이언트는 같은 인터페이스를 통해 서비스들 및 프록시들과 함께 작동해야 한다.
그러면 서비스 객체를 기대하는 모든 코드에 프록시를 전달할 수 있다.
프록시 패턴을 활용하는 방법은 수십 가지가 있다.
대표적으로는 다음이 있다.
자바의 java.lang.reflect
패키지 안에 프록시 기능이 내장돼 있다.
이 패키지를 사용하면 동적 프록시(dynamic proxy)
를 만들 수 있다.
reflect 패키지를 사용해 즉석에서 하나 이상의 인터페이스를 구현하고, 지정한 클래스에 메소드 호출을 전달하는 프록시 클래스를 만든다.
진짜 프록시는 실행 중에 생성된다.
자바에서 Proxy 클래스를 알아서 생성해준다.
따라서 Proxy 클래스에게 무슨 일을 해야 하는지 알려줘야한다.
InvocationHandler
를 이용해 프록시의 일을 작성한다.
InvocationHandler
는 프록시에 호출되는 모든 메소드에 응답한다.
Proxy에서 메소드 호출을 받으면 항상 InvocationHandler에 진짜 작업을 부탁한다.
public interface PersonBean {
String getName();
String getGender();
String getInterests();
int getHotOrNotRating();
void setName(String name);
void setGender(String gender);
void setInterests(String interests);
void setHotOrNotRating(int rating);
}
// 위 인터페이스를 구현하면, 자기의 점수 뿐만 아니라 다른 고객의 정보를 수정할 수 있게 된다.
// 이런 경우 보호 프록시를 사용한다.
// 본인 접근용 proxy, 다른 유저 접근용 proxy 필요
보호 프록시는 접근 권한을 바탕으로 객체로의 접근을 제어하는 프록시다.
InvocationHandler
에서 프록시의 행동을 구현한다.
프록시 클래스 및 객체를 만드는 일은 자바에서 알아서 해주기 때문에, 프록시의 메소드가 호출되었을 때 할 일을 지정해주는 핸들러만 만들면 된다.
호출 핸들러는 두개 만들어야 한다.
호출 핸들러(Invocation Handler)
는 다음과 같이 생각한다.
프록시의 메소드가 호출되면 프록시에서는 그 호출을 호출 핸들러에게 넘긴다.
하지만 호출 핸들러는 호출된 메소드가 무엇이든 무조건 핸들러의 invoke()
메소드가 호출된다.
import java.lang.reflect.*;
public class OwnerInvocationHandler implements InvocationHandler {
PersonBean person;
// 생성자
public OwnerInvocationHandler(PersonBean person) { // PersonBean 주입
this.person = person;
}
// 여기서 행동 정의, 주제 기준
public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException {
try {
if (method.getName().startsWith("get")) { // 주제에게 허용
return method.invoke(person, args);
} else if (method.getName().equals("setHotOrNotRating")) { // 허용 x
throw new IllegalAccessException();
} else if (method.getName().startsWith("set")) { // 나머지 set 허용 O
return method.invoke(person, args);
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
InvocationHandler
는 java.lang.reflect
패키지에 들어있다. 호출 핸들러는 반드시 InvocationHandler 인터페이스를 구현해야 한다.Proxy 클래스를 생성하고 Proxy 객체 인스턴스를 만들어야 한다.
OwnerInvocationHandler
혹은 NonOwnerInvocationHandler
한테 넘겨주는 프록시를 만든다.
PersonBean getOwnerProxy(PersonBean person) {
return (PersonBean) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new OwnerInvocationHandler(person)); // 프록시 생성
}
PersonBean getNonOwnerProxy(PersonBean person) {
return (PersonBean) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new NonOwnerInvocationHandler(person)); // 프록시 생성
}
Person 객체(실제 주 객체)를 인자로 받아오고 프록시를 리턴한다.
프록시의 인터페이는 주 객체의 인터페이스와 같기 때문에 리턴 타입은 PersonBean 이다.
리턴 문에서 프록시를 생성한다. 코드 설명은 다음과 같다.
PersonBean
객체를 사용하는 객체는 고객 자신의 객체거나, 다른 고객의 객체 중 하나이다.
어떤 경우든 해당 PersonBean에 따라 적절한 프록시를 생성해야 한다.
OwnerInvocationHandler
: 자기 자신 빈에 직접 접근하는 경우NonOwnerInvocationHandler
: 다른 사람의 빈에 접근하는 경우Spring AOP는 프록시 기반으로 JDK Dynamic Proxy
와 CGLIB
을 활용하여 AOP 제공하고 있다.
Spring은 AOP Proxy를 생성하는 과정에서 자체 검증 로직을 통해 타깃의 인터페이스 유무를 판단합니다.
타깃이 하나 이상의 인터페이스를 구현하고 있는 클래스라면 JDK Dynamic Proxy의 방식으로 생성한다.
인터페이스를 구현하지 않은 클래스라면 CGLIB의 방식으로 AOP 프록시를 생성한다..