🏢 배달의민족 기술 스택 분석

51 min read
baeminwoowajavaspringkotlinnodejsmysqlredisawskubernetesdeliveryo2o

배달의민족이 한국 배달 문화를 혁신하며 동남아로 확장한 기술 스택 심층 분석 - Java, Spring, Kotlin으로 구축한 O2O 플랫폼

🏢 배달의민족 기술 스택 분석

한국 배달 문화를 재정의하고 동남아시아로 확장 중인 배달의민족의 기술 스택을 심층 분석합니다.
"좋은 음식을 먹고 싶은 곳에서" - 기술로 음식 배달의 새로운 기준을 만드는 혁신


📊 회사 개요

서비스 규모

  • 앱 다운로드: 3,000만+ 회
  • 제휴 음식점: 30만+ 개
  • 배달 파트너: 8만+ 명
  • 연간 주문: 2억+ 건
  • 서비스 지역: 한국 + 동남아 100+ 도시

엔지니어링 조직

  • 개발자: 800명+ (전체 직원 4,000+)
  • 문화: "실패를 두려워하지 않는 도전"
  • 조직: 서비스별 독립 셀
  • 기술 철학: 꾸준한 개선, 실용적 접근

기술적 도전과제

  1. 실시간 매칭: 주문-음식점-라이더 최적화
  2. 피크 타임: 점심/저녁 트래픽 폭증
  3. 위치 정확도: 정확한 배달 주소
  4. 글로벌 확장: 각국 특성 대응

🏗️ 아키텍처 Overview

시스템 다이어그램

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Mobile    │────►│   API       │────►│   Order     │
│    Apps     │     │  Gateway    │     │  Service    │
└─────────────┘     └─────────────┘     └─────────────┘
       │                    │                    │
       │              ┌─────▼─────┐       ┌─────▼─────┐
       │              │  Matching  │       │  Payment   │
       │              │   Engine   │       │  Service   │
       │              └───────────┘       └───────────┘
       │                    │                    │
       └────────────────────┤                    │
                      ┌─────▼─────┐       ┌─────▼─────┐
                      │   Rider    │       │    GPS     │
                      │  Service   │       │  Tracking  │
                      └───────────┘       └───────────┘

핵심 설계 원칙

  1. Reliability First: 안정성이 최우선
  2. Real-time Processing: 실시간 처리
  3. Microservices: 서비스별 독립성
  4. Data Driven: 데이터 기반 의사결정
  5. User Centric: 사용자 중심 설계

기술 진화

  • 2010-2012: 모놀리스 시작
  • 2013-2015: 마이크로서비스 전환
  • 2016-2018: 배민라이더스 출시
  • 2019-2021: B마트, 배민1 서비스
  • 2022-현재: 동남아 진출, AI 도입

🧩 기술 스택 상세

Backend 기술

Spring 기반 주문 시스템

// 배민 주문 처리 시스템
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    private final OrderService orderService;
    private final MatchingService matchingService;
    private final PaymentService paymentService;
    private final NotificationService notificationService;

    @PostMapping
    @Transactional
    public ResponseEntity<OrderResponse> createOrder(@RequestBody @Valid OrderRequest request) {
        // 1. 주문 유효성 검증
        validateOrder(request);

        // 2. 음식점 영업 상태 확인
        Restaurant restaurant = restaurantService.findById(request.getRestaurantId());
        if (!restaurant.isOpen()) {
            throw new RestaurantClosedException("음식점이 영업 중이 아닙니다");
        }

        // 3. 배달 가능 지역 확인
        DeliveryArea area = deliveryAreaService.findByCoordinates(
            request.getDeliveryAddress().getLatitude(),
            request.getDeliveryAddress().getLongitude()
        );

        if (!area.isServiceable(restaurant)) {
            throw new DeliveryAreaException("배달 불가능한 지역입니다");
        }

        // 4. 주문 생성
        Order order = Order.builder()
            .userId(getCurrentUserId())
            .restaurantId(request.getRestaurantId())
            .items(convertToOrderItems(request.getItems()))
            .deliveryAddress(request.getDeliveryAddress())
            .paymentMethod(request.getPaymentMethod())
            .estimatedDeliveryTime(calculateEstimatedTime(restaurant, area))
            .status(OrderStatus.PENDING)
            .build();

        // 5. 가격 계산
        PriceCalculation calculation = priceCalculator.calculate(order);
        order.setSubtotal(calculation.getSubtotal());
        order.setDeliveryFee(calculation.getDeliveryFee());
        order.setTotalAmount(calculation.getTotalAmount());

        // 6. 주문 저장
        Order savedOrder = orderService.save(order);

        // 7. 결제 처리 (비동기)
        CompletableFuture<PaymentResult> paymentFuture =
            CompletableFuture.supplyAsync(() ->
                paymentService.processPayment(savedOrder)
            );

        // 8. 음식점 알림
        notificationService.notifyRestaurant(savedOrder);

        // 9. 라이더 매칭 시작
        matchingService.startMatching(savedOrder);

        return ResponseEntity.ok(OrderResponse.from(savedOrder));
    }

    @GetMapping("/{orderId}/status")
    public ResponseEntity<OrderStatusResponse> getOrderStatus(@PathVariable Long orderId) {
        Order order = orderService.findById(orderId);

        // 실시간 위치 정보 포함
        LocationInfo riderLocation = null;
        if (order.hasRider()) {
            riderLocation = gpsTrackingService.getRiderLocation(order.getRiderId());
        }

        return ResponseEntity.ok(
            OrderStatusResponse.builder()
                .orderId(orderId)
                .status(order.getStatus())
                .estimatedDeliveryTime(order.getEstimatedDeliveryTime())
                .riderLocation(riderLocation)
                .statusHistory(order.getStatusHistory())
                .build()
        );
    }
}

