[서버 개발 일지 #4] 작업 분배 및 동기화 엔드포인트 설계

KimGwangmin·2026년 10월 13일

지난 글에서는 서버-클라이언트 구조를 어떻게 가져갈지 고민한 기록을 남겼다. 이번 글에서는 그 설계를 실제 구현으로 옮기기 위한 첫 단계를 다룬다. 팀의 작업을 어떻게 나눴는지, 그리고 그중 내가 맡은 계약 확정, 서버용 콘텐츠 내보내기 도구, 동기화 엔드포인트와 적용 엔진을 정리한다.

1. 작업 분배

서버 관련 작업을 맡게된 인원은 나를 포함한 3명이다. 역할은 다음과 같이 나눴다.

담당역할영역
리드 (나)데이터 구조와 계약의 설계·결정, 판단이 필요한 핵심 로직서버 + 클라이언트
서버명세대로 서버 구현서버
클라이언트명세대로 클라이언트 구현클라이언트

서버와 클라이언트 역할을 분리한 이유는 각 팀원이 비교적 명확히 서버나 클라이언트 중 한 쪽 작업을 더 희망했고, 작업 영역이 충돌하지 않는 편이 병렬 개발에 유리하기 때문이다.

작업을 나눈 방식

클라이언트 개발 때는 업무를 유연하게 정의하고 각자 판단하면서 진행할 수 있었다. 하지만 서버 개발은 모두 경험이 적어서 같은 방식으로 하기가 어렵다. "이 기능을 서버에서 어떻게 만들지 알아서 정하자"고 하면 판단에 시간이 오래 걸리고, 사람마다 다르게 해석한 결과가 나중에 맞지 않을 위험이 크다.

특히 클라이언트는 작업 영역을 기준으로 분배하면 거의 독립적인 기능을 구현하면 되기에 자율성에 맡길 수 있지만, 서버 작업의 경우 명세대로 데이터를 주고받는 것이 핵심이기 때문에 정확하게 업무를 정의하지 않으면 병합했을 때 동작하지 않을 가능성이 매우 높다.

그래서 작업 방식을 다음과 같이 정했다.

  1. 계약 우선: 리드가 먼저 계약(데이터 형식, DB 스키마, 인터페이스)을 확정해 dev에 병합한다. 서버와 클라이언트 담당은 그 계약만 보고 구현하므로 서로를 기다리지 않는다.
  2. 경계는 인터페이스로: 다른 담당의 결과물이 필요한 곳은 인터페이스와 가짜 구현(fake)으로 먼저 개발하고, 통합할 때 실제 구현으로 바꾼다.
  3. 계약 변경은 리드만: 명세가 모호하거나 틀린 곳을 발견하면 리드에게 알리고, 리드가 문서와 코드를 고친다. 각자 임의로 형식을 바꾸지 않는다.
  4. 계약이 확정되기 전에도 일할 수 있도록, 이미 확정된 부분이나 계약이 필요 없는 작업부터 시작한다.

작업 하나하나는 목표 / 참조 / 산출물 / 완료 조건 / 선행 항목으로 적어서, "무엇이 끝나면 완료인지"를 구현자가 해석할 필요가 없게 했다.

내가 맡은 일

계약을 확정하고, 판단이 많이 필요한 핵심 로직을 맡았다. 이번 글에서는 다음 세 가지를 다룬다.

  1. 계약 확정
  2. 서버용 콘텐츠 내보내기 도구
  3. 동기화 엔드포인트와 적용 엔진

2. 계약 확정

작업 목적

  • 서버와 클라이언트가 만나는 모든 경계(로그 형식, 콘텐츠 데이터, DB 스키마, 처리기 인터페이스)가 정해져 있지 않으면, 다른 두 사람의 작업이 시작도 못 하거나 각자 추측으로 구현하게 된다.
  • 서버 경험이 적은 팀이라, 구현자가 "이 경계는 이렇게 하는 게 맞을까"를 스스로 판단하기 어렵다. 판단은 리드가 미리 끝내 두고, 구현자는 구체적으로 확정된 명세 안에서 구현에만 집중하는 환경을 만들고자 했다.
  • 계약이 먼저 나오면 서버와 클라이언트 작업이 병목 없이 독립적으로 진행된다. 그래서 다른 작업보다 앞에 두었다.

계약: 문서 + 뼈대 코드 + 샘플

계약 문서(docs/server-sync-contracts.md)만 쓰고 끝내지 않았다.

  • 인터페이스와 타입의 뼈대 코드(로그 타입, 거부 코드, payload, 처리기 인터페이스 등)를 양쪽 저장소에 병합해, 계약이 컴파일되는 형태로 존재하게 했다.
  • 타입별 JSON 샘플 파일을 양쪽 저장소에 같은 내용으로 두었다. 클라이언트가 만든 JSON과 서버가 읽는 JSON이 같은 형식인지 테스트로 확인하는 기준이 된다.
  • 실제 값으로 검증을 통과하는 샘플 콘텐츠 묶음도 함께 만들었다.

결정한 내용

장정한 것
1. 콘텐츠서버가 가질 콘텐츠의 범위와 파일 구조, 콘텐츠 버전 계산법, 서버의 전투 규칙 인터페이스
2. 클라이언트 인증 경계로그인 흐름(백그라운드, 타임아웃), 서버가 거부한 것과 서버에 닿지 않은 것의 구분
3. 로그·동기화 JSON필드 단위의 로그 형식, 거부 코드 전체 목록, 요청 한도
4. 서버 스키마와 처리기DB 마이그레이션, 엔진과 처리기의 책임 분리와 인터페이스
5. 클라이언트 로그·저장 경계로그 생성과 저장, 전송, 응답 반영의 흐름

세부 구현까지 적지는 않고, 계약을 정하면서 어떤 판단을 했는지를 몇 가지만 남긴다.

  • 콘텐츠 버전은 파일 바이트의 해시로 한다. 파싱한 결과를 정규화해서 해시하면 C#과 Java의 정규화 차이로 버전이 달라질 수 있다. 파일 바이트를 그대로 해시하면 같은 파일은 어느 언어에서도 같은 버전이다.
  • 스탯과 적 구성의 연결을 코드가 아니라 데이터로 내보낸다. "어떤 업그레이드 수치가 어떤 적의 어떤 값에 닿는가"는 클라이언트 코드에 있는 규칙이다. 이를 Java로 다시 옮기는 대신 내보내기 도구가 데이터로 내보내게 했다. 서버가 옮기는 것은 계산식뿐이고, 클라이언트가 연결을 바꾸면 자동으로 따라간다.
  • 서버의 계산은 클라이언트와 같은 타입과 순서로 한다. 결산 검증에는 정확한 등식 검사가 있어서 계산 결과가 1만 달라도 거부된다. 그래서 float와 double, 반올림 방식, 합산 순서까지 계약에 적었다.
  • 잘못된 로그 하나 때문에 요청 전체를 버리지 않는다. 이유는 4절에서 자세히 다룬다.
  • 로그는 만들 때 한 번 JSON 글자로 직렬화해 보관하고, 재전송할 때 그 글자를 그대로 보낸다. 같은 로그가 재전송되어도 같은 내용임을 보장한다.
  • 처리기는 HTTP, JSON, 저장소를 모른다. 엔진이 형식과 순서를 책임지고, 처리기는 규칙 검사와 진행도 변경만 한다. 이 경계는 4절에서 설명한다.

3. 서버용 콘텐츠 내보내기 도구

작업 목적

오프라인 플레이를 허용하면서 서버가 사후에 검증하는 구조에서는, 서버도 노드와 전투의 규칙·수치를 알아야 한다. 그런데 이 콘텐츠의 원본은 클라이언트에 있다.

  • 노드 시트(CSV), 노드 배치, 전투 데이터(Unity 에셋)는 클라이언트에서 편집한다. 밸런스 조정이 Unity 에디터에서 이루어지기 때문이다.
  • 서버가 이 데이터를 따로 손으로 옮겨 적으면 둘이 어긋난다. 실제로 작업 전 서버의 노드 콘텐츠는 클라이언트와 개수부터 달랐다. (노드 247개 대 276개, 수치 60개 대 71개)

그래서 "원본은 클라이언트 하나, 서버는 내보낸 사본" 원칙을 정했다. 서버 저장소에서 콘텐츠 파일을 손으로 고치지 않고, 클라이언트의 에디터 도구가 서버 형식으로 내보낸 파일만 그대로 커밋한다.

서버에 필요한 데이터

서버는 검증에 필요한 값만 가진다. 검증에 쓰지 않는 값은 내보내지 않는다.

서버에 보유해야 함보유하지 않음
노드, 노드 비용, 노드 효과, 배치(시작 노드, 연결)적 HP, 이동 속도, 반지름, 배치·출현 띠
성장 단계별 레벨 경험치 표, 마일스톤(레벨, 목표 골드)블랙홀 공격 수치(피해, 간격, 범위, 치명)
전투 규칙: 기본 제한 시간, 처치 시 시간 보너스황금 외 사망 효과(번개, 폭발, 레이저 등)
적 종류: id, 수거형 여부, 색 등급별 골드·경험치, 변환 대상, 성질 목록주기 출현(혜성)의 확률·간격
황금 성질의 기본 수치
업그레이드 수치가 어떤 적의 어떤 값에 닿는지의 연결
시작 공급 목록

서버 저장소의 파일 구조

내보낸 묶음은 서버 저장소의 src/main/resources/content/ 아래에 둔다.

content/
├─ manifest.json          목차와 콘텐츠 버전
├─ nodes/
│  ├─ UpgradeStats.csv    업그레이드 수치 정의
│  ├─ Nodes.csv           노드와 Rank 수
│  ├─ NodeCost.csv        (노드, Rank)별 비용
│  ├─ NodeEffects.csv     (노드, Rank)별 효과
│  └─ layout.json         노드 배치, 시작 노드, 연결
└─ battle/
   └─ battle.json         서버 결산 검증에 필요한 전투 콘텐츠
서버 파일클라이언트의 원본변환
nodes/*.csv노드 시트 CSV 4개서버가 읽는 열과 Memo만 남기고 열 이름을 영문으로 바꾼다. 열 안의 값은 그대로다
nodes/layout.json노드 카탈로그 에셋의 배치노드 하나를 한 줄로 쓴다
battle/battle.json게임 콘텐츠 세트 에셋이 가리키는 전투 데이터위 표의 "보유한다" 항목만 추려 JSON으로 쓴다

battle.json은 서버의 결산 검증이 쓰는 값들로 구성된다. 몇 가지 대표 필드는 다음과 같다.

  • rules: 기본 제한 시간, 처치 시간 보너스
  • growth: 레벨별 누적 경험치(levelExp), 마일스톤(레벨, 목표 골드)
  • enemies[]: 종류별 id, 수거형 여부, 변환 대상, 색 등급별 골드·경험치, 성질 목록, 그리고 업그레이드 수치와의 연결(stats)
  • startSupply[]: 시작 공급

콘텐츠 버전

  • manifest.json에 목차와 콘텐츠 버전을 적는다. 버전은 목차 순서대로 각 파일의 경로 + 바이트를 이어 붙여 SHA-256을 구하고 앞 16자를 쓴 값이다.
  • 서버는 시작할 때 버전을 다시 계산해서 목차의 값과 비교하고, 다르면 시작하지 않는다. 손으로 파일을 고친 경우를 잡아낸다.
  • 클라이언트도 게임을 불러올 때 같은 계산으로 버전을 구한다. 그래서 내보내기를 잊어도 클라이언트의 버전은 실제 콘텐츠를 따르고, 서버와 달라지는 것으로 드러난다. 모든 로그는 자신이 쓴 콘텐츠 버전을 싣고, 서버와 다른 로그는 거부된다.
  • 같은 콘텐츠는 언제나 같은 바이트로 내보낸다(키 순서, 들여쓰기, 숫자 표기 고정). 그래야 같은 콘텐츠가 같은 버전이 된다.

에디터 도구

Unity 에디터 메뉴 BlackHole > Server Content > Export...로 실행한다.

  • 지금의 콘텐츠를 서버 형식의 묶음으로 변환해, 선택한 폴더(서버 저장소의 content/)에 쓴다. 목차는 마지막에 쓴다.
  • 게임이 불러올 수 없는 콘텐츠는 내보내지 않는다. 게임과 같은 검사를 먼저 통과해야 한다.
  • 지금 콘텐츠의 버전만 콘솔에 출력하는 메뉴(Log Content Version)도 있다.
  • 완료 조건: 내보낸 묶음이 샘플 묶음과 같은 구조이고, 같은 콘텐츠를 두 번 내보내면 같은 버전이 나온다.

작업 흐름은 이렇게 된다.

클라이언트에서 콘텐츠 편집 → 에디터 메뉴로 내보내기 → 서버 저장소에서 바뀐 파일 커밋
  → 서버 시작 시 버전 재계산·검증 → 클라이언트와 서버가 같은 콘텐츠·같은 버전을 쓴다

4. 동기화 엔드포인트와 적용 엔진

이번 글에서 가장 중심이 되는 부분이다. 3편에서 정한 "로그를 순서대로 적용하며 검증하는 서버"를 실제로 구현한 POST /api/v1/me/sync와 그 안의 적용 엔진이다.

API

  • 요청: 클라이언트가 쌓은 로그 배열(logs). 빈 배열도 허용한다. (로그 없이 진행도만 받고 싶을 때)
  • 응답: 항상 200이고 항상 서버의 현재 진행도가 담긴다. 로그가 거부되어도 마찬가지다.
{
  "appliedThroughSeq": 11,
  "rejected": { "seq": 12, "type": "SETTLEMENT", "code": "SETTLEMENT_EXP_MISMATCH", "message": "..." },
  "contentVersion": "a1b2c3d4e5f60718",
  "progress": { "lastAppliedSeq": 11, "gold": 0, "growthStage": 0, "nodes": [ ... ] }
}
  • appliedThroughSeq: 이 번호까지의 로그는 모두 반영되었다.
  • rejected: 거부된 로그와 사유 코드. 없으면 null이다.
  • 클라이언트는 응답을 받고 반영된 로그를 지우고, 거부가 있으면 이후 로그를 폐기하고, 로컬 진행도를 응답의 progress로 덮어쓴다.

"엔진" 표현의 의미

여기서 말하는 엔진은, 로그의 종류와 상관없이 항상 똑같이 하는 일을 맡는 부분을 가리킨다.

  • 로그를 seq 순서대로 꺼내고, 이미 반영된 로그는 건너뛰고, 형식과 콘텐츠 버전을 확인하고, 거부가 나오면 멈추고, 결과를 기록하고, 서버 진행도를 응답한다.
  • 이 절차에는 "결산 값이 맞는가", "노드를 살 수 있는가" 같은 게임 규칙이 없다. 로그가 구매든 결산이든 같은 절차를 거친다.
  • 로그 종류별 게임 규칙은 처리기(LogHandler) 가 맡는다. 엔진은 처리기를 인터페이스로만 알고, 로그 종류에 맞는 처리기를 찾아 일을 넘긴다.
엔진처리기
아는 것순서, 멱등성, 거부 후 멈춤, 트랜잭션, 기록로그 종류별 게임 규칙
모르는 것각 로그의 의미HTTP, 저장소, 트랜잭션
코드sync/controller, sync/service 등sync/handler의 NodePurchaseHandler, SettlementHandler 등

게임 엔진이나 룰 엔진처럼, 정해진 절차를 돌리면서 그 안에 끼워 넣은 구체적인 부품을 실행해 주는 틀을 흔히 엔진이라고 부르는 관례를 따랐다. 실제 구현은 @Transactional이 붙은 서비스 메서드(SyncService.sync)와 보조 클래스 몇 개이다. 이 구분 덕분에 새로운 로그 종류가 생겨도 엔진은 고치지 않고 처리기만 추가하면 된다.

엔진이 하는 일

요청 하나를 하나의 트랜잭션으로 처리한다.

1. 요청 본문을 읽는다 (요청 전체의 오류는 여기서 가린다)
2. 계정의 진행도 행을 잠근다
3. 로그를 seq 순서대로 하나씩 처리한다
     - 이미 반영된 seq    → 건너뛴다 (SKIPPED)
     - 앞 로그가 비어 있음 → 거부 (SEQ_GAP)
     - 형식 위반          → 거부 (MALFORMED_LOG)
     - 콘텐츠 버전 불일치  → 거부 (CONTENT_VERSION_MISMATCH)
     - 타입별 처리기가 규칙을 검사 → 통과하면 진행도를 바꾼다 / 아니면 거부
     - 거부가 나오면 그 로그에서 멈춘다. 남은 로그는 처리하지 않는다 (NOT_PROCESSED)
4. 받은 로그를 결과와 함께 기록한다 (sync_log)
5. 서버 진행도를 응답한다

한 로그의 검사 순서는 seq → 형식 → 콘텐츠 버전 → 타입별 규칙이다. 이미 반영된 로그는 형식이나 버전이 어긋나도 건너뛴다.

1. 요청 전체의 오류와 로그 하나의 오류를 구분

요청이 잘못되었을 때 어디까지를 "요청 전체의 오류"로 볼지가 중요한 설계 지점이었다.

상황결과
본문이 JSON이 아님, logs가 없음, seq를 읽을 수 없음, 요청 안에서 seq가 1씩 증가하지 않음400 (아무것도 적용하지 않음)
로그가 500개 초과, 본문이 1MB 초과413
로그의 seq는 읽히지만 나머지가 형식에 어긋남그 로그만 MALFORMED_LOG로 거부하고 멈춘다. 앞의 로그는 반영된다

보통은 Spring 컨트롤러에서 요청 본문을 DTO로 받아 @Valid로 검증한다. 하지만 이 API에서는 그렇게 하지 않았다.

  • 컨트롤러에서 @Valid로 검증하면 로그 하나만 잘못되어도 요청 전체가 400이 된다.
  • 클라이언트는 400을 자신의 버그로 보고 대기 중인 로그를 지우지 않는다. 그러면 다음 실행에서 같은 요청이 같은 400을 받고, 영원히 동기화가 막힌다.
  • 그래서 본문을 글자로 받아 직접 읽고, 로그 하나의 형식 오류는 그 로그의 거부로 처리한다. 앞의 정상 로그는 반영되고, 클라이언트는 응답을 보고 문제의 로그부터 폐기할 수 있다.

형식 검사 자체는 Bean Validation(필수, 범위, 길이, UUID 형식)을 쓰되, 컨트롤러 대신 서비스 안에서 로그 하나씩 실행한다. 여기에 중복 항목과 실수의 유한성(NaN, 무한대 방지)을 따로 본다.

2. seq: 순서와 멱등성 보장

  • seq는 계정 단위로 1부터 1씩 증가한다. 서버는 마지막으로 반영한 seq(lastAppliedSeq)를 진행도에 가지고 있다.
  • seq ≤ lastAppliedSeq: 이미 반영된 로그다. 응답이 유실되어 클라이언트가 같은 로그를 다시 보낸 경우이므로 건너뛴다. 두 번 적용하지 않는다.
  • seq > lastAppliedSeq + 1: 앞 로그가 빠졌다. SEQ_GAP으로 거부한다.
  • 앞쪽 일부만 겹쳐서 재전송한 경우에도(예: 1~5를 이미 반영했는데 3~8을 보낸 경우) 3~5는 건너뛰고 6~8만 적용한다.

이 규칙 덕분에 네트워크가 끊기거나 응답을 못 받아도 같은 요청을 안전하게 다시 보낼 수 있다.

3. 첫 거부 직전까지만 반영

  • 로그가 거부되면 그 로그에서 멈춘다. 이후 로그는 앞 로그에 의존하므로(예: 구매로 쓴 골드를 전제로 한 결산) 일부만 골라 적용할 수 없기 때문이다.
  • 거부 직전까지 반영한 결과는 커밋한다. 정상 로그까지 되돌리지 않는다.
  • 반대로 예상하지 못한 예외(코드 버그 등)는 요청 전체를 롤백하고 500을 낸다. 클라이언트는 서버에 닿지 않은 것으로 보고 로그를 지우지 않는다. 버그 때문에 진행도가 어중간한 상태로 저장되는 일이 없게 하려는 것이다.
  • 거부 이후에 남은 로그는 NOT_PROCESSED로 기록만 한다. 서버 진행도가 항상 이기므로, 클라이언트는 응답의 진행도를 채택한다.

4. 같은 계정의 동시 요청은 잠금으로 직렬화

  • 계정의 진행도 행을 비관적 락(SELECT ... FOR UPDATE)으로 조회한다.
  • 같은 계정의 동기화 요청이 동시에 들어와도 한 번에 하나씩 처리된다. 같은 로그를 두 번 적용하거나, 골드를 이중으로 쓰는 일이 없다.
  • 리프레시 토큰 갱신에서 쓴 방식과 같은 이유이다.

5. 엔진과 처리기의 경계

엔진은 순서, 형식, 버전, 기록, 응답을 맡고, 타입별 규칙 검사와 진행도 변경은 처리기(LogHandler)가 맡는다. 둘의 경계가 인터페이스이다.

public interface LogHandler<P> {
    LogType type();
    Class<P> payloadType();
    /** 규칙을 검사하고, 통과하면 context로 진행도를 바꾼다. 거부하면 아무것도 바꾸지 않는다. */
    HandleResult handle(P payload, SyncContext context);
}
  • 처리기는 형식 검사를 통과한 payload만 받는다. null인 필드가 없다고 믿고 쓸 수 있다.
  • 처리기는 HTTP, JSON, 저장소(Repository), 트랜잭션을 모른다. SyncContext(진행도를 읽고 바꾸는 창구)와 콘텐츠 데이터만 쓴다. 그래서 처리기는 DB 없이도 단위 테스트할 수 있다.
  • SyncContext의 변경 메서드(spendGold, earnGold, setNodeRank, resetProgress, openRun 등)는 검사를 모두 마친 뒤에만 부른다. 거부할 때는 아무것도 바꾸지 않는다. 한 로그를 처리하다 중간에서 일부만 반영되는 일이 없게 하기 위해서다.
  • 이 규칙은 엔진이 강제한다. SyncContext의 구현이 변경 횟수를 세고, 처리기가 거부하면서 진행도를 바꾼 경우를 엔진이 발견하면 버그로 보고 예외를 던져 요청 전체를 롤백한다. 처리기를 구현하는 사람이 실수해도 진행도가 어긋난 채로 저장되지 않게 하는 안전장치이다.
  • 처리기는 Spring 빈으로 등록하고, 엔진은 타입으로 처리기를 찾는다. 같은 타입의 처리기가 둘 이상이거나 payload 타입이 계약과 다르면 서버가 시작하지 않는다.

