Try-Catch: 예외 포착의 본질, 구조 및 모바일 개발에서의 작동 방식

저자: IT Sectr 게시일: 2026-05-25 읽는 시간: 9 분

Try-Catch는 잠재적으로 위험한 코드를 보호된 블록에서 실행하고 프로그램이 비정상 종료되지 않도록 오류를 올바르게 처리할 수 있는 예외 포착 구조입니다. try 블록에는 예외를 던질 수 있는 코드가 포함되고, catch는 이를 포착하여 복구 로직을 실행합니다. Apple Swift Documentation (2026)에 따르면, finally 블록은 예외가 던져졌는지 여부와 관계없이 실행되어 리소스 해제를 보장합니다.

핵심 사항

  • Try-Catch는 오류 발생 시 프로그램 비정상 종료를 방지하는 예외 포착 구조입니다
  • try 블록은 예외를 던질 수 있는 코드를 포함합니다 — 첫 번째 오류에서 실행이 중단됩니다
  • catch 블록은 지정된 유형의 예외를 포착하고 처리 또는 복구 로직을 실행합니다
  • finally 블록은 리소스 해제(예: 파일 닫기)를 위해 try/catch 후에 반드시 실행됩니다
  • 중첩 try-catch는 하나의 함수 내에서 다양한 추상화 수준에서 오류를 처리할 수 있습니다

Try-Catch란?

Try-Catch는 대부분의 현대 프로그래밍 언어에 존재하는 기본적인 구조적 예외 처리 구조입니다. 세 가지 블록으로 구성됩니다: try(위험한 코드 실행 시도), catch(예외 포착 및 처리), 선택적 finally(종료). 이 구조의 아이디어는 비즈니스 로직을 오류 처리 로직으로부터 분리하여 코드를 더 읽기 쉽고 예측 가능하게 만드는 것입니다.

이 개념은 C++에서 try/catch로 처음 구현되었으며, 이후 Java, C#, Swift, Kotlin, Dart, Python, JavaScript 및 기타 언어에서 채택되었습니다. 각 언어는 고유한 기능을 추가합니다: Swift에서는 catch 블록이 포괄적이어야 하고, Kotlin에서는 try-catch가 표현식이 될 수 있으며, Dart에서는 스트림 리소스에 finally가 필수입니다. 차이점에도 불구하고 기본 원칙은 동일합니다: 오류는 전역적으로가 아니라 발생 지점에 최대한 가깝게 처리됩니다.

Try-Catch의 사용은 모바일 개발에서 특히 중요합니다. 네트워크 손실, 잘못된 서버 응답, 메모리 부족과 같은 외부 요인이 끊임없이 발생하기 때문입니다. 적절한 예외 처리는 애플리케이션 충돌을 방지하고 올바른 UX를 보장합니다: 사용자는 갑작스러운 앱 종료 대신 오류 메시지를 보게 됩니다. Google Android Kotlin Style Guide (2026)에 따르면, 예외를 던질 수 있는 모든 함수는 try-catch를 통해 처리하거나 시그니처에 throws를 선언해야 합니다.

Try-Catch 작동 방식

try-catch의 실행 메커니즘은 스택 풀기(stack unwinding)를 기반으로 합니다. try 블록 내에서 throw 연산자(또는 시스템 오류의 결과)를 통해 예외가 던져지면, 정상적인 실행 흐름이 즉시 중단됩니다. 실행은 적절한 catch 블록을 찾기 위해 호출 스택에서 한 수준 위로 이동합니다. 최신 언어는 유형 일치 메커니즘(type matching)을 사용하여 던져진 예외의 유형과 일치하는 catch를 검색합니다.

일치하는 catch가 발견되면 그 본문이 실행되고, 그 후 전체 try-catch-finally 구조 이후에 실행이 계속됩니다. catch가 발견되지 않으면 예외는 스택을 더 위로 올라가고 더 높은 수준에서 처리될 수 있습니다 — 모바일 애플리케이션에서 사용자에게 오류 대화상자를 표시하는 전역 처리기까지. 예외가 어디서도 처리되지 않으면 애플리케이션이 비정상 종료됩니다. 이것이 바로 모든 가능한 예외 유형의 올바른 처리가 애플리케이션 안정성에 중요한 이유입니다.

