[스프링 2팀] 10~11장. 유효성 검사와 예외처리 & 액추에이터 활용하기

10장. 유효성 검사와 예외처리
애플리케이션의 비즈니스 로직이 올바르게 동작하려면 데이터를 사전 검증하는 작업이 필요함
=> 유효성 검사 or 데이터 검증이라고 부름
10.1 일반적인 애플리케이션 유효성 검사의 문제점
일반적으로 사용되는 데이터 검증 로직에는 몇 가지 문제점 존재
=> 계층별로 진행하는 유효성 검사는 검증 로직이 각 클래스별로 분산되어 있어 관리하기 어려움
& 검증 로직에 의외로 중복이 많아 여러 곳에 유사한 기능의 코드가 존재 가능
& 검증해야 할 값이 많다면 검증하는 코드가 길어짐
=> 코드가 복잡해지고 가독성 떨어짐
문제 해결을 위해 자바 진영에서는 2009년부터 Bean Validation이라는 데이터 유효성 검사 프레임워크 제공
Bean Validation=> 어노테이션을 통해 다양한 데이터를 검증하는 기능 제공
Bean Validation을 사용한다는 것은 유효성 검사를 위한 로직을 DTO 같은 도메인 모델과 묶어서 각 계층에서 사용하면서 검증 자체를 도메인 모델에 얹는 방식으로 수행한다는 의미임
& Bean Validation은 어노테이션을 사용한 검증 방식이기 때문에 코드의 간결함도 유지 가능함
10.2 Hibernate Validator
Hibernate Validator=> Bean Validation 명세의 구현체임
스프링 부트에서는 Hibernate Validator를 유효성 검사 표준으로 채택해서 사용 중임
Hibernate Validator는 JSR-303 명세의 구현체로서 도메인 모델에서 어노테이션을 통한 필드값 검증을 가능하게 도와줌
10.3 스프링 부트에서의 유효성 검사
애플리케이션에 유효성 검사 기능 추가
프로젝트 생성
- groupId: com.springboot
- artifactId: valid_exception
- name: valid_exception
- Developer Tools: Lombok, Spring Configuration Processor
- Web: Spring Web
- SQL: Spring Data JPA, MariaDB Driver
+ 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;
}
코드를 보면 각 필드에 어노테이션이 선언된 것을 볼 수 있음
각 어노테이션은 유효성 검사를 위한 조건을 설정하는 데 사용됨
대표적인 어노테이션(추려서 작성함)
문자열 검증
- @Null: null 값만 허용함
- @NotEmpty: null, ""을 허용하지 않음, " "는 허용
최댓값/최솟값 검증
- BigDecimal, BigInteger, int, long 등의 타입 지원
- @DemicalMax(value = "$numberString"): $numberString보다 작은 값 허용
값의 범위 검증
- BigDecimal, BigInteger, int, long 등의 타입 지원
- @Positive: 양수를 허용함
- @PositiveOrZero: 0을 포함한 양수를 허용함
시간에 대한 검증
- Date, LocalDate, LocalDateTime 등의 타입 지원
- @Future: 현재보다 미래의 날짜 허용
이메일 검증
- @Email: 이메일 형식 검사, ""는 허용
자릿수 범위 검증
- BigDecimal, BigInteger, int, long 등의 타입 지원
- @Digits(integer = $number1, fraction = $number2): $number1의 정수 자릿수와 $number2의 소수 자릿수 허용
Boolean 검증
- @AssertTrue: true인지 체크, null 값은 체크 x
- @AssertFalse: false인지 체크, null 값은 체크 x
문자열 길이 검증
- @Size(min = $number1, max = $number2): $number1 이상 $number2 이하의 범위 허용
정규식 검증
- @Pattern(regexp = "$expression"): 정규식 검사, 정규식은 자바의 java.util.regex.Pattern 패키지의 컨벤션을 따름
유효성 검사에 사용하는 어노테이션은 인텔리제이 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 줄에서는 두 그룹을 모두 지정함
- @Validated 어노테이션에 특정 그룹을 설정하지 않은 경우에는 groups가 설정되지 않은 필드에 대해 유효성 검사 수행
- @Validated 어노테이션에 특정 그룹을 설정하는 경우에는 지정된 그룹으로 설정된 필드에 대해서만 유효성 검사 수행
=> 그룹을 지정해서 유효성 검사를 실시하는 경우에는 어떤 상황에 사용할지를 적절하게 설계해야 의도대로 유효성 검사 실시 가능, 만약 이를 제대로 설계 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를 통해 지정
지정 가능한 항목
- RetentionPolicy.RUNTIME : 컴파일 이후에도 JVM에 의해 계속 참조, 리플렉션이나 로깅에 많이 사용되는 정책임
- RetentionPolicy.CLASS : 컴파일러가 클래스를 참조할 때까지 유지
- RetentionPolicy.SOURCE : 컴파일 전까지만 유지, 컴파일 이후에는 사라짐
10.4 예외 처리
애플리케이션 개발 시 발생하는 오류-> 자바에서는 try/catch, throw 구문을 활용해 처리함
스프링 부트에서는 더욱 편리하게 예외 처리를 할 수 있는 기능 제공
예외=> 입력 값의 처리가 불가능하거나 참조된 값이 잘못된 경우 등 애플리케이션이 정상적으로 동작하지 못하는 상황
에러=> 주로 자바의 가상머신에서 발생시키는 것, 예외와 달리 애플리케이션 코드에서 처리할 수 있는 것이 거의 없음

