Spring & SpringBoot

스프링 MVC 1편

땅콩콩 2023. 3. 29. 20:16

* 인프런 김영한님의 강의 "스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술"을 듣고 정리한 내용입니다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1

스프링의 전체구조

 

Dispatcher servlet?

스프링 mvc도 프론트 컨트롤러 패턴으로 구현되어있고 그 프론트 컨트롤러 역할을 하는것이 Dispatcher servlet이다.
스프링부트는 이 Dispatcher servlet을 서블릿으로 자동으로 등록하면서 모든 경로에 대해 매핑한다. (urlpatterns="/")
서블릿 호출 > FrameworkServlet에서 오버라이딩한 service()호출 > 여러 메서드가 호출 > DispacherServlet의 doDispatcher()호출
여기서 이 doDispatcher()가 스프링mvc구조의 핵심로직을 모두 가지고있다.

 

1. 핸들러 조회 = 핸들러 매핑으로 요청된 url에 매핑된 핸들러(컨트롤러) 조회

2. 핸들러 어댑터 조회 = 핸들러를 실행할 수 있는 핸들러 어댑터 조회

3. 핸들러 어댑터 실행 

4. 핸들러 실행 = 핸들러 어댑터가 실제 핸들러를 실행

5. ModelAndView반환 = 핸들러 어댑터가 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환

6. viewResolver호출 = 뷰 리졸버를 찾고 실행한다

7. View반환 = 뷰리졸버가 뷰의 논리이름을 물리이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체 반환

8 뷰 렌더링 = 반환받은 뷰 객체를 통해서 뷰를 렌더링한다.

 

mvc 기본기능

1. 요청매핑

@RestController //반환값으로 view를 찾지 않고 http바디에 바로 입력한다.
public class MappingController {
    private Logger log = LoggerFactory.getLogger(getClass());

    //1.기본
    @RequestMapping("/hello-basic")
    public String helloBasic(){
        log.info("helloBasic");
        return "ok";
    }

    //2.http method 매핑
    @RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
    public String mappingGetV1() {
        log.info("mappingGetV1");
        return "ok";
    }

    //3.http method 매핑 축약
    @GetMapping(value = "/mapping-get-v2")
    public String mappingGetV2() {
        log.info("mapping-get-v2");
        return "ok";
    }

    //4.PathVariable(경로변수)사용
    @GetMapping("/mapping/{userId}")
    public String mappingPath(@PathVariable("userId") String data){
        log.info("mappingPath userId={}", data);
        return "ok";
    }

    //5.PathVariable(경로변수)다중사용
    @GetMapping("/mapping/users/{userId}/orders/{orderId}")
    public String mappingPath(@PathVariable String userId, @PathVariable Long orderId){
        log.info("mappingPath userId={}, orderId={}", userId, orderId);
        return "ok";
    }

    //6.특정 파라미터 조건 매핑
    @GetMapping(value = "/mapping-param", params = "mode=debug")
    public String mappingParam(){
        log.info("mappingParam");
        return "ok";
    }

    //7.특정 헤더 조건 매핑. http 헤더를 사용한다.
    @GetMapping(value = "/mapping-header", headers = "mode=debug")
    public String mappingHeader() {
        log.info("mappingHeader");
        return "ok";
    }

    //8.미디어타입 조건 매핑 - http 요청 content-type
    //consumes > http 요청 header의 content-type
    @PostMapping(value = "/mapping-consume", consumes = "application/json")
    public String mappingConsumes() {
        log.info("mappingConsumes");
        return "ok";
    }

    //9.미디어타입 조건 매핑 - http 요청 accept
    //produces > http 요청 header의 accept기반으로 미디어타입 매핑
    @PostMapping(value = "/mapping-produce", produces = "text/html")
    public String mappingProduces() {
        log.info("mappingProduces");
        return "ok";
    }
}

 

2. 요청매핑 api 예시

@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {

    @GetMapping
    public String user(){
        return "get user";
    }

    @PostMapping
    public String addUser(){
        return "post user";
    }

    @GetMapping("/{userId}")
    public String findUser(@PathVariable String userId){
        return "get userId=" + userId;
    }

    @PatchMapping("/{userId}")
    public String updateUser(@PathVariable String userId){
        return "update userId=" + userId;
    }

    @DeleteMapping("/{userId}")
    public String deleteUser(@PathVariable String userId){
        return "delete userId=" + userId;
    }
}

 

3. http 헤더정보 조회

@Slf4j
@RestController
public class RequestHeaderController {

