개요
안녕하세요. J4J입니다.
이번 포스팅은 IoC와 DI에 대해 적어보는 시간을 가져보려고 합니다.
IoC란?
IoC란 Inversion of Controller의 약자로 제어의 역전이라는 의미를 가집니다.
그럼 제어의 역전이란 말은 무엇일까요?
제어의 역전이란 객체의 생명주기(생성 - 설정 - 초기화 - 소멸)를 개발자가 아닌 스프링 프레임워크가 주체가 되어 담당하는 것을 말합니다.
정확하게는 스프링 내부에 존재하는 IoC 컨테이너에서 담당합니다.
제어가 역전되었을 때 나오는 상황들은 무엇이 있을까요?
예를 들어 단순 자바를 사용하다가 스프링을 사용하게 될 경우 객체를 생성할 때 new라는 키워드를 더 이상 사용할 필요가 없어지게 됩니다.
왜냐하면 new라는 키워드를 사용하지 않아도 스프링에서 객체를 생성할 수 있도록 도와주기 때문입니다.
다른 말로 표현하면 개발자들이 해야 할 자질구레한 일들을 스프링이 대신해줌으로 써 개발자가 개발에 더 집중할 수 있는 환경을 만들어주는겁니다.
스프링이 개발자들에게 "너네가 만들 거를 내가 다 만들어 줄 테니 가져다 써"라고 말하고 있는 것입니다.
DI란?
DI는 Dependency Injection의 약자로 의존성 주입이라는 의미를 가집니다.
보통 객체를 생성할 때 클래스 내부에서 값을 넣어주는데 내부에서 값을 넣어주는 것이 아닌 외부에서 넣어주는 것을 의존성 주입이라고 합니다.
저를 포함하여 대부분의 사람들이 처음에 IoC와 DI의 개념에 대해 많이 혼동을 하시는데 간단한 예시를 들어보겠습니다.
예를 들어 케이크 전문점에서 케잌 맞춤 제작을 한다고 가정해보겠습니다.
여기서 케잌 전문점은 IoC컨테이너라고 비유할 수 있습니다.
왜냐하면 기본적인 구조를 기반으로 케이크 주문자의 명세서에 맞게 맞춤 제작을 해주고 또한 보관까지 하고 있다가 주문자가 찾으러오면 완성품을 제공해주기 때문입니다.
그럼 DI를 비유할만한 것은 무엇일까요?
케잌 주문자가 아닌 외부(케이크 전문점)에서 주문 명세서에 맞게 맞춤 제작하는 것을 DI로 비유할 수 있습니다. 이해가 좀 되셨을까요?
의존성 주입을 하는 이유는 무엇일까요?
그 이유는 객체들 간의 의존성을 낮추며 유연성을 높이기 위해서입니다.
자바로 프로그래밍을 하다 보면 객체들끼리 서로 의존되어 있어 한 객체가 이름이 변경되는 등의 변화가 일어나면 의존되어 있는 다른 객체도 변화에 맞는 행동들을 해줘야 됩니다.
만약 의존되어 있는 정도가 낮다면 상관이 없겠지만 시스템의 규모가 커지면 객체들 간의 의존성도 자연스럽게 커지기 마련입니다.
그렇기 때문에 사소한 변화가 일어나더라도 의미 없는 시간이 사용돼야 하는 것이죠.
하지만 의존성 주입과 함께라면 시스템 규모가 크더라도 객체들 간의 의존성이 낮아 이런 의미 없는 시간들이 사용되지 않을 것입니다.
의존성 주입 방법
의존성을 주입하는 방법은 총 3가지가 있습니다.
1. 생성자를 이용한 의존성 주입
2. setter를 이용한 의존성 주입
3. 인터페이스를 이용한 의존성 주입
이 3개 중 1, 2번 내용만 다룰 것이고 3번에 관한 내용은 저도 모를뿐더러 여기저기 찾아봐도 확신을 가지고 답할만한 내용을 찾지 못했기에 내용을 다루지 않겠습니다.
기본적인 예시
의존성 주입을 사용한 경우와 사용하지 않은 경우에 대해 코드로 예시를 들어보겠습니다.
상황은 다음과 같습니다.
한 가정에 두 명의 아이가 있는데 아이들에게 오늘 먹일 음식이 무엇인지 확인하려고 합니다.
음식은 두 아이 모두에게 공평하게 동일한 음식을 제공할 것이고 음식을 확인하는 요일은 월요일과 수요일에만 해당합니다.
이럴 경우 가장 기본적인 코드는 다음과 같이 작성될 수 있습니다.
[ 공통 코드 ]
package com.spring.di.base;
public class MondayFood {
private String breakfast;
public MondayFood(String breakfast) {
this.breakfast = breakfast;
}
public String getBreakfast() {
return "월요일 아침식사는 " + breakfast + "입니다.";
}
}
package com.spring.di.base;
public class WednesdayFood {
private String breakfast;
public WednesdayFood(String breakfast) {
this.breakfast = breakfast;
}
public String getBreakfast() {
return "수요일 아침식사는 " + breakfast + "입니다.";
}
}
[ 첫째 아이와 둘째 아이의 월요일 음식 확인 ]
package com.spring.di.base;
public class FirstBaby {
public static void main(String[] args) {
MondayFood todayFood = new MondayFood("두유");
System.out.println("첫째 아이의 " + todayFood.getBreakfast()); // 첫째 아이의 월요일 아침식사는 두유입니다.
}
}
package com.spring.di.base;
public class SecondBaby {
public static void main(String[] args) {
MondayFood todayFood = new MondayFood("두유");
System.out.println("둘째 아이의 " + todayFood.getBreakfast()); // 둘째 아이의 월요일 아침식사는 두유입니다.
}
}
[ 첫째 아이와 둘째 아이의 수요일 음식 확인 ]
package com.spring.di.base;
public class FirstBaby {
public static void main(String[] args) {
WednesdayFood todayFood = new WednesdayFood("고급 분유");
System.out.println("첫째 아이의 " + todayFood.getBreakfast()); // 첫째 아이의 수요일 아침식사는 고급 분유입니다.
}
}
package com.spring.di.base;
public class SecondBaby {
public static void main(String[] args) {
WednesdayFood todayFood = new WednesdayFood("고급 분유");
System.out.println("둘째 아이의 " + todayFood.getBreakfast()); // 둘째 아이의 수요일 아침식사는 고급 분유입니다.
}
}
코드를 보면 첫째 아이와 둘째 아이의 오늘 음식을 확인할 때마다 변수 타입과 new를 이용한 레퍼런스 타입 모두 변경해야 되는 과정을 거쳐야 합니다.
조금이라도 코드를 덜 수정하기 위해 추상화를 이용해보겠습니다.
추상화 예시
월요일과 수요일 클래스의 상속을 위한 식사 인터페이스를 추가적으로 작성해보도록 하겠습니다.
[ 공통 코드 ]
package com.spring.di.abstraction;
public interface Food {
public String getBreakfast();
}
package com.spring.di.abstraction;
public class MondayFood implements Food {
private String breakfast;
public MondayFood(String breakfast) {
this.breakfast = breakfast;
}
@Override
public String getBreakfast() {
return "월요일 아침식사는 " + breakfast + "입니다.";
}
}
package com.spring.di.abstraction;
public class WednesdayFood implements Food{
private String breakfast;
public WednesdayFood(String breakfast) {
this.breakfast = breakfast;
}
@Override
public String getBreakfast() {
return "수요일 아침식사는 " + breakfast + "입니다.";
}
}
[ 첫째 아이와 둘째 아이의 월요일 음식 확인 ]
package com.spring.di.abstraction;
public class FirstBaby {
public static void main(String[] args) {
Food todayFood = new MondayFood("두유");
System.out.println("첫째 아이의 " + todayFood.getBreakfast()); // 첫째 아이의 월요일 아침식사는 두유입니다.
}
}
package com.spring.di.abstraction;
public class SecondBaby {
public static void main(String[] args) {
Food todayFood = new MondayFood("두유");
System.out.println("둘째 아이의 " + todayFood.getBreakfast()); // 둘째 아이의 월요일 아침식사는 두유입니다.
}
}
[ 첫째 아이와 둘째 아이의 수요일 음식 확인 ]
package com.spring.di.abstraction;
public class FirstBaby {
public static void main(String[] args) {
Food todayFood = new WednesdayFood("고급 분유");
System.out.println("첫째 아이의 " + todayFood.getBreakfast()); // 첫째 아이의 수요일 아침식사는 고급 분유입니다.
}
}
package com.spring.di.abstraction;
public class SecondBaby {
public static void main(String[] args) {
Food todayFood = new WednesdayFood("고급 분유");
System.out.println("둘째 아이의 " + todayFood.getBreakfast()); // 둘째 아이의 수요일 아침식사는 고급 분유입니다.
}
}
초기 코드와 비교해보면 오늘의 음식을 생성할 때 추상화를 이용하는 것을 볼 수 있습니다.
월요일에서 수요일로 요일이 변경될 때 변수 타입은 Food인터페이스로 유지되고 레퍼런스 타입만 변경되기 때문에 처음 예시와 비교했을 때 코드 변경으로 인한 작업이 줄어들게 되었습니다.
하지만 이런 모습 또한 의존되어 있는 클래스가 많을 경우 상당한 양의 작업이 필요해 보입니다.
DI 예시 - 생성자 이용
공통 코드는 추상화 예시와 동일하게 사용되고 요일별 적용되는 코드를 위한 xml파일 하나를 src/main/resources 위치에 applicationContext라는 이름으로 추가해보도록 하겠습니다.
[ 첫째 아이와 둘째 아이의 월요일 음식 확인 ]
<!-- applicationContext.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<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 https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="todayFood" class="com.spring.di.constructor.MondayFood">
<constructor-arg name="breakfast" value="두유"></constructor-arg>
</bean>
</beans>
package com.spring.di.constructor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class FirstBaby {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Food todayFood = (Food)context.getBean("todayFood");
System.out.println("첫째 아이의 " + todayFood.getBreakfast()); // 첫째 아이의 월요일 아침식사는 두유입니다.
}
}
package com.spring.di.constructor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SecondBaby {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Food todayFood = (Food)context.getBean("todayFood");
System.out.println("둘째 아이의 " + todayFood.getBreakfast()); // 둘째 아이의 월요일 아침식사는 두유입니다.
}
}
[ 첫째 아이와 둘째 아이의 수요일 음식 확인 ]
<!-- applicationContext.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<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 https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="todayFood" class="com.spring.di.constructor.WednesdayFood">
<constructor-arg name="breakfast" value="고급 분유"></constructor-arg>
</bean>
</beans>
package com.spring.di.constructor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class FirstBaby {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Food todayFood = (Food)context.getBean("todayFood");
System.out.println("첫째 아이의 " + todayFood.getBreakfast()); // 첫째 아이의 수요일 아침식사는 고급 분유입니다.
}
}
package com.spring.di.constructor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SecondBaby {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Food todayFood = (Food)context.getBean("todayFood");
System.out.println("둘째 아이의 " + todayFood.getBreakfast()); // 둘째 아이의 수요일 아침식사는 고급 분유입니다.
}
}
코드를 보면 첫째 아이와 둘째 아이에 대한 클래스는 요일이 변경될 때 코드가 변경되지 않는 것을 확인하실 수 있습니다.
그리고 단순히 xml파일 하나만 수정하여 이 전에 작성된 코드들과 동일한 결과를 만들었습니다.
이런 결과가 나온 이유는 xml파일에 오늘 음식에 대한 의존성 주입이 되었고 아이들의 클래스에서는 주입되어 만들어진 객체를 가져와 사용했기 때문입니다.
다른 말로는 의존성 주입을 통해 클래스들 간의 의존관계가 낮아졌고 의존되어 있는 클래스가 변경되었을 때 다른 클래스에 영향을 주지 않았다고도 할 수 있습니다.
이런 DI의 특징은 개발자들이 더 이상 의미 없는 시간을 사용하지 않게 만들어주며 개발 효율을 높여주는 스프링의 큰 특징들 중 하나라고 말할 수 있습니다.
추가적으로 보통 스프링에서 IoC 컨테이너에 등록되어 있는 빈을 가져올 때 위와 같이 context변수를 생성하지 않고 @Autowired, @Qualifier 등의 어노테이션을 이용하여 더 간단하게 빈을 가져와 사용합니다.
DI 예시 - Setter 이용
Setter를 이용하여 생성자를 이용한 경우와 동일한 결과를 만들어보겠습니다.
[ 공통 코드 ]
package com.spring.di.setter;
public interface Food {
public String getBreakfast();
}
package com.spring.di.setter;
public class MondayFood implements Food {
private String breakfast;
public void setBreakfast(String breakfast) {
this.breakfast = breakfast;
}
@Override
public String getBreakfast() {
return "월요일 아침식사는 " + breakfast + "입니다.";
}
}
package com.spring.di.setter;
public class WednesdayFood implements Food{
private String breakfast;
public void setBreakfast(String breakfast) {
this.breakfast = breakfast;
}
@Override
public String getBreakfast() {
return "수요일 아침식사는 " + breakfast + "입니다.";
}
}
[ 첫째 아이와 둘째 아이의 월요일 음식 확인 ]
<!-- applicationContext.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<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 https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- <bean id="todayFood" class="com.spring.di.constructor.WednesdayFood">
<constructor-arg name="breakfast" value="고급 분유"></constructor-arg>
</bean> -->
<bean id="todayFood" class="com.spring.di.setter.MondayFood">
<property name="breakfast" value="두유"></property>
</bean>
</beans>
package com.spring.di.setter;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class FirstBaby {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Food todayFood = (Food)context.getBean("todayFood");
System.out.println("첫째 아이의 " + todayFood.getBreakfast()); // 첫째 아이의 월요일 아침식사는 두유입니다.
}
}
package com.spring.di.setter;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SecondBaby {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Food todayFood = (Food)context.getBean("todayFood");
System.out.println("둘째 아이의 " + todayFood.getBreakfast()); // 둘째 아이의 월요일 아침식사는 두유입니다.
}
}
코드의 차이점은 음식 클래스에서 생성자 대신 setter를 작성하였고 xml에서는 속성값을 constructor-arg태그 대신에 property태그를 사용했습니다.
이 두 가지만 수정한다면 setter를 이용하여 생성자를 했을 때와 동일한 결과를 만들 수 있습니다.
정리
이상으로 IoC와 DI에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'Spring > Spring' 카테고리의 다른 글
[Spring] 스프링을 이용한 MVC패턴 구현(4) - @Service 구성 (0) | 2021.02.16 |
---|---|
[Spring] 스프링을 이용한 MVC패턴 구현(3) - MySQL, MyBatis, @Repository 구성 (0) | 2021.02.13 |
[Spring] 스프링을 이용한 MVC패턴 구현(2) - 프로젝트 초기 설정 방법 (0) | 2021.02.11 |
[Spring] 스프링을 이용한 MVC패턴 구현(1) - Spring MVC (0) | 2021.02.09 |
[Spring] 단위 테스트(JUnit Test) (0) | 2021.02.04 |
댓글