Let's Talk

Feel free to reach out. I'll get back to you as soon as possible.

Study 7 min

Spring DI와 IoC 개념정리

Spring 프레임워크의 핵심인 의존성 주입(DI)과 제어의 역전(IoC)을, 컨트롤러만 있는 코드부터 빈(Bean) 주입까지 3단계 시나리오로 따라가며 정리합니다.

Spring DI와 IoC 개념정리

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 — 버튼을 누르기 전 화면

order.html 화면

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

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 를 실행합니다.

gradle task 실행

빌드가 끝나면 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

jar 저장 위치

jar 실행

act jar


한 줄 요약

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