2025. 11. 3. 16:37ㆍJava/Spring
이 포스팅은 Spring Framework의 DI(Dependency Injection, 의존성 주입)을 처음부터 끝까지 한 단계씩 밟아가며 실습으로 정리한 내용입니다. 단순히 코드만 보여주는 게 아니라, 각 방식이 어떤 철학에서 출발했는지, 코드가 내부적으로 어떻게 동작하는지까지 — 아주 상세하게, “스프링 초보 개발자도 완벽히 이해할 수 있도록” 정리했습니다.
❶ 스프링 DI란?
DI는 Dependency Injection, 즉 의존성 주입을 의미합니다. 이 개념은 IoC (Inversion of Control, 제어의 역전)이라는 큰 철학 아래 존재합니다.
일반적으로 자바에서 객체를 사용할 때는 이렇게 작성하죠.
public class Store {
private Pencil pencil = new Pencil();
}
위 코드는 겉보기엔 단순하지만, 설계적으로는 “강한 결합”이 발생합니다. Store 클래스가 Pencil의 구체적인 생성 방식에 직접 의존하기 때문이에요. 만약 Pencil을 Pen으로 바꾸려면? Store 내부 코드를 수정해야 합니다.
DI는 이 의존성을 외부에서 주입함으로써 결합도를 낮춥니다. 스프링 컨테이너가 “누가 누구에게 의존할지”를 대신 관리하고, 개발자는 “무엇을 할지”에만 집중할 수 있죠.
❷ IoC와 DI의 관계
- IoC (제어의 역전) : 객체 생성, 생명주기 관리, 의존 관계 설정 등 프로그램 제어권이 개발자에서 스프링 컨테이너로 넘어가는 것
- DI (의존성 주입) : IoC를 구현하는 방법 중 하나로, 객체가 사용할 의존 객체를 외부(컨테이너)에서 주입하는 행위
즉, IoC는 “누가 제어하느냐”의 개념이고, DI는 그 제어를 “어떻게 구현하느냐”의 구체적인 기술이에요.
❸ 스프링 DI의 세 가지 설정 방식
- XML 기반 설정
- Annotation 기반 설정
- Java Configuration 기반 설정
이 세 가지 방식은 시대 순서대로 발전해 왔고, 각각의 장단점이 있습니다.

