예거's Bicycle for the mind

[Swift] NameSpace(네임스페이스)란 무엇이고, 어떻게 만들면 좋을까? 본문

iOS & Swift

[Swift] NameSpace(네임스페이스)란 무엇이고, 어떻게 만들면 좋을까?

유예거 2021. 10. 17. 17:33

네임스페이스(NameSpace) 란?

네임스페이스는 연관된 값들을 한 공간에 이름을 지어 모아둔 공간을 말한다.

쉽게 예를 들면, 우리가 '서랍'에 물건을 보관할 때, 그 안에 뭐가 들었는지 '라벨링'하는 것과 비슷하다.
유사한 물건들을 모아둠으로써, 관리(유지보수)가 쉬워지고 재사용도 편리해진다.


네임스페이스를 통해서만 문자열에 접근할 수 있게 만드는 캡슐화(Encapsulation) 방법이기도 하다.

 

서랍 안에 뭐가 들었는지 이름을 붙여두는 센스


코딩을 할 때도 이런 네임스페이스를 잘 만들어두면, 하드코딩도 방지하고 코드 가독성도 좋아진다.

이번 글에서는, 출력을 위해 사용할 문자열(String)들을 모아두는 네임스페이스를 만든다고 가정해보자.
네임스페이스를 만드는 방법을 총 6가지로 정리해보겠다.

사실, 6가지 보다 더 다양한 방법으로 네임스페이스를 만들 수 있겠지만, 지금 나의 스위프트 수준에서는 더 많은 종류를 찾기보다, 학습한 6가지를 시의적절하게 사용하는 게 더 중요하다고 생각한다.

 

1. 열거형에 원시값(rawValue)을 사용하는 방법

먼저, 어쩌면 가장 기본적인 형태라고 할 수 있는 열거형의 원시값(rawValue)을 사용한 네임스페이스다.

// 열거형에 원시값(rawValue) 사용
enum enumWithRawValue: String {
    case introMessage = "반갑습니다."
    case exitMessage = "종료합니다."
}

print(enumWithRawValue.introMessage.rawValue) // 반갑습니다.
print(enumWithRawValue.exitMessage.rawValue) // 종료합니다.


원시값을 사용해서 메시지를 출력할 때, 우리는 열거형의 인스턴스를 생성한 뒤, 그 rawValue 를 꺼내오는 방식을 사용하게 된다.

이 경우, 코드 전체에서 메시지 출력이 반복된다면, 그때마다 인스턴스를 불필요하게 여러 번 생성하게 된다.
그렇다면 미미할지라도 메모리가 낭비되지 않을까?

단순하게 메시지만 저장해두고 가져오는 게 목적인 네임스페이스라면, 굳이 인스턴스까지 생성시킬 필요가 있을까?
게다가, 매번 반복적으로 ".rawValue" 키워드를 붙여야 하는 것도 불편하게 느껴질 수 있다.

> 내용 추가
스위프트의 메모리 관리 기술인 ARC 가 불필요한 인스턴스를 바로바로 해제해서 메모리를 아껴줄 것이라 생각했는데
ARC 공식 문서를 살펴보니, ARC 는 클래스의 인스턴스에만 적용된다고 써있다.
구조체와 열거형은 값타입이라서 ARC 로 관리되지 않는다고 한다. 🧐

 

Reference counting applies only to instances of classes. Structures and enumerations are value types, not reference types, and aren’t stored and passed by reference.

 

2. 열거형에 인스턴스 메서드(Method)를 사용하는 방법

열거형의 각 case 에 원시값을 지정하지 말고, 대신 인스턴스 메서드를 만들어서
각 케이스에 해당하는 String 을 리턴해주는 방법도 있다.

 

// 열거형에 인스턴스 메서드 사용
enum enumWithInstanceMethod {
    case intro
    case exit
    
    func message() -> String {
        switch self {
        case .intro:
            return "반갑습니다."
        case .exit:
            return "종료합니다."
        }
    }
}

print(enumWithInstanceMethod.intro.message()) // 반갑습니다.
print(enumWithInstanceMethod.exit.message()) // 종료합니다.


흠... 개인적으로 이 방법은 별로다.


원시값과 동일하게, 여전히 열거형의 인스턴스를 만들게 되고, 메서드를 사용하다 보니, message() 문법으로 사용해야 해서, 코드가 지저분해진 느낌이 든다.

 

3. 열거형에 인스턴스 연산 프로퍼티(Computed Property)를 사용하는 방법

마찬가지로 인스턴스를 만들지만, 이번엔 메서드가 아니라 연산 프로퍼티로 String 을 리턴해주는 방법도 있다.

 

// 열거형에 인스턴스 연산 프로퍼티 사용
enum enumWithComputedProperty {
    case intro
    case exit
    
    var message: String {
        switch self {
        case .intro:
            return "반갑습니다."
        case .exit:
            return "종료합니다."
        }
    }
}

