Burt.K

Awesome Discovery

[SE-0018] 유연한 멤버 초기화

작성일 — 2025년 3월 9일

Table of Contents

유연한 멤버 초기화

소개

현재 Swift 컴파일러는 특정 상황에서 멤버별 초기화를 자동으로 생성할 수 있다. 하지만 이 기능에는 여러 제한 사항이 존재한다. 이 제안은 컴파일러가 멤버별 초기화를 자동으로 생성하는 아이디어를 발전시켜, 이를 원하는 모든 초기화 메서드에서 사용할 수 있도록 확장하는 것을 목표로 한다.

동기

타입의 초기화 메서드를 설계할 때, 사용자에게 더 많은 유연성을 제공하려고 할수록 더 많은 보일러플레이트 코드를 작성하고 유지해야 한다는 불편한 현실에 직면한다. 보통 원하는 만큼의 유연성을 얻지 못한 채 더 많은 보일러플레이트 코드로 끝나기 마련이다. 이 문제를 완화하기 위해 다양한 전략이 사용되어 왔다:

  1. 때로는 불변으로 설계해야 할 프로퍼티를 가변으로 만들고, 잠재적으로 안전하지 않은 임시의 두 단계 초기화 패턴을 사용한다. 인스턴스를 초기화한 후 즉시 구성하는 방식이다. 이렇게 하면 불변 프로퍼티를 초기화하기 위해 모든 초기화 메서드에 포함해야 할 보일러플레이트 코드를 피할 수 있다.

  2. 때로는 적절한 기본값이 있는 가변 프로퍼티를 기본값으로 초기화하고, 기본값이 의도한 용도에 맞지 않을 때 동일한 초기화 후 구성 전략을 사용한다. 이렇게 하면 인스턴스가 의도한 용도에 맞게 올바르게 초기화되기 전에 여러 잘못된 상태를 거칠 수 있다.

이 문제의 근본적인 원인은 초기화가 M x N 복잡도(M은 멤버 수, N은 초기화 메서드 수)로 증가한다는 사실이다. 컴파일러로부터 가능한 한 많은 도움을 받아야 한다!

타입 작성자와 사용자 모두에게 유연하고 간결한 초기화 방식을 제공하면, 가능한 경우 불변성을 사용하도록 장려하고, 타입의 초기화 메서드를 설계할 때 고려해야 할 보일러플레이트 코드의 필요성을 줄일 수 있다.

Chris Lattner의 말을 인용하면:

Swift의 기본 멤버별 초기화 메서드 동작에는 다음과 같은 단점이 있다(IMO):
1) 구조체에서 커스텀 init을 정의하면 멤버별 초기화 메서드가 비활성화되며, 이를 쉽게 복구할 방법이 없다.
2) 접근 제어 + 멤버별 초기화 메서드는 종종 직접 구현해야 한다.
3) 클래스에서는 멤버별 초기화 메서드를 얻을 수 없다.
4) 기본 초기화 메서드가 있는 var 프로퍼티는 합성된 초기화 메서드의 매개변수가 기본값을 가져야 한다.
5) 멤버별 초기화 메서드가 있는 lazy 프로퍼티는 문제가 있다(멤버별 초기화 메서드는 이를 즉시 접근한다).

여기에 “전부 또는 전무” 문제를 추가할 수 있다. 컴파일러는 전체 초기화 메서드를 생성하지만, 일부 멤버에 대해 멤버별 초기화를 사용하고 다른 멤버는 수동으로 초기화하려는 경우 보일러플레이트 코드를 제거하는 데 도움이 되지 않는다.

클라이언트가 구성할 수 있는 여러 공개 멤버와 함께 타입의 구현 세부 사항을 포함하는 일부 비공개 상태를 가진 타입을 갖는 경우가 흔하다. 특히 UI 코드에서 시각적 외관 등을 구성하기 위해 많은 프로퍼티를 노출하는 경우가 많다. 유연한 멤버별 초기화는 이러한 사용 사례에서 큰 이점을 제공할 수 있지만, “전부 또는 전무” 방식이라면 즉시 쓸모없게 된다.

일부 멤버에 대해 멤버별 초기화를 합성하면서도 타입 작성자가 구현 세부 사항의 초기화를 완전히 제어할 수 있는 유연한 해결책이 필요하다.

제안하는 해결책

저는 초기화 메서드에 memberwise 선언 수식어를 추가하여 멤버별 초기화 합성을 *선택적으로 적용*할 수 있도록 하는 방안을 제안한다.

이 제안은 저장 프로퍼티가 여러 이유 중 하나라도 해당하지 않는 한 자동으로 멤버별 초기화 매개변수를 받는 자동 모델을 채택한다. 또한 memberwise 선언 수식어를 사용해 프로퍼티가 멤버별 초기화 합성에 *선택적으로 참여*할 수 있는 옵트인 모델도 가능하다.

두 접근 방식은 상호 배타적이지 않다. 어떤 프로퍼티도 memberwise 선언 수식어를 가지지 않을 때는 자동 모델을 사용하고, 하나 이상의 프로퍼티가 memberwise 수식어를 가질 때는 옵트인 모델을 사용할 수 있다. 이 제안의 향후 개선안에서는 옵트인 모델을 도입해 프로그래머가 작성 중인 특정 타입에 대해 선호하는 모델을 선택할 수 있도록 할 수 있다.

