[Spring][개념]⚡ 개발자가 몰랐던 진짜 IoC와 DI의 세계 🌐

김상욱·2024년 10월 15일
post-thumbnail

IoC(Inversion of Control)와 DI 쉽게 이해하기 ✨

프레임워크가 제어권을 넘겨받으면 개발자는 무엇이 달라질까요? IoC와 DI의 핵심 개념과 구현 방식을 함께 알아봅시다! 🚀


IoC (Inversion of Control, 제어의 역행)란? 🔄

IoC란 제어의 흐름을 개발자가 아닌 외부 프레임워크가 담당하게 하는 개념입니다.
기존에는 코드가 객체의 생성과 연결을 직접 관리했지만, IoC에서는 이를 외부가 대신 수행하여 개발자가 더 중요한 로직에 집중할 수 있도록 돕습니다.


IoC와 DI(Dependency Injection) 관계 📌

  • DI(의존성 주입)은 IoC의 대표적인 구현 방법입니다.
  • 객체가 직접 의존성을 찾기보다는 외부에서 필요한 객체를 주입받아 사용합니다.
    이로 인해 결합도가 줄어들고, 코드의 유연성과 재사용성이 향상됩니다.

객체 간의 연결 시점은 언제? 🕒

IoC를 통해 객체 간의 관계는 런타임에 결정됩니다.

A a = new A();
B b = new B(a);  // 직접 생성 (전통적인 방식)

대신, IoC에서는 설정 파일이나 어노테이션을 통해 객체 간의 의존성을 정의하고,
프레임워크가 런타임에 주입합니다. 이 과정에서 개발자는 세부 구현을 신경 쓰지 않아도 됩니다.


느슨한 결합(Loose Coupling)이란? 🤝

  • 느슨한 결합서로 다른 객체가 최소한의 의존성만 가지는 것을 의미합니다.
  • 결합도가 낮아지면 변경에 유연해지고, 유지보수 및 확장도 쉬워집니다.
  • IoC를 통해 객체 간의 관계가 느슨해지면 교체나 수정 시 다른 부분에 영향을 주지 않습니다.

IoC 구현 방법: DI(의존성 주입) 🎯

IoC를 구현하는 대표적인 방식인 DI(Dependency Injection)에는 다양한 유형이 있습니다.

1. 생성자 주입(Constructor Injection)

객체가 생성될 때, 필요한 의존성을 생성자에서 받습니다.

public class B {
    private A a;
    public B(A a) {
        this.a = a;  // 생성자 주입
    }
}

2. 세터 주입(Setter Injection)

객체 생성 후 세터 메서드를 통해 의존성을 주입합니다.

public class B {
    private A a;
    public void setA(A a) {
        this.a = a;  // 세터 주입
    }
}

3. 인터페이스 주입(Interface Injection)

의존성을 필요로 하는 객체가 주입 인터페이스를 구현합니다.


IoC와 DI의 이점 🌟

1️⃣ 재사용성이 높아집니다.
2️⃣ 유지보수와 테스트가 용이해집니다. (Mock 객체를 통한 유닛 테스트 가능)
3️⃣ 설정 변경만으로도 쉽게 객체 관계를 바꿀 수 있습니다.


예시: 스프링 프레임워크와 IoC 🌿

  • 스프링(Spring)은 IoC와 DI 개념을 가장 잘 활용한 프레임워크입니다.
  • 개발자는 비즈니스 로직에만 집중하고, 객체 생성과 의존성 주입은 스프링이 대신 처리합니다.

IoC의 유형 알아보기: Dependency Lookup vs. Dependency Injection 🚀

의존성 주입(DI)은 왜 중요할까? IoC의 두 가지 대표적인 구현 방식인 Dependency LookupDependency Injection을 명확하게 비교해 봅시다!


IoC의 유형 🌟

IoC(Inversion of Control, 제어의 역행)는 크게 두 가지로 나눌 수 있습니다.

  • Dependency Lookup: 객체가 스스로 의존성을 찾음
  • Dependency Injection: 외부에서 의존성을 주입받음

이제 각각의 특징과 예시를 알아보겠습니다. 🔍


1. Dependency Lookup (의존성 조회) 🔍

Dependency Lookup은 객체가 필요한 의존성을 직접 조회하는 방식입니다.

🔗 JNDI Lookup

  • JNDI(Java Naming and Directory Interface)를 이용해 네트워크 상의 자원을 찾습니다.
  • 데이터베이스 커넥션 풀과 같은 자원을 객체가 직접 조회해서 사용합니다.

💡 단점:
객체가 직접 외부 자원을 조회해야 하므로 코드가 복잡해지고, 외부 환경과의 강한 결합이 발생할 수 있습니다.


2. Dependency Injection (의존성 주입) 🎯

Dependency Injection에서는 객체가 스스로 의존성을 찾지 않고 외부에서 주입받습니다.
이 방식은 코드의 결합도를 낮추고 유지보수성을 높이는 데 매우 유리합니다.

2.1 Setter Injection (세터 주입) 🛠️

객체 생성 후, 세터 메서드를 통해 의존성을 주입하는 방식입니다.

public class B {
    private A a;
    public void setA(A a) {
        this.a = a;  // 세터 주입
    }
}

💡 장점:
의존성을 쉽게 교체할 수 있으며 유연성이 뛰어납니다.


2.2 Constructor Injection (생성자 주입) 🏗️

객체가 생성될 때 생성자를 통해 의존성을 전달받는 방식입니다.

public class B {
    private A a;
    public B(A a) {
        this.a = a;  // 생성자 주입
    }
}

💡 장점:
객체 생성과 동시에 모든 의존성이 주입되기 때문에 일관된 상태를 유지할 수 있습니다.


2.3 Method Injection (메서드 주입) 🔄

의존성이 필요할 때마다 메서드 호출 시점에 전달되는 방식입니다.

public class B {
    public void useDependency(A a) {
        a.execute();  // 필요한 시점에 의존성 사용
    }
}

💡 장점:
메서드를 호출할 때만 의존성을 사용하므로 메모리 절약에 유리합니다.


IoC 유형 간의 비교 📊

