entgo 시작하기: ORM보다 스키마 중심 그래프 모델링으로 보기
entgo를 처음 보면 ORM처럼 보인다. 데이터베이스 테이블에 대응되는 타입이 있고, create, query, update API도 나온다. 여기까지만 보면 GORM 같은 도구와 비슷해 보인다.
하지만 실제로 쓰는 흐름은 조금 다르다. entgo는 쿼리 빌더를 먼저 익히는 도구라기보다, 스키마를 코드로 정의하고 그 정의에서 전체 데이터 접근 코드를 생성하는 도구에 가깝다. 이 관점을 먼저 잡아두면 이후의 fields, edges, query API도 문법보다 설계 흐름으로 읽힌다.
entgo를 ORM으로만 보면 놓치는 것
일반적인 ORM 사용 방식은 보통 데이터베이스 테이블이 먼저 있고, 애플리케이션은 그 위에 모델을 얹는 쪽에 가깝다. 관계는 preload나 join 전략으로 뒤에서 다루는 경우가 많다.
entgo는 중심이 다르다. ent/schema 아래에 엔티티를 정의하고, 필드와 관계를 먼저 적는다. 그 다음 go generate ./ent를 실행하면 client, entity type, create/query/update builder, predicate, migration 코드가 같이 생성된다. 이후 애플리케이션 코드는 이 생성 결과를 기준으로 움직인다.
즉 entgo에서 스키마는 선언 파일 하나가 아니다. 모델, 관계, 타입, 쿼리 API의 출발점이다.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Int("age").Positive(),
}
}
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}이 정의는 컬럼 두 개와 relation 하나를 적는 데서 끝나지 않는다. 이후 생성되는 UserCreate, UserQuery, predicate, edge traversal API까지 같이 결정한다. entgo를 이해할 때는 이 연결을 먼저 보는 편이 낫다.
스키마가 워크플로우의 중심이다
공식 getting started도 결국 같은 흐름을 보여준다.
- 스키마를 만든다.
- 필드와 edge를 정의한다.
- 코드를 생성한다.
- 생성된 client로 migration과 CRUD를 수행한다.
여기서 핵심은 4번보다 앞 단계다. entgo는 런타임에서 모델을 동적으로 조합하기보다, 설계를 먼저 고정하고 그 설계에 맞는 코드를 만들어서 쓰게 한다.
이 방식은 편의 기능이라기보다 제약에 가깝다. 필드 이름, optional 여부, unique 조건, edge 방향을 애매하게 두기 어렵다. 초반에는 돌아가는 코드가 늦게 나오는 느낌이 들 수 있지만, 모델이 커질수록 이 제약이 오히려 도움이 된다. 스키마 정의가 한 번 정리되면 이후의 create, query, relation 탐색 방식도 같이 정리되기 때문이다.
예를 들어 필수 필드를 빠뜨리거나, unique relation을 잘못 이해한 채 코드를 쓰면 저장 시점이나 생성된 API 사용 과정에서 바로 드러난다. 팀 단위에서는 이런 제약이 일관성을 만든다.
코드 생성은 편의 기능보다 설계 방식에 가깝다
entgo의 장점을 말할 때 타입 안전성을 자주 언급한다. 맞는 말이다. 다만 핵심은 안전한 API가 생긴다는 사실 자체보다, 스키마에서 결정한 내용이 코드베이스 전체로 전파된다는 점에 있다.
u, err := client.User.
Create().
SetName("a8m").
SetAge(30).
Save(ctx)이런 API는 쓰기 단순하다. 하지만 이 단순함은 런타임에서 조합된 결과가 아니라, 스키마에서 파생된 결과다. entgo를 쓸 때는 생성 코드를 직접 고치기보다 스키마를 수정하고 다시 생성하는 흐름에 익숙해져야 한다.
그래서 entgo는 ORM 사용법을 외우는 경험보다, 데이터 모델을 정의하고 생성 결과를 신뢰하는 작업 방식에 더 가깝다. 이 차이를 받아들이면 생산성이 올라가고, 받아들이지 못하면 생성 코드가 오히려 답답하게 느껴진다.
관계를 초반부터 같이 설계해야 한다
entgo가 다른 ORM과 가장 다르게 느껴지는 지점은 edge다. 필드만 먼저 만들고 관계는 나중에 생각하는 식으로 접근하면 금방 어색해진다. entgo는 이름 그대로 graph 모델링에 가깝고, 관계가 초반 설계의 일부다.
공식 getting started가 User 하나로 끝나지 않고 Car, Group을 추가해 one-to-many, many-to-many, inverse edge를 바로 소개하는 이유도 여기에 있다. entgo에서는 엔티티 하나보다 엔티티 사이의 연결이 쿼리 경험을 더 크게 바꾼다.
cars, err := client.Group.
Query().
Where(group.Name("GitHub")).
QueryUsers().
QueryCars().
All(ctx)이런 traversal API를 보면 SQL join을 직접 조립하는 감각보다, 그래프 위를 이동하는 감각에 가깝다는 걸 알 수 있다. 그래서 스키마를 설계할 때도 “이 테이블에 어떤 컬럼이 필요한가”만 볼 게 아니라, “이 엔티티가 어디와 연결되고 어떤 방향으로 자주 탐색되는가”를 같이 봐야 한다.
시작할 때 미리 알아둘 점
첫째, 생성 코드를 읽는 습관이 필요하다. 직접 수정하라는 뜻은 아니다. 어떤 builder가 생기는지, edge 이름이 어떤 query API로 바뀌는지 읽어보는 습관이 있어야 한다. entgo는 생성 결과를 이해할수록 편해진다.
둘째, 자동 migration은 개발 초반에는 편하지만 운영 환경에서는 versioned migration을 같이 보는 편이 낫다. 공식 문서도 client.Schema.Create(ctx)는 개발, 테스트, 프로토타입에 더 적합한 흐름으로 설명한다. 운영에 들어가면 Atlas 기반 migration까지 포함해서 보는 게 자연스럽다.
셋째, schema 이름과 relation 이름은 초반에 신중하게 잡는 편이 좋다. entgo에서는 이 이름들이 생성 API 전체에 반영된다. 처음엔 사소해 보여도 query 체인, predicate 이름, 이후의 GraphQL 연동 같은 지점에서 계속 드러난다.
넷째, entgo는 SQL을 몰라도 되는 도구가 아니다. 오히려 unique 제약, relation cardinality, migration 전략을 분명하게 다루게 만들기 때문에 기본적인 데이터베이스 감각이 더 중요해진다.
다음 글로 넘어가기 전에 잡아둘 관점
entgo 입문에서 먼저 잡아야 할 건 문법이 아니다. 스키마를 중심에 두고, 필드와 edge를 먼저 설계하고, 생성된 코드를 통해 데이터 접근 방식을 고정하는 흐름이다.
이 관점을 잡고 나면 공식 getting started의 예제도 더 자연스럽게 읽힌다. User를 만들고 CRUD를 해보는 예제가 핵심이 아니라, 스키마 정의가 create, query, traversal, migration으로 이어지는 연결이 핵심이다.
다음 글에서는 fields와 edges를 조금 더 구체적으로 볼 수 있다. 그 전에 entgo를 또 하나의 ORM으로만 보면 이후 문법은 익혀도 이 도구의 장점은 잘 안 보인다. entgo는 스키마를 코드로 다루고, 그 스키마에서 데이터 접근 방식을 생성하는 쪽에 더 가깝다.
참고 자료
- entgo Getting Started: 첫 스키마, edge, graph traversal, migration 흐름을 한 번에 보여주는 공식 입문 문서.
- entgo Schema Definition: schema, fields, edges, indexes 구조를 설명한다.
- entgo CRUD API: 생성된 client와 create/query/update API 형태를 확인할 수 있다.