스프링 이벤트를 발행하여 트랜잭션과 관심사 분리하기

2025. 4. 29. 14:25·Backend/Spring Boot

Introduce

 

내가 개발중인 서비스에서 특정 상품을 결제하면 FCM을 통해 결제한 유저에게 알림을 전송하는 비즈니스 로직이 있다.

결제 내역을 바탕으로 사용자에게 알림을 전송해야 함으로 결제 처리와 알림 전송이 같은 트랜잭션에 묶이게 된다.

 

현재 비즈니스 로직의 흐름을 살펴보면

 

비즈니스 로직

 

1. 사용자가 상품을 주문한다.
2. 상품을 조회하고, 결제를 진행한다.
3. 재고를 차감하고, 결제 내역을 저장한다.
4. 결제가 완료되면 사용자에게 FCM을 전송하고, 알림내역을 저장한다.

 

다음과 같은 순서로 동작한다. 그리고 다음 로직들은 같은 트랜잭션으로 묶여있다.

 

결제는 외부 API를 통해 이루어진다. 그러므로 우리 서비스의 트랜잭션과는 무관하다.

그런데 만약 FCM 알림 저장 과정에서 예외가 발생하면, 전체 트랜잭션이 롤백되기 때문에 결제 내역이 저장되지 않는다. 그러나 이미 결제는 외부 서비스를 통해 완료된 상태이기 때문에, 우리 서비스에서만 결제가 완료되지 않은 것 처럼 보이게 되는 것이다.

Rollback

 

즉, 사용자는 돈을 지불했음에도 불구하고, 상품을 받을 수 없는 것이다.

 

이러한 문제는 외부 시스템과 내부 트랜잭션을 동일하게 묶었기 때문에 발생한다. 결제와 같이 민감한 작업이나 외부 서비스의 성공을 전제로 하는 작업은 내부 로직과 분리하여 관리하는 것이 안전하다.

 

이를 해결하기위해 트랜잭션이 안전하게 커밋된 이후에만 알림을 전송하는 구조로 변경해야 한다.

 


Introducing Events

 

트랜잭션이 안전하게 커밋된 이후에만 알림을 전송하도록 변경하기 위해, 나는 이벤트 기반 처리 방식을 도입했다.

 

외부 결제는 이미 독립적으로 진행되기 때문에 그 이후의 작업들(알림 전송, 알림 내역 저장)에서 예외가 발생하더라도 핵심 결제 로직은 영향받게 하고 싶지 않았다.

 

이벤트 기반 처리를 통해 핵심 트랜잭션 내에서는 결제 로직만 처리하고, 트랜잭션이 성공적으로 커밋 된 이후에 이벤트를 발행함으로써 부가적인 후처리는 별도로 안전하게 실행되도록 변경하고자 한다.

 

실제로 아마존 CTO인 Werner Vogels는 다음과 같이 말한 바 있다.

 

Everything fails, all the time.

 

그는 시스템은 언제든 실패할 수 있다는 전제 위에서 설계되어야 한다고 강조했다. 그리고 이를 해결할 수 있는 구조로

 

Go event-driven

 

이벤트 드리븐 아키텍처를 언급하였는데, 이는 시스템을 비동기 메시징 기반으로 설계함으로써 트랜잭션을 적절하게 분리하여 유연한 처리, Loose Coupling, 장애 격리와 복원력 확보를 강조한 것 이다.

 

이번에 이벤트 처리 도입 역시 이런 철학을 본받아 핵심 결제 로직의 안전성을 확보하면서도 부가 로직의 확장을 극대화하기 위한 선택이다.

 

📌 기대효과
  1. 트랜잭션 분리
    • 핵심 결제 로직과 부가 로직은 트랜잭션 커밋 이후로 실행되어 실패가 전파되지 않도록 변경
  2. 관심사 분리
    • 도메인과 후처리를 분리해 책임을 명확히 하고, 유지보수 향상
  3. 부가 로직의 확장성
    • 알림 전송 뿐만 아니라 포인트 적립, 쿠폰 발급등 다양한 부가 로직이 추가되더라도 쉽게 확장 가능
  4. 신뢰성 향상
    • 알림 전송 실패로 인해 결제 내역이 유실되는 문제 방지

 


Requirements

 

📌 기존 코드

 

먼저 의존성과 트랜잭션이 분리되지 않은 현재의 코드를 살펴보면

@Component
@RequiredArgsConstructor
public class PaymentUseCase {

