Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CoreData 구축 및 테스트 #75

Merged
merged 17 commits into from
Nov 25, 2024
Merged

CoreData 구축 및 테스트 #75

merged 17 commits into from
Nov 25, 2024

Conversation

k2645
Copy link
Collaborator

@k2645 k2645 commented Nov 25, 2024

#️⃣ 연관된 이슈


⏰ 작업 시간

예상 시간 실제 걸린 시간
2 10 +

테스트 코드 작성과 Data Layer 설계를 고민하다...시간이 저렇게나... 😇

📝 작업 내용

  • CoreData 구축
    • 저장하기, 삭제하기 및 불러오기 기능
  • CoreData의 Entity 추상화
  • CoreData Container 싱글톤 객체 구현
  • CoreData의 BookCover 관련 테스트 코드 작성

📸 스크린샷

image image

📒 리뷰 노트

CoreData의 Entity가 BookCover만 존재하는 상태입니다. 추후 Page와 Book에 대한 Entity 추가도 필요로 합니다.
추가로 Repository를 만들긴 했지만, 아직 Domain Layer와는 연동되어있지 않은 상태입니다. 추후 UseCase와의 연동이 필요합니다.

CoreData를 어떤 식으로 추상화 및 설계하면 좋을지에 대해 고민해보다가 아래와 같은 구조로 만들게 되었습니다.

image

LocalStorage 디렉토리 내부에 BookCover 엔티티를 가져오는 BookCoverStorage가 존재합니다. 해당 객체는 protocol로 추상타입입니다. 상황에 따라 CoreData 뿐만 아니라 다른 Local DB에서도 BookCover를 가져올 수 있도록 하기위해 위와같은 추상화 단계를 도입하게 되었습니다.

그리고 CoreDataStorage의 경우 저희 앱에서 사용하는 CoreData Container 및 context save를 담당하는 Manager 역할의 객체입니다. 다른 타입의 엔티티를 CoreData에 저장하게 될 경우 위 BookCoverCoreDataStorage와 같은 구체타입의 CoreData Storage를 생성하여 Entity 의 생성, 수정, 삭제 등을 구현해야합니다.

원래는 generic을 활용하여 하나의 CoreDataManager와 같은 객체에서 모든 Entity들의 추가, 삭제, 불러오기 등을 관장하고싶었지만, CoreData에서 Entity의 추가나 편집등을 하기 위해서는 Entity 내부 attributes를 모두 알아야함으로 generic으로 구현하기는 불가능하겠다는 결론을 내려 위와같은 형식으로나마 추상화를 해보려 했습니다 !

간결하게 구조에 대한 설명을 적어봤는데 혹시나 이해가 안가시거나 더 좋은 방법이 있다면 말씀해주시면 감사하겠습니다 .ᐟ.ᐟ 🙇🏻‍♀️🙇🏻‍♀️


⚽️ 트러블 슈팅

징글징글한 Swift6..

(마침 매장에 징글벨 노래가 나오길래 징글징글..을..붙여봣읍니다.. 예.... ^^)

CoreData의 test code 작성중 아래와 같은 에러를 맞이했다..

Command SwiftVerifyEmittedModuleInterface failed with a nonzero exit code

해당 에러는 이전에 학습 스프린트 기간에 같은 팀원분을 통해 겪어본 에러였다..
다른 모듈을 호출했을 때, 그 모듈 내부에 있는 class명 등이 겹치는 경우 발생하는 에러였다. Xcode 15.x 버전에서는 정확히 어디서 에러가 뜨는건지 친절하게 알려줬었는데 Xcode16부터는 그냥 저렇게 냅다 한줄.. 알려준다.. (어쩜 Xcode는 버전이 올라가면서 퇴화를 하는지...^^)

하지만 정말... 정말 눈알 빠지게 찾아봤지만 클래스명은 그 어디서도 겹치지 않고있었다.

결국 알아낸 것은 import CoreData를 하는 과정에서 concurrency 문제를 회피(?)하기위해 @preconcurrency를 붙였던 것이 화근이 되었다는 것을 알아냈다..

public static let memorialHouseModel: NSManagedObjectModel = { ... }()

위 코드처럼 NSManagedObjectModel 타입의 객체를 static 프로퍼티로 만들어줘야했는데, 그냥 static으로 만들어줄 경우 NSManagedObjectModel 타입이 동시성에서 안전하지 않다는 아래와 같은 에러가 발생했다.

image

