[Spring] Spring Web MVC

얄루얄루·2022년 12월 5일
0

Spring

목록 보기
4/14

Spring Web MVC란?

Spring에서 제공하는 웹 모듈로 MVC 패턴 즉, Model, View, Controller로 나뉘는 세 요소를 활용해 사용자의 HTTP 요청을 처리하고 응답을 준다.

이 응답은 단순 텍스트 일 수도, RESTful한 응답일 수도, View를 포함한 시스템일 경우 완성된 html 일 수도 있다.

다양한 요청을 처리하고 응답하기 위해 Web MVC는 계속 요청이 올 때까지 대기를 하고 있다가 요청이 오면 그 즉시 처리하고 응답을 보낸다.

Architecture

가장 많은 곳에 관여하는 듯한 Dispatcher Servlet이라고 하는 녀석이 있는데, 이 녀석은 Controller의 일종이다.

이렇게 가장 앞단에서 유저의 유청을 받는 컨트롤러를 Front Controller라고 한다.

이 프론트 컨트롤러를 시작으로 진행되는 과정은 다음과 같다.

  1. 프론트 컨트롤러가 요청을 받고, 본격적인 로직이 시작되기 전에 요청에 대한 선처리 작업을 수행한다. (e.g. 지역 정보 결정, 멀티파트 파일 업로드 처리)
  2. 핸들러 매핑을 통해 해당 요청을 어떤 핸들러(컨트롤러)가 처리해야 할 지 결정한다.
    • 모든 컨트롤러 Bean은 RequestMappingHandlerMapping에 의해 HashMap 으로 관리된다.
    • 요청의 Http Method, URI 등을 참조해 적절한 핸들러를 찾는다.
    • ⭐찾으면 HandlerExecutionChain을 반환한다.
  3. 요청을 컨트롤러로 전달할 핸들러 어답터를 찾아서 요청을 전달한다.
    • 공통적인 pre/post-processing이 요구되기에 어답터를 통해 전달한다.
  4. 핸들러 어답터가 컨트롤러에 요청을 넘긴다.
  5. 컨트롤러가 서비스를 호출해 비즈니스 로직을 처리한다.
  6. 핸들러 어답터가 반환값을 받아 후처리 한 후 프론트 컨트롤러에 전달한다.
  7. 요청이 처리되면 뷰 리졸버를 통해 이름에 맞는 view를 선택한다.
  8. model을 넣고 view를 생성해 응답을 보낸다.

view를 생성하는 부분은 어떤 형식의 응답을 보내는가에 따라 다르다.

REST API를 구성하였다면, view 생성은 프론트단에서 처리가 되므로 그저 모델을 알맞는 형식으로 던져주면 된다.

Controller

@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을 쓰는지, 또 그럴 때 헤더에는 무엇이 들어가야 하는지, 바디에는 또 무엇을 넣어야 하는지, 그런 것이 자세하게 알고 싶다면 여기를 참고해보자.

파라미터 넘기는 법

https://news.naver.com/main/list.naver?mode=LPOD&mid=sec&sid1=001&sid2=140&oid=001&isYeonhapFlash=Y&aid=0013622862

위는 방금 막 월드컵에 대한 아무 기사 주소나 가지고 온 것이다.

주소를 잘 보면 ? 뒤로 mode=LPOD&mid=sec&...&aid=0013622862 라고 막 뭔가가 붙어 있는데 이것들이 파라미터이다.

https://news.naver.com/main/list.naver

이 주소에 들어가면 아마 기사가 엄청나게 많을 것이다.

그렇기 때문에 이 값, 저 값을 주어 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) {
	...
}

헤더는 필요없다면 꼭 받지 않아도 된다.

Filter, Interceptor, AOP

아래 그림은 요청이 들어올 때, 응답이 나갈 때의 흐름이다.

요청이 들어올 때는 왼쪽에서 오른쪽으로 쭉 거치며 들어가고,

응답이 나갈 때는 오른쪽에서 왼쪽으로 쭉 거치며 나간다.

Dispatcher Servlet과 Controller에 대해선 이미 다뤘다.

나머지 3개에 대해 알아보자.

Filter

Spring 밖 즉, Java Servlet에서 제공하는 공통처리 기능이다.

Spring Context가 아닌 Web Context에 속하기에 Web Container(e.g. Tomcat)에 의해 관리된다.

FilterChain을 통해 여러 개의 필터를 연쇄적으로 통과하게 할 수 있다.

주로 요청에 대한 권한 인증을 처리하는데 사용한다.

인터셉터와 큰 기능 차이가 있는 게 아니라서, 굳이 Spring 밖에 있는 필터를 쓰지 않고 인터셉터로 전부 처리해도 큰 상관은 없다.

javax.servletFilter 인터페이스를 이용해 구현할 수 있다.

@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이 된다.

Interceptor

필터가 Spring Context 옆에 붙어서 동작한다고 한다면,

인터셉터는 Dispatch Servlet 옆에 붙어서 동작한다.

즉, 이 친구는 Spring 내부에 있다.

위에서도 말했지만 핸들러 매핑에 성공하면 HandlerExecutionChain이 반환된다고 했다.

Chain. 감이 딱 오지 않는가?

여기에 인터셉터가 붙어있으면 어답터에 의해 컨트롤러가 호출되기 전에 인터셉터가 먼저 호출되고, 없으면 컨트롤러가 바로 호출된다.

하는 일은 필터와 별반 차이가 없다.

  • 권한 확인 및 인증의 공통적인 작업
  • API호출에 대한 logging
  • pre/post-processing

