상세 컨텐츠

본문 제목

[Architecture] 4부 컴포넌트 원칙 : 14장 컴포넌트 결합

프로그래밍/Architecture

by jisooo 2024. 1. 14. 17:42

본문

 

 

#cleanArchitecture #클린아키텍처 #컴포넌트원칙 #컴포넌트결합 #UncleBob

클린 아키텍처 14장에서는,

세 가지 원칙을 설명하며 컴포넌트 사이의 관계를 설명한다.

앞장에서도 강조했던 개발 가능성과 논리적 설계 사이의 균형을 다루고 있다.

또한 저자는 컴포넌트 구조와 관련된 아키텍처는 기술적이며, 정치적이고 가변적이라는 특성을 같이 언급한다.

이번 장에서 설명한 3가지 원칙을 각각 살펴보자.


ADP : 의존성 비순환 원칙

개발자들에게 "숙취 증후군이란?"

많은개발자가 동일한 소스 파일을 수정하는 환경에서 발생하는 현상을 일컫는다.

프로젝트와 개발팀의 규모가 커지면서 이러한 숙취는 개발자들에게 지독한 악몽을 선사하게 된다.

코드를 수정해서 빌드해야하는데 빌드는 커녕 개발팀 모두가 누군가 마지막으로 수정한 코드 때문에

망가진 부분이 동작하도록 다시 고치기 위해 코드를 수정하는 작업만이 반복될 뿐이다.

이러한 숙취증후군을 해결하기 위해 필자는

"주 단위 빌드" 와 "의존성 비순환 원칙"을 해결책으로 제시하고 있다.

주 단위 빌드

주 단위 빌드는 CI / CD에서 CI (지속적 통합)의 내용과 어느정도 상통하는 해결책이다.

모든 개발자는 1주일중에서 약속한 날짜 간격대로 각자 작업한 코드의 내용을 통합하여 시스템을 빌드한다.

이 정기적인 통합 / 배포 날짜 이외의 시간에는

개발자는 모두 코드를 개인적으로 remote 코드 저장소에서 내려받아 각자 필요한 작업을 하며,

전체적인 기준에서 작업을 어떻게 통합할지는 걱정하지 않는다.

하지만 프로젝트가 커지면 개발보다 통합에 드는 시간이 늘어나면서,

팀의 효율성도 서서히 나빠지게 된다.

이같은 흐름은 효율성을 유지하기 위해 결국 빌드 일정을 계속 늘리게 되고,

빌드 주기가 늦어질수록 프로젝트가 감수해야할 위험은 커지게 된다.

통합과 테스트를 수행하기가 점점 더 어려워지고,

팀은 빠른 피드백이 주는 장점을 잃게 된다.

순환 의존성 제거하기

위와 같이 주 단위 빌드 역시 생기는 문제점에 대한 해결책은,

개발 환경을 릴리즈 가능한 컴포넌트 단위로 분리하는 것이다.

이를 통해 컴포넌트는 개별 개발자 또는 단일 개발팀이 책임질 수 있는 작업 단위가 된다.

즉 개발자가 해당 컴포넌트가 동작하도록 만든 후,

해당 컴포넌트를 릴리즈하면, 다른 개발자가 사용할 수 있도록 한다.

담당 개발자는 이 컴포넌트에 릴리즈 번호를 부여하고, 다른 팀에서 사용할 수 있는 디렉토리로 이동시킨다.

그런 다음 개발자는 자신만의 공간에서 해당 컴포넌트를 지속적으로 수정한다.

나머지 개발자는 릴리즈된 버전을 사용한다.

이렇게 컴포넌트 단위로 분리하면, 어떤 팀도 개발하면서 다른 팀에 의해 좌우되지 않는다.

각 팀은 특정 컴포넌트가 새롭게 릴리즈되면, 자신의 컴포넌트를 해당 컴포넌트에 맞게 수정할 시기를 스스로 결정할 수 있다.

지금 당장 신규 릴리즈 버전을 적용할 것이 아니라면, 하던 다른 작업을 계속 하면 되고,

일정이 가능할 때 신규 릴리즈 버전을 적용할지 여부를 결정해서 그에 맞춰 코드를 수정할 수 있다.

즉 통합은 작고 점진적으로 이루어지게 되는 것이다.

특정 시점에 모든 개발자가 한데 모여서 진행 중인 작업을 모두 통합하는 일은 사라진다.

이 절차가 성공적으로 동작하려면,

컴포넌트 사이의 의존성 구조를 반드시 관리해야 한다.

