본문으로 건너뛰기

⚠️ 예외 처리

📖 예외란?

**예외(Exception)**는 프로그램 실행 중 발생하는 오류입니다. 예외를 잘 처리하면 프로그램이 멈추지 않고 안정적으로 동작합니다!

💡 기본 try-catch

기본 사용법

fun main() {
try {
val result = 10 / 0 // 에러 발생!
println(result)
} catch (e: Exception) {
println("에러 발생: ${e.message}")
}
// 에러 발생: / by zero

println("프로그램 계속 실행!")
}

특정 예외 처리

fun main() {
try {
val text = "abc"
val number = text.toInt() // NumberFormatException
} catch (e: NumberFormatException) {
println("숫자로 변환할 수 없습니다: ${e.message}")
} catch (e: Exception) {
println("알 수 없는 에러: ${e.message}")
}
}

finally

fun main() {
try {
println("작업 시작")
// 작업 수행
} catch (e: Exception) {
println("에러 발생")
} finally {
println("정리 작업 (항상 실행)")
}
}

🎯 실전 예제

안전한 숫자 변환

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

fun main() {
println(safeToInt("123")) // 123
println(safeToInt("abc")) // null

val age = safeToInt("25") ?: 0
println("나이: $age")
}

파일 읽기 시뮬레이션

class FileReader {
fun readFile(filename: String): String {
if (filename.isEmpty()) {
throw IllegalArgumentException("파일명이 비어있습니다")
}
if (!filename.endsWith(".txt")) {
throw IllegalArgumentException("텍스트 파일만 지원합니다")
}
return "파일 내용: $filename"
}
}

fun main() {
val reader = FileReader()

try {
val content = reader.readFile("data.txt")
println(content)
} catch (e: IllegalArgumentException) {
println("오류: ${e.message}")
}

try {
reader.readFile("")
} catch (e: IllegalArgumentException) {
println("오류: ${e.message}")
// 오류: 파일명이 비어있습니다
}
}

사용자 입력 검증

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

fun createUser(name: String, age: Int): User {
require(name.isNotBlank()) { "이름은 필수입니다" }
require(age > 0) { "나이는 양수여야 합니다" }
require(age < 150) { "나이가 너무 많습니다" }

return User(name, age)
}

fun main() {
try {
val user1 = createUser("홍길동", 25)
println(user1)

val user2 = createUser("", 25) // 에러!
} catch (e: IllegalArgumentException) {
println("검증 실패: ${e.message}")
// 검증 실패: 이름은 필수입니다
}
}

API 응답 처리

sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}

fun divide(a: Int, b: Int): Result<Int> {
return try {
Result.Success(a / b)
} catch (e: ArithmeticException) {
Result.Error(e)
}
}

fun main() {
val result1 = divide(10, 2)
when (result1) {
is Result.Success -> println("결과: ${result1.data}")
is Result.Error -> println("에러: ${result1.exception.message}")
}

val result2 = divide(10, 0)
when (result2) {
is Result.Success -> println("결과: ${result2.data}")
is Result.Error -> println("에러: ${result2.exception.message}")
}
}

🔍 표현식으로 사용

try를 값으로

fun main() {
val number = try {
"123".toInt()
} catch (e: NumberFormatException) {
0 // 기본값
}

println(number) // 123

val invalid = try {
"abc".toInt()
} catch (e: NumberFormatException) {
-1
}

println(invalid) // -1
}

체이닝

fun safeDivide(a: Int, b: Int): Int? {
return try {
a / b
} catch (e: ArithmeticException) {
null
}
}

fun main() {
val result = safeDivide(10, 2)
?.let { it * 2 }
?.let { it + 5 }
?: 0

println(result) // 15 (10/2 * 2 + 5)
}

🛡️ 안전한 코드 작성

require vs check

fun processOrder(quantity: Int, price: Int) {
// 입력 검증
require(quantity > 0) { "수량은 양수여야 합니다" }
require(price > 0) { "가격은 양수여야 합니다" }

val total = quantity * price

// 상태 검증
check(total < 1000000) { "총액이 너무 큽니다" }

println("주문 완료: ${total}원")
}

fun main() {
try {
processOrder(10, 5000) // 성공
processOrder(-1, 5000) // require 실패
} catch (e: IllegalArgumentException) {
println("입력 오류: ${e.message}")
} catch (e: IllegalStateException) {
println("상태 오류: ${e.message}")
}
}

checkNotNull

fun processName(name: String?) {
val validName = checkNotNull(name) { "이름이 null입니다" }
println("처리: $validName")
}

fun main() {
try {
processName("홍길동") // 성공
processName(null) // 실패
} catch (e: IllegalStateException) {
println("오류: ${e.message}")
}
}

runCatching

Kotlin 1.3부터 제공되는 편리한 방법!

fun main() {
// runCatching 사용
val result = runCatching {
"123".toInt()
}

result
.onSuccess { println("성공: $it") }
.onFailure { println("실패: ${it.message}") }

// getOrNull
val number = runCatching { "abc".toInt() }.getOrNull()
println(number) // null

// getOrDefault
val safe = runCatching { "abc".toInt() }.getOrDefault(0)
println(safe) // 0
}

🎯 실용 패턴

재시도 로직

fun <T> retry(times: Int, block: () -> T): T? {
repeat(times) { attempt ->
try {
return block()
} catch (e: Exception) {
println("시도 ${attempt + 1} 실패: ${e.message}")
if (attempt == times - 1) throw e
}
}
return null
}

fun main() {
var count = 0

try {
val result = retry(3) {
count++
if (count < 3) {
throw Exception("아직 안 됨")
}
"성공!"
}
println(result)
} catch (e: Exception) {
println("최종 실패")
}
}

리소스 정리

class Resource : AutoCloseable {
init {
println("리소스 생성")
}

fun use() {
println("리소스 사용")
}

override fun close() {
println("리소스 정리")
}
}

fun main() {
try {
Resource().use { resource ->
resource.use()
// 자동으로 close() 호출
}
} catch (e: Exception) {
println("에러 발생")
}
}

🤔 자주 묻는 질문

Q1. throw는 언제 쓰나요?

A: 명시적으로 예외를 발생시킬 때!

fun validateAge(age: Int) {
if (age < 0) {
throw IllegalArgumentException("나이는 음수일 수 없습니다")
}
if (age > 150) {
throw IllegalArgumentException("나이가 너무 많습니다")
}
}

Q2. Exception vs RuntimeException?

A: Kotlin은 구분 없음!

// Java에서는 checked/unchecked 구분
// Kotlin은 모두 unchecked (throws 선언 불필요)

fun divide(a: Int, b: Int): Int {
return a / b // throws 선언 불필요!
}

Q3. 커스텀 예외는?

A: Exception 상속!

class InvalidEmailException(message: String) : Exception(message)

fun validateEmail(email: String) {
if (!email.contains("@")) {
throw InvalidEmailException("유효하지 않은 이메일: $email")
}
}

fun main() {
try {
validateEmail("invalid")
} catch (e: InvalidEmailException) {
println(e.message)
}
}

🎬 마치며

예외 처리로 안정적인 프로그램을 만드세요!

핵심 정리:
✅ try-catch로 예외 처리
✅ require/check로 검증
✅ runCatching으로 간편하게
✅ 표현식으로 값 반환
✅ finally로 정리 작업

다음 단계: 파일 입출력에서 파일을 다뤄보세요!