Python property 활용하기: 클래스 속성을 안전하게 관리하는 방법

Ad Placeholder

앞선 글에서는 Python 객체의 속성을 관리하는 방법으로 __dict__와 __slots__를 살펴봤습니다.

이번에는 클래스의 속성을 조금 더 세밀하게 관리하는 방법을 알아보겠습니다.

일반적으로 클래스의 속성은 다음과 같이 직접 접근할 수 있습니다.

class Person:

    def __init__(self, age):
        self.age = age

그리고 객체를 만든 후에는:

person = Person(20)

print(person.age)

person.age = 30

처럼 사용할 수 있습니다.

그런데 만약 나이에 음수가 들어가면 어떻게 될까요?

person.age = -10

Python은 기본적으로 이 값이 올바른지 검사하지 않습니다.

이처럼 속성에 값을 넣거나 읽을 때 추가적인 로직을 실행하고 싶을 때 사용할 수 있는 기능이 property입니다.

property란?

property는 클래스의 메서드를 일반적인 속성처럼 사용할 수 있도록 만들어주는 기능입니다.

간단한 예제를 먼저 살펴보겠습니다.

class Person:

    def __init__(self, age):
        self._age = age

    @property
    def age(self):
        return self._age

객체를 만들어보겠습니다.

person = Person(20)

그리고 age()라는 메서드를 호출하는 것이 아니라:

print(person.age)

처럼 사용할 수 있습니다.

결과:

20

코드만 보면 age는 변수처럼 보이지만 실제로는 age() 메서드가 실행된 것입니다.

이것이 property의 가장 기본적인 특징입니다.

왜 property를 사용하는가?

다음 두 코드를 비교해보겠습니다.

일반적인 속성:

class Person:

    def __init__(self, age):
        self.age = age

이 경우에는 다음과 같이 어떤 값이든 넣을 수 있습니다.

person.age = -100

반면 property를 사용하면 속성에 값을 넣거나 가져오는 과정에 원하는 로직을 추가할 수 있습니다.

예를 들어 나이는 음수가 될 수 없도록 만들 수 있습니다.

class Person:

    def __init__(self, age):
        self._age = age

    @property
    def age(self):
        return self._age

여기에 setter를 추가해보겠습니다.

@property와 getter

먼저 @property가 붙은 메서드를 살펴보겠습니다.

class Person:

    def __init__(self, age):
        self._age = age

    @property
    def age(self):
        return self._age

다음 코드에서:

person.age

를 실행하면 실제로는 다음 메서드가 호출됩니다.

def age(self):
    return self._age

즉 @property는 메서드를 속성처럼 사용할 수 있게 해줍니다.

이 메서드는 일반적으로 getter 역할을 합니다.

person.age
    ↓
@property
    ↓
age()
    ↓
self._age 반환

왜 _age를 사용할까?

예제에서 실제 값을 저장하는 변수는 age가 아니라 _age입니다.

self._age = age

그리고 외부에서는:

person.age

를 사용합니다.

이렇게 하는 이유는 age라는 이름을 property의 인터페이스로 사용하면서 실제 데이터는 _age에 저장하기 위해서입니다.

즉 다음과 같은 구조입니다.

외부 코드
   ↓
person.age
   ↓
@property
   ↓
self._age

여기서 _age의 앞에 붙은 _는 Python에서 흔히 내부적으로 사용하는 속성이라는 의미를 나타내는 관례입니다.

강제적인 접근 제한을 의미하는 것은 아닙니다.

setter란?

이번에는 값을 변경하는 경우를 생각해보겠습니다.

다음과 같이 @age.setter를 사용할 수 있습니다.

class Person:

    def __init__(self, age):
        self._age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        self._age = value

이제 다음과 같이 사용할 수 있습니다.

person = Person(20)

print(person.age)

person.age = 30

print(person.age)

결과:

20
30

여기서:

person.age = 30

을 실행하면 단순히 변수에 값을 대입하는 것이 아니라 setter가 호출됩니다.

개념적으로:

person.age = 30
        ↓
@age.setter
        ↓
age(self, 30)
        ↓
self._age = 30

이렇게 동작한다고 이해하면 됩니다.

