Swift GYB

“보일러 플레이트 (상용구)” 는 과거 출판 미디어 시대에 생긴 단어입니다. 작은 지역 신문사에서는 매번 꼭 채워야하는 공간이 있었는데, 이 공간을 채울 사람이 부족했었습니다. 그래서 많은 신문사가 일간지 뒷면에 변함없는 콘텐츠 흐름을 가진 대형 인쇄 연합체의 도움을 받았습니다. 이 상황을 보일러를 만들때 미리 만들어놓은 강판으로 만들던 것에 비유해서 보일러 플레이트라는 이름이 붙었습니다.

이러한 비유를 통해 “보일러 플레이트 (상용구)”라느 단어가 유명해졌으며 이 개념은 계약서, 편지 양식, 그리고 NSHipster의 이 주의 게시글 등에 사용되고 있습니다.


모든 코드가 매력적일 수는 없습니다. 실제로 모든 것을 작동시키는 많은 로우 레벨 인프라도 상용구로 가득합니다.

Swift 표준 라이브러리 조차도 인티저 타입에 크기만 다른 (Int8, Int16, Int32, Int64) 를 포함하고 있습니다.

코드를 복사 붙여넣기 하는 것은 일회성 해결법 밖에 안되며 지속 가능하지도 않습니다. (그것도 여러분이 처음에 제대로 만들었다고 가정한다면 그렇습니다.) 시간이 지나면서 구현 방식이 파생되면서 변경점이 생길수록 구현하는 방식에 불일치가 발생할 위험도 있습니다. 돌연변이가 무작위긴 하지만 지구의 생명의 다양성과 닮은 것은 아닌 것처럼 말이죠.

프로그래밍 언어들은 C++ 템플릿과 Lisp 매크로에서 C 프리프로세서 조건문과 eval까지 그 문제에 대비할 다양한 기술을 만들어 두었습니다.

Swift는 내부의 표준 라이브러리도 Swift로 작성되었기 때문에 C++ 메타프로그래밍의 이점을 얻지 못했고 그래서 매크로 시스템이 없습니다. 대신에 Swift 메인테이너들은 작은 템플릿 태그 셋을 사용해서 소스 코드를 생성하는 gyb.py라는 Python 스크립트를 사용합니다.

다른 Python 도구인 GYP(“Generate Your Projects”)를 참고해서 “Generate Your Boilerplate”의 약자인 GYB로 이름을 지었습니다.

GYB의 작동 방식

