본문 바로가기
iOS/SWIFT

[Swift] Chapter 11. 인스턴스 생성 및 소멸

by 원만사 2021. 10. 8.
반응형

 

11.1 인스턴스 생성

초기화 과정은 새로운 인스턴스를 사용할 준비를 하기 위하여 저장 프로퍼티의 초깃값을 설정 하는 등의 일을 한다. 이니셜라이저를 정의하면 초기화 과정을 직접 구현할 수 있다. 스위프트의 이니셜라이저는 반환 값이 없다.

이니셜라이저는 해당 타입의 새로운 인스턴스를 생성하기 위해 호출한다. func 키워드를 사용하지 않고 오로지 init 키워드를 사용하여 이니셜라이저 메서드임을 표현한다. init 메서드는 클래스, 구조체, 열거형 등의 구현부 또는 해당 타입의 익스텐션 구현부에 위치한다. 다만 클래스의 지정 이니셜라이저는 익스텐션에서 구현해줄 수 없다.

 

/*
	코드 11-1. 클래스, 구조체, 열거형의 기본적인 형태의 이니셜라이저
*/

class SomeClass {
    init() {
        // 초기화할 때 필요한 코드
    }
}


struct SomeStruct {
    init() {
        // 초기화할 때 필요한 코드
    }
}

enum SomeEnum {
    case someCase
    
    init() {
        // 열거형은 초기화할 때 반드시 case중 하나가 되어야 한다.
        self = .someCase
        // 초기화할 때 필요한 코드
    }
}

 

11.1.1 프로퍼티 기본값

구조체와 클래스의 인스턴스는 처음 생성할 때 옵셔널 저장 프로퍼티를 제외한 모든 저장 프로퍼티에 적절한 초깃값을 할당해야 한다. 초기화 후에 값이 확정되지 않은 저장 프로퍼티는 존재할 수 없다. 프로퍼티를 정의할 때 프로퍼티 기본값을 할당하면 이니셜라이저에서 따로 초깃값을 할당하지 않더라도 프로퍼티 기본값으로 저장 프로퍼티의 값이 초기화된다.

NOTE_초기화와 프로퍼티 감시자 이니셜라이저를 통해 초깃값을 할당하거나, 프로퍼티 기본값을 통해 처음의 저장 프로퍼티가 초기화될 때는 프로퍼티 감시자 메서드가 호출되지 않는다.

 

/*
	코드 11-2. Area 구조체와 이니셜라이저
*/

struct Area {
    var squareMeter: Double
    
    init() {
        squareMeter = 0.0 // squareMeter의 초깃값 할당
    }
}

let room: Area = Area()
print(room.squareMeter) // 0.0

 

/*
	코드 11-3. 프로퍼티 기본값 지정
*/

struct Area {
    var squareMeter: Double = 0.0 // 프로퍼티 기본값 할당
}

let room: Area = Area()
print(room.squareMeter) // 0.0

 

11.1.2 이니셜라이저 매개변수

인스턴스를 초기화하는 과정에 필요한 값을 전달받을 수 있다.

 

/*
	코드 11-4. 이니셜라이저 매개변수
*/

struct Area {
    var squareMeter: Double
    
    init(fromPy py: Double) { // 첫 번째 이니셜라이저
        squareMeter = py * 3.3058
    }
    
    // 두 번째 이니셜라이저
    init(fromSquareMeter squareMeter: Double) {
        self.squareMeter = squareMeter
    }
    
    // 세 번째 이니셜라이저
    init(value: Double) {
        squareMeter = value
    }
    
    // 네 번째 이니셜라이저
    init(_ value: Double) {
        squareMeter = value
    }
}

let roomeOne: Area = Area(fromPy: 15.0)
print(roomeOne.squareMeter) // 49.587

let roomTwo: Area = Area(fromSquareMeter: 33.06)
print(roomTwo.squareMeter) // 33.06

let roomThree: Area = Area(value: 30.0)
let roomFour: Area = Area(55.0)

//Area() // 오류 발생!

 

위와 같이 사용자 정의 이니셜라이저를 만들면 기존의 기본 이니셜라이저 (init( ))는 별도로 구현하지 않는 이상 사용할 수 없다.

 

11.1.3 옵셔널 프로퍼티 타입

초기화 과정에서 값을 초기화하지 않아도 되는, 즉 인스턴스가 사용되는 동안에 값을 꼭 갖지 않아도 되는 저장 프로퍼티가 있다면 해당 프로퍼티를 옵셔널로 선언할 수 있다. 또는 초기화 과정에서 값을 지정해주기 어려운 경우 저장 프로퍼티를 옵셔널로 선언할 수도 있다.

 

