Devin.KR

저장 키와 충돌 정책

75분 안팎

학습 목표

지역·날짜 키가 충돌하는 입력을 찾아 같은 값 재입력과 수정값 입력을 구분해 출력합니다.

개념

왜 파일 이름으로는 중복을 막지 못할까요?

교통량 파일을 오전에 내려받은 뒤 같은 파일을 오후에 다시 처리하는 상황을 생각합니다. 행을 매번 추가하면 하루 통행량이 두 배가 됩니다. 반대로 파일 이름이 같다는 이유로 두 번째 실행을 건너뛰면 공급자가 수정한 값을 놓칩니다. 재실행을 안전하게 만들려면 무엇이 같은 관측인지 먼저 정해야 합니다. 실행한 횟수와 실제 관측의 개수는 다른 숫자입니다. 이번 레슨은 저장 코드를 작성하기 전에 관측의 정체성과 충돌의 의미를 결정하는 단계입니다.

앞 모듈의 결합 표는 지역별 하루 통행량과 그 지역에 대응한 날씨 요약을 한 행에 담았습니다. 이 표의 관측 키는 region과 date의 쌍입니다. 같은 날짜라도 A와 B 지역은 서로 다른 행이며, 같은 지역이라도 날짜가 다르면 새로운 관측입니다. total_vehicles와 rain_mm은 키로 찾은 관측의 값입니다. 값이 바뀌어도 관측의 정체성은 유지됩니다. 통행량을 키에 포함하면 100에서 105로 수정될 때 별도 행으로 남아 정정이 중복으로 바뀝니다.

실습 환경과 이어지는 프로젝트

이 모듈은 Python 3.9 이상과 표준 라이브러리 sqlite3, unittest를 사용합니다. 로컬 ZIP은 루트에서 안내한 검사 명령을 실행하며 외부 계정과 네트워크가 필요하지 않습니다. 브라우저 실습은 표준 입력 JSON 배열을 받고 결과를 줄 단위로 출력합니다. 데이터는 학습용 교통·날씨 표본입니다. 실제 공공기관 관측 결과로 해석하지 않습니다. 각 레슨의 작은 실습을 마친 뒤 미션 ZIP에서 앞 모듈의 전체 산출물과 이번 저장 경로를 연결합니다.

미션은 m07 solution을 그대로 포함합니다. raw 원본, 정제 코드, 지표와 그림, 기존 테스트를 삭제하지 않습니다. 기존 etl.run의 전체 교체와 이번 incremental.run의 증분 병합은 다른 명령입니다. 앞 단계에서 검증된 표를 다시 만드는 부분은 재사용하고 저장 방식만 추가합니다. 모든 따라하기 Python 코드는 독립 실행 예제입니다. Python 파일로 저장하거나 터미널의 python3 대화형 환경에서 실행할 수 있으며 임시 DB는 예제 종료 때 정리됩니다.

키와 값의 비교를 분리합니다

파이썬에서는 두 문자열을 튜플로 묶어 딕셔너리 키로 사용합니다. ('A', '2026-09-01')처럼 구성하면 각 요소의 경계가 분명합니다. 문자열을 구분자 없이 붙여 키로 만들면 다른 조합이 같은 문자열이 될 수 있습니다. 리스트는 가변이므로 딕셔너리 키로 사용하면 unhashable type: 'list' 오류가 납니다. 키를 튜플로 바꾸는 것은 오류를 숨기기 위한 문법 변경이 아니라 관측의 두 요소를 한 쌍으로 표현하는 결정입니다.

seen 딕셔너리는 키마다 직전에 읽은 값 쌍을 보관합니다. 키가 처음 나타나면 NEW, 이미 존재하고 통행량과 강수량이 같으면 SAME, 키가 같지만 값 쌍이 다르면 CHANGED로 분류합니다. 분류한 뒤 seen의 값을 현재 값으로 갱신합니다. 따라서 100, 105, 105가 같은 키로 연속 들어오면 NEW, CHANGED, SAME입니다. 첫 행과만 비교하면 마지막 행을 다시 CHANGED라고 잘못 판단합니다. 어떤 기준과 비교하는지 계약에 적어야 출력이 재현됩니다.

null은 0과 다릅니다. null에서 0으로 바뀌면 값 수정으로 분류합니다. 결측이었던 관측에 실제 영점이 들어왔기 때문입니다. 파이썬에서 JSON null은 None이며 JSON의 0과 0.0은 수치상 같습니다. 이 실습은 숫자의 표현 방식이 아니라 수치의 의미를 비교합니다. 파일의 바이트가 달라도 의미상 같은 관측일 수 있습니다. 입력 해시는 바이트 동일성을 보여 주고 키·값 비교는 관측 동일성을 보여 줍니다. 두 증거를 서로 대체하지 않습니다.

충돌 정책을 업무 의미로 정합니다

같은 키의 수정값을 발견했다고 해서 바로 합산하지 않습니다. 이번 total_vehicles는 그날의 절대 합계입니다. 100을 처리한 뒤 정정된 105가 오면 최종 값은 105여야 합니다. 205로 더하면 서로 다른 관측 두 건을 합친 것처럼 취급합니다. 이벤트마다 증가량이 들어오는 모델이라면 별도의 이벤트 ID와 합산 계약이 필요합니다. 컬럼 이름만 보고 덧셈과 대입을 고르지 않고 공급자가 값의 의미를 어떻게 정의했는지 확인합니다.

