Reactive GraphQL 마스터 노트 시리즈 4편. Spring for GraphQL의 자동 구성, 5 어노테이션(@QueryMapping·@MutationMapping·@SubscriptionMapping·@SchemaMapping·@BatchMapping)의 결정적 차이, BatchMapping으로 N+1 자동 해결, Argument·DataFetchingEnvironment·Authentication 주입까지.
이 글은 Reactive GraphQL 마스터 노트 시리즈의 네 번째 편입니다. 1~3편이 GraphQL 자체였다면, 이번엔 Spring 통합 — Spring for GraphQL.
@QueryMapping 한 줄로 컨트롤러 끝. @BatchMapping으로 N+1 자동 해결. Spring MVC 친화적 어노테이션.
처음 Spring for GraphQL이 어렵게 느껴지는 이유
처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, 5 어노테이션이 한 번에 등장합니다. 둘째, @SchemaMapping vs @BatchMapping 헷갈립니다.
해결법은 한 가지예요. "5 어노테이션 = Schema의 5 위치" 한 줄. Query·Mutation·Subscription·Field·Field(Batch). 각자 명확한 자리.
Spring for GraphQL — 의존성
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-graphql'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
Spring Boot 자동 구성:
- GraphQL 엔진 (graphql-java)
- 컨트롤러 매핑
- WebSocket
- GraphiQL UI
5 핵심 어노테이션
@Controller
public class UserController {
@QueryMapping # type Query 안 필드
public User user(@Argument String id) { ... }
@MutationMapping # type Mutation 안 필드
public User createUser(@Argument CreateUserInput input) { ... }
@SubscriptionMapping # type Subscription 안 필드
public Flux<Post> postCreated() { ... }
@SchemaMapping(typeName = "User", field = "posts") # 다른 타입의 필드
public List<Post> userPosts(User user) { ... }
@BatchMapping(typeName = "User", field = "posts") # N+1 해결
public Map<User, List<Post>> userPosts(List<User> users) { ... }
}
| 어노테이션 | 위치 | 사용처 |
|---|---|---|
| @QueryMapping | Query.X | 단순 조회 |
| @MutationMapping | Mutation.X | 변경 |
| @SubscriptionMapping | Subscription.X | 실시간 |
| @SchemaMapping | Type.field | 외부 데이터 |
| @BatchMapping | Type.field (배치) | N+1 해결 |
@QueryMapping — 자동 매핑
type Query {
user(id: ID!): User
users: [User!]!
searchUsers(name: String): [User!]!
}
@QueryMapping
public User user(@Argument String id) { ... }
@QueryMapping
public List<User> users() { ... }
@QueryMapping
public List<User> searchUsers(@Argument String name) { ... }
여기서 정말 중요한 시험 함정 — 메서드 이름 = Schema 필드 이름 (자동 매핑). 다르면 명시:
@QueryMapping(name = "user")
public User getUser(@Argument String id) { ... }
@Argument — 인자 매핑
@QueryMapping
public List<Post> searchPosts(
@Argument String title,
@Argument PostFilter filter,
@Argument(name = "first") Integer pageSize
) { ... }
Scalar·Input 객체·List 등 모든 타입.
@SchemaMapping — 다른 타입 필드
type User {
id: ID!
name: String!
posts: [Post!]! # 외부 데이터
}
@SchemaMapping(typeName = "User", field = "posts")
public List<Post> userPosts(User user) {
return postService.findByAuthor(user.getId());
}
User 타입의 posts 필드 요청될 때 호출.
여기서 시험 함정이 하나 있어요. 첫 인자 = 부모 객체 (User). 두 번째부터 @Argument·기타.
@SchemaMapping(typeName = "User", field = "posts")
public List<Post> userPosts(
User user, # 부모
@Argument Integer limit, # 인자
DataFetchingEnvironment env # 환경 정보
) { ... }
@BatchMapping — N+1 해결
N+1 문제
{
users { # 1 쿼리
id
posts { # N 쿼리 (각 사용자마다)
title
}
}
}
1 쿼리: SELECT * FROM users (10 사용자)
N 쿼리: SELECT * FROM posts WHERE author = ? × 10
= 11 쿼리
@BatchMapping 해결
@BatchMapping(typeName = "User", field = "posts")
public Map<User, List<Post>> userPosts(List<User> users) {
List<String> userIds = users.stream().map(User::getId).toList();
Map<String, List<Post>> postsByUser = postService.findByAuthors(userIds)
.stream()
.collect(Collectors.groupingBy(Post::getAuthorId));
return users.stream()
.collect(Collectors.toMap(
user -> user,
user -> postsByUser.getOrDefault(user.getId(), List.of())
));
}
1 쿼리: SELECT * FROM users
1 쿼리: SELECT * FROM posts WHERE author IN (...)
= 2 쿼리 (N+1 → 2)
여기서 정말 중요한 시험 함정 — @BatchMapping = N+1 자동 해결. List 부모 → Map 결과. graphql-java DataLoader 자동 사용.
DataFetchingEnvironment
@QueryMapping
public Object myField(DataFetchingEnvironment env) {
Map<String, Object> args = env.getArguments();
String fieldName = env.getField().getName();
Object source = env.getSource();
GraphQLContext context = env.getGraphQlContext();
return ...;
}
GraphQL 실행 환경 전체 정보. 고급 사용.
Authentication 주입
@QueryMapping
public User me(@AuthenticationPrincipal UserDetails user) {
return userService.findByUsername(user.getUsername());
}
@MutationMapping
@PreAuthorize("hasRole('ADMIN')")
public User deleteUser(@Argument String id) { ... }
Spring Security 통합. 6편에서 자세히.
Reactive 반환
@QueryMapping
public Mono<User> user(@Argument String id) {
return userService.findByIdReactive(id);
}
@QueryMapping
public Flux<User> users() {
return userService.findAllReactive();
}
@SubscriptionMapping
public Flux<Post> postCreated() {
return postSink.asFlux();
}
Mono·Flux 자동. WebFlux 환경.
Schema 자동 발견
src/main/resources/graphql/
├── schema.graphqls # 기본
├── user.graphqls
└── post.graphqls
graphql/ 폴더의 모든 .graphqls·.gqls 파일 자동 로드.
설정
spring:
graphql:
path: /graphql # 엔드포인트
websocket:
path: /graphql # WebSocket
graphiql:
enabled: true # GraphiQL UI
path: /graphiql
schema:
printer:
enabled: true # 스키마 자동 출력
locations:
- "classpath:graphql/**"
file-extensions:
- ".graphqls"
- ".gqls"
cors:
allowed-origins: "*"
GraphiQL — 개발 UI
http://localhost:8080/graphiql
스키마 자동 발견·자동완성·문서·쿼리 실행·히스토리. 개발 표준.
여기서 시험 함정이 하나 있어요. 운영 환경 GraphiQL 비활성. 보안 — 스키마 노출. spring.graphql.graphiql.enabled: false.
Custom Scalar
scalar DateTime
scalar BigDecimal
scalar Money
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return wiringBuilder -> wiringBuilder
.scalar(GraphQLScalarType.newScalar()
.name("DateTime")
.coercing(new DateTimeCoercing())
.build());
}
또는 자동:
@Configuration
public class GraphQLConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiring() {
return wiringBuilder -> wiringBuilder
.scalar(ExtendedScalars.DateTime)
.scalar(ExtendedScalars.Json);
}
}
graphql-java-extended-scalars 라이브러리.
Error Handling
@Controller
public class ExceptionController {
@GraphQlExceptionHandler(NotFoundException.class)
public GraphQLError handleNotFound(NotFoundException e) {
return GraphQLError.newError()
.errorType(ErrorType.NOT_FOUND)
.message(e.getMessage())
.build();
}
@GraphQlExceptionHandler(ValidationException.class)
public List<GraphQLError> handleValidation(ValidationException e) {
return e.getErrors().stream()
.map(err -> GraphQLError.newError()
.errorType(ErrorType.BAD_REQUEST)
.message(err.getMessage())
.extensions(Map.of("field", err.getField()))
.build())
.toList();
}
}
Spring @ControllerAdvice 비슷.
클래스 레벨 매핑
@Controller
@SchemaMapping(typeName = "User") # 클래스 레벨
public class UserController {
@SchemaMapping(field = "posts") # User.posts
public List<Post> posts(User user) { ... }
@SchemaMapping(field = "name") # User.name
public String name(User user) {
return user.getFirstName() + " " + user.getLastName();
}
}
같은 타입의 여러 필드 한 컨트롤러에.
Validation
@MutationMapping
public User createUser(@Argument @Valid CreateUserInput input) {
return userService.create(input);
}
public class CreateUserInput {
@NotBlank
private String name;
@Email
private String email;
}
Bean Validation 통합. 자동 검증·에러 응답.
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 4편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- 의존성 —
spring-boot-starter-graphql+ WebFlux - 자동 — GraphQL 엔진·매핑·WebSocket·GraphiQL
- 5 어노테이션 — @QueryMapping·@MutationMapping·@SubscriptionMapping·@SchemaMapping·@BatchMapping
- 메서드 이름 = Schema 필드 이름 (자동)
@Argument= 인자 매핑 (Scalar·Input·List)@SchemaMapping= 다른 타입의 필드- 첫 인자 = 부모 객체 (예: User)
@BatchMapping= N+1 자동 해결- List 부모 → Map 결과
- DataLoader 자동 사용
DataFetchingEnvironment= 실행 환경 정보@AuthenticationPrincipal= Spring Security 사용자@PreAuthorize메서드 보안- Mono·Flux 반환 자동 (WebFlux)
- Schema =
resources/graphql/*.graphqls spring.graphql.*설정- GraphiQL — 개발 UI (운영 OFF)
- Custom Scalar —
RuntimeWiringConfigurer graphql-java-extended-scalars라이브러리@GraphQlExceptionHandler= 글로벌 에러 처리- 클래스 레벨
@SchemaMapping(typeName = "User")가능 - Bean Validation +
@Valid통합
시리즈 다른 편
- 1편 — 기본 개념·Schema
- 2편 — Query·Mutation·Variables
- 3편 — Subscription·실시간 구독
- 4편 — Spring for GraphQL (현재 글)
- 5편 — Reactive GraphQL
- 6편 — Security·Testing
- 7편 — 고급 (DataLoader·Federation·운영)
공식 문서: Spring for GraphQL Reference 에서 더 깊이.
다음 글(5편)에서는 Reactive GraphQL — Mono/Flux 깊은 통합, WebFlux 환경 패턴, R2DBC·Reactive Repositories까지 풀어 갑니다.