제목: Refactoring singleton usage in Swift

더 명료하고, 더 모듈화시키며, 더 테스트용이한 코드베이스를 위한 팁

소프트웨어 개발에서 싱글톤널리 권장되지 않고 눈쌀을 찌푸리게 만든다. 이것을 테스트하는 것은 어렵거나 불가능하고, 묵시적으로 다른 클래스에서 사용하면 여러분의 코드베이스는 헝클어져버린다. 또한 이것은 코드의 재사용도 어렵게 만든다. 오랫동안 싱글톤은 전역 변수나 가변 상태의 변형에 지나지 않다고 생각해왔다. 적어도 많은 사람들은 이 방법이 나쁜 방법이라는 것 정도는 인지하고 있다. 그러나 때때로 싱글톤은 피할수 없는, 필요한 독이기도 하다. 이것을 어떻게 깔끔하고 모듈화되고 테스트용이하게 우리 코드에 집어넣을 수 있을까?

싱글톤은 어디에나 있다
애플 플랫폼에서 싱글톤은 Cocoa와 Cocoa Touch 프레임워크 어디에나 있다. UIApplication.shared, FileManager.default, NotificationCenter.default, UserDefaults.standard, URLSession.shared 등이 있다. 또한 이 디자인 패턴은 Cocoa Core Competencies 가이드의 한 섹션으로 나와있다.

여러분이 묵시적으로 저 싱글톤(혹은 여러분의 싱글톤)을 참조할때 여러분의 코드를 변경하는데 드는 노력이 증가할 것이다. 싱글톤을 사용하는 클래스에서 싱글톤을 변경하거나 목(mock) 할 수 있는 방법이 없기 때문에 여려분의 코드를 테스트하기 어려워지거나 불가능해진다. 아래는 iOS 앱에서 일반적으로 볼 수 있는 것이다.
class MyViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let currentUser = CurrentUserManager.shared.user
        if currentUser != nil {
            // do something with current user
        }

        let mySetting = UserDefaults.standard.bool(forKey: "mySetting")
        if mySetting {
            // do something with setting
        }

        URLSession.shared.dataTask(with: URL(string: "http://someResource")!) { (data, response, error) in
            // handle response
        }
    }
}
이것이 묵시적인 참조이다(클래스 안에서 직접 싱글톤을 사용한다). 더 나아지게 할 수 있는데, 스위프트로 가볍고 쉬우며 의존성을 줄이는 방법이 있다. 또한 스위프트로 우아하게 만들 수도 있다.

의존성 주입
짧게 말해, 답은 의존성 주입이다. 이 원리는 여러분의 함수와 클래스를 모든 입력이 명시적으로 되게 설계하는 방법이다. 위의 코드를 의존성 주입을 사용하여 리팩토링 한다면 아래처럼 생겼을 것이다.
class MyViewController: UIViewController {

    let userManager: CurrentUserManager
    let defaults: UserDefaults
    let urlSession: URLSession

    init(userManager: CurrentUserManager, defaults: UserDefaults, urlSession: URLSession) {
        self.userManager = userManager
        self.defaults = defaults
        self.urlSession = urlSession
        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let currentUser = userManager.user
        if currentUser != nil {
            // do something with current user
        }

        let mySetting = defaults.bool(forKey: "mySetting")
        if mySetting {
            // do something with setting
        }

        urlSession.dataTask(with: URL(string: "http://someResource")!) { (data, response, error) in
            // handle response
        }
    }
}
이 클래스는 이제 더이상 모든 싱글톤에 묵시적으로 의존하지 않는다. 이것은 명시적으로 CurrentUserManager, UserDefault, URLSession에 의존한다. 그러나 이런 의존성들이 이것들이 싱글톤이라는 것을 이 클래스는 전혀 모른다. 이런 세부사항들은 기능이 바뀌지 않은채 문제가 없다. 뷰컨트롤러는 단이 이 오브젝트의 인스턴스가 존재한다는 사실만 알고있고, 호출 시점에서 싱글톤을 담아 보낼 수 있다. 다시 말하지만, 이런 세부사항은 이번 수업의 관점과는 관련이 없다.
let controller = MyViewController(userManager: .shared, defaults: .standard, urlSession: .shared)

present(controller, animated: true, completion: nil)
프로 팁: 여기서 스위프트 타입 추론이 일어난다. URLSession.shared라고 쓰는 것 대신에 .shared라고 쓸 수 있다.

만약 다른 userDefaults를 줘야한다면(예를들어 App Groups과 데이터를 공유해야한다면) 바꾸기 쉽다. 사실 이 클래스에서는 아무것도 바뀌지 않아야한다. UserDefualts.standard를 보내는 것 대신에 UserDefaults(suitName: "com.myApp")을 보내야한다.

게다가 이제 유닛테스트에서 이 클래스의 가짜나 목(mock)을 보낼 수 있다. 실제 스위프트에서 목 하는 것은 불가능하지는 않으나 더 편한 workarounds가 있다. 이것은 여러분의 코드를 구성하고 싶은대로 할 수 있다. CurrentUserManager라는 프로토콜을 쓸 수 있는데, 이것은 테스트에서 "목"할 수 있다. 또한 테스트를 위한 가짜 UserDefault에 제공할 수 있고 URLSession을 옵셔널로 만들어 테스트에선 nil을 넣으면 된다.

리팩토링 지옥
이 방법에 열중하여 이제 기술적 빚을 안고있는 여러분의 코드베이스를 해방시키고 싶을 것이다. 의존성 주입은 이상적이고 더 순수한 객체 모델을 제공하지만, 종종 달성하기 어려울때가 있다. 게다가 처음 코드를 짤 때 이것을 수용하도록 설계하지 않을 것이다.

위에 우리가 리팩토링한 것은 이제 더 모듈화되고 테스트용이하다. 그러나 여기에는 현실적인 문제가 있다. MyViewController의 생성자는 빈(init())것에 익숙한데 이제 3개의 파라미터를 받아야한다. 모든 호출시점이 바뀌게 되는 것이다. 이것을 구성하기위해 깔끔하고 적절한 방법은, 계층 위아래로 혹은 이전 뷰컨트롤러에서 지금 뷰컨트롤러까지 인스턴스를 보내도록 만드는 방법이다. 이것은 객체 그래프의 루트에서부터 모든 자식까지 데이터를 보내야 한다는 뜻이다. iOS에서는 특히 뷰컨트롤러에서 뷰컨트롤러로 데이터를 보내는 것이 머리아픈일이다. 특히 다른 사람에게 넘겨받은 코드베이스는 갑자기 많이 바뀌는 구현에서 애를 먹을 것이다. 그러면 대부분 클래스들(특히 뷰컨트롤러)의 생성자는 바뀌어야한다. 이런 바뀜은 앱 전체를 일괄적으로 리팩토링해야하는데 당신이 인지하고 있기 어려운 범주가 되버린다. 모든 코드를 고칠 수도 있겠지만 아니면 다른 클래스들은 여전히 묵시적으로 싱글톤을 참조하게 두고 몇개만 의존성 주입으로 바꾸는 것이다. 그래도 이런 부조화는 훗날에 문제를 야기할 수도 있다.

따라서 이런 리팩토링은 복잡하고, 크고, 넘겨받은 코드베이스에는 알맞지 않다. 이런 이유로 리팩토링을 하지 말고 이런 기술적 부채와 함께 살아갈것인지도 의논해보아야한다. 그러다가 몇달, 몇년뒤 멀티 사용자 기능을 지원해야할때, 계정을 바꿀때 CurrentUserManager가 동작하지 않을 수 있다. 이것은 어떻게 해결할 것인가?

여러분이 어떤 프로젝트를 시작할때부터는 클래스 설계의 첫 단계부터 이런 종류의 변경을 수용할 수 있게 만드는 방법이 있을 것이다.

디폴트 파라미터 값들
스위프트에서 내가 좋아하는 기능 중 하나는 디폴트 파라미터 값들이다. 이것은 놀랍도록 유용하고 여러분 코드에 엄청난 유연성을 가져다준다. 디폴트 파라미터로, 의존성 주입이라는 토끼구멍으로 들어가지 않고, 또 여러분의 코드베이스에서 엄청난 복잡성도 만들지 않고서, 위에서 말한 문제를 해결할 수 있다. 아마 여러분의 앱은 한명의 유저만을 가질 것이니, 위와같으 의존성 주입의 구현이 불필요하게 과하다.

싱글톤을 디폴트 파라미터로하여 사용할 수 있다.
class MyViewController: UIViewController {

    init(userManager: CurrentUserManager = .shared, defaults: UserDefaults = .standard, urlSession: URLSession = .shared) {
        self.userManager = userManager
        self.defaults = defaults
        self.urlSession = urlSession
        super.init(nibName: nil, bundle: nil)
    }
}
이제 호출 시점을 고칠 필요가 없다. 그러나 클래스 그 자체 안에서는 수많은 다양한 것들이 있다. 이제 의존성 주입을 사용하고 더이상 싱글톤을 참조하지 않는다.
let controller = MyViewController()

present(controller, animated: true, completion: nil)
이러한 변경으로 어떤 이득을 취할 수 있을까? 모든 호출 지점을 바꾸지 않고 이 패턴을 사용하기 위해 모든 클래스를 리팩토링할 수 있다. 의미로나 기능적으로나 바뀐것은 없다. 그러나 여러분의 클래스들은 이제 의존성 주입을 사용한다. 이것들은 단지 내부적으로 인스턴스를 사용하고 있다. 이것을 위에서 설명한 것처럼 테스트할 수 있고 유연하게 모듈화된 API를 유지보수 할 수 있다. (그래도 모든 퍼블릭 인터페이스는 바뀌지 않는다는 점) 본질적으로는 아무것도 바뀌지 않은채 계속 코드베이스에서 작업할 수 있을 것이다.

커스텀을 전달받는 떄가 오면, non-싱글톤 파라미터로 어떤 클래스도 변경하지 않고서 해결할 수 있다. 오직 호출 지점만 바꾸면 된다. 게다가 완전한 의존성 주입을 구현하여 계층의 위에서 애래로 모든 의존성마다 전달해가려면 그냥 드폴트 파라미터를 지우고 그 위에서 의존성으로 전달하면 된다.

필요에따라 어떤 디폴트값의 opt-in 혹은 opt-out도 할 수 있다. 아래 예제에서는 커스텀 UserDefaults를 제공하지만, CurrentUserManagerURLSession을 위해 디폴트 파라미터를 가지고 있는다.
let appGroupDefaults = UserDefaults(suiteName: "com.myApp")!

let controller = MyViewController(defaults: appGroupDefaults)

present(controller, animated: true, completion: nil)

결론
스위프트는 적은 노력으로 "partial" 종류의 의존성 주입을 만든다. 여러분의 클래스에 드폴트값으로 새 프로퍼티와 생성자 파라미터를 추가하여, 코드를 모듈화시키고 테스트하기 좋게 만들 수 있다(리팩토링에 빠지지 않고 완전한 의존성 주입을 만들 필요 없이 가능하다). 만약 프로젝트 시작 시점에 클래스를 이렇게 설계하면 코딩하면서 궁지에 몰리는 일이 더 적어질 것이다(그리고 여러분이 궁지에 몰리더라도 쉽게 빠져나올 수 있을 것이다).

여러분은 이 예제를 넘어 클래스, 구조체, 열거형, 함수 등 코드 전반에 이 개념과 설계를 적용시켜 볼 수 있다. 스위프트의 모든 함수는 디폴트 파라미터 값을 받을 수 있다. 나중에 어떤게 바뀔 수 있을지 생각해봄으로서, 적은 노력으로 변경할 수 있는 타입이나 함수를 만들어낼 수 있을 것이다.

좋은 소프트웨어를 만드는 것과 설계하는 것은 원하는 것을 쉽게 바꿀 수 있지만, 모든것을 바꾸진 않아도 되는 코드를 짠다는 의미이다. 이것이 의존성 주입 뒤에 있는 그 이유이며 스위프트의 디폴트 파라미터가 이것을 빠르고 쉽고 우아하게 해결할 수 있게 도와줄 것이다.


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,