본문 바로가기

개발경험

[Swift Unit Test] "오운완" 효과적인 테스트를 위한 고민(코드 재사용)

반응형

최근 고민했던 내용들을 본격적으로 얘기하기 전에 

 

MVVM패턴과 MVC패턴의 차이에 대한 고민으로부터 시작되었다. 

현재 나는 MVC패턴과 MVVM패턴으로 진행중인 프로젝트가 1개씩있다. 그래서 자연스럽게 차이에 대해서 고민하게 되었다. 

 

MVC패턴은 

Model + View + Controller 구조이다.

결과적으로 뷰를 보여주고 관리하기 위한 로직들이 모두 ViewController 안에 있다. 

 

MVVM패턴은

Model + View + ViewModel 구조이다. 

MVC 패턴과의 차이에 대해선 이제 ViewModel이라는 계층을 하나 더 만들어서 로직을 따로 관리한다는 것이다. 

 

그러면 굳이 왜 ViewModel을 따로 만들고자 했을까?

  • 보통 MVC 패턴에서 ViewController가 비즈니스 로직 말고도 View에 대한 코드를 가지고 있다는 점
  • MVVM 패턴에서 로직을 따로 가지고 있어서 ViewController가 자신만의 역할을 할 수 있다는 점
  • ViewModel에서 비즈니스 로직을 쉽게 관리할 수 있다는 점

등의 이유를 장점으로 들지만 테스트 코드에 대해서 공부하다보니 결국의 로직을 구분하는 이유는 테스트를 용이하기 위해서 라고 생각이 들었다. 

 

 

그럼 현재 내가 지금까지 작성한 ViewModel은 어땠을까?

 

class WorkOutDoneData : Object {
    @Persisted dynamic var id : Int = 0
    @Persisted dynamic var date : String = Date().yyyyMMddToString()
    @Persisted dynamic var frameImage : FrameImage?
    @Persisted dynamic var bodyInfo : BodyInfo?
    @Persisted dynamic var workOutTime : Int?
    @Persisted dynamic var routine : Routine?
    
    convenience init(id: Int, date: String) {
        self.init()
        self.id = id
        self.date = date
    }
    
    override class func primaryKey() -> String? {
        return "id"
    }
}

"오운완" 앱은 날마다 운동 기록을 남기는 앱이기 때문에 "WorkOutDoneData"를 중심으로 데이터 구조가 잡혀있다. 

 

물론 제네릭을 써서 CRUD 기능을 담은 클래스를 만들어서 사용했지만

각각의 ViewModel은 생각보다 많은 코드들이 중복되어 있었다. 

ex) 데이터가 있는지 확인, 데이터 만들기, 업데이트, 삭제 등등..

그럼 여기서 만약에 각각의 ViewModel에 대한 테스트 코드를 만들 때 

  • 그 반복되는 코드들에 대해서 또 테스트코드를 만들어야 하나
  • Realm 코드는 어떻게 테스트 해야 하나(다음 글에서 다룰 예정)

등의 고민을 하게 되었다. 

 

 

아직 개선할 부분도 있다고 생각하지만 일단 나는 "protocol"과 "DI" 개념을 활용했다.

 

우선 전체적인 구조는 다음과 같다.

import RealmSwift

protocol DataManager {
    func createData<T>(data: T)
    func readData<T: Object>(id: Int, type: T.Type) -> T?
    func updateData<T: Object>(data: T, updateBlock: (T) -> Void)
    func deleteData<T>(data: T)
}

DataManager 라는 protocol에는 RealmSwift에서 사용될 기본적인 CRUD 기능의 코드들을 만들었다. 

 

class RealmManager: DataManager {
    let realm: Realm
    
    init(realm: Realm) {
        self.realm = realm
    }
    
