GraphQL의 가장 핵심적인 기능 중 하나는 클라이언트가 필요한 데이터를 명확하게 요청할 수 있는 쿼리(Query) 문법이다.
GraphQL 쿼리를 사용하면 서버가 제공하는 데이터 구조 내에서 클라이언트가 원하는 데이터만 정확히 받아올 수 있다.
GraphQL 쿼리란?
GraphQL 쿼리는 클라이언트가 서버에 보내는 데이터 요청으로, REST API와는 다르게, GraphQL에서는 어떤 데이터가 필요한지 클라이언트가 명확히 정의할 수 있다.
예시
query {
user(id: 1) {
id
name
email
}
}
- 사용자 ID가 1인 유저의 ID, 이름, 이메일을 요청하는 쿼리이다.
쿼리 기본 구문
GraphQL 쿼리는 중괄호 {}를 사용해 요청할 필드를 명시하는 방식이다.
query GetUser {
user(id: 1) {
name
email
}
}
- query: 쿼리 유형을 선언 (생략 가능)
- GetUser: 쿼리 이름, 디버깅과 로깅에 유용
- user: 서버에서 정의된 쿼리 이름
- {}: 요청하고자 하는 하위 필드들
필드 선택과 별칭(Alias) 사용
GraphQL의 유용한 기능 중 하나는 필드 선택과 별칭(alias)이다.
query {
user1: user(id: 1) { name }
user2: user(id: 2) { name }
}
동일한 user 쿼리를 두 번 호출하지만, 별칭을 통해 응답 필드가 충돌하지 않도록 구성한다.
별칭이 유용한 경우는 다음과 같다.
- 동일한 쿼리를 여러 번 호출할 때
- 서로 다른 인수를 가진 동일 쿼리를 구분할 때
- 클라이언트에서 결과를 구분 지을 필요가 있을 때
GraphQL 서버 스키마와의 상호작용
GraphQL은 서버 스키마(Schema)를 기준으로 동작하며, API에서 제공 가능한 데이터 구조와 그 관계를 정의한다.
스키마 예시
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
users: [User]
}
- 스키마에는 객체 타입, 필드, 리턴 타입, 인수 등이 포함된다.
클라이언트 요청 예시
query {
users {
id
name
}
}
- 요청한 필드(id, name)만 반환된다.
Introspection Query (스키마 조회)
GIntrospection Query는 GraphQL 서버의 스키마 정보를 조회할 수 있는 특별한 쿼리이다.
즉, "서버는 어떤 타입을 지원하나요?", "이 타입은 어떤 필드를 가지고 있나요?" 같은 질문을 클라이언트가 직접 물어볼 수 있게 해 준다.
이 방식은 다음과 같은 상황에서 유용하다.
- GraphQL Playground, Apollo Studio, GraphiQL 같은 툴에서 스키마 자동 완성 기능
- 코드 자동 생성 도구에서 타입 정보 수집
- API 문서화 또는 스키마 분석
항목 | 설명 |
__type(name: "타입이름") | 특정 타입에 대한 상세 조회 |
__schema | 전체 스키마 구조, 루트 타입, 디렉티브 등 메타 정보 조회 |
Introspection Query 예시 (__schema)
📥 요청 (Query)
{
__schema {
types {
name
kind
description
}
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
directives {
name
description
}
}
}
항목 | 설명 |
types | 전체 타입 목록 (객체 타입, 스칼라, 유니온, 인터페이스 등) |
queryType | 루트 쿼리 타입의 이름 (보통 Query) |
mutationType | 루트 뮤테이션 타입의 이름 (없으면 null) |
subscriptionType | 루트 구독 타입 이름 (없으면 null) |
directives | 사용 가능한 디렉티브 목록 (@include, @skip 등) |
📤 응답 (Response)
{
"data": {
"__schema": {
"types": [
{
"name": "User",
"kind": "OBJECT",
"description": "A user in the system"
},
{
"name": "Post",
"kind": "OBJECT",
"description": "A blog post"
},
{
"name": "String",
"kind": "SCALAR",
"description": null
}
],
"queryType": {
"name": "Query"
},
"mutationType": {
"name": "Mutation"
},
"subscriptionType": null,
"directives": [
{
"name": "include",
"description": "Includes this field only when the `if` argument is true."
},
{
"name": "skip",
"description": "Skips this field when the `if` argument is true."
}
]
}
}
}
Introspection Query 예시 (__type)
📥 요청 (Query)
{
__type(name: "User") {
name
kind
fields {
name
type {
name
kind
}
}
}
}
- 타입 이름과 종류 (OBJECT, SCALAR, INTERFACE 등)
- 해당 타입이 가지는 필드들
- 각 필드의 이름과 타입 정보
📤 응답 (Response)
{
"data": {
"__type": {
"name": "User",
"kind": "OBJECT",
"fields": [
{
"name": "id",
"type": {
"name": null,
"kind": "NON_NULL"
}
},
{
"name": "name",
"type": {
"name": "String",
"kind": "SCALAR"
}
},
{
"name": "email",
"type": {
"name": "String",
"kind": "SCALAR"
}
}
]
}
}
}
- User 타입은 OBJECT
- id, name, email이라는 필드가 있다.
- name과 email은 String 스칼라 타입이다.
- id는 NON_NULL 타입으로, null이 허용되지 않는 필드임을 나타낸다.
🔧 쿼리 구성 요소
GraphQL 쿼리는 여러 구성 요소로 이루어져 있으며, 각각은 유연한 데이터 요청을 가능하게 한다.
1️⃣ 필드 (Fields)
GraphQL의 가장 기본 단위는 필드이다.
클라이언트는 필드를 통해 필요한 데이터를 선택적으로 요청할 수 있다.
{
user(id: 1) {
id
name
email
}
}
- user는 쿼리의 루트 필드
- { id, name, email }은 하위 필드
- 중첩 필드를 통해 관계형 데이터를 계층적으로 요청 가능
2️⃣ 인수 (Arguments)
Arguments는 필드에 값을 전달할 때 사용된다.
REST API의 쿼리 파라미터처럼 작동하지만, GraphQL에서는 특정 필드에만 국한된다.
{
post(id: 42) {
title
content
}
}
- id: 42는 post 필드에 전달될 인수
- 서버에서는 이 인수를 기반으로 데이터를 필터링하거나 조회
⚠️ 모든 인수가 필수는 아니며, 스키마 정의에 따라 optional 할 수 있다.
3️⃣ 별칭 (Alias)
Alias는 동일한 필드를 여러 번 호출하거나, 응답 필드 이름을 커스터마이징 할 때 사용한다.
{
first: user(id: 1) { name }
second: user(id: 2) { name }
}
응답 결과
{
"first": { "name": "Alice" },
"second": { "name": "Bob" }
}
✅ 같은 필드를 여러 조건으로 요청할 때 유용하다.
4️⃣ 변수 (Variables)
Variables를 사용하면 쿼리를 동적으로 재사용할 수 있다.
서버로 전송할 실제 값은 JSON 형식으로 분리되어 전달된다.
쿼리 작성
query GetUser($userId: ID!) {
user(id: $userId) {
name
}
}
JSON 변수 전달
{
"userId": 1
}
✅ 변수는 보안에도 유리하며, 쿼리를 재사용할 수 있어 유지보수에 좋다.
5️⃣ 프래그먼트 (Fragments)
Fragment는 여러 쿼리 또는 동일 쿼리 내에서 반복되는 필드를 정의해 재사용할 수 있도록 도와준다.
Fragment 정의 및 사용
fragment UserInfo on User {
id
name
email
}
query {
users {
...UserInfo
}
}
✅ 코드 중복을 줄이고, 쿼리를 모듈화 하여 가독성과 유지보수성 향상
6️⃣ 인라인 프래그먼트 (Inline Fragments)
Interface나 Union 타입을 다룰 때 특정 타입에 따라 다른 필드를 요청할 수 있도록 도와준다.
{
search(query: "GraphQL") {
... on User {
name
}
... on Post {
title
}
}
}
- search 필드는 User 또는 Post 타입일 수 있다
- 타입에 따라 조건부 필드를 요청 가능
✅ 다형성(polymorphism)을 쿼리 수준에서 표현할 수 있는 강력한 기능이다.
7️⃣ 디렉티브 (Directives)
디렉티브는 쿼리의 실행 방식을 동적으로 제어한다.
가장 일반적인 디렉티브는 @include와 @skip이다.
query GetUser($showEmail: Boolean!) {
user(id: 1) {
name
email @include(if: $showEmail)
}
}
- @include(if: true) -> 해당 필드 포함
- @skip(if: true) -> 해당 필드 생략
✅ 조건부 렌더링을 서버로 넘김으로써 네트워크 효율을 극대화할 수 있다.
내부 타입 시스템 소개
GraphQL은 내장 타입과 사용자 정의 타입을 통해 구조화된 데이터 요청을 지원한다.
사용자 정의 객체 타입(User-defined Object Types)
type Post {
id: ID!
title: String!
content: String
}
- 개발자가 직접 정의한 데이터 구조
내장 스칼라 타입
- Int, Float, String, Boolean, ID
✍️ 내부 타입 정의 방식
GraphQL의 타입은 SDL(Schema Definition Language)을 통해 정의되며, 다음과 같은 방식으로 활용된다.
- 객체 타입: type, field, resolver 조합
- 내장 스칼라 타입: 별도 정의 없이 즉시 사용
⚙ 리졸버 (Resolver) 개념과 종류
리졸버란?
리졸버는 쿼리에서 요청한 데이터를 어떻게 가져올지를 정의하는 함수이다. 각 필드에 리졸버가 연결되어 있으며, 해당 필드가 요청될 때 실행된다.
📘 DataFetcher
- 각 필드에 단독으로 데이터를 가져오는 함수
- 단순한 API 응답 처리에 유용
📗 DataLoader
- 여러 요청을 배치(batch)로 묶어 처리
- 중복 데이터 요청을 줄이고, DB 호출 수를 최적화함
- 캐싱도 지원
🚨 리졸버 사용 시 주의점: N + 1 문제
GraphQL을 사용하다 보면 흔히 마주하게 되는 성능 문제 중 하나가 바로 N + 1 문제이다. 이 문제는 특히 관계형 데이터를 다룰 때 발생하며, 아무런 최적화 없이 쿼리를 작성하면 불필요하게 많은 DB 쿼리가 실행되어 성능 저하로 이어질 수 있다.
N + 1 문제란?
N + 1 문제란, 하나의 루트 쿼리에 대해 N개의 하위 요청이 발생하여 총 N + 1번의 데이터베이스 쿼리가 실행되는 비효율적인 상황을 의미한다.
이 문제는 GraphQL의 필드 단위 리졸버 구조에서 쉽게 발생한다. 루트 쿼리에서 여러 개의 항목을 가져오고, 각 항목에 대해 또 다른 데이터(연관된 객체)를 가져올게 될 때 문제는 시작된다.
예시: 게시글(Post)과 작성자(User) 데이터를 가져오는 쿼리
query {
posts {
id
title
author {
id
name
}
}
}
- posts: 게시글 리스트
- author: 각 게시글의 작성자
⚙️ 잘못된 리졸버 예시 (N + 1 발생)
@Component
public class PostResolver implements GraphQLResolver<Post> {
@Autowired
private UserRepository userRepository;
public User getAuthor(Post post) {
// 게시글마다 DB 호출 → N + 1 문제 발생
return userRepository.findById(post.getAuthorId()).orElse(null);
}
}
- 게시글 10개 조회 -> findById()가 10번 호출됨
- 총 쿼리 수: 1 (게시글) + 10 (작성자) = 11번
🚨게시글이 100개라면 101번의 쿼리 발생 -> 성능 급감..
💡 해결 방법: DataLoader 사용

