SOLID 원칙
Introduction
SOLID는 Robert C. Martin이 소개한 객체 지향 프로그래밍 및 설계의 첫 다섯 가지 원칙의 약자입니다. 이러한 원칙을 함께 사용하면 프로그래머가 시간이 지나도 유지 관리 및 확장이 용이한 소프트웨어를 쉽게 개발할 수 있습니다.
Single Responsibility Principle (SRP)
단일 책임 원칙은 클래스는 변경해야 할 이유가 하나만 있어야한다는 이야기 입니다. 즉, 클래스는 하나의 직무 또는 책임만 가져야 합니다.
예시
-
CRUD
# Badclass User:def get_user(self, id):passdef save_user(self, user):pass-
개선
역할을 나눔. 위의 경우 OOP 관점으로도 문제가 있음. (
user.get_user?)# Goodclass UserRetriever:def get_user(self, id):passclass UserSaver:def save_user(self, user):pass
-
-
책과 출판사
# Badclass Book:id: intcontent: strpublisher: Publisherauthor: Author-
개선
Book의 identity 를 구별할 수 있는 id, content 와 이 책을 포괄하는 다른 요소 들이 같은 층위로 존재하고 있는데 이를 분리합니다.# Goodclass Meta:publisher: Publisherauthor: Authorclass Book:id: intcontent: strmeta: Meta -
개선 2
Field 가 실제 세계에서 내포하는 객체가 아닐 경우(eg. Book ⊂ Author, Book ⊂ Publisher), 변화에 덜 민감하도록 pk 만 참조하도록 개선합니다.
# Goodclass Meta:publisher_id: intauthor_id: intclass Book:id: intcontent: strmeta: Meta
-
TIP
클래스의 필드를 추가할 때 해당 클래스의 Direct 필드로 적절한지 확인해보자.
Open-Closed Principle (OCP)
개방-폐쇄 원칙에 따르면 소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장을 위해서는 개방적이어야 하지만 수정을 위해서는 폐쇄적이어야 합니다.
예시
-
넓이 구하기
# Badclass Rectangle:def __init__(self, width, height):self.width = widthself.height = heightclass Circle:def __init__(self, radius):self.radiusclass AreaCalculator:def calculate(self, shapes):total_area = 0for shape in shapes:if isinstance(shape, Rectangle):total_area += shape.width * shape.heightelif isinstance(shape, Rectangle):total_area += shape.radius * shape.radius * 3.14return total_area-
개선
# Goodclass Shape:def area(self):passclass Rectangle(Shape):def __init__(self, width, height):self.width = widthself.height = heightdef area(self):return self.width * self.heightclass Circle(Shape):def __init__(self, radius):self.radius = radiusdef area(self):return shape.radius * shape.radius * 3.14class AreaCalculator:def calculate(self, shapes):return sum(shape.area() for shape in shapes)
-
TIP
만약 활용되고 있는 인터페이스의 새로운 구현체를 추가 했는데 해당 인터페이스의 호출부나 다른 클래스의 구현을 수정(Not closed for modification)을 해야하는 경우 이 OCP 원칙을 지키고 있는지 점검해보자.
Liskov Substitution Principle (LSP)
리스코프 대체 원칙에 따르면 프로그램이 기본 클래스를 사용하는 경우 프로그램의 기능에 영향을 주지 않고 기본 클래스에 대한 참조를 파생 클래스로 대체할 수 있어야합니다. 다시 말하면, 부모 클래스의 모든 기능들은 자식 클래스에서 동작해야합니다.
예시
-
새 = 나는 새?
# Badclass Bird:def fly(self):passclass Ostrich(Bird):def fly(self):raise NotImplementedError-
종으로의 새 ≠ 나는 새
# Goodclass Bird:passclass FlyingBird(Bird):def fly(self):passclass Ostrich(Bird):pass
-
TIP
구현이 불가능한 인터페이스를 포함하는 상속이나 구현을 해야할 때, 부모클래스나 인터페이스의 정의가 LSP 원칙을 위배하고 있는지 확인하자. 만약 문제를 해결하다가 부모 클래스가 껍데기만 남을 경우Replace Superclass with Delegate 하자.
Interface Segregation Principle (ISP)
인터페이스 분리 원칙에 따르면 클라이언트가 사용하지 않는 인터페이스에 의존하도록 강요해서는 안 됩니다. 즉, 클래스가 사용하지 않는 메서드를 구현할 필요가 없어야 한다는 뜻입니다.
예시
-
Robot can’t eat
# Badfrom abc import ABCMeta, abstractmethodclass Worker(metaclass=ABCMeta):@abstractmethoddef work(self):pass@abstractmethoddef eat(self):passclass Robot(Worker):def work(self):passdef eat(self):raise NotImplementedError-
개선
# Goodclass Workable(metaclass=ABCMeta):@abstractmethoddef work(self):passclass Eatable(metaclass=ABCMeta):@abstractmethoddef eat(self):passclass Worker(Workable, Eatable):def work(self):passdef eat(self):passclass Robot(Workable):def work(self):pass
-
TIP
인터페이스 이름을 정할 때 기능들을 포괄 할 수 있는 이름을 고민해보자. 혹시 적절한 이름이 떠오르지 않거나 너무 일반적인 이름으로 해야한다면 인터페이스를 분리하고 좀더 명확한 이름을 사용해서 가독성을 올려보자.
Dependency Inversion Principle (DIP)
종속성 반전 원칙에 따르면 상위 레벨 모듈은 하위 레벨 모듈에 종속되어서는 안 됩니다. 둘 다 추상화에 의존해야 합니다. 또한 추상화는 세부 사항에 의존해서는 안 됩니다. 세부 사항은 추상화에 의존해야 합니다.
예시
-
Switch 로 Light 조절하기
class LightBulb:def turn_on(self):passdef turn_off(self):passclass ElectricPowerSwitch:def __init__(self, l: LightBulb):self.lightbulb = lself.on = Falsedef press(self):if self.on:self.lightbulb.turn_off()self.on = Falseelse:self.lightbulb.turn_on()self.on = True-
개선
위 코드에서는
ElectricPowerSwitch클래스가LightBulb클래스와 긴밀하게 결합(Tightly coupling)되었습니다. 스위치는 다른 전자기기도 켜고 끌 수 있으므로 이를 다음과 같이Switchable을 도입해서 개선합니다.# Goodfrom abc import ABC, abstractmethodclass Switchable(ABC):@abstractmethoddef turn_on(self):pass@abstractmethoddef turn_off(self):passclass LightBulb(Switchable):def turn_on(self):passdef turn_off(self):passclass ElectricPowerSwitch:def __init__(self, s: Switchable):self.device = sself.on = Falsedef press(self):if self.on:self.device.turn_off()self.on = Falseelse:self.device.turn_on()self.on = True개선된 코드에는 켜고 끌 수 있는 모든 장치에서 구현할 수 있는
Switchable인터페이스가 있습니다. 이제ElectricPowerSwitch클래스는LightBulb클래스에서 분리되어Switchable인터페이스를 구현하는 모든 장치에서 작동할 수 있습니다.
-
-
Repository 패턴
class ProblemService:def __init__(self):self.repo = MysqlProblemRepo()class MysqlProblemRepo:def __init__(self):self.connection = mysql.connnect("...")def get_problem(self, pk: str):self.connection.query("SELECT * from problems")Mysql 구현체에
ProblemService가 tightly coupling 되어있는 걸 확인 할 수 있습니다.-
개선
from abc import ABC, abstractmethodclass ProblemRepo(ABC):@abstractmethoddef get_problem(self, pk: str):passclass ProblemService:# ProblemService 에 Repository 구현체를 주입def __init__(self, repo: ProblemRepo):self.repo = repoclass RemoteProblemRepo(ProblemRepo):def __init__(self):self.endpoint = "contenthub.mathpresso.net"def get_problem(self, pk: str):# Remote call을 통해 Problem 을 Retreivepassclass MysqlProblemRepo(ProblemRepo):def __init__(self):self.connection = mysql.connnect("...")def get_problem(self, pk: str):self.connection.query("SELECT * from problems")
-
이것으로 각각에 대한 간단한 예제와 함께 SOLID 원칙에 대한 요약을 마칩니다. 이 원칙을 준수하면 코드의 구조와 구성을 개선하여 이해, 유지 관리 및 확장을 더 쉽게 할 수 있습니다.