🏢 배달의민족 기술 스택 분석
배달의민족이 한국 배달 문화를 혁신하며 동남아로 확장한 기술 스택 심층 분석 - 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