병합 전략은 한 버전을 다른 버전으로 대체하는 대신 서로 다른 버전의 충돌하는 변경 사항을 하나의 일관된 상태로 결합하는 데이터 병합 전략입니다. Last Write Wins와 달리 병합은 데이터 손실을 최소화하면서 모든 브랜치의 변경 사항을 보존하려고 합니다. Apache CouchDB 문서, 2025에 따르면 삼자 병합(three-way merge)은 문서 지향 데이터베이스에서 충돌 해결의 표준 메커니즘입니다. 삼자 병합은 각 클라이언트가 어떤 필드를 변경했는지 확인하기 위해 공통 기본 버전을 사용합니다.
주요 내용
병합 전략은 충돌하는 데이터 버전 중 하나를 선택하는 대신 결합하는 알고리즘 집합입니다. 모바일 애플리케이션에서 병합은 두 클라이언트가 동일한 객체의 다른 필드나 속성을 독립적으로 편집할 때 사용됩니다. 이전 버전을 완전히 폐기하는 대신(LWW에서처럼) 시스템은 개별 필드 수준에서 차이를 분석하고 두 버전의 변경 사항을 포함하는 결과 객체를 생성합니다.
주요 차이점은 병합과 LWW 사이에서 각 사용자의 변경 사항이 서로 모순되지 않는 한 보존된다는 것입니다. 사용자 A가 작업 이름을 변경하고 사용자 B가 설명을 변경한 경우 병합은 두 변경 사항을 모두 보존합니다. 둘 다 동일한 필드를 변경한 경우 해결이 필요한 충돌이 등록됩니다. 이로 인해 병합은 사용자가 동일한 데이터를 공동으로 작업하는 애플리케이션에 선호됩니다.
Stripe Engineering Blog(2025)의 보고서에 따르면 LWW 대신 병합 전략을 구현하면 모바일 프로젝트 관리 애플리케이션에서 데이터 손실에 대한 사용자 불만이 76% 감소했습니다. 그러나 충돌 처리 시간이 15~30ms 증가했으며 이는 데이터 무결성을 위한 허용 가능한 비용으로 간주됩니다.
삼자 병합(three-way merge)은 병합 전략의 가장 일반적인 구현입니다. 이 메커니즘은 기본(분기 전 상태), 로컬(현재 클라이언트 버전), 원격(서버 버전)의 세 가지 데이터 버전으로 작동합니다. 시스템은 로컬 및 원격 버전의 각 필드를 기본과 비교하여 어느 쪽이 어떤 필드를 변경했는지 확인합니다.
결정 논리는 간단합니다. 하나의 클라이언트만 필드를 변경한 경우(기준과 비교하여) 해당 변경 사항이 자동으로 수락됩니다. 두 클라이언트가 동일한 필드를 변경한 경우 충돌이 등록되며 자동으로(우선순위에 따라) 해결되거나 사용자에게 위임될 수 있습니다. 어떤 클라이언트도 필드를 변경하지 않은 경우 기본값이 유지됩니다. 이 접근 방식은 독립적인 변경 사항이 손실되거나 충돌하지 않음을 보장합니다.
필드 사전 수준의 삼자 병합 알고리즘:
fun threeWayMerge(
base: Map<String, Any?>,
local: Map<String, Any?>,
remote: Map<String, Any?>
): Map<String, Any?> {
val result = base.toMutableMap()
val allKeys = base.keys + local.keys + remote.keys
allKeys.forEach { key ->
val baseVal = base[key]
val localVal = local[key]
val remoteVal = remote[key]
result[key] = when {
localVal == baseVal -> remoteVal
remoteVal == baseVal -> localVal
localVal == remoteVal -> localVal
else -> // real conflict
resolveConflict(key, localVal, remoteVal)
}
}
return result
}
threeWayMerge 함수는 세 버전의 모든 키를 순차적으로 처리합니다. 로컬 값이 기본과 일치하면 원격 변경이 수락됩니다. 원격이 기본과 일치하면 로컬 변경이 수락됩니다. 둘 다 기본과 다르지만 서로 같으면 둘 중 하나가 수락됩니다. 실제 충돌은 양쪽에 다른 변경 사항이 있을 때만 등록됩니다.
자동 해결은 변경 사항이 겹치지 않거나 시스템이 규칙에 따라 올바른 값을 결정할 수 있을 때 적용됩니다. 예를 들어 숫자 필드의 경우 최대값을 선택할 수 있고 텍스트 필드의 경우 연결 또는 최신 버전을 선택할 수 있습니다. CouchDB는 JSON 문서 필드에 자동 병합을 사용하고 배열에는 중복 제거와 함께 연결을 사용합니다.
수동 해결은 두 사용자가 동일한 필드를 다르게 변경한 경우 필요합니다. 이 경우 애플리케이션은 세 가지 옵션이 있는 대화 상자를 표시합니다: "로컬 버전 수락", "원격 버전 수락" 또는 "수동으로 병합". CMU(카네기 멜론 대학교, 2024)의 연구에 따르면 수동 해결은 사용자 만족도를 40% 감소시키므로 자동 병합을 최대화해야 합니다.
다양한 필드 유형에 대한 해결 전략:
| 필드 유형 | 자동 전략 | 수동 대안 |
|---|---|---|
| 숫자(카운터) | 최대값 취하기 | 두 값 모두 표시 |
| 텍스트(문자열) | 시간별 선택 | 하이라이트 편집기 |
| 부울 | 역할별 우선순위 | 세 가지 선택 옵션 |
| 배열(목록) | 중복 제거와 병합 | 요소별 선택 |
| 중첩 객체 | 재귀적 병합 | 차이점 표시 |
구현을 살펴보겠습니다 REST API를 통한 동기화가 있는 모바일 애플리케이션의 사용자 프로필에 대한 병합 전략입니다. 프로필에는 이름, 이메일, 아바타 및 알림 설정이 포함됩니다. 각 필드는 사용자의 다른 장치에서 독립적으로 변경될 수 있습니다.
필드 수준 버전 관리가 있는 프로필 데이터 클래스:
data class UserProfile(
val displayName: String,
val email: String,
val avatarUrl: String,
val notificationsEnabled: Boolean
)
data class ProfileSnapshot(
val profile: UserProfile,
val version: Int
)
fun mergeProfiles(
base: UserProfile,
local: UserProfile,
remote: UserProfile
): UserProfile {
return UserProfile(
displayName = if (local.displayName != base.displayName)
local.displayName else remote.displayName,
email = if (local.email != base.email)
local.email else remote.email,
avatarUrl = if (remote.avatarUrl != base.avatarUrl)
remote.avatarUrl else local.avatarUrl,
notificationsEnabled = if (local.notificationsEnabled != base.notificationsEnabled)
local.notificationsEnabled
else remote.notificationsEnabled
)
}
mergeProfiles 함수는 프로필의 각 필드를 독립적으로 처리하여 기본과 다른 버전을 선택합니다. 충돌 경우(둘 다 기본과 다른 경우) 우선순위는 애플리케이션 규칙에 따라 결정됩니다. 예제에서 avatarUrl의 경우 원격 버전에 우선순위가 주어지고 나머지 필드의 경우 로컬 버전에 우선순위가 주어집니다.
CouchDB 및 PouchDB는 내장된 병합 전략 지원을 갖춘 가장 잘 알려진 데이터베이스입니다. 문서 복제 중에 CouchDB는 문서 수준에서 충돌 감지와 함께 다중 스레드 복제를 사용합니다. 기본 버전은 수정 기록에 저장되며 충돌이 발생하면 시스템은 모든 충돌 브랜치를 보존하고 병합 메커니즘을 통해 이를 해결하기 위한 API를 애플리케이션에 제공합니다.
Firebase Firestore에서 병합은 낙관적 잠금을 사용한 트랜잭션을 통해 구현됩니다. 개발자는 특정 필드가 FieldValue.serverTimestamp() 및 FieldValue.arrayUnion()을 사용하여 원자적으로 업데이트되어야 함을 지정할 수 있습니다. 그러나 Firestore는 완전한 삼자 병합을 지원하지 않습니다. 충돌 시 트랜잭션이 새 데이터로 다시 시도되며 이는 실제 병합이 아니라 재시도에 해당합니다.
Kotlin Multiplatform 및 React Native의 모바일 애플리케이션의 경우 병합 전략이 클라이언트 측에서 구현됩니다. 로컬 데이터베이스(SQLite, Realm)는 각 문서의 버전을 저장하고 동기화 중에 클라이언트는 서버 버전을 로드하고 결과를 보내기 전에 로컬에서 병합을 수행합니다. 이 접근 방식은 더 많은 충돌이 축적되는 장시간 오프라인 작동 중에도 데이터 무결성을 보장합니다.
자주 묻는 질문
병합 전략은 다른 버전의 변경 사항을 단일 상태로 결합하는 충돌 해결 접근 방식입니다. LWW와 달리 병합은 필드 수준에서 서로 모순되지 않는 경우 두 브랜치의 변경 사항을 보존합니다.
삼자 병합은 기본 버전(분기 전 상태)을 사용하여 각 클라이언트가 어떤 필드를 변경했는지 확인합니다. 양자 병합은 원래 상태를 모르고 두 버전만 비교하므로 잘못된 충돌이 더 자주 발생합니다.
CouchDB 및 PouchDB는 내장된 삼자 병합 지원이 있습니다. Firebase Firestore는 트랜잭션 수준에서 구현이 필요합니다. MongoDB 및 Realm은 낙관적 잠금 메커니즘을 제공하지만 완전한 자동 병합은 제공하지 않습니다.
병합은 적합하지 않습니다 처리 속도가 중요한 데이터(초당 1000개 이상의 충돌), 스트리밍 데이터(로그, 이벤트) 및 변경 사항이 근본적으로 호환되지 않는 경우(다른 스키마 버전)에 적합하지 않습니다. 이러한 경우 LWW 또는 CRDT가 더 효율적입니다.
구현에는 세 단계가 포함됩니다: 서버에서 데이터를 로드할 때 기본 버전 저장, 저장 시 필드 수준에서 변경 감지, 동기화 중 병합 알고리즘 호출. 간단히 하려면 JSON Patch 또는 CRDT 라이브러리를 사용하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.