
안녕하세요. 개발하면서 자주 보는 NSObject, 프로토콜 만들 때 넣어야 했던 그것!
단순히 "상속받아야 하는 것" 정도로만 알고 있었는데, 오늘 제대로 파헤쳐봅니다.
NSObject는 Objective-C의 모든 클래스가 상속받는 최상위 클래스(Root Class)로, 객체의 기본 동작과 런타임 시스템과의 인터페이스를 제공하는 기반 클래스입니다.
NSObject는 세 가지 핵심 문제를 해결하기 위해 만들어졌습니다.
NSObject ← 모든 객체의 시작점
↓
UIResponder
↓
UIView
↓
UILabel
런타임 시스템이란?
| 시점 | 설명 | 예시 |
|---|---|---|
| 컴파일 타임 | 코드 작성 시점에 결정 | let number: Int = 5 (타입이 확정) |
| 런타임 | 프로그램 실행 중 결정 | obj.perform(selector) (실행 시 메서드 결정) |
Objective-C의 특징:
모든 객체가 공통으로 필요한 기능:
alloc(메모리 할당), init(초기화), dealloc(해제)isEqual:(같은지 비교), hash(해시값)description(객체 설명 문자열)💡 디버깅 시 본
alloc의 정체
메모리를 할당한다는 뜻! NSObject가 제공하는 기본 메서드였습니다.
Apple 공식 문서를 보면 엄청나게 많은 기능이 있습니다. 주요 기능을 카테고리별로 정리해보면:
// Objective-C 시절
MyClass *obj = [[MyClass alloc] init]; // 생성 및 초기화
[obj release]; // 해제 (ARC 이전)
// Swift/Modern Objective-C (ARC)
let obj = MyClass() // alloc + init이 자동으로 처리됨
// 자동으로 메모리 해제
표준화의 의미:
class Person: NSObject {
let name: String
let age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
// description 오버라이드
override var description: String {
return "Person(name: \(name), age: \(age))"
}
}
let person = Person(name: "철수", age: 25)
print(person) // Person(name: 철수, age: 25)
class Product: NSObject {
let id: Int
let name: String
init(id: Int, name: String) {
self.id = id
self.name = name
}
// isEqual 오버라이드
override func isEqual(_ object: Any?) -> Bool {
guard let other = object as? Product else { return false }
return self.id == other.id
}
// hash 오버라이드 (Set, Dictionary에서 사용)
override var hash: Int {
return id.hashValue
}
}
let product1 = Product(id: 1, name: "iPhone")
let product2 = Product(id: 1, name: "iPhone")
let product3 = Product(id: 2, name: "iPad")
print(product1.isEqual(product2)) // true (id가 같음)
print(product1.isEqual(product3)) // false (id가 다름)
// Set에서 사용 (hash 메서드 활용)
let products: Set<Product> = [product1, product2] // 중복 제거됨
class Animal: NSObject {}
class Dog: Animal {}
class Cat: Animal {}
let pet: Animal = Dog()
// 런타임에 타입 확인
if pet.isKind(of: Dog.self) {
print("강아지입니다!") // 출력됨
}
if pet.isKind(of: Animal.self) {
print("동물입니다!") // 출력됨 (상위 클래스도 true)
}
if pet.isKind(of: Cat.self) {
print("고양이입니다!") // 출력 안됨
}
NSObject가 지원하는 동적(Dynamic) 기능들을 실제 예제로 이해해봅시다.
컴파일 타임 vs 런타임 메서드 호출:
// 일반적인 Swift (컴파일 타임에 결정)
class Calculator {
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
}
let calc = Calculator()
calc.add(5, 3) // 컴파일 시 add 메서드 확정
// Objective-C 스타일 (런타임에 결정)
class DynamicCalculator: NSObject {
@objc func add(_ a: NSNumber, _ b: NSNumber) -> NSNumber {
return NSNumber(value: a.intValue + b.intValue)
}
@objc func subtract(_ a: NSNumber, _ b: NSNumber) -> NSNumber {
return NSNumber(value: a.intValue - b.intValue)
}
}
let dCalc = DynamicCalculator()
let selector = #selector(DynamicCalculator.add(_:_:))
// 런타임에 메서드가 있는지 확인
if dCalc.responds(to: selector) {
// NSNumber로 감싸서 전달
let result = dCalc.perform(selector, with: NSNumber(value: 5), with: NSNumber(value: 3))
// 결과도 NSNumber로 반환됨
if let number = result?.takeUnretainedValue() as? NSNumber {
print("결과: \(number.intValue)") // 결과: 8
}
}
실전 사용 사례:
// UIKit의 타겟-액션 패턴
button.addTarget(self,
action: #selector(buttonTapped),
for: .touchUpInside)
// 이 selector는 런타임에 해석됨!
@objc func buttonTapped() {
print("버튼 클릭!")
}
속성 이름(문자열)으로 값에 접근:
class User: NSObject {
@objc var username: String = ""
@objc var age: Int = 0
}
let user = User()
// 일반적인 방법
user.username = "철수"
// KVC 방법 (문자열로 접근)
user.setValue("영희", forKey: "username")
let name = user.value(forKey: "username") as? String
print(name) // Optional("영희")
// 런타임에 속성 이름이 결정되는 경우 유용
let propertyName = "age" // 외부에서 받아온 값이라고 가정
user.setValue(25, forKey: propertyName)
속성 변화를 감지:
class Account: NSObject {
@objc dynamic var balance: Double = 0.0
}
class Observer: NSObject {
private var observation: NSKeyValueObservation?
func startObserving(account: Account) {
// balance 변화 감지
observation = account.observe(\.balance, options: [.new, .old]) { account, change in
print("잔액 변화!")
print("이전: \(change.oldValue ?? 0)")
print("현재: \(change.newValue ?? 0)")
}
}
}
let account = Account()
let observer = Observer()
observer.startObserving(account: account)
account.balance = 1000
// 출력:
// 잔액 변화!
// 이전: 0.0
// 현재: 1000.0
NSObject가 필요한 경우:
1. Objective-C와의 상호운용 (@objc 사용)
2. KVO 사용 (@objc dynamic 필요)
3. Selector 기반 타겟-액션 (UIKit의 버튼 등)
4. NSCoding으로 직렬화
5. 특정 프로토콜 요구 (UITableViewDataSource 등)
NSObject가 불필요한 경우:
// 순수 Swift 코드
struct User { // struct로도 충분!
let name: String
let age: Int
}
class ViewModel { // NSObject 상속 불필요
var data: [String] = []
func fetchData() {
// 일반적인 로직
}
}
| Objective-C/NSObject | Swift 대안 |
|---|---|
| KVO | Combine의 @Published, Property Observer |
| NSCoding | Codable 프로토콜 |
isEqual | Equatable 프로토콜 |
description | CustomStringConvertible |
| Target-Action | 클로저 기반 API |
Modern Swift 예제:
// Codable로 간단한 직렬화
struct User: Codable, Equatable {
let name: String
let age: Int
}
let user = User(name: "철수", age: 25)
let jsonData = try? JSONEncoder().encode(user)
let decoded = try? JSONDecoder().decode(User.self, from: jsonData!)
// Equatable로 비교
let user2 = User(name: "철수", age: 25)
print(user == user2) // true
NSObject는 Objective-C 시대의 모든 객체가 공유하는 "공통 DNA"로, 기본적인 생명 주기, 타입 확인, 동적 기능을 제공하는 기반 클래스입니다.
// ❓ NSObject 상속이 필요할까?
// ✅ 필요한 경우
class MyTableViewCell: UITableViewCell { // UIKit 상속
class MyDelegate: NSObject, SomeObjCProtocol { // Obj-C 프로토콜
class Observable: NSObject {
@objc dynamic var value: Int = 0 // KVO 사용
}
// ❌ 불필요한 경우
class Calculator { // 순수 로직
struct User: Codable { // 데이터 모델
class ViewModel { // MVVM의 ViewModel
대략적으로만 정리해봤습니다. 해당 학습 내용이 얼마나 오래갈진 모르겠지만...
뭐 이런거구나 싶고 다음에 기회되면 또 학습해보겠습니다!