알림 발행 실패 시 전체 트랜잭션 롤백 트러블 슈팅 - KimGyuBek/Threadly GitHub Wiki
- 서비스 구성: threadly-service + Kafka + Zookeeper
- 알림 발행 방식: 동기 처리 (트랜잭션 내에서 직접 Kafka 호출)
- PostLike 서비스에서 좋아요 처리 시 알림 동시 발행
테스트 대상: 전체 알림 서비스(게시글 좋아요, 댓글 생성, 팔로우 요청/수락 등) 중 게시글 좋아요 알림에 한정
-
Kafka 장애 상황에서 동기 처리의 문제점
- Kafka 브로커 다운 시 전체 트랜잭션 롤백
- 핵심 기능(좋아요)까지 실패하여 사용자 경험 저하
- 비즈니스 로직과 부가 기능(알림)의 강결합 문제
-
실제 테스트 시나리오
- KafkaException 유발: 알림 발행 과정 중 발생할 수 있는 다양한 오류 중 KafkaException을 의도적으로 유발하여 테스트
- 테스트 목적: 알림 발행 오류로 인한 전체 비즈니스 트랜잭션 롤백 문제 검증
- k6 부하 테스트 도구를 사용한 성능 및 안정성 측정
- duration 기반 테스트: 동일 시간(3분) 동안 최대 처리량으로 공정한 성능 비교
- 동기 vs 비동기 처리 방식의 실증적 비교
동기 방식 테스트 (sync-test.js):
// Kafka 예외를 고려한 좋아요 테스트
function likePostWithKafkaException(authToken, postId, vuId) {
const likeResponse = http.post(
`${BASE_URL}/api/posts/${postId}/likes`,
null,
{
headers: {'Authorization': `Bearer ${authToken}`},
tags: {endpoint: 'posts_like_sync'},
}
);
if (likeResponse.status === 500) {
// 예상된 시나리오: 동기 방식에서 Kafka 예외로 인한 트랜잭션 롤백
businessLogicSuccessRate.add(false);
kafkaFailureRate.add(true);
transactionRollbackRate.add(true);
}
}비동기 방식 테스트 (async-test.js):
// 비동기 방식의 좋아요 테스트
function likePostWithAsyncProcessing(authToken, postId, vuId) {
const likeResponse = http.post(
`${BASE_URL}/api/posts/${postId}/likes`,
null,
{
headers: {'Authorization': `Bearer ${authToken}`},
tags: {endpoint: 'posts_like_async'},
}
);
if (likeResponse.status === 200) {
// 비동기 방식의 핵심: Kafka 예외와 무관하게 200 응답
businessLogicSuccessRate.add(true);
coreFeatureAvailability.add(true);
}
}-
Before (동기 알림 발행):

동기 방식에서는 Kafka 예외로 인해 PostLikeCommandService 트랜잭션이 대부분 롤백됨. 전체 시스템 롤백률이 높게 나타나는 이유는 조회성 트랜잭션과 알림 트랜잭션이 포함된 전체 서비스 트랜잭션 통계이기 때문임.
-
After (비동기 이벤트 처리):

