[GraphQL] Custom Scalar

본 포스트는 공식 레퍼런스를 참고해 GraphQL을 공부하며 직접 작성한 가이드 입니다.
본 포스트는 2021년 7월 최신 버전인 v16.2를 기준으로 작성되어 있습니다.

Scalar

GraphQL에서는 다양한 기본 스칼라를 제공해주고 있다. ID Int String 등등이 이에 해당하는데 각각 스스로의 타입을 의미한다. 다양한 필터가 제공되는 스칼라 확장 라이브러리도 존재하고 그 외에도 개발자가 커스텀하게 스칼라를 만들 수 있도록 지원하기도 하는데, 이번엔 그 방법을 소개한다.

우리는 웹툰에 회차 정보를 넣는 작업부터 시작한다. 먼저 스키마부터 정의하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Episode {
episodeNo: ID!
title: String
releaseDate: Date
}

type WebToon {
id: ID!
webToonType: WebToonType
title: String
image: Image
bgm: Bgm
episodes(episodeNos: [ID!]!): [Episode]
}

WebToonEpisode1 : N구조로 episodeNo들을 받아 이 값들을 웹툰에 넣어줄 것이다. 이제 이 스칼라에 맞춰 모델을 생성하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Builder
@Getter
public class Episode {
private int episodeNo;
private LocalDate releaseDate;
private String title;
}

@Builder
@Value
public class WebToon {
private UUID id;
private String title;
private WebToonType webToonType;
private Image image;
private Bgm bgm;
private List<Episode> episodes;
}

모델을 생성했으면 WebToon[Episode]를 넣어줄 리졸버를 만들어준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@Component
public class EpisodeResolver implements GraphQLResolver<WebToon> {
public List<Episode> episodes(WebToon webToon, List<Integer> episodeNos) {
log.info("request episode data for webToon id : {}", webToon.getId());
log.info("episode no of requests : {}", episodeNos.stream().map(Object::toString).collect(Collectors.joining(", ")));

return episodeNos.stream()
.map(episodeNo -> Episode.builder()
.episodeNo(episodeNo)
.releaseDate(LocalDate.now())
.title(String.format("%d 화", episodeNo))
.build())
.collect(Collectors.toList());
}
}

이제 이에 맞춰 요청을 보내보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 웹툰 에피소드 가져오기
query GET_WEBTOON_AND_EPISODES($id: ID!, $episodeNos: [ID!]!) {
webToon(id: $id) {
id
title
webToonType
episodes(episodeNos: $episodeNos) {
episodeNo
title
releaseDate
}
}
}

{
"id": "3568e088-ec83-11eb-9a03-0242ac130003",
"episodeNos": [1,2,3]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"data": {
"webToon": {
"id": "3568e088-ec83-11eb-9a03-0242ac130003",
"title": "여신강림",
"webToonType": "Originals",
"episodes": [
{
"episodeNo": "1",
"title": "1 화",
"releaseDate": "2021-07-29"
},
{
"episodeNo": "2",
"title": "2 화",
"releaseDate": "2021-07-29"
},
{
"episodeNo": "3",
"title": "3 화",
"releaseDate": "2021-07-29"
}
]
}
}
}

응답이 잘 내려오는걸 확인할 수 있다. 다만 위 스키마는 문제가 있는 스키마 구조이다. 웹툰의 회차번호는 음수가 될 수 없다. 하지만 음수에 대한 요청을 보내면 문제없이 요청이 리졸버 컴포넌트까지 도착하는걸 확인할 수 있다.

1
2021-07-29 00:30:36.577  INFO 18046 --- [nio-8080-exec-1] c.s.g.resolver.webtoon.EpisodeResolver   : episode no of requests : -1, -2, -3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"data": {
"webToon": {
"id": "3568e088-ec83-11eb-9a03-0242ac130003",
"title": "여신강림",
"webToonType": "Originals",
"episodes": [
{
"episodeNo": "1",
"title": "-1 화",
"releaseDate": "2021-07-29"
},
{
"episodeNo": "2",
"title": "-2 화",
"releaseDate": "2021-07-29"
},
{
"episodeNo": "3",
"title": "-3 화",
"releaseDate": "2021-07-29"
}
]
}
}
}

이제 이 버그를 고쳐보자. 먼저 다양한 스칼라 타입을 지원하는 graphql-java-extended-scalars 라이브러리를 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-extended-scalars</artifactId>
<version>${graphql-scalars-version}</version>
<exclusions>
<exclusion>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
</exclusion>
</exclusions>
</dependency>

이제 음수에 대해 제한을 거는 스칼라와 날짜 관련 스칼라를 스프링 빈으로 등록해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class SchemaConfig {

@Bean
public GraphQLScalarType nonNegativeInt() {
return ExtendedScalars.NonNegativeInt;
}

@Bean
public GraphQLScalarType date() {
return ExtendedScalars.Date;
}
}

그리고 관련 스키마를 등록하고 변수 타입을 변경해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scalar Date
scalar NonNegativeInt

# 웹툰 정보 가져오기
type Query {
webToon(id: ID!): WebToon
}

type WebToon {
id: ID!
webToonType: WebToonType
title: String
image: Image
bgm: Bgm
episodes(episodeNos: [NonNegativeInt!]!): [Episode]
}

그리고 이제 음수의 회차 번호를 요청에 실어 보내보면 오류가 떨어짐을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"errors": [
{
"message": "Variable 'episodeNos' has an invalid value : The value must be greater than or equal to zero",
"locations": [
{
"line": 1,
"column": 42
}
],
"extensions": {
"classification": "ValidationError"
}
}
],
"data": null
}

물론 정상 요청의 경우 잘 동작한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"data": {
"webToon": {
"id": "3568e088-ec83-11eb-9a03-0242ac130003",
"title": "여신강림",
"webToonType": "Originals",
"episodes": [
{
"episodeNo": "1",
"title": "1 화",
"releaseDate": "2021-07-29"
},
{
"episodeNo": "2",
"title": "2 화",
"releaseDate": "2021-07-29"
},
{
"episodeNo": "3",
"title": "3 화",
"releaseDate": "2021-07-29"
}
]
}
}
}

다만 여기서 노출 시간 포멧이 yyyy-MM-dd로 되어있다. 이제 이를 변경하기 위해 직접 스칼라 타입을 만들어 패턴을 바꿔보자. 스칼라를 구현하기 위해선 변환을 담당해줄 Coercing만 잘 구현해주면 만들 수 있다. 직접 만든 스칼라를 빈으로 등록해주고 요청을 보내보자.

1
2
3
4
5
6
7
8
9
10
public class DateScalar extends GraphQLScalarType {
public DateScalar(final DateTimeFormatter dateFormatter) {
super("Date", "Sumfi Custom Date Scalar With Own Formatter", new Coercing<LocalDate, String>() { ... });
}
}

@Bean
public GraphQLScalarType date() {
return new DateScalar(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
}

응답이 커스텀하게 바꾼 패턴인 yyyy.MM.dd로 내려오는걸 확인 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"data": {
"webToon": {
"id": "3568e088-ec83-11eb-9a03-0242ac130003",
"title": "여신강림",
"webToonType": "Originals",
"episodes": [
{
"episodeNo": "1",
"title": "1 화",
"releaseDate": "2021.07.29"
},
{
"episodeNo": "2",
"title": "2 화",
"releaseDate": "2021.07.29"
},
{
"episodeNo": "3",
"title": "3 화",
"releaseDate": "2021.07.29"
}
]
}
}
}

Repository

모든 가이드의 예제 코드는 SongHayoung/springboot-graphql-tutorial에서 확인할 수 있습니다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2021/07/29/GraphQL/graphql-10/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.