고졸 4년차 웹땔감 메모앱 개발한다고 했던 사람임.
백엔드 설계가 확정이 되었고 또 구성이 70% 정도는 진행 되어서 글을 올려봄.
먼저 이야기에 앞서 필요한 것부터 간단하게 나열해보겠음.
데이터는 총 4가지로 분류됨.
1. 유저 작성 메모데이터(Yjs 사용함.)
2. 관리용 메타데이터
3. 보관용 청크데이터
4. 검색용 벡터데이터
데이터 흐름은 유저 작성(클라이언트 -> yjs서버) -> 캐시 -> 저장임.
DB는 nebula(메타데이터), MinIo(캐시데이터/스토리지), AstraDB(청크데이터), Zilliz(벡터데이터)를 사용함.
먼저 Yjs Doc에 대해 이야기 해보자면
Yjs Doc은 간단하게 yjs 규격의 document의 데이터를 연결된 커넥터들에게 실시간으로 업데이트 해주는 매개체라고 보면 됨.
실시간 업데이트는 웹소켓으로 이뤄지는데 우리가 직접 구현하기 힘든 crdt 알고리즘으로 실시간 협업을 가능하게 해주는 매우 유용한 라이브러리임.
메모 서비스들 보면 보통 실시간 크로스플랫폼 편집은 거의 다 유료로 서비스를 하는데 본인은 yjs doc을 통해서
실시간 크로스플랫폼 편집 수준이 아니라 실시간 협업 수준의 서비스를 무료로 서비스해보려고 함.
실시간 편집은 최대 5개 디바이스까지로 생각하고 있음.
이 기능을 구현하기 위해 웹소켓 서버를 마련해야 했는데 웬만하면 대기업에 의존하고자 서칭을 해보니 y-partykit이라는 서비스가 있었음.
y-partykit은 클라우드플레어의 Durable Objects(이하 DO라고 하겠음)를 이용해서 웹소켓 서버 구축 없이 클라우드로
실시간 서비스를 제공할 수 있게 해주는 라이브러리임.
처음엔 단순히 이 서비스를 써서 실시간 협업 수준의 크로스플랫폼 메모 서비스를 만들면 되겠다 생각했지만
ai와 심도있는 대화를 해보니 도저히 쓰기 힘들만큼 비싼 서비스였음.
아래부터 DO는 웹소켓서버로 치환해도 무방함.
클라우드플레어 DO를 실제 서비스에 사용하게 되면 과금이 요청 / 실행시간 / 저장 단위로 발생되는데
이때 실행 시간에 대한 과금이 정말 비싼 서비스였음..
1개의 DO를 실행하면 얼마의 메모리를 사용하든 최소 128MB의 메모리로 책정되어 계산되는데 이게 초당으로 과금됨.
초당 128MB = 약 0.125GB이고 프리티어는 최대 월 40만GB/s까지며 이후 100만GB/s당 $12.5의 요금이 발생함.
예를 들어 하나의 DO를 30일 내내 실행시키면 하나의 DO가 월 32.4GB/s 를 사용함.
쉽게 말해서 한달에 DO 하나만 무료로 사용하고 그 이후로는 DO 1개당 약 15000원 더 내라 임.
물론 코드 돌아가는 타이밍을 잘 관리해서 한 번의 요청에 나노초 단위로 끊으면
어느정도 할만한 영역까지 내려오는 건 확인했지만 그렇다해도 직접 VM에 웹소켓 구현하는 것보다
너~~~무나 비싼 가격이라 웹소켓은 VM으로 구현하는 쪽으로 가닥을 잡음.
현재 사용할 수 있는 라이젠 9950x 서버가 있어서 우선은 컨테이너당 4스레드씩 주고 3개정도 생성해서 분산처리 해놓았음.
이때 소켓서버는 상태를 가지지 않게 최대한 성능을 뽑아쓸 수 있게 하려고 함.
ydoc은 하나의 완전한 데이터가 아니라 히스토리 단위로 업데이트가 되어서 소켓서버 메모리에 로드할 때 히스토리들을 합쳐서
하나의 데이터를 만들고 배포하는 방식으로 작동함.
이때 저장하는 방식이 히스토리 방식이 있고 스냅샷 방식이 있다고 하는데 본인은 히스토리로 했음.
어차피 클라이언트에 히스토리(undo/redo) 기능 구현할건데 백엔드에서 지원해준다고 하면 너무 땡큐였음.
이 히스토리로 구성된 ydoc 데이터는 메모가 업데이트 될 때마다 minio 스토리지에 기록이 되고 소켓연결이 끊기면
백업용 스냅샷 데이터(완전한 텍스트데이터)로 변환해서 또 minio 스토리지에 쓰여짐.
소켓연결이 끊기면 소켓서버에서는 해당 유저의 데이터를 메모리에서 완전히 해제하게 됨.
그렇게 빈 공간이 생기면 그곳에 새로 소켓에 연결되는 유저를 할당하고 minio 스토리지에서 ydoc을 로드해서 실시간 서비스를 시작함.
minio 스토리지는 위와 같이 캐시, 백업스토리지 역할을 수행함.
이제 DB 얘기를 해보겠음.
처음엔 단순하게 스토리지 / DB 로 설계하면 되겠지 했다가 ai서비스를 융합하려고 하니 청크데이터와 벡터데이터라는 놈들이
불쑥 튀어나옴.
ai서비스는 유저가 메모했던 내용에서 검색을 할 수 있게 하는 건데 정확한 키워드가 아니더라도 비슷한 의미의 검색결과가 나오게
하는 서비스로 구상함.
예를 들어 과제애 대한 내용을 메모했다면 해당 과제를 의미하는 단어로도 그 메모를 검색할 수도 있고 gpt나 gemini 같은
llm도 연동시키면 검색 결과를 다시 한 번 가공해서 유저에게 맞춤형 응답을 해줄 수 있음.
위 서비스를 구현하는데 청킹과 임베딩이라는 과정이 필요하고 그로부터 청크데이터와 벡터데이터가 산출됨.
우선 청크데이터는 메모를 한 300바이트정도로 잘게 잘라놓은 데이터이고
벡터데이터는 이 청크데이터를 ai임베딩모델(ai 검색을 위한 분류모델)에 넣어서 벡터라고 하는 수치를 뽑아낸 데이터이며
이 데이터들은 벡터DB에 보관되어서 위 ai서비스에 사용됨.
이때 임베딩 과정에서 gpu리소스가 필요한데 임베딩의 경우만 고려했을 때에는 생각보다 비용이 크게 비싸지 않았음.
웬만하면 클라우드 임베딩 서비스는 비용이 100만 토큰 당 $0.01 정도 되었는데
a4용지 한 장 분량의 한글 텍스트데이터가 보수적으로 1500토큰정도 나옴.
이 1500토큰 정도 되는 데이터를 임베딩하면 비용이 0.02원 정도 나오는데
이정도면 ai서비스도 사용량 보면서 무료로 서비스할만한 정도라고 생각되었음..
데이터를 임베딩하면 내용에 따라서 1024차원(bge-m3 모델 기준)의 벡터데이터(수치)가 나오는데
백터DB가 검색 알고리즘에 따라 이 수치들을 방향, 거리 등으로 비슷한 데이터를 찾아서 응답해줄 수 있게 됨.
청크데이터는 AstraDB라고 하는 클라우드DB로 보관되고 벡터데이터는 Zilliz라고 하는 클라우드 벡터DB로 보관되게 함.
위 DB들을 VM으로 구현하면 각각 AstraDB <-> ScyllaDB, Zilliz <-> Milvus라고 하는 서비스들로 대체할 수 있는데
ScyllaDB와 Milvus가 최소 스펙이 너무 높아서 도저히 VM으로는 클러스터를 구성하기가 어려웠음.
그래서 우선은 클라우드로 사용하다가 차후 비용이 부담이 될 때 옮기는 것으로 구상함.
일반 메타데이터의 경우 설계하다가 데이터 중첩이 많아서 그래프DB를 써보았는데 데이터끼리 연관성을 추상적으로 구현하는게 매우 쉬웠음.
예를 들어 page라는 계층엔 메모 텍스트 데이터에 관련한 메타데이터가 저장되고
node라는 계층엔 그 page들이 모였을 때 필요한 메타데이터가 저장되는데
그래프DB를 사용하면 그래프DB의 노드(그래프DB의 데이터 단위)에 해당 메타데이터를 key/value처럼 바인딩해서 생성하고 관계를 이어주기만하면 됐었음.
물론 DB별로 장단점이 있지만 빠르게 개발하고자 하는 분들이 있다면 메타데이터에 그래프DB를 사용해보는 걸 조심스럽게 추천해봄.
메모데이터는 텍스트데이터지만 최대 1MB까지 데이터가 쌓일 수 있기 때문에 스토리지 데이터로 분류하고 취급했음.
따라서 s3 like 스토리지 서비스만 구축해놓고 저장만 하면 되는 크게 품이 안 드는 구조로 구현해놓았음.
여기까지 구현해놓으니 드는 생각이
어차피 worker는 코드만 잘 짜놓으면 코드상 오류가 아닌 다른 변수로는 서비스가 정지될 일이 없겠다 생각했고
AstraDB나 Zilliz 같은 경우는 클라우드서비스이니 향후 비용말고는 관리에 대한 변수가 없다고 생각했음.
남은 데이터만 변수없이 잘 저장하고 로드되게 하면 백엔드는 이제 크게 건드릴 일 없겠다 판단해서
VM에 각 DB들을 4개 단위로 클러스터링 해놓는 걸로 구현해놓았음.
본인이 봐도 프론트 구현도 안 해놓고 백엔드부터 좀 과하게 하는거 아닌가 생각되는데
또 한 편으로는 이렇게 해놓고 혹시나 알지 못 할 변수로 내려가는 서비스만 새로 붙여주면
프론트만 집중해서 할 수 있게 되는 것도 큰 장점이라 생각해서 조금 무리해서 구성해보았음.
그리고 혹시나 차후 마이그레이션이 필요할 때 마이그레이션 할 서비스를 클러스터에 하나 둘 추가하다보면
큰 리스크없이 안정성과 확장성을 확보할 수 있겠다고 생각했음.
그래서 최종적인 백엔드 구조는
api 서비스 - 클라우드플레어 worker
실시간서비스 - yjs 무상태 소켓서버 3개 분산처리
메타데이터DB - nebula 클러스터 4노드
메모데이터 스토리지 - minio 클러스터 4노드
청크데이터DB - astraDB 클라우드서비스
벡터데이터DB - zilliz 클라우드서비스
로 확정되었고 초기 유저를 보수적으로 고려해서 퍼포먼스 테스트까지 한 번 해보고 프론트 넘어가려고 함.
정신없이 찾고 구현하고 하다가 나름 정리도 한 번 필요한 타이밍이기도해서 작성했으며 누군가에게 도움이 되었으면 좋겠음.
메모하다가 갑자기 협업이니, 멀티디바이스니 혼돈이네. 오버엔지니어링의 전형인듯. 님이 빠진건 고객 페스로나와 유저 시나리오임. 고객이 어떤 기능을 기대하고 왜 이 기능을 다른 것들을 두고 써야하는지 말이야. Sqlite로 떡칠 분량을 클라우드에서 다 처리하고 있네.
혼자 이것저것 써보면서 노는게 사이드 플젝이죠.
개발기간이 긴 것도 아니고 어렵게 구현하지도 않았는데 안정적으로 잘 돌아가면 해볼만 한 거 아닌가요 ㅠㅠ
검색에 왠 벡터데이터를 집어넣는지 ㄹㅇ 이해가안됨
시멘틱서치할거면 그냥 api를써