똑똑, 프로젝트에 코틀린을 도입하려고 합니다.

Jul.18.2017 최윤석

APP

이 글은 2017년 4월, 배민프레시에 코틀린 도입하는 과정에서 작성된 글을 바탕으로 재구성되었습니다.

시작하며

2017년 4월, 모바일 플랫폼에서 사용자 경험 극대화를 위한 여정의 첫발로 메인 개편 프로젝트를 시작했습니다. 이 프로젝트의 목표는 다음과 같습니다.

  • 앱 사용성 향상을 위해 기대를 배신하는 UX, 어딘지 모르는 고객 위치, 느린 UI 로딩을 해결
  • 메인 페이지의 이탈률 3% 감소, 로딩 속도를 1초로 개선
  • 모바일 반찬 가게 최초 네이티브 UX 제공

기존 배민프레시의 앱은 웹뷰 기반에 하이브리드 구조로 만들어져 있었습니다. 이를 네이티브 앱으로 전환하며 UX 개선과 성능 향상을 꾀했습니다.

FC서비스개발팀은 새로운 안드로이드 앱을 만들 도구로 코틀린을 도입하고, iOS 앱은 스위프트를 도입하는 것으로 결정했습니다. 이 글은 안드로이드 앱을 개발할 때 접하는 아쉬운(또는 불편한) 점과 그 대안을 생각해보고, 왜 코틀린을 도입했는지에 대한 고민을 담았습니다.

안드로이드 개발을 하면서 (자주) 마주하는 아쉬운 점

NullPointerException

안드로이드 개발을 하면서 크래시 수집된 결과를 보면 정말 많은 부분이 NullPointerException(이하 NPE) 입니다. 이유에 대해 고민해 보니 세 가지 정도로 요약할 수 있었습니다:

  • 필드나 변수를 선언할 때 기본값을 강제하지 않습니다.
  • 안드로이드 프레임워크에서 필드, 메소드 시그니처 등이 null 일 수 있는지, 어느 시점에 초기화가 되는지에 대해 제대로 문서화가 되어 있지 않습니다. Support Annotations 라이브러리에 @NonNull, @Nullable 애너테이션이 도입된 이후로 많이 쓰이고 있지만 아직은 사각지대가 많이 남아있습니다.
  • 비동기 코드를 많이 사용해야 하는 안드로이드 특성과 액티비티, 프래그먼트 자체의 라이프사이클이 있기 때문에 라이프사이클 특정 시점에서 null일 수 있습니다.

NPE에 대처하는 가장 일반적인 방법은 그 부분에 not null 체크를 추가하는 것입니다:

if (foobar != null) {
    // foobar 사용하는 코드
}

다른 방법도 있습니다:

// NPE를 try - catch 로 처리
try {
    foobar.doSomething();
} catch (NullPointerException e) {
    // 예외 처리
}

이렇게 처리를 해두면 당분간은 좋습니다. 그러나 유지보수를 하다 보면 결국 코드의 대부분이 if / try 문에 둘러싸이게 됩니다. 또한, 새로운 코드를 작성할 때 null이 아닐 수 있지만 그래도 null 처리를 해두는 게 안전하지 않을까 하는 마음에 불필요한 처리를 하게 되고 이것을 본, 팀의 다른 개발자분도 별다른 고민 없이 비슷한 코드를 만들 게 됩니다. 왜냐하면, 크래시가 발생해서 앱이 죽는 것보단 낫기 때문입니다. 자바 8에서는 보다 안전한 null 처리를 위해 Optional이 추가됐지만 아쉽게도 자바 8 지원이 포함된 안드로이드 스튜디오 2.4 프리뷰 4 에서도 Optional에 대한 언급은 없었습니다.

안드로이드 보일러 플레이트와 다소 부족한 자바 언어의 표현력

