Spring IoC와 DI 정리
DI(Dependency Injection, 의존성 주입)와 IoC(Inversion of Control, 제어의 역전)는 말로만 들으면 추상적이라 잘 와닿지 않습니다. 그래서 같은 기능을 3단계로 점점 발전시키면서, 코드가 어떻게 바뀌고 무엇이 좋아지는지를 직접 보겠습니다.
- S#1 — Controller 하나로 전부 처리 (DI/IoC 없음)
- S#2 — 객체를 분리해 의존관계만 만들기 (주입은 아직 없음)
- S#3 — 어노테이션으로 의존성 주입, 즉 IoC 적용
환경: Spring Tool Suite 4 · JAVA 17. Spring을 기본적으로 아는 분을 기준으로 작성했습니다.
먼저 전체 파일 구조와 빌드 설정부터 확인하고 시작하겠습니다.
파일 구조
ProjectName/
│
├── gradle/wrapper
├── src/
│ ├── main/
│ │ ├── java/com/example/ProjectName/
│ │ │ ├── beans/ # 생성된 파일 (3개)
│ │ │ ├── Chocolate.java
│ │ │ ├── Strawberry.java
│ │ │ ├── Vanllia.java
│ │ │ │
│ │ │ ├── controller/ # 생성된 파일 (1개)
│ │ │ │ └── control.java
│ │ │ │
│ │ │ └── ProjectnameApplication.java
│ │ │
│ │ └── resources/
│ │ ├── templates
│ │ ├── static/
│ │ │ └── order.html # 생성된 파일
│ │ │
│ │ └── application.properties
│ │
│ └── test/java/
│
├── .gitignore
├── build.gradle
├── gradlew
├── gradlew.bat
└── settings.gradle
아래는 build.gradle 구성입니다.
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.product'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
이제 시작해봅시다.
S#1 : Controller만 존재 (DI·IoC 사용 X)
첫 번째 시나리오는 가장 단순한 형태입니다. 상품을 구매하는 메뉴가 있고, 주문 처리·상품 제조·전달까지를 하나의 흐름으로 처리합니다.
이 단계에서는 화면을 그릴 order.html 과, 요청·응답을 처리할 Controller
하나면 충분합니다. (코드 안의 log 는 테스트용 주석입니다.)
order.html
<!DOCTYPE html>
<html>
<head>
<meta charset="EUC-KR">
<title>Product Info</title>
</head>
<body>
<h2> - item(Product) list </h2>
<p> order something...</p>
<form action="/choosed" method="post">
<input type="radio" name="item" value="1" checked="checked"><label>Chocolate</label><br>
<input type="radio" name="item" value="2"><label>Vanilla</label><br>
<input type="radio" name="item" value="3"><label>Strawberry</label><br>
<p></p>
<input type="submit" value="choose one">
</form>
</body>
</html>
Control.java
@RestController
public class Control {
// 1번째 시나리오 : 일반적으로 웹 생성, DI와 IOC 사용 X, Controller만 존재
// private final static Logger log = LoggerFactory.getLogger(Control.class);
@RequestMapping("/choosed")
public String doOrder(HttpServletRequest request) {
String strTaste = "";
String strPrice = "";
String strMenu = request.getParameter("item");
// log.info(strMenu);
// log.info("--------------------");
if (strMenu.equals("1")) {
strTaste = "Chocolate - sweet";
strPrice = "1000";
} else if (strMenu.equals("2")) {
strTaste = "Vanilla - original";
strPrice = "1500";
} else if (strMenu.equals("3")) {
strTaste = "Strawberry - refresh";
strPrice = "2000";
}
return strTaste+" : "+strPrice;
}
}
Boot Dashboard에서 실행하면 아래처럼 동작합니다.
- order.html — 버튼을 누르기 전 화면

- /choosed — 버튼을 누른 후 화면

