제20강: 대규모 프로젝트 아키텍처

AI와 함께 설계하는 확장 가능한 시스템 아키텍처

난이도: 중급 예상 시간: 65분 카테고리: 중급

학습 목표

  • AI를 활용한 시스템 아키텍처 설계 원칙 이해하기
  • 마이크로서비스 vs 모놀리스 선택과 전환 전략
  • 이벤트 주도 아키텍처와 CQRS 패턴 구현하기
  • 도메인 주도 설계(DDD)와 헥사고날 아키텍처 적용하기
  • 분산 시스템의 복잡성 관리와 모니터링 구축하기

시스템 아키텍처 설계

Cursor AI는 프로젝트의 요구사항을 분석하여 최적의 아키텍처를 제안합니다. 확장성, 유지보수성, 성능을 모두 고려한 설계를 자동으로 생성합니다.

이커머스 플랫폼 아키텍처

요구사항: 대규모 이커머스 플랫폼 설계

AI에게 요청: "일일 100만 주문을 처리할 수 있는 이커머스 아키텍처를 설계해줘"

AI가 제안한 시스템 아키텍처

// architecture/system-design.md
# 대규모 이커머스 플랫폼 아키텍처

## 1. 전체 시스템 구조

```mermaid
graph TB
    subgraph "Client Layer"
        WEB[Web App]
        MOBILE[Mobile App]
        ADMIN[Admin Portal]
    end
    
    subgraph "API Gateway Layer"
        KONG[Kong API Gateway]
        AUTH[Auth Service]
    end
    
    subgraph "Service Layer"
        USER[User Service]
        PRODUCT[Product Service]
        ORDER[Order Service]
        PAYMENT[Payment Service]
        INVENTORY[Inventory Service]
        NOTIFICATION[Notification Service]
        SEARCH[Search Service]
        RECOMMENDATION[Recommendation Service]
    end
    
    subgraph "Message Layer"
        KAFKA[Apache Kafka]
        REDIS_PUBSUB[Redis Pub/Sub]
    end
    
    subgraph "Data Layer"
        POSTGRES[(PostgreSQL)]
        MONGODB[(MongoDB)]
        ELASTICSEARCH[(Elasticsearch)]
        REDIS[(Redis Cache)]
        S3[AWS S3]
    end
    
    subgraph "Infrastructure"
        K8S[Kubernetes]
        PROMETHEUS[Prometheus]
        GRAFANA[Grafana]
        ELK[ELK Stack]
    end
```

## 2. 서비스별 상세 설계

### 2.1 User Service
- **책임**: 사용자 인증/인가, 프로필 관리
- **기술 스택**: Node.js, PostgreSQL, Redis
- **API 설계**:
  ```yaml
  /api/v1/users:
    post:
      summary: 사용자 등록
    get:
      summary: 사용자 목록 조회
  
  /api/v1/users/{userId}:
    get:
      summary: 사용자 정보 조회
    put:
      summary: 사용자 정보 수정
    delete:
      summary: 사용자 삭제
  
  /api/v1/auth/login:
    post:
      summary: 로그인
  
  /api/v1/auth/refresh:
    post:
      summary: 토큰 갱신
  ```

### 2.2 Order Service
- **책임**: 주문 생성, 처리, 상태 관리
- **기술 스택**: Java Spring Boot, PostgreSQL, Kafka
- **이벤트 기반 처리**:
  - OrderCreated
  - OrderPaid
  - OrderShipped
  - OrderDelivered
  - OrderCancelled

### 2.3 Payment Service
- **책임**: 결제 처리, 환불, 정산
- **기술 스택**: Go, PostgreSQL, Redis
- **외부 연동**: Stripe, PayPal, 국내 PG사

## 3. 데이터베이스 설계

### 3.1 분산 데이터 관리
- **CQRS 패턴 적용**
  - Command: PostgreSQL (트랜잭션 보장)
  - Query: Elasticsearch (빠른 조회)
  
### 3.2 데이터베이스 샤딩
- User DB: user_id 기반 샤딩
- Order DB: 날짜 기반 파티셔닝
- Product DB: category_id 기반 샤딩

## 4. 확장성 전략

### 4.1 수평 확장
- Kubernetes HPA 기반 자동 스케일링
- 서비스별 독립적 확장 가능
- 데이터베이스 읽기 복제본 활용

### 4.2 캐싱 전략
- L1 Cache: 애플리케이션 메모리 (LRU)
- L2 Cache: Redis Cluster
- CDN: 정적 자원 및 API 응답 캐싱

## 5. 안정성 보장

### 5.1 서킷 브레이커
- Hystrix/Resilience4j 적용
- 장애 전파 방지
- 우아한 성능 저하

### 5.2 재시도 및 타임아웃
- Exponential Backoff
- Dead Letter Queue
- 비동기 처리

