SPRING&BOOT

AOP(Aspect-Oriented Programming) 개념 및 실습

jki09871 2024. 9. 10. 16:00

1. AOP란?

**AOP(Aspect-Oriented Programming)**는 코드에서 공통적으로 반복되는 횡단 관심사를 핵심 비즈니스 로직에서 분리하는 프로그래밍 기법이다. AOP를 사용하면 로깅, 트랜잭션 관리, 메서드 실행 시간 측정 등 여러 곳에서 사용되는 부가기능을 한 곳에 모아 관리할 수 있다.


2. AOP 용어 정리

  • Aspect(애스팩트): 프로그램에서 반복적으로 필요하지만, 모든 코드에 일일이 넣기 귀찮은 기능들을 한데 묶은 모듈이다. 예를 들어, 로그를 남기거나 보안 처리를 한 곳에 모아서 관리하는 것이라고 보면 된다.
  • Advice(어드바이스): 실제로 실행될 부가기능이다. 예를 들어, 어떤 메소드가 실행될 때 로그를 남기는 구체적인 코드가 어드바이스이다.
  • Pointcut(포인트컷): 어드바이스가 언제 실행될지를 정하는 규칙이다. 예를 들어, 특정 메소드나 클래스가 실행될 때만 어드바이스가 작동하도록 정하는 것이 포인트컷이다.
  • JoinPoint(조인포인트): 어드바이스가 실제로 실행될 수 있는 지점을 의미한다. 메소드가 실행되거나 객체가 생성되는 순간 등이 조인포인트이다.
  • Target(타겟): 어드바이스가 적용될 실제 객체이다. 예를 들어, 어떤 서비스 메소드에 어드바이스를 적용하면 그 메소드를 가지고 있는 객체가 타겟이 된다.

한마디로, Aspect는 여러 부가기능을 묶어서 필요한 곳에 자동으로 적용하게 만드는 기능이고, Advice는 그 부가기능의 실제 코드, Pointcut은 그 코드가 언제 실행될지를 정하는 규칙, JoinPoint는 실행 가능한 지점, Target은 그 기능이 적용되는 실제 객체이다.


3. AOP를 적용한 실습: 메서드 실행 시간 측정

문제 상황

메서드 실행 시간을 측정해야 하는데, 이를 각 서비스 메서드마다 일일이 추가하면 코드가 지저분해지고 유지보수성이 떨어진다. 이런 상황에서 AOP를 적용하면 실행 시간 측정 로직을 하나의 모듈로 관리할 수 있다.

구현 목표

AOP를 사용해 특정 패키지나 특정 어노테이션이 붙은 메서드들에 대해 실행 시간 측정을 적용하는 방법을 살펴보자.


4. 코드 설명

4-1. Aspect 클래스 전체 구조

package com.standard.sparta.aop;

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

@Slf4j
@Aspect
public class AspectPractice {

    // 포인트컷 및 어드바이스들이 정의되어 있는 클래스
}

 

  • 이 클래스는 @Aspect 어노테이션으로 AOP 역할을 하는 클래스임을 표시했다.
  • @Slf4j는 로깅을 위한 롬복 어노테이션으로, 메서드 실행 시간을 로그로 출력하는데 사용된다.

4-2. 포인트컷(Pointcut) 정의

    /**
     * 포인트컷: 서비스 레이어의 모든 메서드에 적용
     */
    @Pointcut("execution(* com.standard.sparta.service..*(..))")
    private void serviceLayer() {}

    /**
     * 포인트컷: @TrackTime 어노테이션이 붙은 메서드에만 적용
     */
    @Pointcut("@annotation(com.standard.sparta.annotation.TrackTime)")
    private void trackTimeAnnotation() {}

 

 

  • serviceLayer: com.standard.sparta.service 패키지 내의 모든 메서드를 대상으로 하는 포인트컷이다. 패키지 경로를 설정하여 특정 클래스나 메서드에 어드바이스를 적용할 수 있다.
    • execution(* com.standard.sparta.service..*(..))는 service 패키지 내에 있는 모든 메서드에 적용하겠다는 의미이다.
  • trackTimeAnnotation: @TrackTime이라는 커스텀 어노테이션이 붙은 메서드에 대해서만 어드바이스를 적용하는 포인트컷이다.

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

