
Spring AOP는 스프링 프레임워크에서 제공하는 기능으로 관점 지향 프로그래밍을 의미한다. 이는 로깅, 보안, 트랜잭션 등과 같은 공통 관심사를 모듈화 하여 코드 중복을 줄이고 유지/보수성을 향상하는데 도움이 된다.
객체 지향 프로그래밍 체계를 보완하는 기술로 각각의 기능을 핵심 관심사와 공통 관심사로 나누어 각각 모듈화 하는 것을 말한다. 핵심 관심사는 우리가 적용하고자 하는 핵심 비즈니스 로직이다. 공통 관심사는 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.
여러개의 클래스에서 반복해서 사용하는 코드가 있으면, 해당 코드를 모듈화 하여 공통 관심사로 분리한다. 이렇게 분리한 공통 관심사를 Aspect로 정의하고 이것을 적용할 메서드나 클래스에 Advice를 적용하여 분리한다. 위의 방법으로 핵심적인 비즈니스 로직에서 분리하여 재사용성을 높이겠다는 것이 AOP의 취지이다.
- 핵심 관심사 : 각 객체가 가져야 할 본래의 기능
- 공통 관심사 : 여러 객체에서 공통적으로 사용되는 코드

| 용어 | 설명 |
|---|---|
| Aspect | 공통 관심사를 모듈화 한 것이다. |
| Target | Aspect가 적용될 대상을 의미하며 메서드, 클래스 등이 해당된다. |
| Join Point | Apsect가 적용될 수 있는 시점을 의미하며 기능의 실행 전, 후 등이 될 수 있다. |
| Advice | Aspect의 기능을 정의한 것으로 메서드의 실행 전/후, 예외 처리 발생 시 실행되는 코드를 의미한다. |
| Point Cut | Advice를 적용할 메서드의 범위를 지정하는 것을 의미한다. |
| 어드바이스 | 설명 |
|---|---|
| @Aspect | 해당 클래스를 Aspect로 사용하겠다는 것을 명시한다. |
| @Before | 대상 메서드가 실행되기 전에 Advice를 실행한다. |
| @After | 대상 메서드가 실행된 후에 Advice를 실행한다. |
| @AfterReturning | 대상 메서드가 정상적으로 실행되고 반환된 후에 Advice를 실행한다. |
| @AfterThrowing | 대상 메서드에서 예외가 발생했을 때 Advice를 실행한다. |
| @Around | 대상 메서드 실행 전/후 또는 예외 발생 시에 Advice를 실행한다. |
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
}

Spring AOP의 사용을 명시하는 @Aspect 어노테이션과 Spring Bean으로 등록해주는 @Component 어노테이션을 사용한다.
Spring Bean에만 AOP를 적용 가능하기 때문에 Bean 등록이 필수적이다.

@Slf4j(topic = "UseTimeAop")
@Aspect
@Component
@RequiredArgsConstructor
public class UseTimeAop {
private final ApiUseTimeRepository apiUseTimeRepository;
@Pointcut("execution(* ~~~.ProductController.*(..))")
private void product() {
}
@Pointcut("execution(* ~~~.FolderController.*(..))")
private void folder() {
}
@Pointcut("execution(* ~~~.NaverApiController.*(..))")
private void naver() {
}
@Around("product() || folder() || naver()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
// 측정 시작 시간
long startTime = System.currentTimeMillis();
try {
// 핵심기능 수행
Object output = joinPoint.proceed();
return output;
} finally {
// 측정 종료 시간
long endTime = System.currentTimeMillis();
// 수행시간 = 종료 시간 - 시작 시간
long runTime = endTime - startTime;
// 로그인 회원이 없는 경우, 수행시간 기록하지 않음
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal().getClass() == UserDetailsImpl.class) {
// 로그인 회원 정보
UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
User loginUser = userDetails.getUser();
// API 사용시간 및 DB 에 기록
ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser).orElse(null);
if (apiUseTime == null) {
// 로그인 회원의 기록이 없으면
apiUseTime = new ApiUseTime(loginUser, runTime);
} else {
// 로그인 회원의 기록이 이미 있으면
apiUseTime.addUseTime(runTime);
}
log.info("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: "
+ apiUseTime.getTotalTime() + " ms");
apiUseTimeRepository.save(apiUseTime);
}
}
}
}
개념적 동작 원리는 앞서 보았던 위의 이미지와 같이 동작한다. 하지만 스프링 실제 동작은 중간에 프록시(가짜 혹은 대리) 객체를 삽입한 후에 핵심 기능이 작동하는 과정을 거치게 된다. DispatcherServlet과 ProductController 입장에서는 변화가 없다. 호출되는 함수의 I/O가 완전히 동일하고, "joinPoint.proceed()"에 의해서 원래 호출하려고 했던 함수(createProduct(requestDto))가 전달된다.

'ProdceedingJoinPoint'는 Spring AOP에서 사용되는 인터페이스이다. 이 인터페이스는 Advice에 전달되는 파라미터로, Advice가 조인 포인트를 실행하고 제어하는데 사용된다. 'JoinPoint' 인터페이스도 있는데 이 것이 더 넓은 범위의 인터페이스이고 'ProdceedingJoinPoint'는 JoinPoint를 상속하기 때문에 주로 Around에서만 사용한다.
https://adjh54.tistory.com/133
https://engkimbs.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81AOP