본문 바로가기
개린이 이야기

Swift의 API Design Guidelines

by iOS 개린이 2023. 6. 12.

소개

Swift API 디자인 가이드라인은 Swift를 이용하여 개발할 때, 어떻게 명료하고, 사용하기 쉬운 API를 작성할 수 있을지에 대한 지침을 제공한다. 가이드라인의 원칙에 대해서 알아보자!

 

 

기본 원칙

1.  사용지점에서의 명료함

Swift에서 가장 중요한 것은 사용지점에서의 명료함이다. 

메서드나 프로퍼티와 같은 요소들은 한 번만 선언되지만, 여러 번 사용되기 때문이다.

따라서 이들이 명확하고 간결하게 사용될 수 있도록 API를 디자인해야 한다.

디자인을 평가할 때는, 항상 사용 사례를 살펴보고, 문맥 내에서 명확하게 보이는지 확인해야 한다.

 

2. 명료함은 간결함보다 중요

Swift 코드는 간결하면 좋지만, 명료한 코드를 작성하는 것보다 중요하지 않다.

 

3. 모든 선언에 대한 주석 작성

모든 선언에 대해 주석을 작성하자. 주석을 작성함으로써 얻는 통찰은 디자인에 깊은 영향을 미칠 수 있다.

 

Swift의 Markdown 문법 사용

-Swift에서는 주석 작성 시 자체의 Markdown 문법을 사용한다.

-선언된 'Entity' 를 설명하는 요약문으로 시작한다. 대부분의 경우, API는 선언과 요약문만으로 이해가 가능하다.

/// Returns a "view" of `self` containing the same elements in
/// reverse order.
func reversed() -> ReverseCollection

 

여기서 가장 중요한 부분은 요약이다. 가능하면 한 문장으로 끝을 맺는다.

 

함수와 메소드에 대한 설명

함수나 메소드가 무엇을 하는지, 그리고 무엇을 반환하는지를 설명한다. 불필요한 효과나 Void 반환은 생략한다.

/// Inserts `newHead` at the beginning of `self`.
mutating func prepend(_ newHead: Int)

/// Returns a `List` containing `head` followed by the elements
/// of `self`.
func prepending(_ head: Element) -> List

 

'prepend()' 메소드의 주석은 "새로 들어오는 'newHead' 를 'self'의 시작 부분에 삽입한다" 라는 의미이다.

'prepending()' 메소드의 주석은 "'head' 요소를 포함한 self의 List를 반환한다." 라는 의미이다.

 

이렇게 무엇을 받아서 어떤 것을 반환하는지 주석에 명료하고 간결하게 작성한다.

 

/// Removes and returns the first element of `self` if non-empty;
/// returns `nil` otherwise.
mutating func popFirst() -> Element?

 

'popFirst()' 메소드는 컬렉션의 첫 번째 요소를 제거하고, 그 요소를 반환하는 작업을 수행한다.

만약 'self' 가 비어있어 제거할 요소가 없다면 'nil' 을 반환한다.

 

주석을 보면, 요약을 세미콜론으로 분리하여 여러 줄로 나누었는데, 이런 경우는 드물다.

즉, 대부분의 경우 하나의 문장으로 요약하는 것이 일반적이지만, 여러 행동을 설명해야 하는 경우 이렇게 몇 줄에 나눠서 사용할 수도 있다.

 

 

서브스크립트와 이니셜라이저에 대한 설명

1. 서브스크립트에 대한 주석

/// Accesses the `index`th element.
subscript(index: Int) -> Element { get set }

 

이 주석은 서브스크립트가 'index' 번째 요소에 접근한다는 것을 설명하고 있다.

 

 

2. 이니셜라이저에 대한 주석

/// Creates an instance containing `n` repetitions of `x`.
init(count n: Int, repeatedElement x: Element)

 

이 주석은 'x' 를 'n' 번째 반복해서 포함하는 인스턴스를 생성한다는 것을 설명하고 있다.

 

3. 구조체, 열거형 등과 같은 다른 종류의 선언에 대한 주석

/// A collection that supports equally efficient insertion/removal
/// at any position.
struct List {

