우와한 개발자

[ Spring ] 폼 입력값 검증 - @Valid와 BindingResult / Validator 인터페이스

by 우와한개발자

[ 폼 입력값 검증 ]

1. 의존성 추가 (pom.xml)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

 

2. Bean Validation 어노테이션 방식 검증 구현

  • DTO 필드에 Bean Vaildation 어노테이션을 붙이면 Spring이 자동으로 검증해주는 방식
  • 형식 검사(필수값, 이메일 형식, 길이 등) 에 적합

 

1) DTO에 검증 어노테이션 적용 : Bean Validation 어노테이션

  • Bean Validation : Java에서 표준으로 정해놓은 검증 어노테이션 모음
어노테이션 의미
@NotNull null 불가
@NotBlank null, 빈 문자열, 공백 불가
@NotEmpty null, 빈 문자열 불가 (공백은 허용)
@Size(min, max) 문자열 길이 또는 컬렉션 크기 범위
@Min(value) 최솟값
@Max(value) 최댓값
@Email 이메일 형식
@Pattern(regexp) 정규식 패턴
@Getter
@Setter
public class UserDto {

    @NotBlank(message = "이름은 필수입니다.")
    @Size(max = 20, message = "이름은 20자 이하로 입력해주세요.")
    private String name;

    @NotBlank(message = "이메일은 필수입니다.")
    @Email(message = "이메일 형식이 올바르지 않습니다.")
    private String email;

    @NotNull(message = "나이는 필수입니다.")
    @Min(value = 1, message = "나이는 1 이상이어야 합니다.")
    @Max(value = 150, message = "나이는 150 이하여야 합니다.")
    private Integer age;

    @Pattern(regexp = "^\\\\d{2,3}-\\\\d{3,4}-\\\\d{4}$",
             message = "전화번호 형식이 올바르지 않습니다. (예: 010-1234-5678)")
    private String phone;
}

 

2) Controller에서 검증 처리

(1) BindingResult 인터페이스

errors (인터페이스)
  └── BindingResult (인터페이스)
          └── AbstractBindingResult (추상 클래스)
                  └── BeanPropertyBindingResult (실제 구현체, Spring이 자동 생성)
  • @Valid로 검증한 결과(에러 정보)를 담아두는 객체
  • 핸들러 메서드에서 @Valid가 붙은 파라미터 바로 뒤에 BindingResult를 선언해야 함
  • Bean Validation 어노테이션(@NotBlank, @Email 등)으로 발생한 에러뿐만 아니라 rejectValue()로 직접 추가한 비즈니스 검증 에러도 함께 관리(아래의 채ㅏㅁ고
  • 즉 BindingResult가 에러를 낚아채서 400 에러가 터지는 걸 막고, 개발자가 직접 에러 상황을 핸들링할 수 있게 해주는 것임.

 

(2) BindingResult 유무에 따른 흐름

  • BindingResult 없을 때 ❌
사용자가 이름 안 쓰고 저장 클릭
        ↓
@Valid가 검증 실패 감지
        ↓
400 에러 페이지 바로 출력 ← 사용자 입장에서 그냥 에러 화면만 보임
  • BindingResult 있을 때 ⭕
사용자가 이름 안 쓰고 저장 클릭
        ↓
@Valid가 검증 실패 감지
        ↓
에러를 BindingResult에 저장 (에러를 잡아둠)
        ↓
400 에러 안 터지고 Controller 로직이 계속 실행됨
        ↓
hasErrors()로 에러 확인 후 폼으로 되돌아감
        ↓
사용자한테 에러 메시지와 함께 폼 다시 보여줌 ← 정상적인 화면

 

 

(3) BindingResult 인터페이스 주요 메서드

메서드 의미
hasErrors() 에러가 하나라도 있으면 true
getErrorCount() 전체 에러 수 반환
rejectValue("필드명", "코드", "메시지") 특정 필드에 에러 직접 추가
reject("코드", "메시지") 폼 전체(글로벌) 에러 추가
getFieldError("필드명") 특정 필드의 에러 객체 반환
@PostMapping("/user/save")
public String save(@Valid UserDto user, BindingResult bindingResult, Model model) {

    // 특정 필드 에러 직접 추가 (DB 중복 확인 등 비즈니스 검증)
    if (userService.isEmailDuplicated(user.getEmail())) {
        bindingResult.rejectValue("email", "duplicate", "이미 사용 중인 이메일입니다.");
    }

    // 에러가 있으면 폼으로 다시 이동
    if (bindingResult.hasErrors()) {
        return "user/form";
    }

    userService.save(user);
    return "redirect:/user/list";
}

 

3. Validator 인터페이스 방식 검증 구현

  • Spring이 제공하는 Validator 인터페이스를 직접 구현하는 방식
  • 어노테이션으로 처리하기 어려운 복잡한 비즈니스 검증 로직을 별도 클래스로 분리할 때 사용
  • DB 중복 확인, 두 필드 비교(비밀번호 확인) 등에 적합

 

1) Validator 인터페이스 구현