// 라이더 매칭 엔진
@Service
@Slf4j
public class RiderMatchingEngine {

    private final RiderRepository riderRepository;
    private final GeoService geoService;
    private final MLPredictionService mlService;

    @Async
    public CompletableFuture<Rider> matchRider(Order order) {
        log.info("Starting rider matching for order: {}", order.getId());

        // 1. 후보 라이더 검색
        List<Rider> candidateRiders = findCandidateRiders(order);

        if (candidateRiders.isEmpty()) {
            // 라이더 부족 시 인센티브 제공
            offerSurgeIncentive(order);
            return CompletableFuture.completedFuture(null);
        }

        // 2. 라이더 점수 계산
        List<RiderScore> scores = candidateRiders.stream()
            .map(rider -> calculateRiderScore(rider, order))
            .sorted(Comparator.reverseOrder())
            .collect(Collectors.toList());

        // 3. 최적 라이더 선택
        for (RiderScore score : scores) {
            if (offerDelivery(score.getRider(), order)) {
                log.info("Matched rider {} for order {}",
                    score.getRider().getId(), order.getId());
                return CompletableFuture.completedFuture(score.getRider());
            }
        }

        // 4. 매칭 실패 처리
        handleMatchingFailure(order);
        return CompletableFuture.completedFuture(null);
    }

    private List<Rider> findCandidateRiders(Order order) {
        // 반경 내 활성 라이더 검색
        double searchRadius = 3.0; // km

        List<Rider> nearbyRiders = geoService.findRidersWithinRadius(
            order.getRestaurant().getLocation(),
            searchRadius
        );

        return nearbyRiders.stream()
            .filter(Rider::isActive)
            .filter(rider -> rider.canDeliver(order))
            .filter(rider -> !rider.isOverloaded())
            .collect(Collectors.toList());
    }

    private RiderScore calculateRiderScore(Rider rider, Order order) {
        double score = 0.0;

        // 거리 점수 (가까울수록 높음)
        double distance = geoService.calculateDistance(
            rider.getCurrentLocation(),
            order.getRestaurant().getLocation()
        );
        score += (10.0 - distance) * 0.3;

        // 평점 점수
        score += rider.getRating() * 0.2;

        // 완료율 점수
        score += rider.getCompletionRate() * 0.2;

        // 현재 배달 수 (적을수록 높음)
        score += (5 - rider.getCurrentDeliveryCount()) * 0.1;

        // ML 예측 점수 (예상 배달 시간)
        double predictedTime = mlService.predictDeliveryTime(rider, order);
        score += (60 - predictedTime) / 60 * 0.2;

        return new RiderScore(rider, score);
    }

    private boolean offerDelivery(Rider rider, Order order) {
        // 라이더에게 배달 제안
        DeliveryOffer offer = DeliveryOffer.builder()
            .orderId(order.getId())
            .pickupLocation(order.getRestaurant().getLocation())
            .deliveryLocation(order.getDeliveryAddress())
            .estimatedDistance(calculateTotalDistance(order))
            .estimatedTime(calculateEstimatedTime(order))
            .deliveryFee(calculateDeliveryFee(order))
            .expiresAt(LocalDateTime.now().plusSeconds(30))
            .build();

        // 푸시 알림 전송
        pushNotificationService.sendToRider(rider, offer);

        // 30초 대기
        return waitForRiderResponse(rider, offer, 30);
    }
}

// 실시간 위치 추적
@Component
public class GPSTrackingService {

    private final RedisTemplate<String, LocationData> redisTemplate;
    private final KafkaTemplate<String, LocationEvent> kafkaTemplate;

