CloudFront๋ฅผ ๊ฑท์–ด๋‚ด๊ณ  Cloudflare๋กœ. Worker + R2 CDN ์ „ํ™˜ ์‚ฝ์งˆ๊ธฐ

seungbo
  • #Cloudflare
  • #R2
  • #Worker
  • #CloudFront
  • #CDN
  • #AWS
  • #Cache

์•ˆ๋…•ํ•˜์„ธ์š”. ๋…ธ๋จธ์Šค์—์„œ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์„ ํ•˜๊ณ  ์žˆ๋Š” ์ง€์Šน๋ณด์ž…๋‹ˆ๋‹ค.

์–ผ๋งˆ ์ „ ์ฐฌํฌ๋‹˜์ด CloudFront + Lambda@Edge to Cloudflare Worker + R2 PoC ๊ธ€์—์„œ โ€œAWS ๋ฏธ๋””์–ด ์Šคํƒ์„ Cloudflare๋กœ ์˜ฎ๊ธธ ์ˆ˜ ์žˆ๋Š”๊ฐ€โ€๋ผ๋Š” ์งˆ๋ฌธ์— ๋‹ต์„ ๋‚ด๋ ค์ฃผ์…จ์Šต๋‹ˆ๋‹ค. ๊ฒฐ๋ก ์€ ์ด๋ฏธ์ง€ยท์ผ๋ฐ˜ VOD๋Š” ์˜ฎ๊ธธ ๋งŒํ•˜๊ณ , 4KยทAES-128์€ MediaConvert๋ฅผ ์œ ์ง€ํ•˜๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ์˜€์Šต๋‹ˆ๋‹ค.

์ด ๊ธ€์€ ๊ทธ PoC์˜ ํ›„์†ํŽธ์ž…๋‹ˆ๋‹ค. โ€œ์˜ฎ๊ธธ ์ˆ˜ ์žˆ๋‹คโ€์™€ โ€œ์ด๋ฏธ ํŠธ๋ž˜ํ”ฝ์ด ํ๋ฅด๊ณ  ์žˆ๋Š” ์šด์˜ ์„œ๋น„์Šค๋ฅผ ์•ˆ ๋Š๊ณ  ์˜ฎ๊ธด๋‹คโ€๋Š” ๋‹ค๋ฅธ ์ด์•ผ๊ธฐ์˜€๊ณ , ๊ทธ ์‚ฌ์ด์—์„œ ์ œ๊ฐ€ ๋ฐŸ์€ ์‚ฝ์งˆ๋“ค์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

TL;DR

  • ๊ฐ€๋Šฅํ•œ๊ฐ€? ์•ˆ ๋Š๊ธฐ๊ณ  ์–ด๋–ป๊ฒŒ?: ํŒŒ์ผ URL์€ RDSยทFirestore ์—ฌ๊ธฐ์ €๊ธฐ์— ์ €์žฅ๋ผ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹ ๊ทœ ํŒŒ์ผ๋งŒ R2์šฉ ๋ณ„๋„ ๋„๋ฉ”์ธ์œผ๋กœ ๋นผ๋‘๋ฉด, ๋‚˜์ค‘์— ์™„์ „ํžˆ R2๋กœ ๋„˜์–ด๊ฐˆ ๋•Œ ์ด๋ฏธ ์ €์žฅ๋œ URL์„ ์ „๋ถ€ ์ฐพ์•„ ๋ฐ”๊ฟ”์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ URL์€ ์†๋Œ€์ง€ ์•Š๊ณ , S3 โ†’ R2 ๋™๊ธฐํ™”๋ฅผ ๋จผ์ € ๋Œ๋ฆฐ ๋’ค DNS๋งŒ Cloudflare๋กœ ๋ฐ”๊ฟ” ๋’ค์—์„œ ์˜ค๋ฆฌ์ง„๋งŒ ๊ฐˆ์•„๋ผ์šฐ๋Š” ์ชฝ์œผ๋กœ ๊ฐ”์Šต๋‹ˆ๋‹ค.
  • Cloudflare ์บ์‹œ๋Š” CloudFront์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค: ๋น„์šฉ์„ ์žก์œผ๋ ค๋ฉด ์บ์‹œ๋กœ R2 ์ ‘๊ทผ์„ ์ค„์—ฌ์•ผ ํ•˜๋Š”๋ฐ, CloudFront์—์„œ ์•Œ๋˜ ์บ์‹œ ์ƒ์‹์ด Cloudflare์—” ๊ทธ๋Œ€๋กœ ํ†ตํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋ฌธ์„œ์™€ ์‹ค์ œ ๋™์ž‘์„ ์ง์ ‘ ์ฐ์–ด๋ณด๋ฉฐ ์บ์‹œ ๊ตฌ์กฐ๋ฅผ ๋‹ค์‹œ ์ตํ˜”์Šต๋‹ˆ๋‹ค.
  • egress 0์›์ด ์ „๋ถ€๊ฐ€ ์•„๋‹ˆ๋‹ค: R2๋Š” ๋ฐ์ดํ„ฐ ์ „์†ก(egress)์€ ๊ณต์งœ์ง€๋งŒ, ํŒŒ์ผ์„ ์ฝ๋Š” ์ ‘๊ทผ(Class B operation) ์ž์ฒด์—๋Š” ๊ณผ๊ธˆ๋ฉ๋‹ˆ๋‹ค. Worker ์บ์‹œ๋กœ R2 ๋„๋‹ฌ ํšŸ์ˆ˜๋ฅผ ์ค„์ด์ง€ ๋ชปํ•˜๋ฉด egress๋ฅผ 0์›์œผ๋กœ ๋งŒ๋“ค์–ด๋†“๊ณ ๋„ ์ „ํ™˜ ์ด๋“์ด ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.
  • private vs public ๋”œ๋ ˆ๋งˆ: ์‚ฌ์šฉ์ž๋ณ„ ๊ถŒํ•œ์ด ๋‹ค๋ฅธ ์ฝ˜ํ…์ธ ๋ผ ํด๋ผ์ด์–ธํŠธ์—๋Š” Cache-Control: private์œผ๋กœ ์‘๋‹ตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. CloudFront๋Š” ์˜ค๋ฆฌ์ง„์ด private์„ ์ค˜๋„ ์•Œ์•„์„œ ์บ์‹ฑํ•ด์คฌ๊ธฐ์— Cloudflare๋„ ๊ทธ๋Ÿฌ๋ ค๋‹ˆ ํ–ˆ๋Š”๋ฐ, Worker ์บ์‹œ(caches.default)๋Š” private ์‘๋‹ต์„ ์กฐ์šฉํžˆ ์ €์žฅ ๊ฑฐ๋ถ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฐ๊ตญ ์ €์žฅํ•  ๋• public, ์‘๋‹ตํ•  ๋• ๋‹ค์‹œ private์œผ๋กœ ๋˜๋Œ๋ฆฌ๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ํ’€์—ˆ์Šต๋‹ˆ๋‹ค.

๋ชฉ์ฐจ

  1. PoC์™€ ์‹ค์ „ ์‚ฌ์ด์˜ ๊ฐ„๊ทน
  2. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „๋žต
  3. ์‚ฝ์งˆ 1 โ€” Cloudflare ์บ์‹œ๋Š” CloudFront์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜์ง€ ์•Š์•˜๋‹ค
  4. ์‚ฝ์งˆ 2 โ€” egress 0์›์˜ ํ•จ์ •, R2๋„ ์ ‘๊ทผํ•˜๋ฉด ๋ˆ์ด๋‹ค
  5. ์‚ฝ์งˆ 3 โ€” private์œผ๋กœ ์‘๋‹ตํ•ด์•ผ ํ•˜๋Š”๋ฐ ์บ์‹œ๋Š” public๋งŒ ์ €์žฅํ•œ๋‹ค
  6. Worker ํ•˜๋‚˜์— ๋ˆŒ๋Ÿฌ ๋‹ด์€ ์ž์ž˜ํ•œ ์ฒ˜๋ฆฌ๋“ค
  7. CloudFront ํฌ๋งท ๊ทธ๋Œ€๋กœ ๊ฒ€์ฆํ•˜๊ธฐ
  8. ๋งˆ์น˜๋ฉฐ

PoC์™€ ์‹ค์ „ ์‚ฌ์ด์˜ ๊ฐ„๊ทน