구분Dependency LookupDependency Injection
제어 흐름객체가 직접 의존성을 찾음외부에서 의존성을 주입함
코드 결합도상대적으로 강한 결합느슨한 결합
유지보수어렵고 복잡함유지보수가 쉬움
테스트 용이성Mock 객체 사용이 어려움테스트가 용이함 (Mock 주입 가능)
예시JNDI LookupSetter, Constructor, Method Injection

예시: 스프링 프레임워크와 DI 🌿

스프링(Spring) 프레임워크DI(Dependency Injection) 방식을 사용해 개발자가 객체 생성과 의존성 관리를 신경 쓰지 않도록 돕습니다. 개발자는 비즈니스 로직에만 집중하고, 스프링이 객체의 생성과 주입을 관리합니다.


Dependency Lookup: 의존성 조회의 모든 것 🔍

컨테이너에서 필요한 객체를 직접 찾는 방법!
Dependency Lookup 방식의 특징과 코드 예제, 그리고 한계점까지 자세히 알아봅니다. 🚀


Dependency Lookup (의존성 조회)란? 🔎

Dependency Lookup이란 객체가 직접 필요한 의존성컨테이너의 Lookup Context를 통해 조회하는 방식입니다. 이 방법은 자바의 JNDI(Java Naming and Directory Interface)와 같은 메커니즘을 사용해 외부 자원을 검색합니다.


1. 컨테이너의 Lookup Context를 사용한 자원 검색 🛠️

  • 컨테이너는 애플리케이션의 객체와 리소스를 관리합니다.
  • Lookup Context는 자원의 이름을 기반으로 필요한 객체를 검색할 수 있는 환경입니다.
    예를 들어, 데이터베이스 커넥션 풀을 검색할 때는 다음과 같이 사용합니다.
InitialContext context = new InitialContext();
DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/MyDB");
  • context.lookup() 메서드는 지정된 이름의 자원을 반환합니다.

2. JNDI 외의 방식 사용 시 코드 복잡성 증가 ⚠️

  • JNDI 외의 메커니즘을 사용할 경우, 기존의 JNDI 관련 코드를 수정해야 하는 문제가 발생합니다.
  • 새로운 Lookup 방식이 도입될 때마다, 기존 코드를 일일이 수정해야 하므로 유연성이 떨어질 수 있습니다.

3. Lookup된 Object를 타입으로 Casting 필요 🌀

  • Lookup 메서드는 기본적으로 Object 타입을 반환합니다. 따라서 사용하기 전에 필요한 타입으로 캐스팅해야 합니다.
Object obj = context.lookup("java:comp/env/jdbc/MyDB");
DataSource ds = (DataSource) obj;  // 명시적 캐스팅 필요

주의할 점: 잘못된 타입으로 캐스팅하면 ClassCastException 예외가 발생할 수 있습니다.


4. Naming Exception 처리 필요 🔧

  • 의존성을 조회할 때, 올바른 이름이 아닐 경우 NamingException이 발생합니다.
  • 이 예외를 처리하는 로직을 작성해야 합니다.
try {
    DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/MyDB");
} catch (NamingException e) {
    e.printStackTrace();  // 예외 처리
}
  • 예외가 발생하는 경우:
    1. 잘못된 자원 이름으로 조회할 때
    2. 리소스가 컨테이너에 등록되지 않았을 때

Dependency Lookup의 한계 🚧

  • 강한 결합: 객체가 외부 자원을 스스로 조회하므로 코드가 외부 환경에 강하게 의존합니다.
  • 테스트 어려움: Mock 객체를 이용한 유닛 테스트가 어렵습니다.
  • 유연성 저하: 환경이 바뀔 때마다 코드를 수정해야 하므로 유지보수가 복잡해집니다.

Dependency Injection과의 비교 🔄

구분Dependency LookupDependency Injection
제어 흐름객체가 스스로 의존성을 조회외부에서 의존성을 주입함
유연성특정 환경에 종속됨다양한 환경에 유연하게 대응
테스트 용이성테스트가 어려움테스트가 용이 (Mock 주입 가능)
예시JNDI LookupSetter, Constructor 주입 방식

예시: 스프링에서의 Dependency Injection 🌿

스프링 프레임워크DI(Dependency Injection) 방식을 사용해 코드의 유연성과 유지보수성을 극대화합니다.

  • 개발자는 객체 생성과 의존성 관리에서 해방되고, 비즈니스 로직에 집중할 수 있습니다.
  • 객체는 외부에서 의존성을 주입받기 때문에, 환경 변화에도 쉽게 적응할 수 있습니다.

Dependency Injection (의존성 주입) 🎯

Dependency Injection (DI)는 객체가 직접 의존성을 조회하지 않고, 외부에서 필요한 의존성을 주입받는 방식입니다. 이 방식을 통해 객체 간 결합도를 낮추고, 유지보수와 테스트가 쉬워지도록 설계합니다.


1. Object가 Lookup 코드를 사용하지 않음 🚫

  • Dependency Injection에서는 객체 내부에서 lookup() 메서드와 같은 코드를 사용하지 않습니다.

  • 필요한 의존성(Dependency)을 객체 스스로 찾지 않고, 컨테이너가 외부에서 의존성을 주입해줍니다.

  • 이를 통해 개발자는 객체의 비즈니스 로직에만 집중할 수 있으며, 의존성 관리의 복잡성은 프레임워크나 컨테이너가 담당합니다.

    예시:

    public class Service {
        private Repository repository;
    
        // 생성자 주입
        public Service(Repository repository) {
            this.repository = repository;
        }
    }

    위 코드에서 Service 객체는 스스로 Repository를 찾지 않고, 외부에서 주입받는 형태로 동작합니다.


2. Object는 컨테이너의 존재를 알 필요가 없음 🧩

  • DI에서는 객체가 외부 컨테이너나 환경의 존재를 알 필요가 없습니다.
    이는 객체가 컨테이너에 종속되지 않게 설계된다는 의미입니다.
  • 예시:
    객체 AB를 의존한다고 할 때, 객체 AB가 어디서 왔는지, 어떤 방식으로 생성되었는지 몰라도 됩니다.
    객체의 생성과 주입은 컨테이너가 처리하며, 이는 객체 간의 느슨한 결합(loose coupling)을 가능하게 합니다.

