Go 기반 MSA 설계: gRPC, Kafka, Keycloak으로 구성하는 백엔드
모놀리식에서 마이크로서비스로 넘어가면 코드보다 구조에 대한 결정이 훨씬 많아진다. 어떤 프로토콜로 통신할 것인지, 서비스 간 의존성을 어떻게 끊을 것인지, 인증은 어디서 처리할 것인지. Go 기반으로 gRPC, Kafka, Keycloak을 조합한 MSA 구조를 정리해봤다.
각 기술의 깊은 내용은 별도 글에서 다룰 예정이고, 여기서는 전체 그림과 트레이드오프에 집중한다. 각 섹션 끝의 판단 기준이 핵심이다. MSA 전환 시점 자체를 고민 중이라면 각 판단 기준을 먼저 확인하자. 트래픽이나 팀 규모가 아직 이 스택을 정당화하지 못하는 단계라면 과한 선택이 된다.
왜 Go인가
MSA에서 Go가 자주 선택되는 이유는 단순하다.
- 컴파일 바이너리. 도커 이미지가 수십 MB 수준이라 배포가 빠르다
- 고루틴으로 수천 개의 동시 요청을 적은 메모리로 처리
- 빠른 빌드. CI에서 이미지 만드는 데 몇 초면 충분하다
- 표준 라이브러리가 강력하다. HTTP 서버, JSON 파싱 등 외부 의존성 없이 웬만한 건 된다
물론 Go가 만능은 아니다. 제네릭이 늦게 도입된 탓에 추상화가 좀 투박하고, 에러 처리가 장황하다는 불만은 여전하다. 복잡한 비즈니스 로직이 많은 도메인이라면 Kotlin이나 Java가 더 편할 수도 있다. 하지만 API 게이트웨이, 메시지 브로커 컨슈머, 프록시 같은 인프라에 가까운 서비스에서는 Go의 가성비가 압도적이다.
서비스 간 통신: gRPC
REST 대신 gRPC를 쓰는 이유
서비스가 2~3개일 때는 REST로 충분하다. 하지만 서비스가 10개를 넘어가면 이야기가 달라진다.
- REST는 JSON 직렬화/역직렬화 비용이 크다
- API 스키마가 문서에만 존재하면 서비스 간 계약이 깨지기 쉽다
- HTTP/1.1 기반이라 연결 효율이 떨어진다
gRPC는 이 문제를 Protocol Buffers(protobuf)로 해결한다.
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
}.proto 파일 하나로 서버/클라이언트 코드가 자동 생성된다. 타입이 보장되고, 바이너리 직렬화라 JSON보다 빠르다. HTTP/2 기반이라 하나의 연결에서 여러 요청을 동시에 처리할 수 있다.
트레이드오프
| REST | gRPC | |
|---|---|---|
| 가독성 | JSON은 사람이 읽을 수 있다 | 바이너리라 디버깅이 번거롭다 |
| 브라우저 호환 | 그대로 호출 가능 | gRPC-Web이나 프록시 필요 |
| 도구 생태계 | Postman, curl 등 풍부 | grpcurl, evans 등 별도 도구 필요 |
| 스키마 | OpenAPI(선택) | protobuf(강제) |
| 성능 | 충분히 빠름 | 직렬화/역직렬화 2~10배 빠름 |
판단 기준: 외부 클라이언트(웹, 모바일)와 직접 통신하는 API Gateway는 REST, 내부 서비스 간 통신은 gRPC. 이 조합이 가장 현실적이다.
gRPC를 쓴다고 모든 서비스 간 통신을 동기 호출로 가져가면 안 된다. 동기 호출이 늘어날수록 서비스 간 결합도가 올라가고, 한 서비스의 장애가 전파된다. 여기서 이벤트 기반 통신이 필요해진다.
이벤트 기반 아키텍처: Kafka
왜 이벤트인가
주문 서비스가 결제 서비스를 직접 호출하고, 결제 서비스가 재고 서비스를 호출하고… 이렇게 체이닝하면 분산 모놀리식가 된다. 마이크로서비스의 독립 배포라는 장점이 사라지는 거다.
이벤트 기반으로 바꾸면:
- 주문 서비스: “주문이 생성됐다”는 이벤트를 발행
- 결제 서비스: 해당 이벤트를 구독해서 결제 처리
- 재고 서비스: 같은 이벤트를 구독해서 재고 차감
서비스들이 서로를 직접 알 필요가 없다. 각자 이벤트만 보고 움직인다.
왜 Kafka인가
메시지 브로커는 RabbitMQ, AWS SQS, Redis Streams 등 여러 선택지가 있다. Kafka가 MSA에서 자주 쓰이는 이유는:
- 로그 기반 저장. 메시지가 디스크에 순서대로 저장되고, 컨슈머가 죽어도 메시지가 사라지지 않는다
- 리플레이 가능. 과거 이벤트를 다시 읽을 수 있어, 새 서비스를 추가할 때 기존 이벤트를 재처리할 수 있다
- 파티셔닝. 토픽을 파티션으로 나눠 병렬 처리하므로 수평 확장이 쉽다
- 컨슈머 그룹. 같은 이벤트를 여러 서비스가 독립적으로 소비
트레이드오프
Kafka는 강력하지만 복잡하다.
- 운영 부담이 크다. Zookeeper(또는 KRaft), 브로커, 파티션 관리. 소규모 팀에겐 과할 수 있다
- 이벤트 순서 보장은 파티션 단위다. 전체 순서가 필요하면 파티션을 1개로 제한해야 하는데, 그러면 병렬 처리의 의미가 없다
- 최종 일관성을 감수해야 한다. 이벤트가 전파되기까지 지연이 있어, “결제 완료했는데 주문 상태가 아직 안 바뀜” 같은 상황이 생긴다
- 디버깅이 어렵다. 동기 호출은 스택트레이스를 따라가면 되지만, 이벤트 기반은 어디서 뭐가 발행되고 소비됐는지 추적 도구(분산 트레이싱)가 필요하다
판단 기준: 서비스가 3개 이하면 RabbitMQ나 Redis Streams로 충분하다. 이벤트 리플레이가 필요하거나, 서비스 수가 많거나, 데이터 파이프라인까지 고려한다면 Kafka.
Kafka 도입의 단계적 기준이 필요하다면 이벤트 기반 아키텍처 입문을 참고하라. BullMQ → Redis Streams → NATS JetStream → Kafka 순의 의사결정 흐름을 다룬다. MSA를 운영하는 팀이어도 서비스가 아직 소수라면 BullMQ나 Redis Streams로 충분한 경우가 많다.
인증과 인가: Keycloak과 IDP
마이크로서비스에서 인증이 복잡해지는 이유
모놀리식에서는 세션 하나로 끝난다. 그런데 MSA에서는 서비스마다 인증 로직을 넣을 수 없다. 그리고 여러 서비스가 “이 사용자가 누구인지”를 알아야 한다.
이걸 해결하는 패턴이 중앙 집중식 Identity Provider(IDP)다.
Keycloak의 역할
Keycloak은 오픈소스 IAM(Identity and Access Management) 솔루션이다. 하는 일은:
- SSO(Single Sign-On). 한 번 로그인하면 모든 서비스에 접근 가능
- OAuth 2.0 / OIDC 표준 프로토콜로 JWT 토큰 발급
- 사용자 관리. 회원가입, 비밀번호 정책, 2FA 등을 Keycloak이 처리
- 역할 기반 접근 제어(RBAC). 사용자에게 역할을 부여하고, 서비스는 역할만 확인
인증 흐름을 간단히 정리하면:
- 클라이언트가 Keycloak에 로그인 → JWT 토큰 획득
- 이후 요청에 JWT를 포함
- API Gateway가 토큰을 검증 (Keycloak의 공개키로 서명 확인)
- 각 서비스는 JWT의 claims(역할, 권한)만 보고 인가 처리
서비스가 Keycloak에 매번 질의할 필요가 없다. JWT 자체에 정보가 담겨 있으니까. 이게 stateless 인증의 핵심이다.
트레이드오프
| 자체 구현 | Keycloak | |
|---|---|---|
| 초기 비용 | 코드 작성 필요 | 설정 + 배포 |
| 유지보수 | 인증 로직 직접 관리 | Keycloak 업데이트 관리 |
| 기능 범위 | 필요한 것만 구현 | SSO, 2FA, 소셜 로그인 등 내장 |
| 리소스 | 가벼움 | JVM 기반이라 메모리를 꽤 먹는다 (개발 512MB~, 프로덕션 권장 1~2GB) |
| 커스터마이징 | 자유도 높음 | 테마/SPI로 확장 가능하지만 제한적 |
판단 기준: 서비스가 하나이고 로그인만 필요하면 자체 구현이 낫다. 서비스가 여러 개이고 SSO가 필요하거나, 소셜 로그인/2FA/RBAC 같은 기능이 필요하다면 Keycloak. 다만 JVM 기반이라 리소스를 고려해야 한다. 가볍게 가고 싶으면 Zitadel이나 Ory 같은 대안도 있다.
전체 구조
graph TD
KC[Keycloak
IDP / SSO]
CL[Client]
GW[API Gateway
Go / REST]
SVC[Services
Go / gRPC]
KF[Kafka
Events]
CL -->|JWT 요청| KC
KC -->|JWT 발급| CL
CL -->|REST + JWT| GW
GW -->|JWT 검증| GW
GW -->|gRPC| SVC
GW -->|이벤트 발행| KF
SVC -->|이벤트 발행·소비| KF - Client → Keycloak: JWT 발급 요청
- Client → API Gateway: REST + JWT (외부 호환성)
- API Gateway: JWT 공개키로 토큰 검증 후 요청 전달
- API Gateway → Services: gRPC (내부 효율)
- Services ↔ Kafka: 이벤트 기반 비동기 통신
실제로 고민해야 할 것들
서비스 경계를 어떻게 나눌 것인가
기술 스택보다 중요한 건 서비스를 어떤 단위로 나눌 것인가다. 이건 결국 모듈화의 문제다. 객체지향에서 클래스의 경계를 어떻게 잡을 것인가와 본질적으로 같다. 응집도는 높이고, 결합도는 낮추는 것.
“사용자”, “주문”, “결제”처럼 도메인 단위로 나누는 게 원칙이다. 각 서비스는 자기 데이터를 소유하고, 명확한 인터페이스(gRPC proto)를 통해서만 외부와 소통한다. 객체지향의 캡슐화를 서비스 레벨로 확장한 것이다.
실용적인 접근: 모놀리식으로 시작하되 내부를 모듈로 잘 나눠두고, 분리가 필요한 시점에 마이크로서비스로 추출한다. 트래픽이나 조직 규모가 뒷받침되지 않는데 처음부터 MSA로 가는 건 대부분의 팀에게 과한 선택이다.
분산 트레이싱
gRPC + Kafka 조합에서는 요청 하나가 여러 서비스를 거치고, 이벤트로 비동기 전파된다. 문제가 생겼을 때 원인을 추적하려면 OpenTelemetry 같은 분산 트레이싱이 필수다.
데이터 일관성
각 서비스가 자기 DB를 갖는 게 MSA의 원칙이다. 그런데 “주문과 결제 데이터가 일관성이 있어야 한다”면? 분산 트랜잭션(2PC)은 현실적으로 어렵고, Saga 패턴으로 보상 트랜잭션을 설계하는 게 일반적이다. 이건 설계 난이도가 꽤 높다.
감수해야 할 것들
Go + gRPC + Kafka + Keycloak은 MSA에서 검증된 조합이다. 하지만 각 기술이 가져오는 복잡성을 무시하면 안 된다.
gRPC를 쓰면 바이너리 프로토콜이라 curl 한 줄로 테스트할 수 없다. grpcurl 같은 별도 도구가 필요하고, 네트워크 패킷을 뜯어볼 때도 JSON처럼 바로 읽히지 않는다. 성능과 타입 안전성을 얻는 대신, 디버깅의 편의를 내놓는 셈이다.
Kafka를 쓰면 최종 일관성(eventual consistency)을 감수해야 한다. 주문을 넣었는데 재고 차감이 수 초 뒤에 반영되는 상황. 사용자에게 “처리 중”을 보여주는 UX 설계가 필수이고, 메시지 순서 보장이나 중복 처리 같은 문제도 직접 다뤄야 한다.
Keycloak은 JVM 위에서 돌아간다. 유휴 상태에서도 메모리를 수백 MB 잡아먹는다. Go 서비스들이 수십 MB 단위로 돌아가는 환경에서 인증 서버 하나가 가장 큰 리소스를 차지하는 아이러니가 생긴다.
참고 자료
- gRPC 공식 문서: 프로토콜 개념, 언어별 구현 가이드
- Protocol Buffers 문서: proto3 문법과 인코딩 규격
- Apache Kafka 공식 문서: 브로커 구조, 컨슈머 그룹, KRaft
- Keycloak 공식 문서: 설치, Realm 설정, OIDC/SAML 연동
- OAuth 2.0 RFC 6749: OAuth 2.0 Authorization Framework 표준
- OpenTelemetry for Go: Go 서비스에 분산 트레이싱 적용
- 백엔드 개발자를 위한 쿠버네티스 (1): K8s 기반 인프라에서 Secret 운용 시 보안 모델 확인 필수 — base64 인코딩 한계, etcd 암호화, RBAC 설정