안드로이드 프레임워크는 성능을 매우 중요하게 생각하는 프레임워크인 만큼 대부분 추상화보단 저수준의 API를 제공합니다. 따라서 관련된 보일러 플레이트 코드가 많을 수밖에 없는데 자바의 다소 부족한 표현력이 더해져 실제 실행시키고 싶은 코드보다 보일러 플레이트 코드가 더 많은 경우가 자주 나타납니다.

button.setOnClickListener(new View.OnClickListener {
    @Override
    void onClick(View view) {
        doSomething(view); // 실행시키고 싶은 코드
    }
});

물론, 자바도 표현력 개선을 위한 여러 가지 시도들이 있었습니다.

// 람다로 보일러 플레이트 코드 제거
button.setOnClickListener(view -> doSomethingWith(view));

// 메소드 레퍼런스
button.setOnClickListener(this::doSomethingWith);

안드로이드 스튜디오 2.4 프리뷰 4 버전에서 공식적으로 자바 8 지원을 하게 됨으로써 더이상 Retrolambda를 사용하지 않고 위의 표현이 가능합니다.

안드로이드에서 현재 자바 현황

  • 안드로이드 스튜디오 2.4 프리뷰 4 부터 JDK 1.8로 빌드가 가능합니다. 단, 자바 1.6/1.7/1.8 기능 및 API를 전부 사용 할 수 있는 건 아니며 그 안에서도 minSdk에 따라 달라집니다.
  • 1.8 지원으로 실질적으로 사용 할 수 있는 기능은 람다, 메소드 레퍼런스 정도며, 지원되는 1.8 API는 minSdk 24부터 기능이라 사실상 사용하지 못합니다.
  • 요약하자면 1.6 기반 API에 주로 문법에 관련된 기능과 안드로이드 SDK가 추가된 형태라고 볼 수 있습니다.

다만, 이렇게 보일러 플레이트 코드를 제거해 표현력을 높일 수 있는 부분은 많지 않으며 람다와 메소드 레퍼런스만으로는 전반적인 안드로이드 보일러 플레이트에 대응하기엔 무리가 있어 보입니다.

대표적인 안드로이드의 보일러 플레이트 코드의 예로는 다음과 같이 XML 레이아웃 파일에 정의한 뷰 레퍼런스를 가지고 오는 부분이나 SQLite 트랜잭션을 처리하는 부분이 있습니다.

