GraphDB로 개발해보았다.

Hansu Park·2024년 2월 4일
1
post-thumbnail

RDB인 Mysql만 사용해오다가 GraphDB인 Neo4j를 이용해 개발하다가 느낀 점들에 대해 소개하고자 합니다.

다루지 않는 것: RDB와 GraphDB(Neo4j)의 이론적인 차이점.
다루는 것: RDB를 이용하여 개발하다, GraphDB를 이용함에 있어서 헷갈렸던 부분들과 그에 대한 해답(이라고 생각한 것들)

RDB와 비교하였을 때 달랐던 점들을 요약하자면 다음과 같습니다.
1. 타입은 있지만, 스키마는 없다.
2. ID 전략이 없다.
3. on update current_timestamp 등을 자동 지원하지 않는다.

1. 타입은 있지만, 스키마는 없다.

NoSQL에 속하는 DB답게 자체적인 스키마는 없으나, Property, structural, and constructed values - Cypher Manual 를 살펴보면 알 수 있듯이 타입은 가지고 있습니다.

스키마가 없다는 것은 정해진 구조가 없고, 데이터를 추가/수정에 있어 자유롭다는 것을 의미합니다.

CREATE (p:Player{name: 'Son', backNumber: 7})
CREATE (p:Player{name: 'Lee', backNumber: "17"})

위 두 쿼리처럼INTEGER, STRING 라는 다른 타입이 backNumber라는 하나의 라벨에 할당될 수 있습니다.

╒════════════════════════════════════════╕
│p                                       │
╞════════════════════════════════════════╡
│(:Player {name: "Son",backNumber: 7})   │
├────────────────────────────────────────┤
│(:Player {name: "Lee",backNumber: "17"})│
└────────────────────────────────────────┘

(조회 결과)
와 같이 에러없이 조회되는 것을 확인해볼 수 있습니다.

Schemaless Database의 장/단점에 대해선

Schemaless의 데이터 검증이 부족하다는 단점을 극복하기 위해 어플리케이션 수준에서 타입검증을 강화하거나 VO를 이용해볼 수도 있습니다.

2. ID 전략이 없다.

Neo4j에서는 RDB와는 다르게 권장가능한 수준의 ID 전략이 없습니다. (권장가능한 수준이 무엇인지는 이후 소개합니다.)

자주 사용하는 JPA에서 엔티티를 정의하는 방식을 생각해보면

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

혹은

@Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;

와 같이 auto_increment를 통하거나 유일키를 만들어내거나 시퀀스 객체를 이용해서유일한 식별 키(이하 기본키)를 만들어내곤 합니다.

