Spring/MVC

[Spring MVC] 스프링 MVC 기본 기능 정리

lumana 2025. 3. 9. 02:37

 

스프링 MVC 기본 기능 정리

#Abstract/SpringMVC


스프링 MVC 구조와 동작원리, 스프링 MVC가 제공하는 기능의 기본적인 사용법을 정리했습니다.


/스프링 MVC 구조


/컨트롤러 등록


/HTTP 조회 방식


/HTTP 응답 방식


스프링 MVC 구조


스프링 MVC의 큰 강점은 DispatcherServlet 코드의 변경 없이, 원하는 기능을 변경하거나 확장할 수 있다는 점이다. 지금까지 설명한 대부분을 확장 가능할 수 있게 인터페이스로 제공한다.


ArgumentResolver와 HTTP 메시지 컨버터

스프링 MVC는 다음의 경우에 ArgumentResolver를 적용한다.

  • HTTP 요청: HttpServletRequest, Model, @RequestParam, @ModelAttribute, @RequestBody, HttpEntity(RequestEntity)
  • HTTP 응답: ModelAndView @ResponseBody, HttpEntity(ResponseEntity)
  • 다형성으로 여러 종류가 있다.

이후 스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용한다.

  • HTTP 요청: @RequestBody, HttpEntity(RequestEntity)
  • HTTP 응답: @ResponseBody, HttpEntity(ResponseEntity)
  • 다형성으로 여러 종류가 있다.

ArgumentResolver


RequestMappingHandlerAdapter 동작 방식


ArgumentResolver
생각해보면, 애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다.


HttpServletRequest, Model은 물론이고, @RequestParam, @ModelAttribute 같은 애노테이션 그리고 @RequestBody, HttpEntity 같은 HTTP 메시지를 처리하는 부분까지 매우 큰 유연함을 보여주었다.
이렇게 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분이다.


애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapter는 바로 이 ArgumentResolver를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성한다. 그리고 이렇게 파라미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨준다.


정확히는 HandlerMethodArgumentResolver인데 줄여서 ArgumentResolver라고 부른다.

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);
    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable
    ModelAndViewContainer mavContainer,
    NativeWebRequest webRequest, @Nullable WebDataBinderFactory
    binderFactory) throws Exception;
}

동작 방식
ArgumentResolversupportsParameter()를 호출해서 해당 파라미터를 지원하는지 체크하고, 지원하면 resolveArgument()를 호출해서 실제 객체를 생성한다. 그리고 이렇게 생성된 객체가 컨트롤러 호출 시에 넘어가는 것이다.


그리고 원한다면 여러분이 직접 이 인터페이스를 확장해서 원하는 ArgumentResolver를 만들 수도 있다.


ReturnValueHandler

HandlerMethodReturnValueHandler를 줄여서 ReturnValueHandler라고 부른다.
ArgumentResolver와 비슷한데, 이것은 응답 값을 변환하고 처리한다.


컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유가 바로 ReturnValueHandler 덕분이다.


HTTP 메시지 컨버터


HTTP 메시지 컨버터를 사용하는 @RequestBody도 컨트롤러가 필요로 하는 파라미터의 값에 사용된다.
@ResponseBody의 경우도 컨트롤러의 반환 값을 이용한다.


요청의 경우 @RequestBody 를 처리하는 ArgumentResolver 가 있고, HttpEntity 를 처리하는 ArgumentResolver 가 있다(다형성). 이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성 하는 것이다. (어떤 종류가 있는지 코드로 살짝 확인해보자)


응답의 경우 @ResponseBodyHttpEntity 를 처리하는 ReturnValueHandler 가 있다. 그리고 여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.


스프링 MVC는@RequestBody @ResponseBody 가 있으면 RequestResponseBodyMethodProcessor(ArgumentResolver, ReturnValueHandler 둘다 구현)


HttpEntity 가 있으면 HttpEntityMethodProcessor(ArgumentResolver, ReturnValueHandler 둘다 구현) 를 사용한다.


public interface HttpMessageConverter<T> {
    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
    List<MediaType> getSupportedMediaTypes();
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
    throws IOException, HttpMessageNotReadableException;
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage
    outputMessage)
    throws IOException, HttpMessageNotWritableException;
}

HTTP 메시지 컨버터는 HTTP 요청, HTTP 응답 둘 다 사용된다. 양방향이다.

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

