본문으로 건너뛰기

고급 OOP

상속 (Inheritance)

기본 상속

# 부모 클래스 (기반 클래스, 슈퍼 클래스)
class Animal:
def __init__(self, name):
self.name = name

def speak(self):
return "소리를 냅니다"

def move(self):
return f"{self.name}가 이동합니다"

# 자식 클래스 (파생 클래스, 서브 클래스)
class Dog(Animal):
def speak(self):
return "멍멍!"

class Cat(Animal):
def speak(self):
return "야옹~"

# 사용
dog = Dog("바둑이")
cat = Cat("나비")

print(dog.name) # 바둑이 (상속받은 속성)
print(dog.speak()) # 멍멍! (오버라이딩)
print(dog.move()) # 바둑이가 이동합니다 (상속받은 메서드)

print(cat.speak()) # 야옹~
print(cat.move()) # 나비가 이동합니다

super()로 부모 메서드 호출

class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model

def info(self):
return f"{self.brand} {self.model}"

class Car(Vehicle):
def __init__(self, brand, model, doors):
# 부모 클래스의 __init__ 호출
super().__init__(brand, model)
self.doors = doors

def info(self):
# 부모의 info() 호출 + 추가 정보
base_info = super().info()
return f"{base_info} ({self.doors}도어)"

car = Car("현대", "소나타", 4)
print(car.info()) # 현대 소나타 (4도어)

# super() 없이 부모 메서드 호출
class Truck(Vehicle):
def __init__(self, brand, model, capacity):
Vehicle.__init__(self, brand, model) # 명시적 호출
self.capacity = capacity

메서드 오버라이딩

class Shape:
def __init__(self, color):
self.color = color

def area(self):
"""넓이 계산 (자식 클래스에서 구현)"""
raise NotImplementedError("서브클래스에서 구현해야 합니다")

def describe(self):
return f"{self.color} 도형"

class Circle(Shape):
def __init__(self, color, radius):
super().__init__(color)
self.radius = radius

def area(self):
"""원의 넓이"""
return 3.14 * self.radius ** 2

def describe(self):
"""부모 메서드 확장"""
base = super().describe()
return f"{base} (반지름: {self.radius})"

class Rectangle(Shape):
def __init__(self, color, width, height):
super().__init__(color)
self.width = width
self.height = height

def area(self):
"""직사각형 넓이"""
return self.width * self.height

# 사용
circle = Circle("빨강", 5)
rectangle = Rectangle("파랑", 4, 6)

print(circle.describe()) # 빨강 도형 (반지름: 5)
print(f"넓이: {circle.area()}") # 넓이: 78.5

print(rectangle.describe()) # 파랑 도형
print(f"넓이: {rectangle.area()}") # 넓이: 24

다중 상속

class Flyable:
def fly(self):
return "날아갑니다"

class Swimmable:
def swim(self):
return "헤엄칩니다"

class Duck(Animal, Flyable, Swimmable):
def speak(self):
return "꽥꽥!"

duck = Duck("도널드")
print(duck.speak()) # 꽥꽥!
print(duck.fly()) # 날아갑니다
print(duck.swim()) # 헤엄칩니다
print(duck.move()) # 도널드가 이동합니다

# 다중 상속 - 믹스인 패턴
class LoggerMixin:
def log(self, message):
print(f"[{self.__class__.__name__}] {message}")

class TimestampMixin:
def get_timestamp(self):
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

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

def login(self):
timestamp = self.get_timestamp()
self.log(f"{self.name} 로그인 ({timestamp})")

user = User("홍길동")
user.login() # [User] 홍길동 로그인 (2024-12-01 14:30:00)

MRO (Method Resolution Order)

class A:
def method(self):
return "A"

class B(A):
def method(self):
return "B"

class C(A):
def method(self):
return "C"

class D(B, C):
pass

# 메서드 해석 순서 확인
print(D.mro())
# [D, B, C, A, object]

d = D()
print(d.method()) # B (왼쪽 우선)

# 다이아몬드 상속 문제 해결
class A:
def __init__(self):
print("A.__init__")
super().__init__()

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

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

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

d = D()
# D.__init__
# B.__init__
# C.__init__
# A.__init__ (한 번만 호출!)

다형성 (Polymorphism)

메서드 오버라이딩을 통한 다형성

class Payment:
def process(self, amount):
raise NotImplementedError

class CreditCard(Payment):
def process(self, amount):
return f"신용카드로 {amount:,}원 결제"

class BankTransfer(Payment):
def process(self, amount):
return f"계좌이체로 {amount:,}원 결제"

