3.개발언어&라이브러리/Python

3. Python 클래스 정리: 클래스, 상속, 메서드, 접근제어

쿼드큐브 2025. 5. 21. 15:02
728x90

Python 클래스의 기본 구조부터 생성자, 메서드, 상속, 소멸자까지 핵심 개념을 정리했습니다.
실무에 바로 적용 가능한 예제와 함께 __init__, self, __del__, __enter__, __exit__까지 전체 흐름을 이해할 수 있습니다.

 

Python 클래스 정리: 클래스, 상속, 메서드, 접근제어

 

목차

1. Python 클래스 기본 구조

2. 생성자(__init__), 소멸자(__del__), self 이해하기

3. 인스턴스 변수 vs 클래스 변수

4. 메서드 종류(인스턴스, 클래스, 정적)

5. 상속과 메서드 오버라이딩

6. 캡슐화와 접근 제어(_와 __ 차이)

7. 특수 메서드(__str__, __repr__, __eq__, __lt__, __len__, __contains__)

8. 컨텍스트 매니저 __enter__, __exit__ 사용하기 : @contextmanager

관련 글 링크

 

 

1. Python 클래스 기본 구조

Python 클래스는 관련 있는 변수와 메서드를 하나로 묶는 사용자 정의 자료형입니다.

 

class MyClass:
    def __init__(self, name):      # 생성자(Constructor)
        self.name = name           # 인스턴스 변수

    def greet(self):               # 인스턴스 메서드
        print(f"Hello, {self.name}!")
user = MyClass("Alice")  # 인스턴스 생성
user.greet()             # Hello, Alice!
요소 설명
class 클래스를 정의하는 키워드
__init__() 인스턴스가 생성될 때 자동 호출되는 생성자 메서드
self 인스턴스 자신을 가리키는 참조자
self.name 인스턴스 변수 (객체마다 고유한 데이터 저장)
greet() 클래스에 소속된 메서드 (함수)

 

◆ 흐름

  1. MyClass("Alice") → 클래스 생성자 __init__() 자동 호출
  2. name 인자를 받아 self.name에 저장
  3. user.greet() 호출 시 self.name을 참조하여 메시지 출력

◆ 클래스 vs 인스턴스

클래스(Class) 객체를 생성하기 위한 설계도 또는 청사진
인스턴스(Instance) 클래스 기반으로 생성된 실제 객체
class Dog:
    pass

d1 = Dog()  # d1은 Dog 클래스의 인스턴스
d2 = Dog()  # d2도 Dog 클래스의 인스턴스 (서로 다른 객체)

 

 

2. 생성자(__init__), 소멸자(__del__), self 이해하기

Python 클래스의 핵심 구성 요소 중 가장 중요한 세 가지는 생성자 __init__, 소멸자 __del__, 그리고 self 키워드입니다.

◆ 생성자: __init__()

__init__()은 객체가 생성될 때 자동으로 호출되는 초기화 메서드입니다.
이 메서드를 통해 인스턴스 변수의 초기값을 설정할 수 있습니다.

class Person:
    def __init__(self, name, age):
        self.name = name    # 인스턴스 변수 설정
        self.age = age

    def introduce(self):
        print(f"{self.name}, {self.age}세입니다.")

#
p = Person("Tom", 30)
p.introduce()  # Tom, 30세입니다.

이 예제에서 p는 Person 클래스의 인스턴스로, 생성 시 __init__()이 호출되어 name, age가 초기화됩니다.

 

◆ self

self는 인스턴스 자신을 가리키는 참조자입니다.
클래스 내부의 메서드에서 인스턴스 변수나 다른 메서드에 접근할 때 반드시 사용해야 합니다.

class Sample:
    def set_data(self, value):
        self.data = value  # self를 통해 인스턴스에 속성 추가

    def get_data(self):
        return self.data
#
s = Sample()
s.set_data(10)
print(s.get_data())  # 10

self는 함수 정의 시 첫 번째 인자로 사용되지만, 인스턴스에서 호출할 때는 생략됩니다.

(s.get_data() → 내부적으로 Sample.get_data(s) 호출)

 

◆ 소멸자: __del__()

__del__()은 객체가 소멸될 때 자동 호출되는 메서드입니다.
주로 파일 닫기, 리소스 해제와 같은 정리 작업에 사용됩니다.

