- 분야: DB
- 기술 스택: TypeScript · Cloudflare Workers · Cloudflare D1(SQLite)
문제와 원인
로그인 뒤 POST /api/sw-study의 sw-import는 게스트의 이론 진도를 먼저 저장하고 활성 세션을 저장했다. 세션 저장 함수가 HTTP 409를 반환해도 상위 분기의 status !== 409 조건이 실패 반환에서 제외해 마지막 HTTP 200 성공 응답으로 넘어갔다.
누락·변경 문항, 오래된 revision, 다른 활성 세션, 제출된 세션의 재활성화가 충돌 조건이었다. 클라이언트도 .catch(() => undefined)로 가져오기 오류를 소거하고 계정 조회로 넘어갔다.
수정 전 코드
sw-import
세션 저장의 HTTP 409를 실패 반환에서 제외했다.
const importedTheoryProgress = await repository.importTheoryProgress(
userKey,
completedTheoryIds,
new Date().toISOString(),
);
if (payload.activeSession && typeof payload.activeSession === "object") {
const session = payload.activeSession as Record<string, unknown>;
const imported = await saveLearningSession(request, repository, userKey, {
...session,
sessionId: session.id,
action: "sw-session-save",
}, "active");
if (!imported.ok && imported.status !== 409) return imported;
}
return response(request, { ok: true, importedTheoryProgress }, "private-session");
계정 동기화
가져오기 오류를 소거한 뒤 계정 조회를 이어갔다.
void requestSwStudyMutation("sw-import", {
completedTheoryIds: local.completedTheoryIds,
activeSession: local.activeSession,
}, controller.signal).catch(() => undefined).then(async () => {
if (controller.signal.aborted) return;
const account = await requestSwStudyData<SwAccountState>({
view: "state",
cacheMode: "none",
signal: controller.signal,
});
// 계정 상태 반영 부분 생략
}).catch(() => {
if (!cancelled && !controller.signal.aborted) {
setContentError("계정의 SW 학습 기록을 동기화하지 못했습니다. 이 브라우저의 기록은 유지됩니다.");
}
});
수정 후 코드
sw-import
모든 세션 저장 실패를 즉시 반환하고 원래 상태·헤더·오류 코드와 이론 진도 반영 수를 함께 보낸다. sessionImport.status는 saved, failed, not-requested로 세션 처리 결과를 구분한다.
if (payload.activeSession && typeof payload.activeSession === "object") {
const session = payload.activeSession as Record<string, unknown>;
const imported = await saveLearningSession(request, repository, userKey, {
...session,
sessionId: session.id,
action: "sw-session-save",
}, "active");
if (!imported.ok) {
const failure = await imported.json() as Record<string, unknown>;
return Response.json({
...failure,
ok: false,
importedTheoryProgress,
sessionImport: { status: "failed", code: failure.code },
}, { status: imported.status, headers: imported.headers });
}
}
return response(request, {
ok: true, importedTheoryProgress,
sessionImport: { status: payload.activeSession ? "saved" : "not-requested" },
}, "private-session");
이론 1개가 먼저 저장된 뒤 누락 문항으로 세션 저장이 거절되면 HTTP 409와 다음 본문을 반환한다.
{
"ok": false,
"code": "SW_SESSION_CONTENT_CHANGED",
"importedTheoryProgress": 1,
"sessionImport": {
"status": "failed",
"code": "SW_SESSION_CONTENT_CHANGED"
}
}
createSwAccountSync.run
React 계정 동기화는 가져오기 성공 후 계정 조회와 로컬 적용을 진행하도록 연결했다. 가져오기 실패에는 오류를 표시하고 브라우저 기록을 보존한다. 가져오기 성공 뒤 계정 조회만 실패한 경우에는 조회부터 재시도한다.
pending = Promise.resolve().then(async () => {
if (!isCurrent()) return false;
const snapshot = options.snapshot();
if (snapshot !== importedSnapshot) {
await options.importSnapshot(snapshot, current.signal);
if (!isCurrent()) return false;
importedSnapshot = snapshot;
}
const account = await options.readAccount(current.signal);
if (!isCurrent()) return false;
options.applyAccount(account);
complete = true;
options.onError(null);
return true;
}).catch((error: unknown) => {
if (isCurrent()) options.onError(error);
return false;
}).finally(() => {
if (controller !== current) return;
pending = undefined;
controller = undefined;
options.onBusy(false);
});
검증 결과
같은 학습 데이터의 API 비교
Swagger UI에서 수정 전후 원본 서비스를 다시 실행했다. 같은 초기 학습 데이터와 로컬 SQLite·D1 호환 어댑터를 사용한 합성 계정으로, 네 충돌 조건은 각 1회, 중복 요청 조건은 2회 실행했다.
| 입력 | 수정 전 | 수정 후 |
|---|---|---|
| 누락 문항을 포함한 활성 세션 | HTTP 200, ok: true | HTTP 409, ok: false, SW_SESSION_CONTENT_CHANGED |
| 오래된 revision | HTTP 200, ok: true | HTTP 409, ok: false, SW_SESSION_CONFLICT |
| 다른 활성 세션 | HTTP 200, ok: true | HTTP 409, ok: false, SW_SESSION_CONFLICT |
| 제출된 세션을 활성 세션으로 가져오기 | HTTP 200, ok: true | HTTP 409, ok: false, SW_SESSION_CONFLICT |
| 네 충돌을 전체 성공으로 응답한 건수 | 4/4건 | 0/4건 |
| 이론 1개 + 누락 문항 세션을 동일하게 2회 요청 | 이론 1행, 세션 0행, 성공 응답 | 이론 1행, 세션 0행, 단계별 실패 응답 |
| 문항을 유효한 1개로 교체 후 재요청 | HTTP 200, 세션 1행 | HTTP 200, saved, 세션 1행 |
revision·활성 세션·제출 세션 충돌에서는 기존 서버 행을 보존했다.