property를 이용해 값 검증하기

property의 가장 유용한 활용 방법 중 하나가 값 검증입니다.

나이가 음수가 되지 않도록 만들어보겠습니다.

class Person:

    def __init__(self, age):
        self.age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("나이는 0보다 작을 수 없습니다.")

        self._age = value

이제 정상적인 값을 넣으면:

person = Person(20)

person.age = 30

print(person.age)

정상적으로 동작합니다.

하지만:

person.age = -10

을 실행하면 ValueError가 발생합니다.

즉 객체의 내부 상태를 무조건 변경하도록 두지 않고, 값이 조건을 만족하는지 검사한 뒤 저장할 수 있습니다.

생성할 때도 setter가 호출될 수 있다

앞의 예제에서 __init__()을 다시 살펴보겠습니다.

def __init__(self, age):
    self.age = age

여기서는:

self._age = age

가 아니라:

self.age = age

를 사용했습니다.

그러면 객체를 생성할 때도 setter가 실행됩니다.

person = Person(-10)

이 경우에도 @age.setter의 검증 코드가 실행되기 때문에 잘못된 값이 저장되는 것을 막을 수 있습니다.

이런 방식으로 작성하면 객체가 생성되는 순간부터 데이터의 조건을 검사할 수 있습니다.

실전 예제: 온도 관리하기

이번에는 조금 더 실용적인 예제를 만들어보겠습니다.

온도는 특정 범위에서만 사용할 수 있다고 가정하겠습니다.

class Temperature:

    def __init__(self, value):
        self.value = value

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, value):
        if value < -273.15:
            raise ValueError("절대온도 0K보다 낮을 수 없습니다.")

        self._value = value

사용해보겠습니다.

temperature = Temperature(25)

print(temperature.value)

결과:

25

잘못된 값을 입력하면:

temperature.value = -300

오류가 발생합니다.

이처럼 property를 사용하면 데이터를 저장하는 것뿐만 아니라 객체의 상태가 올바른지 검사하는 로직까지 함께 관리할 수 있습니다.

읽기 전용 property 만들기

property는 값을 읽을 수만 있도록 만드는 데도 사용할 수 있습니다.

예를 들어 사용자의 이름을 외부에서 마음대로 변경하지 못하게 하고 싶다고 가정해보겠습니다.

class User:

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

    @property
    def name(self):
        return self._name

이제:

user = User("철수")

print(user.name)

은 정상적으로 동작합니다.

하지만:

user.name = "영희"

처럼 값을 변경하려고 하면 setter가 정의되어 있지 않기 때문에 오류가 발생합니다.

즉 getter만 만들면 읽기 전용 property로 사용할 수 있습니다.

@property
    ↓
읽기 가능

@name.setter
    ↓
쓰기 가능

setter가 없다면 외부에서 해당 property에 값을 직접 대입할 수 없습니다.

property로 계산된 값 만들기

property는 단순히 저장된 값을 반환하는 데만 사용할 필요는 없습니다.

다른 데이터를 이용해 계산한 값을 반환할 수도 있습니다.

예를 들어 원의 반지름을 저장하고 넓이를 계산해보겠습니다.

import math


class Circle:

    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return math.pi * self.radius ** 2

사용할 때는:

circle = Circle(10)

print(circle.area)

결과는 원의 넓이가 됩니다.

여기서 area는 실제로 값을 저장하고 있는 변수가 아닙니다.

radius를 이용해 그때그때 계산한 결과를 반환합니다.

그런데 사용하는 입장에서는:

circle.area

처럼 일반적인 속성을 읽는 것처럼 사용할 수 있습니다.

이런 방식이 property의 장점 중 하나입니다.

일반 메서드와 property 비교

같은 기능을 일반 메서드로 만들면 다음과 같습니다.

class Circle:

    def __init__(self, radius):
        self.radius = radius

    def get_area(self):
        return 3.14 * self.radius ** 2

사용할 때:

circle.get_area()

가 됩니다.

반면 property를 사용하면:

@property
def area(self):
    return 3.14 * self.radius ** 2

사용할 때:

circle.area

처럼 사용할 수 있습니다.