/*
	코드 11-5. Person 클래스
*/

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

let yagom: Person = Person(name: "yagom")
print(yagom.name) // "yagom"
print(yagom.age) // nil

yagom.age = 99
print(yagom.age) // 99

yagom.name = "Eric"
print(yagom.name) // "Eric"

 

11.1.4 상수 프로퍼티

상수로 선언된 저장 프로퍼티는 인스턴스를 초기화하는 과정에서만 값을 할당할 수 있으며, 처음 할당된 이후로는 값을 변경할 수 없다.

 

NOTE_상수 프로퍼티와 상속 클래스 인스턴스의 상수 프로퍼티는 프로퍼티가 정의된 클래스에서만 초기화할 수 있다. 해당 클래스를 상속받은 자식 클래스의 이니셜라이저에서는 부모 클래스의 상수 프로퍼티 값을 초기화할 수 없다.

 

/*
	코드 11-6. 상수 프로퍼티의 초기화
*/

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

let yagom: Person = Person(name: "yagom")
//yagom.name = "Eric" // 오류 발생!!

 

11.1.5 기본 이니셜라이저와 멤버와이즈 이니셜라이저

사용자 정의 이니셜라이저를 정의해주지 않으면 클래스나 구조체는 모든 프로퍼티에 기본값이 지정되어 있다는 전제하에 기본 이니셜라이저를 사용한다. 기본 이니셜라이저는 프로퍼티 기본값으로 프로퍼티를 초기화해서 인스턴스를 생성한다. 즉, 기본 이니셜라이저는 저장 프로퍼티의 기본값이 모두 지정되어 있고, 동시에 사용자 정의 이니셜라이저가 정의되어 있지 않은 상태에서 제공된다.

구조체는 사용자 정의 이니셜라이저를 구현하지 않으면 프로퍼티의 이름으로 매개변수를 갖는 이니셜라이저인 멤버와이즈 이니셜라이저를 기본으로 제공한다. 하지만 클래스는 멤버와이즈 이니셜라이저를 지원하지 않는다.

 

/*
	코드 11-7. Point 구조체와 Size 구조체의 선언과 멤버와이즈 이니셜라이저의 사용
*/

struct Point {
    var x: Double = 0.0
    var y: Double = 0.0
}

struct Size {
    var width: Double = 0.0
    var height: Double = 0.0
}

let point: Point = Point(x: 0, y: 0)
let size: Size = Size(width: 50.0, height: 50.0)

// 구조체에 저장 프로퍼티에 기본값이 있는 경우
// 필요한 매개변수만 사용하여 초기화할 수도 있다.
let somePoint: Point = Point()
let someSize: Size = Size(width: 50)
let anotherPoint: Point = Point(y: 100)

 

11.1.6 초기화 위임

값 타입인 구조체와 열거형은 코드의 중복을 피하기 위하여 이니셜라이저가 다른 이니셜라이저에게 일부 초기화를 위임하는 초기화 위임을 간단하게 구현할 수 있다. 하지만 클래스는 상속을 지원하기 때문에 간단한 초기화 위임도 할 수 없다.

값 타입에서 이니셜라이저가 다른 이니셜라이저를 호출하려면 self.init을 사용한다. 초기화 위임을 하려면 최소 두 개 이상의 사용자 정의 이니셜라이저를 정의해야 한다.

NOTE_기본 이니셜라이저를 지키고 싶다면 사용자 정의 이니셜라이저를 정의할 때도 기본 이니셜라이저나 멤버와이즈 이니셜라이저를 사용하고 싶다면 익스텐션을 사용하여 사용자 정의 이니셜라이저를 구현하면 된다.

 

/*
	코드 11-8. Student 열거형과 초기화 위임
*/

enum Student {
    case elementary, middle, high
    case none
    
    // 사용자 정의 이니셜라이저가 있는 경우, init() 메서드를 구현해주어야
    // 기본 이니셜라이저를 사용할 수 있다.
    init() {
        self = .none
    }
    
    // 첫 번째 사용자 정의 이니셜라이저
    init(koreanAge: Int) {
        switch koreanAge {
        case 8...13:
            self = .elementary
        case 14...16:
            self = .middle
        case 17...19:
            self = .high
        default:
            self = .none
        }
    }
    
    // 두 번째 사용자 정의 이니셜라이저
    init(bornAt: Int, currentYear: Int) {
        self.init(koreanAge: currentYear - bornAt + 1)
    }
}

var younger: Student = Student(koreanAge: 16)
print(younger) // middle

younger = Student(bornAt: 1998, currentYear: 2016)
print(younger) // high

 

11.1.7 실패 가능한 이니셜라이저

