Cheondi
개발 · 알고리즘·CS

로컬과 서버 중 누가 이겨야 할까

설정 동기화 충돌에서 무조건 최신 시각 대신 값의 성격에 맞는 병합 규칙을 고민한 기록입니다.

  • #sync
  • #conflict
  • #state

설정 동기화를 붙이고 나니 “로컬 값과 서버 값이 다르면 무엇을 쓸까?”라는 질문이 남았다. 처음에는 수정 시간이 늦은 쪽을 고르면 된다고 생각했다. 기기 시각이 틀리거나 오프라인에서 오래 사용한 뒤 연결되면 단순 비교가 위험했다. 최신이라는 숫자가 사용자의 의도를 항상 설명하진 못했다.

모든 설정에 하나의 승자 규칙을 적용하지 않고 데이터의 성격을 나눠 보기로 했다.

값마다 합칠 수 있는 정도가 달랐다

테마처럼 하나만 선택해야 하는 값은 충돌 시 한쪽을 골라야 했다. 즐겨찾기 목록은 서로 다른 항목을 추가했다면 합칠 수 있었고, 사용자가 삭제한 항목은 다시 살아나지 않게 삭제 표식이 필요했다.

단일 선택 값 : 서버 revision 기준으로 충돌 알림
독립 옵션    : 변경된 필드 단위 병합
목록 추가    : 항목 ID 기준 합집합
목록 삭제    : 삭제 revision을 함께 보존

병합 가능성은 JSON 타입보다 업무 의미에 달려 있었다.

서버를 기준으로 하되 로컬 변경을 잃지 않았다

서버가 최종 revision을 발급하고 충돌을 감지했다. 클라이언트는 충돌 응답을 받으면 최신 서버 값을 가져와 아직 반영되지 않은 로컬 변경을 다시 적용할 수 있는지 판단했다.

var latest = await api.GetSettings();
var merge = merger.TryMerge(latest, pendingLocalChanges);

if (merge.HasConflict)
    ShowConflict(merge);
else
    await api.Update(merge.Value, latest.Revision);

재시도 횟수에는 제한을 두고 같은 충돌을 무한 반복하지 않았다.

조용한 덮어쓰기가 가장 위험했다

사용자가 한 변경이 사라졌는데 아무 안내도 없으면 기능이 고장 난 것처럼 느껴졌다. 자동 병합이 확실한 경우에만 조용히 처리하고 의미를 결정하기 어려운 충돌은 어느 기기의 값을 유지할지 선택하게 했다.

충돌 화면에는 내부 revision보다 변경된 설정과 예상 결과를 보여줬다. 데이터 전체나 계정 식별값은 표시하지 않았다.

동기화에는 정답보다 정책이 필요했다

처음에는 로컬 우선이나 서버 우선 중 하나가 기술적인 정답일 거라 생각했다. 실제로는 데이터 손실, 여러 기기 경험, 오프라인 지원 중 무엇을 중요하게 보는지에 따른 제품 정책이었다.

이 작업 뒤 충돌 해결을 마지막 예외 처리로 미루지 않는다. 어떤 변경이 동시에 일어날 수 있고 합칠 수 없는 경우 사용자에게 어떻게 설명할지를 API와 화면을 만들 때부터 생각하게 됐다.