스프링 프레임워크

spring aop와 annotation 맛보기

blogger903 2024. 9. 3. 09:33
728x90

"k8s 클러스터에 springboot application 배포후 모니터링" 연재를 다룰 예정인데, 그 과정에서 프로파일링 관련 기능이 필요할것 같아 이번에 포스팅하게 되었습니다. 이번 포스팅에서는 spring-aop와 애노테이션을 사용할것인데, 그전에 spring-aop와 annotation 관련 내용을 포스팅하게되었습니다.

간단히 메서드에 실행시간 출력하는 애노테이션을 추가했고, spring-aop를 통해 출력해보도록 하겠습니다.

이번 포스팅은 상황을 가정하고 그 상황에 필요한 기술을 맛보기로 다룹니다. 부하테스트로 timeout이 발생하는 이슈를 파악하고 로컬환경에서 api 요청에 따른 로직의 성능을 파악하기 위해 jaeger를 사용해서 프로파일링하는 것을 목표로 합니다. 

 

다루는 내용

  • spring-aop
  • java annotation
  • java reflection

 

환경

  • java11
  • springboot2.5.5

사전지식

  • java reflection
  • java annotation
  • spring-aop

 

build.gradle

plugins {
	id 'org.springframework.boot' version '2.5.5'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-aop'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	//테스트에서 lombok 사용
	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
}

test {
	useJUnitPlatform()
}


spring-aop

@Service 애노테이션을 붙인 Service 클래스 MyService를 하나 만들겁니다. 그리고 MyService에는 add, multiply 메서드를 만듭니다. 그리고 이 메서드에 LogExecutionTime이라는 이름의 애노테이션을 달겁니다. 그리고 이 메서드를 호출하게 되면 출력시간이 출력되는 예제를 만들어보겠습니다

 

MyService를 만듭니다

import org.springframework.stereotype.Service;

@Service
public class MyService {

    @LogExecutionTime("Adding numbers")
    public int add(int a, int b) throws InterruptedException {
        Thread.sleep(1000); // Simulate some work
        return a + b;
    }

    @LogExecutionTime("Multiplying numbers")
    public int multiply(int a, int b) throws InterruptedException {
        Thread.sleep(1500); // Simulate some work
        return a * b;
    }
}

 

LogExecutionTime이란 애노테이션을 만듭니다

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface LogExecutionTime {
    String value() default "";
}

 

스프링 AOP를 구현하기 위해 @Aspect를 사용합니다

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

@Aspect
@Component
public class LoggingAspect {
    @Around("@annotation(logExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint, LogExecutionTime logExecutionTime) throws Throwable {
        long start = System.currentTimeMillis();
        Object proceed = joinPoint.proceed();
        long executionTime = System.currentTimeMillis() - start;
        System.out.println(logExecutionTime.value() + " executed in " + executionTime + "ms");
        return proceed;
    }
}

 

여기서 개념을 간략하게 설명하겠습니다

포인트컷: 포인트컷은 "어디에" 어드바이스를 적용할지를 정의합니다.

어드바이스: 어드바이스는 "무엇을" 할지, 즉 실제 실행될 로직을 정의합니다.
어드바이저: 하나의 포인트컷 + 하나의 어드바이스를 모듈화 한것, 스프링 AOP에서만 사용되는 특별한 용어

Aspect: 여러 어드바이스, 포인트컷을 모아놓은 더 큰 모듈 

@Aspect: 이 Component스캔이 이 애노테이션이 붙은 클래스를 발견하면, 프록시를 생성합니다

이 프록시는 타겟 객체를 감싸고, 정의된 포인트컷에 따라 어드바이스를 실행합니다.

 

그래서 위 예시코드에서 포인트컷은 "어디에"

@Around("@annotation(logExecutionTime)")

 

 

그리고 어드바이스는 "무엇을"

public Object logExecutionTime(ProceedingJoinPoint joinPoint, LogExecutionTime logExecutionTime) throws Throwable {
    long start = System.currentTimeMillis();
    Object proceed = joinPoint.proceed();
    long executionTime = System.currentTimeMillis() - start;
    System.out.println(logExecutionTime.value() + " executed in " + executionTime + "ms");
    return proceed;
}

 

그리고 이러한 어드바이저를 스프링 프레임워크에서 스프링 컨테이너 로딩 시점에 프록시 생성하는 것을 위빙이라고 합니다

 

스프링부트 main 함수

import hello.proxy.aop.annotation.MyService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication()
public class ProxyApplication {

    public static void main(String[] args) throws InterruptedException {

       ApplicationContext context = SpringApplication.run(ProxyApplication.class, args);

       MyService service = context.getBean(MyService.class);

       System.out.println("Result of add: " + service.add(5, 3));
       System.out.println("Result of multiply: " + service.multiply(5, 3));
    }

}

 

출력결과

2024-09-03 11:52:15.690  INFO 46896 --- [           main] hello.proxy.ProxyApplication             : Starting ProxyApplication using Java 11.0.13 on BE-YOBS.local with PID 46896 (/Users/nohys/Desktop/study/springboot/spring-core/proxy-start/build/classes/java/main started by nohys in /Users/nohys/Desktop/study/springboot/spring-core/proxy-start)
2024-09-03 11:52:15.691  INFO 46896 --- [           main] hello.proxy.ProxyApplication             : No active profile set, falling back to default profiles: default
2024-09-03 11:52:15.964  INFO 46896 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2024-09-03 11:52:15.967  INFO 46896 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2024-09-03 11:52:15.967  INFO 46896 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.53]
2024-09-03 11:52:15.984  INFO 46896 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2024-09-03 11:52:15.984  INFO 46896 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 275 ms
2024-09-03 11:52:16.101  INFO 46896 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2024-09-03 11:52:16.106  INFO 46896 --- [           main] hello.proxy.ProxyApplication             : Started ProxyApplication in 0.517 seconds (JVM running for 0.724)
Adding numbers executed in 1005ms
Result of add: 8
Multiplying numbers executed in 1501ms
Result of multiply: 15

 

이렇게 Aspect를 사용한 프로그래밍 방식으로 AOP라고 합니다

횡단 관심사를 깔끔하게 처리하기 위한 개발 방법론이며 이러한 기능때문에 스프링 프레임워크는 엔터프라이즈급 서비스를 구현하는데 특화된것 같습니다.