개발 · 자동화핵심 · 23분 읽기

Python 클래스·인스턴스와 객체 설계

클래스는 관련 상태와 동작을 묶는 방법입니다. 인스턴스별 상태와 공유 상태를 구분하고 책임이 분명한 객체를 만듭니다.

개념부터 찾아보기

정의·역할·동작 원리를 독립 페이지에서 확인하세요. 본문의 밑줄 용어에도 설명이 있습니다.

이 노트에서 설명할 수 있게 되는 것

  1. 클래스 속성과 인스턴스 속성의 저장 위치를 구분한다.
  2. 속성 조회·메서드 호출·self의 연결을 설명한다.
  3. 공유 상태와 합성 구조를 통해 객체 간 영향을 추적한다.

01class 문도 실행되어 클래스 객체를 만든다

  • 의 문은 내부 이름 공간에서 정의를 실행하고 클래스 객체를 만든다.
  • 클래스 자체도 객체이므로 변수에 넣거나 다른 함수로 전달할 수 있다.
  • 인스턴스는 그 클래스를 바탕으로 만들어진 개별 객체다.
  • 객체와 인스턴스를 주소를 저장한 껍데기와 실제 메모리 공간이라는 별개의 두 물건으로 반드시 나누어 설명하는 것은 의 일반 용어와 맞지 않는다.
  • 클래스를 호출하면 생성 과정이 시작된다.
  • __new__가 객체 생성에 관여하고, 일반적인 경우 새 인스턴스에 대해 __init__가 초기화를 수행한다.
  • __init__는 다른 반환 객체를 만들어 내는 공장 함수가 아니라 초기 상태를 설정하는 메서드이며 None을 반환해야 한다.
  • 각 인스턴스에 필요한 상태는 .name처럼 해당 인스턴스에 저장한다.

02self와 bound method를 실제 호출로 읽기

  • 클래스 안의 일반 함수를 인스턴스에서 가져오면 인스턴스와 함수가 연결된 가 된다.
  • device.label()은 일반적인 경우 Device.label(device)와 같은 인자 관계를 갖는다.
  • 는 예약 키워드가 아니라 첫 매개변수의 관례적 이름이며 메서드가 자신을 호출한 인스턴스에 접근할 통로다. 를 빼고 인자 없는 함수처럼 정의하면 호출할 때 자동 전달된 인스턴스를 받을 수 없어 오류가 날 수 있다.
  • 아래 두 Device는 클래스 정의와 메서드는 공유하지만 각기 다른 name 속성을 가진다.
  • a.label을 변수에 저장해 나중에 호출해도 a에 연결된 메서드라는 관계가 유지된다.
  • 클래스의 기능과 개별 객체 상태를 함께 묶는 이유가 이런 호출 구조에 있다.
class Device:
    def __init__(self, name):
        self.name = name
    def label(self):
        return "device:" + self.name

a = Device("router")
b = Device("switch")
print(a.label(), Device.label(b))
# device:router device:switch

코드 동작을 설명하는 교육 예시. 주석의 결과는 언어 규칙으로 설명한 예상값이며 이 페이지 작성 중 실행하지 않았다.

03클래스 속성·인스턴스 속성과 공유 list

  • 클래스 본문에 선언한 속성은 클래스 객체에 속한다.
  • 인스턴스에서 일반 속성을 찾을 때 인스턴스와 클래스 계층의 조회 규칙을 거치므로 인스턴스에 같은 이름이 없으면 클래스 속성이 보일 수 있다.
  • 인스턴스에서 .kind = 값을 대입하면 보통 그 인스턴스의 속성이 만들어져 클래스 값을 가린다.
    • 그러나 .items.append처럼 이미 조회한 공유 내부를 바꾸면 모든 인스턴스가 영향을 볼 수 있다.
  • 서로 독립된 목록이 필요하면 __init__ 안에서 .items = []를 매번 생성한다.
  • 클래스 속성을 C/Java의 static과 비슷한 용도로 쓸 수는 있지만 정확한 속성 조회·재대입 규칙까지 동일하다고 가정하지 않는다.
  • property 같은 descriptor가 있으면 단순한 dict 조회보다 추가 동작을 수행한다.