	private final UserService userService;
	private final PaymentService paymentService;
	private final NotificationService notificationService;

	@Transactional
	public void pay(final Long paymentId, final Long userId) {
		Payment payment = paymentService.read(paymentId);
		User user = userService.read(userId);
		paymentService.pay(user, payment);
		notificationService.send(user, payment);
	}
}

 

`paymentService.pay()` 를 통해 외부 서비스와 연동하여 결제를 진행하고, 결제 내역을 저장한다.

그리고 결제 완료시`notificationService.send()` 를 통해 알림을 전송한다.

 

알림 내역을 나중에 확인하기 위해서 알림 전송시 데이터베이스에 저장한다고 가정했을 때 해당 메소드에서 오류가 발생해 트랜잭셕이 롤백될 경우 앞선 동작들도 모두 롤백되므로 결제 내역도 저장되지 않을 것이다. 하지만 결제는 외부 서비스를 이용하므로 돈은 빠져나갔다.

 

📌 요구사항 정의

 

현재 내가 개선하고자 하는 기능의 요구사항을 정리해보자

 

1. 결제 처리와 알림 전송 트랜잭션을 분리하여 결제 안전성 향상
2. 결제 로직과 알림 로직의 관심사를 분리하여 결합도 낮추기
3. 알림 전송 비동기 처리

 

 

해당 요구사항들을 스프링 이벤트를 통해 해결할 수 있다.

`ApplicationEventPublisher` 는 Spring의 이벤트 시스템에서 이벤트 발행자 역할을 한다. 내부적으로는 옵저버 패턴 을 기반으로 하며, 이벤트가 발행될 경우 이벤트를 구독하고 있는 `ApplicationListener` 를 구현한 모든 리스너들에게 전달된다. 이벤트 리스너는 적절한 동작을 수행하며, Spring이 IoC 컨테이너를 통해 자동으로 옵저버들을 등록, 관리한다.

 

Spring Event

 

이벤트를 도입할 경우 흐름을 살펴보면 결제 처리 -> 결제 완료 이벤트 발행 -> 결제 완료 이벤트 리스너 동작 -> 알림 서비스 순서로 이루어진다.

 


 

Separation of concerns 

 

스프링 이벤트를 통해 관심사를 분리해보자.

 

 

특정 타입의 이벤트를 전달하면, 그 타입을 파라미터로 받는 이벤트 리스너 메서드가 자동으로 호출된다. 그러므로 이벤트 타입이 이벤트 리스너를 결정하는 역할을 한다.

 

public record PaymentCompletedEvent(Payment payment, User user) {}

 

여기서 `PaymentCompletedEvent` 가 이벤트 리스너를 결정하는 역할을 할 것 이다.

 

그렇다면 해당 이벤트를 비즈니스 로직에서 발행해보자.

@Component
@RequiredArgsConstructor
public class PaymentUseCase {

	private final UserService userService;
	private final PaymentService paymentService;
	private final ApplicationEventPublisher applicationEventPublisher;

	@Transactional
	public void pay(final Long paymentId, final Long userId) {
		Payment payment = paymentService.read(paymentId);
		User user = userService.read(userId);
		paymentService.pay(user, payment);
		applicationEventPublisher.publishEvent(new PaymentCompletedEvent(payment, user));
	}
}

 

기존에는 `NotificationService`를 의존하고 있던 코드가 `ApplicationEventPublisher`를 의존하도록 변경하였고, `PaymentCompletedEventa` 객체를 생성한 뒤 `publishEvent` 메소드를 통해 이벤트를 발행하고 있다.

 

이벤트가 발행되면, 해당 이벤트 타입을 구독하고 있는 리스너가 자동으로 호출이 된다. 여기서는 `PaymentCompletedEvent` 를 구독하고 있는 `PaymentCompletedEventListener` 가 호출이 된다.

@Component
@RequiredArgsConstructor
public class PaymentCompletedEventListener {

	private final NotificationService notificationService;

	@EventListener
	public void sendCompletedMessage(final PaymentCompletedEvent event) {

		notificationService.send(event.user(), event.payment());
	}
}

 

해당 리스너는 `@EventListener` 를 통해 `PaymentCompletedEvent` 타입의 이벤트를 자동으로 수신하며, 이벤트가 발행되면 알림을 전송하는 `NotificationService.send()`를 호출한다.

 

실행 결과 1

