Spring - AOP 정리

DevSeoRex·2022년 12월 9일
0
post-thumbnail

저번에 포스팅 했던 Spring의 Transaction의 내용중에 @Transactional 애너테이션이 AOP를 활용하여 만들어지고 활용된다고 해서, AOP가 무엇인지 어느정도는 알고 있지만 항상 AOP를 사용해 본적이 없기에 이것을 사용해서 얻는 이익이 무엇인지 궁금할때가 많았다.

그래서 오늘은 AOP에 대해서 이론을 정리하고, AOP를 사용해보려고 한다.

여기서 잠깐 ❗ Spring AOP는 무엇일까 ❓

  • AOP란 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다.
  • 관점 지향 프로그래밍은 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다.

💡 핵심적인 관점은 우리가 적용하고자 하는 핵심 비즈니스 로직, 부가적인 관점은 핵심 로직 을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.

💡 위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지이다.

🎓 AOP의 주요 개념

  • Aspect : 흩어진 관심사를 모듈화 하는 것을 말한다. 주로 부가기능을 모듈화한다.
  • Target : Aspect를 적용하는 곳을 뜻한다 → ex) 클래스, 메서드 ..
  • Advice : 실질적으로 어떤 일을 해야할 지에 대한 것을 뜻한다. 실질적인 부가기능을 담은 구현체를 말한다.
  • JoinPoint : Advice가 적용될 위치, 끼어들 수 있는 지점, 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용이 가능하다.
  • PointCut : JoinPoint의 상세한 스펙을 정의한 것이다. 예를 들어, ‘A란 메서드의 진입 지점에 호출할 것’과 같이 더욱 구체적으로 Advice가 실행될 지점을 정할 수 있다.

🐻 Spring AOP의 특징 🐧

  • Proxy 패턴 기반의 AOP 구현체를 사용한다.
  • Proxy 객체를 쓰는 이유는 접근 제어 및 부가기능을 추가하기 위해서이다.
  • Spring Bean에만 AOP를 적용 가능하다.

🛫 Spring AOP의 사용 - 설정

  • Maven에서 Spring AOP를 사용하기 위해서는 아래와 같은 의존성을 추가해야 한다.
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-app</artifactId>
</dependency>
  • gradle의 경우 아래와 같은 의존성을 추가하면 된다.
implementation 'org.springframework.boot:spring-boot-starter-aop'

📰 Spring AOP의 애너테이션의 종류와 역할

애너테이션 이름역할
@AspectAOP로 정의하는 클래스를 지정한다.
@PointCutAOP기능을 메서드, Annotation 등 어디에 적용시킬지 지점을 설정한다. 지점을 설정하기 위한 수식들이 매우 많다.
@Before메서드를 실행하기 이전에 실행한다.
@After메서드가 성공적으로 실행후에 실행한다(예외 발생 되더라도 실행 됨)
@AfterReturing메서드가 정상적으로 종료될때 실행된다.
@AfterThrowing메서드에서 예외가 발생할때 실행된다.
@AroundBefore + After 모두 제어한다(예외 발생 되더라도 실행 됨)

🚀 Spring AOP 활용 예시 - 메서드의 이름과 매개변수를 출력하는 로직 분리

요청에 의해 어떤 Controller 내부의 메서드가 실행되고, 이 메서드가 만약 예외나 문제를 일으킨다면 로그로 남겨서 추적할 필요성이 있다고 볼 수 있다.
그렇다고 해서 매번 Controller 안에서 로깅을 한다면 굉장히 코드가 복잡해지고 책임이 분리되지 못한 코드라고 볼 수 있다.

따라서, Controller에서 메서드의 이름과 매개변수를 출력하지 않고 AOP를 활용해 메서드의 시작 전이나 시작 후, 에러가 발생했을때 등 다양한 상황에서 알맞은 코드가 실행되도록 작성하였다.

// Controller 코드
@RestController
public class AopController {

    @CutomAnnotation
    @GetMapping("/test/aop/get/{id}")
    public String testGetAop(@PathVariable Long id, @RequestParam String pwd) {
        return id + " " + pwd;
    }

}

