Home 스프링 MVC에서 HTTP 요청, 응답 처리
Post
Cancel

스프링 MVC에서 HTTP 요청, 응답 처리

HTTP 요청 - 기본, 헤더 조회

  • HttpServletReqeust, HttpServletResponse
  • HttpMethod : HTTP 메서드 조회
  • Locale : Locale 정보 조회 (언어)
  • @RequestHeader MultiValueMap<String, String> headerMap
    • 모든 HTTP 헤더를 MultiValueMap으로 조회한다.
  • @RequestHeader(“host”) String host
    • 특정 HTTP 헤더를 조회한다.
  • @CookieValue(value = “myCookie”, required = false) String cookie
    • 특정 쿠키를 조회한다.

MultiValueMap

MAP과 유사하지만 하나의 키에 여러 값을 받을 수 있다.

  • HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다.
    • keyA=value1&keyA=value2
1
List<String> values = map.get("keyA");

HTTP 요청 파라미터

서블릿에서 학습했던 HTTP 요청 데이터를 조회하는 방법을 다시 떠올려보자. 서블릿으로 학습했던 내용을 스프링이 얼마나 깔끔하고 효율적으로 바꾸어주는지 알아본다.

HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법

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

요청 파라미터 조회 (쿼리 파라미터, HTML Form)

GET 쿼리 파라미터든 POST HTML Form 전송 방식이든 둘 다 형식이 같으므로 구분없이 조회할 수 있다.

이것을 간단히 요청 파라미터 조회라고 한다.

우선 서블릿처럼 request 를 이용해 request.getParameter(“username”)로 받을 수 있다.

다음을 보자.

@RequestParam

  • @RequestParam
    • 파라미터 이름으로 바인딩
    • 파라미터 이름이 변수명과 같다면 (“username”) 생략 가능
  • @ResponseBody
    • 위를 @RestController가 아닌 @Controller로 했을 때 뷰를 찾게 되는데 @RestController 처럼 VIew 조회를 무시하고 HTTP message body에 직접 해당 내용을 입력한다.

@RequestParam마저 생략 가능하다.

  • String, int 등의 단순 타입일 때 가능하다.

애노테이션이 이렇게 생략은 되지만 명확하게 요청 파라미터에서 데이터를 읽는다는 것을 한눈에 알기는 어려움으로 생략하는 것은 고민해봐야 할 문제.

참고 (required=true, false)

  • 애노테이션을 생략하면 스프링 MVC는 내부에서 required=false를 적용한다.
    • 파라미터가 필수가 아니라는 뜻이다.
    • 반대로 true라고 되어 있으면 파라미터가 없으면 400 예외를 발생시킨다.
    • 기본값은 true이다.
  • /request-param?username=kim 과 같이 주어지고 age에 false로 한다면?
    • 잘 되어야 할 것 같은데 500오류가 발생한다.
    • int age인데 int에는 null 값이 들어갈 수 없다. 따라서 Integer로 바꿔주어야 한다.
  • /request-param?username= 과 같이 빈칸으로 둔다면?
    • null이 아닌 ““로 처리된다.
    • @RequestParam( required = true, defalutValue = “guest”) 와 같이 기본값을 줘서 해결할 수 있다.
      • 이 경우 사실 required의 값은 무의미하다.

Map으로 받기

파라미터를 Map으로 한 번에 받을 수 있다.

파라미터의 값이 1개가 확실하다면 Map을 사용하지만 그렇지 않다면 MultiValueMap을 사용한다. (보통은 하나를 사용하긴 한다.)

@ModelAttribute

실제 개발을 하면 요청 파라미터를 받아 필요한 객체를 만들고 그 객체에 값을 넣어주어야 한다. 보통 아래와 같이 코드를 작성할 것이다.

1
2
3
4
5
6
@RequestParam String username;
@RequestParam int age;

HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);

스프링은 이런 과정을 자동화해주는 @ModelAttribute 기능을 제공한다.

먼저 요청 파라미터를 받을 바인딩 객체를 만들어본다.

1
2
3
4
5
@Data
public class HelloData{
	private String username;
	private int age;
}
  • @Data (롬복)
    • @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor를 자동 적용 해준다.

v1

HelloData 객체가 생성되고 요청 파라미터의 값도 모두 들어가 있다.

