타입 Type

타입이란 직역하면 어떤 부류의 형, 유형, 모양, 생김새 입니다.

프로그래밍에서 타입은 값이 가질 수 있는 형태와 그 값에 대해 수행할 수 있는 연산을 정의하는 개념입니다.

더 나누면 정적타입, 동적타입이 존재하지만 이 글에서는 다루지 않습니다

 

이 글에서는 Swift에서 존재하는 타입들을 살펴볼 것 입니다.

더 자세한 내용은 Swift.org에 있습니다

 

 

Swift의 타입

Swift의 타입은 두가지 종류가 있습니다.

  • 명명된 타입 (named type): 특정 이름을 부여할 수 있는 타입으로 클래스, 구조체, 열거형, 프로토콜, 배열, 딕셔너리, 옵셔널 모두 해당됩니다. 필요에따라 확장할 수 있습니다.
  • 복합 타입 (compound type): 정의된 이름이 없는 타입으로 함수와 튜플이 있습니다. 복합 타입은 말 그대로 다른 명명된 타입과 다른 복합 타입을 합칠 수 있습니다.

 

Swift에서 정의된 타입은 아래와 같이 있습니다.

Grammar of a type

type  function-type 	(함수)
type  array-type		(배열)
type  dictionary-type	 	(딕셔너리)	
type  type-identifier		(타입 식별자)
type  tuple-type		(튜플)
type  optional-type	(옵셔널 타입)
type  implicitly-unwrapped-optional-type	(묵시적 언래핑 옵셔널)
type  protocol-composition-type	(프로토콜 조합 타입)
type → opaque-type (불투명 타입)	(some 키워드를 붙이는 것)
type → boxed-protocol-type	(existential type) (프로토콜 타입으로 선언한 것)
type → metatype-type (타입의 타입) (.self .Type)
type → any-type  
type → self-type
type → ( type )

 

 

타입 식별자 (Type Identifier)

타입 식별자는 타입 별칭(type alias)를 나타냅니다.

대부분은 식별자와 명명된 타입이 같아서 직접 참조합니다 (Int타입은 Int에 참조, Dictionary<String,Int>타입은 Dictionary<string,int></string,int>에 참조)

 

명명된 타입과 식별자가 같지 않은 두가지 경우가 있습니다.

첫번째로는 복합타입의 타입별칭입니다.

typealias Point = (Int, Int)
let origin: Point = (0, 0)

 

두번째로는 다른 모듈에 선언되거나 다른 중첩된 명명 타입입니다.

var someValue: ExampleModule.MyType

 

 

 

튜플 타입 (Tuple Type)

튜플 타입은 소괄호로 묶인 콤마로 구분되는 타입의 리스트입니다.

타입의 요소에 이름을 정할 수 있고, 이름과 타입을 구별하기 위해 세미콜론(:)을 사용합니다.

var someTuple = (top: 10, bottom: 12)
someTuple = (top: 4, bottom: 42)
someTuple = (9, 99)

 

튜플 타입에서 중요한건 요소들의 타입입니다.

someTuple의 타입은 (Int, Int) 이기 때문에 (Int, Int, Int)나 (Int, String)같은 타입은 someTuple에 할당할 수 없습니다.

 

 

 

함수 타입 (Function Type)

함수 타입은 함수, 메서드, 클로저의 타입을 나타냅니다.

함수 타입은 화살표(->)로 구분된 파라미터 타입과 리턴 타입으로 구성됩니다.

