Primitive Type은 기본형 혹은 원시형이라고 번역할 수 있는데, 기본형은 Kotlin의 Basic types으로 해석될 여지가 있기 때문에 여기서는 원시형 혹은 Primitive type이라고 말하겠다.
원시형 자료형으로는 논리형 boolean, 정수형 byte, short, int, long, 실수형 float, double, 문자형 char이 존재한다. 값 자체를 변수에 저장하고 그에 따라 빠르게 접근이 가능하기 때문에 메모리가 많이 절약되고 효율적이다. 기본적으로 메모리의 스택(Stack) 영역에 저장된다. 하지만 null을 가질 수 없고 기본값이 정해져 있다.

그런데 위에서 원시형은 null을 가질 수 없다고 되어 있는데 Kotin을 사용할 때 우리는 흔히 Int?, Double? 등 null을 가질 수 있는 원시형을 사용한다. 여기서 뇌정지가 발생한다.
엄연히 말하자면 Kotlin에서 원시형을 제공하지 않는다. Java처럼 직접적으로 int age = 25; 이렇게 사용이 불가능하다.
대신 Int, Float 같은 Boxed type을 사용하여 원시형을 사용한다. Boxed type은 원시형을 객체로 감싸서 사용하는 방식이다.
그렇다면 여기서 의문이 하나 생긴다.
Boxed type은 객체이기 때문에 참조형이다. 그러면 Heap에 데이터를 두고 그것을 참조해야 하고 원시형을 사용하는 것보다 비효율적이다. Kotlin은 이러한 원시형의 이점을 버린 것일까?
그렇지 않다. 참조형으로 선언한 Boxed type은 컴파일러에 의해 JVM 상에서 원시형으로 변환되고 Java의 원시형과 마찬가지로 스택(Stack) 영역에 저장된다.
fun main() {
val a: Int = 1000
val b: Int = 1000
println(a === b) // true
}
위 예시에서 a와 b는 원시형 int로 변환되므로 ===로 동일성 비교를 하면 동등성 비교로 바뀌어서 작동한다. 그래서 true가 출력된다.
public static final void main() {
int a = 1000;
int b = 1000;
boolean var2 = a == b;
System.out.println(var2);
}
Java로 디컴파일된 코드를 보면 원시형 int로 선언된 것을 알 수 있다. Java의 ==는 동일성 비교이지만 원시형이라서 동등성 비교로 바뀌어서 작동한다.
fun main() {
val a: Int? = 1000
val b: Int? = 1000
println(a === b) // false
}
Int?, Boolean? 같은 nullable 타입은 객체이기 때문에 Heap 영역에 저장된다. 그래서 위 코드는 객체의 동일성 비교이고, 둘은 다른 객체이므로 false가 출력된다.
public static final void main() {
Integer a = 1000;
Integer b = 1000;
boolean var2 = a == b;
System.out.println(var2);
}
디컴파일된 코드를 보면 int가 아닌 Integer 타입으로 선언된 것을 볼 수 있다.
정리하자면 Int, Float, Boolean 같은 non-null 타입은 컴파일 과정에서 원시형으로 변환되어 스택 영역에 저장되고, Int?, Float?, Boolean? 같은 nullable 타입은 참조형으로 힙 영역에 값을 저장하고 객체의 주소값을 갖는 변수는 스택에 저장한다.
근데 Kotlin 관련 글들을 보면 primitive type이라는 용어를 사용하는 경우가 많은데, 엄연히 말하면 Kotlin에서는 prmitive type이 없지만, 개념적으로 이해하기 쉽도록 primitive type이라는 용어를 사용하는 것 뿐이다.
Kotlin에 원시형은 없지만 위에서도 설명했듯이 여기서는 개념적으로 이해하기 쉽도록 원시형으로 변환되는 참조형은 제외하고 설명하겠다.
이외에도 여러가지가 있는데, 사실 원시형이 아니라면 참조형이라고 생각해도 상관없다. 참조형은 힙 영역에 값을 저장하고 객체의 주소값을 갖는 변수는 스택에 저장한다. 단, String은 예외이다.
String도 참조형은 맞지만 일반적인 참조형과는 다른 부분이 있다. 일반적인 참조형이라고 가정하고 실험을 하나 해보자.
fun main( ) {
val str1: String = "Hello"
val str2: String = "World"
val str3: String = "Hello"
println(str1 === str2) // false
println(str1 === str3) // true
}
일반적인 참조형이라면 str1과 str3은 실제로는 다른 객체를 참조하고 있기 때문에 동일성 비교에서 당연히 false가 출력될 것이다. 하지만 String에서는 동일성 비교에서 true가 출력된다.
이것은 Kotlin이 String Pool 방식을 지원하기 때문이다. 스택에 주소값을 갖는 변수를 저장하고 Heap에 저장되어 있는 값을 참조하는 것은 다른 참조형과 똑같지만, String의 값은 Heap 영역의 String Pool이라는 공간에 저장되어 있다. 아래 그림을 참고하자.
그래서 Heap 메모리에 값이 저장되기 전에 동일한 값의 존재 여부를 확인하여 새로운 Heap 영역을 할당하지 않고 기존 String을 참조한다. String은 자주 사용되는 자료형이기 때문에 String Pool을 통해 이러한 방식으로 메모리를 재활용한다. 예시에서 str1의 참조 주소와 str3의 참조 주소는 동일하기 때문에 === 동일성 비교에서 true가 결과로 출력되는 것이다.