응답 객체의 필드 이름을 isSelected(boolean 타입, private, Lombok @Getter 사용)로 지었는데 해당 객체를 사용하는 API 응답 값에서 isSelected 가 아닌 selected 로 출력되는 문제가 있었다.
Jackson(POJOPropertiesCollector) 은 클래스 정보를 조사해서(클래스 파일의 멤버 필드, getter setter 메서드, 생성자 등) 프로퍼티 정보를 저장한다. 그리고 이를 기반으로 직렬화 및 역직렬화 한다. 멤버 필드와 getter 메서드는 접근제어자가 public 인 경우에만 프로터피 정보를 저장하도록 되어있다.(조금 더 자세한 내용은 해결 과정에서 후술) 그러므로 문제 상황에서 멤버 필드의 isSelected 는 프로퍼티로 저장되지 않고 getter 메서드인 isSelected 가 프로퍼티로 저장된다. 이때 getter 메서드의 경우 prefix 를 제거하고 저장하기 때문에 selected 로 출력된 것이다.
해결법에 대한 아이디어는 공식 문서에서 얻을 수 있었다.
기본 ObjectMapper를 완전히 대체하려면 해당 유형의 @Bean을 정의하고 @Primary로 표시하거나 빌더 기반 접근 방식을 선호하는 경우 Jackson2ObjectMapperBuilder @Bean을 정의하십시오. 두 경우 모두 이렇게 하면 ObjectMapper의 모든 자동 구성이 비활성화됩니다.
링크
isSelected 를 출력하기 위해서 두 가지를 설정이 필요하다.
첫 번째 멤버 필드의 접근제어자가 private 인 경우에도 프로퍼티로 저장되도록 해야 할 것,
두 번째 isGetter 메서드의 경우 프로퍼티로 저장되지 않도록 해야 할 것
@Configuration
public class ObjectMapperConfig {
@Bean
@Primary
public ObjectMapper customizeObjectMapper() {
return new ObjectMapper().setVisibility(
VisibilityChecker.Std
.defaultInstance()
// public 만 허용하는 설정에서 private 도 허용하도록 설정
.withFieldVisibility(Visibility.ANY)
).setAccessorNaming(
new DefaultAccessorNamingStrategy
.Provider()
// isGetter 메서드는 조사하지 않도록 설정
// null 을 넣은 이유는 해결과정 2 번의 findNameForIsGetter 메서드 참고
.withIsGetterPrefix(null)
);
}
원인을 파악하기 위해 디버거로 Jackson 라이브러리의 직렬화 과정을 살펴봤고 코드의 흐름을 파악한 뒤 변경해야 할 설정 지점을 찾아냈다.
Jackson 라이브러리에서 프로퍼티 정보를 수집하는 역할은 POJOPropertiesCollector(com.fasterxml.jackson.databind.introspect)가 한다.(collectAll 메서드를 호출해서 수집한다)
public class POJOPropertiesCollector {
// 생략
protected void collectAll() {
LinkedHashMap<String, POJOPropertyBuilder> props = new LinkedHashMap<String, POJOPropertyBuilder>();
// 후보 등록
_addFields(props);
_addMethods(props);
// 생략
// 선별 과정
_removeUnwantedProperties(props);
_removeUnwantedAccessor(props);
// 생략
}
// 생략
protected void _addMethods(Map<String, POJOPropertyBuilder> props) {
for (AnnotatedMethod m : _classDef.memberMethods()) {
int argCount = m.getParameterCount();
if (argCount == 0) {
_addGetterMethod(props, m, _annotationIntrospector);
} else if (argCount == 1) {
_addSetterMethod(props, m, _annotationIntrospector);
} else if (argCount == 2) {
if (Boolean.TRUE.equals(_annotationIntrospector.hasAnySetter(m))) {
if (_anySetters == null) {
_anySetters = new LinkedList<AnnotatedMethod>();
}
_anySetters.add(m);
}
}
}
}
}
public class POJOPropertiesCollector {
// 생략
protected void _addGetterMethod(Map<String, POJOPropertyBuilder> props,
AnnotatedMethod m, AnnotationIntrospector ai) {
// 생략
if (!nameExplicit) {
implName = ai.findImplicitPropertyName(m);
if (implName == null) {
implName = _accessorNaming.findNameForRegularGetter(m, m.getName());
}
if (implName == null) {
implName = _accessorNaming.findNameForIsGetter(m, m.getName());
if (implName == null) {
return;
}
visible = _visibilityChecker.isIsGetterVisible(m);
} else {
visible = _visibilityChecker.isGetterVisible(m);
}
}
// 생략
_property(props, implName).addGetter(m, pn, nameExplicit, visible, ignore);
}
protected POJOPropertyBuilder _property(Map<String, POJOPropertyBuilder> props,
String implName) {
POJOPropertyBuilder prop = props.get(implName);
if (prop == null) {
prop = new POJOPropertyBuilder(_config, _annotationIntrospector, _forSerialization,
PropertyName.construct(implName));
props.put(implName, prop);
}
return prop;
}
}
public class DefaultAccessorNamingStrategy extends AccessorNamingStrategy {
// 생략
@Override
public String findNameForRegularGetter(AnnotatedMethod am, String name) {
if ((_getterPrefix != null) && name.startsWith(_getterPrefix)) {
if ("getCallbacks".equals(name)) {
if (_isCglibGetCallbacks(am)) {
return null;
}
} else if ("getMetaClass".equals(name)) {
if (_isGroovyMetaClassGetter(am)) {
return null;
}
}
return _stdBeanNaming
? stdManglePropertyName(name, _getterPrefix.length())
: legacyManglePropertyName(name, _getterPrefix.length());
}
return null;
}
@Override
public String findNameForIsGetter(AnnotatedMethod am, String name) {
if (_isGetterPrefix != null) {
final Class<?> rt = am.getRawType();
if (rt == Boolean.class || rt == Boolean.TYPE) {
if (name.startsWith(_isGetterPrefix)) {
return _stdBeanNaming
? stdManglePropertyName(name, 2)
: legacyManglePropertyName(name, 2);
}
}
}
return null;
}
}
public class POJOPropertyBuilder extends BeanPropertyDefinition implements Comparable<POJOPropertyBuilder> {
protected final PropertyName _name;
// 사용되지 않는 후보 정보는 null 값이 들어간다.
protected Linked<AnnotatedField> _fields;
protected Linked<AnnotatedMethod> _getters;
// 생략
public void addField(AnnotatedField a, PropertyName name, boolean explName, boolean visible, boolean ignored) {
_fields = new Linked<AnnotatedField>(a, _fields, name, explName, visible, ignored);
}
public void addGetter(AnnotatedMethod a, PropertyName name, boolean explName, boolean visible, boolean ignored) {
_getters = new Linked<AnnotatedMethod>(a, _getters, name, explName, visible, ignored);
}
//생략
}
protected void _removeUnwantedProperties(Map<String, POJOPropertyBuilder> props) {
Iterator<POJOPropertyBuilder> it = props.values().iterator();
while (it.hasNext()) {
POJOPropertyBuilder prop = it.next();
if (!prop.anyVisible()) {
it.remove();
continue;
}
// 생략
}
}