-
Grpc + Spring : 예외 처리 구현Spring 2024. 9. 22. 12:06
이번에는 grpc 서비스의 예외처리를 구현하는 방법을 작성해보고자 한다.
grpc service : 특정 예외 메세지를 담아 지정된 예외로 발생하기
기존 rest 형식과 동일하게 grpc 서비스도 @GrpcExceptionHandler를 이용해서 예외 처리를 구현할 수 있다.
@GrpcAdvice public class GrpcExceptionAdvice { @GrpcExceptionHandler public Status handleInvalidArgument(IllegalArgumentException e) { return Status.INVALID_ARGUMENT.withDescription("Your description").withCause(e); } @GrpcExceptionHandler(ResourceNotFoundException.class) public StatusException handleResourceNotFoundException(ResourceNotFoundException e) { Status status = Status.NOT_FOUND.withDescription("Your description").withCause(e); Metadata metadata = ... return status.asException(metadata); } }
Grpc Status는 http Status 와는 다르다.
- 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): 인증 정보가 누락되었거나 유효하지 않음.
여기서 나는 400대 예외가 아니라서 예외 로그를 찍고 파악해야 하는 예외들이라고 생각되는 예외들은 구분해서 예외 로그를 찍을 수 있도록 판단하는 메서드를 만들었다.
fun isErrorLogRequired(): Boolean = this.status !in listOf( Status.INVALID_ARGUMENT, Status.FAILED_PRECONDITION, Status.OUT_OF_RANGE, Status.UNIMPLEMENTED, Status.PERMISSION_DENIED, Status.UNAUTHENTICATED, Status.NOT_FOUND, )
또한, 기존에 http 요청의 errorResponse에서 커스텀한 예외들을 구분하기 위해서 별도의 코드를 내려주어 구분하고 있었기 때문에, grpc 서비스의 경우에도 예외 응답 형식의 메타데이터를 첨부해서 클라이언트가 이를 활용할 수 있도록 구현하였다.
특정 예외 형식의 proto (ErrorResponse.proto)
message ErrorResponse { string code = 1; string message = 2; }
메타데이터에 해당 데이터를 포함 (GrpcAdvice.kt)
@GrpcAdvice class ControllerAdvice { @GrpcExceptionHandler(Exception::class) fun handleException(e: Exception): StatusRuntimeException { val metadataKey: Metadata.Key<ErrorResponse> = ProtoUtils.keyForProto(ErrorResponse.getDefaultInstance()) val metadata = Metadata() metadata.put(metadataKey, ErrorResponse.newBuilder().setMessage("예시 테스트 예외가 발생했습니다").setCode("50001").build()) return Status.UNKNOWN .withDescription(e.message) .asRuntimeException(metadata) } }
grpc client : 서버의 예외 응답에 따라서 처리하기
위에서 grpc service에서는 클라이언트가 예외를 세부 분류하여 처리할 수 있도록 응답 형식을 메타데이터에 담아 보내주었다.
클라이언트에서 이를 이용해서 예외를 처리해보자.
fun getAuthor(): Author? { val request = Author.newBuilder() .setAuthorId(2) .setFirstName("Gildong") .setLastName("Hong") .setGender("Female") .setBookId(2) .build() return try { bookAuthorService.getAuthor(request) } catch (ex: StatusRuntimeException) { println("Status: ${ex.status}") println(ex.trailers) println(ex.status) val meta = Status.trailersFromThrowable(ex) val data = meta?.get(ProtoUtils.keyForProto(ErrorResponse.getDefaultInstance())) println(data) println(meta) println(meta?.keys()) throw ex throw RuntimeException("Failed to get author") } }
service를 호출하는 메서드마다 try catch로 잡고, 예외 응답의 데이터를 확인하여 처리하도록 할 수도 있겠지만 그렇게 된다면, 호출 정상 처리 로직과는 무관하게 계속해서 예외 처리를 함께 작성해주어야한다. 이를 해결하기 위해 다음과 같이 인터셉터를 활용하는 방안이 있다.
1) 예외 처리 인터셉터 구현하기
먼저 예외처리를 하는 인터셉터를 구현해준다.
인터셉터의 오버라이드 메서드중에서, onClose 메서드에 예외 처리를 구현한다.
class GrpcClientInterceptor : ClientInterceptor { private val logger = org.slf4j.LoggerFactory.getLogger(GrpcClientInterceptor::class.java) override fun <ReqT, RespT> interceptCall( method: MethodDescriptor<ReqT, RespT>, callOptions: CallOptions, next: Channel ): ClientCall<ReqT, RespT> { return object : ClientCall<ReqT, RespT>() { private val delegate = next.newCall(method, callOptions) override fun start(responseListener: Listener<RespT>, headers: io.grpc.Metadata) { delegate.start( object : Listener<RespT>() { override fun onHeaders(headers: io.grpc.Metadata) { responseListener.onHeaders(headers) } override fun onMessage(message: RespT) { responseListener.onMessage(message) } override fun onClose(status: Status, trailers: Metadata) { if (status.code != Status.Code.OK) { logger.info("Trailers received from server: $trailers") } responseListener.onClose(status, trailers) } override fun onReady() { responseListener.onReady() } }, headers ) } override fun request(numMessages: Int) { delegate.request(numMessages) } override fun cancel(message: String?, cause: Throwable?) { delegate.cancel(message, cause) } override fun halfClose() { delegate.halfClose() } override fun sendMessage(message: ReqT) { delegate.sendMessage(message) } } } }
이를 활용할 수 있게 config에 등록해준다.
해당 방법은 global하게 예외 처리 인터셉터를 구현한 방법으로 세부적으로 진행하고 싶다면, 다른 방식을 사용할 수도 있다.
@Configuration class GrpcConfiguration { @Order(1) @GrpcGlobalClientInterceptor fun grpcClientInterceptor(): ClientInterceptor { return GrpcClientInterceptor() } }
2) AOP를 이용해서 예외 처리 구현하기
인터셉터가 아니라, 특정 메서드에나 클래스 단위로 어노테이션을 이용한 AOP를 적용해서 예외 처리를 구현할 수도 있다.
예외 처리 어노테이션 구현
@Documented @Target(AnnotationTarget.CLASS) @Retention(RetentionPolicy.RUNTIME) annotation class GrpcComponent( val exception: KClass<out Exception> = Exception::class )
이를 사용하는 AOP 구현
@Aspect @Component class GrpcExceptionAop { @Pointcut("@within(com.yunhalee.grpcclient.support.annotation.GrpcComponent)") fun grpcClientService() {} // AOP는 함수의 try catch 문까지 모두 실행된 후에 실행이 됨 @AfterThrowing(pointcut = "grpcClientService()", throwing = "ex") fun handleGrpcClientServiceException(joinPoint: JoinPoint, ex: Exception) { if (ex is StatusRuntimeException) { val meta = Status.trailersFromThrowable(ex) val errorResponse = meta?.get(ProtoUtils.keyForProto(ErrorResponse.getDefaultInstance())) as ErrorResponse val grpcErrorResponse = GrpcErrorResponseMapperToDto.mapToDto(errorResponse) // throw GrpcServerException("grpc 서버 예외가 발생하여 예외를 던집니다. 메세지 : ${errorResponse.message}, 예외코드 : ${errorResponse.code}") throw GrpcServerException("grpc 서버 예외가 발생하여 예외를 던집니다. ", grpcErrorResponse, ex) } } }
+ 참고 )
여기서 넘어오는 proto 타입을 grpc client의 사용 타입으로 변환하기 위해 map-struct를 이용해서 mapper를 구현해주었다.
dependency를 추가한다.
// map-struct implementation("org.mapstruct:mapstruct:1.5.5.Final") annotationProcessor("org.mapstruct:mapstruct-processor:1.5.5.Final")
매퍼를 구현한다.
@Mapper( unmappedTargetPolicy = ReportingPolicy.IGNORE, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, ) interface GrpcErrorResponseMapper { fun mapToProto(dto: GrpcErrorResponse): ErrorResponse companion object { private val INSTANCE = Mappers.getMapper(GrpcErrorResponseMapper::class.java) fun GrpcErrorResponse.mapToProto(): ErrorResponse = INSTANCE.mapToProto(this) } } class GrpcErrorResponseMapperToDto { companion object { fun mapToDto(proto: ErrorResponse): GrpcErrorResponse { return GrpcErrorResponse( message = proto.message, code = proto.code, ) } } }
서비스 클래스에서 이를 활용
@Service @GrpcComponent class GrpcCustomService( val channel: Channel ) { private val bookAuthorService: BookAuthorServiceGrpc.BookAuthorServiceBlockingStub by lazy { BookAuthorServiceGrpc.newBlockingStub(channel) } fun getAuthor(): Author? { val request = Author.newBuilder() .setAuthorId(2) .setFirstName("Yunha") .setLastName("Lee") .setGender("Female") .setBookId(2) .build() return try { bookAuthorService.getAuthor(request) }catch (ex: Exception){ println("try catch 문으로 예외를 잡았습니다.: $ex") throw ex } } }
만약 로그로 주고받는 데이터를 확인하고 싶다면, 다음과 같이 로깅을 설정해서 확인해볼 수 있다.
logging: level: root: info io.grpc: DEBUG # gRPC 라이브러리 관련 로그 io.grpc.netty.shaded: DEBUG # Netty 기반 gRPC 로그 org.springframework.grpc: DEBUG # Spring Boot gRPC 로그 (선택 사항)
끝 - ✨
+ 참고 )
- https://grpc.github.io/grpc-java/javadoc/io/grpc/util/TransmitStatusRuntimeExceptionInterceptor.html
- https://github.com/grpc/grpc-java/blob/d8f73e04566fa588889ca1a422e276d71724643c/util/src/main/java/io/grpc/util/TransmitStatusRuntimeExceptionInterceptor.java#L49
- https://grpc.github.io/grpc-java/javadoc/io/grpc/util/TransmitStatusRuntimeExceptionInterceptor.html
- https://talzuchung-kty.tistory.com/6
- https://grpc.io/docs/guides/interceptors/
- https://learn.microsoft.com/en-us/aspnet/core/grpc/interceptors?view=aspnetcore-8.0
- https://yeongcheon.github.io/posts/2020-05-30-grpc-interceptor/
- https://grpc.github.io/grpc-java/javadoc/io/grpc/ServerInterceptor.html
'Spring' 카테고리의 다른 글