클린 아키텍처를 알기 위해서는 MVVM을 알아야 하고 MVVM을 알기 위해서는 SOLID 원칙을 알아야 한다. SOLID 원칙 -> MVVM -> Clean Architecture 순서로 정리하였다.

ex) ViewConntroller의 책임(책임 = 변경이 일어날 때 영향을 받는다라고 가정)
=> 이 4개중에 하나의 책임만 가져야 한다. 여기서는 UI 그리기가 책임이 된다.

ViewModel에서 User데이터에 대한 타입이 변경되면 ViewModel에 의존하는 ViewController가 영향을 받는다. 하지만 ViewController에서 UI에 대한 코드가 변경되면 안된다.
/*
MARK: - 개방 폐쇄 원칙
- viewController가 ViewModelProtocol을 의존하므로 ViewModel 내부 구현이 변경되어도 영향이 없다.
- ex) FriendUser -> FamilyUser로 바뀌어도 ViewController에서 UI코드 변경 필요 없다.
- ViewController 코드를 수정하지 않고 기능을 확장할 수 있다.
*/
protocol UserProtocol { }
protocol ViewModelProtocol {
func getUserList() -> [UserProtocol]
}
struct FriendUser: UserProtocol { }
struct FamilyUser: UserProtocol { }
class ViewModel: ViewModelProtocol {
// ViewController는 각 객체가 어떤 구체 타입인지 몰라도 공통 인터페이스(UserProtocol)로 처리 가능하다.
func getUserList() -> [UserProtocol] {
return [FriendUser(), FamilyUser()]
}
}

사진과 같이 ViewController가 property1과 func3만 사용한다면 ViewController입장에서 나머지 property2, func1, func2는 필요 없다. 이럴때는 인터페이스를 분리해야 한다.

저수준은 UI와 같이 잘 바뀌는 코드를 의미하고 고수준은 앱에서 핵심 기능인 잘 안바뀌는 기능을 의미한다.(쇼핑앱에서는 결제 기능과 유사)
즉 쉽게 바뀌는 코드가 쉽게 바뀌지 않는 코드를 의존해야 한다.
참고로 추상화(인터페이스)에 의존해야한다.
// MARK: - Model
protocol UserProtocol {
var name: String { get }
}
struct FriendUser: UserProtocol {
let name: String = "친구 유저"
}
struct FamilyUser: UserProtocol {
let name: String = "가족 유저"
}
// MARK: - ViewModelProtocol(고수준의 추상화)
// 핵심 로직 담당, UserProtocol 추상화에만 의존
protocol ViewModelProtocol {
var users: [UserProtocol] { get }
}
// 고수준
class ViewModel: ViewModelProtocol {
var users: [UserProtocol] {
return [FriendUser(),FamilyUser()]
}
}
// MARK: - View(저수준)
// UI 담당, ViewModel의 추상화(ViewModelProtocol)에만 의존
class View {
private let viewModel: ViewModelProtocol // 고수준 추상화에 의존
init(viewModel: ViewModelProtocol) {
self.viewModel = viewModel
}
func render() {
for user in viewModel.users {
print("👤 이름: \(user.name)")
}
}
}
// MARK: - 실행
let viewModel = ViewModel()
let view = View(viewModel: viewModel)
view.render()
class Bird {
func fly() {
print("날아갑니다!")
}
}
class Penguin: Bird {
override func fly() {
// 펭귄은 날 수 없음 → LSP 위반
fatalError("펭귄은 날 수 없습니다!")
}
}
func letBirdFly(_ bird: Bird) {
bird.fly()
}
letBirdFly(Bird()) // ✅ "날아갑니다!"
letBirdFly(Penguin()) // ❌ 런타임 에러!
protocol Bird {
func move()
}
class FlyingBird: Bird {
func move() {
print("날아갑니다!")
}
}
class WalkingBird: Bird {
func move() {
print("걸어갑니다!")
}
}
func letBirdMove(_ bird: Bird) {
bird.move()
}
letBirdMove(FlyingBird()) // ✅ 날아갑니다!
letBirdMove(WalkingBird()) // ✅ 걸어갑니다!