class PayPal(Payment):
def process(self, amount):
return f"PayPal로 {amount:,}원 결제"

# 다형성: 같은 인터페이스, 다른 동작
def checkout(payment_method, amount):
"""결제 처리"""
print(payment_method.process(amount))

# 어떤 결제 수단이든 같은 방식으로 호출
checkout(CreditCard(), 50000) # 신용카드로 50,000원 결제
checkout(BankTransfer(), 30000) # 계좌이체로 30,000원 결제
checkout(PayPal(), 20000) # PayPal로 20,000원 결제

덕 타이핑 (Duck Typing)

# "오리처럼 걷고, 오리처럼 운다면, 그것은 오리다"

class Dog:
def speak(self):
return "멍멍!"

class Cat:
def speak(self):
return "야옹~"

class Robot:
def speak(self):
return "삐빅!"

# 상속 관계 없어도 같은 메서드만 있으면 OK
def make_speak(animal):
print(animal.speak())

make_speak(Dog()) # 멍멍!
make_speak(Cat()) # 야옹~
make_speak(Robot()) # 삐빅!

# 리스트처럼 동작하는 객체
class MyList:
def __init__(self):
self.items = []

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

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

def __getitem__(self, index):
return self.items[index]

my_list = MyList()
my_list.append(1)
my_list.append(2)

print(len(my_list)) # 2
print(my_list[0]) # 1

# 리스트처럼 사용 가능
for item in my_list:
print(item)

매직 메서드 (Magic Methods)

문자열 표현

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

def __str__(self):
"""str() - 사용자를 위한 문자열"""
return f"{self.name} ({self.age}세)"

def __repr__(self):
"""repr() - 개발자를 위한 문자열"""
return f"Person(name='{self.name}', age={self.age})"

person = Person("홍길동", 25)

print(str(person)) # 홍길동 (25세)
print(repr(person)) # Person(name='홍길동', age=25)
print(person) # 홍길동 (25세) (__str__ 우선)

# 리스트에 담으면 __repr__ 사용
people = [Person("홍길동", 25), Person("김철수", 30)]
print(people)
# [Person(name='홍길동', age=25), Person(name='김철수', age=30)]

연산자 오버로딩

class Vector:
def __init__(self, x, y):
self.x = x
self.y = y

def __str__(self):
return f"Vector({self.x}, {self.y})"

def __add__(self, other):
"""덧셈: +"""
return Vector(self.x + other.x, self.y + other.y)

def __sub__(self, other):
"""뺄셈: -"""
return Vector(self.x - other.x, self.y - other.y)

def __mul__(self, scalar):
"""스칼라 곱: *"""
return Vector(self.x * scalar, self.y * scalar)

def __eq__(self, other):
"""같음 비교: =="""
return self.x == other.x and self.y == other.y

def __abs__(self):
"""절댓값: abs()"""
return (self.x ** 2 + self.y ** 2) ** 0.5

v1 = Vector(2, 3)
v2 = Vector(1, 4)

print(v1 + v2) # Vector(3, 7)
print(v1 - v2) # Vector(1, -1)
print(v1 * 3) # Vector(6, 9)
print(v1 == v2) # False
print(abs(v1)) # 3.605551275463989

# 더 많은 연산자
class Money:
def __init__(self, amount):
self.amount = amount

def __str__(self):
return f"{self.amount:,}원"

def __add__(self, other):
return Money(self.amount + other.amount)

def __lt__(self, other):
"""작음: <"""
return self.amount < other.amount

def __le__(self, other):
"""작거나 같음: <="""
return self.amount <= other.amount

def __gt__(self, other):
"""큼: >"""
return self.amount > other.amount

def __ge__(self, other):
"""크거나 같음: >="""
return self.amount >= other.amount

m1 = Money(10000)
m2 = Money(20000)

print(m1 + m2) # 30,000원
print(m1 < m2) # True
print(m1 > m2) # False

컨테이너 메서드

class Playlist:
def __init__(self):
self.songs = []

def __len__(self):
"""len() 지원"""
return len(self.songs)

def __getitem__(self, index):
"""인덱싱: playlist[i]"""
return self.songs[index]

def __setitem__(self, index, value):
"""할당: playlist[i] = song"""
self.songs[index] = value

def __delitem__(self, index):
"""삭제: del playlist[i]"""
del self.songs[index]

def __contains__(self, song):
"""포함 여부: song in playlist"""
return song in self.songs

def __iter__(self):
"""반복 가능: for song in playlist"""
return iter(self.songs)