  /// The element at the beginning of `self`, or `nil` if self is
  /// empty.
  var first: Element?
  ...

 

이 주석은 'List' 라는 구조체와 그 안에 있는 'first' 라는 변수에 대해 설명하고 있다.

이렇게 구조체, 클래스, 열거형 등과 같은 종류의 선언에 대한 주석은 선언된 엔티티가 무엇인지 설명한다.

 

 

글머리 기호 항목 사용

/// 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")

 

함수 또는 메소드에 대한 주석을 더 자세하게 작성하고 싶다면 위 주석처럼 글머리 기호를 사용하여 표현할 수 있다.

빈 줄로 단락을 구분하며 완전한 문장을 사용해도 좋다.

 

'Note', 'SeeAlso' 와 같은 'symbol documentation markup' 요소들은 추가적인 정보나 관련된 참조 정보를 제공한다.

또한 이런 기호들은 Xcode에서 특별하게 처리한다.

Attention Author Authors Bug
Complexity Copyright Date Experiment
Important Invariant Note Parameter
Parameters Postcondition Precondition Remark
Requires Returns SeeAlso Since
Throws ToDo Version Warning

 

 

 

Naming

1. 코드를 읽는 사람에게 모호함을 주지않기 위해 필요한 모든 단어를 포함해라.

예제) 컬렉션에서 주어진 위치에 있는 요소를 제거하는 메소드

extension List {
  public mutating func remove(at position: Index) -> Element
}

employees.remove(at: x)

 

위 예제에서 'at' 이라는 단어를 메소드 시그니처에서 생략한다면, 이 메소드가 'x' 라는 요소를 찾아서 제거하는 것이 아니라 'x' 를 사용하여 제거할 요소의 위치를 나타내는 것처럼 보일 수 있다.

 

따라서 필요한 단어를 추가하여 메소드의 의미를 명확하게 전달할 수 있도록 하자.

 

 

2. 불필요한 단어를 생략하라

이름에 있는 모든 단어는 사용 위치에서 중요한 정보를 전달해야 한다.

 

의도를 명확하게 하거나, 의미를 명확하게 하는 데 더 많은 단어가 필요할 수 있지만, 이미 독자가 가지고 있는 정보와 중복되는 단어는 생략해야 한다. 특히, 타입 정보를 단순히 반복하는 단어를 생략해야 한다.

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

allViews.removeElement(cancelButton)

 

위 에제에서 'Element' 는 호출 위치에서 중요한 정보를 추가하지 않는다.

또한 타입 정보를 단순히 반복하는 단어를 생략하라고 했죠? 이 API는 다음과 같이 바꾸는 것이 좋다.

 

public mutating func remove(_ member: Element) -> Element?

allViews.remove(cancelButton) // 더 명확함

 

타입 자체가 이미 의미를 가지고 있기 때문에 메소드의 'Element' 라는 단어는 불필요하게 중복되는 단어로 보인다.

따라서 제거하여 메소드가 명확하게 이해될 수 있도록 하자.

 

 

3. 변수, 매개변수, 연관 타입을 타입 제약이 아닌 역할에 따라 명명하라

var string = "Hello"

protocol ViewController {
  associatedtype ViewType : View
}

class ProductionLine {
  func restock(from widgetFactory: WidgetFactory)
}

 

위 예제에서는 'string' 같이 타입 이름을 재사용하여 명확성과 표현력을 최적화하지 못했다.

이 대신 엔티티의 역할을 표현할 수 있는 네이밍을 하자.

 

var greeting = "Hello"

protocol ViewController {
  associatedtype ContentView : View
}

class ProductionLine {
  func restock(from supplier: WidgetFactory)
}

 

'string' 을 'greeting' 으로 변경함으로써 변수의 역할을 잘 반영했고,

매개변수 'widgetFactory' 를 'supplier' 로 변경하여 매개변수의 역할을 더 잘 반영한 네이밍을 볼 수 있다.

 

연관 타입은 프로토콜을 정의할 때 실제 타입이 결정되지 않고, 프로토콜을 준수하는 타입에서 실제 사용할 타입을 지정해준다.