스프링 MVC는 @ModelAttribute가 있으면 아래를 실행한다.

  • HelloData 객체를 생성한다.
  • 요청 파라미터 이름으로 HelloData 객체의 프로퍼티를 찾는다.
  • 해당 프로퍼티의 setter를 호출해 파라미터의 값을 입력 한다.
    • ex) 파라미터의 이름이 ‘username’이면 setUsername() 메서드를 찾아 호출하면서 값을 입력한다.

바인딩 오류

‘age = abc’ 처럼 숫자가 들어갈 곳에 문자를 넣으면 BindException이 발생한다. 이러한 바인딩 오류를 처리하는 방법은 검증에서 다룬다.

v2

@ModelAttribute의 생략도 가능하다. 그런데 @RequestParam도 생략할 수 있어 혼란이 발생할 수 있다.

그래서 스프링은 해당 생략시 아래와 같은 규칙을 적용한다.

  • ‘String’, ‘int’, ‘Integer’ 같은 단순 타입 = @RequestParam
  • 이 외 나머지 = @ModelAttribute (argument resolver로 지정해둔 타입 제외)
    • argument resolver는 추후 다룬다.

HTTP 요청 메시지

요청 파라미터와 다르게 HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam, @ModelAttribute를 사용할 수 없다.

이런 경우에 대해 알아보자.

단순 텍스트

위의 방법 2가지는 스프링을 어느정도 활용하지 않고 가져올 수 있는 방법이다. inputStream 등등 사용하기 번거롭다.

단순 텍스트를 가져오는 방법 아래를 보자.

HttpEntity

  • HttpEntity: HTTP header, body 정보를 편리하게 조회한다.
    • 메시지 바디 정보를 직접 조회
    • 요청 파라미터를 조회하는 기능과 관계 없음 (@RequestParam, @ModelAttribute)
  • HttpEntity는 응답에도 사용 가능하다.
    • 메시지 바디 정보 직접 반환
    • 헤더 정보 포함 가능
    • 당연히 view 조회 X

HttpEntity를 상속받은 아래의 객체들도 같은 기능을 제공한다.

  • RequestEntity
    • HttpMethod, url 정보가 추가
    • 요청에서 사용된다.
  • ResponseEntity
    • HTTP 상태 코드 설정 가능
    • 응답에서 사용된다.
1
return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED)

@RequestBody

얘가 가장 많이 쓰인다.

  • @RequestBody
    • 메시지 바디를 바로 조회가능.
    • 헤더정보가 필요하다면 @RequestHeader를 이용하면 된다.
    • @RequestParam, @ModelAttribute와 관계없다.
  • @ResponseBody
    • 응답 결과를 HTTP 메시지 바디에 직접 담아 전달한다.
    • 이 경우도 당연히 view를 사용하지 않는다.

JSON 데이터

서블릿에서는 이렇게 받아왔었다.

스프링을 이용해 어떻게 하는지 보자.

@RequestBody 이용

단순히 바디를 @RequestBody로 가져오고 똑같이 매핑해줄 수 있다. 하지만 이것도 번거롭다.

@RequestBody 이용 v2

@ModelAttribute 처럼 바로 객체를 대입해 setUsername()과 같은 작업을 자동으로 하게 할 수 있다.

만약 여기에 JSON이 아닌 일반 TEXT를 POST하면 415 오류가 발생한다. username과 age를 매핑할 수 없기 때문이다.

  • @RequestBody에 직접 만든 객체를 지정할 수 있다.
  • HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해준다.
  • HTTP 메시지 컨버터는 문자 뿐만 아니라 JSON도 객체로 변환해준다.

참고

@RequestBody는 @RequestParam이나 @ModelAttribute처럼 생략하면 안된다.

만약 생략하면 @ModelAttribute가 붙는 것과 같기 때문에 HTTP 메시지 바디가 아닌 요청 파라미터를 처리하게 된다.

객체를 JSON으로 응답 (@ResponseBody)

이렇게 객체를 @ResponseBody를 통해 반환하게 되면 JSON 형태로 응답하게 된다.

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

HTTP 응답

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

  • 정적 리소스
    • ex) 웹 브라우저에 정적인 HTML, css, js 등을 제공할 때는 정적 리소스를 사용한다.
  • 뷰 템플릿 사용
    • ex) 웹 브라우저에 동적인 HTML을 제공할 때는 뷰 템플릿을 사용한다.
  • HTTP 메시지 사용
    • HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.