(<#parameter type#>) -> <#return type#>

 

문서에 따르면 함수타입 () -> T 의 파라미터는 autoclosure 속성을 적용하여 호출 부분에서 암시적으로 클로저를 생성할 수 있습니다. 라고 설명되어 있습니다.

 

autoclosure이란 함수에 인수로 전달되는 표현식을 래핑하기 위해 자동으로 생성되는 클로저입니다.

간단하게 파라미터에 클로저 () -> T 타입이 있다면, 해당 함수를 호출할 때, 대괄호( { )를 사용하지 않고 표현식만 사용해도 자동으로 클로저로 변환됩니다.

func someFun1(closure: () -> String) {
    print(closure())
}
someFun1 { "hello" }

func someFun2(closure: @autoclosure () -> String) {
    print(closure())
}
someFun2(closure: "hello")

 

함수도 타입으로 정의되면 autoclosure처럼 대괄호 { } 없이 호출 할 수 있습니다.

func someFun2(closure: @autoclosure () -> String) {
    print(closure())
}

let functionSomeFun2 = someFun2
functionSomeFun2("hello")

 

또한 가변파라미터와 inout파라미터를 갖는 함수도 타입으로 구현할 수 있습니다.

func swap(_ num1: inout Int,_ num2: inout Int) {
    let temp: Int = num1
    num1 = num2
    num2 = temp
}
let swap1 = swap
var numA = 10
var numB = 15
swap1(&numA, &numB)
print(numA, numB)
// 출력
// 15 10
func printRange(range: Int...) {
    for i in range {
        print(i)
    }
}
let printRange1 = printRange
printRange1(1, 2, 3)
// 출력
// 1
// 2
// 3

 

함수 타입은 위에 서술했듯이 파라미터 타입과 리턴 타입이 같으면 다른 같은 타입의 함수로도 변경할 수 있습니다.

someFunction을 변수 f에 할당하게 되면 f의 타입은 위 사진과 같이 (Int, Int) -> ()으로 설정됩니다.

f의 타입처럼 파라미터 타입은 (Int, Int), 리턴 타입은 Void인 함수라면 f에 다른 함수로 초기화 할 수 있습니다.

func someFunction(left: Int, right: Int) {}
func anotherFunction(left: Int, right: Int) {}
func functionWithDifferentLabels(top: Int, bottom: Int) {}

var f = someFunction
f = anotherFunction
f = functionWithDifferentLabels

위 세가지 함수는 인자의 이름이 달라도 파라미터 타입과 리턴 타입이 같기 때문에 f로 선언된 변수가 세 가지 함수로 타입을 지정 및 변경하여도 에러가 발생하지 않습니다. (모두 같은 타입이기 때문에)

 

다른 함수의 종류로 에러를 반환할 수 있는 throw 함수, async 함수 역시 타입으로 지정할 수 있습니다.

위 사진처럼 자동 완성으로 볼 때 해당 변수의 타입들은 throws, async 함수타입으로 지정된 것을 볼 수 있습니다.

 

 

배열 타입 Array Type

이미 많이 사용해봤던 배열 타입입니다.

[T], 혹은 Array<T> 형식으로 선언합니다.

let someArray1: Array<String> = ["Alex", "Brian", "Dave"]
let someArray2: [String] = ["Alex", "Brian", "Dave"]
let someArray3: Array<Array<String>> = [["Alex", "Brian"], ["Dave"]]
let someArray4: [[String]] = [["Alex", "Brian"], ["Dave"]]

 

 

딕셔너리 타입 Dictionary Type

Dictionary를 선언하는 방식은 아래와 같습니다!

let someDictionary1: [String: Int] = ["Alex": 31, "Paul": 39]
let someDictionary2: Dictionary<String, Int> = ["Alex": 31, "Paul": 39]

 

 

옵셔널 타입 Optional Type

Swift에서는 Optional<Wrapped>에 대해 접미사 ? 구문을 정의합니다.

옵셔널 타입을 다음과 같이 설정할 수 있습니다.

// enum Optional<Wrapped>
var optionalInteger1: Int?
var optionalInteger2: Optional<Int>

 

 

 

암시적 언래핑 옵셔널 타입 Implicitly Unwrapped Optional Type

암시적 언래핑 옵셔널 타입은 스토리보드에 있는 UI를 코드영역으로 드래그하면 자동으로 생성되는 모습을 볼 수 있었습니다.

@IBOutlet var button: UIButton!

이는 해당 인스턴스가 옵셔널 타입이지만, 강제 언래핑 동작을 자동으로 추가하기 위해 타입 뒤에 접미사 ! 를 추가하여 선언합니다.

button이 nil이라면 런타임 에러를 발생하고, nil이 아닌 값이 존재한다면 따로 옵셔널 바인딩 없이 접근할 수 있습니다.

 

 

 

프로토콜 혼합 타입 Protocol Composition Type

프로토콜 혼합 타입은 말 그대로 다중 프로토콜이 혼합된 하나의 타입으로 나타내게 되고 타입은 아래와 같이 나타냅니다.

<#Protocol 1#> & <#Protocol 2#>
protocol Nameable {
    var name: String { get }
}

protocol Ageable {
    var age: Int { get }
}

typealias Person = Nameable & Ageable

class Jenikeju: Person {
    var name: String
    
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

혼합된 프로토콜은 protocol1 & protocol2 로 나타낼 수도 있지만, 이를 typealias를 통해 새로운 타입으로 별칭하고 사용할 수 있습니다.

 

혼합된 프로토콜이여도 하위클래스에 상위클래스를 상속하고 다른 프로토콜을 사용할 때, 상위클래스와 프로토콜을 혼합할 수 있습니다.

protocol Nameable {
    var name: String { get }
}

protocol Ageable {
    var age: Int { get }
}

class Animal {
    func feed() { }
}

// class & protocol1 & protocol2
typealias CatCompositionType = Animal & Nameable & Ageable

class Cat: CatCompositionType {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}
let cat = Cat(name: "meow", age: 5)
cat.feed()

클래스는 하나의 클래스만 상속받을 수 있기 때문에 최대 하나의 클래스만 혼합할 수 있습니다.

 

또한 혼합 과정에서 중복된 프로토콜은 자동으로 무시됩니다.

// 중복된 프로토콜 혼합
// Ageable & Nameable & Ageable & Ageable = Ageable & Nameable
typealias ManyCompositionType = Ageable & Nameable & Ageable & Ageable

class SomeClass: ManyCompositionType {
    var age: Int
    var name: String
    
}

 

 

 

불투명한 타입 Opaque Type

불투명한 타입은 기본 타입 지정없이 프로토콜 또는 프로토콜 구성을 준수하는 타입을 정의합니다.

some <#constraint#>

constraint는 클래스, 프로토콜, 프로토콜 구성 타입, Any 입니다.

 

불투명한 타입을 이해하기위해 some 키워드를 먼저 이해해봅시다.

 

some

불투명한 타입에서 불투명한이란 의미는 어떤 타입인지 정확히 명시하지 않지만, 컴파일러는 내부적으로 알고 있다는 의미로 생각할 수 있습니다.

Swift에서는 프로토콜을 직접 반환할 수 없습니다.

그래서 아래 코드는 컴파일 에러를 발생합니다.

protocol Shape {
    func area() -> Double
}

struct Circle: Shape {
    var radius: Double = 0.0
    func area() -> Double { return .pi * radius * radius }
}

// protocol을 리턴해서 에러 발생
func makeShape() -> Shape {
    return Circle()
}

★ 하지만 Swift5부터 값 타입도 프로토콜 타입을 리턴할 수 있게되어서 위 코드는 이제 오류가 발생하지 않습니다.

그전에 protocol 타입을 리턴할 수 없어서 some 키워드를 추가하여 해당 프로토콜을 준수하는 객체를 리턴하도록 구현할 수 있습니다.

protocol Shape {
    func area() -> Double
}

struct Circle: Shape {
    var radius: Double = 0.0
    func area() -> Double { return .pi * radius * radius }
}

// some 키워드 추가
func makeShape() -> some Shape {
    return Circle()
}

some 키워드가 없을 땐 Shape 프로토콜을 준수하는 많은 객체중 어떤 객체가 리턴되는지 정확하게 명시되어 있지 않아서 컴파일러가 오류를 발생시켰습니다.

some 키워드를 추가하여 something?과 비슷한 의미로 'Shape 프로토콜을 준수하는 어떤 클래스나 구조체, 열거형 등의 타입'을 리턴하겠다고 지정하는 것입니다.

 

some 키워드는 프로토콜 뿐만 아니라 클래스도 사용 가능합니다.

상위 클래스를 리턴할 때 some 키워드와 함께 사용할 수 있습니다.

class Animal {
    func feed() { }
}

class Cat: Animal { }
class Dog: Animal { }

func returnAnimalClass() -> some Animal {
    return Cat()
}

 

 

박스형 프로토콜 타입 Boxed Protocol Type

 

 

'Swift > 문법' 카테고리의 다른 글

ARC dive deep  (0) 2025.03.21
후행 클로저  (0) 2025.03.21
enum의 다른 사용방법  (0) 2025.03.14
Swift Protocols  (0) 2025.03.11
Swift 구조체와 클래스  (0) 2025.02.12

ARC는 Automatic Reference Counting으로 참조타입의 참조 횟수를 자동으로 카운팅 되며 참조 수가 0이 되면 메모리에서 자동으로 해제하여 메모리 사용량을 관리합니다.

자동으로 참조횟수를 트래킹 하지만, 강한 참조 사이클 같은 경우에는 각 객체가 소멸해도 강한 참조가 남아있어 메모리에 계속 유지되어 버리는 메모리 누수 현상이 발생하곤 합니다.

그래서 메모리 누수 방지를 위해 weak, unowned 키워드를 종종 사용하곤 했습니다.

 

 

강한 참조 사이클 예시

// 서로 참조하는 클래스 두개 생성
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

// 인스턴스 생성 (Person의 강한참조: 1, Apartment의 강한참조: 1)
var person: Person? = Person(name: "john")
var apartment: Apartment? = Apartment(unit: "")

// person.apartment는 Apartment 클래스를 강한참조
// apartment.tenant는 Person 클래스를 강한참조
// (Person의 강한참조: 2, Apartment의 강한참조: 2)
person?.apartment = apartment
apartment?.tenant = person

// person과 apartment 소멸하여 강한참조가 1 줄어들지만, 서로 참조하는 카운팅은 남아있음
// 메모리에 계속 유지되는 Person클래스와 Apartment클래스는 어떤 방법으로도 접근할 수 없음
// (Person의 강한참조: 1, Apartment의 강한참조: 1)
person = nil
apartment = nil

 

 

 

weak? unowned?

강한참조 사이클 발생을 방지하기 위해 두 가지 키워드 중 하나를 선택하여 사용할 수 있습니다.

결과적으로 메모리 누수를 피하기 위해 사용되지만 두 키워드의 차이점은 있습니다.

 

weak: 옵셔널 타입으로 명시하며, 참조하는 객체가 소멸되면, 해당 값은 nil이 됩니다.

// 서로 참조하는 클래스 두개 생성
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

// 인스턴스 생성 (Person의 강한참조: 1, Apartment의 강한참조: 1)
var person: Person? = Person(name: "john")
var apartment: Apartment? = Apartment(unit: "")

// person.apartment는 Apartment 클래스를 강한참조
// apartment.tenant는 Person 클래스를 약한참조
// (Person의 강한참조: 1, Apartment의 강한참조: 2)
person?.apartment = apartment
apartment?.tenant = person

// person객체가 소멸되면 apartment.tenant가 약한참조로 nil이 된다.
// (Person의 강한참조: 0, Apartment의 강한참조: 0)
person = nil
apartment = nil

 

 

 

unowned: 옵셔널 형식이 아니며, 참조하는 클래스보다 수명이 같거나 긴 경우에만 사용합니다. Swift5 업데이트 이후로 unowned도 옵셔널 형식을 가질 수 있지만, 참조하는 클래스가 nil이고 접근할 때 앱 크래시가 여전히 발생합니다.

앱크래시를 방지하기 위해 보통 절대로 메모리 해제하지 않는 객체를 참조할 때 사용합니다.

// 서로 참조하는 클래스 두개 생성
class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}

// 인스턴스 생성
var john: Customer?

// (Customer 강한참조: 1, CreditCard 미소유 참조)
// CreditCard의 수명은 Customer과 같기때문에 미소유 참조를 하였다.
// 만약 CreditCard 인스턴스를 생성하고, Customer가 nil일때 CreditCard.customer을 접근하면 앱 크래시 오류가 발생한다
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

 

서로 참조하는 객체의 수명이나 기획의도에 따라 키워드를 선택할 수 있다.

 

 

모든 서로 참조는 weak 키워드와 함께 쓰면 되지않을까?

unowned는 치명적인 오류를 발생하기 때문에 사용하지 않고 모든 서로참조를 weak 키워드와 함께 사용하면 되는 거 아닌가? 생각이 들었다. 기본적으로 weak는 옵셔널 형식의 약한 참조이다. 이에 따라 접근하기 위해 옵셔널 체이닝을 통해 접근하며, 언래핑이 필요하다.

그렇기 때문에 사용함에 있어서 값이 nil이 아님을 항상 증명해야 하고, 참조하는 객체가 메모리에서 내려가면 nil이 되므로 사이드 이펙트 발생 가능성 또한 존재하게 된다.

객체가 nil이 되지 않는 경우에는 weak보다 unowned 키워드가 더 적절하다.

 

 

 

둘 다 쓸 수 있는 상황에선 단순 취향 차이로 사용될 수 있을까?

두 개의 키워드는 사용목적은 같지만 성능면에서 차이가 드러납니다.

그전에 직접 앱을 돌려보면서 reference count를 알아봅시다.

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}


class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

먼저 두 개의 서로참조 클래스를 만들었습니다.

이를 lldb의 refCount를 확인하는 명령어를 통해 참조 카운트가 어떻게 진행되는지 알아보겠습니다.

명령어는 아래와 같습니다.

language swift refcount (클래스 변수명)

 

 

 

첫 번째로는 인스턴스 생성만 구현된 경우입니다.

var person: Person? = Person(name: "john")
var apartment: Apartment? = Apartment(unit: "unit")

중단을 통해 참조카운팅을 확인해 보니 세 가지 유형의 참조가 카운팅되는걸 볼 수 있었습니다.

ARC를 공부했을 때 Strong Count만 세면 되는 줄 알았는데 실제로는 세가지 유형이 존재하는 걸 알 수 있었습니다.

근데 왜 인스턴스만 생성하고 서로 참조하지 않고 있는데 strong count = 3이 될까요?

-> lldb에서 해당 참조카운팅을 확인하기 위해 참조하고 일시적으로 메모리에 남겨두기 때문에 strong count = 3이 됩니다. 실제로는 그렇지 않습니다.

 

 

두 번째로는 서로 참조를 구현한 경우입니다.

person?.apartment = apartment
apartment?.tenant = person

서로 참조하니 strong 카운트가 1 증가했습니다.

바로 person변수와 aparment에 nil을 주입시키겠습니다.

 

 

세 번째로 nil을 할당한 경우

person = nil
apartment = nil

변수에 nil을 대입하니 lldb에서 더 이상 접근할 수 없었습니다. 대신 Debug Memory Graph를 통해 메모리가 누수되었다는 것을 확인할 수 있습니다.

 

위 카운팅처럼 weak, unowned도 모두 카운팅이 되고 있었습니다.

- strong: 객체에 대한 강한 참조의 개수를 셉니다. 강한 참조의 카운트가 0이 되면 deinit이 호출됩니다. 이 시점에서 unowned 접근은 앱크래시, weak 접근은 nil을 리턴합니다.

- unowned: unowned의 참조 개수를 셉니다. 강한 참조가 존재하면 +1이 추가됩니다. deinit이 완료되면 추가된 1이 차감됩니다. unowned가 0이 되면 최종적으로 메모리에서 해제합니다.

- weak: weak의 참조 개수를 셉니다. unowned 참조가 존재하면 +1이 추가됩니다. 객체가 메모리 할당 해제가 되면 추가된 1이 차감됩니다.

 

Side Table

추가적인 정보가 여기서 나오는데, Side Table은 Swift4.0에서부터 도입되었습니다.

객체 인스턴스가 생성됐을 때, ARC 카운팅과 함께 Side Table이 생성됩니다.

이는 객체의 간단한 정보들을 담고 있습니다. (참조 주소와 같은)

Side Table은 weak/unowned참조가 존재할 때 생성되고, 최종적으로 weak카운트가 0이 될 때 소멸됩니다.

 

weak 접근은 사이드 테이블을 통해 객체 참조 주소에 접근하게 됩니다. 이는 간접접근이라 볼 수 있습니다.

unowned은 별도로 사이드테이블을 통한 것이 아닌 직접 접근하게 됩니다.

weak와 unowned의 성능차이는 눈에 띌 정도로 나타나진 않지만, weak를 통한 접근은 추가적인 비용이 발생하기 때문에, 굳이 비교하자면 weak보다 unowned가 더 빠르다고 볼 수 있습니다.

 

 

 

메모리 누수 방지를 위해 strong 참조만 신경 썼지만, 더 많은 카운팅이 존재했고, weak와 unowned의 차이를 알 수 있었습니다!

'Swift > 문법' 카테고리의 다른 글

Swift의 타입들 Types  (0) 2025.03.26
후행 클로저  (0) 2025.03.21
enum의 다른 사용방법  (0) 2025.03.14
Swift Protocols  (0) 2025.03.11
Swift 구조체와 클래스  (0) 2025.02.12

클로저가 아직 익숙하지 않아서 함수에서 사용된 후행클로저를 좀 자세히 알고 싶어서 작성합니다.

 

후행 클로저

함수의 마지막 인수에 클로저 표현식을 전달해야 하고 클로저 표현식이 긴 경우 후행 클로저로 작성하면 가독성이 더 좋아집니다.

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}


