Spring AOP로 API 성능 측정하기 - 프로젝트 예제와 함께

2026. 1. 21. 14:49·Stack/Spring

들어가며

프로젝트를 운영하다 보면 "이 API는 왜 이렇게 느릴까?"라는 의문이 들 때가 있다. 하나하나 메서드마다 시간 측정 코드를 넣자니 번거롭고, 그렇다고 방치하자니 불안하다. 이럴 때 Spring AOP를 사용하면 코드 수정 없이 모든 API의 성능을 측정할 수 있다.

이번 글에서는 Spring AOP가 무엇인지, 그리고 실제 프로젝트에 어떻게 적용하는지 정리해보았다.


AOP란?

**AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)**은 여러 곳에서 반복되는 공통 기능을 분리해서 관리하는 프로그래밍 기법이다.

예를 들어 로깅, 성능 측정, 트랜잭션 관리 같은 기능은 비즈니스 로직과는 별개지만 여러 곳에서 필요하다. 이런 걸 **횡단 관심사(Cross-Cutting Concerns)**라고 부른다.

AOP 없이 코드를 작성하면

 
 
java
@PostMapping
public ResponseEntity<?> createBlog(@RequestBody BlogCreateDto dto) {
    long startTime = System.currentTimeMillis(); // 시간 측정 시작
    
    try {
        // 비즈니스 로직
        Blog blog = blogService.create(dto);
        return ResponseEntity.ok(blog);
    } finally {
        long endTime = System.currentTimeMillis(); // 시간 측정 종료
        log.info("Execution time: {} ms", endTime - startTime);
    }
}

@GetMapping
public ResponseEntity<?> getBlogList() {
    long startTime = System.currentTimeMillis(); // 또 반복...
    
    try {
        List<Blog> blogs = blogService.getList();
        return ResponseEntity.ok(blogs);
    } finally {
        long endTime = System.currentTimeMillis();
        log.info("Execution time: {} ms", endTime - startTime);
    }
}

모든 메서드마다 시간 측정 코드가 반복된다. 코드가 지저분해지고 유지보수가 어려워진다.

AOP를 사용하면

 
 
java
@PostMapping
public ResponseEntity<?> createBlog(@RequestBody BlogCreateDto dto) {
    // 비즈니스 로직만 작성
    Blog blog = blogService.create(dto);
    return ResponseEntity.ok(blog);
}

@GetMapping
public ResponseEntity<?> getBlogList() {
    // 비즈니스 로직만 작성
    List<Blog> blogs = blogService.getList();
    return ResponseEntity.ok(blogs);
}

시간 측정 코드가 사라졌다. AOP가 알아서 처리해준다.


AOP 주요 개념

1. Aspect

공통 기능을 모아놓은 모듈이다. 로깅 Aspect, 성능 측정 Aspect처럼 역할별로 만든다.

2. Join Point

Aspect를 적용할 수 있는 지점이다. 메서드 실행, 객체 생성 등이 Join Point가 될 수 있다.

3. Pointcut

실제로 Aspect를 적용할 Join Point를 선택하는 표현식이다.

 
 
java
@Pointcut("execution(* org.example.myserver.controller..*.*(..))")

"org.example.myserver.controller 패키지 아래의 모든 메서드"라는 의미다.

4. Advice

실제로 실행되는 코드다. 언제 실행할지에 따라 종류가 나뉜다.

  • @Before: 메서드 실행 전
  • @After: 메서드 실행 후
  • @Around: 메서드 실행 전후 (가장 강력함)
  • @AfterReturning: 메서드가 정상 종료된 후
  • @AfterThrowing: 메서드에서 예외가 발생한 후

실제 프로젝트에 적용하기

블로그 관리 API 프로젝트에 성능 측정 기능을 추가해보자.

1단계: 의존성 추가

build.gradle에 AOP 의존성을 추가한다.

 
 
gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    
    // AOP 추가
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    
    // 나머지 의존성들...
}

2단계: Aspect 클래스 작성

모든 Controller 메서드의 실행 시간을 자동으로 측정하는 Aspect를 만든다.

 
 
java
package org.example.myserver.aspect;

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;

@Slf4j
@Aspect
@Component
public class PerformanceAspect {

    // Controller 패키지의 모든 메서드에 적용
    @Pointcut("execution(* org.example.myserver.controller..*.*(..))")
    public void controllerMethods() {}

