Python Descriptor 에 대해 알아보자

김규민·2025년 4월 30일

개발공부

목록 보기
2/2

공식 사이트 및 참고 사이트

Descriptor Guide

용어집


Descriptor

Any object which defines the methods __get__()__set__(), or __delete__().
When a class attribute is a descriptor, its special binding behavior is triggered upon attribute lookup.
Normally, using a.b to get, set or delete an attribute looks up the object named b in the class dictionary for a, but if b is a descriptor, the respective descriptor method gets called.

Python의 descriptor는 단순히 "클래스의 속성을 클래스 형태로 대체"하는 것을 넘어, 속성 접근 방식 자체를 제어할 수 있는 메커니즘입니다.

Python 의 @property 데코레이터를 사용해 본 적 있다면 이해하기 수월합니다.

getter, setter 와 같은 메소드들을 마치 변수처럼 사용할 수 있도록 동작합니다.


descriptor 학습 계기
Django 의 View 클래스의 내부 메소드 중, view_is_async 에 대해 공부하는 중 마주친 @classproperty 라는 데코레이터 때문에 descriptor 라는 개념에 대해 공부하게 되었습니다.

개인적으로 프로그래밍 공부를 하며 생소한 단어가 문법으로 등장하니 머릿속이 복잡했는데, descriptor 라는 단어의 의미 그대로 설명을 대신 해준다는 의미로 받아 들이면 좋을 것 같습니다.

클래스에 직접 메소드를 작성하는 것보다 훨씬 간결하고 재사용성이 높게 메소드를 추가할 수 있습니다.


Descriptor 의 정의

1. Descriptor란?

Descriptor는 클래스 내부에서 __get__, __set__, __delete__ 중 하나 이상을 정의한 객체입니다. 이 객체는 다른 클래스의 속성으로 사용될 때, 속성 접근 (a.b)을 제어할 수 있습니다.

class MyDescriptor:
    def __get__(self, instance, owner):
        print("Getting value")
        return 42

    def __set__(self, instance, value):
        print("Setting value:", value)

class MyClass:
    x = MyDescriptor()  # descriptor 객체

obj = MyClass()
print(obj.x)  # __get__ 호출
obj.x = 100   # __set__ 호출

2. 어디에 쓰나요?

  • 속성 접근 제어 (예: 읽기 전용, 유효성 검사 등)
  • 데이터의 캡슐화 (예: @property 대체)
  • ORM 구현 등 메타 프로그래밍 (예: Django 모델 필드)

3. 종류

이름메서드 필요예시 기능
데이터 디스크립터__get__, __set__ 정의값 설정/검증 가능
비데이터 디스크립터__get__만 정의읽기 전용 속성 등

데이터 디스크립터가 우선순위가 더 높습니다 (인스턴스 딕셔너리보다 먼저 호출됨).


4. @property도 descriptor다

class MyClass:
    def __init__(self):
        self._x = 0

    @property
    def x(self):          # property object가 __get__ 정의된 descriptor
        return self._x

    @x.setter
    def x(self, value):   # __set__ 정의됨
        self._x = value

property도 내부적으로 descriptor 객체입니다. 즉, x = property(...)처럼 속성을 class 수준에서 descriptor로 대체한 것입니다.


5. 결론

Python의 descriptor는 클래스의 속성 접근을 자동으로 가로채어 동작을 정의할 수 있는 프로토콜입니다.

단순한 "클래스 형태로 대체"가 아니라, 속성의 읽기/쓰기/삭제 동작을 사용자 정의로 제어할 수 있는 강력한 도구입니다.


필요하시면 실제 사용 예시나 __get__ 호출 순서 흐름도도 함께 드릴 수 있습니다. 더 자세한 내용이 필요하신가요?


Python Descriptor 와 Java getter/setter 의 차이

Python의 descriptor와 Java의 getter/setter 메서드는 기능적으로 유사한 목적(속성 제어)을 갖지만, 동작 방식과 철학이 완전히 다릅니다.

다음 표와 함께 핵심 차이점을 설명드리겠습니다.


Python descriptor vs Java getter/setter

항목Python descriptorJava getter/setter
핵심 개념속성 접근 자체를 가로채는 특별한 객체 프로토콜속성을 직접 노출하지 않고, 메서드로 값을 얻고(set)/설정(get)
사용 방식__get__, __set__, __delete__ 메서드 구현getX(), setX() 식의 명시적 메서드 호출
문법적 통일성obj.x로 접근해도 자동으로 동작obj.getX()처럼 메서드 호출
속성처럼 사용 가능(ex: @property or descriptor 직접 구현)❌ 메서드이기 때문에 속성처럼 접근 불가
재사용 가능성속성 제어 로직을 여러 클래스에 재사용 가능 (ex: ValidatedField)일반적으로 클래스마다 새로 작성
OOP 철학"속성을 제어하는 방식"도 객체화"정보 은닉(encapsulation)" 강조

예시 비교

📌 Java 스타일

public class Person {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String value) {
        this.name = value;
    }
}

📌 Python descriptor 스타일

class NameDescriptor:
    def __get__(self, instance, owner):
        return instance._name

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise ValueError("name must be a string")
        instance._name = value

class Person:
    name = NameDescriptor()

    def __init__(self, name):
        self.name = name  # -> __set__ 호출됨

p.name으로 접근해도 내부적으로 get이 호출됨 → 속성처럼 보이지만 메서드처럼 동작


