OOP

1. 프로그래밍 패러다임: 절차 지향 vs 객체 지향

1-1. 절차 지향(Procedural Programming)

생각 방식

  • “무엇을 먼저 하고, 그 다음에 무엇을 할까?”
  • 즉, 데이터는 따로, 그 데이터를 처리하는 함수(절차)를 따로 두고 순서대로 실행한다.
name = "Alice"
age = 25

def introduce(name, age):
    print(f"이름은 {name}이고, 나이는 {age}살입니다.")

introduce(name, age)

특징

  • 함수 중심, “위에서부터 아래로” 흐르는 순차적인 코드
  • 데이터가 여러 함수 사이를 왔다 갔다 하면서 사용된다.
  • 프로그램이 커지면
    • “이 데이터가 지금 어디에서 어떻게 바뀌고 있지?” 추적하기 힘들어짐
    • 관련된 데이터/기능이 여기저기 흩어져 있기 쉽다.

1-2. 객체 지향(Object Oriented Programming)

생각 방식

  • “이 문제를 해결하기 위해 필요한 ’대상(객체)’가 뭐지?”
  • 데이터와 그 데이터를 다루는 함수를 하나의 묶음(객체) 으로 만든다.
class Person:
    def __init__(self, name, age):     # 생성자
        self.name = name
        self.age = age

    def introduce(self):               # 메서드(행동)
        print(f"이름은 {self.name}이고, 나이는 {self.age}살입니다.")

alice = Person("Alice", 25)  # 객체(인스턴스) 생성
alice.introduce()            # 객체가 스스로 소개한다

특징

  • 클래스: 설계도
  • 인스턴스(객체): 설계도로부터 실제로 찍어낸 개체
  • 데이터와 기능이 한 덩어리로 묶여 있어서
    • 관련 코드가 한 곳에 모여 있다.
    • 객체끼리 메시지를 주고받으며 동작하는 구조로 생각하기 쉽다.

1-3. 둘의 차이를 한 줄로 요약하면

  • 절차 지향:
  • “외부에서 데이터를 들고 다니면서 함수에 계속 넘겨준다.”
  • 객체 지향:
  • “데이터와 함수를 객체 안에 묶어 넣고, 객체에게 시킨다.”

둘은 상반되는 개념이라기보다 관점과 설계 방법이 다른 것이라고 이해하면 편하다.


2. 객체(Object)와 클래스(Class)

2-1. 객체란?

  • 프로그램 안에서 실제로 사용되는 모든 것
  • 숫자, 문자열, 리스트, 사용자 정의 클래스의 인스턴스까지 전부 “객체”
  • 각각은 상태(데이터)행동(메서드) 를 가진다.

예시: “가수” 객체를 상상해보기

  • 상태(속성, attribute)
    • 이름, 나이, 데뷔연도, 소속사 …
  • 행동(메서드, method)
    • 노래하기, 춤추기, 인사하기 …

2-2. 클래스란?

  • 객체를 만들기 위한 설계도, 틀
  • “가수”라는 클래스를 만들면, 아이유, BTS, 뉴진스 멤버 등은
    그 설계도로부터 만들어진 인스턴스가 된다.
class Singer:   # 가수 설계도
    def __init__(self, name, debut_year):
        self.name = name
        self.debut_year = debut_year

    def sing(self):
        print(f"{self.name}이(가) 노래를 부릅니다!")

3. 클래스와 인스턴스

3-1. 클래스 정의 기본 형식

class MyClass:          # 클래스 이름은 보통 PascalCase
    pass                # 일단 비워둘 때 사용

조금 더 실제 예시:

class Person:
    def __init__(self, name, age):   # 생성자 메서드
        self.name = name
        self.age = age

    def introduce(self):             # 인스턴스 메서드
        print(f"안녕하세요, 저는 {self.name}, {self.age}살입니다.")
  • __init__ : 인스턴스가 만들어질 때 자동으로 호출되는 생성자
  • self : “지금 이 인스턴스 자신”을 가리키는 이름 (관례적으로 self 사용)

3-2. 인스턴스 만들기 & 사용

alice = Person("Alice", 25)  # 인스턴스 생성
bob = Person("Bob", 30)

alice.introduce()  # 안녕하세요, 저는 Alice, 25살입니다.
bob.introduce()    # 안녕하세요, 저는 Bob, 30살입니다.

여기서 기억할 것:

  • 클래스: Person
  • 인스턴스(객체): alice, bob
  • 인스턴스 메서드를 쓸 때는 alice.introduce() 처럼 점(.)으로 호출한다.

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

