ASP.NET Core에서 종속성 주입(의존성 주입) 사용하기

  • 41 minutes to read

ASP.NET Core에서는 ASP.NET 4.8과 달리 의존성 주입을 위한 기본 내장 DI 컨테이너를 지원합니다. 의존성 해결을 위한 3가지 DI 컨테이너를 살펴보고 뷰 페이지에 바로 주입해서 사용할 수 있는 @inject 키워드에 대해서도 살펴보겠습니다.

1. 종속성 주입 소개

.NET에서의 의존성 주입(Dependency Injection, DI) 디자인 패턴은 특정 서비스 클래스와 특정 컨트롤러 클래스 간에 의존 관계를 Program.cs(또는 Startup.cs) 파일에서 설정하는 방식을 사용합니다. ASP.NET Core에서는 커뮤니티 기반 DI 컨테이너가 아닌 자체 내장된 새로운 형태의 의존성 주입(Dependency Injection) 시스템이 기본으로 내장되어 있습니다.

즉, ASP.NET Core 내장된 컨테이너를 사용하면 따로 DI 컨테이너를 NuGet 패키지로 받아서 사용할 필요없이 바로 사용할 수 있습니다.

1.1. 용어: DI 컨테이너

.NET에 내장되어 있는 종속성 주입 관련 엔진을 DI 컨테이너라고 부릅니다. DI 컨테이너는, 미리 등록된 여러 종속성들에 요청할 때 해당 종속성에 대한 인스턴스를 생성해 줍니다.

2. ASP.NET Core DI 컨테이너의 세 가지 모드

ASP.NET에서 제공하는 DI(Dependency Injection) 컨테이너는 다음과 같이 세 가지 라이프타임 유형입니다. 간략히 요약했으니 한 번 읽어본 후, 자세한 특징은 실습 예제를 통해서 이해하도록 합니다.

  • Transient: 새로운 인스턴스가 매번 생성됩니다.
    • 의존성이 요청될 때마다 매번 서비스가 생성됩니다.
  • Singleton: 단일 인스턴스가 생성되고 싱글톤으로 처리됩니다.
    • 응용 프로그램이 살아있는 동안 서비스가 생성 후 유지됩니다.
  • Scoped: 현재 스코프 내에 단일 인스턴스가 생성됩니다. 현재 스코프 내에서는 Singleton과 같습니다. 여기서 스코프란 웹으로 요청된 동일 요청을 말합니다.
    • HTTP 요청당 딱 한번만 서비스가 생성됩니다.

어떤 메서드를 사용해야할 지 모르겠다면, 무조건 Transient를 사용하면 됩니다.

코드 모양은 다음과 같이 사용됩니다. 누군가가 IService를 요청하면 Service 클래스의 인스턴스를 제공하는 형태입니다.

  • builder.Services.AddSingleton<IService, Service>();
    • 하나의 인스턴스만 생성됩니다. 프로젝트 전체에서 같은 인스턴스가 생성(앱 전체에서 하나만 생성)됩니다.
  • builder.Services.AddTransient<IService, Service>();
    • 매번 호출할 때마다 생성되는데 새로운 인스턴스 변수 생성마다 다른 인스턴스가 생성됩니다.
  • builder.Services.AddScoped<IService, Service>();
    • 같은 요청(Request)에는 같은 인스턴스가 생성됩니다.

이와 같이 등록된 형식을 사용하려면 컨트롤러에서는 생성자 주입을 통해서, 뷰에서는 @inject 키워드로 등록해야 사용할 수 있습니다.

2.1. 의존성 주입 컨테이너와 생성자 호출

2.1.1. 의존성 주입 컨테이너(Dependency Injection Container)

builder.Services.AddScoped<I인터페이스, 클래스>();

2.1.2. 생성자 호출시 I인터페이스에 대한 개체 자동 생성

public CtorTest(I인터페이스 개체) { }
  • 인터페이스에 대한 계약에 의존이 있다고 판단되면,
  • 컨테이너에 지정된 개체로 구현해줍니다.

3. 의존성 주입의 장점

의존성 주입을 사용하면 개체에 대한 관리의 복잡성을 떨쳐낼 수 있습니다. 모든 관리는 ASP.NET Core의 기본 제공 DI 컨테이너에 맡기면 됩니다. 또한, 각각의 형식 간에 강력하게 결합된 의존성을 제거할 수 있습니다. 그리고 테스트 기반 개발을 즐겨 하는 사람이라면 테스트 코드 작성 시 필요한 메서드만 테스트하고, 등록된 인터페이스에게 가짜 데이터를 넘겨주어 테스트하는 식으로 적용할 수 있습니다.

3.1. 종속성 주입 장점

현재 시점에서는 종속성 주입의 장점을 이해하기 어려울 수 있습니다. 제 강의 기준으로 상위 과정인 Blazor Server까지 학습이 이루어지면, DI의 장점을 더 잘 이해할 수 있습니다. 지금은 가볍에 읽고 넘어가세요.

  • 결합도 감소
  • 테스팅 편리
  • 서비스 수명 관리

4. [실습] DI 사용을 위한 기본 설정 단계 살펴보기

4.1. 소개

ASP.NET Core에서 DI를 사용하기 위한 기본 설정 절차를 진행해보겠습니다. 미리 살펴보기 형태로 전체 진행 사항을 본 후 다음에 이어질 실습에서 한 번 더 설명합니다.

NOTE