    @Around("controllerMethods()")
    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        
        // 클래스명과 메서드명 추출
        String className = joinPoint.getSignature().getDeclaringType().getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        
        try {
            stopWatch.start();
            
            // 실제 메서드 실행
            Object result = joinPoint.proceed();
            
            return result;
        } finally {
            stopWatch.stop();
            
            // 실행 시간 로깅
            log.info("[Performance] {}.{} - Execution time: {} ms", 
                className, 
                methodName, 
                stopWatch.getTotalTimeMillis());
        }
    }
}

코드 설명

@Aspect: 이 클래스가 Aspect임을 표시한다.

@Component: 스프링 빈으로 등록한다.

@Pointcut: 어떤 메서드에 적용할지 정의한다.

  • execution(* org.example.myserver.controller..*.*(..))
  • 첫 번째 *: 모든 반환 타입
  • org.example.myserver.controller..*: controller 패키지와 하위 패키지
  • .*(..): 모든 메서드명, 모든 파라미터

@Around: 메서드 실행 전후에 코드를 실행한다.

ProceedingJoinPoint: 실제 메서드 정보와 실행을 제어할 수 있다.

  • joinPoint.proceed(): 실제 메서드 실행
  • joinPoint.getSignature(): 메서드 정보 가져오기

StopWatch: 스프링이 제공하는 시간 측정 유틸리티다.


기존 Controller 코드

아래는 실제 프로젝트의 BlogController다. 성능 측정 코드가 전혀 없다.

 
 
java
@Tag(name = "블로그 관리 api")
@RequiredArgsConstructor
@RequestMapping("/blog")
@RestController
public class BlogController {

    private final BlogFacade blogFacade;
    private final UserInfoInJwt userInfoInJwt;

    @Operation(summary = "블로그 생성")
    @PostMapping("")
    public ApiResponse<String> createBlog(
            @RequestBody BlogCreateDto blogCreateDto, 
            HttpServletRequest httpServletRequest) {
        userInfoInJwt.getUserInfo_InJwt(httpServletRequest.getHeader("Authorization"));
        Long userId = userInfoInJwt.getUserPk();
        return ApiResponse.successPOST(blogFacade.createBlog(blogCreateDto, userId));
    }

    @Operation(summary = "사용자 블로그 목록 조회")
    @GetMapping("")
    public ApiResponse<List<BlogListResponseDto>> readBlogList(
            HttpServletRequest httpServletRequest) {
        userInfoInJwt.getUserInfo_InJwt(httpServletRequest.getHeader("Authorization"));
        Long userId = userInfoInJwt.getUserPk();
        List<BlogListResponseDto> blogListResponseDtos = blogFacade.readBlogList(userId);
        return ApiResponse.successGET(blogListResponseDtos, blogListResponseDtos.size());
    }

    @Operation(summary = "블로그 삭제")
    @DeleteMapping("/{blogId}")
    public ApiResponse<String> deleteBlog(
            HttpServletRequest httpServletRequest, 
            @PathVariable Long blogId) {
        userInfoInJwt.getUserInfo_InJwt(httpServletRequest.getHeader("Authorization"));
        Long userId = userInfoInJwt.getUserPk();
        return ApiResponse.successPOST(blogFacade.deleteBlog(userId, blogId));
    }

    @Operation(summary = "전체 블로그 목록 조회")
    @GetMapping("/entire")
    public ApiResponse<List<BlogEntireResponseDto>> readEntireBlogList(
            @RequestParam(required = false) String searchValue,
            @RequestParam(defaultValue = "id") String sortBy,
            @RequestParam(defaultValue = "DESC") String sortDirection,
            @RequestParam(defaultValue = "0") int pageNum,
            @RequestParam(defaultValue = "50") int pageSize) {
        
        BlogEntireListRequestDto requestDto = 
            new BlogEntireListRequestDto(searchValue, sortBy, sortDirection, pageNum, pageSize);
        BlogEntireListResponseDto responseDto = blogFacade.readEntireBlogList(requestDto);
        
        return ApiResponse.successGET(
            responseDto.blogEntireResponseDtos(), 
            responseDto.totalBlogCount()
        );
    }
}
```

코드를 보면 성능 측정과 관련된 코드가 하나도 없다. 순수하게 비즈니스 로직만 작성되어 있다.

---

## 실행 결과

애플리케이션을 실행하고 API를 호출하면 다음과 같은 로그가 출력된다.
```
[Performance] BlogController.createBlog - Execution time: 156 ms
[Performance] BlogController.readBlogList - Execution time: 23 ms
[Performance] BlogController.deleteBlog - Execution time: 45 ms
[Performance] BlogController.readEntireBlogList - Execution time: 189 ms

각 엔드포인트가 얼마나 걸렸는지 자동으로 측정되어 로깅된다. Controller 코드는 전혀 수정하지 않았는데도 말이다.


