while(1) work();
반응형

개요

이제, Spring Web MVC의 구조를 파악했으니, Controller(handler) 구현에 집중한다.

대부분의 프로젝트에서, HandlerMapping과 HandlerAdapter 중에 사용이 쉽고 자유도가 높은 RequestMappingHandlerMapping와 RequestMappingHandlerAdapter를 사용한다.

이제부터는 RequestMappingHandlerMapping와 RequestMappingHandlerAdapter에 대해 보다 자세히 알아본다.

@Controller, @RequestMapping

앞서 살펴보았듯, 클래스에 @Controller 어노테이션을 붙이면 RequestMappingHandlerMapping이 해당 클래스 중 @RequestMapping 어노테이션을 가진 메서드들을 handler로 등록해둔다.

(또한, @Controller 어노테이션은 @Component를 포함하므로 ApplicationContext에 빈으로 등록된다.)

더불어, @RequestMapping 어노테이션을 가진 메서드는 RequestMappingHandlerAdapter가 처리 가능하다.

엄밀하게 말하자면, RequestMappingHandlerAdapter는 handler가 HandlerMethod 타입이기만 하면 처리 가능하다. (@RequestMapping 어노테이션의 존재 여부와 상관 없이)

하지만, @RequestMapping 어노테이션이 있어야 메서드가 RequestMappingHandlerMapping에 의해 handler로 등록(HandlerMethod 타입)되므로, 위와 같이 표현하였다.

다시 한번 실행 순서를 정리하자면 아래와 같다.

  1. DispatcherServlet 이 초기화되면서 ApplicationContext의 빈들 중 HandlerMapping 구현체와 HandlerAdapter 구현체를 모두 찾아 handlerMapping, handlerAdapter List에 등록해둔다.
  2. 이 때 RequestMappingHandlerMapping과 RequestMappingHandlerAdapter가 등록된다.
  3. RequestMappingHandlerMapping 객체가 생성될 때 자체적으로 모든 빈을 대상으로 @Controller 어노테이션이 붙어 있는 경우 해당 클래스의 메서드 중 @RequestMapping 어노테이션이 붙은 메서드를 mappingRegistry에 등록해둔다.
  4. 요청이 들어온다.
  5. DispatcherServlet은 getHandler(request) 메서드를 통해 handlerMapping List에 등록된 HandlerMapping 구현체에게 request를 처리할 수 있는 handler가 있는지 확인하고 handler를 찾는다.
  6. RequestMappingHandlerMapping이 가지고 있던 캐시(mappingRegistry)에 handler가 있으므로(있다고 가정하자) getHandler(request)의 결과는 해당 handler가 된다.
  7. DispatcherServlet은 getAdapter(handler) 메서드를 통해 handlerAdapter List에 등록된 HandlerAdapter 구현체에게 handler를 처리할 수 있는 adapter가 있는지 확인하고, 찾은 adapter에게 handler의 로직 호출을 위임한다. (handler가 HandlerMethod 이면 RequestMappingHandlerAdapter가 처리 가능하다)
  8. HandlerAdapter는 handler의 반환 값을 바탕으로 ModelAndView를 생성해 DispatcherServlet에게 전달한다. (혹은 내부적으로 응답이 이미 생성된 경우, null을 전달한다.)

위 실행 순서를 이해했다면, 아래의 코드가 어떻게 동작하는지 완벽히 이해했다고 볼 수 있다.

@Controller
public class HelloController {

    @GetMapping("/hello")
    public String handle(Model model) {
        model.addAttribute("message", "Hello World!");
        return "index";
    }
}

실행 결과로 index 라는 내용의 응답이 만들어진다고 생각했다면 틀렸다. (5. MVC 패턴과 Spring Web MVC 를 다시 보라)

RequestMappingHandlerAdapter는 String 타입이 반환될 경우, 해당 값으로 ModelAndView를 만들어 DispatcherServlet에게 반환한다.

따라서 ViewResolver(InternalResourceViewResolver)에 의해 index 파일의 내용이 응답으로 만들어진다.

참고로 아래 어노테이션은 @RequestMapping(method = RequestMethod.xxxxxx) 을 포함하는 메타 어노테이션이다.

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

또한, @RequestMapping 어노테이션이 class level에 추가된 경우 아래와 같이 동작한다.

@Controller
@RequestMapping("/v1")
public class HelloController {

    @GetMapping("/hello")
    public String handle(Model model) { // /v1/hello 로 요청 시 동작한다.
        model.addAttribute("message", "Hello World!");
        return "index";
    }
}

@ResponseBody, @RestController

만약, String 타입으로 반환했을 때 해당 값이 그대로 응답으로 작성되길 원한다면 메서드에 @ResponseBody 어노테이션을 추가하면 된다.

@ResponseBody 어노테이션이 추가되면, RequestMappingHandlerAdapter는 MessageConverter를 통해 반환 값을 가지고 응답을 직접 생성하고 DispatcherServlet에게 ModelAndView를 전달하지 않는다. (null을 전달한다.)

  • 반환 값이 String이면 StringHttpMessageConverter를 사용해 해당 값이 그대로 응답 본문이 된다.
  • 반환 값이 Object이면 MappingJackson2HttpMessageConverter를 사용해 JSON으로 변환된 문자열이 응답 본문이 된다.

더불어, DispatcherServlet은 ModelAndView를 받지 못하면 ViewResolver를 실행하지 않는다.

따라서 RequestMappingHandlerAdapter가 만든 응답이 클라이언트에 전달된다.

모든 메서드에 @ResponseBody를 기본으로 사용하기 위해서는 @Controller 어노테이션 대신 @RestController 어노테이션(@Controller 어노테이션을 포함하고 있는 메타 어노테이션)을 사용하면 된다.

