반응형
스프링부트 3.x.x에 gRPC 예외 처리 인터셉터를 적용시켜 보자
📌 서론
저번 포스트에서 SpringBoot3.3.1에 gRPC를 적용시켰다. 완전히 최신 버전을 사용해보고자 했기에 java, gradle, protobuf, spring 버전에 따른 오류도 조금씩 있었는데 아직 이 부분은 조금씩 진행하면서 코드를 개선하는 중이다. (계속 업데이트해서 올릴 예정)
이번 포스트에서 소개할 내용은 gRPC의 예외처리 방법이다. http에서는 예외가 발생하면 @RestControllerAdvice를 사용해서 전역 예외처리를 하곤 한다. 그럼 gRPC도 예외처리를 하는 방법이 있지 않을까? 당연히 예외처리가 가능하다.
검색을 해보니 SpringBoot에서는 gRPC의 예외처리를 인터셉터를 통해 처리한다는 것을 알아냈다. 그래서 나는 이것을 직접 적용시켜 봤다. 지금부터 같이 gRPC 인터셉터를 등록해 보자.
이번 포스트는 아래 글(gRPC 설정)에서 이어진다.
예외처리 인터셉터 등록 로직만 필요하다면 이전 포스트 확인은 하지 않아도 된다. (config 패키지 확인)
1. gRPC 인터셉터 코드 작성
예외처리를 해줄 클래스를 작성한다. (ServerInterceptor 구현)
- ServerInterceptor를 구현해서 gRPC 인터셉터 코드를 작성하면 된다.
- ServerInterceptor를 살펴보면 io.grpc 패키지에서 제공하는 공식적인 gRPC 인터셉터 클래스다.
package io.grpc;
import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
public interface ServerInterceptor {
<ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> var1, Metadata var2, ServerCallHandler<ReqT, RespT> var3);
}
나는 아래와 같이 ServerInterceptor를 구현하는 예외 처리 코드를 작성했다.
/**
* ExceptionHandlingInterceptor는 gRPC 서버 인터셉터로,
* gRPC 메서드 호출 중 발생하는 예외를 중앙에서 처리합니다.
* 예외 발생 시 적절한 gRPC 상태 코드와 메시지를 반환합니다.
*/
@Slf4j
public class ExceptionHandlingInterceptor implements ServerInterceptor {
/**
* gRPC 메서드 호출을 가로채고, 예외를 처리합니다.
*
* @param serverCall gRPC 서버 호출 객체
* @param metadata gRPC 메타데이터
* @param serverCallHandler gRPC 서버 호출 핸들러
* @param <ReqT> 요청 타입
* @param <RespT> 응답 타입
* @return 요청 리스너
*/
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> serverCall,
Metadata metadata,
ServerCallHandler<ReqT, RespT> serverCallHandler
) {
return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(
serverCallHandler.startCall(new ExceptionHandlingServerCall<>(serverCall), metadata)) {
/**
* 클라이언트의 요청 처리를 시도합니다.
* 예외가 발생하면 handleException 메서드가 호출됩니다.
*/
@Override
public void onHalfClose() {
try {
super.onHalfClose();
} catch (Exception e) {
handleException(serverCall, e);
}
}
};
}
/**
* 예외를 처리하고 적절한 gRPC 상태 코드와 메시지를 반환합니다.
*
* @param call gRPC 서버 호출 객체
* @param e 발생한 예외
* @param <RespT> 응답 타입
*/
private <RespT> void handleException(ServerCall<RespT, ?> call, Exception e) {
Status status;
if (e instanceof IllegalArgumentException) {
// 잘못된 인수 예외 처리
log.error("예외처리 인터셉터 동작");
status = Status.INVALID_ARGUMENT.withDescription(e.getMessage());
} else {
// 알 수 없는 예외 처리
status = Status.UNKNOWN.withDescription("Unknown error occurred").withCause(e);
}
// 예외 로그 기록
log.error("Exception: ", e);
// gRPC 호출 종료 및 상태 코드 반환
call.close(status, new Metadata());
}
/**
* ExceptionHandlingServerCall은 ForwardingServerCall을 확장하여,
* 예외 처리를 위한 추가 기능을 제공합니다.
*
* @param <ReqT> 요청 타입
* @param <RespT> 응답 타입
*/
private static class ExceptionHandlingServerCall<ReqT, RespT>
extends ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT> {
/**
* ExceptionHandlingServerCall 생성자
*
* @param delegate 실제 gRPC 서버 호출 객체
*/
protected ExceptionHandlingServerCall(ServerCall<ReqT, RespT> delegate) {
super(delegate);
}
}
}
2. gRPC 인터셉터 스프링 빈 등록
GrpcConfig 클래스를 생성하고 인터셉터를 등록한다.
- @Configuration을 사용해서 생성한 인터셉터를 "스프링 빈"으로 등록해 준다.
- 이때 @GrpcGlobalServerInterceptor 어노테이션을 사용한다.
/**
* gRPC 서버 설정 - 인터셉터를 등록하는 클래스
*/
@Configuration
public class GrpcConfig {
/**
* gRPC 예외 처리 인터셉터 설정
*/
@GrpcGlobalServerInterceptor
ExceptionHandlingInterceptor exceptionHandlingInterceptor() {
return new ExceptionHandlingInterceptor();
}
}
@Bean은 안 적어주는 거야?
- @GrpcClobalServerInterceptor 어노테이션 내부를 살펴보면 @Component, @Bean이 모두 적혀있다. 즉, 이 어노테이션을 사용하면 내가 직접 @Bean을 적어줄 필요가 없는 것이다.
- 인터셉터 클래스를 작성하고 그 클래스 상단에 직접 @GrpcClobalServerInterceptor를 적어줘도 될 것이다. 그래도 나는 이런 식으로 중앙에서 관리하는 1개의 Config 클래스를 두고 여러 개의 빈을 여기서 등록시켜서 한 곳에서 한 번에 관리하는 게 좀 더 보기 좋은 것 같다. (취향차이라고 생각한다.)
package net.devh.boot.grpc.server.interceptor;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
@Bean
public @interface GrpcGlobalServerInterceptor {
}
3. gRPC 서버 코드에서 예외 발생시키기 (예외처리 인터셉터 검증)
gRPC 서버 코드에서 일부로 예외를 발생시킨다.
- 아래 작성된 gRPC 메서드인 createMember가 호출되면 첫 번째 줄에서 일부로 예외를 던지도록 했다. (이 코드는 미리 작성된 .proto 파일을 generateProto로 생성한 뒤 구현이 가능하다. 자세한 내용은 Repo를 확인해 보면 된다.)
- 나의 경우 gRPC client 클래스를 선언하고 Controller에서 이 client 메서드를 호출하도록 해서 검증작업을 진행했다. 이것은 이전 포스트에 상세하게 작성되어 있으니 필요시 확인해 보길 바란다.
@Slf4j
@RequiredArgsConstructor
@GrpcService
public class MemberServiceGrpcImpl extends MemberServiceGrpc.MemberServiceImplBase {
private final MemberService memberService;
private final MemberMapper memberMapper;
@Override
public void createMember(
MemberProto.MemberRequest request,
StreamObserver<MemberProto.MemberCreateResponse> responseObserver
) {
// 아래 코드의 주석을 해제하면 예외처리 인터셉터 테스트 가능
if (request.getEmail().contains("test")) {
throw new IllegalArgumentException("Test exception: Invalid email");
}
// 1. 클라이언트로부터 전달받은 request 데이터를 DTO로 변환한다.
MemberSignUpRequestDTO memberDTO = memberMapper.requestProtoToDto(request);
// 2. 서비스 레이어에서 request 데이터를 사용해서 RDB에 저장하는 로직을 수행하고 결과를 받는다.
Member createdMember = memberService.createMember(memberDTO);
// 3. RDB에 저장된 데이터를 gRPC response 데이터로 변환한다.
MemberProto.MemberCreateResponse response = memberMapper.dtoToResponseProto(createdMember);
// 4. 응답을 클라이언트에게 전달한다.
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
컨트롤러에서 gRPC 클라이언트 메서드를 호출한 뒤 로그를 확인한다.
- 아래와 같이 예외 로그가 제대로 남는 것을 확인할 수 있었다.
- 이것은 gRPC 예외처리 인터셉터가 잘 동작한다는 것을 의미한다.
ERROR 76814 --- [ault-executor-0] c.e.g.c.ExceptionHandlingInterceptor : 예외처리 인터셉터 동작
ERROR 76814 --- [ault-executor-0] c.e.g.c.ExceptionHandlingInterceptor : Exception:
# 예외 stackTrace는 엄청 길지만 2줄로 요약했다.
java.lang.IllegalArgumentException: Test exception: Invalid email
io.grpc.StatusRuntimeException: INVALID_ARGUMENT: Test exception: Invalid email
- 자세히 보면 내가 등록한 인터셉터의 로깅이 모두 남은 것을 확인할 수 있다. (아래는 인터셉터 로직에서 로깅 부분만 잘라온 것이다.)
if (e instanceof IllegalArgumentException) {
// 잘못된 인수 예외 처리
log.error("예외처리 인터셉터 동작");
status = Status.INVALID_ARGUMENT.withDescription(e.getMessage());
} else {
// 알 수 없는 예외 처리
status = Status.UNKNOWN.withDescription("Unknown error occurred").withCause(e);
}
// 예외 로그 기록
log.error("Exception: ", e);
// gRPC 호출 종료 및 상태 코드 반환
call.close(status, new Metadata());
반응형
'gRPC' 카테고리의 다른 글
개발자를 위한 gRPC 기본 개념 (0) | 2024.10.31 |
---|---|
[gRPC] SpringBoot3.3.1에 gRPC 적용하기 (2) | 2024.07.21 |