초기 개발 과정 ①
앞선 두 편에서는 개발보다는 리서치를 바탕으로 한 정보 획득과 논의 중심의 도입 배경, 그리고 비교 분석을 통한 최종 서비스 선택까지의 일련의 과정을 다뤘는데요. 이제부터는 실제 서비스 구현을 위한 구체적인 개발 과정에 대해 이야기해 볼 예정입니다. 먼저 이번 3편에서는 1편에서 잠시 보여드렸던 투덥 시스템의 구조와 동작, 각 동작에 대한 구현 관련 내용을 조금 더 상세히 말씀드려볼게요!
투덥의 데이터는 기본적으로 메인 RDB를 중심으로 관리되어 GDB에서는 소셜 시스템 관련 부분만 구현하면 되었기 때문에 소셜 기능에 필요한 요소들로만 모아서 구성하였습니다. 초기 유저 데이터를 중심으로 유저들 간의 팔로우 관계와 팔로우 관계 기반의 메시지 탐색에 초점을 맞춰 2개의 Node(User, Message)와 4개의 Relationship(FOLLOW, SEND, INVITE, READ)으로 구성했습니다.
최초 데이터는 RDB를 기반으로 Import 하고, 그 뒤에는 각 요소의 생성 및 변경 처리에 GDB 쿼리도 추가하는 형태로 진행했습니다. 대량의 데이터는 일반 쿼리로 하나하나 입력할 경우 시간이 오래 걸리기 때문에 csv파일로 만들어 load하는 방법을 사용하였습니다. Neo4j는 csv파일에 대한 url 경로도 허용했기에 저희 팀은 url 기반 접근 형태로 사용하였고 csv의 column 값을 바탕으로 Property(RDB의 column value 해당)를 채우도록 하였습니다.
Node는 아래와 같은 방법으로 입력하였습니다.
Relationship은 아래와 같은 방법으로 입력하였습니다.
속성 명을 csv의 컬럼 명과 다르게 넣거나 별도의 처리를 하지 않고 그대로 사용할 수 있도록 csv를 구성하면 아래와 같은 형태로 Node 별 속성 쿼리를 다 작성하지 않아도 되기 때문에, 여러가지 데이터를 넣어야 하는 경우에 Label 및 csv 파일 경로만 바꾸어서 사용할 수 있습니다.
그런데 이 과정에서 하나의 문제가 발생합니다. Node는 정상적으로 입력되었으나 Relationship을 입력할 때 타임아웃이 발생한 것이죠. 이는 Relationship이 Node와는 다르게 기준이 되는 Node를 찾는 쿼리가 포함되어 있기 때문에 해당 Node를 찾는 과정에서 모든 Node의 속성을 비교함에 따라 일정 시간 이상이 소요되어 되어 발생하게 된 것이었습니다.
물론 이 부분은 쿼리 상단 PERIODIC COMMIT의 사이즈를 줄여서 피할 수 있었지만 전체적인 처리 시간이 Node에 비해 더 많은 소요되고, 또 일반적으로 Relationship이 Node보다 많은 수를 가지기 때문에 최선의 방법은 아니라고 판단했습니다. 이에 저희 팀은 GDB에서 제공하는 Index를 활용해 해결하는 방법을 선택했습니다.
GDB도 RDB와 마찬가지로 탐색 성능을 높이기 위해 RDB의 key 설정과 같은 Index 기능을 갖고 있습니다. Index는 아래와 같이 설정할 수 있는데, 저희는 여기에 추가적으로 필요한 더 필요한 부분이 있었기 때문에 Constraint로 설정했습니다.
Constraint는 탐색 성능을 올리는 작업 외에도 RDB의 Unique Key처럼 특정 속성에 Unique 설정 등을 함으로써 중복 값이 들어가지 않도록 설정할 수 있습니다. 또한, Neo4j의 경우 Constraint 설정 시 Index가 자동으로 설정되어 별도의 Index 설정을 하지 않더라도 해당 속성에 대한 빠른 탐색까지 가능해집니다.
데이터 입력 전에 미리 Constraint 설정을 해둔다면 입력 과정에서 실수로 발생할 수 있는 중복 데이터 생성 오류를 피할 수 있기 때문에 모든 데이터 입력 전에 미리 Constraint를 설정하는 것을 추천합니다.
지금부터는 실질적으로 그래프 데이터베이스를 사용하여 구현된 투덥 소셜 시스템의 동작들 중 대표적인 것들을 쿼리와 함께 정리해 보여드리겠습니다. 기본적인 Node 및 Relationship 생성은 공식문서에서도 확인 가능하지만, 저희 투덥 시스템에서 구현한 관계 및 메시지 탐색을 위해 사용된 부분들을 참고해 보는 것 역시 좋은 공부가 되시리라 생각합니다. 이에 조금 더 쉽고 직관적으로 이해하실 수 있게끔 각 항목 별로 기본 쿼리들을 그림과 함께 정리했습니다.
※ 항목 제목의 패턴 표기는 Cypher 쿼리의 표기 방법에 따라 Node는 (:Label)의 형태로, Relationship은 [:Type]의 형태 및 Relationship의 방향을 화살표로 표기했습니다.
팔로잉 유저 목록 (:User)-[:FOLLOW]→(:User)
(FOLLOW의 방향만 반대로 바꾸면 팔로워 목록을 얻을 수 있습니다.)
내 팔로우 들의 Out Message 목록 : (:User)-[:FOLLOW]→(:User)-[:SEND]→(:Message)
새로운 메시지 수
[소소한 팁]
1) count() 사용
개별 목록을 Return 하는 형태 보다 count()를 사용하여 개수만 Return 하는 것이 훨씬 빠른 시간 안에 결과를 주기 때문에 개별 항목이 필요하지 않은 경우에서는 count()를 사용했습니다.
2) WHERE 절 활용
Neo4j는 WHERE 절에 단순히 속성 값에 대한 조건 말고도 데이터 요소의 Label/Type이나 Neo4j 내부 ID에 대한 조건 및 패턴 형태로도 입력이 가능했기 때문에 원하는 형태의 데이터를 얻기 위한 쿼리는 대부분 만들어 낼 수 있었습니다.
3) OPTIONAL MATCH의 사용
MATCH와 OPTIONAL MATCH의 차이점은 '결과가 null이 될 수 있는가'입니다. 위의 쿼리에서 OPTIONAL MATCH 절에 써진 구문을 그대로 MATCH 절에 옮겨 붙여 넣어도 리턴할 데이터가 있는 경우에는 같은 결과를 얻을 수 있습니다만, 팔로우 유저나 내가 받아야 하는 메시지가 항상 모든 유저에게 존재해야 하는 필수 요소는 아니므로 MATCH를 사용하게 되면 해당 데이터가 없는 경우에 Query Error가 발생하게 됩니다. 그래서 OPTIONAL MATCH를 사용하여 없는 경우에 결과를 null로 리턴할 수 있도록 OPTIONAL MATCH를 사용했습니다.
위 내용들을 통해 투덥 소셜 시스템의 데이터 입력과 기본 쿼리 구성에 대해 잘 확인하셨으리라 생각합니다. 이어지는 4편에서는 복잡했던 쿼리와 QA 과정을 거치며 추가된 요소, 그리고 RDB와 달라서 힘들었던 부분들에 대해 설명드리도록 하겠습니다 :D