memorialHouseModel프로퍼티에 nonisolated(unsafe)를 붙여줌으로써 에러를 해결할 수 있었다.

CoreData Test

CoreData의 저장및 불러오기 등을 테스트 하기위해 CoreData Model을 어떻게 처리할지 고민해보았습니다. Test 전용 CoreData xcdatamodeld을 만들까도 생각해보았지만 검색결과 같은 xcdatamodeld 파일을 사용하더라도 InMemory 방식을 사용하면 app이 꺼짐과 동시에 test를 사용하면서 저장 및 수정되었던 데이터들이 휘발되기 때문에 이 방식을 활용하여 CoreData의 test를 진행한다는 것을 알게 되었습니다.

.xcdatamodeld 확장파일이 실제 Data가 저장되는 공간 이라고 생각하시면 될 것 같습니다 !

같은 .xcdatamodeld 객체를 공유하면서 test를 할 때에만 MockCoreDataStorage를 만들어내는 과정에서 여러 트러블 슈팅이 존재했습니다.. 더 자세한 내용은 추후.. 노션에 정리하도록 하겠습니당 😇

@k2645 k2645 added ✨ Feature 기능 관련 작업 ✅ Test 테스트 관련 작업 labels Nov 25, 2024
@k2645 k2645 added this to the 0.3 milestone Nov 25, 2024
@k2645 k2645 self-assigned this Nov 25, 2024
@k2645 k2645 linked an issue Nov 25, 2024 that may be closed by this pull request
Copy link
Collaborator

@yuncheol-AHN yuncheol-AHN left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

영현님 주말동안 고생 많으셨습니다 ~
PR에 설명 잘해주셔서 이해하기 편했습니다.

Comment on lines 7 to 15
nonisolated(unsafe) public static let memorialHouseModel: NSManagedObjectModel = {
guard let modelURL = Bundle(for: CoreDataStorage.self).url(
forResource: CoreDataStorage.modelName,
withExtension: "momd"
) else {
fatalError("Error loading model from bundle")
}
return NSManagedObjectModel(contentsOf: modelURL)!
}()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문
혹시 nonisolated에 unsafe를 붙여야하는게 force unwrapping 때문인가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아뇨 ! Swift6 동시성 관련된 에러때문에 붙여진 키워드입니다 !
저걸 때면 아래와 같은 에러가 발생합니다 !
image

NSManagedObjectModel 타입이 Sendable 하지 않은데 그냥 nonisolated를 붙이면 안되나봅니당..

Copy link
Member

@Kyxxn Kyxxn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다
궁금한 점은 코멘트 달아뒀습니다 ~!
집에 조심히 가세요 ^ㅇ^

Comment on lines 5 to 6
func fetch() async -> Result<[BookCoverDTO], MHError>
func create(data: BookCoverDTO) async -> Result<Void, MHError>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3.99
정말 상관없지만, 괜찮으시다면 CRUD 순으로 둬주실 수 있나요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

옹 좋습니다 .ᐟ.ᐟ CRUD 순으로 모두 수정했습니다 .ᐟ.ᐟ

@@ -0,0 +1,10 @@
import MHFoundation