    @RequestMapping("/headers")
    public String headers(HttpServletRequest request,
                          HttpServletResponse response,
                          HttpMethod httpMethod,
                          Locale locale,
                          
                          //모든 http헤더를 MultiValueMap형식으로 조회한다.
                          @RequestHeader MultiValueMap<String, String> headerMap,
                          
                          //특정 http헤더를 조회한다.
                          @RequestHeader("host") String host,
                          
                          //특정 cookie를 조회한다.
                          @CookieValue(value = "myCookie", required = false) String cookie
                          ){
        log.info("request={}", request);
        log.info("response={}", response);
        log.info("httpMethod={}", httpMethod);
        log.info("locale={}", locale);
        log.info("headerMap={}", headerMap);
        log.info("header host={}", host);
        log.info("myCookie={}", cookie);
        return "ok";
    }
}

 

http 요청 데이터 조회

클라이언트가 서버로 요청 데이터를 전달할때는 주로 3가지 방법을 사용한다.

1. GET : 쿼리 파라미터 요청
2. POST : HTML form 요청
3. HTTP message body에 데이터를 직접 담아서 요청

이중 GET 쿼리 파라미터 요청방식과 POST HTMLform요청방식은 서버입장에서 형식이 동일하기 때문에 구분없이 조회할 수 있다. 이것을 요청 파라미터 조회(request parameter)라고 하고, @RequestParam @ModelAttribute를 이용해 처리할 수 있다.

하지만 요청 파라미터와 다르게 @RequestBody(HTTP message converter사용)를 통해 데이터가 직접 넘어오는 경우는 그렇게 처리할 수 없고, InputStream으로 직접 읽어오는 데이터를 HttpMessageConverter를 사용해서 문자나 객체로 변환하여 처리한다.

이렇게 http에서 요청한 데이터를 읽어오는 방식은 크게 두가지이다. 하나씩 살펴보자.

 

4. 요청 파라미터 조회

 

GET 쿼리 파라미터 전송방식, POST html form전송방식으로 온 데이터를 조회할 수 있는 방법이다.

이런 경우 @RequestParam, @ModelAttribute를 사용해서 요청 파라미터들을 처리할 수 있다.

 

@Slf4j
@Controller
public class RequestParamController {

    //1.HttpServletRequest 사용
    @RequestMapping("/request-param-v1")
    public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));
        log.info("username = {}, age = {}", username, age);
        //반환타입이 없으면서 응답에 값을 직접 넣어두면 view조회 안함.
        response.getWriter().write("ok");
    }

    //2.스프링이 제공하는 @RequestParam 사용
    //@RequestParam = 파라미터 이름으로 바인딩.
    //@ResponseBody = view 조회 무시하고 http message body에 직접 내용 입력.
    @ResponseBody
    @RequestMapping("/request-param-v2")
    public String requestParamV2(
            @RequestParam("username") String memberName,
            @RequestParam("age") int memberAge){
        log.info("username = {}, age = {}", memberName, memberAge);
        return "ok";
    }

    //3.@RequestParam 사용. http파라미터 이름이 변수 이름과 같으면 괄호안의 내용은 생략 가능.
    @ResponseBody
    @RequestMapping("/request-param-v3")
    public String requestParamV3(
            @RequestParam String username,
            @RequestParam int age){
        log.info("username = {}, age = {}", username, age);
        return "ok";
    }

    //4.@RequestParam 사용. String, int등의 단순 타입이면 @RequestParam도 생략 가능.
    @ResponseBody
    @RequestMapping("/request-param-v4")
    public String requestParamV4(String username, int age){
        log.info("username = {}, age = {}", username, age);
        return "ok";
    }

    //5. required = true 옵션. 파라미터 필수 여부를 표현
    @ResponseBody
    @RequestMapping("/request-param-required")
    public String requestParamRequired(
            @RequestParam(required = true) String username,
            @RequestParam(required = true) int age){
        log.info("username = {}, age = {}", username, age);
        return "ok";
    }

    //6.defaultValue 옵션. 파라미터에 값이 없는 경우 기본값을 설정할 수 있다.
    @ResponseBody
    @RequestMapping("/request-param-default")
    public String requestParamDefault(
            @RequestParam(required = true, defaultValue = "guest") String username,
            @RequestParam(required = true, defaultValue = "-1") int age){
        log.info("username = {}, age = {}", username, age);
        return "ok";
    }

    //7.파라미터를 Map으로 조회.
    //파라미터의 값이 1개가 확실하다면 Map을 써도 되지만, 그렇지않다면 MultiValueMap을 사용해야 함.
    @ResponseBody
    @RequestMapping("/request-param-map")
    public String requestParamMap(@RequestParam Map<String, Objects> paramMap){
        log.info("username = {}, age = {}", paramMap.get("username"), paramMap.get("age"));
        return "ok";
    }

    //8.@ModelAttribute 사용.
    // 필요한 객체에 값을 넣어서 반환해주어야 하는 과정을 스프링이 자동화해주는 기능이다.
    // ModelAttribute 애노테이션에 의헤 HelloData객체가 생성되고,요청 파라미터의 값이 자동으로 들어간다.
    @ResponseBody
    @RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData){
        log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }

    //9.@ModelAttribute는 생략이 가능하지만 @RequestParam도 생략이 가능하므로 혼란이 발생할 수 있다.
    @ResponseBody
    @RequestMapping("/model-attribute-v2")
    public String modelAttributeV2(HelloData helloData){
        log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }
}
애노테이션 생략

