Error Handling#
gRPC는 분산 시스템에서 서비스 간 통신을 위한 강력한 프레임워크이다. 복잡한 분산 환경에서는 네트워크 문제, 서버 장애, 비즈니스 로직 오류 등 다양한 오류 상황이 발생할 수 있다. gRPC는 이러한 다양한 오류 상황을 일관되고 체계적으로 처리하기 위한 포괄적인 오류 처리 메커니즘을 제공한다.
gRPC의 오류 처리는 HTTP/2 상태 코드, gRPC 상태 코드, 그리고 상세 오류 메시지를 조합하여 클라이언트에게 오류 정보를 전달한다. 이 시스템은 모든 지원 언어에서 일관되게 작동하며, 개발자가 다양한 오류 상황에 적절히 대응할 수 있도록 돕는다.
gRPC 상태 코드(Status Codes)#
gRPC 오류 처리의 핵심은 상태 코드 시스템이다. 각 gRPC 호출은 성공 또는 실패를 나타내는 상태 코드와 함께 완료된다. 이 상태 코드는 오류의 유형과 심각도를 나타낸다.
주요 gRPC 상태 코드#
gRPC는 다음과 같은 주요 상태 코드를 정의한다:
상태 코드 | 값 | 설명 |
---|
OK | 0 | 성공적인 호출 |
CANCELLED | 1 | 클라이언트에 의해 작업이 취소됨 |
UNKNOWN | 2 | 알 수 없는 오류가 발생함 |
INVALID_ARGUMENT | 3 | 클라이언트가 잘못된 인수를 제공함 |
DEADLINE_EXCEEDED | 4 | 작업 완료 전에 데드라인이 만료됨 |
NOT_FOUND | 5 | 요청된 엔티티를 찾을 수 없음 |
ALREADY_EXISTS | 6 | 클라이언트가 생성하려는 엔티티가 이미 존재함 |
PERMISSION_DENIED | 7 | 클라이언트가 작업을 수행할 권한이 없음 |
RESOURCE_EXHAUSTED | 8 | 리소스가 소진됨(할당량 초과 등) |
FAILED_PRECONDITION | 9 | 시스템이 작업을 수행할 상태가 아님 |
ABORTED | 10 | 작업이 중단됨(일반적으로 동시성 문제로 인해) |
OUT_OF_RANGE | 11 | 작업이 유효 범위를 벗어남 |
UNIMPLEMENTED | 12 | 요청된 작업이 구현되지 않음 |
INTERNAL | 13 | 내부 서버 오류 |
UNAVAILABLE | 14 | 서비스를 일시적으로 사용할 수 없음 |
DATA_LOSS | 15 | 복구할 수 없는 데이터 손실이 발생함 |
UNAUTHENTICATED | 16 | 클라이언트가 인증되지 않음 |
상태 코드 선택 가이드라인#
올바른 상태 코드를 선택하는 것은 클라이언트가 오류에 적절히 대응하는 데 중요하다.
다음은 상태 코드 선택에 대한 가이드라인이다:
- 클라이언트 오류 (요청이 잘못된 경우):
INVALID_ARGUMENT
: 요청 매개변수가 잘못된 경우NOT_FOUND
: 요청된 리소스가 존재하지 않는 경우ALREADY_EXISTS
: 리소스 생성 시도시 이미 존재하는 경우PERMISSION_DENIED
: 권한 부족UNAUTHENTICATED
: 인증 실패
- 서버 오류 (서버 측 문제):
INTERNAL
: 일반적인 서버 오류UNAVAILABLE
: 일시적인 서비스 중단DATA_LOSS
: 데이터 손실이나 손상
- 네트워크/타이밍 오류:
DEADLINE_EXCEEDED
: 타임아웃CANCELLED
: 클라이언트에 의해 취소됨ABORTED
: 동시성 충돌
- 프로그래밍 오류:
UNIMPLEMENTED
: 요청된 기능이 구현되지 않음FAILED_PRECONDITION
: 시스템 상태가 작업을 수행할 수 없음
오류 세부 정보(Error Details)#
gRPC는 기본 상태 코드 외에도 더 상세한 오류 정보를 제공할 수 있는 메커니즘을 제공한다.
오류 메시지#
각 gRPC 상태 코드는 문자열 메시지를 포함할 수 있다. 이 메시지는 디버깅 목적으로 사용되며, 최종 사용자에게 직접 표시하도록 설계되지 않았다.
1
2
| // 서버 측 구현 예시 (Go)
return nil, status.Errorf(codes.InvalidArgument, "필드 값 %s이(가) 유효하지 않습니다", value)
|
오류 상세 정보 (Error Details)#
gRPC는 google.rpc.Status
메시지를 확장하여 더 구조화된 오류 정보를 제공할 수 있다. 이를 통해 클라이언트에 다음과 같은 추가 정보를 전달할 수 있다:
- BadRequest: 잘못된 요청의 필드별 오류 정보
- RetryInfo: 재시도 지연 정보
- DebugInfo: 디버깅을 위한 상세 정보
- QuotaFailure: 할당량 위반 정보
- PreconditionFailure: 전제 조건 실패 세부 정보
- ResourceInfo: 리소스 관련 정보
- Help: 도움말 정보와 링크
- LocalizedMessage: 지역화된 오류 메시지
이러한 상세 정보는 google.rpc.ErrorInfo
프로토콜 버퍼 메시지를 사용하여 정의된다.
1
2
3
4
5
6
7
8
| // 오류 상세 정보 예시 (proto 정의)
message BadRequest {
message FieldViolation {
string field = 1;
string description = 2;
}
repeated FieldViolation field_violations = 1;
}
|
구현 예시#
Java에서 오류 상세 정보를 포함한 상태 생성 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 필드 유효성 검사 오류에 대한 상세 정보 추가
Metadata.Key<BadRequest> badRequestKey =
ProtoUtils.keyForProto(BadRequest.getDefaultInstance());
Metadata trailers = new Metadata();
BadRequest badRequest = BadRequest.newBuilder()
.addFieldViolations(FieldViolation.newBuilder()
.setField("email")
.setDescription("올바른 이메일 형식이 아닙니다")
.build())
.build();
trailers.put(badRequestKey, badRequest);
return Status.INVALID_ARGUMENT
.withDescription("요청에 유효하지 않은 필드가 있습니다")
.asRuntimeException(trailers);
|
언어별 오류 처리 구현#
gRPC는 다양한 프로그래밍 언어를 지원하며, 각 언어별로 오류 처리 메커니즘을 제공한다.
Go 언어에서의 오류 처리#
Go 언어에서 gRPC 오류 처리는 status
패키지를 사용한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// 서버 측에서 오류 반환
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
if req.Id <= 0 {
return nil, status.Errorf(codes.InvalidArgument, "사용자 ID는 양수여야 합니다: %v", req.Id)
}
user, err := s.repository.FindUser(req.Id)
if err != nil {
return nil, status.Errorf(codes.Internal, "내부 오류: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "ID가 %d인 사용자를 찾을 수 없습니다", req.Id)
}
return user, nil
}
// 클라이언트 측에서 오류 처리
resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: -1})
if err != nil {
st, ok := status.FromError(err)
if ok {
switch st.Code() {
case codes.InvalidArgument:
log.Printf("잘못된 인수: %v", st.Message())
case codes.NotFound:
log.Printf("사용자를 찾을 수 없음: %v", st.Message())
default:
log.Printf("gRPC 오류: %v", st)
}
} else {
log.Printf("일반 오류: %v", err)
}
return
}
|
Java 언어에서의 오류 처리#
Java에서는 Status
와 StatusRuntimeException
을 사용한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| // 서버 측에서 오류 반환
@Override
public void getUser(GetUserRequest request, StreamObserver<User> responseObserver) {
try {
if (request.getId() <= 0) {
responseObserver.onError(
Status.INVALID_ARGUMENT
.withDescription("사용자 ID는 양수여야 합니다: " + request.getId())
.asRuntimeException());
return;
}
User user = repository.findUser(request.getId());
if (user == null) {
responseObserver.onError(
Status.NOT_FOUND
.withDescription("ID가 " + request.getId() + "인 사용자를 찾을 수 없습니다")
.asRuntimeException());
return;
}
responseObserver.onNext(user);
responseObserver.onCompleted();
} catch (Exception e) {
responseObserver.onError(
Status.INTERNAL
.withDescription("내부 오류: " + e.getMessage())
.withCause(e) // 원인 예외 추가 (전송되지 않음, 로깅용)
.asRuntimeException());
}
}
// 클라이언트 측에서 오류 처리
try {
User user = blockingStub.getUser(GetUserRequest.newBuilder().setId(-1).build());
// 성공적인 응답 처리
} catch (StatusRuntimeException e) {
Status status = e.getStatus();
switch (status.getCode()) {
case INVALID_ARGUMENT:
logger.log(Level.WARNING, "잘못된 인수: {0}", status.getDescription());
break;
case NOT_FOUND:
logger.log(Level.INFO, "사용자를 찾을 수 없음: {0}", status.getDescription());
break;
default:
logger.log(Level.SEVERE, "gRPC 오류: {0}", status);
}
}
|
Python 언어에서의 오류 처리#
Python에서는 grpc.StatusCode
와 예외를 사용한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| # 서버 측에서 오류 반환
def GetUser(self, request, context):
if request.id <= 0:
context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
context.set_details(f"사용자 ID는 양수여야 합니다: {request.id}")
return pb.User() # 더미 응답
try:
user = self.repository.find_user(request.id)
if not user:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details(f"ID가 {request.id}인 사용자를 찾을 수 없습니다")
return pb.User() # 더미 응답
return user
except Exception as e:
context.set_code(grpc.StatusCode.INTERNAL)
context.set_details(f"내부 오류: {str(e)}")
return pb.User() # 더미 응답
# 클라이언트 측에서 오류 처리
try:
response = stub.GetUser(pb.GetUserRequest(id=-1))
# 성공적인 응답 처리
except grpc.RpcError as e:
status_code = e.code()
if status_code == grpc.StatusCode.INVALID_ARGUMENT:
print(f"잘못된 인수: {e.details()}")
elif status_code == grpc.StatusCode.NOT_FOUND:
print(f"사용자를 찾을 수 없음: {e.details()}")
else:
print(f"gRPC 오류: {status_code} - {e.details()}")
|
고급 오류 처리 패턴#
실제 프로덕션 환경에서는 더 복잡한 오류 처리 패턴이 필요하다.
다음은 gRPC 애플리케이션에서 사용할 수 있는 고급 오류 처리 패턴이다.
오류 인터셉터(Error Interceptors)#
인터셉터를 사용하면 오류 처리 로직을 중앙화하고 일관성 있게 적용할 수 있다.
Java 서버 측 인터셉터 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| public class ErrorInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
ServerCall.Listener<ReqT> listener = next.startCall(call, headers);
return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(listener) {
@Override
public void onHalfClose() {
try {
super.onHalfClose();
} catch (RuntimeException ex) {
handleException(call, ex);
throw ex;
}
}
private void handleException(ServerCall<ReqT, RespT> call, RuntimeException ex) {
Status status;
if (ex instanceof IllegalArgumentException) {
status = Status.INVALID_ARGUMENT.withDescription(ex.getMessage());
} else if (ex instanceof ResourceNotFoundException) {
status = Status.NOT_FOUND.withDescription(ex.getMessage());
} else {
status = Status.INTERNAL.withDescription("내부 서버 오류");
}
call.close(status, new Metadata());
}
};
}
}
|
오류 매핑(Error Mapping)#
비즈니스 로직 예외를 적절한 gRPC 상태 코드로 매핑하는 전략은 모든 gRPC 서비스에서 중요하다.
Go 언어에서의 오류 매핑 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // 오류 매핑 함수
func mapToStatusError(err error) error {
switch {
case errors.Is(err, ErrNotFound):
return status.Error(codes.NotFound, err.Error())
case errors.Is(err, ErrInvalidInput):
return status.Error(codes.InvalidArgument, err.Error())
case errors.Is(err, ErrPermissionDenied):
return status.Error(codes.PermissionDenied, err.Error())
case errors.Is(err, ErrUnauthenticated):
return status.Error(codes.Unauthenticated, err.Error())
case errors.Is(err, ErrAlreadyExists):
return status.Error(codes.AlreadyExists, err.Error())
default:
// 내부 오류는 자세한 정보를 노출하지 않음
return status.Error(codes.Internal, "내부 서버 오류가 발생했습니다")
}
}
// 서비스 구현에서 사용
func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
user, err := s.service.CreateUser(req.Name, req.Email)
if err != nil {
return nil, mapToStatusError(err)
}
return user, nil
}
|
오류 처리 모범 사례#
gRPC 애플리케이션에서 오류를 처리할 때 다음 모범 사례를 고려한다:
- 일관된 오류 코드 사용: 애플리케이션 전체에서 오류 코드를 일관되게 사용한다.
- 적절한 상태 코드 선택: 오류 상황에 가장 적합한 상태 코드를 선택한다.
- 유용한 오류 메시지 제공: 디버깅에 도움이 되는 명확한 오류 메시지를 제공한다.
- 민감한 정보 노출 방지: 오류 메시지에 민감한 정보가 포함되지 않도록 주의한다.
- 구조화된 오류 상세 정보 사용: 가능한 경우 구조화된 오류 세부 정보를 사용하여 클라이언트에게 더 많은 컨텍스트를 제공한다.
- 오류 처리 중앙화: 인터셉터나 미들웨어를 사용하여 오류 처리 로직을 중앙화한다.
- 재시도 가능한 오류와 재시도 불가능한 오류 구분: 클라이언트가 적절한 재시도 전략을 선택할 수 있도록 오류를 명확히 구분한다.
오류 전파와 처리 전략#
분산 시스템에서는 오류가 서비스 체인을 통해 전파되는 방식이 중요하다.
오류 전파 패턴#
분산 시스템에서 오류를 처리하는 몇 가지 일반적인 패턴이 있다:
직접 전파(Direct Propagation): 하위 서비스의 오류를 상위 서비스로 그대로 전달한다.
1
2
3
4
| resp, err := downstreamService.Call(ctx, req)
if err != nil {
return nil, err // 오류를 그대로 상위 서비스로 전달
}
|
오류 변환(Error Translation): 하위 서비스의 오류를 상위 서비스 컨텍스트에 맞게 변환한다.
1
2
3
4
5
6
7
8
| resp, err := downstreamService.Call(ctx, req)
if err != nil {
st, ok := status.FromError(err)
if ok && st.Code() == codes.NotFound {
return nil, status.Errorf(codes.NotFound, "사용자 프로필을 찾을 수 없습니다: %v", st.Message())
}
return nil, status.Errorf(codes.Internal, "프로필 서비스 오류")
}
|
오류 래핑(Error Wrapping): 오류 컨텍스트를 추가하면서 원래 오류를 유지한다.
1
2
3
4
| resp, err := downstreamService.Call(ctx, req)
if err != nil {
return nil, fmt.Errorf("프로필 서비스 호출 중 오류: %w", err)
}
|
오류 흡수(Error Absorption): 특정 오류를 처리하고 대체 로직을 실행한다.
1
2
3
4
5
6
7
8
9
| resp, err := downstreamService.Call(ctx, req)
if err != nil {
st, ok := status.FromError(err)
if ok && st.Code() == codes.NotFound {
// 대체 로직: 기본 프로필 반환
return getDefaultProfile(), nil
}
return nil, err
}
|
클라이언트 측 오류 처리 전략#
gRPC 클라이언트는 다양한 오류 상황에 효과적으로 대응하기 위한 전략이 필요하다.
재시도 전략#
일시적인 오류가 발생할 경우 자동 재시도를 구현할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // Java에서의 재시도 예시
public Response callWithRetry(Request request) {
int maxRetries = 3;
int retryCount = 0;
long retryDelayMs = 100;
while (true) {
try {
return stub.serviceMethod(request);
} catch (StatusRuntimeException e) {
retryCount++;
if (retryCount > maxRetries || !isRetryable(e.getStatus().getCode())) {
throw e; // 최대 재시도 횟수 초과 또는 재시도 불가능한 오류
}
logger.warning("재시도 가능한 오류 발생, 재시도 중… (" + retryCount + "/" + maxRetries + ")");
try {
Thread.sleep(retryDelayMs * (1 << (retryCount - 1))); // 지수 백오프
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw e;
}
}
}
}
private boolean isRetryable(Status.Code code) {
return code == Status.Code.UNAVAILABLE ||
code == Status.Code.RESOURCE_EXHAUSTED ||
code == Status.Code.DEADLINE_EXCEEDED;
}
|
서킷 브레이커(Circuit Breaker) 패턴#
연속적인 오류가 발생할 경우 서비스 호출을 일시적으로 중단하는 서킷 브레이커 패턴을 구현할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| // 간단한 서킷 브레이커 구현 예시
public class CircuitBreaker {
private final int failureThreshold;
private final long resetTimeout;
private int failureCount;
private long lastFailureTime;
private State state;
public enum State {
CLOSED, // 정상 작동
OPEN, // 서킷 열림 (호출 중단)
HALF_OPEN // 시험 작동
}
public CircuitBreaker(int failureThreshold, long resetTimeoutMs) {
this.failureThreshold = failureThreshold;
this.resetTimeout = resetTimeoutMs;
this.state = State.CLOSED;
this.failureCount = 0;
}
public synchronized <T> T execute(Supplier<T> operation) throws Exception {
if (state == State.OPEN) {
if (System.currentTimeMillis() - lastFailureTime > resetTimeout) {
state = State.HALF_OPEN;
} else {
throw new CircuitBreakerOpenException("서킷 브레이커가 열려 있습니다");
}
}
try {
T result = operation.get();
if (state == State.HALF_OPEN) {
reset(); // 성공 시 상태 재설정
}
return result;
} catch (Exception e) {
recordFailure(e);
throw e;
}
}
private synchronized void recordFailure(Exception e) {
failureCount++;
lastFailureTime = System.currentTimeMillis();
if (state == State.HALF_OPEN || failureCount >= failureThreshold) {
state = State.OPEN;
}
}
private synchronized void reset() {
failureCount = 0;
state = State.CLOSED;
}
}
|
타임아웃 설정#
모든 gRPC 호출에 적절한 타임아웃을 설정하는 것이 중요하다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Java에서의 타임아웃 설정 예시
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
.usePlaintext()
.build();
// 클라이언트 스텁 생성 시 데드라인 설정
UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(channel)
.withDeadlineAfter(5, TimeUnit.SECONDS);
// 개별 호출에 대한 타임아웃 설정도 가능
try {
// 기본 클라이언트 스텁에서 메서드별 타임아웃 설정
User user = stub.withDeadlineAfter(1, TimeUnit.SECONDS)
.getUser(GetUserRequest.newBuilder().setId(123).build());
} catch (StatusRuntimeException e) {
if (e.getStatus().getCode() == Status.Code.DEADLINE_EXCEEDED) {
logger.severe("호출 타임아웃");
}
}
|
서버 측 오류 처리 전략#
서버 측에서도 오류를 효과적으로 처리하기 위한 전략이 필요하다.
요청 검증(Validation)#
요청을 처리하기 전에 철저히 검증하여 많은 오류를 조기에 방지할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
// 입력 검증
if req.Name == "" {
return nil, status.Error(codes.InvalidArgument, "이름은 필수 필드입니다")
}
if !isValidEmail(req.Email) {
return nil, status.Error(codes.InvalidArgument, "유효하지 않은 이메일 형식입니다")
}
// 비즈니스 로직 실행
user, err := s.service.CreateUser(req.Name, req.Email)
if err != nil {
return nil, mapToStatusError(err)
}
return user, nil
}
|
교착 상태 방지(Deadlock Prevention)#
gRPC 서버에서는 교착 상태를 방지하기 위해 모든 요청에 대해 적절한 타임아웃을 설정하는 것이 중요하다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| func (s *server) ProcessData(ctx context.Context, req *pb.ProcessDataRequest) (*pb.ProcessDataResponse, error) {
// 컨텍스트에서 데드라인 확인
deadline, ok := ctx.Deadline()
if !ok {
// 기본 타임아웃 설정
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
} else {
// 남은 처리 시간 계산
remaining := time.Until(deadline) - 100*time.Millisecond
if remaining <= 0 {
return nil, status.Error(codes.DeadlineExceeded, "데드라인이 너무 짧습니다")
}
}
// 하위 서비스 호출 시 컨텍스트 전파
result, err := s.dataProcessor.Process(ctx, req.Data)
if err != nil {
return nil, err
}
return &pb.ProcessDataResponse{Result: result}, nil
}
|
점진적 성능 저하(Graceful Degradation)#
서비스 일부에 문제가 발생하더라도 전체 기능을 유지하는 전략을 구현할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| public User getUser(GetUserRequest request) {
User user = new User();
// 기본 사용자 정보 조회
try {
UserInfo info = userInfoService.getUserInfo(request.getId());
user.setId(info.getId());
user.setName(info.getName());
user.setEmail(info.getEmail());
} catch (StatusRuntimeException e) {
logger.severe("사용자 기본 정보 조회 실패: " + e.getMessage());
if (e.getStatus().getCode() != Status.Code.NOT_FOUND) {
throw e; // NOT_FOUND가 아닌 경우에만 예외 전파
}
// NOT_FOUND인 경우, 기본값으로 계속 진행
user.setId(request.getId());
user.setName("Unknown");
}
// 사용자 프로필 조회 (선택적 기능)
try {
UserProfile profile = profileService.getProfile(request.getId());
user.setProfilePicture(profile.getPictureUrl());
user.setBio(profile.getBio());
} catch (StatusRuntimeException e) {
// 프로필 정보가 없어도 기본 사용자 정보는 반환
logger.warning("사용자 프로필 조회 실패, 기본값 사용: " + e.getMessage());
user.setProfilePicture("default.png");
}
return user;
}
|
메타데이터를 통한 오류 정보 전달#
gRPC는 메타데이터(Metadata)를 통해 추가적인 오류 정보를 전달할 수 있다.
gRPC는 응답 트레일러(trailers)를 통해 오류에 대한 추가 정보를 전달할 수 있다. 이는 특히 오류 상세 정보를 전달하는 데 유용하다.
Java 서버 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| @Override
public void processRequest(Request request, StreamObserver<Response> responseObserver) {
try {
// 비즈니스 로직 처리
Response response = processBusinessLogic(request);
responseObserver.onNext(response);
// 트레일러 메타데이터 추가
Metadata trailers = new Metadata();
Metadata.Key<String> processingTimeKey =
Metadata.Key.of("processing-time-ms", Metadata.ASCII_STRING_MARSHALLER);
trailers.put(processingTimeKey, String.valueOf(processingTime));
// 완료 시 트레일러 전송
((ServerCallStreamObserver<Response>) responseObserver).setTrailers(trailers);
responseObserver.onCompleted();
} catch (Exception e) {
// 오류 처리 및 트레일러 메타데이터 추가
Metadata trailers = new Metadata();
Metadata.Key<String> errorDetailsKey =
Metadata.Key.of("error-details", Metadata.ASCII_STRING_MARSHALLER);
trailers.put(errorDetailsKey, "오류 세부 정보: " + e.getMessage());
Status status = Status.INTERNAL
.withDescription("처리 중 오류 발생")
.withCause(e);
responseObserver.onError(status.asRuntimeException(trailers));
}
}
|
Java 클라이언트 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| ClientCall<Request, Response> call = channel.newCall(
ServiceGrpc.getProcessRequestMethod(), CallOptions.DEFAULT);
call.start(new ClientCall.Listener<Response>() {
@Override
public void onClose(Status status, Metadata trailers) {
if (!status.isOk()) {
String errorDetails = trailers.get(
Metadata.Key.of("error-details", Metadata.ASCII_STRING_MARSHALLER));
logger.severe("오류 발생: " + status + ", 세부 정보: " + errorDetails);
}
}
@Override
public void onMessage(Response response) {
// 응답 처리
}
}, new Metadata());
call.sendMessage(request);
call.halfClose();
call.request(1);
|
바이너리 메타데이터를 사용한 구조화된 오류 정보#
Protocol Buffers로 정의된 구조화된 오류 정보를 바이너리 메타데이터로 전송할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| // 서버 측 구현
public void processRequest(Request request, StreamObserver<Response> responseObserver) {
try {
// 비즈니스 로직 처리
} catch (ValidationException e) {
// 구조화된 오류 정보 생성
ErrorDetails errorDetails = ErrorDetails.newBuilder()
.setCode("VALIDATION_ERROR")
.setMessage(e.getMessage())
.addFields(FieldError.newBuilder()
.setField(e.getField())
.setDescription(e.getDescription())
.build())
.build();
// 바이너리 메타데이터로 변환
Metadata.Key<byte[]> errorDetailsKey =
Metadata.Key.of("error-details-bin", Metadata.BINARY_BYTE_MARSHALLER);
Metadata trailers = new Metadata();
trailers.put(errorDetailsKey, errorDetails.toByteArray());
// 오류 상태와 함께 트레일러 전송
responseObserver.onError(
Status.INVALID_ARGUMENT
.withDescription("유효성 검사 오류")
.asRuntimeException(trailers));
}
}
// 클라이언트 측 처리
try {
Response response = stub.processRequest(request);
// 성공적인 응답 처리
} catch (StatusRuntimeException e) {
if (e.getStatus().getCode() == Status.Code.INVALID_ARGUMENT) {
Metadata trailers = e.getTrailers();
if (trailers != null) {
Metadata.Key<byte[]> errorDetailsKey =
Metadata.Key.of("error-details-bin", Metadata.BINARY_BYTE_MARSHALLER);
byte[] errorDetailsBytes = trailers.get(errorDetailsKey);
if (errorDetailsBytes != null) {
try {
ErrorDetails errorDetails = ErrorDetails.parseFrom(errorDetailsBytes);
logger.severe("유효성 검사 오류: " + errorDetails.getMessage());
for (FieldError field : errorDetails.getFieldsList()) {
logger.severe("필드 오류: " + field.getField() + " - " + field.getDescription());
}
} catch (InvalidProtocolBufferException ipbe) {
logger.severe("오류 세부 정보 파싱 실패");
}
}
}
}
}
|
스트리밍 RPC에서의 오류 처리#
gRPC는 단일 요청/응답 외에도 스트리밍 RPC를 지원한다.
스트리밍 RPC에서의 오류 처리는 몇 가지 추가적인 고려 사항이 필요하다.
서버 스트리밍 RPC#
서버 스트리밍 RPC에서 서버는 여러 응답을 클라이언트에게 전송한다. 오류가 발생하면 스트림이 종료된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| // 서버 측 구현
@Override
public void listUsers(ListUsersRequest request, StreamObserver<User> responseObserver) {
try {
// 페이지네이션 파라미터 검증
if (request.getPageSize() <= 0) {
responseObserver.onError(
Status.INVALID_ARGUMENT
.withDescription("페이지 크기는 양수여야 합니다")
.asRuntimeException());
return;
}
// 사용자 목록 조회 및 스트리밍
List<User> users = userRepository.findUsers(request.getPageToken(), request.getPageSize());
for (User user : users) {
// 각 사용자를 스트림으로 전송
responseObserver.onNext(user);
// 컨텍스트 취소 여부 확인
if (Context.current().isCancelled()) {
logger.info("클라이언트가 스트림을 취소했습니다");
return;
}
}
responseObserver.onCompleted();
} catch (Exception e) {
logger.severe("사용자 목록 조회 중 오류: " + e.getMessage());
responseObserver.onError(
Status.INTERNAL
.withDescription("내부 서버 오류")
.asRuntimeException());
}
}
// 클라이언트 측 구현
ListUsersRequest request = ListUsersRequest.newBuilder()
.setPageSize(10)
.build();
Iterator<User> users = blockingStub.listUsers(request);
try {
while (users.hasNext()) {
User user = users.next();
// 사용자 처리
}
} catch (StatusRuntimeException e) {
if (e.getStatus().getCode() == Status.Code.INTERNAL) {
logger.severe("서버 내부 오류: " + e.getStatus().getDescription());
} else {
logger.severe("스트리밍 오류: " + e.getStatus());
}
}
|
클라이언트 스트리밍 RPC#
클라이언트 스트리밍 RPC에서는 클라이언트가 여러 요청을 서버에 전송한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
| // 서버 측 구현
@Override
public StreamObserver<User> createUsers(final StreamObserver<CreateUsersResponse> responseObserver) {
return new StreamObserver<User>() {
private int successCount = 0;
private final List<String> failedUsers = new ArrayList<>();
@Override
public void onNext(User user) {
try {
// 사용자 유효성 검사
if (user.getName().isEmpty()) {
failedUsers.add("ID: " + user.getId() + " - 이름은 필수 필드입니다");
return;
}
// 사용자 생성
userRepository.createUser(user);
successCount++;
} catch (Exception e) {
failedUsers.add("ID: " + user.getId() + " - " + e.getMessage());
}
}
@Override
public void onError(Throwable t) {
logger.severe("클라이언트 스트림 오류: " + t.getMessage());
Status status = Status.fromThrowable(t);
responseObserver.onError(status.asRuntimeException());
}
@Override
public void onCompleted() {
// 모든 결과 요약하여 응답
CreateUsersResponse response = CreateUsersResponse.newBuilder()
.setSuccessCount(successCount)
.addAllFailedUsers(failedUsers)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
};
}
// 클라이언트 측 구현
StreamObserver<CreateUsersResponse> responseObserver = new StreamObserver<CreateUsersResponse>() {
@Override
public void onNext(CreateUsersResponse response) {
logger.info("성공적으로 생성된 사용자 수: " + response.getSuccessCount());
for (String failure : response.getFailedUsersList()) {
logger.warning("사용자 생성 실패: " + failure);
}
}
@Override
public void onError(Throwable t) {
Status status = Status.fromThrowable(t);
logger.severe("스트림 오류: " + status);
}
@Override
public void onCompleted() {
logger.info("스트림 완료");
}
};
StreamObserver<User> requestObserver = asyncStub.createUsers(responseObserver);
try {
for (User user : users) {
requestObserver.onNext(user);
// 일시 중단을 요청받았는지 확인
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
}
requestObserver.onCompleted();
} catch (RuntimeException e) {
requestObserver.onError(e);
throw e;
} catch (InterruptedException e) {
requestObserver.onError(Status.CANCELLED.withDescription("취소됨").asRuntimeException());
Thread.currentThread().interrupt();
}
|
양방향 스트리밍 RPC#
양방향 스트리밍 RPC에서는 클라이언트와 서버 모두 스트림을 통해 메시지를 주고받는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
| // 서버 측 구현
@Override
public StreamObserver<ChatMessage> chat(final StreamObserver<ChatMessage> responseObserver) {
return new StreamObserver<ChatMessage>() {
@Override
public void onNext(ChatMessage message) {
try {
// 메시지 유효성 검사
if (message.getContent().isEmpty()) {
responseObserver.onError(
Status.INVALID_ARGUMENT
.withDescription("빈 메시지는 허용되지 않습니다")
.asRuntimeException());
return;
}
// 금지된 단어 검사
if (containsProfanity(message.getContent())) {
responseObserver.onError(
Status.PERMISSION_DENIED
.withDescription("부적절한 콘텐츠가 포함되어 있습니다")
.asRuntimeException());
return;
}
// 메시지 처리 및 응답
ChatMessage response = processAndCreateResponse(message);
responseObserver.onNext(response);
} catch (Exception e) {
logger.severe("채팅 메시지 처리 중 오류: " + e.getMessage());
responseObserver.onError(
Status.INTERNAL
.withDescription("메시지 처리 중 오류가 발생했습니다")
.asRuntimeException());
}
}
@Override
public void onError(Throwable t) {
logger.warning("클라이언트 오류: " + t.getMessage());
// 리소스 정리
}
@Override
public void onCompleted() {
logger.info("클라이언트가 채팅을 완료했습니다");
responseObserver.onCompleted();
}
};
}
// 클라이언트 측 구현
StreamObserver<ChatMessage> requestObserver = asyncStub.chat(
new StreamObserver<ChatMessage>() {
@Override
public void onNext(ChatMessage response) {
// 서버 응답 처리
processServerResponse(response);
}
@Override
public void onError(Throwable t) {
Status status = Status.fromThrowable(t);
if (status.getCode() == Status.Code.INVALID_ARGUMENT) {
logger.warning("잘못된 메시지: " + status.getDescription());
} else if (status.getCode() == Status.Code.PERMISSION_DENIED) {
logger.warning("권한 거부: " + status.getDescription());
} else {
logger.severe("채팅 오류: " + status);
}
// UI에 오류 표시
showErrorToUser(status.getDescription());
}
@Override
public void onCompleted() {
logger.info("채팅이 완료되었습니다");
}
}
);
try {
// 사용자 메시지 전송
for (ChatMessage message : userMessages) {
requestObserver.onNext(message);
}
// 채팅 완료
requestObserver.onCompleted();
} catch (RuntimeException e) {
requestObserver.onError(e);
throw e;
}
|
gRPC 오류 처리와 모니터링#
분산 시스템에서는 오류를 모니터링하고 추적하는 것이 중요하다.
로깅과 추적(Logging and Tracing)#
모든 서비스에서 일관된 로깅 및 추적을 구현하는 것이 중요하다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
| // 인터셉터를 통한 로깅 및 추적 구현
public class LoggingInterceptor implements ServerInterceptor {
private static final Logger logger = Logger.getLogger(LoggingInterceptor.class.getName());
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
String methodName = call.getMethodDescriptor().getFullMethodName();
String traceId = extractTraceId(headers);
logger.info(String.format("[%s] 메서드 호출 시작: %s", traceId, methodName));
return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(
next.startCall(
new ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) {
@Override
public void close(Status status, Metadata trailers) {
logger.info(String.format(
"[%s] 메서드 완료: %s, 상태: %s",
traceId, methodName, status.getCode()));
if (!status.isOk()) {
logger.warning(String.format(
"[%s] 오류 발생: %s, 설명: %s",
traceId, status.getCode(), status.getDescription()));
}
super.close(status, trailers);
}
},
headers)) {
@Override
public void onMessage(ReqT message) {
logger.info(String.format("[%s] 메시지 수신: %s", traceId, methodName));
super.onMessage(message);
}
@Override
public void onHalfClose() {
logger.info(String.format("[%s] 클라이언트 스트림 종료: %s", traceId, methodName));
super.onHalfClose();
}
@Override
public void onCancel() {
logger.warning(String.format("[%s] 호출 취소됨: %s", traceId, methodName));
super.onCancel();
}
@Override
public void onComplete() {
logger.info(String.format("[%s] 호출 완료: %s", traceId, methodName));
super.onComplete();
}
};
}
private String extractTraceId(Metadata headers) {
// 헤더에서 추적 ID 추출 또는 생성
Metadata.Key<String> traceIdKey =
Metadata.Key.of("x-trace-id", Metadata.ASCII_STRING_MARSHALLER);
String traceId = headers.get(traceIdKey);
if (traceId == null) {
traceId = UUID.randomUUID().toString();
// 새로운 추적 ID를 헤더에 추가
headers.put(traceIdKey, traceId);
}
return traceId;
}
}
|
메트릭 수집(Metrics Collection)#
gRPC 호출 성공률, 지연 시간, 오류율 등의 메트릭을 수집하여 서비스 품질을 모니터링할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| // 메트릭 수집 인터셉터 예시
public class MetricsInterceptor implements ServerInterceptor {
private final MeterRegistry registry;
public MetricsInterceptor(MeterRegistry registry) {
this.registry = registry;
}
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
String methodName = call.getMethodDescriptor().getFullMethodName();
Timer.Sample timer = Timer.start(registry);
// 요청 카운터 증가
registry.counter("grpc.requests.total",
"method", methodName).increment();
return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(
next.startCall(
new ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) {
@Override
public void close(Status status, Metadata trailers) {
// 타이머 정지 및 레이턴시 기록
timer.stop(Timer.builder("grpc.requests.latency")
.tag("method", methodName)
.tag("status", status.getCode().toString())
.register(registry));
// 상태 코드별 카운터 증가
registry.counter("grpc.requests.status",
"method", methodName,
"status", status.getCode().toString()).increment();
// 오류인 경우 오류 카운터 증가
if (!status.isOk()) {
registry.counter("grpc.requests.errors",
"method", methodName,
"status", status.getCode().toString()).increment();
}
super.close(status, trailers);
}
},
headers)) {
// 리스너 구현
};
}
}
|
분산 추적(Distributed Tracing)#
OpenTelemetry와 같은 분산 추적 시스템을 사용하여 서비스 간 호출을 추적할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
| // OpenTelemetry를 사용한 분산 추적 인터셉터 예시
public class TracingInterceptor implements ServerInterceptor {
private final Tracer tracer;
public TracingInterceptor(Tracer tracer) {
this.tracer = tracer;
}
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
String methodName = call.getMethodDescriptor().getFullMethodName();
// 헤더에서 상위 컨텍스트 추출
Context parentContext = OpenTelemetry.getPropagators().getTextMapPropagator()
.extract(Context.current(), headers, new HeadersExtractor());
// 새 스팬 생성
Span span = tracer.spanBuilder(methodName)
.setParent(parentContext)
.setSpanKind(SpanKind.SERVER)
.startSpan();
// 스팬에 메서드 정보 추가
span.setAttribute("rpc.service", extractService(methodName));
span.setAttribute("rpc.method", extractMethod(methodName));
try (Scope scope = span.makeCurrent()) {
return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(
next.startCall(
new ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) {
@Override
public void close(Status status, Metadata trailers) {
// 스팬에 상태 정보 추가
span.setAttribute("rpc.status_code", status.getCode().toString());
if (!status.isOk()) {
// 오류 정보 기록
span.setStatus(StatusCode.ERROR);
span.recordException(status.asRuntimeException());
span.setAttribute("error.message", status.getDescription());
}
super.close(status, trailers);
span.end();
}
},
headers)) {
// 리스너 구현
};
}
}
private String extractService(String fullMethodName) {
int index = fullMethodName.indexOf('/');
return index > 0 ? fullMethodName.substring(0, index) : fullMethodName;
}
private String extractMethod(String fullMethodName) {
int index = fullMethodName.indexOf('/');
return index > 0 ? fullMethodName.substring(index + 1) : "";
}
// 헤더에서 컨텍스트 추출을 위한 도우미 클래스
private static class HeadersExtractor implements TextMapGetter<Metadata> {
@Override
public Iterable<String> keys(Metadata carrier) {
// 메타데이터 키 추출 로직
return Collections.emptyList(); // 간단한 예시용
}
@Override
public String get(Metadata carrier, String key) {
// 메타데이터에서 값 추출
Metadata.Key<String> k = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
return carrier.get(k);
}
}
}
|
보안 관련 오류 처리#
gRPC 서비스에서 보안 관련 오류는 특별한 주의가 필요하다.
인증 및 인가 오류#
인증 및 인가 오류를 적절하게 처리하는 것이 중요하다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
| // 인증 인터셉터 예시
public class AuthInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
// … 앞부분 생략 …
try {
// 토큰 검증
UserContext userContext = authService.validateToken(token);
// 접근 권한 확인
if (!hasPermission(userContext, methodName)) {
call.close(
Status.PERMISSION_DENIED
.withDescription("이 작업을 수행할 권한이 없습니다"),
new Metadata());
return new ServerCall.Listener<ReqT>() {};
}
// 유효한 인증 컨텍스트를 다음 인터셉터 또는 서비스로 전달
Context ctx = Context.current().withValue(USER_CONTEXT_KEY, userContext);
return Contexts.interceptCall(ctx, call, headers, next);
} catch (ExpiredTokenException e) {
// 만료된 토큰 처리
Metadata trailers = new Metadata();
trailers.put(
Metadata.Key.of("token-expired", Metadata.ASCII_STRING_MARSHALLER),
"true");
call.close(
Status.UNAUTHENTICATED
.withDescription("인증 토큰이 만료되었습니다"),
trailers);
return new ServerCall.Listener<ReqT>() {};
} catch (InvalidTokenException e) {
// 유효하지 않은 토큰 처리
call.close(
Status.UNAUTHENTICATED
.withDescription("유효하지 않은 인증 토큰입니다"),
new Metadata());
return new ServerCall.Listener<ReqT>() {};
}
}
private String extractToken(Metadata headers) {
return headers.get(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER));
}
private boolean isPublicMethod(String methodName) {
// 공개 메서드 확인 로직
return methodName.endsWith("/Login") || methodName.endsWith("/Register");
}
private boolean hasPermission(UserContext userContext, String methodName) {
// 권한 확인 로직
if (methodName.contains("Admin") && !userContext.hasRole("ADMIN")) {
return false;
}
return true;
}
}
// 클라이언트 측 처리
try {
Response response = stub.protectedMethod(request);
// 성공적인 응답 처리
} catch (StatusRuntimeException e) {
Status status = e.getStatus();
if (status.getCode() == Status.Code.UNAUTHENTICATED) {
Metadata trailers = e.getTrailers();
if (trailers != null &&
"true".equals(trailers.get(
Metadata.Key.of("token-expired", Metadata.ASCII_STRING_MARSHALLER)))) {
// 토큰 재발급 시도
refreshToken();
// 재시도 로직
} else {
// 로그인 화면으로 리디렉션
redirectToLogin();
}
} else if (status.getCode() == Status.Code.PERMISSION_DENIED) {
// 접근 거부 화면 표시
showAccessDenied(status.getDescription());
}
}
|
속도 제한(Rate Limiting) 오류#
서비스 남용을 방지하기 위한 속도 제한과 관련된 오류를 처리하는 방법:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| // 속도 제한 인터셉터 예시
public class RateLimitInterceptor implements ServerInterceptor {
private final RateLimiter rateLimiter;
public RateLimitInterceptor(RateLimiter rateLimiter) {
this.rateLimiter = rateLimiter;
}
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
String methodName = call.getMethodDescriptor().getFullMethodName();
String clientId = extractClientId(headers);
// 속도 제한 확인
if (!rateLimiter.allowRequest(clientId, methodName)) {
// 재시도 정보 추가
Metadata trailers = new Metadata();
trailers.put(
Metadata.Key.of("retry-after-ms", Metadata.ASCII_STRING_MARSHALLER),
String.valueOf(rateLimiter.getRetryAfterMs(clientId, methodName)));
call.close(
Status.RESOURCE_EXHAUSTED
.withDescription("요청 한도 초과. 나중에 다시 시도하세요"),
trailers);
return new ServerCall.Listener<ReqT>() {};
}
return next.startCall(call, headers);
}
private String extractClientId(Metadata headers) {
// 클라이언트 ID 추출 로직
return headers.get(Metadata.Key.of("client-id", Metadata.ASCII_STRING_MARSHALLER));
}
}
// 클라이언트 측 처리
try {
Response response = stub.frequentMethod(request);
// 성공적인 응답 처리
} catch (StatusRuntimeException e) {
if (e.getStatus().getCode() == Status.Code.RESOURCE_EXHAUSTED) {
// 재시도 지연 정보 추출
Metadata trailers = e.getTrailers();
String retryAfterMs = trailers.get(
Metadata.Key.of("retry-after-ms", Metadata.ASCII_STRING_MARSHALLER));
if (retryAfterMs != null) {
long delayMs = Long.parseLong(retryAfterMs);
logger.info("속도 제한 초과, " + delayMs + "ms 후 재시도 예정");
// 지연 후 재시도 로직
scheduler.schedule(() -> retryRequest(request), delayMs, TimeUnit.MILLISECONDS);
}
}
}
|
민감한 정보 처리#
오류 메시지에서 민감한 정보가 노출되지 않도록 주의해야 한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| // 민감한 정보 필터링 예시
public class ErrorSanitizingInterceptor implements ServerInterceptor {
private static final Logger logger = Logger.getLogger(ErrorSanitizingInterceptor.class.getName());
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
return next.startCall(
new ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) {
@Override
public void close(Status status, Metadata trailers) {
if (!status.isOk()) {
// 내부 오류의 경우 상세 정보 로깅 후 일반적인 메시지로 대체
if (status.getCode() == Status.Code.INTERNAL) {
logger.severe("내부 오류: " + status.getDescription());
// 민감한 정보가 포함된 원래 메시지 대신 일반적인 메시지 사용
status = Status.INTERNAL.withDescription("내부 서버 오류가 발생했습니다");
}
// SQL 주입, 스택 트레이스 등의 패턴 제거
String description = status.getDescription();
if (description != null) {
description = sanitizeErrorMessage(description);
status = status.withDescription(description);
}
}
super.close(status, trailers);
}
private String sanitizeErrorMessage(String message) {
// SQL 오류 패턴 제거
message = message.replaceAll("(SQL|sql)\\s+error.*", "데이터베이스 오류");
// 스택 트레이스 제거
message = message.replaceAll("at\\s+[\\w\\.\\$]+\\([^)]*\\).*", "");
// 파일 경로 제거
message = message.replaceAll("/(home|usr|var|etc)/[\\w/]*", "[PATH]");
// IP 주소 제거
message = message.replaceAll("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}", "[IP]");
return message;
}
},
headers);
}
}
|
다국어 지원과 오류 메시지 지역화#
국제적인 사용자를 위한 애플리케이션에서는 오류 메시지의 지역화가 중요하다.
지역화된 오류 메시지 제공#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
| // 오류 메시지 지역화 인터셉터 예시
public class LocalizedErrorInterceptor implements ServerInterceptor {
private final ResourceBundle.Control bundleControl;
private final Map<Locale, ResourceBundle> errorMessageBundles;
public LocalizedErrorInterceptor() {
bundleControl = ResourceBundle.Control.getControl(ResourceBundle.Control.FORMAT_PROPERTIES);
errorMessageBundles = new ConcurrentHashMap<>();
}
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
// 클라이언트 로케일 추출
Locale clientLocale = extractLocale(headers);
return next.startCall(
new ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) {
@Override
public void close(Status status, Metadata trailers) {
if (!status.isOk()) {
// 오류 코드 추출
String errorCode = extractErrorCode(status.getDescription());
if (errorCode != null) {
// 지역화된 오류 메시지 찾기
String localizedMessage = getLocalizedErrorMessage(clientLocale, errorCode);
if (localizedMessage != null) {
// 지역화된 메시지로 상태 업데이트
status = status.withDescription(localizedMessage);
// 원래 오류 코드를 메타데이터에 포함
trailers.put(
Metadata.Key.of("error-code", Metadata.ASCII_STRING_MARSHALLER),
errorCode);
}
}
}
super.close(status, trailers);
}
private String extractErrorCode(String description) {
// 오류 설명에서 코드 추출 (형식: [CODE:xxxx] 메시지)
if (description != null) {
Matcher matcher = Pattern.compile("\\[CODE:(\\w+)\\].*").matcher(description);
if (matcher.matches()) {
return matcher.group(1);
}
}
return null;
}
private String getLocalizedErrorMessage(Locale locale, String errorCode) {
try {
ResourceBundle bundle = errorMessageBundles.computeIfAbsent(
locale,
l -> ResourceBundle.getBundle("errors", l, bundleControl));
return bundle.getString(errorCode);
} catch (MissingResourceException e) {
// 번역이 없는 경우 원래 메시지 사용
return null;
}
}
},
headers);
}
private Locale extractLocale(Metadata headers) {
String localeStr = headers.get(
Metadata.Key.of("accept-language", Metadata.ASCII_STRING_MARSHALLER));
if (localeStr != null) {
String[] parts = localeStr.split("[-_]", 2);
if (parts.length == 2) {
return new Locale(parts[0], parts[1]);
} else {
return new Locale(parts[0]);
}
}
// 기본 로케일
return Locale.US;
}
}
// 서비스 구현에서 사용
@Override
public void createUser(CreateUserRequest request, StreamObserver<User> responseObserver) {
if (request.getEmail() == null || !isValidEmail(request.getEmail())) {
responseObserver.onError(
Status.INVALID_ARGUMENT
.withDescription("[CODE:INVALID_EMAIL] 유효하지 않은 이메일 주소입니다")
.asRuntimeException());
return;
}
// 나머지 처리 로직
}
|
다양한 언어로 오류 메시지 정의#
각 언어별 속성 파일에 오류 메시지를 정의한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # errors_en.properties (영어)
INVALID_EMAIL=Invalid email address
USER_NOT_FOUND=User not found
PERMISSION_DENIED=You do not have permission to perform this action
# errors_ko.properties (한국어)
INVALID_EMAIL=유효하지 않은 이메일 주소입니다
USER_NOT_FOUND=사용자를 찾을 수 없습니다
PERMISSION_DENIED=이 작업을 수행할 권한이 없습니다
# errors_ja.properties (일본어)
INVALID_EMAIL=無効なメールアドレスです
USER_NOT_FOUND=ユーザーが見つかりません
PERMISSION_DENIED=この操作を実行する権限がありません
|
확장성을 고려한 오류 처리 설계#
대규모 마이크로서비스 환경에서는 확장 가능한 오류 처리 시스템이 필요하다.
오류 처리 라이브러리#
모든 서비스가 공통된 오류 처리 패턴을 사용할 수 있도록 라이브러리화할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
| // 공통 오류 처리 라이브러리 예시
public class ErrorLibrary {
// 공통 오류 코드 정의
public enum ErrorCode {
INVALID_INPUT("INVALID_INPUT", Status.Code.INVALID_ARGUMENT),
NOT_FOUND("NOT_FOUND", Status.Code.NOT_FOUND),
ALREADY_EXISTS("ALREADY_EXISTS", Status.Code.ALREADY_EXISTS),
PERMISSION_DENIED("PERMISSION_DENIED", Status.Code.PERMISSION_DENIED),
UNAUTHENTICATED("UNAUTHENTICATED", Status.Code.UNAUTHENTICATED),
INTERNAL("INTERNAL", Status.Code.INTERNAL);
private final String code;
private final Status.Code statusCode;
ErrorCode(String code, Status.Code statusCode) {
this.code = code;
this.statusCode = statusCode;
}
public String getCode() {
return code;
}
public Status.Code getStatusCode() {
return statusCode;
}
}
// 오류 상태 생성 헬퍼 메서드
public static Status createStatus(ErrorCode errorCode, String message) {
return Status.fromCode(errorCode.getStatusCode())
.withDescription(String.format("[CODE:%s] %s", errorCode.getCode(), message));
}
// 오류 상태와 상세 정보를 포함한 예외 생성
public static StatusRuntimeException createException(ErrorCode errorCode, String message) {
return createStatus(errorCode, message).asRuntimeException();
}
public static StatusRuntimeException createException(
ErrorCode errorCode, String message, Metadata trailers) {
return createStatus(errorCode, message).asRuntimeException(trailers);
}
// 필드 오류 상세 정보 생성
public static Metadata createFieldErrorMetadata(String fieldName, String errorMessage) {
Metadata metadata = new Metadata();
BadRequest badRequest = BadRequest.newBuilder()
.addFieldViolations(FieldViolation.newBuilder()
.setField(fieldName)
.setDescription(errorMessage)
.build())
.build();
metadata.put(
ProtoUtils.keyForProto(BadRequest.getDefaultInstance()),
badRequest);
return metadata;
}
// 일반적인 비즈니스 예외를 gRPC 상태로 변환
public static StatusRuntimeException mapBusinessException(BusinessException e) {
// 비즈니스 예외 유형에 따라 적절한 gRPC 상태 코드 매핑
switch (e.getType()) {
case VALIDATION:
return createException(ErrorCode.INVALID_INPUT, e.getMessage());
case NOT_FOUND:
return createException(ErrorCode.NOT_FOUND, e.getMessage());
case DUPLICATE:
return createException(ErrorCode.ALREADY_EXISTS, e.getMessage());
case AUTHORIZATION:
return createException(ErrorCode.PERMISSION_DENIED, e.getMessage());
case AUTHENTICATION:
return createException(ErrorCode.UNAUTHENTICATED, e.getMessage());
default:
// 내부 오류는 로깅하고 일반적인 메시지 반환
logger.severe("내부 오류: " + e.getMessage());
return createException(ErrorCode.INTERNAL, "내부 서버 오류가 발생했습니다");
}
}
}
|
서비스 경계에서의 일관된 오류 변환#
분산 시스템의 각 계층에서 일관된 오류 변환을 적용한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| // 서비스 계층에서의 오류 변환 예시
@GrpcService
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
private final UserManager userManager;
@Override
public void getUser(GetUserRequest request, StreamObserver<User> responseObserver) {
try {
// 비즈니스 로직 호출
User user = userManager.getUser(request.getId());
responseObserver.onNext(user);
responseObserver.onCompleted();
} catch (UserNotFoundException e) {
// 도메인 예외를 gRPC 상태로 변환
responseObserver.onError(
ErrorLibrary.createException(
ErrorLibrary.ErrorCode.NOT_FOUND,
e.getMessage()));
} catch (InvalidInputException e) {
responseObserver.onError(
ErrorLibrary.createException(
ErrorLibrary.ErrorCode.INVALID_INPUT,
e.getMessage()));
} catch (Exception e) {
// 예상치 못한 오류는 로깅하고 일반적인 메시지 반환
logger.severe("사용자 조회 중 예상치 못한 오류: " + e.getMessage());
responseObserver.onError(
ErrorLibrary.createException(
ErrorLibrary.ErrorCode.INTERNAL,
"내부 서버 오류가 발생했습니다"));
}
}
}
|
마이크로서비스 간 오류 전파 표준화#
마이크로서비스 간 통신에서 오류 전파를 표준화하는 것은 매우 중요하다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| // 마이크로서비스 간 오류 전파 예시
public class UserProfileServiceImpl extends UserProfileServiceGrpc.UserProfileServiceImplBase {
private final UserServiceClient userServiceClient;
private final ProfileStore profileStore;
@Override
public void getUserProfile(GetUserProfileRequest request,
StreamObserver<UserProfile> responseObserver) {
try {
// 다른 서비스 호출
User user = userServiceClient.getUser(request.getUserId());
// 프로필 정보 조회
ProfileData profileData = profileStore.getProfileData(request.getUserId());
// 응답 생성
UserProfile profile = UserProfile.newBuilder()
.setUserId(user.getId())
.setName(user.getName())
.setEmail(user.getEmail())
.setBio(profileData.getBio())
.setPictureUrl(profileData.getPictureUrl())
.build();
responseObserver.onNext(profile);
responseObserver.onCompleted();
} catch (StatusRuntimeException e) {
// 다른 서비스에서 발생한 gRPC 오류 처리
Status status = e.getStatus();
if (status.getCode() == Status.Code.NOT_FOUND) {
// 사용자 서비스에서 사용자를 찾지 못한 경우
responseObserver.onError(
Status.NOT_FOUND
.withDescription("사용자 프로필을 찾을 수 없습니다")
.asRuntimeException());
} else if (status.getCode() == Status.Code.PERMISSION_DENIED) {
// 권한 오류 전파
responseObserver.onError(e);
} else {
// 다른 오류의 경우 로깅 후 내부 오류로 변환
logger.severe("사용자 서비스 오류: " + status);
responseObserver.onError(
Status.INTERNAL
.withDescription("프로필을 처리하는 중 오류가 발생했습니다")
.asRuntimeException());
}
} catch (Exception e) {
// 예상치 못한 오류 처리
logger.severe("프로필 처리 중 예상치 못한 오류: " + e.getMessage());
responseObserver.onError(
Status.INTERNAL
.withDescription("내부 서버 오류가 발생했습니다")
.asRuntimeException());
}
}
}
|
테스트와 오류 시뮬레이션#
오류 처리 메커니즘을 철저히 테스트하는 것은 신뢰성 있는 gRPC 서비스를 제공하는 데 필수적이다.
단위 테스트와 오류 시나리오#
다양한 오류 시나리오에 대한 단위 테스트를 작성한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| // 오류 처리 단위 테스트 예시
@Test
public void testInvalidArgumentError() {
// 테스트 설정
UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(channel);
// 잘못된 입력으로 테스트
GetUserRequest request = GetUserRequest.newBuilder()
.setId(-1) // 음수 ID는 유효하지 않음
.build();
// 예외 검증
StatusRuntimeException exception = assertThrows(
StatusRuntimeException.class,
() -> stub.getUser(request));
// 상태 코드 검증
assertEquals(Status.Code.INVALID_ARGUMENT, exception.getStatus().getCode());
assertTrue(exception.getStatus().getDescription().contains("양수여야 합니다"));
}
@Test
public void testUserNotFoundError() {
// 테스트 설정
UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(channel);
// 존재하지 않는 사용자 ID로 테스트
GetUserRequest request = GetUserRequest.newBuilder()
.setId(9999) // 존재하지 않는 ID
.build();
// 예외 검증
StatusRuntimeException exception = assertThrows(
StatusRuntimeException.class,
() -> stub.getUser(request));
// 상태 코드 검증
assertEquals(Status.Code.NOT_FOUND, exception.getStatus().getCode());
assertTrue(exception.getStatus().getDescription().contains("찾을 수 없습니다"));
}
|
통합 테스트와 오류 전파 검증#
여러 서비스 간의 오류 전파를 검증하는 통합 테스트를 작성한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // 오류 전파 통합 테스트 예시
@Test
public void testErrorPropagationAcrossServices() {
// 테스트 설정
UserProfileServiceGrpc.UserProfileServiceBlockingStub profileStub =
UserProfileServiceGrpc.newBlockingStub(profileChannel);
// 존재하지 않는 사용자로 프로필 요청
GetUserProfileRequest request = GetUserProfileRequest.newBuilder()
.setUserId(9999) // 존재하지 않는 ID
.build();
// 예외 검증
StatusRuntimeException exception = assertThrows(
StatusRuntimeException.class,
() -> profileStub.getUserProfile(request));
// 프로필 서비스에서 NOT_FOUND로 변환되었는지 확인
assertEquals(Status.Code.NOT_FOUND, exception.getStatus().getCode());
assertTrue(exception.getStatus().getDescription().contains("프로필을 찾을 수 없습니다"));
}
|
오류 시뮬레이션 테스트#
다양한 오류 상황을 시뮬레이션하여 시스템의 견고성을 테스트한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| // 오류 시뮬레이션 테스트 예시
@Test
public void testRetryMechanismOnUnavailable() throws Exception {
// 일시적으로 사용할 수 없는 서비스 모의 객체 생성
Server flakyServer = createFlakyServer();
try {
flakyServer.start();
// 재시도 메커니즘이 있는 스텁 생성
UserServiceGrpc.UserServiceBlockingStub retryingStub =
UserServiceGrpc.newBlockingStub(ManagedChannelBuilder.forAddress("localhost", flakyServer.getPort())
.usePlaintext()
.build())
.withDeadlineAfter(10, TimeUnit.SECONDS);
// 테스트 요청
GetUserRequest request = GetUserRequest.newBuilder()
.setId(123)
.build();
// 재시도 후 성공적으로 응답을 받아야 함
User user = retryingStub.getUser(request);
// 응답 검증
assertEquals(123, user.getId());
assertEquals("Test User", user.getName());
} finally {
flakyServer.shutdownNow();
}
}
private Server createFlakyServer() {
// 처음 두 번은 UNAVAILABLE 오류를 반환하고, 세 번째 요청부터는 성공하는 서버
AtomicInteger requestCount = new AtomicInteger(0);
return ServerBuilder.forPort(0)
.addService(new UserServiceGrpc.UserServiceImplBase() {
@Override
public void getUser(GetUserRequest request, StreamObserver<User> responseObserver) {
int count = requestCount.incrementAndGet();
if (count <= 2) {
// 처음 두 번은 UNAVAILABLE 오류
responseObserver.onError(
Status.UNAVAILABLE
.withDescription("서비스를 일시적으로 사용할 수 없습니다")
.asRuntimeException());
} else {
// 세 번째부터는 정상 응답
User user = User.newBuilder()
.setId(request.getId())
.setName("Test User")
.setEmail("test@example.com")
.build();
responseObserver.onNext(user);
responseObserver.onCompleted();
}
}
})
.build();
}
|
주요 원칙#
- 일관성: 모든 서비스에서 일관된 오류 코드와 형식을 사용하여 클라이언트가 쉽게 예측하고 대응할 수 있도록 한다.
- 명확성: 오류 메시지와 코드는 오류의 원인과 가능한 해결 방법을 명확하게 전달해야 한다.
- 보안성: 오류 메시지에 민감한 정보가 포함되지 않도록 하고, 인증 및 인가 오류를 적절하게 처리해야 한다.
- 확장성: 오류 처리 메커니즘은 새로운 오류 유형을 추가하고 여러 서비스 간에 오류를 전파할 수 있도록 확장 가능해야 한다.
- 재사용성: 공통 오류 처리 로직을 라이브러리나 인터셉터로 구현하여 코드 중복을 줄이고 일관성을 향상시킨다.
모범 사례 요약#
서버 측 모범 사례#
- 적절한 상태 코드 선택: 오류 상황에 가장 적합한 gRPC 상태 코드를 선택한다.
- 클라이언트 오류에는
INVALID_ARGUMENT
, NOT_FOUND
, ALREADY_EXISTS
등 - 서버 오류에는
INTERNAL
, UNAVAILABLE
등 - 권한 관련 오류에는
PERMISSION_DENIED
, UNAUTHENTICATED
등
- 유용한 오류 메시지 제공: 디버깅에 도움이 되는 명확한 오류 메시지를 제공하되, 민감한 정보는 노출하지 않는다.
- 구조화된 오류 상세 정보: 가능한 경우
google.rpc.Status
확장이나 메타데이터를 사용하여 구조화된 오류 정보를 제공한다. - 오류 처리 중앙화: 인터셉터나 공통 라이브러리를 사용하여 오류 처리 로직을 중앙화한다.
- 오류 매핑 표준화: 비즈니스 로직 예외를 gRPC 상태 코드로 매핑하는 일관된 전략을 사용한다.
- 적절한 로깅: 오류 발생 시 디버깅에 필요한 정보를 로깅하되, 민감한 정보는 제외한다.
- 스트리밍 RPC에서의 적절한 오류 처리: 스트리밍 RPC에서 오류가 발생하면 스트림을 적절하게 종료한다.
클라이언트 측 모범 사례#
- 오류 상태 코드 확인: 오류 응답에서 상태 코드를 확인하고 상황에 맞게 처리한다.
- 재시도 전략 구현: 일시적인 오류(UNAVAILABLE, RESOURCE_EXHAUSTED 등)에 대한 적절한 재시도 전략을 구현한다.
- 적절한 타임아웃 설정: 모든 RPC 호출에 적절한 타임아웃을 설정하여 응답을 무한정 기다리는 상황을 방지한다.
- 점진적 성능 저하 처리: 일부 서비스에 문제가 발생하더라도 가능한 기능은 정상 작동하도록 설계한다.
- 오류 메타데이터 활용: 서버에서 제공하는 추가 오류 정보(트레일러 메타데이터)를 활용한다.
언어별 고려 사항#
각 프로그래밍 언어는 gRPC 오류 처리에 대한 특정 패턴과 관용구를 가지고 있다:
- Java:
Status
, StatusRuntimeException
, StreamObserver.onError()
를 사용한다. - Go:
status
패키지와 codes
패키지를 사용하여 오류를 처리한다. - Python:
grpc.StatusCode
와 context.set_code()
, context.set_details()
를 사용한다. - C#:
RpcException
과 StatusCode
열거형을 사용한다. - JavaScript/TypeScript:
status
객체와 콜백 패턴 또는 Promise를 사용한다.
전체 시스템 수준 고려 사항#
- 일관된 모니터링: 모든 서비스에서 오류 발생률, 유형, 심각도를 모니터링한다.
- 서비스 수준 목표(SLO) 정의: 허용 가능한 오류율과 응답 시간을 정의하고 모니터링한다.
- 문서화: 모든 서비스의 가능한 오류 코드와 처리 방법을 문서화한다.
- 카오스 테스트: 다양한 오류 시나리오를 시뮬레이션하여 시스템의 견고성을 테스트한다.
- 오류 처리 방식 개선: 생산 환경에서 발생하는 실제 오류를 분석하고 오류 처리 메커니즘을 지속적으로 개선한다.
용어 정리#
참고 및 출처#