2026년 6월 8일

마스터하기: 의존성 주입(DI)과 제어의 역전(IoC) - 유연하고 테스트하기 쉬운 코드의 비밀

80
마스터하기: 의존성 주입(DI)과 제어의 역전(IoC) - 유연하고 테스트하기 쉬운 코드의 비밀

마스터하기: 의존성 주입(DI)과 제어의 역전(IoC) - 유연하고 테스트하기 쉬운 코드의 비밀

마스터하기: 의존성 주입(DI)과 제어의 역전(IoC) - 유연하고 테스트하기 쉬운 코드의 비밀

안녕하세요, 10년 차 소프트웨어 엔지니어이자 기술 교육자입니다. 오늘은 여러분의 코드를 더욱 유연하고, 테스트하기 쉬우며, 궁극적으로 유지보수하기 좋게 만드는 마법 같은 개념, 바로 **의존성 주입(Dependency Injection, DI)**과 **제어의 역전(Inversion of Control, IoC)**에 대해 깊이 있게 알아보는 시간을 갖겠습니다. 이 두 가지 개념은 현대 소프트웨어 개발에서 견고하고 확장 가능한 시스템을 구축하기 위한 필수적인 설계 원칙이며, 특히 객체 지향 프로그래밍과 프레임워크 기반 개발에서 그 중요성이 더욱 두드러집니다.

많은 개발자들이 이 용어들을 들어봤지만, 그 핵심 원리와 실질적인 이점을 정확히 이해하고 적용하는 데 어려움을 겪곤 합니다. 하지만 걱정 마세요. 오늘 이 글을 통해 여러분은 DI와 IoC가 무엇이며, 왜 중요하고, 어떻게 효과적으로 적용할 수 있는지 명확히 이해하게 될 것입니다. 실제 면접이나 실무에서 마주칠 수 있는 질문들을 대비하고, 더 나아가 여러분의 소프트웨어 설계 역량을 한 단계 끌어올리는 데 큰 도움이 될 것이라고 확신합니다.

1. 개념 소개: 정의, 탄생 배경, 왜 중요한지

1. 개념 소개: 정의, 탄생 배경, 왜 중요한지

1.1. 정의: IoC와 DI는 무엇인가요?

**제어의 역전(Inversion of Control, IoC)**은 객체의 생성, 생명 주기 관리, 그리고 의존성 주입에 대한 제어권이 개발자(혹은 개발자의 코드)로부터 프레임워크나 컨테이너로 넘어가는 디자인 원칙을 말합니다. 쉽게 말해, "객체를 누가 만들고 관리할 것인가?"에 대한 제어권이 역전되는 것이죠. 전통적인 방식에서는 개발자가 직접 new 키워드를 사용해 객체를 생성하고 의존성을 연결했지만, IoC 환경에서는 이 역할을 프레임워크나 전용 컨테이너가 담당합니다.

**의존성 주입(Dependency Injection, DI)**은 이러한 IoC 원칙을 구현하는 구체적인 방법 중 하나입니다. 객체가 필요로 하는 다른 객체(즉, 의존성)를 직접 생성하거나 찾는 대신, 외부(주로 IoC 컨테이너)에서 해당 의존성을 객체 내부로 '주입'해주는 방식입니다. 이로써 객체는 자신이 사용할 의존성이 무엇인지 알 필요 없이, 그저 주입받은 의존성을 사용하기만 하면 됩니다.

1.2. 탄생 배경: 왜 필요했을까요?

DI와 IoC의 필요성은 소프트웨어 시스템이 복잡해지면서 자연스럽게 대두되었습니다. 초기 소프트웨어 개발 방식에서는 객체들이 필요한 다른 객체들을 직접 생성하고 사용했습니다. 예를 들어, UserServiceUserRepository를 필요로 한다면, UserService 내부에서 new UserRepository()와 같이 직접 UserRepository 인스턴스를 생성했습니다.

