스프링 프레임워크에서 제어의 역전(Inversion of Control, IoC)은 핵심 개념 중 하나입니다. 일반적으로 자바에서 객체를 생성할 때는 개발자가 직접 new 키워드를 사용하여 객체를 생성합니다. 하지만 스프링에서는 이러한 객체 생성과 관리를 IoC 컨테이너가 담당합니다.
의존성 주입(Dependency Injection, DI)는 IoC의 한 형태로, 필요한 객체를 직접 생성하지 않고 IoC 컨테이너가 미리 생성해 둔 객체를 외부에서 주입받아 사용하는 방법입니다. 이를 통해 두 객체 간의 결합도를 낮추고, 객체의 독립성을 높이는 효과를 얻을 수 있습니다. 다시 말해, 개발자는 객체의 생성과 생성 주기를 관리하는 권한을 IoC 컨테이너에 넘겨주기 때문에 '제어의 역전'이라고 합니다.
두 개의 장난감이 있다고 생각해봅시다. 두 장난감은 배터리가 있어야 작동할 수 있으므로 배터리에 의존하고 있습니다.
즉, 결합도가 높으면 독립성이 떨어져 비효율적이고, 독립성이 높으면 결합도가 낮아져 효율적이고 유연해집니다.
다음 코드를 통해 의존성과 독립성에 대해 더욱 명확하게 이해할 수 있습니다.
package com.edu.springboot;
class Persons {
String name;
int age;
/* 생성자가 public이면 외부접근이 가능하므로 인스턴스를 생성할 수 있다.
* 하지만 private으로 변경하면 외부에서 인스턴스 생성을 할 수 없다. */
private Persons() {
System.out.println("public 생성자를 호출하였습니다.");
}
}
public class DI_Test {
/* 강한 결합(독립성 낮음) : new를 통해 직접 인스턴스를 생성한다.
* 이 경우 객체 간의 결합도가 높기 때문에 Persons 클래스의 변화에 직접적인 영향을 받게 된다. */
public static void aPerson() {
// 생성자를 private으로 변경하는 순간 에러가 발생한다.
Persons person1 = new Persons();
person1.name = "손오공";
person1.age = 10;
}
/* 약한 결합(독립성 높음) : 미리 생성된 객체(bean)를 주입(Injection) 받아 사용한다.
* 결합도가 낮아지기 때문에 Persons 클래스에 변화가 생기더라도 직접적인 영향을 받지 않는다.
* 또한 코드도 간결해진다. */
public static void bPerson(Persons person2) {
person2.name = "유비";
person2.age = 45;
}
/* 따라서 DI(의존성 주입)의 목적은 객체 간의 독립성을 높이고, 결합도를 낮춰서 전체적인 프로그램을 간결하게 만드는 것에 있다. */
}
스프링에서는 new 키워드를 사용해 개발자가 직접 객체를 생성하는 대신, 스프링 컨테이너와 같은 외부에서 객체를 주입받아 사용합니다. 이를 통해 객체 간의 결합도를 낮추고 유연성을 높이는 것이 목적입니다. 개발자가 직접 객체를 생성하지 않기 때문에 이 방식은 제어의 역전(IoC, Inversion of Control)이라고 불립니다.

