Spring Advanced

AOP (Part.2)

cwchoiit 2024. 1. 2. 09:54
728x90
반응형
SMALL

이번 포스팅에서는 저번 포스팅인 https://cwchoiit.tistory.com/85 이 Part.1 에 이어 스프링 AOP를 직접 만들어보자.

 

AOP(Aspect Oriented Programming)

이제 드디어 AOP에 대해 진지하게 알아보는 시간을 가져보자. 우선 AOP란 번역 하면 관점 지향 프로그래밍이다. 여기서 관점이란 무엇일까? 관점은 애플리케이션의 핵심적인 관점과 부가적인 관

cwchoiit.tistory.com

 

728x90
SMALL

 

라이브러리 추가

우선 스프링 AOP를 사용하려면 다음과 같은 라이브러리가 필요하다.

 

build.gradle

 implementation 'org.springframework.boot:spring-boot-starter-aop'

 

이 라이브러리를 추가한 후에 다운된 외부 라이브러리 목록을 보면 다음 라이브러리가 있어야 한다.

 

 

 

핵심적인 관점(기능)

애플리케이션의 핵심적인 기능을 담당하는 비즈니스 로직이다. 상품에 대한 주문 관련 코드가 있다고 가정하자.

 

OrderRepository.java

package com.example.aop.order;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

@Slf4j
@Repository
public class OrderRepository {

    public void save(String itemId) {

        log.info("[save] Execute.");

        if (itemId.equals("ex")) {
            throw new IllegalStateException("예외 발생!");
        }

        sleep(1000);
    }

    private void sleep(int ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

 

OrderService.java

package com.example.aop.order;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class OrderService {

    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public void orderItem(String itemId) {
        log.info("[orderItem] Execute.");
        orderRepository.save(itemId);
    }
}

 

이렇게 두 개의 클래스가 있고 Service에서 Repository에 호출이 있다. 이 두 개의 클래스가 모두 공통적으로 관심있는 사항이 있다면 이 두개의 클래스 각각의 같은 코드를 하나하나 넣는게 아니라 부가적인 관점(기능)을 담당하는 모듈을 만들어서 관리하는 AOP를 적용하자.

 

 

부가적인 관점(기능)

예제 코드니까 어떤 로그를 찍는 부가 기능을 가진다고 가정해보자.

 

AspectV1.java

package com.example.aop.order.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

/**
 * 스프링은 프록시 방식의 AOP 를 사용한다. 즉, 프록시를 통하는 메서드만 적용 대상이 된다.
 * 그래서 아래 @Around() 에 정규 표현식도 특정 패키지의 특정 메서드명의 특정 파라미터를 정규식으로 작성하는 것.
 *
 * 스프링 AOP 는 AspectJ의 문법을 차용하고 프록시 방식의 AOP 를 제공한다. AspectJ를 직접 사용하는 것이 아니다.
 * 스프링 AOP 를 사용할 땐 @Aspect 애노테이션을 주로 사용하는데 이 애노테이션도 AspectJ가 제공하는 애노테이션이다.
 * */
@Slf4j
@Aspect
public class AspectV1 {

    @Around("execution(* com.example.aop.order..*(..))")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[doLog] {}", joinPoint.getSignature()); // JoinPoint Signature
        return joinPoint.proceed(); // 실제 Target 호출
    }
}

 

이렇게 부가 기능을 에스팩트로 만들려면 스프링에서는 @Aspect 애노테이션을 달면 된다. 그리고 그 안에 @Around 애노테이션을 달아서 포인트컷을 지정하고 그 안의 로직이 어드바이스가 된다. 포인트컷의 정규 표현식은 com.example.aop.order 패키지와 그 하위 패키지 전부에 해당하는 어떠한 파라미터를 가져도 상관없는 모든 메서드에 적용한다. 

 

적용할 부가 기능은 로그를 하나 출력하는 것인데 로그의 내용은 호출된 메서드의 패키지와 메서드 명을 출력해주는 joinPoint.getSignature()를 호출한다. 그 후 프록시는 실제 객체를 호출하여 실제 객체의 핵심 기능에 대한 코드를 수행해야 하므로 실제 객체 호출 코드인 joinPoint.proceed()를 호출한다.

 

이렇게 애스팩트를 만들면 이 녀석안의 @Around 애노테이션이 달린 메서드가 하나의 어드바이저가 된다. 어드바이저는 포인트컷 + 어드바이스이고 이 어드바이저의 포인트컷은 위에서 설명한 경로와 같고 어드바이스는 위에서 설명한 로그 출력 기능과 같다.

 

이렇게만 만들었다고 해서 AOP가 바로 적용되는 것은 아니다. 이 애스팩트를 스프링 빈으로 등록해줘야 스프링이 AOP 프록시로 만들어준다. 등록해보자.

 

등록하는 방법은 다양하게 있다. @Bean, @Component, @Import,... 여기서는 테스트 코드에만 적용해볼거니까 간단하게 @Import를 사용해보자.

 

AopTest.java

package com.example.aop;

import com.example.aop.order.OrderRepository;
import com.example.aop.order.OrderService;
import com.example.aop.order.aop.*;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

import static org.assertj.core.api.Assertions.*;

@Import(AspectV1.class) //@Import 만으로도 빈으로 등록하는것과 동일하다. 주로 @Configuration 에서 추가할 때 자주 사용됐지만, @Import 로도 그 안에 클래스들을 빈으로 등록한다.
@Slf4j
@SpringBootTest
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
    }

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Test
    void exception() {
        assertThatThrownBy(() -> orderService.orderItem("ex"))
                .isInstanceOf(IllegalStateException.class);
    }
}

 

@Import 애노테이션으로 애스팩트를 스프링 빈으로 등록한다. 그 후 테스트 코드로 success() 메서드를 실행해보면, AOP가 적용된 모습을 확인할 수 있다.

애스팩트에 작성한 로그가 출력된 모습이다. 이렇게 간단하게 AOP를 구현했다. 

 

포인트컷 분리

포인트컷과 어드바이스를 분리할 수도 있다. 

 

AspectV2.java

package com.example.aop.order.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

/**
 * 포인트컷과 어드바이스를 분리하는 법.
 * */
@Slf4j
@Aspect
public class AspectV2 {

    /**
     * 반환 타입은 'void' 여야 한다.
     * 다른 곳에서 이 포인트컷을 사용하려면 public 이어야 하고 이 내부 안에서 사용하는 건 private 이어도 된다.
     * */
    @Pointcut("execution(* com.example.aop.order..*(..))")
    private void allOrder() {

    } // Pointcut Signature

    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[doLog] {}", joinPoint.getSignature()); // JoinPoint Signature
        return joinPoint.proceed(); // 실제 Target 호출
    }
}

 

위 코드처럼 @Pointcut 애노테이션을 사용해서 포인트컷을 작성하고 이 포인트컷을 사용하는 어드바이스에서는 @Around에 해당 메서드를 입력하면 된다. @Around("allOrder()").

 

이렇게 포인트컷을 분리하여 얻는 이점은 다음과 같다.

  • 포인트컷에 의미를 부여할 수 있다. (모든 주문에 대해: allOrder)
  • 여러 어드바이스에서 해당 포인트컷을 가져다가 사용할 수 있다. (쉽게 말해 모듈화가 된다는 것)

한가지 더 알아야 할 내용은 같은 클래스 내에서 포인트컷을 분리할 땐 접근 제어자가 private, public 상관없이 가져다 사용할 수 있지만 외부에서 포인트컷을 사용하려면 public이어야 한다.

 

이 애스팩트(AspectV2)를 임포트해서 테스트 코드를 돌려도 동일한 결과를 얻는다.

 

AopTest.java

package com.example.aop;

import com.example.aop.order.OrderRepository;
import com.example.aop.order.OrderService;
import com.example.aop.order.aop.*;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