## 6. 모니터링 및 관찰성

### 6.1 메트릭
- Prometheus + Grafana
- 커스텀 비즈니스 메트릭
- SLI/SLO 대시보드

### 6.2 로깅
- ELK Stack (Elasticsearch, Logstash, Kibana)
- 구조화된 로깅
- 분산 추적 (Jaeger)

### 6.3 알림
- PagerDuty 연동
- Slack 알림
- 자동 인시던트 생성

마이크로서비스 구현 예시

// services/order-service/src/main/java/com/ecommerce/order/OrderService.java
@Service
@Slf4j
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;
    private final InventoryServiceClient inventoryClient;
    private final PaymentServiceClient paymentClient;
    private final KafkaTemplate kafkaTemplate;
    private final CircuitBreaker circuitBreaker;
    
    @Transactional
    public OrderResponse createOrder(CreateOrderRequest request) {
        log.info("Creating order for user: {}", request.getUserId());
        
        // 1. 재고 확인 (Circuit Breaker 적용)
        boolean stockAvailable = circuitBreaker.executeSupplier(() -> 
            inventoryClient.checkStock(request.getItems())
        );
        
        if (!stockAvailable) {
            throw new InsufficientStockException("Some items are out of stock");
        }
        
        // 2. 주문 생성
        Order order = Order.builder()
            .userId(request.getUserId())
            .items(mapToOrderItems(request.getItems()))
            .status(OrderStatus.PENDING)
            .totalAmount(calculateTotal(request.getItems()))
            .build();
        
        Order savedOrder = orderRepository.save(order);
        
        // 3. 재고 예약 (Saga 패턴)
        CompletableFuture inventoryReservation = CompletableFuture.runAsync(() -> {
            try {
                inventoryClient.reserveStock(savedOrder.getId(), request.getItems());
            } catch (Exception e) {
                log.error("Failed to reserve inventory", e);
                compensateOrder(savedOrder.getId());
                throw new OrderProcessingException("Inventory reservation failed");
            }
        });
        
        // 4. 결제 처리
        CompletableFuture paymentProcessing = CompletableFuture.supplyAsync(() -> {
            try {
                return paymentClient.processPayment(
                    savedOrder.getId(),
                    savedOrder.getTotalAmount(),
                    request.getPaymentMethod()
                );
            } catch (Exception e) {
                log.error("Payment processing failed", e);
                compensateInventory(savedOrder.getId());
                compensateOrder(savedOrder.getId());
                throw new PaymentException("Payment failed");
            }
        });
        
        // 5. 비동기 처리 완료 대기
        try {
            CompletableFuture.allOf(inventoryReservation, paymentProcessing).join();
            PaymentResult paymentResult = paymentProcessing.get();
            
            // 6. 주문 상태 업데이트
            savedOrder.setStatus(OrderStatus.CONFIRMED);
            savedOrder.setPaymentId(paymentResult.getTransactionId());
            orderRepository.save(savedOrder);
            
            // 7. 이벤트 발행
            publishOrderEvent(new OrderCreatedEvent(savedOrder));
            
            return OrderResponse.from(savedOrder);
            
        } catch (Exception e) {
            // 보상 트랜잭션
            compensateAll(savedOrder.getId());
            throw new OrderCreationException("Failed to create order", e);
        }
    }
    
    private void publishOrderEvent(OrderEvent event) {
        kafkaTemplate.send("order-events", event.getOrderId(), event)
            .addCallback(
                result -> log.info("Event published: {}", event),
                ex -> log.error("Failed to publish event", ex)
            );
    }
    
    // Saga 보상 로직
    private void compensateOrder(String orderId) {
        orderRepository.findById(orderId).ifPresent(order -> {
            order.setStatus(OrderStatus.CANCELLED);
            orderRepository.save(order);
            publishOrderEvent(new OrderCancelledEvent(order));
        });
    }
    
    private void compensateInventory(String orderId) {
        try {
            inventoryClient.releaseStock(orderId);
        } catch (Exception e) {
            log.error("Failed to compensate inventory", e);
            // Dead Letter Queue로 전송
            sendToDeadLetterQueue("inventory-compensation", orderId);
        }
    }
}

// Domain Event
@Value
@Builder
public class OrderCreatedEvent implements OrderEvent {
    String eventId = UUID.randomUUID().toString();
    String orderId;
    String userId;
    Instant timestamp = Instant.now();
    List items;
    BigDecimal totalAmount;
    
    @Override
    public String getEventType() {
        return "ORDER_CREATED";
    }
}

// Event Handler
@Component
@Slf4j
@RequiredArgsConstructor
public class OrderEventHandler {
    private final NotificationService notificationService;
    private final AnalyticsService analyticsService;
    private final RecommendationService recommendationService;
    