현재 제안하는 자동 모델은 초기화 메서드 선언과 초기화 메서드와 적어도 동일한 가시성을 가진 모든 프로퍼티의 선언(프로퍼티에 부착된 동작 포함)만을 고려해 멤버별 초기화 매개변수를 받을 프로퍼티 집합을 결정한다. 규칙은 다음과 같다:

  1. 프로퍼티의 접근 수준은 멤버별 초기화 메서드와 적어도 동일한 가시성을 가져야 한다. var 프로퍼티의 경우 setter의 가시성이 사용된다.
  2. 멤버별 초기화를 금지하는 동작(예: ‘lazy’ 동작)이 없어야 한다.
  3. 프로퍼티가 let 프로퍼티인 경우 초기값을 가질 수 없다.

매개변수는 ... 자리 표시자의 위치에 매개변수 목록으로 합성된다. 멤버별 초기화 메서드에서 ... 자리 표시자를 생략하면 컴파일 타임 오류가 발생한다. 매개변수 목록은 다음과 같이 정렬된다:

  1. 기본값이 없는 모든 매개변수가 기본값이 있는 매개변수보다 앞에 온다.
  2. 각 그룹 내에서 매개변수는 프로퍼티 선언 순서를 따른다.

현재 제안에서는 var 프로퍼티만 기본값을 지정할 수 있으며, 이 값은 해당 프로퍼티의 초기값이 된다. 향후 @default 개선안이나 기본값을 지정할 수 있는 다른 메커니즘을 통해 let 프로퍼티도 기본값을 지정할 수 있도록 할 수 있다.

예제

이 문서의 이번 섹션에서는 실제 동작하는 여러 예제를 다룬다. 모든 가능한 시나리오를 포함하지는 않는다. 궁금한 구체적인 예제가 있다면 리스트에 게시하길 바란다. 함께 논의하고, 중요하다고 판단되는 예제는 이 섹션에 추가할 예정이다.

합성이 어떻게 수행되는지에 대한 구체적인 내용은 상세 설계 문서에서 확인할 수 있다.

현재 멤버별 초기화자를 대체하는 방법

struct S {
	let s: String
	let i: Int

	// 사용자가 선언:
	memberwise init(...) {}
	// 컴파일러가 생성:
	init(s: String, i: Int) {
		/* 생성된 코드 */ self.s = s
		/* 생성된 코드 */ self.i = i
	}
}

초기값을 가진 var 프로퍼티

참고: 이 예제는 var 프로퍼티에만 적용된다. let 프로퍼티의 초기화 규칙 때문에 가능하다. 초기화 표현식에 부수 효과가 포함된 경우, 멤버 초기화 함수를 호출할 때 명시적으로 값을 전달하면 부수 효과가 평가되지 않는다.

struct S {
	var s: String = "hello"
	var i: Int = 42

	// 사용자가 선언:
	memberwise init(...) {}
	// 컴파일러가 합성:
	init(s: String = "hello", i: Int = 42) {
		/* 합성된 */ self.s = s
		/* 합성된 */ self.i = i
	}
}

접근 제어

struct S {
	let s: String
	private let i: Int

	// 사용자가 선언:
	memberwise init(...) {
		// 컴파일러 오류, i 멤버 초기화는 이니셜라이저 자체보다 가시성이 낮기 때문에 합성할 수 없음
	}
}
struct S {
	let s: String
	private let i: Int

	// 사용자가 선언:
	memberwise init(...) {
		i = 42
	}
	// 컴파일러가 합성 (낮은 가시성을 가진 프로퍼티에 대한 멤버 초기화를 억제):
	init(s: String) {
		/* 합성 */ self.s = s
		
		// 사용자의 이니셜라이저 본문이 그대로 유지됨
		i = 42
	}
}

수동으로 선언된 매개변수

struct S {
	let s: String
	private let i: Int

	// 사용자가 선언:
	memberwise init(anInt: Int, anotherInt: Int, ...) {
		i = anInt > anotherInt ? anInt : anotherInt
	}
	// 컴파일러가 합성 (더 낮은 가시성을 가진 프로퍼티에 대한 멤버 초기화를 억제):
	init(anInt: Int, anotherInt: Int, s: String) {
		/* 합성된 코드 */ self.s = s
		
		// 사용자의 초기화 본문 유지
		i = anInt > anotherInt ? anInt : anotherInt
	}
}

지연 속성과 호환되지 않는 동작

struct S {
	let s: String
	lazy var i: Int = InitialValueForI()

	// 사용자가 선언:
	memberwise init(...) {
	}
	// 컴파일러가 합성:
	init(s: String) {
		/* 합성된 코드 */ self.s = s
		
		// 컴파일러는 i를 초기화하는 코드를 합성하지 않는다.
		// 왜냐하면 지연 속성은 멤버별 초기화와 호환되지 않는 동작을 포함하기 때문이다.
	}
}

상세 설계

문법 변경 사항

이 제안은 두 가지 새로운 문법 요소를 도입한다: memberwise 선언 수식자와 ... 멤버와이즈 파라미터 플레이스홀더다.

