@dynamicMemberLookup
👋 들어가기 전

오랜만에 wow느낌을 받은 attribute를 하나 학습해보려한다.
바로 본론으로 들어가보자.
🏁 학습할 내용
- 불편함
- @dynamicMemberLookup
- 역할
- 원리
- 실습
- 주의할 점
💢 나만 불편해?
다들 이런 경험이 있을거다.
아래 두 코드를 보면, 둘다 접근하고 싶은데이터가, 어떤 객체안에 private 형태로 존재할 때
우리는 울며 겨자먹기로, public으로 풀거나, 별도 함수나 property를 만들어서 해결에 왔다.
그게 직관적이고 좋은 방법인건 사실이다.
struct JSON {
private var data: [String: Any] = [:]
}
let json = JSON()
// ✅ public으로 바꿔야함
print(json.data["name"])
print(json.data["age"])
struct Outer {
struct Inner {
var name: String
}
private let inner: Inner
init(name: String) {
self.inner = Inner(name: name)
}
}
let outer = Outer(name: "1234")
// ✅ public으로 바꿔야함
print(outer.inner.name)
하지만 오늘은 이런 문제를 보다 쉽게 해결할 수 있는 한가지 방법론을 배워보려고한다.
무조건 사용해라, 무조건 사용하지말라는 절대 아니다.
반드시 이 기능을 도입했을 때, 이득이 많다고 생가하면 사용하면된다.
🌀 @dynamicMemberLookup

✅ 역할
@dynamicMemberLookup는 타입에 선언하면, 점(.) 문법으로 접근하는 멤버 호출을 컴파일 타임에 정적으로 확인하지 않고, 런타임 시 특정 메서드로 위임
🤫 동적으로 접근하면 어떤 장점이 있을까??
- 미리 정의되지 않은 프로퍼티 접근을 가능하게 함
- 객체에 동적으로 속성을 추가하거나 접근하고 싶을 때 사용
🎯 사용 가능한 대상
- class
- struct
- enum
- protocol
🔧 원리
가장 중요한 핵심은 subscript다.
가장 대표적인 예가, 배열과 딕셔너리다.
간단히 요약하면, [] 연산자를 이용해서 값에 접근할 수 있다.

공식문서를 살펴보면, 하나부터 열까지다. subscript와 관련됐다.
즉, @dynamicMemberLookup를 사용할면 subscript 연산자를 필수로 만들어야한다.

📋 실습
처음 코드에 한번 적용해보자.
먼저 attribute만 바로 붙혀보자.
🚨 필수 메서드 정의 오류

바로 오류발생 , 오류를 해석해보자.
- subscript(dynamicMeber:) 메서드를 정의해라
- 그 매개변수는 ExpressibleByStringLiteral 타입 또는 KeyPath로 받아아햔다.
다소 낯선 개념을 먼저 정리해보자.
ExpressibleByStringLiteral


ExpressibleByXXXLiteral 형식의 프로토콜은 결국, XXX엥 들어가는 타입으로 초기화가 가능케 하라는 프로토콜이다.
우리는 XXX가 String이므로, String으로 초기화가 가능하냐는 의미
struct Name: ExpressibleByStringLiteral {
let value: String
init(stringLiteral value: String) {
self.value = value
}
}
let myName: Name = "나야 스트링" // ✅ 스트링으로 최고하 가능!
print(myName.value) // "나야 스트링"
KeyPath

KeyPath는 \.name , \.age 같이 특정 root 타입으로 부터 특정 속성에 접근하는 경로를 표현하는 객체
struct Person {
var name: String
var age: Int
}
let keyPath = \Person.name // 🔑 키패스
let person = Person(name: "키패스", age: 27)
let name = person[keyPath: keyPath] // "키패스"
자 다시 본론으로 돌아와보면, subscript 연산자를 정의할 때, 그 매개변수
String으로 초기화가 가능하거나, KeyPath로 선언해야한다.
⭐️ subscript 정의
1. dynamicMember 이용
@dynamicMemberLookup
struct JSON {
private var data: [String: String] = [:]
subscript(dynamicMember member: String) -> String? {
get { data[member] }
set { data[member] = newValue }
}
}
var json = JSON()
json.name = "헬로"
json.address = "한국"
print(json.name) //✅ 헬로
print(json.address) //✅ 한국
2. Keypath 이용

똑같이 keyPath 방식으로 바꾸니 , set에서 문제가 됐다.
이때 KeyPath -> WritableKeyPath로 바꾸면 set도 가능하다.
@dynamicMemberLookup
struct Outer {
struct Inner {
var name: String
}
private var inner: Inner
init(name: String) {
self.inner = Inner(name: name)
}
// ReadOnly로 쓸 때
subscript<T>(dynamicMember keyPath: KeyPath<Inner, T>) -> T {
inner[keyPath: keyPath]
}
// write도 해야할 때
subscript<T>(dynamicMember keyPath: WritableKeyPath<Inner, T>) -> T {
get { inner[keyPath: keyPath] }
set { inner[keyPath: keyPath] = newValue }
}
}
var outer = Outer(name: "1234")
outer.name = "QWER"
print(outer.name) //✅ QWER
⚠️ 주의할 점
핵심은 컴파일 타임에 없는 멤버여도, 에러가 나지 않아 ,휴먼 에러를 컴파일러가 잡아줄 수 없음
- 타입 안전성이 약해짐 → 컴파일 타임 에러가 런타임 에러로 바뀔 수 있음
- 오타나 잘못된 키를 사용해도 컴파일러가 경고하지 않음
- 너무 남용하면 코드 가독성과 유지보수가 어려워질 수 있음
😀 소감 및 마무리
정말 위험한 만큼, 편한 기능인 것 같다.
생각보다 계층구조가 복잡하고 심지어 접근 제한자가 private임에도 불구하고
가장 바깥에서 .(dot)을 통해 접근이 가능하다.
만약에 실제로 쓰게된다면, 코드 한줄마다, 런타임으로 전환해서 에러갈 발생하는 지, 파악해야할 듯..
출처
https://developer.apple.com/documentation/foundation/attributedynamiclookup
AttributeDynamicLookup | Apple Developer Documentation
A type to support dynamic member lookup of attributes and containers.
developer.apple.com
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0195-dynamic-member-lookup.md
swift-evolution/proposals/0195-dynamic-member-lookup.md at main · swiftlang/swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution
github.com