본문 바로가기

Kotlin/Play Kotlin Example

Kotlin 공식 Examples로 공부하기 - Introduction (공식 튜토리얼 읽기)

Introduction

  • 저는 전문 번역가도 아니고, 의역을 넘어 오역, 심지어 그냥 제가 읽고 싶은대로 읽은 내용이 있을 수 있습니다.
  • 개인 공부를 한 것을 포스트로 남기고 있으며 틀린 부분이 있으면 지적해주시면 수정하도록 하겠습니다.
  • 원문 : https://play.kotlinlang.org/byExample/01_introduction/01_Hello%20world

 


Hello World

package org.kotilnlang.play // 패키지

fun main() {                // 애플리케이션 시작점인 main 함수 선언
    println("Hello, World!")  // 표준 출력
}

코틀린 코드는 보통 패키지 안에 정의됩니다.

패키지를 지정하는 것은 옵션입니다. (소스 파일(.kt)안에 패키지를 지정하지 않는다면, 소스 파일내의 내용은 디폴트 패키지(default package)로 지정됩니다.)

코틀린 애플리케이션의 시작점은 main 함수 입니다.

코틀린 1.3버전에서는 main 함수를 파라미터 없이 선언할 수 있습니다.

코틀린 1.3버전 이전 버전에서는 아래 코드와 같이 main 함수에 반드시 파라미터(Array<String>)를 지정해야합니다.

fun main(args: Array<String>) {
    println("Hello World!")
}

main 함수의 리턴 값은 지정하지 않았습니다.

이것은 main 함수가 리턴 값이 없다는 것을 의미합니다.

println 은 문자열 한 줄에 출력하는 표준 출력 함수고 암묵적으로 임포트(import) 되어있다.

그리고 ; (세미콜론)도 생략되어 있는데 옵션이다. (써도 되고 안 써도 되지만 요즘 트렌드는 안 쓰는 편)


Functions

디폴트 파라미터(Default Parameter)와 지정 인자(Named Arguments)

fun printMessage(message: String): Unit {                               // 1
    println(message)
}

fun printMessageWithPrefix(message: String, prefix: String = "Info") {  // 2
    println("[$prefix] $message") //쌍따옴표로 묶인 문자열 내부에 $(달러표시)를 이용하여 변수 사용이 가능하다.
}

fun sum(x: Int, y: Int): Int {                                          // 3
    return x + y
}

fun multiply(x: Int, y: Int) = x * y                                    // 4

fun main() {
    printMessage("Hello")                                               // 5                    
    printMessageWithPrefix("Hello", "Log")                              // 6
    printMessageWithPrefix("Hello")                                     // 7
    printMessageWithPrefix(prefix = "Log", message = "Hello")           // 8
    println(sum(1, 2))                                                  // 9
}
  1. 가장 단순한 형태의 함수, String 타입의 값(message)를 받아 Unit 타입을 리턴하는 함수
    • Unit 타입은 리턴 값이 없다는 의미다.
  2. 이 함수는 2번 째 파라미터(prefix)가 옵션으로 적용된다. 디폴트 파라미터(Default Parameter)로 prefix의 값은 "info"로 적용되어 있다. 함수 선언 부에 리턴값이 생략되어 있는데 생략되어 있으면 실질적으로 Unit 이 리턴되는 것을 의미한다.
  3. integer 타입이 리턴된다. : (콜론)뒤에 오는 값으로 리턴 타입을 자유롭게 정할 수 있다. (ex. : Boolean, : String, ...)
  4. 중괄호로 표현하지 않고 표현식(Expression)으로도 함수를 정의할 수 있다.
    • Integer 타입 x, y값을 곱하였을 때 값을 리턴하는데 타입은 Integer라고 코틀린이 추론해준다.
  5. 1번 함수를 "Hello"라는 문자열을 인자로 호출
  6. 2번 함수를 2개의 파라미터에 맞는 2개의 인자를 주고 호출
  7. 2번 함수를 2번째 파라미터의 인자를 생략하고 호출 (Default Parameter의 값으로 "Info"가 지정되어 있기 때문에 "Info"가 사용된다.)
  8. 2번 함수를 지정 인자(Named Arguments)로 호출한다.
    • 인자를 파라미터 순서를 바꿔 호출했고(순서대로 하지 않았고), 구체적으로 이 인자가 어떤 파라미터와 대응하는지 표현했다.
  9. println 함수를 호출하는데 sum함수의 결과 값을 출력한다.

