Kotlin Type을 다루는 방법

코틀린을 배우기 위해서 인프런에서 강의를 구매하고 코틀린과 친해지고 기본기를 다지기 위해서 공부하는 중이다. 글 내용은 type을 다루는 방법이고 최태현님의 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide) 강의의 일부이다

기본 타입

Kotlin의 기본 타입은 Java의 기본 타입과 동일하지만, Kotlin에서는 기본 타입과 참조 타입이 구분되지 않는다

  • Java의 기본 타입간의 변환은 암시적으로 이루어지지만, Kotlin 기본 타입간의 변환은 명시적으로 이루어져야 한다
Java
  • Integer 타입은 Long 타입으로 암시적으로 변환된다. Java는 작은 타입에서 큰 타입으로의 변환을 자동 수행
  • 하지만, 큰 타입에서 작은 타입으로의 변환은 명시적으로 해야 한다
int number1 = 4;
long number2 = number1;

long number1 = 100L;
int number2 = number1; // 컴파일 에러
int number2 = (int) number1 // 명시적 캐스팅

명시적 캐스팅을 강제하는 이유

  • 데이터 손실 방지: 큰 타입에서 작은 타입으로 변환할 때 값이 잘리 수 있음을 개발자가 의식적으로 인지하도록 강제
  • 안전성: 암시적 변환을 허용하면 의도하지 않은 데이터 손실이 발생할 수 있어, Java는 이를 방지하기 위해 명시적 캐스팅을 요구
Kotlin
  • Integer 타입을 Long 타입으로 변환할 때는 toLong()을 사용해야 한다. 그렇지 않으면 type missmatch 에러가 발생한다
  • 변수가 nullable 타입인 경우 적절한 처리가 필요하다
val number1 = 3 // Int 타입

val number2: Long = number1 // Type mismatch 에러 발생
val number2: Long = number1.toLong()

val number3: Int? = 3 // Int 타입, null 허용
val number4: Long = number3.toLong() // number3이 null일 수 있으므로 NPE가 발생할 수 있다
val number4: Long = number3?.toLong() ?: 0L
  • Kotlin에서의 타입 변환은 Java보다 엄격하지만, 그만큼 더 예측 가능하고 안전한 코드를 만들 수 있다

Java person class에서 type 캐스팅

public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public static void printAGeIfPerson(Object obj) {

        // instanceof 연산자를 사용하여 obj가 Person 타입인지 확인
        if (obj instanceof Person) {

            // obj를 Person 타입으로 캐스팅
            Person person = (Person) obj;
            System.out.println(person.getAge());

            // obj.getAge() 불가능
        }

        // if (obj instanceof Person)의 반대 경우
        if (!(obj instanceof Person)) {

        }
    }
}

타입 캐스팅

  • Kotlin에서는 타입 캐스팅을 할 때 as 키워드를 사용한다
  • Kotlin에서는 smart cast를 지원한다
  • smart cast는 컴파일러가 변수의 타입을 자동으로 추론하여 캐스팅 하는 기능
  • 예시로 if문에서 변수가 특정 타입으로 체크되면, 이후 코드에서 해당 변수를 해당 타입으로 자동으로 캐스팅해준다

valus is type

  • value가 Type이면 true를 반환하고 아니면 false를 반환한다

valus !is type

  • value가 Type이면 false를 반환하고 아니면 true를 반환한다

valus as type

  • value가 Type이면 type으로 캐스팅하고 아니면 ClassCastException 발생한다

valus as? type

  • value가 Type이면 type으로 캐스팅하고 value가 null이거나 value가 type이 아니면 null 반환한다

Kotlin Smart Cast 활용

fun printAgeIfPerson(Obj: Any) {

    // is는 Java instanceof와 동일한 기능을 함
    if (obj is Person) {

        // Start case 덕분에 obj가 자동으로 Person 타입으로 인식
        println(obj.age) // as 캐스팅 불필요

        // 아래 방식들은 모두 동일하지만 불필요한 코드
        // val person = obj as Person
        // val person = obj
    }

}

fun printAgeIfPersonReverse(obj: Any) {

    // if (!(obj instanceof Person))
    if (obj !is Person) {
        return
    }
}

fun printAgeIfPersonNull(obj: Any?) {

    // obj가 null이 아니라면 Person 타입으로 안전하게 캐스팅 아닐경우 null을 반환
    val person = obj as? Person // Safe call
    println(person?.age) // person이 null이 아니라면 age를 출력, null이라면 아무것도 출력하지 않음
}

각 연산자 비교

