오늘은 V2G (Vehicle-to-Grid)서비스를 개발하는 EV Studio의 황지희님이 글을 써주셨습니다. 최근 출시된 'EVPedia' 앱 개발 과정에서 겪은 챌린지와 이를 해결하기 위한 리팩토링 솔루션에 대한 글을 공유해드립니다.

TCA를 도입하게된 계기

기존 iOS App 구조

기존의 EVPedia iOS App은 SwiftUI에 Model 그리고 각 시나리오의 흐름을 담당하는 Use case만 사용되고 있었습니다. 아래와 같은 이유들로, Clean Architecture의 모든 구조 혹은 다른 아키텍처의 도입이 불필요하다고 생각되었습니다.

1. EVPedia에서 사용되는 대부분의 기능들(Ex. OAuth, Networking, LocalDB 등)은 모듈화를 통해 이미 구현되어있음
2. 모듈이 모든 기능을 담당하며, 앱은 오직 요청 후 응답값만 반환받음
3. 따라서, 모듈의 추상화되어있는 함수만 호출하면 됨
4. 앱에 종속적인 기능들은 많지 않으며, 지속적으로 모듈화를 진행해 앱이 담당하는 역할을 줄여나갈 계획

즉, ViewModel의 기능이 내재되어있는 SwiftUI의 기능을 적극 활용하며, 이미 Networking(Clean Architecture의 Data layer)을 담당하는 기능은 모듈로 분리되어 있으므로 추가적으로 View와 ViewModel에서 담당할 수 없는 책임과 역할을 담당할 Usecase만 존재하면 충분하다 판단되었습니다.

리팩토링을 진행하려는 이유

점차 앱에 다양한 기능들이 추가되기 시작했고 모듈로 분리할 수 없는, 앱에 종속적인 기능들도 늘어났습니다.

유저의 이벤트를 받아 특정 업무를 수행한 후 UI를 업데이트해야하는데 그 과정에서 서버에 API 요청도 해야하며, 모듈의 기능도 가져다 사용해야했습니다.

점점 View와 Usecase가 거대해지기 시작했습니다.

문제점을 직면한 후, 예전에 보았던 마틴 파울러의 Wokrflow of Refactoring 강연이 생각났습니다.

[참고 - 마틴 파울러의 Wokrflow of Refactoring(OOP 2014)]

마틴 파울러의 Wokrflow of Refactoring

⬆️ 해당 강연에선 아래와 같이 설명하고 있습니다.

개발 과정은 2단계로 나뉘어진다.

  1. 기능 구현단계
    1. 개발자들은 기능을 구현하는 것이 첫번째이고, 기능이 정상 동작 하는 것이 우선이다.
    2. 이 단계에선 코드를 아주 잘 짜야한다는 고민을 깊게 하지 않는다.
  2. 리팩토링 단계
    1. 기능은 이전과 동일하게 동작하면서, 소프트웨어 내부 설계를 더 높은 수준으로 진화시킨다.
    2. 이때 버그가 발견되었다면, 다시 기능 구현단계로 돌아가 픽스해야한다.

그렇다면 언제 리팩토링을 수행해야하는가?

개발자는 의식적으로 코드를 비판적으로 대한다. 이는 개발자가 코드와 싸우는 일반적인 상황이다.
코드를 비판적으로 보다보면, 참을 수 없게 된다.
깨끗하게 만들어야겠다는 생각을 하게 될텐데, 이때가 리팩토링의 기점이다.
더불어 개발 프로세스에서 리팩토링 시점을 결정하는 것이 가장 중요하다.

그렇다면 리팩토링이 필요한 이유는 무엇인가?

design stamina hypothesis

소프트웨어 설계에 신경쓰지 않으면, 시간이 지날수록 개발 속도가 현저히 떨어진다.개발자가 기능을 추가할 때, 복잡한 코드와 싸우느라 상당한 시간이 소요되기 때문이다.

좋은 설계를 가졌다면, 새로운 기능을 추가하는 것은 정말 쉬운 일이다.
단지 개발자가 할 일은, 변경해야할 모듈을 찾아 코드를 변경하고 적용하기만 하면 된다.

