Skip to content

Latest commit

Β 

History

History
547 lines (438 loc) Β· 17.6 KB

booking.md

File metadata and controls

547 lines (438 loc) Β· 17.6 KB

온라인 μ˜ˆμ•½ μ‹œμŠ€ν…œ 섀계 (ν˜Έν…”/λ ˆμŠ€ν† λž‘)

λ©΄μ ‘κ΄€: "ν˜Έν…”μ΄λ‚˜ λ ˆμŠ€ν† λž‘κ³Ό 같은 온라인 μ˜ˆμ•½ μ‹œμŠ€ν…œμ„ μ„€κ³„ν•΄μ£Όμ„Έμš”. μ‹€μ‹œκ°„ μ˜ˆμ•½, λ™μ‹œμ„± μ œμ–΄, μ˜ˆμ•½ ν™•μ • κΈ°λŠ₯이 ν•„μš”ν•©λ‹ˆλ‹€."

μ§€μ›μž: λ„€, λͺ‡ 가지 μš”κ΅¬μ‚¬ν•­μ„ ν™•μΈν•˜κ³  μ‹ΆμŠ΅λ‹ˆλ‹€.

  1. μ˜ˆμƒ μ‚¬μš©μž μˆ˜μ™€ 일일 μ˜ˆμ•½ κ±΄μˆ˜λŠ” μ–΄λŠ μ •λ„μΈκ°€μš”?
  2. μ˜ˆμ•½ κ°€λŠ₯ μ‹œκ°„ λ‹¨μœ„μ™€ μ·¨μ†Œ 정책은 μ–΄λ–»κ²Œ λ˜λ‚˜μš”?
  3. λ™μ‹œ μ˜ˆμ•½ 처리 μ‹œ μš°μ„ μˆœμœ„ κ·œμΉ™μ΄ μžˆλ‚˜μš”?
  4. 결제 μ‹œμŠ€ν…œ 연동이 ν•„μš”ν•œκ°€μš”?

λ©΄μ ‘κ΄€:

  1. DAU 50만, 일일 μ˜ˆμ•½ 10만 건
  2. 1μ‹œκ°„ λ‹¨μœ„ μ˜ˆμ•½, 24μ‹œκ°„ μ „ μ·¨μ†Œ κ°€λŠ₯
  3. μ„ μ°©μˆœ κΈ°λ³Έ, VIP μ‚¬μš©μž μš°μ„ μˆœμœ„ 지원
  4. 결제 μ‹œμŠ€ν…œ 연동 ν•„μš” (보증금 μ‹œμŠ€ν…œ)

1. μ˜ˆμ•½ μ‹œμŠ€ν…œ 핡심 둜직

@Service
@Slf4j
public class ReservationService {

    private final ReservationRepository reservationRepository;
    private final InventoryService inventoryService;
    private final LockService lockService;
    private final PaymentService paymentService;

    // 1. μ˜ˆμ•½ 생성 ν”„λ‘œμ„ΈμŠ€
    @Transactional
    public ReservationResult createReservation(ReservationRequest request) {
        String lockKey = generateLockKey(
                request.getResourceId(),
                request.getTimeSlot()
        );

        try {
            // λΆ„μ‚° 락 νšλ“
            boolean locked = lockService.acquire(lockKey,
                    Duration.ofSeconds(10));

            if (!locked) {
                throw new ConcurrentBookingException(
                        "Resource is being booked by another user");
            }

            // 재고 확인
            if (!inventoryService.checkAvailability(
                    request.getResourceId(),
                    request.getTimeSlot())) {
                throw new NoAvailabilityException("No availability");
            }

            // μ˜ˆμ•½κΈˆ 결제 처리
            PaymentResult payment = paymentService.processDeposit(
                    request.getUserId(),
                    request.getDepositAmount()
            );

            // μ˜ˆμ•½ 생성
            Reservation reservation = Reservation.builder()
                    .userId(request.getUserId())
                    .resourceId(request.getResourceId())
                    .timeSlot(request.getTimeSlot())
                    .status(ReservationStatus.CONFIRMED)
                    .paymentId(payment.getPaymentId())
                    .build();

            // 재고 차감
            inventoryService.decrementInventory(
                    request.getResourceId(),
                    request.getTimeSlot()
            );

            return reservationRepository.save(reservation);

        } catch (Exception e) {
            // 결제 μ·¨μ†Œ λ“± λ‘€λ°± 처리
            handleReservationFailure(request, e);
            throw e;
        } finally {
            lockService.release(lockKey);
        }
    }

