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

dasd412·2025년 2월 24일
0

graphql

목록 보기
3/3

entgo와 gqlgen 설정을 마쳤으면, 실제로 federation된 쿼리를 만들어보자.

예제 1

query user{
  user(id: 1) {
    id
    name
    email
    orders {
      id
      status
      totalPrice
    }
  }
}

위 쿼리는 user와 order 서비스를 federation한 것이다.

order 서비스의 extended.graphql에 다음 스키마를 작성한다.

extend type User@key(fields:"id"){
    id:ID! @external
    orders:[Order]
}

그리고 gqlgen으로 생성해보자. 그러면 entity.resolver.go라는 파일이 생긴다. 그리고 다음 코드를 작성하자.

package resolver

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.66

import (
	"context"
	"order/pkg/ent"
	"order/pkg/graph/gen"
	"order/pkg/graph/gen/graphqlmodel"
)

// FindUserByID is the resolver for the findUserByID field.
func (r *entityResolver) FindUserByID(ctx context.Context, id int) (*graphqlmodel.User, error) {
	orders, err := r.orderService.FindOrdersByUserId(ctx, r.entClient, id)

	if err != nil {
		return nil, err
	}

	return &graphqlmodel.User{
		ID:     id,
		Orders: orders,
	}, nil
}

// Entity returns gen.EntityResolver implementation.
func (r *Resolver) Entity() gen.EntityResolver { return &entityResolver{r} }

type entityResolver struct{ *Resolver }

order 스키마는 user_id라는 필드 값을 가지고 있다. 이는 외래키 역할을 하는데, 다른 서비스의 DB의 스키마를 참조하기 때문에 실제 외래키는 아니라고 볼 수 있다.

어쨋든, 위처럼 작성하게 되면 루트 노드인 user 엔티티에 order 엔티티 리스트가 담기게 된다.

리졸버 작동 과정은 다음과 같다.
1. user 서비스의 schema.resolver.go의 user() 호출
2. order 서비스의 entity.resolver.go의 FindUserByID()호출

response

{
  "data": {
    "user": {
      "id": "1",
      "name": "test",
      "email": "test1@test.com",
      "orders": [
        {
          "id": "1",
          "status": "pending",
          "totalPrice": 2
        },
        {
          "id": "2",
          "status": "pending",
          "totalPrice": 2
        },
        {
          "id": "3",
          "status": "pending",
          "totalPrice": 2
        }
      ]
    }
  }
}

예제 2

query order{
  order(id:1){
    id
    userID
    user {
      id
      name
      email
    }
  }
}

역방향도 가능하다.

이번에는 user 서비스의 extended.graphql에 다음 스키마를 작성한다.

extend type Order @key(fields:"userID"){
    userID:Int! @external
    user:User! @requires(fields:"userID")
}

생성하고 entity.resolver.go에 다음 코드를 작성한다.

// FindOrderByUserID is the resolver for the findOrderByUserID field.
func (r *entityResolver) FindOrderByUserID(ctx context.Context, userID int) (*graphqlmodel.Order, error) {
	user, err := r.userService.FindUser(ctx, r.entClient, userID)

	if err != nil {
		return nil, err
	}

	return &graphqlmodel.Order{
		UserID: userID,
		User:   user,
	}, nil
}

위처럼 작성하게 되면 루트 노드인 order 엔티티에 user 엔티티가 담기게 된다.

리졸버 작동 과정은 다음과 같다.
1. oder 서비스의 schema.resolver.go의 order() 호출
2. user 서비스의 entity.resolver.go의 FindOrderByID()호출
response

{
  "data": {
    "order": {
      "id": "1",
      "userID": 1,
      "user": {
        "id": "1",
        "name": "test",
        "email": "test1@test.com"
      }
    }
  }
}

예제 3

query users{
  users{
    edges {
      node {
        id
        name
        email
        orders {
          id
          status
          totalPrice
          delivery {
            id
            userID
            orderID
            status
          }
        }
     }
    }
  }
}

relay 페이지네이션과 연속 중첩을 해보겠다.

먼저 user 서비스에는 당연히 relay 페이지네이션을 해놔야 한다.

order 서비스의 extended.graphql에 다음 스키마를 작성한다.

extend type User@key(fields:"id"){
    id:ID! @external
    orders:[Order]
}

그리고, delivery 서비스의 extended.graphql에 다음 스키마를 작성한다.

extend type Order @key(fields:"id"){
    id:ID! @external
    delivery:Delivery
}

order 서비스의 entity.resolver.go에 다음 코드를 작성한다.

func (r *entityResolver) FindUserByID(ctx context.Context, id int) (*graphqlmodel.User, error) {
	orders, err := r.orderService.FindOrdersByUserId(ctx, r.entClient, id)

	if err != nil {
		return nil, err
	}

	return &graphqlmodel.User{
		ID:     id,
		Orders: orders,
	}, nil
}

그리고, delivery 서비스의 entity.resolver.go에 다음 코드를 작성한다. order가 되있지만, delivery가 안된 경우가 있을 수 있기 때문에 not found 에러를 따로 캐치한다.

func (r *entityResolver) FindOrderByID(ctx context.Context, id int) (*graphqlmodel.Order, error) {
	delivery, err := r.deliveryService.FindDeliveryByOrderId(ctx, r.entClient, id)

	if ent.IsNotFound(err) {
		return &graphqlmodel.Order{
			ID:       id,
			Delivery: nil,
		}, nil
	}

	if err != nil {
		return nil, err
	}

	return &graphqlmodel.Order{
		ID:       id,
		Delivery: delivery,
	}, nil
}

이렇게 작성하게 되면 루트 노드인 user 엔티티에 order 엔티티 리스트가 담기게 된다. 그리고 order 엔티티에 delivery 엔티티가 담기게 된다.

리졸버 작동 과정은 다음과 같다.
1. user 서비스의 schema.resolver.go의 user() 호출
2. order 서비스의 entity.resolver.go의 FindUserByID()호출
3. delivery 서비스의 entity.resolver.go의 FindOrderByID()호출

3.의 경우 graphql 최적화가 필요해보이지만 이 포스트의 관심사가 아니므로 생략한다.

response

{
  "data": {
    "users": {
      "edges": [
        {
          "node": {
            "id": "1",
            "name": "test",
            "email": "test1@test.com",
            "orders": [
              {
                "id": "1",
                "status": "pending",
                "totalPrice": 2,
                "delivery": {
                  "id": "1",
                  "userID": 1,
                  "orderID": 1,
                  "status": "pending"
                }
              },
              {
                "id": "2",
                "status": "pending",
                "totalPrice": 2,
                "delivery": {
                  "id": "2",
                  "userID": 1,
                  "orderID": 2,
                  "status": "pending"
                }
              },
              {
                "id": "3",
                "status": "pending",
                "totalPrice": 2,
                "delivery": null # delivery가 안된 경우의 order
              }
            ]
          }
        }
      ]
    }
  }
}

참고 자료

https://www.apollographql.com/docs/graphos/schema-design/guides/relay-style-connections


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

0개의 댓글