์ฐฌํฌ๋‹˜ PoC์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒƒ๋“ค์„ ๋ฏธ๋ฆฌ ์•Œ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  • R2๋Š” S3 SDK๋ฅผ ๊ทธ๋Œ€๋กœ ์“ธ ์ˆ˜ ์žˆ๋‹ค.
  • Worker๋Š” Lambda@Edge๊ฐ€ ํ•˜๋˜ ์ผ(์ฟ ํ‚ค ๋ฐœ๊ธ‰, Cache-Control ๋ถ€์—ฌ)์„ ๋Œ€์ฒดํ•  ์ˆ˜ ์žˆ๋‹ค.
  • egress 0์› ๋•๋ถ„์— ๋น„์šฉ ์ž„ํŒฉํŠธ๊ฐ€ ํฌ๋‹ค.

โ€œ์˜ฎ๊ธธ ์ˆ˜ ์žˆ๋Š”๊ฐ€โ€์— ๋Œ€ํ•œ ๋‹ต์€ ์ด๊ฑธ๋กœ ๋‚˜์˜จ ์…ˆ์ž…๋‹ˆ๋‹ค. ์ œ ๋ชซ์€ ์ด ๊ฒฐ๋ก ์„ ์ด๋ฏธ ์‚ฌ์šฉ์ž ํŠธ๋ž˜ํ”ฝ์ด ํ๋ฅด๊ณ  ์žˆ๋Š” ์šด์˜ ๋ฏธ๋””์–ด ์Šคํƒ์— ์‹ค์ œ๋กœ ํƒœ์šฐ๋Š” ์ผ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฐœ๋…์„ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ๊ณผ, ๊ทธ ๊ฐœ๋…์„ ํŠธ๋ž˜ํ”ฝ ๋Š๊น€ ์—†์ด ์šด์˜์— ํƒœ์šฐ๋Š” ๊ฒƒ์€ ๊ฒฐ์ด ๋‹ค๋ฅธ ๋ฌธ์ œ๋ผ, ์‹ค์ œ๋กœ ์˜ฎ๊ธฐ๋Š” ๊ณผ์ •์—์„œ ์ด๋Ÿฐ ๊ฒƒ๋“ค์„ ํ•˜๋‚˜์”ฉ ๋งˆ์ฃผํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์–ด๋–ค ์ˆœ์„œ๋กœ ๋ฐฐํฌํ•ด์•ผ ๋‹ค์šดํƒ€์ž„ ์—†์ด ๋„˜์–ด๊ฐ€๋Š”๊ฐ€ โ€” ๋ฐฐํฌ ์ „๋žต
  • ์ด๋ฏธ ์Œ“์—ฌ ์žˆ๋Š” ๊ธฐ์กด ํŒŒ์ผ ์ „์ฒด๋ฅผ ์–ด๋–ป๊ฒŒ R2๋กœ ์˜ฎ๊ธฐ๋Š”๊ฐ€
  • RDSยทFirestore ์—ฌ๊ธฐ์ €๊ธฐ์— ์ €์žฅ๋œ URL์€ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š”๊ฐ€
  • โ€œ์บ์‹œ๊ฐ€ ์ง„์งœ๋กœ ๋จน๊ธด ํ•˜๋‚˜?โ€ ์ฒ˜๋Ÿผ, ์‹ค์ œ ํŠธ๋ž˜ํ”ฝ์—์„œ๋งŒ ํ™•์ธ๋˜๋Š” ๊ฐ€์ •๋“ค

์ด ๊ธ€์˜ ๋Œ€๋ถ€๋ถ„์€ ์ด๋ ‡๊ฒŒ ๋”ธ๋ ค ์˜จ ๊ฒƒ๋“ค๊ณผ์˜ ์”จ๋ฆ„์ž…๋‹ˆ๋‹ค.

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „๋žต

๊ฐ€์žฅ ๋จผ์ € ์„ธ์šด ๊ณ„ํš์€ ๋‹จ์ˆœํ–ˆ์Šต๋‹ˆ๋‹ค.

โ€œ์•ž์œผ๋กœ ์—…๋กœ๋“œ๋˜๋Š” ํŒŒ์ผ์€ R2๋กœ ์˜ฌ๋ฆฌ๊ณ , ์ฃผ์†Œ๋Š” R2์šฉ ๋ณ„๋„ ๋„๋ฉ”์ธ์œผ๋กœ ์ €์žฅํ•˜์ž. ๊ธฐ์กด ํŒŒ์ผ์€ ๋‚˜์ค‘์— ์˜ฎ๊ธฐ๋ฉด ๋˜์ง€.โ€

