https://sihanni.tistory.com/190
[GraphQL] 개념 정리 (REST API와 비교)
GraphQL 이란?GraphQL은 페이스북에서 만든 쿼리 언어이며 서버 측 런타임이다. 클라이언트가 필요한 데이터만 요청할 수 있도록 설계되어 있어, REST API보다 유연하고 효율적인 데이터 fetching이 가능
sihanni.tistory.com
GraphQL을 간단히 정리해보았지만 아직 잘 와닿지 않아서 이번에 진행한 프로젝트인 티켓 예매 시스템의 어드민을 GraphQL을 사용해서 개발해보기로 했다.
간단하게 관리자가 한 화면에서 사용자, 이벤트, 좌석, 예약, 결제 정보를 조회하고, 필요에 따라 강제 취소 등을 수행하게 할 계획이다.
*코드 퍼스트 스키마 방식으로 필드 리졸버, DataLoader을 사용한 N+1 제거, 커서 페이지네이션, 정렬/필터 등을 직접 경험해보는 것이 목표다.
graphQL 학습에 티켓 예매 시스템 어드민을 생각한 이유는 먼저 로컬에서 테스트해본 수만의 유저와 예약 데이터가 존재하기 때문이었다.그리고 어드민이라면 원래라면 다양한 endpoint를 통해 여러 호출을 했을테지만 GraphQL을 사용해서 원하는 필드만 한 번에 선택해 가져오는 것을 효과적으로 체감할 수 있을 것 같았다.
INFO
- 원래라면 어드민에서는 데이터 조회를 읽기 전용 DB에서 하는 것이 좋을 것 같은데, 아직 티켓 예매 시스템은 로컬단계에서 테스트를 해서 그 구분이 되어있지는 않다.
- 페이지네이션은 keyset 방식을 사용(현재 유저 데이터는 6만이 넘어가고, 좌석도 1000개가 넘는 공연장 데이터가 있고, 예약 데이터도 수만개가 존재)
- N+1 방지를 위해 DataLoader을 사용
어드민 기능 구성
1. 예약 리스트
- 공연별 예약 목록 확인
- 기능: 예약 확정, 강제 취소
2. 공연, 공연장, 좌석 요약
- 공연 목록/상세, 오픈일/기간 필터
3. 사용자 조회
- 이메일/가입일/이름 등 조회
- 예약 이력 확인
- 결제 이력 확인
작업
- 서버 세팅
- 예약 리스트 완성: 커서 페이지네이션, 필터, 연결 필드(DataLoader), 확정/강취 뮤테이션(REST 연계), ADMIN 가드
- 공연/공연장/좌석 요약: 이벤트 필터/요약, 좌석 페이지네이션
- 사용자 조회/이력: 사용자 필터/페이지네이션, 예약/결제 이력 조인
- 실시간·가드레일 고도화: Subscription, Complexity/레이트 리밋, 감사 로그
- 프론트 개발
1. 서버 세팅
// app.module.ts 일부
import { GraphQLModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true, // code-first → SDL 자동 생성
sortSchema: true,
playground: true,
Apollo?
Apllo Server (@nestjs/graphql)
GraphQL 서버를 쉽게 만들게 도와주는 라이브러리이다.
Apollo Server에서는 아래 기능 등을 제공해준다. NestJS → Apollo Server → GraphQL 형태의 처리 구조
- 스키마(SDL) 불러오기/빌드
- Query/Mutation/Subscription 처리
- 요청/응답 파싱
- GraphQL Playground(테스트 UI) 제공
- 에러 핸들링, 캐싱, 성능 추적 등
Apllo Client (@apollo/client)
프론트엔드에서 GraphQL 서버에 요청하고, 응답을 관리하는 라이브러리이다.
- Query/Mutation 요청 보내기
- 서버 응답 캐싱
- 로컬 상태 관리 (Redux 대체 가능)
- React, Vue, Angular 등 프레임워크와 바로 연동
GraphQL은 실제로 요청을 받아 처리하고, 스키마를 로드하고 호출하는 등을 직접 구현해야하는데, Apollo를 사용하면 이 부분을 표준화해주고 편의 기능을 제공해주기 때문에 서버의 경우 스키마와 리졸버만 만들면 바로 실행이 가능해지고, 클라이언트 측에서는 useQuery, useMutation 같은 훅을 사용하면 편하게 요청이 가능해진다.
[프론트엔드] Apollo Client
↓ (GraphQL Query/Mutation)
[백엔드] Apollo Server (@nestjs/apollo로 NestJS에 붙임)
↓
[리졸버] 서비스 로직 실행(DB 조회/변경)
2. 공연별 예약 목록을 Keyset(커서) 페이지네이션으로 조회
그리고 사용자, 공연, 좌석, 결제 정보를 필드 리졸버와 DataLoader로 합성하다.
import { Injectable, Scope } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { InjectDataSource } from '@nestjs/typeorm';
import DataLoader from 'dataloader';
@Injectable({ scope: Scope.REQUEST })
export class DataloadersFactory {
constructor(
@InjectDataSource('USER_DB') private readonly userDb: DataSource,
@InjectDataSource('EVENT_DB') private readonly eventDb: DataSource,
@InjectDataSource('VENUE_DB') private readonly venueDb: DataSource,
@InjectDataSource('PAYMENT_DB') private readonly paymentDb: DataSource,
) {}
userByIdLoader() {
return new DataLoader<number, UserRow | null>(
async (ids: readonly number[]) => {
const rows = (await this.userDb.query(
`SELECT id, email, name
FROM users
WHERE id IN (${ids.map(() => '?').join(',')})`,
ids as any[],
)) as UserRow[];
const map = new Map<number, UserRow>(rows.map((r) => [r.id, r]));
return ids.map((id) => map.get(id) ?? null) as (UserRow | null)[];
},
);
}
DataLoader을 사용하면 GraphQL이나 요청 처리 중에 중복 DB 쿼리를 자동으로 모아서, 한번에 가져오고 순서에 맞게 결과를 반환해주는 구조가 된다.
- Batching (일괄 처리): 같은 요청 컨텍스트 안에서, 여러 번 호출된 같은 타입의 조회를 하나의 쿼리로 합침.
- Caching (요청 단위 캐시): 같은 키로 조회하면 DB를 다시 안 찍고 이전 결과를 반환.
- 주로 GraphQL N+1 문제를 해결할 때 사용
요청과 응답 흐름
- 쿼리 요청
- GraphQL 모듈이 요청을 수신
- 인자 검증(ValidationPipe)
- Resolver 진입 → Service 호출
- Repository 질의
- Keyset 페이지네이션으로 예약 목록 조회
- 노드 조립(내부 FK 심기)
- 필드 리졸버 호출 & DataLoader 배치 로딩
- 최종 응답 조립
테스트


이렇게 하나의 엔드포인트에서 원하는 필드만 선택해서 응답을 받을 수 있다.
3. 프론트에서의 요청
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:3000/graphql',
cache: new InMemoryCache(),
});
async function run() {
const { data } = await client.query({
query: gql`
query {
reservations(first: 5, eventId: 6) {
nodes {
id
status
createdAt
event { id title }
user { id name email }
}
endCursor
hasNextPage
}
}
`,
});
console.log(data.reservations);
}
run();
4. 정리
GraphQL을 사용해서 예약 데이터를 가져오는 것을 직접 개발해보았다.
아직은 숙련도가 낮아서 지금까지 느끼기로는 서버쪽에서 오히려 쿼리를 다양한 패턴에 대응하기 위해서 유연성있게 개발을 해야해서 REST API 보다 뭔가 더 복잡하고 어려운 것 같다.
응답 두번째 이미지에서는 단 한번의 요청으로 예약자의 정보와 예약한 공연의 정보, 그리고 예약 상태를 모두 확인할 수 있는데,
REST API의 경우 최소 3번 API 요청을 해야할 것을 GraphQL로는 한번의 요청으로 끝냈다.
물론 REST API도 한번에 예약자, 공연 정보, 예약 정보를 주는 API를 만들순 있겠지만 불필요한 데이터까지 반환될 수 있다.
*Code-first VS Schema-first
- Schema-first(SDL): 먼저 **GraphQL 스키마 파일(.graphql)**을 작성하고, 그 스키마를 만족시키는 리졸버 코드를 구현하는 방식.
- Code-first(코드): TypeScript 클래스/데코레이터로 타입과 리졸버를 작성하면 스키마가 자동 생성되는 방식.
- 비교
- Schema-first
- schema.graphql에 타입/쿼리/뮤테이션을 선언
- 선언한 스키마를 기준으로 리졸버/서비스를 구현
- 스키마가 “계약서”, 코드는 “구현”
- 장점
- 명확한 계약 우선: 프론트/백이 스키마로 먼저 합의하기 쉬움.
- 도구 호환성: 순수 SDL이어서 언어/런타임 무관, Federation/Registry 도입 용이.
- 러닝커브 낮음: 스키마 문법이 간단해 입문자에게 직관적.
- 단점
- 이중 정의 위험: SDL과 TS 타입을 각각 관리하면 드리프트(불일치) 가능.
- 타입 안전성 낮음: TS 타입 추론과 분리되면 컴파일 타임 검증이 약해짐.
- 리팩토링 비용: 모델 변경 시 SDL과 코드 모두 수정해야 할 수 있음.
- Code-first
- TS 클래스/데코레이터로 모델/리졸버 작성 (@ObjectType, @Field, @Resolver 등)
- 빌드시 스키마(SDL)가 자동 생성
- 코드가 “단일 소스”, 스키마는 산출물
- 장점
- 타입 단일 출처: TS 타입이 곧 GraphQL 타입 → 자동 스키마 생성, 드리프트 감소.
- 강한 타입 안정성: 컴파일 타임에 오류 조기 발견.
- 리팩토링 친화적: IDE 리네임/자동완성/추론이 잘 작동.
- 단점
- 런타임/언어 종속: TS/데코레이터에 종속. 다언어 팀/폴리글랏 환경에 덜 유리.
- 스키마 협업 가시성↓: 비개발(기획/모바일) 이해관계자에겐 코드보다 SDL이 읽기 쉬움.
- 메타모델 한계: 복잡한 스키마 주석·지시문(Directive) 설계 시 데코레이터 표현이 번거로울 수 있음.
- 선택
- 프론트/백이 스키마 먼저 합의해야 하고, 여러 언어(예: iOS/Android/Go 백엔드)와 협업 → Schema-first 추천.
- NestJS + TypeScript 단일 스택, 코드 기반 리팩토링/자동완성/테스트를 중시 → Code-first 추천.
- **스키마 레지스트리/게이트웨이(Federation)**를 강하게 쓸 계획 → 대체로 Schema-first가 도입 쉬움(물론 Code-first도 가능하나 운영팀 문화에 따라).
- 입문 단계에서 “타입과 리졸버가 1파일 안에서 끝나면 편한” 접근 → Code-first가 시작하기 수월.
- Schema-first
'GraphQL' 카테고리의 다른 글
| [GraphQL] 개념 정리 (REST API와 비교) (0) | 2025.08.10 |
|---|