본문으로 건너뛰기
Advertisement

접근 제어

Python은 Java나 C++처럼 private, protected, public 키워드를 제공하지 않습니다. 대신 네이밍 컨벤션name mangling이라는 메커니즘으로 접근 제어를 구현합니다. Python의 철학은 "We're all consenting adults here(우리는 모두 동의하는 성인입니다)"입니다 — 개발자를 신뢰하고 관례를 따르도록 합니다.


Public: 기본 접근 수준

접두사 없이 선언된 속성과 메서드는 public입니다. 어디서든 자유롭게 접근할 수 있습니다.

class BankAccount:
def __init__(self, owner: str, balance: float):
self.owner = owner # public 속성
self.balance = balance # public 속성

def deposit(self, amount: float) -> None: # public 메서드
self.balance += amount

def get_info(self) -> str: # public 메서드
return f"{self.owner}: {self.balance}원"


acc = BankAccount("김철수", 100_000)
print(acc.owner) # 김철수 — 직접 접근 가능
print(acc.balance) # 100000 — 직접 접근 가능
acc.balance = 999 # 직접 수정도 가능 (의도치 않은 수정 위험!)
acc.deposit(50_000)
print(acc.get_info())

_protected: 관례적 비공개

단일 밑줄(_)로 시작하는 속성/메서드는 protected입니다. 실제로는 아무 제한이 없지만, "이 속성은 내부용이니 외부에서 직접 사용하지 마세요"라는 신호입니다.

class DataProcessor:
def __init__(self, data: list):
self.data = data # public
self._processed = False # protected — 내부 상태
self._cache: dict = {} # protected — 캐시

def process(self) -> list:
"""public API — 외부에서 사용"""
if self._is_already_processed():
return self._cache.get("result", [])
result = self._do_processing()
self._cache["result"] = result
self._processed = True
return result

def _do_processing(self) -> list:
"""protected — 내부 구현 세부사항"""
return [x * 2 for x in self.data]

def _is_already_processed(self) -> bool:
"""protected — 내부 헬퍼 메서드"""
return self._processed


dp = DataProcessor([1, 2, 3, 4, 5])
print(dp.process()) # [2, 4, 6, 8, 10]

# _로 시작하는 속성에도 접근은 가능하지만 "하지 않는 것이 관례"
print(dp._processed) # True — 가능하지만 권장하지 않음
dp._processed = False # 가능하지만 하지 말 것!

서브클래스에서의 _protected 활용

class Animal:
def __init__(self, name: str, sound: str):
self.name = name
self._sound = sound # 서브클래스에서 접근 가능한 protected

def _make_sound(self) -> str:
"""서브클래스가 오버라이드할 수 있는 protected 메서드"""
return self._sound

def speak(self) -> str:
return f"{self.name}: {self._make_sound()}"


class Dog(Animal):
def __init__(self, name: str):
super().__init__(name, "왈왈")

def _make_sound(self) -> str: # protected 메서드 오버라이드
return f"{self._sound}! {self._sound}!"


d = Dog("바둑이")
print(d.speak()) # 바둑이: 왈왈! 왈왈!

__private: Name Mangling

이중 밑줄(__)로 시작하는 속성/메서드는 Python이 name mangling을 적용하여 외부에서 직접 접근하기 어렵게 만듭니다. __attr_ClassName__attr로 이름이 변경됩니다.

class SecureAccount:
def __init__(self, owner: str, password: str, balance: float):
self.owner = owner # public
self.__password = password # private — name mangling 적용
self.__balance = balance # private

def verify_password(self, password: str) -> bool:
return self.__password == password

def get_balance(self, password: str) -> float:
if not self.verify_password(password):
raise PermissionError("비밀번호가 틀렸습니다.")
return self.__balance

def deposit(self, amount: float, password: str) -> None:
if not self.verify_password(password):
raise PermissionError("비밀번호가 틀렸습니다.")
self.__balance += amount


acc = SecureAccount("김철수", "secret123", 1_000_000)

# __password에 직접 접근 시도
try:
print(acc.__password) # AttributeError!
except AttributeError as e:
print(f"접근 불가: {e}")

# 실제로는 name mangling으로 저장됨
print(acc._SecureAccount__password) # secret123 (우회 가능하지만 하지 말 것)
print(acc._SecureAccount__balance) # 1000000

