안녕하세요 오늘은 에러 처리에 관하여 정리하고자 합니다
개발자라면 무조건 에러는..
마주치게 되는데
만약 우리가 예상치 못한 상황에서 에러를 마주치게된다면??!!
어떤 에러인지도 파악해야하고 그 에러를 처리하기 위해 다뤄야하는것도 다를것이며
이런저런 처리를 하기위해서 고민을 하기 시작할 겁니다
저 역시 집을 빨리 가기 위해서 머리를 굴리고 있을 테니까요^^
예를들어서
디스크의 파일에서 데이터를 읽고 처리하는 작업을 진행할 때
지정된 위치에 파일이 존재하지 않거나
파일에 읽기 권한이 없거나
적절한 형식으로 인코딩 되어있지 않은 것 등등
실패할 수 있는 요인이 많습니다
이걸 방지하기 위해 프로그램에서 일부 실패할 수 있는
요건을 해결하고 해결할 수 없는 에러는 전달하게끔
작성할 수 있답니다
코드를 보면서 에러를 다루는 방법에 관하여 알아볼까요??
우선 에러 처리의 과정은 3단계로 나눌 수 있습니다
우선 어떤 오류가 발생하는지 미리 아는 경우
enum으로 정리할 수 있습니다
Error키워드는 애플에서 제공하고 있는 키워드 입니다
그리고 프로토콜 타입인걸 알 수 있습니다
프로토콜은 앞에서 배웠듯이
타 언어에서 인터페이스와 비슷하다고 알려져 있답니다
세부 구현은 채택된 곳에서 하면 되는 것도 알고 있으시죠??
이렇게 에러를 정의하고
throwing함수를 통하여 세부 내용에 대해서 작성해줍니다
//throwing
func checkingHeight(height: Int) throws -> Bool { //에러를 던질 수 있는 함수 타입입니다
if height > 190 {
throw HeightError.maxHeight
} else if height < 130 {
throw HeightError.minHeight
} else {
//삼항연산자로도 표현할 수 있음
return height >= 160 ? true : false
// if height >= 160 {
// return true
// } else {
// return false
// }
}
}
이렇게 에러를 던질 수 있는 함수를 정의해주고
실행을 하게되면
이렇게 try 키워드를 통해서 실행했을 때
가능한 경우 만약 에러를 발생하게 된다면
catch 구문으로 들어가서 에러를 잡게 됩니다
에러의 표현방법
|
공식문서에 나와있는 에러 표현과 던지고 처리하는 방법에 대해서 좀 더 알아보도록 합시다
에러조건을 enum으로 선언하고 Error 프로토콜을 반드시 채택해준 후
케이스를 나열하면 됩니다
에러 처리
(Handling Errors)
|
에러가 발생할 때 주변 코드의 부분이 에러 처리를 담당해야합니다
문제를 수정하거나 다른 방법을 시도하거나 사용자에게
에러를 알리는 방법으로 에러를 처리해야 합니다
Swift에서는 에러를 처리하는 방법이 4가지
가 있답니다
함수에서 해당 함수를 호출하는 코드로 에러를 전파
do.catch 구문을 사용
옵셔널 값으로 처리
에러가 발생하지 않을것이라고 주장(응??!!)
(근거있는 억지면 괜찮은데 근거 없다면..? 파국..)
던지기 함수를 이용한 에러전파
|
propagating Error Using Throwing Functions
에러가 발생할 수 있는 함수 / 메서드 / 초기화구문을 나타내기 위해 함수의 선언 중 파라미터 뒤에
throws 키워드를 사용합니다
throws로 표시된 함수 == 던지기 함수
다른 예제를 통해 알아볼까용?
struct Item {
var price: Int
var count: Int
}
class VendingMachine {
var inventory = [
"Candy Bar": Item(price: 12, count: 7),
"Chips": Item(price: 10, count: 4),
"Pretzels": Item(price: 7, count: 11)
]
var coinsDeposited = 0
//⭐️
// itemNamed 이름의 아이템을 판매하는 함수
func vend(itemNamed name: String) throws {
// 유효한 아이템인지 확인
guard let item = inventory[name] else {
throw VendingMachineError.invalidSelection
}
// 재고가 있는지 확인
guard item.count > 0 else {
throw VendingMachineError.outOfStock
}
// 투입된 동전이 아이템의 가격 이상인지 확인
guard item.price <= coinsDeposited else {
throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
}
//이러한 오류를 벗어나게 되면
//코드를 실행할 수 있게된다
// 아이템 가격만큼 투입된 동전을 감소
coinsDeposited -= item.price
// 아이템의 재고를 하나 감소시키고 업데이트
var newItem = item
newItem.count -= 1
inventory[name] = newItem
// 아이템을 제공함을 출력
print("Dispensing \(name)")
}
}
let favoriteSnacks = [
"Alice": "Chips",
"Bob": "Licorice",
"Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
try vendingMachine.vend(itemNamed: snackName)
}
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
print("Success! Yum.")
} catch VendingMachineError.invalidSelection {
print("Invalid Selection.")
} catch VendingMachineError.outOfStock {
print("Out of Stock.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
} catch {
print("Unexpected error: \(error).")
}
// Prints "Insufficient funds. Please insert an additional 2 coins."
// 잔액 확인
print("Remaining coins: \(vendingMachine.coinsDeposited)")
//Remaining coins: 8
자판기 객체를 생성하고 동전을 투입한 후 아이템을 구입하는걸 볼 수 있으며
발생할 수 있는 오류를 처리하고 남은 동전의 수를 출력하는 코드를 볼 수 있습니다
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
print("Remaining coins: \(vendingMachine.coinsDeposited)")
func nourish(with item: String) throws {
do {
try vendingMachine.vend(itemNamed: item)
} catch is VendingMachineError { //catch is는 VendingMachineError의 오류만 처리
print("Couldn't buy that from the vending machine.")
//자판기 오류에 대한
/*"Invalid Selection."은 출력되지 않는다
catch {
print("Couldn't buy that from the vending machine.")
throw error // 오류를 다시 던짐
}
이렇게 되어있다면 오류를 다시 던지는 현상이 발생하게 됩니다
"Couldn't buy that from the vending machine."
Unexpected non-vending-machine-related error: invalidSelection
이렇게 오류가 문구가 나오게 됩니다
*/
}
}
do {
try nourish(with: "Beet-Flavored Chips")
} catch {
print("Unexpected non-vending-machine-related error: \(error)")
}
// Prints "Couldn't buy that from the vending machine."
위에서 알아야할 점은
catch is와 catch입니다
이 두개는 각각
결과가 다르게 나오게 됩니다
func eat(item: String) throws {
do {
try vendingMachine.vend(itemNamed: item)
} catch VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock {
//⭐️catch insufficientFunds 매개변수없이 오류명만 작성이 가능함
print("Invalid selection, out of stock, or not enough money.")
}
}
연관된 에러를 포착하기 위해서
콤마로 구분하여
catch 다음에 리스트 형식으로 작성할 수 있습니다
이 리스트 중에 한가지 에러가 발생하게 된다면
catch절은 메세지를 출력하여 처리하게됩니다
에러를 옵셔널 값으로?
|
import Foundation
enum FileError: Error {
case fileNotFound
case unreadable
}
func readFile(named fileName: String) throws -> String {
// 실제 파일 읽기 로직 (여기서는 예제를 위해 단순화)
guard fileName == "validFile.txt" else {
throw FileError.fileNotFound
}
return "File contents"
}
// 에러를 옵셔널 값으로 변환하는 예시
let fileContents1 = try? readFile(named: "validFile.txt") // 성공 시 파일 내용, 실패 시 nil
let fileContents2 = try? readFile(named: "invalidFile.txt") // 실패 시 nil
print(fileContents1) // Optional("File contents")
print(fileContents2) // nil
// do-catch 블록을 사용한 옵셔널 값 처리 예시
let fileContents3: String?
do {
fileContents3 = try readFile(named: "validFile.txt")
} catch {
fileContents3 = nil
}
print(fileContents3) // Optional("File contents")
이렇게 try?로 변환하게되면 오류를 옵셔널 값으로 처리할 수 있습니다
성공하면 파일 내용이
실패하면 nil이 반환됩니다
에러 전파 비활성화
|
Disabling Error propagation
import Foundation
enum FileError: Error {
case fileNotFound
case unreadable
}
func readFile(named fileName: String) throws -> String {
// 실제 파일 읽기 로직 (여기서는 예제를 위해 단순화)
guard fileName == "validFile.txt" else {
throw FileError.fileNotFound
}
return "File contents"
}
// 에러 전파 비활성화: 에러 발생 시 nil 반환
let fileContents = try? readFile(named: "invalidFile.txt")
if let contents = fileContents {
print("File contents: \(contents)")
} else {
print("Failed to read the file.")
}
//"Failed to read the file."
readFile(named:) 파일이름이 존재하지 않을 때 오류를 던지게 됩니다
이 예시는 에러 전파를 비활성화하고
오류 발생 시 nil을 반환하여 안전하게 처리하는 방법을 보여주고 있습니다
마지막으로 defer 구문을 아시나요?
저는 ARC를 배우면서 한 번 서칭해서 머리로 정리했던 기억이 있답니다
정리 작업 지정
|
Specifying Cleanup Actions
func processFile() {
// 파일을 열기
let file = openFile()
// 파일을 처리하는 코드
// 파일을 닫기 (정리 작업)
defer {
closeFile(file)
}
// 파일을 처리하는 코드
// 정리 작업 이후에 추가적인 코드들
}
func openFile() -> File {
// 파일을 열기
return File()
}
func closeFile(_ file: File) {
// 파일을 닫기
}
defer를 사용하는 이유는
코드의 가독성을 높이고
정리 작업을 보장하기 위함이라고 생각하면 됩니다
해당 범위를 빠져나갈때 반드시 실행된다고 생각하면 됩니다
그래서 함수 중간에 종료되거나 예외가 발생하더라도
'반드시!'
defer가 선언되었다면 defer 구문에 명시된 정리 작업은 실행이 됩니다
그리고 특히 주로 파일이나 네트워크 연결과 같은 리소스를 해제하는 등의 작업을 defer를 통해
효율적으로 수행을 할 수 있습니다
메모리 누수와 같은 문제를 방지하고 리소스 관리를 개선하는데 도움이 될 수 있습니다
마지막으로 defer 구문에서 여러개의 작업이 있을때
이들이 나중에 선언된 것부터 역순으로 실행됩니다
실행을하면
마지막 defer부터 역순하여 실행되어서
출력되는걸 확인할 수 있습니다
Swift(4.2이하 버전)에서는 do, try, catch, throws 문법을 제공했습니다
하지만 이러한 경우는 예외 상황에서 대처하기 어려운 단점이 있습니다
(3단계과정이 필요하다보니 function에서 error를 던지기만하고 어떤과정으로 이어지는지
알 수가 잘 없습니다 throws 키워드는 에러를 던진다는 뜻이지만 어떤에러를 던지고 있는지도
특정하기에 어려움이 있습니다)
그래서 Swift5에서는 이런 점을 보완해서 에러를 보다 유연하게처리할 수 있는
문법을 지원하고 있습니다
또 다른 방법으로
오류를 잡을 수 있습니다
Result Type
|
Result 타입은 성공과 실패를 다 만들고 난 후
리턴값을 만들어 처리하면 됩니다
//리턴 값을 받고
let result = resultTypeCheckingHeight(height: 200)
switch result {
case .success(let success):
print("결과값은 \(success) 입니다")
case .failure(let failure):
print(failure)
}
//maxHeight
Result Type에는 여러메서드가 존재하고 있습니다
왜 Result Type를 사용하는 이유는
성공과 실패를 깔끔하게 처리가 가능하기 때문입니다
그리고 .get은 성공했을때 던지는 값입니다
기존의 에러처리 패턴을 완전히 대체하려는 목적이 아니라
개발자들에게 에러 처리에 대한 다양한 처리 방법에 대한 옵션을 제공한다고 합니다
그리고 다음으로 네트워킹 코드에서 Result Type이 있습니다
이렇게 에러 케이스를 만들어 놓고
// performRequest 함수는 URL을 입력으로 받아 비동기 네트워크 요청을 수행하고
// 완료 핸들러를 통해 데이터를 전달합니다.
func performRequest(with url: String, completion: @escaping (Data?, NetworkError?) -> Void) {
// URL 문자열이 유효한지 검사합니다. 유효하지 않으면 함수 종료.
guard let url = URL(string: url) else { return }
// URLSession을 사용하여 비동기 네트워크 요청을 수행합니다.
URLSession.shared.dataTask(with: url) { (data, response, error) in
// 에러가 발생하면 에러 메시지를 출력하고, completion 핸들러에 에러를 전달합니다.
if error != nil {
print(error!) // 에러가 발생했음을 출력
completion(nil, .someError) // 에러가 발생했으니, nil 전달
return
}
// 안전하게 옵셔널 바인딩을 하지 못했으면, completion 핸들러에 에러를 전달합니다.
guard let safeData = data else {
completion(nil, .someError) // 안전하게 옵셔널 바인딩을 하지 못했으니, 데이터는 nil 전달
return
}
// 데이터가 유효하면 completion 핸들러에 데이터를 전달합니다.
completion(safeData, nil)
}.resume() // 데이터 태스크를 시작합니다.
}
completion 핸들러는 (Data?, NetworkError?) 형식의 튜플을 사용하여
데이터와 에러를 전달하고 있습니다
(nil, .somError) 에러 발생시 nil로 설정하고 NetworkError를 전달하고 있습니다
(safeData, nil) 데이터가 유효하면 데이터를 전달하고 에러를 nil로 설정하는걸 알 수 있습니다
함수를 작성한 후 실행해 볼까용?
performRequest(with: "주소") { data, error in
// 데이터를 받아서 처리
if error != nil {
print(error!)
}
// 데이터 처리 관련 코드
}
이렇게 실행할 수 있겠지용??
그리고 다음으로 Result Type으로 사용할 수 있습니다
func performRequest2(with urlString: String, completion: @escaping (Result<Data,NetworkError>) -> Void) {
//urlString이 유효한 URL로 변환 가능한지 확인합니다. 그렇지 않으면 함수가 바로 종료
guard let url = URL(string: urlString) else { return }
//URLSession을 사용하여 비동기 네트워크 요청을 수행
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!) // 에러가 발생했음을 출력
completion(.failure(.someError)) // 실패 케이스 전달
return
}
guard let safeData = data else {
//데이터가 유효하지 않으면, completion 핸들러를 통해 .failure 케이스를 전달합니다
completion(.failure(.someError)) // 실패 케이스 전달
return
}
completion(.success(safeData)) // 성공 케이스 전달
//데이터 태스크를 시작
}.resume()
}
performRequest2(with: "주소") { result in
switch result {
case .failure(let error):
print(error)
case .success(let data):
// 데이터 처리 관련 코드
break
}
}
❤️혹시나 잘못된 부분이 있다면 댓글로 알려주면 감사하겠습니다❤️
✏️참고
앨런스위프트 문법 자료(강의)⭐️⭐️⭐️ -추천
애플 공식 문서
'Swift' 카테고리의 다른 글
Swift | 날짜와 시간에 관하여 - 1 (0) | 2024.06.01 |
---|---|
Swift | weak / unowned에 관하여 (0) | 2024.05.23 |
Swift | ARC에 관하여[WWDC21] (1) | 2024.05.22 |
Swift | 클로저(Closure)에 관하여 -2 (0) | 2024.05.21 |
Swift | 클로저(Closure)에 관하여 -1 (0) | 2024.05.21 |