- 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
'🚗 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 |