1️⃣ XML 기반 DI
초창기 스프링에서는 모든 Bean(객체)을 XML 설정 파일에 등록했습니다. 이 방식은 명시적이고 설정이 한눈에 보이지만, 매번 태그를 작성해야 해서 번거로웠습니다.
🧩 XML 설정 파일 (calc-xml.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">
<!-- Configuration By XML -->
<bean id="calculator" class="com.mycom.myapp.calc.xml.Calculator"/>
</beans>
🧩 Calculator.java
package com.mycom.myapp.calc.xml;
public class Calculator {
public int add(int n1, int n2) {
return n1 + n2;
}
}
🧩 Test.java
package com.mycom.myapp.calc.xml;
import org.springframework.context.support.ClassPathXmlApplicationContext;
// XML 기반 설정
public class Test {
public static void main(String[] args) {
// 1️⃣ XML 파일을 읽어와 스프링 컨테이너 생성
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("xml/calc-xml.xml");
// 2️⃣ XML의 bean 정의를 기반으로 객체를 가져오기 (DI)
Calculator calculator = (Calculator) context.getBean("calculator");
// 3️⃣ 메서드 실행
System.out.println(calculator.add(3,7));
// 4️⃣ 컨텍스트 종료 (자원 반환)
context.close();
}
}
이 방식에서는 객체를 직접 new 하지 않고, getBean()으로 스프링 컨테이너가 생성한 객체를 주입받습니다. 즉, 제어의 주도권이 완전히 스프링에게 넘어간 것이죠.
2️⃣ Annotation 기반 DI
XML에 Bean을 하나씩 등록하는 건 귀찮았어요. 그래서 스프링은 @Component와 @Autowired를 통해 Bean을 자동 등록/주입하는 방법을 제공하게 됩니다.
🧩 XML 설정 (component-scan)
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<!-- Annotation 스캔 설정 -->
<context:component-scan base-package="com.mycom.myapp.calc.annotation"/>
</beans>
🧩 Calculator.java
package com.mycom.myapp.calc.annotation;
import org.springframework.stereotype.Component;
@Component // 스프링이 자동으로 Bean 등록
public class Calculator {
public int add(int n1, int n2) {
return n1 + n2;
}
}
🧩 Test.java
package com.mycom.myapp.calc.annotation;
import org.springframework.context.support.ClassPathXmlApplicationContext;
// Annotation 기반 설정
public class Test {
public static void main(String[] args) {
// Annotation 기반 컨텍스트 로드
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("xml/calc-annotation.xml");
// @Component 로 등록된 Bean을 가져옴
Calculator calculator = (Calculator) context.getBean("calculator");
System.out.println(calculator.add(3,7));
context.close();
}
}
여기서 @Component는 XML의 <bean>을 대체하고, 은 “이 패키지 안에서 @Component가 붙은 클래스를 찾아라”라는 의미입니다.
3️⃣ Java Configuration 기반 DI
스프링 3.x 이후부터는 순수 자바 코드로 설정을 관리할 수 있게 되었어요. XML 파일이 사라지고, 타입 안정성(type safety)도 확보되었습니다.
🧩 CalConfiguration.java
package com.mycom.myapp.calc.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CalConfiguration {
@Bean // Bean 등록
public Calculator calculator() {
return new Calculator();
}
}
🧩 Calculator.java
package com.mycom.myapp.calc.configuration;
public class Calculator {
public int add(int n1, int n2) {
return n1 + n2;
}
}
🧩 Test.java
package com.mycom.myapp.calc.configuration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
// Java Config 기반 설정
public class Test {
public static void main(String[] args) {
// XML 대신 Java Configuration을 사용하는 Application Context
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(CalConfiguration.class);
Calculator calculator = (Calculator) context.getBean("calculator");
System.out.println(calculator.add(3,7));
context.close();
}
}
이제 XML이 완전히 사라졌습니다. 코드 내부에서 설정과 로직이 함께 존재하지만, 훨씬 가볍고 명확하죠. 실무에서는 Java Config 방식이 거의 표준처럼 사용됩니다.
4️⃣ Has-A 관계에서의 DI
객체지향에서 “Has-A” 관계는 한 클래스가 다른 객체를 속성으로 가지고 있는 것을 의미합니다. 예를 들어, HasaCalculator가 Calculator 객체를 멤버 필드로 갖는 구조입니다.
🧩 HasaCalculator.java
@Component // DI가 가능한 클래스
public class HasaCalculator {
// #1 Field Injection
// @Autowired
// Calculator calculator;
// #2 Setter Injection
// Calculator calculator;
// @Autowired
// public void setCalculator(Calculator calculator) {
// this.calculator = calculator;
// }
// #3 Constructor Injection
// public HasaCalculator(Calculator calculator) {
// this.calculator = calculator;
// }
// #4 Constructor + 기본생성자 + @Autowired
Calculator calculator;
public HasaCalculator() {}
@Autowired
public HasaCalculator(Calculator calculator) {
this.calculator = calculator;
}
public int add(int n1, int n2) {
System.out.println("Has A Calculator add()");
return calculator.add(n1, n2);
}
}
🧩 Calculator.java
@Component
public class Calculator {
public int add(int n1, int n2) {
System.out.println("Calculator add()");
return n1 + n2;
}
}
🧩 Test.java
public class Test {
public static void main(String[] args) {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("xml/hasa-calc-annotation.xml");
HasaCalculator hasaCalculator = (HasaCalculator) context.getBean("hasaCalculator");
System.out.println(hasaCalculator.add(30, 9));
context.close();
}
}
실행 결과
Has A Calculator add()
Calculator add()
39
- Field 주입: 간단하지만 테스트하기 어렵고 순환 참조 감지 불가
- Setter 주입: 유연하지만 객체의 불변성이 깨짐
- Constructor 주입: 가장 권장되는 방식 (Spring Boot의 기본)
5️⃣ 인터페이스 다중 구현 + @Qualifier
만약 인터페이스를 여러 클래스가 구현하면, 스프링은 어떤 Bean을 주입해야 할지 모르게 됩니다. 이럴 때 @Qualifier를 사용해 정확한 Bean을 지정할 수 있습니다.
🧩 Calculator.java (인터페이스)
public interface Calculator {
int add(int n1, int n2);
}
🧩 CalculatorImpl.java
@Component("aaa")
public class CalculatorImpl implements Calculator {
@Override
public int add(int n1, int n2) {
System.out.println("CalculatorImpl add()");
return n1 + n2;
}
}
🧩 CalculatorImpl2.java
@Component("bbb")
public class CalculatorImpl2 implements Calculator {
@Override
public int add(int n1, int n2) {
System.out.println("CalculatorImpl2 add()");
return n1 + n2;
}
}
🧩 HasaCalculator.java
@Component
public class HasaCalculator {
private final Calculator calculator;
public HasaCalculator(@Qualifier("bbb") Calculator calculator) {
this.calculator = calculator;
}
public int add(int n1, int n2) {
System.out.println("Has A Calculator add()");
return calculator.add(n1, n2);
}
}
🧩 Test.java
public class Test {
public static void main(String[] args) {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("xml/all-calc-annotation.xml");
HasaCalculator hasaCalculator = (HasaCalculator) context.getBean("hasaCalculator");
System.out.println(hasaCalculator.add(30, 9));
context.close();
}
}
이제 “bbb”로 등록된 Bean이 주입되어 실행됩니다.
Has A Calculator add()
CalculatorImpl2 add()
39
@Qualifier("beanName")은 다중 Bean이 존재할 때 특정 Bean을 명시적으로 선택하도록 도와주는 어노테이션입니다.
만약 @Qualifier 가 되어있지않다면
//No qualifying bean of type 'com.mycom.myapp.calc.all.Calculator' available:
//expected single matching bean but found 2: calculatorImpl,calculatorImpl2
이런 에러가 발생합니다. 강사님이 자주 등장하는 에러라 하더라고요.
6️⃣ 결론 및 핵심 정리
| 구분 | 설정 방식 | 특징 |
|---|---|---|
| XML 기반 | <bean> 설정 | 명시적이지만 번거롭다 |
| Annotation 기반 | @Component / @Autowired | 자동 스캔으로 편리 |
| Java Config 기반 | @Configuration / @Bean | 코드로 설정, 타입 안정성 높음 |
| Has-A 구조 | Field / Setter / Constructor 주입 | 객체 간 합성 관계 실습 |
| Qualifier 사용 | @Qualifier("beanName") | 다중 Bean 충돌 시 명시적 지정 |
이 실습은 단순한 예제가 아니라, 스프링이 어떻게 객체를 만들고 연결하는지를 이해하게 해주는 “스프링의 심장” 같은 내용이에요.
XML → Annotation → Java Config → Has-A → Interface 구조까지 순차적으로 익히면 DI뿐만 아니라 IoC, Bean 스코프, AOP까지 한 번에 연결 지어 이해할 수 있습니다.
📘 마무리
스프링은 결국 “객체의 생명주기를 컨테이너가 관리한다”는 철학에서 출발합니다. DI는 이 철학을 코드로 구현한 구체적인 기술이고, 여기에 Annotation과 Java Config가 더해져 현대적인 스프링 생태계를 완성했습니다.
Spring Boot에서는 이 모든 과정을 @SpringBootApplication 하나로 통합하지만, 그 내부에서는 여전히 같은 DI 원리가 작동하고 있습니다.
스프링을 진짜 이해하려면 “DI와 IoC가 왜 필요한가”를 몸으로 익히는 게 중요합니다. 이번 예제처럼 직접 여러 방식을 시도해보면, 스프링의 매직 같은 Bean 주입이 사실은 “매우 논리적이고 단순한 원리”임을 알게 될 거예요.
'Java > Spring' 카테고리의 다른 글
| [LG U+ 유레카3기]Spring MVC | HttpSession 로그인 → 유지 → 로그아웃 실습 정리 (0) | 2025.11.05 |
|---|---|
| [LG U+ 유레카 3기]Spring MVC | 요청 바인딩 + View/Model/Redirect 실습 정리 (0) | 2025.11.05 |
| Spring MVC 내부 동작 구조 (Deep Dive) (0) | 2025.11.04 |
| [LG U+ 유레카 3기]Spring MVC 요청 흐름 & 매핑 실습 — Boot 위에서 레거시 구조 이해하기 (0) | 2025.11.04 |
| [LG U+ 유레카 3기]Spring Proxy | 프록시 기반 AOP 원리 이해 (0) | 2025.11.03 |