주요 메서드 의미
supports(Class<?> clazz) 이 Validator가 검증할 수 있는 클래스인지 확인
validate(Object target, Errors errors) 실제 검증 로직 작성
@Component
public class UserValidator implements Validator {

    private final UserService userService;

    @Override
    public boolean supports(Class<?> clazz) {
        return UserDto.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        UserDto user = (UserDto) target;

        // 이메일 중복 확인 (DB 조회)
        if (userService.isEmailDuplicated(user.getEmail())) {
            errors.rejectValue("email", "duplicate", "이미 사용 중인 이메일입니다.");
        }

        // 비밀번호 확인 일치 여부
        if (!user.getPassword().equals(user.getPasswordConfirm())) {
            errors.rejectValue("passwordConfirm", "mismatch", "비밀번호가 일치하지 않습니다.");
        }
    }
}

 

2) Controller에 적용

@PostMapping("/user/save")
public String save(@Valid UserDto user, BindingResult bindingResult) {

    // Validator 직접 호출해서 추가 검증
    userValidator.validate(user, bindingResult);

    if (bindingResult.hasErrors()) {
        return "user/form";
    }

    userService.save(user);
    return "redirect:/user/list";
}

 

[검증 결과 출력]

1. JSP에서 출력

  • Spring의 form: 태그 라이브러리를 사용
  • 상단에 태그 라이브러리 선언 필요
태그 의미
<form:form modelAttribute="객체명"> 폼에 객체 바인딩
<form:input path="필드명"> input 태그
<form:errors path="필드명"> 해당 필드 에러 메시지 출력
<form:errors path="*"> 전체 에러 메시지 출력
cssErrorClass 에러 있을 때 자동으로 추가할 CSS 클래스
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<form:form action="/user/save" method="post" modelAttribute="user">

    <%-- 전체 에러 출력 --%>
    <form:errors path="*" cssClass="error-msg" />

    <div>
        <label>이름</label>
        <form:input path="name" cssErrorClass="error-input" />
        <form:errors path="name" cssClass="error-msg" />
    </div>

    <div>
        <label>이메일</label>
        <form:input path="email" cssErrorClass="error-input" />
        <form:errors path="email" cssClass="error-msg" />
    </div>

    <input type="submit" value="저장" />

</form:form>
 

2. Thymeleaf에서 출력

속성 의미
th:object 폼에 객체 바인딩
th:field input의 id, name, value 자동 설정
th:errors 해당 필드 에러 메시지 출력
th:errorclass 에러 있을 때 자동으로 추가할 CSS 클래스
#fields.hasErrors('*') 전체 에러 여부 확인
#fields.errors('*') 전체 에러 메시지 목록 반환
<form th:action="@{/user/save}" method="post" th:object="${user}">

    <!-- 전체 에러 목록 출력 -->
    <ul th:if="${#fields.hasErrors('*')}">
        <li th:each="error : ${#fields.errors('*')}" th:text="${error}"></li>
    </ul>

    <div>
        <label>이름</label>
        <input type="text" th:field="*{name}" th:errorclass="error-input" />
        <span th:errors="*{name}" class="error-msg"></span>
    </div>

    <div>
        <label>이메일</label>
        <input type="email" th:field="*{email}" th:errorclass="error-input" />
        <span th:errors="*{email}" class="error-msg"></span>
    </div>

    <button type="submit">저장</button>
</form>

 

3. JSP vs Thymeleaf 비교

구분 JSP Thymeleaf
객체 바인딩 modelAttribute="user" th:object="${user}"
필드 바인딩 <form:input path="필드명"> th:field="*{필드명}"
에러 출력 <form:errors path="필드명"> th:errors="*{필드명}"
에러 CSS 추가 cssErrorClass="클래스명" th:errorclass="클래스명"
전체 에러 출력 <form:errors path="*"> #fields.errors('*')

블로그의 정보

우와한개발자 님의 블로그

우와한개발자

활동하기