    @KafkaListener(topics = "rider-location-updates")
    public void handleLocationUpdate(LocationUpdateEvent event) {
        // 1. Redis에 최신 위치 저장
        String key = "rider:location:" + event.getRiderId();
        LocationData locationData = LocationData.builder()
            .riderId(event.getRiderId())
            .latitude(event.getLatitude())
            .longitude(event.getLongitude())
            .speed(event.getSpeed())
            .heading(event.getHeading())
            .accuracy(event.getAccuracy())
            .timestamp(event.getTimestamp())
            .build();

        redisTemplate.opsForValue().set(key, locationData, 5, TimeUnit.MINUTES);

        // 2. 이동 경로 저장
        String pathKey = "rider:path:" + event.getRiderId();
        redisTemplate.opsForList().rightPush(pathKey, locationData);
        redisTemplate.expire(pathKey, 1, TimeUnit.HOURS);

        // 3. 지오펜싱 체크
        checkGeofencing(event);

        // 4. 이상 패턴 감지
        detectAnomalies(event);

        // 5. 고객에게 위치 업데이트 전송
        broadcastToCustomers(event);
    }

    private void checkGeofencing(LocationUpdateEvent event) {
        // 배달 중인 주문 조회
        List<Order> activeOrders = orderService.getActiveOrdersByRider(event.getRiderId());

        for (Order order : activeOrders) {
            // 음식점 도착 체크
            if (order.getStatus() == OrderStatus.ACCEPTED) {
                double distanceToRestaurant = calculateDistance(
                    event.getLatitude(),
                    event.getLongitude(),
                    order.getRestaurant().getLatitude(),
                    order.getRestaurant().getLongitude()
                );

                if (distanceToRestaurant < 100) { // 100m 이내
                    orderService.updateStatus(order.getId(), OrderStatus.PICKED_UP);
                    notificationService.notifyCustomer(
                        order.getUserId(),
                        "라이더가 음식을 픽업했습니다!"
                    );
                }
            }

            // 고객 위치 도착 체크
            if (order.getStatus() == OrderStatus.PICKED_UP) {
                double distanceToCustomer = calculateDistance(
                    event.getLatitude(),
                    event.getLongitude(),
                    order.getDeliveryAddress().getLatitude(),
                    order.getDeliveryAddress().getLongitude()
                );

                if (distanceToCustomer < 100) { // 100m 이내
                    notificationService.notifyCustomer(
                        order.getUserId(),
                        "라이더가 곧 도착합니다!"
                    );
                }
            }
        }
    }

    private void detectAnomalies(LocationUpdateEvent event) {
        // 비정상적인 속도 감지
        if (event.getSpeed() > 100) { // 100km/h 초과
            log.warn("Abnormal speed detected for rider {}: {}km/h",
                event.getRiderId(), event.getSpeed());
            alertSafetyTeam(event.getRiderId(), "Abnormal speed");
        }

        // 장시간 정지 감지
        LocationData lastLocation = getLastLocation(event.getRiderId());
        if (lastLocation != null &&
            calculateDistance(lastLocation, event) < 50 && // 50m 미만 이동
            Duration.between(lastLocation.getTimestamp(), event.getTimestamp()).toMinutes() > 10) {

            checkRiderStatus(event.getRiderId());
        }
    }
}

배달 최적화 알고리즘

// 배민1 라우팅 최적화
class DeliveryOptimizer {

    // 다중 픽업/배달 최적화
    fun optimizeMultiDeliveryRoute(
        rider: Rider,
        orders: List<Order>
    ): OptimizedRoute {
        // TSP (Traveling Salesman Problem) 변형
        val locations = extractLocations(orders)
        val distanceMatrix = buildDistanceMatrix(locations)

        // 유전 알고리즘으로 최적 경로 탐색
        val ga = GeneticAlgorithm(
            populationSize = 100,
            mutationRate = 0.01,
            crossoverRate = 0.7,
            eliteSize = 20
        )

        val bestRoute = ga.evolve(
            locations = locations,
            distanceMatrix = distanceMatrix,
            constraints = buildConstraints(orders),
            generations = 1000
        )

        return OptimizedRoute(
            rider = rider,
            waypoints = bestRoute.waypoints,
            totalDistance = bestRoute.distance,
            estimatedTime = calculateTime(bestRoute),
            orders = reorderOrders(orders, bestRoute)
        )
    }