정적 리소스

스프링 부트는 클래스 패스의 다음 디렉토리에 있는 정적 리소스를 제공한다.

  • /static
  • /public
  • /resources
  • /META-INF/resources

정적 리소스 주소 src/main/resources/static

src/main/resources/static/basic/hello-form.html 경로에 파일이 있다고 하면

http://localhost:8080/basic/hello-form.html 로 실행하면 된다.

정적 리소스는 파일을 변경 없이 그대로 서비스 하는 것이다.

뷰 템플릿

스프링 부트의 기본 뷰 템플릿 경로는 아래와 같다.

  • src/main/resources/templates

위 경로에 위와 같은 파일을 넣는다.

th:text=의 data를 사용하려면 컨트롤러가 필요하다. 만들어보자.

v1

data에 hello!를 넣었고 반환해 호출하면 hello!를 출력하게 된다.

v2

String으로 뷰의 논리적 이름을 반환해 뷰를 찾는다.

데이터는 Model 객체에 넣어 addAttribute를 해주면 보낼 수 있다.

이 형태를 주로 사용하게 된다.

복습

@ResponseBody가 없으면 response/hello를 뷰 리졸버가 실행되어 뷰를 찾고, 렌더링 한다.

있으면 메시지 바디에 직접 문자를 입력한다.

HTTP API, 메시지 바디에 직접 입력

이는 위에서 다루었다. 아래 코드들을 보고 복습한다.

@ResponseBody와 String을 반환해 문자를 직접 전달할 수 있었고,

@RequestBody로 JSON 데이터를 객체로 받고, @ResponseBody를 이용해 객체를 반환하면 JSON으로 다시 바꾸어 보여줄 수 있다.

참고

@RestController를 붙이면 @ResponseBody를 안붙여도 되는 것. 이름 그대로 Rest API(HTTP API)를 만드는 데 사용된다.

이를 이용해 보통 HTTP API를 작성할 때는 아래와 같은 구조로 사용하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class APIController{

	@ResponseStatus(HttpStatus.OK)
	@GetMapping("/httpapi")
	public HelloData httpApi(){
		HelloData helloData = new HelloData();
		helloData.setUsername("userA");
		helloData.setAge(20);

		return helloData;
	}	
}

HTTP 메시지 컨버터

뷰 템플릿으로 HTML을 생성해 응답하는 것이 아니라, HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.

  • @ResponseBody를 사용
    • viewResolver 대신 HttpMessageConverter가 동작
      • 기본 문자 처리 : StringHttpMessageConverter
      • 기본 객체 처리 : MappingJackson2HttpMessageConverter
      • byte 처리 등등 기타 여러 HttpMessageConverter가 등록되어 있다.

HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보 이 둘을 조합해 HttpMessageConverter가 선택된다.

HTTP 메시지 컨버터 인터페이스

HTTP 요청, 응답 둘 다 사용된다.

  • canRead(), canWrite() : 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 체크
  • read(), write() : 메시지 컨버터를 통해 메시지를 읽고 쓰는 기능

컨버터 종류

1
2
3
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter

대략 위가 기본 메시지 컨버터이고

대상 클래스 타입과 미디어 타입 둘을 체크해 사용 여부를 결정한다. 만족하지 않으면 다음 메시지 컨버터로 우선순위가 넘어간다.

  • ByteArrayHttpMessageConverter
    • byte[] 데이터를 처리한다.
    • 클래스 타입: ‘byte[]’, 미디어타입: ‘전체’
    • 요청 예시) @RequestBody byte[] data
    • 응답 예시) @ResponseBody return byte[]
  • StringHttpMessageConverter
    • String 문자로 데이터를 처리한다.
    • 클래스 타입: String, 미디어타입: ‘전체’
    • 요청 예시) @RequestBody String data
    • 응답 예시) @ResponseBody return “ok”
  • MappingJackson2HttpMessageConverter
    • JSON 관련 컨버터
    • 클래스 타입: 객체 또는 HashMap, 미디어 타입: application/json 관련
    • 요청 예시) @RequestBody HelloData data
    • 응답 예시) @ResponseBody return data