3. Lookup 코드가 Object 내부에서 사라짐 ❌

  • 전통적인 방식에서는 객체가 직접 lookup 코드를 사용하여 의존성을 찾아야 했습니다.
    하지만 DI를 사용하면 객체 내부에서 lookup 코드가 필요 없으며, 객체는 주입된 의존성을 바로 사용하면 됩니다.

    기존 방식 (Lookup 코드 사용):

    InitialContext context = new InitialContext();
    Repository repo = (Repository) context.lookup("java:comp/env/repository");

    DI 방식:

    public class Service {
        private Repository repository;
    
        // 외부에서 의존성 주입
        public Service(Repository repository) {
            this.repository = repository;
        }
    }
    • DI 방식에서는 객체가 스스로 의존성을 찾는 부담이 없고, 코드가 간결해집니다.
    • 이를 통해 유지보수와 테스트가 훨씬 쉬워지며, 코드는 더 직관적이고 이해하기 쉬워집니다.

4. Setter Injection과 Constructor Injection 🔄

DI에는 주입 방식에 따라 여러 유형이 있습니다. 대표적인 주입 방식은 Setter InjectionConstructor Injection입니다.

1) Setter Injection (세터 주입) 🛠️

  • 객체가 생성된 후, Setter 메서드를 통해 의존성을 주입하는 방식입니다.

    예시:

    public class Service {
        private Repository repository;
    
        public void setRepository(Repository repository) {
            this.repository = repository;  // 세터 주입
        }
    }

    장점:

    • 의존성 교체가 쉬움: 의존성을 나중에 변경할 수 있습니다.

    • 선택적 의존성 주입이 가능합니다.

      단점:

    • 객체가 완전한 초기화 상태가 보장되지 않을 수 있습니다.
      (Setter를 호출하지 않을 경우 문제가 발생할 수 있음)


2) Constructor Injection (생성자 주입) 🏗️

  • 객체가 생성될 때, 생성자를 통해 의존성을 주입받는 방식입니다.

    예시:

    public class Service {
        private Repository repository;
    
        public Service(Repository repository) {
            this.repository = repository;  // 생성자 주입
        }
    }

    장점:

    • 일관성 있는 초기화: 객체가 생성될 때 필요한 모든 의존성이 주입되므로, 완전한 초기화가 보장됩니다.

    • 불변성(immutability): 생성자 주입을 사용하면 객체를 불변하게 만들 수 있습니다.

      단점:

    • 생성자에 너무 많은 인자가 필요할 경우, 코드가 복잡해질 수 있습니다.


Dependency Injection의 장점 🌟

  • 느슨한 결합(loose coupling): 객체가 직접 의존성을 관리하지 않으므로, 서로 강하게 묶이지 않습니다.
  • 테스트 용이성: Mock 객체를 주입하여 단위 테스트가 간편해집니다.
  • 유지보수성 향상: 의존성을 쉽게 교체할 수 있어 유연한 코드 작성이 가능합니다.
  • 재사용성 증가: 객체가 외부 환경에 종속되지 않아 다양한 환경에서 재사용이 용이합니다.

아래는 이미지에 있는 Container에 대한 상세한 설명입니다. 컨테이너의 역할과 기능을 이해하면, 프레임워크나 애플리케이션 서버가 어떻게 객체를 관리하는지 깊이 이해할 수 있습니다.


Container (컨테이너)란? 🧳

Container객체의 생성, 사용, 소멸 등 객체의 생명주기(Lifecycle)를 관리하는 중요한 구조입니다.
프레임워크나 서버 환경에서 컨테이너는 객체의 생성과 관리를 담당하며, 객체 간 의존성(Dependency)도 자동으로 주입해 줍니다. 이 덕분에 개발자는 비즈니스 로직에만 집중할 수 있습니다.


1. Container란 무엇인가? 🧐

  • 객체의 생성, 사용, 소멸에 해당하는 라이프사이클(Lifecycle)을 관리합니다.
  • 애플리케이션이 정상적으로 동작하기 위해 필요한 주요 기능을 제공합니다.
    예를 들어, 스프링 프레임워크(Spring Framework)나 톰캣(Tomcat) 같은 컨테이너가 이를 수행합니다.

컨테이너의 역할:

  1. 객체가 필요할 때 생성하고 사용이 끝나면 소멸시킴.
  2. 객체 간 의존성(Dependency)을 주입해 결합도를 낮춤.
  3. 서버 애플리케이션 실행에 필요한 필수 환경을 제공합니다.

2. Container의 기능 ⚙️

컨테이너는 애플리케이션이 정상적으로 동작하도록 다음과 같은 기능을 수행합니다:

1) 라이프사이클 관리 (Lifecycle Management) 🌀

  • 객체를 필요할 때 생성하고 사용이 끝난 후 소멸합니다.
  • 이를 통해 개발자는 객체의 생명주기를 직접 관리할 필요가 없습니다.

2) Dependency 객체 제공 🎯

  • 의존성 주입(Dependency Injection)을 통해 객체 간 의존성을 자동으로 주입합니다.
  • 이를 통해 객체 간 느슨한 결합(loose coupling)을 유지합니다.

3) Thread 관리 🧵

  • 서버 애플리케이션에서 요청이 많을 때 스레드를 효율적으로 관리합니다.
  • 예를 들어, 톰캣 같은 컨테이너는 스레드 풀(Thread Pool)을 활용해 동시에 여러 요청을 처리합니다.

4) 기타 실행 환경 제공 🛠️

  • 애플리케이션이 구동되는 동안 필요한 환경 설정을 제공합니다.
    예를 들어, 데이터베이스 연결 풀, 트랜잭션 관리, 보안 설정 등을 지원합니다.

3. Container의 필요성 💡

컨테이너는 비즈니스 로직 외에 부가적인 기능을 독립적으로 관리할 수 있도록 설계되었습니다.
다음과 같은 이유로 컨테이너가 필요합니다:

1) 비즈니스 로직 외의 부가 기능 독립적 관리 🎛️

  • 객체의 생성, 의존성 관리, 스레드 관리와 같은 기능을 개발자가 직접 구현할 필요가 없습니다.
  • 컨테이너가 이러한 부가 기능을 독립적으로 관리함으로써, 개발자는 비즈니스 로직에만 집중할 수 있습니다.

2) 일관된 서비스 Lookup 및 Configuration 제공 🛠️

  • 애플리케이션에 필요한 자원들을 컨테이너를 통해 일관된 방식으로 조회할 수 있습니다.
  • Lookup이나 설정(Configuration)을 컨테이너가 대신 관리하므로, 환경이 바뀌더라도 코드 수정 없이 설정만 변경하면 됩니다.

3) Factory 및 Singleton 패턴의 자동 구현 🏭

  • 컨테이너를 사용하면 Factory 패턴이나 Singleton 패턴을 직접 구현할 필요가 없습니다.
    • 컨테이너가 객체를 싱글톤(Singleton)으로 관리하거나 필요할 때마다 팩토리 패턴처럼 객체를 생성합니다.
  • 이를 통해 객체의 생명주기와 메모리 관리가 효율적으로 이루어집니다.

4. 실제 예시: 스프링 컨테이너 🌿

스프링(Spring Framework)에서는 컨테이너가 애플리케이션의 Bean 객체를 관리합니다.

  • 스프링 컨테이너는 애플리케이션이 필요로 하는 객체들을 Bean으로 등록하고, 런타임 시점에 자동으로 주입합니다.

  • 스프링의 ApplicationContext가 대표적인 컨테이너 역할을 합니다.

    예시:

    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    MyService service = context.getBean("myService", MyService.class);
    • 이 코드에서는 스프링 컨테이너가 MyService 객체를 자동으로 생성하고 주입합니다.
    • 개발자는 new 키워드 없이 객체를 사용할 수 있습니다.

5. 컨테이너 사용의 장점 🌟

  • 코드의 재사용성 증가: 객체의 생명주기와 의존성을 컨테이너가 관리하므로, 코드 재사용이 용이합니다.
  • 유지보수와 테스트 용이: 객체 간 결합도가 낮아지기 때문에 테스트와 유지보수가 간편해집니다.
  • 환경 변화에 대한 유연성: 설정 파일이나 어노테이션만 변경하면 다른 환경에서도 쉽게 적응할 수 있습니다.

IoC Container (제어의 역행 컨테이너) 🛠️

IoC(Container)는 객체의 생성과 의존성 관리를 애플리케이션 코드가 아니라 컨테이너가 담당하는 구조입니다. 이 구조를 통해 객체 간 결합도를 낮추고 유연성을 높이는 것이 가능해집니다.


1. IoC Container의 역할 🎯

  • 객체의 생성, 관계 설정, 사용, 제거 등의 작업을 컨테이너가 자동으로 수행합니다.
  • 즉, 객체의 라이프사이클을 개발자가 직접 관리하지 않아도 되며, 컨테이너가 이 과정을 독립적으로 담당합니다.

예시:

public class MyService {
    private final Repository repository;

    public MyService(Repository repository) {
        this.repository = repository;  // 의존성 주입
    }
}
  • 위 코드에서 Repository 객체는 컨테이너가 생성하고, MyService주입합니다.
    개발자가 new 키워드를 사용하지 않아도 의존성이 자동으로 설정됩니다.

2. IoC란 무엇인가? (Inversion of Control) 🔄

  • IoC (제어의 역행)이란 객체의 생성과 의존성 관리의 제어권을 개발자가 아닌 컨테이너가 갖는 것을 의미합니다.
  • 기존에는 개발자가 직접 객체를 생성하고 관리했지만, IoC에서는 컨테이너가 객체 간 관계를 설정하고 관리합니다.

IoC의 핵심 개념:

  • 제어의 흐름이 애플리케이션 코드에서 컨테이너로 역전됩니다.
  • 객체가 자신이 사용할 의존성을 직접 생성하지 않고 필요한 객체를 외부에서 주입받아 사용합니다.

3. 스프링 컨테이너의 역할 🌿

  • 스프링 프레임워크에서 IoC를 담당하는 컨테이너IoC 컨테이너라고 부릅니다.
  • 스프링 컨테이너는 애플리케이션 실행에 필요한 모든 객체를 관리하고, 객체 간의 의존성을 주입해 줍니다.

4. 스프링의 주요 IoC 컨테이너

1️⃣ BeanFactory:

  • BeanFactory는 스프링 컨테이너의 가장 기본적인 형태입니다.

  • 지연 로딩(Lazy Loading)을 사용하여 객체가 필요할 때 생성됩니다.
    이 방식은 메모리를 절약할 수 있지만, 초기화가 느릴 수 있습니다.

    예시:

    BeanFactory factory = new XmlBeanFactory(new FileSystemResource("beans.xml"));
    MyService service = (MyService) factory.getBean("myService");

2️⃣ ApplicationContext:

  • ApplicationContextBeanFactory를 확장한 컨테이너로, 더 많은 기능을 제공합니다.

  • 즉시 로딩(Eager Loading)을 통해 애플리케이션 시작 시 모든 Bean을 미리 생성합니다.

  • 이 방식은 애플리케이션 초기화가 빠르며, 이벤트 리스너나 메시지 소스를 지원합니다.

    예시:

    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    MyService service = context.getBean(MyService.class);

5. IoC Container의 필요성 📌

  1. 비즈니스 로직과 부가적인 기능의 분리 ✂️
  • 객체의 생성, 의존성 주입, 스레드 관리 등의 부가적인 기능을 컨테이너가 관리합니다.
  • 개발자는 비즈니스 로직에만 집중할 수 있게 됩니다.
  1. 객체 간 결합도를 줄이고 유지보수성 향상 🛠️
  • 객체가 다른 객체를 직접 생성하지 않기 때문에, 결합도가 낮아지고 코드의 재사용이 용이해집니다.
  1. Factory와 Singleton 패턴의 자동 구현 🏭
  • IoC 컨테이너는 객체를 Singleton으로 관리할 수 있으므로, 개발자가 직접 싱글톤 패턴을 구현할 필요가 없습니다.
  • 또한, 객체 생성에 필요한 Factory 패턴을 컨테이너가 대신 처리해 줍니다.

