-
Notifications
You must be signed in to change notification settings - Fork 0
Troubleshooting
μ¦μ: 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λ² μμ μ΄νμλ, κ²μκΈ μμ±μκ° μ§μμ μΉμΈνλ μμ κ³Ό μ§μμκ° κ°μ μ§μμ μ·¨μνλ μμ μ΄ κ²ΉμΉλ©΄ κ°νμ μΌλ‘ λ°λλ½ λ°μ
μμΈ: 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λ‘ λͺ μμ μΌλ‘ μ μ ν λ€ μμ ν΄, λ½ νλ μμ μ νΈλμμ μμͺ½μΌλ‘ λΉκΈ°κ³ κ²μκΈ νμ μμ 건λλ¦¬μ§ μλλ‘ λ³κ²½ - μ·¨μ νΈλμμ μ΄ κ²μκΈ λ½μ μμ²νμ§ μμΌλ―λ‘ μΉμΈ νΈλμμ κ³Όμ μμ κ²½ν© μμ²΄κ° μ¬λΌμ§
μ¦μ: λκΈ μμ± 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% ν₯μ)
μ¦μ: @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()
}
}
}μ¦μ: App1μ μ°κ²°λ ν΄λΌμ΄μΈνΈκ° λ³΄λΈ μ±ν μ΄ App2μ μ°κ²°λ ν΄λΌμ΄μΈνΈμκ² μ λ¬ μ λ¨
μμΈ: WebSocket STOMP μΈμ μ μΈμ€ν΄μ€λ³λ‘ λ 립μ β λ€λ₯Έ μΈμ€ν΄μ€μ μ°κ²°μ μ κ·Ό λΆκ°
ν΄κ²°: Redis Pub/SubμΌλ‘ μΈμ€ν΄μ€ κ° λΈλ‘λμΊμ€νΈ
- μ±ν : RedisChatPublisherAdapter.publish() β μ μΈμ€ν΄μ€ ꡬλ β κ°μ WebSocket μ λ¬
- μλ¦Ό: Kafka Consumer β Redis Pub/Sub β μ μΈμ€ν΄μ€ β SSE μ λ¬
μ¦μ: Kafka λΈλ‘컀 μΌμ μ₯μ μ μλ¦Ό μ΄λ²€νΈ μλ©Έ
μμΈ: μ ν리μΌμ΄μ μ΄λ²€νΈ λ°ν ν Kafka μ μ‘ μ€ν¨ μ μ¬μλ μλ¨ μμ
ν΄κ²°: Outbox ν¨ν΄ λμ μλ¦Ό μ΄λ²€νΈ λ°μ β Outbox DB μ μ₯ (κ°μ νΈλμμ ) β Kafka λ°ν μλ β μ€ν¨ μ OutboxRetrySchedulerκ° PENDING μν μ¬μλ β μꡬ μ€ν¨ μ FAILED μ²λ¦¬ ν DLT 격리
μ¦μ: GitHub Actionsμμ generateJooq νμ€ν¬ μ€ν μ DB μ°κ²° μμ΄ λΉλ μ€ν¨
μμΈ: jOOQ μ½λ μμ±μ΄ μ€μ DB μ€ν€λ§λ₯Ό μ½μ΄μΌ νλλ° CI νκ²½μ DB μμ
ν΄κ²°: CI Gradle λͺ λ Ήμ -x generateJooq μΆκ°λ‘ νμ€ν¬ μ€ν΅. μμ±λ jOOQ ν΄λμ€λ μμ€λ‘ 컀λ°ν΄ CIμμ λ³λ μμ± μμ΄ λΉλ
μ¦μ: 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)μ¦μ: κ²μκΈ λ¨κ±΄ μ‘°ν μ 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λ κ°μ λ°©μμΌλ‘ μμ
μ¦μ: λμΌ μ¬μ©μκ° λ€λ₯Έ 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)
)μ¦μ: 15,000 VU μ€νμ΄ν¬ ν μ€νΈμμ dial: i/o timeout λ°μ, μλ¬μ¨ 100%
μμΈ: Tomcat accept ν(κΈ°λ³Έ 200κ°) ν¬ν β TCP μ°κ²° μλμ‘°μ°¨ νμμμ μλ²κ° μλ΅μ λͺ» μ£Όλ κ² μλλΌ μ°κ²° μμ²΄κ° OS λ 벨μμ λλ‘λ¨
λΆμ: accept-countλ₯Ό λ리면 λΉ λ₯Έ μ€ν¨(Fail Fast) μμΉμ μ΄κΈλ¨. κ·Όλ³Έ μμΈμ λ¨μΌ μλ² μΈμ€ν΄μ€ νκ³ β k6μ μ±μ΄ κ°μ λ¨Έμ μμ μμ κ²½μ
κ²°λ‘ : μ€μ κ°μΌλ‘ ν΄κ²°ν λ¬Έμ κ° μλ λ¨μΌ μλ² νκ³. μ€λ¬΄λΌλ©΄ Auto ScalingμΌλ‘ μν νμ₯, prod νκ²½ 2κ° μΈμ€ν΄μ€ + Nginx λ‘λλ°Έλ°μ± ꡬμ±μ΄ μ΄λ―Έ λμ΄ μμ
μ¦μ: 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}`);μ¦μ: μ ν ν°μΌλ‘ 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};
// ...
}
μ€ν λ¨μλ‘ ν€κ° λ¬λΌμ Έ μ΄μ μΊμμ μΆ©λνμ§ μμ
μ¦μ: λ½ μΏΌλ¦¬ μ κ² μ€, μ§μ λμμ± μ μ΄μ μ°μ΄λ 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κ° μ λΉν΄ μ μ§ - μ¬λ° λ°©μ§λ₯Ό μν΄ "μμ μ κ²μ¦ μ μ©" κ²½λ μ‘°ν λ©μλλ₯Ό λ³λλ‘ λκ³ μ¬μ¬μ©νλλ‘ μ 리
μ¦μ: μΉ΄μΉ΄μ€ μ΅μ΄ λ‘κ·ΈμΈ μ λμΌ λλ€μμ κ°μ§ μ¬μ©μκ° λμμ κ°μ
νλ©΄
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ν μ¬μλ
μ¦μ: λ§λ£ κ²μκΈ λ§κ° λ°°μΉμμ ES μμΈ μ€ν¨ λ±μΌλ‘ νΉμ νλͺ©μ΄ μ€ν΅λ λ,
κ°μ μ²ν¬κ° λ€λ₯Έ μ΄μ λ‘ λ‘€λ°±λλ©΄ μ€ν΅ κΈ°λ‘(FailedPostSync)κΉμ§ ν¨κ» μ¬λΌμ§
μμΈ: SkipListener.onSkipInWriteμμ μ μ₯ν κ°μ¬ λ‘κ·Έκ° λ°°μΉμ μ²ν¬ νΈλμμ
μ
μμλμ΄ μμ΄, μ²ν¬κ° λ‘€λ°±λλ©΄ μ€ν΅ κΈ°λ‘λ ν¨κ» λ‘€λ°±λ¨
ν΄κ²°: κ°μ¬ λ‘κ·Έ μ μ₯λ§ λ³λμ REQUIRES_NEW νΈλμμ
μΌλ‘ λΆλ¦¬
@Transactional(propagation = Propagation.REQUIRES_NEW)
override fun saveIndependently(failedPostSync: FailedPostSync): FailedPostSync =
failedPostSyncJpaRepository.save(failedPostSync)- μ²ν¬ νΈλμμ μ΄ λ‘€λ°±λμ΄λ κ°μ¬ λ‘κ·Έλ λ 립 νΈλμμ μΌλ‘ μ΄λ―Έ 컀λ°λμ΄ λ¨μ
- κ°μ λ°°μΉμ
@SchedulerLock(PostScheduler_closeExpiredPosts)μ μΆκ°ν΄ λ©ν° μΈμ€ν΄μ€ νκ²½μμ λ μΈμ€ν΄μ€κ° λμμ κ°μ λ°°μΉλ₯Ό μ€ννλ μ€λ³΅ μ€νλ ν¨κ» λ°©μ§