예외처리와 콜렉션

BS_Lee·2025년 7월 13일

swift

목록 보기
14/21

throws, try, do-catch, 그리고 rethrows

throws는 뭘까?

Swift에서 throws에러를 던질 수 있는 함수를 의미한다.
즉, 이 함수는 문제가 생기면 직접 에러를 호출한 쪽으로 "던질 수 있다".

func someFunc() throws -> String

바로 try를 붙여서 호출해야 한다.

do {
    let result = try someFunc()
} catch {
    print("에러 발생: \(error)")
}

또는 try?를 쓰면 실패 시 nil을 반환하고, try!는 실패 시 런타임 에러를 낸다.


rethrows란?

rethrows는 이렇게 정의할 수 있다.

자신은 에러를 던지지 않지만, 전달받은 클로저가 에러를 던지면 그걸 대신 던질 수 있게 해주는 키워드다.

예를 들어 다음과 같은 함수가 있다고 하자

func fullName(
    firstName: String?, 
    lastName: String?,
    calculator: (String?, String?) throws -> String?
) rethrows -> String? {
    try calculator(firstName, lastName)
}

fullName 함수는 직접 에러를 만들지는 않지만,
calculator라는 클로저가 에러를 던질 수 있다면, 그 에러를 호출한 쪽으로 다시 던질 수 있다.

그럼 실제로 어떻게 동작하는지 예제를 보자.

💡 예제: 에러를 던지는 클로저를 넘기기

먼저, 에러 타입부터 정의해보자.

enum NameErrors: Error {
    case firstNameIsInvalid
    case lastNameIsInvalid
}

이제 +라는 함수(클로저로 넘길 수 있는 형태)를 만들어보자.
이 함수는 에러를 던질 수 있다.

func + (
    firstName: String?,
    lastName: String?
) throws -> String? {
    guard let firstName, !firstName.isEmpty else {
        throw NameErrors.firstNameIsInvalid
    }
    guard let lastName, !lastName.isEmpty else {
        throw NameErrors.lastNameIsInvalid
    }
    return "\(firstName) \(lastName)"
}

그리고 이 + 함수를 fullName 함수에 넘겨서 사용해보자.

do {
    let fooBar = try fullName(firstName: "foo", lastName: nil, calculator: +)
    print(fooBar ?? "")
} catch NameErrors.firstNameIsInvalid {
    print("First name is invalid")
} catch NameErrors.lastNameIsInvalid {
    print("Last name is invalid")
} catch {
    print("Something went wrong")
}

그러면 여기서 생기는 의문 하나

fullName 함수는 에러를 직접 던지지 않는데, 왜 do-catch로 감싸야 하지?

바로 이 지점이 rethrows의 핵심이다.

  • fullName은 에러를 직접 던지지 않는다.
  • 하지만 전달받은 클로저(+ 함수)가 throws 함수이기 때문에,
  • 해당 클로저에서 에러가 발생하면, fullName은 그 에러를 "다시 던질 수" 있다.

🧠 한 줄 요약

rethrows는 "나는 에러를 만들진 않지만, 받은 클로저가 던진다면 그걸 대신 던질게"라는 선언이다.

Swift는 이렇게 정적 타입명시적 에러 처리를 중요하게 여기는 언어다.
그래서 throwsrethrows의 쓰임새를 구분해서 설계하게 만든다.
이런 부분이 처음엔 번거롭지만, 점점 더 코드가 안정적으로 느껴지는 이유이기도 하다.


에러 처리와 옵셔널, 어떤 차이가 있을까?

이건 정말 자주 헷갈렸다.
throwOptional은 모두 실패 상황을 표현할 수 있는데, 어떤 차이점이 있을까?

  • throws예외 상황을 에러 타입으로 전달한다.
  • Optional값이 없을 수도 있는 정상적인 흐름을 표현한다.
func getName() throws -> String?

이렇게 쓰면, "에러"와 "값 없음(nil)"을 명확히 구분할 수 있어서 더 유용한 경우가 많다.


compactMap은 그냥 map이랑 뭐가 다를까?

compactMap은 map의 기능 + nil 제거 기능이 붙어 있다.
즉, 변환을 하고 나서 nil은 제거하는 기능까지 포함이다.

let numbers = [1, 2, 3, 4]
let evenStrings = numbers.compactMap {
    $0 % 2 == 0 ? String($0) : nil
}
// 결과: ["2", "4"]

단순히 map만 쓰면 Optional이 남는데, compactMap은 그런 거 없이 깔끔하게 나온다.


AnyHashable은 언제 쓰는 걸까?

다양한 타입이 하나의 컬렉션(Set, Dictionary 등)에 있어야 할 때 사용한다.
단, 이 타입들은 전부 Hashable이어야 한다.

let items: Set<AnyHashable> = [1, "hi", true]

각 아이템의 원래 타입은 내부적으로 유지되지만, 외부에서는 AnyHashable로 동작한다.


Hashable, hash(into:), 그리고 ==

해시 기반 컬렉션(Set, Dictionary 등)을 쓰려면, 타입이 Hashable을 따라야 한다.
그럼 두 가지 필수 메서드가 생긴다.

hash(into:)

  • 이건 해시값을 계산하는 메서드다.
  • 보통 id 같은 고유값을 이용해서 구현한다.

==

  • 두 객체가 같은지 비교할 때 기준이 되는 연산자다.
struct Person: Hashable {
    let id: UUID
    let name: String
    let age: Int

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }

    static func == (lhs: Person, rhs: Person) -> Bool {
        lhs.id == rhs.id
    }
}

switch에서 let을 꼭 써야 하는 이유

예를 들어 이런 코드가 있다고 하자.

switch result {
case .success(let value):
    print(value)
case .failure(let error):
    print(error)
}

여기서 let은 뭐냐면, case 패턴에서 값을 꺼내서 변수에 바인딩하는 역할이다.
Swift는 명시적인 언어라서, 바인딩 여부도 확실하게 표시해줘야 한다.

그래서 case let .success(value)처럼 써도 된다. 같은 의미다.


[String: Any] 왜 이렇게 캐스팅할까?

딕셔너리에 여러 타입의 값이 섞여 있으면, Swift는 타입을 제대로 추론하지 못한다.
그래서 강제로 [String: Any]로 명시해주는 것이다.

let userInfo = [
    "name": "Foo",
    "age": 20,
    "address": [
        "line": "Dongjak",
        "postCode": "123"
    ]
] as [String: Any]

안 그러면 에러가 난다.


as? [String: String]은 어떤 상황에서 쓰지?

위에서 본 userInfo["address"]는 타입이 Any다.
근데 실제론 [String: String]이니까, 그걸 다시 안전하게 다운캐스팅해서 꺼내야 한다.

if let address = userInfo["address"] as? [String: String] {
    print(address["line"] ?? "")
}

안전하게 타입을 확인하고 꺼내는 데 필요한 문법이다.


핵심 한 줄 요약

개념설명
throws / try함수 내에서 에러를 던지고, 호출자는 반드시 처리
rethrows클로저가 에러를 던지면 그걸 다시 전달
compactMapnil 제거 + 변환 map
AnyHashable서로 다른 타입을 하나의 Hashable 컬렉션에 넣을 수 있음
hash(into:) / ==해시 기반 비교/저장을 위한 필수 요소
switch let바인딩 시 let은 필수
as [String: Any]혼합 타입 딕셔너리에서 명시적 타입 선언
as? [String: String]다운캐스팅을 통한 안전한 타입 접근

0개의 댓글