받은 로그는 모두 기록 (sync_log)

받은 로그는 처리 결과와 함께 sync_log 테이블에 남긴다.

결과의미
APPLIED반영됨
REJECTED거부됨 (거부 코드 포함)
SKIPPED이미 반영된 로그라 건너뜀
NOT_PROCESSED앞 로그가 거부되어 처리하지 않음
  • 받은 로그 원문(JSON)도 그대로 저장한다. 오탐(정상 사용자가 거부당한 경우)이 의심될 때 무엇이 왔는지 확인하고 복구하는 근거가 된다.
  • 같은 seq가 여러 번 들어올 수 있으므로(재전송, 거부 뒤 재사용) seq에는 유니크 제약을 걸지 않았다. 적용 여부의 기준은 잠금 아래의 lastAppliedSeq이다.

현재 구현 상태

이 글을 쓰는 시점의 dev 기준이다.

구현됨

  • V3 마이그레이션: 진행도의 last_applied_seq, 판(battle_run), 받은 로그(sync_log)
  • 로그 타입, 거부 코드 16개, payload 형식
  • 요청 읽기와 형식 검사(400, 413, MALFORMED_LOG)
  • 동기화 엔진과 POST /api/v1/me/sync
  • 테스트 19개: 샘플 요청이 순서대로 반영됨, 재전송과 겹치는 재전송, 빈 요청, 거부되면 멈추고 앞 로그는 유지됨, SEQ_GAP, 형식 오류(필드 타입, 알 수 없는 타입, 잘못된 runId, 중복 처치 칸), 콘텐츠 버전 불일치, 처리기가 거부하면서 진행도를 바꾸면 전체 롤백, 판 시작 값의 불일치는 의심 표시만 남기고 거부하지 않음, 다음 판 시작이 열린 판을 포기로 닫음, 400과 413, 인증 필요

