NSObject란 뭘까?

피터·2025년 10월 22일
post-thumbnail

안녕하세요. 개발하면서 자주 보는 NSObject, 프로토콜 만들 때 넣어야 했던 그것!
단순히 "상속받아야 하는 것" 정도로만 알고 있었는데, 오늘 제대로 파헤쳐봅니다.


한 줄 요약

NSObject는 Objective-C의 모든 클래스가 상속받는 최상위 클래스(Root Class)로, 객체의 기본 동작과 런타임 시스템과의 인터페이스를 제공하는 기반 클래스입니다.


NSObject는 어디서 왔을까?

NS의 의미

  • NS = NeXTSTEP
  • 스티브 잡스가 Apple에서 나와 설립한 NeXT 회사의 운영체제
  • Apple로 복귀 후, NeXTSTEP의 기술이 macOS와 iOS의 기반이 됨

탄생 배경

  • 시기: 1980년대 후반, NeXTSTEP 개발 당시
  • 목적: 강력한 객체 지향 프로그래밍 환경 구축
  • 언어: Objective-C 채택 (C언어에 Smalltalk의 객체 지향 개념 결합)
  • 위치: Foundation Kit의 핵심 클래스

NSObject의 존재 이유

NSObject는 세 가지 핵심 문제를 해결하기 위해 만들어졌습니다.

1. 객체 계층의 기본 틀 제공

       NSObject  ← 모든 객체의 시작점
          ↓
     UIResponder
          ↓
       UIView
          ↓
      UILabel
  • Objective-C의 모든 클래스가 상속받는 최상위 클래스
  • 모든 객체가 가져야 할 공통 기능을 정의
  • Cocoa/Cocoa Touch 프레임워크의 근간

2. 런타임 시스템과의 인터페이스

런타임 시스템이란?

  • 프로그램이 실행되는 동안 동작하는 시스템
  • 컴파일 타임 vs 런타임 비교:
시점설명예시
컴파일 타임코드 작성 시점에 결정let number: Int = 5 (타입이 확정)
런타임프로그램 실행 중 결정obj.perform(selector) (실행 시 메서드 결정)

Objective-C의 특징:

  • 동적 타이핑: 변수 타입을 런타임에 결정
  • 메시지 전달: 메서드 호출을 런타임에 해결
  • NSObject는 이런 동적 기능의 핵심 인터페이스

3. 기본 객체 관리 기능 제공

모든 객체가 공통으로 필요한 기능:

  • 생명 주기: alloc(메모리 할당), init(초기화), dealloc(해제)
  • 비교: isEqual:(같은지 비교), hash(해시값)
  • 디버깅: description(객체 설명 문자열)

💡 디버깅 시 본 alloc의 정체
메모리를 할당한다는 뜻! NSObject가 제공하는 기본 메서드였습니다.


NSObject가 제공하는 핵심 기능

Apple 공식 문서를 보면 엄청나게 많은 기능이 있습니다. 주요 기능을 카테고리별로 정리해보면:

1. 객체 생명 주기 관리

// Objective-C 시절
MyClass *obj = [[MyClass alloc] init];  // 생성 및 초기화
[obj release];                          // 해제 (ARC 이전)

// Swift/Modern Objective-C (ARC)
let obj = MyClass()  // alloc + init이 자동으로 처리됨
// 자동으로 메모리 해제

표준화의 의미:

  • 모든 객체가 동일한 방식으로 생성/해제
  • 개발자가 일관된 패턴 사용 가능
  • 메모리 관리 시스템(ARC)과의 호환

2. 디버깅 및 식별

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)

3. 객체 간 비교

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]  // 중복 제거됨

4. 타입 확인

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) 기능들을 실제 예제로 이해해봅시다.

1. 동적 메시지 전달 (Dynamic Message Dispatch)

컴파일 타임 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("버튼 클릭!")
}

2. Key-Value Coding (KVC)

속성 이름(문자열)으로 값에 접근:

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)

3. Key-Value Observing (KVO)

속성 변화를 감지:

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

Swift 시대의 NSObject

Swift에서는 언제 NSObject를 써야 할까?

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() {
        // 일반적인 로직
    }
}

Swift의 대안

Objective-C/NSObjectSwift 대안
KVOCombine의 @Published, Property Observer
NSCodingCodable 프로토콜
isEqualEquatable 프로토콜
descriptionCustomStringConvertible
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를 한 문장으로

NSObject는 Objective-C 시대의 모든 객체가 공유하는 "공통 DNA"로, 기본적인 생명 주기, 타입 확인, 동적 기능을 제공하는 기반 클래스입니다.

기억해야 할 3가지

  1. 역사적 맥락: NeXTSTEP → macOS/iOS의 근간
  2. 핵심 역할: 객체의 기본 행동 정의 + Objective-C 런타임과의 다리
  3. 현대적 사용: Swift에서는 필요한 경우에만 사용, 대부분 Swift 네이티브 기능 우선

실무에서의 판단 기준

// ❓ 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

대략적으로만 정리해봤습니다. 해당 학습 내용이 얼마나 오래갈진 모르겠지만...
뭐 이런거구나 싶고 다음에 기회되면 또 학습해보겠습니다!

📖 참고 자료

profile
iOS 개발자입니다.

0개의 댓글