스프링 AOP (Aspect Oriented Programming)
- 관점 지향 프로그래밍
- 공통 기능을 분리하고 모듈화 하여 지정 시점에 해당 로직이 실행하게 합니다.
- 모듈 : 소프트웨어 설계에서 기능단위로 분해하고 추상화되어 재사용 및 공유 가능한 수준으로 만들어진 단위
- 모듈화 : 소프트웨어의 성능을 향상시키거나 시스템의 디버깅, 시험, 통합 및 수정을 용이하도록 하는 소프트웨어 설계 기법
위와 같이 흩어진 관심사(Crosscutting Concerns)를 Aspect로 모듈화 하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지입니다.
AOP 적용 방식
- 컴파일 시점
- .java 파일을 컴파일러를 통해 .class를 만드는 시점에 부가 기능 로직을 추가하는 방식
- 모든 지점에 적용 가능
- AspectJ가 제공하는 특별한 컴파일러를 사용해야 하기 때문에 특별할 컴파일러가 필요한 점과 복잡하다는 단점
- 클래스 로딩 시점
- .class 파일을 JVM 내부의 클래스 로더에 보관하기 전에 조작하여 부가 기능 로직 추가하는 방식
- 모든 지점에 적용 가능
- 특별한 옵션과 클래스 로더 조작기를 지정해야 하므로 운영하기 어려움
- 런타임 시점
- 스프링이 사용하는 방식
- 컴파일이 끝나고 클래스 로더에 이미 다 올라가 자바가 실행된 다음에 동작하는 런타임 방식
- 실제 대상 코드는 그대로 유지되고 프록시를 통해 부가 기능이 적용
- 프록시는 메서드 오버라이딩 개념으로 동작하기 때문에 메서드에만 적용 가능 -> 스프링 빈에만 AOP를 적용 가능
- 특별한 컴파일러나, 복잡한 옵션, 클래스 로더 조작기를 사용하지 않아도 스프링만 있으면 AOP를 적용할 수 있기 때문에 스프링 AOP는 런타임 방식을 사용
AOP 주요 개념
- Aspect : 위에서 설명한 흩어진 관심사를 모듈화 한 것입니다. 주로 부가기능을 모듈화 합니다.
- PointCut + Advice을 모듈화 한 것
- @Aspect와 같은 의미
- Target : Aspect를 적용하는 곳입니다(클래스, 메서드.. )
- advice의 대상이 되는 객체
- PointCut으로 결정
- Advice : 실질적으로 어떤 일을 해야 할 지에 대한 것, 실질적인 부가기능을 담은 구현체를 말합니다.
- 공통 기능의 메서드, 코드 = 특정 조인 포인트에서 Aspect에 의해 취해지는 조치
- 동작 시점
- JoinPoint : Advice가 적용될 위치, 끼어들 수 있는 지점을 말합니다. 메서드 진입 지점과 생성자 호출지점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용이 가능합니다.
- 추상적인 개념으로 advice가 적용될 수 있는 모든 위치
- 스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메서드 실행 지점
- PointCut : JoinPoint의 상세한 스펙을 정의한 것을 말합니다. 'A란 메서드의 진입 시점에 호출할 것'과 같이 더욱 구체적으로 Advice가 실행될 지점을 정할 수 있습니다.
- 조인 포인트 중에서 advice가 적용될 위치를 선별하는 기능
- 스프링 AOP는 프록시 기반이기 때문에 조인 포인트가 메서드 실행 시점에만 존재하고 포인트 컷도 동일하다.
- Weaving : AOP Framework가 공통 코드를 핵심 코드에 삽입하는 것
- ex ) AspectJ, Spring AOP, ...
- PointCut으로 결정한 타겟의 JoinPoint에 Advice를 적용하는 것
- AOP 프록시
- AOP 기능을 구현하기 위해 만든 프록시 객체
- 스프링에서 AOP 프록시는 JDK 동적 프록시 또는 CGLIB 프록시
- 스프링의 AOP의 기본값은 CGLIB 프록시
Aspect
스프링은 빈을 등록할 때, 빈 후처리기에서 모든 Advisor 빈을 조회한 뒤 Pointcut으로 매칭해 보면서 프록시 적용 대상인지 판단하고 대상이라면 프록시를 빈으로 등록합니다.
@Aspect Annotation을 사용한다면 Advisor를 더욱 쉽게 구현이 가능합니다.
일단 사용 전 의존성 추가 코드를 추가합니다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
해당 의존성을 추가하면 자동 프록시 생성기(AnnotationAwareAspectJAutoProxyCreator)를 사용할 수 있게 되고 이것이 Advisor기반으로 프록시를 생성하는 역할을 합니다.
자동 프록시 생성기는 @Aspect를 보고 Advisor로 변환하여 저장하는 작업을 수행합니다.
자동 프록시 생성기에 의해 @Aspect에서 Advisor로 변환된 Advisor는 @Aspect Advisor빌더 내부에 저장됩니다.
동작 과정
- 스프링이 BEAN 대상이 되는 객체를 생성합니다. (@Bean, Component 스캔 대상)
- 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달합니다.
- 모든 Advisor 빈을 조회합니다.
- @Aspect Advisor 빌더 내부에 저장된 모든 Advisor를 조회합니다.
- 3, 4에서 조회한 Advisor에 포함되어 있는 포인트컷을 통해 클래스와 메서드 정보를 매칭하면서 프록시를 적용할 대상인지 아닌지 판단합니다.
- 여러 Advisor의 하나라도 포인트 컷의 조건을 만족한다면 프록시를 생성하고 프록시를 Bean 저장소로 반환합니다.
- 만약 프록시 생성 대상이 아니라면 들어온 Bean 그대로 Bean 저장소로 반환합니다.
- Bean 저장소는 객체를 받아 Bean으로 등록합니다.
주의사항
@Aspect는 Advisor를 쉽게 만들어 주는 역할이지 컴포넌트 스캔이 되는 것이 아닙니다.
따라서 반드시 스프링 빈으로 등록을 따로 해주어야 합니다.
- @Bean으로 수동 등록
- @Component로 컴포넌트 스캔 사용하여 자동 등록
- @Import를 사용하여 파일 추가
Advice
Advice는 실질적으로 프록시에서 수행하게 되는 로직을 정의하게 되는 곳입니다.
스프링에서는 Advice에 관련된 5가지 Annotation을 제공합니다.
Annotation 종류에 따라 Pointcut에 지정된 대상 메서드에서 Advice가 실행되는 시점을 정할 수 있습니다.
또한 속성값으로 Pointcut을 지정할 수 있습니다.
Advice 종류
- @Before : 메서드 실행 전 실행, 입력값 조작 불가능 ( setter 수정자가 존재하면 내부값 수정은 가능 )
- @After : 메서드 성공 실행 후( 예외 발생 되더라도 실행 == 무조건 실행 )
- @AfterReturning : (정상 반환 이후)타겟 메서드가 성공적으로 결과값을 반환한 후에 어드바이스 기능을 수행, 반환값 조작 불가능 ( 반환값 내부에 setter 수정자가 존재하면 내부값 수정은 가능 )
- @AfterThrowing : (예외 발생 이후)타겟 메서드가 수행 중 예외를 던지게 되면 어드바이스 기능을 수행 ( 예외 조작 불가능 )
- @Around : (메서드 실행 전후) 어드바이스가 타겟 메서드를 감싸 타겟 메서드 호출전과 후에 어드바이스 기능을 수행 (예외가 발생하더라도 실행 )
@Around
@Slf4j // → Logger 객체 생성 없이 바로 log.debug()로 로그를 찍어볼 수 있게 해주는 역할
@Aspect
public class AspectV6Advice {
@Around("execution(* com.example.mvc.order..*(..))")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
// @Before 수행
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
// @Before 종료
// Target 메서드 호출
Object result = joinPoint.proceed();
// Target 메서드 종료
// @AfterReturning 수행
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
// @AfterReturning 종료
// 값 반환
return result;
} catch (Exception e) {
// @AfterThrowing 수행
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
// @AfterThrowing 종료
} finally {
//@ After 수행
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
//@ After 종료
}
}
}
@Around는 나머지 4개의 annotation을 모두 포함하고 있습니다.
주석을 보면 각 annotation이 어느 시점에 적용되고 있는지 확인할 수 있습니다.
모든 Advice annotation은 첫 번째 파라미터로 org.aspectj.lang.JoinPoint를 사용할 수 있는데 @Around만 예외적으로 JoinPoint의 하위타입인 ProceedingJoinPoint를 사용합니다.
# JoinPoint의 인터페이스 주요 기능
- getArgs() : 메서드 인수 반환
- getThis() : 프록시 객체 반환
- getTarget() : 대상 객체 반환
- getSignature() : 조인되는 메서드에 대한 설명 반환
- toString() : 조인되는 방법에 대한 유용한 설명 인쇄
ProceedingJoinPoint의 주요 기능에는 위의 주요 기능에서 다음 advice나 타겟을 호출하는 proceed() 메서드가 추가됩니다.
이는 @Around를 나머지 4개의 annotation으로 분리한 이유와 연관이 있습니다.
위 코드를 보면 joinPoint.proceed() 메서드로 Target 메서드를 호출하는 코드가 존재합니다.
@Around를 제외한 나머지 4개의 annotation은 보통 인자로 JoinPoint를 받아 사용하고, proceed를 호출하지 않습니다.
즉, @Around의 경우 ProceedingJoinPoint를 인자로 받아 타겟 메서드를 실행하는 proceed 코드를 반드시 적어야 target 메서드를 호출하지만 나머지 annotation의 경우, 타겟 메서드를 호출하는 proceed를 명시하지 않아도 자동으로 호출됩니다.
따라서 실수로 process 코드를 작성하지 않아 발생할 수 있는 실수를 방지할 수 있습니다.
또한, annotation을 분리함으로 인해 의도를 명확하게 판단할 수 있게 되었습니다.
하나 더 큰 차이가 있다면 @Around는 입력, 반환값 자체를 다른 객체로 조작이 가능하지만, 나머지 annotation의 경우에는 입력, 반환값 객체 자체를 다른 객체로 조작할 수 없습니다.
@Before
@Before은 조인 포인트 실행 전( 타겟 메서드 실행 전 )에 작업을 수행합니다.
@Before("execution(* com.example.mvc.order..*(..))")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@Around와 달리 proceed 코드 없이 정의한 로직이 수행된 후 자동으로 target 메서드를 호출합니다.
@AfterReturing
@AfterReturing은 조인 포인트가 정상적으로 실행되고 값을 반환할 때 실행됩니다. ( 타겟 메서드가 예외가 아닌 정상값을 반환할 때 )
@AfterReturning(value = "execution(* com.example.mvc.order..*(..))", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
다른 annotation과 다르게 속성값으로 returning이 추가되었습니다.
이 부분에는 Target 메서드가 반환하는 변수명을 적어주고, advice 메서드의 인자로 변수명을 일치시켜 준다면 해당 값을 가져와서 사용할 수 있습니다.
여기서 주의할 점은 returning값을 받는 인자의 타입이 해당 리턴 값의 부모타입 혹은 같은 타입이어야만 해당 Advice가 동작한다는 것입니다.
타입이 부모 혹은 동일 타입이 아니라면 해당 Advice 자체가 동작하지 않습니다.
@AfterThrowing
@AfterThrowing은 타겟 메서드 실행이 예외를 던져서 종료될 때 실행됩니다.
@AfterThrowing(value = "execution(* com.example.mvc.order..*(..))", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
}
@AfterReturing과 비슷하게 throwing 속성이 추가되고 advice 메서드 인자에 변수명을 일치시키면 받아서 사용할 수 있습니다.
Annotation 동작 순서
동일한 @Aspect 안에서는 위와 같은 우선순위로 동작합니다.
즉, 동일한 @Aspect 안에서 여러 개의 Advice가 존재하는데 타겟 메서드가 여러 Advice의 대상이 될 경우 다음과 같이 동작합니다.
Around → Before → AfterThrowing → AfterReturning → After → Around
Advice 순서 지정하기
annotation 동작 순서는 정의되어 있더라도, 같은 annotation에 대한 동작 순서는 보장되어있지 않습니다.
@Slf4j
@Aspect
public class AspectV1 {
@Around("execution(* com.example.mvc.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint)throws Throwable{
// 생략
}
@Around("execution(* com.example.mvc.service..*(..))")
public Object anotherLog(ProceedingJoinPoint joinPoint)throws Throwable{
// 생략
}
}
위와 같이 같은 수준의 annotation이 붙어 있을 때, 동작 순위를 보장하지 않습니다.
# 순서를 보장하려면?
- @Aspect 적용 단위로 @Order annotation을 지정해야 합니다.
- Advice 단위가 아닌 @Aspect 클래스 단위로 지정해줘야 합니다.
- 하나의 Aspect 안에 여러 Advice가 존재한다면 순서를 보장할 수 없기에 별도의 클래스로 분리해주어야 합니다.
@Slf4j
public class AspectV5Order {
@Aspect
@Order(1)
public static class TxAspect {
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTx(ProceedingJoinPoint joinPoint) throws Throwable {
// 생략
}
}
@Aspect
@Order(2)
public static class LogAspect {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 생략
}
}
}
위에서는 내부 static 클래스로 분리했지만, 따로 클래스를 분리해도 됩니다.
결과적으로 아래 그림처럼 동작하게 됩니다.

