DI(Dependency Injection)에 대해서

이규훈·2023년 5월 28일
1

스프링 정리

목록 보기
28/30

DI란?

의존성 주입(Dependency Injection, DI)은 객체지향 프로그래밍에서 의존성 문제를 해결하는 디자인 패턴 중 하나입니다. DI는 클래스 내부에서 새로운 객체를 생성하지 않고, 외부에서 생성된 객체를 주입받아 사용하는 방식을 의미합니다.

객체지향 프로그래밍에서 클래스 간의 관계는 필수적인 요소입니다. 한 클래스가 다른 클래스의 메소드를 사용하면, 그 클래스는 다른 클래스에 의존하게 됩니다. 이렇게 되면 클래스 간의 결합도(coupling)가 높아져서 코드의 유연성과 재사용성이 떨어지게 됩니다. 코드의 유연성을 높이기 위해 의존성 주입이 사용됩니다.

예를 들어, 어떤 클래스 A가 다른 클래스 B의 메소드를 사용하려면, A는 B의 인스턴스를 가지고 있어야 합니다. 이를 "클래스 A가 클래스 B에 의존한다"고 합니다. 이 의존성을 직접 관리하는 대신, A는 자신이 사용할 B의 인스턴스를 외부로부터 주입받습니다. 이것이 바로 의존성 주입입니다.

DI의 주요 장점은 다음과 같습니다:

  1. 모듈성: 각 클래스는 자신의 역할에 집중할 수 있습니다. 의존성 관리는 외부에 위임되므로, 코드가 간결하고 이해하기 쉬워집니다.

  2. 테스트 용이성: 의존성을 주입하면, 테스트 중에 실제 의존성을 가짜(mock) 객체로 쉽게 교체할 수 있습니다. 이는 단위 테스트를 보다 쉽게 작성하는 데 도움이 됩니다.

  3. 유연성: 클래스는 구체적인 의존성이 아닌 인터페이스에 의존하게 됩니다. 따라서 실행 시점에 의존성을 변경하거나, 다양한 구현을 쉽게 교체할 수 있습니다.

이런 장점들 때문에, 의존성 주입은 스프링 프레임워크 등 많은 프레임워크에서 채택하고 있는 중요한 디자인 패턴입니다.

의존성 주입(Dependency Injection, DI)의 기본적인 예시를 Java 언어를 통해 보여드리겠습니다.

먼저, 의존성 주입을 사용하지 않는 경우를 보겠습니다. 아래의 코드에서 TextEditor 클래스는 SpellChecker 클래스에 의존하고 있습니다. TextEditor는 직접 SpellChecker의 객체를 생성하고 있습니다.

class SpellChecker {
    public SpellChecker() {
        //...
    }
    public void checkSpelling() {
        //...
    }
}

class TextEditor {
    private SpellChecker spellChecker;

    public TextEditor() {
        this.spellChecker = new SpellChecker();
    }
}

하지만, 위의 코드는 의존성이 고정되어 있어, 유연성이 떨어집니다. SpellChecker의 다른 버전이나 모의 객체(mock object)를 사용하기 어렵습니다.

이제 의존성 주입을 사용한 경우를 보겠습니다. 아래의 코드에서는 TextEditor 클래스가 직접 SpellChecker의 객체를 생성하는 대신, 생성자를 통해 SpellChecker의 객체를 주입받습니다.

class SpellChecker {
    public SpellChecker() {
        //...
    }
    public void checkSpelling() {
        //...
    }
}

class TextEditor {
    private SpellChecker spellChecker;

    public TextEditor(SpellChecker spellChecker) {
        this.spellChecker = spellChecker;
    }
}

위의 코드에서 TextEditor는 SpellChecker의 구체적인 구현에 의존하지 않고, SpellChecker 객체를 주입받아 사용합니다. 이로 인해 TextEditor 클래스의 테스트가 용이해지고, SpellChecker의 다른 구현체를 사용하기 쉬워집니다. 이것이 의존성 주입이 주는 이점입니다.

의존성 주입(Dependency Injection, DI)의 기본적인 예시를 Java 언어를 통해 보여드리겠습니다.

먼저, 의존성 주입을 사용하지 않는 경우를 보겠습니다. 아래의 코드에서 TextEditor 클래스는 SpellChecker 클래스에 의존하고 있습니다. TextEditor는 직접 SpellChecker의 객체를 생성하고 있습니다.

