지난 편에서는 함수나 메서드의 원본 코드를 손대지 않고 로깅이나 시간 측정 같은 공통 부가 기능을 깔끔하게 덧붙이는 데코레이터(@decorator)의 원리와 활용법을 배웠습니다.

앞서 5편 캡슐화(Encapsulation)를 다룰 때, 객체 내부의 비공개 속성(__)에 접근하거나 값을 변경하기 위해 get_age(), set_age() 같은 전용 메서드를 직접 만들어 사용했던 기억이 있으실 겁니다. 데이터를 보호하는 목적은 달성했지만, Java나 C++ 스타일의 Getter/Setter 메서드는 문법이 다소 장황해집니다. 속성 하나를 읽고 쓸 때마다 user.set_age(user.get_age() + 1)처럼 괄호를 덧붙여 호출해야 하므로 파이썬 특유의 간결함과 가독성을 떨어뜨리는 단점이 있습니다.

저 역시 초보 시절, 데이터를 보호하고 싶은 맘에 모든 변수에 get_, set_ 메서드를 남발했다가 코드가 지저분해지고 "그냥 user.age += 1처럼 마침표 하나로 깔끔하게 접근할 수 없을까?"라는 고민을 깊게 한 적이 있습니다. 오늘은 데이터 보호와 검증 로직은 유지하면서, 외부에서는 일반 변수처럼 마침표로 편하게 다룰 수 있게 해주는 프로퍼티(@property) 활용법을 정리해 드립니다.


1. Java 스타일 Getter/Setter의 한계와 파이썬의 철학

파이썬이 아닌 다른 객체지향 언어에서는 속성을 직접 노출하는 것을 금기시하며 항상 Getter와 Setter 메서드를 먼저 만드는 관습이 있습니다.

Python
# 기존 방식: 메서드를 통한 접근 (문법이 길어짐)
class Student:
    def __init__(self, age):
        self.__age = age

    def get_age(self):
        return self.__age

    def set_age(self, age):
        if age < 0:
            raise ValueError("나이는 음수가 될 수 없습니다.")
        self.__age = age

s = Student(20)
s.set_age(s.get_age() + 1)  # 읽고 쓰기가 매우 번거롭고 직관적이지 않음

파이썬의 핵심 철학 중 하나는 "단순한 것이 복잡한 것보다 낫다(Simple is better than complex)"입니다. 처음에는 공개 속성(self.age)으로 가볍게 시작했다가, 나중에 나이 검증 로직이 필요해졌을 때 기존에 작성된 외부 코드를 단 한 줄도 수정하지 않고 메서드로 전환할 수 있어야 진정한 파이썬다운(Pythonic) 코드입니다.

이 전환을 가능하게 만들어주는 도구가 바로 내장 데코레이터인 @property입니다.


2. @property를 활용한 깔끔한 속성 제어 3단계

@property 데코레이터를 사용하면 메서드를 정의하되, 외부에서는 괄호() 없이 일반 인스턴스 변수처럼 마침표(.)로 접근하게 만들 수 있습니다.

Python
class Student:
    def __init__(self, age):
        # 생성자에서도 setter를 거치도록 지정
        self.age = age

    # 1. Getter 역할 (@property)
    @property
    def age(self):
        return self.__age

    # 2. Setter 역할 (@변수명.setter)
    @age.setter
    def age(self, value):
        if value < 0:
            print("[오류] 나이는 음수가 될 수 없습니다. 0으로 설정합니다.")
            self.__age = 0
        else:
            self.__age = value

# 객체 사용 테스트
s = Student(20)

# 외부에서는 메서드가 아닌 일반 변수처럼 읽기 가능 (괄호 없음)
print(f"현재 학생 나이: {s.age}")  # 출력: 현재 학생 나이: 20

# 외부에서 일반 변수처럼 값 할당 가능 (Setter 메서드가 자동 호출됨)
s.age = 25
print(f"수정된 나이: {s.age}")       # 출력: 수정된 나이: 25

# 잘못된 값 입력 시 검증 로직 작동
s.age = -5                         # 출력: [오류] 나이는 음수가 될 수 없습니다...
print(f"검증 후 나이: {s.age}")     # 출력: 검증 후 나이: 0

s.age = 25를 실행하는 순간, 파이썬 뒤편에서는 @age.setter가 붙은 메서드를 찾아 25라는 값을 인자로 넘겨주며 자동 실행합니다. 이로써 외부 문법의 깔끔함과 내부 검증 로직의 안전성을 동시에 확보하게 됩니다.


3. 읽기 전용(Read-Only) 속성 및 계산된 속성 만들기

@property가 제공하는 강력한 이점 중 하나는 'Setter를 작성하지 않음으로써 손쉽게 수정 불가능한 읽기 전용 속성을 만드는 것'입니다.

또한, 별도의 인스턴스 변수를 따로 저장하지 않고 메서드가 실시간으로 값을 계산해 주는 '계산된 속성(Computed Property)'을 구현할 때도 최적입니다.

Python
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    # 면적을 실시간으로 계산하는 읽기 전용 프로퍼티
    @property
    def area(self):
        return self.width * self.height

rect = Rectangle(10, 20)
print(f"직사각형 넓이: {rect.area}")  # 출력: 200

# 가로 길이를 수정하면 넓이도 자동 반영
rect.width = 15
print(f"수정된 넓이: {rect.area}")    # 출력: 300

# 읽기 전용 속성에 값을 쓰려고 시도하면 에러 발생!
# rect.area = 500  
# AttributeError: can't set attribute 에러 발생하며 데이터 보호

area는 width와 height를 바탕으로 계산되는 값이므로 별도의 변수로 들고 있으면 값이 꼬일 위험이 있습니다. @property로 만들어두면 괄호 없이 변수처럼 조회되면서도, 함부로 수정할 수 없는 안전한 읽기 전용 속성이 완성됩니다.


4. 프로퍼티 활용 시 주의사항: 재귀 호출(Infinite Recursion)의 함정

@property를 처음 작성할 때 가장 흔하게 범하는 실수가 있습니다. 바로 Getter나 Setter 내부에서 프로퍼티 자기 자신의 이름과 똑같은 변수에 접근하여 무한 루프에 빠지는 현상입니다.

Python
# [잘못된 작성 예시] 무한 재귀 호출로 인해 튕김
class BadExample:
    def __init__(self, name):
        self.name = name

    @property
    def name(self):
        return self.name  # self.name을 부르는 순간 다시 @property name이 작동하여 무한 루프!

    @name.setter
    def name(self, value):
        self.name = value # self.name = value를 실행하는 순간 다시 setter를 불러 무한 루프!

이 코드를 실행하면 RecursionError: maximum recursion depth exceeded 에러가 터지며 프로그램이 다운됩니다.

따라서 프로퍼티 내부에서 실제 데이터를 저장할 때에는 반드시 self.__name이나 self._name 처럼 언더바가 붙은 실제 저장용 변수 이름을 따로 지정하여 연결해주어야 합니다.


핵심요약

  • @property는 장황한 Getter/Setter 메서드 호출 방식 대신, 마침표(.) 하나로 변수처럼 직관적으로 접근하게 해주는 파이썬 전용 데코레이터입니다.

  • @property만 정의하고 @변수명.setter를 정의하지 않으면 외부에서 값을 임의로 수정할 수 없는 읽기 전용(Read-Only) 속성을 쉽게 구축할 수 있습니다.

  • 프로퍼티 내부에서 실제 데이터를 다룰 때는 무한 재귀 호출(RecursionError)을 방지하기 위해 반드시 self._속성명 또는 self.__속성명 형태의 비공개 변수를 활용해야 합니다.