초기화 패턴

스위프트의 초기화는 다른 언어들에 비해 유연하고, 기발하며, 강력하다고 생각합니다. 매우 구체적인 규칙을 따르고, 몇몇은 다른 주요 프로그래밍 언어들과 완전히 다른 방식으로 작동하며 그 규칙을 조금이라도 변형 할 수 있는 범위 또한 없습니다. 스위프트의 아버지라고 할 수 있는 Objective-C 의 초기화도 다른 언어들과의 독특하게 작동하기 때문에 스위프트 또한 그대로 유지되었을까 하는 추측입니다.

일반적인 규칙

  • 모든 저장된 속성은 초기화가 끝나기 전에 값을 가져야 합니다.
  • 초기화시, 모든 속성의 값이 있을 때까지 내부 메서드를 호출하지 않을 수도 있습니다.
  • 기본값을 사용한다면, 사용자가 임의로 정의한 초기화 메서드를 사용하지 않아도 됩니다.
  • 옵셔널 값은 기본값으로 nil 이기 때문에 초기화 시, 기본값을 지정할 필요가 없습니다.
  • 속성을 기본값으로 지정하거나 초기화 도중 값을 주입하게 되면 속성이 트리거가 되지 않습니다.

구조체의 초기화

스위프트의 초기화는 일반적으로 복잡한 규칙 기반으로 되어있지만, 구조체만큼은 예외입니다. 구조체 초기화가 매우 간단하며 사실 멤버 변수 초기화 형식을 기입하지 않아도 알아서 초기화 메서드를 만들어주는 보너스 기능도 있습니다.

struct Person {
  var name: String
}

위의 구조체를 만들었고, 해당 구조체에 값을 주입하려고 하면 자동으로 Person(name:)  이라는 자동으로 초기화 메서드가 생성이 됩니다. 특성을 더 추가하면 매개변수 목록에 순차적으로 추가됩니다. 이러한 기능을 맴버 전용 초기화라고 부르며, 구조체 타입에서만 사용할 수 있습니다. 이러한 기능을 제공하는 이유는 클래스가 상속으로 인해 훨씬 더 복잡해졌기 때문이며 구조체를 쓰는 장점중 하나라고 할 수 있습니다.

멤버 전용 초기화는 내부의 멤버 변수가 정의되지 않았을 경우 (위에 언급한것과 같이 옵셔널 타입은 기본적으로 nil이 정의되어있습니다) 에만 생성이 됩니다. 이 메서드는 멤버 변수에 변경에 따라 자동으로 변경이 되기 때문에 실수로 변경된 멤버 변수를 수정하지 않는 경우의 오류를 사전에 예방할 수 있습니다.

struct Person {
  var name: String

  init() {
    name = "Scott"
  }
}

위의 코드처럼 init()을 개발자가 직접 지정해버린다면, 자동 멤버 변수 초기화 기능은 사라져 버립니다. 나는 자동 변수 초기화도 사용하고 싶고, 위의 소스처럼 고정된 값을 추가하고 싶을 경우, 다음과 같이 사용하면 됩니다.

struct Person {
  var name: String
}

extension Person {
  init() {
    name = "Scott"
  }
}

클래스 초기화

스위프트의 클래스 초기화의 경우는 구조체보다 훨씬 더 복잡하게 되어있습니다. 대부분의 복잡성은 다른 클래스의 상속과도 관련이 있습니다.

클래스 초기화의 규칙

  1. 자신의 초기화 메서드가 끝나기 전에 부모 클래스 초기화 메서드 중에 하나를 호출해야 합니다.
  2. 부모 클래스의 초기화를 호출하기 전에 부모 클래스의 속성을 변경할 수 없습니다.
  3. extension 에는 convenience init() 메서드는 사용할 수 있지만, init() 을 따로 사용할 수 없습니다.
  4. 자식 클래스에 지정된 초기화 메서드가 없다면, 부모 클래스의 지정된 초기화 메서드를 모두 상속 받습니다.
  5. 자식 클래스가 부모 클래스의 모든 지정된 초기화 메서드를 구현할 경우, 모든 자동 초기화 메서드는 자동으로 상속됩니다.
  6. 규칙 4와 5가 적용되지 않는 경우 ( 자식 클래스에 자체 초기화 메서드는 있지만 부모 클래스의 지정된 초기화 메서드를 구현하지 않는 경우) 부모 클래스의 이니셜 라이저를 상속하지 않습니다.

