티스토리 뷰
보상 트랜잭션을 사용한 데이터 정합성 유지
MSA 환경에서 모놀리식 아키텍처와 크게 다른 점은 트랜잭션의 복잡성이라고 할 수 있다. 모놀리식 아키텍처에서는 여러 서비스가 하나의 애플리케이션 안에 포함되어 단일 데이터베이스 내에서 ACID(Atomicity, Consistency, Isolation, Durability) 특성을 가진 로컬 트랜잭션으로 모든 작업을 처리할 수 있다. 이는 서비스 간 호출이 내부 메서드 호출로 이루어지기 때문에 트랜잭션 범위 관리가 쉽고 분산 시스템 환경에 비해 구현이 쉽다는 장점이 있다.
MSA 환경에서는 각 서비스가 자신의 데이터베이스를 가진다. 또한, 서비스 간 호출이 네트워크를 통해 이루어지기 때문에 모놀리식 아키텍처와 달리 분산 트랜잭션 상황이 발생한다. 따라서 사용자가 '주문'을 요청하면 여러 서비스 간의 네트워크가 이루어져 데이터 정합성을 유지하기 위한 트랜잭션 처리의 복잡성이 크게 높아진다. 분산 시스템에서는 2PC(Two-Phase Commit)이나 Saga 패턴의 보상 트랜잭션을 통해 실패 시 보상 동작으로 데이터 정합성을 유지하는 방법이 있다.
GitHub - f-lab-edu/idolu
Contribute to f-lab-edu/idolu development by creating an account on GitHub.
github.com
위 IDOLU 프로젝트를 진행하면서 분산 시스템에서 데이터 정합성을 지키기 위해 보상 트랜잭션을 사용한 비즈니스 로직에 대해 설명하고자 한다.
주문 로직 소개


진행한 프로젝트에서는 사용자, 상품, 주문, 결제(Toss Payments)로 나뉘어져 있다. 결제는 결제 대행사인 Toss Payments를 통해 결제를 처리했다. 사용자가 주문 페이지에서 '주문하기' 버튼을 누르면 '주문' 서비스가 사용자 요청을 받아 사용자 인증, 재고 확인, 결제 완료까지 필요한 로직을 수행한다. 이 때 서비스 간의 통신은 사용자 인증 및 재고 확인의 결과를 알고 넘어가야하기 때문에 왼쪽 그림처럼 HTTP 방식을 사용하여 동기적으로 처리를 진행했다.
상품 서비스에서 재고 차감이 완료된 후에 주문의 마지막 과정인 결제에서 잔액 부족 등의 이유로 실패하는 경우에는 오른쪽 그림처럼 상품 서비스에 재고 원복 요청을 보내야 한다. 재고 원복 요청은 주문 로직 처리 과정 중에 사용자가 기다릴 필요가 없기 때문에 메시징 시스템을 도입하여 비동기 로직으로 분해할 수 있다.
주문 생성

사용자가 주문 페이지에서 '주문하기' 버튼을 클릭하면, 카드 정보를 입력할 수 있는 결제 위젯이 노출된다. 이 위젯을 통해 사용자는 직접 결제 정보를 입력하게 되며, 결제가 시작되기 전인 이 시점에 시스템은 주문 상품 내역과 함께 주문 상태를 NOT_STARTED로 데이터베이스에 저장한다.
이와 동시에, 전역적으로 유일한 주문 ID를 생성하여 클라이언트에 응답을 반환한다. 이 ID는 결제 흐름을 전반에서 멱등성 키(Idempotency Key)로 동작하며, 추후 결제 확정 요청 시 반드시 함께 전달되어야 서버는 전달받은 멱등성 키를 기준으로 이미 처리된 주문이라는 사실을 인식하고 중복 결제를 방지할 수 있다.
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final ProductAdapter productAdapter;
private final OrderAdapter orderAdapter;
public Mono<CheckoutResponse> checkout(CheckoutCommand command) {
return productAdapter.getProductInformation(command.getProductId())
.flatMap(product -> orderAdapter.checkoutOrder(createOrder(command).withOrderItem(createOrderItem(product, command))))
.map(order -> CheckoutResponse.builder()
.orderId(order.getOrderId())
.orderNo(order.getOrderNo())
.amount(order.getOrderItem().getAmount())
.build());
}
// ...
}
서버는 주문을 위한 NOT_STARTED 상태의 주문 정보를 저장한다. 이 과정은 다음과 같은 순서로 진행된다.
우선 productAdapter.getProductInformation()을 통해 WebClient를 사용하여 상품 정보를 외부 API로부터 조회한다. 상품 정보가 정상적으로 확인되면, 이를 바탕으로 주문 객체와 주문 상품 객체를 생성한 뒤, orderAdapter.checkoutOrder()를 호출하여 데이터베이스에 주문 정보와 상품 내역을 함께 저장한다.
이 모든 작업이 완료된 후에는, 주문에 대한 응답으로 멱등성 키 역할을 하는 orderNo를 사용자에게 반환한다. 이 orderNo는 이후 결제 확정 요청 시 중복 요청을 방지하기 위한 핵심 식별자로 사용된다.
주문 확인

