์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ Lumen์œผ๋กœ ๊ฐ€๋ณ๊ฒŒ ๋‚ ์•„์˜ค๋ฅด๋Š” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ ๐Ÿš€

๐Ÿš€ Lumen์œผ๋กœ ๊ฐ€๋ณ๊ฒŒ ๋‚ ์•„์˜ค๋ฅด๋Š” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ ๐Ÿš€

์•ˆ๋…•ํ•˜์„ธ์š” ๊ฐœ๋ฐœ์ž ์—ฌ๋Ÿฌ๋ถ„! ์˜ค๋Š˜์€ PHP ๊ฒฝ๋Ÿ‰ ํ”„๋ ˆ์ž„์›Œํฌ์˜ ์ˆจ์€ ๋ณด์„ ๐Ÿ’Ž Lumen์„ ํ™œ์šฉํ•ด์„œ ์ดˆ๊ฒฝ๋Ÿ‰ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ํ•จ๊ป˜ ์•Œ์•„๋ณผ๊ฒŒ์š”! 2025๋…„ ํ˜„์žฌ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋Š” ๋”์ด์ƒ ์„ ํƒ์ด ์•„๋‹Œ ํ•„์ˆ˜๊ฐ€ ๋˜์—ˆ์ฃ . ๊ฐ€๋ฒผ์šฐ๋ฉด์„œ๋„ ๊ฐ•๋ ฅํ•œ Lumen์œผ๋กœ ์—ฌ๋Ÿฌ๋ถ„์˜ ์„œ๋น„์Šค๋ฅผ ๋‚ ๋ ตํ•˜๊ฒŒ ๋งŒ๋“ค์–ด๋ด์š”! ๐Ÿ˜‰

๐Ÿ“‘ ๋ชฉ์ฐจ

  1. Lumen๊ณผ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ์†Œ๊ฐœ
  2. Lumen ์„ค์น˜ ๋ฐ ํ™˜๊ฒฝ ๊ตฌ์„ฑํ•˜๊ธฐ
  3. ์ฒซ ๋ฒˆ์งธ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๋งŒ๋“ค๊ธฐ
  4. ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹  ๊ตฌํ˜„ํ•˜๊ธฐ
  5. API ๊ฒŒ์ดํŠธ์›จ์ด ๊ตฌ์ถ•ํ•˜๊ธฐ
  6. ์ธ์ฆ ๋ฐ ๋ณด์•ˆ ๊ตฌํ˜„ํ•˜๊ธฐ
  7. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๋™ ๋ฐ ์ตœ์ ํ™”
  8. ์„œ๋น„์Šค ๋ฐฐํฌ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง
  9. ์‹ค์ œ ํ”„๋กœ์ ํŠธ ์‚ฌ๋ก€ ๋ถ„์„
  10. ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์˜ ๋ฏธ๋ž˜

1. Lumen๊ณผ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ์†Œ๊ฐœ ๐ŸŒŸ

์š”์ฆ˜ ๊ฐœ๋ฐœ ํŠธ๋ Œ๋“œ ๋ณด๋ฉด ๋‹ค๋“ค ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ์–˜๊ธฐ๋งŒ ํ•˜๋Š” ๊ฑฐ ๊ฐ™์ฃ ? ใ…‹ใ…‹ใ…‹ ๊ทผ๋ฐ ์ง„์งœ ๋Œ€์„ธ๊ฐ€ ๋œ ์ด์œ ๊ฐ€ ์žˆ์–ด์š”! ํŠนํžˆ PHP ์ƒํƒœ๊ณ„์—์„œ๋Š” Laravel์˜ ๋™์ƒ๋ป˜์ธ Lumen์ด ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ตฌํ˜„์˜ ์™„๋ฒฝํ•œ ํŒŒํŠธ๋„ˆ๋กœ ์ž๋ฆฌ์žก๊ณ  ์žˆ๋‹ต๋‹ˆ๋‹ค.

๐Ÿค” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๊ฐ€ ๋ญ๊ธธ๋ž˜?

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

Lumen์ด ๋ญ๊ธธ๋ž˜ ์ด๋ ‡๊ฒŒ ํ•ซํ•œ๊ฑฐ์ฃ ? ๐Ÿ”ฅ

Lumen์€ Laravel์˜ ์ฐฝ์‹œ์ž Taylor Otwell์ด ๋งŒ๋“  ์ดˆ๊ฒฝ๋Ÿ‰ PHP ๋งˆ์ดํฌ๋กœ ํ”„๋ ˆ์ž„์›Œํฌ์˜ˆ์š”. Laravel์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ๋ถˆํ•„์š”ํ•œ ๋ถ€๋ถ„์„ ๊ณผ๊ฐํžˆ ์ œ๊ฑฐํ•ด์„œ ์†๋„์™€ ํšจ์œจ์„ฑ์„ ๊ทน๋Œ€ํ™”ํ–ˆ์–ด์š”. 2025๋…„ ํ˜„์žฌ Lumen 10.x ๋ฒ„์ „๊นŒ์ง€ ๋‚˜์™”๋Š”๋ฐ, ์„ฑ๋Šฅ์ด ๋ฏธ์ณค๋‹ค๋‹ˆ๊นŒ์š”? ๐Ÿ˜ฒ

๋ชจ๋†€๋ฆฌ์‹ ์•„ํ‚คํ…์ฒ˜ UI ๋ ˆ์ด์–ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค ๋‹จ์ผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ์‚ฌ์šฉ์ž ์„œ๋น„์Šค Lumen ๊ฒฐ์ œ ์„œ๋น„์Šค Lumen ์ƒํ’ˆ ์„œ๋น„์Šค Lumen ์•Œ๋ฆผ ์„œ๋น„์Šค Lumen API ๊ฒŒ์ดํŠธ์›จ์ด DB DB DB DB ์ง„ํ™”

Lumen vs Laravel: ๋ญ๊ฐ€ ๋‹ค๋ฅธ๋ฐ? ๐Ÿคทโ€โ™‚๏ธ

Laravel์ด ํ’€์Šคํƒ ํ”„๋ ˆ์ž„์›Œํฌ๋ผ๋ฉด, Lumen์€ API ๊ฐœ๋ฐœ์— ํŠนํ™”๋œ ๊ฒฝ๋Ÿ‰ ๋ฒ„์ „์ด์—์š”. Laravel์˜ ๋ฌด๊ฑฐ์šด ๊ธฐ๋Šฅ๋“ค(์„ธ์…˜, ๋ทฐ, ํ…œํ”Œ๋ฆฟ ๋“ฑ)์„ ๊ณผ๊ฐํžˆ ์ œ๊ฑฐํ•ด์„œ ์†๋„๋ฅผ ๊ทน๋Œ€ํ™”ํ–ˆ์ฃ . ์‹ค์ œ๋กœ ๋ฒค์น˜๋งˆํฌ ํ…Œ์ŠคํŠธ์—์„œ Lumen์€ Laravel๋ณด๋‹ค ์ตœ๋Œ€ 2๋ฐฐ ๋น ๋ฅธ ์„ฑ๋Šฅ์„ ๋ณด์—ฌ์ค€๋‹ต๋‹ˆ๋‹ค! ๐ŸŽ๏ธ๐Ÿ’จ

ํŠน์ง• Laravel Lumen
์šฉ๋„ ํ’€์Šคํƒ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค, API
ํŒŒ์ผ ํฌ๊ธฐ ๋ฌด๊ฑฐ์›€ (์•ฝ 30MB+) ๊ฐ€๋ฒผ์›€ (์•ฝ 1MB)
์†๋„ ๋ณดํ†ต ๋งค์šฐ ๋น ๋ฆ„
์„ค์ • ํŒŒ์ผ ๋งŽ์Œ ์ตœ์†Œํ™”
ํ…œํ”Œ๋ฆฟ ์—”์ง„ Blade ๋‚ด์žฅ ๊ธฐ๋ณธ ์ œ๊ณต ์•ˆ ํ•จ

์ด์ œ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์„ ๊ฐœ๋ฐœํ•  ๋•Œ๋„ ๋ชจ๋†€๋ฆฌ์‹๋ณด๋‹ค๋Š” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๊ฐ€ ๋Œ€์„ธ์ธ ์ด์œ ๋ฅผ ์•„์‹œ๊ฒ ์ฃ ? ๐Ÿ˜Ž ๊ฐ ๊ธฐ๋Šฅ๋ณ„๋กœ ๋…๋ฆฝ์ ์ธ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๋ฉด ์œ ์ง€๋ณด์ˆ˜๋„ ์‰ฝ๊ณ  ํ™•์žฅ์„ฑ๋„ ์ข‹์•„์ง€๋‹ˆ๊นŒ์š”!

2. Lumen ์„ค์น˜ ๋ฐ ํ™˜๊ฒฝ ๊ตฌ์„ฑํ•˜๊ธฐ ๐Ÿ› ๏ธ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ Lumen์„ ์„ค์น˜ํ•˜๊ณ  ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ๊ตฌ์„ฑํ•ด๋ณผ๊ฒŒ์š”! ์ง„์งœ ์‰ฌ์›Œ์„œ ๋†€๋ž„๊ฑธ์š”? ใ…‹ใ…‹ใ…‹

ํ•„์ˆ˜ ์š”๊ตฌ์‚ฌํ•ญ ์ฒดํฌํ•˜๊ธฐ โœ…

Lumen 10.x(2025๋…„ ์ตœ์‹  ๋ฒ„์ „)๋ฅผ ์„ค์น˜ํ•˜๊ธฐ ์œ„ํ•œ ์š”๊ตฌ์‚ฌํ•ญ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์•„์š”:

  1. PHP >= 8.2
  2. OpenSSL PHP Extension
  3. PDO PHP Extension
  4. Mbstring PHP Extension
  5. Composer (์˜์กด์„ฑ ๊ด€๋ฆฌ ๋„๊ตฌ)

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ

2025๋…„ ํ˜„์žฌ PHP 8.3์ด ์•ˆ์ •ํ™”๋˜์—ˆ์ง€๋งŒ, ์ผ๋ถ€ ๋ ˆ๊ฑฐ์‹œ ์‹œ์Šคํ…œ์—์„œ๋Š” ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์–ด์š”. ํ”„๋กœ์ ํŠธ ์‹œ์ž‘ ์ „ ๋ฐ˜๋“œ์‹œ PHP ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ์„ ํ™•์ธํ•˜์„ธ์š”!

Composer๋กœ Lumen ์„ค์น˜ํ•˜๊ธฐ ๐Ÿš€

Composer๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ •๋ง ๊ฐ„๋‹จํ•˜๊ฒŒ Lumen์„ ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์–ด์š”:

composer create-project --prefer-dist laravel/lumen microservice-project

์ด ๋ช…๋ น์–ด ํ•œ ์ค„๋กœ 'microservice-project'๋ผ๋Š” ์ด๋ฆ„์˜ Lumen ํ”„๋กœ์ ํŠธ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค! ์งฑ ๊ฐ„๋‹จํ•˜์ฃ ? ๐Ÿ˜

ํ™˜๊ฒฝ ์„ค์ •ํ•˜๊ธฐ โš™๏ธ

ํ”„๋กœ์ ํŠธ๊ฐ€ ์ƒ์„ฑ๋˜๋ฉด .env ํŒŒ์ผ์„ ์„ค์ •ํ•ด์•ผ ํ•ด์š”. ๊ธฐ๋ณธ์ ์œผ๋กœ .env.example ํŒŒ์ผ์ด ์ œ๊ณต๋˜๋‹ˆ ์ด๋ฅผ ๋ณต์‚ฌํ•ด์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋ผ์š”:

cp .env.example .env

๊ทธ๋ฆฌ๊ณ  .env ํŒŒ์ผ์„ ์—ด์–ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ •๋ณด ๋“ฑ์„ ์„ค์ •ํ•ด์ฃผ์„ธ์š”:

APP_NAME=MicroserviceAPI
APP_ENV=local
APP_KEY=base64:๋žœ๋คํ‚ค
APP_DEBUG=true
APP_URL=http://localhost
APP_TIMEZONE=UTC

LOG_CHANNEL=stack
LOG_SLACK_WEBHOOK_URL=

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=microservice_db
DB_USERNAME=root
DB_PASSWORD=password

CACHE_DRIVER=file
QUEUE_CONNECTION=sync

APP_KEY๋Š” ๋ณด์•ˆ์„ ์œ„ํ•ด ๋ฐ˜๋“œ์‹œ ์„ค์ •ํ•ด์•ผ ํ•ด์š”! ๋‹ค์Œ ๋ช…๋ น์–ด๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์–ด์š”:

php -r "echo base64_encode(random_bytes(32));"

๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ ์‚ดํŽด๋ณด๊ธฐ ๐Ÿ“

Lumen์˜ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๋Š” Laravel๋ณด๋‹ค ํ›จ์”ฌ ๋‹จ์ˆœํ•ด์š”. ์ฃผ์š” ๋””๋ ‰ํ† ๋ฆฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•„์š”:

Lumen ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ microservice-project/ app/ Http/ Controllers/ Middleware/ Models/ Services/ bootstrap/ app.php database/ migrations/ factories/ seeders/ public/ index.php routes/ web.php storage/ ์„ค์ • ํŒŒ์ผ๋“ค .env composer.json artisan app/: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ•ต์‹ฌ ์ฝ”๋“œ bootstrap/: ํ”„๋ ˆ์ž„์›Œํฌ ๋ถ€ํŒ… ์ฝ”๋“œ database/: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ จ ํŒŒ์ผ public/: ์›น์„œ๋ฒ„ ์ง„์ž…์  routes/: ๋ผ์šฐํŒ… ์ •์˜ storage/: ๋กœ๊ทธ, ์บ์‹œ ๋“ฑ ์ €์žฅ์†Œ .env: ํ™˜๊ฒฝ ์„ค์ • composer.json: ์˜์กด์„ฑ ์ •์˜ artisan: ๋ช…๋ น์ค„ ์ธํ„ฐํŽ˜์ด์Šค

Laravel์— ๋น„ํ•ด ์ •๋ง ์‹ฌํ”Œํ•˜์ฃ ? ์ด๊ฒŒ ๋ฐ”๋กœ Lumen์˜ ๋งค๋ ฅ์ด์—์š”! ํ•„์š”ํ•œ ๊ฒƒ๋งŒ ๋‚จ๊ธฐ๊ณ  ๋‹ค ๋œ์–ด๋ƒˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€๋ณ๊ณ  ๋น ๋ฅธ ๊ฑฐ์˜ˆ์š”. ๐Ÿ˜Š

์„œ๋ฒ„ ์‹คํ–‰ํ•˜๊ธฐ ๐Ÿƒโ€โ™‚๏ธ

๊ฐœ๋ฐœ ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”:

cd microservice-project
php -S localhost:8000 -t public

์ด์ œ ๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:8000์œผ๋กœ ์ ‘์†ํ•˜๋ฉด Lumen์ด ์‹คํ–‰๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š”! ์งœ์ž”~ ๐ŸŽ‰

3. ์ฒซ ๋ฒˆ์งธ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๋งŒ๋“ค๊ธฐ ๐Ÿ”จ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์ฒซ ๋ฒˆ์งธ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”! ์˜ˆ์‹œ๋กœ ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋ฅผ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ์š”. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ๋Š” ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ด๋‹ˆ๊นŒ์š”! ๐Ÿ˜‰

