내배캠/TIL

[스프링] AOP

모닝펄슨 2024. 9. 10. 22:19

AOP 란?

Aspect Oriented Programming : 관점 지향 프로그래밍

횡단 관심사 분리

 

OOP(Object Oriented Programming)가 세상을 객체로 정의하는 개념이라면

AOP는 관심사를 분리하여 모듈로 사용하는 것이다.

 

만약 모든 메서드의 실행시간을 측정하라는 요구사항이 있다고 가정해보자. 이때 다음과 같은 문제가 발생할 수 있다.

  • 중복 문제
    • 모든 서비스 메서드에 반복적으로 사용됨.
    • 적용해야 할 메서드가 너무 많음.
  • 변경 문제
    • 측정 단위가 변경되거나
    • 새로운 기능이 추가된다면
    • 적용된 모든 메서드를 일일이 수정해야 함.

 

이러한 문제를 해결하기 위해 AOP를 적용해 볼 수 있다.

 

 

- 핵심기능 : 메서드의 주요 기능
- 부가기능 : 주요 기능이 아닌 부가 기능
- 횡단관심사 : 여러 곳에서 공통적으로 발생하는 부가기능

 

즉, AOP는 핵심기능과 횡단 관심사(부가 기능)를 분리해서 관리하는 것이라고 보면 된다.

 

 

AOP 핵심 키워드

1. 어드바이스 (Advice)

 

실제로 실행되는 횡단관심사(부가기능) 코드를 의미하며, 로깅 처리, 인증인가 처리, 트랜잭션 처리 등이 있을 수 있다.

 

 

2. 포인트컷 (Point Cut)

 

어드바이스를 적용할 구체적인 범위로, 특정 패키지 내의 모든 메서드들이거나 특정 패키지의 클래스의 모든 메서드들일 수도 있고 특정 애너테이션이 달린 메서드일 수도 있다. 포인트컷을 통해 어드바이스를 어느 범위에 적용할지 정확하게 지정할 수 있다.

 

 

3. 타겟 (Target)

 

어드바이스가 적용되는 객체를 의미한다. 포인트컷으로 적용된 특정 클래스가 타겟이 되면, 그 클래스 내의 모든 메서드들이 어드바이스의 적용을 받게 된다.

 

표현식은 다음과 같다.

execution(int createCourse(int, int)) // 반환타입 int, createCourse 이라는 매서드, 매개변수는 int 형 2개
execution(* createCourse(int, int)) // 반환타입 상관없음, createCourse 이라는 매서드, 매개변수는 int 형 2개
execution(* createCourse(..)) // 반환타입 상관없음, createCourse 이라는 매서드, 매개변수 상관없음
execution(* *(..)) // 반환타입 상관없음, 메서드이름 상관없음, 매개변수 상관없음

 

 

4. 조인 포인트 (Join Point)

 

어드바이스가 적용될 수 있는 실행 지점을 의미한다. 우리가 메서드 기준으로 어드바이스를 동작시킴으로 메서드들이 조인포인트가 된다.

 

 

5. 애스팩트 (Aspect)

 

애스팩트는 어드바이스와 포인트컷을 하나로 묶은 모듈이다.

 

 

 

정리

 

어드바이스는 반복되는 횡단관심사를 정의해 놓은 곳이다.

그 횡단관심사를 어느 범위에 적용할지 선택하는 것이 포인트컷이다.

그리고 그 범위 안에서 선택받은 객체들 혹은 객체가 타겟이 되는 것이다.

그 타겟 내에서 어드바이스가 실제로 실행되는 시점을 조인포인트라고 한다.

 

 

 

예시 코드

  • AspectPractice.java
package com.standard.sparta.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

/**
 * Aspect : 모듈. Advice와 PointCut을 하나로 묶은 모듈
 */
@Aspect
@Slf4j
public class AspectPractice {
    // 포인트컷들...
    /**
     * 포인트컷 : 서비스(Service) 패키지 기반
     * ("execution(* com.standard.sparta.service..*(..))")
     * - * : 반환 타입
     * - 경로 지정
     * - * : 이름 (모든 메서드에 지정한다)
     * - (..) : 매개변수 타입. 아무거나 상관없다.
     */
    @Pointcut("execution(* com.standard.sparta.service..*(..))")
    private void serviceLayer() { }