4-1. 인스턴스 변수

  • 인스턴스마다 각자 다른 값을 가지는 변수
  • 보통 __init__ 안에서 self.변수명 형태로 정의한다.
class Circle:
    def __init__(self, radius):
        self.radius = radius   # 인스턴스 변수

c1 = Circle(1)
c2 = Circle(2)

print(c1.radius)  # 1
print(c2.radius)  # 2

4-2. 클래스 변수

  • 클래스 자체에 붙어 있는 변수
  • 모든 인스턴스가 공유하는 값
class Circle:
    pi = 3.14              # 클래스 변수

    def __init__(self, radius):
        self.radius = radius

c1 = Circle(1)
c2 = Circle(2)

print(c1.pi, c2.pi)   # 3.14 3.14
Circle.pi = 3.14159
print(c1.pi, c2.pi)   # 3.14159 3.14159 (둘 다 같이 바뀐다)
  • Circle.pi 처럼 클래스 이름으로 접근하는 게 기본
  • 인스턴스에서도 c1.pi처럼 접근은 가능하지만,
    “어? 이게 공용 값이구나”를 알아보기 위해서 가급적 클래스이름.변수 형태로 쓰는 습관이 좋다.

5. 메서드의 세 종류

5-1. 인스턴스 메서드(instance method)

  • 가장 기본적인 메서드
  • 첫 번째 인자로 항상 self 를 받는다.
  • 인스턴스의 상태(속성)를 읽거나 바꾸는 역할.
class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):      # 인스턴스 메서드
        self.count += 1

c = Counter()
c.increment()
print(c.count)  # 1

5-2. 클래스 메서드(class method)

  • 클래스 전체와 관련된 동작을 만들 때 사용
  • 첫 번째 인자로 self 대신 cls 를 받는다.
  • 데코레이터 @classmethod 를 붙여서 만든다.
class Person:
    species = "Human"

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

    @classmethod
    def change_species(cls, new_species):
        cls.species = new_species

p1 = Person("Alice")
p2 = Person("Bob")

Person.change_species("Android")
print(p1.species, p2.species)  # Android Android

→ 인스턴스 개별 상태가 아니라, 클래스 수준의 값을 다룰 때 사용.


5-3. 정적 메서드(static method)

  • 클래스, 인스턴스와 상태를 공유하지 않는 독립적인 함수인데,
    “의미상 이 클래스 안에 두면 좋겠다” 싶을 때 넣는 메서드.
class MathUtils:
    @staticmethod
    def add(a, b):
        return a + b

print(MathUtils.add(3, 5))  # 8
  • self, cls 아무것도 받지 않는다는 게 특징.
  • “이 기능은 이 클래스와 연관은 있는데, 상태를 건드리지는 않는다” → @staticmethod.

5-4. 생성자 메서드(특별한 인스턴스 메서드)

  • __init__
  • 인스턴스가 만들어질 때 자동 호출
  • 보통 인스턴스 변수의 초기값을 설정하는 역할.
class BankAccount:
    interest_rate = 0.02  # 클래스 변수: 이자율

    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

6. 메서드 활용 예시 – 은행 계좌 클래스

하나의 예시를 통해 클래스/인스턴스 변수와 메서드가 어떻게 섞여 쓰이는지 보기.

class BankAccount:
    interest_rate = 0.02   # 공통 이자율 (클래스 변수)

    def __init__(self, owner, balance=0):
        self.owner = owner     # 계좌 주인 (인스턴스 변수)
        self.balance = balance # 잔액

    def deposit(self, amount):     # 입금
        self.balance += amount

    def withdraw(self, amount):    # 출금
        if amount <= self.balance:
            self.balance -= amount
        else:
            print("잔액 부족!")

    @classmethod
    def set_interest_rate(cls, rate):   # 이자율 변경(클래스 메서드)
        cls.interest_rate = rate

    def add_interest(self):        # 이자 적용(인스턴스 메서드)
        self.balance += self.balance * self.interest_rate

    @staticmethod
    def is_positive(amount):       # 금액이 양수인지 검사(정적 메서드)
        return amount > 0

사용 예:

alice = BankAccount("Alice", 1000)
alice.deposit(500)          # 잔액 1500
alice.withdraw(200)         # 잔액 1300

BankAccount.set_interest_rate(0.05)
alice.add_interest()        # 5% 이자 적용

print(alice.balance)
print(BankAccount.is_positive(-10))  # False

7. 상속(Inheritance)

