본문 링크 (Original Link)

Objective-C 클래스를 Swift로 옮기기 : 단편적으로 접근하기

2018.08.09

# • #

by Ole Begemann, translated by pilgwon

요즘 저흰 긴 역사를 가진 큰 iOS 앱을 작업중입니다. Swift로 코드를 새로 짜고 있지만 75~80%의 코드는 여전히 Objective-C로 작성돼있습니다. 서브 시스템을 대대적으로 점검해야 할 때가 아니면 Swift로 다시 짜는 경우는 없습니다. 그래서 특정 서브 시스템의 60%가 Swift로 작성되면 한계점에 도달했다 판단하고, 나머지 40%의 Objective-C를 최소한의 고통으로 작업하고 있습니다.

Objective-C와 Swift는 보통 꽤 잘 맞습니다. 사실은 애플이 두 언어가 서로 잘 작동하게 만들었다는 것이 오히려 놀랍습니다. 언제나 두 언어를 섞는 것은 마찰을 발생시키거든요.

익스텐션에는 저장향 속성이 없습니다

상호 운용성과 직접적인 관련은 없지만 마찰의 원인이 되기도 하는, 익스텐션은 저장형 속성을 가질 수 없다는 사실에 대해 말하고 싶습니다. Swift와 Objective-C 둘 다 모든 저장형 속성/ivars는 반드시 메인 타입 정의의 일부여야 합니다. 이것은 상호 운용성과는 관련이 전혀 없는 일반적인 제한이지만 Swift와 Objective-C가 섞여있는 코드에선 가장 큰 영향을 줍니다.

Objective-C 클래스를 Swift로 조금씩 변환하기

다음과 같은 상황을 생각해봅시다

Swift 익스텐션이 저장된 속성을 필요로 한다면 어떨까요?

이런 경우에 제가 주로 쓰는 전략은 Objective-C 클래스를 위한 스위프트 익스텐션을 만드는 것입니다. 새로운 코드는 익스텐션에 작성됩니다. 필요한 경우에는 @objc 주석을 Objective-C 익스텐션 코드에 보여줍니다. 이 방법은 새로운 코드가 저장된 속성을 클래스에 추가해야 할 때까지는 훌륭하게 작동합니다. 익스텐션에는 넣을 수 없으니 Objective-C 클래스 정의에 추가해야 합니다.

이것은 속성이 Swift 코드에서만 사용되더라도 Objective-C와 호환이 되는 타입이어야 한다는 것을 의미합니다. 그리고 제가 정기적으로 실행하는 큰 제한 사항입니다. 구조체도 없고, enum도 없으며 제네릭도 없다는 것을 의미합니다.

해결 방안

다음은 제가 사용하는 해결 방안입니다. Swift의 경우 저는 Objective-C 호환이 되는 클래스를 만들어서 모든 저장된 속성의 래퍼 역할을 하게 만듭니다. 그리고 클래스를 Swift 익스텐션에서 사용합니다. Objective-C의 경우엔 그 클래스의 인스턴스에 대한 속성을 메인 클래스 정의에 추가합니다. 이렇게 작업해두면 모든 것이 Swift 코드에서 발생합니다. 그 속성은 Swift에서만 사용할 수 있는 기능을 사용할 수 있으며 Objective-C에서 클래스 자체만 볼 수 있습니다.

예제

예를 들어 보겠습니다. 확장시키고 싶은 Objective-C 클래스 이름은 NetworkService라고 가정하겠습니다. 제네릭 Result enum을 사용하기 때문에 Objective-C와 호환되지 않는 클로저의 참조를 저장해야 합니다. NetworkServiceProperties 클래스를 Swift에서만 사용가능한 onDownloadComplete 속성을 Swift 익스텐션 다음에 정의했습니다.

@objc class NetworkServiceProperties: NSObject {
    var onDownloadComplete:
        ((Result<Data, NetworkError>) -> Void)? = nil
}

이 클래스는 그 안의 속성이 그러지 않더라도 클래스 자체는 반드시 Objective-C와 호환이 돼야 합니다. (저는 또한 클래스 정의에 속한 코드와 잘 어울릴 것이라 생각하고 클래스 정의를 중첩하려고 했습니다. 슬프게도 이는 작동하지 않았습니다. 중첩된 클래스를 참조하는 Objective-C 속성은 Swift 코드에서 보이지 않습니다.)

다음으로 저는 props 속성을 Objective-C 메인 클래스 정의에 추가했습니다. 이는 먼저 NetworkServiceProperties 정의를 필요로 합니다. 왜냐하면 생성된 Swift 헤더는 Objective-C 헤더 파일에 임포트할 수 없기때문입니다.

@import Foundation;
@class NetworkServiceProperties;

@interface NetworkService : NSObject

@property (nonatomic, strong, readonly, nonnull)
    NetworkServiceProperties *props;

@end

마지막으로 NetworkService 이니셜라이저에는 새로운 속성을 넣겠습니다.

#import "NetworkService.h"
#import "MyApp-Swift.h"

@implementation NetworkService

- (instancetype)init {
    self = [super init];
    if (self) {
        _props = [NetworkServiceProperties new];
    }
    return self;
}

// ...

가능하면 Properties 클래스에 Objective-C와 호환되는 이니셜라이저를 넣으려고 합니다. 그렇게 되면 이 클래스는 메인 클래스의 init 함수에서 호출할 수 있습니다. 몇몇 속성이 특별한 이니셜라이저를 필요로해서 작동하지 않는다면 Swift에서만 사용되는 속성을 초기화하는 Swift 익스텐션에서 swift_init (또는 비슷한) 함수를 정의하면 됩니다. 그리고 클래스의 정규 이니셜라이저에서 swift_init을 호출합니다. 이 방법은 클래스의 초기화 규칙을 Objective-C 컴파일러가 잡는 것은 아니기 때문에 작동합니다.

끝입니다. 이제 익스텐션 어디서든 props를 통해 속성에 접근할 수 있습니다.

extension NetworkService {
    func doSomething() {
        // ...
        // let result = Result(...)
        props.onDownloadComplete?(result)
    }
}

마치며

저는 이 방법이 정말 마음에 듭니다. 이 방법은 정말 간단합니다. 여튼 여러분은 이 방법이 완전히 명백하다고 할 수도 있는데, 저도 동의합니다. 이 프로젝트를 생각해내기까지 몇 개월이 걸렸습니다. 여러분에게 도움이 됐으면 좋겠네요.

마이그레이션이 끝나고 Objective-C 클래스 정의를 완전히 삭제해도 될 때는 속성 정의를 NetworkServiceProperties에서 클래스(이제는 Swift로 작성된)로 이동해서 삭제하기만 하면 됩니다.