DroidCon2019 Vienna 방문 후기

Droidcon 소개

Android 개발자에게 가장 유명한 개발자 콘퍼런스로는 Google에서 직접 주최하는 ‘Google I/O’와 ‘Android Dev Summit’을 꼽을 수 있습니다. 또한 전 세계에 퍼져 있는 글로벌 개발자 그룹인 ‘Google Developer Groups (GDG)’를 기반으로 열리는 콘퍼런스도 있고, 또 독자적으로 각 지역에서 열리는 Android 개발자 콘퍼런스도 있는데요. 그중에서도 ‘DroidCon’은 전 세계에서 가장 큰 Android 개발자 콘퍼런스 네트워크로 알려져 있습니다. 2009년 ‘DroidCon Berlin’을 시작으로 주로 유럽에서 열리다가 현재는 유럽을 넘어 중동, 아프리카, 북미에 걸친 22개 도시에서 매해 열리고 있습니다.

저는 이번에 오스트리아 빈에서 열린 DroidCon2019 Vienna에 다녀왔습니다. 지금까지는 주로 아시아나 북미에서 열린 개발자 콘퍼런스에 참가했었는데 이번에 처음으로 유럽의 개발자들을 현지에서 만나볼 수 있게 되어 많은 기대를 했습니다.

DroidCon 행사가 열렸던 오스트리아 빈 기술 대학

이번에 DroidCon Vienna에 참가하면서 받은 개인적인 인상은, 지금까지 참가했던 콘퍼런스들과는 달리 참가자들의 국적이 매우 다양했다는 점입니다. 서유럽뿐 아니라 동구권과 중동, 그리고 저 멀리 남아프리카 공화국에서 온 개발자까지 정말 다양한 배경을 가진 개발자들을 만날 수 있었습니다.

DroidCon Vienna는 26개의 발표 세션이 3개 트랙으로 이틀간 진행되었습니다. DroidCon의 특징이라고 할 수 있는 ‘Bar-camp’는 둘째 날 오후에 총 12개의 세션으로 진행되었습니다. Bar-camp는 일종의 라이트닝 토크(lightning talk)라고 할 수 있습니다. Pycon이나 다른 개발자 콘퍼런스에서 진행되는 것과 비슷하게 누구나 짧은 시간 동안 자신만의 주제로 발표할 수 있는 세션입니다. 조금 다른 점은, 연사가 미리 정해져 있지 않았고, 행사 진행 도중 발표하고 싶은 사람들이 주제를 올리고 청중들이 그에 대해 투표를 하거나, 즉석에서 신청을 받아서 발표를 진행하는 방식이었다는 점입니다. 이런 이유로 Bar-camp는 주로 콘퍼런스 후반부에 열립니다.

이번 DroidCon Vienna에선 많은 발표가 ‘Kotlin’, ‘테스트’, ‘빌드’와 관련된 주제였습니다.

Kotlin은 이제 Java를 대신하여 Android의 대표 언어로 자리매김하고 있습니다. Google의 지원 아래 Coroutine, Flow 등과 같은 여러 가지 새로운 기능이 연이어 공개되고 있어서 가장 많이 다뤄진 것이 어찌보면 당연하기도 합니다.

다음으로 테스트가 많이 다뤄졌습니다. 순수하게 테스트 기법이나 테스트 라이브러리를 주제로 한 세션뿐 아니라 거의 모든 발표에서 테스트가 언급되었는데요. 이제껏 참가했던 콘퍼런스와는 조금 다른 점이어서 흥미로웠습니다.

마지막으로 빌드도 개발자라면 빼놓을 수 없는 중요한 관심사입니다. Android Studio가 새로운 기능 추가를 잠시 미뤄두고 안정화에 중점을 둔 Project Marble을 진행하고 있는 사례에서 알 수 있듯이, 최근 1-2년간 Android 개발하는 사람들에게 빌드는 좋은 의미에서건 나쁜 의미에서건 항상 화제의 중심에 있었습니다.

 

발표된 세션 소개

저는 이번 글에서 DroidCon Vienna에서 진행됐던 총 26개의 발표 세션 중 가장 인상 깊었던 아래 4개 세션을 소개하려고 합니다. 앞서 말씀드린 대로 가장 많이 다뤄졌던 Kotlin, 테스트, 빌드를 주제로 한 세션 중에서 골랐습니다.

  • Effective Kotlin – Best practices for Kotlin programming 
  • Idiomatic Kotlin
  • My love-hate relationship with Android Studio
  • Deep Dive into Unit Testing

 

