개발, 공부, 일상 블로그

[Design Pattern] DI(Dependency Injection, 의존성 주입)와 IoC(Inversion of Control, 제어 반전)

|
[Design Pattern] DI(Dependency Injection, 의존성 주입)와 IoC(Inversion of Control, 제어 반전)

🤔 DI와 IoC?

duh

처음 스프링을 접하면 DI (Dependency Injection, 의존성 주입)IoC(Inversion of Control, 제어 반전)라는 개념이 등장한다.
흔히 스프링의 3대 요소들 중 하나로 불리는 DI, IoC에 대해 알아보려고 한다.

DI와 IoC는 디자인 패턴으로 스프링에만 국한되는 것 이 아니기 때문에 디자인 패턴이라는 카테고리로 정리했다.

🐥 의존성 (Dependency)

우선 가장 중요한 개념인 의존성에 대해 정리해보도록 하자.

의존성… 뭔가 딱 와닿지 않는다.

나만 그런 것일 수 있겠지만, 난 의존성이라는 단어가 아주 낯설게 느껴진다.
좀 익숙하게 쉽게 풀어보자. ~에 대한 의존성을 가진다 라는 것은 ~가 필요하다 라고 쉽게 풀어 쓸 수 있다.

예를 들어, Gradle(build.gradle)이나 Maven(pom.xml), 더 나아가 NPM(package.json)을 보면 dependency는 외부 패키지, 라이브러리를 나타낸다.
즉, 해당 패키지, 라이브러리가 필요함을 명시할 때 우리는 dependency, 의존성이라는 말을 쓴다.

A가 B에 대한 의존성을 가진다. 또는 B는 A의 의존성이다.

이 말은 A는 B가 필요하다. 라고 쉽게 풀어 쓸 수 있다.

A는 의존성을 스스로 만든다.

이 말은 A는 자기가 필요한 요소를 스스로 생성한다. 라고 쉽게 풀어 쓸 수 있다.

예를 들어보자,

public class Pizza {
    void eat() {
        System.out.println("피자를 먹었다. 냠냠");
    }
}
public class Person {
    void eat() {
        Pizza pizza = new Pizza();
        pizza.eat();
    }
}

위의 코드에서, PersonPizza의 관계는 어떠한가?

PersonPizza를 직접 만들어 먹는다.

따라서 PersonPizza사이에는 강한 결합 (강한 의존성)이 생긴다.

만약 피자(Pizza)라는게 존재하지 않는다면, 사람(Person)은 식사(eat)를 할 수 없다.
Pizza를 다른 음식으로 대체하려면 Person의 대부분의 코드를 수정해야한다.

pizza

↑ 피자에 강한 의존성을 가지는 강아지

그럼 어떻게 하면 요소들 사이의 결합을 약하게 할 수 있을까?

public interface Food {
    void eat();
}

자바에는 Interface가 있다.
자바가 익숙하지 않은 사람들은 도저히 인터페이스가 왜 필요한 것인지 이해하지 못한다. (나도 그랬다.)

인터페이스를 사용하면 특정 메소드를 반드시 구현하도록 강제할 수 있다.
따라서 해당 인터페이스를 implements 하는 클래스에는 특정 메소드가 존재함이 보장된다.

자, 이제 다시 작성해보자.

public class Pizza implements Food{
    @Override
    void eat() {
        System.out.println("피자를 먹었다. 냠냠");
    }
}
public class Hotdog implements Food{
    @Override
    void eat() {
        System.out.println("핫도그를 먹었다. 냠냠");
    }
}
public class Person {
    void eat(Food food) {
        food.eat();
    }
}

이제 Person은 Pizza 외의 음식도 (Food를 implements하는 클래스라면) 먹을 수 있다.
이 것이 바로 느슨한 결합, 또는 약한 결합, 약한 의존성이다.

hotdog

이제 핫도그도 먹을 수 있다.


💉 DI: 의존성을 주입한다고?

injection

그래 이제 의존성은 알겠다.
주입? 또 뭔가 낯설다…

주입(Injection)전달(Pass)로 해석하면 이해하기 좀 더 수월할 것이다.
의존성을 주입한다는 것은 즉, 필요한 것을 전달한다는 것과 같다.

위의 Person, Food의 예로 쉽게 설명하자면

Person에게 Food를 전달하는 것

이다.

의존성을 전달(주입)하기 위해 사용할 수 있는 방법은 뭐가 있을까?