이 때 의존성 구조에 순환이 있어서는 안된다.

의존성 구조에 순환이 생기면 앞에서 언급한 "숙취 증후군"을 다시 맞이하게 된다.

위 이미지는 컴포넌트 다이어그랩인데,

이렇게 각 컴포넌트를 조립하여 애플리케이션을 만드는 구조를 한눈에 볼 수 있다.

이 그림에서 중요한점은 화살표를 통해서 알수 있는 컴포넌트 간의 "의존 구조"이다.

Presenters 컴포넌트를 예시로 들면,

Presenters 컴포넌트 방향으로 화살표를 뻗은 View, Main 컴포넌트는 즉 Presenter 컴포넌트에 의존하고 있다.

또한 Presenters 입장에서는 Interactors 컴포넌트 방향으로 화살표를 뻗고 있으므로,

Presenters 컴포넌트는 Interactors 컴포넌트에 의존하고 있다고 볼 수 있다.

이렇게 컴포넌트들이 특정 컴포넌트에 의존한다는 것은,

의존 대상 컴포넌트의 코드가 변경될 때, 의존하고 있는 다른 컴포넌트들도 그에 따른 영향을 받는다는 것이다.

다시 위 예시로 돌아가서,

View와 Main 컴포넌트 입장에서는 Presenters 컴포넌트에 의존하고 있으므로,

Presenters 컴포넌트의 새로운 릴리즈와 자신의 작업물을 언제 통합할지를 반드시 결정해야 한다.

또한 Main 컴포넌트 방향으로 의존하는 컴포넌트는 0개이므로,

이말은 즉, Main이 새로 릴리즈 되더라도 시스템에서 이로 인해 영향받는 컴포넌트는 전혀 없다는 뜻이다.

그리고 Presenters 입장에서는 이 컴포넌트를 테스트하고자 한다면,

다른 컴포넌트들과의 의존관계가 없으므로 현재 버전의 Interactos와 Entities 컴포넌트들을 이용해서 빌드하고 테스트하면 된다.

즉 Presenters를 만드는 개발자가 테스트를 구성할 때, 대체로 적은 노력이 든다는 뜻이며,

고려해야 할 변수도 상대적으로 적다는 뜻이다.

시스템 전체를 릴리즈할 때는 그 절차가 상향식으로 진행된다.

먼저 Entities를 컴파일, 테스트, 릴리즈한다.

그리고나서 Database, Interacors,

그다음 Presenters, View, Controllers, Authorizer

그다음 마지막으로 Main 컴포넌트도 위와 같은 과정이 진행된다.

순환 컴포넌트가 의존성 그래프에 미치는 영향

위 이미지처럼, Entities에 포함된 클래스 하나가 Authorizer에 포함된 클래스 하나를 사용하도록 변경된다고 가정하자.

그러면 Interactors, Entities, Authorizer 이 세 컴포넌트 사이에 "순환"이 발생한다.

이렇게 해서 발생하는 문제점은 무엇일까?

Database 컴포넌트의 입장에서보면 Entities와 Interactors 컴포넌트에 의존하고 있다.

그런데 Entities 컴포넌트는 Interactors, Entities, Authorizer 컴포넌트들과 "순환"을 이루고 있으므로,

Dtabase 컴포넌트는 또한 Authorizer 컴포넌트와 호환되어야 한다.

이로 인해 Database는 위 순환관계의 컴포넌트들과 모두 항상 정확하게 동일한 릴리즈를 사용해야하며,

앞에서 개발자들의 악몽인 "숙취 증후군"을 또다시 겪게 된다.

이렇게 "순환"이 발생하게 되면, 순환 사이의 컴포넌트들이 하나의 거대한 컴포넌트가 되어버리고,

그 중 하나의 컴포넌트를 다른 컴포넌트에서 의존한다면, 순환관계의 컴포넌트 모두와 얽매이게 되버리는 것이다.

Entities 컴포넌트를 나중에 수정하고 테스트가 필요할 때, 유감스럽게도 Authroizer와 Interactors 까지도

반드시 빌드하고 통합해야 한다.

컴포넌트 상에 이 정도까지 결합이 발생하면 문제가 될 뿐만 아니라, 받아들이기 어려워진다.

여러 클래스중 하나에 간단한 단위테스트를 실행하는데,

왜이렇게도 많은 라비르러리와 다른 사람들의 작업물들을 포함해야하는 상황이 있었다면,

문제는 의존성 그래프에 순환이 있기 때문이라는 사실을 발견할 수 있다.