Effective Kotlin – Best practices for Kotlin programming

세션 제목인 ‘Effective Kotlin’은 발표자인 Marcin Moskala가 직접 쓴 책의 제목이기도 합니다. Java로 개발해 본 사람이라면 누구나 한 번쯤은 들어봤을 책인 ‘Effective Java’의 오마주인 것을 쉽게 알 수 있습니다. Jetbrain의 공식 Kotlin 교육 파트너인 Kt. Academy의 설립자이기도 한 Marcin Moskala는, Kotlin 교육과 관련 저술 활동을 지속적으로 해왔는데요. 그 일환으로 ‘Kotlin을 어떻게 더 잘 사용하고, 어떻게 더 책임감 있게 사용할 것인가?(How we use Kotlin for better, how to use it more responsible)’라는 주제로 Effective Kotlin을 집필하였다고 말했습니다. 

발표 내용은 책 개요와 책 내용 중 일부에 대한 강연으로 구성되었습니다. 이른바 ‘저자 직강’이었습니다. 저는 발표에서 소개된 내용 중에서 기억에 남는 개발 관련 팁 한 가지를 공유하려고 합니다.

 

Iterable vs Sequence

발표 중 가장 재미있었던 내용이기도 한데요. Kotlin의 Iterable과 Sequence의 차이점에 관한 내용이었습니다. Iterable과 Sequence는 이름만으로는 그 차이를 짐작하기 어렵습니다. 게다가 코드를 보면 아래와 같이 내부 정의가 동일하기 때문에 어떤 부분이 다른 것인지, 실제 동작이 어떻게 다를지 유추하는 게 쉽지 않습니다. 

public interface Iterable<out T> {
    public operator fun iterator(): Iterator<T>
}
public interface Sequence<out T> {
    public operator fun iterator(): Iterator<T>
}

하지만 실제 동작을 비교해 보면 둘 사이에는 굉장히 큰 차이가 있고 사용 방법도 다릅니다. Iterable과 Sequence의 차이를 간단히 말하자면, ‘lazy 연산 처리’와 ‘중간 결과 저장’이라고 말할 수 있습니다. 관련하여 예제를 보시겠습니다.

sequenceOf(1,2,3)
       .filter { print("F$it, "); it%2==1}
       .map { print("M$it, "); it *2}
       .forEach { print("E$it, ") }
// Prints: F1, M1, E2, F2, F3, M3, E6,
 
listOf(1,2,3)
       .filter { print("F$it, "); it % 2 == 1 }
       .map { print("M$it, "); it * 2 }
       .forEach { print("E$it, ") }
// Prints: F1, F2, F3, M1, M3, E2, E6,

예제에 나오듯, Sequence의 경우 입력 ‘1’의 모든 처리가 끝난 후에야 입력 ‘2’가 처리가 됩니다. 각각의 입력과 관련된 모든 연쇄 작업이 끝난 후에야 다음 입력을 처리하는 것을 확인할 수 있습니다. 이것을 ‘lazy order’ 혹은 ‘element-by-element’ 처리 방식이라고 부릅니다.

반면 Iterable의 경우, 전체 입력을 각 단계별로 처리합니다. 즉, 전체 입력에 대해서 한 단계의 작업 처리를 모두 끝낸 다음에, 다시 전체 입력에 대해서 다음 단계의 작업을 처리하는 방식입니다. 이런 처리 방식에서는 필연적으로 중간 연산 결과가 메모리에 저장됩니다. 이런 방식을 ‘eager order’ 혹은 ‘step-by-step’이라고 부릅니다.

이런 처리 방식의 차이가 실제 사용법에 어떤 영향을 끼칠까요? 아래 예제 코드를 보겠습니다. 만약 아래 예제 코드에서 ‘ChicagoCrimes.csv’ 파일의 크기가 10GB라면 어떻게 될까요? Iterable의 경우, 각 단계의 연산마다 중간 연산 결과를 저장하기 위한 새로운 collection을 생성해야 합니다. 따라서 결과적으로 원래 입력된 데이터의 크기보다 몇 배나 큰 메모리가 필요합니다. 입력된 데이터의 크기가 크면 OutOfMemoryError가 발생할 수밖에 없는 상황입니다.

