본문 바로가기
프로젝트/PongGame

Account Validation

by 히포파타마스 2022. 5. 10.

Account Validation

Controller로 들어오는 파라미터(Form 객체의 필드)의 값을 검증한다.

검증에는 Bean Validation과 Validator를 사용한다.

 

Bean Validation과 Validator 둘 중 어느 것을 사용했든 이를 적용하려면 다음과 같이 검증할 객체 앞에 @Valid를 붙여주면 된다.

 

[@Valid]

@PostMapping("/account")
public ResultMessage createAccount(@Valid @ModelAttribute AccountForm accountForm)

해당 API를 호출할 때 마다 AccountForm을 검증한다.

 

 

 

1. Form 객체_Bean Validation

Account와 관련된 API에 사용되는 Form 객체

기본적으로 Controller로 들어오는 Form 객체의 필드에 대한 검증은 Bean Validation을 사용한다.

 

 

 

■ AccountForm

회원 가입시 입력되는 정보들을 담을 Form 객체

 

[AccountForm]

@Data
public class AccountForm {

    @Email   //[1]
    @NotBlank   //[2]
    private String username;

    @NotBlank
    @Length(min = 4, max = 30)   //[3]
    private String password;

    @NotBlank
    @Length(max = 30)
    @Pattern(regexp = "^[ㄱ-ㅎ가-힣a-zA-Z0-9_-]{0,9999}$")   //[4]
    private String nickname;

    private MultipartFile profileImgFile;

    @NotNull
    private Gender gender;

    @NotNull
    @DateTimeFormat(pattern = "yyyy-MM-dd")   //[5]
    private LocalDate birthDate;

    public Account toAccount(String profileImgName, String encodePassword) {   //[6]
        return Account.builder()
                .username(this.username)
                .password(encodePassword)
                .nickname(this.nickname)
                .profileImgName(profileImgName)
                .role(Role.NotVerified)
                .gender(this.gender)
                .birthDate(this.birthDate)
                .build();
    }
}

● [1] : 입력된 값이 Email 형식인지를 검증한다.

● [2] : 값이 null 또는 공백으로 이루어져 있는지 검증

● [3] : 최소, 최대 길이를 설정한다.

● [4] : 특정 패턴을 준수하는지 검증한다(한글과 알파벳, 숫자만으로 이루어지도록 설정).

● [5] : 설정한 패턴의 형식을 LocalDate로 변환해준다(Bean Validation이랑 상관없음).

● [6] : Form -> entity로 변환하는 편의 메서드

 

 

 

■ AccountProfileFileForm

프로필 이미지를 수정할 때 프로필 이미지 파일을 담을 Form 객체

 

[AccountProfileFileForm]

@Data
public class AccountProfileFileForm {

    @NotNull
    private MultipartFile profileImgFile;
}

수정시에는 반드시 수정할 이미지 파일이 있어야 하기 때문에 @Notnull을 붙여주었다.

 

 

 

■ AccountSearchCond

Account Page 조회 시 검색 조건으로 입력되는 값을 받을 Form 객체

 

[AccountSearchCond]

@Data
public class AccountSearchCond {

    private String nickname;
}

 

 

 

■ AccountUpdateInfoForm

개인 정보 수정 시 입력되는 정보를 담을 Form 객체

 

[AccountUpdateInfoForm]

@Data
public class AccountUpdateInfoForm {

    @Length(max = 30)
    @Pattern(regexp = "^[ㄱ-ㅎ가-힣a-zA-Z0-9_-]{0,9999}$")
    private String nickname;

    private Gender gender;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate birthDate;

    public Account toAccount() {
        return Account.builder()
                .nickname(this.nickname)
                .gender(this.gender)
                .birthDate(this.birthDate)
                .build();
    }
}

 

 

 

■ AccountUpdatePasswordForm

비밀번호 수정 시 입력되는 정보를 받을 Form 객체

 

[AccountUpdatePasswordForm]

@Data
public class AccountUpdatePasswordForm {

    @NotBlank
    @Length(min = 4, max = 30)
    private String password;