중위 함수(infix functions)

중위 함수는 infix 키워드를 사용하여 함수 호출에서 .() 을 생략한 표현식으로 호출 가능한 함수다.

  • 중위 함수 요구 사항
    • 반드시 클래스의 멤버 함수이거나 확장 함수여야한다.
    • 반드시 함수의 파라미터가 1개여야 한다.
    • 파라미터가 가변 인자여도 안되고 디폴트 값(Default value)을 가져서도 안된다.
fun main() {

  infix fun Int.times(str: String) = str.repeat(this)        // 1
  println(2 times "Bye ")                                    // 2

  val pair = "Ferrari" to "Katrina"                          // 3
  println(pair)

  infix fun String.onto(other: String) = Pair(this, other)   // 4
  val myPair = "McLaren" onto "Lucas"
  println(myPair)

  val sophia = Person("Sophia")
  val claudia = Person("Claudia")
  sophia likes claudia                                       // 5
}

class Person(val name: String) {
  val likedPeople = mutableListOf<Person>()
  infix fun likes(other: Person) { likedPeople.add(other) }  // 6
}
  1. Int 클래스의 중위 확장 함수(infix extendsion function) 'times'를 정의한다.
  2. 중위 확장 함수를 호출한다.
  3. 표준 라이브러리에 있는 to 중위 함수를 불러서 Pair 클래스의 객체를 생성한다.
    • to 함수는 코틀린이 제공하는 표준 라이브러리의 함수로써 <key, value>형식의 Pair 값을 리턴하는 함수다.
  4. onto 라는 중위 함수를 정의하고, 호출해서 to 함수를 구현한 나만의 중위 함수를 만들 수 있습니다.
  5. 중위 함수는 멤버 함수(메소드)로도 정의할 수 있습니다.
    • Person 클래스의 메소드에 infix 키워드를 사용하여 중위 함수를 만든 것을 확인할 수 있습니다.
  6. 멤버 함수(메소드)로 중위 함수가 정의되었다면 중위 함수의 첫 번째 파라미터는 클래스 인스턴스가 됩니다.

연산자 함수(operator functions)

함수를 연산자로 업그레이드 할 수 있습니다. 연산자로 해당 함수를 호출할 수 있습니다.

(연산자 오버로딩 개념이라고 보면 될 듯 합니다.)

operator fun Int.times(str: String) = str.repeat(this)       // 1
println(2 * "Bye ")                                          // 2

operator fun String.get(range: IntRange) = substring(range)  // 3
val str = "Always forgive your enemies; nothing annoys them so much."
println(str[0..14])                                          // 4
  1. operator 변경자(키워드)를 이용하여 한 단계 높은 중위 함수(infix function)를 구현합니다.
  2. times()* (곱셈 연산자)를 의미합니다. 그래서 2 * "Bye " 를 이용하여 위에서 정의한 함수를 호출할 수 있습니다.
    • + 연산자 → .plus
    • - 연산자 → .minus
    • * 연산자 → .times
    • / 연산자 → .div
    • % 연산자 → .rem
    • ... (수 많은 연산자들에 대응하는 함수명이 있다...)
  3. 연산자 함수는 문자열에서 쉽게 범위 접근을 할 수 있다.
    • get/set 접근자에도 operator 키워드를 이용하여 연산자 함수를 정의한 것이다.
  4. get() 함수는 bracket-access 문법을 사용할 수 있다.

가변 인자를 갖는 함수(vararg)

가변 인자를 사용하면 콤마(,)로 구분되는 여러 인자를 전달할 수 있다.

fun printAll(vararg messages: String) {                            // 1
    for (m in messages) println(m)
}
printAll("Hello", "Hallo", "Salut", "Hola", "你好")                 // 2

fun printAllWithPrefix(vararg messages: String, prefix: String) {  // 3
    for (m in messages) println(prefix + m)
}
printAllWithPrefix(
    "Hello", "Hallo", "Salut", "Hola", "你好",
    prefix = "Greeting: "                                          // 4
)

fun log(vararg entries: String) {
    printAll(*entries)                                             // 5
}
  1. vararg 변경자(키워드)는 파라미터를 가변인자로 바꿉니다.
  2. vararg 키워드가 적용된 printAll 함수를 여러 문자열 인자를 이용하여 호출합니다.
  3. 지정 인자(Named Parameters) 덕분에 vararg 인자 뒤에 동일한 타입의 파라미터를 추가할 수 있습니다. 값을 전달할 방법이 없기 때문에 자바에서는 불가능한 기능입니다.
  4. 지정 인자(Named Parameters)를 이용하여 vararg 와 별도로 prefix 값을 설정할 수 있습니다.
  5. 런타임에서 vararg 는 단지 array 입니다. vararg 인자를 호출 인자로 넘기려면, * (spread operator)를 사용하면 됩니다. (Array타입인 entries 대신 *(spread operator)를 이용하여 전달)

Variables

코틀린은 강력한 타입 추론 기능을 가졌습니다. 타입을 명시적으로 선언할 수 있지만 대부분의 경우 컴파일러가 추론을 할 수 있습니다. 코틀린 변수에서 불변성(immutability) 강제하진 않지만 추천됩니다. 꼭 var 를 써야하는 경우가 아니면 var (variable)보다는 val (value)을 사용하는게 좋습니다.

var a: String = "initial"  // var 키워드로 선언한 변수로 mutable 변수고 선언과 함께 초기화도 했습니다.
println(a)
val b: Int = 1             // val 키워드로 선언한 변수로 immutable 변수고 선언과 함께 초기화도 했습니다.
val c = 3                  // immutable 변수고 선언과 함께 초기화도 했습니다. 또한 타입을 명시적으로 적지 않았지만 코틀린 컴파일러가 Int로 추론해줍니다.

var e: Int  // 초기화 없이 var 키워드로 선언했습니다.
println(e)  // 위에서 선언한 변수를 사용하려고 하면 컴파일러는 에러가 발생합니다. "Variable 'e' must be initialzed."

변수 초기화를 언제할지는 자유롭게 선택할 수 있지만, 처음 읽기(변수 사용)전에는 반드시 초기화를 해야합니다.

val d: Int  // 초기화 없이 val(immutable)로 변수 선언

if (someCondition()) { //someCondition에 따라 d 변수는 다른 값으로 초기화
    d = 1
} else {
    d = 2
}

println(d) // d는 이미 초기화되었기 때문에 변수를 읽는게 가능

Null Safety

NullPointerException 세계에서 벗어나기 위해 코틀린에서 변수 타입에 null 을 할당하는 것을 허용하지 않습니다.

만약 null일 수 있는 변수가 필요한 경우, 타입 선언부 끝에 ? 를 붙임으로써 null이 할당 가능한 변수를 선언하면 됩니다.

var neverNull: String = "This can't be null"            // non-null 문자열 변수 선언
neverNull = null                                        // non-null 변수에 null을 할당하려는 순간 컴파일 에러 발생

var nullable: String? = "You can keep a null here"      // nullable 문자열 변수 선언
nullable = null                                         // nullable 변수에 null을 할당하는 건 가능

var inferredNonNull = "The compiler assumes non-null"   // 코틀린 컴파일러가 타입을 추론하는 경우, 초기화된 변수는 non-null로 가정합니다.

inferredNonNull = null                                  // non-null로 가정했기 때문에 null을 할당하려는 순간 컴파일 에러 발생

fun strLength(notNull: String): Int {                   // non-null 문자열 파라미터를 갖는 함수 선언
    return notNull.length
}

strLength(neverNull)                                    // non-null 문자열을 인자로 함수 호출 -> 잘 됨
strLength(nullable)                                     // nullable 문자열을 인자로 함수 호출 -> 컴파일 에러

Null 다루기(Working with Nulls)

때때로 코틀린 프로그램은 null 값을 쓰는게 필요합니다. 예를들어 외부의 자바 코드와 상호작용하거나 진짜로 null값 그 자체(상태)를 나타내야하는 경우입니다.

코틀린은 이런 상황을 우아하게 다루기 위해 null tracking 을 제공합니다.

fun describeString(maybeString: String?): String {         // nullable 문자열을 파라미터로 받아 문자열을 리턴하는 함수
    if (maybeString != null && maybeString.length > 0) {   // null이 아닌 문자열이라면, 길이를 리턴
        return "String of length ${maybeString.length}"
    } else {
        return "Empty or null string"                      // 아니라면 null이거나 빈문자열이라고 리턴
    }
}

뭐가 우아하지? 하고 생각할 수 있는데 if문에서 null체크를 진행했기 때문에 if 블록{}안에서 자연스럽게 null이 아닌 객체(maybeString)라고 판단하고 .length를 호출해도 에러를 내지 않았기 때문에 우아한 것입니다.


Classes

클래스의 선언부는 클래스 이름(name)과 클래스 헤더(header = 파라미터 타입, 기본생성자, ...), 바디(body = 중괄호로 둘러싼 부분)로 구성됩니다.

헤더와 바디는 모두 옵션입니다. 만약 클래스 바디 부분이 없으면 중괄호를 생략할 수 있습니다.

class Customer                                  // 1

class Contact(val id: Int, var email: String)   // 2

fun main() {

    val customer = Customer()                   // 3

    val contact = Contact(1, "mary@gmail.com")  // 4

    println(contact.id)                         // 5
    contact.email = "jane@gmail.com"            // 6
}
  1. Customer 라는 클래스를 어떤 프로퍼티(멤버변수)나 유저가 정의한 생성자없이 선언한다. 파라미터가 없는 기본 생성자는 코틀린에 의해 자동으로 생성됩니다.
  2. 2개의 프로퍼티(불변타입(val)의 id 와 가변타입(var)의 email)와 그 둘을 파라미터로하는 생성자를 갖는 Contact 라는 클래스를 선언한다.
  3. 디폴트 생성자(default constructor)를 통해 Customer 인스턴스를 생성합니다. 코틀린에서는 new 키워를 쓰지 않습니다.
  4. 두 개의 인자를 받는 생성자를 통해 Contact 인스턴스를 생성합니다.
  5. id 프로퍼티에 접근합니다.
  6. email 프로퍼티의 값을 수정합니다.

Generics

Generics 은 모던 프로그래밍 언어에서 표준이 되는 제네릭 메커니즘입니다.

제네릭 클래스와 제네릭 함수는 특정 제네릭 타입과는 독립적인 공통 로직을 캡슐화하여 코드의 재사용성을 증가시킵니다. (List<T> 에서 로직은 제네릭 타입 T 와는 무관한 것 처럼)

Generic Class

코틀린에서 제네릭을 사용하는 첫 번째 방법은 제네릭 클래스를 생성하는 것입니다.

class MutableStack<E>(vararg items: E) {              // 1

  private val elements = items.toMutableList()

  fun push(element: E) = elements.add(element)        // 2

  fun peek(): E = elements.last()                     // 3

  fun pop(): E = elements.removeAt(elements.size - 1)

  fun isEmpty() = elements.isEmpty()

  fun size() = elements.size

  override fun toString() = "MutableStack(${elements.joinToString()})"
}
  1. MutableStack<E> 라는 제네릭 클래스를 만들었습니다. 앞에서 E 는 '제네릭 타입 파라미터'라고 불립니다. 실제 사용할 때는 MutableStack<Int> 처럼 Int 와 같은 특정 타입을 지정해서 사용합니다.
  2. 제네릭 클래스 내부에서 E 는 어떤 다른 타입의 파라미터로 사용될 수 있습니다.
  3. 리턴타입으로 E 를 사용할 수도 있습니다.

구현시 단일 표현식으로 구현될 수 있는 함수에 코틀린의 축약 문법을 많이 사용하게 됩니다.

Generic functions

로직이 특정 타입에 독립적이라면 제네릭 함수를 쓸 수 있습니다. 예를들어 다음과 같이 가변(mutable) 스택을 만드는 유틸리티 함수를 만들 수 있습니다.

fun <E> mutableStackOf(vararg elements: E) = MutableStack(*elements)

fun main() {
  val stack = mutableStackOf(0.62, 3.14, 2.7)
  println(stack)
}

컴파일러는 mutableStackOf 메소드의 파라미터를 통해서 타입을 유추할 수 있습니다. 그렇기 때문에 mutableStackOf<Double>(...) 같은 메소드를 작성할 필요가 없습니다.


Inheritance

코틀린은 전통적인 객체지향의 상속 매커니즘을 완벽하게 지원합니다.

open class Dog {                // 1
    open fun sayHello() {       // 2
        println("wow wow!")
    }
}

class Yorkshire : Dog() {       // 3
    override fun sayHello() {   // 4
        println("wif wif!")
    }
}

fun main() {
    val dog: Dog = Yorkshire()
    dog.sayHello() //wif wif!
}
  1. 코틀린 클래스들은 final 이 디폴트(default)입니다. (상속 불가능한 클래스) 만약, 클래스의 상속을 허가하게 하고 싶으면, open 키워드를 붙이면 됩니다.
  2. 코틀린의 메소드 또한 final 이 디폴트(default)입니다. (오버라이드가 불가능한 메소드) 클래스처럼 open 키워드를 붙여서 오버라이드를 가능하게 합니다.
  3. 클래스 이름 뒤에 : SuperClassName() 을 지정하여 슈퍼클래스(부모클래스)를 상속한 클래스를 만들 수 있습니다. 빈 괄호()는 슈퍼클래스(부모클래스)의 기본 생성자를 나타냅니다.
  4. 메소드나 속성을 오버라이드하려면 override 키워드가 필요합니다.

인자가 있는 생성자를 통한 상속(Inheritance with Parameterized Constructor)

open class Tiger(val origin: String) {
    fun sayHello() {
        println("A tiger from $origin says: grrhhh!")
    }
}

class SiberianTiger : Tiger("Siberia")                  // 1

fun main() {
    val tiger: Tiger = SiberianTiger()
    tiger.sayHello() // A tiger from Siberia says: grrhhh!
}

슈퍼클래스를 생성할 때 슈퍼클래스의 파라미터가 있는 생성자를 이용하려면, 서브클래스의 선언부에서 인자를 제공하면 된다.

슈퍼클래스로 생성자 인자 전달(Passing Constructor Arguments to Superclass)

open class Lion(val name: String, val origin: String) {
    fun sayHello() {
        println("$name, the lion from $origin says: graoh!")
    }
}

class Asiatic(name: String) : Lion(name = name, origin = "India") // 1

fun main() {
    val lion: Lion = Asiatic("Rufo")                              // 2
    lion.sayHello() //Rufo, the lion from India says: graoh!
}
  1. Asiatic 클래스 선언의 namevarval 도 아닙니다. 생성자의 인자이며 값은 슈퍼클래스인 Lion 클래스의 name프로퍼티로 전달됩니다.
  2. 이름이 RufoAsiatic 클래스의 인스턴스를 생성합니다. 이 호출은 RufoIndia 를 인자로 갖는 Lion 클래스의 생성자를 호출합니다.