53๋ถ„์งœ๋ฆฌ ํฌ๋ก  ํ•˜๋‚˜์—์„œ ์‹œ์ž‘ํ•œ ๋ฐฐํฌ ์ข…๋ฃŒ ์„ค๊ณ„

๐Ÿ“š
Backend
2026.09.08

๐Ÿ“š Overview

ํ•œ๋™์•ˆ ๋ฐฐํฌํ•  ๋•Œ ์‚ฌ๋žŒ์ด ๊ธฐ๋‹ค๋ฆฌ๋Š” ์‹œ๊ฐ„์ด ์žˆ์—ˆ๋‹ค. (ํ•œ.. ์˜ค์ „ 09:00 ~ 10:00 ์‚ฌ์ด์ฏค?)

์ด๋ฉ”์ผ์„ ๋ณด๋‚ด๋Š” ํฌ๋ก  ํ•˜๋‚˜๊ฐ€ ํ•œ ํšŒ์ฐจ์— 53๋ถ„ ๊ฑธ๋ ธ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ทธ๊ฒŒ ๋„๋Š” ์ค‘์— ๋ฐฐํฌํ•˜๋ฉด ๋ฐœ์†ก์ด ์ค‘๊ฐ„์—์„œ ๋Š๊ธธ ๊ฒƒ ๊ฐ™์•„, ๋๋‚˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ ๋ฐฐํฌํ•œ ์ ์ด ์žˆ๋‹ค.

์„œ๋น„์Šค๋Š” Kubernetes ์œ„์—์„œ ๋Œ๊ณ  ArgoCD ๊ฐ€ ๋กค๋ง ์—…๋ฐ์ดํŠธ๋กœ ๋ฐฐํฌํ•œ๋‹ค. ์ƒˆ ํŒŒ๋“œ๊ฐ€ ์ค€๋น„๋˜๋ฉด ๊ตฌ ํŒŒ๋“œ๋Š” ์ •ํ•ด์ง„ ์œ ์˜ˆ ์‹œ๊ฐ„ ์•ˆ์— ์‚ฌ๋ผ์ง„๋‹ค. 53๋ถ„์งœ๋ฆฌ ์ž‘์—…์ด ๊ทธ ์•ˆ์— ๋๋‚  ์ˆ˜๋Š” ์—†์œผ๋‹ˆ, ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ชฝ์„ ์‚ฌ๋žŒ์ด ๋งก๊ณ  ์žˆ์—ˆ๋˜ ์…ˆ์ด๋‹ค.

์ฒ˜์Œ์—” ์ด๊ฑธ ์„ฑ๋Šฅ ๋ฌธ์ œ๋กœ ๋ดค๋‹ค. ํฌ๋ก ์ด ๋А๋ฆฌ๋‹ˆ ๋น ๋ฅด๊ฒŒ ๋งŒ๋“ค๋ฉด ๋˜๋Š” ์ผ์ด๋ผ๊ณ .

๊ทธ๋Ÿฐ๋ฐ ์‹คํ–‰ ์‹œ๊ฐ„์„ ๋ถ„ํ•ดํ•ด๋ณด๋‹ˆ ๋Œ€๋ถ€๋ถ„์ด ์˜๋„์ ์œผ๋กœ ๋„ฃ์€ ๋Œ€๊ธฐ ์˜€๋‹ค. ๋ฉ”์ผ ์„œ๋ฒ„์˜ rate limit ์— ๊ฑธ๋ ค ๋ฐœ์†ก์ด ๊ฑฐ๋ถ€๋˜๋Š” ๊ฒƒ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ํ•œ ๊ฑด๊ณผ ๋‹ค์Œ ๊ฑด ์‚ฌ์ด์— ๊ฐ„๊ฒฉ์„ ๋‘๊ณ  ์žˆ์—ˆ๋‹ค. ์„ฑ๋Šฅ์„ ์œ„ํ•ด ์ค„์—ฌ๋„ ๋˜๋Š” ๊ฐ’์ด ์•„๋‹ˆ๋ผ, ๋ฐœ์†ก์ด ์„ฑ๋ฆฝํ•˜๊ธฐ ์œ„ํ•œ ์กฐ๊ฑด์ด์—ˆ๋‹ค.

์ด ํฌ๋ก ์€ v1 ์„œ๋น„์Šค์—์„œ v2 ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•œ ์ฝ”๋“œ์˜€๋‹ค. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์˜ ์•ˆ์ „ ์žฅ์น˜๋กœ v1 ์˜ ๋™์ž‘์„ ๋จผ์ € ํ…Œ์ŠคํŠธ๋กœ ๊ณ ์ •ํ•˜๊ณ  ๊ทธ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค๋Š” ๋ฐฉ์‹์„ ํƒํ–ˆ๋Š”๋ฐ, ๋•๋ถ„์— ๊ธฐ๋Šฅ์ด ์–ด๊ธ‹๋‚˜๋Š” ์ผ์€ ์—†์—ˆ์ง€๋งŒ v1 ์ด ๊ฐ–๊ณ  ์žˆ๋˜ ๊ตฌ์กฐ์  ๋ถˆํ•ฉ๋ฆฌํ•จ๊นŒ์ง€ ๊ณ„์•ฝ์œผ๋กœ ํ•จ๊ป˜ ๊ตณ์–ด์กŒ๋‹ค. ํ•œ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ˆœ์ฐจ๋กœ ๋ฐœ์†กํ•˜๋ฉฐ ๊ทธ ์‚ฌ์ด๋ฅผ ๋Œ€๊ธฐ๋กœ ๋ฉ”์šด๋‹ค๋Š” ํ˜•ํƒœ๊ฐ€ ์ •ํ™•ํžˆ ๊ทธ๋Ÿฐ ํ•ญ๋ชฉ์ด์—ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๋ชฉํ‘œ๋ฅผ ๋‹ค์‹œ ์žก์•˜๋‹ค. ํฌ๋ก ์„ ๋น ๋ฅด๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ํฌ๋ก  ํ•œ ํšŒ์ฐจ๋ฅผ ์งง๊ฒŒ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ ๊ณผ ๋ฐฐํฌ๊ฐ€ ์ง„ํ–‰ ์ค‘์ธ ์ผ์„ ์Šค์Šค๋กœ ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฒƒ ์ด๋‹ค. ์ด ๋‘˜์ด ๋งž๋ฌผ๋ ค์•ผ ์‚ฌ๋žŒ์ด ๋ฐฐํฌ ์•ž์—์„œ ๊ธฐ๋‹ค๋ฆด ํ•„์š”๊ฐ€ ์—†์–ด์ง„๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ํ›„์ž๋Š” ์•„๋ฌด ์žฅ์น˜๊ฐ€ ์—†์—ˆ๋‹ค. ์ข…๋ฃŒ ์‹ ํ˜ธ๋ฅผ ๋ฐ›์œผ๋ฉด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๊ณง๋ฐ”๋กœ ํ”„๋กœ์„ธ์Šค๋ฅผ ๋๋‚ด๊ณ  ์žˆ์–ด์„œ, ์ง„ํ–‰ ์ค‘์ธ ์š”์ฒญ์ด๋‚˜ ๋Œ๊ณ  ์žˆ๋Š” ํฌ๋ก ์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ ˆ์ฐจ ์ž์ฒด๊ฐ€ ์—†์—ˆ๋‹ค. ๋ฐฐํฌ ์•ˆ์ •์„ฑ์ด ์œ ์˜ˆ ์‹œ๊ฐ„๊ณผ ์šด์— ๋งก๊ฒจ์ ธ ์žˆ๋˜ ๊ฒƒ์ด๋‹ค.

