GraphQL 서버 구축하기

이번에 버스 공공api를 이용해 현재 gps를 기반으로한 승차 예약 시스템 프로젝트를 진행중에 있는데, 이때의 구축과정기를 작성하려고 한다.

1. Spring boot에 GraphQL적용 이유

우선, nodeJS를 이용하면 조금 더 편하게 구현할 수 있었을 텐데 그것이 아닌 Spring boot를 이용해서 GraphQL을 사용하는 이유가 프로젝트를 진행하면서 처음 팀원간 합의 할때는 Rest하게 만들 생각이었고, nodeJS는 사용을 저만 해보았기 때문에 Spring boot로 처음 프로젝트를 시작 했다.

그런데, 이미 공공 api에서 DB형태(테이블과 그에 대한 컬럼들)를 제공해주고 우리가 구현할 기능은 많지않았고 그 또한 select하는 쿼리가 대부분의 일이었으며 실제 서비스에 이 많은 컬럼들이 필요하지 않아 쉽게 필요한 데이터만 가져다 쓸 수 있는 형태인 graphQL방식을 사용하기로 했다.

정류장에 속한 각 버스를 구분하는데에는 하나의 key값이 아닌 정류장id, 버스노선id 두개의 키가 복합키로 구분하는 형태이고 이를 REST한 방식으로 설계하면 url이 지저분해질 것 같다고 판단이 되어 graphQL방식을 채택했다.



2. Spring boot에 GraphQL적용하기

1) 라이브러리 추가

처음에 spring boot에서 사용가능한 라이브러리를 찾는데 애를 많이먹었는데 찾고 보면 업데이트를 안한지 오래된 라이브러리이거나 설명문서가 빈약한 경우가 대부분이었기 때문이었다.

그러다가 graphql-java-kickstart 라는 라이브러리를 찾았고, 들어가서 보면 spring boot뿐만이 아닌 java 생태계에서 사용가능한 여러 프로젝트를 진행하고 있는 것을 볼 수 있다.

java-graphql이라는 라이브러리를 시작으로 java생태계에서 graphQL서버 기능을 수행할 수 있는 기능들을 제공하는 프로젝트로 commit이 최근까지 존재해서 선택을 했다.

해당 github에 들어가면 의존성 추가 방법이 나와있는데 나는 gradle로 프로젝트를 진행하였기 때문에 다음과 같이 추가해주었다.

repositories {
    jcenter()
    mavenCentral()
}

dependencies {
  //...다른 라이브러리

  implementation 'com.graphql-java-kickstart:graphql-spring-boot-starter:11.0.0'
  implementation 'com.graphql-java-kickstart:playground-spring-boot-starter:11.0.0'
  testImplementation 'com.graphql-java-kickstart:graphql-spring-boot-starter-test:11.0.0'
}
  • graphql-spring-boot-starter : Query, Mutation등 다양한 graphQL 클래스,인터페이스를 제공하는 라이브러리
  • playground-spring-boot-starter : graphql test tool인 playground를 이용하기위한 라이브러리
  • graphql-spring-boot-starter-test : test code작성을 위해 여러가지 어노테이션을 제공하는 라이브러리
dependencies {
  // to embed Altair tool
  runtimeOnly 'com.graphql-java-kickstart:altair-spring-boot-starter:11.0.0'
  // to embed GraphiQL tool
  runtimeOnly 'com.graphql-java-kickstart:graphiql-spring-boot-starter:11.0.0'
  // to embed Voyager tool
  runtimeOnly 'com.graphql-java-kickstart:voyager-spring-boot-starter:11.0.0'
  // testing facilities
  }

이 외에도 해당 프로젝트는 GraphiQL, Voyager등 다양한 graphQL서버를 제공한다.



3. 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

application.properties.yaml 파일에 graphql 설정을 해줄 수 있고 여기서 mappin의 값이 url의 endpoint가 된다.



4. 스키마 작성

grpahQL은 하나의 엔드포인트에 조회를 담당하는 Query와 삽입/수정/삭제를 담당하는 Mutation으로 나뉜다.

해달 라이브러리는 .graphqls 라는 파일 형식을 통해 스키마를 선언해 주면 된다.

schema {
  query: Query
}

type Query {
  getStations(gpsY: Float!, gpsX: Float!, distance: Float!): [Station]
}

type Station {
  stationId: Int!
  stationName: String!
  mobileNumber: String!
  distance: Int!
}

gps의 위도,경도와 찾고자하는 반경거리를 매개변수로 주어지면, 그에 속하는 정류장 정보(id,이름,모바일번호, 현재위치와의 거리)를 받는 Query문이 들어있는 main/resources/ 밑에 graphql폴더를 생성해주고 query.graphqls 를 작성해 주었다.



5. GraphQLQueryResolver 작성

해당 라이브러리에서는 Query에 GraphQLQueryResolverGraphQLResolver 두개의 인터페이스를 제공하는 데 GraphQLQueryResolver가 Query의 Root가 되는 역할을 한다.

이는 내부적으로 graphql-javaDataFetcher를 활용하여 구현한 더 상위의 라이브러리로 편하게 사용할 수 있도록 제공하는 인터페이스이다.
DataFetcher가 servlet이라면 Resolver는 Spring MVC의 개념

