개발일지/스프링

Spring에 AOP를 적용하고 Prometheus로 수집하기

티에리앙리 2025. 4. 16. 15:46

Spring AOP(Aspect-Oriented Programming)란?

Spring 프레임워크에서 제공하는 관점 지향 프로그래밍을 지원하는 기능이다. 이는 애플리케이션의 핵심 비즈니스 로직과 공통 관심사(예: 로깅, 트랜잭션 관리, 보안 등)를 분리하여 코드의 모듈성과 재사용성을 높이는 데 목적이 있다.

 

Spring에 AOP 적용하기
application.yml에 의존성 추가
Spring AOP (Aspect Oriented Programming) 기능을 쓰기 위해 필요한 기본 세팅을 한 번에 해주는 starter 의존성이다.
implementation 'org.springframework.boot:spring-boot-starter-aop'

 

 

prometheus 의존성 추가하기

micrometer라는 메트릭 수집 라이브러리에서 Prometheus용으로 메트릭을 export할 수 있게 해주는 의존성이다.

implementation 'io.micrometer:micrometer-registry-prometheus'

 

 

다음은 실제로 java/spring 코드에서 AOP를 어떻게 설정하는지 알아보자

 

MetricsConfig 클래스

@Configuration
@EnableAspectJAutoProxy // 해당 어노테이션을 통해 @Aspect 가 붙은 클래스가 동작할 수 있게 된다. (프록시 생성).
public class MetricsConfig {

    @Bean // couponMetricsAspect 라는 AOP 클래스를 빈으로 등록한다.
    public CouponMetricsAspect couponMetricsAspect(MeterRegistry registry) {
        return new CouponMetricsAspect(registry);
    }
}
  • AOP 환경을 활성화하고 CouponMetricsAspect를 Spring 빈으로 등록한다.
  • @EnableAspectJAutoProxy를 통해 Spring AOP를 활성화하여 @Aspect가 붙은 클래스가 동작할 수 있도록 설정한다.
  • MeterRegistry를 주입받아 CouponMetricsAspect를 초기화 한다.

CouponMetered 클래스

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CouponMetered {
    String version() default "v1";
}
  • version 속성을 통해 메서드의 버전을 지정(예: "v1", "v2").
  • @CouponMetered 어노테이션을 명시한 곳에서 원하는 버전을 명시한다.

 

CouponMetricsAspect 클래스

@Aspect
@Component
@RequiredArgsConstructor
public class CouponMetricsAspect {
    // MeterRegistry 는 Spring Boot Actuator + Micrometer + Prometheus 조합이 자동으로 제공하는 메트릭 수집 도구이다.
    private final MeterRegistry registry;

    // @CouponMetered 어노테이션이 붙은 메서드를 가로채서 실행 전후에 작업을 수행한다.
    @Around("@annotation(CouponMetered)")
    public Object measureCouponOperation(ProceedingJoinPoint joinPoint) throws Throwable {
        // 메트릭을 수집하는 작업을 진행한다.
        Timer.Sample sample = Timer.start();
        String version = extractVersion(joinPoint);
        String operation = extractOperation(joinPoint);

        try {
            Object result = joinPoint.proceed(); // 이 줄에서 실제로 @CouponMetered 가 붙은 메서드가 실행된다.

            // 쿠폰 발급 성공 메트릭
            // 성공 메트릭 카운터를 1 증가시킨다
            Counter.builder("coupon.operation.success")
                    .tag("version", version)
                    .tag("operation", operation)
                    .register(registry)
                    .increment();

            // 메서드의 실행 시간(duration) 을 기록한다.
            sample.stop(Timer.builder("coupon.operation.duration")
                    .tag("version", version)
                    .tag("operation", operation)
                    .register(registry));

            return result;
        } catch (Exception e) {
            // 쿠폰 발급 실패 메트릭
            Counter.builder("coupon.operation.failure")
                    .tag("version", version)
                    .tag("operation", operation)
                    .tag("error", e.getClass().getSimpleName())
                    .register(registry)
                    .increment();
            throw e;
        }
    }

    // 현재 실행 중인 메서드에서 @CouponMetered 어노테이션을 가져와서
    // 해당 어노테이션의 version 값을 반환한다.
    private String extractVersion(ProceedingJoinPoint joinPoint) {
        CouponMetered annotation = ((MethodSignature) joinPoint.getSignature())
                .getMethod()
                .getAnnotation(CouponMetered.class);
        return annotation.version();
    }

    // 현재 실행 중인 메서드의 이름(예: issueCoupon 등)을 문자열로 반환한다.
    private String extractOperation(ProceedingJoinPoint joinPoint) {
        return joinPoint.getSignature().getName();
    }
}
  • AOP Aspect 클래스로, @CouponMetered가 붙은 메서드의 실행을 가로채어 메트릭(성공/실패 횟수, 실행 시간)을 수집.
  • MeterRegistry를 사용하여 메트릭을 기록하고, version과 메서드 이름을 태그로 활용한다.
  • 참고로 MeterRegistry Spring Boot Actuator + Micrometer + Prometheus 조합이 자동으로 제공하는 메트릭 수집 도구이다.
  • @Around Advice를 통해 메서드 실행 전후로 로직을 삽입.

 

메서드에서 aop 사용

다음과 같이 @CouponMetered 메서드를 사용해서 version을 명시해 준다

@Transactional
@CouponMetered(version = "v1")
public Coupon issueCoupon(CouponDto.IssueRequest request) {
    CouponPolicy couponPolicy = couponPolicyRepository.findByIdWithLock(request.getCouponPolicyId())
            .orElseThrow(() -> new CouponIssueException("쿠폰 정책을 찾을 수 없습니다."));

    LocalDateTime now = LocalDateTime.now();
    if (now.isBefore(couponPolicy.getStartTime()) || now.isAfter(couponPolicy.getEndTime())) {
        throw new CouponIssueException("쿠폰 발급 기간이 아닙니다.");
    }

    long issuedCouponCount = couponRepository.countByCouponPolicyId(couponPolicy.getId());
    if (issuedCouponCount >= couponPolicy.getTotalQuantity()) {
        throw new CouponIssueException("쿠폰이 모두 소진되었습니다.");
    }

    Coupon coupon = Coupon.builder()
            .couponPolicy(couponPolicy)
            .userId(UserIdInterceptor.getCurrentUserId())
            .couponCode(generateCouponCode())
            .build();

    return couponRepository.save(coupon);
    // countByCouponPolicyId 에 락을 거는건 의미가 없고 coupon 자체가 save할 때까지 락이 걸리는게 중요하다
    // 물론 v1 에서는 findByIdWithLock을 제외하고는 락을 걸지 않는다.
}

 

 

또한 application.yml 에 다음과 같은 prometheus 관련 설정을 한다.

Spring ActuatorMicrometer를 사용해 애플리케이션의 모니터링 및 메트릭 수집 설정을 정의합니다. 이 설정은 애플리케이션의 상태(health), 메트릭(metrics), 그리고 Prometheus와의 통합을 관리하며 HTTP 요청 관련 메트릭의 분포를 세부적으로 기록하도록 구성한다.
management:
  server:
    port: 8080
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    tags:
      application: ${spring.application.name}
    distribution:
      percentiles-histogram:
        http.server.requests: true
      slo:
        http.server.requests: 50ms, 100ms, 200ms

 

현재는 docker-compose.yml 파일에

prometheus 관련 설정을 하고 수집하는 중이다.