| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- 컴퓨터 공학
- JWT
- array
- 컴퓨터 구조
- TestCode
- SpringSecurity
- SpringBoot
- 백준
- Java
- error
- 제로베이스
- 스프링부트
- 프로그래머스
- 서버
- 백엔드스쿨
- QueryDSL
- ORM
- 백엔드
- restTemplate
- actuator
- 핵심가이드
- 백엔드공부
- spring
- queue
- 개발자
- spring boot
- 자료구조
- JPA
- 연관관계
- LinkedList
- Today
- Total
be_better
[Springboot] 유효성 검사와 예외 처리 - 유효성 검사 본문
목차
Background
애플리케이션의 비즈니스 로직이 올바르게 동작하려면 데이터를 사전 검증하는 작업이 필요한데 이것을 유효성 검사 또는 데이터 검증이라 부른다.
일반적인 애플리케이션 유효성 검사의 문제점
일반적으로 사용되는 데이터 검증 로직에는
- 계층별로 진행하는 유효성 검사는 검증 로직이 각 클래스별로 분산되어 있어 관리하기 어렵다.
- 검증 로직에 의외로 중복이 많아 여러 곳에 유사한 기능의 코드가 존재할 수 있다.
- 검증해야할 값이 많다면 검증하는 코드가 길어진다.
등의 문제점들이 있는데 이 같은 문제를 해결하기 위해 Bean Validation 이라는 데이터 유효성 검사 프레임워크를 제공한다.
Bean Validation을 사용한다는 것은 유효성 검사를 위한 로직을 DTO 같은 도메인 모델과 묶어서 각 계층에서 사용하면서 검증 자체를 도메인 모델에 얹는 방식으로 수행한다는 것을 의미한다. 또한 Bean Validation은 어노테이션을 사용한 검증 방식이기 때문에 코드의 간결함도 유지할 수 있다.
Hibernate Validator
Hibernate Validator 는 Bean Validation 명세의 구현체이고 스프링 부트에서는 Hibernate Validator를 유효성 검사 표준으로 채택해서 사용하고 있다.
스프링 부트에서의 유효성 검사
스프링 부트용 유효성 검사 관련 의존성 추가
dependencies 추가
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
스프링 부트의 유효성 검사
유효성 검사는 각 계층으로 데이터가 넘어오는 시점에 해당 데이터에 대한 검사를 실시한다.

