Custom Annotaion으로 시스템 접근이력 수집하기
사용자가 웹사이트에서 어떤 동작을 요청하였는지 로그를 수집하고 싶었다.
AOP 또는 Filter에서 어떤 클래스, 메서드를 실행하였는지 일일이 확인하여 텍스트로 변환 하는 건 너무 번잡해 보여
CustomAnnotation을 이용하기로 했다.
Test Code 작성
우선 간단하게 샘플을 작성하였다. (Controller, Service, Html 까지)
TestController
@Controller
@RequiredArgsConstructor
public class TestController {
private final TestService testService;
@GetMapping("/home")
public String home(){
// home에 진입하였습니다.
System.out.println("/home call");
testService.home();
return "home";
}
@PostMapping("/home")
public ResponseEntity<String> createHome(@RequestParam Map<String, String> params){
// home 을 생성하였습니다.
System.out.println("POST /home call");
System.out.println("params.toString() = " + params.toString());
testService.createHome(params);
return ResponseEntity.status(HttpStatus.CREATED).body("생성이 완료되었습니다.");
}
@GetMapping("/list")
public String list(){
// list에 진입하였습니다.
System.out.println("/list call");
testService.list();
return "list";
}
}
TestService
@Service
public class TestService {
public String home(){
// /home 호출 시 서비스 로직이 동작되었다고 가정합니다.
System.out.println("TestService.home()");
return "service return";
}
public void createHome(Map<String, String> params){
// POST /home 호출 시 서비스 로직이 동작되었다고 가정합니다.
System.out.println("TestService.createHome()");
}
public String list(){
// /list 호출 시 서비스 로직이 동작되었다고 가정합니다.
System.out.println("TestService.list()");
return "list return";
}
}
home.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HOME</title>
</head>
<body>
<h1>HOME 화면입니다.</h1>
<button onclick="callAjax();">post 요청</button>
</body>
<script src="http://code.jquery.com/jquery-latest.min.js"></script>
<script type="application/javascript">
function callAjax() {
let params = {
data: "데이터입니다."
};
$.ajax({
type: 'POST',
url: '/home',
data: params,
success: function (data, status, xhr) {
alert(xhr.responseText);
},
error: function (xhr, status, error) {
// xhr : 응답 메시지 , status : error 고정 / error : 오류
alert('code : ' + xhr.code, ', msg : ' + xhr.responseText);
}
});
}
</script>
</html>
list.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>List</title>
</head>
<body>
<h1>LIST 페이지 입니다.</h1>
</body>
</html>
시나리오
- GET /home 을 조회하면 home 조회 라는 access log를 남긴다.
- POST /home 을 요청하면 home 추가 라는 accesslog를 남긴다.
- GET /list 를 조회하면 list 조회 라는 accesslog를 남긴다.
AOP 추가
의존성 주입 build.gradle
implementation 'org.springframework.boot:spring-boot-starter-aop
AOP 생성
AOPConfig.java
@Configuration
@Aspect
@Slf4j
public class AOPConfig {
@Pointcut("execution(* com.example.{패키지}.controller.*..*(..))")
public void accessLogging() {}
@Around("accessLogging()")
public Object accessLog(ProceedingJoinPoint pjp) throws Throwable {
// @AccessAnnotation 에 저장된 log메시지를 가져와 로깅
MethodSignature signature = (MethodSignature) pjp.getSignature();
if(signature.getMethod().getAnnotation(AccessAnnotation.class) == null){
return pjp.proceed();
}
String action = signature.getMethod().getAnnotation(AccessAnnotation.class).action();
String accessLog = signature.getMethod().getAnnotation(AccessAnnotation.class).log();
log.info("action : {}, accessLog : {}", action, accessLog);
return pjp.proceed();
}
}
커스텀 어노테이션 인터페이스 생성
AccessAnnotation.java
/**
* 컨트롤러에서 @AccessAnnotation(action="사용자 목록 조회") 으로 사용 <br>
*
* @param: log - ex.home 조회 <br><br>
* @param: action - [CREATE, READ, UPDATE, DELETE] 중 택1
* */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessAnnotation {
String action();
String log();
}
Controller에서 매서드마다 커스텀 어노테이션 적용하기
TestController.java
@Controller
@RequiredArgsConstructor
@Slf4j
public class TestController {
private final TestService testService;
@GetMapping("/home")
@AccessAnnotation(log = "home 조회", action = "READ")
public String home(){
// home에 진입하였습니다.
log.info("/home call");
testService.home();
return "home";
}
@PostMapping("/home")
@AccessAnnotation(log = "home 생성", action = "CREATE")
public ResponseEntity<String> createHome(@RequestParam Map<String, String> params){
// home 을 생성하였습니다.
log.info("POST /home call");
log.info("params.toString() = " + params.toString());
testService.createHome(params);
return ResponseEntity.status(HttpStatus.CREATED).body("생성이 완료되었습니다.");
}
@GetMapping("/list")
@AccessAnnotation(log = "list 조회", action = "READ")
public String list(){
// list에 진입하였습니다.
log.info("/list call");
testService.list();
return "list";
}
}
로그
적용 이후 localhost:8080/home 요청 시 로그
2024-01-22T15:55:31.103+09:00 INFO 11828 --- [nio-8080-exec-1] c.e.e.AOPConfig : action : READ, accessLog : home 조회
2024-01-22T15:55:31.105+09:00 INFO 11828 --- [nio-8080-exec-1] c.e.e.controller.TestController : /home call
2024-01-22T15:55:31.106+09:00 INFO 11828 --- [nio-8080-exec-1] c.e.e.service.TestService : TestService.home()
POST /home 요청 시 로그
2024-01-22T15:55:33.302+09:00 INFO 11828 --- [nio-8080-exec-2] c.e.e.AOPConfig : action : CREATE, accessLog : home 생성
2024-01-22T15:55:33.302+09:00 INFO 11828 --- [nio-8080-exec-2] c.e.e.controller.TestController : POST /home call
2024-01-22T15:55:33.303+09:00 INFO 11828 --- [nio-8080-exec-2] c.e.e.controller.TestController : params.toString() = {data=데이터입니다.}
2024-01-22T15:55:33.303+09:00 INFO 11828 --- [nio-8080-exec-2] c.e.e.service.TestService : TestService.createHome()
GET /list 요청 시 로그
2024-01-22T15:55:43.043+09:00 INFO 11828 --- [nio-8080-exec-3] c.e.e.AOPConfig : action : READ, accessLog : list 조회
2024-01-22T15:55:43.044+09:00 INFO 11828 --- [nio-8080-exec-3] c.e.e.controller.TestController : /list call
2024-01-22T15:55:43.044+09:00 INFO 11828 --- [nio-8080-exec-3] c.e.e.service.TestService : TestService.list()
결론
- AOPConfig.java 에서
@Pointcut
을 이용하여 controller 패키지만 AOP를 거치도록 처리 (MethodSignature) pjp.getSignature()
을 이용하여 처리할 매소드 정보를 가져오고 해당 메서드가 커스텀어노테이션(AccessAnnotation) 을 가지고 있는지 확인- 가지고 있지 않다면 추가 작업 없이 프로세스를 실행하도록 하고,
- 가지고 있다면 해당 어노테이션에 등록된 action과 log의 데이터를 가져와 로그 출력 후 프로세스를 실행한다.