티스토리 뷰

반응형

※이 포스팅은 https://www.baeldung.com/spring-graphql 을 번역하고, 예제 코드와 내용을 각색한 포스팅입니다.


1. 개요

GraphQL은 REST API의 대안으로 Facebook에서 제시한 새로운 Web API 컨셉입니다. 이 포스팅에서는 스프링 부트(Spring Boot)를 사용하여 웹 애플리케이션에 GraphQL서버를 구축하는 방법을 소개하겠습니다.

(개인적으로 사용한 예제는 스프링 부트 2.1.8버전, 자바 8버전, JPA를 사용하였습니다.)

 

 

2. GraphQL이 뭐지?

기존 REST API는 다양한 HTTP Method가 서버에 존재하는 리소스(Resource)와 대응되어서 동작합니다. 이는 클라이언트의 요청사항과 리소스(Resource)가 잘 들어맞지 않는다면 (성능적인) 문제가 발생할 수 있다는 것을 암시합니다.

 

예를들어 클라이언트가 블로그의 '게시글' 및 '댓글'을 동시에 필요한 경우 클라이언트는 서버에 '게시글'과 '댓글'을 여러번 요청하거나, 클라이언트가 필요하지 않는 데이터까지 포함한 응답을 받게 될 수 있습니다. (혹은 수많은 클라이언트 요청사항을 정확하게 대응하기 위해서 모든 경우를 개발해야 합니다.)

 

GraphQL은 이러한 문제들에 대한 해결책을 제시합니다. 클라이언트는 단일 요청에서 여러 하위 자원 탐색을 포함하여 원하는 데이터만을 정확하게 지정할 수 있습니다.

 

GraphQL에서 블로그의 '게시글(post)'을 요청하는 쿼리(query)는 예제는 다음과 같습니다.

query {
    recentPosts(count: 10, offset: 0) {
        id
        title
        category
        author {
            id
            name
        }
    }
}

위의 쿼리는

  1. 블로그 게시글(post) 중 가장 최신의 10개를 조회 요청합니다.
  2. 각 게시글의 id, title, category를 조회 요청합니다.
  3. 각 게시글의 작성자(author)를 id, name을 함께 조회 요청합니다.

GraphQL를 사용하면 클라이언트가 필요로 하는 응답만을 단 한번의 요청을 받아낼 수 있습니다.

 

2.1 GraphQL Schemas

GraphQL 서버는 클라이언트에게 API를 설명하는 공개된 스키마(Schemas)를 제공합니다.

아래 GraphQL 스키마(Schema)는 블로그의 게시글(post)과 작성자(author)에 대한 정의조회를 하기 위한 루트 쿼리(Root Query)에 대한 정의 그리고 등록, 수정, 삭제를 위한 루트 뮤테이션(Root Mutation)에 대한 정의를 나타냅니다.

# GraphQL Schema 정의

type Post {
    id: ID!
    title: String!
    text: String!
    category: String
    author: Author!
}
 
type Author {
    id: ID!
    name: String!
    posts: [Post]!
}
 
# 루트 쿼리 (Root Query)
type Query {
    recentPosts(count: Int, offset: Int): [Post]!
}
 
# 루트 뮤테이션 (Root Mutation)
type Mutation {
    writePost(title: String!, text: String!, category: String) : Post!
}

스키마는 타입(type)과 필드(field)를 가질 수 있고 각 필드는 인수(argument)도 가질 수 있습니다.

느낌표(!)가 이름 끝에 붙어 있는 경우는 Null이 불가능한 타입임을 나타냅니다. 느낌표(!)가 없는 타입들은 널을 가질 수 있습니다. GraphQL은 이러한 스키마 특성을 이용해서 Null을 안전하게 다루고, 요청받을 수 있습니다.

클라이언트는 공개된 스키마를 통해 API의 변화를 감지하고 동적으로 적응할 수 있습니다.

 

 

3. 스프링 부트로 GraphQL 시작하기!

Spring Boot GraphQL Starter는 간단한 방법으로 매우 빠르게 GraphQL을 서버에 적용할 수 있는 환상적인 방법을 제공합니다.

