앞선 글에서는 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의 기본 객체처럼 동작하도록 만들 수 있습니다.