class Bag:
    shared = []
    def __init__(self):
        self.own = []

a, b = Bag(), Bag()
a.shared.append("x")
a.own.append("y")
print(b.shared, b.own)  # ["x"] []

코드 동작을 설명하는 교육 예시. 주석의 결과는 언어 규칙으로 설명한 예상값이며 이 페이지 작성 중 실행하지 않았다.

LEARN THROUGH PRACTICE

장비 클래스로 공통 종류와 개별 주소를 나누기

장비 객체를 생성할 때 생기는 와 메서드 호출의 를 실제 클래스 코드에 연결한다.

실습 구성과 전제

  • 사진에는 Server와 Network 클래스, 생성자, 출력 메서드가 함께 있다. 인증 필드는 재구성 코드에서 제거하고 문서용 주소만 사용한다.
  • 핵심 구조가 한 화면에 모여 있어 같은 사진을 여러 단계에 중복 배치하지 않는다. 아래 출력은 재구성 코드의 예상 결과다.
  1. 클래스 몸체와 생성자의 대입 위치를 구별한다

    • 사진의 category는 클래스 수준에 정의되고 __init__은 전달받은 주소를 .ip에 저장한다. 전자는 종류의 공통 기본값, 후자는 생성한 각 객체의 상태를 표현한다.
    • Server(...)를 두 번 호출하면 별도 인스턴스가 생긴다. 는 특정 예약 키워드가 아니라 메서드에서 관례적으로 사용하는 첫 인자 이름이며, 호출 시 인스턴스가 전달된다.
    객체지향 · 코드와 실행 결과 1원본 크기로 보기 ↗
    • Server와 Network에 각각 category와 __init__ 메서드가 정의되어 있다.
    • 생성자에서 self.ip를 설정하고 view가 속성을 출력하며, 아래에서 여러 객체를 리스트에 넣는 구성이 보인다.
    확인 기준

    같은 클래스에서 만들었다는 이유로 모든 속성이 하나의 공용 저장소에 들어간다고 해석하지 않는다. 클래스 대입과 대입 위치를 대조한다.

  2. 두 객체를 만든 뒤 한 객체만 변경한다

    • 재구성 코드에서 a와 b의 ip는 각 인스턴스 사전에 저장된다. a.ip를 새 문자열로 바꿔도 b.ip가 자동 변경되지 않는다.
    • category는 여기서 불변 문자열을 공통으로 읽는다. 클래스 속성에 가변 를 두고 여러 인스턴스가 그 를 수정하는 경우와 구분한다.
    class Server:
        category = "server"
    
        def __init__(self, ip):
            self.ip = ip
    
        def view(self):
            return f"{self.category}: {self.ip}"
    
    a = Server("192.0.2.10")
    b = Server("192.0.2.11")
    a.ip = "192.0.2.20"
    print(a.view())  # server: 192.0.2.20
    print(b.view())  # server: 192.0.2.11

    자격 정보 없이 주소와 종류만 남긴 재구성 예시다.

    확인 기준

    두 인스턴스의 주소 출력이 각각 다르고, a의 변경 뒤 b가 유지되는지 확인한다. 사진의 계정·암호 필드를 복사할 필요는 없다.

  3. bound method와 속성 조회 결과를 연결한다

    • a.view()는 a에 묶인 메서드를 호출한다. 이 예시에서는 Server.view(a)와 같은 인스턴스를 전달하므로 동일 문자열을 반환한다.
    • vars(a)에는 인스턴스에 설정한 ip가 보이고 category는 클래스 쪽에서 찾는다. a.category를 새로 대입하면 그 인스턴스의 이름이 클래스 기본값을 가릴 수 있다.
    print(vars(a))  # {'ip': '192.0.2.20'}
    print(a.view() == Server.view(a))  # True
    print(Server.category, b.category)  # server server

    앞의 재구성 객체를 이어 사용한 예상 출력이다.

    확인 기준

    인스턴스 사전에 없는 속성도 클래스에서 조회될 수 있다. vars 출력에 없다는 이유만으로 속성이 존재하지 않는다고 단정하지 않는다.

