스프링 AOP 에 대하여

땡글이·2023년 1월 31일
0

스프링 AOP

목록 보기
4/5

AOP 는 관점 지향 프로그래밍(Aspect-oriented programming)을 뜻하고, 횡단 관심사(cross-cutting concern)의 분리를 허용함으로써 모듈성을 증가시키는 것이 목적인 프로그래밍 패러다임을 의미한다. 즉, 프로그램 로직을 명확한 부분들(이른바 "관심사")로 나눈다는 것이다.
쉽게 이야기하자면, 애플리케이션을 핵심 기능을 담당하는 로직과 부가 기능을 담당하는 로직으로 구분해서 프로그래밍을 한다는 것이다.

  • 핵심 기능 : 회원가입, 주문, 결제 등
  • 부가 기능 : 로그 추적, 트랜잭션 등
    • 부가 기능은 핵심 기능을 보조하기 위해 제공되는 기능이다.

횡단 관심사(cross-cutting concern) 란 ?
여러 곳에 동일하게 사용되는 하나의 부가 기능을 횡단 관심사라고 부른다. 하지만, 동일한 부가 기능을 100곳에 적용하려면, 100개의 코드를 동일하게 적어줘야 하는 문제가 있다. 그래서 스프링에서는 이런 문제를 해결하기 위한 AOP 기술들(어드바이저, @Aspect, AspectJ 등)이 존재하고, 변경이 일어나도 변경 지점은 하나가 될 수 있게 모듈화가 가능하도록 제공한다. 이런 기능들은 일반적인 OOP 방식으로는 어렵다.

스프링 AOP

앞서 살펴본 프록시 같은 기술로 부가 기능과 부가 기능을 어디에 적용할지 선택하는 기능을 합해서 하나의 모듈로 만들었는데 이것이 바로 애스팩트(Aspect)이다. 애스팩트는 부가 기능과 해당 부가 기능을 어디에 적용할지 정의한 것이다. @Aspect 가 그 중 하나이다. 밑에서 자세히 알아본다.

애스팩트는 말 그대로 관점이라는 뜻을 가진다. 이름 그대로 애플리케이션을 바라보는 관점을 하나하나의 기능에서 횡단 관심사 관점으로 달리 보는 것이다. 이렇게 애스팩트를 활용한 프로그래밍 방식을 관점 지향 프로그래밍, AOP 이라고 한다.

AOP는 OOP를 대체하기 위한 것이 아니라 횡단 관심사를 깔끔하게 처리하기 어려운 OOP의 부족한 부분을 보조하는 목적으로 개발되었다.

AspectJ 프레임워크

AOP의 대표적인 구현으로 AspectJ 프레임워크가 있다. 스프링 AOP에서도 대부분 AspectJ의 문법을 차용하고, AspectJ가 제공하는 기능의 일부만 제공한다.

스프링 AOPAspectJ의 차이점은 ?
(목적) 스프링 AOP 의 목적은 스프링 IoC 전반에 걸쳐서 간단한 AOP 구현을 제공하는 것을 목표로 하기에, 완전한 AOP 솔루션이 목표가 아니다. 하지만, AspectJ 는 완전한 AOP 솔루션을 목표로 하기에, 스프링 AOP 보다 강하지만 복잡하다.
(Weaving) 스프링 AOP는 런타임 시점에서의 위빙만을 제공하고 있다. AspectJ 는 총 세 가지의 위빙(Compile-time weaving, Post-Compile weaving, Load-time weaving)을 지원한다.
(JoinPoint) 스프링 AOP는 대상 객체에 final 키워드가 있으면 AOP 적용 대상이 되지 못하고, 조인포인트는 메서드 실행 지점에서만 가능하다. (프록시는 오버라이딩 개념으로 동작하기 때문에) AspectJ 는 생성자, 필드 값, static 메서드, 메서드 실행 등 다양한 곳에서 가능하다.

(성능) 성능에 관해서는 컴파일 시점 위빙은 런타임 시점 위빙보다 훨씬 빠르다. 스프링 AOP는 프록시 기반 프레임워크이므로, 애플리케이션 실행될 때 프록시가 생성된다. 또한, 애스팩트마다 추가적인 method invocation이 있어 성능에 무리를 줄 수 있다. 하지만, AspectJ는 애플리케이션이 실행되기 전에 위빙을 통해 메인 코드에 부가 기능 로직 코드를 심기에 런타임 오버헤드도 없다. 해당 사이트는 AspectJ가 스프링 AOP에 비해 8~35배 빠르다는 벤치마크를 의미한다.


