본문 바로가기

스프링

Spring Boot에서 GraphQL 활용법

Spring Boot를 활용하여 RESTAPI를 다룰 수 있으며 REST가 무엇인지, 클라이언트와 서버가 통신하는 HTTP에 대해 학습한 지식이 있을 때 유용합니다. 이 글을 읽기 전 사전에 RESTAPI 그리고 HTTP메서드를 참고하고 보시길 바랍니다.

1. GraphQL 정의

  • 페이이스북에서 만든 API를 위한 쿼리 언어
  • 타입과 필드 시스템을 사용하여 쿼리를 실행하는 서버사이드
  • 서버와 클라이언트 사이에 효율적으로 데이터를 주고 받는 것이 목적

2. REST API와 GraphQl의 차이

  • RESTAPI는 url에 http method (post, get, put, delete)를 활용하여 데이터를 주고받는 방식
    • rest api는 /member, /team 이런식으로 다양한 요청 엔드포인트가 있음
  • GraphQL은 /graphql 라는 엔드포인트만 존재
    • 세부사항은 조회(query), 등록,수정,삭제(mutation)로 구분된 쿼리만 존재함
  • RESTAPI는 서버에서 데이터를 가져오면 정의된 데이터를 통째로 가져옴
    • GraphQL은 필요한 데이터만을 정의된 타입에 맞게 한번의 요청의 쿼리만 날리면 됨 (Over-fetching과 Under-fetching 문제에 대한 해결책)
    • Over-fetching은 필요이상의 데이터를 가져오는 것을 말함
    • Under-fetching은 하나의 요청으로 충분한 데이터를 가져오지 못하는 것을 말함

3. GraphQL 사용법

GraphQl을 spring에서 사용하는 방법을 알아보고자 함

환경설정
build.gradle 의존성 추가

dependencies {
	// graphQL 클래스, 인터페이스를 제공하는 라이브러리
  implementation 'com.graphql-java-kickstart:graphql-spring-boot-starter:11.0.0'
	runtimeOnly 'com.graphql-java-kickstart:graphiql-spring-boot-starter:11.0.0'
	// graphQL 쿼리 요청에 사용되는 라이브러리
	implementation 'com.graphql-java-kickstart:playground-spring-boot-starter:11.0.0'	
	// graphQL 테스트에 사용하는 어노테이션을 제공하는 라이브러리
	testImplementation 'com.graphql-java-kickstart:graphql-spring-boot-starter-test:11.0.0'
}

application.properties 설정

