저의 깃 허브 초기 코드들을 보시면 아시겠지만,
보통 아래와 같은 구조로 코드를 작성 하였습니다.
이러한 구조에서의 문제점이 보이실까요?
ViewModel이 너무 바쁩니다.
View에 직접적인 로직과, 비즈니스 로직 등 너무 많은 일을 하게 되는 구조 입니다.
제일 중요한 부분은 외부 변화에 취약하다는 점입니다.
예를 들어 보겠습니다.
API가 일부 변경 되었습니다 ( API 주소, 응답값 변경 )
만약 ViewModel 에서 이를 관리 하였다면? 수많은 뷰에서 사용 했다면?
각 사용하는 뷰모델에서 코드를 수정해 주어야 하는 참사가 일어납니다.
소프트웨어의 설계와 업그레이드를 통제하는 원칙
을 말합니다.
좀더 쉽게 생각하면 해당앱의 구조를 정의하고 규칙을 정의
하는 것을 말합니다.
유연하게 변경하며 견고한 구조를 만들기 위한 소프트웨어 디자인 철학
라고 하는데 역시 말로만 하기엔 어려운 부분인 것 같습니다.
클린 아키텍처 하면 나오는 사진을 보도록 하겠습니다.
무언가 구성요소들을 나눈 것으로 보이는데 감이 잘 안잡히실 것 같습니다.
자 처음 사진을 위와 같이 구성요소로 분리해 보도록 하겠습니다.
클린 아키텍처 레이어는 총 4개의 레이어로 분리되어 있는데
이전 방식에서는 2개의 레이어로 분리 되어 있습니다.
Actor가 필요로 하는 데이터모델, 도메인에서 사용되는 모델 등
핵심은 도메인 지식을 담은 모델(Class, struct 등), 규칙을 말합니다.
예를 들어
struct VideosEntity: Entity, Identifiable {
let identifier: String
let videoURL: URL?
...
}
위와 같은 모델이
Entity
에 해당합니다.
서비스를 사용하고 있는 사용자(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는 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)
}
}
마지막으로 인터페이스와 DB 레이어 입니다.
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
}
}
...
사용자의 행위를 즉 상호작용을 통해 데이터를 화면에 표시하는 레이어 입니다.
이번 시간은 클린 아키텍처 구성요소를 다루어 보았습니다.
다음 시간은 설계 원칙을 다루어 볼 예정입니다.
오늘 하루 고생 많으셨습니다.
너무 좋은 글입니다~!