개발

GraphQL API, 설계할 때 빼먹으면 나중에 아픈 것들

스키마·리졸버·DataLoader·쿼리 제한까지, 튜토리얼에서 잘 안 말해 주는 운영 쪽 이야기 위주로 정리했습니다. 코드는 최소만 넣었어요.

GraphQL API, 설계할 때 빼먹으면 나중에 아픈 것들

GraphQL을 처음 쓸 때 “필요한 필드만 달라”는 말만 기억하는 경우가 많습니다. 맞는 말인데, 그 한 줄 뒤에 리졸버가 몇 번 도는지는 잘 안 보입니다. GraphiQL에서 예쁘게 나온 응답이, 트래픽 붙으면 DB를 갈가리는 패턴으로 바뀌기 쉽거든요. 이 글은 문법 강의가 아니라, 우리가 내부·파트너용 API를 GraphQL로 굴리면서 처음 설계에 박아 두지 않으면 돌이키기 싫어지는 것들만 골라 적었습니다.

GraphQL 서버 레이어

스키마는 그냥 타입 정의가 아니라 계약

SDL이 길어질수록 “이 필드는 언제 null이지?”, “이 관계는 누가 비용을 치르나?”가 팀 안에서 말로만 오가기 시작합니다. 스키마 리뷰할 때 비싼 필드(외부 API, 무거운 집계)를 표시해 두거나, 아예 별도 쿼리로 빼는 식으로 경계를 먼저 정하는 편이 나중에 덜 싸웁니다.

프론트는 user { posts { author { posts { ... } } } } 같은 걸 아무 생각 없이 붙일 수 있습니다. 막는 장치가 없으면 그게 그대로 허용됩니다.

리졸버를 “모델 메서드”처럼만 쓰면 N+1이 온다

가장 흔한 패턴입니다. User.posts에서 글 목록을 가져오고, Post.author에서 다시 유저를 한 명씩 가져옵니다. 목록이 50개면 author만 50번 도는 식이죠. 로컬에 데이터가 적을 땐 절대 안 티 나다가, 스테이징에서 갑자기 “DB CPU가 왜…”로 이어집니다.

DataLoader는 거의 기본 옵션에 가깝다

한 요청 안에서 같은 키로 여러 번 불리는 로드를 모아서 한 방에 치우는 게 DataLoader의 역할입니다. “최적화 테크닉”이라기보다, 그냥 운영 가능한 API를 만들려면 필요한 층에 가깝게 생각해도 됩니다.

요청이 끝나면 로더 인스턴스는 버리는 게 일반적입니다. 글로벌 싱글톤으로 두면 캐시가 요청을 넘어 섞여서 이상한 버그가 납니다.

깊이·비용 제한은 “악의 없는” 쿼리도 잡는다

내부 도구에서 실수로 깊게 중첩된 쿼리를 한 번 날리면 서버가 한동안 멍해질 수 있습니다. graphql-depth-limit이나 graphql-query-complexity 같은 걸로 일단 상한을 걸어 두는 건, 보안 이야기만이 아니라 팀 실수 방지에 가깝습니다.

숫자는 서비스에 맞게 조절하면 되고, 중요한 건 “왜 이 숫자냐”를 팀이 한 번은 합의해 두는 것입니다.

필드마다 권한을 안 걸면 REST 때보다 더 잘 새는 편

REST는 URL 단위로 막기도 했는데, GraphQL은 한 엔드포인트에 필드가 수십 개 붙습니다. Query.me는 로그인한 사람만, User.email은 본인·관리자만 같은 식으로 리졸버 앞단에서 가드를 거는 패턴이 필요합니다. 미들웨어 한 방으로 끝나지 않는다는 걸 늦게 알면 리팩터링 범위가 커집니다.

캐싱은 “필드 단위로 Redis”가 답은 아님

필드마다 키를 뜯어서 Redis에 넣는 예제는 많습니다. 실제로는 무효화(mutation 이후 어떤 키를 지울지)가 더 어렵습니다. 퍼블릭하고 변하지 않는 읽기 위주면 도움이 되고, 유저별로 자주 바끼는 데이터에 무작정 넣으면 캐시 적중률은 낮고 버그만 늘기 쉽습니다. HTTP 캐시·CDN과도 잘 안 맞는 경우가 많아서, “GraphQL이라서 캐시가 어렵다”는 말이 나오기도 하고요.

서브스크립션은 인프라 이야기가 본체

PubSub 예제는 따라 치기 쉬운데, 프로덕션에선 스케일 아웃한 서버 여러 대에서 이벤트가 통해야 합니다. Redis나 전용 브로커 없이 메모리 PubSub만 쓰면 나중에 “왜 B서버 구독자는 안 오지?”로 갑니다.

에러 형태를 통일해 두지 않으면 클라이언트가 미친다

extensions.code를 쓸지, HTTP 상태를 어떻게 줄지 팀마다 갈리는데, 한 서비스 안에서는 한 가지 규칙으로 고정하는 게 좋습니다. 프로덕션에서 스택 트레이스를 그대로 내려주지 말지 같은 것도 여기서 같이 정합니다.

관측은 리졸버 단위로라도 찍어본다

필드별로 ms를 찍어 보면 “생각지도 못한 필드”가 상위에 올라옵니다. Apollo Studio 같은 관리형 도구를 쓰든, OpenTelemetry로 붙이든, 가장 비싼 필드 이름이 뭔지는 숫자로 보는 게 싸움을 줄입니다.

마무리

GraphQL이 나쁜 게 아니라, 유연한 만큼 운영 규칙을 코드로 박아 두는 일이 REST 때보다 더 많이 필요합니다. 스키마 합의, DataLoader, 깊이·비용 제한, 필드 단위 권한, 에러·관측까지. 이걸 “나중에”로 미루면 그때는 이미 클라이언트가 그래프에 의존하고 있어서 고치기 무섭습니다. 처음부터 조금 촌스럽게 규칙을 많이 잡아 두는 쪽이, 이상하게 오래 삽니다.

참고

공유하기

관련 포스트