Gateway는 User, Order, Delivery 서비스들의 GraphQL
을 federation
한다. 다음 공식 예제 이미지처럼 각 서비스끼리는 다른 서비스의 DB에 접근할 수 없다. 단, 프론트엔드에서 쿼리할 때는 Gateway가 해준다.
const { ApolloGateway, IntrospectAndCompose } = require("@apollo/gateway");
const { ApolloServer } = require("@apollo/server");
const { startStandaloneServer } = require("@apollo/server/standalone");
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: "user", url: "http://localhost:8081/graphql" },
{ name: "order", url: "http://localhost:8082/graphql" },
{ name: "delivery", url: "http://localhost:8083/graphql" },
],
}),
});
async function startServer() {
const server = new ApolloServer({ gateway });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`🚀 Apollo Gateway running at ${url}`);
}
startServer();
User, Order, Delivery라는 서비스가 각각 있고, 각각의 서비스는 다음 entgo
스키마를 갖는다. 스키마는 chatgpt 돌려서 대충 만들었으니 양해 바란다.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("email").
Comment("이메일").
Unique(),
field.String("password").
Comment("해시화된 비밀 번호"),
field.String("name").
Comment("이름"),
field.Time("created_at").
Comment("생성 날짜").
Default(time.Now).
Immutable(),
field.Enum("role").
Values("admin", "author", "guest").
Comment("인가 권한"),
}
}
// Order 엔티티 정의
type Order struct {
ent.Schema
}
// Fields of the Order.
func (Order) Fields() []ent.Field {
return []ent.Field{
field.Int("user_id"), // User ID (Foreign Key 역할)
field.Enum("status").Values("pending", "paid", "shipped", "canceled"),
field.Float("total_price"),
field.Time("created_at").Default(time.Now),
}
}
type Delivery struct {
ent.Schema
}
// Fields of the Delivery.
func (Delivery) Fields() []ent.Field {
return []ent.Field{
field.Int("order_id").Unique(),
field.Int("user_id"),
field.Enum("status").Values("pending", "in_transit", "delivered"),
field.String("tracking_number").Optional(),
field.Time("created_at").Default(time.Now),
}
}
entgo 스키마는 자동 생성된 코드가 federation
엔티티로 인식되지 않는다. 이를 해결하려면, 코드를 자동 생성할 때 준비가 필요하다.
먼저, 다음 tmpl
파일을 작성한다.
{{ define "model/additional/gql_federation" }}
// IsEntity implement fedruntime.Entity
func ({{ $.Receiver }} {{ $.Name }}) IsEntity() {}
{{ end }}
그리고 entc.go에 위 tmpl 파일 경로를 주입한다.
opts := []entc.Option{
entc.Extensions(
ex,
),
entc.TemplateDir("./template"),
}
그리고 entgo
로 자동 생성해보자. 뒤에서 다룰 gqlgen
설정을 마치고 나서 gqlgen
자동 생성도 해보면, entity.resolver.go라는 파일이 생길 것이다.
federation
을 위해서는 @key
디렉티브가 필요하다. 예를 들어, User의 경우 다음 GraphQL
스키마가 필요하다.
type User @key(fields: "id") {
id: ID!
"""
이메일
"""
email: String!
"""
해시화된 비밀 번호
"""
password: String!
"""
이름
"""
name: String!
"""
생성 날짜
"""
createdAt: Time!
"""
인가 권한
"""
role: UserRole!
}
그런데 entgo
에 의해 User 엔티티가 자동 생성되기 때문에, 위 @key
디렉티브를 코딩으로 넣을 방법이 없다.
따라서 자동 생성될 때부터 @key
디렉티브를 넣는 방법을 이용한다.
다음 코드를 작성한 다음,
package federation
import (
"entgo.io/contrib/entgql"
"github.com/vektah/gqlparser/v2/ast"
)
func GraphKeyDirective(fields string) entgql.Annotation {
return entgql.Directives(keyDirective(fields))
}
func keyDirective(fields string) entgql.Directive {
var args []*ast.Argument
if fields != "" {
args = append(args, &ast.Argument{
Name: "fields",
Value: &ast.Value{
Raw: fields,
Kind: ast.StringValue,
},
})
}
return entgql.NewDirective("key", args...)
}
entgo
가 제공하는 Annotation
메서드에 다음을 넣는다.
func (User) Annotations() []schema.Annotation {
return []schema.Annotation{
//...
federation.GraphKeyDirective("id"),
}
}
그리고 생성하게 되면 자동 생성된 ent.graphql
파일에 @key
디렉티브가 추가된 걸 확인할 수 있다.
relay connection
을 federation
과 쓰고 싶을 수가 있다. 그런데 둘을 같이 활용하다보면, 문제가 생기는데 바로 자동 생성되는 Node()
와 Nodes()
다. 이 메서드들은 entgo
에서 relay connection
을 사용하면, 자동 생성되는 코드들이다.
하지만, 여러 서비스에서 두 메서드를 자동 생성하면 gateway가 compose를 할 때 충돌이 난다. 따라서 자동 생성할 때 두 메서드를 제외해야 한다.
package federation
import (
"entgo.io/ent/entc/gen"
"errors"
"github.com/vektah/gqlparser/v2/ast"
)
var (
RemoveNodeGoModel = func(_ *gen.Graph, s *ast.Schema) error {
n, ok := s.Types["Node"]
if !ok {
return errors.New("failed to find node interface in schema")
}
dirs := ast.DirectiveList{}
for _, d := range n.Directives {
switch d.Name {
case "goModel":
continue
default:
dirs = append(dirs, d)
}
}
n.Directives = dirs
return nil
}
RemoveNodeQueries = func(_ *gen.Graph, s *ast.Schema) error {
q, ok := s.Types["Query"]
if !ok {
return errors.New("failed to find query definition in schema")
}
fields := ast.FieldList{}
for _, f := range q.Fields {
switch f.Name {
case "node":
case "nodes":
continue
default:
fields = append(fields, f)
}
}
q.Fields = fields
return nil
}
)
구현한 다음, entc.go에 추가하자. entgql.WithSchemaHook(federation.RemoveNodeGoModel, federation.RemoveNodeQueries)
부분을 보자.
//go:build ignore
package main
import (
"entgo.io/contrib/entgql"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"federation"
"log"
)
func main() {
ex, err := entgql.NewExtension(
//...
entgql.WithRelaySpec(true), //relay spec 활성화
entgql.WithConfigPath("../graph/gqlgen.yml"),
entgql.WithSchemaPath("../graph/ent.graphql"),
entgql.WithSchemaHook(federation.RemoveNodeGoModel, federation.RemoveNodeQueries),
)
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
opts := []entc.Option{
entc.Extensions(
ex,
),
entc.TemplateDir("./template"),
}
if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
여기는 별게 없다. 공식 예제 따라 하면 된다. 이걸로 자동 생성하면 federation.go라는 파일이 생긴다.
federation:
filename: gen/federation.go
package: gen
version: 2
만약, relay connection
을 하고 싶다면 다음도 추가하자.
Node:
model:
- your-project/pkg/ent.Noder
최종적으로 자동 생성된 ent.graphql
을 보면, 다음과 같다.
type User implements Node @key(fields: "id") {
id: ID!
"""
이메일
"""
email: String!
"""
해시화된 비밀 번호
"""
password: String!
"""
이름
"""
name: String!
"""
생성 날짜
"""
createdAt: Time!
"""
인가 권한
"""
role: UserRole!
}
https://github.com/ent/ent/issues/3156
https://github.com/infratographer/x/tree/main/entx