Application에서 Validation을 체크하는 과정에서, 이미 정해진 열거형 타입이나, 형식이 있다면 어떻게 할까요?
형식과 타입을 벗어나면, 오류를 뱉도록 개발을 해야만 하는 경우를 말하는 바입니다.
이전 포스팅에서 설명한 영화 추가 API의 요청 DTO의 경우, 아래와 같았습니다.
@Data
public class MovieAddRequest {
@NotBlank(message = "MovieAddRequest.name은 빈 값일 수 없습니다")
private String name;
@NotBlank(message = "MovieAddRequest.category 빈 값일 수 없습니다")
private String category;
@NotBlank(message = "MovieAddRequest.useYn 빈 값일 수 없습니다")
private String useYn;
}
@Data
public class MovieAddRequest {
@NotBlank(message = "MovieAddRequest.name은 빈 값일 수 없습니다")
private String name;
// MovieCategory에 적용할 수 있는 Validator이 없기 때문에, Annotation이 없음
private MovieCategory category;
@NotBlank(message = "MovieAddRequest.useYn 빈 값일 수 없습니다")
private String useYn;
}
올바르지 않은 요청 데이터에 Exception이 발생하며, 문제가 없어보입니다.
심지어 Best Practice의 경우, 요청-응답에 문제가 없습니다.
curl --location --request POST 'http://localhost:8080/' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "제목",
"category": "A",
"useYn": "N"
}'
하지만, 데이터를 잘못 보내서 오류가 발생했을 때
사실 우리가 정의한 Custom Exception이 아닙니다.
파싱 오류입니다.
Resolved [org.springframework.http.converter.HttpMessageNotReadableException:
JSON parse error: Cannot deserialize value of type `kr.rojae.valid.enums.MovieCategory` from String "A": not one of the values accepted for Enum class: [SF, ANIMATION, ROMANCE];
이 마저를 Custom Exception 처리를 하기야 할 수 있습니다.
하지만, 그 모든 경우를 Handling 하기에는 어려움이 많습니다.
왜냐면 요청 데이터가 DTO 객체의 역-직렬화 된 값으로 전이되는 과정에서 Parsing Error가 발생하기 때문이고
이 모든 값의 역-직렬화에 개입하기에는 프로젝트의 Case가 너무 많습니다.
✏️Improvement method
저는 우선 Enum과 같은 Parsing Error를 유발하는 Type은 사용하지 않았습니다. (상당히 번거롭다고 판단)
“Bean Validation”에서 제공하는 “ConstraintValidator” 인터페이스를 구현하였으며,
Annotation을 구현하여 DTO에서 개발자가 자체적으로 설정해줄 수 있는 Validation의 유연하게 하여
비즈니스 로직에 집중하도록 하였습니다.
API에서 제공하고 있는 메소드입니다.
initialize
isValid
물론 특정 요청에서만 체크하는 경우, 별도의 메소드로 빼는 것이 좋겠습니다만, 그런 경우가 없을 것으로 판단되었습니다. 😁
✏️ Better Case
우선 “ConstraintValidator”을 구현하여, 실제 검증 부분을 먼저 개발하였습니다.
또한 “알 수 없는 타입" 자체를 하나의 열거형으로 관리하도록 하였습니다. (MovieCategory 수정)
그리고 이를 체킹하는 부분을 상호간에 연결했습니다.
MovieCategory
public enum MovieCategory {
ROMANCE("로맨스"),
SF("SF"),
ANIMATION("애니메이션"),
UNKNOWN("알 수 없음");
private String value;
MovieCategory(String value) {
this.value = value;
}
public static MovieCategory valueOfName(String name){
for(var e : MovieCategory.values()){
if(e.name().equals(name)){
return e;
}
}
return UNKNOWN;
}
}
MovieCategoryValidator
public class MovieCategoryValidator implements ConstraintValidator<MovieCategoryValid, String> {
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (MovieCategory.UNKNOWN.equals(MovieCategory.valueOfName(s))) {
return false;
} else {
return true;
}
}
}
그리고 Custom Annotation인 @MovieCategoryValid를 추가하였습니다.
@Data
public class MovieAddRequest {
@NotBlank(message = "MovieAddRequest.name은 빈 값일 수 없습니다")
private String name;
@MovieCategoryValid(message = "MovieAddRequest.category이 유효한 값이 아닙니다")
private String category;
@NotBlank(message = "MovieAddRequest.useYn은 빈 값일 수 없습니다")
private String useYn;
}
🧩 Run And Test
일부러 올바르지 못한 값을 던져 보았습니다.
요청 값
curl --location --request POST 'http://localhost:8080/' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "제목",
"category": "A",
"useYn": "N"
}'
응답 값
{
"message": "MovieAddRequest.category이 유효한 값이 아닙니다",
"response": ""
}
개발자의 의도대로 올바르게 Validation Checking이 이루어졌고, Custom Exception 발생으로 인해서
Body도 Control이 가능해졌습니다.
💬 Extension Case
MovieCategoryValidator에서 initialize 함수를 제대로 사용하지 못했습니다.
어떤 경우 유용하게 사용할 수 있을까요? 🤔
바로 Annotation에서 초기화를 위한 값이 있는 경우, 사용이 가능합니다.
(애초에 얘는, 데이터 전달을 해주는 친구가 Annotation 밖에 없거든요 😄)
설명을 위해서 DTO의 “useYn”에 Custom Validation Checking을 적용해 보았습니다.