Spring에서 AOP(Aspect-Oriented Programming) 적용하기

ABL·2024년 6월 20일
2
post-custom-banner

AOP(Aspect-Oriented Programming)란?

횡단 관심사(Cross-Cutting Concerns)를 모듈화하기 위한 기술

여기서 횡단 관심사란, 애플리케이션의 핵심 비즈니스 로직과는 별개로 여러 모듈에 공통적으로 적용되는 기능을 의미합니다!

스프링에서 AOP는 프로그램의 여러 곳에서 반복적으로 사용되는 공통 기능(예: 로그 기록, 보안, 트랜잭션 관리)을 분리해서 코드를 더 깔끔하고 관리하기 쉽게 만드는 방법입니다.
예를 들어, 모든 메소드 호출 전에 로그를 남기고 싶다면, 일일이 모든 메소드에 로그 코드를 넣는 대신 AOP를 사용해 한 번만 정의해두면 됩니다!


간단하게 AOP의 사용방식을 설명하자면,

1. Aspect 클래스 만들기
2. 포인트컷 설정

이렇게 정의할 수 있습니다.

Aspect Class는 공통으로 적용하고 싶은 기능을 정의하는 클래스 입니다. 예를 들어, "메소드가 실행되기 전에 로그를 남기자" 같은 기능을 이 클래스에 명시합니다.

다음으로 포인트컷 설정을 설정해야합니다. 이는 공통 기능을 "언제" 적용할지 정하도록 합니다. 예를 들어, "특정 패키지 안의 모든 메소드에 적용하자" 처럼 동작할 수 있습니다.


주요 개념

Aspect: 횡단 관심사를 모듈화한 것. 여러 개의 애드바이스(Advice)와 포인트컷(Pointcut)으로 구성됨

Advice: 실제로 처리해야 할 로직을 정의한 코드

Join Point: 프로그램 실행 중에 Advice가 적용될 수 있는 특정 시점
(example. 메서드 호출이나 예외 발생 시점 등)

  • Before Advice: 타겟 메서드가 호출되기 전에 실행
  • After Returning Advice: 타겟 메서드가 정상적으로 완료된 후에 실행
  • After Throwing Advice: 타겟 메서드가 예외를 던진 후에 실행
  • After (Finally) Advice: 타겟 메서드의 성공 여부와 상관없이 실행
  • Around Advice: 타겟 메서드 호출 전후에 실행

Pointcut: Advice가 적용될 Join Point를 지정하는 표현식

Target Object: 애드바이스를 받는 객체입니다.

Introduction: 특정 타입의 메서드 구현을 추가하여 기존 클래스를 확장

AOP Proxy: AOP 기능을 구현하기 위해 생성된 프록시 객체


Spring에 적용하기

AspectJ 라이브러리 추가
build.gradle

implementation 'org.springframework.boot:spring-boot-starter-aop'
  1. @Aspect
  • 해당 클래스가 Aspect임을 나타냄.
  • 해당 클래스가 횡단 관심사를 정의하는 클래스임을 명시!
@Aspect
@Component
public class LoggingAspect {
    // 애드바이스 정의
}
  1. @Before - 타겟 메서드가 호출되기 전에 실행, 반환값 참조 불가
@Aspect
public class LoggingAspect {
    
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore() {
        System.out.println("Method execution started");
    }
}
  1. @AfterReturning - 타겟 메서드가 정상적으로 완료된 후에 실행, 반환값 참조 가능
@Aspect
public class LoggingAspect {
    
    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
    public void logAfterReturning(Object result) {
        System.out.println("Method returned value is : " + result);
    }
}
  1. @AfterReturning - 타겟 메서드가 예외를 던진 후에 실행, 예외 객체 참조 가능
@Aspect
public class LoggingAspect {
    
    @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "error")
    public void logAfterThrowing(Throwable error) {
        System.out.println("Exception : " + error);
    }
}
  1. @After - 타겟 메서드가 성공적으로 완료되었든, 예외를 던졌든 상관없이 실행
@Aspect
public class LoggingAspect {
    
    @After("execution(* com.example.service.*.*(..))")
    public void logAfter() {
        System.out.println("Method execution finished");
    }
}
  1. @Around - 타겟 메서드 호출 전후에 실행, ProceedingJoinPoint 객체를 사용하여 타겟 메서드를 호출
  • joinPoint.proceed()를 이용하여 직접 메소드 실행을 제어할 수 있음

@Aspect
public class LoggingAspect {
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Method execution started");
        Object result = joinPoint.proceed(); // 타겟 메서드 호출
        System.out.println("Method execution finished");
        return result;
    }
}
  1. @Pointcut - 포인트컷 표현식을 정의하는 데 사용
@Aspect
public class LoggingAspect {
    
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {
        // 포인트컷 정의
    }

    @Before("serviceLayer()")
    public void logBefore() {
        System.out.println("Method execution started");
    }
}

적용해보기!

AOP를 사용하여 로깅을 구현하는데, 이 때 메소드 실행 시간을 측정하여 같이 기록하기

package com.example.demo.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    @Around("execution(* com.example.demo.service.*.*(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 메소드 시작 시간 기록
        long startTime = System.currentTimeMillis();
        
        // 메소드 이름과 인수 로깅
        logger.info("Entering method: {} with arguments: {}", joinPoint.getSignature(), joinPoint.getArgs());
        
        Object result;
        try {
            // 타겟 메소드 실행
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            // 예외가 발생한 경우 로깅
            logger.error("Exception in method: {} with cause: {}", joinPoint.getSignature(), throwable.getCause() != null ? throwable.getCause() : "NULL");
            throw throwable;
        }

        // 메소드 종료 시간 기록 및 소요 시간 계산
        long endTime = System.currentTimeMillis();
        long duration = endTime - startTime;
        
        // 메소드 종료와 결과 로깅
        logger.info("Exiting method: {} with result: {}. Time taken: {} ms", joinPoint.getSignature(), result, duration);
        
        // 타겟 메소드의 결과 반환
        return result;
    }
}
profile
💻
post-custom-banner

0개의 댓글