확장 패턴

스위프트의 확장(Extension) 기능을 사용하면 기존 데이터 유형의 기능을 추가하거나 수정할 수 있습니다.

개요

다른 언어들과 다른 스위프트만의 확장 패턴의 기능

이 자체로만 본다면 다른 언어들의 공통된 기능입니다. 예를 들자면, 객체지향에서 관점에서 본다면 이건 부모의 객체를 자식 객체가 물려받고 나서 오버 라이딩 또는 오버 로딩 기능이 있겠네요.

그러나 스위프트의 확장기능의 필수적으로 사용해야 할 만한 매력적인 기능으로 만드는 두 가지의 기능을 추가로 제공합니다.

  • 프로토콜을 확장할 수 있고, 구체적인 유형(Implementation)을 확장 할 수도 있습니다. 즉, 한 번에 모든 유형에 기능을 추가할 수 있습니다.
  • 확장 기능에 제약 조건을 추가하여 특정 대상의 하위 집합에서만 수정 내용을 적용할 수 있습니다.

Decorator 패턴 vs Extension 패턴

확장 패턴은 Gang of Four의 Decorator 패턴을 호출하는 것과 유사합니다. 공식적으로 데코레이터 패턴은 기존 객체의 동작을 런타임에 수정해야 하지만 스위프트의 확장 기능은 해당 방법으로 추가할 수 없으며 항상 데이터 유형의 모든 인스턴스에 적용해야 합니다. Apple은 데코레이터 패턴과 확장 패턴 간의 차이점을 설명하면서 다음과 같이 말했습니다. “데코레이터가 추구하는 목적은 같지만, 우리는 다른 방식으로 구현했다

비록 데코레이터 패턴의 장점을 포기해야 하지만, 포기한 만큼 보다 더 장점이 있는 것은 분명하다고 생각합니다.

확장 패턴 사용 시 주의 사항과 런타임 시, 구분방법

확장이 기존 데이터 유형에 메소드를 추가하면 해당 유형의 메소드와 다르게 처리합니다. 한가지 주의해야 할 점은 런타임 시, 같은 메소드라도 원래 제공하던 것인지, 확장 기능에서 제공된 메소드인지 구분할 수가 없습니다. 그러나 확장은 대부분의 저장된 속성을 추가할 수 없으므로 계산된 속성을 사용하거나 연관 스토리지 패턴을 사용하여 저장된 속성을 시뮬레이션합니다.

Objective-C 클래스 확장 vs Swift 확장

일반적으로 스위프트의 확장은 Objective-C 범주와 동일하지만 아주 같다고 말할 수는 없습니다. @objc 를 사용하는 Objective-C 타입의 클래스를 확장하면 Objective-C 범주와 동일하지만, 스위프트에서 제공하는 형식의 경우는 표시되는 코드에만 적용됩니다. 이러한 이유는 Objective-C에서는 어떠한 헤더가 선언되어 가져올지 여부와 상관없이 스위프트에서는 더 안전한 접근 방법으로 가져와야하기때문에 범주의 범위가 더 광범위할 수밖에 없다고 판단하기 때문입니다.

Objective-C와 스위프트의 차이점은 스위프트가 Objective-C가 사용할 수 없는 확장을 생성할 수 있다는 점입니다. 특히 스위프트는 프로토콜의 확장 개념을 가지고 있지만, Objective-C는 프로토콜에 선언된 확장을 사용할 수 없습니다.

주요 용도

논리적 그룹화

확장 기능의 주요 용도 중 하나는 지정한 기준에 따라 코드를 논리적으로 그룹화하는 것입니다. 이렇게 하면 관련 메소드 그룹이 같은 확장자에 배치되어 코드를 이해하기 쉽고 나중에 더 유연하게 사용할 수 있습니다.

논리적인 그룹화로 분류하는 방법은 네 가지 주요 분류가 있습니다. 이 중 3, 4번은 SOLID 원칙과 같은 맥락을 합니다.

  1. 적합성 (Conformance)
  2. 가시성 (Visibility)
  3. 소유권 (Ownership)
  4. 목적성 (Purpose)

