프로그래밍언어

[Swift] Swift Reflection과 런타임 프로그래밍

애기공룡훈련병 2025. 2. 16. 06:45
반응형

Swift에서 리플렉션(Reflection)은 런타임에 객체의 타입 정보를 검사하고, 메서드를 호출하며, 프로퍼티를 동적으로 접근할 수 있도록 해주는 강력한 기능입니다. 이러한 기능은 의존성 주입(Dependency Injection)과 같은 동적 프로그래밍 기법을 구현할 때 활용됩니다.

이번 글에서는 Swift의 리플렉션과 런타임 프로그래밍에 대해 다뤄보고, 이를 이용하여 간단한 의존성 주입 프레임워크를 구현하는 방법까지 살펴보겠습니다.

 

1. Swift의 Reflection

Reflection은 코드가 실행 중에 자신의 타입 및 프로퍼티, 메서드 등을 검사하고 조작할 수 있도록 해주는 기능입니다.

Swift는 Mirror 타입을 통해 기본적인 Reflection 기능을 제공합니다.

Mirror를 이용한 타입 정보 확인

struct Person {
    let name: String
    let age: Int
}

let person = Person(name: "John Doe", age: 30)
let mirror = Mirror(reflecting: person)

print("Type: \(mirror.subjectType)")
for child in mirror.children {
    print("Property: \(child.label ?? "unknown"), Value: \(child.value)")
}

실행 결과

Type: Person
Property: name, Value: John Doe
Property: age, Value: 30

이처럼 Mirror를 사용하면 구조체나 클래스의 프로퍼티 정보를 동적으로 가져올 수 있습니다.

 

2. 런타임에 타입 정보를 검사하고 메서드를 호출하는 방법

Swift는 Objective-C와 달리 완전한 런타임 동적 타입 조작을 지원하지는 않지만, 일부 기능을 활용하여 유사한 작업을 수행할 수 있습니다. 예를 들어, NSObjectProtocol을 따르는 클래스에서는 perform(_:with:)을 사용하여 메서드를 실행할 수 있습니다.

Objective-C 런타임을 활용한 동적 메서드 호출

import Foundation

class Animal: NSObject {
    @objc func speak() {
        print("The animal makes a sound")
    }
}

let animal = Animal()
animal.perform(#selector(Animal.speak))

실행 결과

The animal makes a sound

하지만 struct나 enum은 이러한 방식의 동적 호출을 지원하지 않으므로, Mirror나 프로토콜을 활용하여 해결해야 합니다.

 

3. 의존성 주입(Dependency Injection) 프레임워크 구현

의존성 주입(Dependency Injection, DI)은 객체 간의 의존성을 외부에서 주입하여 결합도를 낮추고 유지보수를 용이하게 하는 기법입니다. Swift에서 리플렉션을 활용하여 간단한 DI 컨테이너를 구현해보겠습니다.

DI 컨테이너 구현

protocol Service {}

class ServiceA: Service {
    func execute() {
        print("ServiceA is executing")
    }
}

class ServiceB: Service {
    func execute() {
        print("ServiceB is executing")
    }
}

class DIContainer {
    private var services: [String: Any] = [:]
    
    func register<T>(_ service: T) {
        let key = String(describing: T.self)
        services[key] = service
    }
    
    func resolve<T>() -> T? {
        let key = String(describing: T.self)
        return services[key] as? T
    }
}

// 컨테이너 생성 및 서비스 등록
let container = DIContainer()
container.register(ServiceA())
container.register(ServiceB())

// 의존성 해결
if let serviceA: ServiceA = container.resolve() {
    serviceA.execute()
}
if let serviceB: ServiceB = container.resolve() {
    serviceB.execute()
}

실행 결과

ServiceA is executing
ServiceB is executing

 

  • DIContainer는 서비스를 저장하는 딕셔너리를 유지하며, 타입 정보를 문자열로 변환하여 저장합니다.
  • register 메서드는 특정 타입의 객체를 저장하고, resolve 메서드는 해당 타입의 객체를 반환합니다.
  • String(describing: T.self)를 이용하여 런타임에서 타입을 식별합니다.

이러한 DI 컨테이너를 활용하면 런타임에 유연하게 객체를 생성하고 주입할 수 있어 유지보수성과 확장성이 향상됩니다.

 

Swift의 리플렉션과 런타임 프로그래밍을 활용하면 보다 동적인 구조를 설계할 수 있으며, 특히 의존성 주입 같은 패턴을 구현할 때 유용합니다. 다만, Swift는 Objective-C에 비해 런타임 동적 기능이 제한적이므로 Mirror, NSObject, @objc 등을 적절히 조합해야 합니다.

이번 글에서는 다음 내용을 다뤘습니다.

  • Swift의 Mirror를 활용한 타입 및 프로퍼티 정보 조회
  • 런타임에서 동적으로 메서드를 호출하는 방법
  • 리플렉션을 이용한 간단한 의존성 주입 프레임워크 구현

리플렉션을 과도하게 사용하면 성능 저하가 발생할 수 있으므로, 적절한 상황에서만 활용하는 것이 중요합니다. 필요에 따라 리플렉션과 프로토콜 기반 설계를 조합하여 사용하면 보다 효율적인 코드 구조를 만들 수 있습니다.

반응형