Python 클래스, 모듈, 패키지, 예외 처리 한 번에 이해하기

Alchemist·2026년 10월 6일

KT AIVLE

목록 보기
40/130

Python 기초 문법을 익힌 뒤에는 코드를 어떻게 구조화할 것인지가 중요해진다.

함수만으로도 많은 프로그램을 만들 수 있지만 상태와 기능을 하나로 묶거나, 여러 파일로 코드를 나누거나, 예상 가능한 오류를 안전하게 처리하려면 클래스·모듈·패키지·예외 처리 개념이 필요하다.


클래스는 언제 필요할까?

사용자 데이터를 딕셔너리로 관리할 수 있다.

user = {
    "name": "kim",
    "point": 1000,
}

포인트 변경 함수도 만들 수 있다.

def add_point(
    user,
    amount,
):
    user["point"] += amount

작은 코드에서는 충분하다.

하지만 동일한 구조의 사용자가 많아지고 각 사용자가 자신의 상태를 가져야 한다면 클래스로 묶는 것이 자연스럽다.


클래스와 인스턴스

class User:
    pass

객체 생성:

user = User()

user는 User 클래스의 인스턴스다.

클래스는 사용자 정의 타입, 인스턴스는 그 타입으로 만든 실제 객체라고 이해하면 된다.


__init__

객체 생성 시 초기 상태를 설정한다.

class User:
    def __init__(
        self,
        name,
        point=0,
    ):
        self.name = name
        self.point = point
user = User(
    "kim",
    1000,
)

print(user.name)
print(user.point)

self

인스턴스 메서드의 첫 번째 매개변수는 관례적으로 self를 사용한다.

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

    def greet(self):
        print(
            f"안녕하세요. "
            f"{self.name}입니다."
        )
user = User("kim")

user.greet()

self는 현재 메서드를 호출한 인스턴스를 가리킨다.


상태와 기능을 함께 묶기

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

    def add_point(
        self,
        amount,
    ):
        self.point += amount

    def summary(self):
        return (
            f"{self.name}: "
            f"{self.point}P"
        )
user = User("kim")

user.add_point(300)
user.add_point(500)

print(user.summary())

각 인스턴스는 독립적인 상태를 가진다.


클래스 변수와 인스턴스 변수

인스턴스마다 다른 값:

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

모든 인스턴스가 공유하는 값:

class User:
    service_name = "My Service"

변경 가능한 리스트 같은 객체를 클래스 변수로 둘 때는 주의해야 한다.

class User:
    tags = []

모든 인스턴스가 같은 리스트를 공유한다.

각 객체마다 별도 리스트가 필요하다면 다음처럼 만든다.

class User:
    def __init__(self):
        self.tags = []

상속

공통 기능을 가진 부모 클래스를 확장할 수 있다.

class Account:
    def __init__(
        self,
        email,
    ):
        self.email = email

    def login(self):
        print(
            f"{self.email} 로그인"
        )
class AdminAccount(
    Account
):
    def delete_user(
        self,
        user_id,
    ):
        print(
            f"{user_id}번 "
            "사용자 삭제"
        )
admin = AdminAccount(
    "admin@example.com"
)

admin.login()
admin.delete_user(10)

메서드 오버라이딩

부모의 동작을 자식 클래스에서 다시 정의할 수 있다.

class Notification:
    def send(
        self,
        message,
    ):
        print(
            f"알림: {message}"
        )
class EmailNotification(
    Notification
):
    def send(
        self,
        message,
    ):
        print(
            f"이메일 발송: "
            f"{message}"
        )

super()

부모 클래스 초기화나 메서드를 활용할 때 사용한다.

class AdminAccount(
    Account
):
    def __init__(
        self,
        email,
        level,
    ):
        super().__init__(
            email
        )

        self.level = level

특수 메서드

Python의 내장 문법과 객체를 연결할 수 있다.

__str__

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

    def __str__(self):
        return (
            f"User("
            f"name={self.name}"
            f")"
        )
print(User("kim"))

__len__

class Team:
    def __init__(
        self,
        members,
    ):
        self.members = members

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

모듈

Python 파일 하나는 모듈로 사용할 수 있다.

price_utils.py

def calculate_discount(
    price,
    rate,
):
    return (
        price
        * (1 - rate)
    )

다른 파일:

import price_utils

result = (
    price_utils
    .calculate_discount(
        50000,
        0.1,
    )
)

from ... import ...

from math import sqrt

print(sqrt(16))

출처가 명확해야 하는 코드에서는 다음처럼 모듈명을 유지하는 편이 읽기 좋을 수 있다.