    @NotBlank
    @Length(min = 4, max = 30)
    private String newPassword;

    @NotBlank
    @Length(min = 4, max = 30)
    private String confirmNewPassword;

}

 

 

 

 

2. Validator

Bean Validator를 적용하기 어려운 검증은 Validator로 처리한다.

 

 

 

■ AccountFormValidator

AccountForm에 적용되는 Validator

Validator를 상속받는다.

 

[AccountFormValidator]

@Component
@RequiredArgsConstructor
public class AccountFormValidator implements Validator {

    private final AccountRepository accountRepository;
    private final FileProcessor fileProcessor;

    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.isAssignableFrom(AccountForm.class);
    }

    @Override
    public void validate(Object target, Errors errors) {
        AccountForm accountForm = (AccountForm) target;

        //** username
        if (accountRepository.existsByUsername(accountForm.getUsername())) {
            errors.rejectValue("username", "UsernameDuplication");
        }

        //** nickname
        if (accountRepository.existsByNickname(accountForm.getNickname())) {
            errors.rejectValue("nickname", "NicknameDuplication");
        }

        //** profileFile
        if (accountForm.getProfileImgFile() != null) {
            String originalFilename = accountForm.getProfileImgFile().getOriginalFilename();
            String extracted = fileProcessor.extracted(originalFilename);

            if (originalFilename.isBlank()) {
                errors.rejectValue("profileImgFile", "BlankFileName");
            } else if (!originalFilename.contains(".") || extracted.isEmpty()) {
                errors.rejectValue("profileImgFile", "NonExtractFileName");
            }
        }

        //** birthDate
        LocalDate birthDate = accountForm.getBirthDate();
        if (birthDate != null && birthDate.isAfter(LocalDate.now()) {
            errors.rejectValue("birthDate", "FutureBirthDate");
        }
    }
}

 

 

□ supports

해당 Validator가 검증할 객체를 설정한다.

 

[supports]

@Override
public boolean supports(Class<?> clazz) {
    return clazz.isAssignableFrom(AccountForm.class);
}

검증할 객체는 AccountForm

 

 

□ validate

검증할 객체에 적용할 로직을 구현하는 메서드

 

[validate]

@Override
public void validate(Object target, Errors errors) {
    AccountForm accountForm = (AccountForm) target;   //[1]

    //** username
    if (accountRepository.existsByUsername(accountForm.getUsername())) {   //[2]
        errors.rejectValue("username", "UsernameDuplication");   //[3]
    }

    //** nickname
    if (accountRepository.existsByNickname(accountForm.getNickname())) {
        errors.rejectValue("nickname", "NicknameDuplication");
    }

    //** profileFile
    if (accountForm.getProfileImgFile() != null) {   //[4]
        String originalFilename = accountForm.getProfileImgFile().getOriginalFilename();
        String extracted = fileProcessor.extracted(originalFilename);

        if (originalFilename.isBlank()) {   //[5]
            errors.rejectValue("profileImgFile", "BlankFileName");
        } else if (!originalFilename.contains(".") || extracted.isEmpty()) {   //[6]
            errors.rejectValue("profileImgFile", "NonExtractFileName");
        }
    }

    //** birthDate
    LocalDate birthDate = accountForm.getBirthDate();
    if (birthDate != null && birthDate.isAfter(LocalDate.now()) {   //[7]
        errors.rejectValue("birthDate", "FutureBirthDate");
    }
}

● [1] : target으로는 @Valid가 적용되고 supports 메서드에서 지원하는 객체가 들어온다.

해당 클래스에서는 AccountForm이 들어오기 때문에 target을 AccountForm으로 cast 한다.

 

● [2] : 입력된 username이 DB에 중복되는지 판단.

● [3] : 만약 중복됐다면 field와 errorCode를 설정해서 erros에 추가한다.

※ field는 검증하는 객체 기준 이다(= AccountForm의 field).

 

● profileFile

○ [4] : profileImgFile이 null이 아닐 경우 profileImgFile의 이름과 확장자를 검증한다.

○ [5] : profileImgFile의 이름이 비어있거나 공백으로 이루어져 있을 경우 errors 추가

○ [6] : profileImgFile의 확장자가 없을 경우 errors 추가

 

● [7] : birthDate가 현재 날짜보다 뒤라면 errors 추가

 

 

 

■ AccountProfileFileFormValidator

AccountProfileFileForm에 적용되는 Validator  

파일 수정 시에 profileImgFile을  검증하는데 검증 로직은 앞의 AccountForm과 같다.

 

[AccountProfileFileFormValidator]

@Component
@RequiredArgsConstructor
public class AccountProfileFileFormValidator implements Validator {

    private final FileProcessor fileProcessor;

    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.isAssignableFrom(AccountProfileFileForm.class);
    }

    @Override
    public void validate(Object target, Errors errors) {

        AccountProfileFileForm accountProfileFileForm = (AccountProfileFileForm) target;
        String originalFilename = accountProfileFileForm.getProfileImgFile().getOriginalFilename();
        String extracted = fileProcessor.extracted(originalFilename);

        if (originalFilename.isBlank()) {
            errors.rejectValue("profileImgFile", "BlankFileName");
        } else if (!originalFilename.contains(".") || extracted.isEmpty()) {
            errors.rejectValue("profileImgFile", "NonExtractFileName");
        }
    }
}

 

 

 