브라우저 분류기는 충돌을 드러내는 진단 도구입니다. 배치 안에 서로 다른 수정값이 섞였을 때 마지막 값을 자동 승인하는 운영 적재기는 아닙니다. 미션에서는 한 배치 내 키 중복이나 충돌을 저장 전에 거절하고, 서로 다른 실행 사이에 들어온 정정본은 기존 행을 갱신합니다. 입력 파일 내부 순서가 공급자의 수정 시각을 뜻하지 않기 때문입니다. 분류와 승인이라는 두 역할을 구분하면 왜 실습은 CHANGED를 출력하고 미션은 충돌 배치를 거절하는지 설명할 수 있습니다.

이번 계약은 운영자가 최신 정정본을 올바른 순서로 제공한다는 전제를 둡니다. 예전 파일을 나중에 재생하면 값이 예전 수치로 돌아갈 수 있습니다. 키만으로 최신성을 보장할 수는 없습니다. 실제 운영에서는 공급자의 개정 번호나 수정 시각을 함께 저장하고 오래된 갱신을 거절하는 정책이 필요할 수 있습니다. 현재 표본에는 그 열이 없으므로 가짜 시각을 만들어 최신 데이터라고 주장하지 않습니다. 재실행 안전성과 정정본 순서 관리가 서로 다른 요구임을 기록합니다.

오류를 비교 단계에서 읽습니다

KeyError: 'region'은 해당 행에 필수 키가 없다는 뜻입니다. 문자열을 아무 값으로 채워 계속 처리하면 다른 관측을 같은 키로 만들 수 있습니다. 앞 모듈의 스키마 검사를 먼저 통과한 행만 이 분류기로 보냅니다. 이번 브라우저 문제도 네 필드가 존재하고 날짜와 숫자 형식이 이미 정제된 입력만 제공합니다. 형식 검사까지 같은 문제에 섞지 않고 키 충돌의 판단에 집중합니다. 공백이 있는 지역명을 임의로 지우거나 날짜 표현을 다시 바꾸지도 않습니다.

행 수만 같다고 재실행이 안전하다고 판단하지 않습니다. A의 행이 사라지고 C의 행이 생겨도 전체 개수는 같습니다. 통행량 합계도 한 행이 늘고 다른 행이 줄면 그대로일 수 있습니다. 키 목록과 행별 값, 결측 개수를 함께 대조해야 결과의 의미를 확인할 수 있습니다. 이번 실습에서는 입력 순서의 각 행을 분류하되 감사용 키 목록은 정렬합니다. 처리 순서와 보고 순서를 분리하면 프로그램의 실제 판단 과정을 숨기지 않으면서 비교 결과를 안정적으로 만들 수 있습니다.

완료 기준을 말로 설명합니다

NEW는 새 키, SAME은 직전 값과 같은 키·값, CHANGED는 같은 키의 다른 값이라는 규칙으로 테스트를 작성합니다. 빈 배열은 출력할 행이 없으므로 아무것도 출력하지 않습니다. 지역만 다른 사례, 날짜만 다른 사례, null과 영점이 바뀌는 사례를 포함해야 정상 두 행만 비교한 테스트가 놓치는 경계를 확인합니다. 더 읽기의 컬렉션 장에서 자료 구조 선택을 확장하고, 여기서는 저장 정책을 설명할 수 있는 키와 비교 규칙을 자신의 말로 정리합니다.

따라하기

지역·날짜를 함께 키로 만들기

같은 날짜의 다른 지역과 같은 지역의 다른 날짜를 별도 관측으로 셉니다.

rows=[('A','2026-09-01'),('B','2026-09-01'),('A','2026-09-02'),('A','2026-09-01')]
print('input_rows',len(rows))
print('unique_keys',len(set(rows)))
print(sorted(set(rows)))

실행 결과

input_rows 4
unique_keys 3
[('A', '2026-09-01'), ('A', '2026-09-02'), ('B', '2026-09-01')]

직전 값과 비교하기

키를 찾은 뒤 값을 비교하고 비교 기준을 현재 값으로 갱신합니다.

seen={}
for count in [100,105,105]:
    key=('A','2026-09-01')
    state='NEW' if key not in seen else ('SAME' if seen[key]==count else 'CHANGED')
    print(state,count)
    seen[key]=count

실행 결과

NEW 100
CHANGED 105
SAME 105

파일 동일성과 관측 동일성 구분

바이트의 표현 차이가 키·값 변경을 뜻하지 않는 사례입니다.

import json,hashlib
a=b'{"count":0}'
b=b'{ "count": 0.0 }'
print('same_bytes',hashlib.sha256(a).hexdigest()==hashlib.sha256(b).hexdigest())
print('same_value',json.loads(a)==json.loads(b))
print('null_equals_zero',None==0)

실행 결과

same_bytes False
same_value True
null_equals_zero False

확인 문제

실습

정제된 JSON 배열의 각 행을 입력 순서대로 분류합니다. 네 필드 region, date, total_vehicles, rain_mm은 존재하며 형식 검사를 통과한 값만 주어집니다. 지역·날짜 키가 처음이면 NEW, 직전 값 쌍과 같으면 SAME, 다르면 CHANGED입니다. 판정 뒤 직전 값을 갱신합니다. 각 행마다 행번호 상태 지역 날짜를 공백으로 구분해 출력합니다. null과 0은 다르고 0과 0.0은 같습니다. 빈 배열은 아무것도 출력하지 않습니다. 이 분류는 진단이며 배치 내부 충돌의 적재 승인은 아닙니다.

모범 답안
import json,sys
rows=json.loads(sys.stdin.read())
seen={}
for i,row in enumerate(rows,1):
    key=(row['region'],row['date'])
    value=(row['total_vehicles'],row['rain_mm'])
    status='NEW' if key not in seen else ('SAME' if seen[key]==value else 'CHANGED')
    print(i,status,row['region'],row['date'])
    seen[key]=value

더 읽기

면접 질문

  • 매일 같은 파일을 읽는 처리에서 중복 적재를 막는 방법을 설명해 주시면 됩니다.