[iOS] 단디 개발일지 #1 - Clean Architecture 적용

kimdocs...📄·2023년 5월 24일
0

개발일지

목록 보기
1/1
post-thumbnail

2023 글로벌미디어학부 졸업작품으로 출품했던 "단디" 제작 과정에 대해 기록해두려고 합니다.

팀 인원은 백앤드 1명, 디자인 1명, iOS 1명 총 3명으로 이루어져 있으며 저는 iOS 개발을 맡았습니다.

"단디"를 기획하게 된 배경은 Repository README에 잘 들어나있는 것 같아서, 이 블로그에서는 어떤 기술적인 도전이 있었는 지에 초점을 맞추어서 작성해볼게요!


졸업작품을 하며 제일 중요하게 생각했던 부분은 최대한 클린하게 아키텍쳐 구조를 가져가는 것 이었습니다.

제가 생각한 "클린"의 기준은 변경사항에 유연해야 하며, 객체가 하나의 역할만 담당해야하고, 아키텍쳐 레이어 분리를 통해 Testable한 구조를 가지는 것 입니다.

졸업작품에서 클린아키텍쳐까지 적용하는 게 배보다 배꼽이 더 크다고 느껴질 수 있습니다. 개발 기간이 3개월이 되지 않고, iOS 개발은 저 혼자 해야했으니까요!

그렇지만 졸업작품이니 더더욱 클린아키텍쳐를 적용해봐야겠다고 생각했습니다.

  • 쉬도 때도 없이 바뀌는 기획
  • 기획에 따라 자주 변경되는 디자인
  • API Response가 확정되지 않은 서버

이 모든 변경사항에 유연하게 대처하고, 제 갈길 갈 수 있는 방법은 아키텍쳐 구조를 초반에 잘 다지는 것이었습니다.

그리하여 클린아키텍쳐를 기반으로 개발하기 시작했습니다.

Clean Architecture

Dandi 프로젝트의 아키텍쳐 구조는 위 사진과 같습니다.


Presentation

사용자 인터페이스(UI)와 관련된 부분을 담당합니다. iOS 어플리케이션에서는 View, ViewController, Storyboard 등이 프레젠테이션 레이어에 해당합니다. 이 레이어는 사용자의 입력을 받아 처리하고, 필요한 데이터를 도메인 레이어로 전달하거나 도메인 레이어에서 반환된 결과를 UI에 표시합니다. 프레젠테이션 레이어는 사용자 인터페이스와 관련된 로직을 포함하고 있으며, UI의 상태 관리와 사용자 입력 처리를 담당합니다.

View

사용자에게 보여질 View를 정의합니다. View는 독립적으로 설계했습니다.

Controller

Reactor와의 Data Binding을 통해 데이터의 흐름을 관리합니다.

Reactor

Controller로 부터 이벤트를 받아 UseCase에게 이벤트를 전달하고, UseCase에게 받은 데이터를 View에 맞게 가공하는 역할을 합니다.


Domain

이 레이어는 iOS 어플리케이션의 핵심 비즈니스 로직을 담당합니다. Repository Protocol, UseCase, Entity 등을 포함하며, 애플리케이션의 핵심 로직을 구현합니다. 이 레이어는 프레젠테이션 레이어와 데이터 레이어와의 의존성을 최소화하고, 독립적으로 테스트하고 변경할 수 있도록 설계되어야 합니다.

UseCase & Repository Protocol

repository 의존성을 주입받아 Reactor로 가공된 데이터를 전달합니다.

final class DefaultChatUseCase: ChatUseCase {
    private let gptRepository: GPTRepository

    init(gptRepository: GPTRepository) {
        self.gptRepository = gptRepository
    }

    func chat(content: String) -> Observable<ChatMessage?> {
        return gptRepository.chat(content: content)
            .map { result in
                switch result {
                case let .success(message):
                    return message
                case .failure:
                    return nil
                }
            }
    }
}

