널 가능성

  • 널 가능성(nullability): NPE를 피할 수 있게 돕기 위한 코틀린 타입 시스템의 특성
  • 코틀린을 비롯한 최신 언어에서 null에 대한 접근 방법은 가능한 한 이 문제를 실행 시점에서 컴파일 시점으로 옮기는 것이다.

널이 될 수 있는 타입[]

  • 모든 타입은 기본적으로 널이 될 수 없는 타입이다.
  • 널을 받을 수 있게 하려면 타입 이름 뒤에 물음표(?)를 명시해야 한다.
  • 널이 될 수 있는 타입의 변수가 있다면 그에 대해 수행할 수 있는 연산이 제한된다.
    • 메소드를 직접 호출할 수 없다.
    • 널이 될 수 있는 값을 널이 될 수 없는 타입의 변수에 대입할 수 없다.
    • 널이 될 수 있는 값을 널이 될 수 없는 타입의 파라미터를 받는 함수에 전달할 수 없다.
  • 널이 될 수 있는 타입은 null과 비교하고 나면 컴파일러는 그 사실을 기억하고 null이 아님을 확실한 영역에서는 해당 값을 널이 될 수 없는 타입의 값처럼 사용할 수 있다.

타입의 의미

  • 타입: 어떤 값들이 가능한지와 그 타입에 대해 수행할 수 있는 연산의 조유를 결정한다.
  • 자바에서 String타입의 변수에는 String이나 null이라는 두 가지 종류의 값이 들어갈 수 있다.
    • 이 두 종류의 값은 서로 완전히 다르다.
    • 두 종류의 값에 대해 실행할 수 있는 연산도 완전히 다르다.
    • 자바의 타입 시스템이 널을 제대로 다루지 못한다.
  • 자바에서 NullPointerException 오류를 다루는 방법
    • @Nullable이나 @NotNull을 사용한다. 하지만 이런 도구는 표준 자바 컴파일 절차의 일부가 아니기 때문에 일관성 있게 적용된다는 보장을 할 수 없다.
    • null 대신 자바8에 새로 도입된 Optional 타입 등의 null을 감싸는 특별한 래퍼 타입을 활용할 수 있다. 하지만 코드가 더 지저분해지고 래퍼가 추가됨에 따라 실행 시점에 성능이 저하되며 전체 에코시스템에서 일관성 있게 활용하기 어렵다.
  • 코틀린은 실행 시점에 널이 될 수 있는 타입이나 널이 될 수 없는 타입의 객체는 같다.
    • 널이 될 수 있는 타입은 널이 될 수 없는 타입을 감싼 래퍼 타입이 아니다.
    • 모든 검사는 컴파일 시점에 수행한다.
    • 코틀린에서는 널이 될 수 있는 타입을 처리하는데 별도의 실행 시점 부가 비용이 들지 않는다.

안전한 호출 연산자: ?.

  • ?.은 null 검사와 메소드 호출을 한 번의 연산으로 수행한다.
  • 호출하려는 값이 null이 아니라면 ?.는 일반 메소드 호출처럼 작동한다. null이면 이 호출은 무시되고 null이 결과 값이 된다.
  • 메소드 호출뿐 아니라 프로퍼티를 읽거나 쓸 때도 안전한 호출을 사용할 수 있다.
  • 객체 그래프에서 널이 될 수 있는 중간 객체가 여럿 있다면 한 식 안에서 안전한 호출을 연쇄해서 함께 사용하면 편할 때가 자주 있다.
val country = this.company?.address?.country

엘비스 연산자: ?:

  • null 대신 사용할 디폴트 값을 지정할 때 편리하게 사용할 수 있는 연산자
  • 이항 연산자로 좌항을 계산한 값이 널인지 검사한다. 좌항 값이 널이 아니라면 좌항 값을 결과로 하고, 좌항 값이 널이면 우항 값을 결과로 한다.
  • 코틀린에서는 return이나 throw 등의 연산도 식이다. 따라서 엘비스 연산자의 우항에 return, throw 등의 연산을 넣을 수 있다.
val address = person.company?.address?: throw IllegalArgumentException("No address")

안전한 캐스트: as?

  • 어떤 값을 지정한 타입으로 캐스트한다. 값을 대상 타입으로 변환할 수 없으면 null을 반환한다.
  • 안전한 캐스트를 사용할 때 일반적인 패턴은 캐스트를 수행한 뒤에 엘비스 연산자를 사용하는 것이다.
