Spring MVC 기본기능-값 조회

강정우·2023년 12월 5일
0

Spring-boot

목록 보기
32/73
post-thumbnail

헤더조회

  • annoation 기반 spring controller를 찾을 때 RequestMappingHandlerAdapter가 동작한다고 하였다.
    그리고 anootation 기반 spring controller는 인터페이스로, 정형화 되어있는게 아니다보니 굉장히 다양한 파라미터를 받아들일 수 있다.
@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);
        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";
    }
}
  • 보면 서블릿의 req,resp는 기본이고 locale, header의 값을 map으로 받고 또 쿠키까지 받을 수 있다.

  • 참고로 locale에서 localeResolver라는게 또 따로 있는데 locale을 쿠키나 세션에 저장해두고 여러 처리를 또 할 수 있다.

  • 또 MultiValueMap은 이름 그대로 여러개의 value, 하나의 키에 여러개의 값을 가질 수 있는 Map 객체이다.

  • 그럼 되는거 안 되는거를 확인하고 싶을 껀데 아래 홈페이지를 들어가면 확인할 수 있다.
    컨트롤러에서 사용 가능한 파라미터 목록 공식 docs
    컨트롤러에서 응답 값 목록 공식 docs

HTTP req parameter

query parameter, HTML Form

@RequestMapping("/request-param-v1")
public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
    String username = request.getParameter("username");
    String age = request.getParameter("age");
    log.info("username={}, age={}", username, age);

    response.getWriter().write("ok");
}
  • 혹은 query string이라고도 불렸던 GET 방식의 요청방식은
    HttpServelet의 request.parameter으로 조회가 가능했는데 이는
    POST, HTML Form이든 모두 조회가 가능했다.

  • 참고로 .jar로 빌드할 시에는 내장톰캣을 사용하기 때문에 webapp 경로를 사용할 수 없기 때문에 정적리소스들을 static 경로에다 넣어주면 된다.
    그러면 spring boot가 알아서 다 서빙해준다.

@RequestParam

@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(@RequestParam("username") String userName, @RequestParam("age") int userAge) {
    log.info("userName={}, userAge={}", userName, userAge);
    return "ok";
}
  • 이것역시 앞서 했던 것인데 문제는 이 전체 클래스다 @RestController가 아닌 일반 @Controller일 때 반환타입이 String이라면 view를 찾는다고 하였는데 @ResponseBody를 넣으면 http response body로 반환되기 때문에 마치 @RestController를 넣은것과 같은 효과를 볼 수 있다.

  • 또 만약 요청 파라미터의 key값과 변수이름을 일치시키면 모두 생략할 수 있다고도 배웠다.
    그런데 추가로 원시타입일때 이때는 @RequestParam을 생략할 수도 있다.
    참조타입일 땐 요청 파라미터의 key값과 변수 이름이 같아도 생략 불가

  • 다만 너무 생략하게 되면 추후 이게 어디서 넘어오는 것인지 직관적이지 않기 때문에 @RequestParam정도는 넣어주는게 좋을 것 같다.

필수 파라미터 여부 세팅

/**
 * @RequestParam.required
 * /request-param-required -> username이 없으므로 예외
 *
 * 주의!
 * /request-param-required?username= -> 빈문자로 통과
 *
 * 주의!
 * /request-param-required
 * int age -> null을 int에 입력하는 것은 불가능, 따라서 Integer 변경해야 함(또는 다음에 나오는
defaultValue 사용)
 */
@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";
}
  • 이렇게 생겼고 default는 required = true이다.
    즉, 위에 변수로 들어갔다면 값이 없으면 400 error bad request 오류가 난다는 것이다.

  • 이때 또 주의해야할 점이
    바로 타입이다. Integer로 한 이유는 만약 클라이언트 개발자가 값을 required가 false라 안 보냈다면 null로 들어올 텐데 그렇게 되면 int는 null을 받을 수 없기 때문에 500 에러가 난다. 그래서 이땐 반드시 Integer로 설정해줘야한다.

  • 또 두번째로 조심해야할 점이
    바로 빈문자열이다. "" != null 이기 때문에 로직상 이상없이 통과해버린다.
    따라서 이것에 대한 로직을 클라이언트가 처리하고 또 서버에서도 처리해주면 좋다.

기본값 설정

/**
 * @RequestParam - defaultValue 사용
 * <p>
 * 참고: defaultValue는 빈 문자의 경우에도 적용
 * /request-param-default?username=
 */
@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";
}
  • 참고로 어차피 값이 없으면 설정한 기본값이 들어가기 때문에 required 속성이 의미가 없어진다.

  • 그리고 이게 좋은게 required과는 다르게 빈 문자열에도 적용이 되어 빈문자열이 넘어오면 자동으로 defaultValue가 들어간다.

map으로 조회

/**
 * @RequestParam Map, MultiValueMap
 * Map(key=value)
 * MultiValueMap(key=[value1, value2, ...]) ex) (key=userIds, value=[id1, id2])
 */
@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";
}
  • 요청 파람의 타입을 맵으로 지정하여 바로 Map으로 받을 수 있다.
    이때 파라미터의 값이 1개가 확실하다면 Map 을 사용해도 되지만, 그렇지 않다면 MultiValueMap 을 사용하면 된다.