규칙 6의 경우, 이 규칙을 이해하지 못하는 개발자들에게 에러를 발생합니다. 예를 들어, 부모클래스에 일반적으로 몇 가지 추가 속성으로 커스텀 클래스를 만들고 나서, 초기화 메서드를 작성하려고 하는 시점에 발생합니다. 이 규칙을 재대로 이해하려고 하려면 convenience 와 required 키워드 와 초기화 위임에 대한 이해가 필요합니다.

지정자와 편의 초기화 ( convenience initializers )

위의 규칙은 이해하면 납득을 하는데 어렵지 않습니다. 초기화 메서드를 만들지 않았거나 상속하지 않으면 걱정할 이유가 없기 때문입니다. 그러나 몇몇 지정 및 편리 이니셜 라이저의 차이점과 코드에서 유용 할 수 있는 이유를 완전히 이해하려면 다음과 같은 규칙을 알아야 합니다.

  • 모든 클래스에는 적어도 하나 이상의 지정된 초기화 메서드가 필요합니다.
  • 다른 클래스에서 상속 받고 자신의 초기화 메서드가 없는 경우, 부모에게 지정된 초기화 메서드를 가지고 옵니다.
  • 만일 모든 속성에 기본값이 있으면, 자동으로 지정된 초기화 메서드를 제공합니다.
  • 단 한개의 고유 초기화 메서드를 작성하는 경우, 초기화가 완료 될 때 까지 모든 특정이 값을 가져야 하며, 상위 클래스가 존재하는 경우 상위 초기화 메서드를 호출 해야 합니다.
  • 편의 초기화는 선택 사항이며, 이 기능을 활용한다면 클래스를 생성하는데 간편해 집니다.
  • 편의 초기화 메서드는 다른 편의 초기화 메서드를 호출 할 수 있지만 필수적으로 지정된 초기화 프로그램을 호출 해야합니다.

필수 초기화 메서드

일부 초기화 메서드는 required 키워드로 표시됩니다. ( 프로토콜에서 초기화 메서드를 작성했을 경우에 많이 볼 수 있습니다.) 즉, 이 클래스를 하위 클래스로 만들 경우 반드시 필수 초기화 메서드를 구현해야합니다. 필요한 이니셜 라이저를 구현할 때 require를 포함 시켜서 추가 서브 클래스가 동일한 제한을 갖도록 해야합니다.

이 required 키워드는 특정 초기화 메서드를 구현하도록 강요 합니다. 일부 필수 초기화 메서드는 정말로 중요합니다. 예를 들어, UIView는 다음과 같은 초기화 메서드가 있습니다.

required init?(coder aDecoder: NSCoder) {}

이 초기화 메서드는 인터페이스 빌더에서 뷰가 로드 될때 사용됩니다. Apple은 개발자에게 해당 메서드가 구현되었는지 확인하기 원하기 때문에 만들었으며, 해당 구현부에서는 하위 클래스의UIView를 만들면 interface Builder와 호환 여부를 구현하던가 또는 호환되지 않게 만들 수 있습니다.

선택적인 초기화 메서드

때때로 버전 상의 이유로 작동되지 않을 경우, 스위프트는 해당 메서드를 제공합니다. 바로 init?()init!() 메서드 입니다. 전자는 다른 소스에서 자주 보았지만, 후자의 경우는 거의 사용하지 않습니다. Objective-C에서 가져온 코드를 사용하는 경우에 볼 수 있기 때문이죠. 이 코드는 null 값의 가능성을 사전에 계산하여 암시 적으로 랩핑되지 않는 옵션을 초기화 메서드를 통해 반환하기 때문입니다.

