Ioc 와 DI 이해하기

‍박태우·2024년 10월 2일

Spring

목록 보기
3/6

IoC (제어의 역전) 과 DI (의존성 주입하기)

간단히 정리하자면 IoC는 설계원칙이고, DI는 디자인 패턴에 해당한다

예시) 김치 볶음밥 만들기

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

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

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

  • 오일을 두른 팬에 채썬 파를 볶는다.
  • 준비한 햄을 넣고 볶다가 간장을 넣어 풍미를낸다.
  • 설탕에 버무린 김치를 넣고 볶는다.
  • 밥을 넣고 볶는다.
  • 기름 넣고 마무리

=> 그러면 좋은 코드를 위한 Spring 의 IoC 와 DI 는 무엇일까?

좋은 코드란?

  • 논리가 간단하고
  • 중복을 제거하고 표현을 명확히 한다.
  • 코드를 처음 보는 사람들도 쉽게 이해하고 수정가능해야한다.
  • 의존성을 최소화 해야 한다.
  • 기능 추가시 구조 변경이 적어야 한다.

* Ioc 는 DI 와 구별하기 보다는 같이 쓰는 편인 듯

그러면 의존성 이 무엇일까?

의존성 : 자바의 경우 어떤 한 클래스가 다른 클래스에 의존하는 경우를 말함

예시 코드로 알아보자!!!!

  • 강한 의존성을 가지는 ConsumerChicken 클래스
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("치킨을 먹는다.");
  }
}

위 코드는 정상적으로 동작하지만 기능을 확장하려면 코드 수정이 많이 필요하다. 예를 들어 Chicken 객체가 아니라 Pizza 객체를 추가해서 Consumer 객체에세 eat 이라는 행동을 시키기 위해서는 새로운 Pizza 클래스를 만들고 객체를 생성하고 Pizza 객체를 eat 하는 것에 대한 메서드도 만들어야 한다. (굉장히 복잡해짐)

  • 인터페이스를 이용하여 해결하기
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("피자를 먹는다.");
  }
}

Food 인터페이스를 통해 모든 음식의 공통점은 eat 을 추상화 하였다. 그리고 각각의 음식 클래스를 구현하면서 해당 메서드만 구현하면 되며, Consumer 또한 음식 클래스에 대한 인스턴스만 만들어서 eat 하면 되는 것이다. 이때 메서드에서 다형성을 통한 방법을 사용함 (인터페이스를 매개변수로)

주입이란?

코드에서의 주입은 여러 방법을 통해 필요로 하는 객체를 해당 객체에 전달하는 것이며 이를 통해 IoC, DI 를 구현 가능하다

1) 필드에 직접 주입

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("피자를 먹는다.");
    }
}

Consumer 클래스 내부에 Food 인터페이스를 가지는 필드를 생성하고 해당 값을 consumer.food = new Chicken() 처럼 직접 필드에 접근하고 변경하는 형식이다. (필드로 직접 접근)

2) 메서드를 통한 주입

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("피자를 먹는다.");
    }
}

이전과 달리 setFood 라는 메서드와 이의 매개변수로 Food 인터페이스를 받고 이를 통해 Consumer 가 먹을 음식을 설정하도록 하였다.
(메서드를 통해 접근)

3) 생성자를 통한 주입 (추천)

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("피자를 먹는다.");
    }
}

제일 추천하는 방식이다. 생상자를 통해 객체의 생성과 동시에 Food 인터페이스를 가져오고 해당 값을 다형성을 통해 구현함으로서 Consumer 가 Food 를 받아먹는 형태로 올바르게 동작한다. (생성자를 통해 접근)
또한 생성과 함께 food 와의 약한 관계가 맺어지기 때문에 객체지향의 목적에 제일 맞는 것 같기도 하다.

위 방법들 모두 제어의 역전을 가리킨다.
제어의 역전 을 위 예시로 보면

  • Consumer -> Food 를 통해 제어의 흐름이 진행되는 경우에는 새로운 Food를 만들 시에 지속적인 코드 변경이 필요하였지만
  • Food -> Consumer 즉 제어의 역전을 하게 되면 Consumer 는 추가적인 요리는 코드 변경 없이 Food 만 받게 되므로 의존성이 약해지는 효과가 있다.

*항상 위 조건을 만족하는 코드를 한번에 만족 시키기란 쉽지 않은 것 같다. 저런 방식을 생각하는 것 자체도 너무 힘들고 구현하는 것도 처음엔 힘들 것을 잘 안다.
그러나 이 이론을 알고 제어의 역전이라는 개념을 알게 되어서 해당 개념을 가지고 만약 코드가 강한 의존관계라면 제어의 역전을 이용하여 약한 의존관계를 만들어 봐야 겠다는 생각을 하게 된 것 같다.

profile
잘 부탁드립니다.

0개의 댓글