지정 이니셜라이저는 memberwise 선언 수식자를 사용해 멤버와이즈 초기화를 선택한다. 이 수식자는 컴파일러가 뒤에서 설명할 절차에 따라 멤버와이즈 파라미터와 이니셜라이저 본문 시작 부분에 멤버와이즈 초기화 코드를 합성하도록 만든다.

개요

이 설계에서 멤버별 초기화 매개변수(memberwise initialization parameter)라는 용어는 컴파일러가 멤버별 초기화 합성(memberwise initialization synthesis)의 일부로 생성한 초기화 매개변수를 가리킨다.

알고리즘

  1. 멤버별 초기화 합성이 가능한 프로퍼티 집합을 결정한다. 프로퍼티가 다음 조건을 모두 만족하면 멤버별 초기화 합성이 가능하다:

    1. 프로퍼티의 접근 수준이 멤버별 초기화 생성자와 동일하거나 더 가시적이어야 한다. var 프로퍼티의 경우 setter의 가시성을 기준으로 한다.
    2. 멤버별 초기화를 금지하는 동작을 포함하지 않아야 한다.
    3. let 프로퍼티인 경우 초기값을 가질 수 없다.
  2. 각 *멤버별 초기화 파라미터*에 대한 기본값을 결정한다. 현재 제안에서는 var 프로퍼티만 기본값을 지정할 수 있으며, 이 값은 해당 프로퍼티의 초기값이 된다.

  3. 초기화 생성자가 멤버별 초기화가 가능한 프로퍼티 이름과 일치하는 외부 라벨을 가진 파라미터를 선언하면 컴파일러 오류를 발생시킨다.

  4. ... 자리 표시자가 지정된 위치에 *멤버별 초기화 파라미터*를 합성한다. 합성된 파라미터는 프로퍼티 이름과 일치하는 외부 라벨을 가져야 한다. 합성된 파라미터는 다음 순서로 배치한다:

    1. 기본값이 없는 모든 파라미터는 기본값이 있는 파라미터보다 앞에 위치한다.
    2. 각 그룹 내에서는 프로퍼티 선언 순서를 따른다.
  5. 초기화 생성자 본문의 시작 부분에서 모든 *멤버별 초기화 파라미터*의 초기화를 합성한다.

  6. 초기화 생성자 본문에서 멤버별 초기화 합성이 적용된 var 프로퍼티에 값을 할당하면 경고를 발생시킨다. 호출자가 제공한 값을 덮어쓰는 것이 의도한 동작일 가능성이 낮기 때문이다.

기존 코드에 미치는 영향

이 제안은 다음과 같은 조건을 충족할 때 클래스와 구조체에 대해 암시적 멤버 초기화자를 생성하는 기능도 지원한다:

  1. 타입이 명시적으로 초기화자를 선언하지 않는다.
  2. 타입이 다음 중 하나에 해당한다:
    1. 구조체
    2. 루트 클래스
    3. 인자가 없는 지정 초기화자를 가진 슈퍼클래스를 상속한 클래스

암시적으로 생성된 멤버 초기화자는 모든 저장 프로퍼티가 멤버 파라미터 합성에 적합할 수 있는 가장 높은 접근 수준을 가지지만, 최대 internal 가시성을 가진다. 현재 이는 타입의 모든 저장 프로퍼티가 최소 internal 가시성을 가진 설정자를 가질 때 internal 가시성을 가지며, 그렇지 않은 경우(하나 이상의 저장 프로퍼티가 private 또는 private(set)인 경우) private 가시성을 가짐을 의미한다.

*암시적*으로 합성된 초기화자는 다음과 같이 *명시적*으로 선언된 초기화자와 동일하다: memberwise init(...) {} 또는 private memberwise init(...) {}.

참고: memberwise 선언 수정자는 지정 초기화자에만 적용되므로 확장에서 정의된 클래스 초기화자와 함께 사용할 수 없다. 그러나 구조체의 모든 저장 프로퍼티가 확장에서 보이는 경우 확장에서 정의된 구조체 초기화자와 함께 사용할 수 있다.

이 제안에서 설명한 변경 사항은 거의 전적으로 추가적인 내용이다. 기존 코드 중에서 문제가 발생하는 경우는 private 저장 프로퍼티를 가진 구조체 또는 private 설정자를 가진 var 프로퍼티가 internal 암시적 멤버 초기화자를 받고 있던 경우뿐이다. 이 영향을 해결하기 위한 옵션은 다음과 같다:

  1. 암시적 멤버 초기화자가 동일한 소스 파일 내에서만 사용되었다면 변경이 필요 없다. 컴파일러는 여전히 암시적 private 멤버 초기화자를 합성할 것이다.
  2. 기계적 마이그레이션을 통해 이전에 암시적이었던 초기화자를 명시적으로 선언하는 코드를 생성할 수 있다. 이는 private 설정자를 가진 저장 프로퍼티를 수동으로 초기화하기 위해 명시적 파라미터를 사용하는 internal 멤버 초기화자가 될 것이다.
  3. “초기화자에 대한 접근 제어” 개선 사항이 채택된다면 private 멤버의 접근 제어를 private internal(init)으로 수정할 수 있다. 이렇게 하면 모든 저장 프로퍼티가 internal 멤버 초기화자에 의해 파라미터 합성이 가능해지므로 암시적 멤버 초기화자가 계속 internal 가시성을 유지할 수 있다.