4-3. 어드바이스(Advice) 설명

   1. @Around 어노테이션을 사용한 어드바이스

    /**
     * 어드바이스: @TrackTime 어노테이션 기반으로 메서드 실행 시간을 측정
     */
    @Around("trackTimeAnnotation()")
    public Object adviceAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis(); // 실행 시간 측정 시작

        try {
            // 메서드 실행
            Object result = joinPoint.proceed();
            return result;
        } catch (Exception ex) {
            log.error("Exception in method: {}", joinPoint.getSignature(), ex);
            throw ex; // 예외 다시 던짐
        } finally {
            // 실행 시간 측정 종료 및 로그 출력
            long endTime = System.currentTimeMillis();
            long executionTime = endTime - startTime;
            log.info("Execution time of {}: {} ms", joinPoint.getSignature(), executionTime);
        }
    }

 

  • 어노테이션 기반 실행 시간 측정
    • 이 어드바이스는 @TrackTime 어노테이션이 붙은 메서드에 대해 적용된다.
    • **joinPoint.proceed()**를 통해 원래 메서드를 실행한 후, 실행 시간이 끝난 후에 측정된 시간을 로그로 출력한다.
    • 예외가 발생할 경우, catch 블록에서 처리한 후 다시 예외를 던진다.

  2.  @AfterReturning 어드바이스

    /**
     * 어드바이스: 서비스 패키지 기반으로 메서드 실행 시간을 측정
     */
    @Around("serviceLayer()")
    public Object advicePackageMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis(); // 실행 시간 측정 시작

        try {
            // 메서드 실행
            Object result = joinPoint.proceed();
            return result;
        } catch (Exception ex) {
            log.error("Exception in method: {}", joinPoint.getSignature(), ex);
            throw ex; // 예외 다시 던짐
        } finally {
            // 실행 시간 측정 종료 및 로그 출력
            long endTime = System.currentTimeMillis();
            long executionTime = endTime - startTime;
            log.info("Execution time of {}: {} ms", joinPoint.getSignature(), executionTime);
        }
    }
  • 패키지 기반 실행 시간 측정
    • 이 어드바이스는 com.standard.sparta.service 패키지 내의 모든 메서드에 대해 실행 시간을 측정한다.
    • **joinPoint.proceed()**를 호출하여 원래 메서드를 실행하고, 실행 시간이 끝나면 측정된 시간을 로그로 출력한다.

어드바이스 종류

더보기
  • @Around: '핵심기능' 수행 전과 후 (@Before + @After)
  • @Before: '핵심기능' 호출 전 (ex. Client 의 입력값 Validation 수행)
  • @After: '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작)
  • @AfterReturning: '핵심기능' 호출 성공 시 (함수의 Return 값 사용 가능)
  • @AfterThrowing: '핵심기능' 호출 실패 시. 즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)

4-4. 기타 어드바이스들

  1. @Before 어드바이스

    /**
     * 어드바이스: 메서드 실행 전에 특정 로직을 실행
     */
    @Before("serviceLayer()")
    public void beforeMethod() {
        log.info("::: BEFORE 메서드 실행 :::");
    }
  • 이 어드바이스는 메서드 실행 전에 동작하며, 서비스 레이어의 모든 메서드에 대해 적용된다. 메서드 실행 전에 특정 작업(로그 출력 등)을 하고 싶을 때 사용된다.

  2. @AfterReturning 어드바이스

    /**
     * 어드바이스: 메서드가 정상적으로 반환된 후에 실행
     */
    @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void afterReturningMethod(Object result) {
        log.info("::: AFTER RETURNING ::: 메서드 실행 완료. 반환 값: {}", result);
    }
  • 메서드가 성공적으로 실행되고 반환된 후에 실행되는 어드바이스다. 정상적인 실행 결과를 받아 후처리할 때 유용하다.

  3. @AfterThrowing 어드바이스

    /**
     * 어드바이스: 메서드 실행 중 예외가 발생했을 때 실행
     */
    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void afterThrowingMethod(Throwable ex) {
        log.info("::: AFTER THROWING ::: 메서드 실행 중 예외 발생: {}", ex.getMessage());
    }
  • 메서드 실행 중 예외가 발생했을 때만 실행되는 어드바이스다. 예외 발생 시 특정 작업을 처리할 수 있다.

  4. @After 어드바이스

    /**
     * 어드바이스: 메서드 실행 후 항상 실행
     */
    @After("serviceLayer()")
    public void afterMethod() {
        log.info("::: AFTER 메서드 실행 :::");
    }
  • 메서드가 정상적으로 실행되든, 예외가 발생하든 상관없이 항상 실행되는 어드바이스다. 주로 리소스 해제 등의 후처리 작업에 유용하다.

  5. @Around 어드바이스

@Around("trackTimeAnnotation()")
public Object adviceAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
    long startTime = System.currentTimeMillis();
    
    // 메서드 실행 전 로그
    log.info("Method {} is starting", joinPoint.getSignature().getName());
    
    try {
        // 메서드 실행
        Object proceed = joinPoint.proceed();
        
        // 메서드 실행 후 로그
        log.info("Method {} successfully finished", joinPoint.getSignature().getName());
        
        return proceed;
    } finally {
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;
        
        // 메서드 실행 시간 로깅
        log.info("Method {} executed in {} ms", joinPoint.getSignature().getName(), executionTime);
    }
}
  • @Before + @After를 합쳐 놓은 것처럼 메서드 실행 전, 후 작동을 한다.

5. 결론

AOP를 사용하면 반복되는 코드를 분리하여 핵심 비즈니스 로직과 부가기능을 명확히 분리할 수 있다. 이번 실습에서는 메서드 실행 시간을 측정하는 로직을 AOP로 구현했고, 포인트컷을 사용해 특정 패키지나 어노테이션이 붙은 메서드에 적용하는 방법을 살펴봤다.