타입 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

숫자 야구게임을 만들어 보며.. 늘 같은 구조로 설계하는 느낌이 들어서 새로운 방법을 시도해보기 위해 최근에 공부했던 protocol을 적극 사용해서 Protocol Oriented Programming 방식으로 리팩토링 해보려고 합니다.

 

리팩토링하기 전엔 각 기능들을 메서드로 구현하여 클래스 안에 담았습니다.

코드는 아래와 같습니다!

더보기
class NumberBaseballGame {
    
    private var answer: [Int] = []
    private var records: [(game: Int, tryCount: Int)] = []
    private var currentGameIndex = 0
    private var currentState: GameState = .none {
        didSet {
            switch currentState {
            case .play:
                playBaseball()
            case .record:
                openRecord()
            case .exit:
                exitGame()
            default:
                print("올바른 숫자를 입력해주세요!")
            }
        }
    }
   
    enum GameState: Int {
        case play = 1
        case record = 2
        case exit = 3
        case none
    }
    
    init() { }
    
    func play() {
        while currentState != .exit {
            selectMode()
        }
    }
    
    private func selectMode() {
        print("환영합니다! 원하시는 번호를 입력해주세요\n1. 게임 시작하기  2. 게임 기록 보기  3. 종료하기")
        guard let input = readLine(),
        let inputInteger = Int(input),
        let state = GameState(rawValue: inputInteger) else { return }
        currentState = state
    }
    
    private func exitGame() {
        records.removeAll()
        currentGameIndex = 0
    }
    
    private func openRecord() {
        records.forEach { (game, count) in
            print("\(game)번째 게임 : 시도 횟수 - \(count)\n")
        }
    }
    
    private func playBaseball() {
        var currentTryCount = 0
        var isFindAnswer = false
        currentGameIndex += 1
        initAnswer()
        print("<게임을 시작합니다.>")
        while !isFindAnswer {
            print("숫자를 입력하세요")
            guard let input = readLine() else { continue }
            if !isCorrectInput(input: input) {
                print("올바르지 않은 입력값입니다.\n")
                continue
            }
            
            currentTryCount += 1
            let inputIntegerArray = input.map{Int(String($0))!}
            let result = compareAnswer(with: inputIntegerArray)
            
            if result.equalPoint == 3 {
                print("정답입니다!")
                isFindAnswer = true
                records.append((game: currentGameIndex, tryCount: currentTryCount))
            } else if result.equalPoint == 0 && result.containsPoint == 0 {
                print("Nothing")
            } else {
                print("\(result.equalPoint) Strike  \(result.containsPoint) Ball")
            }
        }
    }
    
    private func isCorrectInput(input: String) -> Bool {
        if input.prefix(1) == "0" { return false }
        if Int(input) == nil || Set(input.map{$0}).count != 3 {
            return false
        }
        
        return true
    }
    
    private func initAnswer() {
        var answerArray = [Int]()
        
        func randomNumber(range: Range<Int>) -> Int {
            return Int.random(in: range)
        }
        
        while answerArray.count < 3 {
            let num = randomNumber(range: 1..<10)
            if !answerArray.contains(num) { answerArray.append(num)}
        }
        
        answer = answerArray
    }
    
    private func compareAnswer(with numArray: [Int]) -> (equalPoint: Int, containsPoint: Int) {
        var equalPoint = 0
        var containsPoint = 0
        
        for i in 0..<numArray.count {
            if answer[i] == numArray[i] {
                equalPoint += 1
            } else if answer.contains(numArray[i]) {
                containsPoint += 1
            }
        }
        return (equalPoint: equalPoint, containsPoint: containsPoint)
    }
    
}

 

기획

프로토콜을 사용하기 전에 재사용성, 확장 가능성을 고려하기 위해 공통된 목적의 프로퍼티, 메서드를 묶기로 했습니다.

그 전에 숫자 야구게임이 어떻게 진행되는지 먼저 살펴보았습니다.

 

야구게임 흐름

 - readLine() 메서드를 통해 입력받는다.

 - 1: 게임실행, 2: 기록보기, 3: 게임종료 로 선택할 수 있고, 1,2,3을 제외한 다른 입력값은 즉시 종료한다.

 - 게임실행을 선택한 경우 정답을 맞추기위해 3자리 문자열을 입력받는다. 이때 문자열은 모두 중복없는 숫자로 이루어져야 하고, 0으로 시작해선 안된다.

 - 정답을 맞출 때 까지 입력받고 정답인 경우 기록에 저장후 처음으로 돌아온다.

 - 기록보기를 선택한 경우 이전까지의 기록들을 모두 출력하고 처음으로 돌아온다.

 - 게임종료를 선택한 경우 기록을 모두 삭제하고 게임이 종료된다.

 