만약 연관 타입이 프로토콜 제약 조건과 굉장히 밀접하게 연결되어 있어, 프로토콜 이름 자체가 연관 타입의 역할을 나타내는 경우에는 프로토콜 이름에 타입 이름인 'Protocol' 을 붙여 이름 충돌을 피해야 한다.

protocol Sequence {
  associatedtype Iterator : IteratorProtocol
}
protocol IteratorProtocol { ... }

 

'Sequence' 는 타입의 이름인 Protocol을 사용하지 않았고, 'Iterator' 는 왜 Protocol을 붙여주었을까요?

위 예제처럼 제약 조건과 연관타입이 밀접하게 연결되어 있는 경우에 타입 이름 'Protocol'을 추가하여 명확하게 표시해주는 것이다. 이렇게함으로써 'Iterator' 는 'IteratorProtocol' 을 준수해야 하는 타입이어야 한다는 의미를 명확하게 제시하면서, 이름 충돌도 피할 수 있게 된다.

 

 

4. 타입 정보가 약한 매개변수의 역할을 명확하게 하라.

NSObject, Any, AnyObject 또는 기본 타입인 Int, String 등의 매개변수는 타입 정보와 사용 지점의 문맥만으로는 전체적인 의도를 완전히 전달하기 어렵다. 이 경우, 함수 선언은 명확해보일 수 있지만, 실제 사용시에는 애매모호해질 수 있다.

func add(_ observer: NSObject, for keyPath: String)

grid.add(self, for: graphics) // vague

 

'add' 함수는 'observer' 라는 매개변수가 어떤 역할을 하는지 명확하지 않다.

따라서, 함수를 호출하는 'grid' 부분에서 무엇을 하는지 명확하게 전달이 안될 수 있다.

 

이럴 때는 의미가 애매한 타입의 매개변수 앞에 그 역할을 설명하는 명사를 붙여서 명확성을 확보할 수 있다.

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

 

정리하자면, 'observer' 는 이 매개변수가 무슨 역할을 하는지 명확하지 않다.

이를 해결하기 위해 함수 이름에 그 매개변수의 역할을 반영하여, 이 함수가 'observer' 를 추가하는 역할을 한다는 것을 바로 알 수 있도록 한다. 

또한, 'forKeyPath'와 같이 매개변수 이름도 그 매개변수의 역할을 더 잘 설명하도록 변경하는 것이 좋다.

따라서 'observer' 가 어떤 'keyPath' 를 위한 것인지 명확해진다.   

 

 

Strive for Fluent Usage

1. 함수와 메소드의 이름을 선택할 때, 해당 함수나 메소드를 사용하는 코드가 영문법에 따른 문장처럼 읽히도록 하라

올바른 사용 예제

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의 z위치에 y를 insert해라.

x의 서브 뷰 중에서 색상이 y인 것

x에서 명사를 대문자로 시작하게 하라

이렇게 함수나 메소드의 이름이 사용되는 위치에서 영어 문장처럼 읽혀야 한다.

 

올바르지 않은 사용 예제

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

 

 

첫 번째 또는 두 번째 인자 이후에는 이 원칙을 지키지 않아도 된다. 왜냐하면 그 인자들은 대부분 함수 호출의 핵심 의미를 전달하는데 중요한 역할을 하지 않는다.

AudioUnit.instantiate(
  with: description,
  options: [.inProcess], completionHandler: stopProgressBar)

 

위 예제의 경우 'with: description' 이후의 인자들은 함수의 핵심 의미를 전달하는데 중요한 역할을 하지 않기 때문에 굳이 원칙을 지키지 않아도 괜찮다.

 

 

2. 팩토리 메소드의 이름은 'make' 로 시작해라

 

팩토리 메소드란 특정 객체나 값을 생성하는 함수나 메소드를 지칭하는 용어이다.

이런 메소드의 이름은 'make' 로 시작하게 하여 해당 메소드가 어떤 것을 만드는 메소드임을 명확하게 표현해야 한다.

 

x.makeIterator()

 

