코틀린(Kotlin) try-catch 사용법 정리: 예외 처리
개발/안드로이드1 2025. 11. 14. 15:50코틀린에서 예외 처리할 때 제일 먼저 떠오르는 키워드가 바로 try-catch죠.
이 글에서는 코틀린에서 try-catch를 어떻게 쓰는지, 기초부터 실전 팁까지 정리해보겠습니다.
1. 예외(Exception) 처리가 왜 필요할까요?
코드를 짜다 보면 이런 상황들이 자주 생깁니다.
- 서버에서 받은 문자열을 숫자로 바꿔야 하는데, 갑자기 "abc" 같은 값이 들어온다든지
- 파일을 열었는데 파일이 없거나 권한이 없다든지
- 네트워크 통신 중에 타임아웃이 난다든지
이런 "예상 못 한 상황"을 그대로 두면 앱이 바로 크래시 나 버립니다.
그래서 이런 상황을 안전하게 감싸는 것이 예외 처리이고, 그 대표적인 문법이 try-catch입니다.
2. Kotlin 예외 처리 기본 구조
코틀린의 기본 구조는 자바와 거의 비슷합니다.
try {
// 예외가 발생할 수 있는 코드
} catch (e: 예외타입) {
// 예외가 발생했을 때 처리
} finally {
// 예외 발생 여부와 상관없이 항상 실행 (선택)
}
- try 블록 안에서 예외가 발생하면, 바로 해당 타입에 맞는 catch 블록으로 흐름이 이동합니다.
- finally 블록은 예외 발생 여부와 상관없이 항상 실행됩니다. (리소스 정리용으로 많이 사용)
3. 가장 기본적인 try-catch 예제
문자열을 정수로 바꾸는 간단한 예제로 시작해보겠습니다.
fun parseIntSafely(input: String) {
try {
val number = input.toInt() // NumberFormatException 가능
println("변환 성공: $number")
} catch (e: NumberFormatException) {
println("숫자로 변환할 수 없는 값입니다: $input")
}
}
- input.toInt() 에서 숫자가 아닌 값이 들어오면 NumberFormatException이 발생합니다.
- 그 경우 catch 블록이 실행되고, 앱은 크래시 없이 안전하게 메시지만 출력하고 넘어갑니다.
4. 여러 종류의 예외를 나눠서 처리하기
하나의 try 블록 안에서 여러 타입의 예외가 발생할 수 있다면, catch 블록을 여러 개 두고 타입별로 분기할 수 있습니다.
fun readUserData(path: String) {
try {
val file = File(path)
val text = file.readText() // IOException 가능
val id = text.toInt() // NumberFormatException 가능
println("유저 ID: $id")
} catch (e: FileNotFoundException) {
println("파일을 찾을 수 없습니다: $path")
} catch (e: NumberFormatException) {
println("파일 내용이 숫자가 아닙니다.")
} catch (e: IOException) {
println("파일을 읽는 중 오류가 발생했습니다: ${e.message}")
}
}
- catch는 위에서부터 순서대로 검사됩니다.
- 상위 타입(예: Exception)을 위에 두고, 하위 타입(예: IOException)을 아래에 두면
- 하위 타입의 블록이 절대 실행되지 않으니, 항상 “구체적인 예외 → 범용 예외” 순서로 두는 게 좋습니다.
5. finally 블록: 리소스 정리용으로 자주 사용
finally는 예외가 발생하든 안 하든 무조건 실행되는 블록입니다.
주로 파일, 스트림, 잠금(lock) 같은 리소스를 정리할 때 사용합니다.
fun readFile(path: String) {
var reader: BufferedReader? = null
try {
reader = File(path).bufferedReader()
val firstLine = reader.readLine()
println("첫 줄: $firstLine")
} catch (e: IOException) {
println("파일 읽기 실패: ${e.message}")
} finally {
// null 체크 후 안전하게 닫기
try {
reader?.close()
} catch (e: IOException) {
// 닫는 중 예외는 보통 로그만 남기고 넘어감
}
}
}
요즘은 use 확장 함수를 많이 쓰기 때문에 finally를 직접 쓰는 경우는 줄었지만, 동작 원리는 알고 있는 게 좋습니다.
6. Kotlin에서 try는 “표현식”이다
코틀린에서 try는 "문장"이 아니라 "표현식"이라서, 값으로 사용할 수 있습니다.
6-1. try에서 값 반환하기
fun parseIntOrDefault(input: String, default: Int = 0): Int {
val number = try {
input.toInt()
} catch (e: NumberFormatException) {
default
}
return number
}
- try 블록의 마지막 표현식이나 catch 블록의 마지막 표현식이 number에 대입됩니다.
- 예외가 발생하면 catch 블록의 값이 반환되고, 발생하지 않으면 try 블록의 값이 반환됩니다.
6-2. return과 함께 쓰기
fun parseIntOrNull(input: String): Int? {
return try {
input.toInt()
} catch (e: NumberFormatException) {
null
}
}
이렇게 쓰면 코드가 깔끔해지고, if-else 문으로 분기하는 것보다 가독성이 좋은 경우가 많습니다.
7. try + Elvis(?:) 조합으로 간단하게 처리하기
예외가 나면 null을 돌려주고, 호출하는 쪽에서 기본값을 처리하는 패턴도 자주 쓰입니다.
fun parseIntWithDefault(input: String): Int {
val result = try {
input.toInt()
} catch (e: NumberFormatException) {
null
}
// null이면 기본값 0 사용
return result ?: 0
}
또는 한 줄로 줄일 수도 있습니다.
fun parseIntWithDefault(input: String) =
try {
input.toInt()
} catch (e: NumberFormatException) {
null
} ?: 0
8. Kotlin 표준 함수 runCatching 활용하기
코틀린에는 예외를 래핑해서 다루기 좋은 runCatching이라는 표준 함수도 있습니다.
fun parseIntRunCatching(input: String) {
val result = runCatching {
input.toInt()
}
result
.onSuccess { value ->
println("변환 성공: $value")
}
.onFailure { throwable ->
println("변환 실패: ${throwable.message}")
}
}
조금 더 응용하면, 성공/실패 시 다른 값으로 변환해서 쓰기도 쉽습니다.
fun parseIntOrDefaultWithRunCatching(input: String, default: Int = 0): Int {
return runCatching {
input.toInt()
}.getOrElse {
default
}
}
- getOrElse는 예외가 없으면 변환된 값을, 예외가 있으면 람다에서 리턴한 값을 돌려줍니다.
- 비즈니스 로직에서 예외를 “값”처럼 다루고 싶을 때 잘 어울리는 패턴입니다.
9. 사용자 정의 예외와 rethrow
직접 예외 클래스를 만들어서 비즈니스 로직을 명확히 표현할 수도 있습니다.
class InvalidUserIdException(message: String) : RuntimeException(message)
fun findUser(idText: String) {
val id = try {
idText.toInt()
} catch (e: NumberFormatException) {
throw InvalidUserIdException("유효하지 않은 유저 ID 입니다: $idText")
}
// 이후 ID로 유저 조회
}
또한, 예외를 처리한 뒤 "다시 던지는" 패턴도 있습니다.
fun riskyOperation() {
try {
// 뭔가 위험한 작업
} catch (e: IOException) {
// 로그만 남기고
println("로그용 메시지: ${e.message}")
// 다시 던지기
throw e
}
}
- 여기서는 예외를 완전히 해결하지 않고, 상위 레벨에서 처리하도록 넘기는 구조입니다.
- 라이브러리나 공용 모듈에서 많이 쓰는 패턴입니다.
10. try-catch 사용할 때 실전 팁
마지막으로, 실무에서 try-catch를 쓸 때 자주 하는 실수와 함께 팁을 정리해보겠습니다.
- 너무 넓은 범위를 감싸지 말기
// 안 좋은 예
try {
// 이 안에 있는 모든 코드가 예외 처리 대상
} catch (e: Exception) {
// 뭐가 터졌는지 알 수가 없음
}
- 예외가 날 가능성이 있는 부분만 최대한 좁게 감싸는 게 좋습니다.
- 그래야 디버깅할 때 원인을 빠르게 찾을 수 있습니다.
- 무조건 Exception 하나로 다 받지 말기
- 가능하면 구체적인 예외 타입을 잡아서 상황별로 다르게 처리하는 것이 좋습니다.
- 예외를 "묻어버리지" 말기
catch (e: Exception) {
// 아무것도 안 함
}
이렇게 하면 나중에 문제를 찾기 정말 어렵습니다.
최소한 로그는 남겨야 합니다.
catch (e: Exception) {
println("에러 발생: ${e.message}")
}
안드로이드라면 Log.e("TAG", "message", e) 같은 로그를 남기는 것도 필수입니다.
- 예외 처리로 로직을 대신하지 말기
- 예를 들어, null 체크나 기본적인 검증은 if 문으로 미리 걸러내고, 예외 처리는 진짜 "예외적인" 상황에서만 사용하도록 하는 것이 좋습니다.
정리하자면..
- 코틀린의 try-catch는 자바와 비슷하지만, "표현식"으로 값을 반환할 수 있다는 점이 큰 특징입니다.
- catch를 여러 개 둬서 예외 타입별로 깔끔하게 분기할 수 있고, finally로 리소스 정리를 할 수 있습니다.
- 예외가 발생했을 때 기본값을 쓰고 싶다면 try 표현식 + ?: 조합, 또는 runCatching + getOrElse 같은 패턴이 유용합니다.
- 실무에서는 "너무 넓게 감싸지 않기", "예외를 묻어두지 않기", "구체적인 예외 타입 사용하기"를 기억해 두시면 좋습니다.




