Notice
Recent Posts
Recent Comments
Link
관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 프로토콜로 리펙토링하기 본문

Refactoring (리펙토링)

[iOS - swift] 프로토콜로 리펙토링하기

jake-kim 2024. 1. 5. 01:17

프로토콜로 리펙토링하는 아이디어

  • 리펙토링의 핵심: 기존에 있는 코드에 영향을 최소화 하는 것
    • 리펙토링 대상에 해당하는 interface들을 모두 protocol을 만들어서 선언
    • 기존에 있던 리펙토링 대상의 인스턴스에 protocol을 타입을 따르고 기존 구현체를 대입
    • protocol을 준수하는 새로운 구현체를 구현하여 기존것과 변경

리펙토링 전 코드 예제

ex) LogModel이라는 기능이 있고 이 모델을 2곳 이상에서 사용하고 있을때 LogModel내부 코드를 리펙토링 하고 싶은 경우?

  • LogModel은 UI를 탭한 카운트를 기록하는 모델
struct LogModel {
    private var countOfTap = 0
    private var latestDate: Date?

    mutating func addCount() {
        countOfTap += 1
        latestDate = .init()
    }

    func recordToDisk() {
        let recordString = "countOfTap: \(countOfTap), latestDate: \(latestDate ?? .init())"
        print("disk에 데이터 저장:", recordString)
    }
}
  • 이 모델을 여러곳(VC1, VC2)에서 사용하고 있는 상태
    • 요구사항에 의하여 VC1에서는 deinit에서 recordToDisk()를 호출하고 VC2에서는 viewDidDisappear에서 호출
class VC1: UIViewController {
    var logModel = LogModel()
    @objc private func tap() {
        logModel.addCount()
    }
    
    deinit {
        logModel.recordToDisk()
    }
}

class VC2: UIViewController {
    var logModel = LogModel()
    @objc private func tap() {
        logModel.addCount()
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        logModel.recordToDisk()
    }
}

리펙토링 수행

    • 1) LogModelV2라는 것을 만들어서 구현할 예정인데, LogModelV2가 LogModel과 인터페이스가 같도록 먼저 만들게하여 빌드 실패가 여러곳에서 나는것을 막고 수정범위를 좁히기 위해 프로토콜 정의
      • LogModel의 인터페이스는 addCount()와 recordToDisk()이므로 두 함수만 프로토콜로 정의

 

protocol LogModelable {
    mutating func addCount()
    func recordToDisk()
}
  • 2) 기존 모델에 이 프로토콜을 준수하게만들고, 사용하는쪽에 타입을 LogModelable로 수정
struct LogModel: LogModelable {
   ...
}

class VC1: UIViewController {
    var logModel: LogModelable = LogModel() // <-
    @objc private func tap() {
        logModel.addCount()
    }
    
    deinit {
        logModel.recordToDisk()
    }
}

class VC2: UIViewController {
    var logModel: LogModelable = LogModel() // <-
    @objc private func tap() {
        logModel.addCount()
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        logModel.recordToDisk()
    }
}
  • 3) 이제 logModel 인스턴스에 LogModelable 프로토콜만 준수하는 어떤 구현체던지 교체가 손쉽게 가능하므로 새로 리펙토링할 LogModelV2구현
struct LogModelV2: LogModelable {
    private var countOfTap = 0
    private var latestDate: Date?

    mutating func addCount() {
        print("new!")
        // ...
    }

    func recordToDisk() {
        print("new!")
        // ...
    }
}
  • 4) 기존 logModel 인스턴스에 LogModelV2로 교체
class VC1: UIViewController {
    var logModel: LogModelable = LogModelV2() // <-
    @objc private func tap() {
        logModel.addCount()
    }
    
    deinit {
        logModel.recordToDisk()
    }
}

class VC2: UIViewController {
    var logModel: LogModelable = LogModelV2() // <-
    @objc private func tap() {
        logModel.addCount()
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        logModel.recordToDisk()
    }
}
  • 리펙토링 전에 있던 LogModel을 삭제하고, LogModelV2의 이름을 V2를 제외한 이름으로 replace치면 리펙토링 완료
// 삭제
//struct LogModel: LogModelable {
//    private var countOfTap = 0
//    private var latestDate: Date?
//
//    mutating func addCount() {
//        countOfTap += 1
//        latestDate = .init()
//    }
//
//    func recordToDisk() {
//        let recordString = "countOfTap: \(countOfTap), latestDate: \(latestDate ?? .init())"
//        print("disk에 데이터 저장:", recordString)
//    }
//}


// 리펙토링 완료된 모델 (이름 변경: LogModelV2 -> LogModel)
struct LogModel: LogModelable { 
    private var countOfTap = 0
    private var latestDate: Date?

    mutating func addCount() {
        print("new!")
        // ...
    }

    func recordToDisk() {
        print("new!")
        // ...
    }
}

* 전체 코드: https://github.com/JK0369/ExRefactoringUsingProtocol

Comments