xho95 (소중한꿈)'s Swift Life

Apple 에서 공개한 The Swift Programming Language (Swift 5.7) 책의 Methods 부분1을 번역하고, 설명이 필요한 부분은 주석을 달아서 정리한 글입니다. 전체 번역은 Swift 5.7: Swift Programming Language (스위프트 프로그래밍 언어) 에서 확인할 수 있습니다.

Methods (메소드)

메소드 (methods) 는 한 특별한 타입과 결합한 함수입니다.2 클래스, 구조체, 및 열거체 모두 인스턴스 메소드 (instance methods) 를 정의하여, 주어진 타입 인스턴스와 작업하기 위한 특정 임무 (tasks) 와 기능을 은닉할 수 있습니다. 클래스, 구조체, 및 열거체는, 타입 그 자체와 결합한, 타입 메소드 (type methods) 도 정의할 수 있습니다. 타입 메소드는 오브젝티브-C 의 클래스 메소드 (class methods) 와 비슷합니다.

스위프트에선 구조체와 열거체가 메소드를 정의할 수 있다는 사실이 C 및 오브젝티브-C 와의 주요한 차이점입니다.3 오브젝티브-C 에선, 클래스가 메소드를 정의할 수 있는 유일한 타입입니다. 스위프트에선, 생성한 타입에 대한 메소드 정의라는 유연함을 여전히 가지면서, 클래스나, 구조체, 열거체 중 어느 걸로 정의할 지를 선택할 수 있습니다.

Instance Methods (인스턴스 메소드)

인스턴스 메소드 (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 클래스는 세 개의 인스턴스 메소드를 정의합니다:

Counter 클래스는, 현재 횟수를 계속 추적하는, count 라는, 변수 속성도 선언합니다.

속성과 동일한 점 구문으로 인스턴스 메소드를 호출합니다:

let counter = Counter()
// 초기 횟수는 0 임
counter.increment()
// 이제 횟수는 1 임
counter.increment(by: 5)
// 이제 횟수는 6 임
counter.reset()
// 이제 횟수는 0 임

함수 매개 변수는, Function Argument Labels and Parameter Names (함수의 인자 이름표와 매개 변수 이름) 에서 설명한 것 처럼, (함수 본문 안에서 사용하는) 이름과 (함수 호출 때 사용하는) 인자 이름표 둘 다를 가질 수 있습니다. 메소드 매개 변수도 똑같은데, 메소드는 그냥 타입과 결합한 함수일 뿐이기 때문입니다.

The self Property (self 속성)

타입의 모든 인스턴스는, 인스턴스 그 자체와 정확하게 같다고 볼 수 있는, self 라는 암시적인 속성을 가집니다.5 self 속성을 사용하여 자신의 인스턴스 메소드 안에서 현재의 인스턴스를 참조합니다.

위 예제의 increment() 메소드는 다음 같이 작성할 수도 있습니다:

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

실상, 코드에서 self 를 작성할 필요는 거의 없습니다. self 를 명시하지 않으면, 메소드 안에서 알고 있는 속성이나 메소드를 사용할 때마다 현재 인스턴스의 속성이나 메소드를 참조할 거라고 스위프트가 가정합니다. 이 가정은 세 개의 Counter 인스턴스 메소드 안에서 (self.count 보단) count 를 사용함으로써 실증했습니다.

이 규칙에 대한 주요 예외는 인스턴스 메소드의 매개 변수 이름과 그 인스턴스 속성의 이름이 똑같을 때 일어납니다. 이 상황에선, 매개 변수 이름이 우선하며, 속성을 참조하려면 더 규명된 방식으로 할 필요가 있습니다.6 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")
}
// "This point is to the right of the line where x == 1.0" 을 인쇄함

self 접두사가 없다면, 두 x 모두 x 라는 메소드 매개 변수를 참조라는 거라고 스위프트가 가정할 겁니다.

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

구조체와 열거체는 값 타입 (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)
// 이는 에러를 보고할 것임

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

변경 메소드는 암시적인 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:) 변경 메소드는 xy 값을 목표 위치로 설정한 새로운 구조체를 생성합니다. 이 대체 버전 메소드의 호출 결과는 앞선 버전의 호출과 정확히 똑같을 겁니다.

열거체의 변경 메소드는 암시적인 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 (타입 메소드)

인스턴스 메소드는, 위에서 설명한 것처럼, 한 특별한 타입의 인스턴스에서 호출하는 메소드입니다. 타입 그 자체에서 호출하는 메소드도 정의할 수 있습니다. 이런 종류의 메소드를 타입 메소드 (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 이라는 타입 속성에 저장합니다.

LevelTrackerhighestUnlockedLevel 속성과 작업할 두 개의 타입 함수도 정의합니다. 첫 번째는, 새 레벨을 풀 때마다 highestUnlockedLevel 값을 갱신할 , unlock(_:) 이라는 타입 함수입니다. 두 번째는, 한 특별한 레벨이 이미 푼 것이면 true 를 반환할, isUnlocked(_:) 라는 편의(를 위한) 타입 함수입니다. (이러한 타입 메소드는 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 클래스 인스턴스를 생성하고, 참가자가 첫 레벨을 완료할 때 무슨 일이 발생하는지 볼 수 있습니다:

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" 를 인쇄함

다음 장

Subscripts (첨자) >

참고 자료

  1. 이 글에 대한 원문은 Methods 에서 확인할 수 있습니다. 

  2. ‘한 특별한 타입과 결합한 함수’ 를 부르는 호칭은 프로그래밍 언어마다 조금씩 다르며, 스위프트에서 메소드라고 하는 걸 다른 언어에서는 ‘멤버 함수 (member function)’ 나 ‘프로시져 (procedure)’ 라고 하기도 합니다. 

  3. C 언어의 ‘구조체 (structure)’ 는 ‘합성 (composite) 자료 타입’ 이라 ‘메소드’ 를 직접 정의할 수는 없고 ‘함수 포인터 (function pointers)’ 의 형태로 정의할 수 있다고 합니다. 보다 자세한 정보는 위키피디아의 struct (C programming language) 항목과 스택오버플로우 사이트의 Define functions in structs 항목을 보도록 합니다. 

  4. 암시적으로 접근한다는 건 다른 인스턴스 메소드와 속성에 접근할 때 self. 를 붙이지 않아도 된다는 의미입니다. 

  5. ‘암시적인 속성 (implicit property)’ 이란 사용자가 따로 정의하지 않아도 자동으로 가지게 되는 속성입니다. 

  6. 스위프트에서 ‘규명한다 (qualified)’ 라는 건 ‘자신의 소속이 어디인지 밝힌다’ 라는 의미입니다. 즉, 속성을 참조하려면 먼저 이 속성의 소속이 어디인지부터 밝혀야 한다는 의미입니다. 

  7. 즉, 따로 명시하지 않아도 열거체 메소드 안에서 self 를 쓸 수 있습니다. 

  8. ‘규명 안된 (unqualified) 메소드와 속성 이름’ 이란 ‘접두사가 없어서 어디에 소속된 메소드와 속성인지를 알 수 없다’ 는 의미입니다. 이 경우, 스위프트는 자기 나름대로 이를 규명하려고 하는데, 자신이 타입 메소드이기 때문에 다른 타입-수준 메소드와 속성을 참조하는 ㄴ것입니다.