entgo와 gqglen을 활용한 GraphQL federation - (1)

dasd412·2025년 2월 24일
0

graphql

목록 보기
2/3

서비스 설명 및 entgo 스키마

Gateway는 User, Order, Delivery 서비스들의 GraphQLfederation한다. 다음 공식 예제 이미지처럼 각 서비스끼리는 다른 서비스의 DB에 접근할 수 없다. 단, 프론트엔드에서 쿼리할 때는 Gateway가 해준다.

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 돌려서 대충 만들었으니 양해 바란다.

User

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

// 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),
	}
}

Delivery

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 설정

entgo 스키마 자동 생성 코드를 federation 엔티티로 인식하게 하기

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라는 파일이 생길 것이다.

@key directive을 entgo 자동 생성 코드에 넣기

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 디렉티브가 추가된 걸 확인할 수 있다.

entgo 자동 생성 코드에서 Node, Nodes() 메서드 제거하기

relay connectionfederation과 쓰고 싶을 수가 있다. 그런데 둘을 같이 활용하다보면, 문제가 생기는데 바로 자동 생성되는 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)
	}
}

gqlgen 설정

여기는 별게 없다. 공식 예제 따라 하면 된다. 이걸로 자동 생성하면 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


profile
시스템 아키텍쳐 설계에 관심이 많은 백엔드 개발자입니다. (Go/Python/MSA/graphql/Spring)

0개의 댓글