SOLID 원칙 - iOS 객체지향 설계 가이드
SOLID 원칙을 적용하여 유지보수 가능하고 확장 가능한 iOS 코드를 작성한다. 의존성 관리, 책임 분리, 확장성을 고려한 설계를 지원한다.
상세 원칙 참조
각 원칙의 상세 내용, iOS 예제, 안티패턴, 리팩토링 방법은 references 디렉토리를 참고한다:
| 원칙 | 참조 파일 | 핵심 |
|---|---|---|
| SRP | references/srp.md | 모듈은 하나의 Actor에게만 책임 |
| OCP | references/ocp.md | 확장에 열리고 수정에 닫힘 |
| LSP | references/lsp.md | 하위 타입은 상위 타입 대체 가능 |
| ISP | references/isp.md | 사용하지 않는 인터페이스에 의존 금지 |
| DIP | references/dip.md | 고수준이 저수준에 의존하지 않음 |
객체지향의 본질
핵심은 의존성 역전(Dependency Inversion)이다.
- Runtime 흐름: 고수준 → 저수준
- Source Code 의존성: 저수준 → 고수준 (역전!)
- 고수준 정책을 저수준 세부사항으로부터 보호한다.
적용 워크플로우
1단계: 설계 전 체크리스트
□ Actor를 식별했는가? (SRP)
□ 변경 가능한 지점을 파악했는가? (OCP)
□ 추상화가 필요한 곳을 찾았는가? (DIP)
□ 인터페이스가 비대한가? (ISP)
□ 상속 관계가 적절한가? (LSP)
2단계: 코드 작성 중 체크리스트
□ 클래스가 여러 Actor를 섬기고 있는가? (SRP 위반)
□ 새 기능 추가 시 기존 코드를 수정하는가? (OCP 위반)
□ protocol에 구체 클래스를 직접 의존하는가? (DIP 위반)
□ protocol에 사용하지 않는 메서드가 있는가? (ISP 위반)
□ 하위 타입이 상위 타입의 계약을 위반하는가? (LSP 위반)
3단계: 리팩토링 우선순위
우선순위 1: DIP 위반 → protocol 도입으로 의존성 역전
우선순위 2: SRP 위반 → Actor별로 책임 분리
우선순위 3: OCP 위반 → protocol + 구현체로 확장
우선순위 4: ISP 위반 → protocol 분리
우선순위 5: LSP 위반 → 상속 대신 composition 고려
iOS에서 SOLID 적용 요약
Repository 패턴 (DIP + SRP + OCP)
swift1// DIP: 추상화에 의존 2protocol UserRepository { 3 func fetchUser(id: String) async throws -> User 4 func saveUser(_ user: User) async throws 5} 6 7// SRP: 네트워크 데이터 소스는 네트워크만 책임 8class NetworkUserRepository: UserRepository { 9 private let apiClient: APIClient 10 11 init(apiClient: APIClient) { 12 self.apiClient = apiClient 13 } 14 15 func fetchUser(id: String) async throws -> User { 16 try await apiClient.request(.getUser(id)) 17 } 18 19 func saveUser(_ user: User) async throws { 20 try await apiClient.request(.updateUser(user)) 21 } 22} 23 24// OCP: 새로운 저장소 추가 시 기존 코드 수정 불필요 25class CachedUserRepository: UserRepository { 26 private let cache: Cache 27 28 func fetchUser(id: String) async throws -> User { 29 try cache.get(id) 30 } 31 32 func saveUser(_ user: User) async throws { 33 try cache.set(user, key: user.id) 34 } 35} 36 37// UseCase는 추상화에만 의존 (DIP) 38class FetchUserUseCase { 39 private let repository: UserRepository 40 41 init(repository: UserRepository) { 42 self.repository = repository 43 } 44 45 func execute(id: String) async throws -> User { 46 try await repository.fetchUser(id: id) 47 } 48}
안티패턴 감지
Massive View Controller (SRP + DIP 위반)
swift1// ❌ 나쁜 예 2class ProductListViewController: UIViewController { 3 func fetchProducts() { 4 URLSession.shared.dataTask(with: url) { data, _, _ in 5 // JSON 파싱, 할인 계산, UI 업데이트 모두 여기서 6 } 7 } 8} 9 10// ✅ 좋은 예 11class ProductListViewController: UIViewController { 12 private let viewModel: ProductListViewModel 13 // UI만 담당 14}
구체 클래스 직접 의존 (DIP 위반)
swift1// ❌ 나쁜 예 2class OrderService { 3 let database = MySQLDatabase() 4} 5 6// ✅ 좋은 예 7class OrderService { 8 private let repository: OrderRepository // protocol 의존 9 10 init(repository: OrderRepository) { 11 self.repository = repository 12 } 13}
Fat Interface (ISP 위반)
swift1// ❌ 나쁜 예 2protocol DataSource { 3 func fetch() -> [Item] 4 func save(_ item: Item) 5 func delete(_ item: Item) 6 func search(_ query: String) -> [Item] 7} 8 9// ✅ 좋은 예 10protocol Fetchable { func fetch() -> [Item] } 11protocol Savable { func save(_ item: Item) } 12protocol Deletable { func delete(_ item: Item) } 13protocol Searchable { func search(_ query: String) -> [Item] }
실전 팁
- DIP부터 시작: 의존성 방향이 가장 중요
- Actor 찾기: SRP 적용의 시작점
- protocol 먼저: Swift는 protocol-oriented
- 점진적 적용: 한 번에 모두 적용하려 하지 말 것
- 테스트로 검증: SOLID 준수 여부는 테스트 용이성으로 확인