출처: 스프링 부트 핵심 가이드 - 장정우 지음
https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=296591989
유효성 검사 (데이터 검증)
- 어플리케이션 비즈니스 로직이 제대로 동작하도록 데이터를 사전에 검증하는 작업
- 여러 계층에서 들어오는 데이터에 대해 의도한 형식대로 값이 들오는지 체크하는 과정
- 자바에서 가장 신경써야 하는 것 중 하나인 NullPointerException 예외 처리!
일반적인 어플리케이션 유효성 검사의 문제점
- 계층별로 진행하는 유효성 검사는 검증 로직이 각 클래스 별로 분산되어 있어 관리하기 어렵다.
- 검증 로깆에 중복이 많아 여러곳에 유사한 기능의 코드가 존재할 수 있다.
- 검증해야할 값이 많다면 검증 코드가 길어진다.(코드가 복잡해지고 가독성이 저하된다.)
➡️ 이를 해결하기 이해 자바 진영에서는 Bean Valiation이라는 데이터 유효성 검사 프레임워크 제공
Bean Validation
- 어노테이션을 통한 다양한 데이터 검증기능 제공
- 유효성 검사를 위한 로직을 DTO 같은 도메인 모델과 묶어서 각 계층에서 사용하면서 검증 자체를 도메인 모델에 얹는 방식으로 수행
- 어노테이션을 사용한 검증방식이므로 코드의 간결함 유지!
스프링 부트에서의 유효성 검사
Hibernate Validator
- Bean Validation의 구현체인 Hibernate Validator를 표준으로 사용!
스프링 부트용 유효성 검사 관련 의존성 추가
- 원래 스프링붙의 유효성 검사 기능은 spring-boot-starte-web에 포함되어 있었음
- 스프링 부트 2.3버전 이후로 별도록 spring-boot-starter-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은 체크하지 않음
- @AssertFalse : false인지 체크, null은 체크하지 않음.
-
문자열 길이 검증
- @Size(min = $number1, max = $number2) : $number1 이상, $number2 이하의 범위를 허용
-
정규식 검증
- @Pattern(regexp = "$expression") : 정규식 검사
<정규식 요소>
- ^ : 문자열의 시작
- $ : 문자열의 종료
- . : 임의의 한 문자
- * : 앞문자가 하나 이상
- ? : 앞문자가 없거나 하나 존재
- [,] : 문자의 집합이나 범위, 두문자의 사이는 '-' 기호로 범위 표현
- {,} : 횟수 또는 범위
- (,) : 괄호안의 문자를 하나의 문자로 인식
- | : 패턴 안에서 OR 연산 수행
- \ : 정규식에서 역슬래시는 확장문자로 취급, 역슬래시 다음 특수문자는 문자로 인식
- \b : 단어의 경계
- \B : 단어가 아닌 것에 대한 경계
- \A : 입력의 시작 부분
- \G : 이전 매치의 끝
- \Z : 종결자가 있는 경우 입력의 끝
- \z : 입력의 끝
- \s : 공백 문자
- \S : 공백문자가 아닌 나무지 문자 (^\s와 동일)
- \w : 알파벳이나 숫자
- \W : 알파벳이나 숫자가 아닌 문자(^\w와 동일)
- \d : 숫자 [0-9]와 동일하게 취급
- \D : 숫자를 제외한 모든 문자(^0-9와 동일)
@Valid
- 일반적으로 컨트롤러에서 유효성 검사를 하는 DTO를 @RequestBody로 받는 인자에 붙임
- @Valid 어노테이션을 지정해야 DTO객체에 대해 유효성 검사를 수행함!
@Validated 활용
-
@Valid는 자바에서 지원하는 어노테이션
-
스프링도 @Validated라는 별도의 어노테이션으로 유효성 검사 지원!!
-
@Validated는 @Valid 기능을 포함하므로 @Validated로 변경 가능!
-
@BValidated는 리스트로 여러 그룹 지정 가능!
-
@Validated는 유효성 검사를 그룹으로 묶어 대상을 특정하 할 수 있는 기능이 있음!
- DTO에서 어노테이션을 붙여 유효성 검사를 할때 groups 속성을 사용해 어느 그룹에 맞춰 유효성 검사를 실시할지 지정 가능!
- ex)
@Min(value=20, groups = ValidationGrop1.class)
- 컨트롤러에서
커스텀 Validation 추가
- ConstraintValidator와 커스텀 어노테이션을 조합하여 별도의 유효성 검사 어노테이션 생성하는 방법!
- 동일한 정규식을 계속 쓰는 @Pattern 어노테이션의 경우가 가장 흔한 예시임~!
<전화번호 형식이 일치하는지 확인하는 유효성 검사 어노테이션 예시>

1. TelephoneValidator 클래스를 ContraintValidator 인터페이스의 구현체로 정의
2. 인터페이스 선언 시 어노테이션 인터페이스 타입 정의
3. isValid() 메소드 재정의
4. false가 리턴 되면 MethodArgumentNotValid 예외 발생~!
-
어노테이션 인터페이스 타입으로 지정해준 Telephone 인터페이스

