본문 바로가기
  • Let's study
BackEnd/Spring

[Spring] Validation 오류 처리하기 - BindingResult 활용

by 코딩고수이고파 2026. 8. 11.

앞서 @Valid를 사용하여 게시글 작성 Form의 입력값을 검증하였다.

하지만 Validation에 실패했을 때 단순히 예외가 발생하도록 두는 것보다는, 어떤 입력값이 잘못되었는지 사용자에게 알려주고 다시 Form을 보여주는 것이 더 좋은 사용자 경험을 제공할 수 있다.

이번에는 BindingResult를 사용하여 Validation 오류를 직접 처리해 보았다.

1. BindingResult란?

@Valid를 사용하면 Validation 조건에 맞지 않는 입력값이 들어왔을 때 오류가 발생한다.

이때 BindingResult를 Controller 메서드의 매개변수로 함께 사용하면 Validation 결과를 직접 확인하고 처리할 수 있다.

@PostMapping("/posts/doWrite")
@ResponseBody
public String doWrite(
        @Valid PostWriteForm postWriteForm,
        BindingResult bindingResult
) {
    ...
}

BindingResult에는 Spring이 Form 데이터를 객체에 바인딩하는 과정에서 발생한 오류와 Validation 결과가 저장된다.

따라서 다음과 같이 오류가 있는지 확인할 수 있다.

if (bindingResult.hasErrors()) {
    // Validation 실패 처리
}

Validation에 실패했을 때 직접 처리할 수 있기 때문에 사용자가 다시 입력할 수 있도록 Form을 보여주는 등의 작업을 할 수 있다.

2. Validation 오류 가져오기

먼저 발생한 오류 중 하나를 가져와 오류 메시지를 출력해 보았다.

if (bindingResult.hasErrors()) {

    FieldError fieldError = bindingResult.getFieldError();

    String fieldName = fieldError.getField();
    String errorMessage = fieldError.getDefaultMessage();

    System.out.println("fieldName : " + fieldName);
    System.out.println("errorMessage : " + errorMessage);
}

getFieldError()를 사용하면 발생한 FieldError 중 하나를 가져올 수 있다.

FieldError에서는 다음과 같은 정보를 확인할 수 있다.

fieldError.getField();

어떤 필드에서 오류가 발생했는지 확인할 수 있다.

fieldError.getDefaultMessage();

Validation에 설정한 오류 메시지를 가져올 수 있다.

예를 들어 다음과 같이 Validation을 설정했다면,

@NotBlank(message = "제목을 입력해주세요")
private String title;

오류가 발생했을 때 다음과 같은 값을 얻을 수 있다.

fieldName : title
errorMessage : 제목을 입력해주세요

이렇게 오류가 발생한 필드와 메시지를 알 수 있기 때문에 해당 정보를 Form에 표시할 수 있다.

3. Validation 오류가 발생하면 Form 다시 보여주기

Validation에 실패했을 때 오류 메시지만 반환하는 것이 아니라 사용자가 다시 입력할 수 있도록 작성 Form을 다시 보여주도록 만들었다.

if (bindingResult.hasErrors()) {

    FieldError fieldError = bindingResult.getFieldError();

    String fieldName = fieldError.getField();
    String errorMessage = fieldError.getDefaultMessage();

    return getWriteFormHtml(
            errorMessage,
            postWriteForm.getTitle(),
            postWriteForm.getContent(),
            fieldName
    );
}

여기서 postWriteForm에는 사용자가 입력했던 값이 들어 있기 때문에 Form을 다시 생성할 때 기존 입력값을 전달할 수 있다.

private String getWriteFormHtml(
        String errorMessage,
        String title,
        String content,
        String errorField
) {
    ...
}

그리고 Form의 valuetextarea에 기존 값을 넣어준다.

<input
    type="text"
    name="title"
    value="%s"
>

<textarea name="content">%s</textarea>

이렇게 하면 Validation 실패 후 Form을 다시 보여주더라도 사용자가 입력했던 값이 사라지지 않는다.

4. 모든 Validation 오류 보여주기

처음에는 getFieldError()를 사용하여 하나의 오류만 가져왔다.

하지만 제목과 내용이 모두 잘못된 경우 하나의 오류만 보여주는 것은 불편하다.

모든 FieldError를 가져오기 위해 getFieldErrors()를 사용할 수 있다.

String errorMessages = bindingResult.getFieldErrors()
        .stream()
        .map(FieldError::getDefaultMessage)
        .collect(Collectors.joining("<br>"));

각 오류 메시지를 Stream으로 처리한 후 <br>로 연결하면 모든 오류 메시지를 한 번에 보여줄 수 있다.

예를 들어 결과는 다음과 같은 HTML이 된다.

제목을 입력해주세요.<br>
내용을 입력해주세요.

5. 오류 메시지를 일정한 순서로 보여주기

여러 개의 Validation 오류가 발생했을 때 오류 메시지가 항상 원하는 순서로 표시된다는 보장이 필요할 수 있다.

그래서 Validation 메시지 앞에 번호를 붙여 순서를 지정하였다.