    @KafkaListener(topics = "order-events")
    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))
    public void handleOrderEvent(OrderEvent event) {
        log.info("Processing event: {} for order: {}", 
            event.getEventType(), event.getOrderId());
        
        switch (event.getEventType()) {
            case "ORDER_CREATED":
                handleOrderCreated((OrderCreatedEvent) event);
                break;
            case "ORDER_CANCELLED":
                handleOrderCancelled((OrderCancelledEvent) event);
                break;
            default:
                log.warn("Unknown event type: {}", event.getEventType());
        }
    }
    
    private void handleOrderCreated(OrderCreatedEvent event) {
        // 알림 발송
        CompletableFuture.runAsync(() -> 
            notificationService.sendOrderConfirmation(event)
        );
        
        // 분석 데이터 수집
        CompletableFuture.runAsync(() -> 
            analyticsService.trackOrderCreated(event)
        );
        
        // 추천 시스템 업데이트
        CompletableFuture.runAsync(() -> 
            recommendationService.updateUserPreferences(event)
        );
    }
}

헥사고날 아키텍처

포트와 어댑터 패턴

프로젝트 구조

// 헥사고날 아키텍처 프로젝트 구조
src/
├── domain/                 # 비즈니스 로직 (핵심)
│   ├── models/            # 도메인 엔티티
│   ├── ports/             # 인터페이스 정의
│   │   ├── in/           # 인바운드 포트 (Use Cases)
│   │   └── out/          # 아웃바운드 포트 (Repository)
│   └── services/          # 도메인 서비스
├── application/           # 애플리케이션 서비스
│   ├── usecases/         # 유스케이스 구현
│   └── dto/              # DTO 정의
├── adapters/             # 어댑터 (외부 세계와의 연결)
│   ├── in/              # 인바운드 어댑터
│   │   ├── web/        # REST API
│   │   ├── grpc/      # gRPC
│   │   └── graphql/   # GraphQL
│   └── out/             # 아웃바운드 어댑터
│       ├── persistence/ # 데이터베이스
│       ├── messaging/   # 메시지 큐
│       └── external/    # 외부 API
└── infrastructure/       # 인프라 설정
    ├── config/          # 설정 파일
    └── security/        # 보안 설정

헥사고날 아키텍처 구현

// domain/models/Product.ts
export class Product {
    private constructor(
        private readonly id: ProductId,
        private name: string,
        private description: string,
        private price: Money,
        private stock: number,
        private category: Category,
        private status: ProductStatus
    ) {
        this.validate();
    }

    static create(props: CreateProductProps): Product {
        const productId = ProductId.generate();
        return new Product(
            productId,
            props.name,
            props.description,
            Money.fromAmount(props.price),
            props.stock,
            props.category,
            ProductStatus.ACTIVE
        );
    }

    updatePrice(newPrice: Money): void {
        if (newPrice.isNegative()) {
            throw new InvalidPriceException('Price cannot be negative');
        }
        this.price = newPrice;
    }

    decreaseStock(quantity: number): void {
        if (this.stock < quantity) {
            throw new InsufficientStockException(
                `Insufficient stock. Available: ${this.stock}, Requested: ${quantity}`
            );
        }
        this.stock -= quantity;
    }

    private validate(): void {
        if (!this.name || this.name.length < 3) {
            throw new ValidationException('Product name must be at least 3 characters');
        }
        if (this.stock < 0) {
            throw new ValidationException('Stock cannot be negative');
        }
    }
}

// domain/ports/in/ManageProductUseCase.ts
export interface ManageProductUseCase {
    createProduct(command: CreateProductCommand): Promise;
    updateProduct(command: UpdateProductCommand): Promise;
    deleteProduct(productId: ProductId): Promise;
}

// domain/ports/out/ProductRepository.ts
export interface ProductRepository {
    save(product: Product): Promise;
    findById(id: ProductId): Promise;
    findByCategory(category: Category): Promise;
    delete(id: ProductId): Promise;
}

// application/usecases/ManageProductService.ts
@Injectable()
export class ManageProductService implements ManageProductUseCase {
    constructor(
        private readonly productRepository: ProductRepository,
        private readonly eventPublisher: EventPublisher,
        private readonly validator: ProductValidator
    ) {}

    async createProduct(command: CreateProductCommand): Promise {
        // 비즈니스 규칙 검증
        await this.validator.validateCreateProduct(command);

        // 도메인 객체 생성
        const product = Product.create({
            name: command.name,
            description: command.description,
            price: command.price,
            stock: command.stock,
            category: command.category
        });

        // 저장
        await this.productRepository.save(product);

        // 이벤트 발행
        await this.eventPublisher.publish(
            new ProductCreatedEvent(product.getId(), product.getName())
        );

        return product.getId();
    }

