Skip to content

Troubleshooting

TAEHEON KIM edited this page Jul 5, 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. μ•Œλ¦Ό 처리둜 μΈν•œ 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% ν–₯상)

---
3. 비동기 μŠ€λ ˆλ“œμ—μ„œ 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()
        }
    }
}

---
4. λ©€ν‹° μΈμŠ€ν„΄μŠ€ μ•Œλ¦Ό, μ±„νŒ… λ―Έμˆ˜μ‹ 

증상: App1에 μ—°κ²°λœ ν΄λΌμ΄μ–ΈνŠΈκ°€ 보낸 μ±„νŒ…μ΄ App2에 μ—°κ²°λœ ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ 전달 μ•ˆ 됨

원인: WebSocket STOMP μ„Έμ…˜μ€ μΈμŠ€ν„΄μŠ€λ³„λ‘œ 독립적 β†’ λ‹€λ₯Έ μΈμŠ€ν„΄μŠ€μ˜ 연결에 μ ‘κ·Ό λΆˆκ°€

ν•΄κ²°: Redis Pub/Sub으둜 μΈμŠ€ν„΄μŠ€ κ°„ λΈŒλ‘œλ“œμΊμŠ€νŠΈ
- μ±„νŒ…: RedisChatPublisherAdapter.publish() β†’ μ „ μΈμŠ€ν„΄μŠ€ ꡬ독 β†’ 각자 WebSocket 전달
- μ•Œλ¦Ό: Kafka Consumer β†’ Redis Pub/Sub β†’ μ „ μΈμŠ€ν„΄μŠ€ β†’ SSE 전달

---
5. Kafka λ°œν–‰ μ‹€νŒ¨ μ‹œ μ•Œλ¦Ό μœ μ‹€

증상: Kafka 브둜컀 μΌμ‹œ μž₯μ•  μ‹œ μ•Œλ¦Ό 이벀트 μ†Œλ©Έ

원인: μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 이벀트 λ°œν–‰ ν›„ Kafka 전솑 μ‹€νŒ¨ μ‹œ μž¬μ‹œλ„ μˆ˜λ‹¨ μ—†μŒ

ν•΄κ²°: Outbox νŒ¨ν„΄ λ„μž…
μ•Œλ¦Ό 이벀트 λ°œμƒ
  β†’ Outbox DB μ €μž₯ (같은 νŠΈλžœμž­μ…˜)
  β†’ Kafka λ°œν–‰ μ‹œλ„
  β†’ μ‹€νŒ¨ μ‹œ OutboxRetrySchedulerκ°€ PENDING μƒνƒœ μž¬μ‹œλ„
  β†’ 영ꡬ μ‹€νŒ¨ μ‹œ FAILED 처리 ν›„ DLT 격리

---
6. CI λΉŒλ“œ μ‹œ jOOQ μ½”λ“œ 생성 μ‹€νŒ¨

증상: GitHub Actionsμ—μ„œ generateJooq νƒœμŠ€ν¬ μ‹€ν–‰ μ‹œ DB μ—°κ²° μ—†μ–΄ λΉŒλ“œ μ‹€νŒ¨

원인: jOOQ μ½”λ“œ 생성이 μ‹€μ œ DB μŠ€ν‚€λ§ˆλ₯Ό 읽어야 ν•˜λŠ”λ° CI ν™˜κ²½μ— DB μ—†μŒ

ν•΄κ²°: CI Gradle λͺ…령에 -x generateJooq μΆ”κ°€λ‘œ νƒœμŠ€ν¬ μŠ€ν‚΅.
μƒμ„±λœ jOOQ ν΄λž˜μŠ€λŠ” μ†ŒμŠ€λ‘œ 컀밋해 CIμ—μ„œ 별도 생성 없이 λΉŒλ“œ

---
7. IdempotencyFilter 응닡 이쀑 μ“°κΈ°

증상: Idempotency-Key 헀더가 μžˆλŠ” μš”μ²­μ—μ„œ
Trailing token found after value Jackson 역직렬화 였λ₯˜ λ°œμƒ

원인: IdempotencyResponseWrapper.flushBuffer()κ°€ μ‹€μ œ response에 bodyλ₯Ό μ“°κ³ ,
IdempotencyFilterμ—μ„œλ„ wrapper.flushBuffer()λ₯Ό λͺ…μ‹œμ μœΌλ‘œ ν˜ΈμΆœν•΄ bodyκ°€ 두 번 write됨