class TempFile:
    def __init__(self, filename):
        self.file = open(filename, 'w')
        print(f"{filename} 파일 열림")

    def __del__(self):
        print("파일 닫기")
        self.file.close()
        
f = TempFile("test.txt")
del f  # __del__() 호출 → 파일 닫기

 

 __del__() 사용시 주의사항

요소 설명
호출 시점 예측 불가 Python의 **가비지 컬렉터(GC)**가 객체의 참조가 모두 사라졌을 때 호출하므로 타이밍 보장 X
순환 참조 문제 객체가 서로를 참조할 경우, GC가 __del__()을 호출하지 못하는 경우가 발생
예외 안전성 부족 예외 상황에서 소멸자가 실행되지 않아 자원 누수 발생 가능성 있음

실제 파일, 네트워크, DB와 같이 정확한 타이밍에 리소스를 정리해야 하는 경우, __del__() 대신 컨텍스트 매니저를 사용하는 것이 좋습니다.

 

3. 인스턴스 변수 vs 클래스 변수

Python 클래스 내부의 변수는 선언 위치와 사용 방법에 따라 3가지 종류로 나뉩니다:

구분 선언위치 접근방법 특징
클래스 변수 클래스 정의 블록 내부 클래스이름.변수, self.변수 모든 인스턴스가 공유하는 변수
인스턴스 변수 __init__() 또는 메서드 내부 self.변수 각 인스턴스마다 독립적인 값 가짐
지역 변수 메서드 내부 메서드 내부에서만 사용 함수/메서드가 실행될 때만 생성, 종료 시 사라짐
class Dog:
    species = "Canine"  # 클래스 변수

    def __init__(self, name):
        self.name = name  # 인스턴스 변수

    def bark(self):
        sound = "Woof!"  # 지역 변수
        return f"{self.name} says {sound}"

dog1 = Dog("Buddy")
dog2 = Dog("Charlie")

print(dog1.species)  # Canine (클래스 변수)
print(dog1.name)     # Buddy (인스턴스 변수)
print(dog1.bark())   # Buddy says Woof! (지역 변수 사용)
  • dog1.species는 클래스 전체에서 공유되는 값 "Canine"을 참조합니다.
  • dog1.name은 Buddy, dog2.name은 Charlie처럼 각각의 인스턴스에 따라 다르게 설정됩니다.
  • bark() 내부의 sound는 지역 변수로, 함수가 실행될 때만 존재합니다.

◆ 주의 사항: mutable 한 클래스 변수의 공유

클래스 변수는 모든 인스턴스에서 공통으로 공유되기 때문에,
list, dict 같은 변경 가능한 객체를 클래스 변수로 선언하면 의도치 않은 결과가 발생할 수 있습니다.

 

잘못된 예시: 

class MyClass:
    items = []  # 클래스 변수 (mutable)

    def add_item(self, item):
        self.items.append(item)

a = MyClass()
b = MyClass()
a.add_item("apple")
b.add_item("banana")

print(a.items)  # ['apple', 'banana']
print(b.items)  # ['apple', 'banana'] (같은 객체!)

  - 모든 인스턴스가 동일한 리스트 객체를 공유하고 있기 때문에, 한 인스턴스의 변경이 다른 인스턴스에도 영향을 줍니다.

 

해결 방법: 인스턴스 변수로 선언: 

class MyClass:
    def __init__(self):
        self.items = []  # 인스턴스마다 별도의 리스트 생성

    def add_item(self, item):
        self.items.append(item)

 - 이제 self.items는 인스턴스마다 독립적으로 존재하므로 문제가 없습니다.

 

 

4. 메서드 종류(인스턴스, 클래스, 정적)

Python 클래스 안에서 정의되는 메서드는 용도와 호출 방식에 따라 다음 3가지로 나눌 수 있습니다:

  1. 인스턴스 메서드
  2. 클래스 메서드
  3. 정적 메서드

이들은 메서드의 첫 번째 인자접근 범위, 역할이 서로 다릅니다.

class Example:
    def instance_method(self):               # ① 인스턴스 메서드
        print(f"[인스턴스] self: {self}")

    @classmethod
    def class_method(cls):                   # ② 클래스 메서드
        print(f"[클래스] cls: {cls}")

    @staticmethod
    def static_method():                     # ③ 정적 메서드
        print("[정적] 클래스와 인스턴스와 무관")
