“Swift API Design Guidelines”와 “읽기 좋은 코드 작성하기 - Swift API Design Guidelines” 인프런 강의를 바탕으로 정리를 진행했습니다.
목차 보기
Fundamentals
Clarity at the point of use is your most important goal.
사용하는 쪽에서 명확하다고 느끼게 하는 것이 가장 중요한 목표입니다.
해당 문장은 모든 챕터를 관통하는 중요한 내용입니다. API가 명확하고 간결하여 사용자가 쉽게 사용할 수 있도록 API를 설계해야 합니다. 항상 사용자가 사용하는 쪽에서 해당 API가 잘 작성되었는지 판단해야 합니다.
Clarity is more important than brevity.
명확한 것이 간결한 것보다 중요합니다.
코드를 함축적으로 표현하는 것에 목표를 두지 않고 어떻게 하면 오해없이 해당 API를 이해할 수 있을지 고민해야 합니다.
Write a documentation comment for every declaration.
모든 함수, 메서드 선언에 대해 문서 주석 작성을 해야 합니다.
해당 부분은 논란의 여지가 있습니다. 굳이 모든 함수, 메서드에 문서 주석을 작성할 필요는 없습니다. 하지만, 외부 개발자에게 기능을 제공하는 경우에는 이런 문서화가 필수입니다.
API를 간단히 설명하는게 어렵다면, API를 잘못 설계했을 지도 모릅니다.
함수, 메서드가 너무 많은 기능을 가지고 있거나, 너무 많은 덩어리로 쪼개져 있는 경우같이 API를 잘못 설계하여 API를 간단하게 설명하는게 어려울 수도 있습니다.
Swift 마크 다운 용어를 사용해서 문서화할 수 있습니다.
•
문서화 시에 Summary로 주석을 시작해야 합니다.
/// Returns a "view" of `self` containing the same elements in
/// reverse order.
func reversed() -> ReverseCollection
Swift
복사
•
Summary만으로 충분히 설명이 불가능할 시에 추가적인 설명을 적을 수 있습니다.
/// Writes the textual representation of each ← Summary
/// element of `items` to the standard output.
/// ← Blank line
/// The textual representation for each item `x` ← Additional discussion
/// is generated by the expression `String(x)`.
///
/// - Parameter separator: text to be printed ⎫
/// between items. ⎟
/// - Parameter terminator: text to be printed ⎬ Parameters section
/// at the end. ⎟
/// ⎭
/// - Note: To print without a trailing ⎫
/// newline, pass `terminator: ""` ⎟
/// ⎬ Symbol commands
/// - SeeAlso: `CustomDebugStringConvertible`, ⎟
/// `CustomStringConvertible`, `debugPrint`. ⎭
public func print(
_ items: Any..., separator: String = " ", terminator: String = "\n")
Swift
복사
작성된 마크 다운 형식의 주석은 Quick Help inspector에서 확인 가능합니다.
Naming
Promote Clear Usage
해당 챕터에서는 명확한 사용에 대해서 얘기합니다.
필요한 단어는 모두 포함해주세요.
코드를 읽는 사람이 모호하게 느끼지 않아야 합니다.
예를 들어서, Collection의 특정 위치에 있는 요소를 삭제하려고 합니다.
extension List {
public mutating func remove(at position: Index) -> Element
}
employees.remove(at: x)
Swift
복사
만약, at을 제거하게 된다면 어떤 것을 해당 메서드를 통해 제거하는건지 모호해집니다.
employees.remove(x)
Swift
복사
“x와 동일한 값을 가지는 요소를 삭제”하는 건지, “해당 위치에 있는 요소를 삭제”하는건지 의도를 파악할 수 없습니다. 따라서, at을 포함하여 해당 위치에 있는 요소를 제거한다는 의미를 명확하게 줘야 합니다.
불필요한 단어는 생략하세요.
네이밍에 들어가는 모든 단어는 사용되는 시점에 핵심적인 정보만 전달해야 합니다.
예를 들어서, removeElement라는 메서드가 있습니다. Element라는 단어가 빠져도 명확하게 해당 메서드를 이해할 수 있습니다.
public mutating func removeElement(_ member: Element) -> Element?
allViews.removeElement(cancelButton)
Swift
복사
Element에 삭제될 요소가 오기 때문에 removeElement라고 네이밍을 하지 않아도 괜찮습니다.
public mutating func remove(_ member: Element) -> Element?
allViews.remove(cancelButton) // clearer
Swift
복사
때때로, 애매함을 피하기 위해서 타입 정보를 반복해서 네이밍할 수 있지만, 일반적으로는 파라미터의 역할을 네이밍하는 것이 타입으로 네이밍을 하는 것보다 좋습니다.
역할에 따라 variables, parameters, associated types를 네이밍해야 합니다.
타입보다는 역할에 따라 네이밍해주는 것이 코드를 이해하기 쉽습니다.
String 타입을 가지는 변수를 string으로 네이밍을 하면 변수의 이름만으로 어떤 역할을 하는지 구분할 수 없게 됩니다. 따라서, 해당 변수가 어떤 역할을 가지는지에 맞춰서 네이밍을 해주어야 합니다.
var string = "Hello" ❌
var greeting = "Hello" ✅
Swift
복사
associatedtype를 ViewType으로 네이밍하는 경우, 해당 타입이 어떻게 쓰이는 건지, 어떤 View를 넣어줘야 하는건지 알 수 없게 됩니다. 따라서, 명확하게 ContentView라는 이름으로 네이밍을 해주면 해당 View가 ContentView로 역할을 한다는 걸 알 수 있게 됩니다.
protocol ViewController {
associatedtype ViewType : View ❌
}
protocol ViewController {
associatedtype ContentView : View ✅
}
Swift
복사
예외적인 경우
parameter 타입으로 네이밍을 했을 경우에는 어떤 용도로 파라미터가 사용되는지 모르는 경우가 발생합니다. 해당 경우에는 명확하게 파라미터의 역할로 네이밍을 해주는 것이 좋습니다.
class ProductionLine {
func restock(from widgetFactory: WidgetFactory) ❌
}
class ProductionLine {
func restock(from supplier: WidgetFactory) ✅
}
Swift
복사
파라미터의 역할을 명확히 하기 위해서 불충분한 타입 정보를 보충해야 합니다.
파라미터 타입이 NSObject, Any, AnyObject 또는 기본 타입이라면 사용되는 곳에서 명확한 타입으로 문맥 구분이 어렵습니다. 따라서, 추가적인 설명이 필요합니다.
add메서드의 for부분에는 어떤 값을 넘겨야 하는지 명확하지 않습니다. 기본 타입인 String 타입을 가지기 때문에 어떤 값을 넘겨야 할 지 구분할 수 없습니다.
func add(_ observer: NSObject, for keyPath: String)
grid.add(self, for: graphics) ❌
Swift
복사
어떤 값을 넘겨야 하는지 명시하여 해당 파라미터에 넘겨야 하는 값을 명확하게 합니다.
func addObserver(_ observer: NSObject, forKeyPath path: String)
grid.addObserver(self, forKeyPath: graphics) ✅
Swift
복사
Strive for Fluent Usage
해당 챕터에서는 유창한 사용을 위해서 해야하는 노력에 대해서 얘기합니다.
Method, Function 이름을 영어 문장처럼 읽을 수 있도록 작성해야 합니다.
영어 문장처럼 작성하면 읽기가 편해집니다.
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”
Swift
복사
주요 argument가 아닌 argument에 대해서 반드시 이런 규칙을 지키지 않아도 괜찮습니다.
AudioUnit.instantiate(
with: description,
options: [.inProcess], completionHandler: stopProgressBar)
Swift
복사
factory method는 make로 시작해야 합니다.
해당 규칙은 팀마다 다를 수 있겠지만, Apple에서는 make로 메서드 이름을 시작하는걸 추천합니다.
x.makeIterator()
Swift
복사
init argument와 factory method 호출에는 영문법 구절을 구성하지 마세요.
argument로 불리는 것들의 역할만 작성하고 영문법을 구성하지 않습니다.
let foreground = Color(red: 32, green: 64, blue: 128)
let newPart = factory.makeWidget(gears: 42, spindles: 14)
let ref = Link(target: destination)
Swift
복사
만약, 값이 보존되는 경우에는 굳이 argument label를 작성하지 않아도 됩니다.
예를 들어서, RGBColor에 CMYKColor를 넣어서 동일한 컬러로 보존하는 경우에는 Color 매트릭스만 다르기 때문에 값이 보존된다고 판단할 수 있습니다. 이런 경우에는 argument를 생략할 수 있습니다.
let rgbForeground = RGBColor(cmykForeground)
Swift
복사
Side-effect에 기반해서 func, method를 네이밍하세요.
Side-effect가 있느냐, 없느냐에 따라서 mutating, non-mutating이 갈라지게 됩니다.
•
mutating
해당 키워드가 붙으면 Side-effect가 존재합니다. mutating한 func, method는 동사형으로 네이밍을 합니다.
x.sort()
x.append(y)
Swift
복사
동사형 이름을 가진 sort, append는 x 자체의 값을 바꿉니다.
•
non-mutating
non-mutating하다면 내부 값을 변경하지 않습니다. 새로운 결과값을 return해서 사용하게 됩니다. 이러한 func, method는 ed, ing를 붙여서 사용합니다.
z = x.sorted()
z = x.appending(y)
Swift
복사
ed, ing를 붙인 func, method는 x 자체를 바꾸지 않고 x 값에 기반하여 새로운 값을 반환합니다.
만약, operation이 명사로 설명되는 경우, non-mutating은 명사 그대로 사용하고, mutating은 명사 앞에 form을 접두사로 붙여서 사용합니다.
x = y.union(z) // non-mutating
y.formUnion(z) // mutating
Swift
복사
non-mutating인 Boolean 메서드와 프로퍼티는 호출되는 객체에 대한 주장문처럼 읽혀야 합니다.
해당 메서드와 프로퍼티가 변경을 내포하지 않는다는걸 나타내기 위해 그러한 용어를 배제하고, 선언처럼 읽을 수 있도록 작성해야 합니다.
x.isEmpty
line1.intersects(line2)
Swift
복사
어떤 것이 무엇인지 설명하는 프로토콜은 명사로 작성해야 합니다.
protocol Collection { }
Swift
복사
능력을 설명하는 프로토콜은 able, ible, ing를 사용한 접미사로 네이밍해야 합니다.
protocol Equatable { }
protocol ProgressReporting { }
Swift
복사
Types, Properties, variables, constants의 이름은 명사로 읽혀야 합니다.
let label = UILabel()
var age = 2
Swift
복사
Use Terminology Well
해당 챕터에서는 제대로된 용어 사용에 대해서 얘기합니다.
보편적인 단어가 의미를 잘 전달한다면, 전문 용어를 쓰지말고 잘 알려진 용어를 사용하세요.
“epidermis”보다는 “skin”이 모두에게 의미를 전달하기 좋을 겁니다.
전문 용어를 사용하겠다고 한다면, 해당 필드에서 대다수가 인정하는 확립된 언어를 사용하십시오. 전문가들이 들으면 놀라고, 초급자들이 혼란스러워하는 용어를 사용해선 안됩니다.
약어 사용을 피해야 합니다.
약어를 이해하는 것이 어려울 수 있습니다. 그렇기에, 단어를 풀어서 작성해야 합니다.
약어를 사용해야 한다면, 인터넷 검색으로 쉽게 찾을 수 있어야 합니다.
관례를 따르세요.
기존 문화와 다른 용어를 사용하면서까지 초심자를 배려하지 않아도 됩니다.
Array, sin같은 단어는 대다수 사람들이 친숙하게 생각합니다. 굳이 해당 용어를 더 알기 쉽게 풀어 쓰지 않아도 괜찮습니다.
Conventions
General Convention
computed property의 복잡도가 O(1)이 아니라면 복잡도를 주석으로 남겨야 합니다.
computed property를 사람들이 사용할 때에 복잡도가 O(1)이라고 생각하고 사용하게 됩니다. O(1)이 아닌 경우 경고성 주석을 달아주는 것이 좋습니다.
computed property를 사용했을 때, 우리는 복잡한 로직없이 값이 반환될 것이라고 생각합니다.
var numberOfElement: Int {
return 100
}
Swift
복사
하지만, 복잡한 로직이 내부에 존재한다면 어떻게 해야 할까요?
var numberOfElement: Int {
(0...n).forEach {
(0...n).forEach {
// ...
}
}
}
Swift
복사
이런 경우에는 해당 computed property 위에 O(n^2)의 시간 복잡도가 든다는 주석을 달아주어 알려주는 것이 좋습니다.
/// Time Complexity : O(n^2)
var numberOfElement: Int { ... }
Swift
복사
전역 함수 대신 method, property를 사용하는 걸 선호합니다.
전역 함수는 맥락없이 어떤 곳에서든 사용 가능합니다. 이를 Swift Guideline에서는 추천하지 않습니다.
전역 함수를 사용해도 되는 특수한 경우가 있습니다.
1.
명확한 self가 없는 경우
min(x, y, z)
Swift
복사
2.
function이 제한되지 않은 Generic인 경우
print(x)
Swift
복사
3.
function 구문이 해당 도메인의 표기법인 경우
sin(x)
Swift
복사
Case Convention를 따르세요.
type, protocol은 UpperCamelCase를, 나머지는 lowerCamelCase를 따릅니다.
단어의 첫 글자를 모아서 사용하는 단어는 모든 단어를 Upper로 사용합니다. 하지만, lowerCamelCase로 사용하는 경우에 해당 단어가 첫번째로 시작한다면 소문자로 시작해야 합니다.
var utf8Bytes: [UTF8.CodeUnit]
var isRepresentableAsASCII = true
var userSMTPServer: SecureSMTPServer
Swift
복사
기본 뜻이 같거나 서로 구별되는 도메인에서 작동하는 Method는 base name를 동일하게 사용할 수 있습니다.
예를 들어서, Shape 타입이 있습니다. 해당 타입 내부에는 Point 타입을 포함하는지, 다른 Shape 타입을 포함하는지, LineSegment 타입을 포함하는지 확인하는 contains메서드가 있습니다.
같은 맥락이기 때문에, 모두 contains라는 이름으로 네이밍되었습니다.
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 { ... }
}
Swift
복사
base name이 동일하기 때문에 해당 메서드의 동작을 이해하기 쉽습니다.
만약, Shape과 구별되는 Collection 타입에서 contains를 사용하고 싶다면 같은 의미 안에서 사용할 수 있습니다.
extension Collection where Element : Equatable {
/// Returns `true` if `self` contains an element equal to
/// `sought`; otherwise, `false`.
func contains(_ sought: Element) -> Bool { ... }
}
Swift
복사
하지만, 두 개의 맥락이 완전히 다른 상태에서 같은 이름을 사용한다면 사용자에게 혼란을 줄 수 있습니다.
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 { ... }
}
Swift
복사
또한, 같은 이름의 메서드가 다른 return type를 가진다면 사용하는 쪽에서 타입 추론에 따라서 메서드를 사용하게 됩니다. 이는 어떤 값을 사용할지 매번 지정해줘야 하고 어떤 타입이 return될 지 알 수 없다는 걸 뜻합니다.
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? { ... }
}
Swift
복사
Parameter
func move(from start: Point, to end: Point)
Swift
복사
주석을 읽기 쉽게 만들어주는 파라미터 이름을 선택하세요.
파라미터는 function, method를 설명해주는 역할을 갖습니다. 파라미터를 주석으로 작성했을 때, 문법적으로 읽는데 어려움이 있어선 안됩니다.
하단 function에 있는 파라미터 이름은 주석으로 작성했을 때, 주석을 읽기 쉽게 만들어주고 문법적으로 읽는데 어려움이 없습니다.
/// 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])
Swift
복사
하지만, 하단 function은 주석으로 작성했을 시에 읽기에 어려움이 있습니다. 주석으로 들어가도 쉽게 읽히게끔 파라미터 이름을 작성해야 합니다.
/// 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])
Swift
복사
일반적인 사용을 단순화할 수 있다면, defaulted parameter를 사용하세요.
default parameter를 사용하면 간단하고 편하게 API를 사용할 수 있습니다. API를 사용하는 사람들이 모든 parameter를 기억하지 않아도 되기 때문에 인지의 범위를 줄일 수 있습니다.
extension String {
/// ...description...
public func compare(
_ other: String, options: CompareOptions = [],
range: Range? = nil, locale: Locale? = nil
) -> Ordering
}
Swift
복사
default parameter를 사용하는 것이 간결하고 주석도 한 번에 작성할 수 있습니다.
parameter list는 끝 부분에 두는 것이 좋습니다. 여러 패턴이 발생했을 시에 필수 값을 안쪽으로 두는 것이 function, method가 일관적으로 읽힐 수 있기 때문입니다.
x.compare("A")
x.compare("B", options: [])
x.compare("C", options: [], range: nil)
x.compare("D", options: [], range: nil, locale: nil)
Swift
복사
Argument labels
func move(from start: Point, to end: Point)
Swift
복사
label 써도 유용하게 구분되지 않는다면 모든 label을 생략하세요.
label이 있어도 아무런 의미가 없는 경우에는 label을 생략할 수 있습니다.
min(x, y, z)
zip(sequence1, sequence2)
Swift
복사
값을 유지하면서 타입 변환을 해주는 initializer에서 첫번째 argument label은 생략합니다.
값은 유지되면서 타입만 변환하는 것이기에 첫번째 argument는 항상 source of conversion이어야 합니다.
Int64(someUInt32)
Swift
복사
만약, 값의 범위가 좁혀지는 타입 변환이라면, label 붙여서 설명하는걸 추천합니다. 값 표면에 손해가 있는 경우, 값이 좁아질 수 있다는걸 label로 표현할 수 있습니다.
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)
}
Swift
복사
첫번째 argument가 전치사구의 일부일 때, argument label로 지정합니다.
x.removeBoxes(havingLength: 12)
Swift
복사
만약, function, method에 있는 여러 개의 argument가 동일한 추상화 레벨에 있다면 전치사를 function, method 이름으로 뺍니다.
// ❌
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)
Swift
복사
만약 첫번째 argument가 문법적 구절을 만든다면 label를 제거하고 함수 이름에 base name를 추가합니다.
view.addSubview(contentView)
Swift
복사
문법적 구절을 만들지 않는다면, label를 둬야 합니다.
view.dismiss(animated: false)
let text = words.split(maxSplits: 12)
let studentsByName = students.sorted(isOrderedBefore: Student.namePrecedes)
Swift
복사
중요한건 구절이 정확한 의미를 전달하는지가 중요합니다. 문법적이지만 모호하다면 사용자가 이해하기 힘들어 합니다.
view.dismiss(false) // Don't dismiss? Dismiss a Bool?
words.split(12) // Split the number 12?
Swift
복사
나머지 모든 경우, argument는 label를 붙여야 합니다.
Special Instructions
tuple members와 closure parameter에는 label를 붙여야 합니다.
설명력 있고, 문서화된 주석에서 구체적으로 언급 가능하기 때문에 label를 붙여야 합니다.
/// 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` if a new block of memory
/// was allocated; otherwise, `false`.
/// - capacityChanged: `true` if `capacity` was updated;
/// otherwise, `false`.
mutating func ensureUniqueStorage(
minimumCapacity requestedCapacity: Int,
allocate: (_ byteCount: Int) -> UnsafePointer<Void>
) -> (reallocated: Bool, capacityChanged: Bool)
Swift
복사
사용하는 쪽에서도 label이 있어야 expressive한 접근이 가능해집니다.
let ensureStorage = ensureUniqueStorage()
ensureStorage.reallocated
ensureStorage.capacityChanged
Swift
복사
Overload set에서의 모호함 피하기 위해 제약 없는 다형성에 각별히 주의하세요.
Overload set은 메서드 이름은 동일한데 파라미터 차이로 구별이 되는 메서드들을 의미합니다. 그러한 메서드들은 문제가 발생할 위험이 있습니다.
예를 들어서, Array에 append라는 파라미터 타입이 다른 두 개의 메서드를 만들었습니다.
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
}
Swift
복사
[Any] 타입의 Array에 append를 사용해서 요소를 추가했을 시에 해당 요소가 어떤 식으로 append되는지가 모호해집니다.
var values: [Any] = [1, "a"]
values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]?😢
Swift
복사
따라서, 모호함을 제거하기 위해서 두번째 append메서드에는 label에 이름을 명시적으로 지정해줬습니다.
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
}
Swift
복사