유효성 검사와 예외처리

유효성 검사와 예외처리

여러 계층에 들아오는 데이터에 대해 의도한 형식대로 값이 들어오는 지 체크하는 과정

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

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

∴ 코드가 복잡해지고 가독성이 떨어지게 된다.

Bean Vaildation

  • 자바에서 제공하는 데이터 유효성 검사 프레임워크
  • 어노테이션을 통해 다양한 데이터를 검증하는 기능 제공
  • 유효성 검사를 위한 로직을 DTO 같은 도메인 모델과 묶어 각 계층에서 사용하며 검증 자체를 도메인 모델에 얹는 방식으로 수행한다는 의미
  • 어노테이션을 사용한 검증방식이기 때문에 코드의 간결함 유지 가능

Hibernate Vaildation

  • Bean Validation 명세의 구현체
  • 스프링에서의 유효성 검사 표준

스프링 부트에서 유효성 검사

  • 각 계층으로 데이터가 넘어오는 시점에 해당 데이터에 대한 검사 실시
  • 스프링에서는 계층간 데이터 전송에 대체로 DTO 객체를 활용하므로 유효성 검사를 DTO 객체를 대상으로 수행하는 것이 일반적
  1. 문자열 검증 어노테이션
    • @NULL: 널 값만 허용
    • @NotNULL: 널 값 허용 X, “” or “ “는 허용
    • @NotEmpty: “” 허용 X, “ “는 허용
    • @NotBlank: “”, “ “ 허용 X
  2. 최댓값, 최솟값 검증
    • BigDecimal, BigInteger, int, long 타입 지원
    • @DemicalMax(value = "$numberString): 해당 문자열보다 작은 값만 허용
    • @DemicalMin(value = "$numberString): 해당 문자열보다 큰 값만 허용
    • @Min(value = $number): 해당 정수 이상의 값 허용
    • @Max(value = $number): 해당 정수 이하의 값 허용
  3. 값 범위 검증
    • BigDecimal, BingInteger, int, long 타입 지원
    • @Positive: 양수 허용
    • @PositiveOrZero: 양수와 0 허용
    • @Negative: 음수 허용
  4. 시간 검증
    • BigDecimal, BigInteger, int, long 타입 지원
    • @Future: 미래 날짜 허용
    • @FutureOrPresent: 미래 날짜 또는 현재 허용
    • @Past: 과거 날짜 허용
  5. 이메일 검증
    • @Email: 이메일 형식 검사, ““허용
  6. 자릿수 범위 검증
    • BigDecimal, BigInteger, int, long 타입 지원
    • @Digits(integer, fraction): integer의 자릿수와 fraction의 소수 지릿수를 허용
  7. Boolean 검증
    • @AssertTrue: true인지 체크, null은 체크하지 않음
    • @AssertFalse: false인지 체크, null은 체크하지 않음
  8. 문자열 길이 검증
    • @Size(min, max): min값 이상 max값 이하의 범위 허용
  9. 정규식 검증
    • @Pattern(regexp): 정규식을 검사한다.

예시)

```
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) @Max(value = 40)
private int age;

@Size(min = 0, max = 40)
private String description;

@Positive
private int count;

@AssertTrue
private boolean booleanCheck;

}
```
  • 사용할 DTO

      public class ValidationController {
          ...
          @PostMapping("/valid")
          public ResponseEntity<String> checkValidationByValid(
                  @Valid @RequestBody ValidRequestDto validRequestDto) {
              LOGGER.info(validRequestDto.toString());
              return ResponseEntity.status(HttpStatus.OK).body(validRequestDto.toString());
          }
      }    
    
    • @Vaild 어노테이션을 지정해 DTO 객체에 대해 유효성 검사 수행
    • 유효성 검사에 실패할 경우 400 에러 발생

@Validated 활용

  • @Valid 어노테이션은 자바에서 지원하는 어노테이션
  • @Validated는 Spring에서 지원하는 별도의 어노테이션
  • Validated는 Valid 어노테이션 기능을 포함하고 있기 때문에 @Validated로 변경할 수 있다.
  • 그룹별로 묶어 유효성 검사 진행 가능
  • 내용이 존재하지 않는 인터페이스를 그룹화하는 용도로 사용 가능
      public interface ValidationGroup {
      }
    
  • 예제
      ...
    
      @Min(value = 20, groups = ValidationGroup1.class)
      @Max(value = 40, groups = ValidationGroup1.class)
      private int age;
    
      @Size(min = 0, max = 40)
      private String description;
    
      @Positive(groups = ValidationGroup2.class)
      private int count;
    
  • group 속성을 사용하여 ValidationGroup1, ValidationGroup2 그룹을 설정

      @PostMapping("/valid")
      public ResponseEntity<String> checkValidationByValid(
              @Validated @RequestBody ValidRequestDto validRequestDto) {
         ...
      }
    
      @PostMapping("/validated/group1")
      public ResponseEntity<String> checkValidation1(
              @Validated(ValidationGroup1.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
          ...
      }
    
      @PostMapping("/validated/group2")
      public ResponseEntity<String> checkValidation2(
              @Validated(ValidationGroup2.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
          ...
      }
    
      @PostMapping("/validated/all-group")
      public ResponseEntity<String> checkValidation3(
                      @Validated({ValidationGroup1.class,
                      ValidationGroup2.class}) @RequestBody ValidatedRequestDto validatedRequestDto) {
                      ...
          }
    
  • 그룹을 지정해 유효성 검사를 실시하는 경우에는 어떤 상황에서 사용할 지 설정해야 의도대로 사용할 수 있다.

커스텀 Validation 추가

  • ConstraintValidator와 커스텀 어노테이션을 조합해 별도의 유효성 검사 어노테이션 생성 가능
  • 예를 들어 전화번호 형식이 일치하는지 확인하는 유효성 검사 어노테이션
      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})$");
          }
      }
    
    • ContraintValidator 인터페이스의 구현첼로 정의
    • 어떤 어노테이션 인터페이스인지 타입 지정해야 함
    • false가 리턴되면 MethodArgumentNotValidException 예외 발생
      @Target(ElementType.FIELD)
      @Retention(RetentionPolicy.RUNTIME)
      @Constraint(validatedBy = TelephoneValidator.class)
      public @interface Telephone {
          String message() default "전화번호 형식이 일치하지 않습니다.";
          Class[] groups() default {};
          Class[] payload() default {};
      }
    
    • @Target 어노테이션을 통해 이 어노테이션을 어디서 선언할 수 있는지 정의
    • @Retention: 어노테이션이 실제로 적용되고 유지되는 범위
    • @Constraint: TelephoneValidator와 매핑하는 작업 수행
    • @Telephoe 어노테이션으로 사용 가능

