# Kotlin Multiplatform 실제 도입기: 기대했던 것 vs 현실

Kotlin Multiplatform은 코드 공유라는 명확한 약속을 제시했다. 하지만 실제 프로덕션 환경에서는 기술적 이상과 현실 사이의 간극이 존재했다. 2년간의 도입 과정에서 마주한 구조적 한계와 실용적 가능성을 기록한다.
## 시작: 하나의 코드베이스라는 약속

Kotlin Multiplatform(KMP)은 단일 언어로 여러 플랫폼을 지원한다는 명제를 제시한다. iOS, Android, Web, Server를 아우르는 코드 공유. 이론적으로는 개발 리소스의 효율화와 유지보수 비용의 절감, 두 마리 토끼를 동시에 잡을 수 있는 구조다.
그러나 기술적 약속과 실제 구현 사이에는 언제나 간극이 존재한다. 크로스 플랫폼 기술의 역사는 "Write Once, Run Anywhere"라는 이상이 "Write Once, Debug Everywhere"라는 현실로 귀결되어온 과정이기도 하다.
## 기대했던 것
### 비즈니스 로직의 완전한 공유
도입을 결정할 때 가장 먼저 떠올린 것은 비즈니스 로직의 중복 제거였다. 결제 프로세스, 인증 로직, 데이터 검증 규칙 등 플랫폼에 독립적인 영역을 공통 모듈로 추출하면, 하나의 코드로 모든 클라이언트를 지원할 수 있다는 구상이었다.
Android와 iOS에서 각각 Java/Kotlin과 Swift로 동일한 로직을 구현하며 발생하는 불일치, 한쪽 플랫폼에서만 수정되는 버그, 양쪽 코드베이스를 동기화하는 데 드는 인지적 비용. 이 모든 것을 단일 코드베이스로 해결할 수 있다면 구조적 우아함과 실용적 효율을 동시에 얻을 수 있을 것처럼 보였다.
### 네트워크 레이어의 통합
Ktor Client를 활용한 네트워크 레이어 공유도 주요 기대 사항이었다. API 엔드포인트, 요청/응답 모델, 직렬화 로직, 에러 핸들링을 공통 모듈에 집약하면, 각 플랫폼은 이를 소비하는 레이어로만 존재하면 된다.

API 스펙이 변경될 때 하나의 코드만 수정하면 되고, 타입 안정성은 컴파일 타임에 보장된다. Retrofit(Android)과 Alamofire(iOS)를 각각 유지하며 발생하는 구조적 중복을 제거할 수 있다는 점은 설계 차원에서 명백한 개선이었다.
### 개발자 경험의 통합
Kotlin이라는 단일 언어로 모든 플랫폼을 다룰 수 있다는 것은 단순한 코드 공유 이상의 의미를 지닌다. 팀 내에서 플랫폼 간 장벽이 낮아지고, 한 개발자가 전체 스택을 이해할 수 있는 구조가 만들어진다. 소규모 팀에서는 이러한 통합이 생산성에 직접적인 영향을 미칠 것으로 예상했다.
## 현실: 구조적 한계와의 조우
### 플랫폼 추상화의 불완전성
첫 번째로 직면한 현실은 완전한 추상화의 불가능성이었다. KMP의 `expect/actual` 메커니즘은 플랫폼별 구현을 요구하는 지점을 명시적으로 드러낸다. 파일 시스템 접근, 암호화, 생체 인증 등 플랫폼 고유 API에 의존하는 기능은 결국 각 플랫폼에서 별도로 구현해야 한다.
문제는 이 경계가 생각보다 넓다는 점이다. UI 레이어만 분리하면 될 것이라 생각했지만, 실제로는 퍼미션 처리, 백그라운드 작업, 푸시 알림, 딥링크 등 플랫폼 컨텍스트에 깊이 의존하는 영역이 비즈니스 로직과 긴밀하게 결합되어 있었다.
결과적으로 공통 코드의 비율은 예상했던 70%가 아닌 40% 수준에 머물렀다. 나머지 60%는 여전히 플랫폼별로 구현되어야 했다.
### iOS 상호운용성의 복잡성

