[DataBase - Design] MySQL + MongoDB를 분리한 이유 (Project: SpeakNote)

2025. 12. 22. 09:04·ServerDev/Database

1. SpeakNote에서 “저장”이 성능 병목이 되는 지점

SpeakNote는 단순히 텍스트를 저장하는 서비스가 아니라, 브라우저에서 들어오는 음성 청크를 실시간으로 받아서 STT로 변환하고, 그 결과를 다시 요약·주석 형태로 가공해 UI에 즉시 반영하는 흐름을 갖고 있습니다. 이때 사용자가 체감하는 품질은 “정확도”도 중요하지만, 실제로는 끊기지 않고 계속 흘러가는지, 그리고 응답 지연이 특정 순간에 폭발하지 않는지가 훨씬 크게 작동합니다. 실시간 시스템은 어느 한 구간이라도 길게 막히면 그 뒤의 모든 단계가 연쇄적으로 밀려서, 결국 사용자는 “가끔 멈추는 서비스”로 인식하게 되기 때문입니다.

 

 문제는 이 파이프라인에서 저장 계층이 생각보다 쉽게 병목이 된다는 점입니다. 주석은 단순 텍스트 한 줄이 아니라, “어느 페이지의 어디 좌표에 어떤 내용이 놓였는지”, “사용자가 수정했는지”, “AI가 생성한 원본은 무엇이었는지”, “세션/문서 버전은 무엇인지” 같은 메타데이터가 함께 붙고, 심지어 드래그앤드롭 UI에서 위치가 자주 바뀌면 업데이트도 빈번하게 발생합니다. 즉, 저장 요청은 짧고 빈번하며, 데이터는 중첩 구조를 갖는 특성이 있습니다. 이런 데이터가 실시간 파이프라인 중간에서 계속 DB에 반영되기 시작하면, 어느 순간부터 DB가 단순 저장소가 아니라 “실시간 처리 파이프라인의 병목점”이 되어버립니다.

 

 여기서 제가 내린 결론은 단순했습니다. 실시간 처리 경로에 들어오는 데이터와, 서비스 운영을 위해 보존해야 하는 데이터를 동일한 규칙과 동일한 DB에 강제로 담으려고 하면, 결국 둘 다 망가진다는 판단이었습니다.


2. 한 DB로 끝내려다 생기는 문제: 정합성과 실시간성의 충돌

처음에는 “MySQL 하나로도 충분하지 않을까”라는 생각을 당연히 했습니다. Spring Boot 기반 서비스에서 MySQL은 익숙하고, 트랜잭션/정합성/인덱싱/조인까지 다 되니 가장 안전한 선택처럼 보였습니다. 하지만 실시간 파이프라인을 실제로 돌려보면, MySQL을 ‘모든 것의 저장소’로 두는 순간부터 문제가 발생합니다.

 

가장 큰 충돌은 정합성을 지키기 위한 비용과 실시간성을 유지하기 위한 요구가 서로 반대 방향으로 움직인다는 점입니다. MySQL을 기반으로 설계하면 자연스럽게 “정규화”, “관계 설정”, “외래키”, “트랜잭션”을 통해 데이터의 일관성을 지키려는 방향으로 가게 되는데, 실시간 시스템에서는 오히려 “일단 흘려보내고, 나중에 정리하는” 방식이 더 안정적일 때가 많습니다. 예를 들어 주석을 저장할 때, 매번 개별 주석 row를 insert/update하고, 페이지/세션/유저와 관계를 맺고, 변경 이력을 관리하려고 하면, 그 순간 DB는 쓰기 부하에 취약해지고, 락/인덱스 업데이트 비용이 누적되면서 지연이 튀기 시작합니다.

 

또 한 가지는 데이터 구조의 충돌입니다. 주석 데이터는 관계형으로 쪼개기엔 구조가 너무 중첩돼 있습니다. 한 문서에는 여러 페이지가 있고, 각 페이지에는 여러 주석이 있고, 각 주석에는 좌표/스타일/태그/출처/버전/수정 이력 같은 필드가 붙습니다. 이를 관계형으로 완벽히 쪼개면 테이블이 늘어나고 조인이 늘어나며, 반대로 테이블을 단순화하면 중복이 늘어나고 업데이트가 괴로워집니다. 결국 “정합성 확보를 위한 설계”와 “실시간 처리 부담을 줄이는 설계”가 계속 충돌하게 됩니다.

 

이 시점에서 저는 저장소를 하나로 통일하는 대신, 저장해야 하는 데이터의 성격을 먼저 분리하기로 했습니다. 그리고 성격이 다른 데이터는 저장소도 분리하는 것이 가장 단순하고 강력한 해결책이라고 결론 내렸습니다.

 