대부분의 초기화 메서드에는 반환 값이 없지만 선택적 초기화 메서드만큼은 예외입니다. 어떠한 이유로 초기화가 완료되지 않았을 경우, nil을 반환 할 수 있습니다. 해당 클래스가 초기화에 실패했을 경우를 대비해 작성하는 경우 유용합니다.

init?(name: String, age: Int)

TIP: 스위프트에의 고급 기능 중 하나는 사용할 수 없는 초기화 메서드로 사용가능한 메서드를 무시할 수 있는 기능이 있습니다. 즉 , 부모 클래스의 초기화 메서드는 nil을 반환 할 수 있지만, nil 반환하지 않는 초기화 메서드를 사용하여 새 하위 클래스를 만들 수 있습니다. 예를 들어

class Animal {
  var type: String?
  
  init() { }
  init?(type: String) {
    if type.isEmpty { return nil }
    self.type = type
  }
}

이 클래스는 type이라는 값은 옵셔널이지만 강제로 값을 넣지 않는 경우, 생성할 수 없도록 제한을 추가하여 클래스를 생성하는데 제한을 걸 수 있습니다.

class Dog: Animal {
  override init(type: String) {
    super.init()

    if type.isEmpty {
      self.type = "Dog"
    } else {
      self.type = type
    }
  }
}

Dog의 클래스는 init(type:) 초기화 하지만, 반드시 생성할 수 있는 버전의 클래스인것 같지만, 사실은 아닙니다. 반드시 성공하려면 다음과 같은 방식으로 초기화를 해야합니다.

class Dog: Animal {
  override init(type: String) {
    if type.isEmpty {
      super.init(type: "Dog")
    } else {
      self.init(type: type)!
    }
  }
}

초기화 취소

구조체에 대한 클래스만의 주요 이점 중 하나는 객체가 소멸 될 때 실행되는 코드에 Deinitializer를 선언할 수 있다는 것입니다. 이니셜 라이저와 마찬가지로 이 이름은 특별히 명명됩니다. 실제로 매개 변수 절이 없기 때문에 더 많이 사용되지만, 객체에 대한 마지막 참조가 해제 될 때 Swift가 자동으로 호출하기 때문에 작성하기가 훨씬 쉽습니다. 클래스 상속을 사용하여 작업 할 때 Swift는 자식 클래스로 부터 부모 및 조 부모로 이동하여 전체 초기화 스택을 호출합니다.

Deinitializers는 Swift가 메모리를 관리 할 수 없는 외부 리소스로 작업을 하는 경우 매우 유용합니다. 예를 들어 SwiftGD  ( 서버 사이드용 코어 그래픽스 렌더링용 오픈소스 프레임워크) 를 사용하면 GD라는 C 언어 라이브러리가 랩핑되며 포인터를 사용하여 이미지 메모리 자체를 관리하므로 Swift는 RAM 공간을 자동으로 비울 수가 없습니다. 따라서 클래스를 사용하여 메모리 누수를 방지하기 위해 초기화 프로그램에서 GD의 메모리를 자동으로 확보 할 수 있습니다. 이러한 결과를 실험하기 위해서 몇가지 예를 들어 드리겠습니다.

Class Animal {
  deinit {
    print("Animal 클래스가 메모리에서 해제 되었습니다")
  }
}

class Dog: Animal {
  deinit {
    deinit {
      print("Dog 클래스가 메모리에서 해제 되었습니다")
    }
  }
}

Xcode의 콘솔에서 Dog 클래스를 해제 하게 되면, 로그는 다음과 같은 순서로 출력하게 됩니다.

  1. Animal 클래스가 메모리에서 해제 되었습니다
  2. Dog 클래스가 메모리에서 해제 되었습니다

이것의 클래스에 대한 해제를 가장 쉽게하는 방법은 다음과 같은 코드를 사용하면 됩니다.

do {
  let animal = Dog()
}

플레이 그라운드 를 통해 임시 스코프를 만들면 Swift가 플레이그라운드가 끝나기 전에 해당 animal 변수를 파괴하게 됩니다.

동적 생성 (Dynamic initializer)

