-
Next.js와 Node.js 기반 실시간 대용량 편집기 Part3. 동기화 알고리즘 & 보안Tip 2025. 5. 6. 23:37728x90반응형
Yjs 1편과 2편에서는 시스템 아키텍처 설계와 실제 구현 방법 및 성능 최적화를 다뤘습니다.
3편에서는 협업 편집기의 핵심인 데이터 동기화 알고리즘을 분석하고, 보안 및 효율적인 운영 관리를 위한 전략을 살펴보겠습니다.
1. 실시간 협업을 위한 동기화 알고리즘: CRDT vs OT
실시간 협업 편집기의 백미는 여러 사용자의 동시 편집을 일관성 있게 병합하는 동기화 알고리즘입니다.
대표적인 기법으로 OT(Operational Transformation)와 CRDT(Conflict-Free Replicated Data Type)가 있으며, 역사적으로 OT는 Google Docs 등에서 활용되어 왔고, 최근에는 CRDT 기반의 Yjs, Automerge 등이 각광받고 있습니다.
1.1 OT (Operational Transformation)
OT는 사용자들의 편집 연산(operation)을 중앙 서버가 수집하여, 충돌이 발생하지 않도록 다른 연산에 따라 변형(transform)을 가한 후 전파하는 방식입니다.
예를 들어 User1이 "ABC" 문서에서 1번 위치에 "X"를 삽입하고, 거의 동시에 User2가 0번 위치의 문자를 삭제했다고 할 때, 두 연산을 순서 다르게 적용하면 결과가 달라집니다.
OT 알고리즘을 적용한 중앙 서버는 한 연산을 다른 연산의 영향 하에 재계산하여 일관성 유지합니다.
위 예에서 User2의 delete(0)을 서버는 User1의 insert로 인해 delete(1)로 인덱스 조정한 후에 User1에게 보냄으로써, 결국 모두 같은 최종 문서를 보게 됩니다.
OT의 장점은 적은 데이터양으로 동기화가 가능하고, 개념적으로 연산만 주고받으므로 저장이 간편하다는 것입니다.
또한 중앙집중형으로 설계하면 알고리즘이 단순해집니다.
그러나 단점으로 완전 분산 환경(중앙 서버 없는 P2P)을 지원하기 어렵고, 구현이 복잡합니다.
특히 텍스트 외에 이미지 객체, 스타일 등 리치 텍스트 요소까지 OT로 처리하려면 연산의 종류가 기하급수적으로 늘고 알고리즘 난이도가 매우 높습니다.
즉, OT는 중앙 서버 기반 실시간 협업에는 잘 맞지만, 오프라인 편집이나 분산 네트워크에는 부적합하며 구현 시 고려해야 할 예외 상황이 많습니다.
1.2 CRDT (Conflict-Free Replicated Data Type)
CRDT는 데이터를 특수한 방법으로 구조화하여, 여러 복제본에서 동시에 편집해도 최종적으로 자동으로 병합될 수 있게 한 자료구조입니다.
문자 하나하나에 전역적으로 unique 한 ID와 순서 관계를 부여하고, 삽입/삭제 등을 수행할 때 그 ID 체계를 유지하면서 변경하는 식으로 동작합니다.
이렇게 하면 각 사용자가 편집한 결과가 설령 순서가 달라도, 나중에 전체 복제본을 모았을 때 ID 기준으로 정렬하거나 중복 제거하여 일관된 상태를 얻을 수 있습니다.
쉽게 말해, OT가 "연산을 조정"하는 데 반해 CRDT는 "데이터 자체에 충돌을 흡수하는 정보"를 지니게 합니다.
CRDT의 장점은 분산 환경에서 별도의 조정 없이도 최종 상태가 동일해진다는 점 (이것을 Strong Eventual Consistency라고 함)입니다.
따라서 중앙 서버가 없어도 동작 가능하며, 오프라인 작업 후 동기화에도 강합니다.
또한 이론적으로 사용자 수가 늘어나도 OT처럼 연산 변형을 중앙에서 계산할 필요 없이, 모든 복제본이 알아서 합쳐주므로 네트워크 토폴로지의 자유도가 높습니다.
단점으로는 데이터 구조가 OT보다 복잡하여 메모리 오버헤드가 있고, 아주 많은 변경이 누적되면 CRDT 데이터가 커질 수 있다는 점입니다.
또한 완전히 같은 효과를 내더라도 ID 할당 순서 등에 따라 내부 표현이 다를 수 있어 디버깅이 어렵고, 설계 및 구현 난이도가 높습니다.
하지만 최근 연구와 구현체들의 발전으로 이러한 단점이 많이 완화되었습니다.
Yjs와 Automerge는 모두 CRDT의 실제 구현 라이브러리입니다.
두 가지를 비교하면, Yjs는 문서 편집에 특화된 CRDT로 성능과 효율 면에서 뛰어나며, 바이너리 형식으로 업데이트를 주고받아 네트워크 부담이 적습니다.
// components/Editor.tsx import * as Y from 'yjs'; import { WebsocketProvider } from 'y-websocket'; import { useEffect, useRef } from 'react'; export function Editor({ roomId }: { roomId: string }) { const editorRef = useRef<HTMLDivElement>(null); useEffect(() => { // Yjs 문서 및 공유 텍스트 생성 const doc = new Y.Doc(); const yText = doc.getText('content'); // WebSocket Provider로 동기화 const provider = new WebsocketProvider('wss://yjs.example.com', roomId, doc); // 로컬 변경을 DOM에 반영 yText.observe(event => { editorRef.current!.innerText = yText.toString(); }); // DOM 입력을 Yjs에 적용 const handleInput = () => { const text = editorRef.current!.innerText; doc.transact(() => { yText.delete(0, yText.length); yText.insert(0, text); }); }; editorRef.current!.addEventListener('input', handleInput); return () => { provider.disconnect(); doc.destroy(); }; }, [roomId]); return <div ref={editorRef} contentEditable className="editor" />; }
Automerge는 JSON 기반으로 좀 더 범용적인 CRDT이지만, 과거 버전은 메모리/속도 이슈가 지적되었고 최근 개선되고 있습니다.
import Automerge from 'automerge'; let doc = Automerge.init(); doc = Automerge.change(doc, d => { d.text = 'Hello'; }); const changes = Automerge.getLastLocalChange(doc); // 변경 전파: changes 바이트 배열 전송 sendToServer(changes); // 원격 변경 적용 function onRemote(changes: Uint8Array[]) { doc = Automerge.applyChanges(doc, changes); }
Yjs 측의 벤치마크를 보면, Yjs가 Automerge보다 훨씬 빠르고 메모리 효율적임을 알 수 있습니다 (Automerge v1 기준; v2는 개선됨).
또한 Yjs는 풍부한 자료형(shared types)과 Undo/Redo, 커서 공유 등의 편의 기능이 있고, 네트워크 아키텍처 독립적이라 WebSocket, WebRTC 등 어떤 전송이든 붙일 수 있습니다.
Yjs는 수백 명의 동시 편집과 대형 문서도 잘 지원한다고 알려져 있어, 실시간 대용량 편집기 서비스에서는 CRDT + Yjs 조합이 유력한 선택입니다.
Automerge도 API가 단순하고 사용법이 쉬워 매력적이며, 완전 오프라인 P2P라면 Automerge + Hypercore 프로토콜 같은 실험적인 조합도 가능합니다.
정리하자면, 중앙 서버 방식의 한 문서당 수백 명 편집이라면 OT도 구현 가능하고 초기 알고리즘이 단순할 수 있으나, 오프라인 편집, 분산 처리, 풍부한 객체 동기화 측면에서 CRDT 기반이 미래지향적입니다.
특히 CRDT(Yjs 등)는 충돌을 자동으로 해결해 주므로 사용자 경험이 부드럽고, P2P나 브라우저 오프라인 저장을 쉽게 지원할 수 있습니다.
따라서 시스템 설계 시 CRDT (예: Yjs)를 사용하여 충돌 없는 협업을 구현하고, 만약 기존에 OT 라이브러리나 알고리즘(예: ShareDB)을 쓴다면 이를 대체/통합하는 방향을 고려합니다.
2. 데이터 저장 전략: 실시간 DB 업데이트 vs 세션 기반 커밋
실시간 편집 데이터의 영구 저장(persistence)을 어떻게 할 것인가는 시스템의 일관성과 성능에 큰 영향을 줍니다.
두 가지 극단적인 접근이 있는데, (A) 매 변경을 바로바로 DB에 반영하거나, (B) 편집 세션 동안은 메모리나 임시 저장에 두었다가 끝날 때 한꺼번에 반영하는 방식입니다.
2.1 실시간 DB 업데이트 방식
사용자의 편집 연산이 들어올 때마다 즉각적으로 데이터베이스에 적용합니다.
예를 들어 문서 내용 전체를 저장하는 시스템이라면, 매 글자 입력마다 문서 레코드를 업데이트하거나, 변경 diff 로그 테이블에 insert 하는 식입니다.
이 방식의 장점은 서버 메모리에만 존재하는 변경사항이 거의 없으므로 충돌 시 즉각 롤백하거나, 다른 서비스에서 그 데이터를 곧바로 활용할 수 있다는 점입니다.
또한 장애 발생 시 마지막 DB 상태까지의 변경은 안전하게 저장되어 있어 데이터 유실 위험이 낮습니다.
단점은 당연히 DB 부하가 매우 크다는 것입니다.
수십 명의 타이핑이 매초 수백 건의 트랜잭션을 일으킬 수 있고, 이는 RDBMS이든 NoSQL이든 상당한 부담입니다.
또한 DB에 쓰는 속도 < 편집 속도가 되면 곧 병목이 생겨 편집 지연이 발생할 수 있습니다.
따라서 이 방식은 주로 간단한 협업 메모 앱처럼 트래픽이 낮은 경우나, 아니면 충분히 확장 가능한 DB (예: DynamoDB) + 배치식 처리를 병행하는 형태로나 고려됩니다.
2.2 세션 단위 임시 저장 후 커밋 방식
사용자가 문서를 열고 편집하는 동안의 변경을 일단 서버 메모리나 캐시에 보관하고, 편집 세션이 종료되거나 일정 시간이 지나면 최종 결과를 DB에 커밋합니다.
예를 들어 Google Docs의 “모든 변경사항이 저장되었습니다”는 일정 휴지기가 생기면 그 시점의 문서를 Drive에 저장하는 것을 의미합니다.
CRDT를 쓴다면, 편집 중에는 CRDT 상태를 메모리에 유지하고, 일정 주기마다 CRDT 전체 상태 또는 diff를 DB에 저장하거나, 문서를 닫을 때 최종 문자열을 저장할 수 있습니다.
장점은 평소 DB 트랜잭션 부담이 매우 적고, 사용자 경험이 부드러워집니다.
최종 저장만 하면 되니 DB I/O를 크게 줄일 수 있습니다.
단점은 만약 서버가 다운되거나 세션 데이터가 유실되면 그 사이 변경이 잃어버리게 된다는 점입니다.
이를 보완하려면 체크포인트를 주기적으로 찍는 것이 좋습니다.
예컨대 5분마다 한 번씩 현재 문서 상태를 DB에 백업해 둔다면, 서버 장애 시 최대 5분 작업량만 잃고 복구할 수 있습니다.
혹은 아예 Redis 같은 인메모리 DB에 복제본을 저장해 두고, 서버 장애 시 그걸 다른 서버가 이어받게 하는 방안도 있습니다.
현실적인 설계에서는 위 방법들을 절충하는 경우가 많습니다.
변경 기록 로그를 별도로 남기는 전략이 그것입니다.
모든 변경 연산 (OT이든 CRDT이든)이나 버전 스냅샷을 append-only 로그로 파일 또는 DB에 기록해 두면, 온라인으로 DB 상태를 매번 업데이트하지 않더라도 최소한 이력은 쌓입니다.
나중에 이 로그를 적용해 최종 상태를 재현하거나, 장애 시 해당 로그를 기반으로 복구할 수 있습니다.
예를 들어 CRDT 업데이트(Yjs의 update 메시지 등)를 수신할 때마다 Redis 스트림이나 Kafka 토픽에 쌓아두고, 별도 워커가 이를 읽어 주기적으로 DB 문서 스냅샷을 갱신하는 방식도 가능합니다.
이러면 실시간 서비스 성능과 데이터 안정성 둘을 어느 정도 잡을 수 있습니다.
데이터베이스 선택도 중요한 고려사항입니다.
문서 저장에는 보통 NoSQL (JSON 문서형 DB)이나 RDB에 텍스트 필드 저장, 또는 파일 저장소(예: S3에 HTML/json 파일) 등의 방법이 있습니다.
Document DB나 RDB를 쓰면 쿼리로 문서를 검색하거나 버전을 관리하기 편리합니다.
예를 들어 DynamoDB를 사용하면 하나의 문서에 대한 업데이트 쓰기 throughput을 넉넉히 설정해 두고, 최종 상태와 버전을 item으로 관리할 수 있습니다.
반면 S3 같은 객체 스토리지에 최종본을 저장하면 이력 관리나 동시 업데이트 반영에는 직접 로직을 짜야합니다 (예: S3 object versioning을 켜거나).
협업 편집기의 경우 버전 관리(history)도 요구될 수 있는데, 모든 변경 로그를 영구 저장하면 storage 용량이 빠르게 증가할 수 있어 압축 및 정리 정책이 필요합니다 (ex: 30일 이전의 편집 이력은 삭제 혹은 요약).
정리하면, 실시간 대용량 편집기에서는 세션 기반 임시 저장 + 주기적 커밋을 기본으로 하되, 주기적인 스냅샷 저장 또는 로그 저장을 병행하여 데이터 안정성을 높이는 방향이 바람직합니다.
사용자 경험상 "저장" 버튼 없이도 자동으로 저장되되, 내부적으로는 약간 지연을 두고 모아서 저장하는 것입니다.
CRDT(Yjs)를 쓴다면 Yjs 문서 상태를 일정 간격으로 encodeStateAsUpdate 하여 DB에 저장해 둘 수 있습니다.
OT라면 최종 문자열을 저장하거나, diff를 누적해 둘 수 있겠습니다.
이 전략을 통해 실시간 성능과 데이터 영속성의 균형을 맞출 수 있습니다.
3. TLS 인증과 보안 통신 구축
협업 서비스는 민감한 데이터를 다룰 수 있으므로, 전송 구간 보안과 인증이 필수입니다.
기본 원칙은 모든 통신을 TLS 위에서 수행하는 것으로, 웹 프론트엔드와 Node.js 백엔드 간의 WebSocket 연결도 wss:// (WebSocket Secure) 프로토콜로 설정해야 합니다.
AWS 환경에서 TLS는 일반적으로 로드밸런서 (ALB) 또는 API Gateway에서 종단(terminate)하거나, Kubernetes Ingress에서 인증서를 적용하여 처리합니다.
Let’s Encrypt 등의 인증서를 이용해 무료 TLS 세팅을 할 수도 있고, ACM(AWS Certificate Manager)을 통해 인증서를 관리할 수도 있습니다.
클라이언트 측에서는 그냥 wss:// URL을 쓰기만 하면 브라우저가 자동으로 TLS handshake 및 암호화 통신을 처리합니다.
3.1 인증(Authentication)
편집기 서비스는 사용자 로그인이 필요할 것입니다.
WebSocket 연결을 맺을 때 HTTP의 쿠키를 재사용하거나, Query Param 또는 Subprotocol 헤더를 통해 JWT 토큰 등을 전달하여 서버가 사용자를 식별하게 합니다.
예를 들어 new WebSocket("wss://server.com/doc?token=abcd")처럼 토큰을 보내면, 서버 쪽 handshake 요청 URI에서 토큰을 읽어 검증합니다.
또는 Socket.IO를 쓰는 경우 연결 직후 authenticate 이벤트로 토큰을 보내게 할 수도 있습니다.
TLS로 암호화된 통신이므로 토큰도 안전하게 전송됩니다.
서버는 이 토큰을 검사하여 유효한 사용자이고 해당 문서에 편집 권한이 있는지 확인한 뒤 접속을 허용합니다.
이 과정에서 문서 접근 권한(예: 읽기 전용 또는 편집 권한)을 확인하여, 권한 없는 사용자는 편집 이벤트를 무시하거나 뷰만 제공하도록 처리해야 합니다.
3.2 데이터 권한 분리
만약 여러 조직이나 팀이 사용하는 협업 도구라면, 한 사용자가 다른 팀 문서 데이터를 수신해서는 안 됩니다.
이를 위해 서버에서 방(Room) 개념으로 구분했던 것을 엄격히 적용하여, 한 문서의 이벤트는 해당 문서에 속한 사용자 소켓들에만 전송합니다.
또한 클라이언트가 악의적으로 다른 문서 ID로 요청을 보내더라도 권한 검증을 다시 확인하는 이중장치를 둡니다.
3.3 암호화 및 무결성
TLS 자체로 충분히 암호화되지만, 혹시 모를 중간자 공격이나 로드밸런서 이후 구간 보안을 위해, 내부 통신도 TLS 또는 VPC 네트워크로 보호합니다.
예를 들어 AWS의 로드밸런서에서 TLS 종료 후 Kubernetes 서비스 간 통신은 일반적으로 클러스터 내부망으로 안전하지만, 추가로 Pod 간 mTLS를 구현하려면 서비스 메쉬(Istio 등)를 검토할 수도 있습니다.
다만, 일반 웹 애플리케이션 수준에서는 외부->LB 구간 TLS면 충분합니다.
또한 WebSocket 메시지 레벨에서 해시 검증이나 서명까지는 과할 수 있으나, 중요한 명령 (예: 문서 삭제 등)은 서버에서 재검증(예: 이 사용자 정말 삭제 권한 있는지)을 수행합니다.
3.4 콘텐츠 보안
편집 내용 중에 개인정보나 기밀정보가 있을 수 있으므로, 서버에 로그 남길 때 민감한 내용은 기록하지 않거나 마스킹합니다.
예컨대 “사용자 A가 문서 X 편집” 정도는 로그 하지만, 실제 편집한 텍스트 내용은 로그에 남기지 않는 식입니다.
또, XSS나 악성 파일 업로드 등 클라이언트 사이드 보안도 고려해야 합니다.
편집기 콘텐츠에 스크립트가 삽입되지 않도록 input을 적절히 이스케이프 하거나, 이미지 확장자 및 MIME type을 검사하여 안전한 타입만 허용합니다.
특히 실시간 협업이지만 본질적으로 웹 앱이므로 Content Security Policy(CSP) 등을 설정해 두면 좋습니다.
정리하면, TLS를 통한 암호화 통신 (wss://)은 기본이고, JWT 등으로 사용자 인증 및 권한제어를 수행하며, 서버-서버 내부 통신도 안전하게 구성하여 전반적인 보안 레벨을 높입니다.
이를 통해 악의적인 접근이나 도청 없이, 오직 권한 있는 사용자들만 안전하게 협업할 수 있는 환경을 구축합니다.
반응형'Tip' 카테고리의 다른 글
Next.js와 Node.js 기반 실시간 대용량 편집기 Part2. 구현 및 성능 최적화 (0) 2025.05.03 Next.js와 Node.js 기반 실시간 대용량 편집기 Part1. 아키텍처 & 프로토콜 (0) 2025.04.27 스키마 유효성 검사 라이브러리 비교(feat. Zod vs Yup vs Joi) (1) 2025.01.05 C4 Model for Visualizing Frontend Architecture (Feat. FSD) (2) 2024.12.29 HTTP 클라이언트 라이브러리의 변화 Axios에서 Got, Ky로 (1) 2024.12.08