하지만 Neo4j에서 이러한 기능이 없습니다. 그 이유는 아마 테이블 구조가 아니라 순수한 그래프 구조기 때문이라고 생각합니다. 테이블에 따라 ID가 결정되는 테이블 구조가 아니기에 ID 전략이 없는 것 아닐까요? (참고: How to auto increment to get unique value for a new node's field - #5 by mike_r_black - General - Neo4j Online Community)

그럼에도 ID를 사용하고 싶다면 어떤 방법들이 있을까요?

DB를 이용한 방법

별도 시퀀스를 생성하기 위한 노드를 만들거나,

MATCH (a:Person)
WITH a ORDER BY a.id DESC LIMIT 1
CREATE (n:Person {id: a.id+1})
RETURN n

(참고: language agnostic - Auto increment property in Neo4j - Stack Overflow )
와 같이 id를 수동으로 만들 수도 있긴합니다. 하지만 권장하진 않는 것 같습니다.

DB 내부적으로 사용하는 식별키를 활용하는 방법

또한 Neo4j 내부에 자체적으로 elemetIdid 와 같은 노드 식별키를 유지하긴 합니다만,

Scalar functions - Cypher Manual 을 살펴보면 알 수 있듯 이를 서비스의 식별키로 사용하는 것 또한 권장하진 않는다 합니다.

이는
1. 내부적으로 ID를 재사용하기에 예상치못한 문제가 생길 수 있다는 점 (삭제된 노드의 ID를 다른 노드에 재사용합니다.)
2. 서비스가 Neo4j에 의존하게 된다는 점
라는 단점들이 있기 때문으로 보입니다.

추천하는 방식

추천하는 방식은 UUID를 이용하는 방식입니다.
이는
1. 어느 서비스에서나 모듈 등을 이용해 UUID를 만들어낼 수 있다는 점
2. 수학적으로 거의 유일하다는 점
등의 장점이 있습니다.

단점으로는
1. 이전에 사용했던 UUID인지 새롭게 만들어진 UUID인지 판별할 수 없다는 점.
2. 너무 길다는 점. (성능에는 영향이 없을 지 몰라도 URL, RequestBody 등에 적기에는 가시성이 떨어집니다.)

이 있습니다.

이를 보완하기 위해
(1): 통제가능한 서버의 범위 내에서 uuid를 생성해야 합니다.
(2) Create Youtube-Like IDs With PHP/Python/Javascript/Java/SQL | kvz.io 처럼 수학적 공식을 이용하여 짧은 UUID를 만들 수도 있습니다. (유튜브에서 사용하는 방식과 유사합니다.)

3. Auditing을 위한 기능을 자체적으로 지원하지 않는다.

Mysql에서는 Auditing을 위한 기능들이 존재했습니다.

  • CreatedAt을 위한 DEFAULT CURRENT_TIMESTAMP
  • UpdatedAt을 위한 ON UPDATE CURRENT_TIMESTAMP
    등이 있지만, Neo4j에서 제공하는 기능 중에서는 찾을 수 없었습니다.

Spring Data Neo4j에서는 CreatedAt, UpdatedAt, CreatedBy, UpdatedBy 등을 어노테이션 형태로 제공했지만, GoLang을 사용하는 저로썬 도움을 받을 수 없었습니다.

하지만 다행히 https://stackoverflow.com/questions/36897634/there-is-no-procedure-with-the-name-apoc-help-registered-for-this-database-ins 을 참고하였을 때 트리거를 이용해서 CreatedAt, UpdateAt을 설정해줄 수 있다는 것을 알게되어 적용해보았습니다.

트리거를 사용하는 방법은 아래 실습을 통해 설명합니다.

Trggier 적용 실습

이를 이용하기 위해서는 apoc 이라는 플러그인(https://neo4j.com/labs/apoc/) 을 설치하여야 합니다. (다양한 기능들을 제공하여 자주 사용되는 플러그인이라고 합니다.)

도커 환경에서 진행해보겠습니다.

우선

docker run \
    -p 7474:7474 -p 7687:7687 \
	--name neo4j_db \
	--volume=./neo4j:/data \
    -e NEO4J_apoc_export_file_enabled=true \
    -e NEO4J_apoc_import_file_enabled=true \
    -e NEO4J_apoc_import_file_use__neo4j__config=true \
    -e NEO4J_PLUGINS=\[\"apoc\",\"apoc-extended\"\] \
    neo4j:latest

을 통해 플러그인 설치와 함께 도커 컨테이너를 띄웁니다.

이후

apt-get update
apt-get install vim
touch /var/lib/conf/apoc.conf
vi /var/lib/conf/apoc.conf

을 이용해 apoc.conf를 연 후 I를 눌러 Insert 모드로 진입한 다음

apoc.trigger.enabled=true
apoc.trigger.refresh=60000

을 추가해줍니다.

마지막으로 ./bin/neo4j restart을 통해 재시작하여 설정을 적용해주면 됩니다.

이후 localhost:7070/console로 접속하여

CALL apoc.trigger.add('create-timestamp','UNWIND $createdNodes AS node

SET node.lastEditTimestamp = timestamp()', {phase:'before'});

  

// Add a timestamp on every node updated (property added/updated):

CALL apoc.trigger.add('update-insert-timestamp', 'UNWIND keys($assignedNodeProperties) as key

UNWIND apoc.trigger.propertiesByKey($assignedNodeProperties, key) as update

WITH update.node as node

SET node.lastEditTimestamp = timestamp()', {phase:'before'});

  

//Add a timestamp on every node updated (property removed):

CALL apoc.trigger.add('update-remove-timestamp', 'UNWIND keys($removedNodeProperties) as key

UNWIND apoc.trigger.propertiesByKey($removedNodeProperties, key) as update

WITH update.node as node

SET node.lastEditTimestamp = timestamp()', {phase:'before'});

이라는 트리거 쿼리를 작성해주면 됩니다. 해당 쿼리는

작성된 트리거 쿼리는 CREATE (p:Player{}) 와 같이 임의의 노드를 생성, 수정, 삭제하게 되었을 때 lastEditTimestamp라는 라벨이 갱신합니다.

실제로 임의의 노드를 만들어보면

lastEditTimestamp라는 라벨이 생기는 것을 알 수 있습니다.
(참고로 timestamp는 epoch time으로 부터 경과시간을 (ms)단위로 보여나타내주며, 함수일뿐 별도의 타입은 아닙니다.)

0개의 댓글