예외·오류 처리

예외·오류 처리 (Exception and error handling)

예외(exception)는 프로그램 실행을 방해할 수 있는 런타임 오류가 발생해도 코드가 더 예측 가능하게 동작하도록 도와줍니다. Kotlin은 모든 예외를 기본적으로 unchecked로 취급해요. unchecked 예외는 예외 처리 과정을 단순화합니다. 예외를 catch할 수는 있지만, 명시적으로 처리하거나 선언할 필요는 없죠.

Java, Swift, Objective-C와 함께 쓸 때 Kotlin이 예외를 어떻게 다루는지는 Exception interoperability with Java, Swift, and Objective-C 섹션에서 확인하세요.

예외를 다루는 일은 두 가지 핵심 동작으로 이루어집니다.

  • 예외 던지기(throwing) — 문제가 발생했음을 알리는 것.
  • 예외 잡기(catching) — 문제를 해결하거나 개발자·사용자에게 알리는 방식으로, 예상치 못한 예외를 직접 처리하는 것.

예외는 Exception 클래스의 서브클래스로 표현되며, Exception은 다시 Throwable 클래스의 서브클래스예요. 계층 구조에 대한 자세한 내용은 Exception hierarchy 섹션을 참고하세요. Exceptionopen class이므로, 애플리케이션의 특정 요구에 맞는 커스텀 예외를 만들 수 있어요.

출처: Kotlin 공식 문서

본문

예외 던지기

throw 키워드로 예외를 직접 던질 수 있어요. 예외를 던진다는 것은 코드에 예상치 못한 런타임 오류가 발생했음을 나타내는 일입니다. 예외는 객체이며, 던지면 예외 클래스의 인스턴스가 만들어져요.

파라미터 없이 예외를 던질 수 있어요.

throw IllegalArgumentException()

문제의 원인을 더 잘 이해하려면, 커스텀 메시지와 원래 원인(cause) 같은 추가 정보를 포함하세요.

val cause = IllegalStateException("Original cause: illegal state")

// userInput이 음수이면 IllegalArgumentException을 던진다
// 또한 cause인 IllegalStateException으로 표현된 원래 원인을 보여준다
if (userInput < 0) {
    throw IllegalArgumentException("Input must be non-negative", cause)
}

이 예시에서 사용자가 음수 값을 입력하면 IllegalArgumentException이 던져집니다. 커스텀 오류 메시지를 만들고 예외의 원래 원인(cause)을 유지할 수 있는데, 그것은 스택 트레이스에 포함돼요.

전제 조건 함수로 예외 던지기

Kotlin은 전제 조건 함수(precondition function)를 사용해 예외를 자동으로 던지는 추가적인 방법을 제공합니다. 전제 조건 함수는 다음을 포함해요.

전제 조건 함수 용도 던져지는 예외
require() 사용자 입력의 유효성 검사 IllegalArgumentException
check() 객체·변수 상태의 유효성 검사 IllegalStateException
error() 불법 상태나 조건을 나타냄 IllegalStateException

이 함수들은 특정 조건이 충족되지 않으면 프로그램 흐름이 계속될 수 없는 상황에 적합합니다. 코드가 더 간결해지고 이런 검사 처리가 효율적이죠.

require() 함수

실행에 결정적인 입력 인자를 검증할 때, 그 인자가 유효하지 않으면 함수가 진행될 수 없을 때 require() 함수를 사용하세요.

require()의 조건이 충족되지 않으면 IllegalArgumentException을 던집니다.

fun getIndices(count: Int): List<Int> {
    require(count >= 0) { "Count must be non-negative. You set count to $count." }
    return List(count) { it + 1 }
}

fun main() {
    // 이것은 IllegalArgumentException으로 실패한다
    println(getIndices(-1))

    // 아래 줄의 주석을 해제하면 동작하는 예시를 볼 수 있다
    // println(getIndices(3))
    // [1, 2, 3]
}