1. 메소드의 파라미터를 이용한 전달(주입)

public class Person {
    void eat(Food food) {
        food.eat();
    }
}

이게 바로 파라미터를 이용한 전달이다.

하지만 Food를 두고두고 조금씩 먹고싶다면 어떨까?
Person의 필드(Field, 멤버 변수)로 설정한다면 재사용할 수 있을것이다.

public class Person {
    Food food;

    void setFood(Food food) {
        this.food = food;
    }

    void eat() {
        if (food == null) {
            System.out.println("먹을게 없어..");
            return;
        }
        food.eat();
    }
}

이렇게 필드를 설정하는 setFood와 같은 메소드를 Setter라고 한다. (필드 값을 반환하는 getFood와 같은 메소드는 Getter라고 한다.)

위처럼 Setter를 이용해 의존성을 전달(주입)하는 방식을 설정자를 이용한 주입 (Setter Injection)이라고 한다.

2. 생성자를 이용한 전달(주입)

Person 인스턴스를 생성할 때 Food를 전달하는 방식이다.
생성자의 매개변수로 받으면 된다.

public class Person {
    Food food;

    public Person(Food food) {
        if (food == null) {
            throw IllegalArgumentException("음식을 내놔라 😡!!");
        }
        this.food = food;
    }

    void eat() {
        food.eat();
    }
}

이런식으로 만들 수 있겠다. 😏


🙃 IoC: 제어를 반전한다고?

역흐름제어

웹툰 신의탑

자, DI에 대해 알아보았다.
이제 IoC(Inversion of Control, 제어 반전)에 대해 알아보자!

프로그래머가 작성한 프로그램이 재사용 라이브러리의 흐름 제어를 받게 되는 소프트웨어 디자인 패턴
Wiki

한국말이 맞나 싶다… 다른 정의를 찾아보자!

프레임워크가 정의한 인터페이스를 클라이언트 코드가 구현을 하고, 구현된 객체를 프레임워크에 전달(주입)하여
프레임워크가 제어를 가지게 하는 것
Inversion of Control 컨테이너 (IoC Container)란?

좀더 가볍게 정의해보자!

인터페이스를 구현하고, 구현한 객체를 외부에 전달하여 제어를 외부로 넘기는것

이렇게까지 가볍게 정의하면, 앞서 공부한 DI의 개념과 IoC의 개념이 거의 일치한다는 것을 알 수 있다.

Food를 구현한 PizzaPerson에게 전달하여 제어를 Person에게 넘기는 것

PersonPizza를 생성하던 기존의 제어 흐름에서 반전된
Food를 구현하는 Pizza를 생성하여 Person에게 전달하는 역전된 제어 흐름이 바로 IoC의 개념이다.

😎 즉, DI ~= IoC

같다

😱 이게 IoC였어?!!?!?

Java로 쓰레드를 구현해본 적이 있다면 다음과 같은 코드를 한번쯤 사용해봤을 것이다.

new Thread(new Runnable() {
    @Override
    public void run() {
    	// Do something
    }
}).start();

그래서 이게 IoC랑 무슨 상관인데! 싶다면 이 글을 천천히 다시 읽어보자!
(설명이 부족했다 싶으면 다른 포스트를 참고해봐도 좋다.)

Thread의 생성자에 Runnable 인터페이스를 구현한 객체를 넘겨준다.
그리고 start 메소드를 호출하면 내부적으로 Runnable의 run 메소드를 호출한다.

이 것이 바로 DI를 사용한 전달이며, IoC를 사용한 제어이다.

그 외에도 만약 Android 개발을 해보았다면?

btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // Do something
    }
});

리스너로 인터페이스, 콜백을 전달하는 방식도 DI, IoC의 개념이 적용된 것이다!

놀란 고양이


📕 마무리

DI (Dependency Injection, 의존성 주입)IoC(Inversion of Control, 제어 반전)에 대해 깊게 정리해보았다!
우리가 일상속에서 당연한 것으로 받아들이고 개발했던 것들에 이런 개념이 숨어 있었다니… 참 놀랍지 않은가?! (ㅔ)

🚀 참고

  • https://moonscode.tistory.com/58
  • https://vandbt.tistory.com/44
  • https://velog.io/@wickedev/IoC-DIP-IoC-Container-DI-DI-Framework-%EB%8F%84%EB%8C%80%EC%B2%B4-%EA%B7%B8%EA%B2%8C-%EB%AD%94%EB%8D%B0