# 파이썬 클래스와 객체지향: __init__, self, 메서드, 상속, dataclass

URL: https://skillaria.top/ko/guides/python/python-classes

파이썬 클래스는 객체의 설계도입니다. 클래스는 필드와 메서드를 정의하고, 클래스를 호출하면 객체(인스턴스)가 만들어집니다. __init__은 필드를 채우고, self는 현재 객체를 가리킵니다. 상속은 부모의 동작을 물려받아 일부를 바꾸게 하며, @dataclass는 주로 데이터를 담는 클래스의 __init__, __repr__, __eq__를 자동으로 만들어 줍니다.

파이썬 클래스는 객체의 설계도입니다. 객체가 어떤 데이터를 갖고 어떤 동작을 하는지 정의합니다. 객체(인스턴스)는 그 설계도로 만든 실제 물건입니다. 파이썬의 객체지향 프로그래밍은 클래스, 상속, 그리고 `__init__`이나 `__repr__` 같은 특수 메서드에 기반합니다. 이 글에서는 하나의 예제 — 지출 관리 — 를 가지고 전체 흐름을 살펴봅니다.

## 딕셔너리 대신 클래스를 쓰는 이유

데이터는 딕셔너리로도 담을 수 있습니다: `{"title": "커피", "amount": 250}`. 하지만 딕셔너리는 어떤 키가 맞는지 알지 못합니다. `expense.get("amout", 0)`처럼 키 이름을 틀려도 오류가 나지 않고 0이 돌아옵니다. 보고서가 조용히 틀린 값을 내게 됩니다.

클래스 객체는 필드를 미리 선언합니다. 존재하지 않는 속성에 접근하면 바로 `AttributeError`가 나고, 최신 파이썬은 올바른 이름까지 제안합니다. 또 클래스는 데이터와 그 데이터를 다루는 동작을 한곳에 모읍니다. 지출 객체가 세금 포함 금액을 계산하고, 보고서 객체가 합계를 구하는 식입니다.

## 파이썬 클래스 만들기

`class` 키워드로 클래스를 정의합니다. 이름은 PEP 8에 따라 CamelCase로 씁니다. 가장 간단한 클래스 예제는 다음과 같습니다.

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

coffee = Expense("커피", 250)
taxi = Expense("택시", 300)
print(coffee.title, coffee.amount)   # 커피 250
```

`Expense("커피", 250)`을 호출하면 새 인스턴스가 만들어집니다. `coffee`와 `taxi`는 같은 타입이지만 서로 다른 객체이며, 각자 자기 필드 값을 가집니다. `isinstance(coffee, Expense)`는 `True`를 반환합니다.

### 생성자 __init__: 객체 초기화

`__init__`은 흔히 생성자라고 부르지만, 정확히는 초기화 메서드입니다. 객체 자체는 `__new__`가 만들고, `__init__`은 그 직후에 호출되어 받은 인자를 필드에 넣습니다. 이 메서드는 아무 값도 반환하지 않아야 합니다(반드시 `None`을 반환해야 합니다).

`__init__`에서 입력값을 검사하면, 잘못된 객체가 프로그램 안에 생겨나지 않습니다.

```
class Expense:
    def __init__(self, title, amount):
        if amount < 0:
            raise ValueError("금액은 음수가 될 수 없습니다")
        self.title = title
        self.amount = amount
```

### self란 무엇인가

`self`는 메서드가 지금 작업하고 있는 바로 그 객체를 가리킵니다. `coffee.with_tax()`를 호출하면, 파이썬은 사실상 `Expense.with_tax(coffee)`를 실행하며 객체를 첫 번째 인자로 넘깁니다. 그래서 메서드 정의에서는 `self`가 첫 번째 매개변수이고, 호출할 때는 쓰지 않습니다.

`self`는 키워드가 아니라 관례입니다. 이름을 바꿔도 동작하지만 아무도 그렇게 하지 않습니다. 초보자가 자주 하는 실수는 메서드 정의에서 `self`를 빠뜨리는 것입니다. 그러면 `TypeError: Expense.with_tax() takes 0 positional arguments but 1 was given`가 납니다.

## 메서드와 특수 메서드

메서드는 클래스 안에 선언된 함수입니다. `self`를 통해 객체의 필드에 접근합니다.

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    def with_tax(self, rate=0.2):
        return self.amount * (1 + rate)

    def __repr__(self):
        return f"Expense({self.title!r}, {self.amount})"

coffee = Expense("커피", 250)
print(coffee.with_tax())   # 300.0
print(coffee)              # Expense('커피', 250)
```