정리: 동작은 하지만 모든 분기(상품 정보)가 Controller 안에 박혀 있습니다. 상품이 늘어날수록 Controller가 비대해집니다. 다음 단계에서 이 책임을 분리합니다.
S#2 : Repository·Service로 분리 (의존관계 O, 주입 X)
이번에는 각 상품을 별도 객체로 분리해서 바로 꺼내 쓸 수 있게 만듭니다.
상품 객체들을 beans 패키지에 따로 두고, Controller가 이들을 직접 new 해서
사용합니다. 의존관계(Dependency)는 생겼지만 주입(Injection)은 아직 없습니다.
Control.java
@RestController
public class Control {
// 2번째 시나리오 : 미리 구분해서 바로 사용 (Repository, Service 사용)
// 의존관계 - item을 받아오기 위해 외부 클래스인 Chocolate, Vanilla, Strawberry에 의존
@RequestMapping("/choosed")
public String doOrder(HttpServletRequest request) {
String strMenu = request.getParameter("item");
String strRet = "";
if (strMenu.equals("1")) {
Chocolate choco = new Chocolate();
strRet = choco.toString();
} else if (strMenu.equals("2")) {
Vanilla vanilla = new Vanilla();
strRet = vanilla.toString();
} else if (strMenu.equals("3")) {
Strawberry straw = new Strawberry();
strRet = straw.toString();
}
return strRet;
}
}
분리한 상품 객체들은 다음과 같습니다.
Chocolate.java
public class Chocolate {
String strTaste = "Chocolate - sweet";
String strPrice = "1000";
@Override
public String toString() {
return "Chocolate : [strTaste="+strTaste+", strPrice="+strPrice+"]";
}
}
Vanilla.java
public class Vanilla {
String strTaste = "Vanilla - original";
String strPrice = "1500";
@Override
public String toString() {
return "Vanilla : [strTaste="+strTaste+", strPrice="+strPrice+"]";
}
}
Strawberry.java
public class Strawberry {
String strTaste = "Strawberry - refresh";
String strPrice = "2000";
@Override
public String toString() {
return "Strawberry : [strTaste="+strTaste+", strPrice="+strPrice+"]";
}
}
실행 결과의 형태는 달라졌지만 본질은 같습니다. 상품을 선택하면 해당 객체가 가진 데이터를 불러옵니다. 차이는 책임의 분리입니다. 예전엔 Controller가 전부 했다면, 이제는 상품마다 객체로 나눠 두고 필요할 때 꺼내 씁니다.
다만 여전히 Controller가 new Chocolate() 처럼 객체 생성을 직접 합니다.
이 부분을 Spring에게 넘기는 것이 다음 단계입니다.
S#3 : IoC — 스프링이 객체를 주입한다
S#2에서 바뀌는 건 사실 거의 없습니다. 어노테이션을 추가하는 게 전부입니다. 대신 의미는 큽니다. 객체를 직접 만들지 않고, 스프링 컨테이너(BeanFactory)가 만들어 둔 빈(Bean)을 주입(Injection) 받아 쓰게 됩니다.
Controller에서는 상품 객체를 필드/생성자로 받아 옵니다.
Control.java
// 의존성 주입(DI) 방법 1
// @Autowired
// Chocolate choco; // 이게 Injection 이다.
//
// @Autowired
// Vanilla vanilla;
//
// @Autowired
// Strawberry straw;
// 의존성 주입 방법 2
Chocolate choco;
Vanilla vanilla;
Strawberry straw;
// 3번째 시나리오 : 2번째 시나리오에서 IoC추가, 스프링 빈(컨테이너에 보관, BeanFactory, 어노테이션을 사용)
@Autowired // injection을 하기 위해서 생성
public Control(Chocolate cho, Vanilla van, Strawberry straw) {
this.choco = cho;
this.vanilla = van;
this.straw = straw;
}
// bean을 만들어서 주입을 받게 됨.
// 이전에는 불러왔다면, 이제는 Bean 컨테이너에서 받아온다.
@RequestMapping("/choosed")
public String doOrder(HttpServletRequest request) {
String strMenu = request.getParameter("item");
String strRet = "";
if (strMenu.equals("1")) {
strRet = choco.toString();
} else if (strMenu.equals("2")) {
strRet = vanilla.toString();
} else if (strMenu.equals("3")) {
strRet = straw.toString();
}
return strRet;
}
그리고 각 상품 객체에는 @Component 를 붙여 빈으로 등록합니다. 나머지
코드는 그대로라 ... 로 줄였습니다.
Chocolate.java
@Component // 3번째 시나리오일 때 추가, beanFactory의 bean 만들기
public class Chocolate {
...
}
Vanilla.java
@Component
public class Vanilla {
...
}
Strawberry.java
@Component
public class Strawberry {
...
}
결과 자체는 S#2와 같습니다. 하지만 누가 객체를 만드는가가 바뀌었습니다. 이전엔 내가(Controller가) 만들었고, 이제는 스프링이 만들어 주입합니다. 이것이 바로 “제어의 역전(IoC)“이고, 그 구체적 수단이 “의존성 주입(DI)“입니다.
이제 배포까지 해보겠습니다.
내장 Tomcat, Jar로 배포하기
배포는 War로도 가능하지만 여기서는 Jar로 실행해 보겠습니다.
STS(Spring Tool Suite)에서 상단 메뉴 Window > Show View > Other... 를 열어
Gradle Tasks 를 하단에 띄웁니다. 프로젝트 이름이 적힌 항목을 펼쳐
프로젝트 이름(product_DI) > build > bootJar 에서 우클릭 → Run Gradle Tasks
를 실행합니다.

빌드가 끝나면 STS에는 보이지 않지만, 워크스페이스의 build > libs 폴더에
프로젝트 이름으로 된 Jar 파일이 생깁니다. 이 파일을 cmd 에서 java 명령으로
실행합니다.
> cd c:\SpringDI_IOC\product_DI\build\libs # 1. jar 파일이 있는 위치로 이동
> java -jar product_DI-0.0.1-SNAPSHOT.jar
# 파일의 최종경로 = c:\SpringDI_IOC\product_DI\build\libs\product_DI-0.0.1-SNAPSHOT.jar



한 줄 요약
- S#1: 전부 Controller에 → 책임 집중, 확장 어려움
- S#2: 객체 분리 → 책임은 나눴지만 생성은 여전히 내 몫
- S#3:
@Component+@Autowired→ 생성·주입을 스프링에 위임 (= IoC/DI)