유효성 검사와 예외 처리

김하영·2023년 6월 25일

출처: 스프링 부트 핵심 가이드 - 장정우 지음
https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=296591989

유효성 검사 (데이터 검증)

  • 어플리케이션 비즈니스 로직이 제대로 동작하도록 데이터를 사전에 검증하는 작업
  • 여러 계층에서 들어오는 데이터에 대해 의도한 형식대로 값이 들오는지 체크하는 과정
  • 자바에서 가장 신경써야 하는 것 중 하나인 NullPointerException 예외 처리!

일반적인 어플리케이션 유효성 검사의 문제점

  1. 계층별로 진행하는 유효성 검사는 검증 로직이 각 클래스 별로 분산되어 있어 관리하기 어렵다.
  2. 검증 로깆에 중복이 많아 여러곳에 유사한 기능의 코드가 존재할 수 있다.
  3. 검증해야할 값이 많다면 검증 코드가 길어진다.(코드가 복잡해지고 가독성이 저하된다.)

➡️ 이를 해결하기 이해 자바 진영에서는 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)
    • 컨트롤러에서
      • ex )
        public ResponseEntity<?> checkValidation(@Validated({ValidationGroup1.class, ValidationGroup2.class}) @RequestBody ValidatedReqDto dto) { ...} ```
        
      • @Validated 어노테이션에 특정그룹 지정하지 않은 경우 : groups가 설정되지 않은 필드에 대해 유효성 검사 수행
      • @Validated 어노테이션에 특정그룹 지정한 경우 : 지정된 그룹으로 설정된 필드에 대해서만 유효성 검사 수행

커스텀 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 ExceptionUnchecked Exception
    처리 여부반드시 예외 처리 필요명시적 처리를 강제하지 않음
    확인 시점컴파일 단계런타임 단계
    대표적인 예외 클래스IOException, SQLExceptionRuntimeException, NullPointerException, IllegalArgumentException, SystemException
  • 간단히, RuntimeExcepion을 상속받는 Exception클래스는 Unchecked Exception이고 그렇지 않은 Exception클래스는 Checked Exception!

예외 처리 방법

1. 예외 복구

  • 예외 상황을 파악해 문제를 해결하는 방법
  • try/catch 사용

2. 예외 처리 회피

  • 예외가 발생한 시점에서 바로 처리하는 것이 아니라 예외가 발생한 메소드를 호출한 곳에서 처리하도록 전가하는 방식
  • throw 사용

3. 예외 전환

  • 앞의 두방식을 혼합
  • 예외가 발생했을때, 예외의 종류에 따라 호출부로 예외 내용 전달하며 좀 더 적합한 예외 타입으로 전달할 필요 , 또는 어플리케이션에서 예외 처리 단순화를 위해 wrapping해야하는 경우도 있음
  • try/catch방식사용하며 catch 블록에서 throw를 사용하여 다른 예외타입으로 전달
  • 커스텀 예외 만드는 과정에서 사용!!!!

스프링 부트의 예외 처리 방식

  • 웹서비스 어플리케이션에서는 예외가 발생하면 예외를 복구해서 정상적으로 처리하기 보다 요청을 보낸 클라이언트에 어떤 문제가 발생했는지 상황전달하는 경우가 많음!
  • 예외가 발생하면, 클라이언트에 오류 메세지를 전달하려면 각 레이어에 발생한 예외를 엔드포인트 레벨인 컨트롤러로 전달해야함!
  1. @(Rest)ControllerAdvice@ExceptionHandler를 통해 모든 컨트롤러의 예외를 처리

    • basePackages속성을 통해 예외를 관제하는 범위 지정 가능
    • 범위 설정이 없으면 전역 볌위에서 예외 처리!
    • 글로벌 예외 처리!
  2. @ExceptionHandler를 통해 특정 컨트롤러의 예외를 처리(로컬)

예외 처리 우선순위

  • @ControllerAdvice클래스 내에 동일하게 헨들러 메서드가 선언된 상태 - 구체적인 예외 클래스가 지정된 쪽이 우선순위가 더 높음 (ex.) Exception.clss < NullPointerException.class)
  • @ControllerAdvice의 글로벌 예외 처리와 @Controller내의 컨트롤러 예외 처리에 동일한 타입의 예외 처리 - 범위가 좁은 컨트롤러의 핸들러 메소드가 우선순위 (글로벌 exception 처리 <
    로컬 exception 처리)

커스텀 예외

  • 자바에서 제공하는 표준 예외를 사용하면 어플리케이션의 모든 예외 상황 처리가능하다.
  • 그럼에도 커스텀 예외를 사용하는 이유
    • 네이밍에 개발자의 의도를 담을 수 있기때문에 이름만으로도 어느 정도 예외상황 예측 가능

      • 표준 예외를 사용하면 예외 메세지를 상세히 작성해야하는 번거로움
    • 어플리케이션에서 발생하는 예외를 개발자가 직접 관리하기 수월해짐

      • 표준 예외를 상속받은 커스텀 예외들을 개발자가 직접 코드로 관리하므로 책임소재를 어플리케이션 내부로 가져올 수 있음. 이를 통해 동일한 예외 상황 발생 시 한곳에서 처리하며 특정상황에 맞는 예외코드 적용가능
    • 예외 상황에 대한 처리 용이

      • @ControllerAdvice, @ExceptionHandler를 사용하여 예외 상황을 한곳에서 처리 가능
      • 커스텀 예외로 관리하면 의도하지 않았던 부분에서 발생한 예외는 개발자가 관리하는 예외처리 코드가 처리하지 않으므로 개발과정에서 혼동여지 줄어듬!
profile
백엔드 개발자로 일하고 싶어요 제발

0개의 댓글