이러한 방식은 다음과 같은 문제점을 야기했습니다.

  • 강한 결합(Tight Coupling): UserServiceUserRepository의 특정 구현체에 강하게 결합됩니다. UserRepository의 구현이 변경되거나 다른 데이터베이스를 사용하게 되면 UserService 코드도 수정해야 합니다.
  • 테스트의 어려움: UserService를 단위 테스트할 때, 실제 데이터베이스에 연결된 UserRepository가 함께 생성됩니다. 이는 테스트 속도를 저하시키고, 테스트 환경을 구축하기 어렵게 만들며, 특정 상황을 재현하기 어렵게 합니다. 가짜 객체(Mock/Stub)로 대체하기가 매우 까다롭습니다.
  • 재사용성 및 확장성 저하: 특정 구현체에 의존하기 때문에, 다른 컨텍스트에서 UserService를 재사용하거나 UserRepository의 다른 구현을 쉽게 교체하기 어렵습니다.
  • 객체 생명 주기 관리의 복잡성: 객체의 생성 순서, 소멸 시점 등을 개발자가 일일이 관리해야 하므로, 특히 대규모 애플리케이션에서 오류 발생 가능성이 높아집니다.

이러한 문제들을 해결하기 위해, 객체 간의 결합도를 낮추고 유연성을 높이며, 테스트 용이성을 확보하는 방법론이 필요했고, 그 해답 중 하나로 IoC와 DI가 등장하게 된 것입니다.

1.3. 왜 중요한가요?

DI와 IoC는 현대 소프트웨어 개발에서 다음과 같은 이유로 매우 중요합니다.

  1. 높은 유연성과 확장성: 객체가 특정 구현체에 묶이지 않고 인터페이스나 추상 클래스에 의존함으로써, 런타임에 다른 구현체로 쉽게 교체할 수 있습니다. 이는 시스템의 유연성과 확장성을 크게 향상시킵니다.
  2. 테스트 용이성: DI를 통해 의존성을 외부에서 주입받으므로, 단위 테스트 시 실제 의존성 대신 테스트용 Mock 객체나 Stub 객체를 쉽게 주입하여 테스트 환경을 격리하고 효율적인 테스트를 수행할 수 있습니다.
  3. 낮은 결합도: 객체 간의 직접적인 의존성 생성을 제거하여 결합도를 낮춥니다. 이는 코드의 변경이 다른 부분에 미치는 영향을 최소화하여 유지보수를 용이하게 합니다.
  4. 높은 재사용성: 특정 환경이나 구현에 얽매이지 않고 일반화된 형태로 객체를 설계할 수 있어 코드의 재사용성이 높아집니다.
  5. 객체 생명 주기 관리의 간소화: IoC 컨테이너가 객체의 생성, 관리, 소멸을 담당하여 개발자는 비즈니스 로직에 집중할 수 있게 됩니다.

결론적으로, DI와 IoC는 "어떻게 하면 더 좋은 코드를 만들 것인가?"에 대한 해답 중 하나이며, SOLID 원칙 중 특히 **D (Dependency Inversion Principle, 의존성 역전 원칙)**를 실현하는 핵심적인 방법입니다.

2. 핵심 원리 설명

2. 핵심 원리 설명

2.1. 제어의 역전 (Inversion of Control, IoC)

IoC의 핵심은 "누가 제어권을 가지는가?"에 대한 역전입니다. 전통적인 프로그래밍에서는 개발자가 프로그램의 흐름을 직접 제어합니다. 예를 들어, 메인 함수에서 ClassA 인스턴스를 만들고, ClassA 안에서 ClassB 인스턴스를 만들고, 다시 ClassB 안에서 ClassC 인스턴스를 만드는 식이죠.

하지만 IoC에서는 이러한 객체 생성 및 관리의 제어권이 프레임워크나 IoC 컨테이너로 넘어갑니다. 개발자는 필요한 객체들을 정의하고, 이들이 어떤 의존성을 가지는지 설정 파일이나 어노테이션 등으로 컨테이너에 알려줍니다. 그러면 컨테이너가 알아서 객체를 생성하고, 필요한 의존성들을 찾아 연결해 줍니다. 개발자는 그저 "나는 이런 객체가 필요해"라고 선언하고, 컨테이너가 이를 제공해 주는 방식이죠.

비유: 전통적인 방식은 여러분이 직접 자동차 부품을 하나하나 사서 조립하는 것에 비유할 수 있습니다. 엔진, 타이어, 차체 등을 직접 구매하고 연결해야 합니다. 반면 IoC는 여러분이 자동차 제조사에 "이런 사양의 차를 만들어 주세요"라고 주문하고, 제조사가 모든 부품을 조달하고 조립하여 완성된 차를 여러분에게 전달해 주는 것에 비유할 수 있습니다. 여러분은 차를 직접 조립하는 과정에 관여하지 않고, 그저 만들어진 차를 받아서 운전하기만 하면 됩니다.

