Swift. API Design Guidelines

API Design Guidelines 번역

Posted by MinJun on Friday, March 23, 2018 Tags: Swift   26 minute read

godrm님의 번역본을 그대로 가져왔습니다. 자료의 원 주소는

여기 입니다.

1. 스타일/문법 리뷰

1-1 스위프트 API 디자인 가이드라인

https://swift.org/documentation/api-design-guidelines/

  • 사용할 때 기준으로 명확하게 작성하는 게 가장 중요한 지향점이다. 메소드나 프로퍼티 같은 개발 요소는 한 번만 선언하고 반복적으로 사용한다. API를 만들 때는 사용하기 명확하고 편하게 만들어야 한다. 설계를 검증할 때 선언 부분을 읽는 것만으로는 부족하다. 그 대신 사용하는 상황에서 맥락에 맞고 명확한 지 늘 고려해야 한다.

  • 명확한 표현이 압축한 간결성보다 더 중요하다. 스위프트 코드는 압축해서 간결하게 작성할 수 있지만, 단지 글자수를 줄여서 가장 짧은 코드를 만드는 게 목표는 아니다. 스위프트 코드의 간결성은 자연스럽게 반복적으로 재사용하는 코드(boilerplate)를 줄이는 기능과 강한 타입 시스템의 부수효과로 드러날 뿐이다.

  • 모든 선언 부분에 주석을 적극적으로 작성하라. 문서를 작성하면서 얻는 인사이트가 자신의 설계에 깊은 영향을 줄 수 있으니 미루지 말자.

  • Xcode 자동완성에서 볼수 있도록 마크 다운을 적극 활용하라.
  • 선언한 요소에 대해 설명하는 요약으로 시작하라. API 자체는 선언 부분과 요약만으로도 완벽하게 이해할 수 있다.

    /// Returns a "view" of `self` containing the same elements in
    /// reverse order.
    func reversed() -> ReverseCollection
    
  • 필요하다면 하나 이상의 문단이나 문장 요소를 추가한다. 완벽한 문장을 작성하고, 빈 줄로 문단을 나눠라.
    /// Writes the textual representation of each    ← 요약
    /// element of `items` to the standard output.
    ///                                              ← 빈 줄
    /// The textual representation for each item `x` ← 부연 설명
    /// is generated by the expression `String(x)`.
    ///
    /// - Parameter separator: text to be printed    ⎫
    ///   between items.                             ⎟
    /// - Parameter terminator: text to be printed   ⎬ 매개변수 영역
    ///   at the end.                                ⎟
    ///                                              ⎭
    /// - Note: To print without a trailing          ⎫
    ///   newline, pass `terminator: ""`             ⎟
    ///                                              ⎬ 심볼 명령
    /// - SeeAlso: `CustomDebugStringConvertible`,   ⎟
    ///   `CustomStringConvertible`, `debugPrint`.   ⎭
    public func print(
      _ items: Any..., separator: String = " ", terminator: String = "\n")
    

이름짓기(Naming)

  • 이름으로 명확한 사용법을 제시하라.

    그 이름을 사용하는 부분의 코드를 읽는 사람에게 혼란을 줄 수 있는 단어는 피하라.

좋음

```swift
extension List {
	public mutating func remove(at position: Index) -> Element
}
employees.remove(at: x)
```

​ 메소드 시그니처에서 at을 생략한다면 해당 메소드가 x과 같은 요소를 제거하는 건지, x위치에 있는 요소를 찾아서 제거한다는 건지 헷갈릴 수 있다.

나쁨