ValidRequestDto 클래스
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidRequestDto {
@NotBlank
String name;
@Email
String email;
@Pattern(regexp = "01(?:0|1|[6-9]).[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$")
String phoneNumber;
@Min(value = 20) @Max(value = 40)
int age;
@Size(min = 0, max = 40)
String description;
@Positive
int count;
@AssertTrue
boolean booleanCheck;
}
각 어노테이션은 유효성 검사를 위한 조건을 설정하는데 사용된다.
문자열 검증
- @Null: null 값만 허용한다.
- @NotNull: null을 허용하지 않는다. "", " "는 허용한다.
- @NotEmpty: null, ""을 허용하지 않는다. " "는 허용한다.
- @NotBlank: null, "", " "을 허용하지 않는다.
최대값/최소값 검증
- BigDecimal, BigInteger, int, long 등의 타입을 지원한다.
- @DemicalMax(value = "$numberString"): $numberString보다 작은 값을 허용한다.
- @DemicalMin(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: 이메일 형식을 검사한다. ""는 허용한다.
자릿수 범위 검증
- BigDemical, BigInteger, int, long 등의 타입을 지원한다.
- @Digits(integer = $number1, fraction = $number2): $number1의 정수 자릿수와 $number2의 소수 자릿수를 허용한다.
Boolean 검증
- @AssertTrue: true인지 체크한다. null 값은 체크하지 않는다.
- @AssertFalse: false인지 체크한다. null 값은 체크하지 않는다.
문자열 길이 검증
- @Size(min = $number1, max = $number2): $number1 이상 $number2 이하의 범위를 허용한다.
정규식 검증
- @Pattern(regexp = "$expression"): 정규식을 검사한다. 정규식은 자바의 java.util.regex.Pattern 패키지의 컨벤션을 따른다.
ValidationController 생성
@Slf4j
@RestController
@RequestMapping("/validation")
public class ValidationController {
@PostMapping("/valid")
public ResponseEntity<String> checkValidationByValid(
@Valid @RequestBody ValidRequestDto validRequestDto) {
log.info(validRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK)
.body(validRequestDto.toString());
}
}
checkValidationByValid() 메서드는 ValidRequestDto 객체를 RequestBody 값으로 받고 있는데 @Valid 어노테이션을 지정해야 DTO 객체에 대해 유효성 검사를 수행한다.
postman을 통해 테스트
올바른 request body
{
"age": 30,
"booleanCheck": true,
"count": 1,
"description": "Validation 실습 데이터",
"email": "flature@wieibook.com",
"name": "Flature",
"phoneNumber": "010-0000-0000"
}
정상적인 결과 return
잘못된 request body
{
"age": 10,
"booleanCheck": true,
"count": -1,
"description": "Validation 실습 데이터",
"email": "flature@wieibook.com",
"name": "Flature",
"phoneNumber": "010-0000-0000"
}
서버에 찍힌 로그

출력 결과를 보면 'with 2 error'라는 표현으로 두 요소에서 에러가 발생했다는 것이 명시되어 있고 'default message'로 어느 부분이 잘못됐는지 더욱 쉽게 확인할 수 있다.
@Validated 활용
앞서 말한 @Valid 어노테이션은 자바에서 지원하는 어노테이션이며, 스프링에서는 @Validated라는 별도의 어노테이션으로 유효성 검사를 지원한다. @Validated은 @Valid 어노테이션의 기능을 포함하고 있기 때문에 @Validated로 변경할 수 있다. 또한 @Validated는 유효성 검사를 그룹으로 묶어 대상을 특정할 수 있는 기능이 있다.

새로운 Dto 생성 ValidatedRequestDto.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidatedRequestDto {
@NotBlank
String name;
@Email
String email;
@Pattern(regexp = "01(?:0|1|[6-9]).[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$")
String phoneNumber;
@Min(value = 20, groups = ValidationGroup1.class)
@Max(value = 40, groups = ValidationGroup1.class)
int age;
@Size(min = 0, max = 40)
String description;
@Positive(groups = ValidationGroup2.class)
int count;
@AssertTrue
boolean booleanCheck;
}
각 Validation 에서 group 속성을 사용해 어느 그룹에 맞춰 유효성 검사를 실시할 것인지 ValidationGroup1, ValidationGroup2를 설정할 수 있다.
ValidationController 클래스 수정
@Slf4j
@RestController
@RequestMapping("/validation")
public class ValidationController {
@PostMapping("/validated")
public ResponseEntity<String> checkValidation(
@Validated @RequestBody ValidatedRequestDto validatedRequestDto) {
log.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK)
.body(validatedRequestDto.toString());
}
@PostMapping("/validated/group1")
public ResponseEntity<String> checkValidation1(
@Validated(ValidationGroup1.class)
@RequestBody ValidatedRequestDto validatedRequestDto) {
log.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK)
.body(validatedRequestDto.toString());
}
@PostMapping("/validated/group2")
public ResponseEntity<String> checkValidation2(
@Validated(ValidationGroup2.class)
@RequestBody ValidatedRequestDto validatedRequestDto) {
log.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK)
.body(validatedRequestDto.toString());
}
@PostMapping("/validated/all-group")
public ResponseEntity<String> checkValidation3(
@Validated({ValidationGroup2.class, ValidationGroup2.class})
@RequestBody ValidatedRequestDto validatedRequestDto) {
log.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK)
.body(validatedRequestDto.toString());
}
}
제일 위에 있는 checkValidation() 함수를 테스트 하기위한 Request로
{
"age": -1,
"booleanCheck": true,
"count": -1,
"description": "Validation 실습 데이터",
"email": "flature@wieibook.com",
"name": "Flature",
"phoneNumber": "010-0000-0000"
}
잘못된 Request값을 테스트 해봤지만 @Validated에 그룹을 지정해주지 않은 메소드라서 Validation이 적용되지 않고 에러가 발생하지 않는다.
나머지 메서드들에 대해서는 지정된 그룹에 따라서 각각의 Validation 처리가 진행된다.
이처럼 그룹을 지정해서 유효성 검사를 실시하는 경우에는 어떤 상황에 사용할지를 적절하게 설계해야 의도대로 유효성 검사를 실시할 수 있다. 만약 이를 제대로 설계하지 않으면 비효율적이거나 생산적이지 못한 패턴을 의미하는 안티 패턴이 발생하게 된다.
커스텀 Validation 추가
실무에서는 자바나 스프링이 제공하는 유효성 검사 어노테이션에서 제공하지 않는 기능을 써야 할 때도 있다. 이 경우 ConstraintValidator와 커스텀 어노테이션을 조합에서 별도의 유효성 검사 어노테이션을 생성할 수 있다.
전화번호 형식 일치 어노테이션 생성을 위한 TelephoneValidator 클래스 생성

