[Spring] 유효성 검사와 예외 처리

Miin·2023년 12월 16일
0

Spring

목록 보기
15/17

유효성 검사, 데이터 검증

애플리케이션의 비즈니스 로직이 올바르게 동작하도록 사전 검증하는 작업

Bean Validation

데이터 유효성 검사 프레임워크. 어노테이션을 통해 다양한 데이터 검증 기능 제공.

유효성 검사를 위한 로직을 DTO 같은 도메인 모델과 묶어서 각 계층에서 사용하면서 검증 자체를 도메인 모델에 얹는 방식으로 수행.

Hibernate Validator

Bean Validation 명세의 구현체. 스프링 부트 유효성 검사 표준

pom.xml 의존성 추가

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>

스프링 부트의 유효성 검사

각 계층으로 데이터가 넘어오는 시점에 해당 데이터에 대한 검사 실시.

DTO 객체를 대상으로 수행하는 것이 일반적

대표 어노테이션

문자열 검증

  • @Null : null 값만 허용
  • @NotNull : null 허용 X. "", " " 허용
  • @NotEmpty : null, "" 허용 X. " " 허용
  • @NotBlank : null, "", " " 허용 X

최댓값, 최솟값 검증

  • 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 값 체크 X
  • @AssertFalse : false인지 체크. null 값 체크 X

문자열 길이 검증

  • @Size(min = $number1, max = $number2) : $number1 이상 $number2 이하 범위 허용

정규식 검증

  • @Pattern(regexp = "$expression") : 정규식 검사. 정규식은 자바의 java.util.regex.Pattern 패키지 컨벤션 따름
    • ^ : 문자열 시작
    • $ : 문자열 종료
    • . : 임의의 한 문자
    • * : 앞 문자가 없거나 무한정 많음
    • + : 앞문자가 하나 이상
    • ? : 앞 문자 없거나 하나 존재
    • [,] : 문자의 집합이나 범위 나타냄, 두 문자 사이는 - 기호로 범위 표현
    • {,} : 횟수 또는 범위
    • (,) : 괄호 안의 문자를 하나의 문자로 인식
    • | : 패턴 안에서 OR 연산
    • \ : 확장문자 취급, 역슬래시 다음에 특수문자가 오면 문자로 인식
    • \b : 단어의 경계
    • \B : 단어가 아닌 것에 대한 경계
    • \A : 입력 시작 부분
    • \G : 이전 매치의 끝
    • \Z : 종결자가 있는 경우 입력의 끝
    • \z : 입력의 끝
    • \s : 공백 문자
    • \S : 공백 문자가 아닌 나머지 문자(^\s와 동일)
    • \w : 알파벳이나 숫자
    • \W : 알파벳이나 숫자가 아닌 문자(^\w와 동일)
    • \d : 숫자 [0-9]와 동일하게 취급
    • \D : 숫자를 제외한 모든 문자(^\0-9와 동일)

실습 코드

requestDTO

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidRequestDto {

    @NotBlank
    String name; // null, "", " " 허용 X

    @Email
    String email;

    @Pattern(regexp = "01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$")
    String phoneNumber;

    @Min(value = 20) @Max(value = 40)
    int age; // 20살 이상 40살 이하

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

    @Positive
    int count; // 0 아닌 양수

    @AssertTrue
    boolean booleanCheck; // true인지 체크

}

Controller

@RestController
@RequestMapping("/validation")
@Slf4j
public class ValidationController {

    @PostMapping("/valid")
    public ResponseEntity<String> checkValidationByValid(@Valid @RequestBody ValidRequestDto validRequestDto) {
        log.info(validRequestDto.toString());

        return ResponseEntity.status(HttpStatus.OK).body(validRequestDto.toString());
    }

}

유효성 검사 통과

⚠️ 앞서 설정한 규칙을 벗어나는 값은 400 에러 발생


@Valid 어노테이션 대신 @Validated 적용해보기

public interface ValidationGroup1 {
}

public interface ValidationGroup2 {
}

두 인터페이스 모두 내부 코드 없음, 인터페이스만 생성해서 그룹화하는 용도로 사용


requestDTO
DTO 객체에 그룹 설정

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidatedRequestDto {

    @NotBlank
    private String name;

    @Email
    String email;

    @Pattern(regexp = "01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$")
    String phoneNumber;

    @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;

    @AssertTrue
    private boolean booleanCheck;

}

Controller
@Validated 어노테이션 속성으로 그룹 지정

@RestController
@RequestMapping("/validation")
@Slf4j
public class ValidationController {