기본 구조 구문

kotlin
fun readUserData(): User {
    return try {
        val response = api.fetchUser()
        parseUser(response)
    } catch (e: IOException) {
        logError("네트워크 오류", e)
        throw AppException("데이터를 로드하지 못했습니다")
    } catch (e: JsonParseException) {
        logError("구문 분석 오류", e)
        return User.default()
    } finally {
        closeLoadingIndicator()
    }
}

코드는 먼저 API 요청을 실행하고 응답을 구문 분석하려고 시도합니다. IOException(네트워크 문제)이 발생하면 예외가 기록되고 AppException으로 다시 던져집니다. JsonParseException이 발생하면 — 기본 사용자가 반환됩니다. finally 블록은 로딩 표시기를 숨기는 것을 보장하여 화면의 UI 구성 요소 누출을 방지합니다.

Swift에서의 Try-Catch

Swift에서 오류 처리는 Error 프로토콜(이전 ErrorType)을 통해 구현됩니다. Error를 준수하는 모든 유형은 throw 연산자를 통해 던져질 수 있습니다. 오류를 던질 수 있는 함수는 시그니처에 throws 키워드로 표시됩니다. 이러한 함수를 호출하려면 try 접두사(명시적 try-catch의 경우), try?(선택적 결과) 또는 try!(오류 처리 없는 강제 실행)가 필요합니다.

swift
enum NetworkError: Error {
    case noConnection
    case serverError(code: Int)
    case timeout
}

func fetchUser(id: Int) throws -> User {
    guard isConnected() else {
        throw NetworkError.noConnection
    }
    let data = try performRequest(path: "/users/\(id)")
    return try decodeUser(from: data)
}

do {
    let user = try fetchUser(id: 42)
    updateUI(user)
} catch NetworkError.noConnection {
    showOfflineAlert()
} catch let error as NetworkError {
    showError("네트워크 " + error.localizedDescription)
} catch {
    showGenericError()
}

enum NetworkError는 Error 프로토콜을 구현하며 세 가지 케이스를 정의합니다: noConnection, 코드가 있는 serverError, timeout. fetchUser 함수는 throws로 표시됩니다: 먼저 연결을 확인한 다음 요청 및 구문 분석을 수행합니다. do-catch 블록에서 세 개의 catch가 다양한 시나리오를 처리합니다: 특정 noConnection 케이스, 일반 NetworkError 유형 및 기타 모든 오류. 이를 통해 문제 유형에 따라 사용자에게 다른 메시지를 표시할 수 있습니다.

Kotlin에서의 Try-Catch

Kotlin은 Java에서 try-catch-finally를 상속받았지만 중요한 차이점을 추가했습니다: Kotlin에서 try-catch는 표현식(expression)이며 문(statement)이 아닙니다. 즉, try 블록 또는 catch 블록의 결과를 변수에 할당할 수 있습니다. try 블록의 마지막 표현식은 성공 시 결과가 되고, catch의 마지막 표현식은 오류 시 결과가 됩니다. 오류가 어떤 catch에서도 처리되지 않으면 예외는 스택을 올라갑니다.

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

fun loadData(): Result<List<Item>> {
    return try {
        val response = api.getItems()
        Result.Success(response.toList())
    } catch (e: HttpException) {
        Log.e("HTTP ", e)
        Result.Error(e)
    } catch (e: IOException) {
        Log.e("네트워크 ", e)
        Result.Error(e)
    }
}

예제에서 sealed class Result는 성공 응답 또는 오류를 래핑합니다. loadData 함수는 try-catch를 표현식으로 사용합니다: 성공 시 Result.Success를 반환하고, HttpException 또는 IOException 시 — 로깅과 함께 Result.Error를 반환합니다. 이 접근 방식을 통해 호출자는 예외 없이 오류를 처리할 수 있습니다 — Result 유형에 대한 when 표현식을 통해. 이는 Jetpack Compose에서 StateFlow 및 collectAsState를 통해 다양한 UI 상태(Loading, Success, Error)를 표시하는 데 특히 편리합니다.