๊ทธ๋Ÿฐ๋ฐ ์ด๊ฒŒ ์€๊ทผํ•œ ๋ถ€์ฑ„๊ฐ€ ๋์Šต๋‹ˆ๋‹ค. ์‹ ๊ทœ ํŒŒ์ผ๋งŒ ๋ณ„๋„ ๋„๋ฉ”์ธ์œผ๋กœ ์ €์žฅํ•ด๋‘๋ฉด, ๋‚˜์ค‘์— ๊ธฐ์กด ํŒŒ์ผ๊นŒ์ง€ R2๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•  ๋•Œ ํ•œ ์„œ๋น„์Šค ์•ˆ์— ๋‘ ๊ฐœ์˜ ๋ฏธ๋””์–ด ๋„๋ฉ”์ธ์ด ์„ž์ด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ฒŒ๋‹ค๊ฐ€ ์ธํ”„๋ผ ๊ณ„ํš์ƒ ์ตœ์ข…์ ์œผ๋กœ๋Š” R2๊ฐ€ ๊ธฐ์กด ๋„๋ฉ”์ธ์„ ๊ทธ๋Œ€๋กœ ๋„˜๊ฒจ๋ฐ›์•„ ์„œ๋น™ํ•˜๊ฒŒ ๋  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ๋˜๋ฉด ์• ์จ ๋‚˜๋ˆ ๋‘” ๋ณ„๋„ ๋„๋ฉ”์ธ URL๋“ค์„ ๋‹ค์‹œ ์ „๋ถ€ ๋˜๋Œ๋ ค์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋ฌธ์ œ๋Š” ๊ทธ URL๋“ค์ด ํ•œ ๊ณณ์— ์žˆ์ง€๋„ ์•Š๋‹ค๋Š” ์ ์ด์—ˆ์Šต๋‹ˆ๋‹ค. RDS์—๋„, Firestore์—๋„ ํฉ์–ด์ ธ ์žˆ๊ณ , ์ฝ˜ํ…์ธ  ์ข…๋ฅ˜๋งˆ๋‹ค ์ €์žฅ ์œ„์น˜์™€ ํฌ๋งท์ด ๋‹ฌ๋ž์Šต๋‹ˆ๋‹ค. ์ด๊ฑธ ์ „๋ถ€ ์ฐพ์•„ ์›์ž์ ์œผ๋กœ ๋ฐ”๊พธ๋Š” ๊ฑด ๊ทธ ์ž์ฒด๋กœ ๋˜ ํ•˜๋‚˜์˜ ์œ„ํ—˜ํ•œ ๋Œ€ํ˜• ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๋ฐฉํ–ฅ์„ ๋’ค์ง‘์—ˆ์Šต๋‹ˆ๋‹ค. โ€œURL์€ ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.โ€ ์‹ ๊ทœ ํŒŒ์ผ๋„ ๋ณ„๋„ ๋„๋ฉ”์ธ์œผ๋กœ ๋‚˜๋ˆ„์ง€ ์•Š๊ณ , ๊ธฐ์กด ๋„๋ฉ”์ธ์„ ๊ทธ๋Œ€๋กœ ์“ฐ๋˜ ๋’ค์—์„œ ์˜ค๋ฆฌ์ง„๋งŒ ๋ฐ”๊พธ๋Š” ์ชฝ์œผ๋กœ์š”.

  1. ๊ธฐ์กด ํŒŒ์ผ์„ ํ†ต์งธ๋กœ R2๋กœ ์˜ฎ๊ธด๋‹ค. ์ด๋ฏธ S3์— ์Œ“์—ฌ ์žˆ๋Š” ํŒŒ์ผ์€ Cloudflare๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋„๊ตฌ์ธ Super Slurper๋กœ R2์— ๋ณต์‚ฌํ–ˆ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ ์ชฝ ํŒŒ์ผ์€ ๋Œ€๋ถ€๋ถ„ 10MB ๋ฏธ๋งŒ(์ฃผ๋กœ ์ด๋ฏธ์ง€)์ด๋ผ ํŒŒ์ผ๋‹น 2~3์ดˆ๋ฉด ๋ณต์‚ฌ๊ฐ€ ๋๋‚ฌ๊ณ , ์ด ๋™์•ˆ ์„œ๋น„์Šค ํŠธ๋ž˜ํ”ฝ์€ ์—ฌ์ „ํžˆ 100% CloudFront โ†’ S3๋กœ ํ๋ฆ…๋‹ˆ๋‹ค.
  2. R2 โ†’ S3 ์—ญ๋ฐฉํ–ฅ sync๋ฅผ ๋จผ์ € ๊น”๊ณ , ๊ทธ๋‹ค์Œ ์—…๋กœ๋“œ ๋Œ€์ƒ์„ S3์—์„œ R2๋กœ ๋ฐ”๊พผ๋‹ค. ์ˆœ์„œ๊ฐ€ ์ค‘์š”ํ–ˆ์Šต๋‹ˆ๋‹ค. ์—…๋กœ๋“œ๋ฅผ R2๋กœ ๋Œ๋ฆฌ๋Š” ์ˆœ๊ฐ„์—๋„ DNS๋Š” ์•„์ง CloudFront(=S3 ์˜ค๋ฆฌ์ง„)๋ฅผ ๋ณด๊ณ  ์žˆ์œผ๋‹ˆ, R2์— ์˜ฌ๋ผ์˜จ ํŒŒ์ผ์ด S3์—๋„ ์žˆ์–ด์•ผ ํ™”๋ฉด์— ๋œน๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ R2์— ํŒŒ์ผ์ด ์ƒ๊ธฐ๋ฉด ๊ณง๋ฐ”๋กœ S3๋กœ ๋˜๋Œ๋ ค ๋ณต์ œํ•˜๋Š” sync๋ฅผ ๋จผ์ € ๋ฐฐํฌํ•ด๋’€์Šต๋‹ˆ๋‹ค. ์ด sync๋Š” ์ด๋ฏธ S3์— ๊ฐ™์€ ํŒŒ์ผ์ด ์žˆ์œผ๋ฉด ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ์•Š๋„๋ก ํ•ด์„œ, ์•ž์„œ Super Slurper๋กœ ์˜ฎ๊ฒจ๋‘” ํŒŒ์ผ๊ณผ ๋ถ€๋”ชํžˆ์ง€ ์•Š๊ฒŒ ํ–ˆ์Šต๋‹ˆ๋‹ค. R2 โ†’ S3 ๋ณต์ œ๋Š” ๋Œ€๋žต 6์ดˆ์ฏค ๊ฑธ๋ ธ์Šต๋‹ˆ๋‹ค.
  3. ์ด ์งง์€ ์ฐฝ ์•ˆ์—์„œ DNS๋ฅผ CloudFront์—์„œ Cloudflare๋กœ ๋„˜๊ธด๋‹ค. ์ „ํ™˜ ์ฐฝ ๋™์•ˆ์—๋Š” ์œ ์ €๊ฐ€ ์ด๋ฏธ์ง€๋ฅผ ์˜ฌ๋ฆฐ ๋’ค ๊ทธ๊ฒŒ ๋ณด์ด๊ธฐ๊นŒ์ง€ 6์ดˆ ์ •๋„ ์ง€์—ฐ์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์žฅ์• ๋Š” ์•„๋‹ˆ๊ณ , ์ƒˆ๋กœ๊ณ ์นจ์ด๋‚˜ ์•ฑ ๋‚ด ์žฌ์‹œ๋„๋กœ ํก์ˆ˜๋˜๋Š” ์ˆ˜์ค€์ด์—ˆ์Šต๋‹ˆ๋‹ค. ๋งˆ์นจ DNS ๋ ˆ์ฝ”๋“œ์˜ TTL์ด 60์ดˆ๋ผ, ์˜ค๋ฆฌ์ง„์„ ๋„˜๊ธด ๋’ค ์ „ ์„ธ๊ณ„ ๋ฆฌ์กธ๋ฒ„๊ฐ€ ์ƒˆ ์ฃผ์†Œ๋ฅผ ๋ฌผ์–ด๊ฐ€๋Š” ๋ฐ ๊ธธ์–ด์•ผ 1๋ถ„์ด๋ฉด ์ถฉ๋ถ„ํ–ˆ์Šต๋‹ˆ๋‹ค.
  4. DNS๊ฐ€ ๋„˜์–ด๊ฐ€๋ฉด Worker + R2๊ฐ€ ์ง์ ‘ ์„œ๋น™ํ•œ๋‹ค. ๋„๋ฉ”์ธ์€ ๊ทธ๋Œ€๋กœ๋ผ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ณด๋Š” URL์€ ํ•˜๋‚˜๋„ ๋ฐ”๋€Œ์ง€ ์•Š๊ณ , ๋’ค์—์„œ ์˜ค๋ฆฌ์ง„๋งŒ ๊ฐˆ์•„๋ผ์›Œ์ง„ ์…ˆ์ž…๋‹ˆ๋‹ค.

์ด R2 โ†’ S3 ๋ฐฑ์—… ๊ฒฝ๋กœ๋Š” ๋กค๋ฐฑ ์‹œ S3๊ฐ€ ์›๋ณธ์„ ๊ทธ๋Œ€๋กœ ๊ฐ–๊ณ  ์žˆ๋„๋ก ํ•˜๊ธฐ ์œ„ํ•œ ์šฉ๋„์ž…๋‹ˆ๋‹ค. R2๋กœ ๋„˜์–ด๊ฐ„ ๋’ค ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒจ๋„ DNS๋งŒ ๋˜๋Œ๋ฆฌ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ „์ฒด ๊ตฌ์กฐ๋ฅผ ๊ทธ๋ฆผ์œผ๋กœ ๋ณด๋ฉด ์ด๋ ‡์Šต๋‹ˆ๋‹ค.

์ „ํ™˜ ํ›„ ๋ฏธ๋””์–ด ์„œ๋น™ ๊ตฌ์กฐ

์ฝ๊ธฐ ๊ฒฝ๋กœ๋Š” Client โ†’ Worker โ†’ caches.default โ†’ R2. ๋ฐฑ์—… ๊ฒฝ๋กœ๋Š” R2์— ํŒŒ์ผ์ด ์ƒ๊ธฐ๋ฉด R2 ObjectCreated โ†’ Cloudflare Queue โ†’ consumer Worker โ†’ AWS SQS โ†’ Lambda โ†’ S3๋กœ ๋˜๋Œ๋ ค ๋ณต์ œํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ consumer Worker๋Š” ์ด๋ ‡๊ฒŒ ์ƒ๊ฒผ์Šต๋‹ˆ๋‹ค.

// R2 ObjectCreated โ†’ SQS SendMessage (S3 ๋ฐฑ์—… ํ)
export default {
    async queue(batch: MessageBatch<R2EventNotification>, env: Env): Promise<void> {
        for (const message of batch.messages) {
            try {
                await processMessage(message, env);
            } catch (error) {
                message.retry();
            }
        }
    }
};

// MediaConvert ๊ฒฐ๊ณผ๋ฌผ์ฒ˜๋Ÿผ ์ด๋ฏธ S3์— ์กด์žฌํ•˜๋Š” prefix๋Š” ๋˜๋Œ๋ฆด ํ•„์š”๊ฐ€ ์—†์–ด ์ œ์™ธ
const EXCLUDED_PREFIXES: Record<string, string[]> = {
    'fromm-channel-contents': ['fromm_channel/', 'live/']
    // ...
};

