지난 편에서는 객체가 메모리에 태어나는 순간 자동으로 실행되는 생성자(__init__)와, 각 객체가 독립적으로 소유하는 '인스턴스 변수'의 개념을 다루었습니다. 인스턴스 변수 덕분에 수백 개의 객체를 만들어도 서로의 데이터가 섞이지 않고 독립적인 상태를 유지할 수 있었습니다.
하지만 파이썬 클래스 내부를 다루다 보면 인스턴스 변수 외에 또 다른 형태의 변수를 만나게 됩니다. 바로 '클래스 변수(Class Variable)'입니다. 이 두 변수의 차이점과 메모리 동작 원리를 명확히 구분하지 못하면, 특정 객체의 값을 바꿨는데 다른 모든 객체의 값까지 동시에 바뀌어버리는 황당한 치명적 버그를 마주하게 됩니다.
저 역시 초보 시절, 게임 캐릭터의 총 점수나 전체 회원 수를 집계하는 로직을 만들 때 변수 선언 위치를 잘못 잡아서 모든 플레이어의 스코어가 동기화되는 아찔한 실수를 저지른 적이 있습니다. 오늘은 모든 객체가 공유하는 메모리 공간인 클래스 변수와, 독립성을 갖는 인스턴스 변수의 차이점을 완벽하게 정복해 보겠습니다.
1. 클래스 변수와 인스턴스 변수의 핵심 차이
두 변수를 구별하는 가장 쉬운 기준은 "누가 소유하고 있으며, 메모리를 공유하는가?"입니다.
인스턴스 변수 (
self.변수명):__init__등 메서드 내부에서self.을 붙여 선언합니다. 객체(인스턴스)가 생성될 때마다 각 객체의 메모리 공간에 독립적으로 생성됩니다.클래스 변수 (
변수명): 클래스 블록 바로 아래, 메서드 외부에서 선언합니다. 해당 클래스로부터 태어난 모든 객체가 단 하나의 메모리 공간을 '공유'합니다.
즉, 개별 회원마다 달라야 하는 이름, 이메일, 잔여 포인트 등은 '인스턴스 변수'로 관리해야 하고, 회사명, 서비스 이자율, 현재 서비스에 가입한 총 회원 수 등은 '클래스 변수'로 관리해야 합니다.
2. 실전 코딩: 클래스 변수로 전체 회원 수 자동 집계하기
클래스 변수를 활용하여 클래스로부터 객체가 생성될 때마다 전체 회원 수가 1씩 증가하는 카운팅 시스템을 구현해 보겠습니다.
class Member:
# 1. 클래스 변수 선언 (모든 객체가 이 하나의 변수를 공유함)
total_member_count = 0
def __init__(self, name):
# 인스턴스 변수 선언 (개별 객체 고유 데이터)
self.name = name
# 객체가 생성될 때마다 공유 데이터인 클래스 변수를 1 증가
Member.total_member_count += 1
print(f"[가입 알림] {self.name}님 가입 완료 (현재 총 회원 수: {Member.total_member_count}명)")
# 객체 생성 테스트
user1 = Member("김파이")
user2 = Member("이객체")
user3 = Member("박클래스")
# 클래스 변수 접근 (클래스 이름 또는 객체를 통해 접근 가능)
print(f"최종 집계 회원 수 (클래스로 접근): {Member.total_member_count}명")
print(f"최종 집계 회원 수 (user1 객체로 접근): {user1.total_member_count}명")
위 코드를 실행하면 user1, user2, user3이 각각 만들어질 때마다 Member.total_member_count의 값이 1, 2, 3으로 누적 증가합니다. user1.total_member_count를 출력해도 똑같이 3이 출력되는데, 이는 user1이 자신에게 없는 변수명을 찾을 때 상위 클래스 공간으로 올라가 공유된 클래스 변수를 참조하기 때문입니다.
3. 입문자가 가장 많이 빠지는 치명적인 함정: 값 변경의 오해
클래스 변수를 다룰 때 가장 주의해야 할 점은 "클래스 변수의 값을 수정하려고 할 때 어떻게 접근하느냐"입니다.
class Service:
# 클래스 변수
domain = "https://mywebsite.com"
s1 = Service()
s2 = Service()
# [실수 상황] s1 객체를 통해 클래스 변수 값을 변경하려고 시도함
s1.domain = "https://newsite.com"
print(s1.domain) # 출력: https://newsite.com
print(s2.domain) # 출력: https://mywebsite.com (변경되지 않음!)
print(Service.domain) # 출력: https://mywebsite.com (원본 유지!)
왜 s2와 Service의 domain은 바뀌지 않았을까요?
파이썬에서 s1.domain = "값"을 실행하는 순간, 파이썬은 공유 중인 클래스 변수 domain을 수정하는 것이 아니라, s1 객체 전용 메모리 공간에 똑같은 이름을 가진 새로운 '인스턴스 변수 domain'을 새로 만들어 덮어씌워 버립니다.
이로 인해 s1은 이제 원본 클래스 변수를 바라보지 않고 자신이 새로 만든 인스턴스 변수를 바라보게 되며, s2와 Service는 여전히 원본 클래스 변수를 바라보는 데이터 불일치 현상이 발생합니다.
따라서 클래스 변수의 값을 공통으로 수정하고 싶을 때는 반드시 객체명.변수가 아닌 클래스명.변수 (예: Service.domain = "[https://newsite.com](https://newsite.com)") 형태로 직접 수정해야만 모든 객체에 안전하게 변경 사항이 반영됩니다.
4. 메모리 탐색 순서: 네임스페이스(Namespace)의 원리
파이썬이 특정 변수의 값을 찾을 때 거치는 탐색 순서는 다음과 같습니다.
인스턴스 네임스페이스: 먼저 객체 자기 자신의 메모리 공간에 해당 변수(
self.변수)가 있는지 확인합니다.클래스 네임스페이스: 자기 자신에게 없다면, 자신을 만든 클래스 메모리 공간으로 올라가 변수를 찾습니다.
전역(Global) 네임스페이스: 클래스에도 없다면 파일 전체 전역 변수에서 찾습니다.
이처럼 하위에서 상위로 올라가는 네임스페이스 탐색 규칙을 이해하고 있다면, 인스턴스 변수와 클래스 변수의 이름이 같을 때 발생하는 예기치 못한 재정의(Shadowing) 버그를 완벽하게 예방할 수 있습니다.
핵심요약
인스턴스 변수(
self.변수)는 각 객체가 독립 소유하는 고유 데이터 공간이며, 클래스 변수는 클래스로부터 생성된 모든 객체가 단 하나의 공간을 공유하는 데이터입니다.클래스 변수 값을 전체 변경하고자 할 때는
객체.변수 = 값이 아닌 반드시클래스명.변수 = 값형태로 수정해야 의도치 않은 인스턴스 변수 생성을 막을 수 있습니다.파이썬은 변수를 찾을 때 인스턴스 메모리 -> 클래스 메모리 -> 전역 메모리 순서(네임스페이스)로 올라가며 탐색합니다.


0 댓글