코드 응집도 높이기 (해설편)

1


이 포스트는 let us: Go! 2018 summer 세미나 발표 자료의 해설편 입니다.

2


이 글에서 말하는 응집도란, SOLID원칙 - 객체지향 시스템을 설계하기 위한 원칙 - 에서 말하는 응집도는 높이고, 결합도는 낮춰라 에서의 응집도가 아닙니다.

3


디스크의 조각모음 처럼, 파편화된 코드를 관련성에 따라 인접하게 모으는 것을 응집도를 높인다 라고 표현한 것 뿐입니다. 결국은 코드의 조각모음과 같은 의미 입니다.

4


응집도를 높이면 무엇이 좋아질까요? 관련성이 높은 코드가 응집되어 있기 때문에 읽기 쉽고, 흐름을 파악하기 좋아집니다. 특정 동작의 코드를 찾기 쉬워지고 그만큼 유지관리가 편해집니다.

5


결국 최종 목표는 관련있는 코드를 가장 가까이에 배치하도록 리팩토링 하는 것입니다. 이것을 응집도를 높인다라고 하겠습니다.

6


단순하게 관련성 높은 코드를 가까이 두는 것으로만 작업되면 좋겠지만, 경우에 따라 잘 안되는 경우가 있습니다. 몇 가지 사례를 보면서 제가 해결했던 방법을 나눠보겠습니다.

7


처음 코딩을 시작해서 부터 시간이 지나면서 코드가 여기저기 뒤섞이게 되고 응집도는 떨어지게 됩니다. 코드를 정리하는 것 만으로도 응집도를 높일 수 있습니다.

8


단순히 코드를 연관성에 따라 재배치 하는 것 만드으로 응집도가 높아진 것을 볼 수 있습니다. 코드의 가독성이 높아졌다는 것을 알 수 있죠. 코드를 크게 3부분으로 나눴는데, 데이터 + 로직 + UI 로 구분하였습다. 경우에 따라 더 세분화 시킬 수도 있겠지만, 기본적으로 이렇게 세등분 하는 것이 가장 기본적입니다. (MVC의 기본이네요)

9


두번째 사례입니다. count변수의 값을 라벨에 출력하는 것인데요, 두 개의 함수에 동일 코드가 중복되어 나타나고 있습니다. 같은 코드가 분리되어 응집도가 떨어진 상태 입니다.

10


동일 동작의 코드를 한군데로 통합하여 중복을 줄인다면 그만큼 응집도가 높아진 결과를 얻을 수 있습니다.

11


다음 사례는 구현부의 위치가 정해져 있어 어쩔 수 없이 관련 코드가 분산될 수 밖에 없는 경우를 보여줍니다. 오버라이드된 메소드 내에서 구현해야 하는 경우가 그렇죠. 코드의 위치가 오버라이드된 메소드로 정해져 버리기 때문에 코드가 분산될 수 밖에 없고 응집도는 떨어지게 됩니다.

12


상위 클래스와의 사이에 중간 클래스를 두고 이 것을 상속받도록 처리하고 있습니다. 그리고 이 중간 클래스에서 각 오버라이드 메소드에서 해야할 작업을 클로져로 가지고 있다가 필요시점에 호출해 주는 방식으로 해결을 해보았습니다. 하위 클래스에서는 클로져를 등록하는 것으로 관련 코드를 모두 통합할 수 있게 되었고, 응집도는 높아졌습니다.

13


코드가 분산될 수 밖에 없는 경우에는 Selector 를 쓰는 경우도 있습니다. 이벤트를 처리하고자 하는 동작과 그 처리의 구현이 분리될 수 밖에 없어 응집도가 떨어지는 경우 입니다.

14


Wrapper 클래스를 하나 만들어서 이 상황을 해결해 보았습니다. Wrapper에서는 이벤트의 등록과 Selector를 모두 구현하고 해당 동작을 클로져로 가지고 있다가 필요시점에 호출해 줍니다. 이 Wrapper를 사용하는 코드에서는 이벤트에 대한 등록과 처리가 모두 한군데로 모아질 수 있게 되었고, 응집도가 높아졌습니다.

15


Delegate를 사용하는 경우도 관련 코드가 분리될 수 밖에 없는 경우에 해당합니다.

16


역시 Wrapper를 만들어서 해결해 보았습니다.

17 (생략)

지금까지 5가지의 사례로 응집도가 낮아질 수 밖에 없는 환경을 살펴보고, 해겹방법을 알아봤습니다.
모두 동일한 방식으로 해결되는 것을 보았고, 이 외에 다른 경우가 발생하더라도 어떻게 해결할 수 있을지 감이 오실거라고 생각합니다.

18


이번에는 동일한 이슈이지만 RxSwift 를 사용해서 더 고급스럽고 일관된 표현으로 처리하는 방법을 알아보겠습니다.

19


관련 코드를 데이터 + 로직 + 화면 으로 세등분 하는 것을 앞에서 살펴보았습니다. 하지만 이 분리가 모두 같은 파일에 있을 필요는 없죠. 분류에 따라 서로 다른 파일로 나누는 것도 각 분류 별로 응집도를 높여줄 수 있는 방법일 것입니다.

20


데이터의 변경과 그에 대한 처리를 Relay를 통해서 처리하였습니다. 9페이지에서와 비교해 보면 오히려 코드가 더 복잡해진 것 같기도 합니다.

21


그렇지만 하나의 데이터가 서로 다른 의미로 처리되는 이러한 경우를 9페이지 처럼 처리한다면, 서로 다른 의미의 코드가 한군데에 뒤섞여 버리는 결과를 얻게 될 것입니다. 이러한 것도 역시 가독성을 해치는 측면에서 응집도가 떨어졌다고 할 수 있습니다.
하지만 RxSwift 를 사용하니까 동일 데이터로 서로 다른 처리를 분리처리 하면서도 일관성을 유지하고 있으니 가독성과 응집도 모두 높아진 결과를 얻을 수 있습니다.