DataLoader는 여러 개의 ID를 batch로 묶어 한 번에 쿼리하고, 결과를 매핑하여 반환한다.
GraphQL Java는 MappedBatchLoader를 사용한다.
1️⃣ MappedBatchLoader 정의
@Component
public class UserDataLoader implements MappedBatchLoader<Long, User> {
@Autowired
private UserRepository userRepository;
@Override
public CompletionStage<Map<Long, User>> load(Set<Long> userIds) {
return CompletableFuture.supplyAsync(() -> {
List<User> users = userRepository.findAllById(userIds);
return users.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
});
}
}
- MappedBatchLoader는 요청된 ID들을 모아서 한 번에 처리하는 함수이다.
- 결과는 Map<ID, User> 형태로 반환되며, 각 ID에 대해 빠르게 매칭할 수 있다.
2️⃣ DataLoaderRegistry 등록
@Component
public class DataLoaderRegistryFactory {
public static final String USER_DATA_LOADER = "USER_DATA_LOADER";
@Autowired
private UserDataLoader userDataLoader;
public DataLoaderRegistry create() {
DataLoaderRegistry registry = new DataLoaderRegistry();
registry.register(USER_DATA_LOADER, DataLoader.newMappedDataLoader(userDataLoader));
return registry;
}
}
- DataLoaderRegistry는 요청 단위로 DataLoader를 보관하는 저장소이다.
- GraphQL 실행 시 이 Registry에 등록된 모든 DataLoader가 함께 사용된다.
3️⃣ 요청 컨텍스트에 연결
@Component
public class GraphQLContextBuilderImpl implements GraphQLServletContextBuilder {
@Autowired
private DataLoaderRegistryFactory registryFactory;
@Override
public GraphQLContext build(HttpServletRequest request, HttpServletResponse response) {
return DefaultGraphQLServletContext.createServletContext()
.withDataLoaderRegistry(registryFactory.create())
.build();
}
// WebSocket 지원 시 build(Session)도 구현 필요
}
- 요청마다 새로운 DataLoaderRegistry를 생성하여 GraphQL 컨텍스트에 주입한다.
- 이를 통해 DataLoader는 요청 범위 스코프를 유지한다.
4️⃣ 리졸버에서 DataLoader 사용
@Component
public class PostResolver implements GraphQLResolver<Post> {
public CompletableFuture<User> getAuthor(Post post, DataFetchingEnvironment env) {
DataLoader<Long, User> userLoader = env.getDataLoader(DataLoaderRegistryFactory.USER_DATA_LOADER);
return userLoader.load(post.getAuthorId());
}
}
- env.getDataLoader()로 등록된 DataLoader 인스턴스를 가져온다.
- 이후 작성자 ID를 전달하면, 내부적으로 batch 처리되어 효율적인 조회가 가능하다.
- 게시글 10개를 요청하더라도 작성자 조회는 단 1번의 DB 호출로 해결 가능하다.
DataLoader 사용 시 주의사항
항목 | 설명 |
요청 범위 | 요청마다 새로운 DataLoaderRegistry 인스턴스를 생성해야 함 |
캐싱 | 동일한 키로 여러 번 호출해도 재요청되지 않음 |
순서 보장 | MappedBatchLoader는 요청 순서에 맞게 결과를 매핑함 |
비동기 처리 | 리졸버에서 CompltableFuter<T>를 반환해야 정상 동작함 |
전역 사용 금지 | DataLoader는 요청 스코프이므로 전역 빈으로 재사용하면 안 됨 |
'기술(Tech) > Network & System' 카테고리의 다른 글
[GraphQL] GraphQL 쿼리(Query)와 뮤테이션(Mutation) 작성법 (0) | 2025.03.17 |
---|---|
[GraphQL] GraphQL 쿼리(Query) vs 뮤테이션(Mutation) (0) | 2025.03.16 |
[GraphQL] GraphQL의 스키마와 타입 시스템 (0) | 2025.03.16 |
[GraphQL] GraphQL 개요 (0) | 2025.03.15 |
[gRPC] gRPC의 보안 계층 🔒 (0) | 2025.03.15 |
GraphQL의 가장 핵심적인 기능 중 하나는 클라이언트가 필요한 데이터를 명확하게 요청할 수 있는 쿼리(Query) 문법이다.
GraphQL 쿼리를 사용하면 서버가 제공하는 데이터 구조 내에서 클라이언트가 원하는 데이터만 정확히 받아올 수 있다.
GraphQL 쿼리란?
GraphQL 쿼리는 클라이언트가 서버에 보내는 데이터 요청으로, REST API와는 다르게, GraphQL에서는 어떤 데이터가 필요한지 클라이언트가 명확히 정의할 수 있다.
예시
query {
user(id: 1) {
id
name
email
}
}
- 사용자 ID가 1인 유저의 ID, 이름, 이메일을 요청하는 쿼리이다.
쿼리 기본 구문
GraphQL 쿼리는 중괄호 {}를 사용해 요청할 필드를 명시하는 방식이다.
query GetUser {
user(id: 1) {
name
email
}
}
- query: 쿼리 유형을 선언 (생략 가능)
- GetUser: 쿼리 이름, 디버깅과 로깅에 유용
- user: 서버에서 정의된 쿼리 이름
- {}: 요청하고자 하는 하위 필드들
필드 선택과 별칭(Alias) 사용
GraphQL의 유용한 기능 중 하나는 필드 선택과 별칭(alias)이다.
query {
user1: user(id: 1) { name }
user2: user(id: 2) { name }
}
동일한 user 쿼리를 두 번 호출하지만, 별칭을 통해 응답 필드가 충돌하지 않도록 구성한다.
별칭이 유용한 경우는 다음과 같다.
- 동일한 쿼리를 여러 번 호출할 때
- 서로 다른 인수를 가진 동일 쿼리를 구분할 때
- 클라이언트에서 결과를 구분 지을 필요가 있을 때
GraphQL 서버 스키마와의 상호작용
GraphQL은 서버 스키마(Schema)를 기준으로 동작하며, API에서 제공 가능한 데이터 구조와 그 관계를 정의한다.
스키마 예시
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
users: [User]
}
- 스키마에는 객체 타입, 필드, 리턴 타입, 인수 등이 포함된다.
클라이언트 요청 예시
query {
users {
id
name
}
}
- 요청한 필드(id, name)만 반환된다.
Introspection Query (스키마 조회)
GIntrospection Query는 GraphQL 서버의 스키마 정보를 조회할 수 있는 특별한 쿼리이다.
즉, "서버는 어떤 타입을 지원하나요?", "이 타입은 어떤 필드를 가지고 있나요?" 같은 질문을 클라이언트가 직접 물어볼 수 있게 해 준다.
이 방식은 다음과 같은 상황에서 유용하다.
- GraphQL Playground, Apollo Studio, GraphiQL 같은 툴에서 스키마 자동 완성 기능
- 코드 자동 생성 도구에서 타입 정보 수집
- API 문서화 또는 스키마 분석
항목 | 설명 |
__type(name: "타입이름") | 특정 타입에 대한 상세 조회 |
__schema | 전체 스키마 구조, 루트 타입, 디렉티브 등 메타 정보 조회 |
Introspection Query 예시 (__schema)
📥 요청 (Query)
{
__schema {
types {
name
kind
description
}
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
directives {
name
description
}
}
}
항목 | 설명 |
types | 전체 타입 목록 (객체 타입, 스칼라, 유니온, 인터페이스 등) |
queryType | 루트 쿼리 타입의 이름 (보통 Query) |
mutationType | 루트 뮤테이션 타입의 이름 (없으면 null) |
subscriptionType | 루트 구독 타입 이름 (없으면 null) |
directives | 사용 가능한 디렉티브 목록 (@include, @skip 등) |
📤 응답 (Response)
{
"data": {
"__schema": {
"types": [
{
"name": "User",
"kind": "OBJECT",
"description": "A user in the system"
},
{
"name": "Post",
"kind": "OBJECT",
"description": "A blog post"
},
{
"name": "String",
"kind": "SCALAR",
"description": null
}
],
"queryType": {
"name": "Query"
},
"mutationType": {
"name": "Mutation"
},
"subscriptionType": null,
"directives": [
{
"name": "include",
"description": "Includes this field only when the `if` argument is true."
},
{
"name": "skip",
"description": "Skips this field when the `if` argument is true."
}
]
}
}
}
Introspection Query 예시 (__type)
📥 요청 (Query)
{
__type(name: "User") {
name
kind
fields {
name
type {
name
kind
}
}
}
}
- 타입 이름과 종류 (OBJECT, SCALAR, INTERFACE 등)
- 해당 타입이 가지는 필드들
- 각 필드의 이름과 타입 정보
📤 응답 (Response)
{
"data": {
"__type": {
"name": "User",
"kind": "OBJECT",
"fields": [
{
"name": "id",
"type": {
"name": null,
"kind": "NON_NULL"
}
},
{
"name": "name",
"type": {
"name": "String",
"kind": "SCALAR"
}
},
{
"name": "email",
"type": {
"name": "String",
"kind": "SCALAR"
}
}
]
}
}
}
- User 타입은 OBJECT
- id, name, email이라는 필드가 있다.
- name과 email은 String 스칼라 타입이다.
- id는 NON_NULL 타입으로, null이 허용되지 않는 필드임을 나타낸다.
🔧 쿼리 구성 요소
GraphQL 쿼리는 여러 구성 요소로 이루어져 있으며, 각각은 유연한 데이터 요청을 가능하게 한다.
1️⃣ 필드 (Fields)
GraphQL의 가장 기본 단위는 필드이다.
클라이언트는 필드를 통해 필요한 데이터를 선택적으로 요청할 수 있다.
{
user(id: 1) {
id
name
email
}
}
- user는 쿼리의 루트 필드
- { id, name, email }은 하위 필드
- 중첩 필드를 통해 관계형 데이터를 계층적으로 요청 가능
2️⃣ 인수 (Arguments)
Arguments는 필드에 값을 전달할 때 사용된다.
REST API의 쿼리 파라미터처럼 작동하지만, GraphQL에서는 특정 필드에만 국한된다.
{
post(id: 42) {
title
content
}
}
- id: 42는 post 필드에 전달될 인수
- 서버에서는 이 인수를 기반으로 데이터를 필터링하거나 조회
⚠️ 모든 인수가 필수는 아니며, 스키마 정의에 따라 optional 할 수 있다.
3️⃣ 별칭 (Alias)
Alias는 동일한 필드를 여러 번 호출하거나, 응답 필드 이름을 커스터마이징 할 때 사용한다.
{
first: user(id: 1) { name }
second: user(id: 2) { name }
}
응답 결과
{
"first": { "name": "Alice" },
"second": { "name": "Bob" }
}
✅ 같은 필드를 여러 조건으로 요청할 때 유용하다.
4️⃣ 변수 (Variables)
Variables를 사용하면 쿼리를 동적으로 재사용할 수 있다.
서버로 전송할 실제 값은 JSON 형식으로 분리되어 전달된다.
쿼리 작성
query GetUser($userId: ID!) {
user(id: $userId) {
name
}
}
JSON 변수 전달
{
"userId": 1
}
✅ 변수는 보안에도 유리하며, 쿼리를 재사용할 수 있어 유지보수에 좋다.
5️⃣ 프래그먼트 (Fragments)
Fragment는 여러 쿼리 또는 동일 쿼리 내에서 반복되는 필드를 정의해 재사용할 수 있도록 도와준다.
Fragment 정의 및 사용
fragment UserInfo on User {
id
name
email
}
query {
users {
...UserInfo
}
}
✅ 코드 중복을 줄이고, 쿼리를 모듈화 하여 가독성과 유지보수성 향상
6️⃣ 인라인 프래그먼트 (Inline Fragments)
Interface나 Union 타입을 다룰 때 특정 타입에 따라 다른 필드를 요청할 수 있도록 도와준다.
{
search(query: "GraphQL") {
... on User {
name
}
... on Post {
title
}
}
}
- search 필드는 User 또는 Post 타입일 수 있다
- 타입에 따라 조건부 필드를 요청 가능
✅ 다형성(polymorphism)을 쿼리 수준에서 표현할 수 있는 강력한 기능이다.
7️⃣ 디렉티브 (Directives)
디렉티브는 쿼리의 실행 방식을 동적으로 제어한다.
가장 일반적인 디렉티브는 @include와 @skip이다.
query GetUser($showEmail: Boolean!) {
user(id: 1) {
name
email @include(if: $showEmail)
}
}
- @include(if: true) -> 해당 필드 포함
- @skip(if: true) -> 해당 필드 생략
✅ 조건부 렌더링을 서버로 넘김으로써 네트워크 효율을 극대화할 수 있다.
내부 타입 시스템 소개
GraphQL은 내장 타입과 사용자 정의 타입을 통해 구조화된 데이터 요청을 지원한다.
사용자 정의 객체 타입(User-defined Object Types)
type Post {
id: ID!
title: String!
content: String
}
- 개발자가 직접 정의한 데이터 구조
내장 스칼라 타입
- Int, Float, String, Boolean, ID
✍️ 내부 타입 정의 방식
GraphQL의 타입은 SDL(Schema Definition Language)을 통해 정의되며, 다음과 같은 방식으로 활용된다.
- 객체 타입: type, field, resolver 조합
- 내장 스칼라 타입: 별도 정의 없이 즉시 사용
⚙ 리졸버 (Resolver) 개념과 종류
리졸버란?
리졸버는 쿼리에서 요청한 데이터를 어떻게 가져올지를 정의하는 함수이다. 각 필드에 리졸버가 연결되어 있으며, 해당 필드가 요청될 때 실행된다.
📘 DataFetcher
- 각 필드에 단독으로 데이터를 가져오는 함수
- 단순한 API 응답 처리에 유용
📗 DataLoader
- 여러 요청을 배치(batch)로 묶어 처리
- 중복 데이터 요청을 줄이고, DB 호출 수를 최적화함
- 캐싱도 지원
🚨 리졸버 사용 시 주의점: N + 1 문제
GraphQL을 사용하다 보면 흔히 마주하게 되는 성능 문제 중 하나가 바로 N + 1 문제이다. 이 문제는 특히 관계형 데이터를 다룰 때 발생하며, 아무런 최적화 없이 쿼리를 작성하면 불필요하게 많은 DB 쿼리가 실행되어 성능 저하로 이어질 수 있다.
N + 1 문제란?
N + 1 문제란, 하나의 루트 쿼리에 대해 N개의 하위 요청이 발생하여 총 N + 1번의 데이터베이스 쿼리가 실행되는 비효율적인 상황을 의미한다.
이 문제는 GraphQL의 필드 단위 리졸버 구조에서 쉽게 발생한다. 루트 쿼리에서 여러 개의 항목을 가져오고, 각 항목에 대해 또 다른 데이터(연관된 객체)를 가져올게 될 때 문제는 시작된다.
예시: 게시글(Post)과 작성자(User) 데이터를 가져오는 쿼리
query {
posts {
id
title
author {
id
name
}
}
}
- posts: 게시글 리스트
- author: 각 게시글의 작성자
⚙️ 잘못된 리졸버 예시 (N + 1 발생)
@Component
public class PostResolver implements GraphQLResolver<Post> {
@Autowired
private UserRepository userRepository;
public User getAuthor(Post post) {
// 게시글마다 DB 호출 → N + 1 문제 발생
return userRepository.findById(post.getAuthorId()).orElse(null);
}
}
- 게시글 10개 조회 -> findById()가 10번 호출됨
- 총 쿼리 수: 1 (게시글) + 10 (작성자) = 11번
🚨게시글이 100개라면 101번의 쿼리 발생 -> 성능 급감..
💡 해결 방법: DataLoader 사용

