
Q. gRPC에 왜 관심을 갖게 됐는가?
현재 기술 블로그를 MSA 형식으로 만들다 보니 서버간 통신에 기존 사용하던 feign client 대신 "알고만 있던 gRPC를 사용해 학습해보자" 라는 생각을 갖게 됐다.
간단히 이야기 하면
- feign은 Http/1.1에서 동작하며 동기식
- gRPC는 Http/2.0에서 동작하고
- 연결 한개로 동시에 여러개의 메시지를 주고 받을 수 있다.
- 중복되는 헤더는 전송하지 X
- 클라이언트의 요청 없이도 필요한 리소스를 전송 가능
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 은 서버 함수를 이용하게 대리인 사용?