์ฝ”๋“œ์—์„œ ํŠน์ • prefix๋ฅผ ์ œ์™ธํ•˜๋Š” ๊ฑด MediaConvert ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ฐฌํฌ๋‹˜ PoC์˜ ๊ฒฐ๋ก ๋Œ€๋กœ, ์ธ์ฝ”๋”ฉ(MediaConvert)์€ ์˜ฎ๊ธฐ์ง€ ์•Š๊ณ  AWS ๊ฒƒ์„ ๊ทธ๋Œ€๋กœ ์“ฐ๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค. Cloudflare Stream์ด 4KยทAES-128 ๊ฐ™์€ ์˜ต์…˜์„ ๋Œ€๋ถ€๋ถ„ ์ง€์›ํ•˜์ง€ ๋ชปํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์—…๋กœ๋“œ๋Š” R2๋กœ ๋ฐ”๊ฟจ์–ด๋„, MediaConvert๋งŒ์€ ์˜ˆ์™ธ๋กœ ๊ฒฐ๊ณผ๋ฌผ์„ ์—ฌ์ „ํžˆ S3๋กœ ์ถœ๋ ฅํ•˜๊ณ  ๊ทธ๊ฑธ R2๋กœ syncํ•˜๋Š” ๋ฐฉํ–ฅ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰ MediaConvert ๊ฒฐ๊ณผ๋ฌผ์€ ์ฒ˜์Œ๋ถ€ํ„ฐ S3์— ์›๋ณธ์ด ์žˆ์œผ๋‹ˆ, R2 โ†’ S3 ๋ฐฑ์—… ๋Œ€์ƒ์—์„œ ๋นผ๋„ ๋ฉ๋‹ˆ๋‹ค. ์œ„ EXCLUDED_PREFIXES๊ฐ€ ๊ทธ ์˜ˆ์™ธ๋ฅผ ๊ฑธ๋Ÿฌ๋‚ด๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค.

@pulumi/cloudflare๊ฐ€ ์•„์ง R2 Event Notification ๋ฆฌ์†Œ์Šค๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์•„, ์ด๋ฒคํŠธ ์•Œ๋ฆผ โ†’ ํ ์—ฐ๊ฒฐ๋งŒ ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์ˆ˜๋™์œผ๋กœ ๊ฑธ์–ด์ค˜์•ผ ํ–ˆ๋˜ ๊ฑด ๋ค์ด์—ˆ์Šต๋‹ˆ๋‹ค. IaC๋กœ ๋‹ค ๊ฐ์‹ธ๊ณ  ์‹ถ์—ˆ์ง€๋งŒ, ์ง€์›๋˜์ง€ ์•Š๋Š” ๋ถ€๋ถ„์€ ์ธ์ •ํ•˜๊ณ  README์— โ€œ์—ฌ๊ธฐ๋Š” ์†์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•จโ€์„ ๋ช…์‹œํ•ด๋‘๋Š” ํŽธ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

์‚ฝ์งˆ 1 โ€” Cloudflare ์บ์‹œ๋Š” CloudFront์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜์ง€ ์•Š์•˜๋‹ค

์ด๋ฒˆ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์˜ ๊ฐ€์žฅ ํฐ ๋ชฉํ‘œ๋Š” ๋น„์šฉ ์ ˆ๊ฐ์ด์—ˆ์Šต๋‹ˆ๋‹ค. Cloudflare์˜ egress๊ฐ€ ๋ฌด๋ฃŒ๋ผ๋Š” ์ ์„ ์ตœ๋Œ€ํ•œ ์‚ด๋ฆฌ๋Š” ๊ฒŒ ๋ชฉํ‘œ์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ egress๊ฐ€ ๊ณต์งœ์—ฌ๋„ R2๋Š” ํŒŒ์ผ์— ์ ‘๊ทผํ•  ๋•Œ๋งˆ๋‹ค operation ๋น„์šฉ์ด ๋ถ™์Šต๋‹ˆ๋‹ค. ์‹ค์งˆ์ ์ธ ๋น„์šฉ์€ ๊ฒฐ๊ตญ ์—ฌ๊ธฐ์„œ ๋‚œ๋‹ค๊ณ  ๋ดค๊ณ (์ž์„ธํ•œ ์ˆซ์ž๋Š” ๋‹ค์Œ ์žฅ์—์„œ ๋‹ค๋ฃน๋‹ˆ๋‹ค), ๊ทธ๋ž˜์„œ ์บ์‹œ๋กœ R2 ์ ‘๊ทผ ํšŸ์ˆ˜๋ฅผ ์ตœ๋Œ€ํ•œ ์ค„์ด๋Š” ๊ฒŒ ์ด ์ „ํ™˜์˜ ํ•ต์‹ฌ ๊ณผ์ œ์˜€์Šต๋‹ˆ๋‹ค.

๋ฌธ์ œ๋Š” โ€œCloudFront๊ฐ€ ํ•ด์ฃผ๋˜ ์„œ๋น™๊ณผ ์บ์‹ฑ์„ Worker + R2์—์„œ ์–ด๋–ป๊ฒŒ ๋˜‘๊ฐ™์ด ์žฌํ˜„ํ•˜์ง€?โ€์˜€๋Š”๋ฐ, ์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ ์˜ˆ์ƒ์ด ์–ด๊ธ‹๋‚ฌ์Šต๋‹ˆ๋‹ค. ์บ์‹œ ๊ตฌ์กฐ๋ฅผ ์กฐ์‚ฌํ•˜๋˜ ์ค‘ AI๊ฐ€ โ€œ์‘๋‹ต์— Cache-Control: public์„ ์ฃผ๋ฉด Cloudflare CDN์ด ์บ์‹ฑํ•ด์„œ ๊ทธ๋‹ค์Œ๋ถ€ํ„ฐ๋Š” Worker๊นŒ์ง€ ์š”์ฒญ์ด ์˜ค์ง€ ์•Š๋Š”๋‹คโ€๊ณ  ์•Œ๋ ค์คฌ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿด๋“ฏํ•ด์„œ ๊ทธ๋Œ€๋กœ ๋ถ™์—ฌ๋ดค๋Š”๋ฐ, Worker๋Š” ๋งค ์š”์ฒญ ๊ณ„์† ํ˜ธ์ถœ๋์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ํ™•์ธํ•ด๋ณด๋‹ˆ ๊ทธ๋Ÿฐ ๋™์ž‘์€ ์—†์—ˆ๊ณ , ์ด๊ฑธ ๋ถ™์žก๊ณ  ์ž ๊น ํ—ค๋งธ์Šต๋‹ˆ๋‹ค. ๋ฌธ์„œ์™€ ์‹ค์ œ ๋™์ž‘์€ ์ง์ ‘ ์ฐ์–ด๋ณด๋ฉฐ ๊ฐ€์•ผ๊ฒ ๋‹ค๊ณ  ๋งˆ์Œ๋จน์€ ๊ณ„๊ธฐ์ด๊ธฐ๋„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ง์ ‘ ํ™•์ธํ•ด๋ณด๋‹ˆ Cloudflare์˜ caches.default๋Š” CloudFront์ฒ˜๋Ÿผ โ€œWorker ์•ž๋‹จ์—์„œ ์•Œ์•„์„œ ๊ฑธ๋Ÿฌ์ฃผ๋Š” ์ž๋™ CDN ์บ์‹œโ€๊ฐ€ ์•„๋‹ˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฑด Worker ์ฝ”๋“œ ์•ˆ์—์„œ ๋‚ด๊ฐ€ ์ง์ ‘ cache.match()๋กœ ์กฐํšŒํ•˜๊ณ  cache.put()์œผ๋กœ ์ €์žฅํ•˜๋Š” ์ˆ˜๋™ ์บ์‹œ์ž…๋‹ˆ๋‹ค. ์บ์‹œ ๋‚ด์šฉ์ด ์ „ ์„ธ๊ณ„ ์—ฃ์ง€๋กœ ์ž๋™ ๋ณต์ œ๋˜์ง€๋„ ์•Š์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ์‹ค์ œ ๋ชจ๋ธ์€ ์ด๋ ‡์Šต๋‹ˆ๋‹ค.

Client โ†’ Worker(์‹คํ–‰๋จ) โ†’ caches.default(match/put) โ†’ R2

Worker๋Š” ์–ด์ฐจํ”ผ ๋งค ์š”์ฒญ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๋Œ€์‹  ๊ทธ ์•ˆ์—์„œ ์บ์‹œ๋ฅผ ์ž˜ ํ™œ์šฉํ•˜๋ฉด R2 ์ ‘๊ทผ์„ ์ค„์ด๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. โ€œWorker๋ฅผ ์•ˆ ๊ฑฐ์น˜๊ฒŒ ๋งŒ๋“ ๋‹คโ€๊ฐ€ ์•„๋‹ˆ๋ผ โ€œWorker๋Š” ๊ฑฐ์น˜๋˜ R2๋Š” ์•ˆ ๊ฑด๋“œ๋ฆฌ๊ฒŒ ๋งŒ๋“ ๋‹คโ€๊ฐ€ ๋ชฉํ‘œ์˜€๋˜ ๊ฒ๋‹ˆ๋‹ค. ์ด ์ง€์ ์—์„œ ์ ‘๊ทผ ๋ฐฉ์‹์ด ์™„์ „ํžˆ ๋ฐ”๋€Œ์—ˆ์Šต๋‹ˆ๋‹ค.