6. IoC Container의 장점 🌟

  • 유연성: 설정 파일이나 어노테이션을 사용하여 객체 간 관계를 유연하게 변경할 수 있습니다.
  • 테스트 용이성: 객체 간 결합도가 낮아지기 때문에 Mock 객체를 사용한 단위 테스트가 간편해집니다.
  • 코드 간결화: 객체 생성과 의존성 주입을 컨테이너가 담당하므로, 코드가 간결해집니다.

Spring DI Container (스프링 의존성 주입 컨테이너) 🌿

Spring DI Container는 객체를 스프링에서 Bean이라고 부르며, 이 Bean들의 생명주기(Life-Cycle)를 관리합니다.

  • BeanFactoryApplicationContext는 스프링에서 사용하는 대표적인 IoC 컨테이너입니다.
  • BeanFactory는 기본적인 객체 생성 및 관리를 담당하고, 여기에 더 많은 기능을 추가한 것이 ApplicationContext입니다.

1. BeanFactory와 ApplicationContext란? 🛠️

1️⃣ BeanFactory:

  • BeanFactory는 스프링의 가장 기본적인 컨테이너입니다.
  • Lazy Loading(지연 로딩)을 통해 객체가 필요할 때만 생성합니다.
  • 객체의 등록, 생성, 조회, 반환을 담당합니다.

2️⃣ ApplicationContext:

  • ApplicationContextBeanFactory의 기능을 확장한 상위 컨테이너입니다.
  • 즉시 로딩(Eager Loading)을 통해 애플리케이션 시작 시 모든 Bean을 미리 생성합니다.
  • 부가적인 서비스(예: 이벤트 리스너, 국제화, 메시지 소스 지원)도 제공합니다.

2. Bean이란? 🌱

  • Bean스프링 컨테이너가 관리하는 객체를 의미합니다.
  • 이 객체들은 스프링 설정 파일(XML, Java Config) 또는 어노테이션으로 정의됩니다.

Bean은 스프링 애플리케이션에서 핵심적인 역할을 하며, 컨테이너가 이들의 생명주기를 관리하고 의존성 주입(DI)을 통해 객체 간 관계를 설정합니다.


3. BeanFactory의 특징과 역할 ⚙️

  • Bean 등록, 생성, 조회, 반환 등의 기본적인 기능을 담당합니다.

  • getBean() 메서드를 통해 Bean을 가져올 수 있습니다.

    예시:

    BeanFactory factory = new XmlBeanFactory(new FileSystemResource("beans.xml"));
    MyService service = (MyService) factory.getBean("myService");
  • 이처럼 BeanFactory는 요청이 있을 때 객체를 생성하는 지연 로딩(Lazy Loading) 방식을 사용합니다.

장점:

  • 메모리 효율이 높음.
    단점:
  • 부가 기능이 부족하며, 대규모 애플리케이션에 적합하지 않음.

4. ApplicationContext의 특징과 역할 📋

  • BeanFactory의 기능을 모두 포함하면서 부가적인 서비스를 제공합니다.

  • 즉시 로딩(Eager Loading) 방식을 사용해 애플리케이션 초기화 시점에 모든 Bean을 미리 생성합니다.

    예시:

    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    MyService service = context.getBean(MyService.class);
  • ApplicationContext는 이벤트 리스너, 메시지 소스, 국제화 기능 등 다양한 부가 기능을 제공합니다.

장점:

  • 대규모 애플리케이션에 적합하며 다양한 부가 기능 제공.
    단점:
  • 모든 Bean을 초기화할 때 메모리와 시간이 많이 소요될 수 있음.

5. BeanFactory vs. ApplicationContext 비교 🔄

구분BeanFactoryApplicationContext
로딩 방식지연 로딩 (Lazy Loading)즉시 로딩 (Eager Loading)
부가 기능없음이벤트 리스너, 메시지 소스, 국제화 기능 등 제공
사용 목적간단한 애플리케이션대규모 애플리케이션
예시XmlBeanFactory 사용ClassPathXmlApplicationContext 사용
메모리 효율성상대적으로 높음모든 Bean을 미리 로딩하므로 메모리 사용 증가

6. 스프링에서의 ApplicationContext 종류 🧩

스프링은 여러 종류의 ApplicationContext 구현 클래스를 제공합니다:

1️⃣ ClassPathXmlApplicationContext

  • 클래스 경로에 있는 XML 설정 파일을 읽어들여 Bean을 생성합니다.

2️⃣ FileSystemXmlApplicationContext

  • 파일 시스템 경로에 있는 XML 설정 파일을 사용합니다.

3️⃣ AnnotationConfigApplicationContext

  • Java Config 클래스를 사용해 설정된 Bean을 로드합니다.

7. Spring DI Container의 장점 🌟

  1. 유연한 객체 관리
  • 의존성을 컨테이너가 관리하므로, 객체 간 결합도가 낮아집니다.
  1. 테스트 용이성
  • Mock 객체를 주입해 단위 테스트를 쉽게 수행할 수 있습니다.
  1. 다양한 부가 기능 제공
  • ApplicationContext는 이벤트 리스너, 국제화, 메시지 소스 등 여러 기능을 지원합니다.

BeanFactory와 ApplicationContext 계층 구조 🌱

스프링 프레임워크의 핵심 컨테이너BeanFactory와 ApplicationContext입니다.
이 두 인터페이스는 스프링 애플리케이션에서 객체의 생성, 의존성 주입, 생명주기 관리 등의 기능을 담당합니다.
이제 이들 사이의 계층 구조와 각각의 역할을 상세히 살펴보겠습니다.


1. BeanFactory 인터페이스 🏗️

