[Swift] 객체지향 프로그래밍과 스위프트 - 인스턴스의 생성과 소멸

koi·2022년 10월 26일
0
post-thumbnail

구조체와 클래스를 생성할 때 지금까지 기본 이니셜라이저를 사용해 인스턴스를 생성함

  • 초기화는 클래스나 구조체 또는 열거형의 인스턴스를 사용하기 위한 준비 과정
  • 초기화가 완료된 인스턴스는 사용 후 소멸 시점이 오면 소멸함

인스턴스 생성

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

  • 구현된 이니셜라이저는 새로운 인스턴스를 생성할 수 있는 특별한 메서드
  • 스위프트의 이니셜라이저는 반환 값이 없음
    그저 인스턴스의 첫 사용을 위해 초기화하는 역할
  • 이니셜라이저는 해당 타입의 새로운 인스턴스를 생성하기 위해 호출
  • init 키워드를 사용하여 이니셜라이저 메소드임을 표현
  • 클래스, 구조체, 열거형 등의 구현부 또는 해당 타입의 익스텐션 구현부에 위치
    클래스의 지정 이니셜라이저는 익스텐션에 구현할 수 없음

클래스, 구조체, 열거형의 기본적인 형태의 이니셜라이저

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

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

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

프로퍼티 기본값

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

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

Area 구조체와 이니셜라이저

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

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

Area 구조체는 squareMeter라는 Double 타입의 저장 프로퍼티를 가지고 있다.
init 이니셜 라이저로 인스턴스를 초기화하여 square의 초깃값은 0.0이 된다.
이니셜라이저로 저장 프로퍼티에 초깃값을 설정하는 방식도 있지만, 프로퍼티를 정의할 때 기본값을 할당하는 방식을 사용할 수도 있다.

프로퍼티 기본값 지정

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

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

이니셜라이저 매개변수

  • 함수나 메서드를 정의할 때와 마찬가지로 이니셜라이저도 매개변수를 가가질 수 있음
  • 인스턴스를 초기화하는 과정에 필요한 값을 전달받을 수 있음

이니셜라이저 매개변수

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() // 오류 발생!!
  • 첫 번째 이니셜라이저
    평수를 받아 제곱미터로 환산한 값을 squareMeter 프로퍼티에 할당
  • 두 번째 이니셜라이저
    self 프로퍼티를 사용하여 이니셜라이저의 전달인자 레이블과 구분
  • 세 번째 이니셜라이저
    따로 전달인자 레이블을 사용하지 않음
  • 네 번째 이니셜라이저
    와일드 카드 식별자를 사용하여 전달인자 레이블을 없앰

옵셔널 프로퍼티 타입

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

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"

사람의 이름은 아는데 나이는 민감한 부분이므로 모를 수 있기 때문에 age 프로퍼티를 옵셔널로 선언함
이니셜라이저에서 특별히 초기화하지 않았지만 자동으로 nil이 할당
나중에 나이를 알게 되는 시점에 제대로 된 값을 할당할 수 있음

상수 프로퍼티

name 프로퍼티를 상수가 아닌 변수로 선언해둔다면 Eric이라는 이름을 할당하고 난 후에 전혀 다른 사람으로 변할 수 있다.
이런 상황을 방지하려면 name 프로퍼티를 상수로 선언해야한다.
이때 고려해야할 점은 상수로 선언된 저장 프로퍼티는 인스터스를 초기화하는 과정에서만 값을 할당할 수 있으며, 처음 할당된 이후로는 값을 변경할 수 없다.

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

상수 프로퍼티의 초기화

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

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

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

  • 사용자 정의 이니셜라이저를 정의해주지 않으면 클래스나 구조체는 모든 프로퍼티에 기본값이 지정되어 있다는 전제하에 기본 이니셜라이저를 사용함
  • 기본 이니셜라이저는 프로퍼티 기본값으로 프로퍼티를 초기화해 인스턴스를 생성
  • 즉, 기본 이니셜라이저는 저장 프로퍼티의 기본값이 모두 지정되어 있고, 동시에 사용자 정의 이니셜라이저가 정의되어 있지 않는 상태에서 제공됨
  • 저장 프로퍼티를 선언할 때 기본값을 지정해주지 않으면 이니셜라이저에서 초깃값을 설정해야함
  • 구조체는 사용자 정의 이니셜라이저를 구현하지 않으면 프로퍼티의 이리ㅡㅁ으로 매개변수를 갖는 이니셜라이저인 멤버와이즈 이니셔ㄹ라이저를 기본으로 제공
  • 클래스는 멤버와이즈 이니셜라이저를 지원하지 않음
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)

