λ©΄μ κ΄: "νΈν μ΄λ λ μ€ν λκ³Ό κ°μ μ¨λΌμΈ μμ½ μμ€ν μ μ€κ³ν΄μ£ΌμΈμ. μ€μκ° μμ½, λμμ± μ μ΄, μμ½ νμ κΈ°λ₯μ΄ νμν©λλ€."
μ§μμ: λ€, λͺ κ°μ§ μꡬμ¬νμ νμΈνκ³ μΆμ΅λλ€.
- μμ μ¬μ©μ μμ μΌμΌ μμ½ κ±΄μλ μ΄λ μ λμΈκ°μ?
- μμ½ κ°λ₯ μκ° λ¨μμ μ·¨μ μ μ± μ μ΄λ»κ² λλμ?
- λμ μμ½ μ²λ¦¬ μ μ°μ μμ κ·μΉμ΄ μλμ?
- κ²°μ μμ€ν μ°λμ΄ νμνκ°μ?
λ©΄μ κ΄:
- DAU 50λ§, μΌμΌ μμ½ 10λ§ κ±΄
- 1μκ° λ¨μ μμ½, 24μκ° μ μ·¨μ κ°λ₯
- μ μ°©μ κΈ°λ³Έ, VIP μ¬μ©μ μ°μ μμ μ§μ
- κ²°μ μμ€ν μ°λ νμ (보μ¦κΈ μμ€ν )
@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);
}
}
}
@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()
);
}
}
}
μ΄λ¬ν μ€κ³λ₯Ό ν΅ν΄:
-
ν¨μ¨μ μΈ μ¬κ³ κ΄λ¦¬
- Redis κΈ°λ°μ μ€μκ° μ¬κ³ κ΄λ¦¬
- νΈλμμ μ ν΅ν λμμ± μ μ΄
- μΊμλ₯Ό ν΅ν μ±λ₯ μ΅μ ν
-
μ€λ²λΆνΉ λ°©μ§
- μ€μκ° μ¬κ³ νμΈ
- μ§ν μ€μΈ μμ½ κ³ λ €
- μμ λ§μ§ μ€μ
-
μν κ΄λ¦¬
- μ΄λ²€νΈ κΈ°λ° μν κ΄λ¦¬
- μ·¨μ μ μ± μ μ©
- νμ μ‘°μΉ μλν
λ₯Ό ꡬνν μ μμ΅λλ€.
λ©΄μ κ΄: νΌν¬ μκ°λμ λλ μμ½ μμ²μ μ΄λ»κ² μ²λ¦¬νμκ² μ΅λκΉ?
@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()
);
}
}
}
}
μ΄λ¬ν λλ μμ½ μ²λ¦¬ μμ€ν μ ν΅ν΄:
-
ν¨μ¨μ μΈ μμ² νμ
- μ°μ μμ κΈ°λ° μ²λ¦¬
- λ°°μΉ μ²λ¦¬λ‘ μ±λ₯ μ΅μ ν
- VIP μ¬μ©μ μ°μ μ²λ¦¬
-
λΆν μ μ΄
- λμ μ²λ¦¬μ¨ μ‘°μ
- μν· λΈλ μ΄μ»€ ν¨ν΄
- μμ€ν λΆν λͺ¨λν°λ§
-
μ₯μ 볡ꡬ
- μλ μ¬μλ λ©μ»€λμ¦
- μ§μ λ°±μ€ν μ λ΅
- μꡬ μ€ν¨ μ²λ¦¬
-
λͺ¨λν°λ§
- μ€μκ° μ±λ₯ λͺ¨λν°λ§
- μ΄μ μ§ν κ°μ§
- μλ μλ¦Ό μμ€ν
μ ꡬννμ¬ νΌν¬ μκ°λμ λλ μμ½μ μμ μ μΌλ‘ μ²λ¦¬ν μ μμ΅λλ€.