Reactive GraphQL — Spring for GraphQL

2026-05-03확률과 통계 마스터 노트

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 ScalarRuntimeWiringConfigurer
  • graphql-java-extended-scalars 라이브러리
  • @GraphQlExceptionHandler = 글로벌 에러 처리
  • 클래스 레벨 @SchemaMapping(typeName = "User") 가능
  • Bean Validation + @Valid 통합

시리즈 다른 편

공식 문서: Spring for GraphQL Reference 에서 더 깊이.

다음 글(5편)에서는 Reactive GraphQL — Mono/Flux 깊은 통합, WebFlux 환경 패턴, R2DBC·Reactive Repositories까지 풀어 갑니다.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!