// 형 변환까지 해줘야 해서 더 번잡해 보입니다.
progressBar = (ProgressBar) findViewById(R.id.progress_bar);
textView = (TextView) findViewById(R.id.textView);
webView = (WebView) findViewById(R.id.web_view);
db.beginTransaction();
try {
    db.delete("members", "first_name = ?", arrayOf("윤석")); // 실행시키고 싶은 코드
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

이 부분에 유틸리티 클래스나 패턴을 활용해 반복되는 부분을 추출할 수도 있을 겁니다. 다만, 이런 보일러 플레이트가 안드로이드 API 전반에 걸쳐 있기 때문에 파일 처리, 비트맵 처리, 뷰 초기화, UI 관련, DB 등 추가하다 보면 유틸리티 클래스가 비대해질 수밖에 없으며 유지보수 하는 데 어려움이 따르게 됩니다.

이외에도 대부분의 앱이 리스트를 사용하는 만큼 리스트 아이템을 반복하며 처리하는 일이 상당히 많은데 자바 8의 스트림 API를 사용하지 못하는 건 아주 아쉽습니다. 반복문과 조건문을 사용하다 보면 모든 케이스에 대한 테스트가 어려워 실제 라이브 환경에서 예외 상황을 겪는 경우가 많았습니다.

자바에서 아쉬운 점을 보완하는 대안책

보일러 플레이트 코드 제거
APT 쪽은 크게 전반적인 보일러 플레이트를 없앨 수 있는 AndroidAnnotations와 그외 라이브러리로 나눌 수 있습니다. APT 라이브러리는 안드로이드 보일러 플레이트 코드를 하나의 애너테이션으로 대체 할 수 있는 큰 장점을 가집니다. APT마다 사용하는 방법이 다를 수 있습니다만 일반적으로 원래 클래스를 상속해 코드를 자동 생성해주는 방법을 쓰기 때문에 자동 생성된 클래스를 사용하지 않으면 APT 기능을 사용할 수 없으며 빌드를 해봐야 에러를 발견할 수 있고 에러가 발생했을 경우 정확한 원인이 명시되지 않는 경우도 많아 디버깅에 어려움을 가집니다.

스트림 대안
streamsupport, Lightweight-Stream-API는 리스트 처리의 아쉬운 점을 보완할 수 있는 라이브러리로 리스트 처리를 반복문과 조건문보다 스트림을 사용하면 상당히 견고한 코드가 됩니다. 두 라이브러리가 자바 8 API의 대안을 제공하는 부분에는 차이가 있지만 공통적으로 스트림과 Optional을 제공하며, 안드로이드 지원을 명시해 두고 있습니다.

RxJava 또한 자바 8 스트림의 훌륭한 대안이며 이미 안드로이드 커뮤니티에서 널리 쓰이고 있는 라이브러리입니다. 안드로이드에서 많이 사용되면서 메인 스레드에서 구독한 코드가 실행되게 하거나 특정 라이프사이클에서 구독을 중지시키는 등의 보다 안드로이드 플랫폼에 특화된 기능을 제공하고 있습니다. 스트림의 대안을 넘어서 비동기, 이벤트 기반 개발을 위해서 꼭 필요한 라이브러리입니다.

코틀린이 아쉬운 부분을 다루는 방법

편해지는 것 같긴 한데…

저 뿐만 아니라 대부분의 안드로이드 개발 환경이 위의 대안책에 네트워크 라이브러리, 이미지 로더 등을 추가해 프로젝트를 꾸렸을 거라고 생각합니다. 그런데 언제부터인가 편해지기 위해 의존성을 추가하는 게 부담으로 다가오기 시작했습니다. 오픈소스 라이브러리는 작성자가 모두 다르기 때문에 같은 APT 기반의 라이브러리를 사용하더라도 풀어내는 방법이 달라 각각의 라이브러리를 학습해야 하는 부담이 있고 의존성이 늘어갈수록 관리 부담(새 버전 확인, 등록된 이슈 및 회피 방법)이 커지는 부분도 있었습니다. 또한, 어떤 라이브러리를 사용하더라도 NPE에서 자유로울 순 없었습니다.

라이브러리들의 의존을 줄이면서 보다 일관적인 해결책을 가진 방법, 보다 안전하게 사용할 수 있는 방법이 필요하다고 느꼈습니다.

NullPointerException 다루기

코틀린이 언어 수준에서 제공하는 Null Safety는 null을 처리하는 데 있어 안전한 방법을 제공합니다. 코틀린으로 코드를 작성하면서 느꼈던 좋은 점 중 하나는 변수를 선언하는 시점부터, 그 변수의 특성을 고민하게 만들었다는 것입니다. 변수를 선언할 때 읽기만 가능한지 또는 쓰기도 가능한지, null 값을 가질 수 있는지 없는지, 그리고 초깃값으로 뭘 가지는지 정해야 합니다.

// 읽기만 가능한 프로퍼티 선언 및 초기화
val a: Int = 1

// 읽기만 가능한 프로퍼티 초기화 및 타입 추론(Int)
val b = 1

// 읽기 쓰기 전부 가능한 프로퍼티 초기화 및 타입 추론(Int)
var d = 10

// 에러! 초기화하지 않았음
val c:Int

// nullable이지만 초기화하지 않았으므로 생성자에서 반드시 초기화 해줘야 됨
val e: Int?

// nullable이지만 에러! 초기화하지 않았음
var f:Int?

프로퍼티가 어떤 값을 가져야 하는지 그리고 값이 바뀔 수 있는지 아닌지에 대해 먼저 고민하고 코드를 작성하게 해 NPE를 막는 데 많은 도움을 줍니다. 그리고 null을 안전하게 다룰 수 있는 문법을 지원해주기도 하며, 만약 null을 가질 수 있는 변수를 올바르게 처리하지 않으면 컴파일러 수준에서 에러를 발생시킵니다.

// foobar가 null이 아닌 경우에만 doSomething()을 호출함
foobar?.doSomething()

또한 메소드 시그니처에서도 해당 파라미터가 null일 수 있는지 아닌지를 지정할 수 있습니다.

// member는 nullable!
fun sendPushNotification(member:Member?) {
    // 컴파일 에러! null일 수 있으므로 그냥 사용할 수 없음
    if (member.isAgreedPushNotification) {
    }

    if (member?.isAgreedPushNotification) {
        // 멤버가 null이 아니고 푸시 동의를 한 경우
    }
}

이렇게 변수(또는 인자, 매개변수 등)의 선언부터 사용까지 null 상태를 고려하도록 언어가 강제하기 때문에 보다 안전한 코드를 작성할 수 있습니다.

보일러 플레이트 개선하기

Higher-Order Functions and Lambdas, Function References
람다와 메소드 레퍼런스를 지원하고, 자바 6 버전 이상 호환되기 때문에 안드로이드 SDK에 제한적이지 않습니다.

// 마지막 파라미터가 함수인 경우 괄호 생략 가능
button.setOnClickListener 
button.setOnClickListener(this::clickedButton)

// 파라미터를 사용하지 않는 경우 생략 가능
button.setOnClickListener 

Collections (lists, sets, maps, etc)
코틀린의 컬렉션은 filter, map, foreach와 같은 다양한 고차함수 API를 제공하고, 변경 가능한 컬렉션과 불가능한 컬렉션을 엄격히 구분합니다.

val members:MutableList<Member> = mutableListOf(member1, member2, member3)
val readOnlyMembers:List<Member> = members

// 아래 코드들은 컴파일 에러 발생! 리스트를 변경할 수 있는 메소드가 없음
readOnlyMembers.add(member4)
readOnlyMembers.remove(0)
readOnlyMembers.clear()

서버에서 수신받은 데이터를 컬렉션으로 다룰 일이 많기 때문에 이런 컬렉션 기능은 코드를 보다 간결하고, 안전하게 만드는 데 도움이 됩니다.

members
    .filter 
    .forEach 

Extension Functions
확장 함수(Extension Functions)는 이미 존재하는 클래스에 새로운 메소드를 추가할 수 있는 강력한 기능입니다.

// Context 클래스에 toast 함수 추가
fun Context.toast(message: CharSequence) {
    // this는 Context 인스턴스!
    Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

// 사용 예
context.toast("네트워크를 확인해 주세요.")

앞에서 나왔던 SQLite 트랜잭션 코드를 코틀린 확장 함수를 사용해 재구성해보면 다음과 같이 표현해볼 수 있습니다.

// 자바는 함수형 인터페이스를 람다로 표현하는 방식이지만 코틀린은 고차 함수를 지원해 람다 자체를 타입으로 지정 가능
fun SQLiteDatabase.inTransaction(block: (SQLiteDatabase) -> Unit) {
    beginTransaction()
    try {
        block(this)
        setTransactionSuccessful()
    } finally {
        endTransaction()
    }
}

// 사용 예
db.inTransaction {
    // it은 코틀린이 만들어주는 임시 변수
    it.delete("members", "first_name = ?", arrayOf("도연"))
}

코틀린은 고차 함수를 지원하기 때문에 함수를 파라미터로 넘기는 게 가능합니다. 위 코드는 트랜잭션 처리가 되어 있고, 파라미터로 람다를 받는 inTransaction 함수를 SQLiteDatabase에 추가하였습니다. 그리고 전달받은 람다를 트랜잭션 템플릿 안에서 실행해 트랜잭션 관련 보일러 플레이트 코드를 없앨 수 있습니다.

View Injection
제트브레인에서 제공하는 Kotlin Android Extensions 그래들 플러그인을 추가하면 뷰 인젝션을 사용할 수 있습니다.

<!-- product_list_product_item.xml -->
<Button
    android:id="@+id/addToCartButton"
    android:layout_width="50dp"
    android:layout_height="32dp"    
    android:background="@drawable/selector_cart_button"
    android:foreground="?android:attr/selectableItemBackground"/>

지정한 id명으로 프로퍼티가 생성되며 Button 타입이 됩니다. 액티비티뿐만 아니라 프래그먼트, RecyclerView.Adapter에서 인플레이트한 뷰도 사용 가능합니다.

// product_list_product_item.xml 기준으로 뷰 인젝션
import kotlinx.android.synthetic.main.product_list_product_item.view.*

// Button 타입
addToCartButton.setOnClickListener {
   // 선택한 상품 장바구니 담기
}

자바와 호환

코틀린과 자바의 호환성은 정말 탁월합니다. 공식 사이트에서도 100% interoperable with Java and Android 라고 소개하고 있고, 언어 사양에서도 자바와의 연계가 중요한 설계 원칙 중 하나로 세워져 있습니다.

코틀린에서 기존 자바 코드를 사용하는 데 있어 중간에 뭔가를 통하거나 할 필요가 전혀 없고 일반 자바 코드를 작성하듯 사용할 수 있기 때문에 기존 안드로이드 라이브러리를 제약 없이 사용할 수 있습니다. APT 역시 지원하기 때문에 코틀린을 사용함으로써 기존에 잘 사용하던 것을 포기할 필요는 없습니다. (하지만 많은 부분이 코틀린을 사용한 코드로 대체될 겁니다)

도입시 우려되는 부분과 그에 대한 의견

서비스 프로덕트에 새로운 언어를 도입한다는 것은 도전적인 과제입니다.

현재 팀 내 개발자를 포함해 앞으로 팀에 합류할 개발자들 모두가 새로운 언어를 학습해야 하고, 프레임워크 또는 라이브러리를 선별하는 등 새로운 언어에 맞춰 팀 내 개발환경에 변화가 필요합니다. 개발을 진행하는 동안 기술적 이슈가 발생했을 때 대응시간이 상대적으로 오래 걸릴 수도 있고, 수개월에 개발 후 끝나는 것이 아니라 이후 수년에 걸쳐 지속해서 운영하며 개선 및 유지관리 업무도 수행해야 합니다.

코틀린이 내세우는 장점 중에는 언어에 대한 학습 곡선이 매우 낮고, 자바를 다룰 줄 아는 안드로이드 개발자가 더욱 효율적으로 코드를 작성하게 도와준다는 것입니다. 학습에 필요한 내용은 코틀린 레퍼런스 문서에 명료하게 잘 작성되어 있고, 문서 양도 많지 않아 며칠이면 읽고 코틀린 코드를 다룰 수 있습니다. 고급 기능과 함께 능숙하게 언어를 사용하려면 다소 시간이 걸릴 수 있지만, 전반적으로 간결한 언어입니다. 팀 내 코틀린 도입에 대한 의견을 제안한 후 코틀린을 모르던 안드로이드 개발자가 약 일주일간 학습을 시도했습니다. 그 결과 코드를 읽고, 작성하는 데 무리가 없었습니다. 자바와의 호환성이 높고, 상호 운용이 가능하기에 언어의 특징만 잘 숙지하면 언어를 다루는 데 있어 어렵지 않다고 판단해주셨습니다.

또 다른 장점으로 안드로이드/자바 플랫폼과 100% 양방향 호환성을 내세우고 있습니다. 따라서 안드로이드 SDK를 포함해 안드로이드 플랫폼(또는 자바 플랫폼)에서 동작하는 프레임워크와 라이브러리를 그대로 사용할 수 있으며, 그레이들이나 메이븐과 같은 빌드 도구도 사용할 수 있으므로 추가적인 개발비용이 들지 않습니다. 더 나아가 코틀린 생태계에 Kotlin Android Extensions, Anko 등을 통해 생산성 향상을 꾀할 수도 있습니다. 기존 앱에 코틀린 코드를 섞어 테스트를 해보았으며 기능적으로 문제없이 동작한다는 것을 확인했습니다.
또한, 코틀린 개발을 주도하고 있는 곳은 제트브레인입니다. 자바 외에도 스칼라, 스위프트, 파이썬 등 다양한 언어들의 IDE를 개발하고 있습니다. 거기에 인텔리제이를 안드로이드 개발을 위해 특화한 형태인 안드로이드 스튜디오로 안드로이드 앱을 개발하는 것이 업계에 자리 잡은 지 오래입니다. 프로그래밍 언어에 대한 이해도나 관련된 기술력이 코틀린에 대한 신뢰성을 높이 보게 되었습니다.

하지만 코틀린 기반으로 안드로이드 앱을 개발하는 데 있어 공개된 모범 사례들이 많지 않기 때문에 개발함에 있어 시행착오가 있을 수 있습니다.

좋은 소식은 2016년 이후 코틀린 커뮤니티가 가파르게 성장하고 있다는 것입니다. 공식 블로그의 통계에 따르면 16만 명의 사용자와 공식 커뮤니티도 4배 이상 성장, GitHub에 8132개의 저장소가 생성되어 있고, 10만 라인 이상의 코드도 쌓였습니다. 그리고 관련 도서도 출간되고 있으며, 나라별 로컬 커뮤니티도 조금씩 자리 잡고 있습니다. 다양한 안드로이드 오픈 소스로 많이 알려진 Square를 포함해 Pinterest, Basecamp 등의 회사에서 코틀린을 사용하고 있으며, 관련 사례들을 컨퍼런스 또는 블로그 등을 통해 발표하고 있습니다.

이슈를 논할 수 있는 커뮤니티와 다양한 회사에서 도입 사례들의 공유, 제트브레인의 전폭적인 지원이 있기에 시행착오를 줄일 수 있다고 생각합니다.

마치며

이외에도 코틀린이 지원하는 기능들은 더 있습니다만 여기서 언급한 기능들이 도입을 결정한 가장 큰 이유입니다. 코틀린을 도입함으로써 코드를 더 간결하게 표현할 수 있고, 간결하게 표현함으로써 코드에서 발생할 수 있는 버그를 줄이며, 익숙해지면 기존 자바 코드보다 더 나은 가독성을 가진다고 생각합니다. 하지만 코틀린도 은 탄환은 아닙니다. 많은 장점을 얻을 수 있지만 여전히 설계는 온전히 개발자의 몫이며 간결한 코드 이상으로 어떤 설계를 가져갈 것인가 역시 전체 애플리케이션 개발에 있어 정말 중요한 부분이라고 생각합니다.

모든 행위의 동인에는 개인이나 집단의 욕망이 반영되어 있다.

앞서 말했듯이, 코틀린은 은 탄환이 아닙니다. 새로운 기술을 도입하고자 할 때는 그 기술에 강점과 약점을 명확하게 이해하고, 해결하고자 하는 문제에 대한 인식, 그리고 팀을 포함 주변 상황을 잘 살펴보고 판단하고자 하는 노력이 필요하다고 생각합니다.

팀에서 코틀린을 도입하는 데 있어 삿된 욕망이 아예 없다고는 할 수 없을 것 같습니다. 코틀린을 사용하면서 알게 되는 새로운 개념과 패러다임을 익혀가는 과정은 개발자로서 성장에 즐거움을 안겨줄 것이며, 성장에 대한 피드백은 서비스 프로덕트를 더 효율적이고 안정적으로 개발하고 운영할 수 있도록 해줄 것입니다.

Google I/O 2017에서 코틀린이 안드로이드 공식 언어로 지정됐습니다.

참고