[GraphQL] GraphQLResolver

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

GraphQLResolver

앞서 GraphQLResolver를 통해 쿼리를 매핑시키는것을 확인했다. 이제는 GraphQLResolver를 통해 더 유연하게 API를 구성해볼 차례이다. 앞서 이야기한 WebToon의 스키마 구조와 쿼리이다. (내 회사랑은 상관 없다..)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type WebToon {
id: ID!
webToonType: WebToonType
title: String
image: Image
}

type Image {
thumbnail: String!
imageList: [String]
}

query GET_WEBTOON($id: ID!) {
webToon(id: $id) {
id
title
webToonType
image {
thumbnail
imageList
}
}
}

GET_WEBTOON 쿼리를 실행했을 때 id title webToonType만 가져오고 싶을 때가 있고 image를 추가해서 가져오고 싶을때도 있을것이다. image가 필요없는 요청에 대한 케이스에서 저장소에 접근해서 해당 웹툰의 이미지 정보까지 가져오는거는 자원의 낭비라고 볼 수 있다. 게다가 만약 WebToon의 메타데이터는 캐시에 저장해두고 image 데이터는 RDB나 물리적으로 다른 캐시에 저장하고 있다면 어떨까? 실질적으로 WebToonImage1 : N관계이니 나눠서 저장할 수 밖에 없을 것이다. 그렇다면 물리적으로 나뉘는 두 저장소에서 요청에 포함될지 안될지도 모르는 image를 가져오기 위해 필요 없는 접근을 추가해야하니 리소스 낭비라고 볼 수 있다. 기존의 REST에서는 이런 문제가 있어도 개발 생산성이나 다양한 문제 때문에 모든 정보를 내려주고 클라이언트 쪽에서 처리하도록 되어있었다. 혹은 API를 작은 단위로 분리해서 여러번 요청을 통해 정보를 가져오기도 한다. 어느쪽이든 리소스가 낭비되거나 여러번 요청을 통한 서버사이드에 부하가 유발될 수 있다. 이제 이 문제를 GraphQL로 해결하기 위해 우리가 기존에 짜두었던 WebToonResolver를 리팩토링하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@Component
public class WebToonResolver implements GraphQLQueryResolver {
public WebToon webToon(UUID id, DataFetchingEnvironment env) {
GraphQLServletContext context = env.getContext();
String authHeader = context.getHttpServletRequest().getHeader("Auth");

log.info("getWebtoon request accepted id: {}", id);
log.info("getWebtoon request accepted header: {}", authHeader);

return WebToon.builder().id(id).title("여신강림").webToonType(WebToonType.Origianls).build();
}
}

이제 WebToonResolver에서는 image에 대한 정보를 넣어주지 않는다. 그렇다면 image가 필요한 요청에 대해서는 어떻게 해야할까? 답은 웹툰에 대한 GraphQLResolver를 만들어주면 된다.

1
2
3
4
5
6
7
8
9
@Slf4j
@Component
public class ImageResolver implements GraphQLResolver<WebToon> {
public Image image(WebToon webToon) {
log.info("requesting image data for webtoon id: {}", webToon.getId());

return Image.builder().thumbnail("thumbnail URL").imageList(Arrays.asList("episode imgae1 URL")).build();
}
}

이렇게 구조를 계층적으로 잡아두면 WebToon에 대한 요청에서 image가 필요한 경우만 ImageResolver에 매핑되서 결과를 알아서 WebToon에 바인딩해준다.

이렇듯 image에 대한 요청이 있는 쿼리에서는 image에 대한 정보도 가져오고 로그에도 잘 찍힌다.

1
2
3
4
5
6
7
8
9
10
11
query GET_WEBTOON($id: ID!) {
webToon(id: $id) { # WebToonResolver
id
title
webToonType
image { # ImageResolver
thumbnail
imageList
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"data": {
"webToon": {
"id": "3568e088-ec83-11eb-9a03-0242ac130003",
"title": "여신강림",
"webToonType": "Origianls",
"image": {
"thumbnail": "thumbnail URL",
"imageList": [
"episode imgae1 URL"
]
}
}
}
}
1
2
3
2021-07-25 01:22:08.087  INFO 13062 --- [nio-8080-exec-2] c.s.g.r.webtoon.query.WebToonResolver    : getWebtoon request accepted id: 3568e088-ec83-11eb-9a03-0242ac130003
2021-07-25 01:22:08.088 INFO 13062 --- [nio-8080-exec-2] c.s.g.r.webtoon.query.WebToonResolver : getWebtoon request accepted header: Bearer TOKEN
2021-07-25 01:22:08.098 INFO 13062 --- [nio-8080-exec-2] c.s.g.resolver.webtoon.ImageResolver : requesting image data for webtoon id: 3568e088-ec83-11eb-9a03-0242ac130003

물론 image에 대한 요청이 없는 쿼리에 대해서는 ImageResolver를 거치지 않고 바로 응답을 주는걸 확인할 수 있다.

1
2
3
4
5
6
7
8
# 웹툰 가져오기
query GET_WEBTOON($id: ID!) {
webToon(id: $id) { # WebToonResolver
id
title
webToonType
}
}
1
2
3
4
5
6
7
8
9
{
"data": {
"webToon": {
"id": "3568e088-ec83-11eb-9a03-0242ac130003",
"title": "여신강림",
"webToonType": "Origianls"
}
}
}
1
2
2021-07-25 01:23:23.093  INFO 13062 --- [nio-8080-exec-6] c.s.g.r.webtoon.query.WebToonResolver    : getWebtoon request accepted id: 3568e088-ec83-11eb-9a03-0242ac130003
2021-07-25 01:23:23.094 INFO 13062 --- [nio-8080-exec-6] c.s.g.r.webtoon.query.WebToonResolver : getWebtoon request accepted header: Bearer TOKEN

이렇듯 요청에 따라 Resolver를 분리해서 효율적으로 리소스를 사용해서 응답을 내려줄 수 있다. 그런데 물리적으로 분리된 다른 저장소가 아니라 하나의 저장소에서 JOIN과 같은 쿼리를 사용해 데이터를 가져올 수 있는 상황이라면 어떨까? 게다가 JOIN을 사용하는게 더 효율적이라면 어떨까? 그렇게 된다면 이렇게 쿼리를 나눠두는것 보다 image가 있는 요청에서는 JOIN을 사용해서 데이터를 가져오는 방식으로 유연하게 설계를 구성할 수 있을것이다. 다시 우리가 짜둔 코드를 리팩토링 하자.

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
27
28
29
30
31
32
@Slf4j
@Component
@RequiredArgsConstructor
public class WebToonResolver implements GraphQLQueryResolver {
private final ImageBO imageBO;
private final WebToonBO webToonBO;

public DataFetcherResult<WebToon> webToon(UUID id, DataFetchingEnvironment env) {
GraphQLServletContext context = env.getContext();
String authHeader = context.getHttpServletRequest().getHeader("Auth");

log.info("getWebtoon request accepted id: {}", id);
log.info("getWebtoon request accepted header: {}", authHeader);

WebToon webToon = webToonBO.getWebToon(id);

if(env.getSelectionSet().contains("image")) {
log.info("request need image for webtoon");

Image image = imageBO.getImage(id);

return DataFetcherResult.<WebToon>newResult()
.data(webToon)
.localContext(image)
.build();
}

return DataFetcherResult.<WebToon>newResult()
.data(webToon)
.build();
}
}

위 코드에서는 image에 대한 요청이 있을 때 image를 저장소에서 가져와서 localContext에 저장해두고 있다. 이제 ImageResolver는 저장된 imagelocalContext에 가져와서 반환해주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@Component
public class ImageResolver implements GraphQLResolver<WebToon> {
public DataFetcherResult<Image> image(WebToon webToon, DataFetchingEnvironment env) {
log.info("requesting image data for webtoon id: {}", webToon.getId());

Image image = env.getLocalContext();

log.info("get image data from local context. thumbnail : {}",image.getThumbnail());

return DataFetcherResult.<Image>newResult()
.data(image)
.build();
}
}

이렇듯 스토리지의 상황과 api에 따라서 유연하게 코드를 작성해 효율적으로 응답을 내려줄 수 있게 된다.

Repository

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

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