이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
@RequestParam,@PathVariable,@ModelAttribute는 각각 어떤 위치의 데이터를 읽는가?- Form 요청과 query parameter 기반 검색 DTO는 왜
@ModelAttribute와 잘 맞는가?- 타입 변환 실패와 검증 실패는 어떻게 구분하는가?
개요
Spring MVC의 handler method argument는 다양한 요청 데이터를 받을 수 있다. @RequestParam은 요청 파라미터, @PathVariable은 URI template 변수, @ModelAttribute는 요청 파라미터를 객체에 바인딩하는 방식이다. 단순 타입 인자는 애너테이션이 생략되어도 @RequestParam처럼 해석될 수 있고, 단순 타입이 아닌 객체 인자는 @ModelAttribute처럼 해석될 수 있지만, 학습과 협업에서는 명시적으로 붙이는 편이 오해가 적다.
기존 MVC 문서의 RequestParam, Primitive/Wrapper 타입 자동 변환, Model 객체 활용 내용은 이 바인딩 흐름으로 흡수된다.
@RequestParam
@RequestParam은 query parameter나 form parameter를 읽는다.
@GetMapping("/members")
public List<MemberResponse> members(@RequestParam(defaultValue = "0") int page) {
return memberService.findPage(page);
}요청 /members?page=2에서 page 값이 문자열로 들어오지만 Spring이 int로 변환한다.
@RequestParam의 required 기본값은 true다. primitive 타입은 null을 담을 수 없기 때문에 값이 비어 있거나 빠졌을 때 기본값, wrapper 타입, Optional, required=false 중 무엇이 의도에 맞는지 정해야 한다.
@PathVariable
@PathVariable은 path pattern에 선언한 변수를 읽는다.
@GetMapping("/members/{id}")
public MemberResponse member(@PathVariable Long id) {
return memberService.find(id);
}id는 resource identity를 표현한다. 필터, 정렬, 검색어 같은 선택 조건은 path variable보다 query parameter가 자연스러운 경우가 많다.
@ModelAttribute
@ModelAttribute는 여러 요청 파라미터를 객체에 바인딩한다.
public class MemberSearchCondition {
private String keyword;
private Integer age;
}
@GetMapping("/members")
public String members(@ModelAttribute MemberSearchCondition condition, Model model) {
model.addAttribute("members", memberService.search(condition));
return "member/list";
}form submit이나 검색 조건처럼 여러 파라미터가 객체 하나로 묶일 때 유용하다. JSON body를 읽는 @RequestBody와는 다른 경로다.
최신 Spring은 객체 형태에 따라 생성자 바인딩도 지원할 수 있지만, 팀 코드에서는 DTO가 setter 기반인지 생성자 기반인지 일관성을 정해 두는 편이 좋다. 바인딩 방식이 흐려지면 테스트에서는 통과하지만 운영 요청에서 일부 필드가 비는 문제가 생기기 쉽다.
타입 변환과 BindingResult
HTTP 요청 값은 문자열이다. Spring은 ConversionService, Formatter, DataBinder를 사용해 target type으로 바꾼다. 변환 실패가 발생하면 바인딩 오류가 된다.
화면 MVC에서는 BindingResult로 바인딩/검증 오류를 직접 다룰 수 있다. 이때 BindingResult는 검증 대상 파라미터 바로 뒤에 와야 한다. BindingResult는 @ModelAttribute뿐 아니라 검증된 @RequestBody나 @RequestPart 뒤에도 둘 수 있지만, REST API에서는 공통 예외 처리로 일관화하는 경우가 더 많다.
실전 팁
- resource id는 path variable, 검색/필터/정렬은 query parameter로 두는 것이 일반적으로 읽기 쉽다.
@ModelAttributeDTO는 form과 query parameter에 잘 맞고, JSON body에는@RequestBody가 맞다.- primitive 타입은 값이 없을 때 기본값 문제와 null 표현 불가능성을 조심한다. 선택 입력에는 wrapper 타입이 더 안전할 수 있다.
- 기본값이 의미 있는 API라면
defaultValue를 명시한다. - 애너테이션 생략 규칙에 기대기보다 외부 API Controller에서는
@RequestParam,@ModelAttribute를 명시해 입력 출처를 드러낸다. - 바인딩 오류와 validation 오류를 구분해서 에러 메시지를 설계한다.
위험 신호!
- JSON body를
@ModelAttribute로 받으려고 한다. - query parameter와 path variable의 의미를 섞어 path가 불필요하게 복잡해진다.
- 선택 값인데 primitive 타입으로 받아 null 상황을 표현하지 못한다.
BindingResult위치가 잘못되어 예외가 발생한다.- Controller에서 문자열 값을 직접 파싱하는 코드가 반복된다.
확인 질문
확인 질문
@RequestParam은 어떤 데이터를 읽는가?
- query parameter나 form parameter 같은 요청 파라미터를 읽는다.
@ModelAttribute와@RequestBody의 핵심 차이는 무엇인가?
@ModelAttribute는 요청 파라미터를 객체에 바인딩하고,@RequestBody는 HTTP body를 MessageConverter로 읽는다.- 선택 입력에는 primitive와 wrapper 중 무엇이 더 안전한가?
- null을 표현할 수 있는 wrapper 타입이 더 안전한 경우가 많다.