간단하게 스프링 이벤트를 적용한 뒤 로그를 살펴보면 정상적으로 동작하고 있음을 알 수 있다. 이러한 구조로 결제 처리와 알림 전송 로직은 완전히 분리되었으며, 서로의 대해 전혀 알지 못하더라도 동작할 수 있다. 이로써 이벤트를 발행하여 관심사를 분리 하였다.

 

그렇다면 관심사도 분리하였으니 트랜잭션도 분리된 것 일까?

 

@Component
@RequiredArgsConstructor
@Slf4j
public class PaymentCompletedEventListener {

	@EventListener
	public void sendCompletedMessage(final PaymentCompletedEvent event) throws InterruptedException {

		log.info("PaymentCompletedEvent 를 수신하였습니다.");

		// notificationService.send(event.user(), event.payment());
		throw new IllegalArgumentException("일부로 오류 발생");
	}
}

 

이벤트 처리중 예외가 발생하는 상황을 테스트 해보기 위해서 일부로 예외를 발생시켜보았다.

 

에러 발생

 

원래 의도했던 동작대로 결제 내역 저장 후 이벤트 처리 로직에서 에러가 발생하였다. 만약 트랜잭션이 분리되어 부모 트랜잭션으로 에러가 전파되지 않는다면 롤백되지 않을 것이고, 결제 내역이 저장되어 있을 것이다.

 

트랜잭션 전파 확인

 

하지만 결제 내역을 조회해보니 어떠한 데이터도 조회되지 않는다. 즉, 그 말은 이벤트 리스너에서 발생한 오류가 부모 트랜잭션으로 전파되어 롤백된 것이다.

 

 


 

Transaction event listener

 

현재 플로우

 

현재 결제 처리 및 알림 전송 이벤트의 플로우다. 모든 작업이 같은 스레드의 같은 트랜잭션에서 작업이 처리가된다. 이벤트를 발행함으로써 관심사를 분리하는데는 성공했지만 트랜잭션이 분리되지는 않은 것 이다.

 

스프링에서 `@EventListener` 를 사용할 경우, 기본적으로 이벤트를 발행한 트랜잭션과 동일한 트랜잭션에서 실행된다. 따라서 리스너에서 예외가 발생하면 전체 트랜잭션이 롤백된다. 이러한 동작은 트랜잭션의 일관성을 유지하는데는 유리하지만, 현재 경우에는 적합하지 않다. 따라서 결제 처리와 알림 전송의 트랜잭션이 분리되어야 한다.

 

 

결제 완료 이벤트 리스너가 트랜잭션 1의 커밋 이후에 실행되도록 지정한다면, 이벤트 리스너에서 오류가 발생하더라도 기존 결제 처리 로직들은 롤백되지 않을 것이다.

 

📌 트랜잭션 이벤트 리스너

 

Spring에서는 트랜잭션이 성공적으로 완료되었을 때 이벤트를 처리할 수 있도록 지원한다.

 

`@EventListener` 대신 `@TransactionalEventListener`를 사용하면 이벤트를 발행한 트랜잭션의 커밋이 완료되는 경우에만 이벤트를 처리한다.

 

`@TransactionalEventListener`의 `phase` 속성에는 이벤트 처리 시점을 지정할 수 있으며 총 4가지 타입이 존재한다.

 

1. `BEFORE_COMMIT` - 트랜잭션이 커밋되기 전에 이벤트 처리

2. `AFTER_COMMIT` - 트랜잭션이 커밋되어 완료된 후 이벤트 처리

3. `AFTER_ROLLBACK` - 트랜잭션이 롤백된 후 이벤트 처리

4. `AFTER_COMPLETION` - 트랜잭션이 완료된 후(커밋 또는 롤백) 이벤트 처리

 

해당 어노테이션을 사용할 경우 default로는 `AFTER_COMMIT`이 적용되는 것을 볼 수 있다.

 

@Component
@RequiredArgsConstructor
@Slf4j
public class PaymentCompletedEventListener {

	private final NotificationService notificationService;

	@TransactionalEventListener
	public void sendCompletedMessage(final PaymentCompletedEvent event) {

		log.info("PaymentCompletedEvent 를 수신하였습니다.");

		// notificationService.send(event.user(), event.payment());
		throw new IllegalArgumentException("일부로 오류 발생");
	}
}

 

`@EventListener`를 `@TransactionalEventListener`로 변경하고, 이벤트 리스너에 오류를 발생시켜보자.

 

이전에는 이벤트 리스너에 에러가 전파되어 결제 내역이 롤백되었었다.

 

로그

 