employees.remove(x) // 명확하지 않음: x를 제거하는건가?

  • 쓸모없는 단어를 제거하라. 이름의 모든 단어는 사용자 관점에서 주요한 정보를 제공해야만 한다.

    나쁨

    public mutating func removeElement(_ member: Element) -> Element?
    
    allViews.removeElement(cancelButton)
    

    ​ 더 많은 단어를 사용하면 의도가 명확해지거 헷갈리지 않을 수 있지만 코드를 읽는 사람에게 중복된 정보를 제공하는 경우는 제거해야 한다. 위의 코드에서 Element 는 호출하는 지점에서는 의미가 없으니 다음과 같은 코드가 더 좋다.

    좋음

    public mutating func remove(_ member: Element) -> Element?
    	
    allViews.remove(cancelButton) // 보다 명확함
    
  • 변수, 매개변수, 연관 타입은 선언한 타입이나 제약사항 보다는 역할에 맞는 이름을 갖도록 한다.

    나쁨

    var string = "Hello"
    protocol ViewController {
      associatedtype ViewType : View
    }
    class ProductionLine {
      func restock(from widgetFactory: WidgetFactory)
    }
    

    ​ 이처럼 타입 이름을 반복해서 사용하는 것도 표현성이나 명료성을 해치는 요소다. 대신 역할을 표현하는 이름이 더 좋다.

    좋음

    var greeting = "Hello"
    protocol ViewController {
      associatedtype ContentView : View
    }
    class ProductionLine {
      func restock(from supplier: WidgetFactory)
    }
    

    ​ 연관 타입도 제네릭 타입 이름 뒤에 Type을 붙이는 것을 피하라

    protocol Sequence {
      associatedtype IteratorType : Iterator
    }
    
  • 매개변수 역할을 명확하게 넣어서 부족한 타입 정보를 보완하라. 매개변수 타입이 NSObject, Any, AnyObject 이거나 IntString 같은 기본 타입이면, 사용하는 지점에서 맥락상 타입 정보가 불명확할 수 있다.

    나쁨

    func add(_ observer: NSObject, for keyPath: String)
    grid.add(self, for: graphics) // 불분명함
    

    ​ 명확성을 갖도록 부족한 타입 정보마다 역할을 설명하는 명사를 붙여준다.

    좋음

    func addObserver(_ observer: NSObject, forKeyPath path: String)
    grid.addObserver(self, forKeyPath: graphics) // 명확함
    

말하는 것처럼 술술 써지도록 작성하라.

  • 메소드나 함수를 사용할 때 영어 문장을 작성하는 것처럼 느끼도록 제공하라.

    좋음

    x.insert(y, at: z)          x, insert y at z
    x.subViews(havingColor: y)  x's subviews having color y
    x.capitalizingNouns()       x, capitalizing nouns
    

    나쁨

    x.insert(y, position: z)
    x.subViews(color: y)
    x.nounCapitalize()  	
    

    ​ 만약 첫 번째나 두 번째 인자값 다음에 전달하는 값이 중요하지 않은 경우는 예외적으로 생략해도 된다.

    AudioUnit.instantiate(
    with: description, 
    options: [.inProcess], completionHandler: stopProgressBar)
    
  • 팩토리 메소드 이름은 “make”로 시작하라. 예시) x.makeIterator()

  • 초기화(생성) 메소드나 팩토리 메소드에서 첫 번째 인자에 추가적인 설명을 포함하지 않도록 한다. 예시) x.makeWidget(cogCount: 47)

    좋음

    let foreground = Color(red: 32, green: 64, blue: 128)
    let newPart = factory.makeWidget(gears: 42, spindles: 14)
    let ref = Link(target: destination)
    

    나쁨

    let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128)
    let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)
    let ref = Link(to: destination)
    
  • 함수나 메소드 이름은 부작용(side-effects) 여부에 따라 다르게 정한다

    • 부작용이 없는 경우는 명사형으로 작성한다. 예시) x.distance(to: y), i.successor().

    • 부작용이 있는 경우에는 명령형으로 동사로 작성한다. 예시) print(x), x.sort(), x.append(y).

    • 가변/불변 메소드 이름을 함께 고려하라.
    • 동사로 표현하면 자연스럽게 “ed”나 “ing”를 붙여서 불변 메소드 이름을 만들 수 있다.

    가변(Mutating) x.sort(), x.append(y)

    불변(Nonmutating) z = x.sorted(), z = x.appending(y)

    • (“ed”를 붙여서) 동사 과거형으로 불변성을 작성하기 적합한 경우

      /// Reverses `self` in-place.
      mutating func reverse()
      /// Returns a reversed copy of `self`.
      func reversed() -> Self
      ...
      x.reverse()
      let y = x.reversed()
      
      • 목적어가 있어서 문법적으로 “ed”를 붙이기 어렵고, “ing”가 적합한 경우
      /// Strips all the newlines from `self`
      mutating func stripNewlines()
      /// Returns a copy of `self` with all the newlines stripped.
      func strippingNewlines() -> String
      ...
      s.stripNewlines()
      let oneLine = t.strippingNewlines()	
      
    • 동작을 명사로 표현하기 적합한 경우에는 가변 메소드 이름에 “form-“을 머릿말로 붙인다.

      불변 x = y.union(z), j = c.successor(i)

    가변 y.formUnion(z), c.formSuccessor(&i)

  • 불변으로 사용할 때 부울린 메소드나 프로퍼티를 사용할 때는 리턴값을 받아서 단언 구문(Assertion)처럼 읽도록 한다. 예시) x.isEmpty, line1.intersects(line2).

  • 어떤 것을 표현하는 프로토콜은 명사처럼 읽도록 명시한다. 예시) Collection

  • 기능이나 가능성을 표현하는 프로토콜은 -able, -ible, -ing 등을 붙여서 표현한다. 예시) Equatable, ProgressReporting.

  • 그 외에 상수, 변수, 속성, 타입들은 명사로 읽도록 명시한다.

