Kotlin 프로그래밍 언어 알아보기

Kotlin은 전 세계 Android 개발자가 널리 사용하는 프로그래밍 언어입니다. 이 주제는 활용도를 높이기 위한 Kotlin 단기 집중과정입니다.

변수 선언

Kotlin은 두 키워드(valvar)를 사용하여 변수를 선언합니다.

  • 값이 변경되지 않는 변수에 val을 사용합니다. val을 사용하여 선언된 변수에 값을 다시 할당할 수 없습니다.
  • 값이 변경될 수 있는 변수에 var을 사용합니다.

아래 예에서 count는 초기 값 10이 할당되는 Int 유형의 변수입니다.

var count: Int = 10
    

Int는 정수를 나타내는 유형이며 Kotlin에서 표현될 수 있는 많은 숫자 유형 중 하나입니다. 다른 언어와 마찬가지로 숫자 데이터에 따라 Byte, Short, Long, Float, Double을 사용할 수도 있습니다.

var 키워드는 필요에 따라 count에 값을 재할당할 수 있음을 의미합니다. 예를 들어 count 값을 10에서 15로 변경할 수 있습니다.

var count: Int = 10
    count = 15
    

하지만 일부 값은 변경되지 않습니다. languageName이라는 String을 고려합니다. languageName에서 'Kotlin'의 값이 항상 유지되도록 하려면 val 키워드를 사용하여 languageName을 선언합니다.

val languageName: String = "Kotlin"
    

이러한 키워드를 사용하여 변경 가능한 항목을 명시할 수 있습니다. 필요에 따라 키워드를 유용하게 사용하세요. 변수 참조를 재할당할 수 있어야 하는 경우 var로 선언합니다. 그렇지 않은 경우 val을 사용합니다.

유형 추론

계속해서 이전 예에서 languageName에 초기 값을 할당할 때 Kotlin 컴파일러는 할당된 값의 유형을 기반으로 유형을 유추할 수 있습니다.

"Kotlin" 값이 String 유형이므로 컴파일러는 languageName 또한 String이라고 유추합니다. Kotlin은 정적으로 입력되는 언어입니다. 즉, 컴파일 시간에 유형이 확인되고 절대 변경되지 않습니다.

다음 예에서 languageNameString으로 추정되므로 String 클래스의 일부가 아닌 함수를 호출할 수 없습니다.

val languageName = "Kotlin"
    val upperCaseName = languageName.toUpperCase()

    // Fails to compile
    languageName.inc()
    

toUpperCase()String 유형의 변수에서만 호출할 수 있는 함수입니다. Kotlin 컴파일러가 languageNameString으로 추정했으므로 toUpperCase()를 안전하게 호출할 수 있습니다. 하지만 inc()Int 연산자 함수이므로 String에서 호출할 수 없습니다. Kotlin의 유형 추론 방식은 간결성과 유형 안전성을 모두 보장합니다.

Null 안전

일부 언어에서는 초기 값을 명시적으로 제공하지 않고 참조 유형 변수를 선언할 수 있습니다. 이러한 경우 변수에는 일반적으로 null 값이 포함됩니다. Kotlin 변수는 기본적으로 null 값을 보유할 수 없습니다. 즉, 다음 스니펫은 유효하지 않습니다.

// Fails to compile
    val languageName: String = null
    

null 값을 포함하는 변수는 nullable 유형이어야 합니다. 아래 예와 같이 ?를 변수의 접미사로 지정하여 변수를 nullable로 지정할 수 있습니다.

val languageName: String? = null
    

String? 유형을 사용하여 String 값 또는 nulllanguageName에 할당할 수 있습니다.

nullable 변수는 신중하게 처리해야 합니다. 아니면 심각한 NullPointerException이 발생할 위험이 있습니다. 예를 들어, Java에서 null 값에 관해 메서드를 호출하려고 하면 프로그램이 비정상 종료됩니다.

Kotlin은 nullable 변수로 안전하게 작업하기 위한 많은 메커니즘을 제공합니다. 자세한 내용은 Android의 일반 Kotlin 패턴: Null 허용 여부를 참조하세요.

조건부

