LabHub
学习 学习路径 课程

멱등성 — 두 번 눌러도 한 번만 결제되게 · 가져오기 작업을 실패한 다음 행부터 이어간다 · 讲解

가져오기 작업을 실패한 다음 행부터 이어간다의 설계 원리

在 LabHub 中继续学习

한 줄 요약

원본 지문과 체크포인트를 결합해 다른 파일로 잘못 이어받는 일을 막습니다.

概念图: 한 줄 요약 · 왜 이게 필요했나 · 어떻게 동작하나 · 계약을 읽고 실패를 예측하는 워크시트

왜 이게 필요했나

수천 행을 가져오던 작업이 중간에 죽었다. 운영자가 파일을 바꿔 같은 작업 id로 다시 실행하자 앞부분은 옛 파일, 뒷부분은 새 파일인 결과가 생겼다. 처리한 행 번호만 저장하면 입력의 정체성을 확인할 수 없다. 원본 바이트의 지문과 마지막으로 커밋한 위치를 함께 보존해야 한다.

어떻게 동작하나

입력은 중복 id가 없는 JSON 배열이며 각 행은 id와 value를 갖는다. 원본 bytes의 SHA-256과 체크포인트를 imports에 저장한다. 배치의 각 item 삽입과 next_index 증가는 같은 트랜잭션이다. 중간 행에서 예외가 나면 그 배치 전체가 롤백되고 이전에 끝난 배치는 남는다. 재실행은 저장된 인덱스에서 시작하되 원본 지문이 다르면 거절한다.

원본 bytes → 지문 확인 → next_index → 배치 INSERT + 체크포인트 COMMIT                                      └ 중간 실패 → 이번 배치만 ROLLBACK

계약을 읽고 실패를 예측하는 워크시트

다음은 구현을 통째로 외우는 답안이 아니라 단계별 코드 리뷰입니다. 각 변경 조각은 의도적으로 계약을 깨뜨립니다. 변경 후에도 정상 사례가 통과할 수 있다는 점에 주의하세요. 실행 전에 어느 입력·예외·상태를 관측하면 차이가 드러날지 예상하고, 구현 후에는 그 예상과 결과를 비교합니다.

1. 입력 행의 계약을 확인한다

parse_rows(raw)는 JSON 배열 bytes를 읽습니다. 각 항목은 id(비어 있지 않은 str)와 value(bool 제외 int)를 가지며 id 중복은 금지합니다. 위반은 ValueError입니다. {id,value}만 가진 행 리스트를 반환합니다.

판단의 근거: 중복 id를 마지막 행으로 조용히 덮으면 가져오기 결과를 예측할 수 없습니다.

리뷰할 잘못된 변경 조각:

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

2. 원본 바이트의 지문을 고정한다

source_digest(raw)는 bytes에 SHA-256을 적용한 hex 문자열입니다. JSON을 정규화하지 않습니다.

판단의 근거: 재개는 같은 원본에 대해서만 허용하는 계약입니다.

리뷰할 잘못된 변경 조각:

hashlib.sha256(raw.replace(b" ",b""))

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

3. 입력과 체크포인트를 저장한다

init_db(path)는 imports(id TEXT PRIMARY KEY,digest TEXT NOT NULL,next_index INTEGER NOT NULL)와 items(batch TEXT NOT NULL,id TEXT NOT NULL,value INTEGER NOT NULL,PRIMARY KEY(batch,id))를 멱등 생성합니다.

판단의 근거: 서로 다른 가져오기 작업의 같은 행 id는 분리해서 저장합니다.

리뷰할 잘못된 변경 조각:

CREATE TABLE imports

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

4. 다른 원본으로 재개하지 못하게 한다

begin(path,batch,digest)는 새 작업이면 next_index=0을 저장해 0, 기존 같은 지문이면 next_index, 다른 지문이면 ValueError입니다.

판단의 근거: 작업 id와 행 번호가 같아도 입력 파일이 다를 수 있습니다.

리뷰할 잘못된 변경 조각:

if False:

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

5. 배치와 위치를 원자적으로 커밋한다

apply_chunk(path,batch,start,rows,fault=lambda index:None)는 현재 next_index==start일 때만 실행하며 아니면 ValueError입니다. rows를 순서대로 items에 넣고 각 삽입 후 fault(전체인덱스)를 호출합니다. 전부 성공하면 next_index=start+len(rows)를 저장해 반환합니다.

판단의 근거: 행 하나마다 별도 커밋하면 체크포인트와 행 상태가 어긋납니다.

리뷰할 잘못된 변경 조각:

db.commit()            fault(start+offset)

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

6. 현재 위치를 조회한다

checkpoint(path,batch)는 next_index, 없는 작업은 None입니다.

판단의 근거: 마지막 처리 시도 위치가 아니라 마지막으로 커밋된 위치를 읽습니다.

리뷰할 잘못된 변경 조각:

return 0 if row else None

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

7. 작업별 결과를 분리한다

values(path,batch)는 해당 batch의 (id,value) 튜플을 id 오름차순으로 반환합니다.

판단의 근거: 다른 작업의 동일 id 행이 결과에 섞이지 않게 batch를 조건으로 둡니다.

리뷰할 잘못된 변경 조각:

WHERE batch!=? ORDER BY id

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

8. 중간 실패 뒤 안전하게 이어간다

import_all(path,batch,raw,size=2,fault=lambda index:None)는 bool 제외 양의 int size를 검증하고 parse_rows·source_digest·begin을 사용합니다. 남은 행을 size씩 apply_chunk로 처리하고 최종 checkpoint를 반환합니다.

판단의 근거: 첫 배치의 성공을 보존한 채 두 번째 배치 실패 후 재개하는 시나리오를 확인합니다.

리뷰할 잘못된 변경 조각:

begin(path,batch,source_digest(raw))    index=0

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

현장에서 만나는 모습

입력 전체를 메모리에 읽는 작은 데이터용 실습이다. 대용량 파일을 스트리밍 파싱하는 엔진으로 과장하지 않는다. 원본 공백만 달라도 바이트 지문이 달라 재개를 거절한다는 보수적 계약이다. 외부 API 부작용은 이 DB 트랜잭션에 들어가지 않는다.

다음 실습에서 할 것

여덟 단계가 하나의 실행 가능한 결과물로 이어집니다. 입력 행의 계약을 확인한다 → 원본 바이트의 지문을 고정한다 → 입력과 체크포인트를 저장한다 → 다른 원본으로 재개하지 못하게 한다 → 배치와 위치를 원자적으로 커밋한다 → 현재 위치를 조회한다 → 작업별 결과를 분리한다 → 중간 실패 뒤 안전하게 이어간다.

각 단계는 함수나 파일이 존재한다는 사실이 아니라 실제 반환값·예외·상태 변화를 검사합니다. 정답을 본 뒤에는 일부러 경계 비교나 정리 코드를 바꾸어 어떤 시험이 실패하는지 확인하세요. 앞선 시험이 다음 단계에서도 유지되는 이유를 설명하고, 이 실습이 보장하지 않는 운영 조건을 한 가지 적어 보세요.