본문 바로가기
문법관련/Swift

초기화 구문 (꼼꼼한 재은 씨의 Swift : 문법편)

by print_soo 2022. 4. 9.

구조체나 클래스는 모두 정의된 내용을 그대로 사용할 수는 없다. 항상 인스턴스를 생성해서 메모리 공간을 할당받은 다음에 사용해야한다. 

이 과정을 바로 초기화라고 한다. 

 

초기화 과정에서 가장 중요한 것은 저장 프로퍼티이다. 모든 저장 프로퍼티는 인스턴스 생성과정에서 초기화되어야하며 이를 위해서 반드시 초기값이 지정되어 있어야한다. 모든 저장 프로퍼티에 초기값이 지정되어 있다면 기본 초기화 구문을 사용하여 인스턴스를 생성할 수 있다. 

 

<기본 초기화 구문>

Resolution()
Video()
SUV()
Car()
Vehicle()

 

하지만 우리는 초기화를 배울때 위의 기본 초기화 구문말고 멤버와이즈 초기화 구문이라는 것을 배웠다. 이것은 구조체에서만 사용할 수 있는데 아래의 예제를 살펴보면서 다시 한번 이해해보자.

 

struct Resolution {
    var width = 0
    var height = 0

    func desc() -> String {
        return "Resolution 구조체"
    }

}

let rel = Resolution(width: 1000, height: 1080) //멤버 와이즈 초기화 구문

 

위와 같이 구조체 내부에 선언된 모든 저장 프로퍼티를 일괄적으로 외부의 값으로 초기화할 수 있는 구문을 바로 멤버와이즈 초기화 구문이라고 한다. 

 

그렇다면 일괄적으로 초기화 하는 구문말고 부분적으로 초기화할 수는 없을까? 아래와 같이 말이다.

 

let rel = Resolution(width: 1000)

 

위의 초기화 구문은 구조체에서 제공되지 않는다. 따라서 우리가 직접 초기화 구문을 정의해서 사용해야한다.

 

또 의문이 생기는게 구조체와 달리 클래스에서는 맴버와이즈 초기화 구문을 제공하지 않는데 멤버와이즈 초기화 구문을 사용하려면 어떻게 해야할까?

 

이에 대한 대답또한 우리가 직접 초기화 구문을 정의해서 사용해야한다. 

 

이처럼 기본 구문 이외의 형식으로 원하는 인자값을 전달하여 저장 프로퍼티를 초기화하려면 반드시 구조체나 클래스 내부에 그에 맞는 형태와 할 일을 미리 정의해 두어야한다. 이때 사용되는 것이 초기화 메소드이다.  초기화 메소드는 다른 말로 생성자(Constructor)라고한다.

 

[init 초기화 메소드]

<초기화 메소드의 형식>

init(<매개변수> : <타입>, <매개변수> : <타입>, ...) {
	1. 매개변수 초기화
    	2. 인스턴스 생성 시 기타 처리할 내용
}

 

Swift에서 초기화 메소드는 다소 특수한 메소드이기 때문에 다음과 같은 몇가지 특성을 가진다. 

1. 초기화 메소드의 이름은 init으로 통일
만일 다른 이름이 사용되거나 대소문자가 바꾸면 컴파일러가 초기화 메소드로 인식하지 못한다.

2. 매개변수의 개수, 이름, 타입은 임의로 정의할 수 있다.
단, 메소드에 정의된 매개변수의 순서와 형식에 따라 인스턴스 생성 과정에서 넣어야하는 인자값의 순서와 형식이 결정된다.

3. 매개변수의 이름과 개수, 타입이 서로 다른 여러 개의 초기화 메소드를 정의할 수 있다.
초기화 메소드 또한 메소드의 일종이므로 오버로딩할 수 있다. 오버로딩된 메소드는 스위프트에서 서로 다른 메소드로 간주되기 때문에 이 같은 특성을 이용하면 다양한 형식을 갖는 초기화 메소드를 정의할 수 있다.

4. 정의된 초기화 메소드는 직접 호출되기도 하지만, 대부분 인스턴스 생성시 간접적으로 호출된다. 
초기화 구문이 여러개 정의되어 있을 경우, 인스턴스 생성 구문과 매개변수 형식이 일치하는 초기화 메소드가 호출된다. 만약, 인스턴스 생성 구문의 형식과 일치하는 초기화 메소드가 정의되지 않았다면, 오류가 발생한다.

 