# 올바른 접근 방법
print(acc.get_balance("secret123")) # 1000000

# 인스턴스의 실제 속성 이름 확인
print(acc.__dict__)
# {'owner': '김철수', '_SecureAccount__password': 'secret123', '_SecureAccount__balance': 1000000}

Name Mangling 동작 원리

class Parent:
def __init__(self):
self.__private = "부모의 private" # _Parent__private
self._protected = "부모의 protected"

def parent_method(self) -> str:
return self.__private # 클래스 내부에서는 정상 접근


class Child(Parent):
def __init__(self):
super().__init__()
self.__private = "자식의 private" # _Child__private (다른 속성!)

def child_method(self) -> str:
return self.__private # 자식의 private에 접근


child = Child()
print(child.parent_method()) # 부모의 private
print(child.child_method()) # 자식의 private

# name mangling 결과 확인
print(child.__dict__)
# {'_Parent__private': '부모의 private',
# '_protected': '부모의 protected',
# '_Child__private': '자식의 private'}

# 핵심: __private은 상속 시 이름이 겹치지 않도록 보장함

Name Mangling이 필요한 경우

class Validator:
"""유효성 검사기 기반 클래스"""
def __init__(self):
self.__errors: list[str] = [] # 서브클래스가 실수로 덮어쓰지 못하도록

def _add_error(self, msg: str) -> None:
"""서브클래스에서 호출 가능한 protected 메서드"""
self.__errors.append(msg)

def is_valid(self) -> bool:
return len(self.__errors) == 0

def get_errors(self) -> list[str]:
return list(self.__errors)


class EmailValidator(Validator):
def validate(self, email: str) -> bool:
self.__errors = [] # 이건 _EmailValidator__errors — 별도 속성!
if "@" not in email:
self._add_error("@ 기호가 없습니다.")
if "." not in email.split("@")[-1]:
self._add_error("도메인이 유효하지 않습니다.")
return self.is_valid()


v = EmailValidator()
print(v.validate("invalid")) # False
print(v.get_errors()) # ['@ 기호가 없습니다.', '도메인이 유효하지 않습니다.']
print(v.validate("user@example.com")) # True

__slots__: 속성 제한 및 메모리 최적화

__slots__를 클래스에 정의하면 인스턴스에 허용되는 속성을 명시적으로 제한하고, __dict__ 대신 고정 크기 배열을 사용하여 메모리를 절약합니다.

import sys


class PointWithDict:
"""일반 클래스: __dict__ 사용"""
def __init__(self, x: float, y: float):
self.x = x
self.y = y


class PointWithSlots:
"""__slots__ 사용: __dict__ 없음"""
__slots__ = ("x", "y")

def __init__(self, x: float, y: float):
self.x = x
self.y = y


# 메모리 비교
p1 = PointWithDict(1.0, 2.0)
p2 = PointWithSlots(1.0, 2.0)

print(f"__dict__ 사용: {sys.getsizeof(p1.__dict__)} bytes (dict 오버헤드)")
print(f"__slots__ 사용: __dict__ 없음")

# __dict__ vs __slots__
print(hasattr(p1, "__dict__")) # True
print(hasattr(p2, "__dict__")) # False

# __slots__ 클래스는 정의된 속성만 허용
try:
p2.z = 3.0 # AttributeError!
except AttributeError as e:
print(f"오류: {e}")

# 올바른 접근
p2.x = 10.0
print(p2.x) # 10.0

대규모 객체에서 __slots__의 효과

import sys
import time


class RecordWithDict:
def __init__(self, id: int, name: str, value: float):
self.id = id
self.name = name
self.value = value


class RecordWithSlots:
__slots__ = ("id", "name", "value")

def __init__(self, id: int, name: str, value: float):
self.id = id
self.name = name
self.value = value


N = 100_000

# __dict__ 버전
start = time.perf_counter()
records_dict = [RecordWithDict(i, f"item_{i}", float(i)) for i in range(N)]
time_dict = time.perf_counter() - start
mem_dict = sum(sys.getsizeof(r) + sys.getsizeof(r.__dict__) for r in records_dict)

# __slots__ 버전
start = time.perf_counter()
records_slots = [RecordWithSlots(i, f"item_{i}", float(i)) for i in range(N)]
time_slots = time.perf_counter() - start
mem_slots = sum(sys.getsizeof(r) for r in records_slots)

