[스프링부트+JPA+Validation] 회원가입 - Validation 유효성 검사(필드 규칙, 중복체크)

jyleever·2022년 4월 7일
5

validation 유효성 검사

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

  • 클라이언트 검증과 서버 검증을 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수
  • API 방식을 사용하면 API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨주어야 함
  • @Validated 는 스프링 전용 검증 애노테이션이고, @Valid 는 자바 표준 검증 애노테이션이다. 둘 다 동일한 기능. 단 @Validated 는 groups 기능 포함
  • 검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고, 표준화 한 것이 바로 Bean Validation

    Bean Validation을 잘 활용하면, 애노테이션 하나로 검증 로직을 매우 편리하게 적용할 수 있다.

    • 검증 애노테이션과 여러 인터페이스의 모음

검증 순서

  1. @ModelAttribute 각각의 필드에 타입 변환 시도
    값을 넣으며 타입 변환 시도
    1) 성공하면 다음으로
    2) 실패하면 typeMismatchFieldError 추가

  2. Validator 적용
    바인딩에 성공한 필드만 Bean Validation 적용
    BeanValidator는 바인딩에 실패한 필드는 BeanValidation을 적용하지 않는다.

생각해보면 타입 변환에 성공해서 바인딩에 성공한 필드여야 BeanValidation 적용이 의미 있다. (일단 모델 객체에 바인딩 받는 값이 정상으로 들어와야 검증도 의미가 있다.)

@ModelAttribute → 각각의 필드 타입 변환 시도 → 변환에 성공한 필드만 BeanValidation 적용

1. build.gradle 설정

  • spring boot 2.3 이상부터는 모듈로 빠져 validation 의존성을 따로 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'

2. UserDto