    async updateProduct(command: UpdateProductCommand): Promise {
        const product = await this.productRepository.findById(command.productId);
        
        if (!product) {
            throw new ProductNotFoundException(command.productId);
        }

        // 도메인 로직 실행
        if (command.price) {
            product.updatePrice(Money.fromAmount(command.price));
        }
        
        if (command.stock !== undefined) {
            product.updateStock(command.stock);
        }

        // 저장
        await this.productRepository.save(product);

        // 이벤트 발행
        await this.eventPublisher.publish(
            new ProductUpdatedEvent(product.getId(), command)
        );
    }
}

// adapters/in/web/ProductController.ts
@Controller('/api/v1/products')
@UseGuards(AuthGuard)
export class ProductController {
    constructor(
        private readonly manageProductUseCase: ManageProductUseCase,
        private readonly queryProductUseCase: QueryProductUseCase
    ) {}

    @Post()
    @UseInterceptors(ValidationInterceptor)
    async createProduct(@Body() dto: CreateProductDto): Promise {
        const command = CreateProductCommand.fromDto(dto);
        const productId = await this.manageProductUseCase.createProduct(command);
        
        return {
            id: productId.getValue(),
            message: 'Product created successfully'
        };
    }

    @Get(':id')
    async getProduct(@Param('id') id: string): Promise {
        const query = new GetProductQuery(ProductId.fromString(id));
        const product = await this.queryProductUseCase.getProduct(query);
        
        return ProductDto.fromDomain(product);
    }

    @Put(':id')
    async updateProduct(
        @Param('id') id: string,
        @Body() dto: UpdateProductDto
    ): Promise {
        const command = UpdateProductCommand.fromDto(id, dto);
        await this.manageProductUseCase.updateProduct(command);
    }
}

// adapters/out/persistence/ProductPersistenceAdapter.ts
@Injectable()
export class ProductPersistenceAdapter implements ProductRepository {
    constructor(
        private readonly prisma: PrismaService,
        private readonly mapper: ProductMapper
    ) {}

    async save(product: Product): Promise {
        const data = this.mapper.toPersistence(product);
        
        await this.prisma.product.upsert({
            where: { id: data.id },
            create: data,
            update: data
        });
    }

    async findById(id: ProductId): Promise {
        const data = await this.prisma.product.findUnique({
            where: { id: id.getValue() },
            include: {
                category: true,
                reviews: true
            }
        });

        return data ? this.mapper.toDomain(data) : null;
    }

    async findByCategory(category: Category): Promise {
        const data = await this.prisma.product.findMany({
            where: {
                categoryId: category.getId().getValue(),
                status: 'ACTIVE'
            },
            orderBy: { createdAt: 'desc' }
        });

        return data.map(item => this.mapper.toDomain(item));
    }
}

이벤트 주도 아키텍처

Event Sourcing과 CQRS

이벤트 소싱 구현

// domain/aggregates/Order.ts
export class Order extends AggregateRoot {
    private orderId: OrderId;
    private customerId: CustomerId;
    private items: OrderItem[] = [];
    private status: OrderStatus;
    private totalAmount: Money;
    private version: number = 0;

    // 이벤트 소싱: 이벤트에서 상태 복원
    static fromEvents(events: DomainEvent[]): Order {
        const order = new Order();
        
        events.forEach(event => {
            order.apply(event, false);
        });
        
        return order;
    }

    // 명령 처리
    placeOrder(command: PlaceOrderCommand): void {
        // 비즈니스 규칙 검증
        if (this.items.length === 0) {
            throw new EmptyOrderException();
        }

        // 이벤트 생성 및 적용
        const event = new OrderPlacedEvent({
            orderId: this.orderId,
            customerId: this.customerId,
            items: this.items,
            totalAmount: this.calculateTotal(),
            timestamp: new Date()
        });

        this.apply(event);
    }

    cancelOrder(reason: string): void {
        if (!this.canBeCancelled()) {
            throw new OrderCannotBeCancelledException(this.status);
        }

        const event = new OrderCancelledEvent({
            orderId: this.orderId,
            reason,
            timestamp: new Date()
        });

        this.apply(event);
    }

    // 이벤트 적용
    protected when(event: DomainEvent): void {
        switch (event.constructor) {
            case OrderPlacedEvent:
                this.onOrderPlaced(event as OrderPlacedEvent);
                break;
            case OrderCancelledEvent:
                this.onOrderCancelled(event as OrderCancelledEvent);
                break;
            case OrderShippedEvent:
                this.onOrderShipped(event as OrderShippedEvent);
                break;
        }
    }

    private onOrderPlaced(event: OrderPlacedEvent): void {
        this.orderId = event.orderId;
        this.customerId = event.customerId;
        this.items = event.items;
        this.status = OrderStatus.PLACED;
        this.totalAmount = event.totalAmount;
    }

