안드로이드를 위한 코틀린: 베이직

안드로이드를 위한 코틀린: 베이직

2017년 이전에는 안드로이드 앱 개발자들은 거의 독점적으로 자바 6를 프로그래밍 언어로 사용했다. 자바 6는 안드로이드 장치가 출시되기 전 2006년에 출시되었다.

JetBrains는 안드로이드 스튜디오의 기본이 되는 IntelliJ IDEA를 개발했다. 그리고 2011년에 코틀린을 출시했다. 하지만 안드로이드 개발자들은 코틀린을 사용하지 않았다. 코틀린은 출시한 상태였지만, 안정적이지 않았다. 코틀린 언어에 중요한 변화가 있을 때, 개발자들은 소스코드를 변경해야만 했다. 그리고 5년 후, 코틀린 1.0이 출시되었다. 많은 수의 얼리 어답터들 덕분에 무사히 개발될 수 있었다.

2017년 구글 I/O에서, 구글은 코틀린을 메인 언어로 지원할 것이라고 발표했다. 안드로이드 스튜디오 3.0이 코틀린 지원을 추가하면서 이것이 가능했다. 그리고 안드로이드 스튜디오의 세번의 마이너 릴리즈로 코틀린 지원과 툴들을 계속 발전시켰다.

코틀린은 정적 타입의, 현대적인 프로그래밍 언어로서, 코틀린 코드를 자바 바이트 코드로 컴파일해서 자바 가상 머신(JVM)에서 동작한다. 그리고 또한 자바스크립트 소스코드나 네이티브 실행파일로도 컴파일 가능하다. 코틀린은 유연하며 다양한 장점을 가지고 있다.

이 안드로이드를 위한 코틀린 튜토리얼에서는 다음과 같은 내용을 다룬다.

  • 코틀린 개발 환경을 설정하는 법
  • 자바와 코틀린을 하나의 프로젝트에서 사용하는 법
  • 새로운 언어로서 코틀린이 얼마 멋진 언어인가에 대해

추신: 이 튜토리얼은 자바로 안드로이드 개발을 해온 경력자를 위해서 쓰여졌다. 안드로이드 개발에 대해서 기본적인 지식이 없는 경우에는 다른 자료를 통해서 학습하기를 권한다.

왜 코틀린인가?

안드로이드가 세상을 휩쓸어 오는 동안에도, 개발자들은 개발 언어로서 자바이외의 대체제를 찾지 못했다. 자바는 최신의 폰들이 그들의 네이티브 앱과 운영체제를 실행시키는데 사용되는 언어였다.

자바 8는 몇몇의 언어적 문제를 고쳤고, 자바 9, 자바 10에서는 훨씬 더 많이 수정되었다. 자바 8의 특장점들을 사용하기 위해서는 최소 SDK 버전을 안드로이드 24로 설정해야 한다. 안드로이드 개발 시스템의 파편화는 오래된 디바이스들을 가진 유저들을 떠나보내는 걸 힘들게 한다. 대부분의 개발자들은 자바 9과 자바 10은 엄두도 내지 못한다.

코틀린은 현대적 프로그래밍 언어로서 안드로이드 개발에 대한 솔루션이 될 수 있다. 아래와 같은 점들이 코틀린을 안드로이드 개발에 적합한 언어로 만든다.

  1. 호환성: JDK 6와 호환 가능하다, 그래서 오래된 장치들을 계속 지원 가능하다.
  2. 성능: 자바와 거의 같다.
  3. 상호 운용: 자바와 100% 상호 운영 가능하다.
  4. 용량: 코틀린의 런타임 라이브러리는 아주 작다.
  5. 컴파일 타임: 초기 빌드 과정에서는 빠르지 않지만 계속해서 축적된 빌드를 해 나가는 것은 훨씬 빠르다.
  6. 학습 난이도: 모던 언어에 익숙한 사람들에게는 코틀린을 배우기가 쉽다. IntelliJ와 안드로이드 스튜디오에 있는 자바에서 코틀린으로 변환시키는 기능은 더욱 더 접근하기 쉽도록 한다. 한 프로젝트에서 코틀린과 자바를 같이 사용할 수 있다.