ν•΄κ²°:
- flushBuffer()μ—μ„œ μ‹€μ œ response μ“°κΈ° 제거 (λ‚΄λΆ€ 버퍼 flush만 μˆ˜ν–‰)
- IdempotencyFilterμ—μ„œ μΊ‘μ²˜ν•œ bodyλ₯Ό λ”± ν•œ 번만 response에 쓰도둝 μˆ˜μ •
- status code도 ν•¨κ»˜ μΊμ‹œ("201|{body}" ν˜•μ‹)ν•΄ μž¬μ‘λ‹΅ μ‹œ μ›λž˜ μƒνƒœ μ½”λ“œ 볡원

// μˆ˜μ • μ „: flushBuffer()μ—μ„œ response에 write β†’ filterμ—μ„œλ„ write β†’ 이쀑 μ“°κΈ°
// μˆ˜μ • ν›„: filterμ—μ„œλ§Œ write
val capturedBody = wrapper.capturedBody
val capturedStatus = wrapper.statusCode
idempotencyStoragePort.set(redisKey, "$capturedStatus|$capturedBody", TTL)
response.status = capturedStatus
response.writer.write(capturedBody)

---
8. Redis μΊμ‹œ 역직렬화 였λ₯˜ (Jackson 3.x)

증상: κ²Œμ‹œκΈ€ 단건 쑰회 μ‹œ Redis μΊμ‹œ 역직렬화 μ‹€νŒ¨,
Cannot construct instance of StudyPostResponse (no Creators) 였λ₯˜

원인: Spring Boot 4.xλŠ” Jackson 3.x(tools.jackson)λ₯Ό μ‚¬μš©ν•˜λŠ”λ°,
Kotlin data class에 @JsonCreatorκ°€ μ—†μœΌλ©΄ 역직렬화 μ‹œ μƒμ„±μžλ₯Ό μ°Ύμ§€ λͺ»ν•¨

ν•΄κ²°: StudyPostResponse에 @JsonCreator + 각 νŒŒλΌλ―Έν„°μ— @JsonProperty μΆ”κ°€
data class StudyPostResponse @JsonCreator constructor(
    @JsonProperty("id") val id: UUID,
    @JsonProperty("title") val title: String,
    ...
)

동일 문제 λ°œμƒ μœ„μΉ˜: KakaoIdTokenPayload, KakaoTokenResponse도 같은 λ°©μ‹μœΌλ‘œ μˆ˜μ •

---
9. 쀑볡 지원 λΉ„μ¦ˆλ‹ˆμŠ€ λ£° λˆ„λ½

증상: 동일 μ‚¬μš©μžκ°€ λ‹€λ₯Έ Idempotency-Key둜 같은 κ²Œμ‹œκΈ€μ— μž¬μ§€μ› μ‹œ 쀑볡 μ €μž₯

원인: Idempotency-KeyλŠ” λ„€νŠΈμ›Œν¬ μž¬μ‹œλ„λ§Œ λ°©μ§€ν•˜λ©°,
μ‚¬μš©μžκ°€ μ˜λ„μ μœΌλ‘œ λ‹€μ‹œ μ§€μ›ν•˜λŠ” κ²½μš°λŠ” λΉ„μ¦ˆλ‹ˆμŠ€ 룰둜 막아야 함

ν•΄κ²°: 도메인 엔티티에 쀑볡 검증 μΆ”κ°€
// StudyPost.kt
fun validateNotDuplicateApply(alreadyApplied: Boolean) {
    if (alreadyApplied) throw CustomException(ErrorCode.ALREADY_APPLIED)
}

// ApplyStudyPostService.kt
post.validateNotDuplicateApply(
    applyRepository.existsByPostIdAndApplicantId(postId, userId)
)

---
10. 슀파이크 ν…ŒμŠ€νŠΈ dial: i/o timeout

증상: 15,000 VU 슀파이크 ν…ŒμŠ€νŠΈμ—μ„œ dial: i/o timeout λ°œμƒ, μ—λŸ¬μœ¨ 100%

원인: Tomcat accept 큐(κΈ°λ³Έ 200개) 포화 β†’ TCP μ—°κ²° μ‹œλ„μ‘°μ°¨ νƒ€μž„μ•„μ›ƒ
μ„œλ²„κ°€ 응닡을 λͺ» μ£ΌλŠ” 게 μ•„λ‹ˆλΌ μ—°κ²° μžμ²΄κ°€ OS λ ˆλ²¨μ—μ„œ λ“œλ‘­λ¨

