[WWCode Seoul 오리지널 콘텐츠 #6] 스프링 프레임워크(Spring Framework) 알아보기 : AOP[2]

Sabai
wwcodeseoul
Published in
14 min readNov 25, 2022

이 글은 간단한 Java 코드를 읽고 이해할 수 있고 AOP 개념이 생소한 스프링을 시작하는 분들을 대상으로 하고 있습니다.

이전글 : [WWCode Seoul 오리지널 콘텐츠 #4]스프링 프레임워크(Spring Framework) 알아보기 : AOP[1]

1. ProxyFactoryBean

public class ProxyFactoryBean 
extends ProxyCreatorSupport
implements FactoryBean<Object>, BeanClassLoaderAware, BeanFactoryAware {
...
@Nullable
private String[] interceptorNames;
@Nullable
private String targetName;
...
}

Spring의 BeanFactory를 기반으로 AOP를 프록시를 구축하는 FactoryBean 인터페이스를 구현하여 일관된 방식으로 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈으로 getObject() 를 통해 생성되어 반환된 프록시는 스프링의 빈으로 등록됩니다. 반환된 프록시는 Advised에 캐스팅하거나 ProxyFactoryBean 참조를 가져와 프로그래밍 방식으로 조작할 수 있습니다.

Interception의 변경 사항은 바로 반영되고 인터페이스나 타깃의 변경 시에는 FactoryBean으로 부터 새 인스턴스를 얻어와야 합니다. 이는 팩토리에서 얻은 싱글톤 인스턴스가 동일한 개체 ID를 갖지 않음을 의미하지만 동일한 Interceptor와 타겟을 가지고 있으며 참조 변경 시 모든 오브젝트가 변경됩니다.

MethodInterceptors와 Advisor의 경우 interceptorNames 속성을 통해 현재 FactoryBean의 빈 이름 목록으로 추가되는데, 이 속성은 BeanFactory에 FactoryBean을 사용하려면 필수로 설정해야 하는 속성입니다. 참조된 빈은 Interceptor or Advisor or Advice 유형이어야 하며 아닌 경우 SingletonTargetSource가 추가되어 이를 래핑하고 목록의 마지막 항목은 팩토리 빈이나 타깃 이름일 수 있으나 targetName 속성을 대신 사용하는 것이 권장됩니다.

global interceptor나 advisor는 factory level에서 추가 될 수 있고 일치하는 인터셉터는 Ordered 인터페이스를 구현하는 경우 반환된 order 값에 따라 적용됩니다.

프록시 인터페이스가 제공되면 JDK 프록시를 작성하고 그렇지 않은 경우 실제 타깃 클래스에 대한 CGLIB이라는 오픈소스 바이트코드 생성 프레임워크를 통해 프록시를 작성합니다. 후자는 타깃 클래스에 최종 메서드가 없는 경우에만 작동하는데 동적 하위 클래스는 런타임에 생성되기 때문입니다.

MethodInterceptor

@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
@Nullable
Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}

프록시에 적용할 부가 기능을 구현하기 위한 인터페이스로 invoke() 호출 시 ProxyFactoryBean으로 부터 타깃에 대한 정보를 파라미터로 전달받음으로 독립적으로 생성할 수 있고 싱글톤 빈으로 등록할 수 있으며 타깃이 다른 여러 프록시에서 함께 사용이 가능합니다.
타깃 클래스의 메소드 호출 전후 사용자 정의 코드를 호출할 수 있고 이 메소드 호출 전 인수를 호출하거나 전혀 호출하지 않을 수 있습니다.

MethodInvocation

public interface JoinPoint {
@Nullable
Object proceed() throws Throwable;
...
}

public interface Invocation extends JoinPoint {
...
}
public interface MethodInvocation extends Invocation {
@Nonnull
Method getMethod();
}
static class LoggingAdvice implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) {
invocation.proceed();
log.info(..);
}
}

메소드 호출 시 interceptor에게 제공되는 메소드 호출에 대한 설명이자 JoinPoint 중 하나로 Method Interceptor에 의해 가로채 질 수 있습니다.
proceed() 실행 시 타깃 오브젝트의 메소드를 내부적으로 실행할 수 있는 기능이 있기에 부가 기능을 제공하는 데만 집중할 수 있습니다.
ProxyFactoryBean은 작은 단위의 템플릿/콜백 구조를 응용해서 적용했기 때문에 템플릿 역할을 하는 MethodInvocation을 싱글톤으로 두고 공유 할 수 있습니다.

