-
schema, resolver 자동생성 (2) ...또 Circulargraphst프로젝트 2023. 8. 9. 19:36
저번 포스팅에 이어 schema를 만드는 코드를 살펴보겠습니다.
🤚아래 코드는 이해를 위해 아주아주 간략화된 코드로 실제코드와 많이 다릅니다. 원리는 같아요. 실제코드
// @ObjectType을 사용해 저장된 모든 값을 불러옵니다. const objects = this.storage.getObjectTypeAll(); objects.forEach(({ target, name }) => { // @FieldResolver를 사용해 부모가 target인 모든 데이터를 가져옴 const fieldResolverProps = this.storage.getFieldResolver(target) const fieldProps = this.storage.getResolver(target) // getSchema를 사용해 자식 요소의 graphqlType을 만들어 할당 const fields = this.fieldFactory.getSchema([...fieldProps, ...fieldResolverProps]); if (fields) { graphqlEntity = new GraphQLObjectType({ name, fields, }); } }) // fieldFactory getSchema(props: ...) { const filedArg = {} props.forEach(({ name, returnType, description }) => { // () => GraphqlInt이런것들을 객체로 리턴시킴 const argReturn = returnType(); filedArg[name] = { type: argReturn, description }; }) }
생각보다 별게 없습니다. 그냥 데코레이터에서 수집한 데이터를 GraphqlObject로 만들어주는 코드가 루프로 돌고 있을 뿐입니다.
Query, Mutation 또한 비슷합니다. 전부 살펴보면 너무 포스팅이 길어지니... 궁금하면 깃헙을 봐주세요
사실 위 같은 경우는 재귀함수로 만들어야 하는 경우입니다.
Object하위 field의 args또는 returnType가 GraphqlType(GraphqlString 같은 거)이 아닌 아직 생성안된 Object을 바라보고 있을 수 있기 때문입니다.
아직 생성안된 Object를 생성하기 위해 함수 본인을 호출해야겠죠
처음엔 Containor의 의존성 주입에서 사용한 것처럼 재귀함수로 만들었습니다.
getObjectSchema(target) const object = this.storage.getObjectType(target); const fieldResolverProps = this.storage.getFieldResolver(object.target) const fieldProps = this.storage.getResolver(object.target) const fields = {} // field들을 모조리 찾아서 [...fieldResolverProps, ...fieldProps].forEach(({ name, returnType, description }) => { let argReturn = returnType(); if (interfaceof argReturn Function) { // Graphql타입이 아니고 저장된 Object가 없으면 본인을 다시 호출 getObjectSchema = generatedObject.get(argReturn) && this.getObjectSchema(argReturn) } fields[name] = { type: argReturn, description }; }) const result = new GraphQLObjectType({ name: object.name, description: object.description, fields, }); generatedObject.add(target, result) return result }
위 코드 또한 포스팅을 위해 급조한 코드입니다. 감안하고 봐주세요
코드 진행 순서는 아래와 같습니다.
- @ObjectType으로 수집한 class를 parent키로 가지고 있는 field들의 메타 데이터를 사용해 GraphqlObject를 생성
- field의 returnType이 class고 GraphqlObjectType으로 완성되지 않았다면 먼저 만들고 할당
- GraphqlObjectType가 완성될때까지 반복
여기서 문제가 발생합니다.
circular dependencies문제
Contaior만들 때 발생한 문제가 또다시 불거졌습니다.
Type User {
post: Post
}
Type Post {
user: User
}
위 같은 스키마를 만들어야 한다면 당시 만든 로직으론 아래와 같은 코드가 만들어져 무한 루프에 빠집니다.
const graphqlPostType = new GraphQLObjectType({ name: 'Post', fields: { user: { type: new GraphqlObjectType({ name: 'User', fields: { post: { type: new GraphqlObjectType({ name: 'Post', // 무한 반복 }) } } }) } }, });
제작당시 저는 위에 순환참조 문제를 해결하기 위한 두 가지 아이디어가 있었습니다.
1. Contraior의 circular를 해결한 것처럼 해결
이경우도 Contraior의 의존성 순환 문제와 똑같이 tree형태로 뎁스가 깊어지는 상황이라 똑같이 해결할 수 있겠다 싶었습니다.
순환을 감시해서 중복이 발생하면 이전까지 작성한 Object만 만들어주고 루프를 벗어나는 방식이었습니다.
하지만 해결 방식이 되지 못했습니다.
만약 이전까지 작성한 Object까지만 만든다면 아래처럼 코드가 완성될 겁니다.
const graphqlPostType = new GraphQLObjectType({ name: 'Post', // 여긴 나중에 넣어줘야지 라는 안일한 생각 fields: {}, }); const graphqlUserType = new GraphQLObjectType({ name: 'User', fields: { post: { type: graphqlPostType }, }, });
graphqlPostType는 비어있기 때문에 fields를 재할당 해야 하는데 GraphqlObjectType이 가지고 있는 fieldList에 접근할 수 있는 방법이 전혀 없습니다.
게다가 수정할 수 있다고 해도 저 자리에 graphqlUserType을 넣으면 복사해서 넣는 게 아닌 이상 Maximum call stack size exceeded에러가 리턴 됐겠죠 애초에 안 되는 아이디어입니다.
graphqlPostType을 수정하지 않으면
Type Post {
}
이런 식으로 schema가 만들어집니다.
Contaior에선 인스턴스를 미리 만들어 놓고 Object.defineProperty로 프로퍼티를 직접 수정이 가능했기 때문에 사용할 수 있는 방식입니다.
2. 이름이 같은 GraphqlObject를 만들자(복사하자)
결론부터 말하자면 이건 이름 중복 에러가 발생해서 안됩니다.
const graphqlUserType = new GraphQLObjectType({ name: 'User', fields: { post: { type: graphqlPostType }, }, }); const graphqlUserType2 = new GraphQLObjectType({ name: 'User', fields: {}, }); const graphqlPostType = new GraphQLObjectType({ name: 'Post', fields: { user: { type: graphqlUserType2 } }, });
이렇게 만들면 graphqlUserType2는 Post의 user: User만 만드는데 문제가 없다 판단했습니다. User라는 문자를 도출하는 데에 fields가 필요한 건 아니니까 GraphQLSchema 인스턴스를 만들 때 graphqlUserType2를 전달 안 하면 되겠다는 생각이었지만 그건 저의 바람일 뿐이었고 GraphQLSchema는 내부적으로 graphqlPostType의 graphqlUserType2를 바로 사용했습니다. 어찌 보면 당연하죠(너무 늦게 깨달았습니다.)
TypeGraphql의 경우
지난 포스팅에서 TypeGraphql이 리턴타입에 함수구문을 사용하는 이유에 대해 아직 남아있는 게 있다고 했습니다.
다른 패키지에선 어떻게 해당 문제를 해결하고 있는지 궁금해졌습니다. 아래는 TypeGraphql의 공식 Docs에서 가져왔습니다.
Why use function syntax and not a simple { type: Rate } config object? Because, by using function syntax we solve the problem of circular dependencies (e.g. Post <--> User)
단순한 구성 개체가 아닌 함수 구문을 사용하는 이유는 무엇입니까 { type: Rate }? 함수 구문을 사용하여 순환 종속성 문제(예: Post <--> User)를 해결하므로 관례로 채택되었습니다.그냥 rate: Rate 이렇게 있는 그대로 사용하면 되기 때문에 아주 편해집니다.
또한 GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphqlString))) 같이 더러운 코드보단 [String!]! 이렇게 간단히 사용할 수도 있고 패키지 내부에서 만들어진 Graphql타입을 외부로 전달할 방법에 대해 고민할 필요가 없어 여러모로 합리적인 선택입니다.
이쯤 되니 저도 지금이라도 function syntax를 사용해야겠다 생각이 들었지만 하나만 더 해보자 싶었습니다. function syntax는 진짜 쓰기 싫었거든요([String] 이건 배열이잖아...)
해결
graphqlSchema는 어떻게든 아래 같은 string을 전달해 주면 된다는 부분에 집중했습니다.
type Post { id: ID! user: User } type User { id: ID! post: Post } type Query { posts: [Post] }
해결방법은 임의의 GraphqlObject를 생성하는 겁니다.
const graphqlPostType = new GraphQLObjectType({ name: 'Post', fields: { post: { type: graphqlUserType }, }, }); const graphqlPostType__copy = new GraphQLObjectType({ name: 'Post__copy' }); const graphqlUserType = new GraphQLObjectType({ name: 'User', fields: { post: { type: graphqlPostType__copy }, }, });
위처럼 작성하면 schema는 아래처럼 만들어집니다.
type Post { user: User } type User { post: Post__copy } type Post__copy {}
circular가 발생하면 그냥 "__copy"를 붙여 새로운 GraphqlObjectType을 생성해 할당합니다.
GraphqlSchema에선 Post__copy를 사용해 type을 생성하고 이름을 이용해 프린트를 해줄 것이기 때문에 위 경우가 아닌 다른 case가 아예 없습니다.
저 정도는 정규표현식만 잘 짜놓으면 사이드 이펙트는 없겠다 싶어 탄생한 코드가 아래입니다.
import { GraphQLSchema, printSchema } from 'graphql'; ... this.graphqlSchema = printSchema(schema) .split('\n') .filter((line) => { const trimmedLine = line.trim(); return !( trimmedLine.startsWith('type') && trimmedLine.endsWith('__copy') ); }) .join('\n') .replace(/__copy/g, ''); ...
마치며...
거창하게 "해결"이라고 써놨지만 사실은 편법이죠 그것도 굉장히 나쁜 냄새가 나는...
graphst는 아직 갈길이 먼 프레임워크긴 합니다. subscription도 아직 지원하지 않고 Interface, Union, Scalar에 대한 테스트 또한 아직 완성되지 않아 좋은 퍼포먼스를 내기엔 부족합니다. 아마 위 기능들이 들어가야 마이너 버전을 올릴 수 있을거에요...지금은 간단한 graphql server를 만들정도는 되는 듯합니다. 점차 기능을 추가할예정이니 귀엽게 봐주시면 감사하겠습니다.
다음 포스팅부턴 graphst-api, graphst-app과 배포과정을 포스팅하겠습니다.
많이 부족한글 읽어주셔서 감사합니다!
'graphst프로젝트' 카테고리의 다른 글
API서버 배포하기(Docker + Aws + Github) (0) 2023.08.11 graphst를 사용한 API서버(graphst-api)와 리펙토링 (0) 2023.08.10 schema, resolver 자동생성 (1) (0) 2023.08.09 graphst decorator 살펴보기 (0) 2023.08.08 graphql 프레임워크 개발기(Container) (0) 2023.08.07