Dart 및 Flutter에서의 Try-Catch

Dart는 Java와 유사한 구문으로 try-catch-finally를 지원하지만, 변수를 지정하지 않고 예외 유형별로 필터링하기 위한 on 절이 추가되었습니다. 이는 예외 자체가 필요하지 않을 때 편리합니다 — 유형의 사실만 중요합니다. Dart는 또한 두 개의 매개변수(예외 객체 및 StackTrace)를 가진 catch 블록을 지원하여 전체 호출 체인 기록에 유용합니다.

dart
import 'dart:io';
import 'dart:convert';

class UserRepository {
    Future<User> fetchUser(String id) async {
        try {
            final client = HttpClient();
            final request = await client.getUrl(
                Uri.parse('https://api.example.com/users/$id')
            );
            final response = await request.close();
            final body = await response.transform(utf8.decoder).join();
            return User.fromJson(json.decode(body));
        } on SocketException catch (e, stackTrace) {
            log("No internet", e, stackTrace);
            throw AppException("Connection failed");
        } on FormatException {
            throw AppException("Invalid response format");
        } finally {
            client.close();
        }
    }
}

SocketException은 상세 로깅을 위해 예외 객체와 StackTrace로 포착된 후 AppException으로 다시 던져집니다. FormatException은 변수 없이 포착됩니다 — 응답 형식이 잘못되었음을 아는 것으로 충분합니다. finally 블록은 HttpClient를 닫는 것을 보장하여 소켓 누출을 방지합니다. Flutter에서 이 접근 방식은 Widget 테스트에 특히 중요합니다. State.initState에서 처리되지 않은 예외가 전체 테스트 세션을 중단시키기 때문입니다.

Try-Catch 사용 시 흔한 실수

경험이 많은 개발자도 try-catch 작업 시 메모리 누수, 숨겨진 버그 또는 애플리케이션의 부적절한 동작을 유발하는 실수를 범합니다. 모바일 개발에서 가장 흔한 다섯 가지 문제를 살펴보겠습니다.

빈 catch 블록

빈 catch는 최악의 관행 중 하나입니다. 예외가 삼켜지고, 애플리케이션은 잘못된 상태에서 계속 작동하며, 개발자는 문제를 알지 못합니다. 항상 최소한 예외를 기록하세요. Kotlin에서는 catch(e: Exception) { Log.e(...) }를, Swift에서는 catch { print($0) }를 사용하세요. Dart에서는 최소 허용 가능한 catch가 debugPrint를 호출하거나 Crashlytics에 기록해야 합니다.

너무 광범위한 catch

유형을 구분하지 않고 catch (Exception e)를 통해 모든 예외를 포착하면 예상치 못한 오류(NullPointerException, OutOfMemoryError, StackOverflowError)가 숨겨집니다. 예상하고 처리할 수 있는 유형만 포착하세요. 나머지는 상위로 전파되도록 허용하세요. 모바일 개발에서 IOException, TimeoutException, AuthException에 대한 특정 catch가 사용자에게 더 의미 있는 메시지를 제공합니다.

finally 무시

리소스(파일, 소켓, DB 커서, 애니메이션)는 finally 또는 use 블록(AutoCloseable)에서 해제되어야 합니다. 개발자들은 예외 발생 시 리소스를 닫는 것을 자주 잊어 누출을 유발합니다. Kotlin에서는 Closeable 리소스에 .use { }를, Swift에서는 defer { }를, Dart에서는 async 패키지의 await using을 사용하세요. finally 블록은 catch 내에서 예외가 던져져도 해제를 보장합니다.

스레드 및 코루틴에서의 예외

비동기 코드에서 try-catch는 다른 스레드의 예외를 포착하지 않습니다. Kotlin Coroutines에서는 CoroutineExceptionHandler 또는 SupervisorJob을 사용하세요. Swift async/await에서는 Task 내부의 do-catch를 사용하세요. Flutter에서는 전역 포착을 위해 runZonedGuarded를 사용하세요. 이 규칙을 무시하면 프로덕션에서 재현하기 어려운 충돌이 발생합니다.

