Swift의 class는 struct와 다른 점이 참 많다.
오늘은 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
쉽게 말하자면, ==는 값이 같냐를 보는 것이고,
===는 같은 물건이냐를 보는 것이다.
Swift에서는 변수에 private(set)을 붙일 수 있다.
읽을 수는 있지만, 바깥에서는 수정할 수 없다.
class Person {
private(set) var age: Int
init(age: Int) {
self.age = age
}
func growOlder() {
age += 1
}
}
그럼 왜 이렇게 만들까?
캡슐화 때문이다.
외부에서는 값이 어떻게 바뀌는지 알 필요 없고,
내부에서만 규칙적으로 바뀌게 하고 싶을 때 사용한다.
보통 클래스에 여러 종류의 초기화 방법이 필요할 때가 있다.
이럴 때 보조 생성자 역할을 하는 게 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 여러 개 만들면 되는 거 아닌가?
물론 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은 클래스 인스턴스가 메모리에서 해제될 때 자동으로 호출된다.
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, 순환 참조 방지도 다뤄보자!