-
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 ์๋ต
- ๋ฝ ํ๋ ์์๋ฅผ ํญ์ ๊ฒ์๊ธ -> ์ง์ ๋จ๋ฐฉํฅ ๊ณ ์ ์ผ๋ก ๋ฐ๋๋ฝ ์ ๊ฑฐ
์ฆ์: ๋๊ธ ์์ฑ 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๊ฐ ์ ๋นํด ์ ์ง - ์ฌ๋ฐ ๋ฐฉ์ง๋ฅผ ์ํด "์์ ์ ๊ฒ์ฆ ์ ์ฉ" ๊ฒฝ๋ ์กฐํ ๋ฉ์๋๋ฅผ ๋ณ๋๋ก ๋๊ณ ์ฌ์ฌ์ฉํ๋๋ก ์ ๋ฆฌ