단일 데이터 파이프라인 패턴

이전 게시물에서 Clean Architecture Pattern에 대해 이야기했고 이 아름다운 패턴을 사용하여 .NET 6 API 솔루션을 부트스트랩하는 데 사용할 수 있는 .NET 6 솔루션 템플릿을 소개했습니다. -includeEF 매개변수를 사용하여 Entity Framework를 선택하는 경우 이 템플릿에 사용되는 패턴 중 하나는 단일 데이터 파이프라인 패턴입니다. 이 패턴에 대한 공식 명칭이 있는지는 잘 모르겠습니다. 찾으러 갔을 때 찾을 수 없었기 때문에 누군가가 저를 정정할 때까지 이렇게 부르겠습니다.

나는 가능한 한 SOLID 프로그래밍 원칙을 고수하는 것을 좋아합니다. 여기에는 솔리드의 S 부분 또는 단일 책임 원칙이 포함됩니다. 단일 데이터 파이프라인 패턴은 보다 전통적인 리포지토리 패턴과 달리 패턴의 각 클래스가 데이터베이스에 대한 하나의 데이터 파이프라인만 담당하도록 노력합니다. 엔터티의 전체 CRUD와 관련이 없습니다. 단일 형식의 데이터를 읽는 데만 관심이 있습니다. 또는 업데이트에 대해서만. 또는 삭제합니다. 그러나 결코 조합이 아닙니다. 이렇게 하면 이러한 클래스를 모듈식으로 유지하고 SOLID의 O 또는 개방/폐쇄 원칙도 충족합니다.

예를 들어 애플리케이션에서 사용자의 아바타를 가져오려는 경우 다음과 같은 인터페이스와 구현이 있을 수 있습니다.

// Interface definition
public interface IGetUserAvatarDataService
{
    Task<StoredFile?> ExecuteAsync(Guid userUniqueKey, CancellationToken cancellationToken = default);
}

// Implementation
public class GetUserAvatarDataService : IGetUserAvatarDataService
{
    private readonly IApplicationDbContext _dbContext;