File("ChicagoCrimes.csv").readLines()
    .mapNotNull { it.split(",").getOrNull(6) }
    .fitler { "CANNABIS" in it }
    .count()
    .let(::println)
// Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

위 코드를 Sequence로 구현하면 아래와 같습니다. 코드만 보면 크게 다를 게 없지만, Iterable과 다르게 전체 데이터에 대한 중간 연산 결과를 저장하지 않고 각각의 데이터 하나하나를 처리하는 방식이라서 내부적으로 새로운 collection를 생성하지 않습니다. 따라서 메모리를 좀 더 효율적으로 사용할 수 있고, 좀 더 빨리 처리할 수 있습니다.

File("ChicagoCrimes.csv").useLines { lines: Sequence<String> ->
    lines.mapNotNull { it.split(",").getOrNull(6) }
        .fitler { "CANNABIS" in it }
        .count()
        .let(::println)
}
// Works fine with a big size file

이외에도 발표에서 여러 가지 유용한 팁을 많이 알려주셨는데요. 발표를 보고 나니 일단 책을 사서 보고 싶다는 생각이 들어서 ‘역시 저자 직강! 책 광고 제대로 하셨구나!’라는 생각이 들었습니다. 🙂  만약 기회가 된다면 이 책을 한글로 번역해 보는 것도 괜찮겠다는 생각이 들었습니다. 

 

Idiomatic Kotlin

이번 세션은 제목부터 유머러스했는데요. 일반적으로 잘 알기 어려운 Kotlin의 특성을 이용한 재미있는 코드 사례가 소개되었습니다. 사실 발표에 나온 내용 중 바로 실제 업무에 적용해서 활용할 수 있는 부분이 많지는 않았습니다. 하지만 각각의 코드 사례가 왜 그렇게 동작하는지 이해하기 위해선 Kotlin의 내부 동작 원리를 제대로 파악하고 있어야만 했습니다. 그런 측면에서 아주 인상적인 발표였습니다. 

발표에 등장했던 코드 사례 중 아주 재미있었던 코드 예제 하나를 소개하겠습니다.

Spam

이 코드를 처음 봤을 때, 애초에 컴파일이 가능한지조차 예상할 수 없었습니다. 아래 코드에서 3번째 줄과 5번째 줄의 throw와 return은 각각 어떻게 동작할까요? 그리고 제대로 동작한다면 실제 반환값은 무엇이 될까요?

fun process(value: Int): Int {
    if (value % 2 == 0) {
        throw return 78 + value
    } else {
        return throw return 200
    }
}
 
println(process(1)) // return: ??
println(process(2)) // return: ??

신기하게도 1에 대한 반환값은 200, 그리고 2에 대한 반환 값은 80입니다. 마치 throw와 return이 중복되어 있지 않은 것처럼 멀쩡한 결과가 나옵니다. 어떻게 이런 결과가 나올까요?

그 이유는 바로 Kotlin 내부의 상속 관계 때문입니다. 아시다시피 모든 Kotlin 클래스의 최상위 클래스는 Any입니다. 그런데 Nothing이라는 좀 특별한 종류의 클래스도 있습니다. 아마도 Nothing을 throw와 연계해 사용하는 것을 보신 분도 계실 겁니다. Nothing은 실제로 값이 존재하지 않는다는 개념의 클래스입니다. 따라서 인스턴스를 만들지도 못합니다. Nothing은 아래처럼 return 없이 언제나 excpetion을 던지는 경우에 사용합니다.

fun fail(message: String): Nothing {
    throw IllegalArgumentException(message)
}

그런데 이 Nothing엔 한 가지 더 특별한 점이 있는데요. 바로 아래 그림처럼, 컴파일러가 Nothing을 모든 유형의 서브(sub) 유형인 것처럼 인식한다는 점입니다.

따라서 코드의 3번째와 5번째 줄의 값은 중간에 Throwable과 Int가 Nothing으로 캐스팅된 후에, 다시 Int로 캐스팅되어 처리된다고 합니다.

fun process(value: Int): Int {
    if (value % 2 == 0) {
        throw return 78 + value // Throwable <- Nothing
    } else {
        return throw return 200 // Int <- Nothing
    }
}

이미 말씀드렸지만, 위 예제는 사실 실무에선 전혀 유용하지 않습니다. 만약 실제로 사용한다면 동료들로부터 뜨겁고 열렬한 코드 리뷰를 받을 수 있을 거라고 생각합니다. 🙂 그럼에도 그 기반에 깔려 있는 Kotlin 내부에 대한 이해는 앞으로 Kotlin을 사용하는 데 있어서 언젠가 도움이 되리라고 생각합니다.

 

My love-hate relationship with Android Studio

Android 개발자의 주된 관심사 중 하나가 바로 Android Studio 개발 환경과 빌드 환경이라고 생각합니다. 저 역시 LINE과 같이 크기가 큰 앱을 개발하다 보니 빌드하는 데 너무 오래 걸려서 혹시 뭔가 문제가 있는 것은 아닌가 하고 생각한 경우도 있고, 코드 자동 완성이나 린트(lint) 표시 같은 IDE의 필수 기능들이 잘 작동하지 않거나 Android Studio가 갑자기 멈춰버리는 문제를 많이 겪었습니다. 발표자는 서두에서 제가 겪은 이런 문제들이 저만의 문제가 아니라 전 세계 개발자들이 함께 겪고 있는 문제라는 것을 알려주면서 시작했습니다. 또한 이런 문제를 유머로 승화시킨 여러 가지 밈(meme)을 발표 자료에 적절히 사용해서 유쾌하게 공감할 수 있었습니다.

The experience

발표자는 개발 및 빌드 환경에 대한 여러 가지 경험을 공유했습니다. 그중에서도 개발자들의 대표적인 불만사항을 꼽아본다면, 아래와 같은데요. 우선 악명 높은 Android Studio의 인덱스 업데이트를 비롯한 느린 속도, 버그가 많아서 안정적이지 않은 점, 그리고 비정상적으로 많은 컴퓨팅 자원을 요구하는 것을 꼽을 수 있습니다.

Terminal to rescue

이 발표의 핵심을 한 마디로 정리하자면 Android Studio 대신 다른 가벼운 코드 에디터를 사용하고 터미널에서 직접 Gradle 빌드를 실행하자는 것입니다. 언뜻 황당하게 들릴 수도 있습니다만, 발표에선 진지하게 Android Studio의 도움 없이 개발하기 위해 무엇이 필요했고, 어떤 과정을 거쳤으며, 그 과정에서 얻은 팁이 무엇인지 공유되었습니다. 실제로 발표자는 Visual Studio Code 에디터로 개발하고 터미널에서 빌드하여 Android Studio를 사용할 때와 비교해 빌드 시간을 89%나 줄일 수 있었다고 합니다.

그리고 터미널에서 사용할 수 있는 각종 툴에 대해 소개했는데요. 가장 기본적인 툴이라고 할 수 있는 adb(Android Debug Brideg)를 비롯하여 avdmanager(AVD Manager), 여러 가지 SDK 기본 제공 툴, 그리고 Gradle과 lint를 터미널에서 사용하는 것이 소개되었습니다(더 많은 정보는 Android 개발자 페이지인 Command line tools를 참고하시기 바랍니다). 아래는 그 외 발표에서 알게 된 유용한 툴입니다.

  • Detekt: Kotlin 언어를 위한 정적 분석
  • Classyshark: APK 내부 디컴파일(decompile) 및 분석
  • Scrcpy: 터미널을 통한 기기 화면 미러링 및 녹화
  • Battery Historian: Python에 기반한 배터리 관련 정보, 이벤트 수집
  • Mainframer: 원격 프로젝트 빌드. rsync로 프로젝트와 결과 apk 동기화

또한 터미널 명령어, 특히 alias를 이용해서 복잡한 여러 가지 툴 사용을 자동화하는 예를 소개했습니다. 

// 연결되어 있는 모든 기기에 apk를 설치하는 alias
alias apkInstall="adb devices | tail -n +2 | cut -sf 1 | xargs -I X adb -s X install -r $1"
// debug로 apk를 빌드한 후 연결되어 있는 모든 기기에 apk를 설치하는 alias
alias buildAndInstallApk="./gradlew assembleDebug && apkInstall ./app/build/outputs/apk/debug/app-debug.apk"
// 연결된 기기에서 debug apk 실행
function launchDebugApk(){
    local APP_PACKAGE_NAME=$(getPackageName ./app/build/outputs/apk/debug/app-debug.apk)
    adb shell monkey -p $APP_PACKAGE_NAME 1 1>/dev/null 2>&1;
}
// apk를 빌드하고 설치한 후 실행하는 alias
alias buildInstallLaunchDebugApk="buildAndInstallApk && launchDebugApk"
 
