[개발서적] 클린 아키텍처 4부 컴포넌트 원칙 - 내용 정리 및 요약
이번에는 로버트 C 마틴의 클린 아키텍처를 읽은 내용을 정리해보도록 하겠습니다. 개인적인 설명은 기울임으로 표시해두었으니, 읽으면서 참고하시면 될 것 같습니다.
0. 서론
[ 도입 ]
- SOLID 원칙이 벽과 방에 벽돌을 배치하는 방법을 알려준다면, 컴포넌트 원칙은 빌딩에 방을 배치하는 방법을 설명해줌
- 큰 빌딩과 마찬가지로 대규모 소프트웨어 시스템은 작은 컴포넌트들로 만들어짐
- 소프트웨어 컴포넌트와 컴포넌트를 구성하는 요소가 무엇인지, 컴포넌트를 결합하여 시스템을 구성하는 방법에 대해 논의함
12장. 컴포넌트
[ 서론 ]
- 컴포넌트는 시스템의 구성 요소로 배포할 수 있는 가장 작은 단위이며, 자바의 경우 jar 파일이 컴포넌트임
- 컴포넌트는 다양한 형태로 만들어질 수 있음
- 여러 컴포넌트를 서로 링크하여 실행 가능한 단일 파일로 생성할 수 있음
- 여러 컴포넌트를 묶어서 war 파일과 같은 단일 아카이브로 만들 수 있음
- 컴포넌트 각각을 jar과 같이 동적으로 로드 할 수 있는 플러그인이나 실행 가능한 파일로 만들어 독립적으로 배포할 수 있음
- 컴포넌트가 최종적으로 어떤 형태로 배포되든, 잘 설계된 컴포넌트라면 반드시 독립적으로 개발 가능해야 함
[ 결론 ]
- 런타임에 플러그인 형태로 결합할 수 있는 동적 링크 파일이 이 책에서 말하는 소프트웨어 컴포넌트임
- 여기까지 오는데 50년이 걸렸고, 과거에는 초인적인 노력이 필요했음
- 하지만 이제는 기본으로 쉽게 플러그인 아키텍처를 적용할 수 있게 됨
13장. 컴포넌트 응집도
[ 서론 ]
- 어떤 클래스를 어느 컴포넌트에 포함시켜야 할지는 중요한 결정이므로, 제대로 된 소프트웨어 엔지니어링 원칙이 필요함
- 이 장에서는 컴포넌트 응집도와 관련된 3가지 원칙을 논의함
- REP: 재사용/릴리스 등가 원칙
- CCP: 공통 폐쇄 원칙
- CRP: 공통 재사용 원칙
[ REP: 재사용/릴리스 등가 원칙 ]
- 정의: 재사용 단위는 릴리스 단위와 같다.
- 이는 당연한데, 컴포넌트가 릴리스 절차를 통해 관리되지 않거나, 릴리스 번호가 없다면 재사용하고 싶지도, 할 수도 없음
- 릴리스 번호가 없다면 컴포넌트들이 호환되는지 보증할 수 없음
- 개발자는 새로운 버전이 언제 출시되고 무엇이 변했는지 알아야 함(새로운 버전으로의 통합 여부 및 시기 결정)
- REP를 소프트웨어 설계와 아키텍처 관점에서 보면 다음과 같음
- 단일 컴포넌트는 응집성이 높은 클래스와 모듈들로 구성되어야 함
- 이를 다르게 보면 하나의 컴포넌트로 묶인 클래스와 모듈은 반드시 함께 릴리스 할 수 있어야 한다는 것
- 하나의 컴포넌트로 묶인 클래스와 모듈은 버전이 같고, 동일한 릴리스로 관리되고, 동일한 릴리스 문서에 포함되어야 함
- 이 원칙 만으로는 클래스와 모듈을 단일 컴포넌트로 묶는 방법을 제대로 설명하지 못하지만(약점), 이 원칙 자체는 중요함
- 이 원칙을 어기는건 쉽게 발견할 수 있으며, 이 원칙의 약점은 다음 두 원칙(CCP와 CRP)으로 보완할 수 있음
REP는 재사용성과 관련된 원칙이므로 프로젝트 초기에는 등한시 될 가능성이 높다. 당연하게도 프로젝트 초기에는 당장 개발이 급급하기 때문이다. 이 책에서도 이러한 부분에 대해서 아래에서 다시 다룬다.
[ CCP: 공통 폐쇄 원칙 ]
- 정의: 동일한 이유로 동일한 시점에 변경되는 클래스는 같은 컴포넌트로 묶고, 다른 시점에 다른 이유로 변경되는 클래스는 분리하라
- 대다수의 애플리케이션에서 유지보수성은 재사용성보다 훨씬 중요하며, 변경은 단일 컴포넌트에서 발생해야 함(독립적인 배포)
- 이를 통해 소프트웨어 릴리스, 재검증, 배포와 관련된 작업량을 최소화할 수 있음
- 이는 OCP와도 관련이 있으며, 발생 가능성이 있거나 발생했던 대다수의 동일한 변경에 대해 클래스가 닫혀 있도록 설계해야 함
- CCP는 SRP를 컴포넌트 관점에서 다시 적은 것이며, 단일 컴포넌트는 변경의 이유가 여러 개 있어서는 안됨
- SRP: 서로 다른 이유로 변경되는 메소드를 서로 다른 클래스로 분리하라
- CCP: 서로 다른 이유로 변경되는 클래스를 서로 다른 컴포넌트로 분리하라
한번 변경이 가해진 클래스는 다음에 또 변경이 필요해질 가능성이 높다. 물론 처음부터 변경에 유연하게 설계하는 것이 좋지만, 그렇지 못했다면 이후에 변경된 클래스는 확장에 유연하게 설계하는 것이 좋을 것 같다.
또한 만약 하나의 변경을 위해 여러 모듈들이 수정 및 배포가 필요하다면 리소스가 많이 든다. 이는 컴포넌트 응집도가 떨어진다는 증거이므로 설계에 대해 의심해보도록 하자.
[ CRP: 공통 재사용 원칙 ]
- 정의: 컴포넌트 사용자들을 필요로 하지 않는 것에 의존하게 강요하지 말라.
- CRP도 클래스와 모듈을 어느 컴포넌트에 위치시킬지 결정할 때 도움이 됨
- 같이 재사용되는 경향이 있는 클래스와 모듈들은 같은 컴포넌트에 포함해야 한다고 말함
- 개발 클래스가 단독으로 재사용되는 경우는 거의 없음
- 대체로 재사용 가능한 클래스는 재사용 모듈의 일부로써 해당 모듈의 다른 클래스와 상호작용함
- CRP에서는 이런 클래스들이 동일한 컴포넌트에 포함되어 있어야 한다고 말함
- CRP는 심지어 동일한 컴포넌트로 묶어서는 안되는 클래스도 말해줌
- 어떤 컴포넌트가 다른 컴포넌트를 사용하면, 두 컴포넌트 사이에는 의존성이 생김
- 사용되는 컴포넌트에서 단 하나의 클래스만 사용할 수도 있는데, 그렇다고 해서 의존성은 약해지지 않음
- 이로 인해 사용되는 컴포넌트가 변경될 때마다 같이 변경해야 할 가능성이 높음
- 그러므로 의존하는 컴포넌트가 있다면 해당 컴포넌트의 모든 클래스에 대해 의존함을 확실히 인지해야 함
- 그렇지 않으면 필요 이상으로 많은 컴포넌트를 재배포하느라 노력을 허비하게 됨
- CRP는 어떤 클래스를 한데 묶어도 되는지보다는, 어떤 클래스를 한데 묶어서는 안되는 지에 대해 훨씬 많은 것을 이야기함
- CRP는 강하게 결합되지 않은 클래스들을 동일한 컴포넌트에 위치시켜서는 안 된다고 말함
- CRP는 ISP의 포괄 버전이며, 모두 필요하지 않은 것에 의존하지 말라는 것으로 요약됨
- ISP: 사용하지 않는 메소드가 있는 클래스에 의존하지 말라
- CRP: 사용하지 않는 클래스를 가진 컴포넌트에 의존하지 말라
사용하지 않는 클래스를 갖는 컴포넌트에 의존하면 불필요한 단점들이 많이 생긴다. 대표적으로 빌드 작업을 느리게 만들 뿐만 아니라 불필요한 코드 작업이 필요해질 수 있고, 추가적인 배포까지 필요해지는 상황이 올 수 있다.
회사의 common 모듈에는 Spring Security와 관련된 의존성이 있었다. 하지만 모든 프로젝트에서 Spring Security를 사용하지 않는데, 이로 인해 Security를 사용하지 않는 프로젝트에서는 빌드 시에 Spring Security를 받아야 했으며, 개발 시에는 Spring Security 자동 설정을 제거해주어야 했다. 그 외에도 많은 단점들이 있으므로 사용되지 않는 컴포넌트에는 의존하지 않는 것이 좋다. 클래스 패스를 기반으로 자동 설정을 진행하는 SpringBoot를 사용하는 경우에 특히 그렇다. 나도 모르는 자동 설정들이 진행될 수 있다.
[ 컴포넌트 응집도에 대한 균형 다이어그램 ]
- 세 원칙은 서로 상충되는데, REP와 CCP는 포함 원칙(컴포넌트를 크게)이며, CRP는 배제 원칙(컴포넌트를 작게 만듬)임
- 뛰어난 아키텍트는 3가지 원칙들이 균형을 이루는 방법을 찾아야 함.
- 아래의 균형 다이어그램은 응집도에 관한 세 원칙이 어떻게 상호작용하는지 보여줌
- 각 변은 반대쪽 꼭지점에 있는 원칙을 포기했을 때 감수해야 할 비용을 나타냄
- CCP를 포기하면, 사소한 변경이 생겼을 때 너무 많은 컴포넌트에 영향이 미침
- CRP를 포기하면 불필요한 릴리스가 너무 빈번해짐
- REP를 포기하면 재사용이 어려워짐
- 뛰어난 아키텍트라면 개발팀이 현재 관심을 기울이는 부분을 충족시키는 위치(균형)을 찾아야 함
- 또한 시간이 흐르면서 개발팀이 주의를 기울이는 부분 역시 변한다는 사실도 이해하고 있어야 함
프로젝트 처음부터 재사용성이 필요하지는 않는다. 그러다가 어떤 프로젝트로부터 또 다른 프로젝트가 파생하고 성숙해지다보면 점점 재사용성(REP)이 중요해진다. 그래서 책에서는 개발팀이 주의하는 부분이 오른쪽부터 시작해서 점자 왼쪽으로 이동해간다고 나온다.
[ 컴포넌트 응집도에 대한 균형 다이어그램 ]
- 어느 클래스들을 묶어서 컴포넌트로 만들지를 결정할 때, 재사용성과 개발 가능성이라는 상충하는 힘을 반드시 고려해야 함
- 이들 사이에서 애플리케이션의 요구에 맞게 균형을 맞는 일은 중요하며, 이 균형점은 거의 항상 유동적임
- 즉, 두 힘을 현재 상황에 맞게 잘 분배했더라도, 내년이 되면 맞지 않을 수 있음
- 시간이 지나면서 프로젝트의 초점이 개발 가능성에서 재사용성으로 바뀌고, 컴포넌트의 구성 방식도 조금씩 흐트러지고 또 진화함
프로젝트 초기에는 시스템이 간단하므로 재사용성의 중요도가 떨어진다. 하지만 점차 프로젝트가 커지면서 재사용성에 초점을 가져야 중복을 제거할 수 있고, 계속해서 바뀌는 상황에 맞춰 수정을 해야 한다. 나도 회사에 엉망인 구조를 보면서 많이 아쉬웠던 때가 있었는데, 원인을 파고들다 보니 그 때에는 그것이 올바를 수 있었다는 생각이 들곤 한다. 하지만 엉망인 구조는 결국 시간이 지나면서 진화해야 하는 부분들을 반영하지 못했다는 반증이므로, 변경이 되어야 하는 시점에 변경을 놓치지 않음을 많이 느꼈었다.
위의 3가지는 컴포넌트를 어떻게 구성해야 하는지에 대한 원칙들이다. 이 3가지를 준수하면 결국 하나의 잘 설계된 마이크로서비스가 탄생하게 된다는 결론을 얻었으며, 마이크로서비스의 설계에 좋은 원칙이 될 것 같다.
14장. 컴포넌트 결합
[ 서론 ]
- 지금부터 다룰 3가지 원칙은 컴포넌트 사이의 관계를 설명함
- 마찬가지로 개발 가능성과 논리적 설계 사이의 균형을 다룸
[ ADP: 의존성 비순환 원칙 ]
- 정의: 컴포넌트 의존성 그래프에 순환이 있어서는 안된다.
- 많은 개발자가 동일한 소스 파일을 수정하는 환경에서 코드가 동작하지 않게 될 수 있으며, 2가지 해결방법이 발전되어 옴
- 주단위 빌드
- 순환 의존성 제거하기
주단위 빌드
- 4일은 서로를 신경쓰지 않고, 금요일이 되면 코드를 통합하여 시스템을 빌드함
- 프로젝트가 커지면서 통합에 드는 시간이 계속해서 늘어나게 됨
- 결국 빌드 일정을 늘려야 하고, 통합과 테스트는 수행하기 점점 어려워지며, 빠른 피드백이 주는 장점을 잃게됨
순환 의존성 제거하기
- 이 문제의 해결책은 개발 환경을 릴리스 가능한 컴포넌트 단위로 분리하는 것
- 이를 통해 컴포넌트는 개별 관리자 또는 단일 개발팀이 책임질 수 있는 작업 단위가 됨
- 개발자가 해당 컴포넌트가 동작하도록 만든 후, 릴리스하여 다른 개발자가 사용할 수 있도록 만듬
- 이는 단순하며 합리적이라 널리 사용되지만 컴포넌트 사이의 의존성 구조를 반드시 관리해야 함
- 의존성 구조에 순환이 있어서는 안되며, 컴포넌트 간의 의존성은 비순환 방향 그래프(DAG, Directed Acyclic Graph)여야 함
- 어느 컴포넌트에서 시작하더라도 의존성 관계를 따라 최초의 컴포넌트로 돌아갈 수 없음
- Presenters를 담당하는 팀에서 새로운 릴리스를 만들면 이 릴리스에 영향받는 팀을 쉽게 찾을 수 있음
- Main은 새로 릴리스되더라도 시스템에서 영향받는 컴포넌트가 없음
- 시스템 전체를 릴리스한다면 릴리스 절차는 상향식으로 진행됨(Entities부터 시작해 Main은 마지막에 처리)
- 이처럼 구성요소 간 의존성을 파악하고 있으면 시스템을 빌드하는 방법을 알 수 있음
순환이 컴포넌트 의존성 그래프에 미치는 영향
- 요구사항으로 Entities에 포함된 클래스 하나가 Authorizer의 클래스 하나를 사용할 수 밖에 없다면 순환 의존성이 발생함
- 이는 즉각적으로 문제를 일으키게 됨
- Database 컴포넌트 개발자는 컴포넌트를 릴리스 하려면 Entities, Authorizer, Interactors 모두와 호환되어야 함
- 세 컴포넌트는 하나의 거대 컴포넌트가 되며, 개발자 서로가 얽매여 모두 항상 정확하게 동일한 릴리스를 사용해야 함
- Entities를 테스트하려면 Authorizer와 Interactors도 빌드하고 통합해야 하면서 어려워짐
- 모듈의 개수가 많아짐에 따라 빌드 관련 이슈는 기하급수적으로 증가함
- 컴포넌트를 어떤 순서로 빌드해야 올바른지 파악하기 힘들어지며, 올바른 순서라는 것 자체가 없을 수 있음
순환 끊기
- 컴포넌트 사이의 순환을 끊고, DAG로 복구하는 것은 언제든 가능하며, 의존성 역전 원칙 또는 새로운 컴포넌트 생성으로 가능함
- 의존성 역전 원칙
- 다음의 그림처럼 User가 필요로 하는 메소드를 제공하는 인터페이스를 제공함
- 그리고 이 인터페이스는 Entities에, 구현체는 Authorizer에 위치시킴
- 새로운 컴포넌트 생성
- Entities와 Authorizer가 의존하는 새로운 컴포넌트를 만듬
- 그리고 두 컴포넌트가 모두 의존하는 클래스들을 새로운 컴포넌트로 이동시킴
흐뜨러짐(Jitters)
- 두 번째 해결책이 시사하는 바는 요구사항이 변경되면 컴포넌트 구조도 변경될 수 있다는 사실
- 실제로 애플리케이션이 성장하면서 컴포넌트 의존성 구조는 서서히 흐트러지며 또 성장함
- 따라서 의존성 구조에 순환이 발생하는지를 항상 관찰해야 하며, 어떤 식으로든 끊어내야 함
또 한번 나와서 지겹겠지만, 상황에 맞는 구조는 계속 변경된다는 것. 오늘의 정답은 내일의 정답이 아닐 수 있음을 명심하자.
[ 하향식(top-down) 설계 ]
- 프로젝트 초기에는 컴포넌트 구조를 설계할 수 없음. 즉, 컴포넌트 구조는 하향식(top-down)으로 설계될 수 없음
- 컴포넌트 의존성 다이어그램은 애플리케이션 기능과는 거의 관련이 없고, 빌드 가능성과 유지보수성의 지도와 같음
- 빌드 및 유지보수할 소프트웨어가 없다면, 지도 또한 필요 없으므로 컴포넌트 구조는 프로젝트 초기에 설계할 수 없음
- 컴포넌트는 시스템에서 가장 먼저 설계할 수 있는 대상이 아니며, 시스템이 성장하고 변경될 때 함께 진화함
- 하지만 프로젝트 초기에 모듈들이 점차 쌓이기 시작함
- 의존성 관리에 대한 요구가 점차 늘어나고, 변경되는 범위가 시스템의 가능한 한 작은 일부로 한정되기를 원함
- 그래서 단일 책임 원칙과 공통 폐쇄 원칙에 관심을 갖고, 이를 적용해 함께 변경되는 클래스는 같은 위치에 배치시킴
- 의존성 구조와 관련된 최우선 관심사는 변동성의 격리(자주 변경되는 컴포넌트로 부터 다른 컴포넌트를 보호함)
- 애플리케이션이 계속 성장하면서 재사용 가능한 요소를 만드는 일에 관심을 기울이기 시작함
- 이 시점에는 컴포넌트를 조합하는 과정에 공통 재사용 원칙이 영향을 미치기 시작함
- 결국 순환이 발생하면 ADP가 적용되고, 컴포넌트 의존성 그래프는 조금씩 흐트러지고 또 성장함
책에서는 아래와 같이 프로젝트 초기부터 컴포넌트를 설계하려고 하면 위험하다고 나온다. 컴포넌트의 관계는 시스템이 발전하면서 변하는 것이므로, 프로젝트 초기에 설계하지 않도록 주의가 필요할 것 같다.
"아무런 클래스도 설계하지 않은 상태에서 컴포넌트 의존성 구조를 설계하려고 시도하면 큰 실패를 맛볼 수 있다. 공통 폐쇄 원칙에 대해 그다지 파악하지 못하고 있고, 재사용 가능한 요소도 알지 못하며, 컴포넌트를 생성할 때 거의 확실히 순환 의존성이 발생할 것이다. 따라서 컴포넌트 의존성 구조는 시스템의 논리적 설계에 발맞춰 성장하며 또 진화해야 한다."
[ SDP: 안정된 의존성 원칙 ]
- 정의: 안정성의 방향으로(더 안정된 쪽에) 의존하라.
- 컴포넌트 중 일부는 변동성을 지니는데, 이때 변경이 어려운 컴포넌트가 변동이 예상되는 컴포넌트에 절대 의존해서는 안됨
- 변경이 어려운 컴포넌트에 한번 의존하게 되면 변동성이 큰 컴포넌트도 결국 변경이 어려워짐
- 즉, 변경하기 쉽도록 모듈을 설계해도 이 모듈에 누군가가 의존성을 메달아 버리면 이 모듈도 변경하기 어려워짐
- 이때 안정된 의존성 원칙을 준수하면 변경하기 어려운 모듈이, 변경하기 쉽게 만들어진 모듈에 의존하지 않도록 만들 수 있음
친구들끼리 마틴체라고 하는데... 위의 내용을 쉽게 설명하면 변경이 어려운 컴포넌트는 최대한 독립적으로 만들라는 것이다. 그렇지 않으면 변경의 어려움이 다른 컴포넌트에도 전파가 되므로, 이를 막아야 한다는 것이다.
테스트에서도 비슷한 경우가 있는데, 테스트하기 어려운 모듈에 의존하면 나머지 모듈들도 테스트가 어려워진다. 예를 들어 오늘의 요일을 구하는 메소드를 애플리케이션 계층에서 사용한다면, 그 계층은 테스트가 어려워진다. 관련 영상도 있으니 참고하도록 하자.
안정성
- 안정성은 변경의 발생 빈도와는 직접적인 관련이 없고, 변경을 위해 필요한 작업령과 관련됨
- 안정적이라는 것은 변경을 위해 상상한 수고를 감수해야 한다는 것
- 소프트웨어 컴포넌트를 변경하기 어렵게 만드는 많은 요인이 존재함(컴포넌트의 크기, 복잡도, 간결함 등)
- 변경이 어렵게 만드는 확실한 방법 하나는 수 많은 다른 컴포넌트가 해당 컴포넌트에 의존하게 만드는 것
- 컴포넌트 안쪽으로 들어오는 의존성이 많아지면 상당히 안정적이라고 볼 수 있음
- 왜냐하면 사소한 변경이라도 의존하는 모든 컴포넌트를 만족시키면서 변경하려면 상당한 노력이 들기 때문임
- X는 안정된 컴포넌트인데, 세 컴포넌트가 X에 의존하며 X는 변경하지 말아야 할 이유가 3가지나 됨
- 이때 X는 세 컴포넌트를 책임진다고 말하며, 반대로 X는 어디에도 의존하지 않음
- X가 변경되도록 만들 수 있는 외적인 영향이 전혀 없으므로, X는 독립적이라고 말함
- 아래의 Y는 상당히 불안정한 컴포넌트임
- 어떤 컴포넌트도 Y에 의존하지 않으므로 Y는 책임성이 없음
- Y는 세 컴포넌트에 의존하므로 변경이 발생할 수 있는 외부 요인이 3가지이므로, Y는 의존적이라고 함
안정성 지표
- 컴포넌트로 들어오고 나가는 의존성의 개수를 통해 컴포넌트의 불안정성(I)을 계산할 수 있음
- fan-in: 안으로 들어오는 의존성으로 컴포넌트 내부의 클래스에 의존하는 컴포넌트 외부의 클래스 개수
- fan-out: 바깥으로 나가는 의존성으로 컴포넌트 외부의 클래스에 의존하는 컴포넌트 내부의 클래스 개수
- 불안정성(I)은 fan-out / (fan-in + fan-out)으로 계산 가능하며, [0, 1] 사이의 값을 가짐
- 불안정성(I)가 0인 경우(X)
- 해당 컴포넌트에 의존하는 다른 컴포넌트는 있지만, 해당 컴포넌트 자체는 다른 컴포넌트에 의존하지 않음
- 이는 컴포넌트가 가질 수 있는 최고로 안정된 상태이며, 이러한 컴포넌트는 다른 컴포넌트를 책임지며 독립적임
- 자신에게 의존하는 컴포넌트가 있으므로 변경이 어렵지만, 해당 컴포넌트를 변경하도록 강제하는 의존성은 갖지 않음
- 불안정성(I)가 1인 경우(Y)
- 어떤 컴포넌트도 해당 컴포넌트에 의존하지 않지만, 해당 컴포넌트는 다른 컴포넌트에 의존함
- 최고로 불안정한 컴포넌트이며 책임성이 없으므로 의존적임
- 의존하는 컴포넌트가 없으므로 변경하지 말아야 할 이유가 없음
- 반대로 이 컴포넌트가 다른 컴포넌트에 의존한다는 사실은 언젠가 해당 컴포넌트를 변경할 이유가 있다는 뜻임
불안정성은 명확하게 수치로 계산할 수 있는 값이므로, 인텔리제이 플러그인으로 지원해주면 재미있을 것 같다는 생각이 들었다. 혹은 이미 있을 수도 있으니 나중에 천천히 찾아봐야겠다!
모든 컴포넌트가 안정적이어야 하는 것은 아니다
- 모든 컴포넌트가 최고로 안정적인 시스템이라면 변경이 불가능한데, 이는 바람직하지 않음
- 우리가 기대하는 것은 불안정한 컴포넌트와 안정된 컴포넌트가 모두 존재하는 상태
- 위 다이어그램은 세 컴포넌트로 구성된 시스템이 갖는 이상적인 구조임
- 상단에는 변경 가능한 컴포넌트들이 있고, 하단의 안정된 컴포넌트에 의존함
- 위로 향하는 화살표가 있으면 SDP에 위배되는 것인데, 존재하지 않음
- Flexible은 변경하기 쉽도록 설계한 컴포넌트임
- 우리는 Flexible이 불안정한 상태이기를 바라지만, Stable에서 Flexible에 의존성을 걸게 되면 SDP를 위배함
- Stable의 I 지표는 Flexible의 I 지표보다 작고, 결국 Flexible은 변경이 어렵게 됨
- Flexible을 변경하려면 Stable과 Stable에 의존하는 나머지 컴포넌트에도 조치를 취해야 함
- 이를 해결하려면 Flexible에 대한 Stable의 의존성을 끊어야 함
- 예를 들어 Stable의 내부 클래스 U가 Flexible의 내부 클래스 C를 사용할 때, DIP를 도입하면 이 문제를 해결할 수 있음
- US라는 인터페이스를 생성하고 이를 UServer 컴포넌트에 넣은 후 C가 해당 인터페이스를 구현하도록 만듬
- 이를 통해 Flexible에 대한 Stable의 의존성을 끊고, 두 컴포넌트는 모두 UServer에 의존하도록 강제함
- UServer는 매우 안정되며(I=0) Flexible은 불안정성(I=1)을 유지할 수 있고, 모든 의존성은 I가 감소하는 방향으로 향함
- 오로지 인터페이스만을 포함하는 컴포넌트(UServer)를 생성하는 방식이 이상하게 보일 수도 있음
- 하지만 자바와 같은 정적 타입 언어에서는 이 방식이 흔히 사용되며 꼭 필요한 전략으로 알려져 있음
- 이러한 추상 컴포넌트는 상당히 안정적이며, 따라서 덜 안정적인 컴포넌트가 의존할 수 있는 이상적인 대상임
[ SAP: 안정된 추상화 원칙 ]
- 정의: 컴포넌트는 안정된 정도만큼만 추상화되어야 한다.
고수준 정책을 어디에 위치시켜야 하는가?
- 시스템에는 고수준 아키텍처나 업무 로직과 같은 정책 결정과 같은 변경해서는 안되는 소프트웨어도 있음
- 따라서 고수준 정책을 캡슐화하는 소프트웨어는 안정된 컴포넌트에, 변동성이 큰 소프트웨어는 불안정한 컴포넌트에 포함시켜야 함
- 하지만 고수준 정책을 안정된 곳에 위치시키면, 그 정책을 포함하는 소스 코드 수정이 어려워져 시스템 전체 아키텍처가 유연성을 잃음
- 컴포넌트가 최고로 안정된 상태이면서 동시에 변경에 충분히 대응할 수 있도록 유연하려면 OCP를 지켜야 함
- OCP는 클래스를 수정하지 않고도 확장이 충분히 가능할 정도로 클래스를 유연하게 만들 수 있을 뿐만 아니라 바람직한 방식임
- 이 원칙을 준수하는 클래스는 바로 추상 클래스임
안정된 추상화 원칙(SAP)
- 안정된 추상화 원칙은 안정성과 추상화 정도 사이의 관계를 정의함
- 안정된 컴포넌트는 추상 컴포넌트여야 하며, 이를 통해 안정성이 컴포넌트를 확장하는 일을 방해해서는 안됨
- 불안정한 컴포넌트는 반드시 구체 컴포넌트로써, 컴포넌트가 불안정하므로 내부의 구체적인 코드를 쉽게 변경할 수 있어야 함
- 따라서 안정적인 컴포넌트라면 반드시 인터페이스와 추상 클래스로 구성되어 쉽게 확장할 수 있어야 함
- 안정된 컴포넌트가 확장이 가능해지면 유연성을 얻게 되고, 아키텍처를 과도하게 제약하지 않게 됨
- SAP와 SDP를 결합하면 컴포넌트에 대한 DIP나 마친가지임
- SDP에서는 의존성이 반드시 안정성의 방향으로 향해야 한다고 말함
- SAP에서는 안정성이 결국 추상화를 의미한다고 말함
- 따라서 의존성의 방향은 추상화의 방향으로 향하게 됨
- 하지만 DIP는 클래스에 대한 원칙이며, 클래스의 경우 중간은 존재하지 않음(추상적이거나 or 구체적이거나)
- SDP와 SAP의 조합은 컴포넌트에 대한 원칙이며, 컴포넌트는 어떤 부분은 추상적이면서 다른 부분은 안정적일 수 있음
주계열
- 안정성(I)과 추상화 정도(A) 사이의 관계를 표현하면 다음과 같음
- 최고로 안정적이며 추상화된 컴포넌트는 좌측 상단(0,1), 최고로 불안정하며 구체화된 컴포넌트는 (1,0)에 위치함
- 모든 컴포넌트가 이 두 지점에 위치하지는 않으며, 컴포넌트는 추상화와 안정화의 정도가 다양함
- 예를 들어 추상 클래스는 또 다른 추상 클래스로부터 파생가능한데, 이는 추상적이면서도 의존성을 가짐
- 그래서 이 클래스는 최고로 추상적이지만 의존성으로 인해 안정성이 감소하여 최고로 안정적이지는 않을 수 있음
- 컴포넌트가 위치할 수 있는 합리적인 궤적을 표현하면 다음과 같음
- 고통의 영역(Zone of Pain)
- (0, 0) 주변 구역에 위치한 컴포넌트들
- 매우 안정적이며 구체적인데, 컴포넌트가 뻣뻣한 상태이므로 바람직하지 않음
- 추상적이지 않으므로 확장이 어렵고, 안정적이므로 변경이 어려움
- 제대로 설계된 컴포넌트라면 여기에 위치하지 않으며, 배제해야 하는 구역임
- ex) 데이터베이스 스키마 or String 클래스(String은 변동성이 없으므로 해롭지는 않음) 등
- 쓸모없는 구역(Zone of Uselessness)
- (1, 1) 주변 구역에 위치한 컴포넌트들
- 최고로 추상적이지만, 누구도 그 컴포넌트에 의존하지 않음(쓸모 없음)
- 이는 누구도 구현하지 않은 채 남겨진 추상클래스인 경우가 많음
- 주계열(The Main Sequence)
- 변동성이 큰 컴포넌트들을 배제 구역으로부터 가능한 멀리 떨어뜨리는 선분
- 자신의 안정성에 비해 너무 추상적이지도 않고, 추상화 정도에 비해 너무 불안정하지도 않음
- 쓸모없지 않으면서도 심각한 고통을 안겨주지도 않음
- 추상화된 수준에 어울릴 정도로만 다른 컴포넌트가 의존하며, 구체화된 수준에 어울릴 정도로만 다른 컴포넌트에 의존함
- 가장 바람직한 지점은 주 계열의 종점이긴 하지만 일부 컴포넌트는 불가능할 수 있음
[ 결론 ]
- 의존성 관리 지표는 설계의 의존성과 추상화 정도가 내가 “흘륭한” 패턴이라고 생각하는 수준에 얼마나 잘 부합하는지를 측정함
- 좋은 의존성도 있지만 좋지 않은 의존성도 있는데, 이 패턴은 이를 반영 하지만 지표는 신이 아님
- 지표는 임의로 결정된 표준을 기초로 한 측정값에 지나지 않으며, 지표는 아무리 해도 불안정함
- 하지만 이들 지표로부터 무언가 유용한 것을 찾을 수 있기를 바람
"안정된 정도만큼만 추상화되어야 한다"는 얘기는 지나친 추상화도 좋지 않다는 것이다. 추상화를 수준에 맞지 않게 극단적으로 해버리면 코드의 추적성(traceability)가 매우 떨어진다. 그러므로 안정된 만큼만 추상화하는 것은 매우 중요하다.
위의 내용은 로버트 C 마틴의 클린 아키텍처 책을 읽고 정리한 내용입니다. 개인적인 설명은 기울임으로 표시해두었으니, 읽으면서 참고하시면 될 것 같습니다! 혹시 추가적인 의견 있으면 편하게 댓글 남겨주세요ㅎㅎ
관련 포스팅
- 클린 아키텍처 1부 소개 - 내용 정리 및 요약
- 클린 아키텍처 2부 벽돌부터 시작하기: 프로그래밍 패러다임 - 내용 정리 및 요약
- 클린 아키텍처 3부 설계 원칙 - 내용 정리 및 요약
- 클린 아키텍처 4부 컴포넌트 원칙 - 내용 정리 및 요약
- 클린 아키텍처 5부 아키텍처 - 내용 정리 및 요약
- 클린 아키텍처 6부 세부사항 - 내용 정리 및 요약