1. 적합성

가장 일반적인 논리적 그룹화는 프로토콜의 적합성입니다. 이 접근 방식을 사용하여 프로토콜의 준수 없이 데이터 유형을 만들고 다음과 같이 해당 준수 사항을 개별적으로 추가할 수 있습니다.

protocol Compressible { }
protocol Printable { }

struct Image { }

extension Image: Compressible { }
extension Image: Printable { }

해당 방식은 스위프트의 표준 라이브러리가 대부분 이러한 방식으로 작성되어 있습니다. 이 라이브러리는 왜 1000개 정도의 라이브러리가 확장되어 있는지 설명합니다. 스위프트는 클래스가 아닌 구조체에 크게 의존하므로 확장 클래스를 서브 클래싱을 사용하지 않고 기존 유형 및 프로토콜을 빌드하는 유일한 방법이기 때문입니다.

이 접근 방식이 개발자들에게 인기 있는 두 가지 중요한 이유가 있습니다. 먼저 개발자가 주어진 확장 프로그램에서 작업해야 하는 지식의 양을 줄입니다. 만약에 개발자가 extension Image: Printable을 추가로 확장이 필요한 경우, Printable이라는 단 하나의 프로토콜을 이해만 하면 되겠죠?. 다른 한편으로, 만약 그 확장이 또한 3개의 다른 프로토콜을 추가한다면, 개발자들은 리뷰를 통해 다양한 프로토콜의 방법들이 상호간의 연관성에 대한 고민해야 하는 양이 늘어나 혼란스럽다고 바로 알 수 있을 것입니다.

2. 가시성

프로토콜 확장을 그룹화하는 두 번째 이유는 규칙성입니다. 확장 프로그램의 적합성이 맞지 않아 제거해야 한다거나, 또는 대체하려고 하는 경우에 매우 쉽게 할 수 있도록 해야 합니다.

예를 들어, Image 구조체는 현재 Printable 프로토콜을 준수하지만, Printable보다 더 상위의 프로토콜(Rasterizable)로 업그레이드를 하려는 경우, 흩어져있는 메소드를 찾는 것보다 한 곳에서 모든 것을 변경할 수 있어야 합니다. 이와 마찬가지로 누군가 Image가 SecurePrintable이라는 프로토콜을 준수해야 하고, 반면에 Printable 프로토콜을 준수하지 않아야 한다고 결정을 내릴 경우, 단 몇 초 만에 해당 프로토콜은 제거할 수 있어야 합니다.

확장 프로그램을 그룹화하는 방법 중 가장 선호도가 높은 방법으로는 다른 개발자들이 자신 개발한 코드의 확장 프로그램에 대해 잘 읽힐 수 있도록 가시성을 제공해야 하기 때문입니다.

예를 들어, Image 구조체에는 공용으로 사용하는 경우에는 URL 타입을 주입받아 생성되어야 하고, 또한 내부적으로 생성할 경우, Data 타입을 주입받고 생성해야 합니다. 이것을 공개 설정과 비공개 설정으로 분리하면 다음과 같은 코드를 작성하게 됩니다.

struct Image {
  init(from url: URL) {

  }
}

extension Image {
  private init(data: Data) {

  }
}

이 접근 방식의 장점은 코드를 읽는 사람들에게 명확성 제공합니다. 다른 개발자들은 Image 구조체를 보고 그것을 어떻게 사용해야 하는지 알고 싶다면, 몇몇은 공개 메소드 , 몇몇은 일부 비공개 메소드, 몇몇은 좀 더 공개된 메소드 등으로 코드를 해석하고 싶지 않습니다. 그들에게 좋은 경험을 주고 싶은 경우에는 공개 확장 그룹을 보고 나머지는 무시할 수 있어야 합니다.

3. 소유권

확장프로그램을 그룹화하는 세 번째 방법은 소유권 기반으로 하며, 대개 코드가 생성되거나 외부에 확장프로그램을 제공해야 하는 경우입니다. 이 원칙은 SOLID 중 2번째 원칙인 OCP(개방 폐쇄 원칙)와 같은 맥락입니다.