    public GetUserAvatarDataService(IApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<StoredFile?> ExecuteAsync(Guid userUniqueKey, CancellationToken cancellationToken = default)
    {
        return await _dbContext.Users
            .Include(x => x.AvatarStoredFile)
            .Where(x => x.UniqueKey == userUniqueKey)
            .Select(x => x.AvatarStoredFile)
            .FirstOrDefaultAsync(cancellationToken);
    }
}


규칙에 따라 데이터 서비스는 서비스 전용 공용 메서드인 ExecuteAsync 메서드를 노출합니다. 이 클래스에는 사용자의 아바타를 업데이트하거나 삭제할 수 있는 기능이 없습니다. 검색하는 방법만 알고 있습니다. 사용하는 코드가 사용자 아바타 검색 외에 DB에서 다른 작업도 수행해야 하는 경우 각 추가 작업에 대한 데이터 서비스도 주입합니다.

예를 들어 사용자의 아바타를 삭제하기 위한 엔드포인트는 다음과 같을 수 있습니다.

public class DeleteUserAvatarCommand : EndpointBaseAsync
    .WithRequest<DeleteUserAvatarViewModel>
    .WithActionResult
{
    private readonly IRequestValidator<DeleteUserAvatarViewModel> _requestValidator;
    private readonly IGetUserWithAvatarDataService _getUserWithAvatarDataService;
    private readonly IDeleteUserAvatarDataService _deleteUserAvatarDataService;
    private readonly IFileService _fileService;

    public DeleteUserAvatarCommand(
        IRequestValidator<DeleteUserAvatarViewModel> requestValidator,
        IGetUserWithAvatarDataService getUserWithAvatarDataService,
        IDeleteUserAvatarDataService deleteUserAvatarDataService,
        IFileService fileService)
    {
        _requestValidator = requestValidator;
        _getUserWithAvatarDataService = getUserWithAvatarDataService;
        _deleteUserAvatarDataService = deleteUserAvatarDataService;
        _fileService = fileService;
    }

    [HttpDelete("api/v{version:apiVersion}/user/{uniqueKey:guid}/avatar")]
    [Authorize]
    public override async Task<ActionResult> HandleAsync([FromRoute] DeleteUserAvatarViewModel request, CancellationToken cancellationToken = default)
    {
        var validationErrors = _requestValidator.ValidateRequest(request);
        if (validationErrors.Any())
            return UnprocessableEntity(validationErrors.ToArray());

        var user = await _getUserWithAvatarDataService.ExecuteAsync(request.UniqueKey, cancellationToken);
        if (user is null)
            return NotFound();

        var fileUniqueKey = user.AvatarStoredFile?.UniqueKey;
        await _deleteUserAvatarDataService.ExecuteAsync(user, cancellationToken);
        _fileService.DeleteAvatarByKey(fileUniqueKey);

        return NoContent();
    }
}



이 엔드포인트는 두 가지 데이터 서비스를 사용하여 작업을 수행합니다. 하나는 아바타와 함께 사용자의 기록을 가져오고 다른 하나는 아바타 삭제에 영향을 미칩니다. 데이터베이스에 대한 개별 호출은 리포지토리와 동일한 클래스에서 제어되지 않습니다. 각각은 독립적으로 주입되고 유지됩니다. 또한 끝점을 사용하는 클래스를 단위 테스트할 때 조롱하기 쉽습니다.

즉, 단일 데이터 파이프라인 패턴을 사용하는 데이터 서비스는 필요에 따라 ExecuteAsync 메서드 내에서 DB와 두 번 이상 상호 작용할 수 있습니다. 핵심은 이러한 작업이 동일한 데이터 흐름의 일부라는 것입니다. 예를 들어, 다음은 ExecuteAsync에 대한 DeleteUserAvatarDataService 메서드입니다.

public async ValueTask ExecuteAsync(ApplicationUser user, CancellationToken cancellationToken = default)
{
    if (user?.AvatarStoredFile is not null)
    {
        _dbContext.StoredFiles.Remove(user.AvatarStoredFile);
        user.AvatarStoredFile = null;

        await _dbContext.SaveChangesAsync(cancellationToken);
    }
}


이 코드에는 실제로 두 가지 별개의 데이터베이스 작업이 진행되고 있습니다. _dbContext.SaveChangesAsync 메서드가 호출되면 먼저 DELETE 문을 실행하여 StoredFile 레코드(아바타의 메타데이터 또는 내 애플리케이션에서 사용자가 업로드한 기타 파일)를 제거한 다음 사용자 레코드(AvatarStoredFileId 속성이 외래 키를 통해 연결된 AvatarStoredFile 열을 null로 설정)

참고로, 내가 하는 것처럼 이러한 데이터 서비스에 대한 명명 체계를 사용하는 이유는 시작 시 종속성 주입 컨테이너에 자동으로 추가할 일부 부트스트래핑 코드를 생성할 수 있도록 하기 위함입니다. 이 코드는 이 게시물의 시작 부분에서 참조한 .NET 6 솔루션 템플릿에 있습니다. 내 데이터 서비스 계약 각각은 ISomethingDataService 로, 구현 클래스는 SomethingDataService 로 불립니다. 그런 다음 애플리케이션 시작 시 리플렉션을 사용하여 모두 DI 컨테이너로 가져올 수 있습니다.

private static readonly Regex InterfacePattern = new Regex("I(?:.+)DataService", RegexOptions.Compiled);
// ...
(from c in typeof(Application.DependencyInjection).Assembly.GetTypes()
 where c.IsInterface && InterfacePattern.IsMatch(c.Name)
 from i in typeof(Infrastructure.DependencyInjection).Assembly.GetTypes()
 where c.IsAssignableFrom(i)
 select new
 {
     Contract = c,
     Implementation = i
 }).ToList()
 .ForEach(x => services.AddScoped(x.Contract, x.Implementation));

좋은 웹페이지 즐겨찾기