용어를 잘 표현하라 (Use Terminology Well)

  • 애매한 용어를 피하라. ‘피부(skin)’를 ‘표피(epidermis)’라고 하지말고, 더 쉽게 의미를 전달할 수 있는 표현이 있으면 그걸 선택하라. 전문 용어(Term of Art)는 필수적인 소통 도구지만, 사용하지 않을 경우 놓칠 수 있는 중요한 의미를 꼭 표현해야 하는 경우만 사용하세요.

  • 전문 용어를 사용한다면 기존 의미에 맞춰 사용하라. 일반적인 용어가 애매하거나 불명확한 것을 정확하게 표현하기 위해서만 기술적인 용어를 사용하는 것이 좋다.
    • 전문가를 놀라게 하지마라: 기존에 친숙하게 사용하던 용어에 전혀 새로운 의미를 부여한다면 선배들이 놀라거나 화를 낼지도 모른다.
    • 초보자를 헷갈리게 하지마라: 웹에서 용어를 찾아 공부하는 사람들에게도 용어의 전형적인 의미가 중요하다.
  • 축약어를 피하라. 전문 용어면서 표준 형태가 아닌 약자는 잘못 풀어쓰거나 오해를 할 수 있다.

  • 선례를 받아드려라. 기존 문화에 맞춰진 표현은 왕초보를 위해서 줄이지마라.
    • 연속된 데이터 구조 이름은 단순히 초보자가 이해하기에 List가 더 쉬울더라도 Array를 사용하는 게 좋다. 배열(Array)은 프로그래밍을 공부하는 모든 사람들이 공부하는 일반적인 용어라서, 검색하거나 질문을 할 때도 더 적합한 이름이라고 할 수 있다.

    • 수학처럼 전문 영역에 대한 용어도 verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x)보다 sin(x)처럼 폭넓게 사용하는 기존 선계를 지키세요. 비록 sine이 완전한 단어 표현이지만, 개발자나 수학자들은 sin(x)가 더 친숙하기 때문이다.

규칙(Conventions)

