AOP의 등장 배경으로는 핵심 기능과 부가 기능의 분리였다. 그리고 AOP를 학습하다 보면 프록시라는 개념이 많이 등장한다. 오늘은 프록시에 대한 설명과 Spring AOP에서 프록시를 사용하는 방법에 대해 기술하겠다.
목표 : 핵심 기능이 작성된 클래스와 부가 기능이 작성된 개체들을 분리해야 한다.
분리 과정에 있어 두 클래스(핵심 ↔ 부가)가 양방향으로 알아야 할까?
NO ! 부가 기능을 담당하는 쪽에서만 핵심 기능의 개체 정보를 알면 된다.
구체화
프록시
프록시 사용 목적
클라이언트에게 타깃에 대한 레퍼런스를 넘긴다 할 때 !
실제 타깃 대신에 프록시를 넘긴다 !
프록시의 메소드를 통해 타깃 접근 시 → 타깃을 생성하고 요청 위임
- 프록시와 타겟을 동일한 인터페이스를 사용하도록 !
간단한 예제
방식
Java.lang.reflect.Proxy
클래스의 newProxyInstance()
메소드를 이용해 프록시 객체를 생성
JVM
이 실행되면 사용자가 작성한 자바 코드가 컴파일러를 거쳐 바이트 코드로 변환되어JVM Memory
에 저장된다.Reflection API
는 이 정보를 활용해 필요한 정보를 가져온다.
클래스 정보
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<BTS> mClass = BTS.class; // BTS 클래스의 Class 오브젝트
Method method = mClass.getMethod("sayHello"); // 메소드 정보 가져오기
method.invoke(new BTS()); // 메소드 정보를 바탕으로 메소드 호출
}
}
class BTS
{
public void sayHello()
{
System.out.println("안녕하세요 BTS입니다 !");
}
}
주의사항
JDK Dynamic Proxy
Proxy 생성 방법
Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
동작 방법
다이내믹 프록시 → 런타임 시 프록스 팩토리에 의해 만들어지는 동적 오브젝트 (프록시)
내가 만든 프록시하고의 차이 ?
부가기능에 대한 구현을 프록시 내부에 작성하는 것이 아니라 → InvocationHandler에게 위임 !
InvocationHandler
public Object invoke(Object proxy, Method method, Object[] args)
사용 방법
public class UserHandler implements InvocationHandler {
Object target;
UserHandler(Object target)
{
this.target = target; // 다이내믹 프록시로부터의 요청을 다시 타깃에게 위임(타깃 오브젝트 주입)
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("say"))
System.out.println("프록시가 사용자의 say 감지");
else
System.out.println("프록시가 사용자의 eat 감지");
return method.invoke(target,args); // 타깃에게 요청 위임
}
}
결과
TeamService teamService = (TeamService) Enhancer.create(
TeamService.class,
new TeamInterceptor(new TeamService())
);
공식 문서
Classes in Java are loaded dynamically at runtime. Cglib is using this feature of Java language to make it possible to add new classes to an already running Java program.
- Hibernate의 지연 로딩
- Mockito의 mocking method
MethodIntercepter
package com.example.toyjava.module.account.service;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class TeamInterceptor implements MethodInterceptor {
private Object target;
public TeamInterceptor(Object target)
{
this.target = target; // target을 주입받는다.
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("CGLIB Proxy 감지"); // 부가 기능
return methodProxy.invoke(target,objects); // 위임
}
}
// TeamService : class com.example.toyjava.module.account.service.TeamService$$EnhancerByCGLIB$$582c0b44
CGLIB 고찰
JDK 다이나믹 프록시와 CGLIB을 통해 우리는 부가 기능과 핵심 기능의 개체들을 분리할 수 있었다. 또한 위의 방법들은 런타임 중 동적으로 프록시 객체를 생성해주었기 때문에 불필요한 양의 코드 작성 또한 줄일 수 있었다.
- 그렇다면 우리에게 남은 과제는 이러한 프록시를 ‘어떻게 Spring에 녹여낼 것인가’ 이다.
한번에 여러 개의 클래스에 공통적인 부가기능 제공 할 수 없다.
스프링 관점 !
Handler를 Bean으로 등록한다 했을 때 타겟의 개수만큼 중복 등록(생성) → 비효율적
중복을 없애고 모든 타깃에 적용 가능한 싱글톤 빈으로 만들자 !
중복을 없애고 모든 타깃에 적용 가능한 싱글톤 빈으로 만들자 !
ProxyFactoryBean
MethodIntercepter
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
@Nullable
Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}
// MethodInvocation (콜백 오브젝트) : proceed() 메소드 호출 시 -> 타깃 오브젝트의 메소드를 내부적 실행
예제
TeamAdvice
package com.example.toyjava.module.account.service;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class TeamAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("MethodInterceptor 동작");
invocation.proceed(); // 타깃 오브젝트 메소드 실행
return null; // proceed()의 결과값을 반환하는 것도 가능
}
}
프록시 팩토리 빈 사용
public class ProxyTest {
@Test
public void test() {
ProxyFactoryBean pf = new ProxyFactoryBean();
pf.setTarget(new TeamService()); // 타켓 저장
pf.addAdvice(new TeamAdvice()); // 부가 기능 저장 (별도의 싱글톤 빈으로 관리 가능)
TeamService ts = (TeamService) pf.getObject(); // 프록시 객체 반환
ts.testing();
}
}
프록시 팩토리 빈 !
부가 기능을 담당하는 핸들러에서 타깃의 정보를 가지고 있지 않아도 되었다. → 타깃에 구애받지 않고 적용 가능
빈 후처리기의 사용
빈 후처리기 사용 !
스프링이 생성한 빈 오브젝트의 일부를 프록시로 포장 → 프록시를 빈으로 대신 등록하자 !
AnnotationAwareAspectJAutoProxyCreator
라는 빈 후처리기가 스프링 빈으로 자동 등록됨 !
동작 과정
Advisor
을 조회Advisor
내에 있는 Pointcut
을 이용해 모든 클래스와 메서드를 매칭 , 조건이 하나라도 만족하면 프록시 적용 대상Advisor
연결참고 서적
참고 블로그
https://www.youtube.com/watch?v=MFckVKrJLRQ&t=922s
https://www.baeldung.com/cglib
https://woooongs.tistory.com/99