본문 바로가기
Spring

[Spring] DI, DIP, IoC, 컨테이너

by 2nyong 2023. 4. 24.

1. 의존성 주입(Dependency Injection, DI)

 

의존성 주입은 메인 모듈이 직접 다른 하위 모듈에 대한 의존성을 주기보다는 중간에 의존성 주입자(Dependency Injector)를 두고, 이를 활용해 하위 모듈에 대한 의존성을 간접적으로 주입하는 방식이다. (인프런-CS지식의 정석 참고)

 

개념을 이해하고 나니 굉장히 적절한 설명이었지만, 처음 접했을 때는 이해하기 어려웠기 때문에 풀어서 설명해보려고 한다.


- 의존이란?

B가 변하면 A에 영향을 미치는 관계가 있다고 하자. 이 때 의존관계는 어떻게 될까? A가 B에 의존한다(A → B)고 할 수 있을 것이다. 아직은 뭐 그냥저냥 당연한 이야기처럼 느껴지지 않는가? 하지만 이 간단한 이야기는 이 글에서 첫번째로 중요한 개념이다. 이 관계를 염두에 두고 아래 코드를 살펴보자.

 

class B {
    public void hello() {
        System.out.println("B가 인사합니다. 안녕!");
    }
}

class A {
    public void hello() {
        new B().hello();
    }
}

public class Main {
    public static void main(String args[]) {
        new A().hello();
    }
}

 

## 실행결과
B가 인사합니다. 안녕!

 

만약 class B의 hello() 메서드의 이름이 sayHello() 로 바뀐다면 어떻게 될까? class B에 변경 사항이 생김에 따라, class A의 hello() 메서드의 구현 내용도 아래처럼 바뀌어야 할것이다 .

 

class B {
    public void sayHello() { // B에 변경사항이 발생
        System.out.println("B가 인사합니다. 안녕!");
    }
}

class A {
    public void hello() {
        new B().sayHello(); // A도 함께 수정해야 함
    }
}

public class Main {
    public static void main(String args[]) {
        new A().hello();
    }
}

- 의존성 주입(DI)이 없다면?

'의존'에 대한 이해는 충분히 되었다. 하지만 아직 의존성 주입을 설명하기에는 이르다. 우선 의존성 주입이라는 개념을 적용하지 않고 간단한 예제 코드를 작성해보려고 한다. 

 

라면이라는 클래스를 만든다고 가정해보자. 기본적으로 면과 물이 들어가게 될것이고 아래처럼 나타낼 수 있을 것이다.

 

Ramen과 재료의 의존 관계

class Water {
    public void putWater() { // 라면을 끓이기 위해 물을 넣는 메서드
        System.out.println("냄비에 물을 부어요.");
    }
}

Class Noodle {
    public void putNoodle() { // 라면을 끓이기 위해 면을 넣는 메서드
        System.out.println("냄비에 면을 넣어요.");
    }
}

public calss Ramen {
    // Ramen 클래스 안에 water와 noodle을 선언했다.
    private final Water water;
    private final Noodle noodle;
    
    public Ramen(Water water, Noodle noodle) {
        this.water = water; 
        this.noodle = noodle;
    }
    
    public void make() { // 라면을 만드는 메서드
        // 라면을 끓이기 위해서는 각 재료에 따라 알맞은 메서드를 호출해야 한다.
        water.putWater();
        noodle.putNoodle();
    }
    
    public static void main(String args[]) {
        // ramem을 정의하고
        Ramen ramen = new Ramen(new Water(), new Noodle());
        ramen.make(); // ramen을 만든다.
    }
}
## 실행결과
냄비에 물을 부어요.
냄비에 면을 넣어요.

 

