Clean Architecture[iOS] 공부하기

PectoralisHeok·2024년 2월 15일

공부하기전에

개발을 하다보니 내가 짜는 방식이 맞나 라는 생각을 하루에도 수십번씩 하는 것 같다.
다른사람들이 짜놓은 프로젝트들을 보면 파일들도 많고 방식들이 많이 다른 느낌을 받는다.
결국 가장 정답에 가까운 프로젝트를 보고 공부를 했다.

크게 Domain, Data, Presentation layer 가 있다

Domain Layer

가장 안쪽에 있는 Layer이다. Layer간의 규칙은 안쪽 Layer은 바깥 Layer에 있는 것들을 몰라야 된다. 즉 안쪽 Layer에서 바깥쪽 Layer에 의존하면 안된다.
도메인안에는 Entity, Use Cases, Repository Interface가 있다.

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?
}

Use Case

사용자의 기능 또는 시스템의 기능을 나타냄
특정한 작업이나 기능이 어떻게 등장했는지 기술한다

final class FetchRecentMovieQueriesUseCase: UseCase {

  struct RequestValue {
      let maxCount: Int
  }
  typealias ResultValue = (Result<[MovieQuery], Error>)

  private let requestValue: RequestValue
  private let completion: (ResultValue) -> Void
  private let moviesQueriesRepository: MoviesQueriesRepository

  init(
      requestValue: RequestValue,
      completion: @escaping (ResultValue) -> Void,
      moviesQueriesRepository: MoviesQueriesRepository
  ) {

      self.requestValue = requestValue
      self.completion = completion
      self.moviesQueriesRepository = moviesQueriesRepository
  }

  func start() -> Cancellable? {

      moviesQueriesRepository.fetchRecentsQueries(
          maxCount: requestValue.maxCount,
          completion: completion
      )
      return nil
  }
}

Repository interface

비즈니스 로직과 데이터베이스나 외부 데이터 소스 사이의 중재 역할을 합한다.
1.데이터 소스 추상화
2.비즈니스 로직 분리
3.테스트 용이성
데이터 소스를 추상화하여 데이터가 바뀌어도 비즈니스 로직에는 영향이 없다. 또한 DB나 API에 의존하지 않고 테스트가 가능하다.

protocol UserRepository {
    func getUserByID(_ userID: String, completion: @escaping (User?) -> Void)
    func saveUser(_ user: User, completion: @escaping (Error?) -> Void)
}

Presentation Layer

ViewModel

ViewModel은 UI에 표시될 데이터와 화면 상태를 관리하는 역할을 합니다. 도메인의 Entity를 활용하거나, 도메인과 Presentation Layer 간의 매핑을 담당합니다.

class MovieListViewModel {
    // 도메인에서 가져온 데이터를 가공해서 UI에 표시할 데이터로 변환
    func transform(_ movies: [Movie]) -> [MovieCellViewModel] {
        return movies.map { MovieCellViewModel(movie: $0) }
    }
}

View

View는 화면을 구성하고, 사용자의 입력을 감지하는 역할을 한다.
ViewModel을 통해 데이터를 받아오고, 사용자의 입력에 따라 ViewModel에 액션을 전달한다

class MovieListView: UIViewController {
    var viewModel: MovieListViewModel!

    func setupUI() {
        // UI 구성
    }

    func bindViewModel() {
        // ViewModel과의 바인딩
    }
}

Coordinator

종종 Presentation Layer에는 Coordinator 패턴이 사용된다
Coordinator는 화면 전환을 관리하고, 다른 화면으로의 이동을 처리한다.

class AppCoordinator {
    func showMovieListScreen() {
        let movieListViewController = MovieListView()
        movieListViewController.viewModel = MovieListViewModel()
        // 화면 전환 로직
    }
}

Dependency Rule

Presentation Layer에서 주목해야 할 중요한 규칙 중 하나는 의존성 규칙이다. Presentation Layer는 외부 Layer에 의존하면 안 되며, 안쪽 Layer는 Presentation Layer에 의존하지 않아야 한다. 이렇게 함으로써 시스템의 유지보수성과 확장성이 향상되며, 코드의 응집도가 높아짐.

Presentation Layer에서 Domain Layer의 Use Case를 직접 호출하지 않고, Use Case를 통해 데이터를 가져와 처리하는 것이 중요합니다. Presentation Layer는 사용자 인터페이스와 관련된 로직만을 담당해야 하며, 비즈니스 로직은 Domain Layer에 위임하는 것이 좋습니다.

이러한 구조를 유지함으로써 Presentation Layer는 단순하고 테스트 가능한 코드를 유지하면서, 도메인 로직은 안정적으로 변경 가능한 코드를 유지할 수 있음.

Data Layer

Data Layer는 앱의 데이터 관리와 관련된 부분을 담당하고 있음
여기에는 데이터의 로컬 저장소, 원격 서버와의 통신, 데이터 변환 등이 포함

Repository

Repository는 외부 데이터 소스와 앱의 데이터 소스를 중재하는 역할을 수행
비즈니스 로직에서 필요한 데이터를 가져오거나 저장하는 책임을 진다.


protocol UserRepository {
    func getUserByID(_ userID: String, completion: @escaping (User?) -> Void)
    func saveUser(_ user: User, completion: @escaping (Error?) -> Void)
}

Remote Data Source

Remote Data Source는 외부 서버와 통신하여 데이터를 가져오거나 전송하는 역할을 한다.
주로 API 호출이나 원격 데이터베이스와의 상호작용을 처리함

class RemoteUserDataSource: UserRepository {
    func getUserByID(_ userID: String, completion: @escaping (User?) -> Void) {
        // 외부 서버에서 사용자 정보를 가져옴
    }

    func saveUser(_ user: User, completion: @escaping (Error?) -> Void) {
        // 외부 서버에 사용자 정보를 저장함
    }
}

Local Data Source

Local Data Source는 앱 내부에서 데이터를 저장하거나 검색하는 역할을 담당
로컬 데이터베이스, 파일 시스템, 또는 UserDefaults와 같은 로컬 저장소와 상호작용하면서 데이터를 처리

class LocalUserDataSource: UserRepository {
    func getUserByID(_ userID: String, completion: @escaping (User?) -> Void) {
        // 로컬 데이터베이스에서 사용자 정보를 가져옴
    }

    func saveUser(_ user: User, completion: @escaping (Error?) -> Void) {
        // 로컬 데이터베이스에 사용자 정보를 저장함
    }
}

Mapper

Mapper는 데이터를 다른 형식으로 변환하는 역할을 함
주로 데이터 모델과 엔터티 간의 매핑을 담당하고, 데이터의 가공이 필요한 경우에 사용

class UserMapper {
    func mapDTOToEntity(_ userDTO: UserDTO) -> User {
        // 데이터를 엔터티로 매핑함
    }

    func mapEntityToDTO(_ user: User) -> UserDTO {
        // 엔터티를 데이터 모델로 매핑함
    }
}

Data Layer는 외부와의 데이터 통신, 로컬 데이터 관리 등을 효과적으로 처리하여 Domain Layer에 필요한 데이터를 제공하는 역할을 수행
이를 통해 비즈니스 로직은 독립적으로 유지될 수 있고, 데이터 관리는 Data Layer에서 책임지게 됨

profile
공부하고 소통하는 공간

0개의 댓글