ํ•  ์ผ์€ ๋‘ ๊ฐœ์˜€๋‹ค.

  • ๋ฐœ์†ก์„ ํฌ๋ก  ๋ฐ–์œผ๋กœ ๋‚ด๋ณด๋‚ด ํ•œ ํšŒ์ฐจ๋ฅผ ์งง๊ฒŒ ๋งŒ๋“ ๋‹ค.
  • graceful shutdown ์„ ๋„ฃ์–ด ๋‚จ์€ ์ผ์„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ํ•œ๋‹ค.

์ด๋ฒˆ ํฌ์ŠคํŒ…์€ ์ด ๋‘ ๊ฐ€์ง€๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ํ•ด๊ฒฐํ•œ ๊ธฐ๋ก์ด๋‹ค.



1. ๊ธฐ๋‹ค๋ฆด ์ˆ˜ ์žˆ๋Š” ํฌ๊ธฐ๋กœ ๋งŒ๋“ค๊ธฐ โ€” ๋ฐœ์†ก์„ ํฌ๋ก  ๋ฐ–์œผ๋กœ


๋จผ์ € 53๋ถ„์„ ๋ถ„ํ•ดํ–ˆ๋‹ค. ํ•œ ํšŒ์ฐจ 945์ดˆ ๊ฐ€์šด๋ฐ 540์ดˆ(57%)๊ฐ€ ์˜๋„์ ์ธ ๋Œ€๊ธฐ ์˜€๋‹ค. ๋ฐœ์†ก์— ์„ฑ๊ณตํ•  ๋•Œ๋งˆ๋‹ค 5์ดˆ๋ฅผ ์‰ฌ์—ˆ๊ณ , ๊ทธ ํšŒ์ฐจ์˜ ๋Œ€์ƒ์ด 108๊ฑด์ด์—ˆ๋‹ค.

์ด ๋Œ€๊ธฐ๋Š” ์ค„์ผ ์ˆ˜ ์—†๋‹ค. ๋ฉ”์ผ ์„œ๋ฒ„๊ฐ€ ๋ฐ›์•„์ฃผ๋Š” ์†๋„์— ๋งž์ถ˜ ๊ฐ’์ด๋ผ, ์ค„์ด๋ฉด ๋ฐœ์†ก์ด ๊ฑฐ๋ถ€๋˜๊ธฐ ์‹œ์ž‘ํ•œ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋Š” ๊ฑด ํ•˜๋‚˜๋ฟ์ด์—ˆ๋‹ค. ํ•œ ํ”„๋กœ์„ธ์Šค๊ฐ€ 108๊ฑด์˜ ๋Œ€๊ธฐ๋ฅผ ์ „๋ถ€ ํ˜ผ์ž ์งŠ์–ด์ง€๋Š” ๊ตฌ์กฐ ๋‹ค.

๊ทธ๋ž˜์„œ ํฌ๋ก ์€ ๋ณด๋‚ผ ๋ฉ”์‹œ์ง€๋ฅผ outbox ํ…Œ์ด๋ธ”์— ์ ์žฌํ•˜๊ธฐ๋งŒ ํ•˜๊ณ , ๋ฆด๋ ˆ์ด๊ฐ€ ๊ทธ ํ–‰์„ Kafka ๋กœ ๋ฐœํ–‰ํ•˜๊ณ , ์ปจ์Šˆ๋จธ๊ฐ€ ์‹ค์ œ ๋ฐœ์†ก์„ ๋งก๋„๋ก ๋‚˜๋ˆด๋‹ค. 5์ดˆ ๋Œ€๊ธฐ๋Š” ์ปจ์Šˆ๋จธ ์ชฝ์œผ๋กœ ์˜ฎ๊ฒจ๊ฐ„๋‹ค.

์ค‘๋ณต์€ UPDATE ํ•œ ์ค„๋กœ ๋ง‰๋Š”๋‹ค

outbox ๋ฆด๋ ˆ์ด๋Š” ๋ฐœํ–‰์„ ๋†“์น˜์ง€ ์•Š๋Š” ๋Œ€์‹  ์ค‘๋ณต์„ ๋– ์•ˆ๋Š”๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” ๋ฐœํ–‰ํ•˜๊ธฐ ์ „์— ์‹œ๋„ ํšŸ์ˆ˜๋ถ€ํ„ฐ ์˜ฌ๋ฆฌ๋Š” ๋ฐฉ์‹ ์œผ๋กœ ๊ทธ ์ค‘๋ณต์— ์ƒํ•œ์„ ๋’€๋‹ค.