BeanFactory스프링의 가장 기본적인 IoC 컨테이너입니다. 객체(Bean)를 생성, 관리하고 필요할 때만 초기화하는 지연 로딩(Lazy Loading) 방식을 사용합니다.

  • 기능:
    • Bean을 등록, 생성, 조회, 반환하는 역할을 담당합니다.
    • 주로 소규모 애플리케이션에 적합합니다.

2. ApplicationContext 인터페이스 🌿

ApplicationContextBeanFactory의 기능을 확장한 상위 인터페이스입니다.
애플리케이션 초기화 시점에 모든 Bean을 생성하는 즉시 로딩(Eager Loading) 방식을 사용하며, 다양한 부가 기능을 제공합니다.

  • 주요 부가 기능:
    • 이벤트 관리 (Event Handling)
    • 메시지 소스 (Message Source)와 국제화 (I18n) 지원
    • 리소스 로딩 (Resource Loading) 기능

3. ListableBeanFactory 인터페이스 🗂️

  • ListableBeanFactory는 등록된 모든 Bean의 목록을 조회할 수 있는 기능을 제공합니다.

  • 즉, 애플리케이션에 어떤 Bean이 등록되어 있는지 전체 목록을 확인할 수 있습니다.

    예시:

    String[] beanNames = context.getBeanDefinitionNames();
    for (String name : beanNames) {
        System.out.println(name);
    }

4. ApplicationEventPublisher 인터페이스 📢

  • ApplicationEventPublisher스프링의 이벤트 관리 기능을 제공합니다.

  • 애플리케이션 내에서 발생하는 이벤트를 리스너에게 전달합니다.

    예시:

    context.publishEvent(new MyCustomEvent(this, "Custom Event Triggered"));

5. MessageSource 인터페이스 💬

  • MessageSource메시지 소스와 국제화(I18n)를 지원합니다.

  • 다국어 환경을 지원할 때, 언어별 메시지 파일을 관리합니다.

    예시:

    String message = context.getMessage("welcome.message", null, Locale.KOREA);
    System.out.println(message);

6. ResourceLoader 인터페이스 📂

  • ResourceLoader는 다양한 리소스(XML, 파일 등)를 애플리케이션에 로딩할 수 있는 기능을 제공합니다.

    예시:

    Resource resource = context.getResource("classpath:application.properties");

7. WebApplicationContext 인터페이스 🌐

  • WebApplicationContext웹 애플리케이션에 특화된 IoC 컨테이너입니다.

  • ServletContext와 통합되어, 웹 애플리케이션의 Bean을 관리합니다.

    주요 구현체:

    • XmlWebApplicationContext: XML 설정 파일을 사용해 웹 애플리케이션의 Bean을 관리합니다.

8. StaticApplicationContext와 GenericXmlApplicationContext 🛠️

  • StaticApplicationContext: 정적인 환경에서 테스트용으로 사용됩니다.
  • GenericXmlApplicationContext: XML 설정 파일을 로딩하여 다양한 Bean을 관리하는 컨텍스트입니다.

9. 계층 구조 정리 🗺️

  • BeanFactory스프링의 기본 IoC 컨테이너로, 지연 로딩을 통해 객체를 관리합니다.
  • ApplicationContext는 BeanFactory의 기능을 확장하며, 다양한 부가 기능을 제공합니다.
  • WebApplicationContext는 웹 애플리케이션 환경에서 사용되며, 서블릿 컨텍스트와 통합됩니다.

10. BeanFactory vs. ApplicationContext 비교 🔄

구분BeanFactoryApplicationContext
로딩 방식지연 로딩 (Lazy Loading)즉시 로딩 (Eager Loading)
사용 환경간단한 애플리케이션대규모 애플리케이션
부가 기능없음이벤트 관리, 국제화 지원 등 다양한 기능 제공
웹 애플리케이션 지원지원 안 함WebApplicationContext 사용

IoC(제어의 역전) 개념 🔄

1. 객체 제어 방식

1️⃣ 기존 방식:

  • 필요한 위치에서 개발자가 직접 객체를 생성하는 방식입니다.

  • 개발자가 클래스 내부에서 new 키워드를 사용해 객체를 명시적으로 생성하고 사용합니다.

    예시 (기존 방식):

    Repository repository = new Repository();
    Service service = new Service(repository);  // 직접 객체 생성 및 주입
  • 이 방식은 간단한 프로그램에서는 유용하지만, 의존성이 많아질수록 관리가 복잡해지고 유지보수에 어려움이 발생합니다.

2️⃣ IoC 방식:

  • IoC(제어의 역전)에서는 객체 생성과 의존성 관리를 컨테이너에 위임합니다.

  • 개발자는 객체의 생성과 주입 로직을 관리하지 않고, 컨테이너가 대신 객체를 관리합니다.

    예시 (IoC 사용):

    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    Service service = context.getBean(Service.class);  // 컨테이너가 객체 생성 및 주입
  • 컨테이너가 제어권을 가지는 것이 바로 IoC입니다.
    개발자는 비즈니스 로직에 집중할 수 있으며, 객체 간의 결합도를 낮출 수 있습니다.


2. IoC 사용에 따른 장점 🌟

  • 객체 간 결합도를 줄여주는 효과를 얻을 수 있습니다.
    이를 느슨한 결합(loose coupling)이라고 부르며, 객체가 다른 객체와 최소한의 의존성만 가지도록 설계할 수 있습니다.

  • Loose Coupling의 중요성:

    • 변경 용이성: 하나의 클래스가 변경되더라도, 다른 클래스에 미치는 영향이 최소화됩니다.
    • 테스트 용이성: 객체 간 결합도가 낮아지면, 테스트 시 Mock 객체를 쉽게 주입할 수 있습니다.

3. 객체 간 결합도가 높으면 발생하는 문제 ⚠️

  • 결합도가 높을 때 문제점:

    • 만약 하나의 클래스가 변경되거나 유지보수될 때, 해당 클래스와 강하게 결합된 다른 클래스함께 수정해야 할 가능성이 높아집니다.

    • 이런 상황은 코드의 유지보수성을 크게 저하시키며, 큰 애플리케이션일수록 문제의 규모가 커집니다.

      예시:

      public class Service {
          private Repository repository = new Repository();  // 강한 결합
      
          public void doSomething() {
              repository.save();
          }
      }
      • 위 코드에서는 Service 클래스가 Repository 객체를 직접 생성하고 사용합니다.
        만약 Repository의 로직이 변경되면, Service 클래스도 수정이 필요합니다.
  • IoC를 사용하면:

    • 객체가 직접 의존성을 생성하지 않고, 외부에서 주입받으므로, 클래스 간 결합도가 낮아집니다.
    • 유지보수가 쉬워지고, 코드 변경에 유연하게 대응할 수 있습니다.