class SpellChecker {
    public SpellChecker() {
        //...
    }
    public void checkSpelling() {
        //...
    }
}

class TextEditor {
    private SpellChecker spellChecker;

    public TextEditor() {
        this.spellChecker = new SpellChecker();
    }
}

하지만, 위의 코드는 의존성이 고정되어 있어, 유연성이 떨어집니다. SpellChecker의 다른 버전이나 모의 객체(mock object)를 사용하기 어렵습니다.

이제 의존성 주입을 사용한 경우를 보겠습니다. 아래의 코드에서는 TextEditor 클래스가 직접 SpellChecker의 객체를 생성하는 대신, 생성자를 통해 SpellChecker의 객체를 주입받습니다.

class SpellChecker {
    public SpellChecker() {
        //...
    }
    public void checkSpelling() {
        //...
    }
}

class TextEditor {
    private SpellChecker spellChecker;

    public TextEditor(SpellChecker spellChecker) {
        this.spellChecker = spellChecker;
    }
}

위의 코드에서 TextEditor는 SpellChecker의 구체적인 구현에 의존하지 않고, SpellChecker 객체를 주입받아 사용합니다. 이로 인해 TextEditor 클래스의 테스트가 용이해지고, SpellChecker의 다른 구현체를 사용하기 쉬워집니다. 이것이 의존성 주입이 주는 이점입니다.


유연성이 떨어진다?

코드의 "유연성이 떨어진다"는 의미는 클래스나 메소드가 특정 구현에 강하게 결합되어 있어서, 다른 구현이나 환경에 쉽게 대응하지 못하는 상태를 가리킵니다. TextEditor가 SpellChecker의 특정 구현에 직접적으로 의존하고 있으므로, 다른 SpellChecker 구현체를 사용하거나, 테스트를 위한 가짜 객체(mock object)를 사용하기가 어렵습니다.

예를 들어, SpellChecker의 새로운 버전이 나왔다고 가정해봅시다. 이 새로운 버전을 사용하려면 TextEditor 클래스의 코드를 직접 수정해야 합니다. 왜냐하면 TextEditor는 SpellChecker를 직접 생성하기 때문입니다. 이런 경우, 코드의 유연성이 떨어진다고 합니다.

가짜 객체?

TextEditor를 테스트하려면 SpellChecker도 함께 테스트해야 합니다. 왜냐하면 TextEditor는 SpellChecker의 객체를 직접 생성하므로, SpellChecker의 동작이 TextEditor의 동작에 영향을 미치기 때문입니다. 이런 경우, SpellChecker의 가짜 객체(mock object)를 사용하여 TextEditor를 독립적으로 테스트하는 것이 어렵습니다. 가짜 객체는 실제 객체와 동일한 인터페이스를 가지나, 테스트에 필요한 동작만을 구현한 객체를 말합니다.

이런 문제를 해결하는 방법 중 하나가 바로 의존성 주입입니다. TextEditor에 SpellChecker의 객체를 외부에서 주입하면, TextEditor는 SpellChecker의 특정 구현에 의존하지 않게 됩니다. 따라서 SpellChecker의 구현을 바꾸거나, 가짜 객체를 사용하는 것이 쉬워집니다.


여기서 SpellChecker의 새로운 버전이 나왔다고 가정해봅시다. 이 새로운 버전을 사용하려면 TextEditor 클래스의 코드를 직접 수정해야 합니다.가 이해가 안가서 좀 더 조사해봤습니다.

코드에서는 TextEditor 클래스는 SpellChecker 클래스의 특정 구현에 직접적으로 의존하고 있스니다. 즉 TextEditor 클래스는 SpellChecker의 객체를 직접 생성하고 있습니다.

따라서 SpellChecker의 구현이 바뀌면, TextEditor 클래스의 코드도 수정해야 합니다.

예를 들어, SpellChecker의 새로운 버전인 AdvancedSpellChecker가 추가되었다고 가정해 봅시다.

class AdvancedSpellChecker {
    public AdvancedSpellChecker() {
        //...
    }
    public void checkSpelling() {
        //...
    }
}

이 새로운 AdvancedSpellChecker를 사용하려면, TextEditor 클래스의 코드를 직접 수정해야 합니다.

class TextEditor {
    private AdvancedSpellChecker advancedSpellChecker;

    public TextEditor() {
        this.advancedSpellChecker = new AdvancedSpellChecker();
    }
}

위와 같이 TextEditor 내부에서 AdvancedSpellChecker를 생성하는 코드로 변경해야 합니다.