๋ผ์šฐํŒ… ์„ค์ •ํ•˜๊ธฐ ๐Ÿ›ฃ๏ธ

๋จผ์ € routes/web.php ํŒŒ์ผ์„ ์—ด๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ผ์šฐํŒ…์„ ์„ค์ •ํ•ด๋ณผ๊ฒŒ์š”:


$router->group(['prefix' => 'api/v1'], function () use ($router) {
    // ์‚ฌ์šฉ์ž ๊ด€๋ จ ์—”๋“œํฌ์ธํŠธ
    $router->get('users', 'UserController@index');
    $router->get('users/{id}', 'UserController@show');
    $router->post('users', 'UserController@store');
    $router->put('users/{id}', 'UserController@update');
    $router->delete('users/{id}', 'UserController@destroy');
});
    

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด API ๋ฒ„์ „ ๊ด€๋ฆฌ๋„ ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ์–ด์š”. 'api/v1'์ด๋ผ๋Š” ์ ‘๋‘์‚ฌ๋ฅผ ๋ถ™์—ฌ์„œ ๋‚˜์ค‘์— v2๊ฐ€ ํ•„์š”ํ•˜๋ฉด ์‰ฝ๊ฒŒ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์ฃ ! ํ˜„์—…์—์„œ ์ง„์งœ ๋งŽ์ด ์“ฐ๋Š” ํŒจํ„ด์ด์—์š” ใ…‹ใ…‹ใ…‹

๋ชจ๋ธ ๋งŒ๋“ค๊ธฐ ๐Ÿ“Š

์ด์ œ User ๋ชจ๋ธ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/Models ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์—†๋‹ค๋ฉด ๋จผ์ € ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”:

mkdir -p app/Models

๊ทธ๋ฆฌ๊ณ  app/Models/User.php ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”:


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password', 'skills', 'bio'
    ];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = [
        'password',
    ];
}
    

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋Š” ์‚ฌ์šฉ์ž์˜ ์Šคํ‚ฌ์ด๋‚˜ ์ž๊ธฐ์†Œ๊ฐœ ๊ฐ™์€ ์ •๋ณด๋„ ์ค‘์š”ํ•˜๋‹ˆ๊นŒ 'skills'์™€ 'bio' ํ•„๋“œ๋„ ์ถ”๊ฐ€ํ–ˆ์–ด์š”! ๐Ÿ˜Š

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋งŒ๋“ค๊ธฐ ๐Ÿ—ƒ๏ธ

์ด์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด๋ณผ๊ฒŒ์š”:

php artisan make:migration create_users_table

์•„... ๊ทผ๋ฐ ์ž ๊น! Lumen์—์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ artisan ๋ช…๋ น์–ด์˜ ์ผ๋ถ€๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์–ด์š”. ๐Ÿ˜… ์ง์ ‘ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์•ผ ํ•  ์ˆ˜๋„ ์žˆ์–ด์š”. database/migrations ๋””๋ ‰ํ† ๋ฆฌ์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”:


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->text('skills')->nullable();
            $table->text('bio')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}
    

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์‹คํ–‰ํ•˜๋ ค๋ฉด ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”:

php artisan migrate

๋งŒ์•ฝ artisan ๋ช…๋ น์–ด๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด, bootstrap/app.php ํŒŒ์ผ์—์„œ ๋‹ค์Œ ์ค„์˜ ์ฃผ์„์„ ํ•ด์ œํ•ด์ฃผ์„ธ์š”:

// $app->register(App\Providers\AppServiceProvider::class);

์ปจํŠธ๋กค๋Ÿฌ ๋งŒ๋“ค๊ธฐ ๐ŸŽฎ

์ด์ œ UserController๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/Http/Controllers ๋””๋ ‰ํ† ๋ฆฌ์— UserController.php ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”:


<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class UserController extends Controller
{
    /**
     * ๋ชจ๋“  ์‚ฌ์šฉ์ž ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function index()
    {
        return response()->json(User::all());
    }

    /**
     * ์ƒˆ ์‚ฌ์šฉ์ž๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function store(Request $request)
    {
        $this->validate($request, [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:6',
            'skills' => 'nullable|string',
            'bio' => 'nullable|string'
        ]);

        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
            'skills' => $request->skills,
            'bio' => $request->bio
        ]);

        return response()->json($user, 201);
    }

    /**
     * ํŠน์ • ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  int  $id
     * @return \Illuminate\Http\JsonResponse
     */
    public function show($id)
    {
        $user = User::findOrFail($id);
        return response()->json($user);
    }

    /**
     * ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\JsonResponse
     */
    public function update(Request $request, $id)
    {
        $this->validate($request, [
            'name' => 'string|max:255',
            'email' => 'email|unique:users,email,'.$id,
            'password' => 'min:6',
            'skills' => 'nullable|string',
            'bio' => 'nullable|string'
        ]);

        $user = User::findOrFail($id);
        
        if ($request->has('name')) {
            $user->name = $request->name;
        }
        
        if ($request->has('email')) {
            $user->email = $request->email;
        }
        
        if ($request->has('password')) {
            $user->password = Hash::make($request->password);
        }
        
        if ($request->has('skills')) {
            $user->skills = $request->skills;
        }
        
        if ($request->has('bio')) {
            $user->bio = $request->bio;
        }
        
        $user->save();
        
        return response()->json($user);
    }

    /**
     * ์‚ฌ์šฉ์ž๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  int  $id
     * @return \Illuminate\Http\JsonResponse
     */
    public function destroy($id)
    {
        User::findOrFail($id)->delete();
        return response()->json(['message' => '์‚ฌ์šฉ์ž๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'], 200);
    }
}
    

์™€์šฐ! ์ด์ œ ๊ธฐ๋ณธ์ ์ธ CRUD ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ ์‚ฌ์šฉ์ž ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๊ฐ€ ์™„์„ฑ๋์–ด์š”! ๐ŸŽ‰ ์ด๋ ‡๊ฒŒ ๊ฐ„๋‹จํ•˜๊ฒŒ RESTful API๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค๋‹ˆ, Lumen ์ง„์งœ ๋Œ€๋‹จํ•˜์ง€ ์•Š๋‚˜์š”?

API ํ…Œ์ŠคํŠธํ•˜๊ธฐ ๐Ÿงช

์ด์ œ Postman์ด๋‚˜ cURL์„ ์‚ฌ์šฉํ•ด์„œ API๋ฅผ ํ…Œ์ŠคํŠธํ•ด๋ณผ ์ˆ˜ ์žˆ์–ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ƒˆ ์‚ฌ์šฉ์ž๋ฅผ ์ƒ์„ฑํ•˜๋Š” API๋ฅผ ํ…Œ์ŠคํŠธํ•ด๋ณผ๊ฒŒ์š”:


curl -X POST http://localhost:8000/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ํ™๊ธธ๋™",
    "email": "hong@example.com",
    "password": "password123",
    "skills": "PHP, Laravel, Lumen, MySQL",
    "bio": "์•ˆ๋…•ํ•˜์„ธ์š”! ์ €๋Š” ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž์ž…๋‹ˆ๋‹ค."
  }'
    

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ƒˆ ์‚ฌ์šฉ์ž๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  JSON ์‘๋‹ต์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์š”! ๋‹ค๋ฅธ ์—”๋“œํฌ์ธํŠธ๋„ ๋น„์Šทํ•˜๊ฒŒ ํ…Œ์ŠคํŠธํ•ด๋ณผ ์ˆ˜ ์žˆ์–ด์š”. ๐Ÿ˜Š

๐Ÿ’ก ํ”„๋กœ ํŒ!

์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” API ์‘๋‹ต ํ˜•์‹์„ ์ผ๊ด€๋˜๊ฒŒ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด์š”. ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜•์‹์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„์š”:

