GraphQL.js 리졸버의 첫 번째 인자인 source를 다룬 연재 글이다. 상위 필드가 반환한 값이 그대로 들어온다는 단순한 동작이지만, 그 값이 스키마의 타입과 전혀 달라도 문제없다는 점을 여러 형태로 보여준다. 그 특성이 타입 언어와 잘 맞지 않는 지점도 함께 짚는다.
핵심 포인트- 리졸버는 상위 필드에서 하위 필드 순서로 호출되며 source에는 상위 필드가 반환한 값이 들어간다.
- 상위 필드가 없는 최상위 쿼리에는 실행 시 지정한 rootValue가 들어오지만 실무에서 쓴 적은 없다고 밝힌다.
- 스키마의 타입과 구현체의 타입이 같지 않아도 동작한다. 클래스로 정의해보면 그 차이가 눈에 들어온다.
- TypeScript는 필드만 같으면 같은 타입으로 보므로 클래스 인스턴스와 객체 리터럴을 섞어도 통과한다.
- 극단적으로 상위에서 식별자만 담은 객체를 반환하고 모든 필드를 하위 리졸버에서 채우는 것도 가능하다.
- 이 유연함은 코드를 처음 보는 사람에게 혼란을 줄 수 있고 타입 생성 도구와도 충돌한다.
상세 정리- 기본 동작: Java 쪽에서는 환경 객체의 getSource로 같은 값을 얻는다.
- 예제 스키마: 사용자와 게시물 타입을 두고, 게시물은 작성자 식별자와 작성자 객체를 함께 갖는다.
- 예제 구현: 목록 리졸버는 게시물 배열을 그대로 반환하고, 작성자 리졸버는 source에서 식별자를 꺼내 사용자 맵을 조회한다.
- 첫 관찰: 스키마의 게시물 타입과 구현의 게시물 인터페이스가 명백히 다른데도 문제없이 동작한다.
- 차이를 드러내기: 게시물을 클래스로 정의해 인스턴스를 만들면 스키마 타입과 다르다는 점이 분명해진다.
- 타입 검사의 허점: TypeScript는 구조가 같으면 같은 타입으로 인정하므로, 클래스 인스턴스와 객체 리터럴을 섞어 배열에 담아도 컴파일이 통과한다.
- 실행 결과: 그 배열을 순회하며 인스턴스 여부를 찍어보면 첫 항목만 참이고 나머지는 거짓이 나온다.
- 극단적인 예: 목록 리졸버가 식별자만 담은 객체 배열을 반환하고, 식별자와 제목과 작성자 리졸버가 각각 그 값을 받아 원본에서 찾아 채우게 할 수도 있다.
- 결론: 상위 필드가 반환하는 값이 GraphQL 타입과 무관해도 아무 문제가 없다.
- Java 확인: DGS로 같은 구조를 만들어도 동작이 같다. DGS가 GraphQL Java 위에서 동작하므로 차이가 없다고 덧붙인다.
- 부작용 1: 잘 활용하면 좋지만 코드를 새로 접한 사람에게는 혼란을 줄 수 있다.
- 부작용 2: 타입 언어와 어울리지 않는 부분이 생긴다.
- 구체적 충돌: 코드 생성기로 리졸버 타입을 지정하는 프로젝트에서는, 목록 리졸버가 반환하는 값에 non-null인 작성자 필드가 없다는 타입 에러가 나 어쩔 수 없이 any를 쓰고 있다고 밝힌다.
- 호출 순서의 전제: 리졸버는 상위에서 하위 순으로 호출되므로 하위가 받는 값은 언제나 직전 상위가 반환한 것이다. 이 순서를 잡아야 source의 정체가 분명해진다.
- 활용 여지: 상위에서 무거운 조회를 미루고 키만 넘긴 뒤 실제로 요청된 필드의 리졸버에서만 조회하도록 나눌 수 있다.
- 대가: 대신 상위 리졸버의 반환값만 봐서는 어떤 필드가 어떻게 채워지는지 알 수 없어 코드를 따라 읽는 부담이 커진다.
왜 읽나리졸버가 받는 첫 인자의 정체가 모호했다면, 스키마 타입과 실제 전달 값이 별개라는 사실과 그것이 타입 도구와 부딪히는 지점까지 확인할 수 있다.