[GraphQL] GraphQL 처음 시작하기
GraphQL이 뭘까?
GraphQL은 페이스북에서 만든 쿼리 언어입니다. 소식에 조금 늦은 저는 2020년에 처음 들었는데 최근 들어서 GraphQL 소식이 자주 들려오고 있습니다. 현재의 인기는 어느 정도일까요?
아래의 그래프를 보면 GraphQL에 대한 관심도가 어느 정도인지 알 수 있습니다. 꾸준하게 사람들에게 언급되고 배우려고 하는 사람, 사용하고 있는 사람들이 많이 늘고 있습니다.
일단 GraphQL은 뒤에 QL이라고 붙는 것을 보면 SQL(Structed Query Language)과 마찬가지인 것 같은데 사용되는 예시를 살펴보면 매우 다릅니다. GraphQL은 웹 클라이언트와 서버 사이에 사용되고 SQL은 서버와 데이터베이스 사이에 사용됩니다. 그래서 웹 데이터를 처리하는 방법인 REST와 비교하는 것이 적절할 것 같습니다.
GraphQL이란
기존의 REST 방식으로 요청하고 SQL을 사용하여 데이터를 조회하는 방식과 GraphQL의 차이를 살펴보겠습니다.
REST 방식에서는 아래와 같이 특정 URL로 요청을 보내고 그 요청에 해당하는 데이터를 다시 클라이언트에게 전달합니다. 하지만 사용자가 원하는 데이터를 조회하려면 서버 측에서 제공을 해줘야 하고 원하는 데이터만 받지도 못합니다.
사용자의 요구사항을 조금 더 상세하게 들어주면 좋겠네요 GraphQL은 받고 싶은 데이터만 받을 수도 있고 사용자의 요청이 조금 더 다채로워집니다.
위와 같이 요청 사항에 받고 싶은 데이터만 명시하여 보낼 수 있습니다. 그리고 요청 URL은 1개로 통일되어 있죠.
~~/graphql (* Custom 가능)로 쿼리만 수정하여 보낸다면 서버에서 다양한 데이터들을 받을 수 있습니다.
그리고 GraphQL은 EndPoint를 POST로 한 것을 볼 수 있습니다. REST API는 GET, POST, PUT... 여러 가지 EndPoint가 있지만 GraphQL API는 오로지 POST만 사용하여 조회 및 수정을 진행합니다.
즉, REST와 큰 차이점 중의 하나는 여러 요청을 여러 번 네트워크 호출 없이, 여러 URL 사용하지 않고 처리할 수 있다는 것입니다.
GraphQL의 쿼리
GraphQL의 쿼리를 살펴보겠습니다. 직관적으로 쿼리문이 구성되어 있습니다. Content 스키마 중에 contentId, contentTitle 그리고 comment 스키마 중 commentId, commentBody만을 담은 Array가 반환될 것입니다.
//쿼리
{
content {
contentId
contentTitle
comments {
commentId
commentBody
}
}
}
//응답
{
"data": {
"content": {
"contentId": "ct1",
"contentTitle": "title",
"comments": [
{
"commentId": "comment1",
"commentBody": "comment"
}
]
}
}
}
아주 기본적인 쿼리는 위와 같고 조금 더 복잡한 용어들을 살펴보겠습니다.
GraphQL 쿼리 용어
- Fields
//Field { content { contentId } }
이 쿼리에서 contentId가 Field입니다. - Arguments
//Arguments { content(id : 1) { contentId } }
쿼리에 인자를 넣어서 보낼 수도 있습니다. 물론 Int형 말고도 다양한 자료형과 사용자 정의형도 보낼 수 있습니다. - Aliases
//쿼리 { content1 : content(id : 1) { contentId } content2 : content(id : 2) { contentId } } //응답 { "data" : { "content1" : { "contentId" : "1" }, "content2" : { "contentId" : "2" } } }
Aliases는 쿼리의 앞에 위치하여 사용됩니다. 위의 예시와 같이 인자만 다른 같은 쿼리를 보낼 때 Aliases를 사용하지 않는 다면 응답에서 어느 것이 content 데이터가 될지 몰라서 오류가 날 것입니다. 이를 해결하기 위해 Aliases를 사용할 수 있습니다. - Fragments
//Fragments { content1 : content(id : 1) { ...fields } content2 : content(id : 2) { ...fields } fragment fields on Character { contentId contentTitle } }
Fragment는 쿼리에서 불필요한 반복을 줄이기 위해 사용합니다. 공통부분을 Fragment로 따로 빼서 사용할 수 있습니다. - Operation Name
query { content : { contentId } }
맨 앞에 "query"는 꼭 필요하진 않지만 쿼리의 목적을 위해 명시할 수 있습니다. 이것을 Operation Name이라고 합니다. Operation Name에는 query, mutation, subscription이 있습니다. - Variables
query getContents($contentId : Int) { content(id : $contentId) : { contentId } }
웹 클라이언트 쪽에서 쿼리를 보낼 때도 쿼리 내에 static 하게 인자를 넣어서 보내는 경우는 많이 없습니다. 따라서 변수를 사용해야 하죠. 변수의 사용법은 위와 같습니다.
서버 측 GraphQL
앞에서는 쿼리를 어떻게 사용하는지 알아보았고 쿼리의 구성요소들을 알아보았습니다. 이것은 웹 클라이언트가 서버 쪽으로 보내는 내용입니다. 이제는 서버에서 어떻게 받아서 처리하는지 알아보겠습니다.
스키마와 타입(schema/type)
SQL을 사용할 때 데이터베이스에 스키마를 작성합니다. GraphQL도 이것과 같이 스키마를 작성해야 합니다. 기존의 REST API를 사용하는 방식에서 프런트엔드에 제공하는 백엔드 API 기술서 같은 요소입니다.
스키마는 *. graphqls 확장자 파일로 생성해야 합니다. 그리고 classPath 내 어디에든 존재 가능하며 개수도 제한이 없습니다. 단 하나의 조건으로 루트 Query와 루트 Mutation이 존재해야 합니다. 간단한 예제로 schema 파일 작성법을 알아보겠습니다.
// *.graphqls 파일
type Content {
contentId: ID!
contentTitle: String!
contentBody: String
comments: [Comment]
}
type Comment {
commentId: ID!
commentBody: String
}
// Root Query
type Query {
contentList: [Content]
}
extend type Query {
commentList: [Comment]
}
type Mutation {
writeContent(title: String!, body: String): Int
}
schema {
query: Query
mutation: Mutation
}
- type Content, Comment
DB에서 Content 테이블과 Comment 테이블을 만드는 것과 같은 느낌입니다.- Content, Comment : 오브젝트 타입
- contentId, commentId... : 필드
- (!) : 필수 값을 의미
- [] : 배열을 의미
- type Query, Mutation
기본적으로 루트 Query와 루트 Mutation을 의미합니다. 이것들은 단 하나씩만 존재해야 합니다. 이것의 이름은 아래의 schema를 이용해 변경할 수도 있습니다. - extend
스키마 파일에서 이름이 같은 오브젝트는 있을 수 없습니다. 하지만 파일 또는 오브젝트를 분리하고 싶을 수도 있습니다. 이때 extend를 이용해 예제에서는 Query를 확장했고 일부 분리할 수 있었습니다. 이외에도 여러 방식으로 사용되는 extend이지만 저는 일단 앞서 말한 이유로만 사용해보았습니다. - schema
스키마 내의 루트 Query와 루트 Mutation, Subscription을 지정할 수 있습니다. 이것은 명시적인 표현으로서 작동하며 명시하지 않으면 스키마 파일 내의 "Query", "Mutation", "Subscription" 이름을 가진 오브젝트가 루트 오브젝트가 됩니다.
Spring과 함께 사용해보기
위에서 스키마까지는 만들 수 있게 되었습니다. GraphQL을 사용하는 방법은 무수히 많이 있습니다. TypeScript, Apollo 등을 이용하여 Server를 구성하고 사용하는 방법도 있지만 저는 GraphQL을 기존의 웹 프로젝트에 적용해보기 위해 SpringBoot와 함께 사용해 보겠습니다.
1. Maven 기준 pom.xml에 dependency 추가
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>11.1.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>11.0.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter-test</artifactId>
<version>11.0.0</version>
<scope>test</scope>
</dependency>
위의 GraphQL 관련 Dependency를 추가해줍니다. 저는 Maven 기준으로 작성하겠습니다.
2. Schema 파일 작성
위에서 작성했던 Schema 파일을 추가해보겠습니다. 저는 resources폴더 > schema폴더 > Schema.graphqls 에 파일을 생성하겠습니다.
// *.graphqls 파일
type Content {
contentId: ID!
contentTitle: String!
contentBody: String
comments: [Comment]
}
type Comment {
commentId: ID!
commentBody: String
}
// Root Query
type Query {
contentList: [Content]
}
extend type Query {
commentList: [Comment]
}
type Mutation {
writeContent(title: String!, body: String): Int
}
schema {
query: Query
mutation: Mutation
}
3. DB 부분 및 모델들 작성하기
이 부분은 기존의 웹 프로젝트에서 진행하는 것과 똑같습니다. 앞서 만든 Schema에 맞게 DB Schema를 구성하고 해당 데이터를 조회하는 쿼리를 구현하시면 됩니다. 저는 이번 예제에서는 JPA를 사용하여 구성했습니다.
// ContentsRepository.java
@Repository
public interface ContentsRepository extends JpaRepository<Content, Integer> {
}
// model/Content.java
@Entity
@Table(name = "CONTENTS")
@Getter
@Setter
public class Content {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long contentId;
private String contentTitle;
private String contentBody;
@OneToMany
private List<Comment> commentList;
}
// model/Comment.java
@Entity
@Table(name = "CONTENT_COMMENTS")
@Getter
@Setter
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int commentId;
private String commentBody;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "content_id")
private Content content;
}
4. Resolver 작성하기
이 부분이 가장 중요한 부분이 아닌가 싶습니다. GraphQL에서는 Schema 파일에 있는 오브젝트의 조회, 수정 등 사용자의 요청이 서버로 들어오게 되면 해당 오브젝트에 대해 어떠한 처리를 할지 정해놓은 Resolver가 항상 있어야 합니다.
저희가 구현해야 하는 Resolver의 구현체는 크게는 2가지로 구분할 수 있습니다.
- GraphQLResolver
복합적인 처리가 필요할 때 오브젝트에 대한 처리를 구현하는 Resolver. 위의 예시에서는 Content를 조회한다면 Comment도 조회할 수 있게 됩니다. 이때 단순한 구조라면 Getter, Setter를 모델에 구현해놓는 것만으로 처리가 가능하지만 일련의 처리를 거쳐야 할 수도 있습니다. 이때 사용하는 Resolver입니다. - GraphQLQueryResolver, GraphQLMutationResolver
루트 Query와 루트 Mutation의 필드에 대한 처리를 구현하는 Resolver. 이때 필드에 대한 처리는 아래의 규칙을 가진 메서드에서 처리됩니다.- <필드명>
- is<필드명> - (필드 반환 타입이 boolean 인 경우)
- get<필드명>
아래의 예시 코드에서는 전부를 표시하지는 않습니다. 루트 Query에 대한 Resolver와 Content에 대한 GraphQLResolver 구현체만 표시하겠습니다.
아래에서 주의해서 볼 점은 Content를 조회하게 될 때 commentList에 대한 처리를 ContentResolver 내 commentList 메서드에서 처리가 되게 됩니다.
(* 여기서 인자가 자동으로 들어가는 방식이 궁금한데 찾아보고 추가하겠습니다.. :))
// resolver폴더/RootQueryResolver.java
@Component
public class RootQueryResolver implements GraphQLQueryResolver {
private final ContentsRepository contentsRepository;
private final CommentsRepository commentsRepository;
public ContentRootQueryResolver(ContentsRepository contentsRepository, CommentsRepository commentsRepository) {
this.contentsRepository = contentsRepository;
this.commentsRepository = commentsRepository;
}
public List<Content> getContentList() {
return contentsRepository.findAll();
}
public List<Comment> getCommentList() {
return commentsRepository.findAll();
}
}
// resolver/ContentResolver.java
@Component
public class ContentResolver implements GraphQLResolver<Content> {
private final CommentsRepository commentsRepository;
public ContentResolver(CommentsRepository commentsRepository) {
this.commentsRepository = commentsRepository;
}
public List<Comment> commentList(Content content) {
return commentsRepository.findByContentContentId(content.getContentId());
}
}
5. 테스트해보기
자 여기까지 하면 조회 혹은 수정 등이 가능합니다! 기존에는 Controller를 무조건 작성해줬어야 됐는데 여기까지만 했더니 처리가 돼서 참 신기했습니다.
GraphQL로 쿼리를 보내는 url은 <host>:<port>/graphql입니다. 이것은 properties에서 설정을 통해 변경도 가능합니다.
물론 API 테스트 플랫폼들 혹은 브라우저에서 요청을 보내서 테스트해도 되지만 체계적으로 TestCase를 작성해서 테스트해보겠습니다.
위에서 추가한 Dependency 중에 graphql-spring-boot-starter-test를 이용한 테스트입니다.
// test/resources폴더 내에 queryTest.graphql 파일 작성
query {
contentList {
contentId
contentBody
contentTitle
commentList {
commentId
commentBody
}
}
}
// TC 파일
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class GraphQLTest {
@Autowired
private GraphQLTestTemplate graphQLTestTemplate;
@Test
void queryTEST() throws IOException {
GraphQLResponse response = this.graphQLTestTemplate.postForResource("queryTest.graphql");
System.out.println(response.readTree().toString());
assertTrue(response.isOk());
assertEquals("3", response.get("$.data.contentList[2].contentId"));
assertEquals("테스트 해봐요", response.get("$.data.contentList[2].contentTitle"));
assertEquals("2", response.get("$.data.contentList[2].commentList[0].commentId"));
}
}
- SpringBootTest.WebEnvironment.RANDOM_PORT
TestCase를 실행할 때 SpringBootApplication을 무작위 포트로 띄워준다. 이를 설정해주지 않으면 GraphQLTestUtil Bean객체를 생성하는데 에러가 발생합니다. - GraphQLTestTemplate
GraphQL의 테스트를 도와주기 위해 GraphQL Java Tools에서 제공하는 Template입니다. - graphQLTestTemplate.postForResource
classpath 내 resource에서 *.graphql 파일을 찾아오는 메서드입니다.
4. 정리
GraphQL을 처음 사용해보면서 느낀 점은 확실히 프론트와 협업할 때 장점이 있을 것 같은 느낌입니다. 저는 실무에서 프론트와 백엔드 전부를 한꺼번에 개발할 일이 많습니다. 이 경우에는 프론트 코드와 백엔드 코드를 이리저리 왔다 갔다 해야 하는 경우가 많은데 만약 GraphQL을 사용한다면 조금 더 편해질 것 같습니다. 그리고 백엔드 코드만 보면 Controller가 없어지고 비즈니스 로직만 존재하는 것처럼 보여서 깔끔한 코드가 좋았습니다.
하지만 단점도 보이긴 합니다. 아직은 자료가 부족한 느낌이기도 하고 Resolver를 만드는 게 왠지 Controller랑 비슷한 느낌이기도 합니다. 그래서 확실히 REST 보다 낫다, 이렇게 단정하기에는 아직은 이른 느낌입니다.
하지만 추세를 보면 나중에는 REST와 비등비등하게 사용되지 않을까 싶습니다.
참조
2. https://tech.kakao.com/2019/08/01/graphql-basic/