본문 바로가기

컴퓨터공학

[Clean Architecture] - SOLID 원칙 DIP (5)

반응형

오늘은 SOLID 원칙의 마지막, DIP 원칙을 알아본다. 

 

SOLID 원칙 시리즈

해당 시리즈는 로버트 C.마틴의 Clean Architecture 책을 보며 쉬운 이해를 위해 ChatGPT와 함께 공부한 내용을 정리해 놓은 글입니다.

 

SRP: 단일 책임 원칙

OCP: 개방-폐쇄 원칙

LSP: 리스코프 치환 원칙

ISP: 인터페이스 분리 원칙

DIP: 의존성 역전 원칙


DIP: 의존성 역전 원칙

- 고수준 모듈이 저수준 모듈에 의존하지 않고, 추상화에 의존하도록 만드는 것

- 구체적인 클래스(저수준 구현)에 의존하지 않고, 인터페이스(추상화)에 의존하도록 설계

 

이번에도 마찬가지로 파이썬 예제를 살펴보며 이해해보자. 

 

아래의 코드에서 Computer는 Keyboard에 의존한다. 만약 컴퓨터가 키보드가 아닌 다른 input 요소를 사용하고 싶다면, 코드를 변경해야한다. 이는 장기적으로 볼때 확장 및 유지보수에서 효율이 떨어진다. 

class Keyboard:
    def input_text(self):
        return "Typing with a keyboard"

class Computer:
    def __init__(self):
        self.keyboard = Keyboard()  # 구체적인 Keyboard 클래스에 의존

    def get_input(self):
        return self.keyboard.input_text()

# 사용
computer = Computer()
print(computer.get_input())

 

 

이렇게 바꿔보면 어떨까.

from abc import ABC, abstractmethod

# 입력 장치 인터페이스 (추상화)
class InputDevice(ABC):
    @abstractmethod
    def input_text(self):
        pass

# Keyboard 클래스 (구현체)
class Keyboard(InputDevice):
    def input_text(self):
        return "Typing with a keyboard"

# VoiceRecognition 클래스 (구현체)
class VoiceRecognition(InputDevice):
    def input_text(self):
        return "Speaking to the computer"

# Computer 클래스 (추상화에 의존)
class Computer:
    def __init__(self, input_device: InputDevice):
        self.input_device = input_device  # 추상화된 인터페이스에 의존

    def get_input(self):
        return self.input_device.input_text

 

위와 같이 추상화 인터페이스인 InputDevice를 사용한다면, 여러 구현체에 대해 확장도 쉽고, 고수준 클래스인 Computer의 경우 각 구현체에 의존하지 않으니, 안정적이다. 

 

이때, 개발자는 인터페이스의 변동성을 낮추기 위해 노력해야한다. 인터페이스를 변경하지 않고, 구현체에 기능을 추가할 수 있는 방법을 찾는 것이다. DIP를 지키기 위해 아래의 사항들을 체크해보라. 

 

  1. 변동성이 큰 구체 클래스를 참조하지 말고, 추상 인터페이스를 활용하자.
  2. 변동성이 큰 구체 클래스를 상속하는 것은 아주 신중하게
  3. 구체 함수를 오버라이드 하지 말자.
  4. 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말자.

 

이 중 나는 3번이 와닿지 않아서, 구체 함수를 오버라이드 하지 말자에 대해 예제로 이해해보자. 

 

다음과 같은 소리를 낼 수 있는 Dog Class가 있다고 하자. 

class Dog:
    def sound(self):
        return "멍멍"  # 구체적으로 구현된 함수

 

 

이런 상황에서 다음과 같은 Dog를 상속받는 LoudDog이 있다고 하자. LoudDog은 상위클래스인 Dog의 sound 함수를 오버라이드 하고 있다. 만약 Dog 클래스의 sound 구현이 변경된다면, LoudDog의 동작도 영향을 받게 된다. (Dog sound가 멍멍 -> 왈왈로 바뀌게 되면, LoudDog의 sound도 바뀐다)

class LoudDog(Dog):
    def sound(self):  # 구체 함수를 오버라이드
        return super().sound().upper()  # 부모 클래스의 구체 동작을 확장

 

 

