[Spring Framework - Core] 5. Spring AOP

mrcocoball·2023년 11월 28일

Spring Framework

목록 보기
6/20

해당 포스트는 Spring.io의 공식 문서를 포함한 레퍼런스와 코드를 통해 Spring Framework의 구조 / 기술에 대해 확인해보고자 하는 포스트입니다.

1. AOP

개요

AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)은 프로그램 구조에 대한 또 다른 사고 방식을 제공하여 객체 지향 프로그래밍을 보완합니다. 트랜잭션 관리, 로깅 등 여러 유형과 개체에 걸쳐 있는 문제를 횡적 관심사(Cross Concern)라고 부르는데 AOP는 이러한 횡적 관심사를 해결하는데 적합합니다.

스프링 프레임워크는 스키마 기반 방식(Schema Based)과 @Aspect 어노테이션 스타일을 사용하여 AOP를 지원하고 있습니다.

Spring AOP의 목표

스프링 AOP의 주요 목적은 AOP 구현 및 스프링 IoC 컨테이너와의 통합이며 이를 통해 엔터프라이즈 어플리케이션의 일반적인 목표를 해결하는 것입니다.

스프링 AOP는 POJO로 구현되며 특별한 컴파일 과정이 필요하지 않아 서블릿 컨테이너 또는 어플리케이션 서버에서 사용하기에 적합하며 일반적으로 스프링 IoC 컨테이너와 함께 사용됩니다.

또한 보다 많은 기능을 제공하는 AspectJ와의 통합도 가능하게 설계되어 스프링 기반 어플리케이션 아키텍처 내에서 AOP의 모든 기능을 사용하게끔 지원합니다.

AOP의 주요 개념

AOP에서 사용되는 주요 개념과 용어에 대한 정의는 다음과 같습니다.

  • Aspect
    여러 클래스에 걸쳐 적용되는 관심사의 모듈화.
    스프링 AOP에서는 스키마 기반 방식 / @Aspect 어노테이션 방식으로 지원 (@Transaction)

  • JoinPoint
    메소드 실행이나 예외 처리 등 프로그램 실행 중 지점.

  • Advice
    특정 JoinPoint에서 Aspect가 취하는 조치.
    대부분의 AOP 프레임워크는 Advice를 인터셉터로 모델링하고 JoinPoint 주위에 인터셉터 체인을 유지

  • PointCut
    JoinPoint와 일치하는 조건자 / 표현식이며 Advice는 PointCut과 연관되어있음
    스프링 AOP의 핵심이며 스프링 프레임워크는 기본적으로 AspectJ 포인트컷 표현 언어를 사용

  • Introduction
    특정 객체의 타입을 대신하여 추가 메서드나 필드를 선언
    스프링 AOP 사용 시 Advice 대상 객체에 새로운 인터페이스(및 해당 구현)을 도입할 수 있음

  • Target Object
    대상 객체, 하나 이상의 Aspect에서 Advice를 받는 객체
    스프링 AOP는 런타임 프록시를 사용하므로 해당 객체는 항상 프록시 객체

  • AOP Proxy
    Aspect를 구현하기 위해 AOP 프레임워크에서 생성된 객체
    스프링 프레임워크에서는 JDK 동적 프록시 또는 CGLIB 프록시를 사용하여 생성

  • Weaving
    Aspect를 다른 어플리케이션 타입 또는 객체와 연결하여 Advice가 적용된 객체를 생성. 컴파일 타임, 로드 타임, 런타임에 수행될 수 있음. 스프링 AOP는 런타임에 위빙을 수행

이러한 개념들 중에서 PointCut과 일치하는 JoinPoint 개념은 AOP의 핵심이며 PointCut 사용 시 객체 지향 계층 구조와 독립적으로 Advice를 타겟팅할 수 있습니다.

Spring AOP의 Advice