최근의 안드로이드 출시 버전에서 자바 8이 지원 가능하지만, 최근의 개발자 설문에서는 코틀린이 많은 지지를 받고 있다. Realm 팀은 “코틀린이 안드로이드 개발 생태계를 완전히 바꿀 것이다.”라고 했다. 안드로이드 앱 개발을 완전 장악할 것이다. 게다가 이건 새로운 언어다. 더 이상 무엇이 더 신나겠나? iOS 개발자들은 2014년에 스위프트로 한바탕 즐겼다. 이제 우리의 차례다. 🙂

시작하면서

이 튜토리얼에서는, 하나의 샘플 앱을 이용해서 코틀린을 살펴볼 것이다. 샘플 앱은 유저가 책을 검색하고 책의 커버 페이지를 보고 그 책을 친구들과 공유할 수 있는 앱이다.

이 프로젝트는 3개의 파일에 자바로 작성되어 있다.

  • MainActivity.java: 책을 검색하고 검색된 책들의 리스트를 보여주는 액티비티
  • DetailActivity.java: 메인 액티비티에서 전달된 ID값을 가진 책의 커버를 표시하는 액티비티
  • JSONAdapter.java: JSON 오브젝트를 리스트 뷰 아이템으로 만드는 커스텀 BaseAdapter

앱을 빌드하고 실행하면 아래와 같은 화면이 나온다.

메인 화면은 책을 검색하고 검색 결과를 보여주는 곳이다. 타이틀이나 저자를 입력하고 검색 버튼을 눌러보자.

위 화면에서 책을 클릭하면 DetailActivity로 이동하고 책의 커버가 아래와 같이 나오게 된다.

개발 환경 설정하기

안드로이드 3.3이나 그 이상에서는 코틀린을 아래와 같이 격이 다르게 지원한다. 새로운 프로젝트를 만들면 코틀린이 기본으로 설정되어 있다. 여기서는 프로젝트를 생성할 필요는 없다.

안드로이드 스튜디오 3.3은 코틀린 플러그인의 새로운 버전이 있을 때마다 팝업 메시지로 알려줄 것이다. 코틀린 플러그인의 버전을 확인하기 원한다면 Ctrl + Shift + A 버튼울 누르고 Plugins을 타이핑 후 선택하자. 그럼 아래와 같은 창이 뜨고, 그 창에서 Kotlin을 타이핑해서 플러그인을 검색해 보면 아래와 같이 버전을 확인할 수 있다.

자바와 코틀린을 같이 사용하기

코틀린의 가장 뛰어난 점 하나는 자바와 같이 사용할 수 있다는 것이다. 자바 코드는 코틀린 코드를 호출할 수 있고 반대로도 당연히 가능하다. 아마 다른 언어로 작성된 코드를 호출한다는 것을 눈치채지도 못할 것이다.

여기서는, DetailActivity 클래스를 코틀린으로 변경하는 것을 해볼 것이다.

프로젝트 패널에서 패키지 이름을 클릭하자. 그런 다음 File > New > Kotlin File/Class를 선택해서 코틀린 클래스를 생성하자. 패키지를 선택하지 않으면 코틀린 파일 옵션이 활성화 되지 않을 것이다.

아래와 같이 New Kotlin File/Class 팝업창이 뜨면, 종류에서 Class를 선택하고 DetailActivityKotlin을 이름으로 입력하자.

생성한 클래스는 아래와 같을 것이다.

package com.companyname.appname

class DetailActivityKotlin {
}

몇개 짚고 넘어가자.

  1. 코틀린 클래스는 자바와 같이 class 키워드로 선언한다.
  2. 가시성 한정자를 사용하지 않다면 기본적으로 public으로 여긴다.
  3. 클래스와 메소드들은 기본적으로 final 속성이다. 만약 클래스를 상속하거나 메소드를 오버라이딩 하려면 open으로 선언한다.

