본문으로 건너뛰기
Advertisement

TypedDict, NamedTuple, Protocol

파이썬에서 구조화된 데이터를 타입 안전하게 다루는 세 가지 핵심 도구를 다룹니다. 각각 딕셔너리 구조(TypedDict), 불변 레코드(NamedTuple), 덕 타이핑 인터페이스(Protocol)에 특화되어 있습니다.


TypedDict — 딕셔너리 타입 정의

from typing import TypedDict, Required, NotRequired


# 기본 TypedDict
class Point(TypedDict):
x: float
y: float


class UserProfile(TypedDict):
id: int
name: str
email: str
age: int


# 사용
p: Point = {"x": 1.0, "y": 2.0}
user: UserProfile = {"id": 1, "name": "Alice", "email": "alice@example.com", "age": 30}

print(p["x"])
print(user["name"])


# Python 3.11+: Required / NotRequired 키 지정
class Config(TypedDict):
host: Required[str] # 반드시 있어야 함
port: Required[int]
debug: NotRequired[bool] # 없어도 됨 (기본값 없음)
timeout: NotRequired[float] # 없어도 됨


config: Config = {"host": "localhost", "port": 8080} # debug, timeout 생략 가능
config_full: Config = {"host": "localhost", "port": 8080, "debug": True, "timeout": 30.0}


# total=False: 모든 키를 선택적으로
class PartialUser(TypedDict, total=False):
name: str
email: str
age: int


partial: PartialUser = {"name": "Bob"} # 나머지 생략 가능

TypedDict 상속과 중첩

from typing import TypedDict


class BaseEntity(TypedDict):
id: int
created_at: str
updated_at: str


class Address(TypedDict):
street: str
city: str
country: str
postal_code: str


class Person(BaseEntity): # BaseEntity를 상속
name: str
email: str
address: Address # 중첩 TypedDict


# 타입 안전한 JSON 직렬화 함수
import json
from typing import Any


def to_json(data: dict[str, Any]) -> str:
return json.dumps(data, ensure_ascii=False, indent=2)


person: Person = {
"id": 1,
"created_at": "2024-01-01",
"updated_at": "2024-01-15",
"name": "Alice",
"email": "alice@example.com",
"address": {
"street": "강남대로 1",
"city": "서울",
"country": "대한민국",
"postal_code": "06000",
},
}

print(to_json(person))

NamedTuple — 불변 레코드

from typing import NamedTuple


class Point2D(NamedTuple):
x: float
y: float


class Point3D(NamedTuple):
x: float
y: float
z: float = 0.0 # 기본값 지원


class Employee(NamedTuple):
name: str
department: str
salary: float
is_remote: bool = False


# 사용
p = Point2D(1.0, 2.0)
print(p.x, p.y) # 속성 접근
print(p[0], p[1]) # 인덱스 접근 (튜플이므로)
print(p._asdict()) # {'x': 1.0, 'y': 2.0}

# 불변성
try:
p.x = 3.0 # AttributeError!
except AttributeError as e:
print(f"오류: {e}")

# _replace()로 새 인스턴스 생성
p2 = p._replace(x=10.0)
print(p2) # Point2D(x=10.0, y=2.0)


# 구조적 패턴 매칭과 함께
def classify_employee(emp: Employee) -> str:
match emp:
case Employee(department="Engineering", salary=s) if s > 10000:
return "고연봉 엔지니어"
case Employee(is_remote=True):
return "원격 근무자"
case _:
return "일반 직원"


alice = Employee("Alice", "Engineering", 12000.0)
bob = Employee("Bob", "Marketing", 8000.0, is_remote=True)
print(classify_employee(alice)) # 고연봉 엔지니어
print(classify_employee(bob)) # 원격 근무자

Protocol — 구조적 서브타이핑

from typing import Protocol, runtime_checkable


# Protocol 정의: 메서드 시그니처만 명시
class Drawable(Protocol):
def draw(self) -> None: ...
def get_color(self) -> str: ...


class Resizable(Protocol):
def resize(self, factor: float) -> None: ...


# 프로토콜을 구현하는 클래스들 (명시적 상속 불필요)
class Circle:
def __init__(self, radius: float, color: str = "red"):
self.radius = radius
self.color = color

def draw(self) -> None:
print(f"원 그리기: 반지름={self.radius}, 색={self.color}")

def get_color(self) -> str:
return self.color

def resize(self, factor: float) -> None:
self.radius *= factor


class Square:
def __init__(self, side: float, color: str = "blue"):
self.side = side
self.color = color