결제 처리는 완료되었으며 결제 완료 이벤트를 발행하였다. 그리고 해당 이벤트 리스너에서 오류가 발생한 것을 확인할 수 있다. 그렇다면 과연 결제 내역은 롤백되지 않았을까? 확인해보자.

 

@TransactionEventListener 적용 결과

 

쿼리문을 통해 데이터베이스에 저장된 내역을 조회해본 결과 트랜잭션이 롤백되지 않고, 성공적으로 저장된 것을 확인할 수 있다.

이는 `@TransactionalEventListener`의 `AFTER_COMMIT`이 적용되어 결제 처리의 트랜잭션이 커밋된 후 이벤트 리스너가 동작하여 오류가 발생하더라도 이미 트랜잭션이 커밋되었음으로 롤백되지 않은 것이다.

 

그렇다면 이제 정상적인 비즈니스 로직으로 다시 변경해보자. 내가 궁극적으로 원하는 로직은 알림 전송 후 알림 내역을 저장하는 것이다.

 

@TransactionalEventListener
public void sendCompletedMessage(final PaymentCompletedEvent event) throws InterruptedException {

    log.info("PaymentCompletedEvent 를 수신하였습니다.");

    notificationService.send(event.user(), event.payment());
    notificationAppender.append(event.user());
}

 

`NotificationService.send`를 통해 알림을 전송하고, `NotificationAppender.save` 를 통해 알림 내역을 저장하자.

 

알림 내역 저장 로그

 

로그를 확인해보면 `결제 처리 -> 결제 완료 이벤트 발행 -> 알림 전송 -> 알림 내역 저장` 순서로 잘 동작하고 있는 것 같다.

 

 

하지만 저장된 결제 내역의 수 와 알림 내역의 수 를 확인해보면 결제 내역은 성공적으로 저장되는 반면 알림 내역은 저장되지 않았다.

 

AFTER_COMMIT

 

해당 내용은 `TransactionSynchronization`의 `afterCommit` 주석 부분에 설명되어 있다.

 

요약하자면, 이전의 이벤트를 발행하는 코드에서 이미 트랜잭션이 커밋 되었기 때문에 `AFTER_COMMIT` 이후에 새로운 트랜잭션을 수행할 경우 데이터 소스 상에서 트랜잭션을 커밋하지 않는다.

따라서 `PROPAGATION_REQUIRES_NEW` 옵션을 지정하지 않는다면 이벤트 리스너에서 트랜잭션에 의존한 로직을 실행했을 경우 해당 트랜잭션은 커밋되지 않는다는 거시다.

 

이 부분은 엄밀히 말하면 DB 트랜잭션과 스프링의 트랜잭션 컨텍스트의 생명 주기가 달라서 발생하는 문제인데, 해당 내용은 따로 블로그 글을 정리하는게 나을 것 같다. 따라서 해당 글에서는 이러한 문제가 있다는 것만 알고 넘어가자.

 


 

Separation of Transactions

 

앞서 `TransactionEventListener`를 `AFTER_COMMIT`으로 정의할 경우 리스너에서 다시 트랜잭션을 처리하면 해당 트랜잭션은 커밋되지 않는 현상이 있음을 발견하였다. 이는 앞서 언급하였듯이 DB 트랜잭션과 Spring 트랜잭션 컨텍스트의 생명 주기가 달라서 발생하는 문제다.

 

나는 `@TransactionEventListener`를 `AFTER_COMMIT`으로 정의함으로써 트랜잭션이 분리되었다고 생각하였다.

하지만 조금 분석해보면 `@TransactionEventListener`가 호출되는 시점은 DB 트랜잭션이 커밋된 이후지만, Spring 트랜잭션 컨텍스트는 종료되지 않은 상태이다. 이때 `@Transactional` 메서드를 호출하면, 기존 컨텍스트에 재합류하려 시도하지만, 실제 DB 트랜잭션은 종료되어 있어서 알림 내역을 저장할 수 없는 것이다. 즉, 트랜잭션이 분리된 것 처럼 보였지만 실제로는 전혀 분리되지 않은 것이다.

 

해당 문제를 해결하기 위해서는 명시적으로 새로운 트랜잭션을 호출하여 트랜잭션을 분리하여야 한다.

 

📌 Solution

 

새로운 트랜잭션을 명시적으로 생성하기 위해서 `@Transactional(propagation = Propagation.REQUIRES_NEW)`를 사용해보자.

 

@Component
@RequiredArgsConstructor
@Slf4j
public class PaymentCompletedEventListener {