이번 실습 과정을 "ASP.NET Core에서 의존성 주입 사용하기 - DI 사용을 위한 기본 설정 단계 살펴보기"라는 제목의 동영상 강좌로도 마련하였으니 참고하기 바랍니다.

4.2. 따라하기

(1) DotNetNote 프로젝트를 열고 Services 폴더를 생성합니다.

(2) 다음과 같이 InfoService 클래스를 생성합니다. 이곳에 GetUrl() 메서드를 만들고, URL 하나를 반환시켜 줍니다. InfoService 클래스의 인스턴스 생성 후 GetUrl() 메서드를 호출하면 단순히 URL 정보 하나가 반환되는 코드입니다.

코드: Services/InfoService.cs

namespace DotNetNote.Services
{
    public class InfoService
    {
        public string GetUrl()
        {
            return "http://www.gilbut.co.kr";
        }
    }
}

(3) Controllers 폴더에 SingletonDemoController.cs 이름으로 컨트롤러를 생성하고, 다음과 같이 기본 생성 코드를 확인합니다.

코드: Controllers/SingletonDemoController.cs

using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class SingletonDemoController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}

(4) SingletonDemoController.cs 컨트롤러의 Index 액션 메서드에 다음과 같이 간단히 URL 정보를 뷰 페이지로 보내주는 코드를 작성합니다.

코드: Controllers/SingletonDemoController.cs

using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class SingletonDemoController : Controller
    {
        public IActionResult Index()
        {
            ViewData["Url"] = "www.gilbut.co.kr";
            return View();
        }
    }
}

(5) Views 폴더에 SingletonDemo 폴더를 생성합니다. 이 폴더에 Index.cshtml 뷰 페이지를 생성하고 다음과 같이 코드를 작성합니다.

코드: Views/SingletonDemo/Index.cshtml 페이지

//  
@{ 
    Layout = null;
}
 
사이트 URL: @ViewData["Url"]

(6) 웹 브라우저를 실행하고, /SingletonDemo로 경로를 요청하면 다음과 같이 URL이 출력됩니다.

그림: 1. URL 출력

URL 출력

(7) 이번에는 컨트롤러에서 직접 URL을 반환시키는 방식이 아니라 앞서 생성한 InfoService 클래스를 사용해봅니다. SingletonDemoController 컨트롤러에 InfoServiceDemo 액션 메서드를 다음 코드와 같이 추가합니다. InfoService 클래스의 GetUrl() 메서드를 호출해서 그 결괏값을 뷰 페이지에 전달하는 내용입니다. 뷰 페이지는 따로 만들지 않고 Index 뷰 페이지를 호출합니다. 여기서 SingletonDemoController 컨트롤러 클래스는 InfoService 클래스와 의존 관계다.

***코드: ***

//  Controllers/SingletonDemoController.cs
using DotNetNote.Services;
using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class SingletonDemoController : Controller
    {
        public IActionResult Index()
        {
            ViewData["Url"] = "www.gilbut.co.kr";
            return View();
        }
 
        public IActionResult InfoServiceDemo()
        {
            InfoService svc = new InfoService();
            ViewData["Url"] = svc.GetUrl();
            return View("Index");
        }
    }
}

(8) 다시 웹 브라우저를 실행하여 /SingletonDemo/InfoServiceDemo로 경로를 요청하면 다음과 같이 URL이 출력됩니다.

그림: 2. InfoServiceDemo 뷰 페이지 실행

InfoServiceDemo 뷰 페이지 실행

(9) 생성자 주입 방식을 사용하여 의존성을 제거하는 방법을 살펴보겠습니다. 컨트롤러에서 생성자 주입 방식을 사용하여 InfoService클래스를 SingletonDemoController 컨트롤러에 주입시킵니다. 읽기 전용 필드에 값을 반환하고 새롭게 만든 ConstructorInjectionDemo 액션 메서드에서 이 값을 사용해 이를 뷰 페이지에 전달합니다(다음 코드의 _svc 필드는 반드시 readonly일 필요는 없다).

코드: Controllers/SingletonDemoController.cs

using DotNetNote.Services;
using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class SingletonDemoController : Controller
    {
        private readonly InfoService _svc;
 
        public SingletonDemoController(InfoService svc)
        {
            _svc = svc;
        }
 
        public IActionResult ConstructorInjectionDemo()
        {            
            ViewData["Url"] = _svc.GetUrl();
            return View("Index");
        }
 
        public IActionResult Index()
        {
            ViewData["Url"] = "www.gilbut.co.kr";
            return View();
        }
 
        public IActionResult InfoServiceDemo()
        {
            InfoService svc = new InfoService();
            ViewData["Url"] = svc.GetUrl();
            return View("Index");
        }
    }
}
NOTE

필드 자동 생성

생성자 매개변수와 이에 해당하는 필드를 생성하는 방법은 직접 타이핑하는 방식이 있고, Visual Studio에서 필드를 자동으로 생성해주는 방식도 있습니다. private readonly InfoService _svc; 코드와 같이 필드 생성 코드를 먼저 작성하지 않고, 생성자에서 _svc에 커서를 두면 왼쪽에 노랑 전구 표시가 뜬다. 이를 선택하면 아직 만들어지지 않은 _svc 멤버를 필드, 읽기 전용 필드 또는 속성 등으로 자동 생성해주는 메뉴가 나타난다. 이를 선택하면 필드에 대한 코드를 좀 더 빨리 만들어낼 수 있습니다. 하지만 이 책(강의)은 직접 코드 전체를 타이핑하는 방식을 사용하겠습니다.

