들어가며
프로젝트를 운영하다 보면 "이 API는 왜 이렇게 느릴까?"라는 의문이 들 때가 있다. 하나하나 메서드마다 시간 측정 코드를 넣자니 번거롭고, 그렇다고 방치하자니 불안하다. 이럴 때 Spring AOP를 사용하면 코드 수정 없이 모든 API의 성능을 측정할 수 있다.
이번 글에서는 Spring AOP가 무엇인지, 그리고 실제 프로젝트에 어떻게 적용하는지 정리해보았다.
AOP란?
**AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)**은 여러 곳에서 반복되는 공통 기능을 분리해서 관리하는 프로그래밍 기법이다.
예를 들어 로깅, 성능 측정, 트랜잭션 관리 같은 기능은 비즈니스 로직과는 별개지만 여러 곳에서 필요하다. 이런 걸 **횡단 관심사(Cross-Cutting Concerns)**라고 부른다.
AOP 없이 코드를 작성하면
@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를 사용하면
@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를 선택하는 표현식이다.
@Pointcut("execution(* org.example.myserver.controller..*.*(..))")
"org.example.myserver.controller 패키지 아래의 모든 메서드"라는 의미다.
4. Advice
실제로 실행되는 코드다. 언제 실행할지에 따라 종류가 나뉜다.
- @Before: 메서드 실행 전
- @After: 메서드 실행 후
- @Around: 메서드 실행 전후 (가장 강력함)
- @AfterReturning: 메서드가 정상 종료된 후
- @AfterThrowing: 메서드에서 예외가 발생한 후
실제 프로젝트에 적용하기
블로그 관리 API 프로젝트에 성능 측정 기능을 추가해보자.
1단계: 의존성 추가
build.gradle에 AOP 의존성을 추가한다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// AOP 추가
implementation 'org.springframework.boot:spring-boot-starter-aop'
// 나머지 의존성들...
}
2단계: Aspect 클래스 작성
모든 Controller 메서드의 실행 시간을 자동으로 측정하는 Aspect를 만든다.
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다. 성능 측정 코드가 전혀 없다.
@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. 어노테이션 정의
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 작성
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 어노테이션을 추가한다.
@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로 구현되어 있다.
@Transactional
public void transferMoney(Long fromId, Long toId, int amount) {
// 비즈니스 로직만 작성
accountService.withdraw(fromId, amount);
accountService.deposit(toId, amount);
// 트랜잭션 시작/커밋/롤백은 AOP가 처리
}
2. 로깅
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* org.example.myserver.service..*.*(..))")
public void logBefore(JoinPoint joinPoint) {
log.info("Method called: {}", joinPoint.getSignature().getName());
}
}
3. 보안 체크
@Aspect
@Component
public class SecurityAspect {
@Before("@annotation(org.springframework.security.access.prepost.PreAuthorize)")
public void checkSecurity(JoinPoint joinPoint) {
// 권한 체크 로직
}
}
4. 캐싱
@Aspect
@Component
public class CachingAspect {
@Around("@annotation(Cacheable)")
public Object cacheResult(ProceedingJoinPoint joinPoint) throws Throwable {
// 캐시 확인 → 있으면 반환, 없으면 메서드 실행 후 캐시 저장
}
}
주의사항
1. 성능 오버헤드
AOP는 프록시 방식으로 동작하기 때문에 약간의 성능 오버헤드가 있다. 하지만 대부분의 경우 무시할 수 있을 정도다.
2. 내부 메서드 호출
같은 클래스 내부에서 메서드를 호출하면 AOP가 적용되지 않는다.
@Service
public class BlogService {
public void method1() {
this.method2(); // AOP 적용 안 됨!
}
@LogExecutionTime
public void method2() {
// 로직
}
}
이 경우 method1에서 method2를 호출해도 @LogExecutionTime이 동작하지 않는다. 프록시를 거치지 않기 때문이다.
3. final 메서드
프록시는 메서드 오버라이드 방식으로 동작하기 때문에 final 메서드에는 AOP가 적용되지 않는다.
Pointcut 표현식 정리
자주 사용하는 Pointcut 표현식을 정리해보았다.
// 특정 패키지의 모든 메서드
@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에서 조절할 수 있다.
logging:
level:
org.example.myserver.aspect: DEBUG # 개발 환경
# org.example.myserver.aspect: WARN # 운영 환경
2. 조건부 활성화
프로파일별로 Aspect를 켜고 끌 수 있다.
@Profile("dev") // 개발 환경에서만 활성화
@Aspect
@Component
public class PerformanceAspect {
// ...
}
3. 파라미터 로깅
디버깅을 위해 파라미터도 함께 로깅할 수 있다.
@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 |