4. IoC와 유지보수의 관계 🛠️

  • 유지보수가 어려운 코드는 서로 강하게 결합된 객체들로 인해, 작은 수정에도 큰 영향을 미칩니다.
  • 반면에, IoC를 사용한 코드는 객체 간의 의존성이 느슨하므로, 하나의 객체가 변경되더라도 다른 객체에 미치는 영향이 최소화됩니다.

IoC 개념: 결합도를 낮춰 효율적인 객체 관리하기 🌱


1. 강한 결합(Tightly-Coupling)

강한 결합이란 한 객체가 다른 객체를 직접 생성하고 호출하는 방식으로, 코드 간 의존성이 높아지는 문제를 말합니다. 이로 인해 유지보수와 확장성이 저해됩니다.

강한 결합 예제

public class Main {
    public static void main(String[] args) {
        GreetingServiceKor greetingService = new GreetingServiceKor();  // 직접 생성
        String message = greetingService.greet("John");
        System.out.println(message);
    }
}

문제점:

  • 의존성 변경: GreetingServiceKorGreetingServiceEng로 변경될 경우, Main 클래스도 함께 수정해야 함.
  • 확장성 저하: 다른 서비스 추가 시 코드 변경이 필요.
  • 테스트 어려움: GreetingServiceKor 대신 Mock 객체를 주입하기 어려움.

2. 느슨한 결합(Loosely-Coupling) 적용하기

인터페이스의존성 주입(DI)을 사용하여 강한 결합을 느슨하게 바꿉니다.

인터페이스 사용 예제

  1. 인터페이스 정의:
public interface GreetingService {
    String greet(String name);
}
  1. 구현 클래스 작성:
public class GreetingServiceKor implements GreetingService {
    public String greet(String name) {
        return name + "님, 안녕하세요!";
    }
}

public class GreetingServiceEng implements GreetingService {
    public String greet(String name) {
        return "Hello, " + name + "!";
    }
}
  1. Main 클래스에서 인터페이스 사용:
public class Main {
    public static void main(String[] args) {
        GreetingService greetingService = new GreetingServiceKor();  // 다형성 활용
        String message = greetingService.greet("John");
        System.out.println(message);
    }
}

장점:

  • 다형성 활용: 서비스 교체가 간편해짐.
  • 결합도 감소: 코드의 유연성과 확장성이 증가.
  • 테스트 용이: Mock 객체를 사용한 단위 테스트가 가능.

3. Factory 패턴을 통한 결합도 감소

Factory를 활용하면 객체 생성을 외부에서 관리하여, 클래스가 객체 생성 로직을 알 필요가 없습니다.

Factory 예제

  1. GreetingServiceFactory 클래스:
public class GreetingServiceFactory {
    public static GreetingService getGreetingService(String lang) {
        if ("kor".equals(lang)) {
            return new GreetingServiceKor();
        } else if ("eng".equals(lang)) {
            return new GreetingServiceEng();
        } else {
            return null;
        }
    }
}
  1. Main 클래스에서 Factory 사용:
public class Main {
    public static void main(String[] args) {
        GreetingService service = GreetingServiceFactory.getGreetingService("kor");
        String message = service.greet("Alice");
        System.out.println(message);
    }
}

장점:

  • 유지보수 용이: 서비스가 추가/변경될 때 Factory만 수정하면 됨.
  • Main 클래스의 복잡성 감소: 객체 생성과 관련된 로직 제거.

4. 외부 조립기(Assembler)로 의존성 관리

Assembler(조립기)는 객체의 생성과 의존성 설정을 외부에서 수행하여, 클래스 간 결합도를 더욱 낮춥니다.

Assembler 예제

  1. Assembler 클래스:
public class Assembler {
    public GreetingService getGreetingService() {
        return new GreetingServiceKor();  // 필요에 따라 다른 서비스 반환 가능
    }
}
  1. Main 클래스에서 Assembler 사용:
public class Main {
    public static void main(String[] args) {
        Assembler assembler = new Assembler();
        GreetingService service = assembler.getGreetingService();
        String message = service.greet("Alice");
        System.out.println(message);
    }
}

5. Spring Container의 외부 조립기 역할 🚀

Spring IoC 컨테이너는 Assembler와 유사하게 객체의 생성과 의존성 관리를 수행합니다.
Spring을 사용하면 객체 간 의존성을 외부에서 설정하고, 객체 생성을 자동으로 처리해 줍니다.

Spring 설정 예제 (XML):

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="kor" class="GreetingServiceKor"/>
    <bean id="eng" class="GreetingServiceEng"/>
</beans>

Main 클래스에서 Spring 컨테이너 사용:

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        
        GreetingService service = (GreetingService) context.getBean("kor");
        String message = service.greet("Alice");
        System.out.println(message);
    }
}

Spring Container의 이점 🌟

  1. 객체 관리 자동화: 개발자가 객체 생성을 신경 쓰지 않아도 됨.
  2. 의존성 주입(DI): 설정 파일(XML) 또는 어노테이션으로 객체 간 의존성 설정 가능.
  3. 유연성 극대화: 객체 교체와 테스트가 간편해지고, 코드 재사용성 증가.

Spring DI 용어 정리 🌱

1. 빈(Bean)

  • Bean은 스프링이 IoC 방식으로 관리하는 객체를 의미합니다.
  • 스프링 컨테이너가 직접 생성과 제어를 담당하는 객체들만 Bean이라고 부릅니다.
  • 일반적으로 POJO(Plain Old Java Object)로 정의됩니다.
    • POJO: 특별한 규약 없이 간단한 클래스로 작성된 객체를 의미합니다.

