AOP (Aspect Oriented Programming)
AOP는 부가기능을 핵심 기능에서 분리해 한 곳으로 관리하도록 하고, 이 부가 기능을 어디에 적용할지 선택하는 기능을 합한 하나의 모듈입니다.
AOP 용어
- 조인 포인트
- 어드바이스가 적용될 수 있는 위치로, AOP를 적용할 수 있는 모든 지점
- 포인트컷
- 조인 포인트 중에서 어드바이스를 어디에 적용할 지, 적용하지 않을 지 위치를 판단하는 필터링 기능
- 타겟
- 어드바이스를 받는 객체, 포인트컷으로 결정
- Advice
- 부가 기능
- Aspect
- 어드바이스 + 포인터컷을 모듈화 한 것
- Advisor
- 하나의 어드바이스와 하나의 포인트 컷으로 구성
AOP 적용 방식
- 컴파일 시점
- 클래스 로딩 시점
- 런타임 시점
컴파일 시점과 클래스 로딩 시점 적용 방식은 AspectJ 프레임 워크를 직접 사용해야 하지만 AspectJ를 학습하기가 번거로워 주로 런타임 시점 적용 방식을 사용하는 스프링 AOP를 사용한다.
Advice 종류
@Around | 핵심기능 수행 전과 후 (@Before + @After) |
@Before | 핵심기능 호출 전 |
@After | 핵심기능 수행 성공/실패 여부 상관 없이 언제나 동작 |
@AfterReturning | 핵심기능 수행 성공 시 (함수의 return 값 사용가능) |
@AfterThrowing | 핵심기능 수행 실패 시. 즉 예외가 발생한 경우 동작 |
@Around 어드바이스를 사용할 경우 메서드의 파라미터로 "ProceedingJoinPoint"를 꼭 넣어줘야 한다.
ProceedingJoinPoint의 proceed()는 다음 어드바이스나 타겟을 호출 하는 것으로, 꼭 proceed() 메서드를 호출해줘야한다.
이 외에도 ProcedingJoinPoist 인터페이스가 제공하는 메서드를 사용하여 호출되는 객체의 정보나 실행되는 메서드의 정보를 알 수 있다.
아래는 ProcedingJoinPoist 인터페이스가 제공하는 메서드의 일부이다.
Signature get Signature() | 호출되는 메서드에 대한 정보를 반환 |
Object getTarget() | 대상 객체를 반환 |
String getName() | 메서드의 이름을 반환 |
String toLongString() | 메서드를 완전하게 표현한 문장을 반환 |
직접 사용해보기
Spring AOP를 사용하기 위해서는 gradle에 의존성을 추가해줘야한다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
각각의 컨트롤러로 요청이 들어올 때 마다 요청 시각, Url, 사용자 Id, requestBody와 responeBody를 로그에 찍히도록 구현하였다.
@Slf4j(topic = "TodoAop")
@Aspect // 해당 클래스가 Aspect라는 것을 명시
@Component // 스프링 빈으로 등록
@RequiredArgsConstructor
public class TodoAop {
// user.controller 패키지의 모든 클래스
@Pointcut("execution(* com.sparta.todo.domain.user.controller..*(..))")
public void user() {
}
// comment.controller 패키지의 모든 클래스
@Pointcut("execution(* com.sparta.todo.domain.comment.controller..*(..))")
public void comment() {
}
// manager.controller 패키지의 모든 클래스
@Pointcut("execution(* com.sparta.todo.domain.manager.controller..*(..))")
public void manager() {
}
// todo.controller 패키지의 모든 클래스
@Pointcut("execution(* com.sparta.todo.domain.todo.controller..*(..))")
public void todo() {
}
@Around("user() || todo() || comment() || manager()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
LocalDateTime requestTime = LocalDateTime.now();
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
String formattedDate = requestTime.format(format);
HttpServletRequest request =((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
Long userId = (Long) request.getAttribute("userId");
log.info("Request : {} {} {} {}" , request.getMethod(),request.getRequestURI(),params(joinPoint),formattedDate);
log.info("사용자 Id : {}", userId);
Object obj = joinPoint.proceed();
request =((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
log.info("Response : {} {}", request.getRequestURI(), obj);
return obj;
}
private Map<String, Object> params(JoinPoint joinPoint) {
CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
String[] paramNames = codeSignature.getParameterNames();
Object[] paramValues = joinPoint.getArgs();
Map<String, Object> params = new HashMap<>();
for (int i = 0; i < paramNames.length; i++) {
params.put(paramNames[i], paramValues[i]);
}
return params;
}
}
Todo 게시글 저장 요청 시
위 사진처럼 요청 시각과 사용자 Id, 요청 uri, request, responseBody가 로그에 찍힌다 !
'TIL' 카테고리의 다른 글
미리 서명된 URL(Pre-signed url) 사용해보기 (0) | 2024.11.11 |
---|---|
GCS를 이용한 이미지 업로드 기능 구현 (2) | 2024.11.08 |
기능 개선 과제 (0) | 2024.10.31 |
Todo 프로젝트 리팩토링 하기 (0) | 2024.10.30 |
뉴스피드 프로젝트 리팩토링 (1) | 2024.10.25 |