    // 2. λ™μ‹œμ„± μ œμ–΄
    @Component
    public class DistributedLockService {
        private final RedisTemplate<String, String> redisTemplate;

        public boolean acquire(String key, Duration timeout) {
            String token = UUID.randomUUID().toString();

            return redisTemplate.opsForValue()
                    .setIfAbsent(key, token, timeout);
        }

        public void release(String key) {
            redisTemplate.delete(key);
        }
    }
}

2. 재고 관리 및 μ˜ˆμ•½ 확인 μ‹œμŠ€ν…œ

@Service
public class InventoryService {
    
    private final RedisTemplate<String, String> redisTemplate;
    private final InventoryRepository inventoryRepository;

    // 1. 재고 관리
    public class InventoryManager {
        
        // 재고 μ΄ˆκΈ°ν™” (일별/주별 λ‹¨μœ„)
        @Scheduled(cron = "0 0 0 * * *") // 맀일 μžμ •
        public void initializeInventory() {
            List<Resource> resources = resourceRepository.findAll();
            
            resources.forEach(resource -> {
                // ν–₯ν›„ 30일치 재고 μ΄ˆκΈ°ν™”
                LocalDate startDate = LocalDate.now();
                LocalDate endDate = startDate.plusDays(30);
                
                for (LocalDate date = startDate; 
                     date.isBefore(endDate); 
                     date = date.plusDays(1)) {
                    
                    initializeDailyInventory(resource, date);
                }
            });
        }

        private void initializeDailyInventory(
            Resource resource, LocalDate date) {
            
            // μ‹œκ°„λŒ€λ³„ 재고 μ„€μ •
            resource.getOperatingHours().forEach(hour -> {
                String inventoryKey = generateInventoryKey(
                    resource.getId(), 
                    date, 
                    hour
                );
                
                redisTemplate.opsForValue().set(
                    inventoryKey,
                    String.valueOf(resource.getCapacity()),
                    Duration.ofDays(31)
                );
            });
        }
    }

    // 2. 재고 확인 및 μ—…λ°μ΄νŠΈ
    public class InventoryUpdater {
        
        public boolean checkAndDecrementInventory(
            String resourceId, 
            LocalDateTime timeSlot) {
            
            String inventoryKey = generateInventoryKey(
                resourceId, 
                timeSlot.toLocalDate(), 
                timeSlot.getHour()
            );

            // Redis Transaction μ‚¬μš©
            return redisTemplate.execute(new SessionCallback<Boolean>() {
                @Override
                public Boolean execute(RedisOperations operations) {
                    operations.watch(inventoryKey);
                    
                    String currentValue = operations.opsForValue()
                        .get(inventoryKey);
                        
                    int currentInventory = 
                        Integer.parseInt(currentValue);
                        
                    if (currentInventory <= 0) {
                        return false;
                    }

                    operations.multi();
                    operations.opsForValue().set(
                        inventoryKey, 
                        String.valueOf(currentInventory - 1)
                    );
                    
                    return !operations.exec().isEmpty();
                }
            });
        }

        // 재고 볡ꡬ (μ˜ˆμ•½ μ·¨μ†Œ μ‹œ)
        public void incrementInventory(
            String resourceId, 
            LocalDateTime timeSlot) {
            
            String inventoryKey = generateInventoryKey(
                resourceId, 
                timeSlot.toLocalDate(), 
                timeSlot.getHour()
            );

            redisTemplate.opsForValue()
                .increment(inventoryKey);
        }
    }

    // 3. μ˜€λ²„λΆ€ν‚Ή 방지
    public class OverbookingPrevention {
        