    // 동적 재할당
    fun dynamicReallocation(
        activeRiders: List<Rider>,
        pendingOrders: List<Order>
    ): List<Assignment> {
        // 헝가리안 알고리즘 사용
        val costMatrix = buildCostMatrix(activeRiders, pendingOrders)
        val assignments = HungarianAlgorithm.solve(costMatrix)

        return assignments.map { (riderIdx, orderIdx) ->
            Assignment(
                rider = activeRiders[riderIdx],
                order = pendingOrders[orderIdx],
                estimatedPickupTime = calculatePickupTime(
                    activeRiders[riderIdx],
                    pendingOrders[orderIdx]
                )
            )
        }
    }

    // 수요 예측 기반 사전 배치
    suspend fun predictivePositioning(
        currentTime: LocalDateTime,
        area: DeliveryArea
    ): List<RecommendedPosition> {
        // 과거 데이터 기반 수요 예측
        val historicalDemand = getHistoricalDemand(
            area = area,
            timeWindow = TimeWindow(
                currentTime.minusWeeks(4),
                currentTime
            ),
            dayOfWeek = currentTime.dayOfWeek,
            hourOfDay = currentTime.hour
        )

        // 머신러닝 모델로 30분 후 수요 예측
        val predictedDemand = mlModel.predictDemand(
            features = DemandFeatures(
                area = area,
                time = currentTime.plusMinutes(30),
                weather = weatherService.getCurrentWeather(area),
                events = eventService.getNearbyEvents(area),
                historicalPattern = historicalDemand
            )
        )

        // K-means 클러스터링으로 최적 대기 위치 계산
        val hotspots = KMeansClustering.cluster(
            points = predictedDemand.heatmap,
            k = min(predictedDemand.expectedOrders / 10, 20)
        )

        return hotspots.map { hotspot ->
            RecommendedPosition(
                location = hotspot.center,
                expectedOrders = hotspot.weight,
                confidence = hotspot.confidence,
                radius = hotspot.radius
            )
        }
    }
}

// B마트 재고 관리
@Service
class BMartInventoryService {

    @Transactional
    fun processQuickCommerceOrder(order: QuickCommerceOrder): OrderResult {
        // 1. 재고 확인 및 예약
        val reservations = mutableListOf<StockReservation>()

        for (item in order.items) {
            val stock = stockRepository.findBySkuWithLock(item.sku)

            if (stock.available < item.quantity) {
                // 재고 부족 시 대체 상품 제안
                val alternatives = findAlternatives(item)
                if (alternatives.isNotEmpty()) {
                    return OrderResult.NeedsSubstitution(
                        originalItem = item,
                        alternatives = alternatives
                    )
                } else {
                    // 롤백
                    reservations.forEach { it.release() }
                    return OrderResult.OutOfStock(item)
                }
            }

            // 재고 예약
            val reservation = stock.reserve(item.quantity)
            reservations.add(reservation)
        }

        // 2. 피킹 작업 생성
        val pickingTask = PickingTask(
            orderId = order.id,
            items = order.items.map { item ->
                PickingItem(
                    sku = item.sku,
                    quantity = item.quantity,
                    location = warehouseService.getItemLocation(item.sku),
                    pickingOrder = calculateOptimalPickingOrder(item)
                )
            },
            targetTime = LocalDateTime.now().plusMinutes(10) // 10분 내 피킹
        )

        pickingService.assignTask(pickingTask)

        // 3. 배송 스케줄링
        val delivery = scheduleQuickDelivery(order)

        return OrderResult.Success(
            orderId = order.id,
            estimatedDelivery = delivery.estimatedTime,
            trackingId = delivery.trackingId
        )
    }

    // 실시간 재고 동기화
    @EventListener
    fun handleStockUpdate(event: StockUpdateEvent) {
        // 다중 채널 재고 동기화
        when (event.source) {
            StockSource.WAREHOUSE -> updateOnlineStock(event)
            StockSource.POS -> updateOfflineStock(event)
            StockSource.SUPPLIER -> updateIncomingStock(event)
        }

        // 재고 수준 알림
        if (event.currentLevel < event.reorderPoint) {
            triggerReorder(event.sku, event.suggestedQuantity)
        }

        // 품절 임박 상품 관리
        if (event.currentLevel < 10) {
            notifyLowStock(event.sku)
            adjustDemandForecast(event.sku)
        }
    }
}

Frontend 기술

React Native 앱

// 배민 React Native 앱
import React, { useEffect, useState } from "react";
import { View, FlatList, Animated } from "react-native";
import { useRecoilState, useRecoilValue } from "recoil";

