모듈 구조를 개선해 더 나은 뱅크샐러드 iOS 앱 개발하기

모듈 구조를 개선해 더 나은 뱅크샐러드 iOS 앱 개발하기

안녕하세요. 뱅크샐러드 iOS 챕터의 김봉균입니다.

최근 iOS 챕터는 뱅크샐러드 iOS 프로젝트의 모듈 구조를 개선하고 있습니다. 시간이 흐르면서 뱅크샐러드가 더 많은 기능을 제공하게 됐고 이에 따라 코드의 양도 증가했습니다. 이때 각 코드들이 서로 무분별하게 의존하게 된다면 코드의 복잡도는 걷잡을 수 없이 커지게 됩니다.

적정 수준까지는 코드 뭉치 크기가 커져도 엔지니어가 코드를 이해할 수 있고 컴퓨터가 이를 빌드 하는 데 어려움이 따르지 않는다면 문제가 없습니다. 그러나 최근 몇 가지 불편한 점을 겪으며 이제는 코드 뭉치를 다시 나눌 시기가 왔다고 판단해 모듈 구조를 개선하는 작업을 하고 있습니다.

이 글에서는 뱅크샐러드 iOS 프로젝트의 구조에 어떤 문제가 있었고 어떻게 구조를 수정해 문제를 해결했는지 소개합니다. 다음 글에서는 프로젝트 전체에 영향을 미치는 구조 개선 작업을 안정적으로 진행할 수 있게 해준 뱅크샐러드의 협업 과정을 소개합니다.

지금까지의 상황

최초 모듈 구조. 모듈이 하나만 있음.

최초의 뱅크샐러드 iOS 프로젝트는 단일 모듈로 구성되어 있었습니다. 당시는 뱅크샐러드 iOS 서비스가 아직 출시되기 전이었고 사용자가 이 서비스를 사용해 줄지 여부조차 알 수 없었습니다. 따라서 빠르게 서비스를 개발해 사용자에게 제공해 이 서비스가 세상에 필요한 것인지 검증하는 것이 중요했습니다. 출시 이후에는 사용자의 피드백을 듣고 기능을 추가하는 것이 중요해졌습니다. 당시 팀원들은 신속하게 기능을 추가하며 사용자의 요구에 부응했고 덕분에 뱅크샐러드 서비스는 점차 자리를 잡게 됐습니다.

이후 모듈 구조. 한 방향으로 길게 의존 관계가 이어져 있음.

이후 뱅크샐러드는 더 많은 기능을 제공하고자 조직 규모를 확대했습니다. 이로 인해 더 많은 엔지니어들이 동시에 여러 기능을 개발하게 됐고 코드 베이스도 커졌습니다.

이 과정에서 더 커진 코드 베이스 위에서 여러 엔지니어들이 충돌 없이 작업할 수 있도록 모듈을 나누는 작업을 진행했습니다. 공통으로 사용하는 코드, 데이터를 다루는 계층의 코드, 각 기능의 화면을 그리는 계층의 코드 등으로 분리했습니다. 새로운 구조 덕분에 많은 iOS 엔지니어들이 빠르고 충돌 없이 기능을 개발할 수 있었습니다.

그러나 시간이 흐름에 따라 코드 베이스가 점점 더 커졌고 앞서 설계한 모듈 구조의 한계가 드러났습니다. iOS 챕터 구성원들은 문제 해결 방안을 논의했고 이를 바탕으로 추가 모듈 구조 개선 작업을 시작했습니다.

사례: 과도하게 커진 공용 코드 모듈을 작게 분리하기

Live Activity 기능을 샘플앱을 이용해 조작하는 모습.

iOS 16에 새롭게 도입된 Live Activity 기능을 활용하기 위해 별도 모듈을 생성했습니다. 뱅크샐러드 iOS 프로젝트는 원래 UIKit과 RxSwift 기반으로 개발됐지만 Live Activity 기능은 SwiftUI와 SwiftConcurrency를 기반으로 개발해야 했습니다. 기반이 바뀌었기 때문에 기존에 만들어진 많은 공용 코드 중 일부만 필요하게 됐습니다.

엄청 큰 Shared 모듈에 의존하는 새로운 기능.

공용 코드는 Shared라는 하나의 커다란 모듈에서 관리되고 있었습니다. Shared는 Manager, Helper처럼 어떤 내용이라도 포함할 수 있는 모호한 이름이었고 어디에 속해야 할지 애매한 코드들이 모두 Shared로 모였습니다. 시간이 지나면서 Shared 모듈이 어떤 역할을 하는지 파악하기 어려울 정도로 커졌습니다. 이로 인해 다른 모듈이 Shared 모듈의 작은 기능을 필요로 할 때에도 많은 내/외부 코드까지 함께 의존해야 했습니다.