// Here's how you call this function without using a trailing closure:


someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})


// Here's how you call this function with a trailing closure instead:


someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}

 

자주 사용했던 고차함수 map도 후행클로저로 더 가독성 있게 표현할 수 있습니다.

let num = arr.map{ $0.description }

 

이는 파라미터로 인자를 받아서 클로저에 실행할 수도 있고, 받지않고 실행할 수 있습니다.

// 인자 없이 후행 클로저
func calculate(_ closure: (Int, Int) -> String ) {
    print(closure(10, 20))
}
// 인자를 전달하는 후행클로저
func calculate2(_ num1: Int,_ num2: Int,_ closure: (Int, Int) -> String) {
    let result = closure(num1, num2)
}

// 후행 클로저의 구현부는 함수의 호출부에서 구성
calculate { num1, num2 in
    return "\(num1 + num2)"
}

calculate2(10, 20) { num1, num2 in
    return "\(num1 + num2)"
}

 

'Swift > 문법' 카테고리의 다른 글

Swift의 타입들 Types  (0) 2025.03.26
ARC dive deep  (0) 2025.03.21
enum의 다른 사용방법  (0) 2025.03.14
Swift Protocols  (0) 2025.03.11
Swift 구조체와 클래스  (0) 2025.02.12

Enum

어떠한 객체나 UI같은 하나의 상태를 타입으로 나타낼 때 열거형(enum)을 자주 사용했었습니다.

enum State {
    case run
    case walk
    case sleep
    case eat
    case seat
}
// 배열로 사용한다면..?
var state = ["run", "walk", "sleep", "eat", "seat"]

배열로 사용하면 인덱스 범위를 넘어가거나 올바른 문자열을 받지 못하는 사이드 이펙트를 경험하기도 했습니다.

이를 열거형으로 사용하면 정해진 범위의 타입으로 지정할 수 있어서 코드 가독성과 사이드이펙트를 줄일 수 있었습니다.

 

오늘 알아볼 것은 이런 단순하게 상태에 대한 정의를 넘어서 더 다양한 방식들을 사용해보려고 합니다.

 

연관값

열거형의 case는 각 상태와 연관된 값을 추가로 설정할 수 있습니다. 이를 통해서 해당 case를 더 구체화 할 수 있습니다.

enum NetworkResult {
    case success
    case serverError(code: Int)
}

var someEnum: NetworkResult = .serverError(code: 119)
print(someEnum)
// someEnum1(code: 119)

위 예시와 같이 서버에러의 경우 에러와 연관된 에러코드와 함께 지정되어 해당 케이스의 값을 더 구체적으로 설정할 수 있습니다.

 

CaseIterable protocol

열거형에 CaseIterable을 채택하여 모든 케이스를 배열에 담아 리턴할 수 있습니다.

enum Configuration: CaseIterable {
    case setting
    case ui
    case network
    case logic
}


for element in Configuration.allCases {
    print(element)
}
// setting
// ui
// network
// logic

print(Configuration.allCases.count)
// 4

CaseIterable프로토콜의 allCases는 static 변수라 인스턴스가 아닌 타입으로 접근해야합니다.

public protocol CaseIterable {

    /// A type that can represent a collection of all values of this type.
    associatedtype AllCases : Collection = [Self] where Self == Self.AllCases.Element

    /// A collection of all values of this type.
    static var allCases: Self.AllCases { get }
}

 

 

생성자

열거형은 구조체와 같이 값타입 형식입니다. 그래서 생성자를 구현하지 않아도 생성할 수 있지만, 구조체처럼 생성자를 구현할 수 있습니다.

enum GameState: Int, CaseIterable {
    case play = 1
    case record
    case exit
    case none
    
    init(_ index: Int) {
        self = GameState.allCases[index]
    }
}

var baseballGameState1: GameState = .play
var baseballGameState2 = GameState(rawValue: 1)
var baseballGameState3 = GameState(3)
print(GameState.allCases)
print(baseballGameState3)
// 4
// none

GameState(3) 인덱스를 받아서 생성할 수 있습니다.

이렇게 생성자를 사용한다면 코드를 더 간결하게 사용 할 수 있을것이라 생각합니다.

 

 

프로퍼티와 메서드

열거형 안에 프로퍼티와 메서드를 구현할 수 있습니다. 하지만 get-only 프로퍼티만 구현할 수 있습니다.

enum SomeEnum2 {
    case some1
    case some2
    
    var description: String {
        switch self {
        case .some1:
            return "some1"
        case .some2:
            return "some2"
        }
    }
    func printState(state: SomeEnum2) {
        switch self {
        case .some1:
            print("some1")
        case .some2:
            print("some2")
        }
    }
}

var someEnum2: SomeEnum2 = .some1
print(someEnum2.description)
// some1

 

 

static

열거형 안에 static 키워드를 추가하여 타입메서드와 타입 프로퍼티를 생성할 수 있습니다.

enum SomeEnumWithStaticMethod {
    case some1
    case some2
    
    static var count: Int = 0
    
    static func printState(state: SomeEnumWithStaticMethod) {
        switch state {
        case .some1:
            print("some1")
        case .some2:
            print("some2")
        }
    }
}
print(SomeEnumWithStaticMethod.count)
SomeEnumWithStaticMethod.count = 10
SomeEnumWithStaticMethod.printState(state: .some1)
SomeEnumWithStaticMethod.printState(state: .some2)
print(SomeEnumWithStaticMethod.count)
// 0
// some1
// some2
// 10

기존의 프로퍼티는 get-only 프로퍼티만 생성할 수 있지만, static 키워드로 타입프로퍼티를 선언하면 타입에 접근하기 때문에 get-set property로 선언할 수 있습니다. 해당 타입 메서드와 프로퍼티는 값에 접근할 때 타입에 접근하여 사용해야합니다.

이를 통해 UI의 환경설정같은 상수를 선언할 때 자주 사용합니다.

enum AppStyles {
    enum Colors {
        static let mainColor = UIColor(red: 1, green: 0.2, blue: 0.2, alpha: 1)
        static let darkAccent = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
    }
    enum FontSizes {
        static let small: CGFloat = 12
        static let medium: CGFloat = 14
        static let large: CGFloat = 18
        static let xlarge: CGFloat = 21
    }
}

AppStyles.Colors.mainColor
AppStyles.FontSizes.medium

 

 

상태를 변화하는 메서드

값타입 열거형은 자신의 상태를 변경시키는 메서드도 구현할 수 있습니다.

