상속과 합성: 클래스 관계 설계의 두 가지 축

Inheritance and Composition in Python OOP

객체지향 프로그래밍(OOP)은 복잡한 현실 세계를 소프트웨어로 모델링하기 위한 사고 체계다. 이 글에서는 OOP가 등장한 배경부터 클래스와 객체의 관계, 멤버 변수와 메소드의 동작 원리, 그리고 상속, 추상화, 캡슐화, 다형성이라는 4대 원칙을 다룬다. 마지막으로 은행 계좌 관리 시스템이라는 실전 프로젝트를 통해 모든 개념을 통합 적용한다.

Engineering
Python
저자

Kwangmin Kim

공개

2023년 07월 02일

1 개요

이 글은 Python 객체지향 프로그래밍의 전체 흐름을 다룬다:

  1. OOP의 등장 배경 - 왜 객체지향이 필요한가
  2. 객체와 클래스 - 설계도와 실체의 관계
  3. 멤버 변수와 메소드 - 생성자와 self의 동작 원리
  4. 상속과 추상화 - 코드 재사용과 설계 계약
  5. 캡슐화와 다형성 - 데이터 보호와 인터페이스 일관성
  6. 실전 프로젝트 - 은행 계좌 관리 시스템 구현

2 OOP의 등장 배경

객체지향 프로그래밍(Object-oriented Programming, OOP) 은 복잡한 현실 세계를 소프트웨어로 모델링하기 위한 사고 체계다.

2.1 패러다임이란 무엇인가

프로그래밍 패러다임(Programming Paradigm) 은 “어떤 방식으로 문제를 바라보고 해결할 것인가”에 대한 사고 체계다.

절차지향 vs 객체지향
구분 절차지향 객체지향
관점 순서대로 실행되는 명령들의 집합 데이터와 코드를 객체로 묶음
데이터/함수 변수와 함수가 분리되어 산재 객체 안에 함께 캡슐화
대표 언어 C Java, Python, C++
장점 간단한 프로그램에 직관적 대규모 시스템에 유지보수 용이
단점 규모 커지면 스파게티 코드 발생 초기 설계 비용이 높음

Wikipedia는 OOP를 다음과 같이 정의한다.

Object-oriented programming (OOP) is a programming paradigm based on the concept of objects,
which can contain data in the form of fields (often known as attributes or properties),
and code in the form of procedures (often known as methods).

핵심은 데이터(data)와 코드(code)를 하나로 묶는다는 것이다.
* 절차지향에서는 데이터와 함수가 분리되어 있다.
* 변수는 여기 있고, 그 변수를 처리하는 함수는 저기 있다.
* OOP는 이 둘을 하나의 단위, 즉 객체(Object) 안에 함께 가두어 놓는다. * 즉, 객체는 자신이 가진 데이터(속성)와 그 데이터를 처리하는 코드(메소드)를 함께 가지고 있다.
* 자동차 객체는 색상, 속도, 연료량이라는 속성을 가지고 있고, 출발하기, 멈추기, 주유하기라는 메소드를 가지고 있다.
* 사람 객체는 이름, 나이, 직업이라는 속성을 가지고 있고, 말하기, 걷기, 일하기라는 메소드를 가지고 있다. * 코드를 읽는 사람도, 수정하는 사람도 해당 객체만 들여다보면 된다.

2.2 OOP가 선택된 이유

OOP가 널리 채택된 가장 중요한 이유:

  • 인간이 세상을 인식하는 방식과 유사하다
    • 인간은 세상을 “사물”과 “관계”로 인식
    • 사물 = 속성 + 행동
사물 속성 행동
자동차 색상, 속도, 연료량 출발하기, 멈추기, 주유하기
사람 이름, 나이, 직업 말하기, 걷기, 일하기
  • OOP는 이 사고 방식을 코드에 그대로 이식
  • car.accelerate(50) → “자동차를 50으로 가속시킨다”는 의도가 주석 없이 전달됨

2.3 OOP의 네 가지 장점과 그 실질적 의미

