리얼월드를 지탱하는 기술 #1 — Entity Framework Core ORM (소개편)

Yechanny
Uniquegood
Published in
12 min readOct 27, 2021

리얼월드를 지탱하는 기술 #1 — Entity Framework Core ORM (소개편) <<
리얼월드를 지탱하는 기술 #2 — Entity Framework Core ORM (활용편)

안녕하세요 유니크굿컴퍼니에서 서버 개발을 담당하고 있는 책임개발자 이예찬입니다. 앞으로 리얼월드에 핵심적으로 사용 되는 기술들에 대해 간략히(?) 소개 드리는 글들을 종종 작성하려고 합니다. 그 첫번째인 이번 글에서는 저희가 서버 개발을 할 때 데이터베이스로부터 값을 읽고 쓰기 위해 어떤 방법과 라이브러리를 사용하는지 소개 드리면서 그 라이브러리가 갖고 있는 특장점을 소개 드리려고 합니다. 다음 글로는 저희가 그 라이브러리를 실제로 어떻게 활용하고 있는지 사례를 들어보도록 하겠습니다.

ORM (Object-Relation-Mapping)

유니크굿컴퍼니는 데이터베이스 인프라로 90%이상 관계형 데이터베이스를 사용하고 있습니다. 시험적으로 Azure Cosmos DB 같은 NoSQL 데이터베이스를 사용해본적이 있지만, 아직 대부분의 데이터베이스는 Azure SQL(Microsoft SQL Server)로 이루어져있습니다.

그러나 관계형 데이터베이스를 사용하고 있음에도 저희는 SQL 쿼리를 직접 작성하지 않습니다. 거의 모든 쿼리를 ORM(Object-Relation-Mapping) 라이브러리인 Entity Framework Core가 쿼리 빌더(Query Builder)로서 직접 짜주기 때문입니다. 익숙하지 않은 쿼리에 대한 고민을 최소화하고 핵심적인 로직에만 집중할 수 있게 해주는 고마운 도구입니다.

그리고 쿼리를 짜주는 것으로 끝이 아니라, ORM인 만큼 당연하게도 테이블 구조와 실제 객체 구조의 차이에서 오는 임피던스 불일치를 구체화(Materialize) 작업을 통해 해소시켜주어 편하게 작업할 수 있습니다.

Entity Framework Core

Entity Framework Core(이하 EF)는 닷넷 환경에서 동작하는 오픈소스 ORM 라이브러리입니다. 닷넷 생태계에서 사용하는 다른 ORM인 Dapper와는 다르게 EF는 구체화 작업만 하는 게 아니라 쿼리 빌더의 역할과 DB 테이블을 코드로 생성하고 수정하는 마이그레이션 기능까지도 포함하고 있는 커다란 라이브러리입니다.

오픈소스이긴 하지만 Microsoft의 주도로 만들어지고 있는 만큼 Microsoft SQL Server와 사용했을 때 가장 안정적으로 사용할 수 있습니다. 하지만 MySQL(MariaDB)(비공식지원)나 PostgreSQL(비공식지원)같은 DB는 물론이고 SQLite(공식지원)와 같은 로컬 환경에서도 사용할 수 있습니다. 거기다 테스트용으로 In-Memory 데이터베이스도 사용할 수도 있습니다.

EF는 Entity Framework라는 이름으로 버전 6까지 개발이 되다가, 닷넷이 오픈소스화 되어 닷넷 코어가 생겨나면서부터는 Entity Framework Core (EF Core)라는 이름으로 버전 6까지 개발 되고 있습니다. 만약 최신(2016년 이후) 닷넷으로 개발하신다면 EF Core를 사용하시면 됩니다. (앞으로 Entity Framework와 EF라고 작성하는 부분은 모두 EF Core로 읽어주시면 되겠습니다.)

모델 생성

