Spring Framework - AOP

jungseo·2023년 6월 5일
0

Spring

목록 보기
3/23
post-thumbnail

AOP

  • 스프링 프레임워크가 제공하는 AOP 방식은 런타임 시에 프록시 객체를 생성하여 공통 관심 기능을 적용

1) 프록시(Proxy) 객체

  • 핵심 기능을 다른 객체에게 위임하고 실행 시간 측정과 같은 부가적인 기능을 제공하는 객체
  • 핵심 기능의 코드 변경 없이 공통 기능 구현

  • 구구단 interface
public interface Gugudan {
    void calculate(int level, int count);
}

  • forLoop class
public class GugudanByForLoop implements Gugudan {
    @Override
    public void calculate(int level, int number) {

        for(int count = number; count < 10; count++) {
            System.out.printf("%d x %d = %d\n", level, count, level * count);
        }
    }
}

  • recursion class
public class GugudanByRecursion implements Gugudan {
    @Override
    public void calculate(int level, int count) {

        if (count > 9) return;

        System.out.printf("%d x %d = %d\n", level, count, level * count);
        calculate(level, ++count);
    }
}

  • proxy class
public class GugudanProxy implements Gugudan {

    private Gugudan delegator;

    public GugudanProxy(Gugudan delegator) {
        this.delegator = delegator;
    }

    @Override
    public void calculate(int level, int count) {
    long start = System.nanoTime();
    delegator.calculate(level, count);
    long end = System.nanoTime();

        System.out.printf("클래스명 = %s\n", delegator.getClass().getSimpleName());
        System.out.printf("실행 시간 = %d ns\n", end - start);
        System.out.println("-".repeat(20));
    }
}

  • config class
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy // @Aspect 어노테이션 붙인 클래스를 공통 기능으로 적용
public class GugudanConfig {

    @Bean
    public Gugudan gugudanByForLoop() {
        return new GugudanByForLoop();
    }

    @Bean
    public Gugudan gugudanByRecursion() {
        return new GugudanByRecursion();
    }
}

  • test class
public class GugudanTest {
    public static void main(String[] args) {

//        프록시 객체 사용
        System.out.println("for문 구구단");
        GugudanProxy proxy1 = new GugudanProxy(new GugudanByForLoop());
        proxy1.calculate(3, 1);

        System.out.println("재귀 구구단");
        GugudanProxy proxy2 = new GugudanProxy(new GugudanByRecursion());
        proxy2.calculate(3, 1);


    }
}
  • 출력

2) AOP 핵심 개념

  • AOP(Aspect Oriented Programming)

    • 공통 관심 사항과 핵심 관심 사항을 분리해 코드의 중복을 제거하고 코드의 재사용성을 높이는 프로그래밍 방법론
    • 핵심 기능에 공통기능을 삽입해 핵심 관심 사항 코드의 변경 없이 공통 기능 구현을 추가 혹은 변경 가능
  • 스프링 프레임워크는 프록시 객체를 자동으로 생성하여 AOP를 구현
    (프록시 사용 외에 컴파일 시점에 코드로 공통 기능을 삽입하거나 클래스 로딩 시점에 공통기능 삽입도 가능)

  • 스프링 AOP는 타겟 객체(Target Object)를 외부에서 프록시 객체가 한번 감싸는 구조

(1) 에스펙트(Aspect)

  • 공통 관심 사항에 대한 기능
  • 어드바이스(Advice)와 포인트컷(Pointcut)의 조합으로 구성

(2) 어드바이스(Advice)

  • 공통 관심 사항과 적용 시점을 정의(무엇을 언제 적용할 지)
  • 타겟 객체에 종속되지 않아 공통 기능에 집중 가능
  • 스프링에서 구현 가능한 어드바이스 종류
    -
    • @Before
      • 타깃 객체의 메서드 호출 전 공통 기능 실행
    • @After
      • 예외 발생 여부에 관계없이 타깃 객체의 메서드 실행 후 공통 기능 실행
    • @AfterReturning
      • 타깃 객체의 메서드가 예외 없이 값을 반환한 경우 공통 기능 실행
    • @AfterThrowing
      • 타깃 객체의 메서드 실행 중 예외가 발생한 경우 공통 기능을 실행
    • @Around
      • 타깃 객체의 메서드 실행 전과 후 또는 예외 발생 시 공통 기능을 실행