실습에서 확인한 동작 원리

  • 클래스는 공통 속성과 동작을 정의하고, 생성자의 대입은 각 인스턴스의 상태를 만든다.
  • 메서드 호출은 객체 상태를 인자로 전달하는 구조이므로 데이터와 동작의 연결을 추적할 수 있다.
동작·명령 참고 문서

04property는 속성 접근 문법에 검증을 연결한다

  • @property를 붙인 getter는 obj.name처럼 읽을 때 호출되고 @name.setter는 같은 속성에 대입할 때 호출된다.
  • 외부 사용자는 괄호 없는 속성 문법을 유지하면서 클래스 내부에서 검증이나 계산을 추가할 수 있다.
  • 생성 시에도 .name = value처럼 setter 경로를 사용하면 초기화와 변경이 같은 검증을 거친다.
  • _name의 밑줄 하나는 내부 사용이라는 관례이지 외부 접근을 물리적으로 막는 보안 경계가 아니다.
  • __name은 이름 맹글링을 통해 상속 시 충돌을 줄일 수 있지만 악의적인 코드에서 절대 읽을 수 없는 private 저장소가 되지는 않는다.
  • property로 정상 API의 불변 조건을 지키는 것과 같은 의 다른 코드를 보안상 격리하는 것은 다른 문제다.
class Device:
    def __init__(self, name):
        self.name = name
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self, value):
        if not isinstance(value, str) or not value.strip():
            raise ValueError("name must be nonempty text")
        self._name = value

d = Device("router")
print(d.name)   # router
# d.name = "" 는 ValueError를 발생시킨다.

코드 동작을 설명하는 교육 예시. 주석의 결과는 언어 규칙으로 설명한 예상값이며 이 페이지 작성 중 실행하지 않았다.

원리 심화

05속성 조회는 descriptor 규칙을 통과한다

  • 일반적인 object.__getattribute__ 기반 인스턴스 조회를 설명한다. __getattribute__를 직접 바꾸거나 특수 메타클래스를 쓰면 동작을 별도로 분석해야 한다.
  • 클래스의 같은 이름 속성이 __get__과 (__set__ 또는 __delete__)를 제공하는 descriptor이면 읽기에서 인스턴스 사전보다 우선한다. property는 이 규칙으로 속성 읽기·대입을 함수에 연결한다.
  • 우선하는 가 없으면 보통 인스턴스 __dict__의 같은 이름을 찾는다. 여기의 값은 인스턴스마다 따로 존재할 수 있다.
  • 클래스에 정의한 보통 함수는 non-data descriptor다. 인스턴스에서 조회하면 해당 인스턴스를 묶은 가 되므로 호출 때 를 연결한다.
  • 클래스 상속 경로에서도 속성을 얻지 못하면 AttributeError로 이어지고 정의된 __getattr__가 대안 값을 제공할 수 있다. 사전에 키가 없다는 사실만으로 property까지 없다고 판단하지 않는다.
예제로 추적

06property와 같은 이름의 __dict__ 항목 비교

  • 아래 Counter는 실제 값을 _value에 저장하고 value property의 getter가 그 값을 반환한다. value라는 공개 이름과 _value라는 저장 이름이 다르다.
  • 일반 인스턴스 사전에 value=99를 직접 넣어도 인 property가 우선하므로 c.value는 3을 반환한다. 이 삽입은 원리를 드러내는 교육용 조작이다.
  • 이 property에는 setter가 없으므로 c.value=4는 AttributeError가 된다. getter만 정의했으니 같은 이름의 인스턴스 속성으로 자유롭게 덮인다고 생각하면 안 된다.
  • 보통 메서드는 non-data descriptor이므로 인스턴스에 같은 이름의 값이 저장되면 그 값이 먼저 보일 수 있다. 정수로 덮은 뒤 호출하면 호출 불가능 TypeError가 발생할 수 있다.
  • 실제 프로그램에서는 __dict__ 직접 변경으로 검증을 우회하기보다 공개 메서드·property 계약을 따른다. 의 밑줄 관례는 운영체제 접근 통제 같은 보안 경계가 아니다.
class Counter:
    def __init__(self):
        self._value = 3
    @property
    def value(self):
        return self._value

