Spring

유연한 코드 설계: 의존성 주입(DI)과 그 장점

백엔드 유성 2023. 8. 5. 19:46

의존관계(Dependency)가 무엇인지부터 알아보겠습니다.

 

셰프와 재료 공급업체를 예로 들겠습니다.

 

셰프 (클래스 A): 셰프는 요리를 만들기 위해 식재료가 필요합니다. 셰프가 재료 없이는 요리를 만들 수 없으므로, 재료에 의존하고 있습니다.
쿠팡 재료 공급업체 (클래스 B): 재료 공급업체는 셰프가 필요로 하는 재료를 제공합니다.

이처럼 셰프 -> 재료 공급업체 의 의존 관계가 형성되었습니다.

 

이를 코드로 나타내면 다음과 같습니다.

 

public class Chef {

    private CoupangIngredientSupplier coupangIngredientSupplier;

    public Chef() {
        this.coupangIngredientSupplier = new CoupangIngredientSupplier();
    }

    public String cook() {
        Ingredients ingredients = coupangIngredientSupplier.supply();
        return cooking(ingredients);
    }

 

이런 코드는 다음과 같은 문제점이 있습니다.

 

1. 코드의 결합도가 높다.

만약 재료 공급 업체가 바뀌어서 CoupangIngredientSupplier 대신 KakaoIngredientSupplier를 사용해야 한다면, 필드/생성자 및 cook 메서드도 변경해야 합니다. 또한 계속해서 supplier가 변경된다면 코드의 유연성이 떨어지죠.

 

2. 객체들 간의 관계가 아닌 클래스 간의 관계가 맺어진다.

객체 지향 프로그래밍의 핵심 원칙 중 하나는 "추상화에 의존해야 하며, 구체적인 클래스에 의존해서는 안 된다"는 것입니다. 이 원칙은 DIP(Dependency Inversion Principle)라고 불리며, SOLID 원칙 중 하나이며, 이 원칙을 지키지 못하는 상태입니다.

 

 

그럼 외부에서의 의존성 주입 즉, DI(Dependency Injection)를 사용한다면 어떨까요?


DI는 의존 관계를 외부에서 주입하는 방식입니다.

셰프가 직접 재료 공급업체를 선택하지 않고, 외부에서 필요한 재료 공급업체를 셰프에게 전달합니다. 이렇게 하면 셰프는 어떤 재료 공급업체와도 협업할 수 있게 되며, 재료 공급업체가 바뀌어도 셰프의 코드는 변경할 필요가 없습니다.

예를 들어, 셰프 클래스를 아래와 같이 수정할 수 있습니다.

public class Chef {

    private IngredientSupplier ingredientSupplier; // 추상화된 인터페이스 사용

    public Chef(IngredientSupplier ingredientSupplier) {
        this.ingredientSupplier = ingredientSupplier; // 외부에서 재료 공급업체 주입
    }

    public String cook() {
        Ingredients ingredients = ingredientSupplier.supply();
        return cooking(ingredients);
    }
}

 


이 경우, Chef는 어떤 Supplier가 오던지 자신의 일에만 집중하면 됩니다. 즉 셰프 클래스는 변경될 필요가 없습니다.

단순히 셰프 객체를 생성할 때 다른 재료 공급업체를 전달하면 됩니다.

이렇게 DI를 사용하면 의존 관계를 더 유연하고 관리하기 쉽게 만들 수 있습니다. 코드의 결합도가 낮아져 변경이 용이하고, 테스트하기도 편리해집니다.

 

의존관계 주입 방법

1. 생성자 주입 (Constructor Injection)

생성자를 통해 의존성을 주입하는 방법으로 특별한 이유가 없지 않는이상 생성자 주입을 권장합니다.

장점

  • 객체가 생성될 때 반드시 필요한 의존성을 보장합니다.
  • 주입된 의존성은 변경할 수 없으므로(불변) 안정성이 높습니다.

단점

  • 생성자가 많아지면 복잡해질 수 있습니다.
  • 순환 의존성이 있을 경우 문제가 발생할 수 있습니다.

 

2. 세터 주입 (Setter Injection)

Setter 메서드를 통해 의존성 주입하는 방법입니다.

장점

  • 객체 생성 후에도 의존성을 변경할 수 있으므로 유연합니다.
  • 선택적인 의존성을 처리하기 쉽습니다.

단점

  • 객체가 완전히 초기화되지 않은 상태에서 사용될 가능성이 있어 오류가 발생할 수 있습니다.
  • 객체의 상태를 변경할 수 있으므로(가변) 안정성이 떨어질 수 있습니다.

 

3. 메서드 주입 (Method Injection)

특정 메서드를 통해 의존성을 주입하는 방법입니다.

장점

  • 특정 동작 수행 시에만 의존성이 필요할 때 유용합니다.

단점

  • 사용이 복잡하고, 객체 초기화를 제어하기 어렵습니다.

 

4. 필드 주입 (Field Injection)

필드에 직접 의존성을 주입하는 방법입니다. 주로 프레임워크에서 리플렉션을 통해 사용됩니다.

장점

  • 코드가 간결합니다.

단점

  • 테스트하기 어렵고, 객체의 상태를 직접 제어하기 어렵습니다.
  • 의존성이 숨겨져 있어, 클래스의 동작을 이해하기 어렵게 만들 수 있습니다.

 

 

의존 관계 주입(DI)의 장점

1. 결합도가 줄어든다

어떤 객체가 다른 객체에 의존한다는 것은, 그 의존 대상의 변화에 취약하다는 뜻입니다. DI를 이용하면 주입받는 대상이 바뀔지 몰라도 해당 객체의 구현 자체를 수정할 일은 없어집니다.

 

2. 유연성이 높아진다

기존 Chef 클래스는 공급업체를 바꾸기 위해서는 생성자 코드를 직접 변경해야 해서 바꾸기 쉽지 않았다. 하지만 DI를 이용하면 생성자의 인수만 다른 공급업체로 바꿔주기만 하면 됩니다.

 

3. 테스트하기 쉬워진다

DI를 이용한 객체는 자신이 의존하고 있는 인터페이스가 어떤 클래스로 구현되어 있는지 알지 못해도 됩니다.

또한 DI의 가장 중요한 부분이라고 생각되는 것은 필요에 따라 Custom 한 클래스를 주입할 수 있다는 것입니다.

예를 들어 BankApi를 사용하는 송금 서비스를 테스트할 때, BankApi대신 BankTestApi를 DI 하여 테스트 환경을 구축할 수 있습니다.

 

4. 가독성이 높아진다

DI는 객체 사이의 의존 관계를 객체 자신이 아닌 외부에서 연결해주는 디자인 패턴입니다. Spring에서는 DI를 통해 느슨한 결합을 유지하면서 코드를 보다 유연하고 테스트하기 쉽게 만들 수 있습니다.