코틀린 기초

  • 코틀린 기초

  • 코틀린 객체 지향 프로그래밍

  • 함수형 프로그래밍

Kotlin Basic

사용 라이브러리

testImplementation("org.assertj:assertj-core:3.26.3")
testImplementation("org.hamcrest:hamcrest:2.2")

Null 허용 타입

변수가 null 값을 갖지 못하게 하려면

안전 호출 연산자(?.)나 엘비스 연산자(?:)와 결합해서 사용하자.

👉🏻 val 변수의 영리한 타입 변환(smart cast)

  • 널 할당이 불가능한 문자열 타입으로 영리한 변환 가능

val p = Person(first = "North", middle = null, last = "West")
if (p.middle != null) {
    val middleNameLength = p.middle.length
}

👉🏻 var 변수가 널 값이 아님을 단언하기 !!

  • var 변수는 String 타입으로 영리한 타입 변환이 불가능

  • 널 아님 단언 연산자(!!, not-null assertion operator)로 널 아님을 단언할 수 있지만, 코드 스멜이다

    • 변수가 널이 아닌 값으로 다뤄지도록 강제하고 해당 변수가 널이라면 예외를 던진다

    • 널 값에 이 연산자를 사용하는 것은 코틀린에서 NPE를 만날 수 있는 몇 가지 상황 중 하나

    • 가능하면 사용하지 않도록 노력하자.

👉🏻 var 변수에 안전 호출 연산자 사용하기 ?.

  • 이 상황에서 안전 호출(?., safe call)를 사용하는 것이 좋다.

  • 결과 타입은 Type? 형태이고, null 이면 null을 반환

👉🏻 var 변수에 안전 호출 연산자와 엘비스 연산자 사용하기 ?:

  • ?: : 왼쪽 식의 값을 확인해서 해당 값이 널이 아니면 그 값을 리턴, 널이라면 오른쪽 값을 리턴

👉🏻 안전 타입 변환 연산자 as?

  • 타입 변환이 올바르게 동작하지 않은 경우 ClassCastException이 발생하는 상황을 방지


명시적 타입 변환

코틀린은 자동으로 기본 타입을 더 넓은 타입으로 승격하지 않는다. Int -> Long (X)

더 작은 타입을 명시적으로 변환하려면 toInt, toLong 등 구체적인 변환 함수를 사용하자.

사용 가능한 타입 변환 메소드

  • toByte(): Byte

  • toChar(): Char

  • toShort(): Short

  • toInt(): Int

  • toLong(): Long

  • toFloat(): Float

  • toDouble(): Double


중위(infix) 함수

코틀린에는 자바처럼 내장 거듭제곱 연산자가 없다.

👉🏻 시그니처의 확장 함수를 정의

👉🏻 중위 연산자 infix 정의


Pair 인스턴스

중위(infix) to 함수로 Pair 클래스의 인스턴스를 생성할 수 있다.

코틀린은 Pair 인스턴스의 리스트로부터 맵을 생성하는 mapOf와 같은 맵 생성을 위한 최상위 함수 몇 가지를 제공

Pair는 first, second 이름의 두 개의 원소를 갖는 데이터 클래스이다.

Pair 클래스는 두 개의 인자를 받는 생성자를 사용해서 Pair 클래스를 생성할 수 있지만, to 함수를 사용하는 것이 일반적이다.

👉🏻 mapOf 인자인 pair를 생성하기 위해 to 함수 사용하기

⚠️ Pair는 데이터 클래스이므로 구조 분해를 통해서만 개별 원소에 접근할 수 있다.

Triple

세 개의 값을 나타내는 Triple이라는 이름의 클래스도 코틀린 표준 라이브러리에 들어 있다.

객체 지향 프로그래밍

const와 val의 차이

런타임보다 컴파일 타임에 변수가 상수임을 나타내야 한다

컴파일 타임 상수에 const 변경자를 사용하자.

  • val 키워드는 변수에 한 번 할당되면 변경이 불가능함을 나타내지만 이러한 할당은 실행 시간에 일어난다.

👉🏻 컴파일 타임 상수 정의하기

  • const가 val 키워드를 대체하는 것이 아니라 반드시 같이 쓰여야 한다.


사용자 정의 획득자와 설정자 생성하기

코틀린은 모든 것이 기본적으로 public 이다.

데이터 은닉 원칙을 침해하는 딜레마를 클래스에서 필드는 직접 선언할 수 없도록 하는 방법으로 해결하였다.

👉🏻 사용자 정의 획득자와 설정자

속성 정의 문법


데이터 클래스 정의하기