@Builder
@Entity
@Getter @Setter @NoArgsConstructor @AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Station {
    @Id
    private Integer id;

    private String stationName;

    @Column(columnDefinition = "POINT")
    @JsonSerialize(using = GeometrySerializer.class)
    @JsonDeserialize(contentUsing = GeometryDeserializer.class)
    private Point gps;

    private String mobileNumber;
}

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class StationGraphQLDto {
    @NotNull
    private Integer stationId;

    @NotEmpty
    private String stationName;

    @NotEmpty
    private String mobileNumber;

    @NotNull
    private Integer distance;
}
@Component
public class WetayoQuery implements GraphQLQueryResolver {
    private final StationService stationService;
    private final ModelMapper modelMapper;

    WetayoQuery(StationService stationService, ModelMapper modelMapper,) {
        this.stationService = stationService;
        this.modelMapper = modelMapper;
    }

    public List<StationGraphQLDto> getStations(Double x, Double y, Double distance) {
        List<Station> stations = stationService.getNearByStations(x, y, distance);  //근처 정류장 조회
        List<StationGraphQLDto> stationDtos = Arrays.asList(modelMapper.map(stations, StationGraphQLDto[].class)); //distance 필드가 포함된 DTO로 맵핑

        /*distance 계산*/
        int index = 0;
        for (StationGraphQLDto stationDto : stationDtos) {
            stationDto.setDistance(GeometryUtil.calculateDistance(x, y,
                    stations.get(index).getGps().getX(), stations.get(index++).getGps().getY()));
        }
        stationDtos.sort((o1, o2) -> o1.getDistance().compareTo(o2.getDistance()));

        return stationDtos;
    }
}

Query스키마에 해당하는 메서드는 GraphQLQueryResolver 인터페이스를 상속한 클래스에 작성을 해주면 된다.

메서드 이름 맵핑 전략은 query 스키마에 get이 붙지 않는 stations로 작성을 해도 GraphQLQueryResolver에서 우선 stations이름의 메서드를 찾고 없으면 각 메서드들에 get을 붙여 getStations가 있는지 확인하여 메서드를 맵핑시켜준다.

여기 까지 작성하고 어플리케이션을 실행시켜 localhost:8080/playground 에 접속 해보면 다음과 같이 playground 툴 화면이 보이는 것을 확인할 수 있다.

alter-text



6. query문 호출해보기

alter-text

왼쪽 페이지에 query문 양식에 맞게 입력하고 재생버튼을 클릭하면 오른쪽 페이지에 정상적으로 응답이 오는 것을 확인할 수 있다.

이와 같은 방법으로 Query의 다른 메서드와 Mutation을 추가해주는 방식으로 spring boot에 graphQL을 적용시켜 보았다.



7. 사용하고 든 생각

처음 시작할때는 제대로 된 문서를 찾기가 힘들어 많은 삽질끝에 구성을 완료했는데, 이렇게 블로그를 작성하며 다시한번 살펴보니 생각보다 별게 없고, 사용법이 간단하다고 느꼈고, 얼마전에 Netflix의 포스팅을 보다가 발견한건데 Netflix에서 제공하는 어노테이션 방식인 DGS 를 사용해봐도 괜찮겠다고 생각이 들었다. (그래도 GraphQL은 JS/TS를 할 줄 안다면 Apollo server가 더 편하게 작성할 수 있지 않을까…) -> 2022년 현재 spring 공식 프로젝트로 spring graphQL이 추가되었다…





Reference

https://stackoverflow.com/questions/56555001/difference-between-datafetchers-and-resolvers

Related Posts

Optional

Optional

  • Java
  • 2021년 12월 8일

Java 8에 새로 생긴 인터페이스로 라이브러리 메서드가 반환할 결과값이 없음을 명백하게 표현할 필요가 있는 곳에서 제한적으로 사용할 수 있는 메커니즘을 제공하기 위해 새로 생겨났다. Java api doc의 API 노트를 보면 다음과 같이 설명하고 있다. Optional은 주로 결과 없음을 나타낼 필요성이 명확하고 null을 사용하면 오류가 발생할 수 있는 메소드 반환 유형으로 사용하도록 고안되었다....

Read More
자바에서의 정렬

자바에서의 정렬

  • Java
  • 2021년 3월 28일

순서화가 가능한 자료구조를 정렬하는 방법에는 삽입정렬,버블정렬,분할정렬,퀵정렬 등 잘알려진 정렬 외에도 정말 많이 존재하는데 지금은 이러한 정렬의 알고리즘이 아닌 이미 자바 라이브러리에서 제공하는 자료구조들을 쉽게 정렬하는 방법을 정리하고자 한다. 자바에서의 크기를 비교할 수 있는 객체들은 모두 Comparable을 implements하여 compareTo를 구현하여 두 값중 어떤 것이 더 큰 값인지 알 수 있게끔 명시하고 있다....

Read More
JVM과 자바 실행

JVM과 자바 실행

  • Java
  • 2021년 1월 22일

백기선님의 유튜브 로 진행하시는 스터디를 진행하며 올리는 정리 블로그입니다. 1. JVM Java Virtual Machine의 약자로 Java와 OS사이에서 중개자 역할을 하여 메모리 관리, GC(garbage collection)을 수행하는 스택기반의 가상머신으로 C++,C와 같은 언어들과 다르게 OS에 상관없이 동작이 가능하다. Java는 C나 C++과 같이 바로 컴파일되는 컴파일 언어가 아니다. 컴파일언어는 컴파일 환경에 따라 다르게 컴파일이 된다는 특징을 가지고 있기 때문에 Java의 가장 큰 특징인 os와 환경에 구애받지 않는 다는 점을 위반하기 때문이다. (파이썬이나, JS는 인터프리터 언어)...

Read More