1년동안의 iOS 모듈화 진행기

안녕하세요! 앱개발팀의 레이몬드입니다. 2024년 한 해동안 모듈화를 경험하면서 느낀점들과 후회하는 것들을 글로 써보고자 합니다.

2023년 초부터 논의해 오던 모듈화를 2024년에 본격적으로 진행했고 지금도 모듈화 작업을 이어 나가고 있습니다. 1년동안 진행한 작업들을 되돌아 보니 생각보다는 많은 작업들을 진행했더라구요. 많은 모듈화 관련 포스트들이 모듈화의 필요성에 대해 다루고 있어서 비슷한 내용보다는 다른 관점인 모듈화를 진행하면서 느낀점들과 아쉬웠던 것들을 정리하고자 합니다.

모듈화에 대해 단순히 성공적인 결과로 이어진 과정이 아니라, 그 속에서 어떤 어려움과 도전이 있었는지를 공유하고자 합니다.

우리가 처음 목표로 잡은 모듈 구조

모듈화를 진행하면서 목표로 설정한 구조는 아래 그림과 같습니다. 처음에는 단일 프로젝트로만 구성된 상태였고 지금은 피처 모듈들을 원활하게 분리하기 위한 기반 모듈들이 어느정도 구성된 상태입니다.

1.png

크게 3가지 레이어로 구성했습니다.

  • Core: 앱 전반적으로 사용되는 공통 로직 + 모델 + 레포지토리
  • UI: 재사용 가능한 UI 컴포넌트 + 리소스
  • Features: 기능 단위의 독립적인 모듈들

첫번째 모듈 분리 도전: Model과 Repository의 분리

위에서 소개한 Core 레이어에 대한 설명 중 의아한 부분이 하나 있습니다. Core 레이어에 모델과 레포지토리가 포함되어 있다는 점입니다. 앱 전반적으로 사용되는 모델이나 레포지토리가 아니라면, 기본적으로 피처에서 사용하는 모델과 레포지토리는 해당 피처 모듈 내부에 포함되는 것이 맞다고 생각합니다. 하지만 실제 모듈화를 진행하는 과정에서 몇 가지 이유로 인해 별도 모듈로 분리할 필요성이 생겼고, 이에 따라 분리를 진행하게 되었습니다.

모듈화 시작할 당시, 어디서부터 손을 대야 할지 막막함을 느꼈습니다. 그래서 빈 껍데기 형태의 피처 모듈을 먼저 생성한 후, 관련된 파일들을 모듈 내부로 옮겨보는 작업을 시도했습니다. 그런데 이 과정에서 코드 간의 의존성이 강하게 얽혀 있어, 피처 모듈을 완전히 독립적으로 분리하기 어려운 상황임을 깨닫게 되었습니다.

특정 뷰모델에서 사용되는 Dependency만 해도 여러 곳에서 의존을 하고 있는 상황

이러한 경험을 바탕으로, 피처 모듈을 완전히 분리하려면 먼저 의존성을 정리하는 것이 선행되어야 한다는 결론을 내렸습니다. 이후 의존성이 비교적 적은 코드부터 점진적으로 분리하는 방법을 고민했고, 그 과정에서 모델과 레포지토리가 가장 적합한 대상으로 보였습니다.

이에 따라, 우선 모델과 레포지토리를 Core 레이어의 ZCore 모듈로 이동시키는 방향을 선택했습니다. 이렇게 하면 의존성을 미리 분리할 수 있고, 이후 필요에 따라 피처 모듈로 옮길 때도 큰 문제 없이 이동할 수 있을 것이라 판단했습니다.

부담스러웠지만 효과적이었던 모듈화 과정

큰 단위의 모듈 분리

지그재그는 이미 많은 사용자들이 있는 서비스이므로 앱 업데이트 시 가장 중요하게 생각하는 것은 안정성입니다. 기술 과제로 인해 버그가 발생하는 것을 최소화해야 하죠. (물론 다른 서비스들도 마찬가지일 것입니다.) 버그 발생률을 줄이기 위해서는 영향 범위를 최소화해야 했습니다. 하지만 처음에는 이러한 점을 깊이 고려하지 못한 채, 욕심을 부려 한 번에 많은 파일을 이동시켰습니다. 많은 파일을 이동시키다 보니 예상치 못한 오류들이 발생하기도 했고 리뷰 과정에서 팀원들의 부담도 커졌습니다.

특히, 1번에서 설명했던 모델들을 ZCore 모듈로 옮기는 작업이 가장 대표적인 예였습니다. 이미 지그재그 프로젝트는 상당한 규모를 가지고 있었고, 그만큼 모델 파일도 많았습니다. 그런데 모든 모델을 한꺼번에 ZCore로 이동하면서 파일 변경량이 과도하게 커지는 문제가 발생했습니다.