print(f"__dict__ 생성 시간: {time_dict:.4f}s, 메모리: {mem_dict / 1024:.1f} KB")
print(f"__slots__ 생성 시간: {time_slots:.4f}s, 메모리: {mem_slots / 1024:.1f} KB")
print(f"메모리 절약: {(1 - mem_slots / mem_dict) * 100:.1f}%")

__slots__와 상속

class Base:
__slots__ = ("x", "y")

def __init__(self, x, y):
self.x = x
self.y = y


class Extended(Base):
__slots__ = ("z",) # 부모의 슬롯 + 자식의 슬롯

def __init__(self, x, y, z):
super().__init__(x, y)
self.z = z


e = Extended(1, 2, 3)
print(e.x, e.y, e.z) # 1 2 3

# 주의: 부모 클래스에 __slots__가 없으면 __dict__도 생성됨
class NoSlotParent:
pass # __dict__ 있음

class SlotChild(NoSlotParent):
__slots__ = ("value",)

obj = SlotChild()
obj.value = 1
obj.extra = "이건 __dict__에 저장됨" # 부모가 __dict__ 허용하므로 가능

Python의 접근 제어 설계 철학

Python 커뮤니티의 격언: "We're all consenting adults here"

# Python의 접근 제어는 강제가 아닌 신뢰
# 올바른 설계 원칙:

# 1. public API는 명확하게
class Config:
def __init__(self):
self.name = "app" # public: 외부에서 읽고 쓸 수 있음
self._internal = {} # protected: 내부용, 서브클래스 접근 가능
self.__secret = "xyz" # private: 이 클래스만 사용, 상속 시 이름 충돌 방지

# 2. __all__ 로 모듈 레벨 public API 명시
# (클래스가 아닌 모듈에서 from module import * 할 때 제어)


# 3. @property를 사용하여 제어된 접근 제공
class Temperature:
def __init__(self, celsius: float):
self._celsius = celsius # protected, @property로 제어된 접근 제공

@property
def celsius(self) -> float:
return self._celsius

@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("절대 영도 이하는 불가")
self._celsius = value


# 4. __slots__는 성능이 중요한 경우에만
# 일반적인 클래스에는 필요 없음

접근 수준 요약

표기이름실제 제한사용 목적
attrPublic없음외부 API
_attrProtected관례적 (실제 없음)내부용, 서브클래스 허용
__attrPrivateName mangling이름 충돌 방지, 강한 캡슐화
__attr__DunderPython 예약매직 메서드/속성

고수 팁

1. _ 하나로 임시 변수나 "무시" 표시

# 반복문에서 변수 무시
for _ in range(5):
print("Hello!")

# 언패킹에서 불필요한 값 무시
first, *_, last = [1, 2, 3, 4, 5]
print(first, last) # 1 5

# REPL에서 마지막 결과
# _ 는 REPL에서 마지막 표현식의 결과를 담음

2. __all__로 모듈의 public API 명시

# mymodule.py
__all__ = ["PublicClass", "public_function"]

class PublicClass:
pass

class _InternalClass: # from mymodule import * 시 제외됨
pass

def public_function():
pass

def _internal_function(): # 제외됨
pass

3. 접근 제어 패턴 선택 기준

# 언제 무엇을 쓸까?
class MyClass:
def __init__(self):
# 외부 API라면 → public
self.public_data = []

# 내부 구현이지만 서브클래스가 필요하다면 → _protected
self._internal_state = {}

# 서브클래스도 접근하면 안 되는 중요 데이터 → __private
self.__sensitive_data = "secret"

# 하지만 실제로는 @property + _protected 조합이 가장 Python스럽다
self._temperature = 0.0

@property
def temperature(self) -> float:
return self._temperature

@temperature.setter
def temperature(self, value: float) -> None:
# 유효성 검사 후 설정
if value < -273.15:
raise ValueError("유효하지 않은 온도")
self._temperature = value

정리

  • public: 접두사 없음, 외부 API
  • _protected: 관례적 비공개, 서브클래스에서 사용 가능
  • __private: name mangling 적용, 강한 캡슐화 및 이름 충돌 방지
  • __slots__: 허용 속성 제한 + 메모리 최적화, 대규모 인스턴스에 유용
  • Python의 철학: 강제 보다는 신뢰와 관례를 따른다
Advertisement