import static org.assertj.core.api.Assertions.*;

@Import(AspectV2.class)
@Slf4j
@SpringBootTest
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
    }

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Test
    void exception() {
        assertThatThrownBy(() -> orderService.orderItem("ex"))
                .isInstanceOf(IllegalStateException.class);
    }
}

 

 

어드바이스 추가

하나의 프록시는 여러개의 어드바이저를 가질 수 있다고 했다. 그러니까 에스팩트에 여러 어드바이저를 만들어서 그런 상황을 만들어보자. 

 

AspectV3.java

package com.example.aop.order.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Slf4j
@Aspect
public class AspectV3 {

    /**
     * 반환 타입은 'void' 여야 한다.
     * 다른 곳에서 이 포인트컷을 사용하려면 public 이어야 하고 이 내부 안에서 사용하는 건 private 이어도 된다.
     * */
    @Pointcut("execution(* com.example.aop.order..*(..))")
    private void allOrder() {} // Pointcut Signature

    @Pointcut("execution(* *..*Service.*(..))")
    private void allService() {}

    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[doLog] {}", joinPoint.getSignature()); // JoinPoint Signature
        return joinPoint.proceed(); // 실제 Target 호출
    }

    /**
     * com.example.aop.order 의 모든 하위 패키지 이면서 타입(클래스, 인터페이스) 이름 패턴이 *Service
     * '&&'라서 두 조건 모두를 만족해야한다.
     * */
    @Around("allOrder() && allService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {

        try {
            log.info("[doTransaction] Start {}", joinPoint.getSignature());
            Object proceed = joinPoint.proceed();
            log.info("[doTransaction] Commit {}", joinPoint.getSignature());

            return proceed;
        } catch (Exception e) {
            log.info("[doTransaction] Rollback {}", joinPoint.getSignature());
            throw e;
        } finally {
            log.info("[doTransaction] Release {}", joinPoint.getSignature());
        }
    }
}

 

이 에스팩트(AspectV3)는 두 개의 포인트컷이 존재한다. 나는 기존 에스팩트(AspectV2)에서 하나의 어드바이스를 추가했다. 이 어드바이스는 포인트컷 두개를 동시에 만족해야 적용되는 어드바이스이다. 

 

포인트컷 두 개

  • allOrder(): com.example.aop.order 패키지와 그 하위 모든 패키지의 어떠한 파라미터가 들어와도 상관없고 모든 메서드에 대해
  • allService(): 모든 패키지의 타입(클래스, 인터페이스)이름의 패턴이 *Service로 된 내부의 어떠한 파라미터가 들어와도 상관없고 모든 메서드에 대해

저 두개의 조건을 모두 만족하는 조인 포인트만이 적용되는 어드바이스이다. 이 에스팩트를 테스트 코드에 적용하고 어떤 결과가 도출되는지 확인해보자. 

 

AopTest.java

package com.example.aop;

import com.example.aop.order.OrderRepository;
import com.example.aop.order.OrderService;
import com.example.aop.order.aop.*;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

import static org.assertj.core.api.Assertions.*;


@Import(AspectV3.class)
@Slf4j
@SpringBootTest
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
    }

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Test
    void exception() {
        assertThatThrownBy(() -> orderService.orderItem("ex"))
                .isInstanceOf(IllegalStateException.class);
    }
}

 

success() 테스트의 로그 결과를 보면 다음과 같다.

 

결과를 보면 doLog 어드바이스와 doTransaction 어드바이스가 모두 실행된 후 orderItem() 메서드가 실행됐다. 이 말은 orderItem()은 doLog, doTransaction 어드바이스 모두 적용 대상이라는 의미다. 이 후 orderItem()은 내부에서 OrderRepository의 save() 메서드를 호출하는데 잘 보면 doLog 어드바이스만 적용됐고 doTransaction 어드바이스는 호출되지 않았다. 이 의미는 OrderRepository.save() 메서드는 두 번째 어드바이스인 doTransaction에는 적용되지 않는다는 의미다. 그도 그럴것이 포인트컷 중 allService()는 타입의 패턴이 "*Service"여야 하기 때문에 OrderRepository는 적용되지 않는다.

 

