2. Framework: 이름충돌

2. Framework: 이름충돌

지난 포스트에서 framework에 대해서 알아보았습니다. 이번 포스트에서는 여러 framework를 한 프로젝트에서 사용할 때 발생할 수 있는 이름 충돌에 대해서 알아보려고 합니다.

여러 framework를 사용하다보면 중복된 이름을 가진 심볼이 발생할 수 있습니다.
예제를 만들어서 두개의 framework에 중복된 심볼이 있는 경우 어떻게 동작하는지 확인해보겠습니다.



1. Dynamic library test

먼저, Framework1.framework를 생성하고 MyClass1과 TestC 파일을 아래와 같이 생성합니다.
enter image description here

// TestC.h
#ifndef TestC_h
#define TestC_h

#include <stdio.h>

extern void CommonTestFunc(void);

#endif /* TestC_h */

// TestC.c
#include "TestC.h"


void CommonTestFunc(void) {
    printf("Framework1에서 CommonTestFunc가 호출되었습니다.\ns");
}

// MyClass1.h

#import <Foundation/Foundation.h>

@interface MyClass1 : NSObject

- (void)execute;

@end


// MyClass1.m
#import "MyClass1.h"
#import "TestC.h"

@implementation MyClass1

- (void)execute {
    CommonTestFunc();
}

@end

이번에는 Framework2.framework를 생성하고 MyClass2와 TestC파일을 생성합니다.
enter image description here

// TestC.h
#ifndef TestC_h
#define TestC_h

#include <stdio.h>

extern void CommonTestFunc(void);

#endif /* TestC_h */


// TestC.c
#include "TestC.h"

void CommonTestFunc() {
    printf("Framework2에서 CommonTestFunc가 호출되었습니다.\n");
}



// MyClass2.h
#import <Foundation/Foundation.h>

@interface MyClass2 : NSObject

- (void)execute;

@end



// MyClass2.m
#import "MyClass2.h"
#import "TestC.h"

@implementation MyClass2

- (void)execute {
    CommonTestFunc();
}

@end

Framework1과 Framework2에서 CommonTestFunc()가 모두 정의되어 있어 두 framework를 동시에 사용할 경우 중복된 심볼이 발생합니다.

single앱으로 MySample 프로젝트를 생성해서 Framework1과 Framework2를 추가한 다음 MyClass1과 MyClass2를 각각 호출해보겠습니다.


#import "ViewController.h"
@import Framework1;
@import Framework2;

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    MyClass1 *class1 = [[MyClass1 alloc] init];
    [class1 execute];
    
    MyClass2 *class2 = [[MyClass2 alloc] init];
    [class2 execute];
}

@end


// 실행결과
// Framework1에서 CommonTestFunc가 호출되었습니다.
// Framework2에서 CommonTestFunc가 호출되었습니다.

같은 이름을 사용하였지만 framework내에서는 독립적으로 각각 호출되어 사용되었습니다.


2. Static library test

지금까지는 dynamic library로 동작한 framework였습니다.
framework를 static library로 생성할 경우는 동작이 어떻게 될가요?

Framework1과 Framework2를 아래와 같이 static Library로 생성합니다.
enter image description here

MySample에 Framework1과 Framework2를 교체한 후 다시 실행하면 아래와 같은 결과를 볼 수 있습니다.

// 실행결과
// Framework2에서 CommonTestFunc가 호출되었습니다.
// Framework2에서 CommonTestFunc가 호출되었습니다.

dynamic library방식과 다르게 static library 방식에서는 Framework1에 정의된 CommonTestFunc가 호출되지 않고 Framework2에 정의된 CommonTestFunc만 호출되는 것을 확인할 수 있습니다. static library는 빌드할 때 실행파일에 복사되기 때문에 Framework1의 CommonTestFunc가 복사된 뒤 다시 Framework2의 CommonTestFunc가 복사되면서 심볼을 덮어 쓰기때문에 Framework1의 CommonTestFunc가 호출되지 않습니다.

dynamic library는 실행파일에 복사되지 않고 필요할 때 메모리에 로드되는 방식입니다. dynamic 방식도 동시에 메모리에 로드된다면 중복된 심볼은 둘 중에 하나는 undefined될 것으로 보이나 둘다 모두 호출되었습니다.
무엇 때문에 dynamic library방식은 둘다 동작할 수 있었을가요?

그것은 two-level namespace 때문에 같은 이름의 심볼이어도 호출될 수 있었습니다.



3. two-level namespace란?