일반 규칙

  • O(1) 복잡도가 아닌 연산 프로퍼티(Computed Property)는 설명을 만들어라. 대부분 사람들은 저장 프로퍼티에 익숙해서 프로퍼티 값에 접근하는 것은 추가적인 비용이 발생한다고 생각하지 않기 때문이다.

  • 소속이 없는 자유로운 함수를 만들기 보다는 메소드나 속성을 만들어라. 다음의 특별한 경우에만 함수를 고려하라.
    1. 명확하게 self가 없을 때 : min(x,y,z)
    2. 제약없는 제네릭 함수인 경우 : print(x)
    3. 함수 표현이 특정한 도메인 표기법을 준수하는 경우 : sin(x)
  • 대소문자 표기법을 따른다. 타입이나 프로토콜 이름은 UpperCamelCase 형태로 쓰고 그 외에는 모두 lowerCamelCase로 표기한다.
    • 미국식 영어 표현처럼 모두 대문자로 쓰는 두문자(축약어)는 일관되게 대문자나 소문자로 사용하라.

      var utf8Bytes: [UTF8.CodeUnit]
      var isRepresentableAsASCII = true
      var userSMTPServer: SecureSMTPServer		  
      
    • 다른 축약어는 일상적인 영어 단어로 표기하라.

      var radarDetector: RadarScanner
      var enjoysScubaDiving = true
      
  • 메소드가 같은 의미를 갖고 있거나 특정한 도메인 영역에서만 동작을 하는 경우에는 기본 이름을 공유할 수 있다.

    • 다음 메소드들은 본질적으로 동일한 동작을 한다.

      extension Shape {
        /// Returns `true` iff `other` is within the area of `self`.
        func contains(_ other: Point) -> Bool { ... }
      
        /// Returns `true` iff `other` is entirely within the area of `self`.
        func contains(_ other: Shape) -> Bool { ... }
      
        /// Returns `true` iff `other` is within the area of `self`.
        func contains(_ other: LineSegment) -> Bool { ... }
      }
      
    • 위의 기하학 타입과 콜랙션은 다른 영역이라서, 프로그램 내에서 메소드 이름이 같아도 된다.

      extension Collection where Element : Equatable {
        /// Returns `true` iff `self` contains an element equal to
        /// `sought`.
        func contains(_ sought: Element) -> Bool { ... }
      }		
      
    • 다음과 같은 index메소드는 서로 다른 의미를 갖고 있기 때문에 다른 이름을 붙이라.

    나쁨

    extension Database {
    	/// Rebuilds the database's search index
    	func index() { ... }
    
    	/// Returns the `n`th row in the given table.
    	func index(_ n: Int, inTable: TableID) -> TableRow { ... }
    }
    
    • “리턴 타입만 오버로딩”하는 방식은 타입 추론에 혼란을 주기 때문에 피하라.

    나쁨

    extension Box {
       /// Returns the `Int` stored in `self`, if any, and
       /// `nil` otherwise.
       func value() -> Int? { ... }
    
      /// Returns the `String` stored in `self`, if any, and
      /// `nil` otherwise.
      func value() -> String? { ... }
    }
    

매개 변수

func move(from start: Point, to end: Point)

  • 문서화 할 수 있는 매개 변수 이름을 선택하라. 함수나 메소드를 사용할 때 매개변수가 감춰져있더라도 여전히 중요한 역할을 한다.
    • 다음과 같은 이름은 문서화해도 읽기 편하고, 문서로 만들어도 자연스럽다.
      /// Return an `Array` containing the elements of `self`
      /// that satisfy `predicate`.
      func filter(_ predicate: (Element) -> Bool) -> [Generator.Element]
    	
      /// Replace the given `subRange` of elements with `newElements`.
      mutating func replaceRange(_ subRange: Range, with newElements: [E])
    
    • 반면에 다음과 같은 표현은 문법도 안맞고, 불분명하다.

      나쁨

      /// Return an `Array` containing the elements of `self`
      /// that satisfy `includedInResult`.
      func filter(_ includedInResult: (Element) -> Bool) -> [Generator.Element]
    
      /// Replace the range of elements indicated by `r` with
      /// the contents of `with`.
      mutating func replaceRange(_ r: Range, with: [E])
    
  • 매개변수 기본값을 지정해서 편리함을 더하라. 매개 변수에 흔히 넘기는 값 자체를 기본값으로 활용하라.
    • 불필요한 정보를 숨겨서 가독성을 높여준다.

      나쁨

      let order = lastName.compare(
      	royalFamilyName, options: [], range: nil, locale: nil)
      

      간단하게 표현할 수 있습니다.

      let order = lastName.compare(royalFamilyName)
      
      • 기본 인자값을 사용하면 여러 메소드 패밀리를 표현하는 것보다 API를 이해하는데 더 낮은 비용이 들기 때문에 도움이 된다.

      좋음

      extension String {
       /// ...description...
       public func compare(
          _ other: String, options: CompareOptions = [],
          range: Range? = nil, locale: Locale? = nil
       ) -> Ordering
      }
      

      나쁨

      extension String {
       /// ...description 1...
       public func compare(_ other: String) -> Ordering
       /// ...description 2...
       public func compare(_ other: String, options: CompareOptions) -> Ordering
       /// ...description 3...
       public func compare(
          _ other: String, options: CompareOptions, range: Range) -> Ordering
       /// ...description 4...
       public func compare(
          _ other: String, options: StringCompareOptions,
          range: Range, locale: Locale) -> Ordering
      }
      

      이런 경우 메소드 패밀리의 모든 메소드를 각각 문서화하고 따로 이해해야 하는 부담이 생긴다. foo(bar: nil)foo()는 동일한 표현이 아니라서 작은 차이를 문서로 확인해야 하는 불편함을 초래한다. 기본 인자값이 있는 하나의 메소드로 만드는게 더 좋은 개발 경험을 제공한다.

  • 기본 인자값은 매개 변수 목록에서 마지막 위치부터 넣어라. 기본 인자값이 없는 매개변수는 메소드에서 더 중요한 역할을 하고, 메소드를 호출하는 곳에서 안정적으로 초기화하는 형태로 사용할 수 있다.