이렇게 여러개의 어드바이스를 적용할 수 있다. 

 

 

포인트컷 참조

위에서 포인트컷을 분리하면서 외부에서 가져다가 사용하는 경우에 대해 잠깐 말을 했는데 그것을 코드로 구현해보자.

 

Pointcuts.java

package com.example.aop.order.aop;

import org.aspectj.lang.annotation.Pointcut;

public class Pointcuts {

    @Pointcut("execution(* com.example.aop.order..*(..))")
    public void allOrder() {} // Pointcut Signature

    @Pointcut("execution(* *..*Service.*(..))")
    public void allService() {}

    /**
     * Pointcut에 '&&' 로 되어 있으면 둘 다 만족해야한다.
     * 그러니까 이 예제에서 이 포인트컷은 OrderRepository에는 대상이 아닌 것.
     * */
    @Pointcut("allOrder() && allService()")
    public void orderAndService() {}
}

 

이렇게 포인트컷만 따로 모아놓는 클래스를 만들 수도 있다. 그리고 이렇게 외부로 빼면 접근 제어자는 'public'이어야 한다.

 

AspectV4Pointcut.java

package com.example.aop.order.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Slf4j
@Aspect
public class AspectV4Pointcut {

    @Around("com.example.aop.order.aop.Pointcuts.allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[doLog] {}", joinPoint.getSignature()); // JoinPoint Signature
        return joinPoint.proceed(); // 실제 Target 호출
    }

    /**
     * com.example.aop.order 의 모든 하위 패키지 이면서 타입(클래스, 인터페이스) 이름 패턴이 *Service
     * '&&'라서 두 조건 모두를 만족해야한다.
     * */
    @Around("com.example.aop.order.aop.Pointcuts.orderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            log.info("[doTransaction] Start {}", joinPoint.getSignature());
            Object proceed = joinPoint.proceed();
            log.info("[doTransaction] Commit {}", joinPoint.getSignature());

            return proceed;
        } catch (Exception e) {
            log.info("[doTransaction] Rollback {}", joinPoint.getSignature());
            throw e;
        } finally {
            log.info("[doTransaction] Release {}", joinPoint.getSignature());
        }
    }
}

 

이 클래스가 실제로 외부의 포인트컷을 가져다가 사용하는 방식이다. @Around 애노테이션은 외부 포인트컷을 참조하면 된다. 살짝 불편한 부분은 패키지명까지 작성해줘야 한다는 것인데 이는 어쩔 수 없다. 문자로 입력해야 하기 때문에.

 

이러한 에스팩트를 가지고 위에서 사용한 테스트 코드를 수행해도 여전히 동일하게 동작한다.

 

 

어드바이스 순서

어드바이스의 적용 순서를 지정하고 싶을 땐 @Order 애노테이션을 사용하면 되는데 이 애노테이션은 클래스 단위로 사용된다. 그래서 다음처럼 이너 클래스를 사용해야 한다.

 

AspectV5Order.java

package com.example.aop.order.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;

/**
 * 프록시에 어드바이저 두 개가 있을 때 어드바이스의 순서를 변경 또는 지정할 수 있는데 그 방법은 이렇게
 * 클래스로 어드바이저 순서를 지정해줘야 한다. 근데 순서가 다르다고 어드바이저 별로 클래스를 만들기는 귀찮으니까
 * 이렇게 이너 클래스로 만들어서 정리하면 된다. 그리고 그 이너 클래스가 빈으로 등록되면 된다.
 * */
@Slf4j
public class AspectV5Order {