조회한 값 객체로 바꾸기

ModelAttribute (요청파라미터)

  • 원래라면 iter를 돌거나 아래 코드처럼 귀찮게 작성해야하는데 이를 완전히 자동화해주는 키워드가 바로 @ModelAttribute이다.
@RequestParam String username;
@RequestParam int age;
HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);

프로퍼티

  • 객체에 getUsername() , setUsername() 메서드가 있으면, 이 객체는 username 이라는 프로퍼티를 가지고 있다.
    "username 프로퍼티"의 값을 변경하면 setUsername() 이 호출되고, 조회하면 getUsername() 이 호출된다는 것이다.
class HelloData {
 getUsername();
 setUsername();
}

binding error

  • age=abc 처럼 숫자가 들어가야 할 곳에 문자를 넣으면 BindException 이 발생한다.

얘도 생략가능

  • 참고로 RequestParam처럼 얘도 생략이 가능한데 그럼 둘다 생략해버리면 뭐가 적용되는 어캐앎?

  • 스프링은 해당 생략시 다음과 같은 규칙을 적용한다.
    String , int , Integer 같은 단순 타입 생략 = @RequestParam
    나머지 타입 생략 = @ModelAttribute (argument resolver "HttpRequest 처럼 예약어들" 로 지정해둔 타입 외)

요청파라미터 외 req

  • HTTP message body에 데이터를 직접 담아서 요청

    • HTTP API에서 주로 사용, JSON, XML, TEXT
    • 데이터 형식은 주로 JSON 사용
    • POST, PUT, PATCH
  • 요청 파라미터(get, post, HTML Form)와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam, @ModelAttribute 를 사용할 수 없다.

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

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

  • 그런데 사실 너무 귀찮다 개발자가 일일이 직접 HTTP message에 들어가서 지정한 캐릭터 셋으로 변환하는 과정이 말이 안 된다. 그래서 나온 방법이 바로 HttpEntity이다.

HttpEntity

@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");
}
  • HttpEntity<>는 안에 타입을 받는데 String으로 해두면 Spring이 HttpBody에 있는 값을 문자형으로 바꿔서 넘겨준다.
    그래서 HttpMessageConverter가 동작을 한다.

  • 특징은 앞서 언급했듯 message body 정보를 직접조회, 따라서 @RequestPararm, @ModelAttribute와는 전혀 관계가 없다.

  • HttpEntity는 응답에도 사용가능하다. message body 정보를 직접 반환할 수 있고 헤더 정보도 포함하여 가능하다.

RequestEntity, ResponseEntity

@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(RequestEntity<String> httpEntity) throws IOException {
    String messageBody = httpEntity.getBody();
    log.info("messageBody={}", messageBody);
    return new ResponseEntity<>("ok", HttpStatus.CREATED);
}

@RequestBody, @ResponseBody

@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
    log.info("messageBody={}", messageBody);
    return "ok";
}
  • 얘는 더 편리하다 spring이 그냥 HTTP message body를 읽어서 한큐에 넣어준다.
    그리고 returneh @ResponseBody 어노테이션으로 알아서 만들어서 보내준다.

요청 파라미터를 조회한다: @RequestParam, @ModelAttribute
HTTP message body를 직접 조회한다: @RequestBody

JSON 조회

@ReqeustBody, @ResponseBody

/**
 * @RequestBody
 * HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
 *
 * @ResponseBody
 * - 모든 메서드에 @ResponseBody 적용
 * - 메시지 바디 정보 직접 반환(view 조회X)
 * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
 */
@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";
}
/**
 * @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
 * HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (content-type:
application/json)
 *
 */
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) {
    log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

    return "ok";
}
  • 이렇게 json은 더 간단하게 개선할 수 있는데 대충 원리는 HttpEntity , @RequestBody 를 사용했을 때 HTTPMessageConverter가 동작하여
    HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);이 코드를 대신 실행해준다고 생각하면 된다.

  • 또 @RequestBody 생략하면 안 되는데 @ModelAttribute가 디폴트로 적용되어 버리기 때문이다.
    그래서 message body를 조회해야하는데 request param을 찾고있게 된다.

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";
}
  • 소개 정도로 하고 넘어가겠다.

HttpMessageConverter

/**
 * @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
 * HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (content-type:
application/json)
 *
 * @ResponseBody 적용
 * - 메시지 바디 정보 직접 반환(view 조회X)
 * - HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용(Accept:
application/json)
 */
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData helloData) {
    log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
    return helloData;
}
  • HttpMessageConverter가 @ResponseBody가 붙어있다면 자동으로 적용된다.
    전에 String으로 반환할 때도 HttpMessageBody에 들어갔었다. 마찬가지로 반환타입을 지정하면 해당 타입의 객체를 생성하여 이게 HttpMessageConverter를 타고 json으로 값이 바뀐다. 그리고 이 json 문자열이 http message 응답에 들어가서 응답한다.

  • 이때 헤더에 Accept:appliction/json이 명시되어있어야한다.

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글