여기서 저는 아래와 같이 기능적 공통분모를 설정하였습니다.

 

이를 코드로 구현하면 다음과 같습니다.

// 게임모드 (시작, 기록보기, 종료)
protocol GameContentable: AnyObject {
    func gameStart(currentGameIndex: Int) -> (game: Int, tryCount: Int)
    func openRecord(records: [(game: Int, tryCount: Int)])
    func gameExit()
}

// 게임의 전반적인 로직 (정답생성, 입력값 확인, 숫자 비교)
protocol GameLogicProtocol: AnyObject {
    func initAnswerIntegerArray() -> [Int]
    func isCorrectInput(in input: String) -> Bool
    func compare(input: [Int], withAnswer answer: [Int]) -> (equalPoint: Int, containsPoint: Int)
}

// GameManager
final class BaseBallGameManager {
    
    // 현재 게임순서
    private var currentGameIndex = 1
    // 게임모드 Protocol (게임 시작, 기록보기, 종료)
    weak var gameContent: GameContentable?
    // 기록들
    private var records: [(game: Int, tryCount: Int)] = []
    
    enum Gamemode: String {
        case start = "1"
        case record = "2"
        case exit = "3"
    }
    
    // 게임 실행
    func start() {
        while true {
            print("환영합니다! 원하시는 번호를 입력해주세요\n1. 게임 시작하기  2. 게임 기록 보기  3. 종료하기")
            let input = readLine()!
            
            if input == Gamemode.start.rawValue {
                guard let result = gameContent?.gameStart(currentGameIndex: currentGameIndex) else { return }
                currentGameIndex += 1
                records.append(result)
            } else if input == Gamemode.record.rawValue {
                gameContent?.openRecord(records: records)
            } else if input == Gamemode.exit.rawValue {
                gameContent?.gameExit()
                break
            } else {
                print("올바르지 않은 입력값입니다.")
            }
        }
    }
}

// 게임 컨텐츠 클래스
final class GameContent: GameContentable {
    
    // 게임 로직 Protocol
    weak var delegate: GameLogicProtocol?
    
    // 기록 보기
    func openRecord(records: [(game: Int, tryCount: Int)]) {
        if records.isEmpty { print("게임 기록이 없습니다.")}
        records.forEach { (game, count) in
            print("\(game)번째 게임 : 시도 횟수 - \(count)\n")
        }
    }
    
    // 게임 실행
    func gameStart(currentGameIndex: Int) -> (game: Int, tryCount: Int) {
        var currentTryCount = 0
        guard let answer = delegate?.initAnswerIntegerArray() else { return (game: 0, tryCount: 0) }
        
        while true {
            print("숫자를 입력하세요")
            guard let input = readLine() else { continue }
            
            guard let isCorrect = delegate?.isCorrectInput(in: input),
                  isCorrect
            else {
                print("올바르지 않은 입력값입니다.\n")
                continue
            }
            currentTryCount += 1
            let inputIntegerArray = input.map{Int(String($0))!}
            guard let result = delegate?.compare(input: inputIntegerArray, withAnswer: answer) else { break }
            
            if result.equalPoint == 3 {
                print("정답입니다!")
                return (game: currentGameIndex, tryCount: currentTryCount)
            } else if result.equalPoint == 0 && result.containsPoint == 0 {
                print("Nothing")
            } else {
                print("\(result.equalPoint) Strike  \(result.containsPoint) Ball")
            }
        }
        return (game: 0, tryCount: 0)
    }
    
    // 게임 종료
    func gameExit() {
        print("게임 종료")
    }
}

// 게임 로직
final class GameLogic: GameLogicProtocol {
    
    // 정답 생성
    func initAnswerIntegerArray() -> [Int] {
        var answerArray = [Int]()
        func randomNumber(range: Range<Int>) -> Int {
            return Int.random(in: range)
        }
        while answerArray.count < 3 {
            let num = randomNumber(range: 1..<10)
            if !answerArray.contains(num) { answerArray.append(num)}
        }
        return answerArray
    }
    
    // 입력값 검사
    func isCorrectInput(in input: String) -> Bool {
        if input.prefix(1) == "0" { return false }
        if Int(input) == nil || Set(input.map{$0}).count != 3 {
            return false
        }
        return true
    }
    
    // 숫자 비교
    func compare(input: [Int], withAnswer answer: [Int]) -> (equalPoint: Int, containsPoint: Int) {
        var equalPoint = 0
        var containsPoint = 0
        
        for i in 0..<input.count {
            if answer[i] == input[i] {
                equalPoint += 1
            } else if answer.contains(input[i]) {
                containsPoint += 1
            }
        }
        return (equalPoint: equalPoint, containsPoint: containsPoint)
    }
    
}

 

 

