서버측 Kotlin은 GraphiQL을 위한 DGS 프레임워크의 동작을 확인합니다
개시하다
코틀린에서 그래픽QL을 사용하는 방법DGS framework을 찾아보니 네스트된 질의가 어떻게 실행되는지 확인하고 싶어 만져봤다.
저는 지금 Kotlin과GraphiQL에 대해 공부하고 있습니다.
DGS 프레임워크 정보
DGS framework는 넷플릭스 오픈소스화[1] 스프링부트에서 동작하는 그래픽QL의 모드 우선 프레임워크다.넷플릭스 사내에서는 정식 근무[2][3]인 것 같다.
DGS 프레임워크 가져오기
개발 환경에는 IntelliJ IDEA가 사용됩니다.GraphiQL 플러그인 및 DGS 플러그인가 설치되어 있습니다.
DGS 설치 등은 공식 홈페이지Getting Started에 따라 진행됐다.기본적으로 문서의 패턴과 코드에 따라 이동하지만 코드는 패키지와 import을 추가해야 합니다.또한spring-boot-devtools 설정을 통해 자동으로 재부팅할 수 있어 편리하다[4].
일시적으로 이렇게 이동할 수 있지만 문서에도
Note that we have a Codegen plugin that can do this automatically, but in this guide we'll manually write the classes.
위에서 말한 바와 같이 코드를 자동으로 생성하는 플러그인이 하나 있는데 이 덕분에 모델을 우선적으로 실현할 수 있다. 그래서 다음에 나는 이 플러그인을 설치했다. 그러나 이 설정에서 아래의 몇 가지는 이미 끝났다.
1.Kotlin으로 플러그인 설정하는 방법 몰라
문서에는 플러그인 설정 방법이 적혀 있지만 Groovy로 기술한 것으로 보인다. Gradle 설정이 Groovy를 사용한다면 그대로 이동했을 수도 있고 (실제로 시도하지 않았을 수도 있음) Kotlin을 사용할 때Gradle 설정도 Kotlin으로 기술했다.그냥 가만히 있어.코틀린을 어떻게 쓰는지 모르겠지만 샘플로 소개된 창고을 보니 설정된 부분[5]과 창고 문제에 대해 비슷한 대화[6]가 있더라고요. 그래서 이런 설정을 참고해서 움직였어요.구체적인 내용은 다음과 같다.
schemaPaths
기본 설정은 문제가 없는 것 같아서 문서에 설정되었지만 아래는 설정하지 않았습니다.tasks.withType<com.netflix.graphql.dgs.codegen.gradle.GenerateJavaTask> {
packageName = "com.example.demo.generated"
generateClient = true
}
2. Resolver의 샘플 코드가 생성되지 않았습니다.
문서
generateJava generates the data fetchers and places them in build/generated-examples.
이렇게 쓰여 있지만 움직여도 샘플 코드가 생성되지 않아 찾아보니 언어가 코톨린이 되면 생성되지 않는다[7].플러그인 설정에 항목
language
이 있기 때문에 지정java
후 실행하여 샘플 코드를 생성했습니다.생성된 샘플 코드를 확인하면 문서에도NOTE: generateJava does NOT add the data fetchers that it generates to your project’s sources. These fetchers serve mainly as a basic boilerplate code that require further implementation from you.
위에 쓰여 있지만 간단한 코드라 Kottlin에서 이동하지 않아도 문제없다.또한 문서에 생성된 디렉터리
generated-examples
라고 쓰여 있지만 생성된 디렉터리generated/sources/dgs-codegen-generated-examples
는 경로를 설정할 수 있는 설정 항목exampleOutputDir
도 설정에 없다.구체적인 동작의 확인
DGS 프레임워크의 이동 방법을 알았기 때문에 구체적인 동작을 확인하기 위해 다음과 같은 행위의 코드를 적었다.한마디로 도서 관리 앱이다.
type Query {
me(token: String!): User!
books: [Book!]!
}
type Mutation {
signIn(id: ID!): String!
signOut(token: String!): Boolean!
rentBook(token: String!, id: ID!): Book!
returnBook(token: String!, id: ID!): Book!
}
type User {
id: ID!
name: String!
has: [Book!]!
history: [Book!]!
}
type Book {
id: ID!
title: String!
rentedBy: User
}
package com.example.demo
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class DemoApplication
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}
data class User(val id: String, val name: String)
data class Book(val id: String, val title: String)
data class SignedInUser(val id: String, val token: String)
data class RentEvent(val userId: String, val bookId: String, val rentedAt: Int)
data class ReturnEvent(val userId: String, val bookId: String, val returnedAt: Int)
object Resource {
val users = listOf(User("1", "user 1"), User("2", "user 2"))
val books = listOf(Book("1", "book 1"), Book("2", "book 2"), Book("3", "book 3"))
val signedInUsers = mutableListOf<SignedInUser>()
val rentEvents = mutableListOf<RentEvent>()
val returnEvents = mutableListOf<ReturnEvent>()
}
package com.example.demo.datafetchers
import com.example.demo.Resource
import com.example.demo.SignedInUser
import com.example.demo.generated.types.Book
import com.example.demo.generated.types.User
import com.netflix.graphql.dgs.*
import java.security.SecureRandom
@DgsComponent
class UsersDataFetcher {
@DgsQuery
fun me(@InputArgument token: String): User {
val userId = Resource.signedInUsers.find { it.token == token }?.id ?: throw IllegalArgumentException(token)
val user = Resource.users.find { it.id == userId }!!
return User(user.id, user.name, listOf(), listOf())
}
@DgsData(parentType = "User")
fun has(dfe: DgsDataFetchingEnvironment): List<Book> {
val user = dfe.getSource<User>()
return Resource.rentEvents
.filter { it.userId == user.id }
.groupBy { it.bookId }
.map { rentEventsByBookId ->
rentEventsByBookId.value.maxByOrNull { it.rentedAt }!!
}
.filter { rentEvent ->
Resource.returnEvents.find { it.userId == user.id && it.bookId == rentEvent.bookId && it.returnedAt >= rentEvent.rentedAt } == null
}
.map { it.bookId }
.fold(listOf<String>()) { bookIds, bookId -> bookIds + bookId }
.let { bookIds -> Resource.books.filter { bookIds.contains(it.id) } }
.map { Book(it.id, it.title) }
}
@DgsData(parentType = "User")
fun history(dfe: DgsDataFetchingEnvironment): List<Book> {
val user = dfe.getSource<User>()
return Resource.rentEvents
.filter { it.userId == user.id }
.map { rentEvent -> Resource.books.find { it.id == rentEvent.bookId }!! }
.map { Book(it.id, it.title) }
}
@DgsMutation
fun signIn(@InputArgument id: String): String {
val user = Resource.users.find { it.id == id } ?: throw IllegalArgumentException(id)
val token = SecureRandom().nextDouble().toBigDecimal().toPlainString()
Resource.signedInUsers.add(SignedInUser(user.id, token))
return token
}
@DgsMutation
fun signOut(@InputArgument token: String): Boolean {
Resource.signedInUsers.find { it.token == token }?.id ?: throw IllegalArgumentException(token)
Resource.signedInUsers.removeIf { it.token == token }
return true
}
}
package com.example.demo.datafetchers
import com.example.demo.RentEvent
import com.example.demo.Resource
import com.example.demo.ReturnEvent
import com.example.demo.generated.types.Book
import com.example.demo.generated.types.User
import com.netflix.graphql.dgs.*
@DgsComponent
class BooksDataFetcher {
@DgsQuery
fun books(): List<Book> {
return Resource.books.map { Book(it.id, it.title) }
}
@DgsData(parentType = "Book")
fun rentedBy(dfe: DgsDataFetchingEnvironment): User? {
val book = dfe.getSource<Book>()
val lastRentEvent = Resource.rentEvents.filter { it.bookId == book.id }.maxByOrNull { it.rentedAt }
val lastReturnEvent = Resource.returnEvents.filter { it.bookId == book.id }.maxByOrNull { it.returnedAt }
return lastRentEvent?.let {
if (lastReturnEvent == null || lastRentEvent.rentedAt > lastReturnEvent.returnedAt) {
Resource.users
.find { it.id == lastRentEvent.userId }
?.let { User(it.id, it.name, listOf(), listOf()) }
} else {
null
}
}
}
@DgsMutation
fun rentBook(@InputArgument token: String, @InputArgument id: String): Book {
val userId = Resource.signedInUsers.find { it.token == token }?.id ?: throw IllegalArgumentException(token)
val book = Resource.books.find { it.id == id } ?: throw IllegalArgumentException(id)
val lastRentEvent = Resource.rentEvents.filter { it.bookId == book.id }.maxByOrNull { it.rentedAt }
val lastReturnEvent = Resource.returnEvents.filter { it.bookId == book.id }.maxByOrNull { it.returnedAt }
if (lastRentEvent != null && lastReturnEvent == null || lastRentEvent != null && lastReturnEvent != null && lastRentEvent.rentedAt > lastReturnEvent.returnedAt) {
throw IllegalArgumentException(id)
}
Resource.rentEvents.add(RentEvent(userId, id, (System.currentTimeMillis() / 1000).toInt()))
return Book(book.id, book.title)
}
@DgsMutation
fun returnBook(@InputArgument token: String, @InputArgument id: String): Book {
val userId = Resource.signedInUsers.find { it.token == token }?.id ?: throw IllegalArgumentException(token)
val book = Resource.books.find { it.id == id } ?: throw IllegalArgumentException(id)
val lastRentEvent = Resource.rentEvents.filter { it.bookId == book.id }.maxByOrNull { it.rentedAt }
val lastReturnEvent = Resource.returnEvents.filter { it.bookId == book.id }.maxByOrNull { it.returnedAt }
if (lastRentEvent == null || lastReturnEvent != null && lastRentEvent.rentedAt <= lastReturnEvent.returnedAt) {
throw IllegalArgumentException(id)
}
Resource.returnEvents.add(ReturnEvent(userId, id, (System.currentTimeMillis() / 1000).toInt()))
return Book(book.id, book.title)
}
}
에는 DGS 프레임워크의 모조 사용 방법여기.에 대한 설명이 있다.또한 네스트된 필드의 처리는 여기.에 설명되어 있습니다.중첩 필드에는 교차 값을 직접 사용하고 확장 유형을 사용하며 값을 가지는 모드가 있습니다. (모드에 없는 값은 응답할 때 버려집니다.)이번에는 가장 간단하게 지나온 값을 직접 사용하자.설치 과정에서 플러그인 필드를 쉽게 찾을 수 없지만, 목록이 모드의 비목록 필드로 되돌아갔습니다. 형식이 맞지 않아 버려졌을 수도 있습니다.합쳐서 정상적으로 작동할 수 있어요.이 코드에 대해
{
books {
id
}
}
이런 조회{
"data": {
"books": [
{
"id": "1"
},
{
"id": "2"
},
{
"id": "3"
}
]
}
}
등 데이터가 되돌아온다.로그인해서 많이 빌렸어요.query Me($token: String!) {
me(token: $token) {
id
name
has {
id
title
rentedBy {
id
}
}
history {
id
title
rentedBy {
id
name
has {
id
title
rentedBy {
name
}
}
}
}
}
}
이런 조회{
"data": {
"me": {
"id": "1",
"name": "user 1",
"has": [
{
"id": "2",
"title": "book 2",
"rentedBy": {
"id": "1"
}
}
],
"history": [
{
"id": "1",
"title": "book 1",
"rentedBy": null
},
{
"id": "2",
"title": "book 2",
"rentedBy": {
"id": "1",
"name": "user 1",
"has": [
{
"id": "2",
"title": "book 2",
"rentedBy": {
"name": "user 1"
}
}
]
}
}
]
}
}
}
등 데이터가 되돌아온다.이 때 다음과 같은 순서로 처리가 실행되었다.has: [Book!]!
와 같은 필수 유형은 어떻게 해야 좋을까요? 어쨌든 빈 목록을 설정한 다음에 플러그인 필드에 값을 설정하면 그 값으로 덮어씁니다.이번엔 그 정도는 아니지만 N+1 질문 문서도 있기 때문에 대처할 수 있다고 생각한다.
끝말
이번에 DGS 프레임워크를 사용해 실제로 모바일 코드를 통해 어떤 동작을 하는지 확인했다.서버 쪽 그래픽QL 라이브러리를 만져본 적이 없어서 실제로 어떻게 돌아가는지 상상도 못 했는데 이번에 한번 돌려봤는데 실현된 인상이 들어서 다행이에요.
각주
Open Sourcing the Netflix Domain Graph Service Framework: GraphQL for Spring Boot | by Netflix Technology Blog | Netflix TechBlog ↩︎
Home - DGS Framework의 Q&A Is it production ready?섹션 ↩︎
Netflix Open Sources Their Domain Graph Service Framework: GraphQL for Spring Boot ↩︎
그대로 움직일 수 없기 때문에 설정IntelliJ IDEA 202.2 이후 Spring Boot DevTools의 LiveReload 설정↩︎
https://github.com/Netflix/dgs-examples-kotlin/blob/56e7371ffad312a9d59f1318d04ab5426515e842/build.gradle.kts ↩︎
https://github.com/Netflix/dgs-codegen/issues?q=generateJava ↩︎
코드https://github.com/Netflix/dgs-codegen/blob/c790be670061de7544981bc2c72d81de92900116/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt를 추적한 후 Java↩︎만 생성
Reference
이 문제에 관하여(서버측 Kotlin은 GraphiQL을 위한 DGS 프레임워크의 동작을 확인합니다), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/5000164/articles/32a45b10c236c6텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)