const affected = await this.outboxRepository.nativeUpdate(
  {
    outboxUuid,
    status: OutboxStatus.Pending,
    // ๋‚ด๊ฐ€ ์ฝ์€ ๋’ค๋กœ ์•„๋ฌด๋„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์•˜๊ณ ($eq), ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์ƒํ•œ ์•„๋ž˜์ผ ๋•Œ($lt)๋งŒ ๊ฐฑ์‹ ๋œ๋‹ค.
    attempts: { $eq: attempts, $lt: Outbox.MaxAttempts },
  },
  {
    attempts: raw('attempts + 1'),
    nextAttemptAt,
  },
);

// ํ•œ ๊ฑด์ด ๊ฐฑ์‹ ๋์„ ๋•Œ์—๋งŒ ์ด ์‹คํ–‰์ด ๋ฐœํ–‰ํ•  ๊ถŒ๋ฆฌ๋ฅผ ๊ฐ€์ง„๋‹ค.
return affected === 1;

$eq ๋Š” ์—ฌ๋Ÿฌ ํŒŒ๋“œ๊ฐ€ ๊ฐ™์€ ํ–‰์„ ๋™์‹œ์— ์ง‘๋Š” ๊ฒฝ์Ÿ์„, $lt ๋Š” ๋์—†์ด ์‹คํŒจํ•˜๋Š” ํ–‰์„ ๋ง‰๋Š”๋‹ค.

๋‹ค๋งŒ ๋ชจ๋“  ์‹คํŒจ๋ฅผ ์ƒํ•œ๊นŒ์ง€ ์žฌ์‹œ๋„ํ•˜์ง€๋Š” ์•Š๋Š”๋‹ค. ์ˆ˜์‹  ๋ถˆ๊ฐ€ ์ฃผ์†Œ์ฒ˜๋Ÿผ ๋‹ค์‹œ ๋ณด๋‚ด๋„ ๊ฒฐ๊ณผ๊ฐ€ ๊ฐ™์€ ์˜ค๋ฅ˜๋Š” ์žฌ์‹œ๋„ ์—†์ด ๋ฐ”๋กœ ์ข…๊ฒฐํ•˜๋„๋ก ํ–ˆ๋‹ค.

ํšŸ์ˆ˜๋ฅผ ๋ฐœํ–‰ ๋’ค์— ์˜ฌ๋ฆฌ๋ฉด ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด๋‹ค. ๋ฐœํ–‰์€ ์„ฑ๊ณตํ–ˆ๋Š”๋ฐ ์ƒํƒœ ๊ธฐ๋ก์ด ์‹คํŒจํ•œ ํ–‰์€ ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ๊ทธ๋Œ€๋กœ์ธ ์ฑ„ pending ์œผ๋กœ ๋‚จ๊ณ , ๋‹ค์Œ ์ฃผ๊ธฐ๊ฐ€ ๊ทธ ํ–‰์„ ๊ณ„์† ๋‹ค์‹œ ์ง‘๋Š”๋‹ค. ๊ฐ™์€ ๋ฉ”์‹œ์ง€๊ฐ€ ๋์—†์ด ๋ฐœํ–‰๋˜๋Š” ๊ฒƒ์ด๋‹ค. ๋จผ์ € ์˜ฌ๋ ค ๋‘๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ์ด ๋ฐ˜๋ณต์— ๋์ด ์ƒ๊ธด๋‹ค.

๋Œ€๊ธฐ๋Š” ์ปจ์Šˆ๋จธ๋กœ ์˜ฎ๊ฒจ๊ฐ„๋‹ค

์ปจ์Šˆ๋จธ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›์•„ ๋ฉ”์ผ์„ ๋ณด๋‚ธ๋‹ค. ๋ฌธ์ œ์˜ ์ถœ๋ฐœ์ ์ด์—ˆ๋˜ 5์ดˆ ๋Œ€๊ธฐ๊ฐ€ ์—ฌ๊ธฐ๋กœ ์˜จ๋‹ค.

@EventPattern(OutboxTopic.EmailDispatch)
async handleEmailDispatch(@Payload() message: EmailDispatchMessage): Promise<void> {
  // ๋Œ€๊ธฐ๋Š” ์‚ฌ๋ผ์ง„ ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์—ฌ๊ธฐ๋กœ ์˜ฎ๊ฒจ์™”๋‹ค. ๋‹ค๋งŒ ์ด ๋Œ€๊ธฐ๋ฅผ ์งŠ์–ด์ง€๋Š” ๊ฒƒ์€
  // ๋ฐฐํฌ๊ฐ€ ์™„์ฃผ๋ฅผ ๊ธฐ๋‹ค๋ ค์•ผ ํ•˜๋Š” ํฌ๋ก ์ด ์•„๋‹ˆ๋ผ, ์ฃฝ์–ด๋„ ๋‹ค์‹œ ์ง‘ํžˆ๋Š” ์ปจ์Šˆ๋จธ๋‹ค.
  await this.emailRateLimiter.acquire();
  await this.emailSender.send(message);
}

๊ฐ™์€ 5์ดˆ๋ฅผ ์—ฌ์ „ํžˆ ๊ธฐ๋‹ค๋ฆฌ์ง€๋งŒ ์„ฑ์งˆ์ด ๋‹ฌ๋ผ์กŒ๋‹ค. ์ด์ „์—๋Š” ํฌ๋ก  ํ•œ ํšŒ์ฐจ๊ฐ€ 108๊ฑด์˜ ๋Œ€๊ธฐ๋ฅผ ํ˜ผ์ž ์งŠ์–ด์กŒ๊ณ , ๋ฐฐํฌ๋Š” ๊ทธ ํšŒ์ฐจ๊ฐ€ ๋๋‚˜๊ธธ ๊ธฐ๋‹ค๋ ค์•ผ ํ–ˆ๋‹ค. ์ง€๊ธˆ์€ ์—ฌ๋Ÿฌ ์ปจ์Šˆ๋จธ๊ฐ€ ๋‚˜๋ˆ  ๊ฐ€์ ธ๊ฐ€๊ณ , ํ•˜๋‚˜๊ฐ€ ๋„์ค‘์— ์ฃฝ์–ด๋„ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ๋ฉ”์‹œ์ง€๋Š” ๋‹ค๋ฅธ ์ปจ์Šˆ๋จธ๊ฐ€ ๋‹ค์‹œ ๋ฐ›๋Š”๋‹ค.

์ด ๋ณ€ํ™”๊ฐ€ ์ข…๋ฃŒ ์„ค๊ณ„์— ์ค€ ๊ฒƒ์€ ๋‘ ๊ฐ€์ง€๋‹ค.

  • ํฌ๋ก ์ด ์งง์•„์กŒ๋‹ค. ๋ฉ”์‹œ์ง€๋ฅผ ์ ์žฌํ•˜๋Š” ์‹œ๊ฐ„๋งŒ ์“ฐ๋‹ˆ ์‹คํ–‰ ์‹œ๊ฐ„์ด ์ดˆ ๋‹จ์œ„๋กœ ๋‚ด๋ ค์™”๊ณ , ๋น„๋กœ์†Œ โ€œํฌ๋ก ์„ ๋ช‡ ์ดˆ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆด ๊ฒƒ์ธ๊ฐ€โ€ ๋ฅผ ๋ฌผ์„ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.
  • ๋ฆด๋ ˆ์ด์™€ ์ปจ์Šˆ๋จธ๋Š” ๊ธฐ๋‹ค๋ ค์ค„ ํ•„์š”๊ฐ€ ์—†๋‹ค. ๋„์ค‘์— ์ฃฝ์–ด๋„ ๋‹ค๋ฅธ ์ชฝ์ด ์ด์–ด๋ฐ›์œผ๋‹ˆ, ์ „๋‹ฌ์ด ๋๊นŒ์ง€ ๋˜๋Š”์ง€๊ฐ€ ํ”„๋กœ์„ธ์Šค ์ƒ์กด๊ณผ ๋ฌด๊ด€ํ•˜๋‹ค.

์ด ์ˆœ์„œ๋ฅผ ๋’ค์ง‘์–ด ์ข…๋ฃŒ ๋Œ€๊ธฐ๋ฅผ ๋จผ์ € ๋„ฃ์—ˆ๋‹ค๋ฉด, ์ƒํ•œ์„ ๋ช‡ ์ดˆ๋กœ ์žก๋“  ๋งค ๋ฐฐํฌ๊ฐ€ ๊ฐ•์ œ ์ข…๋ฃŒ๋กœ ๋๋‚ฌ์„ ๊ฒƒ์ด๋‹ค. 53๋ถ„์„ ๊ธฐ๋‹ค๋ ค์ฃผ๋Š” ์ƒํ•œ์€ ์กด์žฌํ•  ์ˆ˜ ์—†๋‹ค.



2. ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ ˆ์ฐจ๋ฅผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์†Œ์œ ํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ


Nest ์—๋Š” ์ข…๋ฃŒ ํ›…์ด ์žˆ๊ณ  app.enableShutdownHooks() ํ•œ ์ค„๋กœ ์ผค ์ˆ˜ ์žˆ๋‹ค. ์ฒ˜์Œ์—” ์ด๊ฑธ๋กœ ๋๋‚  ์ผ์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.

๋„ฃ์–ด๋ดค์ง€๋งŒ ์•„๋ฌด๊ฒƒ๋„ ๋‹ฌ๋ผ์ง€์ง€ ์•Š์•˜๋‹ค. ์›์ธ์€ ์ด๋ฏธ SIGTERM ๋ฆฌ์Šค๋„ˆ๊ฐ€ ํ•˜๋‚˜ ๋“ฑ๋ก๋˜์–ด ์žˆ์—ˆ๋‹ค๋Š” ๊ฒƒ ์ด์—ˆ๋‹ค.

// ์‚ฌ๋‚ด ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ ํŒจํ‚ค์ง€๊ฐ€ ์‹ฌ์–ด ๋‘” ๋ฆฌ์Šค๋„ˆ โ€” ํŠธ๋ ˆ์ด์Šค๋ฅผ ๋น„์šด ๋’ค ํ”„๋กœ์„ธ์Šค๋ฅผ ๋๋‚ธ๋‹ค
process.on('SIGTERM', () => {
  sdk.shutdown().finally(() => process.exit(0));
});

์ข…๋ฃŒ ํ›…์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ฐ™์€ ์‹ ํ˜ธ๋ฅผ ๋‘ ๋ฆฌ์Šค๋„ˆ๊ฐ€ ๋™์‹œ์— ๋ฐ›๋Š”๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํŠธ๋ ˆ์ด์Šค๋ฅผ ๋น„์šฐ๋Š” ์ชฝ์ด ์ˆ˜ ๋ฐ€๋ฆฌ์ดˆ ์•ˆ์— process.exit(0) ์„ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ, Nest ๊ฐ€ ๋ง‰ ์‹œ์ž‘ํ•œ app.close() ๋Š” ๋๊นŒ์ง€ ๊ฐ€์ง€ ๋ชปํ•˜๊ณ  ์ž˜๋ฆฐ๋‹ค.

์ฆ‰ ๋ฌธ์ œ๋Š” ์ข…๋ฃŒ ์ ˆ์ฐจ๊ฐ€ ์—†๋‹ค ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ข…๋ฃŒ ์ˆœ์„œ๋ฅผ ๋ˆ„๊ฐ€ ์†Œ์œ ํ•˜๋Š”๊ฐ€ ์˜€๋‹ค.

