후라이

[SpringMVC] Spring MVC 파헤치기 | 요청편 본문

Spring

[SpringMVC] Spring MVC 파헤치기 | 요청편

힐안 2024. 12. 26. 20:45

오늘은 Spring MVC의 기능들을 깊게 살펴보겠습니다.

(복습용..)

 

1. 요청 매핑

@ResrController
@Slf4j
public class MappingController {
	@RequestMapping("/hello/basic")
    public String helloBasic() {
    	log.info("helloBasic");
        return "ok";
    }

}

 

@ResetController : 원래 @Controller는 반환값이 String이면 이 string이 뷰 이름으로 인식됩니다.

그래서 뷰를 찾고 렌더링하게 되는데 @RestController는 반환 값으로 바로 HTTP 메시지 바디에 입력으로 들어가게 됩니다.

 

@RequestMapping : /hello-basic URL 호출이 오면 이 메서드가 실행되도록 매핑합니다.

다중 배열을 제공하므로 {"/hello-basic", "/hello-go"} 와 같이 쓸 수 있습니다.


참고로 여기서 스프링 부트 3.0 이후 버전부터는 /hello-basic과 /hello-basic/은 서로 다른 URL 요청을 사용해야 합니다.

매핑: /hello-basic -> URL 요청: /hello-basic
매핑: /hello-basic/ -> URL 요청: /hello-basic/


 

2. HTTP 메서드 

위에서 본 예시처럼 @RequestMapping에는 별도의 Method가 지정되어 있지 않으므로 HTTP 메서드와 무관하게 호출됩니다. 

GET, HEAD, POST, PUT, PATCH, DELETE

 

2.1) HTTP 메서드 매핑

 

HTTP 메서드 매핑을 위해

@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)

 

이렇게 method 지정자를 사용할 수도 있지만 더 간단하게 

@GetMapping(value = "/mapping-get-v2")

 

@GetMapping 애너테이션을 쓰면 되겠습니다.

 

2.2) @PathVariable

 

또한, PathVariable(경로변수)를 사용할 수도 있습니다.

@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
 log.info("mappingPath userId={}", data);
 return "ok";
}

 

만약 http://localhost:8080/mapping/userA 에 접속하면 HTTP 바디 메시지에 ok가 담겨 ok 사인을 볼 수 있겠죠?

PathVariable()의 이름과 파라미터 이름이 같으면 생략하고

@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable String userId) {
 log.info("mappingPath userId={}", userId);
 return "ok";
}

 

이렇게 쓰면 됩니다 :)

@PathVarible도 다중 사용이 가능합니다.

 

2.3) 미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume

 

@PostMapping(value="/mapping-consume", consumes = "application/json)
public String mappingConsumes() {
	log.info("mappingConsumes");
    	return "ok";
}

 

HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑합니다.

만약, 맞지 않으면 HTTP 415 상태코드(Unsupported Media Type)을 반환하게 됩니다.

 

consumes = "text/plain"
consumes = {"text/plain", "application/*"}
consumes = MediaType.TEXT_PLAIN_VALUE

 

위 예시처럼 사용하면 되겠습니다.

 

2.4) 미디어 타입 조건 매핑 - HTTP 요청 Accept, produce

@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
	log.info("mappingProduces");
    	return "ok";
}

 

HTTP 요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑합니다.

만약, 맞지 않으면 HTTP 406 상태코드(Not Acceptable)을 반환하게 됩니다.

produces = "text/plain"
produces = {"text/plain", "application/*"}
produces = MediaType.TEXT_PLAIN_VALUE
produces = "text/plain;charset=UTF-8"

 

위 예시처럼 사용하면 되겠습니다.

 

3. 요청 매핑 - API 예시

 

회원 관리를 HTTP API로 만든다고 가정해봅시다. 그럼 우리는 아래와 같이 회원관리 API를 설계할 수 있습니다.

  • 회원 목록 조회 : GET  /users
  • 회원 등록 : POST  /users
  • 회원 조회 : GET  /users/{userId}
  • 회원 수정 : PATCH  /users/{userId}
  • 회원 삭제 : DELETE  /users/{userId}

 

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

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

 

이렇게 매핑 애너테이션을 통해 매핑할 수 있었는데요,

이제 HTTP 요청이 보내는 "데이터"들을 스프링 MVC로 어떻게 조회하는지 알아봅시다.

 

 

4. HTTP 요청 - 기본, 헤더 조회

 

애너테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원합니다.

 

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

 

어떤 파라미터들이 있는지 참고용으로 보시면 좋을 것 같습니다.

  • HttpServletRequestHttpServletResponse
  • HttpMethod : HTTP 메서드 조회
  • Locale : Locale 정보 조회
  • @RequestHeader MultivalueMap<String, String> headerMap : 모든 HTTP 헤더를 MultiValueMap 형식으로 조회
  • @RequestHeader("host") String host : 특정 HTTP 헤더를 조회
  • @Cookie Value(value = "myCookie", required = false)  String cookie : 특정 쿠키를 조회

5. HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form

 

클라이언트에서 서버로 요청 데이터를 전달하는 방법은 주로 아래의 3가지 방법을 사용합니다.

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

 

5.1) 요청 파라미터 - 쿼리 파라미터, HTML Form

HttpServletRequest와 request.getPatameter()를 사용하면 쿼리 파라미터 전송과 HTML Form 전송 모두를 조회할 수 있습니다.

 

GET, 쿼리 파라미터 전송

http://localhost:8080/request-param?username=hello&age=20

 

POST, HTML Form 전송

POST /request-param ...
content-type: application/x-www-form-urlencoded
username=hello&age=20

 

GET 쿼리 파라미터 전송 방식, POST HTML Form 전송 방식 모두 사실상 형식이 같으므로 구분없이 조회 가능합니다.

=> 요청 파라미터(request parameter) 조회

 

이제 이 요청 파라미터 조회 방법에 대해 알아봅시다.

 

제일 단순하게는 HttpServletRequest를 사용해서 request.getParameter("username") -> 이런식으로 파라미터를 가져올 수 있겠습니다만, 스프링에서 제공하는 @RequestParam을 사용하면 요청 파라미터를 매우 편리하게 사용할 수 있습니다.

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

 

@RequestParam : 파라미터 이름으로 바인딩

참고로 여기서 나온 @ResponseBody는 @RestController와 비슷하게 View 조회를 무시하고 HTTP message body에 직접 해당 내용을 입력하게 됩니다.

 

그리고 더 간단하게, @RequestParam의 이름과 변수 이름이 같으면 @RequestParam(name="xx") 생략이 가능합니다.

또한, 파라미터가 String, int, Integer 등의 단순 타입이면 @RequestParam도 생략할 수 있습니다!

 

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

 

무척 간단해졌죠?

하지만 애너테이션을 완전히 생략하는 방법은 추천하지 않습니다.
애너테이션을 사용함으로써, 어느정도 코드상으로 요청 파라미터에서 데이터를 읽는다는 정보가 드러나게끔 리팩토링 하는 것이 좋을 듯 합니다.

 

 

5.2) HTTP 요청 파라미터 - @ModelAttribute

 

실제 개발 과정에서 요청 파라미터를 받아 필요한 객체를 만들고, 그 객체에 값을 넣어주는 식으로 설계를 할텐데요

그럼

@RequestParam String username, @RequestParam int age를 가져온 후

HelloData data= new HelloDate()라고 하는 객체에

data.setUsername(username);

data.setAge(age)l 이렇게 작성하게 될 것입니다.

 

스프링은 이 과정을 완전히 자동화해주는 @ModelAttribute 기능을 제공합니다.

 

@Data
public class HelloData {
 private String username;
 private int age;
}

 

우선 이렇게 간단한 HelloData라는 객체가 있다고 합시다.

@ModelAttribute를 어떻게 사용하냐면

@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
 log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
 return "ok";
}

 

이렇게 메서드의 파라미터에 @ModelAttribute HelloData hellodata 를 써주면 됩니다.

정말 신기하게,, HelloData 객체가 생성되고 요청 파라미터 값들이 이 객체에 들어가게 됩니다.

 

  1. HelloData 객체를 생성한다.
  2. 요청 파라미터 이름으로 HelloData 객체의 프로퍼티를 찾습니다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩)하게 됩니다.
  3. ex) 파라미터 이름이 username이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력
프로퍼티? 
객체에 getUsername(), setUsername() 메서드가 있으면, 이 객체는 username이라는 프로퍼티를 가지고 있다고 합니다. 

 

 

앞서 다른 애너테이션처럼 @ModelAttribute도 생략이 가능합니다. 하지만, 개발자가 코드를 보기에 @RequestParam과 혼란이 발생할 수 있습니다.

  • String , int , Integer 같은 단순 타입 = @RequestParam
  • 나머지 = @ModelAttribute (argument resolver 로 지정해둔 타입 외)

 

6. HTTP 요청 메시지 - 단순 텍스트

 

앞서 클라이언트가 서버에게 데이터를 전달할 수 있는 마지막 방식이

바로 HTTP message body에 데이터를 직접 담아서 요청한다는 것이었습니다.

HTTP API에서 주로 사용하며, JSON, XML, TEXT가 담길 수 있다고 하였습니다.

 

요청 파라미터인 GET 쿼리, POST HTML Form과는 다르게 이 방식은

@RequestParam과 @ModelAttribute를 사용할 수 없습니다.

그럼 해당 애너테이션이 없이, 어떻게 전달하게 되는지 한번 알아봅시다!

 

6.1) InputStream

 

가장 단순한 텍스트 메시지를 HTTP 메시지 바디에 담아서 전송하고, 읽어보는 상황입니다.

