Go는 빠르고 가벼워 마이크로서비스 아키텍처(MSA)를 구축하는 데 매우 적합한 언어입니다. REST 대신 빠르고 효율적인 통신을 위해 gRPC와 Protocol 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.
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;
}
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.
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)
}
}
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.
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"]
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).