๊ทธ๋ž˜์„œ ์ด๋ฏธ ๋“ฑ๋ก๋œ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๊ฑท์–ด๋‚ด ๋ณด๊ด€ํ•˜๊ณ , ์šฐ๋ฆฌ ์ ˆ์ฐจ๊ฐ€ ๋๋‚œ ๋งˆ์ง€๋ง‰์— ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์ฃผ๊ธฐ๋กœ ํ–ˆ๋‹ค.

const handlers: SignalHandler[] = [];

for (const signal of params.signals) {
  handlers.push(...(emitter.listeners(signal) as SignalHandler[]));
  emitter.removeAllListeners(signal);
}

return {
  count: handlers.length,
  invoke(signal: NodeJS.Signals): void {
    for (const handler of handlers) {
      // ํ•œ ๊ฑด์ด throw ํ•˜๋ฉด ๋’ค ๋ฆฌ์Šค๋„ˆ์™€ ์ •์ƒ ์ข…๋ฃŒ ๊ฒฝ๋กœ๊ฐ€ ํ†ต์งธ๋กœ ์‚ฌ๋ผ์ง€๋ฏ€๋กœ ๊ฑด๋ณ„๋กœ ๊ฒฉ๋ฆฌํ•œ๋‹ค.
      try {
        handler.call(emitter, signal);
      } catch (error) {
        params.logger.error('gracefulShutdown phase=flush failed', error);
      }
    }
  },
};

ํŒจํ‚ค์ง€๊ฐ€ SDK ์ธ์Šคํ„ด์Šค๋ฅผ ๋…ธ์ถœํ•˜์ง€ ์•Š์•„ sdk.shutdown() ์„ ์ง์ ‘ ๋ถ€๋ฅผ ์ˆ˜๋Š” ์—†์—ˆ๋‹ค. ๋ฆฌ์Šค๋„ˆ ์ž์ฒด๋ฅผ ํ•จ์ˆ˜๋กœ ๋“ค๊ณ  ์žˆ๋‹ค๊ฐ€ ๋˜๋ถ€๋ฅด๋Š” ๋ฐฉ์‹์ด ์œ ์ผํ•œ ์„ ํƒ์ด์—ˆ๋‹ค.

ํ˜ธ์ถœ ์ž๋ฆฌ๋Š” NestFactory.create ์ด์ „, ๋ชจ๋“ˆ ๋กœ๋“œ ์งํ›„๋‹ค. ๋ถ€ํŒ… ๋„์ค‘์— ์˜ค๋Š” ์‹ ํ˜ธ๊นŒ์ง€ ๋ฎ์–ด์•ผ ํ•˜๋ฏ€๋กœ ์•ฑ ์ฐธ์กฐ๋Š” ๊ฐ’์ด ์•„๋‹ˆ๋ผ ์ง€์—ฐ ํ•จ์ˆ˜๋กœ ๋„˜๊ฒผ๋‹ค.

์–ผ๋งˆ๋‚˜ ๊ธฐ๋‹ค๋ฆด ๊ฒƒ์ธ๊ฐ€ โ€” ๋„ค ๊ฐœ์˜ ๊ฐ’์ด ํ•œ ๋ฌถ์Œ์ธ ์ด์œ 

์ˆœ์„œ๋ฅผ ์†Œ์œ ํ•˜๊ฒŒ ๋˜์ž ๋Œ€๊ธฐ ์ƒํ•œ์„ ์ •ํ•  ์ฐจ๋ก€์˜€๋‹ค. ์ฒ˜์Œ์—” ํ•ญ๋ชฉ๋ณ„๋กœ ๋”ฐ๋กœ ์ •ํ•˜๋ฉด ๋˜๋Š” ๊ฐ’์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ๋Š”๋ฐ, ์‹ค์ œ๋กœ๋Š” ๋„ค ๊ฐœ๊ฐ€ ์„œ๋กœ๋ฅผ ์ œ์•ฝํ•˜๋Š” ํ•˜๋‚˜์˜ ์˜ˆ์‚ฐ ์ด์—ˆ๋‹ค.

export const ShutdownTimeout = {
  // ์ •์ƒ ์‘๋‹ต ์ค‘ ๊ฐ€์žฅ ๊ธด ์™ธ๋ถ€ ์„œ๋ฒ„ ํ˜ธ์ถœ ์ƒํ•œ 20์ดˆ + ์—ฌ์œ  5์ดˆ. ์ด ์•„๋ž˜๋กœ ๋‚ด๋ฆฌ๋ฉด ์„ฑ๊ณต ์‘๋‹ต์„ ๋Š๋Š”๋‹ค.
  HttpDrainMs: 25_000,
  ConnectionPollMs: 250,
  // ๊ฐ€์žฅ ๊ธด ํฌ๋ก ์˜ ์‹คํ–‰ ์‹œ๊ฐ„์€ ์ด ๊ฐ’์„ ๋„˜์ง€๋งŒ ํ•˜๋ฃจ 1ํšŒ๋ผ ๊ทธ ์‹œ๊ฐ„๋Œ€ ๋ฐฐํฌ๋ฅผ ํ”ผํ•œ๋‹ค. ๊ทธ ์™ธ ํฌ๋ก ์€ ์ด ๊ฐ’ ์•ˆ์— ๋๋‚˜์•ผ ํ•œ๋‹ค.
  CronDrainMs: 90_000,
  // ํฌ๋ก  ๋Œ€๊ธฐ 90์ดˆ(์š”์ฒญ ๋Œ€๊ธฐ 25์ดˆ์™€ ๋™์‹œ ์ง„ํ–‰) + app.close ์™€ ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ flush ์˜ˆ์‚ฐ 20์ดˆ.
  // k8s ์œ ์˜ˆ 130์ดˆ์—์„œ preStop 10์ดˆ๋ฅผ ๋บ€ 120์ดˆ ์•ˆ์— ๋“ค์–ด๊ฐ„๋‹ค.
  HardDeadlineMs: 110_000,
} as const;

