클라이언트가 원하는 데이터를 명확히 정의하고 효율적으로 요청할 수 있도록, 통제권을 제공하는 API 쿼리 언어
RestAPI는 서버에서 정의한 데이터를 모두 반환한다. 클라이언트에서 필요한 정보는 일부분인데 많은 데이터를 받아오게 되면, 불필요한 리소스 낭비가 발생한다.(네트워크 낭비, 클라이언트 리소스 낭비 등) Graphql을 사용함으로써 필요한 데이터만 조회할 수 있게되었다. (sql select 문에 아스타(*)를 쓰는게 아니라 컬럼을 정의해서 조회하는 것과 유사)
RestAPI는 필요한 정보를 한 번에 가져오지 못한다. 사용자의 주문 목록을 조회할 때 RestAPI는 사용자를 먼저 조회하고 해당 정보로 주문 목록을 조회해야 한다. 이는 불필요하게 API를 두 번 호출해야돼서 리소스 낭비가 발생한다. Graphql을 사용하면 필요한 데이터를 한 번에 조회할 수 있다. (물론 서버에서 Http Response를 반환할 때 여러 연관 데이터를 모두 조회해서 반환해주어야 한다. Graphql은 연관데이터를 필드에 정의하고 한 번에 반환하는 걸 권장한다.)
기본 스칼라 타입:
enum Role {
USER
ADMIN
}
input CreateUserInput {
name: String!
email: String!
}
type User {
id: ID!
name: String!
role: Role!
}
query {
user(id: 1) {
name
posts {
title
}
}
}
mutation {
createUser(input: { name: "Alice", email: "a@ex.com" }) {
id
name
}
}
RestAPI의 테스트 툴로 PostMan이 있다면, Graphql에서는 Altair가 있다.

plugins {
id 'java'
id 'org.springframework.boot' version '3.5.6'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'khyou'
version = '0.0.1-SNAPSHOT'
description = 'graphql-demo'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-graphql'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.graphql:spring-graphql-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation "com.graphql-java:graphql-java-extended-scalars:22.0"
}
tasks.named('test') {
useJUnitPlatform()
}


server:
port: 8081
spring:
graphql:
schema:
file-extensions: graphql
websocket:
path: graphql
@Controller
public class ProductResolver {
private final ProductService productService;
public ProductResolver(ProductService productService) {
this.productService = productService;
}
@QueryMapping
public List<Product> getProducts() {
return productService.getProducts();
}
@MutationMapping
public Product addProduct(@Argument AddProductInput addProductInput) throws BadRequestException {
return productService.addProduct(addProductInput);
}
@SubscriptionMapping
public Flux<Product> newProduct(@Argument String productName) {
return productService.messageFlux(productName);
}
}
Graphql은 REST처럼 HTTP 상태코드로 에러를 표현하지 않고, 항상 200 OK 응답을 내려보내되, 본문 안에 errors 필드로 에러를 전달한다.
{
"data": null,
"errors": [
{
"message": "Post not found",
"path": ["postById"],
"extensions": {
"errorType": "NOT_FOUND"
}
}
]
}
에러는 GraphQL 응답 구조 안에 포함된다. 이걸 커스터마이징하기 위해 Spring은 GraphQlExceptionHandler, DataFetcherExceptionResolver 등을 제공한다.
import org.springframework.graphql.data.method.annotation.GraphQlExceptionHandler;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
import org.springframework.stereotype.Controller;
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
@Controller
public class GlobalGraphQLExceptionHandler {
@GraphQlExceptionHandler(PostNotFoundException.class)
public GraphQLError handlePostNotFound(PostNotFoundException ex) {
return GraphqlErrorBuilder.newError()
.message(ex.getMessage())
.errorType(ErrorType.NOT_FOUND)
.build();
}
@GraphQlExceptionHandler(IllegalArgumentException.class)
public GraphQLError handleInvalidArgument(IllegalArgumentException ex) {
return GraphqlErrorBuilder.newError()
.message("Invalid input: " + ex.getMessage())
.errorType(ErrorType.BAD_REQUEST)
.build();
}
}
REST API와 Graphql을 함께 제공하는 경우 Exception Handler는 아래와 같이 작성하면 된다.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ApiErrorResponse> handleRestException(RuntimeException ex) {
return ResponseEntity.badRequest().body(new ApiErrorResponse(ex.getMessage()));
}
@GraphQlExceptionHandler(RuntimeException.class)
public GraphQLError handleGraphQLException(RuntimeException ex) {
return GraphqlErrorBuilder.newError()
.message(ex.getMessage())
.errorType(ErrorType.INTERNAL_ERROR)
.build();
}
}