포인트 컷 분리
@Slf4j
@Aspect
public class AspectV6Advice {
@Around("execution(* com.example.mvc.order..*(..))")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
// 생략
}
앞서 Advice annotation의 속성으로 Pointcut을 명시에서 사용하였습니다.
이렇게 annotation 마다 각각의 Pointcut을 명시할 수 있지만 보통 분리해서 한 곳에서 만들어두고 가져다가 사용을 합니다.
@Slf4j
@Aspect
public class AspectV2 {
// Pointcut 분리
// com.example.mvc.order패키지와 하위 패키지
@Pointcut("execution(* com.example.mvc.order..*(..))")
private void allOrder(){}
// 분리된 pointcut 적용
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint)throws Throwable{
// 생략
}
// 분리된 pointcut 적용
@Around("allOrder()")
public Object anotherJob(ProceedingJoinPoint joinPoint)throws Throwable{
// 생략
}
// 다른 Pointcut
// 클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
private void allService(){}
// Pointcut 조합
@Around("allOrder() && allService()")
public Object anotherJob(ProceedingJoinPoint joinPoint)throws Throwable{
// 생략
}
}
하나의 Aspect 안에서 여러 개의 Advice를 정의하는데 Pointcut으로 사용하는 것이 같다면 private로 뽑아두고 사용할 수 있습니다.
또한, &&과 ||으로 조합하여 사용할 수도 있습니다.
package com.example.mvc.order.aop;
public class Pointcuts {
@Pointcut("execution(* com.example.mvc.order..*(..))")
public void allOrder(){}
}
@Slf4j
@Aspect
public class AspectV3 {
@Around("com.example.mvc.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint)throws Throwable{
// 생략
}
}
만약 Pointcut을 공통적으로 많은 곳에서 사용한다면 클래스로 빼두고 사용할 수 있습니다.
위처럼 따로 Pointcut을 외부로 뽑아두고 사용하는 경우, 사용하는 Aspect에서는 해당 Pointcut이 위치한 패키지명, 클래스명, 메서드명 형식으로 명시하면 됩니다.
[ 참고 자료 ]
'Spring' 카테고리의 다른 글
| Spring Security / CIA와 RSA (0) | 2023.03.18 |
|---|---|
| Spring Security / TCP (1) | 2023.03.18 |
| Spring Security 적용시 circular reference, dependency cycle 형성 해결법 (0) | 2023.03.17 |
| SPRING ANNOTATION (0) | 2023.03.17 |
| 자바 Extends, Implements 차이점 (0) | 2023.03.17 |




댓글