들어가며
새롭게 입사한 회사에서 graphQL을 사용하고 있어서 처음으로 배우고 있다. 본격적인 업무에 들어가기 앞서 graphQL에 대해 공부하며 그 경험과 활용 철학들을 공유받은 입장이다보니 경험은 없으면서 그 내용은 상당히 성숙한 내용들이 있을 수 있다. 이 글은 literally는 당연히 이해하고 쓰는 글이지만, REST API를 사용해서 겪었던 고통들이 무엇인지, graphQL을 도입하는 과정에서 어떤 문제들을 겪고 고민하며 발전시켜왔는지에 대해 상당히 피상적으로 받아들인 부분이 있으므로 어쩌면 추상적으로 적힌 부분이나 잘못 받아들이고 쓴 오류들이 존재할 수 있다. 이 글은 이제 주니어도 아닌 엔트리 레벨의 프론트엔드 개발자가 처음 graphQL에 대해 이론적으로 공부하며 작성한 글임을 꼭 염두에 두고 읽어주길 바란다.
GraphQL이란?
항상 새로운 기술을 배울 때는 이 기술이 어떤 기술인지도 중요하지만 어떤 문제를 해결하고 있는지, 그래서 내가 왜 이 기술을 사용해야하는지에 대한 고민이 선행되어야한다고 생각한다. GraphQL은 기존 REST API의 한계점을 극복하기 위해 Meta(전 facebook)가 고안한 query language이다. 그러니까 REST API의 어떤 부분이 불편해서 다른 방법을 찾으려고 했는지를 알게되면, graphQL의 존재 의의와 왜, 언제 사용해야하는 지에 대한 그림이 나올 것이다.
graphQL이 극복하고자한 REST API의 한계들은 다음과 같다.
1. overfetching: REST API는 resource를 기반으로하기 때문에, 일부 속성만 필요하다고해도 resource의 전체 데이터를 모두 불러온다. 작은 단위의 프로젝트에서는 크게 문제가 되지 않을 수도 있지만, product가 커지고 방대한 데이터들을 다루게되면 overfetching으로 인한 성능 이슈가 드러나게 된다. graphQL에서는 필요한 필드만 명시적으로 요청할 수 있기 때문에 해당 문제를 근본적으로 해결한다고 볼 수 있다.
❓ 이 문제를 REST API 자체적으로 해결하는 방법은 없을까? 물론, REST API에서도 일부 방법을 통해 이러한 문제를 해결할 수는 있다. 예를 들어, REST API에서 필요한 데이터만을 가져오기 위해 커스텀 엔드포인트를 만들거나, 데이터 필터링 기능을 추가할 수도 있다. 하지만 이러한 해결책은 API의 복잡성을 증가시킬 수 있고, 특정 상황에서만 적용 가능한 제한적인 해결책으로, 이 문제를 근본적으로 해결한다고 보기는 어렵다.
2. underfetching: REST API에서는 특정 데이터를 위해 여러개의 엔드포인트를 통해 데이터를 요청해야하는 상황이 발생하기도한다. 이로인한 네트워크 오버헤드가 발생할 수 있다. graphQL에서는 필요한 데이터구조를 정의하고, 서버에서는 그 구조에 맞춰 데이터를 반환하므로 하나의 쿼리로 원하는 여러 데이터들을 한번에 가져올 수 있다.
3. API 버전관리문제: REST API에서 새로운 기능을 추가하거나 기존 기능을 변경할 때 새로운 버전의 엔드포인트를 만들어야한다. 이로인해 API 유지보수가 복잡해지고, 클라이언트와 서버간의 버전관리가 어려워진다. graphQL은 엔드포인트가 아닌 schema 기반으로 작동하기 때문에 새로운 필드를 추가하거나 변경할 때 기존 쿼리에 영향을 주지 않고 스키마를 확장할 수 있다.
4. 서버-클라이언트 의존성문제: rest api에서 클라이언트는 서버 엔드포인트 구조에 맞춰 데이터를 요청하고 파싱해야한다. 따라서 서버에서의 변경이 발생하면 클라이언트에서 코드를 수정해야하는 경우도 발생한다. graphQL에서는 스키마를 기반으로 클라이언트에서 필요한 데이터 구조를 요청하고 그에 맞는 데이터를 반환하는 구조이므로, 서버와 클라이언트의 의존성을 보다 낮춘다고 볼 수 있다.
언제 GraphQL을 사용해야할까?
위 내용을 읽다보면 GraphQL을 선택하지 않을 이유가 없다는 생각이 든다. 그러나 당연하게도 GraphQL에도 다양한 문제들이 존재한다. 단일 엔드포인트가 REST의 많은 애로사항을 해결해주었지만, 그에 따른 단점들이 따라온다. 요청이 3천 건이 넘어가면 REST보다 성능이 떨어진다는 연구결과가 있는가 하면, 단일 엔드포인트 + 공개된 스키마로 인한 보안 위협도 존재한다. 또한 HTTP 내장 캐시 기능을 사용하기도 어렵다.
그러나 ‘이런 문제점들 때문에 graphQL를 많이 사용하지 않는구나’, 이런 느낌보다는 둘은 채택해야하는 경우가 조금 다르다. REST API가 자원을 효율적으로 주고 받는 방식에 대한 고민에서 고안된 것이라면 graphQL은 화면구현을 더 잘하고자하는 고민에서 시작되었다. 따라서 서비스의 종류에 따라 자원 통신에 더 집중해야하는 경우라면 REST API를 선택하는 것이 옳을 수 있다. 결론적으로 graphQL이 REST의 일부 문제를 해결한다고 해서 무조건 우월한 것은 아니며 위와 같은 단점이 있다고 해서 REST보다 열등한 것도 아니다. ‘그래서 graphQL와 REST API중 어떤 걸 써야하는건데?’는 그저 팀이 만들고자 하는 서비스가 어떤 서비스인지, 서비스 복잡도는 어느정도인지, 팀의 기술수준은 어떠한지 등등 다양한 부분을 고민하고 의사결정을 해야하는 문제인 것이다.
여기서 자원통신에 더 집중해야하는 서비스는 쉽게 말해 UI를 그릴 필요가 없는 경우를 말한다. 가령 구글 맵을 연동하는 것, 네이버 페이를 연동하는 것 등 유저가 보지 않는 내부기능을 구현하기 위한 통신을 하는 경우에는 REST API를 사용하는 것이 더 유리할 수 있다. 그러나 UI를 그리는 부분에 대해서는 화면을 그리는데 필요한 다양한 데이터들을 한번의 요청으로 받아올 수 있는 graphQL이 더 많은 이점을 가진다.
GraphQL은 선언형 프로그래밍 언어이다.
프로그래밍 패러다임은 명령형(imperative)프로그래밍과 선언형(declarative)프로그래밍으로 나뉜다. 여기서 graphQL은 선언형 프로그래밍 언어에 속한다. graphQL에 대한 설명을 이어가기 전에 명령형 프로그래밍과 선언형 프로그래밍이 무엇인지 간단하게 정리해보았다.
명령형 프로그래밍은 가장 오래되고, 가장 기본적인 프로그래밍 접근법이다. 우리가 흔히알고 있는 Java, Javascript, C, Python, Ruby등 대부분의 프로그래밍 언어들은 명령형 프로그래밍 언어이다. 명령형 프로그래밍 코드는 개별의 명령문, 지시, 함수호출등이 이용하여 프로그램의 수행 과정을 순서대로 묘사한다. 만약 프로그램이 어떻게 작동하는지, 혹은 지시문을 전달하고 싶다면 명령형 프로그래밍 방식을 채택하는 것은 옮다. 그러나 만약 그것이 어떻게 작동하는지 보단 프로그램이 달성하길 바라는 것에 관심이 있다면 선언형 프로그래밍 방식이 옳은 결정이다.
그러나 명령형 프로그래밍 언어들이 명령형으로만 작성되어야하는 것은 아니다. 상황에따라 선언적으로 작성할수도 있고, 실제로 코딩을하다보면 두 프로그래밍 방식을 모두 수행하게된다. 가령 어떤 재사용가능한 모듈을 작성하는 입장이라면 그 모듈을 작성하는 입장에서 이 모듈이 어떤 일을 수행하는지 명령형으로 작성해야함은 명확하다. 그러나 그 모듈을 가져다 쓰는 입장을 고려했을 때, 그 모듈이 어떻게 작동하는 지 보단 어떤 값을 도출하는지와 같은 것에 관심이 있기 때문에 선언적인 네이밍으로 작성되는 것이 옳을 것이다.
명령형 프로그래밍언어들이 선언형 프로그래밍을 할 수 있는데 반해 선언형 프로그래밍 언어들로 명령형 프로그래밍을 하는 것은 불가능하거나 가능하더라도 권장되지 않는다. 따라서 선언형 프로그래밍 언어인 graphQL이 명령형으로 작성되는 것은 가능하지도 않고 자연스럽지도 않다. 선언형 프로그래밍에서는 내가 프로그램에게 원하는 것을 생각해보는 것이 보다 명시적인 코드를 작성하는데 도움을 줄 수 있다. 즉 graphQL의 schema는 프론트엔드 개발자가 필요로 하는 것, 기대하는 결과를 선언하는 방식으로 작성되어야 바람직하다.
왜 Fragment-driven development를 해야하는가?
graphQL을 사용하기만 한다고 해서 overfetching, underfetching 등 위에서 나열한 문제가 마법처럼 해결되는 것이 아니다. graphQL을 ‘잘’ 사용해야한다. graphQL을 잘 사용한다는 것은 Fragment-driven하게 개발한다는 것이다. 그리고 Fragment-driven하게 개발한다는 것은 화면을 구성하는 컴포넌트마다 표현해야할 데이터 fragment가 완전히 일치하도록 한다는 것이다.
개별 컴포넌트가 필요한 fragment만을 props으로 넘겨받는 형식이 아닌 부모 컴포넌트가 불러온 데이터를 자식 컴포넌트들이 공유하기 시작하면 컴포넌트간 의존성이 발생하게 된다. 이때 기획의 변동 등의 이유로 어떤 컴포넌트에서 특정 필드를 사용하지 않게되어 삭제를 하게되면, 그 필드를 사용하는 다른 컴포넌트에 버그가 발생할 수 있다. 그리고 이러한 문제는 대규모의 팀이 개발을 진행할 경우 즉시 드러나지 않을 수도 있다. 이것은 어떤 코드 리뷰나 자동화 테스트 등으로 극복할 수 없는 시스템적인 문제다.
그래서 각각의 컴포넌트들은 각각 자신만의 데이터 fragment를 갖는 것이 이상적이다. relay에서는 다른 컴포넌트에서 사용하는 데이터 필드를 현재 컴포넌트에서 사용하지 못하도록 강제하기 위해서 데이터 마스킹기능을 제공하기도 한다. relay에서는 해당 기능을 강제하지만 apollo의 경우 강제하지 않는다. 그러나 relay를 사용하지 않는 개발자일지라도 graphQL의 철학과 사용의의를 고려하여 의식적으로 fragment-driven한 개발을 할 수 있도록 해야할 것이다.
팀에서 fragment-driven한 개발 문화가 합의되면, 향후 컴포넌트 간 의존성에 의한 버그 발생률을 크게 낮출 수 있고, 개발할 때 다른 컴포넌트와의 의존성을 고려하지 않아도 되므로 개발 생산성이 폭발적으로 높아질 수 있을 것이다.
References
GraphQL vs. REST in 2023: Top 4 Advantages & Disadvantages
Declarative vs imperative programming: 5 key differences
댓글