Objective-C 클래스를 Swift로 옮기기 : 단편적으로 접근하기
2018.08.09
#Objective-C • #Swift
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로 조금씩 변환하기
다음과 같은 상황을 생각해봅시다
-
복잡하고 거대한 Objective-C 클래스가 있습니다.
-
우리는 이 클래스에 새로운 기능을 추가하거나 클래스의 일부를 조금이라도 바꾸고 싶습니다. 그리고 우리는 새롭거나 변경된 코드를 Swift로 작성하기를 선호합니다.
-
Swift로 클래스 전체를 다시 작성하는 것은 지금 당장의 목적은 아닙니다. 정기적으로 유지보수하면서 1년에서 2년동안 꾸준히 단편적으로 바꿔가는 것이 더 훌륭한 방법입니다.
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로 작성된)로 이동해서 삭제하기만 하면 됩니다.