예외 발생 시 UI 차단

예외 처리는 사용자 인터페이스를 무기한 차단해서는 안 됩니다. 사용자에게 구체적인 메시지를 표시하고 작업을 재시도할 기회를 제공하세요. Kotlin/Compose의 Retry 버튼이 있는 Snackbar, Swift의 UIAlertController, Flutter의 SnackBar — 네트워크 또는 서버 오류에 대한 최소한의 UX입니다. 복구 옵션이 없는 일반적인 “오류가 발생했습니다” 대화상자는 피하세요.

자주 묻는 질문

try-catch와 Result Type의 차이점은 무엇인가요?

Try-catch는 오류 처리를 위해 예외와 스택 풀기를 사용하며, 많은 오류를 다룰 때 성능 비용이 발생할 수 있습니다. Result Type은 컨테이너 유형(Success 또는 Failure)으로, 스택 풀기 없이 패턴 매칭을 통해 처리되어 예상된 오류에 더 효율적입니다.

모든 try-catch에서 finally를 사용해야 하나요?

Finally는 try 블록이 닫아야 할 리소스(파일, 소켓, 커서)를 여는 경우 필수입니다. 리소스를 열지 않으면 finally가 필요하지 않습니다. 최신 언어에서는 finally 없이 자동 리소스 종료를 위해 AutoCloseable/use/defer를 사용하세요. Kotlin과 Swift의 use 블록은 Closeable 객체에 대해 finally를 대체합니다.

try-catch가 느릴 수 있나요?

정상 흐름(예외 없음)에서 try-catch는 성능에 거의 영향을 미치지 않습니다 — JVM과 Swift 컴파일러가 이 경우를 최적화합니다. 그러나 예외가 던져지면 스택 풀기가 발생하며, 스택 깊이에 따라 10–100 μs가 소요될 수 있습니다. 흐름 제어에 예외를 사용하지 마세요 — 이것은 안티패턴입니다.

Kotlin Coroutines에서 오류를 처리하는 방법은?

코루틴에서는 전역 포착을 위해 coroutineScope 내부의 try-catch 또는 CoroutineExceptionHandler를 사용하세요. SupervisorJob은 자식 코루틴이 실패할 때 부모 코루틴이 취소되는 것을 방지합니다. launch에는 CoroutineExceptionHandler를, async에는 await() 주변에 try-catch를 사용하세요.

여러 catch와 if-else가 있는 하나의 catch 중 무엇이 더 좋나요?

여러 catch가 더 좋습니다: 코드가 선형적으로 읽히고 각 블록이 하나의 예외 유형을 처리합니다. if-else가 있는 하나의 catch는 유지 관리가 어렵고 새로운 예외 유형을 놓치기 쉽습니다. Swift에서는 enum Error의 포괄적 처리를 위해 여러 catch가 필수이며, Kotlin에는 제한이 없지만 최상의 관행은 유형별로 별도의 catch를 사용하는 것입니다.

요약

  • Try-Catch는 try, catch 및 선택적 finally 블록으로 구성된 예외 포착 구조로 리소스 해제를 보장합니다
  • 작동 메커니즘 — 예외 발생 시 스택 풀기 및 오류 유형별 일치하는 catch 검색
  • Swift에서는 enum Error를 사용한 do-catch, 선택적 결과를 위한 try?, 보장된 성공을 위한 try! 사용
  • Kotlin에서는 try-catch는 표현식으로 결과를 변수에 할당 가능, sealed class Result와 함께 편리
  • Dart에서는 변수 없는 유형 필터링을 위한 on 절과 HttpClient 종료를 위한 finally 지원
  • 흔한 실수: 빈 catch, 너무 광범위한 catch, finally 무시, 코루틴에서의 미처리
  • Try-catch는 예상치 못한 오류에, Result Type은 실패 가능성이 있는 예상 시나리오에 사용

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기