값이 변하는 메서드라면 구조체처럼 메서드앞에 mutating 키워드를 사용합니다.

enum Toggle {
    case on
    case off
    
    mutating func toggle() {
        switch self {
        case .on:
            self = .off
        case .off:
            self = .on
        }
    }
}

var buttonState: Toggle = .off
buttonState.toggle()

 

 

If case, guard case

열거형 안의 case에 접근할 때 switch구문을 통해 접근했었습니다.

다만 switch구문을 사용하면 모든 케이스에 대한 동작을 구현해야 하기 때문에 코드가 길어질 수 밖에없습니다.

여러 케이스중 하나에만 접근하고 싶다면 switch 대신 If case, guard case를 사용하면 더 간결하게 열거형에 접근할 수 있습니다.

if case .on = buttonState {
    print("On")
}

if case .on = buttonState {
    print("ON")
}
 On
 ON

guard case .on = buttonState else {
    return
}

 

 

프로토콜 채택

열거형에 프로토콜을 채택할 수 있습니다.

protocol GameStateProtocol {
    static var gameStateTitle: String { get }
    func printGameState()
    mutating func changeGameState(state: BaseballGameState)
}

enum BaseballGameState: String, GameStateProtocol {
    
    static var gameStateTitle: String = "Baseball"
    
    case play = "play"
    case pause = "pause"
    case exit = "exit"
    
    func printGameState() {
        print(BaseballGameState.gameStateTitle)
    }
    
    mutating func changeGameState(state: BaseballGameState) {
        self = state
    }
    
}

var state = BaseballGameState.pause
state.printGameState()
print(BaseballGameState.gameStateTitle)
state.changeGameState(state: .play)
print(state)
// Baseball
// Baseball
// play

프로토콜 채택시 프로토콜의 요구사항을 구현하고 열거형을 사용할 수 있습니다.

 

 

이렇게 열거형의 더 많은 사용방법을 알아보았습니다.

이러한 방법들을 활용하면 더 간결하고 가독성 있게 코드를 작성할 수 있을것이라 생각합니다

'Swift > 문법' 카테고리의 다른 글

ARC dive deep  (0) 2025.03.21
후행 클로저  (0) 2025.03.21
Swift Protocols  (0) 2025.03.11
Swift 구조체와 클래스  (0) 2025.02.12
Swift Concurrency 정리  (7) 2024.11.10

프로토콜(Protocols)

프로토콜은 메서드, 프로퍼티, 그리고 특정 작업이나 기능의 부분이 적합한 다른 요구사항의 청사진을 정의합니다.

프로토콜 안에는 선언만 하고, 구현부분에서 해당 변수나 메서드를 구현합니다.

또한 여러 프로토콜을 채택할 수 있습니다.

protocol Named {
    var fullName: String { get }
    func introduce()
}

protocol Aged {
    var age: Int { get }
}

class Human: Named, Aged {
    var age: Int = 10
    var fullName: String = "Kim Cheol Su"
    
    func introduce() {
        print("hi i am \(fullName)")
    }
}

 

프로토콜에 선언되는 변수와 메서드는 다음과 같습니다.

protocol SomeProtocol {
    
    // gettable settable
    var someProperty1: String { get }
    var someProperty2: String { get set }
    static var someProperty3: String { get set }
    
    // Method
    func someMethod1()
    func someMethod2() -> String
    static func someMethod3() -> String
    mutating func someMethod4() -> String
    
    //initializer
    init()
}

class SomeCalss: SomeProtocol {
    required init() { }
}

 

 

타입으로 사용되는 프로토콜

프로토콜은 선언만으로 기능을 수행하지 않습니다. 이런 점에서 프로토콜은 타입으로도 사용할 수 있습니다.

protocol TableItemPresentable {
    var id: String { get }
    var title: String { get }
    var imageURL: String? { get }

    mutating func configure()
}

struct TableItem: TableItemPresentable {
    var id: String = "TableItem"

    var title: String = "TableItem"

    var imageURL: String? = nil

    mutating func configure() {
        self.title = "TableView Item"
    }
}

struct CollectionItem: TableItemPresentable {
    var id: String = "CollectionItem"

    var title: String = "CollectionItem"

    var imageURL: String? = nil

    mutating func configure() {
        self.title = "CollectionView Item"
    }
}
var items: [TableItemPresentable] = [TableItem(), CollectionItem()]
print(items[0].title)
print(items[1].title)

// 출력
// TableItem
// CollectionItem

 

확장으로 프로토콜의 요구사항을 준수할 수 있습니다.

protocol Favorite {
    func introduce() -> String
}

class Human {
    var favoriteName: String
    init(favoriteName: String) {
        self.favoriteName = favoriteName
    }
}
extension Human: Favorite {
    func introduce() -> String {
        print("my favorite is \(favoriteName)")
    }
}

 

 

프로토콜의 요구사항을 선택적으로 만들기

프로토콜은 선언시 요구사항을 충족해야만 합니다.

프로토콜의 프로퍼티나 메서드의 요구사항을 구현할 때, 해당 구현부분에서 불필요하게 구현하게 될 수 있습니다.

그래서 해당 메서드나 프로퍼티를 옵셔널로 구현할 수 있습니다.

@objc protocol Information {
    var name: String { get set }
    @objc optional func introduce()
}

class Human: Information {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func introduce() {
        print("hi i am \(name)")
    }
}

class Desk: Information {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

사람과 책상을 하나의 프로토콜을 묶었을 때, 책상은 말을 할 수 없으므로 introduce 메서드가 필요하지 않습니다.

이때 @objc optional 키워드를 붙여 해당 요구사항이 선택적으로 구현될 수 있도록 설정합니다.

요구사항이 선택적으로 구성된 경우 protocol 앞에 optional 키워드를 추가해야합니다.

또한 @objc 키워드가 추가되면 클래스에만 상속할 수 있습니다.

 

 

 

'Swift > 문법' 카테고리의 다른 글

후행 클로저  (0) 2025.03.21
enum의 다른 사용방법  (0) 2025.03.14
Swift 구조체와 클래스  (0) 2025.02.12
Swift Concurrency 정리  (7) 2024.11.10
Swift 프로토콜 번역(추가 번역중) (Swift.org)  (1) 2024.02.11

구조체와 클래스는 비슷하면서 서로 다른 성질을 갖고 있습니다.

먼저 공통점으로는 다음과 같습니다.

- 변수나 상수를 사용하여 값을 저장하는 프로퍼티로 정의할 수 있음

- 함수를 사용하여 기능을 제공하는 메서드로 정의할 수 있음

- 속성값에 접근할 수 있는 방법을 제공하는 서브스크립트를 정의할 수 있음

- 객체를 원하는 초기 상태로 설정해주는 초기화 블록(init)을 정의할 수 있음

- 객체에 함수적 기능을 추가하는 확장 구문을 사용할 수 있음

- 특정 형식의 함수적 표준을 제공하기 위한 프로토콜을 구현할 수 있음

 

이와 같은 공통점이 있지만, 구조체에서는 안되지만 클래스에서는 할 수 있는 기능이 다음과 같이 있습니다.

- 클래스의 특성을 다른 클래스에게 물려줄 수 없음

- 실행 시 컴파일러가 클래스 인스턴스의 타입을 미리 파악하고 검사할 수 있음

- 인스턴스가 소멸되기 직전에 처리해야 할 구문을 미리 등록해 놓을 수 있음 (deinit)

- 클래스 인스턴스가 전달될 때에는 참조 형식으로 제공되며, 참조가 가능한 개수는 제약이 없음

 