GYB는 Python 코드를 사용해서 변수 대체 및 흐름 제어를 할 수 있게 해주는 경량화된 템플릿 시스템입니다.

  • %{ <#code#> } 시퀀스는 Python 코드 블럭을 의미합니다.
  • % <#code#>: ... % end 시퀀스는 흐름을 제어합니다.
  • ${ <#code#> } 시퀀스는 표현식의 결과를 대체합니다.

나머지 다른 텍스트들은 모두 그대로 통과합니다.

GYB의 좋은 예제는 Codable.swift.gyb에서 확인할 수 있습니다. 파일의 가장 상위에는 기초가 되는 Codable 타입들이 인스턴스 변수로 할당되어 있습니다.

%{
codable_types = ['Bool', 'String', 'Double', 'Float',
                 'Int', 'Int8', 'Int16', 'Int32', 'Int64',
                 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64']
}%

나중에 SingleValueEncodingContainer를 구현할 때 이 타입들은 반복(iterate)되어 프로토콜이 요구로 하는 메소드 정의를 생성합니다.

% for type in codable_types:
  mutating func encode(_ value: ${type}) throws
% end

GYB 템플릿 결과를 보면 다음과 같습니다.

mutating func encode(_ value: Bool) throws
mutating func encode(_ value: String) throws
mutating func encode(_ value: Double) throws
mutating func encode(_ value: Float) throws
mutating func encode(_ value: Int) throws
mutating func encode(_ value: Int8) throws
mutating func encode(_ value: Int16) throws
mutating func encode(_ value: Int32) throws
mutating func encode(_ value: Int64) throws
mutating func encode(_ value: UInt) throws
mutating func encode(_ value: UInt8) throws
mutating func encode(_ value: UInt16) throws
mutating func encode(_ value: UInt32) throws
mutating func encode(_ value: UInt64) throws

이 패턴은 encode(_:forKey:), decode(_:forKey:), 그리고 decodeIfPresent(_:forKey:) 와 같이 비슷하게 생긴 파일을 만들 때 유용합니다. 종합하면 GYB는 상용구 코드의 양을 수 천 LOC만큼 줄입니다.

$ wc -l Codable.swift.gyb
2183 Codable.swift.gyb
$ wc -l Codable.swift
5790 Codable.swift

중요함 : GYB 템플릿이 유효하다고 유효한 Swift 코드를 생성하는 것은 아닙니다. 결과로 나온 파일에서 컴파일 오류가 나면 어떤 것이 근본적인 원인인지 모르는 경우도 있습니다.

Xcode에서 GYB 사용하기

GYB는 Xcode 툴체인에 기본으로 들어있진 않아서 xcrun에서는 찾을 수 없습니다. 대신에 소스 코드를 다운로드 받을 수 있고 chmod 커맨드를 사용해서 gyb를 실행 가능하게 만들 수 있습니다. (macOS에 기본으로 설치된 Python은 gyb를 실행할 수 있는 버전입니다.)

$ wget https://github.com/apple/swift/raw/master/utils/gyb
$ wget https://github.com/apple/swift/raw/master/utils/gyb.py
$ chmod +x gyb

이 파일들을 Xcode 프로젝트에서 접근할 수 있는 위치로 이동시키되, 소스 파일과는 따로 보관하세요. 예를 들어 Vendor 디렉토리가 여러분의 프로젝트의 루트라고 해봅시다.

Xcode에서 네비게이터에 있는 파란색 프로젝트 파일을 클릭하고, 프로젝트의 활성 타겟을 선택한 다음에 “Build Phase” 패널로 넘어갑니다. 제일 위의 + 모양을 누르고 “Add New Run Script Phase”를 선택한 다음, 다음 소스를 빈칸에 채워넣으세요.

find . -name '*.gyb' |                                               \
    while read file; do                                              \
        ./path/to/gyb --line-directive '' -o "${file%.gyb}" "$file"; \
    done

꼭 Compile Sources 전에 GYB 빌드 페이즈가 들어가도록 해주세요.

이제 .swift.gyb 파일 확장자를 가진 파일과 함께 빌드를 돌리면 그것을 GYB가 .swift 파일로 변환해서 프로젝트의 나머지 코드들과 같이 빌드되게 만들어 줄 것입니다.

GYB를 사용해야하는 시기

어떤 도구든 그렇듯이 사용해야하는 시기를 아는 것은 사용하는 방법을 아는 것 만큼이나 중요합니다. 여기 여러분의 도구 상자를 열어서 GYB를 사용해야 할 시기에 대한 예제를 준비했습니다.

공식같은 코드를 생성할 때

혹시 어떤 아이템들이나 시퀀스를 위해서 비슷한 코드를 복사 붙여넣기 하고 게시나요? for-in 반복문을 변수 치환으로 사용하는 것도 해결책이 될 수 있습니다.

위의 Codable 예제에서 볼 수 있듯이 GYB 템플릿 파일의 제일 위에 콜렉션을 선언한 다음 그 콜렉션을 통해 타입, 속성 또는 메소드 정의를 반복할 수 있습니다.

%{ abilities = ['strength', 'dexterity', 'constitution',
                'intelligence', 'wisdom', 'charisma']
}
class Character {
  var name: String

% for ability in abilities:
  var ${type}: Int
% end
}

반복을 하면 할수록 안좋은 코드라는 사실을 기억하세요. 그리고 여러분의 업무를 해결해줄 더 나은 방법이 존재한다는 것을 알아주세요. 프로토콜 익스텐션과 제네릭처럼 언어에 내장된 기능은 수 많은 코드 복사를 없애줄 수 있습니다. 그러니 무차별적으로 GYB를 사용하지 말고 사용하는 방법을 숙지해주세요.

데이터에서 파생된 코드 생성할 때

혹시 데이터 소스 기반의 코드를 작성중이신가요? 통합 GYB를 사용해보세요!

GYB 파일은 json, xml 그리고 csv같은 Python 패키지를 임포트할 수 있으니 앞으로 어떤 파일을 만나더라도 파싱할 수 있을 것입니다.

%{ import csv }
% with open('path/to/file.csv') as file:
    % for row in csv.DictReader(file):

실제 예제는 ISO 4217에서 정의된 통화에 대한 각각 Swift enumeration을 생성하는 Currencies.swift.gyb를 확인해보세요.

GYB 파일에서 HTTP 요청이나 데이터베이스 쿼리를 작성하는 대신에 파일에 데이터를 다운로드해서 컴파일을 빠르고 결정론적이게 유지하세요.

코드 생성은 코드를 동기화 상태로 유지하기 쉽게 만들어줍니다. 간편하게 데이터 파일을 업데이트하고 GYB를 다시 실행하세요.


최근 Swift는 4.0의 EncodableDecodable, 4.1의 EquatableHashable, 4.2의 CaseIterable을 통해 상용구를 많이 없앴습니다. 저희는 이러한 추세가 향후 언어 업데이트에 반영되기를 바랍니다.

그 동안은 GYB가 코드 생성에 가장 유용한 도구가 될 것입니다.

비슷한 용도로 커뮤니티에서 많은 인기를 얻고 있는 것이 Sourcery이고, Python이 아닌 Stencil을 통해 Swift의 템플릿을 작성합니다.

“같은 내용을 반복하지 마세요! (DRY)”는 프로그래밍의 덕목이지만, 때로는 물건을 작동시키기 위해 몇 번 정도 말 할 필요도 있습니다. 그럴 때 GYB와 같은 도구를 쓰면 정말 최고일 것입니다!

다음 글

Radar에 신고해”라는 말을 듣고 그게 어떤 의미인지 궁금했던 적이 있다면 이 주의 게시글에서 해결해드리겠습니다.