`__repr__`이 없으면 출력이 `<__main__.Expense object at 0x7f...>`처럼 나와서 디버깅에 도움이 되지 않습니다. 앞뒤에 밑줄 두 개가 붙은 메서드를 특수 메서드(던더 메서드)라고 합니다. 파이썬이 상황에 맞춰 자동으로 호출합니다. `__str__`은 사용자에게 보이는 문자열, `__eq__`는 `==` 비교, `__len__`은 `len(obj)`를 담당합니다.

### @classmethod와 @staticmethod

일반 메서드는 인스턴스를 다룹니다. `@classmethod`를 붙인 메서드는 첫 인자로 클래스 자체(`cls`)를 받습니다. 주로 다른 형식의 데이터에서 객체를 만드는 대체 생성자로 씁니다. `@staticmethod`는 객체도 클래스도 받지 않는 일반 함수입니다. 논리적으로 클래스와 관련된 함수일 때 씁니다.

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    @classmethod
    def from_csv(cls, line):
        title, amount = line.split(";")
        return cls(title, float(amount))

    @staticmethod
    def is_valid_amount(value):
        return value >= 0

e = Expense.from_csv("점심;520")
print(e.amount)                    # 520.0
print(Expense.is_valid_amount(-1)) # False
```

### 소멸자 __del__

파이썬에는 객체가 사라지기 직전에 가비지 컬렉터가 호출하는 `__del__` 메서드가 있습니다. 하지만 호출 시점이 보장되지 않으므로 자원 해제를 여기에 맡기면 안 됩니다. 파일이나 연결은 `with` 구문이나 `close()` 메서드로 명시적으로 닫으세요.

## 클래스 속성과 인스턴스 속성

`self.이름 = ...`으로 설정한 필드는 특정 객체에만 속합니다. 클래스 본문에서 바로 선언한 변수는 클래스 속성이며, 모든 인스턴스가 공유합니다. 보통 상수를 이렇게 둡니다.

```
class Expense:
    CURRENCY = "KRW"          # 클래스 속성, 상수

    def __init__(self, title, amount):
        self.title = title    # 인스턴스 속성
        self.amount = amount

a = Expense("커피", 250)
b = Expense("차", 150)
print(a.CURRENCY, Expense.CURRENCY)   # KRW KRW
```

함정은 변경 가능한 클래스 속성입니다. 클래스 본문에 `rows = []`를 쓰고 `self.rows.append(...)`로 채우면, 리스트가 모든 객체에서 공유됩니다. 변경 가능한 필드는 항상 `__init__`에서 만드세요.

객체의 필드를 보려면 `vars(obj)`나 `obj.__dict__`를, 문자열로 된 이름으로 속성을 읽으려면 `getattr(obj, "amount")`를 씁니다.

## 프로퍼티: @property

프로퍼티는 메서드이지만 바깥에서는 괄호 없이 일반 필드처럼 읽힙니다. 계산으로 얻는 값에 씁니다. 합계를 따로 저장해 두고 갱신을 잊는 대신, 필요할 때 계산하면 됩니다.

```
class Report:
    def __init__(self):
        self.rows = []

    def add(self, expense):
        self.rows.append(expense)

    @property
    def total(self):
        return sum(e.amount for e in self.rows)

r = Report()
r.add(Expense("커피", 250))
r.add(Expense("택시", 300))
print(r.total)   # 550
```

값을 쓰는 것도 통제하려면 `@total.setter`를 추가합니다. 세터가 없으면 `r.total = 0`은 `AttributeError`를 냅니다.

캡슐화에 관해서도 짚고 넘어가겠습니다. 파이썬에는 엄격한 비공개 필드가 없습니다. 밑줄 하나(`_cache`)는 '외부에서 건드리지 마세요'라는 약속이고, 밑줄 두 개(`__secret`)는 이름 맹글링(name mangling)을 적용해서, 하위 클래스가 실수로 같은 이름을 덮어쓰지 않도록 합니다.

## 파이썬 상속: 부모 클래스 확장하기

상속은 부모 클래스를 바탕으로 자식 클래스를 만드는 것입니다. 자식은 부모의 필드와 메서드를 모두 받고, 필요한 메서드만 재정의합니다. 부모는 괄호 안에 씁니다.

```
class MonthlyReport(Report):
    def __init__(self, month):
        super().__init__()        # 부모의 __init__ 호출
        self.month = month

    def add(self, expense):       # 메서드 재정의
        print(f"[{self.month}] + {expense}")
        super().add(expense)      # 부모의 동작 유지