하지만 의존성 주입을 사용하면, TextEditor는 SpellChecker의 구현에 의존하지 않고, 외부에서 SpellChecker 객체를 주입받습니다. 따라서 SpellChecker의 다른 구현이 추가되더라도, TextEditor 클래스의 코드는 수정할 필요가 없습니다.

class TextEditor {
    private SpellChecker spellChecker;

    public TextEditor(SpellChecker spellChecker) {
        this.spellChecker = spellChecker;
    }
}

TextEditor 객체를 생성할 때, 원하는 SpellChecker 객체를 주입하면 됩니다.

SpellChecker advancedSpellChecker = new AdvancedSpellChecker();
TextEditor textEditor = new TextEditor(advancedSpellChecker);

이렇게 하면 TextEditor 클래스의 코드는 수정할 필요 없이, 원하는 SpellChecker의 구현을 사용할 수 있습니다. 이것이 의존성 주입의 주요 이점 중 하나입니다.


클래스 이름이 바뀌는거면 의존성주입쓰든말든 수정해야하는거 아닌가?

맞습니다. 클래스의 이름이 변경된 경우는 의존성 주입을 사용하더라도 코드를 수정해야 합니다. 이런 경우를 대비하여 보통은 특정 클래스의 구체적인 구현보다는 인터페이스에 의존하도록 설계합니다.

예를 들어, SpellChecker를 인터페이스로 정의하고, 이를 구현하는 여러 클래스 (예: BasicSpellChecker, AdvancedSpellChecker)를 만들어 둔다면 TextEditor 클래스는 SpellChecker 인터페이스에만 의존하면 됩니다.

interface SpellChecker {
    void checkSpelling();
}

class BasicSpellChecker implements SpellChecker {
    public void checkSpelling() {
        //...
    }
}

class AdvancedSpellChecker implements SpellChecker {
    public void checkSpelling() {
        //...
    }
}

class TextEditor {
    private SpellChecker spellChecker;

    public TextEditor(SpellChecker spellChecker) {
        this.spellChecker = spellChecker;
    }
}

이렇게 되면, 새로운 SpellChecker의 구현이 추가되거나 기존의 구현이 변경되더라도 TextEditor 클래스의 코드는 그대로 유지될 수 있습니다.

SpellChecker spellChecker = new BasicSpellChecker();
TextEditor textEditor = new TextEditor(spellChecker);

이것이 의존성 주입의 기본 원리입니다. TextEditor는 SpellChecker의 구체적인 구현에 의존하지 않기 때문에, 코드의 유연성이 향상되고 테스트하기도 더 쉬워집니다. 이것이 의존성 주입을 사용하는 주요 이유입니다.


spellchecker의 내용이 수정되어도 texteditor는 만질게 없지않나?

여기서 핵심은 TextEditor가 SpellChecker의 특정 구현에 직접 의존하고 있지 않다는 점입니다.

TextEditor는 SpellChecker의 어떤 구현을 사용할지에 대해 알 필요가 없으며, 어떤 SpellChecker 객체가 주입되든 간에 그것을 사용할 수 있습니다. SpellChecker의 인터페이스(메서드 선언)가 바뀌지 않는 한, SpellChecker의 내부 구현이 어떻게 변경되든 TextEditor의 코드를 변경할 필요가 없습니다.

만약 SpellChecker를 인터페이스로 정의하고, 여러 다른 클래스(예: BasicSpellChecker, AdvancedSpellChecker)가 이 인터페이스를 구현한다면, 이러한 각 구현은 SpellChecker 인터페이스를 따르기 때문에 TextEditor는 어떤 종류의 SpellChecker 객체가 주입되든 동일한 방식으로 그것을 사용할 수 있습니다.

다른 관점에서 보면, 이러한 방식은 TextEditor와 SpellChecker의 결합도(coupling)를 낮춥니다. 결합도가 낮다는 것은 한 클래스의 변경이 다른 클래스에 미치는 영향을 최소화한다는 의미입니다. 이것이 소프트웨어 설계에서 바람직한 특징 중 하나입니다.

다시 말해, SpellChecker의 클래스 이름이 변경되거나, 새로운 SpellChecker 구현이 추가되는 경우, TextEditor 클래스의 코드는 그대로 유지될 수 있습니다. 대신에, TextEditor 객체를 생성할 때 주입되는 SpellChecker 객체를 변경하면 됩니다.

