애플리케이션의 비즈니스 로직이 올바르게 동작하려면 데이터를 사전 검증하는 작업이 필요함
=> 유효성 검사 or 데이터 검증이라고 부름
일반적으로 사용되는 데이터 검증 로직에는 몇 가지 문제점 존재
=> 계층별로 진행하는 유효성 검사는 검증 로직이 각 클래스별로 분산되어 있어 관리하기 어려움
& 검증 로직에 의외로 중복이 많아 여러 곳에 유사한 기능의 코드가 존재 가능
& 검증해야 할 값이 많다면 검증하는 코드가 길어짐
=> 코드가 복잡해지고 가독성 떨어짐
문제 해결을 위해 자바 진영에서는 2009년부터 Bean Validation이라는 데이터 유효성 검사 프레임워크 제공
Bean Validation=> 어노테이션을 통해 다양한 데이터를 검증하는 기능 제공
Bean Validation을 사용한다는 것은 유효성 검사를 위한 로직을 DTO 같은 도메인 모델과 묶어서 각 계층에서 사용하면서 검증 자체를 도메인 모델에 얹는 방식으로 수행한다는 의미임
& Bean Validation은 어노테이션을 사용한 검증 방식이기 때문에 코드의 간결함도 유지 가능함
Hibernate Validator=> Bean Validation 명세의 구현체임
스프링 부트에서는 Hibernate Validator를 유효성 검사 표준으로 채택해서 사용 중임
Hibernate Validator는 JSR-303 명세의 구현체로서 도메인 모델에서 어노테이션을 통한 필드값 검증을 가능하게 도와줌
애플리케이션에 유효성 검사 기능 추가
프로젝트 생성
+ 7장에서 자바 파일을 가져와 기본적인 프로젝트 생성
+ 5장에서 다룬 것처럼 SwaggerConfiguration 클래스 파일 생성 & 관련 의존성을 pom.xml에 추가
원래 스트링 부트의 유효성 검사 기능은 spring-boot-starter-web 에 포함돼 있었음
but 스프링 부트 2.3 버전 이후로 별도의 라이브러리로 제공 중임
pom.xml 파일에 유효성 검사 라이브러리를 의존성으로 추가하면 사용 가능함
<dependencies>
//생략
<dependency>
<groupId>org.springframework.boot</groupId>
</dependency>
//생략
</dependencies>
유효성 검사는 각 계층으로 데이터가 넘어오는 시점에 해당 데이터에 대한 검사를 실시함
스프링 부트 프로젝트에서는 계층 간 데이터 전송에 대체로 DTO 객체를 활용하고 있기 때문에 아래와 같이 유효성 검사를 DTO 객체를 대상으로 수행하는 것이 일반적임
DTO와 컨트롤러 생성
ValidRequestDto 라는 이름의 DTO 객체 생성
@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)
String description;
@Positive
int count;
@AsserTrue
boolean booleanCheck;
}
코드를 보면 각 필드에 어노테이션이 선언된 것을 볼 수 있음
각 어노테이션은 유효성 검사를 위한 조건을 설정하는 데 사용됨
대표적인 어노테이션(추려서 작성함)
문자열 검증
최댓값/최솟값 검증
값의 범위 검증
시간에 대한 검증
이메일 검증
자릿수 범위 검증
Boolean 검증
문자열 길이 검증
정규식 검증
유효성 검사에 사용하는 어노테이션은 인텔리제이 IDEA에서도 확인 가능
화면 우측의 [Bean Validation] 탭을 선택하면 목록 확인 가능함
인텔리제이 IDEA에는 자동으로 우측에 [Bean Validation] 탭을 추가하는 기능이 있는데, 만약 화면에서 이를 확인할 수 없다면 메뉴에서 [View]-> [Tool Windows]-> [Bean Validation]을 차례로 선택해 탭 추가 가능
DTO를 사용하는 컨트롤러 객체 생성- ValidationController 생성
@RestController
@RequestMapping("/validation")
public class ValidationController {
private final Logger LOGGER = LoggerFactor.getLogger(ValidationController.class);
@PostMapping("/valid")
public ReponseEntity<String> checkValidationByValid(
@Valid @RequestBody ValidRequestDto validRequestDto) {
LOGGER.info(validRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validRequestDto.toString());
}
}
checkValidationByValid() 메서드는 ValidRequestDto 객체를 RequestBody 값으로 받고 있음
이 경우 @Valid 어노테이션을 지정해야 DTO 객체에 대해 유효성 검사를 수행함
동작을 확인하기 위해 애플리케이션 실행-> Swagger 페이지에 접속
checkValidationByValid() 메서드를 호출하기 위해 아래와 같이 입력
{
"age": 30,
"booleanCheck": true,
"description": "Validation 실습 데이터입니다.",
"email": "flature@wikibooks.co.kr",
"name": "Flature",
"phoneNumber": "010-1234-5678"
}
위의 값은 유효성 검사를 통과할 수 있는 값들임
age: @Min(value=20), @Max(value=40) 데이터만 받겠다는 것을 의미
booleanCheck: @AssertTrue를 통해 값이 true인지 체크
count: @Positive가 설정됨-> 0이 아닌 양수가 값으로 들어오는지 체크
description: @Size를 통해 문자열의 길이 제한
email: 값에 '@' 문자가 있는지 확인
name: @NotBlank로 null 값이나 "", " " 모두 허용하지 않게 설정해서 값을 의무적으로 받도록 설정함
phoneNumber: @Pattern을 통해 정규식 설정, regexp 속성의 값을 "01(?:0|1|[6-9]).-]?(\\d{3}|\\d{4}[.-]?(\\d{4})$"로 설정하면 휴대전화 번호 형식인지 검증 가능
@Valid 어노테이션=> 자바에서 지원하는 어노테이션, 스프링도 @Validated라는 별도의 어노테이션으로 유효성 검사 지원함
@Validated=> @Valid 어노테이션의 기능을 포함하고 있기 때문에 @Validated로 변경 가능
@Validated=> 유효성 검사를 그룹으로 묶어 대상을 특정할 수 있는 기능이 있음
검증 그룹은 별다른 내용이 없는 마커 인터페이스를 생성해서 사용함
data 패키지 내에 group 패키지 생성-> ValidationGroup1 & ValidationGroup2 인터페이스 생성
두 인터페이스 모두 내부 코드 x, 인터페이스만 생성해서 그룹화하는 용도로 사용함
ValidationGroup1 인터페이스
public interface ValidationGroup1{
}
ValidationGroup2 인터페이스
public interface ValidationGroup1{
}
검증 그룹 설정은 DTO 객체에서 함, 새로운 DTO 객체 생성
@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, group=ValidationGroup1.class)
@Max(value=40, group=ValidationGroup1.class)
private int age;
@Size(min=0, max=40)
private String description;
@Positive(groups=ValidationGroup2.class)
private int count;
@AsserTrue
private boolean booleanCheck;
}
17~18번 줄에서 @Min, @Max 어노테이션의 groups 속성을 사용해 ValidationGroup1 그룹 설정 & 24번째 줄에서 ValidationGroup2 설정 => 어느 그룹에 맞춰 유효성 검사를 실시할 것인지 지정함
실제로 그룹을 어떻게 설정해서 유효성 검사를 실시할지 결정하는 것은 @Validated 어노테이션에서 함
유효성 검사 그룹을 설정하기 위해 컨트롤러 클래스에 메서드 추가
@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> checkValidation(
@Validated(ValidationGroup1.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validated/group2")
public ResponseEntity<String> checkValidation(
@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());
}
}
9번째 줄에서는 @Validated 어노테이션이 속성을 지정하지 않고 16번 주에서는 ValidationGroup1, 23번 줄에서는 ValidationGroup2를 그룹을 지정함, 30~31 줄에서는 두 그룹을 모두 지정함
=> 그룹을 지정해서 유효성 검사를 실시하는 경우에는 어떤 상황에 사용할지를 적절하게 설계해야 의도대로 유효성 검사 실시 가능, 만약 이를 제대로 설계 x-> 비효율적이거나 생산적이지 못한 패턴을 의미하는 안티 패턴이 발생하게 됨
전화번호 형식이 일치하는지 확인하는 간단한 유효성 검사 어노테이션 생성
ConstraintValidator 인터페이스를 구현하는 클래스 생성해야 함-> TelephoneValidator 클래스 생성
public class TelephonValidator 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 인터페이스에서 정의한 Telephone 인터페이스
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = TelephoneValidator.class)
public @interface Telephone {
String message() default "전화번호 형식이 일치하지 않습니다.";
Class[] groups() default {};
Class[] payload() default {};
}
@Retention 어노테이션은 이 어노테이션이 실제로 적용되고 유지되는 범위를 의미함
@Retention의 적용 범위는 RetentionPolicy를 통해 지정
지정 가능한 항목
애플리케이션 개발 시 발생하는 오류-> 자바에서는 try/catch, throw 구문을 활용해 처리함
스프링 부트에서는 더욱 편리하게 예외 처리를 할 수 있는 기능 제공
예외=> 입력 값의 처리가 불가능하거나 참조된 값이 잘못된 경우 등 애플리케이션이 정상적으로 동작하지 못하는 상황
에러=> 주로 자바의 가상머신에서 발생시키는 것, 예외와 달리 애플리케이션 코드에서 처리할 수 있는 것이 거의 없음
모든 예외 클래스는 Throwable 클래스를 상속받음-> 크게 Checked Exception & Unchecked Exception 으로 구분 가능
Checked Exception=> 컴파일 단계에서 확인 가능한 예외 상황
Unchecked Exception=> 런타임 단계에서 확인되는 예외 상황
예외가 발생했을 때 처리하는 방법
스프링 부트의 예외 처리 방식
커스텀 예외
커스텀 예외를 만들어서 사용하면 네이밍에 개발자 의도를 담을 수 있음-> 이름만으로도 어느 정도 예외 상황 짐작 가능
표준 예외를 사용할 때는 예외 메시지를 상세히 작성해야 하는 번거로움이 있음
커스텀 예외를 사용하면 애플리케이션에서 발생하는 예외를 개발자가 직접 관리하기 수월해짐
+ 예외 상황에 대한 처리 용이
프로젝트 생성
+ 이전 장의 SwaggerConfiguration 클래스를 가져오고 의존성 추가
+ spring-boot-starter-actuator 모듈의 종속성 추가
<dependencies>
//생략
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
//생략
</dependencies>
액추에이터의 엔드포인트=> 애플리케이션의 모니터링을 사용하는 경로
스프링 부트에는 여러 내장 엔드포인트 포함 & 커스텀 엔드포인트도 추가 가능
자주 활용되는 액추에이터의 엔드포인트(추려서 작성함)
1. 예외 처리 방법 중 예외가 발생한 시점에서 바로 처리하는 것이 아니라 예외가 발생한 메서드를 호출한 곳에서 에러 처리를 할 수 있게 전가하는 방식은 ()이다.
답: 예외 처리 회피
2. 자주 활용되는 액추에이터의 엔드포인트 중 @ConfigurationProperties의 속성 리스트를 표시하는 것은 ()이다.
답: configprops
3. 애플리케이션의 비즈니스 로직이 올바르게 동작하기 위해 데이터를 사전 검증하는 작업은 ()이다.
답: 유효성 검사 또는 데이터 검증
4. 스프링 부트 유효성 검사에 사용되는 대표적인 어노테이션 중 최댓값/최솟값 검증에 쓰이고 $numberString 보다 큰 값을 허용하는 어노테이션은 ()이다.
답: @DemicalMin(value = "$numberString")
5. 스프링 부트 유효성 검사에 사용되는 대표적인 어노테이션 중 값의 범위 검증에 쓰이고 양수를 허용하는 어노테이션은 ()이다.
답: @Positive
6. @Retention 어노테이션이 지정 가능한 RetentionPolicy 항목 중 컴파일러가 클래스를 참조할 때까지 유지하는 것은 ()이다.
답: RetentionPolicy.CLASS
7. Bean Validation 명세의 구현체이며 스프링 부트에서 유효성 검사 표준으로 채택해서 사용 중인 것은 ()이다.
답: Hibernate Validator
1. ValidRequestDto 객체를 생성하는 코드이다. 코드의 '1-1', '1-2' 부분을 채우시오.
//생략
public class ValidRequestDto {
@NotBlank
String name;
@Email
String email;
@Pattern(regexp = "01(?:0|1|[6-9]).-]?(\\d{3}|\\d{4}[.-]?(\\d{4})$")
String phoneNumber;
**1-1**(value = 20) **1-2**(value = 40)
String description;
@Positive
int count;
@AsserTrue
boolean booleanCheck;
}
답: 1-1 @Min, 1-2 @Max
2. 유효성 검사 그룹을 설정하기 위해 컨트롤러 클래스에 메서드를 추가한 코드이다. 코드의 '*****' 부분에 들어갈 어노테이션을 작성하시오.
@RestController
@RequestMapping("/validation")
public class ValidationController {
private final Logger LOGGER = LoggerFactory.getLogger(ValidationController.class);
@PostMapping("/validated")
public ResponseEntity<String> checkValidation(
@***** @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validated/group1")
public ResponseEntity<String> checkValidation(
@*****(ValidationGroup1.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validated/group2")
public ResponseEntity<String> checkValidation(
@*****(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(
@*****({ValidationGroup1.class, ValidationGroup2.class}) @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
}
답: Validated
출처: 스프링부트 핵심 가이드 10장~ 11장
[스프링 2팀] 9장. 연관관계 매핑 (0) | 2025.01.10 |
---|---|
[스프링 2팀] 8장. Spring Data JPA 활용 (0) | 2025.01.03 |
[스프링 2] 7장. 테스트코드 작성하기 (0) | 2024.12.27 |
[스프링 2] 6장. 데이터베이스 연동 (0) | 2024.11.29 |
[스프링 2팀]5~6장. API를 작성하는 다양한 방법 & 데이터베이스 연동 (0) | 2024.11.22 |