brunch

You can make anything
by writing

C.S.Lewis

by 투미유 Nov 18. 2022

[Tech] 투덥 그래프 데이터베이스 도입기 (6편)

서비스 변경 (Neo4j ⇒ Memgraph)

투미유 팀의 GDB 도입기, 대망의 마지막 편입니다. 직전 편에서 언급드린 대로 이번 6편에서는 투덥 소셜 시스템의 DB를 Neo4j에서 Memgraph로 변경하게 된 이야기를 담으려 합니다. Memgraph를 선택한 이유와 변경 방향, 그리고 그에 따른 장/단점 등을 Memgraph 자체의 특성은 물론, Neo4j와의 차이점을 바탕으로 자세히 설명드리겠습니다 :D






(전편에서 말씀드린) Neo4j 기반에서의 프리징 증상 해결을 위해 다양한 방법을 찾아보았으나 Neo4j Enterprise가 아니고서는 문제 해결이 어려웠습니다. 타 서비스로 바꾸는 선택을 가장 마지막에 시도했던 이유는 GDB는 RDB처럼 Query 언어가 표준화되지 않았기 때문인데, Query 언어의 변경은 곧 기존 코드에 포함된 모든 쿼리를 재작성 해야 함을 의미했고 이는 전체 시스템을 재구축하는 규모의 일이라고 생각하고 있었습니다. 따라서 해당 선택지를 배제한 채 이슈 해결을 위해 노력했지만 생각대로 원활히 이루어지지 않았고, 결국 최후의 수단으로 다른 서비스들을 살펴보게 되면서 (리서치 및 사전 테스트를 거쳐) Memgraph로의 변경을 결정하게 되었습니다.




그렇다면 왜 Memgraph인가?


[본문 내용 요약] 

● Cypher 쿼리를 사용하여 기존 쿼리를 그대로 사용할 수 있기 때문
● Disk가 아닌 Memory 기반으로 동작하여 탐색 성능이 뛰어나기 때문 
● Replica 구성을 기본으로 제공하여 Scale-out을 적용할 수 있기 때문


가장 큰 이유는 Cypher 쿼리를 사용한다는 것이었습니다. 위에서도 얘기했지만 이 부분이 대체 서비스 선택을 최후까지 남겨두게 한 이유였습니다. 하지만, Memgraph의 경우 Neo4j와 같은 Cypher 쿼리를 사용기에 비교적 적은 개발 비용으로도 변경이 가능했습니다.

그리고 두 번째 이유는 In-Memory DB라는 점이었습니다. Neo4j의 경우 Disk 기반으로 동작하는 데 반해 Memory 기반으로 동작하는 Memgraph는 탐색 성능 측면에서 Neo4j보다 뛰어났고 이를 통해 응답시간 및 부하를 줄일 수 있다고 생각했습니다. Memgraph 공식 홈페이지에도 자사 서비스와 Neo4j를 비교하는 부분이 있었는데, 이때 대규모 그래프에 대한 실시간 처리에는 Memgraph를 사용하는 것이 더욱 효과적이라는 설명도 확인했습니다.

마지막으로 Enterprise가 아닌 버전에서도 Replica 구성을 기본으로 제공하고 있어 Neo4j와 같은 이슈 상황에서 Scale-out 처리가 가능해 더욱 안정적인 운영이 가능할 것이라 예측할 수 있었습니다.




사전 테스트


이러한 이유를 바탕으로 Memgraph가 우리의 문제를 해결해 줄 수 있는지를 알아보고자 사전 테스트를 계획했습니다.

테스트는 총 두 가지 측면에서 진행되었습니다. 첫 번째 측면은 기존 이슈를 해결해 줄 수 있는 서비스인지를 확인하기 위한 대용량 처리 및 트래픽 안정성 테스트였고, 다른 측면으로는 변경 작업에 드는 개발 비용을 확인하기 위한 호환성 테스트였습니다. 특히, 테스트의 신뢰도를 높이기 위해서는 저희가 가진 데이터의 규모와 복잡도가 비슷한 수준의 데이터가 들어있어야 했는데, 이러한 데이터를 임의로 만들어내는 것은 쉽지 않기 때문에 시스템의 데이터를 그대로 복사해 진행하기로 했습니다.



초기 구축 및 연결

[본문 내용 요약] 

● Docker 기반의 빠르고 쉬운 초기 설정
● Neo4j와 동일한 Driver 사용 가능


