
파이썬의 속성 접근 방식은 겉으로 보기엔 단순해 보이지만, 그 이면에는 디스크립터(Descriptor)라는 강력한 프로토콜이 존재합니다. 우리가 흔히 사용하는 @property, @classmethod, @staticmethod는 모두 이 디스크립터 프로토콜을 기반으로 동작합니다. 하지만 실무에서 커스텀 디스크립터를 설계할 때 가장 많이 겪는 혼란은 "내가 정의한 속성과 인스턴스 변수 중 무엇이 먼저 참조되는가?"에 대한 우선순위 문제입니다. 본 가이드에서는 데이터 디스크립터와 비데이터 디스크립터의 참조 우선순위 결정 방식을 심도 있게 분석하고, 이를 통해 예기치 못한 속성 덮어쓰기 문제를 해결하는 전문적인 접근법을 제시합니다.
1. 디스크립터의 본질: __get__과 __set__
디스크립터는 하나 이상의 "바인딩 동작"을 가진 객체 속성입니다. 파이썬의 속성 조희 시스템은 obj.name을 호출할 때 단순히 딕셔너리를 뒤지는 것이 아니라, 해당 속성이 디스크립터인지 먼저 확인합니다.
- __get__(self, obj, type=None): 속성 값을 반환할 때 호출됩니다.
- __set__(self, obj, value): 속성 값을 설정할 때 호출됩니다.
- __delete__(self, obj): 속성을 삭제할 때 호출됩니다.
2. 데이터 vs 비데이터 디스크립터의 2가지 핵심 차이
디스크립터는 정의된 메서드의 조합에 따라 두 가지로 분류되며, 이는 파이썬의 속성 검색 순서(MRO 및 Lookup Chain)에 결정적인 영향을 미칩니다.
| 항목 | 데이터 디스크립터 (Data Descriptor) | 비데이터 디스크립터 (Non-data Descriptor) |
|---|---|---|
| 필수 메서드 | __get__ 그리고 __set__ (또는 __delete__) |
__get__ 만 정의됨 |
| 참조 우선순위 | 1위 (인스턴스 딕셔너리보다 높음) | 3위 (인스턴스 딕셔너리보다 낮음) |
| 일반적 예시 | @property, 유효성 검사기 |
메서드, @staticmethod, @classmethod |
| 덮어쓰기 결과 | 인스턴스 변수로 대체 불가능 | 동일 이름의 인스턴스 변수에 의해 가려짐 |
3. 파이썬 속성 조회의 우선순위 체계 (Lookup Chain)
파이썬에서 obj.a를 찾을 때 인터프리터는 다음의 엄격한 순서를 따릅니다. 이 순서를 이해하는 것이 디스크립터 관련 버그를 해결하는 핵심입니다.
- 데이터 디스크립터: 클래스 영역에
__set__이 포함된 디스크립터가 있다면 최우선으로 호출됩니다. - 인스턴스 변수:
obj.__dict__에 해당 키가 있는지 확인합니다. - 비데이터 디스크립터: 인스턴스에 값이 없을 경우, 클래스의
__get__만 있는 디스크립터를 실행합니다. - 클래스 변수: 클래스 영역에 정의된 일반 값을 반환합니다.
- __getattr__: 위 단계에서 모두 실패할 경우 마지막 보루로 호출됩니다.
4. 실무 Sample Example: 우선순위 역전 현상
비데이터 디스크립터가 인스턴스 변수에 의해 어떻게 가려지는지, 그리고 데이터 디스크립터는 어떻게 이를 방어하는지 보여주는 예제입니다.
class NonDataDesc:
def __get__(self, obj, objtype=None):
return "Non-data Descriptor Value"
class DataDesc:
def __get__(self, obj, objtype=None):
return "Data Descriptor Value"
def __set__(self, obj, value):
print(f"Setting value to {value}")
class MyClass:
nd = NonDataDesc()
d = DataDesc()
obj = MyClass()
# 1. 비데이터 디스크립터 테스트
print(obj.nd) # 결과: Non-data Descriptor Value
obj.nd = "Instance Value"
print(obj.nd) # 결과: Instance Value (디스크립터가 가려짐!)
# 2. 데이터 디스크립터 테스트
print(obj.d) # 결과: Data Descriptor Value
obj.d = "New Value" # 결과: Setting value to New Value
print(obj.d) # 결과: Data Descriptor Value (인스턴스 변수가 생성되지 않음)
5. 고급 설계 전략: 언제 어떤 것을 사용할까?
전문적인 라이브러리를 설계할 때, 이 우선순위 차이를 전략적으로 활용해야 합니다.
- 데이터 디스크립터 사용: 속성에 값을 할당할 때 타입 체크나 유효성 검사(Validation)가 반드시 필요한 경우 사용합니다. 인스턴스가 제멋대로 값을 바꾸는 것을 원천 봉쇄할 수 있습니다.
- 비데이터 디스크립터 사용: 초기 비용이 큰 연산의 결과를 캐싱(Caching)할 때 유용합니다. 첫 호출 시
__get__에서 연산 후 인스턴스 딕셔너리에 결과를 저장하면, 다음 호출부터는 디스크립터가 아닌 인스턴스 딕셔너리에서 값을 가져오므로 성능이 최적화됩니다.
6. 결론 및 해결책
디스크립터 우선순위로 발생하는 버그를 해결하려면 해당 객체의 dir()나 __dict__를 조사하기에 앞서, 클래스 수준에서 __set__ 메서드의 존재 여부를 먼저 파악해야 합니다. "데이터 디스크립터는 인스턴스의 주권을 침해하고, 비데이터 디스크립터는 인스턴스에게 주권을 양보한다"는 원칙만 기억한다면 파이썬의 복잡한 메타 프로그래밍을 완벽히 통제할 수 있습니다.
7. 정보 출처
- Python Documentation: Descriptor HowTo Guide by Raymond Hettinger
- Fluent Python 2nd Edition: Chapter 23. Attribute Descriptors
- Effective Python: Item 46. Use Descriptors for Reusable @property Methods
'Artificial Intelligence > 60. Python' 카테고리의 다른 글
| [PYTHON] 메타클래스(type)를 활용한 클래스 생성 제어 방법과 3가지 핵심 차이점 (0) | 2026.02.24 |
|---|---|
| [PYTHON] __init_subclass__를 활용한 서브클래스 등록 자동화 방법과 메타클래스와의 3가지 차이점 (0) | 2026.02.24 |
| [PYTHON] 런타임의 마법사, Monkey Patching의 3가지 위험성과 이를 안전하게 테스트하는 5단계 해결 방법 (0) | 2026.02.23 |
| [PYTHON] 확장성을 극대화하는 1가지 비결, Dynamic Import를 활용한 플러그인 아키텍처 설계 방법과 문제 해결 (0) | 2026.02.23 |
| [PYTHON] 객체 내부를 들여다보는 3가지 introspection 도구의 성능 비용 차이와 최적화 해결 방법 (0) | 2026.02.23 |