유효성 검사에 필요한 RequestDto 객체에 Validation 어노테이션 사용

		@NotBlank(message = "아이디는 필수 입력값입니다.")
		@Pattern(regexp = "^[a-z0-9]{4,20}$", message = "아이디는 영어 소문자와 숫자만 사용하여 4~20자리여야 합니다.")
		private String username;
		
		@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[$@$!%*#?&])[A-Za-z\\d$@$!%*#?&]{8,16}$", message = "비밀번호는 8~16자리수여야 합니다. 영문 대소문자, 숫자, 특수문자를 1개 이상 포함해야 합니다.")
		private String password;
		
		@NotBlank(message = "닉네임은 필수 입력값입니다.")
		@Pattern(regexp = "^[가-힣a-zA-Z0-9]{2,10}$" , message = "닉네임은 특수문자를 포함하지 않은 2~10자리여야 합니다.")
		private String nickname;
		
		@NotBlank(message = "이메일은 필수 입력값입니다.")
		@Email
		private String email;

3. Controller

RequestDto 객체 앞에 붙은 @Valid 어노테이션을 사용하고, Errors를 통해 유효성 검사 적합 여부 확인

  • bindingRresult 는 error를 자동으로 model에 담아 전달하는데
    이 때 mustache는 bindingresult 의 에러를 처리하는 기능을 제공하지 않는 것 같다..
    따라서 유효성 검사에서 실패한 필드 목록을 받아 미리 정의된 메시지와 함께 map에 넣고 model에 담아 전달하는 로직을 설계한다.
    이 때 view 단으로 넘길 때 변수명을 valid_에러필드명 으로 통일하도록 수정해준다.
	@PostMapping("/auth/joinProc")
	public String joinProc(@Valid UserDto.RequestUserDto dto, BindingResult bindingResult, Model model) {
		
		/* 검증 */
		if(bindingResult.hasErrors()) {
			/* 회원가입 실패 시 입력 데이터 값 유지 */
			model.addAttribute("userDto", dto);
			
			/* 유효성 검사를 통과하지 못 한 필드와 메시지 핸들링 */
			Map<String, String> errorMap = new HashMap<>();
			
			for(FieldError error : bindingResult.getFieldErrors()) {
				errorMap.put("valid_"+error.getField(), error.getDefaultMessage());
				log.info("error message : "+error.getDefaultMessage());
			}

			/* 회원가입 페이지로 리턴 */
			return "/user/user-join";
		}
		
		// 회원가입 성공 시
		userService.userJoin(dto);
		return "redirect:/auth/login";
	}
	

중복 체크

  • 중복 체크는 validation 어노테이션으로 해결할 수 없기 때문에 따로 로직을 만들어주어야 한다.

1. UserRepository

	/* 유효성 검사 - 중복 체크
	 * 중복 : true
	 * 중복이 아닌 경우 : false
	 */
	boolean existsByUsername(String username);
	boolean existsByNickname(String nickname);
	boolean existsByEmail(String email);
  • jpa의 쿼리 메소드는 해당 데이터가 DB에 존재하는지 확인할 때 existsBy 키워드 사용
  • 해당 데이터가 존재하는 경우 true, 존재하지 않는 경우 false 리턴

2. UserService

/* 아이디, 닉네임, 이메일 중복 여부 확인 */
	@Transactional(readOnly = true)
	@Override
	public boolean checkUsernameDuplication(String username) {
		boolean usernameDuplicate = userRepository.existsByUsername(username);
		return usernameDuplicate;
	}

	@Transactional(readOnly = true)
	@Override
	public boolean checkNicknameDuplication(String nickname) {
		boolean nicknameDuplicate = userRepository.existsByNickname(nickname);
		return nicknameDuplicate;
		
	}

	@Transactional(readOnly = true)
	@Override
	public boolean checkEmailDuplication(String email) {
		boolean emailDuplicate = userRepository.existsByEmail(email);
		return emailDuplicate;
	}

3. UserController

	/* 아이디, 닉네임, 이메일 중복 체크 */
	@GetMapping("/auth/joinProc/{username}/exists")
	public ResponseEntity<Boolean> checkUsernameDuplicate(@PathVariable String username){
		return ResponseEntity.ok(userService.checkUsernameDuplication(username));
	}
	
	@GetMapping("/auth/joinProc/{nickname}/exists")
	public ResponseEntity<Boolean> checkNicknameDuplicate(@PathVariable String nickname){
		return ResponseEntity.ok(userService.checkUsernameDuplication(nickname));
	}
	
	@GetMapping("/auth/joinProc/{email}/exists")
	public ResponseEntity<Boolean> checkEmailDuplicate(@PathVariable String email){
		return ResponseEntity.ok(userService.checkUsernameDuplication(email));
	}

원래는 jquery의 .blur 기능을 통해 마우스 포커스가 벗어나면 바로 중복체크 하게 하려고 했는데, 그러기 위해서는
1. 필드 조건에 맞지 않은 에러들도 포커스가 벗어나자마자 나타나도록 바꿔야 함
2. 유효성 검사를 모두 통과한 데이터만이 가입할 수 있도록 제한해야 함
이 두 가지를 해결해야 했다.

이 부분이 중점이 아니기 때문에 우선 회원가입 버튼 누를 때 두 유효성 검사를 다 실행할 수 있도록 (v1) 로직을 바꿨다.
(네이버 회원가입창 처럼 데이터를 입력하고 마우스 포커스가 벗어나면 바로 중복확인/가입조건에 맞는지 확인하도록 메시지가 아래 뜨도록 하고 싶었음)

  • web 패키지에 validator 패키지를 만들어 validator 구현체들을 담았다.

1. UserRepository

  • 그대로 진행

2. Validator 구현체 AbstractValidator

  • 우선 Validator 클래스를 구현한 AbstractValidator 추상 클래스를 생성해준다.
package com.jy.web.validator;

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import lombok.extern.slf4j.Slf4j;

/* 중복 검사를 위한 Validator 구현 추상 클래스 */
@Slf4j
public abstract class AbstractValidator<T> implements Validator{
	
	
	@Override
	public boolean supports(Class<?> clazz) {
		return true;
	}

	@SuppressWarnings("unchecked")
	@Override
	public void validate(Object target, Errors errors) {
		try {
			doValidate((T) target, errors); // 유효성 검증 로직
		} catch (IllegalStateException e) {
			log.error("중복 검증 에러", e);
			throw e;
		}
	}
	
	/* 유효성 검증 로직 */
	protected abstract void doValidate(final T dto, final Errors errors);
}
  • supports()
    @Validated 는 검증기를 실행하라는 애노테이션.
    이 애노테이션이 붙으면 아래에 나오는 WebDataBinder 에 등록한 검증기를 찾아서 실행한다.
    그런데 여러 검증기(Validator)를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다. 이때 supports() 가 사용된다.

  • 검증 로직이 들어갈 부분은 doValidate 메서드로 따로 작성해둔다.

  • 컴파일러에서 경고하지 않도록 @SuppressWarnings("unchecked") 설정

    @SuppressWarnings("unchecked")?

3. CheckUsernameValidator, CheckNicknameValidator, CheckEmailValidator 클래스

AbstractValidator 추상 클래스를 상속받은 각각 필드의 중복 검사 클래스를 만들어보자. - CheckUsernameValidator, CheckNicknameValidator, CheckEmailValidator

  • doValidate 메소드를 오버라이딩하여 중복 검사 로직을 구현한다.
  • CheckUsernameValidator
package com.jy.web.validator;

import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;

import com.jy.domain.user.UserRepository;
import com.jy.web.dto.UserDto;
import com.jy.web.dto.UserDto.RequestUserDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Component
public class CheckUsernameValidator extends AbstractValidator<UserDto.RequestUserDto>{
	
	private final UserRepository userRepository;

	@Override
	protected void doValidate(RequestUserDto dto, Errors errors) {
		if (userRepository.existsByUsername(dto.toEntity().getUsername())) {
			/* 중복인 경우 */
			errors.rejectValue("username","아이디 중복 오류", "이미 사용 중인 아이디입니다.");
		}	
	}
}

rejectValue

  • FieldError, ObjectError을 직접 생성하지 않고 깔끔하게 검증 오류를 다룰 수 있다.
void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);

