[Spring] DI, DIP, IoC

2023. 9. 9. 16:38학부 강의/웹프로그래밍 (Spring)

0. 참고 자료

아직 배우고 있는 중이라 부정확한 정보가 포함되어 있을 수 있습니다!
주의하세요!

 

올인원 스프링 프레임워크 참고.

 

올인원 스프링 프레임워크 : 네이버 도서

네이버 도서 상세정보를 제공합니다.

search.shopping.naver.com

 

객체 지향 설계 5원칙 - SOLID

객체 지향 설계 5원칙 SOLID에 대해 알아보자.

velog.io

 

Spring | 제어의 역전이란? (What is Inversion of Control?)

# IOC(Inversion of Control) 제어의 역전 직장에 차를 몰고 가는 것은 내가 차를 제어하는 것이다. 직접 차를 운전하는 대신 운전 기사를 고용한다면 이것을 제어의 역전이라고 한다. 차를 직접 운전할

velog.io

 

제어 반전 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. -->

ko.wikipedia.org

 


1. 의존성 주입 (DI)

 

가. 의존성 (Dependency)

 

객체 직접 생성

 

MyCalculator 내부에 CalAdd를 생성한다.

 

MyCalculatorCalAdd을 “이용”한다.

 

MyCalculator는 자신이 직접 연산하지 않고, 각각의 연산 객체들(CalAdd, CalSub, …)에게 위임한다.

 

이는 곧 “MyCalculatorCalAdd에 의존한다”라고 할 수 있다.

 

CalAdd가 변하면 MyCalculator에 영향을 미친다.

 

 


나. 의존성 주입 (DI)

 

  • 의존성 주입 (DI, Dependency Injection)
    : 필요한 (의존하는) 객체를 직접 생성하지 않고 외부에서 주입하는 방식.
    : Dependency Injector가 간접적으로 의존성을 주입하는 방식.

 

MyCalculator가 의존하는 CalAdd, CalSub, CalMul, CalDiv를 외부에서 주입하는 방식으로 변경했다.

 

매개변수를 이용해서 외부에서 전달받았다.

 

 


다. 인터페이스를 활용하도록 수정

package calc_1;

public class MyCalculator {

    public void calAdd(int fNum, int sNum, CalAdd calAdd){
        //ICalculator calculator = new CalAdd();
        int value = calAdd.doOperation(fNum, sNum);
        System.out.println("result : " + value);
    }

    public void calSub(int fNum, int sNum, CalSub calSub){
        //ICalculator calculator = new CalSub();
        int value = calSub.doOperation(fNum, sNum);
        System.out.println("result : " + value);
    }
    public void calMul(int fNum, int sNum, CalMul calMul){
        //ICalculator calculator = new CalMul();
        int value = calMul.doOperation(fNum, sNum);
        System.out.println("result : " + value);
    }
    public void calDiv(int fNum, int sNum, CalDiv calDiv){
        //ICalculator calculator = new CalDiv();
        int value = calDiv.doOperation(fNum, sNum);
        System.out.println("result : " + value);
    }

}

중복되는 코드를 인터페이스를 활용해서 제거한다.

 

package calc_1;

public class MyCalculator {
    public void calculate(int fNum, int sNum, ICalculator calculator){
        int value = calculator.doOperation(fNum, sNum);
        System.out.println("result : " + value);
    }
}

이제 MyCalculator안에는 각각의 연산 객체들(CalAdd, CalSub, …)에 대한 직접적인 언급가 없다.

 

 

package calc_1;

public class MainClass {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        MyCalculator calculator = new MyCalculator();
        calculator.calculate(10,5, new CalAdd());
        calculator.calculate(10,5, new CalSub());
        calculator.calculate(10,5, new CalMul());
        calculator.calculate(10,5, new CalDiv());
    }
}

MainClass에서 같은 calculator.calculate() 메서드에 다른 연산 객체를 전달하면 다른 연산을 수행한다.

 


2. 의존 역전 원칙 (DIP)

 

첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
둘째, 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.

 

  • 의존 역전 원칙
    : Dependency Inversion Principle
    : 객체지향설계 5원칙(SOLID)의 D에 해당한다.
    : 객체에서 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스 or 인터페이스)로 참조하라는 원칙.

 

“이제 MyCalculator안에는 각각의 연산 객체들(CalAdd, CalSub, …)에 대한 직접적인 언급가 없다.” 라는 말의 의미를 조금 더 깊이 이해해 보자.

 

직접적인 언급을 피하면서 외부에서 주입하기 위해서 인터페이스(ICalculator)를 매개변수로 활용했다.

 

즉, 구체적인 각각의 연산 객체들(CalAdd, CalSub, …)보다는 추상화된 상위 요소인 ICalculator에 의존하도록 했다.

 

package calc_1;

public class MyCalculator {

    public void calAdd(int fNum, int sNum){
        ICalculator calculator = new CalAdd();
        int value = calculator.doOperation(fNum, sNum);
        System.out.println("result : " + value);
    }

    public void calSub(int fNum, int sNum){
        ICalculator calculator = new CalSub();
        int value = calculator.doOperation(fNum, sNum);
        System.out.println("result : " + value);
    }
    public void calMul(int fNum, int sNum){
        ICalculator calculator = new CalMul();
        int value = calculator.doOperation(fNum, sNum);
        System.out.println("result : " + value);
    }
    public void calDiv(int fNum, int sNum){
        ICalculator calculator = new CalDiv();
        int value = calculator.doOperation(fNum, sNum);
        System.out.println("result : " + value);
    }

}

