원시값을 감싸서 유지보수하기 쉬운 코드를 만들어보자

최근 몇주는 부트캠프 준비때문에 알고리즘을 빡세게 준비하느라 프로젝트나 CS 공부를 거의 못했지만.. 이젠 끝났으니 다시 시작.

요즘에는 기존 프로젝트를 리팩토링 하는 작업을 하고 있다. 만드는것보다 리팩토링 하는게 더 어렵다. 

 

이번에는 객체지향 생활체조 원칙중 하나인 "모든 원시 값과 문자열을 포장합니다."를 준수해서 리팩토링 해보았다.

 

우선 리팩토링한 부분에 대해 설명하자면 아래와 같다.

첫번째 사진의 목표거리와 목표 시간의 숫자부분을 터치해서 설정화면으로 이동해서 거리와 시간을 설정하고 우측 상단의 설정버튼을 누르면 설정한 거리가 적용되도록 만들었던 기능이다. 

 

리팩토링 전 목표 설정 로직은 다음과 같다.

  1. ViewModel에서 현재 목표 값을 목표 설정 화면을 생성할 때 할당한다.
  2. 이때 시간은 초단위로 되어 있으니 이를 목표시간 설정 화면에서 변환 로직을 통해 "시간:분" 형태로 변환한다
  3. 목표 설정 화면에서 설정 버튼을 누르면 설정한 값으로 목표가 설정된다. 이때 러닝 시작화면 ViewModel에서 변환 로직을 통해 저장 할 수 있는 형태로 변환한다. ex) 목표 시간을 00:69로 설정한 경우 이를 초단위로 변경
  4. ViewModel의 데이터 바인딩을 통해 러닝 시작화면에 변환 로직을 통해 올바른 형태로 표시되도록 한다. ex) 목표 시간을 00:69로 설정한 경우 이를 01:09가 되도록 변경

여기서 몇가지 문제점이 생긴다.

  • 변환 로직이 여러군데 퍼져있음. 즉 값을 변환하는 책임이 여러군데 퍼져 있다. 따라서 중복코드가 발생할 가능성이 높다.
  • 변환 로직에 수정이 생길경우 여러곳을 수정해야 함.
  • 데이터를 전달할때 타입을 변경하면 ViewModel에서 목표 값 타입부터 변환 로직, 러닝 시작화면의 데이터 바인딩, 러닝 목표 설정 화면까지 모두 수정해야 함.
  • 원시값 그대로 사용하기에 원시값을 변경하고 싶다면 여러 군데를 수정해야됨. ex) Int를 Double 타입으로 변경

이런 문제를 해결하기 위해 목표 설정 값에대한 책임을 한군데로 집중시킬 필요가 있다. 그래서 원시값을 가지고 있으면서 변환 로직역시 가지고 있는 객체를 하나 만들었다.

import Foundation

struct Time {
    
    private let seconds: Int
    
    var hour: Int {
        get {
            return seconds / 3600
        }
    }
    
    var minute: Int {
        get {
            return (seconds % 3600) / 60
        }
    }
    
    var second: Int {
        get {
            return seconds % 60
        }
    }
    
    init(seconds: Int = 0, minute: Int = 0, hour: Int = 0) {
        let totalSeconds = seconds + (60*minute) + (3600*hour)
        self.seconds = totalSeconds
    }
    
    init(goalTimeString: String) {
        let time = goalTimeString.split(separator: ":").compactMap { Int($0) }
        let hour = time.first ?? 0
        let minute = time.last ?? 0
        self.init(minute: minute, hour: hour)
    }
    
    func formatedTimeToString(format: TimeFormatType) -> String {
        return format.convertTimeToString(seconds: seconds)
    }
}

해당 객체를 통해 ViewModel과 러닝 목표 시간 설정 화면, 그리고 러닝 시작 화면에 존재하던 변환로직을 없앨 수 있고, 원시값이 변경되어도 Time 내부의 원시값을 변경하면 되는것이므로 외부에서는 수정에 의한 변경이 최소화 될 수 있었다.

 

리팩토링을 하기 이전의 뷰 모델이다. 뷰 모델에서 시간 변환 로직과 그 시간, 분 원시값을 가지고 있다. 

class RunningStartViewModel {
    
    var goalHour: BehaviorRelay<Int> = BehaviorRelay<Int>(value: 0)
    var goalMinute: BehaviorRelay<Int> = BehaviorRelay<Int>(value: 30)
    var goalTimeRelay = BehaviorRelay<String>(value: "")
    
    let disposeBag = DisposeBag()
    
    init(actions: RunningStartViewModelActions) {
        self.actions = actions
        Observable.combineLatest(goalHour, goalMinute).map({ hour, minute in
            "\(String(format: "%.2d", hour)):\(String(format: "%.2d", minute))"
        }).bind(to: goalTimeRelay)
            .disposed(by: disposeBag)
    }
    
    func setGoalTime(goal: String) {
        let splGoal = goal.split(separator: ":").map { Int($0)! }
        var hour = splGoal[0]
        var minute = splGoal[1]
        if minute >= 60 {
            minute = minute % 60
            hour += 1
        }
        goalHour.accept(hour)
        goalMinute.accept(minute)
    }
    
    private func convertTimeToSecond(hour: Int, minute: Int) -> Int {
        let hourToSecond = hour*60 * 60
        let minuteToSecond = minute * 60
        return hourToSecond + minuteToSecond
    }
    
}

하지만 Time으로 원시값을 한번 감싼후에는 뷰모델은 더이상 시간 관련 로직을 처리하지 않으며 변수또한 간단해졌고 코드또한 보기 간결해졌다. 

class RunningStartViewModel {
    private var goalTime: BehaviorRelay<Time> = BehaviorRelay<Time>(value: Time(seconds: Default.defaultTime))
    var goalTimeValue: String {
        get {
            return goalTime.value.formatedTimeToString(format: .goalDistanceFormat)
        }
    }
    
    init(actions: RunningStartViewModelActions) {
        self.actions = actions
    }
    
    func setGoalTime(time: Time) {
        goalTime.accept(time)
    }
}

 

'iOS > 앱 개발' 카테고리의 다른 글

렙케어 출시 프로젝트 회고  (0) 2023.11.07
Swift의 namespace  (0) 2023.07.30
ApplicationDelegate, SceneDelegate  (0) 2023.05.27
iOS) UILabel baseline 문제  (1) 2023.04.08
iOS) 상단 탭바 구현 (2) - 하단 뷰  (0) 2023.03.27