2.2. 의존성 주입 (Dependency Injection, DI)

DI는 IoC를 구현하는 가장 일반적인 패턴입니다. 객체가 자신의 의존성을 직접 생성하거나 찾지 않고, 외부에서 주입받는 방식입니다. 주입 방식에는 크게 세 가지가 있습니다.

  1. 생성자 주입 (Constructor Injection):

    • 객체를 생성할 때 생성자의 파라미터로 필요한 의존성을 전달받는 방식입니다.
    • 장점: 객체가 생성될 때 모든 필수 의존성이 주입되므로, 객체의 불변성을 보장하기 쉽고, 의존성이 명확하며, 순환 의존성을 방지하는 데 효과적입니다. 가장 권장되는 방식입니다.
    • 단점: 의존성이 많아지면 생성자의 파라미터가 길어질 수 있습니다.
    class Engine:
        def start(self):
            return "Engine started"
    
    class Car:
        def __init__(self, engine: Engine): # 생성자 주입
            self.engine = engine
    
        def drive(self):
            return self.engine.start() + ", Car is driving."
    
  2. 세터 주입 (Setter Injection):

    • 객체가 생성된 후, 세터(Setter) 메서드를 통해 의존성을 주입받는 방식입니다.
    • 장점: 의존성이 필수가 아닐 때 유용하며, 런타임에 의존성을 변경할 수 있는 유연성을 제공합니다.
    • 단점: 객체가 생성된 후에도 의존성이 주입되지 않으면 제대로 동작하지 않을 수 있어, 객체의 일관성 유지가 어렵습니다.
    class Engine:
        def start(self):
            return "Engine started"
    
    class Car:
        def __init__(self):
            self._engine = None # 초기에는 엔진 없음
    
        def set_engine(self, engine: Engine): # 세터 주입
            self._engine = engine
    
        def drive(self):
            if self._engine:
                return self._engine.start() + ", Car is driving."
            return "Engine not set, cannot drive."
    
  3. 필드 주입 (Field Injection):

    • 객체의 필드(속성)에 직접 의존성을 주입하는 방식입니다. 주로 프레임워크에서 어노테이션 등을 통해 자동으로 주입해 줍니다.
    • 장점: 코드가 간결해 보입니다.
    • 단점: 객체가 외부에서 의존성을 주입받는다는 사실을 숨기므로 테스트하기 어렵고, 순환 의존성을 만들 위험이 높습니다. 또한, 객체의 불변성을 보장하기 어렵고, DI 컨테이너 없이 객체를 생성할 수 없게 만듭니다. 가장 권장되지 않는 방식입니다.
    # Python에서는 필드 주입을 직접적으로 지원하는 내장 메커니즘이 없으며,
    # 주로 프레임워크(예: Spring의 @Autowired)에서 리플렉션 등을 통해 구현됩니다.
    # 아래는 개념적인 예시입니다.
    # class Car:
    #     @inject_field # 이런 식의 가상 어노테이션이 있다고 가정
    #     engine: Engine
    #
    #     def drive(self):
    #         return self.engine.start() + ", Car is driving."
    

2.3. IoC 컨테이너 / DI 컨테이너

IoC 컨테이너(또는 DI 컨테이너)는 IoC 원칙을 구현하는 핵심 도구입니다. 이 컨테이너는 객체의 생명 주기를 관리하고, 객체 간의 의존성을 자동으로 주입해 주는 역할을 합니다. 개발자는 컨테이너에게 어떤 객체들이 있고, 이들이 어떤 의존성을 가지는지 설정(XML, 어노테이션, 코드)만 해주면 됩니다.

컨테이너의 주요 역할:

  • 객체 생성 및 관리: 개발자가 직접 new 키워드를 사용하지 않아도, 컨테이너가 필요한 시점에 객체를 생성하고 관리합니다. (싱글톤, 프로토타입 등 다양한 스코프 관리)
  • 의존성 해결 및 주입: 특정 객체가 필요로 하는 의존성을 찾아 자동으로 주입해 줍니다.
  • 설정 관리: 애플리케이션의 구성 정보를 한곳에서 관리할 수 있게 해줍니다.

다이어그램으로 보는 강한 결합 vs. 약한 결합 (DI 적용)

강한 결합 (No DI)