예시:

public class UserService {
    public void printServiceName() {
        System.out.println("User Service Bean");
    }
}

2. 빈 팩토리(BeanFactory)

  • BeanFactory스프링의 핵심 IoC 컨테이너로, Bean을 등록, 생성, 조회, 반환하는 기능을 담당합니다.
  • 지연 로딩(Lazy Loading) 방식을 사용하여, 요청이 있을 때만 객체를 생성합니다.
  • 주로 직접 사용하기보다는 확장된 ApplicationContext를 사용합니다.

BeanFactory의 역할:

  • 작은 애플리케이션에서 효율적인 메모리 사용이 가능.
  • 그러나 부가 기능이 부족하여, 대규모 애플리케이션에서는 잘 사용되지 않습니다.

3. 애플리케이션 컨텍스트(ApplicationContext)

  • ApplicationContextBeanFactory를 확장한 IoC 컨테이너입니다.
  • BeanFactory의 기본 기능에 더해 부가 서비스(국제화 지원, 이벤트 처리 등)를 제공합니다.
  • 즉시 로딩(Eager Loading) 방식을 사용하여, 애플리케이션 시작 시점에 모든 Bean을 미리 생성합니다.

BeanFactory vs ApplicationContext:

  • BeanFactory: 주로 빈 생성과 관리에 초점을 맞춤.
  • ApplicationContext: 애플리케이션 지원 기능을 포함한 더 넓은 기능 제공.

4. 설정 정보 / 설정 메타정보 (Configuration Metadata)

  • 설정 정보스프링 컨테이너가 IoC를 적용하기 위해 사용하는 메타정보입니다.
  • 설정 정보는 XML 파일, 어노테이션, 자바 설정 클래스 등을 통해 제공될 수 있습니다.
  • 이 메타정보는 Bean의 생성, 의존성 주입과 같은 과정을 설정합니다.

예시 (XML 설정):

<bean id="userService" class="com.example.UserService"/>

예시 (Java 설정):

@Configuration
public class AppConfig {
    @Bean
    public UserService userService() {
        return new UserService();
    }
}

5. 스프링 프레임워크

  • 스프링 프레임워크IoC 컨테이너와 ApplicationContext를 포함해 스프링이 제공하는 모든 기능을 통칭합니다.
  • IoC, AOP(Aspect-Oriented Programming), 트랜잭션 관리, 데이터 접근 기능 등을 제공합니다.
  • 모듈화된 구조를 갖추고 있어, 필요한 부분만 선택적으로 사용할 수 있습니다.

예시: 스프링 프레임워크의 주요 모듈:

  • Spring Core: IoC 컨테이너와 기본적인 의존성 주입 제공.
  • Spring MVC: 웹 애플리케이션 개발을 위한 MVC 프레임워크.
  • Spring Data: 데이터베이스 접근을 단순화하는 모듈.

🌱 Spring Container: IoC 컨테이너의 역할과 구조 완벽 이해하기


📝 Preview (2줄 요약)

Spring의 BeanFactory, ApplicationContext, WebApplicationContext는 객체의 생성과 생명주기 관리를 담당합니다. 이 글에서는 스프링 컨테이너의 계층 구조와 사용 예시를 알아봅니다.


1. BeanFactory 🏗️

BeanFactory는 스프링의 가장 기본적인 IoC 컨테이너입니다. 객체의 생성과 의존성 주입을 담당하며, 지연 로딩(Lazy Loading) 방식을 사용합니다. 이는 필요한 시점에만 객체를 생성하므로 메모리 사용을 최소화할 수 있습니다.

BeanFactory의 주요 기능:

  • Bean 등록 및 생성: 여러 형태의 Bean을 생성하고 제공합니다.
  • 의존성 설정: 객체 간의 연관 관계를 설정합니다.
  • 요청 시 Bean 반환: 클라이언트의 요청에 따라 Bean을 제공합니다.
  • 생명주기 관리: 객체 생성부터 소멸까지 관리합니다.

2. ApplicationContext 🚀

ApplicationContextBeanFactory를 확장한 상위 IoC 컨테이너로, 부가 기능을 제공합니다. 모든 Bean을 즉시 로딩(Eager Loading) 방식으로 미리 생성하여 빠른 응답을 지원합니다.

ApplicationContext의 주요 기능:

  • BeanFactory의 모든 기능 제공: 기본적인 Bean 생성 및 관리 기능 포함.
  • 국제화 지원 (I18N): 다국어 애플리케이션 지원 기능.
  • 이벤트 처리: 애플리케이션에서 이벤트를 발생시키고 관리합니다.
  • 리소스 로딩: 외부 리소스 파일을 로딩합니다.
  • 모든 Bean을 메모리에 로딩: 초기화 시점에 미리 Bean을 준비해 둡니다.

사용 예시:

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
GreetingService service = (GreetingService) context.getBean("greetingService");
service.greet("Alice");

3. WebApplicationContext 🌐

WebApplicationContext웹 애플리케이션 환경에 특화된 ApplicationContext입니다.

  • 스프링 MVC와 함께 사용되며, HTTP 요청 처리컨트롤러와의 연결을 담당합니다.
  • 웹 환경에서 가장 많이 사용하는 구현체는 XmlWebApplicationContext입니다.

WebApplicationContext의 주요 기능:

  • Spring MVC 통합: DispatcherServlet과 함께 웹 애플리케이션을 구성합니다.
  • HTTP 요청 처리: 컨트롤러와 서비스를 연결하고 주입합니다.
  • 다양한 설정 지원: XML, 자바 설정, 어노테이션 등 다양한 방법을 제공합니다.

Spring Container 계층 구조 정리 🛠️

BeanFactory  
  └── ApplicationContext  
        └── WebApplicationContext
  1. BeanFactory: 기본적인 IoC 컨테이너로 객체 생성과 관리를 담당합니다.
  2. ApplicationContext: BeanFactory를 확장해 부가 기능을 추가 제공합니다.
  3. WebApplicationContext: 웹 환경에서 필요한 기능을 추가한 컨텍스트입니다.

0개의 댓글