=> 코드가 복잡해지고 가독성이 떨어짐
위와 같은 문제 해결을 위해 'Bean Validation'이라는 데이터 유효성 검사 프레임워크를 제공하기 시작함
Hibernate Validator란?
: Bean Validation 명세의 구현체
Hibernate Validator는 JSR-303명세의 구현체로서 도메인 모델에서 어노테이션을 통한 필드값 검증을 가능하도록 함
10.3.1 프로젝트 생성
프로젝트를 새로 생성하여 아래와 같이 프로젝트 구조를 만든다.
SwaggerConfiguration 클래스 파일을 생성한다.
10.3.2 스프링 부트용 유효성 검사 관련 의존성 추가
아래의 사진처럼 pom.xml파일에 유효성 검사 라이브러리를 의존성으로 추가하여 사용한다.
10.3.3 스프링 부트의 유효성 검사
유효성 검사는 데이터가 각 계층을 넘어오는 시점에 검사를 실시한다.
위의 사진처럼 유효성 검사를 DTO 객체를 대상으로 수행한다
->이는 스프링 부트에서 계층 간 데이터 전송에 대체로 DTO를 활용하고 있기 때문
-DTO 객체 생성(data/dto/ValidRequestDto.java)
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidRequestDto {
@NotBlank
String name;
@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;
@Size(min=0,max=40)
String description;
@Positive
int count;
@AssertTrue
boolean booleanCheck;
}
-문자열 검증
-최댓값/최솟값 검증
-값의 범위 검증
-시간에 대한 검증
-이메일 검증
-자릿수 범위 검증
-Boolean 검증
-문자열 길이 검증
-정규식 검증
-앞에서 생성한 DTO를 사용하는 컨트롤러 객체 생성(controller/ValidationController.java)
@Restcontroller
@RequestMapping("/validation")
public class ValidatonController {
private final Logger LOGGER=LoggerFactory.getLogger(ValidatonController.class);
@PostMapping("/valid")
public ResponseEntity<String> checkValidationByValid(
@Valid @RequestBody ValidRequestDto validRequestDto){
LOGGER.info(validRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validRequestDto.toString());
}
}
Swagger 페이지에 접속하여 동작을 확인
아래와 같이 입력한다.
{
"age": 30,
"booleanCheck": true,
"description": "Validation 실습 데이터입니다.",
"email": "flature@wikibooks.co.kr",
"name": "Flature",
"phoneNumber": "010-1234-5678"
}
제대로 입력하여 호출하였다면 '200 OK'로 응답할 것이다.
만약 설정한 규칙에서 벗어나는 값을 호출했다면 아래의 사진처럼 400 에러가 발생한다.
아래의 사진처럼 애플리케이션의 로그에 로그가 출력되어 문제가 발생한 지점을 확인할 수 있다.
아직 예외 처리를 하지 않아 어디에서 에러가 발생했는지는 알 수 없다.
검사를 실패한 개수를 로그에 포함시키면 아래의 사진처럼 'with 2 errors' 라는 문구가 뜨도록 한다.
-정규식이란?
:특정한 규칙을 가진 문자열 집합을 표현하기 위해 쓰이는 형식
10.3.4 @Validated의 활용
data 패키지 안에 group 패키지 생성 -> ValidationGroup1, ValidationGroup2 인터페이스를 생성
-ValidationGroup1
public interface ValidationGroup1 {
}
-ValidationGroup2
public interface ValidationGroup2 {
}
그 다음 data/dto/ValidatedRequestDto 경로가 되도록 DTO 객체를 생성한다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
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,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/ValidationController.java 에 메서드 추가
@Restcontroller
@RequestMapping("/validation")
public class ValidatonController {
private final Logger LOGGER=LoggerFactory.getLogger(ValidatonController.class);
@PostMapping("/valided")
public ResponseEntity<String> checkValidationByValid(
@Validated @RequestBody ValidatedRequestDto validatedRequestDto){
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validation/group1")
public ResponseEntity<String> checkValidation1(
@Validated(ValidationGroup1.class) @RequestBody ValidationRequestDto validatedRequestDto){
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validated/group2")
public ResponseEntity<String> checkValidation2(
@Validated(ValidationGroup2.class) @RequestBody ValidatedRequestDto validatedRequestDto){
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toStirng());
}
@PostMapping("/validated/all-group")
public ResponseEntity<String> checkValidation3(
@Validated({ValidationGroup1.class,
ValidationGroup2.class}) @RequestBody ValidatedRequestDto validatedRequestDto){
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
}
-Swagger 페이지에서 checkValidation() 메서드 호출
{
"age": -1,
"booleanCheck": true,
"count": -1,
"description": "Validation 실습 데이터입니다.",
"email": "flature@wikibooks.co.kr",
"name": "Flature",
"phoneNumber": "010-1234-5678"
}
-checkValidation1()를 호출하면 확인할 수 있는 로그
- 마지막 메서드를 호출하면 다음과 같은 로그 출력
-호출 데이터 변경하여 메서드 호출
{
"age": 30,
"booleanCheck": false,
"count": 30,
"description": "Validation 실습 데이터입니다.",
"email": "flature@wikibooks.co.kr",
"name": "Flature",
"phoneNumber": "010-1234-5678"
}
10.3.5 커스텀 Validation 추가
※ 자바, 스프링의 유효성 검사 어노테이션에서 제공하지 않는 기능 써야할 때 -> ConstraintValidator와 커스텀 어노테이션을 조합하여 유효성 검사 어노테이션 생성 가능
-config/annotation/TelephoneValidator.java
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})[.-]$");
}
}
-config/annotation/Telephone.java
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=TelephoneValidator.class)
public class Telephone {
String message() default "전화번호 형식이 일치하지 않습니다.";
Class[] groups() default {};
Class[] payload() default {};
}
-사용가능한 ElementType
-RetentionPolicy로 지정가능한 항목
-각 요소들이 의미하는 것
인텔리제이 IDEA -> [Bean Validation] 탭에서 @Telephone 추가된 것 확인
-data/dto/ValidatedRequestDto.java 수정
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidatedRequestDto {
@NotBlank
private String name;
@Email
private String email;
@Telephone
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;
}
-Swagger 페이지
{
"age": 30,
"booleanCheck": false,
"count": 30,
"description": "Validation 실습 데이터입니다.",
"email": "flature@wikibooks.co.kr",
"name": "Flature",
"phoneNumber": "010-1234-5678"
}
10.4.1 예외와 에러
10.4.2 예외 클래스
-Checked Exception과 Unchecked Exception 비교
Checked Exception | Unchecked Exception | |
처리 여부 | 반드시 예외 처리 필요 | 명시적 처리를 강제하지 않음 |
확인 시점 | 컴파일 단계 | 실행 중 단계 |
대표적인 예외 클래스 | IOException SQLException |
RuntimeException NullPointerException IllegalArgumentException IndexOutOfBoundException SystemException |
10.4.3 예외 처리 방법
-예외 복구
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));
}
-예외 처리 회피
int a=1;
String b="a"
try{
System.out.println(a+Integer.parseInt(b));
} catch (NumberFormatException e){
throw new NumberFormatException("숫자가 아닙니다.");
}
-예외 전환 방법
10.4.4 스프링 부트의 예외 처리 방식
-전달받은 예외를 처리하는 방식
※@ControllerAdvice 대신 @RestControllerAdvice를 사용하면 결괏값을 JSON 형태로 반환할 수 있음
-common/exception/CustomExcptionHandler.java
@RestControllerAdvice
public class CustomException {
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 내 handleException 호출, {}, {}", requeset.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/ExceptionController.java
@RestController
@RequestMapping("/exception")
public class ExceptionController {
@GetMapping
public void getRuntimeException(){
throw new RuntimeException("getRuntimeException 메서드 호출");
}
}
-Swagger 페이지
-controller/ExceptionController.java
@RestController
@RequestMapping("/exception")
public class ExceptionController {
private final Logger LOGGER=LoggerFactory.getLogger(ExceptionController.class);
@GetMapping
public void getRuntimeException(){
throw new RuntimeException("getRuntimeException 메서드 호출");
}
@ExceptionHandler(value=RuntimeException.class)
public ResponseEntity<Map<String, String>> handleException(RuntimeException e,
HttpServletRequest request){
HttpHeaders responseHeaders=new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus=HttpStatus.BAD_REQUEST;
LOGGER.error("클래스 내 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);
}
}
-Swagger 페이지
-우선순위 비교 방법
1. 예외 타입 레벨에 따른 예외 처리 우선순위
2. 핸들러 위치에 따른 예외 처리 우선순위
10.4.5 커스텀 예외
커스텀 예외를 사용하는 이유는?
10.4.6 커스텀 예외 클래스 생성하기
-Exception 클래스의 커스텀 예외
public class Exception extends Throwable {
static final long serialVersionUID=-3387516993124229948L;
public Exception(){
super();
}
public Exception(String message, Throwable cause){
super(message,cause);
}
public Exception(Throwable cause){
super(cause);
}
protected Exception(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace){
super(message, cause, enableSuppression, writableStackTrace);
}
}
-Throwable 클래스
public class Throwable implements Serializable {
private static final long serialVersionUID=-3042686055658047285L;
private transient Object backtrace;
private String detailMessage;
//생략
public Throwable(){
fillInStackTrace();
}
public Throwable(String message){
fillInStackTrace();
detailMessage=message;
}
public String getMessage(){
return detailMessage;
}
public String getLocalizedMessage(){
return getMessage();
}
//생략
}
-HttpStatus 열거형 : 주요 코드의 일부
public enum HttpStatus{
BAD_REQUEST(400,Series.CLIENT_ERROR, "Bad Request"),
UNAUTHORIZED(401, Series.CLIENT_ERROE,"Unauthorized"),
PAYMENT_REQUIRED(402, Series.CLIENT_ERROR,"Payment Requred"),
FORBIDDEN(403, Series.CLIENT_ERROR, "Forbidden"),
NOT_FOUND(404, Series.CLIENT_ERROE,"Not Found"),
METHOD_NOT_ALLOWED(405, Series.CLIENT_ERROR,"Method Not Allowed"),
HttpStatus(int value, Series series, String reasonPhrase){
this.value=value;
this.series=series;
this.reasonPhrase=reasonPhrase;
}
public int value(){
return this.value;
}
public Series series(){
return this.series;
}
public String getReasonPhrase(){
return this.reasonPhrase;
}
}
-커스텀 예외 클래스를 생성할 때 필요한 내용들
-common/Constants.java
public class Constants {
public enum ExceptionClass{
PRODUCT("Product");
private String exceptionClass;
ExceptionClass(String exceptionClass){
this.exceptionClass=exceptionClass;
}
public String getExceptionClass(){
return exceptionClass;
}
@Override
public String toString(){
return getExceptionClass()+"Exception.";
}
}
}
-common/exception/CustomException.java
public class CustomException extends Exception {
private Constants.ExceptionClass exceptionClass;
private HttpStatus httpStatus;
public CustomException(Constants.ExceptionClass exceptionClass, HttpStatus httpStatus,
String message){
super(exceptionClass.toString()+message);
this.exceptionClass=exceptionClass;
this.httpStatus=httpStatus;
}
public Constants.ExceptionClass getExceptionClass(){
return exceptionClass;
}
public int getHttpStatusCode(){
return httpStatus.value();
}
public String getHttpStatusType(){
return httpStatus.getReasonPhrase();
}
public HttpStatus getHttpStatus(){
return httpStatus;
}
}
-common/exception/CustomExceptionHandler.java의 handleException() 메서드
@ExceptionHandler(value=CustomException.class)
public ResponseEntity<Map<String, String>> handleException(CustomException e,
HttpServletRequest request){
HttpHeaders responseHeaders=new HttpHeaders();
LOGGER.error("Advice 내 handleException 호출, {}, {}",request.getRequestURI(),
e.getMessage());
Map<String, String> map=new HashMap<>();
map.put("error type", e.getHttpStatusType());
map.put("code", Integer.toString(e.getHttpStatusCode()));
map.put("message", e.getMessage());
return new ResponseEntity<>(map, responseHeaders, e.getHttpStatus());
}
-controller/ExceptionController.java
@GetMapping("/custom")
public void getCustomException() throws CustomException{
throw new CustomException(ExceptionClass.PRODUCT, HttpStatus.BAD_REQUEST,"getCustomException 메서드 호출");
}
-Swagger 페이지
Quiz.
1. 기존의 유효성 검사의 문제점을 해결하기 위해 자바 진영에서 2009년부터 제공한 프레임워크의 이름은?
답: Bean Validation
2. 유효성 검사를 위한 조건을 설정하는 데에 사용되는 어노테이션 중 정규식을 검증하는 어노테이션은?
답: @Pattern
3. 유효성 검사를 지원하는 별도의 어노테이션으로 @Valid 어노테이션의 기능을 포함하는 어노테이션은?
답: @Validated
4. @Telephone 인터페이스 내부 요소들 중 유효성 검사를 사용하는 그룹으로 설정하는 역할을 하는 요소는?
답: groups()
5. 프로그래밍에서 입력 값의 처리가 불가능하거나 참조된 값이 잘못된 경우 등 애프리케이션이 정상적으로 동작하지 못하는 상황을 의미하는 개념?
답: 예외(Exception)
6. Exception의 자식 클래스 중 반드시 예외에 대한 처리가 필요하며 컴파일 단계에서 확인이 가능한 예외 상황은?
답: Checked Exception
7. 예외 처리 방법 중 예외 처리를 회피할 때 사용하는 키워드로 어떤 예외가 발생했는지 호출부에 내용을 전달하는 것은?
답: throw
8. 다음은 유효성 검사 그룹을 설정하는 컨트롤러 클래스의 일부이다. 두 그룹을 모두 지정하도록 빈칸을 채우시오.
@PostMapping("/validated/all-group")
public ResponseEntity<String> checkValidation3(
@Validated({ValidationGroup1.class,
ValidationGroup2.class}) @RequestBody ValidatedRequestDto validatedRequestDto){
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
9. 해당 클래스에서만 국한하여 예외 처리를 하도록 아래의 빈 칸을 완성하시오
@RestController
@RequestMapping("/exception")
public class ExceptionController {
private final Logger LOGGER=LoggerFactory.getLogger(ExceptionController.class);
@GetMapping
public void getRuntimeException(){
throw new RuntimeException("getRuntimeException 메서드 호출");
}
(@ExceptionHandler(value=RuntimeException.class))
public ResponseEntity<Map<String, String>> handleException(RuntimeException e,
HttpServletRequest request){
HttpHeaders responseHeaders=new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus=HttpStatus.BAD_REQUEST;
LOGGER.error("클래스 내 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);
}
}
[출처] 장정우, 『스프링 부트 핵심 가이드』, 위키북스(2022), p.292-322.
ⓒ 구름
[스프링 3팀] 13장. 서비스 인증과 권한 부여 (1) | 2024.01.05 |
---|---|
[스프링 3팀] 12. 서버 간 통신 & 섹션 0. 시큐리티 (0) | 2023.12.29 |
[스프링3] 9장 연관관계 매핑 (1) | 2023.12.01 |
[스프링3] 8장. Spring Data JPA 활용 (0) | 2023.11.24 |
[스프링 3] 07 테스트 코드 작성하기 (0) | 2023.11.17 |