        private final LoadingCache<String, Integer> inventoryCache;
        
        public OverbookingPrevention() {
            this.inventoryCache = Caffeine.newBuilder()
                .maximumSize(10_000)
                .expireAfterWrite(Duration.ofMinutes(1))
                .build(this::loadCurrentInventory);
        }

        public boolean isOverbookingRisk(
            String resourceId, 
            LocalDateTime timeSlot) {
            
            String inventoryKey = generateInventoryKey(
                resourceId, 
                timeSlot.toLocalDate(), 
                timeSlot.getHour()
            );

            // ν˜„μž¬ 재고 확인
            int currentInventory = inventoryCache.get(inventoryKey);
            
            // 진행 쀑인 μ˜ˆμ•½ 건수 확인
            int pendingReservations = getPendingReservationsCount(
                resourceId, 
                timeSlot
            );

            return (currentInventory - pendingReservations) <= 0;
        }
    }

    // 4. μ˜ˆμ•½ μƒνƒœ 관리
    @Service
    public class ReservationStateManager {
        
        private final KafkaTemplate<String, ReservationEvent> kafka;
        
        public void handleReservationStateChange(
            Reservation reservation, 
            ReservationStatus newStatus) {
            
            // μƒνƒœ λ³€κ²½ 기둝
            ReservationStatusChange statusChange = 
                ReservationStatusChange.builder()
                    .reservationId(reservation.getId())
                    .previousStatus(reservation.getStatus())
                    .newStatus(newStatus)
                    .timestamp(Instant.now())
                    .build();

            // 이벀트 λ°œν–‰
            ReservationEvent event = ReservationEvent.builder()
                .type(ReservationEventType.STATUS_CHANGED)
                .reservationId(reservation.getId())
                .status(newStatus)
                .timestamp(Instant.now())
                .build();

            kafka.send("reservation-events", event);

            // ν•„μš”ν•œ 후속 쑰치
            switch (newStatus) {
                case CANCELLED:
                    handleCancellation(reservation);
                    break;
                case NO_SHOW:
                    handleNoShow(reservation);
                    break;
                case COMPLETED:
                    handleCompletion(reservation);
                    break;
            }
        }

        private void handleCancellation(Reservation reservation) {
            // μ·¨μ†Œ 수수료 계산
            Money cancellationFee = calculateCancellationFee(
                reservation
            );
            
            if (cancellationFee.isPositive()) {
                paymentService.chargeCancellationFee(
                    reservation.getUserId(), 
                    cancellationFee
                );
            }

            // 재고 볡ꡬ
            inventoryService.incrementInventory(
                reservation.getResourceId(), 
                reservation.getTimeSlot()
            );
        }
    }
}

μ΄λŸ¬ν•œ 섀계λ₯Ό 톡해:

  1. 효율적인 재고 관리

    • Redis 기반의 μ‹€μ‹œκ°„ 재고 관리
    • νŠΈλžœμž­μ…˜μ„ ν†΅ν•œ λ™μ‹œμ„± μ œμ–΄
    • μΊμ‹œλ₯Ό ν†΅ν•œ μ„±λŠ₯ μ΅œμ ν™”
  2. μ˜€λ²„λΆ€ν‚Ή 방지

    • μ‹€μ‹œκ°„ 재고 확인
    • 진행 쀑인 μ˜ˆμ•½ κ³ λ €
    • μ•ˆμ „ λ§ˆμ§„ μ„€μ •
  3. μƒνƒœ 관리

    • 이벀트 기반 μƒνƒœ 관리
    • μ·¨μ†Œ μ •μ±… 적용
    • 후속 쑰치 μžλ™ν™”

λ₯Ό κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

λ©΄μ ‘κ΄€: 피크 μ‹œκ°„λŒ€μ˜ λŒ€λŸ‰ μ˜ˆμ•½ μš”μ²­μ€ μ–΄λ–»κ²Œ μ²˜λ¦¬ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?

3. λŒ€λŸ‰ μ˜ˆμ•½ 처리 μ‹œμŠ€ν…œ

@Service
public class HighLoadReservationService {