위 메소드의 이름을 보면 'make' 를 사용하여 반복자를 만드는 메소드라는 것을 쉽게 이해할 수 있다.

 

 

3. 초기화 메소드와 팩토리 메소드를 호출할 때, 첫 번째 인자의 이름은 메소드 이름과 연관성을 가지지 않아야 한다.

x.makeWidget(cogCount: 47) //관련 없음

 

위 코드처럼 'makeWidget'과 'cogCount'는 관련이 없어야 한다.

 

첫 번째 인자의 이름은 메소드 이름과 '자연스럽게 이어지는 문장'을 만들려고 해서는 안된다.

올바른 예제

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)

 

위 예제에서는 첫 번째 인자들이 메소드 이름과 자연스러운 문장을 만들려고하기 때문에 권장되지 않는다.

이 규칙은 너무 긴 이름과 비일관성 등으로 가독성이 떨어지기 때문이다.

따라서 첫 번째 인자가 그 자체로 의미를 갖도록 하는 것이 중요하다.

 

 

4. 'Side-Effects'에 따라 함수와 메소드를 명명하라

1) 'Side-Effects' 이 없는 함수나 메소드는 명사로 읽히게 작성한다.

x.distance(to: y)
i.successor()

 

이런 경우, 함수나 메소드가 어떤 값을 반환하며, 그 객체 자체에는 변화가 없음을 나타낸다.

 

2) 'Side-Effects' 이 있는 함수나 메소드는 명령형 동사로 읽히게 작성한다.

print(x)
x.sort()
x.append(y)

 

이런 경우, 함수나 메소드가 객체에 변화를 줄 수 있다는 것을 나타낸다.

 

3) 변경(mutating) / 비변경(non-mutating) 메소드 쌍을 일관되게 명명하라

이 두 메서드는 유사한 의미를 갖도록 작성되어야 한다. 또한 일반적으로 'mutating' 메소드에는 동사의 원형을 사용하고, 'non-mutating' 메소드에는 'ed', 'ing' 접미사를 사용한다.

 

예제 'Array' 의 'sort()'와 'sorted()' 메소드

'sort()' 는 배열 자체를 정렬하는 메소드로, 이 메소드를 호출하면 배열의 순서가 바뀐다. (mutating)

'sorted()' 는 배열 자체는 그대로 두고, 정렬된 새로운 배열을 반환한다. (non-mutating)

 

이러한 변경 / 비변경 메소드 쌍을 일관되게 명명하는 것은 코드를 읽는 사람이 메소드의 동작을 명확히 이해할 수 있도록 돕는다.

 

 

5. 'Boolean' 메소드나 프로퍼티는 객체에 대한 어떤 상태 혹은 조건을 설명하라

x.isEmpty
line1.intersects(line2)

 

'x.isEmpty' 라는 프로퍼티는 "x가 비어있는가?" 라는 질문에 답변을 제공하는 역할을 한다.

'line1.intersects(line2)' 라는 메소드는 "line1이 line2와 교차하는가?" 라는 질문에 답변을 제공한다.

이와 같은 방식으로, 불리언 메소드나 프로퍼티는 해당 객체의 상태나 조건을 나타내는 주장의 역할을 하며,

이를 통해 객체의 특정 조건이나 상태에 대한 정보를 얻을 수 있다.

 

 

6. '무엇인가' 를 설명하는 프로토콜은 명사로 읽히는 이름을 사용해야 한다. 

예를 들어 'Collection' 과 같다.

 

 

7. '능력' 을 설명하는 프로토콜은 'able', 'ible', 'ing' 등의 접미사를 사용해야 한다.

예를 들어 'Equatable', 'ProgressReporting' 과 같다.

 

 

8. 그 외 타입, 프로퍼티, 변수, 그리고 상수의 이름은 명사로 읽히게 만들어야 한다.

 

 

 

 

Use Terminology Well(전문 용어를 잘 사용하라)

Term of Art - 특정 분야나 직업 내에서 정확하고 특별한 의미를 가진 단어나 구문

 

1. 보편적인 단어로도 충분히 의미를 전달할 수 있다면, 낯선용어를 사용하지 마라