그림: 3. 필드 자동 생성

필드 자동 생성

(10) 웹 페이지를 실행하여 /SingletonDemo/ConstructorInjectionDemo로 경로를 요청하면 에러가 발생합니다. 이는 의도된 결과이기에 다음 단계에서 이를 해결하도록 합니다.

그림: 4. 의존성 해결이 되지 않은 상태에서 에러 발생

의존성 해결이 되지 않은 상태에서 에러 발생

(11) 자, 발생한 에러를 해결해보겠습니다. 프로젝트 루트의 Startup.cs 파일을 실행합니다. Startup.cs 파일의 상단의 네임스페이스 선언 영역에 DotNetNote.Services 항목을 추가합니다.

코드: Startup.cs 파일에 네임스페이스 추가

using DotNetNote.Services;

(12) Startup.cs 파일의 ConfigureServices() 메서드의 제일 하단에 서비스 등록 코드를 추가합니다. 다음 코드와 같이 Services.AddSingleton() 메서드에 등록합니다. AddSingleton 메서드가 InfoService 클래스의 인스턴스를 생성해 줍니다. AddSingleton 메서드란 특정 컨트롤러에서 InfoService 클래스가 생성자 주입으로 사용될 때 외부 파일인 Startup.cs 파일에서 이에 대한 의존성을 해결해주는 DI 컨테이너 중 하나다.

코드: Startup.cs 파일

//[DI] InfoService 클래스 의존성 주입
services.AddSingleton<InfoService>();

(12) 다시 /SingletonDemo/ConstructorInjectionDemo 경로를 요청하면 정상적으로 실행됩니다.

그림: 5. 의존성 해결 후 정상 실행

의존성 해결 후 정상 실행

(13) 생성자에 클래스를 대입하는 생성자 주입 방식을 통해서 1차로 해결하였습니다. 이번에는 인터페이스로 클래스를 추출하여 인터페이스를 통한 의존성 주입 기능을 구현해보겠습니다. Services 폴더에 IInfoService 인터페이스를 다음과 같이 생성합니다.

***코드: ***

//  Services/IInfoService.cs
namespace DotNetNote.Services
{
    public interface IInfoService
    {
        string GetUrl();
    }
}

(14) IInforService 인터페이스를 상속 받는 형태로 InforService 클래스를 다시 작성합니다.

***코드: ***

//  Services/InfoService.cs
namespace DotNetNote.Services
{
    public class InfoService : IInfoService
    {
        public string GetUrl()
        {
            return "http://www.gilbut.co.kr";
        }
    }
}

(15) SingleonDemoController 컨트롤러에서 InforService 클래스 부분을 인터페이스인 IInfoService로 변경합니다. 다음 코드는 완성된 SingleonDemoController 컨트롤러 클래스다.

***코드: ***

//  Controllers/SingletonDemoController.cs
using DotNetNote.Services;
using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class SingletonDemoController : Controller
    {
        // [1] 생성자에 클래스 주입
        // private readonly InfoService _svc;
 
        // public SingletonDemoController(InfoService svc)
        // {
        //    _svc = svc;
        // }
 
        // [2] 생성자에 인터페이스 주입
        private readonly IInfoService _svc;
 
        public SingletonDemoController(IInfoService svc)
        {
            _svc = svc;
        }
 
        public IActionResult ConstructorInjectionDemo()
        {            
            ViewData["Url"] = _svc.GetUrl();
            return View("Index");
        }
 
        public IActionResult Index()
        {
            ViewData["Url"] = "www.gilbut.co.kr";
            return View();
        }
 
        public IActionResult InfoServiceDemo()
        {
            InfoService svc = new InfoService();
            ViewData["Url"] = svc.GetUrl();
            return View("Index");
        }
    }
}

(16) 다시 Startup.cs 파일의 ConfigureServices 메서드 하단에 IInfoService 인터페이스에 대한 의존성을 해결하는 코드를 다음과 같이 코드를 등록합니다. 참고로 AddSingleton(), AddScoped(), AddTransient() 모두 사용 가능합니다.

***코드: ***

//  Startup.cs
services.AddSingleton<InfoService>();
services.AddSingleton<IInfoService, InfoService>();

(17) 최종적으로 /SingletonDemo/ConstructorInjectionDemo 경로를 요청하면 다음과 같이 IInforService 인터페이스를 상속 받아 구현한 InforService 클래스의 메서드가 SingletonDemo 컨트롤러에 인터페이스 생성자 주입 형태로 주입되어 실행됩니다.

그림: 6. 웹 브라우저 실행 결과

웹 브라우저 실행 결과

4.3 마무리

이번 실습에서 작성한 InfoServiceDemo() 액션 메서드에서 사용한 것처럼 직접 InfoService 클래스의 인스턴스를 생성 후 GetUrl 메서드를 호출해도 전혀 문제가 되지 않습니다. 다만, 이런 방식은 컨트롤러와 외부 서비스 클래스 간에 밀접하게 연관되어 있어서 의존성이 있습니다. 이러한 의존성은 외부 설정 방식(Startup.cs)으로 제거할 수 있습니다. ASP.NET Core에서는 AddSingleton()과 같은 메서드를 기본으로 제공합니다.

5 [실습] 인터페이스를 사용한 생성자 주입으로 DI 구현하기