graph TD
    A[UserService] --> B(new UserRepository())
    B --> C(Database)
  • UserService가 직접 UserRepository의 구체적인 구현체(new UserRepository())를 생성하고 의존합니다.
  • UserRepository의 구현이 변경되면 UserService도 변경되어야 합니다.
  • UserService를 테스트할 때 Database까지 포함되어야 합니다.

약한 결합 (DI 적용)

graph TD
    A[UserService] --> I[UserRepository Interface]
    I --> B1[DatabaseUserRepository]
    I --> B2[MockUserRepository (for Test)]
    C[IoC Container] --> A
    C --> I
  • UserServiceUserRepository 인터페이스(혹은 추상 클래스)에 의존합니다.
  • IoC Container가 런타임에 UserRepository의 어떤 구현체(DatabaseUserRepository 또는 MockUserRepository)를 UserService에 주입할지 결정합니다.
  • UserService는 자신이 어떤 UserRepository 구현체를 사용하는지 알 필요가 없습니다.
  • 테스트 시에는 MockUserRepository를 주입하여 UserService만을 독립적으로 테스트할 수 있습니다.

3. 코드 예제 2개

여기서는 Python과 JavaScript를 사용하여 DI의 개념을 코드 레벨에서 이해해 보겠습니다.

3.1. Python 예제: 수동 DI와 간단한 IoC 컨테이너

Python은 Java나 C#처럼 강력한 타입 시스템이나 인터페이스 개념이 기본적으로 없지만, 추상 클래스와 타입 힌트를 통해 유사한 효과를 낼 수 있습니다. 이 예제에서는 먼저 수동으로 DI를 적용하는 방법을 보여주고, 이어서 매우 간단한 IoC 컨테이너를 직접 구현하여 의존성 관리를 자동화하는 과정을 보여줍니다.

# 1. 의존성 정의 (추상 클래스 또는 인터페이스 역할)
from abc import ABC, abstractmethod

class Notifier(ABC):
    @abstractmethod
    def notify(self, message: str):
        pass

# 2. 구체적인 의존성 구현체
class EmailNotifier(Notifier):
    def notify(self, message: str):
        print(f"Sending email notification: {message}")

class SMSNotifier(Notifier):
    def notify(self, message: str):
        print(f"Sending SMS notification: {message}")

# 3. 서비스 클래스 (Notifier에 의존)
class UserService:
    # 생성자 주입 (Constructor Injection)
    def __init__(self, notifier: Notifier):
        self.notifier = notifier

    def register_user(self, username: str):
        print(f"User '{username}' registered successfully.")
        self.notifier.notify(f"Welcome, {username}! Your registration is complete.")

# ----------------------------------------------------
# 수동 DI (IoC 컨테이너 없이 직접 의존성을 주입)
# ----------------------------------------------------
print("--- 수동 DI 예제 ---")
email_notifier_instance = EmailNotifier()
user_service_with_email = UserService(email_notifier_instance) # EmailNotifier 주입
user_service_with_email.register_user("Alice")

sms_notifier_instance = SMSNotifier()
user_service_with_sms = UserService(sms_notifier_instance)   # SMSNotifier 주입
user_service_with_sms.register_user("Bob")

print("\n--- 테스트 시 Mock 객체 주입 예제 ---")
class MockNotifier(Notifier):
    def notify(self, message: str):
        print(f"[Mock] Notification received: {message}") # 실제 알림 대신 Mock 출력

mock_notifier_instance = MockNotifier()
user_service_for_test = UserService(mock_notifier_instance)
user_service_for_test.register_user("TestUser")

# ----------------------------------------------------
# 간단한 IoC 컨테이너 구현 예제
# 실제 프레임워크의 컨테이너는 훨씬 더 복잡하고 강력합니다.
# ----------------------------------------------------
print("\n--- 간단한 IoC 컨테이너 예제 ---")

