Entity Framework Core가 느리다 혹은 눈먼 엔지니어들
Source: Dev.to
Entity Framework Core는 .NET 생태계에서 가장 생산적인 도구 중 하나입니다.
팀이 빠르게 움직이고, 도메인을 깔끔하게 모델링하며, 기능을 신속하게 배포할 수 있게 해줍니다. 데이터베이스가 어떻게 동작하는지 자세히 알 필요 없이 코드를 작성할 수 있는데, 이는 장점이자 단점이기도 합니다.
EF Core는 프로젝트가 작을 때는 아주 잘 작동합니다.
- 더 많은 데이터
- 더 많은 관계
- 더 많은 엣지 케이스
- 더 많은 성능‑민감 경로
프로젝트가 커지면 문제가 나타나기 시작하고, EF가 자주 비난받게 됩니다. 엔지니어들은 추가 인덱스를 만들기도 하는데, 이는 일시적으로만 문제를 가립니다.
근본 원인: 엔지니어들이 EF 쿼리가 어떻게 SQL로 변환되는지 전혀 모른다는 점입니다.
많은 성숙한 코드베이스에서는 LINQ 쿼리를 작성할 때 SQL 변환을 전혀 고려하지 않습니다. 작은 규모에서는 이것이 눈에 띄지 않을 수 있습니다:
- 레코드 수가 적음
- 동시성이 낮음
- 허용 가능한 지연 시간
하지만 데이터가 늘어나면서 그 “무해한” 쿼리들은 다음과 같은 문제로 이어질 수 있습니다:
- 예상치 못한 다중 조인
- N+1 쿼리
- 전체 테이블 스캔
- 과도한 메모리 사용
- 최적화되지 않은 실행 계획
EF Core는 SQL을 숨기지 않습니다 — 직접 생성합니다. 생성된 SQL을 이해하지 못한다면, 사실상 눈을 가린 채 코딩하는 셈입니다.
실제 프로덕션 예시
몇 년 전, 프로덕션 시스템에서 성능 문제를 조사하던 중 다음과 같은 레거시 코드를 발견했습니다:
- 수백 개의 엔티티를 메모리로 로드
- 변경 추적을 활성화
- 루프 안에서 업데이트 수행
SaveChangesAsync()호출
var cutoff = nowUtc.AddDays(-90);
var users = await db.Users
.Where(u =>
u.Status == UserStatus.Active &&
(u.LastLoginUtc == null || u.LastLoginUtc
u.Status == UserStatus.Active &&
(u.LastLoginUtc == null || u.LastLoginUtc setters
.SetProperty(u => u.Status, UserStatus.Archived)
.SetProperty(u => u.UpdatedAtUtc, nowUtc),
ct);
근본 원인은 “EF가 느리다”가 아니라, 비용을 이해하지 못한 채 ORM 추상화를 사용했기 때문입니다.
주요 교훈
- LINQ가 SQL로 어떻게 변환되는지 이해한다.
- 변경 추적이 필요할 때와 필요하지 않을 때를 구분한다.
- 쿼리를 집합 기반, 컴파일된 형태, 혹은 원시 SQL로 작성해야 할 시점을 인식한다.
- 작업에 맞는 도구를 사용한다: 때로는 EF Core가 최적이지만, 경우에 따라 저장 프로시저, 뷰, 혹은 원시 쿼리가 더 적합할 수 있다.
도구가 어떻게 동작하는지 모른다면, 도구를 비난하지 마세요.