모든 예외 클래스는 Throwable 클래스를 상속받음-> 크게 Checked Exception & Unchecked Exception 으로 구분 가능
Checked Exception=> 컴파일 단계에서 확인 가능한 예외 상황
Unchecked Exception=> 런타임 단계에서 확인되는 예외 상황
예외가 발생했을 때 처리하는 방법
- 예외 복구: 예외 상황을 파악해서 문제를 해결하는 방식
- 예외 처리 회피: 예외가 발생한 시점에서 바로 처리하는 것이 아니라 예외가 발생한 메서드를 호출한 곳에서 에러 처리를 할 수 있게 전가하는 방식
- 예외 전환: 예외 복구 + 예외 처리 회피 방식을 적절하게 섞은 방식
스프링 부트의 예외 처리 방식
- @(Rest)ControllerAdvice와 @ExceptionHandler를 통해 모든 컨트롤러의 예외를 처리
- @ExceptionHandler를 통해 특정 컨트롤러의 예외 처리
커스텀 예외
커스텀 예외를 만들어서 사용하면 네이밍에 개발자 의도를 담을 수 있음-> 이름만으로도 어느 정도 예외 상황 짐작 가능
표준 예외를 사용할 때는 예외 메시지를 상세히 작성해야 하는 번거로움이 있음
커스텀 예외를 사용하면 애플리케이션에서 발생하는 예외를 개발자가 직접 관리하기 수월해짐
+ 예외 상황에 대한 처리 용이
11. 액추에이터 활용하기
11.1 프로젝트 생성 및 액추에이터 종속성 추가
프로젝트 생성
- groupId: com.springboot
- artifactId: actuator
- name: actuator
- Developer Tools: Spring Configuration Processor
- Web: Spring Web
+ 이전 장의 SwaggerConfiguration 클래스를 가져오고 의존성 추가
+ spring-boot-starter-actuator 모듈의 종속성 추가
<dependencies>
//생략
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
//생략
</dependencies>
11.2 엔드포인트
액추에이터의 엔드포인트=> 애플리케이션의 모니터링을 사용하는 경로
스프링 부트에는 여러 내장 엔드포인트 포함 & 커스텀 엔드포인트도 추가 가능
자주 활용되는 액추에이터의 엔드포인트(추려서 작성함)
- auditevents: 호출된 Audit 이벤트 정보 표시, AuditEventRepository 빈이 필요
- configprops: @ConfigurationProperties의 속성 리스트를 표시함
- httptrace: 가장 최근에 이뤄진 100건의 요청 기록을 표시함, HttpTraceRepository 빈이 필요함
- metrics: 애플리케이션의 메트릭 정보를 표시함
- sessions: 스프링 세션 저장소에서 사용자의 세션을 검색하고 삭제 가능, 스프링 세션을 사용하는 서블릿 기반 웹 애플리케이션이 필요함
- threaddump: 스레드 덤프를 수행함
11.3 액추에이터 기능 살펴보기
- 액추에이터의 /info 엔드포인트를 활용하면: 가동 중인 애플리케이션의 정보를 볼 수 있음
- 액추에이터의 /health 엔드포인트를 활용하면: 애플리케이션의 상태를 확인할 수 있음
- 액추에이터의 /beans 엔드포인트를 활용하면: 스프링 컨테이너에 등록된 스프링 빈의 전체 목록을 표시 가능
- 액추에이터의 /conditions 엔드포인트를 활용하면: 스프링 부트의 자동설정(AutoConfiguration) 조건 내역 확인 가능
- 액추에이터의 /env 엔드포인트를 활용하면: 스프링의 환경변수 정보 확인 가능
- 액추에이터의 /loggers 엔드포인트를 활용하면: 애플리케이션의 로깅 레벨 수준이 어떻게 설정돼 있는지 확인 가능
QUIZ
1. 예외 처리 방법 중 예외가 발생한 시점에서 바로 처리하는 것이 아니라 예외가 발생한 메서드를 호출한 곳에서 에러 처리를 할 수 있게 전가하는 방식은 ()이다.
답: 예외 처리 회피
2. 자주 활용되는 액추에이터의 엔드포인트 중 @ConfigurationProperties의 속성 리스트를 표시하는 것은 ()이다.
답: configprops
3. 애플리케이션의 비즈니스 로직이 올바르게 동작하기 위해 데이터를 사전 검증하는 작업은 ()이다.
답: 유효성 검사 또는 데이터 검증
4. 스프링 부트 유효성 검사에 사용되는 대표적인 어노테이션 중 최댓값/최솟값 검증에 쓰이고 $numberString 보다 큰 값을 허용하는 어노테이션은 ()이다.
답: @DemicalMin(value = "$numberString")
5. 스프링 부트 유효성 검사에 사용되는 대표적인 어노테이션 중 값의 범위 검증에 쓰이고 양수를 허용하는 어노테이션은 ()이다.
답: @Positive
6. @Retention 어노테이션이 지정 가능한 RetentionPolicy 항목 중 컴파일러가 클래스를 참조할 때까지 유지하는 것은 ()이다.
답: RetentionPolicy.CLASS
7. Bean Validation 명세의 구현체이며 스프링 부트에서 유효성 검사 표준으로 채택해서 사용 중인 것은 ()이다.
답: Hibernate Validator
코드 QUIZ
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장