이제 예제를 통해서 알아보자.

 

struct Resolution {
    var width = 0
    var height = 0

    init(width: Int) { // width만 인자값으로 받는 초기화 메소드
        self.width = width // 인자값으로 받은 width 값을 구조체에 있는 width 프로퍼티에 할당
    }
}

class VideoMode {
    
    var resolution = Resolution(width: 2048)
    
    var interlaced = false
    var frameRate = 0.0
    var name: String?

    init(interlaced: Bool, frameRate: Double) { //interlaced, frameRate를 인자값으로 받는 초기화 메소드
        self.interlaced = interlaced
        self.frameRate = frameRate
    }

}

// 인스턴스 생성

let resolution = Resolution.init(width: 4096) // 원칙 - init
let resolution = Resolution(width: 4096) // 생략

let videoMode = VideoMode.init(interlaced: true, frameRate: 40.0) // 원칙 - init
let videoMode = VideoMode(interlaced: true, frameRate: 40.0) // 생략

 

인스턴스를 생성할 때는 위의 구문과 같이 생략도 가능하다. 

 

이제는 저장 프로퍼티가 다수인 클래스에서 다양한 초기화 메소드가 필요할 때의 예제를 살펴보자.

 

class VideoMode {
    
    var resolution = Resolution(width: 2048)
    
    var interlaced = false
    var frameRate = 0.0
    var name: String?
    
    init(name: String) {
        self.name = name
    }
    
    init(interlaced: Bool) {
        self.interlaced = interlaced
    }

    init(interlaced: Bool, frameRate: Double) {
        self.interlaced = interlaced
        self.frameRate = frameRate
    }
    
    init(interlaced: Bool, frameRate: Double, name: String) {
        self.interlaced = interlaced
        self.frameRate = frameRate
        self.name = name
    }


}

let name = VideoMode(name: "김준수")
let simple = VideoMode(interlaced: true)
let double = VideoMode(interlaced: true, frameRate: 40.0)
let triple = VideoMode(interlaced: true, frameRate: 40.0, name: "김준수")

 

모든 저장 프로퍼티가 초기화되어 있을 때, 구조체와 클래스는 빈 인자값 형식의 초기화 구문을 제공한다. 하지만 init 메소드가 작성되고 나면, 작성된 init 메소드가 어떤 인자값 형식을 갖는가에 상관없이 그 객체의 기본 초기화 구문은 더이상 제공되지 않는다. (아래 참고)

 

class VideoMode {
    
    var resolution = Resolution(width: 2048)
    
    var interlaced = false
    var frameRate = 0.0
    var name: String?

    init(interlaced: Bool, frameRate: Double) { //interlaced, frameRate를 인자값으로 받는 초기화 메소드
        self.interlaced = interlaced
        self.frameRate = frameRate
    }

}

let vM = VideoMode(interlaced: true, frameRate: 40.0) // 초기화 메소드

let vMM = VideoMode() //Error!!!

 

쉽게 설명하면 기본 초기화 구문은 수입이 없는 기초 생활 수급자에게 제공되는 생계 급여이고 기초 수급자에게 개인 수입이 생기면(init 메소드가 생기면) 생계 급여 지급을 중단하는 것과 같다.

 

따라서 기본 초기화 구문을 사용하려면 아래와 같이 기본 초기화 구문을 직접 메소드를 만들어주어야한다.

 

class VideoMode {
    
    var resolution = Resolution(width: 2048)
    
    var interlaced = false
    var frameRate = 0.0
    var name: String?

    init(interlaced: Bool, frameRate: Double) { //interlaced, frameRate를 인자값으로 받는 초기화 메소드
        self.interlaced = interlaced
        self.frameRate = frameRate
    }
    
    init() { //초기화 구문
    }

}

let vM = VideoMode(interlaced: true, frameRate: 40.0) // 초기화 메소드

let vMM = VideoMode()

 

그런데 init 역시 메소드이므로 매개변수에 기본 값을 지정할 수 있다. 기본 값이 지정된 메소드에서는 인자값을 생략할 수 있으며 , 이떄 생략된 인자갑 대신 기본값이 인자값으로 사용된다.

 