뢄석: accept-countλ₯Ό 늘리면 λΉ λ₯Έ μ‹€νŒ¨(Fail Fast) 원칙에 어긋남.
κ·Όλ³Έ 원인은 단일 μ„œλ²„ μΈμŠ€ν„΄μŠ€ ν•œκ³„ β€” k6와 앱이 같은 λ¨Έμ‹ μ—μ„œ μžμ› 경쟁

κ²°λ‘ : μ„€μ •κ°’μœΌλ‘œ ν•΄κ²°ν•  λ¬Έμ œκ°€ μ•„λ‹Œ 단일 μ„œλ²„ ν•œκ³„.
싀무라면 Auto Scaling으둜 μˆ˜ν‰ ν™•μž₯, prod ν™˜κ²½ 2개 μΈμŠ€ν„΄μŠ€ + Nginx λ‘œλ“œλ°ΈλŸ°μ‹± ꡬ성이 이미 λ˜μ–΄ 있음

---
11. 슀파이크 ν…ŒμŠ€νŠΈ ν•œκΈ€ URL 인코딩 였λ₯˜

증상: spike-search.jsμ—μ„œ Invalid character found in the request target 였λ₯˜,
검색 μš”μ²­ 62% μ‹€νŒ¨

원인: k6μ—μ„œ ν•œκΈ€ ν‚€μ›Œλ“œλ₯Ό URL 인코딩 없이 κ·ΈλŒ€λ‘œ 전솑
β†’ Tomcat이 RFC 7230/3986 μœ„λ°˜μœΌλ‘œ μš”μ²­ κ±°λΆ€ (400)

ν•΄κ²°: encodeURIComponent() 적용
// μˆ˜μ • μ „
const res = http.get(`${BASE_URL}/api/posts/search?keyword=${keyword}`);

// μˆ˜μ • ν›„
const keyword = encodeURIComponent(KEYWORDS[__VU % KEYWORDS.length]);
const res = http.get(`${BASE_URL}/api/posts/search?keyword=${keyword}`);

---
12. 슀파이크 ν…ŒμŠ€νŠΈ Idempotency μΊμ‹œλ‘œ μΈν•œ ν…ŒμŠ€νŠΈ μ‹€νŒ¨

증상: μƒˆ ν† ν°μœΌλ‘œ spike-idempotency.js μž¬μ‹€ν–‰ μ‹œμ—λ„ 100% μ‹€νŒ¨

원인: 이전 μ‹€νŒ¨ ν…ŒμŠ€νŠΈ(만료 ν† ν°μœΌλ‘œ μ‹€ν–‰)μ—μ„œ 401 응닡이 Redis에 μΊμ‹œλ¨
(idem-test-{VU} ν‚€λ‘œ "401|..." μ €μž₯, TTL 24μ‹œκ°„)
μƒˆ ν† ν°μœΌλ‘œ μž¬μ‹€ν–‰ν•΄λ„ 동일 ν‚€λ‘œ μΊμ‹œλœ 401 응닡 λ°˜ν™˜

ν•΄κ²°: ν…ŒμŠ€νŠΈ μ‹€ν–‰λ§ˆλ‹€ κ³ μœ ν•œ ν‚€ prefix μ‚¬μš©
const RUN_ID = Date.now();

export default function () {
    const idempotencyKey = `idem-test-${RUN_ID}-${__VU}`;
    // ...
}
μ‹€ν–‰ λ‹¨μœ„λ‘œ ν‚€κ°€ 달라져 이전 μΊμ‹œμ™€ μΆ©λŒν•˜μ§€ μ•ŠμŒ

---
## 13. 비관적 락 쿼리의 λΆˆν•„μš”ν•œ JOIN FETCH둜 μΈν•œ 락 λ²”μœ„ ν™•λŒ€