클래스를 정의할 때 data 키워드로 equals, hashCode, toString 등을 갖춘 엔티티를 나타낼 수 있다.

코틀린은 데이터를 담는 특정 클래스의 용도로 나타내기 위해 data 키워드를 제공한다.

.

data 키워드로 생성되는 함수들..

👉🏻 주 생성자에 선언된 속성 바탕으로 생성되는 equals, hashCode 함수

👉🏻 클래스의 속성 값을 보여주는 toString 함수

👉🏻 copy 함수

  • 원본과 같은 속성 값으로 시작해서 copy 함수에 제공된 속성 값만을 변경해 새로운 객체를 생성하는 인스턴스 메소드

  • copy 함수는 얕은 복사를 수행

👉🏻 구조 분해를 위한 component 함수

참고

  • 원할 경우 equals, hashCode, toString, copy, _componentN_ 함수를 자유롭게 재정의 가능

  • 데이터 클래스는 기본적으로 데이터가 담긴 클래스를 손쉽게 표현

  • 코틀린 표준 라이브러리에는 2-3개의 제네릭 타입 속성을 담는 Pair, Triple 이라는 데이터 클래스가 존재


지원 속성 기법

클래스의 속성을 클라이언트에게 노출하고 싶지만, 해당 속성을 초기화하거나 읽는 방법을 제어하고 싶다면

같은 타입의 속성을 하나 더 정의하고 사용자 정의 획득자와 설정자를 이용해 원하는 속성에 접근하자.

👉🏻 생성 즉시 초기화되지 않도록 message 속성과 같은 타입의 널 허용 _message 속성을 추가

.

👉🏻 lazy 대리자 함수로 쉽게 지연로딩을 구현


연산자 중복(Overloading)

코틀린의 연산자 중복(overloading) 매커니즘을 사용해서 +, * 등의 연산자와 연관된 함수를 구현할 수 있다.

👉🏻 함수 재정의

많은 연산자가 코틀린에서 함수로 구현되어 있고, 기호를 사용하면 해당 연산자와 연관된 함수에 처리를 위임

  • 모든 연산자 함수 재정의 시 operator 키워드는 필수(equals 제외)

.

👉🏻 확장 함수

자신이 작성하지 않은 클래스에 연산과 관련 함수를 추가하고 싶다면 확장 함수를 사용할 수 있다.

  • Complex 클래스의 이와 같은 기존 함수에 연산을 위임하는 확장 함수


지연 초기화 lateinit

널 비허용으로 선언된 클래스 속성은 생성자에서 초기화되어야 하지만, 속성에 할당할 값의 정보가 충분하지 않다면

나중 초기화를 위해 lateinit을 사용할 수 있다.

단, 의존성 주입의 경우 유용하지만, 일반적으로는 지연 평가 같은 대안을 권장

👉🏻 스프링 컨트롤러 테스트

.

lateinit 변경자는 클래스 몸체에서만 선언되고 사용자 정의 획득자와 설정자가 없는 var 속성에서만 사용할 수 있다.

  • lateinit 사용 가능한 속성 타입은 널 할당이 불가능한 타입

    • 기본 타입에는 lateinit 사용 불가

  • lateinit 추가 시 해당 변수가 처음 사용되기 전에 초기화 가능

    • 사용 전 초기화에 실패하면 예외 발생

👉🏻 lateinit 속성 동작 방식

lateinit, lazy 차이

lateinit 변경자는 var 속성에 사용되고, lazy 대리자는 속성에 처음 접근할 때 평가되는 람다를 받는다.

초기화 비용은 높은데 lazy를 사용한다면 초기화는 반드시 실패한다.

  • 또한, lazy는 val 속성에 사용할 수 있는 반면, lateinit은 var 속성에만 적용 가능

lateinit 속성은 속성에 접근할 수 있는 모든 곳에서 초기화 가능

  • 객체 바깥쪽에서도 초기화 가능


equals 메소드 구현

논리적으로 동등한 인스턴스인지 확인 가능한 equals 재정의를 위해

레퍼런스 동등 연산자(==), 안전 타입 변환 함수(as?), 엘비스 연산자(?:)를 다 같이 사용

👉🏻 Any class

equals 문법에서 equalsa 구현은 반사성(reflexive), 대칭성(symmetirc), 추이성(transitive), 일관성(consistent)이 있어야 하고, 널도 적절하게 처리할 수 있어야 한다.

.

👉🏻 equals 구현의 좋은 예 (KotlinVersion 클래스의 equals)

  • equals 함수가 재정의되면 hashCode 함수도 재정의

.