2.3.1 실제 세계의 모델링에 적합하다.

  • 비즈니스 도메인을 코드로 옮기는 작업이 자연스러워진다.
  • 은행 시스템을 만든다면 “계좌”, “고객”, “거래” 라는 개념이 그대로 클래스가 된다.
  • 따라서, 요구사항 문서와 코드 사이의 간극이 줄어든다.

2.3.2 개념의 정의와 재사용이 자유롭다.

  • 한 번 정의한 “계좌” 클래스는 예금계좌에도, 입출금계좌에도 재활용할 수 있다.
  • 공통 기능은 부모 클래스에 한 번만 작성하고,
  • 자식 클래스는 차이점만 추가로 정의한다.
  • 이것이 상속(Inheritance) 의 실질적 가치다.

2.3.3 유지보수가 쉽다.

  • “상품” 객체의 가격 계산 방식을 변경해야 한다면,
  • Product 클래스의 해당 메소드 하나만 수정하면 된다.
  • 이 클래스로 만들어진 모든 상품 객체에 변경사항이 자동으로 반영된다.
  • 절차지향이었다면 가격 계산 로직이 코드 전체에 흩어져 있을 수 있으므로,
  • 수정 누락으로 인한 버그가 발생할 위험이 크다.

2.3.4 캡슐화를 통한 데이터 보호

  • 객체 내부의 데이터를 외부에서 직접 건드리지 못하게 막을 수 있다.
  • 자동차의 속도를 외부에서 car.speed = -100 처럼 직접 설정하는 것은 현실적으로 말이 안 된다.
  • OOP는 car.drive(-100) 처럼 메소드를 통해서만 속성을 변경하도록 강제함으로써 데이터의 무결성을 지킨다.
  • 이것이 캡슐화(Encapsulation) 의 핵심이다.

2.4 Python과 OOP

Python = 멀티 패러다임(Multi-paradigm) 언어

  • 순수 OOP 언어(Java): 모든 코드를 반드시 클래스 안에 작성
  • Python: OOP + 함수형 + 절차지향 모두 지원

데이터 과학자를 위한 실용적 조언:

상황 권장 방식
간단한 데이터 처리 스크립트 함수 위주, 클래스 불필요
재사용 가능한 전처리 파이프라인 OOP 설계 권장
모델 래퍼(wrapper) OOP 설계 권장
API 연동 모듈 OOP 설계 권장
Scikit-learn의 OOP 활용
from sklearn.linear_model import LogisticRegression

model = LogisticRegression()   # 객체 생성
model.fit(X_train, y_train)    # 메소드 호출
model.predict(X_test)          # 다형성: 모든 모델이 동일한 인터페이스

3 객체와 클래스

3.1 객체란 무엇인가

An object has state (data) and behavior (code).
Objects can correspond to things found in the real world.

객체의 핵심 구성:

  • 상태(state) = 멤버 변수(member variable) / 속성(attribute)
    • Python에서는 보통 속성(attribute) 이라고 부르는 게 더 자연스럽다:
용어 사용 맥락 설명
멤버 변수 (member variable) 일반 OOP / C++/Java 스타일 “클래스의 구성원(member)인 변수”
속성 (attribute) Python 특화 용어 “객체가 가진 특성(attribute)”
필드 (field) Java/C# 스타일 멤버 변수와 동일
인스턴스 변수 (instance variable) Python 기술 용어 인스턴스마다 독립적인 변수임을 강조
class Person:
    def __init__(self, name):
        self.name = name  # Python에선 "속성", 일반 OOP에선 "멤버 변수"라 부른다
  • 행동(behavior) = 메소드(method)
객체 상태(속성) 행동(메소드)
자동차 속도, 연료량, 색상 start(), stop(), refuel()
강아지 이름, 나이, 품종 bark(), run(), eat()
주문 주문번호, 금액, 상태 confirm(), cancel(), track()
객체는 물리적 사물만이 아니다

“주문”, “거래”, “세션”, “권한”과 같은 추상적 개념도 객체가 될 수 있다.
이것이 OOP가 비즈니스 도메인 모델링에 강력한 이유다.