이니셜라이저를 정의할 때 실패 가능성을 염두에 두기도 하는데, 이렇게 실패 가능성을 내포한 이니셜라이저를 실 패 가능한 이니셜라이저라고 부른다. 클래스, 구조체, 열거형 등에 모두 정의할 수 있다. 실패 가능한 이니셜라이저는 실패했을 때 nil을 반환해주므로 반환 타입이 옵셔널로 지정된다. 따라서 실패 가능한 이니셜라이저는 init 대신에 init? 키워드를 사용한다.

 

NOTE_이니셜라이저의 매개변수 실패하지 않는 이니셜라이저와 실패 가능한 이니셜라이저를 같은 이름과 같은 매개변수 타입을 갖도록 정의할 수 없다. 실패 가능한 이니셜라이저는 초기화를 실패했을 때는 return nil을, 반대로 초기화에 성공했을 때는 return을 적어 초기화의 성공과 실패를 표현할 뿐, 실제 값을 반환하지는 않는다.

 

실패 가능한 이니셜라이저를 사용하면 잘못된 전달인자를 전달받았을 때 초기화하지 않을 수 있다.

 

/*
	코드 11-9. 실패 가능한 이니셜라이저
*/

class Person {
    let name: String
    var age: Int?
    
    init?(name: String) {
        if name.isEmpty {
            return nil
        }
        self.name = name
    }
    
    init?(name: String, age: Int) {
        if name.isEmpty || age < 0 {
            return nil
        }
        self.name = name
        self.age = age
    }
}

let yagom: Person? = Person(name: "yagom", age: 99)

if let person: Person = yagom {
    print(person.name)
} else {
    print("Person wasn't initialized")
}
// yagom

let chope: Person? = Person(name: "chope", age: -10)

if let person: Person = chope {
    print(person.name)
} else {
    print("Person wasn't initialized")
}
// Person wasn't initialized

실패 가능한 이니셜라이저는 특히 열거형에서 유용하게 사용할 수 있다. 특정 case에 맞지 않는 값이 들어오면 생성에 실패할 수 있다. 혹은 rawValue로 초기화할 때, 잘못된 rawValue가 전달되어 들어온다면 열거형 인스턴스를 생성하지 못할 수 있다. 따라서 rawValue를 통한 이니셜라이저는 기본적으로 실패 가능한 이니셜라이저로 제공된다.

 

/*
	코드 11-10. 열거형의 실패 가능한 이니셜라이저
*/

enum Student: String {
    case elementary = "초등학생", middle = "중학생", high = "고등학생"
    
    init?(koreanAge: Int) {
        switch koreanAge {
        case 8...13:
            self = .elementary
        case 14...16:
            self = .middle
        case 17...19:
            self = .high
        default:
            return nil
        }
    }
    
    init?(bornAt: Int, currentYear: Int) {
        self.init(koreanAge: currentYear - bornAt + 1)
    }
}

var younger: Student? = Student(koreanAge: 20)
print(younger) // nil

younger = Student(bornAt: 2020, currentYear: 2016)
print(younger) // nil

younger = Student(rawValue: "고등학생")
print(younger) // high

 

11.1.8 함수를 사용한 프로퍼티 기본값 설정

만약 사용자 정의 연산을 통해 저장 프로퍼티 기본값을 설정하고자 한다면 클로저나 함수를 사용하여 프로퍼티 기본값을 제공할 수 있다. 인스턴스를 초기화할 때 함수나 클로저가 호출되면서 연산 결괏값을 프로퍼티 기본값으로 제공해준다. 그렇기 때문에 클로저나 함수의 반환 타입은 프로퍼티의 타입과 일치해야 한다.

프로퍼티 기본값을 설정해주기 위해서 클로저를 사용한다면 클로저가 실행되는 시점은 초기화할 때 인스턴스의 다른 프로퍼티 값이 설정되기 전이라는 것을 명심해야 한다. 즉, 클로저 내부에서는 인스턴스의 다른 프로퍼티를 사용하여 연산할 수는 없다는 것이다. 다른 프로퍼티에 기본값이 있다고 해도 안된다. 또한, 클로저 내부에서 self 프로퍼티도 사용할 수 없으며, 인스턴스 메서드를 호출할 수도 없다.

 

/*
	코드 11-11. 클로저를 통한 프로퍼티 기본값 설정
*/

class SomeClass {
    let someProperty: SomeType = {
        // 새로운 인스턴스를 생성하고 사용자 정의 연산을 통한 후 반환해준다.
        // 반환되는 값의 타입은 SomeType과 같은 타입이어야 한다.
        return someValue
    }()
}