코틀린과 자바는 상호 운용 가능하기 때문에, 자바 프레임워크나 라이브러리를 코틀린 코드 파일에서 사용할 수 있다.

DetailActivityKotlin를 AppCompatActivity의 서브 클래스로 선언하자.

class DetailActivityKotlin : AppCompatActivity() {
...
}

자바와는 다르게, 코틀린에서는  :부모클래스의이름() 형태로 서브 클래스의 선언부에 추가한다. 뒤에 괄호 부분은 부모 클래스의 생성자를 위해서다.

이제 아래와 같이 액티비티의 onCreate() 메소드를 오버라이딩 해보자.

class DetailActivityKotlin: AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
  }
}

※ 참고: 안드로이드 스튜디오의 코드 생성 기능을 사용해서 onCreate 메소드를 만들 수 있다. Ctrl + O 버튼을 누르면 오버라이딩 가능한 메소드들이 뜨는데 onCreate를 선택해보자.

MainActivity.java 파일을 열어서 onItemClick() 메소드 내에 DetailActivity에 대한 참조를 DetailActivityKotlin으로 변경하자.

인텐트 생성 라인은 아래와 같이 변경 되어야 한다.

Intent detailIntent = new Intent(this, DetailActivityKotlin.class);

매니페스트에 선언하기

자바 액티비티와 똑같이, 코틀린 액티비티도 AndroidManifest.xml에 선언해야만 한다. 아래 코드를 application 태그 내, DetailActivity 선언 아래 부분에다가 붙여 넣자.

<activity
    android:name=".DetailActivityKotlin"
    android:label="@string/activity_details_kotlin"
    android:parentActivityName=".MainActivity">
  <meta-data
      android:name="android.support.PARENT_ACTIVITY"
      android:value=".MainActivity"/>
</activity>

AndroidManifest, 레이아웃 파일들, 스타일, 리소스 파일들을 XML파일로 작성하는건 다를 바가 없다. 액티비티의 프로그래밍 언어가 무엇이든지 간에, 매니페스트 파일에 등록하지 않으면 앱이 에러를 발생할 것이다.

내부 동작 구현하기

위에서 생성한 코틀린 파일에 아래와 같은 내용을 붙여 넣자.

class DetailActivityKotlin : AppCompatActivity() {

  private val imgBaseUrl = "http://covers.openlibrary.org/b/id/"
  private var imgURL = ""
  private var shrActionProvider: ShareActionProvider? = null

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContentView(R.layout.activity_detail)

    actionBar?.setDisplayHomeAsUpEnabled(true)

    val imgView = findViewById<ImageView>(R.id.img_cover)

    val coverID = this.intent.extras.getString("coverID")

    val length = coverID?.length ?: 0

    if (length > 0) {
      imgURL = imgBaseUrl + coverID + "-L.jpg"
      Picasso.with(this).load(imgURL).placeholder(R.drawable.img_books_cover_loading).into(imgView)
    }
  }

  private fun setShareIntent() {

    val myShareIntent = Intent(Intent.ACTION_SEND)
    myShareIntent.type = "text/plain"
    myShareIntent.putExtra(Intent.EXTRA_SUBJECT, "책 추천")
    myShareIntent.putExtra(Intent.EXTRA_TEXT, imgURL)

    shrActionProvider?.setShareIntent(myShareIntent)
  }

  override fun onCreateOptionsMenu(menu: Menu): Boolean {

    menuInflater.inflate(R.menu.main, menu)

    val shareItem = menu.findItem(R.id.menu_share_item)

    shrActionProvider = MenuItemCompat.getActionProvider(shareItem) as ShareActionProvider

    setShareIntent()

    return true
  }
}

대략 훑어보면, 자바와 닮은 것 처럼 보인다. 하지만 코틀린 언어만의 특이점들이 많이 있다. 다음 섹션에서 이것에 대해 다뤄보겠다.

빌드해서 실행해보자, 책을 선택해서 커버가 우리가 만든 코틀린 액티비티에서 잘 나오는지 확인해보자.

