"모듈화"란 Software를 각 기능별로 분할하여 설계하는 기법이다.
지난 포스팅에서 모듈화가 적용이 안된 모노리틱 앱 구조는
코드의 영향 범위를 파악하기 어렵고, 앱의 규모가 커질 경우 빌드시간이 오래 걸리게 된다.
이에 따라 Swift에서 모듈화를 진행하는 방법에 대해 알아보았다.
하지만, 이러한 물리적인 코드의 분리는 반쪽짜리 모듈화에 지나지 않는다.
"좋은 모듈화"는 용도에 맞게 잘 구분된 기능을 가진 독립적인 모듈로 나누는 것이다.
모듈의 독립성은 응집도와 결합도로 판단하게 된다.
모듈의 "응집도"란 모듈 내부의 기능적인 집중도를 의미한다.
모듈의 "결합도"란 다른 모듈간의 상호 의존성을 의미한다.
즉, 좋은 모듈이란 응집도는 높고 결합도는 낮아야 한다.
이번 포스팅에선 모듈간의 느슨한 결합을 통해 결합도를 낮추는 방법을 알아보자
Dependency
잘 짜여진 모듈은 아래와 같이 계층화를 할 수 있다.
계층화된 모듈에서 하위모듈이 상위 모듈로 접근 못하도록 단방향으로 참조하도록 하는 것이 제일 좋다.
모듈에서의 Dependency는 단일 타겟내의 객체들 간의 Dependency와는 다르다.
이는 상위 모듈에서 하위모듈을 사용하기 위해 "Import로 인한 Dependency"가 있기 때문이다.
모듈에서의 Dependency는 2가지로 나뉜다.
- 런타임 의존성 (코드호출 의존성) : 하위모듈의 함수 호출이나 프로퍼티 접근으로 생기는 의존성
- 컴파일 타임 의존성 (소스코드 의존성) : 하위모듈의 타입이나 객체가 언급될 때 생기는 의존성 (import로 생기는 의존성)
강한 결합의 모듈 (Compile Time 의존성)
컴파일 타임 의존성은 빌드의 순서를 결정 짓기에
생산성을 저해하는 가장 큰 요소이다.
상위 모듈 1은 Build되기 위해선 중위모듈 1,2가 Build되어야 하고,
중위 모듈은 각각 하위 모듈 1,2가 Build되어야 한다.
중위모듈 1의 경우 병렬빌드라 하여도,
하위 모듈 2가 20초가 걸리기 때문에,
25초의 시간이 소요된다.
중위 모듈 2는 6초의 시간이 걸린다.
따라서, 상위모듈 1은 중위모듈 2의 Build가 완료되어도, 중위 모듈 1을 기다려야 하는 현상이 발생한다.
즉, "컴파일 타임 의존성"은
빌드시간이 오래 걸리는 하위 모듈에 따라 빌드시간이 결정 나게 만들며,
생산정의 저하로 일어나게 된다.
DIP (Dependency Inversion Pattern)
절차지향에선
하위 모듈의 함수나 프로퍼티에 접근하기 위해선 (런타임 의존성)
반드시 import를 해야만 했다. (컴파일 타임 의존성)
즉, 두 의존성이 무조건 같은 방향이었다.
하지만, 객체 지향에선 "다형성"을 이용하여
컴파일 타임 의존성을 역전시킬 수 있게 되었다.
Swift에선 Protocol을 통해 컴파일 타임 의존성을 역전시킨다.
상위 모듈은 비즈니스 로직이고,
하위 모듈은 Repository와 UI라 가정하자.
현재 상태는 앱의 비즈니스 로직만 테스트하고 싶지만,
UI와 Repository를 전부 다 빌드해야 되는 상황이 발생한다.
따라서, Implementation과 Interface 모듈을 분리하여,
상위 모듈에 Interface에 대한 컴파일 타임 의존성을 갖게 하는 것이다.
즉, 위의 예시에선 Buiness Logic을 구현체(Repository와 UI)로부터 독립시킬 수 있게 된다.
이렇게 의존성을 역전시키게 되면, 원하는 것만 빌드할 수 있게 되어
빌드시간도 단축되고 테스트도 용이하게 된다.
예를 들어, 쇼핑몰 어플이 있다고 가정하자.
쇼핑몰 홈화면에는 여러 가지 버튼이 있고, 각 버튼마다 특정 View로 Navigate된다.
해당 경우에 홈화면은 각 View의 비즈니스 로직, View와 컴파일 타임 의존성을 갖게 된다면,
홈화면만 개발하면 되는 팀에겐 모든 View들이 빌드되는 시간까지 기다려야 하는 현상이 발생한다.
따라서, DIP를 활용한다면 빌드시간이 줄어들어 병렬 개발이 가능해지고 테스트에도 용이하다.
Composition Root
Interface를 통해 의존성을 역전시켰다면,
실제 구현체를 어디선가 생성해서 주입시켜주어야 한다.
주입해주는 지점을 "Composition Root"라 부른다.
의존성을 역전해도 Composition Root를 제대로 활용하지 못하면 다음과 같은 문제가 발생한다.
초기 화면은 자신이 필요한 의존성만 들고 있다.
하지만, 화면이 추가될수록 다음 화면이 필요한 의존성을 이전화면이 들고 있게 된다.
화면이 많아질수록 상위의 화면의 의존성은 많아지게 된다.
따라서 Composition Root가 의존성을 들고 있고,
주입을 시켜준다면 각 화면은 이전화면에 대한 의존성을 들고 있지 않아도 된다.
즉, 각 화면은 Interface 모듈을 통해 컴파일 타임 의존성을 가지고
이를 Composition Root에서 실체 객체를 주입시켜 주는 것이다.
DIP를 남용하게 된다면,
RunTime에 어떤 구현체가 쓰였는지 파악하기 어렵기에,
코드를 이해하기 어려워진다.
References
'iOS > iOS' 카테고리의 다른 글
[iOS] Infinite Carousel (0) | 2023.07.23 |
---|---|
[iOS] Floating Custom TabBarController (0) | 2023.07.22 |
[iOS] Modularization(1) - Library, Framework, Swift Package (0) | 2023.05.04 |
[iOS] 동시성 프로그래밍(12) - Actors (0) | 2023.02.22 |
[iOS] 동시성 프로그래밍(11) - Unstructured Concurrency (0) | 2023.02.22 |