클린 아키텍처
클린 아키텍처는 의존성 방향을 바깥에서 안으로만 흐르게 강제.
핵심 비즈니스 로직을 외부 변화에 독립적으로 유지하려는 소프트웨어 설계 원칙.
- 계층 분리
- 가장 안쪽:
Entities
(비즈니스 규칙) - 그 다음:
Use Cases
(애플리케이션 규칙) - 바깥쪽:
Interface Adapters
(Controller, Presenter, Gateway) - 제일 바깥:
Frameworks & Drivers
(UI, DB, 외부 라이브러리 등)
- 가장 안쪽:
- 의존성 규칙 (Dependency Rule)
- 안쪽 계층은 바깥 계층에 대해 몰라야 함.
- 반대로, 바깥 계층은 안쪽 계층을 알아도 됨.
- 예: DB가 바뀌어도
UseCase
는 그대로.
- 인터페이스를 바깥에서 구현하고 안에서 호출
- DIP(Dependency Inversion Principle) 적용
- 테스트 용이성
- 외부 요소 없이 핵심 로직 테스트 가능
- 변화에 유연함
- UI, DB, 프레임워크가 바뀌어도 핵심 로직 영향 없음
- Entities
- 가장 안쪽 계층
- 재사용 가능한 순수 모델
ResponseModel
은 API 응답 구조이기도 하지만,UseCase, ViewModel 등 모든 계층에서 다루는 공통 데이터 기준
1 2 3 4 5 6
struct ResponseModel: Codable { let deleteIds: [String] let editNodes: [Node] let newNodes: [Node] }
- Use Cases
- 앱이 수행해야 하는 기능을 나타내는 로직 계층
- 외부 기술 (UI, DB 등)을 몰라야 함.
- Entity를 사용하지만 의존성은 아래로 흐르지 않음.
- “노드들을 가져온다”라는 도메인 로직을 담당
- 외부 저장소가 어디든, 네트워크가 어떤 방식이든 신경 쓰지 않고
- 오직
NodeRepository
라는 추상화된 인터페이스에만 의존 - 이 구조 덕분에 테스트도 쉽고, 재사용도 쉬움
1 2 3 4 5 6 7 8 9 10 11
class FetchNodesUseCase { private let nodeRepository: NodeRepository init(nodeRepository: NodeRepository) { self.nodeRepository = nodeRepository } func execute() -> Observable<ResponseModel> { return nodeRepository.fetchNodes() } }
- Interface Adapters (ViewModel / RepositoryImpl)
- 보통 ViewModel, RepositoryImpl 등이 여기 위치
- 외부 데이터 포맷을 내부 도메인 포맷으로 변환하거나 연결
- 이 계층은 UseCase에게 통합된 API를 제공하는 브릿지 역할
- 실제 저장소, API가 어디에 있든 RepositoryImpl 내부에서 결정
- 로컬 데이터 + 네트워크 요청을 조합해서 UseCase에게 필요한 구조만 제공
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
protocol NodeRepository { func fetchNodes() -> Observable<ResponseModel> } class NodeRepositoryImpl: NodeRepository { private let local: LocalNodeDataSource private let remote: RemoteNodeDataSource init(local: LocalNodeDataSource, remote: RemoteNodeDataSource) { self.local = local self.remote = remote } func fetchNodes() -> Observable<ResponseModel> { let requestNodeModel = local.fetchNodeSummaries() return remote.fetchNodes(request: RequestModel(nodes: requestNodeModel)) } }
Frameworks & Drivers
외부 기술/환경과 직접 연결되는 계층
- 예: URLSession, UserDefaults, Firebase, Alamofire 등
- 언제든지 교체될 수 있으므로, 반드시 인터페이스로 추상화해서 상위 계층에 넘김
LocalNodeDataSourceImpl
은 내부적으로SaveDataManager
같은 저장소 접근RemoteNodeDataSourceImpl
은URLSession
같은 외부 네트워크 호출- 이 계층은 항상 기술 종속적이므로 추상화(Protocol) 를 통해 상위 계층에 연결
- 상위 계층(Repository, UseCase 등)은 이 구현체의 존재조차 모름 → DIP 만족
1
2
3
4
5
6
7
8
9
10
11
12
protocol LocalNodeDataSource {
func fetchNodeSummaries() -> [RequestNodeModel]
}
class LocalNodeDataSourceImpl: LocalNodeDataSource {
func fetchNodeSummaries() -> [RequestNodeModel] {
// SaveDataManager에서 노드 불러와서 id + lastEdit만 추출
return SaveDataManager.loadNodes()?.map { node in
RequestNodeModel(id: node.id, lastEdit: node.lastEdit)
} ?? []
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
protocol RemoteNodeDataSource {
func fetchNodes(request: RequestModel) -> Observable<ResponseModel>
}
class RemoteNodeDataSourceImpl: RemoteNodeDataSource {
//service
let webService = WebService()
func fetchNodes(request: RequestModel) -> Observable<ResponseModel> {
let url = URL(string: webService.nodeData)!
return Observable.create { observer in
do {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let jsonData = try encoder.encode(request)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = jsonData
// URLSession 생성 (기본 세션)
let session: URLSession = URLSession(configuration: .default)
let task = session.dataTask(with: request) { (data, response, error) in
if let error = error {
Service.myPrint("fetchNodes() - error") {
print(error)
}
observer.onError(error)
}
guard let data = data else { return }
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let responseData = try decoder.decode(ResponseModel.self, from: data)
observer.onNext(responseData)
} catch {
Service.myPrint("fetchNodes() - error") {
print(error)
}
observer.onError(error)
}
}
task.resume()
return Disposables.create {
task.cancel()
}
} catch {
observer.onError(error)
return Disposables.create()
}
}// Observser create
}
}
This post is licensed under CC BY 4.0 by the author.