안녕하세요 펭귄 교수입니다.
이번에는 상속(Inheritance), 캡슐화(Encapsulation), 다형성(Polymorphism)과 같은 객체 지향 프로그래밍의 심화 개념을 다루겠습니다. 이 주제들은 소프트웨어 설계에서 매우 중요한 역할을 하며, 프로그램의 유연성과 재사용성을 극대화할 수 있습니다.
1. 객체 지향
이전 강의, 클래스와 메서드에서 객체란 무엇인지 에 대해서 이야기했었습니다.
하지만 코드 우선으로 설명하다 보니 심화적인 내용은 스킵된 부분들이 있었습니다.
이번에는 그런 부분들을 다뤄보고자 합니다.
이전 글을 한번 보고 오시는 것을 추천드립니다.
1.1 객체 지향의 개념
객체 지향 프로그래밍이란, 하나의 프로그래밍 패러다임입니다.
객체 지향적인 프로그래밍을 쓰는 이유들에 대해 먼저 이야기 해보겠습니다.
1. 자료 추상화
불필요한 정보는 숨기고 중요한 정보만을 보여준다는 것입니다. 일명 캡슐화의 개념입니다.
그렇기 때문에 생성자, 소멸자, 그리고 게터 세터가 해당 개념을 통해 구현된 것이고, 이는 결국 프로그램의 보안과도 연결됩니다.그와 관련해서 아래 자세하게 설명하도록 하겠습니다.
2. 상속
그렇게 추상화된 객체의 개념은 하위 객체의 개념을 포함한다. 이것이 상속의 개념입니다.
예를 들어 차는 바퀴를 가지고 있고 움직일 수 있습니다. 그리고 그 하위 개념으로는 승용차, 트럭, 오토바이가 있을 수 있습니다.
승용차, 트럭, 오토바이는 모두 차의 특성을 가지고 있기 때문에 차의 하위 개념으로 볼 수 있습니다.
이렇게 추상화된 개념들을 상속하게 된다면 코드 재사용성이 높아지고, 가독성이 높아지는 프로그램을 만들 수 있습니다.
3. 다중 상속
다중 상속은 여러 개의 클래스를 상속 받는 것을 의미합니다.
이는 C++, JAVA 등에서는 지원하지 않지만, 파이썬은 이를 지원합니다.
예를 들어, 나는 아버지의 속성을 가지고 있지만, 어머니의 속성 또한 가질 수 있는 것입니다.
그렇게 되면 하나의 클래스에서 여러 클래스의 속성을 사용할 수 있게 됩니다.
4. 다형성 개념
다형성은 쉽게 말하면 오버 라이딩, 오버 로딩의 개념입니다.
오버 라이딩, 상속받은 클래스의 메서드를 재정의 하는 것이고,
오버 로딩, 한 클래스 내 같은 이름의 메서드 내 매개 변수를 달리 해서 여러 상황을 컨트롤 하는 것입니다.
이것 또한 아래에서 자세하게 다뤄보도록 하겠습니다.
5. 동적 바인딩
실행 시간 중에 일어나거나 실행 과정에서 변경될 수 있는 바인딩으로 컴파일 시간에 완료되어 변화하지 않는 정적바인딩과 대비되는 개념입니다.
1.2 객체 지향의 5 원칙 (SOLID)
이에 대해서는 한 번은 자세히 다뤄보고 싶었습니다.
정보 처리 기사 단골 문제이기도 하고, 한 번 이해하면 두고두고 써먹을 수 있는 개념이라 생각했기 때문입니다.
1. 단일 책임의 원칙, Single Responsibility Principle(SRP)
객체는 오직 하나의 책임만 다해야 한다. 라는 원칙입니다.
예를 들어 Person 이라는 객체가 있다면, 이는 Person과 관련된 변수와 메서드만을 가지고 있어야 합니다.
갑자기 이 객체 안에 Car와 관련된 메서드나 변수가 있다면 좋지 않은 거죠.
이러한 원칙이랑 같이 볼만한 개념은 응집도와 결합도입니다.
좋은 객체는 결국 낮은 결합도와 높은 응집도를 갖고 있어야 하는 데, 이를 달리 말해보면 적은 책임 요소에 큰 책임을 다하는 것과 비슷하기 때문이죠.
2. 개방 폐쇄 원칙, Open-Closed Principle(OCP)
객체는 확장에 대해서는 개방적이고, 수정에 대해서는 폐쇄적이어야 한다. 라는 원칙입니다.
확장은 상속에 대한 것으로, 상속이 자유로운, 그만큼 추상적으로 작성되어야 한다는 것이고,
수정은 상위 객체에서는 이루어지지 않고, 하위 객체에서 새로운 메서드를 정의하여야 한다는 것입니다.
3. 리스코프 치환 법칙, Liskov Substitution Principle(LSP)
리스코프는 MIT 컴퓨터 사이언스 교수 Barbara Liskov의 이름에서 따온 것입니다.
이는 부모 클래스의 위치를 자식 클래스가 대체할 수 있다. 라는 원칙입니다.
프로그래밍에서 객체를 공부할 때면, 아래와 같은 형식의 코드를 많이 봤을 겁니다.
// Truck 이라는 객체가 Car 라는 객체를 상속받은 상태
Truck truck1 = new Truck();
Car truck2 = new Truck();
Truck truck3 = new Car();
이것이 리스코프 치환 법칙에 대해 직관적으로 설명하는 코드입니다.
Truck은 Car에 대한 모든 속성, 메서드를 물려받습니다.
is-a 형식으로 이것이 지켜지지 않으면 상속의 개념이 무너져 내리는 것이죠.
4. 인터페이스 분리 원칙, Interface Segregation Principle(ISP)
클라이언트에서 사용하지 않는 메서드는 사용해서는 안된다. 라는 원칙입니다.
개방 폐쇄 원칙과 비슷한 개념인데, 약간의 차이가 있습니다.
OCP 원칙은 상속과 관련된 것이라면, 이 원칙은 인터페이스와 관련있습니다.
특히 송/수신에 있어서 중요한 원칙입니다.
인터페이스는 각 객체, 혹은 클라이언트와 서버끼리 소통할 때 사용되는 하나의 전화기 같은 것입니다.
서로의 규격을 정하고 그 규격에 맞게 소통하는 것이죠.
이 원칙은 소통 과정에서 불필요한 정보들이 오가는 것을 막는 것이 목표입니다.
예를 들어, 일반 유저에게 없고, 관리자에게만 있는 시스템을 다른 시스템과 엮어서 한 인터페이스로 같이 보내는 경우가 이를 위반하는 경우가 됩니다.
5. 의존성 역전 법칙, Dependency Inversion Principle(DIP)
추상성이 높고 안정적인 고수준의 클래스는 그보다 불안정하고 저수준의 클래스에 의존해서는 안된다. 라는 원칙입니다.
사실, 위 원칙들을 잘 수행할 경우 위반할 일이 없는 원칙입니다.
2. 상속(Inheritance)의 심화
상속은 기존 클래스를 기반으로 새로운 클래스를 정의할 수 있는 기능입니다. 파이썬에서 상속을 사용하면 코드의 재사용성을 높일 수 있고, 유지보수를 쉽게 할 수 있습니다. 그러나 상속을 과도하게 사용하면 프로그램의 복잡성이 증가할 수 있으므로 주의가 필요합니다.
2.1 다중 상속(Multiple Inheritance)
파이썬은 다중 상속을 지원합니다. 이는 하나의 클래스가 둘 이상의 부모 클래스로부터 상속을 받을 수 있음을 의미합니다.
class Flyable:
def fly(self):
print("Flying...")
class Animal:
def move(self):
print("Moving...")
class Bird(Animal, Flyable):
pass
bird = Bird()
bird.move() # Moving...
bird.fly() # Flying...
위 예제에서 Bird 클래스는 Animal과 Flyable 두 클래스로부터 상속을 받았습니다. 이처럼 다중 상속을 통해 여러 클래스로부터 기능을 가져올 수 있지만, 너무 많은 부모 클래스를 상속받을 경우 코드가 복잡해지고 유지보수가 어려워질 수 있습니다.
2.2 super()와 메서드 해석 순서(MRO)
super()는 부모 클래스의 메서드를 호출하는 데 사용됩니다. 다중 상속이 사용될 때는 메서드 해석 순서(Method Resolution Order, MRO)가 중요합니다. 파이썬은 MRO를 통해 어떤 순서로 상속된 클래스의 메서드를 호출할지 결정합니다.
class A:
def __init__(self):
print("A's __init__ called")
class B(A):
def __init__(self):
print("B's __init__ called")
super().__init__()
class C(A):
def __init__(self):
print("C's __init__ called")
super().__init__()
class D(B, C):
def __init__(self):
print("D's __init__ called")
super().__init__()
d = D()
# D's __init__ called
# B's __init__ called
# C's __init__ called
# A's __init__ called
위 코드는 super()를 이용한 다중 상속에서의 메서드 호출 순서를 보여줍니다. D 클래스는 B와 C를 상속받고, super()를 통해 부모 클래스들의 __init__ 메서드를 호출합니다. B와 C 모두 A를 상속받고 있으며, super()는 MRO에 따라 A의 __init__을 마지막으로 호출하게 됩니다.
MRO는 class_name.__mro__를 통해 확인할 수 있습니다.
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
3. 캡슐화(Encapsulation)의 심화
캡슐화는 객체의 상태(state)를 숨기고, 상태 변경을 위한 메서드만 제공하는 개념입니다. 이는 데이터의 무결성을 보장하고, 객체의 내부 구현을 외부에 노출하지 않도록 합니다.
3.1 접근 제어자: __(프라이빗), _(프로텍티드)
파이썬에서는 __(더블 언더스코어)를 사용하여 클래스 내부에서만 접근할 수 있는 프라이빗 속성을 정의할 수 있습니다.
또한, _(싱글 언더스코어)를 사용하여 프로텍티드 속성을 정의하여, 해당 클래스와 자식 클래스에서만 접근할 수 있음을 암시할 수 있습니다.
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # Private 변수
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def get_balance(self):
return self.__balance
account = BankAccount("Alice", 1000)
account.deposit(500)
print(account.get_balance()) # 1500
# print(account.__balance) # AttributeError: 'BankAccount' object has no attribute '__balance'
위 예제에서 __balance는 외부에서 직접 접근할 수 없는 프라이빗 변수입니다. get_balance() 메서드를 통해서만 계좌 잔액을 확인할 수 있습니다. 이렇게 프라이빗 속성은 외부의 잘못된 접근을 방지하는 데 유용합니다.
3.2 프로퍼티(property)를 통한 캡슐화
파이썬에서는 property 데코레이터를 사용하여 캡슐화를 더욱 쉽게 할 수 있습니다. 이를 통해 속성에 접근하는 것처럼 보이지만 실제로는 메서드를 호출하는 방식으로 동작하게 할 수 있습니다.
class Temperature:
def __init__(self, celsius):
self._celsius = 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.fahrenheit) # 77.0
temp.fahrenheit = 86
print(temp._celsius) # 30.0
위 코드는 fahrenheit 속성을 프로퍼티로 정의한 예제입니다. 이를 통해 마치 속성에 직접 접근하는 것처럼 보이지만, 실제로는 내부적으로 계산을 수행하는 메서드를 호출하게 됩니다.
4. 다형성(Polymorphism)의 심화
다형성은 같은 인터페이스를 사용하여 서로 다른 동작을 수행할 수 있게 하는 개념입니다.
파이썬에서는 다형성을 메서드 오버라이딩과 덕 타이핑(duck typing)을 통해 쉽게 구현할 수 있습니다.
4.1 메서드 오버라이딩
메서드 오버라이딩은 부모 클래스의 메서드를 하위 클래스에서 재정의하여 사용하는 것을 의미합니다.
class Shape:
def area(self):
raise NotImplementedError("Subclasses must implement this method")
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
shapes = [Rectangle(5, 10), Circle(7)]
for shape in shapes:
print(f"Area: {shape.area()}")
# Area: 50
# Area: 153.86
위 예제에서 Shape 클래스는 area() 메서드를 가지고 있지만, 실제 구현은 하지 않고 하위 클래스에서 오버라이딩하여 사용하고 있습니다. 이를 통해 동일한 인터페이스(area)를 사용하여 서로 다른 객체(Rectangle, Circle)에서 다양한 동작을 수행할 수 있습니다.
4.2 덕 타이핑(Duck Typing)
파이썬에서는 "덕 타이핑"이라는 개념을 사용합니다. 덕 타이핑은 객체의 실제 타입보다는 해당 객체가 어떤 메서드를 구현하고 있는지에 따라 동작을 결정합니다. 즉, 객체가 특정 메서드를 가지고 있다면 그 객체를 특정 타입으로 취급할 수 있다는 의미입니다.
class Dog:
def speak(self):
return "Bark"
class Cat:
def speak(self):
return "Meow"
class Robot:
def speak(self):
return "Beep"
def make_speak(animal):
print(animal.speak())
make_speak(Dog()) # Bark
make_speak(Cat()) # Meow
make_speak(Robot()) # Beep
위 코드에서 make_speak() 함수는 객체가 speak() 메서드를 가지고 있는지 여부만 확인하며, 그 객체가 Dog, Cat, Robot 중 무엇인지 상관하지 않습니다. 덕 타이핑을 사용하면 객체가 특정 인터페이스를 구현했는지 확인할 필요 없이 자유롭게 동작할 수 있습니다.
덕 타이핑에 대해 더 공부해보고 싶으시면 아래 글을 읽어보시는 걸 추천드립니다.
마무리
객체 지향 프로그래밍의 심화 개념인 상속, 캡슐화, 다형성은 코드의 구조를 더욱 유연하고 확장 가능하게 만들어줍니다.
이러한 개념들을 적절하게 사용하면, 복잡한 소프트웨어 시스템에서도 코드의 재사용성과 유지보수성을 극대화할 수 있습니다.
다른 글 더보기
'프로그래밍 > Python' 카테고리의 다른 글
[파이썬 코딩 강의] 웹과 파이썬 (Beautiful Soup 편) (1) | 2024.10.03 |
---|---|
[파이썬 코딩 강의] 웹과 파이썬 (requests 편) (0) | 2024.09.28 |
[파이썬 코딩 강의] 클래스와 메서드 (0) | 2024.09.21 |
[파이썬 코딩 강의] 함수, 모듈화 (0) | 2021.03.09 |
[파이썬 프로그램] 프로그램 다운로드 (0) | 2021.03.01 |