def append(self, song):
"""노래 추가"""
self.songs.append(song)

# 사용
playlist = Playlist()
playlist.append("Song 1")
playlist.append("Song 2")
playlist.append("Song 3")

print(len(playlist)) # 3
print(playlist[0]) # Song 1
print("Song 2" in playlist) # True

playlist[1] = "New Song 2"
print(playlist[1]) # New Song 2

# for 문 사용 가능
for song in playlist:
print(song)

del playlist[0]
print(len(playlist)) # 2

호출 가능 객체

class Counter:
def __init__(self):
self.count = 0

def __call__(self):
"""객체를 함수처럼 호출"""
self.count += 1
return self.count

counter = Counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3

# 실용 예제: 커링(Currying)
class Multiplier:
def __init__(self, factor):
self.factor = factor

def __call__(self, x):
return x * self.factor

double = Multiplier(2)
triple = Multiplier(3)

print(double(5)) # 10
print(triple(5)) # 15

컨텍스트 매니저

class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None

def __enter__(self):
"""with 문 시작"""
print(f"{self.filename} 열기")
self.file = open(self.filename, self.mode)
return self.file

def __exit__(self, exc_type, exc_val, exc_tb):
"""with 문 종료"""
if self.file:
self.file.close()
print(f"{self.filename} 닫기")
return False # 예외를 다시 발생시킴

# with 문으로 사용
with FileManager('test.txt', 'w') as f:
f.write('Hello, World!')
# 자동으로 파일 닫힘

# 실용 예제: 타이머
import time

class Timer:
def __enter__(self):
self.start = time.time()
return self

def __exit__(self, *args):
self.end = time.time()
self.elapsed = self.end - self.start
print(f"실행 시간: {self.elapsed:.4f}초")

with Timer():
# 시간 측정할 코드
total = sum(range(1000000))
# 실행 시간: 0.0234초

추상 클래스 (Abstract Base Class)

abc 모듈 사용

from abc import ABC, abstractmethod

class Shape(ABC):
"""추상 클래스"""

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

@abstractmethod
def area(self):
"""넓이 (반드시 구현해야 함)"""
pass

@abstractmethod
def perimeter(self):
"""둘레 (반드시 구현해야 함)"""
pass

def describe(self):
"""일반 메서드 (선택적)"""
return f"{self.color} 도형"

# shape = Shape("빨강") # ❌ TypeError: 추상 클래스는 인스턴스화 불가

class Circle(Shape):
def __init__(self, color, radius):
super().__init__(color)
self.radius = radius

def area(self):
return 3.14 * self.radius ** 2

def perimeter(self):
return 2 * 3.14 * self.radius

class Rectangle(Shape):
def __init__(self, color, width, height):
super().__init__(color)
self.width = width
self.height = height

def area(self):
return self.width * self.height

def perimeter(self):
return 2 * (self.width + self.height)

# 사용
circle = Circle("빨강", 5)
rectangle = Rectangle("파랑", 4, 6)

print(f"{circle.describe()}: 넓이 {circle.area()}")
print(f"{rectangle.describe()}: 둘레 {rectangle.perimeter()}")

# 다형성
shapes = [Circle("빨강", 5), Rectangle("파랑", 4, 6)]
for shape in shapes:
print(f"넓이: {shape.area()}")

인터페이스 패턴

from abc import ABC, abstractmethod

class DataSource(ABC):
"""데이터 소스 인터페이스"""

@abstractmethod
def read(self):
pass

@abstractmethod
def write(self, data):
pass

class FileDataSource(DataSource):
def __init__(self, filename):
self.filename = filename

def read(self):
with open(self.filename, 'r') as f:
return f.read()

def write(self, data):
with open(self.filename, 'w') as f:
f.write(data)

class DatabaseDataSource(DataSource):
def __init__(self, connection_string):
self.connection_string = connection_string

def read(self):
# 데이터베이스에서 읽기
return "Data from database"

def write(self, data):
# 데이터베이스에 쓰기
print(f"Writing to database: {data}")

# 같은 인터페이스로 다른 데이터 소스 사용
def process_data(data_source: DataSource):
data = data_source.read()
# 데이터 처리
processed = data.upper()
data_source.write(processed)

property 심화

getter, setter, deleter

class Temperature:
def __init__(self, celsius):
self._celsius = celsius

@property
def celsius(self):
"""섭씨 온도 조회"""
print("celsius getter 호출")
return self._celsius

@celsius.setter
def celsius(self, value):
"""섭씨 온도 설정"""
print("celsius setter 호출")
if value < -273.15:
raise ValueError("절대영도보다 낮을 수 없습니다")
self._celsius = value

@celsius.deleter
def celsius(self):
"""섭씨 온도 삭제"""
print("celsius deleter 호출")
del self._celsius

@property
def fahrenheit(self):
"""화씨 온도 (계산됨)"""
return self._celsius * 9/5 + 32

@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9

temp = Temperature(25)
print(temp.celsius) # celsius getter 호출 -> 25
temp.celsius = 30 # celsius setter 호출
del temp.celsius # celsius deleter 호출

계산된 속성

class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height

@property
def area(self):
"""넓이 (읽기 전용)"""
return self.width * self.height

@property
def perimeter(self):
"""둘레 (읽기 전용)"""
return 2 * (self.width + self.height)

@property
def diagonal(self):
"""대각선 (읽기 전용)"""
return (self.width ** 2 + self.height ** 2) ** 0.5

rect = Rectangle(3, 4)
print(rect.area) # 12
print(rect.perimeter) # 14
print(rect.diagonal) # 5.0

rect.width = 6
print(rect.area) # 24 (자동 재계산)

실전 예제

직원 관리 시스템

from abc import ABC, abstractmethod
from datetime import datetime

class Employee(ABC):
"""직원 추상 클래스"""

employee_count = 0

def __init__(self, name, id):
self.name = name
self.id = id
self.hire_date = datetime.now()
Employee.employee_count += 1

@abstractmethod
def calculate_salary(self):
"""급여 계산 (자식 클래스에서 구현)"""
pass

def __str__(self):
return f"{self.name} ({self.id})"

def __repr__(self):
return f"{self.__class__.__name__}('{self.name}', '{self.id}')"


class FullTimeEmployee(Employee):
"""정규직"""

def __init__(self, name, id, monthly_salary):
super().__init__(name, id)
self.monthly_salary = monthly_salary

def calculate_salary(self):
return self.monthly_salary


class PartTimeEmployee(Employee):
"""파트타임"""

def __init__(self, name, id, hourly_rate):
super().__init__(name, id)
self.hourly_rate = hourly_rate
self.hours_worked = 0

def log_hours(self, hours):
"""근무 시간 기록"""
self.hours_worked += hours

def calculate_salary(self):
return self.hourly_rate * self.hours_worked

def reset_hours(self):
"""근무 시간 초기화"""
self.hours_worked = 0


class Contractor(Employee):
"""계약직"""

def __init__(self, name, id, project_fee):
super().__init__(name, id)
self.project_fee = project_fee
self.projects_completed = 0

def complete_project(self):
"""프로젝트 완료"""
self.projects_completed += 1

def calculate_salary(self):
return self.project_fee * self.projects_completed


class Company:
"""회사"""

def __init__(self, name):
self.name = name
self.employees = []

def hire(self, employee):
"""직원 고용"""
self.employees.append(employee)
print(f"{employee.name}님을 고용했습니다")

def fire(self, employee):
"""직원 해고"""
if employee in self.employees:
self.employees.remove(employee)
print(f"{employee.name}님을 해고했습니다")

def calculate_total_payroll(self):
"""총 급여"""
return sum(emp.calculate_salary() for emp in self.employees)

def print_payroll(self):
"""급여 명세서"""
print(f"\n{'='*50}")
print(f"{self.name} 급여 명세서")
print(f"{'='*50}")

for emp in self.employees:
salary = emp.calculate_salary()
print(f"{emp.name:15} {emp.__class__.__name__:20} {salary:>10,}원")

print(f"{'-'*50}")
print(f"{'총 급여':35} {self.calculate_total_payroll():>10,}원")
print(f"{'='*50}\n")

# 사용
company = Company("파이썬 주식회사")

# 직원 고용
emp1 = FullTimeEmployee("홍길동", "FT001", 3000000)
emp2 = FullTimeEmployee("김철수", "FT002", 3500000)
emp3 = PartTimeEmployee("이영희", "PT001", 15000)
emp4 = Contractor("박민수", "CT001", 2000000)

company.hire(emp1)
company.hire(emp2)
company.hire(emp3)
company.hire(emp4)

# 근무 기록
emp3.log_hours(80) # 80시간 근무
emp4.complete_project() # 프로젝트 1개 완료

# 급여 계산
company.print_payroll()

도형 계층 구조

from abc import ABC, abstractmethod
import math

class Shape(ABC):
"""도형 추상 클래스"""

def __init__(self, color="black"):
self._color = color

@property
def color(self):
return self._color

@color.setter
def color(self, value):
self._color = value

