목차
유효성 검사
비즈니스 로직이 올바르게 작동하려면 데이터 사전 검증작업이 필요하다.
이것을 유효성 검사라고 한다.
자바에서는 2009년부터 Bean Validation이라는 데이터 유효성 검사 프레임 워크를 제공한다.
Bean Validation을 사용한다는 것을 유효성 검사를 위한 로직을 도메인 모델과 묶어 각 계층에 사용하겠다는 의미이다.
스프링 부트에서의 유효성 검사
스프링 부트에서는 각 계층으로 데이터가 넘어오는 시점에 데이터 검사를 실시한다.
스프링 부트 프로젝트는 계층간 데이터 전달을 담당하는 DTO 객체를 활용하기에
일반적으로 DTO에서 유효성 검사가 실시된다.
스프링 부트 유효성 검사를 위한 의존성 추가
스프링 부트 유효성 검사 기능은 spring-boot-starter-web에 포함되어있다.
build.gradle에 검사 라이브러리를 의존성으로 추가하면 사용할 수 있다
dependency{
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
유효성 검사 애너테이션
스프링 부트 유효성 검사는 필드에 애너테이션을 붙임으로써 쉽게 수행가능하다
예를 들면 다음과 같이 사용가능하다
public record LoginRequest(
@NotBlank(message = "비밀번호를 입력해주세요") String password,
@Email(message = "아이디를 입력해주세요") String email
) {
}
위의 @NotBlank는 null과 빈값을 허용하지 않는 유효성 검사 애너테이션이다.
위의 @Email은 이메일 형식을 검사하는 애너테이션이다.
그럼 jsr-303과 jsr-380에서 제시하는 Bean Validation Annotation을 살펴보자
유효성 검사 실습
먼저 유효성 검사에 쓸 간단한 Dto 하나를 만들어주었다
public class ValidTestDto {
@NotBlank
String name;
@Email
String email;
@Positive
int number1;
@PositiveOrZero
int number2;
public ValidTestDto(String name, String mail, int nubmer1, int number2) {
this.name = name;
this.mail = mail;
this.nubmer1 = nubmer1;
this.number2 = number2;
}
@Override
public String toString() {
return "ValidTestDto{" +
"name='" + name + '\'' +
", mail='" + mail + '\'' +
", nubmer1=" + nubmer1 +
", number2=" + number2 +
'}';
}
}
각 validation 지정의 의미를 살펴보면 다음과 같다
- name: null, "", " "등이 허용x
- email : email 형식 검사
- number1 : 양수만 허용
- number2 : 양수 or 0만 허용
이제 CheckValidationController에서 @Valid 애너테이션을 활용해 객체가 매핑될 때 유효성 검사를 수행하면 된다.
@Controller
public class ValidationController {
@PostMapping("/")
public ResponseEntity<String> checkValidation(
@RequestBody @Valid ValidTestDto validTestDto) {
return ResponseEntity.ok(validTestDto.toString());
}
}
이제 실제로 유효성검사가 되는지 검증해볼 차례이다.
포스트맨을 통해 number1의 값을 음수로 지정해 post해보자
Bad Requst 에러가 발생한다
또한 애플리케이션에서는 문제가 발생한 지점에 대한 로그가 발생하여 유효성 검사에 실패했음을 알려준다
2024-05-11T16:59:22.597+09:00 WARN 8656 --- [valid_exception] [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<java.lang.String> com.example.valid_exception.controller.ValidationController.checkValidation(com.example.valid_exception.dto.ValidTestDto): [Field error in object 'validTestDto' on field 'nubmer1': rejected value [0]; codes [Positive.validTestDto.nubmer1,Positive.nubmer1,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validTestDto.nubmer1,nubmer1]; arguments []; default message [nubmer1]]; default message [0보다 커야 합니다]] ]
그럼 이번에는 이름도 빈칸으로 만들어 유효성 검사에 실패하는 값을 2개로 설정해보자
2024-05-11T17:02:03.773+09:00 WARN 8656 --- [valid_exception] [nio-8080-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<java.lang.String> com.example.valid_exception.controller.ValidationController.checkValidation(com.example.valid_exception.dto.ValidTestDto) with 2 errors: [Field error in object 'validTestDto' on field 'nubmer1': rejected value [0]; codes [Positive.validTestDto.nubmer1,Positive.nubmer1,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validTestDto.nubmer1,nubmer1]; arguments []; default message [nubmer1]]; default message [0보다 커야 합니다]] [Field error in object 'validTestDto' on field 'name': rejected value []; codes [NotBlank.validTestDto.name,NotBlank.name,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validTestDto.name,name]; arguments []; default message [name]]; default message [공백일 수 없습니다]] ]
2개의 에러가 발생했음을 intellij console에서 log를 통하 알려준다
@Validated 활용
@Valid은 자바에서 지원하는 유효성 검증 어노테이션이다.
스프링도 @Validated 라는 별도의 애너테이션으로 유효성 검사 기능을 제공하는데
@Valid 기능을 포함함과 동시에 그룹으로 묶을 수 있는 기능을 추가적으로 제공한다.
@Validated의 그룹핑 기능을 알아보자
먼저 그룹핑을 위해서는 마커 인터페이스 선언이 필요하다
혹은 서로를 식별할 수 있는 식별자나 클래스 계층이 있다면 이를 활용해도 된다.
마커 인터페이스는 각 그룹의 분류를 위해 선언하는 인터페이스로 어떠한 기능도 제공하지 않는다
그저 분류를 하기 위한 라벨과 같은 역할을 한다
public interface ValidGroup1 {
}
public interface ValidGroup2 {
}
이후 앞선 dto에서 groups 옵션을 통해 유효성 검사 애너테이션을 적용한다.
public class ValidTestDto {
@NotBlank
String name;
@Email
String mail;
@Positive(groups = ValidGroup1.class) // 그룹1로 선언
int nubmer1;
@PositiveOrZero(groups = ValidGroup2.class) // 그룹2로 선언
int number2;
...
}
이제 @Validated 애너테이션을 통해 선택적으로 유효성 검사가 가능하다
먼저 Controller에 각 검증 상황을 살펴보자
@Controller
public class ValidationController {
// 그룹 지정 안함 => groups 속성을 지정하지 않은 필드만 검증
@PostMapping("/validation")
public ResponseEntity<String> checkValidationByValidated (
@RequestBody @Validated ValidTestDto validTestDto) {
return ResponseEntity.ok(validTestDto.toString());
}
// 그룹1만 검증
@PostMapping("/validation/group1")
public ResponseEntity<String> checkValidationGroup1 (
@RequestBody @Validated(ValidGroup1.class) ValidTestDto validTestDto) {
return ResponseEntity.ok(validTestDto.toString());
}
// 그룹2만 검증
@PostMapping("/validation/group2")
public ResponseEntity<String> checkValidationGroup2 (
@RequestBody @Validated(ValidGroup2.class) ValidTestDto validTestDto) {
return ResponseEntity.ok(validTestDto.toString());
}
// 그룹 1,2 모두 검증
@PostMapping("/validation/all-group")
public ResponseEntity<String> checkValidationAllGroup (
@RequestBody
@Validated({ValidGroup1.class, ValidGroup2.class}) ValidTestDto validTestDto) {
return ResponseEntity.ok(validTestDto.toString());
}
}
먼저 결론부터 이야기하면 다음과 같다.
- 그룹 지정 안할 시 => groups 속성이 없는 필드에 대해서만 유효성 검사
- 그룹 지정 시 => 해당 그룹에 대해서만 유효성 검사
예를 들어 다음과 같이 group1인 number1과 group2인 number2를 모두 음수로 설정했다고 해보자
number1은 @Positive, number2는 @PositiveOrZero를 사용하여 validation 했기에 둘다 유효하지 않은 입력값이다.
먼저 그룹을 지정하지 않고 validation을 호출한다면
number1과 number2 검증대상에 포함되지 않은 것을 볼 수 있다
어떤 그룹도 지정해주지 않았으므로 groups 속성이 적용되지 않은 name, email에 대해서만 검증이 수행된 것이다.
이에 반해 group1 validation이 지정된 경로로 post를 호출한다면
group1에 속한 number1만 검증하게 되는데 유효하지 않으므로 bad request를 반환한다
이는 다른 말로 group1에 속한 number1만 유효하다면 유효성 검사를 통과한다는 것을 의미한다.
group2 validation을 지정한 곳으로 요청을 보낸다면 당연히 group2만 유효성검사가 이루어짐을 유추할 수 있다.
마지막으로 group1과 group2를 모두 지정한 validation 경로로 post를 보내본다면
group1과 group2의 유효성 검사가 모두 수행되게 된다.
커스텀 Validation 추가
마지막으로 유효성 검증을 커스터마이징하여 애너테이션으로 등록하는 법을 알아보자
방법은 간단하다
ConstraintValidator와 커스텀 어노테이션을 조합하여 별도의 유효성 검사 어노테이션 선언이 가능하다
이번에는 사용자 이름이 "brocoli"로 시작되어야 한다는 검증 애너테이션을 만든다고 가정하자
step1 : 커스텀 어노테이션 제작
Brocoli 애노테이션 하나를 커스터마이징하여 만들어주자
다음 과정은 Validation에 핵심적인 부분은 아니므로 우선 따라가고 애너테이션에 대한 심화된 이해가 필요한 독자는 다음 링크를 참고하길 바란다.
@Target(ElementType.FIELD) // 이 어노테이션을 어디에 선언할 수 있는지 설정
@Retention(RetentionPolicy.RUNTIME) // 어노테이션의 유지범위
public @interface Brocoli {
}
step2) 유효성 검증 로직이 포함된 ConstraintValidator 제작
public class BrocoliValidator implements ConstraintValidator<Brocoli, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return false;
}
return value.matches("^brocoli.*$"); // "brocoli"로 시작하는 지 검증
}
}
step3) 커스텀 애너테이션에 ConstraintValidator 등록
@Target(ElementType.FIELD) // 이 어노테이션을 어디에 선언할 수 있는지 설정
@Retention(RetentionPolicy.RUNTIME) // 어노테이션의 유지범위
@Constraint(validatedBy = BrocoliValidator.class) // ConstraintValidator 등록
public @interface Brocoli {
String message() default "{이름은 브로콜리로 시작해야 합니다.}"; // 검증에 실패할 경우 반환할 메시지
Class [] groups() default {}; //유효성 검사를 사용하는 그룹으로 설정
Class [] payload() default {}; // 사용자가 추가정보를 위해 전달하는 값
}
이제 애너테이션을 적용해보자
public class ValidTestDto {
@Brocoli
String name;
@Email
String email;
@Positive(groups = ValidGroup1.class) // 그룹1로 선언
int number1;
@PositiveOrZero(groups = ValidGroup2.class) // 그룹2로 선언
int number2;
...
}
postman을 활용하여 유효하지 않은 이름인 "홍길동"으로 요청해보았다
- MethodArgumentNotValid Exeption이 터지며 유효성 검사에 성공했다.
다시 brocoli로 보내면 유효성 검사에 통과한 모습을 볼 수 있다.
개인적으로 반복적으로 이 방법이 반복적으로 적용되는 정규표현식 설정이나
검증 로직 변경 사항이 많은 경우, 클라이언트 코드에는 애너테이션으로 일치화시키며 관리포인트를 줄일 수 있을 것 같다.
예제 코드 링크
'우테코' 카테고리의 다른 글
[Rest Docs vs Swagger] 2편 : Swagger Spring docs적용기 (0) | 2024.06.16 |
---|---|
[Rest Docs vs Swagger] 1편 : Rest Docs로 API 문서 자동화해보기 (0) | 2024.06.13 |
[Spring] 웹 요청 - 응답 과정 (2) | 2024.04.24 |
우테코 4주차- [크리스마스 이벤트] 회고 (0) | 2023.11.15 |
우테코 4주차 - [크리스마스 이벤트] 프로그래밍 요구사항 및 기능목록 작성 (0) | 2023.11.15 |