(3) 조인포인트(Joinpoint)

  • 어드바이스가 적용될 수 있는 위치
  • 스프링 AOP에선 메서드 호출에 대한 조인포인트만 제공
    (메서드 호출, 필드값 변경 등 해당)

(4) 포인트컷(Pointcut)

  • 조인포인트의 부분 집합으로 공통 기능이 적용될 대상을 선정하는 방법
  • 스프링 AOP의 조인포인트는 메서드 호출이므로 스프링에서 포인트컷은 메서드를 선정하는 것과 관련됨
  • 정규 표현식, AspectJ 문법(execute 명시자 표현식)으로 정의 가능
  • execute 표현식
    • @Pointcut("execution([접근제어자] 반환 타입 [패키지.클래스.] 메서드명 (파라미터 타입 | ..)") [throws 예외]
    • [] 생략가능
    • 패키지/클래스는 .으로 연결
    • * : 모든 값 표현
    • .. : 0 이상의 수 표현
    • @Pointcut("execution(public void com..calculate(..))")

(5) 위빙(Weaving)

  • 어드바이스를 핵심 기능 코드에 적용하는 것
  • 시점에 따라 컴파일 시, 클래스 로딩 시, 런타임 시 위빙으로 구분 가능, 대부분 런타임 시 위빙 사용
  • 스프링 AOP 또한 런타임 시 프록시 객체를 생성하여 공통 기능을 삽입

3) advice 사용

  • 구구단 Aspect 클래스
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class GugudanAspect {

//    포인트컷 적용
//    @Pointcut("execution(public void com..calculate(..))")
    @Pointcut("execution(* cal*(..))") // 적용 지점
    private void targetMethod() {}

//    어드바이스 정의
    @Around("targetMethod()") // 적용 시점(메서드 실행 전 후 or 예외 발생 시)
    public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {

//        핵심 기능 로직 실행 전 호출
        long start = System.nanoTime();

//        핵심 기능 호출
        try {
            Object result = joinPoint.proceed(); // 타깃 객체의 실제 메서드를 호출
            return result;
        }

//        핵심 기능 로직 실행 후 호출
        finally {
            long end = System.nanoTime();
            Signature signature = joinPoint.getSignature();
            System.out.printf("%s.%s 메서드 호출!\n", joinPoint.getTarget().getClass().getSimpleName(), signature.getName());
            System.out.printf("실행 시간 : %d ns\n", end - start);
        }
    }
}

  • config class
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy // @Aspect 어노테이션 붙인 클래스를 공통 기능으로 적용
public class GugudanConfig {

    @Bean
    public GugudanAspect gugudanAspect() {
        return new GugudanAspect();
    }

    @Bean
    public Gugudan gugudanByForLoop() {
        return new GugudanByForLoop();
    }

    @Bean
    public Gugudan gugudanByRecursion() {
        return new GugudanByRecursion();
    }
}

  • test class
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.context.ApplicationContext;

import java.lang.annotation.Annotation;

public class GugudanTest {
    public static void main(String[] args) {

        ApplicationContext applicationContext = new AnnotationConfigReactiveWebApplicationContext(GugudanConfig.class);

        Gugudan gugudan = applicationContext.getBean("gugudanByForLoop", Gugudan.class);
        gugudan.calculate(3, 1);

        System.out.println();

        Gugudan gugudan2 = applicationContext.getBean("gugudanByRecursion", Gugudan.class);
        gugudan2.calculate(3, 1);
    }
}

  • 출력
  • @Aspect
    • 클래스에 @Aspect 어노테이션을 붙여 구현
    • 공통 기능과 적용 시점을 @Around로 구현
    • 적용할 지점을 @Pointcut으로 구현
  • @Pointcut
    • 에스펙트를 적용할 위치를 지정
    • execution() 명시자를 사용하여 대상 메서드 지정
  • ProceedingJoingPoint interface
    • getsignature(), getTarget() 등 호출한 메서드의 시그니처와 대상 객체를 확인할 수 있는 메서드 제공
    • getArgs() 매개 변수 목록

