프로토콜의 확장(Protocol Extension)
- Swift의 프로토콜은 확장을 통해 기본 구현을 제공할 수 있다
- 이 방식은 여러 타입에서 동일하게 구현해야 하는 기능을 프로토콜 확장으로 제공하여 코드 중복을 방지하고, 유지보수를 용이하게 만든다
- 또한, Virtual Table과 Witness Table, 구조체의 메모리 저장 위치(Stack 또는 Heap) 에 대한 이해가 중요하다
프로토콜 정의 및 구현의 문제점
- 프로토콜을 정의하고 이를 여러 타입에서 구현할 때, 중복 코드가 발생할 수 있다
프로토콜 정의하기
protocol Device {
func powerOn()
func powerOff()
}
Device 프로토콜은 powerOn()과 powerOff() 메서드의 구현을 요구한다
기존 방식의 구현 (반복적인 구현 문제)
class Laptop: Device {
func powerOn() {
print("노트북 전원 켜기")
}
func powerOff() {
print("노트북 전원 끄기")
}
}
struct Tablet: Device {
func powerOn() {
print("태블릿 전원 켜기")
}
func powerOff() {
print("태블릿 전원 끄기")
}
}
Laptop 클래스와 Tablet 구조체 모두 같은 기능을 구현해야 하는 상황
- 코드가 중복되므로 유지보수가 어려워질 수 있다
프로토콜 확장을 통한 기본 구현 제공
- Swift의 프로토콜 확장(Extension) 을 사용하면 기본 구현을 제공할 수 있다
프로토콜 확장 사용하기
extension Device {
func powerOn() { print("기기 전원 켜기") }
func powerOff() { print("기기 전원 끄기") }
func reset() { print("기기 설정 초기화") }
}
Device 프로토콜의 기본 구현을 제공하여 중복 구현을 방지할 수 있다
reset() 메서드는 요구 사항이 아닌 추가 기능으로 제공된다
프로토콜 확장을 통한 다형성 제공 (프로토콜 지향 프로그래밍)
- 프로토콜 확장을 이용하여 다형성을 구현할 수 있다
클래스에서 프로토콜을 채택하고 구현하기
class Smartphone: Device {
func powerOn() { print("스마트폰 전원 켜기") }
func reset() { print("스마트폰 설정 초기화") }
}
Smartphone 클래스는 powerOn()을 재정의하고 reset()도 재정의한다
- 기본 구현을 제공하지만, 필요시 덮어쓸 수 있다
구조체에서 프로토콜을 채택하고 구현하기
struct SmartWatch: Device {
func powerOn() { print("스마트워치 전원 켜기") }
func reset() { print("스마트워치 설정 초기화") }
}
SmartWatch 구조체는 powerOn()을 재정의하고 reset()도 재정의한다
- 기본 구현과 확장을 모두 사용할 수 있다
Virtual Table과 Witness Table의 차이점
- Swift에서 클래스와 구조체는 프로토콜을 다르게 처리한다
Virtual Table (클래스의 동적 디스패치)
- 클래스는 가상 테이블 (Virtual Table, V-Table) 을 사용한다
- 메서드 호출 시 런타임에 메서드를 동적으로 찾는다
Witness Table (구조체의 정적 디스패치)
- 구조체는 목격자 테이블 (Witness Table, W-Table) 을 사용한다
- 메서드 호출 시 컴파일 타임에 함수 포인터가 결정된다 (정적 디스패치)
프로토콜 확장의 Direct Dispatch 원리
- Swift에서 프로토콜 확장에서 구현된 메서드 (
reset()) 는 Virtual Table 또는 Witness Table 에 포함되지 않는다
- 대신 직접 메서드 주소를 삽입하여 호출하는 방식 (Direct Dispatch) 을 사용한다
코드 예제 (클래스 타입)
let phone: Device = Smartphone()
phone.powerOn()
phone.powerOff()
phone.reset()
코드 예제 (구조체 타입)
var watch: Device = SmartWatch()
watch.powerOn()
watch.powerOff()
watch.reset()
구조체의 메모리 저장 위치 (Stack 또는 Heap)
- Swift에서 구조체의 메모리 저장 위치는 크기와 사용 방식, 그리고 프로토콜 타입으로 선언되었는지 여부에 따라 다르다
구조체의 저장 위치 기준
| 자료형 | 크기 (Byte) | 최대 개수 (스택 저장) | 힙 저장 여부 (개수 초과 시) |
|---|
Int | 8 | 3 | 힙으로 이동 |
Double | 8 | 3 | 힙으로 이동 |
Float | 4 | 6 | 힙으로 이동 |
Bool | 1 | 24 | 힙으로 이동 |
Character | 16 | 1 | 힙으로 이동 |
String | 최소 24 | 항상 힙 | 힙에 저장 |
Array | 최소 24 | 항상 힙 | 힙에 저장 |
Dictionary | 최소 24 | 항상 힙 | 힙에 저장 |
- 작은 구조체 (24바이트 이하): 스택에 저장
- 큰 구조체 (32바이트 이상): 힙에 저장되고, 참조 포인터는 스택에 저장
- 프로토콜 타입으로 선언된 구조체: 무조건 힙에 저장 (타입 소거 발생)
요약
- 프로토콜 확장은 기본 구현을 제공하여 코드 중복을 방지할 수 있다
- Virtual Table (클래스)와 Witness Table (구조체)의 차이를 이해하는 것이 중요하다
- 클래스는 동적 디스패치 방식, 구조체는 정적 디스패치 방식을 사용한다
- 프로토콜 확장에서 제공된 메서드는 Direct Dispatch 방식으로 호출된다 (빠르고 효율적)
- 구조체의 저장 위치는 크기와 프로토콜 사용 여부에 따라 달라진다