์๋ ํ์ธ์ ์ ์ธ์ ๋๋ค :)
์ต๊ทผ์ ์งํํ ๊ฐ์ธ ํ๋ก์ ํธ์ ์ํคํ ์ฒ๋ฅผ 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์ ๋ํ ๋ชจํธํ ๊ฐ๋ ์ ํ์คํ ์ก์ ์ ์์๋ ๊ฒ ๊ฐ์ต๋๋ค.
๊ธด ๊ธ ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค๐๐
[์ฐธ๊ณ ์๋ฃ]
'๐ iOS > Architecture' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Architecture] Clean Architecture (1) | 2024.01.03 |
---|---|
[Architecture] VIPER ํจํด (1) | 2022.04.15 |
[Architecture] MVC, MVP, MVVM ์ํคํ ์ฒ ํจํด (2) | 2022.03.25 |