์‚ฝ์งˆ 2 โ€” egress 0์›์˜ ํ•จ์ •, R2๋„ ์ ‘๊ทผํ•˜๋ฉด ๋ˆ์ด๋‹ค

R2๋ฅผ ์„ ํƒํ•œ ์ด์œ ๋Š” โ€œegress ๋น„์šฉ์ด ์—†๋‹คโ€ ์ž…๋‹ˆ๋‹ค. CloudFront ์ฒญ๊ตฌ์„œ์˜ 90% ์ด์ƒ์ด ๋ฐ์ดํ„ฐ ์ „์†ก ๋น„์šฉ์ด์—ˆ์œผ๋‹ˆ, ์—ฌ๊ธฐ๊ฐ€ 0์›์ด ๋˜๋Š” ๊ฑด ํฐ ์ž„ํŒฉํŠธ์˜€์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์ €๋Š” ๊ทธ ๋ฌธ์žฅ์— ์ทจํ•ด์„œ โ€œR2 ์ ‘๊ทผ ์ž์ฒด๋„ ๋ˆ์ด ๋“ ๋‹คโ€ ๋Š” ๊ฑธ ํ•œ๋™์•ˆ ์žŠ๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

R2๋Š” egress๋Š” ๊ณต์งœ์ง€๋งŒ, ์˜คํผ๋ ˆ์ด์…˜์—๋Š” ๊ณผ๊ธˆํ•ฉ๋‹ˆ๋‹ค. GetObject, HeadObject ๊ฐ™์€ ์ฝ๊ธฐ๋Š” Class B operation์œผ๋กœ ๋ถ„๋ฅ˜๋˜๊ณ , 2026๋…„ 4์›” ๊ธฐ์ค€ ๋ฐฑ๋งŒ ์š”์ฒญ๋‹น $0.36์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ์งš์–ด๋‘˜ ๊ฒŒ, ์ด ๋‹จ๊ฐ€๋Š” S3์™€ ๊ฑฐ์˜ ๊ฐ™์Šต๋‹ˆ๋‹ค. S3 Standard์˜ GET ์š”์ฒญ๋„ ๋ฐฑ๋งŒ ๊ฑด๋‹น $0.40 ์ˆ˜์ค€์ด๋ผ, ์Šคํ† ๋ฆฌ์ง€๋ฅผ R2๋กœ ์˜ฎ๊ธด๋‹ค๊ณ  ์š”์ฒญ ๋‹จ๊ฐ€ ์ž์ฒด๊ฐ€ ์‹ธ์ง€๋Š” ๊ฑด ์•„๋‹ˆ์—์š”. R2๊ฐ€ ์ด๊ธฐ๋Š” ์ง€์ ์€ ์–ด๋””๊นŒ์ง€๋‚˜ egress ํ•œ ๊ณณ๋ฟ์ž…๋‹ˆ๋‹ค.

๋ฏธ๋””์–ด ํŠธ๋ž˜ํ”ฝ์— ๋Œ€์ž…ํ•ด๋ณด๋ฉด ๊ฐ์ด ์˜ต๋‹ˆ๋‹ค. (์•„๋ž˜ ์ˆซ์ž๋Š” ์ดํ•ด๋ฅผ ๋•๊ธฐ ์œ„ํ•œ ์˜ˆ์‹œ์ด๋ฉฐ, ์‹ค์ œ ์„œ๋น„์Šค ํŠธ๋ž˜ํ”ฝ ๊ธฐ์ค€์€ ์•„๋‹™๋‹ˆ๋‹ค.)

  • ํ•˜๋ฃจ 1,000๋งŒ ๊ฑด ์ฝ๊ธฐ ร— 30์ผ = 3์–ต ๊ฑด
  • (3์–ต - ๋ฌด๋ฃŒ 1,000๋งŒ ๊ฑด) / 100๋งŒ ร— $0.36 = $104.40 / ์›”

์ด๊ฑด โ€œ์บ์‹œ๊ฐ€ ํ•˜๋‚˜๋„ ์•ˆ ๋จน์—ˆ์„ ๋•Œโ€์˜ ์ˆซ์ž์ž…๋‹ˆ๋‹ค. egress๋ฅผ 0์›์œผ๋กœ ๋งŒ๋“ค์–ด๋†“๊ณ ๋„ ์บ์‹œ๋กœ R2 ๋„๋‹ฌ์„ ๋ง‰์ง€ ๋ชปํ•˜๋ฉด, S3์—์„œ ๋‚ด๋˜ ๊ฒƒ๊ณผ ๋ณ„๋กœ ๋‹ค๋ฅด์ง€ ์•Š์€ operation ๋น„์šฉ์ด ๊ทธ๋Œ€๋กœ ์ƒˆ์–ด ๋‚˜๊ฐ‘๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ๊ตณ์ด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ๊ฐํ–‰ํ•  ์ด์œ ๊ฐ€ ์—†์„ ๋งŒํผ ๋น„์šฉ ๊ฐœ์„ ํญ์ด ์ž‘์•„์ง‘๋‹ˆ๋‹ค. ์ „ํ™˜์˜ ์ง„์งœ ์ด๋“์€ โ€œegress 0์›โ€์ด ์•„๋‹ˆ๋ผ โ€œR2 ์ ‘๊ทผ์„ ์บ์‹œ๋กœ ์–ผ๋งˆ๋‚˜ ํ‹€์–ด๋ง‰๋А๋ƒโ€ ์— ๋‹ฌ๋ ค ์žˆ์—ˆ๋˜ ๊ฒ๋‹ˆ๋‹ค.

์ด ๊นจ๋‹ฌ์Œ์ด ์ดํ›„ ๋ชจ๋“  ์„ค๊ณ„์˜ ๊ธฐ์ค€์ด ๋์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์š”์ฒญ์€ R2์— ๋„๋‹ฌํ•˜์ง€ ์•Š๋Š” ๊ฑธ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์‚ผ๋Š”๋‹ค. HEAD ์š”์ฒญ ํ•˜๋‚˜๊นŒ์ง€๋„์š”.

์‚ฝ์งˆ 3 โ€” private์œผ๋กœ ์‘๋‹ตํ•ด์•ผ ํ•˜๋Š”๋ฐ ์บ์‹œ๋Š” public๋งŒ ์ €์žฅํ•œ๋‹ค

์—ฌ๊ธฐ๊ฐ€ ์ด๋ฒˆ ์ „ํ™˜์—์„œ ๊ฐ€์žฅ ์˜ค๋ž˜ ๋ถ™์žก๊ณ  ์žˆ์—ˆ๋˜ ์ง€์ ์ž…๋‹ˆ๋‹ค. ๋‘ ์š”๊ตฌ์‚ฌํ•ญ์ด ์ •๋ฉด์œผ๋กœ ์ถฉ๋Œํ–ˆ์Šต๋‹ˆ๋‹ค.

์š”๊ตฌ์‚ฌํ•ญ A. ํด๋ผ์ด์–ธํŠธ์—๋Š” private์œผ๋กœ ์‘๋‹ตํ•ด์•ผ ํ•œ๋‹ค. ์ €ํฌ ๋ฏธ๋””์–ด๋Š” ์‚ฌ์šฉ์ž๋ณ„ ๊ถŒํ•œ์ด ๋‹ค๋ฅธ ์ฝ˜ํ…์ธ ์ž…๋‹ˆ๋‹ค. ๋งŒ์•ฝ Cache-Control: public์œผ๋กœ ๋‚ด๋ ค๋ณด๋‚ด๋ฉด, ์ค‘๊ฐ„์˜ ๋‹ค์šด์ŠคํŠธ๋ฆผ ํ”„๋ก์‹œ๋‚˜ Next.js ์„œ๋ฒ„ ๊ฐ™์€ ๊ณต์œ  ์บ์‹œ๊ฐ€ A ์‚ฌ์šฉ์ž์˜ ์‘๋‹ต์„ B ์‚ฌ์šฉ์ž์—๊ฒŒ ์žฌ์‚ฌ์šฉํ•ด๋ฒ„๋ฆด ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ํด๋ผ์ด์–ธํŠธ ๊ณ„์•ฝ์€ ๋ฌด์กฐ๊ฑด private์ด์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