애노테이션을 생략했을 경우, String int Integer같은 단순타입이면 @RequestParam을, 나머지는 @ModelAttribute를 생략한 것으로 간주된다.

 

5. http 요청 메시지 조회 (단순 텍스트)

 

HTTP message로 온 텍스트형식 데이터를 조회할 수 있는 방법이다.

이런 경우, 위의 방식은 사용할 수 없고 Inputstream data를 문자나 스트링으로 변환해서 처리해야 한다.

@Slf4j
@Controller
public class RequestBodyStringController {

    //1.HttpServletRequest로 request를 받아와서 inputstream 뽑아내기
    @PostMapping("/request-body-string-v1")
    public void requestBodyString(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        log.info("messageBody = {}", messageBody);
        response.getWriter().write("ok");
    }

    //2.inputstream으로 바로 받아오기
    @PostMapping("/request-body-string-v2")
    public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException {
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        log.info("messageBody = {}", messageBody);
        responseWriter.write("ok");
    }

    //3.HttpEntity를 통해 헤더와 바디 조회
    // 메시지 바디 정보를 직접 조회하고 직접 반환.
    // HttpMessageConverter 사용 (StringHttpMessageConverter)
    @PostMapping("/request-body-string-v3")
    public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
        String messageBody = httpEntity.getBody();
        log.info("messageBody = {}", messageBody);
        return new HttpEntity<>("ok");
    }

    //4.@RequestBody로 바디 조회
    // HttpMessageConverter 사용 (StringHttpMessageConverter)
    // @RequestBody를 통해 Http 메시지에서 데이터를 꺼내고 messageBody에 저장한다.
    // @ResponseBody를 사용하면 응답결과를 http바디에 직접 담아 전달할 수 있다.
    @ResponseBody
    @PostMapping("/request-body-string-v4")
    public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {
        log.info("messageBody = {}", messageBody);
        return "ok";
    }
}

 

 

6. http 요청 메시지 조회 (json)

 

HTTP message로 온 json 형식 데이터를 조회할 수 있는 방법이다.

@RequestBody(요청)  = JSON요청 > HttpMessageConverter > 객체
@ResponseBody(응답) = 객체 > HttpMessageConverter > JSON응답
@Slf4j
@Controller
public class RequestBodyJsonController {
    private ObjectMapper objectMapper = new ObjectMapper();

    //1.HttpServletRequest로 stream데이터를 읽어와서 직접 문자로 변횐
    @PostMapping("/request-body-json-v1")
    public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        log.info("messageBody = {}", messageBody);
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
        response.getWriter().write("ok");
    }

    //2.@RequestBody, @ResponseBody 사용
    // StringHttpMessageConverter 사용됨
    //그러나 다시 json으로 변환하는 과정을 거쳐야하는 로직.
    @ResponseBody
    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
        log.info("messageBody = {}", messageBody);
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }

    //3.@RequestBody, @ResponseBody 사용 (string반환)
    // MappingJackson2HttpMessageConverter 사용됨
    // @RequestBody생략시 @ModelAttribute가 생략된것으로 간주되므로 생략 불가
    @ResponseBody
    @PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody HelloData data) {
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

    //4.HttpEntity 사용
    @ResponseBody
    @PostMapping("/request-body-json-v4")
    public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
        HelloData data = httpEntity.getBody();
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

    //5.@RequestBody, @ResponseBody 사용 (객체 반환)
    // MappingJackson2HttpMessageConverter 사용됨
    @ResponseBody
    @PostMapping("/request-body-json-v5")
    public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return data;
    }
}

 

http 응답 데이터 조회

서버에서 응답 데이터를 만드는 방법은 크게 3가지이다.

1. 정적 리소스
2. 뷰 템플릿 사용
3. HTTP 메세지 사용

 

7. http 응답 (정적 리소스, 뷰 템플릿)

@Controller
public class ResponseViewController {

    //1.ModelAndView 반환
    @RequestMapping("/response-view-v1")
    public ModelAndView responseViewV1(){
        ModelAndView mav = new ModelAndView("response/hello")
                .addObject("data", "hello!");
        return mav;
    }