require() 함수는 컴파일러가 스마트 캐스팅을 수행하도록 해요. 검사가 성공한 뒤 변수는 자동으로 널 불가 타입으로 캐스팅됩니다. 이 함수들은 주로 진행 전에 변수가 null이 아닌지 확인하는 널 가능성 검사에 쓰여요. 예를 들어:

fun printNonNullString(str: String?) {
    // 널 가능성 검사
    require(str != null)
    // 이 검사가 성공한 뒤 'str'은 null이 아니라고 보장되며
    // 자동으로 널 불가 String으로 스마트 캐스팅된다
    println(str.length)
}
check() 함수

객체나 변수의 상태를 검증할 때 check() 함수를 사용하세요. 검사가 실패하면 해결해야 할 논리 오류를 나타냅니다.

check() 함수에 지정된 조건이 false이면 IllegalStateException을 던져요.

fun main() {
    var someState: String? = null

    fun getStateValue(): String {

        val state = checkNotNull(someState) { "State must be set beforehand!" }
        check(state.isNotEmpty()) { "State must be non-empty!" }
        return state
    }
    // 아래 줄의 주석을 해제하면 프로그램이 IllegalStateException으로 실패한다
    // getStateValue()

    someState = ""

    // 아래 줄의 주석을 해제하면 프로그램이 IllegalStateException으로 실패한다
    // getStateValue()
    someState = "non-empty-state"

    // 이것은 "non-empty-state"를 출력한다
    println(getStateValue())
}

check() 함수는 컴파일러가 스마트 캐스팅을 수행하도록 해요. 검사가 성공한 뒤 변수는 자동으로 널 불가 타입으로 캐스팅됩니다. 이 함수들은 주로 진행 전에 변수가 null이 아닌지 확인하는 널 가능성 검사에 쓰여요. 예를 들어:

fun printNonNullString(str: String?) {
    // 널 가능성 검사
    check(str != null)
    // 이 검사가 성공한 뒤 'str'은 null이 아니라고 보장되며
    // 자동으로 널 불가 String으로 스마트 캐스팅된다
    println(str.length)
}
error() 함수

error() 함수는 코드에서 논리적으로 발생해서는 안 되는 불법 상태나 조건을 알리는 데 사용해요. 코드가 예상치 못한 상태를 만났을 때 의도적으로 예외를 던지고 싶은 시나리오에 적합합니다. 이 함수는 특히 when 식에서 유용한데, 논리적으로 일어나서는 안 되는 경우를 처리하는 명확한 방법을 제공하죠.

다음 예시에서 error() 함수는 정의되지 않은 사용자 역할을 처리하는 데 사용됩니다. 역할이 미리 정의된 것 중 하나가 아니면 IllegalStateException이 던져져요.

class User(val name: String, val role: String)

fun processUserRole(user: User) {
    when (user.role) {
        "admin" -> println("${user.name} is an admin.")
        "editor" -> println("${user.name} is an editor.")
        "viewer" -> println("${user.name} is a viewer.")
        else -> error("Undefined role: ${user.role}")
    }
}

fun main() {
    // 이것은 예상대로 동작한다
    val user1 = User("Alice", "admin")
    processUserRole(user1)
    // Alice is an admin.

    // 이것은 IllegalStateException을 던진다
    val user2 = User("Bob", "guest")
    processUserRole(user2)
}

try-catch 블록으로 예외 처리하기

예외가 던져지면 프로그램의 정상 실행이 중단됩니다. trycatch 키워드로 예외를 우아하게 처리해 프로그램을 안정적으로 유지할 수 있어요. try 블록은 예외를 던질 수 있는 코드를 담고, catch 블록은 예외가 발생하면 그것을 잡아 처리합니다. 예외는 그 특정 타입이나 그 예외의 수퍼클래스와 일치하는 첫 번째 catch 블록에 잡혀요.

trycatch 키워드를 함께 쓰는 방법은 다음과 같아요.