// 메인 홈 화면
export const HomeScreen: React.FC = () => {
  const [location, setLocation] = useRecoilState(userLocationState);
  const [restaurants, setRestaurants] = useState<Restaurant[]>([]);
  const animatedValue = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    // 위치 권한 및 현재 위치 가져오기
    getCurrentLocation().then((loc) => {
      setLocation(loc);
      loadNearbyRestaurants(loc);
    });

    // 애니메이션 시작
    startAnimation();
  }, []);

  const loadNearbyRestaurants = async (location: Location) => {
    try {
      const response = await api.getRestaurants({
        latitude: location.latitude,
        longitude: location.longitude,
        radius: 3000, // 3km
        sort: "recommended",
      });

      setRestaurants(response.data);
    } catch (error) {
      showError("주변 음식점을 불러올 수 없습니다");
    }
  };

  return (
    <SafeAreaView style={styles.container}>
      <AddressHeader
        address={location?.address}
        onPress={() => navigation.navigate("AddressSearch")}
      />

      <ScrollView showsVerticalScrollIndicator={false}>
        {/* 배너 캐러셀 */}
        <BannerCarousel banners={banners} />

        {/* 빠른 카테고리 */}
        <QuickCategories
          categories={categories}
          onSelect={(category) => navigateToCategory(category)}
        />

        {/* 배민1 섹션 */}
        <BaeminOneSection onPress={() => navigation.navigate("BaeminOne")} />

        {/* 추천 음식점 */}
        <RecommendedRestaurants
          restaurants={restaurants}
          onRestaurantPress={(restaurant) =>
            navigation.navigate("RestaurantDetail", { id: restaurant.id })
          }
        />

        {/* B마트 */}
        <BMartSection
          quickItems={quickCommerceItems}
          onItemPress={(item) => addToCart(item)}
        />
      </ScrollView>

      <CartButton
        itemCount={cartItemCount}
        onPress={() => navigation.navigate("Cart")}
      />
    </SafeAreaView>
  );
};

// 음식점 상세 화면
export const RestaurantDetailScreen: React.FC<{ route: any }> = ({ route }) => {
  const { id } = route.params;
  const [restaurant, setRestaurant] = useState<Restaurant>();
  const [menu, setMenu] = useState<Menu[]>([]);
  const scrollY = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    loadRestaurantDetail();
  }, [id]);

  const headerOpacity = scrollY.interpolate({
    inputRange: [0, 200],
    outputRange: [0, 1],
    extrapolate: "clamp",
  });

  return (
    <View style={styles.container}>
      {/* 패럴랙스 헤더 */}
      <Animated.View style={[styles.header, { opacity: headerOpacity }]}>
        <RestaurantHeader restaurant={restaurant} />
      </Animated.View>

      <Animated.ScrollView
        onScroll={Animated.event(
          [{ nativeEvent: { contentOffset: { y: scrollY } } }],
          { useNativeDriver: false }
        )}
        scrollEventThrottle={16}
      >
        {/* 음식점 정보 */}
        <RestaurantInfo
          restaurant={restaurant}
          onInfoPress={() => showRestaurantInfo(restaurant)}
        />

        {/* 쿠폰/이벤트 */}
        <PromotionBanner
          promotions={restaurant?.promotions}
          onApplyCoupon={(coupon) => applyCoupon(coupon)}
        />

        {/* 메뉴 카테고리 탭 */}
        <MenuCategoryTabs
          categories={menuCategories}
          onSelectCategory={(category) => scrollToCategory(category)}
        />

        {/* 메뉴 리스트 */}
        <MenuList
          sections={menu}
          onAddToCart={(item, options) => {
            addToCart(restaurant, item, options);
            showCartToast();
          }}
        />
      </Animated.ScrollView>

      {/* 하단 주문 버튼 */}
      <CartSummaryBar
        restaurant={restaurant}
        cartItems={getCartItems(restaurant?.id)}
        onViewCart={() => navigation.navigate("Cart")}
      />
    </View>
  );
};

