SOLOD, MVVM, CleanArchitecture

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

원칙이란?

이미지


SOLID원칙

1. 단일 책임

ex) ViewConntroller의 책임(책임 = 변경이 일어날 때 영향을 받는다라고 가정)

=> 이 4개중에 하나의 책임만 가져야 한다. 여기서는 UI 그리기가 책임이 된다.


2. 개팡 폐쇄

이미지
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()]
    }
}



3. 인터페이스 분리 원칙

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


4. 의존성 역전 원칙

이미지
저수준은 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()



5. 리스코프 치환원칙

리스코프 치환 위반 예시

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()) // ✅ 걸어갑니다!


MVVM

이미지

SOLID 원칙을 지키기 위해 MVVM 패턴을 사용한다. MVVM패턴을 지키면 자연스럽게 SOLID 원칙을 지킬 수 있다. MVVM은 Model, View, ViewModel로 3등분으로 구성된다.

그림 상세내용(참고)

MVVM 구성

구성 요소 역할
Model 데이터와 비즈니스 로직 (API 통신, 데이터 가공 등)
ViewModel View에 필요한 상태와 로직을 관리 (UI 상태 결정, 이벤트 처리)
View (UI) 사용자 입력, 화면 표시 (이벤트는 ViewModel로 전달)

🔁 의존성 방향

✅ SOLID 원칙 적용 전 MVVM 코드 (DIP 위반)


// 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()
// 출력: 👤 김동현

✅ SOLID 원칙 적용 후 MVVM 코드 (DIP, OCP 등 만족)


// 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()
// 출력: 👤 김동현

Interface

클린아키텍처를 이해하기위해 인터페이스 개념을 다시 한번 알고 가자.

인터페이스란?

❄️ 냉장고로 예시를 들어본다면

이미지

인터페이스를 정리하자면

이미지

이렇게 구현을 감추고 필요한 기능만 정의한 것**추상화**라고 한다.
인터페이스를 사용하는이유 = 의존성을 최소화 하기 위해 = 변경을 대응하기 위함.


Clean Architecture

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

커스텀셀
모바일 기준으로는 크게 3개의 계층(Layer)로 분류한다. Domain Layer, Data Layer, Presentation Layer로 분류한다.


Domain Layer

Domain Layer는 프로젝트의 가장 핵심적인 영역이며,
비즈니스의 규칙과 요구사항을 직접 담고 있는 계층이다.

구성 요소

왜 Repository를 사용하는가?