본문으로 건너뛰기
김신건의 로그

[Spring MVC] Form Handling: @ModelAttribute, DataBinder, conversion

· 수정 · 📖 약 1분 · 364자/단어 #spring #mvc #form #binding #modelattribute #converter
@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
바인딩WebDataBinderWebExchangeDataBinder
APIsyncreactive
본문@RequestBody DTOMono<DTO>
Form@ModelAttribute동일 (reactive)

흔한 함정

WARNING

  1. DTO 없이 Entity 직접 바인딩 = mass assignment. 항상 DTO 분리.
  2. @InitBinder 의 setAllowedFields/setDisallowedFields 둘 다 명시 = allowed 만 적용. 하나로.
  3. type 변환 실패 시 메시지 = typeMismatch.fieldName i18n 메시지 미정의 → 기본 영문.
  4. MultipartFile기본 size limit = 1MB (기본). spring.servlet.multipart.max-file-size.

관련 위키

이 글의 용어 (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…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기