Swift TIL ( 예외처리 , ARC , 프로토콜 , Extension)

예외 처리

오류가 발생할 수 있는 코드 블록을 구분하고 오류에 대한 처리를 하는 방법

예외 처리는 ‘throw’ , ‘do-catch’ 그리고 ‘Error’ 프로토콜을 사용하여 이루어 진다.

 

1.에러처리

에러처리는 어떤 코드 블록에서 오류가 발생할 수 있는 상황을 정의하고, 이 오류를 처리하는 것을 의미한다. swift 에서는 ‘Error’ 프로토콜

을 준수하는 타입을 사용하여 오류를 표현한다.

 

2. Error 프로토콜

Error 프로토콜은 swift 에서 오류를 나타내기 위한 프로토콜이다. 사용자 정의 오류 타입을 만들때 이 프로토콜을 준수하도록 해야한다.

enum VendingMachineError: Error {
		case invalidSelection
		case insufficientFunds(coinsNeeded: Int)
		case outOfStock
}

 

3. throw

함수나 메서드 내에서 오류를 던지는 역할을 한다. 오류를 던지면 현재 실행중인 코드 블록을 중지하고 해당 오류를 호출한 곳으로 전달한다.

enum CustomError: Error {
    case outOfBounds
    case invalidInput(String)
}

func processValue(_ value: Int) throws -> Int {
    if value < 0 {
        throw CustomError.invalidInput("음수는 불가합니다.")
    } else if value > 100 {
        throw CustomError.outOfBounds
    }
    
    return value * 2
}

 

4. do-catch

do-catch 블록은 오류가 발생할 수 있는 코드를 감싸는데 사용된다. ‘do’ 블록 안에서 코드를 실행하고, 만약 오류가 발생하면 ‘catch’ 블록에서 해당 오류를 처리

func processValue(_ value: Int) throws -> Int {
    if value < 0 {
        throw CustomError.invalidInput("음수는 불가합니다.")
    } else if value > 100 {
        throw CustomError.outOfBounds
    }
    
    return value * 2
}

do {
    let result = try processValue(50)
    print("\\(result) 입니다")
} catch CustomError.outOfBounds {
    print("값 출력!")
} catch CustomError.invalidInput(let errorMessage) {
    print("\\(errorMessage)")
} catch {
    print("error occurred : \\(error)")
}

 

5. try

try 키워드는 오류가 발생할 수 있는 함수나 메서드를 호출할 때 사용된다. ‘try’ 키워드가 붙은 코드는 ‘do-catch’ 블록 내에서 사용되어야 한다.

do {
    let result = try someThrowingFunction(value: 5)
    print(result)
} catch {
    print("Error occurred: \\(error)")
}

do {
    let result = try someThrowingFunction(value: -2) // 에러 발생
    print(result)
} catch {
    print("Error occurred: \\(error)") // 음수 값을 처리하는 에러
}

// try?를 사용하여 에러 처리하기
let result1 = try? someThrowingFunction(value: 5) // 유효한 값 호출
print(result1) // Optional("The value is 5")

let result2 = try? someThrowingFunction(value: -2) // 에러 발생
print(result2) // nil

// try!를 사용하여 에러 처리하기
let result3 = try! someThrowingFunction(value: 5) // 유효한 값 호출
print(result3) // The value is 5

let result4 = try! someThrowingFunction(value: -2) // 에러 발생
print(result4)

ARC

ARC (Automatic Reference Counting) 은 Swift에서 메모리 관리 기법 중 하나이다. 객체가 인스턴스가 참조되는 횟수를 추적하여 메모리에서 해제할 시점을 결정합니다. 객체가 생성될 때마다 참조 횟수가 1 증가하고, 객체를 참조하는 다른 객체나 변수가 없어지거나 더이상 사용되지 않을 때 참조 횟수가 1감소 합니다.

참조 횟수가 0이 되면 해당 객체는 메모리에서 해제 됩니다.

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\\(name)이(가) 생성되었습니다.")
    }
    deinit {
        print("\\(name)이(가) 메모리에서 해제되었습니다.")
    }
}

func createPerson() {
    let person1 = Person(name: "Alice")
    let person2 = person1
    let person3 = Person(name: "Bob")
    
    print("createPerson() 함수 종료")
}

createPerson()

‘ Person ‘ 클래스를 정의하고, createPerson() 함수를 호출하여 객체를 생성하는 예시입니다. 여기서 각 객체의 생성과 해제가 어떻게 이루어지는지 살펴봅시다.

 

1. Person 객체 생성:

person1 , person3 는 각각 Person 클래스의 인스턴스입니다.

person1 은 Alice 를 , person3 은 Bob을 갖고 생성 됩니다.

 

2. 강한 참조:

person2 은 person1 을 참조하고 있습니다. 이때, person1 의 참조 계수는 2가 됩니다.

(Alice 를 참조하는 것은 person1 person2 두 곳입니다.)

 

3. createPerson() 함수 종료:

createPerson() 함수의 실행이 종료되면서 함수 내에서 생성된 객체들의 참조가 모두 제거됩니다.

person1 person2 person3는 함수 내에서 생성되었지만 함수가 종료됨과 동시에 함수 외부에서 사용되지 않으므로 제거됩니다.