위 코드를 작성하고 나서....

프로토콜을 사용하여 기능적 공통분모를 묶어 사용했지만, POP의 장점을 쉽게 찾을 수 없었습니다.

POP의 장점중 하나인 확장성과 유연성을 이 코드에서 찾을 수 있을까? 생각을 하게되었습니다.

만약 1, 2, 3번 입력받고 게임을 실행하고, 게임로직과 게임 컨텐츠를 새로운 게임의 로직과 컨텐츠로 구현하고 매니저에 주입하면 아예 새로운 게임을 할 수 있을까? 생각을 했지만, 대답은 No였습니다.

 

protocol에는 확장 가능성을 열어두기 위해 어느정도의 추상화가 필요하지만 위 코드는 BaseballGame에 구체화 되어있습니다.

위 코드의 프로토콜을 예시로 농구게임에 사용하기 위해 채택한다면, 이미 BaseballGame에 구체화 되었기 때문에 코드구현은 어려울 것으로 보입니다.

결과적으로 위의 코드는 POP형식을 따른게 아닌 프로토콜 기능을 사용하기만 한 숫자야구게임이 되버렸습니다.

 

추상화는 기획단계에서 여러 경우의수에 따른 확장가능성을 열어두고 추상화를 하지만, 숫자야구게임 구현에 목적을 두고 코딩을 하다보니 구체화 했던것 같습니다.

 

 

그러면 확장가능성을 염려해두고 추상화를 해보자

// 게임 컨텐츠
protocol GameStartable {
    func gameStart(currentGameIndex: Int) -> (game: Int, tryCount: Int)
}

protocol RecordOpenable {
    func openRecord(records: [(game: Int, tryCount: Int)])
}

protocol GameExitable {
    func gameExit()
}

// 숫자 야구게임의 게임컨텐츠
protocol BaseBallGameContentable: GameStartable, RecordOpenable, GameExitable { }

// 게임 로직 프로토콜
protocol InitRandomAnswerArrayProtocol {
    func initAnswerIntegerArray() -> [Int]
}

protocol CompareArrayProtocol {
    func compare(input: [Int], withAnswer answer: [Int]) -> (equalPoint: Int, containsPoint: Int)
}

protocol CorrectInputProtocol {
    func isCorrectInput(in input: String) -> Bool
}

// 숫자 야구게임 로직 프로토콜
protocol BaseBallGameLogicProtocol: InitRandomAnswerArrayProtocol, CorrectInputProtocol, CompareArrayProtocol { }

 

프로토콜의 기능중 다중 채택기능을 사용하여 확장 가능성을 열어두었습니다.

만약 농구게임을 개발할 때, 숫자야구게임과 로직이 같다면 똑같이 채택해도 되지만, 몇개의 기능만 같다면 해당 프로토콜을 다중 채택하여 새로운 프로토콜을 만들도록 설계하였습니다.

여기서 추가로 protocol의 요구사항에 있는 메서드가 제네릭 형식으로 주어지면 더 많은 타입과 리턴값에 대한 확장 가능성을 열어둘 수 있을 것으로 느꼈습니다.

 

'Swift' 카테고리의 다른 글

비동기 프로그래밍 DispatchQueue  (0) 2025.02.18
순환 참조  (0) 2025.02.17
Swift 자료구조  (0) 2025.02.13

