AOP는 공통 관심사와 핵심 관심사를 분리하는데 그 목적이 있다. 로거는 인터셉터를 이용해서도 구현할 수 있지만, AOP를 이용하면 비교적 간단하게 구현할 수 있다.
@Slf4j
@Aspect
@Component
public class LogAop {
@Around("within(com.pivot.hp.hometownpolitician.controller..*)")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
Long start = System.currentTimeMillis();
Object returnObj = pjp.proceed();
Long end = System.currentTimeMillis();
logRequest(pjp);
logResponse(pjp, returnObj);
logSummary(pjp, start, end);
return returnObj;
}
private void logRequest(JoinPoint joinPoint) {
log.info("REQUEST of {} ({})\n-> {}", getDeclaringTypeName(joinPoint), getMethod(joinPoint).getName(), getParams(joinPoint));
}
private void logResponse(JoinPoint joinPoint, Object returnObj) {
log.info("RESPONSE of {} ({})\n-> {}", getDeclaringTypeName(joinPoint), getMethod(joinPoint).getName(), getReturnObj(returnObj));
}
private void logSummary(JoinPoint joinPoint, Long start, Long end) {
log.info("SUMMARY of {} ({})\n-> {}", getDeclaringTypeName(joinPoint), getMethod(joinPoint).getName(), getSummary(start, end));
}
private Method getMethod(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
return signature.getMethod();
}
private String getDeclaringTypeName(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
return signature.getDeclaringTypeName();
}
private String getParams(JoinPoint joinPoint) {
Stream<Object> stream = Arrays.stream(joinPoint.getArgs());
if (joinPoint.getArgs().length == 0) {
return "No Parameter";
}
return "[\n" + getParamsInternal(stream) + "\n]";
}
private String getParamsInternal(Stream<Object> stream) {
return stream.map(e -> {
Object[] values = new Object[1];
values[0] = e;
return String.format("\t%s -> %s", e.getClass().getSimpleName(), Joiner.on(",").join(values));
}).collect(Collectors.joining(",\n"));
}
private Object getReturnObj(Object returnObj) {
Object[] values = new Object[1];
values[0] = returnObj;
String obj = String.format("\t%s -> %s", returnObj.getClass().getSimpleName(), Joiner.on(",").join(values));
return "{\n" + obj + "\n}";
}
private Object getSummary(Long start, Long end) {
return "{\n" + getSummaryInternal(start, end) + "\n}";
}
private Object getSummaryInternal(Long start, Long end) {
HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
return new Summary(req, start, end);
}
static class Summary {
String addr;
String uri;
String method;
Long latency;
public Summary(HttpServletRequest req, Long start, Long end) {
addr = getIpAddress(req);
uri = getUri(req);
method = getMethod(req);
latency = end - start;
}
private String getIpAddress(HttpServletRequest req) {
Optional<String> ip = Optional.ofNullable(req.getHeader("X-FORWARDED-FOR"));
return ip.orElse(req.getRemoteAddr());
}
private String getUri(HttpServletRequest req) {
Optional<String> uri = Optional.ofNullable(req.getRequestURI());
return uri.orElse("");
}
private String getMethod(HttpServletRequest req) {
Optional<String> method = Optional.ofNullable(req.getMethod());
return method.orElse("");
}
public String toString() {
return "\taddr -> " + addr + "\n\turi -> " + uri + "\n\tmethod -> " + method + "\n\tlatency -> " + Long.toString(latency);
}
}
}
Java
복사
위와 같은 코드를 이용하여 AOP 객체를 구현하면 main 경로의 controller 아래에 존재하는 객체의 메서드를 이용할 때 로그가 남게 된다. controller의 객체들은 모두 API 호출을 위한 객체들이므로, API 호출로 그 결과를 확인할 수 있다.
@RestController
public class SampleController {
@GetMapping("/sample")
public String sampleCall(@RequestParam String args) {
return "sample";
}
}
Java
복사
작성해둔 간단한 컨트롤러 코드는 위와 같다.
@SpringBootApplication
@EnableAspectJAutoProxy
public class HometownPoliticianApplication {
public static void main(String[] args) {
SpringApplication.run(HometownPoliticianApplication.class, args);
}
}
Java
복사
AOP의 이용을 위해 위처럼 @SpringBootApplication이 있는 진입점에는 @EnableAspectJAutoProxy를 기입해준다.
호출 결과 로깅이 잘 되는 것을 볼 수 있다.