[스프링부트 게시판] 26. 커스텀 Validation을 통한 중복 검사 구현

2022. 7. 2. 13:54·🚗 Backend Toy Project/스프링 부트 게시판
  • Validation 어노테이션으로는 단일 필드에 대한 유효성 검증만 처리가 가능하기 때문에, 중복체크 같은 경우는 해결이 불가능했습니다.
  • 따라서 커스텀 Validation을 따로 만들어 중복 검사를 구현해보았습니다.
  • 중복되는 코드가 조금 생기긴 하였지만 결과는 나름 만족스럽게 나온 것 같습니다.

📝 1. UserRepository

  • 우선 아래와 같이 해당 데이터가 DB에 존재하는지 여부를 확인하기 위한 Named Query를 작성해주었습니다.
  • Spring Data Jpa에서는 이를 exists를 통해 사용할 수 있습니다.
  • 반환 타입은 boolean 형으로, 해당 데이터가 존재할 경우 true, 존재하지 않을 경우 false를 리턴합니다.
package com.cos.blog.repository;

// @Repository
public interface UserRepository extends JpaRepository<User, Long>{
	
	...
	
	boolean existsByUsername(String username);
	boolean existsByNickname(String nickname);
	boolean existsByEmail(String email);
}

📝 2. Validator를 구현한 AbstractValidator 생성

  • 다음으로 Validator 인터페이스를 구현하는 AbstractValidator 클래스를 생성하여 아래와 같이 구현해주었습니다.
package com.cos.blog.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(RuntimeException e) {
			log.error("중복 검증 에러", e);
			throw e;
		}
	}

	protected abstract void doValidate(final T dto, final Errors errors);
}
  • Validator 인터페이스를 구현하는 클래스는 두 메서드를 필수로 구현해야 합니다.
    • boolean supports(Class claszz): 어떤 타입의 객체를 검증할 때, 이 객체의 클래스가 이 Validator가 검증할 수 있는 클래스인지를 판단하는 메서드
    • void validate(Object target, Errors errors): 실제 검증 로직이 이루어지는 메서드
  • validate에서 검증 로직이 들어갈 부분은 doValidate() 메서드로 따로 빼주었습니다.

📝 3. CheckUsernameValidator, CheckNicknameValidator, CheckEmailValidator 클래스 생성

  • 이후 각각의 필드를 검사하기 위해, 위에서 구현한 AbstractValidator를 상속받는 CheckUsernameValidator, CheckNicknameValidator, CheckEmailValidator 클래스를 각각 생성하여 아래와 같이 구현해주었습니다.
package com.cos.blog.validator;

@RequiredArgsConstructor
@Component
public class CheckUsernameValidator extends AbstractValidator<UserRequestDto> {

	private final UserRepository userRepository;

	@Override
	protected void doValidate(UserRequestDto dto, Errors errors) {
		if(userRepository.existsByUsername(dto.getUsername())) {
			errors.rejectValue("username", "아이디 중복 오류", "이미 사용중인 아이디 입니다");
		}
	}
}
package com.cos.blog.validator;

@RequiredArgsConstructor
@Component
public class CheckNicknameValidator extends AbstractValidator<UserRequestDto> {

	private final UserRepository userRepository;

	@Override
	protected void doValidate(UserRequestDto dto, Errors errors) {
		if(userRepository.existsByNickname(dto.getNickname())) {
			errors.rejectValue("nickname", "닉네임 중복 오류", "이미 사용중인 닉네임 입니다");
		}
	}
}
package com.cos.blog.validator;

@RequiredArgsConstructor
@Component
public class CheckEmailValidator extends AbstractValidator<UserRequestDto> {

	private final UserRepository userRepository;

	@Override
	protected void doValidate(UserRequestDto dto, Errors errors) {
		if(userRepository.existsByEmail(dto.getEmail())) {
			errors.rejectValue("email", "이메일 중복 오류", "이미 사용중인 이메일 입니다");
		}
	}
}
rejectValue()는 필드에 대한 에러코드를 추가하는 메서드입니다.

📝 4. Controller

  • Controller는 아래와 같이 수정해주었습니다.
package com.cos.blog.controller.api;

@RestController
@RequiredArgsConstructor
public class UserApiController {

	...
	
	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);
	}
    
	...
}
  • @InitBinder 어노테이션은 해당 Controller로 들어오는 요청에 대해 추가적인 설정을 하고 싶을 때 사용할 수 있습니다.
    • 즉, 이를 사용하면 해당 Controller로 들어오는 모든 요청 전에 @InitBinder를 선언한 메서드가 실행되게 됩니다.
  • 여기서 매개변수로 들어오는 WebDataBinder는 커맨드 객체를 바인딩하는 객체입니다.
    • 즉, addValidators() 메서드를 통해 위에서 작성한 유효성 검사를 추가하는 것입니다.

