애플 디벨로퍼 아카데미에서 진행한 프로젝트, 캐플 Qapple의 리팩토링 작업이 시작됐습니다.
캐플은 애플 디벨로퍼 아카데미 @POSTECH 3기, TEAM QAPPLE에서 시작한 프로젝트로
아카데미 러너들과 익명으로 소통할 수 있는 커뮤니티 서비스입니다! 🍎 AppStore 다운로드하기
1년 가까이 진행하면서 쌓여온 코드는 문제점이 많았습니다. 하지만 신규 기능 추가(게시판, 댓글 등...)에 항상 뒷전이 되어 미루다시피 한 기술 부채는 이제는 너무나 늘어나버렸죠,,!
3기가 마무리되고 4기의 시작을 앞둔 지금이 절호의 기회라고 판단해 시즌2에 새롭게 합류한 멤버(시몬스, 무니)들과 함께 진행 중인 리팩토링 여정을 기록해보려 합니다.
무엇이 문제였을까?
리팩토링 하자!라고 이야기가 나왔다는 것은 분명 어떠한 문제가 있었기 때문입니다. 별다른 문제가 없어 보여도 리팩토링 하자~ 와 같은 말을 입에 달고 사는 제게 '문제가 무엇인지 파악'하는 것은 정말 중요한 일이었습니다. 근거 없이 시작한 리팩토링은 방향성과 설득의 힘을 잃어버릴 수 있을 테니까요.
팀원들과 프로젝트를 진행하면서 실제로 느꼈던 불편한 점, 아쉬웠던 점, 개선점 등을 공유했습니다.
1. 기준이 없는 프로젝트 구조
프로젝트 규모가 커지면서 신규 기능을 추가하는 것만큼 기존 코드의 수정을 요구하는 일도 잦아졌습니다. 그때마다 수정할 파일을 찾아가야 했는데 정말이지 불편했습니다.(이 코드 어디 있더라,,?)
특정 기능을 수정하기 위해 수많은 폴더 중에 하나를 찾아야 했는데 기준 없이 생성된 폴더들은 이를 어렵게 만들었습니다. Manager, Helper, Extension, Protocol 등 합의되지 않은 네이밍과 폴더 구조가 원인이었습니다. (물론 폴더 정리가 안돼서 더 찾기 어려웠을 수도 있습니다)
(여담으로, 저는 결국 Cmd + Shift + O 기능을 사용하는 것으로 파일을 찾아내곤 했습니다)
2. 백엔드 서버에 직접적으로 의존
캐플은 꽤나 많은 API가 존재합니다. 질문, 답변, 게시판, 댓글, 신고, 사용자 등등... 커뮤니티라는 도메인 특성상 많은 데이터를 백엔드 서버에서 내려받아 사용하고 있습니다.
문제는 서버에서 내려주는 데이터, 즉 Response를 타입 그대로 들고 앱 내에서 사용하다 보니 API 명세가 바뀌 기라도 하면 이를 활용하고 있는 모든 곳의 코드를 바꿔줘야 했습니다.
이전에 페이지네이션 기능을 구현할 때, API 명세가 2번 정도 바뀐 적이 있었습니다. 이때 열심히 작업해 놓은 View 곳곳에서 컴파일 에러가 나는 것을 보고 이러면 안 되겠구나,,,를 절실히 느꼈습니다.
3. 상태 관리 + 유닛 테스트의 어려움
캐플은 MVVM 아키텍처를 기반으로 설계되었습니다. SwiftUI에서의 MVVM 사용은 여전히 갑론을박 중인 상황이지만, SwiftUI의 첫 프로젝트로 채택하기엔 가장 쉽고 직관적이었죠.
하지만 문제는 상태 관리였습니다. View 간의 상태 공유를 위해 하위 View에 ViewModel을 전달해 주는 방식은 1:N의 구조를 가지기 때문에 ViewModel의 책임이 모호해질뿐더러, 비대해지는 결과를 낳았습니다. 결국 수많은 비즈니스 로직과 상태 관리 코드가 섞여 코드의 가독성이 떨어지고 유지보수의 어려움을 만들었습니다. 이는 MVVM의 문제라기보다는, 상태 공유 방법을 더 고민해봐야 할 지점인 것 같습니다.
또한 ViewModel의 유닛 테스트는 더더욱 시도하기 어려웠습니다. ViewModel이 비대하기도 했고, 여러 함수가 섞여 있다 보니 유닛 테스트의 필요성을 느끼지 못했던 것일 수도 있습니다.(ViewModel을 손댈 수 없을 정도로 의지가 꺾여버린,,)
4. 중복 코드 생성
URLSession을 이용한 네트워킹, 페이지네이션 기능, UI Component 등 같은 목적의 코드를 응집해 재사용하는 것이 아닌, 그때마다 생성하는 '진짜 중복 코드'가 프로젝트 곳곳에 있었습니다. 이는 코드의 재사용, 일관성을 떨어트렸습니다.(누구는 네트워크 이렇게 짜고~ 누구는 컴포넌트 이거 쓰고~)
어찌 보면 진작에 잡을 수 있는 부분이었겠지만, 서로 바쁘다는 핑계로 신경 쓰지 못했던 것 같아 반성이 됩니다.
그래서 어떻게 리팩토링 할 건데!
결국 캐플 리팩토링의 목적은 문제점을 하나씩 해결해 나가는 것입니다. 정리하자면,
- 유지보수, 확장성을 고려한 설계하기
- 명확한 프로젝트 구조 확립하기
- 백엔드 서버와의 의존성 줄이기
- 상태 관리 방법 개선하기
- 유닛 테스트 가능한 환경 만들기
- 중복 코드 줄이고 응집화하기
위 문제들은 팀 간 합의와 기준 설정을 통해 충분히 해결할 수 있었습니다. 중요한 것은 '어떻게 팀 간 합의와 기준을 설정할 것인가'로 귀결되었고, 이는 자연스레 아키텍처 이야기로 넘어갔습니다.
소프트웨어 개발에서 합의와 기준을 설정하기 위한 가장 좋은 도구 중 하나는 아키텍처입니다. 특정 레이어를 이야기할 때 공통된 맥락을 이해할 수 있고, 어떤 방향성으로 설계해나가야 할지의 이정표와 같은 역할을 할 수 있기 때문입니다.
캐플 팀은 아키텍처 중심의 리팩토링 방향성을 설정했고 어떤 아키텍처를 설정할지 리서치를 시작했습니다. 많은 아키텍처가 있었지만, 가장 보편적으로 사용되고(학습 자료가 많은 것의 이점을 위해) 흥미가 가는 것으로 정리 후 논의를 이어갔습니다.
1. MVVM
MVVM은 현 프로젝트에 적용 중인 아키텍처였습니다. 역시 가장 쉽고 직관적이며 현재 코드를 크게 바꾸지 않아도 되기에 작업량의 이점이 분명했습니다. 하지만 SwiftUI에서의 MVVM의 사용은 항상 문제로 제기되어 왔습니다. UIKit에서는 데이터 바인딩을 ViewModel이 수행함으로써 그 목적이 분명했지만, SwiftUI에서는 @State와 @Binding 등 View 자체에서 이미 데이터 바인딩을 제공하고 있기 때문입니다. 또한 새로운 아키텍처로 전환함으로써 얻는 학습 효과 또한 고려 대상 중 하나였기에, MVVM은 금방 제외되었습니다.
* 참고 자료: "SwiftUI에서 MVVM 사용을 멈추자"라고 생각이 들었던 이유
2. MV
MVVM의 ViewModel이 빠진 형태가 MV입니다. 하지만 결국 많은 로직들이 View에 들어가면서 발생하는 문제점들이 우려되어 목표를 달성하기에는 어려울 것이라 판단했습니다.
3. Clean Architecture
클린 아키텍처는 MVVM, MV와 같이 명확한 레이어로 나누어져 있지는 않습니다. 즉, 팀 간 합의를 통해 적절히 레이어를 분리하는 과정이 필요했습니다. 그러나 비즈니스 중심의 설계 철학을 바탕으로 한 의존성 관리는 분명 이점을 가져다줄 수 있었습니다. 또한 팀원들도 클린 아키텍처를 적용해 본 경험이 조금씩 있었기에 큰 러닝 커브 없이 적용할 수 있다는 점도 있었죠.
4. TCA
TCA는 The Composable Architecture의 줄임말로, PointFree에서 배포한 라이브러리입니다. TCA의 핵심은 일관되고 이해하기 쉬운 방식으로 애플리케이션을 빌드할 수 있는 비즈니스 레벨에서의 방법 제공입니다. 일관된 방식을 제안하기 때문에, 보다 엄격한 규칙과 네이밍을 준수해야 하며 이는 합의와 기준을 설정하기 좋은 선택사항이 될 수 있었습니다. 하지만 가장 큰 장애물은 역시 진입장벽이었습니다. TCA는 기존 개발 경험과 다른 점들이 많았고, 이를 이해하기 위한 학습은 꼭 필요했습니다.
TCA 도전하기
최종 선택한 아키텍처는 TCA였습니다. 클린 아키텍처와 TCA 모두 목표를 달성하는데 좋은 도구가 될 수 있었지만, TCA에서 얻을 수 있는 이점이 더 많다고 판단했기 때문입니다.
첫 번째, 일관된 상태 관리입니다. TCA는 Action의 결과를 통해 State를 변경하고, Effect를 통해 사이드 이펙트를 관리하는 구조로 일관된 방식의 상태 관리가 가능합니다. 또한 TCA의 특징인 Composable(구성 가능한)함 덕분에 부모와 자식 간의 상태 공유 또한 쉽게 가능했습니다.
두 번째, 엄격한 네이밍입니다. Reducer, Store 등의 단어로 객체가 무엇을 의미하는지에 대한 맥락은 전부 TCA에서 가이드하고 있기 때문에, 서로 다른 네이밍으로 혼란을 겪거나 고민하는 상황이 현저히 줄어들 수 있었습니다.
세 번째, 편리한 의존성 관리입니다. @Dependency를 이용해 의존성을 관리하는 방법은 편리하고 Preview, Test 데이터 셋 제공 등의 유연한 이점을 가지고 있습니다.
네 번째, 강력한 테스트입니다. TestStore를 통한 Reducer의 유닛 테스트는 @Dependency와 결합해 다양한 케이스를 빠르고 강력하게 테스트할 수 있습니다.
다섯 번째, 학습 효과입니다. TCA는 SwiftUI의 이점을 살리기 위한 다양한 방법들이 적용된 라이브러리입니다. 이를 통해 SwiftUI에서 시도해 볼 수 있는 방법론들과 단방향 아키텍처의 방향성을 체험 및 학습할 수 있습니다.
(이렇게 적고 보니 적용하지 않을 이유가 없어 보이긴 하지만,,, TCA 신봉자 아님)
물론 러닝 커브와 제대로 이해하고 사용하지 않았을 때의 리스크, 외부 라이브러리 의존성, 우리 정도 규모에 TCA 적용이 맞아? 와 같은 의문들을 전부 해소할 순 없을 것입니다. 하지만 명확한 이점을 살려보고자 TCA를 학습 및 적용하며 리팩토링을 진행하기로 결정되었습니다. 👏
팀원들과 1주일 정도 TCA 공식 튜토리얼과 모의 프로젝트를 통해 기본적인 학습을 진행했습니다. 이젠 실제 프로젝트에 적용하며 겪는 이야기들을 다음 포스팅에 풀어내볼까 합니다!
'iOS' 카테고리의 다른 글
캐플 리팩토링 두 번째 이야기 - 프로젝트 세팅하기 (0) | 2025.01.30 |
---|---|
구조체로 싱글톤 만들기? (0) | 2025.01.30 |
비동기 작업의 단위, Task 알아보기 (0) | 2025.01.22 |
MVC와 Cocoa MVC, 뭐가 다를까? (0) | 2025.01.16 |
인생 첫 번째 iOS 개발 면접 후기 (1) | 2025.01.14 |