    private onOrderCancelled(event: OrderCancelledEvent): void {
        this.status = OrderStatus.CANCELLED;
    }
}

// infrastructure/eventstore/EventStore.ts
export interface EventStore {
    saveEvents(aggregateId: string, events: DomainEvent[], expectedVersion: number): Promise;
    getEvents(aggregateId: string): Promise;
    getEventsFromSnapshot(aggregateId: string, version: number): Promise;
    saveSnapshot(aggregateId: string, snapshot: AggregateSnapshot): Promise;
}

@Injectable()
export class PostgresEventStore implements EventStore {
    constructor(
        private readonly db: DatabaseConnection,
        private readonly serializer: EventSerializer
    ) {}

    async saveEvents(
        aggregateId: string, 
        events: DomainEvent[], 
        expectedVersion: number
    ): Promise {
        const client = await this.db.getClient();
        
        try {
            await client.query('BEGIN');

            // 낙관적 동시성 제어
            const result = await client.query(
                'SELECT MAX(version) as version FROM events WHERE aggregate_id = $1',
                [aggregateId]
            );

            const currentVersion = result.rows[0]?.version || 0;
            
            if (currentVersion !== expectedVersion) {
                throw new ConcurrencyException(
                    `Expected version ${expectedVersion} but was ${currentVersion}`
                );
            }

            // 이벤트 저장
            for (const event of events) {
                const eventData = this.serializer.serialize(event);
                
                await client.query(
                    `INSERT INTO events 
                     (event_id, aggregate_id, event_type, event_data, version, timestamp)
                     VALUES ($1, $2, $3, $4, $5, $6)`,
                    [
                        event.eventId,
                        aggregateId,
                        event.constructor.name,
                        eventData,
                        ++currentVersion,
                        event.timestamp
                    ]
                );
            }

            await client.query('COMMIT');
            
            // 이벤트 발행
            await this.publishEvents(events);
            
        } catch (error) {
            await client.query('ROLLBACK');
            throw error;
        } finally {
            client.release();
        }
    }

    async getEvents(aggregateId: string): Promise {
        const result = await this.db.query(
            `SELECT event_type, event_data, version, timestamp 
             FROM events 
             WHERE aggregate_id = $1 
             ORDER BY version`,
            [aggregateId]
        );

        return result.rows.map(row => 
            this.serializer.deserialize(row.event_type, row.event_data)
        );
    }
}

// application/commands/OrderCommandHandler.ts
@Injectable()
export class OrderCommandHandler {
    constructor(
        private readonly eventStore: EventStore,
        private readonly orderRepository: OrderRepository
    ) {}

    async handle(command: PlaceOrderCommand): Promise {
        // 이벤트 스토어에서 현재 상태 로드
        const events = await this.eventStore.getEvents(command.orderId);
        const order = events.length > 0 
            ? Order.fromEvents(events)
            : Order.create(command);

        // 명령 실행
        order.placeOrder(command);

        // 새 이벤트 저장
        const uncommittedEvents = order.getUncommittedEvents();
        await this.eventStore.saveEvents(
            command.orderId,
            uncommittedEvents,
            order.getVersion()
        );

        // Read Model 업데이트 (CQRS)
        await this.updateReadModel(order);
    }

    private async updateReadModel(order: Order): Promise {
        // 읽기 전용 모델 업데이트
        const orderView = OrderView.fromAggregate(order);
        await this.orderRepository.save(orderView);
    }
}

// application/queries/OrderQueryHandler.ts
@Injectable()
export class OrderQueryHandler {
    constructor(
        private readonly readDb: ReadDatabaseConnection,
        private readonly cache: CacheService
    ) {}

    async getOrderById(orderId: string): Promise {
        // 캐시 확인
        const cached = await this.cache.get(`order:${orderId}`);
        if (cached) return cached;

        // Read DB에서 조회
        const result = await this.readDb.query(
            `SELECT * FROM order_views WHERE order_id = $1`,
            [orderId]
        );

        if (!result.rows[0]) {
            throw new OrderNotFoundException(orderId);
        }

        const orderView = OrderView.fromDb(result.rows[0]);
        
        // 캐시 저장
        await this.cache.set(`order:${orderId}`, orderView, 300);
        
        return orderView;
    }

    async getOrdersByCustomer(
        customerId: string, 
        pagination: PaginationParams
    ): Promise> {
        const result = await this.readDb.query(
            `SELECT * FROM order_views 
             WHERE customer_id = $1 
             ORDER BY created_at DESC 
             LIMIT $2 OFFSET $3`,
            [customerId, pagination.limit, pagination.offset]
        );

        const countResult = await this.readDb.query(
            `SELECT COUNT(*) FROM order_views WHERE customer_id = $1`,
            [customerId]
        );

        return {
            items: result.rows.map(row => OrderView.fromDb(row)),
            total: parseInt(countResult.rows[0].count),
            page: pagination.page,
            pageSize: pagination.limit
        };
    }
}

