Skip to content

5.4. GraphQL vs REST vs gRPC Karşılaştırması

API protokolü seçimi, istemci ihtiyacı, veri şekli, operasyon kabiliyeti ve hata ayıklama kolaylığı arasında verilen bir trade-off'tur. Çoğu ürün için varsayılan REST yeterlidir; GraphQL ve gRPC gerçek bir basınç veya sözleşme ihtiyacı olduğunda anlamlı hale gelir.

Hızlı Karar

İhtiyaçTercihDikkat
Genel amaçlı public APIRESTVersiyonlama ve tutarlı hata modeli şart
Mobilde değişken veri ihtiyacıGraphQLQuery maliyeti ve authorization karmaşıklaşır
Servisler arası düşük gecikmegRPCTarayıcı/debug deneyimi REST kadar kolay değildir
Basit CRUD ve cache dostu endpointRESTFazla endpoint yönetimi oluşabilir

Üretim Kontrol Listesi

  • Problem: Asıl sorun fazla veri mi, çok istek mi, gecikme mi, yoksa sözleşme belirsizliği mi?
  • Çözüm: Protokol seçimi istemci, gateway, observability ve test araçlarıyla uyumlu mu?
  • Trade-off: GraphQL esneklik sağlar; gRPC performans sağlar; REST operasyonel sadelik sağlar.
  • Hata durumu: Hata modeli, timeout, retry ve idempotency davranışı protokole göre açık yazılmalı.
  • Ölçüm: Payload boyutu, round-trip sayısı, p95 latency, resolver süresi, serialization maliyeti ve cache hit oranı izlenmeli.
  • Güvenlik/maliyet: GraphQL query depth/complexity limiti ister; gRPC schema yönetimi ister; her protokol ek operasyon maliyeti getirir.

REST API

Temel Prensipler

  • Kaynak tabanlı
  • Durumsuz (stateless)
  • Önbelleğe alınabilir
  • Tekdüze arayüz

Kullanım Senaryoları

  • CRUD işlemleri
  • Kaynak yönetimi
  • Genel amaçlı API'ler
  • Açık API'ler

Avantajlar

  • Geniş ekosistem
  • Kolay anlaşılır
  • Önbellek dostu
  • Araç desteği

Dezavantajlar

  • Fazla/eksik veri çekme
  • Versiyonlama zorlukları
  • Çoklu istek ihtiyacı
  • Sınırlı gerçek zamanlılık

GraphQL

Temel Prensipler

  • İstemci tarafından belirlenen sorgular
  • Tek uç nokta
  • Güçlü tip kontrolü
  • Kendi kendini tanımlama (introspection)

Kullanım Senaryoları

  • Karmaşık veri ihtiyaçları
  • Mobil uygulamalar
  • Gerçek zamanlı güncellemeler
  • Mikroservisler

Avantajlar

  • Esnek sorgular
  • Azaltılmış ağ trafiği
  • Güçlü tip kontrolü
  • Gerçek zamanlı abonelikler

Dezavantajlar

  • Önbellekleme karmaşıklığı
  • Performans yükü
  • Güvenlik zorlukları
  • Öğrenme eğrisi

gRPC

Temel Prensipler

  • Protocol Buffers
  • HTTP/2
  • Güçlü tip kontrolü
  • Kod üretimi

Kullanım Senaryoları

  • Mikroservisler
  • Düşük gecikmeli sistemler
  • Çoklu dil ortamları
  • Gerçek zamanlı akış

Avantajlar

  • Yüksek performans
  • Çift yönlü akış
  • Güçlü tip kontrolü
  • Kod üretimi

Dezavantajlar

  • Sınırlı tarayıcı desteği
  • Daha dik öğrenme eğrisi
  • Hata ayıklama karmaşıklığı
  • Sınırlı araç desteği

Karşılaştırma Tablosu

ÖzellikRESTGraphQLgRPC
Veri TaşınımıJSON/XMLJSONProtobuf (ikili)
İstek/YanıtKaynak tabanlıİstemci-tarafı sorguSenkron (HTTP/2)
PerformansOrtaYüksekÇok Yüksek
Kullanım SenaryosuGenel amaçlı API'lerEsnek veri sorgulamaYüksek hız gerektiren sistemler
Use CaseGeneral-purpose APIsFlexible data queryingHigh-speed systems

Architecture Diagrams

General Comparison

REST Architecture

GraphQL Architecture

gRPC Architecture

Data Flow Comparison

Spring Boot Uygulama Karşılaştırması

REST API Uygulaması

java
@RestController
@RequestMapping("/api/v1/users")
@Validated
@Slf4j
public class UserRestController {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private PostService postService;
    
    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        UserResponse response = UserResponse.from(user);
        return ResponseEntity.ok(response);
    }
    
    @GetMapping("/{id}/posts")
    public ResponseEntity<PagedResponse<PostResponse>> getUserPosts(
            @PathVariable Long id,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        Pageable pageable = PageRequest.of(page, size);
        Page<Post> posts = postService.findByUserId(id, pageable);
        
        List<PostResponse> postResponses = posts.getContent().stream()
            .map(PostResponse::from)
            .collect(Collectors.toList());
            
        PagedResponse<PostResponse> response = new PagedResponse<>(
            postResponses, posts.getTotalElements(), posts.getTotalPages()
        );
        
        return ResponseEntity.ok(response);
    }
    
    @PostMapping
    public ResponseEntity<UserResponse> createUser(@RequestBody @Valid CreateUserRequest request) {
        User user = userService.create(request);
        UserResponse response = UserResponse.from(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<UserResponse> updateUser(
            @PathVariable Long id,
            @RequestBody @Valid UpdateUserRequest request) {
        
        User user = userService.update(id, request);
        UserResponse response = UserResponse.from(user);
        return ResponseEntity.ok(response);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserResponse {
    private Long id;
    private String firstName;
    private String lastName;
    private String email;
    private String phone;
    private Instant createdAt;
    private Instant updatedAt;
    
    public static UserResponse from(User user) {
        return UserResponse.builder()
            .id(user.getId())
            .firstName(user.getFirstName())
            .lastName(user.getLastName())
            .email(user.getEmail())
            .phone(user.getPhone())
            .createdAt(user.getCreatedAt())
            .updatedAt(user.getUpdatedAt())
            .build();
    }
}

@Data
@AllArgsConstructor
public class PagedResponse<T> {
    private List<T> content;
    private long totalElements;
    private int totalPages;
    private int currentPage;
    private int pageSize;
    
    public PagedResponse(List<T> content, long totalElements, int totalPages) {
        this.content = content;
        this.totalElements = totalElements;
        this.totalPages = totalPages;
    }
}

GraphQL Uygulaması

java
// GraphQL Schema Definition (schema.graphqls)
```graphql
type Query {
    user(id: ID!): User
    users(first: Int, after: String): UserConnection
    post(id: ID!): Post
    posts(first: Int, after: String, userId: ID): PostConnection
}

type Mutation {
    createUser(input: CreateUserInput!): CreateUserPayload
    updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload
    deleteUser(id: ID!): DeleteUserPayload
}

type Subscription {
    userUpdated(id: ID!): User
    postCreated(userId: ID!): Post
}

type User {
    id: ID!
    firstName: String!
    lastName: String!
    email: String!
    phone: String
    posts(first: Int, after: String): PostConnection
    createdAt: String!
    updatedAt: String!
}

type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    createdAt: String!
    updatedAt: String!
}

type UserConnection {
    edges: [UserEdge!]!
    pageInfo: PageInfo!
}

type UserEdge {
    node: User!
    cursor: String!
}

type PostConnection {
    edges: [PostEdge!]!
    pageInfo: PageInfo!
}

type PostEdge {
    node: Post!
    cursor: String!
}

type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: String
    endCursor: String
}

input CreateUserInput {
    firstName: String!
    lastName: String!
    email: String!
    phone: String
}

input UpdateUserInput {
    firstName: String
    lastName: String
    email: String
    phone: String
}

type CreateUserPayload {
    user: User
    errors: [Error!]
}

type UpdateUserPayload {
    user: User
    errors: [Error!]
}

type DeleteUserPayload {
    success: Boolean!
    errors: [Error!]
}

type Error {
    field: String
    message: String!
}
java
@Component
@Slf4j
public class UserGraphQLResolver implements GraphQLQueryResolver, GraphQLMutationResolver {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private PostService postService;
    
    // Query resolvers
    public User user(Long id) {
        return userService.findById(id);
    }
    
    public UserConnection users(Integer first, String after) {
        int limit = first != null ? first : 20;
        int offset = after != null ? decodePageCursor(after) : 0;
        
        Pageable pageable = PageRequest.of(offset / limit, limit);
        Page<User> userPage = userService.findAll(pageable);
        
        return UserConnection.from(userPage);
    }
    
    public Post post(Long id) {
        return postService.findById(id);
    }
    
    public PostConnection posts(Integer first, String after, Long userId) {
        int limit = first != null ? first : 20;
        int offset = after != null ? decodePageCursor(after) : 0;
        
        Pageable pageable = PageRequest.of(offset / limit, limit);
        Page<Post> postPage = userId != null ? 
            postService.findByUserId(userId, pageable) : 
            postService.findAll(pageable);
        
        return PostConnection.from(postPage);
    }
    
    // Mutation resolvers
    public CreateUserPayload createUser(CreateUserInput input) {
        try {
            User user = userService.create(input);
            return CreateUserPayload.success(user);
        } catch (ValidationException e) {
            return CreateUserPayload.withErrors(e.getErrors());
        }
    }
    
    public UpdateUserPayload updateUser(Long id, UpdateUserInput input) {
        try {
            User user = userService.update(id, input);
            return UpdateUserPayload.success(user);
        } catch (ValidationException e) {
            return UpdateUserPayload.withErrors(e.getErrors());
        }
    }
    
    public DeleteUserPayload deleteUser(Long id) {
        try {
            userService.delete(id);
            return DeleteUserPayload.success();
        } catch (Exception e) {
            return DeleteUserPayload.withError("Failed to delete user: " + e.getMessage());
        }
    }
    
    // Field resolvers for User type
    public PostConnection posts(User user, Integer first, String after) {
        int limit = first != null ? first : 20;
        int offset = after != null ? decodePageCursor(after) : 0;
        
        Pageable pageable = PageRequest.of(offset / limit, limit);
        Page<Post> postPage = postService.findByUserId(user.getId(), pageable);
        
        return PostConnection.from(postPage);
    }
    
    // Field resolvers for Post type
    public User author(Post post) {
        return userService.findById(post.getUserId());
    }
    
    private int decodePageCursor(String cursor) {
        try {
            return Integer.parseInt(new String(Base64.getDecoder().decode(cursor)));
        } catch (Exception e) {
            return 0;
        }
    }
}

@Component
public class UserSubscriptionResolver implements GraphQLSubscriptionResolver {
    
    @Autowired
    private ReactiveUserService reactiveUserService;
    
    public Publisher<User> userUpdated(Long id) {
        return reactiveUserService.getUserUpdatesStream(id);
    }
    
    public Publisher<Post> postCreated(Long userId) {
        return reactiveUserService.getPostCreatedStream(userId);
    }
}

// GraphQL Configuration
@Configuration
@EnableGraphQLWebSocket
public class GraphQLConfig {
    
    @Bean
    public GraphQLWebSocketConfigurer graphQLWebSocketConfigurer() {
        return new GraphQLWebSocketConfigurer() {
            @Override
            public void configure(GraphQLWebSocketConfigurationRegistry registry) {
                registry.getConfiguration("default")
                    .setKeepAliveInterval(Duration.ofSeconds(30))
                    .setInitTimeout(Duration.ofSeconds(60));
            }
        };
    }
    
    @Bean
    public DataFetcher<User> userDataFetcher(UserService userService) {
        return environment -> {
            Long id = Long.valueOf(environment.getArgument("id"));
            return userService.findById(id);
        };
    }
    
    @Bean
    public DataFetcher<CompletableFuture<User>> asyncUserDataFetcher(UserService userService) {
        return environment -> {
            Long id = Long.valueOf(environment.getArgument("id"));
            return CompletableFuture.supplyAsync(() -> userService.findById(id));
        };
    }
}

gRPC Uygulaması

protobuf
// user_service.proto
syntax = "proto3";

package com.example.grpc;

option java_multiple_files = true;
option java_package = "com.example.grpc";
option java_outer_classname = "UserServiceProto";

service UserService {
    rpc GetUser(GetUserRequest) returns (UserResponse);
    rpc GetUsers(GetUsersRequest) returns (stream UserResponse);
    rpc CreateUser(CreateUserRequest) returns (UserResponse);
    rpc UpdateUser(UpdateUserRequest) returns (UserResponse);
    rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
    rpc StreamUserUpdates(StreamUserUpdatesRequest) returns (stream UserResponse);
}

message GetUserRequest {
    int64 id = 1;
}

message GetUsersRequest {
    int32 page = 1;
    int32 size = 2;
    string filter = 3;
}

message CreateUserRequest {
    string first_name = 1;
    string last_name = 2;
    string email = 3;
    string phone = 4;
}

message UpdateUserRequest {
    int64 id = 1;
    string first_name = 2;
    string last_name = 3;
    string email = 4;
    string phone = 5;
}

message DeleteUserRequest {
    int64 id = 1;
}

message DeleteUserResponse {
    bool success = 1;
    string message = 2;
}

message UserResponse {
    int64 id = 1;
    string first_name = 2;
    string last_name = 3;
    string email = 4;
    string phone = 5;
    int64 created_at = 6;
    int64 updated_at = 7;
}

message StreamUserUpdatesRequest {
    int64 user_id = 1;
}
java
@GrpcService
@Slf4j
public class UserGrpcService extends UserServiceGrpc.UserServiceImplBase {
    
    @Autowired
    private UserService userService;
    
    @Override
    public void getUser(GetUserRequest request, StreamObserver<UserResponse> responseObserver) {
        try {
            User user = userService.findById(request.getId());
            UserResponse response = convertToGrpcResponse(user);
            
            responseObserver.onNext(response);
            responseObserver.onCompleted();
        } catch (Exception e) {
            log.error("Error getting user: {}", e.getMessage(), e);
            responseObserver.onError(Status.INTERNAL
                .withDescription("Failed to get user: " + e.getMessage())
                .asRuntimeException());
        }
    }
    
    @Override
    public void getUsers(GetUsersRequest request, StreamObserver<UserResponse> responseObserver) {
        try {
            Pageable pageable = PageRequest.of(request.getPage(), request.getSize());
            Page<User> users = userService.findAll(pageable);
            
            users.getContent().forEach(user -> {
                UserResponse response = convertToGrpcResponse(user);
                responseObserver.onNext(response);
            });
            
            responseObserver.onCompleted();
        } catch (Exception e) {
            log.error("Error getting users: {}", e.getMessage(), e);
            responseObserver.onError(Status.INTERNAL
                .withDescription("Failed to get users: " + e.getMessage())
                .asRuntimeException());
        }
    }
    
    @Override
    public void createUser(CreateUserRequest request, StreamObserver<UserResponse> responseObserver) {
        try {
            User user = User.builder()
                .firstName(request.getFirstName())
                .lastName(request.getLastName())
                .email(request.getEmail())
                .phone(request.getPhone())
                .build();
            
            User createdUser = userService.create(user);
            UserResponse response = convertToGrpcResponse(createdUser);
            
            responseObserver.onNext(response);
            responseObserver.onCompleted();
        } catch (ValidationException e) {
            responseObserver.onError(Status.INVALID_ARGUMENT
                .withDescription("Validation failed: " + e.getMessage())
                .asRuntimeException());
        } catch (Exception e) {
            log.error("Error creating user: {}", e.getMessage(), e);
            responseObserver.onError(Status.INTERNAL
                .withDescription("Failed to create user: " + e.getMessage())
                .asRuntimeException());
        }
    }
    
    @Override
    public void updateUser(UpdateUserRequest request, StreamObserver<UserResponse> responseObserver) {
        try {
            User existingUser = userService.findById(request.getId());
            
            User.UserBuilder builder = existingUser.toBuilder();
            if (!request.getFirstName().isEmpty()) {
                builder.firstName(request.getFirstName());
            }
            if (!request.getLastName().isEmpty()) {
                builder.lastName(request.getLastName());
            }
            if (!request.getEmail().isEmpty()) {
                builder.email(request.getEmail());
            }
            if (!request.getPhone().isEmpty()) {
                builder.phone(request.getPhone());
            }
            
            User updatedUser = userService.update(builder.build());
            UserResponse response = convertToGrpcResponse(updatedUser);
            
            responseObserver.onNext(response);
            responseObserver.onCompleted();
        } catch (UserNotFoundException e) {
            responseObserver.onError(Status.NOT_FOUND
                .withDescription("User not found: " + e.getMessage())
                .asRuntimeException());
        } catch (Exception e) {
            log.error("Error updating user: {}", e.getMessage(), e);
            responseObserver.onError(Status.INTERNAL
                .withDescription("Failed to update user: " + e.getMessage())
                .asRuntimeException());
        }
    }
    
    @Override
    public void deleteUser(DeleteUserRequest request, StreamObserver<DeleteUserResponse> responseObserver) {
        try {
            userService.delete(request.getId());
            
            DeleteUserResponse response = DeleteUserResponse.newBuilder()
                .setSuccess(true)
                .setMessage("User deleted successfully")
                .build();
                
            responseObserver.onNext(response);
            responseObserver.onCompleted();
        } catch (UserNotFoundException e) {
            responseObserver.onError(Status.NOT_FOUND
                .withDescription("User not found: " + e.getMessage())
                .asRuntimeException());
        } catch (Exception e) {
            log.error("Error deleting user: {}", e.getMessage(), e);
            responseObserver.onError(Status.INTERNAL
                .withDescription("Failed to delete user: " + e.getMessage())
                .asRuntimeException());
        }
    }
    
    @Override
    public void streamUserUpdates(StreamUserUpdatesRequest request, 
                                 StreamObserver<UserResponse> responseObserver) {
        try {
            // Subscribe to user updates
            userService.subscribeToUserUpdates(request.getUserId())
                .subscribe(
                    user -> {
                        UserResponse response = convertToGrpcResponse(user);
                        responseObserver.onNext(response);
                    },
                    error -> {
                        log.error("Error in user updates stream: {}", error.getMessage(), error);
                        responseObserver.onError(Status.INTERNAL
                            .withDescription("Stream error: " + error.getMessage())
                            .asRuntimeException());
                    },
                    () -> responseObserver.onCompleted()
                );
        } catch (Exception e) {
            log.error("Error setting up user updates stream: {}", e.getMessage(), e);
            responseObserver.onError(Status.INTERNAL
                .withDescription("Failed to setup stream: " + e.getMessage())
                .asRuntimeException());
        }
    }
    
    private UserResponse convertToGrpcResponse(User user) {
        return UserResponse.newBuilder()
            .setId(user.getId())
            .setFirstName(user.getFirstName())
            .setLastName(user.getLastName())
            .setEmail(user.getEmail())
            .setPhone(user.getPhone() != null ? user.getPhone() : "")
            .setCreatedAt(user.getCreatedAt().getEpochSecond())
            .setUpdatedAt(user.getUpdatedAt().getEpochSecond())
            .build();
    }
}

// gRPC Configuration
@Configuration
public class GrpcConfig {
    
    @Bean
    public NettyChannelBuilder grpcChannelBuilder() {
        return NettyChannelBuilder.forAddress("localhost", 9090)
            .keepAliveTime(30, TimeUnit.SECONDS)
            .keepAliveTimeout(5, TimeUnit.SECONDS)
            .keepAliveWithoutCalls(true)
            .maxInboundMessageSize(1024 * 1024) // 1MB
            .usePlaintext();
    }
    
    @Bean
    public ServerInterceptor authenticationInterceptor() {
        return new ServerInterceptor() {
            @Override
            public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
                    ServerCall<ReqT, RespT> call,
                    Metadata headers,
                    ServerCallHandler<ReqT, RespT> next) {
                
                String authToken = headers.get(Metadata.Key.of("authorization", ASCII_STRING_MARSHALLER));
                
                if (authToken == null || !authToken.startsWith("Bearer ")) {
                    call.close(Status.UNAUTHENTICATED.withDescription("Missing or invalid token"), headers);
                    return new ServerCall.Listener<ReqT>() {};
                }
                
                // Validate token
                try {
                    validateToken(authToken.substring(7));
                } catch (Exception e) {
                    call.close(Status.UNAUTHENTICATED.withDescription("Invalid token"), headers);
                    return new ServerCall.Listener<ReqT>() {};
                }
                
                return next.startCall(call, headers);
            }
        };
    }
    
    private void validateToken(String token) throws Exception {
        // Token validation logic
    }
}

Bu kapsamlı uygulama, REST, GraphQL ve gRPC'nin Spring Boot ekosisteminde nasıl uygulanabileceğini ve her birinin güçlü yanlarını gösterir.

Eren Demir tarafından oluşturulmuştur.