본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 객체 지향 설계의 정점 : 디스크립터(Descriptor) 프로토콜 활용 방법과 2가지 핵심 해결책

by Papa Martino V 2026. 3. 12.
728x90
디스크립터(Descriptor) 프로토콜
디스크립터 (Descriptor) 프로토콜

 

파이썬 프로그래밍에서 속성(Attribute)에 접근할 때 단순히 값을 가져오는 것을 넘어, 그 이면에서 유효성 검사, 캐싱, 혹은 동적 계산이 이루어지게 하고 싶을 때가 있습니다. 많은 개발자가 이를 위해 @property를 사용하지만, 여러 속성에 동일한 로직을 반복 적용해야 한다면 코드는 금방 지저분해집니다. 이를 우아하게 해결하기 위한 파이썬의 핵심 메커니즘이 바로 디스크립터(Descriptor) 프로토콜입니다. 본 포스팅에서는 __get__, __set__ 메서드를 이용해 속성 접근 제어권을 완전히 장악하는 방법과, 데이터 디스크립터와 비데이터 디스크립터의 결정적 차이를 심층 분석합니다.


1. 디스크립터(Descriptor)란 무엇인가?

디스크립터는 "하나 이상의 특수 메서드(__get__, __set__, __delete__)를 구현한 클래스의 인스턴스"를 의미합니다. 다른 클래스의 클래스 속성으로 정의되어, 해당 속성에 접근하거나 값을 수정할 때의 동작을 가로채어 정의할 수 있습니다. 이는 파이썬의 @property, @classmethod, 심지어 일반적인 메서드 호출까지도 내부적으로 동작하게 만드는 근간 기술입니다.


2. 데이터 디스크립터 vs 비데이터 디스크립터의 2가지 차이

디스크립터는 구현된 메서드에 따라 두 종류로 나뉘며, 이는 속성 검색 순서(MRO 기반 lookup)에서 매우 중요한 차이를 만듭니다.

구분 항목 데이터 디스크립터 (Data Descriptor) 비데이터 디스크립터 (Non-data Descriptor)
구현 메서드 __get____set__(또는 __delete__) __get__만 구현
우선순위 인스턴스 딕셔너리(__dict__)보다 높음 인스턴스 딕셔너리보다 낮음
주요 용도 유효성 검증, 타입 체크, 읽기 전용 속성 메서드 바인딩, 캐싱(Lazy Property)

3. 디스크립터 프로토콜의 핵심 메서드 활용법

  • __get__(self, obj, objtype=None): 속성 값을 조회할 때 호출됩니다. obj는 인스턴스, objtype은 클래스를 나타냅니다.
  • __set__(self, obj, value): 속성에 값을 할당할 때 호출됩니다. 여기서 데이터 유효성 검사를 수행하여 잘못된 데이터 입력을 원천적으로 차이 낼 수 있습니다.
  • __set_name__(self, owner, name): (Python 3.6+) 디스크립터가 클래스에 할당될 때 속성 이름을 자동으로 인지하게 해주는 편리한 방법입니다.

4. Sample Example: 정수 범위 유효성 검사기

중복되는 유효성 검사 로직을 디스크립터로 분리하여 코드 재사용성을 극대화한 방법입니다.

class IntegerRange:
    def __init__(self, min_val, max_val):
        self.min_val = min_val
        self.max_val = max_val

    def __set_name__(self, owner, name):
        self.name = name

    def __set__(self, obj, value):
        if not isinstance(value, int):
            raise TypeError(f"{self.name}은 정수여야 합니다.")
        if not (self.min_val <= value <= self.max_val):
            raise ValueError(f"{self.name}은 {self.min_val}~{self.max_val} 사이여야 합니다.")
        obj.__dict__[self.name] = value

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

class Player:
    hp = IntegerRange(0, 100)
    level = IntegerRange(1, 99)

# 실전 사용
p = Player()
p.hp = 50       # 정상
# p.hp = 150    # ValueError 발생 (해결: 유효성 자동 검증)
print(f"플레이어 HP: {p.hp}")

5. 실무에서의 해결 전략: Property를 넘어서

단순한 접근 제어라면 @property가 유리하지만, 다음과 같은 상황에서는 디스크립터가 유일한 해결책이 됩니다.

  1. 로직의 재사용: 수십 개의 속성에 동일한 타입 체크나 로그 기록이 필요할 때.
  2. 라이브러리 및 프레임워크 개발: SQLAlchemy나 Django ORM처럼 클래스 선언만으로 DB 필드와 매핑되는 마법 같은 기능을 구현할 때.
  3. 지연 평가(Lazy Evaluation): 고비용 연산 결과를 처음 접근할 때만 계산하고 이후엔 저장된 값을 반환하도록 할 때.

6. 결론: 파이썬 내부 동작의 이해

디스크립터를 이해하는 것은 파이썬이라는 언어의 설계 철학을 깊이 들여다보는 것과 같습니다. 우리가 무심코 사용하는 메서드 호출(instance.method())조차 내부적으로는 함수 객체의 __get__을 통해 바운드 메서드로 변환되는 과정입니다. 이 프로토콜을 자유자재로 다룰 수 있게 될 때, 여러분은 진정으로 파이썬다운(Pythonic) 고수준 설계를 완성할 수 있습니다.


기술적 근거 및 참고 문헌

  • Python Documentation: Descriptor HowTo Guide (Raymond Hettinger)
  • Luciano Ramalho: Fluent Python (Chapter 20: Attribute Descriptors)
  • Brett Slatkin: Effective Python (Item 46: Use Descriptors for Reusable @property Methods)
728x90