val otherPerson = o as? Person ?: return false

널 아님 단언: !!

  • 어떤 값이든 널이 될 수 없는 타입으로 강제로 바꿀 수 있다.
  • 실제 널에 대해 !!를 적용하면 NPE가 발생한다.
  • 근본적으로 !!는 컴파일러에게 “나는 이 값이 null이 아님을 잘 알고 있다. 내가 잘못 생각했다면 예외가 발생해도 감수하겠다"라고 말하는 것이다.
  • !!를 널에 대해 사용해서 발생하는 예외의 스택 트레이스에는 어떤 파일의 몇 번째 줄인지에 대한 정보는 들어있지만 어떤 식에서 예외가 발생했는지에 대한 정보는 즐어있지 않다.
    • 어떤 값이 널이있는지 확실히 하기 위해 여러 !! 단언문을 한 줄에 함께 쓰는 일은 피하라
person.company!!.address!!.country // 이런 식으로  코드를 작성하지 말라.

let 함수

  • 자신의 수신 객체를 인자로 전달받은 람다에게 넘긴다.
  • 널이 될 수 있는 타입의 값을 널이 될 수 없는 타입의 값으로 바꿔서 람다에 전달하게 된다.
  • let 함수는 값이 널이 아닌 경우에만 호출된다.
var email: String? = "yole@example.com"
email?.let { sendEmailTo(it) }

나중에 초기화할 프로퍼티

  • 코틀린에서 클래스 안의 널이 될 수 없는 프로퍼티를 생성자 안에서 초기화하지 않고 특별한 메소드 안에서 초기화할 수는 없다.
  • 이를 해결하기 위해 프로퍼티를 late-initialized 할 수 있다.
  • lateinit 변경자를 붙이면 프로퍼티를 나중에 초기화할 수 있다.
class MyService {  
   fun performAction():String = "foo"  
}  
  
class MyTest {  
   private lateinit var myService: MyService  
   @BeforeAll fun setUp() {  
      myService = MyService()  
   }  
     
   @Test fun testAction() {  
      assertEquals("foo", myService.performAction())  
   }  
}
  • 프로퍼티를 초기화하기 전에 프로퍼티에 접근하면 “lateinit property myService has not been initialized"이라는 예외가 발생한다.
  • lateinit 프로퍼티를 의존관계 주입(DI) 프레임워크와 함께 사용하는 경우가 많다.

널이 될 수 있는 타입 확장

  • 확장 함수의 경우 안전한 호출 없이도 널이 될 수 있는 수신 객체 타입에서 호출이 가능하다.
fun String?.isNullOrBlank(): Boolean = this == null || this.isBlank()
  • 직접 확장 함수를 작성한다면 처음에는 널이 될 수 없는 타입에 대한 확장 함수를 정의하라. 나중에 대부분 널이 될 수 있는 타입에 대해 그 함수를 호출했다는 사실을 깨닫게 되면 확장 함수 안에 널을 제대로 처리하게 하면 안전하게 그 확장 함수를 널이 될 수 있는 타입에 대한 확장 함수로 바꿀 수 있다.

타입 파라미터의 널 가능성

  • 코틀린에서는 함수나 클래스의 모든 타입 파라미터는 기본적으로 널이 될 수 있다.
  • 따라서 타입 파라미터 T를 클래스나 함수 안에서 타입 이름으로 사용하면 이름 끝에 물음표가 없더라도 T가 널이 될 수 있는 타입이다.

  • 타입 파라미터가 널이 아님을 확실히 하려면 얼이 될 수 없는 타입 상한(upper bound)를 지정해야 한다.