기존 Neo4j의 데이터를 Memgraph에 넣기 위해서 변경하는데 있어 Memgraph 서비스를 실행하고 연결하는 것 까지는 수월했습니다. Memgraph는 Docker를 이용한 설치를 제공했기에 간단히 초기 구축이 가능했고, Nodejs 기반의 Driver 또한 Neo4j에서 사용하던 것을 그대로 사용할 수 있었기에 접속주소와 인증정보만 바꾸는 것으로 큰 어려움 없이 Memgraph로의 연결을 확인할 수 있었습니다.



데이터 입력 과정

[본문 내용 요약] 

● 제약 및 인덱스 설정 문법에는 Neo4j와 Memgraph 간 차이가 존재함
● Memgraph에서는 제약 설정해도 인덱스 자동 생성되지 않음


데이터 입력 과정은 순탄치 않았습니다. 정확한 테스트를 위해서는 Neo4j에서 Memgraph로의 데이터 복사가 필요한 상황에서 Neo4j는 여러 가지 Export 옵션을 제공하고 있었으나 Memgraph에서 제공하는 데이터 Import 방법은 cypher와 csv 방식 2가지만 가능했기 때문입니다.


저희 팀은 먼저 Neo4j에서부터 사용한 csv 방식을 사용하여 데이터 Import를 시도했습니다. Memgraph의 경우 csv 파일에 대한 URL 접근을 허용하지 않아 직접 csv 파일을 업로드하여 사용해야 했기 때문에 약간의 불편함은 존재했지만 그래도 딱히 큰 문제가 되지는 않는다고 생각했습니다. 

그런데 Relationship을 입력하는 과정에서 Timeout 에러가 발생했습니다. 데이터 입력 전 최적화를 위해 Neo4j에서의 Constraint와 Index를 그대로 가져와 설정한 뒤 진행했음에도 Timeout 에러가 발생한 것입니다. 그래서 혹시 설정이 제대로 안된 것인지를 확인해 봤지만 설정은 정상이었습니다. 이후 반복된 시도에도 해당 문제는 개선되지 않았고, 이에 Memgraph의 csv Import 기능이 Neo4j에 비해 최적화가 덜 되어있는 것인지를 생각하며 방법을 바꿔 cypher 파일을 이용한 입력을 시도했습니다.


cypher 파일을 이용한 입력의 경우 Neo4j에서 apoc.export.cypher.all() 함수를 이용하여 export 하고, Memgraph Lab의 Import & Export 메뉴를 이용하여 Import 하였습니다. 그런데 이 방법에서도 약간의 호환성 문제가 존재했습니다. Neo4j에서 export 된 파일의 확장자는 .cypher인데 .cypherl로 변경해야만 Memgraph에 Import가 가능했으며, cypher 파일 안에 쓰여진 여러 줄의 쿼리 가운데 Constraint와 Index 설정 구문의 문법이 달랐던 것입니다. 따라서 해당 부분을 제거하고 나서야 Memgraph가 정상적으로 인식하고 처리하는 것이 가능해졌습니다. 호환성을 맞추는 과정이 어려웠던 것과는 별개로 csv 방식에 비해 속도가 현저히 떨어졌고, 관계 정보 입력 부분에서도 동일한 Timeout이 발생함에 따라 결과적으로 문제 해결을 위한 추가 방법을 찾아야만 했습니다.


Contraint와 Index 모두를 제대로 설정했음에도 Timeout 반복해 발생하는 문제를 해결하고자 다시 한번 관련 문서를 찾아보았습니다. 그 결과 Memgraph에서는 Constraint를 설정하더라도 Index가 자동 생성되지 않는다는 것을 알 수 있었고, 이후 별도의 쿼리를 통해 Index를 설정해 준 뒤 csv 방식을 통해 Timeout 문제를 해결하며 데이터를 입력을 완료할 수 있었습니다.



대용량 처리 테스트

[본문 내용 요약]

● Memgraph는 대용량 데이터 처리 측면에서 높은 성능을 보여줌
● Memgraph에서는 APOC 라이브러리 사용할 수 없지만, Neo4j에서 apoc.periodic.iterate() 함수를 이용하여 처리해야 하는 것도 일반 쿼리로 처리 가능함


대용량 처리 테스트는 몇 십만 단위의 노드를 생성/변경/삭제하는 쿼리들과 대용량의 노드 정보를 반환하는 쿼리들, 전체 노드에 대한 속성 업데이트 작업 쿼리 등을 넣어보며 응답시간과 프리징 현상 여부 등을 확인하는 형태로 진행되었습니다. 이 테스트에서 Memgraph는 Neo4j에서 APOC 라이브러리의 iterate 함수를 통해서만 정상 작동이 가능했던 쿼리들까지도 멈추는 현상 없이 일반 쿼리로 실행 가능하였고 대량의 정보(1,000개 이상의 row를 return)를 반환하는 쿼리에서도 1초 이내의 응답속도를 보여주며 대용량 데이터 처리 측면에서는 확실히 Neo4j보다 높은 성능을 낼 수 있음을 확인할 수 있었습니다.



