프레임워크가 제어권을 넘겨받으면 개발자는 무엇이 달라질까요? IoC와 DI의 핵심 개념과 구현 방식을 함께 알아봅시다! 🚀
IoC란 제어의 흐름을 개발자가 아닌 외부 프레임워크가 담당하게 하는 개념입니다.
기존에는 코드가 객체의 생성과 연결을 직접 관리했지만, IoC에서는 이를 외부가 대신 수행하여 개발자가 더 중요한 로직에 집중할 수 있도록 돕습니다.
IoC를 통해 객체 간의 관계는 런타임에 결정됩니다.
A a = new A();
B b = new B(a); // 직접 생성 (전통적인 방식)
대신, IoC에서는 설정 파일이나 어노테이션을 통해 객체 간의 의존성을 정의하고,
프레임워크가 런타임에 주입합니다. 이 과정에서 개발자는 세부 구현을 신경 쓰지 않아도 됩니다.
IoC를 구현하는 대표적인 방식인 DI(Dependency Injection)에는 다양한 유형이 있습니다.
객체가 생성될 때, 필요한 의존성을 생성자에서 받습니다.
public class B {
private A a;
public B(A a) {
this.a = a; // 생성자 주입
}
}
객체 생성 후 세터 메서드를 통해 의존성을 주입합니다.
public class B {
private A a;
public void setA(A a) {
this.a = a; // 세터 주입
}
}
의존성을 필요로 하는 객체가 주입 인터페이스를 구현합니다.
1️⃣ 재사용성이 높아집니다.
2️⃣ 유지보수와 테스트가 용이해집니다. (Mock 객체를 통한 유닛 테스트 가능)
3️⃣ 설정 변경만으로도 쉽게 객체 관계를 바꿀 수 있습니다.
의존성 주입(DI)은 왜 중요할까? IoC의 두 가지 대표적인 구현 방식인 Dependency Lookup과 Dependency Injection을 명확하게 비교해 봅시다!
IoC(Inversion of Control, 제어의 역행)는 크게 두 가지로 나눌 수 있습니다.
이제 각각의 특징과 예시를 알아보겠습니다. 🔍
Dependency Lookup은 객체가 필요한 의존성을 직접 조회하는 방식입니다.
💡 단점:
객체가 직접 외부 자원을 조회해야 하므로 코드가 복잡해지고, 외부 환경과의 강한 결합이 발생할 수 있습니다.
Dependency Injection에서는 객체가 스스로 의존성을 찾지 않고 외부에서 주입받습니다.
이 방식은 코드의 결합도를 낮추고 유지보수성을 높이는 데 매우 유리합니다.
객체 생성 후, 세터 메서드를 통해 의존성을 주입하는 방식입니다.
public class B {
private A a;
public void setA(A a) {
this.a = a; // 세터 주입
}
}
💡 장점:
의존성을 쉽게 교체할 수 있으며 유연성이 뛰어납니다.
객체가 생성될 때 생성자를 통해 의존성을 전달받는 방식입니다.
public class B {
private A a;
public B(A a) {
this.a = a; // 생성자 주입
}
}
💡 장점:
객체 생성과 동시에 모든 의존성이 주입되기 때문에 일관된 상태를 유지할 수 있습니다.
의존성이 필요할 때마다 메서드 호출 시점에 전달되는 방식입니다.
public class B {
public void useDependency(A a) {
a.execute(); // 필요한 시점에 의존성 사용
}
}
💡 장점:
메서드를 호출할 때만 의존성을 사용하므로 메모리 절약에 유리합니다.
| 구분 | Dependency Lookup | Dependency Injection |
|---|---|---|
| 제어 흐름 | 객체가 직접 의존성을 찾음 | 외부에서 의존성을 주입함 |
| 코드 결합도 | 상대적으로 강한 결합 | 느슨한 결합 |
| 유지보수 | 어렵고 복잡함 | 유지보수가 쉬움 |
| 테스트 용이성 | Mock 객체 사용이 어려움 | 테스트가 용이함 (Mock 주입 가능) |
| 예시 | JNDI Lookup | Setter, Constructor, Method Injection |
스프링(Spring) 프레임워크는 DI(Dependency Injection) 방식을 사용해 개발자가 객체 생성과 의존성 관리를 신경 쓰지 않도록 돕습니다. 개발자는 비즈니스 로직에만 집중하고, 스프링이 객체의 생성과 주입을 관리합니다.
컨테이너에서 필요한 객체를 직접 찾는 방법!
Dependency Lookup 방식의 특징과 코드 예제, 그리고 한계점까지 자세히 알아봅니다. 🚀
Dependency Lookup이란 객체가 직접 필요한 의존성을 컨테이너의 Lookup Context를 통해 조회하는 방식입니다. 이 방법은 자바의 JNDI(Java Naming and Directory Interface)와 같은 메커니즘을 사용해 외부 자원을 검색합니다.
InitialContext context = new InitialContext();
DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/MyDB");
Object obj = context.lookup("java:comp/env/jdbc/MyDB");
DataSource ds = (DataSource) obj; // 명시적 캐스팅 필요
주의할 점: 잘못된 타입으로 캐스팅하면 ClassCastException 예외가 발생할 수 있습니다.
NamingException이 발생합니다.try {
DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/MyDB");
} catch (NamingException e) {
e.printStackTrace(); // 예외 처리
}
| 구분 | Dependency Lookup | Dependency Injection |
|---|---|---|
| 제어 흐름 | 객체가 스스로 의존성을 조회 | 외부에서 의존성을 주입함 |
| 유연성 | 특정 환경에 종속됨 | 다양한 환경에 유연하게 대응 |
| 테스트 용이성 | 테스트가 어려움 | 테스트가 용이 (Mock 주입 가능) |
| 예시 | JNDI Lookup | Setter, Constructor 주입 방식 |
스프링 프레임워크는 DI(Dependency Injection) 방식을 사용해 코드의 유연성과 유지보수성을 극대화합니다.
Dependency Injection (DI)는 객체가 직접 의존성을 조회하지 않고, 외부에서 필요한 의존성을 주입받는 방식입니다. 이 방식을 통해 객체 간 결합도를 낮추고, 유지보수와 테스트가 쉬워지도록 설계합니다.
Dependency Injection에서는 객체 내부에서 lookup() 메서드와 같은 코드를 사용하지 않습니다.
필요한 의존성(Dependency)을 객체 스스로 찾지 않고, 컨테이너가 외부에서 의존성을 주입해줍니다.
이를 통해 개발자는 객체의 비즈니스 로직에만 집중할 수 있으며, 의존성 관리의 복잡성은 프레임워크나 컨테이너가 담당합니다.
예시:
public class Service {
private Repository repository;
// 생성자 주입
public Service(Repository repository) {
this.repository = repository;
}
}
위 코드에서 Service 객체는 스스로 Repository를 찾지 않고, 외부에서 주입받는 형태로 동작합니다.
A가 B를 의존한다고 할 때, 객체 A는 B가 어디서 왔는지, 어떤 방식으로 생성되었는지 몰라도 됩니다.전통적인 방식에서는 객체가 직접 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에는 주입 방식에 따라 여러 유형이 있습니다. 대표적인 주입 방식은 Setter Injection과 Constructor Injection입니다.
객체가 생성된 후, Setter 메서드를 통해 의존성을 주입하는 방식입니다.
예시:
public class Service {
private Repository repository;
public void setRepository(Repository repository) {
this.repository = repository; // 세터 주입
}
}
장점:
의존성 교체가 쉬움: 의존성을 나중에 변경할 수 있습니다.
선택적 의존성 주입이 가능합니다.
단점:
객체가 완전한 초기화 상태가 보장되지 않을 수 있습니다.
(Setter를 호출하지 않을 경우 문제가 발생할 수 있음)
객체가 생성될 때, 생성자를 통해 의존성을 주입받는 방식입니다.
예시:
public class Service {
private Repository repository;
public Service(Repository repository) {
this.repository = repository; // 생성자 주입
}
}
장점:
일관성 있는 초기화: 객체가 생성될 때 필요한 모든 의존성이 주입되므로, 완전한 초기화가 보장됩니다.
불변성(immutability): 생성자 주입을 사용하면 객체를 불변하게 만들 수 있습니다.
단점:
생성자에 너무 많은 인자가 필요할 경우, 코드가 복잡해질 수 있습니다.
아래는 이미지에 있는 Container에 대한 상세한 설명입니다. 컨테이너의 역할과 기능을 이해하면, 프레임워크나 애플리케이션 서버가 어떻게 객체를 관리하는지 깊이 이해할 수 있습니다.
Container는 객체의 생성, 사용, 소멸 등 객체의 생명주기(Lifecycle)를 관리하는 중요한 구조입니다.
프레임워크나 서버 환경에서 컨테이너는 객체의 생성과 관리를 담당하며, 객체 간 의존성(Dependency)도 자동으로 주입해 줍니다. 이 덕분에 개발자는 비즈니스 로직에만 집중할 수 있습니다.
컨테이너는 애플리케이션이 정상적으로 동작하도록 다음과 같은 기능을 수행합니다:
컨테이너는 비즈니스 로직 외에 부가적인 기능을 독립적으로 관리할 수 있도록 설계되었습니다.
다음과 같은 이유로 컨테이너가 필요합니다:
스프링(Spring Framework)에서는 컨테이너가 애플리케이션의 Bean 객체를 관리합니다.
스프링 컨테이너는 애플리케이션이 필요로 하는 객체들을 Bean으로 등록하고, 런타임 시점에 자동으로 주입합니다.
스프링의 ApplicationContext가 대표적인 컨테이너 역할을 합니다.
예시:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyService service = context.getBean("myService", MyService.class);
MyService 객체를 자동으로 생성하고 주입합니다.new 키워드 없이 객체를 사용할 수 있습니다.IoC(Container)는 객체의 생성과 의존성 관리를 애플리케이션 코드가 아니라 컨테이너가 담당하는 구조입니다. 이 구조를 통해 객체 간 결합도를 낮추고 유연성을 높이는 것이 가능해집니다.
public class MyService {
private final Repository repository;
public MyService(Repository repository) {
this.repository = repository; // 의존성 주입
}
}
Repository 객체는 컨테이너가 생성하고, MyService에 주입합니다.new 키워드를 사용하지 않아도 의존성이 자동으로 설정됩니다.1️⃣ BeanFactory:
BeanFactory는 스프링 컨테이너의 가장 기본적인 형태입니다.
지연 로딩(Lazy Loading)을 사용하여 객체가 필요할 때 생성됩니다.
이 방식은 메모리를 절약할 수 있지만, 초기화가 느릴 수 있습니다.
예시:
BeanFactory factory = new XmlBeanFactory(new FileSystemResource("beans.xml"));
MyService service = (MyService) factory.getBean("myService");
2️⃣ ApplicationContext:
ApplicationContext는 BeanFactory를 확장한 컨테이너로, 더 많은 기능을 제공합니다.
즉시 로딩(Eager Loading)을 통해 애플리케이션 시작 시 모든 Bean을 미리 생성합니다.
이 방식은 애플리케이션 초기화가 빠르며, 이벤트 리스너나 메시지 소스를 지원합니다.
예시:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyService service = context.getBean(MyService.class);
Spring DI Container는 객체를 스프링에서 Bean이라고 부르며, 이 Bean들의 생명주기(Life-Cycle)를 관리합니다.
1️⃣ BeanFactory:
2️⃣ ApplicationContext:
Bean은 스프링 애플리케이션에서 핵심적인 역할을 하며, 컨테이너가 이들의 생명주기를 관리하고 의존성 주입(DI)을 통해 객체 간 관계를 설정합니다.
Bean 등록, 생성, 조회, 반환 등의 기본적인 기능을 담당합니다.
getBean() 메서드를 통해 Bean을 가져올 수 있습니다.
예시:
BeanFactory factory = new XmlBeanFactory(new FileSystemResource("beans.xml"));
MyService service = (MyService) factory.getBean("myService");
이처럼 BeanFactory는 요청이 있을 때 객체를 생성하는 지연 로딩(Lazy Loading) 방식을 사용합니다.
장점:
BeanFactory의 기능을 모두 포함하면서 부가적인 서비스를 제공합니다.
즉시 로딩(Eager Loading) 방식을 사용해 애플리케이션 초기화 시점에 모든 Bean을 미리 생성합니다.
예시:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyService service = context.getBean(MyService.class);
ApplicationContext는 이벤트 리스너, 메시지 소스, 국제화 기능 등 다양한 부가 기능을 제공합니다.
장점:
| 구분 | BeanFactory | ApplicationContext |
|---|---|---|
| 로딩 방식 | 지연 로딩 (Lazy Loading) | 즉시 로딩 (Eager Loading) |
| 부가 기능 | 없음 | 이벤트 리스너, 메시지 소스, 국제화 기능 등 제공 |
| 사용 목적 | 간단한 애플리케이션 | 대규모 애플리케이션 |
| 예시 | XmlBeanFactory 사용 | ClassPathXmlApplicationContext 사용 |
| 메모리 효율성 | 상대적으로 높음 | 모든 Bean을 미리 로딩하므로 메모리 사용 증가 |
스프링은 여러 종류의 ApplicationContext 구현 클래스를 제공합니다:
1️⃣ ClassPathXmlApplicationContext
2️⃣ FileSystemXmlApplicationContext
3️⃣ AnnotationConfigApplicationContext
스프링 프레임워크의 핵심 컨테이너는 BeanFactory와 ApplicationContext입니다.
이 두 인터페이스는 스프링 애플리케이션에서 객체의 생성, 의존성 주입, 생명주기 관리 등의 기능을 담당합니다.
이제 이들 사이의 계층 구조와 각각의 역할을 상세히 살펴보겠습니다.
BeanFactory는 스프링의 가장 기본적인 IoC 컨테이너입니다. 객체(Bean)를 생성, 관리하고 필요할 때만 초기화하는 지연 로딩(Lazy Loading) 방식을 사용합니다.
ApplicationContext는 BeanFactory의 기능을 확장한 상위 인터페이스입니다.
애플리케이션 초기화 시점에 모든 Bean을 생성하는 즉시 로딩(Eager Loading) 방식을 사용하며, 다양한 부가 기능을 제공합니다.
ListableBeanFactory는 등록된 모든 Bean의 목록을 조회할 수 있는 기능을 제공합니다.
즉, 애플리케이션에 어떤 Bean이 등록되어 있는지 전체 목록을 확인할 수 있습니다.
예시:
String[] beanNames = context.getBeanDefinitionNames();
for (String name : beanNames) {
System.out.println(name);
}
ApplicationEventPublisher는 스프링의 이벤트 관리 기능을 제공합니다.
애플리케이션 내에서 발생하는 이벤트를 리스너에게 전달합니다.
예시:
context.publishEvent(new MyCustomEvent(this, "Custom Event Triggered"));
MessageSource는 메시지 소스와 국제화(I18n)를 지원합니다.
다국어 환경을 지원할 때, 언어별 메시지 파일을 관리합니다.
예시:
String message = context.getMessage("welcome.message", null, Locale.KOREA);
System.out.println(message);
ResourceLoader는 다양한 리소스(XML, 파일 등)를 애플리케이션에 로딩할 수 있는 기능을 제공합니다.
예시:
Resource resource = context.getResource("classpath:application.properties");
WebApplicationContext는 웹 애플리케이션에 특화된 IoC 컨테이너입니다.
ServletContext와 통합되어, 웹 애플리케이션의 Bean을 관리합니다.
주요 구현체:
| 구분 | BeanFactory | ApplicationContext |
|---|---|---|
| 로딩 방식 | 지연 로딩 (Lazy Loading) | 즉시 로딩 (Eager Loading) |
| 사용 환경 | 간단한 애플리케이션 | 대규모 애플리케이션 |
| 부가 기능 | 없음 | 이벤트 관리, 국제화 지원 등 다양한 기능 제공 |
| 웹 애플리케이션 지원 | 지원 안 함 | WebApplicationContext 사용 |
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입니다.
개발자는 비즈니스 로직에 집중할 수 있으며, 객체 간의 결합도를 낮출 수 있습니다.
객체 간 결합도를 줄여주는 효과를 얻을 수 있습니다.
이를 느슨한 결합(loose coupling)이라고 부르며, 객체가 다른 객체와 최소한의 의존성만 가지도록 설계할 수 있습니다.
Loose Coupling의 중요성:
결합도가 높을 때 문제점:
만약 하나의 클래스가 변경되거나 유지보수될 때, 해당 클래스와 강하게 결합된 다른 클래스도 함께 수정해야 할 가능성이 높아집니다.
이런 상황은 코드의 유지보수성을 크게 저하시키며, 큰 애플리케이션일수록 문제의 규모가 커집니다.
예시:
public class Service {
private Repository repository = new Repository(); // 강한 결합
public void doSomething() {
repository.save();
}
}
Service 클래스가 Repository 객체를 직접 생성하고 사용합니다.Repository의 로직이 변경되면, Service 클래스도 수정이 필요합니다.IoC를 사용하면:
강한 결합이란 한 객체가 다른 객체를 직접 생성하고 호출하는 방식으로, 코드 간 의존성이 높아지는 문제를 말합니다. 이로 인해 유지보수와 확장성이 저해됩니다.
public class Main {
public static void main(String[] args) {
GreetingServiceKor greetingService = new GreetingServiceKor(); // 직접 생성
String message = greetingService.greet("John");
System.out.println(message);
}
}
GreetingServiceKor가 GreetingServiceEng로 변경될 경우, Main 클래스도 함께 수정해야 함.GreetingServiceKor 대신 Mock 객체를 주입하기 어려움.인터페이스와 의존성 주입(DI)을 사용하여 강한 결합을 느슨하게 바꿉니다.
public interface GreetingService {
String greet(String name);
}
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 + "!";
}
}
public class Main {
public static void main(String[] args) {
GreetingService greetingService = new GreetingServiceKor(); // 다형성 활용
String message = greetingService.greet("John");
System.out.println(message);
}
}
Factory를 활용하면 객체 생성을 외부에서 관리하여, 클래스가 객체 생성 로직을 알 필요가 없습니다.
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;
}
}
}
public class Main {
public static void main(String[] args) {
GreetingService service = GreetingServiceFactory.getGreetingService("kor");
String message = service.greet("Alice");
System.out.println(message);
}
}
Assembler(조립기)는 객체의 생성과 의존성 설정을 외부에서 수행하여, 클래스 간 결합도를 더욱 낮춥니다.
public class Assembler {
public GreetingService getGreetingService() {
return new GreetingServiceKor(); // 필요에 따라 다른 서비스 반환 가능
}
}
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);
}
}
Spring IoC 컨테이너는 Assembler와 유사하게 객체의 생성과 의존성 관리를 수행합니다.
Spring을 사용하면 객체 간 의존성을 외부에서 설정하고, 객체 생성을 자동으로 처리해 줍니다.
<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>
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);
}
}
public class UserService {
public void printServiceName() {
System.out.println("User Service Bean");
}
}
<bean id="userService" class="com.example.UserService"/>
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserService();
}
}
Spring의 BeanFactory, ApplicationContext, WebApplicationContext는 객체의 생성과 생명주기 관리를 담당합니다. 이 글에서는 스프링 컨테이너의 계층 구조와 사용 예시를 알아봅니다.
BeanFactory는 스프링의 가장 기본적인 IoC 컨테이너입니다. 객체의 생성과 의존성 주입을 담당하며, 지연 로딩(Lazy Loading) 방식을 사용합니다. 이는 필요한 시점에만 객체를 생성하므로 메모리 사용을 최소화할 수 있습니다.
ApplicationContext는 BeanFactory를 확장한 상위 IoC 컨테이너로, 부가 기능을 제공합니다. 모든 Bean을 즉시 로딩(Eager Loading) 방식으로 미리 생성하여 빠른 응답을 지원합니다.
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
GreetingService service = (GreetingService) context.getBean("greetingService");
service.greet("Alice");
WebApplicationContext는 웹 애플리케이션 환경에 특화된 ApplicationContext입니다.
BeanFactory
└── ApplicationContext
└── WebApplicationContext