mybatis for dotnet

**내가 만들었어요! 게시판 공지 사항**의 내용을 숙지하신 후, 글을 게시해주시기 바랍니다.

개인적으로 orm보다 sql이 직관적으로 편하기도 하고 왜 닷넷으로 서비스하고있는지는 모르겠지만 서버도 쥐똥만한거 쓰면서 통계, 연산 위주의 서비스를 dotnet ef로 하려니 성능이나 메모리 문제 잡기 힘들어서 만들어 봤습니다..

13개의 좋아요

좋은 라이브러리를 만들어주셔서 감사합니다. 참고로, 사상적으로 MyBatis와 비슷한 포지션의 잘 알려진 라이브러리로 Dapper가 있습니다. 만드시는 라이브러리에서 참고해볼 만한 사항들이 많이 있을 것 같습니다. :+1:

GitHub - DapperLib/Dapper: Dapper - a simple object mapper for .Net

3개의 좋아요

좋네요!
mybatis의 장점은 group 기능을 이용해 여러 데이터를 매핑해줄 수 있고 entity 재사용이 가장 큰 장점이라 선호하는 편인데요.
dapper는 일반적으로 강력한 orm인데 sql server를 쓰면서 join이 많아지는 sp가 많아지게 되는데 그런 경우 매핑 코드가 너무 늘어나서 유지보수에는 다소 불편함이 있어 좀 아쉬운 감이 있습니다.
최근에 smartsql을 봤었는데 비슷한 성격의 좋은 라이브러리네요. .net10에서도 사용할 수 있으면 좋겠습니다

4개의 좋아요

감사합니다. 이제 닷넷 7,8,9,10 모두 지원합니다.

5개의 좋아요

내친김에 Dapper, EF 등과 벤치마크도 해보았습니다.

벤치 마크 소스 코드를 볼 수 있을까요?

1개의 좋아요

예제프로젝트와 함께 밴치마크 포함되어 있습니다.

벤치마크 프로젝트가 실행가능한 상태인지요?
글로벌 셋업이 공란 아니면 주석처리라…

그리고, 복잡한 쿼리는 전부 건너 띄신 거 같습니다. ^^

귀신이 곡할 노릇이네요 ㅋㅋ 주말 내내 작업했던건데…

문자열 기반 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 의 또 다른 버전이 되고, 만약 대응하지 못한다면 손가락 품을 강요하는 그저 그런 도구로 전락하는 아이러니가 발생합니다.

이런 아이러니는 존중 보다는 섣부른 비판이 먼저였던 것이 원인이지 않을까하는 생각이 듭니다.

1개의 좋아요

제가 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에 대한 시각이 부정적으로 굳어진 면이 있었던 것 같습니다. 비판이 먼저였고 정확한 맥락 설정이 뒤따르지 못했다는 점에서 불쾌하셨다면 죄송합니다.

3개의 좋아요

벤치마크가 뭔가 확증 편향의 증명같다는 느낌을 받았기 때문에 댓글을 쓴 것이지, 제 기분 나쁨을 알리려 쓴 글은 아닙니다.

출발점이 편향적이면 올바른 평가를 할 수 없고, 보다 나은 결과물을 만드는데 방해가 될 것이기 때문입니다.

개인적으로는, 벤치마크를 새로 작성하기 보다는, Frans Bouma 가 작성한 ORM 별 벤치마크 저장소 를 fork 한 후, NuVatis 케이스를 추가하는 방법도 나쁘지 않을 것 같습니다. (필요하다면 기존 프레임워크 중 몇개를 제거하여 좀 더 규모를 줄일 수도 있구요.)

이렇게 하면 다른 프레임워크 코드에 선입견이 개입할 여지가 없어지고 NuVatis 코드에만 집중하는 장점이 있습니다.

1개의 좋아요

편향된 데이터 맞아요 ㅋㅋ 제미나이한테 주석 좀 쓰라고 했더니 정말로 죄다 주석으로 만들어 버리고 대시보드도 목업 데이터로 싹다 박아넣어놨는데 초기에 직접 실측했던 데이터들과 맥락이 비슷해서 의심을 못했었습니다…

진짜 실측데이터 입니다.

1개의 좋아요