Gremlin의 by()

최근 개발 중인 프로젝트에 Graph DB를 사용하게 되면서, Gremlin의 문법을 차근차근 배우고 있다. Gremlin은 Apache Tinkerpop 프레임워크에서 그래프를 순회하기 위해 사용되는 언어로, Neo4j와 OrientDB, JanusGraph 등 다양한 GDB에서 지원되고 있다.

초록색 캐릭터... 귀여운 게 최고야

Gremlin으로 작성된 쿼리는 우선 Vertex 혹은 Edge를 선택한 뒤, 메서드를 여러 개 이어 붙이면서 데이터를 조작하는 방식으로 쓰인다. 따라서 SQL과는 그 작동 방식이 많이 다르다.

가령 그래프 DB에는 보통 다양한 형식의 데이터가 들어가기 때문에, Vertex와 Edge에 Label을 붙여 데이터를 구분한다. 즉 부서 정보와 직원 정보를 Vertex, 부서와 직원의 소속 관계를 Edge로 저장한 그래프가 있다면, 부서 정보에는 ‘Department’, 직원 정보에는 ‘Employee’라는 Label을 붙여 두 데이터를 구분할 것이다.

이때 Label마다 Vertex가 몇 개 있는지 알고 싶다면, 우선 모든 Vertex를 가져온 뒤...

g.V()
==>v[1]
==>v[2]
==>v[3]
==>v[4]
==>v[5]

Label에 따라 Vertex를 묶어준다.

g.V().group().by(label)
==>[department:[v[1],v[3]],employee:[v[2],v[4],v[5]]]

개수를 세고, 값으로 적용한다.

g.V().group().by(label).by(count())
==>[department:2,employee:3]

음... group().by().by()…?

또한 부서별로 직원이 몇 명 있는지 알고 싶다면, 우선 부서 라벨이 붙은 Vertex를 가져온다.

g.V().hasLabel(’department’)
==>v[1]
==>v[3]

주루룩 나오는 Vertex들을 이름으로 묶어준다.

g.V().hasLabel(’department’).group().by(’name’)
==>[marketing:[v[1]],programming:[v[3]]]

그리고 (나가는 방향) Edge를 한 번 넘어가서 만날 수 있는 Vertex를 탐색한 뒤...

g.V().hasLabel(’department’).group().by(’name’).by(out())
==>[marketing:[v[2]],programming:[v[4],v[5]]]

개수를 세고, 값으로 적용한다.

g.V().hasLabel(’department’).group().by(’name’).by(out().count())
==>[marketing:1,programming:2]

역시 group().by().by()라는 기묘한 문법과 마주치게 된다. 첫 번째 by()는 SQL의 GROUP BY 처럼 의미를 쉽게 알 수 있지만, 두 번째 by()는 문법적으로 맞지 않는 느낌을 준다. SQL이라면 SELECT 뒤에 와야 할 것들이 by()로 붙여지는 셈이다.

Gremlin에서 by()의 역할은 무엇일까? Tinkerpop3 Documentation에서는 By Step에 대해서 이렇게 설명하고 있다.

by() Step은 실제 Step이라기보다, as()option()과 같은 "Step-Modulator"에 가깝습니다. […] 아래의 Step은 모두 by()-modulation을 지원합니다. 이런 modulation의 의미는 Step 수준에서 이해되어야 하므로, 각 Step별 섹션에서 논하고 있다는 점을 참고하세요.

Gremlin에서 has()group()같은 메서드들은 그래프 순회 과정에서 하나의 '단계(Step)'를 나타낸다고 할 수 있다. 그 중에서 group()이나 select()같은 특정 단계는 동작 방식을 바꿀(modulate) 수 있도록 뒤에 modulator를 붙이는 것을 허용하는데, by()가 그 역할을 한다는 것이다.

따라서 by()는 그 앞에 어떤 메서드가 오는지에 따라 다른 맥락으로 쓰이게 된다. 예를 들어 path()는 다음와 같이 그래프 순회 경로를 확인할 때 사용되는데,

g.V().has('department', 'name', 'programming').out().path()
==>[v[3],v[4]]
==>[v[3],v[5]]

뒤에 by('name')을 붙이면 Vertex의 이름을 대신 표시할 수 있다. 이렇게 by()는 Vertex의 어떤 속성을 대신 표시할 때 주로 쓰이는데, Documentation에서는 이를 'Projection'이라고 한다.

g.V().has('department', 'name', 'programming').out().path().by('name')
==>[programming,Mary]
==>[programming,Kevin]

그렇다면 group().by().by()는 무엇일까? 공식 문서를 다시 참고하면...

group()에는 by()를 사용하여 두 개의 Projection 파라미터를 전달할 수 있습니다.
1. Key-projection: 어떤 속성으로 묶을까요(맵 키를 반환하는 함수)?
2. Value-projection: 해당 키의 배열에 어떤 속성을 저장할까요?

즉 첫 번째 by(), 두 번째 by()을 담당하는 셈이다. 따라서 Vertex를 Label로 묶고, 개수를 세어 값으로 저장하려면 이렇게 쓰고,

g.V().group().by(label).by(count())

Vertex를 이름으로 묶고, 해당 Vertex에서 (나가는 방향) Edge를 한 번 넘어가서 만날 수 있는 Vertex의 개수를 세어 값으로 저장하려면 이렇게 쓴다.

g.V().group().by('name').by(out().count())

만능 보조사 같다는 느낌이 들기도 한다. 파라미터로 전달하지 않는 것은 왜일까. group().by().by()를 만들고 혹여나 후회하지는 않았을까...?

궁금한 것이 많지만 Tinkerpop Documentation은 모든 내용을 한꺼번에 로드해 그 높이가 254,743px에 다다르므로 나중에 알아보기로 했다.

Gremlin 공부에는 메모리가 필요하다. 여러모로....