Notice
Recent Posts
Recent Comments
Link
250x250
반응형
«   2025/12   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

백고등어 개발 블로그

코틀린 강의 7강: 예외 처리 본문

코틀린 강의

코틀린 강의 7강: 예외 처리

백고등어 2025. 10. 28. 17:39
728x90
반응형

7강: 예외 처리

예외란?

예외(Exception)는 프로그램 실행 중에 발생하는 비정상적인 상황을 나타냅니다. 파일을 찾을 수 없거나, 네트워크 연결이 끊기거나, 0으로 나누기를 시도하는 등의 상황에서 예외가 발생합니다.

try-catch-finally

기본 문법

fun main() {
    try {
        val result = 10 / 0  // ArithmeticException 발생
        println(result)
    } catch (e: ArithmeticException) {
        println("0으로 나눌 수 없습니다: ${e.message}")
    } finally {
        println("finally 블록은 항상 실행됩니다")
    }
}

finally 블록은 예외 발생 여부와 관계없이 항상 실행되며, 주로 리소스 정리 작업에 사용됩니다.

여러 예외 처리

fun parseNumber(str: String) {
    try {
        val number = str.toInt()
        val result = 100 / number
        println("결과: $result")
    } catch (e: NumberFormatException) {
        println("숫자 형식이 잘못되었습니다")
    } catch (e: ArithmeticException) {
        println("산술 연산 오류가 발생했습니다")
    } catch (e: Exception) {
        println("알 수 없는 오류가 발생했습니다: ${e.message}")
    }
}

fun main() {
    parseNumber("abc")  // 숫자 형식이 잘못되었습니다
    parseNumber("0")    // 산술 연산 오류가 발생했습니다
}

try를 표현식으로 사용

코틀린의 try는 표현식이므로 값을 반환할 수 있습니다:

fun readNumber(str: String): Int? {
    return try {
        str.toInt()
    } catch (e: NumberFormatException) {
        null
    }
}

fun main() {
    val num1 = readNumber("123")
    val num2 = readNumber("abc")
    
    println(num1)  // 123
    println(num2)  // null
}

표현식으로 사용할 때는 try 블록이나 catch 블록의 마지막 값이 반환됩니다:

fun calculateResult(a: Int, b: Int): String {
    return try {
        val result = a / b
        "결과: $result"
    } catch (e: ArithmeticException) {
        "계산 불가"
    }
}

fun main() {
    println(calculateResult(10, 2))  // 결과: 5
    println(calculateResult(10, 0))  // 계산 불가
}

예외 던지기(throw)

throw 키워드로 명시적으로 예외를 발생시킬 수 있습니다:

fun validateAge(age: Int) {
    if (age < 0) {
        throw IllegalArgumentException("나이는 0 이상이어야 합니다")
    }
    println("나이: $age")
}

fun main() {
    try {
        validateAge(25)   // 나이: 25
        validateAge(-5)   // 예외 발생
    } catch (e: IllegalArgumentException) {
        println("오류: ${e.message}")
    }
}

throw도 표현식

throw도 표현식이므로 Elvis 연산자와 함께 사용할 수 있습니다:

fun getUsername(name: String?): String {
    return name ?: throw IllegalArgumentException("이름은 null일 수 없습니다")
}

fun main() {
    println(getUsername("김코틀린"))  // 김코틀린
    // println(getUsername(null))     // 예외 발생
}

Nothing 타입

throw와 예외를 던지는 함수는 Nothing 타입을 반환합니다. Nothing은 "값이 없음"을 나타내는 특수한 타입입니다:

fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

fun main() {
    val name: String = readLine() ?: fail("이름을 입력하세요")
    println("안녕하세요, $name님")
}

주요 예외 타입

IllegalArgumentException

잘못된 인자가 전달되었을 때 사용합니다:

fun setAge(age: Int) {
    require(age >= 0) { "나이는 0 이상이어야 합니다" }
    println("나이 설정: $age")
}

require() 함수는 조건이 false면 IllegalArgumentException을 던집니다.

IllegalStateException

객체의 상태가 메서드 호출에 적합하지 않을 때 사용합니다:

class BankAccount {
    private var balance = 0
    private var isOpen = true
    
    fun withdraw(amount: Int) {
        check(isOpen) { "계좌가 닫혀있습니다" }
        check(balance >= amount) { "잔액이 부족합니다" }
        balance -= amount
    }
}

check() 함수는 조건이 false면 IllegalStateException을 던집니다.

NullPointerException

코틀린은 null 안전성을 제공하지만, !! 연산자 사용 시 NPE가 발생할 수 있습니다:

fun main() {
    val name: String? = null
    
    try {
        println(name!!.length)  // NullPointerException 발생
    } catch (e: NullPointerException) {
        println("null 값입니다")
    }
}

Checked vs Unchecked 예외

