AOP가 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 모듈화하는 것을 안다는 정도로 알고있었고, 실제 개발에 적용해볼 기회가 없어서 상세하게 공부하는 것을 미뤘다가 도입해볼 기회가 생겨서 (최종적으로는 반영하지 않음), 무분별한 코드 복붙을 최소화해 학습한 것을 정리해보려고 한다.
인프런 강의가 생각보다 길어서 머리 속에 있는 실타래를 정리한다는 차원에서 정리했다.
AOP가 어떻게 적용되는지를 알기 위해 프록시 패턴에 대해서 알아본다.
대리자
라는 뜻으로, 네트워크에서 클라이언트와 서버간의 중계역할로, 통신을 대리 수행하는 대리자를 말한다.
접근을 제어
하기 위해 대리자를 제공기능 확장
을 위한 유연한 대안 제공그러면 다음 부터 프록시를 어떻게 구현했는지 두 가지 방식을 알아보려고 한다.
프록시 패턴 적용 전 의존 관계
프록시 패턴 적용 후 의존 관계
각 인터페이스에 맞는 프록시 구현체를 추가하고 이를 빈으로 등록하며, 프록시 구현체가 실제 구현체의 참조를 가지고 있는 구조다.
@RequiredArgsConstructor
public class MyServiceInterfaceProxy implments MyService {
private final MyService target;
private final LogTrace logTrace;
@Override
public String do(){
logTrace.begin();
String result = target.do(); // target을 호출
logTrace.end();
return result;
}
}
@Configuration
public class InterfaceProxyConfig {
@Bean
public MyService myService(LogTrace logTrace){
MyServiceImpl serviceImpl = new MyServiceImpl(myRepository(logTrace));
return new MyServiceInterfaceProxy(serviceImpl, logTrae);
}
}
인터페이스가 없고 구체 클래스만 있을 때 프록시를 적용하는 방식은 다음과 같다.
프록시 패턴 적용 전 의존 관계
프록시 패턴 적용 후 의존 관계
@RequiredArgsConstructor
public class MyServiceProxy extends MyConcreteService {
private final MyConcreteService target;
private final LogTrace logTrace;
@Override
public String do(){
logTrace.begin();
String result = target.do(); // target을 호출
logTrace.end();
return result;
}
}
프록시 구현체에 참조로 정의한 형식이 인터페이스이고 구체 클래스를 주입하느냐, 참조 부터 구체 클래스이고 이를 바로 주입하냐의 차이다.
위에서 적용한 프록시는 기존 코드를 변경하지 않고 부가 기능을 적용하지만, 프록시 클래스를 너무 많이 만들어야한다.
프록시를 적용하는 클래스가 다를 뿐, LogTrace라는 하나의 부가 기능을 사용하고 있다.
프록시 클래스를 하나만 만들어 모든 곳에 적용하는 방식을 동적 프록시
라고 한다.
프록시의 로직은 동일하지만 어떤 메소드에 적용할 것인가가 다른 경우, 매번 프록시 클래스를 생성하지 않고 동적으로 런타임에 프록시 객체를 만드는 것을 동적 프록시라 한다.
JDK dynamic proxy와 CGLIB가 어디서 나온 이름인지 찾아보다가 Spring 공식 문서에서 다음과 같이 이야기하고있다.
Spring AOP uses either JDK dynamic proxies or CGLIB to create the proxy for a given target object. JDK dynamic proxies are built into the JDK, whereas CGLIB is a common open-source class definition library
java.lang.reflect.Proxy
, java.lang.reflect.InvocationHandler
를 사용하여 동적 프록시를 생성하는 것이며public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
// proxy : 프록시 자기 자신. method : 호출한 메서드. args : 메서드 전달인자
// 부가 기능 및 method.invoke(target, args); 을 실행하도록 구현한다.
// target : 자신이 호출할 대상 인스턴스로 예시에서 구현할 때는 생성자 주입을 해둠
}
정의
해 두면 N개의 메인 로직 클래스들(MyService, MyRepository, BookingService, BookingRepository.. etc)을 넣을 N개의 프록시 구현 인스턴스
(LogInvocationHandler 인스턴스)를 생성해서 Proxy 객체를 만들면(Proxy.newProxyInstance) 된다.MyRepository myRepositoryTarget = new MyRepositoryImpl();
LogInvocationHandler handler1 = new LogInvocationHandler(myRepositoryTarget);
MyService myServiceTarget = new MyServiceImpl(myRepositoryTarget);
LogInvocationHandler handler2 = new LogInvocationHandler(myServiceTarget);
MyRepository repositoryProxy = (MyRepository) Proxy.newProxyInstance(MyRepository.class.getClassLoader(), new Class[]
{MyRepository.class}, handler1); // $Proxy1
MyService serviceProxy = (MyService) Proxy.newProxyInstance(MyService.class.getClassLoader(), new Class[]
{MyService.class}, handler2); // $Proxy2
// LogInvocationHandler라는 부가기능을 하나만 정의해두어도 기능이 필요한 곳의 동적 프록시를 생성해낼 수 있다.
repositoryProxy.findById(id);
serviceProxy.mapToHotel(id);
baeldung 예시를 봐도 잘 나와있으니 긴 코드는 생략.
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
// obj : CGLIB가 적용된 객체, method : 호출한 메서드. args : 메서드 전달인자, proxy : 메서드 호출에 사용
// target : 자신이 호출할 대상 인스턴스로 예시에서 구현할 때는 생성자 주입을 해둠
// method.invoke를 해도 되지만 성능상 proxy.invoke를 권장한다.
}
baeldung 예시에서 Enhancer를 사용해 프록시를 생성하는 과정이 잘 나와있어 사용하는 코드는 생략.
클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
final
키워드가 붙으면 상속이 불가능하다. CGLIB에서는 예외가 발생한다.final
키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다. CGLIB에서는 프록시 로직이 동작하지 않는다.Spring은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리를 제공한다.
Advice
를 만들어, 개발자가 Advice만 구현해두면 프록시 팩토리에서 InvocationHandler, MethodInterceptor가 알아서 Advice를 호출하도록 해두었다. package org.aopalliance.intercept;
// (X) not CGLIB MethodInterceptor, (O) yes spring-aop MethodInterceptor
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
// MethodInvocation : 내부에 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args , 메서드 정보 등 이전에 파라미터로 들어온 부분들이 다 뭉쳐있다.
// invocation.proceed() 가 메인 메서드 실행 부분
}
Advice 정의와 프록시 팩토리를 사용하는 부분도 코드는 검색하면 많으니까 생략.
AOP를 사용하기 위해 부가 기능을 어떻게 실제 로직에 붙일까? (앞에서 다 이야기함)
특수한 컴파일러나 클래스 로더 조작기를 지정해서 조작하는 방식은 복잡하고 어렵다.
하지만 프록시를 사용하는 방식은 복잡한 설정 없이 Spring만 있으면 AOP를 적용할 수 있다.
그렇다면,
이 질문들의 해답은 빈 후처리기
이다.
스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶을 때 사용한다.
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
}
Spring이 제공하는 빈 후처리기를 설명하기 위해 잠깐 다음 3가지를 간단하게 설명한다.
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
//가장 일반적인 Advisor 구현체. Pointcut과 Advice를 넣어준다.
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
DefaultPointcutAdvisor timeAdvisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
DefaultPointcutAdvisor loggingAdvisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new LoggingAdvice());
proxyFactory.addAdvisor(timeAdvisor);
proxyFactory.addAdvisor(loggingAdvisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
다음 라이브러리를 추가하면 aspectJ 관련 라이브러리를 등록하고, 자동 프록시 생성기가 spring bot가 AOP 관련 클래스를 자동으로 Spring bean에 등록해준다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
포인트컷은 자동 프록시 생성기가 포인트 컷을 사용해 빈이 프록시를 생성해야할지 판단하기 위해 한번,
프록시가 실제 호출 될 때 Advise를 적용할지 판단하는 단계에서 또 한번 사용된다.
자동 프록시 생성기 작동과정이 @Aspect 기준으로 설명해서 이해가 어려우면 @Aspect 내용을 한번 읽고 돌아오자.
자동 프록시 생성기 덕분에 편하게 프록시를 적용할 수 있었고, 개발자는 Advisor만 빈으로 등록해주면 된다.
그럼 이제 Advisor를 편하게 만드는 방법을 알아본다.
Pointcut
과 Advice
로 구성되어 있는 Advisor
를 만들어 스프링 빈으로 등록하면 된다.@Aspect
annotation으로 편리하게 Advisor 생성 기능을 지원한다.@Aspect
는 AOP을 가능하게 하는 AspectJ 프로젝트에서 제공하는 annotation 이다. @Aspect
@Component // spring bean으로 등록해줘야한다.
public class LoggingAspect{
private final Logging logging;
@Around("execution(* hello.proxy.app..*(..))") // Pointcut 표현식
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { // 해당 메소드가 Advice
logging.begin(); // 부가 로직
Object result = jointPoint.proceed(); // 메인 로직. 실제 호출 대상을 호출
logging.end(); // 부가 로직
return result;
}
}
Around를 제일 많이 사용하고 그 다음으로 Before인 것 같아, 간단한 특징과 처리되는 순서만 정리한다.
Around
Before
After Returning
After Throwing
After
public interface ProceedingJoinPoint extends JoinPoint {}
@Around
가 가장 넓은 기능을 제공하는 것은 맞지만, 실수할 가능성이 있다.@Before
, @After
같은 Advice는 기능은 적지만 실수할 가능성이 낮고, 코드도 단순하다.@Around
에 포인트컷 표현식을 직접 넣을 수 도 있지만, @Pointcut
애노테이션을 사용해서 별도로 분리할 수 도 있다.
포인트컷 표현식은 execution과 같은 포인트컷 지시자로 시작한다.
@Aspect
public class MyAspect{
@Pointcut(execution(* hello.aop.member.*.*(..)))
private void allMember(){} // 포인트컷 시그니쳐 : allMember(), 반환 타입은 void여야하며 코드 내용은 비워둔다.
@Pointcut(execution(public *(..)))
private void allPublic(){}
@Around("allMember() && allPublic()") // 포인트컷 시그니쳐 사용하면서 조합 할 수 있다 ( &&, ||, !)
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
execution(접근자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
execution(* *(..))
-> 반환 타입과 메서드 이름이 상관없고 파라미터 타입과 파라미터가 몇 개이던 상관없다는 뜻처음에 AOP Pointcut 보고 이게 무슨 외계어인가 했는데 패턴 매칭 규칙을 간단하게 알아본다.
execution(* hell*(..))
execution(* hello.aop.member.*.*(..))
execution(* hello.aop.member**..***.*(..))
execution(* hello.aop.member.MemberService.*(..))
execution(* *(*))
execution(* *(String, ..))
within(hello.aop.member.MemberServiceImpl)
@Around("allMember() && args(arg,..)")
public Object logArgs2(ProceedingJoinPoint joinPoint, BookInfo arg) throwsb Throwable {
log.info("[logArgs2]{}, arg={}", joinPoint.getSignature(), arg);
return joinPoint.proceed();
}
AOP 내부 호출이 안된다는건 @Transactional 때문에 알고는 있었는데, 입사해서 코드 파악하다가 @Transactional이 붙어있는 메서드인데 외부에서 호출하는 것이 아닌 내부에서만 호출하고 있는 코드를 목격한 적 있다. ㅇㅅㅇ
아무튼 이 부분 때문에 팀장님께 private method에 @Transactional 어떻게 적용하는게 좋을지 여쭤본 적있는데 강의에서 권장하는 방식 처럼 구조를 변경하는 방식을 추천해주셨다.
강의 자체도 길다보니 중간에 핵심 부분은 노션에 정리했지만, 그래도 머릿속에 뭐가 있긴한데..ㅁㄴㅇㄹ 상태였다.
내용이 많아도 학교다닐 때 A4에 정리하던 것 처럼 했더니, 강의의 길었던 내용들이 생각안날 때 이 포스팅 읽어봐도 빨리 리마인드가 될 것 같다.
더 필요한 문법은 공식문서 찾아보자.
학교다닐 땐 전과목을 이렇게 정리하고 모아둠
참고
인프런 스프링 핵심원리-고급편
Baeldung