class SimpleContainer:
    def __init__(self):
        self._dependencies = {} # 의존성을 저장할 딕셔너리

    def register(self, interface: type, implementation: type):
        """인터페이스와 구현체를 등록합니다."""
        self._dependencies[interface] = implementation

    def resolve(self, interface: type):
        """등록된 인터페이스에 해당하는 구현체의 인스턴스를 반환합니다."""
        if interface not in self._dependencies:
            raise ValueError(f"No implementation registered for {interface}")
        
        implementation_class = self._dependencies[interface]
        # 구현체의 생성자에 필요한 의존성이 있다면 재귀적으로 resolve를 호출하여 주입
        # 여기서는 Notifier만 있고 Notifier가 다른 의존성을 가지지 않는다고 가정
        # 실제 컨테이너는 더 복잡한 의존성 그래프를 처리합니다.
        return implementation_class()

    def get_instance(self, service_class: type):
        """서비스 클래스의 인스턴스를 생성하고 의존성을 주입합니다."""
        # 서비스 클래스의 생성자 파라미터(의존성)를 분석
        import inspect
        signature = inspect.signature(service_class.__init__)
        dependencies_to_inject = {}
        for name, param in signature.parameters.items():
            if name == 'self':
                continue
            if param.annotation in self._dependencies: # 타입 힌트를 보고 컨테이너에 등록된 의존성인지 확인
                dependencies_to_inject[name] = self.resolve(param.annotation)
            else:
                # 등록되지 않은 의존성은 직접 제공되거나 다른 방식으로 처리되어야 함
                # 이 예제에서는 간단히 넘어감
                pass

        return service_class(**dependencies_to_inject)

# 컨테이너 생성 및 의존성 등록
container = SimpleContainer()
container.register(Notifier, EmailNotifier) # Notifier 인터페이스에 EmailNotifier 구현체 등록

# 컨테이너를 통해 UserService 인스턴스 가져오기 (의존성 주입 자동화)
user_service_from_container = container.get_instance(UserService)
user_service_from_container.register_user("Charlie")

# 만약 SMSNotifier로 변경하고 싶다면, 등록만 변경하면 됨
print("\n--- 컨테이너 의존성 변경 후 ---")
container.register(Notifier, SMSNotifier)
user_service_from_container_sms = container.get_instance(UserService)
user_service_from_container_sms.register_user("David")

3.2. JavaScript 예제: 클래스와 인터페이스 개념 활용

JavaScript는 타입스크립트를 사용하면 인터페이스를 명시적으로 정의할 수 있지만, 순수 JavaScript에서는 추상 클래스나 클래스 상속을 통해 인터페이스와 유사한 역할을 수행할 수 있습니다. 여기서는 클래스와 타입 힌트 대신 주석을 통해 의존성 역할을 명시합니다.

// 1. 의존성 정의 (인터페이스 역할)
// TypeScript를 사용한다면 interface Notifier { notify(message: string): void; } 로 명확히 정의할 수 있습니다.
class NotifierInterface {
    /**
     * @param {string} message
     */
    notify(message) {
        throw new Error("Method 'notify()' must be implemented.");
    }
}

// 2. 구체적인 의존성 구현체
class EmailNotifier extends NotifierInterface {
    /**
     * @param {string} message
     */
    notify(message) {
        console.log(`Sending email notification: ${message}`);
    }
}

class SMSNotifier extends NotifierInterface {
    /**
     * @param {string} message
     */
    notify(message) {
        console.log(`Sending SMS notification: ${message}`);
    }
}

// 3. 서비스 클래스 (Notifier에 의존)
class UserService {
    /**
     * 생성자 주입 (Constructor Injection)
     * @param {NotifierInterface} notifier
     */
    constructor(notifier) {
        if (!(notifier instanceof NotifierInterface)) {
            // 타입 검사 (런타임)
            throw new Error("Notifier must be an instance of NotifierInterface or its subclass.");
        }
        this.notifier = notifier;
    }

    /**
     * @param {string} username
     */
    registerUser(username) {
        console.log(`User '${username}' registered successfully.`);
        this.notifier.notify(`Welcome, ${username}! Your registration is complete.`);
    }
}

// ----------------------------------------------------
// 수동 DI (IoC 컨테이너 없이 직접 의존성을 주입)
// ----------------------------------------------------
console.log("--- 수동 DI 예제 ---");
const emailNotifierInstance = new EmailNotifier();
const userServiceWithEmail = new UserService(emailNotifierInstance); // EmailNotifier 주입
userServiceWithEmail.registerUser("Alice");

const smsNotifierInstance = new SMSNotifier();
const userServiceWithSms = new UserService(smsNotifierInstance);   // SMSNotifier 주입
userServiceWithSms.registerUser("Bob");

console.log("\n--- 테스트 시 Mock 객체 주입 예제 ---");
class MockNotifier extends NotifierInterface {
    /**
     * @param {string} message
     */
    notify(message) {
        console.log(`[Mock] Notification received: ${message}`); // 실제 알림 대신 Mock 출력
    }
}

