밍쎄의 코딩공간
[스프링 부트 핵심 가이드] - 10. 유효성 검사와 예외처리 본문
애플리케이션의 비즈니스 로직이 올바르게 동작하려면 데이터 사전 검증하는 작업이 필요하다. 이것은 유효성 검사 또는 데이터 검사 또는 데이터 검증이라고 부른다. 유효성의 검사의 예로는 여러 계층에서 들어오는 데이터에 대해 의도한 형식대로 값이 들어오는지 체크하는 과정이 있다. 이 같은 유효성 검사는 프로그래밍에서 매우 중요한 부분이며, 자바에서 가장 신경 써야하는 것 중 하나로 "NullPointException"이 있다.
일반적인 애플리케이션 유효성 검사의 문제점
: 계층별로 진행하는 유효성 검사는 검증 로직이 각 클래스별로 분산되어 관리 어려움
- Bean Validation : 데이터 유효성 검사 프레임 워크 제공
- 어노테이션을 통해 다양한 데이터를 검증하는 기능을 제공
- 유효성 검사를 위한 로직을 DTO 같은 도메인 모델과 묶어서 각 계층에서 사용하면서 검증 자체를 도메인 모델에 얹는 방식으로 수행
- 코드의 간결함 유지 가능
스프링 부트에서의 유효성 검사
유효성 검사는 각 계층으로 데이터가 넘어오는 시점에 해당 데이터에 대한 검사를 실시한다. 스프링 부트 프로젝트에서는 계층 간 데이터 전송에 대체로 DTO객체를 활용하고 있기 때문에 위와 같이 유효성 검사를 DTO객체를 대상으로 수행하는 것이 일반적이다.
유효성 검사를 위한 어노테이션
- 문자열 검증
- @Null : null 값만 허용
- @NotNull : "", " " 허용, null 값 불허
- @NotEmpty : " " 허용, null 값, "" 불허
- @NotBlank : null 값, "", " " 불허
- 최댓값/최솟값 검증
- BigDecimal, BigInteger, int, long 타입 지원
- @DecimalMax(value = "$numberString") : $numberString보다 작은 값 허용
- @DecimalMin(value = "$numberString") : $numberString보다 큰 값 허용
- @Min(value = $number) : $number 이상의 값 허용
- @Max(value = $number) : $number 이하의 값 허용
- 값의 범위 검증
- BigDecimal, BigInteger, int, long 타입 지원
- @Positive : 양수 허용
- @PositiveOrZero : 0을 포함한 양수 허용
- @Negative : 음수 허용
- @NegativeOrZero : 0을 포함한 음수 허용
- 시간에 대한 검증
- Date, LocalDate, LocalDateTime 타입 지원
- @Future : 현재보다 미래의 날짜 허용
- @FutureOrPresent : 현재를 포함한 미래의 날짜 허용
- @Past : 현재보다 과거의 날짜 허용
- @PastOrPresent : 현재를 포함한 과거의 날짜 허용
- 이메일 검증
- @Email : 이메일 형식 검사, "" 허용
- 자릿수 범위 검증
- BigDecimal, BigInteger, int, long 타입 지원
- @Digits(integer = $number1, fraction = $number2) : $number1의 정수 자릿수와 $number2의 소수 자릿수 허용
- Boolean 검증
- @AssertTrue : true인지 체크, null 값 체크 X
- @AssertFalse : falser인지 체크, null 값 체크 X
- 문자열 길이 검증
- @Size(min = $number1, max = $number2) : $number1 이상 $number2 이하의 범위 허용
- 정규식 검증
- @Pattern(regexp = "$expression") : 정규식 검사
@Validated 활용
public interface ValidationGroup1 {
}
public interface ValidationGroup2 {
}
@Validated은 @Valid 어노테이션의 기능을 포함하고 있기 때문에 @Validated로 변경할 수 있다. 또한 @Validated는 유효성 검사를 그룹으로 묶어 대상을 특정할 수 있는 기능이 있다.
검증 그룹은 별다른 내용이 없는 마커 인터페이스를 생성해서 사용한다 실습을 위해 그림과 같이 data 패키지 내에 group 패키지를 생성하고 ValidationGroup1과 ValidationGroup2라는 인터페이스를 생성한다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidatedRequestDto {
@NotBlank
private String name;
@Email
private String email;
@Pattern(regexp = "01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$")
private String phoneNumber;
@Min(value = 20, groups = ValidationGroup1.class)
@Max(value = 40, groups = ValidationGroup1.class)
int age;
@Size(min = 0, max = 40)
private String description;
@Positive(groups = ValidationGroup2.class)
private int count;
@AssertTrue
private boolean booleanCheck;
}
검증 그룹 설정은 DTO 객체에서 한다.
@RestController
@RequestMapping("/validation")
public class ValidationController {
private final Logger LOGGER = LoggerFactory.getLogger(ValidationController.class);
@PostMapping("/validated")
public ResponseEntity<String> checkValidation(
@Validated @RequestBody ValidatedRequestDto validatedRequestDto){
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validated/group1")
public ResponseEntity<String> checkValidation1(
@Validated(ValidationGroup1.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validated/group2")
public ResponseEntity<String> checkValidation2(
@Validated(ValidationGroup2.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validated/all-group")
public ResponseEntity<String> checkValidation3(
@Validated({ValidationGroup1.class,
ValidationGroup2.class}) @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
}
그 뒤 group 속성을 이용하여, 그룹1, 2를 만들어준다. 이 설정을 통해 어느 그룹에 맞춰 유효성 검사를 실시할 것인지 지정하는 것이다.
다시 Swagger 페이지에서 각 메서드를 호출하겠다. 먼저 7~12번 줄의 checkValidation() 메서드를 호출하겠다. 호출할 때 전달하는 데이터는 다음과 같다.
{
"age": -1,
"booleanCheck": true,
"count": -1,
"description": "Validation 실습 데이터입니다.",
"email": "flature@wikibooks.co.kr",
"name": "Flature",
"phoneNumber": "010-1234-5678"
}
위 데이터는 age와 count 변수에 대한 유효성 검사를 통과하지 못하는 데이터이다.
하지만 첫 번째 메서드를 호출했을 경우 정상적으로 통과하는 것을 볼 수 있다. @Validated 어노테이션에 특정 그룹을 지정하지 않는 경우에는 group 속성을 설정하지 낳은 필드에 대해서만 유효성 검사를 실시하게 된다.
데이터는 그래도 이용하면서 14~19번 줄의 checkValidation1() 메서드를 호출하면 다음과 같은 로그를 확인할 수 있다.
[Field error in object 'validatedRequestDto' on field 'age': rejected value [-1]; codes [Min.validatedRequestDto.age,Min.age,Min.int,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validatedRequestDto.age,age]; arguments []; default message [age],20]; default message [20 이상이어야 합니다]] ]
검사 오류가 발생할 수 있는 두 변수 중에서 ValidationGroup1을 그룹으로 설정한 age에 대한 에러가 발생하는 것을 볼 수 있다. 마찬가지로 21~26번 줄의 checkValidation2() 메서드를 호출하면 age에서는 오류가 발생하지 않고 count에 대한 오류만 발생하게 된다.
그리고 28~35번 줄의 마지막 메서드를 호출하면 다음과 같이 검사 오류가 로그가 출력된다.
com.springboot.valid_exception.controller.ValidationController.checkValidation3(com.springboot.valid_exception.data.dto.ValidatedRequestDto) with 2 errors: [Field error in object 'validatedRequestDto' on field 'count': rejected value [-1]; codes [Positive.validatedRequestDto.count,Positive.count,Positive.int,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validatedRequestDto.count,count]; arguments []; default message [count]]; default message [0보다 커야 합니다]] [Field error in object 'validatedRequestDto' on field 'age': rejected value [-1]; codes [Min.validatedRequestDto.age,Min.age,Min.int,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validatedRequestDto.age,age]; arguments []; default message [age],20]; default message [20 이상이어야 합니다]]
한 번 더 메서드를 호출하겠다. 이번에는 호출 데이터를 다음과 같이 변경한다.
{
"age": 30,
"booleanCheck": false,
"count": 30,
"description": "Validation 실습 데이터입니다.",
"email": "flature@wikibooks.co.kr",
"name": "Flature",
"phoneNumber": "010-1234-5678"
}
위 데이터는 age와 count는 검사를 통과하고 booleanCheck변수에서 검사를 실패하는 데이터이다.
한편 checkValidation3() 메서드를 호출하면 정상적으로 응답이 오는 것을 볼 수 있다. 정리하면 다음과 같다.
- @Validated 어노테이션에 특정 그룹을 설정하지 않은 경우에는 groups가 설정되지 않은 필드에 대해 유효성 검사를 수행
- @Validated 어노테이션에 특정 그룹을 설정하는 경우에는 지정된 그룹으로 설정된 필드에 대해서만 유효성 검사를 수행
이처럼 그룹을 지정해서 유효성 검사를 실시하는 경우에는 어떤 상황에 사용할지를 적절하게 설계해야 의도대로 유효성 검사를 실시할 수 있다. 만략 이를 제대로 설계하지 않으면 비효율적이거나 생산적이지 못한 패턴을 의미하는 안티 패턴이 발생하게 된다.
'개발서적 > IT' 카테고리의 다른 글
[스프링 부트 핵심 가이드] - 09. 연관관계 매핑 (0) | 2023.09.25 |
---|---|
[스프링 부트 핵심 가이드] - 08. 데이터베이스 연동 (0) | 2023.09.18 |
[스프링 부트 핵심 가이드] - 06. 데이터베이스 연동 (0) | 2023.09.10 |
[스프링부트 핵심가이드] - 05. API를 작성하는 다양한 방법 (2) | 2023.09.03 |
[스프링부트 핵심가이드] - 04. 개발 환경 구성 (0) | 2023.09.03 |