유효성 검사

유효성 검사란, 요청한 데이터가 어떤 조건에 충족하는지 확인하는 작업입니다.


예를 들어, 회원가입을 할 때 이메일을 입력하는 란이 있다고 가정하겠습니다.

이메일 주소는 흔히 알고 계시듯이 admin123@example.com 형식으로 요청이 되어야 합니다.

'@' 가 없거나, 다른 기호거나, 영문이 아닌 한글이면 이메일 주소가 될 수 없습니다.


그럼 유효성 검사는 어떤 지점에서 해줘야 할까요?

유효성 검사는 프론트와 백엔드 모두에서 해주는 것이 좋습니다.

프론트에서 JS로 유효성 검사를 하는 이유는 사용자에게 알려주기 위한 UX 측면이 강합니다.

물론 불필요한 요청을 서버로 보내지 않아도 되구요.

하지만 프론트에서만 유효성 검사를 하는것은 보안상 위험하기 때문에, 백엔드에서도 체크해주는 것이 필수입니다.


SpringBoot에서는 Dto의 필드에 조건과 메시지를 작성해주면, @Valid 어노테이션과 함께 유효성 검사를 할 수 있습니다.

예전에 Spring에서 유효성 검사하는 방법을 작성했었는데( 링크 ), 방법적인 면에서 큰 차이는 없는 것 같습니다.





1. 환경 셋팅

  • 개발환경
    • IntelliJ 2019.02
    • SpringBoot 2.2.3
    • Gradle 6.0.1


build.gradle

실습에 필요한 의존성은 thymeleaf, lombok 정도입니다.

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}


프로젝트 구조





2. DTO

먼저 User DTO 클래스를 정의해보도록 하겠습니다.

import lombok.*;

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

@Getter
@Setter
@ToString
@NoArgsConstructor
public class UserDto {
private Long id;

@NotBlank(message = "닉네임은 필수 입력 값입니다.")
private String nickname;

@NotBlank(message = "이메일은 필수 입력 값입니다.")
@Email(message = "이메일 형식에 맞지 않습니다.")
private String email;

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


@Builder
public UserDto(Long id, String nickname, String email, String password) {
this.id = id;
this.nickname = nickname;
this.email = email;
this.password = password;
}
}

dto는 데이터 전달 객체로서, 다음과 같은 목적으로 활용됩니다.

  1. 클라이언트의 요청 데이터가 dto 클래스로 캡슐화되어 서버로 전달
  2. Controller <---> Service 계층 간 데이터 전달


따라서 유효성 검사는 1번의 케이스로 dto를 활용을 할 수 있습니다.

이렇게 dto 클래스의 필드에 각종 어노테이션을 추가해주면 유효성 체크를 할 수 있고, 조건을 만족 못할 경우 에러 메시지를 반환할 수 있습니다.


예제에서 사용된 어노테이션은 다음과 같습니다.

더 많은 어노테이션(bean validation)은 javax.validation.constraints 패키지 또는 여기에서 확인해 볼 수 있습니다.

  • @NotBlank
    • null을 허용하지 않음
    • 적어도 white-space가 아닌 문자가 한개 이상 포함되어야 함
  • @Email
    • 이메일 양식이어야 함
  • @Pattern
    • 정규표현식에 맞는 문자열이어야 함
      • (?=.*[0-9])
        • 숫자 적어도 하나
      • (?=.*[a-zA-Z])
        • 영문 대,소문자중 적어도 하나
      • (?=.*\\W)
        • 특수문자 적어도 하나
      • (?=\\S+$)
        • 공백 제거
그 외
  • @NotEmpty
    • null과 공백 문자열("") 을 허용하지 않음
  • @NotBlank
    • null과 빈 공백 문자열(" ")을 허용하지 않음

각각의 어노테이션에 정의된 조건에 실패했을 시, 에러 메시지를 반환하도록 message를 작성할 수 있습니다.





3. Controller

다음으로 컨트롤러를 구현해보겠습니다.

import lombok.AllArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import javax.validation.Valid;
import java.util.Map;

