이번 포스팅에서는 Logging 처리 중 Interceptor와 AOP를 활용한 Logging 처리에 대하여 작성해보고자 합니다.
Spring Interceptor
는 HandlerInterceptor
를 구현하는 별도의 클래스를 작성해서 구현한다.
HandlerInterceptorAdapter
는 Spring Boot 3.x에서는 삭제되었으므로, 따라서 HandlerInterceptor
만 활용한다.
HandlerInterceptor
는 다음과 같이 구성된다.
preHandle()
: 실제 핸들러가 실행되기 이전postHandle()
: 핸들러가 실행 된 이후afterCompletion()
: 요청이 모두 완료된 이후preHandle()
: boolean
타입의 반환값을 가짐. true
인 경우에는 요청을 실행하고, false
인 경우 요청을 중단.
postHandle()
: 핸들러가 실행된 이후 실행됨.
afterCompletion()
: 요청이 모두 완료된 이후, 예외 발생 여부와 관계 없이 클라이언트에 응답이 전달될 때 실행됨.
로그를 언제 기록할것인지에 따라, 3가지 메서드를 각각 재정의 하면 된다.
이번 과제에서 나의 목표는 deleteComment()
시 Interceptor
를 활용하여 어드민 권한 여부를 확인하여 인증되지 않은 사용자의 접근을 차단하고, 인증 성공시 요청 시각과 URL을 로깅하도록 구현하는 것이다.
따라서 preHandle()
에서는 요청 시각과 URL에 대한 Log 를 기록하고 true 를 return 하도록 구현한다.
그 이유는 Filter
에서 사용자가 ADMIN이 아닐 경우 예외 처리를 하고 있으므로, Interceptor
까지 들어온 상태라면 사용자는 ADMIN이기 때문이다.
//LoggingInterceptor 생성
public class LoggingInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object Handler) {
LocalDateTime time = LocalDateTime.now();
logger.info("===Admin Access: {}===", request.getMethod());
logger.info("Time: {}, URL: {}", time, request.getRequestURI());
return true;
}
}
//LoggingInterceptor 등록 (WebConfig 클래스에 등록한다)
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggingInterceptor()).addPathPatterns("/admin/comments/**");
}
인터셉터를 WebConfig에 등록한다.
/admin/comments/
에 해당하는 URL일 경우 해당LoggingInterceptor
를 지나게 된다.
Filter
에서 ADMIN 판별을 했으므로,Interceptor
에서는 시간값과 URL 값을 로그로 기록한 뒤, true 를 return 해준다.
로그가 잘 찍히는 것을 확인할 수 있다.
AOP(Aspect Oriented Programming)
이란 관점 지향 프로그래밍이다.
비즈니스 로직의 기준을 정하고 그 기준에 따라 나눈 부분을 관점이라고 한다면, 해당 관점을 모듈화 하는것이라 할 수 있다.
예를 들어, 로깅, 트랜잭션 관리, 보안 등은 여러 모듈에서 공통적으로 가지고 있는 관점이라 할 수 있다.
해당 관점을 하나의 모듈로 모듈화 하여 가독성을 높이고 중복을 줄이는 것이 목표이다.
Spring에서의 AOP 흐름은 다음과 같다.
1. Request -> Filter
2. Filter -> DispatcherServlet
3. DispatcherServlet -> Interceptor
4. Interceptor -> AOP
5. AOP -> Controller
이후 역순으로 진행된다.
따라서 AOP는 인터셉터 이후, 컨트롤러 이전에 처리된다.
즉, Logging에 효과적으로 사용될 수 있다.
이번 과제에서 나의 목표는 changeUserRole()
시 AOP
를 활용하여 요청/응답 데이터를 Logging하는 것이다.
UserAdminService
의 changeUserRole()
를 처리하기 위해 @Around
를 사용하여 UserController
의 changeUserRole
메서드를 AOP 적용 대상으로 설정한다.
LoggingAspect
클래스를 만들어 해당 클래스에서 Logging 처리를 구현한다.
Logging에 들어갈 정보
해당 정보를 log에 기록하고 메서드를 종료한다.
@Aspect
@Component
@RequiredArgsConstructor
public class LoggingAspect {
private final HttpServletRequest request;
private final ObjectMapper objectMapper = new ObjectMapper();
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Pointcut("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))")
public void pointCut() {
}
@Around("pointCut()")
public Object loggingMethodCall(ProceedingJoinPoint joinPoint) {
Method method = getMethod(joinPoint);
logger.info("===Admin Access: {}===)", method.getName());
return logReturn(joinPoint);
}
//로그 기록 위한 메서드
private Object logReturn(ProceedingJoinPoint joinPoint) {
String userId = String.valueOf(request.getAttribute("userId"));
String requestUrl = request.getRequestURI();
LocalDateTime requestTime = LocalDateTime.now();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
//requestBody JSON -> String
String requestBody = Arrays.stream(args)
.map(arg -> {
try {
return objectMapper.writeValueAsString(arg);
} catch (Exception e) {
return "Failed to convert request body to JSON";
}
})
.toList()
.toString();
//실행
Object result;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
logger.error("[ERROR] {} - Message: {}", methodName, e.getMessage(), e);
throw new RuntimeException(e);
}
//실행 후 결과 값인 responseBody JSON -> String
String responseBody;
try {
responseBody = objectMapper.writeValueAsString(result);
} catch (Exception e) {
responseBody = "Failed to convert response body to JSON";
}
logger.info("User Id: {}, Request Time: {}, Request URL: {}, RequestBody: {}, ResponseBody: {}", userId,
requestTime,
requestUrl, requestBody, responseBody);
return result;
}
private Method getMethod(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
return signature.getMethod();
}
}
pointCut을 통해 특정 메서드에서만 실행 될 수 있도록 한다.
@Around를 사용하여 특정 메서드가 들어올 경우 로그를 남길 수 있도록 한다.
logReturn 메서드는 RequestBody 본문과 ResponseBody 본문을 로그로 출력하기 위하여 따로 메서드를 분리하여 구현하였다.
로그가 잘 찍히는 것을 확인할 수 있다.