print(enumWithComputedProperty.intro.message) // 반갑습니다.
print(enumWithComputedProperty.exit.message) // 종료합니다.


메서드를 사용하는 방법과 비교했을 때, message 끝에 괄호(Parenthesis)를 안 붙여도 되니, 코드가 좀 더 깔끔해진다는 장점이 있다.

 

4. 열거형에 CustomStringConvertible 프로토콜을 사용하는 방법

생소한 CustomStringConvertible 프로토콜을 이해하기 위해 먼저 아래 예시부터 살펴보자.

이번 글에서 정리하고 있는 열거형의 원시값, 인스턴스 메서드/프로퍼티... 이런 거 하나도 사용하지 않은
그냥 평범한 열거형에서 case를 그대로 print() 함수로 찍으면 어떤 결과가 나올까?

 

// 평범한 열거형의 케이스를 그냥 print 하면 case 이름 자체가 String 으로 출력된다.
enum ordinaryEnum {
    case case1
    case case2
}

print(ordinaryEnum.case1) // case1
print(ordinaryEnum.case2) // case2


그냥 정의한 case 이름이 그대로 String 타입으로 출력되는 걸 알 수 있다.

자 그러면, 열거형의 case 를 그대로 출력했을 때, case 이름이 나오는 게 아니라
내가 원하는 String 이 출력되게 해주는 방법은 없을까? 자연스럽게 생각해볼 수 있다.

그걸 가능하게 해주는 것이 CustomStringConvertible 프로토콜이다!

 

 

Types that conform to the CustomStringConvertible protocol can provide their own representation to be used when converting an instance to a string.


CustomStringConvertible 프로토콜을 준수하는 타입들은 인스턴스를 String 으로 바꿀 때(converting), 각자의 텍스트 표현(their own textual representation)을 사용할 수 있게 된다.

자 그럼 적용해보자!
먼저, 이 프로토콜을 준수하기 위해선 반드시 description 프로퍼티를 정의해야 한다.

 

Add CustomStringConvertible conformance to your custom types by defining a description property.


이제 예시 코드를 만들어보자.

 

// 열거형에 CustomStringConvertible 프로토콜 사용
enum enumWithCustomStringConvertible: CustomStringConvertible {
    case introMesssage
    case exitMessage
    
    var description: String {
        switch self {
            case .introMesssage:
            return "반갑습니다."
            case .exitMessage:
            return "종료합니다."
        }
    }
}

print(enumWithCustomStringConvertible.introMesssage) // 반갑습니다.
print(enumWithCustomStringConvertible.exitMessage) // 종료합니다.


print() 함수 안에 열거형의 case 까지만 넣어줬음에도, case 이름이 그대로 나오지 않고
내가 원하는 문자열이 출력되는 걸 확인할 수 있다.

앗, 근데 프로토콜을 준수하기 위해 만들어진 description 이라는 변수...
바로 위에서 다뤘던 연산 프로퍼티와 똑같이 생겼다.

그렇다면 연산 프로퍼티 처럼 사용할 수도 있을까?

 

// 연산 프로퍼티 처럼 사용할 수도 있지만...
print(enumWithCustomStringConvertible.introMesssage.description) // 반갑습니다.
print(enumWithCustomStringConvertible.exitMessage.description) // 종료합니다.


가능은 하다.
근데, 이렇게 사용할 바엔 CustomStringConvertible 프로토콜을 굳이 추가해준 의미가 없다.

실제로, 공식 문서에 description 프로퍼티에 직접 접근하는 건 권장하지 않는다고 적혀있다.

 

Accessing a type’s description property directly or using CustomStringConvertible as a generic constraint is discouraged.


문법을 간단하게 하기 위해 만든 프로토콜인데, 굳이 프로퍼티까지 직접 접근해서 사용할 필요는 없겠다.

 

5. 열거형에 타입 프로퍼티(static let)를 사용하는 방법


네임스페이스를 만들 때 가장 범용적으로 쓰이는 방법이라고 생각한다. (* 객관적 통계는 없음)

이 방법은 쉽게 말하면, case 없는 열거형이라고 할 수 있다.
열거형을 처음 배울 때, 당연히 case 와 함께 정의되어야 할 줄 알았는데, 그렇게 하지 말라는 법은 없으니까!
바로 예시를 만들어보자.

 

// 열거형에 타입 프로퍼티(static let) 사용
enum enumWithTypeProperty {
    static let introMessage = "반갑습니다."
    static let exitMessage = "종료합니다."
}

print(enumWithTypeProperty.introMessage) // 반갑습니다.
print(enumWithTypeProperty.exitMessage) // 종료합니다.


static let 을 통해 정의한 타입 프로퍼티이기 때문에, 위에서 다뤘던 CustomStringConvertible 프로토콜 처럼