스프링 AOP에서는 다음 유형의 Advice를 제공하고 있습니다.

  • After Advice
    JoinPoint 이전에 실행되며 예외가 발생하지 않는 한 실행 흐름이 JoinPoint로 진행되는 것을 방지할 수 있는 기능이 없음

  • AfterReturning Advice
    JoinPoint가 정상적으로 완료된 후 실행(예외가 발생하지 않고 메서드가 반환된 경우 등)

  • AfterThrowing Advice
    예외를 발생시켜 메서드가 종료될 때 실행

  • After Advice
    JoinPoint가 종료되는 방식과 상관 없이 종료된 이후에 실행(반환이건 예외던 관계 없음)

  • Around Advice
    메서드 호출처럼 JoinPoint를 둘러 싸는 어드바이스이며 가장 강력한 기능. 메서드 호출 전후에 사용자 정의 동작을 수행할 수 있음. JoinPoint로 진행할 지, 자체 반환 값을 반환하거나 예외를 발생시키는 것도 가능

공식 문서에 따르면 Around Advice는 가장 강력한 Advice이므로 필요한 기능 / 동작을 구현할 수 있는 구체적이고 가장 덜 강력한 Advice를 사용할 것을 권장합니다.

AOP 프록시 매커니즘

JDK 동적 프록시와 CGLIB 프록시
동적 프록시 기술은 런타임에 동적으로 프록시 객체를 만드는 기술을 의미하며 크게 JDK 동적 프록시와 CGLIB 프록시로 나뉩니다.

  • JDK 동적 프록시는 프록시 생성 대상이 인터페이스 타입일 경우 사용하며 InvocationHandler를 구현하여 적용할 로직을 작성합니다.
  • CGLIB는 바이트 코드를 조작하여 동적으로 프록시를 생성하며 프록시 생성 대상이 인터페이스 타입이 아니라도 적용이 가능합니다. MethodInterceptor를 통해 적용할 로직을 작성합니다.

프록시 팩토리
스프링 프레임워크는 동적 프록시를 생성하기 위해 프록시 팩토리(ProxyFactory)를 지원합니다. 클라이언트에서 프록시를 호출할 때 프록시 팩토리에서 JDK 동적 프록시(인터페이스 기반)나 CGLIB 프록시(클래스 기반) 를 선택하여 프록시를 생성할 수 있습니다.

// NeoCar를 대상으로 프록시 팩토리 생성
ProxyFactory factory = new ProxyFactory(new NeoCar());
// 프록시에 Advice 적용
factory.addAdvice(new EngineAdvice());
// 프록시 획득
NeoCar proxy = (NeoCar) factory.getProxy();

주의 사항
AOP를 적용할 경우 프록시 객체를 호출해야 하지만 프록시 객체가 아닌 실제 객체의 메서드를 호출할 경우 AOP가 적용되지 않습니다. 아래의 예시에서는 AOP 적용 대상인 클래스의 메서드에서 클래스 내 내부 메서드를 호출하는 예시이며 이 경우 프록시 객체의 내부 메서드가 아닌, 실제 객체의 내부 메서드가 호출됩니다. 이 경우 내부 메서드는 AOP가 적용되지 않습니다.

@Slf4j
@Component
public class CallServiceV0 {

    public void external() {
        log.info("외부 호출");
        internal(); // 내부 메서드를 호출 (this, 자기 자신 인스턴스 내부의 메서드)
    }

    public void internal() {
        log.info("내부 호출");
    }

}

이러한 문제를 해결하기 위해서는 내부 메서드 호출을 하지 않도록 구조를 변경하는 것이 좋습니다.

AOP 지원 방식

스프링 AOP는 스키마 기반 방식과 @AspectJ 어노테이션 기반 방식을 지원하며, 그 외에 AspectJ를 직접 사용하는 방식 모두를 지원합니다. 이 중에서 AspectJ를 직접 사용하는 방식을 제외한 두 가지 방식에 대해 설명하자면 다음과 같습니다.

스키마 기반 AOP
XML 기반 형식을 선호하는 경우 사용할 수 있는 방식입니다. 공식 문서에 따르면 스키마 기반 방식은 2가지 단점을 가지고 있습니다.

  • 단일 위치에서 다루는 요구 사항의 구현을 완전히 캡슐화하지 않음
  • @AspectJ 방식보다 표현할 수 있는 내용이 제한됨

