SOLID에 대해 많은 내용을 접했지만, SRP를 읽던 중 퍼사드 패턴에 관한 이야기가 있길래 다시 한번 읽어야 겠다고 생각했다. (다다익읽)
SRP는 모든 모듈이 단 하나의 일만 해야 한다는 의미가 아니다.
이는 SRP가 아니다.
SRP란? -> 단일 모듈은 변경의 이유가 하나, 오직 하나뿐이어야 한다.
하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다.
단일 액터를 책임지는 코드를 함께 묶어주는 힘이 바로 응집성(cohesion)이다.
Employee 클래스
calculatePay()는 회계팀에서, reportHours()는 인사팀에서 사용한다고 가정하자.
3가지의 액터가 단일 클래스에 배치되어 결합되어 버렸다.
regularHours()메서드를 두 곳에서 모두 호출해 사용하고 있다면, 변경이 일어났을 경우 영향도가 매우 크다.
이 문제는 서로 다른 액터가 의존하는 코드를 너무 가까이 배치했기 때문이다.
서로 다른 액터가 의존하는 코드를 서로 분리하여야 한다.
모든 메서드를 각기 다른 클래스로 이동하여 해결할 수 있다.
즉, 아무런 메서드가 없는 간단한 데이터 구조인 Employee 클래스를 만들어, 세 개의 클래스가 공유하도록 한다.
이 해결책은 세 가지 클래스를 인스턴스화하고 추적해야 한다는 단점이 있다.
-> 이것을 해결하는 흔한 기법이 퍼사드(Facade)패턴이다.
퍼사드 패턴이란?
Facade는 "건물의 정면"을 의미하는 단어로 어떤 소프트웨어의 다른 커다란 코드 부분에 대하여 간략화된 인터페이스를 제공해주는 디자인 패턴을 의미한다.
Ex)
TV를 보는 과정
서로 다른 클래스에 있고, TV를 볼 때마다 이 메서드들을 전부 추적해서 찾아야 한다.
그러나, 퍼사드 클래스를 만들어 view_tv() 메서드를 만들어 이 모든 메서드를 실행한다면 TV를 볼 때 퍼사드 클래스의 veiw_tv() 메서드를 호출하면 된다.
class Facade
def view_tv():
리모콘을 찾는다()
TV를 켠다()
채널을 돌린다()
장점 : 복잡한 하위 시스템에서 코드를 별도로 분리할 수 있다.
단점 : 퍼사드는 앱의 모든 클래스에 결합된 전지전능한 객체가 될 수 있다.
SRP는 메서드와 클래스 수준의 원칙이다. 이보다 상위의 두 수준에서도 다른 형태로 다시 등장한다.
소프트웨어 개체는 확장에는 열려 있어야 하고, 변겨에는 닫혀 있어야 한다.
소프트웨어 개체의 행위는 확장할 수 있어야 하지만, 이때 개체를 변경해서는 안된다.
소프트웨어 설계를 공부하기 시작한 지 얼마 안 된 사람들 대다수는 OCP를 클래스와 모듈을 설계할 때 도움되는 원칙이라고 알고 있다. 하지만 아키텍처 컴포넌트 수준에서 OCP를 고려할 때 훨씬 중요한 의미를 가진다.
화살표가 A 클래스에서 B 클래스로 향한다면, A 클래스에서는 B 클래스를 호출하지만, B 클래스에서는 A 클래스를 전형 호출하지 않는다.
Interactor가 업무 규칙을 포함하기 때문이다. Interactor는 애플리케이션에서 가장 높은 수준의 정책을 포함한다.
보호의 계층구조가 '수준'이라는 개념을 바탕임을 주목하자.
Interactor는 가장 높은 수준의 개념이며, 따라서 최고의 보호를 받는다.
Controller는 Interactor보다는 부수적이지만, View에 비해서는 중심적인 문제를 담당한다.
아키텍트는 기능이 어떻게, 왜, 언제 발생하는지에 따라서 기능을 분리하고, 분리한 기능을 컴포넌트의 계층구조로 조직화한다.
이 조직화를 통해 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다.
Controller에서 발생한 변경으로부터 Interactor를 보호하는 일의 우선순위가 가장 높지만, 반대로 Interactor에서 발생한 변경으로부터 Controller도 보호되기를 바란다. 이를 위해 Interactor 내부를 은닉한다.
추이 종속성 : A -> B -> C 형태라면, 결국 A는 C에 의존하게 된다. 이를 추이 종속성이라고 부른다.
리스코프의 하위 타입 정의
"여기에서 필요한 것은 다음과 같은 치환 원칙이다. S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고, T타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리를 o1으로 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위 타입이다."
책에 있는 말을 가져와 어렵게 들릴 수 있지만,
예로 들자면 하나의 인터페이스에서 의존하는 객체를 인터페이스를 구현한 클래스 객체로 변경해도 P의 행위는 변하지 않는다 라는 말이다.
LSP는 아키텍처 수준까지 확장할 수 있고, 반드시 확장해야만 한다.
치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있기 때문이다.
class OPS
op1()
op2()
op3()
User1은 오직 op1을, User2는 op2만을, User3는 op3만을 사용한다고 가정하자.
User1은 op2, op3를 전혀 사용하지 않음에도 두 메서드에 의존하게 된다. op2의 소스 코드가 변경되면 User1도 다시 컴파일한 후 새로 배포해야 한다.
정적 타입 언어는 사용자가 import, user 또는 include와 같은 타입 선언문을 사용하도록 강제한다.
이처럼 소스 코드에 포함된 선언문으로 인해 소스 코드 의존성이 발생하고, 이로 인해 재컴파일 또는 재배포가 강제되는 상황이 무조건 초래된다.
루비나 파이썬과 같은 동적 타입 언어에서는 소스 코드에 이러한 선언문이 존재하지 않는다. 대신 런타임에 추론이 발생한다.
따라서 소스 코드 의존성이 아예 없으며, 결국 재컴파일과 재배포가 필요없다.
동적 타입 언어를 사용하면 정적 타입 언어를 사용할 때보다 유연하며 결합도가 낮은 시스템을 만들 수 있는 이유는 바로 이 때문이다.
-> 파이썬 라이브러리를 업데이트한 적이 있는데, 이때 컴파일을 다시 안해도 되어서 신기했는데 이 이유였다!
이러한 사실로 인해 ISP를 아키텍처가 아니라, 언어와 관련된 문제라고 결론내릴 여지가 있다.
의존성 역전 원칙에서 말하는 '유연성이 극대화된 시스템'이란 소스 코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템이다.
뛰어난 아키텍트는 인터페이스의 변동성을 낮추기 위해 애쓴다.
인터페이스를 변경하지 않고도 구현체에 기능을 추가할 수 있는 방법을 찾는다.
즉, 안정된 소프트웨어 아키텍처란 변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍처이다.
모든 언어에서 객체를 생성하려면 해당 객체를 구체적으로 정의한 코드에 대해 소스 코드 의존성이 발생한다.
자바 등 대다수의 객체 지향 언어에서 이처럼 바람직하지 못한 의존성을 처리할 때 추상 팩토리를 사용하곤 한다.
DIP 위배를 모두 없앨 수는 없다.
DIP를 위배하는 클래스들은 적은 수의 구체 컴포넌트 내부로 모을 수 있고, 이를 통해 시스템의 나머지 부분과는 분리할 수 있다.