[Spring MVC] Form Handling: @ModelAttribute, DataBinder, conversion
@ModelAttribute, Form binding, WebDataBinder, ConversionService, PropertyEditor, InitBinder, Formatter, BindingErrorProcessor
정의
Form Handling = HTML form 의 요청 파라미터 → Java 객체 변환 + 검증 + 에러 처리. 핵심: @ModelAttribute + WebDataBinder + ConversionService.
기본 흐름
flowchart LR
Form[HTML Form] -->|POST x=1&y=hello| Req[HttpServletRequest]
Req --> Binder[WebDataBinder]
Binder -->|타입 변환| Conv[ConversionService]
Conv --> Obj[Form Object]
Obj --> Validator[Bean Validator]
Validator --> BR[BindingResult]
Obj --> Handler[Controller method]
BR --> Handler
자세한 BindingResult 는 spring-mvc-model-bindingresult.
@ModelAttribute 의 3가지 역할
flowchart TB
A["@ModelAttribute"]
A --> R1["1. 핸들러 인자: form 객체 바인딩"]
A --> R2["2. 메서드 어노테이션: 모든 핸들러에 attribute 추가"]
A --> R3["3. 인자 이름 변경 (Model 안에 다른 이름)"]
1. 인자에서 (form 바인딩)
@PostMapping("/signup")
public String signup(@ModelAttribute SignupForm form,
BindingResult result) {
if (result.hasErrors()) return "users/signup-form";
...
}
// @ModelAttribute 생략 가능 (default)
public String signup(SignupForm form, BindingResult result) { ... }
Spring 이 기본 생성자 + setter 또는 all-args constructor 로 객체 생성 + 요청 파라미터 매핑.
2. 메서드 어노테이션 (공통 attribute)
@Controller
public class UserController {
@ModelAttribute("countries")
public List<Country> countries() {
return countryService.findAll();
}
@ModelAttribute
public Locale locale(HttpServletRequest req) {
return req.getLocale();
}
@GetMapping("/signup")
public String signupForm(Model model) {
// countries, locale 자동 추가됨
model.addAttribute("signupForm", new SignupForm());
return "users/signup-form";
}
}
3. 이름 변경
public String signup(@ModelAttribute("signupForm") SignupForm form) {
// Model 에 "signupForm" 이름으로 저장
// 뷰의 th:object="${signupForm}" 와 매칭
}
default 이름 = 클래스 이름 소문자 첫글자 (
signupForm).
WebDataBinder 의 동작
sequenceDiagram
autonumber
Form->>Servlet: POST email=koa@x.com&age=29
Servlet->>DispatcherServlet: forward
DispatcherServlet->>Resolver[ModelAttributeMethodProcessor]: arg resolve
Resolver->>Binder[WebDataBinder]: bind(request, target)
Binder->>Conv[ConversionService]: String "29" → Integer 29
Binder->>Conv: String "koa@x.com" → String
Conv-->>Binder: 변환 결과
Binder->>Validator: @Valid 트리거
Validator-->>Binder: ConstraintViolations
Binder-->>Handler: Form + BindingResult
ConversionService 와 Formatter
기본 변환
Spring 이 자동 처리:
String "29" → Integer 29
String "true" → boolean true
String "2026-06-25" → LocalDate (LocaleContextHolder 의 locale)
String "12345" → enum Status (name 기반)
String "1,2,3" → List<Integer> (separator: comma)
커스텀 Formatter
@Component
public class MoneyFormatter implements Formatter<Money> {
@Override
public Money parse(String text, Locale locale) throws ParseException {
// "1,234,567원" → Money
long amount = Long.parseLong(text.replaceAll("[,원]", ""));
return new Money(amount, Currency.KRW);
}
@Override
public String print(Money money, Locale locale) {
return String.format("%,d원", money.amount());
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new MoneyFormatter());
}
}
이제
@RequestParam Money price자동 변환.
@DateTimeFormat / @NumberFormat
public record OrderForm(
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
LocalDateTime scheduledAt,
@NumberFormat(pattern = "#,###")
Long amount,
@NumberFormat(style = NumberFormat.Style.CURRENCY)
BigDecimal price
) { }
@InitBinder (커스텀 바인딩 설정)
@Controller
public class OrderController {
@InitBinder
public void initBinder(WebDataBinder binder) {
// 1. 특정 필드 *바인딩 차단* (mass assignment 방어!)
binder.setDisallowedFields("id", "createdAt", "user");
// 2. *허용 필드만* 명시 (더 안전)
binder.setAllowedFields("title", "amount", "deliveryAddress");
// 3. 커스텀 editor 등록
binder.registerCustomEditor(Date.class,
new CustomDateEditor(new SimpleDateFormat("yyyy/MM/dd"), false));
}
}
CAUTION
Mass Assignment 취약점: User.role 같이 민감 필드 가 form 으로 update 가능 하면 공격자가 admin 으로 승격. setDisallowedFields 또는 DTO 분리.
DTO 패턴 (권장)
// ✓ 안전: DTO 만 form 으로 받음
public record SignupForm(
@NotBlank @Email String email,
@NotBlank @Size(min = 8) String password,
@NotBlank String name
) {
public User toUser() {
return new User(null, email, hashPassword(password), name, Role.USER);
}
}
// ✗ 위험: Entity 를 form 으로 받지 말 것
@PostMapping("/signup")
public String signup(@ModelAttribute User user) { ... } // role 까지 받음
Content Type 별 바인딩
| Content-Type | 바인딩 |
|---|---|
application/x-www-form-urlencoded | @ModelAttribute (또는 생략) |
multipart/form-data | @ModelAttribute + MultipartFile |
application/json | @RequestBody (Jackson) |
application/xml | @RequestBody (Jackson XML) |
// 파일 업로드
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file,
@ModelAttribute UploadForm form) {
file.transferTo(Path.of("/var/uploads/" + file.getOriginalFilename()));
return "redirect:/files";
}
Spring MVC vs WebFlux 차이
| Spring MVC (Servlet) | WebFlux | |
|---|---|---|
| 바인딩 | WebDataBinder | WebExchangeDataBinder |
| API | sync | reactive |
| 본문 | @RequestBody DTO | Mono<DTO> |
| Form | @ModelAttribute | 동일 (reactive) |
흔한 함정
WARNING
- DTO 없이 Entity 직접 바인딩 = mass assignment. 항상 DTO 분리.
@InitBinder의 setAllowedFields/setDisallowedFields 둘 다 명시 = allowed 만 적용. 하나로.- type 변환 실패 시 메시지 =
typeMismatch.fieldNamei18n 메시지 미정의 → 기본 영문. MultipartFile의 기본 size limit = 1MB (기본).spring.servlet.multipart.max-file-size.
관련 위키
- spring-mvc-model-bindingresult
- spring-validation
- spring-mvc
- spring-thymeleaf (th:field)
- spring-mvc-argument-resolver (커스텀 resolver)
이 글의 용어 (5개)
- [Spring MVC] Argument Resolver: Custom HandlerMethodArgumentResolverspring
- 정의 HandlerMethodArgumentResolver = 컨트롤러 메서드 인자를 어떻게 채울지 결정 하는 확장점. Spring 의 수십 개 표준 resolver + 커스텀 …
- [Spring MVC] Model + BindingResult: 뷰 모델과 바인딩 오류 처리spring
- 정의 Model = 컨트롤러 → 뷰 (Thymeleaf, JSP) 로 데이터 전달. BindingResult = 요청 파라미터 → 객체 바인딩 결과 + 오류. 둘 다 Spring…
- [Spring MVC] Thymeleaf: 서버 사이드 템플릿 엔진spring
- 정의 Thymeleaf = Java 서버 사이드 HTML 템플릿 엔진. natural template 철학 (브라우저에서 그대로 열어도 동작). Spring Boot 가 기본 권…
- [Spring] Bean Validation: @Valid, @NotBlank, customspring
- 정의 Spring은 Jakarta Bean Validation (JSR-380)을 표준으로 사용. , , 등 어노테이션으로 선언적 검증. Spring MVC가 로 자동 통합. 설…
- [Spring] Spring MVC: @RestController, @RequestMappingspring
- 정의 Spring MVC는 Servlet 기반 동기 웹 프레임워크. 이 요청을 받아 핸들러(컨트롤러 메서드)에 라우팅. REST API와 전통 MVC(HTML) 모두 지원. We…
💬 댓글