Apple 에서 공개한 The Swift Programming Language (Swift 5.7) 책의 Methods 부분1을 번역하고, 설명이 필요한 부분은 주석을 달아서 정리한 글입니다. 전체 번역은 Swift 5.7: Swift Programming Language (스위프트 프로그래밍 언어) 에서 확인할 수 있습니다.
메소드 (methods) 는 한 특별한 타입과 결합한 함수입니다.2 클래스, 구조체, 및 열거체 모두 인스턴스 메소드 (instance methods) 를 정의하여, 주어진 타입 인스턴스와 작업하기 위한 특정 임무 (tasks) 와 기능을 은닉할 수 있습니다. 클래스, 구조체, 및 열거체는, 타입 그 자체와 결합한, 타입 메소드 (type methods) 도 정의할 수 있습니다. 타입 메소드는 오브젝티브-C 의 클래스 메소드 (class methods) 와 비슷합니다.
스위프트에선 구조체와 열거체가 메소드를 정의할 수 있다는 사실이 C 및 오브젝티브-C 와의 주요한 차이점입니다.3 오브젝티브-C 에선, 클래스가 메소드를 정의할 수 있는 유일한 타입입니다. 스위프트에선, 생성한 타입에 대한 메소드 정의라는 유연함을 여전히 가지면서, 클래스나, 구조체, 열거체 중 어느 걸로 정의할 지를 선택할 수 있습니다.
인스턴스 메소드 (instance methods) 는 한 특별한 클래스나, 구조체, 및 열거체 인스턴스에 속하는 함수입니다. 이는, 인스턴스 속성의 접근 및 수정 방법을 제공하거나, 아니면 인스턴스 목적과 관련된 기능을 제공함으로써, 그 인스턴스의 기능을 지원합니다. Functions (함수) 에서 설명한 것처럼, 인스턴스 메소드 구문은 함수와 정확하게 똑같습니다.
인스턴스 메소드는 자신이 속한 타입의 여는 중괄호와 닫는 중괄호 안에 작성합니다. 인스턴스 메소드는 그 타입의 모든 다른 인스턴스 메소드와 속성에 암시적으로 접근합니다.4 인스턴스 메소드는 자신이 속한 타입으로 정해진 인스턴스에서만 호출할 수 있습니다. 인스턴스 없이 고립된 채 호출할 순 없습니다.
다음 예제는, 행동이 일어난 횟수를 세는데 사용할 수 있는, 단순한 Counter
클래스를 정의합니다:
class Counter {
var count = 0
func increment() {
count += 1
}
func increment(by amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
Counter
클래스는 세 개의 인스턴스 메소드를 정의합니다:
increment()
는 횟수를 1
만큼 증가합니다.increment(by : Int)
는 횟수를 지정한 정수만큼 증가합니다.reset()
은 횟수를 0
으로 재설정합니다.Counter
클래스는, 현재 횟수를 계속 추적하는, count
라는, 변수 속성도 선언합니다.
속성과 동일한 점 구문으로 인스턴스 메소드를 호출합니다:
let counter = Counter()
// 초기 횟수는 0 임
counter.increment()
// 이제 횟수는 1 임
counter.increment(by: 5)
// 이제 횟수는 6 임
counter.reset()
// 이제 횟수는 0 임
함수 매개 변수는, Function Argument Labels and Parameter Names (함수의 인자 이름표와 매개 변수 이름) 에서 설명한 것 처럼, (함수 본문 안에서 사용하는) 이름과 (함수 호출 때 사용하는) 인자 이름표 둘 다를 가질 수 있습니다. 메소드 매개 변수도 똑같은데, 메소드는 그냥 타입과 결합한 함수일 뿐이기 때문입니다.
타입의 모든 인스턴스는, 인스턴스 그 자체와 정확하게 같다고 볼 수 있는, self
라는 암시적인 속성을 가집니다.5 self
속성을 사용하여 자신의 인스턴스 메소드 안에서 현재의 인스턴스를 참조합니다.
위 예제의 increment()
메소드는 다음 같이 작성할 수도 있습니다:
func increment() {
self.count += 1
}
실상, 코드에서 self
를 작성할 필요는 거의 없습니다. self
를 명시하지 않으면, 메소드 안에서 알고 있는 속성이나 메소드를 사용할 때마다 현재 인스턴스의 속성이나 메소드를 참조할 거라고 스위프트가 가정합니다. 이 가정은 세 개의 Counter
인스턴스 메소드 안에서 (self.count
보단) count
를 사용함으로써 실증했습니다.
이 규칙에 대한 주요 예외는 인스턴스 메소드의 매개 변수 이름과 그 인스턴스 속성의 이름이 똑같을 때 일어납니다. 이 상황에선, 매개 변수 이름이 우선하며, 속성을 참조하려면 더 규명된 방식으로 할 필요가 있습니다.6 self
속성을 사용하여 매개 변수 이름과 속성 이름을 구별합니다.
다음의, self
는 x
라는 메소드 매개 변수와 역시 x
인 인스턴스 속성을 헷갈리지 않게 합니다:
struct Point {
var x = 0.0, y = 0.0
func isToTheRightOf(x: Double) -> Bool {
return self.x > x
}
}
let somePoint = Point(x: 4.0, y: 5.0)
if somePoint.isToTheRightOf(x: 1.0) {
print("This point is to the right of the line where x == 1.0")
}
// "This point is to the right of the line where x == 1.0" 을 인쇄함
self
접두사가 없다면, 두 x
모두 x
라는 메소드 매개 변수를 참조라는 거라고 스위프트가 가정할 겁니다.
구조체와 열거체는 값 타입 (value types) 입니다. 기본적으로, 값 타입의 속성은 자신의 인스턴스 메소드 안에서 수정할 수 없습니다.
하지만, 한 특별한 메소드 안에서 구조체나 열거체의 속성을 수정할 필요가 있다면, 그 메소드에 직접 변경 (mutating) 동작을 선택할 수 있습니다. 그러면 메소드가 메소드 안에서 자신의 속성을 변경 (즉, 바꿀) 수 있으며, 어떻게 바꾸든 메소드가 끝날 때 원본 구조체에 도로 작성합니다. 메소드는 자신의 암시적 self
속성에 완전히 새로운 인스턴스를 할당할 수도 있으며, 이 새 인스턴스는 메소드가 끝날 때 기존 걸 교체할 겁니다.
그 메소드의 func
키워드 앞에 mutating
키워드를 둠으로써 직접 이 동작을 선택할 수 있습니다:
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, deltaY: Double) {
x += deltaX
y += deltaY
}
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveBy(x: 2.0, y: 3.0)
print("The point is now at (\(somePoint.x), \(somePoint.y)")
// "The point is now at (3.0, 4.0)" 를 인쇄함
위 Point
구조체는, 정해진 양만큼 Point
인스턴스를 이동하는, moveBy(x:y:)
라는 변경 메소드를 정의합니다. 새 점 (point) 을 반환하는 대신, 이 메소드는 자신을 호출하는 점을 실제로 이동합니다. mutating
키워드를 정의에 추가하여 자신의 속성을 수정할 수 있도록 합니다.
Stored Properties of Constant Structure Instances (상수 구조체 인스턴스의 저장 속성) 에서 설명한 것처럼, 변수 속성인 경우에도, 자신의 속성을 바꿀 순 없기 때문에, 구조체 타입의 상수에서는 변경 메소드를 호출할 수 없다는 점을 기억하기 바랍니다.
let fixedPoint = Point(x: 3.0, y: 3.0)
fixedPoint.moveBy(x: 2.0, y: 3.0)
// 이는 에러를 보고할 것임
변경 메소드는 암시적인 self
속성에 완전히 새로운 인스턴스를 할당할 수 있습니다. 위에서 본 Point
예제를 다음과 같은 식으로 대신 작성할 수도 있습니다:
struct Point {
var x = 0.0 y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
self = Point(x: x + deltaX, y: y + deltaY)
}
}
이 버전의 moveBy(x:y:)
변경 메소드는 x
와 y
값을 목표 위치로 설정한 새로운 구조체를 생성합니다. 이 대체 버전 메소드의 호출 결과는 앞선 버전의 호출과 정확히 똑같을 겁니다.
열거체의 변경 메소드는 암시적인 self
매개 변수7 에 동일 열거체의 다른 case 를 설정할 수 있습니다:
enum TriStateSwitch {
case off, low, high
mutating func next() {
switch self {
case .off:
self = .low
case .low:
self = .high
case .high:
self = .off
}
}
}
var ovenLight = TriStateSwitch.low
ovenLight.next()
// ovenLight 는 이제 .high 임
ovenLight.next()
// ovenLight 는 이제 .off 임
이 예제는 3-상 (three-state) 스위치를 위한 열거체를 정의합니다. 이 스위치는 자신의 next()
메소드를 호출할 때마다 (off
, low
, 및 high
라는) 서로 다른 세 전원 상태를 순환합니다.
인스턴스 메소드는, 위에서 설명한 것처럼, 한 특별한 타입의 인스턴스에서 호출하는 메소드입니다. 타입 그 자체에서 호출하는 메소드도 정의할 수 있습니다. 이런 종류의 메소드를 타입 메소드 (type methods) 라고 합니다. 타입 메소드는 메소드의 func
키워드 앞에 static
키워드를 작성하여 지시합니다. 클래스는 class
키워드를 대신 사용하여, 그 메소드의 상위 클래스 구현을 하위 클래스가 재정의하도록 허용할 수 있습니다.
오브젝티브-C 에선, 오브젝티브-C 클래스의 타입-수준 메소드만 정의할 수 있습니다. 스위프트에선, 클래스, 구조체, 및 열거체 모두의 타입-수준 메소드를 정의할 수 있습니다. 각각의 타입 메소드 영역은 자신이 지원하는 타입으로 명시적으로 정해집니다.
타입 메소드는, 인스턴스 메소드 같이, 점 구문으로 호출합니다. 하지만, 그 타입의 인스턴스에서가 아닌, 타입에서 타입 메소드를 호출합니다. 다음은 SomeClass
라는 클래스에 대한 타입 메소드 호출 방법입니다:
class SomeClass {
class func someTypeMethod() {
// 타입 메소드 구현은 여기에 둠
}
}
SomeClass.someTypeMethod()
타입 메소드 본문 안에서, 암시적인 self
속성은, 그 타입의 인스턴스 보단, 타입 그 자체를 참조합니다. 이는, 인스턴스 속성과 인스턴스 메소드 매개 변수에서 처럼, 타입 속성과 타입 메소드 매개 변수를 헷갈리지 않게 하는데 게 하는데 self
를 사용할 수 있다는 의미입니다.
더 일반적으로, 타입 메소드 본문 안에서 사용한 규명 안된8 어떤 메소드와 속성 이름이든 다른 타입-수준 메소드와 속성을 참조할 것입니다. 타입 메소드는, 타입 이름을 접두사로 붙이지 않은, 메소드 이름을 가지고 또 다른 타입 메소드를 호출할 수 있습니다. 이와 비슷하게, 구조체와 열거체에 대한 타입 메소드는 타입 이름 접두사를 붙이지 않고 타입 속성 이름으로 타입 속성에 접근할 수 있습니다.
아래 예제는 LevelTracker
라는 구조체를 정의하여, 참여자의 게임 레벨이나 단계별 진행 상황을 추적합니다. 단일-플레이 게임이지만, 한 기기에서 여러 명의 플레이 정보를 저장할 수 있습니다.
게임을 최초로 플레이할 때는 모든 게임 레벨을 (첫 레벨만 빼고) 잠굽니다. 참가자가 레벨을 종료할 때마다, 기기의 모든 참가자에게 그 레벨이 풀립니다. LevelTracker
구조체는 풀린 게임 레벨을 계속 추적하고자 타입 속성과 메소드를 사용합니다. 이는 개별 참가자의 현재 레벨도 추적합니다.
struct LevelTracker {
static var highestUnlockedLevel = 1
var currentLevel = 1
static func unlock(_ level: Int) {
if level > highestUnlockedLevel { highestUnlockedLevel = level }
}
static func isUnlocked(_ level: Int) -> Bool {
return level <= highestUnlockedLevel
}
@discardableResult
mutating func advance(to level: Int) -> Bool {
if LevelTracker.isUnlocked(level) {
currentLevel = level
return true
} else {
return false
}
}
}
LevelTracker
구조체는 어떤 참가자가 풀었든 가장 높은 레벨을 추적합니다. 이 값은 highestUnlockedLevel
이라는 타입 속성에 저장합니다.
LevelTracker
는 highestUnlockedLevel
속성과 작업할 두 개의 타입 함수도 정의합니다. 첫 번째는, 새 레벨을 풀 때마다 highestUnlockedLevel
값을 갱신할 , unlock(_:)
이라는 타입 함수입니다. 두 번째는, 한 특별한 레벨이 이미 푼 것이면 true
를 반환할, isUnlocked(_:)
라는 편의(를 위한) 타입 함수입니다. (이러한 타입 메소드는 LevelTracker.highestUnlockedLevel
이라고 작성하지 않고도 highestUnlockedLevel
타입 속성에 접근할 수 있다는 걸 기억하기 바랍니다.)
자신의 타입 속성과 타입 메소드에 더해, LevelTracker
는 개별 참가자의 게임 진행 상황도 추적합니다. 이는 currentLevel
이라는 인스턴스 속성으로 플레이어가 현재 플레이 중인 레벨을 추적합니다.
currentLevel
속성 관리를 돕고자, LevelTracker
는 advance(to:)
라는 인스턴스 메소드를 정의합니다. currentLevel
갱신 전에, 새로 요청한 레벨을 이미 풀었는지 이 메소드가 검사합니다. advance(to:)
메소드는 currentLevel
설정이 실제로 가능한지 지시하는 불리언 값을 반환합니다. advance(to:)
메소드를 호출한 코드가 반환 값을 무시하는 게 반드시 실수일 필요는 없기 때문에, 이 함수를 @discardableResult
특성으로 표시합니다. 이 특성에 대한 더 많은 정보는, Attributes (특성) 장을 보도록 합니다.
LevelTracker
구조체는, 아래에 보는 것처럼, Player
클래스와 사용하여, 개별 참가자의 진행 상황을 추적하고 갱신합니다:
class Player {
var tracker = LevelTracker()
let playerName: String
func complete(level: Int) {
LevelTracker.unlock(level + 1)
tracker.advance(to: level + 1)
}
init(name: String) {
playerName = name
}
}
Player
클래스는 참가자의 진행 상황을 추적하고자 새로운 LevelTracker
인스턴스를 생성합니다. 이는, 참가자가 한 특별한 레벨을 완료할 때마다 호출되는, complete(level:)
이라는 메소드도 제공합니다. 이 메소드는 모든 참가자에게 그 다음 레벨을 풀고 참가자의 진행 상황을 갱신하여 그 다음 레벨로 이동합니다. (advance(to:)
의 불리언 반환 값은 무시하는데, 이전 줄에서 LevelTracker.unlock(_:)
을 호출함으로써 레벨을 풀은 걸 알고 있기 때문입니다.)
새 참가자를 위한 Player
클래스 인스턴스를 생성하고, 참가자가 첫 레벨을 완료할 때 무슨 일이 발생하는지 볼 수 있습니다:
var player = Player(name: "Argyrious")
player.complete(level: 1)
print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")
// "highest unlocked level is now 2" 를 인쇄함
두 번째 참가자를 생성하여, 게임에서 어떤 참가자도 아직 풀지 않은 레벨로 이동하려고 하면, 참가자의 현재 레벨을 설정하려는 시도가 실패합니다:
player = Player(name: "Beto")
if player.tracker.advance(to: 6) {
print("player is now on level 6")
} else {
print("level 6 has not yet been unlocked")
}
// "level 6 has not yet been unlocked" 를 인쇄함
‘한 특별한 타입과 결합한 함수’ 를 부르는 호칭은 프로그래밍 언어마다 조금씩 다르며, 스위프트에서 메소드라고 하는 걸 다른 언어에서는 ‘멤버 함수 (member function)’ 나 ‘프로시져 (procedure)’ 라고 하기도 합니다. ↩
C 언어의 ‘구조체 (structure)’ 는 ‘합성 (composite) 자료 타입’ 이라 ‘메소드’ 를 직접 정의할 수는 없고 ‘함수 포인터 (function pointers)’ 의 형태로 정의할 수 있다고 합니다. 보다 자세한 정보는 위키피디아의 struct (C programming language) 항목과 스택오버플로우 사이트의 Define functions in structs 항목을 보도록 합니다. ↩
암시적으로 접근한다는 건 다른 인스턴스 메소드와 속성에 접근할 때 self.
를 붙이지 않아도 된다는 의미입니다. ↩
‘암시적인 속성 (implicit property)’ 이란 사용자가 따로 정의하지 않아도 자동으로 가지게 되는 속성입니다. ↩
스위프트에서 ‘규명한다 (qualified)’ 라는 건 ‘자신의 소속이 어디인지 밝힌다’ 라는 의미입니다. 즉, 속성을 참조하려면 먼저 이 속성의 소속이 어디인지부터 밝혀야 한다는 의미입니다. ↩
즉, 따로 명시하지 않아도 열거체 메소드 안에서 self
를 쓸 수 있습니다. ↩
‘규명 안된 (unqualified) 메소드와 속성 이름’ 이란 ‘접두사가 없어서 어디에 소속된 메소드와 속성인지를 알 수 없다’ 는 의미입니다. 이 경우, 스위프트는 자기 나름대로 이를 규명하려고 하는데, 자신이 타입 메소드이기 때문에 다른 타입-수준 메소드와 속성을 참조하는 ㄴ것입니다. ↩