선택적으로 적용하기

모든 메서드가 아니라 특정 메서드만 측정하고 싶을 때는 커스텀 어노테이션을 만들 수 있다.

1. 어노테이션 정의

 
 
java
package org.example.myserver.aspect.annotation;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}

2. Aspect 작성

 
 
java
package org.example.myserver.aspect;

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

@Slf4j
@Aspect
@Component
public class LogExecutionTimeAspect {

    @Around("@annotation(org.example.myserver.aspect.annotation.LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        
        String className = joinPoint.getSignature().getDeclaringType().getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        
        try {
            stopWatch.start();
            Object result = joinPoint.proceed();
            return result;
        } finally {
            stopWatch.stop();
            log.info("[@LogExecutionTime] {}.{} - Execution time: {} ms", 
                className, 
                methodName, 
                stopWatch.getTotalTimeMillis());
        }
    }
}

3. 사용 방법

측정하고 싶은 메서드에만 @LogExecutionTime 어노테이션을 추가한다.

 
 
java
@Service
@RequiredArgsConstructor
public class BlogService {

    @LogExecutionTime  // 이 메서드만 측정
    public List<Blog> complexSearch(SearchCondition condition) {
        // 복잡한 검색 로직
        return blogRepository.findByComplexCondition(condition);
    }
    
    public Blog findById(Long id) {
        // 이 메서드는 측정 안 됨
        return blogRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("블로그 없음"));
    }
}
```

실행 결과:
```
[@LogExecutionTime] BlogService.complexSearch - Execution time: 342 ms

Service 계층에도 적용할 수 있고, 원하는 메서드만 선택적으로 측정할 수 있다.


AOP 활용 사례

성능 측정 외에도 AOP는 다양하게 활용된다.

1. 트랜잭션 관리

스프링의 @Transactional도 AOP로 구현되어 있다.

 
 
java
@Transactional
public void transferMoney(Long fromId, Long toId, int amount) {
    // 비즈니스 로직만 작성
    accountService.withdraw(fromId, amount);
    accountService.deposit(toId, amount);
    // 트랜잭션 시작/커밋/롤백은 AOP가 처리
}

2. 로깅

 
 
java
@Aspect
@Component
public class LoggingAspect {
    
    @Before("execution(* org.example.myserver.service..*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        log.info("Method called: {}", joinPoint.getSignature().getName());
    }
}

3. 보안 체크

 
 
java
@Aspect
@Component
public class SecurityAspect {
    
    @Before("@annotation(org.springframework.security.access.prepost.PreAuthorize)")
    public void checkSecurity(JoinPoint joinPoint) {
        // 권한 체크 로직
    }
}

4. 캐싱

 
 
java
@Aspect
@Component
public class CachingAspect {
    
    @Around("@annotation(Cacheable)")
    public Object cacheResult(ProceedingJoinPoint joinPoint) throws Throwable {
        // 캐시 확인 → 있으면 반환, 없으면 메서드 실행 후 캐시 저장
    }
}

주의사항

1. 성능 오버헤드

AOP는 프록시 방식으로 동작하기 때문에 약간의 성능 오버헤드가 있다. 하지만 대부분의 경우 무시할 수 있을 정도다.

2. 내부 메서드 호출

같은 클래스 내부에서 메서드를 호출하면 AOP가 적용되지 않는다.

 
 
java
@Service
public class BlogService {
    
    public void method1() {
        this.method2();  // AOP 적용 안 됨!
    }
    
    @LogExecutionTime
    public void method2() {
        // 로직
    }
}

이 경우 method1에서 method2를 호출해도 @LogExecutionTime이 동작하지 않는다. 프록시를 거치지 않기 때문이다.

3. final 메서드

프록시는 메서드 오버라이드 방식으로 동작하기 때문에 final 메서드에는 AOP가 적용되지 않는다.


Pointcut 표현식 정리

자주 사용하는 Pointcut 표현식을 정리해보았다.

 
 
java
// 특정 패키지의 모든 메서드
@Pointcut("execution(* com.example.service..*.*(..))")

// 특정 클래스의 모든 메서드
@Pointcut("execution(* com.example.service.UserService.*(..))")

// 특정 메서드명
@Pointcut("execution(* com.example.service..*.*User(..))")

// 특정 어노테이션이 붙은 메서드
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")

// 특정 어노테이션이 붙은 클래스의 모든 메서드
@Pointcut("@within(org.springframework.stereotype.Service)")

// 여러 조건 조합 (AND)
@Pointcut("execution(* com.example.service..*.*(..)) && @annotation(Transactional)")

