Devin.KR

ID로 기록 조회하기

80분 안팎

학습 목표

딕셔너리 키와 중복 정책을 정합니다.

개념

제목 대신 ID를 씁니다

같은 제목의 두 기록을 제목으로 조회하면 어느 기록인지 모호합니다. book-001 같은 별도 ID를 부여하면 제목 수정과 식별을 분리할 수 있습니다. 딕셔너리는 키와 값의 연결이며 store["book-001"]은 해당 ID의 기록을 읽습니다. 기록 한 건도 필드 이름을 키로 쓰는 딕셔너리이므로 바깥은 ID에서 기록으로, 안쪽은 필드에서 값으로 연결됩니다. 두 단계의 키를 혼동하지 않고 먼저 기록을 얻은 뒤 제목을 읽습니다.

키 중복은 자동 거부가 아닙니다

딕셔너리에 같은 키를 대입하면 기존 값이 교체됩니다. 자료구조가 하나의 키에 한 값을 유지한다는 사실과 업무에서 중복 추가를 거부한다는 정책은 다릅니다. 추가 전에 if record_id in store를 검사하고 ERR_DUPLICATE_ID를 반환합니다. 이 검사에서 in은 값을 찾지 않고 키가 있는지 확인합니다. 같은 제목을 가진 다른 ID는 허용합니다. 키가 같고 제목이 다른 입력을 테스트해 첫 기록이 그대로 남는지 확인해야 덮어쓰기 버그를 찾을 수 있습니다.

없는 ID를 정상적인 결과로 다룹니다

사용자가 없는 ID를 조회하는 일은 프로그램 결함과 다릅니다. store[key]는 키가 없으면 KeyError를 발생시키므로 store.get(key)로 None을 받습니다. 값에 None을 저장하지 않는 이 계약에서는 None을 미조회로 해석할 수 있습니다. 값이 0이나 빈 문자열을 허용하는 일반 딕셔너리에서는 if not value만으로 없음을 판단하지 않습니다. 이번 값은 기록 딕셔너리이고 미션 find_record는 None 또는 복사 기록이라는 반환 형태를 명시합니다.

ID 정리 정책을 앞에 둡니다

미션은 문자열 ID의 앞뒤 공백을 strip하고 빈 값이면 ERR_ID를 반환합니다. book-001과 공백을 두른 book-001은 같은 ID입니다. 대소문자는 구분하므로 Book-001은 다른 ID입니다. 조회도 같은 strip 규칙을 적용해야 추가는 성공했는데 조회는 실패하는 혼란을 줄입니다. ID는 임의로 숫자로 바꾸지 않습니다. 001 같은 문자열 식별자에서 앞자리 0은 식별에 필요한 값일 수 있습니다. 입력 계약에서 정한 변환만 수행합니다.

기존 검증을 재사용합니다

새 store.add_record는 ID와 중복을 확인한 뒤 빈 후보 리스트에 기존 records.add_record를 호출합니다. 후보가 성공하면 한 기록에 id 필드를 붙여 원본 store에 넣습니다. 후보가 실패하면 기존 오류 코드만 전달하고 원본은 바꾸지 않습니다. 이렇게 하면 제목·쪽수·금액·완료 검증을 두 곳에 복제하지 않습니다. 먼저 원본에 빈 기록을 만들고 나중에 검증하면 앞 모듈에서 고친 상태 손상 문제가 되살아납니다. 모든 검증이 끝나는 지점을 코드에서 찾습니다.

삽입 순서와 조회 책임

이 실습의 Python 3.7 이상에서는 딕셔너리 삽입 순서를 사용할 수 있습니다. store.values를 순회하면 추가한 순서의 기록을 얻습니다. ID를 사전순으로 정렬한 순서와 다르므로 z를 먼저 넣고 a를 뒤에 넣어 확인합니다. 목록과 키 색인을 각각 원본으로 두면 실패나 수정에서 동기화가 깨질 수 있습니다. 이번 미션은 딕셔너리 하나를 원본으로 두고 필요한 때 리스트를 만듭니다. 순서가 있으므로 정수 위치를 키처럼 사용할 수 있다는 뜻은 아닙니다.

필드 누락과 조회 실패를 나눕니다

KeyError: title은 기록을 찾았지만 title 필드가 없다는 신호일 수 있습니다. KeyError: book-001은 바깥 저장소에서 ID를 찾지 못한 경우일 수 있습니다. traceback에서 어느 대괄호가 실패했는지 읽습니다. 모든 키 오류를 None으로 바꾸면 잘못 만든 기록까지 정상적인 미조회로 숨깁니다. 외부 ID 조회의 부재는 get으로 처리하고 내부 필드 계약 위반은 드러나게 둡니다. 제공된 기록이 계약에 맞는지를 테스트에서 독립적으로 확인합니다.

브라우저에서 정책을 구현합니다

