Ioc와 DI

OneTwoThree·2022년 10월 29일
0

유튜브 강의링크

지금까지 강의를 진행하면서 객체를 만든 적이 없다.
왜그럴까

IoC Container

Ioc Container는 핵심 객체를 만드는 창고다. 각종 객체를 만들고 관리함

Ioc container의 객체들은 다른 객체에 주입될 수 있고 이것은 개발자의 코드가 아닌 Ioc conatiner에 의해 이루어짐

이렇게 개발자 코드가 아닌 외부에 의해 제어되는 것을 IOC라 한다
= Inversion Of Control

필요한 객체를 외부에서 주입하는 것을 DI = Dependency Injection 이라 한다.

DI와 IOC는 객체간 상호 결합을 낮춰 더 유연하고 객체지향적인 코드를 만들게 한다.


  • DI로 어떻게 코드가 개선되는가
  • IOC 컨테이너에 객체를 등록하고 가져다 사용하기


ioc 패키지와 Chef 클래스를 만들어줬다.

Chef 클래스의 테스트코드를 작성하자
generate > test

class ChefTest {

    @Test
    void 돈가스_요리하기(){
        // 준비
        Chef chef = new Chef();
        String menu = "돈가스";

        // 수행
        String food = chef.cook(menu);


        // 예상
        String expected = "한돈 등심으로 만든 돈가스";

        // 검증
        assertEquals(expected,food);
        System.out.println(food);

    }
}

준비 -> 수행 -> 예상 -> 검증의 단계로 테스트를 진행한다.
Chef 클래스의 cook 메소드를 만들어줘야 한다.

package com.example.firstproject.ioc;

import org.junit.jupiter.api.Test;

public class Chef {

    public String cook(String menu) {

            //재료 준비
            Pork pork = new Pork("한돈 등심");

            //요리 반환
            return pork.getName()+"으로 만든 "+menu;
        }

}

Chef 클래스에 cook 메소드를 만들ㄹ어 준다
이제 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 클래스는 이렇게 만들어 준다.

테스트 성공

   @Test
    void 스테이크_요리하기(){
        // 준비
        Chef chef = new Chef();
        String menu = "스테이크";

        // 수행
        String food = chef.cook(menu);


        // 예상
        String expected = "한우 꽃등심으로 만든 스테이크";

        // 검증
        assertEquals(expected,food);
        System.out.println(food);

    }

스테이크 요리하기를 test하면 실패한다
왜냐하면 chef.cook에서 한돈 등심을 사용하도록 코드를 작성했으니까

public class Chef {

    public String cook(String menu) {

            //재료 준비
            //Pork pork = new Pork("한돈 등심");
            Beef beef = new Beef("한우 꽃등심");

            //요리 반환
            return beef.getName()+"으로 만든 "+menu;
        }

}

Chef의 cook을 이렇게 바꿔줘야 한다.
역시 Beef 클래스를 만들어야 함

package com.example.firstproject.ioc;

public class Beef {
    private String name;

    public Beef(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Beef 클래스, Pork처럼 생성자랑 게터 작성
이렇게하고 스테이크 요리하기 테스트하면 통과

하지만 이제는 돈가스 요리하기가 테스트를 탈락함

즉 계속 코드를 바꿔줘야함

이래서 DI가 필요하다


DI

public class Chef {

    public String cook(String menu) {

            //재료 준비
            //Pork pork = new Pork("한돈 등심");
            Beef beef = new Beef("한우 꽃등심");

            //요리 반환
            return beef.getName()+"으로 만든 "+menu;
        }

}

셰프 클래스에서 직접 재료를 만들고 있는데 이렇게 만들지 말고 재료공장에서 재료를 조달받는 형식으로 만들어야 함

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

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;
        }

}

셰프의 멤버로 IngredientFactory 객체를 넣어준다
cook 메소드에서 ingredientFactory로 ingredient를 얻어서 요리를 반환하게 한다

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;
        }
    }
}