def draw(self) -> None:
print(f"정사각형 그리기: 변={self.side}, 색={self.color}")

def get_color(self) -> str:
return self.color


def render(shapes: list[Drawable]) -> None:
for shape in shapes:
shape.draw()


render([Circle(5.0), Square(3.0)]) # 둘 다 Drawable 프로토콜 만족


# @runtime_checkable: isinstance() 검사 가능
@runtime_checkable
class Serializable(Protocol):
def to_dict(self) -> dict: ...
def to_json(self) -> str: ...


class Product:
def __init__(self, name: str, price: float):
self.name = name
self.price = price

def to_dict(self) -> dict:
return {"name": self.name, "price": self.price}

def to_json(self) -> str:
import json
return json.dumps(self.to_dict())


p = Product("Python 책", 35000)
print(isinstance(p, Serializable)) # True

Protocol 상속과 복합 프로토콜

from typing import Protocol, runtime_checkable


class Reader(Protocol):
def read(self, n: int = -1) -> bytes: ...


class Writer(Protocol):
def write(self, data: bytes) -> int: ...


class ReadWriter(Reader, Writer, Protocol):
"""Reader + Writer 복합 프로토콜"""
pass


class Closeable(Protocol):
def close(self) -> None: ...


class Stream(ReadWriter, Closeable, Protocol):
"""완전한 스트림 프로토콜"""
pass


# 실전: 파일-유사 객체를 처리하는 함수
def copy_data(src: Reader, dst: Writer, chunk_size: int = 4096) -> int:
total = 0
while True:
data = src.read(chunk_size)
if not data:
break
written = dst.write(data)
total += written
return total


# 메모리 버퍼 구현 (명시적 상속 없이 프로토콜 만족)
import io

src_buf = io.BytesIO(b"Hello, Protocol World!")
dst_buf = io.BytesIO()
bytes_copied = copy_data(src_buf, dst_buf)
print(f"복사된 바이트: {bytes_copied}")
print(dst_buf.getvalue()) # b'Hello, Protocol World!'

고수 팁

1. TypedDict vs dataclass vs NamedTuple 선택 기준

from dataclasses import dataclass
from typing import TypedDict, NamedTuple


# TypedDict: JSON/dict 인터페이스, 타입 검사만 필요할 때
class APIResponse(TypedDict):
status: int
data: dict
message: str


# NamedTuple: 불변 레코드, 인덱스 접근이 필요할 때, 메모리 효율
class Coordinate(NamedTuple):
lat: float
lon: float
alt: float = 0.0


# dataclass: 뮤터블 객체, 메서드 추가, 기본값, 상속이 필요할 때
@dataclass
class Rectangle:
width: float
height: float

@property
def area(self) -> float:
return self.width * self.height


# 비교
coord = Coordinate(37.5665, 126.9780)
print(coord.lat, coord.lon)
print(coord[0], coord[1]) # 인덱스 접근 가능

rect = Rectangle(3.0, 4.0)
rect.width = 5.0 # 변경 가능
print(rect.area) # 20.0

2. Protocol로 의존성 역전

from typing import Protocol
from abc import abstractmethod


# 나쁜 예: 구체 클래스에 의존
class MySQLDatabase:
def execute(self, sql: str) -> list:
return [] # 실제 구현


class UserService:
def __init__(self, db: MySQLDatabase): # MySQL에 강하게 결합
self.db = db


# 좋은 예: Protocol에 의존 (의존성 역전)
class DatabaseProtocol(Protocol):
def execute(self, sql: str) -> list: ...
def commit(self) -> None: ...
def rollback(self) -> None: ...


class UserServiceV2:
def __init__(self, db: DatabaseProtocol): # 어떤 DB든 OK
self.db = db

def get_user(self, user_id: int) -> dict | None:
rows = self.db.execute(f"SELECT * FROM users WHERE id = {user_id}")
return rows[0] if rows else None


# 테스트: 가짜 DB 주입
class FakeDatabase:
def execute(self, sql: str) -> list:
return [{"id": 1, "name": "Alice"}]

def commit(self) -> None:
pass

def rollback(self) -> None:
pass


service = UserServiceV2(FakeDatabase())
print(service.get_user(1))

정리

도구특징주요 사용처
TypedDict딕셔너리 구조 타입 정의JSON API, 설정, 외부 데이터
NamedTuple불변 튜플 + 이름 접근좌표, 레코드, 경량 값 객체
Protocol구조적 서브타이핑인터페이스 정의, 의존성 역전

세 도구 모두 명시적 상속 없이 타입 안전성을 확보하는 파이썬다운 방식입니다.

Advertisement