22


RxSwift에는 확장된 여러 오픈 소스들이 있습니다. 이 중 RxViewController 를 사용하면 위 예제와 같이 오버라이드해서 구현해야 하는 코드도 일관성있고 응집도 높게 처리가 가능합니다.

23


Selector 로 분리될수 밖에 없던 코드도 RxSwift로 모두 처리가 가능합니다. 동일한 처리 방식, 일관된 코드, 선언적 표현, 높은 응집도를 모두 얻을 수 있습니다.

24


Delegate 처리도 동일하게 처리 가능합니다.
하지만 기본 RxCocoa 에서는 아쉽게도 UIImagePickerController 에는 rx 익스텐션이 제공되지 않습니다. 그렇다면 어떻게 해야 하나요? 물론 다른분들이 만들언 놓은 오픈소스를 활용하면 되겠지만, 이번에는 직접 만들어서 처리해 보겠습니다.

25


어떤 인스턴스에 .rx 했을 때 나오도록 추가하려면 Reactive 를 확장하면 됩니다. 위 소스는 Base가 UIImagePickerController 인 경우에 대해서 rx확장 메소드를 추가하고 있습니다.

26


원리는 16페이지의 Wrapper와 동일합니다. 여기서는 Wrapper 대신에 DelegateProxy 라는 것을 만들게 됩니다. DelegateProxy 를 상속받아 만들게 되는 클래스는 UIImagePickerController 의 Delegate 를 구현하고 여기서 전달되는 결과를 (16페이지에서 클로져로 전달하듯이) subject를 통해서 이벤트로 전달하게 됩니다.
위 코드는 DelegateProxy 를 상속하게 되면 만들어 줘야 하는 코드들이고, 실제 UIImagePickerController의 Delegate 구현부가 핵심이 됩니다.

27


핵심 구현부는 각 Delegate의 결과를 이벤트로 전달 받을 수 있도록 Subject를 하나씩 가지고 있고, Delegate의 구현에서 이 subject에 데이터를 전달하여 이벤트를 발생시키는 방식입니다.

28


이렇게 약간 복잡해 보이지만, 원리는 Wrapper와 동일한 방법을 통해서 rx를 확장하여 기본 제공되지 않는 Delegate 에 대해서도 RxSwift 를 사용한 응집도 높이기가 가능해 집니다.

29 (생략)

30


그럼 지금까지의 사항들을 모두 고려하여 예제를 하나 만들어 보도록 하겠습니다.

31


텍스트 필드 2개, 버튼 1개, 라벨 1개로 구성된 프로그램 입니다. 필드 2개에서 입력받는 숫자를 + 버튼이 눌릴 때 더해서 라베에 표시해 주는 아주 간단한 프로그램 입니다.

32


우선 데이터는 숫자3개가 필요합니다. 더할 두 개의 숫자와 결과 값입니다. 이것이 프로그램에서 사용되는 데이터이기 때문에 Model 을 만들어 두었습니다.

33


데이터를 만들었으니 이젠 로직을 만들겠습니다. 여기서 로직은 더하는 행위밖에 없습니다. 더하는 행위는 순수함수로 만들기 위해 입력값을 받아 새로운 출력값을 전달하도록 구성되었고, 현재의 값을 나타내는 것을 RxCocoa의 BehaviorRelay로 구성하였습니다.

34


ViewController 는 이렇게 구성될 것입니다. 2개의 Input 과 1개의 Output, 1개의 Event 입니다. 각각 Outlet 으로 연결하고 여기에 대해 처리만 하면 완성될 겁니다.

35


처리는 모두 이벤트에 대한 반응을 Reactive 하게 처리하도록 구성하였습니다. 입력값은 기존 값에서 갱신되고 context 의 model 에 다시 저장되도록 처리하였습니다. 버튼이 눌리면 context 의 더하기 함수를 호출하고 그 결과를 다시 context 의 model 에서 유지되도록 하였습니다.
출력은 context.model 을 바라보면서 변경될 때마다 출력을 하고 있습니다.

36


이것을 큰 그림으로 보면 이렇게 구성됩니다. 스토리보드로 만들어진 View와 이 뷰에 연결된 ViewController.
이 ViewController 는 인풋, 이벤트, 아웃풋에 대해서 각각 어떻게 처리할 지를 선언적으로 정의하여 반응하도록 구성되어 있습니다. 그 동작을 하기 위해 현재의 상태 값과 처리 로직을 갖는 Context에 대한 레퍼런스도 갖고 있게 됩니다.
로직을 처리하는 Context 는 Model에서 정의하는 데이터를 사용하고 있고, 현재의 state와 이에 대한 처리 함수를 가지고 있습니다.
처리 로직에서는 View나 ViewController를 의존하고 있지 않습니다. 그리고 화면에 어떻게 보여야 할지는 정의하고 있지도 않습니다. 오직 자신이 처리할 데이터와 그 결과를 만들어 내는 로직으로만 구성되어 있습니다. UnitTest 를 할 대상이 되고, 이 로직만 완벽히 동작한다면 프로그램은 전체적으로 완벽히 동작할 것입니다.

37


코드를 정리하면서 비지니스 로직을 가지고 있는 클래스에 Context 라는 애매한 이름보다는 ViewModel 이라는 그럴듯한 이름으로 변경하였습니다. (View/ViewController 와 Model 사이에 있으니까요)
이것이 MVVM 아키텍쳐의 기본 구성과 동작 방식입니다.

38 (생략)

이 예제의 소스는 여기에서 확인해 보실 수 있습니다.