이전 포스트에서 AOP를 활용한 성능 측정 방법에 대해 알아보았습니다.
AOP의 주요한 목적이 핵심 기능과 부가 기능을 분리하여 프로그래머로 하여금 비지니스 로직에 집중할 수 있게 하는데 있다는 것을 알았습니다.
이제 AOP를 이용하여 실무에서 자주 겪을 수 있는 부가 기능 중 공통 헤더 처리 기능을 분리해봅시다.
사용자가 있는 어플리케이션에는 현재 사용자가 위치한 페이지에 대한 정보가 담겨있는 공통 헤더 부분이 존재하게 됩니다.
이미지 입력
해당 view 파일에 공용 헤더 부분을 직접 수정하는 방식으로 다양한 페이지의 공통 헤더 처리가 가능하지만 오탈자의 위험, view가 많아졌을 때 하나하나 직접 수정하는 데 한계가 존재합니다.
이런 경우 Model 객체를 활용하여 Back단에서 값을 입력하여 처리하는 방식을 많이 사용하고 있을겁니다.
@Controller
public class HelloController(){
@GetMapping("/hello/index")
public String indexPage(Model model){
model.addAttribute("depth1", "헬로우 서비스");
model.addAttribute("depth2", "인덱스");
//...do something
return "index";
}
@GetMapping("/hello/list")
public String listPage(Model model){
model.addAttribute("depth1", "헬로우 서비스");
model.addAttribute("depth2", "목록");
//...do something
return "list";
}
...
}
HelloController Model객체를 사용한 attribute setting
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<script src="/vendor/jquery-easing/jquery.easing.min.js"></script>
</head>
<body>
<div class="common-heaer">
<span class="header-item">{{depth1}}</span>
<span class="header-item">{{depth2}}</span>
</div>
.... render something
</body>
</html>
common header 가 포함된 mustache 파일에서 model 값 사용
하지만 이러한 처리 방식은 html에 직접 작성하던 방식을 back단의 Controller부분으로 위임한 것이지 방식을 개선되었다고 할 수 없습니다. 오히려 Controller에는 공통 헤더 값을 입력해주는 부가 기능이 추가된 모습을 볼 수 있습니다.
공통되는 부가 기능을 확인하였으니 AOP를 활용하여 이를 핵심 기능과 분리해보도록 합시다.
최종적인 목표는 간단합니다. Controller에서 공통 헤더 값을 셋팅해주는 로직을 별도의 Aspect로 떼어네어, 페이지 이동을 담당하는 Controller의 메소드가 실행 될 때 필요한 값들을 model attribute로 알아서 입력해주는 것입니다.
우선 custom annotation class를 하나 작성해보도록 하겠습니다.
@CommonHeader
는 @Column(name = “someThing”)
과 같이 특정 메소드에서 사용될 수 있는 value값을 지정해주는 역할을 합니다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CommonHeader {
String depth1 default "depth1";
String depth2 default "depth2";
}
이제 앞서 작성했던 HelloController에 @CommonHeader를 적용해보도록 하겠습니다.
@Controller
public class HelloController(){
@CommonHeader(depth1 = "헬로우 서비스", depth2 = "인덱스")
@GetMapping("/hello/index")
public String indexPage(Model model){
//...do something
return "index";
}
@CommonHeader(depth1 = "헬로우 서비스", depth2 = "목록")
@GetMapping("/hello/list")
public String listPage(Model model){
//...do something
return "list";
}
...
}
이전에 작성했던 로직보다 한결 간결해졌음은 물론 각 메소드는 비지니스 로직에 집중할 수 있는 구조가 되었습니다.
이제 부가 기능 수행 역할을 하게 될 CommonHeaderAspect.class
를 작성해보겠습니다.
CommonHeaderAspect
가 실행 될 때 일어나는 일들을 정리해 보았습니다. 이를 토대로 코드를 작성해보도록 하겠습니다.
우선 페이지 이동(Router.class)과 API(Controller.class) 역할을 하는 클래스를 패키지 수준에서 분리하도록 하겠습니다.
├── main
│ └── com
│ ├── router
│ └── controller
└── resources
└── template
@Aspect // (1)
public class CommonHeaderAspect {
@After("execution(public String com.router..*Router.*(..)) && @annotation(org.springframework.web.bind.annotation.GetMapping))") // (2)
public void commonHeaderMethod(JoinPoint joinPoint) {
String depth1 = null;
String depth2 = null;
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); // (3)
String requestUri = request.getRequestURI();
Class targetClass = joinPoint.getTarget().getClass(); // (4)
Method[] methods = targetClass.getDeclaredMethods();
for(Method m : methods){
String tempUri = m.getAnnotation(GetMapping.class).value()[0];
if(requestUri.equals(tempUri)){
depth1 = m.getAnnotation(CommonHeader.class).depth1();
depth2 = m.getAnnotation(CommonHeader.class).depth2();
}
}
Object[] args = joinPoint.getArgs(); // (5)
for(Object arg : args){
if(arg instanceof Model){
Model model = (Model)arg;
model.addAttribute("depth1", depth1);
model.addAttribute("depth2", depth2);
}
}
}
}
@Before
, @After
, @Around
, @AfterRetuning
, @AfterThrowing
을 사용 가능합니다.@After
의 경우는 대상이되는 객체의 method 실행 후 공통 기능을 실행함을 명시해주었습니다.execution(public String com.router..*Router.*(..)
: com.router 패키지에 포함되어있는 클래스중 Router로 끝나는 클래스의 method를 대상으로 실행됩니다. (접근제한자는 public, Return type은 String인 method가 실행 대상이 됩니다.)@annotation(org.springframework.web.bind.annotation.GetMapping))
: 화면 이동 역할의 method를 대상으로 실행하기 때문에 GetMapping annotation이 붙어있는 method를 대상으로 이전 조건과 and연산을 하였습니다.RequestContextHolder
: 전역에서 Request에 대한 정보를 사용할 수 있도록 Spring에서 제공하는 클래스입니다. 요청된 Request의 정보를 받아와 사용자 Request Uri를 초기화합니다.이와 같이 처리할 경우 /book/save
, /user/users
, /item/delete
와 같은 주소로의 요청은 간단하게 처리가 가능하지만, /book/1
, /user/update/3
, /item/delete?parameter=true
와 같이 Parameter가 uri에 포함되어 전달되는 경우의 처리가 어려울 수 있습니다.
이런 경우 @Aspect의 공통 기능 로직 내에서 파라미터 포함 여부를 확인하여 파라미터를 제외한 값들로 값 처리를 진행하면 가능하겠지만, 로직이 복잡하고 직관적이지 않게 될 가능성이 있습니다.
지금까지의 학습 내용을 바탕으로 작성한 Aspect는 현 상태가 최선이지만 추후 새로 알게 된 사실이 생긴다면 이와 같은 문제점도 처리할 수 있도록 업데이트 할 예정입니다.