Jackson 라이브러리를 활용한 Map to Json Serialization 처리
실로 오랜만에 포스팅이다. 역시나 바쁘다는 핑계로 이래저래 미루기만하면 할 수 있는건 아무것도 없는 것 같다.
나는 현재 성능 개선과 관련된 사이드(?) 프로젝트를 진행중이다. 원래 하던 일은 훌륭한 부사수님께서 잘 처리해 주시니 오히려 본업이 되버린 듯한 인상이다.
업무용 문서뷰어라고 하는 표현할 수 있는 놈인데 말 그대로 여러가지 상황에서 만들어진 아주 갖가지 문서를 한곳에서 간편하게 조회하기 위한 용도로 만들어진 프로그램이다.
간단하게는 그냥 문서정보를 DB에서 읽어와서 PDF 경로의 파일을 보여주는 프로그램이다.(물론 PDF를 페이징 단위로 스트리밍해서 보여준다거나 그 뒷단의 아키텍처는 꽤 복잡하긴 하지만..)
처음에는 문서 조회라는 하나의 요구사항에서 출발했지만 점점 더 하나씩 하나씩 필요한 기능이 추가되면서 본연의 핵심 기능은 부가 기능이 되어버리고 여러해 유지/보수 되면서 검증 안된 코드들이 덕지덕지 붙어 성능상에 많은 문제점이 노출되어 원래 과업은 뒤로 한채 ㅎㅎ 성능 쪽 이슈해결에만 요새 주로 많은 시간을 할당 하고 있다.
PDF뷰어나 기타 다른 솔루션도 하드하게 사용하기 때문에 Client/Server 구조로 client
는 .Net WinForm
기반으로 Server
는 Java
기반으로 구성되어 있다.
문서뷰어는 처음 로딩이 될 때 문서목록을 서버에서 가져오는데 HTTP Request
를 통해 json 타입의 결과 값을 가져와 C# 오브젝트로 변환해 사용하고 있는데 문서 목록이 많지 않는 경우에는 크게 상관없으나 프로그램의 특성상 페이징 처리가 쉽지가 않고 반드시 전체 목록을 가져와서 처리를 해야 부분이 있어 대용량 문서목록이 있는 경우 심각한 성능저하 현상이 발생되어 해당 부분을 집중적으로 개선하고 있다.
시간이 되면 이번 개선결과들은 정리해서 공유하고픈 내용은 다른 포스트를 통해 차근차근 정래해 볼 생각이다.
주제는 Jackson 라이브러리 사용인데 서두가 너무 길었다.
걸려도 너무 오래 걸리네
문서 목록이 너무 많아져서 한번에 가져와야 할 데이터가 너무 많아 졌다. 모니터링 하기 위한 StopWatch
정보를 보고 개선작업을 정리하던 중...
*** task running time(mills) = 223398
----------------------------------------
ms % Task name
----------------------------------------
00012 003% 초기화
00012 003% ** 정보 조회
23742 018% ** 정보 조회
...
17406 004% json 변환
목록 가져오는데 223초.. 심각한 상황이다. 비즈니스 로직도 정리하고 sql 튜닝도 하고 여러가지 각도로 개선 작업 중
json 변환하는데 17초??
기존에 java로 되어 있는 Server
쪽 모듈은 그 옛날 EJB 2.1
기반의 해당 기관에서 오랫동안 사용되던 프레임워크로 되어 있는데 워낙 시스템이 많다 보니 일일이 Value Object
나 DTO
를 따로 작성하지 않고 HashMap
에 담아서 시스템 간 인터페이스를 하고 있어서 문서 목록이라는 놈은 HashMap
에 담겨서 다시 json String으로 직렬화하고 write하는데 17초가 걸리고 있었던 것이다.
특정 문서집합의 목록의 수가 20,000건이 넘어가고 하나의 row가 대충 컬럼이 30개 정도 되니 HashMap
오브젝트를 Json String
으로 변환하는데 20초씩 걸려버리는 현상이 나타났다.
개선 작업 실시
일단 json 변환 쪽을 보니 HashMap
을 String
으로 직렬화 하는데 JSON-java라는 라이브러리를 사용중이었고 버전도 상당히 낮은 버전이어서 먼저 다른 json 라이브러로 교체해서 테스트 해보았다.
그나마 다행이건 java 버전을 1.4에서 1.6으로 작년에 겨우 올라갔다는것...
가장 많이들 사용하는 json 처리 관련 java 라이브러리는 다음과 같다.
- FasterXML의 Jackson
- Google의 Gson
- Yidong Fang의 JSON.simple
takipi의 블로그에서 퍼포먼스 벤치마크 정보가 도움이 되었고 셋다 테스트 해보았지만 성능면에서는 Jackson이 가장 좋은 결과를 내여 테스트를 진행했다.
HashMap Serialization
Jackson
을 활용하여 HashMap to json string은 매우 간단하게 사용할 수 있다.
간단하게 HashMap
을 하나 생성하고
Map map = new HashMap<String, String>();
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
ObjectMapper
를 사용하여 String
으로 Serialization
이 가능하다.
ObjectMapper mapper = new ObjectMapper();
mapper.writeValueAsString(map);
// 혹은 아름답게 들여쓰기가 들어가 있는걸 보고 싶다면
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(map);
일단 개발서버 기준으로 라이브러리 교체만으로 20초대에서 10초 정도로 줄이는데 성공~
그래도 여전히 긴 시간 하염없이 화면이 나오기를 기다리는건 매한가지 성능향상에 도움 될만한게 없는지 구글링 중 Jackson
의 모듈 중 jackson-module-afterburner를 발견했다.
mapper.registerModule(new AfterburnerModule());
해당 모듈은 위와 같이 사용 가능하고, 직렬화하려는 POJO
의 필드 접근을 위해 최적화 되고, 데이터 바인딩 오버헤드를 최소화하게 해주는 모듈인 것 같다. (자세한 내용은 링크 참조)
이래저래 더 속도는 줄였지만 사실 만들어 내는 json string이 30메가에 육박하니 http로 다운로드 받는 속도도 무시못해서 일단 null이거나 공백으로 채워진 필드들은 제거하고 보내기로 결정했다.
어차피 C# Client에서는 VO가 준비되어 있고 Json.NET을 통해 json 데이터를 C# 오브젝트로 직렬화 하는데 json에서 필드가 없는건 직렬화 시 공백처리해 두니까 굳이 없는 값을 보낼 필요가 없어서 해당 작업을 실시하기로 했다.
null
이나 공백인 문자열로 값이 구성된 필드의 제거는 다음과 같이 옵션이나 Annotation
으로 처리가 가능하다.
mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_EMPTY);
// Annotation으로 처리 하는 경우
@JsonSerialize(include=JsonSerialize.Inclusion.NON_EMPTY) // or Include.NON_NULL
public class MyBean {
// ... only serialize properties with values other than what they default to
}
자 이제 짜자잔 테스트 해볼 차례
Map map = new HashMap<String, String>();
map.put("key1", "value1");
map.put("key2", null);
map.put("key3", "");
map.put("key4", "value4");
ObjectMapper mapper = new ObjectMapper();
mapper.writeValueAsString(map);
mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_EMPTY);
mapper.writerWithDefaultPrettyPrinter().writeValue(System.out, map));
// 결과
{
"key4" : "value4",
"key3" : "",
"key2" : null,
"key1" : "value1"
}
뭐지 장난하나??
혹시니 해서 테스트 해보니 일반 POJO 클래스는 해당 옵션이 아주 잘 먹는데 Map
은 먹지 않는다. 다른 옵션을 검색해보니
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false)
SerializationFeature.WRITE_NULL_MAP_VALUES
옵션 처리 하면 다음과 같이 json
으로 변환된다. null
인 경우 처리는 해주는 것 같지만 여전히 공백으로 할당된 필드는 여전히 그대로 보인다.
{
"key4" : "value4",
"key3" : "",
"key1" : "value1"
}
결국 또 구굴링 끝에 Map
에서 빈 공백문자열 value 처리는 Custom Module
로 처리해야 한다는 결론에 다다랐다. 상단에 적용한 afterburnerModule
처럼 Custom Module
을 작성 했다.
SimpleModule simpleModule = new SimpleModule("emptyFilter", new Version(1, 0, 0, null, null, null));
simpleModule.addSerializer(Map.class, new JsonSerializer<Map>() {
@Override
public void serialize(Map map, JsonGenerator jp, SerializerProvider sp) throws IOException, JsonProcessingException {
jp.writeStartObject();
for (Object key : map.keySet()) {
if (!"".equals((String)map.get(key)) && map.get(key) != null) {
jp.writeStringField((String)key, (String)map.get(key));
}
}
jp.writeEndObject();
}
});
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new AfterburnerModule());
mapper.registerModule(simpleModule);
mapper.setSerializationInclusion(Include.NON_EMPTY);
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
mapper.writerWithDefaultPrettyPrinter().writeValue(System.out, map));
이제 결과를 확인해 보면
{
"key4" : "value4",
"key1" : "value1"
}
제대로 null
과 공백 문자열을 제외하고 json을 생성하는 것을 확인해 볼 수 있다.
DefaultSerializerProvider.serializeValue 메소드 내용을 참조하면 응용해서 Map 안에 Map이 들어간 케이스라던지 응용이 가능할 것 같다.
항상 아이처럼 정성스럽게
이번 성능 개선 건을 진행하면서 느낀점은 소프트웨어는 아이와 같아서 항상 아껴주고 신경써주지 않는 다면 금세 갈 길을 잃어 버리고 방황에 빠지게 마련이라는 것이다.
항상 귀찮다고 별 생각 없이 추가한 코드 한줄이 항상 운영 중에 큰 문제를 일으키는 다반사요 이번건 처럼 성능상에도 큰 문제를 일으킬 수 있다.
코딩은 정성이다
항상 신입사원들에게 해주던 말이다. 나조차 그들보다 뛰어난 코딩 실력을 가지고 있진 않지만 잘하고 못하고를 떠나서 먼저 정성이 있다면 발전 가능성이 무한히 열려 있지 않나 생각해본다. 이번 기회에 다시 반성해 본다.
참고 URL
https://github.com/FasterXML/jackson
https://github.com/google/gson
https://github.com/fangyidong/json-simple
http://blog.takipi.com/the-ultimate-json-library-json-simple-vs-gson-vs-jackson-vs-json/
http://www.davismol.net/2015/05/18/jackson-create-and-register-a-custom-json-serializer-with-stdserializer-and-simplemodule-classes/