개발일지/스프링
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 Actuator와 Micrometer를 사용해 애플리케이션의 모니터링 및 메트릭 수집 설정을 정의합니다. 이 설정은 애플리케이션의 상태(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 관련 설정을 하고 수집하는 중이다.