최근 .NET Core는 급속도로 발전하고 있으며, 본 문서에서는 .NET Core 2.2, Entity Framework Core, 그리고 내장된 의존성 주입(DI) 기능을 기반으로 한 계층형 애플리케이션 아키텍처를 실제로 구현하는 과정을 설명합니다. 이 접근 방식은 유지보수성과 테스트 용이성을 높이는 데 중점을 두며, 특히 저장소(Repository) 패턴과 서비스 계층을 통합하여 데이터 접근 로직을 추상화합니다.
솔루션 구조 설계
Visual Studio 2019를 사용하여 솔루션을 생성합니다. .NET Core 2.2를 사용하기 위해 최신 SDK가 설치되어 있어야 하며, 다음의 프로젝트 구조를 구성합니다:
- Data.Model: 엔티티 클래스 포함 (EF Core 코드 우선 또는 데이터베이스 우선)
- Data.Core: DbContext 및 저장소 구현
- Data.Interface: 인터페이스 정의 (DI 및 유닛 테스트 용이성 확보)
- Business.Service: 비즈니스 로직 처리
- Business.Interface: 서비스 인터페이스 정의
- WebApi: HTTP 요청 처리 및 클라이언트 인터페이스 제공
엔티티 모델 생성
데이터베이스 우선(Database First) 접근 방식을 사용합니다. NuGet을 통해 다음 패키지를 설치합니다:
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 2.2.4
Install-Package Microsoft.EntityFrameworkCore.Tools -Version 2.2.4
Scaffold-DbContext 명령어를 사용해 모델 자동 생성:
Scaffold-DbContext "Server=.;Database=ConCard;User Id=sa;Password=123123;"
Microsoft.EntityFrameworkCore.SqlServer
-OutputDir Models
-Context ConCardDbContext
-Force
생성된 ConCardDbContext는 Data.Model 프로젝트에 위치시키고, 이후 Data.Core로 이동하여 데이터 접근 계층에서 활용합니다.
저장소 패턴 구현
공통 인터페이스 IEntityRepository<T>를 정의하여 CRUD 작업을 표준화합니다:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
namespace Data.Interface
{
public interface IEntityRepository<T> : IDisposable where T : class
{
IQueryable<T> Query { get; }
IQueryable<T> TrackedQuery { get; }
T FindById<K>(K id);
IEnumerable<T> Find(Expression<Func<T, bool>> predicate);
void Insert(T entity);
void InsertRange(IEnumerable<T> entities);
void Update(T entity);
void Delete(T entity);
void DeleteById<K>(K id);
int Commit();
}
}
제네릭 저장소 클래스 EntityRepository<T>는 위 인터페이스를 구현하며, DI를 통해 전달받은 컨텍스트 인스턴스를 사용합니다:
using Data.Interface;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
namespace Data.Core
{
public class EntityRepository<T> : IEntityRepository<T> where T : class
{
private readonly DbContext _context;
private readonly DbSet<T> _set;
public EntityRepository(IApplicationDbContext context)
{
_context = context as DbContext;
_set = _context.Set<T>();
}
public IQueryable<T> Query => _set.AsNoTracking();
public IQueryable<T> TrackedQuery => _set;
public T FindById<K>(K id) => _set.Find(id);
public IEnumerable<T> Find(Expression<Func<T, bool>> predicate)
=> _set.Where(predicate).AsEnumerable();
public void Insert(T entity) => _set.Add(entity);
public void InsertRange(IEnumerable<T> entities) => _set.AddRange(entities);
public void Update(T entity) => _context.Entry(entity).State = EntityState.Modified;
public void Delete(T entity) => _set.Remove(entity);
public void DeleteById<K>(K id) => Delete(FindById<K>(id));
public int Commit() => _context.SaveChanges();
public void Dispose() => _context?.Dispose();
}
}
서비스 계층 및 DI 설정
비즈니스 로직을 캡슐화하기 위해 기본 서비스 클래스를 작성합니다. 각 도메인 서비스는 이를 상속받아 특정 엔티티에 대한 작업을 수행할 수 있습니다:
using Business.Interface;
using Data.Interface;
namespace Business.Service
{
public class BaseService : IBaseService
{
protected readonly IEntityRepositoryFactory _factory;
public BaseService(IEntityRepositoryFactory factory)
{
_factory = factory;
}
}
}
특정 서비스 예시 (User):
using Business.Interface;
using Data.Model.Models;
using System.Collections.Generic;
namespace Business.Service
{
public class UserService : BaseService, IUserService
{
public UserService(IEntityRepositoryFactory factory) : base(factory) { }
public List<User> GetAllUsers()
{
var repository = _factory.Create<User>();
return repository.Query.ToList();
}
}
}
DI 컨테이너 등록
Startup.cs에서 서비스 등록:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// DB Context 등록
services.AddDbContext<ConCardDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
// 인터페이스-구현체 매핑
services.AddScoped<IApplicationDbContext, ConCardDbContext>();
services.AddScoped<IEntityRepositoryFactory, EntityRepositoryFactory>();
services.AddScoped<IUserService, UserService>();
}
API 컨트롤러에서 사용
컨트롤러에서 서비스를 DI를 통해 주입받아 사용:
using Business.Interface;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private readonly IUserService _userService;
public ValuesController(IUserService userService)
{
_userService = userService;
}
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
var users = _userService.GetAllUsers();
return Ok(users.Select(u => u.Name));
}
}
이러한 구조는 관심사 분리(Separation of Concerns) 원칙을 준수하며, 단위 테스트와 확장성이 뛰어난 견고한 백엔드 아키텍처를 구축하는 데 효과적입니다.