위 코드에서는 무엇과 무엇이 의존관계를 갖고 있을까? Ramen 클래스는 Water 클래스와 Noodle 클래스에 의존한다. 이유는 '의존'에 대해 설명한 부분과 같다. 냄비에 물을 붓는 메서드를 putWater()에서 inWater()로 바꾼다면? 냄비에 면을 넣는 메서드를 putNoodle()에서 inNoodle()로 바꾼다면? Ramen 클래스의 라면을 만드는 메서드인 make()의 내용도 수정되어야 하기 때문이다.


- 의존성 주입(DI) 적용

이제 의존성 주입(DI)을 적용해 Ramen을 더 맛있게 완성시켜 보겠다. 방금 더 맛있게 완성시켜 보겠다고 말했는데, DI를 적용하게 되면 라면의 재료(Ingredient) 추가뿐만 아니라 면과 물도 손쉽게 바꿀 수 있기 때문이다.

 

Ramen과 재료의 의존 관계

interface Ingredient { // 라면 재료의 인터페이스
    void put();
}

class Water implements Ingredient { // 재료 인터페이스로부터 파생된 재료 '물'
    @Override
    public void put() {
        putWater();
    }
    
    public void putWater() { 
        System.out.println("냄비에 물을 부어요.");
    }
}

Class Noodle implements Ingredient { // 재료 인터페이스로부터 파생된 재료 '면'
    @Override
    public void put() {
        putNoodle();
    }
    
    public void putNoodle() { 
        System.out.println("냄비에 면을 넣어요.");
    }
}

public calss Ramen {
    private final List<Ingredient> ingredients;
    
    public Ramen(List<Ingredient> ingredients) {
        this.ingredients = ingredients; 
    }
    
    public void make() { 
        for (Ingredient ingredient : ingredients) {
            ingredient.put();
        }
    }
    
    public static void main(String args[]) {
        List<Ingredient> ingredients = new ArrayList<>();
        ingredients.add(new Water());
        ingredients.add(new Noodle());
        Ramen ramen = new Ramen(ingredients);
        ramen.make();
    }
}

 

이 코드는 의존성 주입(DI)를 사용하지 않았던 코드를 약간 업그레이드하면서 의존성 주입(DI)를 적용한 코드다.

 

  1. Ingredient 라는 라면 재료에 관한 인터페이스를 만들었다.
  2. 기존의 Water, Noodle 클래스를 Ingredient 인터페이스로부터 파생된 클래스(구현체)로 변경했다.
  3. 각 재료 클래스들은 인터페이스의 구현체가 되었으니 인터페이스에 선언된 메서드를 구현한다.
  4. 인터페이스에 선언된 put() 메서드는 각 재료 클래스에서 냄비에 재료를 넣도록 구현된다.
  5. Ramen 클래스의 make() 메서드에서는 각 재료의 put() 메서드가 실행되도록 구현했다.
  6. Ramen 클래스의 main 메서드에서는 재료를 리스트에 모두 담고, Ramen을 선언할 때 전달해주어 모든 재료를 담아 ramen을 만들 수 있게 되었다.

 

 

그렇다면 코드를 이렇게 작성했을 때의 의존관계는 어떻게 되었을까? 이제는 Water 클래스의 putWater 메서드를 inWater로 변경한다고 해도 Ramen 클래스에서 코드를 수정할 필요가 없어졌고, 인터페이스의 구현체로써 오버라이드한 자기 자신의 put() 메서드의 내부 상황만 수정해주면 된다. Ingredient 인터페이스에 각 재료 클래스가 의존하게 된 것이다. 그리고 Ramen 클래스까지도 Ingredient 인터페이스에 의존하게 된다.

 

자! 글의 가장 처음에 "의존성 주입은 메인 모듈이 직접 다른 하위 모듈에 대한 의존성을 주기보다는 중간에 의존성 주입자(Dependency Injector)를 두고, 이를 활용해 하위 모듈에 대한 의존성을 간접적으로 주입하는 방식이다." 라고 했다. 지금까지의 과정들이 한 문장에 담겼다. 

 

의존성 주입(DI)를 통해 하위 모듈을 작성하기 조금 더 편한 구조가 되었다.


