ADR-0004: Dict 대신 NamedTuple 반환 선택
상태
Accepted
- 제안일: 2026-02-10
- 승인일: 2026-02-14
맥락 (Context)
aerospike-py에서 get(), query(), batch_get() 등의 operation이 반환하는 레코드 결과의 형식을 결정해야 했습니다.
기존 Aerospike 공식 Python 클라이언트(aerospike PyPI 패키지)는 결과를 (key, meta, bins) tuple 또는 dict를 혼합하여 반환했습니다. 이 방식에는 다음과 같은 문제가 있었습니다:
- 위치 기반 접근의 모호함:
result[0],result[1],result[2]와 같은 인덱스 기반 접근은 코드 가독성을 심각하게 저해 - 타입 안전성 부재: 반환값이 plain tuple/dict이므로 IDE 자동완성, 타입 체커(pyright, mypy)가 내부 구조를 추론 불가
- meta 접근의 불편함: generation count, TTL 등 메타데이터에 접근하려면
result[1]['gen']과 같은 중첩 딕셔너리 접근 필요 - 일관성 없는 반환 형식: operation마다 tuple, dict, None 등 반환 형식이 달라 학습 비용 증가
aerospike-py는 Rust/PyO3 기반으로 새롭게 설계되었으므로, 기존 공식 클라이언트와의 하위 호환성 제약 없이 반환 형식을 자유롭게 결정할 수 있었습니다.
요구사항
- IDE 자동완성(autocomplete)과 타입 체커가 필드를 인식할 것
- 속성(attribute) 접근 방식으로 가독성을 높일 것
- PyO3에서 자연스럽게 구현 가능할 것
- 직렬화(serialization)가 용이할 것
결정 (Decision)
aerospike-py의 모든 레코드 반환값에
NamedTuple패턴을 사용한다.record.bins,record.meta.gen,record.meta.ttl과 같은 속성 접근을 표준으로 한다.
구현 구조
# 반환 타입 정의 (PyO3 #[pyclass])
class RecordMeta:
gen: int # generation count
ttl: int # time-to-live (초)
class Record:
key: RecordKey
meta: RecordMeta
bins: dict[str, Any]
class RecordKey:
namespace: str
set_name: str | None
key: str | int | bytes | None
digest: bytes
사용 예시
# aerospike-py (새로운 NamedTuple 방식)
record = await client.get(key)
print(record.bins["name"]) # bin 접근
print(record.meta.gen) # generation 접근
print(record.meta.ttl) # TTL 접근
print(record.key.namespace) # namespace 접근
# 기존 공식 클라이언트 (비교)
key, meta, bins = client.get(("ns", "set", "pk"))
print(bins["name"]) # bin 접근
print(meta["gen"]) # generation 접근 (dict 키)
적용 범위
get(),exists(): 단일Record반환query(),scan():AsyncIterator[Record]반환batch_get():list[Record | RecordNotFound]반환operate(),operate_ordered():Record반환
대안 검토 (Alternatives Considered)
대안 1: Raw Dict 반환
- 설명:
{"key": {...}, "meta": {"gen": 1, "ttl": 300}, "bins": {"name": "Alice"}}형태의 plain dict 반환 - 장점: 유연성 높음, JSON 직렬화 직접 가능, 추가 클래스 정의 불필요
- 단점: 타입 안전성 없음, IDE 자동완성 불가, 키 오타 런타임까지 미발견 (
result["metta"]같은 오타) - 미선택 사유: 타입 안전성이 핵심 요구사항이었으며 dict는 이를 전혀 충족하지 못함
대안 2: dataclass
- 설명: Python
@dataclass를 사용한 구조화된 반환 타입 - 장점: 타입 안전성, IDE 지원, mutable 속성 변경 가능
- 단점: PyO3에서 Python dataclass를 직접 생성하기 어려움 (Rust 측에서
#[pyclass]로 구현 후 Python dataclass 프로토콜을 에뮬레이션해야 함), 불필요한 mutability - 미선택 사유: PyO3에서의 구현 복잡도가 높고, 레코드 결과는 immutable이 적합
대안 3: TypedDict
- 설명:
TypedDict를 사용하여 dict 키에 타입 힌트 추가 - 장점: dict의 유연성 유지, 타입 체커 지원
- 단점: 런타임 타입 검증 없음 (타입 힌트만 제공), 속성 접근 불가 (여전히
result["bins"]), PyO3에서 TypedDict 생성이 까다로움 - 미선택 사유: 속성 접근이라는 UX 목표를 달성하지 못하며 런타임 안전성도 부족
결과 (Consequences)
긍정적 결과
- 타입 안전성: pyright, mypy에서
record.bins,record.meta.gen등의 타입을 완전히 추론하여 컴파일 타임에 오류 감지 - IDE 자동완성: VS Code, PyCharm 등에서
.입력 시 사용 가능한 필드 목록 자동 표시 - 코드 가독성:
record.meta.gen은result[1]["gen"]보다 의미가 명확 - immutability: NamedTuple 스타일의 frozen 객체로 의도치 않은 수정 방지
- PyO3 호환성: Rust
#[pyclass]로 자연스럽게 구현 가능하며 성능 오버헤드 없음
부정적 결과 / 트레이드오프
- 기존 공식 클라이언트와 비호환:
(key, meta, bins)tuple unpacking 패턴에 익숙한 사용자의 마이그레이션 비용 발생 - 직렬화 추가 단계: JSON으로 직렬화 시
.to_dict()호출이 필요 (plain dict와 달리json.dumps()에 직접 전달 불가) - 학습 비용: aerospike-py 고유의 반환 타입 구조를 새로 학습해야 함
리스크
- 기존 공식 클라이언트에서 마이그레이션하는 사용자가 혼동할 수 있음 (마이그레이션 가이드로 완화)
- Python 생태계에서 NamedTuple보다 dataclass가 표준으로 자리잡을 경우 재검토 필요 (낮은 확률)
영향받는 레포지토리 (Affected Repos)
| 레포 | 영향 내용 |
|---|---|
aerospike-py | 모든 read operation의 반환 타입을 Record NamedTuple로 구현 |
cluster-manager | Backend에서 aerospike-py 호출 시 NamedTuple 속성 접근 패턴 사용 |
plugins | aerospike-py-api Skill에서 NamedTuple 반환 패턴을 가이드 |