m = MonthlyReport("10월")
m.add(Expense("커피", 250))   # [10월] + Expense('커피', 250)
print(m.total)                # 250 — 프로퍼티도 물려받았습니다
```

`super()`는 부모 메서드를 호출하기 위한 중간 객체를 돌려줍니다. 자식의 `__init__`에서 `super().__init__()`을 빠뜨리는 것은 흔한 실수입니다. 부모가 만들 `rows` 필드가 없어서, 첫 `add` 호출에서 `AttributeError`가 납니다.

상속 관계는 `issubclass(MonthlyReport, Report)`로 확인합니다. 파이썬 3의 모든 클래스는 암묵적으로 `object`를 상속하며, 그래서 기본 `__repr__`, `__eq__` 등을 가집니다. 다중 상속(`class C(A, B)`)도 지원하며, 메서드를 찾는 순서는 `C.__mro__`로 볼 수 있습니다.

### 추상 클래스

추상 클래스는 인터페이스를 정합니다. 자식 클래스가 반드시 구현해야 하는 메서드를 정하고, 추상 클래스 자체는 인스턴스로 만들 수 없습니다. 이를 위해 `abc` 모듈을 씁니다.

```
from abc import ABC, abstractmethod

class Exporter(ABC):
    @abstractmethod
    def export(self, report): ...

class CsvExporter(Exporter):
    def export(self, report):
        return "\n".join(f"{e.title};{e.amount}" for e in report.rows)

Exporter()   # TypeError: Can't instantiate abstract class Exporter ...
```

추상 메서드를 모두 구현하지 않은 자식 클래스도 여전히 추상이며, 객체를 만들면 같은 오류가 납니다.

## 상속 대신 조합(컴포지션)

상속을 배우고 나면 계층을 깊게 쌓고 싶어집니다. 기본 지출, 금액 지출, 원화 지출처럼 다섯 단계까지요. 파이썬에서는 이런 구조가 드뭅니다. 상속은 '~이다'(is-a) 관계를, 조합은 '~을 가진다'(has-a) 관계를 나타냅니다. 보고서는 지출 목록이 아니고, 지출 목록을 `rows` 필드에 담고 있을 뿐입니다.

실용적인 규칙은 이렇습니다. 다른 객체의 기능이 필요하면 상속하지 말고 필드로 담으세요. 데이터 중심의 클래스는 dataclass로, 로직은 일반 함수로 두면 충분한 경우가 많습니다.

## dataclass: 데이터 클래스

데이터를 주로 담는 클래스에서 `__init__`, `__repr__`, `__eq__`를 손으로 쓰는 것은 번거롭습니다. `dataclasses` 모듈의 `@dataclass` 데코레이터는 타입 힌트가 달린 필드를 보고 이 메서드들을 생성합니다.

```
from dataclasses import dataclass, field

@dataclass
class Expense:
    title: str
    amount: float
    tags: list[str] = field(default_factory=list)

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("금액은 음수가 될 수 없습니다")

e = Expense("커피", 250)
print(e)                           # Expense(title='커피', amount=250, tags=[])
print(e == Expense("커피", 250))   # True
```

이전에 `__init__`에 쓰던 검증은 `__post_init__`으로 옮깁니다. 이 메서드는 생성된 초기화 코드 직후에 호출됩니다. 변경 가능한 기본값은 `field(default_factory=list)`로만 지정할 수 있습니다. `tags: list = []`처럼 쓰면 dataclass가 막고 `ValueError`를 냅니다.

### frozen과 slots

`frozen=True`는 객체를 변경할 수 없게 만들고, `slots=True`(Python 3.10부터)는 `__slots__`를 만들어 메모리를 아낍니다. 고정된 객체의 필드를 바꾸려 하면 `FrozenInstanceError`가 납니다. 새 값이 필요하면 복사본을 만듭니다.

```
from dataclasses import dataclass, replace

@dataclass(frozen=True, slots=True)
class Expense:
    title: str
    amount: float