널 가능성과 자바

  • 자바 코드에도 애노테이션으로 표시된 널 가능성 정보가 있다.
    • 자바의 @Nullable String은 코틀린 쪽에서 볼 때 String?와 같고, 자바의 @NotNull String은 코틀린쪽에서 볼 때 String과 같다.
    • JSR-305 표준(javax.annotation 패키지), 안드로이드(android.support.annotation 패키지), 젯브레인스 도구들이 지원하는 애노테이션(org.jetbrains.annotations) 등이 코틀린이 이해할 수 있는 널 가능성 애노테이션들이다.
  • 널 가능성 애노테이션이 자바 코드에 없는 경우, 자바의 타입은 코틀린의 플랫폼 타입(platform type)이 된다.
    • 플랫폼 타입: 코틀린이 널 관련 정보를 알 수 없는 타입
    • 코틀린에서 널이 될 수 있는 타입으로 처리해도되고 널이 될 수 없는 타입으로 처리해도 된다.
    • 만약 잘못사용하게 된다면 NullPointerExceptoin이 발생할 수 있다.
    • 플랫폼 타입은 코틀린 컴파일러가 String!과 같이 느낌표를 뒤에 붙인다.
  • 코틀린 컴파일러는 public 코틀린 함수의 널이 아닌 타입인 파라미터와 수신 객체에 대한 널 검사를 추가해준다.
    • 따라서 public 함수에 널 값을 사용하면 즉시 예외가 발생한다. 이 때 NullPointerException이 아니라 수신 객체($receiver)로 널을 받을 수 없다는 더 자세한 예외가 발생한다.
  • 코틀린이 플랫폼 타입을 도입한 이유
    • 모든 자바 타입을 널이 될 수 있는 타입으로 다룰 수도 있었다.
    • 하지만 그러면 널이 될 수 없는 값에 대해서도 불필요한 널 검사가 들어간다. 이 때문에 플랫폼 타입을 도입했다.
  • 코틀린에서 자바 메소드를 오버라이드할 때 그 메소드의 파라미터와 반환 타입을 널이 될 수 있 수 있는 타입으로 선언할지 널이 될 수 없는 타입으로 선언할지 결정할 수 있다.
    • 코틀린 컴파일러는 널이 될 수 없는 타입으로 선언한 모든 파라미터에 대해 널이 아님을 검사하는 단언문을 만들어준다.

코틀린의 원시 타입

  • 코틀린은 원시 타입과 래퍼 타입을 구분하지 않는다.

원시 타입: Int, Boolean 등

  • 자바는 원시 타입과 참조 타입을 구분한다.
    • 원시 타입은 변수에 그 값이 직접 들어가지만, 참조 타입의 변수에는 메모리상의 객체 위치가 들어간다.
    • 원시 타입의 값을 더 효율적으로 저장하고 여기저기 전달 할 수 있다.
    • 하지만 원시 타입의 값에 대해서 메소드를 호출하거나 컬렉션에 원시 타입 값을 담을 수는 없다.
  • 코틀린은 원시 타입과 래퍼 타입을 구분하지 않는다.
    • 실행 시점에 숫자 타입은 가능한 한 가장 효율적인 방식으로 쵸현된다.
    • 대부분의 경우 코틀린의 Int 타입은 자바 int 타입으로 컴파일된다. 이런 컴파일이 불가능한 경우는 컬렉션과 같은 제네릭 클래스를 사용하는 경우뿐이다.

널이 될 수 있는 원시 타입: Int?, Boolean? 등

  • null 참조를 자바의 참조 타입의 변수에만 대입할 수 있기 때문에 널이 될 수 있는 코틀린 타입은 자바 원시 타입으로 표현할 수 없다.
  • 따라서 코틀린이 널이 될 수 있는 원시 타입을 사용하면 그 타입은 자바의 래퍼타입으로 컴파일된다.
  • 재내릭 클래스의 경우 래퍼 타입을 사용한다.
    • 이렇게 컴파일 하는 이유는 JVM에서 제네릭을 구현하는 방법 때문이다. JVM은 타입 인자로 원시 타입을 허용하지 않는다.

숫자 변환

  • 코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다.
  • 결과 타입이 허용하는 숫자의 범위가 원래 타입의 범위보다 넓은 경우조차도 자동 변환은 불가능하다.
    • 직접 변환 메소드를 호출해야 한다.
