삶의 공유
[Spring] Spring DI원리-2 (Bean, ApplicationContext, @Autowired, @Resource) 본문
[Spring] Spring DI원리-2 (Bean, ApplicationContext, @Autowired, @Resource)
dkrehd 2025. 4. 20. 03:11Spring DI 원리 완전 정복: Bean, ApplicationContext, @Autowired, @Resource 비교 분석
이번 글에서는 Spring Framework의 핵심 개념인 **DI(Dependency Injection)**의 작동 원리를 ApplicationContext, @Bean, @Autowired, @Resource를 중심으로 분석하고, 수동 연결 방식과 자동 주입 방식의 차이까지 예제 코드와 함께 정리해보겠습니다.
🔍 AppContext란 무엇이고 왜 필요한가?
AppContext는 우리가 직접 만든 간이 IoC(Inversion of Control) 컨테이너입니다. Spring의 ApplicationContext처럼, 객체(Bean)의 생성과 의존성 주입을 담당하는 역할을 합니다.
Spring에서는 보통 @ComponentScan과 같은 어노테이션 기반 자동 등록 기능을 통해 의존성을 주입하지만, 본 예제에서는 AppContext라는 클래스를 통해 수동으로 그 구조를 모방하고 있습니다. 즉, AppContext는 다음과 같은 이유로 필요합니다:
- 객체를 직접 new 하지 않고도 필요한 Bean을 꺼내서 사용할 수 있게 해줍니다.
- 의존성 주입이 필요할 때 @Autowired 또는 @Resource와 같은 어노테이션을 분석하여 객체를 연결합니다.
- 유지보수성과 재사용성을 높이고, 객체 간 결합도를 낮추는 데 핵심 역할을 합니다.
이제부터 AppContext가 어떻게 작동하며, Bean을 어떤 방식으로 등록하고 꺼내오는지를 자세히 살펴보겠습니다.
✅ Spring DI의 진화 과정: config.txt → Java 설정 클래스
이전에는 config.txt 같은 외부 설정 파일에서 클래스 이름을 읽고 객체를 생성했다면, 이제는 Java 파일(Appconfig.java) 을 통해 객체를 설정하고 주입합니다. 이 방식은 타입 안정성과 IDE 지원을 확보할 수 있어 더욱 실용적입니다.
🔍 AppContext: Bean 저장소의 핵심 역할
AppContext는 내부적으로 Bean 객체들을 저장하고 관리하기 위한 저장소 역할을 합니다. 이 저장소는 Java의 Map 구조를 사용하며, 객체를 이름 또는 타입을 기준으로 등록하고 검색할 수 있도록 설계되어 있습니다.
즉, 우리가 객체를 직접 생성(new)하지 않고도 필요한 Bean을 꺼내 쓸 수 있도록 하기 위해, 이 Map 구조가 활용되는 것입니다. AppContext 내부에서 이 Map에 Bean을 등록하는 방식은 다음과 같이 크게 두 가지입니다:
Map<String, Object> map = new HashMap<>();
// 1. 직접 수동 등록
map.put("car", new SportCar());
map.put("engine", new Engine());
map.put("door", new Door());
- map.put("car", new SportCar()) → 이름을 키로 등록 (직접 수동 등록)
🧠 Java 설정 클래스 기반 DI: @Bean
// 2. 설정 클래스(Appconfig)의 @Bean 메서드를 통해 자동 등록
Method[] methods = configClass.getDeclaredMethods();
for (Method m : methods) {
if (m.isAnnotationPresent(Bean.class)) {
map.put(m.getName(), m.invoke(config));
}
}
public class Appconfig {
@Bean public Car car() { return new Car(); }
@Bean public Engine engine() { return new Engine(); }
@Bean public Door door() { return new Door(); }
}
- 메서드 이름이 Bean 이름이 됩니다.
- AppContext(Appconfig.class)를 통해 설정 클래스를 기반으로 Bean을 생성하고 등록합니다.
이 방식은 XML 설정의 단점을 보완하고, 순수 Java 코드 기반 설정을 가능하게 합니다.
🔁 byName vs byType 비교
Spring에서는 Bean을 주입할 때 크게 두 가지 방식이 있습니다:
방식기준예시설명
byName | 이름 | getBean("door") | 필드 이름을 기준으로 찾음. @Resource 주입 시 사용 |
byType | 클래스 | getBean(Engine.class) | 타입(클래스)을 기준으로 찾음. @Autowired 주입 시 사용 |
🔎 예시 비교
AppContext ac = new AppContext(Appconfig.class); // 설정 파일 지정
Car car1 = (Car) ac.getBean("car"); // byName
Car car2 = (Car) ac.getBean(Car.class); // byType
- byName 방식은 이름 중복 방지와 명확한 제어에 유리하지만, 오타에 민감
- byType 방식은 직관적이고 타입 안정성이 높지만 동일 타입 Bean이 2개 이상일 경우 오류 발생 가능
📦 객체 간의 연결_수동 주입 방식 vs 자동 주입 방식
스프링에서 객체 간의 연결은 핵심 개념입니다. 예를 들어, 자동차(Car)는 엔진(Engine)과 문(Door) 객체를 필요로 합니다. 이처럼 객체가 다른 객체를 사용하는 상황을 **의존성(Dependency)**이라고 하며, 이러한 의존 관계를 해결하는 과정이 바로 **의존성 주입(Dependency Injection)**입니다.
DI를 적용하면 객체가 직접 필요한 의존 객체를 생성하지 않고, 외부에서 주입받기 때문에 결합도가 낮아지고, 유지보수가 쉬워지며, 유연한 구조를 설계할 수 있습니다.
아래에서는 의존 객체를 직접 연결하는 수동 방식과, 어노테이션을 통한 자동 주입 방식의 차이를 비교해보겠습니다.
✅ 수동 Setter 기반 연결
car.setEngine(engine);
car.setDoor(door);
- 명시적으로 각 객체의 관계를 코드로 설정
- 장점: 명확하고 디버깅 용이
- 단점: 코드 중복, 확장성 낮음
✅ 자동 주입 방식: @Autowired, @Resource
1. @Autowired (byType 기반)
@Autowired
Engine engine;
- 타입 기준으로 객체를 자동 주입
- 스프링이 동일 타입 Bean을 찾아 자동 연결
2. @Resource (byName 기반)
@Resource
Door door;
- 필드 이름 기준으로 Bean을 찾아 연결 (@Resource(name="door")와 동일)
- 같은 이름의 Bean이 map에 등록돼 있어야 동작함
✨ 내부 작동 방식 (AppContext 기준)
field.set(bean, getBean(field.getType())); // @Autowired
field.set(bean, getBean(field.getName())); // @Resource
🔧 자동 주입 방식의 장점
자동 주입(@Autowired, @Resource)은 객체 간 의존성을 설정할 때 아래와 같은 장점이 있습니다:
- 코드의 간결함: 반복적인 setXxx() 호출을 줄여줍니다.
- 유지보수 용이: 의존 관계가 코드가 아닌 어노테이션으로 선언되어 있어 가독성과 구조 파악이 쉬움
- 재사용성과 유연성 증가: 객체 구성 방식이 설정 파일 혹은 컴포넌트 스캔에 따라 변경되더라도, 의존성 주입 방식은 변경 없이 그대로 유지 가능
- 테스트 용이성 향상: Mock 객체나 테스트용 Bean을 손쉽게 주입할 수 있어 단위 테스트 작성이 편리해짐
자동 주입은 특히 대규모 시스템에서 객체 간의 연결 관계가 복잡해질수록 생산성과 안정성을 크게 향상시켜줍니다.
📌 전체 예제 요약 흐름
- Appconfig.java → 객체 생성 방법 정의 (@Bean)
- AppContext → 설정 클래스를 분석해 Bean 등록
- @Autowired, @Resource → 각 필드에 객체 자동 주입
- Main → ApplicationContext로부터 Bean 꺼내서 사용
✅ 결론: 언제 어떻게 쓸까?
- 작은 프로젝트: 수동 Setter 주입도 충분히 실용적
- 중/대형 프로젝트 or 협업 시: @Autowired, @Resource를 통한 자동 주입 권장
- 설정 파일 관리: Java 기반 설정 클래스 사용 시 유지보수 용이하며, 타입 안정성 확보 가능
Spring DI는 결국 이러한 원리 위에서 구성되며, ApplicationContext, @Bean, @Autowired, @Resource가 핵심 축을 이루게 됩니다. 직접 DI 구조를 흉내 내며 구성해보면 스프링의 내부 동작에 대한 이해도가 훨씬 높아집니다 😎
💻 전체 예제 코드
Main.java
public class Main {
public static void main(String[] args) {
AppContext ac = new AppContext(Appconfig.class); // 설정 파일 지정
Car car = (Car) ac.getBean("car"); // byName
Car car2 = (Car) ac.getBean(Car.class); // byType
Engine engine = (Engine) ac.getBean("engine");
Door door = (Door) ac.getBean(Door.class);
// 수동 주입 가능
// car.setEngine(engine);
// car.setDoor(door);
System.out.println("car = " + car);
System.out.println("car2 = " + car2);
System.out.println("engine = " + engine);
System.out.println("door = " + door);
}
}
Appconfig.java
public class Appconfig {
@Bean public Car car() { return new Car(); }
@Bean public Engine engine() { return new Engine(); }
@Bean public Door door() { return new Door(); }
}
AppContext.java
public class AppContext {
Map<String, Object> map = new HashMap<>();
public AppContext(Class configClass) {
try {
Object config = configClass.newInstance();
for (Method m : configClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Bean.class)) {
map.put(m.getName(), m.invoke(config));
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
doAutowired();
doResource();
}
private void doAutowired() {
for (Object bean : map.values()) {
for (Field field : bean.getClass().getDeclaredFields()) {
if (field.getAnnotation(Autowired.class) != null) {
try {
field.set(bean, getBean(field.getType()));
} catch (Exception e) { throw new RuntimeException(e); }
}
}
}
}
private void doResource() {
for (Object bean : map.values()) {
for (Field field : bean.getClass().getDeclaredFields()) {
if (field.getAnnotation(Resource.class) != null) {
try {
field.set(bean, getBean(field.getName()));
} catch (Exception e) { throw new RuntimeException(e); }
}
}
}
}
public Object getBean(String name) {
return map.get(name);
}
public Object getBean(Class clazz) {
for (Object obj : map.values()) {
if (clazz.isInstance(obj)) return obj;
}
return null;
}
}
Car.java
public class Car {
@Autowired
Engine engine;
@Resource
Door door;
public void setEngine(Engine engine) {
this.engine = engine;
}
public void setDoor(Door door) {
this.door = door;
}
@Override
public String toString() {
return "Car{" + "engine=" + engine + ", door=" + door + '}';
}
}
기타 클래스
public class Engine {}
public class Door {}
'Web Dev > BackEnd' 카테고리의 다른 글
[Spring] 고급 어노테이션 정리: @Conditional, @Import 완벽 가이드 (0) | 2025.04.25 |
---|---|
[Spring] 어노테이션 완전정복: @ComponentScan, @Component, @Value, 의존성 주입 방식 총정리 (0) | 2025.04.22 |
[Spring] Java Reflection API: 런타임에 클래스를 조작하는 마법 (0) | 2025.04.18 |
[Spring] Spring DI(Dependency Injection) 원리-1 (0) | 2025.04.17 |
[Spring] Thymeleaf 사용법 - 실전 예제로 배우는 문법 정리 (0) | 2025.04.17 |