[OOP] 제어의 역전 IoC(Inversion of Control)와 의존관계 주입 DI(Dependency Injection)
- 개요
안녕하세요. 이번 시간에는 제어의 역전 IoC와 의존관계 주입 DI에 대해 알아보겠습니다.
객체 지향 프로그래밍 공부를 하면 반드시 나오는 하나의 개념입니다.
이해하시는데 도움이 되면 좋겠습니다. 혹여나 틀린 부분이 있다면 댓글을 남겨주세요.
- 개념
제어의 역전 IoC(Inversion of Control)
제어의 역전은 제어의 흐름 구조가 뒤바뀌는 것이라고 생각하면 됩니다.
기존에 프로그램에서 실행에 필요한 객체 생성, 연결 그리고 실행하는 데 있어서 프로그래머가 제어하는 방식입니다.
하지만 프로그램을 하다 보면 제어해야 하는 객체가 있는 반면에 없는 객체도 있습니다.
제어하지 않는 객체를 프로그램상 위임하여 제어의 흐름을 바꾸는 방식을 제어의 역전 "IoC(Inversion of Control)"이라고 합니다.
명심해야 하는 개념은 프로그래머가 제어하지 않고 외부에 프로그램이 관리하는 것이라고 생각하시면 됩니다.
의존관계 주입 DI(Dependency Injection)
의존관계 주입이라는 건 "A가 B를 의존한다."라는 표현처럼 B는 의존 대상이며, 그렇기 때문에 A는 B를 의존한다고 볼 수 있습니다.(먼... 개... 삐 소리일까요 ㅋㅋ)
너무 당연한 말을 하는 것 같아서 어이없을 겁니다. 하지만 이 부분이 핵심입니다.
우리는 프로그래밍을 하다보면 이런 식으로 소스 코드를 작성할 때가 있습니다.
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
ArrayList<String> list1 = new ArrayList<>();
}
}
List <String>와 ArrayList <String> 둘의 차이는 무엇일까요? new 키워드를 통해 객체를 인스턴스화 하는 과정은 동일하지만 실제 인스턴스 주체는 다릅니다.
저는 처음에 저 부분을 이해하지 못하고 항상 ArrayLIst를 선언해서 사용했습니다.(사실 프로그래밍을 할 때 문제가 되지 않습니다.)
하지만, 여기서 우리가 알아야할 부분은 확장성입니다.
List로 선언된 변수는 다른 인스턴스 클래스로 변경이 가능합니다. 하지만 ArrayList로 선언한 변수는 ArrayList만 선언받을 수 있습니다.
왜냐고요? List는 클래스 구현체가 아닌 인터페이스 추상화이기 때문입니다.
List 인터페이스
ArrayList 클래스
이 얘기를 왜 하는 걸까...?라는 생각이 드실 거예요 그렇죠? 간단한 예시를 통해 설명드릴게요!
예시 코드
식당에는 매일매일 근무를 해야 하는 사람들이 필요합니다. 그렇기 때문에 하루하루 근무 스케줄표가 존재합니다.
이 부분을 객체지향에 의존관계 주입을 생각하면서 만든다면 어떤 식으로 만들어야 할까요?
스케줄이라는 클래스를 하나 만들겠습니다. (SOLID 원칙이라는 주제를 설명할 때 사용했던 클래스입니다.)
import com.solid_example.interfaces.Calculation;
import com.solid_example.interfaces.Cleanse;
import com.solid_example.interfaces.Cook;
import com.solid_example.interfaces.Serving;
/**
* @author Ryan
* @description SOLID 원칙 - DIP : 프로그래머는 "추상화에 의존해야하며, 구체화(구현체)에 의존하면 안된다." 의존성 주입 원칙을 따르는 방법중 하나입니다.
*
* Serving, Cook, Cleanse, Calculation 은 특정 클래스가 아닌 Interface 입니다.
* 구체화(구현체)에 의존하지 않고 추상화에 의존하고 있습니다.
*
* 이렇게 설정했기 때문에 해당 인터페스를 implements 한 클래스는 다형성에 의해 의존관계가 성립됩니다.
*/
public class Schedule {
private String day; // 근무 요일
private Serving serving; // 서빙직원
private Cook cook; // 요리직원
private Cleanse cleanse; // 세척직원
private Calculation calculation; // 계산 직원
public Schedule(String day, Serving serving, Cook cook, Cleanse cleanse, Calculation calculation) {
this.day = day;
this.serving = serving;
this.cook = cook;
this.cleanse = cleanse;
this.calculation = calculation;
}
@Override
public String toString() {
return "Schedule{" +
"day='" + day + '\'' +
", serving=" + serving +
", cook=" + cook +
", cleanse=" + cleanse +
", calculation=" + calculation +
'}';
}
}
Schedule클래스는 Serving, Cook, Cleanse, Calculation이라는 인터페이스를 의존합니다.
ArrayList처럼 클래스 구현체가 아닌 추상화에 의존하고 있는 걸 알 수 있습니다. 이렇게 만들면 어떻게 될까요?
아까 List 인터페이스로 구현한 변수는 확장을 할 때 편하다고 말씀드렸습니다.
하루하루 근무 스케줄은 매일 다른 사람이 올 수 있지만, 같은 사람이 올수 있습니다. 즉, 동적으로 구현하고 자하는 객체가 변경된다는 것입니다.
실제 Schedule 클래스를 생성하는 부분을 확인해보겠습니다.
/**
* @author Ryan
* @description SOLID 원칙 - SRP : 단일 책임 원칙(Single responsibility principle)
* 모든 클래스는 하나의 책임만 가져야 한다.
*
* Main 클래스는 프로젝트 실행에 대한 책임을 갖는다.
* 나의 책임이라는 것은 모호합니다. 책임이 클수도 있고, 작을수 있다.
* 중요한 기준은 변경입니다. 변경이 있을 때 문제가 생긴다면 그건 단일 책임원칙을 잘 따른 부분이 압니다.
*/
public class Main {
public static void main(String[] args) {
Manager ryanManger = new Manager("Ryan");// 매니저
Chef frodoChef = new Chef("Frodo ");// 주방장
HallStaff tubeHallStaff = new HallStaff("Tube"); //홀직원
KitchenStaff muziKitchenStaff = new KitchenStaff("muzi"); //주방직원
CalculationStaff apeachCalculationStaff = new CalculationStaff("Apeach "); //계산 직원
CleanseStaff neoCleanseStaffr = new CleanseStaff("Neo");// 세척 직원
/**
* @author Ryan
* @description SOLID 원칙 - LSP - 리스코프 치환 원칙(Liskov substitution principle)
* 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
*
* 인터페이스로 설정했기 때문에 하위 구현체가 변경되도 규약을 지킬수 있습니다.
*/
Schedule monday = new Schedule("월요일", tubeHallStaff, muziKitchenStaff, neoCleanseStaffr, apeachCalculationStaff);
Schedule tuesday = new Schedule("화요일", ryanManger, frodoChef, tubeHallStaff, ryanManger);
System.out.println("monday = " + monday);
System.out.println();
System.out.println("tuesday = " + tuesday);
}
}
월요일 근무 스케줄을 만들 때 각 담당해야 하는 포지션에 직원들이 할당되는 걸 확인할 수 있습니다.
여기서 중요한 개념인 "생성자 주입"이라는 개념이 나옵니다.
스케줄 클래스를 생성할 때 실제 구현해야 하는 구현체를 주입합니다.
스케줄 클래스는 하나의 직원을 의존하고 있는게 아니라 추상적인 포지션을 의존하고 있습니다.
(Serving, Cook, Cleanse, Calculation이라는 인터페이스를 의존)
이렇기 때문에 같은 추상 인터페이스를 가지고 있는 구현체는 동일하게 연결될 수 있습니다.
의존관계 주입에서는 생성자 주입, Setter 주입 두 가지 방법이 있습니다. 보편적으로 생성자 주입을 사용합니다.
SOLID원칙에서 OCP원칙을 지키기 위해서입니다. "확장에는 열려있지만, 변경에는 닫혀있어야 한다" 했습니다.
소스 저장소
아래 소스 코드를 통해 관계도를 확인해보세요. :)
Github : https://github.com/Ryan-Sin/solid_example