의존성 역전 원칙(DIP )
의존성 역전 원칙(Dependency Inversion Principle, DIP)은 객체 지향 설계 원칙 중 하나로, 시스템의 고수준 모듈이 저수준 모듈에 직접적으로 의존하는 것을 피하고, 대신 둘 모두가 추상화에 의존하도록 설계해야 한다는 원칙입니다. 이 원칙은 SOLID 원칙 중 하나로, 특히 대규모 소프트웨어 시스템의 유지 보수성과 확장성을 향상시키는 데 중요한 역할을 합니다.
DIP의 주요 내용은 다음과 같습니다:
- 고수준 모듈은 저수준 모듈에 의존해서는 안 됩니다. 둘 다 추상화에 의존해야 합니다.
- 추상화는 세부 사항에 의존해서는 안 됩니다. 세부 사항이 추상화에 의존해야 합니다.
DIP를 지키지 않았을 때의 문제
문제점:
- 코드의 재사용성이 떨어진다.
- 인터페이스(추상화)에 의존하면 쉽게 코드를 재사용할 수 있다. 아래 예제 기준으로 Switchable 을 상속해서 구현한 다른 클래스도 LightSwitch 구현을 사용할 수 있다.
- 변경에 취약하다. 저수준 모듈에 변화가 생기면, 이를 사용하는 고수준 모듈도 변경해야 할 가능성이 높아진다.
- 구현과 직접적으로 연결되어 있기 때문에 구현이 변경되면 영향을 받을 수 있음.
- Database라는 저수준의 모듈이 있고, 이를 사용하는 UserService라는 고수준의 모듈이 있다고 가정합시다.
class Database:
def get_user(self, user_id):
# 데이터베이스에서 사용자 정보를 가져오는 로직
pass
class UserService:
def __init__(self):
self.database = Database()
def get_user_details(self, user_id):
return self.database.get_user(user_id)
위의 설계에서, 만약 Database 클래스의 get_user 메서드가 변경되어 인자를 추가적으로 필요로 한다면, UserService의 get_user_details 메서드도 수정해야 할 것입니다.
예를 들어, get_user가 보안상의 이유로 auth_token을 추가로 받게 변경된다면, UserService도 이 변경에 따라 수정해야 합니다.
- 유닛 테스트가 어렵다.
- 저수준 모듈에 의존하는 고수준 모듈을 독립적으로 테스트하기 어렵다.UserService는 Database에 직접적으로 의존하고 있습니다. 이로 인해 UserService의 메서드를 테스트하려면 실제 Database의 연결이 필요하게 됩니다. 따라서, 다음과 같은 문제가 발생할 수 있습니다.
- 데이터베이스 연결이 필요한 환경에서만 테스트를 실행할 수 있습니다.
- 테스트 데이터를 준비하고, 테스트 후에 원래 상태로 롤백해야 합니다.
- 테스트 실행 속도가 느려집니다.
- 저수준 모듈에 의존하는 고수준 모듈을 독립적으로 테스트하기 어렵다.UserService는 Database에 직접적으로 의존하고 있습니다. 이로 인해 UserService의 메서드를 테스트하려면 실제 Database의 연결이 필요하게 됩니다. 따라서, 다음과 같은 문제가 발생할 수 있습니다.
예제
고수준 모듈인 LightSwitch 클래스와 저수준 모듈인 Bulb 클래스가 있다고 가정해보겠습니다.
class Bulb:
def turn_on(self):
print("Bulb has been lit")
def turn_off(self):
print("Darkness!")
class LightSwitch:
def __init__(self, bulb):
self.bulb = bulb
def operate(self):
# ... some logic to decide
self.bulb.turn_on()
위 예제에서 LightSwitch는 Bulb 클래스에 직접적으로 의존하고 있습니다. 이렇게 설계된 경우, 만약 Bulb 클래스에 변화가 생기면 LightSwitch도 영향을 받게 됩니다. 또한, LightSwitch를 테스트하려면 실제 Bulb 클래스의 인스턴스와 함께 테스트해야 합니다.
DIP를 적용하여 문제를 해결하려면, 먼저 Bulb와 LightSwitch가 의존할 수 있는 추상화를 정의해야 합니다.
from abc import ABC, abstractmethod
class Switchable(ABC):
@abstractmethod
def turn_on(self):
pass
@abstractmethod
def turn_off(self):
pass
class Bulb(Switchable):
def turn_on(self):
print("Bulb has been lit")
def turn_off(self):
print("Darkness!")
class LightSwitch:
def __init__(self, device):
self.device = device
def operate(self):
# ... some logic to decide
self.device.turn_on()
이렇게 설계하면, LightSwitch는 Switchable이라는 추상화에만 의존하게 되고, Bulb 클래스의 변경에 덜 영향을 받게 됩니다. 또한, 다양한 종류의 스위치 가능한 장치(예: 팬, 라디오 등)를 쉽게 추가할 수 있습니다.
이름이 의존성 역전 원칙인 이유
"의존성 역전 원칙(Dependency Inversion Principle, DIP)"의 이름에서 "역전"이라는 단어는 전통적인 의존 관계가 "역전"되었다는 것을 나타냅니다.
전통적인 소프트웨어 설계에서는 고수준의 모듈(비즈니스 로직을 포함하는 모듈)이 저수준의 모듈(데이터 액세스 또는 기본 연산을 수행하는 모듈)에 의존합니다. 이러한 의존 관계는 직관적입니다. 예를 들어, 라이트 스위치는 전구에 의존하게 됩니다. 여기서 라이트 스위치는 고수준 모듈이며, 전구는 저수준 모듈입니다.
그러나 DIP를 적용할 때, 이 의존성 관계가 "역전"됩니다. 고수준 모듈과 저수준 모듈 모두 중간에 있는 추상화(인터페이스나 추상 클래스)에 의존하게 됩니다. 이렇게 하면 고수준 모듈은 저수준 모듈의 구체적인 구현에 의존하지 않게 되어, 유연성과 확장성이 향상됩니다.
이 원칙의 이름은 이러한 "의존성의 방향이 역전되었다"는 개념을 반영하여 "의존성 역전 원칙"이라고 명명되었습니다.
의존성 역전 원칙이 오버엔지니어링인 경우 ->고수준 모듈에서 사용하는 저수준 모듈이 확장될 가능성이 없는 경우
DIP는 많은 경우에 유용하지만, 모든 상황에서 적용해야 하는 것은 아닙니다. 특히 고수준 모듈이 여러 저수준 모듈을 사용하지 않는 간단한 경우나**, 변경의 가능성이 거의 없는 경**우에 DIP를 적용하면 오버엔지니어링이 될 수 있습니다.
DIP를 적용할 때의 장점과 단점을 정리해보면 다음과 같습니다.
장점:
- 확장성: 추후에 다른 저수준 모듈을 추가하거나 교체하기 쉽습니다.
- 테스트 용이성: 고수준 모듈을 저수준 모듈로부터 독립적으로 테스트할 수 있습니다.
단점:
- 복잡성 증가: 인터페이스나 추상 클래스를 도입함으로써 코드의 복잡성이 증가할 수 있습니다.
- 학습 곡선: 새로운 개발자가 시스템을 이해하려면 추가적인 추상화를 학습해야 합니다.
따라서, 프로젝트의 요구사항, 팀의 경험, 예상되는 변경의 범위 및 빈도 등을 고려하여 DIP를 적용할지 결정해야 합니다.
간단한 프로젝트나 프로토타입, 빠른 개발이 필요한 경우에는 DIP를 적용하지 않아도 될 수 있습니다. 그러나 시스템이 커지고 복잡해질 때, 특히 여러 저수준 모듈이나 외부 시스템과의 상호작용이 많아질 때 DIP를 적용하는 것이 유리할 수 있습니다.
결론적으로, DIP의 적용 여부는 상황에 따라 다르며, 항상 "적절한" 설계 원칙을 선택하는 것이 중요합니다.
무작정 DIP를 사용해서 구현하는 것보다는 상황에 맞게 사용하는 것이 중요합니다!
'Computer Engineering > Design' 카테고리의 다른 글
간단한 원칙으로 좋은 HTTP API 만들기 (0) | 2024.02.18 |
---|---|
좋은 함수 작성하기 (좋은 코드란 😇) (0) | 2023.02.26 |