목차
I. 머리말
(0) 요약
📝 4개의 주제표
아래와 같이 요약할 수 있다. 긴 글을 읽기 전에 일단 살펴 보고 가자.
용어 | 정의 | |
문제 | 의존 관계 발생 | 1. 하위 모듈의 인스턴스를 상위 모듈 내부에 직접 생성할 경우 발생 2. 하위 모듈이 변경될 때마다 상위 모듈 또한 매번 대응해야 함 |
해결책 | 의존성 분리 | 1. Swift의 인터페이스인 프로토콜(Protocol)을 이용한 추상화 진행 2. 정의한 프로토콜을 실제 모듈에 채택하여 요구사항을 준수하도록 요구 |
수단 | 의존성 주입 (Dependency Injection) |
1. 하위 모듈의 인스턴스를 외부에서 생성 2. 상위 모듈 내부에는 프로토콜을 프로퍼티로 정의하고 생성자를 통해 프로퍼티를 생성 |
원칙 | 의존성 역전 원칙 (DIP; Dependency Inversion Principle) |
1. 의존성 주입을 이용하여 다음과 같은 의존성의 방향 역전 화면 → 비즈니스 로직 → 데이터 화면 → 비즈니스 로직 ← 데이터 |
(1) 상위 모듈과 하위 모듈
(1-0) 예시 - 점심을 먹으려는 방문객과 레스토랑, 주인
상황
- 점심을 먹고 싶은 Andy는 햄버거를 먹거나 분식을 먹으려 한다.
- 주변에는 2개의 레스토랑이 있다. 이곳에서 점심 메뉴를 선택할 수 있다.
- (1) NoahRestaurant라는 햄버거 집과 (2) CallieRestaurant이라는 분식 집이 있다.
- 또한 각각의 레스토랑은 주인이 (1) Noah, (2) Callie이다.
설명
- 여기서 상위 모듈은 방문객(Andy)이고,
- 하위 모듈은 레스토랑(NoahRestaurant & CallieRestaurant)과 주인(Noah & Callie)이다.
- 상대적으로 레스토랑도 주인의 상위 모듈이다.
(1-1) 상위 모듈 - 방문객(Andy)
class Andy {
var hamburgerRestaurant = NoahRestaurant()
var bunsikRestaurant = CallieRestaurant()
func selectHamburger() -> String {
hamburgerRestaurant.makeLunchMenu()
}
func selectBunsik() -> String {
selectBunsik.makeLunchMenu()
}
}
(1-2) 하위 모듈 A - 레스토랑 (NoahRestaurant & CallieRestaurant)
각 레스토랑은 주인이 있는데, 해당 주인의 메뉴에 따라서 점심 메뉴를 만든다.
class NoahRestaurant {
private let owner = Noah()
func makeLunchMenu() -> String {
return owner.makeSignatureHamburger() + owner.makeFrenchFries() + owner.makeSoftDrink()
}
}
class CallieRestaurant {
private let owner = Callie()
func makeLunchMenu() -> String {
return owner.makeGimbap() + owner.makeRamyeon() + owner.makeMandu()
}
}
(1-3) 하위 모듈 B - 주인(Noah & Callie)
각 식당은 주인이 있는데, 이름 그대로 Noah와 Callie가 주인이다. 각 주인은 본인의 레시피 대로 만들어 본인의 레스토랑에 음식을 제공한다.
class Noah {
func makeSignatureHamburger() -> String {
return "Signature Hamburger"
}
func makeFrenchFries() -> String {
return "FrenchFries"
}
func makeSoftDrink() -> String {
return "Soft Drink"
}
}
class Callie {
func makeGimbap() -> String {
return "Beef Gimbap"
}
func makeRamyeon() -> String {
return "Hot Ramyeon"
}
func makeMandu() -> String {
return "Kimchi Mandu"
}
}
(2) 문제 발생 🔥
(2-1) 의존성 문제 발생
- 이제 아래와 같은 의존성 문제가 발생한다.
- 오른쪽으로 갈수록 하위 모듈인데, low-level의 모듈의 변경이 생길 때마다 그의 상위의 모듈에 매번 변경을 해야하는 문제가 발생할 것이다.
- 방문객은 레스토랑을, 레스토랑은 주인에게 의존하는 관계가 성립된다
(2-2) 예시 - 주인의 레시피 변경
- 아래처럼 Noah라는 주인이 메뉴 방식을 프랜치프라이가 아니라 어니언링으로 변경했다면,
- 이를 인스턴스로 갖고 있는 NoahRestaurant라는 상위 모듈에 적용을 해줘야 할 것이다.
class Noah {
// ❓ 주인의 레시피 변경 (makeFrenchFries -> makeOnionRing)
func makeOnionRing() -> String {
return "OnionRing"
}
}
class NoahRestaurant {
private let owner = Noah()
// 🔥 ERROR: 레스토랑의 메뉴를 변경해야 한다
func makeLunchMenu() -> String {
return owner.makeSignatureHamburger() + owner.makeFrenchFries() + owner.makeSoftDrink()
}
}
- 나아가 해당 레스토랑에 주인이 바뀐다거나, 구제척인 메뉴가 바뀌는 등 해당 클래스 인스턴스를 갖고 있는 상위 모듈에 직접적으로 영향을 미치게 된다.
II. 의존성 주입 (Dependency Injection) by Protocol
(1) 프로토콜을 이용한 의존성 문제 해결
프로토콜을 Swift에서 정의한 인터페이스이자, 추상화의 수단이다.
(1-1) 프로토콜 정의 (인터페이스 생성)
방문객과, 레스토랑과 주인을 위한 각각의 프로토콜을 정의한다.
//MARK: - ⭐️ (0) Protocol
protocol Customer {
func selectHamburger() -> String
func selectBunsik() -> String
}
protocol HamburgerRestaurant {
func makeLunchMenu() -> String
}
protocol BunsikRestaurant {
func makeLunchMenu() -> String
}
protocol Owner {
func makeMainMenu() -> String
func makeSideMenu() -> String
func makeDrink() -> String
}
(1-2) 기존 구조체/클래스에 프로토콜 채택
기존 커스텀 타입인 구조체/클래스에 (1)의 프로토콜을 채택하면, 해당 프로토콜의 프로퍼티/메서드를 준수(conform to) 해야 한다.
방문객
//MARK: - (1) Customer
final class Andy: Customer {
private var hamburgerRestaurant: HamburgerRestaurant
private var bunsikRestaurant: BunsikRestaurant
init(
hamburgerRestaurant: HamburgerRestaurant,
bunsikRestaurant: BunsikRestaurant
) {
self.hamburgerRestaurant = hamburgerRestaurant
self.bunsikRestaurant = bunsikRestaurant
}
func selectHamburger() -> String {
hamburgerRestaurant.makeLunchMenu()
}
func selectBunsik() -> String {
bunsikRestaurant.makeLunchMenu()
}
}
주인과 레스토랑
//MARK: - (2-1) Owners
final class Noah: Owner {
func makeMainMenu() -> String {
return "Noah's Signature Hamburger"
}
func makeSideMenu() -> String {
return "Noah's FrenchFries"
}
func makeDrink() -> String {
return "Noah's Soft Drink"
}
}
final class Callie: Owner {
func makeMainMenu() -> String
return "Callie's Gimbap"
}
func makeSideMenu() -> String {
return "Callie's Ramyeon"
}
func makeDrink() -> String {
return "Callie's EomukSoup"
}
}
//MARK: - (2-2) Restaurants
final class NoahRestaurant: HamburgerRestaurant {
private let owner: Owner
init(
owner: Owner
) {
self.owner = owner
}
func makeLunchMenu() -> String {
return owner.makeMainMenu() + owner.makeSideMenu() + owner.makeDrink()
}
}
final class CallieRestaurant: BunsikRestaurant {
private let owner: Owner
init(
owner: Owner
) {
self.owner = owner
}
func makeLunchMenu() -> String {
return owner.makeMainMenu() + owner.makeSideMenu() + owner.makeDrink()
}
}
(1-3) 의존성 주입(Dependency Injection) (외부 → 내부)
실제 인스턴스를 외부에 생성하여 이를 코드 내부에 할당한다.
//MARK: - (3) 의존성 주입 (Dependency Injection)
let noah = Noah()
let noahRestaurant = NoahRestaurant(owner: noah)
let callie = Callie()
let callieRestaurant = CallieRestaurant(owner: callie)
let andy = Andy(
hamburgerRestaurant: noahRestaurant,
bunsikRestaurant: callieRestaurant
)
III. DI vs DIP (Injection vs Inversion)
(0) 수단 vs 원칙
(0-1) 주입 vs 역전
- 줄임말만 보면 너무 비슷하게 보여 헷갈리는 DI와 DIP는 엄연히 다른 말이다.
- 심지어 각각의 "I"는 주입(Injection)과 역전(Inversion)으로 전혀 다른 의미를 갖고 있다.
- 또한 하나는 추상화를 이용한 수단이고, 다른 하나는 추상화에 대한 원칙이다.
(0-2) DI를 하면 DIP를 지킨 것인가?
- 그렇다면 의존성 주입을 하면 의존성 역전 원칙을 지킨 것일까?
- 그것은 아니다. 단지, 인스턴스를 밖에서 생성하여 주입하는 것일 뿐, 의존 관계가 뒤바뀌는 것은 아니다.
- 다만, 📝 여기서 DIP는 SOLID로 대표되는 객체지향의 5원칙 중 하나이다.
- DI라는 수단을 통해 DIP라는 원칙을 지킬 수 있다
- 이제 실제로 진행하고 있는 프로젝트(LSLP)의 코드르 통해 알아 보자.
(1) 의존성 주입(Injection)
(1-1) LSLP 프로젝트 - Coordinator
외부(Coordinator)에 인스턴스(VC, VM, UseCase & Repo)를 생성하여 내부로 주입했다.
final class StoryContentCoordinator: Coordinator {
// ...
}
extension StoryContentCoordinator {
// Coordinator 클래스라는 외부에서 인스턴스(VC, VM, UseCase & Repo)를 생성하여 이를 주입
func showStoryListViewController() {
self.navigationController.pushViewController(
StoryListViewController(
viewModel: StoryListViewModel(
coordinator: self,
storyListUseCase: StoryListUseCase(
storyPostRepository: StoryPostRepository(),
keychainRepository: KeychainRepository()
)
)
),
animated: true
)
}
}
(2) 의존성 역전 원칙 (Inversion Principle)
(2-0) LSLP 프로젝트 - Repository Protocol
현재 진행 중인 LSLP(Light Service Level Project)의 구조 일부이다.
가운데 위치한 빨간 화살표와 별(⭐️)이 의존성 역전이다
- 여기서 Repositories는 Data Layer에 위치해 있는데, 기존의 방식으로 인스턴스를 매번 직접 생성한다면 아래와 같은 의존 관계가 성립 된다.
- 하지만 여기서, Domain의 Repository Protocol을 정의하여 Data Repositories에 해당 프로토콜을 채택하게 한다면 의존성이 역전(Inversion)된다.
- (이전) (Data) DataSources ← (Data) Repositories ← (Domain) UseCases ← ViewModel ← View
- (이후) (Data) DataSources ← (Data) Repositories → ⭐️ (Domain) Repository Protocol ←(Domain) UseCases ← ViewModel ← View
(2-1) KeychainRepositoryProtocol
프로토콜에 대해 정의했다.
enum KeyType: String {
case userID
case accessToken
case refreshToken
}
protocol KeychainRepositoryProtocol {
func save(key: String, value: String, type: KeyType) -> Bool
func find(key: String, type: KeyType) -> String?
func delete(key: String, type: KeyType) -> Bool
}
(2-2) KeychainRepository
프로토콜을 채택하여 이를 준수한 실제 구현체를 만들었다.
final class KeychainRepository: KeychainRepositoryProtocol {
private let keySecurityClass = kSecClassGenericPassword
//MARK: - 비즈니스 로직 (1)
private func logError(_ status: OSStatus) {
//...
}
//MARK: - 비즈니스 로직 (2)
private func update(account: String, value: Data) -> Bool {
//...
}
//MARK: - (1) save - KeychainRepositoryProtocol
func save(key: String, value: String, type: KeyType) -> Bool {
// 자세한 구현...
}
//MARK: - (2) find - KeychainRepositoryProtocol
func find(key: String, type: KeyType) -> String? {
// 자세한 구현...
}
//MARK: - (3) delete - KeychainRepositoryProtocol
func delete(key: String, type: KeyType) -> Bool {
// 자세한 구현...
}
}
(2-3) StoryListUseCase
UseCase 또한 프로토콜을 프로퍼티로 갖고 있기에, 생성자(initializer)를 통해 이를 생성하였다.
final class StoryListUseCase: StoryListUseCaseProtocol {
// 프로토콜을 프로퍼티로 정의
private let storyPostRepository: StoryPostRepositoryProtocol
private let keychainRepository: KeychainRepositoryProtocol
// 프로토콜을 클래스의 생성자로 호출
init(
storyPostRepository: StoryPostRepositoryProtocol,
keychainRepository: KeychainRepositoryProtocol
) {
self.storyPostRepository = storyPostRepository
self.keychainRepository = keychainRepository
}
// 자세한 비즈니스 로직 구현...
}
IV. 꼬리말
정리
머리말에 있는 주제표를 다시 한번 살펴 보자 :)
- (문제) 의존 관계 문제 발생
- (해결책) 의존성 분리
- (수단) 의존성 주입(Dependency Injection)
- (원칙) 의존성 역전 원칙(Dependency Inversion Principle)
'iOS > Swift' 카테고리의 다른 글
Swift) inout 파라미터 (2) | 2024.04.10 |
---|---|
Swift에서타입에 상관없이 코드를 작성할 수 없을까? (feat. Swift Generics) (0) | 2024.01.02 |
Swift) static member cannot be used on instance of type 오류 (feat. 중첩한 타입) (1) | 2023.11.09 |
Swift) 클로저의 캡처와 참조 타입 (1) | 2023.08.31 |
[Swift] didSet과 값 타입, 참조 타입의 인스턴스 (1) | 2023.08.22 |
댓글