swift.org의 에러 핸들링 파트를 보고 클로드랑 같이 병행해서 개념을 익히고 정리해보았읍니다
1. Error Handling
에러 핸들링(Error Handling)은 프로그램 실행 중 발생할 수 있는 오류를 감지하고, 이를 적절히 처리하여 정상적인 흐름을 유지하는 과정
예를 들어, 네트워크 메서드를 처리해줌에 있어 발생할 수 있는 오류로 토큰 만료로 인한 인증 오류,, 요청 보낼 당시의 파라미터의 부재,,, 등등 많음. 요렇게 일어날 수 있는 에러들에 있어 어떻게 처리해주느냐! 가 에러 핸들링이라고 생각할 수 있음
에러를 미리 잡아두지 않고 그저 이 함수는 제대로 돌아갈거야! 라고 한다면 후에 런타임 에러가 날 수 있음. 이를 방지하기 위해 쓰는 것.
2. throwing 함수
함수, 메서드 혹은 선언자가 에러를 throw할 수 있음을 나타내기 위해선, 함수를 선언할 때 그것의 파라미터들 뒤에 throws 키워드를 붙여야함. throws로 마크된 함수는 throwing 함수라고 불리고, 함수가 리턴 타입을 특정지으면 화살표 전에 throws 키워드를 써야함.
func canThrowErrors() throws -> String
func cannotThrowError() -> String
3. 에러 타입 정의
enum ScoreError: Error {
case underZero
case overHundred
case typeError
case unknown(String)
}
이렇게 enum 타입으로 일어날 수 있는 에러들에 대해 경우의 수를 정리해둘 수 있음. 해당 포스트에선 간단한 예제를 쓰면서 공부했기 때문에 저런 ScoreError가 나오는데 실제 네트워크 레이어에서 나올 수 있는 에러로는 alreadyRegistered, tooManyRequestCode 등 각 서비스의 api에 관련된 에러가 있음.
앞에선 어떻게 에러를 미리 타입을 이용해 정의하고 함수에서 에러를 어떻게 말해줄지에 대한 설정을 알려줬는데, 미리 전체 코드를 보고 가겠음
import Foundation
enum ScoreError: Error {
case underZero
case overHundred
case typeError
case unknown(String)
}
struct Mom {
let name: String
}
struct Child {
let name: String
let parent: Mom
var score: Int
}
class ScoreSystem {
func inputScore(newScore: Int) throws -> Int {
guard newScore <= 100 else {
throw ScoreError.overHundred
}
guard newScore >= 0 else {
throw ScoreError.underZero
}
guard newScore is Int else { // 이 경우 무조건 true
throw ScoreError.typeError
}
noticeScoreToParent(score: newScore)
return newScore
}
func noticeScoreToParent(score: Int) {
print("자녀의 점수는 \(score)점입니다.")
}
}
let gildongMom = Mom(name: "길동맘")
let gildong = Child(name: "길동", parent: gildongMom, score: -11)
let scoreSystem = ScoreSystem()
func sendChildScore(child: Child, scoreInput: ScoreSystem) throws {
let score = child.score
let validatedScore = try scoreSystem.inputScore(newScore: score)
print("\(child.name)의 점수 \(validatedScore)점이 \(child.parent.name)에게 전달되었습니다.")
}
do {
try sendChildScore(child: gildong, scoreInput: scoreSystem)
} catch ScoreError.underZero {
print("오류: 0점 미만은 입력할 수 없습니다.")
} catch ScoreError.overHundred {
print("오류: 100점 초과는 입력할 수 없습니다.")
} catch {
print("알 수 없는 오류: \(error)")
}
저렇게 throw한 에러들을 처리하는 방법이 몇 가지 있음
4. do - catch 문
do {
try sendChildScore(child: gildong, scoreInput: scoreSystem)
} catch ScoreError.underZero {
print("오류: 0점 미만은 입력할 수 없습니다.")
} catch ScoreError.overHundred {
print("오류: 100점 초과는 입력할 수 없습니다.")
} catch {
print("알 수 없는 오류: \(error)")
}
do 이후에 나오는 블럭 안에 에러를 throw한다고 미리 정의해둔 함수를 쓴다면 try를 앞에 붙여 시도함을 알려야 함.
그 뒤에 그 try ~를 함으로서 나올 수 있는 에러들에 대해서 처리해줘야 하기에 그 에러들을 잡아줘야 함.
catch 라는 키워드를 씀으로서 에러를 잡고, 그 바로 뒤에 어느 에런지 특정지을 수 있어 해당 에러에 대한 처리를 정의해줄 수 있음
어찌보면 switch 문이랑 살짝 유사한 감이 없지않아 있긴함.
맨 막줄에 특정 에러 없이 catch만 하는 경우엔 앞에서 정의해둔 케이스 외 모든 에러를 반환받으면 해당 처리를 한다! 이런 코드임.
catch is ScoreError {
print("오류: 0점 미만은 입력할 수 없습니다.")
이런 코드도 보면 ScoreError 자체에 비교하기 때문에, 결국 해당 에러 타입이기만 하면 처리를 해줄 수도 있다는 점을 알 수 있음.
5. try?를 사용한 옵셔널 변환
// 2. try?를 사용한 옵셔널 변환
if let _ = try? sendChildScore(child: gildong, scoreInput: scoreSystem) {
print("점수 전송 성공")
} else {
print("점수 전송 실패")
}
해당 방식은 스로잉 함수 자체의 성공 여부에 좀 더 초점이 맞춰져있다고 보여짐. try뒤에 ?을 붙임으로서 뒤에 올 함수는 nil을 반환할 수 있음을 명시해주고 그 앞 if let을 통한 바인딩으로 반환값이 제대로 온다면 그 안에 블럭을 처리해주고 오지 않는다면 실패 시 블럭을 처리 할 수 있도록 한다.
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
이렇게 각 바인딩을 통해 우선순위에 따른 시도를 할 수 있다. 윗 줄에서 실패하게 되면 아랫 줄에서 시도하게 되고 성공 시 그 데이터를 반환하는 것. 그냥 옵셔널이라는 특성을 이용한다고 생각하면 될듯??
6. 에러 유형 지정(Type throws)
앞에선 그저 throws를 붙여 에러를 던질 수 있음을 보여주고 그걸 어느 타입의 어느 에러인지를 던지거나 해당 에러를 잡는 등의 흐름이었다면, 이번엔 어느 타입인지를 throws를 붙였을때 함께 선언해준다.
throws(에러타입) 이런식으로.
해당 함수는 내가 정의해둔 에러 타입에 정의된 에러들만 나니깐 이 안에서 다뤄!라는 뜻으로 이해하면 될 것 같음
func someFunction() throws(SpecificError) {
// SpecificError 유형의 에러만 던질 수 있음
}
요런식으로 ㅇㅇㅇ
do {
try sendChildScore(child: gildong, scoreInput: scoreSystem)
} catch .underZero{
print("오류: 0점 미만은 입력할 수 없습니다.")
} catch .overHundred {
print("오류: 100점 초과는 입력할 수 없습니다.")
} catch {
print("알 수 없는 오류: \(error)")
}
저렇게 미리 에러 타입을 말해준다면 바로 .case명으로 접근해줄 수 있음. throw해줄때나 catch할때나.
7. defer 함수
이건 에러 자체를 catch하는 등이랑 좀 거리가 있지만 연관은 되어있는 함수임.
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
}
while let line = try file.readline() {
// Work with the file.
}
// close(file) is called here, at the end of the scope.
}
}
함수의 스코프 안에 있는 내용이 전부 끝나면 defer의 코드블럭에 있는 함수는 무조건 실행해야 한다는 내용임. 즉 저 코드 상에선 file을 읽어서 작업을 한 후 맨 마지막에 file을 close한다는 내용.
throw error해서 catch error후 처리를 해준다고 쳐도 그 함수를 끝내게 될때 꼭 이것만큼은 해야한다 싶은 것을 정의하는 부분이라고 보면 됨.
지금 하고 있는 사이드 프로젝트에서 에러를 좀 더 개발자답게 핸들링해야겠다 싶어 이전엔 상태 코드에 switch 문을 써서 해결하던걸 이젠 미리 네트워크 에러 타입을 정의하고 그걸 받아와 throw, catch를 이용해 처리하기 시작했음. 어느정도 감은 왔지만 제대로 알아본 부분이 아니고 내가 쓸 수 있을 정도만 알아봤던 부분이라, 좀 더 알 필요가 있었는데 이렇게 정리하면서 그 외의 문법도 알게 되고 좋은 스터디였다~!
다만 개념적으로만 이해했고 이를 체계화해서 가독성 있게 만들 실력은 아직 되지 않아 다양한 네트워크 관련 에러를 throw해보면서 유연성 있는 핸들링을 할 수 있도록 해야겠다 !!
'iOS > Swift' 카테고리의 다른 글
[TIL / 25.03.13] 의존성 주입 (Dependency Injection) (0) | 2025.03.13 |
---|---|
[TIL / 25.03.12] Alamofire Interceptor에 관한 문제 해결 (0) | 2025.03.12 |
[TIL / 25.03.05] 이미지 슬라이더 + 커스텀 인디케이터 (2) | 2025.03.05 |
[TIL / 25.02.25] 자료구조, 메모리 구조, ARC에 대해 간략하게! (0) | 2025.02.25 |
[TIL / 25.02.21] SkeletonView 적용 및 생명주기 관련 문제 해결 (0) | 2025.02.21 |