**내가 만들었어요! 게시판 공지 사항**의 내용을 숙지하신 후, 글을 게시해주시기 바랍니다.
개인적으로 orm보다 sql이 직관적으로 편하기도 하고 왜 닷넷으로 서비스하고있는지는 모르겠지만 서버도 쥐똥만한거 쓰면서 통계, 연산 위주의 서비스를 dotnet ef로 하려니 성능이나 메모리 문제 잡기 힘들어서 만들어 봤습니다..
**내가 만들었어요! 게시판 공지 사항**의 내용을 숙지하신 후, 글을 게시해주시기 바랍니다.
개인적으로 orm보다 sql이 직관적으로 편하기도 하고 왜 닷넷으로 서비스하고있는지는 모르겠지만 서버도 쥐똥만한거 쓰면서 통계, 연산 위주의 서비스를 dotnet ef로 하려니 성능이나 메모리 문제 잡기 힘들어서 만들어 봤습니다..
좋은 라이브러리를 만들어주셔서 감사합니다. 참고로, 사상적으로 MyBatis와 비슷한 포지션의 잘 알려진 라이브러리로 Dapper가 있습니다. 만드시는 라이브러리에서 참고해볼 만한 사항들이 많이 있을 것 같습니다. ![]()
GitHub - DapperLib/Dapper: Dapper - a simple object mapper for .Net
좋네요!
mybatis의 장점은 group 기능을 이용해 여러 데이터를 매핑해줄 수 있고 entity 재사용이 가장 큰 장점이라 선호하는 편인데요.
dapper는 일반적으로 강력한 orm인데 sql server를 쓰면서 join이 많아지는 sp가 많아지게 되는데 그런 경우 매핑 코드가 너무 늘어나서 유지보수에는 다소 불편함이 있어 좀 아쉬운 감이 있습니다.
최근에 smartsql을 봤었는데 비슷한 성격의 좋은 라이브러리네요. .net10에서도 사용할 수 있으면 좋겠습니다
감사합니다. 이제 닷넷 7,8,9,10 모두 지원합니다.
벤치 마크 소스 코드를 볼 수 있을까요?
예제프로젝트와 함께 밴치마크 포함되어 있습니다.
벤치마크 프로젝트가 실행가능한 상태인지요?
글로벌 셋업이 공란 아니면 주석처리라…
그리고, 복잡한 쿼리는 전부 건너 띄신 거 같습니다. ^^
귀신이 곡할 노릇이네요 ㅋㅋ 주말 내내 작업했던건데…
문자열 기반 orm 의 한계는 사실 아래와 같은 복잡한 요구사항에 대응할 때 너무 많은 노동을 강요한다는 점에 있습니다.
// Dapper
public async Task<long> CreateCompleteOrderAsync(Order order, IEnumerable<OrderItem> items, Payment payment, Shipment shipment)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync();
await using var transaction = await conn.BeginTransactionAsync();
try
{
var orderId = await InsertAsync(order);
await transaction.CommitAsync();
return orderId;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
// Nuvatis
public async Task<long> CreateCompleteOrderAsync(Order order, IEnumerable<OrderItem> items, Payment payment, Shipment shipment)
{
// 트랜잭션 처리 필요 - 간소화
var orderId = await _mapper.InsertAsync(order);
return orderId;
}
그런 점때문에 벤치마크에서도 구현하지 못하고 Insert 로 대신한 듯 보입니다.
사실, 위 코드는 테스트 실패라서, 벤치마크의 결과가 아무런 의미가 없습니다.
테스트를 패스하려면, 로직과 쿼리가 매우 복잡하게 얽히는 상황을 피할 수는 없을 것입니다.
// Dapper
public async Task<long> CreateCompleteOrderAsync(Order order, IEnumerable<OrderItem> items, Payment payment, Shipment shipment)
{
if (order.Id != default || order.OrderItems.Count != 0 || order.Shipment != null || order.Payment != null)
throw new InvalidArguementException("신규로 생성된 객체가 아님.");
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync();
await using var transaction = await conn.BeginTransactionAsync();
try
{
// order 저장 후 외래키 획득,
foreach(var item in items)
{
if (item.Id == default)
{
// Insert
}
else
{
// Update
}
}
// shipment, payment 동일 과정 반복
이런 상황이 이 메서드가 유일하지도 않을 것입니다.
재밌는 점은 EF Core 의 구현도 실패 코드를 기계적으로 답습하고 있습니다.
// EF Core
public async Task<long> CreateCompleteOrderAsync(Order order, IEnumerable<OrderItem> items, Payment payment, Shipment shipment)
{
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
_context.Orders.Add(order);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
return order.Id;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
사실, EF Core 는 ChangeTracker 가 복잡한 로직 - Insert 할 것과 Update 할 것을 구분하는 것을 다 해결해주기 때문에, 아래와 같이 간단하게 구현해도 됩니다.
// EF Core
public async Task<long> CreateCompleteOrderAsync(Order order)
{
_context.Orders.Add(order);
var affected = await _context.SaveChangesAsync();
return affected > 0 ? order.Id : -1;
}
SaveChanges(Async) 는 암묵적으로 Transcation 을 쓰기 때문에 굳이 Transaction 을 추가할 필요는 없습니다.
ChangeTracker 에 기반한 위 코드는 사실 테스트가 필요없을 정도로 정확하게 동작합니다.
성능 비용을 지불하면서까지 EF Core를 사용하는 이유는 이런 정확성과 코딩 효율때문입니다. (Linq 의 효율도 어마어마하고 DB 관리도구도 엄청나죠)
NuVatis 가 위와 같은 복잡한 요구 사항에도 대응하기 위해서는 ChangeTracker 와 유사한 기능을 구현해야 하는데, 그러면 정작 비효율이라고 비판하던 EF Core 의 또 다른 버전이 되고, 만약 대응하지 못한다면 손가락 품을 강요하는 그저 그런 도구로 전락하는 아이러니가 발생합니다.
이런 아이러니는 존중 보다는 섣부른 비판이 먼저였던 것이 원인이지 않을까하는 생각이 듭니다.
제가 NuVatis를 만들게 된 이유는,
현 직장에서 처음 맡은 일이 슬로우 쿼리로 인한 타임아웃 문제 해결이었습니다.
이후에도 한동안 전임자가 구현해놓은 통계, 지표 조회를 짬짬이 최적화했고, 조회 속도는 최대 3600배까지도 개선할 수 있었지만 메모리 문제만큼은 좀처럼 해결이 되지 않았습니다. 아마존에서 4GB 인스턴스를 쓰고 있는데 OOM으로 서비스가 멎는 일이 몇 번 반복됐음에도 "돈 안 쓰는 해결책을 찾으라"는 대표의 의지가 확고했고, AI를 활용해도 제가 이미 최적화한 것 이상의 효과를 낼 수 없었습니다.
그때 떠올린 것이 이전 직장에서의 경험이었습니다. 그전에는 자바쪽 일만 했었고 훨씬 크고 거대한 서비스를 운영하면서도 이런 종류의 문제가 없었는데 언어와 프레임워크가 다르다고는 해도 이 정도 차이가 날까 싶어 생각하다 마이바티스가 떠올랐고, 실제로 가장 부하가 큰 조회 쿼리를 XML 매퍼 방식으로 구현해봤더니 메모리 사용량을 80% 이상 줄일 수 있었습니다. NuVatis는 그 경험을 기반으로 만들었습니다.
지적하신 내용은 맞습니다. 벤치마크의 CreateCompleteOrderAsync 구현은 items, payment, shipment를 파라미터로 받으면서 실제로는 order 하나만 저장합니다.
이것은 간소화가 아니라 미완성 구현이고, 이 상태에서 측정된 수치는 “완전한 주문 생성” 시나리오의 성능이 아닙니다. NuVatis, Dapper 모두 같은 결함을 가지며 해당 시나리오의 벤치마크 결과는 의미가 없습니다.
EF Core의 ChangeTracker가 해결해주는 문제도 실제로 큰 가치가 있습니다. 객체 그래프의 상태를 추적해 INSERT/UPDATE/DELETE를 자동 판별하고 SaveChangesAsync가 암묵적 트랜잭션으로 이를 원자적으로 처리해준다는 것은, 복잡한 도메인 로직에서 단순한 편의성을 넘어 정확성을 보장하는 수단입니다.
NuVatis나 Dapper에서 이 수준의 시나리오를 다루려면 개발자가 상태 판별과 실행 순서를 전부 직접 관리해야 하고, 복잡도가 높아질수록 그 비용은 선형이 아니라 지수적으로 늘어나겠죠.
"NuVatis가 ChangeTracker를 구현하면 EF Core의 또 다른 버전이 된다"는 지적도 정확합니다. 이것은 ORM 설계 공간의 근본적인 분기점입니다. 성능과 편의성은 동일한 설계에서 동시에 최대화될 수 없고, 도구는 이 스펙트럼 위의 어느 지점을 선택할 수밖에 없습니다.
NuVatis는 제 개인적인 경험에서 나온 도구이다 보니, 제가 겪었던 문제, 즉 대량 데이터를 다루는 읽기 중심 시나리오에서의 메모리 효율에 최적화되어 있습니다. 복잡한 도메인 객체 그래프를 다루는 쓰기 시나리오는 솔직히 EF Core가 더 적합한 도구입니다. 이 경계를 명확히 하지 않은 채 벤치마크를 구성한 것은 제 실수가 맞습니다.
원래부터 ORM이 별로 직관적이지 않다고 생각해왔던 데다 실무 경험까지 겹치면서 EF Core에 대한 시각이 부정적으로 굳어진 면이 있었던 것 같습니다. 비판이 먼저였고 정확한 맥락 설정이 뒤따르지 못했다는 점에서 불쾌하셨다면 죄송합니다.
벤치마크가 뭔가 확증 편향의 증명같다는 느낌을 받았기 때문에 댓글을 쓴 것이지, 제 기분 나쁨을 알리려 쓴 글은 아닙니다.
출발점이 편향적이면 올바른 평가를 할 수 없고, 보다 나은 결과물을 만드는데 방해가 될 것이기 때문입니다.
개인적으로는, 벤치마크를 새로 작성하기 보다는, Frans Bouma 가 작성한 ORM 별 벤치마크 저장소 를 fork 한 후, NuVatis 케이스를 추가하는 방법도 나쁘지 않을 것 같습니다. (필요하다면 기존 프레임워크 중 몇개를 제거하여 좀 더 규모를 줄일 수도 있구요.)
이렇게 하면 다른 프레임워크 코드에 선입견이 개입할 여지가 없어지고 NuVatis 코드에만 집중하는 장점이 있습니다.
편향된 데이터 맞아요 ㅋㅋ 제미나이한테 주석 좀 쓰라고 했더니 정말로 죄다 주석으로 만들어 버리고 대시보드도 목업 데이터로 싹다 박아넣어놨는데 초기에 직접 실측했던 데이터들과 맥락이 비슷해서 의심을 못했었습니다…
진짜 실측데이터 입니다.