SpeakNote에서 발생하는 데이터는 크게 두 종류로 나뉩니다.

 

첫째는 정형적이고 관계가 명확한 데이터입니다. 사용자, 세션 메타 정보, 파일 메타데이터, 주석 생성 요청 이력 등은 정합성과 트랜잭션 안정성이 매우 중요하며, 조회 패턴도 비교적 예측 가능합니다.

 

둘째는 실시간으로 생성되고 계속 변화하는 데이터입니다. STT 결과가 누적되며 만들어지는 주석 초안, AI 요약 결과의 중간 상태, 사용자가 수정하거나 되돌릴 수 있는 주석 스냅샷 등은 하나의 고정된 스키마로 다루기 어려운 데이터였습니다.

 

SpeakNote 데이터 저장 구조 – MySQL과 MongoDB 역할 분리

 

해당 이미지를 보시면 Service Catalog + MongoDB 쪽은 필드가 자주 바뀌고, 중첩 구조가 많고, 버전별 구조가 달라질 수 있는 아이들입니다. 

이걸 SpeakNote 에 대입 시켜본다면, 

 

  • 페이지 번호
  • 좌표(x, y, width, height)
  • 텍스트
  • 생성 시각
  • 수정 이력
  • 태그/타입

등의 정보들은 구조가 고정되어있지 않고, 계속적인 확장을 진행하기 때문에 MongoDB가 자연스러운 선택이라 볼 수 있습니다.

Service Ordering 부분은 데이터 무결성이 필수이고, 롤백이 필요하며, 관계형 모델이 명확한 정보들인데, 

SpeakNote에 대응시키면 MySQL = 핵심 서비스 메타데이터

  • 사용자
  • 강의 세션
  • 파일 메타정보
  • 권한
  • 프로젝트/노트 단위 정보

이기에 Mysql 을 선택했습니다.

 

서비스 책임에 따라 데이터 특성을 분리했다고 볼 수 있을 거 같습니다.

트랜잭션 / 정합성 MySQL
유연한 구조 / 확장 MongoDB
실시간 / 임시 / 속도 Redis

 


 

3. MySQL에 남겨야 했던 데이터의 성격

 

MySQL에는 “서비스의 기준선이 되는 데이터”, 즉 정합성이 무너지면 서비스 자체가 깨지는 데이터를 둬야 한다고 판단했습니다. SpeakNote 기준으로는 이런 범주입니다.

 

첫째, 사용자/인증/권한 같은 계정 데이터입니다. 이 데이터는 조인과 제약 조건이 명확하고, 일관성이 깨지면 접근 제어 자체가 망가지므로 관계형 DB가 매우 적합합니다. 둘째, 파일 메타데이터나 문서 업로드 정보 같은 “참조를 위한 인덱스”가 되는 데이터입니다. 예를 들어 파일의 생성 시간, 소유자, 접근 토큰, 만료 시간, 저장 경로 같은 값은 조회 패턴이 명확하고, 범용적인 필터링/정렬/검색이 필요하기 때문에 MySQL의 장점이 크게 살아납니다. 셋째, 결제/구독 같은 상용화 요소가 붙는다면 반드시 ACID가 필요한 영역이므로 이 또한 MySQL이 맞습니다.

 

즉, MySQL은 SpeakNote에서 “서비스 운영의 뼈대”를 잡는 곳입니다. 이 뼈대는 반드시 정합성을 지켜야 하고, 데이터 모델이 비교적 평면적이며, 조회가 다면적이고, 제약조건이 명확합니다. 그래서 MySQL은 변하지 않는 핵심 데이터의 저장소로 두는 것이 가장 납득 가능한 선택이었습니다.


4. MongoDB로 보내야 했던 데이터의 성격

반면 MongoDB로 보낸 것은 “실시간으로 계속 변하고, 구조가 중첩되고, 쓰기 패턴이 거칠며, 버전/스냅샷이 중요한 데이터”입니다. SpeakNote에서 그 대표가 바로 주석 데이터입니다.

 

주석 데이터는 생성 이후에도 계속 수정되고, 위치가 바뀌고, 삭제/복원되기도 합니다. 또한 사용자가 주석을 페이지별로 편집할 수도 있고, AI가 생성한 주석을 사용자가 일부만 채택할 수도 있습니다. 이 과정을 “주석 하나의 row 단위 변경”으로 관리하면 관계형에서도 가능은 하지만, 실시간 시스템에서는 오히려 데이터 변경 단위가 너무 잦아져서 저장 비용이 올라가고, 변경 이력을 남기거나 복원을 지원하려 할 때 복잡도가 폭발합니다.

 