// 실시간 주문 추적
export const OrderTrackingScreen: React.FC<{ route: any }> = ({ route }) => {
  const { orderId } = route.params;
  const [order, setOrder] = useState<Order>();
  const [riderLocation, setRiderLocation] = useState<Location>();
  const mapRef = useRef<MapView>(null);

  useEffect(() => {
    // 주문 정보 로드
    loadOrderDetail(orderId);

    // 실시간 위치 구독
    const unsubscribe = subscribeToRiderLocation(orderId, (location) => {
      setRiderLocation(location);
      animateToLocation(location);
    });

    return () => unsubscribe();
  }, [orderId]);

  const animateToLocation = (location: Location) => {
    mapRef.current?.animateToRegion(
      {
        latitude: location.latitude,
        longitude: location.longitude,
        latitudeDelta: 0.005,
        longitudeDelta: 0.005,
      },
      1000
    );
  };

  return (
    <View style={styles.container}>
      {/* 지도 */}
      <MapView
        ref={mapRef}
        style={styles.map}
        initialRegion={getInitialRegion(order)}
      >
        {/* 음식점 마커 */}
        <Marker
          coordinate={order?.restaurant.location}
          title={order?.restaurant.name}
        >
          <RestaurantMarker />
        </Marker>

        {/* 배달 주소 마커 */}
        <Marker coordinate={order?.deliveryAddress} title="배달 주소">
          <DeliveryMarker />
        </Marker>

        {/* 라이더 위치 */}
        {riderLocation && (
          <Marker coordinate={riderLocation} title="라이더">
            <RiderMarker heading={riderLocation.heading} />
          </Marker>
        )}

        {/* 경로 표시 */}
        <Polyline
          coordinates={order?.route || []}
          strokeColor="#FF6B6B"
          strokeWidth={3}
        />
      </MapView>

      {/* 주문 상태 카드 */}
      <View style={styles.statusCard}>
        <OrderStatusTimeline
          status={order?.status}
          estimatedTime={order?.estimatedDeliveryTime}
        />

        {order?.rider && (
          <RiderInfo
            rider={order.rider}
            onCall={() => callRider(order.rider.phone)}
            onMessage={() => openChat(order.id)}
          />
        )}

        <OrderSummary items={order?.items} total={order?.totalAmount} />
      </View>
    </View>
  );
};

// 라이더 앱 컴포넌트
export const RiderDeliveryScreen: React.FC = () => {
  const [currentDelivery, setCurrentDelivery] = useState<Delivery>();
  const [isNavigating, setIsNavigating] = useState(false);

  useEffect(() => {
    // 백그라운드 위치 추적
    startBackgroundLocationTracking();

    // 새 배달 알림 수신
    const unsubscribe = subscribeToDeliveryOffers((offer) => {
      showDeliveryOffer(offer);
    });

    return () => {
      stopBackgroundLocationTracking();
      unsubscribe();
    };
  }, []);

  const handleDeliveryComplete = async () => {
    try {
      // 사진 촬영
      const photo = await captureDeliveryPhoto();

      // 완료 처리
      await api.completeDelivery({
        deliveryId: currentDelivery.id,
        photo: photo,
        location: await getCurrentLocation(),
        completedAt: new Date(),
      });

      // 다음 배달 확인
      checkNextDelivery();
    } catch (error) {
      showError("배달 완료 처리 중 오류가 발생했습니다");
    }
  };

  return (
    <View style={styles.container}>
      {currentDelivery ? (
        <>
          <DeliveryMap
            pickup={currentDelivery.pickupLocation}
            delivery={currentDelivery.deliveryLocation}
            onStartNavigation={() => setIsNavigating(true)}
          />

          <DeliveryDetails
            delivery={currentDelivery}
            onPickupComplete={() => updateDeliveryStatus("PICKED_UP")}
            onDeliveryComplete={handleDeliveryComplete}
          />

          <CustomerContact
            customer={currentDelivery.customer}
            onCall={() => callCustomer(currentDelivery.customer.phone)}
            onMessage={() => openCustomerChat(currentDelivery.id)}
          />
        </>
      ) : (
        <WaitingForDelivery
          onToggleStatus={(online) => updateOnlineStatus(online)}
          earnings={todayEarnings}
          deliveryCount={todayDeliveryCount}
        />
      )}
    </View>
  );
};

데이터 분석 시스템

실시간 분석 파이프라인

# 배민 데이터 분석 시스템
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql.types import *
import mlflow