    @Aspect
    @Order(2)
    public static class LogAspect {
        @Around("com.example.aop.order.aop.Pointcuts.allOrder()")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[doLog] {}", joinPoint.getSignature()); // JoinPoint Signature
            return joinPoint.proceed(); // 실제 Target 호출
        }
    }

    @Aspect
    @Order(1)
    public static class TxAspect {

        /**
         * com.example.aop.order 의 모든 하위 패키지 이면서 타입(클래스, 인터페이스) 이름 패턴이 *Service
         * '&&'라서 두 조건 모두를 만족해야한다.
         * */
        @Around("com.example.aop.order.aop.Pointcuts.orderAndService()")
        public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
            try {
                log.info("[doTransaction] Start {}", joinPoint.getSignature());
                Object proceed = joinPoint.proceed();
                log.info("[doTransaction] Commit {}", joinPoint.getSignature());

                return proceed;
            } catch (Exception e) {
                log.info("[doTransaction] Rollback {}", joinPoint.getSignature());
                throw e;
            } finally {
                log.info("[doTransaction] Release {}", joinPoint.getSignature());
            }
        }
    }
}

 

이렇게 각각의 이너클래스가 에스팩트로 되고 클래스 단위로 @Order 애노테이션을 사용해야 정상적으로 어드바이스 순서가 적용된다.

이렇게 변경한 상태에서 테스트 코드를 돌려보자. 트랜잭션 관련 에스팩트가 먼저 실행되어야 한다.

 

AopTest.java

package com.example.aop;

import com.example.aop.order.OrderRepository;
import com.example.aop.order.OrderService;
import com.example.aop.order.aop.*;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

import static org.assertj.core.api.Assertions.*;


@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
@Slf4j
@SpringBootTest
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
    }

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Test
    void exception() {
        assertThatThrownBy(() -> orderService.orderItem("ex"))
                .isInstanceOf(IllegalStateException.class);
    }
}

 

@Import를 두 클래스 모두 해줘야한다. 각각이 에스팩트니까. 이 상태에서 실행해보면 트랜잭션이 먼저 실행된다.

결과를 보면 트랜잭션이 먼저 실행된 모습이다. 이렇게 어드바이스 적용 순서도 지정할 수 있다. 반대로 트랜잭션을 두번째로 하고 로그 어드바이스를 먼저 수행하면 로그 어드바이스가 먼저 진행된다.

 

 

어드바이스 종류

지금까지는 @Around만을 사용했었는데, @Around가 가장 강력한 어드바이스이고 그 안에서 세부적으로 나뉘어질 수 있다.

 

나뉘어지는 각 부분은 4가지이다. 

  • @Before: 조인 포인트 실행 이전에 실행
  • @AfterReturning: 조인 포인트가 정상 완료 후 실행
  • @AfterThrowing: 메서드가 예외를 던지는 경우 실행
  • @After: 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)

코드로 보면 확실히 이해가 된다. 코드를 보자.

 

AspectV6Advice.java

package com.example.aop.order.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

/**
 * 스프링 AOP 에서 @Around를 조각으로 분리할 수가 있다.
 * 그러니까 어드바이스 로직 흐름에 따라 @Around를 @Before, @AfterReturning, @AfterThrowing, @After로 분리할 수 있다.
 * {@code @Before}는 실제 타겟을 호출하기 전까지의 과정만을 작성하고 @AfterReturning은 실제 타겟을 호출한 후 과정을,
 * {@code @AfterThrowing}은 예외가 발생한 후 과정을, @After는 모든 로직이 다 끝난 마지막 부분을 담당한다.
 * 솔직히 @Around만 알면 나머지는 굳이 알 필요가 없다고 생각하는데 그럼에도 한가지는 알아야 한다. @Around는 파라미터로 ProceedingJoinPoint를 받는다.
 * 그러나 나머지는 그것을 받을수가 없다. JoinPoint로 받아야 한다. 왜냐하면 ProceedingJoinPoint는 JoinPoint를 상속받는데 둘 차이는 ProceedingJoinPoint는
 * 실제 타겟을 호출할 수가 있다. 'proceed()'가 존재한다. 근데 나머지 @Before, @AfterReturning, @AfterThrowing, @After가 담당하는 부분은
 * 실제 타켓을 호출하는 부분을 담당하지 않으니 당연히 받지 않는 것이다. 이 정도 차이만? 알고 있으면 좋을 것 같다.
 * 그리고 @Around를 사용할 때 발생할 수 있는 예외 상황(proceed()를 호출하지 않는 경우)를 방지할 수 있는 장점이 있다.
 *
 * 그리고 저 말은 @Around는 반드시 proceed()를 호출해야만 정상 흐름으로 진행할 수 있다. 아니면 다음 타겟으로 진행되지가 않는다.
 * */