ex = Example()
ex.instance_method()   # [인스턴스] self: <__main__.Example object at ...>
ex.class_method()      # [클래스] cls: <class '__main__.Example'>
ex.static_method()     # [정적] 클래스와 인스턴스와 무관

Example.class_method()  # 클래스에서도 호출 가능
Example.static_method() # 인스턴스 없이도 호출 가능

 

◆ 인스턴스 메서드

  • 가장 일반적인 메서드 형태
  • self를 첫 번째 인자로 받으며, 인스턴스의 상태(속성 등)에 접근할 수 있음
  • 객체의 행동/기능을 구현하는 데 사용됨
class User:
    def __init__(self, name):
        self.name = name

    def say_hello(self):
        print(f"안녕하세요, 저는 {self.name}입니다.")

#
u = User("Tom")
u.say_hello()  # 안녕하세요, 저는 Tom입니다.

 

 클래스 메서드(@classmethod)

  • 클래스 전체에 영향을 미치는 작업에 사용
  • 첫 번째 인자가 cls이며, 클래스 객체 자체를 가리킴
  • 인스턴스 없이도 호출 가능
  • 주로 클래스 수준에서 데이터 초기화, 팩토리 메서드 구현에 쓰임

팩토리 메서드 구현 예시

 - 팩토리 메서드는 클래스의 생성 방식을 다양화할 수 있어서 매우 유용합니다.

class User:
    def __init__(self, name):
        self.name = name

    @classmethod
    def from_email(cls, email):
        name = email.split("@")[0]
        return cls(name)  #cls("alice") → User("alice") 객체 생성
#
u = User.from_email("alice@example.com")

 

클래스 수준의 설정/초기화 관리

 - @classmethod는 클래스 자체에 대한 정보나 상태를 활용할 때 유용합니다.

class Config:
    _env = "dev"

    @classmethod
    def set_env(cls, env):
        cls._env = env

#
Config.set_env("prod")
print(Config._env)  # prod

 

 

정적 메서드(@staticmethod)

  • self, cls를 받지 않음 → 인스턴스나 클래스 상태에 전혀 접근하지 않음
  • 일반적인 함수와 동일하게 작동하지만 클래스 이름 공간 내에 존재
  • 보통 클래스와 논리적으로 관련은 있지만 독립적인 유틸리티 함수를 만들 때 사용
class Math:
    @staticmethod
    def add(a, b):
        return a + b

#
print(Math.add(2, 3))  # 5

 

외부 함수처럼 보이지만, 의미상 클래스에 소속된 기능일 때 정적 메서드로 정의합니다.

 

 

5. 상속과 메소드 오버라이딩

◆ 상속(Inheritance)

상속은 기존 클래스의 속성과 메서드를 새로운 클래스에 물려주는 객체지향 프로그래밍(OOP)의 핵심 개념입니다.
코드 재사용성을 높이고, 기능 확장에 매우 유용합니다.

class Animal:
    def sound(self):
        print("동물 소리")

class Dog(Animal):  # Animal을 상속받음
    def sound(self):
        print("멍멍!")

class Cat(Animal):
    def sound(self):
        print("야옹!")

dog = Dog()
cat = Cat()

dog.sound()  # 멍멍!
cat.sound()  # 야옹!
  • Dog, Cat 클래스는 Animal 클래스의 속성과 메서드를 그대로 사용할 수 있음
  • sound() 메서드를 자식 클래스에서 재정의(오버라이딩) 할 수 있음

 메서드 오버라이딩(Method Overriding)

오버라이딩이란 부모 클래스에 정의된 메서드를 자식 클래스에서 같은 이름으로 다시 정의하는 것입니다.
자식 클래스의 인스턴스에서 해당 메서드를 호출하면, 부모가 아닌 자식의 메서드가 실행됩니다.

class Animal:
    def sound(self):
        print("동물이 소리낸다")

class Dog(Animal):
    def sound(self):  # 오버라이딩
        print("멍멍!")

 

super()를 사용한 부모 메서드 호출

오버라이딩하면서도 부모 클래스의 기능을 일부 재사용하고 싶다면 super()를 사용합니다.

class Animal:
    def sound(self):
        print("기본 동물 소리")

class Dog(Animal):
    def sound(self):
        super().sound()  # 부모 메서드 호출
        print("멍멍!")

dog = Dog()
dog.sound()

#
기본 동물 소리
멍멍!
  • super()는 현재 클래스의 부모를 가리키며,
  • super().메서드()를 호출하면 부모의 기능을 그대로 사용할 수 있습니다.

 