Saga 패턴 구현

// sagas/OrderSaga.ts
export class OrderSaga {
    private readonly steps: SagaStep[] = [];
    private readonly compensations: Map = new Map();

    constructor(
        private readonly orderService: OrderService,
        private readonly inventoryService: InventoryService,
        private readonly paymentService: PaymentService,
        private readonly shippingService: ShippingService
    ) {
        this.defineSteps();
    }

    private defineSteps(): void {
        // Step 1: 주문 생성
        this.addStep({
            name: 'CREATE_ORDER',
            action: async (context: OrderContext) => {
                const order = await this.orderService.createOrder(context.orderData);
                context.orderId = order.id;
                return order;
            },
            compensation: async (context: OrderContext) => {
                if (context.orderId) {
                    await this.orderService.cancelOrder(context.orderId);
                }
            }
        });

        // Step 2: 재고 예약
        this.addStep({
            name: 'RESERVE_INVENTORY',
            action: async (context: OrderContext) => {
                const reservation = await this.inventoryService.reserveItems(
                    context.orderId,
                    context.orderData.items
                );
                context.reservationId = reservation.id;
                return reservation;
            },
            compensation: async (context: OrderContext) => {
                if (context.reservationId) {
                    await this.inventoryService.releaseReservation(context.reservationId);
                }
            }
        });

        // Step 3: 결제 처리
        this.addStep({
            name: 'PROCESS_PAYMENT',
            action: async (context: OrderContext) => {
                const payment = await this.paymentService.processPayment({
                    orderId: context.orderId,
                    amount: context.orderData.totalAmount,
                    paymentMethod: context.orderData.paymentMethod
                });
                context.paymentId = payment.transactionId;
                return payment;
            },
            compensation: async (context: OrderContext) => {
                if (context.paymentId) {
                    await this.paymentService.refund(context.paymentId);
                }
            }
        });

        // Step 4: 배송 준비
        this.addStep({
            name: 'PREPARE_SHIPPING',
            action: async (context: OrderContext) => {
                const shipping = await this.shippingService.createShipment({
                    orderId: context.orderId,
                    address: context.orderData.shippingAddress,
                    items: context.orderData.items
                });
                context.shipmentId = shipping.id;
                return shipping;
            },
            compensation: async (context: OrderContext) => {
                if (context.shipmentId) {
                    await this.shippingService.cancelShipment(context.shipmentId);
                }
            }
        });
    }

    async execute(orderData: CreateOrderData): Promise {
        const context: OrderContext = { orderData };
        const executedSteps: string[] = [];

        try {
            // 각 스텝 실행
            for (const step of this.steps) {
                log.info(`Executing saga step: ${step.name}`);
                
                await step.action(context);
                executedSteps.push(step.name);
                
                // 스텝 완료 이벤트 발행
                await this.publishStepCompleted(step.name, context);
            }

            // 성공 시 완료 이벤트 발행
            await this.publishSagaCompleted(context);
            
            return {
                success: true,
                orderId: context.orderId,
                message: 'Order processed successfully'
            };

        } catch (error) {
            log.error(`Saga failed at step: ${executedSteps[executedSteps.length - 1]}`, error);
            
            // 보상 트랜잭션 실행
            await this.compensate(executedSteps, context);
            
            // 실패 이벤트 발행
            await this.publishSagaFailed(context, error);
            
            throw new SagaExecutionException('Order processing failed', error);
        }
    }

    private async compensate(executedSteps: string[], context: OrderContext): Promise {
        // 역순으로 보상 실행
        for (let i = executedSteps.length - 1; i >= 0; i--) {
            const stepName = executedSteps[i];
            const compensation = this.compensations.get(stepName);
            
            if (compensation) {
                try {
                    log.info(`Executing compensation for: ${stepName}`);
                    await compensation(context);
                } catch (error) {
                    log.error(`Compensation failed for: ${stepName}`, error);
                    // 보상 실패는 Dead Letter Queue로
                    await this.sendToDeadLetterQueue(stepName, context, error);
                }
            }
        }
    }
}

// Orchestrator 기반 Saga
@Injectable()
export class OrderSagaOrchestrator {
    constructor(
        private readonly sagaRepository: SagaRepository,
        private readonly eventBus: EventBus
    ) {}

    @EventHandler(OrderCreatedEvent)
    async handleOrderCreated(event: OrderCreatedEvent): Promise {
        // Saga 인스턴스 생성
        const saga = await this.sagaRepository.create({
            sagaId: generateSagaId(),
            orderId: event.orderId,
            status: SagaStatus.STARTED,
            currentStep: 'ORDER_CREATED'
        });

        // 다음 스텝 실행
        await this.executeNextStep(saga);
    }

