이 글에서 얻는 것

  • 마이크로서비스 핵심 패턴을 이해하고 적용 시점을 판단합니다
  • Service DiscoveryService Mesh 차이를 알아봅니다
  • BFF(Backend for Frontend) 패턴으로 클라이언트별 최적화를 합니다

Service Discovery

왜 필요한가?

flowchart TB
    subgraph "❌ 정적 설정의 문제"
        C1[Client] -->|"order-service:8080"| O1[Order Service]
        O1 -->|"payment-service:8081"| P1[Payment Service]
    end
    
    Note["서비스 IP/포트 변경 시\n모든 클라이언트 설정 수정 필요"]

마이크로서비스 환경에서는:

  • 서비스 인스턴스가 동적으로 생성/삭제
  • 오토스케일링으로 인스턴스 수 변동
  • 컨테이너 재시작 시 IP 변경

Service Discovery 패턴

flowchart LR
    subgraph "Client-side Discovery"
        C1[Client] --> R1[(Registry)]
        R1 -->|"인스턴스 목록"| C1
        C1 -->|"직접 호출"| S1[Service A]
        C1 -->|"직접 호출"| S2[Service A]
    end
flowchart LR
    subgraph "Server-side Discovery"
        C2[Client] --> LB[Load Balancer]
        LB --> R2[(Registry)]
        LB --> S3[Service A]
        LB --> S4[Service A]
    end
패턴장점단점예시
Client-side직접 제어, 낮은 지연클라이언트 복잡도Netflix Eureka
Server-side클라이언트 단순화LB가 병목 가능AWS ALB, K8s Service

Spring Cloud + Eureka 예시

// Eureka Server
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

// Eureka Client (서비스 등록)
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

// 서비스 호출 (FeignClient)
@FeignClient(name = "payment-service")  // 서비스 이름으로 호출
public interface PaymentClient {
    @PostMapping("/api/payments")
    PaymentResponse processPayment(@RequestBody PaymentRequest request);
}

Sidecar 패턴

개념

flowchart TB
    subgraph Pod["Pod / Container Group"]
        App[Application]
        Sidecar[Sidecar Proxy]
        App <-->|localhost| Sidecar
    end
    
    Sidecar <-->|"mTLS, Retry,\nCircuit Breaker"| Network[Network]
    
    style Sidecar fill:#e3f2fd,stroke:#1565c0

Sidecar: 애플리케이션과 함께 배포되어 횡단 관심사를 처리

  • 로깅/모니터링: Fluentd, Prometheus Agent
  • 프록시: Envoy, Linkerd
  • 보안: mTLS 처리
  • 트래픽 관리: Retry, Circuit Breaker

Service Mesh

flowchart TB
    subgraph "Service Mesh (Istio)"
        subgraph Pod1["Order Service Pod"]
            O[Order App]
            E1[Envoy Proxy]
            O <--> E1
        end
        
        subgraph Pod2["Payment Service Pod"]
            P[Payment App]
            E2[Envoy Proxy]
            P <--> E2
        end
        
        E1 <-->|mTLS| E2
        
        CP[Control Plane\nIstiod]
        CP -->|Config| E1
        CP -->|Config| E2
    end
    
    style E1 fill:#e3f2fd,stroke:#1565c0
    style E2 fill:#e3f2fd,stroke:#1565c0
    style CP fill:#fff3e0,stroke:#ef6c00

Service Mesh 기능:

  • 트래픽 관리: Canary 배포, A/B 테스트
  • 보안: mTLS 자동화, 인증/인가
  • 관측성: 분산 트레이싱, 메트릭 수집
  • 복원력: Retry, Timeout, Circuit Breaker

Sidecar vs Library

비교Sidecar (Envoy)Library (Resilience4j)
언어 독립✅ 모든 언어❌ 특정 언어
업데이트앱 재배포 없이앱 재배포 필요
리소스추가 메모리/CPU앱 내부 사용
디버깅복잡단순
적합 케이스다양한 언어, 큰 조직단일 언어, 작은 팀

BFF (Backend for Frontend)

문제: 클라이언트별 다른 요구사항

