1. 프로젝트 세팅과 스키마 작성 ← 현재 문서
이번 시리즈에서는 도서 관리 GraphQL API를 처음부터 만든다. 첫 편에서는 Spring Boot 프로젝트를 생성하고, GraphQL 스키마를 작성한다.
예시 프로젝트 소개
만들 프로젝트는 BookStore GraphQL API다. 책과 저자를 관리하는 단순한 CRUD API지만, GraphQL의 핵심 기능을 전부 다룬다.
기술 스택
- Spring Boot 3.x, Java 17
- Spring GraphQL + GraphiQL
- Spring Data JPA + H2 (인메모리 DB)
- Lombok
핵심 기능
- 도서/저자 조회, 검색, 생성, 수정, 삭제 (Query + Mutation)
- N+1 문제 해결 (@BatchMapping)
- 실시간 알림 (Subscription + WebSocket)
- 전역 예외 처리
패키지 구조
com.codeit.graphql
├── config/ ← 초기 데이터 설정
├── controller/ ← REST 컨트롤러 (필요시)
├── entity/ ← JPA 엔티티 (Book, Author)
├── exception/ ← 커스텀 예외, 전역 핸들러
├── input/ ← GraphQL Input 타입 클래스
├── repository/ ← JPA Repository
├── resolver/ ← GraphQL Resolver (Query, Mutation, Subscription)
└── service/ ← 비즈니스 로직
단계별로 만들기
단계 1 — Spring Boot 프로젝트 생성
Spring Initializr에서 프로젝트를 생성한다. 핵심 의존성은 네 가지다.
build.gradle의 dependencies 블록을 이렇게 구성한다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.11'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.codeit'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-graphql'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework:spring-webflux'
testImplementation 'org.springframework.graphql:spring-graphql-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
spring-boot-starter-graphql이 GraphQL 관련 라이브러리 전체를 가져온다. spring-boot-starter-web은 HTTP 서버, spring-boot-starter-data-jpa는 JPA, h2는 인메모리 데이터베이스다.
단계 2 — application.yml 설정
src/main/resources/application.yml에 서버, DB, GraphQL 설정을 넣는다.
server:
port: 8080
spring:
datasource:
url: jdbc:h2:mem:bookdb
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
path: /h2-console
jpa:
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate:
format_sql: true
graphql:
graphiql:
enabled: true
schema:
locations: classpath:graphql/
cors:
allowed-origins: "*"
allowed-methods: "*"
logging:
level:
com.codeit.graphql: DEBUG
org.springframework.graphql: DEBUG
설정별로 짚어보면 이렇다.
jdbc:h2:mem:bookdb: 메모리에서 동작하는 H2 DB. 서버 종료 시 데이터가 사라진다.ddl-auto: create: 엔티티 기반으로 테이블을 자동 생성한다. 개발용 설정이다.graphiql.enabled: true: 브라우저에서 GraphQL 쿼리를 테스트할 수 있는 UI를 활성화한다.schema.locations:.graphqls스키마 파일을 찾는 경로다.cors.allowed-origins: "*": 모든 출처에서의 요청을 허용한다. 개발 환경 전용이다.
allowed-origins: "*"는 모든 도메인에서 API를 호출할 수 있다는 뜻이다. 개발 단계에서는 편리하지만, 운영 환경에서는 허용할 도메인을 명시적으로 지정해야 한다.
단계 3 — GraphQL 스키마 작성
src/main/resources/graphql/schema.graphqls 파일을 생성한다. 이 파일이 API의 전체 명세가 된다.
먼저 오브젝트 타입부터 정의한다.
type Book {
id: ID!
title: String!
isbn: String!
publishedYear: Int!
price: Float!
author: Author!
createAt: String!
}
type Author {
id: ID!
name: String!
email: String!
books: [Book!]!
}
Book과 Author가 서로를 참조하는 양방향 구조다. author: Author!는 책이 반드시 저자를 가진다는 뜻이고, books: [Book!]!는 저자가 여러 권의 책을 가질 수 있다는 뜻이다.
다음으로 Input 타입을 정의한다. 클라이언트가 데이터를 보낼 때 사용하는 구조다.
input CreateBookInput {
title: String!
isbn: String!
publishedYear: Int!
price: Float!
authorId: ID!
}
input UpdateBookInput {
title: String
isbn: String
publishedYear: Int
price: Float
}
input CreateAuthorInput {
name: String!
email: String!
}
input UpdateAuthorInput {
name: String
email: String
}
CreateBookInput의 모든 필드는 !(필수)이지만, UpdateBookInput의 필드는 전부 선택이다. 수정할 때는 바꿀 필드만 보내면 되기 때문이다.
마지막으로 세 가지 루트 타입을 정의한다.
type Query {
books: [Book!]!
book(id: ID!): Book
searchBooks(title: String!): [Book!]!
authors: [Author!]!
author(id: ID!): Author
booksByAuthor(authorId: ID!): [Book!]!
}
type Mutation {
createBook(input: CreateBookInput!): Book!
updateBook(id: ID!, input: UpdateBookInput!): Book!
deleteBook(id: ID!): Boolean!
createAuthor(input: CreateAuthorInput!): Author!
updateAuthor(id: ID!, input: UpdateAuthorInput!): Author!
deleteAuthor(id: ID!): Boolean!
}
type Subscription {
bookAdded: Book!
}
Query는 6개의 조회 연산, Mutation은 6개의 변경 연산, Subscription은 1개의 실시간 구독을 정의한다.
코드 뜯어보기
schema.graphqls — 타입 관계
Book의 author: Author!와 Author의 books: [Book!]!가 양방향 관계를 형성한다. 클라이언트는 이 관계를 따라 원하는 깊이까지 탐색할 수 있다.
# 이런 쿼리가 가능하다
{
books {
title
author {
name
books { # 저자의 다른 책까지 탐색
title
}
}
}
}
이 유연함은 강력하지만, 깊이 제한 없이 방치하면 서버에 부하를 줄 수 있다. 개념 1편에서 다룬 내용이다.
Query와 Mutation의 차이
book(id: ID!): Book의 반환 타입은 Book이다. !가 없으므로 null을 반환할 수 있다. 해당 ID의 책이 없을 때를 대비한 설계다.
반면 createBook(input: CreateBookInput!): Book!의 반환 타입은 Book!이다. 생성에 성공하면 반드시 Book 객체를 반환한다는 보장이다.
전체 요청 흐름
스키마를 정의한 뒤, 클라이언트의 요청이 어떤 경로를 타는지 보면 다음 편에서 구현할 내용이 명확해진다.
타입이 맞는가? alt 스키마 검증 실패 SCH-->>C: 에러 응답 (VALIDATION_ERROR) else 스키마 검증 성공 GQL->>R: 매칭된 Resolver 호출 R->>S: 비즈니스 로직 위임 S-->>R: 결과 반환 R-->>C: JSON 응답 end
현재 편에서 스키마까지 만들었다. Resolver와 Service는 다음 편부터 구현한다.
이번 편 최종 전체 코드
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.11'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.codeit'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-graphql'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework:spring-webflux'
testImplementation 'org.springframework.graphql:spring-graphql-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:h2:mem:bookdb
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
path: /h2-console
jpa:
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate:
format_sql: true
graphql:
graphiql:
enabled: true
schema:
locations: classpath:graphql/
cors:
allowed-origins: "*"
allowed-methods: "*"
logging:
level:
com.codeit.graphql: DEBUG
org.springframework.graphql: DEBUG
schema.graphqls
type Book {
id: ID!
title: String!
isbn: String!
publishedYear: Int!
price: Float!
author: Author!
createAt: String!
}
type Author {
id: ID!
name: String!
email: String!
books: [Book!]!
}
input CreateBookInput {
title: String!
isbn: String!
publishedYear: Int!
price: Float!
authorId: ID!
}
input UpdateBookInput {
title: String
isbn: String
publishedYear: Int
price: Float
}
input CreateAuthorInput {
name: String!
email: String!
}
input UpdateAuthorInput {
name: String
email: String
}
type Query {
books: [Book!]!
book(id: ID!): Book
searchBooks(title: String!): [Book!]!
authors: [Author!]!
author(id: ID!): Author
booksByAuthor(authorId: ID!): [Book!]!
}
type Mutation {
createBook(input: CreateBookInput!): Book!
updateBook(id: ID!, input: UpdateBookInput!): Book!
deleteBook(id: ID!): Boolean!
createAuthor(input: CreateAuthorInput!): Author!
updateAuthor(id: ID!, input: UpdateAuthorInput!): Author!
deleteAuthor(id: ID!): Boolean!
}
type Subscription {
bookAdded: Book!
}
자주 하는 실수
스키마 파일은 반드시 src/main/resources/graphql/ 디렉토리 안에 있어야 한다. application.yml에서 schema.locations: classpath:graphql/로 지정했기 때문이다. 루트에 놓거나 resources/ 바로 아래에 놓으면 Spring GraphQL이 스키마를 찾지 못한다.
[!DANGER] 스키마의 필드명과 Java 필드명 불일치
스키마에 publishedYear로 정의했으면 Java 엔티티에도 정확히 publishedYear여야 한다. published_year나 year로 이름을 다르게 지으면 자동 매핑이 실패한다. 카멜케이스를 일관되게 맞춰야 한다.