져니의 개발 정원 가꾸기

AOP - Spring에서 AOP기술을 어떻게 쓸까? 본문

개발노트/Spring | Java

AOP - Spring에서 AOP기술을 어떻게 쓸까?

전전쪄니 2023. 12. 30. 23:17

목차

     

    이번주에 회사에서 aop를 적용해 보았는데, 내친김에 aop 기술과 스프링AOP를 사용하는 간단한 방법에 대해 다뤄보고자 한다.

    AOP(Aspect Oriented Programming)란 무엇일까?

    한국말로 직역해보자면 AOP는 '관점 지향 프로그래밍'이라는 뜻이다. 여기서 말하는 관점(Aspect)는 부가기능 모듈로 핵심기능은 아니지만 핵심기능에 부가 기능을 제공하는 의미있는 모듈을 말한다. 부가기능의 대표적인 예로 로깅과 트랜잭션이 있고 세트메뉴처럼 핵심기능에 부가적으로 제공되는 기능들이라고 보면 된다.

    AOP가 등장하게 된 배경에는 객체지향 프로그래밍(OOP)이 있다. OOP는 객체지향 설계 원칙(SOLID)에 따라 관심사 분리를 지향한다. 클래스가 하나의 목적 아래 동작하고 수정될 수 있도록 응집도를 높여 관심사들을 모으고 쪼개 소프트웨어의 품질과 유지보수성을 높인다. 그런데 문제는 부가기능 자체가 핵심로직을 가지지 않고 여러 핵심기능에 따라 붙어 애플리케이션 여기저기에 산재해있어 응집도를 높이는 일반적인 방식으로 모듈화하기 어렵다는 점이다. 간단한 방법으로 부가기능을 모듈화할 수 없기 때문에 AOP라는 부가기능 모듈의 프로그래밍 방법이 등장하게 되었다.

    AOP란 부가기능 모듈에 Aspect라는 이름으로 부여하여 모듈화하는 개발 방법으로, 특정 부가기능 관점에서 부가기능에 집중해 설계하고 개발하는 것을 말한다. Aspect를 분리해 애플리케이션 전반에 나타나는 횡단관심사를 모듈화하고 핵심기능의 OOP 가치를 지킬 수 있게 한다. 또한 스프링프레임워크에서만이 아닌 OOP 전반적으로 사용되는 기술이지만, 스프링의 3대 핵심기술(IoC/DI, AOP, 서비스 추상화) 중 하나로 스프링프레임워크의 근간이 되는 중요한 기술이다.

     

    AOP에서 대표적으로 등장하는 용어들은 다음과 같다.

    Aspect

    AOP의 기본 모듈을 말한다. 한 개 혹은 그 이상의 포인트컷과 어드바이스 조합으로 구성되며, 보통은 싱글톤 객체로 존재한다.

    e.g. 스프링 프레임워크의 어드바이저

    Advisor(어드바이저)

    어드바이저 = 포인트컷 + 어드바이스.

    포인트컷과, 어드바이스를 각각 하나씩 갖는 오브젝트를 어드바이저라고 하며 아주 단순한 aspect 라고 할 수 있다. 일반적인 AOP에서 사용되는 용어가 아닌 스프링 AOP에서만 사용되는 특별한 용어이다.

    Target(타깃)

    부가기능을 제공할 대상.

    주로 핵심기능을 담은 클래스가 대상이 된다. 간혹 다른 부가기능을 제공하는 프록시 오브젝트가 되기도 함.

    Advice(어드바이스)

    타깃에게 제공할 부가기능을 담은 모듈. 

    Join Point(조인 포인트)

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

    e.g. 스프링의 프록시를 이용한 AOP에서 조인 포인트는 메서드 실행 단계이다.

    PointCut(포인트컷)

    어드바이스를(부가기능 모듈)을 적용할 조인 포인트를 선별하는 기능.

    e.g. 스프링 AOP의 조인 포인트는 메서드 실행이기 때문에 스프링의 포인트컷이라고 하면 메서드를 선정하는 기능을 말한다.

    Proxy(프록시)

    클라이언트와 타깃 사이에 존재하여 부가기능을 제공하는 객체.

    프록시는 대리자라고도 하는데, 이는 중간에서 클라이언트의 호출을 대신 받아 타깃에 위임하기 때문이다. 위임 과정 중에 부가기능을 부여하는데 스프링프레임워크는 이러한 프록시 객체를 통해 AOP를 지원한다. 

    스프링의 AOP

    스프링에서 적용할 수 있는 AOP기술은 크게 두 가지가 있다.

    • 프록시 사용하기
    • 바이트코드 조작하기(ex. AspectJ) 

    프록시 (ex. Spring AOP)

    스프링은 기본적으로 프록시 방식의 AOP기술을 사용한다.
    앞서 말한 것처럼 AOP에서 프록시는 클라이언트와 의존성 주입으로 연결된 빈 사이에 존재해 타깃(핵심기능)에게 부가기능을 제공한다. 프록시에 전달된 클라이언트의 요청은 어드바이스 객체(부가기능을 담은 모듈)로 전달되는데, 이 어드바이스 객체가 스프링 어드바이스 인터페이스( e.g.
    InvocationInterceptor/MethodInterceptor 인터페이스)를 구현한 객체로 부가기능을 제공하고 핵심기능을 호출한다. 이렇게 요청을 처리한 후에 대한 결과는 어드바이스에서 다시 프록시를 거쳐 클라이언트에게 전달된다. 좀 더 자세한 흐름은 아래 그림을 참고하자. 


    어드바이스를 다양한 타깃 메소드에 동적으로 적용해 주기 위해 스프링 AOP는 다양한 기술을 조합하여(ex. 데코레이터 패턴, 다이나믹 프록시, 자동 프록시 생성기법, 빈 후처리 조작 기법 등) 프록시를 지원한다. 스프링 AOP에서 사용하는 프록시는 크게 두 가지로 구분할 수 있는데,
    타깃 오브젝트의 인터페이스의 유무와 프록시의 인터페이스 구현 여부에 따라 나뉘에 동작한다.

    • JDK 다이나믹 프록시
    • CGLib 프록시

    JDK 다이나믹 프록시 (타깃 인터페이스 구현 O)

    • 기본 JDK와 스프링 컨테이너로 프록시 객체를 생성한다. 
    • 타깃의 인터페이스가 존재하고, 실제 타깃은 인터페이스를 구현한 클래스이다.
    • 프록시가 타깃의 인터페이스를 구현한다.
    • Reflection을 사용한다.

    CGLib 프록시 (타깃 인터페이스 구현X, 타깃 클래스 상속)

    • (외부)CGLib라이브러리를 사용하여 바이트코드를 조작해 프록시 객체를 생성한다.
    • 타깃의 인터페이스가 존재하지 않는다.
    • 프록시가 타깃 클래스를 상속받는다.
      • final 클래스 final 메서드에는 적용이 안 된다.
      • 상속으로, 타킷 클래스의 생성자가 두 번 호출된다.

    참고로 spring 프레임워크에서는 JDK 다이나믹프록시를, springboot 프레임워크에서는 CGLib프록시를 디폴트로 사용하고 있다. springboot에서 CGLib의 단점들을 보완하여 Reflection 사용으로 unexpected cast exception가 날 수 있는 JDK 다이나믹 프록시 대신 CGLib 프록시 방식을 사용하게 되었다고 한다.

    바이트코드 생성과 조작 (ex. AspectJ)

    프록시를 사용하지 않는 대표적인 AOP 프레임워크는 AspectJ이다. 이 프레임워크는 프록시와는 달리 타깃 오브젝트를 직접 뜯어고쳐서 부가기능을 넣는 방법 (컴파일된 타깃의 클래스 파일 자체를 수정하거나 JVM에 클래스가 로딩되는 시점을 가로채서 바이트코드를 조작하는 복잡한 방법)을 사용한다. 바이트코드를 조작함에 따라 JVM 실행 옵션을 변경하거나 별도의 바이트코드 컴파일러가 필요할 수도 있다. 

    복잡한 방법이긴 하지만 스프링 컨테이너가 사용되지 않는 환경에서도 AOP를 적용할 수 있고 메서드 실행만이 아닌 오브젝트 생성, 필드 값 수정 등 다양한 작업에 부가기능을 적용할 수 있게 한다는 장점이 있다.

     

    자세한 비교한 내용은 아래 블로그를 참고하자!

    https://www.baeldung.com/spring-aop-vs-aspectj

    @AspectJ (AspectJ를 차용한 Spring AOP)

    @AspectJ는 자바 클래스와 메서드, 애노테이션을 이용해서 애스펙트를 정의하는 방법으로 애너테이션으로 aop를 손쉽게 적용할 수 있는 방식이다. 유념해야할 점은 문법과 AspectJ의 애스펙트 정의 방법을 차용할뿐 AspectJ AOP를 사용하는 것은 아니라는 점이다. 

    애스펙트를 클래스를 정의하기 위해서는 클래스에 @Aspect를 선언하고, 메서드를 만들고 @Before, @Around, @After와 같은 어드바이스 구현을 지원하는 애너테이션을 붙여 어드바이스 메서드를 만들면 된다. 어드바이스 구현을 지원하는 애너테이션의 인수로는 pointCut 표현식이 들어간다. poinCut 표현식과 어드바이스 관련 애너테이션도 종류가 많기 때문에 상황에 맞춰서 사용하면 된다. 

     

    다음은 커스텀 애노테이션을 포인트 컷으로 사용해 @AspectJ aop를 적용하는 간단한 예시이다.

     

    1. build.gradle 에 의존성 추가

        implementation 'org.springframework.boot:spring-boot-starter-aop'
        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'

     

    2. 커스텀 애노테이션 만들기

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MethodLogging {
    }

     

    3. aspect 만들기

    • @MethodLogging 애너테이션가 선언된 메서드가 실행될 때 어드바이스 메서드가 실행된다.
    • PointCut을 메서드로 여러개 만들고 이를 @Before, @Around와 같은 어드바이스 애너테이션 인수로 넘겨줄 수도 있다. 단, &&로 묶는다.
      ex) @Before("methodLogger() && validateArgs()")
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.stereotype.Component;
    
    @Slf4j
    @Aspect
    @Component
    public class MethodLoggingAspect {
    
        @Pointcut("@annotation(com.example.simple_practice.aop.MethodLogging)")
        public void methodLogger() {
        }
    
        @Before("methodLogger()")
        public void LoggingMethod() {
            log.info("Order Controller method just called!...logging...");
        }
    
    }

    4. aop를 적용할 곳에 만든 애노테이션 달아주기

    @RestController
    @RequestMapping("/api/order")
    public class OrderController {
    
        @MethodLogging
        @GetMapping()
        public Order getOrder(@RequestParam int orderNumber) {
            return new Order(orderNumber, "홍길동", 13000, LocalDateTime.now(), LocalDateTime.now());
        }

    참고