    private final NotificationAppender notificationAppender;
    private final NotificationService notificationService;

    @TransactionalEventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendCompletedMessage(final PaymentCompletedEvent event) {

       log.info("PaymentCompletedEvent 를 수신하였습니다.");

       notificationService.send(event.user(), event.payment());
       notificationAppender.append(event.user());
    }
}

 

그리고 결제 내역과 알림 내역이 제대로 저장되었는지 확인해보자.

 

저장된 내역

 

결제 내역과 알림 내역이 모두 1개씩 잘 저장된 것을 확인할 수 있다.

 

REQUIRES_NEW

 

최종적으로 결제 요청이 들어오면 `PaymentService`의 메인 트랜잭션이 시작되고, 결제 처리 및 결제 내역 저장이 수행된다. 결제 완료 후에는 이벤트를 발행하고, 메인 트랜잭션은 정상적으로 커밋되고, 종료된다.

 

그 이후 `@TransactionalEventListener`가 `AFTER_COMMIT`으로 호출되고, 리스너 내부에서는 `@TransactionalEventListener(propagation = REQUIRES_NEW)`를 통해 새로운 트랜잭션을 시작하여 알림 전송 및 알림 내역 저장을 처리한다. 이로써 결제 처리 트랜잭션과 알림 전송 트랜잭션이 완전히 분리되어 한쪽의 실패가 다른 쪽에 영향을 주지 않도록 변경되었다.


 

Asynchronize

 

현재 결제 처리와 알림 전송 이벤트까지 동기적으로 동작하고 있다. 하지만 유저 입장에서 결제의 성공 유무만 기다리면 되지 알림 전송이 완료될때까지 기다릴 필요는 없다. 따라서 알림 전송에 대해서 비동기로 처리할 필요성이 있다.

 

스프링에서 이벤트 처리를 비동기로 수행하는 방법은 두 가지가 있다.

 

1. 이벤트 리스너 메서드에 `@Async`를 추가

2. `ApplicationEventMulticaster`를 커스텀하여 빈으로 등록

 

이 중 나는 `@Async`를 적용하는것을 선택하였다. 그 이유는 `ApplicationEventMulticaster`를 직접 등록하면 모든 이벤트 리스너를 비동기로 전역 처리할 수 있다는 장점은 있지만, 해당 설정을 직접 커스텀하고 등록해 줄 만큼 실질적인 이점이 있는지 의문이었다.

 

그리고 무엇보다도 `@Async`는 이벤트 리스너 메서드에 어노테이션만 명시하면 되기 때문에 적용 방식이 훨씬 간단하고 직관적이라고 생각한다. 특히, 여러 명이 함께 작업하는 프로젝트라면, 메서드에 `@Async`가 명시적으로 선언되어 있는 것이 "이 메서드는 비동기로 동작한다"는 의도를 코드 레벨에서 바로 파악할 수 있다는 점에서도 효과적이라고 생각한다.

 

물론 모든 이벤트 처리 정책을 항상 일관되게 비동기로 처리한다는 정책이 있다면 `ApplicationEventMulticaster`를 통해 비동기 처리를 일괄 적용하는 것이 더 좋은 선택일 수 있다.

 

나의 견해이지만 항상 정답은 없다. 다만 중요한 것은 명확한 기준과 타당한 이유를 바탕으로 선택이 이루어졌는가 라고 생각한다.

 

📌 Solution

 

먼저 `@Async`를 이용해 메서드를 비동기로 실행하기 위해서는 `@EnableAsync`를 추가해 비동기 처리를 활성화 해야한다.

@Configuration
@EnableAsync
public class AsyncConfig {
}

 

그런 다음 이벤트 리스너 메서드에서 알림을 전송하므로 해당 메서드에 `@Async` 를 추가해 호출한 스레드 이외의 다른 스레드에서 비동기로 실행되도록 한다.

 

@Component
@RequiredArgsConstructor
@Slf4j
public class PaymentCompletedEventListener {

    private final NotificationAppender notificationAppender;
	private final NotificationService notificationService;

	@Async
	@TransactionalEventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
	public void sendCompletedMessage(final PaymentCompletedEvent event) throws InterruptedException {

		Thread.sleep(10000);
		log.info("PaymentCompletedEvent 를 수신하였습니다.");
		notificationService.send(event.user(), event.payment());
        notificationAppender.append(event.user());
	}
}

 

`Thread.sleep`으로 스레드를 10초동안 멈추게 하여 비동기 로직이 정상적으로 동작하는지 검증해보자.