📝 5. UserService

  • 아래 메서드는 유효성 검사에 실패한 필드들을 Map 자료구조를 통해 {키 값, 에러 메시지}의 형태로 응답합니다.
    • Key : valid_{dto 필드명}
    • Message : dto에서 작성한 message 값
@Transactional(readOnly = true)
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;
}

📝 6. user.js

  • 이후 자바스크립트를 통해 위에서 전달받은 에러 메시지를 사용자에게 출력해줍니다.
update: function() {
    let data = {
        username: $("#username").val(),
        password: $("#password").val(),
        nickname: $("#nickname").val()
    };

    if(!data.nickname || data.nickname.trim() === "" || !data.password || data.password.trim() === "") {            
        alert("공백 또는 입력하지 않은 부분이 있습니다.");            
        return false;        
    } else if(!/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,16}/.test(data.password)) {            
        alert("비밀번호는 8~16자 영문 대 소문자, 숫자, 특수문자를 사용하세요.");            
        $('#password').focus();            
        return false;        
    } else if(!/^[ㄱ-ㅎ가-힣a-z0-9-_]{2,10}$/.test(data.nickname)) {            
        alert("닉네임은 특수문자를 제외한 2~10자리여야 합니다.");            
        $('#nickname').focus();            
        return false;        
    }

    $.ajax({
        type: "PUT",
        url: "/user",
        data: JSON.stringify(data),
        contentType: "application/json; charset=utf-8",
        //dataType: "json"
    }).done(function(resp) {
        if(resp.status === 500) {
            alert("이미 사용중인 닉네임 입니다.");
            $("#nickname").focus();
            return false;
        }
        alert("회원정보 수정이 완료되었습니다.");
        location.href = "/";
    }).fail(function(error) {
        alert(JSON.stringify(error));
    });
}

💡 알게 된 점

  • 직접 Validation을 구현하는 방법

📌 Reference

  • https://dev-coco.tistory.com/125?category=1032063
 

GitHub - Daegwon-Kim/SpringBoot-JPA-Blog

Contribute to Daegwon-Kim/SpringBoot-JPA-Blog development by creating an account on GitHub.

github.com

 

저작자표시 (새창열림)

'🚗 Backend Toy Project > 스프링 부트 게시판' 카테고리의 다른 글

[스프링부트 게시판] 28. 게시글 검색 기능 구현  (0) 2022.08.30
[스프링부트 게시판] 27. 게시글 작성일 및 조회수 추가  (0) 2022.07.03
[스프링부트 게시판] 25. 회원가입시 validation 체크  (0) 2022.06.30
[스프링부트 게시판] 24. Remember Me 기능 구현  (0) 2022.06.29
[스프링부트 게시판] 23. 로그인 실패 예외 처리  (0) 2022.06.28
'🚗 Backend Toy Project/스프링 부트 게시판' 카테고리의 다른 글
  • [스프링부트 게시판] 28. 게시글 검색 기능 구현
  • [스프링부트 게시판] 27. 게시글 작성일 및 조회수 추가
  • [스프링부트 게시판] 25. 회원가입시 validation 체크
  • [스프링부트 게시판] 24. Remember Me 기능 구현
Baeg-won
Baeg-won
  • Baeg-won
    좋았다면 추억이고 나빴다면 경험이다.
    Baeg-won
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 🍃 Spring, Spring Boot
        • 스프링 프레임워크 기초
        • 스프링 핵심 원리 - 기본편
        • 자바 ORM 표준 JPA 프로그래밍 - 기본편
        • 스프링 MVC
        • 실전! 스프링 부트와 JPA 활용1 - 웹 애플리..
      • 🥑 Web Technoloy
      • 🚗 Backend Toy Project
        • 스프링 부트 게시판
        • Photogram
        • Baeg-won Clothing Gallery
      • 🥇 Problem Solving
        • Breadth-First Search
        • Depth-First Search
        • Backtracking
        • Simulation
        • Two-pointer
        • Binary Search
        • Greedy
        • Dynamic Programming
        • Minimum Spanning Tree
        • Dijkstra
        • Floyd warshall
      • ☕ Java
        • 명품 자바 에센셜
        • Applications
      • 🍦 JavaScript
        • JavaScript 기초
      • 🐧 Linux
        • 이것이 리눅스다(CentOS 8)
      • 📟 Database
        • 혼자 공부하는 SQL
      • 🧬 Data Structure
      • 🎬 HTML
      • 🎤 Tech Interview
      • 📌 etc
        • Unity 2D Raising Jelly Game
        • C++
        • 영어 쉐도잉
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Baeg-won
[스프링부트 게시판] 26. 커스텀 Validation을 통한 중복 검사 구현
상단으로

티스토리툴바