✍️ 오늘의 내용
오늘은 상반기 프로젝트를 진행하면서 적용해본 Clean Architecture에 대해서 작성해보았습니다.
개발자라면 한 번쯤은 읽어봤을 그 책을, 저도 공부해보고 실제 프로젝트에 적용해보고 싶다는 생각이 들었습니다. 그래서 이번 프로젝트에서 처음으로 시도해보았고, 운 좋게도 함께 개발하시는 분이 초고수셔서 많은 도움을 받고 있습니다. 😊
제가 정답을 말하는 건 아니지만, 공부한 내용을 바탕으로 정리해보았습니다.
참고용으로 가볍게 봐주시면 감사하겠습니다!
1. CleanArchitecture이란?
Clean Architecture는 로버트 C. 마틴(Robert C. Martin)이 고안한 소프트웨어 아키텍처입니다.
그는 미국의 소프트웨어 엔지니어이자 에자일(Agile) 창시자 중 한 명,
그리고 객체지향 설계 원칙인 SOLID 개념을 만든 인물로, 개발자들 사이에서는 “밥 아저씨(Uncle Bob)” 라는 별명으로도 잘 알려져 있습니다.
Clean Architecture의 핵심은 소프트웨어 구조를 계층화하여 관심사를 분리하는 것입니다.
즉, 각 계층이 독립적이고 명확한 책임을 갖도록 설계하고,
이를 통해 계층 간의 의존성을 최소화하는 것을 목표로 합니다.
그림에서 안쪽 원들은 바깥 쪽 원에 대해서 몰라야 합니다.! 이것은 Dependency Rule에 관한 것 입니다!
2. Dependency Rule - 의존성은 안쪽을 향해야 한다
Clean Architecture를 공부하면서 가장 인상 깊었던 개념 중 하나가 바로 Dependency Rule입니다.
이 규칙은 계층 간 의존성의 방향을 정해주는 아주 중요한 원칙입니다.
🔧 의존성(Dependency)은 항상 안쪽 계층을 향해야 한다
의존성은 Frameworks → InterfaceAdapters → UseCases → Entities 방향으로만 흐를 수 있습니다.
반대로, 안쪽 계층(Entities, UseCases)은 바깥 계층에 대해 몰라야 합니다.
- 즉, UseCase는 UIKit이나 Alamofire 같은 외부 라이브러리를 직접 사용해서는 안 됩니다.
- Entity는 DB, 네트워크, UI에 대해 아무것도 몰라야 합니다.
❓ 왜 이게 중요할까?
바깥 계층은 변화가 자주 일어나는 영역입니다.
- UI 프레임워크 교체
- DB 라이브러리 변경
- 외부 API 수정 등
이런 변화가 핵심 비즈니스 로직(UseCase, Entity) 에 영향을 준다면, 아키텍처 전체가 흔들리고 유지보수 난이도가 폭증합니다.
그래서 의존성의 방향을 안정적인 영역을 향하도록 제한하는 것이죠.
📚 적용 방법 정리
계층 | 참조 가능한 대상 | 절대 참조하면 안 되는 대상 |
역할 | 예시 |
Frameworks & Drivers | InterfaceAdapters | UseCases, Entities 직접 참조 ❌ |
기술적 구현 (UI 프레임워크, DB, API 호출 등) |
Retrofit, Room, UIKit/ Jetpack Compose 등 |
InterfaceAdapters | UseCases | Frameworks & Drivers, Entities 직접 사용 ❌ |
외부 데이터 변환 / UI 연동 / DB 연동 (중간 Adapter 역할) |
ViewModel → UseCase 호출, DTO ↔ Entity 변환 |
UseCases | Entities | InterfaceAdapters, Frameworks ❌ |
앱 기능 로직 담당 (“무엇을 할 것인가”) |
회원가입 처리, 결제 처리 등 |
Entities | 없음 (완전 독립) | 모든 다른 계층 ❌ |
핵심 규칙과 상태만 보관 (회사 정책, 도메인 규칙) |
User, Order 등 도메인 모델 |
“변화가 많은 것(Framework, UI 등)이 변화가 적은 것(정책, UseCase, Entity)에 영향을 주면 안 된다.” → 이 원칙이 바로 Clean Architecture에서 안정성을 지키는 가장 핵심적인 원칙입니다.
2. Clean Architecture 4가지 구성
Clean Architecture는 소프트웨어를 명확한 관심사 별로 분리하기 위해, 4개의 계층으로 구성되어 있습니다.
① Entities (Enterprise Business Rules)
Clean Architecture에서 가장 중심이 되는 계층은 바로 Entities입니다.
Entities는 시스템의 가장 핵심적인 비즈니스 규칙과 상태를 표현하는 계층이며,
이 계층은 외부 기술(UI, DB, API) 변화에 영향을 받지 않아야 합니다.
❓ Entities 정의
회사 정책, 도메인 규칙, 핵심 개념을 담고 있으며, 외부에 의존하지 않는 완전히 독립적인 객체”
🔧 Entities의 역할
- 핵심 비즈니스 개념을 가장 변화가 적은 형태로 코드화
- UseCase 실행 중 필요한 데이터, 도메인 규칙 적용/검증을 담당
- DB, API, UI 등에 절대 의존하지 않음
- 재사용성과 테스트 용이성을 극대화
- 정책(Policy) 그 자체를 코드로 표현한 계층
📚 Entities의 예시
제가 참고한 예제는 iOS-Clean-Architecture-MVVM 예제입니다.
아래와 같은 Movie 앱이 있다고 가정해볼게요.
이 앱에서 사용자에게 보여주는 영화 정보(Movie)는 어떤 형태로든 화면에, API로, DB로 전달되겠지만…
그와 관계없이 “영화”라는 개념 자체는 변하지 않죠. 그것이 바로 Entity입니다!
struct Movie: Equatable, Identifiable {
typealias Identifier = String
enum Genre {
case adventure
case scienceFiction
}
let id: Identifier
let title: String?
let genre: Genre?
let posterPath: String?
let overview: String?
let releaseDate: Date?
}
이 구조체는 UI를 몰라도 되고 API 응답 구조도 몰라도 되고 DB 스키마도 몰라도 됩니다.
그저 “영화”라는 개념에 대해 정책/정의만 갖고 있는 순수 데이터 구조입니다.
② UseCases (Application Business Rules)
Clean Architecture에서 두 번째로 중요한 계층은 UseCases입니다.
UseCase는 말 그대로 “앱에서 제공해야 하는 기능/시나리오”를 구현하는 계층이며,
Entity를 이용해 실제로 어떤 일이 일어날지 정의하는 로직의 중심입니다.
❓ UseCases 정의
- 앱이 제공해야 하는 기능/행위의 흐름을 담당합니다.
- “사용자가 어떤 행동을 하면 어떤 결과가 나와야 하는가”를 정의한 코드입니다.
- Entity를 조합하거나 검증하며 비즈니스 규칙을 실행합니다.
- ViewModel이나 Controller 등 상위 계층에서 호출되며, Repository를 통해 데이터를 받아 Entity로 변환하여 처리합니다.
🔧 UseCases의 역할
- 기능 흐름 구현이며 Entity 사용하여 특정 기능 처리
- 복잡한 조건, 정책 판단 및 적용
- Repository를 통해 Data Layer에 요청하고 Entity로 변환 후 결과 반환
이렇게 흐름이 구성되며, UseCase는 외부 구현을 몰라도 로직만 테스트할 수 있도록 독립적으로 유지됩니다.
📚 UseCases의 예시
📁 위치: Domain/UseCases/FetchMoviesUseCase.swift
protocol FetchMoviesUseCase {
func execute(
requestValue: FetchMoviesUseCaseRequestValue,
cached: @escaping ([Movie]) -> Void,
completion: @escaping (Result<[Movie], Error>) -> Void
) -> Cancellable?
}
✅ 구현체 예시
final class DefaultFetchMoviesUseCase: FetchMoviesUseCase {
private let moviesRepository: MoviesRepository
init(moviesRepository: MoviesRepository) {
self.moviesRepository = moviesRepository
}
func execute(requestValue: FetchMoviesUseCaseRequestValue,
cached: @escaping ([Movie]) -> Void,
completion: @escaping (Result<[Movie], Error>) -> Void) -> Cancellable? {
return moviesRepository.fetchMovies(query: requestValue.query,
page: requestValue.page,
cached: cached,
completion: completion)
}
}
ViewModel은 UseCase를 호출만 하고, 내부 구현에 대해 몰라도 됩니다. → 의존성 방향이 안쪽(UseCase) 으로만 흐릅니다.
UseCase는 외부 API/DB를 직접 알지 않음 → Repository를 통해 데이터를 받고
UseCase는 Entity를 사용해 결과를 처리한 뒤, 다시 ViewModel(Interface Adapter)에 결과를 넘겨줍니다.
③ InterfaceAdapters
Clean Architecture에서 세 번째 계층은 InterfaceAdapters입니다.
이 계층은 안쪽 계층(UseCase, Entity)와 바깥쪽 계층(UI, Framework) 사이에서 데이터를 변환하고 흐름을 연결하는
중간 계층입니다. 즉, UseCase의 결과를 사용자에게 보여줄 수 있도록 가공하고, 사용자 액션을
UseCase로 연결하는 다리 역할을 합니다.
❓ InterfaceAdapters 정의
- 안쪽 계층(UseCase, Entity) ↔ 바깥쪽 계층(UI, Framework)을 연결하는 브리지 역할
- 외부에서 들어오는 데이터를 Entity로 변환
- Entity → ViewModel 등 UI가 사용할 수 있는 형태로 가공
🔧 InterfaceAdapters의 역할
- API/DB에서 받아온 DTO → Entity,
- Entity → View에서 사용할 ViewModel로 변환
- UI에서 발생한 사용자 액션을 받아 UseCase를 호출
- UseCase에서 받은 데이터를 화면 표현용 데이터로 가공
- Repository 구현체가 주로 이 계층에 위치 (Data Layer에 해당)
- UseCase는 Framework를 모르고, Framework는 Entity를 직접 쓸 수 없기에 중간에서 가공해 연결
📚 InterfaceAdapters의 예시
ViewModel → InterfaceAdapter 역할(📁 위치: Presentation/MoviesList/MoviesListViewModel.swift)
final class MoviesListViewModel: MoviesListViewModelInput, MoviesListViewModelOutput {
private let fetchMoviesUseCase: FetchMoviesUseCase
init(fetchMoviesUseCase: FetchMoviesUseCase) {
self.fetchMoviesUseCase = fetchMoviesUseCase
}
func didSearch(query: String) {
self.fetchMoviesUseCase.execute(requestValue: .init(query: query, page: 1),
cached: { movies in
self.update(movies)
},
completion: { result in
// 성공 시 업데이트
})
}
}
→ ViewModel이 UseCase만 의존하고 호출 → 결과를 화면 표시용 데이터로 가공해서 View에 전달
Repository 구현체 → InterfaceAdapter 역할(📁 위치: Data/Repositories/DefaultMoviesRepository.swift)
final class DefaultMoviesRepository: MoviesRepository {
private let dataTransferService: DataTransferService
func fetchMovies(query: String, page: Int, ...) -> Result<[Movie], Error> {
// API 호출 → MovieResponseDTO 응답
// DTO → Entity(Movie) 변환 후 반환
}
}
→ Repository 구현체는 APIClient 호출 → DTO → Entity 변환 후 UseCase에 Entity로 반환
InterfaceAdapters는 UseCase/Entity ↔ UI/Framework 사이에서 데이터를 변환하고 흐름을 연결하는 중간 계층입니다.
👉 ViewModel, Repository 구현체, DTO↔︎Entity Mapper가 모두 이 계층에 포함됩니다.
④ Frameworks & Drivers
Frameworks & Drivers는 우리가 앱에서 직접적으로 사용하는 기술들(프레임워크, 라이브러리, 디바이스 등) 이 들어가는 계층입니다.
❓ Frameworks & Drivers 정의
- 구체적인 기술적 구현(Framework, Library, Device, Driver 등) 이 존재하는 계층
- 바깥쪽 계층으로 → 기술 변화가 가장 많음 → 다른 계층은 이 계층에 의존하면 ❌ 안됨.
- 보통 “실제 동작을 담당하는” 코드가 들어감
- 기술 교체가 가능해야 함 → 이 계층의 변경이 안쪽 계층(UseCase, Entity)에 영향을 주면 안됨.
🔧 Frameworks & Drivers의 역할
역할 | 설명 | 예시 기술 |
UI 프레임워크 구현 | 화면 구성 담당 | UIKit, SwiftUI, AppKit 등 |
네트워크 라이브러리 | 외부 API 요청 처리 | Alamofire, URLSession, Moya 등 |
DB 라이브러리 사용 | 데이터 저장/조회 | Realm, CoreData, SQLite 등 |
외부 서비스 연동 |
푸시 알림, 사용자 분석 등의 외부 연동 처리 | Analytics, Push Notification 등 |
디바이스 기능 연동 |
카메라, GPS, 센서 등 하드웨어 기능 연동 | 카메라, 센서, GPS 등 |
📚 Frameworks & Drivers의 예시
📁 위치: MoviesListViewController.swift
final class MoviesListViewController: UIViewController {
private var viewModel: MoviesListViewModel!
func searchButtonTapped(query: String) {
viewModel.didSearch(query: query)
}
}
- UIKit 기반 ViewController는 버튼 클릭, 스크롤 등의 사용자 입력(UI 이벤트) 을 처리하는 코드만 담당합니다.
- 이 ViewController는 내부적으로 ViewModel(InterfaceAdapter) 을 호출하지만, 반대로 ViewModel은 UIKit을 전혀 몰라야 합니다. (의존 금지!)
Frameworks & Drivers는 구체적 기술 구현이 들어가는 계층이며, 기술 변화가 가장 많기 때문에 UseCase/Entity는 이 계층에 의존하면 안 되고, InterfaceAdapters를 통해 간접적으로만 연결된다.
3. Present/Domain/Data Layer
실무에서는 팀원 간 협업을 위해 위와 같이 Presentation, Domain, Data Layer로 구분하여 역할을 분담합니다.
① Presentation Layer (Domain Layer을 의존)
❓Presentation Layer의 정의
- 화면(View)에 보여질 데이터를 준비하는 계층
- 사용자 입력을 받아 Domain Layer를 호출하고, 화면에 표시할 형태로 데이터를 변환함
- MVVM 구조에서는 View + ViewModel이 여기에 해당
❓왜 ViewModel이 Presentation Layer에 속하는가?
- ViewModel은 비즈니스 로직을 포함하지 않고, 오직 화면에 필요한 데이터 가공 및 전달만 담당
- UseCase로부터 데이터를 받아와 UI에 맞게 변환하여 View에 전달
- 즉, ViewModel은 Domain Layer를 참조하지만, Domain Layer는 ViewModel을 몰라야 하므로 의존성 규칙(Dependency Rule) 유지
🔧 Presentation Layer의 역할
- 버튼 클릭, 검색 입력 등 UI 이벤트 처리
- UseCase 호출 → 데이터 요청
- 결과(Entity)를 UI-friendly 데이터로 가공
- View에 전달하거나 바인딩
📚 Presentation Layer의 예시
📁 위치: Presentation/MoviesList/MoviesListViewModel.swift
final class MoviesListViewModel {
private let fetchMoviesUseCase: FetchMoviesUseCase
init(fetchMoviesUseCase: FetchMoviesUseCase) {
self.fetchMoviesUseCase = fetchMoviesUseCase
}
func didSearch(query: String) {
self.fetchMoviesUseCase.execute(requestValue: .init(query: query, page: 1),
cached: { movies in
self.update(movies)
},
completion: { result in
// 성공 시 UI 업데이트용 데이터 구성
})
}
}
ViewModel은 Domain Layer (UseCase, Entity)를 호출해 데이터를 받고, UI에 맞게 가공하여 View에 전달하는 Presentation Layer의 핵심 구성요소입니다. 비즈니스 로직은 처리하지 않고, UI 표시용 데이터 준비만 담당 합니다.
② Domain Layer
❓Domain Layer의 정의
- 앱의 핵심 비즈니스 로직이 담긴 계층
- Clean Architecture에서 가장 변경에 강해야 하고 의존받지 않아야 하는 핵심
- “앱이 무엇을 하는가?”에 대한 본질적 정의를 내리는 계층
- 구성 요소: Entities, UseCases, Repository Interface
🔧 Domain Layer의 역할
- 앱 기능이 어떤 식으로 동작해야 하는지 정의 (UseCase)
- 도메인 모델(Entity)에 비즈니스 상태와 규칙을 구현
- 외부 구현(DB, API 등)과 분리된 Repository Interface 정의
- Framework/UI/DB 등의 변화에 영향 받지 않도록 의존성 역전 원칙 적용
📚 Domain Layer의 예시
Entities 예시(📁 위치: Domain/Entities/Movie.swift)
struct Movie {
let id: String
let title: String
let genre: Genre
let releaseDate: Date
}
UseCase 예시(📁 위치: Domain/UseCases/FetchMoviesUseCase.swift)
protocol FetchMoviesUseCase {
func execute(...) -> Cancellable?
}
Repository Interface 예시(📁 위치: Domain/Repositories/MoviesRepository.swift)
protocol MoviesRepository {
func fetchMovies(...) -> Result<[Movie], Error>
}
Domain Layer는 외부 변화에 영향을 받지 않으며, 앱이 “무엇을 해야 하는지” 를 정의하는 가장 중요한 계층이다. View나 DB가 어떻게 생겼든 상관없이, 핵심 로직은 여기서 보호받아야 한다.
③ Data Layer
❓ Data Layer의 정의
- 외부 데이터 소스(API, DB 등) 와 통신을 담당하는 계층
- 구성 요소: Repository 구현체 (Repository Implementation), Data Source (API / DB 등)
- Domain Layer의 Repository Interface를 구현하여 UseCase가 원하는 데이터를 제공
- Domain Layer는 이 데이터가 어디서 왔는지 몰라도 됨 → 의존성 역전 원칙
🔧 Data Layer의 역할
- Repository Interface를 구현하여 데이터 제공
- 외부 API/DB에 실제 요청을 보내고 결과를 받아옴
- 받은 데이터(DTO)를 → Entity로 변환해서 전달
- 비즈니스 로직은 Domain Layer에서만 담당하고, Data Layer는 데이터 전달만 책임
📚 Data Layerr의 예시
Repository Implementation 예시(📁 위치: Data/Repositories/DefaultMoviesRepository.swift)
final class DefaultMoviesRepository: MoviesRepository {
private let dataTransferService: DataTransferService
func fetchMovies(...) -> Result<[Movie], Error> {
// API 호출 → MovieResponseDTO 받음
// DTO → Movie(Entity)로 변환 후 반환
}
}
Data Source 예시 (📁 위치: Data/Network/DataTransferService.swift)
final class DefaultDataTransferService: DataTransferService {
func request(...) {
// URLSession 사용해서 API 호출
}
}
Data Layer는 외부에서 받은 데이터를 Entity로 변환해서 Domain Layer에 전달하는 역할만 수행하고 비즈니스 로직은 전혀 포함하지 않고, 오직 데이터 조회/변환/연결에만 집중한다.
🧩 마무리
이렇게 클린 아키텍처의 기본 개념과 각 계층의 역할에 대해 공부한 내용을 정리해보았습니다.
제가 정리한 내용이 정답은 아니지만, 참고용으로 봐주시면 감사하겠습니다!
다음 포스팅에서는 이번에 공부한 내용을 실제 프로젝트에 어떻게 적용해보았는지를 소개해보려고 해요.
앞으로도 꾸준히 기록하고, 성장하는 개발자가 되기 위해 한 걸음씩 달려보겠습니다! 🚀
읽어주셔서 감사합니다 😊
참고한 블로그는 아래에 함께 남겨두도록 하겠습니다.
https://velog.io/@yoosa3004/iOS-CleanArchitecture-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0
[iOS] CleanArchitecture 톺아보기
CleanArchitecture 공부하면서 헷갈렸던 용어들과각 layer들에 그래서 정확히 어떤게 들어가야 할지Clean Architecture and MVVM on iOS 참고해서 정리했습니다.CleanArchitecture은 로머트 C 마틴이 고안한 아키텍처
velog.io
https://medium.com/@hyosing92/ios-cleanarchitecture-mvvm-e1b390b18e83
[iOS] CleanArchitecture + MVVM
클린아키텍처와 MVVM을 사용하면서 생각한 점입니다.
medium.com
https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3
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
https://zeddios.tistory.com/1065
Clean Architecture
안녕하세요 :) Zedd입니다. Zedd신곡 나왔는데...엄청 좋아요 갓제드ㅠ 이거들으면서 쓰는 중 오늘은..클린 아키텍쳐 + MVVM에 대해서 공부를 해보려고 합니다! 클린 아키텍쳐는 한번쯤은 들어보셨을
zeddios.tistory.com
https://github.com/kudoleh/iOS-Clean-Architecture-MVVM
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