Post

클린 아키텍처

클린 아키텍처는 의존성 방향을 바깥에서 안으로만 흐르게 강제.

핵심 비즈니스 로직을 외부 변화에 독립적으로 유지하려는 소프트웨어 설계 원칙.

  1. 계층 분리
    • 가장 안쪽: Entities (비즈니스 규칙)
    • 그 다음: Use Cases (애플리케이션 규칙)
    • 바깥쪽: Interface Adapters (Controller, Presenter, Gateway)
    • 제일 바깥: Frameworks & Drivers (UI, DB, 외부 라이브러리 등)
  2. 의존성 규칙 (Dependency Rule)
    • 안쪽 계층은 바깥 계층에 대해 몰라야 함.
    • 반대로, 바깥 계층은 안쪽 계층을 알아도 됨.
    • 예: DB가 바뀌어도 UseCase는 그대로.
  3. 인터페이스를 바깥에서 구현하고 안에서 호출
    • DIP(Dependency Inversion Principle) 적용
  4. 테스트 용이성
    • 외부 요소 없이 핵심 로직 테스트 가능
  5. 변화에 유연함
    • UI, DB, 프레임워크가 바뀌어도 핵심 로직 영향 없음
  6. Entities
    • 가장 안쪽 계층
    • 재사용 가능한 순수 모델
    • ResponseModelAPI 응답 구조이기도 하지만,

      UseCase, ViewModel 등 모든 계층에서 다루는 공통 데이터 기준

    1
    2
    3
    4
    5
    6
    
     struct ResponseModel: Codable {
         let deleteIds: [String]
         let editNodes: [Node]
         let newNodes: [Node]
     }
        
    
  7. 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()
         }
     } 
    
  8. 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))
         }
     }
    
  9. Frameworks & Drivers

    외부 기술/환경과 직접 연결되는 계층

    • 예: URLSession, UserDefaults, Firebase, Alamofire 등
    • 언제든지 교체될 수 있으므로, 반드시 인터페이스로 추상화해서 상위 계층에 넘김
    • LocalNodeDataSourceImpl은 내부적으로 SaveDataManager 같은 저장소 접근
    • RemoteNodeDataSourceImplURLSession 같은 외부 네트워크 호출
    • 이 계층은 항상 기술 종속적이므로 추상화(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.