AOP의 개념과 @Aspect

조갱·2024년 6월 23일
0

스프링 강의

목록 보기
20/23

지금까지 로그 찍기 예제를 통해 프록시에 대해 살펴보았다.
프록시는 AOP (Aspect-Oriented Programming: 관점 지향 프로그래밍)의 근간이 되는 기술이다.

지금까지 프록시에 대해 살펴보았으니, 이제 AOP 에 대한 내용을 알아보자.

AOP 개념

핵심기능과 부가기능

어플리케이션은 크게 핵심 기능부가 기능으로 나눌 수 있다.
핵심 기능 : 어플리케이션의 동작을 위한 필수적인 로직 (ex: 주문 서비스, 상품 검색)
부가 기능 : 필수적이진 않지만, 핵심 기능을 보조하기 위한 기능 (ex: 로그 찍기 등)

처음엔 핵심 기능이 있는 서비스단에 부가 기능을 구현했다.
하지만 하나 둘 개발하다보니, 핵심 로직과 부가 기능이 섞이며 코드가 지저분해졌고, 진짜 문제는 동일한 역할의 부가 기능이 중복으로 개발되었다.

이렇게 여러 곳에서 동일한 역할의 부가 기능을 횡단 관심사 라고 부르고, 이를 개선하기 위해 등장한 개념이 Aspect (관점)이다.

Aspect

횡단 관심사를 핵심 로직과 분리하여, 한 곳에서 처리하기 위한 개념이 등장한다.

  • 부가 기능을 별도로 관리
  • 이 부가 기능을 어디에 적용할까?

이 2가지를 모듈화 한 것이 Aspect 이다.
다시 살펴 보면, 이전에 프록시를 학습하며 봤던 Advisor와 동일한 개념이다.

  • 부가 기능을 별도로 관리 -> Advice
  • 이 부가 기능을 어디에 적용할까? -> Pointcut

Aspect 를 사용하는 프로그래밍 방식을 AOP (Aspect-Oriented Programming) 이라고 한다.

* AOP는 OOP를 대체하는 것이 아니라, OOP로 해결하지 못하는 부족한 부분을 채워주는 목적으로 개발되었다.
* 자바 진영에서 Aspect를 적용하기 위한 대표적인 프레임워크로 AspectJ 라는것이 있지만 사용 방법이 복잡하다.
* Spring도 AOP를 지원하며, AspectJ의 문법을 차용한다. AspectJ보다 기능은 적지만, 실무에서 필요한 정도는 충분히 커버한다.

AOP 적용 방식

AOP 를 적용하면, 핵심 기능부가 기능을 완전히 분리하여 개발이 가능해진다.
그러면 AOP 는 어떤식으로 적용이 되는걸까?
크게, 컴파일 시점 / 클래스 로딩 시점 / 런타임 시점 (프록시) 3가지 방식이 있다.

* 위빙(Weaving): Aspect와 실제 코드를 연결해서 붙이는 것

컴파일 시점

java 파일을 class파일로 컴파일할 때, AspectJ 컴파일러가 바이트 코드에 부가 기능 코드를 붙여버린다.
그래서 위 사진으로 만들어진 orderService.class 파일을 디컴파일해보면, 소스코드 상에 로그 추적 코드가 적용되어있음을 알 수 있다.

장점 : 클래스 코드에 로직이 적용되어있기 때문에, 실행 속도가 빠르다.
단점 : 별도의 컴파일러를 사용해야하고, AspectJ를 별도로 학습하고 적용해야하기 때문에 러닝커브가 생긴다.

클래스 로딩 시점

OrderService.java 파일이 컴파일될 때는 원본 코드로 컴파일되지만, 클래스를 로딩하는 시점에 AspectJ 클래스 로더 조작기가 클래스 파일의 내용을 바꿔버린다. 그렇게 부가 기능 코드가 적용된 클래스가 로드된다.

장점 : 컴파일 시점과 동일하게, 클래스 코드에 로직이 적용되어있기 때문에, 실행 속도가 빠르다.
단점 : 자바를 실행할 때 옵션(java - javaagent)을 통해 클래스 로더 조작기를 지정해야 하는데, 이 부분이 번거롭고 운영하기 어렵다.

런타임 시점 (프록시)

우리가 지금까지 실습했던 프록시를 활용한 방법이다.

장점 : 스프링만 있으면 AOP를 쉽게 적용할 수 있다.
단점 : 런타임에 프록시를 사용하기 때문에 실행 속도가 느리다. final 클래스와 같이 상속이 불가능한 경우 프록시를 적용할 수 없다. 프록시를 사용하기 때문에 static 메소드, 필드 등에는 AOP를 적용할 수 없다. 이외에도 프록시를 사용하기 때문에 AOP 기능 일부에 제약이 생긴다.

AOP 적용 위치

AOP는 지금까지 실습한 일반 메소드 뿐만 아니라, 생성자/static 메소드/필드 등에도 적용할 수 있다.
이렇게 AOP를 적용할 수 있는 위치를 조인 포인트(Join Point) 라고 한다.

AspectJ를 사용해서 컴파일 시점이나 클래스 로딩 시점에 AOP를 적용한다면
바이트 코드를 실제로 조작하기 때문에 적용하는 위치에 제약이 없다.

하지만 런타임 시점 (프록시)에 AOP를 적용한다면 조인 포인트(Join Point)에 제약이 생긴다.
일반 메소드 실행 시점에만 AOP를 적용할 수 있는데, 이는 프록시의 원리를 생각해보면 답을 알 수 있다.
-> 프록시 방식은 스프링 컨테이너가 관리하는 Bean에 프록시를 적용하는 원리이다.
-> 필드, static 메소드, 생성자 등은 Spring Bean으로 등록될 수 없기 때문이다.