리팩토링을 통해 코드를 항상 클린하게 만들어, 좋은 아키텍처로 가도록 유도하게 된다. 개발자로써 전문성을 갖고 있다면, 항상 깨끗한 코드를 만들기위해 노력해야한다. 리팩토링은 소프트웨어의 엔트로피와 설계의 발전에 기여하는 것이다.

다시 리팩토링을 진행하려는 이유로 돌아와서 ..

EVPedia의 기능이 추가됨에따라 가독성이 저하되기 시작했고, 문제점을 발견한 지금이 현재의 구조를 변경할 최적의 타이밍이라 판단되었습니다.

TCA를 채택한 이유

가장 먼저 필요했던 것은, View의 상태 관리를 도와줄 객체였습니다. (유저의 이벤트가 발생하면 → 해당 이벤트에 맞는 로직을 처리하고 → View의 상태 값을 변경)

이를 위한 아키텍처로 iOS 생태계에서는 TCA가 많이 언급되고 있습니다.
단순히 많이 언급된다고 해서 사용을 할 수는 없기에, TCA가 왜 좋은지 학습해보았습니다.

TCA는 다음과 같은 장점을 갖고 있습니다.

1. 일관된 상태와 데이터 플로우 관리

SwiftUI는 @State, @StateObject 등과 같은 Property Wrapper를 통해 상태와 데이터 플로우를 관리합니다.

하지만 Property Wrapper 마다 초기화 조건이 다르고, 특히 EnvironmentObject는 초기화 하지 않았을 때 런타임 에러가 발생하게 됩니다.

TCA는 Scope된 Store와 ViewStore를 통해, 의존성 주입을 강제화하고있습니다. 상태는 Store를 통해 개발자가 직접 제어합니다.

더불어 State와 Action 그리고 Effect를 정형화시켜 팀원 모두 일관된 구조를 갖을 수 있게 도와줍니다.

2. 결합과 분리의 유연함

struct Feature1: Reducer {
  
  struct State: Equatable {
    var count = 0
  }
  
  enum Action: Equatable {
    
  }
  
  var body: some ReducerOf {
    Reduce { state, action in
      return .none
    }
    
  }
}

struct Feature2: Reducer {
  
  struct State: Equatable {
    var count = 0
  }
  
  enum Action: Equatable {
    
  }
  
  var body: some ReducerOf {
    Reduce { state, action in
      return .none
    }
    
  }
}

위와 같이 각각의 기능을 선언해둔 후, 아래와 같이 손쉽게 결합해 사용할 수 있습니다.

struct Feature3: Reducer {
  
  // ✅✅ 결합
  struct State: Equatable {
    var reducerA: Feature1.State
    var reducerB: Feature2.State
  }
  
  enum Action: Equatable {
    
  }
  
  var body: some ReducerOf {
    Reduce { state, action in
      return .none
    }
    
  }
}

위와 같은 장점을 바탕으로, 유연한 대응과 일관된 구조 유지를 위해 TCA 채택을 결정하였습니다.

리팩토링 전/후 비교

단편적으로 같은 기능을 하는 View의 코드 양이 절반 이하로 줄었습니다.

어떤 상태값과 어떤 액션을 수행하는지 직관적으로 파악할 수 있게되었고, 그에 따른 로직 처리도 한 눈에 확인할 수 있게 되었습니다.

TCA와 함께 Clean Architecture도 도입하여 각 객체의 책임과 역할이 분명해졌습니다. 모든 로직을 View와 Usecase에서 처리하던 기존 로직과 달리 이제는 View는 UI만 담당하고, Reducer가 View의 상태를 관리하고, Usecase는 시나리오를, 그 이후 레이어 및 객체에서는 저마다의 책임만 담당하게 되었습니다.

더불어 추상화를 통해 결합도를 낮추고, 테스트 용이성을 높였습니다.

이상으로 EVPedia TCA 도입기를 마치겠습니다.

written by: EV Studio iOS 개발자 황지희

참고:

  1. Martin Fowler @ OOP2014 "Workflows of Refactoring"
  2. Swift Composable Architecture 를 도입하며 겪었던 문제와 해결법​