MongoDB는 이 지점에서 강점이 명확합니다. 문서형 DB는 기본적으로 중첩 구조를 자연스럽게 담을 수 있고, “한 문서(도큐먼트)”를 단위로 원자적으로 갱신할 수 있습니다. SpeakNote에서 제가 바라본 이상적인 단위는 “주석 하나”가 아니라 “문서(혹은 세션)의 주석 상태 전체”였습니다. 즉, 사용자가 보는 것은 결국 특정 시점의 주석 스냅샷이며, 실시간 시스템에서는 이 스냅샷을 빠르게 저장하고 빠르게 불러오는 것이 핵심이 됩니다.

 

또한 MongoDB는 조회 패턴이 “한 번에 통으로 가져오기”에 최적화되기 때문에, PDF 렌더링 화면에서 주석을 한 번에 복원해야 하는 SpeakNote UI와도 잘 맞습니다. 페이지/주석 단위로 조인을 여러 번 하는 대신, 문서 단위로 한 번에 가져오면 프론트도 단순해지고 서버도 단순해집니다.


5. “주석은 왜 문서형 스냅샷이 맞는가”

 

주석 데이터를 스냅샷으로 보는 관점은 단순히 MongoDB를 쓰기 위해 끼워 맞춘 것이 아니라, SpeakNote UX 자체가 스냅샷 모델에 가깝기 때문에 자연스럽게 도출된 결론이었습니다.

 

사용자는 강의를 듣는 동안 주석을 계속 생성하고 편집하지만, 어느 시점이든 “현재 화면에 표시된 상태”가 가장 중요합니다. 또한 저장/다운로드 시점에는 “그 순간의 주석 상태”를 PDF에 고정해야 합니다. 결국 시스템이 제공해야 하는 것은 “주석 상태의 현재 버전”이며, 필요하다면 “이전 버전으로 롤백할 수 있는 기록”입니다. 이 관점에서 관계형의 이벤트 로그 방식(주석 변경을 모두 row로 누적)은 구현이 가능하더라도 복잡하고, 읽기 복원 비용이 커지며, 실시간 렌더링과 맞지 않습니다.

 

반대로 스냅샷은 단순합니다. 특정 문서/세션에 대해 “현재 버전의 스냅샷”을 유지하고, 업데이트가 발생하면 스냅샷을 갱신하며, 필요하다면 version 필드를 올려 이전 스냅샷을 남기면 됩니다. 이렇게 하면 읽기는 항상 단순해지고, 쓰기는 중첩 구조 전체를 다루더라도 하나의 문서 업데이트로 정리됩니다. SpeakNote의 요구사항인 “실시간 주석 표시 + 다운로드 시 상태 고정”에 매우 직접적으로 부합하는 구조였습니다.


6. 분리 이후 얻은 것: 지연 안정성, 확장성, 운영 난이도

 

MySQL과 MongoDB를 분리한 이후 가장 크게 체감된 것은 “지연이 튀는 지점이 줄었다”는 점이었습니다. 실시간 파이프라인에서 가장 무서운 것은 평균 지연이 아니라, 특정 순간에 발생하는 지연 스파이크인데, 저장 계층이 단일 DB였을 때는 부하가 몰리는 순간의 락/인덱스 갱신/연쇄 쿼리가 스파이크를 만들기 쉬웠습니다. 반면 주석은 MongoDB에서 스냅샷 단위로 처리하고, MySQL은 핵심 메타데이터만 관리하도록 역할을 분리하니, DB가 파이프라인 전체를 막는 경우가 크게 줄었습니다. 확장성도 좋아졌습니다. MySQL은 트랜잭션이 중요한 영역을 안정적으로 유지하고, MongoDB는 주석 저장 용량이 늘어나더라도 샤딩/수평 확장 여지가 있습니다. 또한 운영 관점에서도 “어떤 문제가 발생했을 때 어디를 먼저 의심해야 하는지”가 명확해집니다. 계정/파일/권한 문제가 생기면 MySQL을 보고, 주석 복원/버전 문제는 MongoDB를 보면 됩니다. 장애 범위가 분리되는 것이 실서비스 운영에서 생각보다 큰 장점이 됩니다.


7. 트레이드오프: 분산 저장의 비용과 저의 선택 기준

 

물론 저장소 분리는 공짜가 아닙니다. 가장 큰 비용은 “데이터가 두 곳에 나뉘어 있으므로, 모델링과 운영이 복잡해진다”는 점입니다. 예를 들어 주석 스냅샷의 소유자/권한/파일 참조는 결국 MySQL의 데이터를 기반으로 검증해야 하므로, 두 저장소 사이의 연결 키를 어떻게 잡을지(예: fileId, sessionId), 삭제 정책을 어떻게 맞출지(파일 삭제 시 MongoDB 스냅샷도 정리할지), TTL이나 만료 정책을 어떻게 운영할지 같은 의사결정이 늘어납니다.

 

