이 글에서는
- 직렬화의 개념
- Springboot에서 직렬화를 하는 방식(Jackson)
- Jackson의 동작 방식
에 대해 정리하였습니다.
1. 문제 상황
간단한 조회 api를 만들고 postman을 통해 호출했는데, Dto 객체를 읽어올 때 다음과 같은 에러가 발생했다.
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.ebcho.homecook.web.dto.RecipeListResponseDto and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.ArrayList[0])
2. 원인 파악
Dto 클래스를 위한 serializer가 없다고 한다.
2. 1 Serialization 직렬화란?
사람들이 서로 소통할 때 언어(abcd,가나다라)가 있는 것처럼, 컴퓨터는 '01010101' 처럼 이루어진 바이트 형식 데이터를 통해서 소통을 한다. 따라서 컴퓨터간 데이터를 주고 받을 때는 바이트 형태로 변환을 해주어야 하는데, 이것을 데이터 직렬화라고 한다. 데이터를 파일로 저장하거나, 네트워크를 통해 다른 컴퓨터에게 전달하거나, 데이터베이스에 저장할 때 등 데이터를 주고 받을 때는 꼭 직렬화를 통해 컴퓨터가 이해할 수 있는 언어로 변환을 해주어야 한다.
2.2 Jackson을 이용한 직렬화
spring-boot-starter-web을 dependency로 설정하면 spring-boot-starter-json을 포함하고, Jackson이 내장되어 있다. Jackson이 클래스 경로에 있으면 ObjectMapper라는 빈이 자동으로 구성된다.
결론부터 말하자면, 직렬화에 실패한 이유는 Jackson이 직렬화를 하는 과정에서 Dto의 모든 필드가 private이거나 패키지가 private일 경우 Dto를 직렬화하는 데에 실패하기 때문이다. 가장 명료한 해결 방법은 Dto 클래스에 (public) getter를 추가하는 것이다.
Dto에 @Getter를 추가해준다.
@Getter
public class RecipeResponseDto {
private Long id;
private String title;
private String author;
...
import com.ebcho.homecook.domain.recipe.Recipe;
import com.ebcho.homecook.web.dto.RecipeResponseDto;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
public class JacksonTest {
@Test
void givenDtoWithGetters_whenSerializingWithDefault_thenNoException() throws JsonProcessingException {
//given
ObjectMapper objectMapper = new ObjectMapper();
RecipeResponseDto dto = new RecipeResponseDto(Recipe.builder().title("title").content("test123").build());
//when
String dtoAsString = objectMapper.writeValueAsString(dto);
//then
assertThat(dtoAsString, containsString("title"));
assertThat(dtoAsString, containsString("content"));
//passed
}
}
getter를 추가한 후 위 테스트 코드의 결과는 pass이다.
2.2.1 Jackson ObjectMapper 동작 방식
어떻게 동작하길래 getter가 있어야 동작하는걸까? 라이브러리 코드를 분석해보자.
(jackson-databind-2.13.4.2 기준)
ObjectMapper.class
public class ObjectMapper extends ObjectCodec implements Versioned, Serializable {
public ObjectMapper(JsonFactory jf, DefaultSerializerProvider sp, DefaultDeserializationContext dc) {
//중략...
this._configOverrides = new ConfigOverrides();
this._coercionConfigs = new CoercionConfigs();
this._serializationConfig = new SerializationConfig(base, this._subtypeResolver, mixins, rootNames, this._configOverrides);
ObjectMapper 생성시 new ConfigOverrides()를 호출한다.
ConfigOverrides.class
public class ConfigOverrides implements Serializable {
private static final long serialVersionUID = 1L;
protected Map<Class<?>, MutableConfigOverride> _overrides;
protected JsonInclude.Value _defaultInclusion;
protected JsonSetter.Value _defaultSetterInfo;
protected VisibilityChecker<?> _visibilityChecker;
protected Boolean _defaultMergeable;
protected Boolean _defaultLeniency;
public ConfigOverrides() {
this((Map)null, Value.empty(), com.fasterxml.jackson.annotation.JsonSetter.Value.empty(), Std.defaultInstance(), (Boolean)null, (Boolean)null);
}
ConfigOverrides에서 Std.defaultInstance()를 호출한다.
VisibilityChecker.class
public interface VisibilityChecker<T extends VisibilityChecker<T>> {
//...
public static Std defaultInstance() {
return DEFAULT;
}
//constructor
public Std(JsonAutoDetect.Visibility getter, JsonAutoDetect.Visibility isGetter, JsonAutoDetect.Visibility setter, JsonAutoDetect.Visibility creator, JsonAutoDetect.Visibility field) {
this._getterMinLevel = getter;
this._isGetterMinLevel = isGetter;
this._setterMinLevel = setter;
this._creatorMinLevel = creator;
this._fieldMinLevel = field;
}
//default로 설정된 Visibility level
static {
DEFAULT = new Std(Visibility.PUBLIC_ONLY, Visibility.PUBLIC_ONLY, Visibility.ANY, Visibility.ANY, Visibility.PUBLIC_ONLY);
ALL_PUBLIC = new Std(Visibility.PUBLIC_ONLY, Visibility.PUBLIC_ONLY, Visibility.PUBLIC_ONLY, Visibility.PUBLIC_ONLY, Visibility.PUBLIC_ONLY);
}
default로 설정된 Visibility를 볼 수 있다. getter, isgetter, field의 경우 PUBLIC_ONLY, setter,creator일 경우 ANY이다.
위 조건 중 하나라도 만족하는 것이 있어야 객체를 직렬화할 수 있다.
field를 읽어오는 default 접근 제어자는 PUBLIC_ONLY이다. 객체 필드에 직접 접근하는 것을 방지하기 위해 private 접근 제어자를 설정하는 것처럼 Jackson도 그 의도에 맞는 정책을 가지고 있는 것 같다.
2.3 getter를 추가할 수 없을 경우, private 필드를 직렬화하려면?
소스를 수정하는 것이 불가능할 경우(ex. 외부 라이브러리 사용)를 대비해 Jackson은 대체 방안을 제공하고 있다.
2.3.1 모든 접근 제한자(private포함)에 대해 필드를 탐지하도록 ObjectMapper의 설정을 변경
@Test
void givenDtoWithOnlyPrivateFields_whenSerializingWithAnyVisibility_thenNoException() throws JsonProcessingException {
//given
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
RecipeResponseDto dto = new RecipeResponseDto(Recipe.builder().title("title").content("test123").build());
//when
String dtoAsString = objectMapper.writeValueAsString(dto);
//then
assertThat(dtoAsString, containsString("title"));
assertThat(dtoAsString, containsString("content"));
//passed
}
2.3.2 클래스 레벨에서 모든 필드가 탐지되도록 설정 - @JsonAutoDetect
@JsonAutoDetect를 사용해 클래스 별로 Visibility를 ANY로 설정할 수 있다.
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class RecipeResponseDto {
private Long id;
private String title;
@Test
void givenDtoWithVisibilityAny_whenSerializingWithDefault_thenNoException() throws JsonProcessingException {
//given
ObjectMapper objectMapper = new ObjectMapper();
RecipeResponseDto dto = new RecipeResponseDto(Recipe.builder().title("title").content("test123").build());
//when
String dtoAsString = objectMapper.writeValueAsString(dto);
//then
assertThat(dtoAsString, containsString("title"));
assertThat(dtoAsString, containsString("content"));
//passed
}
3. 결론
데이터를 파일로 저장하거나, 다른 네트워크로 전송하거나, 데이터베이스에 저장할 경우 등 데이터를 주고 받을 경우 데이터는 바이트 형태로 직렬화되어야 한다. 스프링 부트 내부의 Jackson은 객체 데이터를 직렬화한 후, 사람이 알아보기 쉬운 JSON 형태로 결과값을 만들어준다. Jackson의 기본 정책은 private 필드에 대해 접근이 불가능하기 때문에 getter를 추가하여 getter를 통해 접근하거나, Visibility 권한 레벨을 ANY로 변경하여 private 필드에 직접 접근하도록 하는 방법이 있다.
4. 참고
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.json
https://www.baeldung.com/jackson-jsonmappingexception
* 잘못된 부분이 있다면 지적해주시면 감사하겠습니다!
'Spring' 카테고리의 다른 글
ShedLock 사용하기 (2) | 2021.05.11 |
---|