프로토콜(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

비동기 프로그래밍은 개발에 있어서 중요한 부분입니다.

하나의 앱은 한개의 프로세스가 순차적으로 이루어지는 것이 아닌 다양한 작업이 동시다발적으로 이루어지기 때문에 이러한 작업을 효율적으로 나누고 한번에 처리하도록 설계해야합니다.

설계하는데 있어서 중요한 비동기 프로그래밍에 대해서 적어보고자 합니다.

 

동기 / 비동기

동기(Synchronous): 작업이 완료될 때까지 대기한 후, 작업이 완료되면 다음 작업을 실행

비동기(Asynchronous): 작업 완료를 기다리지 않고 바로 다음 작업을 실행

 

 

 

직렬(serial) / 병렬(concurrent)

직렬(Serial): 순차적으로 진행

병렬(Concurrent): 동시에 진행

 

이름에서 의미를 찾아 볼 수 있지만, 직렬 병렬을 나누는 이유는 작업이 들어가는 큐에는 직렬 수행, 병렬 수행을 지정할 수 있기 떄문입니다.

 

Main Queue(Serial): Main thread로 serial queue를 사용합니다.

Global Queue(Concurrent): 그 외 thread로 concurrent queue를 사용합니다.

Custom Queue(Serial & Concurrent): 직렬, 병렬 수행을 직접 지정한 queue를 사용합니다.

 

 

DispatchQueue

iOS에서 비동기 프로그래밍을 설계할 수 있도록 지원하는 API입니다.

별도로 설치없이 사용할 수 있고 구문은 다음과 같습니다.

 

// Main Thread에서 비동기적 실행
DispatchQueue.main.async {
    
}
// Sub Thread에서 동기적 실행
DispatchQueue.global().sync {
    
}
// Sub Thread에서 비동기적 실행
DispatchQueue.global().async {
    
}

'Swift' 카테고리의 다른 글

숫자 야구게임을 프로토콜지향 프로그래밍으로 리팩토링 해보기  (0) 2025.03.12
순환 참조  (0) 2025.02.17
Swift 자료구조  (0) 2025.02.13

class 와 같은 인스턴스는 참조 형식 인스턴스 입니다.

두 개의 참조 형식 인스턴스가 서로 강한 참조(Strong reference)를 한다면 순환 참조가 발생합니다.

이는 두 인스턴스가 메모리에서 해제되어도 서로에 대한 참조가 해제되지 않기 때문에 메모리에 남게되고 심해지면 메모리 릭 현상이 발생합니다.

이러한 순환참조를 해결하기 위해 참조할 때 weak, unowned 키워드를 사용합니다.

 

먼저 순환참조가 발생하는 예시입니다.

// Person Class
class Person {
    var name: String
    var pet: Pet?
    
    init(name: String) {
        self.name = name
        print("Person init")
    }
    deinit {
        print("Person deinit")
    }
}
// Pet Class
class Pet {
    var owner: Person?
    
    init() {
        print("Pet init")
    }
    
    deinit {
        print("Pet deinit")
    }
}

// 두 개의 인스턴스 생성
var person: Person?
var pet: Pet?
person = Person(name: "cheolsu")
pet = Pet()

// 각 인스턴스의 속성을 다른 인스턴스가 강한 참조하도록 설정
person?.pet = pet
pet?.owner = person

// 메모리에서 해제한다.
person = nil
pet = nil

// 출력 결과
Person init
Pet init
-> Person 클래스와 Pet 클래스의 메모리 해제가 이루어지지 않음

person, pet 인스턴스를 모두 메모리에서 해제하여도 deinit 이 되지 않았습니다.

 

결국 서로의 참조 상태는 메모리에 남아있습니다.

 

약한 참조 weak

선언하는 프로퍼티 앞에 weak 키워드를 붙여 약한 참조하도록 명시적으로 설정하는 방법입니다.

class Pet {
    weak var owner: Person?
    
    init() {
        print("Pet init")
    }
    
    deinit {
        print("Pet deinit")
    }
}

weak 키워드가 붙여진 프로퍼티는 다음과 같은 특징을 갖고 있습니다.

- 참조하는 인스턴스의 RC(Reference Count)를 증가시키지 않습니다.

- 참조하는 인스턴스가 메모리에서 해제되면 nil이 할당되어 메모리에서 해제시킵니다.

- 런타임중 nil이 할당되는 경우가 존재할 수 있으므로 옵셔널 형식입니다.

 

 

unowned

두번째로는 unowned 키워드를 선언하는 프로퍼티 앞에 붙이는 방법이 있습니다.

class Pet {
    unowned var owner: Person
    
    init() {
        print("Pet init")
    }
    
    deinit {
        print("Pet deinit")
    }
}

unowned 키워드가 붙여진 프로퍼티는 다음과 같은 특징을 갖고 있습니다.

- 참조하는 인스턴스의 RC(Reference Count)를 증가시키지 않습니다.

- 참조하는 인스턴스가 메모리에서 해제되어도 여전히 Heap영역의 인스턴스를 가리키는 포인터가 됩니다.

따라서 unowned은 옵셔널 형식이 아니게 됩니다.

 

 

weak와 unowned의 차이점은 참조하는 인스턴스가 메모리에서 해제될 때 나타납니다.

weak은 nil이 되지만, unowned은 여전히 포인터로 남습니다.

따라서 참조되는 인스턴스가 메모리에 할당되는 기간이 참조하는 인스턴스보다 짧다면 weak을 사용하는 편이 좋고,

참조되는 인스턴스가 메모리에 할당되는 기간이 참조하는 인스턴스보다 같거나 길다면, 또는 필연적인 관계라면 unowned를 사용하는게 좋습니다.

 

 

+ Recent posts