์ค์นผ๋ผ ํ์
, ์คํค๋ง ์ ์(SDL), Query/Mutation/Subscription ๋ฌธ๋ฒ, Spring for GraphQL ์ฐ๋(@QueryMapping, @BatchMapping), N+1 DataLoader ์ต์ ํ ๋ฐ ๋ณด์ ๋ ํผ๋ฐ์ค
## 1. GraphQL ํต์ฌ ๊ฐ๋
& REST ๋น๊ต
```text
[GraphQL vs REST API ํต์ฌ ๋น๊ต]
โข ์๋ํฌ์ธํธ : REST๋ ๋ฆฌ์์ค๋ณ ์๋ง์ ์๋ํฌ์ธํธ vs GraphQL์ ๋จ์ผ ์๋ํฌ์ธํธ (/graphql)
โข Over-fetching : REST๋ ๋ถํ์ํ ํ๋๊น์ง ์ ๋ถ ๋ฐํ vs GraphQL์ ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญํ ํ๋๋ง ์ ํํ ๋ฐํ
โข Under-fetching: REST๋ ์ฌ๋ฌ ๋ฒ์ ์ฐ์ HTTP ์์ฒญ ํ์ vs GraphQL์ 1๋ฒ์ ์ฟผ๋ฆฌ๋ก ์ฐ๊ด ๋ฐ์ดํฐ ์ผ๊ด ์กฐํ
โข ํ์
์์คํ
: ์คํค๋ง(SDL)๋ฅผ ํตํ ์๊ฒฉํ ์ ์ ํ์
๊ฒ์ฆ ๋ฐ ์๋ ๋ฌธ์ํ(Introspection) ์ง์
โข HTTP ๋ฉ์๋ : ๋๋ถ๋ถ POST ์์ฒญ body(JSON)์ ์ฟผ๋ฆฌ ์คํธ๋ง์ ๋ด์ ์ ์ก
```
```graphql
# ํด๋ผ์ด์ธํธ ์์ฒญ ์์ (POST /graphql)
query {
user(id: "usr_100") {
name
email
posts(limit: 2) {
title
}
}
}
```
## 2. ๊ธฐ๋ณธ ์ค์นผ๋ผ ํ์
& Nullability
```graphql
# [5๊ฐ์ง ๋ด์ฅ ๊ธฐ๋ณธ ์ค์นผ๋ผ(Scalar) ํ์
]
# Int : ๋ถํธ ์๋ 32๋นํธ ์ ์
# Float : ๋ถํธ ์๋ ๋ฐฐ์ ๋ฐ๋ ๋ถ๋์์์
# String : UTF-8 ๋ฌธ์์ด
# Boolean : true ๋๋ false
# ID : ๊ณ ์ ์๋ณ์ (๋ด๋ถ์ ์ผ๋ก๋ String ์ง๋ ฌํ)
type Product {
id: ID! # ํ์(Non-null) ID
name: String! # ํ์ ๋ฌธ์์ด
price: Float! # ํ์ ๋ถ๋์์์
rating: Int # ๋ ํ์ฉ(Nullable) ์ ์ (๊ฐ ์์ผ๋ฉด null)
isAvailable: Boolean! # ํ์ ๋ถ๋ฆฌ์ธ
tags: [String!]! # ๋ฐฐ์ด๊ณผ ๋ด๋ถ ์์ ๋ชจ๋ non-null
}
# [๋ฆฌ์คํธ์ Nullability ์กฐํฉ ๊ท์น]
# [String] : ๋ฐฐ์ด ์์ฒด๋ null ๊ฐ๋ฅ, ๋ฐฐ์ด ๋ด๋ถ ์์๋ null ๊ฐ๋ฅ (ex: null, [null, "a"])
# [String!] : ๋ฐฐ์ด ์์ฒด๋ null ๊ฐ๋ฅ, ๋ด๋ถ ์์๋ ๋ฐ๋์ ๋ฌธ์์ด (ex: null, ["a", "b"])
# [String]! : ๋ฐฐ์ด์ ํ์ ์กด์ฌ, ๋ด๋ถ ์์๋ null ๊ฐ๋ฅ (ex: [], ["a", null])
# [String!]! : ๋ฐฐ์ด๊ณผ ๋ด๋ถ ์์ ๋ชจ๋ ํ์ ์กด์ฌ (ex: [], ["a", "b"])
```
## 3. ์คํค๋ง ์ ์ ์ธ์ด (SDL: Types, Enums & Custom Scalars)
```graphql
# ์ปค์คํ
์ค์นผ๋ผ ์ ์ธ
scalar DateTime
scalar JSON
# ์ด๊ฑฐํ(Enum) ์ ์
enum UserRole {
ADMIN
USER
GUEST
}
# ๊ฐ์ฒด ํ์
(Object Type) ์ ์
type User {
id: ID!
email: String!
name: String!
role: UserRole!
createdAt: DateTime!
profile: Profile
posts: [Post!]!
}
type Profile {
avatarUrl: String
bio: String
}
type Post {
id: ID!
title: String!
content: String!
author: User!
likeCount: Int!
}
```
## 4. Query ์ค์ ๋ฌธ๋ฒ (์ธ์, ๋ณ์นญ & ํ๋๊ทธ๋จผํธ)
```graphql
# 1. ์ธ์(Arguments) ์ ๋ฌ & ์ค์ฒฉ ์กฐํ
query GetUserProfile {
user(id: "usr_42") {
name
profile {
avatarUrl
}
}
}
# 2. ๋ณ์นญ(Aliases) : ๋์ผํ ํ๋๋ฅผ ์๋ก ๋ค๋ฅธ ์ธ์๋ก ํ ๋ฒ์ ์กฐํ
query CompareUsers {
firstUser: user(id: "usr_1") {
...UserBasicInfo
}
secondUser: user(id: "usr_2") {
...UserBasicInfo
}
}
# 3. ํ๋๊ทธ๋จผํธ(Fragments) : ๊ณตํต ํ๋ ๋ชจ์ ์ฌ์ฌ์ฉ
fragment UserBasicInfo on User {
id
name
email
role
}
```
## 5. ๋ณ์(Variables) & ์ง์์ด(@include, @skip)
```graphql
# ๋ณ์ ์ ์ธ ์ ํ์
๊ณผ ๊ธฐ๋ณธ๊ฐ ์ง์ ($withPosts: Boolean = false)
query GetUserWithCondition($userId: ID!, $withPosts: Boolean!) {
user(id: $userId) {
id
name
email
# @include : ์กฐ๊ฑด์ด true์ผ ๋๋ง ํ๋ ํฌํจ
posts @include(if: $withPosts) {
id
title
}
# @skip : ์กฐ๊ฑด์ด true์ด๋ฉด ํ๋ ์ ์ธ
profile @skip(if: $withPosts) {
bio
}
}
}
# [GraphQL ๋ณ์ JSON (HTTP Body์ "variables" ํ๋)]
# {
# "userId": "usr_42",
# "withPosts": true
# }
```
## 6. Mutation ๋ฐ์ดํฐ ๋ณ๊ฒฝ & ์
๋ ฅ ๊ฐ์ฒด(Input)
```graphql
# ์คํค๋ง ์ ์: Mutation ์ ์ฉ Input Object
input CreatePostInput {
title: String!
content: String!
tags: [String!]
}
# ๋ณ๊ฒฝ ๊ฒฐ๊ณผ Payload ๋ฐํ ํจํด (UI ์บ์ ๊ฐฑ์ ์ฉ์ด)
type CreatePostPayload {
success: Boolean!
post: Post
clientMutationId: String
}
# Mutation ์์ฒญ ์ฟผ๋ฆฌ
mutation CreateNewPost($input: CreatePostInput!) {
createPost(input: $input) {
success
post {
id
title
createdAt
author {
id
name
}
}
}
}
```
## 7. Subscription ์ค์๊ฐ ์ด๋ฒคํธ ๊ตฌ๋
(WebSockets)
```graphql
# ์คํค๋ง ์ ์
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
messageSent(roomId: ID!): Message!
}
# ํด๋ผ์ด์ธํธ ๊ตฌ๋
์์ฒญ (WebSocket ํ๋กํ ์ฝ ws:// ๋๋ wss://)
subscription WatchRoomMessages($roomId: ID!) {
messageSent(roomId: $roomId) {
id
sender {
name
}
text
sentAt
}
}
```
```text
[์ค๋ฌด ๊ถ์ฅ์ฌํญ]
โข ํ๋กํ ์ฝ : graphql-ws ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ๊ถ์ฅ (๊ตฌํ subscriptions-transport-ws๋ deprecated)
โข ํํธ๋นํธ : ์ฃผ๊ธฐ์ ์ธ Ping/Pong ํจํท์ผ๋ก ๋ชจ๋ฐ์ผ ๋๊น ๋ฐ ์ ํด ์ปค๋ฅ์
๊ฐ์ง
```
## 8. ์ธํฐํ์ด์ค(Interface) & ์ ๋์จ(Union) ๋คํ์ฑ
```graphql
# ์ธํฐํ์ด์ค ์ ์
interface Node {
id: ID!
}
interface Content {
id: ID!
title: String!
author: User!
}
# ์ธํฐํ์ด์ค ๊ตฌํ
type Article implements Content {
id: ID!
title: String!
author: User!
wordCount: Int!
}
type Video implements Content {
id: ID!
title: String!
author: User!
durationSeconds: Int!
}
# ์ ๋์จ(Union) ํ์
์ ์ (์ค์นผ๋ผ๊ฐ ์๋ Object ํ์
๋ค์ ํฉ์งํฉ)
union SearchResult = Article | Video | User
# ์ธ๋ผ์ธ ํ๋๊ทธ๋จผํธ(... on Type) ๋ฐ __typename์ ํ์ฉํ ์ฟผ๋ฆฌ
query UniversalSearch($keyword: String!) {
search(keyword: $keyword) {
__typename # ์ค์ ๊ฐ์ฒด ํ์
๋ช
๋ฐํ ("Article", "Video", "User")
... on Article {
id
title
wordCount
}
... on Video {
id
title
durationSeconds
}
... on User {
id
name
email
}
}
}
```
## 9. ๋ฐฑ์๋ ๋ฆฌ์กธ๋ฒ & N+1 ํด๊ฒฐ (DataLoader)
```javascript
// ๋ฆฌ์กธ๋ฒ ๊ธฐ๋ณธ ์๊ทธ๋์ฒ: (parent, args, context, info)
const resolvers = {
Query: {
user: async (_, { id }, { db }) => db.findUserById(id),
posts: async (_, { limit }, { db }) => db.findRecentPosts(limit),
},
Post: {
// [N+1 ๋ฐ์ ์์ธ]: Post๊ฐ 100๊ฑด์ด๋ฉด author ๋ฆฌ์กธ๋ฒ๊ฐ 100๋ฒ ๊ฐ๋ณ SQL ์คํ!
// author: async (post, _, { db }) => db.findUserById(post.authorId),
// [DataLoader ํด๊ฒฐ]: 1๊ฐ์ ์ด๋ฒคํธ ๋ฃจํ ํฑ ๋์์ ์์ฒญ์ ๋ชจ์ 1๋ฒ์ 'WHERE id IN (...)'์ผ๋ก ์ผ๊ด ์กฐํ
author: (post, _, { loaders }) => loaders.userLoader.load(post.authorId),
},
};
// DataLoader ์ธ์คํด์ค ์์ฑ (์์ฒญ(Context) ๋จ์ ๊ฒฉ๋ฆฌ ํ์!)
import DataLoader from 'dataloader';
export function createLoaders(db) {
return {
userLoader: new DataLoader(async (userIds) => {
// SELECT * FROM users WHERE id IN (1, 2, 3...)
const users = await db.findUsersByIds(userIds);
// ๋ฐ๋์ ์ ๋ฌ๋ฐ์ userIds ์์์ 1:1๋ก ๋งคํํ์ฌ ๋ฐํํด์ผ ํจ!
const userMap = new Map(users.map(u => [u.id, u]));
return userIds.map(id => userMap.get(id) || null);
}),
};
}
```
## 10. ํ์ด์ง๋ค์ด์
(Cursor-based Relay Connection)
```graphql
# ํ์ค Relay Connection ๋ช
์ธ
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
cursor: String!
node: Post!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# ์ฟผ๋ฆฌ ์์ฒญ: ํน์ ์ปค์ ์ดํ(after) N๊ฐ(first) ์กฐํ
query GetPaginatedPosts($first: Int!, $after: String) {
posts(first: $first, after: $after) {
totalCount
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node {
id
title
likeCount
}
}
}
}
```
## 11. ์๋ฌ ์๋ต ๊ตฌ์กฐ & ํธ๋ค๋ง ๋ชจ๋ฒ ์ฌ๋ก
```json
{
"data": {
"user": null
},
"errors": [
{
"message": "ํด๋น ์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"],
"extensions": {
"code": "NOT_FOUND",
"invalidArgs": ["usr_999"],
"timestamp": "2026-09-29T11:30:00Z"
}
}
]
}
```
```text
[GraphQL ์๋ฌ ์ฒ๋ฆฌ ํต์ฌ ์์น]
โข HTTP 200 OK : GraphQL ์์ง์ด ์ฟผ๋ฆฌ๋ฅผ ์ ์ ํ์ฑ/์คํํ๋ค๋ฉด ํ๋ ์๋ฌ๊ฐ ์๋๋ผ๋ HTTP ์ํ ์ฝ๋๋ 200 ๋ฐํ
โข ๋ถ๋ถ ์๋ต : ์๋ฌ๊ฐ ๋ฐ์ํ์ง ์์ ๋ค๋ฅธ ํ๋๋ "data"์ ์ ์ ํฌํจ๋์ด ๋ ๋๋ง ๊ฐ๋ฅ (Partial Data)
โข extensions : ํด๋ผ์ด์ธํธ ๋ถ๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์ํด ํ์ค ์ฝ๋(UNAUTHENTICATED, FORBIDDEN, BAD_USER_INPUT)๋ฅผ extensions์ ํฌํจ
โข ๋ณด์ ์ฃผ์ : ์ด์(Production) ํ๊ฒฝ์์๋ ๋ด๋ถ SQL/์คํํธ๋ ์ด์ค๊ฐ ํด๋ผ์ด์ธํธ๋ก ๋
ธ์ถ๋์ง ์๋๋ก ์๋ฌ ๋ง์คํน ํ์
```
## 12. ์ด์ ๋ฐ ๋ณด์ ๋ชจ๋ฒ ์ฌ๋ก (Security & Production)
```text
[GraphQL ๋ณด์ 5๋ ์์น]
1. ์ธํธ๋ก์คํ์
(Introspection) ๋นํ์ฑํ
- ์ด์ ๋ฐฐํฌ ์ __schema, __type ์ฟผ๋ฆฌ๋ฅผ ๋ง์ ๋ด๋ถ ๊ตฌ์กฐ ๋
ธ์ถ ๋ฐฉ์ง
2. ์ฟผ๋ฆฌ ๊น์ด ์ ํ (Query Depth Limit)
- ์
์์ ์ธ ๋ฌดํ ์ํ ์ฟผ๋ฆฌ ๋ฐฉ์ง (user { posts { author { posts { author... } } } })
3. ์ฟผ๋ฆฌ ๋ณต์ก๋ ๋ถ์ (Query Cost/Complexity Analysis)
- ๊ณ์ฐ ๋น์ฉ์ด ํฐ ํ๋์ ๊ฐ์ค์น๋ฅผ ๋ถ์ฌํ๊ณ , ์๊ณ์น ์ด๊ณผ ์ฟผ๋ฆฌ ๊ฑฐ๋ถ
4. ์ง์ ์ฟผ๋ฆฌ (Persisted Queries)
- ํด๋ผ์ด์ธํธ๋ ์ฟผ๋ฆฌ ๋ณธ๋ฌธ ๋์ ์ฌ์ ์ ๋ฑ๋ก๋ ํด์(SHA-256)๊ฐ๋ง ์ ์กํ์ฌ ๋์ญํญ ์ ์ฝ ๋ฐ ์์ ์ฟผ๋ฆฌ ์ฐจ๋จ
5. ๋ฐฐ์น ์์ฒญ ๊ฐ์ ์ ํ (Batching Attack ๋ฐฉ์ง)
- ๋จ์ผ HTTP ์์ฒญ์ ์์ฒ ๊ฐ์ query ๋ฐฐ์ด์ ๋ด์ ๋ณด๋ด๋ ๋ฌด์ฐจ๋ณ ๋์
(Brute-force) ๊ณต๊ฒฉ ์ฐจ๋จ
```
## 13. Spring for GraphQL ์ค์ & ์คํค๋ง ๊ตฌ์ฑ
```groovy
// build.gradle (Spring Boot 3.x / 2.7+ ๊ณต์ ์คํํฐ)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-graphql'
implementation 'org.springframework.boot:spring-boot-starter-web'
// WebSocket ๊ตฌ๋
์ง์ ํ์ ์ ์ถ๊ฐ
implementation 'org.springframework.boot:spring-boot-starter-websocket'
}
```
```yaml
# application.yml
spring:
graphql:
graphiql:
enabled: true # ๋ธ๋ผ์ฐ์ ํ
์คํธ UI ํ์ฑํ (http://localhost:8080/graphiql)
path: /graphiql
http:
path: /graphql # GraphQL ๋จ์ผ HTTP ์๋ํฌ์ธํธ (๊ธฐ๋ณธ๊ฐ: /graphql)
websocket:
path: /graphql # Subscription์ฉ ์น์์ผ ์๋ํฌ์ธํธ
schema:
printer:
enabled: true # ์๋ฒ ๊ธฐ๋ ์ ๋ณํฉ๋ ์ต์ข
์คํค๋ง๋ฅผ ์ฝ์์ ์ถ๋ ฅ
```
```graphql
# src/main/resources/graphql/schema.graphqls
# Spring Boot๊ฐ ํด๋น ๋๋ ํฐ๋ฆฌ์ ๋ชจ๋ .graphqls ํ์ผ์ ์๋ ๋ก๋ํ์ฌ ๋ณํฉํฉ๋๋ค.
type Query {
bookById(id: ID!): Book
allBooks: [Book!]!
}
type Mutation {
createBook(input: CreateBookInput!): Book!
}
type Subscription {
bookAdded: Book!
}
type Book {
id: ID!
title: String!
pageCount: Int!
author: Author!
}
type Author {
id: ID!
name: String!
}
input CreateBookInput {
title: String!
pageCount: Int!
authorId: ID!
}
```
## 14. Spring ์ปจํธ๋กค๋ฌ ๊ตฌํ (@QueryMapping, @MutationMapping)
```java
package com.example.demo.controller;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Flux;
import java.util.List;
@Controller
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
// 1. Query.bookById(id: ID!) ๋งคํ (๋ฉ์๋๋ช
๊ณผ ์คํค๋ง ํ๋๋ช
์ผ์น)
@QueryMapping
public Book bookById(@Argument Long id) {
return bookService.findById(id);
}
// 2. Query.allBooks ๋งคํ
@QueryMapping
public List<Book> allBooks() {
return bookService.findAll();
}
// 3. Mutation.createBook(input: CreateBookInput!) ๋งคํ
// Java 17+ record ํด๋์ค๋ฅผ ์ฌ์ฉํ๋ฉด ๋ณ๋ ๋งคํผ ์์ด ์๋ ๋ฐ์ธ๋ฉ
@MutationMapping
public Book createBook(@Argument CreateBookInput input) {
return bookService.create(input);
}
// 4. Subscription.bookAdded ๋งคํ (Reactive Flux ๋ฐํ)
@SubscriptionMapping
public Flux<Book> bookAdded() {
return bookService.getBookAddedFlux();
}
}
// DTO๋ record๋ก ์ ์ ๊ถ์ฅ
public record CreateBookInput(String title, int pageCount, Long authorId) {}
```
## 15. Spring N+1 ํด๊ฒฐ (@BatchMapping & DataLoader)
```java
@Controller
public class BookAuthorController {
private final AuthorService authorService;
public BookAuthorController(AuthorService authorService) {
this.authorService = authorService;
}
// [๋จ๊ฑด ํด๊ฒฐ]: @SchemaMapping (N+1 ๋ฌธ์ ๋ฐ์ ๊ฐ๋ฅ!)
// allBooks๋ก ์ฑ
100๊ฑด ์กฐํ ์, ๊ฐ ์ฑ
๋ง๋ค author ๋ฉ์๋๊ฐ 100๋ฒ ํธ์ถ๋จ
// @SchemaMapping(typeName = "Book", field = "author")
// public Author getAuthor(Book book) {
// return authorService.findById(book.authorId());
// }
// [โญ๏ธ ์ค๋ฌด ๊ถ์ฅ]: @BatchMapping (DataLoader ์๋ ๊ตฌ์ฑ)
// ๋ถ๋ชจ ๋ฆฌ์คํธ(List<Book>)๋ฅผ ํ ๋ฒ์ ์ธ์๋ก ์ ๋ฌ๋ฐ์ ์ธ๋ํค ๋ชฉ๋ก์ ์ถ์ถํ๊ณ ,
// "SELECT * FROM author WHERE id IN (...)" 1๋ฒ์ ์ฟผ๋ฆฌ๋ก ๋ฌถ์ด์ Map ํํ๋ก ๋ฐํ
@BatchMapping(typeName = "Book", field = "author")
public Map<Book, Author> author(List<Book> books) {
List<Long> authorIds = books.stream().map(Book::authorId).toList();
Map<Long, Author> authorMap = authorService.findAllByIds(authorIds);
// Book ๊ฐ์ฒด์ ๋์ํ๋ Author ๊ฐ์ฒด๋ฅผ Map์ผ๋ก ๋งคํํ์ฌ ๋ฐํ
return books.stream()
.collect(Collectors.toMap(
book -> book,
book -> authorMap.get(book.authorId())
));
}
}
```
```text
[@BatchMapping ํต์ฌ ๋์ ๋ฐฉ์]
โข GraphQL Java์ DataLoader๋ฅผ ์๋ ๋ฑ๋กํ ํ์ ์์ด Spring์ด ๋ฐํ์์ ์๋ ์ฃผ์
โข ๋์ผ ์คํ ์ปจํ
์คํธ(Event Loop/Execution loop) ๋ด์ Book ํ๋ ์์ฒญ์ ํ๊ณณ์ ์ทจํฉํ์ฌ 1ํ ๋ฐฐ์น ์กฐํ ์คํ
โข ๋ฐํ ํ์
: Map<๋ถ๋ชจ, ์์> ๋๋ Flux<์์> (์์ ๋ณด์ฅํ) ์ง์
```
## 16. Spring ์์ธ ์ฒ๋ฆฌ & ๋ณด์ (@GraphQlExceptionHandler & Security)
```java
// 1. ์ ์ญ GraphQL ์์ธ ํธ๋ค๋ฌ (@ControllerAdvice)
@ControllerAdvice
public class GraphQlExceptionResolver {
@GraphQlExceptionHandler
public GraphQLError handleNotFound(EntityNotFoundException ex, DataFetchingEnvironment env) {
return GraphQLError.newError()
.errorType(ErrorType.NOT_FOUND)
.message(ex.getMessage())
.path(env.getExecutionStepInfo().getPath())
.location(env.getField().getSourceLocation())
.extensions(Map.of(
"errorCode", "RESOURCE_NOT_FOUND",
"timestamp", Instant.now().toString()
))
.build();
}
}
```
```java
// 2. Spring Security ์ฐ๋ (๋ฉ์๋ ๋ณด์ ์ ์ฉ)
@Controller
public class SecureAdminController {
// Spring Security์ @PreAuthorize๋ฅผ ์ปจํธ๋กค๋ฌ ๋ฉ์๋์ ๊ทธ๋๋ก ์ ์ฉ ๊ฐ๋ฅ!
@PreAuthorize("hasRole('ADMIN')")
@MutationMapping
public Boolean deleteBook(@Argument Long id) {
bookService.delete(id);
return true;
}
// ํ์ฌ ์ธ์ฆ๋ ์ฌ์ฉ์ Principal ์ฃผ์
@QueryMapping
public UserProfile me(@AuthenticationPrincipal CustomUserDetails user) {
return user.toProfile();
}
}
```
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์