-
Notifications
You must be signed in to change notification settings - Fork 0
Troubleshooting
TAEHEON KIM edited this page Jul 5, 2026
·
8 revisions
μ¦μ: 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κ° μ λΉν΄ μ μ§
- μ¬λ° λ°©μ§λ₯Ό μν΄ "μμ μ κ²μ¦ μ μ©" κ²½λ μ‘°ν λ©μλλ₯Ό λ³λλ‘ λκ³ μ¬μ¬μ©νλλ‘ μ 리