하나의 모듈은 하나의, 오직 하나의 Actor 에 대해서만 책임을 져야 한다. The module here refers to a cohesive set of codes.
예를 들어 급여 애플리케이션의 Employee Class 가 있다고 해보자. SRP 를 위반하는 이 클래스는 다음과 같이 서로 성격이 다른 method 를 가지게 된다.
class Employee():
def __init__(self):
self.employees = []
def getWorkingHours(self):
pass
def calculatePay(self):
"""
finance team needs this api
"""
hrs = self.getWorkingHours()
pay = hrs*10
return pay
def reportHours(self):
"""
HR team needs this api
"""
_hrs = self.getWorkingHours()
hrs = _hrs * 1.3
return hrs
def save(self):
pass
여기서 문제는 만약 getWorkingHours
함수를 바꾸게 되면 서로 다른 actor (finance and HR team) 에게 영향을 준다는 것이다.
이러한 문제는 서로 다른 actor 가 의존하는 코드를 너무 가까이 배치했기 때문에 발생한다. 해결책은 이를 분리하는 것이다.
위 사례에서 finance, HR 각각의 개발자가 Employee 클래스를 수정하게 되면 conflict 가 발생하게 되고 merge 가 불가피하다.
이 역시 서로 다른 액터가 동일한 소스 파일을 접근하려고 하기 때문에 발생하게 된다.
소프트웨어 개체는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
즉, 아키텍처가 훌륭하다면 변경대신 코드를 추가하는 것만으로 새로운 기능을 추가할 수 있어야 한다는 말이다.
Financial Data Gateway
인터페이스의 경우 별 역할은 없지만 interactor 와 database 간의 의존성을 역전시키기 위해 존재한다.
이게 없었다면 interactor 는 Financial Data Mapper
에 직접 접근하게 되어 interactor 를 최상위로 보호하고자 하는 목적이 깨지게 된다.
S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2 가 있고, T 타입을 이용해서 정의한 모든프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위 타입이다.
LSP 를 위반하는 유명한 문제가 있다.
class User():
def __init__(self):
self.rect = Rectangle()
def validate(self):
if self.H == self.W:
raise Exception("It is a square!")
class Rectangle():
def setH():
pass
def setW():
pass
class Square(Rectangle):
def setSide():
pass
위 코드에서 Square
가 Rectangle
을 치환할 수 없다.
system S → framework F → database D 의 관계대로 의존도가 성립되어 있을 때, F에서는 불필요한 기능인 S와는 전혀 관계없는 기능이 D에 포함된다고 가정하자. 이 기능 때문에 D 내부가 변경된다면 F를 재배포해야 할 수도 있고, 따라서 S까지 재배포해야 할지 모른다.
이럴땐 operation 단위로 D의 기능을 나누어 F가 각각의 Ops 를 의존하도록 분리할 수 있다.
의존성이 추상 abstraction 에 의존하며 구체 concretion 에는 의존하지 않는 시스템을 말한다.
우리가 의존하지 않도록 최대한 피하고자 하는 것은 변동성이 큰 구체적인 요소다.
안정된 소프트웨어 아키텍처란 변동성이 큰 구현체에 의존하는 일은 피하고, 안정된 추상 인터페이스를 선호하는 아키텍처 라는 뜻이다.
DIP 위배를 모두 없앨 수는 없다. 하지만 DIP 를 위배하는 클래스들은 적은 수의 구체 컴포넌트 내부로 모을 수 있고, 이를 통해 시스템의 나머지 부분과는 분리할 수 있다.