4) 여러개의 advice 사용

  • cacheAspect class
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Aspect
public class GugudanCacheAspect {

//    임시 캐시 저장소
    List<Object> cache = new ArrayList<>();

//    포인트컷 적용
    @Pointcut("execution(* cal*(..))")
    public void cacheTarget() {}

//    어드바이스 정의
    @Around("cacheTarget()")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {

//        데이터 초기화
        Object[] argumentObject = joinPoint.getArgs();
        String argumentToString = Arrays.toString(argumentObject);

//        만약 데이터가 있다면, 캐시에서 꺼내서 전달
        if (cache.size() != 0) {
            for (Object element : cache) {

                String elementToString = Arrays.toString((Object[]) element);

                if (elementToString.equals(argumentToString)) {
                    System.out.printf("캐시에서 데이터 불러오기 [%s]\n", elementToString);
                    return elementToString;
                }
            }
        }

//        데이터가 없다면, 타깃 객체의 메서드를 호출하여 캐시에 데이터 추가
        Object result = joinPoint.proceed();
        cache.add(argumentObject);
        System.out.printf("캐시에 데이터 추가[%s]\n", Arrays.toString(argumentObject));

        return result;
    }
}

  • aspect class
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;

@Aspect
@Order(value = 1)
public class GugudanAspect {

//    포인트컷 적용
//    @Pointcut("execution(public void com..calculate(..))")
    @Pointcut("execution(* cal*(..))") // 적용 지점
    private void targetMethod() {}

//    어드바이스 정의
    @Around("targetMethod()") // 적용 시점(메서드 실행 전 후 or 예외 발생 시)
    public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {

//        핵심 기능 로직 실행 전 호출
        long start = System.nanoTime();

//        핵심 기능 호출
        try {
//            Object result = joinPoint.proceed(); // 타깃 객체의 실제 메서드를 호출
            return joinPoint.proceed();
        }

//        핵심 기능 로직 실행 후 호출
        finally {
            long end = System.nanoTime();
            Signature signature = joinPoint.getSignature();
            System.out.printf("%s.%s 메서드 호출!\n", joinPoint.getTarget().getClass().getSimpleName(), signature.getName());
            System.out.printf("실행 시간 : %d ns\n", end - start);
        }
    }
}

  • config class
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy // @Aspect 어노테이션 붙인 클래스를 공통 기능으로 적용
public class GugudanConfig {

    @Bean // 추가
    public GugudanCacheAspect cacheAspect() {
        return new GugudanCacheAspect();
    }

    @Bean
    public GugudanAspect gugudanAspect() {
        return new GugudanAspect();
    }

    @Bean
    public Gugudan gugudanByForLoop() {
        return new GugudanByForLoop();
    }

    @Bean
    public Gugudan gugudanByRecursion() {
        return new GugudanByRecursion();
    }
}

  • test class
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.context.ApplicationContext;


public class GugudanTest {
    public static void main(String[] args) {

        ApplicationContext applicationContext = new AnnotationConfigReactiveWebApplicationContext(GugudanConfig.class);

        Gugudan gugudan = applicationContext.getBean("gugudanByForLoop", Gugudan.class);
        gugudan.calculate(3, 1);
        gugudan.calculate(3, 1);
        gugudan.calculate(3, 1);

    }
}

  • 출력
  1. 최고 호출 시 데이터가 없기 때문에 joinPoint.proceed() 메서드 호출
  2. joinPoint.proceed()의 대상 객체가 GugudanAspect 프록시 객체이기 때문에 해당 객체의 measureTime() 메서드 실행
  3. measureTime() 메서드는 타깃 객체 메서드인 GugudanByForLoop의 calculate() 메서드 실행
  4. 실행 시간 측정 및 출력
  5. GugudanCacheAspect 클래스에서 반환된 값을 캐시에 추가하고 이를 확인하는 메세지 출력
  6. 두번째, 세번째 calculate() 메서드 호출 시 기존 저장된 데이터가 반환되어 GugudanAspect 클래스 미실행
  • GugudanCacheAspect 프록시 ⇒ GugudanAspect 프록시 ⇒ GugudanByRecursion 객체 순으로 어드바이스 적용

  • @Order(value = num)으로 애스펙트 적용 순서 지정 가능

    • 위 예시의 애스펙트 순서를 바꿨을 때 출력