two-level namespace란 무엇일가요? 단어 뜻 그래도 두단계의 이름 영역을 사용하는 것입니다.
CommonTestFunc는 같은 이름의 심볼이지만 Framework1과 Framework2의 다른 이름의 framework에 사용되고 있습니다.
Framework1과 Framework2를 1단계 이름으로 둔다면 CommonTestFunc는 Framework1_CommonTestFunc과 Framework2_CommonTestFunc와 같은 방식의 이름이 될 수 있습니다. 2단계 이름 영역을 사용하면 같은 이름의 심볼이 사용되어도 각각 구분할 수 있습니다. dynamic framework에서는 two-level namespace가 기본 설정이기 때문에 샘플코드 실행 시 각각 호출될 수 있었습니다.
framework가 two-level namespace가 적용되어 있는지 확인하는 방법은 아래와 같습니다. (디바이스버전으로 빌드해야 확인이 가능)

> otool -hv '파일명'

enter image description here

dynamic library에서 two-level namespace때문에 각각 호출될 수 있었던 것인지 확인하기 위해서 이번에는 two-level namespace를 제거한 후 테스트해보겠습니다.
Framework1과 Framework2를 다시 dynamic library로 설정한 후 other linker flags에 '-flat_namespace’를 추가합니다.
enter image description here
flat_namespace는 bitcode를 지원하지 않기 때문에 enable bitCode를 no로 변경합니다.
enter image description here

otool로 Framework1과 Framework2를 확인해보면 아래 이미지와 같이 two-level namespace가 제거된 것을 확인할 수 있습니다.
enter image description here

다시 MySample에 Framework1과 Framework2를 교체한 후 실행하면 static library와 같은 결과를 볼수 있습니다.

// 실행결과
// Framework1에서 CommonTestFunc가 호출되었습니다.
// Framework2에서 CommonTestFunc가 호출되었습니다.

이번 결과를 통해 two-level namespace를 통해 여러 framework들 간에 중복된 심볼이 있더라도 충돌없이 동작할 수 있음을 확인할 수 있었습니다.



이번에는 Framework1과 Framework2에 이름이 같은 Objective-C class를 추가하고 외부에서 직접 가능하도록 추가해보겠습니다.

framework1에는 아래와 같이 CommonClass를 추가합니다.

// CommonClass.h
#import <Foundation/Foundation.h>

@interface CommonClass : NSObject

- (void)execute;

@end



// CommonClass.m
#import "CommonClass.h"

@implementation CommonClass

- (void)execute {
    NSLog(@"Framework1의 CommonClass가 호출되었습니다.");
}

@end

framework2에는 아래와 같이 CommonClass를 추가합니다.

// CommonClass.m
#import "CommonClass.h"

@implementation CommonClass

- (void)execute {
    NSLog(@"Framework2의 CommonClass가 호출되었습니다.");
}

@end

MySample에서 아래와 같이 서로 다른 곳에서 CommonClass를 호출해보겠습니다.

// AppDelegate.m
#import "AppDelegate.h"
@import Framework1;

@interface AppDelegate ()

@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    CommonClass *commonClass = [[CommonClass alloc] init];
    [commonClass execute];
    
    return YES;
}

@end



// ViewController.m
#import "ViewController.h"
@import Framework2;

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    CommonClass *commonClass = [[CommonClass alloc] init];
    [commonClass execute];
}


@end



// 실행결과
objc[996]: Class CommonClass is implemented in both
 /private/var/containers/Bundle/Application/4995DA7D-3173-4A2B-8219-
 2D32299A36DA/MySample.app/Frameworks/Framework1.framework/Framework1 (0x102b4c1e8) and 
 /private/var/containers/Bundle/Application/4995DA7D-3173-4A2B-8219-
 2D32299A36DA/MySample.app/Frameworks/Framework2.framework/Framework2 (0x102b34198). One of the two will be used. Which one is undefined.
2020-02-27 21:14:33.159021+0900 MySample[996:65504] Framework2CommonClass가 호출되었습니다.
2020-02-27 21:14:33.160515+0900 MySample[996:65504] Framework2CommonClass가 호출되었습니다.

중복 심볼이 발생해서 하나의 종복 심볼이 undefined된다고 출력되고 Framework2의 CommonClass만 실행되었습니다. dynamic framework는 two-level namespace를 사용하기때문에 중복심볼이어도 사용할 수 있다고 위 예제에서 확인했는데 이번에는 왜 하나의 심볼이 undefined가 될가요? two-level namespace의 동작범위가 내부에서만 유효하기 때문일 것입니다. Framework1의 Myclass1과 Framework2의 MyClass2에서 CommonClass를 호출하면 자신의 CommonClass가 정확히 호출됩니다.



4. dynamic library 동적로드

dynamic library는 동적으로 로드해서 사용할 수 있다고 하였습니다. 동적으로 로드하는 코드를 구현하면 각각의 CommonClass를 호출할 수 있지 않을가요?

MySample에서 동적으로 로드하는 코드를 작성해 보겠습니다.
먼저 MySample에서 Framework1과 Framework2를 embeded frameworks에만 추가하고 link binary with libraries에서는 제거합니다.
enter image description here

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self executeFramework1];
    [self executeFramework2];
}


