-
GraphQL에 대하여 ( Resolver & Dataloader / Aliases & Fragment)Graphql 2023. 6. 3. 12:11
2023.05.07 - [Graphql] - GraphQL에 대하여 (Interface & Union Type)
GraphQL에 대하여 (Interface & Union Type)
2023.04.23 - [Graphql] - GraphQL (Schema & Type) 에 이어서 interface & union type에 대해 설명하고자 한다. Interface 다른 많은 타입 시스템처럼 GraphQL은 interface를 지원한다. 인터페이스는 Type이 무엇 인가를 구현
zzihyeon.tistory.com
에 이어서 포스팅 하고자 한다.
Resolver
GraphQL에서 Resolver는 쿼리의 각 필드에 대한 데이터를 제공하는 함수이다.
Resolver는 GraphQL 서버에서 데이터를 가져오는 데 사용되며, 클라이언트가 요청한 필드의 값을 반환한다.
Resolver는 GraphQL Schema의 field와 연결된다.
각 field에는 해당 필드의 값을 검색하고 반환하는 Resolver 함수가 있어야 한다. (기본 함수는 받은 값을 그대로 return하는 함수이다.)
Resolver 함수는 데이터베이스, 외부 API 호출, 메모리 캐시, 또는 다른 소스로부터 데이터를 가져올 수 있다.
GraphQL 스키마는 타입과 필드의 구조를 정의하는데, 이를 기반으로 Resolver는 어떤 데이터를 반환해야 하는지 결정한다.
Resolver는 필드의 값을 동적으로 계산하거나 적절한 데이터 소스로부터 데이터를 검색하여 반환할 수 있다.글로만 작성하면 이해가 어려울 수 있으니 간단히 예를 들어 보겠다. 지난 글에서 사용한 SellerUser Schema를 사용하도록 하겠다.
우선 아래와 같이 allusers를 호출한다고 가정해보자.query{ allusers{ name ... on SellerUser { stuff{ id name total } bizInfo{ biz_num biz_img } } } }
field로는 name, stuff, stuff.id, stuff.name, stuff.total, bizInfo, bizInfo.biz_num, bizInfo.biz_img 총 8개가 있고 8번의 resolver가 호출된다. 따로 함수를 작성하지 않으면 그냥 field의 값을 그대로 return한다.
그럼 이 resolver는 왜 사용해야 할까?
type SellerUser implements UserInterface { uid: Int! email: String! name: String! phone: String! gender: gender! birth_date: Time! created_at: Time! updated_at: Time! bizInfo: [BizInfo!]! stuff: [Stuff!]! }
만약 RDB를 사용했다고 가정하면 여기서 UserInterface에서 가져오는 field들은 하나의 table에 저장되어 있을 가능성이 높다. 즉, 아래의 field들은 data join 없이도 하나의 table에서 가져올 수 있다.
uid: Int! email: String! name: String! phone: String! gender: gender! birth_date: Time! created_at: Time! updated_at: Time!
하지만 bizInfo와 stuff는 다른 table에 저장되어 있을 가능성이 높다. 이를 가져오기 위해서는 table을 join해서 가져와야 한다.
SellerUser를 조회할 때마다 bizInfo, stuff 정보를 join해서 가져오면 맨 처음에 말했던 graphQL의 장점인 overfetching을 하지 않는다를 지킬 수 없게 된다.
이 때, resolver를 통해 사용자가 field를 요청할 때만 해당 data를 조회해 오도록 할 수 있다.만약 그렇게 변경한다면 위의 sellerUser에서는 field마다 resolver가 아래와 같이 동작할 것이다.
func (r *sellerUserResolver) uid(ctx context.Context, obj *model.SellerUser) (string, error) { return obj.Uid, error; } /* ... bizInfo, stuff를 제외하고는 생략되어 있으며 resolver에서 그냥 자신의 값을 return한다. */ func (r *sellerUserResolver) BizInfo(ctx context.Context, obj *model.SellerUser) ([]*model.BizInfo, error) { // bizInfo table 조회 } func (r *sellerUserResolver) Stuff(ctx context.Context, obj *model.SellerUser) ([]*model.Stuff, error) { // Stuff table 조회 }
이렇게 변경하면 overfetching은 막을 수 있지만 user조회 시 DB를 총 3번 조회해야 하는 문제가 생길 수 있다.
따라서, 전략적으로 잘 활용해야 한다.DataLoader
Resolver를 사용할 때 N+1이라는 문제가 발생하게 된다.
N+1 문제란 요청은 한번만 했지만 Resolver에 의해 N번의 트랜젝션이 발생할 수 있는 것이다.예를 들면, user, bizInfo, stuff 이 3개의 table이 있다고 가정해보자.
이 상황에서 alluser query를 호출하면 User정보를 위해 DB 조회 1회, user가 N명 있다고 가정하면 user 별 bizInfo조회를 위한 DB 조회 N회, user별 Stuff조회를 N회 해야 한다.
아래와 같이 1회의 query요청으로 1+N+N회의 조회를 해야 한다.SellerUser { data: [ { uid: 1, name: "a", bizInfo: [...], //resolver 실행 1회 stuff: [...], //resolver 실행 1회 }, { uid: 2, name: "b", bizInfo: [...], //resolver 실행 1회 stuff: [...], //resolver 실행 1회 }, ... { uid: N, name: "c", bizInfo: [...], //resolver 실행 1회 stuff: [...], //resolver 실행 1회 } ] }
이는 N이 커지면 커질수록 DB에 과부화를 준다. 이 때문에 이를 해결하기 위해 DataLoader가 개발되었다.
위의 예시에서 dataLoader는 user의 uid를 key 값으로 사용하고 이 uid를 배열로 모아서 각각 1번의 트랜잭션 Or 연산으로 bizInfo와 stuff를 찾을 것이다DataLoader의 동작 방식은 아주 간단하고 아래와 같다:
dataloader github- Batch func을 선언한다.
- middleware에서 context에 해당 batch func을 삽입한다.
- 필요한 resolver에서 dataLoader의 값을 Load해온다.
이 때, 주의할 점은 dataLoader에 요청한 키 값의 길이와 데이터를 조회한 결과 값의 길이는 반드시 같아야 한다.
즉, 위에 예시를 보면 조회 시 keys는 uid의 배열 [1,2,...,n]으로 길이가 n인 배열일 것이다.
실제로 db에 조회를 하면SELECT * FROM bizInfos WHERE user_id = ANY([1,2,...n])
과 같이 조회를 할 것이다.
이 때 bizInfo 값이 존재하지 않아 null 이더라도 [bizInfo1, null, ...., bizInfoN] 과 같이 null로 배열을 채워 길이를 맞춰야 한다.Aliases
schema의 경우는 이전 포스팅에서 사용하던 정보를 그대로 사용하겠다. 만약, 우리가 아래와 같은 화면이 필요하다고 가정해보자.
세개의 component 모두 user에 대한 data를 필요로 한다. 이 때 동일한 query를 사용하며 argument만 다르게 사용할 가능성이 높다. 그렇다면 이를 어떻게 처리해야 할까?
총 3가지의 user에 대한 정보를 얻기 위해서 아래와 같이 사용한다고 가정해 보자.query { allUser(input: { sortBy: PURCHASE_COUNT, limit: 10}) { name } allUser(input: { sortBy: TOTAL_PRICE, limit: 10}) { name } allUser(input: { sortBy: INVITE_FRIEND, limit: 10}) { name } }
이렇게 사용하면 서버는 data에 allUser라는 같은 field에 3개의 data를 return할 수 없기 때문에 에러가 발생할 것이다.
그럼 이는 어떻게 처리할 수 있을까?
이를 위해서 graphql은 aliases를 지원한다. aliases를 사용하면 위의 query를 아래와 같이 변경할 수 있다.query { BestPurchaseUser: allUser(input: { sortBy: PURCHASE_COUNT, limit: 10}) { name } BestPriceUser: allUser(input: { sortBy: TOTAL_PRICE, limit: 10}) { name } BestInviteFriendUser: allUser(input: { sortBy: INVITE_FRIEND, limit: 10}) { name } }
이에 대해서 서버는 아래와 같이 return 해 준다.
{ data: { BestPurchaseUser:[ { name: "zzihyeon" }, ... ] BestPriceUser: [ { name: "zzihyeon2" }, ... ] BestInviteFriendUser: [ { name: "zzihyeon3" }, ... ] } }
이제 우리는 하나의 쿼리를 이용하여 여러개의 요청을 한번에 할 수 있게 되었다.
Fragments
graphQL을 사용하다보면 component에 따라 field들을 다르게 사용할 경우가 많다. 하지만 GraphQL에서는 query / mutation을 요청할 때 field를 지정하기 때문에 이는 중복 작업이고 불편할 수 있다. 바로 위에서 사용한 예를 들어보자.
아래와 같은 요청을 필요로 한다고 가정하자.query { BestPurchaseUser: allUser(input: { sortBy: PURCHASE_COUNT, limit: 10}) { name email gender purchases{ id amount } } BestPriceUser: allUser(input: { sortBy: TOTAL_PRICE, limit: 10}) { name email gender purchases{ id amount } } BestInviteFriendUser: allUser(input: { sortBy: INVITE_FRIEND, limit: 10}) { name email gender purchases{ id amount } friend{ name } } }
쓸데없이 너무 길어보이고 중복이 많다. 이를 해결하기 위해 fragment를 사용한다.
query { BestPurchaseUser: allUser(input: { sortBy: PURCHASE_COUNT, limit: 10}) { ...userFields } BestPriceUser: allUser(input: { sortBy: TOTAL_PRICE, limit: 10}) { ...userFields } BestInviteFriendUser: allUser(input: { sortBy: INVITE_FRIEND, limit: 10}) { ...userFields friend{ name } } } fragment userFields on User { name email gender purchases{ id amount }}
위 처럼 fragment를 사용해서 query field조각으로 관리하면 한결 편안함을 느낄 수 있다.
그럼 fragment는 이렇게 중복 방지를 위해서만 사용되는 것일까?
fragment라는 개념은 복잡한 애플리케이션 데이터 요구 사항을 더 작은 덩어리로 분할하는 데 자주 사용되고 서로 다른 fragment을 가진 많은 UI 구성 요소를 하나의 초기 데이터 가져오기로 결합해야 할 때 더욱 유용하다.
예를 들어 아래와 같은 화면이 있다고 가정해 보자.
구성하기에 따라서 위의 user 셋은 userField라는 동일한 fragment를 사용할 것이고 아래의 둘은 stuffField라는 동일한 fragment를 사용할 것이고 맨 오른쪽의 광고 상품은 광고 상품을 위한 특정 정보가 있을 것이기 때문에 stuffAdField라는 fragment를 사용할 것이다. 이를 query로 요청한다고 가정해 보자.query { BestPurchaseUser: allUser(input: { sortBy: PURCHASE_COUNT, limit: 10}) { ...userFields } BestPriceUser: allUser(input: { sortBy: TOTAL_PRICE, limit: 10}) { ...userFields } BestInviteFriendUser: allUser(input: { sortBy: INVITE_FRIEND, limit: 10}) { ...userFields friend{ name } } BestSellerStuff: allStuff(input: { sortBy: Sell, limit: 10}) { ...stuffFields } BestPriceAmountStuff: allStuff(input: { sortBy: PriceAmount, limit: 10}) { ...stuffFields } AdStuff: allStuff(input: {isAd: true, sortBy: adPriority, limit: 10}) { ...adStuffFields } } fragment userFields on User { name email gender purchases{ id amount } } fragment stuffFields on Stuff { name kind price } fragment adStuffFields on Stuff { name kind price ad_type ad_time }
여기서 각각의 fragment들은 component에 대한 의존성을 갖기 때문에 설계 및 관리를 더욱 편하게 할 수 있다.
'Graphql' 카테고리의 다른 글
GraphQL에 대하여 (Interface & Union Type) (0) 2023.05.07 GraphQL (Schema & Type) (0) 2023.04.23 GraphQL이란 (0) 2023.04.16 DataLoader (0) 2022.10.09