5.1 소개

ASP.NET Core에서 의존성 주입을 적용하는 내용을 단계별로 다시 한 번 진행해 보겠습니다.

5.2 따라하기 1: ViewBag으로 문자열 받아 출력하기

(1) ASP.NET Core 학습용 실습 데모 프로젝트인 DotNetNote 웹 프로젝트의 Controllers 폴더에 DependencyInjectionDemoController.cs 컨트롤러 클래스 파일을 생성합니다.

***코드: ***

//  Controllers/DependencyInjectionDemoController.cs
using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class DependencyInjectionDemoController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}

(2) 자동 생성된 Index 액션 메서드에 다음과 같이 코드를 작성합니다. Index 액션 메서드는 단순히 ViewBag 개체에 Copyright 속성을 만들어 카피라이트 정보를 문자열로 담는 역할을 합니다.

***코드: ***

//  Controllers/DependencyInjectionDemoController.cs 코드 변경
using Microsoft.AspNetCore.Mvc;
using System;
 
namespace DotNetNote.Controllers
{
    public class DependencyInjectionDemoController : Controller
    {
        public IActionResult Index()
        {
            ViewBag.Copyright =
                $"Copyright {DateTime.Now.Year} all right reserved.";
 
            return View();
        }
    }
}

(3) Views 폴더에 DependencyInjectionDemo 이름으로 폴더를 생성합니다. 이곳에 Index.cshtml 뷰 페이지를 생성하고 다음과 같이 코드를 작성합니다. 컨트롤러에서 전송된 ViewBag 개체의 값을 출력하는 게 전부다.

***코드: ***

//  Views/DependencyInjectionDemo/Index.cshtml
@{
    Layout = null;
}
 
@ViewBag.Copyright

(4) DotNetNote 프로젝트를 웹 브라우저로 실행 후 /DependencyInjectionDemo/Index 경로를 실행하여 카피라이트 문자열이 정상 출력됨을 확인합니다. 한 줄짜리 코드지만 10줄 또는 100줄짜리 어떤 기능의 결과라고 생각하고, 이를 액션 메서드에서 직접 기입하는 게 아니라 다른 클래스에서 구현하는 방식으로 확장해 나갑니다.

그림: 7. ViewBag 개체를 사용한 뷰 페이지 실행 결과

ViewBag 개체를 사용한 뷰 페이지 실행 결과

5.3 따라하기 2: 클래스를 사용하여 문자열 받아 출력하기

(1) 이번에는 위에서 생성된 문자열(또는 어떤 기능)을 직접 입력하지 않고, 서비스 클래스를 만들어 반환해 주는 방식으로 한 단계 더 진행해보겠습니다. DotNetNote 프로젝트 루트의 Services 폴더에 CopyrightService.cs 이름으로 클래스 파일을 생성하고 다음과 같이 코드를 작성합니다.

***코드: ***

//  Services/CopyrightService.cs 
using System;
 
namespace DotNetNote.Services
{
    public class CopyrightService
    {
        public string GetCopyrightString()
        {
            return $"Copyright {DateTime.Now.Year} all right reserved."
                + $" from CopyrightService";
        }
    }
}

(2) DependencyInjectionDemoController.cs 파일의 Index 액션 메서드로 돌아와서 직접 문자열을 입력했던 부분을 지우고 CopyrightService 서비스 클래스의 인스턴스를 생성하고 GetCopyrightString() 메서드의 결괏값을 전달합니다.

***코드: ***

//  Controllers/DependencyInjectionDemoController.cs 코드 변경
using DotNetNote.Services;
using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class DependencyInjectionDemoController : Controller
    {
        public IActionResult Index()
        {
            CopyrightService _svc = new CopyrightService();
            ViewBag.Copyright = _svc.GetCopyrightString();
 
            return View();
        }
    }
}

(3) DotNetNote 프로젝트를 웹 브라우저로 실행 후 /DependencyInjectionDemo/Index 경로를 실행하여 다시 웹 브라우저로 실행해보면 결과는 같습니다. 구분하기 위해 뒤에 “from CopyrightService” 문자열을 추가한 것뿐입니다.

그림: 8. 서비스 클래스를 사용한 뷰 페이지 실행 결과

서비스 클래스를 사용한 뷰 페이지 실행 결과

(4) 서비스 클래스로 분리한 것의 장점을 살려 재사용한다는 것을 확인해보겠습니다. DependencyInjectionDemoController.cs 컨트롤러에 다음과 같이 About 액션 페이지를 만들고 서비스 클래스를 똑같이 호출해서 사용해봅니다.

***코드: ***

//  Controllers/DependencyInjectionDemoController.cs 코드 추가
using DotNetNote.Services;
using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class DependencyInjectionDemoController : Controller
    {
        public IActionResult Index()
        {
            CopyrightService _svc = new CopyrightService();
            ViewBag.Copyright = _svc.GetCopyrightString();
 
            return View();
        }
 
        public IActionResult About()
        {
            CopyrightService _svc = new CopyrightService();
            ViewBag.Copyright = _svc.GetCopyrightString();
 
            return View();
        }
    }
}

(5) Views 폴더의 DependencyInjectionDemo 폴더에 About.cshtml 뷰 페이지를 생성 후 다음과 같이 코드를 작성합니다.

***코드: ***

//  Views/DependencyInjectionDemo/About.cshtml 
@{
    Layout = null;
}
 
@ViewBag.Copyright