이처럼 컴포넌트 간 순환이 생기면, 컴포넌트를 분리하기가 상당히 어려워진다.

단위 테스트를 하고 릴리즈 하는일도 굉장히 어려워지고 에러도 쉽게 발생한다.

게다가 모듈의 개수가 많아짐에 따라 빌드 관련 이슈는 기하급수적으로 증가한다.

또한 컴포넌트를 어떤 순서로 빌드해야할지 파악하기도 상당히 복잡하고 힘들어진다.

특히 자바와 같이 컴파일된 바이너리 파일에서 선언문을 읽어들이는 언어라면 끔찍한 문제가 일어나게 될 것이다.

순환 끊기

위의 컴포넌트 상태에서 순환을 끊는 방법은 아래와 같다.

1) 의존성 역전 법칙(DIP)를 적용한다.

위의 그림처럼 Entity 컴포넌트에 속하는 User 클래스에서 호출하는 외부 컴포넌트용 메서드를 제공하는 별도의 인터페이스를 작성한다.

그리고 이 인터페이스는 Entities에 위치시키고, Authorizer에서는 이 인터페이스를 상속받는다.

그럼 기존 Entities -> Authorizer 사이에 있던 의존성 방향을 이 추가된 인터페이스를 통해 역전시킬수 있고,

이를 통해 Entities, Authorizer, Interactors 컴포넌트 사이에 있던 순환을 끊을 수 있다.

2) Entities와 Authorizer가 모두 의존하는 새로운 컴포넌트(Permissions)를 만든다.

그리고 두 컴포넌트가 의존하는 클래스들을 새로운 컴포넌트(Permissions)로 이동시킨다.

위 해결책을 통해 우리는, 시간이 지나면서 어플리케이션의 요구사항이 변경되면

컴포넌트의 구조로 얼마든지 변경될 수 있다는 사실이다.

실제로 어플리케이션이 성장함에 따라 의존성 구조는 서서히 흐트러지며 또 성장한다.

따라서 의존성 구조에 순환이 발생하는지를 항상 관찰해야 한다.

앞에서 살펴본 순환의 부작용을 확인하였으므로, 순환이 생긴다면 위에서 제시한 해결책들로 끊어낼야한다.

이말은 때론 새로운 컴포넌트를 생성하거나 의존성 구조가 더 커질 수 있음을 의미한다. (그림 14.4 참조)


하향식(top-down) 설계

의존성 비순환 원칙에서 살펴본 내용에 따르면, 컴포넌트 구조는 하향식으로 설계될 수 없다.

컴포넌트는 시스템에서 가장 먼저 설계할수 없고 오히려 시스템이 성장하고 변경될 때 함께 진화한다.

위에서 제시한 그림들에서 보인 "컴포넌트 의존성 다이어그램"은 애플리케이션의 기능을 기술하는 역할이 아닌,

어플리케이션의 빌드 가능성, 유지 보수성을 보여주는 지도와 같다.

따라서 컴포넌트 구조는 프로젝트 초기에 설계할 수 없다.

빌드하거나 유지보수 할 소프트웨어가 없다면 빌드와 유지보수에 관한 지도 또한 필요없기 때문이다.

하지만 점점 모듈들이 쌓이기 시작하면,

"숙취 증후군"을 겪지 않고 프로젝트를 개발하기 위해서 의존성 관리에 대한 요구가 점차 늘어나게 된다.

그렇게 의존성이 강하고 많아지게 됨에 따라서 우리는 변경되는 범위가 시스템의 가능한 한 작은 일부로 한정되기를 원한다.

그래서 결국 단일 책임 원칙, 공통 폐쇄 원칙에 관심을 갖기 시작하고,

이를 적용해 함께 변경되는 클래스는 같은 위치에 배치되도록 만든다.

따라서 컴포넌트 의존성 그래프의 최종 목표는,

자주 변경되는 컴포넌트로부터 안정적이며 가치가 높은 컴포넌트를 보호하려는 아키텍트이다.

애플리케이션이 계속 성장함에 따라 우리는 재사용 가능한 요소를 만드는 일에 관심을 기울인다.

이 때 컴포넌트를 조합하는 과정에 공통 재사용 원칙이 영향을 미치기 시작한다.

결국 순환이 발생하면 ADP(의존성 비순환 법칙)가 적용되고, 컴포넌트 의존성 그래프는 조금씩 흐트러지며 성장한다.


SDP : 안정된 의존성 원칙

어플리케이션의 설계는 시간이 지나다보면 변경이 불가피해진다.