활성화된 참조변수가 존재하지 않는다면 ARC는 특정 객체를 메모리로부터 "해제”

강한 참조 순환 문제

강한 참조 순환이란 객체 간 서로가 서로를 강한 참조하는 상황을 의미한다.

이는 메모리 누수로 이어질 수 있다

class Person {
    var name: String
    var pet: Pet?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\\(name)은 해제 되었습니다.")
    }
    
}

class Pet {
    var name: String
    var owner: Person?
    
    init(name: String, owner: Person? = nil) {
        self.name = name
    }
    
    deinit {
        print("\\(name)은 해제 되었습니다.")
    }
}

func createPersonAndPet() {
    var person: Person? = Person(name: "Alice")
    var pet: Pet? = Pet(name: "Fluffy")
    
    person?.pet = pet
    pet?.owner = person
    
    person = nil
    pet = nil
    
}

createPersonAndPet()

이 예시에서 Person Pet 클래스가 서로 강한 참조를 하고 있다. createPersonAndPet 함수에서 person , pet 을 생성한 후 서로를 참조 하고 있다. 그런다음 person , pet에 nil을 할당하여 참조를 해제한다. 그러나 두 객체는 여전히 서로를 강한 참조하고 있는 상태이기 때문에 메모리에서 해제 되지 않는다.

(deinit 이 실행되지 않는 것을 바탕으로 확인)

이러한 문제를 해결하기 위해 약한 참조나 미소유 참조 등을 사용한다.

 

약한 참조 (Weak Reference) :

약한 참조는 참조하는 객체를 강한 참조하지 않고, 객체가 메모리에서 해제될 때 자동으로 nil 로 설정된다.

보통 옵셔널 타입으로 선언되며. weak 키워드를 사용하여 선언

주로 순환 참조를 방지하기 위해 사용된다.

class Person {
    var name: String
    weak var pet: Pet?
    
    init(name: String, pet: Pet? = nil) {
        self.name = name
        self.pet = pet
    }
}

미소유 참조 (Unowned Reference) :

미소유 참조는 참조하는 객체를 강한 참조하지 않지만, 참조하는 객체가 해제되더라도 자동으로 nil 로 설정되지 않습니다.

따라서 참조하는 개체가 해제되었을 때, 미소유 참조를 사용하는 객체를 접근하면 런타임 에러가 발생할 수 있다.

주로 참조하는 객체의 생명주기가 항상 참조하는 객체의 생명주기보다 길거나 같은 경우에 사용된다.

class Pet {
    var name: String
    unowned var owner: Person // 미소유 참조
    
    init(name: String, owner: Person) {
        self.name = name
        self.owner = owner
    }
}

위 예시에서 Person 클래스는 Pet 객체를 약한 참조로 참조하고 있으며, Pet 클래스는 owner 속성을 미소유 참조로 참조하고 있습니다.

이러한 방식으로 순환 참조를 방지하면서 메모리 누수를 방지할 수 있다.

단, 미소유 참조는 참조하는 객체가 해제되었을 때 런타임 에러가 발생할 수 있다.

프로토콜

프로토콜(Protocol)은 특정 작업 또는 기능에 대한 인터페이스를 정의하는 추상적인 개념입니다.

클래스, 구조체, 열거형 등 프로토콜을 채택하여 해당 프로토콜에서 요구하는 작업이나 기능을 제공할 수 있습니다.

protocol Transportaion {
    var brand: String { get }
    var maxSpeed: Double { get }
    
    func start()
    func accelerate()
    func stop()
}

class Car: Transportaion {
    
    var brand: String
    var maxSpeed: Double
    
    init(brand: String, maxSpeed: Double) {
        self.brand = brand
        self.maxSpeed = maxSpeed
    }
    
    func start() {
        print("Car Start")
    }
    
    func accelerate() {
        print("Car accelerate")
    }
    
    func stop() {
        print("Car stop")
    }
}

class Airplane: Transportaion {
    var brand: String
    var maxSpeed: Double
    
    init(brand: String, maxSpeed: Double) {
        self.brand = brand
        self.maxSpeed = maxSpeed
    }
    
    func start() {
        print("start")
    }
    
    func accelerate() {
        print("accel")
    }
    
    func stop() {
        print("stop")
    }
}

예시와 같이 이렇게 프로토콜을 채택하여 구성하면 ‘Car’ , ‘Airplane’ 객체는 각각 자동차와 비행기 특성을 가지게 됩니다.

이러한 유연성 덕분에 다형성을 활용하여 동일한 방식으로 여러 종류의 운송 수단을 다룰 수 있습니다.

Extension (확장)

Extension은 기존 클래스, 구조체, 열거형 또는 프로토콜에 새로운 기능을 추가하는 기능입니다.

이를 통해 기존의 코드를 수정하지 않고도 새로운 기능을 추가하거나, 기존 기능을 확장할 수 있습니다.

extension String {
    func reverse() -> String {
        return String(self.reversed())
    }
}

let str = "Hello"
print(str.reverse()) // olleH

이 예시는 기존의 String 타입에 reverse() 메서드를 추가하고 있습니다.

이렇게하면 String 타입을 사용하는 모든 곳에서 reverse() 메서드를 호출 할 수 있습니다.