Wawancara Spring GraphQL: Resolver, DataLoader, dan Solusi Masalah N+1
Persiapkan diri untuk wawancara Spring GraphQL dengan panduan lengkap ini. Resolver, DataLoader, penanganan masalah N+1, mutation, dan praktik terbaik untuk pertanyaan teknis.

Spring for GraphQL menyederhanakan integrasi GraphQL dalam aplikasi Spring Boot. Teknologi ini telah menjadi sangat penting untuk API modern, dan pertanyaan wawancara tentang topik ini semakin sering muncul. Panduan ini membahas konsep inti: resolver, DataLoader, masalah N+1, dan pola lanjutan.
Perekrut secara khusus menguji pemahaman tentang masalah N+1 dan penggunaan DataLoader. Kedua topik ini mewakili 60% pertanyaan dalam wawancara Spring GraphQL.
Apa itu Spring for GraphQL?
Spring for GraphQL adalah penerus resmi dari GraphQL Java Spring, terintegrasi secara native ke dalam framework Spring sejak versi 2.7. Integrasi ini memberikan beberapa keunggulan: dukungan anotasi Spring, integrasi dengan Spring Security, dan penggunaan WebFlux atau MVC yang transparan.
Konfigurasi dasar hanya memerlukan dependensi spring-boot-starter-graphql dan skema GraphQL.
dependencies {
// Spring GraphQL starter dengan Spring MVC
implementation("org.springframework.boot:spring-boot-starter-graphql")
implementation("org.springframework.boot:spring-boot-starter-web")
// Untuk pengujian GraphQL
testImplementation("org.springframework.graphql:spring-graphql-test")
}Skema GraphQL berada di src/main/resources/graphql/ dengan ekstensi .graphqls.
# schema.graphqls
type Query {
# Mengambil artikel berdasarkan identifikatornya
article(id: ID!): Article
# Daftar artikel berhalaman
articles(page: Int = 0, size: Int = 10): [Article!]!
}
type Article {
id: ID!
title: String!
content: String!
# Relasi dengan penulis
author: Author!
# Daftar komentar
comments: [Comment!]!
createdAt: String!
}
type Author {
id: ID!
name: String!
email: String!
# Artikel yang ditulis oleh penulis ini
articles: [Article!]!
}
type Comment {
id: ID!
content: String!
author: Author!
createdAt: String!
}Arsitektur ini mendeklarasikan tipe dan relasinya. GraphQL secara otomatis menghasilkan dokumentasi dan memvalidasi query di sisi server.
Bagaimana Cara Kerja Resolver Spring GraphQL?
Resolver merupakan inti dari eksekusi GraphQL. Setiap field skema dapat memiliki resolver khusus. Spring menggunakan anotasi @QueryMapping untuk query root dan @SchemaMapping untuk relasi.
@Controller
public class ArticleController {
private final ArticleRepository articleRepository;
private final AuthorRepository authorRepository;
public ArticleController(ArticleRepository articleRepository,
AuthorRepository authorRepository) {
this.articleRepository = articleRepository;
this.authorRepository = authorRepository;
}
// Resolver untuk Query.article(id)
@QueryMapping
public Article article(@Argument Long id) {
return articleRepository.findById(id)
.orElseThrow(() -> new ArticleNotFoundException(id));
}
// Resolver untuk Query.articles(page, size)
@QueryMapping
public List<Article> articles(@Argument int page,
@Argument int size) {
Pageable pageable = PageRequest.of(page, size);
return articleRepository.findAll(pageable).getContent();
}
// Resolver untuk Article.author - dipanggil untuk setiap artikel
@SchemaMapping(typeName = "Article", field = "author")
public Author author(Article article) {
return authorRepository.findById(article.getAuthorId())
.orElseThrow(() -> new AuthorNotFoundException(article.getAuthorId()));
}
}Resolver author dijalankan untuk setiap artikel yang dikembalikan. Arsitektur fleksibel ini memuat data sesuai permintaan tetapi memperkenalkan masalah N+1.
Kandidat yang mengimplementasikan resolver @SchemaMapping tanpa menyebutkan masalah N+1 menunjukkan pemahaman GraphQL yang tidak lengkap. Perekrut secara sistematis mengharapkan analisis ini.
Apa itu Masalah N+1 di GraphQL?
Masalah N+1 muncul ketika query GraphQL memicu N query tambahan untuk memuat relasi. Pola destruktif ini terjadi secara sistematis dengan resolver dasar.
Pertimbangkan query yang mengambil 50 artikel beserta penulisnya:
# Query GraphQL
query {
articles(size: 50) {
id
title
author {
name
}
}
}Dengan resolver sebelumnya, query ini mengeksekusi:
- 1 query untuk 50 artikel
- 50 query untuk memuat setiap penulis secara individual
-- Query 1: pengambilan artikel
SELECT * FROM articles LIMIT 50
-- Query 2-51: setiap penulis secara individual
SELECT * FROM authors WHERE id = 1
SELECT * FROM authors WHERE id = 2
SELECT * FROM authors WHERE id = 3
-- ... 47 query tambahanPenggandaan query ini secara drastis menurunkan kinerja. Endpoint yang merespons dalam 50ms dapat melonjak menjadi 2 detik dengan N+1.
Bagaimana Cara Kerja DataLoader?
DataLoader mengelompokkan query individual menjadi permintaan batch. Alih-alih memuat setiap penulis secara terpisah, DataLoader mengumpulkan semua ID yang diminta dan mengeksekusi satu query.
@Component
public class AuthorBatchLoader {
private final AuthorRepository authorRepository;
public AuthorBatchLoader(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
// Metode pemuatan batch
public Mono<Map<Long, Author>> loadAuthors(Set<Long> authorIds) {
// Query tunggal untuk semua penulis
List<Author> authors = authorRepository.findAllById(authorIds);
// Transformasi ke Map untuk pencarian cepat
Map<Long, Author> authorMap = authors.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
return Mono.just(authorMap);
}
}Pendaftaran DataLoader dilakukan melalui BatchLoaderRegistry dalam konfigurasi khusus.
@Configuration
public class GraphQLConfig {
@Bean
public BatchLoaderRegistry batchLoaderRegistry(
AuthorBatchLoader authorBatchLoader) {
return registry -> {
// Pendaftaran DataLoader untuk penulis
registry.forTypePair(Long.class, Author.class)
.registerMappedBatchLoader((authorIds, env) ->
authorBatchLoader.loadAuthors(authorIds));
};
}
}Resolver yang dimodifikasi sekarang menggunakan DataLoader alih-alih akses langsung:
@Controller
public class ArticleController {
private final ArticleRepository articleRepository;
public ArticleController(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
@QueryMapping
public List<Article> articles(@Argument int page,
@Argument int size) {
Pageable pageable = PageRequest.of(page, size);
return articleRepository.findAll(pageable).getContent();
}
// Resolver yang dioptimalkan dengan DataLoader
@SchemaMapping(typeName = "Article", field = "author")
public CompletableFuture<Author> author(
Article article,
DataLoader<Long, Author> authorDataLoader) {
// DataLoader secara otomatis mengelompokkan panggilan
return authorDataLoader.load(article.getAuthorId());
}
}Dengan pendekatan ini, query yang sama untuk 50 artikel hanya mengeksekusi 2 query SQL:
-- Query 1: pengambilan artikel
SELECT * FROM articles LIMIT 50
-- Query 2: semua penulis sekaligus
SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ..., 50)Siap menguasai wawancara Spring Boot Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Bagaimana Cara Mengimplementasikan DataLoader dengan Konteks?
DataLoader lanjutan terkadang memerlukan konteks tambahan, seperti identifikator tenant untuk aplikasi multi-tenant atau filter keamanan.
@Component
public class SecuredAuthorBatchLoader {
private final AuthorRepository authorRepository;
private final SecurityContextHolder securityContext;
public SecuredAuthorBatchLoader(AuthorRepository authorRepository,
SecurityContextHolder securityContext) {
this.authorRepository = authorRepository;
this.securityContext = securityContext;
}
public Mono<Map<Long, Author>> loadAuthorsForTenant(
Set<Long> authorIds,
BatchLoaderEnvironment env) {
// Mengambil konteks GraphQL
GraphQLContext context = env.getContext();
String tenantId = context.get("tenantId");
// Query difilter berdasarkan tenant
List<Author> authors = authorRepository
.findAllByIdInAndTenantId(authorIds, tenantId);
Map<Long, Author> authorMap = authors.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
return Mono.just(authorMap);
}
}Konfigurasi konteks dilakukan melalui interceptor WebGraphQL:
@Component
public class TenantInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(
WebGraphQlRequest request,
Chain chain) {
// Mengekstrak tenant dari header
String tenantId = request.getHeaders()
.getFirst("X-Tenant-ID");
// Menambahkan ke konteks GraphQL
request.configureExecutionInput((input, builder) ->
builder.graphQLContext(ctx ->
ctx.put("tenantId", tenantId)
).build()
);
return chain.next(request);
}
}Apa Perbedaan Antara @QueryMapping dan @SchemaMapping?
Pertanyaan wawancara klasik ini memverifikasi pemahaman tentang hierarki resolver.
| Anotasi | Penggunaan | Setara |
|---------|------------|--------|
| @QueryMapping | Field root tipe Query | @SchemaMapping(typeName = "Query") |
| @MutationMapping | Field root tipe Mutation | @SchemaMapping(typeName = "Mutation") |
| @SubscriptionMapping | Subscription waktu nyata | @SchemaMapping(typeName = "Subscription") |
| @SchemaMapping | Semua field dari tipe apa pun | Bentuk umum |
@Controller
public class EquivalenceDemo {
// Kedua deklarasi ini setara
@QueryMapping
public Article article(@Argument Long id) {
return findArticle(id);
}
@SchemaMapping(typeName = "Query", field = "article")
public Article articleEquivalent(@Argument Long id) {
return findArticle(id);
}
// Untuk field bersarang, hanya @SchemaMapping yang berfungsi
@SchemaMapping(typeName = "Article", field = "author")
public Author author(Article article) {
return findAuthor(article.getAuthorId());
}
// Sintaksis alternatif dengan tipe sebagai parameter
@SchemaMapping
public Author author(Article article) {
// Spring menyimpulkan typeName = "Article" dari parameter
return findAuthor(article.getAuthorId());
}
}Bagaimana Mengelola Mutation dengan Validasi?
Mutation GraphQL memodifikasi data. Spring GraphQL terintegrasi dengan Bean Validation untuk memvalidasi input.
# schema.graphqls
type Mutation {
createArticle(input: CreateArticleInput!): Article!
updateArticle(id: ID!, input: UpdateArticleInput!): Article!
deleteArticle(id: ID!): Boolean!
}
input CreateArticleInput {
title: String!
content: String!
authorId: ID!
}
input UpdateArticleInput {
title: String
content: String
}Controller menggunakan @Valid untuk memicu validasi:
@Controller
public class ArticleMutationController {
private final ArticleService articleService;
public ArticleMutationController(ArticleService articleService) {
this.articleService = articleService;
}
@MutationMapping
public Article createArticle(@Argument @Valid CreateArticleInput input) {
// Validasi dijalankan secara otomatis
return articleService.create(input);
}
@MutationMapping
public Article updateArticle(@Argument Long id,
@Argument @Valid UpdateArticleInput input) {
return articleService.update(id, input);
}
@MutationMapping
public boolean deleteArticle(@Argument Long id) {
articleService.delete(id);
return true;
}
}public record CreateArticleInput(
@NotBlank(message = "Judul wajib diisi")
@Size(min = 5, max = 200, message = "Judul harus antara 5 dan 200 karakter")
String title,
@NotBlank(message = "Konten wajib diisi")
@Size(min = 100, message = "Konten harus minimal 100 karakter")
String content,
@NotNull(message = "Penulis wajib diisi")
Long authorId
) {}Kesalahan validasi secara otomatis mengembalikan kesalahan GraphQL terstruktur dengan jalur field yang tidak valid. Spring GraphQL memformat kesalahan ini sesuai dengan spesifikasi GraphQL.
Bagaimana Mengoptimalkan Query dengan @BatchMapping?
Anotasi @BatchMapping menyederhanakan pembuatan DataLoader langsung di controller. Pendekatan ini menghindari konfigurasi BatchLoaderRegistry yang eksplisit.
@Controller
public class OptimizedArticleController {
private final ArticleRepository articleRepository;
private final AuthorRepository authorRepository;
private final CommentRepository commentRepository;
public OptimizedArticleController(ArticleRepository articleRepository,
AuthorRepository authorRepository,
CommentRepository commentRepository) {
this.articleRepository = articleRepository;
this.authorRepository = authorRepository;
this.commentRepository = commentRepository;
}
@QueryMapping
public List<Article> articles(@Argument int page,
@Argument int size) {
return articleRepository.findAll(PageRequest.of(page, size))
.getContent();
}
// BatchMapping untuk penulis - memproses semua artikel sekaligus
@BatchMapping
public Map<Article, Author> author(List<Article> articles) {
// Mengumpulkan ID penulis unik
Set<Long> authorIds = articles.stream()
.map(Article::getAuthorId)
.collect(Collectors.toSet());
// Query tunggal untuk semua penulis
Map<Long, Author> authorsById = authorRepository
.findAllById(authorIds)
.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
// Asosiasi artikel -> penulis
return articles.stream()
.collect(Collectors.toMap(
Function.identity(),
article -> authorsById.get(article.getAuthorId())
));
}
// BatchMapping untuk komentar
@BatchMapping
public Map<Article, List<Comment>> comments(List<Article> articles) {
List<Long> articleIds = articles.stream()
.map(Article::getId)
.toList();
// Pengambilan batch komentar
List<Comment> allComments = commentRepository
.findByArticleIdIn(articleIds);
// Pengelompokan per artikel
Map<Long, List<Comment>> commentsByArticle = allComments.stream()
.collect(Collectors.groupingBy(Comment::getArticleId));
return articles.stream()
.collect(Collectors.toMap(
Function.identity(),
article -> commentsByArticle
.getOrDefault(article.getId(), List.of())
));
}
}@BatchMapping menawarkan sintaksis yang lebih ringkas daripada konfigurasi DataLoader manual sambil mempertahankan jaminan kinerja yang sama.
Bagaimana Menguji Resolver GraphQL?
Spring GraphQL menyediakan GraphQlTester untuk menulis pengujian yang ekspresif dan mudah dibaca.
@SpringBootTest
@AutoConfigureGraphQlTester
class ArticleControllerTest {
@Autowired
private GraphQlTester graphQlTester;
@Test
void shouldReturnArticleById() {
// Given: dokumen GraphQL
String query = """
query {
article(id: 1) {
id
title
author {
name
}
}
}
""";
// When & Then
graphQlTester.document(query)
.execute()
.path("article.id").entity(String.class).isEqualTo("1")
.path("article.title").entity(String.class).isNotEmpty()
.path("article.author.name").entity(String.class).isNotEmpty();
}
@Test
void shouldCreateArticle() {
String mutation = """
mutation {
createArticle(input: {
title: "Judul baru",
content: "Konten pengujian yang cukup panjang untuk memenuhi batasan validasi",
authorId: 1
}) {
id
title
}
}
""";
graphQlTester.document(mutation)
.execute()
.path("createArticle.id").entity(String.class).isNotEmpty()
.path("createArticle.title").entity(String.class)
.isEqualTo("Judul baru");
}
@Test
void shouldReturnErrorForInvalidInput() {
String mutation = """
mutation {
createArticle(input: {
title: "",
content: "pendek",
authorId: 1
}) {
id
}
}
""";
graphQlTester.document(mutation)
.execute()
.errors()
.expect(error -> error.getMessage()
.contains("Judul wajib diisi"));
}
@Test
void shouldHandleVariables() {
String query = """
query GetArticle($id: ID!) {
article(id: $id) {
id
title
}
}
""";
graphQlTester.document(query)
.variable("id", 42)
.execute()
.path("article").valueIsNull();
}
}Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Bagaimana Mengelola Subscription Waktu Nyata?
Subscription memungkinkan pengiriman data waktu nyata ke klien melalui WebSocket.
# schema.graphqls
type Subscription {
articleCreated: Article!
commentAdded(articleId: ID!): Comment!
}@Controller
public class SubscriptionController {
private final Sinks.Many<Article> articleSink =
Sinks.many().multicast().onBackpressureBuffer();
private final Sinks.Many<Comment> commentSink =
Sinks.many().multicast().onBackpressureBuffer();
@SubscriptionMapping
public Flux<Article> articleCreated() {
return articleSink.asFlux();
}
@SubscriptionMapping
public Flux<Comment> commentAdded(@Argument Long articleId) {
return commentSink.asFlux()
.filter(comment -> comment.getArticleId().equals(articleId));
}
// Metode yang dipanggil saat artikel dibuat
public void publishArticle(Article article) {
articleSink.tryEmitNext(article);
}
// Metode yang dipanggil saat komentar ditambahkan
public void publishComment(Comment comment) {
commentSink.tryEmitNext(comment);
}
}Konfigurasi WebSocket memerlukan dependensi tambahan:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-websocket")
}@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}Apa Praktik Terbaik Keamanan GraphQL?
Keamanan GraphQL memerlukan beberapa lapisan perlindungan terhadap serangan denial-of-service dan akses tidak sah.
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
// Endpoint GraphQL terlindungi
.requestMatchers("/graphql").authenticated()
// GraphiQL hanya dapat diakses di dev
.requestMatchers("/graphiql").permitAll()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build();
}
}@Controller
public class SecuredArticleController {
private final ArticleRepository articleRepository;
public SecuredArticleController(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
// Akses dibatasi untuk pengguna terautentikasi
@QueryMapping
@PreAuthorize("isAuthenticated()")
public List<Article> articles(@Argument int page,
@Argument int size) {
return articleRepository.findAll(PageRequest.of(page, size))
.getContent();
}
// Hanya administrator yang dapat menghapus
@MutationMapping
@PreAuthorize("hasRole('ADMIN')")
public boolean deleteArticle(@Argument Long id) {
articleRepository.deleteById(id);
return true;
}
// Pemeriksaan halus: hanya penulis yang dapat memodifikasi
@MutationMapping
@PreAuthorize("@articleSecurity.isAuthor(#id, authentication)")
public Article updateArticle(@Argument Long id,
@Argument UpdateArticleInput input) {
return articleRepository.findById(id)
.map(article -> {
article.setTitle(input.title());
article.setContent(input.content());
return articleRepository.save(article);
})
.orElseThrow();
}
}Perlindungan terhadap query kompleks:
# application.yml
spring:
graphql:
schema:
introspection:
# Nonaktifkan introspection di produksi
enabled: false@Component
public class QueryComplexityInstrumentation extends SimplePerformantInstrumentation {
private static final int MAX_COMPLEXITY = 100;
@Override
public InstrumentationState createState(InstrumentationCreateStateParameters parameters) {
return new ComplexityState();
}
@Override
public InstrumentationContext<ExecutionResult> beginExecution(
InstrumentationExecutionParameters parameters,
InstrumentationState state) {
int complexity = calculateComplexity(parameters.getDocument());
if (complexity > MAX_COMPLEXITY) {
throw new GraphQLException(
"Query terlalu kompleks: " + complexity + " (max: " + MAX_COMPLEXITY + ")"
);
}
return super.beginExecution(parameters, state);
}
}Kesimpulan
Spring for GraphQL menawarkan integrasi GraphQL yang elegan ke dalam ekosistem Spring. Penguasaan resolver, DataLoader, dan pengelolaan masalah N+1 merupakan dasar pengetahuan yang diharapkan dalam wawancara.
Daftar Periksa Wawancara Spring GraphQL:
- ✅ Menjelaskan peran anotasi
@QueryMapping,@MutationMapping, dan@SchemaMapping - ✅ Menjelaskan masalah N+1 dan dampaknya terhadap kinerja GraphQL
- ✅ Mengimplementasikan DataLoader dengan
@BatchMappingatau BatchLoaderRegistry - ✅ Memvalidasi input mutation dengan Bean Validation
- ✅ Mengamankan resolver dengan
@PreAuthorizedan Spring Security - ✅ Menguji resolver dengan GraphQlTester
- ✅ Mengkonfigurasi subscription untuk fungsi waktu nyata
- ✅ Melindungi API dari query yang terlalu kompleks
Tag
Bagikan
Artikel terkait

Wawancara Spring Boot: Propagasi Transaksi Dijelaskan
Kuasai propagasi transaksi Spring Boot: REQUIRED, REQUIRES_NEW, NESTED dan lainnya. 12 pertanyaan wawancara dengan contoh kode dan jebakan umum.

Solusi N+1 di Spring Data JPA Tahun 2026: Fetch Join dan EntityGraph
Panduan lengkap untuk mendeteksi dan memperbaiki masalah N+1 di Spring Data JPA. Fetch join, @EntityGraph, batch fetching, dan strategi performa kueri.

30 Pertanyaan Wawancara Spring Boot: Panduan Lengkap untuk Developer Java
Persiapkan wawancara Spring Boot dengan 30 pertanyaan penting tentang auto-configuration, starter, Spring Data JPA, keamanan, dan testing.