방법 1: 일반적인 객체 생성
A 객체가 필요할 때마다 new 키워드를 사용해 직접 B와 C 객체를 생성합니다. 이 방식은 A, B, C 객체 간의 결합도가 높아져, 변경이 일어날 경우 수정이 어렵고 유지보수가 힘듭니다.
방법 2: 의존성 주입
A 객체는 B와 C 객체를 직접 생성하지 않고, 외부에서 주입받습니다. 이 경우 스프링 컨테이너가 B와 C 객체를 생성하고, A 객체에 주입합니다. 이를 통해 A, B, C 객체 간의 결합도를 낮출 수 있고, 코드의 유연성과 유지보수성이 향상됩니다. 결국, 의존성 주입(DI)은 객체 간의 결합도를 낮추고 독립성을 높이는 데 중요한 역할을 합니다.
DI의 외부는 바로 IoC 컨테이너, 즉 스프링 컨테이너를 의미합니다.
VO 객체를 생성합니다. 다음과 같이 com.edu.springboot.bean1.Person.java 를 작성하세요.
package com.edu.springboot.bean1;
// 데이터 저장 기능만 있는 일반적인 DTO 클래스
public class Person {
// 멤버변수
private String name;
private int age;
private Notebook notebook;
// 생성자 (디폴트 생성자와 인수 생성자)
public Person() {}
public Person(String name, int age, Notebook notebook) {
super();
this.name = name;
this.age = age;
this.notebook = notebook;
}
// 게터와 세터
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Notebook getNotebook() {
return notebook;
}
public void setNotebook(Notebook notebook) {
this.notebook = notebook;
}
// toString() 메서드 오버라이딩
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", notebook=" + notebook + "]";
}
}
VO 객체를 생성합니다. 다음과 같이 com.edu.springboot.bean1.Notebook.java 를 작성하세요.
package com.edu.springboot.bean1;
public class Notebook {
// 멤버변수
private String cpu;
// 생성자
public Notebook(String cpu) {
this.cpu = cpu;
}
// toString() 메서드 오버라이딩
@Override
public String toString() {
return "Notebook [cpu=" + cpu + "]";
}
}
설정파일을 생성합니다. 다음과 같이 com.edu.springboot.bean1.BeanConfig.java 를 작성하세요.
package com.edu.springboot.bean1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/* 클래스에 설정파일의 역할을 부여하기 위해 @Configuration 어노테이션을 부착한다.
* 스프링이 구동될 때 자동으로 빈이 생성되어 내부의 코드가 실행된다. */
@Configuration
public class BeanConfig {
/* @Bean 어노테이션을 통해 자바빈을 생성한다.
* 이때 참조변수명은 별도의 설정이 없으므로 메서드명인 person1으로 생성한다. */
@Bean
public Person person1() {
// 인스턴스를 생성한 후 setter를 통해 초기화한다.
Person person = new Person();
person.setName("성유겸");
person.setAge(11);
person.setNotebook(new Notebook("레노버"));
return person;
}
/* 위와 동일하게 자바빈을 생성하되 name 속성을 부여했으므로 해당명인 person2로 생성된다. */
@Bean(name="person2")
public Person ptemp() {
// 생성자를 통해 인스턴스를 초기화한다.
Person person = new Person("알파고", 20, new Notebook("인텔"));
return person;
}
}
컨트롤러를 생성합니다. 다음과 같이 com.edu.springboot.bean1.Di1Controller.java 를 작성하세요.
package com.edu.springboot.bean1;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class Di1Controller {
/* @ResponseBody : 컨트롤러에서 처리된 내용을 View로 전달하지 않고, 즉시 출력하고 싶을 때 사용하는 어노테이션이다.
* String으로 반환하면 단순히 문자열이 출력되고, Map 혹은 List로 반환하면 JSON 객체 혹은 배열 형태로 출력된다. */
@RequestMapping("/di1")
@ResponseBody
public String home() {
// Java 설정파일을 기반으로 스프링 컨테이너를 생성한다.
ApplicationContext context = new AnnotationConfigApplicationContext(BeanConfig.class);
// 컨테이너에 미리 생성된 person1 빈을 주입받는다. (형변환 필요)
Person person1 = (Person) context.getBean("person1");
// 빈의 정보를 toString()을 통해 출력한다.
System.out.println(person1);
// 두번째 인자를 통해 타입을 명시하면 주입받은 후 별도의 형변환이 필요없다.
Person person2 = context.getBean("person2", Person.class);
System.out.println(person2);
/* 해당 메서드의 반환타입이 String 이므로 @ResponseBody 어노테이션이 없다면 View의 경로를 반환하게 되지만,
* 현재는 단순한 문자열을 반환하여 컨트롤러에서 즉시 출력한다. */
return "Dependency Injection 1 (의존주입 1)";
}
}
뷰를 생성합니다. 다음과 같이 src/main/webapp/WEB-INF/views/home.jsp 를 작성하세요.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>의존성 주입(Dependency Injection)</h2>
<ul>
<li><a href="/">최상위루트</a></li>
<li><a href="/di1">의존주입1</a></li>
<li><a href="/di2">의존주입2</a></li>
</ul>
</body>
</html>
다음과 같이 실행됩니다.