(6) DotNetNote 프로젝트를 웹 브라우저로 실행 후 /DependencyInjectionDemo/About 경로를 실행하면 Index와 똑같이 출력됩니다. 한 번 만들어 놓은 CopyrightService를 Index와 About에서 같이 호출해서 사용하는 것을 보여줍니다.

그림: 9. About 액션 메서드 실행

About 액션 메서드 실행

5.4 따라하기 3: 생성자에서 직접 클래스의 인스턴스 생성

(1) DependencyInjectionDemoController.cs 컨트롤러 클래스를 열고 다음과 같이 코드를 변경합니다.

***코드: ***

//  Controllers/DependencyInjectionDemoController.cs 코드 변경
using DotNetNote.Services;
using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class DependencyInjectionDemoController : Controller
    {
        private CopyrightService _svc;
        public DependencyInjectionDemoController()
        {
            _svc = new CopyrightService();
        }
 
        public IActionResult Index()
        {
            ViewBag.Copyright = _svc.GetCopyrightString();
 
            return View();
        }
 
        public IActionResult About()
        {
            ViewBag.Copyright = _svc.GetCopyrightString();
 
            return View();
        }
    }
}

이번에는 각각의 액션 메서드에서 서비스 클래스의 인스턴스를 생성하고 메서드를 호출하는 대신 컨트롤러 클래스의 생성자에서 한번 인스턴스 생성 후 _svc 변수로 Index 액션 메서드와 About 액션 메서드에서 함께 사용해봅니다.

(2) DotNetNote 프로젝트를 웹 브라우저로 실행 후 /DependencyInjectionDemo 경로와 /DependencyInjectionDemo/About 경로를 요청해보겠습니다. 실행 결과는 같습니다.

그림: 10. Index와 About 뷰 페이지 실행

Index와 About 뷰 페이지 실행

5.5 따라하기 4: 생성자 주입을 통한 클래스의 인스턴스 생성

(1) DependencyInjectionDemoController.cs 컨트롤러 클래스를 열고 다음과 같이 코드를 변경합니다.

***코드: ***

//  Controllers/DependencyInjectionDemoController.cs 코드 변경
using DotNetNote.Services;
using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class DependencyInjectionDemoController : Controller
    {
        private CopyrightService _service;
        public DependencyInjectionDemoController(CopyrightService service)
        {
            _service = service;
        }
 
        public IActionResult Index()
        {
            ViewBag.Copyright = _service.GetCopyrightString();
 
            return View();
        }
 
        public IActionResult About()
        {
            ViewBag.Copyright = _service.GetCopyrightString();
 
            return View();
        }
    }
}

(2) DotNetNote 프로젝트를 웹 브라우저로 실행 후 /DependencyInjectionDemo 경로와 /DependencyInjectionDemo/About 경로를 요청해보겠습니다. 정상적으로 실행되지 않고 에러가 발생할 것입니다.

그림: 11. 의존성 해결이 되지 않아 에러 발생

의존성 해결이 되지 않아 에러 발생

(3) 에러를 해결하기 위해 프로젝트 루트에 있는 Startup.cs 파일을 실행합니다. Startup.cs 파일의 ConfigureService 메서드에 서비스 클래스를 등록하는 코드가 필요합니다. 다음 코드처럼 상단에 네임스페이스를 추가하고, ConfigureServices 메서드 하단에 서비스 등록 코드 한 줄을 추가합니다. AddTransient() 메서드를 통해서 CopyrightService 클래스가 생성자 매개변수로 전달 시 해당 클래스의 인스턴스가 자동 생성되도록 설정하는 코드입니다. 참고로 Startup.cs 파일의 나머지 코드는 생략한 상태입니다.

***코드: ***

//  Startup.cs 파일의 ConfigureService에 서비스 등록 코드 추가
using DotNetNote.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
 
namespace DotNetNote
{
    public class Startup
    {
        public Startup(IWebHostEnvironment env)
        {

        }
 
        public IConfigurationRoot Configuration { get; }
 
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
 
            //[DI(Dependency Injection)] 서비스 등록
            services.AddTransient<CopyrightService>();
        }
        