스프링 부트 기본 메시지 컨버터
(일부 생략)

0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter


  • ByteArrayHttpMessageConverter
    • byte[] 데이터를 처리한다.
    • 클래스 타입: byte[], 미디어타입: */*
    • 요청 예) @RequestBody byte[] data
    • 응답 예) @ResponseBody return byte[]
    • 쓰기 미디어타입: application/octet-stream
  • StringHttpMessageConverter
    • String 문자로 데이터를 처리한다.
    • 클래스 타입: String, 미디어타입: */*
    • 요청 예) @RequestBody String data
    • 응답 예) @ResponseBody return "ok"
    • 쓰기 미디어타입: text/plain
  • MappingJackson2HttpMessageConverter
    • application/json
    • 클래스 타입: 객체 또는 HashMap, 미디어타입 application/json 관련
    • 요청 예) @RequestBody HelloData data
    • 응답 예) @ResponseBody return helloData
    • 쓰기 미디어타입: application/json 관련

? (안 되는 케이스)

content-type: text/html
@RequestMapping
void hello(@RequestBody HelloData data) {}

미디어 타입이 application/json 관련 X 라서 실패한다.


HTTP 요청 데이터 읽기

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

HTTP 응답 데이터 생성

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

스프링 MVC 확장

스프링은 다음을 모두 인터페이스로 제공한다. 따라서 필요하면 언제든지 기능을 확장할 수 있다.

  • HandlerMethodArgumentResolver
  • HandlerMethodReturnValueHandler
  • HttpMessageConverter

스프링이 필요한 대부분의 기능을 제공하기 때문에 실제 기능을 확장할 일이 많지는 않다. 기능 확장은 WebMvcConfigurer를 상속 받아서 스프링 빈으로 등록하면 된다.



컨트롤러 등록

HandlerMapping

0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.

HandlerAdapter
0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리

ViewResolver

1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능 에 사용) 
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다. 

컨트롤러 등록 - Controller 인터페이스 구현(거의 안씀)

  1. 스프링 빈의 이름으로 URL을 매핑(스프링 빈 이름으로 핸들러를 찾고, 어댑터가 이 핸들러를 실행해줌)
    • HandlerMapping = BeanNameUrlHandlerMapping
    • HandlerAdapter = SimpleControllerHandlerAdapter
  2. Controller 인터페이스 구현
    • 로직 구현하고, ModelAndView를 반환
      • BeanNameViewResolver 탐색 : 스프링 빈으로 등록된 뷰가 없음
      • InternalResourceViewResolver라는 뷰 리졸버가 논리 뷰 이름을 보고 JSP를 처리할 수 있는 뷰를 반환한다. (InternalResourceView 반환, Thymeleaf 뷰 템플릿을 사용하면 ThymeleafViewResolver)
      • view.render() InternalResourceViewforward() 를 사용(JSP가 아닌 경우 foward 과정 X)
@Component("/springmvc/old-controller")
public class OldController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("OldController.handleRequest");
        return null;
    }
}

컨트롤러 등록 - HttpRequestHandler 인터페이스 구현(거의 안씀)

  1. 스프링 빈의 이름으로 URL을 매핑(스프링 빈 이름으로 핸들러를 찾고, 어댑터가 이 핸들러를 실행해줌)
    • HandlerMapping = BeanNameUrlHandlerMapping
    • HandlerAdapter = HttpRequestHandlerAdapter
  2. HttpRequestHandler 인터페이스 구현
@Component("/springmvc/request-handler")
public class MyHttpRequestHandler implements HttpRequestHandler { // 서블릿과 유사
    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("MyHttpRequestHandler.handleRequest");
    }
}

컨트롤러 등록 - @Controller, @RestController + @RequestMapping, @XXXMapping

@RequestMapping

  • RequestMappingHandlerMapping
  • RequestMappingHandlerAdapter

@Controller

  • 스프링이 자동으로 스프링 빈으로 등록한다. (내부에 @Component 애노테이션이 있어서 컴포넌트 스캔의 대상이 됨)
  • 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다.
    • RequestMappingHandlerMapping이 꺼내지는 기준이 된다.

