- Cypher & GQL
편집자 주: 지난 10월 GraphConnect 샌프란시스코에서 FiftyThree의 엔지니어링 리드인 Asseem Kishore가 Neo4j의 고급 읽기 및 쓰기 쿼리에 대해 프레젠테이션을 했어요.
GraphConnect SF 및 GraphConnect Europe의 더 많은 비디오를 보려면 graphconnect.com을 확인해보세요.
제 이름은 아심 키쇼어이고, 뉴욕에 있는 스타트업 개발자로 일하고 있어요. FiftyThree는 iPhone 및 iPad에서 사용할 수 있는 iOS 앱인 Paper를 만들고 있죠. 이 앱의 목적은 아이디어를 최대한 빠르고 효율적이고 아름답게 포착할 수 있도록 돕는 거예요.
아이디어는 텍스트, 목록, 체크리스트, 사진, 그림 등 다양한 형태와 모습으로 나타나잖아요. 저희 앱을 사용하면 텍스트를 잘라내어 붙여넣을 필요가 없어요. "스와이프 스타일"이라는 기능을 사용해서 텍스트를 드래그 앤 드롭할 수 있고, 스와이프로 목록을 들여쓸 수도 있고요.
사진을 넣고 주석을 달 수도 있고, 그래프를 스케치하고 그림을 그릴 수도 있어요. 소셜 및 협업 계층을 통해 아이디어를 공유하고, 다른 사람을 팔로우하고, 다른 사람의 아이디어를 리믹스할 수 있는 클라우드도 제공하고 있는데, 이 공유 서비스는 전적으로 로 만들어졌답니다.
저는 이전 GraphConnect 컨퍼런스에서 FiftyThree의 Neo4j 작업에 대해 발표한 적이 있어요. 2013년 10월과 2014년 10월에요.
이번 블로그 포스팅에서는 쿼리 읽기와 쓰기, 이렇게 두 가지 기본적인 내용을 다뤄볼까 해요.
읽기와 쓰기의 미묘한 부분을 깊이 파고들면 우리가 당연하게 생각하는 것들이 많다는 걸 알게 될 거예요. 예를 들어, 읽기에서는 일관성이 데이터에 매우 중요한 부분인 반면, 쓰기에서는 원자성(Atomicity)이 중요한 기본 원칙이죠.
읽기 (Reading)
다음은 Paper에 그려진 일반적인 Neo4j 클러스터 설정 모습이에요. (Paper 기능 중 하나는 모양을 자동으로 수정하는 건데요. 정사각형처럼 보이는 걸 그리면 Paper가 자동으로 형식을 지정해 준답니다.)
이 표준 클러스터는 서로 통신하는 마스터와 두 개의 슬레이브로 구성되어 있어요. 권장되는 설정은 HAProxy 같은 로드 밸런서를 앞에 두는 건데요, 클라이언트가 이걸 통해 통신하는 거죠.
모든 트래픽을 단일 인스턴스로 보내거나 트래픽을 임의로 로드 밸런싱하는 대신, 두 개의 개별 풀을 사용할 수 있어요. 하나는 마스터에 요청을 보내고, 다른 하나는 슬레이브에 요청을 보내는 방식이죠.
Neo4j는 마스터/슬레이브 설정이기 때문에, 이러한 풀을 분할하는 일반적인 방법은 읽기와 쓰기를 기준으로 나누는 거예요. 하지만 많은 애플리케이션의 경우 쓰기보다 읽기가 훨씬 많기 때문에, 읽기를 수평으로 확장하는 게 정말 효과적이고 가치가 있답니다. 저희는 읽기 대 쓰기 대신 원하는 일관성 수준에 따라 트래픽을 분할하는 게 실제로 더 효과적이라는 걸 알게 됐어요.
Paper의 소셜 계층을 통해 Neo4j에 유지되는 계정이 있는데요. 읽기와 쓰기를 기준으로 분할했을 때 재현할 수 없는 일시적인 오류가 나타나기 시작했어요. 예를 들어, 누군가가 계정을 만들면 자동으로 로그아웃되는 문제가 있었죠.
원인은 인증 관련 이유로 요청이 실패했을 때 비밀번호가 변경되었거나 계정이 삭제되었다고 가정하는 코드가 있는 클라이언트 앱 때문이었어요. 계정 생성을 위한 쓰기 요청 직후 해당 사용자에 대한 인증 조회를 수행하는 읽기 요청이 이어졌는데, 해당 읽기 요청이 슬레이브로 가는 경우 해당 사용자가 즉시 동기화되지 않았을 수 있는 거죠.
그래서 가입 직후 새 계정이 최신 상태인지 확인하기 위해 바로 다음 읽기를 마스터에 보내야 한다는 걸 깨달았어요. 비밀번호를 변경하거나 비밀번호를 재설정하거나 이메일 주소를 변경하는 경우에도 마찬가지고요.
또 다른 좋은 예가 있는데요. Paper에서 프로필을 팔로우한 다음 닫기 버튼을 누르면 홈 스트림으로 돌아가잖아요. Paper에서는 홈 스트림을 새로 고쳐 해당 계정의 콘텐츠를 볼 수 있는데, 슬레이브가 새로 만든 follow 관계를 아직 가져오지 못했다면 슬레이브에서 홈 스트림 쿼리를 수행해도 해당 콘텐츠를 얻을 수 없게 되는 거죠.
사용자별 쓰기 후 읽기 일관성
저희는 "사용자별 쓰기 후 읽기 일관성"이라는 개념에 도달했어요. 이는 방금 쓰기를 수행한 경우 바로 후속 읽기가 강력한 일관성을 가져야 함을 의미하죠.
이를 달성하기 위해 마지막으로 쓰기를 수행한 타임스탬프를 추적하고 인증 조회 과정에서 해당 데이터를 가져왔어요. 이렇게 하면 최근에 쓰기를 수행한 경우, 이 요청이 읽기 요청이더라도 강력한 일관성을 위해 읽기 요청을 마스터에 보내야 한다는 것을 알 수 있었죠.
인증 조회도 강력한 일관성을 유지해야 해요. 다음은 모든 인증 조회가 마스터로 전송되는 샘플 코드인데요. 그 후 쿼리가 읽기 요청이고 강력한 일관성이 필요하지 않은 경우 해당 쿼리를 슬레이브에 보내게 됩니다.
이건 모든 인증 조회 읽기가 수평적으로 확장되지 않은 `master`로 간다는 뜻이에요. 이런 인증 조회는 쿼리가 엄청 빠르고 간단해서 지금까지는 문제가 없었어요. 혹시 문제가 생긴다면 Neo4j 같은 `master`/`slave` 시스템에서 계정을 오프로드해서 수평 확장 시스템으로 옮길 수도 있고요.
그렇다면 우리가 글을 쓴 시점은 얼마나 최근일까요? "최근"을 어떻게 정의하느냐에 따라 다르겠죠. 기준점을 어디에 둬야 할까요? 쉽게 답할 수 있는 질문은 아니지만, 결국 `slave` 지연 문제로 귀결돼요. `Slave`가 `master`에 비해 얼마나 뒤쳐져 있는지, 따라잡는 데 얼마나 오래 걸리는지가 중요한 거죠.
Neo4j에서 이런 부분을 모니터링하는 게 가능하긴 하지만, 쉽지만은 않아요. 하지만 Neo4j 개발팀도 이 점을 인지하고 개선 작업을 진행 중이에요. 저희는 모든 인스턴스에서 마지막으로 커밋된 트랜잭션을 보고, 어떤 인스턴스가 `master`이고 어떤 인스턴스가 `slave`인지 식별할 수 있는 도구를 개발했어요. 15분마다 데이터를 샘플링하고, 현재 시스템을 통과하는 원시 트랜잭션 속도에 비해 `slave` 지연이 최소화되는 걸 확인했죠.
저희가 모니터링하는 다른 주요 요소로는 다양한 HA 구성, 풀 간격, 푸시 요소 및 전략 등이 있어요.
아래 그림은 기본적인 코드 스니펫이에요.
이게 반드시 여러분에게 최적인 스니펫은 아닐 거예요. 저희에게도 처음부터 딱 맞진 않았고요. 여기에는 `slave`가 `master`에 쿼리하는 빈도를 나타내는 풀 간격(처음에는 10초로 설정)이 포함되어 있는데, 지금은 500ms로 설정했어요.
500ms라는 짧은 간격과 그에 따른 `slave` 지연 부족을 고려해서, 쓰기 후 읽기 일관성 임계값을 2초로 설정했어요. 즉, 쓰기를 수행한 후 다음 2초 동안 모든 읽기 쿼리가 `master`로 이동한다는 뜻인데, 지금까지는 잘 작동하고 있어요.
사용자별 읽기 후 읽기 일관성
사용자별 읽기 후 일관성은 또 다른 중요한 일관성 문제일 수 있어요.
만약 두 개 이상의 슬레이브가 있다면, 하나의 읽기 쿼리가 슬레이브 A로 가고, 그 다음 읽기 쿼리가 슬레이브 B로 갔는데 아직 동기화가 안 됐다면 일관성이 깨질 수 있죠. 예를 들어, 1초 동안은 사진이 보였다가 다음 순간에는 사라지는 것처럼 보일 수 있어요.
슬레이브 지연이 아주 짧으면 이런 문제가 잘 안 생기지만, 만약 발생한다면 사용자별로 데이터베이스 인스턴스 고정성을 적용할 수 있답니다.
HAProxy 설정에서 앱 코드가 사용자 ID를 헤더로 보내고, 이 헤더의 특정 값(사용자 ID)을 항상 같은 슬레이브로 라우팅하도록 할 수 있어요.
글쓰기
앞서 말씀드렸듯이, Paper에는 사용자들이 서로 팔로우할 수 있는 소셜 기능이 있어요.
이걸 효과적으로 하려면 사람들이 사용자를 팔로우하거나 언팔로우할 수 있어야 하고, 성능을 모니터링하기 위해 몇 가지 통계를 캐싱해야 해요. Twitter가 팔로워 수와 사용자당 팔로잉 수를 보여주는 것처럼, 캐시는 전체 목록을 직접 가져오지 않고도 해당 정보를 보여줄 수 있어야 하죠.
Paper에서 똑같은 작업을 하기 위해 사용자별로 "팔로잉" 속성과 "팔로워" 속성을 캐싱해요. 즉, 제가 누군가를 팔로우할 때마다 쓰기 속성을 증가시키고, 언팔로우할 때 쓰기 속성을 감소시키는 거예요.
아주 간단한 한 줄 예시를 한번 살펴볼까요? Cypher:
혹시 어떤 코딩 언어로든 멀티스레드 프로그래밍을 해본 적이 있다면, 아마 고전적인 C = C + 1 사례를 바로 알아보실 거예요. 이게 왜 고전적이냐 하면, 쓰기 전에 읽기를 수행하는 아주 흔한 경쟁 조건이기 때문이죠. 두 개의 병렬 액터가 동일한 호출을 순서대로 수행하면 문제없이 잘 작동해요.
하지만 호출이 순서대로 발생하지 않으면, 서로 값을 더해서 쌓아가는 대신 둘 다 동일한 값을 쓰게 되는 문제가 생겨요.
Neo4j는 “커밋된 읽기 격리 수준”을 사용하는데, 이는 다른 트랜잭션의 동시 수정으로부터 읽기가 보호되지 않는다는 의미예요. 쓰기 잠금은 트랜잭션이 끝날 때까지 유지되죠.
이전의 읽기-증분-쓰기 흐름에서는 일련의 전체 단계가 트랜잭션이고, 잠금은 마지막 단계에서만 수행돼요.
잠금은 쓰기에 대해서만 유지되기 때문에, 실제로 읽기가 순서대로 발생하는 것을 막지는 못해요.
이게 상당한 성능 이점을 제공하기 때문에 기본 동작이긴 한데요. 하지만 `Node` 및 `Relationship`에 대한 쓰기 잠금을 수동으로 획득해서 사용 사례에 맞는 더 높은 수준의 격리, 즉 직렬화를 달성하기 위해 이 문제를 해결할 수 있다는 점!
다음 예에서는 의미 체계를 변경하지 않는 증분을 제거하고 읽기 전에 쓰기를 추가했어요.
이렇게 하면 원하는 쓰기 잠금을 획득해서 전체 트랜잭션이 직렬화되도록 할 수 있어요. 이제 두 트랜잭션이 동시에 시작되더라도, 하나만 잠금을 획득하고 다른 트랜잭션은 첫 번째 트랜잭션이 완료될 때까지 기다려야 하는 거죠.
이건 경쟁 조건을 방지하는 데 필요한 직렬화를 보장해 준답니다.
Cypher에서 이걸 수행하려면 다음 사항을 염두에 둬야 해요. 잠금은 `Node` 및/또는 `Relationship` 수준에서 필요하다는 거죠. `Property` 생성, 삭제 또는 변경과 같이 `Node` 또는 `Relationship`의 `Property`를 수정할 때, `Property`가 속한 엔터티에 잠금이 적용된답니다.
`Node`에 대한 잠금을 획득하려면 해당 `Node`에 임의의 값의 `Property`를 설정해야 해요. 다음은 자체 Cypher `Query`에서 사용하는 규칙이에요.
여기서 _lock은 아마 의미론적인 의미는 없을 거예요. true 값이 중요한 건 아니고요. 하지만 모든 쿼리에서 동일한 방식으로 잠금을 확인하기 위해 저희가 사용하는 규칙이라고 생각하시면 돼요. 이렇게 쿼리를 작성하면 쓰기 잠금을 획득해서 경쟁 조건을 방지할 수 있어요.
다음 예시를 한번 살펴볼까요? 이걸 실행하려면 당연히 node가 필요하겠죠. MATCH 절을 사용해서 속성을 증가시키려는 사용자를 가져올 거예요.
그런데 만약 해당 `user` label을 제거하는 동시 쿼리가 있다면 어떻게 될까요? 이건 Paper에서 실제로 일어나는 일인데요. 삭제된 node에 대한 일부 기록을 유지하기 위해서 부분 소프트 삭제를 구현하고 있어요. `user` label을 `deleted user` label로 변경해서 구현하는 거죠:
그럼 만약 첫 번째 MATCH 쿼리가 사용자를 찾은 *다음에*, 두 번째 쿼리가 `user` label을 `deleted label`로 바꿔버린다면 어떻게 될까요?
이제, 실제로는 원하지 않더라도 첫 번째 쿼리를 실행하게 되겠죠.
이 문제를 해결하기 위해서 저희는 이중 확인 차단을 사용하고 있어요. 이렇게 하려면 변경되었을 수도 있는 쓰기 잠금 이전에 수행된 모든 읽기를 확인해야 해요. 이 경우, 저희에게 변경될 수 있는 부분은 `user` label이니까, 쓰기 전에 사용자 label이 이 node에 여전히 존재하는지 확인하기만 하면 되는 거죠.
그렇다면 이게 Relationship에서 어떻게 작동할까요? 다음은 Relationship에 IDE 입력이 있는지 확인하는 아주 일반적인 개념이에요. 우리 경우에는 이게 딱 하나, 즉 나와 앨리스 사이에 follows Relationship이 두 개가 아니라 하나만 있어야 한다는 의미죠. 만약 이 Relationship이 존재하지 않는다면, 다음과 같이 만들 수 있어요.
Relationship을 생성하고 삭제할 때, Relationship뿐만 아니라 시작 `Node`와 끝 `Node`에서도 잠금(Locking)이 수행된다는 점을 기억하는 게 중요해요. 그러니 경쟁 조건(Race Condition)에 취약하지 않도록 해야 하죠. 이 경우 경쟁 조건은 나와 Alice 사이에 Relationship이 없다고 생각하지만, 동시에 두 번째 `Query`가 `Query`를 시작한 직후에 하나를 추가하는 상황을 말해요.
이를 방지하는 간단한 방법은 시작 `Node`와 끝 `Node` 모두에 명시적인 잠금(Locking)을 적용해서, Relationship이 존재하는지 확인하기 위해 읽기를 수행하면 Relationship이 동시에 추가되거나 삭제되지 않도록 하는 거예요.
다행히도 Cypher의 `merge` 키워드가 바로 이런 작업을 수행해준답니다.
하지만 아쉽게도, 일반적으로 전체 패턴을 `MERGE`하고 싶어 하지는 않기 때문에 `MERGE`가 만능 해결책은 아니에요. 아래 예시에서는 사용자 Label을 내 Label에 넣지 않았는데요. 왜냐하면 Alice가 단지 Bob을 따르려고 한다면, 실제로 사용자를 생성하고 싶지는 않기 때문이에요. Relationship만 `merge`하고 싶은 거죠.
저는 MATCH 전에 MERGE를 사용하는데, 이는 쓰기 잠금을 수행하기 전에 일부 읽기를 수행하고 있다는 의미에요. 이 문제를 해결하기 위해, 먼저 `MATCH`를 수행하고 명시적으로 쓰기 잠금을 가져온 다음, `MATCH`를 확인하는 거죠.
Paper 네트워크에서 우리 시스템에는 Twitter나 Facebook과 마찬가지로 차단 개념이 있어요. 다음 예에서는 제가 Alice를 팔로우하려고 할 때 Alice가 나를 차단하지 않았는지 확인하고 싶은 경우를 보여줘요.
이 블록 검사는 다음의 일부가 아니에요. MERGE는 또 다른 경쟁 조건을 제시하죠. 없다고 생각하실 수도 있겠지만 blocks 관계가 있지만, 두 번째 동시 쿼리가 하나를 만들 수 있어요. 이제 내 follows 관계가 불법이거나 유효하지 않게 되는 거죠.
해결책은 이전 예와 동일해요.
두 개의 잠금을 사용하는 게 기술적으로 꼭 필요한 건 아니지만, 두 개의 개별 쿼리가 결국 동일한 잠금, 즉 하나의 잠금을 사용하는 경우를 방지하는 데 도움이 된다는 걸 알게 됐어요. 예를 들어, Alice와 Bob에 대해 작업하는 쿼리 하나와 Bob과 Carol에 대해 작업하는 쿼리가 있다면, 의도한 것과는 달리 Alice와 Carol에 대한 잠금이 필요할 수도 있거든요.
두 개 이상의 잠금을 사용해서 발생할 수 있는 문제를 방지하기 위해 Neo4j에는 교착 상태 감지 기능이 내장되어 있답니다. ForsetiClient:
이건 각 쿼리가 서로의 잠금을 기다리고 있기 때문에 두 쿼리를 실행할 수 없는 Neo4j에서 동시 작업을 수행하려고 할 때 발생할 수 있는 일반적인 오류에요.
이 오류는 Neo.transienterror.transaction.deadlockdetected로 표시되죠. 여기서 중요한 건 '일시적인 오류'라는 분류인데요. 반환되는 모든 오류에서 Neo4j는 이를 클라이언트 오류, 즉 버그가 있다는 의미, 또는 데이터베이스 오류로 분류해요. 데이터베이스 오류라면 Neo4j에 버그가 있는 거니까 보고해야겠죠. 하지만 일시적인 오류라면 이번에는 쿼리가 작동하지 않았지만, 다시 시도하면 작동할 가능성이 높다는 뜻이에요.
이 문제를 해결하기 위해 재시도 루프에서 Cypher 쿼리를 실행하게 돼요. 즉, 일시적인 오류가 발생하면 지수 백오프를 수행하고 쿼리를 다시 시도하는 거죠. 그리고 이렇게 재시도 아키텍처를 마련하고 나면, 이 아키텍처를 구현하려는 다른 여러 위치도 알 수 있게 될 거예요.
하지만 이 단일 쿼리가 더 큰 트랜잭션의 일부라면 어떻게 될까요? 쿼리 중 하나라도 오류를 반환하면 전체 트랜잭션이 실패하게 돼요. 그런 일이 발생하면 하나의 쿼리를 다시 시도할 수 없고, 전체 트랜잭션을 다시 시도해야 하죠.
우리가 트랜잭션 쿼리를 사용하는 주요 장소는 복잡한 삭제 작업이에요. 사용자 삭제는 재귀적인 비즈니스 논리가 너무 많아서 Cypher 쿼리로 간단하게 표현할 수 있는 작업이 아니거든요. 사용자는 여러 개의 공유 ID를 갖고 있고, 팔로워가 있고, 다른 계정을 팔로우하고 있잖아요.
이 모든 항목을 단일 Cypher 쿼리에 삭제하기 위한 비즈니스 로직을 넣을 수는 없어요. 즉, 트랜잭션을 인식하는 코드로 코드를 래핑한 다음 재시도 루프에서 해당 코드를 실행해야 한다는 의미이죠.
아래에서는 트랜잭션을 시작한 다음 코드, 쿼리 및 비즈니스 로직을 실행한 다음 일시적인 오류가 있는 경우 전체를 다시 시도하는 것을 보여줍니다.
다중 언어 지속성 아키텍처를 사용하고 여러 데이터 저장소와 쓰기가 있는 경우, 전체를 다시 시도하기 전에 다른 쓰기를 롤백해야 한다는 점을 명심하세요.
위의 예는 상당히 단순하지만, 현실 세계에서는 이 모든 것이 훨씬 더 복잡해져요. 어떤 읽기가 권리를 알리는지, 해당 읽기의 어떤 부분이 변경될 수 있는지, 어떤 잠금을 가져와야 하는지, 모든 잠금을 가져올 때 알아야 할 경합의 종류를 결정하는 것은 더 어렵거든요.
좋은 소식은 다양한 범위의 절충안이 있다는 것이에요. 한편으로는 위에 나열된 주의 사항에 대해 걱정할 필요 없이 간단한 쿼리를 수행할 수 있죠. 스펙트럼의 반대편에서는 가능한 한 솔직하고 조심스러울 수 있으며 쿼리는 조금 더 복잡해지고 추론하기가 조금 더 어려워질 거예요. 하지만 아마도 어딘가에 여러분에게 좋은 중간 지점이 있을 거예요.
Paper의 경우, 그 중간 지점은 기본적으로 대부분의 쿼리가 이 항목에 대해 크게 걱정하지 않는다는 것이에요. 우리는 속성 증가 및 관계 확인과 같은 핵심 사항 중 일부를 일부 도우미로 추상화하고 그 복잡성을 꽤 잘 추상화하죠. 그런 다음 실제로 이러한 문제가 발생한 소수의 경우에만 라벨 교체에 대한 이중 확인 차단과 같은 다른 문제를 해결해요.
현재 우리 시스템은 매우 원활하게 운영되고 있어요. 우리의 오류율은 그 어느 때보다 낮으며 데이터베이스에서 이전에 보았던 불일치가 더 이상 보이지 않아요.
폐쇄
다음 마지막 수업을 고려해 보세요. 앞서 우리는 잠금이 Node 및 Relationship 수준에 있다는 것을 배웠어요. 다음은 일반적인 데이터 모델이에요.
시간이 지나면서 데이터베이스에 저장되는 데이터의 양과 유형이 늘어나죠. 높은 쓰기 처리량을 가질 수 있는 앞서 언급한 모든 사항을 고려하세요.
- 팔로어가 아닌 통계
이러한 모든 것들은 덮어쓰기율이 높을 수 있어요. 특히 대부분이 여러분뿐만 아니라 다른 사람의 행동에 영향을 받기 때문이죠.
예를 들어, 100명의 사람을 팔로우하면 그 사람 중 한 명이 새로운 아이디어를 게시할 때마다 홈 스트림에 표시돼요. 누군가가 여러분의 아이디어 중 하나에 별표를 표시하는 경우에도 마찬가지고요.
두 번 모두 알림을 받게 되죠. 그러면 목록의 머리 부분이 바뀔 거예요. 이는 새로운 Relationship을 삭제하고 생성하는 것과 같아요.
우리가 본 것은 속성을 수정하면 Node 수준에서 잠금이 발생하는 반면 Relationship을 생성하고 삭제하면 Relationship과 Node 수준 모두에서 잠금이 발생한다는 것이에요. 하나의 모놀리식 Node 대신 다양한 문제에 해당하는 별도의 Node를 갖는 것이 더 나을 수 있어요.
이렇게 하면 더 세밀하게 쓰기 처리량을 관리할 수 있어요.
위 자료에서 가장 중요한 내용은 다음과 같아요.
Aseem의 강연에서 영감을 받으셨나요? 아래를 클릭해서 나머지 영상도 확인해 보세요! GraphConnect Europe.
- Cypher
- Deadlock Detection
- FiftyThree
- GraphConnect
- HAProxy
- Multithreaded Programming
- Race Condition
- Slave Lag
- Transient Error
에이치시스템즈의 LogTree는 Neo4j 기반 GraphRAG 플랫폼으로, 데이터를 자동으로 지식그래프화하고 자연어 질의로 즉시 답을 제공합니다.
'Neo4j' 카테고리의 다른 글
| 그래프 기반 메타데이터 저장소의 장점 [커뮤니티 게시글] (0) | 2026.09.04 |
|---|---|
| 새로운 스크린캐스트: Neo4j로 더 깊이 파고드는 Ruby on Rails 고급 활용법 (0) | 2026.09.04 |
| Neo4j MCP 서버: ADHD 친화적인 이사 도우미 (1) | 2026.09.04 |
| Node.js/React.js Neo4j 영화 앱에 사용자 추가하기 (0) | 2026.09.04 |
| 데이터 일관성 모델: ACID vs. BASE 완벽 해부 (0) | 2026.09.03 |