// 스크린숏 촬영
alias screenshot="adb exec-out screencap -p > screen-$(nowdate).png"
// 연결된 모든 기기에서 앱 제거
alias rmApp="adb devices | tail -n +2 | cut -sf 1 | xargs -I X adb -s X uninstall $1"
// 연결된 모든 기기에서 앱 데이터 제거
alias clearApp="adb devices | tail -n +2 | cut -sf 1 | xargs -I X adb -s X shell pm clear $1"

마지막으로 터미널을 통한 툴 활용의 한계도 짚어 주었는데요. 대표적으로 Constraint layout editor, Profiler, Resource manager는 Android Studio에 내장되어 있기 때문에 터미널에선 사용할 수가 없고, 오직 Android Studio를 통해서 사용할 수밖에 없다고 말했습니다.

최근 Google에서도 Android Studio의 여러 문제점을 인지하고 안정성에 초점을 맞춘 일명 ‘Project Marble’을 시작했고, Android Studio 3.5의 배포에 맞춰 본격적으로 개선이 시작되고 있습니다. 이런 점진적인 개선 상황은 Android Studio의 Known issue 페이지를 통해서 업데이트되고 있으니, 관심을 갖고 개선 사항을 지켜보면 좋을 것 같습니다.

 

Deep Dive into Unit Testing

마지막으로 소개할 발표 세션은 유닛 테스트에 관한 내용입니다. 아시다시피 테스트는 개발에서 핵심적인 단계이지만, 좋은 테스트를 만드는 것은 언제나 쉬운 일이 아닙니다. 이번 발표에서는 편하게 유닛 테스트할 수 있는 여러 가지 방법들과, 특히 Kotlin에서 간결하면서도 효과적인 테스트를 작성할 수 있게 도와주는 테스트 콘셉트와 프레임워크를 소개했습니다. 실제 발표 내용은 굉장히 많은 내용을 담고 있었는데요. 그중에서 가장 쉽게 사용해 볼 수 있는 내용 한 가지를 소개하겠습니다.

Parameterized test

테스트를 작성하는 것은 코드가 조건에 따라 예상한 대로 작동하느냐를 검증하는 작업입니다. 따라서 많은 경우 입력 파라미터에 따라 작동의 변화가 발생하는 경계 조건을 테스트해야 하는데요. 이때 JUnit의 Parameterized test를 이용하면 여러 가지 입력 값을 손으로 일일이 작성하지 않고 쉽게 테스트를 작성할 수 있습니다.

아래 예제는 각각 JUnit4와 JUnit5에서 Parameterized test를 작성한 것입니다. Parameterized test를 선언하고 그에 따른 argument를 제공하는 것은 동일하지만, 그 형식이 조금 다릅니다.

JUnit4에서는 한 테스트에 하나의 runner만 실행되고, 파라미터들이 static 함수를 통해서 제공됩니다. 따라서 하나의 테스트 클래스 내에서 동일한 파라미터를 공유해야 하기 때문에 여러 개의 Parameterized test를 유연하게 수행하는 것이 어려웠습니다. 하지만 이와 달리 JUnit5에서는 개별 테스트 함수 별로 Parameterized test을 선언할 수 있고, 각 테스트마다 별도 클래스로 파라미터를 제공할 수 있어서 좀 더 유연하게 Parameterized test를 수행할 수 있다고 말했습니다.

JUnit4JUnit5
  • 클래스에 Parameterized runner를 선언
  • Static으로 Parameterized parameters를 선언하고 Collection으로 입력 값을 반환
  • 개별 메서드에 Parameterized test를 선언
  • Argument provider 클래스를 제공

 

마치며

이상으로 DroidCon Vienna 방문 후기를 마치겠습니다. 이번 콘퍼런스는 쉽게 만나기 힘들었던 유럽의 개발자들을 만나 그들의 고민과 생각을 들어볼 수 있는 아주 좋은 기회였습니다. 서로 하고 있는 일은 달라도 같은 고민을 하고 있으며, 앞으로 개선되길 원하는 부분도 비슷하다는 게 정말 재밌었습니다. 또한 항상 테스트를 중요하게 여기는 모습이 신선하게 느껴졌습니다. 이번 콘퍼런스의 경험이 앞으로 LINE Android 앱 개발과 테스트 환경을 개선하는데 많은 도움이 될 것 같습니다.

 


 

레퍼런스

Related Post