만약 의존성 주입을 사용하지 않고 TextEditor 내부에서 SpellChecker 객체를 직접 생성한다면, 다른 SpellChecker 구현을 사용하려면 TextEditor의 코드를 직접 수정해야 할 것입니다. 이것은 유지 보수가 어렵고 버그를 발생시킬 가능성이 높은 방식입니다.


객체를 직접 생성할경우 코드를 수정해야한다?

객체를 직접 생성할경우 코드를 수정해야한다라고 말했는데 객체를 생성하면 aa.method()라는 형식으로 함수를 불러와서 쓸텐데 그럴 경우에는 수정할 필요가 없지않나라고 생각할 수 있습니다.

코드를 통해 설명하면 이해가 더 쉬울 것 같습니다. 다음 예를 봅시다:

class SpellChecker {
    public void check() {
        // check spelling
    }
}

class TextEditor {
    private SpellChecker spellChecker = new SpellChecker();

    public void doSpellCheck() {
        spellChecker.check();
    }
}

위의 코드에서 TextEditor는 SpellChecker의 특정 구현에 의존하고 있습니다. 즉, SpellChecker 클래스를 직접적으로 사용하고 있습니다. 만약 SpellChecker 클래스의 이름이 변경되거나, check 메서드가 변경되면 TextEditor도 그에 맞게 변경해야 합니다. 또한, SpellChecker의 다른 버전이나 구현을 사용하려면 TextEditor 코드를 직접 변경해야 합니다.

하지만 다음과 같이 인터페이스와 의존성 주입을 사용하면 다르게 작동합니다:

interface SpellChecker {
    void check();
}

class BasicSpellChecker implements SpellChecker {
    public void check() {
        // basic spelling check
    }
}

class AdvancedSpellChecker implements SpellChecker {
    public void check() {
        // advanced spelling check
    }
}

class TextEditor {
    private SpellChecker spellChecker;

    public TextEditor(SpellChecker spellChecker) {
        this.spellChecker = spellChecker;
    }

    public void doSpellCheck() {
        spellChecker.check();
    }
}

여기서 TextEditor는 SpellChecker 인터페이스에만 의존하며, 어떤 구현이 사용될지는 알지 못합니다. BasicSpellChecker나 AdvancedSpellChecker 등의 구체적인 구현은 주입되는 객체에 따라 달라집니다.

이렇게 하면 SpellChecker의 구현이 바뀌어도 (새로운 메서드가 추가되거나, 클래스 이름이 변경되거나, 새로운 SpellChecker 구현이 생기더라도), TextEditor 코드는 그대로 둘 수 있습니다. 단지, TextEditor 객체를 생성할 때 사용하는 SpellChecker 객체만 바꾸면 됩니다.

이 방식은 코드의 유지보수성을 향상시키며, 테스트 용이성을 높입니다. 예를 들어, 테스트 중에는 실제 철자 검사 로직 대신 모의 객체(mock object)를 사용할 수 있습니다. 이렇게 하면 실제 철자 검사 로직이 없어도 TextEditor 클래스의 동작을 테스트할 수 있습니다.


TextEditor 객체를 생성할 때 사용하는 SpellChecker 객체만 바꾸면 됩니다?

TextEditor는 SpellChecker 인터페이스에 의존하고 있으며, TextEditor의 생성자에 SpellChecker 타입의 객체를 주입받아 사용합니다. 따라서 TextEditor가 사용할 SpellChecker의 구현은 TextEditor 객체를 생성할 때 결정됩니다.

예를 들어, 기본 철자 검사를 원한다면 BasicSpellChecker 객체를 TextEditor에 주입하고:

SpellChecker basicSpellChecker = new BasicSpellChecker();
TextEditor textEditorWithBasicSpellChecker = new TextEditor(basicSpellChecker);

고급 철자 검사를 원한다면 AdvancedSpellChecker 객체를 TextEditor에 주입합니다:

SpellChecker advancedSpellChecker = new AdvancedSpellChecker();
TextEditor textEditorWithAdvancedSpellChecker = new TextEditor(advancedSpellChecker);

이처럼 TextEditor는 사용할 SpellChecker의 구현을 명시적으로 결정하지 않습니다. 그 대신 TextEditor 객체를 생성할 때 사용할 SpellChecker의 구현을 주입받아 사용합니다. 이런 의존성 주입 패턴은 코드의 유연성을 높여주고, 테스트나 유지보수를 쉽게 해줍니다.

profile
개발취준생

0개의 댓글

관련 채용 정보