클래스는 멤버와이즈 이니셜라이저를 지원하지 않으므로 멤버와이즈 이니셜라이즈는 구조체만의 특권

초기화 위임

값 타입인 구조체와 열거형은 코드의 중복을 피하기 위하여 이니셜라이저가 다른 이니셜라이저에게 일부 초기화를 위임을 간단하게 구현할 수 있다.

하지만 클래스는 상속을 지원하는 터라 간단한 초기화도 위임할 수 없음
값 타입에서 이니셜라이저가 다른 이니셜라이저를 호출하려면 self.init를 사용한다

당연히 self.init은 이니셜라이저 안에서만 사용할 수 있는데 self.init을 사용한다는 것 자체가 사용자 정의 이니셜라이저를 정의하고 있다는 뜻이다.

그런데 사용자 정의 이니셜라이저를 정의하면 기본 이니셜라이저와 멤버와이즈 이니셜라이저를 사용할 수 없다고 했다. 따라서 초기화 위임을 하려면 최소 두 개 이상의 이니셜라이저를 정의해야 한다

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

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
  • 두번째 사용자 이니셜라이저는 출생년도와 현재연도를 전달받아 나이를 계산한 후 첫번째 이니셜라이저에 초기화를 위임
  • 이런 식으로 코드를 중복으로 쓰지않고 효율적으로 여러 case의 이니셜라이저를 만들 수 있음

실패 가능한 이니셜라이저

  • 이니셜라이저를 통해 인스턴스를 초기화할 수 없는 여러 가지 예외 상황이 있음
    • 대표적으로 이니셜라이저의 전달인자로 잘못된 값이나 적절치 못한 값이 전달되었을 때 이니셜라이저는 인스턴스 초기화에 실패할 수 있음
  • 이렇게 실패 가능성을 내포한 이니셜라이저를 실패 가능한 이니셜라이저라고 함
  • 실패가능한 이니셜라이저는 클래스, 구조체, 열거형 등에 모두 정의 가능
  • 실패했을 때 nil을 반환하므로 반환 타입이 옵셔널로 지정됨
    • 따라서 init 대신 init? 키워드 사용

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

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

실패 가능한 이니셜라이저

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를 통한 이니셜라이저는 기본적으로 실패가능한 이니셜라이저로 제공된다.

열거형의 실패 가능한 이니셜라이저

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: "고등학생")

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

만약 사용자 정의 연산을 통해 저장 프로퍼티 기본값을 설정하고자 한다면 클로저나 함수를 사용하여 프로퍼티 기본값을 제공할 수 있다.

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

프로퍼티 기본값을 설정해주기 위해서 클로저를 사용한다면 클로저가 실행되는 시점은 초기화할 때 인스턴스의 다른 프로퍼티 값이 설정되기 전이기때문에, 다른 프로퍼티를 사용하여 연산할 수 없다.

다른 프로퍼티에 기본값이 있다고해도 안됨. 또한 클로저 내부에서 self 프로퍼티도 사용할 수 없고, 인스턴스 메서드를 호출할 수도 없음

클로저를 통한 프로퍼티 기본값 설정

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

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

만약 소괄호가 없다면 프로퍼티의 기본값은 클로저 그 자체가 된다.

클로저를 통한 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

인스턴스 소멸

  • 클래스의 인스턴스는 디이셜라이저를 구현할 수 있음
  • 디이셜라이저는 이니셜라이저와 반대 역할을 함
    • 메모리에서 해제되기 직전 클래스 인스턴스와 관련하여 원하는 정리 작업을 구현할 수 있음
  • 클래스의 인스턴스가 메모리에서 소멸되기 바로 직전에 호출
  • deinit 키워드를 사용하여 구현하면 자동으로 호출
  • 클래스의 인스턴스에서만 구현 가능
  • 단 하나만 구현할 수 있음
  • 이니셜라이저와는 달리 매개변수를 갖지 않으며, 소괄호도 적지 않음
  • 소멸 직전에 호출되므로 모든 프로퍼티에 접근할 수 있으며 값을 변경할 수도 있음

디이니셜라이저 구현

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

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

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

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

profile
Don't think, just do 🎸

0개의 댓글