NSClassFromString() 이라는 메서드를 사용하여 클래스를 동적으로 초기화하는 기능입니다. 이것에 대해서는 정말 위험한 방법이라 다소 비관적일 수 도 있지만, 많은 애플 의 플랫폼 패턴과 마찬가지로, 이 또한 기능을 제공합니다.

동적 생성은 Apple 플랫폼 모든 곳에서 이루어지는 아카이빙 패턴과 밀접하게 연결되어 있습니다. 인터페이스 빌더를 사용하여 ViewController 또는 TableViewController 의 Cell 클래스의 이름을 추가하면 IB에서는 이를 문자열로 저장합니다. 런타임에 플랫폼은 NSClassFromString()으로 실제 연결된 클래스가 무엇인지 알아 내기 위해 사용하여, 해당 메서드로 인스턴스화 할 수 있습니다.

Swift는 모든 클래스를 소속 모듈에 따라 자동으로 네임 스페이스로 지정합니다. 이는 프로젝트 이름이 MyProject이고 클래스 이름이 MyClass이라고 한다면, 인터페이스 빌더에는 MyProject.MyClass로 문자로 저장하게 됩니다. 이것은 인터페이스 빌더가 “Inherit From Target” 체크 박스를 사용하여 클래스 이름이 속한 모듈을 묻는 것과 같은 이유로 기본 “MyProject”라는 접두어를 얻게 됩니다.

NSClassFromString()을 직접 해보고 싶다면 MyProject라는 새로운 iOS Sing VIew Application 프로젝트를 만든 후 다음 클래스를 추가해봅시다

@objc class Animal: NSObject {
  override init() {
    print("Animal 클래스가 생성되었습니다!")
  }
}

이 클래스는 내부에서 다음과 같은 절차로 만들어 집니다

  1. 먼저 해당 클래스를 MyProject.Animal 이라는 문자열을 클래스로 변환합니다.
  2. 그 다음 NSObject.Type 클래스를 NSObject로 NSObject.Type으로 type-casting을 합니다.
  3. 이것으로 인하여 우리는 init() 사용할 수 있게 됩니다.
let className = "MyProject.Animal"
if let actureClass = NSClassFromString(className) as? NSObject.Type {
  let animal = actureClass.init()
} else {
  print("클래스를 찾지 못했습니다")
}

마지막으로

스위프트 초기화에는 많은 규칙이 있지만 가능한 한 간결하게 만들기 위해 노력했습니다. 초기화의 최소한의 규칙은 다음과 같습니다.

  • 클래스가 아닌 구조체를 사용하면 초기화가 문제가 되지 않도록 할 수 있습니다.
  • 가능하면 기본값을 제공하거나 속성 옵션을 만들어 초기화의 필요성을 더욱 줄입니다.
  • convenience init() 를 사용하면 더 쉽게 사용할 수 있습니다.
  • 유익한 init() 함수는 실패를 정말로 깨끗하게 표현하고, 많은 내용을 포함하려고 하지 않습니다 (예, guard let, if let )
  • 정말 필요성이 있다고 판단하지 않으면, 동적 생성을 사용하지 마십시오. 인터페이스 빌더가 그 코드를 계속 사용하도록 만들지만, 그것을 답습해서 자신의 코드를 오염시킬 필요는 없습니다.

해당 포스팅은 Swift Design Patterns글을 번역하였습니다.

1 Shares:
You May Also Like
Read More

확장 패턴

스위프트의 확장(Extension) 기능을 사용하면 기존 데이터 유형의 기능을 추가하거나 수정할 수 있습니다.
Read More

프로토콜 패턴

프로토콜이란 사전적 의미로 조약 또는 규약이라는 뜻으로, 특정 작업이나 기능의 일부분을 구현하기 위해 필수, 선택적인 유형을 정의한 인터페이스를 지칭합니다. 프로토콜을 실제로 정의라는 부분을 담당하기 때문에 인터페이스로만 사용이 되며, 구체적인 세부 프로세스가 없기 때문에 이는 추상체라고도 불립니다.