3.2 쇼핑몰 예시로 보는 객체의 구체적 의미

쇼핑몰 상품 목록 화면을 예로 들자면,
* 화면에는 가방, 지갑, 캐리어 등 10개의 상품이 나열되어 있다.
* 각 상품은 상품명, 브랜드, 원래 가격, 할인 가격이라는 정보를 보여준다.
* 그리고 각 상품 카드에는 좋아요, 장바구니 담기, 구매하기 버튼이 있다.

OOP 관점에서 이 화면을 분석하면 다음과 같다.

  • 화면에 있는 상품 하나하나가 각각 하나의 객체다.
  • 상품명, 브랜드, 원래 가격, 할인 가격은 속성(attribute) 이다.
  • 좋아요, 장바구니 담기, 구매하기는 메소드(method) 다.
  • 각 상품은 서로 다른 이름과 가격을 가지지만, 속성의 종류는 모든 상품이 동일하다.
    • 가방도 상품명이 있고, 지갑도 상품명이 있다.
    • 가방에는 상품명이 있는데 캐리어에는 상품명이 없는 경우는 없다.
  • 모든 상품은 동일한 기능을 수행할 수 있다.
    • 가방에는 좋아요를 누를 수 있고, 지갑에도 좋아요를 누를 수 있다.
    • 반면 상품 객체에 “로그인하기”라는 기능은 정의되어 있지 않으므로,
    • 상품에 로그인을 시도하는 것은 불가능하다.
  • 이 마지막 로그인 포인트가 중요하다.
    • 객체는 자신에게 정의된 기능만 수행할 수 있다.
    • 이 제약이 코드를 예측 가능하게 만든다.
    • 어떤 객체가 무슨 일을 할 수 있는지가 명확하게 정의되어 있기 때문에,
    • 해당 객체를 사용하는 사람은 그 범위 안에서만 상호작용한다.
  • 또한 각 상품은 서로 독립적이다.
    • 1번 상품의 가격을 변경해도 2번 상품의 가격은 그대로다.
    • 하나의 객체에 가해진 변경이 다른 객체에 영향을 주지 않는다.
    • 이 독립성이 복잡한 시스템을 관리 가능하게 만드는 핵심 원리다.

3.3 클래스란 무엇인가

A class is a program-code-template for creating objects.

3.3.1 클래스 = 객체를 찍어내는 틀(template) = 건축 설계도

비유 설계도(클래스) 건물(객체)
설계도 자체는 건물이 아니다 Product 클래스 아직 객체 없음
설계도로 건물을 짓는다 Product("가방", 79000) 가방 객체 생성
하나의 설계도로 여러 건물 같은 클래스로 여러 인스턴스
한 건물 리모델링 ≠ 다른 건물 영향 객체 변경이 다른 객체에 영향 없음
  • 클래스(Class): 객체를 만들기 위한 설계도, 개념의 정의
  • 객체/인스턴스(Object/Instance): 클래스로부터 실제로 생성된 구체적인 실체
    • “객체” = 독립적인 실체 강조 (개념적/실체적 관점)
    • “인스턴스” = 특정 클래스로부터 생성되었다는 관계 강조 (기술적/관계적 관점)

3.4 클래스와 인스턴스의 관계

원칙 설명
클래스 = 설계도 클래스 자체는 데이터가 없고, 속성과 기능의 정의만 존재
인스턴스별 고유 속성값 같은 클래스로 만든 객체들도 각각 다른 데이터를 가질 수 있음
인스턴스 간 독립성 한 인스턴스 변경이 다른 인스턴스에 영향 없음
클래스 변경 = 전체 반영 클래스 메소드 수정 시 모든 인스턴스에 적용
class Person: # 매개변수(parameter)
    def __init__(self, name, age):
        self.name = name   # 인스턴스마다 다른 값
        self.age = age
    
    def hello(self):
        print(f"안녕하세요, {self.name}입니다.")

# 인스턴스 생성: 인자(argument)
man = Person("Robert", 30)    # man.name = "Robert"
woman = Person("Julia", 32)   # woman.name = "Julia"