dot syntax 1번(쩜을 1번만 찍었다) 만에 원하는 문자열을 출력할 수 있다.

하지만 정의하는 방법은 훨씬 간단하다.
프로토콜을 준수하게 만드는 것도 아니고, 열거형 안에 새로운 연산 프로퍼티를 넣어주지도 않았다.
즉, 가독성이 매우 좋다.

열거형은 값타입(Value Type)이지만, 스위프트에는 COW(Copy On Write) 장치가 있기 때문에
static let 으로 선언한 문자열을 계속 불러오더라도, 값이 복사되는 게 아니고 참조되는 상태라고 할 수 있다.

정리하자면
열거형 안에 case 를 정의하고 문자열을 꺼내 쓰는 방식은, 문자열을 불러올 때마다 인스턴스를 생성해서 가져오는 방법이다.

만약, 타입 프로퍼티로 문자열을 정의해두면, 프로그램이 실행되자마자 문자열이 메모리에 올라간다.
문자열을 여러 번 불러오더라도, 값의 복사 없이 참조만으로 문자열을 꺼내쓸 수 있는 것이다.
미약하게나마, 메모리를 효율적으로 쓸 수 있다.

 

6. 구조체(struct)에 타입 프로퍼티(static let)를 사용하는 방법

네임스페이스를 만드는 방법으로 계속 열거형만 다뤘는데, 처음으로 구조체가 등장했다.
열거형에 타입 프로퍼티를 넣을 수 있다면, 구조체 또한 가능하다.

 

// 구조체에 타입 프로퍼티(static let) 사용
struct structWithTypeProperty {
    static let introMessage = "반갑습니다."
    static let exitMessage = "종료합니다."
}

print(structWithTypeProperty.introMessage) // 반갑습니다.
print(structWithTypeProperty.exitMessage) // 종료합니다.


열거형에 static let 을 쓴 것과 문법이 똑같다.

타입 프로퍼티를 써서 네임스페이스를 만들기로 결정했다면
열거형과 구조체 중에 뭐가 더 적합할까 고민해보는 게 중요하겠다.

이 고민을 끝낼 포인트는 바로 인스턴스를 만들 수 있는지, 없는지에 있다.

 

// 구조체는 '별도의 처리'를 해주지 않으면 인스턴스를 만들 수 있는 게 당연하다.
let initTest = structWithTypeProperty()


인스턴스를 만들 수 있다는 거 자체가 문제는 아니다.
문제는, 어차피 네임스페이스 목적으로 만든 구조체라서 인스턴스를 만들어도 할 수 있는 게 없다는 것이다.

나 혼자서만 프로젝트를 하면 모르겠지만, 같이 프로젝트를 하는 동료 개발자가 있다면?
그 동료 개발자가 실수로 인스턴스를 만들 수도 있다.

물론 인스턴스 만들어도 할 수 있는 게 없으니, 뭔가 잘못됐음을 깨닫고 수정하겠지만
애초에 불필요한 인스턴스를 만들지 못하도록, 이니셜라이저를 막아보자!

 

// 인스턴스를 만들 수 없도록 막아보자
struct structWithTypeProperty {
    static let introMessage = "반갑습니다."
    static let exitMessage = "종료합니다."
    
    private init() {} // private 접근제어 적용
}

print(structWithTypeProperty.introMessage) // 반갑습니다.
print(structWithTypeProperty.exitMessage) // 종료합니다.

let initTest = structWithTypeProperty()
// 'structWithTypeProperty' initializer is inaccessible due to 'private' protection level


구조체 안에 텅 빈 이니셜라이저를 만들고 private 접근제어를 붙여주면, 인스턴스를 만들 수 없는 구조체가 된다.
이렇게 하면 다른 개발자가 실수로라도 인스턴스를 만드는 걸 방지할 수 있다.

근데...
애초에 열거형에 static let 만 써서 네임스페이스를 만들었다면, 이런 귀찮은 처리를 해줄 필요도 없다.
왜냐면 case 가 없는 열거형은 인스턴스를 만들 수가 없으니까.

 

// case 가 없는 열거형은 인스턴스를 만들 수 없다.
enum enumWithTypeProperty {
    static let introMessage = "반갑습니다."
    static let exitMessage = "종료합니다."
}

print(enumWithTypeProperty.introMessage) // 반갑습니다.
print(enumWithTypeProperty.exitMessage) // 종료합니다.

let initTest = enumWithTypeProperty()
// 'enumWithTypeProperty' cannot be constructed because it has no accessible initializers


case 가 없는 열거형은 accessible initializers 가 없어서 인스턴스를 만들 수가 없다. ㅎㅎㅎ

 

네임스페이스 결론

오직 출력을 위해 사용할 문자열(String)을 모아두는 목적의 네임스페이스를 만든다면
case 없는 열거형을 만들고, static let 선언을 통해 타입 프로퍼티만 만들어서 쓰자!

Comments