안녕하세요. J4J입니다.
이번 포스팅은 GraphQL사용을 위한 환경 설정 방법에 대해 적어보는 시간을 가져보려고 합니다.
GraphQL이란?
GraphQL은 API를 위한 쿼리 언어로 기존에 서버와 클라이언트 간 데이터 전달을 위해 많이 사용되는 Rest API의 단점들을 보완해줄 수 있는 기술입니다.
Rest API의 단점은 다음과 같이 크게 2가지가 있습니다.
- OverFetching (오버패칭)
- UnderFetching (언더패칭)
OverFetching은 클라이언트에서 실제로 사용되는 데이터만 불러오지 않고 사용되지 않는 데이터도 함께 불러옴으로 써 리소스의 낭비를 발생시키는 것을 의미합니다.
예를 들어 사용자 정보 중 사용자 아이디를 화면에서 사용하기 위해 API를 요청하게 되면 일반적으로 사용자 아이디뿐만 아니라 다음과 같이 사용자의 부가 정보들도 같이 Response로 전달됩니다.
{
"id": 1,
"name": "철수",
"age": 23
}
실제로 사용에 필요한 데이터는 id 뿐이지만 필요하지 않은 데이터들도 함께 넘어오게 됨으로 써 리소스 낭비가 발생됩니다.
이런 현상을 OverFetching이라고 부릅니다.
UnderFetching은 클라이언트에서 데이터를 활용하기 위해 API를 요청할 때 EndPoint마다 Response 되는 값들이 정해져 있어서 필요한 데이터들을 모두 불러오기 위해 여러 개의 API를 요청하는 것을 의미합니다.
여기서 EndPoint란 Controller에서 만들어지는 하나의 API 매핑이라고 생각하시면 됩니다.
예를 들어 화면에서 사용자 정보와 학교의 정보가 모두 사용된다면 일반적으로 다음과 같은 두 개의 API 요청을 서버에 전달하게 됩니다.
모든 사용자 정보 → /api/user
모든 학교 정보 → /api/school
예시는 2개지만 한 화면에 데이터를 활용하기 위해 보내는 요청들이 많이 존재하기 때문에 필요한 데이터가 많을수록 이 또한 리소스 낭비가 발생됩니다.
이런 현상을 UnderFetching이라고 부릅니다.
장점
GraphQL은 Rest API의 단점인 OverFetching과 UnderFetching의 문제를 해결해줍니다.
OverFetching의 해결 방법은 다음과 같습니다.
- 쿼리문을 이용하여 클라이언트가 전달받을 데이터를 지정
예를 들어 위에서 언급한 예시처럼 사용자 아이디를 활용하기 위해 사용자 정보를 서버에 요청했을 때 서버는 id, name, age 등을 모두 전달해줄 수 있지만 다음과 같은 쿼리문을 이용하여 화면에서 필요한 id만 전달받을 수 있습니다.
query {
user {
id
}
}
UnderFetching의 해결 방법은 다음과 같습니다.
- 하나의 EndPoint에 필요한 데이터를 한 번에 요청
GraphQL은 Rest API와 다르게 하나의 EndPoint만 존재합니다.
그래서 API 요청을 할 때 EndPoint마다 전달 가능한 데이터가 정해져 있지 않기 때문에 한 번에 여러 데이터를 다음과 같이 요청할 수 있습니다.
query {
user {
id
age
}
school {
id
name
}
}
단점
어떻게 보면 GraphQL이 Rest API보다 훨씬 좋아 보인다라고도 생각할 수 있지만 장점이 있는 만큼 다음과 같은 단점도 가지고 있습니다.
- 사용되는 GraphQL을 위한 스키마를 추가 작성
GraphQL도 Rest API에서 Controller를 구성하듯이 GraphQL이 데이터 처리를 할 수 있는 스키마를 구성해줘야 합니다.
사실 여기까지는 Rest API와 다를 게 없다고 느낄 수 있지만 객체들에 대한 정보들도 추가적인 스키마를 구성해줘야 합니다.
그렇기 때문에 자바로 구현되는 클래스와 별개로 스키마까지 따로 관리해줘야 하는 단점을 가지고 있습니다.
- 항상 고정된 Response만 있을 경우 불필요
클라이언트에서 Response 받는 값이 항상 정해져 있다면 GraphQL의 효용성은 작아지게 됩니다.
일반적으로 서로 다른 화면에서 동일한 API 호출을 했을 때 각 화면에서 전달받아 사용돼야 하는 데이터들이 서로 다르기 때문에 Rest API보다 GraphQL의 효용성이 더 높아지는 것으로 알고 있습니다.
하지만 Response 받는 값이 항상 동일하다면 쿼리 작성을 위한 추가 리소스 작업만 생긴다는 단점을 가지게 됩니다.
Rest API 예시
이젠 GraphQL과 Rest API를 동일한 기능을 간단히 구현해보며 비교해보겠습니다.
모든 설정을 다 하지는 않고 GraphQL과 Rest API를 사용했을 때 서로 다른 점만 보여드리도록 하겠습니다.
먼저 많은 사람들에게 친근한 Rest API를 JPA를 이용하여 구현해보겠습니다.
참고적으로 데이터베이스 테이블 레이아웃은 다음과 같습니다.
// MySQL
create table person (
id int auto_increment primary key,
name varchar(50),
phone varchar(50),
age int
);
create table pet (
id int auto_increment primary key,
person_id int,
name varchar(50)
);
[ 1. Entity ]
package com.spring.restApi.entity;
import java.io.Serializable;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "person")
public class PersonEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private String phone;
private int age;
@OneToMany
@JoinColumn(name = "person_id")
private List<PetEntity> pets;
}
package com.spring.restApi.entity;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "pet")
public class PetEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private int person_id;
private String name;
}
[ 2. Repository ]
package com.spring.restApi.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.spring.restApi.entity.PersonEntity;
public interface PersonRepository extends JpaRepository<PersonEntity, Integer>{
}
[ 3. Service ]
package com.spring.restApi.service;
import java.util.List;
import com.spring.restApi.entity.PersonEntity;
public interface PersonService {
public List<PersonEntity> persons();
public PersonEntity person(PersonEntity person);
public PersonEntity savePerson(PersonEntity person);
public void deletePerson(PersonEntity person);
}
package com.spring.restApi.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.spring.restApi.entity.PersonEntity;
import com.spring.restApi.repository.PersonRepository;
@Service
public class PersonServiceImpl implements PersonService {
@Autowired
PersonRepository personRepository;
@Override
public List<PersonEntity> persons() {
return personRepository.findAll();
}
@Override
public PersonEntity person(PersonEntity person) {
return personRepository.findById(person.getId()).get();
}
@Override
public PersonEntity savePerson(PersonEntity person) {
return personRepository.save(person);
}
@Override
public void deletePerson(PersonEntity person) {
personRepository.deleteById(person.getId());
}
}
[ 4. Controller ]
package com.spring.restApi.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.spring.restApi.entity.PersonEntity;
import com.spring.restApi.service.PersonService;
@RestController
public class PersonController {
@Autowired
PersonService personService;
@GetMapping("/persons")
public ResponseEntity<Object> persons() {
return new ResponseEntity<Object>(personService.persons(), HttpStatus.OK);
}
@GetMapping("/person")
public ResponseEntity<Object> person(PersonEntity person) {
return new ResponseEntity<Object>(personService.person(person), HttpStatus.OK);
}
@PostMapping("/savePerson")
public ResponseEntity<Object> savePerson(@RequestBody PersonEntity person) {
return new ResponseEntity<Object>(personService.savePerson(person), HttpStatus.OK);
}
@DeleteMapping("/deletePerson")
public ResponseEntity<Object> deletePerson(PersonEntity person) {
personService.deletePerson(person);
return new ResponseEntity<Object>(true, HttpStatus.OK);
}
}
위와 같이 설정을 하고 persons API를 호출해보면 다음과 같이 Response가 오는 것이 확인됩니다.
GraphQL 예시
GraphQL 설정을 해보기 전에 하나 알고 가셔야 될 점이 있습니다.
GraphQL을 위한 스키마를 작성할 때 Rest API에 GET, POST, PUT, DELETE가 있는 것처럼 GraphQL에는 Query, Mutation이 존재합니다.
Query는 R(select)을 위한 것이고 Mutation은 C(insert), U(update), D(delete)를 위한 것입니다.
이 개념들을 참고하여 위와 같이 구현된 Rest API를 GraphQL에서는 어떻게 코드를 작성해야 하는지 한번 구현해보겠습니다.
[ 1. Dependency 추가 ]
<dependencies>
<!-- graphql -->
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.2.4</version>
</dependency>
<dependency>
<groupId>io.leangen.graphql</groupId>
<artifactId>graphql-spqr-spring-boot-starter</artifactId>
<version>0.0.4</version>
</dependency>
</dependencies>
[ 2. 스키마 추가 ]
GraphQL에서 기본적으로 스키마로 인식되는 파일은 .graphqls에 해당하는 확장자입니다.
그렇기 때문에 /src/main/resources 위치에 person.graphqls 파일을 만들어서 다음과 같이 스키마를 작성해보겠습니다.
# Query root
type Query {
persons: [PersonGraphqlEntity]
person(id: Int!): PersonGraphqlEntity
}
type PersonGraphqlEntity {
age: Int!
id: Int!
name: String
pets: [PetGraphqlEntity]
phone: String
}
type PetGraphqlEntity {
id: Int!
name: String
person_id: Int!
}
# Mutation root
type Mutation {
deletePerson(person: PersonGraphqlEntityInput): Boolean!
savePerson(person: PersonGraphqlEntityInput): PersonGraphqlEntity
}
input PersonGraphqlEntityInput {
name: String
phone: String
id: Int!
age: Int!
pets: [PetGraphqlEntityInput]
}
input PetGraphqlEntityInput {
id: Int!
person_id: Int!
name: String
}
여기서 Query는 위에서 언급한 R(select)를 위해 사용되고 Mutation도 동일하게 C(insert), U(update), D(delete)를 위해 사용되는 것을 작성합니다.
또한 콜론(:)을 기준으로 왼쪽은 컬럼명 오른쪽은 타입을 의미하고 타입에 !가 붙어있는 것은 null값을 허용하지 않는다는 뜻입니다.
[ 3. Entity ]
package com.spring.graphQL.entity;
import java.io.Serializable;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "person")
public class PersonEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private String phone;
private int age;
@OneToMany(fetch = FetchType.EAGER) // 어노테이션으로 하지 않을 경우 EAGER처리, Service단에서 @Transaction로 해결이 안됌 ㅠㅠ
@JoinColumn(name = "person_id")
private List<PetEntity> pets;
}
package com.spring.graphQL.entity;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "pet")
public class PetEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private int person_id;
private String name;
}
[ 4. Repository ]
package com.spring.graphQL.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.spring.graphQL.entity.PersonEntity;
public interface PersonRepository extends JpaRepository<PersonEntity, Integer> {
}
[ 5. Service ]
package com.spring.graphQL.service;
import java.util.List;
import com.spring.graphQL.entity.PersonEntity;
public interface PersonService {
public List<PersonEntity> persons();
public PersonEntity person(int id);
public PersonEntity savePerson(PersonEntity person);
public boolean deletePerson(PersonEntity person);
}
package com.spring.graphQL.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import com.spring.graphQL.entity.PersonEntity;
import com.spring.graphQL.repository.PersonRepository;
@Service
public class PersonServiceImpl implements PersonService, GraphQLQueryResolver, GraphQLMutationResolver {
@Autowired
PersonRepository personRespository;
/*
query {
persons {
id
name
pets {
id
name
person_id
}
}
}
*/
@Override
public List<PersonEntity> persons() {
return personRespository.findAll();
}
/*
query {
person(id: 3) {
id
age
pets {
id
name
person_id
}
}
}
*/
@Override
public PersonEntity person(int id) {
return personRespository.findById(id).get();
}
/*
mutation {
savePerson (
person: {
id: 0,
name: "myName",
phone: "010-xxxx-xxxx",
age: 33
}
) {
id
name
}
}
*/
@Override
public PersonEntity savePerson(PersonEntity person) {
return personRespository.save(person);
}
/*
mutation {
deletePerson(
person: {
id: 10,
age: 0
}
)
}
*/
@Override
public boolean deletePerson(PersonEntity person) {
personRespository.deleteById(person.getId());
return true;
}
}
위와 같이 설정을 한 뒤 포스트맨에서 테스트를 해보겠습니다.
하나 알고 가셔야 될 점은 GraphQL은 EndPoint가 동일하기 때문에 URL은 /graphql로 모두 동일하게 사용됩니다.
( + GraphQL을 사용할 때 Controll 구간이 따로 필요없는 이유가 됩니다.)
또한 모든 HTTP Method는 POST로 사용됩니다.
데이터를 조회하는 쿼리문을 작성할 때 id와 name만 입력했기 때문에 Response로 id와 name만 오는 것을 확인할 수 있습니다.
하지만 단방향 매핑이 되어 있는 pet정보까지 얻고자 한다면 다음과 같이 수정해주면 됩니다.
GUI
개발된 GraphQL을 테스트할 때 포스트맨 말고도 서버 자체에서 자체적으로 GUI를 만들어서 테스트할 수도 있습니다.
GUI 테스트하는 방법은 다음과 같습니다.
[ 1. application.yml에 gui 설정 추가 ]
graphql:
spqr:
gui:
enabled: true
[ 2. 서버 실행 후 /gui로 접속 ]
이렇게 GUI를 사용한다면 설정한 스키마들도 같이 확인할 수 있다는 장점을 제공해줍니다.
스키마를 작성하지 않는 GraphQL 예시
GraphQL을 아직 깊게 사용해보지는 않았지만 알 수 있는 것은 스키마를 작성하는 일은 까다롭게 느껴질 것이라고 생각했습니다.
그래서 스키마를 작성하지 않고도 GraphQL을 사용하는 방법에 대해 찾아봤고 GraphQL을 사용할 때 스키마를 작성하지 않고 구현하기를 원하시는 분은 다음 구현 코드를 참고하시면 될 것 같습니다.
[ 1. Dependency 추가 ]
<dependencies>
<!-- graphql -->
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.2.4</version>
</dependency>
<dependency>
<groupId>io.leangen.graphql</groupId>
<artifactId>graphql-spqr-spring-boot-starter</artifactId>
<version>0.0.4</version>
</dependency>
</dependencies>
[ 2. Entity ]
package com.spring.graphQL.entity;
import java.io.Serializable;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import io.leangen.graphql.annotations.types.GraphQLType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "person")
@GraphQLType
public class PersonEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private String phone;
private int age;
@OneToMany
@JoinColumn(name = "person_id")
private List<PetEntity> pets;
}
package com.spring.graphQL.entity;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import io.leangen.graphql.annotations.types.GraphQLType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "pet")
@GraphQLType
public class PetEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private int person_id;
private String name;
}
[ 3. Repository ]
package com.spring.graphQL.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.spring.graphQL.entity.PersonEntity;
public interface PersonRepository extends JpaRepository<PersonEntity, Integer> {
}
[ 4. Service ]
package com.spring.graphQL.service;
import java.util.List;
import com.spring.graphQL.entity.PersonEntity;
public interface PersonService {
public List<PersonEntity> persons();
public PersonEntity person(int id);
public PersonEntity savePerson(PersonEntity person);
public boolean deletePerson(PersonEntity person);
}
package com.spring.graphQL.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.spring.graphQL.entity.PersonEntity;
import com.spring.graphQL.repository.PersonRepository;
import io.leangen.graphql.annotations.GraphQLMutation;
import io.leangen.graphql.annotations.GraphQLQuery;
import io.leangen.graphql.spqr.spring.annotations.GraphQLApi;
@Service
@GraphQLApi
public class PersonServiceImpl implements PersonService {
@Autowired
PersonRepository personRespository;
/*
query {
persons {
id
name
pets {
id
name
person_id
}
}
}
*/
@Override
@GraphQLQuery
public List<PersonEntity> persons() {
return personRespository.findAll();
}
/*
query {
person(id: 3) {
id
age
pets {
id
name
person_id
}
}
}
*/
@Override
@GraphQLQuery
public PersonEntity person(int id) {
return personRespository.findById(id).get();
}
/*
mutation {
savePerson (
person: {
id: 0,
name: "myName",
phone: "010-xxxx-xxxx",
age: 33
}
) {
id
name
}
}
*/
@Override
@GraphQLMutation
public PersonEntity savePerson(PersonEntity person) {
return personRespository.save(person);
}
/*
mutation {
deletePerson(
person: {
id: 10,
age: 0
}
)
}
*/
@Override
@GraphQLMutation
public boolean deletePerson(PersonEntity person) {
personRespository.deleteById(person.getId());
return true;
}
}
위와 같이 설정할 경우 스키마를 따로 개발자가 작성하지 않아도 되고 스키마 작성을 어노테이션이 대신 처리해줍니다.
만약 어노테이션이 작성한 스키마가 궁금하신 분들은 GUI에 접속하셔서 스키마 탭을 확인하면 자동으로 만들어진 스키마를 확인할 수 있습니다.
참조
[GraphQL] Spring Boot + 그래프QL 사용하기 (CRUD)
이상으로 GraphQL사용을 위한 환경 설정 방법에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'Spring > SpringBoot' 카테고리의 다른 글
[SpringBoot] AWS S3에 파일 업로드하기 (1) | 2022.04.23 |
---|---|
[SpringBoot] 환경 변수 파일 사용하기 (0) | 2022.03.28 |
[SpringBoot] 파일 다운로드 (0) | 2021.06.07 |
[SpringBoot] 파일 업로드 - MultipartFile(With. React) (0) | 2021.05.27 |
[SpringBoot] 다중 DB 및 다중 개발환경에서 JNDI 설정 (0) | 2021.05.25 |
댓글