man.age = 31                  # man만 변경, woman.age는 여전히 32

3.5 Python에서 클래스 정의하기

class Person:
    def hello(self):
        print("Hello!")
man = Person()  # Person 클래스의 인스턴스 생성

핵심 문법:

요소 설명
class 키워드 클래스 정의 시작
클래스 이름 PascalCase 사용 (Person, BankAccount)
self 메소드의 첫 번째 매개변수, 호출한 인스턴스 자기 자신

self의 동작 원리:

  • man.hello() 메서드 호출 시 → Python 내부에서 Person.hello(man)으로 처리
  • selfman 인스턴스 자신이 직접 자동 전달됨
  • 개발자가 self를 직접 전달할 필요 없음

인스턴스 생성과 사용:

man = Person()      # 인스턴스 생성
woman = Person()    # 별도의 독립된 인스턴스

man.hello()         # hello() 메소드 호출, Hello! 출력

3.6 클래스 설계 시 고려할 사항

단일 책임 원칙(Single Responsibility Principle):

  • 하나의 클래스는 하나의 개념, 하나의 역할만 담당
  • UserAuthenticationAndEmailSendingAndLogging (세 가지 역할 혼합)
  • UserAuth, EmailSender, Logger (역할별 분리)

속성과 메소드 결정 기준:

  • 속성 = “특정 개념이 현실에서 가지는 것”
  • 메소드 = “특정 개념이 현실에서 하는 것”
  • 예: 상품 객체에 “로그인하기” 메소드가 없는 이유 → 현실에서 상품은 로그인 안 함

4 멤버 변수와 메소드

4.1 멤버 변수란 무엇인가

4.1.1 멤버 변수 = 객체의 데이터 = 상태(state)

  • 동의어: 속성(attribute), 필드(field), 인스턴스 변수(instance variable)
  • 인스턴스마다 독립적으로 존재 (man.name ≠ woman.name)

4.1.2 접근 방법:

위치 문법 예시
외부에서 . 연산자 man.name, man.age
메소드 내부 self.변수명 self.name, self.age
man = Person("Robert", 30)
print(man.name)   # 외부 접근: Robert

def hello(self):
    print(self.name)   # 내부 접근

4.2 생성자(init): 객체가 태어나는 순간

4.2.1 생성자(Constructor)

  • 생성자 = 인스턴스 생성 시 자동 호출되는 특수 메소드
    • Python에서는 __init__()이라는 이름으로 예약 (발음: dunder init)
    • __ (더블 언더스코어) = 던더 메소드(dunder method)
    • dunder score = “double underscore”의 줄임말

4.2.2 생성자의 역할:

역할 설명
멤버 변수 초기화 self.name = name으로 속성에 초기값 부여
설정 작업 수행 DB 연결, 파일 핸들 초기화, 기본값 설정 등
class Person:
    def __init__(self, name, age):   # 생성자
        self.name = name             # 멤버 변수 초기화
        self.age = age
    
    def hello(self):
        print(f"Hello! I'm {self.name}")

man = Person("Robert", 30)   # 생성자 자동 호출

Person("Robert", 30) 실행 시 내부 과정:

  1. Person 인스턴스를 위한 메모리 공간 할당
  2. __init__(self=<새 인스턴스 메모리 주소>, "Robert", 30) 자동 호출
  3. self.name = "Robert", self.age = 30 실행
  4. man = 새 인스턴스 메모리 주소 (완성된 인스턴스를 man 변수에 할당)

개발자는 Person("Robert", 30) 한 줄만 작성하지만,
Python이 이 네 단계를 자동으로 처리한다.
생성자가 이 과정을 담당하기 때문에
개발자는 “어떤 데이터로 초기화할 것인가”만 결정하면 된다.

4.3 self의 본질

인스턴스 자기 자신에 대한 참조

4.3.1 self = 메소드를 호출한 인스턴스 자신에 대한 참조

  • 메소드는 모든 인스턴스가 공유하는 코드
  • man.hello() vs woman.hello() 구분이 필요
  • self가 “이 메소드를 호출한 인스턴스”를 가리킴