@RequestMapping

  • 요청 정보를 매핑한다.
  • 해당 URL이 호출되면 이 메서드가 호출된다.
  • 애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지으면 된다.
    • RequestMappingHandlerMapping 은 스프링 빈 중에서 @Controller클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식한다.
  • 클래스 단위 + 메서드 단위로 조합하여 사용할 수 있다.
    • ex)
      • 클래스 레벨: @RequestMapping("/springmvc/v2/members")
      • 메서드 레벨:
        • @RequestMapping("/new-form")
        • @RequestMapping("/save")
  • 대부분의 속성을 배열 [] 로 제공하므로 다중 설정이 가능하다. {"/hello-basic", "/hello-go"}

@Controller
public class SpringMemberFormControllerV1 {
    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process() {
        return new ModelAndView("new-form");
    }
}

@GetMapping, @PostMapping
@RequestMapping 은 URL만 매칭하는 것이 아니라, HTTP Method도 함께 구분할 수 있다.
예를 들어서 URL이 /new-form 이고, HTTP Method가 GET인 경우를 모두 만족하는 매핑을 하려면 다음과 같이 처리하면 된다.

@RequestMapping(value = "/new-form", method = RequestMethod.GET)

이것을 @GetMapping, @PostMapping 으로 더 편리하게 사용할 수 있다.
참고로 @Get, @Post, @Put, @Delete, @Patch 모두 애노테이션이 준비되어 있다.


@GetMapping
public String members(Model model)

@RestController

  • @Controller 는 반환 값이 String 이면 뷰 이름으로 인식된다. 그래서 뷰를 찾고 뷰가 랜더링 된다.
  • @RestController 는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한다.

HTTP 조회 방식

HTTP 요청 파라미터 조회

두 가지 방식을 HTTP 요청 파라미터(request parameter) 조회라고 한다.

  1. GET - 쿼리 파라미터
    • /url?username=hello&age=20
    • 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
    • 예) 검색, 필터, 페이징 등에서 많이 사용하는 방식
  2. POST - HTML Form
    • Content-Type: application/x-www-form-urlencoded
    • 메시지 바디에 쿼리 파라미터 형식으로 전달 username=hello&age=20
    • 예) 회원 가입, 상품 주문, HTML Form 사용

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


HTTP 요청 파라미터 조회 - 스프링 MVC

GET: http://localhost:8080/request-param-v1?username=hello&age=20
또는
POST: Form


1. HttpServletRequest에서 request.getParameter()로 꺼내서 조회

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

2. @RequestParam

스프링은 HTTP 요청 파라미터를 @RequestParam 으로 받을 수 있다.

public String requestParamV2(
    @RequestParam("username") String memberName,
    @RequestParam("age") int memberAge
  • @RequestParam: 파라미터 이름으로 바인딩
  • @ResponseBody: View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
  • HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
    • public String requestParamV3(@RequestParam String username, @RequestParam int age)
  • String, int 등의 단순 타입이면 @RequestParam 도 생략 가능
    • 주의! @RequestParam 애노테이션을 생략하면 스프링 MVC는 내부에서 required=false를 적용한다.
    • 개인적으론 @RequestParam 은 생략 안하는게 코드를 이해하는데 더 나은듯.
  • 파라미터 필수 여부를 지정할 수 있다.
    • @RequestParam.required
    • 기본값이 파라미터 필수 (true)이다.
    • public String requestParamRequired(@RequestParam(required = true) String username)
    • 주의! - 파라미터 이름만 사용
      • /request-param-required?username=
      • 파라미터 이름만 있고 값이 없는 경우 빈문자로 통과. null이 아니다.
    • 주의! - 기본형(primitive)에 null 입력
      • /request-param-required
      • @RequestParam(required = false) int age
        • null을 int에 입력하는 것은 불가능(500 예외 발생)
  • 기본값을 지정할 수 있다.
    • 파라미터에 값이 없는 경우 defaultValue를 사용하면 기본 값을 적용할 수 있다.
    • 이미 기본 값이 있기 때문에 required는 의미가 없다.
    • defaultValue빈 문자의 경우에도 설정한 기본 값이 적용된다.
    • /request-param-default?username=
public String requestParamDefault(
    @RequestParam(required = true, defaultValue = "guest") String username,
    @RequestParam(required = false, defaultValue = "-1") int age

3. Map으로 조회

파라미터를 Map, MultiValueMap으로 조회할 수 있다.

  • @RequestParam Map
    • Map(key=value)
  • @RequestParam MultiValueMap
    • MultiValueMap(key=[value1, value2, ...])
    • 예) key=userIds, value=[id1, id2]
  • 파라미터의 값이 1개가 확실하다면 Map을 사용해도 되지만, 그렇지 않다면 MultiValueMap을 사용하자.

@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam Map<String, Object> paramMap)