HTTP 메시지 바디의 데이터를 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");
 
 }
}

 

 

6.2) Input, Output 스트림, Reader

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

 

springMVC는 다음 파라미터를 지원합니다.

  • InputStream(Reader) : HTTP 요청 메시지 바디의 내용을 직접 조회
  • OutputStream(Writer) : HTTP 응답 메시지의 바디에 직접 결과 조회

 

그런데, 지금 위 두 가지 방식은 꽤나 귀찮습니다..

뭔가 자동화되는 스프링MVC의 기능을 사용하고 싶다는 생각이 들기도 하죠.?

 

6.3) HttpEntity

 

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

 

스프링 MVC는 HttpEntity를 파라미터를 받도록 지원하는 기능을 가지고 있습니다.

<HttpEntity>

-  HTTP header, body 정보를 편리하게 조회

- 메시지 바디 정보를 직접 조회

- 요청 파라미터를 조회하는 기능과 관계 없음 -> @RequestParam x, @ModelAttribute x

- 응답에도 사용 가능 -> 메시지 바디 정보 직접 반환, 헤더 정보 포함 가능, view 조회 x

 

이 HttpEntity의 상속을 받은 두 가지 객체들도 같은 기능을 제공합니다.

 

  • RequestEntity
    - HttpMethod, url 정보 추가, 요청에서 사용
  • ResponseEntity
    - HTTP 상태 코드 설정 가능, 응답에서 사용
    - return new ResponseEntity<String>("Hello world", responseHeaders, HttpStatus.CREATED)
참고
스프링 MVC 내부에서 HTTP 메시지 body를 읽어 문자나 객체로 변환해서 전달해주는데, 이때 HTTP Messgae Converter라는 기능을 사용합니다. 추후에 설명하겠습니다.

 

6.4) @RequestBody

 

HttpEntity보다 더 간단하게 사용할 수 있는 애너테이션이 있습니다.

@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
 log.info("messageBody={}", messageBody);
 return "ok";
}

 

그냥 @RequestBody 애너테이션을 파라미터로 넣어서 HTTP 메시지 바디 정보를 편리하게 조회할 수 있습니다.

참고로 헤더 정보가 필요하다면 HttpEntity를 사용하거나 @RequestHeader를 사용하면 됩니다.

*메시지 바디를 직접 조회하는 기능은 요청 파라미터를 조회하는 @RequestParam, @ModelAttribute와 전혀 관계가 없다.

 

  • 요청 파라미터를 조회하는 기능: @RequestParam , @ModelAttribute
  • HTTP 메시지 바디를 직접 조회하는 기능: @RequestBody

 

7. HTTP 요청 메시지 - JSON

 

7.1) ObjectMapper

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

 

이렇게 사용할 수 있습니다.

HttpServletRequest를 사용해 직접 HTTP 메시지 바디에서 데이터를 읽어와서, 문자로 변환하는 방식입니다.

문자로 된 JSON 데이터를 Jackson 라이브러리인 ObjectMapper를 사용해서 자바 객체로 변환합니다.

 

이 방식 또한 복잡하고 길어집니다..

 

7.2) @RequestBody

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

 

그렇다면 이전에 배웠던 @RequestBody를 사용해서 HTTP 메시지에서 데이터를 꺼내고 messageBody에 저장하면 됩니다.

문자로 된 JSON 데이터인 messageBody를 objectMapper를 통해 자바 객체로 변환합니다.

 

아니 그럼, 문자로 변환하고 다시 json으로 변환하는 과정이 불편한데?

@ModelAttribute처럼 한번에 객체로 변환할 순 없나?

 

7.3) @RequestBody 객체 변환

@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) { 
 log.info("username={}, age={}", data.getUsername(), data.getAge());
 return "ok";
}

 

그냥 @RequestBody에 냅다 객체를 꼬라박으면 됩니다.

앞전에 계속 사용한 HelloData 객체를 위처럼 @RequestBody 파라미터로 넣은 것을 확인할 수 있습니다.

 

HttpEntity,@RequestBody를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해줍니다.
HTTP 메시지 컨버터는 문자 뿐만 아니라 JSON도 객체로 변환해줍니다.

 

위 경우엔 @RequestBody를 생략해버리면 @ModelAttibute가 적용되어 요청 파라미터를 처리하게 되어버립니다..

생략하면 안됩니다.

 

 

7.4)  HttpEntuty

 

앞서 단순 텍스트에서 사용했듯이 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";
}

 

7.5) 객체 직접 반환

@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
 log.info("username={}, age={}", data.getUsername(), data.getAge());
 return data;
}

 

위 처럼, 객체를 HTTP 메시지 바디에 직접 넣어줄 수도 있습니다. (해당 경우에도 HttpEntity를 사용해도 됨)

@RequestBody 요청 -> JSON 요청 HTTP 메시지 컨버터 객체