IoC와 DI를 가르쳐주신 강사님이 해주신 말씀이 있다.
강사님은 이 용어들을 요리에 비교해서 설명해 주셨다.
맛있는 김치 볶음밥을 만들기 위한 원칙
- 신선한 재료를 사용한다.
- 신 김치를 사용한다.
- 밥과 김치의 비율을 잘 맞춰야 한다.
- 볶을 때 재료의 순서가 중요하다.
맛있는 김치 볶음밥을 만들기 위한 황금 레시피
1. 오일을 두른 팬에 채썬 파를 볶아 파기름을 만든다.
2. 준비한 햄을 넣고 볶다가, 간장 한스푼을 넣어 풍미를 낸다.
3. 설탕에 버무린 김치를 넣고 함께 볶는다.
4. 미리 식혀 둔 밥을 넣어 함께 볶는다.
5. 참기름 한스푼을 넣어 마무리한다.
요리에 비유해 알 수 있듯 IoC는 맛있는 요리를 위해 지켜야할 원칙이며 DI는 실제 요리를 하는 방법이다.
Dependency Injection 의존성 주입
먼저 의존성이란 무엇인지 생각해야한다.
의존성이란 우리가 목발을 사용한다면 우리는 목발에 의존하는 것이 된다. 즉 우리는 목발에 의존성을 두게 된 것이다.
이게 무슨 말인가 싶지만 아래의 코드를 보고 생각을 해보자.
public class Consumer {
void eat() {
Chicken chicken = new Chicken();
chicken.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.eat();
}
}
class Chicken {
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
위 코드에서 Consumer와 Chicken의 관계는 강한결합이 되어있다. 코드는 문제 없이 실행 되지만 Consumer가 치킨이 아니라 피자가 먹고싶으면 몇안되는 코드인데도 수정이 꽤나 필요하다.
그렇다면 Consumer와 Chicken의 결합을 약하게 하는 방법이 뭐가 있을까
Java의 경우 Interface를 활용하면 될 것 같다.
public class Consumer {
void eat(Food food) {
food.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.eat(new Chicken());
consumer.eat(new Pizza());
}
}
interface Food {
void eat();
}
class Chicken implements Food{
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food{
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
이제 Consumer는 먹고싶은 음식을 생성해서 먹기만 하면되고
음식을 추가 하고 싶으면 Food를 implements해서 만들기만 하면된다 Consumer의 코드는 변함이 없다.
이러한 관계를 약한 결합, 약한 의존성 이라 할 수 있다.
그렇다면 주입은 무엇일까
주입은 우리가 주사기를 통해 백신을 맞아 백신을 우리 몸에 주입 하는 것 처럼 코드에서도 여러 방법을 통해 필요로 하는 객체를 해당 객체에 전달하는 것이다.
주입을 하는 방법을 알아보자
public class Consumer {
Food food;
void eat() {
this.food.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.food = new Chicken();
consumer.eat();
consumer.food = new Pizza();
consumer.eat();
}
}
interface Food {
void eat();
}
class Chicken implements Food{
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food{
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
이처럼 Food를 Consumer에 포함시키고 Food에 필요한 객체를 주입받아 사용할 수 있다.
public class Consumer {
Food food;
void eat() {
this.food.eat();
}
public void setFood(Food food) {
this.food = food;
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.setFood(new Chicken());
consumer.eat();
consumer.setFood(new Pizza());
consumer.eat();
}
}
interface Food {
void eat();
}
class Chicken implements Food{
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food{
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
public class Consumer {
Food food;
public Consumer(Food food) {
this.food = food;
}
void eat() {
this.food.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer(new Chicken());
consumer.eat();
consumer = new Consumer(new Pizza());
consumer.eat();
}
}
interface Food {
void eat();
}
class Chicken implements Food{
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food{
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
이런 식으로 주입을 할 수 있다.
Inversion of Control 제어의 역전
제어의 역전이란 무엇인가.
강한결합의 코드를 보면
public class Consumer {
void eat() {
Chicken chicken = new Chicken();
chicken.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.eat();
}
}
class Chicken {
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
Consumer 내부에서 직접 Chicken을 생성하고있다. 이는 Consumer가 Chicken을 제어하고있는 것이다. 하지만 위에서 말했듯 이런 경우는 강한결합이기 때문에 수정할 때 코드변경이 많이 일어난다.
그에 반해 아래쪽 생성자를 통한 주입을 보면
public class Consumer {
Food food;
public Consumer(Food food) {
this.food = food;
}
void eat() {
this.food.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer(new Chicken());
consumer.eat();
consumer = new Consumer(new Pizza());
consumer.eat();
}
}
interface Food {
void eat();
}
class Chicken implements Food{
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food{
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
Consumer는 가만히 있다 Food를 상속받은 객체들만 늘어있고 Consumer는 생성될 때 Food를 주입받는다.
강한결합과는 다르게 제어의 흐름을 Consumer에서 Food로 바꿨다 보통의 경우 Consumer가 Food를 제어하는게 맞다고 생각하지만 개발의 경우 그 반대가 되어야 좋은 코드가 만들어 진다.
즉, IoC를 지킨 코드가 좋은 코드가 되었다.
코드에 DI를 행하니 IoC원칙이 지켜졌다.
좋은 코드란
이렇듯 좋은 코드를 작성하기 위해서는 신경써야할 부분이 정말 많다.
Spring은 개발자가 Java를 사용하여 쉽게 좋은 코드를 작성할 수 있도록 도와주는 역할을 해준다.
여기서 IoC와 DI는 좋은 코드 작성을 위한 Spring의 핵심 기술 중 하나이다.