AOP의 기본 개념과 Spring에서의 적용 방법

김유정·2023년 7월 1일
0

들어가며

트랜잭션의 동작과정이 궁금해서 살펴보려 하니 계속해서 AOP라는 개념이 등장해서 공부하게 됐다. 구체적인 예시를 설명하기보다는 AOP를 이해하기 위해 필요한 기본적인 개념과 간략한 활용 방법을 정리해보려 한다.

AOP란?

  • AOP는 aspect-oriented programming의 약자로 관점 지향 프로그래밍이라고도 말한다.
  • AOP는 프로그램 구조에 대한 또 다른 사고 방식을 제공하여 객체 지향 프로그래밍(OOP)을 보완한다. OOP에서 모듈화의 핵심 단위가 "클래스"라고 한다면, AOP에서는 핵심 단위가 "관점"이다.
  • 반복되고 공통적으로 사용되는 부분을 분리함으로써 모듈성을 증가시키는 프로그래밍 방법이다.

"관점 지향"이라는 말이 잘 와닿지 않는데, 분업을 떠올리면 좀 더 이해가 쉽다. 로깅과 트랜잭션, 보안 기능을 AOP의 대표적인 예시로 많이 등장하는데, 트랜잭션을 예로 들어보자. 비즈니스 로직 전과 후에 트랜잭션을 위한 처리를 해주어야 한다.

  1. 비즈니스 로직 전: 비즈니스 메서드가 현재 실행중인 데이터베이스 트랜잭션에서 실행되야 하는지 새로운 독립적인 트랜잭션에서 실행되어야하는지 결정한다.
  2. 비즈니스 로직 실행
  3. 비즈니스 로직 후: 예외가 발생했다면 트랜잭션을 롤백 처리하고, 그렇지 않다면 커밋한다.

Serive 계층에는 요구사항을 처리하기 위한 수많은 메서드가 있는데, 트랜잭션 처리가 필요한 메서드마다 로직 전 후로 코드를 작성해야 한다면 매우 번거로워진다. 또한 에러가 발생하기 쉽고 관리가 어려워진다.

가장 핵심적인 비즈니스 로직에 집중하고 싶다면? 관심사를 분리하자.

비즈니스를 위해 필요한 부분과 트랜잭션 처리를 위해 필요한 부분은 관심사가 다르다고 볼 수 있다. 이를 분리한다면 각각의 관심사에 집중할 수 있다. 여러 메서드에 반복적으로 사용되며 흩어져있던 코드가 한 곳으로 모아지게 되기 때문에, 아래와 같은 장점이 있다.

  • 수정이 편해진다.
  • 반복되는 코드가 줄어든다.
  • 재사용성이 높아진다.
  • 비즈니스 로직에 집중할 수 있다.

AOP 용어 정리

  • Cross-cutting concerns(횡단 관심사)
    로직 전 또는 후에 실행되어야하는 공통적인 작업.
  • Advice
    aspect가 해야하는 작업과 시기.
  • Target
    advice가 적용될 객체.
  • Join Point
    advice를 적용할 수 있는 지점들. 메서드, 필드, 객체, 생성자 등. Spring AOP에서는 메서드가 실행될 때만 적용되도록 한정하고 있다.
  • Point Cut
    실제 advice가 적용될 지점. advice가 적용될 수 있지만 적용되지 않는 곳도 존재할 것이다. point cut은 join point 중에서도 advice가 적용되는 지점을 말한다.
  • Proxy
    advice를 target 객체에 적용하면 생성되는 객체. Aspect를 구현하기 위해 프레임워크로부터 만들어진 객체라고도 말한다. Spring 프레임워크에서 AOP proxy는 JDK dynamic proxy 또는 CGLIB proxy이다.
  • Aspect
    AOP에서는 횡단관심사를 aspect라는 특별한 객체로 모듈화한다. 이는 advice와 point cut을 합친 것으로, 무엇을 언제 어디서 할지에 대한 정보가 모두 정의되어 있다.
  • Weaving
    target 객체에 aspect를 적용해서 새로운 프록시 객체를 생성하는 절차. aspect는 target 객체의 join point로 위빙된다. 위빙은 대상 객체의 생애 중 다음과 같은 시점에서 수행될 수 있다.