graphql.servlet.mapping=/graphql
graphql.tools.schema-location-pattern=**/*.graphqls
graphql.servlet.cors-enabled=true
graphql.servlet.max-query-depth=100
graphql.servlet.exception-handlers-enabled=true

스키마 정의

 

schema

GraphQL에서 스키마란 데이터 타입의 집합, 스키마 대부분의 타입은 일반 객체 타입이지만 스키마 내에는 특수한 두 가지 타입이 있음 조회를 위한 query 타입, 등록,수정,삭제를 위한 mutation타입으로 정의 함
(Root Query와 Root Mutation에 대한 정의는 하나만 존재 해야함)
이러한 타입은 일반 객체 타입과 동일하지만 모든 GraphQL 쿼리의 진입점(entry point)을 정의 함

 

type

type은 스키마의 핵심 단위이며 하나의 객체로 이해하면 됨 새로운 타입을 정의하고 타입의 내용을 정의

💡 GraphQL 에서는 스칼라 타입들이 기본 제공
Int: 부호가 있는 32비트 정수.
Float: 부호가 있는 부동소수점 값. String: UTF-8 문자열.
Boolean: true 또는 false.
ID: ID 스칼라 타입은 객체를 다시 요청하거나 캐시의 키로써 자주 사용되는 고유 식별자를 나타냅니다. ID 타입은 String 과 같은 방법으로 직렬화되지만, ID 로 정의하는 것은 사람이 읽을 수 있도록 하는 의도가 아니라는 것을 의미

 

schema.graphqls 정의

# Root쿼리를 정의 (조회)
# 조회 요청을 할때 getAllActors를 요청하고 반환값은 Actor객체가 됨을 의미
# getActorById 요청시 보내는 파라미터는 id로 정의하며 id는 Int타입을 의미 
type Query{
    getAllActors:[Actor],
    getActorById(id:Int):Actor
}

# Root뮤테이션을 정의 (등록,수정,삭제)
# updateAddress 요청시 Int타입의 id값과 String타입의 address값을 파라미터로 보내며 Actor 객체를 받겠다는 의미
# input타입을 정의하여 입력 파라미터 값을 묶어서 사용할 수 있음
type Mutation{
    # 매개변수 id는 int타입, adress는 문자열 타입
    # 반환값은 Actor
    updateAddress(id:Int, address:String):Actor,
    # Input값을 정의
    updateAddressByInputObject(input:AddressInput):Actor
}

# Actor객체를 정의
# actorId는 고유한 정수값을 나타내기 위해 ID로 정의, !가 붙으면 값이 null이 되면 안된다는 의미
type Actor{
    actorId:ID!,
    firstName:String,
    lastName:String,
    dob:String,
    address:String,
    filmId:Int
    film:Film # film도 가져올 수 있도록 정의
							# 소스코드내의 Actor객체에 Filme객체가 정의되어있지 않아도 Film을 가져올 수 있음
}

# Film객체를 정의
type Film{
    filmId:ID!,
    name:String,
    regDt:String
}
# 입력 파라미터로 사용될 입력타입의 AddressInput을 정의 
input AddressInput{
    actorId:Int,
    address:String
}

구성요소(Domain, Resolver, Service, Repository) 생성

소스코드는 Java Spring Boot로 작성되었으며 임시로 데이터를 저장하기 위해 h2데이터베이스와 JPA를 활용, 폴더의 구조는 다음과 같음
domain
           ——Actor
           ——Film
           ——AddressInput
repository
           ——ActorRepository
           ——FilmRepository
service
           ——ActorService
           ——DataLoaderService
           ——FilmResolver
           ——FilmService
resources
           ——schema.graphqls (스키마 정의)

Domain

영화배우를 속성을 가진 엔티티를 정의 하였음 영화배우를 식별하기 위한 actorId, 이름을 정의하는 firstName, lastName, 배우의 생년월일의 속성 dob, 주소의 속성 address,
대표영화의 식별값의 관계를 갖기위한 fimId로 생성

- Actor

영화배우를 속성을 가진 엔티티를 정의 하였음 영화배우를 식별하기 위한 actorId, 이름을 정의하는 firstName, lastName, 배우의 생년월일의 속성 dob, 주소의 속성 address, 대표영화의 식별값의 관계를 갖기위한 fimId로 생성

@Entity
@Table(name="ACTOR")
@Getter
@Setter
public class Actor {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ACTOR_ID")
    private Integer actorId;
    @Column(name = "FIRST_NAME")
    private String firstName;
    @Column(name = "LAST_NAME")
    private String lastName;
    @Column(name = "DOB")
    private LocalDate dob;
    @Column(name = "ADDRESS")
    private String address;
    @Column(name = "FILM_ID")
    private Integer filmId;
    public Actor() {

    }
    public Actor(String firstName, String lastName, LocalDate dob, String address, Integer filmId) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
        this.address = address;
        this.filmId = filmId;
    }

 

- Film

film은 영화를 정의한 엔티티이며 영화를 고유하게 식별한 filmId, 영화제목의 속성인 name, 영화 개봉일의 속성 regDt로 정의 하였음

@Entity
@Table(name = "FILM")
@Getter
@Setter
public class Film {

    @Id
    @Column(name = "FILM_ID")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer filmId;

    @Column(name = "NAME")
    private String name;
    @Column(name = "regDt")
    private LocalDate regDt;

    public Film(){}
    public Film(String name, LocalDate regDt) {
        this.name = name;
        this.regDt = regDt;
    }

 

- AddressInput

film은 영화를 정의한 엔티티이며 영화를 고유하게 식별한 filmId, 영화제목의 속성인 name, 영화 개봉일의 속성 regDt로 정의 하였음

public class AddressInput implements GraphQLInputType {
    private Integer actorId;
    private String address;

    public Integer getActorId() {
        return actorId;
    }

    public void setActorId(Integer actorId) {
        this.actorId = actorId;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public List<GraphQLSchemaElement> getChildren() {
        return GraphQLInputType.super.getChildren();
    }

    @Override
    public SchemaElementChildrenContainer getChildrenWithTypeReferences() {
        return GraphQLInputType.super.getChildrenWithTypeReferences();
    }

    @Override
    public GraphQLSchemaElement withNewChildren(SchemaElementChildrenContainer newChildren) {
        return GraphQLInputType.super.withNewChildren(newChildren);
    }

    @Override
    public TraversalControl accept(TraverserContext<GraphQLSchemaElement> context, GraphQLTypeVisitor visitor) {
        return null;
    }
}

Repository

JPA를 활용하여 데이터를 처리하기위한 인터페이스

- ActorRepository

영화배우 엔티티를 데이터베이스와 연결하기 위한 ActorRepository

@Repository
public interface FilmRepository extends JpaRepository<Film, Integer> {
}

 

- FilmRepository

영화 엔티티를 데이터베이스와 연결하기 위한 FilmRepository

@Repository
public interface FilmRepository extends JpaRepository<Film, Integer> {
}

Service

클라이언트가 요청하는 GraphQL을 처리하여 필요한 데이터를 반환해주는 비즈니스 로직 GraphQL 쿼리를 통해 호출 되는 실제 행동을 정의하는 곳이며 이를 Resolver라고 함 GraphQL의 여러 타입 중 mutation, query, subscription 등의 타입이 실제로 동작하는 부분 Spring에서는 GraphQLQueryResolver, GraphQLMutationResolver 인터페이스를 제공함.

- ActorService

mutation(등록)쿼리를 사용하기위한 updateAddress 메서드를 정의, 입력으로 actorId, adress값을 받아서 해당하는 Actor(배우)의 주소를 수정하는 로직, 반환값은 Actor객체 updateAddressByInputObject 메서드는 AdressInput으로 정의한 타입을 입력값으로 받아서 input에 해당하는 값을 수정하는 로직 (updateAddress와 같은 기능이지만 Input값을 활용)

@Service
public class ActorService implements GraphQLQueryResolver, GraphQLMutationResolver{
    private ActorRepository actorRepository;

    @Autowired
    public ActorService(ActorRepository actorRepository) {
        this.actorRepository = actorRepository;
    }
    public List<Actor> getAllActors(){
        return actorRepository.findAll();
    }
    public Actor getActorById(Integer id){
        return actorRepository.findById(id).get();
    }

    // mutation
    @Transactional
    public Actor updateAddress(Integer id, String address){
        Actor actor =  actorRepository.findById(id).get();
        actor.setAddress(address);
        actorRepository.save(actor);
        return actor;
    }
    // 입력 개체를 얻을 수 있음
    @Transactional
    public Actor updateAddressByInputObject(AddressInput input){
        Actor actor =  actorRepository.findById(input.getActorId()).get();
        actor.setAddress(input.getAddress());
        actorRepository.save(actor);
        return actor;
    }
}

- DataLoaderService

h2 데이터베이스에 영화배우와 영화배우의 대표작에 대한 데이터의 초기값을 설정하여 저장

@Service
    public class DataLoaderService {

        @Autowired
        private FilmRepository filmRepository;

        @Autowired
        private ActorRepository actorRepository;

        @PostConstruct
        public void loadData() {
            String[] actors = {"Song Kangho", "Lee Jeongjae", "Choi Minsik", "Ha Jeongwoo"};
            Map<String, String> films = new HashMap<String, String>() {
                {
                    put("Song Kangho", "TheHost");
                    put("Lee Jeongjae", "NewWorld");
                    put("Choi Minsik", "OldBoy");
                    put("Ha Jeongwoo", "NamelessGangster");
                }
            };
            List<LocalDate> arrDate = new ArrayList<>();
            arrDate.add(LocalDate.of(2006, 07, 27));
            arrDate.add(LocalDate.of(2013, 02, 21));
            arrDate.add(LocalDate.of(2003, 11, 21));
            arrDate.add(LocalDate.of(2012, 02, 02));

            int i = 0;
            for (String actorName : actors) {
                String[] names = actorName.split(" ");
                Film film = new Film(films.get(actorName), arrDate.get(i));
                filmRepository.save(film);
                Actor actor = new Actor(names[0], names[1], arrDate.get(i), "seoul korea", film.getFilmId());
                actorRepository.save(actor);
                i++;
            }
        }

 

- FilmResolver

Spring에서 제공하는 GraphQLResolver<T> 인터페이스는 query, mutation 요청을 모두 구현 할 수 있으며 input타입에 대하여 보다 복잡한 요청이 필요할 때 사용함 GraphQLResolver<T> 인터페이스는 Object Resolver 라고도 하는데, 이를 정의하고자 할 때는 대상에 기반한 인터페이스를 구현해야 함 예시에선 GraphQLResolver<Actor>를 구현하고 있는데 Actor에 대한 query 또는 mutation 요청을 받아 로직을 수행할 수 있으며 입력값에 Actor를 사용하여 요청을 하였을 때 사용 하게 됨 Actor에 대한 쿼리를 요청시 이와 관련된 film에 대한 데이터를 같이 요청하면 getFilm메서드를  실행됨

@Service
public class FilmResolver implements GraphQLResolver<Actor> {
    @Autowired
    private FilmRepository filmRepository;

    @Transactional
    public Film getFilm(Actor actor){
        return filmRepository.findById(actor.getFilmId()).get();
    }
}

- FilmService

@Service
public class FilmService {
}

 

4. GraphQL 호출

로컬환경에서 서버를 실행하여 playground 라이브러리 실행 ( localhost:9000/playground ) playground를 활용하면 GraphQL를 간단히 요청하고 확인 할 수 있음 소스코드로 구현한 서버에 요청을 보내서 어떤식으로 값을 가져오는지 확인 해보자

  • query (조회) - 입력값에 따른 요청
    • 쿼리를 보낼때는 query(매개변수:매개변수타입){메서드명{메서드의 인스턴스 변수}} 형태
    • getAllActors메서드를 호출하고 Actor 객체 리스트를 요청
    • id가 4인 getActorById메서드를 호출하고 필요한 데이터를 요청

  • query (조회) - 서로 관계된 객체를 요청
    • id가 4인 getActorById 메서드를 호출하고 반환되는 Actor객체의 filmId에 해당하는 film객체를 요청 (Actor객체에 정의되어 있지 않아도 관계된 데이터를 요청하면 가져옴)

  • mutation(수정)
    • id가 4인 updateAddress에 정의된 Actor객체를 찾아서 adress 값을 변경

  • mutation(input 타입으로 받은 입력값에 해당하는 데이터 수정)
    • input타입으로 입력값을 받아서 AddressInput에 정의된 인스턴스 변수에 파라미터를 받아서 해당하는 데이터를 찾아서 adress 값을 변경

  • @include를 활용한 조건 부여 (true)
    • 입력값이 true일 경우 film객체를 가져옴

  • @include를 활용한 조건 부여 (false)
    • 입력값이 false일 경우 film객체를 가져오지 않음

  • 공통된 인스턴스 변수들을 보관하는 fragment 객체 (true)
    • 공통된 인스턴스 변수를 보관하는 fragment 객체를 활용하면 중복된 정의가 필요없이 … fragment이름 형태로 값을 정의 할 수 있음

4. GraphQL 예제 소스코드

https://github.com/ljh468/GraphQL

 

GitHub - ljh468/GraphQL: GrapQL 예제코드

GrapQL 예제코드. Contribute to ljh468/GraphQL development by creating an account on GitHub.

github.com

5. Reference

https://docs.spring.io/spring-graphql/docs/current-SNAPSHOT/reference/html/ (spring doc)

https://www.baeldung.com/spring-graphql (Getting Started with GraphQL and Spring Boot - balding)

https://youtu.be/JEe8FUCz4VY (pring Boot GraphQL 튜토리얼 - JavaFun)

https://youtu.be/1p-s99REAus (Hello, GraphQL! Graph QL 도입으로 얻었던 효과 - 조민환)

https://velog.io/@jay2u8809/SpringBoot-GraphQL을-써보자 (GraphQL을 써보자 - Jung Ian 블로그)

https://gowoonsori.com/projects/spring-graphql/ (GraphQL 서버 구축하기 - 고운소리의 블로그)

 

Spring Boot에서 일반적인 RESTAPI가 아닌 GraphQL을 사용하게 되면서 학습한 내용을 공유하고 싶어서 블로그에 적게되었습니다. REST가 낫다 GraphQL이 낫다라고 단정할 수는 없지만 각 방식에 대해 학습하고 유용하게 사용하길 기대합니다. 부족한 글 읽어 주셔서 감사합니다. 잘못된 내용 있으면 지적해주시면 감사하겠습니다.

 

하얀종이개발자