    //2.String 반환
    //@ResponseBody가 없으면 뷰리졸버가 "response/hello"로 실행, 뷰를 찾고 렌더링.
    //@ResponseBody가 있으면 뷰리졸버 실행하지 않고 "response/hello"를 http메시지 바디에 직접
    @RequestMapping("/response-view-v2")
    public String responseViewV2(Model model){
        model.addAttribute("data", "hello");
        return "response/hello";
    }

    //3.void 반환
    //명시적이지 않아 비추천.
    @RequestMapping("/response/hello")
    public void responseViewV3(Model model){
        model.addAttribute("data", "hello");
    }
}

 

 

8. http 응답 (http api, 메시지 바디에 직접 입력)

@Slf4j
@Controller
public class ResponseBodyController {
    
    //1.HttpServletResponse 객체를 통해 메시지 바디에 직접 값을 전달.
    @GetMapping("/response-body-string-v1")
    public void responseBodyV1(HttpServletResponse response) throws IOException {
        response.getWriter().write("ok");
    }

    //2.ResponseEntity 반환
    //ResponseEntity는 httpEntity를 상속받았다.
    //httpEntity는 http 메시지의 헤더+바디 정보를 가지고 있고, ResponseEntity는 여기에 http 응답코드 추가 가능.
    @GetMapping("/response-body-string-v2")
    public ResponseEntity<String> responseBodyV2() {
        return new ResponseEntity<>("ok", HttpStatus.OK);
    }

    //3.@ResponseBody 사용
    //view를 사용하지 않고 http 메시지 컨버터를 통해 http 메시지 직접 입력 가능.
    @ResponseBody
    @GetMapping("/response-body-string-v3")
    public String responseBodyV3() {
        return "ok";
    }

    //4.ResponseEntity 반환(json 타입)
    @GetMapping("/response-body-json-v1")
    public ResponseEntity<HelloData> responseBodyJsonV1(){
        HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);
        return new ResponseEntity<>(helloData, HttpStatus.OK);
    }

    //5.@ResponseBody를 쓸때는 @ResponseStatus로 http 응답코드 나타내는것이 편리함.
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    @GetMapping("/response-body-json-v2")
    public HelloData responseBodyJsonV2(){
        HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);
        return helloData;
    }
}
@RestController

@Controller대신 @RestController를 쓰면 해당하는 컨트롤러에 모두 @ResponseBody가 적용되는 효과가 있다.
따라서 뷰 템플릿을 사용하는 것이 아니라, http 메시지 바디에 직접 데이터를 입력한다. 
Rest API를 만들때 사용하는 컨트롤러이다.

 

 

HTTP 메시지 컨버터

http api처럼 json데이터를 http메시지바디에서 직접 읽거나 쓰는 경우 http 메시지 컨버터를 사용하면 편리하다.

 

스프링 mvc는 다음의 경우에 http 메시지 컨버터를 사용한다.

1. http 요청 : @RequestBody, HttpEntity(RequestEntity)

2. http 응답 : @ResponseBody, HttpEntity(RequestEntity)

 

http 메시지 컨버터는 요청과 응답에 모두 사용되고, 스프링부트는 정말 다양한 종류의 메시지 컨버터를 제공하기 때문에 대상 클래스 타입미디어 타입 둘을 체크해서 사용할 메시지 컨버터를 결정한다.

 

그 중 주로 사용되는 메시지 컨버터는 아래와 같다.

0. ByteArrayHttpMessageConverter : byte[] 데이터를 처리한다. 클래스타입은 byte[], 미디어타입은 */*

1. StringHttpMessageConverter : String 문자로 데이터를 처리한다. 클래스타입은 String, 미디어타입은 */*

2. MappingJackson2HttpMessageConverter : 클래스타입은 객체 또는 HashMap, 미디어타입은 application/json

 

이 http 메시지 컨버터가 사용되는 위치는 어딜까?

애노테이션 기반의 컨트롤러, 즉 @RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter의 동작방식을 살펴보자.

<요청>

1. 애노테이션 기반의 컨트롤러를 처리하는 RequestMappingHandlerAdapter가 Argument resolver를 호출.

2. Argument resolver가 수많은 형태의 파라미터를 유연하게 처리하고 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터값(객체)을 생성. (여기서 Argument resolverhttp message converter를 사용해서 필요한 객체를 생성하는 것이다.)

3. 파라미터값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨준다.

 

<응답>

@ResponseBody, HttpEntity를 처리하는 ReturnValueHandler가 있고, 여기에서 http message converter를 호출해서 응답 결과를 만든다.