클로저 뒤에 소괄호가 붙은 이유는 클로저를 실행하기 위해서이다. 클로저 뒤에 소괄호가 붙어 클로저가 실행한 결괏값은 프로퍼티의 기본값이 된다. 만약 소괄호가 없다면 프로퍼티의 기본값은 클로저 그 자체가 된다.

 

/*
	코드 11-12. 클로저를 통한 student 프로퍼티 기본값 설정
*/

struct Student {
    var name: String?
    var number: Int?
}

class SchoolClass {
    var students: [Student] = {
        // 새로운 인스턴스를 생성하고 사용자 정의 연산을 통한 후 반환해준다.
        // 반환되는 값의 타입은 [Student] 타입이어야 한다.
        var arr: [Student] = [Student]()
        
        for num in 1...15 {
            var student: Student = Student(name: nil, number: num)
            arr.append(student)
        }
        
        return arr
    }()
}

let myClass: SchoolClass = SchoolClass()
print(myClass.students.count) // 15

 

11.2 인스턴스 소멸


디이시녈라이저는 메모리에서 해제되기 직전 클래스 인스턴스와 관련하여 원하는 정리 작업을 구현할 수 있다. 디이니셜라이저는 클래스의 인스턴스가 메모리에서 소멸되기 바로 직전에 호출된다. deinit 키워드를 사용하여 디이니셜라이저를 구현하면 자동으로 호출된다. 디이니셜라이저는 클래스의 인스턴스에만 구현할 수 있다.

스위프트는 인스턴스가 더 이상 필요하지 않으면 자동으로 메모리에서 소멸시킨다. 인스턴스 내부에서 파일을 불러와 열어보는 등의 외부 자원을 사용했다면 인스턴스를 소멸하기 직전에 파일을 다시 저장하고 닫아주는 등의 부가 작업을 해야할때나 인스턴스를 메모리에서 소멸하기 직전에 인스턴스에 저장되어 있던 데이터를 디스크에 파일로 저장해줘야 하는 경우 유용하게 사용할 수 있다.

클래스에는 디이니셜라이저를 단 하나만 구현할 수 있다. 이시녈라이저와는 다르게 매개변수를 갖지 않으며, 소괄호도 적어주지 않는다. 또, 자동으로 호출되기 때문에 별도의 코드로 호출할 수도 없다.

디이니셜라이저는 인스턴스를 소멸하기 직전에 호출되므로 인스턴스의 모든 프로퍼티에 접근할 수 있으며 프로퍼티의 값을 변경할 수도 있다.

 

/*
	코드 11-13. 디이니셜라이저의 구현
*/

class SomeClass {
    deinit {
        print("Instance will be deallocated immediately")
    }
}

var instance: SomeClass? = SomeClass()
instance = nil // Instance will be deallocated immediately

 

/*
	코드 11-14. FileManager 클래스의 디이니셜라이저 활용
*/

class FileManager {
    var fileName: String
    
    init(fileName: String) {
        self.fileName = fileName
    }
    
    func openFile() {
        print("Open File: \(self.fileName)")
    }
    
    func modifyFile() {
        print("Modify File: \(self.fileName)")
    }
    
    func writeFile() {
        print("Write File: \(self.fileName)")
    }
    
    func closeFile() {
        print("Close File: \(self.fileName)")
    }
    
    deinit {
        print("Deinit instance")
        self.writeFile()
        self.closeFile()
    }
}


var fileManager: FileManager? = FileManager(fileName: "abc.txt")

if let manager: FileManager = fileManager {
    manager.openFile() // Open File: abc.txt
    manager.modifyFile() // Modify File: abc.txt
}

fileManager = nil
// Deinit instance
// Write File: abc.txt
// Close File: abc.txt

디이니셜라이저를 잘 활용하면 메모리 관리 측면 외에도 프로그래머가 설계한 로직에 따라 인스턴스가 메모리에서 해제되기 직전에 적절한 작업을 하도록 할 수 있을 것이다.

 

 

스위프트 프로그래밍: Swift 5 - 교보문고

객체지향, 함수형, 프로토콜 지향 패러다임까지 한 번에! | 스위프트를 제대로 이해하고 싶은 개발자를 위한 책스위프트는 iOS와 macOS용 애플리케이션 개발에 주로 사용하는 프로그래밍 언어입니

www.kyobobook.co.kr

 

반응형

'iOS > SWIFT' 카테고리의 다른 글

[Swift] Chapter 13. 클로저  (0) 2021.10.09
[Swift] Chapter 12. 접근제어  (0) 2021.10.08
[Swift] Chapter 10. 프로퍼티와 메서드  (0) 2021.10.08
[Swift] Chapter 09. 구조체와 클래스  (0) 2021.10.08
[Swift] Chapter 08. 옵셔널  (0) 2021.10.08

댓글