3.1 초기 설정하기

스프링 부트의 의존성 주입은 다음과 같이 진행합니다. (메이븐을 이용해서 설정했습니다.)

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-spring-boot-starter</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>5.2.4</version>
</dependency>

스프링 부트는 적절한 핸들러(handler)를 자동으로 설정합니다.

의존성 주입만으로 /graphql 로 GraphQL 엔드포인트가 생성되고, 이 엔드포인트로 GrpahQL 요청을 POST 방식으로 받게 됩니다. (즉, 엔드포인트를 위한 별도의 컨트롤러를 개발자가 생성하지 않습니다.) 자동 설정은 application.properties와 같은 설정파일로 커스터마이징 할 수 있습니다. 

 

3.2 스키마 (파일) 작성하기

스키마 파일은 *.graphqls라는 확장자로 저장해야 합니다. 클래스패스(classpath) 어느 곳에나 존재할 수 있으며, 여러개의 스키마 파일을 작성하여서 개발자가 원하는 만큼 모듈화 할 수 있습니다.

한가지 요구사항은 루트 쿼리(Root Query)와 루트 뮤테이션(Root Mutation)에 대한 정의는 하나만 존재해야 합니다. (이것은 자바나 스프링 부트의 요구사항이 아니라, GraphQL 스키마의 요구사항 입니다.)

Spring Boot GraphQL Starter는 스키마 파일을 자동으로 탐색하고, 이를 GraphQL을 담당하는 스프링 빈(bean)에게 명시된 스키마 구조를 주입시켜 줍니다.

 

3.3 루트 쿼리 (Root Query Resolver)

루트 쿼리(Root Query)를 처리하기 위한 스프링 빈(bean)이 필요합니다. (스키마 정의와 달리 루트 쿼리(Root Query)를 처리하기 위한 빈(bean)은 여러개 존재 할 수 있습니다.)

루트 쿼리(Root Query)를 처리하기 위한 스프링 빈(bean) 클래스는 GraphQLQueryResolver 인터페이스를 구현하고, 스키마에 있는 필드가 클래스의 메소드로써 모두 존재해야 합니다. (메소드의 인수값과 반환값의 타입은 Java에서 사용되는 형태로 자동 매핑됩니다.)

@Component
@RequiredArgsConstructor
public class MyQuery implements GraphQLQueryResolver {
    private final PostRepository postRepository;

    public List<PostResponse> getRecentPosts(int count, int offset) {
        final List<Post> all = postRepository.findAll();
        return PostResponse.from(all);
    }
}

스키마에서 정의한 type Query의 recentPosts필드는 getRecentPosts()메소드가 처리하게 됩니다.

메소드의 이름의 명명규칙은 다음과 같습니다.

  1. <필드명>
  2. is<필드명> - (필드 반환 타입이 boolean 인 경우)
  3. get<필드명>

 

3.4 GraphQL 타입(type) 나타내기

GraphQL의 타입들은 자바 클래스로 나타낼 수 있습니다. 자바 클래스는 한가지 GraphQL 타입만을 나타내어야 하지만, 클래스명을 반드시 일치시킬 필요는 없습니다.

public class Post {
    private String id;
    private String title;
    private String category;
    private String authorId;
}

클래스 내부의 필드와 GraphQL 응답은 '이름'을 기반으로 매핑(mapping)됩니다. 스키마와 매핑되지 않은 자바 클래스 필드나 메소드는 무시되며 문제를 일으키지 않습니다.

예를들어, 위의 자바 클래스 필드 authorId는 이전에 정의한 스키마 필드와 어떤 것도 일치하지 않아 매핑되지 않습니다.

 

실습 예제에서는 위의 자바 클래스를 사용하지 않고 아래의 클래스를 사용했습니다. 게시글(post)에 대한 응답 전용 DTO 클래스입니다.

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PostResponse {
    private long id;
    private String title;
    private String text;
    private String category;
    private Author author;

    public static List<PostResponse> from(Collection<Post> entities) {
        return entities.stream().map(PostResponse::from).collect(Collectors.toList());
    }

    public static PostResponse from(Post entity) {
        return PostResponse.builder()
                .id(entity.getId())
                .title(entity.getTitle())
                .text(entity.getText())
                .category(entity.getCategory())
                .author(entity.getAuthor())
                .build();
    }
}

 

