Categories
Algorithm🧩
백준 📝
BookReview📕
CleanCode✨
Network 📨
Database 🗄
DevOps☁️
에러 일기📕
Etc💬
Fishy Fish 🎣
Spring🌱
유효성 검사와 예외처리
유효성 검사와 예외처리
여러 계층에 들아오는 데이터에 대해 의도한 형식대로 값이 들어오는 지 체크하는 과정
일반적인 애플리케이션 유효성 검사의 문제점
- 계층별 진행하는 유효성 검사는 검증 로직이 클래스별로 분산되어 있어 관리하기 어렵다.
- 검증 로직에 중복이 많아 여러 곳에 유사한 기능의 코드가 존재할 수 있다.
- 검증해야 할 값이 많다면 검증하는 코드가 길어진다.
∴ 코드가 복잡해지고 가독성이 떨어지게 된다.
Bean Vaildation
- 자바에서 제공하는 데이터 유효성 검사 프레임워크
- 어노테이션을 통해 다양한 데이터를 검증하는 기능 제공
- 유효성 검사를 위한 로직을 DTO 같은 도메인 모델과 묶어 각 계층에서 사용하며 검증 자체를 도메인 모델에 얹는 방식으로 수행한다는 의미
- 어노테이션을 사용한 검증방식이기 때문에 코드의 간결함 유지 가능
Hibernate Vaildation
- Bean Validation 명세의 구현체
- 스프링에서의 유효성 검사 표준
스프링 부트에서 유효성 검사
- 각 계층으로 데이터가 넘어오는 시점에 해당 데이터에 대한 검사 실시
- 스프링에서는 계층간 데이터 전송에 대체로 DTO 객체를 활용하므로 유효성 검사를 DTO 객체를 대상으로 수행하는 것이 일반적
- 문자열 검증 어노테이션
@NULL: 널 값만 허용@NotNULL: 널 값 허용 X, “” or “ “는 허용@NotEmpty: “” 허용 X, “ “는 허용@NotBlank: “”, “ “ 허용 X
- 최댓값, 최솟값 검증
- BigDecimal, BigInteger, int, long 타입 지원
@DemicalMax(value = "$numberString): 해당 문자열보다 작은 값만 허용@DemicalMin(value = "$numberString): 해당 문자열보다 큰 값만 허용@Min(value = $number): 해당 정수 이상의 값 허용@Max(value = $number): 해당 정수 이하의 값 허용
- 값 범위 검증
- BigDecimal, BingInteger, int, long 타입 지원
@Positive: 양수 허용@PositiveOrZero: 양수와 0 허용@Negative: 음수 허용
- 시간 검증
- BigDecimal, BigInteger, int, long 타입 지원
@Future: 미래 날짜 허용@FutureOrPresent: 미래 날짜 또는 현재 허용@Past: 과거 날짜 허용
- 이메일 검증
@Email: 이메일 형식 검사, ““허용
- 자릿수 범위 검증
- BigDecimal, BigInteger, int, long 타입 지원
@Digits(integer, fraction): integer의 자릿수와 fraction의 소수 지릿수를 허용
- Boolean 검증
@AssertTrue: true인지 체크, null은 체크하지 않음@AssertFalse: false인지 체크, null은 체크하지 않음
- 문자열 길이 검증
@Size(min, max): min값 이상 max값 이하의 범위 허용
- 정규식 검증
@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: 명시적 처리가 필요하지 않으며 실행 중 시점에 확인
- 런타임에서 확인되는 예외상황
- 문법상의 문제는 없지만 프로그램이 동작하는 도중 예기치 않은 상황이 생겨 발생하는 예외
예외 처리 방법
- 예외 복구
- 예외 상황을 파악해 문제를 해결하는 방식
- try/catch 방식
- 예외 처리 회피
- 예외가 발생한 시점에서 바로 처리하지 않고 예외가 발생한 메서드를 호출한 곳에서 에러 처리를 할 수 있도록 전가하는 방식
- throw 키워드를 사용해 어떤 예외가 발생했는지 호출부에 전달 가능
- 예외 전환
- 예외가 발생했을 때 어떤 예외가 발생했느냐에 따라 호출부로 예외 내용을 전달하며 좀 더 적합한 예외 타입으로 전달하는 방식
- try/catch 방식을 사용하며 catch 블록에서 throw 키워드를 사용해 다른 예외 타입으로 전달
스프링 부트의 예외 처리 방법
- 예외가 발생한 경우 클라이언트에 오류 메시지를 전달하려면 각 레이어에서 발생한 예외를 엔드포인트 레벨인 컨트롤러로 전달해야 함
- 전달 받은 예외를 스프링 부트에서 처리하는 방식은 크게 두 가지
- @(Rest)ControllerAdvice, @ExceptionHandler를 통해 모든 컨트롤러의 예외 처리
- @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
- 동일한 타입의 예외처리의 경우 범위가 좁은 컨트롤러의 핸들러 메서드가 우선순위
커스텀 예외
- 예외 처리 영역이 늘어나고 상황이 다양해지며 사용하는 예외 타입이 많아 질 때 사용
- 커스텀 예외를 만들면 네이밍에 개발자의 의도를 담을 수 있어 이름만으로 예외 상황 짐작 가능
- 표준 예외에서는 이름만으로 이해하기 어려운 경우가 있어 예외 메세지를 상세히 작성해야 함
- 발생하는 예외를 개발자가 직접 관리하기 수월해짐
- 동일한 예외가 발생한 경우 한 곳에서 처리하며 특정 상황에 맞는 예외 코드 적용
- 예외가 발생하는 상황에 해당하는 상위 예외 클래스 상속받음