Spring에서 제공하는 웹 모듈로 MVC 패턴 즉, Model, View, Controller로 나뉘는 세 요소를 활용해 사용자의 HTTP 요청을 처리하고 응답을 준다.
이 응답은 단순 텍스트 일 수도, RESTful한 응답일 수도, View를 포함한 시스템일 경우 완성된 html 일 수도 있다.
다양한 요청을 처리하고 응답하기 위해 Web MVC는 계속 요청이 올 때까지 대기를 하고 있다가 요청이 오면 그 즉시 처리하고 응답을 보낸다.
가장 많은 곳에 관여하는 듯한 Dispatcher Servlet이라고 하는 녀석이 있는데, 이 녀석은 Controller의 일종이다.
이렇게 가장 앞단에서 유저의 유청을 받는 컨트롤러를 Front Controller라고 한다.
이 프론트 컨트롤러를 시작으로 진행되는 과정은 다음과 같다.
RequestMappingHandlerMapping
에 의해 HashMap 으로 관리된다.HandlerExecutionChain
을 반환한다.view를 생성하는 부분은 어떤 형식의 응답을 보내는가에 따라 다르다.
REST API를 구성하였다면, view 생성은 프론트단에서 처리가 되므로 그저 모델을 알맞는 형식으로 던져주면 된다.
@Controller : 응답으로 html주소를 넘기게 되어 있다.
@RestController : 응답으로 Rest API 요청에 대한 응답 (JSON 등)을 주게 되어 있다.
@RequestMapping : GET, POST 등 요청 방식을 직접 지정하여 받는다.
@RequestMapping(value = "/order/1", method = RequestMethod.GET)
public String getOrder() {
log.info("Get some order information");
return "orderId:1, orderAmount:100";
}
@GetMapping : Get 타입으로 온 요청을 받는다. 주로 조회에 쓰인다.
@PostMapping : Post 타입으로 온 요청을 받는다. 주로 새 레코드를 생성해야 할 때 쓰인다.
@PutMapping : Put 타입으로 온 요청을 받는다. 주로 전체 수정을 해야 할 때 쓰인다.
@PatchMapping : Patch 타입으로 온 요청을 받는다. 주로 일부만 수정해야 할 때 쓰인다.
@DeleteMapping : Delete 타입으로 온 요청을 받는다. 주로 레코드를 삭제해야 할 때 쓰인다.
@PostMapping(value={"/order/add", "/order/edit"})
public String createOrder() {
log.info("Add/Edit order");
return "orderId:1, orderAmount:100";
}
위와 같이 한 메소드로 두 개 이상의 경로를 매핑할 수도 있다.
Add와 Edit의 경우 로직이 상당히 비슷하기 때문에 저런식으로 구현하는 것도 가능하지만, RESTful한 형태는 아니다.
이유는 Convention (관례, 규약)을 지키지 않았기 때문.
괜히 위쪽에 주로 ~할 때 쓰인다 라고 열심히 쓴 게 아니다.
이러한 Convention은 요청의 타입만 보고도 어떤 류의 요청인지 빠르게 알아볼 수 있도록 하는 일종의 약속인 것이다.
어떨 때 Get을 쓰는지, 또 그럴 때 헤더에는 무엇이 들어가야 하는지, 바디에는 또 무엇을 넣어야 하는지, 그런 것이 자세하게 알고 싶다면 여기를 참고해보자.
위는 방금 막 월드컵에 대한 아무 기사 주소나 가지고 온 것이다.
주소를 잘 보면 ? 뒤로 mode=LPOD&mid=sec&...&aid=0013622862 라고 막 뭔가가 붙어 있는데 이것들이 파라미터이다.
이 주소에 들어가면 아마 기사가 엄청나게 많을 것이다.
그렇기 때문에 이 값, 저 값을 주어 DB 내에서 저 모든 값을 가진 유일한 레코드를 찾는데 도움을 줘야 한다.
파라미터를 넘기는 데에는 크게 2가지 방법이 있다.
1. 주소창을 통해 넘긴다 (위의 예시가 이 방법이다)
2. 요청의 body 부분을 통해 넘긴다 (회원가입이 대개 이 방법이다)
1번 방법은 또다시 2가지로 나뉘는데,
예를 들어 a=1, b=2, c=3 이라는 값 3개를 넘긴다고 가정하면,
www.어쩌고저쩌고?a=1&b=2&c=3
이렇게 넘길 수도 있지만
www.어쩌고저쩌고/1/2/3
이렇게 넘길 수도 있다. 물론 이 경우에는 순서에 맞게 값을 넣는 것이 중요하다.
자세한 방법을 알아보자
먼저 주소창으로 넘기는 1번 방법이다. Get, Delete 타입에서 사용된다.
각각 @PathVariable과 @RequestParam을 사용한다.
@GetMapping("/news/{newsId}")
public String getOrder(@PathVariable("newsId") String id) {
...
}
@GetMapping("/news")
public String getOrder(@RequestParam("newsId") String id) {
...
}
@PathVariable과 @RequestParam 안쪽에 문자열을 쓰기가 귀찮다면 주소에 쓰이는 변수명과 메소드의 파라미터로 쓰이는 변수명을 맞추자. 그러면 생략 가능하다.
GetMapping("/news/{newsId}")
public String getOrder(@PathVariable String newsId);
이런식으로!
다음은 RequestBody를 통해 넘기는 방법이다.
@PostMapping("/news")
public String createNews(@RequestBody CreateNewsRequest request,
@RequestHeader String userId) {
...
}
헤더는 필요없다면 꼭 받지 않아도 된다.
아래 그림은 요청이 들어올 때, 응답이 나갈 때의 흐름이다.
요청이 들어올 때는 왼쪽에서 오른쪽으로 쭉 거치며 들어가고,
응답이 나갈 때는 오른쪽에서 왼쪽으로 쭉 거치며 나간다.
Dispatcher Servlet과 Controller에 대해선 이미 다뤘다.
나머지 3개에 대해 알아보자.
Spring 밖 즉, Java Servlet에서 제공하는 공통처리 기능이다.
Spring Context가 아닌 Web Context에 속하기에 Web Container(e.g. Tomcat)에 의해 관리된다.
FilterChain을 통해 여러 개의 필터를 연쇄적으로 통과하게 할 수 있다.
주로 요청에 대한 권한 인증을 처리하는데 사용한다.
인터셉터와 큰 기능 차이가 있는 게 아니라서, 굳이 Spring 밖에 있는 필터를 쓰지 않고 인터셉터로 전부 처리해도 큰 상관은 없다.
javax.servlet
의 Filter
인터페이스를 이용해 구현할 수 있다.
@Component
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 커스텀 초기화
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
// 필터링 수행
// 모든 필터는 FilterConfig 오브젝트에 접근 가능.
// ServletContext에도 접근 가능.
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 커스텀 파괴자
Filter.super.destroy();
}
}
2개 이상의 필터를 생성해 체인으로 연결하고 싶다면?
@Order(n) 어노테이션을 붙이면 된다.
@Component
@Order(1)
public class MyFilter1 implements Filter {
...
}
@Component
@Order(2)
public class MyFilter2 implements Filter {
...
}
필터는 요청/응답 두 경우에 모두 작동하므로,
위의 경우, 작동 순서는
Filter1
-> Filter2
-> Spring 내부 처리 -> Filter2
-> Filter1
이 된다.
필터가 Spring Context 옆에 붙어서 동작한다고 한다면,
인터셉터는 Dispatch Servlet 옆에 붙어서 동작한다.
즉, 이 친구는 Spring 내부에 있다.
위에서도 말했지만 핸들러 매핑에 성공하면 HandlerExecutionChain
이 반환된다고 했다.
Chain. 감이 딱 오지 않는가?
여기에 인터셉터가 붙어있으면 어답터에 의해 컨트롤러가 호출되기 전에 인터셉터가 먼저 호출되고, 없으면 컨트롤러가 바로 호출된다.
하는 일은 필터와 별반 차이가 없다.
이 정도다.
다만, 필터와 다르게 HttpServletRequest & HttpServletResponse 등의 객체에 접근이 가능하기 때문에, 정보의 선처리에 용이하다.
인터셉터는 HandlerInterceptor
인터페이스를 통해 구현할 수 있다.
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws Exception {
// 컨트롤러 호출 전에 동작
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler, ModelAndView modelAndView)
throws Exception {
HandlerInterceptor.super.postHandle(request, response,
handler, modelAndView);
// 컨트롤러 응답 받고나서 뷰 리졸버가 불리기 전에 동작
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// 요청 처리가 끝난 후에 동작
HandlerInterceptor.super.afterCompletion(request, response,
handler, ex);
}
}
그리고 이번엔 경로를 추가/제거하는 과정이 필요하기 때문에 Configuration 파일을 생성하고 거기서 Bean으로 등록해주자.
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor())
.addPathPatterns("/{아무 Path}/**");
}
}
관점 지향 프로그래밍이라는 뜻이다.
⭐관점 지향 프로그래밍⭐
로직을 핵심 관심사와 부가적 관심사 (혹은 횡단 관심사)로 나누고 이 중에서 부가적 관심사 부분을 따로 모아 모듈화 하는 것.
라고 하는데 엄청 장황해서 도저히 한 번에 알아먹을 수가 없다.
그러니까 예를 들어보자.
어떤 DB를 조회해서 그 결과를 보여주는 기능이 있다고 해보자.
그런데 아무나 조회해 주기는 좀 그래서 회원만 조회가 가능하게 했다.
그리고 누가 뭘 조회해갔는지도 궁금해서 조회 후에 로그를 따로 저장하기로도 했다.
그럼 이 로직에는 관심사가 얼마나 있는가?
참고로, 관심사(concern)라는 것은 한 로직을 구성하는 몇 개의 특징적인 부분, 그 하나하나를 말한다.
이렇게 3개로 나눌 수 있을 것이다.
그럼 여기서 핵심 관심사는 무엇일까?
위쪽에서도 말했지만 이 기능은 애초에 DB를 조회해서 결과를 보여주는 게 목적이었다. 그러니까 2번 관심사가 핵심 관심사이다.
그 외의 관심사들 중에서 여러 기능에 걸쳐 계속 중복되는 관심사들이 있다.
그 대표적인 것이 인증, 트랜잭션 관리, 로깅 등이다.
데이터 추가/삭제를 하면? 기록을 안 할 건가?
데이터 추가/삭제는 아무나 막 할 수 있게 할 건가?
이렇듯 여러 개의 기능에 걸쳐서 계속해서 반복하듯 나와 어딘가에 집약되어 있지 않고, 코드 여기저기에 퍼진 채로 시스템에 복잡하게 얽혀있는 관심사를,
횡단 관심사 (cross-cutting concern) 라고 한다.
OOP에서도 말하지 않았는가.
같은 책임은 한 클래스에 모으라고.
그래야 유지보수가 쉽다고.
그래서 나온게 AOP이다.
여기저기에 퍼져 있는 횡단 관심사들을 한 부분에 모아서 관리할 수 있게 해주는 기법이라고 할 수 있다.
Spring AOP의 특징으로는 프록시 패턴 기반의 AOP 구현체를 사용한다는 것이 있다.
왜? 프록시 객체 자체가 원 기능에 더해 부가 기능이 달려있기 때문이다. 이 부가 기능이 있어야 Aspect화가 가능하다.
관련 용어부터 잠시 짚고 넘어가자.
AspectJ : AOP를 제대로 활용하기 위해 꼭 필요한 라이브러리이다.
기본적으로 제공되는 Spring AOP로는 다양한 기법(Point cut 등)을 사용하기가 힘들다.
우선 Join Point를 설정하는데에는 2가지 방법이 있다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PostLog {
}
@Retention은 해당 어노테이션 정보를 어디까지 유지할 것인지를 설정한다.
@Target은 해당 어노테이션을 어디다가 붙일 수 있는 지를 설정한다.
다음은 Advice용 클래스를 만들어야 한다.
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class LogAspect {
// @Around("execution(* com.example.demo..*.financeService.*(..))")
// demo 패키지 아래의 financeService 내부 모든 메소드에 적용
@Around("@annotation(com.example.demo.aop.PostLog)")
public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
// Around로 해도 여기에 쓰면 Before랑 별 차이 없음.
Object obj = pjp.proceed();
// After~
log.info("aop zz");
return obj;
}
}
위에서 만든 어노테이션을 활용하여, 해당 어노테이션이 붙은 곳에서 Aspect를 적용할 수 있다.
다시금 여기저기 찾아다니면 어노테이션을 붙이는 게 별로라면 위쪽에 주석처리 된 방식으로 Point Cut을 선언하여 정해진 대상들에 Aspect를 적용할 수 있다.
@PostLog
public void checkBalance(long accountId) {
...
}
어노테이션 방식을 사용했다면, 마지막으로 적용할 부분에 가서 어노테이션을 붙여주면 된다.