AOP를 통해 모든 request parameter(혹은 requset body)와 response를 로그를 찍어보기로 한다.
현재 컨트롤러에서 수동으로 각각의 endpoint마다 로그를 찍는 코드가 추가되어 있는데, AOP를 이용하면 좋을 것 같았다!
의존성 추가
build.gradle
에 의존성을 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
Aspect작성
aop
라는 패키지를 생성하고, 패키지 하위에 LoggingAspect
라는 클래스를 만들었다..
@Aspect // AOP 사용
@Component // Bean 으로 등록
public class LoggingAspect {
}
Pointcut
controller 패키지 하위의 모든 public 메서드와 매칭시킨다.@annotation
을 이용하면 애노테이션별로 매칭시킬 수도 있다. @PostMapping
과 매칭시켜서 POST
요청에만 로그를 찍는다던지..
/* controller 패키지에 포함된 public 메서드와 매칭 */
@Pointcut("within(test.rest.api.controller..*)")
public void onRequest() { }
참고로 애노테이션으로 매칭시키는 방법이다.
// POST
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
// GET
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
Advice
Advice는 실제로 실행될 내용을 적는 부분이다.
Logger
를 매칭되어 실행될 메서드의 클래스를 통해 생성한다- result에는 해당 메서드의
return value
가 반환된다 (ResponseEntity
) joinPoint.proceed()
를 통해 프록시가 호출되고, 프록시가 실제 메서드를 호출한다finally
블록에서 requstURI, parameters, response 로그를 찍는다
/* Pointcut 과 매칭되는 메서드의 실행 전, 후에 실행
* @Around advice 는 꼭 proceed()가 필요하다. */
@Around("onRequest()")
public Object logAction(ProceedingJoinPoint joinPoint) throws Throwable{
Class clazz = joinPoint.getTarget().getClass();
Logger logger = LoggerFactory.getLogger(clazz);
Object result = null;
try {
result = joinPoint.proceed(joinPoint.getArgs());
return result;
} finally {
logger.info(getRequestUrl(joinPoint, clazz));
logger.info("parameters" + JSON.toJSONString(params(joinPoint)));
logger.info("response: " + JSON.toJSONString(result, true));
}
}
getRequestUrl()
JoinPoint
와 joinPoint가 실행된 class
를 이용해 요청 URI를 구한다.
HttpServletRequset
를 통해 구하는 방법도 있지만, 이 방법을 택했다.
- class에 선언된
@RequestMapping
의 value를 구한다 == beseURL - 해당 메서드에 선언된 애노테이션을 찾는다 (filter)
- 매칭된 애노테이션을 통해 URL을 구한다
private String getRequestUrl(JoinPoint joinPoint, Class clazz) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
RequestMapping requestMapping = (RequestMapping) clazz.getAnnotation(RequestMapping.class);
String baseUrl = requestMapping.value()[0];
String url = Stream.of( GetMapping.class, PutMapping.class, PostMapping.class,
PatchMapping.class, DeleteMapping.class, RequestMapping.class)
.filter(mappingClass -> method.isAnnotationPresent(mappingClass))
.map(mappingClass -> getUrl(method, mappingClass, baseUrl))
.findFirst().orElse(null);
return url;
}
/* httpMETHOD + requestURI 를 반환 */
private String getUrl(Method method, Class<? extends Annotation> annotationClass, String baseUrl){
Annotation annotation = method.getAnnotation(annotationClass);
String[] value;
String httpMethod = null;
try {
value = (String[])annotationClass.getMethod("value").invoke(annotation);
httpMethod = (annotationClass.getSimpleName().replace("Mapping", "")).toUpperCase();
} catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
return null;
}
return String.format("%s %s%s", httpMethod, baseUrl, value.length > 0 ? value[0] : "") ;
}
params
joinPoint.getArgs()
는 해당 메서드의 인자값들을 반환한다. 반환 받은 값을 이용해서 파라미터 맵을 만든다.
/* printing request parameter or request body */
private Map params(JoinPoint joinPoint) {
CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
String[] parameterNames = codeSignature.getParameterNames();
Object[] args = joinPoint.getArgs();
Map<String, Object> params = new HashMap<>();
for (int i = 0; i < parameterNames.length; i++) {
params.put(parameterNames[i], args[i]);
}
return params;
}
출력되는 모습
모든 요청마다 uri와 parameter, response를 자동으로 로그 출력한다.
'STUDY > Spring' 카테고리의 다른 글
Spring Boot | JPA 사용해보기 (2) Spring Data JPA (0) | 2021.04.23 |
---|---|
Spring Boot | logback.xml 파일 경로 설정 (0) | 2021.04.22 |
Spring Boot | AOP(Aspect Oriented Programming) (0) | 2021.04.16 |
Spring Boot | @ConfigurationProperties 알아보기 (0) | 2021.04.15 |
Spring Boot | JPA 사용해보기 (1) + H2데이터베이스 설치 (0) | 2021.04.06 |