예외 처리

예외와 에러

  • 예외: 값의 처리가 불가능하거나 참조된 값이 잘못된 경우 등 애플리케이션이 정상적으로 동작하지 못하는 상황
    • 개발자가 코드 설계를 통해 처리할 수 있음
  • 에러: 주로 자바 가상머신에서 발생시키는 것으로 예외와 달리 애플리케이션 코드에서 처리할 수 있는 것이 거의 없다.
    • 예시: 메모리 부족, 스택 오버플로
    • 미리 애플리케이션 코드를 보며 문제가 발생하지 않도록 예방하여 원천적으로 차단할 필요가 있다.

예외 클래스

  • 모든 예외는 Throwable 클래스를 상속 바음
  • Exception 클래스는 Checked와 Unchecked Exception으로 구분 가능
  • Checked Exception: 반드시 예외처리가 필요하며 컴파일 단계에서 확인.
    • 컴파일 단계에서 확인 가능한 예외 상황
    • IDE에서 캐치해 반드시 예외처리를 할 수 있게 표시
  • Unchecked Exception: 명시적 처리가 필요하지 않으며 실행 중 시점에 확인
    • 런타임에서 확인되는 예외상황
    • 문법상의 문제는 없지만 프로그램이 동작하는 도중 예기치 않은 상황이 생겨 발생하는 예외