4. @ModelAttribute

요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주는 작업을 @ModelAttribute를 통해 자동화한다.

@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData)

  • 스프링 MVC는 @ModelAttribute가 있으면 다음을 실행한다.
    1. HelloData 객체를 생성한다.
    2. 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩)한다.
      • 예) 파라미터 이름이 username이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력한다.
      • 프로퍼티
        • getXxx xxx
        • setXxx xxx
  • age=abc처럼 숫자가 들어가야 할 곳에 문자를 넣으면 BindException이 발생한다.
  • @ModelAttribute는 생략할 수 있다.
    • 그런데 @RequestParam도 생략할 수 있으니 혼란이 발생할 수 있다. 개인적으론 생략 안하는게 좋은듯
  • 스프링은 해당 생략 시 다음과 같은 규칙을 적용한다.
    • String, int, Integer 같은 단순 타입 = @RequestParam
    • 나머지 = @ModelAttribute (argument resolver로 지정해둔 타입은 ModelAttribute가 적용되지 않는다. e.g. HttpServletRequest. 추가로 지정할 수 있음)

Model에 자동으로 추가되는 기능

  • model.addAttribute(helloData) 코드도 함께 자동 적용된다.
    • ModelAttribute로 받은 객체는 view에서 다시 사용할 확률이 높기 때문에 자동으로 model에 넣어주는 것이다.
  • Model에는 key-value로 저장하는데, key로는 @ModelAttribute(“item”)에서 지정한 “item”이, 그리고 value 객체로는 ModelAttribute의 대상이 되는 item 객체가 들어간다. 만약 @ModelAttribute(“item2”)라면 item 객체의 key는 “item2”가 된다.
  • 모델에 데이터를 담을 때는 이름이 필요하다. 이름은@ModelAttribute 에 지정한 name(value) 속성을 사용한다. 만약 다음과 같이 @ModelAttribute 의 이름을 다르게 지정하면 다른 이름으로 모델에 포함된다.
  • @ModelAttribute("hello") Item item
    • 이름을 hello 로 지정
    • model.addAttribute("hello", item);
      • 모델에 hello 이름으로저장
  • @ModelAttribute의 이름을 생략할 수 있다. 이 때 item 객체의 key로는 어떤 규칙에 따라 사용될까?
    • 클래스 이름의 첫 글자만 소문자로 바꿔서 key로 사용된다. 객체의 이름으로 key가 사용될 것이라고 헷갈리지 말자.
      • Item item
      • HelloWorld helloWorld
  • @ModelAttribute 자체도 생략 가능하다. 대상 객체는 모델에 자동 등록된다. 나머지 사항은 기존과 동일하다.


HTTP 메시지 바디 조회

HTTP message body에 데이터를 직접 담아서 요청하는 경우

  • HTTP API에서 주로 사용, JSON, XML, TEXT
  • 데이터 형식은 주로 JSON 사용
  • POST, PUT, PATCH

보통 메시지 Body에 데이터를 담아서 요청하는 경우 응답도 Body에 담아서 보내주는 경우가 많다.


HTTP 메시지 바디 텍스트 조회 - 스프링 MVC

요청 파라미터와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam , @ModelAttribute 를 사용할 수 없다. (물론 HTML Form 형식으로 전달되는 경우는 요청 파라미터로 인정된다.)


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

2. Input, Output 스트림 파라미터 바로 사용

public void requestBodyStringV2(InputStream inputStream, Writer responseWriter)

스프링 MVC는 다음 파라미터를 지원한다.

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

3. HttpEntity 파라미터 사용

@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
    String messageBody = httpEntity.getBody();

