[파이썬 100강] 76강. enum으로 상태와 상수를 안전하게 관리하기
숫자 1, 문자열 "done", 대문자 상수 STATUS_OK = "ok"를 여기저기 흩어 쓰기 시작하면, 프로젝트가 커질수록 상태값 관리가 급격히 불안정해집니다. 이번 강의에서는 enum을 사용해 상태와 상수의 의미를 코드에 명확히 고정하는 방법을 바로 예제로 익힙니다.
핵심 개념
Enum은 “허용 가능한 값 집합”을 이름 있는 멤버로 표현해, 매직 문자열/숫자를 줄여줍니다.IntEnum은 정수와의 호환이 필요한 레거시 연동에서 유용하고,StrEnum(3.11+)은 문자열 기반 API 응답과 잘 맞습니다.auto()를 쓰면 값 부여를 자동화할 수 있지만, 외부 시스템과 계약이 있는 경우에는 값을 명시하는 편이 안전합니다.
현업에서 가장 자주 생기는 버그 중 하나가 “값은 맞는데 의미가 틀린 상태”입니다. 예를 들어 주문 상태를 "paid", "PAID", "payment_done"처럼 팀원마다 다르게 쓰면, 분기문은 통과했는데 집계가 틀어지는 상황이 반복됩니다. Enum을 쓰면 가능한 상태를 코드 레벨에서 닫아 둘 수 있어, 오타·중복·비일관성을 크게 줄일 수 있습니다. 또 IDE 자동완성과 타입 힌트가 강하게 작동해 리뷰와 유지보수 속도도 올라갑니다. 특히 결제/배송/권한처럼 “상태 전이”가 중요한 도메인에서는 Enum 도입만으로도 운영 장애율이 눈에 띄게 줄어듭니다.
기본 사용
예제 1) 가장 기본적인 Enum 선언과 비교
>>> from enum import Enum
>>>
>>> class OrderStatus(Enum):
... PENDING = "pending"
... PAID = "paid"
... SHIPPED = "shipped"
... CANCELED = "canceled"
...
>>> OrderStatus.PAID
<OrderStatus.PAID: 'paid'>
>>> OrderStatus.PAID.value
'paid'
>>> OrderStatus.PAID == "paid"
False
>>> OrderStatus.PAID.value == "paid"
True
해설:
Enum멤버 자체와 원시 값(value)은 다릅니다. 분기에서는 멤버를 비교하고, 직렬화 직전에만value를 꺼내는 습관이 좋습니다.== "paid"같은 비교는 의도와 다르게 동작할 수 있으니, 코드베이스 전체에서 비교 규칙을 통일하세요.
예제 2) 문자열 입력을 안전하게 Enum으로 변환
>>> from enum import StrEnum
>>>
>>> class Role(StrEnum):
... ADMIN = "admin"
... EDITOR = "editor"
... VIEWER = "viewer"
...
>>> Role("admin")
<Role.ADMIN: 'admin'>
>>> Role("ADMIN")
Traceback (most recent call last):
...
ValueError: 'ADMIN' is not a valid Role
해설:
- 외부 입력 문자열을
Role(raw)로 즉시 변환하면, 유효하지 않은 값이 초기에 차단됩니다. - 사용자 입력은 대소문자/공백 정규화를 먼저 한 뒤 변환하면 운영 에러를 줄일 수 있습니다.
예제 3) IntEnum으로 레거시 숫자 코드와 호환
>>> from enum import IntEnum
>>>
>>> class HttpCode(IntEnum):
... OK = 200
... BAD_REQUEST = 400
... NOT_FOUND = 404
...
>>> HttpCode.OK == 200
True
>>> int(HttpCode.NOT_FOUND)
404
>>> [code.name for code in HttpCode if code >= 400]
['BAD_REQUEST', 'NOT_FOUND']
해설:
- 정수 비교가 필요한 기존 코드와 연결할 때
IntEnum은 이행 비용을 낮춰 줍니다. - 다만 새 코드에서는 숫자 자체보다
HttpCode.NOT_FOUND처럼 의미 있는 이름 중심으로 작성해야 가독성이 유지됩니다.
예제 4) 상태 전이 규칙을 Enum 기반으로 명시
>>> from enum import Enum
>>>
>>> class TicketStatus(Enum):
... OPEN = "open"
... IN_PROGRESS = "in_progress"
... RESOLVED = "resolved"
... CLOSED = "closed"
...
>>> ALLOWED_TRANSITIONS = {
... TicketStatus.OPEN: {TicketStatus.IN_PROGRESS, TicketStatus.CLOSED},
... TicketStatus.IN_PROGRESS: {TicketStatus.RESOLVED, TicketStatus.CLOSED},
... TicketStatus.RESOLVED: {TicketStatus.CLOSED},
... TicketStatus.CLOSED: set(),
... }
>>>
>>> def can_move(current: TicketStatus, target: TicketStatus) -> bool:
... return target in ALLOWED_TRANSITIONS[current]
...
>>> can_move(TicketStatus.OPEN, TicketStatus.RESOLVED)
False
>>> can_move(TicketStatus.IN_PROGRESS, TicketStatus.RESOLVED)
True
해설:
- 상태 전이표를 Enum 키로 두면 문자열 오타로 인한 규칙 누락을 막을 수 있습니다.
- QA 시나리오를 만들 때도 상태 이름이 일관되어 테스트 가독성이 좋아집니다.
자주 하는 실수
실수 1) Enum 멤버와 원시 값을 섞어 비교
>>> from enum import Enum
>>> class Payment(Enum):
... READY = "ready"
... DONE = "done"
...
>>> status = Payment.DONE
>>> if status == "done":
... print("결제 완료")
...
원인:
Enum객체와 문자열을 동일하게 생각해 비교합니다. 실행은 되지만 조건이 거짓이라 조용히 로직이 틀어집니다.
해결:
>>> if status is Payment.DONE:
... print("결제 완료")
...
결제 완료
실수 2) auto()를 외부 계약 코드에 사용
>>> from enum import Enum, auto
>>> class ExternalStatus(Enum):
... OK = auto()
... FAIL = auto()
...
>>> ExternalStatus.OK.value
1
원인:
- 외부 API가
"OK","FAIL"같은 고정 문자열을 요구하는데 내부에서 자동 숫자를 써 버리면 직렬화 시 계약이 깨집니다.
해결:
>>> class ExternalStatus(Enum):
... OK = "OK"
... FAIL = "FAIL"
...
>>> ExternalStatus.OK.value
'OK'
실수 3) DB/JSON에서 읽은 문자열을 바로 신뢰
- 증상: 알 수 없는 상태값이 들어왔는데도 분기문 default로 흘러가 장애를 늦게 발견함
- 원인: 역직렬화 시 유효성 검증 없이 문자열을 그대로 사용
- 해결:
status = OrderStatus(raw_status)로 즉시 검증하고,ValueError를 잡아 경고 로그 + 격리 처리
실수 4) Enum 이름(name)과 값(value)의 용도를 혼동
- 증상: 운영 로그에는
PAID를 남겼는데 API 응답에는paid가 필요해 불일치 발생 - 원인:
name(개발자용 식별자)과value(외부 표현)를 구분하지 않음 - 해결: 내부 비교/분기에는 멤버 자체 사용, 외부 I/O 직전만
value, 디버그/로그 정책 문서화
실무 패턴
-
입력 검증 규칙
- API/큐/DB에서 들어오는 상태 문자열은 경계 계층에서 즉시 Enum으로 변환합니다.
- 변환 실패는 400/도메인 에러로 명확히 반환하고, 원본 페이로드를 마스킹 로그로 남깁니다.
-
로그/예외 처리 규칙
- 상태 변경 로그는
from=OrderStatus.PENDING to=OrderStatus.PAID처럼 멤버 단위로 기록해 추적성을 높입니다. - 허용되지 않은 전이는
ValueError가 아니라 도메인 전용 예외(예:InvalidTransitionError)로 분리하는 것이 좋습니다.
- 상태 변경 로그는
-
재사용 함수/구조화 팁
parse_status(raw: str) -> OrderStatus같은 단일 진입점을 만들어 변환 정책을 중앙화합니다.- Enum과 상태 전이표를 같은 모듈에 두고, 테스트도 전이표 기준으로 파라미터화하면 누락이 줄어듭니다.
-
성능/메모리 체크 포인트
- 일반적인 웹/백엔드 트래픽에서는 Enum 오버헤드가 미미하지만, 초고빈도 루프에서는 반복적인 문자열↔Enum 변환을 캐시하세요.
- 대량 직렬화 시
.value추출을 배치 처리하면 미세한 비용을 줄일 수 있습니다.
오늘의 결론
한 줄 요약: 상태/상수는 문자열로 흩뿌리지 말고 Enum으로 닫아 두면, 코드 품질과 운영 안정성이 같이 올라갑니다.
기억할 것:
- 내부 분기에서는
Enum 멤버를 비교하고, 외부 I/O 직전에만.value를 사용하세요. - 외부 계약(HTTP, DB 코드)이 있는 값은
auto()보다 명시값이 안전합니다. - 상태 전이 규칙을 Enum 기반 테이블로 고정하면 테스트·리뷰·운영 대응이 쉬워집니다.
연습문제
OrderPhaseEnum(CREATED,PAID,PACKED,SHIPPED)을 만들고, 문자열 입력을 Enum으로 변환하는parse_order_phase()함수를 작성하세요. 잘못된 입력에는ValueError를 발생시키세요.IntEnum으로 우선순위(LOW=1,MEDIUM=2,HIGH=3)를 만들고, 우선순위 목록을 높은 순으로 정렬하는 코드를 작성하세요.- 티켓 상태 Enum을 만들고, 현재 상태에서 이동 가능한 다음 상태 목록을 반환하는
next_states()함수를 작성하세요.
이전 강의 정답
EvenNumbers(n)시퀀스 구현 (음수 인덱스/슬라이스 지원)
>>> from collections.abc import Sequence
>>> class EvenNumbers(Sequence):
... def __init__(self, n):
... self.n = n
... def __len__(self):
... return self.n
... def __getitem__(self, index):
... if isinstance(index, slice):
... s, e, step = index.indices(len(self))
... return [2 * i for i in range(s, e, step)]
... if index < 0:
... index += len(self)
... if index < 0 or index >= len(self):
... raise IndexError("index out of range")
... return 2 * index
...
>>> nums = EvenNumbers(5)
>>> nums[-1]
8
>>> nums[1:4]
[2, 4, 6]
- 키 앞뒤 공백을 제거하는
TrimmedConfig
>>> from collections.abc import MutableMapping
>>> class TrimmedConfig(MutableMapping):
... def __init__(self):
... self._data = {}
... def __getitem__(self, key):
... return self._data[key.strip()]
... def __setitem__(self, key, value):
... self._data[key.strip()] = value
... def __delitem__(self, key):
... del self._data[key.strip()]
... def __iter__(self):
... return iter(self._data)
... def __len__(self):
... return len(self._data)
...
>>> cfg = TrimmedConfig()
>>> cfg[" host "] = "localhost"
>>> cfg["host"]
'localhost'
Mapping만 받는to_query_string()구현
>>> from collections.abc import Mapping
>>> def to_query_string(params):
... if not isinstance(params, Mapping):
... raise TypeError("params must satisfy Mapping")
... return "&".join(f"{k}={v}" for k, v in params.items())
...
>>> to_query_string({"page": 1, "size": 20})
'page=1&size=20'
>>> to_query_string([("page", 1)])
Traceback (most recent call last):
...
TypeError: params must satisfy Mapping
실습 환경/재현 정보
- 실행 환경:
condaenvpython100(Python 3.11.14) - 가정한 OS: macOS/Linux 공통
- 사용 모듈:
enum(Enum,IntEnum,StrEnum,auto) - 실행 방법: 터미널에서
python또는ipython실행 후 예제를 순서대로 입력 - 재현 체크:
- Enum 멤버 비교(
is)와.value비교 차이를 직접 확인 - 문자열 입력 변환 시 잘못된 값이
ValueError로 차단되는지 확인 - 상태 전이 테이블에서 허용/비허용 전이가 기대대로 판단되는지 확인
- Enum 멤버 비교(