미래 지향적인 웹 앱 구축: The Codest의 전문가 팀이 제공하는 인사이트
The Codest가 최첨단 기술로 확장 가능한 대화형 웹 애플리케이션을 제작하고 모든 플랫폼에서 원활한 사용자 경험을 제공하는 데 탁월한 성능을 발휘하는 방법을 알아보세요. Adobe의 전문성이 어떻게 디지털 혁신과 비즈니스를 촉진하는지 알아보세요...
다른 기술과 마찬가지로 GraphQL에도 문제가 있으며, 그 중 일부는 아키텍처에서 직접적으로 발생하는 문제이고 일부는 다른 애플리케이션에서 볼 수 있는 문제와 동일합니다. 하지만 해결책은 완전히 다릅니다.
문제를 제시하기 위해 다음과 같은 애플리케이션 아키텍처를 가정해 보겠습니다:
그리고 여기에 해당 쿼리는 GraphQL 를 클릭하여 데이터를 다운로드합니다. 시스템에 추가된 포스터 및 해당 링크와 함께 모든 링크를 가져옵니다,
{
모든 링크 {
id
url
설명
createdAt
게시자 {
id
name
링크 {
id
}
}
}
}
아래에 표시된 것처럼, 여기서 관계의 고전적인 N + 1 문제를 볼 수 있습니다.
링크 로드 (0.4ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
사용자 로드(0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 40], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
링크 로드(0.3ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 40]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
사용자 로드(0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 38], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
링크 로드(0.1ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 38]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
사용자 로드(0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 36], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
링크 로드 (0.1ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 36]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
사용자 로드(0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 34], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
링크 로드(0.2ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 34]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
사용자 로드(0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 32], ["LIMIT", 1]]
이 경우 다음과 같이 작동합니다. 코드:Link.all.map(&:사용자).map(&:링크)
.
우리는 문제의 해결책을 알고 있는 것 같습니다: 링크.포함(사용자: :링크).지도(&:사용자).지도(&:링크)
하지만 정말 효과가 있을까요? 확인해 보겠습니다!
수정 사항을 확인하기 위해 GraphQL 쿼리를 사용하여 관계 없이 몇 개의 필드만 사용하도록 설정합니다.
{
모든 링크 {
id
url
설명
createdAt
}
}
안타깝게도 결과는 사용자와 그 링크와 관련된 링크가 없음에도 불구하고 여전히 이 데이터를 데이터베이스 쿼리에 첨부하고 있음을 보여줍니다. 안타깝게도 이러한 데이터는 중복되고 훨씬 더 복잡한 구조로 인해 비효율적인 것으로 밝혀졌습니다.
GraphqlController#execute로 처리 */*
매개변수: {"query"=>"{n allLinks {n idn urln descriptionn createdAtn }n}", "graphql"=>{"query"=>"{n allLinks {n idn urln descriptionn createdAtn }n}"}}
링크 로드 (0.3ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
사용자 로드 (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ??) [["id", 40], ["id", 38], ["id", 36], ["id", 34], ["id", 32], ["id", 30], ["id", 28], ["id", 26], ["id", 24], ["id, 22], ["id", 20], ["id", 18], ["id", 16], ["id", 14], ["id", 12], ["id", 10], ["id", 8], ["id", 6], ["id", 4], ["id", 2]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
링크 로드 (0.3ms) SELECT "links".* FROM "links" WHERE "links"."user_id" IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [["user_id", 2], ["user_id", 4], ["user_id", 6], ["user_id", 8], ["user_id", 10], ["user_id", 12], ["user_id", 14], ["user_id", 14], ["user_id", 16], ["user_id", 17], ["user_id", 18], ["user_id", 20], ["user_id", 22], ["user_id", 24], ["user_id", 26], ["user_id", 28], ["user_id", 30], ["user_id", 32], ["user_id", 34], ["user_id", 36], ["user_id", 38], ["user_id", 40]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
39ms에 200 OK 완료(조회: 0.7ms | 액티브 레코드: 0.9ms | 할당: 8730)
In GraphQL를 사용하면 쿼리에 데이터를 넣을 때 데이터가 필요하다고 가정하여 데이터를 일괄적으로 로드하는 방식으로 이러한 문제가 다르게 해결됩니다. 이는 매우 느린 로딩입니다. 가장 많이 사용되는 라이브러리 중 하나는 https://github.com/Shopify/graphql-batch/.
안타깝게도 설치가 생각만큼 번거롭지는 않습니다. 데이터 로더는 여기에서 사용할 수 있습니다(https://github.com/Shopify/graphql-batch/tree/master/examples). 레코드 로더
클래스와협회 로더
클래스입니다. 클래스를 설치해 보겠습니다. gem 'graphql-batch'
라이브러리를 생성한 다음 로더와 스키마에 추가합니다:
# 그래프큐엘-루비/앱/그래프큐엘/그래프큐l_튜토리얼_스키마.rb
그래프큐 튜토리얼 스키마 클래스 < 그래프큐::스키마
쿼리 유형::쿼리 유형
돌연변이 유형::돌연변이 유형
GraphQL::Batch 사용
...
end
그리고 우리의 유형:
# 그래프큐엘-루비/앱/그래프큐엘/types/link_type.rb
모듈 유형
LinkType 클래스 < BaseNode
field :created_at, DateTimeType, null: false
field :url, 문자열, null: false
field :description, String, null: false
field :posted_by, UserType, null: false, method: :user
field :votes, [Types::VoteType], null: false
def user
Loaders::RecordLoader.for(User).load(object.user_id)
end
end
end
# 그래프큐엘-루비/앱/그래프큐엘/types/user_type.rb
모듈 타입
UserType 클래스 < BaseNode
field :created_at, DateTimeType, null: false
field :name, String, null: false
field :email, String, null: false
field :votes, [VoteType], null: false
field :links, [LinkType], null: false
def links
로더::어소시에이션로더.for(사용자, :링크).로드(객체)
end
end
end
로더를 사용한 결과, 데이터를 일괄 처리하고 두 개의 간단한 SQL 쿼리로 데이터를 쿼리합니다:
2021-06-16 22:40:17 +0200에서 ::1에 대해 "/graphql" POST를 시작했다.
(0.1ms) SELECT sqlite_version(*)
GraphqlController#execute로 처리 */*
파라미터 {"query"=>"{n 모든링크 {n idn urln 설명n createdAtn 게시자 {n idn 이름 링크 {n idn }n }n }n}", "graphql"=>{"query"=>"{n 모든링크 {n idn urln 설명n createdAtn 게시자 {n idn 이름 링크 {n idn }n }n}"}}
링크 로드 (0.4ms) SELECT "links".* FROM "links"
↳ app/controllers/graphql_controller.rb:5:in `execute'
사용자 로드 (0.9ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ??) [["id", 2], ["id", 4], ["id", 6], ["id", 8], ["id", 10], ["id", 12], ["id", 14], ["id", 16], ["id", 18], ["id", 20], ["id", 22], ["id", 24], ["id", 26], ["id", 28], ["id", 30], ["id", 32], ["id", 34], ["id", 36], ["id", 36], ["id", 38], ["id", 40]]
앱/그래프QL/로더/기록_로더.rb:12:in `수행'
Link Load (0.5ms) SELECT "links".* FROM "links" WHERE "links"."user_id" IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [["user_id", 2], ["user_id", 4], ["user_id", 6], ["user_id", 8], ["user_id", 10], ["user_id", 12], ["user_id", 14], ["user_id", 14], ["user_id", 16], ["user_id", 17], ["user_id", 18], ["user_id", 20], ["user_id", 22], ["user_id", 24], ["user_id", 26], ["user_id", 28], ["user_id", 30], ["user_id", 32], ["user_id", 34], ["user_id", 34], ["user_id", 36], ["user_id", 38], ["user_id", 40]]
↳ app/graphql/loaders/association_loader.rb:46:in `preload_association'
62ms에 200 OK 완료(조회: 1.3ms | 액티브 레코드: 1.8ms | 할당: 39887)
이 문제를 해결하는 다른 솔루션도 있습니다:
N + 1 쿼리가 전부는 아닙니다. GraphQL 를 초과하면 다음 속성을 자유롭게 이월할 수 있습니다. 기본적으로 1로 설정되어 있습니다. 특히 데이터를 자유롭게 중첩할 수 있는 상황에서는 서버에 과부하가 걸릴 수 있습니다. 어떻게 처리할 수 있을까요? 쿼리의 복잡성을 제한할 수 있지만 이를 위해서는 속성에 비용을 지정해야 합니다. 기본적으로 1로 설정되어 있습니다. 이 비용은 복잡성:
속성을 사용하여 데이터를 입력할 수 있습니다: 필드: 링크, [링크 유형], 널: 거짓, 복잡도: 101
. 제한이 실제로 작동하려면 계획에 최대 한도를 도입해야 합니다:
GraphqlTutorialSchema 클래스 < GraphQL::스키마
쿼리 유형::쿼리 유형
돌연변이 유형::돌연변이 유형
GraphQL::Batch 사용
max_complicity 100
...
end
GraphQL 는 쿼리를 다르게 처리하며, 로컬에서 할 수 있는 작업과 비교하면 추적이 그렇게 간단하지 않습니다. 안타깝게도 랙 미니 프로파일러나 일반 SQL 로그는 모든 것을 알려주지 않으며 쿼리의 어느 부분이 주어진 시간 조각에 책임이 있는지도 알려주지 않습니다. GraphQL-Ruby의 경우 여기에서 제공되는 상용 솔루션을 사용할 수 있습니다: https://graphql-ruby.org/queries/tracing를 사용하거나 자체 추적을 준비해 보세요. 아래 스니펫은 로컬 추적기처럼 보입니다.
# lib/my_custom_tracer.rb
MyCustomTracer 클래스 'graphql.lex',
'parse' => 'graphql.parse',
'validate' => 'graphql.validate',
'analyze_query' => 'graphql.analyze_query',
'analyze_multiplex' => 'graphql.analyze_multiplex',
'실행_멀티플렉스' => '그래프QL.실행_멀티플렉스',
'실행_쿼리' => '그래프QL.실행_쿼리',
'execute_query_lazy' => 'graphql.execute_query_lazy'
}
def platform_trace(platform_key, key, _data, &block)
start = ::Process.clock_gettime ::Process::CLOCK_MONOTONIC
결과 = block.call
duration = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start
observe(platform_key, key, duration)
result
end
def platform_field_key(type, field)
"graphql.#{type.graphql_name}.#{field.graphql_name}"
end
def platform_authorized_key(type)
"graphql.authorized.#{type.graphql_name}"
end
def platform_resolve_type_key(type)
"graphql.resolve_type.#{type.graphql_name}"
end
def observe(platform_key, key, duration)
return if key == 'authorized'
"플랫폼_키: #{플랫폼_키}, 키: #{키}, 기간: #{(duration * 1000).round(5)} ms".yellow
end
end
설치도 매우 간단하며, 스키마에 추적기 정보를 포함하기만 하면 됩니다. 트레이서(MyCustomTracer.new)
구성이 필요합니다. 아래 예시와 같습니다:
# 그래프큐엘-루비/앱/그래프큐엘/그래프큐l_튜토리얼_스키마.rb
그래프큐 튜토리얼 스키마 클래스 < 그래프큐::스키마
쿼리 유형::쿼리 유형
돌연변이 유형::돌연변이 유형
GraphQL::Batch 사용
tracer(MyCustomTracer.new)
...
end
이러한 추적의 출력은 다음과 같습니다:
2021-06-17 22:02:44 +0200에서 ::1에 대해 "/graphql" POST를 시작했다.
(0.1ms) SELECT sqlite_version(*)
GraphqlController#execute로 처리 */*
파라미터 {"query"=>"{n 모든링크 {n idn urln 설명n createdAtn 게시자 {n idn 이름 링크 {n idn }n }n }n}", "graphql"=>{"query"=>"{n 모든링크 {n idn urln 설명n createdAtn 게시자 {n idn 이름 링크 {n idn }n }n}", "}}"}}
플랫폼_키: graphql.lex, 키: lex, 기간: 0.156ms
platform_key: graphql.parse, key: parse, duration: 0.108 ms
platform_key: graphql.validate, key: validate, duration: 0.537 ms
platform_key: graphql.analyze_query, key: analyze_query, duration: 0.123 ms
플랫폼_키: 그래프QL_분석_멀티플렉스, 키: 분석_멀티플렉스, 기간: 0.159ms
링크 로드(0.4ms) SELECT "links".* FROM "links"
↳ 앱/그래프QL/그래프QL 튜토리얼 스키마.rb:21:in `플랫폼_트레이스'
platform_key: graphql.execute_query, key: execute_query, duration: 15.562ms
↳ app/graphql/loaders/record_loader.rb:12:in `perform'
↳ app/graphql/loaders/association_loader.rb:46:in `preload_association'
플랫폼_키: graphql.execute_query_lazy, 키: 실행_쿼리_레이지, 지속 시간: 14.12ms
플랫폼_키: graphql.execute_multiplex, 키: execute_multiplex, 기간: 14.12ms 31.11ms
48ms에 200 OK 완료(조회: 1.2ms | 액티브 레코드: 2.0ms | 할당: 40128)
GraphQL 는 더 이상 새로운 기술이 아니지만, 라이브러리에 포함되지 않으면 문제에 대한 해결책이 완전히 표준화되지 않습니다. 이 기술의 구현은 프로젝트 는 프론트엔드와 상호 작용할 수 있는 많은 기회를 제공하며, 개인적으로 REST API가 제공하는 것과 관련하여 새로운 품질이라고 생각합니다.