Graphql 내용 정리

유기훈·2025년 10월 6일

Graphql 사용하는 이유

Graphql 이란

클라이언트가 원하는 데이터를 명확히 정의하고 효율적으로 요청할 수 있도록, 통제권을 제공하는 API 쿼리 언어

Overfetching

RestAPI는 서버에서 정의한 데이터를 모두 반환한다. 클라이언트에서 필요한 정보는 일부분인데 많은 데이터를 받아오게 되면, 불필요한 리소스 낭비가 발생한다.(네트워크 낭비, 클라이언트 리소스 낭비 등) Graphql을 사용함으로써 필요한 데이터만 조회할 수 있게되었다. (sql select 문에 아스타(*)를 쓰는게 아니라 컬럼을 정의해서 조회하는 것과 유사)

Underfetching

RestAPI는 필요한 정보를 한 번에 가져오지 못한다. 사용자의 주문 목록을 조회할 때 RestAPI는 사용자를 먼저 조회하고 해당 정보로 주문 목록을 조회해야 한다. 이는 불필요하게 API를 두 번 호출해야돼서 리소스 낭비가 발생한다. Graphql을 사용하면 필요한 데이터를 한 번에 조회할 수 있다. (물론 서버에서 Http Response를 반환할 때 여러 연관 데이터를 모두 조회해서 반환해주어야 한다. Graphql은 연관데이터를 필드에 정의하고 한 번에 반환하는 걸 권장한다.)

Graphql 주요 개념

단 하나의 엔드포인트

  • REST처럼 여러 엔드포인트(/users, /posts, /comments)를 두지 않고, 단 하나의 엔드포인트(/graphql)에서 클라이언트가 필요한 데이터 구조를 직접 명시하여 요청할 수 있다.
  • HTTP REQUEST METHOD는 POST만 주로 사용한다.

스키마

  • GraphQL 서버의 데이터 구조(타입, 쿼리, 변형 가능 항목 등)를 정의하는 계약서이다.
  • REST의 OpenAPI(Swagger)와 유사한 역할을 한다.

타입 시스템

기본 스칼라 타입:

  • Int, Float, String, Boolean, ID
    사용자 정의 타입:
  • type, input, enum, interface, union
enum Role {
  USER
  ADMIN
}

input CreateUserInput {
  name: String!
  email: String!
}

type User {
  id: ID!
  name: String!
  role: Role!
}

Query (조회)

  • 데이터를 조회(Read) 하는 GraphQL의 핵심 개념
  • SQL의 SELECT, REST의 GET과 유사한 역할을 한다.
query {
  user(id: 1) {
    name
    posts {
      title
    }
  }
}

Mutation (변경)

  • 데이터를 변경(Create, Update, Delete) 하는 작업
  • REST의 POST/PUT/DELETE에 해당한다.
mutation {
  createUser(input: { name: "Alice", email: "a@ex.com" }) {
    id
    name
  }
}

Graphql 사용 방법

Test Tool

RestAPI의 테스트 툴로 PostMan이 있다면, Graphql에서는 Altair가 있다.

Graphql with Spring

세팅 방법

Graphql 라이브러리 설치

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()  
}

Graphql Intellij 플러그인 설치

Graphql 파일 위치

application.yml

server:  
  port: 8081  
  
spring:  
  graphql:  
    schema:  
      file-extensions: graphql  
    websocket:  
      path: graphql

리졸버

  • Controller 어노테이션을 사용한다.
  • 쿼리는 @QueryMapping 어노테이션을, Mutation은 @MutationMapping 어노테이션을, 구독(websocket 사용)은 @SubscriptionMapping 어노테이션을 각 메서드에 입력한다.
  • 파라미터는 @Argument 어노테이션을 붙인다.
@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 등을 제공한다.

@GraphQlExceptionHandler

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

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();
    }
}
profile
개발 블로그

0개의 댓글