class

BS_Lee·2025년 6월 30일

swift

목록 보기
10/21

Swift의 class는 struct와 다른 점이 참 많다.
오늘은 class의 개념부터 초기화 방식, 접근 제어자, 메모리 관리까지 정리해본다.


class란?

Swift에서 class참조 타입(reference type)이다.
같은 인스턴스를 여러 변수가 공유할 수 있고, 값이 바뀌면 모든 참조가 영향을 받는다.

class Person {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    func sayHello() {
        print("Hello, my name is \(name)")
    }
}

그럼 여기서 궁금한 게 하나 생긴다.
값을 비교하고 싶을 때는 어떻게 해야 할까?
클래스는 참조 타입인데, ==만 써도 되는 걸까?


== vs === (값 비교와 참조 비교)

  • ==는 값을 비교하는 연산자다. Equatable 프로토콜을 따르지 않으면 쓸 수 없다.
  • ===는 클래스 인스턴스가 동일한 객체인지 비교한다. 즉, 주소값 비교한다.
class Dog {
    var name: String
    init(name: String) { self.name = name }
}

let a = Dog(name: "Buddy")
let b = Dog(name: "Buddy")
let c = a

print(a === b) // false
print(a === c) // true

쉽게 말하자면, ==는 값이 같냐를 보는 것이고,
===는 같은 물건이냐를 보는 것이다.


private(set)은 왜 쓸까?

Swift에서는 변수에 private(set)을 붙일 수 있다.
읽을 수는 있지만, 바깥에서는 수정할 수 없다.

class Person {
    private(set) var age: Int

    init(age: Int) {
        self.age = age
    }

    func growOlder() {
        age += 1
    }
}

그럼 왜 이렇게 만들까?
캡슐화 때문이다.
외부에서는 값이 어떻게 바뀌는지 알 필요 없고,
내부에서만 규칙적으로 바뀌게 하고 싶을 때 사용한다.


convenience init이란?

보통 클래스에 여러 종류의 초기화 방법이 필요할 때가 있다.
이럴 때 보조 생성자 역할을 하는 게 convenience init이다.

class Car {
    var manufacturer: String = "Tesla"
    var model: String
    var year: Int

    init(model: String, year: Int) {
        self.model = model
        self.year = year
    }

    convenience init(model: String) {
        self.init(model: model, year: 2025)
    }
    
    func startEngine() {
    	print("start Engine.")
    }
    
    func drive() {
    	print("I'm driving.")
    }
}

근데 여기서 또 질문이 생긴다.
그냥 init 여러 개 만들면 되는 거 아닌가?


init 오버로딩 vs convenience init

물론 Swift도 init 오버로딩이 가능하다.
그치만 중복 코드가 생긴다.

init(model: String) {
    self.model = model
    self.year = 2025
    startEngine()
    drive()
}

init(model: String, year: Int) {
    self.model = model
    self.year = year
    startEngine()
    drive()
    
}
// startEngine과 drive의 함수의 중복코드 발생!

반면 convenience init을 쓰면
중복 없이 기존 init을 재활용할 수 있다.

convenience init(model: String) {
    self.init(model: model, year: 2025)
    startEngine()
    drive()
}

결국, 편리하고 중복 없는 초기화를 위해 convenience init이 존재하는 것이다.


그럼 자식 클래스에서 super.init()은 어떻게 써야 할까?

아래 코드는 에러가 난다.

class TeslaY: Car {
    override init() {
        super.init(model: "Y") // 에러
    }
}

왜냐면 부모의 convenience init
자식에서 super.init로 호출할 수 없다.

해결하려면 반드시 지정 생성자를 호출해야 한다.

override init() {
    super.init(model: "Y", year: 2025) // OK!
}

Swift는 상속 구조에서 초기화를 명확하게 통제한다.
super.init()은 꼭 지정 생성자만 호출할 수 있다.
보조 생성자는 내부에서 self.init()로만 호출 가능하다.


deinit은 언제 호출될까?

deinit은 클래스 인스턴스가 메모리에서 해제될 때 자동으로 호출된다.

class MyClass {
    init() {
        print("Initialized")
    }

    func doSomething() {}

    deinit {
        print("Deinitialized")
    }
}

예를 들어 이런 코드가 있다면?

let myClosure = {
    let obj = MyClass()
    obj.doSomething()
}
myClosure()

// 출력:
// Initialized
// Deinitialized

클로저 블록이 끝나면서 obj의 참조 카운트가 0이 되고,
그 순간 deinit이 호출된다.

이게 바로 ARC (Automatic Reference Counting)의 힘이다.
Swift는 메모리를 자동으로 관리해준다.
단, 순환 참조는 주의해야한다.


마무리 한 줄 요약

개념설명
class참조 타입, 상속/소멸자 지원, ARC 동작
== / ===값 비교 / 참조 비교
private(set)외부에서는 읽기만, 내부에서는 수정 가능
convenience init지정 생성자 위임용 보조 생성자
super.init()지정 생성자만 호출 가능, convenience는 호출 불가
deinit인스턴스가 메모리에서 해제될 때 호출됨 (struct에는 없음)

Swift에서 class를 제대로 이해하려면
참조 타입의 특성과 ARC를 잘 알아야 한다.
그리고 생성자와 소멸자의 규칙도 깔끔하게 정리해두면
실전에서 훨씬 유연하게 대응할 수 있다.

필요하면 다음 포스팅에서
required init, weak/unowned, 순환 참조 방지도 다뤄보자!


0개의 댓글