AOP

공통 관심 사항(부가 로직)과 핵심 관심 사항(핵심 로직)을 분리한다.

데이터를 CRUD할 때마다 로그를 남기는 로직을 구현하고자 한다면 CRUD가 핵심 로직이고, 로그를 남기는 로직이 부가 로직이다.

 

 

Aspect

부가 기능을 핵심 기능에서 분리하고 한 곳에서 관리하도록 만든 기능이다.

부가 기능을 어떤 위치에 적용할지 선택하는 기능도 포함한다.

 

부가 기능(Advice)을 하나의 위치에서 관리한다.

어떤 위치에 적용(Pointcut)할지 정의한다. 

Aspect = Advice + Pointcut

 

JoinPoint

AOP 적용 가능 지점(생성자, 필드, method 실행, static method 실행)

Spring AOP는 Spring Container가 관리하는 Spring Bean에만 AOP를 적용할 수 있다.

AspectJ라면 AOP 적용 가능 지점에 모두 적용이 가능하지만, Spring AOP는 프록시 기반이기 때문에 메서드 실행 시에만 적용이 가능하다.

 

Aspect 구현

@Around : 메서드 실행 전 후

@Before : 메서드 실행 전

@After : 메서드 실행 후

import jakarta.servlet.http.HttpServletRequest;
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.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j // Lombok을 사용하여 로그를 사용하도록 설정한다.
@Aspect // 해당 클래스가 AspectJ를 사용한 AOP 클래스임을 나타낸다. AOP 구현을 위한 Proxy 생성 등을 자동으로 해준다.
@Component // Spring에서 관리하는 Bean으로 설정한다. 
public class RequestLogAop {

    // @PointCut : 실행 시점을 설정하는 어노테이션
    // 첫번째 * : 메서드의 반환 타입을 지정한다, 반환 타입에 상관없이 포인트컷을 적용한다는 뜻
    // com.thesun4sky.todoparty : 패키지 경로를 의미한다.
    // .. : 하위 디렉토리를 뜻한다.
    // *Controller : 클래스 이름을 지정한다. Controller로 끝나는 모든 클래스를 의미한다.
    // . : 클래스 내의 메서드를 뜻한다.
    // * : 메서드 이름을 지정한다. 즉, 모든 메서드 이름을 뜻한다.
    // (..) : 메서드의 매개변수를 지정한다. ".."은 0개 이상의 모든 매개변수를 의미한다.
    
    // com.thesun4sky.todoparty 패키지 및 하위 패키지에 있는, 이름이 Controller로 끝나는 모든 클래스의 모든 메서드(매개변수와 상관없이)를 대상으로 한다
    @Pointcut("execution(* com.thesun4sky.todoparty..*Controller.*(..))")
    private void controller() {}

    // 포인트컷으로 설정된 메서드 실행 전후에 로그를 출력하는 Around Advice Around, Before, After
    @Around("controller()")
    public Object loggingBefore(ProceedingJoinPoint joinPoint) throws Throwable {
        
        // 현재 HTTP 요청을 가져온다
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); // 요청 데이터를 전역(global)에서 가져오기 위해서

				// null 일 경우를 대비하여 null인 경우 경고 로그를 출력한다.
        if (requestAttributes == null) {
            log.warn("RequestAttributes가 null 입니다!");
            return joinPoint.proceed();
        }

				// HttpServletRequest 객체를 가져온다.
        HttpServletRequest request = requestAttributes.getRequest();
        
        // 현재 실행 중인 메서드 이름을 가져온다.
        String methodName = joinPoint.getSignature().getName();
        
	      // 요청 URI를 UTF-8로 디코딩하여 가져온다.
        String requestUri = URLDecoder.decode(request.getRequestURI(), StandardCharsets.UTF_8);
        
        // HTTP 메서드(GET, POST 등)를 가져온다.
        String httpMethod = request.getMethod();
        
        // 요청 파라미터를 가져와 문자열로 변환한다.
        String params = getParams(request); 

        log.info("[{}] {}", httpMethod, requestUri);
        log.info("method: {}", methodName);
        log.info("params: {}", params);

		// 메서드 실행 시점 이전에 한번만 원래 호출되어야할 메서드를 실행한다.
		// proceed()가 호출되기 전에는 실행 전 로직이 호출된다.
		// proceed()가 호출된 후에는 실행 후 로직이 호출된다.
        
        return joinPoint.proceed(); 
        
        // 메서드 실행 후에도 로그가 찍히도록 만드는 방법
        // proceed() 호출 이전 코드, 메서드 실행 시점 이전에 호출된다.
        // Object result = joinPoint.procced();
        
        // log.info("[{}] {}", httpMethod, requestUri);
        // log.info("method: {}", methodName);
        // log.info("params: {}", params);
        
        // return result;
    }

    // HTTP 요청의 파라미터를 문자열로 변환하여 반환한다
    private static String getParams(HttpServletRequest request) {
				
				// parameter를 Map 형태로 가져온다.
        Map<String, String[]> parameterMap = request.getParameterMap();
        
        // 맵의 Entry들을 Stream으로 변환한다.
        return parameterMap.entrySet().stream()
                .map(entry -> entry.getKey() + "=" + Arrays.toString(entry.getValue())) // 각 엔트리를 "key=[value]" 형태의 문자열로 변환한다.
                .collect(Collectors.joining(", ")); // 변환된 문자열들을 ", "로 구분하여 하나의 문자열로 합친다.
    }
}

 


Interceptor 인터셉터

요청과 응답을 가로채서 원하는 동작을 추가한다.

컨트롤러 진입 전에 해당하는 작업을 가로채서 처리한다는 점에서 filter와 같다고 느낄 수 있지만 차이점이 존재한다.

출처 : https://gngsn.tistory.com/153

dispatcher servlet 전에 처리하는 filter와 달리, interceptor는 dispatcher servlet과 controller 사이에서 작업을 처리한다.

그리고 필터는 요청에 대해서만 작업하지만 인터셉터는 요청과 응답 모두 처리한다.

 

public interface HandlerInterceptor {

    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable Exception ex) throws Exception {
    }
}

출처: https://gngsn.tistory.com/153 [ENFJ.dev:티스토리]

 

preHandle : 컨트롤러 진입 전 요청에 대한 처리

postHandle : 컨트롤러를 거쳐 클라이언트로 반환되는 데이터에 대한 처리

afterCompletion : 화면 처리(뷰)가 완료된 상태에서 처리 / 오류가 발생하든 발생하지 않든 무조건 처리

 

 


** 추가 공부 할 것**

https://gngsn.tistory.com/153

 

Spring Interceptor, 제대로 이해하기

Interceptor는 컨트롤러에 들어오는 요청 HttpRequest와 컨트롤러가 응답하는 HttpResponse를 가로채는 역할을 합니다. 스프링의 Intercepter의 이해와 사용법을 익히는 것이 본 포스팅의 목표입니다. ---------

gngsn.tistory.com

 

++

  • AspectJ, Spring AOP 차이
  • Spring Container
  • DI
  • Spring AOP 프록시
    • JDK 동적 프록시
    • CGLIB 프록시
  • 빈 후처리기
  • 빈 포스트 프로세서
  • Runtime Weaving
  • Compile Time Weaving
  • Load-Time Weaving