기존 코드에 미치는 다른 유일한 영향은 초기값을 가진 var 프로퍼티에 해당하는 멤버 파라미터가 이제 기본값을 가지게 된다는 점이다. 이는 암시적 멤버 초기화자의 동작 변경이지만 기존 코드를 깨뜨리지는 않는다. 이 변경은 단순히 새로운 코드가 해당 파라미터에 대한 인자를 제공하지 않고도 초기화자를 사용할 수 있게 해준다.

향후 개선 사항

점진적인 변화를 지향하는 현재 제안은 핵심 기능에 초점을 맞추고 있다. 이 핵심 기능을 추가적인 기능으로 확장할 수 있다. 이러한 개선 사항은 현재 제안이 승인된 후 새로운 제안으로 발전할 가능성이 있다.

현재 제안에서는 let 프로퍼티의 멤버 초기화 파라미터에 기본값을 지정할 수 없다. 이는 아쉬운 제한 사항이며, 이를 해결할 수 있는 방법은 현재 제안에서 매우 기대되는 개선 사항이다.

이 문제를 해결하기 위한 한 가지 방법은 @default 속성을 도입하여 let 프로퍼티가 컴파일러가 멤버 초기화에서 생성하는 파라미터에 대한 기본값을 지정할 수 있게 하는 것이다.

@default는 두 가지 구문적 접근 방식을 고려할 수 있다:

  1. @default를 수식어로 사용한다. 초기값을 지정하는 것과 동일한 구문을 사용하지만, @default 속성이 지정된 프로퍼티의 경우 지정된 값은 초기값이 아닌 기본값이 된다.
  2. 속성 인자를 사용해 기본값을 지정할 수 있게 한다.

각 구문은 장단점이 있다:

  1. 첫 번째 구문은 더 깔끔하고 가독성이 좋다.
  2. 첫 번째 구문은 동일한 프로퍼티에 초기값과 기본값을 모두 지정할 수 없게 한다. 이는 let 프로퍼티가 둘 다 가져서는 안 되며, var 프로퍼티의 초기값은 기본값과 동일하기 때문에 장점으로 작용한다.
  3. 두 번째 구문은 초기값과 기본값을 지정하는 구문이 크게 다르기 때문에 혼동의 여지가 적고 더 명확할 수 있다.

첫 번째 문법 옵션을 사용한 예제

struct S {
	@default let s: String = "hello"
	@default let i: Int = 42

	// 사용자가 선언:
	memberwise init(...) {}
	// 컴파일러가 생성:
	init(s: String = "hello", i: Int = 42) {
		/* 생성된 코드 */ self.s = s
		/* 생성된 코드 */ self.i = i
	}
}

두 번째 문법 옵션을 사용한 예제

struct S {
	@default("hello") let s: String
	@default(42) let i: Int

	// 사용자가 선언:
	memberwise init(...) {}
	// 컴파일러가 생성:
	init(s: String = "hello", i: Int = 42) {
		/* 생성됨 */ self.s = s
		/* 생성됨 */ self.i = i
	}
}

memberwise 프로퍼티

현재 제안하는 규칙은 가능한 한 정확한 프로퍼티 집합에 대해 멤버별 초기화 매개변수를 합성하도록 설계되었다. 하지만 이러한 규칙이 원하는 결과와 일치하지 않는 경우도 있다.

프로퍼티에 memberwise 선언 수식어를 도입하면, 프로그래머가 멤버별 초기화 합성에 참여할 프로퍼티를 정확히 지정할 수 있다. 이를 통해 완전한 제어가 가능해지고, 명시적 선언으로 인해 코드의 명확성이 높아진다.

이 기능이 지원하는 구체적인 사용 사례는 다음과 같다: - private 프로퍼티가 public 초기화 구문에서 합성된 멤버별 매개변수를 받도록 허용 - public 프로퍼티를 매개변수 합성에서 제외

예시를 살펴보자:

struct S {
  // 두 프로퍼티 모두 접근 제어와 상관없이 멤버별 초기화 매개변수를 받음
  memberwise public let s: String
  memberwise private let i: Int

  // 두 프로퍼티 모두 접근 제어와 상관없이 멤버별 초기화 매개변수를 받지 않음
  public var s2 = ""
  private var i2 = 42

  // 사용자가 선언:
  memberwise init(...) {}

  // 컴파일러가 합성:
  init(s: String, i: Int) {
    /* 합성 */ self.s = s
    /* 합성 */ self.i = i
  }
}

초기화 접근 제어

특정 경우에는 자동 모델을 사용할 때 멤버별 초기화에 대해 별도의 접근 제어를 지정할 필요가 있다. 예를 들어, 해당 모델이 거의 원하는 동작을 수행하지만, 특정 프로퍼티의 초기화 가시성을 조정해야만 원하는 결과를 얻을 수 있는 상황이 있다.