์š”๊ตฌ์‚ฌํ•ญ B. ์บ์‹œ์— ์ €์žฅํ•˜๋ ค๋ฉด public์ด์–ด์•ผ ํ•œ๋‹ค. ๋ฌธ์ œ๋Š” ์—ฌ๊ธฐ์„œ ๋“œ๋Ÿฌ๋‚ฌ์Šต๋‹ˆ๋‹ค. ์š”๊ตฌ์‚ฌํ•ญ A๋ฅผ ์ง€ํ‚ค๋ ค๊ณ  private์œผ๋กœ ์‘๋‹ตํ•˜๊ฒŒ ํ–ˆ๋”๋‹ˆ, ์‹œ๊ฐ„์ด ์ง€๋‚˜๋„ ์บ์‹œ๊ฐ€ ๊ณ„์† MISS๋กœ๋งŒ ์žกํ˜”์Šต๋‹ˆ๋‹ค. ์•Œ๊ณ  ๋ณด๋‹ˆ caches.default๋Š” Cache-Control: private ์‘๋‹ต์„ cache.put() ํ•ด๋„ ์—๋Ÿฌ๋„ ๋กœ๊ทธ๋„ ์—†์ด ์กฐ์šฉํžˆ ์ €์žฅ์„ ๊ฑด๋„ˆ๋›ฐ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ์‚ฌ์‹ค์„ ๋ชจ๋ฅธ ์ฑ„ ํ•œ์ฐธ์„ ๋ถ™๋“ค๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์บ์‹œ๊ฐ€ MISS๋กœ๋งŒ ์žกํžŒ๋‹ค๋Š” ๊ฑธ ๋ˆˆ์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ๊ฑด, Worker์—์„œ ์‘๋‹ต์„ ์บ์‹œ๋กœ ๋‚ด์ค„ ๋• X-Cache: HIT, R2๊นŒ์ง€ ๋‹ค๋…€์™”์„ ๋• X-Cache: MISS๋ฅผ ์ปค์Šคํ…€ ํ—ค๋”๋กœ ๋ถ™์—ฌ๋‘” ๋•๋ถ„์ž…๋‹ˆ๋‹ค. curl -I๋กœ ์ฐ์–ด๋ณด๋ฉด ์•„๋ฌด๋ฆฌ ๋‹ค์‹œ ์š”์ฒญํ•ด๋„ HIT์ด ๋œจ์งˆ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

์ •๋ฆฌํ•˜๋ฉด ์ด๋ ‡์Šต๋‹ˆ๋‹ค.

  • private์œผ๋กœ ์‘๋‹ตํ•ด์•ผ โ†’ ๋ณด์•ˆ ๊ณ„์•ฝ ์ง€ํ‚ด โ†’ ํ•˜์ง€๋งŒ ์บ์‹œ ์ €์žฅ ์‹คํŒจ
  • public์œผ๋กœ ์‘๋‹ตํ•˜๋ฉด โ†’ ์บ์‹œ ์ €์žฅ ์„ฑ๊ณต โ†’ ํ•˜์ง€๋งŒ ๋ณด์•ˆ ๊ณ„์•ฝ ๊นจ์ง

ํ•œ๋™์•ˆ ๋‹ต์ด ์—†์–ด ๋ณด์˜€์Šต๋‹ˆ๋‹ค. ํŒ€์›๊ณผ ํ™”์ดํŠธ๋ณด๋“œ ์•ž์—์„œ ํ•œ์ฐธ ์ด์•ผ๊ธฐํ•˜๋‹ค, ๋ฐ˜์ฏค ๋†๋‹ด์ฒ˜๋Ÿผ ๋‚˜์˜จ ์•„์ด๋””์–ด๊ฐ€ ๊ฒฐ๊ตญ ์ฑ„ํƒ๋์Šต๋‹ˆ๋‹ค.

โ€œ๊ทธ๋Ÿผโ€ฆ ์ €์žฅํ•  ๋•Œ๋Š” public์œผ๋กœ ์†์—ฌ์„œ ๋„ฃ๊ณ , ๊บผ๋‚ด์„œ ์‘๋‹ตํ•  ๋•Œ ๋‹ค์‹œ private์œผ๋กœ ๋˜๋Œ๋ฆฌ๋ฉด ๋˜๋Š” ๊ฑฐ ์•„๋ƒ?โ€

์ฒ˜์Œ์—” ์ด๊ฑธ ์ œ๋Œ€๋กœ ๋œ ๋ฐฉ๋ฒ•์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜์ง„ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์ด๊ฒƒ ๋ง๊ณ ๋Š” ๋‘ ์š”๊ตฌ์‚ฌํ•ญ์„ ๋™์‹œ์— ๋งŒ์กฑ์‹œํ‚ฌ ๊ธธ์ด ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ํ•ต์‹ฌ์€ ์›๋ณธ Cache-Control์„ ๋ณ„๋„ ํ—ค๋”์— ๋ฐฑ์—…ํ•ด๋‘๊ณ , ์บ์‹œ์—๋Š” public์œผ๋กœ ์ €์žฅํ•œ ๋’ค, HIT๋˜์–ด ๊บผ๋‚ผ ๋•Œ ๋ฐฑ์—…ํ•ด๋‘” ๊ฐ’์œผ๋กœ ๋ณต์›ํ•˜๋Š” ๊ฒ๋‹ˆ๋‹ค.

์ €์žฅํ•  ๋• public, ์‘๋‹ตํ•  ๋• private

// ์ €์žฅ์šฉ: ์›๋ณธ CC๋ฅผ X-Original-Cache-Control ์— ๋ณด์กดํ•˜๊ณ , ์ €์žฅ๋ณธ์€ public ์œผ๋กœ ์šฐํšŒ
function buildCacheEntry(response: Response, isSigned: boolean): Response {
    const cloned = response.clone();
    const toCache = new Response(cloned.body, {
        status: cloned.status,
        headers: new Headers(cloned.headers)
    });

    if (isSigned) {
        toCache.headers.set('X-Original-Cache-Control', toCache.headers.get('Cache-Control') ?? '');
        toCache.headers.set('Cache-Control', 'public, max-age=86400');
    }
    return toCache;
}

// ์‘๋‹ต์šฉ: HIT ์‹œ ์›๋ณธ CC ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์› โ†’ ํด๋ผ์ด์–ธํŠธ๋Š” ์—ฌ์ „ํžˆ private ์„ ์ˆ˜์‹ 
function restoreFromCacheEntry(cached: Response): Response {
    const response = new Response(cached.body, {
        status: cached.status,
        headers: new Headers(cached.headers)
    });

    if (response.headers.has('X-Original-Cache-Control')) {
        response.headers.set('Cache-Control', response.headers.get('X-Original-Cache-Control') ?? '');
        response.headers.delete('X-Original-Cache-Control');
    }
    return response;
}

์ด๋ ‡๊ฒŒ ํ•˜๋‹ˆ ๊ณ„์ธต๋งˆ๋‹ค ๋ณด๋Š” ๊ฐ’์ด ์ •ํ™•ํžˆ ๊ฐˆ๋ฆฝ๋‹ˆ๋‹ค.

๊ณ„์ธต๋ณด์ด๋Š” Cache-Control์˜๋„
ํด๋ผ์ด์–ธํŠธ/๋ธŒ๋ผ์šฐ์ €private, max-age=86400๋ธŒ๋ผ์šฐ์ € ๋กœ์ปฌ ์บ์‹œ๋Š” ํ—ˆ์šฉ, ๊ณต์œ  ์บ์‹œ ์žฌ์‚ฌ์šฉ์€ ์ฐจ๋‹จ
์ค‘๊ฐ„ ํ”„๋ก์‹œ/CDNprivate์‚ฌ์šฉ์ž ๊ฐ„ ์‘๋‹ต ์žฌ์‚ฌ์šฉ ๋ฐฉ์ง€
Worker Cache APIpublic, max-age=86400cache.put() ์ €์žฅ ํ™œ์„ฑํ™”
Worker ์ธ์ฆ๋งค ์š”์ฒญ ์ˆ˜ํ–‰signed cookie/query ๊ฒ€์ฆ ๋ณด์žฅ