예를 들어, Xcode에서 코어 데이터는 모델 클래스를 생성할 수 있지만, 생성된 코드를 외부에서 수정해버리면, 다시 클래스를 재생성 되었을 때 변경 내용이 손실되기 때문에 위험할 수 있습니다.

이러한 문제를 해결하기 위해 XCode는 확장을 사용합니다. UserXcode 라는 클래스가 있으면 Author+CoreDataClass.swift라는 파일을 생성하고 Author+CoreDataProperties.swift라는 파일이 User와 함께 User 모델의 저장된 속성 및 메소드가 구현되는 위치에 확장기능이 포함됩니다. 이 접근 방식을 사용하면 Xcode는 코드를 다시 생성해야 할 때 확장 파일만 변경할 수 있으며 클래스의 변경 사항은 그대로 유지됩니다.

Tip: 일반적으로 확장 기능은 저장된 속성을 추가할 수 없다고 생각되지만, 코어 데이터에서 몇몇 예외가 있을 경우 @Managed 라는 어노테이션을 사용하여 확장에 새로운 저장된 속성을 삽입합니다. 즉, 코어 데이터는 실제로 클래스에 속하는 저장공간(스토리지)과 반대로, 애플리케이션이 실행될 때 이러한 속성을 저장할 수 있는 실제 공간을 제공하는 역할을 담당하게 되기 때문입니다. [ 코어 데이터 경우에 한정 ]

4. 목적성

코드를 확장하는 마지막 방법은 목적성입니다. 이 방법은 SOLID 중 첫 번째인 SRP (단일 책임 원칙)과 같은 맥락입니다.

대규모 데이터 유형을 사용하는 경우 조직에 대해 극도로 주의를 기울이지 않는 한 모든 것을 하나의 거대한 정의로 유지하는 것은 매우 정신적으로 어려울 수 있습니다. 실제로 SwiftLint는 파일이 400라인이 넘으면 경고를 시작하고 1000줄이 넘으면 오류를 발생시킵니다.

목적에 따라 코드를 구성한다는 것은 하나의 큰 유형을 몇 개의 작은 확장으로 분할하고 각 확장이 특정 유형의 메소드를 담당한다는 것을 의미합니다. 예를 들어, 주요한 유형 정의에서 핵심 비즈니스 로직을 구현한 다음, 하나의 확장에 코어를 추가하여 불러오기를 구현하고, 또 다른 확장에 저장 기능을 구현하는 경우가 있습니다.

프로토콜을 확장하는 것과 마찬가지로 목적에 따라 확장하면 코드를 읽거나 수정하는 동안 머릿속에 기억해야 하는 정보의 양이 적어집니다. 예를 들어 데이터 저장을 처리하는 확장 프로그램 작성하다 보면 다른 확장 프로그램인 저장된 데이터를 불러오기 하는 기능 구현을 잊어버릴 수 있습니다. 이 경우에 서로 섞이지 않기 때문에 문제가 되지 않습니다.

이는 시스템의 두 부분의 기능적으로 독립적인 경우에 특히 중요합니다. 예를 들어, 스위프트의 String(NSString) 에는 렌더링 컨텍스트에 텍스트를 그릴 수 있는 다양한 기본 제공 드로잉 기능이 있습니다. Apple은 문자열 API와 드로잉 API를 모두 소유하고 있기 때문에 함께 사용할 수 있지만, 코드를 확장할 때에는 분할하게 작성되었습니다. 이는 용도에 따라 항목을 그룹화하고 Apple에서 iOS와 macOS 코드를 더 쉽게 분리할 수 있기 때문입니다.

논리적 그룹을 사용하면서 가져오는 이점

어떤 접근법을 확장에 적용하든지 상관없이 모든 파일에 확장 기능을 추가할 수 있다는 이점이 있습니다. 이는 개발자가 동일한 파일에서 작업할 가능성이 작기 때문에 여러 개발자가 공동 작업을 할 경우, 소스 제어를 더 쉽게 만듭니다. 또한 Xcode 컴파일은 변경된 작은 확장에 대해서만 추가로 빌드하기 때문에 컴파일 시간 또한 전체 파일 컴파일 시간보다 단축할 수 있습니다.

