Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
iOS Developer/Concurrency

0. Do - Catch / Try / Throw 에러처리

by Toughie 2023. 7. 5.

Do - Catch / Try / Throw 에러처리

네트워킹, 비동기 처리를 하다보면 매우 자주 보이는 do - catch, try, throw에 대해 알아보자.

변천사를 알아보는 느낌으로..

 

 

데이터를 다운받는 데이터 매니저, 그리고 뷰모델, 간단한 데이터를 보여주는 뷰가 있다고 치자.

데이터 매니저에서 데이터를 다운받는 함수가 만약 실패 가능성이 있다면 어떻게 처리할 수 있을까?

 

먼저 옵셔널로 처리한다면

import SwiftUI

final class DoCatchTryThrowDataManager {
    
    let isActive: Bool = false
    
        func getTitle() -> String? {
            if isActive {
                return "New Text"
            } else {
                return nil
            }
        }
...

이 경우 isActive에 따라 옵셔널 스트링이 반환되거나, nil이 반환 될 것이다.

 

하지만 왜 데이터 다운로드를 실패했는지 알고 싶다면 어떻게 할 수 있을까?

에러도 같이 반환하도록 하면 될까?

    func getTitle() -> (title: String?, error: Error?) {
        if isActive {
            return ("New Text", nil)
        } else {
            return (nil, URLError(.badURL))
        }
    }
    
    
    //뷰모델에서 해당 코드를 호출
    @Published var text: String = "Start Text"
    
    func fetchTitle() {
        let returnedValue = manager.getTitle()
        if let newtitle = returnedValue.title {
            self.text = newtitle
        } else if let error = returnedValue.error {
            self.text = error.localizedDescription
        }
    }

하지만 이렇게 튜플 형태로 리턴형을 사용하는 경우는 드물고.. 다소 비효율적으로 보인다.

데이터와 에러를 둘 다 반환해서 확인하기 보다는 데이터 다운로드 성공/실패를 나눠서 볼 수 있다면?

    func getTitle2() -> Result<String, Error> {
        if isActive {
            return .success("New Text")
        } else {
            return .failure(URLError(.badServerResponse))
        }
    }
    
    //뷰모델에서 해당 코드를 호출
    
        @Published var text: String = "Start Text"
    
        func fetchTitle2() {
        let result = manager.getTitle2()
        switch result {
        case .success(let newTitle):
            self.text = newTitle
        case .failure(let error):
            self.text = error.localizedDescription
        }
    }

Result 타입을 활용해서 좀 더 깔끔해 졌지만 결국 케이스를 switch 해줘야 한다.

그냥 함수만 보고 에러발생 가능성을 한 눈에 알 수 있고, 에러 핸들링도 필요에 따라 가능하도록 throw를 써보면

    func getTitle3() throws -> String {
        if isActive {
            return "New Text"
        } else {
            throw URLError(.badServerResponse)
        }
    }

    //뷰모델에서 호출
    
        func fetchTitle3() {
        do {
            let newTitle = try manager.getTitle3()
            self.text = newTitle
        } catch let error {
            self.text = error.localizedDescription
        }
    }

메서드에 throw가 붙어있기 때문에, 호출했을 때 단번에 에러를 던질 수 있다는 것을 컴파일러를 통해서도 알 수 있고,

에러에 대한 상세한 정보 및 처리까지 가능하다.

 

do 블럭 안에서 throw 메서드를 호출할 때는 앞에 꼭 try를 붙여줘야한다.

말 그대로 에러를 던질 수도 있지만 트라이 해본다라는 의미.

do 블럭 안에서는 여러 try를 해 볼 수 있다는 사실!

    func getTitle3() throws -> String {
        if isActive {
            return "New Text"
        } else {
            throw URLError(.badServerResponse)
        }
    }
    
    func getTitle4() throws -> String {
        if isActive {
            return "Final Text"
        } else {
            throw URLError(.badServerResponse)
        }
    }

위와 같이 에러를 던지는 두 메서드를 do블럭에서 같이 호출해본다면

    func fetchTitle4() {
        do {
            //실패하면 아래 구문은 실행되지 않고 바로 catch 블럭으로 넘어감
            let newTitle = try manager.getTitle3()
            self.text = newTitle
            
            let finalTitle = try manager.getTitle4()
            self.text = finalTitle
            
        } catch let error {
            self.text = error.localizedDescription
        }
    }

위에서부터 차례대로 try 하다가 만약 에러를 던지면 거기서 바로 catch블럭으로 넘어간다.

여기서는 getTitle3와 getTitle4가 둘 다 에러 없이 string을 리턴할 것이기 때문에 
self.text = newTitle, self.text = finalTitle이 순차적으로 전부 실행될 것이고.

만약 getTitle3에서 에러를 던지면 let finalTitle = try manager.getTitle4()는 실행되지 않고
바로 catch 블럭으로 넘어간다는 얘기!

하지만 do 블럭 안에 여러 try 메서드를 호출하면 에러처리가 힘들지 않을까 싶다. 그냥 분리하는게 나은 거 같기도..

 

분기처리도 잘 되고, 에러 핸들링도 잘 되는데 문제는 do - catch 블럭을 사용하면 코드가 다소 길어지는 문제가 있다.

만약 에러에 대한 자세한 정보가 그렇게 중요하지 않고, 따로 에러처리를 상세하게 할 것이 아니면

간편하게 try?를 사용할 수 있다.

func getTitle4() throws -> String {
    if isActive {
        return "Final Text"
    } else {
        throw URLError(.badServerResponse)
    }
}



func fetchTitle5() {
    let newTitle = try? manager.getTitle4()
    if let newTitle = newTitle {
        self.text = newTitle
    }
}

getTitle4는 에러를 던지는 메서드다.

하지만 이 메서드를 호출하는 fetchtitle5에서는 do - catch 구문이 아닌 try?를 사용했다.

이렇게 되면 getTitle4의 반환형이 String이지만, try?를 사용했기 때문에 

fetchTitle5에서 newTitle은 옵셔널 스트링이 된다.

 

그래서 if let 바인딩을 해준 것이고, 만약 getTitle4에서 에러를 던지면,

newTitle에 Optional String이 아닌 nil이 할당되기 때문에 

if let 바인딩에서 바인딩이 되지 않아 함수를 탈출하게 되는 것이다.

혹은 nil coalescing으로 처리를 해 줄수도 있고.

 

즉 try?는 do-catch구문을 간편하게 대체하는 것이다.

try?를 사용하면 에러를 던질 수 있는 코드를 실행하고, 에러가 발생하면 nil을 반환한다.

오류 처리를 하지 않고, 오류를 무시할 수 있는 것이다.

혹은 옵셔널 값으로 처리되기 때문에 옵셔널 체이닝이 필요한 경우에 사용할 수도 있다.

 

다만 말 그대로 에러에 대한 추가 정보를 받아오지 않기 때문에 디버깅이 어려울 수 있다.

제대로 에러처리가 필요한 경우에는 do-catch구문을 사용하는 것이 좋다.