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

[Spring MVC] Model + BindingResult: 뷰 모델과 바인딩 오류 처리

· 수정 · 📖 약 1분 · 498자/단어 #spring #mvc #model #bindingresult #form #validation
Spring Model, BindingResult, ModelAndView, RedirectAttributes, @ModelAttribute, Errors, FieldError, ObjectError

정의

Model = 컨트롤러 → 뷰 (Thymeleaf, JSP) 로 데이터 전달. BindingResult = 요청 파라미터 → 객체 바인딩 결과 + 오류. 둘 다 Spring MVC 핸들러 메서드의 특별 인자.

IMPORTANT

REST API (@RestController) 는 JSON 응답 이라 Model 거의 사용 안 함. 전통 MVC (@Controller + 뷰 렌더링) 의 핵심.

Model 의 본질

flowchart LR
    Req[HTTP Request] --> Controller["@Controller 메서드"]
    Controller -->|model.addAttribute| Model
    Controller -->|return view_name| Resolver[ViewResolver]
    Model --> View[Thymeleaf / JSP]
    Resolver --> View
    View --> Response[HTML Response]

사용

@Controller
@RequestMapping("/users")
public class UserViewController {

    @GetMapping
    public String list(Model model) {
        model.addAttribute("users", userService.findAll());
        model.addAttribute("total", userService.count());
        return "users/list";   // → resolved → templates/users/list.html
    }

    @GetMapping("/{id}")
    public String detail(@PathVariable Long id, Model model) {
        User user = userService.findById(id);
        model.addAttribute("user", user);
        return "users/detail";
    }
}
<!-- templates/users/list.html -->
<table>
  <tr th:each="u : ${users}">
    <td th:text="${u.name}"></td>
  </tr>
</table>
<p th:text="${'Total: ' + total}"></p>

Model 의 4가지 사용 방법

방식코드
Model 파라미터model.addAttribute("user", user)
ModelMap동일
Map<String, Object>동일 (자동 변환)
@ModelAttribute 메서드모든 핸들러 공통 attribute
@ModelAttribute("categories")
public List<Category> categories() {
    return categoryService.findAll();   // 모든 핸들러에서 자동 추가
}

ModelAndView

@GetMapping("/{id}")
public ModelAndView detail(@PathVariable Long id) {
    ModelAndView mv = new ModelAndView("users/detail");
    mv.addObject("user", userService.findById(id));
    return mv;
}

Model 분리 방식이 깔끔하지만 옛 스타일. 2026 시점 Model + return view_name 이 표준.

BindingResult: 바인딩 오류 처리

폼 전송 → 객체 바인딩 → 타입 변환 오류 + Validation 오류BindingResult 에 모임.

@PostMapping("/signup")
public String signup(
    @ModelAttribute @Valid SignupForm form,
    BindingResult result,
    Model model
) {
    if (result.hasErrors()) {
        // 에러 정보 + 폼 데이터 유지하며 다시 폼 페이지로
        return "users/signup-form";
    }
    userService.signup(form);
    return "redirect:/users";
}

CAUTION

BindingResult반드시 @Valid 또는 @ModelAttribute 인자 바로 다음 에 위치. 위치 잘못이면 MethodArgumentNotValidException 발생 (400).

// ✗ 잘못된 위치
public String signup(@Valid Form form, Model model, BindingResult result) { ... }

// ✓ 올바른 위치
public String signup(@Valid Form form, BindingResult result, Model model) { ... }

BindingResult 의 핵심 API

result.hasErrors();                      // 전체 에러
result.hasFieldErrors();                 // 필드 단위
result.hasGlobalErrors();                // 객체 전체 (cross-field)
result.hasFieldErrors("email");          // 특정 필드

result.getFieldErrors();                 // List<FieldError>
result.getFieldError("email");           // FieldError 또는 null
result.getAllErrors();                   // List<ObjectError>

result.rejectValue("email", "duplicate", "이미 사용 중인 이메일");
result.reject("invalid", "전체 객체 오류");

Error 객체

flowchart TB
    Errors[Errors interface]
    Errors --> Bind[BindingResult]
    Bind --> Field["FieldError<br/>(필드 단위)"]
    Bind --> Object["ObjectError<br/>(객체 전체)"]
    Field --> Fields["objectName, field, rejectedValue, codes, defaultMessage"]
    Object --> Objects["objectName, codes, defaultMessage"]
속성의미
objectName폼 객체 이름 (보통 signupForm)
field필드 이름
rejectedValue입력한 원본 값
codes메시지 코드 우선순위 (i18n)
defaultMessage기본 메시지

뷰에서 에러 표시 (Thymeleaf)

<form th:action="@{/signup}" th:object="${signupForm}" method="post">

  <input type="text" th:field="*{email}"
         th:classappend="${#fields.hasErrors('email')} ? 'is-invalid'" />
  <div class="error" th:if="${#fields.hasErrors('email')}"
       th:errors="*{email}"></div>

  <input type="password" th:field="*{password}" />
  <div class="error" th:errors="*{password}"></div>

  <!-- 전체 (global) 에러 -->
  <ul th:if="${#fields.hasGlobalErrors()}">
    <li th:each="err : ${#fields.globalErrors()}" th:text="${err}"></li>
  </ul>

  <button>가입</button>