전문 용어는 의미를 표현하는데 필수적이지만, 굳이 낯선 용어를 사용해서 이해를 방해할 필요는 없다.

예를 들어, "피부" 라는 의미를 전달하려 할 때, "에피더미스" 라는 전문 용어를 사용할 필요가 없다. 

 

2. 전문 용어를 사용하려면 그 용어의 정해진 의미를 따라라

전문 용어를 사용하는 이유는 그것이 다른 방식으로는 애매하거나 불명확하게 표현될 수 있는 것을 정확하게 전달하기 위함이기 때문에, API는 그 용어를 통상적인 의미에 따라 명확하게 사용해야 한다.

 

전문 용어에 새로운 의미를 부여한 것처럼 보이면 전문가들은 혼란스러워 할 수 있다. 따라서 기존의 의미를 변경하지마라. 

또한 초보자는 그 용어를 배우기 위해 기존의 의미를 찾고 있을텐데, 기존의 의미를 변경하여 학습자를 혼란스럽게 하지마라.

 

3. 약어를 피하라

약식언어가 종종 혼란을 초래할 수 있기 때문에 의미를 명확하게 이해할 수 있는 한에서 사용해야 한다!

 

4. 전통을 존중해라

연속적인 데이터 구조를 'Array' 라고 표현하는 것이 익숙하다. 하지만 처음 배우는 초보자에게는 'List' 라고 하면 의미가 좀 더 쉽게 다가올 것입니다. 하지만 'Array'는 전통적으로 사용해왔던 용어이고, 모든 프로그래머가 알고 있기 때문에 'Array'를 사용하는 것이 좋다.

따라서 전통적인 의미를 가지고 있는 단어를 사용하자!

 

 

Conventions

1. 0(1) 이 아닌 연산 프로퍼티를 문서화하라

여기서 0(1)이란 '시간 복잡도' 를 나타내는 컴퓨터 용어로, 어떤 작업을 수행하는 데 걸리는 시간이 입력 데이터의 크기에 관계없이 일정하는 것을 의미한다.

하지만, 모든 작업이 일정하게 수행되는 것은 아니다. 예를 들어, 배열의 모든 요소를 검사해야 하는 경우, 배열의 크기가 커질수록 더 많은 시간이 걸린다. 이런 경우 작업의 복잡도가 변경될 수 있다.

 

자 연산 프로퍼티는 값을 저장하지 않고, 요청할 때마다 계산하여 값을 제공하는 프로퍼티고, 또한 이 계산에는 시간이 걸릴 수 있다. 

따라서 프로퍼티를 사용하면 복잡한 계산이 없이 즉시 값을 얻을 수 있다고 대부분의 사람들이 생각하기 때문에, 만약 프로퍼티가 복잡한 계산이 필요하다면 그 사실을 사용자에게 알려주어야 한다는 것이다. 이를 통해 사용자는 그에 따른 성능 상의 영향을 미리 알 수 있게 된다!

 

 

2. 함수보다는 메소드와 프로퍼티를 선호하라

이 규칙은 프로그래밍을 할 때, 클래스나 구조체 등에 속해있지 않은 독립적인 함수보다는 메소드와 프로퍼티를 사용하라는 것이다. 왜냐하면 메소드와 프로퍼티가 데이터를 포함하는 객체와 연관되어 있기 때문에 코드가 더 객체지향적이고, 이해하고 유지하기가 더 쉽다.

 

하지만, 특정한 경우에는 함수가 더 적절할 수도 있다. 예를 보자.

min(x, y, z) //'min()' 함수는 특정 객체에 속하지 않는 여러 값을 비교할 수 있기 때문에 좋음
print(x)    //print() 함수는 어떤 타입의 객체도 처리할 수 있기 때문에 좋음.
sin(x)     //'sin()' 함수는 수학적 표현에 널리 사용되므로, 이런 경우 사용하는 것이 좋음.

 

3. 대소문자 규칙을 따라라

타입과 프로토콜의 이름은 UpperCamelCase를 사용해라.(첫 글자가 대문자)

