Skip to content

Troubleshooting

TAEHEON KIM edited this page Jun 26, 2026 · 8 revisions

Troubleshooting

1. 동시 지원 레이스컨디션

증상: 20명이 동시에 같은 게시글에 지원 시 DataIntegrityViolationException 발생 + 일부 요청 500 에러

원인: SELECT (중복 체크) -> INSERT 사이에 다른 트랜잭션이 끼어들어 유니크 제약 위반

해결:

@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints(QueryHint(name = JAKARTA_LOCK_TIMEOUT, value = "3000"))
fun findByPostIdWithLock(postId: UUID): StudyPost?
  • 비관적 락으로 게시글 행 선점
  • 락 타임아웃 3초로 무한 대기 방지
  • DataIntegrityViolationException catch -> 409 응답 (예외를 에러가 아닌 정상 흐름으로 처리)
  • 락 획득 순서를 항상 게시글 -> 지원 단방향 고정으로 순환 대기(데드락) 제거

2. HikariCP 커넥션 고갈

증상: 1만 VUs 부하 시 p(95) 응답시간 9.76초, 커넥션 대기 타임아웃 다수 발생

원인: 기본 HikariCP 커넥션 풀 10개로 동시 요청 처리 불가

해결: maximum-pool-size: 50으로 상향

결과: p(95) 9.76s -> 7.1s (27% 개선)

커넥션 수 = (CPU 코어 수 × 2) + 유효 디스크 스핀들 수 공식 참고. t3.large 2 vCPU 기준 이론값은 ~5개이나 I/O 대기 시간이 길어 실측 기준 50개로 설정


3. 알림 처리로 인한 API 응답 지연

증상: 댓글 작성 API p(95) 급등, 에러율 87%

원인: 댓글 저장 + 알림 DB 저장 + Redis Pub/Sub 발행이 동기 처리 -> 커넥션 즉시 고갈

해결: @Async("notificationExecutor")로 알림 처리 분리

@Bean(name = ["notificationExecutor"])
fun notificationExecutor(): Executor {
    val executor = SimpleAsyncTaskExecutor("notification-")
    executor.setVirtualThreads(true)
    executor.setTaskDecorator(mdcTaskDecorator())
    return executor
}

ThreadPoolExecutor.DiscardPolicy 적용 — 풀 포화 시 알림만 조용히 버리고 API 정상 응답 유지

결과: 에러율 87% -> 2.3%, RPS 137 -> 940 (585% 향상)


4. 비동기 스레드에서 MDC 컨텍스트 소실

증상: @Async 처리 시 로그에 requestId, userId가 빠져 Kibana에서 요청 추적 불가

원인: MDC는 ThreadLocal 기반 -> 부모 스레드의 컨텍스트가 자식 스레드로 전파되지 않음

해결: TaskDecorator로 비동기 실행 전 MDC 컨텍스트를 캡처 후 주입

private fun mdcTaskDecorator(): TaskDecorator = TaskDecorator { task ->
    val mdcContext = MDC.getCopyOfContextMap()
    Runnable {
        try {
            if (mdcContext != null) MDC.setContextMap(mdcContext)
            task.run()
        } finally {
            MDC.clear()
        }
    }
}

5. 멀티 인스턴스 알림·채팅 미수신

증상: App1에 연결된 클라이언트가 보낸 채팅이 App2에 연결된 클라이언트에게 전달 안 됨

원인: WebSocket STOMP 세션은 인스턴스별로 독립적 -> 다른 인스턴스의 연결에 접근 불가

해결: Redis Pub/Sub으로 인스턴스 간 브로드캐스트

  • 채팅: RedisChatPublisherAdapter.publish() -> 전 인스턴스 구독 -> 각자 WebSocket 전달
  • 알림: Kafka Consumer -> Redis Pub/Sub -> 전 인스턴스 -> SSE 전달

6. Kafka 발행 실패 시 알림 유실

증상: Kafka 브로커 일시 장애 시 알림 이벤트 소멸

원인: 애플리케이션 이벤트 발행 후 Kafka 전송 실패 시 재시도 수단 없음

해결: Outbox 패턴 도입

알림 이벤트 발생
  -> Outbox DB 저장 (같은 트랜잭션)
  -> Kafka 발행 시도
  -> 실패 시 OutboxRetryScheduler가 PENDING 상태 재시도
  -> 영구 실패 시 FAILED 처리 후 DLT 격리

7. CI 빌드 시 jOOQ 코드 생성 실패

증상: GitHub Actions에서 generateJooq 태스크 실행 시 DB 연결 없어 빌드 실패

원인: jOOQ 코드 생성이 실제 DB 스키마를 읽어야 하는데 CI 환경에 DB 없음

해결: CI Gradle 명령에 -x generateJooq 추가로 코드 생성 태스크 스킵 생성된 jOOQ 클래스는 소스로 커밋해 CI에서 별도 생성 없이 빌드

Clone this wiki locally