EF Core에서 Prisma와 같은 개발자 경험을 달성하세요! Linqraft 소개

발행: (2025년 12월 2일 오전 09:49 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

동기

C#는 멋진 언어입니다. 강력한 타입 안전성, 풍부한 표준 라이브러리, 그리고 GUI 애플리케이션 개발부터 웹 개발까지 모든 것을 처리할 수 있는 능력을 갖춘 C#는 거의 모든 목적에 적합한 훌륭한 언어라고 생각합니다.

하지만, 매일매일 C#에서 나를 좌절하게 하는 무언가가 있습니다.

클래스를 정의하는 것이 귀찮다! 그리고 Select 쿼리를 작성하는 것이 귀찮다!

C#는 정적 타입 언어이기 때문에 사용하려는 모든 클래스를 정의해야 합니다. 어느 정도는 피할 수 없지만, 매번 파생 클래스를 정의해야 한다는 점은 매우 번거롭습니다.

특히 데이터베이스 접근을 위해 ORM(Object‑Relational Mapping)을 사용할 때는 원하는 데이터 형태를 DTO(Data Transfer Object)로 매번 정의해야 하므로, 비슷한 클래스 정의를 반복해서 작성하게 됩니다.

Prisma vs. EF Core

Prisma(TypeScript ORM)에서는 다음과 같이 작성할 수 있습니다:

// user type is automatically generated from schema file
const users = await prisma.user.findMany({
  // Specify the data you want with select
  select: {
    id: true,
    name: true,
    posts: {
      // You can also specify related table data with select
      select: {
        title: true,
      },
    },
  },
});

// The type of users automatically becomes:
// type Users = {
//   id: number;
//   name: string;
//   posts: { title: string }[];
// }[];

같은 일을 C#의 EF Core에서 시도하면 다음과 같습니다:

// Assume Users type is defined in a separate file
var users = dbContext.Users
    // Specifying the data you want with Select is the same
    .Select(u => new UserWithPostDto
    {
        Id = u.Id,
        Name = u.Name,
        // Child classes are also specified with Select in the same way
        Posts = u.Posts.Select(p => new PostDto { Title = p.Title }).ToList()
    })
    .ToList();

// You have to define the DTO class yourself!
public class UserWithPostDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List Posts { get; set; }
}

// Same for child classes
public class PostDto
{
    public string Title { get; set; }
}

이미 User 클래스가 존재함에도 불구하고, 여기서는 자동으로 생성될 수 있을 것처럼 보입니다…

쿼리가 복잡해질수록 보일러플레이트 코드가 급격히 늘어납니다:

var result = dbContext.Orders
    .Select(o => new OrderDto
    {
        Id = o.Id,
        Customer = new CustomerDto
        {
            CustomerId = o.Customer.Id,
            CustomerName = o.Customer.Name,
            // Tedious part
            CustomerAddress = o.Customer.Address != null
                ? o.Customer.Address.Location
                : null,
            // Wrap in another DTO because we don't want to check every time
            AdditionalInfo = o.Customer.AdditionalInfo != null
                ? new CustomerAdditionalInfoDto
                {
                    InfoDetail = o.Customer.AdditionalInfo.InfoDetail,
                    CreatedAt = o.Customer.AdditionalInfo.CreatedAt
                }
                : null
        },
        Items = o.Items.Select(i => new OrderItemDto
        {
            ProductId = i.ProductId,
            Quantity = i.Quantity,
            // Same for arrays. Hard to read...
            ProductComments = i.CommentInfo != null
                ? i.CommentInfo.Comments.Select(c => new ProductCommentDto
                {
                    CommentText = c.CommentText,
                    CreatedBy = c.CreatedBy
                }).ToList()
                : new List()
        }).ToList()
    })
    .ToList();

Note: 위에서 사용된 모든 DTO 클래스 정의도 별도로 정의해야 합니다.

Null‑checking 문제점

EF Core의 Select 식에서는 null‑조건 연산자(?.)를 Expression 내부에서 사용할 수 없습니다. 따라서 아래와 같이 장황한 삼항 연산자를 써야 합니다:

// Unbelievably hard to read
Property = o.A != null && o.A.B != null && o.A.B.C != null
    ? o.A.B.C.D
    : null;

컬렉션이 null일 수 있을 때도 같은 문제가 발생합니다:

// Give me a break
Items = o.Child != null
    ? o.Child.Items.Select(i => new ItemDto { /* ... */ }).ToList()
    : new List();

어떻게 생각하시나요? 저는 이게 정말 싫습니다.

내가 원한 것

다시 Prisma 예제를 보면 대략 다음과 같은 특징이 있습니다(또한 TypeScript 언어 기능을 활용):

  • 한 번 쿼리를 작성하면 해당 타입이 자동으로 생성됩니다.
  • ?. 를 쿼리 안에서 바로 사용할 수 있어 null 체크에 대해 고민할 필요가 없습니다.

몇 번 고민한 끝에 익명 타입, 소스 제너레이터, 인터셉터를 결합하면 이러한 기능을 구현할 수 있겠다는 결론에 도달했습니다.

구현 시도

익명 타입 사용

C#의 익명 타입은 new { ... } 를 작성하면 컴파일러가 자동으로 클래스를 생성해 줍니다:

// No explicit type name after new
var anon = new
{
    Id = 1,
    Name = "Alice",
    IsActive = true
};

Select 쿼리 안에서 일회성 클래스를 정의할 때 유용합니다:

var users = dbContext.Users
    .Select(u => new
    {
        Id = u.Id,
        Name = u.Name,
        Posts = u.Posts.Select(p => new { Title = p.Title }).ToList()
    })
    .ToList();

// You can access and use it normally
var user = users[0];
Console.WriteLine(user.Name);
foreach (var post in user.Posts)
{
    Console.WriteLine(post.Title);
}

하지만 익명 타입은 이름이 없기 때문에 메서드 인자나 반환값으로 사용할 수 없습니다. 이 제한 때문에 많은 상황에서 활용도가 떨어집니다.

해당 클래스 자동 생성

쿼리에서 사용된 익명 타입의 형태를 분석해 소스 제너레이터가 자동으로 구체 클래스를 생성한다면 위 제한을 극복할 수 있습니다. 바로 Linqraft가 하는 일입니다.

(이후 구현 세부 내용이 이어집니다…)

Back to Blog

관련 글

더 보기 »