EF에서 데이터베이스 테이블에 매핑되는 객체를 모델이라고 합니다. 그리고 그 모델은 두 가지 방법으로 만들 수 있습니다. 하나는 이미 만들어져있는 데이터베이스 스키마를 읽어들어와서 모델의 코드를 생성하는 DB-First 방식이고, 다른 하나는 코드로 미리 모델을 만들어두고 이를 바탕으로 마이그레이션 코드(DB 테이블의 생성과 수정 정보를 담은 코드)를 자동으로 생성한 다음 최종적으로 DB스키마를 생성하는 Code-First 방식이 있습니다. Code-First는 모델 코드의 수정사항을 EF가 분석하고 판단하여 자동으로 마이그레이션 정보를 생성해주니 매우 편리한 기능입니다.

DB-First 방식의 경우 데이터베이스 전문가가 있어서 데이터베이스 스키마를 먼저 만들기 용이한 경우나, 이미 레거시 테이블이 있는 경우 사용하기 좋은 방법입니다. 하지만 저희는 별도의 데이터베이스 전문가도 없고, 레거시도 없는 상황이므로 Code-First로 모델을 먼저 만들고 마이그레이션을 통해 데이터베이스 스키마를 생성합니다.

DB-First 방식의 작동 과정
Code-First 방식의 작동 과정

특히 Code-First로 작업할 경우 테이블이나 열의 추가와 같이 스키마의 변경사항이 있을 때, 모델 코드만 수정하면 마이그레이션 정보를 만들고 DB에 반영되게 만들 수 있습니다. 즉 모델 코드가 단일 진실 공급원(Single source of truth)이 되기 때문에 모델과 관계의 매핑에 있어서 관리 지점이 줄어드는 장점이 있습니다.

하나의 추상적인 데이터 모델에 대해서 모델 코드로써 표현 되는 방법과 데이터베이스 스키마로써 표현 되는 방법 두 가지가 모두 공존하기 때문에 어느 한 쪽만 수정 되면 안 됩니다. 모델 코드가 바뀌면 스키마도 바뀌어야 하고 스키마가 바뀌면 모델 코드도 바뀌어야 합니다. 그러나 둘 다 자유롭게 바꿀 수 있게 되면 관리해야 하는 지점이 늘어나기 때문에 사람이 실수 할 수 있는 부분이 많아집니다. 그래서 어느 한쪽을 진실(Truth)로 정해두고 다른 한 쪽을 어떠한 도구를 사용하여 그 진실에 맞게 반영하는 편이 훨씬 덜 복잡합니다. 그리고 EF는 현재 모델 코드를 단일 진실 공급원으로 사용하여 모델 코드와 데이터베이스 스키마 사이에 지속적인 동기화를 지원하고 있습니다.

사실 EF Core에서 지원하는 DB-First는 과거 EF에서 지원하던 DB-First와는 다릅니다. EF Core의 DB-First는 DB로부터 모델을 생성만 하는 수준입니다. 한 번 모델 코드를 생성하면 스키마가 수정 되더라도 모델 코드의 업데이트를 할 수 없었죠. 반면, 과거 EF의 DB-First는 DB 스키마를 단일 진실 공급원으로 사용하고, DB 스키마가 업데이트 되면 업데이트 마법사(…)을 통해 모델 코드를 업데이트 하는 방식이었습니다만, 현재는 이러한 방식이 지원되지는 않습니다.

이 기능은 EF Core 오픈소스 프로젝트의 백로그에 올라와 있는 기능이지만 2014년부터 지금까지 백로그에 있는 걸 봐서는 확실히 Code-First를 더 우선적으로 생각하고 있다는 것을 알 수 있습니다.

마이그레이션

위에서 마이그레이션에 대한 얘기를 잠깐 했는데요, 코드를 단일 진실 공급원으로 사용할 때 꼭 필요한 기능이라고 할 수 있습니다. 마이그레이션은 데이터베이스 테이블의 생성과 수정을 코드로 조작할 수 있게 하는 것입니다. 많은 ORM들이 마이그레이션 기능을 제공합니다. 제가 사용해본 게 많지는 않지만, JavaScript Node.js 쪽에서 많이 쓰이는 Sequelize도 마이그레이션 기능을 제공하고, PHP Laravel의 Eloquent ORM에서도 마이그레이션 기능을 제공합니다. 그러나 자동으로 모델 코드를 분석해서 마이그레이션 코드를 제공해주는 경우는 많지 않은 것 같습니다. (Python Django에서는 제공해주는 것을 확인했습니다)