        public void Configure(
            IApplicationBuilder app, 
            IWebHostEnvironment env, 
            ILoggerFactory loggerFactory)
        {
            app.UseStaticFiles();
 
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}
TIP

네임스페이스 자동 생성

Startup.cs 파일에서 특정 코드 작성시 그림과 같이 빨간색 밑줄이 가면 이는 네임스페이스를 찾지 못하는 걸 나타냅니다. 이런 경우에는 이곳에 마우스 캐럿을 두고 [Ctrl]+[.] 단축키를 사용해서 그림과 같이 자동으로 네임스페이스를 추가해주는 기능을 사용하여 손쉽게 네임스페이스를 코드 상단에 추가할 수 있습니다.

그림: 12. 네임스페이스 자동 생성

네임스페이스 자동 생성

</참고>

(4) 다시 DotNetNote 프로젝트를 웹 브라우저로 실행 후 /DependencyInjectionDemo 경로와 /DependencyInjectionDemo/About 경로를 요청해보겠습니다. 에러가 발생하지 않고 정상적으로 출력되는 것을 확인할 수 있습니다.

그림: 13. 의존성 해결 후 정상 실행

의존성 해결 후 정상 실행

5.6 따라하기 5: 인터페이스를 사용한 생성자 주입

(1) 자, 이번에는 생성자 매개변수로 클래스를 전달하는 방식이 아닌 인터페이스 형식의 매개변수를 전달하는 방식으로 바꿔 보겠습니다. DotNetNote 프로젝트의 Services 폴더에 ICopyrightService.cs 이름으로 인터페이스를 생성하고 다음과 같이 코드를 작성합니다.

***코드: ***

//  Services/ICopyrightService.cs
namespace DotNetNote.Services
{
    public interface ICopyrightService
    {
        string GetCopyrightString();
    }
}

(2) Services 폴더에 있는 CopyrightService.cs 파일을 열고 다음과 같이 코드를 변경합니다. ICopyrightService 인터페이스를 상속하는 CopyrightService 클래스 형태로 변경하는 것입니다.

***코드: ***

//  Services/CopyrightService.cs 코드 변경
using System;
 
namespace DotNetNote.Services
{
    public class CopyrightService : ICopyrightService
    {
        public string GetCopyrightString()
        {
            return $"Copyright {DateTime.Now.Year} all right reserved."
                + $" from CopyrightService";
        }
    }
}

(3) DependencyInjectionDemoController.cs 컨트롤러 클래스를 열고 다음과 같이 코드를 변경합니다. 컨트롤러의 코드를 다음과 같이 CopyrightService 클래스 매개변수를 받는 방식에서 ICopyrightService 인터페이스 형식의 매개변수를 받는 방식으로 변경합니다.

***코드: ***

//  Controllers/DependencyInjectionDemoController.cs 코드 변경
using DotNetNote.Services;
using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class DependencyInjectionDemoController : Controller
    {
        private ICopyrightService _service;
        public DependencyInjectionDemoController(ICopyrightService service)
        {
            _service = service;
        }
 
        public IActionResult Index()
        {
            ViewBag.Copyright = _service.GetCopyrightString();
 
            return View();
        }
 
        public IActionResult About()
        {
            ViewBag.Copyright = _service.GetCopyrightString();
 
            return View();
        }
    }
}

(4) DotNetNote 프로젝트를 웹 브라우저로 실행 후 /DependencyInjectionDemo 경로와 /DependencyInjectionDemo/About 경로를 요청해보겠습니다. 정상적으로 실행되지 않고 에러가 발생할 것입니다.

그림: 14. 의존성 해결 전에 발생하는 에러

의존성 해결 전에 발생하는 에러

(5) 에러를 해결하기 위해 프로젝트 루트에 있는 Startup.cs 파일을 다시 실행합니다. Startup.cs 파일의 ConfigureService에 등록된 코드를 다음과 같이 변경합니다.

코드: Startup.cs 파일의 ConfigureService에 서비스 등록 코드 변경

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    //[DI(Dependency Injection)] 서비스 등록
    services.AddTransient<ICopyrightService, CopyrightService>();
}

(6) 다시 DotNetNote 프로젝트를 웹 브라우저로 실행 후 /DependencyInjectionDemo 경로와 /DependencyInjectionDemo/About 경로를 요청해보겠습니다. 에러가 발생하지 않고 정상적으로 출력되는 것을 확인할 수 있습니다.

그림: 15. 의존성 해결 후 정상 출력

의존성 해결 후 정상 출력

(7) 이번에는 Startup.cs 파일의 코드를 AddTransient에서 AddSingleton으로 변경하고 (6)번 순서를 실행해봅니다. 실행에 전혀 문제 없이 똑같이 실행됩니다.

코드: Startup.cs 파일의 ConfigureService에 서비스 등록 코드 변경

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    //[DI(Dependency Injection)] 서비스 등록
    services.AddSingleton<ICopyrightService, CopyrightService>();
}

(8) 마찬가지로 이번에는 Startup.cs 파일의 코드를 AddSingleton에서 AddScoped로 변경하고 (6)번 순서를 실행해봅니다. 역시 실행에 전혀 문제 없이 똑같이 실행됩니다.

코드: Startup.cs 파일의 ConfigureService에 서비스 등록 코드 변경

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    //[DI(Dependency Injection)] 서비스 등록
    services.AddScoped<ICopyrightService, CopyrightService>();
}

5.7 따라하기 6: AddTransient(), AddSingleton(), AddScoped() 비교하기

(1) Startup.cs 파일에서 서비스 등록 시 사용되는 세 가지 메서드의 특징을 알아 보겠습니다. 단, 굳이 세 가지 메서드를 구분해서 사용할 필요는 없습니다. Services 폴더에 생성했던 CopyrightService.cs 파일을 열고 서비스 클래스 코드에 각 인스턴스의 고유한 값을 출력해 주는 GetHashCode() 메서드를 추가합니다.

코드: Services/CopyrightService.cs 코드 변경

using System;
 
namespace DotNetNote.Services
{
    public class CopyrightService : ICopyrightService
    {
        public string GetCopyrightString()
        {
            return $"Copyright {DateTime.Now.Year} all right reserved."
                + $" from CopyrightService. {GetHashCode()}";
        }
    }
}

(2) Controllers 폴더의 DependencyInjectionDemoController.cs 파일을 실행합니다. 컨트롤러 클래스의 코드에 서비스 두 개를 주입하는 형태로 모양을 변경합니다. 변경된 전체 소스는 다음과 같습니다.

코드: Controllers/DependencyInjectionDemoController.cs

using DotNetNote.Services;
using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class DependencyInjectionDemoController : Controller
    {
        private ICopyrightService _service;
        private ICopyrightService _service2;
 
        public DependencyInjectionDemoController(
            ICopyrightService service, ICopyrightService service2)
        {
            _service = service;
            _service2 = service2;
        }
 
        public IActionResult Index()
        {
            ViewBag.Copyright = 
                _service.GetCopyrightString() + ", " + 
                _service2.GetCopyrightString();
            return View();
        }
 
        public IActionResult About()
        {
            ViewBag.Copyright = _service.GetCopyrightString();
            return View();
        }
     }
}

(3) Startup.cs 파일에서 AddTransient 메서드로 변경한 후 /DependencyInjectionDemo 경로를 호출해봅니다. AddTransient, AddSingleton, AddScoped로 변경해보면서 다음의 (4), (5), (6)번 실행 결과를 확인해보겠습니다.

***코드: ***

// 
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ICopyrightService, CopyrightService>();
}

(4) AddTransient 메서드 호출 시 각각의 인터페이스 변수에 따라서 서로 다른 인스턴스가 생성됩니다. 즉, 매번 호출할 때마다 다른 인스턴스가 생성되는 형태입니다.

그림: 16. AddTransient 메서드로 호출시 서로 다른 인스턴스 생성 확인

AddTransient 메서드로 호출시 서로 다른 인스턴스 생성 확인

(5) AddSingleton 메서드 변경 후 호출 시 프로젝트 내에서 같은 인스턴스가 생성되어 같은 개체로 인식합니다.

그림: 17. AddSingleton 메서드 호출시 같은 인스턴스 생성 확인

AddSingleton 메서드 호출시 같은 인스턴스 생성 확인

(6) AddScoped 메서드로 변경 후 실행해보면 같은 요청(Request) 상태이기에 같은 인스턴스가 생성됩니다. 이 예제에서는 AddSingleton과 AddScoped 메서드는 구분되지 않습니다. 다만, 새로 고침할 때마다 인스턴스가 새로 생성되는 차이점은 존재합니다.

그림: 18. AddScoped 메서드 호출시 같은 인스턴스 생성 확인

AddScoped 메서드 호출시 같은 인스턴스 생성 확인

5.8 마무리

특정 클래스의 인스턴스 생성을 직접 컨트롤러에서 인스턴스 생성, 생성자에서 인스턴스 생성 및 생성자 매개변수에 값 지정 후 DI 컨테이너에 의해서 외부에서 인스턴스를 생성하는 식으로 단계별로 코드를 확장해 보았습니다. ASP.NET MVC 5까지는 이러한 의존성 주입과 관련된 기능은 외부 기능의 도움을 받아서 구현했습니다. ASP.NET Core의 MVC부터는 자체 내장된 DI 컨테이너를 제공합니다. 이 책에서는 앞으로 모든 내용을 자체 내장 방식을 사용하여 진행할 것입니다.

6 @inject 키워드로 뷰에 직접 의존성 주입 적용하기

ASP.NET Core에서는 뷰에 클래스나 서비스에 정의된 속성 또는 메서드를 직접 호출할 수 있습니다. 전체 절차를 간략히 요약해보면 다음과 같이 네 순서로 진행됩니다.

(1) 클래스 구현

public string MyProperty { get; set; } = "안녕하세요."; 

(2) Startup.cs 파일에 클래스 등록

services.AddTransient<ClassType>();

(3) 뷰 페이지 상단에 등록 후 별칭 부여

@inject ClassType NickName

(4) 뷰 페이지에서 Razor 표현식으로 사용

@NickName.MyProperty

@inject로 뷰에서 의존성 주입(Dependency Injection)을 사용하면 유용하지만 사용자 주의해야 합니다.

특정 클래스의 메서드의 결괏값이나 속성의 값을 뷰 페이지에서 바로 사용하고자할 때에는 @inject 키워드로 해당 클래스를 뷰 페이지 상단에 지정해주어야 합니다. 이 때, 이 클래스는 services.AddSingleton()과 같은 메서드로 Startup.cs 파일에서 의존성 주입을 해결해주어야 합니다.

7 [실습] @inject 키워드로 뷰에 직접 의존성 주입 적용하기

7.1 소개

일반적으로 DI는 컨트롤러의 생성자에 주입해서 각각의 액션 메서드에서 사용했는데 ASP.NET Core은 컨트롤러에 주입하는 방식은 물론 뷰 페이지에 주입하는 방식도 제공합니다. 이에 대한 간단한 예제를 따라하기를 통해서 살펴보겠습니다.

7.2 따라하기

(1) 앞의 실습에서 구현한 CopyrightService 클래스에 속성을 하나 더 추가합니다. 속성 또는 메서드 모두 똑같이 뷰 페이지에 직접 주입해서 사용할 수 있습니다.

코드: Services/CopyrightService.cs 코드 추가

using System;
 
namespace DotNetNote.Services
{
    public class CopyrightService : ICopyrightService
    {
        public string GetCopyrightString()
        {
            return $"Copyright {DateTime.Now.Year} all right reserved." 
                + $" from CopyrightService. {GetHashCode()}";
        }
        
        // @inject 키워드로 뷰에 직접 주입해서 사용하기 
        public string CopyrightString { get; set; } = 
            $"Copyright {DateTime.Now.Year} all right reserved.";
    }
}

(2) Startup.cs 파일에 다음과 같이 서비스 등록 코드를 한 줄 더 추가합니다. 나머지 코드는 생략된 상태입니다.

