본문 바로가기

공부/백엔드

[스프링 부트 핵심 가이드] 10 : 유효성 검사와 예외 처리

반응형

유효성 검사

유효성 검사 : 입력으로 들어오는 데이터의 형식을 검증한다. 잘못된 값이 들어왔을 때는 알맞은 예외처리를 한다.

  • Bean Validation : 조건문으로 모든 유효성 검사를 하면 코드가 길어지므로 어노테이션을 통한 필드값 검증

-> spring-boot-starter-validation dependency 추가

 

 

유효성 검사 시점

클라이언트 -> 컨트롤러 -> 서비스 -> 리포지토리 -> DB

  • 각 계층이 이동될 때 전달되는 데이터를 유효성 검사한다.

-> 데이터 전송에 쓰이는 DTO에서 유효성 검사를 수행한다.

@PostMapping("/valid")
public ResponseEntity<?> validate(
	@Valid @RequestBody Dto dto
) {
	return ResponseEntity.ok();
}

-> DTO를 받는 부분에서 @Valid 를 써주어야 검증을 수행한다.

 

 

검증 어노테이션

 

문자열 검증

  • @Null : null 값만 가능하다.
  • @NotNull : null 값은 넣을 수 없다.
  • @NotEmpty : null, "" (빈 문자열) 을 넣을 수 없다.
  • @NotBlank : null, "", " " (공백) 을 넣을 수 없다.

최댓값/최솟값 검증

: 입력값은 int, long, bigDecimal, bigInt 타입이 가능하다.

  • @Min(value=) : 최솟값이 num이다.
  • @Max(value=) : 최댓값이 num이다.
  • @DecimalMin(value=) : 최솟값이 num이다. 문자열로 받을 수도 있다.
  • @DecimalMax(value=) : 최댓값이 num이다. 문자열로 받을 수도 있다.

양수, 음수 검증

  • @Positive : 양수를 허용한다.
  • @PositiveOrZero : 0과 양수를 허용한다.
  • @Negative : 음수를 허용한다.
  • @NegativeOrZero : 0과 음수를 허용한다.

시간 검증

: 입력값은 Date, LocalDate, LocalDateTime 타입이 가능하다.

  • @Future : 현재보다 미래의 날짜를 허용한다.
  • @FutureOrPresent : 현재 포함 미래의 날짜를 허용한다.
  • @Past : 현재보다 과거의 날짜를 허용한다.
  • @PastOrPresent : 현재 포함 과거의 날짜를 허용한다.

이메일 검증

  • @Email : 이메일 형식 (id@aaa.com)을 검증한다. ("" 는 허용)

자릿수 검증

  • @Digits(integer=, fraction=) : 정수 자릿수 integer와 소수 자릿수 fraction을 지정한다. 

Boolean 검증

  • @AssertTrue : true만 허용
  • @AssertFalse : false만 허용

문자열 길이 검증

  • @Size(min=, max=) : min 이상 max 이하 값을 허용

정규식 검증

: https://regexr.com/ : 정규식 테스트

  • @Pattern(regexp="") : 정규 표현식을 이용해 검증한다.
    • ^ : 문자열의 시작 (^a : a로 시작하는 문자)
    • $ : 문자열의 종료 (z& : z로 끝나는 문자)
    • . : 임의의 한 문자 (1)
    • * : 앞에 있는 문자가 없거나 여러개 (0~n)
    • + : 앞에 있는 문자가 하나 이상 (1~n)
    • ? : 앞에 있는 문자가 없거나 하나 존재 (0~1)
    • [ ] : 문자 집합([ab])이나 범위([1-3])
    • { } : 횟수 또는 범위 - a{n} : n번 반복, a{n,m} : n번~m번 사이에서 반복
    • ( ) : 괄호로 묶어서 하나의 문자로 사용 (ab)*
    • | : OR
    • \ : 확장 문자로 다음에 특수문자(+, ^ 등)가 오면 문자로 인식
      • \b : 문자와 공백(not 문자)사이의 문자 (^\b = \B)
      • \A : 문자열의 시작 부분 (=^)
      • \G : 이전 매치의 끝을 연속적으로 매치
      • \Z : 문자열의 끝 부분  (=$) \n 이 있으면 앞부분을 비교
      • \s : 공백 문자 (^\s = \S)
      • \w : 알파벳이나 숫자 (^\w = \W)
      • \d : 숫자 (0~9) (^\d = \D)

-> 검증에서 허용하지 않은 값을 넣을 경우 400 error를 리턴한다.

 

 

유효성 그룹

@Min(value=20, groups=ValidationGroup.class)
int age;

public ResponseEntity<?> validation(
	@Validated(ValidationGroup.class) @RequestBody Dto dto
) {
	return ResponseEntity.ok();
}
  • @__(groups=__.class) : 인터페이스 class로 검증 어노테이션에 그룹을 지정한다.
  • @Validated({__.class}) : 검증할 그룹을 지정한다.

-> @Validated에서 그룹을 지정하면 groups에 해당하는 검증만 수행한다.

-> @Validated에서 그룹을 지정하지 않으면 groups가 설정되지 않은 검증만 수행한다.

 

 

커스텀 Validation

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CustomValidator.class)
public @interface T {
	String message() default "";
    	Class[] groups() default {};
    	Class[] payload() default {};
}

public class CustomValidator implements ConstraintValidator<T, String> {
	@Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            if (value == null) { 
                return false;
            }
            return value.matches(" "); // 표현식과 value가 일치하면 true를 리턴
        }
}

