# 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. - Published: 2026-03-25 - Updated: 2026-05-04 - Author: SharpSkill - Tags: spring graphql, dataloaders, n+1 problem, interview, graphql java - Reading time: 14 min --- 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. > **Fokus Wawancara** > > 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. ```java // build.gradle.kts 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`. ```graphql # 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. ```java // ArticleController.java @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
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. > **Jebakan Klasik Wawancara** > > 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: ```graphql # 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 ```sql -- 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 tambahan ``` Penggandaan 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. ```java // AuthorBatchLoader.java @Component public class AuthorBatchLoader { private final AuthorRepository authorRepository; public AuthorBatchLoader(AuthorRepository authorRepository) { this.authorRepository = authorRepository; } // Metode pemuatan batch public Mono> loadAuthors(Set authorIds) { // Query tunggal untuk semua penulis List authors = authorRepository.findAllById(authorIds); // Transformasi ke Map untuk pencarian cepat Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` Pendaftaran DataLoader dilakukan melalui `BatchLoaderRegistry` dalam konfigurasi khusus. ```java // GraphQLConfig.java @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: ```java // ArticleController.java @Controller public class ArticleController { private final ArticleRepository articleRepository; public ArticleController(ArticleRepository articleRepository) { this.articleRepository = articleRepository; } @QueryMapping public List
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( Article article, DataLoader 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: ```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) ``` ## Bagaimana Cara Mengimplementasikan DataLoader dengan Konteks? DataLoader lanjutan terkadang memerlukan konteks tambahan, seperti identifikator tenant untuk aplikasi multi-tenant atau filter keamanan. ```java // SecuredAuthorBatchLoader.java @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> loadAuthorsForTenant( Set authorIds, BatchLoaderEnvironment env) { // Mengambil konteks GraphQL GraphQLContext context = env.getContext(); String tenantId = context.get("tenantId"); // Query difilter berdasarkan tenant List authors = authorRepository .findAllByIdInAndTenantId(authorIds, tenantId); Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` Konfigurasi konteks dilakukan melalui interceptor WebGraphQL: ```java // TenantInterceptor.java @Component public class TenantInterceptor implements WebGraphQlInterceptor { @Override public Mono 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 | ```java // EquivalenceDemo.java @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. ```graphql # 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: ```java // ArticleMutationController.java @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; } } ``` ```java // CreateArticleInput.java 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 ) {} ``` > **Penanganan Kesalahan** > > 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. ```java // OptimizedArticleController.java @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
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 author(List
articles) { // Mengumpulkan ID penulis unik Set authorIds = articles.stream() .map(Article::getAuthorId) .collect(Collectors.toSet()); // Query tunggal untuk semua penulis Map 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> comments(List
articles) { List articleIds = articles.stream() .map(Article::getId) .toList(); // Pengambilan batch komentar List allComments = commentRepository .findByArticleIdIn(articleIds); // Pengelompokan per artikel Map> 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. ```java // ArticleControllerTest.java @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(); } } ``` ## Bagaimana Mengelola Subscription Waktu Nyata? Subscription memungkinkan pengiriman data waktu nyata ke klien melalui WebSocket. ```graphql # schema.graphqls type Subscription { articleCreated: Article! commentAdded(articleId: ID!): Comment! } ``` ```java // SubscriptionController.java @Controller public class SubscriptionController { private final Sinks.Many
articleSink = Sinks.many().multicast().onBackpressureBuffer(); private final Sinks.Many commentSink = Sinks.many().multicast().onBackpressureBuffer(); @SubscriptionMapping public Flux
articleCreated() { return articleSink.asFlux(); } @SubscriptionMapping public Flux 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: ```java // build.gradle.kts dependencies { implementation("org.springframework.boot:spring-boot-starter-websocket") } ``` ```java // WebSocketConfig.java @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. ```java // SecurityConfig.java @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(); } } ``` ```java // SecuredArticleController.java @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
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: ```yaml # application.yml spring: graphql: schema: introspection: # Nonaktifkan introspection di produksi enabled: false ``` ```java // QueryComplexityInstrumentation.java @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 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 `@BatchMapping` atau BatchLoaderRegistry - ✅ Memvalidasi input mutation dengan Bean Validation - ✅ Mengamankan resolver dengan `@PreAuthorize` dan Spring Security - ✅ Menguji resolver dengan GraphQlTester - ✅ Mengkonfigurasi subscription untuk fungsi waktu nyata - ✅ Melindungi API dari query yang terlalu kompleks --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/spring-boot/spring-graphql-interview-resolvers-dataloaders-n1