TIL

AOP

haseung22 2024. 10. 31. 12:36

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가 로그에 찍힌다 !