 또한 클래스는 참조형식, 구조체는 값형식 프로퍼티로 클래스는 값이 변경됨에 따라 참조된 클래스의 값에 영향을 주고 구조체는 영향을 주지 않는 성질을 갖고 있습니다.

// 구조체
struct User {
    var name: String
    var age: Int
}

var userStruct = User(name: "", age: 0)
var cheolsu = userStruct
var yuri = userStruct

print("cheolsu: \(cheolsu)")
print("yuri: \(yuri)")

yuri.name = "yuri"
yuri.age = 5

print("cheolsu: \(cheolsu)")
print("yuri: \(yuri)")

// 출력 결과
cheolsu: User(name: "", age: 0)
yuri: User(name: "", age: 0)
cheolsu: User(name: "", age: 0)
yuri: User(name: "yuri", age: 5)
// yuri의 값을 변경했지만 cheolsu 객체에 영향을 주지 않는다.

// 클래스
class User {
    var name: String = ""
    var age: Int = 0
}

var userClass = User()
var cheolsuClass = userClass
var yuriClass = userClass

yuriClass.name = "yuri"
yuriClass.age = 5

print(yuriClass.age)
print(yuriClass.name)
print(cheolsuClass.age)
print(cheolsuClass.name)

// 출력 결과
5
yuri
5
yuri
// yuri의 값을 변경했지만, cheolsu객체의 특성도 변경되었다.

 

 

 

'Swift > 문법' 카테고리의 다른 글

후행 클로저  (0) 2025.03.21
enum의 다른 사용방법  (0) 2025.03.14
Swift Protocols  (0) 2025.03.11
Swift Concurrency 정리  (7) 2024.11.10
Swift 프로토콜 번역(추가 번역중) (Swift.org)  (1) 2024.02.11

- Swift.org를 참고하여 작성

 

Swift는 구조화된 방식으로 비동기 및 병렬 코드를 작성하기위해 기본적으로 기능을 제공합니다.

Swift의 언어자원을 사용하지 않고도 동시코드를 작성할 수 있지만, 읽기가 더 어렵고 복잡합니다.

 

사용하지 않은 예

listPhotos(inGallery: "Summer Vacation") { photoNames in
    let sortedNames = photoNames.sorted()
    let name = sortedNames[0]
    downloadPhoto(named: name) { photo in
        show(photo)
    }
}

위 예제에서도 코드를 완료 핸들러로 작성해야 하기 때문에 중첩된 클로저를 사용하게 되고, 코드가 더 복잡하여 다루기 힘들어질 수 있습니다.

 

 

비동기 함수 및 호출

Swift에서 제공하는 비동기 함수 또는 비동기 메서드는 실행 중간에 일시 중단될 수 있는 특수한 종류의 함수 또는 메서드 입니다.

비동기 함수는 동기함수처럼 실행되거나 오류를 발생시키거나 반환하지않는 세가지 중 하나를 수행하지만, 중간에 일시 중단할 수 도 있습니다. 본문 내부에서 실행을 일시 중단할 수 있는 위치를 표시합니다.

 

먼저 함수나 메서드가 비동기함수임을 나타내려면 선언에서 매개변수 뒤 async 키워드를 작성하면됩니다. 이는 throws throw 함수를 표시하는데 사용하는 방법과 비슷합니다.

함수나 메서드가 값을 반환하는 경우 return 화살표 앞에 async 를 쓰면 됩니다. 

func listPhotos(inGallery name: String) async -> [String] {
    let result = // ... some asynchronous networking code ...
    return result
}

- 비동기면서 에러를 반환하는 throws기능을 하는 함수나 메서드인 경우 throws 앞에 async를 작성합니다.

func dataFetch<T: Decodable>(type: T.Type) async throws -> T? {
        return try await AF.request(components.getComponents()).serializingDecodable(T.self).value
    }

 

비동기 메서드를 호출할 때 해당 메서드가 반환될 때까지 실행이 중단됩니다. 이때 await 키워드를 작성하여 비동기 함수를 호출 하는 지점 앞에 중단지점을 표시합니다. 이는 try throw 함수를 호출할 때 오류가 발생하는 경우 프로그램 흐름에 대한 변경 가능 사항을 표시하기 위해 작성하는 것과 같습니다. 중단은 중단지점 없이 암묵적으로 진행되거나 먼저 진행되지 않습니다.

 

코드의 실행 과정

let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)

 

1. 첫째줄 코드가 실행을 시작하여 listPhotos함수가 실행되고 반환될 때 까지 일시 중단됩니다.

2. 이 코드가 중단되는 동안 다른 동시코드들은 실행이 됩니다.

3. listPhotos가 반환되고 반환된 값을 photoNames에 할당합니다. 코드는 일시중단된 지점에서 다시 시작합니다.

4. sortedName를 정의하고 name에 값을 할당합니다. await 표시가 없는 동기 함수이므로 그대로 실행됩니다.

5. 다음 함수는 downloadPhoto를 호출하는 함수입니다. 해당 함수가 반환될때 까지 일시 중단하여 다른 동시코드가 실행할 수 있도록 기회를 제공합니다.

6. downloadPhoto 반환 후 반환된 값이 할당되고 show함수를 호출할 때 인자로 전달됩니다.

 

-> 비동기함수를 호출할 때 await 키워드가 적힌 곳은 현재 코드가 실행을 일시 중지할 수 있음을 나타냅니다. 이를 스레드 양보라고 합니다. Swift가 내부적으로 현재 스레드에서 코드 실행을 일시 중지하고 대신 해당 스레드에서 다른 코드를 실행하기 때문입니다. 

 

Task.yield()

Task.yield() 메서드를 호출하여 명시적으로 중단지점을 삽입 할 수 있습니다.

func generateSlideshow(forGallery gallery: String) async {
    let photos = await listPhotos(inGallery: gallery)
    for photo in photos {
        // ... render a few seconds of video for this photo ...
        await Task.yield()
    }
}

예시로 비디오를 렌더링하는 코드가 동기식이라면, 그 코드는 await같은 정지지점을 포함하지 않습니다. 비디오를 렌더링 하는 작업은 오랜시간이 걸릴 수 있으므로 위와 같이 주기적으로 호출하여 정지점을 명시적으로 추가할 수 있습니다.

 

Task.sleep(for: second)

중단 최소시간을 지정하여 일시중단할 수 있습니다.

func listPhotos(inGallery name: String) async throws -> [String] {
    try await Task.sleep(for: .seconds(2))
    return ["IMG001", "IMG99", "IMG0404"]
}

위 함수는 비동기적이면서 오류를 리턴할 수 있는 async throw 함수입니다. 위와 같은 함수를 호출할 때는 try await 모두 작성합니다.

let photos = try await listPhotos(inGallery: "A Rainy Weekend")

 

비동기 함수는 throw함수와 유사하지만 중요한 차이점이 있습니다.

throw코드는 do-catch블록으로 감싸서 오류를 처리하거나 Result<Data, Error>오류를 저장하여 다른곳에서 처리할 수 있습니다.

func availableRainyWeekendPhotos() -> Result<[String], Error> {
    return Result {
        try listDownloadedPhotos(inGallery: "A Rainy Weekend")
    }
}

반면 비동기 코드를 래핑하여 동기 코드에서 호출하고 결과를 기다릴 수 있는 안전한 방법은 없습니다. 이를 직접 구현하려고 하면 미묘한 경합, 스레딩 문제 및 교착상태와 같은 문제가 발생할 수 있습니다. 

 

 

비동기 시퀀스

이전에는 배열의 모든 요소가 준비된 후 전체배열을 한번에 비동기적으로 반환했지만 비동기 시퀀스를 사용하여 한번에 한개의 요소를 기다는 방법도 있습니다. 비동기 시퀀스를 반복하는 모습은 다음과 같습니다.

import Foundation


let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
    print(line)
}

일반적은 for-in루프를 사용하는 대신 뒤에 try await을 사용합니다.

 

비동기 함수를 병렬로 호출

비동기 함수를 호출하면 한번에 한개의 코드만 실행됩니다. 비동기 코드가 실행되는 동안 호출자는 다음 코드 줄을 실행하기 전 해당 코드가 완료될 때까지 기다립니다. 예를 들어 갤러리에서 처음 세 장의 사진을 가져오려면 다음과 같이 downloadPhoto 함수에 대한 세 번의 호출을 기다릴 수 있습니다.

let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])


let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

이 접근 방식에는 중요한 단점이 있습니다. 다운로드가 완료되기 전에 다음 다운로드가 진행되지 않는다는 점 입니다.

이러한 작업을 기다릴 필요 없이 독립적으로 또는 동시에 다운로드할 수 있습니다.

 

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])


let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

비동기 함수를 호출하고 주변 코드와 병렬로 실행하려면 상수를 정의할 때 let앞에 async를 쓰고 상수를 사용할 때마다 뒤에 await를 씁니다. 위 예제에서 세 개의 호출은 모두 이전 호출이 완료될 때까지 기다리지 않고 시작합니다. 코드가 함수의 결과를 기다리지 위해 일시 중지 되지 않기 때문에 함수 호출 중 어느곳도 await로 표시되지 않습니다. 대신 photos가 정의된 줄까지 실행이 계속 됩니다. 이 비동기 호출의 결과가 필요하므로 세장의 사진 모두 다운로드가 완료될 때까지 실행을 일시 중지하기 위해 await를 작성합니다.

 

두가지 접근 방식의 차이점은 다음과 같습니다.

  • 다음 줄의 코드가 해당 함수의 결과에따라 달라지는 경우 await를 사용하여 비동기 함수를 호출합니다. 이렇게 하면 순차적으로 수행되는 작업이 생성됩니다.
  • 해당 함수의 결과와 상관없는경우 async-let으로 비동기 함수를 호출합니다. 이렇게하면 병렬로 수행할 수 있는 작업이 생성됩니다.
  • await 및 async-let 둘 다 일시중단된 동안 다른코드가 실행되도록 허용합니다.
  • await는 두 경우 모두 필요한 경우 비동기 함수가 반환될 때 까지 실행이 일시 중지됨을 나타냅니다.

 

작업 및 작업 그룹 Task and Task Groups

Task는 프로그램의 일부가 비동기적으로 실행할 수 있는 작업단위입니다. 모든 비동기 코드는 일부 작업의 일부로 실행됩니다. Task 자체는 한가지 작업만 수행하지만 여러 작업을 만들면 동시에 실행되도록 예약할 수 있습니다.

 

이전 섹션에서 설명한 async-let구문은 암시적으로 자식 Task를 생성합니다. 이 구문은 프로그램에서 실행해야 하는 작업을 이미 알고 있는 경우에 효과적입니다. Task Group을 생성하고 해당 그룹에 자식 Task를 명시적으로 추가할 수도 있습니다. 이를 통해 우선수위와 작업 취소를 더 제어하기 쉽고 동적인 작업 수를 생성할 수 있습니다.

 

Task는 계층구조로 배열됩니다. 주어진 Task Group 에서 각 Task는 동일한 부모 작업을 갖고 자식 Task를 가질 수 있습니다.