c = Counter()
c.__dict__["value"] = 99
print(c.__dict__["value"])  # 99
print(c.value)              # 3
# c.value = 4  # setter가 없어 AttributeError

실행하지 않은 교육 예시. 주석의 출력은 언어 규칙으로 추적한 예상값이다.

검증·예외

07속성 오류와 객체 초기화 실패의 위치 찾기

  • obj.name과 type(obj).name을 비교하면 인스턴스 값인지 클래스 정의인지 구분하는 데 도움이 된다. property 조회는 함수 실행을 일으킬 수 있어 단순 메모리 열람으로만 보지 않는다.
  • __init__ 도중 예외가 나면 필수 속성이 모두 준비됐다고 가정할 수 없다. 생성 성공 뒤에만 객체를 다른 구성요소에 등록하는 방식으로 불완전한 상태 노출을 줄인다.
  • setter 안에서 .name=value를 다시 실행하면 같은 setter를 재호출할 수 있다. ._name처럼 실제 저장소와 공개 접근 이름을 분리한 이유를 확인한다.
  • super는 단순히 소스에 적힌 부모 하나를 하드코딩해 부르는 기능이 아니라 현재 클래스 다음의 MRO 경로를 따른다. 다중 상속에서는 실제 __mro__와 협력적 호출을 함께 본다.
  • 다른 인스턴스에서도 값이 바뀌면 클래스 수준 가변 객체·공유 인자·얕은 복사를 구분한다. 객체지향 문법을 썼다는 사실만으로 상태가 자동 격리되는 것은 아니다.

08인스턴스·클래스·정적 메서드의 자동 인자

  • 일반 메서드는 로 인스턴스를, @classmethod는 cls로 클래스를 자동 전달받는다.
  • 클래스 메서드는 다른 입력 형식에서 인스턴스를 만드는 대체 생성 경로에 유용하다.
  • @staticmethod는 나 cls를 자동 전달받지 않는 함수이며 클래스와 논리적으로 관련된 계산을 묶는 데 쓸 수 있다.
  • 정적 메서드라고 반드시 순수하거나 외부 상태를 변경할 수 없는 것은 아니다.
  • classmethod에서 고정된 부모 이름 대신 cls(...)를 사용하면 자식 클래스에서 호출했을 때 해당 클래스를 만드는 흐름을 구성할 수 있다.
    • 다만 상속한 생성자의 인자 규약이 맞아야 한다.
  • 자동 인자가 무엇인지 이해하면 장식자 이름만 외우는 것보다 호출 오류를 정확히 설명할 수 있다.
class Endpoint:
    def __init__(self, host, port):
        self.host, self.port = host, port
    @classmethod
    def from_text(cls, text):
        host, port = text.rsplit(":", 1)
        return cls(host, int(port))
    @staticmethod
    def valid_port(port):
        return 1 <= port <= 65535

e = Endpoint.from_text("server.example:22")
print(e.host, e.port)  # server.example 22

코드 동작을 설명하는 교육 예시. 주석의 결과는 언어 규칙으로 설명한 예상값이며 이 페이지 작성 중 실행하지 않았다.

09상속·다형성·super와 객체 조합

  • 상속은 기반 클래스의 인터페이스와 동작을 확장하거나 재정의하는 방식이다.
  • 같은 이름의 메서드를 가진 서로 다른 객체를 같은 호출 코드에서 사용할 수 있는 것이 다형성의 한 예다.
  • 은 특정 부모 클래스만 고집하기보다 필요한 동작을 제공하는 객체를 받는 duck typing도 널리 사용한다.
  • 객체가 다른 객체를 속성으로 보유하고 기능을 위임하는 조합은 상속과 다른 재사용 방식이다.
  • super()는 단순히 소스에 적힌 부모 하나를 무조건 호출하는 표현이 아니라 메서드 해석 순서 MRO에 따라 다음 구현을 찾는다.
  • 다중 상속에서는 협력적인 인자 전달과 각 구현의 super 호출이 중요하다.
  • 자식에서 __init__를 다시 정의하면 필요한 기반 초기화를 명시적으로 이어야 한다.
  • 모든 관계를 상속으로 표현하기보다 is-a 관계와 has-a 관계를 구별한다.

