개요
- 네이버 부스트캠프 마스터 클래스, 미션을 수행하는 과정에서 아키텍처에 관한 이야기를 많이 접하게 되었다.
- 기능 구현에만 급급하던 상황을 보냈다.
- 다시 한 번 SOLID 원칙을 점검하며 리팩토링을 하는 과정을 수행하는 과정에서
- 아키텍처에 대해서도 공부를 한 뒤, 적용해보고 싶다는 생각을 하게 되었다.
- 당장 미션을 위한 것이라면 기능만 돌아가도록 하면 된다.
- 그치만, 지식을 적립하는 과정이 더욱 중요하다는 생각을 하게 된 것 같다.
- 이럴려고 네부캠 신청한 거 아니겠어?!
우선, 처음 접한 지식과 이를 적용하려는 과정에서 제가 이해한 바를 나타낸 것이기 때문에, 오류나 피드백이 있다면 언제나 환영입니다 :)
소프트웨어 아키텍처?
- 최근들어 주위에서 아키텍처란 말을 많이 듣게 된 것 같다.
- 이는 무엇을 나타내는 걸까?
- 소프트웨어 아키텍처는 소프트웨어의 ‘구조’를 의미한다.
- 즉, 시스템의 구성 요소와 그 상호작용을 정의하는 틀이다.
- 왜 중요성이 대두되게 되었나?
- 소프트웨어는 크게 ‘기능’, ‘구조’로 나눌 수 있다.
- 우리는 보통 ‘기능’ 구현에 집중하는 경향이 있다.
- “되기만 하면 끝!” 과 같은 판단은 추후 확장, 테스트, 빌드 과정에서 많은 시간과 노력을 잡아먹는다.
기능은 사용자에게 직접적인 가치를 제공하지만, 이러한 기능들이 어떻게 구성되고 유지되는지가 장기적으로 더 중요할 수도 있다.
- 로버트 C.마틴은 소프트웨어 아키텍처에서 기능보다 구조가 더 중요하다고 말한다.
즉, 요약을 하자면
- 소프트웨어는 ‘기능’, ‘구조’로 나눠 볼 수 있고
- 기능은 사용자가 직접 경험하는 부분으로, 특정 작업을 수행하는 소프트웨어의 동작
- 구조는 이러한 기능을 어떻게 구현하고 조직하는지에 관한 설계
- 코드가 잘 정리되고 모듈화되어, 유지보수와 확장이 용이하도록 만드는 역할이다.
그래서 아키텍처의 중요성을 정의하자면
좋은 아키텍처의 중요성
- 기능은 구조에 의존한다
- 좋은 구조 없이는 기능을 추가하거나 수정하는 것이 어렵고 비효율적이다.
- 구조가 잘 잡혀 있어야 새로운 기능을 쉽게 추가하고, 기존 기능을 빠르게 수정할 수 있다.
- 좋은 아키텍처의 목표
- 소프트웨어 아키텍처의 핵심 목표는 시스템을 효율적으로 만들고, 시간이 지나면서 유지보수에 드는 비용을 최소화하는 것이다.
- 구조가 잘 잡혀 있을수록 코드 수정에 대한 비용이 줄어든다.
- 비용: 시간, 인건비
클린 아키텍처란?
- 로버트 C. 마틴(Robert C. Martin)이 제안한 아키텍처 원칙으로
- 소프트웨어 시스템을 더 유연하고 유지보수하기 쉽게 만들기 위한 구조적 방법론이다.
- 다양한 아키텍처 패턴들을 통합하여, 소프트웨어를 여러 계층으로 나누고, 각 계층에 명확한 책임을 부여함으로써 시스템을 독립적이고 견고하게 설계하는 것을 목표로 하는 패턴이다.
- 주요 개념
- 관심사의 분리
- 소프트웨어를 계층으로 나누어 관심사를 분리하자!
- 이를 통해 각 계층이 서로에게 의존하지 않고 독립적으로 동작할 수 있게 한다.
- 예) UI, 비즈니스 로직은 다른 계층에 존재하여, 하나가 변경되어도 다른 곳에서는 영향을 받지 않게함.
- 의존성 규칙
- 의존성은 항상 내부로 향해야 한다.
- 즉, 외부 계층은 내부 계층에 의존할 수 있지만, 내부 계층은 외부 계층에 대해 알지 못한다.
- 관심사의 분리
입문해보기
- 우선 내가 진행하고 있는 프로젝트에서의 적용을 시도해보았다.
- MVVM으로 구현중이었다.
- 모든 기능을 처음부터 적용해보려 하지 않고, 미션 요구사항 중 특정 화면에서의 적용부터 시도해보았다.
💡 간략히 프로젝트의 내용을 설명하면, ‘깃허브 이슈를 받아와서 화면에서 출력하는 기능’ 화면이다.
- 기존 나의 설계였다.
- 사용자가 화면에 진입한다.
- API요청을 한다.
- 응답을 받는다.
- 응답된 데이터를 가공해서 화면에 보여줄 정보만 남긴 뒤
- UI를 업데이트 한다.
- 해당 설계의 문제점은 뭘까?
- 우선 ViewModel의 역할부터 생각해보자.
- View들의 데이터를 보관하고 관리하는 역할을 수행
- 또한 데이터를 ViewModel에서 관리하기 때문에 UI와 데이터 간의 응집도 또한 낮출 수 있게 된다.
- 하지만 나의 ViewModel은 추가적인 책임을 가지고 있다.
- 데이터 접근 로직: GithubAPICaller를 직접 호출하여 데이터를 가져오는 책임
- 네트워크 통신 관리: API 요청과 응답 처리를 직접 관리
- 데이터 변환: API로부터 받은 데이터를 뷰에 적합한 형태로 변환
- 에러 핸들링: API 호출 시 발생할 수 있는 오류 처리
- 즉, 단일 책임 원칙을 위반함의 문제를 대표적으로 관찰 할 수 있다.
- 우선 ViewModel의 역할부터 생각해보자.
- 우선 요구사항을 정리해보았다.
- 해당 화면에서 요구사항이 뭔데?
- 이슈 List를 보여주는 메인 화면이다.
- 해당 화면에서의 세부적인 기능이 뭔데?
- 이슈 리스트를 출력하는 것이다.
- 과정이 어떻게 되는데?
- 화면이 로딩 → API로 데이터를 요청 → 받아온다 → 필요한 정보들을 Cell로 만든다 → 보여준다.
- 해당 화면에서 요구사항이 뭔데?
- Presentation, Domain, Data Layer로 나눠서 다이어그램을 수정해보았다.
- 데이터의 흐름은 아래와 같다.
- View가 ViewModel의 method 호출
- ViewModel이 UseCase 실행
- UseCase가 Repository의 data를 조합
- Repository는 Network에서 data 받아와 리턴
- View에 Information 표시
- 데이터의 흐름은 아래와 같다.
- 이전 다이어그램과의 차이가 무엇일까?
- 계층, 책임 분리
- Domain Layer (UseCase)
- 핵심 비즈니스 로직을 포함, 외부 의존성 없이 독립적
- Data Layer (Repository)
- 데이터 접근을 추상화, Domain Layer에서 정의한 인터페이스를 구현
- Presentation Layer (View, ViewModel)
- UI 관련 로직을 처리, UseCase를 통해 비즈니스 로직에 접근
- Domain Layer (UseCase)
- 계층, 책임 분리
- 프로젝트 디렉토리 세팅 결과
- 트리 구조는 아래와 같다.
├── IssueTracker
│ ├── IssueTracker
│ │ ├── Application
│ │ │ ├── AppDelegate.swift
│ │ │ ├── DIContainer
│ │ │ │ └── AppDIContainer.swift
│ │ │ └── SceneDelegate.swift
│ │ ├── Data
│ │ │ ├── Network
│ │ │ │ ├── DTO
│ │ │ │ │ ├── IssueRequestDTO+Mapping.swift
│ │ │ │ │ └── IssueResponseDTO+Mapping.swift
│ │ │ │ └── GithubAPICaller.swift
│ │ │ ├── Repositoires
│ │ │ │ └── GithubIssueRepository.swift
│ │ │ └── Storages
│ │ ├── Domains
│ │ │ ├── Entities
│ │ │ │ ├── BaseEntities
│ │ │ │ │ ├── Issue.swift
│ │ │ │ │ ├── Label.swift
│ │ │ │ │ └── Milestone.swift
│ │ │ │ ├── IssueCellItem.swift
│ │ │ │ ├── IssueDetail.swift
│ │ │ │ └── IssueInit.swift
│ │ │ ├── Interfaces
│ │ │ │ └── Repositories
│ │ │ │ └── IssueRepository.swift
│ │ │ └── UseCases
│ │ │ ├── CreateIssue
│ │ │ │ └── CreateIssueInitUseCase.swift
│ │ │ └── FetchIssue
│ │ │ └── FetchIssueUseCase.swift
│ │ ├── Presentation
│ │ │ ├── Base.lproj
│ │ │ ├── Features
│ │ │ │ ├── CreateIssue
│ │ │ │ │ ├── CreateIssueDetail
│ │ │ │ │ │ ├── View
│ │ │ │ │ │ ├── ViewController
│ │ │ │ │ │ │ └── CreateIssueDetailViewController.swift
│ │ │ │ │ │ └── ViewModel
│ │ │ │ │ │ └── CreateIssueDetailViewModel.swift
│ │ │ │ │ ├── CreateIssueInit
│ │ │ │ │ │ ├── EnterTitle+Content
│ │ │ │ │ │ │ ├── View
│ │ │ │ │ │ │ │ └── CreateIssueCellItems.swift
│ │ │ │ │ │ │ ├── ViewController
│ │ │ │ │ │ │ │ └── CreateIssueInitViewController.swift
│ │ │ │ │ │ │ └── ViewModel
│ │ │ │ │ │ │ └── CreateIssueInitViewModel.swift
│ │ │ │ │ │ └── SelectManager
│ │ │ │ │ │ ├── View
│ │ │ │ │ │ │ └── SelectManagerTableViewCell.swift
│ │ │ │ │ │ ├── ViewController
│ │ │ │ │ │ │ └── SelectManagerViewController.swift
│ │ │ │ │ │ └── ViewModel
│ │ │ │ │ │ └── SelectManagerViewModel.swift
│ │ │ │ │ └── SelectManagerFeature
│ │ │ │ │ ├── ViewControllers
│ │ │ │ │ │ └── SelectManagerViewController.swift
│ │ │ │ │ ├── ViewModels
│ │ │ │ │ │ └── SelectManagerViewModel.swift
│ │ │ │ │ └── Views
│ │ │ │ │ └── SelectManagerTableViewCell.swift
│ │ │ │ └── MainIssueList
│ │ │ │ ├── View
│ │ │ │ │ └── IssueTableViewCell.swift
│ │ │ │ ├── ViewController
│ │ │ │ │ └── MainViewController.swift
│ │ │ │ └── ViewModel
│ │ │ │ └── MainViewModel.swift
│ │ │ └── Utils
│ │ │ └── Extensions
│ │ │ └── UIFont+Extension.swift
│ │ └── Resources
│ │ ├── ApiKey.xcconfig
│ │ ├── Assets.xcassets
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ └── codeSquad.imageset
│ │ │ ├── Contents.json
│ │ │ └── codeSquad.jpg
│ │ ├── Base.lproj
│ │ │ └── LaunchScreen.storyboard
│ │ ├── Fonts
│ │ │ ├── SF-Pro-Italic.ttf
│ │ │ └── SF-Pro.ttf
│ │ └── Info.plist
의존성 방향
의존성 역전 구현 방법
[github Issue를 fetch하는 과정]
1. 추상화 정의
- Domain Layer에 위치
protocol IssueRepository {
func fetchIssues() -> AnyPublisher<[Issue], Error>
// ...
}
2. 구체적인 구현(해당 프로토콜을 구현하는 구체 클래스 작성)
- Data Layer에서 구체적으로 구현
class GithubIssueRepository: IssueRepository {
private let apiCaller: GithubAPICaller
init(apiCaller: GithubAPICaller) {
self.apiCaller = apiCaller
}
func fetchIssues() -> AnyPublisher<[Issue], Error> {
// 실제 구현...
}
}
3. Use Case 정의(Use Case는 추상화된 IssueRepository
에 의존)
- Domain Layer에 위치
protocol FetchIssuesUseCase {
func execute() -> AnyPublisher<[Issue], Error>
}
class DefaultFetchIssuesUseCase: FetchIssuesUseCase {
private let repository: IssueRepository
init(repository: IssueRepository) {
self.repository = repository
}
func execute() -> AnyPublisher<[Issue], Error> {
return repository.fetchIssues()
}
}
4. ViewModel 구현(ViewModel은 Use Case에 의존)
- Presentation Layer가 Use Case에 의존
final class MainViewModel {
private let fetchIssuesUseCase: FetchIssuesUseCase
init(fetchIssuesUseCase: FetchIssuesUseCase) {
self.fetchIssuesUseCase = fetchIssuesUseCase
}
func fetchIssues() {
fetchIssuesUseCase.execute()
// 결과 처리...
}
}
우당탕탕 마치며…
- 아키텍처를 처음 접하게 되면서 … 많은 시간을 투자해봤던 거 같다.
- Layer간의 관계
- 책임 분리
- 의존성 주입 방법
- …
- 어려웠다.
- 다음 글은 이를 통해 추가 기능 개발 구현, 테스트 과정에서 어떠한 이득을 얻을 수 있었는지에 대해 정리해보려고 한다!
참고 링크
[NHN FORWARD 22] 클린 아키텍처 애매한 부분 정해 드립니다.
[NHN FORWARD 22] Dooray! 모바일 앱의 클린 아키텍처 적용기
Clean Architecture and MVVM on iOS (강추)
Clean Architecture and MVVM on iOS
When we develop software it is important to not only use design patterns, but also architectural patterns. There are many different…
tech.olx.com
iOS의 Clean Architecture | Hohyeon Moon
iOS의 Clean Architecture | Hohyeon Moon
UIKit/SwiftUI를 사용하는 iOS 개발에서 Clean Architecture 사용하기
www.hohyeonmoon.com
[iOS - swift] clean architecture를 적용한 MVVM 개념 맛보기
[iOS - swift] clean architecture를 적용한 MVVM 개념 맛보기
MVVM 구조 Presentation Layer: View + ViewModel ViewModel은 UI이벤트가 발생하면 '무엇'을 해야하는지 알고, UseCase를 요청 후 View에 업데이트 알림 역할 Domain Layer: UseCase + Model 비즈니스 로직 계층 저장소에 관
ios-development.tistory.com
'iOS' 카테고리의 다른 글
단방향 / 양방향 데이터 흐름 (0) | 2024.11.10 |
---|---|
MVVM, Binding (0) | 2024.10.30 |
추상화, 그리고 타입 캐스팅? (0) | 2024.09.10 |
의존성, 의존성 주입?(2) (2) | 2024.09.09 |
의존성, 의존성 주입(1) ? (2) | 2024.09.09 |