[Design pattern - 실무편] 의존성 주입 (Dependency Injection) 구현하기


Study/Design Pattern  2019. 6. 21. 09:00

안녕하세요. 명월입니다.


이 글은 의존성 주입(Dependency Injection)의 대한 설명입니다.


최근에 웹 프레임워크로 MVC 모델을 많이 사용합니다. 아니 많이 사용하는 정도를 넘어서 MVC 모델이 아니면 프로젝트가 진행이 안될 정도로 거의 표준으로 자리 잡았습니다.

거기서 Java의 Spring이나 C#의 MVC 프레임워크에서 의존성 주입, DI 관계로 객체를 선언합니다.


사실 이 글을 쓰기 전에 많이 고민했었습니다. 의존성 주입 패턴이 과연 Gof의 디자인 패턴에 어디에 해당되는 걸까 하고 말입니다.

결론은 인터프리터 패턴과 Builder 패턴이지 않을까 싶네요.


최초에 제가 의존성 주입 패턴이 무엇인가에 대해 처음 공부할 때 DI는 Singleton의 단점을 해결하기 위한 패턴이다라고 들었습니다.

링크 - [Design Pattern] 싱글톤 패턴 (Singleton)


Singleton의 장점이 무엇이냐고 하면 일단 객체를 한 번 생성해서 계속적으로 재 사용하여 성능과 가독성을 아끼는 패턴입니다. 많은 프로그램에서 가장 즐겨쓰는 패턴이지 않을까 싶네요.

그러나 Singleton의 치명적인 단점은 생성자가 private라는 것입니다. 즉, 상속이나 확장이 불가능한 패턴입니다.


왜냐하면 Singleton의 class를 상속받으면 생성자가 private이기 때문에 상속받은 클래스는 생성자를 만들 수 없습니다. 생성자가 없는 클래스는 존재하지 않습니다.

이런 단점이 있기 때문에 프로그램에서 Dao(Database access object)의 경우는 커넥션 제어를 위해 Singleton을 만드는 게 유리하지만 확장성이 용이하지 않기 때문에 singleton을 만들지 못합니다.

singleton이 되지 않으면 유저들이 무분별하게 Dao를 생성할 수 있기 때문에, 리소스의 관리가 어려워 집니다.


이런 단점을 해결하기 위해 고안된 패턴이지 않을까 싶습니다.


예전에 잠시 FactoryDao를 설명할 때 약간 맛보기로 설명한 적이 있습니다.

링크 - [Design pattern - 실무편] FactoryDao 예제 ※ DI(의존성 주입) 구현 예제


그럼 제대로 된 의존성 주입 패턴을 만들어 보겠습니다.

using System;
using System.Linq;
using System.Reflection;

namespace Example
{
  // Interface Model
  interface IModel
  {
    String GetData();
    void Print();
  }
  // Model1
  class Model1 : IModel
  {
    // protected를 함으로써 상속이 가능하나 외부에서는 할당(new)을 할 수 없다.
    protected Model1()
    {

    }
    public String GetData()
    {
      return "Model1";
    }

    public void Print()
    {
      Console.WriteLine("Thid class is Model1");
    }
  }
  // Model2
  class Model2 : IModel
  {
    // protected를 함으로써 상속이 가능하나 외부에서는 할당(new)을 할 수 없다.
    protected Model2()
    {

    }

    public String GetData()
    {
      return "Model2";
    }

    public void Print()
    {
      Console.WriteLine("Thid class is Model2");
    }
  }
  // 의존성 주입의 프로퍼티를 명시할 어트리뷰트
  [AttributeUsage(AttributeTargets.Property)]
  public class DI : Attribute
  {
    public string Name { get; set; }
    public DI(string name)
    {
      this.Name = name;
    }
  }
  
  // 의존성 주입 추상 클래스
  abstract class AbstractController
  {
    public AbstractController()
    {
      // 선언된 클래스 안의 프로퍼티 내에서 DI 어트리뷰트를 가지고 있는 프로퍼티를 찾는다.
      var properties = this.GetType()
                 .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                 .Where(x => x.GetCustomAttribute(typeof(DI)) != null)
                 .ToList();
      // 각 프로퍼티에 의존성 주입을 시작한다.
      foreach (var property in properties)
      {
        // 어트리뷰트의 DI값을 취득한다.
        var di = (DI)property.GetCustomAttributes(true).Where(x => x.GetType() == typeof(DI)).FirstOrDefault();
        Type type = Type.GetType(di.Name);
        // 어트리뷰트를 잘못 작성하면 에러난다.
        if(type == null)
        {
          throw new NotSupportedException();
        }
        // 생성자가 protected이기 때문에 생성자를 가져와서 Invoke형식으로 할당한다.
        var constructor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[0], null);
        var instance = constructor.Invoke(null);
        
        // 프로퍼티가 set 메서드를 가지고 있지 않기 때문에 프로퍼티의 변수를 찾는다.
        var field = this.GetType()
                .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                .Where(x => x.Name.Contains("<" + property.Name + ">"))
                .First();
        // 변수에 할당한다.
        field.SetValue(this, instance);
      }
    }

  }

  class Controller: AbstractController
  {
    // 의존성 타입의 예제. (예제를 위하여 어트리뷰트를 이용했으나 config 파일로 선언해도 된다.)
    [DI("Example.Model1")]
    // 접근자는 private이고 set 메서드는 존재하지 않는다. 의존성 주입으로 클래스를 할당한다.
    private IModel Model { get; }
    
    public void Exec()
    {
      // Model의 값이 의존성 주입으로 Model1 클래스가 할당 되었기 때문에 Model1의 값이 Data를 취득한다.
      Console.WriteLine("Model data = " + Model.GetData());
      // Model1의 클래스의 Print가 실행된다.
      Model.Print();
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // 클래스를 선언하고
      var controller = new Controller();
      // Exec의 함수를 실행한다.
      controller.Exec();

      Console.WriteLine("Press any key...");
      Console.ReadKey();
    }
  }
}

위 소스를 보면 Controller의 클래스의 Model이라는 프로퍼티에 의존성 주입을 할 생각입니다. 보시다시피 접근자는 private이고 set메서드는 선언되지 않았습니다.

그러나 AbstractControler의 클래스에서 의존성 주입으로 Model1를 선언햿습니다.


그러므로 Model의 프로퍼티에는 Model1이 할당되겠네요.

원래 DI 패턴의 특징은 소스 내에서의 클래스 선언이 아닌 환경 설정에서의 클래스 선언이 맞습니다. 그게 가장 정확합니다.

저는 예를 들기 위해 어트리뷰트를 사용했습니다만, App.config를 사용해서 의존성 주입을 한다면 좀 더 완벽한 DI 패턴이지 않을까 싶습니다.


여기까지 의존성 주입(Dependency Injection) 패턴에 대한 설명이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.