- (void)executeFramework1 {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"Frameworks/Framework1" ofType:@"framework"];
    NSBundle *bundle = [NSBundle bundleWithPath:path];
    [bundle load];
    Class CommonClass = NSClassFromString(@"CommonClass");
    id instance = [[CommonClass alloc] init];
    [instance performSelector:@selector(execute) withObject:nil];
    [bundle unload];
}


- (void)executeFramework2 {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"Frameworks/Framework2" ofType:@"framework"];
    NSBundle *bundle = [NSBundle bundleWithPath:path];
    [bundle load];
    Class CommonClass = NSClassFromString(@"CommonClass");
    id instance = [[CommonClass alloc] init];
    [instance performSelector:@selector(execute) withObject:nil];
    [bundle unload];
}

@end


// 실행결과
2020-02-27 21:44:04.227702+0900 MySample[1265:80337] Framework1CommonClass가 호출되었습니다.
objc[1265]: Class CommonClass is implemented in both /private/var/containers/Bundle/Application/B684FDB1-00AC-446D-86FD-85F4377E376A/MySample.app/Frameworks/Framework1.framework/Framework1 (0x10390c218) and /private/var/containers/Bundle/Application/B684FDB1-00AC-446D-86FD-85F4377E376A/MySample.app/Frameworks/Framework2.framework/Framework2 (0x1039241c8). One of the two will be used. Which one is undefined.
2020-02-27 21:44:04.280982+0900 MySample[1265:80337] Framework1CommonClass가 호출되었습니다.

Framework1을 동적으로 로드 후에 다시 unload를 하였지만 Framework2를 동적로드하면 중복 심볼이 발생했다고 경고가발생하며 Framework2가 아닌 Framework1에서 이미 로드한 CommonClass가 한번 더 실행됩니다.
unload를 하면 로드한 class 정보도 해제되어 Framework2의 CommonClass를 접근할 수 있을거라 생각했지만 뜻대로 되지 않았습니다.
unload 관련해서 Apple문서에 다음과 같이 정의된 내용이 있습니다.

unload은 cocoa 런타임 환경에서 사용하는데 제한되어 있습니다. Objective-C 런타임에서는 Objective-C의 심볼 unloading을 지원하지 않습니다. 따라서 cocoa 번들에서 한번 로드한 것은 다시 unload할 수 없습니다.

Objective-C에서는 unloading을 지원하지 않기 때문에 framework를 사용하는 외부에서는 중복된 Objective-C 심볼을 사용할 때 충돌이 발생하는 것입니다.

중복된 C함수는 동적으로 로드해서 사용할 때 unloading을 지원할가요?
MySample을 아래과 같이 작성해서 CommonTestFunc를 동적로드해 보겠습니다.

#import "ViewController.h"
#import <dlfcn.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self executeFramework1];
    [self executeFramework2];
}


- (void)executeFramework1 {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"Frameworks/Framework1" ofType:@"framework"];
    void *handle = dlopen([[path stringByAppendingPathComponent:@"Framework1"] UTF8String], RTLD_LOCAL);
    if (handle) {
        void (*CommonTestFunc)(void) = dlsym(handle, "CommonTestFunc");
        CommonTestFunc();
        dlclose(handle);
    } else {
        printf("dlopen failed! %s\n", dlerror());
    }
}


- (void)executeFramework2 {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"Frameworks/Framework2" ofType:@"framework"];
    void *handle = dlopen([[path stringByAppendingPathComponent:@"Framework2"] UTF8String], RTLD_LOCAL);
    if (handle) {
        void (*CommonTestFunc)(void) = dlsym(handle, "CommonTestFunc");
        CommonTestFunc();
        dlclose(handle);
    } else {
        printf("dlopen failed! %s\n", dlerror());
    }
}

@end


// 실행결과
// Framework1에서 CommonTestFunc가 호출되었습니다.
// Framework2에서 CommonTestFunc가 호출되었습니다.

Objective-C와는 다르게 동적로드로 이름이 같은 서로 다른 함수를 호출할 수 있었습니다.




지금까지 Framework의 이름 충돌에 대해서 알아보았습니다. framework를 만들 때 이름을 짓는 부분이 중요하며 특히 외부로 노출되는 API는 유니크한 이름을 지어야 충돌을 방지할 수 있다는 것을 알게 되었습니다. 물론, 내부에서만 사용하는 심볼들은 two-level namespace 덕분에 이름을 충돌을 방지할 수 있지만 그렇더라도 이름을 너무 쉽게 짓는 것은 지양해야 합니다.
다음 포스트는 framework의 weak linking에 대해서 알아볼 예정입니다.

참고

댓글

이 블로그의 인기 게시물

dismiss에 대해서 알아봅시다

1. Framework: framework란?

closure에서는 왜 self를 사용해야 할까?