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 |
댓글