iOS/iOS 지식

[iOS] Realm 사용하면서 생긴 문제들

홍복치 2024. 7. 5. 23:00

Realm을 처음 알게되고 사용했을 때는 편리하다는 생각이 컸는데 쓰다보니 생각보다 고려해야 할 지점이 많다고 생각이 들었음

개발하면서 마주친 문제점과 해결한 방식들을 써보려고 함

 

일단 개발한 앱은

1) 검색을 하면 네이버쇼핑API로 상품정보를 불러와서 보여줌 (네트워크 통신)

2) 그 다음 좋아요를 누르면 Realm에 해당 상품이 저장됨 (DB 저장)

 

사실 상품 ID로 정보를 조회할 수 있는 API가 있다면 똑같이 네트워크 통신을 통해 해결하면 되는 일이지만, 없다는 게 문제임.

그렇다면 내부적으로 Realm으로 저장하는 방법을 사용할 수 밖에 없음. 

어쨌든 그래서 문제는 뭐냐면

 

1. 네트워크 통신에서 받아온 데이터는 Realm Object가 아님

아래 화면은 네이버 쇼핑 API로 조회한 결과값임. API로 조회된 결과값은 아래처럼 Decodable을 채택한 구조체에서 관리함

struct Shop: Decodable {
    let title: String
    let link: String
    let image: String
    let lprice: String
    let mallName: String
    let productId: String
    let titleDescription: String
    let priceDescription: String
 }

근데 Realm은 Object임 정확하게 말하면 Realm에서 정의한 RealmSwiftObject

class Like: Object {
    @Persisted(primaryKey: true) var productId: Int
    @Persisted(indexed: true) var title: String
    @Persisted var link: String
    @Persisted var image: String
    @Persisted var lprice: String
    @Persisted var mallName: String
    
    convenience init(productId: Int, title: String, link: String, image: String, lprice: String, mallName: String) {
        self.init()
        self.productId = productId
        self.title = title
        self.link = link
        self.image = image
        self.lprice = lprice
        self.mallName = mallName
    }
}

아래처럼 RealmRepository에서 add, delete를 하고 있는데 매개변수 Object임

class RealmRepository: RealmProtocol {
    private let realm = try! Realm()
    
    func addLike(_ item: Like) {
        do{
            try realm.write {
                realm.add(item)
            }
        }catch{
            print("AddLike Failed")
        }
    }
  
    func deleteLike(_ item: Like) {
        do{
            try realm.write {
                realm.delete(item)
            }
        }catch{
            print("deleLike Failed")
        }
    }
}

아래처럼 Object형태를 만들어서 addLike, deleLike를 하면 오류가 날 수밖에 없음

오류를 보면 Realm이 가지고 있는 오브젝트만 삭제 할 수 있다고 함. 

네트워크를 통해 가져온 정보들은 struct기 때문에 아무리 Object형태를 만든다 해도 삭제는 안됨

그렇기 때문에 struct의 특정 ID로 테이블의 데이터를 찾고, 그 다음에 삭제를 해주는 방식으로 해야 함

        let data = shopResult.items[indexPath.item]
        
        let like = Like(
            productId: Int(data.productId)!,
            title: data.title,
            link: data.link,
            image: data.image,
            lprice: data.lprice,
            mallName: data.mallName
        )
        
        if isClicked {
            repository.addLike(like)
        }else{
            repository.deleteLike(like)
       }

아래처럼 PK로 특정 레코드를 찾고, 삭제하는 방식으로 동작하게 했음.

PK가 product_id이기 때문에, 구조체에서도 해당 값을 가지고 있고 이 값으로 조회를 하면됨.

    func deleteLike(_ id: Int) {
        if let item = realm.object(ofType: Like.self, forPrimaryKey: id){
            do{
                try realm.write {
                    realm.delete(item)
                }
            }catch{
                print("deleLike Failed")
            }
        }
    }

2. Results<Element: RealmCollectionValue>는 객체는 변동사항을 가지고 있음