const mockNotifierInstance = new MockNotifier();
const userServiceForTest = new UserService(mockNotifierInstance);
userServiceForTest.registerUser("TestUser");

// ----------------------------------------------------
// 간단한 IoC 컨테이너 구현 예제
// NestJS와 같은 프레임워크는 더욱 강력한 DI 컨테이너를 제공합니다.
// ----------------------------------------------------
console.log("\n--- 간단한 IoC 컨테이너 예제 ---");

class SimpleContainer {
    constructor() {
        this._dependencies = new Map(); // 의존성을 저장할 Map
    }

    /**
     * 인터페이스와 구현체를 등록합니다.
     * @param {NotifierInterface | Function} interfaceOrClass - 인터페이스 또는 서비스 클래스
     * @param {NotifierInterface | Function} implementationOrDependency - 구현체 또는 의존성 객체
     */
    register(interfaceOrClass, implementationOrDependency) {
        this._dependencies.set(interfaceOrClass, implementationOrDependency);
    }

    /**
     * 등록된 인터페이스/클래스에 해당하는 인스턴스를 반환합니다.
     * @param {NotifierInterface | Function} interfaceOrClass
     * @returns {any}
     */
    resolve(interfaceOrClass) {
        if (!this._dependencies.has(interfaceOrClass)) {
            throw new Error(`No implementation registered for ${interfaceOrClass.name}`);
        }

        const registered = this._dependencies.get(interfaceOrClass);

        // 등록된 것이 클래스라면 인스턴스를 생성하여 반환 (싱글톤 아님, 매번 새 인스턴스)
        // 실제 컨테이너는 싱글톤, 스코프 등 복잡한 생명주기 관리
        if (typeof registered === 'function' && registered.prototype instanceof NotifierInterface) {
            // NotifierInterface를 상속받는 구현체라면
            return new registered();
        } else if (typeof registered === 'function') {
            // 다른 서비스 클래스 (예: UserService)라면, 해당 클래스의 생성자 의존성을 재귀적으로 해결
            // 여기서는 UserService의 Notifier 의존성만 처리한다고 가정
            const constructorParams = Reflect.getMetadata("design:paramtypes", registered) || [];
            const resolvedParams = constructorParams.map(paramType => this.resolve(paramType));
            return new registered(...resolvedParams);
        } else {
            // 이미 인스턴스로 등록된 경우 (예: 특정 설정을 가진 인스턴스)
            return registered;
        }
    }
}

// 컨테이너 생성 및 의존성 등록
const container = new SimpleContainer();
// Notifier 인터페이스에 EmailNotifier 구현체 등록
container.register(NotifierInterface, EmailNotifier); 

// UserService 클래스를 컨테이너에 등록 (이 클래스는 NotifierInterface에 의존함)
// JavaScript에서는 constructor의 파라미터 타입을 런타임에 직접 알기 어려움
// TypeScript의 Reflect Metadata를 사용하거나, 수동으로 의존성을 명시해야 함.
// 이 예제에서는 단순화를 위해 'resolve' 함수가 NotifierInterface를 직접 찾아 주입한다고 가정합니다.
// 실제 NestJS 등은 데코레이터와 Reflect Metadata를 사용하여 이를 자동화합니다.
container.register(UserService, UserService); // UserService 자체도 컨테이너가 생성하도록 등록

// 컨테이너를 통해 UserService 인스턴스 가져오기 (의존성 주입 자동화)
// 여기서는 UserService 생성자의 NotifierInterface 타입을 보고 자동으로 주입
const userServiceFromContainer = container.resolve(UserService);
userServiceFromContainer.registerUser("Charlie");

// 만약 SMSNotifier로 변경하고 싶다면, 등록만 변경하면 됨
console.log("\n--- 컨테이너 의존성 변경 후 ---");
container.register(NotifierInterface, SMSNotifier);
const userServiceFromContainerSms = container.resolve(UserService);
userServiceFromContainerSms.registerUser("David");

주의: JavaScript 예제에서 Reflect.getMetadata는 TypeScript 컴파일 시 --emitDecoratorMetadata 옵션을 통해 생성되는 메타데이터를 활용합니다. 순수 JavaScript에서는 이 정보를 얻기 어렵거나, 직접 의존성을 명시적으로 등록해야 합니다.