// @CustomAnnotation 코드
@Retention(value = RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CutomAnnotation {

}
  • Controller안에는 Logging이나 출력에 관한 어떤 코드도 작성되지 않았다.
  • @CustomAnnotation은 사용자가 직접 만든 애너테이션으로, AOP에서는 특정 애너테이션을 지정해서 부가 기능을 추가할 수 있으므로 애너테이션을 직접 정의 해보았다.
// Aspect 클래스 작성
@Aspect
@Component
public class AopLesson {

    // com/example/practice/demo/controller 패키지의 하위 클래스들을 모두 적용
    @Pointcut("execution(* com.example.practice.demo.controller..*.*(..))")
    public void start() {}

    // start() 메서드 시작전에 prepare() 메서드 실행
    @Before("start()")
    public void prepare(JoinPoint joinPoint) {

        System.out.println("=========== @Before -> prepare Start ============");

        // 실행되는 메서드 이름을 가져오고 출력
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        System.out.println(method.getName() + "메서드가 실행중입니다.");

        // 메서드에 들어가는 매개변수 배열을 읽어온다.
        Object[] paramArgs = joinPoint.getArgs();

        // 매개변수 배열의 종류와 값을 출력한다.
        for(Object object : paramArgs) {
            System.out.println("type = " + object.getClass().getSimpleName());
            System.out.println("value = " + object);
        }

        System.out.println("=========== @Before -> prepare End ============");
        System.out.println();
    }

    // start() 메서드가 종료되는 시점에 afterReturn() 메서드가 실행된다.
    // @AfterReturning() 애너테이션의 returning 값과 afterReturn 매개변수 obj의 이름이 같아야 한다.
    @AfterReturning(value = "start()", returning = "obj")
    public void afterReturn(JoinPoint joinPoint, Object obj) {

        System.out.println("=========== @AfterReturning Start ============");

        // 메서드에 들어가는 매개변수 배열을 읽어온다.
        Object[] paramArgs = joinPoint.getArgs();

        // 매개변수 배열의 종류와 값을 출력한다.
        for(Object object : paramArgs) {
            System.out.println("type = " + object.getClass().getSimpleName());
            System.out.println("value = " + object);
        }

        System.out.println("메서드 종료");

        System.out.println("=========== @AfterReturning END ============");
        System.out.println();
    }

    @Before("@annotation(com.example.practice.demo.annotation.CutomAnnotation)")
    public void annoPrepared() {
        System.out.println("=========== @Before -> annoPrepared Start ============");

        System.out.println("@CustomAnnotation 붙었다!");

        System.out.println("=========== @Before -> annoPrepared End ============");
        System.out.println();
    }

}
  • AOP 기능을 사용하기 위해 작성한 Aspect 클래스에는 @Aspect와 @Component 애너테이션을 붙여줘야 한다.

위의 코드가 각각 어떤 동작을 하는지 아래에서 자세히 설명하겠다.

  • @Pointcut("execution( com.example.practice.demo.controller...*(..))")

    • 위의 코드는 com/example/practice/demo/controller 패키지의 하위 클래스들을 모두 적용하겠다는 뜻이다.
  • @Before("start()")

    • 위의 코드는 start( )라는 이름의 메서드가 시작하기 전에 이 애너테이션이 붙은 메서드를 실행하라는 의미이다.
  • @AfterReturning(value = "start()", returning = "obj")

    • 위의 코드는 start() 메서드가 성공적으로 종료되는 시점에 이 애너테이션이 붙은 메서드를 실행하라는 의미이다.
    • @AfterReturning( ) 애너테이션의 returning 값과, 위 애너테이션이 붙은 메서드의 매개변수 obj는 이름이 같아야 한다.
  • @Before("@annotation(com.example.practice.demo.annotation.CutomAnnotation)")

    • @CustomAnnotation 애너테이션이 붙어있는 메서드가 시작되기 전에 @Before 애너테이션이 붙은 메서드를 실행하라는 의미이다.

실행 - AOP 적용 전

  • AopLesson 클래스를 잠시 주석처리하면 콘솔에 아무것도 나오지 않는것을 확인할 수 있다.

실행 - AOP 적용 후

  • AopLesson 클래스를 주석 해제하면 콘솔에 메서드 호출 전후로 추가한 부가기능들이 호출 된 것을 확인할 수 있다.

💡 AOP를 사용하니, Controller에는 출력에 관한 코드를 작성하지 않고도 AOP를 사용해 메서드 사용 전후로 다양한 상황에서 부가 기능을 추가할 수 있게된다.

0개의 댓글