Methods

Methods

메서드(method)는 어떤 타입에 속한 함수예요. 클래스, 구조체, 열거형 모두 인스턴스 메서드를 정의할 수 있고, 거기에 더해 타입 자체에 속한 타입 메서드도 정의할 수 있죠. 구조체와 열거형에 메서드를 붙일 수 있다는 점은 Swift가 C나 Objective-C와 크게 달라진 부분이에요. Objective-C에서는 클래스만 메서드를 가질 수 있었는데, Swift에서는 어떤 타입을 만들든 그 위에 메서드를 자유롭게 정의할 수 있거든요.

출처: The Swift Programming Language

본문

인스턴스 메서드 (Instance Methods)

인스턴스 메서드(instance method)는 특정 클래스, 구조체, 열거형의 인스턴스에 속한 함수예요. 인스턴스의 프로퍼티를 읽고 바꾸는 방법을 제공하거나, 인스턴스가 맡은 일과 관련된 기능을 제공하는 식으로 그 인스턴스를 돕는 역할을 하죠. 인스턴스 메서드의 문법은 함수와 똑같아요. 함수 챕터에서 본 그대로예요.

인스턴스 메서드는 자신이 속한 타입의 중괄호 안에 작성해요. 그리고 그 타입의 다른 인스턴스 메서드와 프로퍼티에 암시적으로 접근할 수 있어요. 한 가지 주의할 점은, 인스턴스 메서드는 자기 타입의 특정 인스턴스에서만 부를 수 있다는 거예요. 인스턴스 없이 홀로 호출할 수는 없어요.

간단한 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으로 되돌려요.

그리고 현재 카운터 값을 기억하는 변수 프로퍼티 count도 선언돼 있죠.

인스턴스 메서드는 프로퍼티와 똑같이 점(dot) 문법으로 호출해요.

let counter = Counter()
// the initial counter value is 0
counter.increment()
// the counter's value is now 1
counter.increment(by: 5)
// the counter's value is now 6
counter.reset()
// the counter's value is now 0

함수 파라미터는 함수 안에서 쓰는 이름(parameter name)과 호출할 때 쓰는 인자 레이블(argument label)을 따로 가질 수 있어요. 메서드 파라미터도 마찬가지예요. 메서드는 결국 타입에 속한 함수니까 같은 규칙이 그대로 적용돼요.

self 프로퍼티 (The self Property)

타입의 모든 인스턴스에는 self라는 암시적 프로퍼티가 있어요. 이 프로퍼티는 바로 인스턴스 자신과 정확히 같은 값이에요. 인스턴스 메서드 안에서는 이 self를 써서 현재 인스턴스를 가리킬 수 있어요.

아까 예시의 increment() 메서드는 이렇게 쓸 수도 있었어요.

func increment() {
    self.count += 1
}

하지만 실제 코드에서 self를 매번 쓸 필요는 거의 없어요. self를 생략하면, 메서드 안에서 알고 있는 프로퍼티나 메서드 이름을 쓸 때 Swift가 자동으로 '현재 인스턴스의 프로퍼티나 메서드'라고 가정하거든요. Counter의 인스턴스 메서드 세 개 안에서 self.count 대신 그냥 count를 쓴 것도 바로 그 이유예요.

이 규칙에는 중요한 예외가 있어요. 인스턴스 메서드의 파라미터 이름이 그 인스턴스의 프로퍼티 이름과 같을 때예요. 이런 상황에서는 파라미터 이름이 우선이라서, 프로퍼티를 가리키려면 더 명확하게 표현해야 해요. 바로 그때 self를 써서 파라미터 이름과 프로퍼티 이름을 구분하는 거예요.

여기서 selfx라는 메서드 파라미터와 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")
}
// Prints "This point is to the right of the line where x == 1.0".

self 접두사를 빼면, 두 곳의 x를 모두 메서드 파라미터인 x로 해석해 버려요. 그래서 프로퍼티 쪽에는 self.x를 붙여 명확히 해 주는 거예요.

인스턴스 메서드 안에서 값 타입 수정하기 (Modifying Value Types from Within Instance Methods)

구조체와 열거형은 **값 타입(value type)**이에요. 값 타입은 기본적으로 인스턴스 메서드 안에서 자신의 프로퍼티를 수정할 수 없어요.

하지만 특정 메서드 안에서 구조체나 열거형의 프로퍼티를 바꿔야 한다면, 그 메서드에 mutating(변경) 동작을 허용할 수 있어요. 그렇게 하면 메서드는 자신의 프로퍼티를 메서드 안에서 변경(mutate)할 수 있고, 메서드가 끝나면 바뀐 내용이 원래 구조체에 그대로 기록돼요. 게다가 암시적 self 프로퍼티에 완전히 새로운 인스턴스를 통째로 할당할 수도 있는데, 이때는 메서드가 끝나면 새 인스턴스가 기존 인스턴스를 대체해요.

이 동작을 허용하려면 해당 메서드의 func 키워드 앞에 mutating 키워드를 붙이면 돼요.

struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(x deltaX: Double, y 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))")
// Prints "The point is now at (3.0, 4.0)".

Point 구조체는 mutating 메서드 moveBy(x:y:)를 정의해요. 이 메서드는 Point 인스턴스를 일정량만큼 이동시키죠. 새 점을 반환하는 대신, 호출된 그 점 자체를 실제로 수정해요. 정의에 mutating 키워드를 붙인 덕분에 프로퍼티를 바꿀 수 있게 된 거예요.

한 가지 주의할 점이 있어요. 구조체 타입의 상수에서는 mutating 메서드를 호출할 수 없어요. 프로퍼티가 변수 프로퍼티라도, 상수 인스턴스의 프로퍼티는 바꿀 수 없기 때문이에요.

