🏢 배달의민족 기술 스택 분석
배달의민족이 한국 배달 문화를 혁신하며 동남아로 확장한 기술 스택 심층 분석 - Java, Spring, Kotlin으로 구축한 O2O 플랫폼
🏢 배달의민족 기술 스택 분석
한국 배달 문화를 재정의하고 동남아시아로 확장 중인 배달의민족의 기술 스택을 심층 분석합니다.
"좋은 음식을 먹고 싶은 곳에서" - 기술로 음식 배달의 새로운 기준을 만드는 혁신
📊 회사 개요
서비스 규모
- 앱 다운로드: 3,000만+ 회
- 제휴 음식점: 30만+ 개
- 배달 파트너: 8만+ 명
- 연간 주문: 2억+ 건
- 서비스 지역: 한국 + 동남아 100+ 도시
엔지니어링 조직
- 개발자: 800명+ (전체 직원 4,000+)
- 문화: "실패를 두려워하지 않는 도전"
- 조직: 서비스별 독립 셀
- 기술 철학: 꾸준한 개선, 실용적 접근
기술적 도전과제
- 실시간 매칭: 주문-음식점-라이더 최적화
- 피크 타임: 점심/저녁 트래픽 폭증
- 위치 정확도: 정확한 배달 주소
- 글로벌 확장: 각국 특성 대응
🏗️ 아키텍처 Overview
시스템 다이어그램
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Mobile │────►│ API │────►│ Order │ │ Apps │ │ Gateway │ │ Service │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────▼─────┐ ┌─────▼─────┐ │ │ Matching │ │ Payment │ │ │ Engine │ │ Service │ │ └───────────┘ └───────────┘ │ │ │ └────────────────────┤ │ ┌─────▼─────┐ ┌─────▼─────┐ │ Rider │ │ GPS │ │ Service │ │ Tracking │ └───────────┘ └───────────┘ 핵심 설계 원칙
- Reliability First: 안정성이 최우선
- Real-time Processing: 실시간 처리
- Microservices: 서비스별 독립성
- Data Driven: 데이터 기반 의사결정
- 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년)
🎓 배민에서 배울 점
✅ 적용 가능한 패턴
- 3자 매칭: 고객-음식점-라이더
- 실시간 추적: GPS 기반 투명성
- 동적 가격: 수요공급 기반
- 퀵커머스: 즉시 배달 시스템
- 데이터 기반: A/B 테스트 문화
❌ 주의사항
- 운영 복잡도: 3자 조율의 어려움
- 품질 관리: 음식점 퀄리티
- 라이더 관리: 고용 vs 파트너
- 경쟁 심화: 쿠팡이츠 등
📚 추천 리소스
🔮 미래 전망
현재 집중 분야
- 배민1: 한 번에 여러 주문 배달
- B마트: 즉시 배달 확대
- 로봇 배달: 자율주행 로봇
- 해외 진출: 동남아시아 확대
- 친환경: 일회용품 감소
기술 투자 영역
- AI/ML: 수요 예측, 라우팅
- 자율주행: 배달 로봇
- 드론: 원거리 배달
- 음성 주문: AI 스피커
- 메타버스: 가상 레스토랑
"배달의민족은 기술로 '문 앞으로 배달되는 일상의 행복'을 만들어갑니다."
- 우아한형제들 기술 조직
마지막 업데이트: 2025-01-28