이 특성을 이용하여 init 메소드를 정의하면 자동으로 기본 초기화 메소드를 정의할 수 있다. 즉, 기본 초기화 + 커스텀 초기화

 

class VideoMode {

    var name: String?

    init(name: String = "") {
        self.name = name
    }

}

let defaulVM = VideoMode()
let customVM = VideoMode(name: "김준수")

 

init 메소드르 추가하고 나면 더이상 기본 초기화 구문을 제공하지 않는 특성은 구조체의 멤버와이즈 초기화 구문에도 동일하게 적용이 되기 때문에 구조체에 init 메소드를 추가할 때에는 특히 주의해야 한다.

 

[초기화 구문 오버라이딩]

클래스에서는 초기화 구문도 일종의 메소드이므로, 자식 클래스에서 'override' 키워드로 오버라이딩할 수 있다. 

상위 클래스에서 선언된 적 없는 초기화 형식일 때는 override 키워드를 붙이면 안되는데 예외로 기본 초기화구문 init()은 부모 클래스에서 명시적으로 선언된적이 없어도 자식 클래스에서는 반드시 오버라이딩 형식으로 작성해야한다.(아래 참고)

class Base {
    
}

class ExBase: Base {
    override init() {
        
    }
}

메소드와 달리 초기화 구문에서의 오버라이딩은 예기치 않은 오류를 발생시킬 수 있다.

그 이유는 초기화 구문을 오버라이딩하면 더이상 부모 클래스에서 정의한 초기화 구문은 실행되지 않기 때문이다. 

위의 상황을 겪으면 프로퍼티가 초기화 되지 않고 그 상황 자체는 오류를 발생시킨다.

 

이러한 상황를 방지하고자 초기화 구문을 오버라이딩할 경우 부모 클래스에서 정의된 초기화 구문을 내부적으로 호출해야하는데, 오버라이딩된 초기화 구문 내부에 super.init 구문을 작성하면 이 상황을 해결할 수 있다. 

 

class Base {
    var baseValue: Double
    init(inputValue: Double) {
        self.baseValue = inputValue
    }
}

class ExBase: Base {
    override init(inputValue: Double) { // 초기화 구문을 오버라이딩하는 구문에서 부모 클래스의 초기화 구문 직접 실행
        super.init(inputValue: 10.5)
    }
}

 

만약 ExBase 클래스를 상속 받는 자식 클래스가 초기화 구문을 다시 오버라이드 하면 이때도 마찬가지로 super.init(inputValue:)를 호출하여 부모클래스의 초기화 구문을 호출해야한다. 이런식으로 계속되면 연쇄적으로 계속 초기화 구문을 호출하게된다. (아래 사진 참고)

초기화 구문 델리게이션

이처럼 연쇄적으로 오버라이딩된 자식 클래스의 초기화 구문에서 부모클래스의 초기화 구문에 대한 호출이 발생하는 것초기화 구문 델리게이션이라고 한다.

 

기본 초기화 구문을 제외한 나머지 구문을 오버라이딩할 때는 반드시 부모 클래스의 초기화 구문을 호출함으로써 델리게이션 처리를 해주어야한다. (기본 초기화 구문은 상황마다 다름)

 

부모 클래스에 기본 초기화 구문만 정의되어 있거나 기본 초기화 구문이 아예 명시적으로 정의되어 있지 않은 상태에서 자식 클래가 오버라이딩할 때에는 super.init() 구문을 호출해주지 않아도 자동으로 부모 클래스의 초기화 구문이 호출되는데 이때 초기화 구문의 호출은 자식 클래스부터 역순으로 진행된다.

class Base {
    var baseValue: Double
    init() {
        self.baseValue = 0.0
        print("base init")
    }
}

class ExBase: Base {
    override init() {
        print("exbase init")
    }
}

let ex = ExBase()
------------------------------
// 실행 결과

//exbase init
//base init

 

하지만 만약 부모 클래스에서 기본 초기화 구문 외에 다른 초기화 구문이 추가되어 있다면 super.init()으로 직접 기본 초기화 구문을 호출해야한다. 

class Base {
    var baseValue: Double
    init() {
        self.baseValue = 0.0
        print("base init")
    }
    
    init(baseValue: Double) {
        self.baseValue = baseValue
    }
}

class ExBase: Base {
    override init() {
        super.init()
        print("exbase init")
    }
}