    // 어드바이스들...
    /**
     * 어드바이스 : @Before
     * 메서드 실행 전에 수행되는 로직을 처리할 때 사용
     */
    @Before("serviceLayer()")
    public void beforeMethod() {
        log.info("::: BEFORE 실행 :::");
    }

    /**
     * 어드바이스 : @AfterReturning
     * 메서드가 정상적으로 반환된 후에 실행.
     * 예외가 발생하지 않고 정상적으로 결과 값 반환됐을 때 동작!
     */
    @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void afterReturningMethod(Object result) {
        // result 관련 로직 ...
        log.info("::: AFTER RETURNING 실행 :::");
    }

    /**
     * 어드바이스 : @AfterThrowing
     * 메서드 실행 중 예외가 발생했을 때만 실행!
     */
    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void afterThrowingMethod(Throwable ex) {
        // ex로 예외가 발생했을 때 필요한 조작
        log.info("::: AFTER THROWING :::");
    }

    /**
     * 어드바이스 : @After
     * 메서드가 정상적으로 실행되건, 예외가 발생하건 항상 실행!
     */
    @After("serviceLayer()")
    public void afterMethod() {
        log.info("::: AFTER :::");
    }

    /**
     * !!가장 중요!!
     * 어드바이스 : @Around
     * 가장 강력한 어드바이스, 전체 흐름을 제어할 수 있음!
     * joinPoint : 메서드들에 대한 정보.
     * 매개변수 검증 가능. 실행되는 시점에 매개변수에 대한 정보 받아옴.
     */
    @Around("serviceLayer()")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("::: BEFORE :::");
        try {
            Object result = joinPoint.proceed();
            log.info("::: AFTER RETURNING :::");
            return result;
        } catch (Exception e) {
            log.info("::: AFTER THROWING :::");
            throw e;
        } finally {
            log.info("::: AFTER :::");
        }
    }

    /*
    시간 측정 메서드
     */
    @Around("serviceLayer()")
    public Object advicePackageMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        // 측정 시작
        long startTime = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed();
            return result;
        } finally {
            long endTime = System.currentTimeMillis();
            long executionTime = endTime - startTime;
            log.info("::: Execution Time : {} ms", executionTime);
        }
    }

    /////////////////////////

    /**
     * 포인트컷 : 어노테이션 범위 기반
     * 포인트컷이 어노테이션으로 잡음. 이 어노테이션 메서드 위에 달면 포인트컷으로 잡히는 것!
     */
    @Pointcut("@annotation(com.standard.sparta.annotation.TrackTime)")
    private void trackTimeAnnotation() { }

    /**
     * 어드바이스 : 어노테이션 범위 기반
     */
    @Around("trackTimeAnnotation()")
    public Object adviceAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();

        try {
            Object result = joinPoint.proceed();
            return result;
        } finally {
            long endTime = System.currentTimeMillis();
            long executionTime = endTime - startTime;
            log.info("::: Execution Time : {} ms", executionTime);
        }
    }
}

 

 

  • TrackTime.java
package com.standard.sparta.annotation;

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 TrackTime {
}

 

 

  • WebConfig.java
package com.standard.sparta;

import com.standard.sparta.aop.AspectPractice;
import com.standard.sparta.filter.CustomFilter;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WebConfig {

    @Bean
    FilterRegistrationBean addFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new CustomFilter());
        filterFilterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
        filterFilterRegistrationBean.addUrlPatterns("/*");
        return filterFilterRegistrationBean;
    }

    /**
     * AOP 모듈 등록
     */
    @Bean
    public AspectPractice getAspectPracticeAop() {
        return new AspectPractice();
    }
}

 

 

 

동작 원리

 

스프링에서 객체를 빈으로 등록할 때 빈후처리기를 거치게 되는데, 이때 빈후처리기는 AOP 설정이 적용된 객체를 감지하고 해당 객체를 프록시 객체로 감싼 후 스프링 컨테이너에 등록하게 된다.

 

 

호출 시 실제 대상 객체가 아닌 proxy 객체가 호출이 된다.

 

com.standard.sparta.service.CourseService // APO 미적용: 실제 객체  사용
com.standard.sparta.service.CourseService$$SpringCGLIB$$0 // AOP 적용: Proxy 객체 사용