그리고 그 외 모든 것은 lowerCamelCase를 사용해라.

 

 

4. 같은 기본의미를 공유하는 경우 같은 이름을 사용하라

extension Shape {
  /// Returns `true` if `other` is within the area of `self`;
  /// otherwise, `false`.
  func contains(_ other: Point) -> Bool { ... }

  /// Returns `true` if `other` is entirely within the area of `self`;
  /// otherwise, `false`.
  func contains(_ other: Shape) -> Bool { ... }

  /// Returns `true` if `other` is within the area of `self`;
  /// otherwise, `false`.
  func contains(_ other: LineSegment) -> Bool { ... }
}

 

'contains' 라는 동일한 이름을 공유하고 있지만, 각각 다른 타입의 매개변수를 가지고 있다.

이렇게 메소드가 기본적으로 같은 작업을 수행하면 같은 이름을 사용해도 된다.

 

또한 서로 다른 별개의 도메인에서 동작하는 경우 같은 이름을 사용해도 된다.

extension Collection where Element : Equatable {
  /// Returns `true` if `self` contains an element equal to
  /// `sought`; otherwise, `false`.
  func contains(_ sought: Element) -> Bool { ... }
}

 

'Shape' 과 'Collection' 의 'contains()' 메소드는 전혀 다른 도메인에서 동작하기 때문에 같은 이름을 사용해도 된다.

 

반면 다른 의미를 가진 메소드라면 다르게 명명해야 한다.(올바르지 않은 예제)

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 { ... }
}

 

'Database' 클래스의 두 'index()' 메소드는 각각 다른 기능을 수행하기 때문에 메소드의 이름을 변경해주어야 한다.

 

또한 반환 타입에 따라 오버로딩을 하지 마라(올바르지 않은 예제)

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? { ... }
}

타입 추론의 혼란을 피하기 위해, 같은 이름의 메소드가 서로 다른 타입을 반환하는 것에 따라 오버로딩을 하지마라.

 

 

Parameters

1. 매개변수 이름을 선택하는 방법

함수나 메소드의 매개변수 이름은 그 기능을 설명하는데 중요한 역할을 한다. 호출 시점에서는 매개변수 이름이 나타나지 않지만, 코드를 읽는 사람에게 함수나 메소드의 기능을 설명하는데 큰 도움이 된다. 

/// 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])

 

'filter()' 함수에서는 조건을 테스트하는 함수를 매개변수로 받는다. 이 매개변수의 이름을 'predicate' 로 지어서, 함수가 요소 중에 'predicate' 를 만족하는 요소를 필터링하여 반환한다는 것을 명확히 설명해준다.

 

반면 이렇게 작명하면 문서를 어색하게 만든다

/// 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])

 

위 메소드는 문맥을 고려하면 맞지 않는 이름이라고 함.

암튼 매개변수의 이름은 해당 함수가 어떤 작업을 수행하는지 설명하는 데 도움이 되도록 명명해야 한다!

 

 

2. 기본값이 제공된 매개변수 활용

함수나 메소드에서 매개변수의 기본값을 설정하는 것은 코드를 단순화하고 가독성을 높이는 좋은 방법이다.

특히 한 가지 값이 매개변수에 대해 기본값을 설정하면, 코드 작성자나 사용자가 매번 그 값을 명시적으로 제공하지 않아도 되므로 편리하다.

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

 

'options', 'range', 'locale' 은 필요없는데 굳이 사용되면서 가독성을 낮추고 있죠?

 

let order = lastName.compare(royalFamilyName)

 

'compare' 라는 함수에서 매개변수에 대해 기본값을 설정해두면, 사용자가 필요한 경우에만 추가 매개변수를 사용하면 되기 때문에 코드가 간결해지고, 가독성이 향상된다.

 

 

3. 기본값을 가진 매개변수는 파라미터 리스트의 끝으로 위치시키는 것이 좋다

메소드의 매개변수 중 기본값이 제공되는 매개변수는 일반적으로 핵심 의미가 상대적으로 떨어진다.

따라서 매개변수의 끝에 위치시키는 것이 좋다.

 

 