AOP 적용 방법

위빙은 대상 객체의 생애 중 수행되는 시점에 따라 아래 3가지로 나뉠 수 있다.
1. 컴파일 타임 위빙(Compile time weaving = CTW)
타겟 클래스가 컴파일될 때 aspect가 위빙되며 별도의 컴파일러가 필요하다. AspectJ의 위빙 컴파일러는 이러한 목적으로 사용된다.
2. 로드 타임 위빙(Load time weaving = LTW)
클래스가 JVM에 적재될 때 aspect가 위빙된다. 이를 위해서는 애플리케이션에서 사용되기 전에 타켓 클래스의 바이트코드를 enhance하는 특별한 ClassLoader가 필요하다. AspectJ의 로드시간 위빙 기능을 사용하면 클래스로드시간에 위빙된다.
3. 실행 시간 위빙(Runtime Weaving = RTW)
애플리케이션 실행 중에 aspect가 위빙된다. 보통 타겟 객체에 호출을 위임하는 구조의 프록시 객체를, 위빙 중에 AOP 컨테이너가 동적으로 만들어낸다. 스프링 자체 AOP의 aspect는 실시간 위빙을 사용한다.

이제 AOP가 뭔지는 알겠는데, 그래서 이걸 어떻게 사용할 수 있다는 걸까? Spring에서 AOP를 구현하는 방법에 대해 알아보자!

AOP 구현 방법

AOP는 패러다임이기 때문에, 각 언어마다 구현체가 있다. Java 프로젝트에서 AOP를 구현하기 위해 지원하는 프레임워크는 AspectJ, JBoss AOP, Spring AOP가 있다고 한다.

Spring에서는 다음 3가지 방법 중 한 가지를 선택하여 AOP를 구현할 수 있다. Spring 공식문서에서는 일반적인 애플리케이션의 경우 AspectJ pointcut을 활용하여 AOP를 사용할 것을 권장하고 있다.
1. @AspectJ 어노테이션 활용
2. 스키마 기반 접근법(schema-based approach)
3. Spring AOP API 활용

의존성이 아래와 같이 구성되어 있기 때문에, spring-boot-starter-data-jpa 나 spring-boot-starter-data-aop를 의존성으로 추가하면, Spring AOP와 AspectJ를 모두 사용할 수 있다.

구현하는 방법에 어떤 것들이 있는지 알았으니, 이제 대략적으로 어떻게 사용하는지 알아보려한다.

@AspectJ 어노테이션 활용

1. @AspectJ 를 사용하기 위한 설정

@AsjpectJ의 지원을 허용하는 것이 가장 첫 번째 단계이다. 설정 파일에 다음과 같이 @EnableAspectJAutoProxy 어노테이션을 활용하여 허용할 수 있다.

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

2. Aspect 구현하기

Aspect는 앞서 용어 정리에서 언급한 듯이 Advice와 Pointcut으로 구성된다. 우선 Advice의 경우 아래처럼 5개의 어노테이션을 지원한다.

어노테이션설명
@Before비즈니스 로직 실행 전
@AfterReturning비즈니스 로직 실행 결과가 정상적으로 반환된 후
@AfterThrowing비즈니스 로직 실행 시 예외가 발생된 후
@After비즈니스 로직 실행 후
@Around비즈니스 로직 실행 전과 후

@Pointcut 어노테이션은 반복적으로 사용되는 pointcut을 지정할 수 있다. 해당 어노테이션을 활용하지 않고, advice 어노테이션 안에 pointcut을 지정해도 된다.