๊ฐ’์˜ ๊ทผ๊ฑฐ๋Š” ์‹ค์ธก์—์„œ ๊ฐ€์ ธ์™”๋‹ค. HTTP 25์ดˆ๋Š” ์ •์ƒ ์‘๋‹ต ์ค‘ ๊ฐ€์žฅ ๋А๋ฆฐ ๊ฒฝ๋กœ๊ฐ€ ์™ธ๋ถ€ ํ˜ธ์ถœ 20์ดˆ ์ƒํ•œ์— ๊ฑธ๋ฆฌ๋Š” API ์˜€๊ธฐ ๋•Œ๋ฌธ์ด๊ณ , ํฌ๋ก  90์ดˆ๋Š” APM ์—์„œ ํฌ๋ก ๋ณ„ ์‹คํ–‰ ์‹œ๊ฐ„ ์ตœ๋Œ“๊ฐ’์„ ๋ฝ‘์•„ ์ •ํ–ˆ๋‹ค.

์ด ๊ณผ์ •์—์„œ ํ•˜๋‚˜๋ฅผ ๋ฐฐ์› ๋‹ค. ์กฐํšŒ ์ฐฝ์˜ ๊ธธ์ด๊ฐ€ ๊ฒฐ๋ก ์„ ๋ฐ”๊พผ๋‹ค. 24์‹œ๊ฐ„ ์ฐฝ์—์„œ๋Š” ๊ฐ€์žฅ ๊ธด ํฌ๋ก ์˜ ์‹คํ–‰ ์‹œ๊ฐ„์ด 80์ดˆ ์ •๋„๋ผ 90์ดˆ๊ฐ€ ๋„‰๋„‰ํ•ด ๋ณด์˜€๋Š”๋ฐ, 7์ผ ์ฐฝ์œผ๋กœ ๋Š˜๋ฆฌ์ž ๊ฐ™์€ ํฌ๋ก ์ด 121์ดˆ๊นŒ์ง€ ํŠ€๋Š” ๋‚ ์ด ์žˆ์—ˆ๋‹ค.

๊ทธ ํฌ๋ก ์€ ์ž์ •์— ํ•˜๋ฃจ ํ•œ ๋ฒˆ๋งŒ ๋„๋Š” ์ข…๋ฅ˜์˜€๋‹ค. ์ƒํ•œ์„ ์˜ฌ๋ ค ๋ชจ๋“  ๋ฐฐํฌ๋ฅผ ๋А๋ฆฌ๊ฒŒ ๋งŒ๋“œ๋Š” ๋Œ€์‹ , ๊ทธ ์‹œ๊ฐ„๋Œ€๋ฅผ ๋ฐฐํฌ ๊ธˆ์ง€ ๊ตฌ๊ฐ„์œผ๋กœ ์ •ํ•˜๊ณ  ์ €์žฅ์†Œ์˜ PR ๊ทœ์น™ ๋ฌธ์„œ์— ๋„ฃ์—ˆ๋‹ค. main ๋ธŒ๋žœ์น˜๋กœ ๋ฐฐํฌ PR ์„ ๋งŒ๋“ค ๋•Œ ํ™•์ธํ•˜๋Š” ํ•ญ๋ชฉ์ด ๋˜๋„๋ก ํ•ด์„œ, ์‚ฌ๋žŒ์ด ๊ธฐ์–ตํ•˜์ง€ ์•Š์•„๋„ ๋ฐฐํฌ ํ๋ฆ„์ด ๊ฑธ๋Ÿฌ์ฃผ๊ฒŒ ํ–ˆ๋‹ค.

๋งˆ์ง€๋ง‰ ์ค„์˜ 110์ดˆ๋Š” ์šฐ๋ฆฌ๊ฐ€ ๊ณ ๋ฅธ ๊ฐ’์ด ์•„๋‹ˆ๋ผ ์ฃผ์–ด์ง„ ์‹œ๊ฐ„์—์„œ ๊ฑฐ๊พธ๋กœ ๊ณ„์‚ฐํ•œ ๊ฐ’ ์ด๋‹ค.

Kubernetes ๋Š” ํŒŒ๋“œ์— ์ข…๋ฃŒ ์‹ ํ˜ธ๋ฅผ ๋ณด๋‚ธ ๋’ค 130์ดˆ๋ฅผ ๊ธฐ๋‹ค๋ ค์ฃผ๊ณ , ๊ทธ๋ž˜๋„ ๋๋‚˜์ง€ ์•Š์œผ๋ฉด ๊ฐ•์ œ๋กœ ์ฃฝ์ธ๋‹ค. ๊ทธ์ค‘ ์ฒ˜์Œ 10์ดˆ๋Š” ์ƒˆ ์š”์ฒญ์ด ๋” ๋“ค์–ด์˜ค์ง€ ์•Š๊ฒŒ ์ž ์‹œ ๋ฉˆ์ถฐ ์žˆ๋Š” ์‹œ๊ฐ„(preStop)์ด๋ผ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹ค์ œ๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ๊ฑด 120์ดˆ๋‹ค. ๊ทธ ์•ˆ์—์„œ ํ™•์‹คํžˆ ๋๋‚ด๊ธฐ ์œ„ํ•ด 110์ดˆ๊ฐ€ ๋˜๋ฉด ์Šค์Šค๋กœ ๋ฉˆ์ถ”๋„๋ก ํ–ˆ๋‹ค.

์ด 130์ดˆ๋Š” ๋ฐฐํฌํ•  ๋•Œ๋ฟ ์•„๋‹ˆ๋ผ ํŠธ๋ž˜ํ”ฝ์ด ์ค„์–ด ํŒŒ๋“œ ์ˆ˜๋ฅผ ์ค„์ผ ๋•Œ๋„ ๋˜‘๊ฐ™์ด ์“ฐ์ธ๋‹ค. ๊ทธ๋ž˜์„œ ๋„‰๋„‰ํ•˜๊ฒŒ ๋Š˜๋ฆฌ๋ฉด ๊ทธ๋งŒํผ ๋ชจ๋“  ์ถ•์†Œ๊ฐ€ ๋А๋ ค์ง„๋‹ค.