UseCase에 Repository를 주입받으려면 Repository를 Domain 레이어에 위치해야하지 않냐는 의문점이 생길 수도 있습니다.
Repository를 Domain레이어에 위치시키면 Data와 Domain이 쌍방 의존이 생기게 됩니다. 이는 클린 아키텍쳐에 어긋나는 개념인 것이죠 !

그리하여 Repository 구현체는 Data에 위치하도록 하고, Repository는 Domain 레이어에 위치하게 함으로써 의존성 역전을 통해 Data가 Domain에게만 의존하는 단방향 의존성을 만듭니다.

이렇게 구현하면, 주입받는 Repository에 따라 UseCase의 기능이 한층 더 자유로워질 수 있습니다.

자유로워진다는 의미가 무엇일까요?

예를 들어, PostListRepository가 있다고 봅시다.

PostListRepository
protocol PostListRepository {
	func getList() -> Observable<[Post]>
}

이 PostListRepository Protocol을 따르는 객체를 주입받는 PostListUseCase가 있습니다.

PostListUseCase
final class PostListUseCase {
	init(postListRepository: PostListRepository) {
    	~~~~
        postListRepository.getList()
        ~~~
    }
}

PostListRepository을 따르는 Repository 구현체를 만듭니다. 서버에서 List를 받아오는 경우와, 내부 DB에서 List를 받아오는 경우 두가지를 만들어볼게요!

ServerPostListRepository
final class ServerPostListRepository:  PostListRepository{
	func getList() -> Observable<[Post]> {
    	// 서버에서 Post List 를 가져오는 구현
    }
}
RealmPostListRepository
final class ServerPostListRepository:  PostListRepository{
	func getList() -> Observable<[Post]> {
    	// 내부 DB에서 Post List 를 가져오는 구현
    }
}

이제 어느 Repository를 주입해주느냐에 따라 PostListUseCase가 서버 혹은 Realm으로부터 데이터를 받아올 수 있습니다. 나중에 CoreData가 생겼다고 할지라도 PostListRepository 프로토콜을 만족하는 CoreDataRepository를 만들어 PostListUseCase에 주입해주면 그만입니다!

이를 통해 Domain Layer에 위치한 Repository protocol과 UseCase의 변경사항 없이 의존성 주입을 통해 UseCase의 기능에 변화를 줄 수 있습니다. 감이 잡히시나요?!?

Entity

어플리케이션 내부에서 쓰일 모델을 정의합니다. 어느 레이어에 종속적이어선 안됩니다.


Data

로컬 데이터베이스, 서버, 파일 시스템 등과의 상호작용을 관리하고, 데이터의 저장, 검색, 조작 등을 처리합니다. iOS 어플리케이션에서는 Repository 구현체, Core Data, Realm, 네트워크 라이브러리 등이 데이터 레이어의 구성요소로 사용될 수 있습니다. 데이터 레이어는 도메인 레이어와의 인터페이스(Repository)를 통해 데이터를 주고 받습니다.

Network

네트워크 통신 관련 모델(ResponseDTO)를 정의하고, 네트워크 통신을 위한 Service를 구현합니다.

Repository

Domain에 정의된 Repository Protocol을 구현합니다.

DataMapping

DTO 별로 Domain에 정의된 Entitiy로 변경해줍니다.
저는 DTO안에 extension을 통해 함께 작성해주었습니다!

struct TokenDTO: Decodable {
    let accessToken: String
    let refreshToken: String
}

extension TokenDTO {
    func toDomain() -> Token {
        return Token(accessToken: accessToken, refreshToken: refreshToken)
    }
}

더 자세한 코드는 https://github.com/2023-dandi/dandi-iOS 에서 확인하실 수 있습니다.

profile
👩‍🌾 GitHub: ezidayzi / 📂 Contact: ezidayzi@gmail.com

0개의 댓글