04.1 감추고 통로로만 다룬다
캡슐화(encapsulation, 객체의 내부 데이터를 감추고 정해진 통로로만 접근하게 하는 것)는 객체지향의 안전장치다. 잔액이든 나이든 온도든, 아무 값이나 함부로 들어오면 객체가 말이 안 되는 상태가 된다. 그래서 내부 데이터를 밖에서 직접 못 건드리게 하고, 메서드나 프로퍼티라는 정해진 통로로만 읽고 쓰게 만든다.
파이썬은 다른 언어처럼 접근을 강하게 막지 않는 대신 두 가지 장치를 쓴다. 하나는 밑줄 관례로, 이름 앞 밑줄 하나 _x는 내부용이니 건드리지 말라는 약속이다. 다른 하나는 프로퍼티(property, 겉보기엔 속성 접근이지만 실제로는 함수가 돌아 검증을 걸 수 있는 장치)다. @property를 쓰면 값처럼 읽고 쓰는데 그 안에서 함수가 검사를 한다. 순서대로 본다.
04.2 검증 없음과 프로퍼티
온도 객체의 섭씨 값에 절대영도인 영하 273.15도보다 낮은 값을 넣으면 물리적으로 말이 안 된다. 검증 없는 코드와 프로퍼티로 검증을 건 코드를 비교한다.
# before: 검증 없음, 아무 값이나 들어감
class ThermoBad:
def __init__(self):
self.celsius = 25
tb = ThermoBad()
tb.celsius = -9999 # 불가능한 값도 그냥 허용
print("검증 없음:", tb.celsius)
# after: property의 setter로 검증
class Thermo:
def __init__(self):
self._celsius = 25
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("절대영도 미만 불가")
self._celsius = value
th = Thermo()
th.celsius = 30 # setter가 검증 후 저장
print("검증 통과:", th.celsius)
try:
th.celsius = -9999
except ValueError as e:
print("검증 실패:", e)
출력은 다음과 같다.
검증 없음: -9999
검증 통과: 30
검증 실패: 절대영도 미만 불가
th.celsius = 30은 그냥 대입처럼 보이지만, 실제로는 @celsius.setter가 붙은 함수가 돌아 값을 검사한 뒤 저장한다. 그래서 영하 9999도를 넣으려 하면 ValueError(값 오류)로 막힌다. 검증 로직이 Thermo 한 곳에 모여 있으니, 온도를 다루는 모든 코드가 자동으로 보호된다. 잘못된 값이 객체에 들어오는 것을 통로 입구에서 원천 차단하는 것이다.
04.3 자바식 게터/세터와 파이썬 프로퍼티
나이에 음수를 못 넣게 막고 싶다. 다른 언어 습관대로 get_age와 set_age 같은 메서드를 만들 수도 있지만, 파이썬에서는 프로퍼티가 더 자연스럽다. 두 방식을 나란히 둔다.
# before: 게터/세터 흉내(비파이썬적)
class PersonJava:
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
pj = PersonJava(20)
pj.set_age(21)
print("게터/세터식:", pj.get_age())
# after: 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("음수 나이 불가")
self._age = value
pp = Person(20)
pp.age = 21 # 함수 호출처럼 안 보이지만 setter 실행
print("파이썬식:", pp.age)
두 방식 모두 21을 낸다.
게터/세터식: 21
파이썬식: 21
결과는 같지만 더 중요한 장점이 프로퍼티 쪽에 있다. 파이썬에서는 처음에 그냥 공개 속성으로 두다가, 나중에 검증이 필요해지면 같은 이름의 프로퍼티로 바꾸면 된다. 이때 pp.age = 21처럼 사용하는 쪽 코드는 한 글자도 안 고쳐도 된다. 게터/세터를 미리 깔아둘 필요 없이, 필요해진 순간 자연스럽게 검증을 끼워 넣는 이 유연함이 파이썬다운 방식이다.
04.4 읽기 전용 프로퍼티
성과 이름을 합친 전체 이름은 따로 저장할 필요 없이 그때그때 계산하면 된다. setter를 안 만들면 그 프로퍼티는 읽기 전용이 된다.
class Employee:
def __init__(self, first, last):
self.first = first
self.last = last
@property
def full_name(self):
return f"{self.first} {self.last}"
e = Employee("길동", "홍")
print(e.full_name)
try:
e.full_name = "임꺽정" # setter가 없음
except AttributeError as e2:
print("AttributeError:", e2)
읽기는 되고 쓰기는 막힌다.
길동 홍
AttributeError: property 'full_name' of 'Employee' object has no setter
getter만 있고 setter가 없는 프로퍼티는 읽기 전용이다. e.full_name = ...으로 덮어쓰려 하면 AttributeError(속성 오류)가 난다. 이 방식의 이득은 데이터가 어긋날 수 없다는 것이다. full_name을 따로 저장하지 않으니 first가 바뀌면 full_name도 자동으로 최신 값을 낸다. 저장된 이름과 계산된 이름이 서로 안 맞는 사고가 원천적으로 안 생긴다.
04.5 이름 맹글링
밑줄 하나 _x가 약한 약속이라면, 밑줄 두 개로 시작하는 이름은 조금 더 강한 장치다. 파이썬이 이름을 자동으로 변형(맹글링)해서 밖에서 실수로 접근하기 어렵게 만든다.
class Safe:
def __init__(self):
self.__secret = 42 # 내부적으로 _Safe__secret 으로 변형됨
def reveal(self):
return self.__secret
s = Safe()
print("메서드 통해:", s.reveal())
try:
print(s.__secret)
except AttributeError as e:
print("직접 접근 실패:", e)
print("맹글링된 실제 이름:", s._Safe__secret)
출력은 다음과 같다.
메서드 통해: 42
직접 접근 실패: 'Safe' object has no attribute '__secret'
맹글링된 실제 이름: 42
__secret은 파이썬이 _Safe__secret으로 자동 변형한다. 그래서 s.__secret으로는 못 찾아 AttributeError가 난다. 다만 완전한 은폐는 아니다. 변형된 실제 이름을 알면 여전히 접근된다. 진짜 목적은 값을 숨기는 것이 아니라, 자식 클래스가 우연히 같은 이름을 써서 부모 값을 덮어쓰는 사고를 막는 것이다. 밑줄 하나 _x는 단순 관례, 밑줄 둘 __x는 맹글링이라는 차이를 기억한다.
04.6 정리
캡슐화는 객체의 내부를 감추고 정해진 통로로만 다루게 하는 것이다. 파이썬은 강제 대신 관례와 프로퍼티를 쓴다. 밑줄 하나는 내부용이라는 약속, @property는 속성처럼 쓰되 함수가 돌아 검증을 거는 장치다. setter를 두면 잘못된 값을 막고, setter를 빼면 읽기 전용이 된다. 핵심은 하나다. 객체가 늘 올바른 상태를 유지하도록, 값이 드나드는 길목을 클래스가 스스로 지키게 하는 것이다.