@Slf4j
@Aspect
public class AspectV6Advice {

    /*@Around("com.example.aop.order.aop.Pointcuts.orderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            // @Before
            log.info("[doTransaction] Start {}", joinPoint.getSignature());

            Object proceed = joinPoint.proceed();

            // @AfterReturning
            log.info("[doTransaction] Commit {}", joinPoint.getSignature());

            return proceed;
        } catch (Exception e) {
            // @AfterThrowing
            log.info("[doTransaction] Rollback {}", joinPoint.getSignature());

            throw e;
        } finally {
            // @After
            log.info("[doTransaction] Release {}", joinPoint.getSignature());
        }
    }*/

    /**
     * {@code @Before}는 한가지 더 알아야 할 게 이 @Before가 호출된 후 자동으로 실제 타겟을 호출한다.
     * 실제 타겟을 호출한다는 건 @Around에서 joinPoint.proceed() 실행하는 것을 말한다.
     * */
    @Before("com.example.aop.order.aop.Pointcuts.orderAndService()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("[doBefore] {}", joinPoint.getSignature());
    }

    /**
     * {@code @AfterReturning}도 하나 알아야 할 것이 있다. 뭐냐면 result 의 타입이다.
     * 실제 타겟이 반환하는 타입이 일치하거나 그보다 상위 타입으로 받아야 정상적으로 호출할 수 있다.
     * 즉, 만약 서비스가 반환 하는 타입이 String 이면 이 @AfterReturning도 String 또는 그 상위인 Object여야 정상 호출이 된다.
     * */
    @AfterReturning(value = "com.example.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
    public void doReturning(JoinPoint joinPoint, Object result) {
        log.info("[doReturning] {}, return = {}", joinPoint.getSignature(), result);
    }

    /**
     * {@code @AfterThrowing}은 위 @AfterReturning과 마찬가지로 예외의 타입이 실제 타겟이 던지는 예외 타입과 일치하거나 그 상위 예외여야 한다.
     * */
    @AfterThrowing(value = "com.example.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
    public void doThrowing(JoinPoint joinPoint, Exception ex) {
        log.info("[doThrowing] {}, message = {}", joinPoint.getSignature(), ex.getMessage());
    }

    @After(value = "com.example.aop.order.aop.Pointcuts.orderAndService()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("[doAfter] {}", joinPoint.getSignature());
    }
}

 

알고 넘어가야 할 부분이 있다.

  • 첫번째, 우선 모든 어드바이스는 JoinPoint를 첫번째 파라미터로 받을 수 있다. 생략도 가능하다. 그러나, @Around는 반드시 ProceedingJoinPoint를 받아야 한다. 
    • 그 이유는 @Around 같은 경우 개발자가 직접 타겟을 호출하는 코드를 작성해야 한다. joinPoint.proceed() 이 코드. 그 외 나머지 어드바이스는 개발자가 직접 타겟을 호출하지 않는다. 그래서 @Around는 ProceedingJoinPoint를 파라미터로 받아야 하고 그 외 나머지 어드바이스는 JoinPoint를 받는다. 
  • 두번째, @Before는 실제 타겟을 호출하는 코드를 작성안하지만 @Before의 모든 코드가 다 수행되면 자동으로 호출한다.
  • 세번째, @AfterThrowing 역시 실제 에러에 대한 코드가 없어도 이미 에러가 터진 후 이후의 상황이다. (try - catch의 catch 안 코드라고 생각)
  • 네번째, @AfterReturning, @AfterThrowing은 각각 실제 타겟 호출의 결과와 에러를 파라미터로 받고 그 파라미터의 이름은 애노테이션에서 작성한 이름과 동일해야 한다.
  • 다섯번째, @AfterReturning, @AfterThrowing에서 파라미터로 받는 실제 타겟 호출 반환값과 에러의 타입은 해당 타입과 일치하거나 그 상위 타입이어야 한다.
  • 여섯번째, @AfterReturning에서는 @Around와 다르게 실제 타겟 호출 반환값에 대한 변경(조작)이 불가능하다. 
    • 이는 단순하게 생각해보면 된다. @Around는 개발자가 직접 실제 타겟을 호출하여 돌려받는 결과를 리턴하는데 그렇기 때문에 리턴값에 조작이 가능한것이고 @AfterReturning은 그렇지 않기 때문에 불가능한 것.
  • 일곱번째, @Around는 joinPoint.proceed()를 여러번 호출할 수도 있다.
  • 여덟번째, @Around는 joinPoint.proceed()를 반드시 호출해야 한다. 즉, 실제 객체를 반드시 호출해서 리턴해야 한다.

 

이 여러 어드바이스의 호출 순서는 다음과 같다.

@Around -> @Before -> @After -> @AfterReturning -> @AfterThrowing

물론, @Aspect 안에 동일한 종류의 어드바이스가 2개 이상이면 순서가 보장되지 않는다. 이 경우에 보장된 순서를 원한다면 @Aspect를 분리해서 @Order를 적용해서 순서를 적용해야 한다.

 

그럼 왜 @Around만 사용하더라도 모든게 가능한데 이렇게 부분적으로 나뉘어진 어드바이스가 있을까?

 

이 부분에 대한 답은 이런것들이다. 다음 코드엔 심각한 문제가 있다.

@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
 public void doBefore(ProceedingJoinPoint joinPoint) {
     log.info("[before] {}", joinPoint.getSignature());
 }

 

어떤 문제가 있을까? 바로 @Around 어드바이스인데도 실제 객체를 호출하지 않는다. 이 코드를 작성한 개발자의 의도는 실제 객체를 호출하기 전에 무언가를 로그로 출력하고 싶었던 것 뿐인데 @Around이기 때문에 실제 객체를 반드시 호출해야 한다. 

 

그럼 이 코드를 보자. 이 코드에는 문제가 전혀 없다

@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
 public void doBefore(JoinPoint joinPoint) {
     log.info("[before] {}", joinPoint.getSignature());
 }

 

@Before이기 때문에 실제 객체를 호출하는 고민을 전혀 할 필요가 없다.

 

이 말은, @Around는 가장 넓은 기능을 제공하나 실수할 가능성이 있다. 반면 @Before, @After 같은 어드바이스는 기능은 적더라도 실수할 가능성이 적으며 코드가 단순해진다. 그리고 가장 중요한 부분은 이 코드를 작성한 의도가 분명해진다는 것이다. @Before 애노테이션을 본 순간 "아, 이 코드는 실제 객체를 호출하기 전에 무언가를 하기 위해 만들어진 어드바이스구나" 라고 자연스레 생각할 수 있다.

 

즉, 좋은 설계는 제약이 있는 것이다. 제약은 실수의 가능성을 줄여준다. 애시당초 @Around가 아니라 @Before를 사용하면 실제 객체를 호출할 고민조차 할 필요가 없기 때문에 그 부분을 고려하지 않아도 되는것이다. 

 

728x90
반응형
LIST

'Spring Advanced' 카테고리의 다른 글

AOP (Part. 3) - 포인트컷  (0) 2024.01.02
AOP(Aspect Oriented Programming)  (0) 2023.12.29
AOP와 @Aspect, @Around  (0) 2023.12.29
빈 후처리기(BeanPostProcessor)  (2) 2023.12.27
Advisor, Advice, Pointcut  (0) 2023.12.15