8. 스프링 MVC 기본 기능
2025. 5. 8. 18:06ㆍSpring/MVC
[목차]
1. 요청 매핑
1) 요청 매핑
2) @RequestMapping의 주요 애트리뷰트
3) 경로변수 @PathVariable
4) HTTP API 예제
2. HTTP 요청/응답 복습
1) HTTP 메시지 구조
2) HTTP 요청/응답 데이터 전달 방식
3) HTTP 요청/응답 메시지 바디에 들어가는 데이터의 종류
3. HTTP 요청 메시지 조회
3-1. 요청 메시지의 시작라인, 헤더 조회
3-2. 요청 메시지의 파라미터 조회
1) 요청 파라미터 전송 방식 2가지
2) @RequestParam
3) @ModelAttribute
3-3. 요청 메시지의 메시지 바디 조회
1) 단순 텍스트: @RequestBody + String 파라미터
2) JSON: @RequestBody + 객체 파라미터
4. HTTP 응답 메시지 전달
1) 정적 리소스 자동 서빙
2) 뷰 템플릿을 통한 HTML 전달 (SSR)
3) JSON 데이터 전달 - HTTP API (CSR): @RestController + 객체 리턴
1. 요청 매핑
1) 요청 매핑
- 클라이언트가 보낸 HTTP 요청(주소 + 메서드)을 어떤 컨트롤러 메서드가 처리할지 매핑(연결)하는 역할
- 요청 매핑은 @RequestMapping 어노테이션을 사용해서 지정할 수 있다.
- 요청 매핑을 처리하는 주체는 RequestMappingHandlerMapping
- DispatcherServlet 초기화 시 handlerMappings가 초기화 되는 과정에서 RequestMappingHandlerMapping도 초기화
- RequestMappingHandlerMapping은 컨트롤러 클래스에서 @RequestMapping 애노테이션이 붙은 메서드를 스캔한다.
- 각 메서드에 정의된 value, method, params, headers 등 애트리뷰트를 읽음
- 그걸 기반으로 "URL + HTTP 메서드 조합" → "어떤 메서드를 호출해야 할지"의 매핑 정보를 만들어 저장한다
- 이후 DispatcherServlet이 요청을 받으면 RequestMappingHandlerMapping.getHandler()를 호출해서
해당 요청을 처리할 핸들러(컨트롤러 메서드)를 찾아줌
2) @RequestMapping의 주요 애트리뷰트(attributes)
애트리뷰트 이름 | 설명 | 값 예시 |
value 또는 path | 요청 URL 경로 | /users, /articles/{id} |
method | HTTP 메서드 제한 | GET, POST, PUT, DELETE 등 |
params | 특정 요청 파라미터가 있을 때만 처리 | params="mode=debug" |
headers | 특정 헤더 조건이 있을 때만 처리 | headers="mode=debug" |
consumes | 요청 헤더 Content-Type 제한 | application/json 등 |
produces | 요청 헤더 Accept 제한 | application/json, text/html 등 |
- value, method → 기본 매핑 조건
- params, headers → 부가 조건 (파라미터/헤더 값이 일치할 때만 실행)
- consumes, produces → 미디어 타입 조건 (API 만들 때 자주 등장)
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
//1. value 또는 path: 요청 URL 경로
// value 또는 path는 단독으로 사용할 때 속성명 생략 가능하다
// 다중 설정도 가능하다. {"/hello-basic", "/hello-go"}
// 스프링3.0부터 끝에 슬레시 유무 구분해서 다른 요청으로 판단함: /hello-basic, /hello-basic
@RequestMapping("/hello-basic")
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
//2. method: 특정 HTTP 메서드 요청만 허용
// GET, HEAD, POST, PUT, PATCH, DELETE
// 만약 여기에 POST 요청을 하면 스프링 MVC는 HTTP 405 상태코드(Method Not Allowed)를 반환한다
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
// method 설정 대신 편리하게 축약 어노테이션을 사용할 수 있다.
//@GetMapping, @PostMapping, @PutMapping
//@DeleteMapping, @PatchMapping
@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
//3. params: 특정 요청 파라미터가 있을 때만 처리
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
log.info("mappingParam");
return "ok";
}
//4. headers: 특정 요청 헤더가 있을 때만 처리
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
log.info("mappingHeader");
return "ok";
}
//5. consumes: 요청 헤더 Content-Type 제한
// 아래의 메서드는 JSON 요청만 처리 가능
// 만약 Content-Type이 text/plain인 요청이 오면 415 Unsupported Media Type 에러 발생
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
//6. produces: 요청 헤더 Accept 제한
// 클라이언트가 요청할 때 Accept 헤더에 “나는 이런 타입의 응답이 좋아요~”라고 말해놓을 수 있음
// 이 메서드는 text/html으로 응답 가능
// 만약 Accept이 application/json인 요청이 오면 406 Not Acceptable 에러 발생
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
}
3) 경로변수 @PathVariable 사용
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
//@PathVariable 사용
// 변수명이 같으면 생략 가능
// @PathVariable("userId") String userId -> @PathVariable String userId
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
}
- 최근 HTTP API는 다음과 같이 리소스 경로에 식별자를 넣는 스타일을 선호한다.
/mapping/userA
/users/1 - @RequestMapping은 URL 경로를 템플릿화 할 수 있는데, @PathVariable을 사용하면 매칭 되는 부분을 편리하게 조회 가능
- 경로변수 @PathVariable를 처리하는 주체도 동일하게 RequestMappingHandlerMapping
- 서버 시작 시: RequestMappingHandlerMapping이 @RequestMapping("/users/{id}")를 패턴 기반 매핑 정보로 등록
(URL만 매핑하는 게 아니라, method, params 등 조건도 같이 등록) - 요청이 오면 getHandler(request) 메서드 내부에서 요청 URI와 등록된 패턴들을 비교
/users/42 → /users/{id}와 매칭됨! → 해당 핸들러 반환 - 이후 HandlerMethodArgumentResolver가 @PathVariable("id")에 해당하는 값을 request에서 추출해서 바인딩해줌
- 서버 시작 시: RequestMappingHandlerMapping이 @RequestMapping("/users/{id}")를 패턴 기반 매핑 정보로 등록
4) HTTP API 예제
@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;
}
}
2. HTTP 메시지 구조 및 요청/응답 데이터 복습
1) HTTP 메시지 구조
1) 시작 라인 (start-line)
- 요청 시작 라인: HTTP메서드 요청대상(절대경로) HTTP버전
예시: GET /members HTTP/1.1
- 응답 시작 라인: HTTP버전 HTTP상태코드 이유문구
예시: HTTP/1.1 200 OK
2) 헤더 (header)
- 필드네임 ":" 필드값
- http 전송에 필요한 모든 부가 정보가 다 들어가 있음
- 중요한 헤더 정보
Content-Type: 메시지 바디에 담긴 데이터의 타입 지정
Accept: 클라이언트가 응답으로 받고 싶은 타입을 서버에 알려줌
3) 메시지 바디 (message body)
- 실제로 전송할 데이터가 담김
- HTML 문서, 이미지, 영상, JSON 등등 byte로 표현할 수 있는 모든 데이터를 전송 가능
HTTP 웹 기본 지식 (2) | HTTP 개념, 메서드, 상태 코드
본 게시글은 아래의 강의를 수강하고 요약 및 추가 정리한 게시글 입니다. 모든 개발자를 위한 HTTP 웹 기본 지식 강의 | 김영한 - 인프런김영한 | , [사진] 📣 확인해주세요!본 강의는 자바 스프
hnjee.tistory.com
2) HTTP 요청/응답 데이터 전달 방식
<요청: 클라이언트 → 서버>
1. URL에 쿼리 파라미터 붙이기: GET 방식에서 주로 사용
2. 메시지 바디에 담기: POST, PUT, PATCH 요청 등에서 사용됨
<응답: 서버 → 클라이언트>
주로 메시지 바디에 담는 방식만을 사용한다.
3) HTTP 요청/응답 메시지 바디에 들어가는 데이터의 종류
<중요한 점!>
HTTP 요청/응답 메시지 바디에 데이터를 담을 때는
서버와 브라우저가 데이터를 올바르게 해석할 수 있도록
반드시 헤더의 Content-Type에 데이터의 타입을 명시해주어야 한다.
<요청 메시지 바디에 들어가는 데이터>
1. HTTP Form 형식
: application/x-www-form-urlencoded
2. JSON 형식 (HTTP API 방식)
: application/json
3. 파일, 이미지 등등
: multipart/form-data
<응답 메시지 바디에 들어가는 데이터>
1. 정적 리소스
: text/html, text/css, image/png 등
2. HTML 문서 → SSR 방식
: text/html
3. JSON, XML 등 순수 데이터 (HTTP API 응답) → CSR 방식
: application/json, application/xml 등
3. HTTP 요청 메시지 조회
3-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);
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";
}
}
- 동작 방식 정리
파라미터 | 설명 | 처리 방식 |
HttpServletRequest, HttpServletResponse |
서블릿 기본 객체 | 서블릿 직접 주입 |
HttpMethod | HTTP 요청 메서드 (GET, POST 등) |
스프링이 감지해서 주입 |
Locale | 클라이언트의 언어 설정 | Accept-Language 헤더 기반 자동 주입 |
@RequestHeader MultiValueMap<String, String> |
모든 요청 헤더 조회 | 모든 헤더 값을 Map 형태로 주입 |
@RequestHeader("host") String | 특정 헤더값 조회 | host 헤더만 추출 |
@CookieValue("myCookie") | 쿠키 조회 | 해당 이름의 쿠키 값만 추출 (required=false이면 없으면 null) |
- 사용된 Argument Resolver
파라미터 | 사용된 Argument Resolver |
HttpServletRequest, HttpServletResponse |
ServletRequestMethodArgumentResolver |
HttpMethod | RequestMethodArgumentResolver |
Locale | LocaleResolver에 의해 LocaleContextResolver가 결정 (내부적으로 LocaleMethodArgumentResolver) |
@RequestHeader | RequestHeaderMethodArgumentResolver |
@CookieValue | CookieValueMethodArgumentResolver |
- 흐름 요약
요청 도착
→ DispatcherServlet
→ RequestMappingHandlerAdapter
→ 각 파라미터를 HandlerMethodArgumentResolver로 처리
- 서블릿 기본 객체 → ServletRequestMethodArgumentResolver
- @RequestHeader → RequestHeaderMethodArgumentResolver
- @CookieValue → CookieValueMethodArgumentResolver
→ 모두 주입된 뒤 컨트롤러 메서드 실행
→ 리턴 값은 "ok" + @RestController의 @ResponseBody
→ HandlerMethodReturnValueHandler: RequestResponseBodyMethodReturnValueHandler
→ 내부적으로는 StringHttpMessageConverter가 "ok"를 HTTP 바디에 씀
3-2. 요청 메시지의 파라미터 조회
1) 파라미터 전송 방법
1. 요청 파라미터로 데이터 전송
http://localhost:8080/request-param?username=hello&age=20
2. Http 요청 메시지 바디에 form 데이터 전송
POST /request-param ...
content-type: application/x-www-form-urlencoded
username=hello&age=20
- 요청 시 쿼리 파라미터 방식과 Http 메시지 바디에 HTML Form을 담아 전송하는 방식은
전달되는 데이터의 형식이 같기 때문에 HttpServletRequest.getParameter()로 구분없이 조회할 수 있다. - 스프링 MVC에서는 요청 파라미터를 간편하게 조회하도록 @RequestParam과 @ModelAttribute을 지원한다.
2) @RequestParam
@Slf4j
@Controller
public class RequestParamController {
//반환 타입이 없으면서 이렇게 응답에 값을 직접 집어넣으면, view 조회X
//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);
response.getWriter().write("ok");
}
//@RequestParam : 파라미터 이름으로 바인딩
//@ResponseBody : View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String username,
@RequestParam("age") int memberAge) {
log.info("username={}, age={}", username, memberAge);
return "ok";
}
//HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam 뒤 (name="xx") 생략 가능
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
@RequestParam String username,
@RequestParam int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
//String, int, Integer 등의 단순 타입이면 @RequestParam도 생략 가능
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
}
- 동작 및 Argument Resolver, ReturnValueHandler 정리
메서드 | 요청 파라미터 처리 방식 | Argument Resolver | ReturnValueHandler |
v1 | 서블릿 직접 처리 | ❌ 없음 | ❌ 직접 응답 처리 |
v2 | @RequestParam("name") | RequestParamMethodArgumentResolver | RequestResponseBodyMethodReturnValueHandler |
v3 | @RequestParam (생략 X) | 동일 | 동일 |
v4 | @RequestParam 생략 | 동일 | 동일 |
- 흐름 요약
클라이언트 → 파라미터 포함 요청
→ DispatcherServlet
→ RequestMappingHandlerAdapter
→ RequestParamMethodArgumentResolver가 파라미터 분석
→ 메서드 실행
→ 리턴 값은 "ok" + @RestController의 @ResponseBody
→ HandlerMethodReturnValueHandler: RequestResponseBodyMethodReturnValueHandler
→ 내부적으로는 StringHttpMessageConverter가 "ok"를 HTTP 바디에 씀
- @RequestParam 설정: required, defaultValue
//@RequestParam.required: 파라미터 필수 여부, 기본값 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.defaultValue: 파라미터에 값이 없는 경우 기본 값 적용 가능
//defaultValue를 사용하면 required는 의미가 없어진다.
@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";
}
//파라미터를 Map으로 조회 가능
@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";
}
3) @ModelAttribute
@Data
public class HelloData {
private String username;
private int age;
}
//1. HelloData 객체를 직접 생성 + setter로 수동 바인딩
@ResponseBody
@RequestMapping("/model-attribute-before")
public String modelAttributeV1(@RequestParam String username, @RequestParam int age) {
HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
//2. @ModelAttribute 명시 사용
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
//3. @ModelAttribute 생략하여 사용
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(),
helloData.getAge());
return "ok";
}
- @ModelAttribute 미사용 (수동 바인딩)
항목 | 설명 |
방식 | @RequestParam으로 각각의 값 바인딩 |
바인딩 | 컨트롤러 내부에서 직접 HelloData 객체 생성 및 값 할당 |
목적 | @ModelAttribute 사용 전 상황 설명용 |
Argument Resolver | RequestParamMethodArgumentResolver × 2개 사용 |
특징 | 값이 많아질수록 코드가 반복됨 → 객체 바인딩이 더 편리함 |
- @ModelAttribute 사용
항목 | 설명 |
방식 | 요청 파라미터를 HelloData 객체에 자동 바인딩, model에 값 넣기 |
동작 | ① 객체 생성 → setter로 파라미터 값 바인딩 ② @ModelAttribute로 지정한 객체를 Model에 자동으로 넣어줌 예를 들어 @ModelAttribute("helloData1", HelloData helloData2)하면 → model.addAttribute("helloData1", helloData); 자동으로 수행됨 |
Argument Resolver | ModelAttributeMethodArgumentResolver |
특징 | 객체 프로퍼티명과 파라미터명이 매칭되면 자동 주입 @ModelAttribute의 이름을 생략하는 경우, 모델에 저장될 때 클래스명을 사용한다. (클래스 명의 첫글자를 소문자로 바꿔서 모델 애트리뷰트의 이름으로 사용) |
요청 예시 | /model-attribute-v1?username=kim&age=30 →@ModelAttribute HelloData helloData → new HelloData() → setUsername("kim"), setAge(30) → model.addAttribute("helloData1", helloData); |
- @ModelAttribute는 생략시 규칙
- String, int, Integer 같은 단순 타입 = @RequestParam
- 나머지 = @ModelAttribute
- 흐름 요약
클라이언트 요청: ?username=kim&age=20
→ DispatcherServlet
→ RequestMappingHandlerAdapter
→ ModelAttributeMethodArgumentResolver
→ new HelloData()
→ setUsername("kim")
→ setAge(20)
→ 컨트롤러 메서드 호출
→ 리턴 값은 "ok" + @RestController의 @ResponseBody
→ HandlerMethodReturnValueHandler: RequestResponseBodyMethodReturnValueHandler
→ 내부적으로는 StringHttpMessageConverter가 "ok"를 HTTP 바디에 씀
3-3. 요청 메시지의 메시지 바디 조회
1) 단순 텍스트
@Slf4j
@Controller
public class RequestBodyStringController {
//HTTP 메시지 바디의 데이터를 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");
}
//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");
}
//HttpEntity: HTTP header, body 정보를 편리하게 조회
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
//@RequestBody: HTTP 메시지 바디 정보를 편리하게 조회할 수 있다
//@RequestHeader: HTTP 메시지 헤더 정보 조회
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
log.info("messageBody={}", messageBody);
return "ok";
}
}
버전 | 입력 처리 방식 | Argument Resolver | 출력 처리 방식 | ReturnValueHandler |
v1 | request. getInputStream() |
❌ (서블릿 API 직접 사용) | response. getWriter() |
❌ |
v2 | InputStream | ServletRequestMethodArgumentResolver | Writer | ❌ |
v3 | HttpEntity<String> | HttpEntityMethodProcessor | HttpEntity | HttpEntityMethodProcessor |
v4 | @RequestBody String |
RequestResponseBodyMethodProcessor | @ResponseBody | RequestResponseBodyMethodProcessor |
- InputStream, Reader → 입출력 수동 처리, 스프링 관여 적음
- HttpEntity → 요청 메시지 전체를 하나의 객체로 다룸
- @RequestBody → 가장 간결하고 실무에서 가장 많이 쓰임
- 이 모든 방식은 Content-Type과 관계없이 바디를 문자열로 읽을 수 있음
2) JSON
//요청 예시
POST /request-body-json-vX
Content-Type: application/json
Body: { "username": "hyunji", "age": 28 }
@Slf4j
@Controller
public class RequestBodyJsonController {
private ObjectMapper objectMapper = new ObjectMapper();
//HttpServletRequest를 사용해서 직접 HTTP 메시지 바디에서 데이터를 읽어와서, 문자로 변환한다.
//문자로 된 JSON 데이터를 Jackson 라이브러리인 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");
}
//@RequestBody를 사용해서 HTTP 메시지에서 데이터를 꺼내고 messageBody에 저장한다.
@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 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해준다.
//@RequestBody는 생략 불가능: HelloData에 @RequestBody를 생략하면 @ModelAttribute가 적용됨
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
//HttpEntity 사용
//HttpEntity나 @RequestBody를 사용하면 메시지 컨버터가 사용되어 메시지 바디 내용을 변환해준다.
@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";
}
}
버전 | 입력 처리 방식 | Argument Resolver | 메시지 컨버터 |
v1 | request. getInputStream() + 수동 파싱 |
❌ 없음 | 직접 objectMapper 사용 |
v2 | @RequestBody String |
RequestResponseBodyMethodProcessor | StringHttpMessageConverter |
v3 | @RequestBody HelloData |
RequestResponseBodyMethodProcessor | MappingJackson2HttpMessageConverter |
v4 | HttpEntity <HelloData> |
HttpEntityMethodProcessor | MappingJackson2HttpMessageConverter |
- @RequestBody
- HttpServletRequest의 InputStream을 직접 다루지 않고, 메시지 바디(JSON 등)를 바로 객체로 변환해줌
- 내부적으로 HttpMessageConverter가 JSON → Java 객체 변환 (ObjectMapper 사용)
4. HTTP 응답 메시지 전달
1) 정적 리소스 자동 서빙
- 정적 리소스는 서버에서 그대로 내려주는 파일을 말한다.
JSP나 Thymeleaf 같은 템플릿 엔진처럼 가공 없이, 있는 그대로 HTTP 응답 메시지 바디에 실려서 브라우저로 전송된다. - 스프링 부트에서 정적 리소스 위치
src/main/resources/static
src/main/resources/public
src/main/resources/resources
src/main/resources/META-INF/resources
- 정적 리소스 서빙 흐름
요청: http://localhost:8080/basic/hello-form.html
➜ DispatcherServlet 보다 먼저 실행되는 필터들이 정적 리소스 요청인지 확인
➜ basic/hello-form.html 경로가 /static 아래에 있으므로
Spring의 ResourceHttpRequestHandler가 파일을 찾아서 반환
➜ 해당 HTML 파일의 내용이 HTTP 메시지 바디에 그대로 실려서 클라이언트로 응답
- 정적 리소스 종류
파일 종류 | Content-Type | 메시지 바디 내용 |
.html | text/html | HTML 텍스트 |
.css | text/css | CSS 텍스트 |
.js | application/javascript | JS 텍스트 |
.png | image/png | 바이너리 이미지 |
.woff2 | font/woff2 | 바이너리 폰트 |
- 요약: src/main/resources/static 폴더 아래에 위치한 정적 리소스들은
컨트롤러 없이도 DispatcherServlet보다 앞 단계에서 Spring Boot가 자동으로 서빙해준다.
요청 경로와 동일한 이름의 파일을 찾아 응답 메시지 바디에 그대로 담아 전송한다
2) 뷰 템플릿을 통한 HTML 전달 (SSR)
- 스프링 MVC는 ModelAndView나 문자열 리턴 방식으로 뷰 템플릿(Thymeleaf, JSP 등)을 사용해 HTML 응답을 생성할 수 있다
- 이렇게 생성된 HTML은 HTTP 메시지 바디에 그대로 포함되어 전송된다.
- 뷰 템플릿 경로:src/main/resources/templates
@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 리턴 + Model 세팅
@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!!");
}
}
- ModelAndView 직접 생성+세팅해서 리턴
뷰 이름 | "response/hello" |
ReturnValueHandler | ModelAndViewMethodReturnValueHandler 사용됨 |
ViewResolver 동작 | "response/hello" → response/hello.html 찾음 |
응답 | 렌더링된 HTML이 메시지 바디에 담겨서 전달됨 |
- String 리턴 + Model 세팅
뷰 이름 | "response/hello" |
ReturnValueHandler | ViewNameMethodReturnValueHandler 사용됨 |
- void 리턴 + 암묵적 뷰 이름 결정
뷰 이름 | 리턴 없음 → 요청 URL 기준 "response/hello" |
ReturnValueHandler | ViewNameMethodReturnValueHandler가 URL에서 뷰 이름 유추 |
- 응답 흐름 요약
컨트롤러가 뷰 템플릿 이름을 문자열로 리턴하거나, void를 리턴해 요청 URL로부터 뷰 이름을 유추하는 경우
→ ViewNameMethodReturnValueHandler가 동작
뷰 이름과 모델을 담은 ModelAndView 객체를 생성해 DispatcherServlet에 전달
→ DispatcherServlet은 ModelAndView에서 뷰 이름을 꺼내어 ViewResolver를 통해 해당 뷰를 조회
→ ViewResolver는 논리 뷰 이름을 물리 경로(예: /templates/response/hello.html)로 변환한 뒤,
뷰 렌더링을 담당하는 View 객체를 반환한다.
→ 최종적으로 View 객체가 모델 데이터를 바탕으로 HTML을 렌더링
→ HTTP 응답 메시지 바디에 HTML 결과를 담아 클라이언트에 전달
3) HTTP API - JSON 데이터 전달 (CSR)
@Slf4j
@Controller
//@RestController
public class ResponseBodyController {
//1. 서블릿 직접 사용 (HttpServletResponse)
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException{
response.getWriter().write("ok");
}
//2. ResponseEntity<String> 사용
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
return new ResponseEntity<>("ok", HttpStatus.OK);
}
//3. String + @ResponseBody 사용
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
return "ok";
}
//4. ResponseEntity<HelloData> 사용
@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. HelloData + @ResponseBody 사용
//@ResponseStatus로 응답코드 설정 가능 (동적 설정은 불가능)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
}
메서드 | 리턴타입 | ReturnValueHandler | MessageConverter |
v1 | void + 수동 작성 | ❌ | ❌ |
v2 | ResponseEntity <String> |
HttpEntityMethodProcessor | StringHttpMessageConverter |
v3 | String + @ResponseBody |
RequestResponseBodyMethodProcessor | StringHttpMessageConverter |
v4 | ResponseEntity <HelloData> |
HttpEntityMethodProcessor | MappingJackson2HttpMessageConverter |
v5 | @ResponseBody HelloData |
RequestResponseBodyMethodProcessor | MappingJackson2HttpMessageConverter |
- @ResponseBody
- HttpServletResponse의 OutputStream을 직접 다루지 않고 객체 → JSON 변환해서 HTTP 메시지 바디에 넣어줌
- 내부적으로 HttpMessageConverter를 사용해 리턴 객체를 JSON, String 등으로 직렬화
- 변환된 데이터는 HttpServletResponse의 메시지 바디에 직접 작성된다.
- 즉 @ResponseBody, @RequestBody는 HttpServletRequest의 Stream을 직접 다루기 않도 메시지 바디에서 데이터를 편리하게 다룰 수 있도록 해주는 기능이다.
- @ResponseBody가 메시지 바디에 바로 데이터를 입력하는 경우
- DispatcherServlet에는 null을 반환하므로 이후 DispatcherServlet은 뷰를 처리할 필요가 없다.
- 컨트롤러 메서드의 리턴값을 ModelAndView로 변환하여 DispatcherServlet에 반환하는 @Controller 방식과 다르다.
- @RestController
- @RestController = @Controller + @ResponseBody
- @Controller 대신에 @RestController 애노테이션을 사용하면,
해당 컨트롤러에 모두 @ResponseBody가 적용되는 효과가 있다. - 이름 그대로 Rest API(HTTP API)를 만들 때 사용하는 컨트롤러이다.