public class TelephoneValidator implements ConstraintValidator<Telephone, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
return value.matches("01(?:0|1|[6-9]).[.-]?(\\\\d{3}|\\\\d{4})[.-]?(\\\\d{4})$");
}
}
ConstraintValidator 인터페이스의 구현체로 정의한다. 인터페이스를 선언할 때는 어떤 어노테이션 인터페이스인지 타입을 지정해야 한다. 이 로직에서 false가 리턴되면 MethodArgumentNotValidException 예외가 발생한다.
Telephone 어노테이션 인터페이스 생성
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = TelephoneValidator.class)
public @interface Telephone {
String message() default "전화번호 형식이 일치하지 않습니다.";
Class[] group() default {};
Class[] payload() default {};
}
첫째 줄에 @Target 어노테이션은 이 어노테이션을 어디서 선언할 수 있는지 정의하는데 사용된다.
- ElementType.PACKAGE
- ElementType.TYPE
- ElementType.CONSTRUCTOR
- ElementType.FIELD
- ElementType.METHOD
- ElementType.ANNOTATION_TYPE
- ElementType.LOCAL_VARIABLE
- ElementType.PARAMETER
- ElementType.TYPE_PARAMETER
- ElementType.TYPE_USE
2번 줄에 사용된 @Retention 어노테이션은 이 어노테이션이 실제로 적용되고 유지되는 범위를 의미한다.
- RetentionPolicy.RUNTIME: 컴파일 이후에도 JVM에 의해 계속 참조. 리플렉션이나 로깅에 많이 사용되는 정책
- RetentionPolicy.CLASS: 컴파일러가 클래스를 참조할 때까지 유지
- RetentionPolicy.SOURCE: 컴파일 전까지만 유지. 컴파일 이후에는 사라진다.
@Constraint 어노테이션은 TelephoneValidator와 매핑하는 작업을 수행한다.
@Telephone 내부의 항목들은 다음과 같은 의미를 가지고 있다.
- message(): 유효성 검사가 실패할 경우 반환되는 메시지
- groups(): 유효성 검사를 사용하는 그룹으로 설정
- payload(): 사용자가 추가 정보를 위해 전달하는 값
ValidatedRequestDto에 @Telephone 어노테이션 적용
@Telephone
String phoneNumber;
테스트 Request
{
"age": 30,
"booleanCheck": true,
"count": 1,
"description": "Validation 실습 데이터",
"email": "flature@wieibook.com",
"name": "Flature",
"phoneNumber": "1234567"
}
실행 결과 log

'Springboot > 이론' 카테고리의 다른 글
| [Springboot] 액추에이터 활용하기 - 엔드포인트 (0) | 2023.11.29 |
|---|---|
| [Springboot] 유효성 검사와 예외 처리 - 예외 처리 (0) | 2023.11.28 |
| [Springboot] 연관관계 매핑 - 영속성 전이 (0) | 2023.11.19 |
| [Springboot] 연관관계 매핑 - 다대다 매핑 (0) | 2023.11.18 |
| [Springboot] 연관관계 매핑 - 다대일, 일대다 매핑 (0) | 2023.11.17 |