4 분 소요

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 원칙에 대한 요약을 마칩니다. 이 원칙을 준수하면 코드의 구조와 구성을 개선하여 이해, 유지 관리 및 확장을 더 쉽게 할 수 있습니다.

댓글남기기