    @PostMapping("/valid")
    public ResponseEntity<String> checkValidationByValid(@Valid @RequestBody ValidRequestDto validRequestDto) {
        log.info(validRequestDto.toString());

        return ResponseEntity.status(HttpStatus.OK).body(validRequestDto.toString());
    }

    @PostMapping("/validated/group1")
    public ResponseEntity<String> checkValidation1(@Validated(ValidationGroup1.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
        log.info(validatedRequestDto.toString());

        return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
    }

    @PostMapping("validated/group2")
    public ResponseEntity<String> checkValidation2(@Validated(ValidationGroup2.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
        log.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) {
        log.info(validatedRequestDto.toString());

        return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
    }

}
  • @Validated 어노테이션에 특정 그룹을 지정하지 않는 경우, groups 속성을 설정하지 않은 필드에 대해 유효성 검사 실시
  • @Validated 어노테이션에 특정 그룹을 지정하는 경우, 지정된 그룹으로 설정된 필드에 대해서만 유효성 검사 실시



예외 처리

자바에서는 try/catch, throw 구문 활용

ㅇ 예외(exception)란?
입력 값의 처리가 불가능하거나 참조된 값이 잘못된 경우 등 애플리케이션이 정상적으로 동작하지 못하는 상황. 코드 설계를 통해 개발자가 직접 처리 가능

ㅇ 에러(error)란?
주로 자바의 가상머신에서 발생시키는 것으로 애플리케이션 코드에서 처리할 수 있는 것이 거의 없음.
ex) 메모리 부족(OutOfMemory), 스택 오버플로 등

예외 클래스 상속 구조

  • 모든 예외 클래스는 Throwable 클래스 상속받음

  • Exception 클래스는 크게 Checked Exception, Unchecked Exception 구분

    • Checked Exception : 컴파일 단계에서 확인 가능한 예외 상황. IDE에서 반드시 예외 처리하도록 표시
    • Unchecked Exception : 런타임 단계에서 확인되는 예외 상황. 문법상 문제는 없지만 프로그램 동작 도중 발생

예외 처리 방법

  1. 예외 복구
  2. 예외 처리 회피
  3. 예외 전환

예외 복구

try/catch 구문

try 블록 - 예외 발생 가능 코드 작성
catch 블록 - try 블록에서 발생하는 예외 상황 처리 내용 작성. 여러개 작성 가능(여러개의 catch 블록을 순차적으로 거치면서 예외 유형과 매칭되는 블록을 찾아 예외 처리 동작 수행)

int a = 1;
String b = "a";

try {
	System.out.println(a + Integer.parseInt(b));
} catch (NumberFormatException e) {
	b = "2";
    System.out.println(a + Integer.parseInt(b));
}

예외 처리 회피

예외 발생 시점에서 바로 처리하는 것이 아니라 예외 발생 메서드를 호출한 곳에서 에러 처리를 할 수 있게 전가하는 방식

throw 키워드 사용 - 어떤 예외가 발생했는지 호출부에 내용 전달

int a = 1;
String b = "a";

try {
	System.out.println(a + Integer.parseInt(b));
} catch (NumberFormatException e) {
	throw new NumberFormatException("숫자가 아닙니다.");
}

스프링 부트 예외 처리 방식

  • @(Rest)ControllerAdvice와 @ExceptionHandler를 통해 모든 컨트롤러 예외 처리
    • @ControllerAdvice 대신 @RestControllerAdvice 사용하면 JSON 형태 결과값 반환
  • @ExceptionHandler 통해 특정 컨트롤러 예외 처리

Handler

@RestControllerAdvice
@Slf4j
public class CustomExceptionHandler {
    
    @ExceptionHandler(value = RuntimeException.class)
    public ResponseEntity<Map<String, String>> handleException(RuntimeException e, HttpServletRequest request) {
        HttpHeaders responseHeaders = new HttpHeaders();
        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
        
        log.error("Advice 내 handleException 호출, {}, {}", 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);
    }
    
}
  • @ExceptionHandler : @Controller나 @RestController가 적용된 빈에서 발생하는 예외를 잡아 처리하는 메서드 정의할 때 사용, 배열을 통해 value 속성 여러 클래스 가능

예외를 발생시킬 Controller

@RestController
@RequestMapping("/exception")
public class ExceptionController {
    
    @GetMapping
    public void getRuntimeException() {
        throw new RuntimeException("getRuntimeException 메서드 호출");
    }
    
}

컨트롤러로 요청이 들어오면 RuntimeException 발생


출처 : (책) 스프링 부트 핵심 가이드 / 장정우, 위키북스

profile
컴퓨터공학전공 학부생 Back-end Developer

0개의 댓글