Task와 Task Group간의 명시적 관계 때문에 이 접근 방식을 구조적 동시성 이라고 합니다. 이런 관계에는 여러가지 이점이 있습니다.

  • 부모 작업에서는 자식 작업이 완료될 때까지 기다리는 것을 잊을 수 없다.
  • 자식 작업에 더 높은 우선순위를 설정하면 부모 작업의 우선순위가 자동으로 높아진다.
  • 부모 작업이 취소되면, 해당 자식 작업도 모두 자동으로 취소된다.
  • 작업 로컬 값은 자식 작업으로 효율적이고 자동으로 전파됩니다.

다음은 많은 사진 다운로드 할 수 있는 코드입니다.

await withTaskGroup(of: Data.self) { group in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
        group.addTask {
            return await downloadPhoto(named: name)
        }
    }


    for await photo in group {
        show(photo)
    }
}

위 코드는 새로운 Task Group을 만든 다음, 갤러리의 각 사진을 다운로드하기 위한 자식 Task를 만듭니다. Swift는 조건이 허락되면 작업을 동시에 실행합니다. 자식 Task가 사진 다운로드를 완료하자마자 해당 사진이 표시됩니다. 완료되는 순서에 대한 보장은 없어서 갤러리의 사진은 어떤 순서로든 표시될 수 있습니다.

- 사진을 다운로드하는 코드에서 오류가 발생하면 대신 withThrowingTaskGruop을 호출하면 됩니다.

 

위 코드는 사진을 다운로드된 다음 표시되므로 Task Group은 결과를 반환하지 않습니다.

결과를 변환하는 Task Group인 경우 클로저 내부에 결과를 누적하여 전달하는 코드를 추가합니다.

let photos = await withTaskGroup(of: Data.self) { group in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
        group.addTask {
            return await downloadPhoto(named: name)
        }
    }


    var results: [Data] = []
    for await photo in group {
        results.append(photo)
    }


    return results
}

이전 예제와 마찬가자로 이 예제도 각 사진에 대한 다운로드할 자식 Task를 생성합니다. for-await-in 루프는 다음 작업이 완료될 때까지 기다리고 해당 작업의 결과를 결과배열에 추가한 다음 모든 자식 Task가 완료될 때까지 계속 기다립니다. 마지막으로 다운로드한 사진배열을 전체 결과로 반환합니다.

 

 

작업 취소 Task Cancellation

Swift 동시성은 협력 취소 모델을 사용합니다. 각 Task는 실행의 적절한 지점에서 취소되었는지 확인하고 적절하게 대응합니다.

취소에 대한 대응은 일반적으로 다음 중 하나를 의미합니다.

  • CancellationErro와 같은 오류가 발생했을 때
  • nil 또는 빈 컬렉션을 반환할 때
  • 부분적으로 완료된 작업을 반환할 때

데이터가 크거나 네트워크가 느린경우 다운로드하는데 시간이 오래걸릴 수 있습니다. 모든 작업이 완료될 때까지 기다리지 않고 사용자가 작업중지할 수 있도록 하려면 작업이 취소되었는지 확인하고 취소된 경우 실행을 중지해야 합니다. 여기엔 두가지 방법이 있습니다.

 

Task.checkCancellation() type메서드를 호출하거나 Task.isCancelled type 속성을 읽는것

작업이 취소되면 check Cancellation() 호출시 오류가 발생합니다. throw 작업은 오류를 작업 외부로 전파하여 Task의 모든 작업을 중지할 수 있습니다. 이는 구현과 이해가 간단하다는 장점이 있습니다.

 

유연성을 높이려면 isCancelled속성을 사용하는 방법이 있습니다. 이 속성을 사용하면 네트워크 연결 닫기, 임시 파일 삭제 등 작업 중지의 일부로 정리 작업을 수행할 수 있습니다.

let photos = await withTaskGroup(of: Optional<Data>.self) { group in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
        let added = group.addTaskUnlessCancelled {
            guard !Task.isCancelled else { return nil }
            return await downloadPhoto(named: name)
        }
        guard added else { break }
    }


    var results: [Data] = []
    for await photo in group {
        if let photo { results.append(photo) }
    }
    return results
}

위 코드는 이전 버전과 비교해 여러 가지 변경 사항을 적용했습니다.

  • 각 작업은 취소 후 새 작업을 시작하는 것을 방지하기 위해 TaskGroup.addTaskUnlessCancelled(priority: operation: ) 사용합니다.
  • addTaskUnlessCancelled 에 대한 호출 후 코드는 새 자식 Task가 추가되었음을 확인합니다. 그룹이 취소되면 added 값은 false가 됩니다. 이 경우 코드는 추가 사진을 다운로드 하려는 시도를 중단합니다.
  • 각 Task는 사진을 다운로드하기 전에 취소 여부를 확인합니다. 취소된 경우 Task는 nil로 반환됩니다.
  • 마지막으로 각 Task Group은 결과를 확인할 때 nil 값을 건너뜁니다. nil 반환을 통해 취소를 처리한다는 것은 Task Group이 완료된 작업을 삭제하는 대신 취소 시 이미 다운로드한 사진으로 부분 결과를 반환할 수 있음을 의미합니다. 

- 해당 작업 외부에서 작업이 취소되었는지 확인하려면 Task.isCancelled type 속성 대신 instance 속성을 사용합니다.

 

만약 취소에 대한 즉각적인 통지가 필요한 작업의 경우 이 방법을 사용합니다:

Task.withTaskCancellationHandler(operation: onCancel: isolation:)

let task = await Task.withTaskCancellationHandler {
    // ...
} onCancel: {
    print("Canceled!")
}


// ... some time later...
task.cancel()  // Prints "Canceled!"

취소 핸들러를 사용할때 작업 취소는 여전히 협력적입니다. 작업은 완료될 때까지 실행되거나 취소를 확인한 후 일찍 중지됩니다. 취소 핸들러가 시작될 떄 작업이 계속 실행 중이므로 작업과 해당 취소 핸들러 사이 상태를 공유하지 마세요. 이로 인해 데이터 레이스 현상이 발생할 수 있습니다.

 

 

구조화되지 않은 동시성 Unstructured Concurrency

이전 섹션에서는 동시성에 대한 구조화된 접근방식이였습니다. 그외에도 Swift는 구조화되지 않은 동시성도 지원합니다.

Task Group의 일부 Task와 다르게 구조화되지 않은 작업에는 부모 작업이 없습니다. 프로그램이 의도한 방식대로 구조화되지 않은 작업을 관리할 수 있다는 완전한 유연성을 갖고 있지만 정확성에 대한 전적인 책임도 프로그램한테 존재합니다. Actor에서 실행되는 구조화되지 않은 Task를 생성하려면 Task.init(priority: Operation:) 생성자를 호출하세요. 액터의 일부가 아닌 구조화되지 않은 작업을 생성하려면 Task.detached(priority: Operation:) 클래스 메서드를 호출하세요. 두 작업 모두 결과를 기다리거나 취소하는 등 상호작용할 수 있는 Task를 반환합니다.