2. 의존관계 역전 원칙(Dependency Inversion Principle)

의존관계 역전 원칙(DIP)은 의존성 주입(DI)을 할 때 자연스럽게 적용된다. 

 

우리가 앞서 의존성 주입(DI)을 적용하지 않았을 때 의존관계는 이랬다.

DI 적용전 Ramen과 재료의 의존 관계

그리고 의존성 주입(DI)을 적용했을 때의 의존관계는 이렇게 바뀌었다.

DI 적용후 Ramen과 재료의 의존 관계

의존관계를 모아놓고 보니 Noodle과 Water의 화살표 방향이 뒤집어진것이 보이는데, 이를 의존관계 역전 원칙이라고 한다. Ramen이라는 상위 모듈이 Noodle과 Water라는 하위 모듈에 의존하는 의존 관계였다면 Ingredient라는 인터페이스를 중간에 넣어두면서 이를 뒤집어 놓은 상황이 된것이다. 

 

의존관계 역전 원칙은 2가지 규칙을 따라야 한다.

  1. 상위 모듈(Ramen)은 하위 모듈(Noodle, Water)에 의존해서는 안된다. 둘 다 추상화(Ingredient)에 의존해야 한다.
  2. 추상화(Ingredient)는 세부사항(putWater, putNoodle 등)에 의존해서는 안된다. 세부사항(put)은 추상화에 따라 달라져야 한다.

3. 제어의 역전 Ioc(Inversion of Control)

  • 개발자 입장에서의 자연스러운 코드 흐름이란 일반적으로 코드에서 직접 객체를 생성하고, 호출하는 것이다.
  • 그러나 개발자가 직접 객체를 생성/호출하는 것이 아니라, 프레임워크에 의해서 객체가 생성/호출된다면 제어의 권한은 개발자가 아닌 프레임워크가 갖게 된다.
  • 이렇게 제어의 권한이 뒤바뀌는 것을 제어의 역전(IoC)라고 하며, 제어의 권한을 갖고 있는 것을 IoC 컨테이너라고 한다.

회고

 

사람이 아닌 대학원생으로 살던 시절.. 연구비를 위해 연구 과제를 소화하고, 동시에 학위를 따기 위해 절차 지향 언어로 매일 엄청나게 많은 코딩을 했었다.

 

일을 하다보니 공통적으로 많이 사용되는 분석 기법들이 눈에 들어오기 시작했다.

 

결국에는 NCL이라는 기상 데이터 분석 및 가시화에 특화된 언어를 사용해 데이터 분석용 코드를 작성하고, 그것들을 감싸는 shell로 작성된 설정 코드에서 간단한 파라미터 조작을 통해 자동화 아닌 자동화를 이루고자 노력했었던 기억이 있다.

 

매번 비슷한 코드를 작성하고, 복잡한 데이터 분석을 위해 여러개의 코드를 순서에 맞게 일일히 수행하다 보니 시간에 쫓기기도 하고, 조금은 편해지고 싶었기(귀찮았기) 때문이었다.

 

하지만 NCL이 다형성을 지원하지 않는 언어이기 때문에 결국 설정 코드가 만만치 않게 복잡해졌었다. (설정 코드의 설정 코드의 설정 코드의 설정 코드....)

 

어쨋든 앞서 설명한 개념들이 단순히 귀찮음을 해소하기 위한 것이 아니라, 객체 지향을 더 객체 지향답게 사용할 수 있도록 돕기 위한 것에 비중이 훨씬 높다는 것을 잘 알고 있지만, 간단한 조작으로 코드가 적절히 조합되고 잘 수행되기를 바랬던 마음과 노력만큼은 IoC, DI, 컨테이너를 발견한 그 누군가와 비슷한게 아니었을까 라는 생각이 들었다.

'Spring' 카테고리의 다른 글

[Spring] Controller와 HTTP Request 메시지  (0) 2023.04.15
[Spring] Controller와 HTTP Response 메시지  (4) 2023.04.14

댓글