</form>

자세한 Thymeleaf 는 spring-thymeleaf.

Bind 메시지 (i18n)

# messages.properties
NotBlank.signupForm.email=이메일을 입력해주세요
Size.signupForm.password=비밀번호는 {2}자 이상 {1}자 이하
Email.signupForm.email=올바른 이메일 형식이 아닙니다

# typeMismatch 는 special
typeMismatch.signupForm.age=숫자만 입력 가능합니다
typeMismatch.java.lang.Integer=숫자 형식이 잘못되었습니다

Spring 이 자동으로 코드 우선순위 로 메시지 lookup.

RedirectAttributes (PRG 패턴)

@PostMapping
public String signup(
    @Valid SignupForm form,
    BindingResult result,
    RedirectAttributes redirectAttrs
) {
    if (result.hasErrors()) return "users/signup-form";

    User u = userService.signup(form);
    redirectAttrs.addAttribute("id", u.getId());          // URL 쿼리스트링
    redirectAttrs.addFlashAttribute("message", "가입 완료!"); // session 1회 유지

    return "redirect:/users/{id}";   // /users/42?id=42 (PathVariable + Query)
}

@GetMapping("/{id}")
public String detail(@PathVariable Long id, Model model) {
    // Flash attribute 가 자동으로 Model 에 들어감
    // 뷰에서 ${message} 로 사용 가능
    return "users/detail";
}

IMPORTANT

POST-Redirect-GET (PRG) 패턴. 폼 전송 후 redirect새로고침 시 중복 제출 방지. Flash 로 일회성 메시지 전달.

동작 흐름

sequenceDiagram
    autonumber
    participant Browser
    participant DS as DispatcherServlet
    participant Handler as Controller method
    participant Binder as DataBinder
    participant Valid as Validator
    participant View as ViewResolver

    Browser->>DS: POST /signup (form data)
    DS->>Handler: invoke
    Handler->>Binder: bind request params → SignupForm
    Note over Binder: 타입 변환 오류 → BindingResult
    Binder->>Valid: @Valid 트리거
    Valid->>Valid: Hibernate Validator 실행
    Valid->>Binder: ConstraintViolations → BindingResult
    Binder-->>Handler: SignupForm + BindingResult
    alt hasErrors
        Handler->>Model: form + errors
        Handler-->>View: "users/signup-form"
        View-->>Browser: HTML (오류 메시지)
    else 성공
        Handler->>Model: Flash attributes
        Handler-->>View: "redirect:/users/{id}"
        View-->>Browser: 302 → /users/42
    end

REST 에서의 BindingResult

@RestController + @RequestBody @Valid 면 BindingResult 대신 MethodArgumentNotValidException 자동 발생. @ControllerAdvice 에서 처리.

@PostMapping
public User create(@RequestBody @Valid CreateUserDto dto) {
    // 위 메서드는 *BindingResult 없으면* 검증 실패 시 자동 예외
    return userService.create(dto);
}

// → @ExceptionHandler 에서 처리

자세한 건 spring-mvc-exception-handler.

흔한 함정

WARNING

  1. BindingResult 위치 = @Valid 인자 바로 다음 이 아니면 작동 안 함.
  2. Model 에서 민감 정보 노출 = model.addAttribute("user", user) → user 의 비밀번호가 HTML 에 노출.
  3. @ModelAttribute 의 이름 충돌 = @ModelAttribute("signupForm") 와 뷰 th:object="${signupForm}" 이름 일치.
  4. Flash attribute 가 사라짐 = redirect 직후 1회만. 뒤로가기 시 재호출하면 없음.

관련 위키

이 글의 용어 (6개)
[Spring MVC] Exception Handling: @ExceptionHandler, ControllerAdvice, ProblemDetailspring
정의 Spring MVC 의 예외 처리 3 계층: 1. - 컨트롤러 내부 2. / - 전역 3. 상속 - Spring 의 표준 예외 자동 처리 처리 우선순위 컨트롤러 내부 컨트롤…
[Spring MVC] Form Handling: @ModelAttribute, DataBinder, conversionspring
정의 Form Handling = HTML form 의 요청 파라미터 → Java 객체 변환 + 검증 + 에러 처리. 핵심: + + . 기본 흐름 자세한 BindingResult…
[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] DispatcherServlet: Front Controller 내부spring
정의 가 / 사용법 을 다룬다면, 이 페이지는 그 모든 요청을 받아 처리하는 의 내부 메커니즘 을 다룬다. 은 본질적으로 하나의 Servlet (). Front Controlle…
[Spring] Spring MVC: @RestController, @RequestMappingspring
정의 Spring MVC는 Servlet 기반 동기 웹 프레임워크. 이 요청을 받아 핸들러(컨트롤러 메서드)에 라우팅. REST API와 전통 MVC(HTML) 모두 지원. We…

💬 댓글

사이트 검색 / 명령어

검색

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