์๋ ํ์ธ์ ์ ์ธ์ ๋๋ค :)
์ต๊ทผ์ ์งํํ ๊ฐ์ธ ํ๋ก์ ํธ์ ์ํคํ ์ฒ๋ฅผ MVVM-Clean Architecture ๋ก ์ค๊ณํ๊ณ ,
๊ฐ ๋ ์ด์ด์ ์ญํ ์ ๋ช ํํ ๊ตฌ๋ถํด ์ ์ฒด ์ฝ๋์ ์ ์ง๋ณด์์ฑ์ ๋์ด๋ ๋ฐ ์ด์ ์ ๋ง์ถ์ด ๊ฐ๋ฐ์ ํ์๋๋ฐ์,
์ด ๊ณผ์ ์์ ํนํ UseCase์ ์ญํ ์ ๋ํด ๋ง์ด ๊ณ ๋ฏผ์ ํ ๊ฒ ๊ฐ์์!
๊ทธ๋์ ์ ๊ฐ ์ ์ ๋ฐฉ์๋๋ก UseCase๋ฅผ ๊ตฌ์ฑํ ๋ด์ฉ์ ํ ๋ฒ ๊ณต์ ํด๋ณด๋ ค๊ณ ํฉ๋๋ค.
UseCase๋?
UseCase ๋ ์ด์ด์ ์ญํ ์ ํ ์ค๋ก ์ ์ํ๋ค๋ฉด ์๋์ ๊ฐ์ด ์ ์ํ ์ ์์ ๊ฒ ๊ฐ์ต๋๋ค.
UseCase๋ ๋น์ฆ๋์ค ๋ก์ง์ด ์์นํ๋ ๊ณณ์ผ๋ก, ์ํฐํฐ๋ก ๋ค์ด์ค๊ณ ๋๊ฐ๋ ๋ฐ์ดํฐ ํ๋ฆ์ ์กฐ์ ํฉ๋๋ค.
์ด ์ ์๋ฅผ ์ข ํ์ด์ ์๊ฐํด๋ณผ๊ฒ์!
ํ๋ฉด์ ๋ณด์ฌ์ค ๋ฐ์ดํฐ๋ฅผ ์ํ๋ ํํ๋ก ์ป์ผ๋ ค๋ฉด?
1. ๋คํธ์ํฌ ํต์ ์ ํตํด DB์ ์๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ๋ค.
2. ์๋ฆฌ์กฐ๋ฆฌ ๊ณ์ฐ์ ํด์(= ์ด๋ค ๋ก์ง์ ๊ฑฐ์ณ์) ์ต์ข ์ ์ผ๋ก Entity ํ๋
๋ณดํต ์ด๋ฐ ๊ณผ์ ์ ๊ฑฐ์ณ ๋ฐ์ดํฐ๋ฅผ ์ป๊ฒ ๋ฉ๋๋ค.
์ฌ๊ธฐ์ UseCase ๋ ์ด์ด์ ์ญํ ์
2. ์๋ฆฌ์กฐ๋ฆฌ ๊ณ์ฐ์ ํด์(= ์ด๋ค ๋ก์ง์ ๊ฑฐ์ณ์) ์ต์ข ์ ์ผ๋ก Entity ํ๋
์ฌ๊ธฐ์์ '๋ก์ง'์ ์๋ฏธํ๊ฒ ๋๋ ๊ฒ ๊ฐ์์. (์ ๊ฐ ์๋ฌธ์ ํตํด ๊ณต๋ถํ๊ณ ํ๋ก์ ํธ์ ์ ์ฉํด๋ณธ ๊ฒฐ๊ณผ ๋๋ ๊ฒฐ๋ก ์ ์ด๋ ์ต๋๋ค)
์ ๋ MVVM-Clean Architecture ๊ตฌ์กฐ๋ก ํ๋ก์ ํธ๋ฅผ ์ค๊ณํ์๋๋ฐ์, (๋ค๋ฅธ ํจํด๊ณผ๋ ์ ์ฉ ๊ฐ๋ฅ)
๊ธฐ์กด MVVM ํจํด์์๋ ViewModel์ด ๋น์ฆ๋์ค ๋ก์ง์ ๋ด๋นํ์์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด MVVM-Clean Architecture ๊ตฌ์กฐ์์๋ MVVM์ด ๋ด๋นํ๋ ๋น์ฆ๋์ค ๋ก์ง์ UseCase๋ก ๋์ด๋ด๊ฒ ๋๊ฒ ์ฃ ?
- ViewModel: ๋ทฐ๋ก๋ถํฐ Input์ด ๋ค์ด์ค๋ฉด ์ ์ ํ Output์ ๋ง๋ค์ด ์ต์ข ์ ์ผ๋ก ๋ทฐ์ entity๋ฅผ ํ์ถํด์ค ์ ์๋๋ก
- UseCase: Repository์ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํด์ DB๋ก๋ถํฐ ๋ทฐ์ ํ์ถํ ๋ฐ์ดํฐ๋ฅผ ์ป๊ณ , ์ด ๋ฐ์ดํฐ๋ฅผ ๊ฐ๊ณต or ๋ฐ์ดํฐ ๊ด๋ จ ํ์ฒ๋ฆฌ
๊ตฌ์ฒด์ ์ผ๋ก ๊ฐ๊ฐ ์์ ๊ฐ์ด
ViewModel์ Input-Output์ ํ๋ฆ์ ์กฐ์ ํ๋ ์ญํ , UseCase๋ ๋ฐ์ดํฐ์ ํ๋ฆ์ ์กฐ์ ํ๋ ์ญํ ์ ๋ด๋นํ๊ฒ ๋์ด
๊ด์ฌ์ฌ๊ฐ ํ ๋จ๊ณ ๋ ๋ถ๋ฆฌ๋ฉ๋๋ค.
์ฒ์ ํด๋ฆฐ์ํคํ ์ฒ๋ฅผ ๊ณต๋ถํ๋ฉฐ UseCase๊ฐ ๋ฌด์จ ์ญํ ์ ํ๋์ง ๋ช ํํ๊ฒ ์ดํดํ๊ธฐ๊ฐ ํ๋ค์๋ ์ด์ ๋
ViewModel๊ณผ Repository ์ฌ์ด์์ ๋์ฒด ์์ ์ญํ ์ด ๋ฌด์์ธ์ง?!?!?! ์ดํดํ๋ ค๊ณ ๊ฐ๋จํ ์ฝ๋๋ฅผ ์์ฑํด๋ณด๋ฉด,
๊ฐ๋จํ ์์ ์ ๊ฒฝ์ฐ ๋ฑํ UseCase์์ ์ํํ๊ฒ ๋๋ ์ผ์ด ์์ด์ง๊ธฐ ๋๋ฌธ.. ์ด์๋ ๊ฒ ๊ฐ์์!
protocol UseCase {
func execute(request: Request) async throws -> [Item]
}
class DefaultUseCase: UseCase {
private let repository = DefaultRepository()
func execute(request: Request) async throws -> [Item] {
let result = try await repository.fetchList(query: request.query, page: request.page)
return result
}
}
์์ ๊ฐ์ด UseCase๋ Repository๋ฅผ execute() ๋ผ๋ ํจ์๋ก ํ๋ฒ ๋ ๊ฐ์ธ๊ธฐ๋ง ํ๊ณ .. ๊ฑฐ์ ์ฝ๋๊ฐ ๋์ผํด์ง๊ธฐ ๋๋ฌธ์..
๊ทธ๋ฌ๋ฉด ํต๋ก ์ญํ ๋ง ํ๋ ๊ฐ์ฒด์ธ UseCase๋ฅผ ๋ ํ์๊ฐ ์์ง ์๋? ํ๋ ์๊ฐ์ ๋ญ์ง...ํ๋ ์๋ฌธ์ด ๋ง์ด ๋ค์์๋๋ฐ
๋ ๋ง์ ์์ ๋ฅผ ์ฐพ์๋ณด๊ณ , ์ค์ ๋ก ํ๋ก์ ํธ์ ์ ์ฉ๊น์ง ํด๋ณด๋ ์ด๋ค ์ญํ ์ ํ๋์ง ๊ฐ์ด ์กํ๋๋ผ๊ตฌ์..!
์ ๊ฐ ํ๋ก์ ํธ์ UseCase๋ฅผ ์ด์ฉํด์ ViewModel์ ์ญํ ์ ๋์ด๋ธ ๊ณผ์ ์ ์๊ฐํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
UseCase๋ฅผ ์ด์ฉํด ViewModel์ ๋ก์ง์ ๋์ด๋ด๊ธฐ
์ผ๋จ, ์ ๊ฐ ๊ตฌํํ ๋ก์ง์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
1. ์ฌ์ฉ์๊ฐ ๋ฒํผ์ ๋๋ฆ ๋๋ค.
2. ViewModel๋ก ์ฌ์ฉ์ ์ด๋ฒคํธ๊ฐ ๋ค์ด์ค๋ฉด OpenAI API๋ฅผ ์ด์ฉํด AI์ ๋ต๋ณ์ ์์ฒญํ๋ ๋คํธ์ํน ์์ ์ ์ํํฉ๋๋ค.
3. ๋คํธ์ํน์ ์ฑ๊ณตํ์ฌ ๋ต๋ณ์ ์ป์ผ๋ฉด DB์ ์ฌ์ฉ์์ ์ด์ฉ ํ์๋ฅผ ์ ๋ฐ์ดํธ ์ํต๋๋ค.
๊ธฐ์กด์ UseCase๋ฅผ ์ฌ์ฉํ์ง ์์์ ๋๋, (๊ตณ์ด ๋ชจ๋ layer๋ฅผ ๊ผญ ๊ฐ์ถ์ด์ผ ํ ํ์๋ ์๋ค๊ณ ์๊ฐํด์ ๊ธฐ์กด์ UseCase๋ฅผ ์ฌ์ฉX)
ViewModel์ด Repository๋ก ๋คํธ์ํน ์์ฒญ์ ํ๊ณ Repository๋ ์ค์ ๋คํธ์ํน์ ์ํํ๋ Service ๊ฐ์ฒด๋ฅผ ์ด์ฉํด ์ํ๋ ๋ฐ์ดํฐ(ai์ ๋ต๋ณ ๊ฒฐ๊ณผ)๋ฅผ fetch ํด์์ ํ๋ฉด์ ๋ณด์ฌ์ค ์ ์๋๋ก View์ ๊ฒฐ๊ณผ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ธ๋ฉํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ , ViewModel์์ ๋ค์ Repository๋ก DB ๋ฐ์ดํฐ์ ์ ๋ฐ์ดํธ๋ฅผ ์์ฒญํ๋ ๊ตฌ์กฐ์์ต๋๋ค.
๊ธฐ์กด ์ฝ๋
final class InputTroubleViewModel {
// ...
private func fetchData(
systemContent: String,
userContent: String
) {
// ๋ฐ์ดํฐ fetch ์์ฒญ
repository.fetchResultData(systemContent: systemContent,
userContent: userContent)
.sink(receiveCompletion: { [weak self] completion in
switch completion {
case .finished:
// ๋ฐ์ดํฐ fetch ์ฑ๊ณตํ๋ฉด DB์ ์ ์ ์ ๋ณด ์
๋ฐ์ดํธ
self?.saveData()
case .failure(_):
self?.state.hasErrorOccurred = true
}
self?.state.isLoading = false
}, receiveValue: { [weak self] result in
self?.state.result = result.reply
self?.state.onCompleted = true
}).store(in: &cancellables)
}
// DB ์ ์ ์ ๋ณด ์
๋ฐ์ดํธ
private func saveData() {
repository.updateUserData(userId: UserDefaults.userId,
lastUsedDate: Date().today,
usedCount: UserDefaults.usedCount + 1)
.sink(receiveCompletion: { [weak self] completion in
if case let .failure(error) = completion {
self?.state.hasErrorOccurred = true
}
}, receiveValue: { }
).store(in: &cancellables)
}
}
์์์ ์ ๋ฆฌํ ๋ด์ฉ์ ๋ฐ์ํด ์ฝ๋๋ฅผ ๋ฆฌํฉํ ๋ง ํด๋ณด๊ฒ ์ต๋๋ค.
UseCase๋ฅผ ๋์ ํด ๋ฐ์ดํฐ์ ํ๋ฆ์ ์กฐ์ ํ๋ ์ญํ ์ UseCase๊ฐ, Input-Output์ ํ๋ฆ์ ์กฐ์ ํ๋ ์ญํ ์ ViewModel์ด ๋ด๋นํ๋๋ก ํ์ฌ ๋ทฐ๋ชจ๋ธ์ ์ญํ ์ ๋์ด๋ด๋ณผ๊ฒ์.
๊ฐ์ ์ฝ๋ - UseCase
protocol ConvertTroubleUseCase {
func execute(systemContent: String, userContent: String) -> AnyPublisher<ReplyEntity, NetworkError>
}
final class ConvertTroubleUseCaseImpl: ConvertTroubleUseCase {
private let gptRepository: GptRepository
private let userRepository: UserRepository
init(gptRepository: GptRepository,
userRepository: UserRepository
) {
self.gptRepository = gptRepository
self.userRepository = userRepository
}
func execute(systemContent: String, userContent: String) -> AnyPublisher<ReplyEntity, NetworkError> {
return gptRepository
.fetchResultData(systemContent: systemContent, userContent: userContent)
.flatMap { [weak self] replyEntity -> AnyPublisher<ReplyEntity, NetworkError> in
guard let self = self
else {
return Fail(error: NetworkError.serverError).eraseToAnyPublisher()
}
return self.userRepository
.updateUserData(userId: UserDefaults.userId,
lastUsedDate: Date().today,
usedCount: UserDefaults.usedCount + 1)
.map { _ in replyEntity }
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
์์ UseCase์์๋ ๋ ๊ฐ์ repository๋ฅผ ์ฎ์ด์ ๋ฐ์ดํฐ์ ๊ด๋ จ๋ ๋ก์ง์ ๋ชจ๋ ๋ด๋นํ๊ณ ์์ต๋๋ค.
๋ ์์ธํ ๋ณด๋ฉด, gptRepository๋ฅผ ํตํด ๊ฒฐ๊ณผ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ณ , flatMap์ ์ฌ์ฉํ์ฌ gptRepository๋ก๋ถํฐ ๋ฐ์ ๊ฒฐ๊ณผ(= replyEntity)๋ฅผ ๊ฐ์ง๊ณ ์ถ๊ฐ์ ์ธ ์ฌ์ฉ์ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ ์์ ์ ๋น๋๊ธฐ์ ์ผ๋ก ์ํํฉ๋๋ค.
๋ง์ฝ ์ค๊ฐ ๊ณผ์ ์์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ์ฆ์ ์ข ๋ฃํ๊ณ ์๋ฌ๋ฅผ ๋ฐํํ๊ณ , ๋ชจ๋ ์์ ์ด ์๋ฃ๋๋ฉด ReplyEntity๋ฅผ ๋ฐํํฉ๋๋ค.
๊ฐ์ ์ฝ๋ - ViewModel
final class InputTroubleViewModel {
// ...
private func fetchData(
systemContent: String,
userContent: String
) {
convertTroubleUseCase
.execute(systemContent: systemContent,userContent: userContent)
.sink(receiveCompletion: { [weak self] completion in
if case .failure(_) = completion {
self?.state.hasErrorOccurred = true
}
self?.state.isLoading = false
}, receiveValue: { [weak self] result in
self?.state.result = result.reply
self?.state.onCompleted = true
}).store(in: &cancellables)
}
}
๊ธฐ์กด์ ViewModel์ด ๋ด๋นํ๋ ์ญํ ์ UseCase์์ ์ผ๋ถ ์ํํ๊ฒ ๋์๊ธฐ ๋๋ฌธ์ ํจ์ฌ ๊ฐ๋จํด์ก์ต๋๋ค.
๋ทฐ๋ชจ๋ธ์์๋ UseCase์ ๊ฒฐ๊ณผ ๊ฐ(= ReplyEntity)์ ๋ฐ์์ Output์ผ๋ก ๋ด๋ณด๋ด ๋ทฐ์ ๋ฟ๋ ค์ฃผ๊ฑฐ๋(์ฌ๊ธฐ์ state๋ฅผ ์ ๋ฐ์ดํธํด์ฃผ๋ ๋ฐฉ์) ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ์๋ฌ์ ๋ฐ๋ฅธ ์ฒ๋ฆฌ๋ฅผ ํด์ฃผ๊ธฐ๋ง ํ๋ฉด ๋ฉ๋๋ค. ์ฆ, Presentation์ ํ์ํ ์ ๋ณด๋ง ๋ฐ์ ์ฒ๋ฆฌํ ์ ์๊ฒ ๋ ๊ฒ ์ ๋๋ค.
์๋ฌ ์ฒ๋ฆฌ ๊ฐ์ ๊ฒฝ์ฐ์๋ ๋ทฐ๋ชจ๋ธ์์๋ ์ต์ข ์ ์ผ๋ก ํ ๋ฒ๋ง ๋ฐ์ ํจ์ฌ ๊ฐ๊ฒฐํด์ง ๊ฒ์ ํ์ธ ํ ์ ์์ต๋๋ค!
UseCase ๋์ ์ผ๋ก ์ป์ ์ ์๋ ์ด์
1. ์ฝ๋ ์ค๋ณต์ ๋ฐฉ์ง
๊ธฐ์กด ์ฝ๋ vs ๊ฐ์ ์ฝ๋๋ฅผ ๋น๊ตํ๋ฉด์ ํ์ธํ ์ ์์์ฃ ? (์๋ฌ ์ฒ๋ฆฌ์ ๊ฒฝ์ฐ)
ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง์ ์บก์ํํ๊ณ , ์ฌ์ฌ์ฉ ํ ์ ์๋๋ก ํจ์ผ๋ก์จ ๊ฐ์ ์ฝ๋๊ฐ ์ฌ๋ฌ ๊ณณ์ ์ฐ์ฌ๋๋ ๊ฒ์ ๋ฐฉ์งํ ์ ์์ต๋๋ค.
2. ์ฑ ์์ ๋๋์ด ์ปค๋ค๋ ๊ฐ์ฒด๋ฅผ ํผํ ์ ์๋ค.
์ด๊ฒ๋ ๋ทฐ๋ชจ๋ธ์ด ํจ์ฌ ๊ฐ๊ฒฐํด์ง ๊ฒ์ผ๋ก ์์์ ํ์ธํ ์ ์์์ต๋๋ค.
ViewModel์ด ๋น๋ํด์ง๋ ๋ฌธ์ ๋ฅผ ๊ฐ์ ํ๊ฐ์ง ์ฑ ์๋ง ๊ฐ์ง๋ UseCase๋ค์ ๋์ด์ ํด๊ฒฐํ ์ ์์ต๋๋ค.
3. ํ ์คํธ์ ์ฉ์ด
2๋ฒ๊ณผ๋ ์ด์ด์ง๋ ์ด์ ์ ๋๋ค!
ํ๊ฐ์ง ์ฑ ์๋ง ๊ฐ๊ณ , ์ํ๋ฅผ ๊ฐ์ง ์๊ธฐ ๋๋ฌธ์ ํ ์คํธ ๋ฒ์๊ฐ ๋ช ํํด์ง๋๋ค.
4. ์ ์ง๋ณด์์ฑ ์ฉ์ด
์ฌ๋ฌ ๊ธฐ๋ฅ์ด ํ ๊ณณ์ ๋ชจ์ฌ์์ง ์๊ณ , ํ ๊ฐ์ UseCase๊ฐ ๋จ ํ๋์ ๋น์ฆ๋์ค ๋ก์ง๋ง ๋ด๋นํ๊ธฐ ๋๋ฌธ์,
UseCase ๋ด๋ถ์ ์ผ๋ถ ๋ก์ง์ด ๋ณ๊ฒฝ๋์ด๋ ๋ค๋ฅธ ๊ฐ์ฒด๋ ๋ ์ด์ด์ ์ํฅ์ ์ฃผ์ง ์๊ฒ ๋ฉ๋๋ค.
๊ทธ๋ฌ๋ฉด ์์ ํ๊ธฐ ์ฌ์ด ์ฝ๋๊ฐ ๋๊ฒ ์ฃ !
์ง๊ธ๊น์ง ์ ๊ฐ ๋ฆฌํฉํ ๋งํ ๊ฒฝํ์ ์ ๋ฆฌํ๋ฉด์ UseCase์ ๋ํด ๋ ์์ธํ ๊ณต๋ถํด๋ณด์๋๋ฐ์!
์ ๋ ์ด๋ฒ ๊ธฐํ๋ก UseCase์ ๋ํ ๋ชจํธํ ๊ฐ๋ ์ ํ์คํ ์ก์ ์ ์์๋ ๊ฒ ๊ฐ์ต๋๋ค.
๊ธด ๊ธ ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค๐๐
[์ฐธ๊ณ ์๋ฃ]
GitHub - kudoleh/iOS-Clean-Architecture-MVVM: Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoor
Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoordinator, DTO, Response Caching and one of the views in SwiftUI - GitHub - kudoleh/iOS-Clean-Architecture-MVVM: Tem...
github.com
[iOS] 3. Clean Architecture + MVVM ๊ฐ๋ ํ์คํ๊ฒ ์ดํดํ๊ธฐ (Actor๊ฐ Entity๋ฅผ ๋ฐ๊ธฐ๊น์ง)
Actor๊ฐ Entity๋ฅผ ๋ฐ๊ธฐ๊น์ง View๋ ViewModel์ ๋ฉ์๋๋ฅผ ํธ์ถ viewModel์ useCase ์คํ > useCase๋ Repository(DB or Network)์ ๋ฐ์ดํฐ ์์ฒญ Repository์์ cache์ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ๋ฐ๋ก ํ๋, ์์ผ๋ฉด memory cache, disk cac
ios-development.tistory.com
๐ง๐ปโ๏ธ PRND iOSํ์ UseCase ํ์ฉ๊ธฐ
PRND iOSํ์์๋ Clean Architecture๋ฅผ ๋์ ํด ์ฌ์ฉํ๊ณ ์๊ณ , ๊ณ์ธต ๊ฐ ๊ด์ฌ์ฌ ๋ถ๋ฆฌ๋ฅผ ํตํด ๋ง์ ์ด์ ์ ์ฒด๊ฐํ๊ณ ์์ต๋๋ค.
medium.com
'๐ iOS > Architecture' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Architecture] Clean Architecture (1) | 2024.01.03 |
---|---|
[Architecture] VIPER ํจํด (1) | 2022.04.15 |
[Architecture] MVC, MVP, MVVM ์ํคํ ์ฒ ํจํด (2) | 2022.03.25 |