하지만 저는 SpeakNote의 핵심 가치를 “실시간 파이프라인이 멈추지 않는 것”이라고 놓고 봤을 때, 이 복잡도를 감수하는 편이 더 합리적

이라고 판단했습니다. 특히 실시간 시스템에서는 저장 계층이 느려지는 순간 사용자가 바로 불편을 느끼고, 그 불편은 UI나 모델 성능으로 덮기 어렵습니다. 그래서 설계 기준을 “정합성 100%를 모든 데이터에 적용하기”가 아니라, “정합성이 필요한 데이터와 유연성이 필요한 데이터를 분리하기”로 잡았고, 저장소 분리는 그 기준을 가장 직접적으로 만족시키는 선택이었습니다.

 


8. 마무리 및 다음 글 예고

 

정리하면, SpeakNote에서 MySQL과 MongoDB를 분리한 이유는 단순히 기술 스택을 다양화하려는 목적이 아니라, 실시간 시스템에서 흔히 발생하는 “정합성과 실시간성의 충돌”을 구조적으로 해결하기 위한 선택이었습니다. MySQL은 서비스 운영의 기준선이 되는 데이터에 집중하고, MongoDB는 실시간으로 변하며 중첩 구조를 갖는 주석 상태를 스냅샷 형태로 담는 역할을 맡으면서, 저장 계층이 실시간 파이프라인의 병목이 되는 상황을 줄이고자 했습니다.

 

다음 글에서는 실제로 MongoDB에 주석을 어떤 구조로 저장했는지, 즉 “주석 스냅샷 스키마를 어떻게 잡았는지”, 그리고 “버전/복원/다운로드(PDF 고정)까지 고려했을 때 어떤 필드가 필요했는지”를 더 구체적으로 다루겠습니다. MongoDB의 도큐먼트 모델링 자체에 대한 설명이나, 스냅샷 vs 이벤트 소싱 같은 설계 비교는 내용이 길어지기 때문에 별도의 글로 분리해서 정리하겠습니다. 

 

https://ye-seul0-0.tistory.com/129

 

[DataEngineering - MongoDB] 실시간 주석 스냅샷 스키마 설계 (Project: SpeakNote)

1. 실시간 주석 데이터는 왜 기존 DB 모델로 다루기 어려운가SpeakNote에서 다루는 주석 데이터는 일반적인 CRUD 중심의 데이터와 성격이 완전히 다릅니다.주석은 한 번 생성되고 끝나는 데이터가 아

ye-seul0-0.tistory.com

 

 

'ServerDev > Database' 카테고리의 다른 글

[DataEngineering - MongoDB] 실시간 주석 스냅샷 스키마 설계 (Project: SpeakNote)  (0) 2025.12.24
'ServerDev/Database' 카테고리의 다른 글
  • [DataEngineering - MongoDB] 실시간 주석 스냅샷 스키마 설계 (Project: SpeakNote)
yeseul-kim01
yeseul-kim01
  • yeseul-kim01
    슬 개발일지
    yeseul-kim01
  • 전체
    오늘
    어제
    • 분류 전체보기 (79)
      • 자격증 (1)
        • 정보보안기사 (0)
      • DevOps (17)
        • Docker (6)
        • Kubernetes (1)
        • GitHub Actions (0)
        • AWS (4)
        • Monitoring (1)
        • Nginx (1)
        • GCP (3)
      • ServerDev (34)
        • SpringBoot (13)
        • DJango (5)
        • FastAPI (14)
        • Next (0)
        • Flask (0)
        • Database (2)
      • Algorithm (2)
        • BFS (0)
        • DFS (1)
        • 다익스트라 (0)
      • CS (8)
      • Data Engineering (1)
      • AI&MLOps (2)
      • Architecture (6)
      • Software Engineering (0)
        • Library Packaging (0)
      • Project (5)
        • docx-generator (0)
        • speak-note (2)
        • ms-serving (1)
        • keyshield (2)
      • ProgrammingLanguages (3)
        • Python (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    SpeakNote
    FastAPI - CORS 마스터
    SpringBoot
    아키텍처
    멀티모듈
    프로젝트기록-KeyShield
    실무일기-인프라편
    실시간시스템
    Django
    동시성제어
    multipartfile
    rag
    depends
    grpc
    docker
    STT
    di
    KeyShield
    NLP부트캠프
    트러블슈팅
    MLops
    비동기처리
    프로젝트기록-speaknote
    백엔드
    하이브리드아키텍처
    아키텍처설계
    FastAPI
    Kubernetes
    KServe
    실무일기-백엔드편
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
yeseul-kim01
[DataBase - Design] MySQL + MongoDB를 분리한 이유 (Project: SpeakNote)
상단으로

티스토리툴바