이러한 소스 코드 의존성에 대한 자유도를 위해, 아래와 같이 추상화를 사용하자. 아래의 코드에서는 LoudDog가 Dog의 구체 함수에 의존하지 않고, Animal이라는 추상화만 의존하므로, Dog의 구현이 바뀌더라도 LoudDog는 영향을 받지 않게된다.

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def sound(self):
        pass

class Dog(Animal):
    def sound(self):
        return "멍멍"

class LoudDog(Animal):
    def sound(self):
        return "멍멍".upper()  # 구체 동작에 의존하지 않고 독립적으로 구현

 

 

팩토리

위의 예제에서 느낀바와 같이, 변동성이 큰 구체적인 객체는 특별히 주의해서 생성하여야 한다. 하지만 개발을 할때 객체를 생성하려면, 해당 객체를 정의한 코드에 대해 의존성이 생기기 마련인데, 이러한 바람직하지 못한 의존성을 처리할 때 추상 팩토리를 사용해 의존성을 낮출 수 있다. 책에 나온 예제가 안 와닿으므로, 윈도우/맥에서의 UI 버튼과 관련된 아래의 예제를 살펴보자. 

 

from abc import ABC, abstractmethod

# 1. 추상 제품: Button과 Checkbox
class Button(ABC):
    @abstractmethod
    def render(self):
        pass

class Checkbox(ABC):
    @abstractmethod
    def render(self):
        pass

# 2. 구체 제품: Windows 스타일과 Mac 스타일
class WindowsButton(Button):
    def render(self):
        return "Windows 스타일 버튼 생성"

class MacButton(Button):
    def render(self):
        return "Mac 스타일 버튼 생성"

class WindowsCheckbox(Checkbox):
    def render(self):
        return "Windows 스타일 체크박스 생성"

class MacCheckbox(Checkbox):
    def render(self):
        return "Mac 스타일 체크박스 생성"

# 3. 추상 팩토리
class UIFactory(ABC):
    @abstractmethod
    def create_button(self):
        pass

    @abstractmethod
    def create_checkbox(self):
        pass

# 4. 구체 팩토리: Windows 팩토리와 Mac 팩토리
class WindowsFactory(UIFactory):
    def create_button(self):
        return WindowsButton()

    def create_checkbox(self):
        return WindowsCheckbox()

class MacFactory(UIFactory):
    def create_button(self):
        return MacButton()

    def create_checkbox(self):
        return MacCheckbox()

# 5. 클라이언트 코드
def create_ui(factory: UIFactory):
    button = factory.create_button()
    checkbox = factory.create_checkbox()

    print(button.render())
    print(checkbox.render())

# 6. 사용
if __name__ == "__main__":
    os_type = "Windows"  # "Mac"으로 변경 가능

    if os_type == "Windows":
        factory = WindowsFactory()
    else:
        factory = MacFactory()

    create_ui(factory)

 

위의 예제에서 create_ui 에서 WindowsButton 인스턴스를 생성하여 사용하여하 하는데, WindowsButton을 직접 생성하지 않고, WindowsFactory를 통해 사용한다. 따라서 어떤 플랫폼의 팩토리가 추가되던 create_ui의 변동성은 현저히 낮아지게 된다. 정리해보면 다음과 같다. 

 

의존성 역전 전의 문제점:

  • 클라이언트 코드(Application)가 특정 구현체(WindowsButton, MacButton)에 의존

 

의존성 역전 후의 개선:

  • 클라이언트(Application)는 추상 팩토리 인터페이스(UIFactory)에만 의존
  • 구체적인 제품(WindowsButton, MacButton)은 추상 제품 인터페이스(Button, Checkbox)를 구현
  • 구체 팩토리(WindowsFactory, MacFactory)는 추상 팩토리(UIFactory)를 구현

 

이 구조에서 클라이언트는 구체 구현에 전혀 의존하지 않는다. 즉, 구체적인 팩토리와 제품의 생성 책임은 추상 인터페이스에 의해 역전된다.

 

 

  • 기존: 고수준 모듈 → 저수준 모듈 (create_ui → WindowsButton)
  • 역전된 흐름: 저수준 모듈 → 추상 계층 ← 고수준 모듈 (WindowsButton   abstract ← create_ui)

 

반응형