fun demonstrateTypeCasting() {
    val obj: Any = Person("혁", 100)
    val nullObj: Any? = null
    val stringObj: Any = "Hello"

    // 1. is 연산자 타입 체크
    println(obj is Person) // true
    println(stringObj is Person) // false

    // 2. !is 연산자 타입 체크
    println(obj !is Person) // false
    println(stringObj !is Person) // true

    // 3. as 연산자 - 강제 캐스팅
    val person1 = obj as Person // 성공
    val person2 = stringObj as person // ClassCastException

    // 4. as? 연산자 - 안전 캐스팅
    val person3 = obj as? Person // Person 객체 반환
    val person4 = stringObj as? Person // null 반환
    val person5 = nullObj as? Person // null 반환
}

실무 활용 패턴

// 1. Smart cast + early return
fun processIfPerson(obj: Any) {

    if (obj !is Person) return // early return

    // 이후 코드에서 obj는 자동으로 Person 타입
    println("이름: ${obj.name}, 나이: ${obj.age}")
}

// 2. as? + safe call 조합
fun safeProcessperson(obj: Any?) {

    val person = obj as? Person
    person?.net {
        println("안전하게 처리: ${it.name}")
    }
}

// 3. when 표현식과 함께
fun handleDifferentType(obj: Any) {

    when (obj) {
        is String -> println("문자열: $obj")
        is Person -> println("사람: ${obj.name}")
        is Int -> println("숫자: $obj")
        else -> println("알 수 없는 타입")
    }
}
  • Smart cast is 체크 후 자동 타입 변환
  • Safe call: as?로 null 안전성 확보
  • 간결한 문법: !is로 부정 체크 간편화

Kotlin에 3가지 특이한 타입

Any
Kotlin의 모든 타입은 Any 타입을 상속받는다. Java의 Object와 유사하다 (모든 객체의 최상위 타입)
val string: Any = "Hello" // String → Any
val number: Any = 42 // Int → Any
val person: Any = Person("혁") // Person → Any
val list: Any = listOf(1, 2, 3) // List → Any
모든 Primitive Type의 최상위 타입이기도 하다. 단, Java의 Primitive 타입은 Object를 최상위 타입으로 두지 않는다
// Kotlin: Primitive도 Any 상속
val intValue: Any = 10

// Java
Object obj = 10; // 오토박싱으로 Integer 객체로 변환
// int primitive는 Object를 상속받지 않음
Any는 null을 포함할 수 없기 때문에 null을 포함하고 싶으면 Any?로 선언해야 한다. null을 포함할 수 있는 타입은 Any?이다
val nonNull: Any = "Hello" // 가능
val withNull: Any = null // 컴파일 에러

val nullable: Any? = null // Any?는 null 허용
val nullable2: Any? = "Hello" // non-null 값도 허용

Unit
Java의 void와 유사하지만 void와 다르게 Unit 타입 인자로 사용할 수 있다
// Kotlin: Unit을 타입 인자로 직접 사용
fun proccessAsync(): CompletableFuture<Unit> {
    return CompletableFuture.completableFuture(Unit)
}

val callback: (String) -> Unit = { println(it) }

// Java: void는 타입 인자로 사용 불가
// CompletableFuture<void> future 컴파일 에러
CompletableFuture<Void> future // Void 클래스 사용

Function<String, Void> callback = s -> {
    System.out.println(s);
    return null; // Void는 null 반환 필요
}
Java에서는 void말고 Void class가 따로 있지만 Kotlin은 Unit을 직접 사용 가능하다
// Unit은 실제 인스턴스를 가진다
val unitValue: Unit = Unit
println(unitValue) // kotlin.Unit

// 함수에서 자동 반환
fun printHello(): Unit { // Unit 타입 생략도 가능하다
    println("Hello")
    // return Unit 생략 가능 - 자동으로 Unit 반환
}

// Java
Void voidValue = null; // 유일한 값 null
// Void voidValue2 = new Void(); // 생성자 없음

// 함수에서 Void 반환
public Void doSomething() {
    System.out.println("장업 수행");
    return null; // 명시적으로 null 반환 필요
}
함수형 프로그래밍에서 Unit은 단 하나의 인스턴스만 가지는 타입을 의미한다. 즉, Kotlin의 Unit은 실제로 존재하는 타입임을 나타낸다
// Unit은 "의미 있는 반환값이 없음"을 타입으로 표현
val actions: List<() -> Unit> = ListOf(
    { println("액션 1") }
    { println("액션 2") }
    { println("액션 3") }
)

