선생님, 개발을 잘하고 싶어요.

Kotlin in Action - 1부 6장 - 코틀린 타입 시스템 본문

일상/책 리뷰

Kotlin in Action - 1부 6장 - 코틀린 타입 시스템

알고싶은 승민 2022. 5. 23. 21:45

다루는 거

  • Nullable Type
    • 안전한 호출 ?.
    • 엘비스 연산자 ?:
    • 널 아님 단언 !!
    • ?.let { }
    • as?
  • 원시 타입
  • Any, Any? 타입
  • Unit 타입
  • Nothing 타입
  • 읽기 전용 컬렉션
  • 플랫폼타입

널 가능성

코틀린은 null에 대한 접근 시 발생 가능한 NPE 문제를 런타임 에러에서 컴파일 타임 에러로 옮기려고 한다. 널이 될 수 있는지 여부를 타입 시스템에 추가해서 컴파일러가 여러 오류를 컴파일 시 미리 감지하는 기법을 도입한다.

코틀린과 자바의 가장 중요한 차이는 코틀린 타입 시스템이 널이 될 수 있는 타입을 명시적으로 지원한다는 점이다.

코틀린에서 String 타입의 의미는 이 변수가 런타임에 절대로 null이 아니라는 걸 장담한다는 것, 즉 NPE가 발생되지 않을 것이라는 걸 장담한다는 뜻이다.

Type? → Type 이거나 null일 수 있다.

즉 ?가 없는 타입은 null 참조를 저장할 수 없다는 뜻이다.

null 타입 처리를 단순히 if 분기로만 처리할 수 있다면 매우 번거로울 것이고 이를 해결하기 위해서 코틀린은 다른 도움이 되는 여러 도구를 제공한다.

타입의 의미

타입은 어떤 값들이 가능한지와 그 타입에 대해 수행할 수 있는 연산의 종류를 결정한다.

자바에서는 String 타입에 인스턴스가 null인지 아닌지에 따라서 수행할 수 있는 연산이 달라진다. 이는 자바의 타입시스템이 널을 제대로 다루지 못한다는 뜻이다. 변수에는 선언된 타입이 있지만, 널 여부를 추가로 검사하기 전에는 그 변수에 대한 어떤 연산을 수행할 수 있을지 알 수 없다.

Optional 타입 등 null을 감싸는 별도의 래퍼 타입을 활용하기도 하지만

  1. 코드가 더 지저분해지고
  2. 래퍼가 추가됨에 따라 실행 시점에 성능이 저하되고
  3. 전체 에코시스템에서 일관성 있게 활용하기 어렵다

실행 시점에는 널이 될 수 있는 타입이나 널이 될 수 없는 타입의 객체는 같다. 모든 검사는 컴파일 시점에 수행된다. 따라서 코틀린은 널 타입 문제를 해결하는데 별도의 실행 시점 부가 비용이 들지 않는다.

  • 안전한 호출 연산자: ?.
// 다음 두 라인은 같다.
s?.toUpperCase()
if (s != null) s.toUpperCase() else null

// 널이 될 수 있는 중간 객체가 여럿 있다면 연쇄 사용을 편하게 할 수 있다.
val country: String? = this.company?.address?.country
  • 엘비스 연산자: ?:
    • null 대신 사용할 디폴트 값을 지정할 때 편리하게 사용한다.
    • 엘비스 연산자는 이항 연산자로 좌항을 계산한 값이 널인지 검사하고 널이 아니라면 그 값을, 널이라면 우항 값을 결과로 한다.
fun foo(s: String?) {
  val t: String = s ?: "" // s가 null이면, 빈 문자열이다.
}
// 코틀린에선 return, throw등의 연산도 식이라서 다음과 같이 편리하게 사용할 수 있다.

val address: String = person.company?.address
  ?: throw IllegalArgumentException("No address") // 주소 없으면 예외 발생

