IoC & DI

김재익·2023년 6월 29일
0

SPRING FRAMEWORK

목록 보기
4/6
post-thumbnail

IoC와 DI를 가르쳐주신 강사님이 해주신 말씀이 있다.

  • Spring 으로 개발을 입문한 분들이 처음에 가장 많이 오해하는 것이 바로 IoC 와 DI 가 Spring 에서 처음으로 만든 기능이라고 생각하는 것입니다.
  • IoC, DI는 객체지향의 SOLID 원칙 그리고 GoF 의 디자인 패턴과 같은 설계 원칙 및 디자인 패턴입니다.
  • 이 둘을 더 자세하게 구분해 보자면 IoC는 설계 원칙에 해당하고 DI는 디자인 패턴에 해당합니다.

강사님은 이 용어들을 요리에 비교해서 설명해 주셨다.

김치 볶음밥 맛있게 만드는 방법 (설계 원칙)

맛있는 김치 볶음밥을 만들기 위한 원칙

  • 신선한 재료를 사용한다.
  • 신 김치를 사용한다.
  • 밥과 김치의 비율을 잘 맞춰야 한다.
  • 볶을 때 재료의 순서가 중요하다.

김치 볶음밥 레시피 (디자인 패턴)

맛있는 김치 볶음밥을 만들기 위한 황금 레시피
1. 오일을 두른 팬에 채썬 파를 볶아 파기름을 만든다.
2. 준비한 햄을 넣고 볶다가, 간장 한스푼을 넣어 풍미를 낸다.
3. 설탕에 버무린 김치를 넣고 함께 볶는다.
4. 미리 식혀 둔 밥을 넣어 함께 볶는다.
5. 참기름 한스푼을 넣어 마무리한다.

요리에 비유해 알 수 있듯 IoC는 맛있는 요리를 위해 지켜야할 원칙이며 DI는 실제 요리를 하는 방법이다.

DI

Dependency Injection 의존성 주입

Dependency

먼저 의존성이란 무엇인지 생각해야한다.
의존성이란 우리가 목발을 사용한다면 우리는 목발에 의존하는 것이 된다. 즉 우리는 목발에 의존성을 두게 된 것이다.
이게 무슨 말인가 싶지만 아래의 코드를 보고 생각을 해보자.

public class Consumer {

    void eat() {
        Chicken chicken = new Chicken();
        chicken.eat();
    }

    public static void main(String[] args) {
        Consumer consumer = new Consumer();
        consumer.eat();
    }
}

class Chicken {
    public void eat() {
        System.out.println("치킨을 먹는다.");
    }
}

위 코드에서 Consumer와 Chicken의 관계는 강한결합이 되어있다. 코드는 문제 없이 실행 되지만 Consumer가 치킨이 아니라 피자가 먹고싶으면 몇안되는 코드인데도 수정이 꽤나 필요하다.
그렇다면 Consumer와 Chicken의 결합을 약하게 하는 방법이 뭐가 있을까

Java의 경우 Interface를 활용하면 될 것 같다.

public class Consumer {

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

    public static void main(String[] args) {
        Consumer consumer = new Consumer();
        consumer.eat(new Chicken());
        consumer.eat(new Pizza());
    }
}

interface Food {
    void eat();
}

class Chicken implements Food{
    @Override
    public void eat() {
        System.out.println("치킨을 먹는다.");
    }
}

class Pizza implements Food{
    @Override
    public void eat() {
        System.out.println("피자를 먹는다.");
    }
}

이제 Consumer는 먹고싶은 음식을 생성해서 먹기만 하면되고
음식을 추가 하고 싶으면 Food를 implements해서 만들기만 하면된다 Consumer의 코드는 변함이 없다.
이러한 관계를 약한 결합, 약한 의존성 이라 할 수 있다.

Injection

그렇다면 주입은 무엇일까
주입은 우리가 주사기를 통해 백신을 맞아 백신을 우리 몸에 주입 하는 것 처럼 코드에서도 여러 방법을 통해 필요로 하는 객체를 해당 객체에 전달하는 것이다.

주입을 하는 방법을 알아보자

필드에 직접 주입

public class Consumer {

    Food food;

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

    public static void main(String[] args) {
        Consumer consumer = new Consumer();
        consumer.food = new Chicken();
        consumer.eat();

        consumer.food = new Pizza();
        consumer.eat();
    }
}