// 모든 액션 실행
actions.forEach { action -> action() } // 각각 Unit 반환
Nothing
  • Kotlin의 Nothing 타입은 어떤 값도 가질 수 없는 타입이다
  • Nothing 타입은 함수가 절대 반환하지 않는다는 것을 나타낸다
    • 함수가 정상적으로 끝나지 않았다는 사실을 표현하는 역할이다
  • 무조건 예외를 반환하는 함수 / 무한 루프 함수 등에서 사용된다
// 예외를 던지는 함수
fun fail(message: String): Nothing {
    throw IllegalArgumentException(message)
}

// 무한 루프 함수
fun infiniteLoop(): Nothing {
    while (true) {
        println("무한 실행")
    }
}

// 프로그램 종료 함수
fun exitProgram(): Nothing {
    System.exit(1)
}
Nothing의 실용적 활용
// Elvis operator와 함계 사용
fun getName(person: Person?): String {
    return person?.name ?: fail("Person이 null입니다")
    // fail()이 Nothing을 반환하므로 컴파일러가 안전하다고 인식
}

// when 표현식에서 활용
fun handleValue(value: Any): String {
    return when (value) {
        is String -> value.uppercase()
        is Int -> value.toString()
        else -> fail("지원하지 않는 타입: ${value::class}")
    }
}
Nothing의 타입시스템에서의 역할
// Nothing은 모든 타입의 하위 타입 (Bottom Type)
val stringOrNothing: String = fail("에러") // Nothing → String 자동 변환
val intOrNothing: Int = fail("에러") // Nothing → Int 자동 변환

// 컬렉션에서도 활용
val list: List<String> = listOf("A", "B") ?: fail ("리스타가 null")

String Interpolation

  • Kotlin에서는 String Interpolation을 지원한다
  • String Interpolation은 ${}를 사용하여 변수나 표현식을 문자열에 삽입하는 방법
  • String Interpolation은 가독성이 좋고, 코드가 간결해진다
  • 문자열을 생성할 때 StringBuilder를 사용하지 않고, 문자열을 생성할 수 있다. 또한 문자열 생성할 때 성능이 더 좋다
    • 내부적으로 StringBuilder를 자동 사용하여 성능이 좋다
// Java
Person person = new Person("혁", 5);
String message1 = String.format("사람의 이름은 %s이고 나이는 %s세 입니다", person.getName(), person.getAge());

StringBuilder builder = new StringBuilder();
builder.append("사람의 이름은 ");
builder.append(person.getName());
builder.append("이고 나이는 ");
builder.append(person.getAge());
builder.append("세 입니다");

// Kotlin
val person2 = Person("혁 개발자", 100)
val message2 = "사람의 이름은 ${person2.name}이고 나이는 ${person2.age}세 입니다"

val name = "혁 개발자"
// String Interpolation - 내부적으로 StringBuilder 사용
val message3 = "사람의 이름은 $name이다."

// 실제 컴파일 결과 (개념적)
val message3 = StringBuilder()
    .append("사람의 이름은")
    .append(name)
    .append("이다.")
    .toString()
${} 사용 규칙
val name = "혁"
val age = 100

// ${} 필수인 경우들
println("${name}님") // 변수 뒤에 문자가 올 때
println("${name.uppercase()}")  // 메서드 호출이나 표현식
println("${age + 10}") 연산식

// ${} 선택사항
println(이름: $name) // 단순 변수
println("이름: $name{}") //통일성을 위해 사용 가능
Raw String
val name = "혁"
val str = """
    가나다
    타파하
    $name
""".trimIndent()

// Raw String("")의 장점
val json = """
    "name": "$name",
    "age" : "$age",
    "active": true
""".trimIndent()

// 이스케이프 문자 불필요
val regex = """\\d{3}-\\d{4}-\\d{4}""" // 정규식에서 유용

// 디버깅에서 유용
val result = complexCalculation()
println("결과: $result, 타입: ${result::class}")

String Indexing

  • String Indexing은 문자열의 특정 위치에 있는 문자를 가져오는 방법이다
  • Kotlin에서는 문자열의 특정 위치에 있는 문자를 가져올 때, []를 사용한다
val str = "ABCDE"

// Kotlin: 배열처럼 [] 사용
println(str[0]) // A
println(str[4]) // E

// java 방식도 여전히 사용 가능
println(str.get(1)) // B (내부적으로 charAt 호출)
println(str.charAt(1)) // B (Java 호환 - Java 사용방식)

Kotlin의 String Indexing은 배열과 일관된 문법을 제공하여 가독성을 높이면서도 내부적으로 Java의 charAt과 동일한 성능을 보장한다

출처 – 인프런 강의 중 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide)