글을 비판적으로 읽어주세요! 잘못된 부분이 있을수 있습니다.
개요
글은 왜 UIButton과 같은 Component들이 function 과 매칭되기 위해 @objc키워드를 붙여야 하는지에 관해 궁금해서 글을 작성했습니다.
swift관점에서 @objc를 쓰는 이유
Swift관섬에서 @objc를 쓰는 이유는, UIButton과 같은 UIKit컴포넌트들과 다양한 Class들이 Objective-C 기반이기 때문입니다. Class나 method를 적용할 떄 Objective-C에서 운용 가능하게 만듭니다. Swift는 정적으로 타입을 확인하고 매서드를 호출하지만 Ojective-C에서는 런타임에 접근합니다.
Runtime Introspection vs Reflection이 뭐예요?
Objective-C에서 Introspection과 Reflection은 모두 런타임(Runtime) 단계에서 객체와 클래스 정보를 탐색하거나 조작하는 기능과 관련이 있습니다. 이 둘은 종종 혼용되지만, 엄연히 다른 역할을 합니다.
Introspection
Introspection은 객체나 클래스의 타입(type), 구조(structure), 특성(characteristics)을 확인하는 작업입니다.
쉽게 말해, 객체가 어떤 클래스인지, 특정 메서드나 프로퍼티를 가지고 있는지 등을 알아보는 과정입니다.
Reflection
Reflection은 객체나 클래스의 정보를 확인(Introspection 포함)하는 데 그치지 않고, 동적으로 조작하거나 변경할 수 있는 기능입니다.
Objective-C에서 Runtime Dispatch에 관하여
Objectvie-C에서 Runtime시 Dispatch Method테이블을 만듭니다. 그리고 DispatchTable에 할당된 함수를 실행시킵니다. 상기 @objc를 붙인 이유는 runtime시 적절한 함수를 Dispatch 테이블에서 찾기 위함입니다. Objective-C 런타임은 메서드 호출을 위해 클래스 메타정보에 접근하여 Selector를 기반으로 메서드를 찾습니다. 이 메커니즘은 동적 연결이 가능한데, Swift 메서드가 @objc로 선언되지 않으면 이 메타정보에 포함되지 않아 Selector를 통해 호출할 수 없습니다.
Method Swizzling의 기본 원리
Objective-C의 메서드는 메서드 디스패치 테이블(Method Dispatch Table)을 통해 호출됩니다. 메서드 디스패치 테이블은 클래스와 메서드 이름을 키로 하여 C 함수 포인터(IMP, Implementation Pointer)를 값으로 갖는 구조입니다.
Method Swizzling은 이 테이블에서 특정 메서드의 구현(IMP)을 다른 함수로 교체하여, 메서드 호출 시 새로 지정한 함수가 호출되도록 합니다.
그러면 Swift에서 다른 클래스의 함수를 실행할 수 있을까?
넵 가능합니다. 이부분은 GPT를 동룸을 받아 작성되었습니다.
실제 swift에서 제공하는 unsafeBitCast로 내부에 있는 class값을 조종할 수 있었습니다. property도 kvo 로 저장된 class_getInstanceVariable로 끌고 올 수 있었습니다.
import Foundation
class MyClass {
private let privateUUID: String = UUID().uuidString
private let name: String
private static var staticProperty: String = "Initial Static Value"
init(name: String) {
self.name = name
}
}
private func reflectAndModifyUsingPointer(instance: AnyObject) {
let mirror = Mirror(reflecting: instance)
print("Before modification:")
for child in mirror.children {
if let propertyName = child.label {
print("\(propertyName): \(child.value)")
}
}
// UnsafeMutablePointer를 사용해 런타임 "name" 속성 값 변경
if let nameOffset = class_getInstanceVariable(type(of: instance), "name") { // ✅ 포인터 접근
let pointer = unsafeBitCast(instance, to: UnsafeMutableRawPointer.self)
let offsetPointer = pointer.advanced(by: ivar_getOffset(nameOffset))
offsetPointer.assumingMemoryBound(to: String.self).pointee = "Modified Value"
}
// UnsafeMutablePointer를 사용해 런타임 "privateUUID" 속성 값 변경
if let nameOffset = class_getInstanceVariable(type(of: instance), "privateUUID") { // ✅ 포인터 접근
let pointer = unsafeBitCast(instance, to: UnsafeMutableRawPointer.self)
let offsetPointer = pointer.advanced(by: ivar_getOffset(nameOffset))
offsetPointer.assumingMemoryBound(to: String.self).pointee = "Modified Value + \(UUID().uuidString)"
}
print("\nAfter modification:")
for child in mirror.children {
if let propertyName = child.label {
print("\(propertyName): \(child.value)")
}
}
}
// 실제 실행 부분
let myClass = MyClass(name: "initialized by MyViewController at viewDidAppear")
reflectAndModifyUsingPointer(instance: myClass)
실세 화면은 다음과 같습니다. PrivateUUID, name 모두 다른 private속성이지만 reflaction을 통해서 수정된 것을 볼 수 있었습니다.
그러면, 다른 클래스에 있는 Action도 수행 가능한가? Introspection을 활용하기 위해 다른 ViewController에서 정의된 Action을 수행할 수도 있습니다.
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
view.addSubview(myButton)
myButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true
myButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true
myButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 50).isActive = true
myButton.heightAnchor.constraint(equalToConstant: 100).isActive = true
myButton.addTarget(self, action: #selector(handleButtonTap), for: .touchUpInside)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let myClass = MyClass(name: "initialized by MyViewController at viewDidAppear")
reflectAndModifyUsingPointer(instance: myClass)
}
// MARK: - Action
@objc func handleButtonTap() {
print("1️⃣ \(Self.self)")
print("Button tapped! Now invoking OtherViewController via Reflection.")
// MyViewController에서 호출
let className = "\(Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "").OtherViewController"
let targetClass = NSClassFromString(className) as! UIViewController.Type
// Step 2: Instantiate OtherViewController
let targetInstance = targetClass.init()
// Step 3: Set a dynamic property on OtherViewController
targetInstance.setValue("Reflection works!", forKey: "data")
// Step 4: Dynamically call a method on OtherViewController
let selectorName = "dynamicMethod:"
let selector = NSSelectorFromString(selectorName)
if targetInstance.responds(to: selector) {
targetInstance.perform(selector, with: "Hello from MyViewController!")
} else {
print("Error: Selector '\(selectorName)' not found in \(className).")
}
}
}
@objcMembers class OtherViewController: UIViewController {
// MARK: - Properties
var data: String = "Default Data"
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
}
// MARK: - Dynamic Method
@objc private func dynamicMethod(_ argument: String) {
print("2️⃣ \(Self.self)")
print("dynamicMethod called with argument: \(argument)")
print("Current data property: \(data)")
}
}
Button을 눌렀을 경우 OtherViewController에 private으로 지정된 함수가 Linking되어 실행되는 것을 볼 수 있습니다.
글 작성 후기
일단 너무 재미있었습니다. 단순하게 작성하는 코드에도 다양한 것들이 숨겨져 있어서 공부하는 재미를 느꼈습니다. CS에서 배운 Class가 Memory에 적재된다는 개념을 실제로 확인할 수 있었던 것 같습니다.
글은 왜 @objc키워드를 붙일까에 대한 단순한 질문에서 시작했지만 다양한 것을 통찰할 수 있었습니다. reflection이나 introsepction과 같은 개념들을 알아 볼 수 있었습니다. 다른 언어에서 활용하는 측면과 swift에서 활용하는 측면 등 언어의 패러다임에 대해서 조금 인사이트를 얻을 수 있었습니다.
그리고 Swift가 안정성 지향한 언어라고 다들 말을 많이 했지만, 그 이유에 대해서 아무도 알려주지 않았습니다. C계열은 MMM(Manual Memory Management)를 하고 Null 발생해도 프로그램이 죽지 않나? 라는 단순한 생각도 하였지만, 이는 정적으로 컴파일하는 Swift의 특성임을 알아챘습니다. Objective-C에서는 Runtime에서 Dispatch를 위한 기법들이 많은 반면, Swift에서는 정적 컴파일된 코드블럭을 자주 활용하는구나 라는 인사이트를 얻었습니다.
그래서 "Reflection과 Introspection을 쓸까에 관한 물음"은 "정확하게 대답하기 어렵다."로 결론지었습니다. 나중에 private 속성을 바꿔야만 하는 일이 생길 수도 있고, 다른 함수의 private functgion을 활용하는 일이 생길 수 도 있습니다. 특히 Test환경에서 이는 빛을 바랄 것으로 생각됩니다. 만약 테스트를 BlackBox하다가 WhiteBox로 전환하기 위해 Property를 public으로 바꿀 수도 없고 등등 다양한 환경에서 유연하게 쓸 수 있을 것이라 생각합니다.
그래도 메모리를 직접적으로 건드리는 행위는 안정성을 떨어뜨립니다. 실제 코드를 작성하면서도 프로세스가 강제 종료되거나 FatalError가 발생하여 죽는 경우를 겪었습니다. 정말 잘 알고 쓰고, 앱 개발에서는 최대한 지양하는 것이 좋을 것으로 생각됩니다.
작성 코드는 아래 링크를 통해 확인할 수 있습니다.
https://github.com/MaraMincho/MakingFrogWithoutDissecting/tree/main/TestIntrospectionAndReflection
MakingFrogWithoutDissecting/TestIntrospectionAndReflection at main · MaraMincho/MakingFrogWithoutDissecting
개구리를 해부하지 않고 직접 만들기, 공부 레포. Contribute to MaraMincho/MakingFrogWithoutDissecting development by creating an account on GitHub.
github.com
레퍼런스
https://www.hackingwithswift.com/example-code/language/what-is-the-objc-attribute
https://stackoverflow.com/questions/30795117/when-to-use-objc-in-swift
https://www.baeldung.com/cs/oop-introspection-reflection-difference
'Swift' 카테고리의 다른 글
[Swift] escaping vs non-escaping 차이점에 대해서 (0) | 2024.12.27 |
---|---|
Approach to load testing iOS app (stackoverflow 질문글 한국어 해석) (2) | 2024.12.03 |
[iOS] 모듈화 하기 vs 그냥 살기. (주관적으로 느낀 모듈화 장점 3가지) (0) | 2024.11.22 |
[iOS] SwiftData를 활용한 간단한 Todo 어플리케이션 만들어 보기 (0) | 2024.06.02 |
[iOS] Weak Dictionary 다이브 (1) | 2024.02.16 |