4. 프로덕션에서 실행되는 API에 대해서는 #fileID 를 선호해라

Swift는 소스 코드 위치를 추적할 수 있는 여러가지 키워드를 제공한다.

 

'#file' 은

소스 파일의 전체 경로를 문자열로 반환한다. Swift 5.2 이전 버전과의 소스 호환성을 유지하기 위해 사용된다.

 

'#filePath' 는 

'#file' 과 같지만, Swift 5.3에서 도입된 새로운 키워드이다. 테스트 도우미나 스크립트와 같이 최종 사용자가 실행하지 않는 API에서 사용된다.

 

'#fileID' 는 

소스파일의 이름과 줄 번호를 포함한 문자열을 반환한다. 이는 공간을 절약하고 개발자의 개인정보를 보호하는데 도움이 된다.

 

 

ArgumentLabels

Swift 에서는 함수의 매개변수에 레이블을 추가할 수 있어, 함수를 호출할 때, 매개변수의 목적을 더 명확하게 이해할 수 있도록 도와준다.

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

 

 

1. 하지만 모든 함수에서 ArgumentLabel 이 유용한 것은 아니다.

min(number1, number2)
zip(sequence1, sequence2)

 

위 예제처럼 두 매개변수 사이에 명확한 차이가 없다면 ArgumentLabel을 생략하는 것이 좋다.

이런 경우, 매개변수의 순서가 함수의 동작을 결정하게 된다.

 

2. 타입 변환을 보존하는 값을 수행하는 이니셜라이저에서는 첫 번째 ArgumentLabel을 생략해라.

보존하는 값을 수행하는 타입 변환이라는 것은 원본 값의 훼손없이 결과 값도 동일하게 매핑되어 변환되는 것을 의미한다.

ex) UInt32에서 Int64로 변환은 모든 UInt32 값이 그대로 Int64 값으로 변환되기 때문에 보존하는 값을 수행하는 타입 변환이다.

 

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)

 

초기화 함수는 'BigInt' 타입의 값을 받아 문자열로 변환하는 함수이다. 두 번째 인자 'radix' 의 기본값은 10진수를 의미한다.

따라서 'BigInt' 타입의 값을 받아 10진수의 문자열로 변환하는 함수이다.

여기서 'BigInt' 타입의 값이 고유한 'String' 값으로 변환될 수 있기 때문에 첫 번째 ArgumentLabel 이 생략되어 있는 것을 볼 수 있다.

 

반면에 '좁아지는(narrowing)' 타입변환을 수행하는 이니셜라이저에서는 변환을 설명해주는 ArgumentLabel을 추가해줘야 한다.

'좁아지는' 타입변환이란 원본 타입의 값이 결과 타입의 값으로 변환되면서 변환이 가능한 값의 범위를 줄이는 것을 의미한다.

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)
}

 

init(_ value: Int16)

모든 'Int16' 값은 고유하게 UInt32로 변환될 수 있기 때문에 값을 보존하는 타입 변환이다.

 

init(truncating source: UInt64)

위 함수는 UInt64의 하위 32비트를 가져와 UInt32 값을 만드는 경우이기 때문에 '좁아지는' 타입 변환이다.

따라서 변환을 좁혀주는 동작을 설명하는 'truncating' 레이블이 사용되었다.

 

init(saturating valueToApproximate: UInt64)

위 함수도 동일하게 '좁아지는' 타입 변환이다.

따라서 근사하는 동작을 설명하는 'saturating' 레이블이 사용되었다.

 

 

3. 메소드의 첫 번째 인자가 전치사의 일부를 형성하는 경우 ArgumentLabel 를 설정해라

일반적으로 첫 번째 인자가 전치사의 일부를 형성할 때는 인자에 ArgumentLabel 을 지정해야 한다.

x.removeBoxes(havingLength: 12)

 

'havingLength' 는 첫 번째 인자에 대한 레이블로 전치사의 일부이다.

 

4. 첫 번째 인자가 문법적 구문의 일부를 형성하는 경우 ArgumentLabel을 생략해라