    private async executeNextStep(saga: SagaInstance): Promise {
        const nextStep = this.getNextStep(saga.currentStep);
        
        if (!nextStep) {
            // Saga 완료
            saga.status = SagaStatus.COMPLETED;
            await this.sagaRepository.update(saga);
            return;
        }

        try {
            // 스텝 실행
            await this.executeStep(nextStep, saga);
            
            // 상태 업데이트
            saga.currentStep = nextStep;
            await this.sagaRepository.update(saga);
            
        } catch (error) {
            // 실패 처리
            saga.status = SagaStatus.COMPENSATING;
            await this.sagaRepository.update(saga);
            
            // 보상 시작
            await this.startCompensation(saga);
        }
    }
}

분산 시스템 패턴

분산 시스템 핵심 패턴

Service Mesh 구현

# istio-service-mesh.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: order-service
  namespace: production
spec:
  hosts:
  - order-service
  http:
  - match:
    - headers:
        x-version:
          exact: v2
    route:
    - destination:
        host: order-service
        subset: v2
      weight: 100
  - route:
    - destination:
        host: order-service
        subset: v1
      weight: 90
    - destination:
        host: order-service
        subset: v2
      weight: 10
    timeout: 30s
    retries:
      attempts: 3
      perTryTimeout: 10s
      retryOn: 5xx,reset,connect-failure,refused-stream

---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: order-service
  namespace: production
spec:
  host: order-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http1MaxPendingRequests: 50
        http2MaxRequests: 100
        maxRequestsPerConnection: 2
    loadBalancer:
      consistentHash:
        httpCookie:
          name: "session-affinity"
          ttl: 3600s
    outlierDetection:
      consecutiveErrors: 5
      interval: 30s
      baseEjectionTime: 30s
      maxEjectionPercent: 50
      minHealthPercent: 30
  subsets:
  - name: v1
    labels:
      version: v1
    trafficPolicy:
      connectionPool:
        tcp:
          maxConnections: 50
  - name: v2
    labels:
      version: v2

---
# Circuit Breaker
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: payment-service
spec:
  hosts:
  - payment-service
  http:
  - fault:
      delay:
        percentage:
          value: 0.1
        fixedDelay: 5s
      abort:
        percentage:
          value: 0.1
        httpStatus: 503
    route:
    - destination:
        host: payment-service

---
# Distributed Tracing
apiVersion: v1
kind: ConfigMap
metadata:
  name: jaeger-config
data:
  sampling.json: |
    {
      "service_strategies": [
        {
          "service": "order-service",
          "type": "probabilistic",
          "param": 0.1
        },
        {
          "service": "payment-service",
          "type": "adaptive",
          "max_traces_per_second": 100
        }
      ],
      "default_strategy": {
        "type": "probabilistic",
        "param": 0.01
      }
    }

분산 락과 리더 선출

// distributed/DistributedLock.ts
export class RedisDistributedLock {
    private readonly redis: RedisClient;
    private readonly defaultTTL = 30000; // 30초

    async acquireLock(
        key: string, 
        ttl: number = this.defaultTTL
    ): Promise {
        const lockId = generateLockId();
        const lockKey = `lock:${key}`;
        
        // SET NX EX 원자적 연산
        const acquired = await this.redis.set(
            lockKey,
            lockId,
            'PX',
            ttl,
            'NX'
        );

        if (!acquired) {
            return null;
        }

        return new Lock(lockKey, lockId, ttl);
    }

    async releaseLock(lock: Lock): Promise {
        // Lua 스크립트로 원자적 해제
        const script = `
            if redis.call("get", KEYS[1]) == ARGV[1] then
                return redis.call("del", KEYS[1])
            else
                return 0
            end
        `;

        const result = await this.redis.eval(
            script,
            1,
            lock.key,
            lock.id
        );

        return result === 1;
    }

    async extendLock(lock: Lock, ttl: number): Promise {
        const script = `
            if redis.call("get", KEYS[1]) == ARGV[1] then
                return redis.call("pexpire", KEYS[1], ARGV[2])
            else
                return 0
            end
        `;

        const result = await this.redis.eval(
            script,
            1,
            lock.key,
            lock.id,
            ttl
        );

        return result === 1;
    }
}

// 리더 선출
export class LeaderElection {
    private readonly lockKey = 'leader-election';
    private readonly heartbeatInterval = 5000; // 5초
    private isLeader = false;
    private heartbeatTimer?: NodeJS.Timer;

    constructor(
        private readonly lock: DistributedLock,
        private readonly nodeId: string
    ) {}

