Error Propagation은 오류가 발생한 지점에서 콜 스택을 따라 위로 핸들러까지 전파하는 메커니즘입니다. 함수가 오류를 자체적으로 처리할 수 없을 때, 예외(exception), throws 선언 또는 Return Type을 통해 호출자에게 전달합니다. 모바일 애플리케이션의 안정성을 위해서는 propagation의 올바른 구현이 중요합니다. 처리되지 않거나 잘못 전달된 오류는 충돌로 이어집니다. Apple Swift Documentation (2026)에 따르면, Swift에서 throws를 통한 자동 전파는 상용구 코드 없이 모든 수준으로 오류를 전달할 수 있게 해줍니다.
핵심 사항
Error Propagation(오류 전파)은 오류 객체가 발생한 함수에서 호출 체인을 따라 위로 가장 가까운 적절한 핸들러까지 전달되는 프로세스입니다. 호출 스택을 상상해보세요: ViewController가 ViewModel을 호출하고, ViewModel이 Repository를 호출하고, Repository가 API를 호출합니다. API가 네트워크 오류를 반환하면, Repository와 ViewModel을 통해 ViewController까지 전달되어 사용자에게 메시지를 표시합니다. 각 중간 함수는 오류를 처리할지 아니면 더 위로 전달(전파)할지 결정합니다.
전파에는 자동과 수동 두 가지 접근 방식이 있습니다. 자동 접근 방식(Swift throws, Java checked exceptions)에서는 컴파일러가 개발자에게 오류를 처리하거나 시그니처에서 전파를 선언하도록 강제합니다. 수동 접근 방식(Result Type, Kotlin Try)에서는 오류가 값으로 전달됩니다 — 개발자가 명시적으로 오류를 전달하거나 변환하는 코드를 작성합니다. Kotlin Result Docs (2026)에 따르면, Kotlin의 Result<T>는 함수 경계를 직접 넘는 전파를 위해 설계되지 않았습니다 — 각 수준에서 변환하거나 처리해야 하며, 이는 전파를 더 의식적으로 만들지만 더 장황하게 만듭니다.
접근 방식의 선택은 애플리케이션 아키텍처와 언어에 따라 달라집니다. Swift에서는 throws를 통한 자동 전파가 지배적이며, Kotlin에서는 예외(예기치 않은 오류)와 Result 유사 컨테이너(예상된 오류)의 혼합이 사용됩니다. 이해해야 할 중요한 점: 전파는 목적이 아니라 필요성입니다. 이상적인 아키텍처는 전파 깊이를 최소화하고, 결정을 내릴 충분한 컨텍스트가 있는 가장 낮은 수준에서 오류를 처리합니다.
Swift에서 throws를 통한 전파는 자동입니다: throws가 있는 함수 A가 throws가 있는 함수 B를 호출하고, A가 do-catch에서 B의 오류를 처리하지 않으면, 오류가 자동으로 A의 호출자에게 전달됩니다. 이는 체인의 각 메서드에서 throws를 선언해야 하는 Java checked exceptions의 전형적인 상용구 코드를 제거합니다. Swift는 원칙을 사용합니다: 체인에 하나의 throws 함수 = 전체 체인이 throws가 됩니다(중간 수준에서 처리되지 않는 경우).
struct UserRepository {
func fetchUser(id: Int) throws -> User {
let data = try networkService.request(path: "/users/\(id)")
return try parseUser(from: data)
}
}
class UserViewModel {
let repo = UserRepository()
func loadUser(id: Int) throws -> User {
return try repo.fetchUser(id: id)
}
}
// ViewController — 최종 핸들러
func onButtonTap() {
let vm = UserViewModel()
do {
let user = try vm.loadUser(id: 42)
updateUI(user)
} catch {
showError("사용자를 불러오지 못했습니다")
}
}
전파 체인: networkService.request -> fetchUser -> loadUser -> onButtonTap. 각 중간 함수는 throws로 표시되고 do-catch를 포함하지 않습니다 — 오류가 자동으로 위로 전달됩니다. ViewController onButtonTap이 do-catch를 가진 최종 핸들러입니다. ViewModel이 오류를 변환(다른 유형으로 래핑)하기로 결정했다면, do-catch와 새로운 throw를 사용할 수 있었을 것입니다. 자동 전파는 코드를 줄입니다: Repository는 오류 처리 방법을 알 필요가 없습니다 — 이는 사용자에게 메시지를 표시하기 위해 UI에 접근할 수 있는 ViewController의 책임입니다.
Kotlin에서 예외를 통한 전파는 시그니처에 throws 선언이 필요하지 않습니다(모든 예외는 unchecked). 예외는 try-catch를 만날 때까지 자동으로 스택을 올라갑니다. 그러나 시그니처에 throws가 없으면 전파가 암시적이 됩니다: 개발자는 함수 시그니처에서 예외를 던질 수 있다는 것을 알 수 없습니다. 이는 장점(상용구 감소)이자 단점(처리하기 쉽게 잊어버림)입니다. Kotlin은 이 문제를 언어 자체가 아니라 규칙과 아키텍처 패턴을 통해 해결합니다.
class UserRepository(
private val api: ApiService,
private val db: Database
) {
suspend fun getUser(id: String): User {
return try {
api.fetchUser(id)
} catch (e: IOException) {
db.getCachedUser(id) ?: throw AppException("User unavailable")
}
}
}
class UserViewModel(private val repo: UserRepository) {
private val _state = MutableStateFlow<UiState<User>>(UiState.Loading)
val state: StateFlow<UiState<User>> = _state
fun loadUser(id: String) {
viewModelScope.launch {
try {
val user = repo.getUser(id)
_state.value = UiState.Success(user)
} catch (e: AppException) {
_state.value = UiState.Error(e.message ?: "Unknown")
}
}
}
}
Repository에서의 변환을 통한 전파: IOException(네트워크 사용 불가) 발생 시 함수는 데이터베이스에서 캐시된 데이터를 가져오려고 시도합니다. 캐시가 비어 있으면 AppException을 던집니다 — 전파가 새로운 오류 유형으로 계속됩니다. ViewModel이 AppException을 잡아 UiState.Error로 변환합니다 — 오류는 더 이상 진행되지 않고, UI 레이어 수준에서 전파가 종료됩니다. Kotlin Coroutines는 특성을 추가합니다: launch의 예외는 CoroutineExceptionHandler를 통해 자동으로 전파되고, async에서는 await() 호출 시에만 전파됩니다. 코루틴에서 전파를 설계할 때 이는 중요합니다 — SupervisorJob은 하위 코루틴의 오류 시 상위 코루틴의 취소를 방지합니다.
예외의 대안은 성공 또는 오류를 값으로 전달하는 컨테이너 유형을 통한 전파입니다. 이 접근 방식에서 함수는 값을 반환하지 않고 래퍼를 반환합니다: Swift의 Result<T, E>, Kotlin의 Result<T>, Dart의 Either<L, R>(fpdart 또는 dartz 패키지). 오류는 스택을 해제하지 않습니다 — 단순히 컨테이너 안에 있으며, 다음 수준이 그것을 어떻게 처리할지 결정합니다. 이는 전파를 더 명시적이고 제어 가능하게 만듭니다.
data class HttpResult<out T>(
val data: T?,
val error: AppError?
) {
val isSuccess: Boolean get() = data != null
val isError: Boolean get() = error != null
}
sealed class AppError {
data class Network(val message: String) : AppError()
data class Auth(val message: String) : AppError()
}
fun fetchUser(id: String): HttpResult<User> {
return try {
val response = api.get("/users/$id")
HttpResult(data = parseUser(response), error = null)
} catch (e: IOException) {
HttpResult(data = null, error = AppError.Network("No internet"))
}
}
HttpResult<T>는 data 및 error 필드가 있는 간단한 컨테이너입니다. Sealed class AppError는 오류 유형(Network, Auth)을 정의합니다. fetchUser 함수는 HttpResult를 반환하고, 전파에 스택 해제가 필요하지 않습니다 — 호출자는 단순히 isSuccess/isError를 확인합니다. 이 접근 방식은 특히 Clean Architecture에서 유용하며, 각 레이어(data, domain, presentation)가 오류를 변환할 수 있습니다: IOError -> DomainError -> UiError. 컨테이너를 통한 전파는 이러한 변환을 명시적이고 테스트 가능하게 만듭니다. 변환 체인이 함수 시그니처에서 보이지 않는 예외와는 대조적입니다.
오류 처리 설계의 핵심 결정 중 하나는 전파(위로 전달)와 처리(여기서 처리) 사이의 선택입니다. 결정 규칙: 의미 있는 조치를 위한 충분한 컨텍스트가 있는 수준에서 오류를 처리하십시오. UI에 접근할 수 있는 경우 — 사용자에게 메시지를 표시합니다. 캐시에 접근할 수 있는 경우 — 복구를 시도합니다. 둘 다 없는 경우 — 전파합니다.
| 시나리오 | 조치 | 근거 |
|---|---|---|
| Repository의 네트워크 오류 | 전파 | Repository는 사용자가 요청을 재시도할지 알 수 없음 |
| Repository의 파싱 오류 | 처리(기본값 반환) | Repository는 형식을 알고 있어 폴백 값을 반환 가능 |
| ViewModel의 시간 초과 | 처리(UiState.Error) | ViewModel이 UiState를 관리하고 오류 변환 방법을 알고 있음 |
| Interceptor의 인증 오류 | 처리(토큰 갱신) | Interceptor가 토큰에 접근 가능하고 세션 복원 가능 |
| UseCase의 알 수 없는 오류 | 전파 | UseCase에 UI 컨텍스트 없음 — 비즈니스 로직만 |
황금률: 최소한의 전파, 하위 수준에서 최대한의 처리. Repository가 캐시에서 복구할 수 있다면 — 오류를 위로 전달하지 않고 그렇게 해야 합니다. ViewModel이 Snackbar를 표시할 수 있다면 — ViewController의 추가 코드 없이 표시하게 하십시오. 전파의 각 수준은 결합도를 높이고 테스트를 복잡하게 만듭니다. Google Android Architecture Guide (2026)에 따르면, ViewModel 수준에서 sealed class UiState를 사용하여 가능한 모든 상태(Loading, Success, Error)를 표현하고 예외를 UI 레이어에 직접 전달하지 않음으로써 레이어 경계를 넘는 전파를 최소화하는 것이 좋습니다.
잘못된 전파는 모바일 애플리케이션에서 찾기 어려운 버그의 원인입니다. 개발자가 직면하는 다섯 가지 주요 문제와 해결 방법을 살펴보겠습니다.
가장 흔한 문제: 전파 중에 예외가 잡히고 로깅된 후 원래 예외 없이 새로운 예외가 던져집니다. 개발자는 StackTrace를 잃고 오류가 정확히 어디서 발생했는지 이해할 수 없습니다. Swift에서는 오류 체이닝을 사용하세요: throw MyError(context: originalError). Kotlin에서는: throw AppException(cause = originalException). Dart에서는: throw AppException(message, originalException). cause/underlyingError를 전달하지 않고 새 예외를 절대 생성하지 마십시오.
catch (e: Exception) { /* 아무것도 하지 않음 */ }은 애플리케이션이 잘못된 상태에서 계속 작동하게 하는 안티패턴입니다. 오류를 무시해도 된다고 확신하는 경우 — 근거와 함께 주석을 추가하십시오. Swift에서는 선택적 무시를 위해 try?(오류 -> nil)를 사용하세요. Kotlin에서는 — Result<T>.onFailure { /* 로그 */ }. 로깅 없이 예외를 삼키지 마십시오.
오류가 처리 없이 5+ 수준을 통과하는 경우 아키텍처를 재고해야 합니다. 각 전파 수준은 하위 함수의 throws 시그니처에 대한 의존성입니다. 해결책: 레이어 경계에서 실패 컨테이너(sealed class Result { Success, Error })를 사용하여 전파를 명시적이고 제한적으로 만드십시오. 전파 체인이 짧을수록 코드 테스트와 디버깅이 더 쉬워집니다.
Kotlin Coroutines에서 launch의 예외는 기본적으로 상위 코루틴과 모든 sibling(동일 범위의 자식)을 취소합니다. 10개의 병렬 작업 중 하나가 실패하면 나머지 9개가 취소되며, 이는 종종 바람직하지 않습니다. 오류를 격리하려면 SupervisorJob 또는 supervisorScope를 사용하십시오: 하나의 자식에서 오류가 sibling을 취소하지 않습니다. ViewModelScope는 기본적으로 SupervisorJob을 사용하여 Android에서 이 문제를 방지합니다.
콜백 기반 API에서 오류는 종종 콜백의 매개변수로 전달됩니다. 콜백이 오류를 처리하지 않거나(또는 잘못 처리하면) 전파가 암시적이 되어 쉽게 손실됩니다. 해결책: async/await(Swift) 또는 코루틴(Kotlin)으로 마이그레이션하십시오. 여기서 전파는 표준 try-catch 메커니즘을 통해 작동합니다. 콜백이 불가피한 경우 — Either<Error, T> 또는 Result<T>를 사용하여 두 경우 모두를 강제로 처리하십시오.
자주 묻는 질문
Throw는 예외를 던지는 일회성 작업입니다. Error Propagation은 throw에서 catch까지 여러 스택 수준을 통해 오류를 전달하는 전체 프로세스입니다. 전파에는 throw, 중간 함수를 통한 자동 또는 수동 전달 및 최종 처리가 포함됩니다. 오류의 수명 주기를 설명하는 더 넓은 개념입니다.
특정 시나리오에서 예외를 던지는 모의 객체를 사용하십시오. assertThrows(Kotlin/JUnit) 또는 XCTAssertThrowsError(Swift/XCTest)를 통해 함수가 오류를 올바르게 전파하거나 처리하는지 확인하십시오. Result 기반 전파의 경우 isSuccess/isError와 두 경우의 값을 확인하십시오.
Result 전파는 하나의 아키텍처 경계 내에서 예상된 오류(잘못된 데이터, 비즈니스 규칙)에 적합합니다. 예외는 높은 수준에서 처리되어야 하는 예기치 않은 오류(네트워크 손실, I/O 오류)에 더 적합합니다. 오류가 있는 결과는 실행 흐름을 중단하지 않지만 예외는 중단합니다.
Kotlin Coroutines에서 launch의 예외는 sibling 취소와 함께 CoroutineScope를 통해 자동으로 전파됩니다. 격리를 위해 supervisorScope 또는 SupervisorJob을 사용하십시오: 하나의 코루틴에서 오류가 다른 코루틴을 취소하지 않습니다. async의 경우 await() 호출 시 try-catch를 통해 명시적으로 오류를 처리해야 하며, 그렇지 않으면 삼켜집니다.
이상적인 최종 핸들러는 UI 레이어(ViewController, Fragment/Composable)입니다. 오직 UI 레이어만이 사용자 인터페이스에 접근할 수 있고 메시지, Snackbar 또는 대화상자를 표시할 수 있습니다. 중간 레이어(Repository, UseCase, ViewModel)는 오류를 전파하고, 필요한 경우 더 추상적인 도메인 유형으로 변환합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.