๋‚จ๋Š” ์—ฌ์œ ๊ฐ€ 10์ดˆ๋ฟ์ด๋‹ˆ, HTTP ์š”์ฒญ๊ณผ ํฌ๋ก ์„ ์ฐจ๋ก€๋กœ ๊ธฐ๋‹ค๋ฆด ์ˆ˜๋Š” ์—†์—ˆ๋‹ค. ๋‘˜์„ ๋™์‹œ์— ๊ธฐ๋‹ค๋ ค์•ผ ํ•œ๋‹ค.

// ์ง„ํ–‰ ์ค‘ ํฌ๋ก ์ด ์—†์„ ๋•Œ ๋ฐฐํฌ ์‹œ๊ฐ„์ด ๋Š˜์ง€ ์•Š๋„๋ก ์š”์ฒญ๊ณผ ํฌ๋ก ์„ ๋™์‹œ์— ๊ธฐ๋‹ค๋ฆฐ๋‹ค.
const [drain, cron] = await Promise.all([
  this.drainHttp().catch(/* ... */),
  ShutdownState.waitForCronsIdle(ShutdownTimeout.CronDrainMs).catch(/* ... */),
]);

์ด Promise.all ์€ ์„ฑ๋Šฅ ์ตœ์ ํ™”๊ฐ€ ์•„๋‹ˆ๋ผ ์˜ˆ์‚ฐ์ด ์„ฑ๋ฆฝํ•˜๊ธฐ ์œ„ํ•œ ํ•„์š”์กฐ๊ฑด ์ด๋‹ค. ์ง๋ ฌ๋กœ ๋˜๋Œ๋ฆฌ๋ฉด 25 + 90 = 115์ดˆ๊ฐ€ ๋˜์–ด ๋ฐ๋“œ๋ผ์ธ 110์ดˆ๋ฅผ ๋„˜๊ธฐ๊ณ , ์ •์ƒ์ ์œผ๋กœ ์ข…๋ฃŒ๋˜๋Š” ๊ฒฝ์šฐ์—๋„ ๊ฐ•์ œ ์ข…๋ฃŒ๋กœ ๋ฐ”๋€๋‹ค.

์ฝ”๋“œ๋งŒ ๋ณด๋ฉด โ€œ๋™์‹œ์— ๊ธฐ๋‹ค๋ฆฌ๋‹ˆ ์กฐ๊ธˆ ๋น ๋ฅด๊ฒ ๋„คโ€ ๋กœ ์ฝํžŒ๋‹ค. ๊ทธ๋ž˜์„œ ์ฃผ์„์— ์ตœ์ ํ™”๊ฐ€ ์•„๋‹ˆ๋ผ๋Š” ์‚ฌ์‹ค์„ ๋‚จ๊ฒผ๋‹ค. ๋ˆ„๊ตฐ๊ฐ€ ๊ฐ€๋…์„ฑ์„ ์ด์œ ๋กœ ์ง๋ ฌ๋กœ ํŽด๋Š” ์ˆœ๊ฐ„ ๋ชจ๋“  ๋ฐฐํฌ๊ฐ€ ๊ฐ•์ œ ์ข…๋ฃŒ๋˜๋Š”๋ฐ, ๊ทธ ์‹ ํ˜ธ๋Š” ๋กœ๊ทธ ํ•œ ์ค„๋ฟ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.



๐Ÿค” Understanding

์ด๋ฒˆ ์ž‘์—…์—์„œ ๊ฐ€์žฅ ํฌ๊ฒŒ ๋‚จ์€ ๊ฒƒ์€, ๋ฌด์ค‘๋‹จ ๋ฐฐํฌ๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ •์ด ์•„๋‹ˆ๋ผ ์•ฑ๊ณผ ์ธํ”„๋ผ๊ฐ€ ๊ณต์œ ํ•˜๋Š” ํ•˜๋‚˜์˜ ์˜ˆ์‚ฐ ์ด๋ผ๋Š” ์ ์ด๋‹ค.

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด 110์ดˆ๋ฅผ ์“ฐ๊ฒ ๋‹ค๊ณ  ์ •ํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋Š” ์˜๋ฏธ๊ฐ€ ์—†๋‹ค. ํŒŒ๋“œ ์ข…๋ฃŒ ์œ ์˜ˆ๊ฐ€ ๊ธฐ๋ณธ๊ฐ’ 30์ดˆ๋กœ ๋‚จ์•„ ์žˆ์œผ๋ฉด ์š”์ฒญ๊ณผ ํฌ๋ก ์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ ˆ์ฐจ๊ฐ€ 30์ดˆ์— ์ž˜๋ฆฐ๋‹ค. ๊ทธ๋ž˜์„œ ๋ฐฐํฌ ์ˆœ์„œ๊ฐ€ ๊ณง ์•ˆ์ „ ์กฐ๊ฑด์ด ๋˜์—ˆ๋‹ค. ์ฐจํŠธ์˜ ์œ ์˜ˆ ์‹œ๊ฐ„๊ณผ preStop ์ด ๋จผ์ € ๋ฐ˜์˜๋œ ๋’ค์—์•ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ณ€๊ฒฝ์ด ๋‚˜๊ฐˆ ์ˆ˜ ์žˆ๋‹ค.