    async start(onBecomeLeader: () => void, onLoseLeadership: () => void): Promise {
        while (true) {
            try {
                // 리더 선출 시도
                const acquired = await this.lock.acquireLock(
                    this.lockKey,
                    this.heartbeatInterval * 2
                );

                if (acquired) {
                    this.isLeader = true;
                    log.info(`Node ${this.nodeId} became leader`);
                    
                    onBecomeLeader();
                    
                    // 하트비트 시작
                    this.startHeartbeat(acquired);
                    
                    // 리더십 유지
                    await this.maintainLeadership(acquired, onLoseLeadership);
                } else {
                    // 팔로워로 대기
                    await this.waitAsFollower();
                }
            } catch (error) {
                log.error('Leader election error:', error);
                await this.delay(this.heartbeatInterval);
            }
        }
    }

    private async maintainLeadership(
        lock: Lock, 
        onLoseLeadership: () => void
    ): Promise {
        while (this.isLeader) {
            try {
                // 락 갱신
                const extended = await this.lock.extendLock(
                    lock,
                    this.heartbeatInterval * 2
                );

                if (!extended) {
                    // 리더십 상실
                    this.isLeader = false;
                    this.stopHeartbeat();
                    onLoseLeadership();
                    break;
                }

                await this.delay(this.heartbeatInterval);
            } catch (error) {
                log.error('Failed to maintain leadership:', error);
                this.isLeader = false;
                this.stopHeartbeat();
                onLoseLeadership();
                break;
            }
        }
    }
}

// 분산 합의 (Raft 간단 구현)
export class SimpleRaft {
    private state: 'follower' | 'candidate' | 'leader' = 'follower';
    private currentTerm = 0;
    private votedFor: string | null = null;
    private log: LogEntry[] = [];
    private commitIndex = 0;

    constructor(
        private readonly nodeId: string,
        private readonly peers: string[],
        private readonly rpc: RaftRPC
    ) {}

    // 리더 선출
    private async startElection(): Promise {
        this.state = 'candidate';
        this.currentTerm++;
        this.votedFor = this.nodeId;
        
        const votes = 1; // 자신의 표
        const majority = Math.floor(this.peers.length / 2) + 1;

        // 동시에 모든 노드에 투표 요청
        const votePromises = this.peers.map(peer => 
            this.requestVote(peer)
        );

        const results = await Promise.allSettled(votePromises);
        const approvals = results.filter(r => 
            r.status === 'fulfilled' && r.value.voteGranted
        ).length;

        if (votes + approvals >= majority) {
            this.becomeLeader();
        } else {
            this.state = 'follower';
        }
    }

    private async requestVote(peer: string): Promise {
        return this.rpc.requestVote(peer, {
            term: this.currentTerm,
            candidateId: this.nodeId,
            lastLogIndex: this.log.length - 1,
            lastLogTerm: this.log[this.log.length - 1]?.term || 0
        });
    }
}

실습: 마이크로서비스 설계

온라인 뱅킹 시스템 설계

AI와 함께 대규모 온라인 뱅킹 시스템을 설계해봅시다.

시스템 요구사항

  • 일일 1000만 거래 처리
  • 99.99% 가용성 보장
  • 강력한 보안과 규정 준수
  • 실시간 잔액 업데이트
  • 다중 통화 지원
  • 사기 탐지 시스템

설계 단계

1. 도메인 분석

Chat에 요청: "온라인 뱅킹의 핵심 도메인과 바운디드 컨텍스트를 정의해줘"

2. 서비스 분해

Cmd+K: "각 도메인별 마이크로서비스 구조 설계"

3. 데이터 전략

Composer로 CQRS와 이벤트 소싱 구현

4. 보안 아키텍처

AI에게: "금융 규정을 준수하는 보안 아키텍처 설계해줘"

5. 장애 대응

복원력 있는 시스템을 위한 패턴 적용

🏛️ 적용 패턴

  • Event Sourcing: 모든 거래 내역 추적
  • CQRS: 읽기/쓰기 분리
  • Saga: 분산 트랜잭션 관리
  • Circuit Breaker: 장애 격리
  • Bulkhead: 리소스 격리
  • Service Mesh: 서비스 간 통신 관리

핵심 정리

지능적인 아키텍처 설계

AI가 프로젝트 요구사항을 분석하여 최적의 아키텍처를 제안합니다.

패턴 자동 적용

헥사고날, 이벤트 소싱, CQRS 등 복잡한 패턴을 쉽게 구현합니다.

분산 시스템 관리

마이크로서비스의 복잡성을 AI가 관리하고 최적화합니다.

확장 가능한 설계

처음부터 확장성을 고려한 아키텍처를 구축합니다.

다음 강의 예고

다음 강의에서는 AI 활용 고급 기법을 배웁니다.

다음 강의로 이동
20/30 완료