Skip to content

Troubleshooting

TAEHEON KIM edited this page Jul 7, 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 응닡
  • 락 νšλ“ μˆœμ„œλ₯Ό 항상 κ²Œμ‹œκΈ€ β†’ 지원 단방ν–₯ κ³ μ •μœΌλ‘œ λ°λ“œλ½ 제거 (단, μ·¨μ†Œ κ²½λ‘œμ—μ„œ λ³„λ„μ˜ λ°λ“œλ½μ΄ 재발 -> 1-1 μ°Έκ³ )

1-1. Apply μ·¨μ†Œ κ²½λ‘œμ—μ„œ μž¬λ°œν•œ λ°λ“œλ½

증상: μœ„ 1번 μˆ˜μ • 이후에도, κ²Œμ‹œκΈ€ μž‘μ„±μžκ°€ 지원을 μŠΉμΈν•˜λŠ” μ‹œμ κ³Ό μ§€μ›μžκ°€ 같은 지원을 μ·¨μ†Œν•˜λŠ” μ‹œμ μ΄ 겹치면 κ°„ν—μ μœΌλ‘œ λ°λ“œλ½ λ°œμƒ

원인: ApproveApplyServiceλŠ” κ²Œμ‹œκΈ€ -> 지원 μˆœμ„œλ‘œ λͺ…μ‹œμ  PESSIMISTIC_WRITE 락을 μž‘λŠ” 반면, CancelApplyServiceλŠ” 지원 행을 락 없이 μ‘°νšŒν•œ λ’€ λ°”λ‘œ DELETEλ₯Ό 싀행함. 지원(Apply)은 κ²Œμ‹œκΈ€(Post)을 μ°Έμ‘°ν•˜λŠ” FKκ°€ μžˆμ–΄ DELETE μ‹œμ μ— InnoDBκ°€ μ°Έμ‘° 무결성 검사λ₯Ό μœ„ν•΄ κ²Œμ‹œκΈ€ 행에 λŒ€ν•œ 락을 μ•”λ¬΅μ μœΌλ‘œ μš”κ΅¬ν•¨ -> 결과적으둜 "지원 -> κ²Œμ‹œκΈ€" μ—­μˆœ λŒ€κΈ°κ°€ λ°œμƒν•΄ "κ²Œμ‹œκΈ€ -> 지원" μˆœμ„œλ‘œ 락을 μž‘λŠ” 승인 νŠΈλžœμž­μ…˜κ³Ό μˆœν™˜ λŒ€κΈ°(circular wait)κ°€ λ§Œλ“€μ–΄μ§

ν•΄κ²°:

// CancelApplyService
val applyId = applyRepository.findByPostIdAndApplicantId(postId, userId)?.id
    ?: throw CustomException(ErrorCode.APPLICATION_NOT_FOUND)

// Post 락이 ν•„μš” μ—†μœΌλ―€λ‘œ Apply ν–‰λ§Œ μž κ°€ Approve(Post -> Apply)μ™€μ˜
// μ—­μˆœ 락으둜 μΈν•œ λ°λ“œλ½μ„ ν”Όν•œλ‹€.
val apply = applyRepository.findByIdForUpdate(applyId)
    ?: throw CustomException(ErrorCode.APPLICATION_NOT_FOUND)
apply.validatePending()
applyRepository.delete(apply)
  • μ·¨μ†ŒλŠ” κ²Œμ‹œκΈ€ μƒνƒœλ₯Ό λ³€κ²½ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ μ• μ΄ˆμ— κ²Œμ‹œκΈ€ 락이 λΆˆν•„μš”
  • 지원 ν–‰λ§Œ PESSIMISTIC_WRITE둜 λͺ…μ‹œμ μœΌλ‘œ μ„ μ ν•œ λ’€ μ‚­μ œν•΄, 락 νšλ“ μ‹œμ μ„ νŠΈλžœμž­μ…˜ μ•žμͺ½μœΌλ‘œ λ‹ΉκΈ°κ³  κ²Œμ‹œκΈ€ 행은 μ•„μ˜ˆ κ±΄λ“œλ¦¬μ§€ μ•Šλ„λ‘ λ³€κ²½
  • μ·¨μ†Œ νŠΈλžœμž­μ…˜μ΄ κ²Œμ‹œκΈ€ 락을 μš”μ²­ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ 승인 νŠΈλžœμž­μ…˜κ³Όμ˜ μ—­μˆœ κ²½ν•© μžμ²΄κ°€ 사라짐

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 μΏΌλ¦¬λŠ” 쑰인에 ν¬ν•¨λœ λͺ¨λ“  ν…Œμ΄λΈ” 행을 잠금 λ²”μœ„μ— ν¬ν•¨μ‹œν‚΄.

ν•΄κ²°:

// μˆ˜μ • μ „: 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κ°€ μ •λ‹Ήν•΄ μœ μ§€
  • 재발 λ°©μ§€λ₯Ό μœ„ν•΄ "μ†Œμœ μž 검증 μ „μš©" κ²½λŸ‰ 쑰회 λ©”μ„œλ“œλ₯Ό λ³„λ„λ‘œ 두고 μž¬μ‚¬μš©ν•˜λ„λ‘ 정리

14. λ™μ‹œ 졜초 둜그인 μ‹œ λ‹‰λ„€μž„ 좩돌둜 500