public struct BookCoverDTO {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2
해당 DTO가 public일 필요가 있나요 ?
혹시 테스트에서 사용하기 때문이라면 public을 제거하고 @testable import MHData를 하면 됩니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BookCoverStorageLocalBookCoverRepository의 생성자 부분에 들어가게 되는데, 이때 BookCoverStorage는 public protocol이고 BookCoverStorage 메서드 내부의 프로퍼티 타입에 BookCoverDTO가 들어가므로 해당 객체가 public이 되어야했습니다..

Comment on lines +42 to +57
func create(data: BookCoverDTO) async -> Result<Void, MHError> {
let context = coreDataStorage.persistentContainer.viewContext
guard let entity = NSEntityDescription.entity(forEntityName: "BookCoverEntity", in: context) else {
return .failure(.DIContainerResolveFailure(key: "BookCoverEntity"))
}
let bookCover = NSManagedObject(entity: entity, insertInto: context)
bookCover.setValue(data.identifier, forKey: "identifier")
bookCover.setValue(data.title, forKey: "title")
bookCover.setValue(data.category, forKey: "category")
bookCover.setValue(data.color, forKey: "color")
bookCover.setValue(data.imageURL, forKey: "imageURL")
bookCover.setValue(data.favorite, forKey: "favorite")

await coreDataStorage.saveContext()
return .success(())
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3
전에 DTO 자체를 추상화할 수 있냐는 얘기가 나왔던 거 같은데,
BookCoverDTO에 대한 처리만 하는 것으로 결정하신 거 맞나용 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분에 대해서는 PR에도 아주 짧게나마 언급하긴 했는데, CoreData에서 객체를 생성하는 경우 꼭 생성하려는 Entity의 모든 attributes를 알고있어야했습니다. 이때문에 DTO자체를 제네릭으로 추상화하기는 힘들다고 판단하였고, 대신 각각의 엔티티 별 CoreDataStorage를 만들도록 설계하게되었습니다 !

결론적으로 해당 struct는 BookCoverDTO에 대해서만 처리하게 되는게 맞습니당

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다 !!

Comment on lines 7 to 15
nonisolated(unsafe) public static let memorialHouseModel: NSManagedObjectModel = {
guard let modelURL = Bundle(for: CoreDataStorage.self).url(
forResource: CoreDataStorage.modelName,
withExtension: "momd"
) else {
fatalError("Error loading model from bundle")
}
return NSManagedObjectModel(contentsOf: modelURL)!
}()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오,, 이 파일의 코드가 아직 읽히지 않네요
코어 데이터 공부를 더 하고 봐보도록 하겠습니다

CoreData Stack에 대한 내용인 거죠 ?!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 맞습니다 .ᐟ.ᐟ 간단하게 NSPersistentContainer를 만들기 위해 필요한 NSManagedObjectModel이라고 생각하시면 됩니당 ~

import CoreData
import MHCore

public class CoreDataStorage {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2
해당 코드도 public을 붙이신 이유가 궁금합니다
밑에 생성자는 또 internal로 되어 있네요 이유가 있나용 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

허거덩 제가 접근제어자를 잘못 설정했네욥;; 바로 수정했숩니다 !

Comment on lines 59 to 63
public func deleteBookCover(_ id: UUID) async {
await storage.delete(with: id)
}

public func create(bookCover: BookCover) async {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3
CRUD 순으로 작성되거나, 프로토콜 순서대로 작성되면 다음에 유지보수할 때 조금 더 유용하지 않을까 라는 생각이 듭니다..!
개인적으로 한 파일 내에서 CRUD 메소드 순서가 있으면 가독성이 더 좋았습니다 !

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 프로토콜과 메소드 순서는 같군요..!!
CRUD 순서 지키자는 건 개인적 의견이라 과감하게 무시하셔도 됩니다

Comment on lines +34 to +38
init() async {
for bookCover in CoreDataBookCoverStorageTests.bookCovers {
_ = await sut.create(data: bookCover)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오잉 생성자로 넣어주셨군요
저도 다음에 테스트 작성할 때 참고하겠습니다

Copy link
Collaborator

@iceHood iceHood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빛빛빛님! 너무 고생하셨습니다!! 그냥 자잘한 코멘트만 좀 달아서 크게 신경 쓸 것은 없을 것같아요!

Comment on lines 17 to 26
return bookCoverDTOs.compactMap { (dto: BookCoverDTO) -> BookCover? in
guard let color = BookColor(rawValue: dto.color) else { return nil }
return BookCover(
identifier: dto.identifier,
title: dto.title,
imageURL: dto.imageURL,
color: color,
category: dto.category,
favorite: dto.favorite
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3. 아래 하나 생성하는 거랑 매핑로직이 중복되는 것같아서그러는데 mapping함수 만들어서 처리하는 것은 어떤가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 아래 모두 구현해서 넣어놓았습니당 .ᐟ.ᐟ

Comment on lines 64 to 71
let bookCoverDTO = BookCoverDTO(
identifier: bookCover.identifier,
title: bookCover.title,
imageURL: bookCover.imageURL,
color: bookCover.color.rawValue,
category: bookCover.category,
favorite: bookCover.favorite
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3. 여기도 위랑 동일한 의견입니다!

@k2645 k2645 requested a review from Kyxxn November 25, 2024 14:42
Copy link
Member

@Kyxxn Kyxxn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제일 어렵다고 생각되는 한 파트를 먼저 뚫어주셨군요..!

빛영현 대영현 고생하셨습니다 〰️

@k2645 k2645 merged commit e3e1e01 into develop Nov 25, 2024
2 checks passed
@k2645 k2645 deleted the feature/build-core-data branch November 25, 2024 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✨ Feature 기능 관련 작업 ✅ Test 테스트 관련 작업
Projects
None yet
Development

Successfully merging this pull request may close these issues.

CoreData 구축
4 participants