지피지기 백전백퇴

Prototyping entity framework


문제

얼기설기 짜던 개인 프로젝트에 이런저런 데이터를 쌓아놓고 있었는데, 약간의 바이너리가 들어가긴 했지만 sqlite3 데이터베이스 파일 크기가 대충 200MB쯤 되니 슬슬 데이터베이스 스키마가 마음에 안들기 시작했다.

  • 몇몇 테이블은 2~3년 전에 만들어 기억이 가물가물하고
  • 개인개발의 특성상 만들어놓고 안쓰는 쓰레기 인덱스나 데이터 관리가 잘 안되기도 하고
  • 매번 뭔가 고칠때마다 신중하고 sqlite3 데이터베이스를 백업하고 조심조심 DDL 쿼리를 날리는 것도 부담스럽고

그래서, 대안이 뭔데?

딱 요 두 테이블만 가지고 다 해먹어보겠다.

CREATE TABLE entities (
   id INTEGER PRIMARY KEY,
   type TEXT NOT NULL,
   data TEXT NOT NULL
);

CREATE TABLE edges (
   source INTEGER NOT NULL,
   type BLOB NOT NULL, -- Can be binary
   dest INTEGER NOT NULL
);

이렇게 써놓고 보니, TAO인데?

Entity(객체)

그냥 객체…라고하면 설명이 부족한 것 같으니, 대충 이렇게 설명해 보겠다:

https://domain/some/path/123123 에서 123123이 id인 무언가랑, https://domain/other/path/112233 에서 112233이 id인 무언가들이 한 테이블에 섞여있는 짬뽕

type은 해당 id에 해당하는 모델을 지정한다.

data는 해당 entity에서 id를 뺀 다음 JSON serialize한 데이터이다.

Edge(연결)

그냥 객체와 객체를 연결하는 무언가…라고 하면 여전히 설명이 부족한 것 같으니, 대충 다음 조건을 추가해 보겠다.

어떤 객체의 데이터를 딱 보면 해당 객체를 dest로 갖는 모든 연결을 알 수 있어야 한다.

즉 객체 데이터에 해당 객체가 가지고 있는 모든 연결 정보를 이미 알고 있어야 한다… 이말이다. 이를테면 게시물 객체는 이미 게시판 id를 필드에 보관하고 있어서, 그걸 가지고 게시판 -> 게시물 연결을 바로 알 수 있어야 한다…는 식이지.

구현

기존 코드가 Rust로 작성되어 있었기 때문에 일단 Rust에서 사용가능한 entity-rs 라이브러리를 사용하고 있지만, 모든 기능을 다 사용하는 것은 아니다.

일단 라이브러리 자체가 현재 Archive 모드로, 더이상 개발되지 않고 있기도 하고, 몇몇 기능은 내가 원하던 모습이 아니라서, 현재는 딱 #[simple_ent] 매크로에서 제공하는 기능 정도만 사용하고, Database 인터페이스 등은 사용하고 있지 않다.

Atomicity? Transaction!

원래 TAO 디자인은 eventual consistency였지만, 나는 어차피 sqlite3 데이터베이스 하나 뿐이니, Consistency와 Availability를 모두 잡을 수 있다… (흑흑 쪼개질 파티션이 없어요)

그리고 설령 분산 시스템으로 간다고 해도, DynamoDB나 Spanner도 요새는 다 Transaction API를 지원하기 때문에 구현이 크게 달라지지는 않을 거라고 생각한다.

다만 기존에는 테이블에 UNIQUE 인덱스를 걸었거나, UPDATE ... WHERE ... 등의 문법으로 compare and set 등을 구현했던 것들을 모조리 Transaction을 이용한 구현으로 바꿔야 하고, sqlite3의 transaction은 충돌이 일어났을 때 BUSY 에러가 발생하고 그걸 클라이언트가 받아서 처리해야 하니 더 번거로워진 것은 분명하다.

Transaction과 Rust의 lifetime 개념

Rust의 Sqlite 클라이언트 구현체는 트랜잭션에 객체의 lifetime을 강하게 묶어놓았다. 이를테면 트랜잭션을 COMMIT 혹은 ROLLBACK하는 시점에 객체가 사라지도록 fn commit(self)(self를 빌리는게 아니라 move하는 것이 포인트)와 같이 선언한 것이다.

위에 설명한 대로 다수의 business logic에 트랜잭션을 사용해야 하게 되었고, 실패시 retry도 해야하는 판이라, 가능한 한 트랜잭션이 필요한 로직 단위를 하나의 closure로 감싸는 방향으로 구현하…는 중인데… 현실은 시궁창… (안되는 건 아니지만 꽤나 번거롭다)

AI를 활용한 개발 workflow

결국 기존에 테이블 스키마를 잡고 SQL을 바로 짜서 구현한 기능들을 객체 기반으로 옮겨야 하는 상황인데, AI를 써서 이전을 시키고 있다. AI가 짠 코드를 리뷰하긴 귀찮으니 다음과 같이 작업을 한다.

  • 기존 코드를 설명할 수 있는 trait을 정의하라고 AI에게 시킨다.
  • trait만 리뷰한다.
  • trait 구현은 맡길게!
    • 혹시 trait 구현에 일반적으로 적용할 수 있는 테스트 케이스도 만들어봐
  • 트랜잭션을 써야하니 &self로 되어있는 부분들을 self로 바꾼다. (귀찮아서 수동으로 함)
  • 알아서 고쳐줘!
  • 자 이제 객체 기반 구현을 새로 만들어줘
  • 기존 테스트 케이스가 객체 기반 구현에서도 잘 작동하니?

하여튼 포인트는 trait까지만 리뷰하고 그 밑은 AI들끼리 싸우게 시키라… 이말이다.

결론

삽질중