즉 호출해야 하는 동작이라기보다 객체의 속성처럼 의미가 전달되는 값이라면 property를 사용하면 코드가 자연스러워질 수 있습니다.

property와 캡슐화

property를 설명할 때 캡슐화(Encapsulation)라는 개념도 함께 알아두면 좋습니다.

캡슐화는 객체 내부의 데이터를 외부에서 무분별하게 변경하지 않고, 필요한 인터페이스를 통해 접근하도록 구성하는 개념입니다.

예를 들어 다음과 같이 단순하게 작성하면:

class BankAccount:

    def __init__(self, balance):
        self.balance = balance

외부에서 다음과 같은 값을 넣을 수 있습니다.

account.balance = -100000

물론 실제 은행 계좌라면 잔액에 대한 조건을 확인해야 할 것입니다.

property를 사용하면 이런 조건을 코드 안에 넣을 수 있습니다.

class BankAccount:

    def __init__(self, balance):
        self.balance = balance

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("잔액은 음수가 될 수 없습니다.")

        self._balance = value

이제 외부에서는:

account.balance = 10000

처럼 일반적인 속성처럼 사용하지만, 내부에서는 setter를 통해 조건을 검사합니다.

property를 사용할 때 주의할 점

property가 편리하다고 해서 모든 속성에 사용할 필요는 없습니다.

단순한 데이터를 저장하고 특별한 검증이나 계산이 필요하지 않다면 일반적인 인스턴스 변수로 충분합니다.

class Person:

    def __init__(self, name):
        self.name = name

이런 경우 굳이 property를 추가할 필요는 없습니다.

반면 다음과 같은 상황이라면 property를 고려할 수 있습니다.

  • 값을 가져올 때 계산이 필요하다.
  • 값을 변경할 때 검증이 필요하다.
  • 외부에서 읽기만 가능하도록 만들고 싶다.
  • 속성에 접근할 때 추가적인 로직이 필요하다.
  • 기존 속성 인터페이스를 유지하면서 내부 동작을 변경하고 싶다.

property의 기본 구조 정리

가장 기본적인 형태는 다음과 같습니다.

class Person:

    def __init__(self, age):
        self._age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        self._age = value

각 부분의 역할을 정리하면:

@property
    ↓
값을 읽을 때 실행

@age.setter
    ↓
값을 변경할 때 실행

사용할 때는 일반적인 변수처럼:

person.age

또는:

person.age = 30

처럼 사용할 수 있습니다.

하지만 실제로는 각각 getter와 setter가 실행됩니다.

마무리

Python의 property는 메서드를 일반적인 속성처럼 사용할 수 있게 해주는 기능입니다.

특히 setter와 함께 사용하면 객체의 속성에 값을 저장하기 전에 검증 로직을 넣을 수 있기 때문에 유용합니다.

핵심을 정리하면 다음과 같습니다.

  • @property는 메서드를 속성처럼 사용할 수 있게 해준다.
  • @property가 붙은 메서드는 getter 역할을 한다.
  • @변수.setter를 사용하면 값을 변경할 때 실행할 로직을 만들 수 있다.
  • setter를 만들지 않으면 읽기 전용 property로 사용할 수 있다.
  • property 안에서 계산된 값을 반환할 수도 있다.
  • 값 검증이나 캡슐화가 필요한 경우 유용하다.
  • 단순한 데이터라면 굳이 property를 사용할 필요는 없다.

특히 다음 형태는 Python 클래스에서 자주 볼 수 있으므로 익숙해지는 것이 좋습니다.

@property
def value(self):
    return self._value

@value.setter
def value(self, new_value):
    self._value = new_value

다음 글에서는 클래스에서 제공하는 특별한 메서드인 Special Method(특수 메서드)를 살펴보겠습니다. __init__()뿐만 아니라 __str__(), __len__(), __add__() 같은 메서드를 이용하면 직접 만든 객체도 Python의 기본 객체처럼 동작하도록 만들 수 있습니다.

jiji

AI, Python, 생활정보와 정부 정책 정보를 쉽게 설명하는 정보 블로그입니다.

Ad Placeholder
Scroll to Top