프로젝트/Tempus

클린아키텍처로 각 계층의 관심사 분리

코르피 2023. 8. 25. 16:29
반응형

 

목차

  • 클린아키텍처에 대한 설명
  • 프로젝트 적용사례

 

 

클린아키텍처

클린아키텍처는 로버트 C. 마틴 아저씨가 고안한 아키텍처이다.

모든 아키텍처의 목표는 계층을 분리하고 관심사를 분리하고 의존도를 줄여 기능이 추가되거나 수정되는 등의 유지보수를 쉽게 하고,

다른 계층에 인터페이스에 의존하여 결합도를 줄이고 독립적인 테스트가 가능하기 위함이다.

로버트 마틴은 이 목표들을 하나로 모아 '클린아키텍처'로 정리했다.

 

클린아키텍처는 한마디로 모듈과 의존성을 체계적으로 구성하는 아키텍처이다. 

 

 

클린 아키텍처 모식도

 

이 아키텍처의 핵심은 "바깥원의 구역은 안쪽원의 구역을 의존하며 안쪽원의 구역은 바깥원의 구역을 알 수 없다." 이다.

로버트 C. 마틴은 코드를 작성하다보면 더 많은 Layer가 필요할 것이라 한다.

바깥이 안쪽을 의존한다는 규칙만 지켜지면 Layer가 계속 늘어나더라도 괜찮다.

 

Layer를 구성할 때 체크해야할 것

  • 하나의 타입이 하나의 역할을 하고 있는가
  • 다른 타입과의 상호작용은 protocol에 의존하고 있는가

위 두가지만 확실하게 지킨다면 클린한 아키텍처가 될 것이라 생각한다.

 

각 Layer의 간단한 설명이다. 다시말하지만 각 Layer는 필요시 늘어나도 괜찮다.

Enterprise Business Rules

제일 안쪽의 원 영역이다.

이 영역은 Entity로 전체 앱의 비즈니스로직을 얘기한다.

이 개체는 메서드를 가질수도, 데이터 구조나 함수집합일 수도 있다.

 

가장 일반적이고 높은수준의 규칙을 캡슐화 하며 가장 변화할 가능성이 적은 부분이다.

 

서로 다른 여러 애플리케이션이 여기에 접근를 해서 사용한다면 문제가 되지 않고

만약 iOS 같은 하나의 애플리케이션에서 이를 사용한다면 이것이 Application Business Rules가 될 수 있다.

 

Application Business Rules

대체로 iOS에서 클린아키텍처를 사용한다면 이부분이 가장 변하지 않는 부분이 될 것이다.

(iOS 하나만 지원하기 때문에, iPad나 watch를 지원한다면 달라질 수 있다.)

 

앱에서 사용하는 전체 비즈니스 로직이 여기에 담긴다. UseCase로 사용을 하며

앱의 운영정책에 따라 이 부분이 바뀔 수 있다.

 

 

Interface Adapters

MVC, MVVM 등의 모든 부분이 여기에 속한다.

어떤 데이터(View에서 받은 데이터)는 ViewController에서 같은계층인 VIewModel로 전달되고 값을 모델로 만들어서

다음 계층인 UseCase로 전달된 다음, 어떤 작업을 수행한 뒤

다시 다른 ViewController나 Presenter, 데이터베이스 등으로 흘러간다.

 

그렇기 때문에 ViewController, ViewModel에서는 다른 ViewController나, ViewModel, Presenter, DB에 관련된 것을 몰라야한다.

 

 

 

Frameworks & Drivers

가장 바깥쪽 계층은 UI, DB와 같은 프레임워크가 들어간다.

iOS에서는 View가 이부분에 위치하는데 ViewController로 함께 묶여있으므로 View 부분만 따로 떼내어 볼 수 있고

CoreData같은 데이터베이스 프레임워크가 이부분에 위치할 수 있다.

 

 

 

프로젝트 적용사례

 

위의 내용들을 모두 프로젝트에 녹여내려 노력했다.

 

 

 