Kotlin은 조건부 논리를 구현하기 위한 몇 가지 메커니즘을 제공합니다. 가장 일반적인 것은 if-else 문입니다. if 키워드 옆의 괄호 안에 포함된 식이 true로 평가되는 경우 분기 내 코드(즉, 중괄호 안에 포함된 바로 다음 코드)가 실행됩니다. 그렇지 않은 경우 else 분기 내 코드가 실행됩니다.

if (count == 42) {
        println("I have the answer.")
    } else {
        println("The answer eludes me.")
    }
    

else if를 사용하여 여러 조건을 나타낼 수 있습니다. 즉, 다음 예와 같이 단일 조건문 내에서 보다 세분화되고 복잡한 논리를 표현할 수 있습니다.

if (count == 42) {
        println("I have the answer.")
    } else if (count > 35) {
        println("The answer is close.")
    } else {
        println("The answer eludes me.")
    }
    

조건문은 스테이트풀(stateful) 논리를 나타내는 데 유용하지만 작성 시 반복될 수 있습니다. 위의 예에서는 각 분기에 String을 인쇄합니다. 이 반복을 피하기 위해 Kotlin은 조건식을 제공합니다. 마지막 예는 다음과 같이 다시 작성될 수 있습니다.

val answerString: String = if (count == 42) {
        "I have the answer."
    } else if (count > 35) {
        "The answer is close."
    } else {
        "The answer eludes me."
    }

    println(answerString)
    

암시적으로 각 조건부 분기는 마지막 줄에 표현식의 결과를 반환하므로 return 키워드를 사용할 필요가 없습니다. 세 분기의 결과는 모두 String 유형이므로 if-else 표현식의 결과도 String 유형입니다. 이 예에서 answerString에는 if-else 표현식의 결과에서 초기 값이 할당됩니다. 유형 추론을 사용하여 answerString에 대한 명시적 유형 선언을 생략할 수 있지만, 명확히 하기 위해 유형 선언을 포함하는 것이 좋습니다.

아래 예와 같이 조건문의 복잡도가 증가하면 if-else 표현식을 when 표현식으로 교체할 것을 고려할 수 있습니다.

val answerString = when {
        count == 42 -> "I have the answer."
        count > 35 -> "The answer is close."
        else -> "The answer eludes me."
    }

    println(answerString)
    

when 표현식의 각 분기는 조건, 화살표(->) 및 결과로 표시됩니다. 화살표의 왼쪽 조건이 true로 평가되면 오른쪽에 있는 표현식의 결과가 반환됩니다. 한 분기에서 다음 분기로 실행되지 않습니다. when 표현식 예의 코드는 이전 예의 코드와 기능적으로 동일하지만 쉽게 읽을 수 있습니다.

Kotlin의 조건부는 이 언어의 강력한 기능 중 하나인 스마트 캐스팅(smart casting)을 강조합니다. safe-call 연산자 또는 not-null assertion 연산자를 사용하여 nullable 값을 처리하는 대신 아래 예와 같이 조건식을 사용하여 변수에 null 값에 대한 참조가 있는지 확인할 수 있습니다.

val languageName: String? = null
    if (languageName != null) {
        // No need to write languageName?.toUpperCase()
        println(languageName.toUpperCase())
    }
    

조건부 분기 내에서 languageName은 nullable이 아닌 것으로 간주될 수 있습니다. Kotlin에서는 분기 실행 조건에 따라 languageName은 null 값을 보유할 수 없으므로 분기 내에서 languageName을 nullable로 처리할 필요가 없습니다. 이 스마트 캐스팅은 null 검사, 유형 검사 또는 컨트랙트를 충족하는 모든 조건에 적용됩니다.

함수

하나 이상의 표현식을 함수로 그룹화할 수 있습니다. 결과가 필요할 때마다 동일한 일련의 표현식을 반복하는 대신 함수에 표현식을 포함한 다음 함수를 호출할 수 있습니다.

함수를 선언하려면 fun 키워드 뒤에 함수 이름이 오도록 사용합니다. 그런 다음 함수에 사용되는 입력 유형(있는 경우)을 정의하고 반환하는 출력 유형을 선언합니다. 함수의 본문에서는 함수를 호출할 때 호출되는 표현식을 정의합니다.

이전 예제를 기반으로 완성된 Kotlin 함수는 다음과 같습니다.