let newPhoto = // ... some photo data ...
let handle = Task {
    return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value

 

Task에 대한 자세한 정보

https://developer.apple.com/documentation/swift/task

 

Task | Apple Developer Documentation

A unit of asynchronous work.

developer.apple.com

 

 

Actors

Task를 사용하여 프로그램을 격리도니 동시적 부분으로 나눌 수 있습니다. 작업은 서로 격리되어 있어 동시에 실행해도 안전하지만, 때로는 작업 간에 일부 정보를 공유해야 합니다. actor를 사용하면 동시적 코드 간에 안전하게 정보를 공유할 수 있습니다.

클래스와 마찬가지로 actor는 참조유형입니다. 참조 유형과 값 유형의 비교는 클래스뿐만 아니라 actor에도 적용됩니다.

클래스와 달리 actor는 한번에 하나의 작업만 변경 가능한 상태에 액세스하도록 허용하므로 여러 작업의 코드가 액터의 동일한 인스턴스와 상호 작용하는 것이 안전합니다.

 

여기 온도를 기록하는 actor가 있습니다.

actor TemperatureLogger {
    let label: String
    var measurements: [Int]
    private(set) var max: Int


    init(label: String, measurement: Int) {
        self.label = label
        self.measurements = [measurement]
        self.max = measurement
    }
}

actor 키워드로 소개한 다음 중괄호 쌍으로 정의합니다. TemperatureLogger actor는 외부의 다른 코드가 액세스할 수 있는 속성을 가지고 있으며 actor 내부의 코드만 최대값을 업데이트할 수 있도록 max 속성을 제한합니다. 구조체 및 클래스와 동일한 초기화 구문을 사용하여 actor의 인스턴스를 생성합니다. actor의 속성이나 메서드에 액세스할 때 await 키워드를 사용하여 잠재적 일시 중단 지점을 표시합니다.

let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"

이 예시에서 logger.max에 액세스하는 것이 일시 정지 지점이 될 수 있습니다. actor는 한번에 하나의 작업만 변경 가능한 상태에 액세스하도록 허용하므로 다른 작업의 코드가 이미 logger와 상호 작용하고 있는 경우 이 코드는 속성에 액세스하기를 기다리는 동안 일시 중지됩니다.

 

대조적으로 actor의 일부인 코드는 actor의 속성에 액세스할 때 대기를 작성하지 않습니다.

예시로 새로운 온도로 TemperatureLogger를 업데이트하는 방법은 다음과 같습니다.

extension TemperatureLogger {
    func update(with measurement: Int) {
        measurements.append(measurement)
        if measurement > max {
            max = measurement
        }
    }
}

이 update 메서드는 이미 actor에서 실행 중이므로 max와 같은 속성에 대한 엑세스를 await로 표시하지 않습니다. 또한 이 방법은 actor가 변경 가능한 상태와 상호 작용하기 위해 한 번에 하나의 작업만 허용하는 이유 중 하나를 보여줍니다. actor의 상태에 대한 일부 업데이트는 일시적으로 불변성을 깨트립니다. TemperatureLogger actor는 온도 목록과 최대 온도를 추적하고 새 측정 값을 기록할 때 최대 온도를 업데이트합니다. 업데이트 도중에 새 측정값을 추가한 후 최대값을 업데이트하기 전에 TemperatureLogger가 일시적으로 일관되지 않은 상태에 있습니다. 여러 작업이 동일한 인스턴스와 동시에 상호작용하는 것을 방지하면 다음과 같은 일련의 이벤트와 같은 문제를 방지할 수 있습니다.

 

1. update 메서드를 호출합니다. 이것은 measurements 배열을 처음으로 업데이트합니다.

2. max가 업데이트 되기전에 다른 곳의 코드가 max와 온도 배열을 읽습니다.

3. 코드는 max를 변경하여 업데이트를 완료합니다.

 

이 경우 다른 곳에서 실행되는 코드는 데이터가 일시적으로 유효하지 않은 동안 update 호출 중간에 actor에 대한 엑세스가 인터리브 되었기 때문에 잘못된 정보를 읽습니다. swift actor를 사용하면 한번에 해당 상태에서 하나의 작업만 허용하고 해당 코드는 정지 지점을 표시하는 await가 정지 지점을 표시하는 위치에서만 중단될 수 있기 때문에 이 문제를 방지할 수 있습니다. update에는 정지 지점이 포함되어 있지 않으므로 업데이트 도중에는 다른 코드가 데이터에 엑세스할 수 없습니다.

actor 외부의 코드가 구조체나 클래스의 속성에 접근하는 것처럼 해당 속성에 직접 접근하려고 하면 컴파일 타임 오류가 발생합니다.

print(logger.max)  // Error

await를 쓰지 않고 logger.max에 액세스하면 actor의 격리된 로컬 상태의 일부이기 때문에 실패합니다. 이 속성에 액세스하는 코드는 actor의 일부로 실행되어야 하며, 이는 비동기 작업이며 await키워드가 필요합니다. Swift는 actor에서 실행되는 코드만 해당 actor의 로컬 상태에 액세스할 수 있음을 보장합니다. 이러한 보장을 actore isolation 이라고 합니다.

 

Swift 동시성 모델의 다음 측면은 공유된 변경 가능 상태에 대해 더 쉽게 추론할 수 있도록 함께 작동합니다.

  • 일시 중단 가능성이 있는 지점 사이에 있는 코드는 다른 동시 코드로 인해 중단될 가능성 없이 순차적으로 실행됩니다.
  • actor의 로컬 상태와 상호 작용하는 코드는 해당 actore내부에서만 실행됩니다.
  • actor는 한번에 하나의 코드만 실행합니다.

이러한 보장으로 인해 await를 포함하지 않고 actor 내부에 있는 코드는 프로그램의 다른 위치에서 일시적으로 유효하지 않은 상태를 관찰할 위험 없이 업데이트를 수행할 수 있습니다. 

아래 코드는 측정된 온도를 화씨에서 섭씨로 변환합니다.

extension TemperatureLogger {
    func convertFahrenheitToCelsius() {
        measurements = measurements.map { measurement in
            (measurement - 32) * 5 / 9
        }
    }
}

위의 코드는 측정 배열을 한 번에 하나씩 변환합니다. map 작업이 진행되는 동안 일부 온도는 화씨, 다른 온도는 섭씨로 표시됩니다. 그러나 코드에는 await가 포함되어 있지 않으므로 이 메서드에는 잠재적인 일시 중단 지점이 없습니다. 이 메서드가 수정하는 상태는 actor에 속하며 해당 코드가 액터에서 실행될 때를 제외하고는 코드를 읽거나 수정하지 못하도록 보호합니다. 즉, 단위 변환이 진행되는 동안 다른 코드에서 부분적으로 변환된 온도 목록을 읽을 수 있는 방법이 없습니다.

 

잠재적인 중단 지점을 생략하여 일시적인 유효하지 않은 상태를 보호하는 actor에 코드를 작성하는 것 외에도 해당 코드를 동기식 메서드로 이동할 수 있습니다. 위의 ConvertFahrenheitToCelsius()메서드는 동기 메서드이므로 잠재적인 정지 지점이 포함되지 않음이 보장됩니다. 이 기능은 데이터 모델을 일시적으로 불일치하게 만드는 코드를 캡슐화하고, 작업을 완료하여 데이터 일관성이 복원되기 전에는 다른 코드를 실행할 수 없다는 것을 코드를 읽는 사람이 더 쉽게 인식할 수 있도록 합니다. 앞으로 이 함수에 동시 코드를 추가하여 일시 중단 지점을 도입하려고 하면 버그가 발생하는 대신 컴파일 시간 오류가 발생하게 됩니다.

 

 

보낼 수 있는 유형 Sendable Types

Task와 Actor를 사용하면 프로그램을 안전하게 동시에 실행할 수 있는 부분으로 나눌 수 있습니다. Task나 Actor 인스턴스 내부에서 변수와 속성과 같은 변경 가능한 상태를 포함하는 프로그램 부분을 동시성 도메인(Concurrency Domain) 이라고합니다. 일부 데이터는 변경 가능한 상태를 포함하기 때문에 동시성 도메인 간에 공유할 수 없지만 중복 액세스로부터 보호하지는 않습니다.

 

한 동시성 도메인에서 다른 동시성 도메인으로 공유할 수 있는 유형을 Sendable type이라고 합니다.

예를 들어, actor 메서드를 호출할 때 인수로 전달하거나 Task의 결과로 반환할 수 있습니다. 이 장의 앞부분에 나온 예제에서는 sendability에 대해 설명하지 않았는데, 이러한 예제에서는 동시성 도메인 간에 전달되는 데이터에 대해 항상 안전하게 공유할 수 있는 간단한 값 유형을 사용하기 때문입니다. 반면, 일부 유형은 동시성 도메인 간에 전달하는 것이 안전하지 않습니다. 예를 들어, 변경 가능한 속성을 포함하고 해당 속성에 대한 액세스를 직렬화하지 않는 클래스는 다른 직업간에 해당 클래스의 인스턴스를 전달할 때 예측할 수 없고 잘못된 결과를 생성할 수 있습니다

 

Sendable protocol을 사용하여 보낼 수 있는 유형으로 표시합니다. 해당 프로토콜에는 코드 요구 사항이 없지만 Swift에서 적용하는 의미적 요구 사항이 있습니다.

일반적으로 유형을 보낼 수 있는 방법에는 세 가지가 있습니다.

  • 유형은 값 유형이고, 변경 가능한 상태는 다른 전송 가능한 데이터로 구성됩니다. 예를 들어, 전송 가능한 저장된 속성이 있는 구조체나 전송 가능한 연관된 값이 있는 열거형이 있습니다.
  • 이 유형에는 변경 가능한 상태가 없으며, 변경 불가능한 상태는 다른 전송 가능한 데이터(예: 읽기 전용 속성만 있는 구조체나 클래스)로 구성됩니다.
  • 이 유형에는 변경 가능한 상태의 안정성을 보장하는 코드가 있는데, 이는 @MainActor 표시된 클래스나 특정 스레드나 큐에서 속성에 다ㅐ한 액세스를 직렬화하는 클래스와 같습니다.

일부 유형은 항상 보낼 수 있습니다.

예를 들어, 보낼 수 있는 속성만 있는 구조체와 보낼 수 있는 연관된 값만 있는 열거형이 있습니다.

struct TemperatureReading: Sendable {
    var measurement: Int
}


extension TemperatureLogger {
    func addReading(from reading: TemperatureReading) {
        measurements.append(reading.measurement)
    }
}


let logger = TemperatureLogger(label: "Tea kettle", measurement: 85)
let reading = TemperatureReading(measurement: 45)
await logger.addReading(from: reading)

TemperatureReading은 sendable 속성만 있는 구조이고, 구조가 public 또는 @usableFromInline으로 표시되지 않은 경우 암묵적으로 sendable입니다. 다음은 Sendable protocol 준수가 암묵적으로 포함된 구조 버전입니다.

struct TemperatureReading {
    var measurement: Int
}

Sendable protocol에 대한 암묵적 적합성을 재정의하여 유형을 보낼 수 없음으로 명시적으로 표시하려면 extension을 사용합니다.

struct FileDescriptor {
    let rawValue: CInt
}


@available(*, unavailable)
extension FileDescriptor: Sendable { }

위 코드는 POSIX 파일 디스크립터에 대한 래퍼의 일부를 보여줍니다. 파일 디스크립터에 대한 인터페이스는 정수를 사용하여 열린 파일을 식별하고 상호 작용하고 정수 값을 보낼 수 있지만, 파일 디스크립터는 동시성 도메인을 통해 보내는 것이 안전하지 않습니다.

 

위의 코드에서 FileDescriptor는 암묵적으로 보낼 수 있는 기준을 충족하는 구조입니다. 그러나 확장은 해당 적합성을 사용할 수 없게 만들어서 해당 유형이 보낼 수 없게 합니다.

 

 

 

 

'Swift > 문법' 카테고리의 다른 글

후행 클로저  (0) 2025.03.21
enum의 다른 사용방법  (0) 2025.03.14
Swift Protocols  (0) 2025.03.11
Swift 구조체와 클래스  (0) 2025.02.12
Swift 프로토콜 번역(추가 번역중) (Swift.org)  (1) 2024.02.11

(내가 보려고 만든 번역)

(오역이 있다면 제보 부탁합니다)

 

링크: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols

 

Documentation

 

docs.swift.org

 

 

Protocols

-> 적합한 유형을 구현해야하는 요구사항을 정의한다.

 

- 프로토콜은 특정 작업이나 기능에 적합한 메소드와 프로퍼티들,그리고 다른 요구사항들의 청사진을 정의한다.

- 프로토콜은 해당 요구사항의 실제 구현을 제공하기 위해 클래스, 구조체, 열거형에 채택할 수 있다.

- 프로토콜의 요구사항을 충족하는 모든 유형은 해당 프로토콜을 준수한다고 한다.

 

구현해야하는 요구사항을 지정하는것 외에도 프로토콜을 확장할 수 있다.

- 요구사항의 일부를 구현

- 추가기능 구현

Protocol Syntax

클래스, 구조체, 열거형에 모두 유사한 방식으로 프로토콜을 정의

protocol SomeProtocol {
    // protocol definition goes here
}

 

이름 뒤에 콜론으로 구분하여 프로토콜을 배치할 수 있고, 채택한다고 명시한다.

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure definition goes here
}

 