@Test
public void proxyFactoryBean(){
ProxyFactoryBean factory = new ProxyFactoryBean();
factory.setTarget(new MemberBusiness()); // 타깃 설정
factory.addAdvice(new LoggingAdvice()); // 부가기능을 담은 어드바이스 추가

// FactoryBean을 구현한 클래스임으로 getObject()를 통해 프록시 Bean을 반환받는다.
Latte latte = (Latte)factory.getObject();
assertThat(Proxy.isProxyClass(factory.getObjectType()), is(true));
}
public class ProxyFactoryBean 
extends ProxyCreatorSupport
implements FactoryBean<Object>, BeanClassLoaderAware, BeanFactoryAware {
...
private synchronized Object getSingletonInstance() {
...
this.setInterface(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader));
}
}

ProxyFactoryBean에 있는 인터페이스 자동 검출 기능으로 타깃 오브젝트가 구현하고 있는 모든 인터페이스 정보 알아 이를 구현하는 프록시를 만들어 주기에 인터페이스를 알고 있지 않아도 이를 구현한 프록시를 생성할 수 있습니다.

public class ProxyCreatorSupport extends AdvisedSupport {
...
}

public class AdvisedSupport extends ProxyConfig implements Advised {
private List<Advisor> advisors;
...
public void addAdvisor(Advisor advisor) {
int pos = this.advisor.size();
this.addAdvisor(pos, advisor);
}

public void addAdvisor(int pos, Advisor advisor) throws AopConfigException {
...
this.addAdvisorInternal(pos, advisor);
}

public void addAdvisorInternal(int pos, Advisor advisor) throws AopConfigException {
...
this.advisors.add(pos, advisor);
}
}

또한, ProxyFactoryBean이 MethodInterceptor를 설정해 줄 때 일반적인 DI처럼 수정자를 사용하지 않고 addAdvice() 를 사용함으로써 1개의 ProxyFactoryBean에 N개의 MethodInterceptor를 추가할 수 있음을 명시적으로 나타냅니다.

2. AOP 용어

1) AOP(Aspect Oriented Programming)

핵심 기능을 가진 모듈은 그 자체로 독립적으로 존재, 테스트, 최소한의 인터페이스를 통해 다른 모듈과 결합해 사용하면 되지만 부가 기능은 핵심 기능과 같은 레벨에서는 독립적으로 존재하는 것 자체가 불가능합니다.
부가 가능 모듈화 작업을 통해 기존 OOP(Object Oriented Programming)와는 다른 사고방식으로 OOP에서 모듈화의 핵심 단위는 클래스지만, AOP에서 모듈화의 단위는 Aspect로 이는 OOP를 보완합니다.
Aspect는 트랜잭션 관리와 같은 여러 유형 및 개체에 걸친 관심사의 모듈화를 가능하게 하여 부가 기능이 핵심 기능의 모듈에 런타임시 동적으로 참여하여 설계와 코드가 모두 지저분해지는 문제를 해결합니다.

Aspect
여러 클래스를 아우르는 관심사의 모듈화로 스프링에서 Aspect는 schema-based / @Aspect annotation 두 가지가 존재합니다.

JoinPoint
메서드의 실행이나 예외 처리와 같은 프로그램을 실행하는 동안의 지점으로 스프링 AOP에서 항상 메서드 실행을 의미합니다.

Advice
특정 JoinPoint의 Aspect에 의해 타깃 오브젝트에 적용하는 부가 기능 적용을 수행되는 작업으로 다른 유형의 Advice에는 “around”, “before” 및 “after” 이 포함됩니다.
Spring을 포함한 많은 AOP 프레임워크는 Advice을 Interceptor로 모델링하고 JoinPoint 주변에 Interceptor 체인을 유지합니다.

Pointcut
JoinPoint와 매치되는 술어로 Advice는 Pointcut 표현 식과 연관되어 있고, Pointcut에 의해 일치되는 어떠한 JoinPoint에서든 실행됩니다. Spring 에서는 AspectJ를 Pointcut 표현 식으로 사용하는 것을 기본으로 사용합니다.

Advisor
Advice와 Pointcut을 결합한 개념

Introduction
타입을 대신하여 추가적인 메소드나 필드를 선언하는 것으로 Spring AOP는 어떠한 advised 된 객체에 새로운 인터페이스(및 해당 구현)를 도입할 수 있습니다.

