ASP.NET Core 종속성 주입(의존성 주입)
ASP.NET Core에서 종속성 주입을 사용하면, 응용 프로그램의 클래스 간 결합도를 낮추고 테스트 용이성을 높이면서 필요한 서비스와 구성 요소를 동적으로 주입하여 유연하고 확장 가능한 코드 구조를 구현할 수 있습니다.
ASP.NET Core에서는 ASP.NET 4.8과 달리 의존성 주입을 위한 기본 내장 DI 컨테이너를 지원합니다. 의존성 해결을 위한 3가지 DI 컨테이너를 살펴보고 뷰 페이지에 바로 주입해서 사용할 수 있는 @inject
키워드에 대해서도 살펴보겠습니다.
NOTE
의존성 주입은 클래스 간의 결합도를 낮추고 유연한 코드 관리를 가능하게 하는 강력한 기능
(이 강좌는 회원 전용 강좌입니다. 최종 완성된 강의는 데브렉에서 서비스됩니다.)
1. 종속성 주입 소개
.NET에서의 의존성 주입(Dependency Injection, DI) 디자인 패턴은 특정 서비스 클래스와 특정 컨트롤러 클래스 간에 의존 관계를 Program.cs(또는 Startup.cs) 파일에서 설정하는 방식을 사용합니다. ASP.NET Core에서는 커뮤니티 기반 DI 컨테이너가 아닌 자체 내장된 새로운 형태의 의존성 주입(Dependency Injection) 시스템이 기본으로 내장되어 있습니다.
즉, ASP.NET Core 내장된 컨테이너를 사용하면 따로 DI 컨테이너를 NuGet 패키지로 받아서 사용할 필요없이 바로 사용할 수 있습니다.
DI 컨테이너
.NET에 내장되어 있는 종속성 주입 관련 엔진을 DI 컨테이너라고 부릅니다. DI 컨테이너는, 미리 등록된 여러 종속성들에 요청할 때 해당 종속성에 대한 인스턴스를 생성해 줍니다.
의존성 주입의 장점
의존성 주입을 사용하면 개체에 대한 관리의 복잡성을 떨쳐낼 수 있습니다. 모든 관리는 ASP.NET Core의 기본 제공 DI 컨테이너에 맡기면 됩니다. 또한, 각각의 형식 간에 강력하게 결합된 의존성을 제거할 수 있습니다. 그리고 테스트 기반 개발을 즐겨 하는 사람이라면 테스트 코드 작성 시 필요한 메서드만 테스트하고, 등록된 인터페이스에게 가짜 데이터를 넘겨주어 테스트하는 식으로 적용할 수 있습니다.
종속성 주입 장점
현재 시점에서는 종속성 주입의 장점을 이해하기 어려울 수 있습니다. 제 강의 기준으로 상위 과정인 Blazor Server까지 학습이 이루어지면, 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
키워드로 등록해야 사용할 수 있습니다.
의존성 주입 컨테이너와 생성자 호출
의존성 주입 컨테이너(Dependency Injection Container)
builder.Services.AddScoped<I인터페이스, 클래스>();
생성자 호출시 I인터페이스
에 대한 개체 자동 생성
public CtorTest(I인터페이스 개체) { }
인터페이스에 대한 계약에 의존이 있다고 판단되면, 컨테이너에 지정된 개체로 구현해줍니다.
C# 12.0 기본 생성자 사용
Primary Constructor를 사용하면 클래스 정의와 함께 생성자 파라미터를 선언할 수 있습니다. 이를 통해 의존성 주입을 더욱 명확하게 표현할 수 있습니다.
public class CtorTest(I인터페이스 개체)
{
// Primary Constructor를 통해 I인터페이스 의존성 주입
// 클래스 내에서 개체를 사용할 수 있습니다.
}
- Primary Constructor는 클래스 선언부에 직접 인터페이스 타입의 파라미터를 포함하여 선언합니다.
- 인터페이스에 대한 계약에 의존이 있다고 판단되면, 컨테이너에 지정된 개체로 구현해줍니다.
- 이 방식은 코드를 더욱 간결하고 읽기 쉽게 만들어, 의존성 주입의 의도를 명확하게 표현할 수 있습니다.
C# 12.0의 Primary Constructor 기능을 활용하면, 의존성 주입과 관련된 코드를 더욱 간결하고 직관적으로 작성할 수 있어, 유지보수와 이해가 더 쉬워집니다.
3. [실습] 종속성 주입 사용 컬렉션 형태의 데이터 출력하기
종속성 주입(Dependency Injection, DI) 패턴은 개체 간의 결합도를 낮추고, 코드의 유연성과 테스트 용이성을 향상시키는 방법입니다. 이 패턴을 사용하여 ASP.NET Core 애플리케이션에서 컬렉션 형태의 데이터를 출력하는 과정을 단계별로 설명하겠습니다.
(이 강좌는 회원 전용 강좌입니다. 최종 완성된 강의는 데브렉에서 서비스됩니다.)
1. 모델 클래스 정의
Category
모델 클래스를 정의하여 카테고리 정보를 나타냅니다.
코드: DotNetNote\DotNetNote.Models\Categories\Category.cs
namespace DotNetNote.Models.Categories;
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
}
2. 리포지토리 인터페이스 생성
ICategoryRepository
인터페이스를 정의하여 카테고리 데이터에 대한 추상화된 접근 방식을 제공합니다.
코드: DotNetNote\DotNetNote.Models\Categories\ICategoryRepository.cs
using System.Collections.Generic;
namespace DotNetNote.Models.Categories;
public interface ICategoryRepository
{
List<Category> GetCategories();
}
3. 리포지토리 구현
메모리 기반 및 SQL Server 기반의 두 가지 리포지토리 구현을 제공합니다.
코드: DotNetNote\DotNetNote.Models\Categories\CategoryRepositoryInMemory.cs
using System.Collections.Generic;
namespace DotNetNote.Models.Categories;
public class CategoryRepositoryInMemory : ICategoryRepository
{
public List<Category> GetCategories()
{
// 컬렉션 이니셜라이저를 사용하여 카테고리 리스트 만들기
var categories = new List<Category>()
{
new Category() { CategoryId = 1, CategoryName = "좋은 책" },
new Category() { CategoryId = 2, CategoryName = "좋은 강의" },
new Category() { CategoryId = 3, CategoryName = "좋은 컴퓨터" }
};
return categories;
}
}
코드: DotNetNote\DotNetNote.Models\Categories\CategoryRepositorySqlServer.cs
using System.Collections.Generic;
namespace DotNetNote.Models.Categories;
public class CategoryRepositorySqlServer : ICategoryRepository
{
public List<Category> GetCategories()
{
var categories = new List<Category>()
{
new Category() { CategoryId = 1, CategoryName = "[DB에서] 좋은 책" },
new Category() { CategoryId = 2, CategoryName = "[DB에서] 좋은 강의" },
new Category() { CategoryId = 3, CategoryName = "[DB에서] 좋은 컴퓨터" }
};
return categories;
}
}
4. 종속성 주입 설정
Program.cs
파일에서 ICategoryRepository
인터페이스와 그 구현체를 종속성 주입 컨테이너에 등록합니다.
코드: DotNetNote\DotNetNote\Program.cs
using DotNetNote.Models.Categories;
var builder = WebApplication.CreateBuilder(args);
// 이 부분에 다른 서비스 등록이 이루어질 수 있음
// 종속성 주입(DI) 설정
builder.Services.AddTransient<ICategoryRepository, CategoryRepositoryInMemory>();
var app = builder.Build();
// 애플리케이션 구성 및 실행 코드
5. 컨트롤러에서 DI 사용
컨트롤러에서 ICategoryRepository
인터페이스를 주입받아 카테고리 데이터를 가져옵니다.
코드: DotNetNote\DotNetNote\Controllers\ListOfCategoryController.cs
using DotNetNote.Models.Categories;
using Microsoft.AspNetCore.Mvc;
namespace DotNetNote.Controllers;
public class ListOfCategoryController : Controller
{
private readonly ICategoryRepository _repository;
public ListOfCategoryController(ICategoryRepository repository)
{
_repository = repository;
}
public IActionResult Index()
{
var categories = _repository.GetCategories();
return View(categories);
}
}
위 코드는 다음과 같이 기본 생성자(Primary Constructor)를 사용하여 좀 더 간결하게 코드를 표현할 수 있습니다.
using DotNetNote.Models.Categories;
using Microsoft.AspNetCore.Mvc;
namespace DotNetNote.Controllers;
public class ListOfCategoryController(ICategoryRepository repository) : Controller
{
//private readonly ICategoryRepository _repository;
//public ListOfCategoryController(ICategoryRepository repository) => _repository = repository;
public IActionResult Index()
{
// var categoryRepository = new CategoryRepositoryInMemory();
// var categories = categoryRepository.GetCategories();
var categories = repository.GetCategories();
return View(categories);
}
}
6. 뷰에서 데이터 표시
Index.cshtml
뷰 파일에서 모델로 전달된 카테고리 데이터를 사용하여 화면에 출력합니다.
코드: DotNetNote\DotNetNote\Views\ListOfCategory\Index.cshtml
@using DotNetNote.Models.Categories
@model List<Category>
@{
Layout = null;
}
<h1>카테고리 리스트</h1>
<ul>
@foreach (var category in Model)
{
<li>@category.CategoryId - @category.CategoryName</li>
}
</ul>
이러한 단계를 통해 종속성 주입을 사용하여 컬렉션 형태의 데이터를 출력하는 방법을 구현할 수 있습니다. 이 방식은 코드의 유지보수성과 테스트 용이성을 크게 향상시키며, 다양한 데이터 소스와의 연동을 쉽게 할 수 있도록 해줍니다.
4. [실습] DI 사용을 위한 기본 설정 단계 살펴보기
(이 강좌는 회원 전용 강좌입니다. 최종 완성된 강의는 데브렉에서 서비스됩니다.)
소개
ASP.NET Core에서 DI를 사용하기 위한 기본 설정 절차를 진행해보겠습니다. 미리 살펴보기 형태로 전체 진행 사항을 본 후 다음에 이어질 실습에서 한 번 더 설명합니다.
따라하기
(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";
}
}
}
좀 더 축약된 코드는 다음과 같습니다.
namespace DotNetNote.Services;
public class InfoService : IInfoService
{
public string GetUrl() => "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를 사용하여 뷰로 데이터 전달
ViewData["Url"] = "www.gilbut.co.kr";
return View();
}
}
}
ViewData
: 컨트롤러와 뷰 사이에 데이터를 전달할 때 사용하는 사전(dictionary) 형태의 컬렉션입니다. 여기서는 "Url"이라는 키를 가지고 URL 정보를 저장하고 있습니다. ViewData
는 유연하지만, 형식 안전(type-safe)이 아니므로 오류가 발생하기 쉽습니다.
(5) Views 폴더에 SingletonDemo 폴더를 생성합니다. 이 폴더에 Index.cshtml 뷰 페이지를 생성하고 다음과 같이 코드를 작성합니다.
코드: Views/SingletonDemo/Index.cshtml 페이지
@{
Layout = null;
}
사이트 URL: @ViewData["Url"]
뷰에서 @ViewData["Url"]
을 사용하여 컨트롤러에서 설정한 URL 정보를 표시합니다. ViewData
를 통해 컨트롤러에서 뷰로 데이터를 전달하는 방법을 보여줍니다.
또한, 비슷한 용도로 사용되는 ViewBag
에 대해서도 설명드립니다.
ViewBag
: ViewData
와 유사한 방법으로 데이터를 전달하지만, 동적 속성(dynamic properties)을 제공합니다. 이는 ViewBag
을 사용할 때 특정 형식을 지정할 필요 없이 속성을 추가할 수 있음을 의미합니다. 예를 들어, ViewBag.Url = "www.gilbut.co.kr";
와 같이 사용할 수 있습니다. 하지만 이 역시 형식 안전이 아니므로 오류가 발생할 가능성이 있습니다.
이러한 방법들은 MVC 패턴에서 컨트롤러와 뷰 간의 데이터 전달을 위해 사용되며, 각각의 방법은 상황에 따라 선택하여 사용할 수 있습니다.
(6) 웹 브라우저를 실행하고, /SingletonDemo
로 경로를 요청하면 다음과 같이 URL이 출력됩니다.
그림: 1. 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");
}
}
}
C# 12.0 버전의 기본 생성자를 사용한 코드 모양은 다음과 같습니다.
코드: DotNetNote\Controllers\SingletonDemoController.cs
namespace DotNetNote.Controllers;
public class SingletonDemoController(IInfoService svc) : Controller
{
public IActionResult ConstructorInjectionDemo()
{
ViewData["Url"] = svc.GetUrl();
return View("Index");
}
public IActionResult Index()
{
// ViewData를 사용하여 뷰로 데이터 전달
ViewData["Url"] = "www.gilbut.co.kr";
return View();
}
public IActionResult InfoServiceDemo()
{
InfoService svc = new();
ViewData["Url"] = svc.GetUrl();
return View("Index");
}
}
(8) 다시 웹 브라우저를 실행하여 '/SingletonDemo/InfoServiceDemo'로 경로를 요청하면 다음과 같이 URL이 출력됩니다.
그림: 2. 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) 자, 발생한 에러를 해결해보겠습니다. 프로젝트 루트의 Program.cs 또는 Startup.cs 파일을 오픈합니다. Startup.cs 파일의 상단의 네임스페이스 선언 영역에 using
구문으로 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>();
뒤에서 인터페이스를 추가한 이후에는 다음과 같이 코드를 추가하면 됩니다.
// AddSingleton 메서드로 의존성 주입 사용하기_DI 사용을 위한 기본 설정 단계 살펴보기
// AddSingletonDemoController.cs 클래스에서 IInfoService 인터페이스 사용
services.AddSingleton<InfoService>();
services.AddSingleton<IInfoService, InfoService>();
(13) 다시 '/SingletonDemo/ConstructorInjectionDemo' 경로를 요청하면 정상적으로 실행됩니다.
그림: 5. 의존성 해결 후 정상 실행
(14) 생성자에 클래스를 대입하는 생성자 주입 방식을 통해서 1차로 해결하였습니다. 이번에는 인터페이스로 클래스를 추출하여 인터페이스를 통한 의존성 주입 기능을 구현해보겠습니다. Services 폴더에 IInfoService
인터페이스를 다음과 같이 생성합니다.
코드: Services/IInfoService.cs
namespace DotNetNote.Services
{
public interface IInfoService
{
string GetUrl();
}
}
namespace DotNetNote.Services;
public interface IInfoService
{
string GetUrl();
}
(15) IInforService
인터페이스를 상속 받는 형태로 InforService
클래스를 다시 작성합니다.
코드: Services/InfoService.cs
namespace DotNetNote.Services
{
public class InfoService : IInfoService
{
public string GetUrl()
{
return "http://www.gilbut.co.kr";
}
}
}
namespace DotNetNote.Services;
public class InfoService : IInfoService
{
public string GetUrl() => "http://www.gilbut.co.kr";
}
(16) 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");
}
}
}
C# 12.0 버전의 기본 생성자(Primary Constructor)를 사용한 간결한 코드는 다음과 같습니다.
namespace DotNetNote.Controllers;
public class SingletonDemoController(IInfoService svc) : Controller
{
public IActionResult ConstructorInjectionDemo()
{
ViewData["Url"] = svc.GetUrl();
return View("Index");
}
public IActionResult Index()
{
// ViewData를 사용하여 뷰로 데이터 전달
ViewData["Url"] = "www.gilbut.co.kr";
return View();
}
public IActionResult InfoServiceDemo()
{
InfoService svc = new();
ViewData["Url"] = svc.GetUrl();
return View("Index");
}
}
(17) 다시 Startup.cs 파일의 ConfigureServices
메서드 하단에 IInfoService
인터페이스에 대한 의존성을 해결하는 코드를 다음과 같이 코드를 등록합니다. 참고로 AddSingleton()
, AddScoped()
, AddTransient()
모두 사용 가능합니다.
코드: Startup.cs
services.AddSingleton<InfoService>();
services.AddSingleton<IInfoService, InfoService>();
(18) 최종적으로 '/SingletonDemo/ConstructorInjectionDemo' 경로를 요청하면 다음과 같이 IInforService
인터페이스를 상속 받아 구현한 InforService
클래스의 메서드가 SingletonDemo
컨트롤러에 인터페이스 생성자 주입 형태로 주입되어 실행됩니다.
그림: 6. 웹 브라우저 실행 결과
마무리
이번 실습에서 작성한 InfoServiceDemo()
액션 메서드에서 사용한 것처럼 직접 InfoService
클래스의 인스턴스를 생성 후 GetUrl
메서드를 호출해도 전혀 문제가 되지 않습니다. 다만, 이런 방식은 컨트롤러와 외부 서비스 클래스 간에 밀접하게 연관되어 있어서 의존성이 있습니다. 이러한 의존성은 외부 설정 방식(Startup.cs)으로 제거할 수 있습니다. ASP.NET Core에서는 AddSingleton()
과 같은 메서드를 기본으로 제공합니다.
5. [실습] 인터페이스를 사용한 생성자 주입으로 DI 구현
(이 강좌는 회원 전용 강좌입니다. 최종 완성된 강의는 데브렉에서 서비스됩니다.)
소개
ASP.NET Core에서 의존성 주입을 적용하는 내용을 단계별로 다시 한 번 진행해 보겠습니다.
따라하기 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 개체를 사용한 뷰 페이지 실행 결과
따라하기 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
를 Inde
x와 About
에서 같이 호출해서 사용하는 것을 보여줍니다.
그림: 9. About 액션 메서드 실행
따라하기 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 뷰 페이지 실행
따라하기 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: 인터페이스를 사용한 생성자 주입
(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>();
}
따라하기 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 메서드로 호출시 서로 다른 인스턴스 생성 확인
(5) AddSingleton
메서드 변경 후 호출 시 프로젝트 내에서 같은 인스턴스가 생성되어 같은 개체로 인식합니다.
그림: 17. AddSingleton 메서드 호출시 같은 인스턴스 생성 확인
(6) AddScoped
메서드로 변경 후 실행해보면 같은 요청(Request) 상태이기에 같은 인스턴스가 생성됩니다. 이 예제에서는 AddSingleton
과 AddScoped
메서드는 구분되지 않습니다. 다만, 새로 고침할 때마다 인스턴스가 새로 생성되는 차이점은 존재합니다.
그림: 18. AddScoped 메서드 호출시 같은 인스턴스 생성 확인
마무리
특정 클래스의 인스턴스 생성을 직접 컨트롤러에서 인스턴스 생성, 생성자에서 인스턴스 생성 및 생성자 매개변수에 값 지정 후 DI 컨테이너에 의해서 외부에서 인스턴스를 생성하는 식으로 단계별로 코드를 확장해 보았습니다. ASP.NET MVC 5까지는 이러한 의존성 주입과 관련된 기능은 외부 기능의 도움을 받아서 구현했습니다. ASP.NET Core의 MVC부터는 자체 내장된 DI 컨테이너를 제공합니다. 이 책에서는 앞으로 모든 내용을 자체 내장 방식을 사용하여 진행할 것입니다.
6. @inject
키워드로 뷰에 직접 의존성 주입 적용하기
ASP.NET Core에서는 뷰에 클래스나 서비스에 정의된 속성 또는 메서드를 직접 호출할 수 있게 해주는 @inject
키워드를 통해 의존성 주입(Dependency Injection)을 뷰 레벨에서 직접 적용할 수 있습니다. 이를 통해 컨트롤러나 모델을 거치지 않고도 뷰에서 필요한 데이터나 로직에 직접 접근할 수 있으며, 이는 뷰의 유연성과 재사용성을 높여줍니다.
전체 절차를 간략히 요약해보면 다음과 같이 네 순서로 진행됩니다.
클래스 구현
public class ClassType { public string MyProperty { get; set; } = "안녕하세요."; }
Program.cs(Startup.cs) 파일에 클래스 등록
services.AddTransient<ClassType>();
뷰 페이지 상단에 등록 후 별칭 부여
@inject ClassType NickName
뷰 페이지에서 Razor 표현식으로 사용
<p>@NickName.MyProperty</p>
@inject
를 사용하여 뷰에서 의존성 주입을 적용하는 것은 매우 유용하나, 몇 가지 주의 사항을 알고 있어야 합니다:
- 명확한 책임 분리:
@inject
를 사용함으로써 뷰가 비즈니스 로직이나 데이터 액세스 로직에 직접적으로 종속되지 않도록 해야 합니다. 즉, 뷰는 가능한 한 간결하게 유지하고, 비즈니스 로직은 서비스 레이어에 두어야 합니다. - 성능 고려: 서비스가 뷰에 주입될 때, 해당 서비스의 생명주기와 뷰의 요청 빈도를 고려하여 적절한 생명주기 관리 전략(예: Singleton, Scoped, Transient)을 선택해야 합니다.
- 보안: 뷰에서 직접 클래스를 사용할 때는 보안 측면을 고려해야 합니다. 사용자 입력에 대해 검증하고, 적절한 권한이 있는 사용자만이 민감한 데이터에 접근할 수 있도록 해야 합니다.
7. [실습] @inject
키워드로 뷰에 직접 의존성 주입 적용하기
(이 강좌는 회원 전용 강좌입니다. 최종 완성된 강의는 데브렉에서 서비스됩니다.)
소개
일반적으로 DI는 컨트롤러의 생성자에 주입해서 각각의 액션 메서드에서 사용했는데 ASP.NET Core 기술은 컨트롤러에 주입하는 방식은 물론 뷰 페이지에 주입하는 방식도 제공합니다. 이에 대한 간단한 예제를 따라하기를 통해서 살펴보겠습니다.
따라하기
(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) Program.cs(Startup.cs) 파일에 다음과 같이 서비스 등록 코드를 한 줄 더 추가합니다. 나머지 코드는 생략된 상태입니다.
***코드: Program.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
컨트롤러에 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>
© @CopyrightService.CopyrightString
</p>
<p>
© @CopyrightService.GetCopyrightString()
</p>
(5) DotNetNote
프로젝트를 실행 후 /DependencyInjectionDemo/AtInjectDemo
경로를 요청하여 실행합니다. 다음과 같이 문자열이 적용된 상태로 출력됨을 확인할 수 있습니다. 서비스 클래스의 속성과 메서드 모두 호출해서 사용할 수 있습니다.
그림: 19. 카피라이트 적용 결과 확인
마무리
ASP.NET Core에서는 이처럼 뷰 페이지에 @inject
키워드로 직접 특정 클래스의 내용을 주입한후 별칭을 이용해서 사용할 수 있는 기능을 제공합니다. 이를 사용하면 _Layout.cshtml 페이지처럼 컨트롤러 없이 뷰 페이지로만 이루어진 레이아웃 페이지에서도 @inject
키워드로 뷰 페이지에 서비스 등록 후 원하는 위치에 데이터를 출력할 수 있습니다.
8. [실습] 초간단 리포지토리 패턴 구현: 컬렉션 형태의 데이터 가져오기
(이 강좌는 회원 전용 강좌입니다. 최종 완성된 강의는 데브렉에서 서비스됩니다.)
이 강의에서는 .NET 환경에서 컬렉션 형태의 데이터를 인-메모리 또는 DB에서 가져오는 리포지토리 패턴의 구현 방법을 설명합니다.
1. Variable 클래스 정의 (01_Variable.cs
)
본 예제의 시작점은 Variable
클래스의 정의입니다. 이 클래스는 데이터 모델을 나타내며, 각 변수의 ID, 텍스트, 그리고 값을 속성으로 가집니다. Variable
클래스는 기본적으로 데이터베이스의 테이블 열을 모델링하며, 다양한 데이터 타입을 저장하는 데 사용됩니다.
코드: DotNetNote\DotNetNote.Models\Variables\01_Variable.cs
namespace DotNetNote.Models
{
public class Variable
{
public int Id { get; set; }
public string Text { get; set; }
public string Value { get; set; }
}
}
2. IVariableRepository 인터페이스 (02_IVariableRepository.cs
)
다음으로, 데이터 액세스 계층의 추상화를 담당하는 IVariableRepository
인터페이스를 정의합니다. 이 인터페이스는 GetAll
메서드를 포함하며, 이 메서드는 Variable
개체의 리스트를 반환하는 역할을 합니다. 이러한 추상화를 통해 구체적인 데이터 저장소에 대한 의존성을 줄일 수 있습니다.
코드: DotNetNote\DotNetNote.Models\Variables\02_IVariableRepository.cs
using System.Collections.Generic;
namespace DotNetNote.Models
{
public interface IVariableRepository
{
List<Variable> GetAll();
}
}
3. VariableRepositoryInMemory 구현 (03_VariableRepositoryInMemory.cs
)
VariableRepositoryInMemory
는 IVariableRepository
인터페이스의 인-메모리 구현체입니다. 이 클래스는 GetAll
메서드를 통해 정의된 Variable
개체들의 리스트를 반환합니다. 이 구현은 실제 데이터베이스가 아닌 메모리 내에서 데이터를 관리하여 테스트와 초기 프로토타이핑에 적합합니다.
코드: DotNetNote\DotNetNote.Models\Variables\03_VariableRepositoryInMemory.cs
using System.Collections.Generic;
namespace DotNetNote.Models
{
public class VariableRepositoryInMemory : IVariableRepository
{
public List<Variable> GetAll()
{
List<Variable> variables = new List<Variable>()
{
new Variable { Id = 1, Text = "한국어", Value = "KO" },
new Variable { Id = 2, Text = "영어", Value = "EN" }
};
return variables;
}
}
}
4. DI 설정 (Program.cs 또는 Startup.cs)
이 부분에서는 의존성 주입을 설정합니다. IVariableRepository
인터페이스와 그 구현체인 VariableRepositoryInMemory
클래스를 연결함으로써, IVariableRepository
가 필요한 경우 자동으로 VariableRepositoryInMemory
의 인스턴스가 제공되도록 합니다.
코드: Program.cs(Startup.cs)
services.AddTransient<IVariableRepository, VariableRepositoryInMemory>();
5. VariableTestController 구현 (VariableTestController.cs
)
VariableTestController
는 MVC 패턴의 컨트롤러로서, IVariableRepository
를 통해 데이터를 가져옵니다. Index
메서드에서는 repository.GetAll()
을 호출해 모든 Variable
개체를 조회하고, 이를 뷰로 전달하기 위해 ViewBag.Variables
에 저장합니다.
코드: DotNetNote\DotNetNote\Controllers\VariableTestController.cs
namespace DotNetNote.Controllers;
public class VariableTestController(IVariableRepository repository) : Controller
{
public IActionResult Index()
{
var variables = repository.GetAll();
ViewBag.Variables = variables;
return View();
}
}
6. 뷰 (Index.cshtml)
마지막 단계는 사용자에게 Variable
개체들을 보여주는 뷰의 구현입니다. ViewBag.Variables
에서 받은 데이터를 반복문을 이용해 화면에 리스트 형태로 표시합니다. 각 리스트 항목은 Variable
개체의 Text
와 Value
를 보여줍니다.
코드: DotNetNote\DotNetNote\Views\VariableTest\Index.cshtml
@{
var variables = ViewBag.Variables as List<Variable>;
}
<ul>
@foreach (var v in variables)
{
<li>@v.Text - @v.Value</li>
}
</ul>
7. 실행 결과 확인
프로젝트를 실행하면 다음과 같이 출력됩니다.
그림: VariableTest 컨트롤러 실행 결과
다음 링크의 데모 사이트를 통해서 직접 실행 결과를 볼 수도 있습니다.
https://www.dotnetnote.com/VariableTest
마무리
이 강의를 통해 각 단계의 구현과 그 중요성을 자세히 이해할 수 있으며, 리포지토리 패턴을 통해 데이터 액세스 로직의 분리가 애플리케이션의 유지보수성과 확장성을 어떻게 향상시키는지 알 수 있습니다.
9. appsettings.json 파일의 내용을 레이아웃 페이지에 주입해서 사용하기
ASP.NET Core 2.0 이상부터는 appsettings.json
파일의 정보를 뷰 페이지에 직접 주입하여 IConfiguration
개체의 인덱서 형태로 호출해서 값을 사용할 수 있습니다.
이어서 appsettings.json
파일에 TenantName
을 "VisualAcademy"로 설정하고, 이를 _Layout.cshtml
과 같은 뷰 페이지에 주입하여 사용하는 과정은 다음과 같은 단계로 진행할 수 있습니다:
코드: VisualAcademy\appsettings.json
{
"TenantName": "VisualAcademy",
"Creator": "박용준(https://www.youtube.com/user/visualacademy)"
}
코드: DotNetNote\Views\Shared_Layout.cshtml 코드: VisualAcademy\Pages\Shared_Layout.cshtml
@*appsettings.json 파일의 내용을 레이아웃 페이지에 주입해서 사용하기*@
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
<!DOCTYPE html>
<html>
<head>
<title>@Configuration["TenantName"]</title>
</head>
<body>
<!-- 나머지 레이아웃 내용 -->
<footer>
<p>© @DateTime.Now.Year - DotNetNote</p>
<p><em>Creator: @Configuration["Creator"] </em></p>
</footer>
</body>
</html>
이를 통해 _Layout.cshtml
뷰 페이지에 appsettings.json
의 TenantName
과 Creator
정보를 주입하여 사용할 수 있습니다. 이 방식으로 다양한 설정 값을 뷰에 동적으로 표시하고 관리할 수 있습니다.
IsEnrollmentSoftware 설정 값을 활용한 페이지 제목 동적 설정
애플리케이션의 등록 관련 기능이 활성화되어 있는지를 나타내는 IsEnrollmentSoftware
설정을 appsettings.json
파일에 추가하고, 이 설정에 따라 _Layout
페이지의 제목을 동적으로 변경하는 방법을 아래 단계별로 설명합니다:
appsettings.json
파일 수정appsettings.json
파일에 등록 기능 활성화 여부를 나타내는IsEnrollmentSoftware
설정을 추가합니다. 이 설정은 애플리케이션이 등록 관련 소프트웨어인지 아닌지를 결정합니다.코드: appsettings.json
{ "TenantName": "VisualAcademy", "Creator": "박용준(https://www.youtube.com/user/visualacademy)", "IsEnrollmentSoftware": true }
이 설정에서
IsEnrollmentSoftware
키는true
또는false
값을 받으며, 애플리케이션이 등록 소프트웨어 기능을 제공하는지 여부를 나타냅니다._Layout.cshtml
파일 수정_Layout.cshtml
파일을 수정하여IsEnrollmentSoftware
설정에 따라 제목에 "Enrollment Enabled" 또는 "Enrollment Disabled"를 동적으로 추가합니다.코드: Views/Shared/_Layout.cshtml 또는 Pages/Shared/_Layout.cshtml
@*appsettings.json 파일의 설정 값을 페이지에 주입하여 동적으로 사용하기*@ @using Microsoft.Extensions.Configuration @inject IConfiguration Configuration @{ var isEnrollmentSoftware = Configuration.GetValue<bool>("IsEnrollmentSoftware"); var titleSuffix = isEnrollmentSoftware ? " - Enrollment Software" : " - Non-Enrollment Software"; } <!DOCTYPE html> <html> <head> <title>@Configuration["TenantName"]@titleSuffix</title> </head> <body> <!-- 페이지 내용 --> <footer> <p>© @DateTime.Now.Year - DotNetNote</p> <p><em>Creator: @Configuration["Creator"] </em></p> </footer> </body> </html>
여기서
Configuration.GetValue<bool>("IsEnrollmentSoftware")
를 통해IsEnrollmentSoftware
값을 불리언으로 가져오고, 이를 기반으로 제목에 적절한 접미사를 추가합니다.
이 접근 방식을 통해, 애플리케이션의 설정 값에 따라 레이아웃 페이지의 동적인 변경을 실시간으로 반영할 수 있습니다. 이로써, 애플리케이션의 설정을 유연하게 관리하며 사용자 인터페이스에 필요한 정보를 적절히 표시할 수 있습니다.
10. appsettings.json
파일의 여러 정보를 구분해서 읽어오기
appsettings.json
파일은 .NET 애플리케이션의 중요한 구성 정보를 저장하는 데 사용됩니다. 이 파일에서 설정된 정보를 읽는 방법은 다음과 같습니다.
코드: appsettings.json
{
"Con": {
"Server": "Data Source",
"Database": "Initial Catalog",
"User ID": "UID",
"Password": "PWD"
},
...
}
코드: /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()
{
string server = _config.GetSection("Con").GetSection("Server").Value;
string database = "데이터베이스"; // 동적으로 변경 가능
string userId = _config["Con:User ID"];
string password = _config["Con:Password"];
return $"{server};{database};{userId};{password}";
}
}
}
웹 브라우저에서 해당 컨트롤러에 접근하면 다음과 같은 문자열이 출력됩니다, 이를 통해 설정 값이 정확하게 읽혔는지 확인할 수 있습니다.
Data Source;데이터베이스;UID;PWD
이 방식을 통해 appsettings.json
에서 복잡한 구성 정보를 읽고, 이를 애플리케이션 내에서 유연하게 사용할 수 있습니다. 이런 접근법은 설정 관리를 용이하게 하며, 코드의 구조를 개선합니다.
11. appsettings.json
파일의 내용을 secrets.json
파일로 재정의
(이 강좌는 회원 전용 강좌입니다. 최종 완성된 강의는 데브렉에서 서비스됩니다.)
애플리케이션의 보안을 강화하기 위해 중요한 정보를 secrets.json
파일에 저장하는 방법을 알아봅니다. 이 방법은 개발 중에 중요한 설정 값을 소스 코드에 직접 포함시키지 않도록 도와줍니다.
코드: DotNetNote\appsettings.json
{
"Con": {
"Server": "Data Source",
"Database": "Intital Catalog",
"User ID": "UID",
"Password": "PWD"
},
...
"Creator": "박용준(https://www.youtube.com/VisualAcademy)",
"StorageConnectionString1": "Storage String 1",
"BlobStorageConnectionString": {
"Site1": "Site 1 Storage String",
"Site2": "Site 2 Storage String"
}
}
코드: DotNetNote\Controllers\AppSettingsDemo.cs
using Microsoft.Extensions.Configuration;
namespace DotNetNote.Controllers
{
//[Authorize(Roles = "Administrators")]
public class AppSettingsDemo : Controller
{
private readonly IConfiguration _configuration;
public AppSettingsDemo(IConfiguration configuration)
{
this._configuration = configuration;
}
public IActionResult Index()
{
string con1 = _configuration.GetSection("StorageConnectionString1").Value;
string site1 = _configuration["BlogStorageConnectionString:Site1"];
string site2 = _configuration["BlogStorageConnectionString:Site2"];
return Content($"{con1}, {site1}, {site2}");
}
}
}
현재의 코드는 appsettings.json
파일의 내용을 웹으로 직접 출력하는데, 이는 보안상 위험할 수 있습니다. 따라서 기본 연습 후, [Authorize(Roles = "Administrators")]
어노테이션을 추가하여 특정 권한이 없는 사용자는 이 정보에 접근할 수 없도록 할 계획입니다.
최종 소스 코드는 다음과 같습니다.
using Microsoft.Extensions.Configuration;
namespace DotNetNote.Controllers
{
[Authorize(Roles = "Administrators")]
public class AppSettingsDemo : Controller
{
private readonly IConfiguration _configuration;
public AppSettingsDemo(IConfiguration configuration)
{
this._configuration = configuration;
}
public IActionResult Index()
{
string con1 = _configuration.GetSection("StorageConnectionString1").Value;
string site1 = _configuration["BlogStorageConnectionString:Site1"];
string site2 = _configuration["BlogStorageConnectionString:Site2"];
return Content($"{con1}, {site1}, {site2}");
}
}
}
이러한 변경을 통해 보안을 강화하고, 중요한 설정 정보의 관리를 더욱 안전하게 할 수 있습니다.
secrets.json
파일을 사용한 안전한 보안 데이터 관리
보안상 중요한 데이터를 보다 안전하게 관리하기 위해 secrets.json
파일을 사용하는 방법에 대해 알아보겠습니다. 이 방법은 특히 소스 코드가 공개적인 저장소에 업로드되는 경우에 유용합니다. 예를 들어, GitHub에 공통 소스 코드를 업로드하고 로컬 PC에서만 보안 데이터를 관리할 수 있도록 설정할 수 있습니다.
secrets.json
파일 생성 및 설정
secrets.json
파일은 프로젝트의 루트 폴더 밖에 위치하며, 로컬 개발 환경에서만 사용되는 보안 정보를 포함합니다. 이 파일은 일반적으로 버전 관리 시스템에 포함되지 않아, 중요한 설정 정보가 외부로 유출되는 것을 방지할 수 있습니다.
secrets.json
파일 예시
{
"BlobStorageConnectionString": {
"Site1": "Secure - Site 1 Storage String",
"Site2": "Secure - Site 2 Storage String"
}
}
위와 같이 BlobStorageConnectionString
섹션에는 각 사이트의 보안 스토리지 연결 문자열 정보를 포함할 수 있습니다.
코드에서 secrets.json
사용하기
.NET Core 애플리케이션은 기본적으로 appsettings.json
파일과 secrets.json
파일의 설정을 병합하여 사용합니다. 이를 통해 개발 환경에서 secrets.json
의 설정을 우선적으로 적용할 수 있습니다.
AppSettingsDemo
컨트롤러 수정
using Microsoft.Extensions.Configuration;
namespace DotNetNote.Controllers
{
[Authorize(Roles = "Administrators")]
public class AppSettingsDemo : Controller
{
private readonly IConfiguration _configuration;
public AppSettingsDemo(IConfiguration configuration)
{
this._configuration = configuration;
}
public IActionResult Index()
{
string blobSite1 = _configuration["BlobStorageConnectionString:Site1"];
string blobSite2 = _configuration["BlobStorageConnectionString:Site2"];
return Content($"Blob Site 1: {blobSite1}, Blob Site 2: {blobSite2}");
}
}
}
위의 예시에서는 BlobStorageConnectionString
섹션 아래에 정의된 Site1
및 Site2
의 값을 읽어오고 있습니다. 이렇게 하면 로컬 개발 환경에서는 secrets.json
의 값을 사용하고, 프로덕션 환경에서는 appsettings.json
또는 환경 변수 등의 다른 설정 소스를 사용할 수 있습니다.
보안과 관리의 이점
이 방식을 사용하면 중요한 설정 정보를 안전하게 보관할 수 있고, 로컬과 프로덕션 환경에서 다른 설정을 쉽게 관리할 수 있습니다. 또한, 소스 코드 공유 시 보안 정보가 유출될 위험이 크게 줄어듭니다.
secrets.json
은 개발 중에만 사용되며, 프로덕션 환경에서는 환경 변수, Azure Key Vault, AWS Secret Manager 등의 보다 안전한 저장소를 사용하는 것이 일반적입니다.