    // 1. μ˜ˆμ•½ μš”μ²­ νμž‰ μ‹œμŠ€ν…œ
    @Component
    public class ReservationQueue {
        private final PriorityBlockingQueue<ReservationRequest> requestQueue;
        private final KafkaTemplate<String, ReservationRequest> kafkaTemplate;
        
        public void enqueueReservation(ReservationRequest request) {
            // μš°μ„ μˆœμœ„ μ„€μ •
            int priority = calculatePriority(request);
            request.setPriority(priority);

            // VIP μ‚¬μš©μžλŠ” Kafka μš°μ„ μˆœμœ„ 큐둜 전솑
            if (isVipUser(request.getUserId())) {
                kafkaTemplate.send("vip-reservations", request);
            } else {
                // 일반 μ‚¬μš©μžλŠ” 일반 큐둜 전솑
                kafkaTemplate.send("regular-reservations", request);
            }
        }

        private int calculatePriority(ReservationRequest request) {
            int basePriority = 0;
            
            // VIP μƒνƒœμ— λ”°λ₯Έ κ°€μ€‘μΉ˜
            basePriority += getUserPriorityWeight(request.getUserId());
            
            // μ˜ˆμ•½ μ‹œκ°„λŒ€λ³„ κ°€μ€‘μΉ˜
            basePriority += getTimeSlotWeight(request.getTimeSlot());
            
            return basePriority;
        }
    }

    // 2. μš”μ²­ 처리 μŠ€μΌ€μ€„λŸ¬
    @Component
    public class RequestScheduler {
        
        @KafkaListener(topics = {"vip-reservations", "regular-reservations"}, 
                      containerFactory = "batchListener")
        public void processReservationBatch(List<ReservationRequest> batch) {
            // 배치 크기에 λ”°λ₯Έ 처리 쑰절
            int batchSize = batch.size();
            int workerThreads = calculateOptimalThreads(batchSize);
            
            ExecutorService executor = 
                Executors.newFixedThreadPool(workerThreads);
            
            List<CompletableFuture<ReservationResult>> futures = 
                batch.stream()
                    .map(request -> CompletableFuture.supplyAsync(
                        () -> processReservation(request), executor))
                    .collect(Collectors.toList());

            // κ²°κ³Ό μˆ˜μ§‘ 및 처리
            CompletableFuture.allOf(
                futures.toArray(new CompletableFuture[0]))
                .thenAccept(v -> handleBatchResults(futures));
        }

        private int calculateOptimalThreads(int batchSize) {
            return Math.min(
                batchSize, 
                Runtime.getRuntime().availableProcessors() * 2
            );
        }
    }

    // 3. λΆ€ν•˜ μ œμ–΄ μ‹œμŠ€ν…œ
    @Component
    public class LoadController {
        
        private final RateLimiter rateLimiter;
        private final CircuitBreaker circuitBreaker;
        
        public LoadController() {
            this.rateLimiter = RateLimiter.create(1000); // μ΄ˆλ‹Ή 1000개 μ œν•œ
            this.circuitBreaker = CircuitBreaker.builder()
                .failureRateThreshold(50)
                .waitDurationInOpenState(Duration.ofSeconds(10))
                .build();
        }

        public boolean shouldAcceptRequest() {
            if (!circuitBreaker.isAllowingRequests()) {
                return false;
            }

            return rateLimiter.tryAcquire();
        }

        @Scheduled(fixedRate = 1000)
        public void adjustRateLimit() {
            // μ‹œμŠ€ν…œ λΆ€ν•˜ μ§€ν‘œ μˆ˜μ§‘
            SystemMetrics metrics = collectSystemMetrics();
            
            // λ™μ μœΌλ‘œ 처리율 μ‘°μ •
            if (metrics.getCpuUsage() > 80) {
                rateLimiter.setRate(rateLimiter.getRate() * 0.8);
            } else if (metrics.getCpuUsage() < 50) {
                rateLimiter.setRate(rateLimiter.getRate() * 1.2);
            }
        }
    }