스프링 MVC는 다음 파라미터를 지원한다.

  • HttpEntity: HTTP header, body 정보를 편리하게 조회
    • 스프링이 HttpBody가 바디가 문자임을 확인 HttpMessageConverter가 동작해서 HttpBody에 있는 걸 문자로 바꿔서 HttpEntity에다가 넣어준다.
      • InputStream을 String으로 변환하는 과정을 스프링이 해준다.
      • HttpEntity는 Http 스펙을 객체화 해둔거라 보면 된다.
    • 메시지 바디 정보를 직접 조회
    • 요청 파라미터를 조회하는 기능과 관계 없음 @RequestParam X, @ModelAttribute X
      • Get에 쿼리 스트링, Post Form 데이터 전송만 위 두 개를 사용한다.
  • HttpEntity는 응답에도 사용 가능
    • 메시지 바디 정보 직접 반환
    • 헤더 정보 포함 가능
    • view 조회X

4. HttpEntity 상속받은 객체 사용

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

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

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

5. @RequestBody 사용

@RequestBody를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회할 수 있다. 참고로 헤더 정보가 필요하다면 HttpEntity를 사용하거나 @RequestHeader를 사용하면 된다.


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

@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {

@ResponseBody
@ResponseBody를 사용하면 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달할 수 있다. 물론 이 경우에도 view를 사용하지 않는다.


HTTP 메시지 바디 JSON 조회 - 스프링 MVC

HTTP API에서 주로 사용하는 JSON 데이터 형식을 사용한다.


1. 문자열 꺼낸 뒤 ObjectMapper 사용

public class RequestBodyJsonController {
    private ObjectMapper objectMapper = new 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);
        HelloData data = objectMapper.readValue(messageBody, HelloData.class);

  • HttpServletRequest를 사용해서 직접 HTTP 메시지 바디에서 데이터를 읽어와서, 문자로 변환한다.
  • 문자로 된 JSON 데이터를 Jackson 라이브러리인 objectMapper를 사용해서 자바 객체로 변환한다.

문자열 꺼내는 과정을

  • 직접 파라미터에 InputStream 사용해도 된다.
  • HttpEntity.getBody() 사용
  • HttpEntity 상속받은 RequestEntity 사용
  • @RequestBody 문자변환

으로 간소화할 수 있다.


그리고 @RequestBody, @HttpEntity 는 문자 변환 말고도 객체변환을 지원한다.


2. 객체 변환(@RequestBody, HttpEntity)

@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {

@RequestBody 객체 파라미터

  • @RequestBody에 직접 만든 객체를 지정할 수 있다.
  • @RequestBody 생략 불가능
    • 생략하면 @ModelAttribute 가 적용되어 버림
    • HTTP 메시지 바디가 아니라 요청 파라미터를 처리하게 된다.
  • HttpMessageConverter 사용 MappingJackson2HttpMessageConverter (content-type: application/json)
    • HttpEntity, @RequestBody를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해준다.
    • HTTP 메시지 컨버터는 문자 뿐만 아니라 JSON도 객체로 변환해주는데(MappingJackson2HttpMessageConverter), 우리가 방금 V2에서 했던 작업을 대신 처리해준다.
  • HTTP 요청 시에 content-typeapplication/json인지 꼭! 확인해야 한다. 그래야 JSON을 처리할 수 있는 HTTP 메시지 컨버터가 실행된다.

HttpEntity 객체 파라미터

@ResponseBody
@PostMapping("/request-body-json-v4")
public HelloData requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
    HelloData data = httpEntity.getBody();
	return data;
}

@ResponseBody
응답의 경우에도 @ResponseBody를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있다. 물론 이 경우에도 HttpEntity를 사용해도 된다.


@RequestBody 요청

  • JSON 요청 HTTP 메시지 컨버터 객체

@ResponseBody 응답

  • 객체 HTTP 메시지 컨버터 JSON 응답

경로변수: @PathVariable

  • @RequestMapping은 URL 경로를 템플릿화 할 수 있는데, @PathVariable을 사용하면 매칭 되는 부분을 편리하게 조회할 수 있다.
  • 파라미터 이름이 경로 변수 이름(@PathVariable의 이름)과 같으면 생략 가능
  • @PathVariable("userId") String userId @PathVariable String userId
  • 주의! @PathVariable 자체를 생략할 수는 없다.
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable String userId) {

  • PathVariable을 다중으로 사용할 수도 있다.
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) {

특정 조건 매핑(잘 안씀)

특정 파라미터 정보가 있어야 호출 되도록 한다.


파라미터로 추가 매핑

  • params="mode",
  • params="!mode"
  • params="mode=debug"
  • params="mode!=debug" (! = )
  • params = {"mode=debug","data=good"}
  • @GetMapping(value = "/mapping-param", params = "mode=debug")

특정 헤더 조건 매핑

  • headers="mode",
  • headers="!mode"
  • headers="mode=debug"
  • headers="mode!=debug" (! = )
  • @GetMapping(value = "/mapping-header", headers = "mode=debug")

미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume
클라이언트한테 받아서 서버에서 사용 consumes

  • consumes="application/json"
  • consumes="!application/json"
  • consumes="application/*"
  • consumes="/"
  • consumes = MediaType.APPLICATION_JSON_VALUE
  • consumes = "text/plain"
  • consumes = {"text/plain", "application/*"}
  • consumes = MediaType.TEXT_PLAIN_VALUE
  • @PostMapping(value = "/mapping-consume", consumes = "application/json")

미디어 타입 조건 매핑 - HTTP 요청 Accept, produce
서버가 만들어서 반환해주는 것 : produces

  • HTTP 요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑한다.
  • 만약 맞지 않으면 HTTP 406 상태코드(Not Acceptable)을 반환한다.
  • Accept 헤더는 보통 */*인데, 클라이언트가 헤더에 application/json을 넣으면 나는 응답으로 application/json 밖에 못 받는다는 말이다. 그래서 text/html을 반환해주는 컨트롤러에서 406 에러를 반환한다.
  • produces = "text/html"
  • produces = "!text/html"
  • produces = "text/*"
  • produces = "/"
  • produces = "text/plain"
  • produces = {"text/plain", "application/*"}
  • produces = MediaType.TEXT_PLAIN_VALUE
  • produces = "text/plain;charset=UTF-8"
  • @PostMapping(value = "/mapping-produce", produces = "text/html")

HTTP 헤더 정보 파라미터

  • HttpServletRequest
  • HttpServletResponse
  • HttpMethod: HTTP 메서드를 조회한다. org.springframework.http.HttpMethod
  • Locale: Locale 정보를 조회한다.
  • @RequestHeader MultiValueMap<String, String> headerMap
    • 모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다.
  • @RequestHeader("host") String host
    • 특정 HTTP 헤더를 조회한다.
    • 속성
      • 필수 값 여부:required
      • 기본 값 속성:defaultValue
  • @CookieValue(value = "myCookie", required = false) String cookie
    • 특정 쿠키를 조회한다.
    • 속성
      • 필수 값 여부:required
      • 기본 값: defaultValue
@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
) 

HTTP 응답 방식

HTTP 응답 - 정적 리소스, 뷰 템플릿

  1. 정적 리소스
    • 정적 리소스 경로: src/main/resources/static
    • 리소스를 보관하는 곳이고, 또 클래스패스의 시작 경로이다. 따라서 위 디렉토리에 리소스를 넣어두면 스프링 부트가 정적 리소스로 서비스를 제공한다.
    • 스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공한다.
      • /static, /public, /resources, /META-INF/resources
    • 예) 웹 브라우저에 정적인 HTML, CSS, JS를 제공할 때는, 정적 리소스를 사용한다.
  2. 뷰 템플릿 사용
    • 뷰 템플릿 경로: src/main/resources/templates
    • 뷰 템플릿을 거쳐서 HTML이 생성되고, 뷰가 응답을 만들어서 전달한다.
      • 일반적으로 HTML을 동적으로 생성하는 용도로 사용하지만, 다른 것들도 가능하다.
    • 예) 웹 브라우저에 동적인 HTML을 제공할 때는 뷰 템플릿을 사용한다.

HTTP 응답 - 정적 리소스, 뷰 템플릿: 스프링 MVC 뷰 템플릿 호출


1. ModelAndView 반환

@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1() {
    ModelAndView mav = new ModelAndView("response/hello")
    .addObject("data", "hello!");
    return mav;
}

스프링이 제공하는 ModelAndView 를 통해 Model 데이터를 추가할 때는 addObject() 를 사용하면 된다. 이 데이터는 이후 뷰를 렌더링 할 때 사용된다.

  • ex)
    • mv.addObject("member", member);
    • mv.addObject("members", members);

2. 논리 이름 반환

@RequestMapping("/response-view-v2")
public String responseViewV2(Model model) {
    model.addAttribute("data", "hello!!");
    return "response/hello";
}

Model
파라미터에 모델 넣으면 됨.

@GetMapping
public String members(Model model) {

ViewName 직접 반환
뷰의 논리 이름을 반환

@GetMapping
public String members(Model model) {
	// ...
	return "members";
}

String을 반환하는 경우 - View or HTTP 메시지
@ResponseBody가 없으면 response/hello로 뷰 리졸버가 실행되어서 뷰를 찾고, 렌더링 한다.
@ResponseBody가 있으면 뷰 리졸버를 실행하지 않고, HTTP 메시지 바디에 직접 response/hello라는 문자가 입력된다.


3. void 반환

@Controller를 사용하고, HttpServletResponse, OutputStream(Writer) 같은 HTTP 메시지 바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용

  • 요청 URL: /response/hello
  • 실행: templates/response/hello.html
  • 참고로 이 방식은 명시성이 너무 떨어지고 이렇게 딱 맞는 경우도 많이 없어서, 권장하지 않는다.

HTTP 응답 - HTTP 메시지 바디(HTTP API)

HTTP 메시지 사용

  • HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.

HTTP 응답 - HTTP 메시지 문자열: Spring MVC

1. HttpServletResponse의 writer에 문자열 전달

@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
    response.getWriter().write("ok");
}

2. HttpEntity, ResponseEntity(Http Status 추가)

ResponseEntity 엔티티는 HttpEntity를 상속받았는데, HttpEntity는 HTTP 메시지의 헤더, 바디 정보를 가지고 있다. ResponseEntity는 여기에 더해서 HTTP 응답 코드를 설정할 수 있다.


HttpStatus.CREATED로 변경하면 201 응답이 나가는 것을 확인할 수 있다.

@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
    return new ResponseEntity<>("ok", HttpStatus.OK);
}
return new ResponseEntity<String>("Hello World", responseHeaders,
HttpStatus.CREATED)

3. @ResponseBody 문자열 반환

@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
    return "ok";
}

@ResponseBody를 사용하면 view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력할 수 있다. ResponseEntity도 동일한 방식으로 동작한다.


HTTP 응답 - HTTP 메시지 JSON: Spring MVC

1. ResponseEntity 객체 JSON으로 바로 변환

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

ResponseEntity를 반환한다. HTTP 메시지 컨버터를 통해서 JSON 형식으로 변환되어서 반환된다. 동적으로 응답 코드를 바꿀 수 있다.


2. @RequestBody: 객체 JSON으로 바로 변환

@ResponseStatus(HttpStatus.OK) // 단순히 객체를 반환하면 상태 코드 지정이 안 되니, 애노테이션을 사용한다
// 애노테이션 방식이므로 동적으로 응답 상태 코드를 바꿀 수 없다.
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2() {
    HelloData helloData = new HelloData();
    helloData.setUsername("userA");
    helloData.setAge(20);
    return helloData;
}

(클래스 레벨에 @RestController가 아니라 @Controller가 붙어있으면 @ResponseBody를 붙여줘야 객체가 바로 변환된다)


ResponseEntity는 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody를 사용하면 이런 것을 설정하기는 어렵다.
@ResponseStatus(HttpStatus.OK) 애노테이션을 사용하면 응답 코드도 설정할 수 있다.


물론 애노테이션이기 때문에 응답 코드를 동적으로 변경할 수는 없다. 프로그램 조건에 따라서 동적으로 변경하려면 ResponseEntity를 사용하면 된다.


@RestController
@Controller 대신에 @RestController 애노테이션을 사용하면, 해당 컨트롤러에 모두 @ResponseBody가 적용되는 효과가 있다. 따라서 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 데이터를 입력한다. 이름 그대로 REST API(HTTP API)를 만들 때 사용하는 컨트롤러이다.


참고로 @ResponseBody는 클래스 레벨에 두면 전체 메서드에 적용되는데, @RestController 애노테이션 안에 @ResponseBody가 적용되어 있다.


Ref) 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의 | 김영한 - 인프런

'Spring > MVC' 카테고리의 다른 글

[Spring MVC] 스프링 MVC Redirect 처리 방법  (0) 2025.03.09
[Spring MVC] 타임리프 문법 정리  (0) 2025.02.13
[Spring/MVC] 12. Bean Validation  (0) 2025.01.19
[Spring/MVC] 11. Validation  (0) 2025.01.19
[Spring/MVC] 10. 메시지 국제화  (0) 2025.01.19