출시·고도화 중
Kotlin 안내서 · 5/6
이 장에서는 Kotlin에서 예외를 다루는 방법과, 예외 대신 결과 값으로 실패를 표현하는 Result, 조건 검사 함수 require와 check, 그리고 kotlin.test로 테스트를 작성하는 방법을 다룹니다.
Kotlin의 예외 문법은 throw, try, catch, finally로 Java와 비슷합니다. 가장 큰 차이는 검사 예외(checked exception)가 없다는 점입니다. 함수가 어떤 예외를 던지는지 throws로 선언하지 않아도 되고, 호출하는 쪽도 반드시 잡을 필요가 없습니다.
class InsufficientFundsException(val shortage: Long) :
Exception("잔액이 ${shortage}원 부족합니다")
fun withdraw(balance: Long, amount: Long): Long {
if (amount > balance) throw InsufficientFundsException(amount - balance)
return balance - amount
}
fun main() {
try {
withdraw(1_000, 5_000)
} catch (e: InsufficientFundsException) {
println(e.message) // 잔액이 4000원 부족합니다
} finally {
println("거래 종료")
}
}Java 코드에서 Kotlin 함수를 부르면서 검사 예외를 잡아야 한다면 함수에 @Throws(IOException::class) 주석을 붙여 Java 쪽에 알려 줍니다.
if, when처럼 try도 값을 돌려줍니다. try 블록의 마지막 식이나 catch 블록의 마지막 식이 결과가 됩니다. throw 역시 Nothing 타입의 식이라서 식이 들어갈 자리 어디에나 쓸 수 있습니다.
fun parsePort(text: String): Int {
val port = try {
text.toInt()
} catch (e: NumberFormatException) {
8080 // 숫자가 아니면 기본 포트
}
return port
}
fun fail(message: String): Nothing = throw IllegalStateException(message)
fun main() {
println(parsePort("9000")) // 9000
println(parsePort("abc")) // 8080
}Nothing을 돌려주는 함수는 정상적으로 끝나지 않는다는 뜻입니다. 컴파일러는 이 정보를 이용해 그 뒤의 코드가 실행되지 않는다는 것을 압니다.
함수의 전제 조건을 검사하는 표준 함수가 있습니다. 조건이 거짓이면 알맞은 예외를 던지므로, if와 throw를 직접 쓰는 것보다 의도가 잘 드러납니다.
| 함수 | 실패하면 | 쓰는 곳 |
|---|---|---|
require(조건) | IllegalArgumentException | 인자가 잘못되었을 때 |
check(조건) | IllegalStateException | 객체 상태가 잘못되었을 때 |
requireNotNull(값) | IllegalArgumentException | 인자가 null이면 안 될 때 |
error(메시지) | IllegalStateException | 도달하면 안 되는 곳 |
class Account(initial: Long) {
var balance = initial
private set
private var closed = false
init {
require(initial >= 0) { "초기 잔액은 음수일 수 없습니다: $initial" }
}
fun deposit(amount: Long) {
require(amount > 0) { "입금액은 0보다 커야 합니다" }
check(!closed) { "닫힌 계좌입니다" }
balance += amount
}
fun close() {
if (closed) error("이미 닫힌 계좌입니다")
closed = true
}
}메시지를 람다로 넘기므로 조건이 참일 때는 메시지 문자열을 만들지 않습니다. private set은 밖에서는 읽기만 하고 클래스 안에서만 바꿀 수 있게 합니다.
실패가 흔하고 예상 가능한 상황이라면 예외를 던지는 대신 결과 값으로 다룰 수 있습니다. runCatching은 블록을 실행하고 성공한 값이나 잡은 예외를 Result<T>로 감싸서 돌려줍니다.
import java.io.File
fun main() {
val result: Result<String> = runCatching { File("app.conf").readText() }
result
.onSuccess { println("읽은 글자 수: ${it.length}") }
.onFailure { println("읽기 실패: ${it.message}") }
val text = result.getOrElse { "기본 설정" }
val lineCount = result.map { it.lines().size }.getOrDefault(0)
println("$text / $lineCount")
}Result에는 isSuccess, getOrNull, exceptionOrNull, fold 같은 함수도 있습니다. 다만 runCatching은 모든 Throwable을 잡으므로, 코루틴 안에서는 취소 신호인 CancellationException까지 삼키지 않도록 주의해야 합니다.
kotlin.test는 여러 플랫폼에서 같은 방식으로 쓸 수 있는 테스트 라이브러리입니다. JVM에서는 JUnit 위에서 동작합니다. Gradle 설정은 다음과 같습니다.
// build.gradle.kts
dependencies {
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}useJUnitPlatform()을 쓰면 Kotlin Gradle 플러그인이 JUnit 5용 kotlin-test 구현을 알아서 고릅니다. 테스트는 src/test/kotlin 아래에 둡니다.
// src/test/kotlin/AccountTest.kt
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class AccountTest {
@Test
fun `입금하면 잔액이 늘어난다`() {
val account = Account(1_000)
account.deposit(500)
assertEquals(1_500L, account.balance)
}
@Test
fun `0 이하 입금은 거부된다`() {
val account = Account(0)
val e = assertFailsWith<IllegalArgumentException> {
account.deposit(-1)
}
assertEquals("입금액은 0보다 커야 합니다", e.message)
}
}함수 이름을 백틱으로 감싸면 공백이 든 문장을 테스트 이름으로 쓸 수 있어 결과 보고서가 읽기 쉬워집니다. ./gradlew test로 실행합니다. 더 표현력 있는 단언과 스타일이 필요하면 Kotest, 목 객체가 필요하면 MockK, 코루틴 코드를 테스트할 때는 kotlinx-coroutines-test의 runTest를 많이 씁니다.
throws 선언이나 강제된 catch가 없습니다.try와 throw는 식이므로 결과를 바로 변수에 넣거나 엘비스 연산자 뒤에 쓸 수 있습니다.require, 상태 검사는 check, 도달하면 안 되는 곳은 error를 씁니다.runCatching은 결과를 Result로 감싸지만, 코루틴 안에서는 취소 예외를 삼키지 않도록 조심합니다.kotlin.test의 @Test, assertEquals, assertFailsWith로 쓰고 ./gradlew test로 실행합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.