변수 선언

코틀린에서 변수를 선언하는 것은 간단하고 명료하다. 코틀린에서 가능하거나 가능하지 않는것을 잘 숙지해야만 원하는 타입의 변수를 선언할 수 있다. 아래는 자주 사용할만한 변수 선언들이다.

var item = "building"
val points = 25

var car: String = "AUDI"

val과 var의 차이가 무엇일까? val로 선언한 것은 나중에 값을 다시 대입시킬 수 없다. 하지만 var로는 가능하다.

points = 26

points는 선언할 때 이미 25값을 대입했다. 이건 자바의 final과 비슷한 타입이다.

위에 변수 선언한 부분에서 또 자바와 다른 점은 변수 타입이다. 코틀린의 변수 타입 추론은 굉장히 뛰어나다. 그래서 대부분의 경우 변수의 타입을 명확하게 표시할 필요는 없다. null 같은 비필수적인 변수를 선언할 때 명확하게 표시하는게 좋다. 위에 car 변수 같은 경우, 변수 타입을 쓸 필요는 없다.

var highestScore = points + 999

위의 코드 같은 경우에는 코틀린 컴파일러가 highestScore를 int 타입의 변수라는 걸 추론해낸다.

Null 안전성

일반적으로 프로그래밍 언어들에서 좌절을 주는 것은 null 참조를 가진 멤버를 접근하는 것이다. null 참조는 변수를 선언 했지만 값을 할당하지 않았거나, null을 할당할 때 일어난다. 프로그램이 실행되고 그 변수에 접근하려 할 때, 안에 값이 없기 때문에 어디를 참조할지 모르게 된다.

대부분의 경우에 어플리케이션은 급작스런 실행 중지, 에러가 발생하게 된다. 아마 자바의 악명 높은 NullPointerException 에러와 아주 친숙할 것이다.

코틀린의 가장 뛰어난 점 하나는 NullPointerException을 없애는 타입 시스템이다.

코틀린에서 NullPointerException 을 일으키는 원인들은 다음과 같다.

  • 외부 자바 코드
  • throw NullPointerException()을 호출
  • !! 오퍼레이터의 사용
  • 액세스 전에 초기화 되지 않은 lateinit var

Nullable 타입들과 Non-Null 타입들

코틀린은 nullable 타입들과 non-null 타입들을 가지고 있다. 변수를 nullable로 선언하지 않는다면 null 값을 할당할 수 없다. 컴파일러가 이 룰을 강력하게 적용한다. 그래야 의도치 않은 에러를 방지할 수 있기 때문이다.

자바와는 다르게, 모든 변수들은 lateinit var를 제외하고는 선언과 동시에 생성자든 init안에서든 초기화가 되어야 한다.

변수를 nullable로 선언하려면, 선언 부분에서 ? 마크를 붙여야만 한다. 위 소스코드 shareActionProvider 부분에서 볼 수 있다.

private var shrActionProvider: ShareActionProvider? = null

나중에 초기화 해도 되는 변수

나중에 초기화 해도 되는 변수 lateinit은 개발자가 그 변수를 액세스 하기 전까지 나중에 초기화 하겠다는 뜻을 가지고 있다. 이건 Null Pointer Exception의 가장 흔한 원인이다. 생성자나 init 블록에서 초기화 하지 않고 변수를 선언하려면 lateinit 키워드를 붙이자.

public class Test {

  lateinit var mock: Mock

  fun setup() {
     mock = Mock()
  }

  fun test() {
     mock.do()
  }
}

만약 test()를 먼저 호출한다면, 앱은 에러를 낼 것이다. mock 변수를 접근하기 전에 초기화하겠다고 선언했지만 그렇게 하지 않았기 때문이다. 반대로 setup()을 먼저 호출하면 당연히 에러가 나지 않을 것이다.