증상: 카카였 졜초 둜그인 μ‹œ 동일 λ‹‰λ„€μž„μ„ κ°€μ§„ μ‚¬μš©μžκ°€ λ™μ‹œμ— κ°€μž…ν•˜λ©΄ DataIntegrityViolationException이 κ·ΈλŒ€λ‘œ μ „νŒŒλ˜μ–΄ 500 μ—λŸ¬

원인: λ‹‰λ„€μž„ 쀑볡 체크와 INSERT 사이에 λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ΄ 끼어듀면 같은 λ‹‰λ„€μž„μœΌλ‘œ λ™μ‹œ μ €μž₯이 μ‹œλ„λ¨. μœ λ‹ˆν¬ μ œμ•½ μœ„λ°˜μ— λŒ€ν•œ μž¬μ‹œλ„ 둜직이 μ—†μ–΄ μ˜ˆμ™Έκ°€ μ»¨νŠΈλ‘€λŸ¬κΉŒμ§€ κ·ΈλŒ€λ‘œ μ „νŒŒλ¨

ν•΄κ²°: μ €μž₯을 별도 νŠΈλžœμž­μ…˜μœΌλ‘œ λΆ„λ¦¬ν•˜κ³ , μœ λ‹ˆν¬ μ œμ•½ μœ„λ°˜ μ›μΈλ³„λ‘œ λΆ„κΈ° 처리

private fun createUser(kakaoId: String, nickname: String, email: String): User =
    try {
        userRepository.saveNew(User.create(kakaoId, resolveUniqueNickname(nickname), email))
    } catch (e: DataIntegrityViolationException) {
        // kakaoId 좩돌(λ™μ‹œ 졜초 둜그인)이면 방금 μƒμ„±λœ μœ μ €λ₯Ό λ°˜ν™˜ν•˜κ³ ,
        // λ‹‰λ„€μž„ 좩돌(λ‹€λ₯Έ μ‚¬μš©μžμ™€μ˜ 경쟁)이면 λ‹‰λ„€μž„μ„ λ‹€μ‹œ μ±„λ²ˆν•΄ ν•œ 번 더 μ‹œλ„ν•œλ‹€.
        userRepository.findByKakaoId(kakaoId) ?: retryCreateUser(kakaoId, nickname, email)
    }
  • saveNewλŠ” REQUIRES_NEW + saveAndFlush둜 μ¦‰μ‹œ μ œμ•½ μœ„λ°˜μ„ 확인해 호좜자(kakaoLogin)의 μ˜μ†μ„± μ»¨ν…μŠ€νŠΈλ₯Ό μ˜€μ—Όμ‹œν‚€μ§€ μ•ŠμŒ
  • kakaoId 좩돌(같은 μ‚¬μš©μžμ˜ λ™μ‹œ 졜초 둜그인)이면 이미 μƒμ„±λœ μœ μ €λ₯Ό μ‘°νšŒν•΄ λ°˜ν™˜
  • λ‹‰λ„€μž„ 좩돌(λ‹€λ₯Έ μ‚¬μš©μžμ™€μ˜ 경쟁)이면 λ‹‰λ„€μž„μ„ λ‹€μ‹œ μ±„λ²ˆν•΄ 1회 μž¬μ‹œλ„

15. 배치 청크 λ‘€λ°± μ‹œ μŠ€ν‚΅ 감사 둜그 μœ μ‹€

증상: 만료 κ²Œμ‹œκΈ€ 마감 λ°°μΉ˜μ—μ„œ ES 색인 μ‹€νŒ¨ λ“±μœΌλ‘œ νŠΉμ • ν•­λͺ©μ΄ μŠ€ν‚΅λ  λ•Œ, 같은 청크가 λ‹€λ₯Έ 이유둜 둀백되면 μŠ€ν‚΅ 기둝(FailedPostSync)κΉŒμ§€ ν•¨κ»˜ 사라짐

원인: SkipListener.onSkipInWriteμ—μ„œ μ €μž₯ν•œ 감사 λ‘œκ·Έκ°€ 배치의 청크 νŠΈλžœμž­μ…˜μ— μ†Œμ†λ˜μ–΄ μžˆμ–΄, 청크가 둀백되면 μŠ€ν‚΅ 기둝도 ν•¨κ»˜ 둀백됨

ν•΄κ²°: 감사 둜그 μ €μž₯만 λ³„λ„μ˜ REQUIRES_NEW νŠΈλžœμž­μ…˜μœΌλ‘œ 뢄리

@Transactional(propagation = Propagation.REQUIRES_NEW)
override fun saveIndependently(failedPostSync: FailedPostSync): FailedPostSync =
    failedPostSyncJpaRepository.save(failedPostSync)
  • 청크 νŠΈλžœμž­μ…˜μ΄ λ‘€λ°±λ˜μ–΄λ„ 감사 λ‘œκ·ΈλŠ” 독립 νŠΈλžœμž­μ…˜μœΌλ‘œ 이미 μ»€λ°‹λ˜μ–΄ λ‚¨μŒ
  • 같은 λ°°μΉ˜μ— @SchedulerLock(PostScheduler_closeExpiredPosts)을 μΆ”κ°€ν•΄ λ©€ν‹° μΈμŠ€ν„΄μŠ€ ν™˜κ²½μ—μ„œ 두 μΈμŠ€ν„΄μŠ€κ°€ λ™μ‹œμ— 같은 배치λ₯Ό μ‹€ν–‰ν•˜λŠ” 쀑볡 싀행도 ν•¨κ»˜ λ°©μ§€

Clone this wiki locally