let fixedPoint = Point(x: 3.0, y: 3.0)
fixedPoint.moveBy(x: 2.0, y: 3.0)
// this will report an error

mutating 메서드 안에서 self에 할당하기 (Assigning to self Within a Mutating Method)

mutating 메서드는 암시적 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)
    }
}

이 버전의 mutating moveBy(x:y:) 메서드는 xy 값을 목표 위치로 설정한 새로운 구조체를 만들어요. 호출 결과는 앞선 버전과 완전히 똑같아요.

열거형의 mutating 메서드는 암시적 self 파라미터를 같은 열거형의 다른 케이스로 바꿔 놓을 수도 있어요.

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 is now equal to .high
ovenLight.next()
// ovenLight is now equal to .off

이 예시는 삼단 스위치(tri-state switch)를 위한 열거형이에요. next() 메서드를 부를 때마다 스위치가 off, low, high 세 가지 전원 상태 사이를 순환해요.

타입 메서드 (Type Methods)

지금까지 본 인스턴스 메서드는 특정 타입의 인스턴스에서 호출하는 메서드였어요. 그런데 타입 자체에서 호출하는 메서드도 정의할 수 있어요. 이걸 *타입 메서드(type method)*라고 불러요. 타입 메서드는 메서드의 func 키워드 앞에 static 키워드를 붙여서 표시해요. 클래스에서는 class 키워드를 쓸 수도 있어요. 그러면 서브클래스가 슈퍼클래스의 해당 메서드 구현을 오버라이드할 수 있게 돼요.

참고: Objective-C에서는 Objective-C 클래스에 대해서만 타입 수준 메서드를 정의할 수 있었어요. Swift에서는 모든 클래스, 구조체, 열거형에 타입 수준 메서드를 정의할 수 있고, 각 타입 메서드는 자신이 지원하는 타입에 명확히 한정돼요.

타입 메서드도 인스턴스 메서드처럼 점 문법으로 호출해요. 다만 인스턴스가 아니라 타입에 대해 호출한다는 점이 다르죠. 여기 SomeClass라는 클래스에 타입 메서드를 호출하는 방법이 있어요.

class SomeClass {
    class func someTypeMethod() {
        // type method implementation goes here
    }
}
SomeClass.someTypeMethod()

타입 메서드 본문 안에서는 암시적 self 프로퍼티가 인스턴스가 아니라 타입 그 자체를 가리켜요. 그래서 인스턴스 프로퍼티와 인스턴스 메서드 파라미터를 구분하던 것처럼, self를 써서 타입 프로퍼티와 타입 메서드 파라미터를 구분할 수 있어요.

더 일반적으로 말하면, 타입 메서드 본문 안에서 한정되지 않고 쓴 메서드·프로퍼티 이름은 모두 다른 타입 수준의 메서드와 프로퍼티를 가리켜요. 타입 메서드는 다른 타입 메서드를 타입 이름 접두사 없이 그냥 이름만으로 호출할 수 있고, 구조체와 열거형의 타입 메서드도 타입 프로퍼티 이름만으로 접근할 수 있어요.

아래 예시는 LevelTracker라는 구조체를 정의해요. 게임의 여러 레벨·단계를 지나가는 플레이어의 진행 상황을 추적하는 구조체예요. 1인용 게임이지만, 한 기기에서 여러 플레이어의 정보를 저장할 수 있어요.

게임을 처음 시작하면 1레벨 빼고 모든 레벨이 잠겨 있어요. 플레이어가 레벨을 끝낼 때마다 그 레벨이 기기에 있는 모든 플레이어에게 잠금 해제돼요. 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 프로퍼티를 다루는 타입 함수도 두 개 정의해요. 첫 번째는 unlock(_:)이라는 타입 함수로, 새 레벨이 잠금 해제될 때마다 highestUnlockedLevel의 값을 갱신해요. 두 번째는 isUnlocked(_:)이라는 편의용 타입 함수로, 특정 레벨 번호가 이미 잠금 해제됐으면 true를 반환해요. (이 타입 메서드들은 LevelTracker.highestUnlockedLevel이라고 쓰지 않고도 highestUnlockedLevel 타입 프로퍼티에 접근할 수 있어요.)

타입 프로퍼티와 타입 메서드에 더해, LevelTracker는 게임을 지나는 개별 플레이어의 진행 상황도 추적해요. currentLevel이라는 인스턴스 프로퍼티로 플레이어가 지금 플레이 중인 레벨을 기억해 두죠.

currentLevel 프로퍼티를 관리하기 위해 LevelTrackeradvance(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 클래스의 인스턴스를 만들고, 플레이어가 1레벨을 끝내면 어떤 일이 생기는지 살펴볼게요.

var player = Player(name: "Argyrios")
player.complete(level: 1)
print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")
// Prints "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 hasn't yet been unlocked")
}
// Prints "level 6 hasn't yet been unlocked".

더 알아보기

  • 인스턴스 메서드: 특정 인스턴스에 속한 함수로, 점 문법으로 호출하며 그 타입의 다른 메서드·프로퍼티에 암시적으로 접근해요.
  • self 프로퍼티: 인스턴스 자신을 가리키는 암시적 프로퍼티. 파라미터 이름과 프로퍼티 이름이 겹칠 때 구분하는 데 주로 쓰여요.
  • mutating: 구조체·열거형 같은 값 타입의 메서드가 자신의 프로퍼티를 바꾸도록 허용하는 키워드. self에 새 인스턴스를 통째로 할당할 수도 있어요.
  • 타입 메서드: static(또는 클래스에서 class) 키워드로 정의하며 타입 자체에서 호출해요. self는 타입 자신을 가리켜요.
  • 관련 챕터: 함수(Functions), 프로퍼티(Properties), 속성(Attributes)