flowchart TB
    subgraph "❌ 단일 API의 문제"
        Mobile[Mobile App]
        Web[Web App]
        
        Mobile --> API[General API]
        Web --> API
        
        API --> US[User Service]
        API --> OS[Order Service]
        API --> PS[Product Service]
    end
    
    Note["Mobile: 최소 데이터 필요\nWeb: 풍부한 데이터 필요\n→ Over-fetching / Under-fetching"]

해결: 클라이언트별 BFF

flowchart TB
    Mobile[Mobile App] --> MBFF[Mobile BFF]
    Web[Web App] --> WBFF[Web BFF]
    
    MBFF --> US[User Service]
    MBFF --> OS[Order Service]
    
    WBFF --> US
    WBFF --> OS
    WBFF --> PS[Product Service]
    WBFF --> RS[Review Service]
    
    style MBFF fill:#e8f5e9,stroke:#2e7d32
    style WBFF fill:#e3f2fd,stroke:#1565c0

구현 예시

// Mobile BFF - 최소한의 데이터
@RestController
@RequestMapping("/mobile/api")
public class MobileBffController {
    
    @GetMapping("/orders/{orderId}")
    public MobileOrderResponse getOrder(@PathVariable String orderId) {
        Order order = orderService.getOrder(orderId);
        
        // Mobile에 필요한 최소 정보만 반환
        return MobileOrderResponse.builder()
            .orderId(order.getId())
            .status(order.getStatus())
            .totalAmount(order.getTotalAmount())
            .build();
    }
}

// Web BFF - 상세 데이터
@RestController
@RequestMapping("/web/api")
public class WebBffController {
    
    @GetMapping("/orders/{orderId}")
    public WebOrderResponse getOrder(@PathVariable String orderId) {
        Order order = orderService.getOrder(orderId);
        User user = userService.getUser(order.getUserId());
        List<Product> products = productService.getProducts(order.getProductIds());
        List<Review> reviews = reviewService.getReviewsForProducts(order.getProductIds());
        
        // Web에 필요한 풍부한 정보 반환
        return WebOrderResponse.builder()
            .order(order)
            .user(user)
            .products(products)
            .reviews(reviews)
            .shippingDetails(order.getShipping())
            .paymentHistory(paymentService.getHistory(orderId))
            .build();
    }
}

BFF 장단점

장점단점
클라이언트 최적화BFF 수 증가 (관리 비용)
백엔드 변경 격리코드 중복 가능성
팀별 독립 개발배포 복잡도 증가

Strangler Fig 패턴

레거시 시스템 점진적 마이그레이션

flowchart TB
    subgraph "Phase 1: Proxy 추가"
        C1[Client] --> P1[Proxy]
        P1 --> L1[Legacy System]
    end
flowchart TB
    subgraph "Phase 2: 일부 기능 마이그레이션"
        C2[Client] --> P2[Proxy]
        P2 -->|"/orders"| MS[New Microservice]
        P2 -->|"나머지"| L2[Legacy System]
    end
    
    style MS fill:#e8f5e9,stroke:#2e7d32
flowchart TB
    subgraph "Phase 3: 완전 마이그레이션"
        C3[Client] --> P3[Proxy]
        P3 --> MS1[Order Service]
        P3 --> MS2[User Service]
        P3 --> MS3[Payment Service]
        
        L3[Legacy System]
        L3 -.->|"제거"| X[❌]
    end
    
    style MS1 fill:#e8f5e9,stroke:#2e7d32
    style MS2 fill:#e8f5e9,stroke:#2e7d32
    style MS3 fill:#e8f5e9,stroke:#2e7d32
    style L3 fill:#ffebee,stroke:#c62828

구현 전략

# Nginx 기반 라우팅 예시
upstream legacy {
    server legacy-monolith:8080;
}

upstream new_orders {
    server order-service:8080;
}

server {
    listen 80;
    
    # 새 서비스로 라우팅
    location /api/orders {
        proxy_pass http://new_orders;
    }
    
    # 나머지는 레거시로
    location / {
        proxy_pass http://legacy;
    }
}

요약

패턴 선택 가이드

패턴언제 사용
Service Discovery동적 스케일링, 컨테이너 환경
Sidecar횡단 관심사 분리, 다양한 언어
Service Mesh대규모 마이크로서비스, 복잡한 네트워크
BFF다양한 클라이언트, 최적화 필요
Strangler Fig레거시 마이그레이션