5) AOP 구현 예시 및 로그 라이브러리(SLF4J) 사용법

  • 구현 예시 코드
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Aspect
@Slf4j
@Component
public class LoggingAspect {
    @Pointcut("execution(public void com..*(..))")
    private void before(){}

    @Before("before()")
    public void printMethod(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        Object[] parameters = joinPoint.getArgs();

        log.info("⭐ 메서드 호출 ⭐");
        log.info("===== 메서드 이름 = {} =====", signature.getName());
        if (parameters.length == 0)
            log.info("[파라미터] = 없음");
        else {
            String typeOfParameter = parameters.getClass().getSimpleName();
            String valueOfParameters = Arrays.toString(parameters);
            log.info("[파라미터 타입] = {}, [파라미터 값] = {}", typeOfParameter, valueOfParameters);
        }
    }
    @Pointcut("execution(public int com..*(..))")
    private void after(){}

    @AfterReturning(pointcut = "after()", returning = "returnValue")
    private void returnValue (JoinPoint joinPoint, Object returnValue) {

        log.info("⭐️ 메서드 호출️ 후 ⭐");
        Signature signature = joinPoint.getSignature();
        log.info("===== 메서드 이름 = {} =====", signature.getName());
        log.info("[반환값] = {}", returnValue);
    }
}

(1) Logging

  • 시스템에서 발생하는 이벤트, 상태, 오류 등의 정보를 제공하는 일련의 기록(log)를 생성하는 과정 또는 결과

  • 스트링 부트에 기본적으로 포함되는 라이브러리

    • SLF4J

      • 로그 선언 - private static final Logger log = LoggerFactory.getLogger(Xxx.class)

        • lombok 라이브러리 사용시 클래스 레벨에 @Slf4j 어노테이션을 사용하여 선언 없이 사용 가능
        			String testData = "exString";
        			log.info("example = {}", testData);
        
        			// 출력값
        			example = exString
    • Logback

(2) JoinPoint interface

  • AOP가 적용된 메서드 실행 시점에서의 정보를 제공
  • 실행 중인 메서드 이름, 인수, 타겟 객체 등
  • 모든 종류의 어드바이스에서 사용 가능

(3) ProceedingJoinPoint interface

  • JoinPoint를 확장한 인터페이스
  • @Around 어드바이스에서 사용
  • proceed() 메서드를 호출하여 타겟 메서드의 실행 시점을 제어 가능
```java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class GugudanAspect {

//    포인트컷 적용
//    @Pointcut("execution(public void com..calculate(..))")
    @Pointcut("execution(* cal*(..))") // 적용 지점
    private void targetMethod() {}

//    어드바이스 정의
    @Around("targetMethod()") // 적용 시점(메서드 실행 전 후 or 예외 발생 시)
    public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {

//        핵심 기능 로직 실행 전 호출
        long start = System.nanoTime();

//        핵심 기능 호출
        try {
            Object result = joinPoint.proceed(); // 타깃 객체의 실제 메서드를 호출
            return result;
        }

//        핵심 기능 로직 실행 후 호출
        finally {
            long end = System.nanoTime();
            Signature signature = joinPoint.getSignature();
            System.out.printf("%s.%s 메서드 호출!\n", joinPoint.getTarget().getClass().getSimpleName(), signature.getName());
            System.out.printf("실행 시간 : %d ns\n", end - start);
        }
    }
}

0개의 댓글