요청 매핑과 요청 데이터
3줄 요약
Spring MVC는 URL, HTTP method, header, content type 조건으로 handler method를 선택한다.
선택된 handler method의 파라미터는 ArgumentResolver가 요청 정보에서 값을 꺼내 만들어 준다.
요청 데이터는 query parameter, path variable, form data, cookie, header, request body처럼 출처가 다르므로 애너테이션도 다르게 선택해야 한다.
핵심 정리
@RequestMapping과 @GetMapping, @PostMapping은 요청 조건을 handler method에 연결한다.
@RequestParam은 query string이나 form field 같은 요청 파라미터를 읽는다.
@PathVariable은 URL 경로 템플릿의 일부를 읽는다.
@ModelAttribute는 요청 파라미터를 객체의 프로퍼티나 생성자 인자에 바인딩한다.
@RequestBody는 HTTP body를 HttpMessageConverter로 읽어 객체로 변환한다.
form data는 Servlet request parameter 처리와 연결되므로 JSON body처럼 무조건 @RequestBody로 읽는다고 생각하면 안 된다. 일반 form은 @RequestParam이나 @ModelAttribute, multipart의 일부 JSON은 @RequestPart를 검토한다.
헷갈리는 지점
@RequestParam과 @RequestBody를 둘 다 클라이언트가 보낸 값이라고만 이해하기 쉽다. 둘 다 요청 데이터이기 때문이다.
핵심은 데이터 위치가 다르다는 점이다.
query/form parameter는 @RequestParam 계열이고 JSON body는 @RequestBody 계열이다.
@PathVariable과 @RequestParam을 모두 문자열 값 추출로만 보기 쉽다. Controller에서는 둘 다 파라미터로 들어오기 때문이다.
핵심은 URL 경로의 식별자는 @PathVariable, 필터나 옵션 값은 @RequestParam이 자연스럽다는 점이다.
/members/{id}의 id와 /members?page=1의 page는 의미가 다르다.
@ModelAttribute를 화면 MVC 전용으로만 생각하기 쉽다. 이름에 Model이 들어가기 때문이다.
핵심은 요청 파라미터를 객체에 바인딩하는 기능이라는 점이다.
form 제출과 query parameter 기반 검색 조건 DTO에 모두 사용할 수 있다.
@ModelAttribute와 @RequestBody를 둘 다 객체 생성 도구로만 보기 쉽다. 둘 다 DTO를 만들어 주기 때문이다.
핵심은 입력을 읽는 통로가 다르다는 점이다.
@ModelAttribute는 request parameter 기반 바인딩이고, @RequestBody는 MessageConverter 기반 body 읽기다.
확인 질문
JSON 요청 body를 DTO로 받고 싶다면 무엇을 쓰는가?
URL 경로의 일부 값을 받고 싶다면 무엇을 쓰는가?
여러 요청 파라미터를 객체 하나에 묶고 싶다면 무엇을 쓰는가?
타입 변환과 데이터 바인딩
3줄 요약
HTTP 요청 값은 기본적으로 문자열이므로 Java 타입으로 변환하는 과정이 필요하다.
Spring은 ConversionService와 DataBinder를 통해 문자열 값을 primitive, wrapper, enum, date, 객체 프로퍼티로 변환한다.
바인딩 오류와 검증 오류를 구분해야 입력 처리 흐름을 안정적으로 설계할 수 있다.
핵심 정리
?age=20은 HTTP에서는 문자열이지만 Controller에서는 int age로 받을 수 있다.
Spring은 conversion과 binding 인프라를 통해 문자열 값을 필요한 타입으로 변환한다.
@ModelAttribute는 기본 생성자, setter, constructor binding 등 객체 생성/프로퍼티 바인딩 규칙과 연결된다.
타입 변환 실패는 validation 실패와 다르다. 숫자로 바꿀 수 없는 문자열은 검증 이전의 binding 문제다.
바인딩 결과를 직접 다뤄야 하는 화면 MVC에서는 BindingResult 위치가 중요하다.
헷갈리는 지점
타입 변환 실패와 Bean Validation 실패를 같은 것으로 보기 쉽다. 둘 다 입력 오류로 보이기 때문이다.
핵심은 변환 실패는 Java 타입을 만들지 못한 문제이고, validation 실패는 만들어진 값이 규칙을 어긴 문제라는 점이다.
“abc를 숫자로 변환 실패”와 “나이가 음수라 검증 실패”는 다른 층위다.
DTO에 setter가 없으면 바인딩이 무조건 안 된다고 생각하기 쉽다. 전통적인 JavaBean 바인딩 경험 때문이다.
핵심은 Spring 버전과 객체 형태에 따라 constructor binding도 가능하다는 점이다.
다만 팀 코드에서는 DTO 생성 규칙을 명확히 통일하는 편이 좋다.
모든 변환 규칙을 Controller에서 직접 처리하려고 하기 쉽다. 입력 값이 Controller에 들어오기 때문이다.
핵심은 반복되는 타입 변환은 Converter, Formatter, WebDataBinder 등 공통 인프라로 빼는 것이 좋다는 점이다.
Controller는 변환된 결과를 받아 요청 흐름을 제어하는 데 집중한다.
확인 질문
HTTP 요청 값이 Java 숫자 타입으로 들어올 수 있는 이유는 무엇인가?
Spring의 타입 변환 인프라가 문자열 요청 값을 대상 타입으로 변환하기 때문이다.
타입 변환 실패와 검증 실패는 무엇이 다른가?
변환 실패는 Java 타입 생성 자체가 실패한 것이고, 검증 실패는 변환된 값이 비즈니스/입력 규칙을 어긴 것이다.
반복되는 타입 변환 규칙은 어디로 분리하는 것이 좋은가?
Converter, Formatter, WebDataBinder 같은 공통 변환 인프라로 분리하는 것이 좋다.
RequestBody와 MessageConverter
3줄 요약
@RequestBody는 HTTP body를 읽어 Java 객체로 변환한다.
JSON 변환은 보통 Jackson 기반 HttpMessageConverter가 담당한다.
Content-Type, body 구조, DTO 생성 규칙이 맞지 않으면 400 또는 415 계열 문제가 발생할 수 있다.
핵심 정리
@RequestBody는 query parameter나 form parameter가 아니라 request body를 읽는다.
JSON 요청은 Content-Type: application/json과 converter 선택이 중요하다.
Jackson은 JSON 필드와 DTO 필드를 매핑하며, 생성자와 setter, record 구조에 따라 역직렬화 방식이 달라질 수 있다.
body는 기본적으로 한 번 읽는 stream 성격이 있으므로 Filter나 Interceptor에서 무심코 읽으면 Controller에서 문제가 생길 수 있다.
@Valid @RequestBody 검증 실패는 기본적으로 MethodArgumentNotValidException으로 이어지고, method parameter 자체의 제약 조건 검증은 HandlerMethodValidationException으로 이어질 수 있다.
요청 DTO는 API 계약이므로 Entity와 분리하는 편이 안전하다.
헷갈리는 지점
@RequestBody가 없는데도 JSON이 DTO에 들어올 것이라고 기대하기 쉽다. @ModelAttribute도 객체를 만들어 주기 때문이다.
핵심은 JSON body와 요청 파라미터 바인딩은 다른 경로라는 점이다.
JSON body는 MessageConverter가 읽어야 하므로 @RequestBody가 필요하다.
form submit도 body에 실려 오니까 @RequestBody가 맞다고 생각하기 쉽다. HTTP body라는 단어가 같기 때문이다.
핵심은 Servlet API에서 form parameter 접근은 body 파싱과 연결되어 있어 @RequestBody로 안정적으로 다시 읽기 어렵다는 점이다.
HTML form은 @ModelAttribute나 @RequestParam, multipart part 단위 데이터는 @RequestPart를 우선 검토한다.
Content-Type과 Accept를 혼동하기 쉽다. 둘 다 HTTP header이고 JSON 문제에서 자주 등장하기 때문이다.
핵심은 Content-Type은 요청 body 형식이고, Accept는 원하는 응답 형식이라는 점이다.
415는 주로 요청 body media type 문제, 406은 응답 형식 협상 문제로 본다.
Entity를 곧바로 요청 DTO로 쓰면 편하다고 느끼기 쉽다. 필드가 비슷하기 때문이다.
핵심은 API 입력 계약과 도메인 모델의 변경 이유가 다르다는 점이다.
요청 DTO를 분리하면 검증, 보안, 내부 모델 변경에 강해진다.
확인 질문
JSON 요청 body를 Java 객체로 변환하는 주요 컴포넌트는 무엇인가?
HttpMessageConverter이며, JSON은 보통 Jackson 기반 converter가 담당한다.
Content-Type은 무엇을 나타내는가?
클라이언트가 보낸 요청 body의 media type을 나타낸다.
요청 DTO와 Entity를 분리하는 이유는 무엇인가?
외부 API 계약과 내부 도메인 모델의 변경 이유가 다르기 때문이다.
응답 바인딩과 반환값 처리
3줄 요약
Controller 반환값은 ReturnValueHandler가 해석해 view rendering 또는 response body 작성 흐름으로 보낸다.
@ResponseBody, @RestController, ResponseEntity는 객체를 HTTP body로 변환하는 대표 방식이다.
응답 DTO, status, header, content type을 명시적으로 설계해야 API가 안정적이다.
핵심 정리
문자열 반환은 @Controller에서는 view name, @ResponseBody 맥락에서는 body 문자열일 수 있다.
ResponseEntity는 body뿐 아니라 HTTP status와 header를 함께 표현한다.
객체 응답은 MessageConverter를 통해 JSON 등으로 직렬화된다.
응답 DTO는 Entity 노출, 순환 참조, lazy loading 문제를 줄이는 안전장치다.
에러 응답도 정상 응답처럼 구조와 status를 설계해야 클라이언트가 안정적으로 처리할 수 있다.
헷갈리는 지점
Controller가 객체를 반환하면 항상 JSON이라고 생각하기 쉽다. REST API 예제에 익숙하기 때문이다.
핵심은 @ResponseBody 맥락이 있어야 body 변환 흐름으로 간다는 점이다.
일반 @Controller에서는 반환값 종류와 설정에 따라 view 처리 흐름이 될 수 있다.
ResponseEntity를 항상 써야 한다고 생각하기 쉽다. status를 명시할 수 있어서 안전해 보이기 때문이다.
핵심은 status와 header를 제어해야 할 때 특히 유용하다는 점이다.
단순 200 응답은 DTO 반환과 @ResponseStatus 등으로도 충분할 수 있다.
Entity를 응답으로 바로 내보내도 된다고 느끼기 쉽다. 필드가 이미 있으므로 편하기 때문이다.
핵심은 응답은 외부 계약이라는 점이다.
Entity 직접 노출은 내부 구조 노출, lazy loading, 순환 참조, 보안 필드 노출로 이어질 수 있다.
확인 질문
Controller 반환값은 누가 해석하는가?
HandlerAdapter 내부의 ReturnValueHandler들이 해석한다.
ResponseEntity는 언제 유용한가?
HTTP status, header, body를 함께 명시적으로 제어해야 할 때 유용하다.
Entity를 응답 DTO와 분리하는 이유는 무엇인가?
외부 API 계약을 안정화하고 내부 모델, lazy loading, 보안 필드 노출 문제를 줄이기 위해서다.