@Aspect 프록시

스프링 어플리케이션에 프록시를 적용하려면, 포인트컷과 어드바이스로 구성된 어드바이저를 스프링 빈으로 등록해야 한다. 그렇게 되면 나머지는 자동 프록시 생성기가 자동으로 처리해준다.

자동 프록시 생성기는 스프링 빈으로 등록된 어드바이저들을 찾고, 스프링 빈들에 자동으로 프록시를 적용해준다.
자동 프록시 생성기에 대한 내용은 해당 포스팅을 참고해주시기 바랍니다.

스프링에서는 @Aspect 어노테이션을 이용해서 매우 간단하게 어드바이저 생성 기능을 지원한다.

@Aspect 는 관점 지향 프로그래밍(AOP)를 가능하게 하는 AspectJ 프로젝트에서 제공하는 어노테이션이다. 스프링은 이것을 차용해서 프록시를 통한 AOP를 가능하게 한다.

이전 포스팅에서는 자동 프록시 생성기(AnnotationAwareAspectJAutoProxyCreator)는 Advisor를 자동으로 찾아와서 필요한 곳에 프록시를 생성하고 적용해준다고 했다. 하지만, 자동 프록시 생성기는 하나의 역할을 더 한다. 즉, @Aspect 를 찾아서 Advisor로 만들어준다. 즉, @Aspect 객체를 어드바이저로 변환해 저장한다.

예제

@Slf4j
@Aspect
public class AspectV1 {

    @Around("execution(* hello.aop.order..*(..))")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처

        return joinPoint.proceed();
    }
}
  • @Around(...) : 포인트컷을 의미한다.
  • doLog() 함수 내의 내용은 어드바이스를 의미한다.

@Aspect 를 어드바이저로 변환해 저장하는 과정

  1. 실행 : 스프링 애플리케이션 로딩 시점에 자동 프록시 생성기를 호출한다.
  2. 모든 @Aspect 빈 조회 : 자동 프록시 생성기는 스프링 컨테이너에서 @Aspect 어노테이션이 붙은 스프링 빈을 모두 조회한다.
  3. 어드바이저 생성 : @Aspect 어드바이저 빌더를 통해 @Aspect 어노테이션 정보를 기반으로 어드바이저를 생성한다.
  4. @Aspect 기반 어드바이저 저장 : 생성한 어드바이저를 @Aspect 어드바이저 빌더 내부에 저장한다.

@Aspect 어드바이저 빌더
BeanFactoryAspectJAdvisorBuilder 클래스이다. @Aspect 의 정보를 기반으로 포인트컷, 어드바이스, 어드바이저를 생성하고 보관하는 것을 담당한다. @Aspect 의 정보를 기반으로 어드바이저를 만들고, @Aspect 어드바이저 빌더 내부 저장소에 캐시한다. 캐시에 어드바이저가 이미 만들어져 있는 경우 캐시에 저장된 어드바이저를 반환한다.

자동 프록시 생성기 작동 과정


1. 실행 : 스프링 빈 대상이 되는 객체를 생성한다. (@Bean, 컴포넌트 스캔 포함)
2. 전달 : 생성된 객체를 빈 저장소에 등록하기 전 빈 후처리기(자동 프록시 생성기)에 전달한다.
3-1. Advisor 빈 조회 : 스프링 컨테이너에서 Advisor 빈을 모두 조회한다.
3-2. @Aspect Advisor 조회 : @Aspect 어드바이저 빌더 내부에 저장된 Advisor 를 모두 조회한다.
4. 프록시 적용 대상 여부 체크 : 앞서 조회한 모든 Advisor에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 조건이 하나라도 만족하면, 프록시 적용 대상이 된다.
5. 프록시 생성 : 프록시 적용 대상이면 프록시 생성하고고 프록시 객체를 반환한다. 프록시 대상이 아니면 원본 객체를 반환한다.
6. 빈 등록 : 반환된 객체를 빈으로 등록한다.

스프링 AOP 예제