e = Expense("커피", 250)
# e.amount = 300   -> dataclasses.FrozenInstanceError
e2 = replace(e, amount=300)
print(e, e2)   # Expense(title='커피', amount=250) Expense(title='커피', amount=300)
```

Python 3.13부터는 같은 일을 범용 함수 `copy.replace()`로도 할 수 있습니다. 고정된 dataclass 객체는 `__hash__`가 있으므로 집합의 원소나 딕셔너리의 키로 쓸 수 있습니다.

## 클래스 관련 새 기능: Self, @override, __slots__

타입 힌트로 클래스를 표현하는 데 쓰는 도구가 두 가지 있습니다. `typing.Self`(Python 3.11, PEP 673)는 '같은 클래스의 객체를 반환한다'는 뜻이며, 상속받은 메서드에서도 올바르게 동작합니다. `@override` 데코레이터(Python 3.12, PEP 698)는 부모 메서드를 재정의한다고 표시합니다. 이름에 오타가 있으면 mypy나 pyright가 부모에 그런 메서드가 없다고 알려 줍니다.

```
from typing import Self, override

class Report:
    def __init__(self):
        self.rows = []

    def add(self, e) -> Self:
        self.rows.append(e)
        return self              # 체이닝 호출이 가능합니다

class MonthlyReport(Report):
    @override
    def add(self, e) -> Self:
        return super().add(e)
```

Python 3.15에서는 좁은 제약 하나가 풀렸습니다. `tuple`의 하위 클래스에서 비어 있지 않은 `__slots__`를 쓸 수 있고, 모든 클래스의 `__slots__`에 `__dict__`와 `__weakref__`를 넣을 수 있습니다. Python 3.14에서는 튜플 하위 클래스가 이렇게 선언하면 `TypeError: nonempty __slots__ not supported for subtype of 'tuple'`가 났습니다.

## 클래스에서 자주 하는 실수

| 실수 | 증상 | 해결 |
| --- | --- | --- |

| 메서드 정의에서 `self` 빠뜨림 | `TypeError: ... takes 0 positional arguments but 1 was given` | 메서드의 첫 매개변수는 항상 `self` |

| `__init__`에서 `self.` 없이 대입 | 값이 생성자를 벗어나면 사라짐 | `self.amount = amount`로 쓰기 |

| 클래스 속성으로 리스트 선언 | 데이터가 객체끼리 섞임 | `__init__`이나 `default_factory`에서 생성 |

| `super().__init__()` 누락 | 부모 필드에서 `AttributeError` | 자식 초기화에서 부모 초기화 호출 |

| 프로퍼티를 괄호로 호출 | `TypeError: 'int' object is not callable` | 괄호 없이 `r.total`로 읽기 |

## 정리

파이썬 객체지향은 간단한 원칙에서 출발합니다. 클래스가 필드와 메서드를 정의하고, `__init__`이 필드를 채우고, `self`가 현재 객체를 가리킵니다. 상속과 `super()`로 동작을 확장하고, 추상 클래스로 인터페이스를 정하고, `@property`로 계산되는 필드를 만듭니다. 데이터 중심의 클래스에는 `@dataclass`를 쓰고, 복잡한 계층보다는 조합을 선택하세요. 이 주제는 과정 강의에서 실습합니다. 지출 관리기에서 지출과 보고서를 클래스로 나눕니다.

## FAQ

**클래스와 객체는 무엇이 다른가요?**

클래스는 타입의 설명입니다. 어떤 필드와 메서드를 가지는지를 정합니다. 객체(인스턴스)는 Expense("커피", 250)처럼 클래스를 호출해 만들며, 구체적인 값을 가집니다.

**__init__을 반드시 써야 하나요?**

아닙니다. 선언하지 않으면 object에서 상속한 기본 초기화가 쓰이며, 인자를 받지 않습니다. dataclass를 쓰면 데코레이터가 __init__을 만들어 줍니다.

**__str__과 __repr__은 무엇이 다른가요?**

__repr__은 개발자와 디버거를 위한 정확한 표현이고, __str__은 print나 f-문자열에서 사용자에게 보이는 문장입니다. __str__이 없으면 파이썬은 __repr__을 사용합니다.

**dataclass와 일반 클래스 중 무엇을 골라야 하나요?**

주로 데이터를 담고 __init__, __repr__, 비교 기능이 필요하면 dataclass가 적합합니다. 복잡한 초기화 로직이나 동작이 중요한 클래스는 일반 클래스가 낫습니다.

**파이썬에도 비공개 필드가 있나요?**

엄격한 비공개는 없습니다. 이름 앞의 밑줄 하나는 '외부 사용 금지'라는 관례이고, 밑줄 두 개는 이름 맹글링으로 하위 클래스와의 이름 충돌을 막아 줍니다.


---
Source: Skillaria.Top (https://skillaria.top)