4. WebDataBinder

  • Validator 클래스 사용을 위해 @InitBinder이 붙은 WebDataBinder을 인자로 받는 메소드를 작성해 검증 Validator 추가

  • InitBinder : 특정 컨트롤러에서 바인딩 또는 검증 설정을 변경하고 싶을 때 사용

    • WebDataBinder binder : HTTP 요청 정보를 컨트롤러 메소드의 파라미터나 모델에 바인딩할 때 사용되는 바인딩 객체. 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다.
    • WebDataBinderaddValidators 메소드를 이용해 검증기(Validator)를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다. @InitBinder 해당 컨트롤러에만 영향을 준다.

      즉, 컨트롤러가 요청 올 때마다 WebDataBinder가 호출되면서 WebDataBinder 에 등록한 검증기가 매번 적용되게 할 수 있음

  • UserController

	...
	/* 중복 체크 유효성 검사 */
	private final CheckUsernameValidator checkUsernameValidator;
	private final CheckNicknameValidator checkNicknameValidator;
	private final CheckEmailValidator checkEmailValidator;
	
	/* 커스텀 유효성 검증 */
	@InitBinder
	public void validatorBinder(WebDataBinder binder) {
		binder.addValidators(checkUsernameValidator);
		binder.addValidators(checkNicknameValidator);
		binder.addValidators(checkEmailValidator);
	}
	

회원가입을 진행하는 joinProc 메소드에서 error에 함께 포함되어 view단에서 에러 메시지를 출력할 수 있다. (폼 입력 시 오류가 발생하면, 고객이 입력한 데이터를 유지한 상태로 어떤 오류가 발생했는지 친절하게 알려주어야 한다.)

Validator을 구현한 커스텀 Validation으로 검증 로직을 작성한다.
해당 Validator을 사용하기 위해 Controller에 @InitBinder가 붙은 WebDataBinder을 파라미터로 받는 메서드를 반드시 추가해준다.


출처
김영한의 스프링 MVC 2편
https://blog.naver.com/lovewouldnever/222543144867

0개의 댓글