우선 LayerArchitecture를 이용해 Presentation, Domain, Data로 나누어 파악을 시작했다.

ViewController, ViewModel 등의 Interface AdaptersPresentation으로,

UseCase는 Application Business Rules 으로써 Domain 으로

CoreDataRepositoryInterface Adapters 로써 데이터베이스에 저장하는 역할로써 Data 영역,

나머지 CoreData는 실제 DB로써 FrameWorks & Driver 에 속한다.

 

 

전체 앱의 프로세스.

ViewController 에서 값들이 ViewModel과 바인딩 되어있고 어떤 비즈니스 정책(생성, 삭제, 타이머 시작 등)에 대한 이벤트가 들어오면

Domain 영역의 UseCase에게 요청하여 작업을 수행한 뒤 DataMangerRepository protocol을 통해 데이터베이스에 저장한다.

또한 ViewModel이 다른 ViewModel과 상호작용이 필요하다면 viewModel 사이에 protocol을 통해 작업을 수행할 수 있도록 했다.

 

프로젝트 코드

BlockEditViewModel

func transform<InputType, OutputType>(input: InputType, disposeBag: DisposeBag) -> OutputType? {
    guard let input = input as? Input else {
        return nil
    }

    let editUseCaseInput = BlockEditUseCase.Input(modelEditEvent: self.doneButtonTapEvent)
    let editUseCaseOutput = blockEditUseCase.transform(input: editUseCaseInput,
                                                       disposeBag: disposeBag)
    let output = Output(...)

    bindDoneButtonTapEvent(input.doneButtonTapEvent, disposeBag)
    // ... 다른 bindMethod

    return output as? OutputType
}

func bindDoneButtonTapEvent(_ doneButtonTapEvent: Observable<Void>, _ disposeBag: DisposeBag) {
    doneButtonTapEvent
        .subscribe(onNext: { [weak self] in
            guard let self else { return }
            self.doneButtonTapEvent.onNext(self.originModel)
        }).disposed(by: disposeBag)
}

func bindEditSuccess(_ isEditSuccess: PublishSubject<Bool>, _ disposeBag: DisposeBag) {
    isEditSuccess
        .subscribe(onNext: { [weak self] isSuccess in
            guard let self else { return }
            if isSuccess {
                self.fetchRefreshDelegate?.refresh()
                self.editReflectDelegate?.reflect(self.originModel)
            }
        }).disposed(by: disposeBag)
}

 

프로젝트 내 BlockEditViewModel의 일부코드를 가져왔다. 위 코드는 블록 모드의 수정완료 버튼이 클릭되었을 때의 코드이다.

 

Input, Output만 보고 작동흐름을 파악할 수 있다는 장점 때문에 Input, Output에 대한 개념을 UseCase에도 적용해보았다.

 

BlockViewController에 대한 수정버튼 클릭 이벤트를 BlockEditViewModel에서 바인딩 할 때,

ViewModel 과 UseCase도 바인딩을 해주었다.

 

 

리스트화면 -> 상세화면 -> 수정화면의 플로우를 가지고 있다.

 

수정완료버튼을 클릭하면 현재 viewModel이 가지고 있는 값들을 모델로 만들어 EditUseCase에 보낸다.

이후 성공 한다면 리스트화면에서 값을 다시 반영할 수 있도록 fetchRefreshDelegate를 통해 새로 반영해주고

수정화면이 완료되었을 때, 상세화면또한 수정되어 있어야 하므로 상세화면을 editReflectDelegate를 통해 수정된 모델로 반영한다.

 

여기서 fetchRefreshDelegate, editReflectDelegate가 viewModel이 UseCase에서 들고있어도 괜찮다고 생각했다.

하지만 UseCase가 최대한 Entity로써의 역할을 지키고 수정이 별로 되지 않아야 하며 외부 뷰 조작에 관여하면 안된다고 생각이 들어

ViewModel에서 가지도록 했다.

반응형