예외 처리 방법

  1. 예외 복구
    • 예외 상황을 파악해 문제를 해결하는 방식
    • try/catch 방식
  2. 예외 처리 회피
    • 예외가 발생한 시점에서 바로 처리하지 않고 예외가 발생한 메서드를 호출한 곳에서 에러 처리를 할 수 있도록 전가하는 방식
    • throw 키워드를 사용해 어떤 예외가 발생했는지 호출부에 전달 가능
  3. 예외 전환
    • 예외가 발생했을 때 어떤 예외가 발생했느냐에 따라 호출부로 예외 내용을 전달하며 좀 더 적합한 예외 타입으로 전달하는 방식
    • try/catch 방식을 사용하며 catch 블록에서 throw 키워드를 사용해 다른 예외 타입으로 전달

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

  • 예외가 발생한 경우 클라이언트에 오류 메시지를 전달하려면 각 레이어에서 발생한 예외를 엔드포인트 레벨인 컨트롤러로 전달해야 함
  • 전달 받은 예외를 스프링 부트에서 처리하는 방식은 크게 두 가지
    1. @(Rest)ControllerAdvice, @ExceptionHandler를 통해 모든 컨트롤러의 예외 처리
    2. @ExceptionHandler를 통해 특정 컨트롤러의 예외 처리
      @RestControllerAdvice 
      public class CustomExceptionHandler {
    
      private final Logger LOGGER = LoggerFactory.getLogger(CustomExceptionHandler.class);
    
      @ExceptionHandler(value = RuntimeException.class)
      public ResponseEntity<Map<String, String>> handleException(RuntimeException e,
          HttpServletRequest request) {
          HttpHeaders responseHeaders = new HttpHeaders();
          HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
    
          LOGGER.error("Advice 내 exceptionHandler 호출, {}, {}", request.getRequestURI(),
              e.getMessage());
    
          Map<String, String> map = new HashMap<>();
          map.put("error type", httpStatus.getReasonPhrase());
          map.put("code", "400");
          map.put("message", e.getMessage());
    
          return new ResponseEntity<>(map, responseHeaders, httpStatus);
      }
    
  • @Controller, @RestController에서 발생하는 예외를 한 곳에서 관리하고 처리할 수 있게 하는 기능 수행
  • RestContrllerAdvice(basePackages = “com.springboot.valid_exception”)를 통해 범위 지정 가능

      @GetMapping
      public void getRuntimeException() {
          throw new RuntimeException("getRuntimeException 메소드 호출");
      }
    
  • 컨트롤러 요청이 들어오면 RuntimeException 발생
  • 컨트롤러에서 던진 예외는 @ControllerAdvice가 선언되어 있는 핸들러 클래스에서 매핑된 예외 타입을 찾아 처리
  • 별도의 범위설정이 없는 경우 전역 범위에서 예외 처리
  • ControllerAdvice 클래스 내에서 동일하게 핸들러 메서드가 선언된 상태이면 더 구체적인 예외처리가 우선순위
    • Exception클래스 < NullPointException
  • 동일한 타입의 예외처리의 경우 범위가 좁은 컨트롤러의 핸들러 메서드가 우선순위

커스텀 예외

  • 예외 처리 영역이 늘어나고 상황이 다양해지며 사용하는 예외 타입이 많아 질 때 사용
  • 커스텀 예외를 만들면 네이밍에 개발자의 의도를 담을 수 있어 이름만으로 예외 상황 짐작 가능
    • 표준 예외에서는 이름만으로 이해하기 어려운 경우가 있어 예외 메세지를 상세히 작성해야 함
  • 발생하는 예외를 개발자가 직접 관리하기 수월해짐
    • 동일한 예외가 발생한 경우 한 곳에서 처리하며 특정 상황에 맞는 예외 코드 적용
  • 예외가 발생하는 상황에 해당하는 상위 예외 클래스 상속받음

homebdy
homebdy 개발에 이제 막 발 담근 사람. 개발에 이제 막 발 담근 사람. 개발에 이제 막 발 담근 사람. 개발에 이제 막 발 담근 사람.
comments powered by Disqus