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