본문 바로가기

웹 개발/ASP .NET

[ASP.NET MVC5] Ninject로 의존성 주입(DI) 구현하기

의존성 주입(DI)이란?


**의존성 주입(Dependency Injection)**은 인터페이스와 DI 컨테이너를 활용해 MVC 응용프로그램 내부의 구성 요소들을 서로 분리시키는 설계 방식이다. DI 컨테이너는 다음과 같은 역할을 수행한다:

  1. 인터페이스 구현 인스턴스 생성: 객체가 의존하는 인터페이스 구현체의 인스턴스를 생성한다.
  2. 생성자 주입: 생성된 인스턴스를 의존하는 객체의 생성자에 주입한다.
  3. 결합도 감소: 클래스 간의 강한 결합을 제거하고 느슨한 결합을 구현한다.

 

 

 DI를 도입하지 않은 경우: 강한 결합


DI를 도입하지 않으면 클래스 간의 결합도가 높아지고, 재사용성과 확장성이 떨어진다. 아래는 DI 없이 강하게 결합된 구조의 예제다.

 

Product.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace EssentialTools.Models
{
    public class Product
    {
        public int ProductID { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public decimal Price {  get; set; }
        public string Category { get; set; }


    }
}

 

LinqValueCalculator.cs

namespace EssentialTools.Models
{
    public class LinqValueCalculator
    {
        public decimal ValueProducts(IEnumerable<Product> products) {
            return products.Sum(p => p.Price);
        }
    }
}

 

ShoppingCart.cs

namespace EssentialTools.Models
{
    public class ShoppingCart
    {
        private LinqValueCalculator calc;

        public ShoppingCart(LinqValueCalculator calc)
        {
            this.calc = calc;
        }

        public IEnumerable<Product> Products { get; set; }

        public decimal CalculateProductTotal() {
            return calc.ValueProducts(Products);
        }
    }
}

 

=> ShoppingCart 클래스는 LinqValueCalculator와 강하게 결합되어 있다.

 

 

HomeController.cs

namespace EssentialTools.Controllers
{
    public class HomeController : Controller
    {
        private Product[] products = {
            new Product { Name="kayak", Category="WaterSports", Price=275M},
            new Product { Name="Lifejacket", Category="WaterSports", Price=48.95M},
            new Product { Name="Soccer ball", Category="Soccer", Price=19.50M},
            new Product { Name="Corner flag", Category="Soccer", Price=34.95M},
        };

        // GET: Home
        public ActionResult Index()
        {
            LinqValueCalculator calc = new LinqValueCalculator();

            ShoppingCart cart = new ShoppingCart(calc) { Products = products };

            decimal totalValue = cart.CalculateProductTotal();

            return View(totalValue);
        }
    }
}

 

=> HomeController 클래스는 ShoppingCart와 LinqValueCalculator에 강하게 결합되어 있다.

 

문제점

  1. 강한 결합: HomeController는 ShoppingCart와 LinqValueCalculator에 직접적으로 의존한다.
  2. 테스트 어려움: LinqValueCalculator를 대체하거나 테스트용 Mock 객체로 교체하기 어렵다.
  3. 유연성 부족: LinqValueCalculator의 구현을 수정하거나 교체하려면 모든 의존성을 수동으로 수정해야 한다.

 

Ninject 사용하기


인터페이스로 의존성 분리


먼저 인터페이스를 도입하여 계산 로직의 정의와 구현을 분리한다. 이를 통해 구현체를 교체하거나 확장할 수 있는 유연성을 확보할 수 있다.

 

IValueCalculator 인터페이스

계산 로직의 정의를 인터페이스로 추상화한다.

namespace EssentialTools.Models {
    public interface IValueCalculator {
        decimal ValueProducts(IEnumerable<Product> products);
    }
}

 

LinqValueCalculator 구현

인터페이스를 구현한 실제 클래스이다.

namespace EssentialTools.Models {
    public class LinqValueCalculator : IValueCalculator {
        public decimal ValueProducts(IEnumerable<Product> products) {
            return products.Sum(p => p.Price);
        }
    }
}

ShoppingCart 클래스

IValueCalculator 인터페이스를 주입받아 계산 작업을 수행한다.

namespace EssentialTools.Models {
    public class ShoppingCart {
        private IValueCalculator calc;

        public ShoppingCart(IValueCalculator calc) {
            this.calc = calc;
        }

        public IEnumerable<Product> Products { get; set; }

        public decimal CalculateProductTotal() {
            return calc.ValueProducts(Products);
        }
    }
}

 

문제점

ShoppingCart 클래스는 인터페이스를 사용하므로 결합도가 낮지만, 컨트롤러에서 여전히 구현체를 직접 생성해야 한다.

IValueCalculator calc = new LinqValueCalculator();

컨트롤러가 구체적인 구현 클래스에 의존하기 때문에 유연성이 떨어진다.

 

 

 Ninject로 의존성 주입 구현


Ninject 커널 설정

Ninject를 사용하면 의존성을 관리하는 코드를 컨트롤러에서 제거할 수 있다. 아래는 Ninject 커널을 설정하여 IValueCalculator와 LinqValueCalculator 간의 관계를 정의하는 코드이다.

 

NuGet 패키지 설치

먼저 Ninject 패키지를 설치한다.

Install-Package Ninject -Version 3.0.1.10
Install-Package Ninject.Web.Common -Version 3.0.0.7
Install-Package Ninject.MVC3 -Version 3.0.0.6

 

커널 생성 및 객체 생성

public ActionResult Index() {
    IKernel ninjectKernel = new StandardKernel();
    ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();

    IValueCalculator calc = ninjectKernel.Get<IValueCalculator>();

    ShoppingCart cart = new ShoppingCart(calc) {
        Products = products
    };

    decimal totalValue = cart.CalculateProductTotal();

    return View(totalValue);
}

 

설명

  1. 커널 생성: IKernel ninjectKernel = new StandardKernel();
    • Ninject 커널은 의존성 관리를 위한 핵심 엔진이다.
  2. 의존성 바인딩: ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
    • 인터페이스와 구현 클래스 간의 관계를 설정한다.
  3. 객체 생성: ninjectKernel.Get<IValueCalculator>();
    • Ninject가 IValueCalculator의 구현체(LinqValueCalculator)를 생성한다.

 

 

MVC에 의존성 해결자 등록


MVC 응용프로그램이 Ninject를 사용하도록 설정하려면 **의존성 해결자(Dependency Resolver)**를 구현해야 한다.

Ninject 의존성 해결자

namespace EssentialTools.Infrastructure {
    public class NinjectDependencyResolver : IDependencyResolver {
        private IKernel kernel;

        public NinjectDependencyResolver(IKernel kernel) {
            this.kernel = kernel;
            AddBindings();
        }

        public object GetService(Type serviceType) {
            return kernel.TryGet(serviceType);
        }

        public IEnumerable<object> GetServices(Type serviceType) {
            return kernel.GetAll(serviceType);
        }

        private void AddBindings() {
            kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
        }
    }
}

 

  • AddBindings 메서드: 인터페이스와 구현 클래스 간의 관계를 정의한다.
  • GetService 및 GetServices 메서드: MVC가 의존성을 해결할 때 호출하는 메서드로, Ninject 커널을 통해 객체를 생성한다.

App_Start 폴더에 생성된 NinjectWebCommon.cs 파일에 다음 코드를 추가한다.

private static void RegisterServices(IKernel kernel) {
    System.Web.Mvc.DependencyResolver.SetResolver(
        new EssentialTools.Infrastructure.NinjectDependencyResolver(kernel)
    );
}

 

 

HomeController 리팩토링


컨트롤러가 의존성을 선언만 하고, Ninject가 생성자 주입을 통해 구현체를 전달하도록 변경한다.

namespace EssentialTools.Controllers {
    public class HomeController : Controller {
        private IValueCalculator calc;

        private Product[] products = {
            new Product { Name = "Kayak", Category = "WaterSports", Price = 275M },
            new Product { Name = "Lifejacket", Category = "WaterSports", Price = 48.95M },
            new Product { Name = "Soccer ball", Category = "Soccer", Price = 19.50M },
            new Product { Name = "Corner flag", Category = "Soccer", Price = 34.95M },
        };

        public HomeController(IValueCalculator calcParam) {
            calc = calcParam;
        }

        public ActionResult Index() {
            ShoppingCart cart = new ShoppingCart(calc) { Products = products };

            decimal totalValue = cart.CalculateProductTotal();

            return View(totalValue);
        }
    }
}

 

 

의존성 주입의 효과


  1. 클래스 간 결합도 감소: HomeController는 IValueCalculator에만 의존하며, 구체적인 구현체를 알 필요가 없다.
  2. 테스트 용이성: IValueCalculator의 Mock 객체를 쉽게 주입하여 단위 테스트를 수행할 수 있다.
  3. 확장성 향상: 새로운 계산 로직이 필요할 때 인터페이스를 구현한 새로운 클래스를 작성하고, 바인딩만 변경하면 된다.

Ninject는 MVC의 DI를 간단하고 직관적으로 구현할 수 있는 강력한 도구로, DI를 통해 유연하고 테스트 가능한 코드를 작성할 수 있다.