확인 상태를 돌려받았는데 바꾸려던 출처는 그대로였다. K-POP PULSE의 잘못된 수정 필드 기록을 읽고 나는 기대한 값과 저장 후 읽은 값을 따로 비교하는 작은 프로그램을 만들었다. 그때의 API를 고친 코드나 원본 프로그램 복원본은 아니다.

홈페이지마다 바꿀 필드는 다르다. 제목과 상태, 설정값처럼 해당 사이트에서 확인할 필드를 공급하면 결과를 나누어 준다. 읽지 못한 값을 성공으로 채우지 않으며, 코드와 합성 입력, 실제 CLI 검사와 적용 조건을 함께 제공한다.

응답의 기분 좋은 낱말보다 목표 필드를 선택한다

프로그램은 확인 상태가 있다는 이유로 완료를 정하지 않는다. expected에는 실제로 바뀌어야 할 필드가 들어가야 한다. 상태 이름 하나만 비교하면 그 상태는 같고 다른 값은 그대로인 상황을 놓칠 수 있다. 어느 값이 정답인지 고르는 판단은 사이트의 작업 요청과 입력 계약에서 해야 한다.

아래 합성 예제에서는 확인 상태가 있어도 source_id가 기대와 다르다. 실제 고객 자료나 원본 행사 데이터를 옮긴 예제가 아니다. 날짜와 주소, 제목 등 어떤 필드를 비교할지는 그 사이트의 의미에 맞추고 읽기 결과가 같은 대상의 것인지도 먼저 확인한다.

코드와 구현 살펴보기

새 합성 입력. 실제 목표 필드와 별도로 읽은 레코드로 바꾼다.

{
  "expected": {
    "source_id": "replacement",
    "enabled": true
  },
  "readback": {
    "source_id": "prior",
    "enabled": true,
    "status": "CONFIRMED"
  }
}

일치와 불일치, 읽지 못한 상태를 나눈다

명시한 필드의 값과 종류가 같으면 match, 확인한 값이나 종류가 다르면 mismatch다. 읽기 결과가 없거나 필드가 없으면 unverifiable로 남긴다. 미확인 필드를 빈 문자열이나0으로 만들어 같다고 처리하지 않는다. 값이 null인 경우와 필드 자체가 없는 경우도 다르다.

여러 필드 중 불일치와 검사불가가 함께 있으면 전체는 불일치로 끝나고 개별 상태를 모두 남긴다. 실제로 틀린 값이 있는 상황과 추가로 읽어야 할 항목을 함께 볼 수 있다. 일부 확인이 맞았다는 이유로 다른 항목까지 완료 상태로 올리지 않는다.

비교의 엄격함도 적용할 계약으로 남긴다

새 코드의 비교는 true와 1, 정수 1과 실수 1.0을 다른 종류로 다룬다. 이는 이 Python 프로그램의 명시한 계약이며 모든 API의 의미를 자동 판정하는 표준은 아니다. 해당 서버가 정한 자료 형태를 읽고 필요하면 어댑터에서 의미가 확인된 정규화를 수행해야 한다.

날짜나 오프셋 문자열, URL을 비슷해 보인다고 자동으로 같게 만들지 않는다. 요청하지 않은 최상위 읽기 필드는 무시하지만 명시한 필드의 중첩 객체는 전체 키와 값을 비교하고 배열의 순서를 유지한다. 부분 객체를 비교하려면 필요한 값을 의도대로 추출해 공급한다.

읽기 결과의 출처와 시점은 별도 책임이다

이 프로그램은 API를 호출하거나 쓰기와 재시도를 하지 않는다. 어떤 저장소에서 언제 읽었고 그 레코드가 요청 대상과 맞는지는 호출자가 확인한다. 예전에 저장한 사본을 새 읽기처럼 넣으면 비교가 맞아도 현재 반영의 증거가 되지 않는다.

서버가 반영에 시간이 필요한 구조라면 그 상태와 다시 읽을 조건도 사이트에 맞게 정한다. 입력에 값이 들어 있다는 이유로 실제 요청이 수행됐거나 최신이라고 인증하지 않는다. 나는 확인한 자료를 비교하는 책임과 자료를 올바르게 가져오는 책임을 함께 설명해 적용할 범위를 남긴다.

실행 결과를 다른 종료 코드로 전달한다