7-1. 상속의 기본 개념

  • 부모 클래스의 속성과 메서드를 자식 클래스가 물려받는 것.
  • 같은 종류의 객체들이 공통으로 가지는 부분은 부모에 두고,
    각자의 차이점은 자식 클래스에만 추가하는 방식.
class Animal:
    def eat(self):
        print("냠냠 먹습니다.")

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

my_dog = Dog()
my_dog.eat()   # 부모에게서 물려받은 메서드
my_dog.bark()  # 자식이 가진 메서드

7-2. 상속이 필요한 이유

  1. 코드 재사용
    • 공통 기능은 부모 클래스에 한 번만 써두고, 재사용 가능.
  2. 유지보수 용이
    • 공통 로직 수정 → 부모만 고치면 자식 전부에 반영.
  3. 구조적 설계
    • “사람 → 학생/교수”, “동물 → 포유류/조류” 이런 관계를 코드에 그대로 표현.

7-3. 사람 → 학생/교수 예시

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

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

class Student(Person):
    def __init__(self, name, age, student_id):
        super().__init__(name, age)   # 부모 초기화
        self.student_id = student_id

    def talk(self):
        print(f"저는 학생이고 학번은 {self.student_id}입니다.")

class Professor(Person):
    def __init__(self, name, age, department):
        super().__init__(name, age)
        self.department = department

    def talk(self):
        print(f"저는 교수이고 소속 학과는 {self.department}입니다.")
  • Student, Professor는 Person의 자식 클래스
  • introduce()는 공통 기능 → 부모(Person)에 한 번 정의
  • 각자의 talk() 메서드는 다르게 동작 (오버라이딩, 아래에서 설명).

8. 메서드 오버라이딩(Overriding)

8-1. 개념

  • 부모 클래스에 있던 메서드를 자식 클래스에서 다시 정의해서 덮어쓰기
  • 이름은 같지만, 자식에서 원하는 방식으로 동작을 바꿀 수 있다.

위 Student/Professor 예시에서 talk() 가 바로 오버라이딩 예이다.

s = Student("Alice", 20, "2024001")
p = Professor("Bob", 45, "Computer Science")

s.introduce()  # Person의 introduce 사용
s.talk()       # Student에서 재정의한 talk

p.introduce()  # Person의 introduce 사용
p.talk()       # Professor에서 재정의한 talk
  • 같은 이름의 메서드를 호출해도, 인스턴스의 실제 타입(Student/Professor)에 따라 다른 코드가 실행된다 → 다형성(polymorphism).

9. 다중 상속(Multiple Inheritance)과 MRO

9-1. 다중 상속

  • 한 클래스가 여러 부모 클래스로부터 동시에 상속받는 것.

간단 예시:

class ParentA:
    def show(self):
        print("A")

class ParentB:
    def show(self):
        print("B")

class Child(ParentA, ParentB):
    pass

c = Child()
c.show()   # 어떤 show가 불릴까?
  • 파이썬은 MRO(Method Resolution Order) 라는 규칙에 따라
    어떤 부모의 메서드를 먼저 찾을지 순서를 정한다. (일반적으로 왼쪽 → 오른쪽)

다이아몬드 상속(여러 경로로 같은 조상이 중복되는 경우)에서도 MRO가
“한 번만 호출되게” 순서를 잘 계산해 준다.


9-2. super()의 역할

  • 부모 클래스의 메서드를 호출할 때 쓰는 도구.
  • 단일 상속에서는 super().메서드() 가 직접 부모를 부르지만,
  • 다중 상속에서는 MRO에 따른 다음 클래스의 메서드를 호출한다.

단일 상속 예:

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

class Student(Person):
    def __init__(self, name, student_id):
        super().__init__(name)  # Person.__init__ 호출
        self.student_id = student_id

다중 상속 예(개략):

class A:
    def __init__(self):
        print("A")
        super().__init__()

class B:
    def __init__(self):
        print("B")
        super().__init__()

class C(A, B):
    def __init__(self):
        print("C")
        super().__init__()

c = C()
# C -> A -> B 순서로 super가 이어서 호출됨 (MRO에 따라)

10. 에러와 예외, 디버깅

10-1. 버그와 디버깅

  • 버그: 프로그램이 원래 의도와 다르게 동작하게 만드는 오류
  • 디버깅: 그 오류를 찾고 고치는 과정
    • print 찍어보기
    • IDE의 디버거 사용
    • 에러 메세지 꼼꼼히 읽기 등등