Target Object
N개의 aspect에 의해 advised 되는 객체로 advised object로도 불립니다.
Spring AOP에서는 런타임 프록시로 구현되기 때문에, 이 객체는 프록시 오브젝트입니다.

AOP proxy
aspect 관점(advice 메소드 실행 등)을 실행하기 위해 AOP framework에 의해 생성된 객체로 Spring AOP에서는 JDK 동적 프록시와 CGLIB 프록시가 존재합니다. 전자의 경우 인터페이스를 구현한 프록시 객체를 생성함으로 인터페이스가 필수이고 후자의 경우 인터페이스가 있어도 구체 클래스를 상속 받아 프록시 객체를 생성합니다.

Weaving
다른 어플리케이션 타입이나 객체를 Aspect에 연결하여 Advised 된 객체를 만드는 행위로 컴파일 시간, 로드 시간 또는 런타임시 수행 할 수 있습니다.
Spring AOP는 다른 순수 자바 AOP 프레임워크와 같이 런타임시 Weaving을 수행합니다.

2) Spring AOP

순수한 자바 내에서 구현되고 특별한 편집 실행이 필요하지 않으며 클래스 로더 체계를 수정할 필요 없고 servlet container나 application server에 적합합니다.
오직 스프링 빈의 메소드 실행 JoinPoint만을 지원하고 field 가로채기(interception)는 구현되지 않지만 핵심 Spring AOP API를 해치지 않고 필드 가로채기 지원을 추가 하기 위해서는 AspectJ와 같은 언어를 사용해야 합니다.
Spring의 핵심 신조 중 하나는 침입하지 않는다 인데, 프레임 워크 특정 클래스와 인터페이스를 비즈니스 또는 도메인 모델에 강제로 도입해서는 안 된다는 것입니다. 이에 따라 Spring AOP는 완전한 AOP 구현을 제공하는 것이 아닌 AOP 구현과 Spring IoC 사이의 긴밀한 통합을 제공하여 엔터프라이즈 애플리케이션의 일반적인 문제를 해결할 수 있습니다.

3. @AspectJ

스프링 설정에서 @AspectJ Aspect를 사용하기 위해서는 이에 기반한 스프링 AOP 설정과 이러한 Aspect에 의해 Advised 되는지 아닌지를 판단하는 자동 프록시 빈의 스프링 지원을 활성화해야 합니다.

자동 프록시 ?
Spring이 N개의 Aspect에서 빈이 Advised 된다고 판단하면 메서드 호출을 가로채기 위해 해당 빈에 대한 프록시를 자동으로 생성하고
Advice가 필요에 따라 실행되도록 보장한다는 것을 의미합니다.

@Configuration으로 @AspectJ 지원을 활성화하려면 @EnableAspectJAutoProxy를 추가합니다.

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

* spring-boot-starter-aop 의존성이 추가되어 있다면 AopAutoConfiguration을 통한 자동 설정에 의해 @EnableAspectJAutoProxy를 추가하지 않아도 됩니다.

@AspectJ 지원이 활성화되면 @AspectJ의 Aspect 클래스가 있는 애플리케이션 컨텍스트에 정의된 모든 빈이 스프링에 의해 자동으로 감지되고 스프링 AOP를 구성하는 데 사용됩니다.

@Component
@Aspect
public class ExampleAspect {

}

@Configuration@Component를 통해 Aspect 클래스를 일반 빈으로 등록하거나 다른 Spring 관리 빈과 동일하게 클래스 경로 스캐닝을 통해 자동 감지하도록 가능합니다. 하지만 Aspect 어노테이션 만으로는 클래스 경로에서 자동 탐지를 수행할 수 없어 이를 위해 별도의 @Component 주석을 추가해야 합니다.

글쓴이 이슬기

해당 글은 WWCode Seoul의 입장을 대변하고 있지 않습니다.

출저 :
이일민 저, 토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리』, 에이콘 출판사

https://github.com/spring-projects/spring-framework
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/aop/framework/ProxyFactoryBean.html
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/aopalliance/intercept/MethodInvocation.html
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html
https://gmoon92.github.io/spring/aop/2019/02/23/spring-aop-proxy-bean.html
https://stackoverflow.com/questions/15447397/spring-aop-whats-the-difference-between-joinpoint-and-pointcut

--

--