4.3.2 내부 동작:

외부 호출 내부 변환
man.hello() Person.hello(man)
woman.hello() Person.hello(woman)
  • 첫 번째 인자를 받는 매개변수 이름: 관례적으로 self
  • this, me로 작성해도 동작하지만 Python 커뮤니티 규칙상 self 필수
man = Person("Robert", 30)
print(man.name)   # Robert
print(man.age)    # 30
print(man.job)    # AttributeError: 정의되지 않은 속성
경고

정의되지 않은 속성(job)에 접근하면 AttributeError 발생
→ 객체가 가질 수 있는 속성 범위는 클래스 정의에 의해 결정됨

4.4 메소드

  • 객체가 할 수 있는 행동의 정의
  • 메소드 = 클래스에 정의된 함수 = 인스턴스가 수행할 수 있는 행동

4.4.1 일반 함수와의 차이점:

구분 일반 함수 메소드
정의 위치 모듈 수준 클래스 블록 안
첫 번째 매개변수 없음 반드시 self

4.4.2 self를 통한 접근:

  • self.변수명: 인스턴스의 속성에 접근/수정
  • self.메소드명(): 같은 인스턴스의 다른 메소드 호출
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def hello(self):
        print("Hello! I'm " + self.name)

    def update_age(self, new_age):
        if new_age > 0:
            self.age = new_age
            print(f"I'm {self.age} years old now!")
        else:
            raise ValueError("나이는 0살보다 어릴 수 없습니다.")

man = Person("Robert", 30)
man.hello()           # Hello! I'm Robert
man.update_age(31)    # I'm 31 years old now!
man.update_age(-5)    # ValueError 발생
힌트

메소드를 통한 속성 변경의 핵심 가치:

  • update_age()는 단순 값 변경이 아닌 유효성 검증 포함
  • man.age = -5 (직접 접근) → 검증 없이 음수 설정됨
  • man.update_age(-5) (메소드 접근) → “나이는 양수” 규칙 강제
  • 이것이 캡슐화가 데이터 무결성을 보호한다는 의미

4.5 객체와 멤버 변수의 생명 주기

생명 주기(lifecycle) = 객체와 속성이 메모리에 언제 생성/소멸하는지

단계 시점 상태
생성 Person("Robert", 30) 호출 인스턴스 + 멤버 변수 메모리에 생성
상주 참조 변수(man)가 살아있는 동안 메모리에 계속 유지
소멸 스코프 벗어남 / del man 가비지 컬렉터가 메모리 해제

스코프 규칙: 일반 변수와 동일

def create_person():
    temp = Person("Temp", 25)
    temp.hello()   # Hello! I'm Temp
    # 함수가 끝나면 temp가 스코프를 벗어나 인스턴스 소멸

create_person()
# 이 시점에서 temp 인스턴스는 더 이상 존재하지 않음

반면 모듈 수준에서 생성된 인스턴스는 프로그램이 실행되는 동안 유지된다.
이 차이를 이해하지 못하면
“분명히 인스턴스를 만들었는데 데이터가 없다”는 버그를 만나게 된다.

4.6 인스턴스 변수와 클래스 변수의 차이

구분 인스턴스 변수 클래스 변수
소속 각 인스턴스 클래스 자체
공유 범위 인스턴스마다 독립 모든 인스턴스가 공유
정의 위치 __init__ 내부 클래스 블록 내 (메소드 외부)
접근 self.name ClassName.var 또는 self.var
class Person:
    species = "Homo sapiens"   # 클래스 변수: 모든 인스턴스가 공유

    def __init__(self, name, age):
        self.name = name       # 인스턴스 변수: 인스턴스마다 독립적
        self.age = age

man = Person("Robert", 30)
woman = Person("Julia", 32)

print(man.species)    # Homo sapiens
print(woman.species)  # Homo sapiens
print(Person.species) # Homo sapiens (클래스에서 직접 접근 가능)

Person.species = "Human"
print(man.species)    # Human (클래스 변수 변경이 모든 인스턴스에 반영)

