GO에서 사용할 ORM: GORM, sqlc, Ent 또는 Bun?

GORM vs sqlc vs Ent vs Bun ```

Page content

Go의 생태계는 ORM (Object-Relational Mapping) 도구와 데이터베이스 라이브러리의 다양한 범위를 제공하며, 각각은 자체적인 철학을 가지고 있습니다. 여기에 PostgreSQL을 Go로 사용하는 방법: GORM, sqlc, Ent, Bun에 대한 포괄적인 비교가 있습니다.

이제 성능, 개발자 경험, 인기, 기능/확장성에 대해 평가해보겠습니다. User 모델에 대한 기본 CRUD 작업을 수행하는 코드 예시를 통해 각 도구의 장단점을 살펴보겠습니다. 중급 및 고급 Go 개발자는 각 도구의 트레이드오프를 이해하고, 자신에게 가장 적합한 도구를 선택하는 데 도움을 받을 수 있습니다.

노트북 화면에 일부 코드가 있는 이미지

ORM 개요

  • GORM – 기능이 풍부한 Active Record 스타일의 ORM입니다. GORM은 Go 구조체를 모델로 정의하게 하며, 메서드 체이닝을 사용하여 데이터를 쿼리하고 조작할 수 있는 광범위한 API를 제공합니다. 수년간 사용되어 왔으며, Go ORM 중 가장 널리 사용되는 도구 중 하나로, GitHub에서 거의 39,000개의 스타를 받았습니다. GORM의 매력은 강력한 기능 세트(자동 마이그레이션, 관계 처리, 간편한 로딩, 트랜잭션, 훅 등)로 인해 최소한의 원시 SQL을 사용하여 깔끔한 Go 코드베이스를 만들 수 있다는 점입니다. 그러나 이는 자체적인 패턴을 도입하고, 반사 및 인터페이스 사용으로 인한 런타임 오버헤드를 유발하여, 대규모 작업에서 성능에 영향을 줄 수 있습니다.

  • sqlc – 전통적인 ORM이 아니라 SQL 코드 생성기입니다. sqlc를 사용하면 .sql 파일에 일반 SQL 쿼리를 작성하고, 이 도구는 해당 쿼리를 실행할 수 있는 타입 안전한 Go 코드(DAO 함수)를 생성합니다. 이는 생성된 Go 함수(예: CreateUser, GetUser)를 호출하여 데이터베이스와 상호작용하고, 행을 수동으로 스캔하지 않고 강력하게 타입이 지정된 결과를 얻을 수 있습니다. sqlc는 런타임 오버헤드를 완전히 피하고, 준비된 SQL을 활용하여 database/sql을 직접 사용하는 것만큼 빠르게 작동한다는 점에서 간단함과 성능으로 인해 인기를 끌고 있습니다. 이의 대가로, 쿼리에 대한 SQL 문을 작성하고 유지보수해야 하며, ORM이 SQL을 자동으로 생성해 주는 것보다 동적 쿼리 논리가 덜 직관적일 수 있습니다.

  • Ent (Entgo)코드 중심의 ORM으로, 코드 생성을 통해 데이터 모델에 대한 타입 안전한 API를 생성합니다. Go에서 스키마를 정의(Ent의 DSL을 사용하여 필드, 엣지/관계, 제약 조건 등)하고, Ent는 모델과 쿼리 메서드에 대한 Go 패키지를 생성합니다. 생성된 코드는 쿼리를 구성하는 데 사용되는 유창한 API를 사용하며, 컴파일 시 타입 검사를 수행합니다. Ent는 비교적 새롭고, Linux Foundation의 후원과 Ariga의 개발로 인해 인기를 끌고 있습니다. 개발자 경험과 정확성에 집중하며, 쿼리는 원시 문자열 대신 체인 가능한 메서드를 통해 구성되므로, 더 쉽게 조합되고 오류가 적습니다. Ent의 접근 방식은 고도로 최적화된 SQL 쿼리를 생성하고, 쿼리 결과를 캐시하여 데이터베이스에 대한 중복 요청을 줄이는 데 유리합니다. 확장성을 고려하여 설계되었으며, 복잡한 스키마와 관계를 효율적으로 처리합니다. 그러나 Ent를 사용하면 코드 생성(코드젠)을 실행하는 빌드 단계가 필요하며, 이 생태계에 묶이게 됩니다. 현재 PostgreSQL, MySQL, SQLite(그 외 데이터베이스에 대한 실험적 지원 포함)를 지원하지만, GORM은 SQL Server 및 ClickHouse를 포함한 더 넓은 범위의 데이터베이스를 지원합니다.

  • Bun“SQL 중심” 접근 방식을 가진 새로운 ORM입니다. Bun은 Go의 database/sql과 오래된 go-pg 라이브러리에서 영감을 받아, 가볍고 빠르며 PostgreSQL 기능에 중점을 두고 설계되었습니다. Active Record 패턴 대신, Bun은 유창한 쿼리 빌더를 제공합니다. db.NewSelect() / NewInsert() 등을 사용하여 쿼리를 시작하고, SQL에 가까운 메서드를 사용하여 쿼리를 구성할 수 있습니다. GORM과 유사한 구조 태그를 사용하여 쿼리 결과를 Go 구조체에 매핑합니다. Bun은 명확성을 중시하며, 필요할 때 원시 SQL로 쉽게 전환할 수 있습니다. Bun은 자동 마이그레이션을 실행하지 않으며, 마이그레이션을 직접 작성하거나 Bun의 migrate 패키지를 수동으로 사용해야 합니다. 이는 일부 개발자에게 제어를 위한 장점으로 여겨집니다. Bun은 복잡한 쿼리를 우아하게 처리하고, 고급 Postgres 타입(배열, JSON 등)을 기본적으로 지원합니다. 실제로, Bun은 GORM보다 훨씬 적은 런타임 오버헤드를 강요하며, 고부하 시나리오에서 더 높은 처리량을 제공합니다. 커뮤니티는 약 4,000개의 스타를 받았으며, 기능 세트는 견고하지만 GORM만큼 광범위하지 않습니다. Bun은 효과적으로 사용하려면 SQL 지식이 어느 정도 필요하지만, 이는 성능과 유연성으로 보답합니다.

성능 벤치마크

성능(쿼리 지연 시간 및 처리량) 측면에서, 이러한 도구들의 행동에는 특히 부하가 증가할수록 큰 차이가 있습니다. 최근 벤치마크와 사용자 경험은 몇 가지 주요 포인트를 강조합니다:

  • GORM: GORM의 편리함은 성능 희생을 동반합니다. 간단한 작업이나 작은 결과 집합에 대해서는 GORM이 매우 빠를 수 있습니다. 사실, 하나의 벤치마크에서는 GORM이 매우 작은 쿼리(예: 1개 또는 10개의 레코드를 가져오기)에 대해 가장 빠른 실행 시간을 보였습니다. 그러나 레코드 수가 증가할수록 GORM의 오버헤드로 인해 매우 뒤처질 수 있습니다. 15,000개의 행을 가져오는 테스트에서 GORM은 sqlc 또는 raw database/sql보다 약 2배 느렸습니다(59.3ms의 GORM 대 31.7ms의 sqlc 및 32.0ms의 database/sql). 반사 중심의 설계와 쿼리 빌딩 추상화는 지연 시간을 증가시키며, GORM은 복잡한 로드(예: Preload를 기본값으로 사용할 경우 관련 엔티티당 하나의 쿼리)로 인해 여러 쿼리를 실행할 수 있습니다. 요약: GORM은 일반적인 작업량에서는 충분하지만, 고 처리량 시나리오나 대규모 일괄 작업에서는 오버헤드가 병목 현상이 될 수 있습니다.

  • sqlc: sqlc의 호출은 사실상 수작업으로 작성된 SQL이기 때문에, 표준 라이브러리 사용 시 성능이 비슷합니다. 벤치마크는 sqlc가 가장 빠른 옵션 중 하나로 일관되게 나타납니다. 비교 가능한 작업에서 raw database/sql보다 약간 느리거나 심지어 약간 빠를 수 있습니다. 예를 들어, 10,000개 이상의 레코드에서 sqlc는 plain database/sql보다 처리량에서 약간 우위를 차지했습니다(스캐닝의 보일러플레이트를 피하고 효율적인 코드젠을 사용하기 때문). sqlc에는 런타임 쿼리 빌더가 없으며, 쿼리는 준비된 문으로 실행되며 최소한의 오버헤드가 있습니다. 핵심은 이미 최적화된 SQL 쿼리를 작성했기 때문에, sqlc는 행을 Go 타입으로 스캔하는 작업을 피하게 됩니다. 실제로, 이는 우수한 확장성을 의미합니다. sqlc는 대규모 데이터 로드를 raw SQL만큼 잘 처리할 수 있기 때문에, raw 속도가 중요한 경우 최고의 선택입니다.

  • Ent: Ent의 성능은 raw-SQL 접근 방식과 전통적인 ORM 사이에 위치합니다. Ent는 타입 안전한 코드를 생성하기 때문에, 반사와 대부분의 런타임 쿼리 구성 비용을 피합니다. 생성하는 SQL은 매우 최적화되어 있으며, Ent API를 통해 효율적인 쿼리를 작성할 수 있습니다. Ent는 일부 경우에 쿼리 실행 계획/결과를 재사용할 수 있는 내부 캐싱 레이어를 포함합니다. 이는 복잡한 워크플로우에서 반복적인 데이터베이스 호출을 줄일 수 있습니다. Ent와 다른 도구의 구체적인 벤치마크는 다를 수 있지만, 많은 개발자들이 Ent가 동등한 작업에서 GORM보다 우수하다고 보고합니다, 특히 복잡성이 증가할수록. 그 이유 중 하나는 GORM이 최적화되지 않은 쿼리를 생성할 수 있기 때문입니다(예: 조인/프리로드를 신중하게 사용하지 않을 경우 N+1 선택). 반면, Ent는 관계의 명시적인 간편한 로딩을 장려하며, 더 적은 쿼리로 데이터를 조인합니다. Ent는 또한 GORM보다 작업당 메모리 할당이 적기 때문에, Go의 가비지 컬렉터에 대한 압박을 줄여 처리량을 향상시킬 수 있습니다. 전체적으로, Ent는 고성능과 대규모 스키마를 위해 설계되었으며, 오버헤드가 낮고 복잡한 쿼리를 효율적으로 처리할 수 있습니다. 그러나 모든 시나리오에서 수작업 SQL(sqlc)의 원시 처리량과 동일하게는 않을 수 있습니다. SQL을 직접 작성하는 것이 필요한 경우, Ent는 ORM 계층의 안전성과 속도를 모두 갖춘 강력한 후보입니다.

  • Bun: Bun은 성능을 고려하여 설계되었으며, 종종 GORM보다 더 빠른 대안으로 언급됩니다. SQL 쿼리를 생성하는 데 유창한 API를 사용하지만, 이 빌더는 가볍습니다. Bun은 SQL을 숨기지 않으며, database/sql 위에 얇은 레이어로, Go 표준 라이브러리가 수행하는 것 이상의 오버헤드가 없습니다. 대규모 프로젝트에서 GORM으로 Bun으로 전환한 사용자들은 큰 성능 개선을 경험했습니다. 예를 들어, 한 보고서에서는 GORM이 그들의 규모에서 “매우 느리다"고 언급했으며, Bun으로 교체하고 일부 원시 SQL을 사용함으로써 성능 문제를 해결했습니다. go-orm-benchmarks와 같은 벤치마크에서 Bun은 대부분의 작업에서 속도면에서 상위권에 있으며, raw sqlx/sql과 거의 1.5배 차이를 보이며, GORM보다 처리량에서 우위를 차지합니다. 또한 배치 삽입, 조인 대 대체 쿼리의 수동 제어, 기타 최적화 등 개발자가 활용할 수 있는 기능을 지원합니다. 결론: Bun은 베어메탈에 가까운 성능을 제공하며, ORM의 편리함을 원하면서 속도를 포기할 수 없는 경우에 좋은 선택입니다. SQL 중심의 성격은 생성된 쿼리가 최적화되지 않은 경우에도 쿼리를 최적화할 수 있도록 보장합니다.

성능 요약: 최대 성능이 목표인 경우(예: 데이터 집약적인 애플리케이션, 고부하 상태의 마이크로서비스), sqlc 또는 심지어 수작업 database/sql 호출이 우승합니다. 이들은 거의 추상화 비용이 없으며 선형적으로 확장됩니다. EntBun도 매우 잘 작동하며 복잡한 쿼리를 효율적으로 처리할 수 있습니다. 이들은 속도와 추상화 사이의 균형을 맞춥니다. GORM은 기능이 가장 풍부하지만, 오버헤드가 있으며, 많은 애플리케이션에서는 완전히 수용 가능하지만, 대규모 데이터량을 처리하거나 초저지연이 필요한 경우, 주의해야 합니다. (이러한 경우, Joins를 사용하여 Preload 대신 쿼리 수를 줄이는 것 또는 중요한 경로에 원시 SQL을 섞는 것 등으로 GORM의 비용을 완화할 수 있지만, 이는 복잡성을 증가시킵니다.)

개발자 경험 및 사용 편의성

개발자 경험은 주관적이지만, 학습 곡선, API의 명확성, 일상적인 코딩에서 얼마나 생산적인지를 포함합니다. 여기에 우리의 네 후보가 사용 편의성 및 개발 워크플로우 측면에서 어떻게 비교되는지 살펴보겠습니다:

  • GORM – 기능이 풍부하지만 학습 곡선이 존재: GORM은 종종 Go 초보자가 처음 시도하는 ORM이며, 완전히 자동화된 경험을 약속하기 때문입니다(구조체를 정의하고 SQL을 작성하지 않고 즉시 Create/Find할 수 있음). 문서는 매우 포괄적이며, 많은 가이드가 있어 도움이 됩니다. 그러나 GORM은 자체적인 방식으로 DB와 상호작용하는 방식(“코드 기반 접근”)을 가지고 있으며, 원시 SQL에 익숙한 경우 처음에는 약간 불편하게 느껴질 수 있습니다. 많은 일반적인 작업은 직관적입니다(예: db.Find(&objs)), 그러나 연관성, 다형적 관계, 고급 쿼리 등으로 이동할 경우, GORM의 관습(구조체 태그, PreloadJoins 등)을 배워야 합니다. 이는 왜 종종 경사가 있는 초기 학습 곡선에 대한 언급이 있는지입니다.
    곡선을 오르면 GORM은 매우 생산적이며, 반복적인 SQL을 작성하는 시간을 줄이고 Go 로직에 더 많은 시간을 할애할 수 있습니다. 사실, 마스터하면 GORM으로 기능을 개발하는 것이 더 빠를 수 있다고 많은 사람들이 느낍니다.
    간단히 말해, GORM의 DX: 초기 복잡성이 높지만 경험을 통해 매끄럽게 흘러갑니다. 대규모 커뮤니티는 많은 예제와 StackOverflow 답변을 제공하며, 풍부한 플러그인 생태계는 작업을 더 간단하게 만들 수 있습니다(예: 소프트 삭제, 감사 등에 대한 플러그인). 주의할 점은 GORM이 내부에서 무엇을 수행하는지 인식해야 하며, 실수를 피하기 위해(예: 사고된 N+1 쿼리) 주의해야 합니다. 그러나 GORM에 시간을 투자한 개발자에게는 실제로 “강력하고 통합된 솔루션"처럼 느껴질 수 있습니다.

  • Ent – 스키마 중심 및 타입 안전: Ent는 ORM의 개발자 경험을 개선하기 위해 명확히 설계되었습니다. 스키마를 한 곳에서(Go 코드) 정의하고, 강력하게 타입이 지정된 API를 사용할 수 있습니다. 이는 문자열 기반 쿼리가 없으며, 많은 오류가 컴파일 시에 포착됩니다. 예를 들어, 존재하지 않는 필드나 엣지에 참조하려 시도하면 코드가 단순히 컴파일되지 않습니다. 이 안전망은 대규모 프로젝트에 대해 큰 DX 이점입니다. Ent의 쿼리 생성 API는 유창하고 직관적이며, .Where(user.EmailEQ("alice@example.com"))와 같은 메서드를 체인하여 거의 영어처럼 읽을 수 있습니다. 다른 언어의 ORM에서 온 개발자들은 Ent의 접근 방식이 자연스럽다고 느낄 수 있습니다(이것은 Entity Framework 또는 Prisma와 유사하지만 Go에서). Ent를 학습하려면 그의 코드 생성 워크플로우를 이해해야 합니다. 스키마를 수정할 때마다 entc generate(또는 go generate)를 실행해야 합니다. 이는 추가 단계이지만, 일반적으로 빌드 프로세스의 일부입니다. Ent의 학습 곡선은 중간 수준입니다. Go와 SQL의 기본 지식을 알고 있다면, Ent의 개념(필드, 엣지 등)은 직관적입니다. 사실, 많은 사람들이 GORM보다 Ent를 더 쉽게 이해한다고 느끼며, SQL이 생성되는 것을 걱정할 필요가 없기 때문입니다. 메서드 이름에서 예측할 수 있으며, 필요할 경우 쿼리를 출력하여 디버깅할 수 있습니다. Ent의 문서와 예제는 매우 좋으며, 회사의 후원으로 인해 적극적으로 유지보수가 이루어집니다. Ent의 전체 DX: 명확성과 안전성을 원하는 사람들에게 매우 친절합니다. 작성하는 코드는 원시 SQL보다는 더 길지만, 자가 설명이 가능합니다. 또한, 리팩토링은 더 안전합니다(스키마에서 필드를 이름 변경하고 재생성하면 모든 쿼리 사용이 업데이트됩니다). 단점은 Ent 코드 생성이 제공하는 것에 어느 정도 제약을 받을 수 있다는 점입니다. 매우 맞춤형 쿼리가 필요할 경우, Ent API로 작성해야 하며(복잡한 조인을 처리할 수 있지만, 때로는 원시 SQL로 작성하는 것이 더 쉬울 수 있음). Ent는 필요할 경우 원시 SQL 스니펫을 허용하지만, 자주 그렇게 하게 되면 ORM이 적합한지 의심하게 될 수 있습니다. 요약하자면, Ent는 코드 생성 단계를 실행하고 Ent가 강제하는 패턴을 따르는 것에 문제가 없다면 대부분의 CRUD 및 쿼리 논리에 대해 부드러운 개발자 경험을 제공합니다.

  • Bun – SQL에 더 가까우며, 마법이 적음: Bun을 사용하는 것은 GORM이나 Ent를 사용하는 것과 다릅니다. Bun의 철학은 SQL을 숨기지 않기입니다. SQL을 아시면, Bun을 사용하는 방법의 대부분을 이미 아시고 있습니다. 예를 들어, 사용자 쿼리를 위해 다음과 같이 작성할 수 있습니다: db.NewSelect().Model(&users).Where("name = ?", name).Scan(ctx). 이는 유창한 API이지만, 실제 SELECT 문의 구조에 매우 가까운 것입니다. Bun의 학습 곡선은 일반적으로 낮습니다. SQL 자체에 익숙하다면.
    학습해야 할 “ORM 마법"은 적으며, 쿼리를 구성하는 메서드 이름과 구조 태그의 관습을 배우는 것이 주요합니다. 이는 database/sql이나 sqlx와 같은 다른 쿼리 빌더를 사용해온 경험이 있는 개발자에게 매우 접근성이 좋습니다. 반면, SQL 작성을 자신 있게 하지 못하는 초보자는 GORM보다 Bun이 덜 도움이 될 수 있습니다. Bun은 관계를 자동으로 생성해 주지 않으며, 이를 명시적으로 지정해야 합니다. 그러나 Bun은 관계 및 간편한 로딩(예: Relation() 메서드로 테이블을 조인)을 지원합니다. 이는 명시적으로 수행되며, 일부 개발자는 이 명확성에 선호합니다. Bun의 개발자 경험가볍고 예측 가능하다고 설명할 수 있습니다. GORM보다 일부 작업에서 약간 더 많은 코드를 작성해야 하지만, 이는 더 많은 제어와 가시성을 제공합니다. 내부 마법은 최소이며, 디버깅이 더 쉽습니다(일반적으로 Bun이 생성한 쿼리 문자열을 로깅할 수 있습니다). Bun의 문서(예: uptrace.dev 가이드)는 철저하며, 마이그레이션, 트랜잭션 등에 대한 패턴을 포함하지만, 커뮤니티가 작기 때문에 제3자 튜토리얼은 적습니다. DX의 또 다른 측면은 제공되는 도구입니다. Bun이 database/sql의 확장이기 때문에, sql.DB와 호환되는 도구(예: 디버깅 프록시, 쿼리 로거)는 Bun과 쉽게 작동합니다. 요약, Bun은 단순하고 직관적인 경험을 제공합니다. 이는 제어와 성능을 중시하는 개발자에게 좋으며, GORM만큼 초보자에게 많은 손을 잡아주는 것은 아닙니다. 합의는 Bun이 간단한 작업은 쉽게, 복잡한 작업은 가능하게 만든다는 것입니다(원시 SQL과 유사하게). 이는 당신에게 많은 프레임워크를 강요하지 않습니다.

  • sqlc – SQL을 작성하고 Go 코드를 얻는다: sqlc의 접근 방식은 일반적인 ORM의 서사와 반대됩니다. SQL을 작성하고 Go 코드를 얻는 것이 아니라, Go 코드를 작성하여 SQL을 생성하는 것이 일반적인 ORM의 방식입니다. SQL을 사랑하는 개발자들에게 이는 훌륭한 점입니다. SQL의 모든 힘(복잡한 조인, CTE, 윈도우 함수 등)을 사용할 수 있으며, ORM의 제한이 전혀 없습니다. sqlc 자체의 학습 곡선은 매우 작습니다. SELECT/INSERT/UPDATE를 SQL에서 작성하는 방법을 아시면, 어려운 부분은 이미 완료되었습니다. -- name: Name :one/many/exec 주석으로 쿼리를 주석하고, config 파일을 설정해야 하지만, 이는 간단한 작업입니다. 생성된 코드는 직관적인 함수 정의이며, 이는 일반적인 Go 함수처럼 호출됩니다. 개발자 경험의 장점: ORM API를 배우지 않아도 되며, 놀라움이 없습니다. 쿼리는 작성한 대로 실행됩니다. ORM이 특정 JOIN을 생성하는 이유를 파악하거나 쿼리를 조정하는 것과 같은 전체 클래스의 ORM 문제를 피할 수 있습니다. 또한, 코드 리뷰에서는 SQL 자체를 검토할 수 있으며, 복잡한 논리에 대해 더 명확합니다. 또 다른 큰 DX 이점은 타입 안전성 및 IDE 지원입니다. 생성된 메서드와 구조체는 편집기에서 이동할 수 있으며, 리팩토링 도구가 작동합니다. 반면, 원시 문자열 쿼리는 IDE에 불투명합니다. 단점은 sqlc가 SQL 스크립트를 관리하도록 요구한다는 점입니다. 이는 스키마나 요구사항이 변경되면, 관련 SQL을 수동으로 업데이트하거나 추가하고 codegen을 다시 실행해야 합니다. 이는 어렵지 않지만, ORM 메서드를 호출하는 것보다 더 수동적인 작업입니다. 또한, 동적 쿼리(SQL의 일부가 조건에 따라 달라지는 경우)는 번거로울 수 있습니다. 여러 SQL 변형을 작성하거나 SQL 문법의 트릭을 사용해야 합니다. 일부 개발자는 이가 sqlc 접근 방식의 한계라고 언급합니다. 실제로, 데이터 액세스를 구조화하여 지나치게 동적인 SQL이 필요하지 않도록 할 수 있으며, 또는 이러한 경계 사례에 대해 raw database/sql을 호출할 수 있습니다. 그러나 고려할 점은 sqlc가 정의된 쿼리에 대해 매우 우수하지만, 임의의 쿼리 구축에는 적합하지 않을 수 있다는 점입니다. 요약, SQL에 숙련된 개발자에게는 sqlc 사용이 자연스럽고 매우 효율적입니다. 배워야 할 것이 거의 없으며, 반복적인 Go 보일러플레이트를 제거합니다. SQL에 익숙하지 않은 개발자에게는 초기에는 ORM이 기본 CRUD 작업을 자동으로 생성하는 것보다 느리게 느껴질 수 있지만, 많은 Go 개발자는 sqlc가 필요하다고 생각합니다. 이는 수동 제어와 높은 안전성, 런타임 비용 없이 sweet spot을 제공하기 때문입니다.

인기도와 생태계 지원

채택 및 커뮤니티 지원은 선택에 영향을 줄 수 있습니다. 인기 있는 라이브러리는 커뮤니티 기여가 많고, 유지보수가 잘 되며, 학습 자료가 풍부합니다.

  • GORM: 네 가지 중 가장 오래되고 성숙한 GORM은 훨씬 더 큰 사용자 기반과 생태계를 가지고 있습니다. 현재 GitHub에서 최고로 많은 별표를 받은 Go ORM(38,000개 이상의 별표)이며, 수많은 프로덕션 프로젝트에서 사용되고 있습니다. 유지자들은 활발하며, 프로젝트는 정기적으로 업데이트되고 있습니다(GORM v2는 성능과 아키텍처를 개선하는 주요 개편). GORM의 인기의 큰 장점은 풍부한 확장 및 통합입니다. 데이터베이스 드라이버(예: PostgreSQL, MySQL, SQLite, SQL Server, ClickHouse) 등에 대한 공식 및 제3자 플러그인을 제공합니다. GORM은 넓은 범위의 사용 사례를 지원합니다: 마이그레이션, 스키마 자동 생성, 소프트 삭제, JSON 필드(gorm.io/datatypes), 전체 텍스트 검색 등, 내장 기능 또는 추가 기능을 통해 제공됩니다. 커뮤니티는 gormt(기존 데이터베이스에서 구조 정의를 생성하는 데 사용)와 같은 다양한 도구, 그리고 많은 튜토리얼과 예제를 제공합니다. 문제가 발생하면, 빠른 검색으로 다른 사람이 질문한 이슈나 Stack Overflow 질문을 찾을 가능성이 높습니다. 생태계 요약: GORM은 매우 잘 지원되고 있습니다. 많은 사람들은 GORM을 “기본” ORM 선택으로 여기며, 이는 프레임워크 및 보일러플레이트에서 찾을 수 있습니다. 단점은 그 크기가 너무 커서 무겁게 느껴질 수 있지만, 커뮤니티의 깊이를 고려하면 많은 사람에게는 그 희생이 가치가 있습니다.

  • Ent: Ent는 2019년에 오픈소스화된 이후로 커뮤니티가 성장하고 있습니다. 약 16,000개의 별표와 Linux Foundation의 후원을 받고 있으므로, 주변에 있는 프로젝트가 아닙니다. 대규모 회사들이 Ent의 스키마 중심의 장점을 활용하여 사용하고 있으며, 유지자(Ariga)는 업무에 중요한 사용 사례에 대한 기업 지원을 제공하고 있습니다. Ent 주변의 생태계는 성장하고 있습니다: OpenAPI/GraphQL 통합, gRPC 통합, Ent 스키마와 함께 작동하는 SQL 마이그레이션 도구 등에 대한 Ent 확장(ent/go)이 있습니다. Ent는 코드를 생성하므로, 일부 생태계 패턴이 다릅니다. 예를 들어, GraphQL과 통합하려면 Ent의 코드 생성을 사용하여 GraphQL 리졸버를 생성할 수 있습니다. Ent의 학습 자료는 좋습니다(공식 문서와 간단한 앱을 다루는 예제 프로젝트). 커뮤니티 포럼과 GitHub 토론은 스키마 설계 질문과 팁에 활발하게 참여하고 있습니다. 커뮤니티 지원 측면에서 Ent는 “초기 채택자” 단계를 넘어섰으며, 생산성과 신뢰성이 있는 것으로 간주됩니다. GORM만큼의 Stack Overflow 답변은 아직 없지만, 빠르게 따라잡고 있습니다. 주의할 점은 Ent가 약간 더 의견이 있는 점입니다(예: 스키마를 관리하고 싶어 함), 따라서 다른 ORM과 함께 사용하기보다는 독립적으로 사용할 가능성이 높습니다. GORM처럼 “플러그인"은 제공하지 않지만, 사용자 정의 템플릿을 작성하여 코드 생성을 확장하거나 생성된 클라이언트에 생명주기 이벤트에 연결할 수 있습니다(Ent는 생성된 클라이언트에 대한 훅/미들웨어 지원을 제공합니다). 기초의 후원은 장기적인 지원을 나타내므로, Ent의 모델이 필요하다면 Ent를 선택하는 것은 안전한 선택입니다.

  • Bun: Bun(Uptrace 오픈소스 패키지의 일부)은 go-pg 라이브러리의 팬들이 증가하고 있는 것으로 인해 인기를 얻고 있습니다. 약 4,300개의 별표를 받았으며, 이 비교에서 가장 작은 커뮤니티를 가지고 있지만, 매우 활발한 프로젝트입니다. 유지자는 반응이 빠르고, 기능을 빠르게 추가하고 있습니다. Bun의 커뮤니티는 성능에 대한 열정이 있습니다. Go 포럼과 Reddit에서 속도를 위해 Bun으로 전환한 개발자들의 토론을 찾을 수 있습니다. 그러나 사용자 기반이 작기 때문에, 특정 질문에 대한 답변을 쉽게 찾을 수는 없으며, 때로는 문서/소스를 읽거나 Bun의 GitHub 또는 Discord(Uptrace는 커뮤니티 채팅을 제공)에서 질문해야 할 수도 있습니다. 확장 생태계는 제한적이지만, Bun은 자체 마이그레이션 라이브러리, 펙스 로더, Uptrace의 관찰 도구와 통합되어 있습니다. GORM이 제공하는 풍부한 플러그인은 없습니다. 그러나 Bun은 sql.DB 사용과 호환되므로, 혼합 사용이 가능합니다(예: github.com/jackc/pgx를 하위 드라이버로 사용하거나 다른 *sql.DB 기대 패키지와 통합). Bun은 사용자에게 구속하지 않습니다. 지원 측면에서, 새로운 프로젝트라는 점은 문서가 최신이고 예제가 현대적이며(종종 컨텍스트와 함께 사용법을 보여줍니다) 유리합니다. 공식 문서는 Bun을 GORM과 Ent와 직접 비교하여 설명하고 있습니다. 이는 도움이 됩니다. 커뮤니티 크기가 걱정된다면, Bun의 장점을 활용하면서도 사용을 상대적으로 얕게 유지하는 전략을 고려할 수 있습니다(예: 필요하다면 다른 솔루션으로 교체할 수 있는 경우). Bun의 전망은 상승하고 있으며, 특정 니치(성능 중심 ORM)를 채우고 있어 지속력을 가집니다.

  • sqlc: sqlc는 Go 커뮤니티에서 매우 인기 있으며, 약 15,900개의 별표와 성능에 민감한 커뮤니티에서 특히 많은 지지자들이 있습니다. “ORM 회피"에 대한 토론에서 종종 추천되는 이유는 균형 잡힌 절충점을 제공하기 때문입니다. 도구는 기여자들(원저자 포함, 그는 개선에 적극적으로 참여하고 있습니다)에 의해 유지됩니다. 런타임 라이브러리보다는 컴파일러에 더 가까운 sqlc의 생태계는 통합에 중심을 두고 있습니다: 예를 들어, 편집기/IDE는 .sql 파일에 대한 구문 강조를 제공하고, sqlc generate를 빌드 또는 CI 파이프라인의 일부로 실행합니다. 커뮤니티는 sqlc와 함께 코드를 구성하는 방법에 대한 템플릿과 기초 레포 예제를 생성했습니다(종종 마이그레이션 도구인 Flyway 또는 Golang-Migrate와 함께 스키마 버전 관리를 위해 사용, sqlc 자체는 스키마 관리를 하지 않습니다). sqlc에 대한 공식 Slack/Discord가 있으며, 여기서 질문을 할 수 있고, GitHub의 이슈는 주목을 받습니다. 일반적인 패턴(예: nullable 값 처리, JSON 필드)은 sqlc 문서에 문서화되어 있으며, 커뮤니티 블로그 게시물도 있습니다. 강조할 점은 sqlc는 Go에 특화되지 않음입니다. TypeScript, Python 등 다른 언어에서도 코드를 생성할 수 있습니다. 이는 Go 커뮤니티를 넘어 확장되지만, Go에서는 널리 존중받고 있습니다. sqlc를 선택하면 좋은 동료들과 함께 있습니다: 많은 스타트업과 대규모 회사에서 sqlc를 프로덕션에 사용하고 있습니다(커뮤니티 전시 및 레포에 나열된 후원자들에 따름). sqlc의 주요 생태계 고려사항은 sqlc가 ORM과 같은 런타임 기능을 제공하지 않기 때문에, 트랜잭션과 같은 기능을 위해 다른 라이브러리를 가져야 할 수 있습니다(하지만 sql.Tx를 쉽게 사용할 수 있습니다). 또는 가벼운 DAL 래퍼를 사용할 수 있습니다. 실제로, 대부분은 sqlc와 표준 라이브러리와 함께 사용합니다(트랜잭션, 컨텍스트 종료 등은 코드에서 직접 database/sql의 관용구를 사용합니다). 이는 벤더 잠금 해제가 적습니다. sqlc에서 벗어나는 것은 단지 자신의 데이터 레이어를 작성하는 것일 뿐, 이는 이미 작성한 SQL과 동일한 난이도입니다. 전체적으로, sqlc의 커뮤니티 및 지원은 견고하며, 많은 사람들이 SQL 데이터베이스와 상호작용하는 Go 프로젝트에 대해 “반드시 사용해야 할” 도구로 추천합니다. 그 이유는 간단함과 신뢰성 때문입니다.

기능 세트 및 확장성

이 도구들은 각각 다른 기능 세트를 제공합니다. 여기서 그들의 능력과 고급 사용 사례에 대한 확장성을 비교합니다:

  • GORM 기능: GORM은 전체 서비스 ORM이 되려는 목표를 가지고 있습니다. 제공 기능 목록은 매우 광범위합니다:
  • 다중 데이터베이스: PostgreSQL, MySQL, SQLite, SQL Server 및 기타에 대한 일등급 지원. DB 전환은 일반적으로 연결 드라이버를 변경하는 것만큼 간단합니다.
  • 마이그레이션: 모델에서 스키마를 자동으로 마이그레이션할 수 있습니다(db.AutoMigrate(&User{})users 테이블을 생성하거나 수정합니다). 편리하지만, 프로덕션에서 자동 마이그레이션을 사용하는 것은 주의가 필요합니다. 많은 사람들이 개발 환경에서 사용하고 있으며, 프로덕션에서는 더 제어된 마이그레이션을 사용합니다.
  • 관계: GORM의 구조 태그(gorm:"foreignKey:...,references:...")를 사용하여 일대다, 다대다 등 관계를 정의할 수 있습니다. 다대다 관계의 연결 테이블을 처리할 수 있으며, Preload를 사용하여 관계를 즉시 로드할 수 있습니다. GORM은 기본적으로 지연 로딩(즉, 별도의 쿼리)을 사용하지만, Preload 또는 Joins를 사용하여 이를 커스터마이징할 수 있습니다. 새 버전에는 연관 쿼리에 대한 더 쉬운 API를 제공하는 제네릭 기반 API도 포함되어 있습니다.
  • 트랜잭션: GORM은 사용하기 쉬운 트랜잭션 래퍼(db.Transaction(func(tx *gorm.DB) error { ... }))를 제공하며, 중첩 트랜잭션(세이브포인트)도 지원합니다.
  • 훅 및 콜백: 모델 구조체에 BeforeCreate, AfterUpdate와 같은 메서드를 정의하거나, GORM이 특정 생명주기 이벤트에서 호출할 전역 콜백을 등록할 수 있습니다. 이는 자동으로 타임스탬프를 설정하거나 소프트 삭제 동작을 수행하는 데 매우 유용합니다.
  • 확장성: GORM의 플러그인 시스템(gorm.Plugin 인터페이스)은 기능을 확장할 수 있습니다. 예: gorm-gen 패키지는 컴파일 시간 쿼리 확인을 선호하는 경우 유형 안전한 쿼리 메서드를 생성하거나, 감사, 다중 테넌트 등 커뮤니티 플러그인을 제공합니다. 필요할 때마다 db.Raw("SELECT ...", params).Scan(&result)를 통해 원시 SQL로 돌아갈 수도 있습니다.
  • 기타 편의 기능: GORM은 복합 기본 키, 모델 임베딩, 다형성 관계, 여러 데이터베이스를 사용하는 스키마 리졸버(읽기 복제, 셰딩 등)를 지원합니다.

종합적으로, GORM은 매우 확장 가능하고 기능이 풍부한 ORM입니다. ORM 관련 기능 중 거의 모든 것을 GORM에서 내장 메커니즘 또는 문서화된 패턴으로 제공합니다. 이 광범위한 기능의 비용은 복잡성과 일부 경직성(종종 GORM의 방식에 맞춰야 함)입니다. 하지만 하나의 종합 솔루션이 필요한 경우, GORM이 제공합니다.

  • Ent 기능: Ent의 철학은 스키마를 코드로 중심에 두고 있습니다. 주요 기능은 다음과 같습니다:
  • 스키마 정의: 필드에 제약 조건(유니크, 기본값, 열거 등)과, 카디널리티(일대다 등)를 가진 엣지(관계)를 정의할 수 있습니다. Ent는 이를 사용하여 코드를 생성하고, 현재 스키마와 원하는 스키마 간의 차이 SQL을 생성할 수도 있습니다(ent/migrate 컴포넌트가 이를 제공합니다).
  • 타입 안전 및 검증: 필드가 강하게 타입화되어 있기 때문에, 예를 들어, 정수 필드에 문자열을 실수로 설정하는 것처럼 할 수 없습니다. Ent는 또한 사용자 정의 필드 타입(예: sql.Scanner/driver.Valuer를 사용하여 JSONB 필드 또는 기타 복잡한 타입과 통합)을 허용합니다.
  • 쿼리 빌더: Ent의 생성된 쿼리 API는 대부분의 SQL 구조를 커버합니다: 선택, 필터, 정렬, 제한, 집계, 엣지 간 조인, 하위 쿼리 등이 가능합니다. 표현력이 높습니다. 예를 들어, client.User.Query().WithOrders(func(q *ent.OrderQuery) { q.Limit(5) }).Where(user.StatusEQ(user.StatusActive)).All(ctx)를 사용하여 활성 사용자와 각 사용자의 처음 5개 주문을 한 번에 가져올 수 있습니다.
  • 트랜잭션: Ent 클라이언트는 트랜잭션을 지원합니다. 클라이언트의 트랜잭셔널 버전을 노출하여(tx, err := client.Tx(ctx)ent.Tx를 얻을 수 있으며, 여러 작업을 수행한 후 커밋하거나 롤백할 수 있습니다.
  • 훅 및 미들웨어: Ent는 생성/업데이트/삭제 작업에 대한 훅을 등록할 수 있습니다. 이는 예를 들어, 자동으로 필드를 채우거나 사용자 정의 규칙을 강제하는 인터셉터처럼 작동합니다. 클라이언트에 대한 미들웨어도 제공되어 작업을 래핑할 수 있습니다(로그, 모니터링 등에 유용).
  • 확장성: Ent는 “플러그인"이 없지만, 코드 생성은 템플릿화되어 있으며, 필요하다면 생성된 코드를 확장하기 위해 사용자 정의 템플릿을 작성할 수 있습니다. 많은 고급 기능은 이 방식으로 구현되었습니다: 예를 들어, OpenTelemetry과 통합하여 DB 호출을 추적하거나, Ent 스키마에서 GraphQL 리졸버를 생성하는 등. Ent는 필요할 때 entsql 패키지나 하위 드라이버를 통해 원시 SQL을 사용할 수 있도록 허용합니다. 추가 코드 생성이 가능하므로, 팀은 Ent를 기반으로 필요하다면 자신의 패턴을 층으로 추가할 수 있습니다.
  • GraphQL/REST 통합: Ent의 그래프 기반 접근 방식의 큰 매력점은 GraphQL과 잘 어울린다는 것입니다. Ent 스키마는 거의 직접적으로 GraphQL 스키마에 매핑할 수 있습니다. 이를 자동화하는 도구가 존재합니다. 이는 API 서버를 구축할 때 생산성 향상에 도움이 될 수 있습니다.
  • 성능 최적화: Ent는 예를 들어, 관련 엣지를 일괄로 로드하여 N+1 쿼리 문제를 피할 수 있습니다(이에 대한 즉시 로딩 API .With<EdgeName>()이 있습니다). 내부적으로 JOIN 또는 추가 쿼리를 최적화된 방식으로 사용합니다. 또한, 쿼리 결과의 캐싱(메모리 내)을 활성화하여 짧은 시간 내 동일한 쿼리를 DB에 다시 요청하지 않도록 할 수 있습니다.

요약하자면, Ent의 기능 세트는 대규모, 유지보수가 가능한 프로젝트에 맞춰 설계되어 있습니다. GORM의 즉시 제공되는 기능(예: GORM의 자동 스키마 마이그레이션 대신 Ent의 생성된 마이그레이션 스크립트)이 부족할 수 있지만, 강력한 개발 도구와 타입 안전성으로 보완됩니다. Ent의 확장성은 필요한 것을 생성하는 것에 관한 것입니다. entc(코드 생성)의 작동 방식에 대해 깊이 파고들면 매우 유연합니다.

  • Bun 기능: Bun은 SQL을 잘 아는 사람들을 위한 강력한 도구입니다. 일부 기능과 확장성 포인트:
  • 쿼리 빌더: Bun의 핵심은 대부분의 SQL 구조를 지원하는 유창한 쿼리 빌더입니다(SELECT와 조인, CTE, 하위 쿼리, INSERT, UPDATE와 대량 지원). 쿼리를 시작하고 깊이 맞춤화할 수 있으며, 원시 SQL을 삽입할 수도 있습니다(.Where("some_condition (?)", value)).
  • PostgreSQL 특화 기능: Bun은 Postgres 배열 타입, JSON/JSONB( []<type> 또는 map[string]interface{} 또는 사용자 정의 타입으로 매핑)에 대한 내장 지원을 제공하며, 복합 타입으로 스캔하는 것도 지원합니다. 예를 들어, JSONB 열이 있다면 json: 태그를 사용하여 Go 구조체에 매핑할 수 있으며, Bun은 이를 위해 marshaling을 처리합니다. 이는 JSONB를 불투명하게 처리하는 일부 ORM보다 강점입니다.
  • 관계/즉시 로딩: 앞서 보았듯이, Bun은 관계를 위한 struct 태그를 정의할 수 있으며, 쿼리에서 .Relation("FieldName")을 사용하여 조인 및 관련 엔티티를 로드할 수 있습니다. 기본적으로 .Relation은 LEFT JOIN을 사용합니다(내부 조인 또는 다른 유형을 시뮬레이션하려면 조건을 추가할 수 있습니다). 이는 관련 데이터가 어떻게 가져오는지에 대한 세부적인 제어를 제공합니다(하나의 쿼리 대신 여러 개). 수동 쿼리를 선호하는 경우, Bun의 SQL 빌더에서 직접 조인을 작성할 수 있습니다. GORM과 달리, Bun은 자동으로 관계를 로드하지 않습니다. 이는 사고 없이 N+1 문제를 피하게 합니다.
  • 마이그레이션: Bun은 별도의 마이그레이션 툴킷(github.com/uptrace/bun/migrate)을 제공합니다. 마이그레이션은 Go(또는 SQL 문자열)에서 정의되고 버전화됩니다. GORM의 자동 마이그레이션만큼 자동화되지는 않지만, 라이브러리와 잘 통합되어 있습니다. 이는 스키마 변경을 명시적으로 유지하는 데 좋습니다.
  • 확장성: Bun은 database/sql의 인터페이스와 유사한 인터페이스로 구성되어 있으며, 심지어 *sql.DB를 래핑합니다. 따라서 필요하다면 하위 레벨 도구(예: *pgx.Conn 쿼리를 실행하고 Bun 모델에 스캔)를 사용할 수 있습니다. Bun은 사용자 정의 값 스캐너를 허용하므로, 복잡한 타입을 저장하려면 sql.Scanner/driver.Valuer를 구현하고 Bun이 이를 사용하도록 할 수 있습니다. Bun의 기능을 확장하려면, 상대적으로 간단하므로, 많은 사람들이 프로젝트에서 Bun의 API 위에 추가 도움 함수를 작성하는 것보다는 구분된 플러그인을 작성하지 않습니다. 라이브러리 자체는 진화하고 있으며, 유지자들이 사용자 요청에 따라 새로운 기능(추가 쿼리 도움 기능 또는 통합)을 추가하고 있습니다.
  • 기타 기능: Bun은 ctx를 모든 쿼리에 전달하는 컨텍스트 취소를 지원하며, database/sql을 통해 연결 풀을 제공합니다(구성 가능), pgx를 사용하는 경우 준비된 문 캐싱을 지원합니다. Bun은 구조체 포인터 또는 원시 슬라이스로 쉽게 스캔할 수 있는 기능도 제공합니다(예: 하나의 열을 []string에 직접 선택). 이러한 작은 편의 기능은 반복적인 스캔 코드를 싫어하는 사람들에게 Bun을 즐겁게 만듭니다.

요약하자면, Bun의 기능 세트는 ORM의 90% 요구 사항을 커버하지만, 투명성과 성능에 중점을 두고 있습니다. 모든 기능(예: 자동 스키마 업데이트 또는 액티브 레코드 패턴)이 있는 것은 아닐 수 있지만, 필요한 것을 구현할 수 있는 기본 블록을 제공합니다. 아직 어린 프로젝트이기 때문에, 기능 세트가 계속 확장될 것으로 기대할 수 있습니다. 유지자들이 사용자 요청에 따라 기능을 추가하고 있습니다.

  • sqlc 기능: sqlc의 “기능"은 생성기이기 때문에 매우 다릅니다:
  • 완전한 SQL 지원: 가장 큰 기능은 단순히 PostgreSQL 자체의 기능을 직접 사용할 수 있다는 점입니다. Postgres가 지원하면 sqlc에서 사용할 수 있습니다. 이는 CTE, 윈도우 함수, JSON 연산자, 공간 쿼리(PostGIS) 등이 포함됩니다. 라이브러리 자체가 이를 사용하기 위해 특별한 것을 구현할 필요가 없습니다.
  • 타입 매핑: sqlc는 SQL 타입을 Go 타입으로 매핑하는 데 매우 똑똑합니다. 표준 타입을 처리하고, 사용자 정의 타입 또는 열거에 대한 매핑을 구성하거나 확장할 수 있습니다. 예를 들어, Postgres의 UUIDgithub.com/google/uuid 타입으로 매핑할 수 있으며, 도메인 타입은 기본 Go 타입으로 매핑할 수 있습니다.
  • Null 처리: nullable 열에 대해 sql.NullString 또는 포인터를 생성할 수 있으며, 선호에 따라 선택할 수 있습니다. 이는 null 스캔과 싸우지 않도록 합니다.
  • 배치 작업: sqlc 자체는 고급 API를 제공하지 않지만, 대량 삽입 SQL을 작성하고 코드를 생성할 수 있습니다. 또는 저장 프로시저를 호출할 수 있습니다. 이는 또 다른 기능: SQL이기 때문에 저장 프로시저 또는 DB 함수를 활용하고, sqlc가 Go 래퍼를 생성할 수 있습니다.
  • 다중 문 쿼리: 하나의 이름 지정 쿼리에 여러 SQL 문을 넣을 수 있으며, sqlc는 드라이버가 지원하는 경우 트랜잭션에서 실행합니다. 마지막 쿼리 또는 지정한 결과를 반환합니다. 이는 예를 들어, “생성하고 나서 선택"을 하나의 호출로 수행하는 방법입니다.
  • 확장성: 컴파일러이기 때문에, 확장성은 새로운 언어를 위한 플러그인 또는 커뮤니티 기여로 새로운 SQL 구조를 지원하는 형태로 제공됩니다. 예를 들어, 새로운 PostgreSQL 데이터 타입이 출시되면 sqlc를 업데이트하여 매핑을 지원할 수 있습니다. 애플리케이션에서는 sqlc 주변에 래퍼 함수를 작성하여 확장할 수 있습니다. 생성된 코드는 사용자의 통제 하에 있으므로, 수정할 수 있지만, 일반적으로는 SQL을 수정하고 재생성하는 것이 좋습니다. 필요하다면, 항상 sqlc와 database/sql의 원시 호출을 코드베이스에서 혼합할 수 있습니다(충돌은 없으며, sqlc는 단지 일부 Go 코드를 출력합니다).
  • 최소한의 런타임 종속성: sqlc가 생성하는 코드는 일반적으로 표준 database/sql(및 특정 드라이버, 예: pgx)에만 의존합니다. 무거운 런타임 라이브러리는 없으며, 단지 몇 가지 도움 타입만 있습니다. 이는 라이브러리 측에서 프로덕션에서의 오버헤드가 없음을 의미합니다. 모든 작업은 컴파일 시간에 수행됩니다. 이는 또한 ORM에서 제공하는 객체 캐시 또는 식별자 맵과 같은 기능을 제공하지 않음을 의미합니다. 캐싱이 필요한 경우, 서비스 레이어에서 구현하거나 별도의 라이브러리를 사용해야 합니다.

결론적으로, sqlc의 “기능"은 SQL 데이터베이스와 Go 코드 사이의 갭을 줄이되, 중간에 추가 사항을 추가하지 않도록 설계되었습니다. 이는 특수화된 도구이며, 마이그레이션, 객체 상태 추적 등은 의도적으로 제공하지 않습니다. 이러한 문제는 다른 도구나 개발자에게 맡겨집니다. 이는 가벼운 데이터 레이어를 원하는 경우에 매력적이지만, 스키마부터 쿼리, 관계까지 코드에서 모든 것을 처리하는 하나의 종합 솔루션을 원하는 경우, sqlc만으로는 충분하지 않습니다. 다른 패턴 또는 도구와 함께 사용해야 합니다.

이 섹션을 요약하자면, GORM은 기능이 풍부하며, 플러그인을 통해 적응할 수 있는 성숙하고 확장 가능한 ORM으로 명확한 선택입니다. Ent는 현대적이고 타입 안전한 기능을 제공하며, 코드 생성, 훅, API 레이어 통합 등으로 정확성과 유지보수성을 우선시합니다. Bun은 기본적인 기능을 제공하며, SQL 숙련도에 의존하여, 더 많은 SQL을 작성하거나 약간의 래퍼를 작성함으로써 쉽게 확장할 수 있습니다. sqlc는 기능을 최소한으로 줄여, SQL 자체만큼 기능이 풍부하게 설계되어 있으며, 캐싱 등은 사용자가 직접 층으로 추가해야 합니다.

코드 예제: 각 ORM에서의 CRUD 작업

각 도구가 간단한 모델에 대한 기본 CRUD 작업을 어떻게 처리하는지 보는 것이 차이를 가장 잘 보여줍니다. User 모델이 ID, Name, Email 필드를 가진다고 가정해 보겠습니다. 아래는 PostgreSQL을 데이터베이스로 사용하여 각 라이브러리에서 사용자 생성, 읽기, 업데이트, 삭제에 대한 옆으로 놓은 코드 조각입니다.

참고: 모든 경우에서 데이터베이스 연결(db 또는 client)이 이미 설정되어 있다고 가정하고, 간결성을 위해 오류 처리는 생략했습니다. 목적은 각 접근 방식의 API와 복잡성을 비교하는 것입니다.

GORM (Active Record 스타일)

    // 모델 정의
    type User struct {
        ID    uint   `gorm:"primaryKey"`
        Name  string
        Email string
    }

    // 새 사용자 생성
    user := User{Name: "Alice", Email: "alice@example.com"}
    db.Create(&user)                      // INSERT INTO users (name,email) VALUES ('Alice','alice@example.com')

    // 읽기 (기본 키로 찾기)
    var u User
    db.First(&u, user.ID)                // SELECT * FROM users WHERE id = X LIMIT 1

    // 업데이트 (단일 열)
    db.Model(&u).Update("Email", "alice_new@example.com")
    // (생성: UPDATE users SET email='alice_new@example.com' WHERE id = X)

    // 삭제
    db.Delete(&User{}, u.ID)             // DELETE FROM users WHERE id = X

Ent (Codegen, fluent API)

    // (Ent 스키마는 다른 곳에서 정의되고 코드가 생성됩니다. client는 ent.Client라고 가정합니다.)

    // 새 사용자 생성
    u, err := client.User.
        Create().
        SetName("Alice").
        SetEmail("alice@example.com").
        Save(ctx)
    // SQL: INSERT INTO users (name, email) VALUES ('Alice','alice@example.com') RETURNING id, name, email

    // 읽기 (ID로)
    u2, err := client.User.Get(ctx, u.ID)
    // SQL: SELECT * FROM users WHERE id = X

    // 업데이트 (단일 필드)
    u3, err := client.User.
        UpdateOneID(u.ID).
        SetEmail("alice_new@example.com").
        Save(ctx)
    // SQL: UPDATE users SET email='alice_new@example.com' WHERE id = X RETURNING id, name, email

    // 삭제
    err = client.User.DeleteOneID(u.ID).Exec(ctx)
    // SQL: DELETE FROM users WHERE id = X

Bun

    // 모델 정의
    type User struct {
        bun.BaseModel `bun:"table:users"` 
        ID   int64    `bun:",pk,autoincrement"`
        Name string   `bun:",notnull"`
        Email string  `bun:",unique,notnull"`
    }

    // 새 사용자 생성
    user := &User{Name: "Alice", Email: "alice@example.com"}
    _, err := db.NewInsert().Model(user).Exec(ctx)
    // INSERT INTO users (name, email) VALUES ('Alice','alice@example.com')

    // 읽기 (ID로 찾기)
    var u User
    err = db.NewSelect().Model(&u).
          Where("id = ?", user.ID).
          Scan(ctx)
    // SELECT * FROM users WHERE id = X

    // 업데이트 (단일 열)
    _, err = db.NewUpdate().Model(&u).
          Set("email = ?", "alice_new@example.com").
          Where("id = ?", u.ID).
          Exec(ctx)
    // UPDATE users SET email='alice_new@example.com' WHERE id = X

    // 삭제
    _, err = db.NewDelete().Model((*User)(nil)).
          Where("id = ?", u.ID).
          Exec(ctx)
    // DELETE FROM users WHERE id = X

sqlc


    // SQL 파일에 SQL을 작성하고 sqlc가 Queries 구조체와 메서드를 생성한다고 가정합니다.
    queries := New(db)  // New는 *sql.DB (또는 pgx.Conn)를 받아 생성된 Queries를 반환합니다.

    // 새 사용자 생성 (생성된 메서드는 INSERT를 실행하고 결과를 스캔합니다)
    newUser, err := queries.CreateUser(ctx, "Alice", "alice@example.com")
    // queries.sql에서의 SQL -> INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email

    // 읽기 (ID로)
    user, err := queries.GetUser(ctx, newUser.ID)
    // SQL -> SELECT id, name, email FROM users WHERE id = $1

    // 업데이트 (ID로 이메일 변경)
    updatedUser, err := queries.UpdateUserEmail(ctx, newUser.ID, "alice_new@example.com")
    // SQL -> UPDATE users SET email=$2 WHERE id = $1 RETURNING id, name, email

    // 삭제
    err = queries.DeleteUser(ctx, newUser.ID)
    // SQL -> DELETE FROM users WHERE id = $1

위의 코드를 보면, 각 접근 방식은 다른 느낌을 줍니다:

  • GORMdb.Model(&obj) 또는 db 객체에 직접적으로 struct 메서드와 fluent chaining을 사용합니다. Createuser.ID가 설정되는 등, struct에 반환된 값을 자동으로 채웁니다. 또한, SQL 세부 사항을 숨기는 것이 설계상 목적이기 때문에 일반적으로 쿼리를 보는 것은 어렵습니다 (디버깅을 하지 않는 한).

  • Ent은 생성된 fluent API를 사용합니다. Create().SetX().Save(ctx) 또는 UpdateOneID(id).SetX().Save(ctx)와 같은 메서드는 명확하게 build와 execute 단계를 분리합니다. Ent는 ent.Type 객체(행에 해당하는 객체)와 오류를 반환하며, SQL 쿼리가 결과를 반환하거나 오류를 반환하는 방식과 유사합니다.

  • Bun은 더 명시적으로 지정해야 합니다 (예: 업데이트 시 Set("email = ?", ...) 사용). 이는 SQL을 작성하는 것과 매우 유사하지만 Go 문법을 사용합니다. 삽입 후 user struct는 RETURNING 절을 추가하지 않으면 자동으로 새 ID가 채워지지 않습니다 (Bun은 필요 시 .Returning()을 지원합니다). 위 예제는 간단하게 유지합니다.

  • sqlc는 Go 함수를 호출하는 것처럼 보입니다. queries.CreateUser 등을 호출하고, 그 뒤에서 준비된 명령문이 실행됩니다. SQL은 외부 파일에 작성되므로 Go 코드에서는 보이지 않지만, 완전한 제어가 가능합니다. 반환된 객체(예: newUser)는 sqlc가 데이터를 모델링하기 위해 생성한 일반적인 Go struct입니다.

한 가지는 명확히 볼 수 있습니다: 복잡성 차이(GORM은 매우 간결하지만, Bun과 sqlc는 코드 또는 SQL에서 더 많은 타이핑이 필요함)와 스타일 차이(Ent와 GORM은 더 높은 수준의 추상화를 제공하지만, Bun과 sqlc는 원시 쿼리에 더 가까움). 선호에 따라 간결성보다 명확성을 선호하거나 그 반대를 선택할 수 있습니다.


TL;DR

Go에서 “올바른” ORM 또는 데이터베이스 라이브러리를 선택하는 것은 애플리케이션의 요구사항과 팀의 선호도에 달려 있습니다:

  • GORM은 많은 것을 처리해 주는 시도되고 검증된 ORM이기 때문에, CRUD 애플리케이션의 빠른 개발에 적합합니다. 편리함과 풍부한 기능 세트가 성능 최적화보다 더 중요할 때 GORM은 좋은 선택입니다. 커뮤니티 지원과 문서는 훌륭하며, 학습 곡선의 날카로운 부분을 부드럽게 해 줍니다. 다만, 기능을 적절히 사용해야 하며(예: Preload 또는 Joins를 사용하여 지연 로딩의 함정을 피해야 함), 일정한 오버헤드를 기대해야 합니다. 대신, 생산성과 대부분의 문제에 대한 일괄 솔루션을 제공합니다. 여러 데이터베이스 지원이나 광범위한 플러그인 생태계가 필요하다면 GORM은 당신을 도와 줍니다.

  • Ent타입 안정성, 명확성, 유지보수성을 우선시하는 사람들에게 적합합니다. 스키마 변경이 빈번한 대규모 코드베이스에 적합하며, 컴파일러가 오류를 잡아 줍니다. Ent는 초기 설계(스키마 정의, 생성 실행)가 더 필요하지만, 프로젝트가 성장하면서 코드가 견고하고 리팩토링에 유리해집니다. 성능 측면에서는 최적화된 SQL 생성과 캐싱 덕분에, 활성 기록 ORM보다 중복된 쿼리와 복잡한 쿼리 처리에 효율적입니다. Ent는 빠른 스크립트에 대해 “즉시 사용 가능"이 덜 하지만, 장기적인 서비스에 대해서는 견고하고 확장 가능한 기반을 제공합니다. 현대적인 접근 방식과 활발한 개발 덕분에 미래 지향적인 선택입니다.

  • Bun은 “SQL을 알고 있고 단순히 가벼운 도움을 원하는” 개발자에게 이상적입니다. 일부 마법을 포기하고 당신에게 제어속도를 제공합니다. 성능 민감한 서비스를 구축하고 데이터 액세스 코드에서 명시적인 작업을 두려워하지 않는다면, Bun은 매력적인 선택입니다. 또한, database/sql+sqlx에서 이동하면서 구조를 추가하고 효율성을 거의 희생하지 않기를 원한다면, Bun은 중간 지점으로 적합합니다. 교환 조건은 더 작은 커뮤니티와 더 적은 고수준 추상화입니다—일부 코드는 직접 작성해야 합니다. Bun 문서에 따르면, 그것은 당신의 방해를 하지 않습니다. PostgreSQL 중심 프로젝트에서 데이터베이스 특정 기능을 활용하고 싶다면, 특히 SQL을 직접 사용하는 ORM을 원할 때 Bun을 사용하세요.

  • sqlc는 자체 카테고리에 속합니다. “ORM이 아니라 SQL에 대한 컴파일 시간 보장이 필요하다"고 말하는 Go 팀에게 완벽합니다. 강력한 SQL 기술을 가지고 있고 쿼리 및 스키마를 SQL에서 관리하는 것을 선호하는 경우(sqlc는 DBA가 있거나 효율적인 SQL을 작성하는 것을 좋아하는 경우), sqlc는 생산성과 자신감을 높여 줍니다. 성능은 기본적으로 최적화되어 있으며, 쿼리와 데이터베이스 사이에 아무것도 없습니다. sqlc를 사용하지 않는 이유는 SQL을 정말 싫어하거나 쿼리가 너무 동적으로 만들어져서 여러 변형을 작성하는 것이 부담스러울 때입니다. 그조차도, sqlc를 대부분의 정적 쿼리에 사용하고 몇 가지 동적 케이스는 다른 접근 방식으로 처리할 수 있습니다. sqlc는 다른 것들과도 잘 어울립니다—프로젝트의 일부분에서 ORM을 사용하는 것을 막지 않습니다(예: GORM은 간단한 작업에 사용되고 sqlc는 중요한 경로에 사용되는 경우가 있습니다). 간단히 말해, 명확성, 제로 오버헤드, 타입 안전한 SQL을 중시하는 경우 sqlc를 선택하세요—강력한 도구입니다.

마지막으로, 이 도구들은 생태계에서 상호 배타적이지 않다는 점을 언급할 가치가 있습니다. 각각의 장단점이 있으며, Go의 실용적인 정신에 따라 많은 팀은 경우에 따라 교환을 평가합니다. GORM 또는 Ent와 같은 ORM에서 개발 속도를 위해 시작하고, sqlc 또는 Bun을 특정 핫 경로에 최대 성능이 필요한 경우에 사용하는 것이 흔합니다. 네 가지 솔루션 모두 활발히 유지되고 널리 사용되므로, 전체적으로 “잘못된 선택"은 없습니다—문맥에 맞는 “올바른 선택"을 찾는 것입니다. 이 비교가 GORM, Ent, Bun, sqlc가 어떻게 견줘지는지 더 명확하게 보여주었기를 바랍니다. 그리고 당신의 다음 Go 프로젝트에 대해 정보 있는 결정을 도와주기를 바랍니다.

유용한 링크