10dunder·__dict__·__slots__·자원 수명

  • __str__은 문자열 표현, __len__은 len(), __iter__와 __next__는 반복 규약처럼 특별 메서드는 연산과 연결되는 사용자 정의 확장 지점이다.
    • 사용자가 정의할 수 없는 내부 전용 함수라고 보면 안 된다.
  • 일반 사용자 클래스는 인스턴스 __dict__로 동적 속성을 저장할 수 있지만 모든 내장형과 모든 클래스가 이를 갖는 것은 아니다.
  • __slots__는 지정된 속성 저장을 선언하고 경우에 따라 인스턴스 dict를 생략해 공간을 줄인다.
  • 기반 클래스가 dict를 제공하거나 slots에 __dict__를 포함하는 등 상속 조건에 따라 결과가 달라지므로 항상 모든 동적 속성이 금지된다고 일반화하지 않는다.
  • del name은 이름 연결 제거이며 객체의 즉시 파괴와 __del__ 호출을 언어 전체에서 보장하지 않는다. 파일·소켓 정리는 __del__ 시점에 맡기기보다 with나 명시적 close로 수명을 정한다.

11캡슐화와 검증

  • 밑줄로 시작하는 이름이나 이름 맹글링은 내부 사용 의도를 표현하고 충돌을 줄이는 수단이다.
    • 악의적인 코드의 접근을 막는 보안 경계로 취급하지 않는다.
  • 객체가 유효한 상태를 유지하도록 생성과 변경 지점에서 검증한다.
class Inventory:
    def __init__(self, names):
        self.names = list(names)

    def count(self):
        return len(self.names)

print(Inventory(["router", "switch"]).count())

입력 목록을 복사해 인스턴스별 상태를 만들고 개수를 조회한다.

12인스턴스 두 개가 같은 목록을 보게 되는 이유

  • 클래스 본문에 items = []를 두면 그 목록은 클래스 속성으로 저장된다.
  • 인스턴스 p와 q가 자기 items 속성을 따로 갖지 않을 때 p.items와 q.items는 클래스 쪽 목록을 찾아 같은 객체를 볼 수 있다.
  • p.items.append("a")는 속성 이름을 새로 만드는 것이 아니라 조회된 그 목록을 바꾸므로 q에서도 변화가 보인다.
  • 상태가 각 인스턴스의 것이라고 생각했지만 실제로는 클래스에 놓인 공유 객체였던 경우다.
  • 인스턴스마다 목록이 필요하면 __init__에서 .items = []처럼 개별 속성을 초기화한다.
  • p.items = []는 p에 인스턴스 속성을 설정해 일반적인 조회에서 클래스 속성을 가리지만 q가 보는 클래스 목록을 지우지 않는다.
  • 실제 속성 조회에는 descriptor와 property 등의 규칙도 있으므로 여기서는 그런 기능이 없는 단순 클래스 사례를 다룬다.
  • 메서드에서 를 받는다는 사실만으로 모든 사용 데이터가 인스턴스별로 복제되는 것은 아니다.
  1. 본문의 items=[]가 실행되어 클래스 속성이 목록 하나를 참조한다. 인스턴스 p와 q를 만들되 각각의 items 속성은 아직 설정하지 않았다고 가정한다.
  2. 일반적인 속성 조회에서 p 자체에 items가 없으면 클래스의 items를 찾는다. 이 사례에서 p.items는 클래스가 보관한 목록을 참조한다.
  3. p.items.append("a")는 조회한 목록을 변경한다. p.items에 새 값을 대입하는 동작과 다르므로 p 전용 목록을 만들지 않는다.
  4. q에도 자체 items가 없으므로 같은 클래스 목록을 찾고 ["a"]를 보게 된다. 인스턴스별 목록이 필요하면 생성 시 .items에 별도 목록을 연결한다.

설명과 함께 보는 보조 도식

경로와 상태

클래스의 가변 속성이 공유되는 구조

descriptor가 없는 단순 클래스에서 p와 q 모두 인스턴스 items 속성을 아직 만들지 않은 사례다.

