Throw와 Throws는 프로그래밍 언어에서 예외를 던지고 선언하는 메커니즘입니다. throw 연산자는 함수의 정상적인 실행을 중단하고 오류 객체를 호출 스택 위로 전달합니다. 함수 시그니처의 throws 키워드는 호출하는 쪽에 오류 가능성을 경고하여 코드를 예측 가능하게 만듭니다. Kotlin 문서(2026)에 따르면, Kotlin의 throw는 표현식(expression)이며 명령문(statement)이 아니므로 when 블록과 엘비스 연산자 내에서 사용할 수 있습니다.
핵심 요점
Throw는 프로그램 실행 지점에서 예외를 생성하는 연산자입니다. throw를 만나면 현재 실행 흐름이 즉시 중단되고 제어가 호출 스택에서 가장 가까운 catch 핸들러로 전송됩니다. 핸들러를 찾을 수 없으면 애플리케이션이 충돌합니다. 모바일 개발에서 throw는 현재 추상화 수준에서 처리할 수 없는 오류(예: 잘못된 서버 응답, 네트워크 부재, 잘못된 인수)를 알리는 데 사용됩니다.
Throws — 함수 시그니처의 수정자(주로 Swift)로, 함수가 오류를 던질 수 있음을 선언합니다. 이는 Swift의 checked 오류 메커니즘의 일부입니다. 호출하는 쪽은 do-catch, try?, try!를 통해 오류를 처리하거나 추가 전파를 위해 자체 함수를 throws로 표시해야 합니다. Kotlin과 Java에도 throws가 존재하지만, Kotlin은 이를 중복이라고 간주합니다. Kotlin의 모든 예외는 unchecked이며, 구문적 강제 없이 처리되지 않을 수 있습니다. Apple Swift 문서(2026)에 따르면, Swift의 throws는 함수 타입에서 오류 가능성을 명시적으로 선언하는 유일한 방법이며, 개발자에게 API 계약을 투명하게 만듭니다.
throw와 throws의 차이는 근본적입니다. throw는 동작(런타임에 예외 던지기)이고, throws는 선언(컴파일 타임 계약)입니다. throws가 없는 함수는 throw를 사용할 수 없습니다. Swift 컴파일러가 오류를 생성합니다. throws가 있는 함수는 throw를 사용하지 않을 수 있습니다(허용되지만 무의미함). 이러한 분리는 throw/throws를 강력한 API 설계 도구로 만들어, 함수 호출 전에 시그니처에서 오류 계약을 볼 수 있게 합니다.
Swift에서 throw 연산자는 Error 프로토콜을 구현하는 모든 타입을 허용합니다. 가장 일반적인 것은 다양한 종류의 오류에 대한 케이스가 있는 enum입니다. Swift는 Java 스타일의 checked 예외를 지원하지 않습니다. 대신 함수 타입이 throws로 표시되고 처리가 호출하는 쪽에 위임됩니다. 이렇게 하면 Swift의 throw가 더 유연해지지만 개발자에게 더 많은 책임이 생깁니다.
Error 프로토콜을 준수하는 모든 타입은 throw를 통해 던질 수 있습니다. 개발자는 일반적으로 간단한 오류의 경우 연관 값이 없는 enum을, 컨텍스트를 전달하는 경우 연관 값이 있는 enum을 사용합니다. Swift는 오류가 enum일 것을 요구하지 않습니다. Error를 구현하는 struct나 class를 사용할 수 있지만, 핸들러 측의 exhaustive switch 덕분에 enum이 선호됩니다. 컴파일러는 do-catch에서 모든 케이스가 처리되었는지 확인합니다.
enum AuthError: Error {
case invalidCredentials
case tokenExpired
case accountLocked(remainingMinutes: Int)
}
func login(username: String, password: String) throws -> Session {
guard isValid(username) else {
throw AuthError.invalidCredentials
}
let response = try api.authenticate(username, password)
if response.isLocked {
throw AuthError.accountLocked(
remainingMinutes: response.lockDuration
)
}
return Session(token: response.token)
}
AuthError는 세 가지 시나리오(잘못된 자격 증명, 만료된 토큰, remainingMinutes의 연관 값이 있는 잠긴 계정)를 정의합니다. login 함수는 throws로 선언되며, 컴파일러는 try를 통한 호출을 요구합니다. 함수 내에서 throw는 잘못된 사용자 이름과 잠긴 계정의 두 곳에서 사용됩니다. 연관 값 accountLocked는 사용자에게 구체적인 데이터(잠금 해제까지 기다려야 하는 시간)를 전달할 수 있게 합니다. 이 접근 방식은 잠금 상태를 확인하기 위한 별도의 API 엔드포인트를 제거합니다.
Kotlin에서 throw는 Nothing 타입의 표현식이며 명령문이 아닙니다. 즉, throw는 할당의 오른쪽, when 표현식 내부, 엘비스 연산자 ?:에서 사용할 수 있습니다. Nothing 타입은 Kotlin의 모든 타입의 특별한 하위 타입으로, 모든 타입의 값이 필요한 곳에서 throw를 사용할 수 있게 합니다. 컴파일러는 throw 이후에 실행이 계속되지 않음을 이해하고 이 경우에 대한 분기를 요구하지 않습니다.
Nothing은 Kotlin의 고유한 타입으로, 모든 가능한 타입의 하위 타입입니다. Nothing을 반환하는 함수(예: TODO())는 정상적으로 완료되지 않으며, 항상 예외를 던지거나 무한 루프에 빠집니다. 이렇게 하면 throw는 값이 필요한 곳(엘비스 연산자, else 없는 when, 변수 초기화)에서 사용하기에 자연스러운 후보가 됩니다. throw가 when 분기에 있으면 컴파일러는 해당 분기가 Nothing으로 이어짐을 이해하고 해당 분기에 대한 return이나 else를 요구하지 않습니다.
data class Config(val apiUrl: String, val timeoutSec: Int)
class ConfigParser {
fun parse(json: String): Config {
val obj = JSONObject(json)
val url = obj.optString("apiUrl")
?: throw IllegalArgumentException("apiUrl is required")
val timeout = obj.optInt("timeoutSec", 30)
return Config(url, timeout)
}
fun getErrorMessage(code: Int): String {
return when (code) {
404 -> "Not found"
500 -> "Server error"
else -> throw IllegalArgumentException("Unknown code: $code")
}
}
}
첫 번째 예제에서는 엘비스 연산자 ?:에서 throw가 사용됩니다. JSON에 apiUrl 필드가 없으면 throw 표현식이 즉시 실행을 중단하고 IllegalArgumentException을 던집니다. Nothing 타입을 통해 컴파일러는 오른쪽의 타입을 String으로 추론할 수 있습니다(엘비스는 String을 기대하고, throw의 타입은 Nothing이며, Nothing은 String의 하위 타입입니다). 두 번째 예제에서는 when 표현식 내부에서 throw가 사용됩니다. 코드가 알려진 코드와 일치하지 않으면 예외가 던져집니다. 컴파일러는 throw 이후의 코드에 도달할 수 없음을 이해하므로 함수의 반환 타입 String이 위반되지 않습니다.
Swift에서 throws는 매개변수 목록 뒤, 반환 타입 화살표 앞에 지정됩니다. throws가 있는 함수는 do-catch 내부 또는 try?를 통해서만 다른 throws 함수를 호출할 수 있습니다. throws 함수가 오류를 처리하지 않으면 호출하는 쪽으로 전달합니다. Swift는 또한 rethrows를 지원합니다. throws 클로저를 받아 그 오류를 전파하는 고차 함수를 위한 수정자입니다. rethrows는 전달된 클로저가 오류를 던진 경우에만 함수가 오류를 던짐을 의미합니다. 함수 자체는 오류를 생성하지 않습니다.
func mapValues<T>(
_ array: [T],
transform: (T) throws -> U
) rethrows -> [U] {
var result = [U]()
for element in array {
result.append(try transform(element))
}
return result
}
// throws 클로저와 함께 사용
let parsed = try mapValues(jsonStrings) { str in
let data = Data(str.utf8)
return try JSONDecoder().decode(Item.self, from: data)
}
rethrows는 mapValues 함수를 유연하게 만듭니다. throws 클로저와 일반 클로저를 모두 허용합니다. throws 클로저가 전달되면 mapValues 호출에 try가 필요하고, 일반 클로저의 경우 try가 필요하지 않습니다. 이렇게 하면 rethrows는 Swift 표준 라이브러리의 map, filter, reduce와 같은 고차 함수에 이상적입니다. Apple의 권장 사항: throws 클로저를 받고 오류의 유일한 소스가 해당 클로저인 API에는 rethrows를 사용하세요. 함수가 자체 오류를 던질 수 있으면 throws를 사용하세요.
Swift의 throws는 checked 예외(Java처럼)에 더 가깝습니다. 던져진 오류는 시그니처에서 선언됩니다. Kotlin과 Dart는 unchecked 예외를 사용합니다. 시그니처에 throws가 필요하지 않습니다. 차이는 근본적입니다. checked는 개발자에게 오류 처리를 강제하고(더 안전하지만 장황함), unchecked는 자유를 주지만 오류 처리를 잊을 위험을 높입니다. Swift는 throws에 checked를 선택했고, Kotlin은 모든 예외에 unchecked를 선택했습니다. 두 접근 방식 모두 장점이 있습니다. Swift는 언어 수준에서 더 안정적이고, Kotlin은 더 간결하며 함수형 변환 체인에 편리합니다.
모바일 애플리케이션에서 구조화된 오류 처리를 위해서는 기본 Exception이나 Error 대신 사용자 정의 오류 타입을 만드는 것이 좋습니다. Swift에서는 Error 프로토콜이 있는 enum, Kotlin에서는 Throwable(또는 Exception)을 상속하는 sealed class, Dart에서는 Exception을 상속하는 class가 사용됩니다. 사용자 정의 타입을 사용하면 오류를 범주별로 그룹화하고 연관 데이터를 전달할 수 있습니다.
| 언어 | 오류 타입 | 특징 |
|---|---|---|
| Swift | enum: Error { ... } | 연관 값, catch에서 exhaustive switch |
| Kotlin | sealed class : Throwable() | 필드가 있는 오류용 Data class, when 표현식 |
| Dart | class implements Exception | Message 필드, catch의 on 절 |
| Java | class extends Exception | Checked vs unchecked, 시그니처에 throws 필수 |
사용자 정의 오류를 설계할 때는 하나의 오류 — 하나의 시나리오 규칙을 따르세요. String message 플래그가 있는 하나의 타입에 여러 원인을 결합하지 말고 각 시나리오에 대해 별도의 케이스/하위 클래스를 만드세요. 이렇게 하면 호출하는 쪽이 문자열 비교 대신 패턴 매칭(when/switch)을 통해 각 케이스를 처리할 수 있습니다. Swift에서는 이것이 exhaustive checking을 제공하여 NetworkError enum의 케이스가 처리되지 않은 경우 컴파일러가 경고합니다.
sealed class NetworkError(val message: String) : Throwable(message) {
data class Timeout(val durationMs: Long) :
NetworkError("Request timed out after ${durationMs}ms")
data class HttpError(val code: Int, val body: String?) :
NetworkError("HTTP $code")
data class NoConnection(val cause: IOException) :
NetworkError("No internet connection")
}
Sealed class NetworkError는 Throwable(Kotlin의 표준 예외 타입)을 상속합니다. 각 하위 클래스는 자체 필드가 있는 data class입니다. Timeout에는 밀리초 단위의 제한 시간이 포함되고, HttpError에는 코드와 응답 본문이 포함되며, NoConnection에는 원래 IOException이 포함됩니다. 이 설계를 통해 when을 통한 전체 매칭으로 각 오류를 처리할 수 있습니다(새 하위 클래스를 추가하면 컴파일러가 모든 when 표현식을 업데이트하도록 강제합니다).
Swift는 throws 함수를 호출하는 세 가지 방법을 제공하며, 각각 고유한 안전 계약이 있습니다. try는 표준 방식으로, do-catch가 필요하거나 throws 함수 내부에 있어야 합니다. try?는 오류를 nil로 변환합니다. 결과는 옵셔널이 되고, 오류 시 nil을 반환하며, 타입은 T에서 T?로 변경됩니다. try!는 처리 없이 강제 실행합니다. 오류가 던져지면 애플리케이션이 충돌합니다. try!는 오류가 절대 불가능하다고 확신하는 경우(예: 명백히 유효한 데이터)에만 사용하세요.
let configPath = Bundle.main.path(forResource: "config", ofType: "json")!
// try? — 옵셔널 결과
let data = try? Data(contentsOf: URL(fileURLWithPath: configPath))
let json = try? JSONSerialization.jsonObject(with: data ?? Data())
// try! — 보장된 성공(확실한 경우에만)
let decoder = JSONDecoder()
let defaultConfig = try! decoder.decode(
Config.self,
from: Config.defaultJSON
)
// try — 표준 처리
do {
let user = try fetchUser()
showUser(user)
} catch let error as NetworkError {
showRetryAlert(error.message)
}
예제에서 try!는 앱 번들에 포함된 명백히 유효한 JSON에 사용됩니다. 올바른 릴리스에서는 디코딩 오류가 불가능합니다. try?는 설정 파일 읽기에 사용됩니다. 파일이 없거나 손상된 경우 앱은 충돌 대신 기본값을 사용합니다. do-catch의 try는 오류가 예상되고 사용자 응답이 필요한 네트워크 요청에 사용됩니다. 권장 사항: 프로덕션 코드에서 try!를 피하고 빌드 시간에 검증된 상수 데이터에만 사용하세요.
자주 묻는 질문
Throw는 프로그램 실행 중에 오류를 던져 흐름을 중단하는 연산자입니다. Throws는 함수가 오류를 던질 수 있음을 선언하는 함수 시그니처 수정자입니다. throws가 없는 함수는 throw를 사용할 수 없습니다. Throws는 컴파일 타임 계약이고, throw는 런타임 동작입니다.
Kotlin은 unchecked 예외의 철학을 따릅니다. 모든 예외는 구문적 강제 없이 처리되지 않은 상태로 남을 수 있습니다. Kotlin 개발자는 Java의 throws가 과도한 try-catch 블록과 빈 catch를 통한 checked 예외 무시로 이어진다고 생각합니다. Kotlin의 Nothing 타입은 throw를 표현식으로 사용할 수 있게 하여 throws를 더 유연한 접근 방식으로 대체합니다.
try!는 번들의 명백히 유효한 JSON, 상수 데이터, 올바른 URL 스키마 등 오류가 절대 불가능하다고 확신하는 경우에만 허용됩니다. 프로덕션 코드에서 try!는 예외이며 규칙이 아닙니다. try?는 폴백 값이 있는 옵셔널 시나리오에 선호되며, 필수 오류 처리에는 try와 do-catch를 사용하세요.
Rethrows는 throws 클로저를 받는 함수를 위한 수정자입니다. rethrows가 있는 함수는 전달된 클로저가 오류를 던진 경우에만 오류를 던집니다. 이렇게 하면 고차 함수(map, filter)가 호출하는 쪽에 try를 강제하지 않고 throws와 비-throws 클로저 모두에서 작동할 수 있습니다.
네, catch 내에서 throw를 사용하여 오류를 다른 타입으로 래핑하거나 컨텍스트를 추가하여 스택 위로 전파할 수 있습니다. 이를 오류 체이닝(error chaining) 또는 다시 던지기(rethrow)라고 합니다. Swift에서는 catch 내에서 또 다른 throw만으로 충분하고, Kotlin에서는 catch 블록 내에서 throw합니다. finally 블록은 제어가 더 전달되기 전에 실행됩니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.