이로 인해 다음과 같은 어려움이 있었습니다

  1. 변경된 파일이 너무 많아 PR 크기가 커짐: PR이 커질수록 코드 리뷰어의 부담이 커졌고, 리뷰 자체가 지연되는 문제가 발생했습니다. 3.png

  2. 모든 관련 파일에 import를 추가해야 하는 부담: 모델을 사용하는 파일마다 import ZCore를 추가해야 했고, 예상보다 변경해야 할 부분이 많아졌습니다.

  3. 예상치 못한 의존성 문제 발생: 기존에는 같은 모듈 내에서 사용하던 모델들이 ZCore로 이동하면서, 예상하지 못했던 의존성 문제가 드러나기도 했습니다.

결과적으로 작업량이 증가하면서 예상보다 시간이 많이 소요되었고, 팀원들에게도 부담을 주는 작업이 되어버렸습니다.

하지만, 결과적으로는 긍정적인 변화였다.

비록 부담이 컸던 작업이었지만, 모듈화의 효과는 분명히 있었습니다. 모델들이 별도의 모듈로 이동하면서 의존성이 정리되었고, 이후 다른 파일들을 분리하는 작업이 이전보다 훨씬 수월해졌습니다. 이 경험을 바탕으로, 앞으로 유사한 작업을 다시 수행해야 할 경우에는 더 나은 접근 방식을 적용해 영향 범위를 최소화하려고 합니다. 아래와 같은 방식으로 진행하면 더 효과적일 것 같습니다.

✅ 더 나은 접근 방식

  1. 하나의 피처(또는 특정 화면)에서만 사용하는 모델들부터 ZCore 모듈로 이동: 한 번에 모든 모델을 옮기는 것이 아니라, 개별 피처에서만 사용하는 모델부터 순차적으로 이동합니다.
  2. 공용 모델이라면 우선 ZCore와 앱 모듈에 중복 선언: 모델을 ZCore로 이동하기 전에, 기존 앱 모듈에도 동일한 모델을 유지하여 기존 코드에 미치는 영향을 최소화합니다.
  3. 다음 업데이트에서 중복된 모델을 정리하며 완전한 분리 진행: 한 번에 모든 파일을 옮기지 않고, 기존 모델을 점진적으로 대체하는 방식으로 진행합니다.

이런 방식을 적용하면, 영향 범위를 기존보다 훨씬 줄일 수 있고 PR 크기도 작아져 리뷰어들에게도 부담을 덜 줄 수 있을 것이라 생각합니다.

아쉬웠던 모듈화 과정

모듈 네이밍 규칙 적용으로 인한 크래시 ⚠️

모듈화를 시작하면서 네이밍 규칙을 처음부터 정하지 않았던 것이 큰 실수였습니다. 모듈을 하나씩 나누다 보니 네이밍이 일관되지 않은 상황이 발생했습니다.

  • 예시 모듈 이름: ZigZagCore, ZigZagUI, MarketingLogger, Utils

모듈이 앞으로 더 늘어날 것을 생각해 지금이라도 네이밍 규칙을 정해야 한다고 판단했습니다. 그래서 Feature Layout 모듈을 제외한 나머지 Layer 모듈들에는 Z prefix를 붙여 일관되게 통일했어요. 예를 들어 ZCore, ZUI, ZMarketingLogger와 같이 명명했습니다.

네이밍만 변경한 작업이라 로직에 영향이 없을 줄 알았고, 코드 리뷰와 개발자 자체 검증(QA팀의 검증 없음)만으로 간단히 진행했습니다. 하지만 예상치 못한 런타임 크래시가 발생했습니다.

문제는 UI 파일들이 모여있는 ZUI 모듈에 존재하던 레거시 xib 파일 때문이었습니다. xib 파일은 모듈 이름이 변경될 때 Inspectors에서 수동으로 설정을 변경해야 하는데, 이를 간과했죠.

Xib Inspectors에서의 모듈 설정

코드 검색 기능으로 모든 파일의 네이밍을 변경했지만 xib 파일내의 속성까지는 검색되지 않아 기존 설정이 남아 있었고, 결국 앱 실행 시 크래시가 발생했습니다.

  • 해당 xib는 모듈 이름 변경전 이름으로 되어있어서 찾을 수 없는 모듈이라는 에러와 함께 크래시가 발생했습니다.

이 사건을 겪으면서 잘 사용하지 않던 xib의 속성에 대해서 알게 되었고 이참에 xib 파일을 빠르게 제거해야겠다는 생각도 들었습니다. 무엇보다 처음부터 네이밍 규칙을 잘 정하고 파일들을 옮겼더라면 이런 문제가 발생하지 않았을 거란 교훈을 얻었습니다.

모듈화를 진행하며 한 고민들

모듈화 초기, 앱 실행시간(App Launch Time) 증가 문제

모듈화 초기에는 앱 실행시간에 대한 우려 없이 모듈을 나누기 시작했습니다. 하지만 초기 단계에서 나눈 모듈들은 대부분 Core Layer에 속했고, 여러 모듈에서 공통으로 사용되다 보니 자연스럽게 Dynamic Framework 형태로 구성될 수밖에 없었습니다.