입력은 첫 줄 n 뒤 ID 한 줄과 제목 한 줄을 n번 받고 마지막에 조회 ID 한 줄을 받습니다. ID와 제목은 비어 있지 않으며 ID 앞뒤 공백을 정리합니다. 중복 ID가 나오면 첫 값을 유지하고 거부 횟수를 셉니다. 마지막 출력은 FOUND|제목 또는 NOT_FOUND이고 그 다음 줄은 DUPLICATES|거부횟수입니다. 제목의 내부 공백과 한글을 유지하며 같은 제목의 다른 ID는 거부 횟수에 포함하지 않습니다. 빈 저장소 조회와 중복이 없는 입력을 먼저 시험합니다.

오류 우선순위를 확인합니다

빈 ID와 빈 제목을 동시에 넣으면 ERR_ID가 먼저 나와야 합니다. 이미 있는 ID에 잘못된 쪽수를 넣으면 ERR_DUPLICATE_ID를 반환합니다. 이 순서는 사용자에게 어떤 입력을 먼저 고쳐 달라고 안내할지 결정합니다. 테스트는 중복 ID와 잘못된 필드를 한꺼번에 넣어 코드의 우선순위를 확인합니다. 오류가 나온 뒤 dict 전체를 호출 전 스냅샷과 비교합니다. 길이는 같아도 기존 값이 바뀔 수 있으므로 제목·쪽수·금액·완료와 ID 필드를 모두 포함한 내용 비교가 필요합니다.

브라우저 출력과 미션 반환은 다릅니다

브라우저 과제에서는 거부 횟수와 조회 제목을 표준 출력으로 보입니다. 미션 API는 화면에 인쇄하지 않고 코드와 값을 반환합니다. 함수 안에 디버깅 print를 남기면 이를 호출하는 CLI 출력이나 테스트에 섞입니다. 저장 성공 OK와 조회 성공 기록은 서로 다른 반환 형태이므로 한 변수의 의미를 이름으로 드러냅니다. status는 추가 결과, record는 조회 결과처럼 나누어 읽으면 오류 문자열을 기록 딕셔너리처럼 인덱싱하는 실수를 줄일 수 있습니다.

키와 값의 순회를 선택합니다

for key in store는 ID 문자열들을 하나씩 받습니다. for record in store.values는 기록들을 받으며 for key, record in store.items는 둘을 함께 받습니다. 목록에 제목을 보여 줄 때는 values를, ID와 제목을 함께 표시할 때는 items를 고릅니다. get은 한 키를 조회할 때 쓰는 연산입니다. 목록 전체를 출력하려고 ID마다 get을 호출하는 코드보다 필요한 순회를 직접 선택하면 의도가 명확합니다. 값의 title 필드와 바깥 키를 같은 변수에 번갈아 넣지 않습니다. 각 이름이 무엇을 가리키는지 함수 계약에 적습니다.

따라하기

키 조회와 부재

다음 코드를 demo.py에 저장하고 python3 demo.py로 실행합니다. 단계마다 파일 전체를 교체하므로 이전 단계의 변수에 의존하지 않습니다.

store = {"book-001": {"title": "달빛"}}
print(store["book-001"]["title"])
print(store.get("missing"))
print("달빛" in store)

실행 결과

달빛
None
False

중복 거부

다음 코드를 demo.py에 저장하고 python3 demo.py로 실행합니다. 단계마다 파일 전체를 교체하므로 이전 단계의 변수에 의존하지 않습니다.

store = {"book-001": "달빛"}
key = " book-001 ".strip()
if key in store:
    print("ERR_DUPLICATE_ID")
else:
    store[key] = "별빛"
print(store[key])

실행 결과

ERR_DUPLICATE_ID
달빛

추가 순서와 동등한 문자열

다음 코드를 demo.py에 저장하고 python3 demo.py로 실행합니다. 단계마다 파일 전체를 교체하므로 이전 단계의 변수에 의존하지 않습니다.

store = {}
store["z"] = "달빛"
store["a"] = "별빛"
print(list(store))
key = "".join(["z"])
print(key == "z", store.get(key))
try:
    store["missing"]
except KeyError as error:
    print(type(error).__name__, str(error))

실행 결과

['z', 'a']
True 달빛
KeyError 'missing'

확인 문제

실습

첫 줄 n 뒤 ID·제목을 각각 한 줄씩 n번 받고 마지막 줄 조회 ID를 읽습니다. ID·제목은 비어 있지 않으며 ID는 strip합니다. 중복 ID는 첫 기록을 유지하고 거부 횟수를 셉니다. FOUND|제목 또는 NOT_FOUND 뒤 DUPLICATES|횟수를 한 줄씩 출력합니다. 같은 제목의 다른 ID는 허용합니다.

모범 답안
n = int(input())
store = {}
duplicates = 0
for _ in range(n):
    key = input().strip()
    title = input()
    if key in store:
        duplicates += 1
    else:
        store[key] = title
key = input().strip()
value = store.get(key)
print("NOT_FOUND" if value is None else "FOUND|" + value)
print(f"DUPLICATES|{duplicates}")

더 읽기

면접 질문

  • 리스트 대신 딕셔너리를 선택하는 사례를 설명합니다.