val i = 1  
val l: Long = i.toLong()
  • 코틀린은 모든 원시 타입(Boolean 제외)에 대한 변환 함수를 제공한다.
  • 코틀린은 개발자의 혼란으 피하기 위해 타입 변환을 명시하기로 결정했다.
  • 원시 타입 리터럴
    • L 접미사가 붙은 Long 타입 리터럴: 123L
    • 표준 부동소수점 표기법을 사용한 Double 타입 리터럴: 0.12, 2.0, 1.2e10, 1.2e-10
    • f나 F 접미사가 붙은 Float 타입 리터럴: 123.4f, .456F, 1e3f
    • 0x나 0X 접두사가 붙은 16진 리터럴: 0xCAFEBABE, 0xbcdL
    • 0b나 0B 접두사가 붙은 2진 리터럴: 0b00000101
  • 숫자 리터럴을 사용할 때는 보통 변환 함수를 호출할 필요가 없다. 직접 변환하지 않아도 숫자 리터럴을 타입이 알려진 변수에 대입하거나 함수에게 인자로 넘기면 컴파일러라 필요한 변환을 자동으로 넣어준다.
    • 산술 연산자는 적당한 타입의 값을 받아들일 수 있게 이미 오버로드돼 있다.
    • 코틀린 산술 연산자에서도 자바와 똑같이 숫자 연산시 overflow가 발생할 수 있다. 코틀린은 overflow를 검사하느라 추가 비용을 들이지 않는다.
  • 코틀린은 문자열을 원시 타입으로 변환하는 여러 함수를 제공한다.
    • toInt, toByte, toBoolean
    • 파싱에 실패하면 NumberFormatException이 발생한다.

Any, Any?: 최상위 타입

  • 자바에서는 참조 타입만 Object를 정점으로 하는 타입 계층에 포함되며, 원시 타입은 그런 계층에 들어있지 않다.
  • 코틀린에서는 AnyInt 등의 원시 타입을 포함한 모든 타입의 조상 타입이다.
  • 자바 메소드에서 Object를 인자로 반환하면 코틀린에서는 Any!로 취급한다.
  • 코틀린 함수가 Any를 사용하면 자바 바이트코드의 Object로 컴파일된다.
  • toString, equals, hashCode를 제외하고, Object에 있는 다른 메소드(wait, notify 등)은 Any에서 사용할 수 없다. 그런 메소드를 호출하고 싶다면 Object 타입으로 값을 캐스트해야 한다.

Unit 타입: 코틀린의 void

  • 코틀린 Unit 타입은 자바 void와 같은 기능을 한다.
  • 코틀린 함수의 반환 타입이 Unit이고 그 함수가 제네릭 함수를 오버라이드하지 않는다면 그 함수는 내부에서 자바 void 함수로 컴파일된다.
  • Unit의 차이점은, Unit은 일반적인 타입이며, void와 달리 Unit을 타입 인자로 쓸 수 있다.
  • Unit 타입에 속한 값은 단 하나뿐이며 그 이름도 Unit이다.
  • 이름이 Unit인 이유: 단 하나의 인스턴스만 갖는 타입이기 때문이다.

Nothing 타입: 이 함수는 결코 정상적으로 끝나지 않는다

  • 코틀린에는 결코 성공적으로 값을 돌려주는 일이 없으므로 ‘반환 값’이라는 개념 자체가 의미 없는 함수가 일부 존재한다.
  • 그런 경우를 표헌하기 위해 코틀린에는 Nothing이라는 특별한 반환 타입이 있다.
  • 인스턴스를 생성할 수 없고, 이 함수가 절대 값을 반환할 일이 없다는 것을 의미한다.

컬렉션과 배열

널 가능성과 컬렉션

  • 널이 될 수 있는 Int로 이뤄진 리스트: List<Int?>
  • Int로 이뤄진 널이 될 수 있는 리스트: List<Int>?
  • 널이 될 수 있는 Int로 이뤄진 널이 될 수 있는 리스트: List<Int?>?

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

  • 코틀린에서는 컬렉션 안의 데이터에 접근하는 인터페이스와 컬렉션 안의 데이터를 변경하는 인터페이스를 분리했다.
  • Collection 인터페이스를 사용하면 컬렉션 안의 원소에 대해 이터레이션하고, 컬렉션의 크기를 얻고, 어떤 값이 컬렉션 안에 들어있는지 검사하고, 컬렉션에서 데이터를 읽는 여러 다른 연산을 수행할 수 있다.
  • MutableCollection은 일반 인터페이스인 Collection을 확장하여 원소를 추가하거나, 삭제하거나, 컬렉션 안의 원소를 모두 지우는 등의 메소드를 더 제공한다.

  • 코드에서 가능하면 항상 읽기 전용 인터페이스를 사용하는 것을 일반적인 규칙으로 삼아라.
    • 프로그램에서 데이터에 어떤 일이 벌어지는지를 더 쉽게 이해하기 위함이다.
    • 어떤 컴포넌트의 내부 상태에 컬렉션이 포함된다면 그 컬렉션을 MutableCollection을 인자로 받는 함수에 전달할 때는 어쩌면 원본의 변경을 막기 위해 컬렉션을 복사해야할 수도 있다. (이런 패턴을 방어적 복사라고 부른다.)
  • 컬렉션 인터페이스를 사용할 때 읽기 전용 컬렉션이라고해서 꼭 변경 불가능한 컬렉션을 필요는 없다.
    • 이런 상황에서 컬렉션을 참조하는 다른 코드를 호출하거나 병렬 실행한다면 컬렉션을 사용하는 도중에 다른 컬렉션이 그 컬렉션의 내용을 변경하는 상황이 생길 수 있고, 이런 상황에서는 ConcurrentModificationException이나 다른 오류가 발생할 수 있다.
    • 따라서 읽기 전용 컬렉션이 thread safe 하지 않다는 점을 명심해야 한다.