결제 위젯을 통해 결제가 생성되면, 사용자는 성공 또는 실패 URL로 리다이렉션된다. 이 URL은 클라이언트에서 지정한 경로로, 결제 생성 결과에 따라 사용자 화면이 이동하는 흐름을 구성한다.
결제 생성이 성공적으로 이루어지면 클라이언트는 내부적으로 주문 서비스의 /confirm API를 호출하고, 이 승인 요청은 최종 재고 확인 및 결제 승인을 포함하여 실제 주문이 완료되는 중요한 단계이다. 즉, 주문 최종 플로우는 성공 URL -> 클라이언트 -> 주문 서비스 /confirm 요청 -> 재고 차감 및 결제 승인 완료 -> 주문 완료라는 절차를 주문이 최종 확정되고 마무리된다.
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final ProductAdapter productAdapter;
private final OrderAdapter orderAdapter;
private final PaymentExecutorAdapter paymentExecutorAdapter;
private final OrderFailureService orderFailureService;
public Mono<OrderConfirmationResponse> confirm(OrderConfirmCommand command) {
return orderAdapter.updatePaymentPaymentStatusToExecuting(command) // NOT_STARTED -> CONFIRM_PRODUCT_EXECUTING 으로 상태 업데이트
// 재고 확인 및 차감 요청
.filterWhen(order -> productAdapter.decreaseProductStock(ProductStockUpdateRequest.builder()
.orderNo(command.getOrderNo())
.productId(command.getProductId())
.stock(command.getQuantity())
.stockType("DECREASE")
.build()))
// 재고 차감 성공 시 CONFIRM_PRODUCT_EXECUTING -> CONFIRM_PAYMENT_EXECUTING으로 상태 업데이트
.flatMap(order -> orderAdapter.updateOrderStatus(order, OrderStatus.CONFIRM_PAYMENT_EXECUTING, "CONFIRMATION_PAYMENT_START"))
// 결제 승인 요청
.flatMap(order -> paymentExecutorAdapter.execute(command))
// 결제 승인 성공 시 CONFIRM_SUCCESS 상태 업데이트 및 승인 결과 업데이트
.flatMap(paymentExecutionResult ->
orderAdapter.finalizeOrderStatus(OrderStatusUpdateCommand.builder()
.paymentKey(paymentExecutionResult.getPaymentKey())
.orderNo(paymentExecutionResult.getOrderNo())
.orderStatus(paymentExecutionResult.toOrderStatus())
.extraDetails(paymentExecutionResult.getExtraDetails())
.build())
.thenReturn(paymentExecutionResult))
.map(paymentExecutionResult -> OrderConfirmationResponse.builder()
.status(paymentExecutionResult.toOrderStatus())
.build())
// 위 로직에서 예외 발생 시 해당 오퍼레이터에서 처리
.onErrorResume(error -> orderFailureService.handleOrderConfirmationError(error, command));
}
// ...
}
@Component
@RequiredArgsConstructor
public class OrderAdapter {
private final OrderRepository orderRepository;
@Transactional
public Mono<Order> updatePaymentPaymentStatusToExecuting(OrderConfirmCommand command) {
return checkPaymentOrderStatus(command.getOrderNo())
.filterWhen(order -> validateOrder(order, command))
.flatMap(order -> insertPaymentHistory(order, CONFIRM_PRODUCT_EXECUTING, "CONFIRMATION_PRODUCT_START").thenReturn(order))
.flatMap(order -> orderRepository.save(order.toExecutingWithPaymentKey(command.getPaymentKey())));
}
// ...
}
주문 확인 요청이 오면 다음 과정을 거친다.
- 적절한 주문 확인 요청이 왔는지 유효성 검사를 진행한 후 CONFIRM_PRODUCT_EXECUTING으로 업데이트한다.
- 상품 서비스에 재고 차감을 요청한다.
- 재고 차감이 성공했다면 주문 상태를 CONFIRM_PAYMENT_EXECUTING으로 업데이트한다.
- 결제 서비스(PG사)에 결제 승인 요청을 보낸다.
- 결제 승인에 성공하면 응답받은 정보를 바탕으로 주문 상태를 CONFIRM_SUCCESS로 업데이트 한 후, 사용자에게 최종 주문 상태를 포함한 응답 객체를 반환한다.
- 전체 흐름 중 어느 한 단계에서라도 오류가 발생하면, OrderFailureService를 통해 실패 처리를 위임한다.
@Slf4j
@Component
@RequiredArgsConstructor
public class PaymentExecutorAdapter {
private final WebClient tossWebClient;
public Mono<PaymentExecutionResult> execute(OrderConfirmCommand command) {
return tossWebClient.post() // POST 요청
.uri("/v1/payments/confirm")
.header("Idempotency-Key", command.getOrderNo())
.bodyValue(PaymentExecutionRequest.builder() // 결제 승인 RequestBody 구성
.paymentKey(command.getPaymentKey())
.orderId(command.getOrderNo())
.amount(command.getAmount().intValue())
.build())
// 응답 처리 및 예외 처리
.retrieve()
.onStatus(statusCode -> statusCode.is4xxClientError() || statusCode.is5xxServerError(), this::createTossPaymentException)
.bodyToMono(TossPaymentConfirmationResponse.class)
.map(response -> PaymentExecutionResult.builder()
.paymentKey(command.getPaymentKey())
.orderNo(command.getOrderNo())
.isSuccess(true)
.isFailure(false)
.isUnknown(false)
.isRetryable(false)
.extraDetails(PaymentExtraDetails.builder()
.type(PaymentType.from(response.getType()))
.method(PaymentMethod.from(response.getMethod()))
.approvedAt(LocalDateTime.parse(response.getApprovedAt(), DateTimeFormatter.ISO_OFFSET_DATE_TIME)) // yyyy-MM-dd'T'HH:mm:ss±hh:mm ISO 8601 형식
.orderName(response.getOrderName())
.paymentStatus(PaymentStatus.from(response.getStatus()))
.totalAmount(response.getTotalAmount())
.balanceAmount(response.getBalanceAmount())
.build())
.build())
.retryWhen(Retry.backoff(2, Duration.ofSeconds(1)).jitter(0.1) // 재시도
.filter(ex -> ex instanceof PaymentRequestException && ((PaymentRequestException) ex).getIsUnknown())
.doBeforeRetry(retrySignal -> log.info("retryCount: {}, errorCode: {}", retrySignal.totalRetries(), retrySignal.failure().toString()))
.onRetryExhaustedThrow((spec, retrySignal) -> retrySignal.failure())); // 재시도 모두 소진 시 발생한 예외 그대로 전달
}
private Mono<PaymentRequestException> createTossPaymentException(ClientResponse clientResponse) {
return clientResponse.bodyToMono(TossPaymentConfirmationResponse.TossFailureResponse.class)
.flatMap(response -> {
TossPaymentError tossPaymentError = TossPaymentError.from(response.getCode());
return Mono.error(PaymentRequestException.builder()
.errorCode(tossPaymentError.name())
.errorMessage(tossPaymentError.getDescription())
.isSuccess(tossPaymentError.isSuccess())
.isFailure(tossPaymentError.isFailure())
.isUnknown(tossPaymentError.isUnknown())
.build());
});
}
}
결제 서비스에 보내는 요청은 WebClient를 사용하여 PG사의 결제 승인 API에 POST 요청을 보낸다. Idempotency-Key로 orderNo를 지정하여 결제 요청이 중복되더라도 멱등하게 처리될 수 있도록 처리한다.
WebClient를 사용하여 외부 시스템과의 통신으로 인해 네트워크 오류나 장애 등 다양한 실패 상황이 발생할 수 있으며, 이에 대한 적절한 예외 처리가 중요하다. 위 로직에서는 PaymentRequestException 중 PG사로부터 UNKNOWN(알 수 없는 에러), PROVIDER_ERROR(일시적인 오류), CARD_PROCESSING_ERROR(카드사 오류) 등의 에러 코드로 응답이 오는 경우에 한하여 해당 요청에 대한 결과를 다시 확인하기 위해 재시도(backoff + jitter)를 수행한다. 이 때, PG사 측에서는 Idempotency-Key 헤더를 통해 결제 요청이 중복되지 않도록 멱등하게 처리할 수 있다.
@Component
@RequiredArgsConstructor
public class OrderAdapter {
// ...
@Transactional
public Mono<Boolean> finalizeOrderStatus(OrderStatusUpdateCommand command) {
return switch (command.getOrderStatus()) {
case CONFIRM_SUCCESS -> updateOrderStatusToSuccess(command);
case CONFIRM_FAILURE -> updateOrderStatusToFailure(command);
case CONFIRM_UNKNOWN -> updateOrderStatusToUnknown(command);
default -> Mono.error(new IllegalArgumentException("결제 상태(status: %s)는 올바르지 않습니다.".formatted(command.getOrderNo())));
};
}
// ...
}
최종 결제 승인까지 성공하면 위 OrderService.confirm 메서드 내 orderAdapter.finalizeOrderStatus()를 호출하여 CONFIRM_SUCCESS로 업데이트한 후 주문 완료를 사용자에게 응답합니다.
보상 트랜잭션을 사용하여 재고 데이터 최종적 일관성 유지하기
@Service
@RequiredArgsConstructor
public class OrderFailureService {
private final OrderAdapter orderAdapter;
private final Map<Class<? extends Throwable>, BiFunction<Throwable, OrderConfirmCommand, Mono<OrderConfirmationResponse>>> handlers = Map.of(
OrderException.class, this::handleOrderException,
ProductRequestException.class, this::handleProductRequestException,
PaymentRequestException.class, this::handlePaymentRequestException
);
public Mono<OrderConfirmationResponse> handleOrderConfirmationError(Throwable exception, OrderConfirmCommand command) {
return handlers.entrySet().stream()
.filter(e -> e.getKey().isAssignableFrom(exception.getClass()))
.findFirst()
.map(e -> e.getValue().apply(exception, command))
.orElseGet(() -> {
// TimeoutException 등의 예외는 UNKNOWN 상태로 처리
OrderStatus orderStatus = OrderStatus.CONFIRM_UNKNOWN;
OrderFailure orderFailure = OrderFailure.builder()
.errorCode(exception.getClass().getSimpleName())
.message(exception.getMessage())
.build();
return orderAdapter.finalizeOrderStatus(OrderStatusUpdateCommand.builder()
.paymentKey(command.getPaymentKey())
.orderNo(command.getOrderNo())
.orderStatus(orderStatus)
.orderFailure(orderFailure)
.build())
.map(result -> OrderConfirmationResponse.builder()
.status(orderStatus)
.failure(orderFailure)
.build());
});
}
// ...
private Mono<OrderConfirmationResponse> handlePaymentRequestException(Throwable exception, OrderConfirmCommand command) {
PaymentRequestException paymentRequestException = (PaymentRequestException) exception;
OrderStatus orderStatus = paymentRequestException.toOrderStatus();
OrderFailure orderFailure = OrderFailure.builder()
.errorCode(paymentRequestException.getErrorCode())
.message(paymentRequestException.getErrorMessage())
.build();
return orderAdapter.updateOrderStatusByPaymentRequestException(OrderStatusUpdateCommand.builder()
.paymentKey(command.getPaymentKey())
.orderNo(command.getOrderNo())
.orderStatus(orderStatus)
.orderFailure(orderFailure)
.productId(command.getProductId())
.quantity(command.getQuantity())
.build())
.map(result -> OrderConfirmationResponse.builder()
.status(orderStatus)
.failure(orderFailure)
.build());
}
}
결제 승인 과정 중 어느 한 단계에서라도 예외가 발생하면, 이 예외는 OrderFailureService로 위엄되어 처리된다. OrderFailureService는 예외 유형에 따라 적절한 주문 상태로 변경하고, 실패 원인을 기록하여 일관된 응답을 생성하는 역할을 수행한다.
주문 처리 과정에서 유효성 검증 실패나 재고 차감 실패와 같은 경우에는 즉시 주문 상태를 실패로 업데이트하고, 사용자에게 응답을 반환할 수 있다. 하지만 결제 승인 단계에서의 실패는 다르다. 이전 단계에서 이미 재고 차감이 성공된 상태이기 때문에 결제 승인에 실패하면 차감된 재고를 원복하는 로직이 반드시 뒤따라야 한다.
이때, 재고 원복을 WebClient 등의 동기적인 HTTP 요청으로 처리하면 사용자는 이 과정을 모두 기다려야 하므로 응답 시간이 길어질 수 밖에 없다. 하지만 재고 원복 자체는 사용제에게 즉각적인 결과를 제공할 필요가 없는 작업이다.
따라서, 이 재고 복원 처리를 비동기로 분리하면 사용제에게 빠르게 응답을 반환하면서도 시스템은 재고 정합성을 유지할 수 있다. 이 때 사용하는 패턴이 Transactional Outbox Pattern이다.
@Component
@RequiredArgsConstructor
public class OrderAdapter {
// ...
@Transactional
public Mono<Boolean> updateOrderStatusByPaymentRequestException(OrderStatusUpdateCommand command) {
return switch (command.getOrderStatus()) {
case CONFIRM_FAILURE -> updateOrderStatusToFailureByPaymentRequestException(command);
case CONFIRM_UNKNOWN -> updateOrderStatusToUnknown(command);
default -> Mono.error(new IllegalArgumentException("결제 상태(status: %s)는 올바르지 않습니다.".formatted(command.getOrderNo())));
};
}
private Mono<Boolean> updateOrderStatusToFailureByPaymentRequestException(OrderStatusUpdateCommand command) {
return updateOrderStatusToFailure(command)
.flatMap(result -> outboxAdapter.savePaymentFailureEventMessage(command))
.flatMap(eventMessagePublisher::publishEvent)
.thenReturn(true);
}
// ...
}
@Service
public class OrderEventMessagePublisher {
private final ApplicationEventPublisher eventPublisher;
private final TransactionalEventPublisher transactionalEventPublisher;
public OrderEventMessagePublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
this.transactionalEventPublisher = new TransactionalEventPublisher(eventPublisher);
}
public Mono<StockRollbackMessageCommand> publishEvent(StockRollbackMessageCommand command) {
return transactionalEventPublisher.publishEvent(command)
.thenReturn(command);
}
}
결제 승인 실패로 주문 상태 업데이트 시 updateOrderStatusToFailureByPaymentRequestException 내부 로직을 살펴보면 outbox 테이블에 재고 원복을 위한 메시지를 저장한 후, 해당 메시지를 OrderEventMessagePublisher를 통해 발행한다. 이 흐름은 모두 하나의 트랜잭션 내에서 수행되므로 원자성을 보장한다.
@Slf4j
@Component
@RequiredArgsConstructor
public class KafkaMessageSender {
private final ReactiveKafkaProducerTemplate<String, String> kafkaProducer;
private final OutboxAdapter outboxAdapter;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendMessageAfterCommit(StockRollbackMessageCommand command) {
dispatchStockRollbackMessage(command)
.onErrorContinue((e, o) -> log.error("sendMessageEvent", e))
.subscribe();
}
public Mono<Boolean> dispatchStockRollbackMessage(StockRollbackMessageCommand command) {
return kafkaProducer.send(
command.getTopic(),
command.getKey(),
command.getPayload())
.flatMap(senderResult -> {
if (senderResult.exception() != null) {
return outboxAdapter.updateMessageStatus(command, MessageStatus.SUCCESS);
}
return outboxAdapter.updateMessageStatus(command, MessageStatus.SUCCESS);
});
}
}
위 sendMessageAfterCommit 메서드는 StockRollbackMessageCommand 타입의 이벤트가 발생하고, 해당 트랜잭션이 커밋된 후에만 호출된다. 즉, DB에 oubox 메시지가 정상적으로 저장된 경우에만 실제 Kafka 메시지를 전송하도록 보장한다.
@Slf4j
@Service
@RequiredArgsConstructor
public class StockRollbackMessageRelayService {
private final OutboxRepository outboxRepository;
private final KafkaMessageSender messageSender;
@Scheduled(fixedDelay = 60, initialDelay = 60, timeUnit = TimeUnit.SECONDS)
public void relay() {
outboxRepository.findOutboxByTypeAndStatusInOrderByOutboxIdAsc(PAYMENT_CONFIRMATION_FAILURE, List.of(INIT, FAILURE))
.doOnNext(outbox -> log.info("send stock rollback message. outbox id: {}", outbox.getOutboxId()))
.flatMap(outbox -> messageSender.dispatchStockRollbackMessage(StockRollbackMessageCommand.builder()
.type(outbox.getType())
.topic(outbox.getTopic())
.key(outbox.getTopicKey())
.payload(outbox.getPayload())
.build()))
.subscribeOn(Schedulers.newSingle("stock-rollback-message-relay"))
.subscribe();
}
}
StockRollbackMessageRelayService는 Kafka 메시지 전송 실패 시 oubox 테이블에 남은 메시지를 주기적으로 재처리하는 역할을 수행한다. 비정상적인 메시지 전송 실패를 보완하고 재고 복원 메시지를 유실 없이 Kafka에 전달하는 기능을 담당한다.
메시지 상태가 INIT 또는 FAILURE로 남아있는 oubox 데이터를 조회해서 각 메시지를 위에서 살펴본 KafkaMessageSender를 통해 Kafka로 재전송한다. 전송 성공 시 메시지 상태는 SUCCESS로 업데이트된다.
재고 원복 메시지 소비
@Slf4j
@Component
@RequiredArgsConstructor
public class StockRollbackConsumer {
@Value("${spring.kafka.consumer.topics.order-stock-rollback-dlt}")
private String dltTopic;
private final ReactiveKafkaProducerTemplate<String, String> kafkaProducer;
private final JsonConverter jsonConverter;
private final ProductService productService;
private final Retry defaultBackoffRetry;
@Bean
public Disposable kafkaListener(ReactiveKafkaConsumerTemplate<String, String> consumerTemplate) {
return consumerTemplate
.receive()
.groupBy(record -> record.receiverOffset().topicPartition()) // Partition 별로 groupBy
.flatMap(partitions -> {
// Partition 별로 record 순차적으로 실행
return partitions.concatMap(record -> {
ProductStockUpdateCommand command = jsonConverter.toJson(record.value(), ProductStockUpdateCommand.class);
return productService.updateProductStock(command)
.retryWhen(defaultBackoffRetry)
.onErrorResume(error -> {
log.error("consume stock rollback message error", error);
return kafkaProducer.send(dltTopic, record.key(), record.value())
.thenReturn(true);
})
.doOnSuccess(result -> record.receiverOffset().acknowledge()); // commit 처리
});
})
.subscribe();
}
}
StockRollbackConsumer는 주문 서비스에서 전송한 주문 복원 메시지를 수신하여 상품 재고를 복원하는 역할을 수행한다. 실패 시에는 DLT(Dead Letter Topic)로 메시지를 전송하여 유실을 방지한다. 상품 서비스의 분산락과 낙관적 락을 사용한 재고 관련 비즈니스 로직은 다음 블로깅을 통해 설명할 예정이다.
주문 확인 전체 흐름

주문 확인 전체 흐름은 위 시퀀스 다이어그램을 통해 확인할 수 있다.
'Java > 트러블 슈팅' 카테고리의 다른 글
| 분산락과 낙관적 락을 사용한 데이터 정합성 지키기 (0) | 2025.08.03 |
|---|---|
| Spring WebFlux + Spring Security 적용 (0) | 2025.05.19 |
| nGrinder를 사용한 성능 테스트: 비관적 락 vs 분산 락 (1) | 2024.01.21 |
| MySQL Replication 적용하기 (1) | 2024.01.15 |
| 분산 락 사용 시 상위 트랜잭션이 있으면 안되는 이유 (2) | 2024.01.07 |
- Total
- Today
- Yesterday
- Kafka
- Redisson
- transaction
- postgresql
- pessimistic lock
- 구름톤챌린지
- 구름톤 챌린지
- socket
- redis session
- Java
- Synchronized
- 넥스트스탭
- spring session
- 카프카
- spring webflux
- 트랜잭션
- sql
- NeXTSTEP
- annotation
- 분산 락
- TDD
- 람다
- mdcfilter
- 낙관적 락
- nginx configuration
- 비관적 락
- jvm 메모리 구조
- mysql
- EKS
- nginx
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