아직 구현되지 않음

  • 로그 타입별 실제 처리기: 노드 구매, 판 시작, 전투 결산, 새 게임. 노드 구매·새 게임·판 시작은 서버 담당이, 전투 결산은 내가 이어서 구현한다.
    • 처리기가 없는 타입은 서버 시작 시 경고만 남기고, 그 타입의 로그가 오면 500이 된다.
    • 현재 테스트는 빈으로 등록하지 않은 테스트용 처리기로 엔진의 책임만 검증한다. 실제 처리기가 생겨도 겹치지 않는다.

5. 정리

작업핵심
작업 분배서버 경험이 적은 팀이라 계약을 먼저 확정하고, 각자 확정된 명세 안에서 병목 없이 구현하게 했다
계약 확정문서뿐 아니라 컴파일되는 뼈대 코드와 양쪽 저장소의 같은 JSON 샘플로 경계를 고정했다. 변경은 리드만 한다
콘텐츠 내보내기원본은 클라이언트 하나. 서버는 에디터 도구가 내보낸 사본만 쓰고, 파일 바이트의 해시를 버전으로 양쪽이 맞춘다
동기화 엔진seq 순차 적용, 멱등 재전송, 거부 직전까지 커밋, 계정 단위 잠금, 엔진과 처리기의 분리
  • 로그 한 건의 문제가 전체 동기화를 막지 않게 하고(로그 단위 거부), 버그가 진행도를 어긋나게 하지 않게 하는(롤백, 처리기 변경 감시) 두 가지가 엔진 설계에서 가장 신경 쓴 부분이다.
  • 다음 글에서는 이 엔진 위에서 로그 타입별 처리기가 어떻게 규칙을 검사하는지를 다룬다.

0개의 댓글