@NotBlank(message = "1-제목을 입력해주세요")
@Size(
        min = 2,
        max = 10,
        message = "2-제목은 2자 이상 10자 이하로 입력해주세요."
)
private String title;

@NotBlank(message = "3-내용을 입력해주세요")
@Size(
        min = 2,
        max = 100,
        message = "4-내용은 2자 이상 100자 이하로 입력해주세요."
)
private String content;

이제 오류 메시지는 다음과 같은 번호를 가지게 된다.

1-제목을 입력해주세요
2-제목은 2자 이상 10자 이하로 입력해주세요.
3-내용을 입력해주세요
4-내용은 2자 이상 100자 이하로 입력해주세요.

그리고 Stream의 sorted()를 사용하여 번호 순서대로 정렬하였다.

String errorMessages = bindingResult.getFieldErrors()
        .stream()
        .map(f ->
                f.getDefaultMessage() + "-" + f.getField()
        )
        .map(message -> message.split("-"))
        .map(bits -> """
                <!-- %s -->
                <li data-error-field-name="%s">%s</li>
                """.formatted(
                        bits[0],
                        bits[2],
                        bits[1]
                ))
        .sorted()
        .collect(Collectors.joining("\n"));

이 과정에서 오류 메시지를 단순한 문자열이 아니라 <li> 요소로 만들어 사용하였다.

<!-- 1 -->
<li data-error-field-name="title">
    제목을 입력해주세요
</li>

각 오류에 번호를 붙인 주석도 함께 추가하였다.

이렇게 하면 sorted()를 사용하여 오류 메시지를 번호 순서대로 정렬할 수 있다.

6. 오류가 발생한 필드에 자동으로 포커스하기

오류 메시지를 보여주는 것에서 한 단계 더 나아가, 가장 먼저 발생한 오류의 입력창에 자동으로 포커스를 이동하도록 구현하였다.

각 오류 메시지에 data-error-field-name 속성을 추가하였다.

<li data-error-field-name="title">
    제목을 입력해주세요
</li>

이 속성에는 오류가 발생한 필드의 이름을 저장한다.

JavaScript에서는 첫 번째 오류를 찾아 data-error-field-name 값을 가져온다.

const li = document.querySelector("ul li");
const errorFieldName = li.dataset.errorFieldName;

예를 들어 첫 번째 오류가 제목이라면 다음과 같은 값이 된다.

errorFieldName = "title"

그리고 Form에서 해당 이름을 가진 입력 요소를 찾아 포커스를 이동한다.

if(errorFieldName.length > 0) {
    const form = document.querySelector("form");
    form[errorFieldName].focus();
}

따라서 사용자가 제목을 잘못 입력했다면 제목 입력창으로 바로 포커스가 이동하고, 내용에 오류가 있다면 내용 입력창으로 이동하게 된다.

7. 최종 코드

Validation 오류 처리까지 적용한 doWrite() 메서드는 다음과 같다.

@PostMapping("/posts/doWrite")
@ResponseBody
public String doWrite(
        @Valid PostWriteForm postWriteForm,
        BindingResult bindingResult
) {

    if (bindingResult.hasErrors()) {

        String errorMessages = bindingResult.getFieldErrors()
                .stream()
                .map(f ->
                        f.getDefaultMessage() + "-" + f.getField()
                )
                .map(message -> message.split("-"))
                .map(bits -> """
                        <!-- %s --><li data-error-field-name="%s">%s</li>
                        """.formatted(
                                bits[0],
                                bits[2],
                                bits[1]
                        ))
                .sorted()
                .collect(Collectors.joining("\n"));

        return getWriteFormHtml(
                errorMessages,
                postWriteForm.getTitle(),
                postWriteForm.getContent()
        );
    }

    Post post = postService.write(
            postWriteForm.getTitle(),
            postWriteForm.getContent()
    );

    return "%d번 글이 작성되었습니다.".formatted(post.getId());
}

Form을 다시 생성하는 메서드는 다음과 같이 작성하였다.

private String getWriteFormHtml(
        String errorMessage,
        String title,
        String content
) {
    return """
            <ul style="color:red">
                %s
            </ul>

            <form method="POST" action="/posts/doWrite">
                <input
                    type="text"
                    name="title"
                    value="%s"
                    autoFocus
                >
                <br>

                <textarea name="content">%s</textarea>
                <br>

                <input type="submit" value="작성">
            </form>

            <script>
                const li = document.querySelector("ul li");
                const errorFieldName = li.dataset.errorFieldName;

                if(errorFieldName.length > 0) {
                    const form = document.querySelector("form");
                    form[errorFieldName].focus();
                }
            </script>
            """.formatted(
                    errorMessage,
                    title,
                    content
            );
}

이제 Validation에 실패하더라도 단순히 오류가 발생하는 것으로 끝나지 않는다.

사용자가 입력했던 값은 유지되고, 발생한 모든 오류 메시지를 확인할 수 있으며, 가장 먼저 수정해야 할 입력창에 자동으로 포커스가 이동한다.

댓글