    // 4. μž₯μ•  볡ꡬ μ‹œμŠ€ν…œ
    @Component
    public class FailureRecoverySystem {
        
        private final ReservationRepository repository;
        private final KafkaTemplate<String, ReservationRetry> kafkaTemplate;

        @Scheduled(fixedRate = 5000)
        public void processFailedReservations() {
            List<Reservation> failedReservations = 
                repository.findByStatus(ReservationStatus.FAILED);
                
            for (Reservation reservation : failedReservations) {
                RetryContext context = buildRetryContext(reservation);
                
                if (shouldRetry(context)) {
                    kafkaTemplate.send("reservation-retries", 
                        new ReservationRetry(reservation, context));
                } else {
                    handlePermanentFailure(reservation);
                }
            }
        }

        private boolean shouldRetry(RetryContext context) {
            return context.getAttempts() < 3 && 
                   context.getLastAttempt()
                         .plus(getBackoffInterval(context.getAttempts()))
                         .isBefore(Instant.now());
        }

        private Duration getBackoffInterval(int attempts) {
            return Duration.ofSeconds((long) Math.pow(2, attempts));
        }
    }

    // 5. λͺ¨λ‹ˆν„°λ§ 및 μ•Œλ¦Ό
    @Component
    @Slf4j
    public class ReservationMonitor {
        
        private final MeterRegistry registry;
        private final AlertService alertService;

        public void recordReservationMetrics(ReservationResult result) {
            // μ„±λŠ₯ λ©”νŠΈλ¦­ 기둝
            registry.timer("reservation.processing.time")
                .record(result.getProcessingTime());
                
            // 성곡/μ‹€νŒ¨μœ¨ λͺ¨λ‹ˆν„°λ§
            if (result.isSuccess()) {
                registry.counter("reservation.success").increment();
            } else {
                registry.counter("reservation.failure").increment();
            }

            // μž„κ³„μΉ˜ 초과 μ‹œ μ•Œλ¦Ό
            if (result.getProcessingTime().toMillis() > 1000) {
                alertService.sendAlert(
                    AlertLevel.WARNING,
                    "High reservation processing time detected"
                );
            }
        }

        @Scheduled(fixedRate = 60000)
        public void checkSystemHealth() {
            HealthMetrics metrics = collectHealthMetrics();
            
            if (metrics.hasAnomalies()) {
                alertService.sendAlert(
                    AlertLevel.CRITICAL,
                    "System anomalies detected: " + metrics.getDetails()
                );
            }
        }
    }
}

μ΄λŸ¬ν•œ λŒ€λŸ‰ μ˜ˆμ•½ 처리 μ‹œμŠ€ν…œμ„ 톡해:

  1. 효율적인 μš”μ²­ νμž‰

    • μš°μ„ μˆœμœ„ 기반 처리
    • 배치 처리둜 μ„±λŠ₯ μ΅œμ ν™”
    • VIP μ‚¬μš©μž μš°μ„  처리
  2. λΆ€ν•˜ μ œμ–΄

    • 동적 처리율 μ‘°μ •
    • μ„œν‚· 브레이컀 νŒ¨ν„΄
    • μ‹œμŠ€ν…œ λΆ€ν•˜ λͺ¨λ‹ˆν„°λ§
  3. μž₯μ•  볡ꡬ

    • μžλ™ μž¬μ‹œλ„ λ©”μ»€λ‹ˆμ¦˜
    • μ§€μˆ˜ λ°±μ˜€ν”„ μ „λž΅
    • 영ꡬ μ‹€νŒ¨ 처리
  4. λͺ¨λ‹ˆν„°λ§

    • μ‹€μ‹œκ°„ μ„±λŠ₯ λͺ¨λ‹ˆν„°λ§
    • 이상 징후 감지
    • μžλ™ μ•Œλ¦Ό μ‹œμŠ€ν…œ

을 κ΅¬ν˜„ν•˜μ—¬ 피크 μ‹œκ°„λŒ€μ˜ λŒ€λŸ‰ μ˜ˆμ•½μ„ μ•ˆμ •μ μœΌλ‘œ μ²˜λ¦¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.