이 정도다.

다만, 필터와 다르게 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}/**");
    }
}

AOP (Aspect-Oriented Programming)

관점 지향 프로그래밍이라는 뜻이다.

⭐관점 지향 프로그래밍⭐
로직을 핵심 관심사와 부가적 관심사 (혹은 횡단 관심사)로 나누고 이 중에서 부가적 관심사 부분을 따로 모아 모듈화 하는 것.

라고 하는데 엄청 장황해서 도저히 한 번에 알아먹을 수가 없다.

그러니까 예를 들어보자.

어떤 DB를 조회해서 그 결과를 보여주는 기능이 있다고 해보자.

그런데 아무나 조회해 주기는 좀 그래서 회원만 조회가 가능하게 했다.

그리고 누가 뭘 조회해갔는지도 궁금해서 조회 후에 로그를 따로 저장하기로도 했다.

그럼 이 로직에는 관심사가 얼마나 있는가?

참고로, 관심사(concern)라는 것은 한 로직을 구성하는 몇 개의 특징적인 부분, 그 하나하나를 말한다.

  1. 보안/인증 - 회원인지 아닌지에 관심
  2. DB조회 - 데이터를 읽어오는 것에 관심
  3. 로깅 - 결과를 기록하는 것에 관심

이렇게 3개로 나눌 수 있을 것이다.

그럼 여기서 핵심 관심사는 무엇일까?

위쪽에서도 말했지만 이 기능은 애초에 DB를 조회해서 결과를 보여주는 게 목적이었다. 그러니까 2번 관심사가 핵심 관심사이다.

그 외의 관심사들 중에서 여러 기능에 걸쳐 계속 중복되는 관심사들이 있다.

그 대표적인 것이 인증, 트랜잭션 관리, 로깅 등이다.

데이터 추가/삭제를 하면? 기록을 안 할 건가?

데이터 추가/삭제는 아무나 막 할 수 있게 할 건가?

이렇듯 여러 개의 기능에 걸쳐서 계속해서 반복하듯 나와 어딘가에 집약되어 있지 않고, 코드 여기저기에 퍼진 채로 시스템에 복잡하게 얽혀있는 관심사를,

횡단 관심사 (cross-cutting concern) 라고 한다.

OOP에서도 말하지 않았는가.

같은 책임은 한 클래스에 모으라고.

그래야 유지보수가 쉽다고.

그래서 나온게 AOP이다.

여기저기에 퍼져 있는 횡단 관심사들을 한 부분에 모아서 관리할 수 있게 해주는 기법이라고 할 수 있다.

Spring AOP의 특징으로는 프록시 패턴 기반의 AOP 구현체를 사용한다는 것이 있다.

왜? 프록시 객체 자체가 원 기능에 더해 부가 기능이 달려있기 때문이다. 이 부가 기능이 있어야 Aspect화가 가능하다.

용어

관련 용어부터 잠시 짚고 넘어가자.

  • Aspect: 횡단 관심사를 모듈화 한 것이다. Point Cut + Advice라고 봐도 무방.
  • Advice: 'before', 'after', 'around' 등과 같이 정해진 Join Point에서 실질적인 일을 수행하는 동작 가능한 코드.
  • Point Cut: Join point 중에서 해당 Aspect를 적용할 대상을 뽑을 조건식.
  • Join Point: 모듈화된 특정 기능이 실행 될 수 있는 포인트.
  • Target Object: Advice가 적용될 대상 오브젝트.
  • AOP proxy: 대상 오브젝트에 Aspect를 적용하는 경우 Advice를 덧붙이기 위해 하는 작업을 AOP proxy라고 함. 주로 CGLIB (실행 중에 실시간으로 코드를 생성하는 라이브러리) 프록시를 사용하여 프록싱 처리를 한다.
  • Weaving: Advice를 비즈니스 로직 코드에 삽입하는 것.

AspectJ : AOP를 제대로 활용하기 위해 꼭 필요한 라이브러리이다.
기본적으로 제공되는 Spring AOP로는 다양한 기법(Point cut 등)을 사용하기가 힘들다.

예시

우선 Join Point를 설정하는데에는 2가지 방법이 있다.

  • 커스텀 어노테이션을 활용
  • Point Cut으로 선언
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PostLog {
}

@Retention은 해당 어노테이션 정보를 어디까지 유지할 것인지를 설정한다.

  • RetentionPolicy.RUNTIME : 컴파일 이후에도 JVM에 의해 참조 가능.
  • RetentionPolicy.CLASS : 컴파일러가 클래스 참조할 떄까지 유효.
  • RetentionPolicy.SOURCE : 컴파일 이후 없어짐.

@Target은 해당 어노테이션을 어디다가 붙일 수 있는 지를 설정한다.

  • ElementType.PACKAGE : 패키지 선언시
  • ElementType.TYPE : 타입 선언시
  • ElementType.CONSTRUCTOR : 생성자 선언시
  • ElementType.FIELD : 멤버 변수 선언시
  • ElementType.METHOD : 메소드 선언시
  • ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언시
  • ElementType.LOCAL_VARIABLE : 지역 변수 선언시
  • ElementType.TYPE_PARAMETER : 매개 변수 타입 선언시

다음은 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) {
 	...
 }

어노테이션 방식을 사용했다면, 마지막으로 적용할 부분에 가서 어노테이션을 붙여주면 된다.

References

profile
시간아 늘어라 하루 48시간으로!

0개의 댓글