8. 스프링 MVC 기본 기능

2025. 5. 8. 18:06Spring/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에서 추출해서 바인딩해줌

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)를 만들 때 사용하는 컨트롤러이다.