'의존주입1' 링크를 클릭하면 콘솔에 다음과 같이 출력됩니다.
Person [name=성유겸, age=11, notebook=Notebook [cpu=레노버]]
Person [name=알파고, age=20, notebook=Notebook [cpu=인텔]]
VO 객체를 생성합니다. 다음과 같이 com.edu.springboot.bean2.Computer.java 를 작성합니다.
package com.edu.springboot.bean2;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;
/* 스프링 컨테이너를 시작할 때 이름을 지정해서 빈을 생성한다.
* 즉 Computer 타입의 macBook이라는 빈이 생성된다.
* Computer macBook = new Computer()와 같은 의미이다. */
@Component("macBook")
public class Computer {
// 멤버변수는 지정한 값으로 초기화
@Value("M1")
private String cpu;
public String getCpu() {
return cpu;
}
public void setCpu(String cpu) {
this.cpu = cpu;
}
@Override
public String toString() {
return "Computer [cpu=" + cpu + "]";
}
}
VO 객체를 생성합니다. 다음과 같이 com.edu.springboot.bean2.Student.java 를 작성합니다.
package com.edu.springboot.bean2;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Qualifier;
/* @Component를 통해 스프링 컨테이너 시작 시 자동으로 빈이 생성된다.
* 여기서는 별도의 이름을 지정하지 않았으므로 클래스명의 첫글자를 소문자로 변경한 student라는 이름을 빈이 생성된다. */
@Component
public class Student {
/* @Value 어노테이션에 지정한 값으로 멤버변수가 초기화된다. 이 값은 setter를 통해 설정된다. */
@Value("이순신")
private String name;
@Value("30")
private int age;
/* 객체타입의 멤버변수는 @Autowired를 통해 자동으로 빈을 주입받을 수 있다.
* 이때 Qualifier가 있으면 빈의 이름까지 지정해서 주입받는다.
* 만약 없다면 타입으로 빈을 찾아 주입받게 된다. */
@Autowired
@Qualifier("macBook")
private Computer notebook;
public Student() {}
public Student(String name, int age, Computer notebook) {
super();
this.name = name;
this.age = age;
this.notebook = notebook;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Computer getNotebook() {
return notebook;
}
public void setNotebook(Computer notebook) {
this.notebook = notebook;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + ", notebook=" + notebook + "]";
}
}
컨트롤러를 생성합니다. 다음과 같이 com.edu.springboot.bean2.Di2Controller.java 를 작성합니다.
package com.edu.springboot.bean2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@Controller
public class Di2Controller {
// 단순히 타입만으로 자바빈을 자동주입 받는다.
@Autowired
Student student;
// 타입과 이름까지 지정해서 자바빈을 자동주입 받는다.
@Autowired
@Qualifier("macBook")
Computer computer;
// student의 내용을 출력한다.
@RequestMapping("/di2")
@ResponseBody
public String home() {
System.out.println(student);
System.out.println(computer);
return "Dependency Injection 2";
}
}
다음과 같이 실행됩니다.

'의존주입2' 링크를 클릭하면 콘솔에 다음과 같이 출력됩니다.
Student [name=이순신, age=30, notebook=Computer [cpu=M1]]
Computer [cpu=M1]