- @Target : 이 어노테이션을 어디서 선언할 수 있는지 정의 (여기선 필드에서 선언 가능하도록 설정함)
- EntityType.PACKAGE,
- EntityType.TYPE,
- EntityType.CONTRUCTOR,
- EntityType.FIELD,
- EntityType.METHOD,
- EntityType.ANNOTAION_TYPE,
- EntityType.LOCAL_VARIABLE,
- EntityType.PARAMETER,
- EntityType.TYPE_PARAMETER,
- EntityType.TYPE_USE,
- @Retention : 이 어노테이션이 실제로 적용되고 유지되는 범위
- RetentionPolicy.RUNTIME : 컴파일 이후에도 JVM에 의해 계속 참조, 리플렉션이나 로깅에 많이 사용
- RetentionPolicy.CLASS : 컴파일러가 클래스를 참조할 때까지 유지
- RetentionPolicy.SOURCE : 컴파일 전까지만 유지, 컴파일 이후에는 사라짐
- @Constraint : TelephoneValidator와 매핑하는 작업 수행
- message() : 유효성 검사가 실패할 경우 반환되는 메세지
- groups() : 유효성 검사를 사용하는 그룹으로 설정
- payload() : 사용자가 추가 정보를 위해 전달하는 값
예외 처리
-
자바에서는 오류를 try/catch, throw구문을 활용하여 처리
-
스프링부트에서는 더욱 편리하게 예외처리를 하는 기능 제공!
예외와 에러
-
예외 (exception) :
- 어플리케이션이 정상적으로 동작하지 못하는 상황
- 입력값의 처리가 불가능하거나, 참조된 갑싱 잘못된 경우 등
- 개발자가 직접 처리할 수 있는 것이므로 미리 코드 설계를 통해 처리 가능!
-
에러 (error) :
-
- 주로 자바의 가상 머신에서 발생시키는 것
- 예외와 달리 어플리케이션 코드에서 처리할 수 있는 것이 거의 없음.
- 메모리 부족(OutOrMemory), 스택 오버블로(StackOverFlow) 등
- 에러는 발생 시점에 처리하는 것이 아니라, 미리 코드를 보며 문제가 발생하지 않도록 예방해서 원천적으로 차단 해야함!
예외 클래스

| 제목 | Checked Exception | Unchecked Exception |
|---|
| 처리 여부 | 반드시 예외 처리 필요 | 명시적 처리를 강제하지 않음 |
| 확인 시점 | 컴파일 단계 | 런타임 단계 |
| 대표적인 예외 클래스 | IOException, SQLException | RuntimeException, NullPointerException, IllegalArgumentException, SystemException |
-
간단히, RuntimeExcepion을 상속받는 Exception클래스는 Unchecked Exception이고 그렇지 않은 Exception클래스는 Checked Exception!
예외 처리 방법
1. 예외 복구
- 예외 상황을 파악해 문제를 해결하는 방법
- try/catch 사용
2. 예외 처리 회피
- 예외가 발생한 시점에서 바로 처리하는 것이 아니라 예외가 발생한 메소드를 호출한 곳에서 처리하도록 전가하는 방식
- throw 사용
3. 예외 전환
- 앞의 두방식을 혼합
- 예외가 발생했을때, 예외의 종류에 따라 호출부로 예외 내용 전달하며 좀 더 적합한 예외 타입으로 전달할 필요 , 또는 어플리케이션에서 예외 처리 단순화를 위해 wrapping해야하는 경우도 있음
- try/catch방식사용하며 catch 블록에서 throw를 사용하여 다른 예외타입으로 전달
- 커스텀 예외 만드는 과정에서 사용!!!!
스프링 부트의 예외 처리 방식
- 웹서비스 어플리케이션에서는 예외가 발생하면 예외를 복구해서 정상적으로 처리하기 보다 요청을 보낸 클라이언트에 어떤 문제가 발생했는지 상황전달하는 경우가 많음!
- 예외가 발생하면, 클라이언트에 오류 메세지를 전달하려면 각 레이어에 발생한 예외를 엔드포인트 레벨인 컨트롤러로 전달해야함!
-
@(Rest)ControllerAdvice와 @ExceptionHandler를 통해 모든 컨트롤러의 예외를 처리
- basePackages속성을 통해 예외를 관제하는 범위 지정 가능
- 범위 설정이 없으면 전역 볌위에서 예외 처리!
- 글로벌 예외 처리!
-
@ExceptionHandler를 통해 특정 컨트롤러의 예외를 처리(로컬)
예외 처리 우선순위
- @ControllerAdvice클래스 내에 동일하게 헨들러 메서드가 선언된 상태 - 구체적인 예외 클래스가 지정된 쪽이 우선순위가 더 높음 (ex.) Exception.clss < NullPointerException.class)
- @ControllerAdvice의 글로벌 예외 처리와 @Controller내의 컨트롤러 예외 처리에 동일한 타입의 예외 처리 - 범위가 좁은 컨트롤러의 핸들러 메소드가 우선순위 (글로벌 exception 처리 <
로컬 exception 처리)
커스텀 예외
- 자바에서 제공하는 표준 예외를 사용하면 어플리케이션의 모든 예외 상황 처리가능하다.
- 그럼에도 커스텀 예외를 사용하는 이유