    // create
    func createData<T>(data: T) {
        do {
            try realm.write {
                if let dataArray = data as? [Object] {
                    realm.add(dataArray)
                } else if let object = data as? Object {
                    realm.add(object)
                } else {
                    print("Unsupported data type: \(type(of: data))")
                }
            }
        } catch {
            print("Error saving data: \(error)")
        }
    }
    // read
    func readData<T: Object>(id: Int, type: T.Type) -> T? {
        let data = realm.object(ofType: type, forPrimaryKey: id)
        return data
    }
    // update
    func updateData<T: Object>(data: T, updateBlock: (T) -> Void) {
        do {
            try realm.write {
                updateBlock(data)
            }
        } catch {
            print("Error saving data: \(error)")
        }
    }
    // delete
    func deleteData<T>(data: T) {
        do {
            let realm = try Realm()
            try realm.write {
                if let data = data as? Object {
                    realm.delete(data)
                } else {
                    print("Unsupported data type: \(type(of: data))")
                }
            }
        } catch {
            print("Error deleting data: \(error)")
        }
    }
}

RealmManager는 DataManager 프로토콜을 채택해서 CRUD 코드들을 구체화하였다.

또한 의존성 주입을 통해 실제 테스트 시 임시 Realm 데이터베이스를 만들어서 사용할 수 있게끔 하였다. 

 

 

class BodyInfoDataManager {
    let realmManager: RealmManager
    
    init(realmManager: RealmManager) {
        self.realmManager = realmManager
    }
    
    func readBodyInfoData(id: Int) -> BodyInfo? {
        let bodyInfoData = realmManager.readData(id: id, type: WorkOutDoneData.self)?.bodyInfo
        return bodyInfoData
    }
    
    func createBodyInfoData(weight: Double?, skeletalMusleMass: Double?, fatPercentage: Double?, date: String, id: Int) {
        let workoutDoneData = WorkOutDoneData(id: id, date: date)
        let bodyInfo = BodyInfo()
        bodyInfo.weight = weight
        bodyInfo.skeletalMuscleMass = skeletalMusleMass
        bodyInfo.fatPercentage = fatPercentage
        workoutDoneData.bodyInfo = bodyInfo
        realmManager.createData(data: workoutDoneData)
    }
    func deleteBodyInfoData(id: Int) {
        if let workoutDoneData = realmManager.readData(id: id, type: WorkOutDoneData.self) {
            realmManager.deleteData(data: workoutDoneData.bodyInfo!)
        }
    }
    func updateBodyInfoData(workoutDoneData: WorkOutDoneData, weight: Double?, skeletalMuscleMass: Double?, fatPercentage: Double?) {
        realmManager.updateData(data: workoutDoneData) { updatedWorkOutDoneData in
            let bodyInfo = BodyInfo()
            bodyInfo.weight = weight
            bodyInfo.skeletalMuscleMass = skeletalMuscleMass
            bodyInfo.fatPercentage = fatPercentage
            updatedWorkOutDoneData.bodyInfo = bodyInfo
            
        }
    }
}

그리고 각각의 데이터 구조에 사용될 RealmDataManager를 만들어주어 코드의 재사용성을 높이고자 하였다. 꼭 필요한 코드들만을 ModelDataManager에 담았다!!

 

struct RegisterMyBodyInfoViewModel: ViewModelType {
 	let realmProvider: RealmProviderProtocol
    let workoutdataManager: WorkoutDoneDataManager
    let bodyInfoDataManager: BodyInfoDataManager
    init(realmProvider: RealmProviderProtocol) {
        self.realmProvider = realmProvider
        let realmManager = RealmManager(realm: try! realmProvider.makeRealm())
        self.workoutdataManager = WorkoutDoneDataManager(realmManager: realmManager)
        self.bodyInfoDataManager = BodyInfoDataManager(realmManager: realmManager)
    }
}

실제 ViewModel에서 사용

 

 

이렇게 만들어본 결과 

ViewModel 코드들의 양이 줄어들었고
테스트 코드를 작성할 때도 중복되는 코드를 또 테스트할 필요가 없어졌다. 

 

 

느낀점

사실 이렇게 구조를 바꾸기 전에 이렇게 했는데 잘못된 방법이 아닐까? 좀 더 개념 공부를 하고 나서 구조를 바꿔볼까라는 생각을 잠깐 헀었다.. 그치만 개발이라는 것은 언제나 준비가 안되어 있는 듯한 느낌을 주기 때문에 일단 시도해보려고 노력했다. 또한 지금 또 이렇게 만들어봐야 나중에 다른 개념들을 공부해서 다시 구조를 바꾸게 되었을 때 장단점을 더 명확히 알 수 있게 되기를 기대한다 ㅎㅎ

 

반응형