10-2. 문법 오류(Syntax Error) vs 예외(Exception)

  1. 문법 오류
    • 파이썬이 코드를 읽는 단계에서
      “이건 문법에 맞지 않는다” 하고 바로 중단하는 오류.
    • 예: 괄호 안 닫힘, if 뒤에 : 빠짐 등.
  2. 예외(Exception)
    • 문법은 맞지만, 실행 중에 문제가 생겼을 때 발생.
    • 예: 0으로 나누기, 리스트 인덱스 범위 초과, 존재하지 않는 키 접근 등.

10-3. 자주 보는 내장 예외들

  • ZeroDivisionError : 0으로 나눴을 때
  • NameError : 정의되지 않은 이름 사용
  • TypeError : 타입이 맞지 않을 때 (예: 숫자 + 문자열)
  • ValueError : 타입은 맞는데 값이 이상할 때 (예: int("abc"))
  • IndexError : 리스트/튜플 인덱스 범위 벗어남
  • KeyError : 딕셔너리에 없는 키를 조회할 때
  • ImportError, ModuleNotFoundError : 잘못된 모듈 import 시도
  • IndentationError : 들여쓰기 잘못했을 때 등.

11. 예외 처리 – try / except / else / finally

11-1. 기본 구조

try:
    # 에러(예외)가 발생할 수 있는 코드
    result = 10 / x
except ZeroDivisionError:
    # 위에서 지정한 예외가 발생했을 때 실행할 코드
    print("0으로 나눌 수 없습니다.")
  • try 블록 안에서 ZeroDivisionError가 일어나면
    프로그램이 바로 죽지 않고, except로 흐름이 넘어간다.

11-2. 여러 예외 처리

try:
    num = int(input("100을 나눌 숫자를 입력하세요: "))
    print(100 / num)
except ZeroDivisionError:
    print("0으로는 나눌 수 없습니다.")
except ValueError:
    print("정수를 입력해야 합니다.")
  • 서로 다른 예외를 각각 다른 방식으로 처리하고 싶으면 except를 여러 개 둔다.

11-3. else와 finally

try:
    num = int(input("100을 나눌 숫자를 입력하세요: "))
    result = 100 / num
except (ZeroDivisionError, ValueError):
    print("입력이 잘못되었습니다.")
else:
    print("결과:", result)    # 예외가 하나도 없었을 때만 실행
finally:
    print("프로그램을 종료합니다.")  # 예외와 상관 없이 항상 실행
  • else : 예외가 발생하지 않았을 때만 실행
  • finally : 예외 여부와 상관 없이 항상 실행 (파일 닫기, DB 연결 종료 등에 많이 사용)

11-4. 예외 객체 다루기

my_list = [1, 2, 3]

try:
    print(my_list[5])
except IndexError as error:
    print("에러 타입:", type(error))
    print("에러 메시지:", error)
  • as error 로 예외 객체를 받아서,
    에러 메시지나 타입을 확인할 수 있다.

12. EAFP vs LBYL

12-1. 두 가지 스타일

  1. EAFP (Easier to Ask Forgiveness than Permission)
    • “먼저 시도해 보고, 안 되면 사과(예외 처리)하자”
    • 파이썬스러운 스타일 (try / except 중심)
# EAFP 스타일
try:
    value = my_dict['key']
except KeyError:
    print("키가 없습니다.")
  1. LBYL (Look Before You Leap)
    • “하기 전에 먼저 조건을 꼼꼼히 검사하자”
    • if로 다 체크한 후 실행하는 스타일
# LBYL 스타일
if 'key' in my_dict:
    value = my_dict['key']
else:
    print("키가 없습니다.")
  • 파이썬에서는 보통 EAFP 스타일이 더 자연스럽다고 많이들 말한다.
  • 하지만 상황에 따라 LBYL이 더 읽기 좋을 때도 있으니 둘 다 알고 쓰면 된다.

오늘 복습

  • 객체, 클래스, 인스턴스, 속성, 메서드
  • 인스턴스 변수 vs 클래스 변수
  • 인스턴스 / 클래스 / 정적 메서드, 생성자 __init__
  • 상속, 메서드 오버라이딩, 다중 상속, MRO, super()
  • 버그, 디버깅, 문법 오류 vs 예외
  • 내장 예외 종류들
  • try / except / else / finally 구조
  • EAFP vs LBYL
    \

'Language& Framework > Python' 카테고리의 다른 글

Data Structure  (0) 2025.12.12
Functions. & Module Control of Flow  (0) 2025.12.12
Python Basic Syntax  (0) 2025.12.11
myoskin