로직의 실제 플로우를 확인하기 위해서 컨트롤러 메소드에 로그를 추가하였다.

 

1. 결제가 완료 되었습니다. 이벤트를 발행합니다.
2. PaymentCompletedEvent를 수신하였습니다.
3. 알림을 전송하였습니다.
4. 알림 내역을 저장하였습니다.
5. pay() 메소드 종료

 

만약 비동기가 적용되어 있지 않다면 해당 순서로 로그가 출력이 될 것 이고, 적용되어 있다면  1 -> 5 -> 2 -> 3 -> 4 순서로 로그가 출력될 것이다.

 

@Async 결과

 

로그를 살펴보니 1 -> 5 -> 2 -> 3 -> 4  순서로 로그가 출력되는 것을 확인할 수 있다.

또한 해당 메서드들을 실행한 스레드를 살펴보면 서로 다른 스레드에서 메소드를 실행하였음을 알 수 있다.

 

최종적으로 메인 트랜잭션과 이벤트 리스너 비즈니스 로직이 서로 다른 스레드에서 독립적으로 실행되었음을 확인할수 있었다. 이를 통해 메인 서비스 로직은 빠르게 종료되고, 알림 전송과 알림 내역 저장은 백그라운드 스레드에서 별도로 처리되는 구조가 완성되었다.

 

그렇다면 얼마나 개선되었을까? 바로 확인해보자.

 

📌 K6 부하 테스트

 

나는 K6를 이용해 부하 테스트를 해볼 것이다.

 

 

간단하게 부하테스트를 위한 스크립트를 스윽스윽 작성해주고,

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
    stages: [
        { duration: '30s', target: 50 },
    ],
};

export default function () {
    const url = '테스트 URL';
    const payload = JSON.stringify({
        paymentId: 1,
        userId: 1,
    });

    const params = {
        headers: { 'Content-Type': 'application/json' },
    };

    http.post(url, payload, params);

    sleep(1);
}

 

나는 대충 GPT를 이용해서 스크립트문을 작성했다. 테스트 시나리오는 30초동안 50명의 유저가 요청을 보낸다고 가정하였다.

 

1. 비동기 미적용

비동기 미적용 테스트 결과 K6
비동기 미적용 테스트 결과 그라파나

 

비동기를 적용하지 않았을 경우 부하 테스트의 결과이다.

 

2. 비동기 적용

비동기 적용 테스트 결과 K6
비동기 적용 테스트 결과 그라파나

 

비동기를 적용하였을때 테스트 결과이다.

항목 비동기 미적용 비동기 적용 개선 포인트
평균 응답 시간 112ms 5.3ms 🚀 응답 속도 95% 개선
초당 요청 수 (TPS) 22.45 TPS 24.83 TPS 🚀 처리량 10% 증가
총 요청 수 698건 770건 🚀 서버 처리 여력 증가
에러율 0% 0% ✅ 안정성 유지

 

테스트 결과를 분석해보면 비동기 적용으로 서버의 응답 시간은 약 95% 개선되었고, 초당 처리량(TPS)도 약 10% 증가한 것을 확인할 수 있다.

'Backend > Spring Boot' 카테고리의 다른 글

동시성 문제에 대한 고찰, 점진적으로 접근하기  (0) 2025.03.17
'Backend/Spring Boot' 카테고리의 다른 글
  • 동시성 문제에 대한 고찰, 점진적으로 접근하기
WooJJam
WooJJam
  • WooJJam
    우쨈의 개발 블로그
    WooJJam
  • 전체
    오늘
    어제
    • 분류 전체보기 (11)
      • 끄적끄적 (1)
      • Backend (3)
        • Spring Boot (2)
        • MySQL (1)
      • DevOps (4)
        • Monitoring (3)
        • Deployment (1)
      • Computer Science (3)
        • Network (1)
        • Operating System (0)
        • Database (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 공지사항

  • 인기 글

  • 태그

    트랜잭션 분리
    phantom read
    2PL
    격리 수준
    3-Way Handshake
    비관적 락
    non repeatable
    TransactionalEventListener
    promtail
    낙관적 락
    무중단 배포
    plg stack
    로깅 시스템
    모니터링
    공간인덱스
    격리수준
    스프링 이벤트
    dirty read
    관심사 분리
    동시성
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
WooJJam
스프링 이벤트를 발행하여 트랜잭션과 관심사 분리하기
상단으로

티스토리툴바