요약: 주요 차이점

  • Java: getX()/setX() 메서드를 명시적으로 호출해야 함
  • Python: obj.x처럼 속성처럼 사용하면서 내부적으로 __get__/__set__ 호출 가능

즉, Python은 "속성과 메서드의 구분을 숨길 수 있음" (→ 내부 구현은 유연, 인터페이스는 단순)


필요하다면 @property vs descriptor 차이도 정리해드릴 수 있어요. 추가로 비교해볼까요?


@property vs descriptor 차이

Python에서 @property와 descriptor는 둘 다 속성 접근 제어를 위해 사용되며, 실제로 @property도 내부적으로 descriptor입니다. 하지만 두 방식은 목적과 사용 편의성, 유연성 측면에서 차이가 있습니다.


@property vs descriptor 차이 비교

항목@propertydescriptor (직접 구현)
정의 방법메서드에 @property / @x.setter 데코레이터 사용__get__, __set__ 메서드를 가진 클래스를 속성으로 정의
사용 목적단일 클래스 내의 속성 제어 (간편함)복수 클래스에서 속성 제어 로직을 재사용하고 싶을 때
캡슐화 수준간결하고 명확 (읽기 전용, 유효성 검사 등)복잡한 제어, 공통 로직 추출 시 유리
재사용성낮음 (클래스마다 새로 정의)높음 (여러 클래스에 descriptor 객체 재사용 가능)
읽기 전용 구현간단 (@property만 정의)__get__만 정의하면 됨
표준 라이브러리 지원 여부Python 문법에 기본 내장Python의 descriptor protocol로 지원됨

예제 비교

📌 @property 사용 (간단하고 클래스 내부 집중)

class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):  # 읽기
        return self._name

    @name.setter
    def name(self, value):  # 쓰기
        if not isinstance(value, str):
            raise ValueError("이름은 문자열이어야 합니다.")
        self._name = value

➤ 가장 많이 쓰이는 패턴

➤ 명확하고 직관적

➤ 하지만 다른 클래스에 그대로 재사용하기는 어려움


📌 descriptor 사용 (공통 속성 로직이 필요할 때)

class StringField:
    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise ValueError("문자열만 허용됩니다")
        instance.__dict__[self.name] = value

    def __set_name__(self, owner, name):  # 파이썬 3.6+
        self.name = name

class Person:
    name = StringField()
    nickname = StringField()

➤ StringField 하나로 여러 속성 재사용 가능

➤ 동적으로 속성 이름 바인딩도 가능 (__set_name__)

➤ 다소 복잡하지만 매우 유연


결론

  • @property: 간단하고 클래스 내부 전용. 메서드처럼 정의하지만 속성처럼 접근할 수 있음.
  • descriptor: 복잡하거나 공통된 속성 제어 로직이 필요한 경우. 전략 패턴처럼 활용 가능.

실제 프로젝트에서는 @property로 시작하다가, 반복되는 속성 제어 로직이 많아질 경우 descriptor로 refactor하는 경우도 많습니다.

원하시면 Django ORM에서 descriptor가 어떻게 사용되는지도 알려드릴까요?


Django ORM 에서의 Descriptor

Python의 descriptor는 Django ORM에서 핵심적인 역할을 하고 있습니다. 우리가 흔히 사용하는 models.CharField, models.ForeignKey 같은 필드들이 모두 descriptor 객체입니다.


Django ORM에서 descriptor 사용 예

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

위 코드에서 titleauthor는 그냥 일반 값처럼 보이지만, 사실은 Book 클래스의 클래스 속성으로 등록된 descriptor입니다.


무슨 일이 일어나는가?

book = Book.objects.get(pk=1)

book.title  # => 내부적으로 CharField의 __get__ 호출
book.author  # => ForeignKey의 __get__ 호출 → Author 객체 반환

✔ 작동 방식:

  1. title, author는 실제 데이터를 가지고 있지 않음
  2. 대신 CharField, ForeignKey 등의 하위 클래스가 __get__, __set__ 등을 오버라이드
  3. Django가 이 필드 객체를 model class에 descriptor로 등록

예: ForeignKeyDescriptor

Django는 ForeignKey 필드를 다음처럼 descriptor로 구현합니다 (django.db.models.fields.related_descriptors 참고):

class ForwardManyToOneDescriptor:
    def __get__(self, instance, cls=None):
        # 예: book.author -> Author 인스턴스를 DB에서 가져옴
        return related_instance_from_db()

    def __set__(self, instance, value):
        # 예: book.author = author_instance
        set_related_instance()

이 구조 덕분에 단순히 book.author만 접근해도 자동으로 관련된 Author 인스턴스를 DB에서 불러오는 게 가능해집니다.


정리

Django 요소내부 구현 방식
CharField, IntegerField, DateFieldField 클래스 → descriptor 구현
ForeignKey, ManyToManyFieldrelated descriptor로 구현
model.field 접근__get__ 호출 → 내부 DB 접근 또는 캐싱 처리
model.field = 값__set__ 호출 → DB 값 캐싱 또는 관계 연결

결론

Django에서 필드 정의는 단순 속성처럼 보이지만, 실제로는 descriptor 패턴을 이용해 복잡한 로직을 숨기고,

ORM 유저는 직관적인 속성 접근만으로도 DB와의 상호작용이 가능하도록 만들어져 있습니다.

필요하시면 CharFieldForeignKey의 내부 코드를 부분적으로 정리해서 보여드릴 수도 있어요. 원하시나요?

profile
To Infinity, and Beyond!

0개의 댓글