try {
    // 예외를 던질 수 있는 코드
} catch (e: SomeException) {
    // 예외를 처리하는 코드
}

try-catch를 식(expression)으로 사용해, try 블록이나 catch 블록 중 하나에서 값을 반환하게 하는 것은 흔한 방식입니다.

fun main() {
    val num: Int = try {

        // count()가 성공적으로 완료되면 그 반환 값이 num에 할당된다
        count()

    } catch (e: ArithmeticException) {

        // count()가 예외를 던지면 catch 블록이 -1을 반환하고
        // 그것이 num에 할당된다
        -1
    }
    println("Result: $num")
}

// ArithmeticException을 던질 수 있는 함수를 시뮬레이션한다
fun count(): Int {

    // 이 값을 바꾸면 num에 다른 값이 반환된다
    val a = 0

    return 10 / a
}

예외 인스턴스를 사용하지 않고 예외를 처리할 수도 있어요. 예를 들어 catch 블록에서 폴백 값이나 일반적인 오류 메시지를 제공할 수 있죠.

예외 인스턴스를 의도적으로 무시한다는 뜻으로, 예외 파라미터 이름 대신 밑줄(_)을 사용합니다.

import java.io.File
import java.io.IOException

//sampleStart
fun main() {
    val userSettings = try {
        File("user-settings.json").readText()

    // 예외 인스턴스를 사용하지 않고 IOException을 잡는다
    } catch (_: IOException) {
        // 파일 로딩에 실패하면 폴백 값을 사용한다
        "{}"
    }

    println(userSettings)
}
//sampleEnd

같은 try 블록에 여러 catch 핸들러를 사용할 수 있어요. 서로 다른 예외를 구분해 처리하도록 catch 블록을 필요한 만큼 추가할 수 있죠. 여러 catch 블록이 있을 때는 코드에 위에서 아래로, 가장 구체적인 예외부터 가장 덜 구체적인 예외 순서로 정렬하는 게 중요합니다. 이 순서가 프로그램의 실행 흐름과 맞아떨어져요.

커스텀 예외가 있는 다음 예시를 볼게요.

open class WithdrawalException(message: String) : Exception(message)
class InsufficientFundsException(message: String) : WithdrawalException(message)

fun processWithdrawal(amount: Double, availableFunds: Double) {
    if (amount > availableFunds) {
        throw InsufficientFundsException("Insufficient funds for the withdrawal.")
    }
    if (amount < 1 || amount % 1 != 0.0) {
        throw WithdrawalException("Invalid withdrawal amount.")
    }
    println("Withdrawal processed")
}

fun main() {
    val availableFunds = 500.0

    // 이 값을 바꾸면 다른 시나리오를 테스트할 수 있다
    val withdrawalAmount = 500.5

    try {
        processWithdrawal(withdrawalAmount.toDouble(), availableFunds)

    // catch 블록의 순서가 중요하다!
    } catch (e: InsufficientFundsException) {
        println("Caught an InsufficientFundsException: ${e.message}")
    } catch (e: WithdrawalException) {
        println("Caught a WithdrawalException: ${e.message}")
    }
}

WithdrawalException을 처리하는 일반적인 catch 블록은, 더 구체적인 catch 블록이 먼저 잡지 않는 한, 자신의 타입의 모든 예외(InsufficientFundsException 같은 구체적인 것 포함)를 잡습니다.

finally 블록

finally 블록은 try 블록이 성공적으로 완료되든 예외를 던지든 항상 실행되는 코드를 담아요. finally 블록으로 trycatch 블록 실행 후 정리(cleanup) 코드를 넣을 수 있습니다. 파일이나 네트워크 연결 같은 리소스를 다룰 때 특히 중요한데, finally가 그것들이 제대로 닫히거나 해제되도록 보장하거든요.

일반적으로 try-catch-finally 블록을 함께 쓰는 방법은 이렇습니다.

try {
    // 예외를 던질 수 있는 코드
}
catch (e: YourException) {
    // 예외 핸들러
}
finally {
    // 항상 실행되는 코드
}