이때 사용하는 문법은 세터에 대한 접근 제어를 지정할 때와 동일하다. 이 기능은 더 비공개인 멤버가 더 공개적인 멤버별 초기화에 참여할 수 있도록 허용하는 데 가장 유용할 것이다. 또한 일부 멤버에 대해 멤버별 초기화를 제한하는 데 사용할 수도 있지만, @nomemberwise 제안이 채택된 경우에는 일반적으로 이 사용법을 권장하지 않는다.

struct S {
	private internal(init) let s: String
	private i: Int

	// 사용자가 선언:
	memberwise init(...) {
		i = getTheValueForI()
	}
	// 컴파일러가 합성 (내부 멤버별 초기화임에도 불구하고 비공개 멤버 s에 대한 매개변수 포함):
	init(s: String) {
		/* 합성 */ self.s = s
		
		// 사용자의 초기화 본문 유지
		i = getTheValueForI()
	}
}

이 기능이 추가되면 첫 번째 프로퍼티 자격 규칙이 다음과 같이 업데이트된다:

  1. 프로퍼티의 init 접근 수준이 멤버별 초기화보다 적어도 동일하거나 더 공개적이어야 한다. 프로퍼티에 init 접근 수준이 없는 경우, 세터의 접근 수준이 멤버별 초기화보다 적어도 동일하거나 더 공개적이어야 한다.

@nomemberwise

타입의 작성자가 특정 프로퍼티가 모든 이니셜라이저나 특정 이니셜라이저에서 멤버별 초기화에 참여하지 못하도록 방지하고 싶은 경우가 있다. 프로퍼티와 이니셜라이저에 사용하는 @nomemberwise 속성은 이러한 경우를 지원한다.

멤버별 이니셜라이저는 @nomemberwise(prop1, prop2)와 같이 @nomemberwise 속성에 제공된 프로퍼티 이름 목록에 특정 프로퍼티를 포함시켜 명시적으로 멤버별 초기화를 방지할 수 있다.

자동 모델에서는 프로퍼티가 @nomemberwise 속성을 사용해 명시적으로 멤버별 초기화에서 제외될 수 있다. 이 경우 해당 프로퍼티는 멤버별 초기화 합성에 적합하지 않게 된다. 따라서 직접 초기값을 할당하거나 타입의 모든 이니셜라이저에서 직접 초기화해야 한다.

@nomemberwise 속성은 멤버별 초기화에 참여할 수 있는 프로퍼티를 결정할 때 두 가지 추가 규칙을 도입한다.

  1. 프로퍼티가 @nomemberwise 속성으로 주석 처리되지 않아야 한다.
  2. 프로퍼티가 이니셜라이저에 부착된 @nomemberwise 속성 목록에 포함되지 않아야 한다. super@nomemberwise 속성 목록에 포함된 경우, 어떤 슈퍼클래스 프로퍼티도 멤버별 초기화에 참여하지 않는다.

예제

참고: 이 예제는 실제로 많은 이점을 제공하지 않는다. 멤버별 초기화에서 하나만 제외된 10개의 프로퍼티를 상상해보자.

struct S {
	let s: String
	let i: Int

	// 사용자가 선언:
	@nomemberwise(i)
	memberwise init(...) {
		i = getTheValueForI()
	}
	// 컴파일러가 합성 (@nomemberwise 속성에 언급된 프로퍼티에 대해 멤버별 초기화를 억제):
	init(s: String) {
		/* 합성 */ self.s = s
		
		// 사용자의 초기화 본문 유지
		i = getTheValueForI()
	}
}
struct S {
	let s: String
	@nomemberwise let i: Int

	// 사용자가 선언:
	memberwise init() {
		i = 42
	}
	// 컴파일러가 합성:
	init(s: String) {
		/* 합성 */ self.s = s
		
		// 사용자의 초기화 본문 유지
		i = 42
	}
}

멤버 초기화 체이닝 및 인자 전달

이상적으로는 편의 초기화자와 위임 초기화자를 정의할 때, 멤버 초기화를 위해 지정된 초기화자에 매개변수를 수동으로 선언하고 인자를 전달하지 않아도 되면 좋을 것이다. 또한 지정된 초기화자도 상위 클래스의 멤버 초기화 매개변수에 대해 동일한 작업을 하지 않아도 되는 것이 바람직하다.

인자 전달에 대한 일반적인 해결책이 이 문제를 해결할 수 있다. 이러한 사용 사례와 기타 상황을 지원하기 위한 인자 전달 제안이 추후 논의될 가능성이 높다.

Objective-C 클래스 임포트

대부분의 Swift 개발자에게 Objective-C 프레임워크는 매우 중요하다. Cocoa 프레임워크를 사용하는 Swift 코드에서 유연한 멤버별 초기화의 장점을 제공하기 위해, 앞으로 제안될 프로포절은 Objective-C 프로퍼티와 초기화 메서드에 적용할 수 있는 MEMBERWISE 속성을 도입할 것을 권장할 수 있다.

변경 가능한 Objective-C 프로퍼티는 MEMBERWISE 속성으로 표시할 수 있다. 그러나 읽기 전용 Objective-C 프로퍼티는 MEMBERWISE 속성으로 표시할 수 없다. MEMBERWISE 속성은 클래스의 모든 초기화 메서드에서 기본값으로 초기화되는 프로퍼티에만 사용해야 한다. (호출자가 직접 제공하거나 계산된 값이 아닌)

Objective-C 초기화 메서드도 MEMBERWISE 속성으로 표시할 수 있다. Swift가 이 속성이 표시된 Objective-C 초기화 메서드를 임포트할 때, 호출자가 MEMBERWISE 속성이 표시된 클래스 프로퍼티에 대해 멤버별 값을 제공할 수 있도록 허용할 수 있다. 이러한 초기화 메서드의 호출 지점에서 컴파일러는 인스턴스 초기화가 완료된 직후 제공된 값으로 멤버별 프로퍼티를 설정하는 변환을 수행할 수 있다.

필요한 경우 특정 초기화 메서드에서 특정 프로퍼티의 멤버별 파라미터를 숨길 수 있도록 허용하는 것도 바람직할 수 있다. 예를 들어 NOMEMBERWISE(prop1, prop2)와 같은 방식으로.

Objective-C 클래스의 멤버별 초기화 메커니즘(초기화 후 세터 호출)은 네이티브 Swift의 멤버별 초기화와는 다른 방식으로 구현되어야 한다. 개발자가 Objective-C 타입을 어떻게 주석 처리하는지 주의한다면, 이 구현 차이는 호출자에게 어떤 차이점도 발생시키지 않을 것이다.

Cocoa 클래스의 인스턴스를 초기화할 때 Swift에서 호출 지점 멤버별 초기화 구문을 사용하려면 이 구현 차이가 필요하다. Cocoa 클래스 인스턴스의 멤버를 초기화하기 위한 더 나은 구문에 대한 여러 아이디어가 논의된 적이 있다. 나는 멤버별 초기화가 이를 위한 최선의 방법이라고 생각한다. 이 방법은 초기화 호출에서 인스턴스의 전체 구성을 허용하기 때문이다.

당연히 Cocoa 클래스에서 멤버별 초기화를 지원하려면 Apple이 적절한 위치에 MEMBERWISE 속성을 추가해야 한다. 이 작업이 이루어지지 않으면 Objective-C 클래스 임포트 프로비전에 대한 제안은 상당히 가치가 떨어질 것이다. 따라서 이 제안이 제출되면 Objective-C 임포트 제안도 작성하고 제출할 것을 권장한다. 단, Apple이 자신들의 프레임워크에 필요한 주석을 추가할 것이라고 코어 팀이 확신할 때까지는 제출하지 말아야 한다.

고려한 대안들

저장 프로퍼티가 멤버 초기화에 참여하려면 명시적으로 선택해야 함

이 옵션은 합리적이며, 어떤 기본값이 더 나은지에 대해 건강한 논의가 있을 것으로 예상한다. 기본적으로 자동 모델을 채택하기로 결정한 데에는 몇 가지 이유가 있다:

  1. 구조체의 멤버 초기화는 현재 프로퍼티가 참여하기 위해 어노테이션을 요구하지 않는다. 해당 메커니즘을 대체하도록 설계된 메커니즘에 어노테이션을 요구하는 것은 보일러플레이트로 간주될 수 있다.
  2. 공개 가시성을 가진 저장 프로퍼티는 종종 호출자가 제공한 값으로 직접 초기화된다.
  3. 멤버 초기화보다 낮은 가시성을 가진 저장 프로퍼티는 멤버 초기화에 참여할 수 없다. 이를 나타내기 위해 어노테이션이 필요하지 않으며, 일반적으로 이를 원하지 않는다.
  4. 자동 모델은 기본값이 아니면 존재할 수 없다. 선택적 모델은 자동 모델과 공존할 수 있으며, 하나 이상의 프로퍼티에 memberwise 선언 수정자를 지정함으로써 선택적으로 활성화할 수 있다.

저장 프로퍼티가 멤버 초기화에 참여하려면 memberwise 선언 수정자를 명시적으로 요구하는 것이 더 명확할 수 있다는 강력한 주장이 가능하다고 생각한다.

모든 초기화 메서드가 멤버별 초기화에 참여하도록 허용

이 옵션은 심각하게 고려되지 않았다. 기존 코드에 영향을 미칠 수 있으며, 컴파일러가 추가 매개변수를 합성하고 초기화 메서드 본문에서 저장 프로퍼티를 추가로 초기화할 것이라는 명확한 표시를 제공하지 않기 때문이다.

멤버별 초기화에서 제외하려면 초기화자를 요구한다

이 옵션도 심각하게 고려되지 않았다. 모든 초기화자가 멤버별 초기화에 참여할 수 있도록 허용하는 것과 동일한 문제점을 가지고 있다.

초기화자보다 낮은 접근 수준의 프로퍼티에 대해 파라미터 합성을 허용하는 것에 대한 고려

초기화자를 호출하는 쪽에서 직접 볼 수 없는 프로퍼티에 대해 파라미터 합성을 허용하는 것을 고려했다. 접근 제어자와 직접적인 충돌은 없으며, 수동으로도 이런 코드를 작성할 수 있다. 하지만 이 방식이 대부분의 경우 적절하지 않을 가능성이 높다고 판단해 이 접근 방식을 채택하지 않기로 했다. 적절한 경우라면 개발자가 수동으로 이 코드를 작성하도록 요구하는 것이 더 나은 선택이라고 생각한다.

초기화자와 적어도 동일한 접근 수준을 가진 멤버에만 멤버별 초기화 파라미터 합성을 제한하는 이유는 다음과 같다:

  1. 직관적으로 논리적이다. 멤버별 초기화가 비공개 상태를 노출하는 것은 이상하거나 예상치 못한 동작이다.
  2. 기본적으로 더 안전하다. 실수로 원하지 않는 내용을 멤버별 초기화를 통해 노출하는 것을 방지할 수 있다.
  3. 초기화자를 작성하는 개발자의 의도와 더 부합할 가능성이 높다. 호출자가 멤버를 볼 수 없다면, 해당 멤버를 초기화하도록 허용하는 것은 의미가 없을 것이다.
  4. 기본적으로 더 많은 비공개 멤버를 노출하면, 현재 제안하는 방식에서 멤버별 초기화는 많은 경우 쓸모없게 된다. 더 비공개인 멤버에 대한 파라미터 합성을 방지할 방법이 없기 때문이다. 호출자가 내부 상태를 초기화하도록 허용하거나, 멤버별 초기화의 이점을 포기하는 것 사이에서 선택해야 한다.
  5. @nomemberwise 제안이 채택되면, 원하는 멤버에 대한 파라미터 합성을 방지할 수 있다. 하지만 @nomemberwise를 더 비공개인 멤버에 대한 파라미터 합성을 방지하기 위해 더 자주 사용해야 한다는 단점이 있다. 대부분의 경우 @nomemberwise가 필요하지 않은 것이 더 바람직하다.
  6. 호출자가 더 비공개인 멤버에 대한 멤버별 인자를 직접 제공해야 한다면, 동일하거나 더 낮은 접근 수준의 멤버에 대해 멤버별 초기화를 활용하면서도 이를 허용할 수 있다. 더 비공개인 멤버에 대해 명시적으로 파라미터를 선언하고, 본문에서 수동으로 초기화하는 memberwise init을 선언하면 된다. “초기화자 접근 제어” 개선안이 채택되면, 다른 옵션으로는 getter와 setter의 접근 수준을 유지하면서 초기화자의 가시성을 높이는 방법이 있다. 프로그래머가 초기화자 접근 제어를 통해 명시적으로 더 비공개인 멤버를 노출하거나, 직접 코드를 작성하도록 요구하는 것은 매우 바람직한 일이다.

초기화자보다 낮은 접근 수준의 멤버에 대해 멤버별 파라미터 합성을 허용할 수 있는 이유는 다음과 같다:

  1. 이를 허용하지 않으면 저장 프로퍼티의 접근 제어와 멤버별 초기화 사이에 긴장이 생긴다. 더 좁은 접근 제어를 선택하거나, 멤버별 초기화의 이점을 얻는 것 사이에서 선택해야 한다. 다른 말로 표현하면, 이 설계는 좁은 접근 제어가 보일러플레이트 코드를 유발한다는 것을 의미한다.

참고: 위에서 언급한 긴장은 위의 #6에 의해 완화된다. 동일하거나 더 낮은 접근 수준의 멤버에 대해 멤버별 초기화를 사용할 수 있으며, 더 비공개인 멤버를 더 공개된 초기화자에 명시적으로 노출하도록 요구하는 것은 바람직한 일이다.

멤버 초기화 파라미터를 명시적으로 지정하도록 요구하기

파라미터와 동일한 이름의 프로퍼티 초기화를 위한 헬퍼“라는 스레드에서, 이니셜라이저 본문에서 프로퍼티 초기화를 자동으로 합성하면서도 파라미터를 명시적으로 선언해야 한다는 아이디어를 논의했다.

struct Foo {
    let bar: String
    let bas: Int
    let baz: Double
    init(self.bar: String, self.bas: Int, bax: Int) {
		  // self.bar = bar는 컴파일러에 의해 합성됨
		  // self.bas = bas는 컴파일러에 의해 합성됨
        self.baz = Double(bax)
    }
}

이 방식의 단점은 동기 부여 섹션에서 언급된 M x N 스케일링 문제를 해결하지 못한다는 점이다. 수동 초기화 문장은 생략되지만, 보일러플레이트 파라미터 선언은 여전히 MxN(프로퍼티 수 x 이니셜라이저 수) 비율로 증가한다. 또한 멤버 초기화 파라미터 전달 문제도 해결하지 못해, 편의 이니셜라이저나 위임 이니셜라이저에는 쓸모가 없다.

이 방식을 지지하는 사람들은 현재 제안보다 더 명확하고 제어 가능한 방법이라고 믿는다.

현재 제안에서도 완전한 제어는 여전히 가능하다. 이니셜라이저가 멤버 초기화를 선택적으로 사용하도록 요구한다. 완전한 제어가 필요한 경우, 이니셜라이저는 멤버 초기화 합성을 선택하지 않으면 된다. 리스트에 있는 예제에서 절약되는 보일러플레이트는 상대적으로 미미하며, 초기화를 완전히 제어해야 하는 상황에서는 감수할 만하다.

나는 이니셜라이저에 memberwise 선언 수식어와 파라미터 목록의 플레이스홀더가 컴파일러가 추가 파라미터를 합성할 것임을 명확히 한다고 생각한다. 또한, IDE와 생성된 문서에는 이니셜라이저의 완전한 합성된 시그니처가 포함될 것이다.

마지막으로, 이 아이디어는 현재 제안과 상호 배타적이지 않다. 멤버 이니셜라이저 선언에서도 작동할 수 있으며, 해당 프로퍼티가 멤버 초기화 합성에 적합하지 않도록 만들기만 하면 된다.

Kotlin과 Scala의 “타입 파라미터 리스트” 구문 채택

메일링 리스트 스레드에서 여러 의견이 Kotlin과 Scala의 구문을 사용하는 것을 제안했다. 예를 들어 다음과 같은 형태다:

struct Rect(var origin: Point = Point(), var size: Size = Size()) {}

이는 다음과 같이 확장될 것이다:

struct Rect {}
  // 여기에 초기값이 포함될지 여부는 명확하지 않다.
  // 메일링 리스트 제안에서는 이를 포함하지 않았다.
  var origin: Point // = Point()
  var size: Size // = Size()
  init(origin: Point = Point(), size: Size = Size()) {
    self.origin = origin
    self.size = size
  }

이 접근 방식은 제안의 목표인 멤버 초기화에 대한 유연하고 확장 가능한 솔루션 제공과 호환되지 않아 채택되지 않았다. 구체적인 이유는 다음과 같다:

  1. 이 제안은 부분적인 멤버 초기화를 지원한다.
    초기화자는 비멤버 파라미터를 받을 수 있고, 수동으로 private 상태를 초기화할 수 있으며, 여전히 호출자가 멤버 초기화를 통해 public 프로퍼티를 직접 초기화할 수 있다.

  2. 이 제안은 여러 멤버 초기화자를 지원한다.
    private 상태를 초기화하는 데 여러 방법이 필요할 수 있으며, 여전히 public 프로퍼티를 직접 초기화할 수 있는 멤버 초기화를 원할 수 있다.

  3. 이 제안은 프로퍼티 선언을 유연하게 구성할 수 있도록 지원한다.
    Scala / Kotlin 구문은 매우 간단한 경우에는 적합할 수 있지만, 타입 선언의 시작 부분에 프로퍼티 선언을 단일 리스트로 배치해야 한다는 제약이 있다. 이는 매우 제한적이다.
    특히, 제네릭 파라미터와 상속 절을 포함하는 타입의 경우, 프로퍼티 선언이 이 두 절 사이에 끼어들어 타입 수준 정보와 멤버 수준 정보가 혼재되기 때문에 더욱 문제가 된다.

이 제안은 “타입 파라미터 리스트” 구문을 지원하는 것과 상호 배타적이지 않다. 이들은 서로 다른 문제를 해결하기 위한 것이며, 동시에 존재할 수 있다. 향후 제안에서 유사한 구문을 도입할 수 있다. 그러한 제안의 한 가지 옵션은 프로퍼티 선언으로의 간단한 확장을 제공하는 것이며, 현재 제안이 초기화자 합성을 주도하도록 할 수 있다(let 프로퍼티의 기본값 문제가 그때쯤 해결된다고 가정).

공정성을 위해 Scala / Kotlin 구문의 장점을 다시 언급한다:

  1. let 프로퍼티에 해당하는 파라미터의 기본값을 지원할 수 있다.
  2. 파라미터 레이블을 지정할 수 있다.
  3. 일부 경우에 현재 제안보다 간결하다.

이러한 점에 대한 응답은 다음과 같다:

  1. 이 구문의 확장이 합성된 프로퍼티에 초기값을 제공하지 않고, 합성된 초기화자의 파라미터에만 기본값을 사용한다면 이는 사실이다. 이렇게 할 때의 단점은 var 프로퍼티가 더 이상 초기값을 가지지 않는다는 점이다. 이는 타입에 추가 초기화자를 작성할 때 바람직할 수 있다.
    let 프로퍼티의 기본값에 대한 논의를 계속 이어가야 한다고 믿는다. 이상적으로는 현재 제안과 향후 추가할 수 있는 구문 설탕 모두와 호환되는 해결책을 찾을 수 있을 것이다.

  2. 멤버 초기화 파라미터에 대한 파라미터 레이블을 허용하는 것이 좋은 아이디어라고 생각하지 않는다. 호출자가 프로퍼티를 직접 초기화하는 데는 프로퍼티 이름과 일치하는 레이블이 가장 적합하다. 다른 이름을 제공해야 한다면 수동으로 초기화자를 작성하면 된다. 현재 제안의 향후 개선을 통해 커스텀 레이블이 필요하지 않은 프로퍼티에는 멤버 초기화를 사용하고, 필요한 프로퍼티는 수동으로 초기화할 수 있을 것이다.

  3. Scala / Kotlin 구문은 일부 경우에 더 간결하지만, 모든 경우에 그런 것은 아니다. 이 제안에서는 위의 예제가 오히려 더 간결하다:

    struct Rect { var origin: Point = Point(), size: Size = Size() }

vs

struct Rect(var origin: Point = Point(), var size: Size = Size()) {}