Exception & Validation
1. 예외 처리 Exception Handling
: 예측하지 못한 상황이 발생했을 때, 프로그램은 예외를 발생시킨다.
1) ResponseStatusException
: 간단한 예외처리 방법.
- 간편하게 사용할 수 있다
- 다만, 전체 프로젝트 단위에서 예외 처리를 적용하기 어려운 구조/ 똑같은 코드를 여러번 반복한다는 단점.
public UserDto updateUser(Long id, UserDto dto) {
Optional<User> optionalUser
= repository.findById(id);
if (optionalUser.isEmpty())
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
...
try {
Files.createDirectories(Path.of("media"));
Path path = Path.of("media/filename.png");
multipartFile.transferTo(path);
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
}
2) ExceptionHandler
: Controller에 @ExceptionHandler 어노테이션 추가.
- 해당 컨트롤러 안에서 인자로 전달된 예외가 발생하면 >> 해당 예외를 인자로 전달받고 >> 전달받은 예외를 바탕으로 응답을 보낼 수 있다.
// UserController
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorDto handleIllegalArgument(
final IllegalArgumentException exception) {
ErrorDto dto = new ErrorDto();
dto.setMessage(exception.getMessage());
log.warn(exception.getMessage());
return dto;
}
@ExceptionHandler(IllegalArgumentException.class) //어떤 예외가 발생했을 때
public ResponseEntity<ErrorResponseDto> handleIllegalArgument(
final IllegalArgumentException exception) { // 해당 예외를 인자로서 메서드를 실행하게 됨.
return ResponseEntity
.badRequest()
.body(new ErrorResponseDto(exception.getMessage()));
}
// UserService
public UserDto create(UserDto dto) {
if (repository.existsByUsername(dto.getUsername())) {
throw new IllegalArgumentException("duplicate username");
}
User user = new User(dto.getUsername(), dto.getEmail(), dto.getPhone(),
dto.getBio());
return UserDto.fromEntity(repository.save(user));
}
--> 단점 : Controller 단위로만 작동한다..
3) @ControllerAdvice & @RestControllerAdvice
: 여러 Controller를 다루는 큰 프로젝트에서 전체 프로젝트에 관찰되는 예외를 정리하고 싶다면!? (프로젝트 전역 예외 처리)
- @ExceptionHandler를 모아두기 위한 Component의 일종
- @RestControllerAdvice : @ResponseBody가 포함된 형태.
@RestControllerAdvice
public class UserControllerAdvice {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponseDto> handleIllegalArgument(
final IllegalArgumentException exception) {
return ResponseEntity
.badRequest()
.body(new ErrorResponseDto(exception.getMessage()));
}
}
4) 커스텀 예외 활용하기
: 프로젝트가 커지면서 예외 발생 상황도 다양하게 변할 수 있다. > 이 때 커스텀 예외를 만들 수 있음.
- 이름에 상황에 대한 정보를 포함해서 빠르게 예외 상황에 대한 정보를 전달하기 편함.
- 상속 관계를 활용하여 예외 처리 과정 간소화
- @ResponseStatus 어노테이션과 함께 응답 상태 코드를 일관성 있게 전달 가능.
ex. 사용자 이름이 중복되었을 때
// UserService. class
public UserDto create(UserDto dto) {
if (repository.existsByUsername(dto.getUsername())) {
// throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
throw new UsernameExistsException();
}
User user = new User(dto.getUsername(), dto.getEmail(), dto.getPhone(),
dto.getBio());
return UserDto.fromEntity(repository.save(user));
}
// 사용자 이름이 중복일 때 발생하는 예외
public class UsernameExistsException extends RuntimeException {
public UsernameExistsException() {
super("username already exists");
}
}
2. 유효성 검사
: 사용자가 입력한 데이터가 허용하는 형태의 데이터인지를 검증하는 작업.
1) spring-boot-starter-validation
- Jakarta Bean Validation : 유효성 검증을 위한 기술 명세. 어떤 항목이 어떤 규칙을 지켜야 하는지를 표시하는 기준.
- Hibernate Validation : Jakarta Bean Validation을 토대로 실제로 검증해주는 프레임워크.
(JPA - Hibernate ORM의 관계와 비슷하다. Jakarta Bean Validation - Hibernate Validation)
>> 이들을 묶어서 Spring Boot에서 적용하게 해주는 것이 이 의존성
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'
3. @Valid / @Validated 써보기
1) @Valid
- 사용자가 입력하는 데이터 중 검증하고 싶은 데이터에 @검증사항 애너테이션을 붙이고, 그 데이터의 입력을 검증해야 하는 곳에 @Valid를 사용한다.
// UserDto
@Data
public class UserDto {
private Long id;
@NotNull(message = "username is required")
private String username;
private String email;
}
// UserController
@PostMapping("/validate-dto")
public String validateDto(@Valid @RequestBody UserDto dto) {
log.info(dto.toString());
return "done";
}
Jakarta Bean Validation specification
Jakarta Bean Validation specification
BeanNode, PropertyNode and ContainerElementNode host getContainerClass() and getTypeArgumentIndex(). If the node represents an element that is contained in a container such as Optional, List or Map, the former returns the declared type of the container and
beanvalidation.org
간단 정리
@AssertTrue | boolean이 true인가? |
@AssertFalse | boolean이 false인가? |
@Min(숫자) | 지정 값 이상인가? |
@Max(숫자) | 지정 값 이하인가? |
@DecimalMin(value=실수) | 지정된 값(실수) 이상인가? |
@DecimalMax(value=실수) | 지정된 값(실수) 이하인가? |
@Negative | 음수만 가능 |
@Positive | 양수만 가능 |
@NegativeOrZero | 음수와 0만 가능 |
@PositiveOrZero | 양수와 0만 가능 |
@Digits(integer=정수, fraction=소수자리수) | 대상 수가 지정된 정수와 소수 자리수보다 작은가? |
@Pattern(regex=정규식) | 정규식을 만족하는가 |
@Past | 현재보다 과거인가? |
@Future | 현재보다 미래인가? |
@NotNull | 데이터가 null이 아님을 검증. 대부분의 null이 할당될 수 있는 타입에 사용. - " " 공백 문자열을 넣어도 가능. |
@NotEmpty | 데이터가 비어있지 않음을 검증. String, List, 배열 등 여러 데이터를 가지고 있는 자료형에서 사용 가능. - String : 길이가 0이 아님 / " " 공백 문자열을 넣어도 가능. - List 등 Collection : 아이템이 하나 이상 존재하는지 검증. |
@NotBlank | 문자열에 대해 공백이 아님을 검증. - 공백 기준 : 공백문자, 탭, 개행문자 등 공백으로 나타나는 문자들을 제외한 문자열의 길이를 의미. - " " 공백 문자열 불가능. "" 이것도 불가능. |
@Null | null만 가능 |
이메일 형식만 가능 | |
@Size(min=최소, max=최대) | 문자열, 배열 등의 크기가 만족하는지 |
![](https://blog.kakaocdn.net/dn/bdfA5G/btsDRbhQyeY/YaqwIKbUoS5x2SKDQPx8Y1/img.png)
2) @Validated
- 메서드 파라미터 개별 검증
: 어떤 메서드의 파라미터를 개별적으로 검증해야 하는 상황에서 사용 가능.
- 꼭 Controller가 아니고, Service에도 첨부하여 메서드 인자를 검증할 수 있다.
@Validated
@RestController
public class UserController {
@GetMapping("/validate-params")
public Map<String, String> validateParams(
@RequestParam("age")
@Min(19)
Integer age
) {
// ...
}
}
- 요청 객체 부분 검증
- 회원가입 단계 : username, password만 검증
- 추가정보 기입 단계 : email, phone만 검증
1) 그룹으로 묶기 위해 interface 정의
2) 각 속성의 제약사항의 groups 인자로 전달한다.
3) 검증 단계에서는 @Valid 대신 @Validated 사용 >> 이 때 어떤 그룹의 속성들을 검증할 것인지 인자로 전달!
// 그룹 interface
public interface MandatoryStep { }
// UserPartialDto class
@Data
public class UserPartialDto {
//회원가입 단계에서 반드시 첨부해야하는 데이터
// 단 회원정보 업데이트 단계에서는 반드시는 아님.
@Size(min = 8, groups = MandatoryStep.class,
message = "회원 이름은 8자 이상 입력해주세요.")
private String username;
@Size(min = 10, groups = MandatoryStep.class,
message = "비밀번호는 10자 이상 입력해주세요.")
private String password;
// 회원가입 완료 후 추가 정보 기입 단계에서 첨부하는 데이터
// 단 추가정보 기입시에는 반드시 포함해야 한다.
@NotNull
@Email
private String email;
@NotNull
private String phone;
}
// UserController class
// 필수 정보 유효성 검사
@PostMapping("/validate-man")
public ResponseEntity<Map<String, String>> validateMan(
@Validated(MandatoryStep.class)
@RequestBody
UserPartialDto dto
) {
log.info(dto.toString());
Map<String, String> responseBody = new HashMap<>();
responseBody.put("message", "success!");
return ResponseEntity.ok(responseBody);
}
1) 유효성 검사 통과시 :: message가 표시된다.
2) 유효성 검사 미통과시 :: username과 password에 대한 유효성 검증 실패 이유만 나온다. email, phone은 유효성에 맞지 않아도 상관없다.
- 검증 실패시 응답을 원하는 형태로 정의하기 : ExceptionHandler 사용하여 예외처리 해주기
유효성 검증에 실패하면 >> MethodArgumentNotValidException이 발생한다!! > 이에 대해 ExceptionHandler 정의하면 된다.
+ 사용자 오류의 응답 코드 400 설정.
1) 유효성 검사 미통과시 :: 자동 오류 메시지
2) 유효성 검사 미통과시 :: 개발자 설정 오류 메시지가 보여진다.
// UserController class
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, Object> handleValidationException(
final MethodArgumentNotValidException exception
) {
Map<String, Object> errors = new HashMap<>();
List<FieldError> fieldErrorList = exception.getBindingResult().getFieldErrors(); // 필드 유효성 검사중 발생한 오류 목록이 저장된다.
for (FieldError error : fieldErrorList) {
errors.put(error.getField(), error.getDefaultMessage());
}
return errors;
}
4. 사용자 지정 유효성 검사
: 상황에 따라 부족한 경우 어노테이션을 직접 만들고, 그 어노테이션이 적용된 필드를 검사하는 방법.
1) 어노테이션 만들기
- Java 파일에 class 대신 @interface를 붙여준다.
public @interface EmailWhiteList {}
- @Target(ElementType.xxx) 어노테이션 추가 :: 어디에 덧붙일 수 있는지를 정의하는 용도.
@Target(ElementType.FIELD)
public @interface EmailWhiteList {}
- @Retention(RetentionPolicy.xxx) 어노테이션 추가 :: 어노테이션이 어느 시점까지 유지될지를 정의하는 용도. > 유효성 검사는 서비스 실행 중에도 확인가능해야 하기 때문에 > RUNTIME으로 설정.
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EmailWhiteList {}
2) Annotation Element 정의하기
- message
- groups
- payload
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EmailWhiteList {
String message() default "Email not in whitelist";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
3) ConstraintValidator
: 어노테이션이 붙은 필드가 조건을 만족하는지 검증하는 클래스를 만든다.
- @EmailWhiteList가 붙은 String 데이터에 대해서 동작하는 검증기
- + ConstraintValidator를 상속받았으므로 isValid() 메서드를 구현한다. => 이 때 어떤 검증과정이 있는지를 구현.
public class EmailWhiteListValidator
implements ConstraintValidator<EmailWhiteList, String> {
private final Set<String> whiteList;
public EmailWhiteListValidator() {
this.whiteList = new HashSet<>();
this.whiteList.add("gmail.com");
this.whiteList.add("naver.com");
}
// EmailWhiteList 어노테이션이 붙은 대상의 데이터가
// 검사를 통과하면 true, 실패하면 false 반환하도록
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// value = 사용자가 실제로 입력한 내용이다.
// value가 null인지 체크, value에 @ 포함 체크
if (value == null || !value.contains("@")) {
return false;
}
// value를 @기준으로 자른 뒤 EmailWhiteList에 담긴 값 중 하나인지 확인
String[] split = value.split("@");
if (! this.whiteList.contains(split[split.length-1])) {
return false;
}
return true;
}
}
- 그리고 나서 이 ConstraintValidator를 어떤 어노테이션에서 검증을 할지를 표시해주어야 한다.
>> @Constraint(validatedBy= xx)로 정의.
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EmailWhiteListValidator.class)
public @interface EmailWhiteList {
String message() default "Email not in whitelist";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
이렇게 하고 실제 Dto에 어노테이션을 붙여주어야 한다.
@Data
public class UserDto {
private Long id;
@NotBlank(message = "username is required")
private String username;
@EmailWhiteList
private String email;
private int age;
@Future(message = "time") //현재 이후의 시간만 가능.
private LocalDate validUntil;
}
결과로 잘 나왔다~~
** BlackList도 만들어보았다! 이번에는 생성자에서 블랙리스트를 지정해주는 것이 아닌, 실제 사용시에 설정해주는 방법.
- EmailBlackList @interface
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EmailBlackListValidator.class)
public @interface EmailBlackList {
String message() default "Email in BlackList";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 어노테이션 사용시 blacklist()를 설정해줄 수 있다.
String[] blacklist() default {};
}
- EmailBlackListValidator : 검증기 만들어주기
public class EmailBlackListValidator
implements ConstraintValidator<EmailBlackList, String> {
Set<String> blacklist;
// initialize시 받아오는 것을 가져와서 blacklist에 넣기.
@Override
public void initialize(EmailBlackList annotation) {
this.blacklist = new HashSet<>();
this.blacklist.addAll(Arrays.asList(annotation.blacklist()));
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// value = 사용자가 실제로 입력한 내용이다.
// value가 null인지 체크, value에 @ 포함 체크
if (value == null || !value.contains("@")) {
return false;
}
// value를 @기준으로 자른 뒤 EmailWhiteList에 담긴 값 중 하나인지 확인
String[] split = value.split("@");
if (this.blacklist.contains(split[split.length - 1])) {
return false;
}
return true;
}
}
- UserDto 에서 어노테이션 달아주기.
@Data
public class UserDto {
private Long id;
@NotBlank(message = "username is required")
private String username;
@EmailBlackList(blacklist = {"hanmail.net", "yahoo.com"})
@EmailWhiteList
private String email;
private int age;
@Future(message = "time") //현재 이후의 시간만 가능.
private LocalDate validUntil;
}
![](https://blog.kakaocdn.net/dn/x1w5r/btsDQdOiSUd/djnkHrdzWMk9hFEM1nrbEk/img.png)
![](https://blog.kakaocdn.net/dn/Bc2Gp/btsDSsD7zVc/oQHAbz6ok0pigdlZCnSLz1/img.png)
결과도 잘 나왔다~~~~ 예
'Programming > Spring, SpringBoot' 카테고리의 다른 글
Spring Security... 그리고 인터페이스 (0) | 2024.01.25 |
---|---|
Testing (1) | 2024.01.24 |
File Handling (정적파일과 Multipart/form-data) (0) | 2024.01.22 |
Query Parameter (0) | 2024.01.22 |
Spring Boot & REST (0) | 2024.01.18 |