메인 콘텐츠로 건너뛰기

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples on this page work on VillageSQL. Install Now →
ORM은 대부분의 애플리케이션이 실제로 MySQL과 대화하는 방식이고, 대부분의 ORM 문제는 몇 줄의 설정과 한 가지 쿼리 패턴으로 거슬러 올라갑니다. 이 가이드는 Django, Rails, Prisma를 위한 MySQL 쪽 설정과, ORM이 생성한 SQL이 제품의 느린 부분이 되지 않게 하는 습관을 다룹니다. VillageSQL은 MySQL의 드롭인 대체품이므로 여기의 모든 설정은 수정 없이 그대로 동작합니다: ORM은 차이를 알아차리지 못합니다.

어디서나 중요한 설정

프레임워크가 무엇이든 세 가지는 반드시 참이어야 합니다:
  • 연결이 utf8mb4를 사용합니다. 그렇지 않으면 이모지가 전송 중에 깨집니다. utf8mb4와 이모지를 참고하세요.
  • Strict SQL 모드가 켜져 있습니다(MySQL 8.x의 기본값). 그래서 잘못된 데이터는 조용히 잘리는 대신 오류를 냅니다.
  • 풀 크기는 의도적으로 정합니다. 모든 프레임워크는 프로세스마다 풀을 엽니다. 숫자를 고르기 전에 프로세스 수를 곱하세요. 연결 풀링을 참고하세요.

Django

Django 관련 참고 사항:
  • Django 문서가 권장하는, 유지 관리되는 C 드라이버인 mysqlclient를 사용하세요.
  • Django는 일부 제약 조건을 Python에서 흉내 냅니다. 실제 데이터베이스 제약 조건도 추가하세요(unique=True는 실제 고유 인덱스가 되지만, 검사 제약 조건은 Meta에 CheckConstraint가 필요합니다).
  • select_related()(JOIN)와 prefetch_related()(두 번째 쿼리)가 N+1 도구입니다. 언제 쓰는지는 아래를 참고하세요.

Rails

Rails 관련 참고 사항:
  • database.yml의 encoding: utf8mb4는 연결 문자 집합과 rails db:create의 기본값을 모두 설정합니다.
  • ActiveRecord 유효성 검사(validates_uniqueness_of)는 동시성 아래에서 경쟁 상태가 됩니다. 모든 고유성 검사를 실제 고유 인덱스로 뒷받침하고, 1062 중복 키 오류가 마지막 방어선이 되게 하세요.
  • includes가 N+1 도구입니다. strict_loading 모드는 지연 로딩을 오류로 바꾸어 N+1이 배포되는 대신 개발 중에 실패하게 합니다.
  • Rails 8.1은 이전 버전이 pool을 생성하던 자리에 database.yml에 max_connections를 생성합니다. pool: 키는 max_connections가 읽는 대체값으로 여전히 조용히 동작합니다. 둘을 서로 다른 값으로 설정하면 부팅 시 예외가 발생합니다. Rails 8이 더 이상 MySQL을 기본값으로 하지 않아 필요한 --database=mysql 플래그를 포함한 전체 앱은 Rails 8로 MySQL 사용하기를 참고하세요.

Prisma

Prisma 관련 참고 사항:
  • Prisma는 기본적으로 utf8mb4를 사용합니다. 주의할 설정은 URL의 connection_limit이며, 기본값이 낮습니다.
  • prisma migrate는 스키마 드리프트에서 DDL을 생성합니다. 직접 작성한 마이그레이션과 똑같이, 프로덕션에 적용하기 전에 생성된 SQL을 읽으세요. 스키마 마이그레이션을 참고하세요.
  • 관계 쿼리는 include를 사용합니다. 결과를 반복하며 관계를 하나씩 접근할 때 N+1 형태가 나타납니다.
  • Prisma 7은 위의 연결 URL을 schema.prisma 밖의 별도 설정 파일로 옮기고, 연결에 드라이버 어댑터를 요구합니다. 전체 Prisma 7 앱은 Next.js와 Prisma로 MySQL 사용하기를 참고하세요.

N+1 문제, 한 번에 정리

보편적인 ORM 성능 버그: 주문 100개를 로드한 뒤 각 주문의 고객을 지연 로드하면 ORM은 쿼리 101개를 실행합니다. 해결책은 모든 프레임워크에서 같은 아이디어입니다. 필요한 관계를 미리 선언해 ORM이 한두 개의 쿼리로 가져오게 하세요(select_related / includes / include). 탐지 도구도 보편적입니다: 개발 환경에서 쿼리 로깅을 켜고 ORM이 실제로 보내는 것을 읽거나, 프로덕션에서 슬로 쿼리 로그를 지켜보세요.

SQL로 내려가야 할 때

ORM은 CRUD에서 제값을 하고 분석에서는 그렇지 못합니다. 윈도우 함수, CTE, 대량 업서트, 다중 테이블 보고 쿼리는 그것을 표현하려 애쓰는 쿼리 빌더 체인보다 SQL에서 더 명확합니다. 모든 ORM에는 탈출구가 있습니다(Django의 raw(), Rails의 find_by_sql, Prisma의 $queryRaw). 그럴 가치가 있는 5퍼센트의 쿼리에 그것을 쓰는 것은 패배가 아니라 좋은 엔지니어링입니다. 이것은 또한 VillageSQL 확장 함수가 ORM 애플리케이션에 도달하는 방법이기도 합니다: SELECT UUID_V7()이나 ai_prompt(...)는 이 탈출구 중 어느 것을 통해서든 동작합니다. ORM은 SQL을 그대로 통과시킬 뿐이기 때문입니다.

함께 보기