try 식의 반환 값은 try 또는 catch 블록에서 마지막으로 실행된 식에 의해 결정됩니다. 예외가 없으면 결과는 try 블록에서, 예외가 처리되면 catch 블록에서 나와요. finally 블록은 항상 실행되지만 try-catch 블록의 결과를 바꾸지는 않습니다.

예시로 확인해 볼게요.

fun divideOrNull(a: Int): Int {

    // try 블록은 항상 실행된다
    // 여기서 예외(0으로 나누기)가 나면 즉시 catch 블록으로 점프한다
    try {
        val b = 44 / a
        println("try block: Executing division: $b")
        return b
    }

    // catch 블록은 ArithmeticException(a == 0이면 0으로 나누기) 때문에 실행된다
    catch (e: ArithmeticException) {
        println("catch block: Encountered ArithmeticException $e")
        return -1
    }
    finally {
        println("finally block: The finally block is always executed")
    }
}

fun main() {

    // 이 값을 바꾸면 다른 결과를 얻는다. ArithmeticException이면 -1이 반환된다
    divideOrNull(0)
}

Kotlin에서 AutoClosable 인터페이스를 구현하는 리소스(예: FileInputStream·FileOutputStream 같은 파일 스트림)를 관리하는 관용적 방법은 .use() 함수를 쓰는 것입니다. 이 함수는 예외가 던져지든 아니든 코드 블록이 완료되면 리소스를 자동으로 닫아, finally 블록의 필요성을 없애 줍니다. 그래서 Kotlin은 리소스 관리를 위해 Java의 try-with-resources 같은 특별한 문법을 요구하지 않아요.

FileWriter("test.txt").use { writer ->
    writer.write("some text")
    // 이 블록 후에 .use 함수가 finally 블록처럼 writer.close()를 자동으로 호출한다
}

코드가 예외를 처리하지 않고 리소스 정리만 필요하다면, catch 블록 없이 tryfinally 블록을 쓸 수도 있어요.

class MockResource {
    fun use() {
        println("Resource being used")
        // 리소스가 사용되고 있음을 시뮬레이션한다
        // 0으로 나누면 ArithmeticException을 던진다
        val result = 100 / 0

        // 예외가 던져지면 이 줄은 실행되지 않는다
        println("Result: $result")
    }

    fun close() {
        println("Resource closed")
    }
}

fun main() {
    val resource = MockResource()
//sampleStart
    try {

        // 리소스를 사용하려 시도한다
        resource.use()

    } finally {

        // 예외가 발생하더라도 리소스가 항상 닫히도록 보장한다
        resource.close()
    }

    // 예외가 던져지면 이 줄은 출력되지 않는다
    println("End of the program")
//sampleEnd
}

보시다시피 finally 블록은 예외 발생 여부와 관계없이 리소스가 닫히도록 보장합니다. Kotlin에서는 필요에 따라 catch 블록만, finally 블록만, 또는 둘 다 사용할 수 있는 유연함이 있어요. 다만 try 블록은 반드시 catch 블록이나 finally 블록 중 적어도 하나를 동반해야 합니다.

커스텀 예외 만들기

Kotlin에서 내장된 Exception 클래스를 확장하는 클래스를 만들어 커스텀 예외를 정의할 수 있어요. 이를 통해 애플리케이션의 요구에 맞는 더 구체적인 오류 타입을 만들 수 있습니다.

만들려면 Exception을 확장하는 클래스를 정의하면 됩니다.

class MyException: Exception("My message")

이 예시에는 "My message"라는 기본 오류 메시지가 있지만, 원한다면 비워 둘 수도 있어요.

Kotlin의 예외는 상태ful한 객체로, 생성 당시 맥락에 특화된 정보(이를 스택 트레이스라고 합니다)를 담습니다. 객체 선언으로 예외를 만들지 마세요. 필요할 때마다 예외의 새 인스턴스를 만들어야, 예외의 상태가 특정 맥락을 정확히 반영할 수 있어요.

