코틀린(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를 쓸 때 자주 하는 실수와 함께 팁을 정리해보겠습니다.

  1. 너무 넓은 범위를 감싸지 말기
// 안 좋은 예
try {
    // 이 안에 있는 모든 코드가 예외 처리 대상
} catch (e: Exception) {
    // 뭐가 터졌는지 알 수가 없음
}
  • 예외가 날 가능성이 있는 부분만 최대한 좁게 감싸는 게 좋습니다.
  • 그래야 디버깅할 때 원인을 빠르게 찾을 수 있습니다.
  1. 무조건 Exception 하나로 다 받지 말기
  2. 가능하면 구체적인 예외 타입을 잡아서 상황별로 다르게 처리하는 것이 좋습니다.
  3. 예외를 "묻어버리지" 말기
catch (e: Exception) {
    // 아무것도 안 함
}

이렇게 하면 나중에 문제를 찾기 정말 어렵습니다.
최소한 로그는 남겨야 합니다.

catch (e: Exception) {
    println("에러 발생: ${e.message}")
}

안드로이드라면 Log.e("TAG", "message", e) 같은 로그를 남기는 것도 필수입니다.

  1. 예외 처리로 로직을 대신하지 말기
  2. 예를 들어, null 체크나 기본적인 검증은 if 문으로 미리 걸러내고, 예외 처리는 진짜 "예외적인" 상황에서만 사용하도록 하는 것이 좋습니다.

 


정리하자면..

  • 코틀린의 try-catch는 자바와 비슷하지만, "표현식"으로 값을 반환할 수 있다는 점이 큰 특징입니다.
  • catch를 여러 개 둬서 예외 타입별로 깔끔하게 분기할 수 있고, finally로 리소스 정리를 할 수 있습니다.
  • 예외가 발생했을 때 기본값을 쓰고 싶다면 try 표현식 + ?: 조합, 또는 runCatching + getOrElse 같은 패턴이 유용합니다.
  • 실무에서는 "너무 넓게 감싸지 않기", "예외를 묻어두지 않기", "구체적인 예외 타입 사용하기"를 기억해 두시면 좋습니다.
반응형
admin