메소드를 전체적으로 보았을 때, 첫 번째 인자가 문법적으로 자연스러운 경우 ArgumentLabel을 생략하고, 메소드의 이름에 추가한다. 

x.addSubview(y)

 

만약 메소드의 이름이 'add' 였고, 'Subview' 가  ArgumentLabel로 들어갔다면?

첫 번째 인자가 문법적으로 자연스러운 경우이기 때문에 ArgumentLabel을 생략하고, 메소드의 이름에 추가한다.

 

 

5. 다른 모든 인자에 레이블을 붙여라

 

 

 

정리

1. 기본원칙 

-명료함을 가장 우선으로 하는 코드를 작성하자!

-모든 선언에 주석을 작성하자!

 

2. Naming

-코드를 이해하는데 필요한 모든 단어를 사용하자, But 중복되거나 불필요한 단어는 생략하라!

-타입이름과 동일한 이름으로 작성하지 말고, 엔티티의 역할을 표현할 수 있는 이름을 작성하라!

-매개변수의 역할을 타입 이름을 통해 이해하기 쉽지 않다면, 역할을 설명하는 부가적인 이름을 추가하자!

 

3. Strive for Fluent Usage

 -함수나 메소드의 이름을 작명할 때, 하나의 문장처럼 자연스럽게 읽히도록 만들자!

-무언가를 만드는 '팩토리 메소드' 의 이름은 'make' 로 시작해라!

-하지만 초기화나 팩토리 메소드를 호출할 때, 첫 번째 인자와 메소드의 이름이 연관성을 가지지 않도록 만들자!

-'Side-effects' 가 있는 함수나 메소드는 명사로 읽히게 작성하자!

 

4. Use Terminology Well

-전문용어는 다른 방식으로는 불명확하게 표현될 수 있는 것을 정확하게 전달하기 위함이다.

따라서 전문용어를 굳이 사용하지 않아도 의미를 잘 전달할 수 있다면 사용하지 말자!

-전문용어의 정해진 의미에 맞춰 사용하자!

 

5. Conventions

-시간이 걸릴 수 있는 동작에는 복잡도에 대한 설명을 해주자!

-함수보다는 메소드와 프로퍼티를 사용하자!(객체지향적)

-같은 의미를 사용하는 함수나 메소드라면, 오버로딩을 하자, 하지만 반환타입에 따라서는 오버로딩 금지!

 

6. Parameters

-매개변수 이름은 해당 함수가 어떤 작업을 수행하는지 설명하는데 도움이 되도록 하자!

-기본값을 제공하는 매개변수를 활용하자!

 

7. ArgumentLabel

-매개변수의 목적을 더 명확하게 이해할 수 있도록 ArgumentLabel을 작성하자!

-하지만 각 상황마다 ArgumentLabel이 필요한 경우가 있고, 생략해야 하는 경우가 있다.

 

 

 

이점

1. 일관성

이 가이드라인을 준수하면, API를 일관성있게 작성할 수 있다. 이로인해 사용자는 특정 동작을 예상할 수 있고, 코드를 이해하는데 필요한 시간을 줄일 수 있다.

 

2. 가독성

가이드라인은 명확한 작성 규칙과 사용을 제시한다. 특히 함수나 메소드를 문장처럼 자연스럽게 읽히도록 권장하므로 코드를 읽는 사람은 더 쉽게 이해할 수 있다. 

 

3. Swift의 이해

Swift 표준 라이브러리는 이 가이드라인을 준수하면서 만들어져있다. 그래서 가이드라인을 보다보면, "이 코드가 이런 이유로 이런 방식으로 작성되었구나" 라는 것을 알 수있다. 따라서 가이드라인을 준수하면서 Swift 코드가 작성되었기 때문에 이해하기 쉽다!

 

 

 

 

 

Reference:

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

'개린이 이야기' 카테고리의 다른 글

Git Flow에 관하여  (0) 2023.06.26
Git에 관하여  (0) 2023.06.25
Protocol과 associatedtype에 관하여  (0) 2023.06.06
Protocol Composition에 관하여  (0) 2023.06.06
Protocol-Extension에 관하여  (0) 2023.06.05