{
  "success": true,
  "data": { ... },
  "message": "์ž‘์—…์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
  "errors": []
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์‘๋‹ต์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ๊ฐ€ ํ›จ์”ฌ ์‰ฌ์›Œ์ ธ์š”!

4. ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹  ๊ตฌํ˜„ํ•˜๊ธฐ ๐Ÿ“ก

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„ ์ค‘ ํ•˜๋‚˜๋Š” ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์ด์—์š”. ์—ฌ๋Ÿฌ ์„œ๋น„์Šค๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋ฉด์„œ๋„ ํ•„์š”ํ•  ๋•Œ ์„œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๋‹ˆ๊นŒ์š”! ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ์•Œ์•„๋ณผ๊นŒ์š”? ๐Ÿค”

HTTP ํ†ต์‹  ๊ตฌํ˜„ํ•˜๊ธฐ ๐ŸŒ

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ๋ฐฉ๋ฒ•์€ HTTP ์š”์ฒญ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์˜ˆ์š”. Lumen์—์„œ๋Š” Guzzle HTTP ํด๋ผ์ด์–ธํŠธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. ๋จผ์ € Guzzle์„ ์„ค์น˜ํ•ด๋ณผ๊ฒŒ์š”:

composer require guzzlehttp/guzzle

์ด์ œ ์„œ๋น„์Šค ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋‹ค๋ฅธ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์™€ ํ†ต์‹ ํ•ด๋ณผ๊ฒŒ์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, ๊ฒฐ์ œ ์„œ๋น„์Šค์™€ ํ†ต์‹ ํ•˜๋Š” PaymentService ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


<?php

namespace App\Services;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

class PaymentService
{
    protected $client;
    protected $baseUrl;

    public function __construct()
    {
        $this->client = new Client();
        $this->baseUrl = env('PAYMENT_SERVICE_URL', 'http://payment-service:8001/api/v1');
    }

    /**
     * ๊ฒฐ์ œ ์ฒ˜๋ฆฌ๋ฅผ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param int $userId
     * @param float $amount
     * @param string $description
     * @return array
     */
    public function processPayment($userId, $amount, $description)
    {
        try {
            $response = $this->client->post($this->baseUrl . '/payments', [
                'json' => [
                    'user_id' => $userId,
                    'amount' => $amount,
                    'description' => $description
                ]
            ]);

            return json_decode($response->getBody(), true);
        } catch (RequestException $e) {
            // ์—๋Ÿฌ ์ฒ˜๋ฆฌ
            return [
                'success' => false,
                'message' => $e->getMessage()
            ];
        }
    }

    /**
     * ์‚ฌ์šฉ์ž์˜ ๊ฒฐ์ œ ๋‚ด์—ญ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param int $userId
     * @return array
     */
    public function getUserPayments($userId)
    {
        try {
            $response = $this->client->get($this->baseUrl . '/users/' . $userId . '/payments');
            return json_decode($response->getBody(), true);
        } catch (RequestException $e) {
            // ์—๋Ÿฌ ์ฒ˜๋ฆฌ
            return [
                'success' => false,
                'message' => $e->getMessage()
            ];
        }
    }
}
    

์ด์ œ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ด ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”:


<?php

namespace App\Http\Controllers;

use App\Services\PaymentService;
use Illuminate\Http\Request;

class PaymentController extends Controller
{
    protected $paymentService;

    public function __construct(PaymentService $paymentService)
    {
        $this->paymentService = $paymentService;
    }

    /**
     * ๊ฒฐ์ œ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function processPayment(Request $request)
    {
        $this->validate($request, [
            'user_id' => 'required|integer',
            'amount' => 'required|numeric|min:0',
            'description' => 'required|string'
        ]);

        $result = $this->paymentService->processPayment(
            $request->user_id,
            $request->amount,
            $request->description
        );

        return response()->json($result);
    }

    /**
     * ์‚ฌ์šฉ์ž์˜ ๊ฒฐ์ œ ๋‚ด์—ญ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  int  $userId
     * @return \Illuminate\Http\JsonResponse
     */
    public function getUserPayments($userId)
    {
        $payments = $this->paymentService->getUserPayments($userId);
        return response()->json($payments);
    }
}
    

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด HTTP๋ฅผ ํ†ตํ•ด ๋‹ค๋ฅธ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์™€ ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ์–ด์š”! ๊ทผ๋ฐ HTTP ํ†ต์‹ ์€ ๋™๊ธฐ์‹์ด๋ผ ์„ฑ๋Šฅ ์ด์Šˆ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์–ด์š”. ๊ทธ๋ž˜์„œ ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฉ”์‹œ์ง€ ํ๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•ด์š”. ๐Ÿ˜‰

๋ฉ”์‹œ์ง€ ํ๋ฅผ ์‚ฌ์šฉํ•œ ๋น„๋™๊ธฐ ํ†ต์‹  ๐Ÿ“จ

๋ฉ”์‹œ์ง€ ํ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์„œ๋น„์Šค ๊ฐ„ ๋น„๋™๊ธฐ ํ†ต์‹ ์ด ๊ฐ€๋Šฅํ•ด์ ธ์š”. RabbitMQ๋‚˜ Apache Kafka ๊ฐ™์€ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. ์—ฌ๊ธฐ์„œ๋Š” Laravel/Lumen์˜ ๊ธฐ๋ณธ ํ ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•ด๋ณผ๊ฒŒ์š”.

๋จผ์ € .env ํŒŒ์ผ์—์„œ ํ ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์„ค์ •ํ•ด์ฃผ์„ธ์š”:

QUEUE_CONNECTION=database

๊ทธ๋ฆฌ๊ณ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— jobs ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ค์–ด์•ผ ํ•ด์š”:

php artisan queue:table
php artisan migrate

์ด์ œ Job ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, ๊ฒฐ์ œ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์•Œ๋ฆผ์„ ๋ณด๋‚ด๋Š” ์ž‘์—…์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


<?php

namespace App\Jobs;

use App\Services\NotificationService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class SendPaymentNotification implements ShouldQueue
{
    use InteractsWithQueue, Queueable, SerializesModels;

    protected $userId;
    protected $paymentId;
    protected $amount;

    /**
     * Create a new job instance.
     *
     * @param int $userId
     * @param int $paymentId
     * @param float $amount
     * @return void
     */
    public function __construct($userId, $paymentId, $amount)
    {
        $this->userId = $userId;
        $this->paymentId = $paymentId;
        $this->amount = $amount;
    }

    /**
     * Execute the job.
     *
     * @param NotificationService $notificationService
     * @return void
     */
    public function handle(NotificationService $notificationService)
    {
        $notificationService->sendPaymentNotification(
            $this->userId,
            $this->paymentId,
            $this->amount
        );
    }
}
    

์ด์ œ ๊ฒฐ์ œ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์ด ์ž‘์—…์„ ํ์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด์š”:


// PaymentController.php์˜ processPayment ๋ฉ”์†Œ๋“œ ๋‚ด๋ถ€
$result = $this->paymentService->processPayment(
    $request->user_id,
    $request->amount,
    $request->description
);

if ($result['success']) {
    dispatch(new SendPaymentNotification(
        $request->user_id,
        $result['data']['id'],
        $request->amount
    ));
}

return response()->json($result);
    

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ฒฐ์ œ ์ฒ˜๋ฆฌ์™€ ์•Œ๋ฆผ ์ „์†ก์ด ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ด๋ฃจ์–ด์ ธ์š”! ์‚ฌ์šฉ์ž๋Š” ๊ฒฐ์ œ ์™„๋ฃŒ ์‘๋‹ต์„ ๋ฐ”๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ณ , ์•Œ๋ฆผ์€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ฒ˜๋ฆฌ๋˜์ฃ . ๐Ÿ‘

์‚ฌ์šฉ์ž ์„œ๋น„์Šค Lumen ๊ฒฐ์ œ ์„œ๋น„์Šค Lumen ์•Œ๋ฆผ ์„œ๋น„์Šค Lumen ๋ฉ”์‹œ์ง€ ํ RabbitMQ/Kafka HTTP ์š”์ฒญ HTTP ์‘๋‹ต ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ ๋ฉ”์‹œ์ง€ ๊ตฌ๋… ์ฒ˜๋ฆฌ

์„œ๋น„์Šค ๋””์Šค์ปค๋ฒ„๋ฆฌ ๊ตฌํ˜„ํ•˜๊ธฐ ๐Ÿ”

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๊ฐ€ ๋งŽ์•„์ง€๋ฉด ๊ฐ ์„œ๋น„์Šค์˜ ์œ„์น˜(URL)๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๋ณต์žกํ•ด์ ธ์š”. ์ด๋•Œ ์„œ๋น„์Šค ๋””์Šค์ปค๋ฒ„๋ฆฌ ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋ฉด ์ข‹์•„์š”. Consul์ด๋‚˜ etcd ๊ฐ™์€ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”.

ํ•˜์ง€๋งŒ ๊ฐ„๋‹จํ•œ ํ”„๋กœ์ ํŠธ๋ผ๋ฉด .env ํŒŒ์ผ์— ๊ฐ ์„œ๋น„์Šค์˜ URL์„ ์ •์˜ํ•˜๊ณ , ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•ด ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ๊ดœ์ฐฎ์•„์š”:


# .env ํŒŒ์ผ
USER_SERVICE_URL=http://user-service:8000/api/v1
PAYMENT_SERVICE_URL=http://payment-service:8001/api/v1
NOTIFICATION_SERVICE_URL=http://notification-service:8002/api/v1
    

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์„œ๋น„์Šค URL์ด ๋ณ€๊ฒฝ๋˜์–ด๋„ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•  ํ•„์š” ์—†์ด ํ™˜๊ฒฝ ์„ค์ •๋งŒ ๋ณ€๊ฒฝํ•˜๋ฉด ๋ผ์š”! ๐Ÿ‘

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์—์„œ๋Š” ์žฅ์•  ํ—ˆ์šฉ(Fault Tolerance)์„ ๊ณ ๋ คํ•ด์•ผ ํ•ด์š”. ์„œํ‚ท ๋ธŒ๋ ˆ์ด์ปค ํŒจํ„ด์„ ๊ตฌํ˜„ํ•˜์—ฌ ํ•œ ์„œ๋น„์Šค์˜ ์žฅ์• ๊ฐ€ ์ „์ฒด ์‹œ์Šคํ…œ์— ์˜ํ–ฅ์„ ๋ฏธ์น˜์ง€ ์•Š๋„๋ก ํ•ด์•ผ ํ•ด์š”. PHP์—์„œ๋Š” Ganesha ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”.

5. API ๊ฒŒ์ดํŠธ์›จ์ด ๊ตฌ์ถ•ํ•˜๊ธฐ ๐Ÿšช

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์—์„œ API ๊ฒŒ์ดํŠธ์›จ์ด๋Š” ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ์˜ ๋‹จ์ผ ์ง„์ž…์  ์—ญํ• ์„ ํ•ด์š”. ํด๋ผ์ด์–ธํŠธ๋Š” ์—ฌ๋Ÿฌ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์— ์ง์ ‘ ์ ‘๊ทผํ•˜๋Š” ๋Œ€์‹  ๊ฒŒ์ดํŠธ์›จ์ด๋ฅผ ํ†ตํ•ด ํ†ต์‹ ํ•˜์ฃ . ์ด๊ฒŒ ์™œ ์ค‘์š”ํ• ๊นŒ์š”? ๐Ÿค”

๐ŸŒŸ API ๊ฒŒ์ดํŠธ์›จ์ด์˜ ์žฅ์ 

  1. ํด๋ผ์ด์–ธํŠธ๋Š” ๋‹จ์ผ ์—”๋“œํฌ์ธํŠธ๋งŒ ์•Œ๋ฉด ๋จ
  2. ์ธ์ฆ/์ธ๊ฐ€๋ฅผ ์ค‘์•™์—์„œ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
  3. ์š”์ฒญ/์‘๋‹ต ๋ณ€ํ™˜ ๋ฐ ์ง‘๊ณ„ ๊ฐ€๋Šฅ
  4. ์†๋„ ์ œํ•œ, ์บ์‹ฑ ๋“ฑ ๊ณตํ†ต ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ฐ€๋Šฅ
  5. ์„œ๋น„์Šค ๋””์Šค์ปค๋ฒ„๋ฆฌ ํ†ตํ•ฉ ๊ฐ€๋Šฅ

Lumen์œผ๋กœ API ๊ฒŒ์ดํŠธ์›จ์ด ๊ตฌํ˜„ํ•˜๊ธฐ ๐Ÿ› ๏ธ

Lumen์€ ๊ฒฝ๋Ÿ‰์ด๋ฉด์„œ๋„ ๊ฐ•๋ ฅํ•ด์„œ API ๊ฒŒ์ดํŠธ์›จ์ด๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์ข‹์•„์š”! ์ƒˆ๋กœ์šด Lumen ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•ด๋ณผ๊ฒŒ์š”:

composer create-project --prefer-dist laravel/lumen api-gateway

์ด์ œ ๋ผ์šฐํŒ…์„ ์„ค์ •ํ•ด๋ณผ๊ฒŒ์š”. routes/web.php ํŒŒ์ผ์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•ด์ฃผ์„ธ์š”:


<?php

$router->group(['prefix' => 'api/v1'], function () use ($router) {
    // ์‚ฌ์šฉ์ž ์„œ๋น„์Šค๋กœ ๋ผ์šฐํŒ…
    $router->group(['prefix' => 'users'], function () use ($router) {
        $router->get('/', 'GatewayController@forwardUserRequest');
        $router->get('/{id}', 'GatewayController@forwardUserRequest');
        $router->post('/', 'GatewayController@forwardUserRequest');
        $router->put('/{id}', 'GatewayController@forwardUserRequest');
        $router->delete('/{id}', 'GatewayController@forwardUserRequest');
    });

    // ๊ฒฐ์ œ ์„œ๋น„์Šค๋กœ ๋ผ์šฐํŒ…
    $router->group(['prefix' => 'payments'], function () use ($router) {
        $router->get('/', 'GatewayController@forwardPaymentRequest');
        $router->post('/', 'GatewayController@forwardPaymentRequest');
        $router->get('/{id}', 'GatewayController@forwardPaymentRequest');
    });

    // ์•Œ๋ฆผ ์„œ๋น„์Šค๋กœ ๋ผ์šฐํŒ…
    $router->group(['prefix' => 'notifications'], function () use ($router) {
        $router->get('/', 'GatewayController@forwardNotificationRequest');
        $router->post('/', 'GatewayController@forwardNotificationRequest');
    });
});
    

์ด์ œ GatewayController๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


<?php

namespace App\Http\Controllers;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Http\Request;
use Laravel\Lumen\Routing\Controller as BaseController;

class GatewayController extends BaseController
{
    protected $client;

    public function __construct()
    {
        $this->client = new Client();
    }

    /**
     * ์‚ฌ์šฉ์ž ์„œ๋น„์Šค๋กœ ์š”์ฒญ์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function forwardUserRequest(Request $request)
    {
        return $this->forwardRequest($request, env('USER_SERVICE_URL', 'http://user-service:8000/api/v1'));
    }

    /**
     * ๊ฒฐ์ œ ์„œ๋น„์Šค๋กœ ์š”์ฒญ์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function forwardPaymentRequest(Request $request)
    {
        return $this->forwardRequest($request, env('PAYMENT_SERVICE_URL', 'http://payment-service:8001/api/v1'));
    }

    /**
     * ์•Œ๋ฆผ ์„œ๋น„์Šค๋กœ ์š”์ฒญ์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function forwardNotificationRequest(Request $request)
    {
        return $this->forwardRequest($request, env('NOTIFICATION_SERVICE_URL', 'http://notification-service:8002/api/v1'));
    }

    /**
     * ์š”์ฒญ์„ ์ง€์ •๋œ ์„œ๋น„์Šค๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  string  $serviceUrl
     * @return \Illuminate\Http\JsonResponse
     */
    protected function forwardRequest(Request $request, $serviceUrl)
    {
        // ์š”์ฒญ ๊ฒฝ๋กœ์—์„œ API ๋ฒ„์ „๊ณผ ์„œ๋น„์Šค ์ ‘๋‘์‚ฌ ์ œ๊ฑฐ
        $path = preg_replace('/^\/api\/v1\/[^\/]+/', '', $request->getPathInfo());
        $url = rtrim($serviceUrl, '/') . $path;

        try {
            // ์›๋ณธ ์š”์ฒญ ํ—ค๋” ๊ฐ€์ ธ์˜ค๊ธฐ
            $headers = $this->getHeadersForForwarding($request);

            // ์š”์ฒญ ๋ฉ”์†Œ๋“œ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ Guzzle ์š”์ฒญ ์ƒ์„ฑ
            $options = [
                'headers' => $headers,
            ];

            // ์š”์ฒญ ๋ณธ๋ฌธ์ด ์žˆ์œผ๋ฉด ์ถ”๊ฐ€
            if (in_array($request->method(), ['POST', 'PUT', 'PATCH']) && $request->getContent()) {
                $options['json'] = $request->json()->all();
            }

            // ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์ถ”๊ฐ€
            if (!empty($request->query())) {
                $options['query'] = $request->query();
            }

            // ์š”์ฒญ ์ „๋‹ฌ
            $response = $this->client->request($request->method(), $url, $options);

            // ์‘๋‹ต ๋ฐ˜ํ™˜
            return response()->json(
                json_decode($response->getBody(), true),
                $response->getStatusCode(),
                $this->getResponseHeaders($response)
            );
        } catch (RequestException $e) {
            // ์—๋Ÿฌ ์ฒ˜๋ฆฌ
            $statusCode = $e->hasResponse() ? $e->getResponse()->getStatusCode() : 500;
            $message = $e->hasResponse() ? json_decode($e->getResponse()->getBody(), true) : ['message' => $e->getMessage()];

            return response()->json($message, $statusCode);
        }
    }

    /**
     * ์ „๋‹ฌํ•  ์š”์ฒญ ํ—ค๋”๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    protected function getHeadersForForwarding(Request $request)
    {
        $headers = [];
        foreach ($request->headers as $key => $value) {
            // ์ผ๋ถ€ ํ—ค๋”๋Š” ์ œ์™ธ
            if (!in_array(strtolower($key), ['host', 'content-length'])) {
                $headers[$key] = $value[0];
            }
        }

        // ์ธ์ฆ ํ† ํฐ์ด ์žˆ์œผ๋ฉด ์ถ”๊ฐ€
        if ($request->bearerToken()) {
            $headers['Authorization'] = 'Bearer ' . $request->bearerToken();
        }

        return $headers;
    }

    /**
     * ์‘๋‹ต ํ—ค๋”๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
     *
     * @param  \GuzzleHttp\Psr7\Response  $response
     * @return array
     */
    protected function getResponseHeaders($response)
    {
        $headers = [];
        foreach ($response->getHeaders() as $key => $value) {
            // ์ผ๋ถ€ ํ—ค๋”๋งŒ ํฌํ•จ
            if (in_array(strtolower($key), ['content-type', 'cache-control'])) {
                $headers[$key] = $value[0];
            }
        }
        return $headers;
    }
}
    

์ด์ œ API ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ ์ค€๋น„๋์–ด์š”! ํด๋ผ์ด์–ธํŠธ๋Š” ๊ฒŒ์ดํŠธ์›จ์ด์˜ ์—”๋“œํฌ์ธํŠธ๋งŒ ์•Œ๋ฉด ๋˜๊ณ , ๊ฒŒ์ดํŠธ์›จ์ด๋Š” ์š”์ฒญ์„ ์ ์ ˆํ•œ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋กœ ์ „๋‹ฌํ•ด์ค˜์š”. ๐Ÿ˜Š

๋ชจ๋ฐ”์ผ ์•ฑ ์›น ์•ฑ ์„œ๋“œํŒŒํ‹ฐ API ๊ฒŒ์ดํŠธ์›จ์ด (Lumen) ์‚ฌ์šฉ์ž ์„œ๋น„์Šค Lumen ๊ฒฐ์ œ ์„œ๋น„์Šค Lumen ์•Œ๋ฆผ ์„œ๋น„์Šค Lumen ๊ธฐํƒ€ ์„œ๋น„์Šค Lumen DB DB DB DB

์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ์ถ”๊ฐ€ํ•˜๊ธฐ ๐Ÿ”’

API ๊ฒŒ์ดํŠธ์›จ์ด์˜ ์ค‘์š”ํ•œ ์—ญํ•  ์ค‘ ํ•˜๋‚˜๋Š” ์ธ์ฆ๊ณผ ๊ถŒํ•œ ๋ถ€์—ฌ๋ฅผ ์ค‘์•™์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฑฐ์˜ˆ์š”. Lumen์— ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณผ๊ฒŒ์š”:


<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class AuthenticateGateway
{
    /**
     * ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        // ์ธ์ฆ ํ† ํฐ ํ™•์ธ
        $token = $request->bearerToken();
        
        if (!$token) {
            return response()->json(['message' => '์ธ์ฆ ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'], 401);
        }
        
        // ์—ฌ๊ธฐ์„œ ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง ๊ตฌํ˜„
        // JWT ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‚˜ ์™ธ๋ถ€ ์ธ์ฆ ์„œ๋น„์Šค ์‚ฌ์šฉ ๊ฐ€๋Šฅ
        
        // ์˜ˆ์‹œ: ๊ฐ„๋‹จํ•œ ํ† ํฐ ๊ฒ€์ฆ
        if (!$this->validateToken($token)) {
            return response()->json(['message' => '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค.'], 401);
        }
        
        // ์ธ์ฆ ์„ฑ๊ณต, ์š”์ฒญ ๊ณ„์† ์ฒ˜๋ฆฌ
        return $next($request);
    }
    
    /**
     * ํ† ํฐ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  string  $token
     * @return bool
     */
    protected function validateToken($token)
    {
        // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” JWT ๊ฒ€์ฆ ๋“ฑ์˜ ๋กœ์ง ํ•„์š”
        // ์˜ˆ์‹œ ์ฝ”๋“œ์ด๋ฏ€๋กœ ํ•ญ์ƒ true ๋ฐ˜ํ™˜
        return true;
    }
}
    

์ด์ œ bootstrap/app.php ํŒŒ์ผ์— ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋“ฑ๋กํ•ด์ฃผ์„ธ์š”:


// ์ „์—ญ ๋ฏธ๋“ค์›จ์–ด ๋“ฑ๋ก
$app->middleware([
    App\Http\Middleware\AuthenticateGateway::class,
]);
    

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ชจ๋“  API ์š”์ฒญ์— ์ธ์ฆ์ด ์ ์šฉ๋ผ์š”. ํŠน์ • ๊ฒฝ๋กœ๋งŒ ์ธ์ฆ์„ ์ ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ๋ผ์šฐํŠธ ๊ทธ๋ฃน์— ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ ์šฉํ•˜๋ฉด ๋ผ์š”:


$router->group(['prefix' => 'api/v1', 'middleware' => 'auth'], function () use ($router) {
    // ์ธ์ฆ์ด ํ•„์š”ํ•œ ๋ผ์šฐํŠธ๋“ค...
});
    

์†๋„ ์ œํ•œ ๊ตฌํ˜„ํ•˜๊ธฐ โฑ๏ธ

API ๊ฒŒ์ดํŠธ์›จ์ด์˜ ๋˜ ๋‹ค๋ฅธ ์ค‘์š”ํ•œ ๊ธฐ๋Šฅ์€ ์†๋„ ์ œํ•œ(Rate Limiting)์ด์—์š”. ์ด๋ฅผ ํ†ตํ•ด API ๋‚จ์šฉ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์–ด์š”. Lumen์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”:


<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Cache\RateLimiter;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class ThrottleRequests
{
    /**
     * The rate limiter instance.
     *
     * @var \Illuminate\Cache\RateLimiter
     */
    protected $limiter;

    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Cache\RateLimiter  $limiter
     * @return void
     */
    public function __construct(RateLimiter $limiter)
    {
        $this->limiter = $limiter;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  int  $maxAttempts
     * @param  int  $decayMinutes
     * @return mixed
     */
    public function handle(Request $request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
    {
        $key = $this->resolveRequestSignature($request);

        if ($this->limiter->tooManyAttempts($key, $maxAttempts)) {
            return $this->buildResponse($key, $maxAttempts);
        }

        $this->limiter->hit($key, $decayMinutes * 60);

        $response = $next($request);

        return $this->addHeaders(
            $response, $maxAttempts,
            $this->calculateRemainingAttempts($key, $maxAttempts)
        );
    }

    /**
     * Resolve request signature.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string
     */
    protected function resolveRequestSignature($request)
    {
        return sha1(
            $request->method() .
            '|' . $request->getHost() .
            '|' . $request->ip()
        );
    }

    /**
     * Create a 'too many attempts' response.
     *
     * @param  string  $key
     * @param  int  $maxAttempts
     * @return \Symfony\Component\HttpFoundation\Response
     */
    protected function buildResponse($key, $maxAttempts)
    {
        $retryAfter = $this->limiter->availableIn($key);

        $headers = [
            'X-RateLimit-Limit' => $maxAttempts,
            'X-RateLimit-Remaining' => 0,
            'Retry-After' => $retryAfter,
            'X-RateLimit-Reset' => time() + $retryAfter,
        ];

        return response()->json(['message' => '๋„ˆ๋ฌด ๋งŽ์€ ์š”์ฒญ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'], 429, $headers);
    }

    /**
     * Add the limit header information to the given response.
     *
     * @param  \Symfony\Component\HttpFoundation\Response  $response
     * @param  int  $maxAttempts
     * @param  int  $remainingAttempts
     * @return \Symfony\Component\HttpFoundation\Response
     */
    protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts)
    {
        $response->headers->add([
            'X-RateLimit-Limit' => $maxAttempts,
            'X-RateLimit-Remaining' => $remainingAttempts,
        ]);

        return $response;
    }

    /**
     * Calculate the number of remaining attempts.
     *
     * @param  string  $key
     * @param  int  $maxAttempts
     * @return int
     */
    protected function calculateRemainingAttempts($key, $maxAttempts)
    {
        return $maxAttempts - $this->limiter->attempts($key) + 1;
    }
}
    

์ด์ œ bootstrap/app.php ํŒŒ์ผ์— ์†๋„ ์ œํ•œ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋“ฑ๋กํ•˜๊ณ , ๋ผ์šฐํŠธ์— ์ ์šฉํ•ด์ฃผ์„ธ์š”:


// ์†๋„ ์ œํ•œ ๋ฏธ๋“ค์›จ์–ด ๋“ฑ๋ก
$app->routeMiddleware([
    'throttle' => App\Http\Middleware\ThrottleRequests::class,
]);

// ๋ผ์šฐํŠธ์— ์ ์šฉ
$router->group(['prefix' => 'api/v1', 'middleware' => 'throttle:60,1'], function () use ($router) {
    // ์†๋„ ์ œํ•œ์ด ์ ์šฉ๋œ ๋ผ์šฐํŠธ๋“ค...
});
    

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ๋Š” 1๋ถ„๋‹น ์ตœ๋Œ€ 60๋ฒˆ์˜ ์š”์ฒญ๋งŒ ํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ด๋ฅผ ํ†ตํ•ด API ๋‚จ์šฉ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์„œ๋น„์Šค์˜ ์•ˆ์ •์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์–ด์š”! ๐Ÿ‘

๐Ÿ’ก ํ”„๋กœ ํŒ!

์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” Kong, Tyk, AWS API Gateway ๊ฐ™์€ ์ „๋ฌธ API ๊ฒŒ์ดํŠธ์›จ์ด ์†”๋ฃจ์…˜์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”. ์ด๋Ÿฐ ๋„๊ตฌ๋“ค์€ ๋” ๋งŽ์€ ๊ธฐ๋Šฅ๊ณผ ํ™•์žฅ์„ฑ์„ ์ œ๊ณตํ•ด์š”. ํ•˜์ง€๋งŒ ์ž‘์€ ํ”„๋กœ์ ํŠธ๋‚˜ ํ•™์Šต ๋ชฉ์ ์ด๋ผ๋ฉด Lumen์œผ๋กœ ๊ตฌํ˜„ํ•œ ๊ฒŒ์ดํŠธ์›จ์ด๋„ ์ถฉ๋ถ„ํžˆ ์ข‹์€ ์„ ํƒ์ด์—์š”!

6. ์ธ์ฆ ๋ฐ ๋ณด์•ˆ ๊ตฌํ˜„ํ•˜๊ธฐ ๐Ÿ”

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์—์„œ ์ธ์ฆ๊ณผ ๋ณด์•ˆ์€ ๋งค์šฐ ์ค‘์š”ํ•œ ์ฃผ์ œ์˜ˆ์š”. ํŠนํžˆ ์—ฌ๋Ÿฌ ์„œ๋น„์Šค๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š” ํ™˜๊ฒฝ์—์„œ๋Š” ์ผ๊ด€๋œ ์ธ์ฆ ์ฒด๊ณ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๊ฒƒ์ด ํ•„์ˆ˜์ ์ด์ฃ ! ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์•Œ์•„๋ณผ๊นŒ์š”? ๐Ÿง

JWT(JSON Web Token) ์ธ์ฆ ๊ตฌํ˜„ํ•˜๊ธฐ ๐ŸŽซ

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํ™˜๊ฒฝ์—์„œ๋Š” JWT(JSON Web Token)๊ฐ€ ์ธ์ฆ ํ‘œ์ค€์œผ๋กœ ๋งŽ์ด ์‚ฌ์šฉ๋ผ์š”. ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š๊ณ (stateless) ํ† ํฐ ์ž์ฒด์— ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ๋‹ด์„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด์ฃ . Lumen์—์„œ JWT ์ธ์ฆ์„ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ์š”!

๋จผ์ € JWT ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ด์ฃผ์„ธ์š”:

composer require firebase/php-jwt

์ด์ œ JWT๋ฅผ ์ฒ˜๋ฆฌํ•  ์„œ๋น„์Šค ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


<?php

namespace App\Services;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Illuminate\Support\Facades\Log;

class JwtService
{
    /**
     * JWT ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param array $payload
     * @return string
     */
    public function encode(array $payload): string
    {
        $payload['iat'] = time(); // ๋ฐœํ–‰ ์‹œ๊ฐ„
        $payload['exp'] = time() + 60 * 60; // 1์‹œ๊ฐ„ ํ›„ ๋งŒ๋ฃŒ
        
        return JWT::encode(
            $payload,
            env('JWT_SECRET', 'your-secret-key'),
            'HS256'
        );
    }
    
    /**
     * JWT ํ† ํฐ์„ ๋””์ฝ”๋”ฉํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param string $token
     * @return object|null
     */
    public function decode(string $token)
    {
        try {
            return JWT::decode(
                $token,
                new Key(env('JWT_SECRET', 'your-secret-key'), 'HS256')
            );
        } catch (\Exception $e) {
            Log::error('JWT ๋””์ฝ”๋”ฉ ์˜ค๋ฅ˜: ' . $e->getMessage());
            return null;
        }
    }
    
    /**
     * ํ† ํฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param string $token
     * @return bool
     */
    public function validate(string $token): bool
    {
        return $this->decode($token) !== null;
    }
}
    

์ด์ œ ์ธ์ฆ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


<?php

namespace App\Http\Controllers;

use App\Models\User;
use App\Services\JwtService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class AuthController extends Controller
{
    protected $jwtService;
    
    public function __construct(JwtService $jwtService)
    {
        $this->jwtService = $jwtService;
    }
    
    /**
     * ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ๋ฐ JWT ํ† ํฐ ๋ฐœ๊ธ‰
     *
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(Request $request)
    {
        $this->validate($request, [
            'email' => 'required|email',
            'password' => 'required'
        ]);
        
        // ์‚ฌ์šฉ์ž ํ™•์ธ
        $user = User::where('email', $request->email)->first();
        
        if (!$user || !Hash::check($request->password, $user->password)) {
            return response()->json([
                'message' => '์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'
            ], 401);
        }
        
        // JWT ํ† ํฐ ์ƒ์„ฑ
        $token = $this->jwtService->encode([
            'sub' => $user->id, // ์‚ฌ์šฉ์ž ID
            'name' => $user->name,
            'email' => $user->email
        ]);
        
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => 3600 // 1์‹œ๊ฐ„
        ]);
    }
    
    /**
     * ํ˜„์žฌ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ
     *
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function me(Request $request)
    {
        $token = $request->bearerToken();
        $payload = $this->jwtService->decode($token);
        
        if (!$payload) {
            return response()->json(['message' => '์ธ์ฆ ์‹คํŒจ'], 401);
        }
        
        $user = User::find($payload->sub);
        
        return response()->json($user);
    }
    
    /**
     * ํ† ํฐ ๊ฐฑ์‹ 
     *
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh(Request $request)
    {
        $token = $request->bearerToken();
        $payload = $this->jwtService->decode($token);
        
        if (!$payload) {
            return response()->json(['message' => '์ธ์ฆ ์‹คํŒจ'], 401);
        }
        
        // ์ƒˆ ํ† ํฐ ์ƒ์„ฑ
        $newToken = $this->jwtService->encode([
            'sub' => $payload->sub,
            'name' => $payload->name,
            'email' => $payload->email
        ]);
        
        return response()->json([
            'access_token' => $newToken,
            'token_type' => 'bearer',
            'expires_in' => 3600 // 1์‹œ๊ฐ„
        ]);
    }
}
    

์ด์ œ ์ธ์ฆ ๋ผ์šฐํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณผ๊ฒŒ์š”:


$router->group(['prefix' => 'api/v1/auth'], function () use ($router) {
    $router->post('login', 'AuthController@login');
    $router->get('me', 'AuthController@me');
    $router->post('refresh', 'AuthController@refresh');
});
    

JWT ์ธ์ฆ์„ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•œ ๋ฏธ๋“ค์›จ์–ด๋„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


<?php

namespace App\Http\Middleware;

use App\Services\JwtService;
use Closure;
use Illuminate\Http\Request;

class JwtAuthenticate
{
    protected $jwtService;
    
    public function __construct(JwtService $jwtService)
    {
        $this->jwtService = $jwtService;
    }
    
    /**
     * JWT ํ† ํฐ์„ ๊ฒ€์ฆํ•˜๊ณ  ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        $token = $request->bearerToken();
        
        if (!$token) {
            return response()->json(['message' => '์ธ์ฆ ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'], 401);
        }
        
        $payload = $this->jwtService->decode($token);
        
        if (!$payload) {
            return response()->json(['message' => '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค.'], 401);
        }
        
        // ์š”์ฒญ์— ์‚ฌ์šฉ์ž ID ์ถ”๊ฐ€
        $request->attributes->add(['user_id' => $payload->sub]);
        
        return $next($request);
    }
}
    

๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋“ฑ๋กํ•˜๊ณ  ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ์— ์ ์šฉํ•ด๋ณผ๊ฒŒ์š”:


// bootstrap/app.php์— ๋ฏธ๋“ค์›จ์–ด ๋“ฑ๋ก
$app->routeMiddleware([
    'jwt.auth' => App\Http\Middleware\JwtAuthenticate::class,
]);

// ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ์— ์ ์šฉ
$router->group(['prefix' => 'api/v1', 'middleware' => 'jwt.auth'], function () use ($router) {
    // ์ธ์ฆ์ด ํ•„์š”ํ•œ ๋ผ์šฐํŠธ๋“ค...
    $router->get('users/profile', 'UserController@profile');
});
    
ํด๋ผ์ด์–ธํŠธ API ๊ฒŒ์ดํŠธ์›จ์ด ์ธ์ฆ ์„œ๋น„์Šค ์„œ๋น„์Šค A ์„œ๋น„์Šค B ์„œ๋น„์Šค C 1. ๋กœ๊ทธ์ธ ์š”์ฒญ 2. ์ธ์ฆ ์š”์ฒญ ์ „๋‹ฌ 3. JWT ํ† ํฐ ๋ฐœ๊ธ‰ 4. ํ† ํฐ ์ „๋‹ฌ 5. ํ† ํฐ์œผ๋กœ API ์š”์ฒญ 6. ํ† ํฐ ๊ฒ€์ฆ 7. ์š”์ฒญ ์ „๋‹ฌ JWT ํ† ํฐ ๊ตฌ์กฐ Header.Payload.Signature ์•Œ๊ณ ๋ฆฌ์ฆ˜, ํ† ํฐ ํƒ€์ž… ์‚ฌ์šฉ์ž ์ •๋ณด, ๊ถŒํ•œ, ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ

CORS(Cross-Origin Resource Sharing) ์„ค์ •ํ•˜๊ธฐ ๐ŸŒ

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํ™˜๊ฒฝ์—์„œ๋Š” CORS(Cross-Origin Resource Sharing) ์„ค์ •์ด ํ•„์ˆ˜์ ์ด์—์š”. ์—ฌ๋Ÿฌ ๋„๋ฉ”์ธ์—์„œ API์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๋‹ˆ๊นŒ์š”. Lumen์—์„œ CORS๋ฅผ ์„ค์ •ํ•ด๋ณผ๊ฒŒ์š”:


<?php

namespace App\Http\Middleware;

use Closure;

class CorsMiddleware
{
    /**
     * CORS ํ—ค๋”๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $headers = [
            'Access-Control-Allow-Origin'      => '*',
            'Access-Control-Allow-Methods'     => 'POST, GET, OPTIONS, PUT, DELETE',
            'Access-Control-Allow-Credentials' => 'true',
            'Access-Control-Max-Age'           => '86400',
            'Access-Control-Allow-Headers'     => 'Content-Type, Authorization, X-Requested-With'
        ];

        if ($request->isMethod('OPTIONS')) {
            return response()->json('{"method":"OPTIONS"}', 200, $headers);
        }

        $response = $next($request);
        
        foreach ($headers as $key => $value) {
            $response->header($key, $value);
        }

        return $response;
    }
}
    

์ด์ œ bootstrap/app.php ํŒŒ์ผ์— CORS ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋“ฑ๋กํ•ด์ฃผ์„ธ์š”:


$app->middleware([
    App\Http\Middleware\CorsMiddleware::class
]);
    

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์—์„œ๋„ API์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด์š”! ๐Ÿ‘

๋ฐ์ดํ„ฐ ์•”ํ˜ธํ™” ๊ตฌํ˜„ํ•˜๊ธฐ ๐Ÿ”’

๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ๋Š” ์•”ํ˜ธํ™”๊ฐ€ ํ•„์ˆ˜์ ์ด์—์š”. Lumen์—์„œ๋Š” ๋‚ด์žฅ๋œ ์•”ํ˜ธํ™” ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”:


// bootstrap/app.php์—์„œ ์•”ํ˜ธํ™” ์„œ๋น„์Šค ๋“ฑ๋ก
$app->register(Illuminate\Encryption\EncryptionServiceProvider::class);

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$encryptedData = app('encrypter')->encrypt($sensitiveData);
$decryptedData = app('encrypter')->decrypt($encryptedData);
    

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณดํ˜ธํ•  ์ˆ˜ ์žˆ์–ด์š”! ๐Ÿ˜Š

โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ

  1. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ณดํ˜ธ: .env ํŒŒ์ผ์— ์žˆ๋Š” ๋น„๋ฐ€ ํ‚ค์™€ ์ž๊ฒฉ ์ฆ๋ช…์„ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด๊ด€ํ•˜์„ธ์š”.
  2. HTTPS ์‚ฌ์šฉ: ๋ชจ๋“  API ํ†ต์‹ ์€ HTTPS๋ฅผ ํ†ตํ•ด ์ด๋ฃจ์–ด์ ธ์•ผ ํ•ด์š”.
  3. ์ž…๋ ฅ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ: ๋ชจ๋“  ์‚ฌ์šฉ์ž ์ž…๋ ฅ์€ ์ฒ ์ €ํžˆ ๊ฒ€์ฆํ•ด์•ผ ํ•ด์š”.
  4. ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ •: JWT ํ† ํฐ์—๋Š” ์ ์ ˆํ•œ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ ์„ค์ •ํ•˜์„ธ์š”.
  5. ๋กœ๊น…๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง: ๋ณด์•ˆ ์ด๋ฒคํŠธ๋ฅผ ๋กœ๊น…ํ•˜๊ณ  ๋ชจ๋‹ˆํ„ฐ๋งํ•˜์„ธ์š”.

7. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๋™ ๋ฐ ์ตœ์ ํ™” ๐Ÿ’พ

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„๋Š” ๋งค์šฐ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์—์š”. ๊ฐ ์„œ๋น„์Šค๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋ ค๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋„ ์ ์ ˆํžˆ ๋ถ„๋ฆฌ๋˜์–ด์•ผ ํ•˜์ฃ . ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ์•Œ์•„๋ณผ๊นŒ์š”? ๐Ÿง

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒจํ„ด ์„ ํƒํ•˜๊ธฐ ๐Ÿ“Š

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์—์„œ๋Š” ์ฃผ๋กœ ๋‘ ๊ฐ€์ง€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒจํ„ด์„ ์‚ฌ์šฉํ•ด์š”:

  1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค per ์„œ๋น„์Šค: ๊ฐ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๊ฐ€ ์ž์ฒด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๊ฐ€์ง
  2. ๊ณต์œ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: ์—ฌ๋Ÿฌ ์„œ๋น„์Šค๊ฐ€ ํ•˜๋‚˜์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๊ณต์œ 

์ผ๋ฐ˜์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค per ์„œ๋น„์Šค ํŒจํ„ด์ด ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์ฒ ํ•™์— ๋” ๋ถ€ํ•ฉํ•ด์š”. ๊ฐ ์„œ๋น„์Šค๊ฐ€ ์™„์ „ํžˆ ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ์š”! ํ•˜์ง€๋งŒ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ์œ ์ง€๊ฐ€ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์–ด์š”.

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค per ์„œ๋น„์Šค ์‚ฌ์šฉ์ž ์„œ๋น„์Šค ์‚ฌ์šฉ์ž DB ๊ฒฐ์ œ ์„œ๋น„์Šค ๊ฒฐ์ œ DB ์ƒํ’ˆ ์„œ๋น„์Šค ์ƒํ’ˆ DB ์•Œ๋ฆผ ์„œ๋น„์Šค ์•Œ๋ฆผ DB ๊ณต์œ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ์ž ์„œ๋น„์Šค ๊ฒฐ์ œ ์„œ๋น„์Šค ์ƒํ’ˆ ์„œ๋น„์Šค ์•Œ๋ฆผ ์„œ๋น„์Šค ๊ณต์œ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค โœ“ ์™„์ „ํ•œ ๋…๋ฆฝ์„ฑ โœ— ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ์œ ์ง€ ์–ด๋ ค์›€ โœ“ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ์œ ์ง€ ์‰ฌ์›€ โœ— ์„œ๋น„์Šค ๊ฐ„ ๊ฒฐํ•ฉ๋„ ์ฆ๊ฐ€

Lumen์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์„ค์ •ํ•˜๊ธฐ ๐Ÿ”Œ

Lumen์—์„œ๋Š” .env ํŒŒ์ผ์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด์š”:


# .env ํŒŒ์ผ
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=user_service_db
DB_USERNAME=root
DB_PASSWORD=password
    

์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด config/database.php ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด์š”:


<?php

return [
    'default' => env('DB_CONNECTION', 'mysql'),
    
    'connections' => [
        'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', 3306),
            'database' => env('DB_DATABASE', 'user_service_db'),
            'username' => env('DB_USERNAME', 'root'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => env('DB_CHARSET', 'utf8mb4'),
            'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
            'prefix' => env('DB_PREFIX', ''),
            'strict' => env('DB_STRICT_MODE', true),
            'engine' => env('DB_ENGINE', null),
        ],
        
        'payment_db' => [
            'driver' => 'mysql',
            'host' => env('PAYMENT_DB_HOST', '127.0.0.1'),
            'port' => env('PAYMENT_DB_PORT', 3306),
            'database' => env('PAYMENT_DB_DATABASE', 'payment_service_db'),
            'username' => env('PAYMENT_DB_USERNAME', 'root'),
            'password' => env('PAYMENT_DB_PASSWORD', ''),
            'unix_socket' => env('PAYMENT_DB_SOCKET', ''),
            'charset' => env('PAYMENT_DB_CHARSET', 'utf8mb4'),
            'collation' => env('PAYMENT_DB_COLLATION', 'utf8mb4_unicode_ci'),
            'prefix' => env('PAYMENT_DB_PREFIX', ''),
            'strict' => env('PAYMENT_DB_STRICT_MODE', true),
            'engine' => env('PAYMENT_DB_ENGINE', null),
        ],
    ],
    
    'migrations' => 'migrations',
    
    'redis' => [
        'client' => env('REDIS_CLIENT', 'phpredis'),
        'default' => [
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'password' => env('REDIS_PASSWORD', null),
            'port' => env('REDIS_PORT', 6379),
            'database' => env('REDIS_DB', 0),
        ],
        'cache' => [
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'password' => env('REDIS_PASSWORD', null),
            'port' => env('REDIS_PORT', 6379),
            'database' => env('REDIS_CACHE_DB', 1),
        ],
    ],
];
    

์ด์ œ bootstrap/app.php ํŒŒ์ผ์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •์„ ๋กœ๋“œํ•ด์ฃผ์„ธ์š”:


$app->configure('database');
    

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”! ํŠน์ • ์—ฐ๊ฒฐ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•˜๋ฉด ๋ผ์š”:


// ๊ธฐ๋ณธ ์—ฐ๊ฒฐ ์‚ฌ์šฉ
$users = DB::table('users')->get();

// ๊ฒฐ์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์‚ฌ์šฉ
$payments = DB::connection('payment_db')->table('payments')->get();
    

Eloquent ORM ์‚ฌ์šฉํ•˜๊ธฐ ๐Ÿง™โ€โ™‚๏ธ

Lumen์—์„œ๋„ Laravel์˜ Eloquent ORM์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. ๋จผ์ € bootstrap/app.php ํŒŒ์ผ์—์„œ Eloquent๋ฅผ ํ™œ์„ฑํ™”ํ•ด์ฃผ์„ธ์š”:


$app->withEloquent();
    

์ด์ œ ๋ชจ๋ธ์„ ๋งŒ๋“ค๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”:


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Payment extends Model
{
    // ๊ธฐ๋ณธ ์—ฐ๊ฒฐ ๋Œ€์‹  payment_db ์—ฐ๊ฒฐ ์‚ฌ์šฉ
    protected $connection = 'payment_db';
    
    // ํ…Œ์ด๋ธ” ์ด๋ฆ„ ์ง€์ •
    protected $table = 'payments';
    
    // ๋Œ€๋Ÿ‰ ํ• ๋‹น ๊ฐ€๋Šฅํ•œ ์†์„ฑ
    protected $fillable = [
        'user_id', 'amount', 'status', 'description'
    ];
    
    // ํƒ€์ž… ์บ์ŠคํŒ…
    protected $casts = [
        'amount' => 'float',
        'created_at' => 'datetime',
        'updated_at' => 'datetime'
    ];
    
    // ์‚ฌ์šฉ์ž์™€์˜ ๊ด€๊ณ„ ์ •์˜
    public function user()
    {
        // ๋‹ค๋ฅธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋ชจ๋ธ๊ณผ ๊ด€๊ณ„ ์„ค์ •
        return $this->belongsTo(User::class);
    }
}
    

์ด์ œ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”:


<?php

namespace App\Http\Controllers;

use App\Models\Payment;
use Illuminate\Http\Request;

class PaymentController extends Controller
{
    public function index()
    {
        $payments = Payment::all();
        return response()->json($payments);
    }
    
    public function store(Request $request)
    {
        $this->validate($request, [
            'user_id' => 'required|integer',
            'amount' => 'required|numeric',
            'description' => 'required|string'
        ]);
        
        $payment = Payment::create([
            'user_id' => $request->user_id,
            'amount' => $request->amount,
            'status' => 'pending',
            'description' => $request->description
        ]);
        
        return response()->json($payment, 201);
    }
    
    // ๋‹ค๋ฅธ ๋ฉ”์†Œ๋“œ๋“ค...
}
    

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ตœ์ ํ™” ๊ธฐ๋ฒ• ๐Ÿš€

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ฑ๋Šฅ์€ ๋งค์šฐ ์ค‘์š”ํ•ด์š”. ๋‹ค์Œ์€ ๋ช‡ ๊ฐ€์ง€ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์ด์—์š”:

  1. ์ธ๋ฑ์Šค ํ™œ์šฉ: ์ž์ฃผ ์กฐํšŒํ•˜๋Š” ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”.
  2. ์ฟผ๋ฆฌ ์ตœ์ ํ™”: N+1 ๋ฌธ์ œ๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด Eager Loading์„ ์‚ฌ์šฉํ•˜์„ธ์š”.
  3. ์ปค๋„ฅ์…˜ ํ’€๋ง: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ์žฌ์‚ฌ์šฉํ•˜์—ฌ ์˜ค๋ฒ„ํ—ค๋“œ๋ฅผ ์ค„์ด์„ธ์š”.
  4. ์บ์‹ฑ: ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ดํ„ฐ๋Š” Redis๋‚˜ Memcached์— ์บ์‹ฑํ•˜์„ธ์š”.
  5. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒค๋”ฉ: ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ๋ฅผ ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ถ„์‚ฐํ•˜์„ธ์š”.

์˜ˆ๋ฅผ ๋“ค์–ด, Lumen์—์„œ Redis ์บ์‹ฑ์„ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•˜๋ฉด ๋ผ์š”:


// Redis ํŒจํ‚ค์ง€ ์„ค์น˜
composer require predis/predis

// .env ํŒŒ์ผ์— Redis ์„ค์ • ์ถ”๊ฐ€
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

// bootstrap/app.php์— ์บ์‹œ ๋“œ๋ผ์ด๋ฒ„ ์„ค์ •
$app->register(Illuminate\Redis\RedisServiceProvider::class);
$app->configure('cache');

// ์บ์‹ฑ ์‚ฌ์šฉ ์˜ˆ์‹œ
$users = Cache::remember('users', 60, function () {
    return User::all();
});
    

๐Ÿ’ก ํ”„๋กœ ํŒ!

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋Š” ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ๊ณผ ์„ฑ๋Šฅ ์‚ฌ์ด์˜ ๊ท ํ˜•์ด ์ค‘์š”ํ•ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž ํ”„๋กœํ•„๊ณผ ๊ฒฐ์ œ ์ •๋ณด๋Š” ๋ณ„๋„์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•˜๋˜, ํ•„์š”ํ•  ๋•Œ API๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์„œ๋น„์Šค ๋…๋ฆฝ์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์–ด์š”!

8. ์„œ๋น„์Šค ๋ฐฐํฌ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ๐Ÿšข

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋ฅผ ๊ฐœ๋ฐœํ–ˆ๋‹ค๋ฉด ์ด์ œ ๋ฐฐํฌ์™€ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ๊ณ ๋ คํ•ด์•ผ ํ•ด์š”. ์—ฌ๋Ÿฌ ์„œ๋น„์Šค๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ์š”! ๐Ÿง

Docker๋ฅผ ์‚ฌ์šฉํ•œ ์ปจํ…Œ์ด๋„ˆํ™” ๐Ÿณ

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๋ฐฐํฌ์—๋Š” Docker๊ฐ€ ์ตœ์ ์˜ ์„ ํƒ์ด์—์š”. ๊ฐ ์„œ๋น„์Šค๋ฅผ ๋…๋ฆฝ์ ์ธ ์ปจํ…Œ์ด๋„ˆ๋กœ ํŒจํ‚ค์ง•ํ•˜๋ฉด ํ™˜๊ฒฝ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๊ณ  ๋ฐฐํฌ๋ฅผ ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์žˆ์–ด์š”.

Lumen ์„œ๋น„์Šค๋ฅผ ์œ„ํ•œ Dockerfile์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


FROM php:8.2-fpm

# ์˜์กด์„ฑ ์„ค์น˜
RUN apt-get update && apt-get install -y \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip

# PHP ํ™•์žฅ ์„ค์น˜
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd

# Composer ์„ค์น˜
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
WORKDIR /var/www/html

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒŒ์ผ ๋ณต์‚ฌ
COPY . .

# Composer ์˜์กด์„ฑ ์„ค์น˜
RUN composer install --no-dev --optimize-autoloader

# ๊ถŒํ•œ ์„ค์ •
RUN chown -R www-data:www-data /var/www/html

# ํฌํŠธ ๋…ธ์ถœ
EXPOSE 8000

# ์„œ๋ฒ„ ์‹คํ–‰
CMD php -S 0.0.0.0:8000 -t public
    

์ด์ œ Docker Compose๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์—ฌ๋Ÿฌ ์„œ๋น„์Šค๋ฅผ ํ•จ๊ป˜ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”. docker-compose.yml ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


version: '3'

services:
  # API ๊ฒŒ์ดํŠธ์›จ์ด
  api-gateway:
    build:
      context: ./api-gateway
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    depends_on:
      - user-service
      - payment-service
      - notification-service
    environment:
      - USER_SERVICE_URL=http://user-service:8001/api/v1
      - PAYMENT_SERVICE_URL=http://payment-service:8002/api/v1
      - NOTIFICATION_SERVICE_URL=http://notification-service:8003/api/v1

  # ์‚ฌ์šฉ์ž ์„œ๋น„์Šค
  user-service:
    build:
      context: ./user-service
      dockerfile: Dockerfile
    ports:
      - "8001:8000"
    depends_on:
      - user-db
    environment:
      - DB_CONNECTION=mysql
      - DB_HOST=user-db
      - DB_PORT=3306
      - DB_DATABASE=user_service_db
      - DB_USERNAME=root
      - DB_PASSWORD=root_password

  # ๊ฒฐ์ œ ์„œ๋น„์Šค
  payment-service:
    build:
      context: ./payment-service
      dockerfile: Dockerfile
    ports:
      - "8002:8000"
    depends_on:
      - payment-db
    environment:
      - DB_CONNECTION=mysql
      - DB_HOST=payment-db
      - DB_PORT=3306
      - DB_DATABASE=payment_service_db
      - DB_USERNAME=root
      - DB_PASSWORD=root_password

  # ์•Œ๋ฆผ ์„œ๋น„์Šค
  notification-service:
    build:
      context: ./notification-service
      dockerfile: Dockerfile
    ports:
      - "8003:8000"
    depends_on:
      - notification-db
    environment:
      - DB_CONNECTION=mysql
      - DB_HOST=notification-db
      - DB_PORT=3306
      - DB_DATABASE=notification_service_db
      - DB_USERNAME=root
      - DB_PASSWORD=root_password

  # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
  user-db:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=root_password
      - MYSQL_DATABASE=user_service_db
    volumes:
      - user-db-data:/var/lib/mysql

  payment-db:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=root_password
      - MYSQL_DATABASE=payment_service_db
    volumes:
      - payment-db-data:/var/lib/mysql

  notification-db:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=root_password
      - MYSQL_DATABASE=notification_service_db
    volumes:
      - notification-db-data:/var/lib/mysql

volumes:
  user-db-data:
  payment-db-data:
  notification-db-data:
    

์ด์ œ ๋‹ค์Œ ๋ช…๋ น์–ด๋กœ ๋ชจ๋“  ์„œ๋น„์Šค๋ฅผ ํ•œ ๋ฒˆ์— ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์š”:

docker-compose up -d

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ฐ ์„œ๋น„์Šค๊ฐ€ ๋…๋ฆฝ์ ์ธ ์ปจํ…Œ์ด๋„ˆ์—์„œ ์‹คํ–‰๋˜๊ณ , ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋„ ํ•จ๊ป˜ ์‹คํ–‰๋ผ์š”! ๐Ÿ‘

Kubernetes๋ฅผ ์‚ฌ์šฉํ•œ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ โš“

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

๊ฐ„๋‹จํ•œ Kubernetes ๋ฐฐํฌ ํŒŒ์ผ(deployment.yaml)์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
  labels:
    app: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: your-registry/user-service:latest
        ports:
        - containerPort: 8000
        env:
        - name: DB_CONNECTION
          value: "mysql"
        - name: DB_HOST
          value: "user-db-service"
        - name: DB_PORT
          value: "3306"
        - name: DB_DATABASE
          value: "user_service_db"
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: username
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password
        resources:
          limits:
            cpu: "0.5"
            memory: "512Mi"
          requests:
            cpu: "0.2"
            memory: "256Mi"
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
  - port: 80
    targetPort: 8000
  type: ClusterIP
    

์ด ํŒŒ์ผ์€ ์‚ฌ์šฉ์ž ์„œ๋น„์Šค๋ฅผ 3๊ฐœ์˜ ๋ณต์ œ๋ณธ์œผ๋กœ ๋ฐฐํฌํ•˜๊ณ , ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ์„ ์œ„ํ•œ ์„œ๋น„์Šค๋ฅผ ์ƒ์„ฑํ•ด์š”. ๋‹ค์Œ ๋ช…๋ น์–ด๋กœ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์–ด์š”:

kubectl apply -f deployment.yaml
Kubernetes ํด๋Ÿฌ์Šคํ„ฐ ์ธ๊ทธ๋ ˆ์Šค ์ปจํŠธ๋กค๋Ÿฌ ์‚ฌ์šฉ์ž ์„œ๋น„์Šค ๊ฒฐ์ œ ์„œ๋น„์Šค ์•Œ๋ฆผ ์„œ๋น„์Šค Pod 1 Pod 2 Pod 3 Pod 1 Pod 2 Pod 3 Pod 1 Pod 2 ์‚ฌ์šฉ์ž DB ๊ฒฐ์ œ DB ์•Œ๋ฆผ DB Prometheus Grafana ELK Stack

CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•ํ•˜๊ธฐ ๐Ÿ”„

CI/CD(์ง€์†์  ํ†ตํ•ฉ/์ง€์†์  ๋ฐฐํฌ) ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ•˜๋ฉด ์ฝ”๋“œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ž๋™์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ณ  ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์–ด์š”. GitHub Actions๋ฅผ ์‚ฌ์šฉํ•œ ๊ฐ„๋‹จํ•œ CI/CD ํŒŒ์ดํ”„๋ผ์ธ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.2'
        
    - name: Install Dependencies
      run: composer install --no-ansi --no-interaction --no-progress
      
    - name: Run Tests
      run: vendor/bin/phpunit
      
  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Login to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_HUB_USERNAME }}
        password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
        
    - name: Build and Push Docker Image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: ${{ secrets.DOCKER_HUB_USERNAME }}/user-service:latest
        
    - name: Deploy to Kubernetes
      uses: steebchen/kubectl@v2
      with:
        config: ${{ secrets.KUBE_CONFIG_DATA }}
        command: apply -f k8s/deployment.yaml
        
    - name: Verify Deployment
      uses: steebchen/kubectl@v2
      with:
        config: ${{ secrets.KUBE_CONFIG_DATA }}
        command: rollout status deployment/user-service
    

์ด ์›Œํฌํ”Œ๋กœ์šฐ๋Š” ์ฝ”๋“œ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ณ , Docker ์ด๋ฏธ์ง€๋ฅผ ๋นŒ๋“œํ•˜์—ฌ Docker Hub์— ํ‘ธ์‹œํ•œ ๋‹ค์Œ, Kubernetes ํด๋Ÿฌ์Šคํ„ฐ์— ๋ฐฐํฌํ•ด์š”. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ฝ”๋“œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์ž๋™์œผ๋กœ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ๋ฐ˜์˜๋ผ์š”! ๐Ÿš€

๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋กœ๊น… ๊ตฌํ˜„ํ•˜๊ธฐ ๐Ÿ“Š

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํ™˜๊ฒฝ์—์„œ๋Š” ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น…์ด ๋งค์šฐ ์ค‘์š”ํ•ด์š”. ์—ฌ๋Ÿฌ ์„œ๋น„์Šค์˜ ์ƒํƒœ๋ฅผ ํ•œ๋ˆˆ์— ํŒŒ์•…ํ•˜๊ณ  ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ๋น ๋ฅด๊ฒŒ ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๋‹ˆ๊นŒ์š”.

Lumen์—์„œ ๋กœ๊น…์„ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•˜๋ฉด ๋ผ์š”:


// bootstrap/app.php์—์„œ ๋กœ๊น… ์„ค์ •
$app->configureMonologUsing(function ($monolog) {
    $monolog->pushHandler(
        new \Monolog\Handler\StreamHandler(
            storage_path('logs/lumen.log'),
            \Monolog\Logger::DEBUG
        )
    );
    
    // JSON ํ˜•์‹์œผ๋กœ ๋กœ๊ทธ ์ถœ๋ ฅ
    $monolog->pushProcessor(new \Monolog\Processor\WebProcessor());
    $monolog->pushProcessor(new \Monolog\Processor\IntrospectionProcessor());
    $monolog->pushProcessor(new \Monolog\Processor\UidProcessor());
    
    return $monolog;
});

// ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋กœ๊ทธ ์‚ฌ์šฉ
Log::info('์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ', ['user_id' => $user->id, 'email' => $user->email]);
Log::error('๊ฒฐ์ œ ์‹คํŒจ', ['user_id' => $userId, 'amount' => $amount, 'error' => $e->getMessage()]);
    

๋กœ๊ทธ๋ฅผ ์ค‘์•™ ์ง‘์ค‘ํ™”ํ•˜๋ ค๋ฉด ELK Stack(Elasticsearch, Logstash, Kibana)์ด๋‚˜ Graylog ๊ฐ™์€ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์—ฌ๋Ÿฌ ์„œ๋น„์Šค์˜ ๋กœ๊ทธ๋ฅผ ํ•œ ๊ณณ์—์„œ ํ™•์ธํ•˜๊ณ  ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์–ด์š”!

์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์œ„ํ•ด์„œ๋Š” Prometheus์™€ Grafana๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„์š”. Lumen์— Prometheus ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•˜๋ฉด ๋ผ์š”:


// Prometheus ํด๋ผ์ด์–ธํŠธ ์„ค์น˜
composer require promphp/prometheus_client_php

// ๋ฏธ๋“ค์›จ์–ด ์ƒ์„ฑ
<?php

namespace App\Http\Middleware;

use Closure;
use Prometheus\CollectorRegistry;
use Prometheus\Storage\APC;

class PrometheusMiddleware
{
    protected $registry;
    
    public function __construct()
    {
        $this->registry = new CollectorRegistry(new APC());
    }
    
    public function handle($request, Closure $next)
    {
        $start = microtime(true);
        
        $response = $next($request);
        
        $duration = microtime(true) - $start;
        
        // ์š”์ฒญ ์นด์šดํ„ฐ ์ฆ๊ฐ€
        $counter = $this->registry->getOrRegisterCounter(
            'app',
            'http_requests_total',
            'Total number of HTTP requests',
            ['method', 'endpoint', 'status']
        );
        
        $counter->inc([
            $request->method(),
            $request->path(),
            $response->getStatusCode()
        ]);
        
        // ์š”์ฒญ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ๊ธฐ๋ก
        $histogram = $this->registry->getOrRegisterHistogram(
            'app',
            'http_request_duration_seconds',
            'HTTP request duration in seconds',
            ['method', 'endpoint'],
            [0.01, 0.05, 0.1, 0.5, 1, 2, 5]
        );
        
        $histogram->observe(
            $duration,
            [$request->method(), $request->path()]
        );
        
        return $response;
    }
}

// ๋ฉ”ํŠธ๋ฆญ ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€
$router->get('/metrics', function () {
    $registry = new \Prometheus\CollectorRegistry(new \Prometheus\Storage\APC());
    $renderer = new \Prometheus\RenderTextFormat();
    
    return response($renderer->render($registry->getMetricFamilySamples()), 200)
        ->header('Content-Type', 'text/plain');
});
    

์ด์ œ Prometheus๋กœ ๋ฉ”ํŠธ๋ฆญ์„ ์ˆ˜์ง‘ํ•˜๊ณ  Grafana๋กœ ์‹œ๊ฐํ™”ํ•  ์ˆ˜ ์žˆ์–ด์š”! ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์„œ๋น„์Šค์˜ ์„ฑ๋Šฅ๊ณผ ์ƒํƒœ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์–ด์š”. ๐Ÿ˜Š

๐Ÿ’ก ํ”„๋กœ ํŒ!

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋Š” ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๋ชจ๋‹ˆํ„ฐ๋ง๋„ ์ค‘์š”ํ•ด์š”. ์‹ค์ œ ์‚ฌ์šฉ์ž์˜ ๊ฒฝํ—˜์„ ์ธก์ •ํ•˜๋Š” RUM(Real User Monitoring) ๋„๊ตฌ๋ฅผ ๋„์ž…ํ•˜๋ฉด ์‚ฌ์šฉ์ž ๊ด€์ ์—์„œ์˜ ์„ฑ๋Šฅ ๋ฌธ์ œ๋ฅผ ํŒŒ์•…ํ•˜๊ณ  ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์–ด์š”. ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ ๋ชจ๋‘๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋ฉด ์ „์ฒด์ ์ธ ์„œ๋น„์Šค ํ’ˆ์งˆ์„ ๋†’์ผ ์ˆ˜ ์žˆ์–ด์š”!

9. ์‹ค์ œ ํ”„๋กœ์ ํŠธ ์‚ฌ๋ก€ ๋ถ„์„ ๐Ÿ”

์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ๋ฐฐ์› ์œผ๋‹ˆ, ์ด์ œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ ์‚ฌ๋ก€๋ฅผ ํ†ตํ•ด Lumen ๊ธฐ๋ฐ˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๊ฐ€ ์–ด๋–ป๊ฒŒ ํ™œ์šฉ๋˜๋Š”์ง€ ์•Œ์•„๋ณผ๊ฒŒ์š”! ๐Ÿง

์žฌ๋Šฅ ๊ณต์œ  ํ”Œ๋žซํผ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ตฌํ˜„ ์‚ฌ๋ก€ ๐ŸŒŸ

์žฌ๋Šฅ๋„ท๊ณผ ๊ฐ™์€ ์žฌ๋Šฅ ๊ณต์œ  ํ”Œ๋žซํผ์„ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋กœ ๊ตฌํ˜„ํ•œ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”? ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์„œ๋น„์Šค๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์–ด์š”:

  1. ์‚ฌ์šฉ์ž ์„œ๋น„์Šค: ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ, ํ”„๋กœํ•„ ๊ด€๋ฆฌ
  2. ์žฌ๋Šฅ ์„œ๋น„์Šค: ์žฌ๋Šฅ ๋“ฑ๋ก, ์กฐํšŒ, ๊ฒ€์ƒ‰
  3. ์ฃผ๋ฌธ ์„œ๋น„์Šค: ์žฌ๋Šฅ ๊ตฌ๋งค, ์ฃผ๋ฌธ ๊ด€๋ฆฌ
  4. ๊ฒฐ์ œ ์„œ๋น„์Šค: ๊ฒฐ์ œ ์ฒ˜๋ฆฌ, ํ™˜๋ถˆ
  5. ๋ฆฌ๋ทฐ ์„œ๋น„์Šค: ๋ฆฌ๋ทฐ ์ž‘์„ฑ, ์กฐํšŒ
  6. ์•Œ๋ฆผ ์„œ๋น„์Šค: ์ด๋ฉ”์ผ, ํ‘ธ์‹œ ์•Œ๋ฆผ
  7. ๊ฒ€์ƒ‰ ์„œ๋น„์Šค: ํ†ตํ•ฉ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ

๊ฐ ์„œ๋น„์Šค๋Š” Lumen์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ณ , API ๊ฒŒ์ดํŠธ์›จ์ด๋ฅผ ํ†ตํ•ด ํด๋ผ์ด์–ธํŠธ์™€ ํ†ต์‹ ํ•ด์š”. ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์€ HTTP API์™€ ๋ฉ”์‹œ์ง€ ํ๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”.

์›น ์•ฑ ๋ชจ๋ฐ”์ผ ์•ฑ API ๊ฒŒ์ดํŠธ์›จ์ด ์‚ฌ์šฉ์ž ์„œ๋น„์Šค ์žฌ๋Šฅ ์„œ๋น„์Šค ์ฃผ๋ฌธ ์„œ๋น„์Šค ๊ฒฐ์ œ ์„œ๋น„์Šค ๋ฆฌ๋ทฐ ์„œ๋น„์Šค ์•Œ๋ฆผ ์„œ๋น„์Šค ๊ฒ€์ƒ‰ ์„œ๋น„์Šค ๋ฉ”์‹œ์ง€ ํ ์‚ฌ์šฉ์ž DB ์žฌ๋Šฅ DB ์ฃผ๋ฌธ DB ๊ฒฐ์ œ DB ๋ฆฌ๋ทฐ DB ์•Œ๋ฆผ DB ๊ฒ€์ƒ‰ ์ธ๋ฑ์Šค

์ฃผ์š” ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์‚ฌ๋ก€ ๐Ÿ’ก

์žฌ๋Šฅ ๊ณต์œ  ํ”Œ๋žซํผ์˜ ์ฃผ์š” ๊ธฐ๋Šฅ์„ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋กœ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์‚ดํŽด๋ณผ๊ฒŒ์š”:

1. ์žฌ๋Šฅ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๐Ÿ”

์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์žฌ๋Šฅ์„ ๊ฒ€์ƒ‰ํ•˜๋Š” ๊ธฐ๋Šฅ์€ ๊ฒ€์ƒ‰ ์„œ๋น„์Šค์—์„œ ๋‹ด๋‹นํ•ด์š”. Elasticsearch๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํšจ์œจ์ ์ธ ๊ฒ€์ƒ‰์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”:


<?php

namespace App\Http\Controllers;

use App\Services\ElasticsearchService;
use Illuminate\Http\Request;

class SearchController extends Controller
{
    protected $elasticsearch;
    
    public function __construct(ElasticsearchService $elasticsearch)
    {
        $this->elasticsearch = $elasticsearch;
    }
    
    /**
     * ์žฌ๋Šฅ ๊ฒ€์ƒ‰
     *
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function search(Request $request)
    {
        $this->validate($request, [
            'query' => 'required|string|min:2',
            'category' => 'nullable|string',
            'price_min' => 'nullable|numeric|min:0',
            'price_max' => 'nullable|numeric|min:0',
            'sort' => 'nullable|in:price_asc,price_desc,rating_desc,newest',
            'page' => 'nullable|integer|min:1',
            'per_page' => 'nullable|integer|min:1|max:100'
        ]);
        
        $page = $request->input('page', 1);
        $perPage = $request->input('per_page', 20);
        
        $searchParams = [
            'index' => 'talents',
            'body' => [
                'from' => ($page - 1) * $perPage,
                'size' => $perPage,
                'query' => [
                    'bool' => [
                        'must' => [
                            'multi_match' => [
                                'query' => $request->query,
                                'fields' => ['title^3', 'description', 'skills^2', 'tags']
                            ]
                        ],
                        'filter' => []
                    ]
                ]
            ]
        ];
        
        // ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ ์ถ”๊ฐ€
        if ($request->has('category')) {
            $searchParams['body']['query']['bool']['filter'][] = [
                'term' => ['category' => $request->category]
            ];
        }
        
        // ๊ฐ€๊ฒฉ ๋ฒ”์œ„ ํ•„ํ„ฐ ์ถ”๊ฐ€
        if ($request->has('price_min') || $request->has('price_max')) {
            $range = [];
            
            if ($request->has('price_min')) {
                $range['gte'] = $request->price_min;
            }
            
            if ($request->has('price_max')) {
                $range['lte'] = $request->price_max;
            }
            
            $searchParams['body']['query']['bool']['filter'][] = [
                'range' => ['price' => $range]
            ];
        }
        
        // ์ •๋ ฌ ์ถ”๊ฐ€
        if ($request->has('sort')) {
            switch ($request->sort) {
                case 'price_asc':
                    $searchParams['body']['sort'] = [['price' => 'asc']];
                    break;
                case 'price_desc':
                    $searchParams['body']['sort'] = [['price' => 'desc']];
                    break;
                case 'rating_desc':
                    $searchParams['body']['sort'] = [['average_rating' => 'desc']];
                    break;
                case 'newest':
                    $searchParams['body']['sort'] = [['created_at' => 'desc']];
                    break;
            }
        }
        
        $results = $this->elasticsearch->search($searchParams);
        
        return response()->json([
            'total' => $results['hits']['total']['value'],
            'page' => $page,
            'per_page' => $perPage,
            'results' => array_map(function ($hit) {
                return array_merge(
                    ['id' => $hit['_id']],
                    $hit['_source']
                );
            }, $results['hits']['hits'])
        ]);
    }
}
    

2. ๊ฒฐ์ œ ์ฒ˜๋ฆฌ ๊ธฐ๋Šฅ ๐Ÿ’ฐ

์žฌ๋Šฅ ๊ตฌ๋งค ์‹œ ๊ฒฐ์ œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ธฐ๋Šฅ์€ ๊ฒฐ์ œ ์„œ๋น„์Šค์—์„œ ๋‹ด๋‹นํ•ด์š”. ์™ธ๋ถ€ ๊ฒฐ์ œ ๊ฒŒ์ดํŠธ์›จ์ด์™€ ์—ฐ๋™ํ•˜์—ฌ ์•ˆ์ „ํ•œ ๊ฒฐ์ œ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”:


<?php

namespace App\Http\Controllers;

use App\Models\Payment;
use App\Services\PaymentGatewayService;
use App\Jobs\SendPaymentNotification;
use App\Jobs\UpdateOrderStatus;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class PaymentController extends Controller
{
    protected $paymentGateway;
    
    public function __construct(PaymentGatewayService $paymentGateway)
    {
        $this->paymentGateway = $paymentGateway;
    }
    
    /**
     * ๊ฒฐ์ œ ์ฒ˜๋ฆฌ
     *
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function processPayment(Request $request)
    {
        $this->validate($request, [
            'order_id' => 'required|string',
            'amount' => 'required|numeric|min:1',
            'payment_method' => 'required|in:card,bank,paypal',
            'card_number' => 'required_if:payment_method,card',
            'expiry_month' => 'required_if:payment_method,card',
            'expiry_year' => 'required_if:payment_method,card',
            'cvv' => 'required_if:payment_method,card'
        ]);
        
        try {
            DB::beginTransaction();
            
            // ์™ธ๋ถ€ ๊ฒฐ์ œ ๊ฒŒ์ดํŠธ์›จ์ด๋กœ ๊ฒฐ์ œ ์š”์ฒญ
            $paymentResult = $this->paymentGateway->processPayment(
                $request->payment_method,
                $request->amount,
                $request->all()
            );
            
            if (!$paymentResult['success']) {
                return response()->json([
                    'success' => false,
                    'message' => $paymentResult['message']
                ], 400);
            }
            
            // ๊ฒฐ์ œ ์ •๋ณด ์ €์žฅ
            $payment = Payment::create([
                'order_id' => $request->order_id,
                'transaction_id' => $paymentResult['transaction_id'],
                'amount' => $request->amount,
                'payment_method' => $request->payment_method,
                'status' => 'completed',
                'payment_details' => json_encode($paymentResult['details'])
            ]);
            
            DB::commit();
            
            // ๋น„๋™๊ธฐ ์ž‘์—… ํ์— ์ถ”๊ฐ€
            dispatch(new SendPaymentNotification($payment->id));
            dispatch(new UpdateOrderStatus($request->order_id, 'paid'));
            
            return response()->json([
                'success' => true,
                'message' => '๊ฒฐ์ œ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
                'payment_id' => $payment->id,
                'transaction_id' => $paymentResult['transaction_id']
            ]);
            
        } catch (\Exception $e) {
            DB::rollBack();
            
            return response()->json([
                'success' => false,
                'message' => '๊ฒฐ์ œ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.',
                'error' => $e->getMessage()
            ], 500);
        }
    }
    
    /**
     * ๊ฒฐ์ œ ์ƒํƒœ ์กฐํšŒ
     *
     * @param string $paymentId
     * @return \Illuminate\Http\JsonResponse
     */
    public function getPaymentStatus($paymentId)
    {
        $payment = Payment::findOrFail($paymentId);
        
        return response()->json([
            'payment_id' => $payment->id,
            'order_id' => $payment->order_id,
            'amount' => $payment->amount,
            'status' => $payment->status,
            'payment_method' => $payment->payment_method,
            'created_at' => $payment->created_at
        ]);
    }
}
    

3. ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ๊ธฐ๋Šฅ ๐Ÿ“ฑ

์ƒˆ๋กœ์šด ์ฃผ๋ฌธ์ด๋‚˜ ๋ฉ”์‹œ์ง€๊ฐ€ ๋„์ฐฉํ–ˆ์„ ๋•Œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ์„ ๋ณด๋‚ด๋Š” ๊ธฐ๋Šฅ์€ ์•Œ๋ฆผ ์„œ๋น„์Šค์—์„œ ๋‹ด๋‹นํ•ด์š”. ์ด๋ฉ”์ผ, ํ‘ธ์‹œ ์•Œ๋ฆผ, SMS ๋“ฑ ๋‹ค์–‘ํ•œ ์ฑ„๋„์„ ์ง€์›ํ•  ์ˆ˜ ์žˆ์–ด์š”:


<?php

namespace App\Services;

use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;

class NotificationService
{
    protected $client;
    
    public function __construct()
    {
        $this->client = new Client();
    }
    
    /**
     * ์ด๋ฉ”์ผ ์•Œ๋ฆผ ์ „์†ก
     *
     * @param string $email
     * @param string $subject
     * @param string $template
     * @param array $data
     * @return bool
     */
    public function sendEmail($email, $subject, $template, $data)
    {
        try {
            Mail::send($template, $data, function ($message) use ($email, $subject) {
                $message->to($email)->subject($subject);
            });
            
            return true;
        } catch (\Exception $e) {
            Log::error('์ด๋ฉ”์ผ ์ „์†ก ์‹คํŒจ', [
                'email' => $email,
                'subject' => $subject,
                'error' => $e->getMessage()
            ]);
            
            return false;
        }
    }
    
    /**
     * ํ‘ธ์‹œ ์•Œ๋ฆผ ์ „์†ก
     *
     * @param string $userId
     * @param string $title
     * @param string $body
     * @param array $data
     * @return bool
     */
    public function sendPushNotification($userId, $title, $body, $data = [])
    {
        try {
            // Firebase Cloud Messaging API ํ˜ธ์ถœ
            $response = $this->client->post(env('FCM_API_URL'), [
                'headers' => [
                    'Authorization' => 'key=' . env('FCM_SERVER_KEY'),
                    'Content-Type' => 'application/json'
                ],
                'json' => [
                    'to' => '/topics/user_' . $userId,
                    'notification' => [
                        'title' => $title,
                        'body' => $body,
                        'sound' => 'default'
                    ],
                    'data' => $data
                ]
            ]);
            
            $result = json_decode($response->getBody(), true);
            
            return $result['success'] == 1;
        } catch (\Exception $e) {
            Log::error('ํ‘ธ์‹œ ์•Œ๋ฆผ ์ „์†ก ์‹คํŒจ', [
                'user_id' => $userId,
                'title' => $title,
                'error' => $e->getMessage()
            ]);
            
            return false;
        }
    }
    
    /**
     * SMS ์•Œ๋ฆผ ์ „์†ก
     *
     * @param string $phoneNumber
     * @param string $message
     * @return bool
     */
    public function sendSms($phoneNumber, $message)
    {
        try {
            // SMS API ํ˜ธ์ถœ (์˜ˆ: Twilio)
            $response = $this->client->post(env('SMS_API_URL'), [
                'auth' => [env('SMS_API_SID'), env('SMS_API_TOKEN')],
                'form_params' => [
                    'From' => env('SMS_FROM_NUMBER'),
                    'To' => $phoneNumber,
                    'Body' => $message
                ]
            ]);
            
            return true;
        } catch (\Exception $e) {
            Log::error('SMS ์ „์†ก ์‹คํŒจ', [
                'phone' => $phoneNumber,
                'error' => $e->getMessage()
            ]);
            
            return false;
        }
    }
}
    

์„ฑ๋Šฅ ์ตœ์ ํ™” ์‚ฌ๋ก€ ๐Ÿš€

์žฌ๋Šฅ๋„ท๊ณผ ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋Š” ์„ฑ๋Šฅ ์ตœ์ ํ™”๊ฐ€ ๋งค์šฐ ์ค‘์š”ํ•ด์š”. ๋‹ค์Œ์€ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์ด์—์š”:

  1. API ์‘๋‹ต ์บ์‹ฑ: ์ž์ฃผ ์š”์ฒญ๋˜๋Š” API ์‘๋‹ต์„ Redis์— ์บ์‹ฑํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ถ€ํ•˜ ๊ฐ์†Œ
  2. ์ด๋ฏธ์ง€ ์ตœ์ ํ™”: CDN์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€ ๋ฐ ์ •์  ํŒŒ์ผ ์ œ๊ณต ์†๋„ ํ–ฅ์ƒ
  3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์‹ฑ: ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ํ•„๋“œ์— ์ธ๋ฑ์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ฟผ๋ฆฌ ์„ฑ๋Šฅ ํ–ฅ์ƒ
  4. ์ˆ˜ํ‰์  ํ™•์žฅ: ํŠธ๋ž˜ํ”ฝ์ด ๋งŽ์€ ์„œ๋น„์Šค๋Š” ์—ฌ๋Ÿฌ ์ธ์Šคํ„ด์Šค๋กœ ํ™•์žฅํ•˜์—ฌ ๋ถ€ํ•˜ ๋ถ„์‚ฐ
  5. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ: ๊ฒฐ์ œ ํ™•์ธ, ์ด๋ฉ”์ผ ์ „์†ก ๋“ฑ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์€ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌ

๐Ÿ’ก ์‹ค์ œ ์‚ฌ๋ก€ - ์žฌ๋Šฅ๋„ท์˜ ์„ฑ๋Šฅ ๊ฐœ์„ 

์žฌ๋Šฅ๋„ท ํ”Œ๋žซํผ์€ ์ดˆ๊ธฐ์— ๋ชจ๋†€๋ฆฌ์‹ ์•„ํ‚คํ…์ฒ˜๋กœ ๊ตฌ์ถ•๋˜์—ˆ์ง€๋งŒ, ์‚ฌ์šฉ์ž๊ฐ€ ์ฆ๊ฐ€ํ•˜๋ฉด์„œ ์„ฑ๋Šฅ ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”. Lumen ๊ธฐ๋ฐ˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋กœ ์ „ํ™˜ํ•œ ํ›„ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฐœ์„  ํšจ๊ณผ๋ฅผ ์–ป์—ˆ์Šต๋‹ˆ๋‹ค:

  • API ์‘๋‹ต ์‹œ๊ฐ„ 60% ๊ฐ์†Œ
  • ์„œ๋ฒ„ ์ž์› ์‚ฌ์šฉ๋ฅ  40% ๊ฐ์†Œ
  • ๋™์‹œ ์‚ฌ์šฉ์ž ์ฒ˜๋ฆฌ ๋Šฅ๋ ฅ 3๋ฐฐ ํ–ฅ์ƒ
  • ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถœ์‹œ ์ฃผ๊ธฐ 2์ฃผ์—์„œ 3์ผ๋กœ ๋‹จ์ถ•

ํŠนํžˆ ๊ฒ€์ƒ‰ ์„œ๋น„์Šค๋ฅผ ๋ณ„๋„๋กœ ๋ถ„๋ฆฌํ•˜๊ณ  Elasticsearch๋ฅผ ๋„์ž…ํ•œ ๊ฒฐ๊ณผ, ๋ณต์žกํ•œ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ์˜ ์‘๋‹ต ์‹œ๊ฐ„์ด 2์ดˆ์—์„œ 0.2์ดˆ๋กœ ๋‹จ์ถ•๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

10. ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์˜ ๋ฏธ๋ž˜ ๐Ÿ”ฎ

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋Š” ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด์š”. 2025๋…„ ํ˜„์žฌ์˜ ํŠธ๋ Œ๋“œ์™€ ์•ž์œผ๋กœ์˜ ๋ฐœ์ „ ๋ฐฉํ–ฅ์„ ์•Œ์•„๋ณผ๊ฒŒ์š”! ๐Ÿš€

ํ˜„์žฌ ํŠธ๋ Œ๋“œ (2025๋…„) ๐Ÿ“ˆ

2025๋…„ ํ˜„์žฌ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์—์„œ ์ฃผ๋ชฉ๋ฐ›๊ณ  ์žˆ๋Š” ํŠธ๋ Œ๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•„์š”:

  1. ์„œ๋ฒ„๋ฆฌ์Šค ์•„ํ‚คํ…์ฒ˜: AWS Lambda, Google Cloud Functions ๊ฐ™์€ ์„œ๋ฒ„๋ฆฌ์Šค ํ”Œ๋žซํผ์„ ํ™œ์šฉํ•œ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ตฌํ˜„
  2. ๋ฉ”์‹œ ์•„ํ‚คํ…์ฒ˜: ์„œ๋น„์Šค ๋ฉ”์‹œ(Istio, Linkerd)๋ฅผ ํ™œ์šฉํ•œ ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹  ๊ด€๋ฆฌ
  3. GitOps: Git์„ ์ค‘์‹ฌ์œผ๋กœ ํ•œ ์ธํ”„๋ผ ๋ฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฐํฌ ์ž๋™ํ™”
  4. eBPF: ์ปค๋„ ์ˆ˜์ค€์˜ ๊ด€์ฐฐ์„ฑ๊ณผ ๋„คํŠธ์›Œํ‚น ์ตœ์ ํ™”
  5. AI ๊ธฐ๋ฐ˜ ์šด์˜: ์ธ๊ณต์ง€๋Šฅ์„ ํ™œ์šฉํ•œ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์ตœ์ ํ™”

Lumen๋„ ์ด๋Ÿฌํ•œ ํŠธ๋ Œ๋“œ์— ๋งž์ถฐ ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด์š”. PHP 8.3์˜ ์„ฑ๋Šฅ ํ–ฅ์ƒ๊ณผ ํ•จ๊ป˜ Lumen์˜ ์†๋„์™€ ํšจ์œจ์„ฑ์ด ๋”์šฑ ๊ฐœ์„ ๋˜์—ˆ์–ด์š”!

2020 ์ปจํ…Œ์ด๋„ˆํ™” 2022 ์„œ๋น„์Šค ๋ฉ”์‹œ 2025 ์„œ๋ฒ„๋ฆฌ์Šค + AI 2027 ์ž์œจ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค 2030 ์–‘์ž ์ปดํ“จํŒ… ํ†ตํ•ฉ 2025๋…„ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํŠธ๋ Œ๋“œ โ€ข ์„œ๋ฒ„๋ฆฌ์Šค ์•„ํ‚คํ…์ฒ˜ โ€ข ๋ฉ”์‹œ ์•„ํ‚คํ…์ฒ˜ โ€ข GitOps โ€ข eBPF ๊ธฐ์ˆ  โ€ข AI ๊ธฐ๋ฐ˜ ์šด์˜ โ€ข ์ œ๋กœ ํŠธ๋Ÿฌ์ŠคํŠธ ๋ณด์•ˆ โ€ข WebAssembly โ€ข ์—ฃ์ง€ ์ปดํ“จํŒ… โ€ข ๋ถ„์‚ฐ ์ถ”์ 

์•ž์œผ๋กœ์˜ ๋ฐœ์ „ ๋ฐฉํ–ฅ ๐Ÿ”ญ

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋Š” ์•ž์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉํ–ฅ์œผ๋กœ ๋ฐœ์ „ํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ผ์š”:

  1. ์ž์œจ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค: AI๊ฐ€ ์„œ๋น„์Šค์˜ ํ™•์žฅ, ๋ณต๊ตฌ, ์ตœ์ ํ™”๋ฅผ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ
  2. ์—ฃ์ง€ ์ปดํ“จํŒ… ํ†ตํ•ฉ: ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉ์ž์™€ ๊ฐ€๊นŒ์šด ์—ฃ์ง€์— ๋ฐฐํฌํ•˜์—ฌ ์ง€์—ฐ ์‹œ๊ฐ„ ์ตœ์†Œํ™”
  3. WebAssembly: ์–ธ์–ด์— ๊ตฌ์• ๋ฐ›์ง€ ์•Š๋Š” ๊ณ ์„ฑ๋Šฅ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์‹คํ–‰ ํ™˜๊ฒฝ
  4. ์–‘์ž ์ปดํ“จํŒ… ํ†ตํ•ฉ: ํŠน์ • ์—ฐ์‚ฐ์— ์–‘์ž ์ปดํ“จํŒ…์„ ํ™œ์šฉํ•œ ์ดˆ๊ณ ์† ์ฒ˜๋ฆฌ
  5. ์ œ๋กœ ํŠธ๋Ÿฌ์ŠคํŠธ ๋ณด์•ˆ: ๋ชจ๋“  ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์— ๊ฐ•๋ ฅํ•œ ์ธ์ฆ๊ณผ ์•”ํ˜ธํ™” ์ ์šฉ

PHP์™€ Lumen๋„ ์ด๋Ÿฌํ•œ ๋ณ€ํ™”์— ์ ์‘ํ•˜๊ณ  ์žˆ์–ด์š”. JIT ์ปดํŒŒ์ผ๋Ÿฌ์˜ ๋ฐœ์ „๊ณผ ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์ง€์› ๊ฐ•ํ™”๋กœ ๋”์šฑ ๋น ๋ฅด๊ณ  ํšจ์œจ์ ์ธ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋  ๊ฑฐ์˜ˆ์š”!

Lumen์˜ ๋ฏธ๋ž˜ ๐ŸŒŸ

Lumen์€ ๊ฒฝ๋Ÿ‰ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํ”„๋ ˆ์ž„์›Œํฌ๋กœ์„œ ๊ณ„์†ํ•ด์„œ ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด์š”. ์•ž์œผ๋กœ์˜ Lumen์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠน์ง•์„ ๊ฐ€์งˆ ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ผ์š”:

  1. ๋” ๋น ๋ฅธ ์„ฑ๋Šฅ: PHP 8.3+์˜ JIT ์ปดํŒŒ์ผ๋Ÿฌ์™€ ์ตœ์ ํ™”๋œ ์ฝ”๋“œ๋กœ ๋”์šฑ ๋น ๋ฅธ ์„ฑ๋Šฅ ์ œ๊ณต
  2. ๋น„๋™๊ธฐ ์ง€์› ๊ฐ•ํ™”: ReactPHP, Swoole ๋“ฑ๊ณผ์˜ ํ†ตํ•ฉ์œผ๋กœ ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์ง€์› ๊ฐ•ํ™”
  3. ์„œ๋ฒ„๋ฆฌ์Šค ์ตœ์ ํ™”: AWS Lambda, Google Cloud Functions ๋“ฑ ์„œ๋ฒ„๋ฆฌ์Šค ํ™˜๊ฒฝ์— ์ตœ์ ํ™”๋œ ๋ฐฐํฌ ์ง€์›
  4. AI ํ†ตํ•ฉ: ์ธ๊ณต์ง€๋Šฅ ์„œ๋น„์Šค์™€์˜ ํ†ตํ•ฉ์„ ์œ„ํ•œ ๋„๊ตฌ ๋ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ œ๊ณต
  5. ๋ณด์•ˆ ๊ฐ•ํ™”: ์ œ๋กœ ํŠธ๋Ÿฌ์ŠคํŠธ ๋ชจ๋ธ์„ ์ง€์›ํ•˜๋Š” ๋ณด์•ˆ ๊ธฐ๋Šฅ ๊ฐ•ํ™”

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ

์ƒˆ๋กœ์šด ๊ธฐ์ˆ ์„ ๋„์ž…ํ•  ๋•Œ๋Š” ์•ˆ์ •์„ฑ๊ณผ ์„ฑ์ˆ™๋„๋ฅผ ๊ณ ๋ คํ•ด์•ผ ํ•ด์š”. ๋ชจ๋“  ์ตœ์‹  ํŠธ๋ Œ๋“œ๊ฐ€ ๋ชจ๋“  ํ”„๋กœ์ ํŠธ์— ์ ํ•ฉํ•œ ๊ฒƒ์€ ์•„๋‹ˆ์—์š”. ํ”„๋กœ์ ํŠธ์˜ ์š”๊ตฌ์‚ฌํ•ญ๊ณผ ํŒ€์˜ ์—ญ๋Ÿ‰์— ๋งž๋Š” ๊ธฐ์ˆ ์„ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด์š”!

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์˜ ๋„์ „ ๊ณผ์ œ ๐Ÿงฉ

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋Š” ๋งŽ์€ ์žฅ์ ์ด ์žˆ์ง€๋งŒ, ์—ฌ์ „ํžˆ ํ•ด๊ฒฐํ•ด์•ผ ํ•  ๋„์ „ ๊ณผ์ œ๋„ ์žˆ์–ด์š”:

  1. ๋ณต์žก์„ฑ ๊ด€๋ฆฌ: ์„œ๋น„์Šค๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ์ „์ฒด ์‹œ์Šคํ…œ์˜ ๋ณต์žก์„ฑ์ด ์ฆ๊ฐ€
  2. ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜: ์—ฌ๋Ÿฌ ์„œ๋น„์Šค์— ๊ฑธ์นœ ํŠธ๋žœ์žญ์…˜์˜ ์ผ๊ด€์„ฑ ์œ ์ง€
  3. ํ…Œ์ŠคํŠธ ๋ณต์žก์„ฑ: ์—ฌ๋Ÿฌ ์„œ๋น„์Šค ๊ฐ„์˜ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ ์–ด๋ ค์›€
  4. ์šด์˜ ์˜ค๋ฒ„ํ—ค๋“œ: ์—ฌ๋Ÿฌ ์„œ๋น„์Šค๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๋ถ€๋‹ด
  5. ํŒ€ ์กฐ์ง: ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์— ์ ํ•ฉํ•œ ํŒ€ ๊ตฌ์กฐ์™€ ํ˜‘์—… ๋ฐฉ์‹ ํ•„์š”

์ด๋Ÿฌํ•œ ๋„์ „ ๊ณผ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์„œ๋น„์Šค ๋ฉ”์‹œ, ๋ถ„์‚ฐ ์ถ”์ , ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ ๋“ฑ์˜ ๋„๊ตฌ์™€ ๋ฐฉ๋ฒ•๋ก ์ด ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด์š”!

๊ฒฐ๋ก  ๐ŸŽฏ

Lumen์„ ์ด์šฉํ•œ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋Š” ํ™•์žฅ์„ฑ, ์œ ์—ฐ์„ฑ, ๊ฐœ๋ฐœ ์†๋„๋ฅผ ์ค‘์š”์‹œํ•˜๋Š” ํ˜„๋Œ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์— ๋งค์šฐ ์ ํ•ฉํ•ด์š”. ํŠนํžˆ ์žฌ๋Šฅ๋„ท๊ณผ ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋Š” ๊ฐ ๊ธฐ๋Šฅ์„ ๋…๋ฆฝ์ ์œผ๋กœ ๊ฐœ๋ฐœํ•˜๊ณ  ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์–ด ๋น ๋ฅด๊ฒŒ ๋ณ€ํ™”ํ•˜๋Š” ์‹œ์žฅ์— ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ์–ด์š”!

PHP์™€ Lumen์ด ๊ฐ€๋ณ๊ณ  ๋น ๋ฅด๋‹ค๋Š” ์žฅ์ ์„ ์‚ด๋ ค ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค๋ฉด, ๋†’์€ ์„ฑ๋Šฅ๊ณผ ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์„ ๋ชจ๋‘ ์–ป์„ ์ˆ˜ ์žˆ์–ด์š”. ์•ž์œผ๋กœ๋„ ๊ณ„์†ํ•ด์„œ ๋ฐœ์ „ํ•˜๋Š” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ธฐ์ˆ ๊ณผ ํ•จ๊ป˜ Lumen๋„ ๋”์šฑ ๊ฐ•๋ ฅํ•ด์งˆ ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€๋ผ์š”! ๐Ÿ˜Š

๐Ÿ’ก ๋งˆ์ง€๋ง‰ ํŒ!

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

๋งˆ๋ฌด๋ฆฌ ๐Ÿ

์ง€๊ธˆ๊นŒ์ง€ Lumen์„ ์ด์šฉํ•œ ๊ฒฝ๋Ÿ‰ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ๊ตฌํ˜„์— ๋Œ€ํ•ด ์•Œ์•„๋ดค์–ด์š”! ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ํ•จ๊ป˜ ํ•ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ด์š”. ์ด ๊ธ€์ด ์—ฌ๋Ÿฌ๋ถ„์˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์—ฌ์ •์— ๋„์›€์ด ๋˜์—ˆ๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค! ๐Ÿ˜Š

์žฌ๋Šฅ๋„ท์—์„œ๋Š” ๋‹ค์–‘ํ•œ ๊ฐœ๋ฐœ ๊ด€๋ จ ์ง€์‹๊ณผ ๋…ธํ•˜์šฐ๋ฅผ ๊ณต์œ ํ•˜๊ณ  ์žˆ์–ด์š”. ๊ฐœ๋ฐœ์ž๋กœ์„œ์˜ ์žฌ๋Šฅ์„ ๋‚˜๋ˆ„๊ณ  ์‹ถ๊ฑฐ๋‚˜, ํ•„์š”ํ•œ ๊ฐœ๋ฐœ ์„œ๋น„์Šค๋ฅผ ์ฐพ๊ณ  ๊ณ„์‹œ๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท(https://www.jaenung.net)์„ ๋ฐฉ๋ฌธํ•ด๋ณด์„ธ์š”!

๋” ๋งŽ์€ ๊ฐœ๋ฐœ ๊ด€๋ จ ์ง€์‹์€ ์žฌ๋Šฅ๋„ท์˜ '์ง€์‹์ธ์˜ ์ˆฒ' ๋ฉ”๋‰ด์—์„œ ์ฐพ์•„๋ณด์‹ค ์ˆ˜ ์žˆ์–ด์š”. ์—ฌ๋Ÿฌ๋ถ„์˜ ๊ฐœ๋ฐœ ์—ฌ์ •์— ํ•ญ์ƒ ํ–‰์šด์ด ํ•จ๊ป˜ํ•˜๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค! ๐Ÿ‘‹

Happy Coding! with Lumen & Microservices
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.