속성 조회속성 조회items 참조자신의 items 속성이 없다.인스턴스 p자신의 items 속성이 없다.인스턴스 qitems 속성에 리스트 참조를 보관한다.클래스클래스 본문에서 만들어진 객체공유 리스트
전체 단계를 한 번에 읽기
  1. 01

    클래스 정의

    클래스 속성 items가 빈 목록을 참조한다.

    items
    []
  2. 02

    p에서 조회

    p에 속성이 없으므로 클래스 쪽의 목록을 얻는다.

    p.items
    공유 리스트
  3. 03

    p.items.append

    조회된 목록에 a를 추가한다.

    items
    ["a"]
  4. 04

    q에서 조회

    q도 같은 클래스 속성을 찾아 변경된 내용을 본다.

    q.items
    ["a"]

화살표: 진행 또는 참조 방향 · 파란색: 현재 단계에서 다루는 관계

학습 모식도. 참조 또는 연결 관계를 표현한다. 흐리게 표시된 요소는 해당 단계에서 아직 사용되지 않거나 제외된 요소다.

13객체가 책임지는 상태와 자원 구분하기

  • 객체 두 개의 속성 값을 출력하는 데 그치지 말고 변경되는 자료의 동일성과 저장 위치를 확인한다.
  • 클래스 속성의 불변 설정은 공유해도 적절할 수 있지만 연결·파일·가변 캐시처럼 수명이 있는 자원은 생성·소유·정리 책임을 명확히 해야 한다.
  • 한 객체를 닫았는데 다른 객체도 실패한다면 같은 연결을 공유하는지 살펴본다.
  • 목록을 복사해도 목록 안에 든 연결 객체까지 독립적으로 생성되는 것은 아니다.
  • 상속으로 메서드를 바꿀 때는 호출자가 기대하는 입력·출력과 오류 계약을 지켜야 한다.
  • 연결 기능을 별도 객체로 받아 사용하는 합성은 가짜 응답을 넣어 처리 로직을 확인하는 데도 도움이 된다.
  • property는 접근을 메서드로 제어할 수 있지만 외부 악성 코드에 대한 보안 격리는 아니다.
    • 밑줄이나 이름 맹글링도 의도 전달과 충돌 회피의 도구다.
  • 클래스가 크고 많은 전역 상태를 대신 갖고 있다면 관련 없는 책임이 한 객체에 모였는지 검토한다.
구성·관찰 대상확인 정보판단할 차이
클래스 속성여러 인스턴스의 공통 참조공유 가변 자료
인스턴스 속성에 저장된 상태각 객체의 독립 상태
메서드와 호출 계약속성 조회와 객체 변경
합성 자원연결·파일의 소유자공유와 종료 책임
READ · OBSERVE · CONNECT

함께 보는 학습 자료

아래 공식 자료를 본문의 개념·예제와 함께 읽어 보세요. 자료별로 확인할 위치와 관찰할 내용을 적었습니다.

실습 가이드

Python 클래스·인스턴스 변수 실습

and Instance Variables의 공유 리스트 예제를 먼저 읽는다. 같은 클래스의 두 인스턴스에서 메서드를 호출할 때 어느 리스트를 수정하는지 손으로 추적한 뒤 초기화 방식과 비교한다.

읽으면서 확인할 것
  • 클래스 변수와 인스턴스 변수의 저장 위치
  • 를 받는 메서드 바인딩
  • 가변 클래스 속성의 공유 결과
공식 가이드

Descriptor의 실제 속성 조회 우선순위

Invocation from an instance의 등가 코드를 읽고 property와 동일 이름의 __dict__ 항목을 대입해 본다. __get__ 존재 여부와 판정이 별개 검사임을 코드 순서로 확인한다.

읽으면서 확인할 것
  • →인스턴스 사전→비데이터 descriptor 순서
  • 읽기 전용 property가 대입을 거부하는 원리
  • 클래스 속성 조회와 인스턴스 조회의 차이
학습 확인

이해했는지 확인하기

  • 클래스 공유 속성과 인스턴스별 속성을 구분한다.
  • 초기화와 메서드의 입력·출력 계약을 확인한다.
  • 상속이 적절한 관계인지 합성이 나은지 검토한다.

함께 확인할 공식 자료