코틀린 컬렉션과 자바

컬렉션 타입읽기 전용 타입변경 가능 타입
ListlistOfmutableListOf, arrayListOf
SetsetOfmutableSetOf, hashSetOf, linkedSetOf, sortedSetOf
MapmapOfmutableMapOf, hashMapOf,linkedMapOf, sortedMapOf
  • 읽기 전용 타입이어도, 내부에서는 변경 가능한 클래스다.
  • 자바는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않으므로, 코틀린에서 읽기전용 Collection으로 선언된 객체라도 자바 코드에서는 그 컬렉션 객체의 내용을 변경할 수 있다.
  • 널이 아닌 원소로 이뤄진 컬렉션 타입에서도 발생한다.
    • 널이 아닌 원소로 이뤄진 컬렉션을 자바 메소드로 넘겼는데 자바 메소드가 널을 컬렉션에 넣을 수도 있다.

컬렉션을 플랫폼 타입으로 다루기

  • 자바 쪽에서 선언한 컬렉션 타입의 변수를 코틀린에서는 플랫폼 타입으로 본다.
  • 플랫폼 타입인 컬렉션은 기본적으로 변경 가능성에 대해 알 수 없다. 따라서 코틀린 코드는 그 타입을 읽기 전용 컬렉션이나 변경 가능한 컬렉션 어느 쪽으로든 다룰 수 있다.
  • 오버라이드하려는 메소드의 자바 컬렉션 타입을 어떤 코틀린 컬렉션 타입으로 표현할지 결정해야 한다.
    • 컬렉션이 널이 될 수 있는가?
    • 컬렉션의 원소가 널이 될 수 있는가?
    • 오버라이드하는 메소드가 컬렉션을 변경할 수 있는가?

객체의 배열과 원시 타입의 배열

  • 코틀린에서 배열을 만드는 방법
    • arrayOf 함수에 원소를 넘기면 배열을 만들 수 있다.
    • arrayOfNulls 함수에 정수 값을 인자로 넘기면 모든 원소가 null이고 인자로 넘긴 값과 크기가 같은 배열을 만들 수 있다. 물론 원소 타입이 널이 될 수 있는 타입인 경우엠나 이 함수를 쓸 수 있다.
    • Array 생성자는 배열 크기와 람다를 인자로 받아서 람다를 호출해서 각 배열 원소를 초기화해준다. arrayOf를 쓰지 않고 각 원소나 널이 아닌 배열을 만들어야 하는 경우 이 생성자를 사용한다.
  • Array<Int>같은 타입을 선언하면 그 배열은 박싱된 정수 배열이 된다.
  • 코틀린은 원시 타입의 배열을 표현하는 별도 클래스를 각 원시 타입마다 하나씩 ㅔ공한다.
    • IntArray, ByteArray, CharArray, BooleanArray
  • 박싱되지 않은 원시 타입의 배열을 만드는 방법
    • 각 배열 타입의 생성자는 size 인자를 받아서 해당 원시 타입의 디폴트 값(보통은 0)으로 초기화된 size 크기의 배열을 반환한다.
    • 팩토리 함수(IntArray를 생성하는 intArrayOf 등) 여러 값을 가변 인자로 받아서 그런 값이 들어간 배열을 반환한다.
    • 크기와 람다를 인자로 받는 생성자를 사용한다.