gRPC를 사용하다 보면 다양한 상황에서 에러와 예외가 발생할 수 있다.

예를 들어, 잘못된 요청, 인증 실패, 서버 내부 오류, 타임아웃 등 다양한 이슈가 존재한다.

 

gRPC에서는 이를 처리하기 위해 고유한 상태 코드(Status Code) 체계를 사용하며, Java나 다른 언어에서는 이 상태 코드와 함께 예외(Exception)를 활용한 에러 핸들링이 가능하다.

 

📌 gRPC 에러 유형


gRPC는 클라이언트-서버 통신 중 발생할 수 있는 다양한 에러 상황을 Status Code로 정의한다. 이러한 상태 코드는 gRPC 내부에서 사용되며, 클라이언트와 서버 간에 명확한 오류 전달을 가능하게 해준다.

 

대표적인 gRPC 에러 상태 코드

상태 코드 설명
CANCELLED 작업이 클라이언트에 의해 취소되었습니다.
UNKNOWN 알 수 없는 에러가 발생했습니다. 이는 예상치 못한 조건이 발생했음을 의미합니다.
INVALID_ARGUMENT 클라이언트가 잘못된 인수가 제공했습니다.
DEADLINE_EXCEEDED 작업이 지정된 마감 시간을 초과했습니다.
NOT_FOUND 지정된 리소스를 찾을 수 없습니다.
ALREADY_EXISTS 생성하려는 리소스가 이미 존재합니다.
PERMISSION_DENIED 클라이언트가 리소스에 대한 액세스 권한이 없습니다.
RESOURCE_EXHAUSTED 리소스 할당량이 소진되었습니다.
FAILED_PRECONDITION 시스템의 상태가 작업을 실행하기에 적합하지 않습니다.
ABORTED 동시성 충돌 등으로 인해 작업이 중단되었습니다.
OUT_OF_RANGE 작업이 허용된 범위를 벗어났습니다.
UNIMPLEMENTED 요청된 작업이 서버에서 구현되지 않았습니다.
INTERNAL 내부 에러가 발생했습니다.
UNAVAILABLE 서비스가 현재 사용 불가능합니다.
DATA_LOSS 데이터 손실이 발생했습니다.
UNAUTHENTICATED 요청이 인증되지 않았습니다.

 

 


📍 서버 측 에러 핸들링


서버 측에서는 StreamObserver.onError() 메서드를 통해 클라이언트에 에러를 전달할 수 있다. 이때 StatusRuntimeException을 사용하여 적절한 상태 코드와 메시지를 설정한다.

 

기본 예시
@Override
public void getUser(UserRequest request, StreamObserver<UserResponse> responseObserver) {
    if (request.getUserId() <= 0) {
        responseObserver.onError(
            Status.INVALID_ARGUMENT
                .withDescription("User ID must be greater than 0")
                .asRuntimeException()
        );
        return;
    }

    UserResponse response = UserResponse.newBuilder()
        .setUserId(request.getUserId())
        .setName("Alice")
        .setEmail("alice@example.com")
        .build();

    responseObserver.onNext(response);
    responseObserver.onCompleted();
}

 

예외 처리 및 로깅 적용
@Override
public void getUser(UserRequest request, StreamObserver<UserResponse> responseObserver) {
    try {
        if (request.getUserId() < 0) {
            throw new IllegalArgumentException("User ID is negative");
        }
        // 비즈니스 로직 수행
    } catch (IllegalArgumentException e) {
        log.warn("입력 오류: {}", e.getMessage());
        responseObserver.onError(Status.INVALID_ARGUMENT
            .withDescription(e.getMessage())
            .asRuntimeException());
    } catch (Exception e) {
        log.error("알 수 없는 서버 오류", e);
        responseObserver.onError(Status.INTERNAL
            .withDescription("Internal server error")
            .asRuntimeException());
    }
}

 

서버 에러 핸들링 전략

1️⃣ 명확하고 유용한 에러 메시지 제공

에러 메시지는 사용자나 개발자가 이해할 수 있도록 명확하게 작성한다.

Status.INVALID_ARGUMENT.withDescription("이메일 형식이 유효하지 않습니다").asRuntimeException()

 

2️⃣ 적절한 상태 코드 사용

Status.PERMISSION_DENIED.withDescription("관리자만 접근 가능한 리소스입니다").asRuntimeException()

 

3️⃣ 추가 정보 제공

Builder statusBuilder = Status.INVALID_ARGUMENT.withDescription(e.getMessage()).toStatus().toBUilder();
ErrorInfo errorInfo = ErrorInfo.newBuilder()
    .putMetadata("InvalidField", "username")
    .putMetadata("InvalidValue", request.getUsername())
    .build();
com.google.rpc.Status status = statusBuilder.addDetails(Any.pack(errorInfo)).build();
responseObserver.onError(StatusProto.toStatusRuntimeException(status));

 

4️⃣ 상태 세부정보 포함 (메타데이터를 사용한 추가 정보 제공)

커스텀 에러 정보를 Protobuf 메시지로 정의하여 에러에 포함시킬 수 있다.

Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of("error-code", Metadata.ASCII_STRING_MARSHALLER), "USR_001");
responseObserver.onError(Status.UNAUTHENTICATED.asRuntimeException());

 

 


📍 클라이언트 측 에러 핸들링


gRPC 클라이언트는 서버에서 반환된 에러를 StatusRuntimeException으로 처리한다.

 

클라이언트 예외 핸들링 예시
try {
    UserResponse response = stub.getUser(request);
    System.out.println("Name: " + response.getName());
} catch (StatusRuntimeException e) {
    Status status = Status.fromThrowable(e);
    switch (status.getCode()) {
        case INVALID_ARGUMENT:
            System.err.println("입력값이 잘못되었습니다: " + status.getDescription());
            break;
        case UNAUTHENTICATED:
            System.err.println("인증 실패: 다시 로그인 해주세요");
            break;
        case INTERNAL:
            System.err.println("서버 오류 발생: 관리자에게 문의하세요");
            break;
        default:
            System.err.println("알 수 없는 오류: " + status);
    }
}

 

클라이언트 에러 핸들링 전략

재시도 및 지수 백오프 전략

서버가 UNAVAILABLE 상태일 때는 재시도하며, 점진적으로 대기시간을 증가시키는 것이 좋다.

for (int i = 0; i < 3; i++) {
    try {
        return stub.getUser(request);
    } catch (StatusRuntimeException e) {
        if (e.getStatus().getCode() == Status.Code.UNAVAILABLE) {
            Thread.sleep((long) Math.pow(2, i) * 1000);
        } else {
            throw e;
        }
    }
}
  • grpc-retry 라이브러리를 사용하면 자동 재시도 정책도 구성 가능하다.

 


gRPC 에러 핸들링의 중요성


  • API 사용성 향상: 클라이언트가 어떤 상황인지 명확히 이해 가능
  • 문제 진단의 용이성: 로그 및 상태 코드 기반 분석 용이
  • 예측 가능한 에러 처리: 일관된 정책으로 처리 가능
  • 중단 없는 서비스 제공: 재시도 및 백오프 전략 도입 시 효과적
  • 안전한 에러 정보 공개: 민감한 정보는 숨기고 필요한 메시지만 전달
  • 효율적인 네트워크 사용: 무분별한 재요청 방지
  • 사용자 경험 향상: 친절한 메시지 제공으로 UX 향상
  • 로깅 및 모니터링 강화: 알림 및 알림 시스템과 연계 기능
  • 확장성 및 유연성: 마이크로서비스에서도 통일된 에러 구조 유지 가능