// 여기로 넘어온 건 예외가 발생하지 않았다는 뜻이므로 주소가 있다는 뜻이다.
  • 안전한 캐스트: as?
    • as? 연산을 활용하면 캐스트가 될 때는 해당 타입으로 반환되고, 캐스트 할 수 없으면 null을 반환한다.
    • 자바 타입 캐스트와 마찬가지로 대상 값을 as로 지정한 타입으로 바꿀 수 없을 때 ClassCastException이 발생한다.
    • as 하기 전에 is인지 검사하는 코드를 추가하면 되긴 하겠으나 여전히 번거롭다.
      val otherPerson = o as? Person ?: return false
      
  • 널 아님 단언: !!
    • 발생한 예외는 null 값을 사용하는 코드가 아니라 단언문이 있는 !!를 사용한 위치를 가리킨다.
    • !! 에러에는 라인 정보밖에 안들어가므로 !! 연산 체인을 한 라인에 쓰는 걸 피해라.
    • 어떤 값이든 널이 될 수 없는 타입으로 강제로 바꾸고 이때 null이라면 NPE가 발생한다.
  • let 함수 
    • 만약 널이 아닌 타입을 함수의 인자로 넣어야 한다면?
email?.let { sendEmailTo(it }
  • 나중에 초기화할 프로퍼티 lateinit var
    • 실제로는 널이 될 수 없는 프로퍼티인데 생성자 안에서 널이 아닌 값으로 초기화할 방법이 없는 경우가 있다.
    • 객체 인스턴스를 일단 생성하고 나중에 초기화 하는 프레임워크가 많다 (안드로이드, 제이유닛)null이 될 수 없는 타입이라도 생성자 안에서 초기화할 필요가 없다.
    • lateinit를 붙히면 프로퍼티를 나중에 초기화 할 수 있다.
  • 널이 될 수 있는 타입의 확장 e.g. String?.isNullOrBlank()
    • 오직 확장 함수에서만 가능하다.
    • 직접 변수에 대해 메서드를 호출해도 확장 함수인 메서드가 알아서 널을 처리해준다.
fun String?.isNullOrBlank(): Boolean = // nullable String의 확장
  this == null || this.isBlank() // 확장 함수의 this는 null일 수 있다.

var s: String?
s.isNullOrBlank() // nullable 타입이라고 해도 안전한 호출 없이 활용해도 괜찮다.
  • 타입 파라미터의 널 가능성
fun <T> printHashCode(t: T) {
  println(t?.hashCode()) // T는 기본 nullable
}

fun <T : Any> printHashCode(t: T) { // Any 타입 확장이므로 기본 non-nullable
  println(t?.hashCode())
}
  • 자바와 널 가능성: 플랫폼 타입
    • 자바의 @Nullable String → 코틀린의 String?
    • 자바의 @NonNull String → 코틀린의 String
    • 어노테이션이 붙지 않은 경우 코틀린은 이를 널일 수도 아닐 수 도 있는 플랫폼 타입으로 다룬다.
    • 플랫폼 타입은 코틀린이 널 관련 정보를 알 수 없는 타입을 말한다.
    • 오류를 피하기 위해서 자바 메서드의 문서를 잘 보고 인자나 반환이 어떤 속성을 가지는지 파악해야한다.

 

  • 모든 플랫폼 타입을 nullable로 취급한다면 일견 안전한 코드를 작성할 수 있겠으나 그로인해 당연히 null이 반환되지 않는 경우에도 성능을 희생하게 된다.
  • 따라서 우리가 프로퍼티의 널 가능성을 제대로 알고 사용해야한다.
  • 상속의 경우
    • 플랫폼 타입을 상속 받는 경우도 마찬가지로 nullable로 상속받거나 non-null로 상속받는 걸 결정해야한다.

원시 타입

자바는 원시 타입(primitive type, int char 등)과 참조 타입(refernce type, String 등)을 구분한다.

코틀린은 원시 타입과 래퍼 타입을 구분하지 않고 항상 같은 타입을 사용한다. (int, Integer의 구분)

슷지 타입은 가능한 가장 효율적인 방식으로 표현된다. 대부분의 경우 코틀린 Int 타입은 int 원시 타입으로 컴파일된다.

하지만 컬렉션과 같은 제네릭 클래스를 사용하는 경우는 Integer등 래퍼 타입 객체가 들어갈 것이다.

Int와 같은 코틀린 타입은 null이 들어갈 수 없기 때문에 자바의 원시 타입 int와 상응할 수 있는 것이다.

 

널이 될 수 있는 원시 타입 (Int?, Boolean?)는 자바의 래퍼 타입으로 컴파일된다.

JVM이 타입 인자로 원시 타입을 허용하지 않으므로 제네릭 타입의 인자로 쓰이는 Int는 래퍼 타입이 들어간다.

 

코틀린은 숫자를 다른 타입의 숫자로 자동 변환하지 않는다. Int를 Long으로 자동 변환하지 않고 그 반대도 마찬가지다. 다만 Int.toLong, Long.toInt 등 변환 함수를 제공한다.

 

 

특별 타입

  • Any, Any?: 최상위 타입
    • 코틀린에서는 Any가 Int등 원시 타입을 포함한 모든 타입의 조상 타입
    • 널을 포함하는 모든 값을 대입할 변수를 선언하려면 Any? 타입
  • Unit: 코틀린의 void
    • 자바의 void와 비슷하지만 조금 다르다.
    • 관심을 가질 만한 내용을 전혀 반환하지 않는 함수 타입이다.
    • Unit은 모든 기능을 갖는 일반적인 타입으로, void와 달리 타입 인자로 쓸 수 있다. (제네릭에 넘길 수 있다는 말)
    • Unit 타입의 함수는 Unit 반환 타입을 명시할 필요도 없고 묵시적으로 반환하므로 직접 Unit을 명시적 반환할 필요가 없다.
    interface Processor<T> {
      fun process(): T
    }
    
    class NoResultProcessor : Processor<Unit> { // Unit을 타입 인자로 쓸 수 있다.
      override fun process() { // Unit 타입은 반환 타입을 명시적으로 안적어도 된다.
        // something
      } // Unit 반환은 묵시적으로 이루어진다.
    }
    
    • 함수형 프로그래밍에서 전통적으로 Unit은 “단 하나의 인스턴스만 갖는 타입”을 의미하고 인스턴스의 유무가 자바 void와 코틀린 Unit의 가장 큰 차이다.
  • Nothing 타입: 이 함수는 결코 정상적으로 끝나지 않는다.
    • 반환 값이라는 개념 자체가 의미 없는 함수가 일부 존재한다.
    • Nothing 타입으로 인해서 함수가 정상적으로 끝나지 않는다는 걸 컴파일러가 알 수 있게된다.
    fun fail(message: String) : Nothing {
      throw Exception()
    }
    
    val address = company.address ?: fail("No address") 
    
    // null이였다면 fail 함수가 호출되었을 것이다.
    // fail 함수가 Nothing 이므로 그 경우에는 정상적이지 않은 걸 컴파일러가 알 수 있다.
    // 따라서 여기에 온다면 address는 null 타입이라는 걸 추론할 수 있게된다.
    println(address.city) 
    

컬렉션과 배열

리스트의 아이템이 널 가능한지, 컬렉션 자체가 널 가능한지 구분해서 생각하자.

읽기 전용 컬렉션 vs 변경 가능한 컬렉션

코틀린은 데이터 접근 인터페이스와 데이터 변경 인터페이스가 분리되어있다.

Collection은 원소를 추가 제거하는 메서드가 없다. 이런 동작을 하려면 MutableCollection를 확장해야한다.

코드에서 가능하면 항상 읽기 전용 인터페이스를 사용하자.

컬렉션을 변경할 필요가 있을 때만 변경 가능한 버전을 사용하자.

읽기 전용 컬렉션이라고 해도, 내부 구현체는 변경 불가능한 컬렉션이 아닐 수 있다. 따라서 읽기 전용 컬렉션이 항상 스레드 안전하지는 않다는 점을 명심해야한다.

 

코틀린은 모든 자바 컬렉션 인터페이스마다 읽기 전용 인터페이스와 변경 가능한 인터페이스라는 두 가지 표현을 제공한다.

코틀린은 자바 호환성을 제공하는 한편 읽기 전용 인터페이스와 변경 가능 인터페이스를 분리한다.

 

코틀린에서 읽기 전용 Collection으로 선언된 객체라도 자바 코드에서는 그 컬렉션 객체의 내용을 변경할 수 있다.

컬렉션을 자바로 넘기는 코틀린 프로그램을 작성한다면 호출하려는 자바 코드가 컬렉션을 변경할지 여부에 따라 올바른 파라미터 타입을 사용할 책임을 가져야한다.

컬렉션을 자바 코드에 넘길 때는 특별히 주의를 기울여야한다.

 

자바 인터페이스를 상속 받을 때도 마찬가지의 결정을 해야한다.

  • 컬렉션이 변경 가능한가 아닌가?
  • 타입이 nullable한가 아닌가?

이러한 선택을 제대로 하려면 자바 인터페이스나 클래스가 어떤 맥락에서 사용되는지 정확히 알아야 한다.

배열

객체의 배열과 원시 타입 배열 모두 운용할 수 있다.

Array 타입은 참조 타입 배열이 생긴다.

IntArray, CharArray 타입 등 원시 타입 배열이 제공된다.

Comments