**증상**: 락 쿼리 점검 쀑, 지원 λ™μ‹œμ„± μ œμ–΄μ— μ“°μ΄λŠ” `PESSIMISTIC_WRITE` 쿼리가 락 λŒ€μƒ
μ—”ν‹°ν‹° 외에 μ—°κ΄€ μ—”ν‹°ν‹°κΉŒμ§€ `JOIN FETCH`ν•˜κ³  μžˆλŠ” 것을 발견. `SELECT ... FOR UPDATE`κ°€
쑰인된 ν…Œμ΄λΈ” ν–‰κΉŒμ§€ ν•¨κ»˜ μž κ°€, κ²Œμ‹œκΈ€ ν•˜λ‚˜μ— 지원이 λͺ°λ¦΄ λ•Œ κ·Έ κ²Œμ‹œκΈ€ μž‘μ„±μžμ˜ User 행이
λ§€ 지원 μš”μ²­λ§ˆλ‹€ 반볡적으둜 μž κΈ°λŠ” κ΅¬μ‘°μ˜€μŒ β€” μž‘μ„±μžμ™€ λ¬΄κ΄€ν•œ 지원 νŠΈλžœμž­μ…˜λ“€μ΄ μž‘μ„±μžμ˜
User 행을 두고 λΆˆν•„μš”ν•˜κ²Œ κ²½ν•©.

**원인**: 지원 락 쿼리(`findByIdWithPostAndApplicantForUpdate`)와 κ²Œμ‹œκΈ€ 락 쿼리
(`findByIdWithAuthorForUpdate`)κ°€ μ‹€μ œλ‘œλŠ” ν•„λ“œλ₯Ό μ“°μ§€ μ•ŠλŠ” 연관관계(`a.applicant`,
`p.author`)κΉŒμ§€ `JOIN FETCH`ν•˜κ³  μžˆμ—ˆμŒ. 락 λŒ€μƒ ν–‰ ν•˜λ‚˜λ§Œ μž κ·Έλ €λŠ” μ˜λ„μ™€ 달리, JPAκ°€
μƒμ„±ν•œ `FOR UPDATE` μΏΌλ¦¬λŠ” 쑰인에 ν¬ν•¨λœ λͺ¨λ“  ν…Œμ΄λΈ” 행을 잠금 λ²”μœ„μ— ν¬ν•¨μ‹œν‚΄.

**ν•΄κ²°**:
\```kotlin
// μˆ˜μ • μ „: a.applicantκΉŒμ§€ JOIN FETCH β†’ applicant(User) 행도 락 λ²”μœ„μ— 포함
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM ApplyJpaEntity a JOIN FETCH a.post JOIN FETCH a.applicant WHERE a.id = :applyId")

// μˆ˜μ • ν›„: μ‹€μ œ 값이 ν•„μš”ν•œ a.post만 μœ μ§€, λ―Έμ‚¬μš© a.applicant 제거
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM ApplyJpaEntity a JOIN FETCH a.post WHERE a.id = :applyId")
fun findByIdWithPostAndApplicantForUpdate(...)
\```

- 락 μΏΌλ¦¬λŠ” μ‹€μ œ ν•„λ“œ μ‚¬μš© μ—¬λΆ€ κΈ°μ€€μœΌλ‘œ JOIN 제거 β†’ 락 λ²”μœ„λ₯Ό λŒ€μƒ ν–‰ 자체둜 ν•œμ •
- 같은 νŒ¨ν„΄μ΄ 락과 λ¬΄κ΄€ν•œ 쑰회 7κ³³(`DeleteStudyPostService`, `CloseStudyPostService`,
  `UpdateStudyPostService`, `FindAppliesService`, `UpdateCommentService`,
  `DeleteCommentService`, `PostScheduler`)
  `author.id`/`applicant.id`만 λΉ„κ΅ν•˜λŠ”λ° JOIN FETCH λ²„μ „μœΌλ‘œ μ‘°νšŒν•˜κ³  μžˆμ–΄ κΈ°λ³Έ
  `findById()`둜 ꡐ체해도 λ™μž‘ 동일
- λ°˜λŒ€λ‘œ 응닡 직렬화(`FindStudyPostService`), ES 색인(`PostSyncKafkaConsumer`),
  μ‹€μ œ μ—°κ΄€ ν•„λ“œ μ‚¬μš©(`TeamScheduleJpaRepository`)은 JOIN FETCHκ°€ μ •λ‹Ήν•΄ μœ μ§€
- 재발 λ°©μ§€λ₯Ό μœ„ν•΄ "μ†Œμœ μž 검증 μ „μš©" κ²½λŸ‰ 쑰회 λ©”μ„œλ“œλ₯Ό λ³„λ„λ‘œ 두고 μž¬μ‚¬μš©ν•˜λ„λ‘ 정리

Clone this wiki locally