spring.jpa.show-sql= true
@Slf4j
애노테이션을 이용한 log.info()
를 사용하는 것이다.AOP
(Aspect Oriented Programming)을 사용하여 AOP
의 개념과 구현 방법을 학습하려고 한다.@RestController
을 붙인 클래스의 메서드가 호출될 때마다{methodName} in {className} ({api-url]) was called by {IPaddress}
로 로그처리하기@Repository
을 붙인 인터페이스의 메서드가 호출될 때마다{methodName} in {interfaceName} was executed
로 로그처리하기 // AOP
implementation 'org.springframework.boot:spring-boot-starter-aop'
@Slf4j
@Aspect
@Component
public class ControllerLoggingAspect {
@Before("@within(org.springframework.web.bind.annotation.RestController)")
public void beforeControllerMethods(JoinPoint joinPoint) {
Method method = getMethod(joinPoint);
String className = joinPoint.getTarget().getClass().getSimpleName();
String clientIp = getclientIp();
String mappingInfo = getClassMappingInfo(joinPoint.getTarget().getClass()) + getMappingInfo(method);
log.info("[{}] in {} ({}) was called from {}", method.getName(), className, mappingInfo, clientIp);
}
private Method getMethod(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
return signature.getMethod();
}
private String getClassMappingInfo(Class<?> targetClass) {
String url;
if (targetClass.isAnnotationPresent(RequestMapping.class)) {
url = Arrays.toString(targetClass.getAnnotation(RequestMapping.class).value());
return url.substring(1, url.length() - 1);
}
return "";
}
private String getMappingInfo(Method method) {
String mappingValue = "";
if (method.isAnnotationPresent(GetMapping.class)) {
mappingValue = Arrays.toString(method.getAnnotation(GetMapping.class).value());
} else if (method.isAnnotationPresent(PostMapping.class)) {
mappingValue = Arrays.toString(method.getAnnotation(PostMapping.class).value());
} else if (method.isAnnotationPresent(PutMapping.class)) {
mappingValue = Arrays.toString(method.getAnnotation(PutMapping.class).value());
} else if (method.isAnnotationPresent(DeleteMapping.class)) {
mappingValue = Arrays.toString(method.getAnnotation(DeleteMapping.class).value());
} else if (method.isAnnotationPresent(RequestMapping.class)) {
mappingValue = Arrays.toString(method.getAnnotation(RequestMapping.class).value());
}
return mappingValue.substring(1, mappingValue.length() - 1);
}
private String getclientIp() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return "Unkown IP";
}
HttpServletRequest request = attributes.getRequest();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) ip = request.getRemoteAddr();
if (ip.equals("0:0:0:0:0:0:0:1")) return "localhost";
return ip;
}
}
@Aspect
: 해당 클래스를 AOP
기능을 사용할 수 있도록 설정
@Component
: 해당 클래스를 빈 컴포넌트로 설정
@Before(~)
: ~에 해당하는 메소드가 실행되기 전에 호출되도록 설정
@After(~)
: 은 메소드가 실행된 후 호출되도록 설정
@within(org.springframework.web.bind.annotation.RestController)
: @RestControler
애노테이션이 붙은 클래스의 메소드들을 대상으로 하겠다는 뜻이다.
즉, @Before("@within(org.springframework.web.bind.annotation.RestController)")
은 @RestController
애노테이션이 붙은 클래스의 메소드가 실행되기 전에 호출되는 메서드로 정의하겠다 라는 뜻이다.
JoinPoint
: 실행 중인 메서드와 실행 정보를 제공
이를 이용하여 어떤 메서드가 호출되었는지, 어떤 클래스에 속하는지, 어떤 인수와 함께 호출되었는지에 관한 정보를 얻을 수 있다.
호출한 url을 mappingInfo
에 담았다.
클래스 레벨의 @RequestMapping("/api/v1/member")
과 메서드 레벨의 @(Get|Post|Patch|Put|Delete)Mapping("/create")
이 더해져 url이 만들어진다.
이에 클래스 레벨과 메서드 레벨의 경로값을 각각 구해 mappingInfo
를 로깅처리하였다.
IP는 RequestContextHolder
에서 가져올 수 있다. 이때 프록시 혹은 로드밸런서를 거치면 X-Forwarded-For
라는 HTTP헤더에 IP가 담기므로 이를 고려하여 IP를 가져올 수 있다. (0:0:0:0:0:0:0:1 는 localhost의 IPv6)
@Slf4j
@Aspect
@Component
public class RepositoryLoggingAspect {
@Before("@within(org.springframework.stereotype.Repository)")
public void beforeRepositoryMethod(JoinPoint joinPoint) {
Method method = getMethod(joinPoint);
Class<?>[] interfaces = joinPoint.getTarget().getClass().getInterfaces();
String interfaceName = interfaces.length > 0 ? interfaces[0].getSimpleName() : "unknown Interface";
log.info("[{}] in {} executed", method.getName(), interfaceName);
}
private Method getMethod(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
return signature.getMethod();
}
}
위의 ControllerLoggingAspect
와 비슷하다.
@within(org.springframework.stereotype.Repository)
를 사용하여 @Repository
애노테이션을 붙인 인터페이스의 메서드를 대상임을 설정한다.
여기서 주의할 점은 인터페이스가 아닌 구현 클래스의 이름으로 로그를 찍게 되면 안된다.
Spring Data JPA에서 Repository의 구현체는 런타임시점에 프록시 객체로 생성되므로 AOP에서 클래스이름을 가져오게 되면 다음과 같이 프록시 이름이 찍히게 된다.
getTarget().getClass().getInterfaces()
를 통해 프록시가 구현하는 인터페이스를 가져와야 한다.
특정 구현체가 구현한 인터페이스가 여러 개 일 수 있다. 하지만 해당 JpaRepository구현체는 JpaRepository 하나만 구현하기 때문에 interfaces[0].getSimpleName()
를 통해 인터페이스의 이름을 가져올 수 있다.
ControllerLoggingAspect
와 RepositoryLoggingAspect
에서 설정한 메서드들이 호출될 때 로그가 찍히는 것을 확인할 수 있었다.