class BaeminAnalytics:
    def __init__(self):
        self.spark = SparkSession.builder \
            .appName("BaeminRealtimeAnalytics") \
            .config("spark.streaming.kafka.maxRatePerPartition", "10000") \
            .getOrCreate()

    def analyze_order_patterns(self):
        # Kafka에서 주문 스트림 읽기
        order_stream = self.spark \
            .readStream \
            .format("kafka") \
            .option("kafka.bootstrap.servers", "kafka:9092") \
            .option("subscribe", "orders") \
            .load()

        # 주문 데이터 파싱
        orders = order_stream.select(
            from_json(col("value").cast("string"), self.order_schema).alias("data")
        ).select("data.*")

        # 실시간 메트릭 계산
        # 1. 지역별 주문량
        area_orders = orders \
            .withWatermark("timestamp", "10 minutes") \
            .groupBy(
                window("timestamp", "5 minutes"),
                "delivery_area"
            ) \
            .agg(
                count("order_id").alias("order_count"),
                avg("total_amount").alias("avg_order_value"),
                avg("delivery_time").alias("avg_delivery_time")
            )

        # 2. 음식점별 인기도
        restaurant_popularity = orders \
            .groupBy(
                window("timestamp", "1 hour"),
                "restaurant_id"
            ) \
            .agg(
                count("order_id").alias("order_count"),
                sum("total_amount").alias("total_revenue"),
                collect_list("items").alias("popular_items")
            )

        # 3. 피크 타임 분석
        peak_time_analysis = orders \
            .withColumn("hour", hour("timestamp")) \
            .groupBy("hour") \
            .agg(
                count("order_id").alias("order_count"),
                avg("preparation_time").alias("avg_prep_time"),
                avg("delivery_time").alias("avg_delivery_time")
            )

        # 결과 저장
        area_orders.writeStream \
            .outputMode("update") \
            .format("console") \
            .start()

        # Elasticsearch로 전송
        restaurant_popularity.writeStream \
            .outputMode("update") \
            .format("es") \
            .option("es.resource", "baemin/restaurant_metrics") \
            .start()

    def predict_delivery_time(self):
        # 특징 엔지니어링
        features = VectorAssembler(
            inputCols=[
                "restaurant_prep_time",
                "distance",
                "time_of_day",
                "day_of_week",
                "weather_condition",
                "rider_availability",
                "traffic_index",
                "order_item_count"
            ],
            outputCol="features"
        )

        # 모델 학습
        from pyspark.ml.regression import GBTRegressor

        gbt = GBTRegressor(
            featuresCol="features",
            labelCol="actual_delivery_time",
            maxDepth=5,
            maxIter=100
        )

        # 파이프라인
        pipeline = Pipeline(stages=[features, gbt])

        # 학습
        model = pipeline.fit(training_data)

        # MLflow 저장
        with mlflow.start_run():
            mlflow.spark.log_model(model, "delivery_time_predictor")
            mlflow.log_metric("rmse", evaluator.evaluate(predictions))

        return model

    def analyze_rider_performance(self):
        # 라이더 성과 분석
        rider_metrics = self.spark.sql("""
            WITH rider_stats AS (
                SELECT
                    rider_id,
                    DATE(delivery_time) as delivery_date,
                    COUNT(*) as delivery_count,
                    AVG(delivery_duration) as avg_delivery_time,
                    AVG(customer_rating) as avg_rating,
                    SUM(delivery_fee) as total_earnings,
                    AVG(distance) as avg_distance
                FROM deliveries
                WHERE delivery_status = 'COMPLETED'
                GROUP BY rider_id, DATE(delivery_time)
            )
            SELECT
                rider_id,
                AVG(delivery_count) as avg_daily_deliveries,
                AVG(avg_delivery_time) as overall_avg_time,
                AVG(avg_rating) as overall_rating,
                AVG(total_earnings) as avg_daily_earnings,
                STDDEV(delivery_count) as delivery_consistency
            FROM rider_stats
            GROUP BY rider_id
        """)

        # 라이더 등급 분류
        def classify_rider(row):
            score = (
                row.overall_rating * 0.4 +
                (30 - row.overall_avg_time) / 30 * 0.3 +
                row.avg_daily_deliveries / 50 * 0.2 +
                (1 - row.delivery_consistency / 10) * 0.1
            )

            if score > 0.8:
                return 'PLATINUM'
            elif score > 0.6:
                return 'GOLD'
            elif score > 0.4:
                return 'SILVER'
            else:
                return 'BRONZE'

        rider_metrics = rider_metrics.withColumn(
            "tier",
            udf(classify_rider, StringType())(struct("*"))
        )

        return rider_metrics

# A/B 테스트 분석
class ABTestAnalyzer:
    def __init__(self):
        self.experiments = {}

    def analyze_experiment(self, experiment_id: str):
        # 실험 데이터 로드
        experiment_data = self.load_experiment_data(experiment_id)

        # 통계 분석
        from scipy import stats

        control_group = experiment_data[experiment_data.group == 'control']
        treatment_group = experiment_data[experiment_data.group == 'treatment']

        # 주요 메트릭 비교
        metrics = {
            'conversion_rate': {
                'control': control_group.converted.mean(),
                'treatment': treatment_group.converted.mean()
            },
            'avg_order_value': {
                'control': control_group.order_value.mean(),
                'treatment': treatment_group.order_value.mean()
            },
            'retention_rate': {
                'control': control_group.returned.mean(),
                'treatment': treatment_group.returned.mean()
            }
        }

        # 통계적 유의성 검정
        results = {}
        for metric in metrics:
            t_stat, p_value = stats.ttest_ind(
                control_group[metric],
                treatment_group[metric]
            )

            results[metric] = {
                'lift': (metrics[metric]['treatment'] - metrics[metric]['control']) / metrics[metric]['control'],
                'p_value': p_value,
                'significant': p_value < 0.05
            }

        return results