직접 스프링 AOP를 활용한 @Trace 어노테이션과 @Retry 어노테이션을 만들어보자.

  • @Trace : 어노테이션으로 로그 출력하기
  • @Retry : 어노테이션으로 예외 발생시 재시도하기

우선 상황을 가정하자면, 5번에 1번 정도 실패하는 저장소가 있다고 가정한다.

@Repository
public class ExamRepository {

    private static int seq = 0;

    public String save(String itemId) {
        seq++;
        if (seq % 5 == 0) {
            throw new IllegalStateException("예외 발생");
        }

        return "ok";
    }
}
@Service
@RequiredArgsConstructor
public class ExamService {

    private final ExamRepository examRepository;

    public void request(String itemId) {
        examRepository.save(itemId);
    }
}

이런 로직에서는 저장 로직이 5번마다 한 번씩 예외가 발생한다. 그러므로 재시도하는 AOP 로직이 있어야 한다. 그리고 기본적으로 로그를 출력하는 AOP를 만들어보자.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Trace {
}
@Slf4j
@Aspect
public class TraceAspect {

    @Before("@annotation(hello.aop.exam.annotation.Trace)")
    public void doTrace(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        log.info("[trace] {}, args={}", joinPoint.getSignature(), args);
    }
}

@annotation(hello.aop.exam.annotation.Trace) : @Trace 어노테이션이 붙은 메서드에 어드바이스를 적용한다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
    int value() default 3;
}
@Slf4j
@Aspect
public class RetryAspect {

    @Around("@annotation(retry)")
    public Object doRetry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {

        log.info("[retry] {}, retry={}", joinPoint.getSignature(), retry);
        int maxRetry = retry.value();
        Exception exceptionHolder = null;

        for (int retryCount = 1; retryCount <= maxRetry; retryCount++) {
            try {
                log.info("[retry] try count={}/{}", retryCount, maxRetry);
                return joinPoint.proceed();
            } catch (Exception e) {
                exceptionHolder = e;
            }
        }

        throw exceptionHolder;
    }
}

@annotation(retry) : @Retry 어노테이션이 붙은 메서드에 어드바이스를 적용한다. 메서드에서 매개변수 retry를 전달받아 포인트컷 표현식 작성했다.

이제 재시도 AOP와 로그 출력 AOP를 적용해서 결과를 확인해본다.

@Repository
public class ExamRepository {

    private static int seq = 0;
	
    @Trace
    @Retry
    public String save(String itemId) {
        seq++;
        if (seq % 5 == 0) {
            throw new IllegalStateException("예외 발생");
        }

        return "ok";
    }
}
@Service
@RequiredArgsConstructor
public class ExamService {

    private final ExamRepository examRepository;

	@Trace
    public void request(String itemId) {
        examRepository.save(itemId);
    }
}
@Slf4j
@Import({TraceAspect.class, RetryAspect.class})
//@Import({RetryAspect.class, TraceAspect.class})
@SpringBootTest
public class ExamTest {

    @Autowired
    ExamService examService;

    @Test
    void test() {
        for (int i = 0; i < 5; i++) {
            log.info("client request i = {}", i);
            examService.request("data" + i);
        }
    }
}
client request i = 0
[trace] void hello.aop.exam.ExamService.request(String), args=[data0]
[trace] String hello.aop.exam.ExamRepository.save(String), args=[data0]
[retry] String hello.aop.exam.ExamRepository.save(String), retry=@hello.aop.exam.annotation.Retry(value=4)
[retry] try count=1/4

...

client request i = 4
[trace] void hello.aop.exam.ExamService.request(String), args=[data4]
[trace] String hello.aop.exam.ExamRepository.save(String), args=[data4]
[retry] String hello.aop.exam.ExamRepository.save(String), retry=@hello.aop.exam.annotation.Retry(value=4)
[retry] try count=1/4
[retry] try count=2/4

Reference

https://ko.wikipedia.org/wiki/%EA%B4%80%EC%A0%90_%EC%A7%80%ED%96%A5_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D
https://www.baeldung.com/spring-aop-vs-aspectj
https://web.archive.org/web/20150520175004/https://docs.codehaus.org/display/AW/AOP+Benchmark

profile
꾸벅 🙇‍♂️ 매일매일 한발씩 나아가자잇!

0개의 댓글