@RestController
public class MyController {

    @RequestMapping("/members")
    public List<Member> getAllMembers() {...}

}

헤더, 매개변수 매핑

요청 헤더의 Content-type (요청 본문의 content type)과 Accept (클라이언트가 응답으로 받고자 하는 content-type) 값을 가지고 요청을 매핑할 수 있다.

@PostMapping(path = "/pets", consumes = "application/json") 
public void addPet(@RequestBody Pet pet) {
    // ...
}

위 코드는 Content-type이 application/json 인 경우에만 동작한다.

@GetMapping(path = "/pets/{petId}", produces = "application/json") 
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    // ...
}

위 코드는 Accept이 application/json 인 경우에만 동작한다.

특정 헤더의 값에 따라 매핑하고자 하는 경우 아래와 같이 사용한다.

@GetMapping(path = "/pets", headers = "myHeader=myValue") 
public void findPet(@PathVariable String petId) {
    // ...
}

또, 파라미터의 조건에 따라 매핑할 수도 있다.

@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") 
public void findPet(@PathVariable String petId) {
    // ...
}

Handler 매개변수

Handler에는 여러 타입의 매개변수를 유연하게 사용할 수 있으며, 심지어 순서조차 상관 없다.

아래 페이지에 있는 매개변수를 사용해 값을 주입받아 사용할 수 있다.

https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-methods

자주 사용되는 내용은 아래와 같다.

@PathVariable

URL을 통해 값을 받고자 하는 경우, URL을 PathPattern 문법으로 작성하고 @PathVariable 어노테이션을 사용하면 된다.

@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
    // ...
}

기존에는 AntPathMatcher 클래스를 사용해 URL 패턴을 분석했지만, Spring 6.0부터는 PathPattern 클래스(Spring 5.3에서 도입)가 기본적으로 사용된다.

  • "/resources/ima?e.png"경로 세그먼트에서 한 문자 일치
  • "/resources/*.png"경로 세그먼트에서 0개 이상의 문자와 일치
  • "/resources/**"여러 경로 세그먼트 일치
  • "/projects/{project}/versions"경로 세그먼트를 일치시키고 변수로 캡처
  • "/projects/{project:[a-z]+}/versions"정규식으로 변수 일치 및 캡처

아래와 같이 class level에서도 사용할 수 있다.

@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

    @GetMapping("/pets/{petId}")
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        // ...
    }
}

@RequestParam

URL의 매개변수(query parameter)의 값을 주입받을 수 있다.

@GetMapping("/") 
public String setupForm(@RequestParam("petId") int petId) { // /?petId=Ace
    // ...
}

@RequestHeader

헤더의 값을 주입받을 수 있다.

@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, 
        @RequestHeader("Keep-Alive") long keepAlive) { 
    //...
}

@CookieValue

쿠키 값을 주입받을 수 있다.

@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { 
    //...
}

@ModelAttribute

URL의 매개변수들을 이용해 객체로 변환(바인딩) 시켜 주입받을 수 있다.

더불어 value(아래 코드에서는 account)라는 이름으로 model에 자동으로 객체가 추가된다.

@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account accountObj) { 
    // ...
}

@RequestBody

요청 본문을 객체로 변환시켜 주입받을 수 있다.

@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
    // ...
}

Handler 반환 타입

Handler는 여러 타입을 반환할 수 있으며, HandlerMappingHandlerAdapter에 의해 적절한 응답으로 변환(MessageConverter)되거나 ModelAndView로 변환되어 DispatcherServlet에게 반환되어 ViewResolver에 의해 처리된다.

지원하는 반환 타입은 아래와 같다.

https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-return-types

@ResponseBody

@ResponseBody 어노테이션 추가 시, DispatcherServlet에게 ModelAndView 대신 null을 반환한다.

대신 HandlerMappingHandlerAdapter가 모든 응답을 처리한다.

자세한 내용은 앞에서 설명한 바와 같다.

ResponseEntity

ResponseEntity 타입 반환을 통해 상태 코드를 포함한 응답을 만들어 반환할 수 있다.

ResponseEntity가 반환되는 경우 @ResponseBody 없이도 MessageConverter가 응답을 처리한다.

@GetMapping("/something")
public ResponseEntity<String> handle() {
    String body = ... ;
    String etag = ... ;
    return ResponseEntity.ok().eTag(etag).body(body);
}

CORS

CORS(Cross-Origin Resource Sharing)를 처리하기 위해 @Cross-Origin 어노테이션을 사용한다.

@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
    // ...
}

위 코드의 경우 모든 origin과 header에 대해 CORS를 허용한다.

아래와 같이 class level에 어노테이션 추가가 가능하고, 상세 옵션을 지정할 수 있다.

@CrossOrigin(origins = "<https://domain2.com>", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @GetMapping("/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}

Filter나 WebMvcConfigurer를 통해 전역으로 설정할 수도 있다.

Spring Security를 이용하는 경우 일반적으로 Spring Security의 CorsFilter를 이용한다.

반응형

'핥아먹기 시리즈 > Spring Web MVC 핥아먹기' 카테고리의 다른 글

10. 마무리  (2) 2023.01.02
8. 예외 처리  (0) 2023.01.02
7. Filter와 Handler Interceptor  (0) 2023.01.02
6. DispatcherServlet 사용 및 MVC 패턴 구현  (0) 2023.01.02
5. MVC 패턴과 Spring Web MVC  (0) 2023.01.02
profile

while(1) work();

@유호건

❤️댓글은 언제나 힘이 됩니다❤️ 궁금한 점이나 잘못된 내용이 있다면 댓글로 남겨주세요.

검색 태그