Clean Architecture + Dispatch Q&A
2026-06-26 학습 중 나온 질문과 답변 정리
1. Clean Architecture 심화
Domain Layer에 있다. Protocol과 구현체 둘 다.
Domain/
├── UseCases/
│ └── SearchMoviesUseCase.swift ← Protocol + 구현체 둘 다
└── Interfaces/Repositories/
└── MoviesRepository.swift ← Protocol만 (구현체는 Data Layer)
이유: UseCase 구현체는 순수 비즈니스 로직만 담음. 네트워크/DB 코드 없음. Repository는 외부 의존성 있어서 구현체가 Data Layer에.
Data Layer의 Repository 구현체에서 호출한다.
// Data/Repositories/DefaultMoviesRepository.swift
func fetchMoviesList(...) {
dataTransferService.request(with: endpoint) { result in
case .success(let responseDTO):
completion(.success(responseDTO.toDomain())) // ⭐ 여기
}
}
UseCase는 DTO 존재 자체를 모른다. Entity만 받음.
| DI (Dependency Injection) | DIP (Dependency Inversion) | |
|---|---|---|
| 한국어 | 의존성 주입 | 의존성 역전 원칙 |
| 분류 | 기법 (How) | 원칙 (Why) |
| 의미 | 외부에서 의존성을 넣어준다 | 추상화(Protocol)에 의존해라 |
DIP: 고수준(UseCase)이 저수준(Repository 구현체)을 직접 의존하지 않고, 둘 다 Protocol에 의존.
구독 해제 시 식별자 역할. 클로저는 비교 불가능해서 누가 등록했는지 알 수 없음.
// observer 없으면
func remove(???) {
// 어떤 클로저를 지워야 하지? 클로저끼리 == 안 됨
}
// observer 있으면
func remove(observer: AnyObject) {
observers = observers.filter { $0.observer !== observer }
}
간단한 앱이면 observer 없이 클로저만 써도 됨. 구독 해제가 필요하면 필요.
아니다. NavigationController가 소유한다.
Coordinator
└── weak navigationController // 참조만
NavigationController
└── viewControllers: [VC1, VC2, ...] // ⭐ 여기서 소유
Coordinator는 VC를 생성하고 push만 하고, 저장하지 않음.
할 수 있다. 전부 strong이면 A가 D를 간접 소유.
| 표현 | 의미 |
|---|---|
| A가 B를 직접 소유 | A가 B를 strong 참조 |
| A가 D를 간접 소유 | A 해제되면 → B → C → D 다 해제됨 |
class DefaultMoviesListViewModel {
private let actions: MoviesListViewModelActions? // ⭐ 프로퍼티로 저장
init(actions: MoviesListViewModelActions?) {
self.actions = actions
}
}
Actions는 struct. 안에 클로저들이 Coordinator 메서드를 캡처.
상황에 따라 다름. 메서드 참조는 self를 strong 캡처함.
// strong 캡처
let actions = Actions(showDetail: showDetail)
// weak 캡처 (안전)
let actions = Actions(showDetail: { [weak self] movie in
self?.showDetail(movie: movie)
})
Coordinator가 앱 수명 동안 살면 생략 가능. 아니면 weak self 권장.
2. Static vs Dynamic Dispatch
맞다. 런타임에 어떤 메서드 실행할지 결정.
| Static Dispatch | Dynamic Dispatch | |
|---|---|---|
| 결정 시점 | 컴파일 타임 | 런타임 |
| 방법 | 함수 주소 고정 | vtable/PWT 조회 |
아니다. 제네릭은 호출부에서 컴파일 타임에 T 확정.
| any Drawable (existential) | <T: Drawable> (generic) | |
|---|---|---|
| 타입 결정 | 런타임 | 컴파일 타임 |
| Dispatch | Dynamic | Static (간접 호출) |
// Dynamic — 런타임에 타입 확인
let item: any Drawable = Circle()
item.draw() // PWT 조회, 타입 모름
// Static — 컴파일 타임에 T = Circle 확정
func process<T: Drawable>(_ item: T) { item.draw() }
process(Circle()) // PWT 파라미터로 전달, 타입 알고 있음
맞다. 프로토콜 타입으로 쓰면 Dynamic Dispatch.
let item: Drawable = Circle() // 프로토콜 타입 item.draw() // ⭐ Dynamic Dispatch (PWT 조회)
| 사용 방식 | Dispatch |
|---|---|
let item: Drawable = ... | Dynamic |
let item: any Drawable = ... | Dynamic |
func foo<T: Drawable>(_ item: T) | Static |
let item: Circle = ... | Static |
프로퍼티도 포함. 프로퍼티는 내부적으로 getter/setter 메서드.
protocol Drawable {
var color: String { get } // getter 메서드
func draw()
}
let item: Drawable = Circle()
item.color // ⭐ Dynamic Dispatch (PWT에서 getter 조회)
item.draw() // ⭐ Dynamic Dispatch
아니다. Static Dispatch는 Inlining의 전제 조건일 뿐.
| Static Dispatch | Inlining | |
|---|---|---|
| 의미 | 컴파일 타임에 함수 확정 | 함수 본문을 호출부에 복사 |
| 결과 | 함수 주소로 직접 호출 | 함수 호출 자체가 없어짐 |
Static Dispatch여도 컴파일러 판단에 따라 인라인 안 될 수 있음.
기본적으로 맞다.
| 상황 | 인라인 |
|---|---|
| 같은 모듈 | ✅ 가능 (컴파일러 판단) |
| 다른 모듈 (기본) | ❌ 불가 (본문 못 봄) |
다른 모듈 + @inlinable | ✅ 가능 |
맞다. 런타임에 뭐가 들어있든 컴파일러는 정적 타입만 본다.
protocol Drawable { func draw() }
class Circle: Drawable {
func draw() { }
func resize() { }
}
let a: Circle = Circle()
a.draw() // ✅ Circle에 있음
a.resize() // ✅ Circle에 있음
let b: Drawable = Circle()
b.draw() // ✅ Drawable에 있음
b.resize() // ❌ Drawable에 없음 (컴파일 에러)
| 선언 | 정적 타입 | 호출 가능 |
|---|---|---|
let a: Circle = Circle() | Circle | draw(), resize() |
let b: Drawable = Circle() | Drawable | draw()만 |
let c: any Drawable = Circle() | any Drawable | draw()만 |
metadata가 vtable을 가리키고 있어도, 컴파일러가 타입 안전성을 보장 못하면 호출 자체를 막는다. Dynamic Dispatch는 "어떤 구현체"를 런타임에 정하는 거지, "메서드 존재 여부"는 컴파일 타임에 정적 타입으로 확인.
// 호출하고 싶으면 다운캐스팅
if let circle = b as? Circle {
circle.resize() // ✅ OK
}
vtable 조회하면 Dynamic. 하지만 vtable 있어도 최적화로 조회 생략 가능.
| 상황 | Dispatch |
|---|---|
| 일반 class 메서드 | Dynamic (vtable) |
final class / final func | Static |
private func | Static |
static func | Static |
class Animal {
func speak() { } // vtable에 등록됨
final func eat() { } // vtable에 없음 (final)
}
let a: Animal = Dog()
a.speak() // vtable 조회 → Dynamic
a.eat() // vtable 조회 안 함 → Static
호출하는 코드에 직접 박혀있다. 인스턴스에 저장되는 게 아님.
| Dynamic (vtable) | Static | |
|---|---|---|
| 함수 주소 위치 | 인스턴스 metadata → vtable | 호출 코드에 직접 |
| 런타임 조회 | ✅ 필요 | ❌ 불필요 |
Dynamic: 인스턴스 → metadata → vtable → 함수 주소 → 호출 Static: call 0x1234ABCD (주소가 코드에 박힘)
3. Existential Container / Type Metadata
Type Metadata Pointer = 실제 타입 정보를 가리키는 포인터
Existential Container (40 bytes) ┌─────────────────────────────┐ │ Value Buffer (24B) │ ← 값 저장 ├─────────────────────────────┤ │ Type Metadata Pointer (8B) │ ← ⭐ 실제 타입 (is, as? 등) ├─────────────────────────────┤ │ PWT Pointer (8B) │ ← 프로토콜 메서드 테이블 └─────────────────────────────┘
Type Metadata는 타입 확인(is, as?), 메모리 관리에 사용.
Type Metadata 안에 있다. Existential Container에 직접 없음.
Existential Container
│
└── Type Metadata Pointer ──→ Type Metadata
│
└── VWT Pointer ──→ Value Witness Table
├── copy()
├── move()
├── destroy()
├── size
└── alignment
VWT = 값 복사/이동/해제 방법. 런타임에 타입 크기, 관리 방법 모를 때 사용.
아니다. vtable은 class 상속 다형성용. any랑 무관하게 class면 항상 있음.
// Class는 원래 vtable 사용
class Animal { func speak() { } }
class Dog: Animal { override func speak() { } }
let animal: Animal = Dog()
animal.speak() // vtable로 Dog.speak() 찾음
// Protocol은 PWT 사용
let drawable: Drawable = Circle()
drawable.draw() // PWT로 Circle.draw() 찾음
찾는다. 인스턴스 자체가 Dog의 metadata를 가리키고 있어서.
let animal: Animal = Dog()
animal 변수
│
▼
Dog 인스턴스 (힙)
┌─────────────────────┐
│ metadata ptr ───────│──→ Dog Type Metadata (Animal 아님!)
│ (stored properties) │ │
└─────────────────────┘ ▼
Dog vtable
└── speak() → Dog.speak()
핵심: 변수 타입(Animal)은 컴파일 타임용. 런타임엔 인스턴스의 실제 metadata를 따라감.