Day95

강태훈·2026년 5월 18일

nbcamp TIL

목록 보기
95/97

서버 간의 gRPC 통신이 처음이라면 이 흐름이 복잡하고 어색하게 느껴지는 것이 아주 당연합니다

가장 최근에 만든 캐릭터 이름 수정 (4.5 API)을 예시로, 사용자의 요청이 데이터베이스에 반영될 때까지의 여정을 5개의 단계로 아주 쉽게 나누어서 설명해 드릴게요.

전체 구조는 다음과 같습니다:
[클라이언트] -(HTTP)-> [Gateway] -(gRPC)-> [Character 모듈] -> [데이터베이스(JPA)]


1단계: Gateway - CharacterController (외부 REST API 입구)

외부 인터넷(웹 브라우저나 모바일 앱)에서 PATCH /api/character/v1/characters/10 같은 HTTP 요청이 들어오는 곳입니다.

@org.springframework.web.bind.annotation.PatchMapping("/v1/characters/{characterId}")
public ApiResponse<UpdateCharacterNameResponse> updateCharacterName(
        @PathVariable Long characterId,
        @LoginUserId Long userId,  // 🔐 로그인한 사용자 ID (인증 토큰에서 추출)
        @RequestBody UpdateCharacterNameRequest request) { // 수정할 이름 {"name": "노바별"}
    
    // GatewayService로 데이터를 토스합니다.
    return ApiResponse.success(characterGatewayService.updateCharacterName(characterId, userId, request));
}
  • 역할: 사용자 인증(로그인 여부)을 확인하고, HTTP 요청 정보를 자바 객체로 변환하여 내부 서비스에 토스합니다. Gateway는 데이터베이스(JPA)에 직접 접근하지 않습니다.

2단계: Gateway - CharacterGatewayService (gRPC 클라이언트)

여기서 서버 간의 gRPC 통신이 시작됩니다. Gateway가 Character 서버에게 전화를 거는 과정이라고 생각하시면 됩니다.

public UpdateCharacterNameResponse updateCharacterName(Long characterId, Long userId, UpdateCharacterNameRequest request) {
    
    // 1. protobuf 규격에 맞춰 전송용 "택배 박스(Request)"를 조립합니다.
    var grpcRequest = com.p5laris.proto.character.v1.UpdateCharacterNameRequest.newBuilder()
            .setCharacterId(characterId)
            .setUserId(userId)
            .setName(request.name())
            .build();

    // 2. characterStub(전화기)를 사용해 Character 서버의 updateCharacterName 함수를 호출합니다. (네트워크 통신)
    var grpcResponse = characterStub.updateCharacterName(grpcRequest);

    // 3. Character 서버에서 돌려받은 결과(택배 박스)를 다시 웹 응답용 DTO로 포장합니다.
    return UpdateCharacterNameResponse.builder()
            .id(grpcResponse.getId())
            .name(grpcResponse.getName())
            .updatedAt(java.time.Instant.parse(grpcResponse.getUpdatedAt()))
            .build();
}
  • 역할: 일반 Java 객체 데이터를 네트워크를 통해 전송하기 편리한 gRPC/Protobuf 메시지 포맷으로 직렬화(Serialization)하여 실제 Character 서버로 발송합니다.

3단계: 규격 정의서 - character_service.proto (Interface Contract)

통신할 때 "우리는 이 구조의 데이터를 주고받을 것이다"라고 정의해 둔 규격서(약속)입니다. 이 파일을 기반으로 Java 통신 코드가 자동으로 컴파일 및 생성됩니다.

// 약속 1: 이런 함수(RPC)를 주고받을 거야.
rpc UpdateCharacterName(UpdateCharacterNameRequest) returns (UpdateCharacterNameResponse);

// 약속 2: 보낼 때는 이 데이터를 꼭 담아줘.
message UpdateCharacterNameRequest {
  int64 character_id = 1;
  int64 user_id = 2;
  string name = 3;
}

// 약속 3: 응답할 때는 이 데이터를 돌려줄게.
message UpdateCharacterNameResponse {
  int64 id = 1;
  string name = 2;
  string updated_at = 3;
}

4단계: Character 모듈 - CharacterGrpcController (gRPC 서버 입구)

Character 서버가 Gateway에서 보낸 gRPC 전화를 받는 곳입니다.

@Override
public void updateCharacterName(UpdateCharacterNameRequest request,
                                StreamObserver<UpdateCharacterNameResponse> responseObserver) {
    // 1. gRPC 요청 박스를 열어 데이터를 꺼내고, 실제 비즈니스 로직(Service)을 실행합니다.
    var result = characterService.updateCharacterName(
            request.getCharacterId(),
            request.getUserId(),
            request.getName()
    );

    // 2. 돌려줄 gRPC 응답 박스를 조립합니다.
    UpdateCharacterNameResponse response = UpdateCharacterNameResponse.newBuilder()
            .setId(result.id())
            .setName(result.name())
            .setUpdatedAt(result.updatedAt().toString())
            .build();

    // 3. 통신망을 통해 Gateway 서버로 전송합니다.
    responseObserver.onNext(response);
    responseObserver.onCompleted();
}
  • 역할: 외부 모듈(Gateway)로부터 전달받은 gRPC 네트워크 호출을 받아 해석한 뒤, 로컬 Spring 컨테이너의 서비스 레이어로 비즈니스 처리를 위임하고 결과를 돌려줍니다.

5단계: Character 모듈 - CharacterService (순수 비즈니스 로직 / JPA)

여기서부터는 질문자님이 이미 아주 잘 알고 계시는 순수 CRUD/JPA 영역입니다!

@Transactional
public UpdateCharacterNameResponse updateCharacterName(Long characterId, Long userId, String newName) {
    // 1. DB에서 캐릭터 엔티티를 꺼내옵니다.
    UserCharacter userCharacter = userCharacterRepository.findById(characterId)
            .orElseThrow(() -> new IllegalArgumentException("Character not found: " + characterId));

    // 2. 권한 검증: 이 캐릭터가 해당 유저의 것이 맞는지 검사합니다.
    if (!userCharacter.getUserId().equals(userId)) {
        throw new IllegalArgumentException("User does not own this character");
    }

    // 3. 엔티티 내부에서 비즈니스 검증(글자 수 등)을 하며 이름을 변경합니다. (JPA Dirty Checking으로 자동 변경 감지 후 DB 반영)
    userCharacter.updateName(newName);

    // 4. 결과를 반환합니다.
    return UpdateCharacterNameResponse.builder()
            .id(userCharacter.getId())
            .name(userCharacter.getName())
            .updatedAt(userCharacter.getUpdatedAt())
            .build();
}

💡 요약하자면!

  • Controller / Service / Entity / DB (5단계, 4단계 일부): 기존 싱글 서버 환경에서 작성하던 데이터 조작 logic과 100% 동일합니다.
  • Gateway (1단계, 2단계): 외부 사용자로부터 HTTP 요청을 받아, 어느 마이크로서비스로 전화를 걸어야 할지 찾아서 조율(Orchestration)해 주는 얇은 껍데기입니다.
  • Proto & Stub (3단계): 마이크로서비스 간에 데이터를 빠르고 안전하게 전송하기 위한 통로이자 규격 약속서입니다.

이렇게 중간에 통신 규격(gRPC)을 거침으로써, GatewayCharacter 서버가 독립적인 서버로 실행되면서도 서로 함수 호출하듯이 통신할 수 있게 됩니다.

0개의 댓글