[SpringBoot] 예시 코드로 알아보는 IoC와 DI 개념 실습! - 의존 객체 주입

준우·2022년 6월 23일
2

Spring Boot

목록 보기
6/8
post-thumbnail

🤓 IoC 와 DI

IOC : Inversion of Control
DI : Dependency Injection

IoC 컨테이너는 controller,service, repository 등 핵심 객체 및 다양한 객체를 저장하고 관리하는 창고이다. IoC에 담긴 객체들은 필요에 따라 또다른 객체들에게로 주입될 수 있다. 이렇게 객체 생성과 관리, 주입이 개발자의 코드가 아닌 컨트롤러에 의해 제어되고 프로그램의 흐름이 외부에 의해 통제되는 개념을 IoC라고 한다. 그리고 필요 객체를 외부에서 또다른 객체로 주입하는 방식을 DI라고 한다. IoC와 DI는 객체간의 상호 결합을 낮춰서 더욱 객체 지향적이고 유연한 확장성을 가진 코드를 만들 수 있도록 한다. 예시 코드로 자세히 알아보자.

👨‍💻 예시 코드

😕 Before (의존성 주입 전)

🐷 Pork : 돼지고기

package com.example.firstproject.ioc;

public class Pork {
    private String name;
    public Pork(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

식당에는 많은 메뉴가 있겠지만 그 중 돼지고기는 Pork 클래스를 가진다. 이름 속성을 가지고 있다.

👩‍🍳 Chef : 요리사

package com.example.firstproject.ioc;

public class Chef {
    public String cook(String menu) {
    
        // 재료 준비
        Pork pork = new Pork("한돈 등심");
        
        // 요리 반환
        return pork.getName() + "으로 만든 " + menu;
    }
}

위와 같은 Chef 클래스가 있다. 요리사는 주문이 매개변수로 들어오면 해당 메뉴에 맞는 요리를 위해 재료를 준비하고, 손님에게 요리를 반환한다. 오늘은 식당 이벤트로, 돼지 고기 중에서도 한돈 등심으로 요리를 만들 예정이다.

🍽 ChefTest : 식당 OPEN

package com.example.firstproject.ioc;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ChefTest {

    @Test
    void 돈가스_요리하기() {
        // 준비
        Chef chef = new Chef();
        String menu = "돈가스";
        
        // 수행
        String food = chef.cook(menu);
        
        // 예상
        String expected = "한돈 등심으로 만든 돈가스";
        
        // 검증
        assertEquals(expected, food);
        System.out.println(food);
    }
}

레스토랑이 OPEN 했다. 그리고 요리사에게 돈가스 주문이 전달 됐다. 오늘은 한돈 등심 이벤트 날이다. 그래서 한돈 등심으로 만든 돈가스가 나오는 것을 예상하고 있다. 과연 기대에 맞는 음식이 나올 것인지 테스트 하는 코드이다.

👩‍🍳 Chef : 요리사

package com.example.firstproject.ioc;

public class Chef {
    public String cook(String menu) {
        // 재료 준비
//        Pork pork = new Pork("한돈 등심");
        Beef beef = new Beef("한우 꽃등심");
        // 요리 반환
//        return pork.getName() + "으로 만든 " + menu;
        return beef.getName() + "으로 만든 " + menu;
    }
}

그런데 만약 재료가 소진되어 다른 재료를 사용해야 한다면, 위와 같이 매번 코드를 변경하고 재료 클래스를 생성해야 할까? 요리사가 아니라 공장에서 재료를 준비하도록 하면 된다. 의존 객체를 외부에서 주입 (dependency injection) 하는 것이다.

😚 After (의존성 주입 후)

🥦 Ingredient : 재료

package com.example.firstproject.ioc;

public class Ingredient {
    private String name;
    public Ingredient(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

돼지고기, 소고기 등 재료를 관리하는 부모클래스로 Ingredient 클래스를 만들고, 아래와 같이 재료 들이 Ingredient 클래스를 상속받도록 한다.

🐷 Pork : 돼지고기

package com.example.firstproject.ioc;

public class Pork extends Ingredient {
    public Pork(String name) {
        super(name);
    }
}

🐮 Beef : 소고기

package com.example.firstproject.ioc;

public class Beef extends Ingredient {
    public Beef(String name) {
        super(name);
    }
}

🏭 IngredientFactory : 재료 공장

package com.example.firstproject.ioc;
public class IngredientFactory {
    public Ingredient get(String menu) {
        switch (menu) {
            case "돈가스":
                return new Pork("한돈 등심");
            case "스테이크":
                return new Beef("한우 꽃등심");
            default:
                return null;
        }
    }
}

그리고 이렇게 재료를 관리하는 공장에서 메뉴에 맞는 식재료를 반환해주도록 한다.

이렇게 역할을 분리해서 의존성을 줄이는 방식을 DI라고 하는 것이다.

👩‍🍳 Chef : 요리사

package com.example.firstproject.ioc;

public class Chef {
	// 셰프는 식재료 공장을 알고 있음
    private IngredientFactory ingredientFactory;
    
    // 셰프가 식재료 공장과 협업하기 위한 DI
    public Chef(IngredientFactory ingredientFactory) {
        this.ingredientFactory = ingredientFactory;
    }
    
    public String cook(String menu) {
        // 재료 준비
        Ingredient ingredient = ingredientFactory.get(menu);
        // 요리 반환
        return ingredient.getName() + "으로 만든 " + menu;
    }
}

이제 요리사는 재료를 NEW 키워드로 생성하는 것이 아니라, 공장에서 알아서 메뉴에 맞는 식재료를 반환 받도록 했다. 역할을 분담한 덕분에 요리사는 더 쉽게 요리를 할 수 있게 되었다.

🍽 ChefTest : 식당 OPEN

package com.example.firstproject.ioc;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ChefTest {
    @Test
    void 돈가스_요리하기() {
    
        // 준비
        IngredientFactory ingredientFactory = new IngredientFactory();
        Chef chef = new Chef(ingredientFactory);
        String menu = "돈가스";
        
        // 수행
        String food = chef.cook(menu);
        
        // 예상
        String expected = "한돈 등심으로 만든 돈가스";
        
        // 검증
        assertEquals(expected, food);
        System.out.println(food);
    }
    
    @Test
    void 스테이크_요리하기() {
        
        // 준비
        IngredientFactory ingredientFactory = new IngredientFactory();
        Chef chef = new Chef(ingredientFactory);
        String menu = "스테이크";
        
        // 수행
        String food = chef.cook(menu);
        
        // 예상
        String expected = "한우 꽃등심으로 만든 스테이크";
        
        // 검증
        assertEquals(expected, food);
        System.out.println(food);
    }
}

이렇게 DI를 통해 객체 사이의 상호 결합을 낮춰서 더욱 유연하고 객체지향적인 코드를 만들 수 있게 되었다. 돈가스와 스테이크 모두 코드 변경 없이 요리가 가능하게 코드가 개선된 것이다.

👼 Reference

0개의 댓글