이 포스트는 김영한님의 ‘스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술’을 수강하고 작성하였습니다.

AOP

AOP는 Aspect Oriented Programming의 약자로, 관점 지향 프로그래밍을 말한다.

어떤 로직을 기준으로 핵심 관심 사항(core concern)과 공통 관심 사항(cross-cutting concern)을 나누어 공통 관심 사항을 모듈화하는 것이 핵심이다.

AOP를 언제 쓰는가?

회원 관리 예제를 진행하는 입장에서 한 가지 가정을 해보자. 만약 내가 지금까지 만들었던 컨트롤러, 서비스, 리포지토리 함수의 실행 시간을 측정해야 한다고 했을 때, 나는 각각의 함수마다 시작 시간과 종료 시간을 구해서 그 차이를 계산할 수 있다.

그런데 그 작업을 일일이 모든 함수에 적용해야 한다면 생각만 해도 머리 아프고 유지보수성 떨어지는 게 안 봐도 뻔하다.

이때 컨트롤러, 서비스, 리포지토리에서 하는 일(= 비즈니스 로직)은 핵심 관심 사항이고, 시간을 측정하는 일은 공통 관심 사항이다. 때문에 시간을 측정하는 함수를 구현할 때 AOP 방식을 사용할 것이다.

AOP 예시

수동 Bean 등록

간단하게 컴포넌트 스캔을 이용하는 방법도 있지만, 수동으로 Bean에 등록하면 명시적으로 내가 어떤 AOP를 사용하고 있는지를 볼 수 있기 때문에 이 방법을 권장한다고 한다.
따라서 나는 이 방법으로 예제를 구현했다.

MyTimeTraceAOP 생성

package hello.hellospring.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect // 공통 관심 사항을 모듈화한 것임을 의미
public class MyTimeTraceAOP {
    // @Around("execution()")로 hello.hellospring 패키지 내 SpringConfig을 제외한 모든 곳에서 실행
    @Around("execution(* hello.hellospring..*(..)) && !target(hello.hellospring.SpringConfig)")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("START: " + joinPoint.toString());
        try {
            // 원래 실행하려는 함수(= 핵심 관심 사항, 비즈니스 로직) 실행
            return joinPoint.proceed();
        } finally {
            long end = System.currentTimeMillis();
            long ms = end - start;
            System.out.println("END: " + joinPoint.toString() + " => " + ms + "ms");
        }
    }
}

SpringConfig에 AOP 등록

package hello.hellospring;

// import 생략

@Configuration
public class SpringConfig {

    // 생략
    
    @Bean
    public MyTimeTraceAOP myTimeTraceAOP() {
        return new MyTimeTraceAOP();
    }
}

컴포넌트 스캔 사용

MyTimeTraceAOP 생성

package hello.hellospring.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect
@Component
public class MyTimeTraceAOP {
    @Around("execution(* hello.hellospring..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("START: " + joinPoint.toString());
        try {
            return joinPoint.proceed();
        } finally {
            long end = System.currentTimeMillis();
            long ms = end - start;
            System.out.println("END: " + joinPoint.toString() + " => " + ms + "ms");
        }
    }
}

AOP 동작 방식

예시로 서비스의 한 함수에 MyTimeTraceAOP가 적용된다고 가정해보면 다음과 같은 순서로 동작한다.

helloController프록시helloService스프링helloServicejoinPoint.proceed()스프링 컨테이너출처: 김영한, 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술
  1. helloController에서 helloService를 호출한다.
  2. 스프링 컨테이너에 등록된 실제 스프링 helloService 대신, 프록시 helloService를 호출한다.
  3. 프록시 helloService 내에서 joinPoint.proceed()를 실행하면 그때 스프링 helloService를 실행한다.

여담

TimeTraceAOP 였던 것

MyTimeTraceAOP 클래스를 만들 때 처음에는 TimeTraceAOP로 했다가 Bean을 만들 때 이미 같은 이름이 있다고 빌드를 실패했다…

순환 참조 발생

참고: AOP(TimeTraceAop)를 @Component 로 선언 vs SpringConfig에 @Bean으로 등록

수동으로 Bean에 등록했을 때의 MyTimeTraceAOP와 컴포넌트 스캔을 이용한 MyTimeTraceAOP@Aroundexecution 표현식을 보면 차이가 있다.

  • 수동으로 Bean 등록 시 MyTimeTraceAOP
    @Around("execution(* hello.hellospring..*(..)) && !target(hello.hellospring.SpringConfig)")
    
  • 컴포넌트 스캔 MyTimeTraceAOP
    @Around("execution(* hello.hellospring..*(..))")
    

수동으로 Bean을 등록했을 때 execution 범위에서 SpringConfig을 제외하지 않으면, SpringConfigmyTimeTraceAOP()를 호출하여 스프링 컨테이너에 빈을 등록할 때 순환 참조가 발생한다.

그러나 컴포넌트 스캔 방식을 이용하면 SpringConfigmyTimeTraceAOP() 함수 호출이 필요가 없어 해당 부분이 없기 때문에 순환 참조가 발생하지 않는다.

댓글남기기