인자 레이블

func move(from start: Point, to end: Point) x.move(from: x, to: y)

  • 레이블이 인자값을 구분하는데 도움이 안되면 모든 레이블을 생략하라. 예시) min(number1, number2), zip(sequence1, sequence2)
  • 초기화 생성함수에서 다른 타입 값을 받아서 변환하고 보관하는 (값보존 변환) 경우에는 첫 번째 인자 레이블을 생략하라. 예시) Int64(someUInt32)

    첫 번째 인자값은 항상 변환할 값을 전달하라.

      extension String {
        // Convert `x` into its textual representation in the given radix
        init(_ x: BigInt, radix: Int = 10)    Note the initial underscore
      }
      	
      text = "The value is: "
      text += String(veryLargeNumber)
      text += " and in hexadecimal, it's"
      text += String(veryLargeNumber, radix: 16)
    

    정밀한(narrowing) 타입 변환은 레이블에 정밀도를 표현하라. (아래 코드에서 64비트에서 32비트로 그냥 줄어들 때는 truncating를, 64비트에서 32비트 근사값을 처리할 때는 saturating로 표기하고 있다.)

      extension UInt32 {
        /// Creates an instance having the specified `value`.
        init(_ value: Int16)             Widening, so no label
        /// Creates an instance having the lowest 32 bits of `source`.
        init(truncating source: UInt64)
        /// Creates an instance having the nearest representable
        /// approximation of `valueToApproximate`.
        init(saturating valueToApproximate: UInt64)
      }
    

    값보존 변환은 (입력값에 따라 1:1로 고유한 결과값에 매칭되는) 모노모피즘(monomorphism) 방식이다. Int8 값을 Int64로 변환할 때는 모든 Int8 값이 그대로 Int64 값으로 보존된다. 반대 경우에는 Int8 보다 Int64가 더 표현할 수 있는 값이 많기 때문에 Int64 값이 보존되지 않는다. 노트 : 원래 값을 가져올 수 있느냐는 변환할 때 값을 보존하는 지와 관련이 없다. 값을 보존하지 않더라도 원래 값을 그대로 가져올 수 있는 경우도 있고 값에 따라 다를 수 있다.

  • 첫 번째 인자값이 (영어에서) 전치사 형태라면 인자값 레이블을 표기하라. x.removeBoxes(havingLength: 12)처럼 인자값 레이블은 보통 전치사보다 앞에 표시한다.

    처음 두 인자값이 동일한 추상화 수준인 경우에 전치사를 붙이면 예외(exception) 처리가 된다.

    나쁨

    a.move(toX: b, y: c)
    a.fade(fromRed: b, green: c, blue: d)
    

    이런 경우는 인자값 레이블을 전치사 다음에 표기해서, 추상화 수준을 유지한다.

    좋음

    a.moveTo(x: b, y: c)
    a.fadeFrom(red: b, green: c, blue: d)
    
  • 첫 번째 인자값이 (전치사가 아니라) 다른 문법상의 구문이라면 레이블은 생략하고, x.addSubview(y)처럼 기본 이름에 의미있는 단어를 뒤에 붙여도 된다.

    첫 번째 인자값이 문법상의 구문이 아니라면, 레이블은 표기한다.

      view.dismiss(animated: false)
      let text = words.split(maxSplits: 12)
      let studentsByName = students.sorted(isOrderedBefore: Student.namePrecedes)
    

    문장이 올바른 의미를 전달하고 있는지가 중요하다. 다음 코드는 문법상으로 정확하지만 의미상으로 잘못된 내용을 표현한다.

    나쁨

    view.dismiss(false)   사라지는게 실패한건가? Bool 값이 사라져야 하는걸까?
    words.split(12)       숫자 12 나눠야 하는건가?
    

    기본값이 있는 인자값은 생략가능해서 문법상 의미있는 역할을 하지못하기 때문에 반드시 레이블이 있어야 한다.

  • 그 외에 모든 경우는 레이블을 표기하라.

