최근에 사이드 프로젝트를 진행하면서 겪었던 문제사항과 해결(?)방법에 대해 공유드리고자 합니다.
PostgresSql을 사용하여 EF Core를 사용중에 Transaction과 관련된 문제입니다.
클린아키텍쳐 구조를 사용했습니다.
[Authorize]
public record CreateRecipeByBarCommand : IRequest<BaseResponse<long>>, ITransactional
{
public string? RecipeName { get; init; }
public string? Description { get; init; }
public bool IsPublic { get; init; }
public bool IsChangeableByOthers { get; init; }
public ICollection<CreateRecipeAlcohol> Alcohols { get; init; } = new List<CreateRecipeAlcohol>();
public ICollection<CreateRecipeIngredient> Ingredients { get; init; } = new List<CreateRecipeIngredient>();
}
public class CreateRecipeByBarCommandHandler : IRequestHandler<CreateRecipeByBarCommand, BaseResponse<long>>
{
private readonly IApplicationDbContext _context;
private readonly IMediator _sender;
public CreateRecipeByBarCommandHandler(IApplicationDbContext context, IMediator sender)
{
_context = context;
_sender = sender;
}
public async Task<BaseResponse<long>> Handle(CreateRecipeByBarCommand request, CancellationToken cancellationToken)
{
var recipe = new Recipe
{
Name = request.RecipeName,
Description = request.Description,
IsPublic = request.IsPublic,
IsChangeableByOthers = request.IsChangeableByOthers
};
recipe.AddDomainEvent(new RecipeCreatedEvent(recipe));
_context.Recipes.Add(recipe);
await _context.SaveChangesAsync(cancellationToken);
var commandRecipeAlcohols = new CreateRecipeAlcoholsCommand
{
RecipeId = recipe.Id,
Alcohols = request.Alcohols
};
await _sender.Send(commandRecipeAlcohols, cancellationToken);
var commandRecipeIngredients = new CreateRecipeIngredientsCommand
{
RecipeId = recipe.Id,
Ingredients = request.Ingredients
};
await _sender.Send(commandRecipeIngredients, cancellationToken);
return BaseResponse<long>.Success(recipe.Id);
}
}
다음과 같이 Recipe를 생성하는 메소드가 있습니다. 여기서 ITransactional이라는 마커인터페이스를 상속받아서 트랜잭션을 활성화 시켰습니다.
public record CreateRecipeAlcoholsCommand : IRequest
{
public ICollection<CreateRecipeAlcohol> Alcohols { get; init; } = new List<CreateRecipeAlcohol>();
public long RecipeId { get; init; }
}
public record CreateRecipeAlcohol
{
public long AlcoholId { get; init; }
public decimal AlcoholCost { get; init; }
// [9] 액체 단위 타입 코드
public int SystemUnitTypeCode { get; init; }
}
public class CreateRecipeAlcoholsCommandHandler : IRequestHandler<CreateRecipeAlcoholsCommand>
{
private readonly IApplicationDbContext _context;
public CreateRecipeAlcoholsCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task Handle(CreateRecipeAlcoholsCommand request, CancellationToken cancellationToken)
{
{
var entities = request.Alcohols.Select(alcohol =>
{
var entity = new RecipeAlcohol
{
RecipeId = request.RecipeId,
AlcoholId = alcohol.AlcoholId,
AlcoholCost = alcohol.AlcoholCost.ToMilliLiterDecimal(alcohol.SystemUnitTypeCode),
SystemUnitTypeCode = alcohol.SystemUnitTypeCode
};
entity.AddDomainEvent(new RecipeAlcoholCreatedEvent(entity));
//_context.RecipeAlcohols.Add(entity);
return entity;
}).ToList();
_context.RecipeAlcohols.AddRange(entities);
await _context.SaveChangesAsync(cancellationToken);
}
}
}
그리고 위와 같이 AlcoholRecipe 리스트를 생성해주는 메서드도 호출했습니다 (IngredientRecipe의 모습은 동일하여, 생략합니다.)
다만,
앗… 여기서 멘붕이 오더군요.
일단 구글을 좀 찾아보니 AddRange를 저장할 경우에는 단건의 쿼리를 여러번 전송한다. 라고 확인을 했습니다.
흠, 그렇다면 여기서 방법을 바꿔서
Add는 어떨까 하는 실험정신으로 단건으로 여러번 추가해본결과, 어래래… 잘되네여…? 다만 더 찾아보니 Add는 벤치마크에서도 확실히 시간차이가 많이벌어지네여…
그래서 제가 선택한방법은
ef core의 extension nuget을 사용했습니다.
public async Task BulkInsertAuditableEntitiesAsync<T>(IList<T> entities, BulkConfig? bulkConfig = null,
Action<decimal>? progress = null, Type? type = null, CancellationToken cancellationToken = default)
where T : BaseAuditableEntity
{
entities.UpdateEntities(_userService.Id);
await this.BulkInsertAsync(entities, bulkConfig, progress, type, cancellationToken);
}
public async Task BulkInsertEntitiesAsync<T>(IList<T> entities, BulkConfig? bulkConfig = null,
Action<decimal>? progress = null, Type? type = null, CancellationToken cancellationToken = default)
where T : class
{
await this.BulkInsertAsync(entities, bulkConfig, progress, type, cancellationToken);
}
위와 같이 진행하였으며,

네 테스트가 통과되었습니다.
결과적으로 아래와 같이 유추가 가능한 것 같습니다.
- AddRange 는 SaveChangesAsync를 호출하는 순간 transaction을 건다.
- Add로 단건씩 추가한경우에는 transaction을 걸지않고, 단건씩 저장한다.
다만, 몇몇부분들은 좀 더 고민이 필요한 것 같지만, EF Core에서의 대량 업데이트가 필요하거나, 상단에서 Transaction이 걸려있다면, BulkInsert Extension을 고려해볼만 한것같습니다.
추가로, 제가 틀린부분이 있거나, 더 좋은 방법이 있다면 언제든 저에게 알려주세요. 감사합니다.