초반에는 모든 게 순조로웠다. timetable_db라는 커다란 데이터베이스 하나에 시간표, 캘린더, 할 일, 사용자 프로필, 모듈 설정까지 몽땅 때려 넣고 개발하는 건 확실히 빨랐다. 하지만 기능 정리를 어느 정도 마치고 프로젝트를 확장하려고 보니, 하나의 앱 데이터베이스에 모든 모듈이 찰싹 달라붙어 있는 기형적인 구조가 눈에 밟혔다.
하나의 거대한 가방에 모든 물건을 욱여넣으면, 당장 꺼내 쓰긴 편해도 짐이 20개, 30개로 늘어나면 안은 아수라장이 된다. 배포, 테스트, 데이터 구조 변경이 모두 한 공간에 묶여 있다 보니, 달력 기능 하나 고치려다 시간표 전체가 뻗어버리는 끔찍한 결말이 뻔히 보였다. 진정한 모듈화를 이루려면 데이터베이스 분리가 필수라는 결론이 나왔다. 앞으로 모듈이 무한정 늘어날 것을 생각하면 나중에 피눈물 흘리며 분리하느니 지금 당장 뼈대를 쪼개는 편이 훨씬 싸게 먹히는 길이었다.
대공사 전 세운 철칙과 데이터 분리 기준
가장 먼저 데이터베이스의 경계를 명확히 긋기 위한 원칙을 세웠다. 공용 데이터와 개별 도메인 데이터가 한곳에서 뒹굴게 둘 수는 없었다.
데이터는 크게 두 가지로 구분했다.
- 공용 데이터 (workspace_core)
→ 활성 모듈 목록, 사용자 정보, 워크스페이스 상태
→ 시스템 전체의 기준이 되는 중심 데이터 - 도메인 데이터 (timetable_db, calendar_db 등)
→ 시간표, 캘린더 이벤트, 할 일 등
→ 각 기능에 종속된 독립 데이터
처음에는 사용자 정보를 기존 시간표 DB에 계속 넣고 싶은 유혹도 있었다.
하지만 그렇게 되면 다른 모듈들이 시간표 DB에 의존하게 되는 구조가 된다.
그래서 공용 데이터는 workspace_core라는 별도 공간으로 완전히 분리했다.
모든 모듈은 이 코어를 기준으로만 동작하도록 강제했다.
사용자 프로필이나 모듈 상태를 그냥 기존 시간표 데이터베이스의 유저 테이블에 계속 밀어 넣고 싶은 유혹도 있었다. 하지만 그렇게 되면 캘린더나 할 일 모듈이 시간표 데이터베이스를 또다시 쳐다봐야 하는 꼬인 구조가 된다. 그래서 workspace_core라는 전용 공간을 파고 공통 프로필과 모듈 선호도 상태를 완전히 독립시켰다.
파편화된 시스템을 잇는 식별자
데이터를 쪼개놓고 나니 서로 다른 데이터베이스 간에 데이터를 어떻게 엮을지가 문제였다. 기존처럼 데이터베이스 수준의 외래 키를 직접 거는 건 분산 환경에서 아키텍처를 망가뜨리는 지름길이다.
초기에는 이메일을 기준으로 삼을까 했지만, 사용자가 이메일을 변경하는 순간 시스템 전역에 혼란이 올 수밖에 없다. 그래서 이메일보다 훨씬 안정적인 authUserId를 공통 연결 키로 통일했다. 이 고유 식별자만 있으면 물리적으로 떨어진 데이터베이스라도 애플리케이션 레벨에서 얼마든지 유연하게 참조하고 엮어낼 수 있다.
프리즈마의 한계와 런타임 분리 삽질
코드 레벨의 뼈대도 싹 갈아엎어야 했다. 프리즈마 하나로 모든 스키마를 밀어버리던 구조에서 벗어나, core, timetable, calendar, tasks 단위로 런타임 경계를 쪼갰다. 실제 물리적인 데이터베이스를 여러 개로 찢어버리기 전, 애플리케이션 내부에서 먼저 논리적인 격벽을 치기 위한 중간 단계였다.
이 과정에서 도커와 프리즈마가 환장의 콜라보를 만들어냈다. 앱을 띄우는 런타임 이미지와 데이터 구조를 갱신하는 마이그레이터 이미지를 하나로 뒀더니 프리즈마 의존성이 미친 듯이 꼬여버렸다. 결국 실제 서비스를 돌리는 런타임 이미지에는 가벼운 실행 환경만 남기고, 스키마 동기화를 담당하는 마이그레이터 이미지를 완전히 분리해서 띄웠다.
- 런타임: 실제 서비스 실행만 담당
- 마이그레이터: 스키마 변경 전용
이렇게 나누고 나서야 배포 파이프라인이 안정됐다.
조용한 실패와 운영 현장에서 맞은 뒤통수
데이터베이스를 여러 개로 쪼개어 운영 환경에 올리니 엉뚱한 곳에서 지뢰가 터졌다.
가장 골치 아팠던 건 분할된 데이터베이스 접속 주소가 누락되었을 때의 대처였다. 설정값이 없으면 시스템이 낡은 기본값으로 우회해서 엉뚱한 데이터를 찌르는 환장할 상황이 발생했다. 이 조용한 실패를 당장 금지하고, 환경 변수나 접속 정보가 하나라도 누락되면 시스템이 에러를 내며 뻗어버리는 빠른 실패 구조로 방어막을 쳤다. 마이그레이터가 실제로 어떤 데이터베이스를 쳐다보고 있는지 꼼꼼하게 검증하는 로직도 추가했다.
복구와 동기화 과정도 순탄치 않았다. 핵심 공간인 workspace_core에 일시적인 장애가 나면, 변경된 모듈 구성 정보가 허공으로 증발해버리는 아찔한 문제가 있었다. 코어 저장소가 복구된 후 낡은 데이터가 최신 설정을 덮어쓰거나, 기본 모듈이 초기화될 때 여러 요청이 동시에 몰리며 경쟁 상태가 발생하는 등 온갖 트러블슈팅을 거쳐야만 했다.
지금 우리가 서 있는 곳, 그리고 앞으로의 룰
이런 대공사를 치르기 전 문서와 코드 리뷰 기준을 먼저 빡빡하게 세워둔 덕에 험난한 여정을 버틸 수 있었다. 현재 workspace_core는 완벽히 분가했고, 캘린더와 할 일 모듈도 런타임 분리를 무사히 마쳤다. 앞으로 붙일 프로젝트 모듈도 분리 예정이며, 최초의 뼈대였던 시간표 데이터베이스에는 아직 레거시가 약간 묻어있어 청소가 필요한 상태다.
고생 끝에 시스템의 경계를 완벽하게 재정의했다. 이제 새로운 모듈을 추가할 때 지켜야 할 명확하고 단단한 룰이 생겼다.
- 새 모듈은 무조건 처음부터 별도의 데이터베이스로 띄운다.
- 공용 데이터는 무조건 코어에, 모듈 데이터는 철저히 각 모듈에 보관한다.
- 데이터베이스 주소 누락 시 조용히 넘어가지 않고 무조건 터뜨린다.