구독, 오류 및 스키마 보안
구독을 통해 실시간 업데이트를 스트리밍하고 필드 수준 권한 부여로 스키마를 강화합니다.
구독, 오류 및 스키마 보안은(는) CoddyKit의 무료 Spring Boot 4 Complete Guide 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Spring Boot 4 Complete Guide 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Spring Boot 4 Complete Guide 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Beyond Query and Mutation
Spring for GraphQL supports three root operation types. So far you have used Query (read) and Mutation (write). The third is Subscription — a long-lived stream that pushes data to the client whenever a server-side event occurs.
- Query/Mutation: one request, one response.
- Subscription: one request, many responses over time.
In this lesson you will stream live updates with subscriptions, shape GraphQL errors cleanly, and lock down individual fields with method security.
Declaring a Subscription in the Schema
Subscriptions are declared in the GraphQL schema just like queries. Each subscription field describes a stream of a given type. Below, messageAdded streams a Message for a particular room.
The transport for subscriptions is typically WebSocket (graphql-ws protocol), so make sure your client connects over ws:// rather than plain HTTP POST.
type Subscription {
messageAdded(roomId: ID!): Message!
}
type Message {
id: ID!
roomId: ID!
text: String!
author: String!
}A Subscription Returns a Reactive Stream
In Spring for GraphQL, a method annotated with @SubscriptionMapping must return a Reactive Streams Publisher — most commonly a Project Reactor Flux. Each element the Flux emits is delivered to the subscribed client as a separate GraphQL response.
- The method name maps to the subscription field (
messageAdded). - Arguments come from
@Argument, exactly like queries. - The
Fluxstays open until it completes, errors, or the client disconnects.
@Controller
public class ChatSubscriptionController {
private final ChatService chatService;
public ChatSubscriptionController(ChatService chatService) {
this.chatService = chatService;
}
@SubscriptionMapping
public Flux<Message> messageAdded(@Argument String roomId) {
return chatService.streamMessages(roomId);
}
}Backing the Stream with a Sink
Where does the Flux come from? A common pattern is a Reactor Sinks.Many as a hot multicast source. Mutations push new events into the sink with tryEmitNext, and every active subscriber receives them.
Use multicast().onBackpressureBuffer() so multiple subscribers can share one source, and filter per room so each client only gets its own messages.
@Service
public class ChatService {
private final Sinks.Many<Message> sink =
Sinks.many().multicast().onBackpressureBuffer();
public Message publish(Message message) {
sink.tryEmitNext(message);
return message;
}
public Flux<Message> streamMessages(String roomId) {
return sink.asFlux()
.filter(m -> m.roomId().equals(roomId));
}
}Wiring a Mutation to the Stream
A subscription only emits when something feeds it. Here a @MutationMapping creates a message and publishes it into the sink. Any client subscribed to that room receives the new message immediately.
This decoupling — mutation writes, subscription reads from the same sink — is the backbone of real-time GraphQL.
@Controller
public class ChatMutationController {
private final ChatService chatService;
public ChatMutationController(ChatService chatService) {
this.chatService = chatService;
}
@MutationMapping
public Message postMessage(@Argument String roomId,
@Argument String text,
@Argument String author) {
Message message = new Message(
UUID.randomUUID().toString(), roomId, text, author);
return chatService.publish(message);
}
}Enabling the WebSocket Transport
Subscriptions need the WebSocket endpoint enabled. With Spring Boot 4 and the GraphQL starter, set the path in application.yml. The HTTP endpoint (/graphql) stays for queries and mutations; the WebSocket endpoint (/graphql over ws) handles subscriptions.
Without this property the WebSocket handler is not registered and subscription clients fail to connect.
spring:
graphql:
websocket:
path: /graphql
connection-init-timeout: 60s
schema:
printer:
enabled: trueWhy Default GraphQL Errors Leak Detail
When an exception escapes a controller method, Spring for GraphQL turns it into a GraphQL error. By default many exceptions surface as INTERNAL_ERROR with a generic message, but stack traces and unexpected exception messages can leak implementation details if you are not careful.
The fix is a DataFetcherExceptionResolver: map your domain exceptions to clean, well-classified GraphQLError objects with the right ErrorType.
Mapping Exceptions with @GraphQlExceptionHandler
The simplest approach is an @GraphQlExceptionHandler method inside a @Controller (or a @ControllerAdvice for global scope). It works like Spring MVC exception handling: catch a specific exception and return a GraphQLError.
ErrorType.NOT_FOUND→ resource missing.ErrorType.BAD_REQUEST→ invalid input.ErrorType.FORBIDDEN→ authorization failure.
The extensions map carries machine-readable detail the client can act on.
@ControllerAdvice
public class GraphQlExceptionAdvice {
@GraphQlExceptionHandler
public GraphQLError handleNotFound(MessageNotFoundException ex) {
return GraphQLError.newError()
.errorType(ErrorType.NOT_FOUND)
.message(ex.getMessage())
.extensions(Map.of("code", "MESSAGE_NOT_FOUND"))
.build();
}
}Field-Level Authorization with @PreAuthorize
GraphQL exposes a single endpoint, so you cannot rely on URL-based security. Instead secure individual fields at the method level. With Spring Security's method security enabled (@EnableMethodSecurity), annotate controller methods with @PreAuthorize.
If the check fails, the field resolves to null and an authorization error is added to the GraphQL errors array — sibling fields still resolve normally.
@Controller
public class AdminController {
@QueryMapping
@PreAuthorize("hasRole('ADMIN')")
public List<AuditEntry> auditLog() {
return auditService.findAll();
}
@SchemaMapping(typeName = "Message", field = "author")
@PreAuthorize("isAuthenticated()")
public String author(Message message) {
return message.author();
}
}Propagating Security to Reactive Subscriptions
Subscriptions run on the reactive WebSocket transport, so the SecurityContext must travel through the reactive chain. Spring Security populates the Reactor context; access the authenticated principal with ReactiveSecurityContextHolder rather than the thread-local SecurityContextHolder.
This lets you filter a subscription stream by the current user — for example, only emitting messages from rooms the user belongs to.
@SubscriptionMapping
@PreAuthorize("isAuthenticated()")
public Flux<Message> messageAdded(@Argument String roomId) {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getName())
.flatMapMany(user ->
chatService.streamMessages(roomId, user));
}A Standalone Flux Stream Demo
You do not need a running server to understand how a subscription stream behaves. The example below uses a plain Reactor Flux with a filter — exactly the shape streamMessages returns — and prints each emitted element, mimicking what a subscribed client would receive.
Notice how only messages matching the room pass through, just like server-side per-room filtering.
import reactor.core.publisher.Flux;
public class StreamDemo {
record Message(String roomId, String text) {}
public static void main(String[] args) {
Flux<Message> source = Flux.just(
new Message("general", "hi"),
new Message("random", "noise"),
new Message("general", "streaming works"));
source.filter(m -> m.roomId().equals("general"))
.subscribe(m -> System.out.println("Push -> " + m.text()));
}
}Quick Check: Securing a Field
You want only users with the ADMIN role to be able to read the auditLog query field, while every other field in the same response keeps resolving normally for all users. Which approach fits GraphQL best?
Recap
You now have the real-time and security toolkit for Spring for GraphQL:
- Subscriptions are declared in the schema and implemented with
@SubscriptionMappingreturning a reactiveFlux/Publisher. - A
Sinks.Manyhot source lets mutations push events that subscribers stream, filtered per room. - Enable the WebSocket transport via
spring.graphql.websocket.path. - Map domain exceptions to clean
GraphQLErrors with@GraphQlExceptionHandlerand the rightErrorType. - Secure individual fields with
@PreAuthorize; failures null the field and add an error without breaking siblings. - For subscriptions, read the principal from the reactive
ReactiveSecurityContextHolder.
AI 튜터와 함께 Java을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 21
- 레슨
- 84
자주 묻는 질문
“구독, 오류 및 스키마 보안” 강의는 무료인가요?
네 — “구독, 오류 및 스키마 보안” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Spring Boot 4 Complete Guide 강의 전체를 잠금 해제할 수 있습니다. Spring Boot 4 Complete Guide 강의에는 총 4개의 강의가 포함되어 있습니다.
“구독, 오류 및 스키마 보안”에서 뭘 배우나요?
구독을 통해 실시간 업데이트를 스트리밍하고 필드 수준 권한 부여로 스키마를 강화합니다. 브라우저에서 직접 실행하는 실습 코드로 Spring Boot 4 Complete Guide을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Spring Boot 4 Complete Guide을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Spring Boot 4 Complete Guide은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“구독, 오류 및 스키마 보안” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Spring Boot 4 Complete Guide 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Spring Boot 4 Complete Guide 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 스키마 우선 설계와 타입 매핑
- 데이터 패처와 인수 바인딩
- 배치 로더로 N+1 문제 해결
- 구독, 오류 및 스키마 보안