클린아키텍처 - CleanArchitecture 1편 - 구성요소

김재형·2024년 11월 7일
0

이전까지 사용하던 방법

저의 깃 허브 초기 코드들을 보시면 아시겠지만,
보통 아래와 같은 구조로 코드를 작성 하였습니다.

이러한 구조에서의 문제점이 보이실까요?
ViewModel이 너무 바쁩니다.
View에 직접적인 로직과, 비즈니스 로직 등 너무 많은 일을 하게 되는 구조 입니다.
제일 중요한 부분은 외부 변화에 취약하다는 점입니다.

예를 들어 보겠습니다.
API가 일부 변경 되었습니다 ( API 주소, 응답값 변경 )
만약 ViewModel 에서 이를 관리 하였다면? 수많은 뷰에서 사용 했다면?
각 사용하는 뷰모델에서 코드를 수정해 주어야 하는 참사가 일어납니다.

클린 아키텍처

아키텍처란 무엇인가?

소프트웨어의 설계와 업그레이드를 통제하는 원칙을 말합니다.
좀더 쉽게 생각하면 해당 앱의 구조를 정의하고 규칙을 정의 하는 것을 말합니다.

클린 아키텍처란?

유연하게 변경하며 견고한 구조를 만들기 위한 소프트웨어 디자인 철학
라고 하는데 역시 말로만 하기엔 어려운 부분인 것 같습니다.
클린 아키텍처 하면 나오는 사진을 보도록 하겠습니다.

무언가 구성요소들을 나눈 것으로 보이는데 감이 잘 안잡히실 것 같습니다.
자 처음 사진을 위와 같이 구성요소로 분리해 보도록 하겠습니다.

클린 아키텍처 레이어는 총 4개의 레이어로 분리되어 있는데
이전 방식에서는 2개의 레이어로 분리 되어 있습니다.

Entity (Enterprise Business Rules)

Actor가 필요로 하는 데이터모델, 도메인에서 사용되는 모델 등
핵심은 도메인 지식을 담은 모델(Class, struct 등), 규칙을 말합니다.
예를 들어

struct VideosEntity: Entity, Identifiable {
    let identifier: String
    let videoURL: URL?
    ...
}

위와 같은 모델이 Entity에 해당합니다.

UseCase (Application Business Rules)

서비스를 사용하고 있는 사용자(User)가 해당 서비스를 통해 하고자 하는 것 좀더 쉽게 생각하자면
Actor 가 Entity를 요구할때에 얻어지는 과정을 말합니다.
조오옴 더 쉽게 생각하자면
Entity를 얻어가는데 예를들어 개발용, 고객용이 다를때 관한 로직을 구현하는 라이니
UseCase 입니다.

final class VideoRepository: @unchecked Sendable {
    @Dependency(\.networkManager) var network
    @Dependency(\.videoMapper) var videoMapper
    @Dependency(\.errorMapper) var errorMapper
    
    func fetchVideoHeader(identifier: String) async throws -> (header: HeaderEntity, video: VideosEntity)? {
        let data = try await network.requestNetwork(dto: VideoDTO.self, router: VideoRouter.fetchVideos(identifier: identifier))
      ...
        return (headerData, videoData)
    }
    
    func fetchVideos(channel: Const.Channel, skip: Int = 0, limit: Int = 10) async throws -> [VideosEntity] {
        
        if channel.channelIDs.isEmpty {
            let data = try await network.requestNetwork(dto: VideoDTO.self, router: VideoRouter.fetchVideos(skip: skip, limit: limit))
            
            return await videoMapper.dtoToEntity(data.videos, channel: channel)
        } else {
            var tempData: [VideosEntity] = []
            
            for id in channel.channelIDs {
                let data = try await network.requestNetwork(dto: VideoDTO.self, router: VideoRouter.fetchVideos(channelId: id, skip: skip, limit: limit))
                
                await tempData.append(contentsOf: videoMapper.dtoToEntity(data.videos, channel: channel, channelID: id))
            }
            return tempData.sorted { $0.updatedAt > $1.updatedAt }
        }
    }
    
#if DEBUG
    deinit {
        print(" DIE TO VIdeoRepository  ")
    }
#endif
    
}

위와 같은 코드를 보면 이해가 더 잘되지 않을까 싶습니다.

Presenter, 레포지토리

Presenter는 Entity 데이터를 그대로 표현 ('present') 하는데 필요한 계층을 의미합니다.
다만 레포지토리는 데이터를 생성, 저장, 변경 방식을 결정하는 것을 말하는데
Presenter는 ViewModel 를 생각 하시면 될것 같습니다.
아래는 레포지토리 예시 입니다.

final class CharacterRepository: @unchecked Sendable {
    @Dependency(\.networkManager) var network
    @Dependency(\.characterMapper) var mapper
    @Dependency(\.errorMapper) var errorMapper
}

extension CharacterRepository {
    
    func fetchCharacter(id: Int) async throws -> CharacterEntity {
        let data = try await network.requestNetwork(dto: CharacterDTO.self, router: CharacterRouter.fetchCharacter(id))
        
        return mapper.dtoToEntity(data)
    }
    
    func fetchCharacters(id: String) async throws -> [YoutubeCharacterEntity] {
        let data = try await network.requestNetwork(dto: DTOList<YoutubeCharacterDTO>.self, router: VideoRouter.fetchCharacters(id))
        
        return await mapper.dtoToEntity(data.elements)
    }
    
    func fetchMemes(id: String) async throws -> [BookElementsEntity] {
        let data = try await network.requestNetwork(dto: DTOList<BookElementDTO>.self, router: VideoRouter.fetchMemes(id))
        
        return await mapper.dtoToEntity(data.elements)
    }
}

Interfaces & DB

마지막으로 인터페이스와 DB 레이어 입니다.

DB Layer

DB 레이어 부터 생각해 보면
다른 말로 DataSource레이어 라고 생각하시면 됩니다.
네트워크 등을 통한 데이터를 Local에 생성, 저장, 수정, 삭제 를 수행하는 레이어 이죠.

import Foundation
import RealmSwift

final actor DataSourceActor {
    
    private var realm: Realm?
    
    private let mapper: SearchMapper
    private let videoMapper: VideoMapper
    
    init() {
        self.mapper = SearchMapper()
        self.videoMapper = VideoMapper()
        Task {
            await self.setup()
        }
    }
    
    private func setup() async { // 내쓰레드
        do {
            realm = try await Realm.open()
        } catch {
            realm = nil
        }
    }
... 

Interface Layer

사용자의 행위를 즉 상호작용을 통해 데이터를 화면에 표시하는 레이어 입니다.

마무리 하며

이번 시간은 클린 아키텍처 구성요소를 다루어 보았습니다.
다음 시간은 설계 원칙을 다루어 볼 예정입니다.
오늘 하루 고생 많으셨습니다.

profile
IOS 개발자 새싹이
post-custom-banner

1개의 댓글

comment-user-thumbnail
2024년 11월 8일

너무 좋은 글입니다~!

답글 달기