본문 바로가기
Backend/Spring | SpringBoot

[SpringBoot] HTTP 요청 조회 애노테이션

by 2245 2023. 8. 25.

목차

     

    전체 구조

     

     

    HTTP 요청 매핑

    회원 관리 HTTP API를 만든다 생각하고 매핑을 어떻게 하는지 알아봅시다.

    (URL 매핑에만 집중합니다.)

    회원 관리 API

    • 회원 목록 조회: GET /users
    • 회원 등록: POST /users
    • 회원 조회: GET /users/{userId}
    • 회원 수정: PATCH /users/{userId}
    • 회원 삭제: DELETE /users/{userId}
    @RestController
    @RequestMapping("/mapping/users")
    public class MappingController {
    
        /**
         * 회원 목록 조회
         * GET /mapping/users
         */
        @GetMapping
        public String users() {
            return "get users";
        }
    
        /**
         * 회원 등록
         * POST /mapping/users
         */
        @PostMapping
        public String addUser() {
            return "post user";
        }
    
        /**
         * 회원 조회
         * GET /mapping/users/{userId}
         */
        @GetMapping("/{userId}")
        public String findUser(@PathVariable String userId) {
            return "get userId = " + userId;
        }
    
        /**
         * 회원 수정
         * PATCH /mapping/users/{userId}
         */
        @PatchMapping("/{userId}")
        public String updateUser(@PathVariable String userId) {
            return "update userId=" + userId;
        }
    
        /**
         * 회원 삭제
         * DELETE /mapping/users/{userId}
         */
        @DeleteMapping("/{userId}")
        public String deleteUser(@PathVariable String userId) {
            return "delete userId=" + userId;
        }
    
    }

     

    매핑 방법을 이해했으니, 이제부터 HTTL 요청이 보내는 데이터들을 스프링 MVC에서 어떻게 조회하는지 알아봅시다.

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

     

    요청 데이터 전달 방법 3가지 

    • GET - 쿼리 파라미터
      • /url?username=hello&age=20
      • 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함하여 전달
      • 예) 검색, 필터, 페이징 등에서 많이 사용하는 방식
    • POST - HTML Form
      • content-type: application/x-www-form-urlencoded
      • 메시지 바디에 쿼리 파라미터 형식으로 전달 username=hello&age=20
      • 예) 회원 가입, 상품 주문, HTML Form 사용
    • HTTP message body에 데이터를 직접 담아서 요청
      • HTTP API에서 주로 사용, JSON, XML, TEXT
      • 데이터 형식은 주로 JSON 사용
      • POST, PUT, PATCH

     

    1. 기본, 헤더 조회

    @Slf4j
    @RestController
    public class RequestHeaderController {
        @RequestMapping("/headers")
        public String headers(HttpServletRequest request,
                              HttpServletResponse response,
                              HttpMethod httpMethod,
                              Locale locale,
                              @RequestHeader MultiValueMap<String, String> headerMap,
                              @RequestHeader("host") String host,
                              @CookieValue(value="myCookie", required = false) String cookie) {
            
            log.info("request={}", request);        //request=org.apache.catalina.connector.RequestFacade@3800ac47
            log.info("response={}", response);      //response=org.apache.catalina.connector.ResponseFacade@6034b23d
            log.info("httpMethod={}", httpMethod);  //httpMethod=GET
            log.info("locale={}", locale);          //locale=ko_KR
            log.info("headerMap={}", headerMap);    //headerMap={host=[localhost:8080], connection=[keep-alive], sec-ch-ua=["Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"], sec-ch-ua-mobile=[?0], sec-ch-ua-platform=["Windows"], upgrade-insecure-requests=[1], user-agent=[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36], accept=[text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7], sec-fetch-site=[none], sec-fetch-mode=[navigate], sec-fetch-user=[?1], sec-fetch-dest=[document], accept-encoding=[gzip, deflate, br], accept-language=[ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7], cookie=[JSESSIONID=7EACCFF0CCB074BEF481999AC64102F8]}
            log.info("header host={}", host);       //header host=localhost:8080
            log.info("myCookie={}", cookie);        //myCookie=null
    
            return "ok";
        }
    }
    • HttpServletRequest
    • HttpServletResponse 
    • HttpMethod 
      • HTTP 메서드를 조회합니다.
      • ex) GET
    • Locale
      • Locale 정보를 조회합니다.
      • ex) ko_KR
    • @RequestHeader MultiValueMap<String, String> headerMap
      • 모든 HTTP 헤더를 MultiValueMap 형식으로 조회합니다.
      • ex) {host=[Localhost:8080], connection=[keep=alive], ...}
    • @RequestHeader("host") String host
      • 특정 HTTP 헤더를 조회합니다.
      • ex) Localhost:8080
    • @CookieValue(value="myCookie", required=false) String cookie
      • 특정 쿠키를 조회합니다.
    참고 MultiValueMap vs Map
    Map과 유사하지만, 차이점은 하나의 키에 여러 값을 받을 수 있습니다.
    HTTP Header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용합니다.
    ex) keyA=value1&keyA=value2
    MultiValueMap<String, String> map = new LinkedMultiValueMap();
    map.add("keyA", "value1");				//동일한 키에 value 삽입
    map.add("keyA", "value2");				//동일한 키에 value 삽입
    
    List<String> values = map.get("keyA");	//[value1, value2]

     

     

    2. 쿼리 파라미터, HTML Form (요청 파라미터)

    • Get 쿼리 파라미터 전송과 POST HTML Form 방식은 전송 형식은 다르지만, 데이터의 형식이 같으므로 조회에선 같은 방법을 사용할 수 있습니다.
    • 이 둘을 합쳐 간단히 요청 파라미터(Request Parameter) 조회라고 합니다.

    1. request.getParameter()

    @Slf4j
    @Controller
    public class RequestParamController {
    
        //반환 타입이 없으면서 이렇게 응답에 값을 직접 집어넣으면 view 조회 X
        @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);
    
            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";
    }

     

    파라미터의 이름이 변수 이름과 같다면 (name="xxx") 생략

    @ResponseBody
    @RequestMapping("/request-param-v3")
    public String requestParamV3(@RequestParam String username, @RequestParam int age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }

     

    @RequestParam 생략

    String, int, Integer 등 단순 타입이면 (객체 제외) @RequestParam도 생략이 가능합니다.

    @ResponseBody
    @RequestMapping("/request-param-v4")
    public String requestParamV4(String username, int age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }
    주의 requried=false 자동 적용
    @RequestParam 애노테이션을 생략하면 스프링 MVC는 내부에서 required=false를 자동으로 적용합니다. 
    참고 너무 많은 생략
    이렇게 애노테이션을 완전히 생략해도 동작은 하지만, 너무 없는 것도 과하다는 주관적 생각이 있습니다.
    @RequestParam이 있다면 명확하게 요청 파라미터에서 데이터를 읽을 수 있다는 것을 알 수 있습니다. 

     

     

    [참고] 파라미터 필수 여부 - RequestPramRequired

    • 파라미터 포함 필수 여부를 나타냅니다.
    • 기본 값이 필수(true)입니다. 
    @ResponseBody
    @RequestMapping("/request-param-required")
    public String requestParamRequired(@RequestParam(required = true) String username, @RequestParam(required = false) Integer age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }
    • @RequestParam(required = true) String username  → 반드시 포함해야 한다.
    • @RequestParam(required = false) Integer age  → 포함하지 않아도 된다.

     

    테스트

    /request-param-required

     username이 없으므로 예외가 발생합니다.

     

    주의 username에 빈 문자열 전송
    /request-param?username=
    로 전송하면 값이 있다고 판단하여 통과합니다. (null과  ""는 다릅니다.) 
    주의 기본형(primitive)에 null 입력
    /request-param?username="주희"
    로 전송하면 500에러가 발생합니다. (int age에 null 입력)
    int 형에 null을 입력하는 것은 불가능합니다.
    따라서, null을 입력받을 수 있는 Integer로 변경하거나, defaultValue를 사용해야 합니다.

     

    [참고] 기본 값 적용 - DefaultValue

    파라미터에 값이 없을 경우 대신해서 사용할 수 있는 default 값을 지정합니다.

    이때는 required=true가 의미가 없어집니다.

    @ResponseBody
    @RequestMapping("/request-param-default")
    public String requestParamDefault (
            @RequestParam(required = true, defaultValue = "guest") String username,
            @RequestParam(required = false, defaultValue = "-1") int age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }
    주의 빈 문자열
    defaultValue는 빈 문자의 경우에도, 값이 없다고 판단하여 설정한 기본 값이 적용됩니다.
    /request-param-default?username=

     

    3. Map으로 조회 - @RequestParam Map<>

    파라미터를 Map, MultiValueMap으로 조회할 수 있습니다. 

    @ResponseBody
    @RequestMapping("/request-param-map")
    public  String requestParamMap(@RequestParam Map<String, Object> paramMap) {
        log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
        return "ok";
    }
    • @RequestParam Map<>
      • Map(key=value)
    • @RequestParam MultiValueMap<>
      • MultiValueMap(key=[value1, value2, ...] (ex) key=userId, value=[id1, id2])
    • 파라미터 키의 Value값이 1개가 확실하다면 Map을 사용해도 되지만, 그렇지 않다면 MultiValueMap을 사용하도록 합시다.

     

    4. @PathVariable

    최근 HTTP API는 리소스 경로에 식별자를 넣는 스타일을 선호합니다.

    • /mapping/userA
    • /users/1
    /**
     * PathVariable(경로 변수) 사용
     * 변수명이 같으면 ("userId") 생략 가능 (@PathVariable String userId)
     */
    @GetMapping("/mapping/{userId}")
    public String mappingPath(@PathVariable("userId") String data) {
        log.info("mappingPath userId={}", data);
        return "ok";
    }
    /**
     * 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";
    }

     

    5. @ModelAttribute 객체

    • 실제 개발을 해보면, 요청 파라미터를 받아 필요한 객체를 생성한 후, setter를 통해 그 객체에 값을 넣어주어야 합니다.
    • 스프링은 이 과정을 완전히 자동으로 해주는 @ModelAttribute 기능을 제공합니다.
    @RequestParam String username;
    @RequestParam int age;
    ---
    HelloData data = new HelloData();
    data.setUsername(username);
    data.setAge(age);
    @Data
    public class HelloData {
        private String username;
        private int age;
    }
    @ResponseBody
    @RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData) {
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }
    주의 바인딩 오류
    만약, age="abc" 처럼 숫자가 들어가야할 곳에 문자가 들어가면 BindException 에러가 발생합니다.
    이런 바인딩 오류를 처리하는 방법은 검증 부분에서 다룹니다. 

     

     @ModelAttrtibute 생략

    @ResponseBody
    @RequestMapping("/model-attribute-v2")
    public String modelAttributeV2(HelloData helloData) {
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }
    • @ModelAttribute는 생략할 수 있습니다.
    • 하지만, @RequestParam도 생략할 수 있으므로 혼란이 발생할 수 있습니다.
    • 스프링은 생략 시 다음과 같은 규칙을 적용합니다.
    • String, int, Integer 같은 단순 타입 = @RequestParam
    • 나머지 = @ModelAttribute

     

    3. 메시지 바디

    • HTTP message body에 데이터를 직접 담아서 요청
    • HTTP 메시지 바디를 통해 직접 넘어오는 경우는 @RequestParam, @ModelAttribute를 사용할 수 없습니다. 

    1) 단순 텍스트(String)

    1. InputStream

    @Slf4j
    @Controller
    public class RequestBodyStringController {
    
        @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. Input, Output 스트림 파라미터

    스프링 MVC는 InputStream(Reader), OutputStream(Writer) 파라미터를 지원합니다.

    • InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
    • OutputStream(Writer): HTTP 응답 메시지 바디에 직접 결과 출력
    @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

    스프링 MVC는 HTTP header, body 정보를 편리하게 조회하기 위해 HttpEntity를 제공합니다. 

    • 메시지 바디 정보를 직접 조회
    • String을 변환 시 HttpMessageConverter 중 StringHttpMessageConverter가 사용됩니다. 
    @PostMapping("/request-body-string-v3")
    public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
        String messageBody = httpEntity.getBody();
        log.info("messageBody={}", messageBody);
    
        return new HttpEntity<>("ok");
    }
    • HttpEntity는 응답에도 사용할 수 있습니다.
      • 메시지 바디 정보를 직접 반환합니다.
      • 헤더 정보를 포함할 수 있습니다.
      • view를 조회하지 않습니다. 
    • HttpEntity를 상속받은 다음 객체들도 같은 기능을 제공합니다.
      • RequestEntity
        • 요청에서 사용합니다.
        • HttpMethod, url 정보가 추가되었습니다.
      • ResponseEntity
        • 응답에서 사용합니다.
        • HTTP 상태 코드 설정이 가능합니다.
        • ex) return new ResponseEntity<String>("Hello world", responseHeaders, HttpStatus.CREATED)
    참고 HttpMessageConverter
    스프링 MVC 내부에서 HTTP 메시지 바디를 읽어 문자나 객체로 변환하여 전달해주는데, 이 때 Http 메시지 컨버터(HttpMessageConverter)라는 기능을 사용합니다.

     

    4. @RequestBody

    • HTTP 메시지 바디를 편리하게 조회할 수 있습니다. 
    • HttpMessageConverter 중 StringHttpMessageConverter가 사용됩니다. 
    @ResponseBody
    @PostMapping("/request-body-string-v4")
    public String requestBodyStringV4(@RequestBody String messageBody) {
        log.info("messageBody={}", messageBody);
        return "ok";
    }
    참고 헤더 정보 필요하다면?
    HttpEntity를 사용하거나, @RequestHeader를 사용하면 됩니다. 
    참고 @ReseponseBody
    - 응답 결과를 HTTP 메시지 바디에 직접 담아 반환합니다. (view를 조회하지 않습니다.)
    - HttpMessageConverter 중 StringMessageConverter가 사용됩니다. 

     

     

    2) JSON

    이번에는 HTTP API에서 주로 사용하는 JSON 데이터 형식에 대해 알아봅시다.

    {"username":"hello", "age":20}
    content-type: application/json

     

    1. InputStream

    • HttpServletRequest를 사용해 직접 HTTP 메시지 바디에서 데이터를 읽어와서 문자로 변환합니다.
    • 문자로 된 JSON 데이터를 Jackson 라이브러리인 obejectMapper를 사용해서 자바 객체로 변환합니다. 
    @Slf4j
    @Controller
    public class RequestBodyJsonController {
    
        private ObjectMapper objectMapper = new ObjectMapper();
    
        @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 data = objectMapper.readValue(messageBody, HelloData.class);
            log.info("username={}, age={}", data.getUsername(), data.getAge());
    
            response.getWriter().write("ok");
        }
    }

     

    2. @RequestBody String

    • @RequestBody를 사용해 HTTP 메시지 바디에서 데이터를 꺼냅니다. 
    • 이때, HttpMessageConverter 중 StringHttpConverter가 사용됩니다.
    • 문자로 된 JSON 데이터인 messageBody를 objectMapper를 통해 자바 객체로 변환합니다. 
    @ResponseBody
    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
        HelloData data = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

     

    3. @RequestBody 객체

    • 문자로 변환하고 다시 객체로 변환하는 과정이 불편합니다. @ModelAttribute처럼 한 번에 객체로 받을 수 있습니다.
    • HttpMessageConverter 중 MappingJackson2HttpMessageConverter 가 사용이 됩니다. (content-type이 application/json 일 경우만 적용)
    @ResponseBody
    @PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody HelloData data) {
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

     

    참고 HttpEntity<>, @RequestBody
    두 타입을 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해줍니다. 
    HTTP 메시지 컨버터는 String 뿐만 아니라 JSON도 객체로 변환해줍니다. 
    주의 @RequestBody는 생략 불가능
    @RequestBody를 생략하면 @ModelAttribute가 적용이 됩니다. 
    따라서 생략하면 HTTP 메시지 바디가 아니라 요청 파라미터를 처리하게 됩니다. 
    주의 요청 Content-type은 반드시 application/json
    HTTP 요청 시에 content-type이 application/json이어야 JSON을 객체로 처리할 수 있는 HTTP 메시지 컨버터가 실행됩니다. 
    참고 @ResponseBody
    응답의 경우에도 @ResponseBody를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣을 수 있습니다.
    물론, 이 경우에도 HttpEntity<>를 사용해도 됩니다.
    @ResponseBody
    @PostMapping("/request-body-json-v5")
    public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return data;
    }​

    - @RequestBody 객체
      - JSON 요청 → HTTP 메시지 컨버터 → 객체
    - @ResponseBody 응답
      - 객체 → HTTP 메시지 컨버터 → JSON

     

    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";
    }

     

     

     

    정리: 파라미터 vs 메시지 바디

    • 파라미터 조회 : @RequestParam, @PathVariable, @ModelAttribute
    • 메시지 바디 조회: @RequestBody

     

     

     

    다음 글에선 응답 관련 애노테이션을 알아보겠습니다.


    출처

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

     

    스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

    웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 원

    www.inflearn.com