인터페이스를 활용하기 전 두 MyCalculator 코드에는 각각의 연산 객체들이 구체적으로 언급된다.

 

MyCalculatorCalAdd 등 각각의 연산 객체에 크게 의존한다.

 

 

package calc_1;

public class MyCalculator {
    public void calculate(int fNum, int sNum, ICalculator calculator){
        int value = calculator.doOperation(fNum, sNum);
        System.out.println("result : " + value);
    }
}

의존성 주입 + 인터페이스를 활용하면서 의존성 역전을 이뤄 객체 간의 결합도를 낮출 수 있었다.

 

 

DI는 DIP를 달성하는 하나의 방식이다.

 


3. 제어의 역전 (IoC)

 

  • IoC
    : Inversion of Control
    : 제어의 역전
    : 전통적인 프로그래밍에서 흐름은 프로그래머가 작성한 프로그램이 외부 라이브러리의 코드를 호출해 이용한다. 하지만 제어 반전이 적용된 구조에서는 외부 라이브러리나 프레임워크의 코드가 프로그래머가 작성한 코드를 호출한다.

 

만약 택시를 탔다고 가정하자.

 

택시 기사에게 자동차의 목적지 외에도 사소한 것 하나하나 모두 지시해야 한다면 번거로울 것이다.

 

 

탑승자 입장에서 “이런 것까지 우리가 알아야하나?” 라는 생각이 들 것이다.

 

탑승자는 그저 가고 싶은 목적지만 알고 있고, 그곳으로 가고 싶을 뿐이다.

 

위와 같은 상황이 “일반적인 제어 흐름”이다.

 

상위 모듈(손님)이 하위 모듈(택시 기사)에게 모든 행동을 지시해야 하므로 명령적 프로그래밍이라고 할 수 있다.

 

 

제어의 역전이 일어나면 …

  1. 손님은 목적지만 전달한다.
  2. 택시 기사는 어떻게든 스스로 목적지까지 도달한다.
  3. 택시가 목적지에 도착하면 택시 기사는 손님에게 도착 사실을 알린다.

 

손님이 택시 기사에게 명령하던 것이 택시 기사가 손님에게 도착함을 알리는 방식으로 제어가 역전된 것이다.

 

출처 : https://tecoble.techcourse.co.kr/post/2021-05-14-inversion-of-control/

 


가. IoC 구현하기.

 

프로그램에서 main()는 프로그램의 시작을 나타낸다.

 

따라서 main()MyCalculator, CalAdd, CalSub, CalMul, CalDiv를 생성하는 것은 main()에게 과도한 업무를 부여한 것이다.

 

이때 “과도한 업무”란 “실제 수행하는 연산 수행의 양, 복잡성 그리고 부담 크다”라는 뜻이 아니다.

 

“복잡한 코드, 떨어지는 가독성, 높은 결합도, 목적을 알 수 없음”을 의미한다.

 

 

모든 작업을 main()이 하나 하나 “제어”해야 한다.

 

프로그램 실행에 필요한 객체는 별도의 클래스(CalAssembler)에서 생성하도록 수정한다.

 

main()은 이 CalAssembler만 생성하면 CalAssembler가 모든 일을 맡아서 처리한다.

 

 

package calc_1;
public class CalAssembler{
    MyCalculator calculator;
    CalAdd calAdd;
    CalSub calSub;
    CalMul calMul;
    CalDiv calDiv;

    public CalAssembler(){
        calculator = new MyCalculator();
        calAdd = new CalAdd();
        calSub = new CalSub();
        calMul = new CalMul();
        calDiv = new CalDiv();

        assemble();
    }

    public void assemble() {
        calculator.calculate(10, 5, calAdd);
        calculator.calculate(10, 5, calSub);
        calculator.calculate(10, 5, calMul);
        calculator.calculate(10, 5, calDiv);
    }
}
package calc_1;

public class MainClass {
    public static void main(String[] args){
        new CalAssembler();
    }
}
  • new CalAssembler();
    : 다음과 같이 객체를 생성하면 해당 객체는 변수나 참조로 바인딩되지 않음. 이는 "익명 객체" 생성으로 볼 수 있으며, 이렇게 생성된 객체는 해당 라인 이후로 접근이 불가능.

 

생성자 안에서 assemble 메소드를 호출하여 연산을 수행한다.

 

이렇게 따로 CalAssembler 클래스를 사용하면, 각 객체의 생성 및 연산의 조합이 CalAssembler 내에 캡슐화되므로 MainClass는 단순해진다.

 

MainClass는 모든 작업을 직접 제어할 필요 없이 CalAssembler가 알아서 처리하고 결과를 알려준다.

 

MainClass의 업무가 간단해졌다.

 


나. IoC 컨테이너와 Bean

 

이제 main() 메서드는 CalAssembler 객체만 그것도 익명 객체만 생성한다. (게으른 놈)

 

CalAssembler와 같이 객체를 생성하고 조립하는 특별한 공간을 스프링에서는 “IoC 컨테이너”라고 한다.

 

IoC 컨테이너 속의 객체를 “Bean”이라고 한다.

 

스프링의 IoC 컨테이너는 Bean을 생성하고 이를 DI(의존성 주입)한다.

 

 

 


4. 정리

 

  • DI : 방식, 테크닉
  • DIP : 원칙
  • IoC : 디자인 패턴

 

DI를 통해서 DIPIoC를 만족하는 프로그램을 구현할 수 있다.

 

실습 코드

project.zip
0.03MB