// 여러 조건 조합 (OR)
@Pointcut("execution(* com.example.service..*.*(..)) || execution(* com.example.controller..*.*(..))")

실무 팁

1. 로그 레벨 설정

운영 환경에서는 INFO 레벨로 너무 많은 로그가 쌓일 수 있다. application.yaml에서 조절할 수 있다.

 
 
yaml
logging:
  level:
    org.example.myserver.aspect: DEBUG  # 개발 환경
    # org.example.myserver.aspect: WARN  # 운영 환경

2. 조건부 활성화

프로파일별로 Aspect를 켜고 끌 수 있다.

 
 
java
@Profile("dev")  // 개발 환경에서만 활성화
@Aspect
@Component
public class PerformanceAspect {
    // ...
}

3. 파라미터 로깅

디버깅을 위해 파라미터도 함께 로깅할 수 있다.

 
 
java
@Around("controllerMethods()")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    Object[] args = joinPoint.getArgs();
    log.info("Parameters: {}", Arrays.toString(args));
    
    // 시간 측정 로직
}

단, 민감한 정보(비밀번호 등)가 로깅되지 않도록 주의해야 한다.


마치며

AOP는 횡단 관심사를 깔끔하게 분리할 수 있는 강력한 도구다. 성능 측정, 로깅, 보안 체크 같은 공통 기능을 비즈니스 로직과 분리하면 코드가 훨씬 깔끔해진다.

실제로 프로젝트에 적용해보니 다음과 같은 장점이 있었다.

1. 코드 중복 제거: 모든 Controller에 반복되던 시간 측정 코드가 사라졌다.

2. 유지보수 편리: 측정 로직을 수정할 때 Aspect 클래스 하나만 수정하면 된다.

3. 비즈니스 로직 집중: Controller와 Service는 순수하게 비즈니스 로직만 담당한다.

4. 유연성: 언제든지 측정 대상을 추가하거나 제거할 수 있다.

처음에는 프록시 개념이 낯설고 어려울 수 있지만, 한 번 이해하고 나면 정말 유용하게 쓸 수 있다. 특히 성능 모니터링이나 로깅처럼 모든 메서드에 필요한 기능은 AOP로 구현하는 게 거의 필수다.

여러분의 프로젝트에도 AOP를 적용해서 더 깔끔한 코드를 작성해보시길 바란다.

저작자표시 (새창열림)

'Stack > Spring' 카테고리의 다른 글

스프링부트 DB 작동 방식과 원리  (1) 2026.01.21
N+1 문제 해결로 쿼리 성능 개선하기  (0) 2022.08.11
JPA 특정 엔티티 삭제시 연관된 엔티티도 함께 삭제하기  (0) 2022.07.12
(CI/CD) Codedeploy 배포 GitAction 에 GitIgnore 파일 적용하는법  (0) 2022.07.12
SQL 관계 데이터 베이스 삭제 시 참조키 관련 주의 사항  (0) 2022.07.12
'Stack/Spring' 카테고리의 다른 글
  • 스프링부트 DB 작동 방식과 원리
  • N+1 문제 해결로 쿼리 성능 개선하기
  • JPA 특정 엔티티 삭제시 연관된 엔티티도 함께 삭제하기
  • (CI/CD) Codedeploy 배포 GitAction 에 GitIgnore 파일 적용하는법
김코딩개발자
김코딩개발자
  • 김코딩개발자
    김코딩의 개발로그
    김코딩개발자
  • 전체
    오늘
    어제
    • 분류 전체보기 (61)
      • 개발이야기 (15)
        • 개발로그 (4)
        • 항해일지 (11)
      • Develop (0)
      • Life (0)
      • Stack (28)
        • C++ (6)
        • Ext.js (1)
        • Spring (18)
        • Java (2)
        • JavaScript (1)
      • TechTrend (0)
      • TechKnowledge (18)
        • CS관련지식 (7)
        • 알고리즘 (9)
        • 네트워크 (2)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    오버로딩
    서비스경험
    피보나치수열
    Spring AOP
    aop
    lan 통신
    데이터 마이그레이션
    네트워크
    DB원리
    ip통신
    자바스크립트입문
    개발일기
    java Stack
    osi 2계층
    동적계획법
    Connection Pool
    시간복잡도
    올바른 괄호
    프로그래머스 멀리뛰기
    JPA
    서비스 경험
    프로그래머스
    직장인
    괄호문제
    프로그래머스 LV2
    SpringBoot DB
    관점지향프로그래밍
    OSI 3계층
    개발입문
    괄호 회전하기
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
김코딩개발자
Spring AOP로 API 성능 측정하기 - 프로젝트 예제와 함께
상단으로

티스토리툴바