***코드: Startup.cs ***

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    //[DI(Dependency Injection)] 서비스 등록
    //services.AddTransient<ICopyrightService, CopyrightService>();
    //services.AddSingleton<ICopyrightService, CopyrightService>();
    services.AddScoped<ICopyrightService, CopyrightService>();
    //[DI] @inject 키워드로 뷰에 직접 클래스의 속성 또는 메서드 값 출력
    services.AddSingleton<CopyrightService>();
}

(3) DependencyInjectionDemoController.cs 컨트롤러에 AtInjectDemo() 액션 메서드를 추가합니다.

코드: Controllers/DependencyInjectionDemoController.cs에 액션 메서드 추가

using DotNetNote.Services;
using Microsoft.AspNetCore.Mvc;
 
namespace DotNetNote.Controllers
{
    public class DependencyInjectionDemoController : Controller
    {
        private ICopyrightService _service;
        private ICopyrightService _service2;
 
        public DependencyInjectionDemoController(
            ICopyrightService service, ICopyrightService service2)
        {
            _service = service;
            _service2 = service2;
        }
 
        public IActionResult Index()
        {
            ViewBag.Copyright = 
                _service.GetCopyrightString() + ", " + 
                _service2.GetCopyrightString();
            return View();
        }
 
        public IActionResult About()
        {
            ViewBag.Copyright = _service.GetCopyrightString();
            return View();
        }
 
        public IActionResult AtInjectDemo()
        {
            return View();
        }
    }
}

(4) Views 폴더의 DependencyInjectionDemo 폴더에 AtInjectDemo.cshtml 뷰 페이지를 생성하고 다음과 같이 코드를 작성합니다. @using으로 네임스페이스를 추가하고, @inject 키워드로 CopyrightService 클래스를 현재 페이지에서 같은 이름의 별칭으로 주입해서 사용하겠다고 선언합니다. 내용 출력이 필요한 부분에서 @별칭.속성 또는 @별칭.메서드() 형태로 작성합니다.

코드: /Views/DependencyInjectionDemo/AtInjectDemo.cshtml

@{ 
    Layout = null;
}
 
@using DotNetNote.Services
 
@inject CopyrightService CopyrightService
 
<p>
    &copy; @CopyrightService.CopyrightString
</p>
<p>
    &copy; @CopyrightService.GetCopyrightString()
</p>

(5) DotNetNote 프로젝트를 실행 후 /DependencyInjectionDemo/AtInjectDemo 경로를 요청하여 실행합니다. 다음과 같이 문자열이 적용된 상태로 출력됨을 확인할 수 있습니다. 서비스 클래스의 속성과 메서드 모두 호출해서 사용할 수 있습니다.

그림: 19. 카피라이트 적용 결과 확인

카피라이트 적용 결과 확인

7.3 마무리

ASP.NET Core에서는 이처럼 뷰 페이지에 @inject 키워드로 직접 특정 클래스의 내용을 주입한후 별칭을 이용해서 사용할 수 있는 기능을 제공합니다. 이를 사용하면 _Layout.cshtml 페이지처럼 컨트롤러 없이 뷰 페이지로만 이루어진 레이아웃 페이지에서도 @inject 키워드로 뷰 페이지에 서비스 등록 후 원하는 위치에 데이터를 출력할 수 있습니다.

8 appsettings.json 파일의 내용을 레이아웃 페이지에 주입해서 사용하기

ASP.NET Core 2.0 이상부터는 appsettigns.json 파일의 정보를 뷰 페이지에 직접 주입해서 인덱서를 호출해서 값을 사용할 수 있습니다.

코드: appsettings.json

{
  "Creator": "박용준(https://www.youtube.com/user/visualacademy)"
}

코드: _Layout.cshtml

@*appsettings.json 파일의 내용을 레이아웃 페이지에 주입해서 사용하기*@
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

<!DOCTYPE html>
…코드 생략…
<footer>
    <p>&copy; @DateTime.Now.Year - DotNetNote</p>
    <p><em>Creator: @Configuration["Creator"] </em></p>
</footer>

9 appsettings.json 파일의 여러 정보를 구분해서 읽어오기

참고 URL:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration?tabs=basicconfiguration

분리된 형태의 데이터베이스 연결 문자열을 appsettings.json 파일에 아래와 같이 설정했습니다.

코드: appsettings.json

"Con": {
  "Server": "Data Source",
  "Database": "Intital Catalog",
  "User ID": "UID",
  "Password": "PWD"
},

ConfigController 컨트롤러 클래스를 만들고 실행 후 /Config 경로를 요청합니다.

코드: /Controllers/ConfigController.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;

namespace DotNetNoteCom.Controllers
{
    public class ConfigController : Controller
    {
        private IConfiguration _config;

        public ConfigController(IConfiguration config)
        {
            _config = config;
        }

        public string Index()
        {
            // 1.X 버전은 GetSecion() 메서드 사용
            string srv = _config.GetSection("Con").GetSection("Server").Value; 
            string rdb = "데이터베이스"; // 동적으로 변경 가능
            string uid= _config["Con:User ID"]; // 2.X 버전
            string pwd= _config["Con:Password"];

            // 원하는 모양으로 가공해서 사용 가능
            return $"{srv};{rdb};{uid};{pwd}";
        }
    }
}

웹 브라우저로 가종된 문자열이 출력됩니다.

Data Source;데이터베이스;UID;PWD
VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com