마지막으로, 초기화 패턴에서 언급한 구조체 초기화 메소드가 없을 경우, memberwise 초기화 메서드가 자동으로 확장 기능으로 추가가 됩니다. 구조체 자신의 초기화 메소드를 구현하지 않는 한 모든 구조체는 기본적으로 memberwise 초기화 메소드가 함께 제공되지만, 만약에 초기화 메소드를 직접 구현하게 될 경우에는 memberwise 초기화 메소드는 사라집니다.

memberwise 초기화 메소드를 유지하면서 따로 초기화 메소드를 추가하려면, 해당 개발자가 직접 구현한 초기화 메소드를 확장에 작성하게 되면 스위프트는 두 가지 모두 제공하게 됩니다.

기능 대체

새로운 기능을 추가하는 것뿐만 아니라, 확장 기능도 기존의 기능을 교체할 수 있습니다. 하지만 여기서 매우 조심하지 않으면 개발자의 의도한 대로 작동 안 하는 버그의 가능성이 있기 때문에 좀 더 신중하게 사용해야 합니다.

Apple의 자체 데이터 유형에 정의된 메소드를 포함하여 모든 메소드를 대체하려면 다음과 같이 확장자는 덮어씁니다.

extension String {
    func trimmingCharacters(in set: CharacterSet) -> String {
        print("I don't think so!")
        return self
    }
}

이 코드를 적용할 경우 기존 trimmingCharacters(in:) 가 작동되지 않고 I don’t think so! 라는 메시지가 출력되겠죠? 그러나 빌드 시 에러가 발생합니다.

이러한 메소드 재정의는 기존 메소드를 액세스할 수 없기 때문에 기존 기능을 향상하는 데 사용할 수 없습니다. 따라서 원본에서 제공하는 모든 기능을 구현하고 이를 증명할 수 있는 몇 가지 테스트를 거쳐 사용하시길 바랍니다.

제약 조건

스위트프의 강점 중 하나는 원하는 기준에 따라 확장 기능을 제한할 수 있다는 것입니다. 이렇게 하면 변경 내용을 상속하는 형식을 제한할 수 있으므로 유형 검사에 실패한 메소드를 작성할 수 있습니다.

예를 들어 다음과 같은 숫자 배열이 있습니다

let numbers = Array(1...100)

이 숫자 배열의 확장에 total이라는 모든 배열의 숫자를 합산하는 계산하는 기능을 추가하려면 다음과 같이 작성할 수 있습니다

extension Array {
   var total: Element {
     return reduce(0, +)
   }
}

그러나 컴파일이 되지 않습니다. 스위프트는 배열에 포함된 내용을 알지 못하기 때문에 + 연산자를 지원하지 않거나 여러 유형이 혼합되어 있을 수 있습니다.

다음과 같은 숫자 항목에 포함하는 배열에서만 확장 기능을 사용할 수 있다고 표시할 수 있기 때문에 제약 조건이 올 수 있습니다.

extension Array where Element: Numeric {
   var total: Element {
     return reduce(0, +)
   }
}

확장에 대한 구체적인 타입을 명시하는 제약 조건을 추가하였기 때문에 +가 숫자에 부합하는 유형이기 때문에 사용이 가능해집니다. 물론 컴파일도 잘됩니다.

하나의 확장에 원하는 만큼의 제약 조건을 추가할 수 있으며 각 제약 조건은 쉼표로 구분됩니다. 다음 == 과같이 구체적인 유형 제약 조건을 추가 할 수도 있습니다. 예를 들어, Numeric 보다 하위의 있는 Int를 추가해봅시다