SOLID 원칙을 지키기 위해 MVVM 패턴을 사용한다. MVVM패턴을 지키면 자연스럽게 SOLID 원칙을 지킬 수 있다. MVVM은 Model, View, ViewModel로 3등분으로 구성된다.
| 구성 요소 | 역할 |
|---|---|
| Model | 데이터와 비즈니스 로직 (API 통신, 데이터 가공 등) |
| ViewModel | View에 필요한 상태와 로직을 관리 (UI 상태 결정, 이벤트 처리) |
| View (UI) | 사용자 입력, 화면 표시 (이벤트는 ViewModel로 전달) |
// MARK: - Model
struct User {
let name: String
}
// MARK: - ViewModel (구체 타입에 직접 의존)
class UserViewModel {
private let user: User
init(user: User) {
self.user = user
}
var displayName: String {
"👤 \(user.name)"
}
}
// MARK: - View (ViewModel의 구체 타입에 의존함 → DIP 위반)
class UserView {
private let viewModel: UserViewModel // ❌ 구체 클래스에 의존
init(viewModel: UserViewModel) {
self.viewModel = viewModel
}
func render() {
print(viewModel.displayName)
}
}
// MARK: - 실행
let user = User(name: "김동현")
let viewModel = UserViewModel(user: user)
let view = UserView(viewModel: viewModel)
view.render()
// 출력: 👤 김동현
// MARK: - Model
struct User {
let name: String
}
// MARK: - ViewModel 추상화 (DIP 적용)
protocol UserViewModelProtocol {
var displayName: String { get }
}
// MARK: - ViewModel 구현체
class UserViewModel: UserViewModelProtocol {
private let user: User
init(user: User) {
self.user = user
}
var displayName: String {
"👤 \(user.name)"
}
}
// MARK: - View (프로토콜에 의존함 → DIP 만족)
class UserView {
private let viewModel: UserViewModelProtocol // ✅ 프로토콜에 의존
init(viewModel: UserViewModelProtocol) {
self.viewModel = viewModel
}
func render() {
print(viewModel.displayName)
}
}
// MARK: - 실행
let user = User(name: "김동현")
let viewModel = UserViewModel(user: user)
let view = UserView(viewModel: viewModel)
view.render()
// 출력: 👤 김동현
클린아키텍처를 이해하기위해 인터페이스 개념을 다시 한번 알고 가자.

ViewController, 내부 로직은 ViewModel로 비유할 수 있다.ViewController는 “냉장고 온도를 내려주세요”라고 요청만 한다.ViewModel이 처리한다.
ViewController가 ViewModel을 직접 구현이 아닌 인터페이스(Protocol)에 의존하면,이렇게 구현을 감추고 필요한 기능만 정의한 것을 **추상화**라고 한다.
인터페이스를 사용하는이유 = 의존성을 최소화 하기 위해 = 변경을 대응하기 위함.

모바일 뿐만 아니라 웹이나 서버 등 여러 곳에서 많이 사용하는 아키텍처이다. 모바일 기준으로는 조금 단순화한 아래 사진을 참고하면 된다.

모바일 기준으로는 크게 3개의 계층(Layer)로 분류한다. Domain Layer, Data Layer, Presentation Layer로 분류한다.
Domain Layer는 프로젝트의 가장 핵심적인 영역이며,
비즈니스의 규칙과 요구사항을 직접 담고 있는 계층이다.
UseCase와 Data Layer 사이를 연결하는 추상화 계층UseCase는 몰라도 된다UseCase는 데이터가 어디서 오는지 알 필요 없다.
Repository가 데이터의 출처를 감싸서 대신 처리해준다.
UseCase는 오직 기능 수행에만 집중 가능Repository는 인터페이스만 존재하며, 실제 구현은 Data Layer에서 처리한다.