Spring Boot AOP 적용

최민길(Gale)·2023년 1월 17일
1

Spring Boot 적용기

목록 보기
14/46

안녕하세요 오늘은 Spring Boot에 AOP 적용 실습에 대해 포스팅해보도록 하겠습니다.

AOP(Aspect Oriented Programming)란 관점 지향 프로그래밍으로, 각 계층에서 공통적으로 담당하는 로직(=횡단 관심사)를 처리하는 방법론입니다. 부가 기능이 핵심 기능과 섞이게 될 경우 부가 기능의 수정이 발생하면 모든 핵심 기능에 속해 있는 부가 기능을 일일히 수정해야 합니다. 이런 번거로움을 방지하기 위해 핵심 기능에 부여되는 부가 기능을 모듈화하는 방법을 찾으면서 AOP가 나오게 됩니다.

위에서 언급한 부가 기능을 Aspect라고 부릅니다. Aspect는 어플리케이션의 핵심 기능을 담고 있지 않지만 핵심 기능에 부가되어 의미를 갖는 모듈입니다. 이런 Aspect들을 모듈화하여 설계하는 방법이 AOP이며, AOP는 OOP(객체 지향 프로그래밍)을 대체하는 기술이 아니라 보조 기술로서 핵심 기능을 설계하고 구현할 때 객체 지향적인 가치를 지킬 수 있도록 도와줍니다.

AOP를 이용하는 방법은 크게 Spring AOP와 AspectJ AOP가 있습니다. 우선 Spring AOP의 경우 다음의 순서로 동작합니다.

  1. 다이나믹 프록시 객체의 생성 요청
  2. pointcut을 통해 부가 기능(Aspect) 적용 대상 여부 확인
  3. advice로 부가 기능(Aspect) 적용
  4. 실제 기능 처리


출처 : https://mangkyu.tistory.com/175

Spring AOP는 프록시를 이용하기 때문에 Spring 컨테이너와 기본 JDK만 있으면 구현 가능합니다. 하지만 수동으로 직접 AOP 프록시를 구현할 경우 1개 타입에 대해 불필요하게 여러 Bean을 관리해야 하기 때문에 Bean의 의존성 주입 시 문제가 발생할 여지가 있습니다. 따라서 자동 프록시 생성기를 통해 다이나믹 프록시 객체를 생성하여 프록시 Bean을 실제 Bean처럼 구현하고 기존의 Bean을 대체합니다. 하지만 프록시를 적용하기 위해서 반드시 인터페이스를 생성해야 한다는 한계점이 존재합니다. 이를 해결하기 위해 Spring에서는 CGLib이라는 바이트 조작 라이브러리를 통해 클래스 상속으로 프록시를 구현하여 인터페이스가 없어도 적용 가능하게 되었습니다. 단 상속을 이용하기 때문에 기본 생성자를 필요로 하고, 생성자가 2번 호출되거나 final 클래스 또는 메소드면 안된다는 제약이 있습니다.

pointcut은 메소드 선정 알고리즘을 담은 오브젝트입니다. 이를 이용하여 어떤 코드에 Aspect를 적용시킬 지 필터링합니다. advice는 타깃 오브젝트에 적용하는 부가 기능(Aspect)를 담은 오브젝트입니다. 저번 시간에 사용했던 @ControllerAdvice의 경우 타깃 오브젝트(모든 Controller)에 적용할 부가 기능(에러 캐치해서 반환)을 담고 있습니다. 그 후 처리할 로직을 진행하며 부가 기능을 모듈화합니다.

    // AOP
    implementation "org.springframework.boot:spring-boot-starter-aop"

그럼 저번 시간에 진행했던 ThreadPoolTaskExecutor가 과연 성능 향상에 얼마나 큰 도움이 되는지를 확인하기 위해 실행 시간을 측정하는 로직을 AOP로 만들어보겠습니다. 우선 위의 dependency를 build.gradle에 추가해줍니다.

package com.example.test.annotation;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface SlackNotification {
}

이어서 SlackNotification이라는 커스텀 어노테이션을 만들어줍니다. @Retention은 어노테이션이 언제까지 살아 남아 있을지를 결정하며 런타임까지 계속 살아있도록 설정했습니다. @Target은 어노테이션이 적용될 레벨을 의미하며 클래스 또는 메소드에 적용하고자 각각 TYPE, METHOD를 추가했습니다.

package com.example.test.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

@Aspect
@Component
public class ExecutionTimeAop {
    
    @Around("@annotation(com.example.test.annotation.SlackNotification)")
    public Object calculateExecutionTime(ProceedingJoinPoint pjp) throws Throwable{

        StopWatch sw = new StopWatch();
        sw.start();

        Object result = pjp.proceed();

        sw.stop();
        long executionTime = sw.getTotalTimeMillis();

        String className = pjp.getTarget().getClass().getName();
        String methodName = pjp.getSignature().getName();
        String task = className + "." + methodName;

        System.out.println("[ExecutionTime] " + task + "-->" + executionTime + "(ms)");

        return result;
    }
}

이어서 실행 시간을 측정하는 로직을 만들어 @Aspect 어노테이션을 통해 Aspect로 설정합니다. @Around의 경우 동작 시점을 설정하며, 메소드 호출 자체를 가로채서 비즈니스 메소드 실행 전후에 모두 처리할 로직을 삽입합니다. 따라서 @Arount 내부에 적용시킬 어노테이션 주소를 넣으면 @SlackNotification 어노테이션을 추가할 때 calculateExecutionTime이 실행되게 됩니다.

그럼 Slack 메시지 발송 시 ThreadPoolTaskExecutor를 이용하여 ThreadPool을 이용한 비동기 방식으로 처리하는게 얼마나 성능 향상에 도움이 되는지 테스트해보겠습니다. 테스트는 "test"라는 메시지를 1. ThreadPoolTaskExecutor를 사용하지 않고 2번 발송하며, 2. 한 번은 그냥 발송, 한 번은 ThreadPoolTaskExecutor를 사용하여 2번 발송하는 방식으로 실행 시간을 비교해보겠습니다. 우선 1번 실험 결과는 다음과 같으며 784ms가 측정되었습니다.

2번 실험 결과는 다음과 같으며 479ms가 측정되었습니다. 이를 통해 약 1.5배의 성능 향상이 발생하였으며, 실험 특성 상 2번 실험에서 메시지를 하나는 Thread Pool을 이용하지 않고 발송했기 때문에 실질적으로 더 큰 차이가 있을 것으로 기대할 수 있습니다.

이번 실험을 진행하면서 ThreadPoolTaskExecutor만 사용해서 테스트를 진행할 경우 정상적으로 메시지 출력이 되지 않는 것을 확인했습니다. 앞으로 원인을 분석해서 그 결과를 다시 포스팅하도록 하겠습니다. 긴 글 읽어주셔서 감사합니다!

출처
: https://mangkyu.tistory.com/161
: https://mangkyu.tistory.com/121
: https://mangkyu.tistory.com/175

profile
저는 상황에 맞는 최적의 솔루션을 깊고 정확한 개념의 이해를 통한 다양한 방식으로 해결해오면서 지난 3년 동안 신규 서비스를 20만 회원 서비스로 성장시킨 Software Developer 최민길입니다.

0개의 댓글