GraphQL 서버 구축하기

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

Spring boot에 GraphQL적용 이유

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

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

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



Spring boot에 GraphQL적용하기

◾ 라이브러리 추가

처음에 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서버를 제공한다.



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가 된다.



스키마 작성

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를 작성해 주었다.



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 툴 화면이 보이는 것을 확인할 수 있다.

playground



query문 호출해보기

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

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



사용하고 든 생각

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





Reference

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