기술 스택 요약:

  • Backend: Java (Spring), Kotlin, Node.js
  • Frontend: React Native, React, Vue.js
  • 데이터베이스: MySQL, MongoDB, Redis
  • 메시지큐: Kafka, RabbitMQ
  • 인프라: AWS, Kubernetes
  • 데이터: Spark, Hadoop, Presto
  • 모니터링: Pinpoint, Grafana
  • AI/ML: TensorFlow, PyTorch

💡 핵심 기술 인사이트

1. O2O 플랫폼의 정수

  • 온라인-오프라인 완벽한 연결
  • 실시간 매칭 알고리즘
  • 위치 기반 최적화

2. 대규모 트래픽 처리

  • 피크 타임 대응
  • 마이크로서비스 아키텍처
  • 탄력적 확장

3. 라이더 중심 설계

  • 효율적인 배차 시스템
  • 수익 최적화
  • 안전 관리

📈 성과 & 지표

기술적 성과

  • 주문 처리: 초당 10,000+ 건
  • 평균 배달 시간: 30분 이내
  • 시스템 가용성: 99.9%+
  • 앱 크래시율: < 0.1%
  • API 응답시간: P50 < 50ms

비즈니스 영향

  • 시장 점유율: 한국 배달앱 1위
  • 거래액: 연간 20조원+
  • 제휴 음식점: 30만+ 개
  • DT 인수: 4조원 (2019년)

🎓 배민에서 배울 점

✅ 적용 가능한 패턴

  1. 3자 매칭: 고객-음식점-라이더
  2. 실시간 추적: GPS 기반 투명성
  3. 동적 가격: 수요공급 기반
  4. 퀵커머스: 즉시 배달 시스템
  5. 데이터 기반: A/B 테스트 문화

❌ 주의사항

  1. 운영 복잡도: 3자 조율의 어려움
  2. 품질 관리: 음식점 퀄리티
  3. 라이더 관리: 고용 vs 파트너
  4. 경쟁 심화: 쿠팡이츠 등

📚 추천 리소스


🔮 미래 전망

현재 집중 분야

  1. 배민1: 한 번에 여러 주문 배달
  2. B마트: 즉시 배달 확대
  3. 로봇 배달: 자율주행 로봇
  4. 해외 진출: 동남아시아 확대
  5. 친환경: 일회용품 감소

기술 투자 영역

  • AI/ML: 수요 예측, 라우팅
  • 자율주행: 배달 로봇
  • 드론: 원거리 배달
  • 음성 주문: AI 스피커
  • 메타버스: 가상 레스토랑

"배달의민족은 기술로 '문 앞으로 배달되는 일상의 행복'을 만들어갑니다."

  • 우아한형제들 기술 조직

마지막 업데이트: 2025-01-28

Found this helpful? Share it with others!
Tweet

🔗 Related Content

You might also be interested in these articles

🏢 company

🏢 카카오 기술 스택 분석

카카오가 5천만 한국인의 일상을 연결하는 기술 스택 심층 분석 - Java, Kotlin, MySQL로 구축한 국민 메신저부터 슈퍼앱까지

32 min read
kakao, java +9
Read more
🏢 company

🏢 Airbnb 기술 스택 분석

에어비앤비가 전 세계 400만 개의 숙소를 연결하는 글로벌 마켓플레이스 기술 스택 심층 분석 - Ruby on Rails에서 Service-Oriented Architecture로의 진화

24 min read
airbnb, ruby +12
Read more
🏢 company

🏢 Canva 기술 스택 분석

캔바가 1억 명 이상의 사용자에게 쉽고 강력한 디자인 도구를 제공하는 기술 스택 심층 분석 - Java, TypeScript, AWS로 구축한 대규모 디자인 플랫폼

32 min read
canva, java +10
Read more
🏢 company

🏢 당근마켓 기술 스택 분석

당근마켓이 동네 커뮤니티를 혁신하며 유니콘으로 성장한 기술 스택 심층 분석 - Ruby on Rails, Kotlin, Swift로 만든 하이퍼로컬 플랫폼

48 min read
karrot, daangn +9
Read more
🏢 company

🏢 토스 기술 스택 분석

토스가 한국 금융을 혁신하며 유니콘으로 성장한 기술 스택 심층 분석 - Node.js, React Native, Kotlin으로 만든 모바일 금융 플랫폼

39 min read
toss, nodejs +8
Read more
🏢 company

🏢 네이버 기술 스택 분석

네이버가 대한민국 최대 검색 포털에서 글로벌 기술 기업으로 성장한 기술 스택 심층 분석 - Java, Spring, Hadoop으로 구축한 초대규모 플랫폼

34 min read
naver, java +10
Read more

Found this helpful?

Help us improve this content by contributing on GitHub or sharing your feedback with the community.