2021. 12. 20. 22:26ㆍSpring Boot/Information
[ 이전 내용 보러가기 ]
[ 완성 코드 보러가기 ]
→ Branch: gql_mutation
이전 내용에서는 CRUD(Create, Read, Update, Delete)중, "R" 에 속하는 query만 작성했으니 이번에는 CUD에 속하는 mutation을 작성하고자 한다.
GraphQL에서는 하나의 EndPoint에서 2개의 Schema로 관리를 한다고 했다.(mutation, query)
여기서 query는 단순히 읽기를 뜻하는것이고, mutation은 데이터를 조작하는 역할을 한다.
먼저 이전 예제에서 조금 수정해야 하는 부분이 있다.
가짜 Database를 만들고, 이에 대해 Spring Session이 유지되는 동안 데이터를 조작하는 기능을 만들 것이다.
(API를 사용하는 서버에 CUD 작업을 진행할 수는 있으나, 그러지 않도록 하겠다..)
Spring boot이 켜질 때 Fake Rest API를 이용해 데이터를 받아오고, 해당 데이터를 저장해놓은 다음에 데이터베이스의 역할처럼 하게끔 할 것이다.
그러기 위해서는 Spring boot가 다 켜졌을 때를 감지해 API를 호출하여 데이터를 저장하는 Event Listener가 필요하다.
아래와 같이 구현해주도록 하자.
FakeDatabase.kt
import org.springframework.stereotype.Component
import shin.yongha.springgql.model.api.CommentDTO
import shin.yongha.springgql.model.api.PostDTO
@Component
class FakeDatabase {
var postList: MutableList<PostDTO> = mutableListOf()
var commentList: MutableList<CommentDTO> = mutableListOf()
}
Initialize.kt
import org.springframework.stereotype.Component
import shin.yongha.springgql.env.FakeDatabase
import shin.yongha.springgql.service.ApiService
import javax.inject.Inject
@Component
class Initialize @Inject constructor(
private val fakeDatabase: FakeDatabase,
private val apiService: ApiService
) {
fun initialize() {
// 가짜 Database에 데이터를 채워넣는 역할
fakeDatabase.postList.addAll(apiService.getPosts())
fakeDatabase.commentList.addAll(apiService.getComments())
}
}
StartupSpringApplicationListener.kt
import org.springframework.context.ApplicationListener
import org.springframework.context.event.ContextRefreshedEvent
import org.springframework.stereotype.Component
import javax.inject.Inject
@Component
class StartupSpringApplicationListener @Inject constructor(
private val initialize: Initialize
) : ApplicationListener<ContextRefreshedEvent> {
override fun onApplicationEvent(event: ContextRefreshedEvent) {
/**
* Spring Application Context 의 Bean 이 모두 초기화 되고 딱 한번 실행됩니다.
*/
initialize.initialize()
println("########## initialize Complete #########")
}
}
FakeDatabase Class를 Bean으로 만들어, Spring Application 어디에서든 주입을 받을 수 있게 설정하고
Spring Boot Context의 Bean이 모두 초기화 되었을 때 실행되는 로직을 추가해줬다.
위처럼 코드를 작성하게 되면, Spring boot이 켜질 때 딱 한번만 Rest API를 호출하게 될 것이다.(Initialize.kt)
그리고 그에 대한 결과 값을 FakeDatabase Class에 모두 저장하게 되는 것이다.
FakeDatabase에 데이터를 모두 저장하도록 하였으니, 기존에 있는 QueryResolver도 모두 수정을 해주어야 한다.
(데이터 조작 이후 업데이트된 데이터를 보려면 FakeDatabase에서 갖고와야함. Rest API로 다시 갖고오게 되면 실제 서버에는 수정을 일으키지 않았기 때문에 GQL로 수정하지 않은 데이터가 내려오게 됨.)
CommentQueryResolver.kt
import com.coxautodev.graphql.tools.GraphQLQueryResolver
import lombok.RequiredArgsConstructor
import org.springframework.stereotype.Component
import shin.yongha.springgql.env.FakeDatabase
import shin.yongha.springgql.model.api.CommentDTO
import shin.yongha.springgql.service.ApiService
import javax.inject.Inject
@RequiredArgsConstructor
@Component
class CommentQueryResolver @Inject constructor(
private val fakeDatabase: FakeDatabase
) : GraphQLQueryResolver {
fun getComment(): List<CommentDTO> {
return fakeDatabase.commentList
}
fun getCommentByPostId(postId: Long): List<CommentDTO> {
if(postId <= 0) {
throw Exception("ID는 1 이상이어야 합니다.")
}
return fakeDatabase.commentList
.filter { comment ->
return@filter comment.postId == postId
}
}
}
PostQueryResolver.kt
import com.coxautodev.graphql.tools.GraphQLQueryResolver
import lombok.RequiredArgsConstructor
import org.springframework.stereotype.Component
import shin.yongha.springgql.env.FakeDatabase
import shin.yongha.springgql.model.api.PostDTO
import shin.yongha.springgql.service.ApiService
import javax.inject.Inject
@RequiredArgsConstructor
@Component
class PostQueryResolver @Inject constructor(
private val fakeDatabase: FakeDatabase
) : GraphQLQueryResolver {
fun getPost(): List<PostDTO> {
return fakeDatabase.postList
}
fun getPostById(id: Long): PostDTO {
if(id <= 0) {
throw Exception("ID는 1 이상이어야 합니다.")
}
return fakeDatabase.postList
.find { post ->
post.id == id
} ?: throw Exception("데이터가 없습니다.")
}
}
Query를 호출해도 Rest API로 계속해서 갖고오는 것이 아닌, FakeDatabase에 저장되어 있는 데이터를 불러오게 될 것이다.
본격적으로 데이터 조작 로직을 작성해보도록 하자.
GraphQL을 이용하여 로직을 구현할 때에는 반드시 type을 먼저 선언해주는 습관을 기르도록 하자.
comment.graphqls
type Comment {
postId: Int!,
id: Int!,
name: String!,
email: String!,
body: String!,
}
input IComment {
name: String!,
email: String!,
body: String!,
}
이전과 비교했을 때 'input IComment' 라는 내용이 추가되었다.
이는, GraphQL에서 데이터를 받을 Type을 설정하는 것이다.(객체 개념)
postId는 따로 변수로 받을 것 이기 때문에 정의해주지 않았고, id는 Resolver에서 자동으로 부여하도록 할 것이다.
똑같이 Post에도 추가해주도록 하자.
post.graphqls
type Post {
userId: Int!,
id: Int!,
title: String!,
body: String!,
}
input IPost {
userId: Int!,
title: String!,
body: String!,
}
이 역시 마찬가지로 id는 Resolver에서 자동으로 부여를 해줄 것 이기 때문에 따로 정의하지 않았다.
common.graphqls
schema {
query: Query,
mutation: Mutation
}
# Root Query
type Query {
getPost: [Post],
getPostById(id: Int!): Post,
getComment: [Comment],
getCommentByPostId(postId: Int!): [Comment],
}
# Root Mutation
type Mutation {
insertPost(post: IPost!): Boolean,
insertPostAfterView(post: IPost!): Post,
deletePostById(id: Int!): Boolean,
deleteCommentByPostId(postId: Int!): Boolean
deleteCommentById(id: Int): Boolean,
insertComment(postId: Int!, comment: IComment!): Boolean
}
mutation이라는 Schema가 추가되었다.
또 Root Mutation이 추가되었으며 Mutation Type 안에서는 동작할 내용들에 대해 정의를 진행했다.
설명하자면 아래와 같은 기능이다.
- insertPost : IPost를 인자로 받아서 Resolver에서 Data Insert 처리
- insertPostAfterView : IPost를 인자로 받아서 Resolver에서 Data Insert 처리 후 해당 데이터 리턴
- deletePostById : id를 기준으로 Post 삭제
- deleteCommentByPostId : PostId를 기준으로 Comment 삭제(게시글 내 댓글 전체삭제 기능)
- deleteCommentById : id를 기준으로 Comment 삭제(댓글 1개만 삭제)
- insertComment : 특정 PostId에 IComment를 인자로 받아 Resolver에서 Data Insert 처리
GraphQL은 모두 작성을 마쳤으니, 이제 Resolver를 정의하러 가자.
CommentMutationResolver.kt
import com.coxautodev.graphql.tools.GraphQLMutationResolver
import com.coxautodev.graphql.tools.GraphQLQueryResolver
import lombok.RequiredArgsConstructor
import org.springframework.stereotype.Component
import shin.yongha.springgql.env.FakeDatabase
import shin.yongha.springgql.model.api.CommentDTO
import shin.yongha.springgql.service.ApiService
import javax.inject.Inject
@RequiredArgsConstructor
@Component
class CommentMutationResolver @Inject constructor(
private val fakeDatabase: FakeDatabase
) : GraphQLMutationResolver {
fun deleteCommentByPostId(postId: Long): Boolean {
fakeDatabase.commentList = fakeDatabase.commentList.filter { it.postId != postId }.toMutableList()
return true
}
fun deleteCommentById(id: Long): Boolean {
fakeDatabase.commentList = fakeDatabase.commentList.filter { it.id != id }.toMutableList()
return true
}
fun insertComment(postId: Long, comment: CommentDTO): Boolean {
fakeDatabase.commentList.add(
CommentDTO(
postId,
fakeDatabase.commentList.maxOf { it.id } + 1L,
comment.name,
comment.email,
comment.body
)
)
return true
}
}
PostMutationResolver.kt
import com.coxautodev.graphql.tools.GraphQLMutationResolver
import com.coxautodev.graphql.tools.GraphQLQueryResolver
import lombok.RequiredArgsConstructor
import org.springframework.stereotype.Component
import shin.yongha.springgql.env.FakeDatabase
import shin.yongha.springgql.model.api.CommentDTO
import shin.yongha.springgql.model.api.PostDTO
import shin.yongha.springgql.service.ApiService
import javax.inject.Inject
@RequiredArgsConstructor
@Component
class PostMutationResolver @Inject constructor(
private val fakeDatabase: FakeDatabase
) : GraphQLMutationResolver {
fun deletePostById(id: Long): Boolean {
fakeDatabase.postList = fakeDatabase.postList.filter { it.id != id }.toMutableList()
return true
}
fun insertPost(post: PostDTO): Boolean {
fakeDatabase.postList.add(PostDTO(
post.userId,
fakeDatabase.postList.maxOf { it.id } + 1L,
post.title,
post.body
))
return true
}
fun insertPostAfterView(post: PostDTO): PostDTO {
if(!insertPost(post)) {
throw Exception("Insert Post Failed")
}
return fakeDatabase.postList.last()
}
}
QueryResolver와 다른 점은, 다른 GraphQL Resolver를 상속하고 있다는 점이다.
바로 이것이 mutation과 query schema를 구분하여 각각 역할에 맞는 Resolver로 보내는 역할을 하는 것이다.
insertPostAfterView는 조금 특이한 동작을 하고 있는데, 데이터를 넣고 나서 다시 해당 데이터를 Return 해주고 있다.
이렇게 작성할 경우 mutation schema를 호출해도, 데이터를 Return 받을 수 있다.
나머지는 데이터를 전부 돌려받지 못하며, 성공/실패 여부만 확인할 수 있다.
다른 부분은 굳이 설명하지 않아도 간단한 코드이기 때문에 이해하기 쉬울 것 이라고 생각된다.
그럼 이제 Query를 호출해 확인해보도록 하자.
위 Query는 이전에 이미 작성을 마친 Query다.
여기 있는 Comment들이 거슬리니 전부 다 지워주도록 하자.
성공했다고 하니, 다시 한번 조회해보도록 하자.
깔끔하게 지워졌으니, 이제 Comment를 작성해주도록 하자.
insert 또한 성공했다고 한다. 그럼 다시 확인해보면, 내가 작성한 댓글이 달려있을 것이다.
이번엔 새로운 글을 쓰고, 내가 쓴 글이 제대로 써졌는지 확인하는 insertPostAfterView Mutation을 호출해보도록 하자.
데이터가 잘 들어가고, 내가 쓴 글이 잘 들어가 있는 것을 확인할 수 있다.
마치며
GraphQL은 상당히 편하고 적용하기 좋은 기능이다.
하지만 Rest API가 이미 점령을 한 만큼, Rest API를 모두 걷어내고 GraphQL만 사용하기란 무리가 있을 것으로 보인다.
시간이 좀 걸리더라도 GraphQL로 Refactoring하며 줄여나가면 상당히 이점이 있지 않을까 싶다.
이 글을 끝으로 이제 React에서 GraphQL을 사용하고 있는 서버와 통신하는 방법에 대해 정리해야겠다.
'Spring Boot > Information' 카테고리의 다른 글
[GraphQL] Spring Boot로 Graph QL 적용하기 (1) (0) | 2021.12.19 |
---|---|
[JWT] JWT Token 정리 (0) | 2021.12.14 |
[Spring Boot] REST API 구축시 Local Date Time 받기 (0) | 2021.11.10 |
[build.gradle.kts] Spring Build 파일 실행시 ReactJS도 같이 실행하는 방법(3) (0) | 2021.11.08 |
[Spring Boot] Kotlin + Gradle MyBatis 연동 (0) | 2021.11.02 |