👉🏻 equals, hashCode 구현해 보기


싱글톤 생성하기

클래스 하나당 인스턴스를 하나만 만드려면 class 대신 object 키워드를 사용하자.

싱글톤 디자인 패턴은 특정 클래스의 인스턴스를 오직 하나만 존재하도록 메커니즘을 정의

  • 클래스의 모든 생성자를 private 로 정의

  • 필요하다면 해당 클래스를 인스턴스화하고 그 인스턴스 레퍼런스를 리턴하는 정적 팩토리 메소드를 제공

👉🏻 코틀린에서 싱글톤 선언

👉🏻 싱글톤을 위해 object를 디컴파일한 코드

코틀린 object와 인자 전달

코틀린 object는 생성자를 가질 수 없기 때문에 쉽게 인자를 전달할 수 있는 방법이 없다.


Nothing에 관한 야단법석

코틀린의 Nothing 클래스

  • Nothing의 인스턴스는 존재하지 않는다.

  • 결코 존재할 수 없는 값을 나타내기 위해 Nothing을 사용할 수 있다.

.

Nothing 클래스는 아래 두 가지 상황에서 사용될 수 있다

👉🏻 코틀린에서 예외 던지기

  • 리턴 타입을 반드시 명시해야 하지만 리턴하지 않으므로 리턴 타입은 Nothing

👉🏻 변수에 널을 할당할 때 구체적인 타입을 명시하지 않는 경우

  • x에 대한 다른 정보가 없으므로 추론된 x의 타입은 Nothing?

  • 코틀린에서 Nothing 클래스는 실제로 다른 모든 타입의 하위 타입

함수형 프로그래밍

알고리즘에서 fold 사용하기

fold 함수를 사용해 시퀀스나 컬렉션을 하나의 값으로 축약할 수 있다.

fold 함수는 배열 또는 반복 가능한 컬렉션에 적용할 수 있는 축약 연산

  • fold는 두 개의 인자를 받는다.

    • 첫 번째는 누적자의 초기값

    • 두 번째는 두 개의 인자를 받아 누적자를 위해 새로운 값을 리턴하는 함수

👉🏻 fold를 사용해 정수의 합 계산하기

👉🏻 fold를 사용해 팩토리얼 구현하기

👉🏻 fold를 사용해 피보나치 수 계산하기


reduce 함수를 사용해 축약하기

비어 있지 않는 컬렉션의 값을 축약하고 싶지만 누적자의 초기값을 설정하고 싶지 안다면,

fold 대신 reduce 연산을 사용

reduce 함수는 fold 함수와 사용 목적도 같고 비슷하다.

  • 큰 차이점은 reduce 함수에는 누적자의 초기값 인자가 없는 것

  • 따라서 누적자의 초기값은 컬렉션의 첫 번째 값으로 초기화

reduce 함수는 누적자를 컬렉션의 첫 번째 값으로 초기화할 수 있는 경우에만 사용 가능

  • 인자와 함께 함수가 호출되면 첫 번째 인자가 누적자로 초기화되며 다른 값들은 한 번에 하나씩 누적자에 더해진다.

  • 인자 없이 호출될 경우 예외를 던진다.

👉🏻 reduce로 구현한 sum 함수

reduce 사용

컬렉션의 첫 번째 값으로 누적자를 초기화하고 컬렉션의 다른 값에 추가 연산을 필요로 하지 않는 경우에만 reduce를 사용하자.


꼬리 재귀 적용하기

재귀 프로세스를 실행하는 데 필요한 메모리를 최소화하고 싶다면, tailrec 키워드를 추가하자.

꼬리 재귀로 알려진 접근법은 콜 스택에 새 스택 프레임을 추가하지 않게 구현하는 특별한 종류의 재귀이다.

👉🏻 꼬리 호출 알고리즘을 사용하는 팩토리얼 구현

  • 팩토리얼 연산의 누적자 역할을 하는 두 번째 인자가 필요

  • 마지막 평가 식은 더 작은 수와 증가된 누적자를 이용해 자신 스스로를 호출

tailrec 변경자를 적용할 수 있는 함수의 자격 요건

  • 해당 함수는 반드시 수행하는 마지막 연산으로서 자신을 호출해야 한다.

  • try/catch/finally 블록 안에서는 tailrec을 사용할 수 없다.

  • 오직 JVM 백엔드에서만 꼬리 재귀가 지원된다.

tailrec 키워드

컴파일러에게 해당 재귀 호출을 최적화해야 한다고 알려준다.

자바에서 똑같은 알고리즘을 재귀로 작성하면 메모리 제약이 있다.

Last updated