Aspect-Oriented-Programing(AOP)는 프로그램의 관심가를 분리하여 코드의 모듈성을 높이고, 유지보수를 용이하게 하는 프로그래밍 패러다임이다. 주로 로그, 보안, 트렌잭션 관리 등 공통적인 관심사(cross-cutting concerns)를 처리할 때 사용된다.
AOP의 주요 개념
주요 개념
Aspect:
- Aspect는 특정 관심사 또는 기능을 모듈화한 것이다. 예를 들어, 로그 기록, 보안 검사, 트랜잭션 관리 등이 Aspect로 정의될 수 있다.
- Aspect는 일반적으로 @Aspect 어노테이션을 통해 정의된다.
Join Point:
- Join Point는 Aspect가 적용될 수 있는 프로그램의 특정 지점을 말한다.
- 예를 들어, 메소드 호출, 메소드 실행 후, 객체 생성 시점 등이 Join Point가 될 수 있다.
Advice:
- Advice는 Join Point에서 실행되는 코드로, Aspect에서 정의합니다. Advice는 다음과 같은 종류가 있다:
Before Advice: Join Point 전에 실행된다. 주로 메소드 호출 전에 수행할 작업을 정의할 때 사용한다.
After Advice: Join Point 후에 실행된다. 메소드 호출이 완료된 후에 수행할 작업을 정의한다.
Around Advice: Join Point 전후 모두 실행된다. 메소드 실행을 가로채고, 메소드 실행을 결정할 수 있다.
After Returning Advice: Join Point가 성공적으로 실행된 후에 실행된다. 메소드 호출이 정상적으로 완료된 후 작업을 정의한다.
After Throwing Advice: Join Point에서 예외가 발생했을 때 실행된다. 예외 처리를 위한 작업을 정의한다.
Pointcut:
- Pointcut은 Advice가 적용될 Join Point를 정의하는 표현식입니다. 특정 메소드나 클래스, 또는 메소드의 속성에 따라 Advice를 적용할 수 있다.
- 예를 들어, execution(* com.example.service.*.*(..))는 com.example.service 패키지의 모든 메소드에 대해 Advice를 적용하겠다는 의미한다.
Weaving:
- Weaving은 Aspect를 애플리케이션 코드에 적용하는 과정이다. AOP에서는 다음과 같은 Weaving 시점이 있습니다:
Compile-time Weaving: 컴파일 시점에 Aspect를 코드에 적용한다. AspectJ와 같은 도구를 사용한다.
Load-time Weaving: 클래스 로딩 시점에 Aspect를 적용한다. JVM이 클래스를 로딩할 때 Aspect를 적용한다.
Runtime Weaving: 애플리케이션 실행 중에 Aspect를 적용한다. Spring AOP에서 주로 사용된다
Spring AOP 예제
Spring AOP를 사용하여 로그를 기록하는 Aspect를 정의하는 방법을 예제로 설명을 해보겠다.
Aspect 정의예
다음은 Spring AOP를 사용한 간단한 예제이다. 이 예제는 LoggingAspect라는 Aspect를 정의하고, 메소드 호출 전후에 로그를 기록하는 Advice를 설정한다.
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature());
}
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After method: " + joinPoint.getSignature());
}
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Around method: " + joinPoint.getSignature() + " - Before");
Object result = joinPoint.proceed();
System.out.println("Around method: " + joinPoint.getSignature() + " - After");
return result;
}
}
Spring 설정:
Java Config 방식
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
@Bean
public LoggingAspect loggingAspect() {
return new LoggingAspect();
}
}
이렇게 하면 com.example.service 패키지의 모든 메소드에 대해 LoggingAspect가 적용되어, 메소드 호출 전후와 실행 시점에 로그가 기록된다.
한 그림으로 정리하자면...
어드바이스: 반복되는 횡단관심사를 정의해 놓은곳.
포인트컷: 그 횡단관심사를 어느 범위에 적용할지 선택하는 것이 포인트컷.
타켓: 그 범위 안에서 선택받은 객체들 혹은 객체가 타켓이 되는것.
조인포인트: 그 타겟 내에서 어드바이스 실제로 실행되는 시점.
코드로 알아보는 AOP
package org.example.expert.aop;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
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.example.expert.domain.user.entity.ApiUseTime;
import org.example.expert.domain.user.entity.User;
import org.example.expert.domain.user.repository.ApiUseTimeRepository;
import org.example.expert.domain.user.repository.UserRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.LocalDateTime;
@Slf4j(topic = "AdminAop")
@Aspect
@Component
@RequiredArgsConstructor
public class AdminAop {
private final ApiUseTimeRepository apiUseTimeRepository;
private final UserRepository userRepository;
@Pointcut("execution(* org.example.expert.domain.comment.commentcontroller.CommentAdminController.deleteComment(..))")
private void commentDelete() {
}
@Pointcut("execution(* org.example.expert.domain.user.service.UserAdminService.changeUserRole(..))")
private void userRoleChange() {
}
@Around("commentDelete() || userRoleChange()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
//측정 시간
long startTime = System.currentTimeMillis();
// 요청 사항
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String requestUrl = request.getRequestURI().toString();
LocalDateTime requestTime = LocalDateTime.now(); // API 요청 시각
Long userId = (Long) request.getAttribute("userId"); // Attribute에서 사용자 ID 가져오기
// userId로 user 객체를 조회
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("해당 사용자를 찾을 수 없다."));
try {
// 핵심기능 수행
Object output = joinPoint.proceed();
return output;
} finally {
// 측정 종료 시간
long endTime = System.currentTimeMillis();
// 수행시간 = 종료 시간 - 시작 시간
long runTime = endTime - startTime;
log.info("[API 사용 시간] 사용자 ID: " + userId +
", 총 소요 시간: " + runTime + " ms" +
", 요청 URL: " + requestUrl +
", 요청 Time: " + requestTime);
// API 사용시간 및 DB에 기록
ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(user)
.orElse(new ApiUseTime(user, runTime));
apiUseTime.addUseTime(runTime);
apiUseTimeRepository.save(apiUseTime);
}
}
}
해당 코드는 AOP(Aspect-Oriented Programming)를 사용하여 특정 API 호출 시 로깅과 사용자별 API 사용 시간을 기록하는 기능을 구현한 것이다. AOP는 비즈니스 로직과 분리된 공통 기능(로깅, 트랜잭션 관리, 보안 등)을 코드에 일관되게 적용하는 데 유용하다. 이 코드에서는 AdminAop 클래스가 주된 역할을 담당하고 있다.
주요 구성 요소
Aspect와 AOP 설정
- @Aspect: 이 클래스가 AOP를 위한 클래스라는 것을 선언. 즉, 이 클래스는 다른 메서드에 부가적인 기능(로깅, API 사용 시간 측정 등)을 적용하기 위한 클래스이다.
- @Component: 이 클래스가 스프링 빈으로 등록된다는 것을 의미한다. 스프링이 이 클래스를 관리할 수 있게 된다.
- @RequiredArgsConstructor: 롬복(Lombok)을 사용하여 클래스의 필드(apiUseTimeRepository, userRepository)에 대해 생성자를 자동으로 생성해준다.
Pointcut 정의
- @Pointcut: 특정 메서드가 실행될 때 AOP의 부가 기능을 적용할 지점을 정의한다.
- commentDelete(): CommentAdminController의 deleteComment() 메서드가 호출될 때를 나타낸다.
- userRoleChange(): UserAdminService의 changeUserRole() 메서드가 호출될 때를 나타낸다.
Around Advice
- @Around: AOP의 Advice 중 하나로, 메서드 호출 전후에 부가 기능을 적용할 수 있다. 여기서 부가 기능은 API 요청 시간 기록 및 실행 시간 측정이다.
- joinPoint.proceed(): 실제 비즈니스 로직을 실행하는 부분이다. AOP는 이 메서드를 호출하기 전과 후에 코드를 추가할 수 있다.
핵심 로직 설명
시작 시간 측정:
- System.currentTimeMillis()를 통해 API 호출이 시작된 시간을 기록한다.
HttpServletRequest 정보 가져오기:
- RequestContextHolder.getRequestAttributes()로 현재 요청의 HttpServletRequest 객체를 가져와, 요청 URL(requestUrl)과 사용자 ID(userId)를 확인한다.
- 사용자 ID는 HttpServletRequest의 attribute에서 가져온다. 이는 JWT 토큰 등의 인증 정보를 기반으로 사전에 설정된 사용자 ID일 것이다.
사용자 조회:
- userId로 UserRepository를 사용하여 DB에서 사용자 정보를 조회한다. 만약 해당 사용자를 찾을 수 없으면 IllegalArgumentException을 던진다.
핵심 로직 실행:
- joinPoint.proceed()가 핵심 기능(실제 deleteComment()나 changeUserRole() 메서드)을 수행한다.
종료 시간 및 수행 시간 측정:
- 메서드가 끝난 후 종료 시간을 측정하고, 실행 시간을 계산한다.
로깅 및 API 사용 시간 저장:
- 로그에 사용자 ID, 실행 시간, 요청 URL, 요청 시간을 기록한다.
- 사용자의 API 사용 시간을 DB에 저장하기 위해, ApiUseTimeRepository를 통해 ApiUseTime 엔티티를 조회하거나 새로 생성한다. 이후 addUseTime 메서드를 호출하여 이번 API 호출 시간을 추가하고, 변경된 정보를 DB에 저장한다.
이 코드의 주요 AOP 기능
- 비즈니스 로직과 부가 기능의 분리: 비즈니스 로직(deleteComment, changeUserRole)과 로깅, 실행 시간 측정과 같은 부가적인 기능을 분리하여, 각 기능을 독립적으로 유지 관리할 수 있다.
- API 요청 기록 및 성능 모니터링: 모든 API 요청에 대해, 사용자가 어떤 API를 언제 호출했고, 얼마나 오랫동안 실행되었는지 기록하는 기능을 추가함으로써 성능 모니터링 및 추적이 용이해진다.
- 확장성: 이 AOP 코드 구조는 추후 다른 API에도 쉽게 적용할 수 있다. 새로운 Pointcut을 추가하고 @Around 메서드에서 조건을 걸어주면 된다.
이 방식은 성능 로깅뿐만 아니라, 보안 점검, 트랜잭션 관리 등 다양한 목적의 부가 기능을 추가할 때에도 유용하게 사용된다.
'Spring' 카테고리의 다른 글
Error 메시지(enum 활용) (3) | 2024.09.13 |
---|---|
Service 테스트 코드, controller 테스트 코드 (0) | 2024.09.13 |
단위 테스트(Unit Testing), JUnit5 (0) | 2024.09.10 |
커스텀 어노테이션 (2) | 2024.09.05 |
Edit Configuration을 통해 SQL DB로그인하기 (0) | 2024.09.03 |