extension Array where Element == Int { ...

정확한 타입의 제약 조건으로 인해 Int8, UInt64 등을 제외한 Int 타입에서만 영향을 받게 되었네요

이러한 확장 제한 조건은 충돌을 해결할 때 매우 유용합니다. 두 확장이 동일한 유형에 적용할 메소드를 선언할 때 말입니다.

여기서 스위프트의 규칙은 간단합니다. 가장 제한적인 확장이 사용됩니다. 예를 들어, 다음 코드의 확장을 만듭니다.

extension Collection where Element: Hashable {
  var isUnique: Bool {
     return self.count == Self(self).count
  }
}

해당 코드는 Hashable 를 사용하는 모든 컬렉션 타입에 isUnique라는 계산된 속성을 추가하게 됩니다. 이렇게 하면 set(중복이 허용되지 않는) 컬렉션에도 넣을 수 있으며, 원래의 컬렉션과 세트의 크기가 동일한지 확인 할 수 있게 됩니다.

예를들어 해당 코드를 사용하면 두 행 모두 false를 반환하게 됩니다.

[1,2,3,4,5,1].isUnique
"the rain in Spain".isUnique

그러니 isUnique 구현이 문자열에 적합하지 않다고 내부적으로 결정할 수도 있습니다. 내부적으로 검토 결과는 해당 검증을 문자가 아니라 단어로 검증하자고 합니다.

스위프트의 String에는 동일한 계산 속성을 구현하는 확장 프로그램을 작성할 수 있습니다

extension String {
  var isUnique: Bool {
     let words = self.components(separatedBy: " ")
     return words.count == Set(words).count
  }
}

해당 데이터 유형으로 교체될 수 있는 이유는 컬렉션의 포함된 프로토콜의 유형보다 더 제약되기 때문에, String의 isUnique 설정 값을 해당 코드로 대체되었습니다. 이제 the rain is Spain은 더 이상 false로 나오지 않게 됩니다

만약 두 개의 확장이 동등하게 제한되면 두 가지 중에 하나를 채택하게 됩니다. 하지만 두 확장 모두가 동일한 빌드 타깃에 있을 경우, 스위프트는 오류가 발생합니다. 이 중 하나를 제거하거나 더 상세한 제한으로 코드를 수정 해야 합니다.

개요

비록 확장이 저장 속성을 추가할 수는 없지만, 확장은 기존 유형을 확장하거나 심지어 현명하게 사용될 때 전체 메소드를 대체하는 강력한 방법입니다. 확장은 모든 스위프트 코드 베이스에 일반적으로 사용되기 때문에 학습 및 이해하는데 시간을 투자 할 가치가 있습니다.

확장 프로그램을 최대한 활용하려면 다음 권장 가이드 라인을 따르시길 바랍니다.

  • 확장 기능을 사용하여 기능을 논리적으로 그룹화하세요
  • 기존 기능을 대체 할 경우, 만드시 헤당 기능을 제대로 숙지하시고 대체하시길 바랍니다. 예를 들어, 애플의 사용자 인터페이스 라이브러리인 AppKit과 UIKit에는 놓치기 쉬운 사례가 많습니다.
  • 확장 기능이 적용되는 위치를 제한하고, 기능으로 네임 스페이스를 오염이 되는걸 방지 하기위해, Swift API 가이드라인을 준수 하여 만드시길 바랍니다.

해당 포스팅은 Swift Design Patterns글을 번역하였습니다.

8 Shares:
You May Also Like
Read More

초기화 패턴

스위프트의 초기화는 다른 언어들에 비해 유연하고, 기발하며, 강력하다고 생각합니다. 매우 구체적인 규칙을 따르고, 몇몇은 다른 주요 프로그래밍 언어들과 완전히 다른 방식으로 작동하며 그 규칙을 조금이라도 변형 할 수 있는 범위 또한 없습니다. 스위프트의 아버지라고 할 수 있는 Objective-C 의 초기화도 다른 언어들과의 독특하게 작동하기 때문에 스위프트 또한 그대로 유지되었을까 하는 추측입니다.
Read More

프로토콜 패턴

프로토콜이란 사전적 의미로 조약 또는 규약이라는 뜻으로, 특정 작업이나 기능의 일부분을 구현하기 위해 필수, 선택적인 유형을 정의한 인터페이스를 지칭합니다. 프로토콜을 실제로 정의라는 부분을 담당하기 때문에 인터페이스로만 사용이 되며, 구체적인 세부 프로세스가 없기 때문에 이는 추상체라고도 불립니다.