import math

print(
    math.sqrt(16)
)

from module import *는 어떤 이름이 들어왔는지 알기 어려워질 수 있어 피하는 편이 좋다.


__name__ == "__main__"

파일이 직접 실행된 경우에만 특정 코드를 실행하고 싶을 때 사용한다.

def main():
    print("프로그램 시작")


if __name__ == "__main__":
    main()

다른 파일에서 import할 때는 main()이 자동 실행되지 않는다.


패키지

여러 모듈을 디렉터리 구조로 묶을 수 있다.

my_app/
    main.py
    services/
        __init__.py
        user_service.py
        order_service.py
    utils/
        __init__.py
        price.py

사용:

from services.user_service import create_user
from utils.price import calculate_total

현대 Python은 namespace package도 지원하지만 일반적인 프로젝트에서는 __init__.py를 두는 구조를 여전히 자주 볼 수 있다.


ModuleNotFoundError가 발생한다면

먼저 현재 Python과 모듈 검색 경로를 확인한다.

import sys

print(sys.executable)

for path in sys.path:
    print(path)

외부 패키지라면 설치 환경도 확인한다.

python -m pip --version
python -m pip list

예외 처리

실행 중 발생할 수 있는 오류를 처리한다.

try:
    age = int(
        input("나이: ")
    )
except ValueError:
    print(
        "숫자를 입력해 주세요."
    )

여러 예외

try:
    number = int(
        input("숫자: ")
    )

    print(
        100 / number
    )

except ValueError:
    print(
        "정수가 필요합니다."
    )

except ZeroDivisionError:
    print(
        "0으로 나눌 수 없습니다."
    )

처리 가능한 구체적인 예외를 지정하는 것이 좋다.


else

예외가 없었을 때만 실행된다.

try:
    number = int(
        input("숫자: ")
    )
except ValueError:
    print("잘못된 입력")
else:
    print(
        f"입력 성공: "
        f"{number}"
    )

finally

예외 여부와 관계없이 실행된다.

try:
    print("작업")
finally:
    print("정리")

파일과 같은 자원은 with 문을 사용하는 편이 더 편리한 경우가 많다.


raise

프로그램 규칙에 맞지 않는 상태를 직접 예외로 만들 수 있다.

def withdraw(
    balance,
    amount,
):
    if amount <= 0:
        raise ValueError(
            "금액은 0보다 "
            "커야 합니다."
        )

    if amount > balance:
        raise ValueError(
            "잔액이 부족합니다."
        )

    return (
        balance
        - amount
    )

사용자 정의 예외

class InsufficientStockError(
    Exception
):
    pass
def purchase(
    stock,
    quantity,
):
    if quantity > stock:
        raise (
            InsufficientStockError(
                "재고가 부족합니다."
            )
        )

비즈니스 의미가 분명해진다.


모든 예외를 무시하면 안 되는 이유

다음 패턴은 문제를 숨길 수 있다.

try:
    ...
except:
    pass

오류를 처리할 수 없다면 적어도 로그를 남기거나 상위 코드에 다시 전달하는 것이 좋다.

try:
    ...
except ValueError:
    print("값 처리 실패")
    raise

클래스는 언제 쓰고 함수는 언제 쓸까?

상태가 거의 없는 단순 계산은 함수가 간단하다.

def normalize_email(
    email,
):
    return (
        email
        .strip()
        .lower()
    )

반면 다음 조건이 많다면 클래스를 고려할 수 있다.

  • 객체별 상태를 유지해야 한다.
  • 데이터와 관련 기능이 함께 움직인다.
  • 같은 구조의 객체를 여러 개 만든다.
  • 상속이나 다형성이 필요하다.

핵심 정리

  • 클래스는 상태와 기능을 하나의 사용자 정의 타입으로 묶을 수 있다.
  • self는 현재 인스턴스, __init__()은 초기화를 담당한다.
  • 상속과 오버라이딩으로 기존 동작을 재사용하고 확장할 수 있다.
  • .py 파일 하나는 모듈로 사용할 수 있고 여러 모듈을 패키지로 구조화할 수 있다.
  • __name__ == "__main__" 패턴은 직접 실행과 import를 구분한다.
  • try-except는 실행 중 예상 가능한 예외를 처리한다.
  • raise를 사용하면 프로그램 규칙 위반을 명시적인 오류로 표현할 수 있다.
  • 모든 예외를 무조건 무시하지 말고 처리 가능한 예외를 구체적으로 다루는 것이 좋다.
profile
html_programming_language

0개의 댓글