DataLoader는 여러 개의 ID를 batch로 묶어 한 번에 쿼리하고, 결과를 매핑하여 반환한다.
GraphQL Java는 MappedBatchLoader를 사용한다.
1️⃣ MappedBatchLoader 정의
@Component
public class UserDataLoader implements MappedBatchLoader<Long, User> {
@Autowired
private UserRepository userRepository;
@Override
public CompletionStage<Map<Long, User>> load(Set<Long> userIds) {
return CompletableFuture.supplyAsync(() -> {
List<User> users = userRepository.findAllById(userIds);
return users.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
});
}
}
- MappedBatchLoader는 요청된 ID들을 모아서 한 번에 처리하는 함수이다.
- 결과는 Map<ID, User> 형태로 반환되며, 각 ID에 대해 빠르게 매칭할 수 있다.
2️⃣ DataLoaderRegistry 등록
@Component
public class DataLoaderRegistryFactory {
public static final String USER_DATA_LOADER = "USER_DATA_LOADER";
@Autowired
private UserDataLoader userDataLoader;
public DataLoaderRegistry create() {
DataLoaderRegistry registry = new DataLoaderRegistry();
registry.register(USER_DATA_LOADER, DataLoader.newMappedDataLoader(userDataLoader));
return registry;
}
}
- DataLoaderRegistry는 요청 단위로 DataLoader를 보관하는 저장소이다.
- GraphQL 실행 시 이 Registry에 등록된 모든 DataLoader가 함께 사용된다.
3️⃣ 요청 컨텍스트에 연결
@Component
public class GraphQLContextBuilderImpl implements GraphQLServletContextBuilder {
@Autowired
private DataLoaderRegistryFactory registryFactory;
@Override
public GraphQLContext build(HttpServletRequest request, HttpServletResponse response) {
return DefaultGraphQLServletContext.createServletContext()
.withDataLoaderRegistry(registryFactory.create())
.build();
}
// WebSocket 지원 시 build(Session)도 구현 필요
}
- 요청마다 새로운 DataLoaderRegistry를 생성하여 GraphQL 컨텍스트에 주입한다.
- 이를 통해 DataLoader는 요청 범위 스코프를 유지한다.
4️⃣ 리졸버에서 DataLoader 사용
@Component
public class PostResolver implements GraphQLResolver<Post> {
public CompletableFuture<User> getAuthor(Post post, DataFetchingEnvironment env) {
DataLoader<Long, User> userLoader = env.getDataLoader(DataLoaderRegistryFactory.USER_DATA_LOADER);
return userLoader.load(post.getAuthorId());
}
}
- env.getDataLoader()로 등록된 DataLoader 인스턴스를 가져온다.
- 이후 작성자 ID를 전달하면, 내부적으로 batch 처리되어 효율적인 조회가 가능하다.
- 게시글 10개를 요청하더라도 작성자 조회는 단 1번의 DB 호출로 해결 가능하다.
DataLoader 사용 시 주의사항
항목 | 설명 |
요청 범위 | 요청마다 새로운 DataLoaderRegistry 인스턴스를 생성해야 함 |
캐싱 | 동일한 키로 여러 번 호출해도 재요청되지 않음 |
순서 보장 | MappedBatchLoader는 요청 순서에 맞게 결과를 매핑함 |
비동기 처리 | 리졸버에서 CompltableFuter<T>를 반환해야 정상 동작함 |
전역 사용 금지 | DataLoader는 요청 스코프이므로 전역 빈으로 재사용하면 안 됨 |
'기술(Tech) > Network & System' 카테고리의 다른 글
[GraphQL] GraphQL 쿼리(Query)와 뮤테이션(Mutation) 작성법 (0) | 2025.03.17 |
---|---|
[GraphQL] GraphQL 쿼리(Query) vs 뮤테이션(Mutation) (0) | 2025.03.16 |
[GraphQL] GraphQL의 스키마와 타입 시스템 (0) | 2025.03.16 |
[GraphQL] GraphQL 개요 (0) | 2025.03.15 |
[gRPC] gRPC의 보안 계층 🔒 (0) | 2025.03.15 |