들어가기 전에
서버에서 서버로 RestTemplate을 이용하는데 Json에서 Dto로 변환할 때(역직렬화) 이슈가 있었다.
이때 직렬화, 역직렬화에 대한 개념을 정리하고 과정에서 생긴 이슈를 분석하고자 한다.
여기에서 사용된 코드는 실제 회사에서 사용한 코드는 아니며, 포스팅을 위해 최대한 유사하게 만들어진 별도의 샘플 코드이다.
스프링에서의 직렬화와 역직렬화란?
스프링 HTTP 요청 / 응답 과정에서 자바 객체 ↔ JSON 간에 변환을 하는 것이 직렬화와 역직렬화이다.
- HTTP 응답은 서버 → 클라이언트 흐름이기 때문에 자바 객체 → JSON 변환인 직렬화 과정이고,
- HTTP 요청은 클라이언트 → 서버 흐름이기 때문에 JSON → 자바 객체 변환인 역직렬화 과정이다.
즉, ResponseDto를 HTTP 응답에 담을 때 ResponseDto를 JSON으로 변환하는 과정이 직렬화 과정이고
HTTP 요청 시 JSON이 RequestDto로 변환되는 과정이 역직렬화 과정이다.
문제 상황
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class PersonDto {
private String name;
private int age;
//Enum 클래스
private Gender gender;
}
@AllArgsConstructor
@JsonFormat(shape=JsonFormat.Shape.OBJECT)
public enum Gender {
MALE(1,"여성"),
FEMALE(2,"남성");
@Getter
private int code;
@Getter
private String name;
public static Gender valueOf(int code) {
for(Gender gender : values())
if(gender.code == code )
return gender;
throw new IllegalArgumentException("Invalid code: " + code);
}
RestTemplate을 이용해 Response를 PersonDto를 받아야할 때, PersonDto의 enum타입인 Gender을 역직렬화하지 못했었다.
예시 response(PersonDto)
name = "jone", age = 25, gender={code=2, name="남성"}
에러 메세지
java.lang.IllegalArgumentException: Cannot deserialize value of type `com.example.sericalizeTest.mappedenum.Gender` from Object value (token `JsonToken.START_OBJECT`)
at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: com.example.sericalizeTest.dto.PersonDto["gender"])
해결책
Gender의 value만 넘기면 되는데 Gender Object로 넘겨줘서 생기는 문제였다. (디버깅하면서도 큰차이점을 몰랐다....)
넘겨줘야할 값
code=2, name="남성"
실제로 넘겨준 값
gender = {code=2, name="남성"}
@JsonProperty 어노테이션을 추가해서 Object로 넘어오는 값을 역직렬화가 가능하게 해줬다.
@JsonCreator
public static Gender fromJson(@JsonProperty("code")int code, @JsonProperty("name")String name) {
for (Gender gender: Gender.values()) {
if (gender.code == code && gender.name.equals(name)) {
return gender;
}
}
throw new IllegalArgumentException("Invalid code or name");
}
테스트 코드
@Getter
public class ResultJson {
private Object object;
public ResultJson(Object ob){
this.object=ob;
}
}
@SpringBootTest
public class DeserializeTest {
@MockBean
private RestTemplate restTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
void includeEnumDtoDeserialize() throws Exception {
//response에 담을 DTO
PersonDto personResponse = new PersonDto();
personResponse.setAge(25);
personResponse.setName("name");
personResponse.setGender(Gender.FEMALE);
//주어진 상황은 Object 필드를 가진 ResultJson을 이용해야했다.
ResultJson resultJson = new ResultJson(personResponse);
PersonDto personDto = objectMapper.convertValue(resultJson.getObject(), PersonDto.class);
assertEquals(Gender.FEMALE, personDto.getGender());
assertEquals(2,personDto.getGender().getCode() );
assertEquals("남성", personDto.getGender().getName());
}
}
+추가적으로
@JsonFormat(shape=JsonFormat.Shape.OBJECT)
JsonFormat.Shape.OBJECT는 객체 형태로 직렬화하도록 지정하는 역할을 합니다. 일반적으로 Jackson은 기본적으로 클래스의 필드를 키로 하고 그 값들을 직렬화하여 JSON 객체로 만듭니다. 그러나 때로는 클래스 자체를 하나의 JSON 객체로 표현하고 싶을 때가 있습니다. 이때 @JsonFormat(shape=JsonFormat.Shape.OBJECT)를 사용하여 해당 클래스를 JSON 객체로 직렬화하도록 지정할 수 있습니다.
앞으로 더 공부해보고 싶은 것 등등
RestTemplate는 동기 방식이지만 WebClient는 비동기 방식으로 이에 대한 개념을 알고 상황에 따라서 어떤것을 적용할지 고민해야겠다.
'Backend > Java' 카테고리의 다른 글
[JAVA] UUID란? 사용이유, 사용법 (0) | 2024.02.22 |
---|---|
[Java] Static (0) | 2024.02.22 |
equals()와 hashCode() (0) | 2023.04.08 |
[StringUtills] String의 null / 빈 값 / 공백 처리하기 (0) | 2023.02.27 |
[Jackson] Json 날짜 타입 매핑하기 (0) | 2022.02.10 |