커스텀 예외는 ArithmeticException 서브클래스처럼 기존 예외 서브클래스의 서브클래스가 될 수도 있어요.

class NumberTooLargeException: ArithmeticException("My message")

커스텀 예외의 서브클래스를 만들고 싶다면 상위 클래스를 open으로 선언해야 해요. 클래스는 기본적으로 final이라, 그렇지 않으면 서브클래스를 만들 수 없으니까요.

예를 들어:

// 커스텀 예외를 open 클래스로 선언해 서브클래스를 만들 수 있게 한다
open class MyCustomException(message: String): Exception(message)

// 커스텀 예외의 서브클래스를 만든다
class SpecificCustomException: MyCustomException("Specific error message")

커스텀 예외는 내장 예외처럼 동작합니다. throw 키워드로 던질 수 있고, try-catch-finally 블록으로 처리할 수 있죠. 예시로 확인해 볼게요.

class NegativeNumberException: Exception("Parameter is less than zero.")
class NonNegativeNumberException: Exception("Parameter is a non-negative number.")

fun myFunction(number: Int) {
    if (number < 0) throw NegativeNumberException()
    else if (number >= 0) throw NonNegativeNumberException()
}

fun main() {

    // 이 함수의 값을 바꾸면 다른 예외를 얻는다
    myFunction(1)
}

다양한 오류 시나리오가 있는 애플리케이션에서는 예외 계층을 만드는 것이 코드를 더 명확하고 구체적으로 만들어 줄 수 있어요. 공통 예외 기능의 기반으로 추상 클래스sealed class를 사용하고, 구체적인 예외 유형을 위한 특정 서브클래스를 만드는 방식으로요. 또한 기본값 있는 파라미터를 포함한 커스텀 예외는 다양한 메시지로 초기화할 수 있어 유연성을 제공하며, 더 세밀한 오류 처리를 가능하게 해요.

sealed class AccountException을 예외 계층의 기반으로, 그 서브클래스 APIKeyExpiredException은 기본값 있는 파라미터 사용을 보여주는 예시를 볼게요.

//sampleStart
// 계정 관련 오류를 위한 예외 계층의 기반으로 sealed class를 만든다
sealed class AccountException(message: String, cause: Throwable? = null):
Exception(message, cause)

// AccountException의 서브클래스를 만든다
class InvalidAccountCredentialsException : AccountException("Invalid account credentials detected")

// AccountException의 서브클래스를 만든다. 커스텀 메시지와 cause 추가를 허용한다
class APIKeyExpiredException(message: String = "API key expired", cause: Throwable? = null)	: AccountException(message, cause)

// placeholder 함수의 값을 바꾸면 다른 결과를 얻는다
fun areCredentialsValid(): Boolean = true
fun isAPIKeyExpired(): Boolean = true
//sampleEnd

// 계정 자격 증명과 API 키를 검증한다
fun validateAccount() {
    if (!areCredentialsValid()) throw InvalidAccountCredentialsException()
    if (isAPIKeyExpired()) {
        // 특정 cause와 함께 APIKeyExpiredException을 던지는 예
        val cause = RuntimeException("API key validation failed due to network error")
        throw APIKeyExpiredException(cause = cause)
    }
}

fun main() {
    try {
        validateAccount()
        println("Operation successful: Account credentials and API key are valid.")
    } catch (e: AccountException) {
        println("Error: ${e.message}")
        e.cause?.let { println("Caused by: ${it.message}") }
    }
}

Nothing 타입

Kotlin에서 모든 표현식은 타입을 가집니다. throw IllegalArgumentException()이라는 표현식의 타입은 Nothing인데, 이것은 모든 다른 타입의 서브타입인 내장 타입으로 bottom type이라고도 합니다. 이는 Nothing이 다른 어떤 타입이 기대되는 곳에서도 반환 타입이나 제네릭 타입으로 사용될 수 있고, 타입 오류를 일으키지 않는다는 뜻이에요.