클래스 변수 사용 예시:

  • 모든 인스턴스에 공통 적용되는 설정값
  • 인스턴스 생성 횟수 카운터
  • “클래스 전체에서 공유되어야 하는 데이터”
경고

주의: 인스턴스 변수와 클래스 변수를 혼동하면
의도치 않게 모든 인스턴스의 값이 동시에 바뀌는 버그 발생 가능

Scikit-learn 모든 모델이 멤버 변수 + 메소드 구조를 따름:

from sklearn.linear_model import LinearRegression

model = LinearRegression()   # 인스턴스 생성
model.fit(X_train, y_train)  # 메소드 호출 → self.coef_ 등 설정
predictions = model.predict(X_test)  # 메소드 호출

print(model.coef_)           # 멤버 변수 접근
print(model.intercept_)      # 멤버 변수 접근
노트

동적 속성 생성:

  • model.coef_fit() 실행 후에만 존재하는 멤버 변수
  • fit() 이전 접근 시 AttributeError 발생
  • 가능하면 생성자에서 모든 멤버 변수를 초기화하는 것이 좋은 설계 관례

5 상속과 추상화

개념 정의
상속 기존 클래스를 기반으로 새로운 클래스를 만드는 메커니즘
추상화 여러 클래스의 공통점을 추출해 상위 개념으로 정의

관계: 상속이 추상화를 구현하는 도구

5.1 상속이란 무엇인가

상속(Inheritance) = 기존 클래스(부모)의 속성/메소드를 새로운 클래스(자식)가 물려받는 메커니즘

핵심 이점: 코드 중복 제거

은행 시스템 예시:

구분 상속 없이 상속 사용 시
공통 로직 (계좌번호, 잔액, 입금, 출금) 예금계좌/입출금계좌에 각각 작성 BankAccount에 한 번만 작성
입금 로직 수정 시 두 클래스 모두 수정 필요 BankAccount.deposit() 하나만 수정
수정 누락 버그 위험 자동 반영

5.2 부모 클래스와 자식 클래스의 관계

구분 부모 클래스 (Parent) 자식 클래스 (Child)
동의어 슈퍼클래스, 베이스클래스 서브클래스, 파생클래스
개념 수준 포괄적/일반적 구체적/특수
역할 공통 기능 정의 부모 기능 + 고유 기능 추가

예시:

  • 사람 > 배우: 배우는 사람이다 (is-a)
  • 이동수단 > 자동차 > SUV: 계층이 내려갈수록 구체적
중요

is-a vs has-a 판단:

  • is-a (“배우는 사람이다”) → 상속 적합
  • has-a (“자동차는 엔진을 가진다”) → 합성(Composition) 사용
  • 판단 오류 시 클래스 계층이 비틀림
    • “A는 B이다” → 상속 예) Dog는 Animal이다 ✅
    • “A는 B를 가진다” → 합성 예) Car는 Engine을 가진다 ✅
    • “A는 B이다”가 어색하면 → 합성 예) Car는 Engine이다 ❌
  • 실무에서는 상속보다 합성을 더 많이 사용한다.
    • 상속은 부모 클래스가 바뀌면 자식 클래스 전체가 영향을 받지만, 합성은 부품만 교체하면 되기 때문이다.
  • 강한 종속: 합성(Composition) = 한 클래스가 다른 클래스의 인스턴스를 멤버 변수로 소유하는 것
    • Car가 Engine을 직접 생성하고 소유
    • Car 없이 Engine이 독립적으로 존재할 수 없음
    • 생명주기가 같음
class Car:
    def __init__(self):
        self.engine = Engine()  # Car가 직접 생성
                                # Car가 사라지면 Engine도 함께 사라짐
  • 약한 종속: 집합(Aggregation ) = 한 클래스가 다른 클래스의 인스턴스를 멤버 변수로 참조하지만, 소유하지는 않는 것
    • Engine이 외부에서 생성되어 parameter에 전달됨
    • Car가 사라져도 Engine은 살아있음
    • 생명주기가 다름
class Car:
    def __init__(self, engine):  # 외부에서 만들어진 Engine을 주입받음
        self.engine = engine     # Car가 사라져도 Engine은 독립적으로 존재 가능

engine = Engine()   # Engine이 먼저 독립적으로 존재
car1 = Car(engine)  # 같은 engine을 여러 곳에서 공유 가능
car2 = Car(engine)
  • 실무에서는 이 둘을 엄격히 구분하기보다 “외부에서 주입받는 방식(Aggregation)”을 더 많이 권장한다.

  • 테스트할 때 가짜 Engine(MockEngine)을 주입할 수 있어서 유연하기 때문이다.

  • 이 패턴을 의존성 주입(Dependency Injection) 이라고 부른다.

  • 예시

# "개는 동물이다" — is-a 관계 → 상속 적합
class Animal:
    def __init__(self, name):
        self.name = name

    def breathe(self):
        print(f"{self.name}이 숨을 쉰다")

    def eat(self):
        print(f"{self.name}이 먹는다")

class Dog(Animal):          # Dog is-a Animal
    def bark(self):
        print(f"{self.name}이 짖는다")

class Cat(Animal):          # Cat is-a Animal
    def meow(self):
        print(f"{self.name}이 야옹한다")

dog = Dog("멍멍이")
dog.breathe()   # Animal에서 상속 — "멍멍이이 숨을 쉰다"
dog.eat()       # Animal에서 상속 — "멍멍이이 먹는다"
dog.bark()      # Dog 고유 기능 — "멍멍이이 짖는다"


# "자동차는 엔진을 가진다" — has-a 관계 → 합성 적합
class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

    def start(self):
        print(f"{self.horsepower}마력 엔진 시동")

    def stop(self):
        print("엔진 정지")

class GPS:
    def navigate(self, destination):
        print(f"{destination}으로 경로 안내 시작")

class Car:
    def __init__(self, model):
        self.model = model
        self.engine = Engine(200)   # Car has-a Engine
        self.gps = GPS()            # Car has-a GPS

    def start(self):
        print(f"{self.model} 출발 준비")
        self.engine.start()         # Engine 기능을 위임

    def go_to(self, destination):
        self.gps.navigate(destination)  # GPS 기능을 위임

car = Car("아반떼")
car.start()           # 아반떼 출발 준비 / 200마력 엔진 시동
car.go_to("서울역")   # 서울역으로 경로 안내 시작

# 합성의 유연성 — 엔진만 교체 가능
class ElectricEngine:
    def start(self):
        print("전기 모터 구동")

car.engine = ElectricEngine()   # 엔진만 바꿔끼우면 됨
car.start()                     # 나머지 Car 코드는 변경 불필요

5.3 Python에서 상속 구현하기

  • Python에서 상속은 클래스 정의 시 부모 클래스 이름을 괄호 안에 넣는 방식으로 선언한다.
# 부모 클래스
class Person:
    def __init__(self, name, job):
        self.name = name
        self.job = job

    def introduce(self):
        print(f"제 이름은 {self.name}입니다. 직업은 {self.job}입니다.")

# 자식 클래스
class Actor(Person):
    def __init__(self, name, best_movie):
        super().__init__(name, job="배우")  # 부모 생성자 호출
        self.best_movie = best_movie

    def filmography(self):
        print(f"대표 작품은 {self.best_movie}입니다.")

# 인스턴스 생성 및 사용
actor_song = Actor("송강호", best_movie="기생충")

actor_song.introduce()     # 제 이름은 송강호입니다. 직업은 배우입니다.
actor_song.filmography()   # 대표 작품은 기생충입니다.

주요 포인트:

요소 설명
Actor(Person) ActorPerson을 상속받는다는 선언
super().__init__() 부모 클래스 생성자 호출 (필수)
self.best_movie Actor 클래스만의 고유 속성
경고

super().__init__() 호출을 빠뜨리면 부모 클래스 속성이 초기화되지 않아
actor_song.name 접근 시 AttributeError 발생

5.4 메소드 오버라이딩: 부모의 기능을 자식이 재정의하기