아래는 com.example.aop.annotation.service에 있는 모든 메서드에 Before Advice와 After Advice를 적용하는 예시인데, @Pointcut 어노테이션을 활용해 반복되는 pointcut을 변수로 만들어 활용하고 있다.

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Slf4j
@Aspect
public class TimeLog {
    @Pointcut("within(com.example.aop.annotation.service..*)")
    private void annotationServicePointcut() {}

    @Before("annotationServicePointcut()")
    public void start() {
        log.info("시작: {}ms", System.currentTimeMillis());
    }

    @After("annotationServicePointcut()")
    public void end() {
        log.info("종료: {}ms", System.currentTimeMillis());
    }
}

Spring AOP API 활용

Spring AOP에서는 Proxy를 이용하여 AOP를 구현하기 때문에, 위에서 AspectJ를 활용한 방식과는 조금 다르다. 우선 동작과정에 대해 간단하게라도 살펴보고 넘어가자.

  1. 스프링 빈 대상이 되는 객체를 생성한다.(@Bean, 콤포넌트 스캔 대상)
  2. 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
  3. 모든 Advisor 빈을 조회하고 Pointcut을 통해 클래스와 메서드 정보를 매칭해보면서 프록시를 적용할 대상인지 판단한다.
  4. 객체의 모든 메서드를 포인트컷에 비교해보면서 조건이 하나라도 만족한다면 프록시를 생성하고 프록시를 빈 저장소로 판단한다.
  5. 만약 프록시 생성 대상이 아니라면 들어온 빈 그대로 빈 저장소로 반환한다.
    빈 저장소는 객체를 받아서 빈으로 등록합니다.

Aspect 구현

여기서도 Advice를 정의해줘야한다. Spring AOP에서는 Advice 유형에 따른 인터페이스를 아래와 같이 지원한다.

어드바이스 유형인터페이스
Beforeorg.springframework.aop.MethodBeforeAdvice
After-returningorg.springframework.aop.AfterReturningAdvice
After-throwingorg.springframework.aop.ThrowsAdvice
Aroundorg.aopalliance.intercept.MethodInterceptor
Introductionorg.springframework.aop.IntroductionInterceptor

예시

MethodInterceptor 인터페이스를 활용하여 Around Advice를 생성하고 Target에 적용했다.

public class DynamicProxyTest {
    @Test
    public void proxyFactoryBean() {
        ProxyFactoryBean pfBean = new ProxyFactoryBean();
        // 타깃 설정
        pfBean.setTarget(new HelloTarget());
        // 부가기능을 담은 어드바이스 추가
        // 여러개 추가도 가능
        pfBean.addAdvice(new UppercaseAdvice());

        // FactoryBean 이므로 getObject()로 생성된 프록시를 가져온다.
        Hello proxiedHello = (Hello) pfBean.getObject();
        System.out.println(proxiedHello.sayHello("toby"));
    }

    static class UppercaseAdvice implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            System.out.println("시작");
            // 타깃 오브젝트의 메서드 실행
            String ret = (String)invocation.proceed();
            System.out.println("종료");
            return ret.toUpperCase();
        }
    }

    // 타깃과 프록시가 구현할 인터페이스
    static interface Hello {
        String sayHello(String name);
        String sayThankYou(String name);
    }

    // 타깃 클래스
    static class HelloTarget implements Hello {
        @Override
        public String sayHello(String name) {
            return "Hello " + name;
        }

        @Override
        public String sayThankYou(String name) {
            return "Thank You " + name;
        }
    }

}

마치며

아직 어떤 방법이 좋은 방법인지, 실무에서는 어떻게 활용하고 있는지 잘 감이 오지 않는다. AOP가 뭔지는 알았는데, Proxy에 대해서는 온전히 이해하지 못한 것 같다. 런타임시에 Aspect를 적용하기 위해 만들어지는 객체라는 정도만 이해했다.

트랜잭션의 처리 과정을 보면서 실제로 Spring에서 AOP는 어떤식으로 활용되었는지 살펴보면서 Proxy에 대한 부분도 추가로 공부하는 게 좋을 것 같다.

참고

이미지 출처

0개의 댓글