Kotlin + GraphQL 로 개인 토이프로젝트를 진행하다 부딪현던 문제에 대한 공유입니다.
진행하던 토이프로젝트는 Kotlin + Spring-Boot + JPA + GraphQL 이 사용한 대표적인 기술스택이었고
GraphQL 을 사용하기위하여 GrapQL-spqr-starter 의존성을 사용하였었습니다.
GraphQL-spqr 은 기존의 Schema-first 즉 graphQL 스키마를 먼저 정의하고 코드를 작성하는 방식에서
Code-first, 코드를 작성하면 그 코드를 바탕으로 스키마를 만들어주는 라이브러리인데요.
그렇게 작업을 하던 과정에서 Java 에서 GraphQL-spqr 을 통하여 작업할때는 마주할 수 없었던 컴파일 오류를 맞이합니다..
처음보는 오류에 당황했지만 침착하게 대응해봅니다. (사실 해결에 꽤 오래걸렸습니다..)
일단 오류로그만 살펴보면 GraphQLSchemaBuilder 에서 스키마를 generate 하는 과정에서 Companion 오브젝트에 문제가 있다.
정도의 정보를 얻을 수 있습니다.
오류를 살펴보기전에 일단 스키마 생성시 사용했던 DTO 중 Companion 오브젝트를 사용했던 클래스를 하나 살펴보겠습니다.
코틀린의 Companion 오브젝트의 메서드는 사용은 마치 static method 처럼 사용하지만,
사실은 Companion 이라는 이너 클래스를 생성하고 해당 클래스를 static 필드로 지니면서,
호출 시에 Companion 이라는 오브젝트를 통하여 메서드를 호출하도록 컴파일 됩니다.
이로서 로그를 완전히 이해할 수 있습니다. Spqr 이 코드를 스키마로 읽으려 하는데,
DTO 에 Companion 이라는 nested 오브젝트가 존재하여 Companion 역시 스키마화 하려했으나 아무런 필드를 지니지 않고 있어 스키마가 생성되지 않았던 것이었습니다. (물론 이렇게 뜯어보지 않아도 충분히 이해할 수 있는 로그 내용이지만 상세히 알아보았습니다.)
그렇다면 이제 라이브러리 내에서 커스텀할 수 있는 부분이 있는지 뜯어봅시다.
로그 최하단에서 호출하는 메서드입니다. graphQLSchemaGenerator 란 빈도 AutoConfiguration 에 포함되어 있겠군요
해당 AutoConfiguration 의 상단에는 @Autowired(required=false) 로 많은 빈들은 Optional 하게 주입받고,
해당 빈이 있다면 GraphQLSchemaGenerator 에 주입해주고 있습니다.
대충 주입되는 빈들의 이름만 봐도 스키마 생성의 전략설정으로 보입니다. 이 중 하나가 해결의 실마리가 될 것 같습니다.
이번엔 GraphQLSchemaGenerator 가 설정빈 들을 주입받아 어떻게 동작하는지 살펴보겠습니다.
요리조리 뜯어봤지만, 주입받은 빈은 BuildContext 를 생성하는데에만 쓰이고 생성한 BuildContext 를 OperationMapper 라는 인스턴스를 생성하여 쿼리와, 뮤테이션을 생헝하는데 사용하고 있습니다.
우선 operationMapper 가 generateQueries, generateMutations 등의 퍼블릭 인터페이스가 존재하여 살펴보도록 하겠습니다.
처음에는 resolveTypeReference 혹은 toGraphQLField 등에서 변환을 체크할 것으로 보였으나, 타고 들어가도 필터링 조건은 보이지 않고 단순 변환 작업만 수행하고 있었습니다. 그렇다면 애초에 쿼리를 가져올 때, 필터링이 되서 가져올 것으로 생각됩니다.
그래서 여기서 operationRegistry 가 쿼리 정보를 들고 있다고 유추되어 해당 클래스를 살펴보기로 했습니다.
뭔가가 해결될 것 같습니다.
리플렉션을 통하여 GraphQLAPI 빈 메서드들을 탐색하고, 포함된 필드들을 필터링 하여 모아서 반환하고 있습니다.
이 중 하나의 메서드에 들어가보겠습니다.
필터링 조건 중 유일하게 핸들링할 수 있어 보이는 부분.
params.getInclusionStrategy().includeOperation() 이 보입니다. 상위 클래스로 부터 계속 물려받아온 파라미터 였는데
아까 AutoConfiguration 에서도 존재했는지 다시 확인합니다.
찾았습니다. 이제 해당 빈을 커스텀하여 등록해줍니다.
빈으로 등록만하면 @Autowired(required=false) 에 의해 주입되고 BuildContext 에도 전달될 것입니다.
@Component
class CompanionClassFilteringInclusionStrategy() : DefaultInclusionStrategy() {
override fun includeOperation(elements: List<AnnotatedElement>, declaringType: AnnotatedType): Boolean {
return if (isCompanionClass(elements)) {
false
} else {
super.includeOperation(elements, declaringType)
}
}
private fun isCompanionClass(elements: List<AnnotatedElement>): Boolean =
(elements.firstOrNull { it is Field } as Field?)?.type?.typeName?.contains("\$Companion") ?: false
}
Companion 일 경우 제외하고, 아닐 경우 기본 전략을 선택하도록 해줍니다.
결과. 정상적으로 빌드되고 스키마도 생성되었습니다.
'Kotlin' 카테고리의 다른 글
Kotlin 의 널 (Null) 을 다루는 방법 (0) | 2022.05.24 |
---|---|
Kotlin 의 inline 함수 (0) | 2022.03.24 |
Mockk 으로 Kotlin 현재 시간 관련 로직 테스트하기 (0) | 2022.03.21 |