@AspectJ 기반 AOP
AspectJ 프로젝트에 의해 도입된 방식으로, 어노테이션을 통해 Advice를 선언하는 방식입니다.

공식 문서에서는 스키마 기반 AOP의 단점으로 @AspectJ 기반을 사용하는 것을 권장하고 있습니다.

@AspectJ 기반 지원 상세

Aspect 선언
@Aspect 어노테이션을 부착하여 해당 클래스를 Advice 클래스로 선언할 경우 해당 빈은 스프링에 의해 자동으로 감지되어 스프링 AOP를 구성하는데 사용됩니다. 단, 어디까지나 해당 클래스가 Advice 클래스라고 선언하는 것이지 빈으로 등록되는 것은 아니며 @Component나 @Bean 등으로 해당 클래스를 빈으로 등록해야 합니다. 또한 @Aspect가 부착된 Advice는 다른 Aspect의 Advice 대상이 되지 않습니다.

@Aspect
@Component
public class MyAspect {
	// Advice와 관련된 메서드...
}

PointCut 선언
@PointCut 어노테이션을 사용하고 내부에 PointCut 표현식을 작성합니다. 이 때 사용하는 표현식은 AspectJ PointCut 지정자 (PCD) 입니다.

@Aspect
@Component
public class MyAspect {

	@Pointcut("표현식")
    public void myPointCut() {}

}

스프링 AOP에서 지원하는 PointCut 지정자는 다음과 같은 종류가 있습니다.

  • execution
  • within
  • this
  • target
  • args
  • @target
  • @args
  • @within
  • @annotation

PointCut 표현식은 결합이 가능하다는 특징이 있습니다.

@Aspect
@Component
public class MyAspect {

	@Pointcut("표현식1")
    public void myPointCut1() {}
    
    @Pointcut("표현식2")
    public void myPointCut2() {}
    
    @Pointcut("myPointCut1() && myPointCut2()")
    public void myCompositionPointCut() {}

}

Advice 선언
PointCut 표현식과 함께 각종 Advice 어노테이션을 사용하여 Advice를 선언할 수 있습니다. 종류는 다음과 같습니다.

  • @Around
  • @Before
  • @AfterReturning
  • @AfterThrowing
  • @After

이 때 PointCut 표현식은 어노테이션 내에 직접 작성하거나, 이미 명명된 PointCut을 사용할 수 있습니다.

@Aspect
@Component
public class MyAspect {

	@Pointcut("표현식1")
    public void myPointCut1() {}
    
    @Pointcut("표현식2")
    public void myPointCut2() {}
    
    @Pointcut("myPointCut1() && myPointCut2()")
    public void myCompositionPointCut() {}
    
    @After("표현식")
    public void myAfterAdvice() {
    	// Advice 관련 내용
    }
    
    @After("myPointCut1()")
    public void myAfterAdviceAtMyPointCut1() {
    	// Advice 관련 내용
    }

}

2. Spring AOP API

앞에서 소개한 @AspectJ, 스키마 기반 방식 외 하위 수준의 스프링 AOP API도 존재합니다.

Pointcut API

스프링의 Pointcut 모델은 어드바이스 유형에 관계 없이 Pointcut 재사용을 가능하게 하며 동일한 Pointcut으로 다른 Advice를 목표로 삼을 수 있다고 합니다. 아래 인터페이스는 특정 클래스 및 메서드에 대한 조언들 대상으로 하는데 사용하는 중앙 인터페이스입니다.

// Pointcut 인터페이스
public interface Pointcut {

	// Pointcut을 주어진 대상 클래스로 제한하는데에 사용하는 ClassFilter
    ClassFilter getClassFilter();
    
    // Pointcut이 대상 클래스의 주어진 메서드와 일치하는지 여부를 테스트하는데에 사용하는 MethodMatcher
    MethodMatcher getMethodMatcher();

}

// ClassFilter 인터페이스
public interface ClassFilter {

	// Pointcut을 주어진 대상 클래스 세트로 제한하는데에 사용
	boolean matches(Class clazz);
}

// MethodMatcher 인터페이스
public interface MethodMatcher {