메소드 오버라이딩(Method Overriding) = 상속받은 메소드를 같은 이름으로 재정의

사용 시점: 부모 클래스의 기본 동작이 자식 클래스에 맞지 않을 때

class Person:
    def introduce(self):
        print(f"제 이름은 {self.name}입니다. 직업은 {self.job}입니다.")

class Actor(Person):
    def introduce(self):  # 오버라이딩: 같은 이름으로 재정의
        print(f"안녕하세요, 배우 {self.name}입니다.")
        print(f"대표작은 {self.best_movie}입니다.")
        super().introduce()  # 필요하면 부모 메소드도 함께 호출 가능

동작 방식:

  • actor_song.introduce() 호출 시 → Actor.introduce() 실행
  • 자식 클래스에 동일 이름 메소드 있으면 자식 것이 우선
  • 다형성(Polymorphism)의 핵심 구현 수단

5.5 다중 상속과 다이아몬드 문제

다중 상속(Multiple Inheritance) = 하나의 자식 클래스가 두 개 이상의 부모를 동시 상속

# 부모 클래스 1: 새 (날 수 있음)
class Bird:
    def __init__(self):
        self.can_fly = True

    def fly(self):
        print("This bird can fly.")

# 부모 클래스 2: 물고기 (수영할 수 있음)
class Fish:
    def __init__(self):
        self.can_swim = True

    def swim(self):
        print("This fish can swim.")

# 자식 클래스: 날으는 물고기 (다중 상속)
class FlyingFish(Fish, Bird):
    def __init__(self):
        Bird.__init__(self)   # 두 부모의 생성자를 명시적으로 호출
        Fish.__init__(self)

    def ability(self):
        if self.can_fly and self.can_swim:
            print("This creature can both fly and swim.")

다중 상속은 강력하지만 다이아몬드 상속 문제(Diamond Inheritance Problem)
일으킬 수 있다.

class A:
    def method(self):
        print("Method from class A")

class B(A):
    def method(self):
        print("Method from class B")

class C(A):
    def method(self):
        print("Method from class C")

class D(B, C):   # B와 C를 동시에 상속
    pass

d = D()
d.method()   # 어떤 method가 호출될까?

DBC를 동시에 상속받는데,
BC 둘 다 A를 상속받으며 method()를 각자 오버라이딩했다.
d.method()를 호출하면 B의 것을 써야 하는가, C의 것을 써야 하는가?
이 모호성이 다이아몬드 문제다.

Python은 이 문제를 MRO(Method Resolution Order) 로 해결한다.
MRO는 메소드를 탐색할 순서를 결정하는 알고리즘으로,
Python은 C3 선형화(C3 Linearization) 알고리즘을 사용한다.

print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

MRO(Method Resolution Order) 해결:

  • D > B > C > A > object 순서로 탐색
  • D(B, C)에서 먼저 나열된 B가 우선순위
주의

실무 권장사항:

  • 다중 상속은 동작 예측이 어렵고 가독성 저하
  • 다중 상속 최대한 회피
  • 필요 시 믹스인(Mixin) 패턴 사용
  • Java/C#이 인터페이스(interface)로 대체한 이유

6 정리

이 글에서 다룬 OOP의 핵심 개념을 연결해서 보면 하나의 흐름으로 이어진다:

개념 역할
클래스 객체를 생성하기 위한 설계도, 속성과 메소드의 정의
객체 클래스로부터 생성된 독립적인 실체
생성자 인스턴스 생성 시 자동 호출되어 멤버 변수를 초기화
상속 기존 클래스를 기반으로 새로운 클래스를 정의, 코드 재사용
추상화 공통점을 추출해 상위 개념으로 정의, 설계 계약 강제
캡슐화 데이터를 보호하고 메소드를 통해서만 접근 허용
다형성 동일한 인터페이스가 객체 타입에 따라 다르게 동작

이 개념들이 함께 작동할 때 예측 가능하고, 변경하기 쉽고, 확장하기 쉬운 코드가 만들어진다.

Subscribe

Enjoy this blog? Get notified of new posts by email: