본문으로 건너뛰기
0%

실전 마이크로서비스 아키텍처: Kubernetes로 확장 가능한 시스템 구축하기

자율주행 차량 플랫폼 개발 경험을 바탕으로 한 실용적인 마이크로서비스 설계와 운영 가이드

마이크로서비스 여행기: 삽질과 깨달음의 1년

안녕하세요! 오늘은 지난 1년간 제가 겪었던 마이크로서비스 도입 여정을 솔직하게 풀어보려고 해요. 성공담보다는 실패담이 더 많을 수도 있는데, 그래도 누군가에게는 도움이 될 것 같아서 용기를 내어 써봅니다.

시작은 이랬어요: “뭔가 한계에 부딪혔다”

2년 전쯤 우리 팀이 만든 차량 데이터 플랫폼이 점점 커지면서 정말 답답한 상황들이 생기기 시작했어요:

  • 배포 한 번 하는데 30분: 커피 마시고 와도 아직 배포 중이더라고요 😅
  • 한 부분 오류나면 전체 다운: 새벽에 장애 전화 받는 게 일상이 되었어요
  • 다른 팀과 일정 맞추기: “저희 팀 개발 끝나면 배포해주세요”라는 말을 너무 자주 들었죠

처음엔 “이 정도면 괜찮지 않나?”라고 생각했는데, 팀이 커지고 서비스가 복잡해지면서 한계를 확실히 느끼게 되었어요. 그래서 마이크로서비스로 전환하기로 결정했습니다.

첫 번째 큰 실수: 잘못된 서비스 분할

기술적으로 나누면 안 된다는걸 뒤늦게 깨달았어요

마이크로서비스 도입을 결정하고 가장 먼저 한 일이 “어떻게 서비스를 나눌까?”였는데, 처음엔 정말 단순하게 생각했어요:

처음에 이렇게 나눴어요 (지금 생각해보면 정말 잘못된 방법):

database-service (DB 관련 모든 것)
api-service (API 관련 모든 것)
auth-service (인증 관련)  notification-service (알림 관련)

몇 개월 써보니까 정말 답답하더라고요. 하나의 기능을 추가하려면 여러 서비스를 동시에 수정해야 하고, 결국 배포도 함께 해야 하는 상황이 반복되었어요.

그래서 다시 이렇게 바꿨어요 (비즈니스 중심으로):

vehicle-management (차량 관리의 모든 것)
trip-analytics (여행 분석 관련)
user-profiles (사용자 프로필)
billing-payments (결제 관련)

이렇게 바꾸고 나니까 각 팀이 독립적으로 개발할 수 있게 되었어요. 정말 큰 차이였습니다!

실제 도메인 분할 사례

// 자율주행 플랫폼의 도메인 분할
interface DomainBoundaries {  vehicleFleet: {  responsibilities: ['차량 등록', '상태 모니터링', '펌웨어 관리'];  dataOwnership: ['vehicles', 'sensors', 'diagnostics'];  apis: ['/vehicles', '/fleet/status', '/diagnostics'];  };  tripManagement: {  responsibilities: ['여행 생성', '경로 최적화', '실시간 추적'];  dataOwnership: ['trips', 'routes', 'locations'];  apis: ['/trips', '/routes', '/tracking'];  };  userExperience: {  responsibilities: ['사용자 인터페이스', '알림', '피드백'];  dataOwnership: ['users', 'preferences', 'feedback'];  apis: ['/users', '/notifications', '/feedback'];  };
}

데이터 일관성 전략

이벤트 소싱 패턴 구현

// 이벤트 기반 데이터 동기화
interface DomainEvent {  eventId: string;  aggregateId: string;  eventType: string;  timestamp: Date;  version: number;  data: any;
}

class VehicleEventHandler {  async handleTripCompleted(event: DomainEvent) {  const { tripId, vehicleId, mileage, fuelConsumption } = event.data;  // 1. Vehicle Service: 차량 상태 업데이트  await this.vehicleService.updateMileage(vehicleId, mileage);  // 2. Analytics Service: 주행 데이터 저장  await this.analyticsService.recordTripData({  tripId, vehicleId, mileage, fuelConsumption  });  // 3. Billing Service: 요금 계산 이벤트 발행  await this.eventPublisher.publish('billing.calculate', {  tripId, mileage, userId: event.data.userId  });  }
}

SAGA 패턴으로 분산 트랜잭션 관리

# trip-booking-saga.yml
saga:  name: "TripBookingSaga"  steps:  - service: "user-service"  action: "reserve-credits"  compensate: "release-credits"  - service: "vehicle-service"  action: "reserve-vehicle"  compensate: "release-vehicle"  - service: "trip-service"  action: "create-trip"  compensate: "cancel-trip"  - service: "notification-service"  action: "send-confirmation"  compensate: "send-cancellation"

Kubernetes 클러스터 구성

️ 인프라 아키�ecture

# cluster-architecture.yml
apiVersion: v1
kind: ConfigMap
metadata:  name: cluster-config
data:  # 프로덕션 클러스터 구성  nodes: |  master-nodes: 3 (HA 구성)  worker-nodes: 12 (auto-scaling)  resources:  cpu: "48 cores per node"  memory: "192GB per node"  storage: "2TB NVMe SSD"  networking:  cni: "Calico"  service-mesh: "Istio"  ingress: "NGINX + Cert-Manager"

서비스별 배포 설정

1. 고가용성 서비스 (Vehicle Management)

# vehicle-service-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:  name: vehicle-service  labels:  app: vehicle-service  version: v2.1.3
spec:  replicas: 5  strategy:  type: RollingUpdate  rollingUpdate:  maxSurge: 2  maxUnavailable: 1  selector:  matchLabels:  app: vehicle-service  template:  metadata:  labels:  app: vehicle-service  version: v2.1.3  spec:  containers:  - name: vehicle-service  image: myregistry/vehicle-service:v2.1.3  ports:  - containerPort: 8080  env:  - name: DATABASE_URL  valueFrom:  secretKeyRef:  name: db-credentials  key: url  - name: REDIS_URL  valueFrom:  configMapKeyRef:  name: cache-config  key: redis-url  resources:  requests:  memory: "512Mi"  cpu: "250m"  limits:  memory: "1Gi"  cpu: "500m"  livenessProbe:  httpGet:  path: /health  port: 8080  initialDelaySeconds: 30  periodSeconds: 10  readinessProbe:  httpGet:  path: /ready  port: 8080  initialDelaySeconds: 15  periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:  name: vehicle-service
spec:  selector:  app: vehicle-service  ports:  - port: 80  targetPort: 8080  type: ClusterIP

2. HPA (Horizontal Pod Autoscaler) 설정

# vehicle-service-hpa.yml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:  name: vehicle-service-hpa
spec:  scaleTargetRef:  apiVersion: apps/v1  kind: Deployment  name: vehicle-service  minReplicas: 3  maxReplicas: 20  metrics:  - type: Resource  resource:  name: cpu  target:  type: Utilization  averageUtilization: 70  - type: Resource  resource:  name: memory  target:  type: Utilization  averageUtilization: 80  - type: Pods  pods:  metric:  name: requests_per_second  target:  type: AverageValue  averageValue: "100"

CI/CD 파이프라인

GitOps 기반 배포 자동화

# .github/workflows/deploy.yml
name: Deploy to Kubernetes
on:  push:  branches: [main]  jobs:  test:  runs-on: ubuntu-latest  steps:  - uses: actions/checkout@v3  - name: Run Tests  run: |  npm test  npm run test:integration  security-scan:  runs-on: ubuntu-latest  steps:  - name: Security Scan  run: |  docker run --rm -v $(pwd):/app \  aquasec/trivy fs /app  deploy:  needs: [test, security-scan]  runs-on: ubuntu-latest  steps:  - name: Deploy to Staging  run: |  kubectl apply -f k8s/staging/  kubectl wait --for=condition=ready pod \  -l app=vehicle-service --timeout=300s  - name: Run E2E Tests  run: |  npm run test:e2e:staging  - name: Deploy to Production  if: success()  run: |  kubectl apply -f k8s/production/  kubectl rollout status deployment/vehicle-service

서비스 메시(Service Mesh) 구현

️ Istio로 트래픽 관리

카나리 배포 설정

# canary-deployment.yml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:  name: vehicle-service-canary
spec:  hosts:  - vehicle-service  http:  - match:  - headers:  canary:  exact: "true"  route:  - destination:  host: vehicle-service  subset: v2  - route:  - destination:  host: vehicle-service  subset: v1  weight: 90  - destination:  host: vehicle-service  subset: v2  weight: 10
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:  name: vehicle-service-destination
spec:  host: vehicle-service  subsets:  - name: v1  labels:  version: v2.1.2  - name: v2  labels:  version: v2.1.3  trafficPolicy:  loadBalancer:  simple: LEAST_CONN

회로 차단기(Circuit Breaker) 패턴

# circuit-breaker.yml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:  name: trip-service-circuit-breaker
spec:  host: trip-service  trafficPolicy:  outlierDetection:  consecutiveErrors: 3  interval: 30s  baseEjectionTime: 30s  maxEjectionPercent: 50  connectionPool:  tcp:  maxConnections: 100  http:  http1MaxPendingRequests: 50  maxRequestsPerConnection: 10  maxRetries: 3  connectTimeout: 30s

모니터링과 관찰성(Observability)

통합 모니터링 스택

# monitoring-stack.yml
apiVersion: v1
kind: ConfigMap
metadata:  name: monitoring-config
data:  prometheus.yml: |  global:  scrape_interval: 15s  scrape_configs:  - job_name: 'kubernetes-pods'  kubernetes_sd_configs:  - role: pod  relabel_configs:  - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]  action: keep  regex: true  grafana-dashboard.json: |  {  "dashboard": {  "title": "Microservices Overview",  "panels": [  {  "title": "Request Rate",  "type": "graph",  "targets": [  {  "expr": "rate(http_requests_total[5m])",  "legendFormat": "{{service}}"  }  ]  },  {  "title": "Error Rate",  "type": "singlestat",  "targets": [  {  "expr": "rate(http_requests_total{status=~'5..'}[5m]) / rate(http_requests_total[5m])",  "legendFormat": "Error Rate %"  }  ]  }  ]  }  }

분산 추적(Distributed Tracing)

// OpenTelemetry 계측 예제
import { trace, SpanStatusCode } from '@opentelemetry/api';
import { NodeSDK } from '@opentelemetry/auto-instrumentations-node';

class TripService {  private tracer = trace.getTracer('trip-service');  async createTrip(tripData: CreateTripRequest): Promise<Trip> {  const span = this.tracer.startSpan('create-trip');  try {  span.setAttributes({  'trip.user_id': tripData.userId,  'trip.vehicle_id': tripData.vehicleId,  'trip.route_length': tripData.routeLength  });  // 1. 사용자 크레딧 확인  const creditSpan = this.tracer.startSpan('check-user-credits', { parent: span });  const hasCredits = await this.userService.checkCredits(tripData.userId);  creditSpan.end();  if (!hasCredits) {  span.recordException(new Error('Insufficient credits'));  span.setStatus({ code: SpanStatusCode.ERROR });  throw new InsufficientCreditsError();  }  // 2. 차량 예약  const vehicleSpan = this.tracer.startSpan('reserve-vehicle', { parent: span });  await this.vehicleService.reserveVehicle(tripData.vehicleId);  vehicleSpan.end();  // 3. 여행 생성  const trip = await this.tripRepository.create(tripData);  span.setAttributes({  'trip.id': trip.id,  'trip.status': trip.status  });  span.setStatus({ code: SpanStatusCode.OK });  return trip;  } catch (error) {  span.recordException(error);  span.setStatus({ code: SpanStatusCode.ERROR });  throw error;  } finally {  span.end();  }  }
}

성능 최적화 전략

캐싱 전략

다계층 캐싱 구현

// 스마트 캐싱 시스템
interface CacheStrategy {  l1: 'in-memory'; // 애플리케이션 레벨 (100ms TTL)  l2: 'redis';  // 클러스터 레벨 (5min TTL)  l3: 'database';  // 영구 저장소
}

class VehicleLocationService {  private memoryCache = new Map();  private redisClient: Redis;  async getVehicleLocation(vehicleId: string): Promise<Location> {  // L1: 메모리 캐시 확인  const memCached = this.memoryCache.get(vehicleId);  if (memCached && this.isValid(memCached, 100)) {  return memCached.data;  }  // L2: Redis 캐시 확인  const redisCached = await this.redisClient.get(`location:${vehicleId}`);  if (redisCached) {  const data = JSON.parse(redisCached);  this.memoryCache.set(vehicleId, { data, timestamp: Date.now() });  return data;  }  // L3: 데이터베이스 조회  const location = await this.locationRepository.findByVehicleId(vehicleId);  // 캐시에 저장 (상위 레벨로)  await this.redisClient.setex(`location:${vehicleId}`, 300, JSON.stringify(location));  this.memoryCache.set(vehicleId, { data: location, timestamp: Date.now() });  return location;  }
}

데이터베이스 최적화

-- 인덱스 최적화 예제
-- 복합 인덱스로 조회 성능 개선
CREATE INDEX CONCURRENTLY idx_trips_user_status_date 
ON trips(user_id, status, created_at DESC);

-- 파티셔닝으로 대용량 데이터 관리
CREATE TABLE trips_2024_q1 PARTITION OF trips 
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');

-- 읽기 전용 복제본 활용
-- application.yml
spring:  datasource:  master:  url: jdbc:postgresql://master-db:5432/trips  slave:  url: jdbc:postgresql://readonly-db:5432/trips

오토스케일링 최적화

사용자 정의 메트릭 기반 스케일링

# custom-metrics-hpa.yml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:  name: vehicle-service-custom-hpa
spec:  scaleTargetRef:  apiVersion: apps/v1  kind: Deployment  name: vehicle-service  minReplicas: 3  maxReplicas: 50  metrics:  # 큐 길이 기반 스케일링  - type: External  external:  metric:  name: rabbitmq_queue_messages  selector:  matchLabels:  queue: "vehicle-updates"  target:  type: AverageValue  averageValue: "30"  # 응답 시간 기반 스케일링  - type: Pods  pods:  metric:  name: http_request_duration_95percentile  target:  type: AverageValue  averageValue: "500m"  # 500ms

장애 대응과 복구 전략

장애 시나리오별 대응

1. 서비스 간 통신 장애

// Retry 정책과 Circuit Breaker
import { CircuitBreaker } from 'opossum';

class VehicleApiClient {  private circuitBreaker: CircuitBreaker;  constructor() {  const options = {  timeout: 3000,  errorThresholdPercentage: 50,  resetTimeout: 30000,  rollingCountTimeout: 10000,  rollingCountBuckets: 10  };  this.circuitBreaker = new CircuitBreaker(this.callVehicleAPI.bind(this), options);  // 폴백 전략  this.circuitBreaker.fallback(() => {  return this.getCachedVehicleData() || this.getDefaultVehicleData();  });  }  async getVehicleData(vehicleId: string): Promise<VehicleData> {  try {  return await this.circuitBreaker.fire(vehicleId);  } catch (error) {  // 로깅 및 메트릭 수집  this.logger.error('Vehicle API call failed', { vehicleId, error });  this.metrics.increment('vehicle_api_failure');  throw error;  }  }
}

2. 데이터베이스 장애

# postgresql-ha.yml - 고가용성 DB 설정
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:  name: postgres-cluster
spec:  instances: 3  postgresql:  parameters:  max_connections: "200"  shared_buffers: "256MB"  effective_cache_size: "1GB"  bootstrap:  initdb:  database: trips_db  secret:  name: postgres-credentials  backup:  retention: "30d"  barmanObjectStore:  s3Credentials:  accessKeyId:  name: s3-credentials  key: ACCESS_KEY_ID  secretAccessKey:  name: s3-credentials  key: SECRET_ACCESS_KEY  wal:  retention: "5d"  monitoring:  enabled: true

무중단 배포 전략

Blue-Green 배포

#!/bin/bash
# blue-green-deploy.sh

# 현재 활성 환경 확인
CURRENT=$(kubectl get service vehicle-service -o jsonpath='{.spec.selector.version}')
NEW_VERSION="v2.1.4"

if [ "$CURRENT" = "blue" ]; then  NEW_ENV="green"  OLD_ENV="blue"  else  NEW_ENV="blue"  OLD_ENV="green"
fi

echo "Deploying $NEW_VERSION to $NEW_ENV environment..."

# 1. 새 버전을 비활성 환경에 배포
kubectl set image deployment/vehicle-service-$NEW_ENV \  vehicle-service=myregistry/vehicle-service:$NEW_VERSION

# 2. 배포 완료 대기
kubectl rollout status deployment/vehicle-service-$NEW_ENV --timeout=600s

# 3. 헬스 체크
kubectl wait --for=condition=ready pod \  -l app=vehicle-service,version=$NEW_ENV --timeout=300s

# 4. 스모크 테스트
curl -f http://vehicle-service-$NEW_ENV/health || exit 1

# 5. 트래픽 전환
kubectl patch service vehicle-service \  -p '{"spec":{"selector":{"version":"'$NEW_ENV'"}}}'

echo "Traffic switched to $NEW_ENV environment"

# 6. 5분 후 이전 환경 정리 (선택적)
sleep 300
kubectl scale deployment vehicle-service-$OLD_ENV --replicas=0

운영 경험에서 배운 교훈

성공 요인들

1. 팀 구조와 조직 정렬

graph TD  A[Product Team] --> B[Domain Team 1: Vehicle]  A --> C[Domain Team 2: Trip]  A --> D[Domain Team 3: User]  B --> E[Backend Developer]  B --> F[DevOps Engineer]  B --> G[QA Engineer]  H[Platform Team] --> I[Infrastructure]  H --> J[Monitoring]  H --> K[Security]

Conway’s Law 활용: 조직 구조가 아키텍처를 결정한다는 것을 인정하고 의도적으로 설계

2. 단계적 마이그레이션

// Strangler Fig 패턴으로 점진적 전환
class LegacyTripService {  async createTrip(tripData: TripData): Promise<Trip> {  // Feature Flag로 새/구 시스템 분기  if (this.featureFlag.isEnabled('NEW_TRIP_SERVICE', tripData.userId)) {  return this.newTripService.createTrip(tripData);  } else {  return this.legacyCreateTrip(tripData);  }  }  private async legacyCreateTrip(tripData: TripData): Promise<Trip> {  // 기존 모놀리식 로직  }
}

3. 관찰성 우선 개발

// 메트릭 수집을 코드 작성 시점부터 고려
class PaymentService {  async processPayment(payment: Payment): Promise<PaymentResult> {  const timer = this.metrics.startTimer('payment_processing_duration');  try {  this.metrics.increment('payment_attempts_total', {  payment_method: payment.method,  amount_range: this.getAmountRange(payment.amount)  });  const result = await this.paymentGateway.charge(payment);  this.metrics.increment('payment_success_total');  return result;  } catch (error) {  this.metrics.increment('payment_failure_total', {  error_type: error.constructor.name  });  throw error;  } finally {  timer.end();  }  }
}

실패에서 배운 교훈

1. 너무 작은 서비스의 함정

문제: 과도한 네트워크 호출과 복잡한 오케스트레이션

// ❌ 너무 세분화된 서비스
interface MicroServices {  userIdService: 'generates user IDs';  userNameService: 'manages user names';  userEmailService: 'handles user emails';  userPhoneService: 'manages phone numbers';
}

// ✅ 적절한 크기의 서비스
interface RightSizedServices {  userManagementService: 'complete user lifecycle';  authenticationService: 'login, logout, tokens';  profileService: 'user preferences, settings';
}

2. 분산 모놀리스의 위험

분산되어 있지만 여전히 강하게 결합된 시스템:

  • 서비스 간 동기 호출 체인
  • 공유 데이터베이스
  • 동시 배포 필요성

해결책: 이벤트 기반 아키텍처와 CQRS 패턴 도입

3. 테스트 전략의 중요성

// 컨트랙트 테스트로 서비스 간 호환성 보장
describe('Vehicle Service Contract', () => {  it('should return vehicle data in expected format', async () => {  const pact = new Pact({  consumer: 'trip-service',  provider: 'vehicle-service'  });  await pact  .given('vehicle exists')  .uponReceiving('a request for vehicle data')  .withRequest({  method: 'GET',  path: '/vehicles/123'  })  .willRespondWith({  status: 200,  headers: { 'Content-Type': 'application/json' },  body: {  id: like('123'),  status: like('available'),  location: {  lat: like(37.5665),  lng: like(126.9780)  }  }  });  const vehicle = await vehicleClient.getVehicle('123');  expect(vehicle).toHaveProperty('id');  expect(vehicle).toHaveProperty('status');  });
});

성과 측정과 지속적 개선

핵심 지표 (KPIs)

기술적 지표

지표목표현재개선도
배포 빈도일 5회일 8회✅ +60%
배포 리드타임30분8분✅ -73%
MTTR (평균 복구 시간)1시간15분✅ -75%
변경 실패율<5%2.3%✅ -54%

비즈니스 지표

  • 서비스 가용성: 99.97% → 99.99%
  • 응답 시간: P95 500ms → 150ms
  • 동시 사용자: 1만명 → 10만명 처리 가능

지속적 개선 프로세스

# 월간 회고 프로세스
retrospective:  what_went_well:  - "카나리 배포로 장애 최소화"  - "모니터링 대시보드로 빠른 문제 파악"  what_needs_improvement:  - "크로스팀 의존성 해결"  - "테스트 자동화 범위 확대"  action_items:  - name: "이벤트 기반 통신 확대"  owner: "architecture-team"  due_date: "2025-02-28"  - name: "E2E 테스트 커버리지 80% 달성"  owner: "qa-team"  due_date: "2025-02-15"

앞으로의 계획

다음 단계 로드맵

Q1 2025: Platform Engineering 강화

  • Developer Portal 구축 (Backstage 기반)
  • 셀프서비스 인프라 도입
  • 개발자 생산성 메트릭 수집

Q2 2025: AI/ML 통합

  • 예측적 오토스케일링 (머신러닝 기반)
  • 이상 탐지 자동화
  • 성능 최적화 AI 어시스턴트

Q3-Q4 2025: 글로벌 확장

  • 멀티 리전 아키텍처
  • 지리적 데이터 분산
  • 엣지 컴퓨팅 도입

마무리: 실무 적용 가이드

단계별 체크리스트

설계 단계

  • 도메인 경계 명확히 정의
  • 데이터 소유권 분할
  • 통신 패턴 결정 (동기/비동기)
  • 장애 시나리오 계획

개발 단계 - [ ] API 버전 관리 전략

  • 로깅/메트릭 표준화
  • 컨트랙트 테스트 작성
  • 보안 정책 적용

배포 단계

  • CI/CD 파이프라인 구축
  • 카나리/블루-그린 배포
  • 모니터링 대시보드 설정
  • 알림 규칙 정의

운영 단계

  • SLA/SLO 정의
  • 장애 대응 플레이북
  • 정기적 재해 복구 훈련
  • 성능 튜닝 및 최적화

정말로 중요한 것들 (제가 깨달은 것들)

1년 동안 삽질하면서 깨달은 가장 중요한 것들을 정리해봤어요:

  1. 한 번에 다 바꾸려 하지 마세요: 저희도 처음엔 욕심을 부렸다가 정말 고생했어요. 하나씩 천천히 바꿔가는 게 훨씬 안전해요
  2. 모니터링부터 하세요: 뭔가 문제가 생겼을 때 원인을 찾을 수 없으면 정말 막막해져요. 로그와 메트릭 수집은 필수입니다
  3. 팀 구조도 같이 바뀌어야 해요: 조직 구조와 시스템 구조가 닮아간다는 Conway’s Law가 정말 맞더라고요
  4. 자동화에 투자하세요: 서비스가 많아지면 수동으로는 절대 관리할 수 없어요

솔직한 고백

마이크로서비스가 모든 문제를 해결해주는 마법의 해답은 아니에요. 오히려 새로운 복잡성을 가져다주기도 하고, 때로는 모놀리식이 더 나은 선택일 수도 있어요.

하지만 제대로만 도입하면 정말 개발팀의 생산성과 시스템 안정성이 크게 올라갈 수 있어요. 저희 팀도 힘들었지만 지금은 정말 만족하고 있거든요.

혹시 마이크로서비스 도입을 고민하고 계신다면, 언제든지 질문 주세요. 제가 겪었던 삽질들을 미리 알려드릴 수 있을 것 같아요! 😊


다음에는 이런 이야기들을 나눠볼게요:

  • Kubernetes 운영하면서 배운 성능 최적화 팁들 (다음주 계획)
  • 이벤트 기반 아키텍처, 정말 필요한 건지에 대한 고민 (2월쯤)

더 이야기하고 싶으시면:

댓글 남기기

여러분의 생각을 들려주세요

댓글

GitHub 계정으로 로그인하여 댓글을 남겨보세요. 건설적인 의견과 질문을 환영합니다!

댓글을 불러오는 중...

목차