EF에서는 이러한 마이그레이션 코드를 모델 코드를 분석하여 자동으로 생성해줍니다. 자동으로 생성할 경우 모델 코드가 단일 진실 공급원이 되는 것이므로 관리 포인트가 줄어든다는 장점이 있습니다. 물론 자동으로 생성된 것인만큼 생성 된 마이그레이션 코드를 직접 검토해보는 과정은 필요합니다. 의도치 않은 변경 사항이 들어가있을 수도 있으니까요. 그럼에도 대부분의 코드를 자동으로 생성해주는 것은 실수를 많이 줄여주는 도구입니다.

아래 예제는 자동으로 생성 된 마이그레이션 코드 예제입니다. 테이블 열에 대한 내용과 제약조건들(기본 키, 외래 키 등)이 코드로써 정의 되는 것을 보실 수 있습니다.

자동 생성 마이그레이션 코드. 테이블 열에 대한 내용과 제약조건들(기본 키, 외래 키 등)이 코드로써 정의 됩니다.

쿼리

저는 ORM 라이브러리에 있어서 가장 중요한 능력 중 하나는 쿼리를 만들어내는 능력이라고 생각합니다. 테이블의 데이터를 관계로 매핑하는 구체화 기능은 기본이죠. 저는 EF가 빛을 발하는 부분이 바로 이 지점에 있다고 생각합니다.

EF에서 쿼리를 만드는 경험은 마치 일반적인 Enumerable 확장 메서드(Java의 Stream API와 유사한 개념)를 다루는 것과 같은 경험을 제공합니다. 람다식으로 데이터를 필터하고 정렬하고 다른 타입으로 매핑할 수 있습니다. 아래 실제 리얼월드에서 댓글(도움글, 후기글)을 조회하는 데에 사용 중인 쿼리를 일부 가져왔습니다.

얼핏 보기에는 단순하게 Enumerable 확장 메서드를 다루는 것과 별반 다르지 않아 보입니다. 그러나 위 코드의 결과로 나온 commentsQuery 변수는 실제 값을 가지고 있거나 일반적인 Enumerable이 아니라 데이터베이스 쿼리의 형태로 존재합니다. 아래 코드로 데이터베이스에 전송 될 쿼리를 가져올 수도 있고, 쿼리를 실행해 실제 값으로 받아올 수도 있습니다.

가져온 SQL 쿼리는 아래와 같습니다. 파라미터화까지 알아서 처리 해 주기 때문에 SQL Injection 등의 보안 위협으로부터 훨씬 안전하다는 걸 알 수 있습니다.

Enumerable 확장 메서드를 다루는 것과 비슷하다는 뜻은 학습에 대한 비용이 그만큼 낮다는 것이고, 타입에 대한 안정성이 컴파일 타임에서 보장 된다는 뜻입니다. 그리고 ‘원하는 데이터를 얻기 위해 이렇게 하면 되나?’ 하며 Enumerable 다루듯이 코드를 짜보면 대부분 원하는 데이터를 잘 가져온다는 말이기도 합니다. (물론 이 말이 완벽하게 최적화 된 쿼리를 만들어준다는 말은 아닙니다)

제가 매우 복잡한 쿼리를 다뤄본 적이 많지 않아서 “복잡한 쿼리도 다 됩니다!” 라고 단언하긴 어렵지만 여태까지 제가 겪어본 한, 관계에 대한 Join은 물론이고 중첩 쿼리 등 꽤나 많은 요구사항들을 잘 충족해왔습니다.

예를들어, ORM이라는 이름에 걸맞게 테이블 간 관계를 풀어내는 것도 가능합니다. 코드 상으로는 Join에 대한 고민을 하지 않아도 작성할 수 있지만, 실제로 쿼리로 만들어질 때는 Join을 통해 작동할 수 있도록 잘 추상화 되어 있습니다. 흔히 겪는 N+1 문제를 발생시키는 주범이지만 다음과 같이 한 번에 쿼리 하면 잘 풀어갈 수 있습니다. 아래는 위에서와 같은 상황이지만 각 댓글에 대한 작성자 정보까지 한번에 가져오도록 하는 쿼리입니다.

Join 말고 중첩 쿼리는 어떨까요? 글 목록을 불러올 때, 사용자에 의해 차단 된 사람의 글은 불러오지 않는 경우를 생각해볼 수 있습니다. 그럴 경우 아래과 같이 Enumerable 확장 메서드를 다루는 모습으로 쿼리를 만들 수 있고, 이 때 중첩 쿼리가 사용 되지만 SQL로 잘 변역이 됩니다.

이외에도 문자열 조작, DateTime 조작, Full Text Search 등 조금 복잡할 수 있는 쿼리들도 SQL 쿼리가 지원하는 다양한 함수와 기능들로 잘 번역 해줍니다.

식 트리 (Expression Tree)

위와 같이 다양한 쿼리를 마치 Enumerable 확장 메서드를 다루듯이 자유자재로 다룰 수 있게 되는 배경에는 C#에서 제공 되는 언어 내장 식 트리 생성 기술이 있습니다. 식 트리란 각종 연산이나 메소드 호출, 람다식 등을 트리(Tree) 형태로 표현한 것을 말합니다.

(12 / 4) + (5–1)을 트리 형태로 표현한 것

그리고 이 식 트리는 직접 호출되기 보다는 프로그래밍적으로 분석되는 데에 주로 사용 됩니다. Reflection같은 일종의 메타프로그래밍이라고 볼 수 있습니다. 물론 식 트리를 CIL(Common Intermediate Language, 닷넷 런타임의 바이트코드와 같은 개념)로 컴파일 해서 호출 하기도 하지만 식 트리 자체를 호출한다고 할 수는 없습니다.

C#에는 람다식을 식 트리로 평가하고 변환하는 기능이 내장 돼 있습니다. 예컨대 아래와 같은 코드는 함수 대리자(Delegate)로 평가 될 수도 있지만 식 트리로 평가 될 수도 있습니다.

위의 expression이 식 트리(Expression<Func<string, int>>)일 경우 다음과 같은 식 트리로 표현 될 것입니다.

EF는 이러한 식 트리를 분석해 CIL 대신 SQL로 컴파일 하고 이를 데이터베이스로 쿼리 하는 것입니다. 물론 모든 식 트리가 EF에서 SQL로 변환 되는 것은 아닙니다. 데이터베이스 엔진에서 평가(Evaluation)될 수 있는 식만 사용 가능한데, 대표적으로 객체의 메소드를 호출하는 식은 데이터베이스 엔진에서 평가 될 수 없는 식이기 때문에 사용할 수 없습니다.

식 트리 자체야 어느 언어에서든 구현할 수 있겠지만 람다식을 식 트리로 평가하는 언어 내장 기능은 제가 아는 한 C# 밖에 없습니다. 그리고 이렇게 식 트리로 모든 쿼리가 작성 되기 때문에 컴파일 시점에 타입 오류를 체크할 수 있어 훨씬 안정적으로 데이터베이스 작업을 할 수 있습니다. 쿼리를 만들기 위한 별도의 함수를 공부할 필요도 없죠. EF가 특별할 수 있는 이유라고 할 수 있습니다.

마무리

이번 글에서는 저희 유니크굿컴퍼니에서 핵심적으로 사용하고 있는 ORM인 Entity Framework Core가 어떤 것인지, 어떤 원리로 작동하는지 간략하게 소개 드렸습니다. 다음 글에는 복잡한 쿼리 상황들에 EF가 어떻게 쓰이고 있는지, 그리고 저희는 데이터베이스를 어떻게 최적화(?)해서 사용하고 있는지 리얼월드의 사례를 들어 소개해보겠습니다. 감사합니다!

--

--