문제는 Dynamic Framework의 수가 늘어나면서 점점 앱 실행시간이 길어지기 시작한 점이었습니다. 특히 현재 상황에서는 나중에 Static Framework로 이동할 코드들조차 Core Layer에 모여 있기 때문에 일시적으로 앱 실행시간이 늘어난 것이 아닌가 하는 생각도 들었죠.

5.png

하지만 앞으로 Feature 모듈이 분리되고 Core Layer가 최적화되면 실행시간이 모듈화 이전 수준까지 줄어들지에 대한 의문이 여전히 남아 있습니다.

지금은 Core Layer에 있는 특정 모듈들을 static으로 변경해보거나 Dynamic framework로 사용하는 CocoaPod을 SPM으로 변경해보는 것으로 앱 실행시간을 줄여보고 있습니다.

이 문제는 단순히 현재 상태를 유지하는 것이 아니라, 모듈화가 발전해 나가는 과정에서 다양한 방안을 고민하고 실험하며 개선해 나가야 할 과제라고 생각합니다.

모듈 역할 분리의 모호함

Core Layer의 모듈들을 분리하면서 코드가 역할과 의존성에 따라 정리되는 느낌이 들어 나름 청소가 잘 되고 있다고 생각했습니다. 하지만 진행하다 보니 역할이 모호한 코드들이 하나둘씩 눈에 띄기 시작했어요. 대표적인 예로 다음과 같은 코드가 있습니다.

enum UserAccountInitType: String, Decodable {
    case general = "GENERAL"
    case kakao = "KAKAO"
    case apple = "APPLE"
    case google = "GOOGLE"
    case facebook = "FACEBOOK"
}

extension UserAccountInitType {
    var localizedName: String? {
        switch self {
        case .general:  return nil
        case .kakao:    return Strings.Common.kakao
        case .apple:    return Strings.Common.apple
        case .google:   return Strings.Common.google
        case .facebook: return Strings.Common.facebook
    }
}

UserAccountInitType은 사용자가 회원가입이나 로그인을 할 때 선택할 수 있는 방식을 enum 형태로 정의한 모델입니다.localizedName은 화면에서 각 회원가입/로그인 타입에 맞게 노출되는 텍스트입니다.

여기서 문제는 enum 타입 자체는 모델에 속하기 때문에 ZCore 모듈에 넣는 것이 타당하지만, localizedName 변수는 문자열 리소스를 담고 있기 때문에 ZResource 모듈에 속한다고도 볼 수 있다는 점이었어요.

이런 경우처럼 역할이 애매한 코드가 발생하면 모듈을 나누는 기준이 모호해지기 시작합니다. localizedName을 ZCore에 두자니 문자열 리소스와의 의존성이 혼재되고, ZResource에 두자니 모델과의 분리가 어색해져 결국 ZCore와 ZResource 간의 불필요한 의존 관계가 생기게 되죠.

이러한 상황을 해결하기 위해 여러 방법을 고민 중입니다:

  • 익스텐션 전용 모듈을 만들어 이런 애매한 역할을 별도로 관리하는 방안

아직 어떤 방법이 최선인지 확답을 내리기 어렵지만, 이런 고민을 하나씩 해결해 나가면서 더 명확한 구조를 만들어 가려고 합니다.

얻은 교훈과 앞으로의 계획

모듈화 작업을 통해 다음과 같은 교훈을 얻었습니다:

  1. 작게 시작하고 점진적으로 확장하라: 처음부터 모든 것을 분리하려 하면 오히려 복잡성이 증가합니다. 핵심 레이어부터 시작해 점진적으로 확장하는 것이 중요합니다.
  2. 명확한 역할 분리 정의: 각 모듈 간의 의존성을 최소화하고, 명확하게 정의해야 유지보수가 쉬워집니다.
  3. 최대한 영향 범위를 줄여 안정성을 높여 작업해야 한다.

올해 iOS 모듈화 작업은 많은 도전과 성취를 안겨준 경험이었습니다. 이번 글은 모듈화를 경험하며 느꼈던 점들을 간략하게 적어보았는데요 다음번에는 모듈화를 진행했던 방법들에 대하여 하나씩 적어보겠습니다. 예) 코드 결합도를 해결하기 위한 방법들

앞으로도 더 효율적이고 유연한 코드를 만들기 위해 노력하겠습니다. 혹시 모듈화에 대해 고민하고 계시다면, 이 글이 작은 도움이 되었으면 좋겠습니다. 감사합니다!

✏️ 편집자주: 지그재그 코드 베이스는 벌써 10년이 됐습니다. 파일 개수만 수천개에 이르는 큰 프로젝트를 유지 보수하는 일은 쉽지 않습니다. 하지만 어렵다고 방치하면 나중에 더 큰 어려움이 되어 되돌아옵니다. 지그재그 앱 개발팀은 Swift 전환에 이어, 모듈화까지, 새로 시작하고 싶은 유혹을 참고, 달리는 자동차를 멈추지 않고 바퀴를 교체해오고 있습니다.



comments powered by Disqus