HTTP 요청 데이터 읽기 과정

  • HTTP 요청이 오고 컨트롤러에서 @RequestBody, HttpEntity 파라미터를 사용한다.
  • 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead()를 호출한다.
    • 대상 클래스 타입을 지원하는가
      • ex) @RequestBody의 대상 클래스(‘byte[]’, String, ‘HelloData’)
    • HTTP 요청의 Content-Type 미디어 타입을 지원하는가
      • ex) ‘text/plain’, ‘application/json’
  • canRead() 조건을 만족하면 read()를 호출해 객체를 생성하고 반환한다.

HTTP 응답 데이터 생성 과정

  • 컨트롤러에서 @ResponseBody, HttpEntity로 값이 반환된다.
  • 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite()를 호출한다.
    • 대상 클래스 타입을 지원하는가
      • ex) return 대상의 클래스(byte[], String, HelloData)
    • HTTP 요청의 Accept 미디어 타입을 지원하는가 (@RequestMapping의 produces)
      • ex) text/plain, application/json 등
  • canWrite() 조건을 만족하면 write()를 호출해 HTTP 응답 메시지 바디에 데이터를 생성한다.

예시

1
2
3
4
content-type: application/json

@RequestMapping
void hello(@RequestBody String data) {}

이면?

ByteArrayhttpMessageConverter은 클래스 타입이 맞지 않으니 패스

StringHttpMessageConverter는 클래스 타입 일치하고, 미디어타입은 전체이니까 가능.

얘를 쓰게 된다.

1
2
3
4
content-type: application/json

@RequestMapping
void hello(@RequestBody HelloData data) {}

이면?

같은 과정으로 MappingJackson2HttpMessageConverter를 사용하게 된다.

여기서 만약 content-type: text/html 이라면?

MappingJackson2HttpMessageConverter도 탈락.

요청 매핑 핸들러 어댑터(RequestMappingHandlerAdapter) 구조

HTTP 메시지 컨버터는 스프링 MVC 어디쯤에서 사용되는 것인가.

비밀은 애노테이션 기반의 컨트롤러, 즉 @RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter에 있다.

동작 방식

  • ArgumentResolver
    • 애노테이션 기반 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다.
    • HttpServletRequest, Model, @RequestParam, @RequestBody 등등..
    • 이렇게 파라미터를 유연하게 처리할 수 있는 이유가 ArgumentResolver이다.
    • RequestMappingHandlerAdapter는 얘를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터 값(객체)을 생성한다. 그리고 이렇게 파라미터의 값이 모두 준비되면 컨트롤러를 호출하며 값을 넘겨준다.
    • supportsParameter()를 호출해 해당 파라미터를 지원하는지 체크하고,
    • 지원한다면 resolveArgument()를 호출해 실제 객체를 생성한다.
    • 그리고 컨트롤러 호출 시 객체를 넘긴다.
  • ReturnValueHandler
    • ArgumentResolver와 비슷한데, 이것은 응답 값을 변환하고 처리한다.
    • 컨트롤러에서 String으로 뷰 이름을 반환해도 동작하는 이유가 얘 덕분이다.

메시지 컨버터의 위치

HTTP 메시지 컨버터를 사용하는 @RequestBody도 컨트롤러가 필요로 하는 파라미터의 값에 사용된다.

@ResponseBody도 컨트롤러의 반환 값을 이용한다.

  • 요청
    • @RequestBody를 처리하는 ArgumentResolver가 있고, HttpEntity를 처리하는 ArgumentResolver가 있다.
    • 이 ArgumentResolver들이 HTTP 메시지 컨버터를 사용해 필요한 객체를 생성하는 것이다.
  • 응답
    • 마찬가지로 RetrunValueHandler에서 HTTP 메시지 컨버터를 호출해 응답 결과를 만든다.

확장

스프링은 아래를 모두 인터페이스로 제공한다. 따라서 확장이 가능하다.

  • HandlerMethodArgumentResolver
  • HandlerMethodReturnValueHandler
  • HttpMessageConverter

스프링이 필요한 대부분의 기능을 제공하기 때문에 확장할 일은 별로 없다. 기능 확장은 WebMvcConfigurer를 상속받아 스프링 빈으로 등록하면 된다. 실제 필요할 때 WebMvcConfigurer를 검색해보자.

This post is licensed under CC BY 4.0 by the author.