Validation이라는 라이브러리를 사용해 유효성 검사를 해주자.
Blog-V2에서 if로 직접 null 체크했던 과정을 Validation 라이브러리가 해주는 것이다.
아주 간편하다.
체크 후 오류가 있는지 없는지 확인하는 건 따로 해줘야 한다. 나중에 해보자.
@PostMapping("/join")
public String join(@Valid JoinReqDto joinReqDto, BindingResult bindingResult) {
System.out.println(bindingResult.hasErrors()); // 하나라도 오류가 있다면 true
return "redirect:login-form";
}
실행 시에 @Valid가 붙어있는 JoinReqDto를 확인해보고,
자신의 어노테이션인 @Size와 @NotBlank를 보고 유효성 검사를 한다.
이를 리플렉션이라고 한다.
DS가 요청을 받을 때까지는 체크하지 않고 받는데
DS가 컨트롤러에 join 메서드를 호출할 때 @Valid가 붙어있는 것을 확인하고 joinReqDto를 체킹 한다.
체킹 하러 들어가서 어노테이션을 확인하고 체킹 하는 것이다.
@Valid가 안 붙어있거나 @Size, @NotBlank가 안 붙어있으면 체킹 하지 않는다.
유효성 검사를 한 결과 값은 BindingResult에 담긴다.
이렇게 사용하면 우리는 Dto만 만들어주면 되어 아주 편안해진다.
그래서 Dto를 만드는 것이다!!
여기서 TIP!
BindingResult 변수는 무조건 검사하는 Dto 변수 바로 뒤에 적어줘야 한다.
Dto와 BindingResult 사이에는 다른 매개변수가 오면 안 된다!!
주의하자!!!
어노테이션 NotNull과 NotEmpty 두 개의 조합이 NotBlank이다.
이거만 쓰면 끝!
* @Bean Validation 어노테이션 종류 *
@Null | Null 값만 허용 됨 |
@NotNull | null (X), "" (O), " " (O) |
@NotEmpty | null (X), "" (X), " " (O) |
@NotBlank | null (X), "" (X), " " (X) |
@Pattern(regexp="정규표현식", message="에러메시지") | 입력값이 정규표현식에 해당되어야 함 |
@Range(min=최소값, max=최대값) | 입력값이 범위 내의 값이어야 함 |
@Size(min=최소값, max=최대값) | 입력크기가 범위 내의 크기이어야 함 |
@Max(최대값) | 입력값이 최대값보다 작아야 함 |
@Min(최소값) | 입력값이 최소값보다 커야 함 |
유효한 이메일인지 확인 함 | |
@DateTimeFormat(pattern="YYYYMMDD") | 패턴에 맞는 날짜 형식이어야 함 |
이메일 체크하는 어노테이션도 있으니 JoinReqDto의 email 필드에 걸어주자.
@Size(min = 8, max = 60)
@NotBlank
@Email
private String email;
유저네임에 한글을 적지 못하게 정규표현식 패턴도 걸어준다.
영문과 숫자를 4자에서 20자까지 적을 수 있다고 걸어주는 것이다.
패턴에 적합하지 않은 값을 입력하면 뱉어줄 수 있는 오류 메세지도 지정해 줄 수 있다.
@Size(min = 4, max = 20)
@NotBlank
@Pattern(regexp = "[a-zA-Z1-9]{4,20}", message = "유저네임은 한글이 들어갈 수 없습니다.")
private String username;
package site.metacoding.blogv3.web.dto.user;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import site.metacoding.blogv3.domain.user.User;
@AllArgsConstructor
@NoArgsConstructor
@Data // Getter(필수), Setter, toString
public class JoinReqDto {
@Size(min = 4, max = 20)
@NotBlank
@Pattern(regexp = "[a-zA-Z1-9]{4,20}", message = "유저네임은 한글이 들어갈 수 없습니다.")
private String username;
@Size(min = 4, max = 20)
@NotBlank
private String password;
@Size(min = 8, max = 60)
@NotBlank
@Email
private String email;
public User toEntity() {
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setEmail(email);
return user;
}
}
Validation이 체크 후 결과를 BindingResult에 담아준다고 했다.
bindingResult의 hasErrors( ) 메서드는 오류가 있는지 없는지 알려준다.
하나라도 오류가 발생한다면 true를 뱉어준다.
getFieldErrors( ) 메서드는 오류가 하나가 아닐 수 있기 때문에 Errors이고, 이는 for문을 돌려 확인해볼 수 있다.
getField( ) 메서드는 어떤 필드에서 오류가 발생했는지 알려준다.
Dto의 필드에서 오류가 발생했을 때 메세지를 지정해주지 않아도
getDefaultMessage( ) 메서드에서 디폴트 메세지가 정해져 있다.
@PostMapping("/join")
public String join(@Valid JoinReqDto joinReqDto, BindingResult bindingResult) {
// 회원가입 로직에서 유효성 검사 코드는 부가적인 코드!! -> AOP
if (bindingResult.hasErrors() == true) { // 하나라도 오류가 있다면 true
Map<String, String> errorMap = new HashMap<>();
for (FieldError fe : bindingResult.getFieldErrors()) {
// System.out.println(fe.getField()); // 어느 변수에서 오류가 났는지 알려줌
// System.out.println(fe.getDefaultMessage()); // 메세지 지정안해줘도 디폴트 메세지가 있음
errorMap.put(fe.getField(), fe.getDefaultMessage());
}
throw new CustomException(errorMap.toString());
}
return "redirect:login-form";
}
올바르지 않은 요청을 했을 때 Exception을 잡아채갈 에러 핸들러를 만들어주자.
fetch와 같이 json 데이터를 리턴해주는 요청에서 오류가 발생했을 때 잡아가는 핸들러 하나,
파일을 돌려줘야 하는 요청에서 오류가 발생했을 때 잡아가는 핸들러 하나를 따로 분리해서 만들어준다.
package site.metacoding.blogv3.handler;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import site.metacoding.blogv3.handler.ex.CustomApiException;
import site.metacoding.blogv3.handler.ex.CustomException;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomApiException.class)
public ResponseEntity<?> apiException(Exception e) { // fetch 요청 시 발동 -> json 응답
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(CustomException.class)
public String htmlException(Exception e) { // 일반적인 Get(a태그), Post(form태그) 요청 -> html 응답
StringBuilder sb = new StringBuilder();
sb.append("<script>");
sb.append("alert('" + e.getMessage() + "');");
sb.append("history.back();");
sb.append("</script>");
return sb.toString();
}
}
package site.metacoding.blogv3.handler.ex;
public class CustomApiException extends RuntimeException {
public CustomApiException(String message) {
super(message);
}
}
package site.metacoding.blogv3.handler.ex;
public class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
}
Exception의 생성자에서 super(message)를 호출하면 RuntimeException의 생성자가 호출되는데
그 위를 계속 따라가다 보면 Throwable이 최종 부모 클래스이다.
여기서 받은 message가 detailMessage에 담기게 되고
우리가 e.getMessage( ) 하고 받는 메세지가 바로 이 detailMessage가 된다.
위 컨트롤러 코드는 로그인 로직에서도 반복되는데
이렇게 반복되는 코드는 AOP로 분리시킬 수 있겠다.
AOP는 인터셉터와 다르게 메서드 내부의 전후를 관리할 수 있으니까!
우리는 AOP를 사용하지 않고 해 줄 것이다.
[출처]
https://cafe.naver.com/metacoding
메타 코딩 유튜브
https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9