공통 폐쇄 원칙을 준수함으로써, 컴포넌트가 다른 유형의 변경에는 영향받지 않으면서도,

특정 유형의 변경에만 민감하게 만들 수 있다.

이처럼 컴포넌트 중 일부는 의도적으로 "변동성"을 지니도록 설계된다.

변경이 쉽지 않은 컴포넌트가 변동이 예상되는 컴포넌트에 의존하게 만들어서는 절대 안된다.

의존한다는 것은 변동성이 큰 컴포넌트도 본인에 의존하는 다른 컴포넌트때문에 변경이 어려워진다.

다시말해

우리가 모듈을 만들 땐, 변경하기 쉽게 초기에 설계하였지만,

이 모듈에 다른 컴포넌트의 의존성을 누군가가 매달아버리면 우리의 모듈도 변경하지 어려워지는 것이다.

이 때 "안정된 의존성 원칙"을 준수하면

변경하기 어려운 모듈이

변경하기 쉽게 만들어진 모듈에 의존하지 않도록 만들 수 있다.

소프트웨어 컴포넌트의 변경을 어렵게 만드는 것은 위처럼

수많은 컴포넌트가 해당 컴포넌트에 의존하는 상황일 것이다.

소프트웨어의 "안정성"의 측면에서 보면,

컴포넌트 안쪽으로 들어오는 의존성이 많아지면 상당히 안정적이라고 볼 수 있는데,

안정적이라는 것은 사소한 변경이라도 의존하는 모든 컴포넌트를 만족시키면서 변경하려면

상당한 노력이 들기 때문이다.

위 그림에서 X는 3개의 컴포넌트가 의존하고 있다.

따라서 X 컴포넌트는 "변경하지 말아야할 이유"가 3가지나 되기 때문에 안정된 컴포넌트라고 할 수 있다.

반대로 X는 어떤 컴포넌트에도 의존하지 않으므로 X가 변경되도록 만들수 있는 외적인 영향이 전혀 없다.

따라서 X는 독립적인 컴포넌트이기도 하다.

이번에 Y는 상당히 불안정한 컴포넌트이다.

총 3개의 컴포넌트에 의존하고 있고, 그만큼 많은 컴포넌트가 변경되면 함께 변경해야함을 의미한다.

즉 변경이 발생할 수 있는 외부 요인이 세가지이며 상당히 불안정한 컴포넌트라고 할 수 있다.

모든 컴포넌트가 최고로 안정적인 시스템이라고 가정한다면, 이말인 즉슨 변경이 불가능한 구조라는 것이다.

바람직한 상황이 아닌 것이다.

사실 우리가 컴포넌트를 설계할때 기대하는건 불안정한 컴포넌트와 안정된 컴포넌트의 공존이다.

14.8 그림을 보면 위쪽에는 변경 가능한 컴포넌트가 보이고, 아래의 안정된 컴포넌트에 의존한다.

다이어그램에서 불안정한 컴포넌트는 위처럼 관례적으로 위쪽에 두는데, 이를 이상적인 구성이라고 본다.

하지만,아래 그림을 보면 SDP가 위배되는 점을 확인할 수 있다.

우리는 "Flexible"이란 이름 그대로 변경에 유연한 컴포넌트를 설계했다고 가정하자.

하지만 Stable 컴포넌트가 Flexible 컴포넌트에 의존성을 걸게 되었다.

따라서 Flexible 컴포넌트를 나중에 변경하게 되면,

이제 Stable과 Stable에 의존하는 나머지 컴포넌트에도 어떤 조치를 취해야한다.

이를 해결하기 위해 Stable <-> Flexible 사이에 있는 의존성을 어떤식으로든 끊어놔야 하는데

이때도 DIP (의존성 역전 법칙)을 통하여 의존성 방향을 역전시킬 수 있다.

Stable 내부의 클래스가 Flexible 내부의 클래스를 사용함으로써 의존하고 있다고 가정해보자.

위 그림처럼 US라는 인터페이스를 추가하고, Stable 내부에서 Flexible 내 클래스의 메서드를 해당 인터페이스에 선언한다.

그러고 나서, Flexible 컴포넌트 내의 C 클래스가 해당 US 인터페이스를 구현하도록 한다.

이렇게 되면, 기존에 Stable -> Flexible이었던 의존 관계가 US 인터페이스의 추가로 인해 역전된다.

위 그림처럼 UServer 컴포넌트를 Stable, Flexible 각각이 의존하는 형태가 나오게 될 것이다.

