최근 프로젝트를 진행하면서 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); | |
} | |
} |
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 | |
나는 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); | |
} | |
} |
이를 구현한 ParamMap과 JsonMap 두 가지 클래스를 설계했다. ParamMap은 내부적으로 HashMap을 래핑하고 있으며, 일반 Map보다 약간 더 편하게 사용할 수 있도록 도와준다. 반면 JsonMap은 ObjectNode를 래핑 한 구조로 Json을 Map처럼 다루기 위한 구현체다.
( ParamMap은 단순한 Map 래퍼이므로 path 형식 접근은 지원하지 않는다. path 접근이 필요한 경우에는 jsonMap() 메서드를 통해 JsonMap으로 변환해서 사용하면 됨. )
참고로 JsonMap 내부 값들은 JsonNode로 변환되기 때문에 일반 Map에 비해 약간의 오버헤드가 존재할 수 있다. 하지만 대용량 데이터를 다루는 게 아니라면 JsonMap을 사용하는 것도 큰 문제는 없어 보인다. -_-y