■ AccountUpdateInfoFormValidator

AccountUpdateInfoForm에 적용되는 Validator

nickname과 birthDate를 검증하는데 검증 로직은 앞의 AccountForm과 같다.

 

[AccountUpdateInfoFormValidator]

@Component
@RequiredArgsConstructor
public class AccountUpdateInfoFormValidator implements Validator {

    private final AccountRepository accountRepository;

    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.isAssignableFrom(AccountUpdateInfoForm.class);
    }

    @Override
    public void validate(Object target, Errors errors) {

        AccountUpdateInfoForm accountUpdateInfoForm = (AccountUpdateInfoForm) target;

        //nickname//
        if (accountRepository.existsByNickname(accountUpdateInfoForm.getNickname())) {
            errors.rejectValue("nickname", "NicknameDuplication");
        }
        //nickname//

        //birthDate//
        LocalDate birtDate = accountUpdateInfoForm.getBirthDate();

        if (birtDate != null && birtDate.isAfter(LocalDate.now())) {
            errors.rejectValue("birthDate", "FutureBirthDate");
        }
        //birthDate//
    }
}

 

 

 

■ AccountUpdatePasswordFormValidator

AccountUpdatePasswordForm에 적용되는 Validator

 

[AccountUpdatePasswordFormValidator]

@Component
@RequiredArgsConstructor
public class AccountUpdatePasswordFormValidator implements Validator {

    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.isAssignableFrom(AccountUpdatePasswordForm.class);
    }

    @Override
    public void validate(Object target, Errors errors) {
        AccountUpdatePasswordForm accountUpdatePasswordForm = (AccountUpdatePasswordForm) target;

		//[1]
        UserAccount userAccount = (UserAccount) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        Account account = userAccount.getAccount();

		//[2]
        //현재 비밀번호 확인//
        if (!bCryptPasswordEncoder.matches(accountUpdatePasswordForm.getPassword(), account.getPassword())) {
            errors.rejectValue("password", "WrongPassword");
        }
        //현재 비밀번호 확인//

		//[3]
        //현재 비밀번호와 새 비밀번호 일치 확인//
        if (bCryptPasswordEncoder.matches(accountUpdatePasswordForm.getNewPassword(), account.getPassword())) {
            errors.reject("SamePassword");
        }
        //현재 비밀번호와 새 비밀번호 일치 확인//

		//[4]
        //새 비밀번호 확인//
        if (!accountUpdatePasswordForm.getNewPassword().equals(accountUpdatePasswordForm.getConfirmNewPassword())) {
            errors.reject("Discord");
        }
        //새 비밀번호 확인//
    }
}

● [1] : 현재 로그인된 Account의 정보를 조회

● [2] : 현재 로그인된 Account의 password와 입력된 password가 일치하는지 검증

