[Spring Boot] 유효성 검사를 위한 Validation 활용

뽀삐용·2023년 9월 28일

Spring Boot

목록 보기
1/4

validation 유효성 검사

회원가입 시 사용자가 입력한 정보가 서버로 전송되기 전에 특정 규칙에 맞게 입력됐는지, 아이디와 닉네임이 중복됐는지 등 확인하는 검증 단계가 필요하다.


1. build.gradle 설정

spring boot 2.3 이상부터는 모듈로 빠져 validation 의존성을 따로 추가해야 한다.

implementation 'org.springframework.boot:spring-boot-starter-validation'

2. RegisterDTO에 유효성 검사 내용 작성

유효성 검사에 필요한 RequestDTO 객체에 Validation 어노테이션을 사용한다.

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

import lombok.Getter;
import lombok.Setter;

import java.sql.Timestamp;

@Getter
@Setter
public class RegisterRequest {
    @NotBlank(message = "아이디는 필수 입력값입니다.")
    @Pattern(regexp = "^[a-z0-9]{4,20}$", message = "아이디는 영문 소문자와 숫자 4~12자리여야 합니다.")
    private String id;

    @NotBlank(message = "비밀번호는 필수 입력값입니다.")
    @Pattern(regexp="(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}",
            message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8자 ~ 20자의 비밀번호여야 합니다.")
    private String password;
    private String confirm_password;

    @NotBlank(message = "이메일은 필수 입력값입니다.")
    @Email
    private String email;

    @NotBlank(message = "닉네임은 필수 입력값입니다.")
    @Pattern(regexp = "^[가-힣a-zA-Z0-9]{2,10}$" , message = "닉네임은 특수문자를 포함하지 않은 2~10자리여야 합니다.")
    private String nickname;            // 닉네임

    private String name;                // 이름
    private String phone;               // 핸드폰
    private String post_no;             // 우편번호
    private String post_addr1;          // 주소
    private String post_addr2;          // 상세주소
    private Timestamp reg_date;         // 가입일
    private Timestamp cancel_date;      // 탈퇴일
    private Timestamp save_date;        // 저장일
}

💡유효성 검사를 해주는 다양한 어노테이션의 종류는 아래 공식 문서에서 확인할 수 있다.
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-builtin-constraints

많이 사용하는 어노테이션

  • @Email
    - 주석이 달린 문자열은 이메일 형식이어야한다.
  • @NotBlank
    - 주석이 달린 요소는 Null이 아니어야하며 하나 이상의 공백이 아닌 문자를 포함해야한다.
  • @NotEmpty
    - 주석이 달린 요서는 Null이거나 비어있으면 안된다.
  • @Pattern(regexp=, message=)
    - 주석이 달린 문자열은 regexp에 지정된 정규식과 일치하여야한다.
  • @Size(min=, max=)
    - 주석이 달린 요소의 크기는 해당 범위 내에 있어야한다.

3. Controller에서 유효성 검증

    /**
     * 회원가입 (Form)
     * @return
     */
    @GetMapping("/member/register.do")
    public String registerForm(Model model){
        // th:object는 form 태그에 작성하여 해당 태그가 Model 객체에서 넘어온 객체의 필드값들을 참조하도록 만든다.
        model.addAttribute("registerRequest", new RegisterRequest());
        return "member/register";
    }
    
    /**
     * 회원가입
     * @return
     */
    @PostMapping("/member/save.do")
    public String saveMember(@Valid RegisterRequest registerRequest, BindingResult bindingResult, Model model) {

        if(bindingResult.hasErrors()) {
            return "member/register";
        }

        if (!registerRequest.getPassword().equals(registerRequest.getConfirm_password())) {
            bindingResult.rejectValue("password", "passwordInCorrect",
                    "2개의 패스워드가 일치하지 않습니다.");
            return "member/register";
        }

        memberService.saveMember(registerRequest);
        MessageDto message = new MessageDto("회원가입이 완료되었습니다.", "/member/login.do", RequestMethod.GET, null);
        return showMessageAndRedirect(message, model);
    }

Controller에서 @Valid어노테이션을 붙여주면 작성해놓은 유효성 검증을 진행하게 됩니다.

이렇게 해서 발생한 에러를 받기 위해서 BindingResult를 사용하게 되는데 검색을 하다보니 Errors로 작성하신 분들도 많더라고요..?


BindingResult와 Errors 차이점

BindingResult는 인터페이스고 Errors 인터페이스를 상속받고 있다. 실제 넘어오는 구현체는 BeanPropertyBindingResult 인데 BindingResult대신 Errors를 사용해도 된다.
Errors 인터페이스는 단순한 오류 저장과 조회 기능을 제공한다. BindingResult는 여기에 더해서 추가적인 기능들을 제공한다. (ex. addError())


4. html 에러 메시지 전달

<div class="login_form_inner register_form_inner">
    <h3>Create an account</h3>
    <form action="/member/save.do" th:object="${registerRequest}" id="registerForm" method="post" autocomplete="off" class="row login_form">
        <div class="col-md-12 form-group">
            <div style="display:inline-block; width: 65%;">
                <input type="text" th:field="*{id}" class="form-control" id="id" name="id" placeholder="아이디(영문소문자/숫자 사용, 4~12자리 이내)" onfocus="this.placeholder = ''" onblur="this.placeholder = '아이디'" >
            </div>
            <div style="display:inline-block; width: 35%; float: right;">
                <button type="button" class="button button-hero" id="idCheckBtn" onclick="checkId();">중복 확인</button>
            </div>
            <p th:if="${#fields.hasErrors('id')}" th:errors="*{id}" class="fieldError">Incorrect Data</p>
        </div>
        <div class="col-md-12 form-group">
            <input type="text" th:field="*{email}" class="form-control" id="email" name="email" placeholder="이메일" onfocus="this.placeholder = ''" onblur="this.placeholder = '이메일'">
            <p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="fieldError">Incorrect Data</p>
        </div>
        <div class="col-md-12 form-group">
            <input type="password" th:field="*{password}" class="form-control" id="password" name="password" placeholder="비밀번호(영문대소문자/숫자/특수기호 1개 이상 필수, 8~20자 이내)" onfocus="this.placeholder = ''" onblur="this.placeholder = 'Password'">
            <p th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="fieldError">Incorrect Data</p>
        </div>
        <div class="col-md-12 form-group">
            <input type="password" th:field="*{confirm_password}" class="form-control" id="confirm_password" name="confirm_password" placeholder="비밀번호 확인" onfocus="this.placeholder = ''" onblur="this.placeholder = '비밀번호 확인'">
            <p th:if="${#fields.hasErrors('confirm_password')}" th:errors="*{confirm_password}" class="fieldError">Incorrect Data</p>
        </div>
        <div class="col-md-12 form-group">
            <input type="text" th:field="*{nickname}" class="form-control" id="nickname" name="nickname" placeholder="닉네임" onfocus="this.placeholder = ''" onblur="this.placeholder = '닉네임'">
            <p th:if="${#fields.hasErrors('nickname')}" th:errors="*{nickname}" class="fieldError">Incorrect Data</p>
        </div>
        <div class="col-md-12 form-group">
            <input type="text" th:field="*{name}" class="form-control" id="name" name="name" placeholder="이름" onfocus="this.placeholder = ''" onblur="this.placeholder = '이름'">
            <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="fieldError">Incorrect Data</p>
        </div>
        <div class="col-md-12 form-group">
            <input type="text" th:field="*{phone}" class="form-control" id="phone" name="phone" placeholder="핸드폰" onfocus="this.placeholder = ''" onblur="this.placeholder = '핸드폰'">
            <p th:if="${#fields.hasErrors('phone')}" th:errors="*{phone}" class="fieldError">Incorrect Data</p>
        </div>
        <div class="col-md-12 form-group">
            <div style="display:inline-block; width: 55%;">
                <input type="text" class="form-control" id="post_no" name="post_no" placeholder="우편번호" onfocus="this.placeholder = ''" onblur="this.placeholder = '우편번호'" disabled="disabled">
            </div>
            <div style="display:inline-block; width: 45%; float: right;">
                <button type="button" class="button button-hero" onclick="execPostCode();">우편번호 찾기</button>
            </div>
        </div>
        <div class="col-md-12 form-group">
            <input type="text" class="form-control" id="post_addr1" name="post_addr1" placeholder="주소" onfocus="this.placeholder = ''" onblur="this.placeholder = '주소'" disabled="disabled">
        </div>
        <div class="col-md-12 form-group">
            <input type="text" class="form-control" id="post_addr2" name="post_addr2" placeholder="상세주소" onfocus="this.placeholder = ''" onblur="this.placeholder = '상세주소'">
        </div>
        <div class="col-md-12 form-group">
            <button type="submit" class="button button-register w-100">가입하기</button>
            <!--<button type="button" id="saveBtn" onclick="saveMember();" class="button button-register w-100">가입하기</button>-->
        </div>
    </form>
</div>
profile
하고 싶은 일 한 가지를 하려면

0개의 댓글