← 가이드 목록으로 ← Back to guides

Go 마이크로서비스 완벽 가이드 (gRPC, Docker) Go Microservices Complete Guide (gRPC, Docker)

Go는 빠르고 가벼워 마이크로서비스 아키텍처(MSA)를 구축하는 데 매우 적합한 언어입니다. REST 대신 빠르고 효율적인 통신을 위해 gRPCProtocol Buffers를 활용하는 방법, 그리고 Docker 컨테이너라이제이션 기법을 알아봅니다.

Go is fast and lightweight, making it an excellent language for building Microservices Architecture (MSA). Let's explore how to use gRPC and Protocol Buffers for fast and efficient communication instead of REST, along with Docker containerization techniques.

Protocol Buffers 정의하기 Defining Protocol Buffers

gRPC 서비스는 `.proto` 파일에 서비스 인터페이스와 메시지 타입을 정의하는 것으로 시작합니다.

gRPC services start by defining the service interface and message types in a `.proto` file.

// user.proto
syntax = "proto3";

package user;
option go_package = "./pb";

// 서비스 정의
service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
}

// 요청 및 응답 메시지 정의
message UserRequest {
  string user_id = 1;
}

message UserResponse {
  string id = 1;
  string name = 2;
  string email = 3;
}
// user.proto
syntax = "proto3";

package user;
option go_package = "./pb";

// Service definition
service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
}

// Request and response message definitions
message UserRequest {
  string user_id = 1;
}

message UserResponse {
  string id = 1;
  string name = 2;
  string email = 3;
}

Go gRPC 서버 구현 Implementing Go gRPC Server

`protoc` 컴파일러를 통해 생성된 Go 코드를 바탕으로 서버를 구현합니다.

Implement the server based on the Go code generated by the `protoc` compiler.

package main

import (
  "context"
  "log"
  "net"

  "google.golang.org/grpc"
  pb "my-microservice/pb"
)

// 서버 구조체
type server struct {
  pb.UnimplementedUserServiceServer
}

// GetUser 메서드 구현
func (s *server) GetUser(ctx context.Context, in *pb.UserRequest) (*pb.UserResponse, error) {
  // 실제로는 데이터베이스 조회 로직이 들어갑니다
  log.Printf("Received request for user: %v", in.GetUserId())
  return &pb.UserResponse{
    Id: in.GetUserId(),
    Name: "Alice",
    Email: "[email protected]",
  }, nil
}

func main() {
  lis, err := net.Listen("tcp", ":50051")
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  s := grpc.NewServer()
  pb.RegisterUserServiceServer(s, &server{})

  log.Printf("Server listening at %v", lis.Addr())
  if err := s.Serve(lis); err != nil {
    log.Fatalf("failed to serve: %v", err)
  }
}
package main

import (
  "context"
  "log"
  "net"

  "google.golang.org/grpc"
  pb "my-microservice/pb"
)

// Server struct
type server struct {
  pb.UnimplementedUserServiceServer
}

// Implement GetUser method
func (s *server) GetUser(ctx context.Context, in *pb.UserRequest) (*pb.UserResponse, error) {
  // Database lookup logic would go here
  log.Printf("Received request for user: %v", in.GetUserId())
  return &pb.UserResponse{
    Id: in.GetUserId(),
    Name: "Alice",
    Email: "[email protected]",
  }, nil
}

func main() {
  lis, err := net.Listen("tcp", ":50051")
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  s := grpc.NewServer()
  pb.RegisterUserServiceServer(s, &server{})

  log.Printf("Server listening at %v", lis.Addr())
  if err := s.Serve(lis); err != nil {
    log.Fatalf("failed to serve: %v", err)
  }
}

Docker 컨테이너로 배포하기 Deploying with Docker Containers

Go 애플리케이션의 큰 장점은 매우 작고 독립적인 바이너리를 생성할 수 있다는 점입니다. 이를 활용한 멀티 스테이지 빌드 Dockerfile을 작성합니다.

A major advantage of Go applications is the ability to create very small, standalone binaries. Let's write a multi-stage build Dockerfile leveraging this.

# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# CGO 비활성화 및 정적 링킹하여 호환성 향상
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./main.go

# Final stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/server .

EXPOSE 50051
CMD ["./server"]
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Disable CGO and link statically for better compatibility
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./main.go

# Final stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/server .

EXPOSE 50051
CMD ["./server"]

✨ gRPC를 사용하는 이유 ✨ Why use gRPC?

  • 성능: HTTP/2 기반으로 Multiplexing을 지원하며 빠릅니다.
  • 페이로드 크기: JSON보다 작은 바이너리 형식(Protobuf)을 사용하여 네트워크 대역폭을 절약합니다.
  • 강타입 통신: 스키마 중심적 통신으로 타입 안정성을 보장합니다.
  • 코드 자동 생성: 다양한 언어로 클라이언트와 서버 코드를 자동 생성해 생산성이 높습니다.
  • Performance: Fast, supporting multiplexing based on HTTP/2.
  • Payload Size: Uses a smaller binary format (Protobuf) than JSON, saving network bandwidth.
  • Strongly-typed Communication: Schema-driven communication ensures type safety.
  • Code Generation: Automatically generates client and server code in various languages, significantly boosting productivity.

💡 Docker 멀티 스테이지 빌드 💡 Docker Multi-stage Builds

멀티 스테이지 빌드를 사용하면, 첫 번째 빌드 단계에서는 큰 용량의 Go 컴파일러와 소스 코드를 사용하지만, 최종 실행 이미지에는 컴파일러 없이 순수 실행 파일(`binary`)만 남겨 이미지 크기를 극단적으로 줄일 수 있습니다(수 MB 수준).

By using multi-stage builds, the first build stage uses the bulky Go compiler and source code, but the final runtime image only keeps the pure executable (`binary`) without the compiler. This dramatically reduces the image size (to just a few megabytes).

S

이성현 Sunghyun Lee

DevType 운영자. 개발자를 위한 타이핑 연습 콘텐츠와 기술 가이드를 작성하고 있습니다. Creator of DevType. Writing typing practice content and technical guides for developers.