쿼리 호환성 테스트

대용량 테스트를 진행하며 기본 형태의 쿼리들을 동일하게 사용할 수 있음을 확인했기 때문에, 이후의 호환성 테스트에서는 Memgraph 문서에 쓰여 있는 Neo4j와 Cypher 쿼리 사용의 차이점을 바탕으로 기존 쿼리들 중 형태가 복잡한 쿼리들을 중심으로 테스트를 진행했습니다. 이 테스트를 통해 Neo4j와 Memgraph 간의 호환되지 않는 쿼리 형태를 확인하였고, 변경 작업의 대부분은 이 쿼리들의 형태를 바꾸는 작업이었습니다.



변경 과정


[아래 내용 요약]

Memgraph ↔ Neo4j 쿼리 차이점 (Memgraph 중심으로 작성)

● WHERE 절에 패턴 입력 불가 (Memgraph 문서 링크)
● Subquery 사용 불가
● apoc 라이브러리 사용 불가
● UNWIND 사용 문법 차이
● 부족한 Datetime 자료형 지원
    → 커스텀 모듈로 만들어 사용 가능
       (C와 Python로 제작 가능하며, Procedure와 Function 2가지 타입의 로직 제작이 가능)


[ Neo4j 에서 사용 가능한 WHERE 절 패턴 입력 형태를 가지는 쿼리 ]
[ Memgraph에서 동작 가능하도록 WHERE 절 패턴 입력 부분을 OPTIONAL MATCH로 변경한 쿼리 ]

먼저 Memgraph 문서에서도 확인할 수 있는 WHERE 절 패턴 입력 형태입니다. Memgraph에서는 이것을 지원하지 않고 OPTIONAL MATCH를 통해 사용하도록 안내하고 있습니다.

기존 쿼리 중 패턴을 넣어서 사용하고 있던 쿼리들이 있었기 때문에 변경이 필요한 상황이었습니다. WHERE 절에 복합 조건 부분들이 OPTIONAL MATHC로 분리했을 때 까다로운 부분이 있었습니다.

(Memgraph 문서에서는 단독으로 사용될 때의 예만 나와있었는데요. 위 쿼리처럼 복합 조건으로 사용되고 있는 WHERE 절 패턴의 경우는 해당 부분만 떼어내서 OPTIONAL MATCH로 구현하게 되면 단순히 OPTIONAL MATCH의 결과만 null로 나오고 MATCH 절에서 쿼리한 부분에 대한 필터링을 할 수 없기 때문에 해당하는 부분 전체를 OPTIONAL MATCH로 변경해서 처리해야 합니다.)


[ Memgraph 에서의 Random 처리 ]

기존의 Neo4j에서는 랜덤 정렬 구현이 까다로웠기 때문에 해당 과정에서 랜덤 변수 생성 및 Subquery를 통해 랜덤 정렬을 구현하여 사용하고 있었습니다. 그런데 Memgraph에서는 Subquery의 사용이 불가능했고 이 때문에 다른 방법을 찾아야만 했습니다. 이때 Memgraph 기본 제공 함수 중 uniformSample 함수를 발견했습니다. 해당 함수는 기존에 작성했던 랜덤 쿼리보다 사용/성능 모든 면에서 더 우수했습니다.

MATCH를 통해 만들어진 목록을 collect()를 이용하여 리스트로 변환한 뒤 uniformSample() 함수에 필요한 개수 정보와 함께 넣기만 하면 랜덤하게 선택된 30개를 리스트로 만들어 반환해 줍니다. 또한, collect()에 넣을 때 노드 자체가 아니라 n.key 등의 프로퍼티 형태로 넣게 되면 해당 프로퍼티의 값만으로 이루어진 리스트를 반환받을 수 있습니다.


[ UNWIND 쿼리 방식의 차이 ]

변경 작업 중 까다로웠던 부분 중 하나가 바로 UNWIND입니다. 처음부터 Memgraph로 시작한 것이 아닌, Neo4j를 거쳐왔기 때문에 이렇게 조금씩 다른 부분들을 작업할 때 기존에 사용하던 형태가 먼저 떠오르게 되어 바뀐 형태로 쿼리를 만들어내는 작업이 쉽지만은 않았습니다.

