momodudu.zip
[Swift/Objective-C] Objective-C로 만들어진 헤더파일을 Framework에서 숨기는 방법 본문
제목 한번 길다...
iOS개발을 하다보면, 사실 C/C++ library에 디펜던시를 가지는 경우게 많다. 그래서 사실 많은 iOS 앱들은 코어는 objective-C & C++로 이루어져 있고, 그 코어를 Swift로 wrapping해서 제공하는 경우가 많다.
swift에서는 property나 method에 대해 framework 내외부 여부와 오버라이딩 여부까지 해서 모두 접근제한자를 지정할 수 있기 때문에, 특정 Property/Method를 프레임웍 내부에서만 볼 수 있게 하고 외부로는 노출하지 않게 하는게 가능하다. 그러나 Objective-C의 경우, property는 가능하지만 method에 대한 접근제한자는 존재하지 않는다. 그래서 해당 헤더파일이 Public으로 지정되면, 프레임웍으로 아무리 감싸봤자 내부에 무슨함수가 있는지 모든 함수들이 프레임웍 내/외부로 노출이 된다.
이번에도 일을 하다가 나온 문제점이, 새로 만든 Objective-C 파일이 프레임워크 내부에만 노출이 되어야 하는데, 외부로도 노출이 되는게 문제점이었다. 사실 framework이런걸 만들어본적 없고, 그냥 코어 내부에 디립따 코딩만 해오던 나는 이런게 문제가 된다는걸 인지조차 못하고 있었는데 사수가 이런것은 문제가 되고, 이렇게 고쳤다 라는걸 알려주셨다.. 완전 리스펙 짱짱맨
그래서 좋은걸(?) 배운거 같아서, 정리할 겸 쓰는 오랜만의 포스팅~
완전 똑같은 고민을 했던 개발자가 미디엄에 올린 아티클이다. 요걸 참고했다 :-)
// MyPrivateClass.h
@interface MyPrivateClass: NSObject
-(void)doSomePrivateFunc:(NSInteger)number;
@end
// MyPublicClass.swift
public class MyPublicClass: NSObject {
func doSomething() {
myClass.doSomePrivateFunc()
}
private let myClass: MyPrivateClass
}
위와 같은 옵씨, 스위프트를 묶어서 하나의 프레임웍으로 제공하고 싶다고 가정한다. 즉, 외부 유저는 이 프레임웍을 import해서 사용할 경우 MyPublicClass의 publicClass.doSomething()과 같은 형태로만 사용하도록 하고싶다.
이럴 경우 Objective-C의 코드를 Swift에서 참조가 가능해야 하므로 Bridging Header 혹은 Umbrella header가 필요하다. 그러나 이 경우 스위프트에서 참조하려면 Umbrella header가 참조하는 파일들은 "public"으로 되어있어야 참조가 가능하다 (...)
즉, 오브젝티브씨에서 편법(?)으로 쓰는 함수 숨기기 기법.. 즉 헤더에는 선언하지 않고 소스파일에만 구현해서 숨기는 기법 같은건 통하지 않는다는 얘기다. 스위프트에서 참조하려면 Public Header파일이 꼭 있어야 한다.
Objective C 파일이 Public으로 지정되어 있어서 결국 이 프레임웍을 임포트하는 외부 유저들은 MyPrivateClass의 존재도 알게되고, MyPrivateClass의 헤더파일을 보면 이 친구가 대충 무슨일을 하는 친구구나... 라는거 까지 유추가 가능하다. 별로 안중요해보이지만, 이런 프레임워크를 OpenAPI로 제공하게 된다면 내부에서 무슨일이 벌어지는지 대략적으로 유추가 가능하므로 보안상에 문제가 발생할 수 있다.
그래서, 결국 이걸 어떻게 숨겨야할까!!!
이 방법의 핵심은 "프레임워크 내부에서만 Objective-C 함수가 노출 가능하고, 프레임워크 내부, 즉 유저에게는 노출하지 않는것"이다. 즉 프레임워크내부에서 Objective-C 모듈 내에 이러이러한 함수가 있다 라는거를 간접적으로 알려주기만해도 좋다는 말이 된다. 즉, 함수 내부에선 뭘 하는지 알 필요 없고 해당 함수의 유무만 있으면 되니까 프로토콜을 사용해도 가능하다는 이야기이다.
즉!!! Objective-C 내부 인터널 함수들을 옵씨 프로토콜로 wrapping해서 이 프로토콜을 public으로 선언하여, swift모듈에서 사용할 수 있게끔 할 수 있다는 얘기다! 백문이불어일견. 스텝바이 스텝으로 한번 보자.
1. 숨기려는 Objective-C와 매칭되는 Swift Protocol을 만든다.
// ObjectiveCToSwift.swift
@objc(MyPrivateClassProtocol)
internal protocol MyPrivateClass {
init()
func doSomePrivateFunc(number: Int)
}
기존에 숨겨지지 않았던 MyPrivateClass objective c class에 매핑될 수 있도록 위와 같이 인터널 프로토콜을 선언한다.
2. Internal Swift Protocol을 노출시키기
위 선언된 MyPrivateClassProtocol은 인터널 프로토콜이므로, Myframework-Swift.h에 자동으로 생성되지 않는다. 그래서 Swift와 objective c로 연결시켜줄 추가적인 헤더가 필요하다.
// SwiftToObjectiveC.h
SWIFT_PROTOCOL_NAMED("MyPrivateClassProtocol")
@protocol MyPrivateClassProtocol {
-(nonnull instancetype)init;
-(void)doSomePrivateFunc:(NSInteger)number;
}
@end
이렇게 직접 작성해줘도 되지만, 귀찮다면 Swift Protocol을 public으로 먼저 빌드한 후 framework-Swift.h에 생기는 헤더 내용을 복붙하고 internal로 바꿔줘도 된다.
3. Objective-C에서 스위프트 프로토콜을 adopt해주기
이제 오브젝티브 씨로 연결된 스위프트 프로토콜을 PrivateClass에서 어답트해준다.
// MyPrivateClass.h
#import <SwiftToObjectiveC.h>
@interface MyPrivateClass: NSObject<MyPrivateClassProtocol>
- (void)doSomePrivateFunc:(NSInteger)number;
@end
4. 프레임워크 내부 Swift에서 사용할 수 있도록 인스턴스 만들어주기(=팩토리패턴)
위의 브릿징 프로토콜을 채택한 MyPrivateClass에서 init이 없으면 Protocol은 인스턴스화 할 수 없다. 그래서 팩토리패턴을 여기서 적용한다.
일단 먼저 아래 예제를 보자.
protocol SomeProtocol {
init()
}
class SomeClass: SomeProtocol {
...
}
// 인스턴스 생성 가능
let instance: Protocol = SomeClass.init()
// 인스턴스 생성 불가능
let instance: Protocol = SomeProtocol.init()
var type: SomeProtocol.Type!
type = SomeClass.self
let instance: SomeProtocol = type.init()
먼저 두가지 방법으로 인스턴스 생성 예제를 보면, 클래스로 init을 할 경우 인스턴스가 생성되고 Protocol.init()을 할 경우 생성되지 않을것이다. 그러나 아래 세줄을 보면 type을 Protocol.Type!으로 지정해놓고, 이 타입에다가 class의 타입을 넣어주면 init을 사용할 수 있게 된다. 즉, type 변수의 타입을 선언해주는 부분은 Swift로부터 가져오고, objective c 클래스 내부에서 위와 같이 인스턴스화 할 수 있다는 말이다. 신기... 이런 방법에 착안해서, 팩토리 클래스를 하나 만들도록 한다.
@objc(Factory)
internal class Factory: NSObject {
private static var classType: MyPrivateClass.Type! // Swift internal Protocol type
@objc static func registerType(type: MyPrivateClass.Type) {
classType = type
}
func createMyPrivateClass() -> MyPrivateClass {
return Factory.classsType.init()
}
}
위에서 SomeProtocol/Clas에서 설명한것과 똑같은 기능을 하는 Factory라는 이름의 클래스로 하나 생성하였다. 즉, Objective-C에서 이 팩토리 클래스를 이용해서 reigsterType함수를 이용해서 PrivateClass의 타입을 넣어주면 스위프트에서 이 변수를 이용해서 인스턴스화 시킬 수 있다는 말이다!!! 이제 위의 factory클래스를 똑같은 방법으로 오브젝티브 씨에도 노출시켜준다.
그리고나서 이제 아래와 같이 objective-C에서 registerType을 실행시켜주는 부분만 작성해주면 된다.
#import "MyPrivateClass.h"
@implementation
+(void)load
{
[Factory registerType: [MyPrivateClass class]];
}
-(void)doSomePrivateFunc:(NSInteger)number
{
....
}
@end
로드 함수는 모듈이 로딩될 때 한번만 호출된다.
브릿징 헤더파일을 이것저것 만들어줘야해서 조금 복잡하지만, 이렇게 되면 objectiveC 모듈을 인터널 스위프트 파일로 랩핑할 수 있게되고, 프레임워크 외부에서는 이 파일을 볼 수 없다.
와우... 엄청나게 복잡하지만 약간 이런 접근제한에 대한 개념이 정립되어있지 않은 나한테는 엄청나게 유용했다!!!
'ios' 카테고리의 다른 글
Xcode Modules, Framework, Library 비교 (0) | 2022.02.18 |
---|---|
jazzy를 이용해서 swift/objective-C로 작성된 코드 개발 문서 작성 (0) | 2019.12.28 |