표준입력이나 --input 파일로 JSON을 읽는다. 일치는 종료 0, 불일치는 종료 1, 검사불가와 입력 오류는 종료 2로 반환하며 JSON state에서 두 경우를 구별한다. 기본 계약은 예상 필드 1~1000개, 중첩 25단계, CLI 입력 1MiB까지다. 필요한 한도는 계약과 검사를 함께 바꾼다.

출력에는 비교 값 대신 필드명과 상태가 남는다. 필드명 자체가 민감할 가능성까지 자동 판정하지는 않는다. 공개 예제와 로그에 비밀값을 넣지 않고, 실제 사이트의 결과를 공유할 때는 정보 범위를 따로 검토한다. 보안과 개인정보 전체를 인증하는 도구로 넓히지 않는다.

작은 비교가 실제 운영의 다음 행동을 만든다

새 합성 입력과 실제 CLI를 포함한 26개 검사가 통과했다. 누락과 null, 종류의 차이, 중첩 값과 순서, 잘못된 계약과 종료 코드를 확인했다. 원본 API가 지금도 같은 결함을 가진다거나 모든 쓰기와 화면 반영을 확인한 결과는 아니다.

아래 구현과 공유 ZIP의 검사·예제·사용법을 함께 가져갈 수 있다. 다른 홈페이지에서는 불일치 항목을 다시 읽거나 입력을 고치고, 검사불가 항목의 자료를 더 확보하는 흐름과 연결한다. 나는 성공처럼 보이는 한 줄을 넘어 무엇을 확인했고 무엇이 남았는지 알 수 있는 결과를 공유하고 싶다.

코드와 구현 살펴보기

새 명시 필드 비교 구현. 원본 API 수정이나 쓰기 프로그램이 아니다.

"""Compare explicit expected JSON fields with a supplied readback; no request or write."""
import argparse,json,math,sys
from pathlib import Path

def checked_json(value,depth=0):
    if depth>25:raise ValueError('JSON nesting limit')
    if value is None or type(value) in (str,bool,int):return
    if type(value) is float and math.isfinite(value):return
    if type(value) is list:
        for item in value:checked_json(item,depth+1)
        return
    if type(value) is dict and all(type(key) is str for key in value):
        for item in value.values():checked_json(item,depth+1)
        return
    raise ValueError('JSON-native values required')

def same_json(a,b):
    if type(a) is not type(b):return False
    if type(a) is dict:return set(a)==set(b) and all(same_json(a[key],b[key]) for key in a)
    if type(a) is list:return len(a)==len(b) and all(same_json(x,y) for x,y in zip(a,b))
    return a==b

def check_readback(record):
    if type(record) is not dict or set(record)-{'expected','readback'}:raise ValueError('Unsupported input')
    expected=record.get('expected');readback=record.get('readback')
    if type(expected) is not dict or not 1<=len(expected)<=1000:raise ValueError('One to1000 expected fields required')
    if readback is not None and type(readback) is not dict:raise ValueError('Readback must be a record or null')
    checked_json(expected);checked_json(readback);rows=[]
    for name,value in expected.items():
        if readback is None:state,reason='unverifiable','readback_unavailable'
        elif name not in readback:state,reason='unverifiable','field_absent'
        elif same_json(value,readback[name]):state,reason='match','equal'
        else:state,reason='mismatch','value_or_type_differs'
        rows.append({'field':name,'state':state,'reason':reason})
    state='mismatch' if any(x['state']=='mismatch' for x in rows) else 'unverifiable' if any(x['state']=='unverifiable' for x in rows) else 'match'
    return {'state':state,'fields':rows,'counts':{s:sum(x['state']==s for x in rows) for s in ['match','mismatch','unverifiable']}}

def main():
    parser=argparse.ArgumentParser();parser.add_argument('--input',type=Path);args=parser.parse_args()
    try:
        if args.input:
            with args.input.open('rb') as stream:raw=stream.read(1048577)
        else:raw=sys.stdin.buffer.read(1048577)
        if len(raw)>1048576:raise ValueError('Input limit')
        result=check_readback(json.loads(raw.decode('utf-8')))
    except (ValueError,TypeError,OSError,RecursionError):
        print(json.dumps({'state':'invalid_input','error':'Invalid comparison contract'}));return 2
    print(json.dumps(result,ensure_ascii=False));return {'match':0,'mismatch':1,'unverifiable':2}[result['state']]
if __name__=='__main__':raise SystemExit(main())
새 비교 코드·예제·검사·설명 ZIP