caches.default๋ผ๋Š” ๋‚ด๋ถ€ ์ €์žฅ์†Œ์—๋งŒ ์ž ๊น public์ด๋ผ๋Š” ๊ฐ€๋ฉด์„ ์”Œ์šฐ๊ณ , ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€๋Š” ์ˆœ๊ฐ„ ๋‹ค์‹œ private ์–ผ๊ตด๋กœ ๋Œ๋ ค๋†“๋Š” ์…ˆ์ž…๋‹ˆ๋‹ค. private ๊ฒฝ๋กœ(signed)๋Š” Edge ์ž๋™ ์บ์‹œ๋ฅผ ํƒ€์ง€ ์•Š์œผ๋‹ˆ Worker๊ฐ€ ๋งค ์š”์ฒญ ์‹คํ–‰๋˜์–ด ์ธ์ฆ์€ ํ•ญ์ƒ ๋ณด์žฅ๋˜๊ณ , ๊ทธ๋Ÿฌ๋ฉด์„œ๋„ Worker Cache๋กœ R2 ์ ‘๊ทผ์€ ์ค„์–ด๋“ญ๋‹ˆ๋‹ค.

Worker ํ•˜๋‚˜์— ๋ˆŒ๋Ÿฌ ๋‹ด์€ ์ž์ž˜ํ•œ ์ฒ˜๋ฆฌ๋“ค

์บ์‹œ ๋”œ๋ ˆ๋งˆ๋ฅผ ํ’€๊ณ  ๋‚˜์„œ๋„, โ€œCloudFront๊ฐ€ ์•„๋ฌด ๋ง ์—†์ด ํ•ด์ฃผ๋˜ ๊ฒƒ๋“คโ€์„ ํ•˜๋‚˜ํ•˜๋‚˜ ์†์œผ๋กœ ๋˜์‚ด๋ ค์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์‚ฝ์งˆ 2์—์„œ ์ •ํ•œ ๊ธฐ์ค€ โ€” ๊ธฐ๋ณธ๊ฐ’์€ R2์— ๋„๋‹ฌํ•˜์ง€ ์•Š๋Š” ๊ฒƒ โ€” ์„ ์ง€ํ‚ค๊ธฐ ์œ„ํ•œ ๋””ํ…Œ์ผ๋“ค์ž…๋‹ˆ๋‹ค.

Signed URL ์บ์‹œ ํ‚ค ์ •๊ทœํ™”. Signed URL์€ ์‚ฌ์šฉ์ž๋งˆ๋‹ค ?Policy=...&Signature=... query๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ์ด๊ฑธ ๊ทธ๋Œ€๋กœ ์บ์‹œ ํ‚ค๋กœ ์“ฐ๋ฉด ๊ฐ™์€ ํŒŒ์ผ์ธ๋ฐ๋„ ์‚ฌ์šฉ์ž๋งˆ๋‹ค ์บ์‹œ๊ฐ€ ๋”ฐ๋กœ ์žกํ˜€์„œ ํžˆํŠธ์œจ์ด ๋ฐ”๋‹ฅ์„ ์นฉ๋‹ˆ๋‹ค. ์บ์‹œ ํ‚ค์—์„œ๋Š” query๋ฅผ ๋ฒ„๋ฆฌ๊ณ  pathname๋งŒ ์“ด ์ด์œ ์ž…๋‹ˆ๋‹ค.

// signed URL ์˜ ํ† ํฐ query ์ œ๊ฑฐ โ†’ ๋™์ผ ํŒŒ์ผ์ด ๊ฐ™์€ ์บ์‹œ ์—”ํŠธ๋ฆฌ๋ฅผ ๊ณต์œ 
function signedRequestToCacheKey(request: Request): Request {
    const url = new URL(request.url);
    return new Request(`${url.origin}${url.pathname}`);
}

HEAD ์š”์ฒญ๋„ GET ์บ์‹œ๋กœ ๋•Œ์šด๋‹ค. HEAD ์š”์ฒญ ์—ญ์‹œ R2 ์ž…์žฅ์—์„  Class B operation, ๊ณง ๋ˆ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ HEAD๊ฐ€ ํ•„์š”๋กœ ํ•˜๋Š” ๊ฑด ETagยทLast-ModifiedยทContent-TypeยทContent-Length ๊ฐ™์€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฟ์ด๊ณ , ์ด๊ฑด ์ด๋ฏธ ์ €์žฅํ•ด๋‘” GET ์บ์‹œ ์•ˆ์— ๋‹ค ์žˆ์Šต๋‹ˆ๋‹ค. HEAD๊ฐ€ ์˜ค๋ฉด GET ์บ์‹œ๋ฅผ ํ—ค๋” ์†Œ์Šค๋กœ ์žฌ์‚ฌ์šฉํ•ด R2๊นŒ์ง€ ์•ˆ ๊ฐ‘๋‹ˆ๋‹ค (Cache API๋Š” GET๋งŒ ๋งค์นญํ•˜๋ฏ€๋กœ ์กฐํšŒ ์‹œ ํ‚ค๋„ GET์œผ๋กœ ๋งŒ๋“ค์–ด ๋„˜๊น๋‹ˆ๋‹ค).

์กฐ๊ฑด๋ถ€ ์š”์ฒญ 304๋ฅผ Worker์—์„œ ๋๋‚ธ๋‹ค. If-None-Match / If-Modified-Since๊ฐ€ ๋ถ™์€ ์š”์ฒญ์ด ์˜ค๋ฉด, ์บ์‹œ๋œ ์‘๋‹ต์˜ ETagยทLast-Modified๋งŒ์œผ๋กœ Worker๊ฐ€ ์ง์ ‘ 304๋ฅผ ๋งŒ๋“ค์–ด ๋Œ๋ ค์ค๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ์ด ์—†๋‹ค๋Š” ๊ฑธ ํ™•์ธํ•˜๋ ค๊ณ  R2๊นŒ์ง€ ๋‹ค๋…€์˜ฌ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

Range ์š”์ฒญ์€ ์ •์งํ•˜๊ฒŒ ์šฐํšŒํ•œ๋‹ค. ๋™์˜์ƒ seek์ฒ˜๋Ÿผ Range(206 Partial Content) ์š”์ฒญ์€ Cache API๊ฐ€ 206์„ ์ €์žฅํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฑด ์–ต์ง€๋กœ ์บ์‹ฑํ•˜๋ ค ํ•˜์ง€ ์•Š๊ณ  R2๋กœ ์ง์ ‘ ์„œ๋น™ํ•˜๋˜, ์‘๋‹ต์— X-Cache: BYPASS๋ฅผ ๋ฐ•์•„ โ€œ์–˜๋Š” ์˜๋„์ ์œผ๋กœ ์บ์‹œ๋ฅผ ์šฐํšŒํ–ˆ๋‹คโ€๋ฅผ ๋ช…์‹œํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๋””๋ฒ„๊น…์šฉ์œผ๋กœ ๋ชจ๋“  ์‘๋‹ต์— X-Cache ํ—ค๋”๋ฅผ ๋‚จ๊ฒผ์Šต๋‹ˆ๋‹ค.

  • HIT โ€” Worker Cache์—์„œ ์ฐพ์Œ (R2 ์•ˆ ๊ฐ)
  • MISS โ€” R2์—์„œ ์ฝ์–ด์˜ด (๊ทธ๋ฆฌ๊ณ  ์บ์‹œ์— ์ €์žฅ)
  • BYPASS โ€” Range ๋“ฑ์œผ๋กœ ์˜๋„์  ์šฐํšŒ

์šด์˜์—์„œ ์บ์‹œ๊ฐ€ ๋„๋Š”์ง€ ์•„๋‹Œ์ง€๋ฅผ curl -I๋กœ ํ•œ ๋ฒˆ์— ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์„œ, ์ด ํ—ค๋” ํ•˜๋‚˜๊ฐ€ ๋‘๊ณ ๋‘๊ณ  ๋ฐฅ๊ฐ’์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