앱에서 좋아요는 어디서든 발생할 수 있음. 이 좋아요가 발생할때마다 realm에서 가지고 있는 정보가 달라짐

일반적으로 collectionView나 tableView를 생각해보면, list = 새로운정보 & tableView.reloadData()라는 로직이 들어감

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        count = repository.fetchAllCount()
        resultCollectionView.reloadData()
    }

좋아요한 아이템만 보는 화면에서 위와 같이 코드를 짜면, reloadData()만으로도 업데이트 된 데이터를 가져옴

얘는 일반적인 우리가 알고 있는 배열의 느낌과 살짝 다름

한번 viewDidLoad에서 값을 넣어주면 상태가 변경되면 알아서 변경됨

그니까 새로 fetching을 하지 않고도 이 list를 데이터로 갖고있는 collectionView는 reload만 해주면 알아서 바뀐다는 것임

   var list: Results<Like>!
  
   override func viewDidLoad() {
        list = repository.fetchAll() //try! Realm().objects(Like.self)
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return list.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ResultCollectionViewCell.identifier, for: indexPath) as? ResultCollectionViewCell else {
            return UICollectionViewCell()
        }
        
        let data = list[indexPath.item]
        cell.configureData(Shop.init(managedObject: data))
        cell.indexPath = indexPath
        return cell
        
    }

공식문서에서 이 사실을 확인할 수 있는데 대충 요약하면

1. 결과는 복사본이 아니고 live상태임. 그렇기 때문에 데이터를 변경하면 디스크에 바로 반영됨

2. 결과는 Lazy하게 동작함. 실제로 요청이 있을 때만 쿼리를 실행해서 성능이 뛰어남. pagination도 필요하지 않다고 함

3.  Realm이 모든 객체 관계를 직접참조로 유지해서 쿼리로 관계 그래프를 직접 탐색할 수 있음

 

3. struct <-> Object 사이 값 변환

	if isClicked {
            let like = Like(
                productId: Int(data.productId)!,
                title: data.title,
                link: data.link,
                image: data.image,
                lprice: data.lprice,
                mallName: data.mallName
            )
            repository.addLike(like)
        }else{
            repository.deleteLike(Int(data.productId)!)

항상 Realm에 데이터를 넣어주려면 struct일 경우 Realm의 Object로 변경해서 사용을 해야함

반대로 Realm object를 struct로 바꿀때도 마찬가지임

과정이 복잡하진 않지만, 코드가 길어지기 때문에 어떻게 코드를 줄일 수 있을까 찾아보았음.

 

1. 프로토콜을 선언

struct -> object 변환 해주는 생성자와, object -> struct로 변환해주는 함수를 정의해놓음

protocol Persistable {
    associatedtype ManagedObject: RealmSwift.Object
    init(managedObject: ManagedObject)
    func managedObject() -> ManagedObject
}

2. 사용하고 있는 구조체에 Persistable 프로토콜을 채택

extension Shop : Persistable {
    typealias ManagedObject = Like

    //Like(object) -> Shop(struct)
    init(managedObject: Like) {
        title = managedObject.title
        link = managedObject.link
        image = managedObject.image
        lprice = managedObject.lprice
        mallName = managedObject.mallName
        productId = "\(managedObject.productId)"
    }
    
    //Shop(struct) -> Like(object)
    func managedObject() -> Like {
        let like = Like(
            productId: Int(productId)!,
            title: titleDescription,
            link: link,
            image: image,
            lprice: lprice,
            mallName: mallName
        )
        return like
    }
}

3. 사용

       if isClicked {
            repository.addLike(data.managedObject())
        }else{
            repository.deleteLike(Int(data.productId)!)
       }

 

참고 자료

공식문서 https://www.mongodb.com/docs/atlas/device-sdks/sdk/swift/crud/read/#read-characteristics

https://medium.com/@ludovicjamet/how-to-use-struct-with-realm-615fcbc8f0ee