	// Pointcut이 대상 클래스의 주어진 메서드와 일치하는지 여부를 테스트
	boolean matches(Method m, Class<?> targetClass);

	boolean isRuntime();

	boolean matches(Method m, Class<?> targetClass, Object... args);
}

Advice API

Advice의 수명 주기
각 Advice는 스프링 빈이며 해당 인스턴스는 모든 Advice 객체에서 고유하거나 각 Advice마다 고유할 수 있습니다. 클래스별, 인스턴스별 Advice는 수명 주기가 각자 다릅니다.

클래스별 Advice는 가장 자주 사용되며 일반적인 Advice에 적합하고 프록시된 객체의 상태에 의존하거나 새로운 상태를 추가하지 않습니다. 인스턴스별 Advice는 프록시된 객체에 상태를 추가합니다.

Spring Advice 유형
스프링은 다양한 Advice 유형을 제공하며 임의의 Advice 유형을 지원하도록 확장할 수도 있습니다.

Interception Around Advice
가장 기본적인 Advice 유형이며 MethodInterceptor를 구현하고 Around Advice를 구현하는 클래스는 아래의 인터페이스도 같이 구현해야 합니다.

public interface MethodInterceptor extends Interceptor {
	// MethodInvocation : 호출중인 메서드, 대상 JoinPoint, AOP 프록시 및 메서드에 대한 인수 노출
    // invoke 메서드는 호출의 결과 즉 JoinPoint의 반환값을 반환해야 함
    Object invoke(MethodInvocation invocation) throws Throwable;
}

Before Advice
가장 간단한 Advice 유형이며 메서드에 들어가기 전에만 호출되므로 MethodInvocation이 필요가 없으며 proceed() 실수로 인터셉터 체인이 실패할 가능성이 없다는 장점이 있습니다.
주의할 점은, Before Advice가 예외를 던지면 인터셉터 체인의 추가 실행이 중지되며 예외가 인터셉터 체인으로 다시 전파되고 이 예외가 호출된 메서드의 시그니처에 있지 않다면 AOP 프록시에 의해 확인되지 않은 예외로 래핑됩니다.

// BeforeAdvice를 확장한 MethodBeforeAdvice
public interface MethodBeforeAdvice extends BeforeAdvice {

	void before(Method m, Object[] args, Object target) throws Throwable;
}

Throws Advice
JoinPoint가 예외를 던진 경우 JoinPoint 반환 후 호출됩니다.
주의할 점은 ThrowsAdvice 인터페이스에는 어떠한 메서드도 포함되어 있지 않고 마지막 인자인 타입화된 throwable만 필수라는 점입니다. (주어진 객체가 하나 이상의 타입화된 Throws 어드바이스 메서드를 구현한다는 것)

afterThrowing([Method, args, target], subclassOfThrowable)

ThrowsAdvice가 예외 자체를 발생시키는 경우 원래 예외를 재정의하며 재정의죈 예외는 일반적으로 모든 메서드 서명과 호환되는 RuntimeException입니다. 그러나 ThrowsAdvice 메서드가 확인된 예외를 발생시니는 경우 대상 메서드의 선언된 예외와 일치해야 합니다. 따라서 대상 메서드의 시그니처와 호환되지 ㅇ낳는 선언되지 않은 확인 예외를 발생시켜서는 안됩니다.

After Returing Advice
반환 값(수정할 수는 없음), 호출된 메서드, 메서드의 인수 및 대상에 액세스 할 수 있습니다.

public interface AfterReturningAdvice extends Advice {

	void afterReturning(Object returnValue, Method m, Object[] args, Object target)
			throws Throwable;
}

Advisor API

Advisor는 Pointcut 표현식과 관련된 단일 Advice 객체만을 포함하는 Aspect입니다. 즉, Pointcut + Advice라고 할 수 있습니다.

ProxyFactoryBean

위에서 언급한 프록시 팩토리와 관련된 자바 빈입니다. ProxyFactoryBean은 프록시할 대상을 지정하고 CGLIB을 사용할 지 여부를 지정할 수 있습니다.

Appendix. 출처

https://docs.spring.io/spring-framework/reference/core/aop.html

profile
Backend Developer

0개의 댓글