๊ฐ™์€ ์„ฑ๊ฒฉ์˜ ์˜์กด์ด ๊ทธ๋ณด๋‹ค ์•ž์— ํ•˜๋‚˜ ๋” ์žˆ์—ˆ๋‹ค. 53๋ถ„์งœ๋ฆฌ ์ž‘์—…์„ outbox ์™€ ๋ธŒ๋กœ์ปค ๋’ค๋กœ ์˜ฎ๊ธฐ๋Š” ์ผ์ด ์ข…๋ฃŒ ์ ˆ์ฐจ๋ณด๋‹ค ๋จผ์ € ์™€์•ผ ํ–ˆ๋‹ค. ๋Œ์ด์ผœ๋ณด๋ฉด ์ข…๋ฃŒ๋ฅผ ์„ค๊ณ„ํ•˜๋Š” ์ผ์˜ ์ ˆ๋ฐ˜์€ ๊ธฐ๋‹ค๋ฆด ์ˆ˜ ์žˆ๋Š” ํฌ๊ธฐ์˜ ์ผ๋กœ ๋งŒ๋“ค์–ด ๋‘๋Š” ๊ฒƒ ์ด์—ˆ๋‹ค.

๋Œ€๊ธฐ๊ฐ€ ์‚ฌ๋ผ์ง„ ๊ฒƒ์ด ์•„๋‹ˆ๋ผ๋Š” ์ ๋„ ์ ์–ด ๋‘”๋‹ค. rate limit ์— ๋งž์ถ˜ 5์ดˆ๋Š” ์ปจ์Šˆ๋จธ์— ๊ทธ๋Œ€๋กœ ๋‚จ์•„ ์žˆ๋‹ค. ๋‹ฌ๋ผ์ง„ ๊ฒƒ์€ ๊ทธ ์‹œ๊ฐ„์„ ๋ˆ„๊ฐ€ ์งŠ์–ด์ง€๋Š”๊ฐ€ ์˜€๊ณ , ์ข…๋ฃŒ๊ฐ€ ๊ธฐ๋‹ค๋ ค์•ผ ํ•˜๋Š” ์ชฝ์—์„œ ๊ธฐ๋‹ค๋ ค์ฃผ์ง€ ์•Š์•„๋„ ๋˜๋Š” ์ชฝ์œผ๋กœ ์˜ฎ๊ธด ๊ฒƒ์ด ์ด๋ฒˆ ๋ณ€๊ฒฝ์˜ ์‹ค์ฒด์˜€๋‹ค.

๊ฒ€์ฆ๋„ ๋กœ์ปฌ์ด ์•„๋‹ˆ๋ผ ๋กค์•„์›ƒ ๋กœ๊ทธ๋กœ ํ–ˆ๋‹ค. ์‹ค์ œ ๋กค์•„์›ƒ์—์„œ ๊ตฌ ํŒŒ๋“œ ์ „๋ถ€๊ฐ€ ์‹ ํ˜ธ ์ˆ˜์‹  โ†’ ์š”์ฒญยทํฌ๋ก  ๋Œ€๊ธฐ โ†’ ์ž์› ์ •๋ฆฌ โ†’ ํŠธ๋ ˆ์ด์Šค ๋น„์šฐ๊ธฐ ๋ฅผ ์ˆ˜์‹ญ ๋ฐ€๋ฆฌ์ดˆ ์•ˆ์— ์™„์ฃผํ–ˆ๊ณ , ๋ฐ๋“œ๋ผ์ธ์œผ๋กœ ๋Š๊ธด ๊ฒฝ์šฐ๋Š” ์—†์—ˆ๋‹ค.

๋‹ค๋งŒ ์ง€๊ธˆ์˜ ์ˆ˜์น˜๊ฐ€ ๋์€ ์•„๋‹ˆ๋‹ค. 25์ดˆยท90์ดˆยท110์ดˆ๋Š” ๋ชจ๋‘ ํ˜„์žฌ์˜ ํŠธ๋ž˜ํ”ฝ๊ณผ ํฌ๋ก  ์‹คํ–‰ ์‹œ๊ฐ„์„ ๊ธฐ์ค€์œผ๋กœ ์ •ํ•œ ๊ฐ’์ด๋ผ, ์„œ๋น„์Šค๊ฐ€ ๋‹ฌ๋ผ์ง€๋ฉด ํ•จ๊ป˜ ๋‹ฌ๋ผ์ ธ์•ผ ํ•œ๋‹ค. ๊พธ์ค€ํžˆ ๊ด€์ธกํ•˜๋ฉด์„œ ์กฐ์ ˆํ•ด ๋‚˜๊ฐˆ ์ƒ๊ฐ์ด๋‹ค.

๋ฐฐํฌ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋˜ 53๋ถ„์—์„œ ์‹œ์ž‘ํ•ด ๊ฒฐ๊ตญ ์‹ ํ˜ธ์˜ ์†Œ์œ ๊ถŒ๊ณผ ์‹œ๊ฐ„ ์˜ˆ์‚ฐ์„ ๋‹ค์‹œ ์„ค๊ณ„ํ•˜๋Š” ์ผ์ด ๋˜์—ˆ๋‹ค. ์ข…๋ฃŒ๋ฅผ ์ •๋ฆฌ ์ž‘์—… ์ด ์•„๋‹ˆ๋ผ ์ œํ•œ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฐ ์ ˆ์ฐจ ๋กœ ๋ณด๊ฒŒ ๋œ ๊ฒƒ์ด ์ด๋ฒˆ์— ์–ป์€ ๊ฐ€์žฅ ํฐ ๊ด€์ ์˜ ๋ณ€ํ™”์˜€๋‹ค.


์ฐธ๊ณ  ์ž๋ฃŒ

Previous
๋ณตํ•ฉ ์ธ๋ฑ์Šค๋Š” ์–ธ์ œ ํ•„์š”ํ• ๊นŒ?
๐Ÿ“šBackend
2026.08.09
Next
์กฐ์šฉํžˆ ๋น„์–ด๋ฒ„๋ฆฐ ๊ฐ’ ํ•˜๋‚˜์—์„œ ์‹œ์ž‘ํ•œ ORM ๋ฉ”์ด์ € ์—…๊ทธ๋ ˆ์ด๋“œ ๊ฒ€์ฆ ์ „๋žต
๐Ÿ“šBackend
2026.09.18