@Controller
@AllArgsConstructor
public class UserController {
private UserService userService;

@GetMapping("/user/signup")
public String dispSignup(UserDto userDto) {
return "/signup";
}

@PostMapping("/user/signup")
public String execSignup(@Valid UserDto userDto, Errors errors, Model model) {
if (errors.hasErrors()) {
// 회원가입 실패시, 입력 데이터를 유지
model.addAttribute("userDto", userDto);

// 유효성 통과 못한 필드와 메시지를 핸들링
Map<String, String> validatorResult = userService.validateHandling(errors);
for (String key : validatorResult.keySet()) {
model.addAttribute(key, validatorResult.get(key));
}

return "/signup";
}

userService.signUp(userDto);
return "redirect:/user/login";
}

@GetMapping("/user/login")
public String displogin() {
return "/login";
}
}
  • @Valid
    • 클라이언트의 입력 데이터가 dto 클래스로 캡슐화되어 넘어올 때, 유효성을 체크하라는 어노테이션입니다.
    • 앞에서 dto 클래스에 작성한 어노테이션을 기준으로 유효성을 체크합니다.
  • Errors 객체
    • dto에 binding된 필드의 유효성 검사 오류에 대한 정보를 저장하고 노출합니다.
    • errors.hasErrors()
      • 유효성 검사에 실패한 필드가 있는지 확인합니다.
    • 더 많은 정보는 API를 참고해주세요
  • model.addAttribute("userDto", userDto);
    • 회원가입 실패 시, 회원가입 페이지에서 입력했던 정보들을 그대로 유지하기 위해 입력받았던 데이터를 그대로 할당합니다.
      • dispSignup(UserDto userDto) 함수에 파라미터를 정의해준 이유입니다.
      • Validation 관점에서는 필요없는 부분이지만, UX 측면에서 구현해주는 것이 좋아보입니다.
      • 물론, thymeleaf에서도 코드가 들어가야 합니다.




4. Service
Controller에서 유효성 검사에 실패한 필드가 있다면, Service 계층으로 Errors 객체를 전달하여 비즈니스 로직을 구현합니다.
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;

import java.util.HashMap;
import java.util.Map;

@Service
@AllArgsConstructor
public class UserService {
// 회원가입 시, 유효성 체크
public Map<String, String> validateHandling(Errors errors) {
Map<String, String> validatorResult = new HashMap<>();

for (FieldError error : errors.getFieldErrors()) {
String validKeyName = String.format("valid_%s", error.getField());
validatorResult.put(validKeyName, error.getDefaultMessage());
}

return validatorResult;
}

// 회원가입
public void signUp(UserDto userDto) {
// 회원 가입 비즈니스 로직 구현
}
}
  • 유효성 검사에 실패한 필드들은 Map 자료구조를 이용하여 키값과 에러 메시지를 응답합니다.
    • 키 : valid_{dto 필드명}
    • 메시지 : dto에서 작성한 message 값
  • errors.getFieldErrors()
    • 유효성 검사에 실패한 필드 목록을 가져옵니다.
  • error.getField()
    • 유효성 검사에 실패한 필드명을 가져옵니다.
  • error.getDefaultMessage()
    • 유효성 검사에 실패한 필드에 정의된 메시지를 가져옵니다.




5. 퍼블리싱

마지막으로 회원가입 페이지와 로그인 페이지를 구현합니다.


1) resources/templates/signup.html

<!DOCTYPE html>
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">

<title>회원가입</title>
</head>
<body>
<h1>회원 가입</h1>
<hr>

<form th:action="@{/user/signup}" method="post" modelAttribute="userDto">
<div>
<input type="text" name="email" th:value="${userDto.email}" placeholder="이메일을 입력해주세요.">
<span th:text="${valid_email}"></span>
</div>

<div>
<input type="text" name="nickname" th:value="${userDto.nickname}" placeholder="닉네임을 입력해주세요.">
<span th:text="${valid_nickname}"></span>
</div>

<div>
<input type="password" name="password" placeholder="비밀번호">
<span th:text="${valid_password}"></span>
</div>

<button>등록</button>
</form>
</body>
</html>
  • modelAttribute="userDto"
    • 회원가입 실패시, 할당된 dto를 form의 modelAttribute 애트리뷰트로 할당해줍니다.
    • th:value="${userDto.email}"
      • 이렇게 값을 넣어주면 회원가입 실패시에도 입력값을 그대로 유지할 수 있습니다.
  • <span th:text="${valid_email}"></span>
    • 에러 메시지를 노출할 부분입니다.
    • Service에서 valid_{dto 필드명} 포맷으로 키를 전달했던 것을 참고하시면 됩니다.




2) resources/templates/login.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>로그인</title>
</head>
<body>
<h1>로그인 페이지</h1>
<hr>
</body>
</html>

그냥 의미없는 파일입니다...





6. 테스트

1) 아무것도 입력 안했을 때




2) 비밀번호 정규식에 맞지 않을 때



3) 유효성 검사 통과




이상으로 @Valid 어노테이션을 통해 dto 클래스 필드의 유효성 검사를 하는 방법에 대해 알아보았습니다.




[참고 자료]

https://www.baeldung.com/javax-validation

https://055055.tistory.com/37