SH0123 / BookAndMe

[책과 나의 조각] iOS 앱
MIT License
1 stars 0 forks source link

[기록] 목적에 따른 데이터 구조 분리와 매핑 #4

Open SH0123 opened 7 months ago

SH0123 commented 7 months ago

배경

앱을 개선하고 방향성을 변경하면서 두가지 요구사항이 발생했다.

이 때 아래와 같은 문제가 발생했다.

고민사항

  1. 두 가지 API를 사용하는 함수, 객체를 각각 작성해야하는데 어떻게 하면 코드를 최대한 재사용하여 만들 수 있을까?
  2. 데이터 구조체와 다른 객체들간의 결합도를 어떻게 낮출 수 있을까?

내 생각

오늘은 2번에 대해 학습하고 정리해보겠다. 클린 아키텍쳐 with mvvm 글들에서 관련 내용을 간단히 접해볼 수 있었다

SH0123 commented 7 months ago

Reference

Clean Architecture with mvvm

dto와 Entity의 분리

SH0123 commented 6 months ago

기존 상황

기존에는 빠르게 결과물을 도출하여 좋은 성적을 받자는 생각 아래에 간편하고, 지금 당장 쉬운 코드를 작성해왔다. 아래처럼 @FetchRequest와 같은 property wrapper를 사용해서 NSObject 타입의 CoreData 객체를 직접 불러왔다.

@FetchRequest(
        sortDescriptors: [], predicate: NSPredicate(format:"readingStatus == true"),
        animation: .default)
    private var items: FetchedResults<BookInfoEntity>

이는 아래와 같은 장점이 있었다.

  1. 간편한 데이터 fetching
  2. CoreData에 데이터를 추가, 삭제할 때 마다 빠른 데이터 동기화

문제점

프로젝트 종료 후 개선을 위해 CoreData Entity의 Attribute 이름을 바꾸고 추가, 삭제 하는 과정에서 문제가 생겼다. Attribute 이름 하나를 바꾸자, 해당 객체를 사용하는 모든 View에서 변경이 필요해졌고 컴파일 에러 수정에 정말 많은 시간이 소요됐다. 이는 객체들간의 결합도가 아래 그림과 같이 높았기 때문이라고 생각한다.

또한 NSObject 객체를 직접 쓰다보니 Int16과 같이 데이터 타입이 맞지 않는 부분이 있어, 코드를 작성하며 반복적으로 converting 작업을 해줘야해서 번거로웠으며, 이 작업이 항상 필요하다는 것을 인지해야하고 있었다.

정리하자면 아래와 같은 문제점이 있었다.

  1. 목적에 따라 엔티티를 분리 하지 않아 결합도가 높았고, 이는 변경이 미치는 범위가 커져 취약해짐
  2. 데이터 타입의 불일치로 인해 지속적인 캐스팅 작업이 필요함

해결 방법

이를 해결하기 위해 자료를 찾으며 SwiftUI mvvm Clean architecture 글과 Spring에서 DTO 관련 글들을 볼 수 있었다. (Spring은 모르겠다..) 아래 링크의 코드를 참고하여 목적에 따라 View에서 보여질 Entity와 DB에서 사용될 Entity를 분리했다. iOS-Clean-Architecture-MVVM mapping part

extension MoviesResponseEntity {
    func toDTO() -> MoviesResponseDTO {
        return .init(
            page: Int(page),
            totalPages: Int(totalPages),
            movies: movies?.allObjects.map { ($0 as! MovieResponseEntity).toDTO() } ?? []
        )
    }
}

이를 바탕으로 아래와 같이 코드를 작성했다

//MARK: DB Entity -> Domain
extension BookInfoEntity {
    func toDomain() -> BookInfo {
        return .init(
            id: Int(id),
            author: author ?? "",
            bookDescription: bookDescription ?? "",
            coverImageUrl: "",
            image: UIImage(data: image ?? Data()),
            isbn: isbn,
            link: link ?? "",
            readingStatus: readingStatus,
            repeatTime: Int(repeatTime),
            page: Int(page),
            publisher: publisher ?? "",
            title: title ?? "제목 없음",
            wish: wish,
            notes: bookNotes?.allObjects.map { ($0 as! BookNoteEntity).toDomain() } ?? [],
            trackings: readingTrackings?.allObjects.map { ($0 as! ReadingTrackingEntity).toDomain() } ?? [],
            readbooks: readBooks?.allObjects.map { ($0 as! ReadBookEntity).toDomain() } ?? []
        )
    }
}

이로 인해 아래와 같이 결합도가 낮아졌고, 추후에는 DB에서 사용하는 엔티티를 외부에 모두 드러내지 않아 안정성이 보장될 것이라 생각한다. 또한 기존의 문제였던 attribute name을 바꾸더라도 변경의 범위가 아래의 mapping function까지 밖에 미치지 않을 것이다.

한계

.toDomain()과 같은 함수를 사용하게 되면 DB Entity와 Domain Entity가 서로에 대해 알고 있어야한다. 즉 결합도가 증가하게 된다. Domain Entity의 property 변경이 발생이 미치는 범위가 DB Entity까지 이어지게 된다. mapper 객체를 만들어서 사용해주는 게 된다면 변경이 발생해도 mapper 객체에서만 변경이 이뤄지면 되기때문에 문제가 해결 될 것으로 생각된다. 좀 더 정확한 관련 자료를 찾아서 공부해보고 적용하고 싶은데 잘 안찾아진다...