gRPC

·2026년 2월 18일

Q. gRPC에 왜 관심을 갖게 됐는가?
현재 기술 블로그를 MSA 형식으로 만들다 보니 서버간 통신에 기존 사용하던 feign client 대신 "알고만 있던 gRPC를 사용해 학습해보자" 라는 생각을 갖게 됐다.
간단히 이야기 하면

  • feign은 Http/1.1에서 동작하며 동기식
  • gRPC는 Http/2.0에서 동작하고
    • 연결 한개로 동시에 여러개의 메시지를 주고 받을 수 있다.
    • 중복되는 헤더는 전송하지 X
    • 클라이언트의 요청 없이도 필요한 리소스를 전송 가능

본론에 들어가기

1. 핵심

  • 다른 서버에 있는 함수를 내 클라이언트에 있는 함수처럼 호출하기

2. 사용법 작성

  • 일단 각 서비스에 사용될 메서드가 존재해야만 한다.
  • .proto(ProtoBuf) 작성
  • .proto(ProtoBuf) 컴파일 시 -> 해당 언어에 해당한 클래스들이 생성
  • 서버 측 구현
  • 클라이언트 측 구현

3. 예시 (일단 각 서비스에 사용될 메서드가 존재한다 간주)

  • ㄱ) .proto

syntax = "proto3"; //proto3 버전을 사용

package userservice; // 나중에 import 할때 시작하는 패키지 이름

option java_package = "daulspring.grpc.user"; // 생성될 java 클래스들의 경로
option java_outer_classname = "UserProto";
option java_multiple_files = true; // 각 메시지/서비스마다 별도의 .java 파일을 생성

service UserGrpcService { // 서비스 정의 

  rpc CreateProfile (CreateProfileRequest) returns (CreateProfileResponse);
  // CreateProfile를 CreateProfileRequest으로 그리고 CreateProfileResponse 반환
  
  rpc ExistsByEmail (ExistsByEmailRequest) returns (ExistsByEmailResponse);
  // ExistsByEmail를 ExistsByEmailRequest으로 그리고 ExistsByEmailResponse 반환
  
  rpc ExistsByNickname (ExistsByNicknameRequest) returns (ExistsByNicknameResponse);
    // ExistsByNickname를 ExistsByNicknameRequest으로 그리고 ExistsByNicknameResponse 반환
  
  
}

message CreateProfileRequest { 
  string email = 1; // 그리고 1, 2, 3, 4는 필드의 고유 번호로 중구난방으로 10,2,5,3 이렇게 해도 문 제는 없다고 하지만, 순서대로 하는게 가장 좋다고 한다.
  string user_name = 2; 
  string nickname = 3;
  string profile_img = 4;
}

message CreateProfileResponse {
  int64 user_id = 1;
}

message ExistsByEmailRequest {
  string email = 1;
}

message ExistsByEmailResponse {
  bool exists = 1;
}

message ExistsByNicknameRequest {
  string nickname = 1;
}

message ExistsByNicknameResponse {
  bool exists = 1;
}
  • ㄴ) 서버 측 구현

@Slf4j
@GrpcService //Grpc서비스 엔드포인트를 spring boot에게 알리는 역할
@RequiredArgsConstructor
public class UserGrpcServiceImpl extends UserGrpcServiceGrpc.UserGrpcServiceImplBase {

  private final UserService userService;

  @Override
  public void createProfile(CreateProfileRequest request,
      StreamObserver<CreateProfileResponse> responseObserver) {
    try {
      UserCreateRequestDTO dto = new UserCreateRequestDTO(
          request.getEmail(),
          request.getUserName(),
          request.getNickname(),
          request.getProfileImg()
      );

      Long userId = userService.createProfile(dto);

      responseObserver.onNext(
          CreateProfileResponse.newBuilder()
              .setUserId(userId)
              .build()
      );
      responseObserver.onCompleted();

    } catch (IllegalArgumentException e) {
      log.warn("createProfile 실패: {}", e.getMessage());
      responseObserver.onError(
          Status.INVALID_ARGUMENT
              .withDescription(e.getMessage())
              .asRuntimeException()
      );
    } catch (Exception e) {
      log.error("createProfile 서버 오류", e);
      responseObserver.onError(
          Status.INTERNAL
              .withDescription("서버 오류가 발생했습니다.")
              .asRuntimeException()
      );
    }
  }

  @Override
  public void existsByEmail(ExistsByEmailRequest request,
      StreamObserver<ExistsByEmailResponse> responseObserver) {
    try {
      boolean exists = userService.existsByEmail(request.getEmail());

      responseObserver.onNext(
          ExistsByEmailResponse.newBuilder()
              .setExists(exists)
              .build()
      );
      responseObserver.onCompleted();

    } catch (Exception e) {
      log.error("existsByEmail 서버 오류", e);
      responseObserver.onError(
          Status.INTERNAL
              .withDescription("서버 오류가 발생했습니다.")
              .asRuntimeException()
      );
    }
  }

  @Override
  public void existsByNickname(ExistsByNicknameRequest request,
      StreamObserver<ExistsByNicknameResponse> responseObserver) {
    try {
      boolean exists = userService.existsByNickname(request.getNickname());

      responseObserver.onNext(
          ExistsByNicknameResponse.newBuilder()
              .setExists(exists)
              .build()
      );
      responseObserver.onCompleted();

    } catch (Exception e) {
      log.error("existsByNickname 서버 오류", e);
      responseObserver.onError(
          Status.INTERNAL
              .withDescription("서버 오류가 발생했습니다.")
              .asRuntimeException()
      );
    }
  }
}



// StreamObserver<T> 가 핵심 : retuen 키워드로 반환X, 콜백 객체를 통해 응답을 반환
// 1.  .onNext() : 클라이언트에게 보낼 실제 응답 데이터를 전송
// 2.  .onCompleted() : 클라이언트에게 통신 종료를 알림
// 3.  .OnError() : 클라이언트에게 에러 내용을 전달하며, 통신 종료를 알림
  • ㄷ) 클라이언트 측 구현

@Slf4j
@Component
public class UserGrpcClient {

  @Value("${grpc.client.user-service.host}")
  private String host;

  @Value("${grpc.client.user-service.port}") // 서버측 포트
  private int port;

  private UserGrpcServiceGrpc.UserGrpcServiceBlockingStub stub;  // 비동기 
  // 동기라면 타입을 UserGrpcServiceStub && 아래에서 newStub(channel) 사용 

  @PostConstruct // Spring이 Bean 생성 이후 딱 한번 실행. 역할은 서버와의 통신 채널을 연결 && stub객체를 이용해 서버 함수를 로컬 함수로 사용
  public void init() {
    ManagedChannel channel = ManagedChannelBuilder
        .forAddress(host, port)
        .usePlaintext()
        .build();
    stub = UserGrpcServiceGrpc.newBlockingStub(channel); // 비동기
  }

  public Long createProfile(String email, String userName,
      String nickname, String profileImg) {

    CreateProfileRequest request = CreateProfileRequest.newBuilder()
        .setEmail(email)
        .setUserName(userName)
        .setNickname(nickname)
        .setProfileImg(profileImg != null ? profileImg : "")
        .build();

    CreateProfileResponse response = stub.createProfile(request);
    return response.getUserId();
  }

  public boolean existsByEmail(String email) {
    return stub.existsByEmail(
        ExistsByEmailRequest.newBuilder()
            .setEmail(email)
            .build()
    ).getExists();
  }

  public boolean existsByNickname(String nickname) {
    return stub.existsByNickname(
        ExistsByNicknameRequest.newBuilder()
            .setNickname(nickname)
            .build()
    ).getExists();
  }
}


// 1. 핵심 : ManagedChannel, Stub
//		ㄱ.  ManagedChannel 는 간단히 통신선 연결
//		ㄴ. Stub 은 서버 함수를 이용하게 대리인 사용?
profile
# h

0개의 댓글