해설판: Hey Kotlin, how it works? [기초편]

Chang W. Doh
TIL: Kotlin in practice (한국어)
10 min readNov 23, 2017

이전에 GDG DevFest Seoul 2017에서 발표했던 슬라이드를 공유하는 것만으로는 처음 접하시는 분들에게 설명이 조금 부족할 것 같아 해설판을 추가합니다. :)\

Caution: 이 자료는 생성된 Bytecode를 기반으로 디컴파일된 코드를 살펴보고 코틀린의 코드 생성 목적이나 언어 설계의 원인(혹은 painpoint)를 찾아보는 과정의 일부입니다. 이 글을 보고 Kotlin을 그저 Java의 wrapper인 것으로 오해하지는 않기를 바랍니다.

들어가기 전에: Kotlin Bytecode Inspector

많은 분들이 코틀린의 동작 형태에 대해 궁금하시리라 생각합니다. 레퍼런스라던지 여러가지 문서와 책을 읽는 과정은 분명히 필요하리라 생각되지만, 모르는 분들을 위해 한가지를 더 설명드리고자 합니다.

Kotlin Bytecode Inspector의 decompile을 이용한 자바 코드 확인

Search Everywhere(Shift > Shift) 를 누르고 Show Kotlin Bytecode 를 입력하면 코틀린 코드에 대한 바이트코드를 볼 수 있습니다. Decompile을 선택하면 쉽게 동등한 자바 코드를 볼 수 있는데 이미 자바에 익숙하신 분들은 이것이 코틀린 코드를 이해하는데 도움을 줄 수 있습니다.

Basic features

아주 기본적인 내용에 대해서 빠르게 훑어 보도록 하겠습니다.

위 코드는 매우 간단한 코틀린 클래스입니다만, 유사한 문법을 가진 몇몇 언어를 해보지 않은 분들에게는 생소하게 보이는 부분들이 있을 겁니다. 살펴보도록 하죠.

val vs var

val은 값(value)를 의미하고, var는 변수(variable)을 의미합니다. 복잡하지 않습니다. 값은 그 자체로 초기에 가진 값이 변하지 않는 불변성(immutable)을 가지고 있습니다. 반대로 변수는 가진 값이 언제든지 변할 수 있습니다.

기본적으로 valvar로 정의된 변수나 프로퍼티는 언어 수준에서 제한점을 가지게 됩니다만, 이를 가장 쉽게 확인할 수 있는 것은 역시 프로퍼티를 통해 확인하는 것입니다.

property

프로퍼티는 객체의 일부로써 어떠한 값을 쓰거나 읽을 수 있는 인터페이스를 제공하는 역할을 합니다. 이때 보통 읽는 동작은 getter라고 하고, 쓰는 동작은 setter라고 합니다. getter/setter가 중요한 점은 값이 설정하거나 읽을 때 객체 내의 다른 멤버나 어떠한 계산 과정이 관여할 수 있다는 점입니다.

앞에서 말한 바와 같이 읽기/쓰기 속성을 모두 가지는 var를 통해 프로퍼티를 선언은 위와 같은 자바 코드로부터 다음과 같은 내용을 확인할 수 있습니다.

  • 선언된 프로퍼티의 필드는 private를 통해 외부 접근으로부터 보호된다.
  • 해당 필드에 대한 접근은 생성된 getter/setter를 통해서 이루어진다.

val의 경우는 어떨까요?

비슷하죠? 다만, 읽기 전용의 속성을 가진 이상 setter는 배제되고 getter만이 생성되었습니다.

local variable

그렇다면 변수의 경우는 어떨까요?

gettersetter의 흔적은 확인할 수 없습니다. 우리가 일반적으로 선언하던 방식 그대로 코드가 생성됩니다. 그렇다면 로컬 변수의 경우 자유롭게 접근이 가능할까요? 그렇지 않습니다. 언어 수준에서 이미 값과 변수의 차이가 처리되므로, val로 선언된 변수에 대해서 배정문을 수행하는 것은 허가되지 않습니다.

Nullable vs NotNull

Kotlin이 Null-safe하다라는 표현은 익히 들어보셨을 것입니다. null은 굉장히 오래된 발명품이고, 발명가조차 실수라고 표현하는 우리에게는 가끔(?) 큰 괴로움을 주는 존재입니다. 이를 해결하기 위해 모던 프로그래밍 언어들은 Nullable과 NotNull을 구분하는 방식을 도입하는 경우들이 많아졌고, Kotlin 역시 그 중의 하나입니다. 코드를 보죠.

일반적인 방식으로 타입을 선언하면, 기본적으로 NotNull로 처리됩니다. NotNull의 대상이 프로퍼티라면 setter 내에 필드의 null 여부를 확인하고 NPE(Null Pointer Exeception)를 발생하는 코드를 확인할 수 있습니다.

NotNull의 NPE 처리는 함수의 파라메터에서도 마찬가지입니다.

어떤 분들은 이러한 부분에서 오버헤드가 발생하는 것은 아닌지 의문을 가지실 겁니다. 그렇습니다. 그러나, 약간의 오버헤드는 있겠지만 Null 체크는 개발단계에서 그 이상으로 많은 효과를 가져다 줍니다.

그리고, 생각보다 코틀린이 생성하는 코드는 문맥에 잘 최적화되는 편입니다.

String template

코드에서 빠질 수 없는 것이 바로 문자열입니다. 문자열은 디버깅을 위한 로그부터 사용자에게 노출할 적당한 문구를 생성하는 등 주로 사용되는 기능들 중의 하나입니다만, 문자열을 만드는 것은 때때로 지루한 작업이 될 수 있습니다.

Kotlin은 이를 해결하기 위해 String template를 도입하였습니다. $필드명/변수명 혹은 ${표현식} 형태로 기술되는 String interpolation을 통해 우리가 원하는 값이 대입된 문자열을 얻을 수 있습니다.

이 코드를 디컴파일해보면 다음과 같습니다.

디컴파일된 코드에서 보면 String concatenation을 사용하는 코드는 절마다 새로운 문자열을 생성하는 것 아닌가 하는 의문이 있을 수 있습니다. 실제 바이트 코드로 변환하여 확인하면 아래와 같이 StringBuilder를 이용하도록 되어 있으므로, 안심하고 사용하셔도 좋습니다.

Inheritance

다른 언어들과는 달리 Kotlin의 클래스는 기본적으로 더 이상 상속될 수 없도록 제한을 받고 있습니다.

위의 코드에서 보듯이 생성된 코드에서 final이 기본으로 들어가기 때문에 미리 확장을 예정하고 openabstract로 이를 설정하지 않으면 상속은 불가능합니다.

아마 Kotlin 언어 설계자들은 의도되지 않은 클래스 상속으로 인한 문제를 줄이고 싶지 않았을까 추측해봅니다.

클래스의 상속 대신 여러 기능 단위를 조합해서 사용하는 패턴이 더 나을 수도 있으며 Delegation은 이러한 구현 패턴에 매우 훌륭합니다. 궁금하신 분은 이 글 마지막의 링크를 참조하시기 바랍니다.

Constructor, Initializer, Property declaration

코틀린에서 클래스의 초기화를 담당하는 부분은 크게 3가지로 나누어 볼 수 있습니다.

  • 클래스 생성자(Constructor)
  • 초기화 블럭(Initializer)
  • 프로퍼티 선언(Property declaration)

Constructor

생성자(Constructor)를 살펴보도록 하겠습니다.

primary constructor에서 접근수정자(modifier) 등을 선언하지 않는다면 constructor는 생략 가능합니다.

생성자에서 val이나 var을 통해 인자를 선언하지 않으면 생성자 함수에 전달되는 값으로만 처리됩니다. 그러나, 생성자에서 val이나 var을 통해 선언된 인자는 다음 코드와 같이 프로퍼티로써 처리됩니다.

Initializer

초기화 블럭(Initializer)은 전달 인자에 직접 의존하지 않는 공통적인 초기화 코드를 다룰 수 있습니다.

생성된 바이트코드를 디컴파일해보면 실제로 초기화 블럭의 코드가 생성자에 위치한 것을 확인할 수 있습니다.

Constructor delegation

또한, 생성자를 정의할 때 같이 호출되어야 할 생성자를 지정할 수 있습니다. 이를 생성자 위임(Constructor delegation)이라고 하며, 생성자보다 먼저 호출됩니다.

객체 초기화의 순서

Kotlin에서 객체가 초기화될 때 다음과 같은 순서로 초기화 과정이 일어납니다.

  • 생성자 인자(Constructor argument)
  • 위임된 생성자의 호출(Constructor delegation call)
  • 프로퍼티의 선언(Property declarations)
  • 초기화 블럭(Initializer)
  • 생성자 바디(Constructor body)

곰곰히 생각해보면 이는 매우 당연한 순서입니다만, 혼동이 일어날 수 있으니 다음 코드의 실행 과정을 기억하시기 바랍니다.

An in-depth look at Kotlin’s initializers 에서 좀 더 자세한 내용을 참조할 수 있습니다.

Custom getter/setter

우리는 앞에서 Kotlin의 프로퍼티 선언은 자동화된 getter/setter를 포함하고 있음을 확인하였습니다. 더불어 우리는 값의 접근에 직접 관여하는 방법을 사용할 수 있습니다.

Custom getter

정의된 get()에 의해 프로퍼티를 직접 선언할 수 있습니다. 프로퍼티에서 실제 값을 저장하는 필드가 반드시 1:1 관계일 필요는 없으므로, 위와 같이 다른 필드를 참조하고 계산만을 수행하는 필드를 정의하는 것도 가능합니다.

Custom setter의 작성 시 주의할 점

반대로 set()에 의해 값을 설정하는 것 역시 일반 함수와 같은 방식으로 가능합니다만, 반드시 주의해야 할 점이 있습니다. 다음 코드를 보도록 하겠습니다.

재귀 호출이 일어납니다.

위 코드에 의해 age에 값을 저장하는 배정문은 setAge()를 호출하게 됩니다. 그런데, age에 구현된 setter가 다시 age에 대해 배정문을 정의하였으므로, setter 자신을 다시 부르는 재귀 호출을 발생하게 합니다. (당연히 결과물은 스택 오버플로일 것입니다.)

그렇다면 프로퍼티 자신의 값을 나타내는 방법은 어떤게 있을까요? 바로 field를 사용하는 것입니다. this와 마찬가지로 field는 문맥에 따라 적절한 프로퍼티 자신을 나타냅니다.

setter의 작성 시 습관적으로 field를 사용하는 습관을 들이시기 바랍니다.

맺음

이번 글은 Kotlin 세션에서 기초적인 문법들과 관련된 부분들을 살펴보았습니다. 혹시 클래스와 프로퍼티에 대한 위임(Delegation)이 어떻게 구현되었는지 궁금하시다면 다음 글을 참조하시기 바랍니다. :)

슬라이드는 아래에서 확인하실 수 있습니다.

--

--

Chang W. Doh
TIL: Kotlin in practice (한국어)

I’m doing something looks like development. Community diver. ex-Google Developer Expert for Web 😎