Neo4j에서는 UNWIND 사용 시 MATCH 결과에 대해서 바로 적용할 수 있는데 반해, Memgraph에서는 리스트 타입만을 입력으로 받기 때문에 위처럼 MATCH의 결과에 대해서 collect()를 사용하여 리스트 형태로 변환해야지만 사용이 가능합니다.

(아직도 UNWIND를 사용하거나 WHERE 절 패턴 입력 형태가 먼저 떠오르거나 하는 상황에서는 약간의 혼란을 경험하기도 합니다.)


다음은 Datetime 자료형에 대한 부분입니다. (사실 저희 팀이 가장 고생을 많이 한 부분이기도 합니다.)


Memgraph에서 제공하는 부분은 localDateTime() 함수뿐이고 기존 Neo4j의 Datetime 타입의 데이터와 호환되지도 않았으며 string 타입과 Datetime 타입 간 변환을 지원해 주는 부분조차도 없었기 때문에 기존 데이터 중 Datetime 자료형에 대한 변환을 위해서 Python으로 커스텀 모듈을 만들어 진행해야 했습니다. 또한 데이터 입력 부분의 쿼리들 역시 모두 수정해야 했습니다. 여기에 추가적으로 Memgraph의 경우 Neo4j에 비해 사용량이 높지 않았기에 정보를 찾기 어렵다는 점도 있었습니다. 이로 인해 Memgraph와 Neo4j 간 차이가 있는 쿼리나 Memgraph에서 발생하는 이슈들에 대해 찾고자 할 때, Neo4j에 대한 답변만 나오거나 해당 사례를 전혀 찾을 수 없는 것들이 대부분이었습니다. Memgraph Forum 내에도 정보가 많지 않아서 최대한 공식 문서와 테스트를 통해 해결할 수밖에 없다는 어려움이 컸습니다.


마지막은 커스텀 모듈의 사용입니다.


Neo4j에서 잘 사용하던 apoc 라이브러리를 사용할 수 없다는 부분과 필요하면 만들어서 써야 한다는 부분이 부담이자 큰 불편함이었습니다. 하지만 함수 단위로 작업해서 넣어주면 되었기 때문에 간단히 만들어서 사용 가능했고, 필요한 로직을 직접 만들어서 사용할 수 있는 점은 오히려 큰 장점으로 다가왔습니다. GDB로부터 데이터를 받아서 처리해도 되지만 직접적으로 GDB 데이터를 수정하거나 탐색하면서 작업해야 하는 경우 , 커스텀 모듈을 통해 훨씬 더 효율적인 처리가 가능했습니다. 이를 바탕으로 전체 GDB 데이터를 탐색하고 업데이트 해줘야 하는 매칭/추천 알고리즘도 커스텀 모듈을 통해 작업할 수 있었습니다.

 

이렇듯 Memgraph를 통해 기존 Neo4j에서의 안정성 문제를 해결하며 더 좋은 성능을 낼 수 있는 시스템을 만들 수 있었습니다. 이러한 과정을 거치며 우리 서비스에 맞는 기술 스택의 선택이 얼마나 중요한지를 다시 한번 배울 수 있었고, 또 어떻게 선택해야 하는지에 대해서도 한 번 더 고민해 볼 수 있었습니다.





글을 마치며


최종적으로 GraphDatabase를 사용한 투덥의 소셜 시스템은 초기 해결하려 했던 기존 알림 시스템의 구조적 문제를 해결했습니다. 현재도 투덥은 Memgraph를 기반으로 프리징 문제나 별다른 이슈 없이 안정적으로 동작하고 있습니다.


저희 팀은 GDB를 처음 도입하는 과정에서 정말 다양한 상황들을 겪었습니다. 리서치 결과를 토대로 했으나 예상치 못한 이슈가 발생했던 일, 초기 시스템 도입 후에 서비스에 장애가 발생했던 상황 등이 그것이죠. 이처럼 쉽지 않은 순간들의 연속이었지만 그러한 문제를 해결하는 과정에서 리서치 단계부터 테스트, 구축, 운영, 이슈 대응에 이르기까지 전 과정에 걸쳐 팀원들과 적극적인 논의 과정을 거쳤고, 솔직한 피드백을 통해 팀 전체가 한 뼘 더 성장할 수 있는 기회였다고 생각합니다. 또한 기존 시스템에 존재하던 여러 문제들을 해결하는 데 필요한 다양한 아이디어들도 얻을 수 있던 값진 시간들로 기억에 남아있습니다. :D


GDB편 작성자 : Black(CTO), Josh(Server Engineer)

투덥 웹서비스 : https://2dub.me/

매거진의 이전글 [Tech] 투덥 그래프 데이터베이스 도입기 (5편)
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari