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 향상
- 로깅 및 모니터링 강화: 알림 및 알림 시스템과 연계 기능
- 확장성 및 유연성: 마이크로서비스에서도 통일된 에러 구조 유지 가능
'기술(Tech) > Network & System' 카테고리의 다른 글
[GraphQL] GraphQL 개요 (0) | 2025.03.15 |
---|---|
[gRPC] gRPC의 보안 계층 🔒 (0) | 2025.03.15 |
[gRPC] gRPC 상태 코드 (0) | 2025.03.03 |
[gRPC] RESTful API와 gRPC 간 변환 방법 (with. gRPC-Web) (1) | 2025.03.02 |
[gRPC] gRPC 서비스 요청 방식과 내부 동작 원리 (0) | 2025.02.23 |
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 향상
- 로깅 및 모니터링 강화: 알림 및 알림 시스템과 연계 기능
- 확장성 및 유연성: 마이크로서비스에서도 통일된 에러 구조 유지 가능
'기술(Tech) > Network & System' 카테고리의 다른 글
[GraphQL] GraphQL 개요 (0) | 2025.03.15 |
---|---|
[gRPC] gRPC의 보안 계층 🔒 (0) | 2025.03.15 |
[gRPC] gRPC 상태 코드 (0) | 2025.03.03 |
[gRPC] RESTful API와 gRPC 간 변환 방법 (with. gRPC-Web) (1) | 2025.03.02 |
[gRPC] gRPC 서비스 요청 방식과 내부 동작 원리 (0) | 2025.02.23 |