6. 캡슐화와 접근 제어(_와 __ 차이)

◆ 캡슐화란?

캡슐화(encapsulation)는 객체의 내부 상태(데이터)를 외부에서 직접 접근하지 못하도록 보호하고, 공개된 인터페이스(메서드)를 통해서만 조작할 수 있도록 하는 객체지향 설계 원칙입니다.

Python은 Java, C++처럼 강력한 접근 제한자(private, protected 등)를 지원하지 않습니다.
대신 개발자 관례와 특수 문법(Name Mangling)을 통해 이를 우회적으로 구현합니다.

표기법 의미 및 용도
name public (공개 속성)
_name protected (내부 사용 권장, 강제는 아님)
__name private (Name Mangling 적용, 외부 접근 차단)
@property 속성을 안전하게 읽고 쓰기 위한 우아한 방법
class Account:
    def __init__(self):
        self.name = "홍길동"        # 공개 속성
        self._balance = 1000       # 보호 속성 (접근 권장 X)
        self.__password = "1234"   # 비공개 속성 (Name Mangling 적용)
self.name public 누구나 접근 가능. 공식 인터페이스로 제공되는 속성
self._balance protected 관례상 내부용. 외부에서 접근 가능하지만 사용 자제 권장
self.__password private 이름 변경(Name Mangling)을 통해 외부 접근 차단

 

◆ Name Mangling이란?

Python에서 __이름 형태로 정의된 속성은 클래스 내부에서 자동으로 이름이 바뀝니다.

class Test:
    def __init__(self):
        self.__secret = "비밀"

t = Test()
# print(t.__secret)  # AttributeError
print(t._Test__secret)  # 비밀

__secret은 실제로는 _Test__secret이라는 이름으로 저장됩니다.
이를 통해 우연한 접근이나 오용을 막는 것이지, 절대적 차단은 아닙니다.

 

  캡슐화 + 접근 메서드(getter/setter) 조합

접근을 제한하면서, 공식 인터페이스로 값을 읽거나 변경하는 방법:

class User:
    def __init__(self):
        self.__age = 0

    def get_age(self):
        return self.__age

    def set_age(self, value):
        if value >= 0:
            self.__age = value
        else:
            print("나이는 음수일 수 없습니다.")

#
u = User()
u.set_age(25)
print(u.get_age())  # 25

 

◆  @property를 활용한 캡슐화(Python 3.6+)

class Product:
    def __init__(self, price):
        self._price = price

    @property
    def price(self):       # getter
        return self._price

    @price.setter
    def price(self, value):  # setter
        if value >= 0:
            self._price = value
        else:
            raise ValueError("가격은 0 이상이어야 합니다.")

#
p = Product(10000)
p.price = 12000
print(p.price)  # 12000

읽기/쓰기 로직을 메서드로 제어하면서도, 사용자 입장에서는 속성처럼 .으로 접근 가능

 

7. 특수 메서드(__str__, __repr__, __eq__, __lt__, __len__, __contains__)

◆ __str__() : 사용자 친화적인 출력

  • 목적: 일반 사용자에게 친숙한 형식으로 객체 표현
class Product:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return f"제품명: {self.name}"

#
p = Product("노트북")
print(p)           # 제품명: 노트북
print(str(p))      # 제품명: 노트북
 __repr__() : 개발자/디버깅용 표현
  • repr() 함수 또는 인터프리터에서 객체를 직접 출력할 때 호출됩니다.
  • 목적: 개발자용 디버깅에 적합한 형식, 가능하다면 해당 문자열을 eval()로 객체 복원 가능하게 작성하는 것이 이상적입니다.
class Product:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"Product('{self.name}')"

#
p = Product("노트북")
print(repr(p))     # Product('노트북')
p                  # 인터프리터에서도 같은 결과

 

 __eq__() : 동등성 비교 (==)

class User:
    def __init__(self, username):
        self.username = username

    def __eq__(self, other):
        return self.username == other.username

#
u1 = User("alice")
u2 = User("alice")
print(u1 == u2)  # True
  • __eq__()을 구현하지 않으면, 기본은 객체 주소(id) 비교로 처리됩니다.
  • 필요하다면 __hash__()도 함께 정의해야 set, dict에서 동작이 일관됩니다.

◆ __lt__ : 순서 비교 (<)

class Box:
    def __init__(self, volume):
        self.volume = volume

    def __lt__(self, other):
        return self.volume < other.volume