UServer는 의존하는 컴포넌트가 없으므로 매우 안정된 상태이며

Flexible은 자신에게 맞는 불안정성을 그대로 유지할 수 있다.

위에서 추가한 US 인터페이스를 "추상 컴포넌트"라고 할 수 있는데,

추상 컴포넌트는 상당히 안정적이며, (상대적으로 덜 변경되는)

따라서 덜 안정적인 컴포넌트 (변경하기 쉬운)가 의존할 수 있는 이상적인 대상이다.


SAP : 안정된 추상화 원칙

고수준 아키텍처나 정책 결정과 관련된 소프트웨어는 반드시 안정된 컴포넌트,

즉 자주 변경해서는 안되는 컴포넌트에 위치해야 한다.

하지만 회사의 사정에 따라 나중에 정책은 언제든지 바뀔 수 있고,

이말은 즉, 정책을 담은 코드도 얼마든지 변경해야하는 니즈가 있다.

하지만 안정된 컴포넌트에 고수준 정책의 코드를 포함시키면 나중에 변경이 어려워질 것이다.

컴포넌트가 최고로 안정된 상태이면서도, 동시에 변경에 충분히 대응할수 있게 유연하게 만들수 있도록

추상클래스를 이용하여 "개방 폐쇄 원칙"을 적용할 수 있다.

안정된 추상화 원칙

안정된 추상화 원칙은 "안정성"과 "추상화" 정도 사이의 관계를 정의한다.

안정된 컴포넌트는 "추상 컴포넌트"여야 하며,

이를 통해 안정성이 컴포넌트를 확장하는 일을 방해해서는 안된다고 말한다.

반대로 불안정한 컴포넌트는 반드시 구체 컴포넌트여야 한다고 말하는데,

컴포넌트가 불안정하므로 컴포넌트 내부의 구체적인 코드를 쉽게 변경할 수 있어야 한다.

따라서 안정적인 컴포넌트라면 반드시 인터페이스와 추상 클래스로 구성되어 쉽게 확장할 수 있어야 한다.

안정된 컴포넌트가 확장 가능해지면 유연성을 얻게 되어 아키텍처를 과도하게 제약하지 않게 된다.

의존성은 반드시 안정성(덜 변하는)이 높은 컴포넌트 방향으로 향해야 하고,

SAP에서는 안정성이 결국 "추상화"를 의미한다고 말한다.

따라서 컴포넌트 다이어그램에서 의존성은 추상화의 방향으로 향하게 된다.


정리

이렇게 14장에서는 컴포넌트 간 의존성 관계를 통하여 컴포넌트 / 모듈 간 빌드나 테스트 배포와 유지보수 측면에서

서로의 영향도를 최대한 줄여서 개발자들이 각자 담당한 컴포넌트의 개발에 집중할 수 있도록 한다.

쓸모없이 빌드, 테스트에 번거롭고 불필요한 시간들을 낭비하지 않도록 한다.

현재 컴포넌트들의 의존성을 명확하게 분석하여 의존도를 가능한 줄이고,

이를 위해 개발 환경을 릴리즈 가능한 컴포넌트 단위로 분리하고,

컴포넌트 간에 의존성 순환이 발생하진 않았는지 꾸준히 분석하고,

순환이 발생했다면 컴포넌트간 의존도가 비대해지기 때문에 "숙취 증후군"에 결국 다시 시달리게 될 것이다.

개발자들은 DIP를 통해 의존성을 어떻게든 끊어내야할 책임감을 가져야한다.

또한 컴포넌트 구성에서

자주 변경되는 컴포넌트와 변경되지 않는 컴포넌트 두가지 측면에서 생각해보면,

자주 변경되는(불안정한) 컴포넌트는 변경되지 않는(안정된) 컴포넌트에 의존하도록 설계해야한다.

만약 이 반대의 방향으로 의존하게 된다면, 불안정한 컴포넌트가 상황에 따라 자주 바뀜으로 인해서,

굳이 바뀔 필요가 없는 안정된 컴포넌트 마저 영향을 받고 수정을 해야하는 가능성이 많기 때문이다.

안정된 컴포넌트라는것은 "추상 컴포넌트"로 선언하여 변경에는 닫혀있지만,

추상클래스나 인터페이스를 활용하여 구현을 통한 확장에 제약이 없도록 해야 한다.

다음에는 총 15장이나 포함하고 있는.. 마의 구간인 5부 아키텍처 포스팅을 이어 작성할 예정이다.

관련글 더보기

댓글 영역