interface Food {
    void eat();
}

class Chicken implements Food{
    @Override
    public void eat() {
        System.out.println("치킨을 먹는다.");
    }
}

class Pizza implements Food{
    @Override
    public void eat() {
        System.out.println("피자를 먹는다.");
    }
}

이처럼 Food를 Consumer에 포함시키고 Food에 필요한 객체를 주입받아 사용할 수 있다.

메서드를 통한 주입

public class Consumer {

    Food food;

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

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

    public static void main(String[] args) {
        Consumer consumer = new Consumer();
        consumer.setFood(new Chicken());
        consumer.eat();

        consumer.setFood(new Pizza());
        consumer.eat();
    }
}

interface Food {
    void eat();
}

class Chicken implements Food{
    @Override
    public void eat() {
        System.out.println("치킨을 먹는다.");
    }
}

class Pizza implements Food{
    @Override
    public void eat() {
        System.out.println("피자를 먹는다.");
    }
}

생성자를 통한 주입

public class Consumer {

    Food food;

    public Consumer(Food food) {
        this.food = food;
    }

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

    public static void main(String[] args) {
        Consumer consumer = new Consumer(new Chicken());
        consumer.eat();

        consumer = new Consumer(new Pizza());
        consumer.eat();
    }
}

interface Food {
    void eat();
}

class Chicken implements Food{
    @Override
    public void eat() {
        System.out.println("치킨을 먹는다.");
    }
}

class Pizza implements Food{
    @Override
    public void eat() {
        System.out.println("피자를 먹는다.");
    }
}

이런 식으로 주입을 할 수 있다.

IoC

Inversion of Control 제어의 역전

제어의 역전이란 무엇인가.

강한결합의 코드를 보면

public class Consumer {

    void eat() {
        Chicken chicken = new Chicken();
        chicken.eat();
    }

    public static void main(String[] args) {
        Consumer consumer = new Consumer();
        consumer.eat();
    }
}

class Chicken {
    public void eat() {
        System.out.println("치킨을 먹는다.");
    }
}

Consumer 내부에서 직접 Chicken을 생성하고있다. 이는 Consumer가 Chicken을 제어하고있는 것이다. 하지만 위에서 말했듯 이런 경우는 강한결합이기 때문에 수정할 때 코드변경이 많이 일어난다.

그에 반해 아래쪽 생성자를 통한 주입을 보면

public class Consumer {

    Food food;

    public Consumer(Food food) {
        this.food = food;
    }

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

    public static void main(String[] args) {
        Consumer consumer = new Consumer(new Chicken());
        consumer.eat();

        consumer = new Consumer(new Pizza());
        consumer.eat();
    }
}

interface Food {
    void eat();
}

class Chicken implements Food{
    @Override
    public void eat() {
        System.out.println("치킨을 먹는다.");
    }
}

class Pizza implements Food{
    @Override
    public void eat() {
        System.out.println("피자를 먹는다.");
    }
}

Consumer는 가만히 있다 Food를 상속받은 객체들만 늘어있고 Consumer는 생성될 때 Food를 주입받는다.
강한결합과는 다르게 제어의 흐름을 Consumer에서 Food로 바꿨다 보통의 경우 Consumer가 Food를 제어하는게 맞다고 생각하지만 개발의 경우 그 반대가 되어야 좋은 코드가 만들어 진다.

즉, IoC를 지킨 코드가 좋은 코드가 되었다.
코드에 DI를 행하니 IoC원칙이 지켜졌다.

좋은 코드

좋은 코드란

  • 논리가 간단해야 한다.
  • 중복을 제거하고 표현을 명확하게 한다.
  • 코드를 처음 보는 사람도 쉽게 이해하고 수정할 수 있어야 한다.
  • 의존성을 최소화해야 한다.
  • 새로운 기능을 추가 하더라도 크게 구조의 변경이 없어야 한다.

이렇듯 좋은 코드를 작성하기 위해서는 신경써야할 부분이 정말 많다.

Spring은 개발자가 Java를 사용하여 쉽게 좋은 코드를 작성할 수 있도록 도와주는 역할을 해준다.

여기서 IoC와 DI는 좋은 코드 작성을 위한 Spring의 핵심 기술 중 하나이다.

profile
개발자호소인

0개의 댓글