Nothing은 Kotlin에서 성공적으로 완료되지 않는 — 항상 예외를 던지거나 무한 루프 같은 무한 실행 경로에 들어가는 — 함수나 표현식을 나타내는 특별한 타입입니다. Nothing으로 아직 구현되지 않았거나 항상 예외를 던지도록 설계된 함수를 표시해서, 컴파일러와 코드를 읽는 사람 모두에게 의도를 분명히 알릴 수 있어요. 컴파일러가 함수 시그니처에서 Nothing 타입을 추론하면 경고를 보낼 텐데, 반환 타입으로 Nothing을 명시하면 이 경고를 없앨 수 있습니다.

다음 Kotlin 코드는 Nothing 타입의 사용을 보여줍니다. 컴파일러는 함수 호출 뒤의 코드를 도달 불가(unreachable)로 표시해요.

class Person(val name: String?)

fun fail(message: String): Nothing {
    throw IllegalArgumentException(message)
    // 이 함수는 절대 성공적으로 반환되지 않는다.
    // 항상 예외를 던진다.
}

fun main() {
    // 'name'을 null로 하여 Person 인스턴스를 만든다
    val person = Person(name = null)

    val s: String = person.name ?: fail("Name required")

    // 이 시점에 's'는 초기화가 보장된다
    println(s)
}

Kotlin의 TODO() 함수도 Nothing 타입을 사용하며, 향후 구현이 필요한 코드 영역을 표시하는 placeholder 역할을 합니다.

fun notImplementedFunction(): Int {
    TODO("This function is not yet implemented")
}

fun main() {
    val result = notImplementedFunction()
    // 이것은 NotImplementedError를 던진다
    println(result)
}

보시다시피 TODO() 함수는 항상 NotImplementedError 예외를 던집니다.

예외 클래스

Kotlin에서 흔히 볼 수 있는 예외 유형을 살펴볼게요. 이들은 모두 RuntimeException 클래스의 서브클래스입니다.

  • ArithmeticException: 0으로 나누기 같은, 수행할 수 없는 산술 연산에서 발생하는 예외예요.
val example = 2 / 0 // throws ArithmeticException
  • IndexOutOfBoundsException: 배열이나 문자열 같은 어떤 종류의 인덱스가 범위를 벗어났음을 나타내기 위해 던져지는 예외예요.
val myList = mutableListOf(1, 2, 3)
myList.removeAt(3)  // throws IndexOutOfBoundsException

이 예외를 피하려면 getOrNull() 함수 같은 더 안전한 대안을 사용하세요.

val myList = listOf(1, 2, 3)
// IndexOutOfBoundsException 대신 null을 반환한다
val element = myList.getOrNull(3)
println("Element at index 3: $element")
  • NoSuchElementException: 특정 컬렉션에 존재하지 않는 요소에 접근할 때 던져지는 예외예요. first()last()처럼 특정 요소를 기대하는 메서드를 쓸 때 발생합니다.
val emptyList = listOf<Int>()
val firstElement = emptyList.first()  // throws NoSuchElementException

이 예외를 피하려면 firstOrNull() 함수 같은 더 안전한 대안을 사용하세요.

val emptyList = listOf<Int>()
// NoSuchElementException 대신 null을 반환한다
val firstElement = emptyList.firstOrNull()
println("First element in empty list: $firstElement")
  • NumberFormatException: 문자열을 숫자 타입으로 변환하려는데 그 문자열이 적절한 형식이 아닐 때 발생하는 예외예요.
val string = "This is not a number"
val number = string.toInt() // throws NumberFormatException

이 예외를 피하려면 toIntOrNull() 함수 같은 더 안전한 대안을 사용하세요.

