javascript로도 유효성 검사를 할 수 있지만 서버단 측에서도 유효성 체크를 할 수 있다는 걸 오늘 알았다! 우선 validation 체크를 하기 위해서는 pom.xml에 의존성 추가가 필요하다.

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

주요 어노테이션

유효성 검사 시 사용하는 주요 어노테이션을 살펴보자.

@Email

- email 형식의 값만 들어가야 한다

@NotBlank

- 공백, 빈 문자열, null값을 허용하지 않는다

@NotEmpty

- 빈 문자열, null값은 허용하지 않지만 공백은 허용한다

@Pattern

- 빈 문자열, null값은 허용하지 않지만 공백은 허용한다

- 그 외 @Past, @Future, @Positive, @Min, @Max, @Size 등이 존재한다.

1. 회원가입 폼 생성

회원가입 시의 유효성 검사를 하기 위해 UserRegisterForm을 생성한다.

public class UserRegisterForm {

	@NotBlank(message = "이메일은 필수 입력값입니다.")
	@Email(message = "유효한 이메일 형식이 아닙니다.")
	private String email;
	
	@NotBlank(message = "비밀번호는 필수 입력값입니다.")
	@Pattern(regexp = "[0-9a-zA-Z_!?]{8,20}", message = "유효한 비밀번호 형식이 아닙니다.")
	private String password;
	
	@NotBlank(message = "이름은 필수 입력값입니다.")
	@Pattern(regexp = "[가-힣]{2,}", message = "유효한 이름 형식이 아닙니다.")
	private String name;
	
	@NotBlank(message = "전화번호는 필수 입력값입니다.")
	@Pattern(regexp = "\\d{2,3}-\\d{3,4}-\\d{4}", message = "유효한 전화번호 형식이 아닙니다.")
	private String tel;
	
	public User getEntity() {
		User user = new User();
		user.setEmail(email);
		user.setPassword(password);
		user.setName(name);
		user.setTel(tel);
		
		return user;
	}
	
	/*
	@Past(message = "생일은 현재 시간보다 과거여야 한다.")
	private Date birth;
	
	@Future
	private Date reserveDate;
	
	@Positive
	private int quantity;
	
	@Min(value = 0)
	@Max(value = 100)
	private int score;
	
	@Size(min = 3, max = 10)
	private String nickName;
	*/
}

2. Service 코드

값의 형식에 대해서는 어노테이션으로 충분히 커버가 가능하지만, 이메일이 중복되는지의 확인 등은 DB 접속 후에 알 수 있는 정보들이다. 이 경우는 Service 코드에서 정의해 줄 수 있다. DB 조회 후 중복되는 이메일이 존재하면 RuntimeException을 던진다.

참고: Optional
Optional은 자바8 버전부터 등장한 API
NullPointerExcetion을 피하기 위해서 null을 검사하는 기능이 포함된 Optional가 추가되었다

    @RequiredArgsConstructor
    @Service
    public class UserService {

        private final UserRepository userRepository;
        
        public void saveUser(User user) {
            Optional<User> optional = userRepository.findByEmail(user.getEmail());
            if (optional.isPresent()) {
                throw new RuntimeException("이미 가입한 이메일 입니다.");
            }
            userRepository.save(user);
        }
    }

3. Controller 코드

controller에서 주요 코드는 두 가지가 있는데, 하나씩 살펴보도록 하자.
사용자가 회원가입 버튼을 클릭할 때 registerForm jsp로 이동한다.
아래 코드에서 중요한 부분은 model에 위에서 생성한 UserRegisterForm을 담아주는 것이다.

    @RequiredArgsConstructor
    @Controller
    public class HomeController {

        private final UserService userService;
        @GetMapping("/register")
        public String registerForm(Model model) {
            // 회원가입폼에 회원가입정보를 저장할 UserRegisterForm객체를 생성해서 Model에 담아서 전달한다.
            model.addAttribute("userForm", new UserRegisterForm());
            return "registerForm";
        }
    }

@ModelAttribute("userForm") : 모델에서 userForm이라는 이름으로 저장된 객체를 찾아온다.

@Valid : 폼입력값에 대한 유효성 체크를 수행시킨다. 유효성 체크를 수행할 값을 포함하고 있는 객체 앞에 설정한다.

@BindingResult : 유효성 체크 검사결과를 저장하는 객체다. 유효성케르를 수행할 값을 포함하고 있는 객체 바로 다음에 위치시킨다.

    @RequiredArgsConstructor
    @Controller
    public class HomeController {
        public String register(@ModelAttribute("userForm") @Valid UserRegisterForm form, BindingResult errors) {

            // BindingResult객체에 에러가 포함되어 있는 경우 다시 회원가입페이지로 이동한다.
            if (errors.hasErrors()) {
                return "registerForm";
            }
            
            try {
                userService.saveUser(form.getEntity());
            } catch (RuntimeException e) {

                /*
                    UserRegisterForm에서 어노테이션으로 체크할 수 없는 폼입력값에 대한 체크를 수행하고,
                    체크를 통과하지 못했을 때
                    BindingResult객체의 rejectValue(입력필드명, 에러코드, 기본에러메세지) 메소드를 이용해서 BindingResult객체에
                    에러를 추가할 수 있다.
                */
                errors.rejectValue("email", null, e.getMessage());
                return "registerForm";
            }
            return "redirect:/completed";
        }
    }

4. JSP 코드

jsp 코드에서는 스프링 프레임워크에서 제공해 주는 태그 두 개를 포함시켜 주어야 한다. form은 유효성 체크 시 필요한 태그이니 꼭 include 시켜주자!

    <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
    <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<form:form />태그는 form태그를 나타낸다.
<form:form modelAttribute=”userForm” />은 회원정보를 저장하기 위해서 Model에 저장했던 UserRegisterForm의 이름이다.

<form:input path=”email” />는 <input type=”text” name=”email” />을 나타낸다.
path는 UserRegisterForm에서 해당 입력필드와 대응되는 멤버변수 이름이다.
<form:errors path=”email” /> 이메일 필드의 값이 서버에서 유효성 체크를 통과하지 못했을 때 에러 메세지가 표시되는 곳이다.

    <div class="row mb-3">
        <div class="col">
            <p>이메일, 비밀번호, 이름, 전화번호를 입력하세요</p>
            <form:form modelAttribute="userForm" action="/register" class="border bg-light p-3" method="post">
                <div class="mb-3">
                    <label for="email-field" class="form-label">이메일</label>
                    <form:input class="form-control" id="email-field" path="email" />
                    <form:errors path="email" cssClass="text-danger"/>
                </div>
                <div class="mb-3">
                    <label for="password-field" class="form-label">비밀번호</label>
                    <form:password class="form-control" id="password-field" path="password" />
	                <form:errors path="password" cssClass="text-danger"/>
                </div>
                <div class="mb-3">
                    <label for="name-field" class="form-label">이름</label>
                    <form:input class="form-control" id="name-field" path="name" />
	                <form:errors path="name" cssClass="text-danger"/>
                </div>
                <div class="mb-3">
                    <label for="tel-field" class="form-label">전화번호</label>
                    <form:input class="form-control" id="tel-field" path="tel" />
	                <form:errors path="tel"  cssClass="text-danger"/>
                </div>
                <div class="text-end">
                    <a href="/" class="btn btn-secondary">취소</a>
                    <button type="submit" class="btn btn-primary">회원가입</button>
                </div>
            </form:form>
        </div>
    </div>