클래스가 슈퍼클래스가 있는 경우, 채택하는 프로토콜 앞에 슈퍼클래스를 나열하고 그 뒤로 쉼표로 구별하여 추가한다.

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}

 

- 프로토콜은 타입이기때문에 이름을 대문자로 시작한다.

 

Property Requirements

- 프로토콜은 인스턴스 프로퍼티나 특정 이름과 타입의 프로퍼티를 제공하기위해 모든 알맞은 타입을 요구할 수 있다.

- 프로토콜은 프로퍼티가 저장 프로퍼티인지, 계산 프로퍼티인지 지정하지 않고, 필수 프로퍼티 이름과 타입만 지정한다.

- 또한 각 프로퍼티가 gettable이어야하는지 gettable 및 settable이어야 하는지 지정한다.

 

- 프로토콜이 gettable 및 settable 프로퍼티을 요구하는 경우, 해당 속성은 상수 저장 프로퍼티나, 읽기 전용 계산된 프로퍼티로 충족될 수 없다.

- 프로토콜이 gettable 프로퍼티만 요구하는 경우, 모든 종류의 프로퍼티가 가능하며, 자신의 코드에 맞게 프로퍼티도 설정이 가능하다?

 

- 프로퍼티는 항상 키워드 var 접두사가 붙은 변수 프로퍼티으로 선언된다.

-  gettable 및 settable은 { get set }을, gettable 프로퍼티는 { get }을 유형 선언뒤에 표시한다.

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

 

- 프로토콜 내에서 static 키워드는 항상 접두사 키워드 앞에 붙인다.

- 이 규칙은 클래스에서 구현될때도 타입 프로퍼티 앞에 class or static 키워드가 붙는 경우에도 적용된다.

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}

 

단일 인스턴스 프로퍼티가 있는 프로토콜의 예시

protocol FullyNamed {
    var fullName: String { get }
}

 

- FullyNamed 프로토콜은 완전한 이름을 제공하기 위해 적합한 타입이 필요하다.

- 프로토콜은 적합한 타입의 특성에 대해 다른 어떤 것도 지정하지 않는다.

- 단지 타입이 자체적으로 전체 이름을 제공할 수 있어야 한다는 것만 지정한다.

- 프로토콜은 모든 타입에 fullname 이라는 string 형식의 gettable 인스턴스 프로퍼티가 있어야 한다고 명시되어있다.

 

프로토콜을 채택하고 사용하는 간단한 구조체의 예시

struct Person: FullyNamed {
    var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"

 

- 여기 예시는 특정한 사람의 이름을 나타내는 Person이라는 구조체가 정의되었다.

- 첫번째줄에 FullyNamed 프로토콜을 채택한다고 명시되어있다.

- Person의 각 인스턴스는 fullName이라는 String 타입의 단일 저장 프로퍼티가 있다.

- 이는 프로토콜의 단일 요구사항과 일치하며, 프로토콜의 요구사항을 준수하고있음을 의미한다.( 준수하고있지 않다면 컴파일 타임에 에러 보고)

 

좀더 복잡한 예시

class Starship: FullyNamed {
    var prefix: String?
    var name: String
    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    var fullName: String {
        return (prefix != nil ? prefix! + " " : "") + name
    }
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"

 

- 이 클래스는 fullName 프로퍼티를 Starship에 대한 계산된 읽기 전용 프로퍼티로 프로토콜의 요구사항을 구현했다.

- 각 클래스 인스턴스는 필수인 name과 옵셔널 prefix를 저장한다.

- fullName 프로퍼티는 prefix 값이 존재하면 사용하고, name 앞에 추가한다.

 

Method Requirements

- 프로토콜은 특정 인스턴스 메소드와 타입을 준수하여 구현되는 타입 메소드를 요구할 수 있다.

- 이러한 메소드는 일반 인스턴스 및 타입 메소드와 정확히 동일한 방식으로 프로토콜 정의의 일부로 작성되지만, 중괄호나 메소드 본문은 없다.

- 일반 방법과 동일한 규칙에 따라 가변 매개변수가 허용된다.

- 그러나 기본값은 프로토콜 정의 안에서 메소드 파라미터로 지정될 수 없다.

 

- 타입 프로퍼티와 마찬가지로 메소드 요구사항에도 접두사 static이 붙을 수 있다.

protocol SomeProtocol {
    static func someTypeMethod()
}

- 다음 예제는 단일 인스턴스 메소드 요구사항이 있는 프로토콜을 정의

protocol RandomNumberGenerator {
    func random() -> Double
}

 

- RandomNumberGenerator 프로토콜은 호출될때마다 Double 값을 리턴하는 Random 인스턴스 메소드를 요구한다.

- 프로토콜의 일부로 지정되지 않았지만, 이 값은 최소 0.0 에서 최대 1.0 사이의 숫자로 지정된다.

- RandomNumberGenerator 프로토콜은 각 난수가 어떻게 생성되는지 지정하지 않았다.

- 단지 Random 메소드가 채택된 다른 곳에서 새로운 난수를 생성하는 표준 방법을 제공하기만 하면 된다.

 

RandomNumberGenerator 프로토콜을 채택한 클래스

- 이 클래스는 선형 합동 생성기로 일려진 난수 생성기를 구현한다.

class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    func random() -> Double {
        lastRandom = ((lastRandom * a + c)
            .truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"

 

Mutating Method Requirements

- 메소드가 자신이 속한 인스턴스를 수정해야하는 경우가 있다.

- 값 유형(구조체, 열거형)의 경우 mutating 키워드를 func 키워드 앞에 배치하여 해당 메소드가 속한 인스턴스의 모든 프로퍼티가 수정가능함을 알린다.

 

- 프로토콜을 채택하는 모든 타입의 인스턴스를 수정하려면, 프로토콜 정의 내에서 일부 메소드를 mutating 키워드를 붙여야한다.

- 이를 통해 구조체와 열거형이 프로토콜을 채택하고 해당 메소드의 요구사항을 충족할 수 있다.

- 클래스에 대한 해당 메소드 구현을 작성할때 mutating 키워드를 작성할 필요가 없다. 구조체와 열거형에서만 사용된다.

 

아래 예시는 toggle 메소드가 정의된 Togglable 프로토콜을 정의한다.

- 이름에서 알다시피 toggle 메소드는 해당 프로퍼티를 수정하여 적합한 상태로 전환하거나 반전시키는 것이다.

- toggle 메소드는 Togglable 프로토콜 정의부분에서 mutating 키워드를 붙였다.

- 메소드가 호출될때 적합한 인스턴스의 상태를 변경할 것으로 예상된다.

protocol Togglable {
    mutating func toggle()
}

 

- 구조체나 열거형에 Togglable 프로토콜을 구현하는 경우, 해당 구조체나 열거형은 mutating으로 표시된 toggle() 메소드를 구현하여 프로토콜을 준수할 수 있다.

 

아래 예시는 OnOffSwitch 열거형을 정의한다.

- 이 열거형은 두가지 상태 사이를 전환한다. ( case On , case Off )

- 이 열거형의 toggle은 Togglable 프로토콜과 일치하도록 mutating을 표시하여 구현하도록 한다.

enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch is now equal to .on

 

Initializer Requirements

'Swift > 문법' 카테고리의 다른 글

후행 클로저  (0) 2025.03.21
enum의 다른 사용방법  (0) 2025.03.14
Swift Protocols  (0) 2025.03.11
Swift 구조체와 클래스  (0) 2025.02.12
Swift Concurrency 정리  (7) 2024.11.10

+ Recent posts