IngredientFactory에서는 전달한 문자열에 따라 적합한 재료를 반환하도록 한다.
Pork와 Beef는 Ingredient를 상속한다.

    @Test
    void 돈가스_요리하기(){
        // 준비
        IngredientFactory ingredientFactory = new IngredientFactory();
        Chef chef = new Chef(ingredientFactory); // DI : 동작에 필요한 객체를 외부에서 받아옴

        String menu = "돈가스";

        // 수행
        String food = chef.cook(menu);


        // 예상
        String expected = "한돈 등심으로 만든 돈가스";

        // 검증
        assertEquals(expected,food);
        System.out.println(food);

    }

테스트 메소드에서는 셰프를 생성할 때 필요한 객체를 생성자의 인자로 전달한다. 이렇게 동작에 필요한 객체를 외부에서 받아오는 것을 의존성 주입이라 하고 이렇게 작성하면 코드 내용을 변경하지 않아도 상황에 맞게 동작한다.

DI 방식을 사용하면 코드 확장도 편리하다
예를 들어 크리스피 치킨을 만드는 메소드도 테스트하면

 @Test
    void 크리스피_치킨_요리하기(){
        // 준비
        IngredientFactory ingredientFactory = new IngredientFactory();
        Chef chef = new Chef(ingredientFactory);
        String menu = "크리스피 치킨";

        // 수행
        String food = chef.cook(menu);


        // 예상
        String expected = "국내산 10호 닭으로 만든 크리스피 치킨";

        // 검증
        assertEquals(expected,food);
        System.out.println(food);
    }

이렇게 코드를 작성해 주면 된다.
그리고 IngredientFactory에서 크리스피 치킨을 처리할 수 있게 해준다

 public Ingredient get(String menu) {
        switch (menu){
            case "돈가스" :
                return new Pork("한돈 등심");
            case "스테이크" :
                return new Beef("한우 꽃등심");
            case "크리스피 치킨":
                return new Chicken("국내산 10호 닭");
            default :
                return null;
        }
    }

이렇게 get 크리스피 치킨이 인자로 전달될 경우 Chicken을 반환하게 하고 Chicken 클래스를 정의해주면 된다.

이렇게 간단하게 확장할 수 있다.


IOC

이제 만든 IngredientFactory랑 Chef 객체를 IOC 컨테이너에 등록해서 가져올 수 있게 해보자.

package com.example.firstproject.ioc;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class ChefTest {

    @Autowired IngredientFactory ingredientFactory;

    @Autowired Chef chef;


    @Test
    void 돈가스_요리하기(){
        // 준비
        //IngredientFactory ingredientFactory = new IngredientFactory();
        //Chef chef = new Chef(ingredientFactory); // DI : 동작에 필요한 객체를 외부에서 받아옴

        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);

    }

    @Test
    void 크리스피_치킨_요리하기(){
        // 준비
        //IngredientFactory ingredientFactory = new IngredientFactory();
        //Chef chef = new Chef(ingredientFactory);
        String menu = "크리스피 치킨";

        // 수행
        String food = chef.cook(menu);


        // 예상
        String expected = "국내산 10호 닭으로 만든 크리스피 치킨";

        // 검증
        assertEquals(expected,food);
        System.out.println(food);
    }
}

클래스에 @SpringBoot 어노테이션을 달아서 스프링부트랑 연동한 테스트로 만들고 @AutoWired로 IngredientFactory 필드랑 셰프 필드를 만들어준다.

package com.example.firstproject.ioc;

import org.springframework.stereotype.Component;

@Component // 해당 클래스를 객체로 만들고 이를 IOC 컨테이너에 등록
public class IngredientFactory {

    public Ingredient get(String menu) {
        switch (menu){
            case "돈가스" :
                return new Pork("한돈 등심");
            case "스테이크" :
                return new Beef("한우 꽃등심");
            case "크리스피 치킨":
                return new Chicken("국내산 10호 닭");
            default :
                return null;
        }
    }
}

IOC 컨테이너에 IngredientFactory를 등록하기 위해 @Component 어노테이션을 달아준다. 해당 클래스를 객체로 만들고 IOC 컨테이너에 등록해준다. Chef 클래스도 마찬가지로 @Component 어노테이션을 달아준다.


요약

  • 객체간 의존성이 높은 코드 : 요구사항 변경에 취약함
  • DI로 코드 개선 : 외부의 요구사항이 변경되어도 내부의 코드 변경 X
  • IOC 컨테이너에 필요한 객체를 @Component로 등록하고 @Autowired를 통해 가져온다.

0개의 댓글