3.5 복잡한 처리를 위한 필드 리졸버 (Field Resolver)

필드의 값들을 단순히 매핑시키는 것만으로는 부족할 수 있습니다. 예를들어 데이터베이스 조회하여 복잡한 계산한 값이 필요한 경우, GraphQL은 이러한 경우를 대처하기 위한 별도의 스프링 빈(bean)을 사용합니다.

@Component
@RequiredArgsConstructor
public class PostResolver implements GraphQLResolver<PostResponse> {
    private final AuthorRepository authorRepository;

    public Author getAuthor(PostResponse postResponse) {
        return authorRepository.findById(postResponse
				.getAuthor().getId())
				.orElseThrow(NullPointerException::new);
    }
}

필드 리졸버(Field Resolver)는 클래스 이름에 'Resolver'라는 접미사가 붙어 있고, GraphQLResolver 인터페이스를 구현한 스프링 빈(bean)입니다. 사용할 DTO 클래스는 제네릭 타입의 매개변수로 사용합니다.

(만약 필드 리졸버(Field Resolver)와 데이터 빈이 모두 동일한 GraphQL 필드에 대한 메소드를 갖는 경우 필드 리졸버(Field Resolver)가 우선됩니다.)

클라이언트가 필드를 요청하지 않으면 GraphQL 서버는 필드를 검색하는 작업을 수행하지 않습니다. 즉, 클라이언트가 게시글(post)를 요청하면서 작성자(author)정보를 요청하지 않으면 getAuthor()메소드는 실행되지 않습니다.

 

3.6 Nullable 값

GraphQL 스키마는 Null이 가능한 경우와 아닌 경우에 대한 타입을 명시적으로 정의할 수 있습니다. 이 사실은 Null 값을 직접 Java 코드에서 처리 하거나 Java 8의 Optional을 사용하여 Null이 가능한 타입을 유용하게 다룰 수 있습니다.

 

3.7 뮤테이션 (Mutation) - (등록, 수정, 삭제)

GraphQL은 서버에 저장된 데이터를 업데이트 하기 위한 행위를 뮤테이션(Mutation)이라고 합니다.

@Component
@RequiredArgsConstructor
public class MyMutation implements GraphQLMutationResolver {
    private final PostRepository postRepository;
    private final AuthorRepository authorRepository;

    public PostResponse writePost(String title, String text, String category) {
        Post post = new Post();
        post.setTitle(title);
        post.setText(text);
        post.setCategory(category);
        post.setAuthor(authorRepository.getOne(1L));

        final Post save = postRepository.save(post);

        return PostResponse.from(save);
    }
}

코드만 보게되면 뮤테이션(Mutation)과 쿼리(Query)는 다르지 않아 서버에 저장된 데이터를 업데이트 하는 기능을 수행할 수도 있겠지만, 이는 API의 부작용을 발생 시킬 수 있기 때문에 뮤테이션(Mutation)을 별도로 사용하고 클라이언트에게 데이터가 변경될 것 임을 암시합니다.

뮤테이션(Mutation) 자바 클래스는 쿼리(Query)에 적용한 규칙을 모두 따르지만, 쿼리(Query)와 다른 점은 GraphQLMutationResolver를 구현해야 합니다.

 

 


예제 Github 주소

  • https://github.com/siyoon210/spring-framework/tree/master/graphql/baeldung-example
  • (응답을 테스트하기 위해서 포스트맨과 같은 REST 테스트 툴을 사용할 수도 있고 GraphiQL (그래프 아이큐엘)이라는 툴을 사용할 수 도 있습니다. 개인적으로 포스트맨이 편해서 사용했는데, 간혹 소스코드가 바뀌어도 응답이 바로 바뀌지 않은 경우가 있었습니다. 2~3번 여러번 요청 진행하면 정상적인 응답을 받을 수 있었습니다.)

참고자료

반응형
댓글