이건 초기화를 분명히 할 거긴 한데 생성자나 init블록에서는 하기가 힘든 경우에 매우 유용하다. 안드로이드 개발에서, 뷰들은 onCreate()에서 할당된다. 만약 뷰를 참조하는 변수를 클래스 멤버로 갖고 있다면 lateinit 으로 선언하는게 바람직하다.

안전한 호출

자바에서 nullable 변수를 접근할 때, null 체크를 아마 할 것이다. DetailActivity.java 예를 들어 보겠다.

if (shareActionProvider != null) {
  shareActionProvider.setShareIntent(shareIntent);
}

코틀린에서는 안전 호출 명령어 “?”을 사용해서 위와 똑같은 표현을 간단하게 할 수 있다. 변수나 메소드들은 nullable 변수들이 null 아닐 때만 호출된다.

shareActionProvider?.setShareIntent(shareIntent)

여기선 shareActionProvider가 null이 아닐 때만 setShareIntent 메소드를 호출한다.

“!!” 명령어

앞에서 언급한 것과 같이, 이 명령어는 그 무서운 NullPointerException의 원인 중에 하나이다. nullable 참조가 null이 아니라고 확신할 때, !! 명령어를 객체를 역참조하기 위해서 사용하자.

예시를 setShareIntent()에서 볼 수 있다.

shareActionProvider = MenuItemCompat.getActionProvider(shareItem!!) as ShareActionProvider

여기선, shareItem이 null이라면 NullPointerException이 발생할 것이다.

엘비스(?:) 명령어

엘비스 명령어는 자바에서 삼항 조건 연산자(ternary conditional operator)처럼 보이지만 완전 다른 것이다. 엘비스라고 불리는 이유는 엘비스 프레슬리의 헤어 스타일 같아 보이기 때문이다. 위의 예시에서 coverID 값의 길이를 가져오려고 하는 걸 볼 수 있다.

val length = coverID?.length ?: 0

만약 엘비스 명령어의 왼쪽 부분이 null이 아니라면, 왼쪽 부분의 실행 결과가 리턴된다. 만약 null이라면, 오른쪽 부분의 실행 결과가 리턴된다.

if-else 구문과 비슷하게, 엘비스 명령어도 왼쪽 부분이 null일 때만, 오른쪽 부분을 실행한다. 위의 엘비스 구문은 아래와 똑같다.

val length : Int
if(coverID?.length != null) {
  length = coverID?.length
} else {
  length = 0
}

엘비스 명령어를 쓰는게 훨씬 간단하다.

타입 추론

코틀린은 또한 타입 추론을 지원한다. 이건 초기화하는 코드에서 변수 타입을 추측할 수 있다는 의미다. 물론 타입을 명확히 선언하기 원하거나 컴파일러가 추론을 할 수 없을 때는 타입을 선언할 해야한다. 위에 소스코드에서 예를 든다면, imgBaseUrl와 imgURL을 초기화하는 구문만으로 변수 타입을 추론 가능하다.

private val imgBaseUrl = "http://covers.openlibrary.org/b/id/"
private var imgURL = ""

컴파일러는 String으로 추론된 각 변수들의 타입을 추적해간다. 앞으로도 이 변수들에 할당되는 값들은 String 타입이어야 한다. 숫자는 컴파일러가 명확히 추론할 수 있도록 하는게 좋다.

var longVariable = 7L
var floatVariable = 7.7f
var doubleVariable = 7.7
var integerVariable = 7

자바에서 코틀린으로 변환시키는 컨버터

이 자바에서 코틀린으로 소스코드를 변환시켜주는 컨버터는 한 프로그래밍 언어에서 다른 언어로 옮겨가는 것을 쉽게 만들어주는 최고의 툴이다.

코틀린은 개발자에 의해서 개발자를 위해 만들어졌기 때문에, 개발자들이 무엇을 원하는지 안다. 이런 툴들은 언어 개발 팀이 자바에서 코틀린으로 변경할 때 사용했던 것이다. 그래서 이게 이렇게 뛰어난 것이다.

한번 시도해보자. 이 사람을 구할 수도 있는 기능을 사용해서 DetailActivity.java를 코틀린으로 변경해보자.

DetailActivity.java 파일을 열고 “Code > Convert Java File to Kotlin File”을 선택하자. 아니면 단축키 “Ctrl + Alt + Shift + K”를 사용해도 좋다.

변환이 잘 마무리 되었다면 DetailActivity와 DetailActivityKotlin 클래스를 한번 비교해보자. 컨버터가 결정한 것들을 살펴보자. 항상 최고의 결정을 하는 건 아니지만, 사용할만한 소스코드를 만들기는 한다. 이 기능을 사용할 때는 항상 변환된 코드를 리뷰하도록 하자. 컨버터로부터 코틀린 언어 특징에 대해서 배우게 될 것이다.

이제, 메인 액티비티에서 디테일 액티비티로 넘어가는 부분을 변경해보자.

Intent detailIntent = new Intent(this, DetailActivityKotlin.class);

빌드하고 실행한 후 디테일 액티비티 화면으로 가보자. 변환된 코드가 아주 잘 실행되는걸 확인할 수 있을 것이다.

코틀린은 진화하고 있다

다른 유명한 프로그래밍 언어들은 이미 성숙기에 접어들었지만, 코틀린은 아직 유아기에 머물러 있다. 지금 최신의 버전은 1.3이다 그리고 새로운 특성들이 계속 추가되고 있다. 몇몇의 특성들은 이미 실험적인 것들로 코틀린에 있는 것이지만, 몇몇은 완전 새로운 것들이다. 또한 몇몇은 이미 존재하는 전체 구조에 큰 향상을 가져왔다. 아래는 알아두면 좋을만한 것을 요약한 내용이다.

Coroutines

코루틴은 개발자들이 비동기화 코드를 실행하는걸 허용하는 코틀린의 고급 기능이다. 아직 코루틴에 대해서는 모르겠지만, 이 기능이 안정화 되었다는 사실을 알 필요가 있다. non-blocking 코드를 실행해야만 한다면, 코루틴에 대해서 시간을 내서 공부해 보기 바란다.

when 표현의 개선

자바의 switch에 해당되는 것이 코틀린의 when이다. 코틀린 1.3에서는, 비교 대상은 즉시 포착된다. 더 이상 구문을 시작하기 전에 변수에 값을 할당할 필요가 없다.

when (val response = executeRequest()) {
   is Success -> response.body
   is HttpError -> throw HttpException(response.status)
}

더 많은 숫자 변수들

어떤 어플리케이션들에게는, 부호가 없는 숫자 타입을 사용하는게 적당하다. 부호를 제외시키는건 새로운 가능성을 추가할 수 있다. 코틀린 1.3은 예전의 부호가 없는(unsigned) 컨셉을 다시 가져왔다: UByte, UShort, UInt, ULong는 새로운 변수들이다.

IDE에서의 코드 스타일 지원

기억나는 자바 코딩 스타일이 있는가? 나도 없다. 코틀린은 코틀린 개발자라면 꼭 공부하고 따라야할 매우 구체적인 코딩 스타일에 대한 문서가 있다. 그래서 안드로이드 스튜디오를 코틀린스럽게 코딩하기 위해서 셋팅을 좀 해야한다. 이것이 소스코드를 잘 작성하고 코틀린의 권고를 잘 지키면서 코딩할 수 있게 해준다.

마치면서…

축하한다! 코틀린에 대해서 기초 공부가 끝났다! 자바 액티비티를 코틀린으로 다시 작성했고 코틀린 컨버터 플러그인을 이용해서 자바 소스코드를 코틀린 소스코드로 변경하기도 했다.

하지만 이 튜토리얼은 엄청나게 뛰어난 코틀린의 아주 일부분만 다뤘다. Data Classes, Extensions, Lambdas, String Templates 등등 아직 공부해야할 것이 아주 많다.

이 튜토리얼로 코틀린을 조금이라도 이해했길 바란다. 질문이나 커멘트는 아래에 남겨주기 바란다!

답글 남기기