@abstractmethod
def area(self):
pass

@abstractmethod
def perimeter(self):
pass

def __str__(self):
return f"{self.color} {self.__class__.__name__}"

def __lt__(self, other):
"""넓이로 비교"""
return self.area() < other.area()

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


class Circle(Shape):
def __init__(self, radius, color="black"):
super().__init__(color)
self._radius = radius

@property
def radius(self):
return self._radius

@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("반지름은 양수여야 합니다")
self._radius = value

def area(self):
return math.pi * self._radius ** 2

def perimeter(self):
return 2 * math.pi * self._radius

@property
def diameter(self):
return 2 * self._radius


class Rectangle(Shape):
def __init__(self, width, height, color="black"):
super().__init__(color)
self._width = width
self._height = height

@property
def width(self):
return self._width

@width.setter
def width(self, value):
if value <= 0:
raise ValueError("너비는 양수여야 합니다")
self._width = value

@property
def height(self):
return self._height

@height.setter
def height(self, value):
if value <= 0:
raise ValueError("높이는 양수여야 합니다")
self._height = value

def area(self):
return self._width * self._height

def perimeter(self):
return 2 * (self._width + self._height)

def is_square(self):
return self._width == self._height


class Triangle(Shape):
def __init__(self, a, b, c, color="black"):
super().__init__(color)
if a + b <= c or b + c <= a or c + a <= b:
raise ValueError("유효한 삼각형이 아닙니다")
self.a = a
self.b = b
self.c = c

def area(self):
# 헤론의 공식
s = self.perimeter() / 2
return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))

def perimeter(self):
return self.a + self.b + self.c


# 사용
shapes = [
Circle(5, "빨강"),
Rectangle(4, 6, "파랑"),
Triangle(3, 4, 5, "초록"),
Circle(3, "노랑")
]

# 정렬 (넓이 기준)
shapes.sort()

print("넓이 순 정렬:")
for shape in shapes:
print(f"{shape}: 넓이={shape.area():.2f}, 둘레={shape.perimeter():.2f}")

# 가장 큰 도형
largest = max(shapes)
print(f"\n가장 큰 도형: {largest} (넓이: {largest.area():.2f})")

자주 묻는 질문

Q1. 상속은 언제 사용해야 하나요?

A: "is-a" 관계일 때 사용하세요

# ✅ 좋은 예: Dog "is a" Animal
class Animal:
pass

class Dog(Animal):
pass

# ❌ 나쁜 예: Car "has a" Engine (컴포지션 사용)
class Engine:
pass

class Car:
def __init__(self):
self.engine = Engine() # 상속 대신 포함

Q2. 다중 상속은 언제 사용하나요?

A: 믹스인 패턴으로 기능을 추가할 때

# 믹스인: 재사용 가능한 기능 조각
class JSONMixin:
def to_json(self):
import json
return json.dumps(self.__dict__)

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

user = User("홍길동", "hong@example.com")
print(user.to_json())
# {"name": "홍길동", "email": "hong@example.com"}

Q3. 추상 클래스와 인터페이스의 차이는?

A: Python에서는 같은 개념 (abc 모듈 사용)

from abc import ABC, abstractmethod

# 추상 클래스 (일부 구현 포함 가능)
class Animal(ABC):
def __init__(self, name):
self.name = name # 공통 속성

@abstractmethod
def speak(self):
pass # 반드시 구현

def move(self):
return f"{self.name}가 이동합니다" # 선택적

# 인터페이스 (모든 메서드가 추상)
class Drawable(ABC):
@abstractmethod
def draw(self):
pass

@abstractmethod
def erase(self):
pass

Q4. __str__과 __repr__의 차이는?

A: __str__은 사용자용, __repr__은 개발자용

class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def __str__(self):
# 사용자 친화적
return f"({self.x}, {self.y})"

def __repr__(self):
# 재생성 가능한 형태
return f"Point({self.x}, {self.y})"

p = Point(3, 4)
print(str(p)) # (3, 4)
print(repr(p)) # Point(3, 4)

# eval()로 재생성 가능
p2 = eval(repr(p))
print(p2) # (3, 4)

다음 단계

고급 OOP를 마스터했습니다!

핵심 정리:
✅ 상속과 super()
✅ 다형성과 덕 타이핑
✅ 매직 메서드로 연산자 오버로딩
✅ 추상 클래스와 인터페이스
✅ property로 속성 관리
✅ 실전 예제 (직원 관리, 도형 계층)

다음 단계: 타입 힌팅에서 타입 힌트로 코드를 더 명확하게 작성해보세요!