비동기 방식에서 전체 롤백률이 20%로 나타나는 것은 조회성 트랜잭션과 기타 서비스 트랜잭션이 포함된 수치임. 즉 PostLikeCommandService의 좋아요 기능은 100% 커밋 상태 유지, Kafka 예외가 핵심 비즈니스 로직에 완전히 격리됨.
| 메트릭 | 수치 | 분석 |
|---|---|---|
| 총 요청 수 | 3,430개 | 3분 duration 테스트 (10 VU) |
| 성공 요청 | 1,720개 (50.1%) | 절반만 성공 |
| 실패 요청 | 1,710개 (49.9%) | Kafka 예외로 인한 실패 |
| 비즈니스 로직 성공률 | 0% | 모든 좋아요 기능 실패 |
| 트랜잭션 롤백률 | 100% | 알림 실패 시 전체 롤백 |
| 평균 응답시간 | 26ms | - |
| P95 응답시간 | 40ms | 성공 응답 기준 |
| P99 응답시간 | 94ms | 성공 응답 기준 |
| 메트릭 | 수치 | 분석 |
|---|---|---|
| 총 요청 수 | 6,550개 | 3분 duration 테스트 - 성능 향상으로 더 많은 요청 처리 가능 |
| 성공 요청 | 6,550개 (100%) | 모든 요청 성공 |
| 실패 요청 | 0개 (0%) | Kafka 예외와 격리됨 |
| 비즈니스 로직 성공률 | 100% | 핵심 기능 완전 보장 |
| 트랜잭션 롤백률 | 0% | 알림과 무관하게 커밋 |
| 평균 응답시간 | 25ms | 동기 방식보다 빠름 |
| P95 응답시간 | 31ms | 22% 개선 (40ms → 31ms) |
| P99 응답시간 | 36ms | 62% 개선 (94ms → 36ms) |
@Service
@RequiredArgsConstructor
public class PostLikeCommandService implements LikePostUseCase {
private final NotificationService notificationService; // 동기 처리
private final CreatePostLikePort createPostLikePort;
@Transactional
@Override
public LikePostApiResponse likePost(LikePostCommand command) {
// 1. 비즈니스 로직 (좋아요 저장)
PostLike newLike = post.like(command.getUserId());
createPostLikePort.createPostLike(newLike); // 성공
// 2. 알림 발행 (동기) - Kafka 예외 발생 지점
try {
notificationService.publish(createNotificationCommand());
} catch (KafkaException e) {
log.error("Kafka 발행 실패: {}", e.getMessage());
throw e; // 예외 재발생 → 전체 롤백!
}
return new LikePostApiResponse(post.getPostId(), getLikeCount(command));
}
}문제점:
- Kafka 예외 시 100% 트랜잭션 롤백
- 좋아요 기능까지 완전 중단
- 사용자에게 500 에러 반환
@Service
@RequiredArgsConstructor
public class PostLikeCommandService implements LikePostUseCase {
private final ApplicationEventPublisher eventPublisher; // 비동기 처리
private final CreatePostLikePort createPostLikePort;
@Transactional
@Override
public LikePostApiResponse likePost(LikePostCommand command) {
// 1. 비즈니스 로직 (좋아요 저장)
PostLike newLike = post.like(command.getUserId());
createPostLikePort.createPostLike(newLike); // 성공
// 2. 이벤트 발행 (메모리상 저장) - 예외 없음
eventPublisher.publishEvent(PostLikedEvent.create(
post.getPostId(), command.getUserId(), post.getUserId()
));
return new LikePostApiResponse(post.getPostId(), getLikeCount(command));
// 트랜잭션 커밋 완료 → 좋아요 저장 성공!
}
}
// 비동기 이벤트 핸들러
@Component
@RequiredArgsConstructor
public class PostLikeEventHandler {
private final NotificationService notificationService;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async("eventExecutor")
public void handlePostLikedEvent(PostLikedEvent event) {
try {
// 트랜잭션 커밋 후 별도 스레드에서 처리
NotificationPublishCommand command = new NotificationPublishCommand(
event.getPostAuthorId(),
event.getUserId(),
NotificationType.POST_LIKE,
new PostLikeMeta(event.getPostId())
);
notificationService.publish(command);
} catch (Exception e) {
// 알림 실패해도 좋아요 기능에는 영향 없음
log.error("알림 발행 실패 (좋아요는 정상 저장됨): postId={}, error={}",
event.getPostId(), e.getMessage());
}
}
}개선 효과:
- Kafka 예외와 비즈니스 로직 완전 격리
- 좋아요 기능 100% 가용성 확보
- 알림은 백그라운드에서 처리
| 지표 | 동기 방식 | 비동기 방식 | 개선 효과 |
|---|---|---|---|
| 비즈니스 로직 성공률 | 0% | 100% | 100%p 개선 |
| 트랜잭션 롤백률 | 100% | 0% | 100%p 개선 |
| HTTP 에러율 | 49.85% | 0% | 49.85%p 개선 |
| 평균 응답시간 | 26ms | 25ms | 4% 개선 |
| P95 응답시간 | 40ms | 31ms | 22% 개선 |
| P99 응답시간 | 94ms | 36ms | 62% 개선 |
| 처리량 (RPS) | 19.0 req/s | 36.3 req/s | 91% 향상 |
| 반복 처리율 | 9.48 iter/s | 18.13 iter/s | 91% 향상 |
사용자 행동: 좋아요 버튼 클릭
시스템 응답: "Internal Server Error" (500)
실제 결과: 좋아요 저장 실패
성공률: 0% (기능 사용 불가)사용자 행동: 좋아요 버튼 클릭
시스템 응답: "좋아요 완료" (200 OK)
실제 결과: 좋아요 저장 성공, 알림만 지연 또는 실패
성공률: 100% (알림 발행 오류는 체감하지 못 함){
"business_logic_success_rate": 0,
// 0% 성공
"kafka_failure_rate": 1,
// 100% Kafka 실패
"estimated_rollback_rate": 1,
// 100% 롤백
"http_error_rate": 0.49854227405247814,
// 49.85% HTTP 에러
"worker_failure_rate_percent": 100
// 100% 워커 실패
}{
"business_logic_success_rate": 1,
// 100% 성공
"core_feature_availability": 1,
// 100% 핵심 기능 가용
"event_publishing_failure_rate": 0,
// 0% 이벤트 발행 실패
"http_error_rate": 0,
// 0% HTTP 에러
"worker_failure_rate_percent": 0
// 0% 워커 실패
}적용 범위: 알림 발행 과정에서 발생할 수 있는 모든 오류 (Kafka 브로커 장애, 네트워크 오류, 인증 실패, 직렬화 오류 등)
동기 방식 대응:
- 즉시 전체 핵심 기능(좋아요) 중단
- 사용자에게 500 Internal Server Error 반환
- 긴급 알림 서비스 복구 작업 필요
- 사용자 공지 필요 ("일시적 장애")
- 복구 완료까지 대상 서비스 중단
비동기 방식 대응:
- 핵심 기능(좋아요) 정상 동작 유지
- 사용자에게 200 OK 응답 (즉시 성공 처리)
- 알림만 백그라운드에서 지연 처리
- 여유 있는 알림 서비스 복구 작업 가능
- 사용자 공지 불필요 (핵심 기능 정상 운영)
- "좋아요를 눌렀는데 에러가 나요"
- "좋아요 기능이 작동하지 않아요"
- "서버 오류가 계속 발생해요"
- "알림이 조금 늦게 와요"
- "특정 알림이 안 와요"
좋아요 기능 관련 문의 대폭 감소
테스트 공정성 확보:
- duration 기반 테스트 (3분간 지속)
- 동일한 VU 수 (10개)
- 동일한 Kafka 장애 조건
- 총 요청 수 차이는 성능 향상의 결과 (동기: 3,430개 → 비동기: 6,550개)
# 알림 발행 시 KafkaException 발생하도록 유도 (동일 조건)
# 동기 방식 테스트 실행 (3분간 지속)
k6 run sync-test.js
# 결과: sync.json 파일 생성# 알림 발행 시 KafkaException 발생하도록 유도 (동일 조건)
docker-compose stop kafka
# 비동기 방식 테스트 실행 (3분간 지속)
k6 run async-test.js
# 결과: async.json 파일 생성- 완벽한 장애 격리: 비즈니스 로직 성공률 0% → 100%
- 트랜잭션 안정성: 롤백률 100% → 0% (완전 해소)
- 성능 대폭 향상: P99 응답시간 94ms → 36ms (62% 개선)
- 처리량 증가: 19 RPS → 36.3 RPS (91% 향상)
- 사용자 경험: HTTP 에러율 49.85% → 0% (완전 해소)
- 정량적 측정의 중요성: k6 부하 테스트를 통한 객관적 성능 비교
- 장애 격리 설계: 핵심 기능과 부가 기능의 명확한 분리 필요
- 비동기 처리의 효과: 단순한 아키텍처 변경으로 극적인 성능 개선 가능
- 사용자 중심 사고: 기술적 완벽함보다 사용자 경험 우선 고려
- 유사한 문제 상황에서 k6 테스트를 통한 정량적 분석 선행
- 핵심 기능과 부가 기능 분리 시 이벤트 기반 아키텍처 적극 활용
- 장애 시나리오별 테스트 자동화로 아키텍처 변경의 효과 지속 검증
- 모니터링 메트릭을 세분화하여 장애 영향도 최소화