근 한 달만의 포스팅이다.. 포트폴리오 작성, 자기소개서 작성, 여행 등으로 정신이 없어서 포스팅을 미루다가 이제서야 작성한다...
오늘은 관리자 API 중 멤버의 정보를 변경하는 API를 개발하고 테스트하던 중에 겪었던JSON parse error: Cannot coerce empty String ("") to 'CSS.ReservationSystem.domain.Role'
오류에 대해 포스팅 하려한다.
받은 request의 값들이 유효한 값인지 확인하는 코드를 Service단에서 작성하였고 이를 테스트 하는 중이었다.
@Override
public void updateMember(MemberRequestDto request, Long id) throws Exception {
Member member = memberRepository.findByid(id);
if(!Objects.nonNull(request.getStudentId())) {
throw new NullPointerException("Input Student ID is Null");
}
if(request.getPassword() == null || request.getPassword().isEmpty()) {
throw new NullPointerException("Input Password is Null");
}
if(request.getName() == null || request.getName().isEmpty()) {
throw new NullPointerException("Input Name is Null");
}
// value()는 role을 String으로 return하는 메서드
if(request.getRole() == null || request.getRole().value().isEmpty()) {
throw new NullPointerException("Input Role is Null or Invalid Value");
}
if(request.getEmail() == null || request.getEmail().isEmpty()) {
throw new NullPointerException("Email is Null");
}
member.updateStudentId(request.getStudentId());
member.updatePassword(passwordEncoder.encode(request.getPassword()));
member.updateName(request.getName());
member.updateRole(request.getRole());
member.updateEmail(request.getEmail());
memberRepository.save(member);
}
request의 값이 null이거나 빈 String이면 Exception을 throw하도록 하였다.
다른 값들은 문제가 없이 잘 테스트 되었고 role의 null체크도 원하는 대로 잘 테스트 되었다.
문제는 role에 빈 String을 작성해 요청을 보낼 때 였다.
request를 보낼 때 role 부분에 아무것도 없는, 즉 빈 String을 넣어 보냈더니 의도한 INTERNAL_SERVER_ERROR가 아닌 400 Bad Request가 응답되었다.
IntelliJ log에도 의도한 NullPointerException
이 아닌 HttpMessageNotReadableException
와 함께 JSON parse error: Cannot coerce empty String ("") to 'CSS.ReservationSystem.domain.Role'
가 찍혀있었다. 내 생각대로라면 Service 코드 중 if문의 request.getRole().value().isEmpty()
에서 걸려 NullPointerException
을 발생시켜야 하는데 뭔가 잘못된 것이다. 도대체 왜 이런가 싶어서 구글링을 해보았다.
오류 메시지에 해답이 있었다. JSON parse error: Cannot coerce empty String ("") to 'CSS.ReservationSystem.domain.Role'
즉 Request Body로 받은 JSON을 분석하는 과정에서 문제가 생겼고 빈 문자열을 enum 형태인 Role로 변환할 수 없다는 것이다. 그렇다면 enum Role을 확인해보자.
public enum Role {
ROLE_STUDENT("ROLE_STUDENT"),
ROLE_PROFESSOR("ROLE_PROFESSOR"),
ROLE_ADMIN("ROLE_ADMIN");
String role;
Role(String role) {
this.role = role;
}
public String value() {
return role;
}
}
empty String ("")에 대한 enum 필드값이 있는가? 없다. 그래서 오류가 발생한 것이다.
즉 enum 형태인 Role이 포함된 MemberRequestDto를 통해 JSON형태로 request를 받았지만 JSON을 Object로 deserialize(역직렬화)하는 과정에서 빈 문자열 ("")에 매핑되는 enum 필드값이 없기 때문에 발생한 것이다.
비단 빈 문자열 ("")에만 발생하는 오류가 아닌 enum에 없는 값이 들어오면 발생하는 오류이다.
(예를 들어 빈 문자열이 아닌 "role_student"와 같이 소문자로 된 값이 들어와도 매핑되는 enum 필드값이 없기 때문에 오류가 발생한다.)
의외로 간단하다.
enum Role 필드에 존재하지 않는 값이 들어오면 null을 반환하도록 하는 메서드를 작성하고 이 메서드에 @JsonCreator라는 어노테이션을 붙여주면 된다.
public enum Role {
ROLE_STUDENT("ROLE_STUDENT"),
ROLE_PROFESSOR("ROLE_PROFESSOR"),
ROLE_ADMIN("ROLE_ADMIN");
String role;
Role(String role) {
this.role = role;
}
@JsonValue
public String value() {
return role;
}
@JsonCreator
public static Role parsing(String inputValue) {
return Arrays.stream(Role.values()).filter(type
-> type.value().equals(inputValue)).findFirst().orElse(null);
}
}
JSON을 Object로 역직렬화 할 때 Jackson 라이브러리를 통해 역직렬화 하는데 이 때 별도의 설정이 없다면 다음과 같이 처리한다.
1. 기본 생성자를 이용해서 생성
2. 필드가 public이라면 직접 할당
3. 필드가 private이라면 setter를 사용
@JsonCreator는 위 과정 중 기본 생성자 + setter 조합을 대체하는 어노테이션이다. 이 어노테이션이 생성자나 메서드 위에 선언되면 Jackson은 해당 생성자나 메서드를 통해 객체를 생성하고 필드를 생성과 동시에 채운다.
여담으로 @JsonValue 어노테이션은 Object를 JSON으로 serialize(직렬화)할 때 상수명이 아닌 필드값으로 직렬화 시키기 위한 어노테이션이다.
예를 들어 TYPE1("아이폰")이라는 상수명과 필드값이 있을 때 어노테이션이 없다면 "TYPE1"으로 직렬화 되지만 어노테이션이 있다면 "아이폰"으로 직렬화된다.
잘못된 값이 들어오면 null을 반환하는 메서드를 작성하고 @JsonCreator 어노테이션을 붙여준 뒤 다시 테스트 해보았다.
role에 빈 문자열 ("")을 넣어 요청을 보냈을 때 의도한 대로 "Input Role is Null or Invalid Value"라는 메시지와 함께 NullPointerException
이 발생한 것을 확인할 수 있다.
필자는 enum의 필드값과 완벽히 일치하지 않으면 null을 반환하도록 작성했기 때문에 role에 소문자로 작성된 문자열을 넣어 요청을 보냈을 때에도 NullPointerException
이 발생한 것을 확인할 수 있다.
처음에 권한에 관한 설계를 할 때 String이 아닌 enum을 사용하기로 한 이유가 API에서 request를 받을 때 ROLE_STUDENT, ROLE_PROFESSOR, ROLE_ADMIN 외 잘못된 값이 들어오는 것을 방지하기 위함이었는데 API를 작성하다보니 이 점을 간과하였다. 언제나 초심을 잊지 말아야 하는 이유인 것 같다.
또한 애초에 Service 단에서 빈 문자열인지 확인하는 이 코드는 잘못된 것이었다.
// value()는 role을 String으로 return하는 메서드
if(request.getRole() == null || request.getRole().value().isEmpty()) {
throw new NullPointerException("Input Role is Null or Invalid Value");
}
매핑되는 필드값이 없어서 역직렬화를 하지 못했는데 enum Role의 value 메서드나 isEmpty 메서드가 동작할리 없었던 것이다.
https://green-bin.tistory.com/90
https://inkyu-yoon.github.io/docs/Language/SpringBoot/EnumValidation
https://eclipse4j.tistory.com/283