AOP 용어 정리

조인 포인트(Join point)

어드바이스가 적용될 수 있는 위치

추상적인 개념으로, AOP를 적용할 수 있는 모든 위치.
Spring AOP는 프록시 방식을 사용하므로, 항상 일반 메소드 실행 시점으로 제한된다.

포인트컷(Pointcut)

조인 포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능

주로 AspectJ 표현식을 사용해서 지정.
Spring AOP는 일반 메소드 실행 지점만 Pointcut으로 지정 가능하다.

타겟(Target)

Advice를 받는 객체

포인트컷으로 결정된다.

어드바이스(Advice)

부가 기능 로직

Around(주변), Before(전), After(후)와 같은 다양한 종류의 어드바이스가 있음

애스펙트(Aspect)

Advice + Pointcut을 모듈화 한 것

Spring에서는 @Aspect 를 사용한다.
클래스에 사용할 수 있으며, 해당 클래스가 횡단 관심사 클래스임을 의미한다.
클래스에 적용되기 때문에, 내부에 여러 어드바이스와 포인트 컷이 존재한다.

어드바이저(Advisor)

1 Advice + 1 Pointcut

Spring AOP에서만 사용되는 용어이다.

위빙(Weaving)

Pointcut 으로 결정된 Target의 Joinpoint에 Advice를 적용하는 것.

컴파일 시점, 클래스 로드 시점, 런타임 (프록시) 시점이 있다.

@Aspect

이전 포스팅 에서

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

의존성을 추가하면 AutoProxyCreator 인 AnnotationAwareAspectJAutoProxyCreator 빈 후처리기가 빈으로 등록됨을 확인했다. 이를 통해 개발자는 Advisor만 빈으로 등록해두면, 프록시가 자동으로 생성되는 편리함을 누렸다.

하지만 위의 자동 프록시 생성기는 더 강력한 기능을 제공한다.

Spring이 제공하는 @Aspect 어노테이션을 사용하면, 자동 프록시 생성기가 @Aspect를 찾아서 Pointcut + Advice로 구성되어 있는 어드바이저를 생성해준다.

이름 앞에 AnnotationAware (어노테이션을 인지하는) 이 붙어있는 이유이다.

예제 코드

LogTraceAspect

@Slf4j
@Aspect
public class LogTraceAspect {

    private final LogTrace logTrace;

    public LogTraceAspect(LogTrace logTrace) {
        this.logTrace = logTrace;
    }

    @Around("execution(* hello.proxy.app..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        TraceStatus status = null;
        // log.info("target={}", joinPoint.getTarget()); 실제 호출 대상
        // log.info("getArgs={}", joinPoint.getArgs()); 전달인자
        // log.info("getSignature={}", joinPoint.getSignature()); join point 시그니처
        try {
            String message = joinPoint.getSignature().toShortString();
            status = logTrace.begin(message);

            //로직 호출
            Object result = joinPoint.proceed();
            
            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

클래스레벨에 @Aspect 어노테이션이 적용됐다.
이제부터 이 클래스는 횡단관심사를 다루는 클래스이다.

자동 프록시 생성기인 AnnotationAwareAspectJAutoProxyCreator는 이 @Aspect 어노테이션을 찾아서 execute 메소드를 Advisor로 만들어준다.

execute 메소드 내에 ProceedingJoinPoint 는 이전에 Advice에서 살펴본 MethodInvocation 과 유사한 기능이다.
내부에 실제 호출 대상, 전달 인자, 그리고 어떤 객체와 어떤 메서드가 호출되었는지 정보가 포함되어 있다.

@Aspect어노테이션이 적용된 클래스 안에는 여러 어드바이저가 포함될 수 있다.
이 경우 1개의 프록시 안에 여러 어드바이저가 있는 프록시가 된다.

AopConfig

@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AopConfig {
    @Bean
    public LogTraceAspect logTraceAspect(LogTrace logTrace) {
        return new LogTraceAspect(logTrace);
    }
}

Application

@Import(AopConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app")
public class ProxyApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProxyApplication.class, args);
    }

    @Bean
    public LogTrace logTrace() {
        return new ThreadLocalLogTrace();
    }
}

@Aspect 설명

AnnotationAwareAspectJAutoProxyCreator

자동 빈 등록기는 2가지 역할을 한다.

  • Aspect 어노테이션을 찾아서 Advisor로 변환한다.
  • Advisor를 찾아 프록시를 생성한다.

Aspect 어노테이션을 찾아서 Advisor로 변환하는 과정

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

Advisor를 찾아 프록시를 생성하는 과정

  • (1) 생성
    스프링 빈 대상이 되는 객체를 생성한다. (@Bean, 컴포넌트 스캔 모두 포함)
  • (2) 전달
    생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
  • (3-1) Advisor 빈 조회
    스프링 컨테이너에서 Advisor 빈을 모두 조회한다.
  • (3-2) @Aspect Advisor 조회
    Aspect 어드바이저 빌더 내부에 저장된 Advisor 를 모두 조회한다.
  • (4) 프록시 적용 대상 체크
    앞서 3-1, 3-2에서 조회한 Advisor 에 있는 Pointcut을 통해, 해당 객체가 프록시 적용 대상인지 판단한다.
  • (5) 프록시 생성
    프록시 적용 대상이면 프록시를 생성하고 프록시를 반환한다.
    프록시 적용 대상이 아니라면 원본 객체를 반환한다.
  • (6) 빈 등록
    반환된 객체는 스프링 빈으로 등록된다.
profile
A fast learner.

0개의 댓글