#
b1 = Box(10)
b2 = Box(20)
print(b1 < b2)  # True

#함께 사용되는 관련 메서드:
# __le__: <=
# __gt__: >
# __ge__: >=
  • 객체 간 대소 비교를 할 때 호출됩니다.
  • < 연산자에 대응합니다.

◆ __len__ : 길이 반환

  • len(obj) 호출 시 실행됩니다.
  • 리스트나 문자열처럼 객체의 크기나 개수를 제공하고자 할 때 사용합니다.
class Cart:
    def __init__(self, items):
        self.items = items

    def __len__(self):
        return len(self.items)

#
c = Cart(["apple", "banana"])
print(len(c))  # 2

 

◆ __contains__ : 포함 여부 검사

  • in 연산자 또는 item in obj 표현에서 호출됩니다.
  • 내부 컨테이너(리스트, 집합 등)에 포함 여부 판단 로직을 직접 정의할 수 있습니다.
class Bag:
    def __init__(self, items):
        self.items = items

    def __contains__(self, item):
        return item in self.items

#
b = Bag(["pen", "notebook"])
print("pen" in b)     # True
print("eraser" in b)  # False

__contains__()이 없으면 __iter__()를 통해 순회하며 검사합니다.

 

 

8. 컨텍스트 매니저 __enter__, __exit__ 사용하기

Python의 with 문은 자원(resource)의 획득과 해제 과정을 자동화해주는 문법입니다.
이를 가능하게 해주는 것이 바로 컨텍스트 매니저(Context Manager)이며, 클래스 내부에 __enter__()와 __exit__() 메서드를 정의함으로써 구현할 수 있습니다.

class FileHandler:
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        self.file = open(self.name, 'w')   # 자원 획득
        print("파일 열림")
        return self.file                   # with문의 as 변수로 전달됨

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("파일 닫힘")
        self.file.close()                  # 자원 해제
with FileHandler("log.txt") as f:
    raise RuntimeError("문제 발생!")  # 예외 발생!
    f.write("이 코드는 실행되지 않음")

 

이 코드는 다음과 같이 내부적으로 처리됩니다:

# 내부적으로는 아래와 같은 동작
f = FileHandler("log.txt").__enter__()
try:
    f.write("로그 기록")
finally:
    FileHandler("log.txt").__exit__(None, None, None)

with 문 내부에서 에러가 발생해도 __exit__()는 무조건 호출되기 때문에, 파일, DB, 소켓 등의 자원을 안정적으로 정리할 수 있습니다.

 

◆ __exit__() 메서드

인자 설명
exc_type 발생한 예외 클래스 (예: ZeroDivisionError)
exc_val 예외 객체 자체 (예: ZeroDivisionError('...'))
exc_tb 예외 traceback 객체

필요하다면 __exit__() 안에서 예외를 로깅하거나 무시할 수도 있습니다.

def __exit__(self, exc_type, exc_val, exc_tb):
    if exc_type:
        print("예외 발생:", exc_val)
    self.file.close()
    return True  # 예외를 무시함

 

활용 예시

1. 파일 열기/닫기 자동화

with open("sample.txt", "r") as file:
    data = file.read()

 

2. DB 연결과 트랜잭션 관리

import sqlite3

class DB:
    def __enter__(self):
        self.conn = sqlite3.connect(":memory:")
        return self.conn

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.conn.close()

with DB() as conn:
    conn.execute("CREATE TABLE test (id INT)")

 

3. Lock 처리 (멀티스레딩)

from threading import Lock

lock = Lock()
with lock:
    # 임계영역
    pass

 

  컨텍스트 매니저의 구현 방법 2가지

클래스 기반 __enter__, __exit__ 직접 구현
데코레이터 기반 @contextmanager 사용 (from contextlib)

 

예: 데코레이터 방식

from contextlib import contextmanager

@contextmanager
def file_handler(name):
    f = open(name, 'w')
    try:
        yield f
    finally:
        f.close()

with file_handler("test.txt") as f:
    f.write("Hello")

 

 


 

 

 

관련 글 링크

2. Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수

 

2. Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수

Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수 목차 1. 지역변수 vs 전역변수 2. global 키워드의 역할과 주의사항 3. nonlocal 키워드 4. 변수처럼 다루는 함수-일급객체로서의 함수 5. 클래스

quadcube.tistory.com

 

728x90