KMP로 작성된 코드는 Objective-C 헤더를 통해 iOS에 노출된다. 이 과정에서 Kotlin의 타입 시스템과 Swift의 타입 시스템 사이에서 변환이 일어나는데, 이것이 생각보다 매끄럽지 않았다.
Kotlin의 `suspend` 함수는 Swift에서 콜백 기반으로 변환되며, Flow는 자동으로 브리징되지 않는다. Sealed class는 Swift enum으로 매핑되지 않고 일반 클래스 계층으로 노출된다. Nullable 타입의 처리도 일관되지 않아, Swift에서 Optional unwrapping을 추가로 해야 하는 경우가 빈번했다.
결국 iOS 팀은 KMP 모듈을 직접 사용하는 대신, 얇은 래퍼 레이어를 추가로 작성해야 했다. 이 레이어는 Kotlin의 API를 Swift 관점에서 자연스럽게 만드는 어댑터 역할을 했지만, 동시에 추가적인 유지보수 지점이 되었다.
### 빌드 시스템의 복잡성
KMP 프로젝트의 빌드 설정은 단일 플랫폼 프로젝트에 비해 현저히 복잡하다. Gradle 멀티모듈 구조, CocoaPods 통합, 각 플랫폼별 타겟 설정, 의존성 버전 관리 등이 얽혀 있다.
특히 iOS 빌드에서 문제가 자주 발생했다. Xcode 버전 업데이트 시 KMP 프레임워크가 빌드되지 않거나, CocoaPods 캐시 문제로 의존성이 제대로 링크되지 않는 경우가 반복되었다. Android Studio에서는 정상 작동하지만 Xcode에서는 실패하는 빌드 오류를 디버깅하는 것은 시간 소모적인 작업이었다.
CI/CD 파이프라인 구성도 복잡도를 높였다. 공통 모듈 변경 시 모든 플랫폼의 빌드를 트리거해야 하고, 각 플랫폼의 빌드 환경을 동기화해야 했다. 이는 빌드 시간 증가와 파이프라인 유지보수 비용 증가로 이어졌다.
### 디버깅과 프로파일링의 어려움
공통 모듈에서 발생한 버그를 추적하는 것은 단일 플랫폼 코드보다 까다로웠다. Android에서는 일반적인 Kotlin 디버깅이 가능하지만, iOS에서는 KMP 모듈 내부를 디버깅하기 위해 추가적인 설정이 필요했다.
성능 문제도 마찬가지였다. 공통 모듈의 특정 로직이 iOS에서만 성능 저하를 일으키는 경우, 원인을 파악하기 위해 Kotlin/Native 컴파일러의 최적화 방식을 이해해야 했다. 메모리 릭이 발생했을 때, 그것이 Kotlin 코드의 문제인지 플랫폼 브리징 레이어의 문제인지 구분하는 것도 쉽지 않았다.
## 그럼에도 남는 것
### 도메인 모델과 비즈니스 규칙의 안정성
한계에도 불구하고, KMP가 명확한 가치를 제공하는 영역이 존재했다. 가장 성공적이었던 부분은 도메인 모델과 핵심 비즈니스 규칙의 공유였다.
Data class로 정의된 엔티티, 유효성 검증 로직, 상태 전이 규칙 등은 플랫폼에 독립적이며, 한 번 작성하면 모든 클라이언트에서 동일하게 작동했다. 이는 단순히 코드 중복을 줄인 것 이상의 의미를 지녔다. 비즈니스 로직이 단일 진실 공급원(Single Source of Truth)으로 존재함으로써, 플랫폼 간 불일치가 구조적으로 방지되었다.
복잡한 할인 정책 계산 로직이나 주문 상태 머신 같은 경우, 양쪽 플랫폼에서 독립적으로 구현했다면 미묘한 차이가 발생했을 것이다. KMP를 통해 이러한 로직을 공유함으로써, 한 번의 테스트와 검증으로 모든 플랫폼의 정합성을 보장할 수 있었다.
### 네트워크 레이어의 일관성
Ktor Client를 활용한 네트워크 레이어 공유도 예상대로 작동했다. API 스펙 변경 시 하나의 모델만 수정하면 되었고, 직렬화 오류는 컴파일 타임에 잡혔다. 각 플랫폼에서 네트워크 레이어를 별도로 유지할 때 발생하던 동기화 비용이 사라졌다.
에러 핸들링을 공통 모듈에서 처리함으로써, 네트워크 오류에 대한 대응 로직이 플랫폼 간 일관되게 유지되었다. 이는 사용자 경험의 통일성에도 기여했다.
### 팀의 인지적 부담 감소
예상치 못한 긍정적 효과는 팀의 인지적 부담 감소였다. 비즈니스 로직을 논의할 때 플랫폼 구분이 사라졌다. "이 로직은 Android에서 어떻게 구현되어 있고, iOS에서는 어떻게 되어 있는가?"를 확인할 필요 없이, 공통 모듈의 코드 하나만 보면 되었다.
새로운 팀원이 합류했을 때도 온보딩 시간이 단축되었다. 플랫폼별 코드베이스를 각각 이해하는 대신, 공통 모듈의 구조를 먼저 파악하면 전체 시스템의 핵심을 이해할 수 있었다.
## 교훈: 기술 선택의 본질
KMP 도입 경험에서 얻은 가장 중요한 교훈은, 기술은 만능 해결책이 아니라 트레이드오프의 집합이라는 점이다.
KMP는 코드 공유라는 명확한 이점을 제공하지만, 그 대가로 빌드 복잡성, 디버깅 난이도, 플랫폼 상호운용성 문제를 안겨준다. 이 트레이드오프가 합리적인지는 프로젝트의 맥락에 달려 있다.
공유할 수 있는 로직이 충분히 많고, 팀이 Kotlin에 익숙하며, 플랫폼 고유 기능에 대한 의존도가 낮다면 KMP는 가치 있는 선택이다. 반대로 UI 중심적이고 플랫폼 네이티브 기능을 많이 사용하는 앱이라면, KMP의 추상화 비용이 이점을 상쇄할 수 있다.
### 도입을 고려할 때 질문해야 할 것들
실제 도입을 검토한다면, 다음을 먼저 질문해야 한다.
**공유 가능한 로직의 비율은?** "비즈니스 로직"이라는 추상적 범주가 아니라, 구체적으로 어떤 모듈이 플랫폼 독립적인가를 파악해야 한다. 실제 코드베이스를 분석해보면 예상보다 플랫폼 의존적인 부분이 많을 수 있다.
**팀의 기술 스택은?** Android 팀과 iOS 팀이 분리된 구조라면, KMP 도입은 기술적 결정이 아니라 조직적 변화를 수반한다. Swift에 익숙한 iOS 개발자가 Kotlin 코드를 유지보수해야 하는 상황을 받아들일 수 있는가?
**빌드 및 배포 파이프라인은?** KMP를 도입하면 빌드 시스템이 복잡해진다. 이를 관리할 인프라와 역량이 팀에 있는가?
**장기적 유지보수는?** KMP는 아직 성숙 단계에 있는 기술이다. Kotlin 버전 업그레이드, 플랫폼 API 변경, 라이브러리 호환성 문제 등을 지속적으로 대응할 준비가 되어 있는가?
## 결론: 이상과 현실 사이의 균형
Kotlin Multiplatform은 완벽한 크로스 플랫폼 솔루션이 아니다. 그러나 그것은 실패를 의미하지 않는다. 기술은 약속을 100% 이행하지 못해도, 특정 맥락에서 충분히 가치 있을 수 있다.
KMP의 진정한 가치는 "모든 것을 공유"하는 데 있지 않다. 공유할 수 있는 것과 공유해야 하는 것을 구분하고, 그 경계를 명확히 설정하는 데 있다. 도메인 로직과 네트워크 레이어는 공유하되, UI와 플랫폼 특화 기능은 네이티브로 구현하는 하이브리드 접근이 현실적이다.
2년간의 경험이 남긴 것은 환멸도, 맹신도 아닌 균형 잡힌 시선이다. 기술은 도구이며, 도구의 가치는 그것을 사용하는 맥락에서 결정된다. KMP는 특정 조건 하에서 명확한 이점을 제공하는 도구이며, 그 조건을 이해하는 것이 성공적인 도입의 전제다.
기대했던 이상과 마주한 현실 사이에서, 우리는 기술을 선택하는 방식 자체를 배워간다. 완벽한 해결책을 찾는 대신, 합리적인 트레이드오프를 수용하는 것. 그것이 엔지니어링의 본질이다.
1
조회수
0
좋아요