자바와 달리 코틀린은 checked 예외를 구분하지 않습니다. 모든 예외는 unchecked이므로 throws 선언이 필요 없습니다:

// 자바에서는 throws IOException 선언 필요
// 코틀린에서는 필요 없음
fun readFile(path: String): String {
    return java.io.File(path).readText()
}

이는 코드를 더 간결하게 만들지만, 어떤 예외가 발생할 수 있는지 문서화가 중요합니다.

리소스 자동 관리

use 함수

코틀린의 use 함수는 자바의 try-with-resources와 유사합니다:

import java.io.File

fun readFirstLine(path: String): String? {
    return File(path).bufferedReader().use { reader ->
        reader.readLine()
    }
}
// use 블록이 끝나면 자동으로 close() 호출

use 함수는 Closeable 인터페이스를 구현한 모든 객체에 사용할 수 있습니다.

runCatching을 이용한 함수형 예외 처리

코틀린 1.3부터 runCatching 함수를 제공합니다:

fun parseNumber(str: String): Result<Int> {
    return runCatching { str.toInt() }
}

fun main() {
    val result1 = parseNumber("123")
    val result2 = parseNumber("abc")
    
    // 성공/실패 확인
    println(result1.isSuccess)  // true
    println(result2.isFailure)  // true
    
    // 값 가져오기
    println(result1.getOrNull())  // 123
    println(result2.getOrNull())  // null
    
    // 기본값 제공
    println(result2.getOrDefault(0))  // 0
    
    // 예외 처리
    result1.onSuccess { println("성공: $it") }
    result2.onFailure { println("실패: ${it.message}") }
}

체이닝

runCatching은 함수형 스타일로 체이닝할 수 있습니다:

fun divide(a: Int, b: Int): Result<Int> {
    return runCatching { a / b }
}

fun main() {
    val result = runCatching { "10".toInt() }
        .map { it * 2 }
        .mapCatching { divide(100, it).getOrThrow() }
        .getOrElse { 0 }
    
    println(result)
}

실전 예제

data class User(val name: String, val age: Int, val email: String)

class UserValidator {
    fun validateUser(name: String?, age: Int?, email: String?): Result<User> {
        return runCatching {
            // 이름 검증
            require(!name.isNullOrBlank()) { "이름은 필수입니다" }
            
            // 나이 검증
            requireNotNull(age) { "나이는 필수입니다" }
            require(age in 0..150) { "나이는 0-150 사이여야 합니다" }
            
            // 이메일 검증
            require(!email.isNullOrBlank()) { "이메일은 필수입니다" }
            require("@" in email) { "올바른 이메일 형식이 아닙니다" }
            
            User(name, age, email)
        }
    }
}

fun main() {
    val validator = UserValidator()
    
    // 유효한 사용자
    validator.validateUser("김코틀린", 25, "kotlin@example.com")
        .onSuccess { println("사용자 생성 성공: $it") }
        .onFailure { println("오류: ${it.message}") }
    
    // 무효한 사용자
    validator.validateUser("", 200, "invalid")
        .onSuccess { println("사용자 생성 성공: $it") }
        .onFailure { println("오류: ${it.message}") }
}

커스텀 예외

필요한 경우 커스텀 예외를 정의할 수 있습니다:

class InsufficientBalanceException(
    val balance: Int,
    val required: Int
) : Exception("잔액 부족: 현재 $balance원, 필요 ${required}원")

class BankAccount(private var balance: Int) {
    fun withdraw(amount: Int) {
        if (balance < amount) {
            throw InsufficientBalanceException(balance, amount)
        }
        balance -= amount
        println("출금 성공: ${amount}원, 잔액: ${balance}원")
    }
}

fun main() {
    val account = BankAccount(10000)
    
    try {
        account.withdraw(5000)   // 성공
        account.withdraw(8000)   // 예외 발생
    } catch (e: InsufficientBalanceException) {
        println(e.message)
        println("부족액: ${e.required - e.balance}원")
    }
}

예외 처리 모범 사례

  1. 구체적인 예외를 먼저 처리: 상위 예외 클래스는 나중에 catch합니다.
  2. finally로 리소스 정리: 또는 use 함수를 사용합니다.
  3. 예외를 무시하지 마세요: 최소한 로그는 남깁니다.
  4. 적절한 예외 타입 사용: 상황에 맞는 예외를 던집니다.
  5. 문서화: 어떤 예외가 발생할 수 있는지 명시합니다.

마치며

이번 강의에서는 코틀린의 예외 처리에 대해 알아보았습니다. try를 표현식으로 사용할 수 있다는 점, runCatching을 통한 함수형 예외 처리, 그리고 코틀린이 checked 예외를 구분하지 않는다는 점을 기억하세요. 다음 강의에서는 제네릭에 대해 알아보겠습니다.

728x90
반응형