SpringBoot + GraphQL (1/2)

2023. 7. 13. 19:59Spring 기초

들어가며

최근에 TypeScript + GraphQL을 경험했습니다.
Spring 서버에서는 GraphQL을 어떤 방식으로 제공하는지 궁금해서 간단히 실습했습니다. 

이번 포스팅에서는 GraphQL이 무엇이며, 장/단점은 무엇인지에 대해 다룹니다.
SpringBoot에서 GraphQL을 어떻게 사용하는지는 다음 포스팅을 참고해주세요.

 

GraphQL이란?

GraphQL(gql)은 페이스북에서 만든 쿼리 언어입니다. 
'쿼리 언어' 하면 SQL이 가장 먼저 떠오르는데요, SQL은 데이터베이스 시스템에 저장된 데이터를 효율적으로 가져오는 것이 목적이고, gql은  클라이언트가 데이터를 서버로 부터 효율적으로 가져오는 것이 목적입니다.

따라서 sql의 문장(statement)은 주로 백앤드 시스템에서 작성하고 호출 하는 반면, gql의 문장은 주로 클라이언트 시스템에서 작성하고 호출 합니다.

그렇다면 페이스북은 왜 REST 방식이 아닌 GraphQL을 개발해서 사용하게 되었을까요?
그것은 REST API 호출 방식의 한계인 'Over-fetching'과 'Under-fetching' 문제를 해결하기 위해서입니다.

 

Over-fetching

사용할 데이터보다 더 많은 데이터를 받아오는 문제를 말합니다.
예로, 수학 선생님이 학생들의 수학 성적을 조회하려고 할 경우를 가정하겠습니다.

수학 선생님에게 필요한 데이터는 다음과 같이 [학번, 이름, 수학점수]일 것입니다.

[
{ "student_no" : "1", "student_name": "홍길동", "score_math": 60}, 
{ "student_no" : "1", "student_name": "고길동", "score_math": 60}, 
{ "student_no" : "1", "student_name": "저길동", "score_math": 60}
]

하지만 REST API를 이용해 요청을 보낼 경우 다음과 같은 데이터가 반환될 것입니다.

REST API
/classes/{반 Idx}/students

결과
[
{ "student_no" : "1", "student_name": "홍길동", "score_korean": 55, "score_math": 90, "score_english": 80, "sex": "남", "address": "서울시 마포구 합정동"},
{ "student_no" : "2", "student_name": "고길동", "score_korean": 35, "score_math": 80, "score_english": 80, "sex": "남", "address": "도쿄"},

{ "student_no" : "3", "student_name": "저길동", "score_korean": 15, "score_math": 70, "score_english": 100, "sex": "남", "address": "아랍에미리트”}
]

 

수학점수뿐만 아니라 학생이 가지고 있는 정보를 몽땅 받게됩니다.
그렇다고 API를 /classes/{반 Idx}/students/math_score로 설계한다면 국어/영어 과목에 해당하는 API도 추가로 필요할 것입니다. 

물론, 전송받는 데이터가 많아서 나쁠 것은 없겠지만, 전송되는 데이터의 양이 클 수록 네트워크 소모가 크다는 문제가 있습니다.
이러한 문제를 Over-fetching이라고 합니다.

 

Under-fetching

오버페칭과 반대 개념으로, 너무 적은 데이터를 받는 문제를 말합니다.
이번에는 가정을 조금 바꿔서, 수학 선생님이 6반에 대한 정보와 6반 학생의 명단 정보가 필요한 상황이라고 해보겠습니다.

REST API에서는 다음과 같은 플로우로 데이터를 얻을 것입니다.

1. 6반 정보 API 요청: classes/{반 idx}
2. 결과 수신 
{"classNo": 6, "teacher": "김담임", "studentNo": 25}

3. 6반 학생정보 API 요청: classes/{반 idx}/students
4. 결과 수신
[{ "student_no" : "1", "student_name": "홍길동", "score_korean": 55, "score_math": 90, "score_english": 80, "sex": "남", "address": "서울시 마포구 합정동"},
{ "student_no" : "2", "student_name": "고길동", "score_korean": 35, "score_math": 80, "score_english": 80, "sex": "남", "address": "도쿄"},
{ "student_no" : "3", "student_name": "저길동", "score_korean": 15, "score_math": 70, "score_english": 100, "sex": "남", "address": "아랍에미리트”}]

먼저, 반의 정보를 얻은 뒤 학생의 정보를 요청하는 API를 추가적으로 요청해야 합니다. 
페이지(View)가 복잡해질수록 필요한 데이터를 얻기 위해 호출하는 API 수는 증가할 것입니다. 

이는 REST API가 각 API마다 받을 수 있는 데이터가 정해져있기 때문에 발생하는 문제입니다. 

REST API 요청/응답

 

 

GraphQL의 장점

GraphQL을 이용하면 Over-fetching과 Under-fetching 문제를 해결할 수 있습니다.

 

장점1. 클라이언트가 필요한 데이터만을 요청할 수 있다.

클라이언트는 쿼리를 통해 필요한 데이터를 요청합니다. 
GraphQL은 클라이언트가 넘긴 쿼리를 해석한 뒤, 서버로 부터 필요한 데이터를 가져와서 클라이언트에게 반환합니다.

다음은 사용자의 로그인ID를 이용해 사용자의 이메일을 얻어오는 쿼리의 예시입니다.

query ($loginId: String!){
    findUser(loginId: $loginId){
        email
    }
}

만약 이메일뿐만 아니라 사용자의 이름까지 필요하다면 다음과 같이 쿼리에 데이터만 추가해주면 끝입니다.

query ($loginId: String!){
    findUser(loginId: $loginId){
        email
        username # 추가된 부분
    }
}

뿐만아니라, depth가 있어도 해당 객체의 필요한 필드만 요청할 수 있습니다. 따라서 반복문으로 DTO를 파싱하거나 필터링 할 필요가 없어지는 것입니다.

아래와 같이 student.mother의 필드 중에서 name이라는 필드만 요청할 수 있습니다.

query ($loginId: String!){
    findUser(loginId: $loginId){
        email
        username
	mother{
        	name
        }
    }
}

 

 

장점 2. 선언적 방식으로 데이터를 요청한다.

위의 쿼리에서 보셨듯, GraphQL을 이용하면 클라이언트가 서버로부터 수신하려는 데이터를 정확히 지정하는 선언적 방식으로 요청합니다.
따라서 어떤 데이터가 넘어오는지 직관적으로 이해할 수 있으며, 원하는 것을 지정하므로 효율적이고 유연하게 데이터를 가져올 수 있습니다.

 

장점 3. 단일 엔드포인트를 사용한다.

REST 방식에서는 URL과 HTTP Method에 따라 접근할 수 있는 데이터가 달라집니다. 
하지만 GraphQL에서는 하나의 엔드포인트를 이용하며 GraphQL 스키마(쿼리, 뮤테이션)에 따라 접근할 수 있는 데이터가 달라집니다.

  • REST API의 경우 아래와 같은 4개의 end point가 필요합니다.
    • [GET] /api/v1/user
    • [POST] /api/v1/user
    • [PUT] /api/v1/user/:id
    • [DELETE] /api/v1/user/:id

  • GraphQL는 아래와 같이 1개의 end point만 사용합니다.
    • [POST] /graphql

즉, 하나의 엔드포인트에 쿼리를 원하는 대로 요청하면 각기 다른 결과를 얻을 수 있다는 뜻입니다.

 

GraphQL 핵심 개념

마지막으로, GraphQL의 핵심 개념에 대해 간략히 살펴보겠습니다.

Query/Mutation

  • Query
    • 데이터를 받아올 때 사용합니다. CRUD 중 Read, HTTP method의 GET과 비슷합니다.
    • 여러개의 Query를 동시에 요청할 경우 쿼리는 병렬적으로 실행됩니다(Parallel)
  • Mutation
    • 데이터를 변경하는 작업에 사용됩니다. CRUD 중 C,U,D
    • Query와 달리 여러개의 Mutation을 동시 요청할 경우 직렬 실행됩니다(순차 처리)

 

일반쿼리/Operation

일반 쿼리와 오퍼레이션 네임 쿼리의 차이는 변수를 매개변수로 받는지 여부입니다.

코드로 직접 살펴보겠습니다.

// 일반 쿼리
{
  human(id: "1000") {
    name
    height
  }
}

// 오퍼레이션 네임 쿼리
query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

오퍼레이션 네임 쿼리를 이용하면 한 번의 네트워크 왕복으로 원하는 데이터를 몽땅 가져올 수 있습니다.

query getStudentInfomation($studentId: ID){
  personalInfo(studentId: $studentId) {
    // 개인정보들
  }
  classInfo(year: 2018, studentId: $studentId) {
    // 수업정보들
  }
  SATInfo(schoolCode: 0412, studentId: $studentId) {
    // SAT에 대한 정보들
  }
}

getStudentInformation 이라는 오퍼레이션 네임 쿼리를 이용해 단 한번의 네트워크 요청으로 3가지 정보를 모두 불러오는 모습입니다.


바로 다음 포스팅에서는 SpringBoot를 이용해 GraphQL을 간단히 실습하는 내용을 다루겠습니다.