저희는 각 모듈이 최대한 단순한 구조를 가지고 가볍게 동작하기를 원했습니다. 모두가 모여 커다란 뱅크샐러드 앱을 개발하지만 각 기능을 개발할 때는 독립된 작은 앱을 만드는 것 같은 환경을 만들고 싶었습니다. 이를 위해 각 모듈이 다른 모듈에 의존하는 상황을 최소화하려 했고 이 같은 요구사항은 LiveActivity 뿐만 아니라 다른 기능을 개발할 경우에도 발생할 것이라 판단했습니다. 따라서 커다란 공용 코드 모듈인 Shared를 독립된 여러 모듈로 분리하기로 결정했습니다.

Shared 모듈 분리 전 상황. 독립 가능한 기능과 불필요한 코드로 나누었음.

우선 작업 범위를 좁히기 위해 유효한 공용 코드만 남기는 준비 작업을 진행했습니다. 이를 위해 사용하지 않는 코드를 제거하고 특정 모듈에서만 사용하는 코드는 해당 모듈로 이동시켰습니다. 준비 작업 후에는 독립 가능한 기능을 파악해 추후에 개별 모듈로 분리할 수 있는 기능을 나열했습니다. 그중에서도 우선순위가 높고 추가 의존성이 적은 기능부터 독립 모듈로 분리하기 시작했습니다.

Shared 모듈 분리 후 상황. 불필요한 코드가 제거되고 독립 가능한 기능들이 분리되었음.

이에 따라 다음과 같이 모듈을 분리했습니다.

  • Constant 모듈 : 프로젝트 전역에서 사용하는 URL, Key, 설정값 등을 상수로 관리하는 모듈
  • SwiftExtension 모듈 : Swift 언어를 확장해 편의 기능을 덧붙인 모듈
  • DIContainer 모듈 : 의존성을 주입하는 컨테이너를 관리하는 모듈

그 결과 새로 생성되는 모듈에서 필요한 공용 기능을 더 가볍게 사용하고 불필요한 내부 또는 외부 코드에 의존하지 않을 수 있게 됐습니다.

여기서 그치지 않고 앞으로도 우선순위에 따라 공용 코드를 더 작은 단위의 모듈로 분리할 계획입니다. 최종적으로는 각 기능이 명확한 이름과 독립된 기능을 가지는 모듈로 분리되어 Shared 모듈이 사라지는 것을 목표로 하고 있습니다.

사례: 의존성 역전(Dependency Inversion)을 활용해 유연한 구조 세우기

뱅크샐러드 iOS 프로젝트는 클린 아키텍처를 적용하면서 Domain과 Data 레이어를 분리했습니다. Domain 레이어는 사용처의 필요에 따라 데이터를 가공하는 역할만 하며 데이터를 가져오는 역할은 다른 레이어에 위임하기로 결정했습니다. 이로 인해 데이터를 가져오는 역할을 하는 Data 레이어가 생성됐습니다.

의존성 역전을 설명하는 그림.

Data 레이어의 구현체와 Domain 레이어의 구현체는 서로의 구체적인 형태를 알지 못하지만 명세를 통해 요구사항을 교환합니다. 이를 의존성 역전 원칙(Dependency Inversion Principle)이라 부릅니다. 의존성 역전 원칙은 고수준 모듈이 저수준 모듈에 의존하지 않고 둘 다 추상화에 의존하도록 하는 소프트웨어 디자인 원칙입니다. 이 원칙을 통해 소프트웨어는 더 유연하고 확장 가능해지며 컴포넌트 간의 결합도도 낮아지는 장점이 있습니다.

Data 모듈의 명세를 Data 모듈이 가지고 있는 구조.

그러나 뱅크샐러드 iOS 프로젝트는 최초에 모듈을 분리하는 과정에서 Data 레이어의 인터페이스를 Data 레이어가 가지고 있도록 분리가 됐습니다.

아직은 Data 레이어의 역할이 단순하고 하나의 모듈만 존재하기 때문에 크게 문제가 되지 않습니다. 그러나 앞으로 Data 모듈이 REST, gRPC 등의 통신 방식이나 서버, 클라이언트 캐시 등의 출처에 따라 별도의 모듈로 분리될 경우에는 문제가 될 수 있습니다. 이때는 Data 레이어의 여러 모듈이 공통된 명세에 의존해야 하는데 Data 레이어에서 어느 하나의 Data 모듈이 명세를 가지고 있으면 Data 레이어의 모듈 간에 불필요한 의존이 발생하기 때문입니다.

Data 모듈에서 하나의 명세를 구현한 여러 구현체가 있는 구조.

상황을 예로 들어 설명하겠습니다. 뱅크샐러드의 알림 화면은 사용자에게 알림 목록을 지연 없이 보여주기 위해 사용자의 기기에 저장된 최근 응답을 활용해 화면을 먼저 그려줍니다. 그 후 서버에서 최신 목록을 받아와 화면을 다시 그려줍니다. 이때 Data 모듈에는 알림 목록을 가져오는 함수 명세가 있고 이를 기기에서 캐싱 된 목록을 가져오는 객체와 서버에서 최신 목록을 가져오는 객체가 각각 구현합니다.

Data 모듈이 여럿으로 나뉘었을 때 명세가 속할 곳이 애매한 상황.

만약 이때 데이터의 출처에 따라 Data 모듈을 분리한다면 명세가 위치해야 할 곳이 모호해지는 문제가 발생합니다. 또한 Domain 모듈은 데이터를 가져오는 객체의 동작 방식이나 출처가 바뀌었을 때 대응하지 않아야 함에도 불구하고 명세 위치 변경에 대응해야 하는 문제도 있습니다.

DataInterface 모듈이 분리되어 어느 구현 모듈에도 속하지 않게 되는 상황.

점차 gRPC 도입이나 클라이언트 캐싱의 중요성이 언급되는 상황에서 문제를 미리 방지하고 순리에 맞는 구조를 세우기 위해 Data 모듈에서 명세 부분을 DataInterface 모듈로 분리하기로 했습니다. 그리고 Domain 모듈과 Data 모듈 모두 DataInterface 모듈에 의존하도록 구조를 설계했습니다.

우선 Data 모듈에서 구현체의 접근 제한자를 모듈 내에서만 접근 가능한 수준으로 변경하고 외부 모듈에서 구현체를 직접 생성하는 곳은 명세를 통해 의존성을 주입받는 방식으로 변경했습니다. 또한 구현체를 상속받아 기능을 추가한 사용처도 있었는데 상속 관계를 합성 관계로 바꾼 후 의존성을 주입받도록 하여 구현체에 직접 의존하는 경우를 없앴습니다.

준비 작업이 끝난 후엔 DataInterface 모듈을 생성하고 명세를 하나씩 이동하기 시작했습니다. 준비 작업이 잘 끝났기 때문에 명세 이동 작업은 원활히 진행됐습니다. 마지막으로 Domain 모듈이 Data 모듈에 의존하는 구조를 끊고 DataInterface 모듈에 의존하도록 만들면서 의존관계 정리 작업이 완료됐습니다.

마치며

이번 개선 작업을 통해 모듈 구조가 많이 개선됐고 여러 문제가 해결됐지만 여전히 많은 숙제가 남아있습니다.

필요에 따라 모듈을 더 작은 단위로 분리해 새로운 기능 모듈이 생겼을 때 기존 공용 모듈을 재사용 하기 쉽게 만들 수 있습니다. 또한 지금까지 모듈 분리 과정에서 프로젝트 설정을 일일이 해왔는데 이를 편리하게 만들어 주는 도구를 도입해 모듈 생성 과정을 간편하게 만드는 작업도 계획하고 있습니다.

이번 작업을 진행하는 동안 다른 iOS 엔지니어들의 작업과 충돌이 거의 없었고 릴리즈 후 사용자에게 발생한 문제 또한 없었습니다. 이는 뱅크샐러드의 협업 방식이 크게 도움이 됐기 때문입니다.

다음 글에서는 프로젝트 전역에 영향을 미치는 구조 개선 작업을 여러 구성원과 협업해 안정적으로 진행한 비결을 <계획-작업-리뷰-테스트-모니터링> 전 과정에 걸쳐 소개 드릴 예정입니다. 저희의 노하우가 가득 담긴 글이니 많은 관심 부탁드립니다!

마지막으로 앞으로의 작업을 뱅크샐러드 iOS 챕터와 함께할 새로운 엔지니어 분을 모시고 있습니다. 자세한 내용은 아래 지원하기 버튼을 눌러 확인 부탁드립니다. 감사합니다.

보다 빠르게 뱅크샐러드에 도달하는 방법 🚀

지원하기

Featured Posts

post preview
post preview
post preview
post preview
post preview

Related Posts