프로젝트에 객체를 Map<String, String>으로 바꿔 주는 유틸 메서드가 있었다. 열어 보니 Reflection으로 필드를 하나씩 훑고 있었다. 동작은 한다. 다만 Jackson이 이미 의존성에 있는데 같은 일을 손으로 하고 있었다.
이 글에서는 기존 메서드가 무엇을 하는지 보고, ObjectMapper로 바꾼 코드와 바꿀 때 챙길 점을 정리한다.
기존 메서드는 선언된 필드를 전부 연다
public <T> Map<String, String> convertToMap(T object) {
Map<String, String> map = new HashMap<>();
try {
Class<?> clazz = object.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String fieldName = field.getName();
Object value = field.get(object);
if (value != null) {
map.put(fieldName, value.toString());
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return map;
}
흐름은 세 단계다.
getDeclaredFields()로 필드 목록을 가져온다.getDeclaredXXX()계열은 상속받은 클래스와 인터페이스를 빼고 그 클래스에 직접 정의된 요소만 가져온다. 대신 접근 제어자는 가리지 않아서 private, protected, public 필드가 전부 나온다.필드마다
setAccessible(true)로 접근 제한을 풀고, 값을 읽어 null이 아니면toString()결과를 Map에 넣는다.접근할 수 없는 필드를 읽으면
IllegalAccessException이 난다. 이 메서드는 그걸printStackTrace()로 찍고 넘어간다.
마지막 줄이 걸린다. 예외가 나면 호출한 쪽은 그때까지 채워진 Map을 받을 뿐, 변환이 실패했는지 알 방법이 없다.
ObjectMapper로 바꾸면 한 줄이다
public <T> Map<String, String> convertToMap(T object) {
ObjectMapper mapper = new ObjectMapper();
return mapper.convertValue(object, new TypeReference<Map<String, String>>() {});
}
Jackson의 ObjectMapper는 JSON을 다룰 때 쓰는 도구지만 convertValue로 객체를 다른 타입으로 바로 옮길 수 있다. 필드 순회, 접근 권한, 예외 처리를 전부 Jackson에 넘긴다. 변환에 실패하면 IllegalArgumentException이 올라오니 삼켜지는 예외도 사라진다.
TypeReference는 결과 타입을 명시하는 장치다. Java 제네릭은 런타임에 지워지기 때문에 Map.class만 넘기면 Jackson은 값 타입을 모른다. new TypeReference<Map<String, String>>() {}로 넘기면 값을 String으로 맞춰 담는다.
바꿀 때 챙길 점
성능은 원래 이 리팩토링의 이유 중 하나였다. Jackson도 내부적으로는 Reflection을 쓰지만, 클래스를 한 번 분석한 결과를 ObjectMapper 인스턴스에 캐싱해 두고 재사용한다. ObjectMapper를 재사용해야 캐싱 효과가 난다. 위 코드처럼 호출마다 new ObjectMapper()를 만들면 매번 처음부터 분석한다. 필드로 한 번만 만들어 두거나, Spring Boot라면 이미 등록된 ObjectMapper Bean을 주입받는 편이 낫다.
두 메서드가 돌려주는 Map도 같지 않다.
기존 메서드는 선언된 필드를 읽고, Jackson은 기본 설정에서 getter와 public 필드를 읽는다. getter 없는 private 필드는 빠진다.
null 값을 기존 메서드는 건너뛰었고, Jackson은 기본적으로 키를 남긴 채 값에 null을 넣는다.
상속받은 필드는 기존 메서드가 읽지 않았다. Jackson은 부모 클래스의 getter도 읽는다.
호출하는 쪽이 이 차이에 기대고 있지 않은지 한 번 보고 바꾸는 게 안전하다.