이게 무슨오류일까 ??
enum을 validationCheck해주는 어노테이션을 만들다가 오류가 발생했다.
기본적으로 json body를 받을때, json to object 과정에서 deserialize 는Jackson라이브러리에서 실행됩니다.
이 경우 enum value의 name과 완전 동일한경우, 기본 deserialize가 있기에 문제는 없으나, 변수가 조금이라도 틀리게되면 바로 이런 에러를 맞이하게됩니다.
JSON parse error: Cannot deserialize value of type `enum package`
이경우 해당 에러를 맞이하지않고, 내가 지정한 setter를 사용하게끔 지원하는 @JsonCreator라는 어노테이션이 필요합니다.
youth,adult,oldMan;
@JsonCreator
public static VideoAgeCategoryEnum fromVideoAgeCategoryEnum(String val){
return Arrays.stream(VideoAgeCategoryEnum.values())
.filter(testEnum -> testEnum.name().equals(val))
.findFirst()
.orElse(null);
}
다음과 같이 지정하는경우, API 요청을 받아올때, VideoAgeCategoryEnum 값으로 받은 string은 @JsonCreator를 지정한 메서드의 파라미터로 들어가게 되고, 해당 메서드 기능을 통해, request dto에 저장되게됩니다.
쉽게 이야기하면 json body 받을 때 json to object과정에서 deserialize 는 jackson라이브러리에서 실행시켜준다.
하지만 enum value는 name과 동일했을 때는 deserialize 에서 문제가없지만 변수가 틀리게되면 deserialize가 실패해서 다음과 같은
에러가 뜨게된다. 이럴경우에 enum으로 들어오는 값을 @JsonCreator를 사용하여 틀린값이 들어오든 맞는 값이 들어오든
deserialize가 되게한다!
추가적으로 Enum값을 Validation하는 방법을 적어보겠습니다.
1. 어노테이션을 직접만들고, 구현체를 만들어줍니다.
@Constraint(validatedBy = EnumValidator.class)
/* 해당 annotation이 실행 할 ConstraintValidator 구현체를 `EnumValidator`로 지정합니다. */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
/* 해당 annotation은 메소드, 필드, 파라미터에 적용 할 수 있습니다. */
@Retention(RetentionPolicy.RUNTIME)
/* annotation을 Runtime까지 유지합니다. */
public @interface EnumValid {
String message() default "Invalid value. This is not permitted.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends java.lang.Enum<?>> enumClass();
}
2. 해당 어노테이션지정 시 실제 실행될 로직을 작성해줍니다.
package com.example.springboot.handler.Exception;
import com.example.springboot.domain.video.entity.VideoAgeCategoryEnum;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class EnumValidator implements ConstraintValidator<EnumValid, Enum> {
private EnumValid annotation;
@Override
public void initialize(EnumValid constraintAnnotation) {
this.annotation = constraintAnnotation;
}
//isValid() : 실제 유효성 판단을 내리는 곳으로
// initialize를 통해서 Enum 타입들을 얻었으니 contains 메소드를 활용해서 현재 Param 클래스의
// gender 변수로 들어온 값이 types에 존재하는지만 판단해주면 됩니다.
@Override
public boolean isValid(Enum value, ConstraintValidatorContext context) {
boolean result = false;
Object[] enumValues = this.annotation.enumClass().getEnumConstants();
if (enumValues != null) {
for (Object enumValue : enumValues) {
if (value == enumValue) {
result = true;
break;
}
}
}
return result;
}
}
3. 해당하는 dto에 어노테이션을 붙여주고 Controller에서 @Valid , BindingResult 선언을 통해 오류를 반환할 수 있도록합니다.
@EnumValid(enumClass = VideoAgeCategoryEnum.class, message = "정해진 값들만 넣을 수 있습니다")
private VideoAgeCategoryEnum ageCategoey;
4. CustomApiException을 만들어주고
package com.example.springboot.handler.Exception.ex;
import java.util.Map;
public class CustomVaildationApiException extends RuntimeException {
//객체를 구분할 떄!! serialvseionUID
private Map<String,String> errorMap;
public CustomVaildationApiException(String message) {
super(message);
}
public CustomVaildationApiException(String message, Map<String, String> errorMap) {
super(message);
this.errorMap = errorMap;
}
public Map<String,String> getErrorMap(){
return errorMap;
}
}
5. 해당 오류를 반환해주도록 합시다.
package com.example.springboot.handler.Exception;
import com.example.springboot.common.dto.CMResDto;
import com.example.springboot.handler.Exception.ex.CustomApiException;
import com.example.springboot.handler.Exception.ex.CustomVaildationApiException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
@RestController
@ControllerAdvice // 모든 exception을 낚아챔
public class ControllerExceptionHandler {
@ExceptionHandler(CustomVaildationApiException.class) //RuntimeException 함수를 가로챔
public ResponseEntity<?> validationApiException(CustomVaildationApiException e){
//CMResDto,Script비교
// 클라이언트에게 응답할때는 script가 좋음
return new ResponseEntity<>(new CMResDto<>(-400,e.getMessage(),e.getErrorMap()),HttpStatus.BAD_REQUEST);
}
}
{
"code": -400,
"message": "유효성검사 실패함",
"data": {
"ageCategoey": "정해진 값들만 넣을 수 있습니다"
}
}
이제 Enum값도 하나의 어노테이션을 통해 validationCheck를 할 수 있게 되었습니다.
에러페이지 작동 원리
- 컨트롤러에서 예외가 발생할 경우 컨트롤러 → 인터셉터 → 서블릿 → 필터 → WAS 순으로 예외가 전달되면 WAS는 에러 페이지 정보를 확인하고 다시 에러 페이지 출력을 위해 재요청
- HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 - 예외 발생 -> 인터셉터 -> 서블릿 -> 필터 -> WAS - 예외 페이지 정보 확인 -> 필터 -> 서블릿 -> 인터셉터 -> 예외 페이지 컨트롤러 -> 예외 페이지 (View)
Auth Level: doFilter
인증 인가에 관련된 인증처리는 doFilter단에서 처리를 주로한다.
Controller Level: @ExceptionHandler
Controller 메서드 내의 하위 서비스 (Service, Repository등등)에서 예외가 발생하더라도,
중간에 처리하지 않는 이상 Controller단까지 예외가 던져지게 되고 @ExceptionHandler가 예외를 처리하게 된다.
Checked Exception, Runtime Exception 상관 없이 Controller까지 예외를 throw하면 처리가 가능하다.
Global Level: @ControllerAdvice
하나의 Controller가 아닌 여러 Controller에서 발생하는 예외를 처리하려면 @ControllerAdvice를 사용해야 한다.
@ControllerAdvice는 모든 Controller에서 발생하는 예외를 처리할 수 있게 해주는 애노테이션이다.
DispatcherServlet에서 발생하는 예외를 전역적으로 처리해준다.
DispatcherServlet에서 발생하는 예외만 처리할 수 있고 Filter에서 발생하는 예외는 따로 처리를 하지 않으면 처리가 불가능하다.
Controller의 @ExceptionHandler와 ControllerAdvice의 @ExceptionHandler중 높은 우선순위는?
Controller의 @ExceptionHandler가 먼저다.
'JPA' 카테고리의 다른 글
Spring Data JPA 에서 Page와 Slice (0) | 2023.11.19 |
---|---|
DTO에 Noargs 사용하는이유와 aceessLevel Protected을 사용하는이유 (0) | 2023.08.27 |
컬렉션 fetch join 및 페이징 applying in memory 오류 (0) | 2023.08.02 |
JPA에서 Entity에 protected 생성자를 만드는 이유. (0) | 2023.02.16 |
Entity를 Dto로 쉽게 변환 및 Entity노출을 최대한 자제해라. (0) | 2023.02.09 |