Home 객체 지향 프로그래밍과 SOLID 원칙(with Python)
Post
Cancel

객체 지향 프로그래밍과 SOLID 원칙(with Python)

객체 지향 프로그래밍(OOP)

데이터와 데이터를 처리하는 방법(메소드)을 하나의 단위(객체)로 묶는 프로그래밍

캡슐화(Encapsulation)

캡슐화는 객체의 데이터와 메소드를 하나의 단위(클래스)로 묶는 것을 의미한다. 이를 통해 데이터를 숨기고, 객체 내부의 구현 세부 사항을 외부로부터 보호한다. 외부에서는 객체가 노출되는 메소드를 통해서만 객체의 상태를 변경할 수 있다.

상속(Inheritance)

상속은 부모 클래스의 속성과 메소드를 자식 클래스가 받아서 사용할 수 있게 하는 메커니즘이다. 이를 통해 코드의 재사용성을 높이고, 중복을 줄이며 계층적 구조를 만들 수 있다. 자식 클래스는 부모 클래스의 특성을 상속받아 확장하거나 변경한다.

다형성(Polymorphism)

다향성은 같은 이름의 메소드가 다른 클래스에서 다양한 방식으로 실행될 수 있는 능력을 의미한다. 이는 메소드 오버로딩과 메소드 오버라이딩을 통해 구현된다. 다향성은 인터페이스의 일관성을 유지하면서도 다양한 구현을 가능하게 한다.

추상화(Abstraction)

추상화는 복잡한 현실 세계의 문제를 단순화하여 프로그래밍 내에서 관리하기 쉬운 형태로 만드는 과정이다. 이를 통해 프로그래머는 가장 중요한 특성에 집중하며 덜 중요한 세부 사항을 숨길 수 있다. 추상화는 일반적으로 추상 클래스나 인터페이스를 통해 구현된다.

SOLID 원칙

SOLID 원칙이란 객체지향 설계에서 지켜줘야 할 5개의 소프트웨어 개발 원칙을 뜻한다.

S - 단일 책임 원칙(SRP)

하나의 클래스는 하나의 책임만 가져야 한다.

1
2
3
4
5
6
7
8
9
10
11
class Order:
	def __init__(self, order_id, product):
		self.order_id = order_id
		self.product = product

	def get_order_id(self):
		return self.order_id

class OrederRepository:
	def save_order(self, order):
		print(f"{order.get_order_id()} 주문이 저장되었습니다.")

Order 클래스는 주문 정보를 관리하는 책임만 가지고, OrderRepository클래스는 주문을 저장하는 책임만 가진다. 다음과 같이 각 클래스는 하나의 책임만 가진다.

O - 개방-폐쇄 원칙(Open-Closed Principle)

소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에서는 열려 있어야 하지만 수정에는 닫혀 있어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Shape:
	def area(self):
		pass

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

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

class Circle(self):
	def __init__(self, r):
		self.radius = r

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

Shape 추상 클래스를 확장 가능하도록 설계되어 있다. 새로운 도형 클래스는 Shape 클래스를 상속 받아 area 메소드를 구현한다. 새로운 도형을 추가해도 기존 코드를 수정할 필요가 없으며, 단지 새로운 클래스를 추가함으로써 확장할 수 있다.

L - 리스코프 치환 원칙(Liskov Substitution Principle)

프로그램에서 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 대체할 수 있어야 합니다(예: 자식 클래스는 부모 클래스의 기능을 손상시키지 않아야 함).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Bird:
	def fly(self):
		pritn("fly!")

class Duck:
	def fly(self):
		print("Duck flying")

class Ostrich(Bird):
	def fly(self):
		raise Exception("Can't fly")

def make_bird_fly(bird):
	bird.fly()

duck = Duck()
ostrich = Ostrich()

make_bird_fly(duck)
# make_bird_fly(ostrich) # 이 부분은 리스코프 치환 원칙을 위반

I - 인터페이스 분리 원칙(Interface Segregation Principle)

클라이언트는 사용하지 않는 인터페이스에 의존하지 않아야 합니다. 즉, 큰 인터페이스보다는 필요한 기능만을 제공하는 작은 인터페이스가 더 좋습니다.

1
2
3
4
5
6
7
class Worker:
	def work(self):
		print("Working")

class Eater:
	def eat(self):
		print("Eating")

구체적인 행동을 나타내는 작은 인터페이스이다. 한 객체가 Worker, Eater의 기능이 모두 필요한 경우 두 인터페이스를 모두 구현할 수 있으나, 각각의 인터페이스는 그 기능에만 초점을 맞춘다.

D - 의존성 역전 원칙(Dependency Inversion Principle)

고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 합니다. 이 원칙은 시스템 결합도를 줄이고 유연성을 증가시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from abc import ABC, abstractmethod

# 추상화: 데이터 저장을 위한 추상 인터페이스 정의
class Storage(ABC):
    @abstractmethod
    def save(self, data):
        pass

# 저수준 모듈: 파일 시스템을 사용하여 데이터 저장
class FileStorage(Storage):
    def save(self, data):
        print(f"Saving {data} to a file")

# 저수준 모듈: 데이터베이스에 데이터 저장
class DatabaseStorage(Storage):
    def save(self, data):
        print(f"Saving {data} to a database")

# 고수준 모듈: 데이터 저장 로직을 관리
class DataManager:
    def __init__(self, storage: Storage):
        self.storage = storage

    def save_data(self, data):
        self.storage.save(data)

# 사용 예시
file_storage = FileStorage()
db_storage = DatabaseStorage()

data_manager_file = DataManager(file_storage)
data_manager_file.save_data("File data")

data_manager_db = DataManager(db_storage)
data_manager_db.save_data("Database data")

이 코드에서 DataManger는 고수준 모듈로, 데이터 저장 로직을 관리한다. FileStorage, DatabaseStorage는 저수준 모듈로 실제 데이터 저장 방식을 구현한다. Storage 인터페이스는 추상화로서, 고수준 모듈과 저수준 모듈 사이의 의존성을 역전시키는데 사용된다. DataManager는 저수준 모듈에 의존하지 않고 Storage 인터페이스에 의존한다. 이로써, 새로운 저장 방식이 추가되거나 기존 방식이 변경되어도 고수준 모듈을 수정할 필요가 없어지며 시스템 유연성 & 확작성이 향상된다.

This post is licensed under CC BY 4.0 by the author.