fun generateAnswerString(): String {
        val answerString = if (count == 42) {
            "I have the answer."
        } else {
            "The answer eludes me"
        }

        return answerString
    }
    

위 예에서 함수의 이름은 generateAnswerString입니다. 입력 값은 받지 않으며, String 유형의 결과를 출력합니다. 함수를 호출하려면 함수의 이름 뒤에 호출 연산자(())를 사용합니다. 아래 예에서 answerString 변수는 generateAnswerString()의 결과에 따라 초기화됩니다.

val answerString = generateAnswerString()
    

아래 예와 같이 함수에서는 인수를 입력으로 사용할 수 있습니다.

fun generateAnswerString(countThreshold: Int): String {
        val answerString = if (count > countThreshold) {
            "I have the answer."
        } else {
            "The answer eludes me."
        }

        return answerString
    }
    

함수를 선언할 때 인수의 개수와 유형을 지정할 수 있습니다. 위의 예에서 generateAnswerString()Int 유형의 countThreshold 인수 한 개를 사용합니다. 함수 내에서 이름을 사용하여 인수를 참조할 수 있습니다.

이 함수를 호출할 때 함수 호출 괄호 안에 인수를 포함해야 합니다.

val answerString = generateAnswerString(42)
    

함수 선언 단순화

generateAnswerString()은 매우 간단한 함수입니다. 이 함수는 변수를 선언한 다음 즉시 반환됩니다. 아래 예제와 같이 단일 표현식의 결과가 함수에서 반환되는 경우 함수에 포함된 if-else 표현식의 결과를 직접 반환하여 로컬 변수 선언을 건너뛸 수 있습니다.

fun generateAnswerString(countThreshold: Int): String {
        return if (count > countThreshold) {
            "I have the answer."
        } else {
            "The answer eludes me."
        }
    }
    

return 키워드를 대입 연산자로 바꿀 수도 있습니다.

fun generateAnswerString(countThreshold: Int): String = if (count > countThreshold) {
            "I have the answer"
        } else {
            "The answer eludes me"
        }
    

익명 함수

모든 함수에 이름이 필요하지는 않습니다. 일부 함수는 입력과 출력에 의해 더 직접적으로 식별됩니다. 이러한 함수를 익명 함수라고 합니다. 이 참조를 사용하여 나중에 익명 함수를 호출하면 익명 함수에 대한 참조를 유지할 수 있습니다. 다른 참조 유형과 마찬가지로 애플리케이션에서 참조를 전달할 수도 있습니다.

val stringLengthFunc: (String) -> Int = { input ->
        input.length
    }
    

이름이 지정된 함수와 마찬가지로 익명 함수는 표현식을 제한 없이 포함할 수 있습니다. 함수의 반환 값은 최종 표현식의 결과입니다.

위의 예에서 stringLengthFuncString을 입력으로 사용하고 String 입력 길이를 Int 유형의 출력으로 반환하는 익명 함수에 대한 참조를 포함합니다. 따라서 함수의 유형은 (String) -> Int로 표시됩니다. 하지만 이 코드는 함수를 호출하지 않습니다. 함수의 결과를 가져오려면 이름이 지정된 함수처럼 호출해야 합니다. 아래 예제와 같이 stringLengthFunc를 호출할 때 String을 공급해야 합니다.

val stringLengthFunc: (String) -> Int = { input ->
        input.length
    }

    val stringLength: Int = stringLengthFunc("Android")
    

고차 함수

함수는 다른 함수를 인수로 취할 수 있습니다. 다른 함수를 인수로 사용하는 함수를 고차 함수라고 합니다. 이 패턴은 Java에서 콜백 인터페이스를 사용할 때와 동일한 방식으로 구성요소 간에 통신하는 데 유용합니다.

다음은 고차 함수의 예입니다.

fun stringMapper(str: String, mapper: (String) -> Int): Int {
        // Invoke function
        return mapper(str)
    }
    

stringMapper() 함수는 전달된 String에서 Int 값을 파생하는 함수와 함께 String를 가져옵니다.

아래 예제와 같이 다른 입력 매개변수를 충족하는 함수, 즉 String을 입력으로 사용하고 Int를 출력하는 함수와 을 전달하여 stringMapper()를 호출할 수 있습니다.

stringMapper("Android", { input ->
        input.length
    })
    

아래 예제와 같이 익명 함수가 함수에 정의된 마지막 매개변수인 경우 함수를 호출하는 데 사용된 괄호 밖에서 함수를 전달할 수 있습니다.

stringMapper("Android") { input ->
        input.length
    }
    

익명 함수는 Kotlin 표준 라이브러리 전체에서 찾을 수 있습니다. 자세한 내용은 고차 함수 및 람다를 참조하세요.

클래스

지금까지 언급된 모든 유형은 Kotlin 프로그래밍 언어에 내장되어 있습니다. 아래 예제와 같이 맞춤 유형을 추가하려는 경우 class 키워드를 사용하여 클래스를 정의할 수 있습니다.

class Car
    

속성

클래스는 속성을 사용하여 상태를 나타냅니다. 속성은 getter, setter 및 backing 필드를 포함할 수 있는 클래스 수준 변수입니다. 아래 예제와 같이 자동차를 운전하려면 바퀴가 필요하므로 Wheel 객체 목록을 Car의 속성으로 추가합니다.

class Car {
        val wheels = listOf<Wheel>()
    }
    

wheelspublic val입니다. 즉, Car 클래스 외부에서 에 액세스할 수 있지만 다시 할당할 수 없습니다. Car의 인스턴스를 가져오려면 먼저 생성자를 호출해야 합니다. 그런 다음 액세스 가능한 모든 속성에 액세스할 수 있습니다.

val car = Car() // construct a Car
    val wheels = car.wheels // retrieve the wheels value from the Car
    

바퀴를 맞춤 설정하려면 클래스 속성을 초기화하는 방법을 지정하는 맞춤 생성자를 정의합니다.

class Car(val wheels: List<Wheel>)
    

위의 예에서 클래스 생성자는 List<Wheel>을 생성자 인수로 취하고 인수를 사용하여 wheels 속성을 초기화합니다.

클래스 함수 및 캡슐화

클래스는 함수를 사용하여 동작을 모델링합니다. 함수는 상태를 수정할 수 있으므로 노출하려는 데이터만 노출할 수 있습니다. 이 액세스 제어는 캡슐화라는 더 큰 객체 지향 개념의 일부입니다.

다음 예에서 doorLock 속성은 Car 클래스 외부의 모든 항목에서 비공개로 유지됩니다. 아래 예제와 같이 자동차를 잠금 해제하려면 유효한 키를 전달하는 unlockDoor() 함수를 호출해야 합니다.

class Car(val wheels: List<Wheel>) {

        private val doorLock: DoorLock = ...

        fun unlockDoor(key: Key): Boolean {
            // Return true if key is valid for door lock, false otherwise
        }
    }
    

속성을 참조하는 방법을 맞춤 설정하려면 맞춤 getter 및 setter를 제공합니다. 예를 들어 속성의 setter에 대한 액세스를 제한하면서 속성의 getter를 노출하려면 setter를 private으로 지정합니다.

class Car(val wheels: List<Wheel>) {

        private val doorLock: DoorLock = ...

        val gallonsOfFuelInTank: Int = 15
            private set

        fun unlockDoor(key: Key): Boolean {
            // Return true if key is valid for door lock, false otherwise
        }
    }
    

속성과 함수를 조합하여 모든 유형의 객체를 모델링하는 클래스를 만들 수 있습니다.

상호운용성

Kotlin의 가장 중요한 기능 중 하나는 Java와의 유연한 상호운용성입니다. Kotlin 코드는 JVM 바이트 코드로 컴파일되기 때문에 Kotlin 코드는 Java 코드로 직접 호출될 수 있으며 그 반대의 경우도 마찬가지입니다. 즉, 기존 Java 라이브러리를 Kotlin에서 직접 활용할 수 있습니다. 또한 대부분의 Android API는 Java로 작성되어 Kotlin에서 바로 호출할 수 있습니다.

Kotlin은 증가하는 지원으로 성장세를 이어가고 있는 유연하고 실용적인 언어입니다. 아직 해보지 않으셨다면 시도해 보시기 바랍니다. 다음 단계에서는 Android 앱에서 일반 Kotlin 패턴을 적용하는 방법에 대한 가이드와 함께 공식 Kotlin 문서를 살펴봅니다.