val nonNumericString = "not a number"
// NumberFormatException 대신 null을 반환한다
val number = nonNumericString.toIntOrNull()
println("Converted number: $number")
  • NullPointerException: 애플리케이션이 null 값을 가진 객체 참조를 사용하려 할 때 던져지는 예외예요. Kotlin의 널 안전성 기능이 NullPointerException의 위험을 크게 줄여 주지만, !! 연산자를 의도적으로 사용하거나 Kotlin의 널 안전성이 없는 Java와 상호 작용할 때는 여전히 발생할 수 있습니다.
val text: String? = null
println(text!!.length)  // throws a NullPointerException

Kotlin에서 모든 예외는 unchecked이며 명시적으로 catch할 필요는 없지만, 원한다면 catch할 수 있는 유연함이 있습니다.

예외 계층 구조

Kotlin 예외 계층의 뿌리는 Throwable 클래스입니다. 여기에는 두 개의 직접 서브클래스, ErrorException이 있어요.

  • Error 서브클래스는 애플리케이션이 스스로 회복하지 못할 수도 있는 심각한 근본 문제를 나타냅니다. OutOfMemoryErrorStackOverflowError처럼 일반적으로 처리하려 시도하지 않는 문제들이죠.
  • Exception 서브클래스는 처리하고 싶을 법한 조건에 사용됩니다. RuntimeExceptionIOException(입출력 예외) 같은 Exception 타입의 서브타입들은 애플리케이션의 예외적인 사건을 다룹니다.

RuntimeException은 보통 프로그램 코드의 검사 부족으로 발생하며 프로그래밍 방식으로 예방할 수 있어요. Kotlin은 NullPointerException 같은 흔한 RuntimeException을 예방하는 데 도움을 주고, 0으로 나누기 같은 잠재적 런타임 오류에 대해 컴파일 타임 경고를 제공합니다. 다음 그림은 RuntimeException에서 파생된 서브타입의 계층 구조를 보여줍니다.

스택 트레이스

스택 트레이스(stack trace)는 런타임 환경이 생성하는 디버깅용 리포트로, 오류나 예외가 발생한 지점 특히 프로그램의 특정 지점으로 이어지는 함수 호출의 시퀀스를 보여줍니다.

JVM 환경에서 예외 때문에 스택 트레이스가 자동으로 출력되는 예시를 볼게요.

fun main() {
//sampleStart
    throw ArithmeticException("This is an arithmetic exception!")
//sampleEnd
}

이 코드를 JVM 환경에서 실행하면 다음과 같은 출력이 나옵니다.

Exception in thread "main" java.lang.ArithmeticException: This is an arithmetic exception!
    at MainKt.main(Main.kt:3)
    at MainKt.main(Main.kt)

첫 번째 줄은 예외 설명인데, 다음을 포함합니다.

  • 예외 타입: java.lang.ArithmeticException
  • 스레드: main
  • 예외 메시지: "This is an arithmetic exception!"

예외 설명 뒤에 at으로 시작하는 나머지 각 줄이 스택 트레이스예요. 한 줄을 스택 트레이스 요소(stack trace element) 또는 스택 프레임(stack frame)이라고 합니다.

  • at MainKt.main (Main.kt:3): 메서드 이름(MainKt.main)과 그 메서드가 호출된 소스 파일·줄 번호(Main.kt:3)를 보여줍니다.
  • at MainKt.main (Main.kt): 예외가 Main.kt 파일의 main() 함수에서 발생함을 보여줍니다.

Java, Swift, Objective-C와의 예외 상호 운용

Kotlin은 모든 예외를 unchecked로 취급하므로, checked와 unchecked 예외를 구분하는 언어에서 이런 예외를 호출할 때 복잡함이 생길 수 있어요. Kotlin과 Java, Swift, Objective-C 같은 언어 간의 예외 처리 차이를 해결하려면 @Throws 애노테이션을 사용할 수 있습니다. 이 애노테이션은 호출자에게 발생할 수 있는 예외를 알려 줘요. 자세한 내용은 Calling Kotlin from JavaInteroperability with Swift/Objective-C를 참고하세요.

더 알아보기 (Learn more)