● [3] : 현재 비밀번호와 새 비밀번호가 일치하는지 확인. 일치한다면 errors 추가

● [4] : 새 비밀번호와 확인 비밀번호가 일치하는지 확인. 일치하지 않는다면 errors 추가

 

 

 

■ Validator 적용

Validator를 구현만 해서는 적용되지 않고 컨트롤러에 요청이 들어올 때 구현한 Validator가 적용되도록 설정해주어야 한다.

 

 

□ @InitBinder

컨트롤러 안에 해당 어노테이션을 붙인 메서드를 추가하면 컨트롤러 실행 전에 해당 메서드가 실행된다.

@InitBinder는 다음과 같이 적용할 수 있다.

 

[@InitBinder 적용]

@RestController
@RequiredArgsConstructor
public class AccountController {

    @InitBinder("accountForm")
    public void accountFormBinder(WebDataBinder webDataBinder) {
        webDataBinder.addValidators(accountFormValidator);
    }
    
    .
    .
    .
   
   @PostMapping("/account")
    public ResultMessage createAccount(@Valid @ModelAttribute AccountForm accountForm) throws IOException {
    
    .
    .
    .
    
    }
}

● 컨트롤러 실행전에 메서드를 적용할 객체를 설정해주는 것이 가능하다.

● 컨트롤러 실행전에 accountForm에 accountFormBinder가 실행되어 Validator가 적용된다.

 

 

□ ContorllerAdvice

InitBinder는 한 번에 한 객체만 설정 가능하기 때문에 검증할 Form이 많아질수록 컨트롤러에 @InitBinder가 적용된 메서드가 쌓이게 된다.

컨트롤러의 메서드와 직접 연관이 없는 메서드가 쌓이면 보기 안 좋기 때문에 따로 정리할 필요가 있다.

 

@ContorllerAdvice는 모든 컨트롤러에 전역적으로 설정을 추가할 수 있게 해주는 어노테이션이다.

@ContorllerAdvice를 이용해서 @InitBinder를 적용한 메서드를 모두 한 클래스로 정리한다.

 

[InitBinderControllerAdvice]

@RestControllerAdvice(annotations = RestController.class)   //[1]
@Component
@RequiredArgsConstructor
public class InitBinderControllerAdvice {

    private final AccountFormValidator accountFormValidator;
    private final AccountUpdateInfoFormValidator accountUpdateInfoFormValidator;
    private final AccountProfileFileFormValidator accountProfileFileFormValidator;
    private final AccountUpdatePasswordFormValidator accountUpdatePasswordFormValidator;
    private final LoginFormValidator loginFormValidator;


    //AccountController//
    @InitBinder("accountForm")
    public void accountFormBinder(WebDataBinder webDataBinder) {
        webDataBinder.addValidators(accountFormValidator);
    }

    @InitBinder("accountUpdateInfoForm")
    public void accountUpdateInfoFormBinder(WebDataBinder webDataBinder) {
        webDataBinder.addValidators(accountUpdateInfoFormValidator);
    }

    @InitBinder("accountProfileFileForm")
    public void accountProfileFileFormBinder(WebDataBinder webDataBinder) {
        webDataBinder.addValidators(accountProfileFileFormValidator);
    }

    @InitBinder("accountUpdatePasswordForm")
    public void accountUpdatePasswordFormBinder(WebDataBinder webDataBinder) {
        webDataBinder.addValidators(accountUpdatePasswordFormValidator);
    }
    //AccountController//

● [1] : 컨트롤러 중에서도 @RestController가 붙은 컨트롤러에만 적용되도록 설정 

● 해당 클래스 내에 정의된 @InitBinder가 붙은 메서드는 @RestController가 붙은 모든 컨트롤러에 적용된다.

 

 

 

'프로젝트 > PongGame' 카테고리의 다른 글

Account Service  (0) 2022.05.11
Account_exceptionHandler  (0) 2022.05.11
Account Repository  (0) 2022.05.10
Account API 명세  (0) 2022.05.05
EntityListeners  (0) 2022.04.28

댓글