-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
지속 가능한 iOS 앱 개발을 위한 아키텍처 설계와 모듈화 전략에 대해 설명해주세요. #29
Comments
지속 가능한 iOS 앱 개발을 위한 아키텍처 설계와 모듈화 전략에 대해 설명해주세요.
모듈화의 장점은 다양합니다.
위에서 설명한 모듈화를 잘 하기 위해선 앱을 개발하고 유지 보수하는 동안 확장성, 유지 관리성, 재사용성 등을 고려하여 설계해야 하며, 몇가지 중요한 원칙과 전략을 설명하자면..
위의 전략을 적용해서 아래에 간단한 MVVM패턴의 코드를 작성해보겠습니다. // 모델
struct Item {
let id: UUID
let name: String
}
// 뷰모델
class ItemListViewModel: ObservableObject {
@Published var items: [Item] = []
func fetchItems() {
// 네트워크 요청을 통해 데이터 패치
}
}
// 뷰
struct ItemListView: View {
@ObservedObject var viewModel: ItemListViewModel
var body: some View {
List(viewModel.items, id: \.id) { item in
Text(item.name)
}
.onAppear {
viewModel.fetchItems()
}
}
}
// 메인 앱
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ItemListView(viewModel: ItemListViewModel())
}
}
} Clean Architecture, VIPER 등의 아키텍처 패턴과 적용 방법을 소개해주세요.클린 아키텍처(Clean Architecture)는 시스템의 각 요소들을 명확하게 분리하면서도, 유연하게 연결될 수 있도록 디자인 하는 SW 설계 구조 입니다. 클린아키텍처의 가장 큰 특징들을 나열해보자면..
클린 아키텍처는 소프트웨어 시스템의 다양한 부분을 독립적인 구성 요소로 분리하는 것을 강조하기에, 시간이 지남에 따라 시스템을 더 쉽게 유지 관리할 수 있습니다.
클린 아키텍처는 모듈식 설계를 권장합니다. 이는 시스템의 개별 구성 요소를 분리 해 주고, 테스트와 유지보수를 쉽게 만들어 줍니다.
클린 아키텍처는 시스템 구축에 사용되는 기본 기술과 요구사항의 변화를 수용할 수 있는 확장 가능한 설계를 제공합니다.
클린 아키텍처는 여러 시스템에서 재사용 가능한 컴포넌트를 만드는 것을 장려합니다. 이이 따라, 개발자가 다른 SW혹은 기능을 개발 할 때 구축하는 데 필요한 시간과 노력이 줄어들게 됩니다.
클린 아키텍쳐의 개념은 단순하고, 이해 하기도 굉장히 간단합니다. 워낙 개념이 이해하기 쉽기에 여러 개발자들과 IT 스타트업들이 도입을 시도했고, 하나의 큰 트렌드로 이어지게 되었습니다. 위 사진은 클린 아키텍처에 대한 그래프입니다. 간단히 설명하면,
Clean Architecture의 다른 패턴과의 차이점에 대해선 제가 이전에 정리해둔 글이 있어 자세히 보고 싶으면 여기 글에 가서 보시면 더 편하게 볼 수 있습니다. Viper패턴은 위 클린 아키텍처 구조를 iOS에 맞게 변형하여 대중화되어 많이 사용되고 있는 패턴입니다. View, Interactor, Presenter, Entity, Router의 약자를 따와서 VIPER라는 이름이 명명된 단일책임원칙 기반의 클린 아키텍쳐입니다.
패턴에 대한 설명은 위 설명이 전부이고, 아래에 예시 코드들을 보겠습니다. Viewimport Foundation
import UIKit
// ViewController
// protocol
// reference presenter
protocol AnyView {
var presenter: AnyPresenter? { get set }
func update(with users: [User])
func update(with error: String)
}
class UserViewController: UIViewController, AnyView {
var presenter: AnyPresenter?
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self,
forCellReuseIdentifier: "cell")
table.isHidden = true
return table
}()
private let label: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.isHidden = true
return label
}()
var users: [User] = []
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
view.addSubview(label)
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
label.frame = CGRect(x: 0, y: 0, width: 200, height: 50)
label.center = view.center
}
func update(with users: [User]) {
print("got users")
DispatchQueue.main.async {
self.users = users
self.tableView.reloadData()
self.tableView.isHidden = false
}
}
func update(with error: String) {
print(error)
DispatchQueue.main.async {
self.users = []
self.label.text = error
self.tableView.isHidden = true
self.label.isHidden = false
}
}
}
extension UserViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = users[indexPath.row].name
return cell
}
} Interactorimport Foundation
// object
// protocol
// ref to presenter
// https://jsonplaceholder.typicode.com/users
protocol AnyInteractor {
var presenter: AnyPresenter? { get set }
func getUsers()
}
class UserInteracotr: AnyInteractor {
var presenter: AnyPresenter?
func getUsers() {
print("Start fetching")
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else { return }
let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
guard let data = data, error == nil else {
self?.presenter?.interactorDidFetchUsers(with: .failure(FetchError.failed))
return
}
do {
let entities = try JSONDecoder().decode([User].self, from: data)
self?.presenter?.interactorDidFetchUsers(with: .success(entities))
} catch {
self?.presenter?.interactorDidFetchUsers(with: .failure(error))
}
}
task.resume()
}
} Presenterimport Foundation
// Object
// protocol
// ref to interactor, router, view
enum FetchError: Error {
case failed
}
protocol AnyPresenter {
var router: AnyRouter? { get set }
var interactor: AnyInteractor? { get set }
var view: AnyView? { get set }
func interactorDidFetchUsers(with result: Result<[User], Error>)
}
class UserPresenter: AnyPresenter {
var router: AnyRouter?
var interactor: AnyInteractor? {
didSet {
interactor?.getUsers()
}
}
var view: AnyView?
func interactorDidFetchUsers(with result: Result<[User], Error>) {
switch result {
case .success(let users):
view?.update(with: users)
case .failure:
view?.update(with: "Something went wrong")
}
}
} Entityimport Foundation
// Model
struct User: Codable {
let name: String
} Routerimport Foundation
// Object
// Entry point
import UIKit
typealias EntryPoint = AnyView & UIViewController
protocol AnyRouter {
var entry: EntryPoint? { get }
// func stop()
// func route(to destination)
static func start() -> AnyRouter
}
class UserRouter: AnyRouter {
var entry: EntryPoint?
static func start() -> AnyRouter {
let router = UserRouter()
// Assign VIP
var view: AnyView = UserViewController()
var presenter: AnyPresenter = UserPresenter()
var interactor: AnyInteractor = UserInteracotr()
view.presenter = presenter
interactor.presenter = presenter
presenter.router = router
presenter.view = view
presenter.interactor = interactor
router.entry = view as? EntryPoint
return router
}
} Viper패턴을 적용했을 때의 장점
Viper패턴의 단점
위 Viper패턴의 단점들을 해결하기 위해 나온 패턴인 Ribs패턴에 대해서는 다음에 알아보도록 하겠습니다. 기능 모듈화, 라이브러리 모듈화 등을 통한 코드 재사용성과 유지보수성 향상 방안을 제시해주세요.
기능 모듈화는 앱의 기능을 별도의 모듈로 분리하여 개발하는 접근 방식입니다. 각 모듈은 특정 기능 또는 부분을 담당하고, 이를 독립적으로 테스트하고 재사용할 수 있습니다. 예를 들어, 게시판이나 채팅 기능을 담당하는 모듈을 만들 수 있습니다.
라이브러리 모듈화는 공통 기능이나 유틸리티를 담당하는 모듈을 만드는 것입니다. 이러한 모듈은 다른 앱이나 프로젝트에서도 재사용될 수 있습니다. 예를 들어, 네트워킹 기능을 담당하는 라이브러리를 만들 수 있습니다. 라이브러리 모듈화 예시 : Hsungjin/SnipImage |
의존성 주입, 인터페이스 분리 등의 설계 원칙을 적용한 모듈 간 느슨한 결합 방법을 설명해주세요.의존성 주입의존성 주입은 위에서 설명하였듯이 클래스나 모듈이 직접 생성하는 의존성을 제거하고 외부에서 주입받도록 만드는 디자인 패턴입니다. 모듈이 필요로 하는 의존성을 외부에서 주입받도록 하기에 모듈은 직접 의존성을 생성하거나 초기화하지 않고 외부에서 주입받아 사용할 수 있습니다. import Foundation
protocol DataFetcher {
func fetchData(url: URL) async throws -> Data
}
class NetworkManager: DataFetcher {
static let shared = NetworkManager()
func fetchData(url: URL) async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
struct DataRepository {
let dataFetcher: DataFetcher
func fetchData(url: URL) async throws -> Data {
return try await dataFetcher.fetchData(from: url)
}
}
let networkManager = NetworkManager.shared
let dataRepository = DataRepository(dataFetcher: networkManager)
do {
let url = URL(string: "https://github.com/hamfan524")!
let data = try await dataRepository.fetchData(from: url)
// 데이터 사용
} catch {
print("에러처리하시면 됩니다.")
}
인터페이스 분리인터페이스 분리는 클라이언트가 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 분리하는 것입니다.
// 데이터 로드 프로토콜
protocol DataLoader {
func loadData() -> [String]
}
// 데이터 저장 프로토콜
protocol DataSaver {
func saveData(data: [String])
}
// 파일로부터 데이터를 로드하는 클래스
class FileDataLoader: DataLoader {
func loadData() -> [String] {
// 파일에서 데이터를 로드하는 로직
return ["Data1", "Data2", "Data3"]
}
}
// 데이터를 파일에 저장하는 클래스
class FileDataSaver: DataSaver {
func saveData(data: [String]) {
// 데이터를 파일에 저장하는 로직
}
}
// 네트워크 통신으로 데이터 받아오는 클래스
class NetworkDataLoader: DataLoader {
func loadData() -> [String] {
// 네트워크 통신으로 데이터 받아오는 로직
return ["DataA", "DataB", "DataC"]
}
}
// 데이터를 네트워크에 저장하는 클래스
class NetworkDataSaver: DataSaver {
func saveData(data: [String]) {
// 데이터를 네트워크에 저장하는 로직
}
}
// 데이터 처리 모듈
class DataManager {
let dataLoader: DataLoader
let dataSaver: DataSaver
init(dataLoader: DataLoader, dataSaver: DataSaver) {
self.dataLoader = dataLoader
self.dataSaver = dataSaver
}
func processData() {
let data = dataLoader.loadData()
// 데이터 처리 로직
dataSaver.saveData(data: data)
}
}
// 파일에서 데이터를 로드하고 파일에 저장하는 경우
let fileDataLoader = FileDataLoader()
let fileDataSaver = FileDataSaver()
let fileDataManager = DataManager(dataLoader: fileDataLoader, dataSaver: fileDataSaver)
fileDataManager.processData()
// 네트워크에서 데이터를 로드하고 네트워크에 저장하는 경우
let networkDataLoader = NetworkDataLoader()
let networkDataSaver = NetworkDataSaver()
let networkDataManager = DataManager(dataLoader: networkDataLoader, dataSaver: networkDataSaver)
networkDataManager.processData() 위 코드에서는 그리고 이렇게 함으로써 각 모듈은 인터페이스에만 의존하고, 구현체에는 의존하지 않는 느슨한 결합이 된 코드를 작성 가능합니다. |
Clean Architecture, VIPER 등의 아키텍처 패턴과 적용 방법을 소개해주세요.
기능 모듈화, 라이브러리 모듈화 등을 통한 코드 재사용성과 유지보수성 향상 방안을 제시해주세요.
의존성 주입, 인터페이스 분리 등의 설계 원칙을 적용한 모듈 간 느슨한 결합 방법을 설명해주세요.
The text was updated successfully, but these errors were encountered: