제목: 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

트랙백  0 , 댓글  0개가 달렸습니다.
secret


유닛 테스트에서 가장 힘든 시점은 시작 시점이다. 그 이유는 모든 아키텍처가 유닛 테스트 되지 않기 때문이다. 만약 유닛 테스트를 하고자 한다면(Part1(링크)에서 왜 해야하는지 설명했다.) 앱의 아키텍처를 주의깊게 만들어야 한다.


이 아키텍처에 관해 좀 더 세부적으로 들어가기 전에 먼저 한가지 키포인트를 강조하고 싶다:

우리 아키텍처를 보다 더 테스트하기 쉽게 만드는 과정은, 코드를 다른 방면으로도 더 낫게 만들어 줄 것이다. 보통 테스트 가능한 설계는 좋은 소프트웨어 설계와 직결된다.

테스트 가능한 아키텍처로 바꿀 때, 우리는 앱 컴포넌트들을 더욱더 독립적으로 만들어야 하는데 특히 외부 라이브러리로부터 분리해야한다. 이렇게 바꾸면 더 다루기 쉬워지고, 이 점은 소프트웨어 개발의 불변의 법칙이다. 또한 우리는 앱을 만들때 시작 시점에 어떻게 해야하는지, 긴 개발기간동안 어떻게 시간을 단축시킬 수 있는지에대해 주목해볼 것이다.

이제 이 일을 정확히 어떻게 하는지 보자

ViewController와 함께 다루기
모의 객체로 ViewController를 본따는 일은 쉽지 않다. ViewController를 테스트 하는것도 쉽지 않은 일이다. 따라서 무턱대고 작업해서는 안된다.

음... 뭐라고?

우리는 우리에 맞는 MVVM 아키텍처를 사용할 것이다. MVVM에 대한 글이 수도 없이 많기 때문에(아래 링크 참고) 이 부분에 대해 깊이 들어가지는 않겠지만 기본 원칙은 다음과 같다. 모든 로직을 ViewController 바깥에 두어 UIKit과 로직이 섞이지 않게 한다. 이 로직에는 모든 모델의 변화, 다른 서비스를 호출, 상태 변화, 예외 처리 등이 있다. 이 모든 로직은 ViewModel에 담겨있다. 모든 ViewController는 ViewModel로부터 데이터를 바인딩하고 유저 인터렉션에 그것을 보내며, 유저 인터렉션은 애니메이션이나 뷰를 준비하는 화면 표시의 코드이다. 아래에 MVVM에관한 글이 더 있다:


MVVM에는 각기 다른 관점들이 존재한다. 어떤 사람들은 ViewModel이 모델과 한께 초기화된 값 타입이여야한다고 하기도 하지만 우리는 조금 다르다. 우리 아키텍처에서의 ViewModel은 UI 상태를 가지고 있고, 다른 서비스를 호출하며, ViewController에 보여줄 가공되지 않은 데이터를 제공하는 역하을 한다.

이렇게 함으로서 ViewController는 굉장히 가볍고 간단한 UIKit 관련 레이어가 되므로 대부분의 앱 로직을 쉽게 테스트할 수 있다.

(Note: ViewController를 테스트 할 수도 있지만 솔찍한 내 의견은 MVVM이 MVC보다 더 나은 아키텍처라고 생각한다.)

의존성 주입(Dependency Injection)
유닛 테스트에서 각 '유닛'은 완전히 분리되어 있기 때문에 앱의 각 컴포넌트를 독립시킬 필요가 있다. 우리가 테스트 하고 있는 각 클래스에 '약한' 의존성을 제공할 방법이 필요하고, 의존성때문에 특정 테스트가 실패하는 것은 아닌지 알아야한다.(This means that we need a way to provide “sterile” dependencies to each class we are testing, to know for certain tests won’t fail because of the dependencies.)

의존성 주입이란 그냥 한 클래스에 외부적인 의존성을 제공한다는 의미의 그럴싸한 표현이다. 클래스가 자기 스스로 의존성이 생길 수는 없다. 다른 클래스를 호출하고자 한다면 그 객체를 초기화때 파라미터로 받아두어야한다. (이것을 constructor injection이라 부른다)

또 다른 한가지 양상은 모든 의존성은 프로토콜 객체로서 선언된다는 것이다. 이러한 방법으로 그 클래스가 필요로하는 오브젝트를 메소드에 담아 우리가 원하는 클래스/구조체의 객체를 초기화할때 쉽게 전달할 수 있다.


이렇게하면 우리의 통제된 의존성을 클래스에 제공할 수 있게 해주고, 나머지 앱 부분으로부터 와넞ㄴ히 독립적이게 만들어준다. 또한 모의 객체 의존성과 함께 깔끔하고 작은 일을 할 수 있게 해준다.

이런식으로 코드를 작성하면 꼭 테스트 뿐만 아니라 코드를 분리시킬 수 있다는 점에서 좋은 방법이다. 그 클래스는 구체적인 구현에 의존하지 않고 그냥 프로토콜로서 들고 있는 것이므로 다른 클래스의 구현을 변경하더라해도 그 클래스는 건드리지 않아도 된다.

외부 프레임워크
만약 클래스가 외부 프레임워크에 의존하고 있으면 어떻게 될까? 우리 클래스가 NSURLRequest나 CoreData에 의존하고 있으면 어떨까? 아마 꼬일 것이다.

우리는 외부 프레임워크를 감싸는 helper를 만들 것이다. 기본적으로 helper는 감싸고 있는 외부 프레임워크의 무엇이든 불러오고, 프레임워크에 함수 호출을 전하며, 외부 프레임워크에 의존없이 우리 코드베이스에서 사용할 수 있는 형태의 결과로 변형하는 역할을 한다. 한가지 일반적인 규칙은, helper는 한 프레임워크당 하나씩 불러온다.

helper 안의 로직은 가능한 작아야한다. ViewController처럼 테스트 하기 쉽지 않기 때문이다. 그리고 그 프레임워크의 같은 기능을 재정의하는것이 아니라 helper를 코딩 기준과 필요한 것에 맞추어야한다.

다른 의존성에도 같은 원리를 적용시켜 모든 클래스는 세부적인 구현을 하는게 아니라 helper에 맞춘 프로토콜의 객체를 받는다.

이렇게하면 커다란 이점이 있다. 예를들어 당신이 CoreData가 너무 복잡해서 Realm으로 바꾸려 할때, 오직 한 클래스만 고치면 된다.

그러면 자신의 의존성을 제공하지 않는 클래스는 무엇이 있을까? 바로 서비스 팩토리이다.

서비스 팩토리(Service Factory)
서비스 팩토리는 앱 전체에 의존성을 만드는 변수를 get-only로 모아놓은 집합체이다. 예를들어 ViewModel은 APIServiceProtocol 타입에 의존성을 가지는데, 그것이 ViewModel로서 컨스트럭트 될 것이다. (apiService: ServiceFactory.apiService)

클래스가 다른 것을 호출하는 장소는 한 곳에 모여있어야 한다. 이것은 마치 통제실 같은 느낌을 주며, 어떤 개발자가 보아도 한번에 이 클래스가 어떻게 돌아가는지 알 수 있어야 한다.

또한 의존성 구현을 맞바꾸는 유일한 장소이기도 하다. 예를 들어보자면 싱글톤에서 일반 객체로 바꾸는데 5초정도 걸린다. 그렇게 하기 너무 크다면 한 클래스를 두 부분으로 쪼개어 시간을 절약할 수 있다.

그리고 여러분의 클래스에 더미나 목(mock) 의존성을 제공하여 실행때 인자레 의존하므로 UI 테스트나 디버깅에 유용하다. UI 테스트시 네트워크로부터 독립적이거나 백엔드에서의 기능은 여전히 잘 동작하고 있을 것이다.

좋다. 이제 우리 앱이 실제 구현과 모의 객체를 맞바꿀 수 있다. 그러나 이것을 어떻게 할 수 있을까?

모의 객체(Mocks)
모의 객체는 여러분이 테스트하고 있는 클래스의 의존성으로서 같은 프로토콜을 따르는 클래스 혹은 구조체이다. 모의 객체는 클래스에 유닛테스트 할 수 있게 해준다.모의 객체는 모통 no-op 메소드나 유닛 테스트에 유용한 작은 기능들을 가지고 있다.

일반적인 역할은 모의 객체가 테스트할 타겟 속으로 들어간다는 점이다. 이렇게 하면 모의 객체에 구현된 코드들이 우리 앱을 더럽히지 않을 수 있다.

당신은 모의 객체 의존성만 생각해선 안된다. 완전한 테스트를 위해 종종 모의 객체 델리게이트 오브젝트를 만들어야한다. 정확한 원칙은 이것이지만 노력하고자하는 델리게이트 메소드를 검증해야 한다는 점을 잊어선 안된다.

Swift에서는 제한된 런타임 접근 때문에 아직 안드로이드용 Mockito나 Objective-C용 OCMock과 같은 좋은 모의객체 프레임워크가 나오지 않았다. 따라서 직접 모의객체를 만들어보자.

지루한 작업일 수 있으나 모의객체 프레임워크를 쓰는 것 보다 더 자유롭게 작업할 수 있을것이다. 대부분 모의객체가 보편적으로 다음과같은 설계를 가진다는 것을 알아냈다:
  1. 당신이 모의로 할 프로토콜을 구현한다.
  2. 각 메소드는 모의객체 안에 methodDidGetCalled 라는 불리언 프로퍼티를 가지고 있어야하고 requestedParameterX 프로퍼티는 옵셔널하게 가지고 있는다. 이 메소드 구현은 보통 마지막에 이 프로퍼티를 설정하는 것이다. 여러분은 테스트한 클래스에 그 메소드가 옳바른 파라미터와 함께 호출 되었는지 나중에 확인해 볼 수 있다.
  3. 만약 메소드가 리턴값이나 완료 핸들러의 뭉치를 가진다면 구조체에 methodXShouldFail이라는 불리언 프로퍼티가 있다. 이 메소드 구현은 불리언을 체크하고 성공하든 못했든 결과를 반환한다. 이렇게하면 테스트한 클래스에 실패가 생겼을 때 다루기 유용해진다.

테스트 작성하기
앱을 옳바르게 준비하고, 무엇을 테스트할지 안다면 이번에는 꽤 같단하게 끝날 것이다. 이번에는 단지 테스트의 동작을 확인한다.

테스트를 도와주는 수많은 라이브러리가 존재한다. 먼저 애플의 XCTest이다. Xcode와 연동하여 사용할 수 있고, 타이핑할 것이 좀 많기는 하나 꽤 좋은 테스트 라이브러리이다. 그리고 써드파티 라이브러리인 QuickNimble이 있다. 우리는 Nimble과 함께 XCTest를 사용한다.

다른 메소드를 호출하는 메소드를 위해, 그 메소드가 호출되었는지, 옳바른 파라미터를 전달했는지 체크한다.

당신의 클래스가 델리게이트에게 알리는지 혹은 옳바른 콜백을 호출하는지 확인한다.

이제 당신이 할 수 있는 모든 것을 테스트 할때까지 두 세번정도 이 일을 하면 된다.

이 두 포스트를 통해 왜 테스트를 해야하는지 아는데 도움을 주고, 유닛테스트를 어떻게 시작하는지에대한 가이드가 되면 좋겠다. 즐거운 테스팅하길 바란다!








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

트랙백  0 , 댓글  0개가 달렸습니다.
secret