본문 바로가기

⭐ SpringBoot/𝄜 게시판 with SpringBoot

29. AOP에 대해 알아보

(추후 정리 다시 해야함...)

# 요약

댓글 생성 서비스에서 입력된 파라미터 반환값을 로그로 확인해 보았다. aopo 패키지 폴더안에 DebbuggingAspect와 PerformanceAspect 파일을 생성하여 디버깅과 메소드의 러닝타임 체크를 진행 하였다.

DebbuggingAspect 에서는 AOP 클래스를 선언 하였고 @컴포넌트로 등록한 다음에 @포인터 컷으로 대상을 지정할 수 있었다. 대상이 실행되기 이전에 실행 할 수도 있고, 정상적으로 실행이 된 이후에 실행 할 수도 있다. @애프터리터링 파라미터 값과 오브젝트의 값이 같아야 실행이 가능 했다.

끝으로 DELETE 메소드의 실행시간 측정도 진행 하였는데, 실행시간 측정을 위해서 RunningTime이라는 어노테이션을 직접 정의를 해보았고, 어노테이션이 붙여져 있으면서 동시에 기본 패키지인 애들을 @Around("cut() && enableRunningTime()")을 통해 동시 만족시키는 대상을 선정할 수 있었고 대상을 앞뒤로 전후해서 실행하는 부가 기능은 @Around를 통해서 수행 할 수 있었다.

 

# AOP 개념을 설명하고, 이를 활용한 예시 코드를 작성한다.

 

AOP란?

부가 기능을 특정 지점에 잘라 넣는 기법 으로써, DI가 특정 객체를 주입하는것 처럼 틋정 로직을 주입 하는 것이다.

대표적인 AOP의 예로는 @Transactional이 있다. 이 간단한 어노테이션 하나의 추가로 데이터를 롤백 할 수 있었다.

이처럼 AOP는 부가기능을 특정 지점에 삽입 함으로써 더욱 간결하고 효율적인 프로그래밍이 되도록 구현할 수 있다.

AOP에서 사용하는 주요 어노테이션은 다음과 같은 것들이 있다.

DebuggingAspect 전체 코드

package com.example.firstproject.aop;

import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.Joinpoint;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect // AOP 클래스 선언: 부가 기능을 주입하는 클래스
@Component // IoC 컨테이너가 해당 객체를 생성 및 관리
@Slf4j
public class DebuggingAspect {

    // 어느지점, 어느대상을 타겟으로 해서 부가 기능을 찔러 넣을것인지, 주입할 것인지 정하는것이 바로 @Pointcut 이다.
    // 대상 메소드 선택: CommentService#create()
    @Pointcut("execution(* com.example.firstproject.service.CommentService.*(..))")
    private void cut() {
    }

    // 실행 시점 설정: cut()의 대상이 수행되기 이전에 아래의 메소드가 실행 된다.
    @Before("cut()")
    public void loggingArgs(JoinPoint joinPoint) { // cut()의 대상 메소드
        // 입력값 가져오기
        Object[] args = joinPoint.getArgs();

        // 클래스명
        String className = joinPoint.getTarget()
                .getClass()
                .getSimpleName();
        // 메소드명
        String methodName = joinPoint.getSignature()
                .getName();

        // 입력값 로깅하기
        // CommentService#create()의 입력값 => 5
        // CommentService#create()의 입력값 => CommentDto(id=null, ...)
        for (Object obj : args) { // foreach 문
            log.info("{}#{}의 입력값 => {}", className, methodName, obj);
        }
    }

    // 실행 시점 설정: cut()에 지정된 대상 호출 성공 후에 아래의 메소드가 실행 된다.
    @AfterReturning(value = "cut()", returning = "returnObj")
    public void loggingReturnValue(JoinPoint joinPoint, // cut()의 대상 메소드
                                   Object returnObj) { // 리턴 값

        // 클래스명
        String className = joinPoint.getTarget()
                .getClass()
                .getSimpleName();
        // 메소드명
        String methodName = joinPoint.getSignature()
                .getName();

        // 반환값 로깅
        // CommentService#create()의 입력값 => CommentDto(id=10, ...)
        log.info("{}#{}의 반환값 => {}", className, methodName, returnObj);
    }
}

PerformanceAspect 전체 코드

package com.example.firstproject.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

@Aspect
@Component
@Slf4j
public class PerformanceAspect {
    // 특정 어노티에션을 대상 지정
    @Pointcut("@annotation(com.example.firstproject.annotation.RunningTime)")
    private void enableRunningTime() {
    }

    // 기본 패키지의 모든 메소드드
    @Pointcut("execution(* com.example.firstproject..*.*(..))")
    private void cut() {
    }

    // 실행 시점 설정: 두 조건을 모두 만족하는 대상을 전후로 부가 기능을 삽입
    @Around("cut() && enableRunningTime()")
    public void loggingRunningTime(ProceedingJoinPoint joinPoint) throws Throwable {
        // 메소드 수행 전, 측정 시작
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 메소드 수행
        // joinPoint.proceed()로 타겟팅 된 대상을 수행하고, 헤당 수행의 반환값을 returningObj로 받는다.
        Object returningObj = joinPoint.proceed();

        // 메소드 수행 후, 측정 종료 및 로깅
        stopWatch.stop();
        String methodName = joinPoint.getSignature()
                .getName();
        log.info("{}의 총 수행시간 => {} sec", methodName, stopWatch.getTotalTimeSeconds());
    }
}

RunningTime 의 전체 코드

package com.example.firstproject.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Type;

// 궁금하면 구글링!
@Target({ElementType.TYPE, ElementType.METHOD}) // 어노테이션 적용 대상
@Retention(RetentionPolicy.RUNTIME) // 어노테이션 유지 기간
public @interface RunningTime {
}