⬜섹션9 스프링 AOP 개념
◼️ AOP 개념
앱의 기능은 핵심 기능과 부가 기능으로 나뉜다. 로깅처럼, 보통 부가 기능은 여러 클래스에 걸쳐서 함께 사용된다. 이러한 부가 기능은 횡단 관심사(cross-cutting concerns)이다.
🟢 AOP 탄생 과정
- 부가 기능을 적용할 때 반복과 중복 코드가 많이 발생하고, 수정 시에도 많은 작업이 필요했음
- 이를 해결하기 위해 @Aspect를 사용해 부가 기능과 적용 범위를 정의
- 이렇게 Aspect를 사용하는 프로그래밍 방식을 관점 지향 프로그래밍(AOP, Aspect-Oriented Programming)이라 하며, AOP는 객체 지향 프로그래밍(OOP)을 보완하여 횡단 관심사를 깔끔하게 처리하기 위해 개발됨
◼️ AOP 적용 방식
🟢 AOP 부가 기능 로직 추가 방식 세가지
- 컴파일 시점 (.java → .class)
- 클래스 로딩 시점 (.class → JVM에 저장 전 조작)
- 런타임 시점(프록시) (main메서드 실행 다음)
1,2번은 특별한 컴파일러나, 자바를 실행할 때 복잡한 옵션과 로더 조작기를 설정해야 한다. 하지만 3은 앱스프링만 있으면 얼마든지 AOP를 적용할 수 있다.
프록시는 메서드 오버라이딩 개념으로 동작하기 때문에, 프록시를 사용하는 스프링 AOP의 조인 포인트(AOP를 적용할 수 있는 지점)는 메서드 실행으로 제한된다. 그리고 스프링 컨테이너가 관리할 수 있는 스프링 빈에만 AOP를 적용할 수 있다.
◼️ AOP 용어 정리
- 조인 포인트(Join point): AOP를 적용할 수 있는 모든 지점 (스프링 AOP - 프록시 방식을 사용하므로 메소드 실행 지점으로 제한)
- 포인트컷(Pointcut): 조인 포인트 중 어드바이스가 적용될 위치를 선별하는 기능
- 타겟(Target): 어드바이스를 받는 객체, 포인트컷으로 결정
- 어드바이스(Advice): 부가 기능
- 에스펙트(Aspect): 어드바이스 + 포인트컷을 모듈화 한 것 @Aspect
- 어드바이저(Advisor): 1 어드바이스 + 1 포인트컷
- 위빙(Weaving): 포인트컷으로 결정한 타겟 조인 포인트에 어드바이스를 적용하는 것
- AOP 프록시: AOP 기능 구현을 위해 만든 프록시 객체 (JDK 동적 프록시 또는 CGLIB프록시)
⬜섹션10 스프링 AOP 구현
AOP 기능을 사용하려면 build.gradle에 아래를 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
◼️ 예제 프로젝트 - AOP 프록시 설정 및 테스트
아래와 같이 AOP 프록시를 설정하면, 프록시가 잘 적용됨을 확인할 수 있다.
@Slf4j
@Aspect
public class AspectV1 {
//hello.aop.order 패키지와 하위 패키지
@Around("execution(* hello.aop.order..*(..))") //포인트컷
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable { //어드바이스
log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
return joinPoint.proceed();
}
}
@Slf4j
@Import(AspectV1.class) // 추가
@SpringBootTest
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void aopInfo() { // AOP 프록시가 적용 되었는지 확인
log.info("isAopProxy, orderService={}", AopUtils.isAopProxy(orderService)); // true
log.info("isAopProxy, orderRepository={}", AopUtils.isAopProxy(orderRepository)); // true
}
@Test
void success() {
orderService.orderItem("itemA");
}
@Test
void exception() {
assertThatThrownBy(() -> orderService.orderItem("ex"))
.isInstanceOf(IllegalStateException.class);
}
}
◼️ 예제 프로젝트 - 포인트컷 분리
아래와 같이 포인트 컷을 분리할 수 있다. allOrder()는 이름 그대로 주문 관련 모든 기능을 대상으로 하는 포인트 컷이다.
@Slf4j
@Aspect
public class AspectV2 {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))") //pointcut expression
private void allOrder() {} // pointcut signature(메서드이름 + 파라미터)
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
◼️ 예제 프로젝트 - 어드바이스 추가
기존의 로깅 기능에 추가로 트랜잭션 적용 코드도 추가해보자. 여기서는 진짜 트랜잭션을 실행하는 건아니고, 기능이 동작한 것처럼 로그만 남긴다.
🟢 트랜잭션 기능 동작
- 트랜잭션 시작
- 핵심 로직 실행
- 핵심 로직 실행에 문제가 없으면 커밋
- 핵심 로직 실행에 예외가 발생하면 롤백
🔶 트랜잭션 적용 AOP 코드
@Slf4j
@Aspect
public class AspectV3 {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder(){}
//클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
private void allService(){}
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//hello.aop.order 패키지와 하위 패키지면서 클래스 이름 패턴이 *Service
@Around("allOrder() && allService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable
{
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
log.info("[트랜잭션 롤백] {}",joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
orderService 에는 doLog(), doTransaction() 두가지 어드바이스가 적용되어 있고, orderRepository 에는 doLog() 하나의 어드바이스만 적용된 것을 확인할 수 있다.
또한 예외 상황에서는 트랜잭션 커밋대신에 롤백이 호출됨을 볼 수 있다.
◼️ 스프링 AOP 구현4 - 포인트컷 참조
포인트컷을 공용으로 사용하기 위해 별도의 외부 클래스에 모아두어도 된다.
(+ 외부에서 호출할 때는 포인트컷 접근 제어자를 public 으로 해야 한다.)
package hello.aop.order.aop;
public class Pointcuts {
//hello.springaop.app 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))") public void allOrder(){}
//타입 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))") public void allService(){}
//allOrder && allService
@Pointcut("allOrder() && allService()")
public void orderAndService(){}
}
사용법은 아래처럼, 패키지명을 포함한 클래스 이름과 포인트컷 시그니처를 모두 지정하면 된다.
@Slf4j
@Aspect
public class AspectV4Pointcut {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable
{
try {
log.info(" [트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
◼️ 스프링 AOP 구현5 - 어드바이스 순서
어드바이스는 순서를 보장하지 않는다. 순서를 지정하려면 @Aspect 적용단위로 @~..Order 애노테이션을 적용해야 한다.
즉, 에스펙트를 별도의 클래스로 분리해야 한다. 참고로 Order는 숫자가 작을수록 먼저 실행된다.
@Slf4j
public class AspectV5Order {
@Aspect
@Order(2)
public static class LogAspect {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
public static class TxAspect {
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws
Throwable {
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature()); Object result = joinPoint.proceed(); log.info("[트랜잭션 커밋] {}", joinPoint.getSignature()); return result;
} catch (Exception e) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature()); }
}
}
}
◼️ 스프링 AOP 구현6 - 어드바이스 종류
- @Around: 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능
- @Before: 조인 포인트 실행 이전에 실행
- @AfterReturning: 조인 포인트가 정상 완료후 실행 `@AfterThrowing` : 메서드가 예외를 던지는 경우 실행
- @After: 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
🟢 좋은 설계는 제약이 있다!
어노테이션을 통해 코드의 작성 의도를 명확히 할 수 있다. 예를들어, @Before라는 애노테이션을 보는 순간 타겟 실행 전에 한정해서 일을 하는 코드임을 알 수 있다.
🔶 실습 코드
@Slf4j
@Aspect
public class AspectV6Advice {
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable
{
try {
//@Before
log.info("[around][트랜잭션 시작] {}", joinPoint.getSignature()); Object result = joinPoint.proceed();
//@AfterReturning
log.info("[around][트랜잭션 커밋] {}", joinPoint.getSignature()); return result;
} catch (Exception e) {
//@AfterThrowing
log.info("[around][트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
//@After
log.info("[around][리소스 릴리즈] {}", joinPoint.getSignature()); }
}
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()",
throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[ex] {} message={}", joinPoint.getSignature(),
ex.getMessage());
}
@After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
◼️ 참고 정보 획득
모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫번째 파라미터에 사용할 수 있다. (생략해도 된다.)
단 @Around 는 ProceedingJoinPoint 을 사용해야 한다.
참고로 ProceedingJoinPoint 는 org.aspectj.lang.JoinPoint 의 하위 타입인데, proceed()로 다음 어드바이스나 타겟을 호출한다.
🟢 JoinPoint 인터페이스 주요 기능
- getArgs(): 메서드 인수를 반환
- getThis(): 프록시 객체를 반환
- getTarget(): 대상 객체를 반환
- getSignature(): 조언되는 메서드에 대한 설명을 반환
- toString(): 조언되는 방법에 대한 유용한 설명을 print
'Spring' 카테고리의 다른 글
[Spring] 스프링 부트 - 핵심 원리와 활용 섹션2,3 스프링부트,웹 서버와 서블릿 컨테이너 (1) | 2024.09.20 |
---|---|
[Spring] 스프링 핵심 원리 - 고급편 섹션11 스프링 AOP - 포인트컷 (0) | 2024.08.21 |
[Spring] 스프링 핵심 원리 - 고급편 섹션8 @Aspect AOP (0) | 2024.08.16 |
[Spring] 스프링 핵심 원리 - 고급편 섹션7 빈 후처리기 (0) | 2024.08.14 |
[Spring] 스프링 핵심 원리 - 고급편 섹션6 스프링이 지원하는 프록시 (0) | 2024.08.04 |