CORS ์˜ค์—ผ ๋ฐฉ์ง€. CORS ์‘๋‹ต์€ ์š”์ฒญ Origin์— ๋”ฐ๋ผ Access-Control-Allow-Origin ๊ฐ’์ด ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค. ์ด๊ฑธ ์บ์‹œ๊ฐ€ ๋ฌด์‹ฌ์ฝ” ์žฌ์‚ฌ์šฉํ•˜๋ฉด A ๋„๋ฉ”์ธ์šฉ ACAO๊ฐ€ B ๋„๋ฉ”์ธ์— ์‘๋‹ต๋˜์–ด CORS ์—๋Ÿฌ๊ฐ€ ํ„ฐ์ง‘๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ Vary: Origin์„ ๋ถ™์—ฌ โ€œ์ด ์‘๋‹ต์€ Origin๋งˆ๋‹ค ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋‹คโ€๋ฅผ ๋ช…์‹œํ–ˆ์Šต๋‹ˆ๋‹ค.

CloudFront ํฌ๋งท ๊ทธ๋Œ€๋กœ ๊ฒ€์ฆํ•˜๊ธฐ

์ฐฌํฌ๋‹˜ PoC์—์„œ๋Š” ์ ‘๊ทผ ์ œ์–ด๋ฅผ โ€œWorker์—์„œ JWT ๊ฐ™์€ ๊ฑธ ์ƒˆ๋กœ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋‹คโ€๋Š” ๊ฐ€๋Šฅ์„ฑ์œผ๋กœ ์—ด์–ด๋’€์Šต๋‹ˆ๋‹ค. ์ž์œ ๋„๋Š” ๋†’์ง€๋งŒ, ์‹ค์ „์—์„œ ๊ทธ ๊ธธ๋กœ ๊ฐ€๋ฉด ํด๋ผ์ด์–ธํŠธ(์•ฑ/์›น)๋ฅผ ์ „๋ถ€ ์ƒˆ ํ† ํฐ ๋ฐฉ์‹์œผ๋กœ ์žฌ๋ฐฐํฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ๋งŽ์€ ์•ฑ๋“ค์ด CloudFront Signed Cookie/URL์„ ์“ฐ๊ณ  ์žˆ๋Š” ์ƒํ™ฉ์—์„œ, ๊ทธ๊ฑด ์ „ํ™˜์˜ ๋ฆฌ์Šคํฌ๋ฅผ ํ‚ค์šฐ๋Š” ์„ ํƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ •๋ฐ˜๋Œ€๋กœ ๊ฐ”์Šต๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” ์•„๋ฌด๊ฒƒ๋„ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ง€๊ธˆ ๋ณด๋‚ด๋Š” CloudFront Signed Cookie/URL์„, Worker๊ฐ€ CloudFront์ธ ์ฒ™ ๊ทธ๋Œ€๋กœ ๊ฒ€์ฆํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

๋ฌธ์ œ๋Š” CloudFront์˜ ์„œ๋ช…์ด ํ‘œ์ค€ JWT๊ฐ€ ์•„๋‹ˆ๋ผ๋Š” ์ ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ช… ์•Œ๊ณ ๋ฆฌ์ฆ˜๋„, base64 ์ธ์ฝ”๋”ฉ ๋ฐฉ์‹๋„ CloudFront ๊ณ ์œ  ๊ทœ๊ฒฉ์ด๋ผ ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ๋Š” ๊ฒ€์ฆ์ด ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ด ๋ถ€๋ถ„์€ AI์˜ ๋„์›€์„ ๋ฐ›์•„ CloudFront ํฌ๋งท์„ ๊ทธ๋Œ€๋กœ ๊ฒ€์ฆํ•˜๋Š” ์ธ์ฆ ๋กœ์ง์„ ์„ค๊ณ„ํ•˜๊ณ  ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋•๋ถ„์— ์›น์—์„œ ์˜ค๋Š” Signed Cookie๋„, ์•ˆ๋“œ๋กœ์ด๋“œ ์™ธ๋ถ€ ํ”Œ๋ ˆ์ด์–ด์ฒ˜๋Ÿผ ์ฟ ํ‚ค ์ „๋‹ฌ์ด ๊นŒ๋‹ค๋กœ์šด ํ™˜๊ฒฝ์—์„œ ์˜ค๋Š” Signed URL๋„ Worker๊ฐ€ ๊ทธ๋Œ€๋กœ ๋ฐ›์•„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋์Šต๋‹ˆ๋‹ค.

๋งˆ์น˜๋ฉฐ

์ด๋ฒˆ ์ „ํ™˜์—์„œ ๋ชธ์œผ๋กœ ๋ฐฐ์šด ๊ฑด ๋ช‡ ๊ฐ€์ง€์˜€์Šต๋‹ˆ๋‹ค. egress๊ฐ€ 0์›์ด๋ผ๋Š” ๊ฒƒ๋งŒ ๋ณด๊ณ  ๋“ค์–ด๊ฐ”์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” R2 ์ ‘๊ทผ์„ ์บ์‹œ๋กœ ์–ผ๋งˆ๋‚˜ ์ค„์ด๋А๋ƒ๊ฐ€ ๊ด€๊ฑด์ด์—ˆ์Šต๋‹ˆ๋‹ค. CloudFront์—์„œ ๋˜๋˜ ๊ฒŒ Cloudflare์—์„œ ๊ทธ๋Œ€๋กœ ๋˜์ง€ ์•Š์•„, ๋ฌธ์„œ๋ณด๋‹ค ์ง์ ‘ ์ฐ์–ด๋ณธ ๊ฐ’์„ ๋ฏฟ์–ด์•ผ ํ–ˆ๋˜ ๊ฒƒ๋„ ์ปธ๊ณ ์š”.

buildCacheEntry / restoreFromCacheEntry ๊ฐ™์€ ์ฝ”๋“œ๋Š” ์ฒ˜์Œ ๋ณด๋ฉด โ€œ์™œ Cache-Control์„ ๋‘ ๋ฒˆ ๋ฐ”๊พธ์ง€?โ€ ์‹ถ์Šต๋‹ˆ๋‹ค. ์ด๊ฑด ์‹ค์ˆ˜๋กœ ์ƒ๊ธด ๊ฒŒ ์•„๋‹ˆ๋ผ, caches.default๊ฐ€ private์„ ๊ฑฐ๋ถ€ํ•œ๋‹ค๋Š” ์ œ์•ฝ๊ณผ ํด๋ผ์ด์–ธํŠธ์— private์„ ๋ณด์žฅํ•ด์•ผ ํ•œ๋‹ค๋Š” ์š”๊ตฌ๊ฐ€ ๋งŒ๋‚˜๋Š” ์ง€์ ์— ์ผ๋ถ€๋Ÿฌ ๊ทธ์–ด๋‘” ๊ฒฝ๊ณ„์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ฃผ์„์„ ์œ ๋‚œํžˆ ๊ธธ๊ฒŒ ๋‹ฌ์•„๋’€์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ์‚ฌ๋žŒ์ด โ€œ์ด๊ฑฐ ์™œ ์ด๋ ‡๊ฒŒ ์งฐ์ง€?โ€ ํ•  ๋•Œ ์ฝ”๋“œ๊ฐ€ ์Šค์Šค๋กœ ๋Œ€๋‹ตํ•  ์ˆ˜ ์žˆ๋„๋ก์š”.

์ฐฌํฌ๋‹˜ PoC๊ฐ€ โ€œ์˜ฎ๊ธธ ์ˆ˜ ์žˆ๋‹คโ€์˜ ์ง€๋„๋ฅผ ๊ทธ๋ ค์คฌ๋‹ค๋ฉด, ์ด ๊ธ€์€ ๊ทธ ์ง€๋„๋ฅผ ๋“ค๊ณ  ์‹ค์ œ๋กœ ๊ฑธ์–ด๋ณด๋ฉฐ ๋งŒ๋‚œ ์›…๋ฉ์ด๋“ค์˜ ๊ธฐ๋ก์ž…๋‹ˆ๋‹ค. 4KยทAES-128์„ MediaConvert์— ๋‚จ๊ธด ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒฐ๋ก ์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€๋˜๊ณ  ์žˆ๊ณ , ์ด๋ฏธ์ง€ยท์ผ๋ฐ˜ VOD ๋ผ์ธ์€ ์ด์ œ Cloudflare ์œ„์—์„œ ๋Œ์•„๊ฐ€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธด ๊ธ€ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ

Art Changes Life

๋…ธ๋จธ์Šค์™€ ํ•จ๊ป˜ ์—”ํ„ฐํ…Œํฌ ์‚ฐ์—…์„ ํ˜์‹ ํ•ด๋‚˜๊ฐˆ ๋ฉค๋ฒ„๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.

์ฑ„์šฉ ์ค‘์ธ ๊ณต๊ณ  ๋ณด๊ธฐ