특별 지침

  • 튜플 멤버에 레이블을, 클로저 매개변수에 이름을 표기하라.

    • 이렇게 이름을 붙이면 문서화를 위한 주석에 활용할 수 있고 튜플 멤버에 접근할 때도 편리하다.
    /// Ensure that we hold uniquely-referenced storage for at least
    /// `requestedCapacity` elements.
    ///
    /// If more storage is needed, `allocate` is called with
    /// `byteCount` equal to the number of maximally-aligned
    /// bytes to allocate.
    ///
    /// - Returns:
    ///   - reallocated: `true` iff a new block of memory
    ///     was allocated.
    ///   - capacityChanged: `true` iff `capacity` was updated.
    mutating func ensureUniqueStorage(
      minimumCapacity requestedCapacity: Int, 
      allocate: (_ byteCount: Int) -> UnsafePointer<Void>
    ) -> (reallocated: Bool, capacityChanged: Bool)
    

    클로저 매개변수에서 사용하는 이름은 최고-수준 함수의 매개변수 이름 수준과 맞춰서 선택한다. 호출 지점에서 표시하는 클로저 인자값 레이블은 지원하지 않는다.

  • AnyAnyObject, 제약없는 제네릭 매개변수처럼 제약없는 다형성(polymorphism)은 오버로드할 때 혼동하지 않도록 주의하라.

    다음과 같은 오버로드 형태는 좋지 않다.

    struct Array {
      /// Inserts `newElement` at `self.endIndex`.
      public mutating func append(_ newElement: Element)
    
      /// Inserts the contents of `newElements`, in order, at
      /// `self.endIndex`.
      public mutating func append(_ newElements: S)
        where S.Generator.Element == Element
    }
    

    이런 메소드들은 의미상 동일한 형태를 갖고 인자값에 대한 타입이 명확하게 구분되는 것처럼 보인다. 하지만 ElementAny인 경우에 하나의 요소 타입과 시퀀스 타입이 같은 타입일 수 있다.

    var values: [Any] = [1, "a"]
    values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]?
    

    이런 모호한 경우를 피해가려면 두 번째 오버로드에 명시적으로 이름을 붙여주자.

    struct Array {
      /// Inserts `newElement` at `self.endIndex`.
      public mutating func append(_ newElement: Element)
    
      /// Inserts the contents of `newElements`, in order, at
      /// `self.endIndex`.
      public mutating func append(contentsOf newElements: S)
        where S.Generator.Element == Element
    }
    

    새로운 이름이 주석에 문서 내용과 얼마나 더 잘 일치하는지 살펴보라. 이처럼 주석에 설명을 작성하는 것도 API 작성자가 집중해야 하는 일이다.