@T
private String t; // t에 값이 들어오면 CustomValidator로 검증한다.
  • ConstraintValidator를 구현하여 isValid 메서드를 통해 직접 유효성 검증을 수행한다.
  • false가 리턴되면 MethodArgumentNotValidException 발생
  • ContraintValidator에서 지정한 Annotation T = Entity에서 사용할 유효성 검증 어노테이션
    • Target : 해당 어노테이션을 어디서 선언할 수 있는지 정의한다.
      • PACKAGE, TYPE, CONSTRUCTOR, FIELD, METHOD, ANNOTATION_TYPE, LOCAL_VARIABLE, PARAMETER, TYPE_PARAMETER, TYPE_USE
    • Retention : 어노테이션이 실제로 적용되고 유지되는 범위
      • RUNTIME(컴파일 이후에도), CLASS, SOURCE
    • Constraint : CustomValidator를 매핑한다.
    • message() : 유효성 검사가 실패할 경우 리턴할 메시지
    • groups() : 유효성 검사를 사용하는 그룹 설정
    • payload() : 사용자가 추가 정보를 위해 전달하는 값

예외 처리

- 예외 (Exception) : 애플리케이션에 정상적으로 동작할 수 없는 상황에 발생하며 try-catch 로 발생한 예외를 처리하거나 throw로 원하는 예외를 발생시킬 수도 있다.

- 에러 : StackOverFlow, OutOfMemory 등 자바 가상머신에서 발생하는 문제로, 코드에서 처리할 수 없고 발생 가능성을 원천 차단해야 한다.

 

-> 모든 예외는 Throwable 을 상속 받는다.

-> Exception의 자식 클래스는 Checked Exception, Unchecked Exception으로 나뉜다.

  • CheckedException : 컴파일 과정에서 확인 가능한 예외이므로 무조건 예외 처리를 해주어야 한다.
    • IOException, SQLException 등
  • UncheckedException : 실행 중에 발생을 알 수 있는 예외로 처리하지 않아도 따로 에러가 뜨지 않는다.
    • RuntimeException, NullPointerException, IllegalArgumentException, IndexOutOfBound 등

 

예외 처리 방법

  1. 예외 복구 : 예외 상황을 파악해 처리 -> try-catch
  2. 예외 처리 회피 : 예외를 다시 발생시켜 해당 메서드를 호출한 곳으로 예외 처리를 전가 -> 해당 메서드는 throw를 하므로 호출하는 곳에서 예외처리가 강제적으로 필요하다.
  3. 예외 전환 : 발생한 예외를 다른 적합한 예외로 다시 발생시켜 처리를 위임 -> throw new otherException

스프링의 예외 처리

: 스프링에서는 주로 클라이언트에게 문제 발생/원인을 알리기 위해 컨트롤러로 오류메시지를 전달하는  방식으로 예외를 처리한다.

 

@(Rest)ControllerAdvice : 모든 컨트롤러의 예외를 처리하는 클래스에 사용한다.

@RestControllerAdvice(basePackages="com.org.*")
public class CustomExceptionHandler {
	@ExceptionHandler(value=Exception.class)
   	public ResponseEntity<?> handle(Exception e, HttpServletRequest request) {
            HttpHeaders header = new HttpHeaders();
            HttpStatus status = HttpStatus.BAD_REQUEST;
            Map<String, Object> map = new HashMap<>();

            map.put("message", "");
            map.put("code", 500);

            return ResponseEntity<>(map, header, status);
        }
}
  • basePackage로 해당 예외 처리를 하게 될 패키지 범위를 지정할 수 있다.
  • 컨트롤러에서 리턴하는 것과 무관하게 해당 ExceptionHandler 메서드에서 리턴하는 값을 클라이언트로 리턴한다.
  • @ExceptionHandler : value에서 발생하는 예외를 처리하는 메서드를 정의하는데 사용하며, value는 배열로 입력하여 여러 예외를 처리할 수도 있다. 특정 컨트롤러에 작성하면 해당 컨트롤러에서 발생하는 예외만 처리한다.

 

Exception 우선순위

: Exception Handler가 여러개 있고, 발생한 Exception이 여러 개에 해당되는 경우 우선순위에 따라 처리된다.

  • 전역 Handler (ControllerAdvice) < 컨트롤러 내 Handler
  • 구체적인 (하위) Exception : Exception < RuntimeException < NullPointerException 

 

커스텀 예외

: 직접 Throwable 하위(Exception 등)클래스를 상속받아 만들어서 호출(throw)하고 처리할 수 있는 예외.

 

장점 및 효과

  1. 발생하는 예외에 적합한 원하는 이름을 지을 수 있다.
  2. 직접 코드로 예외를 관리하며 상황에 맞게 처리 할 수 있다.
  3. 의도하지 않은 경우에서는 커스텀 Exception이 발생하지 않는다.
public class CustomException extends Exception {
	private HttpStatus httpStatus;
    
        public CustomException(String message, HttpStatus httpStatus) {
        	super(message);
            	this.httpStatus = httpStatus;
        }
}
  • 부모 클래스인 Exception(String message)로 생성자 호출
  • HttpStatus의 value(error code), series, resonPhrase(error type)를 이용해서 필요한 값을 편리하게 정의할 수 있고 status에 맞는 예외 처리를 할 수 있다.
  • throw new CustomException("message", HttpStatus.__) 로 호출
반응형