본문 바로가기

Java

Map과 Json..?

최근 프로젝트를 진행하면서 Json 형태의 데이터를 저장하고 조회해야 하는 일이 생겼다.

( 만약 NoSQL을 사용했다면 더 쉬웠을까? 하지만 사용이 불가능한 상황 =_=;; )

Json 문자열로 변환도 해보고, 객체로 직렬화도 해보고, 여러 가지 시도 끝에 결국 Map이 가장 잘 맞는다고 판단해서 Map을 이용하기로 결정했다.

 

간단한 예제로 설명하자면, persons라는 배열 안에 여러 개의 객체가 있고, 각 객체마다 friends라는 key가 존재한다. 나는 이 friends 값들만 모두 뽑아서 합치고 싶었다.

{
"persons" : [ {
"name" : "muheun",
"age" : 20,
"friends" : [ "friend1", "friend2", "friend3" ]
}, {
"name" : "test",
"age" : 10,
"friends" : [ "muheun", "test2", "test3" ]
} ]
}

뭐, persons 배열을 순회하면서 각 객체에서 friends 키의 값을 꺼내고, 이들을 모으면 원하는 결과를 얻을 수 있다. 하지만 더 복잡한 구조의 Json이라면..?!

 

그래서 나는 path 기반 접근 방식을 사용해 friends 값들을 쉽게 추출하는 방법 ( JsonNode의 at() 기능 사용 )을 구현하고자 했다.

class IMapTest {
@Test
void jsonPathTest() {
Params<Object> persons = Params.obj("persons", getPersonList());
System.out.println(persons.jsonString(true)); // json pretty 출력
assertNull(persons.get("persons[].friends[]"));
Object obj = persons.jsonMap().get("persons[].friends[]"); // 같음 <=> person.*.friends.*
assertInstanceOf(List.class, obj);
assertEquals(6, ((List<Object>) obj).size());
assertEquals("muheun", persons.jsonMap().get("persons[1].friends[0]"));
Debug.json(obj);
Debug.json(persons.get("persons[1].friends[0]"));
}
private static List<Person> getPersonList() {
Person p1 = new Person("muheun", 20);
p1.setFriends(Arrays.asList("friend1", "friend2", "friend3"));
Person p2 = new Person("test", 10);
p2.setFriends(Arrays.asList("muheun", "test2", "test3"));
return Arrays.asList(p1, p2);
}
}
view raw IMapTest.java hosted with ❤ by GitHub

persons는 배열이므로 대괄호([])를 이용해 표현했고, 필요하다면 인덱스를 통해 각 요소에 접근할 수도 있다.

 

테스트한 내용은 다음과 같다.

  • Params는 단순한 Map 래퍼이므로 path 기반 접근은 지원하지 않는다.
    • 따라서 persons.get("persons[].friends[]")는 null을 반환해야 한다.
  • jsonMap()으로 변환 후에는 path 기반 접근이 가능해진다.
    • "persons[].friends[]" 경로로 접근한 값은 List 타입이어야 한다.
  • friends 항목은 총 6개가 존재해야 하므로, List의 크기는 6이어야 한다.
  • 명확한 인덱스를 사용하면 특정 요소에 직접 접근 가능해야 한다.
    • persons[1].friends[0] == "muheun"

검증 이후 출력은,,

JSON => [ "friend1", "friend2", "friend3", "muheun", "test2", "test3" ] -> java.util.ArrayList
JSON => {} -> null
view raw print.out hosted with ❤ by GitHub

나는 Json을 최대한 Map처럼 다루고 싶었다. 그래서 Map 기능을 확장한 Params이라는 인터페이스를 만들었고,

( Params를 사용하되 내부적으로 ParamMap을 사용하는지 JsonMap을 사용하는지 사용하는 입장에서는 알 필요가 없다. 기본적으로 ParamMap을 사용하게끔 설계함. )

인터페이스 보기
public interface Params<T> extends Map<String, T>, Serializable {
int NUMBER_DEFAULT_VALUE = -1;
String STRING_DEFAULT_VALUE = "";
default String getString(String key) {
return getString(key, STRING_DEFAULT_VALUE);
}
String getString(String key, String defaultValue);
...
Params<T> putParam(String key, T value);
Params<T> putParams(Map<String, T> m);
String jsonString();
String jsonString(boolean isPretty);
Map<String, T> toMap();
ParamMap<Object> params();
JsonMap jsonMap();
static Params<Object> obj() {
return ParamMap.obj();
}
static Params<Object> obj(String key, Object value) {
return obj().putParam(key, value);
}
static Params<String> str() {
return ParamMap.str();
}
static Params<String> str(String key, String value) {
return str().putParam(key, value);
}
}
view raw IMap.java hosted with ❤ by GitHub

이를 구현한 ParamMap과 JsonMap 두 가지 클래스를 설계했다. ParamMap은 내부적으로 HashMap을 래핑하고 있으며, 일반 Map보다 약간 더 편하게 사용할 수 있도록 도와준다. 반면 JsonMap은 ObjectNode를 래핑 한 구조로 Json을 Map처럼 다루기 위한 구현체다.

 

( ParamMap은 단순한 Map 래퍼이므로 path 형식 접근은 지원하지 않는다. path 접근이 필요한 경우에는 jsonMap() 메서드를 통해 JsonMap으로 변환해서 사용하면 됨. )

 

참고로 JsonMap 내부 값들은 JsonNode로 변환되기 때문에 일반 Map에 비해 약간의 오버헤드가 존재할 수 있다. 하지만 대용량 데이터를 다루는 게 아니라면 JsonMap을 사용하는 것도 큰 문제는 없어 보인다. -_-y