์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ ์Šค์œ„ํ”„ํŠธ๋กœ ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ! Vapor & Perfect ์™„์ „์ •๋ณต

๐Ÿš€ ์Šค์œ„ํ”„ํŠธ๋กœ ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ! Vapor & Perfect ์™„์ „์ •๋ณต

iOS ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ฐฑ์—”๋“œ๊นŒ์ง€? ์ด์ œ Swift๋กœ ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž ๋˜๋Š” ๋ฒ• ๐ŸŽฏ

## ๐ŸŽฌ Swift๊ฐ€ ์„œ๋ฒ„ ๊ฐœ๋ฐœ๊นŒ์ง€ ํ•œ๋‹ค๊ณ ? ์•ผ, ๋„ˆ Swift ์•Œ์ง€? ๊ทธ ์•„์ดํฐ ์•ฑ ๋งŒ๋“ค ๋•Œ ์“ฐ๋Š” ๊ทธ ์–ธ์–ด ๋ง์ด์•ผ! ๊ทผ๋ฐ ๋ง์ด์•ผ, ์ด Swift๊ฐ€ ์ด์ œ ์„œ๋ฒ„ ๊ฐœ๋ฐœ๊นŒ์ง€ ํ•œ๋‹ค๋Š” ๊ฑฐ ์•Œ์•„? ๐Ÿ˜ฒ

์ฒ˜์Œ ๋“ค์œผ๋ฉด "๋ญ? Swift๋กœ ์„œ๋ฒ„๋ฅผ?" ์ด๋Ÿฐ ๋ฐ˜์‘ ๋‚˜์˜ฌ ์ˆ˜ ์žˆ์–ด. ๋‚˜๋„ ์ฒ˜์Œ์—” ๊ทธ๋žฌ๊ฑฐ๋“ . ๊ทผ๋ฐ ์ƒ๊ฐํ•ด๋ณด๋ฉด ์™„์ „ ํ•ฉ๋ฆฌ์ ์ด์•ผ. ํ”„๋ก ํŠธ์—”๋“œ(iOS ์•ฑ)๋ž‘ ๋ฐฑ์—”๋“œ(์„œ๋ฒ„)๋ฅผ ๊ฐ™์€ ์–ธ์–ด๋กœ ๊ฐœ๋ฐœํ•˜๋ฉด ์–ผ๋งˆ๋‚˜ ํŽธํ•ด? ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ๋„ ๋˜๊ณ , ํŒ€ ๋‚ด์—์„œ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜๋„ ํ›จ์”ฌ ์ˆ˜์›”ํ•˜๊ณ !

2015๋…„ ์• ํ”Œ์ด Swift๋ฅผ ์˜คํ”ˆ์†Œ์Šค๋กœ ๊ณต๊ฐœํ•˜๋ฉด์„œ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ Swift์˜ ๊ฐ€๋Šฅ์„ฑ์ด ์—ด๋ ธ์–ด. ๊ทธ๋ฆฌ๊ณ  ์ง€๊ธˆ์€ **Vapor**์™€ **Perfect** ๊ฐ™์€ ๊ฐ•๋ ฅํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋“ค์ด ๋‚˜์™€์„œ ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋„ ์ถฉ๋ถ„ํžˆ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์ด ๋์ง€. ๐Ÿ”ฅ

๐Ÿ’ก ์™œ Swift๋กœ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค๊นŒ? โ€ข ํƒ€์ž… ์•ˆ์ •์„ฑ: ์ปดํŒŒ์ผ ํƒ€์ž„์— ์—๋Ÿฌ๋ฅผ ์žก์•„๋‚ด๋‹ˆ๊นŒ ๋Ÿฐํƒ€์ž„ ๋ฒ„๊ทธ๊ฐ€ ํ™• ์ค„์–ด๋“ค์–ด
โ€ข ์„ฑ๋Šฅ: C/C++ ์ˆ˜์ค€์˜ ๋น ๋ฅธ ์†๋„๋ฅผ ์ž๋ž‘ํ•ด
โ€ข ๋ชจ๋˜ํ•œ ๋ฌธ๋ฒ•: ์˜ต์…”๋„, ํด๋กœ์ €, ํ”„๋กœํ† ์ฝœ ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋“ฑ ๊ฐœ๋ฐœ์ž ์นœํ™”์ 
โ€ข ์ฝ”๋“œ ๊ณต์œ : iOS/macOS ์•ฑ๊ณผ ์„œ๋ฒ„ ๊ฐ„ ๋ชจ๋ธ ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ๊ณต์œ  ๊ฐ€๋Šฅ
โ€ข ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์ „์„ฑ: ARC(Automatic Reference Counting)๋กœ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๊ฐ€ ์ž๋™ํ™”๋ผ
์‹ค์ œ๋กœ IBM ๊ฐ™์€ ๋Œ€๊ธฐ์—…๋„ Swift ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๊ฐœ๋ฐœ์— ํˆฌ์žํ–ˆ์—ˆ๊ณ (Kitura ํ”„๋ ˆ์ž„์›Œํฌ), ์ง€๊ธˆ๋„ ๋งŽ์€ ์Šคํƒ€ํŠธ์—…๋“ค์ด Swift ๋ฐฑ์—”๋“œ๋ฅผ ์„ ํƒํ•˜๊ณ  ์žˆ์–ด. ํŠนํžˆ iOS ์ค‘์‹ฌ์˜ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“œ๋Š” ํŒ€์ด๋ผ๋ฉด ๋”๋”์šฑ ๋งค๋ ฅ์ ์ธ ์„ ํƒ์ง€์ง€!
Swift Vapor Framework Perfect Framework Backend Server RESTful API Database
## ๐ŸŒŸ Vapor vs Perfect: ์–ด๋–ค ๊ฑธ ์„ ํƒํ• ๊นŒ? ์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ๋‘ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๋น„๊ตํ•ด๋ณผ๊นŒ? Vapor์™€ Perfect๋Š” Swift ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๊ฐœ๋ฐœ์˜ ์–‘๋Œ€ ์‚ฐ๋งฅ์ด์•ผ. ๊ฐ๊ฐ ์žฅ๋‹จ์ ์ด ์žˆ์œผ๋‹ˆ๊นŒ ๋„ค ํ”„๋กœ์ ํŠธ์— ๋งž๋Š” ๊ฑธ ์„ ํƒํ•˜๋ฉด ๋ผ! ๐Ÿค”

### ๐Ÿ”ต Vapor - ํ˜„๋Œ€์ ์ด๊ณ  ํ™œ๋ฐœํ•œ ์ปค๋ฎค๋‹ˆํ‹ฐ Vapor๋Š” 2016๋…„์— ์ฒ˜์Œ ๋“ฑ์žฅํ–ˆ๊ณ , ํ˜„์žฌ ๊ฐ€์žฅ ์ธ๊ธฐ ์žˆ๋Š” Swift ์„œ๋ฒ„ ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. GitHub ์Šคํƒ€๊ฐ€ 2๋งŒ ๊ฐœ๊ฐ€ ๋„˜์„ ์ •๋„๋กœ ์ปค๋ฎค๋‹ˆํ‹ฐ๊ฐ€ ํ™œ๋ฐœํ•ด.

โœจ Vapor์˜ ์ฃผ์š” ํŠน์ง• 1. ๋ชจ๋˜ํ•œ ์•„ํ‚คํ…์ฒ˜
Async/Await๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ์ง€์›ํ•˜๊ณ , Swift Concurrency ๋ชจ๋ธ์„ ์ ๊ทน ํ™œ์šฉํ•ด. ๋•๋ถ„์— ๋น„๋™๊ธฐ ์ฝ”๋“œ๋ฅผ ๋งˆ์น˜ ๋™๊ธฐ ์ฝ”๋“œ์ฒ˜๋Ÿผ ๊น”๋”ํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด.

2. ํ’๋ถ€ํ•œ ์ƒํƒœ๊ณ„
ORM(Fluent), ํ…œํ”Œ๋ฆฟ ์—”์ง„(Leaf), ์ธ์ฆ ์‹œ์Šคํ…œ, WebSocket ์ง€์› ๋“ฑ ํ•„์š”ํ•œ ๊ฑฐ์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ๊ณต์‹ ํŒจํ‚ค์ง€๋กœ ์ œ๊ณต๋ผ. ๋ฐ”ํ€ด๋ฅผ ๋‹ค์‹œ ๋ฐœ๋ช…ํ•  ํ•„์š”๊ฐ€ ์—†์ง€!

3. ํ›Œ๋ฅญํ•œ ๋ฌธ์„œํ™”
๊ณต์‹ ๋ฌธ์„œ๊ฐ€ ์ •๋ง ์ž˜ ๋˜์–ด ์žˆ์–ด. ํŠœํ† ๋ฆฌ์–ผ๋ถ€ํ„ฐ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๊นŒ์ง€ ๋‹จ๊ณ„๋ณ„๋กœ ์„ค๋ช…๋˜์–ด ์žˆ์–ด์„œ ์ดˆ๋ณด์ž๋„ ์‰ฝ๊ฒŒ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์–ด.

4. ํ™œ๋ฐœํ•œ ์—…๋ฐ์ดํŠธ
Swift์˜ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์ด ๋‚˜์˜ค๋ฉด ๋น ๋ฅด๊ฒŒ ์ ์šฉ๋ผ. Swift 5.5์˜ async/await๋„ ๋ฐ”๋กœ ์ง€์›ํ–ˆ๊ณ , ์ตœ์‹  ํŠธ๋ Œ๋“œ๋ฅผ ์ž˜ ๋”ฐ๋ผ๊ฐ€.
### ๐ŸŸฃ Perfect - ์•ˆ์ •์ ์ด๊ณ  ์„ฑ์ˆ™ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ Perfect๋Š” Vapor๋ณด๋‹ค ์กฐ๊ธˆ ๋จผ์ € ๋‚˜์™”๊ณ , ์ดˆ๊ธฐ์—๋Š” ๊ฐ€์žฅ ์ธ๊ธฐ ์žˆ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์˜€์–ด. ์ง€๊ธˆ์€ Vapor์— ๋ฐ€๋ฆฌ๋Š” ๊ฐ์ด ์žˆ์ง€๋งŒ, ์—ฌ์ „ํžˆ ๊ฐ•๋ ฅํ•œ ์„ ํƒ์ง€์•ผ.

โœจ Perfect์˜ ์ฃผ์š” ํŠน์ง• 1. ์„ฑ์ˆ™ํ•œ ์•ˆ์ •์„ฑ
์˜ค๋žœ ๊ธฐ๊ฐ„ ๊ฒ€์ฆ๋œ ํ”„๋ ˆ์ž„์›Œํฌ๋ผ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์•ˆ์ •์ ์œผ๋กœ ๋™์ž‘ํ•ด. ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฒ„๊ทธ๊ฐ€ ์ ์–ด.

2. ๋ชจ๋“ˆ์‹ ์„ค๊ณ„
ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋งŒ ๊ณจ๋ผ์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ๊ฐ€๋ฒผ์šด API ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ๋‹ค๋ฉด ์ตœ์†Œํ•œ์˜ ๋ชจ๋“ˆ๋งŒ ํฌํ•จ์‹œํ‚ฌ ์ˆ˜ ์žˆ์ง€.

3. C ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ†ตํ•ฉ
๊ธฐ์กด C/C++ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€์˜ ํ†ตํ•ฉ์ด ์ƒ๋Œ€์ ์œผ๋กœ ์‰ฌ์›Œ. ๋ ˆ๊ฑฐ์‹œ ์‹œ์Šคํ…œ๊ณผ ์—ฐ๋™ํ•ด์•ผ ํ•œ๋‹ค๋ฉด ์œ ๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด.

4. ๋‚ฎ์€ ๋Ÿฌ๋‹ ์ปค๋ธŒ
API๊ฐ€ ๋น„๊ต์  ๋‹จ์ˆœํ•˜๊ณ  ์ง๊ด€์ ์ด์•ผ. ๋ณต์žกํ•œ ์ถ”์ƒํ™”๊ฐ€ ์ ์–ด์„œ ๋‚ด๋ถ€ ๋™์ž‘์„ ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์›Œ.
๐ŸŽฏ ์„ ํƒ ๊ฐ€์ด๋“œ Vapor๋ฅผ ์„ ํƒํ•ด์•ผ ํ•  ๋•Œ:
โ€ข ์ตœ์‹  Swift ๊ธฐ๋Šฅ์„ ์ ๊ทน ํ™œ์šฉํ•˜๊ณ  ์‹ถ์„ ๋•Œ
โ€ข ํ’๋ถ€ํ•œ ์ปค๋ฎค๋‹ˆํ‹ฐ ์ง€์›์ด ํ•„์š”ํ•  ๋•Œ
โ€ข ๋น ๋ฅด๊ฒŒ ํ”„๋กœํ† ํƒ€์ž…์„ ๋งŒ๋“ค๊ณ  ์‹ถ์„ ๋•Œ
โ€ข ์žฅ๊ธฐ์ ์ธ ์œ ์ง€๋ณด์ˆ˜๋ฅผ ๊ณ ๋ คํ•  ๋•Œ

Perfect๋ฅผ ์„ ํƒํ•ด์•ผ ํ•  ๋•Œ:
โ€ข ์•ˆ์ •์„ฑ์ด ์ตœ์šฐ์„ ์ผ ๋•Œ
โ€ข ๊ฐ€๋ฒผ์šด ์„œ๋ฒ„๊ฐ€ ํ•„์š”ํ•  ๋•Œ
โ€ข C ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ†ตํ•ฉ์ด ํ•„์š”ํ•  ๋•Œ
โ€ข ๋‹จ์ˆœํ•œ API ๊ตฌ์กฐ๋ฅผ ์„ ํ˜ธํ•  ๋•Œ
์†”์งํžˆ ๋งํ•˜๋ฉด, 2024๋…„ ํ˜„์žฌ ์‹œ์ ์—์„œ๋Š” **Vapor๋ฅผ ์ถ”์ฒœ**ํ•ด. ์ปค๋ฎค๋‹ˆํ‹ฐ๊ฐ€ ํ›จ์”ฌ ํ™œ๋ฐœํ•˜๊ณ , ๋ฌธ์„œ๋„ ์ž˜ ๋˜์–ด ์žˆ๊ณ , ์ตœ์‹  ๊ธฐ๋Šฅ ์ง€์›๋„ ๋น ๋ฅด๊ฑฐ๋“ . ํ•˜์ง€๋งŒ Perfect๋„ ์—ฌ์ „ํžˆ ํ›Œ๋ฅญํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋‹ˆ๊นŒ ํ”„๋กœ์ ํŠธ ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์„ ํƒํ•˜๋ฉด ๋ผ!
## ๐Ÿš€ Vapor ์‹œ์ž‘ํ•˜๊ธฐ: ์ฒซ ํ”„๋กœ์ ํŠธ ๋งŒ๋“ค๊ธฐ ์ž, ์ด์ œ ์‹ค์ „์œผ๋กœ ๋“ค์–ด๊ฐ€๋ณผ๊นŒ? Vapor๋กœ ์ฒซ ๋ฒˆ์งธ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ์ƒ๊ฐ๋ณด๋‹ค ํ›จ์”ฌ ์‰ฌ์›Œ. ๐Ÿ˜Ž

### ๐Ÿ“ฆ ์„ค์น˜ ๋ฐ ํ™˜๊ฒฝ ์„ค์ • ๋จผ์ € Vapor๋ฅผ ์„ค์น˜ํ•ด์•ผ ํ•ด. macOS๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด Homebrew๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์–ด:

brew install vapor

Linux๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ๊ณต์‹ ๋ฌธ์„œ์˜ ์„ค์น˜ ๊ฐ€์ด๋“œ๋ฅผ ๋”ฐ๋ผ๊ฐ€๋ฉด ๋ผ. Swift 5.7 ์ด์ƒ์ด ํ•„์š”ํ•˜๋‹ˆ๊นŒ Swift ๋ฒ„์ „๋„ ํ™•์ธํ•ด์ค˜!

์„ค์น˜๊ฐ€ ๋๋‚ฌ์œผ๋ฉด ์ƒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž:

vapor new MyFirstServer
cd MyFirstServer
vapor build

๐ŸŽจ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์‚ดํŽด๋ณด๊ธฐ Vapor ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ์ด๋Ÿฐ ๊ตฌ์กฐ๊ฐ€ ๋งŒ๋“ค์–ด์ ธ:

Sources/App/ - ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ
Controllers/ - ๋ผ์šฐํŠธ ํ•ธ๋“ค๋Ÿฌ๋“ค
Models/ - ๋ฐ์ดํ„ฐ ๋ชจ๋ธ
Migrations/ - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
configure.swift - ์•ฑ ์„ค์ •
routes.swift - ๋ผ์šฐํŠธ ์ •์˜

์ด ๊ตฌ์กฐ๋Š” MVC ํŒจํ„ด์„ ๋”ฐ๋ฅด๊ณ  ์žˆ์–ด์„œ ์ต์ˆ™ํ•  ๊ฑฐ์•ผ!
### ๐ŸŽฏ ์ฒซ ๋ฒˆ์งธ API ์—”๋“œํฌ์ธํŠธ ๋งŒ๋“ค๊ธฐ ์ด์ œ ๊ฐ„๋‹จํ•œ "Hello, World!" API๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. routes.swift ํŒŒ์ผ์„ ์—ด์–ด๋ด:

import Vapor

func routes(_ app: Application) throws {
    // GET /hello ์—”๋“œํฌ์ธํŠธ
    app.get("hello") { req async -> String in
        return "Hello, Vapor! ๐Ÿš€"
    }
    
    // GET /hello/:name ์—”๋“œํฌ์ธํŠธ (ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ฌ์šฉ)
    app.get("hello", ":name") { req async throws -> String in
        guard let name = req.parameters.get("name") else {
            throw Abort(.badRequest)
        }
        return "Hello, \(name)! ๐Ÿ‘‹"
    }
    
    // POST /user ์—”๋“œํฌ์ธํŠธ (JSON ๋ฐ›๊ธฐ)
    app.post("user") { req async throws -> User in
        let user = try req.content.decode(User.self)
        // ์—ฌ๊ธฐ์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด
        return user
    }
}

// User ๋ชจ๋ธ ์ •์˜
struct User: Content {
    var name: String
    var email: String
    var age: Int
}

๋ณด๋‹ค์‹œํ”ผ ์ฝ”๋“œ๊ฐ€ ์—„์ฒญ ์ง๊ด€์ ์ด์ง€? async/await๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋น„๋™๊ธฐ ์ฝ”๋“œ๋„ ๊น”๋”ํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด.

์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•ด๋ณด์ž:

vapor run

์ด์ œ ๋ธŒ๋ผ์šฐ์ €๋‚˜ Postman์—์„œ http://localhost:8080/hello์— ์ ‘์†ํ•˜๋ฉด "Hello, Vapor! ๐Ÿš€"๊ฐ€ ๋ณด์ผ ๊ฑฐ์•ผ!
## ๐Ÿ’พ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๋™: Fluent ORM ํ™œ์šฉํ•˜๊ธฐ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค๋ฉด ๋‹น์—ฐํžˆ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ํ•„์š”ํ•˜์ง€? Vapor๋Š” **Fluent**๋ผ๋Š” ์ž์ฒด ORM์„ ์ œ๊ณตํ•ด. PostgreSQL, MySQL, SQLite, MongoDB ๋“ฑ ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ง€์›ํ•ด! ๐Ÿ—„๏ธ

### ๐Ÿ”ง Fluent ์„ค์ •ํ•˜๊ธฐ ๋จผ์ € Package.swift์— Fluent ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด. PostgreSQL์„ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๋ฉด:

dependencies: [
    .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
    .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
    .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0")
]

๊ทธ ๋‹ค์Œ configure.swift์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์„ค์ •ํ•ด:

import Fluent
import FluentPostgresDriver
import Vapor

public func configure(_ app: Application) throws {
    // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •
    app.databases.use(.postgres(
        hostname: Environment.get("DATABASE_HOST") ?? "localhost",
        port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? 5432,
        username: Environment.get("DATABASE_USERNAME") ?? "vapor",
        password: Environment.get("DATABASE_PASSWORD") ?? "password",
        database: Environment.get("DATABASE_NAME") ?? "vapor_db"
    ), as: .psql)
    
    // ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ถ”๊ฐ€
    app.migrations.add(CreateUser())
    
    // ๋ผ์šฐํŠธ ๋“ฑ๋ก
    try routes(app)
}

๐Ÿ” ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์‚ฌ์šฉํ•˜๊ธฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฐ™์€ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋ฉด ์•ˆ ๋ผ! Environment.get()์„ ์‚ฌ์šฉํ•ด์„œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒŒ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค์•ผ.
### ๐Ÿ“ ๋ชจ๋ธ ์ •์˜ํ•˜๊ธฐ Fluent์—์„œ ๋ชจ๋ธ์„ ์ •์˜ํ•˜๋Š” ๊ฑด ์ •๋ง ๊ฐ„๋‹จํ•ด. Model ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•˜๋ฉด ๋ผ:

import Fluent
import Vapor

final class User: Model, Content {
    static let schema = "users"
    
    @ID(key: .id)
    var id: UUID?
    
    @Field(key: "name")
    var name: String
    
    @Field(key: "email")
    var email: String
    
    @Field(key: "age")
    var age: Int
    
    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?
    
    @Timestamp(key: "updated_at", on: .update)
    var updatedAt: Date?
    
    init() { }
    
    init(id: UUID? = nil, name: String, email: String, age: Int) {
        self.id = id
        self.name = name
        self.email = email
        self.age = age
    }
}

์—ฌ๊ธฐ์„œ ์ฃผ๋ชฉํ•  ์ ๋“ค:
โ€ข @ID: ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜๋Š” UUID ๊ธฐ๋ณธ ํ‚ค
โ€ข @Field: ์ผ๋ฐ˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ•„๋“œ
โ€ข @Timestamp: ์ƒ์„ฑ/์ˆ˜์ • ์‹œ๊ฐ„ ์ž๋™ ๊ด€๋ฆฌ

### ๐Ÿ”„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋งŒ๋“ค๊ธฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ๋ฅผ ์ƒ์„ฑํ•˜๋ ค๋ฉด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ •์˜ํ•ด์•ผ ํ•ด:

import Fluent

struct CreateUser: AsyncMigration {
    func prepare(on database: Database) async throws {
        try await database.schema("users")
            .id()
            .field("name", .string, .required)
            .field("email", .string, .required)
            .field("age", .int, .required)
            .field("created_at", .datetime)
            .field("updated_at", .datetime)
            .unique(on: "email")  // ์ด๋ฉ”์ผ ์ค‘๋ณต ๋ฐฉ์ง€
            .create()
    }
    
    func revert(on database: Database) async throws {
        try await database.schema("users").delete()
    }
}

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์‹คํ–‰ํ•˜๋ ค๋ฉด:

vapor run migrate

### ๐ŸŽฎ CRUD ์ž‘์—… ๊ตฌํ˜„ํ•˜๊ธฐ ์ด์ œ ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” API๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž:

import Vapor

func routes(_ app: Application) throws {
    let users = app.grouped("users")
    
    // CREATE - ์ƒˆ ์‚ฌ์šฉ์ž ์ƒ์„ฑ
    users.post { req async throws -> User in
        let user = try req.content.decode(User.self)
        try await user.save(on: req.db)
        return user
    }
    
    // READ - ๋ชจ๋“  ์‚ฌ์šฉ์ž ์กฐํšŒ
    users.get { req async throws -> [User] in
        try await User.query(on: req.db).all()
    }
    
    // READ - ํŠน์ • ์‚ฌ์šฉ์ž ์กฐํšŒ
    users.get(":id") { req async throws -> User in
        guard let user = try await User.find(req.parameters.get("id"), on: req.db) else {
            throw Abort(.notFound)
        }
        return user
    }
    
    // UPDATE - ์‚ฌ์šฉ์ž ์ •๋ณด ์ˆ˜์ •
    users.put(":id") { req async throws -> User in
        guard let user = try await User.find(req.parameters.get("id"), on: req.db) else {
            throw Abort(.notFound)
        }
        
        let updatedUser = try req.content.decode(User.self)
        user.name = updatedUser.name
        user.email = updatedUser.email
        user.age = updatedUser.age
        
        try await user.save(on: req.db)
        return user
    }
    
    // DELETE - ์‚ฌ์šฉ์ž ์‚ญ์ œ
    users.delete(":id") { req async throws -> HTTPStatus in
        guard let user = try await User.find(req.parameters.get("id"), on: req.db) else {
            throw Abort(.notFound)
        }
        
        try await user.delete(on: req.db)
        return .noContent
    }
}

์™„์ „ ๊น”๋”ํ•˜์ง€? ๐Ÿ˜ Fluent์˜ ์ฟผ๋ฆฌ ๋นŒ๋”๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ณต์žกํ•œ SQL ์—†์ด๋„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—…์„ ํ•  ์ˆ˜ ์žˆ์–ด!
Vapor Architecture Flow Client Request HTTP/JSON Router routes.swift Controller Business Logic Model Fluent ORM Database PostgreSQL/MySQL Response JSON/HTML
## ๐Ÿ” ์ธ์ฆ๊ณผ ๋ณด์•ˆ: JWT ํ† ํฐ ๊ตฌํ˜„ํ•˜๊ธฐ API๋ฅผ ๋งŒ๋“ค๋ฉด ๋‹น์—ฐํžˆ ๋ณด์•ˆ์ด ์ค‘์š”ํ•˜์ง€! ๋ˆ„๊ตฌ๋‚˜ ๋ง‰ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์œผ๋ฉด ์•ˆ ๋˜๋‹ˆ๊นŒ. Vapor์—์„œ๋Š” JWT(JSON Web Token)๋ฅผ ์‚ฌ์šฉํ•œ ์ธ์ฆ์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿ”’

### ๐ŸŽซ JWT๋ž€? JWT๋Š” ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ „๋‹ฌํ•˜๋Š” ํ† ํฐ ๋ฐฉ์‹์ด์•ผ. ์„œ๋ฒ„๊ฐ€ ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ๋Š” ์ด ํ† ํฐ์„ ํ—ค๋”์— ํฌํ•จ์‹œ์ผœ์„œ ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๊ฑฐ์ง€. ์„ธ์…˜ ๋ฐฉ์‹๋ณด๋‹ค ํ™•์žฅ์„ฑ์ด ์ข‹์•„์„œ RESTful API์—์„œ ๋งŽ์ด ์‚ฌ์šฉํ•ด.

### ๐Ÿ“ฆ JWT ํŒจํ‚ค์ง€ ์ถ”๊ฐ€ํ•˜๊ธฐ ๋จผ์ € Package.swift์— JWT ํŒจํ‚ค์ง€๋ฅผ ์ถ”๊ฐ€ํ•ด:

dependencies: [
    .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0")
]

### ๐Ÿ”‘ JWT ์„ค์ •ํ•˜๊ธฐ configure.swift์—์„œ JWT ์‹œํฌ๋ฆฟ ํ‚ค๋ฅผ ์„ค์ •ํ•ด:

import JWT

public func configure(_ app: Application) throws {
    // JWT ์‹œํฌ๋ฆฟ ํ‚ค ์„ค์ • (ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ)
    app.jwt.signers.use(.hs256(key: Environment.get("JWT_SECRET") ?? "secret-key"))
    
    // ... ๋‚˜๋จธ์ง€ ์„ค์ •
}

โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ JWT ์‹œํฌ๋ฆฟ ํ‚ค๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋ฉด ์•ˆ ๋ผ! ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์ถฉ๋ถ„ํžˆ ๋ณต์žกํ•œ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. ์ตœ์†Œ 32์ž ์ด์ƒ์˜ ๋žœ๋ค ๋ฌธ์ž์—ด์„ ์ถ”์ฒœํ•ด!
### ๐Ÿ‘ค ์‚ฌ์šฉ์ž ์ธ์ฆ ๊ตฌํ˜„ํ•˜๊ธฐ ์ด์ œ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด๋ณด์ž:

import Vapor
import JWT

// JWT ํŽ˜์ด๋กœ๋“œ ์ •์˜
struct UserPayload: JWTPayload {
    var userId: UUID
    var email: String
    var exp: ExpirationClaim  // ๋งŒ๋ฃŒ ์‹œ๊ฐ„
    
    func verify(using signer: JWTSigner) throws {
        try exp.verifyNotExpired()
    }
}

// ๋กœ๊ทธ์ธ ์š”์ฒญ ๊ตฌ์กฐ์ฒด
struct LoginRequest: Content {
    var email: String
    var password: String
}

// ๋กœ๊ทธ์ธ ์‘๋‹ต ๊ตฌ์กฐ์ฒด
struct LoginResponse: Content {
    var token: String
    var user: User.Public
}

extension User {
    struct Public: Content {
        var id: UUID?
        var name: String
        var email: String
    }
    
    var publicView: Public {
        Public(id: id, name: name, email: email)
    }
}

func routes(_ app: Application) throws {
    // ํšŒ์›๊ฐ€์ž…
    app.post("register") { req async throws -> User.Public in
        let user = try req.content.decode(User.self)
        
        // ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ (Bcrypt ์‚ฌ์šฉ)
        user.passwordHash = try Bcrypt.hash(user.password)
        
        try await user.save(on: req.db)
        return user.publicView
    }
    
    // ๋กœ๊ทธ์ธ
    app.post("login") { req async throws -> LoginResponse in
        let loginRequest = try req.content.decode(LoginRequest.self)
        
        // ์ด๋ฉ”์ผ๋กœ ์‚ฌ์šฉ์ž ์ฐพ๊ธฐ
        guard let user = try await User.query(on: req.db)
            .filter(\.$email == loginRequest.email)
            .first() else {
            throw Abort(.unauthorized, reason: "Invalid email or password")
        }
        
        // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ
        guard try Bcrypt.verify(loginRequest.password, created: user.passwordHash) else {
            throw Abort(.unauthorized, reason: "Invalid email or password")
        }
        
        // JWT ํ† ํฐ ์ƒ์„ฑ
        let payload = UserPayload(
            userId: try user.requireID(),
            email: user.email,
            exp: .init(value: Date().addingTimeInterval(86400))  // 24์‹œ๊ฐ„ ํ›„ ๋งŒ๋ฃŒ
        )
        
        let token = try req.jwt.sign(payload)
        
        return LoginResponse(token: token, user: user.publicView)
    }
}

### ๐Ÿ›ก๏ธ ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ๋งŒ๋“ค๊ธฐ ์ด์ œ ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŠน์ • ๋ผ์šฐํŠธ์—๋งŒ ์ธ์ฆ์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์–ด:

import Vapor
import JWT

struct JWTAuthenticator: AsyncRequestAuthenticator {
    func authenticate(request: Request) async throws {
        // Authorization ํ—ค๋”์—์„œ ํ† ํฐ ์ถ”์ถœ
        guard let token = request.headers.bearerAuthorization?.token else {
            return
        }
        
        // ํ† ํฐ ๊ฒ€์ฆ ๋ฐ ํŽ˜์ด๋กœ๋“œ ์ถ”์ถœ
        let payload = try request.jwt.verify(token, as: UserPayload.self)
        
        // ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ
        guard let user = try await User.find(payload.userId, on: request.db) else {
            return
        }
        
        // ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ request์— ์ €์žฅ
        request.auth.login(user)
    }
}

// ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ ๊ทธ๋ฃน ๋งŒ๋“ค๊ธฐ
func routes(_ app: Application) throws {
    let protected = app.grouped(JWTAuthenticator())
        .grouped(User.guardMiddleware())
    
    // ์ธ์ฆ์ด ํ•„์š”ํ•œ ๋ผ์šฐํŠธ๋“ค
    protected.get("profile") { req async throws -> User.Public in
        let user = try req.auth.require(User.self)
        return user.publicView
    }
    
    protected.put("profile") { req async throws -> User.Public in
        let user = try req.auth.require(User.self)
        let updatedData = try req.content.decode(User.self)
        
        user.name = updatedData.name
        try await user.save(on: req.db)
        
        return user.publicView
    }
}

์ด์ œ ํด๋ผ์ด์–ธํŠธ๋Š” ๋กœ๊ทธ์ธ ํ›„ ๋ฐ›์€ ํ† ํฐ์„ ํ—ค๋”์— ํฌํ•จ์‹œ์ผœ์„œ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ๋ผ:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

์™„๋ฒฝํ•˜์ง€? ์ด์ œ ์•ˆ์ „ํ•œ API๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐ŸŽ‰
## ๐ŸŽจ Perfect ํ”„๋ ˆ์ž„์›Œํฌ ์‚ดํŽด๋ณด๊ธฐ ์ž, ์ด์ œ Perfect๋„ ํ•œ๋ฒˆ ์‚ดํŽด๋ณผ๊นŒ? Vapor์™€๋Š” ์กฐ๊ธˆ ๋‹ค๋ฅธ ์ ‘๊ทผ ๋ฐฉ์‹์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์„œ ๋น„๊ตํ•ด๋ณด๋ฉด ์žฌ๋ฏธ์žˆ์„ ๊ฑฐ์•ผ! ๐Ÿ”

### ๐Ÿš€ Perfect ์‹œ์ž‘ํ•˜๊ธฐ Perfect๋Š” Swift Package Manager๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด. ๋จผ์ € ์ƒˆ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋งŒ๋“ค๊ณ  ํŒจํ‚ค์ง€๋ฅผ ์ดˆ๊ธฐํ™”ํ•ด:

mkdir MyPerfectServer
cd MyPerfectServer
swift package init --type executable

๊ทธ ๋‹ค์Œ Package.swift๋ฅผ ์ˆ˜์ •ํ•ด์„œ Perfect ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด:

// swift-tools-version:5.7
import PackageDescription

let package = Package(
    name: "MyPerfectServer",
    platforms: [
        .macOS(.v12)
    ],
    dependencies: [
        .package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", from: "3.0.0")
    ],
    targets: [
        .executableTarget(
            name: "MyPerfectServer",
            dependencies: [
                .product(name: "PerfectHTTPServer", package: "Perfect-HTTPServer")
            ]
        )
    ]
)

### ๐ŸŽฏ ์ฒซ ๋ฒˆ์งธ Perfect ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ main.swift ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  ๊ฐ„๋‹จํ•œ ์„œ๋ฒ„๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž:

import PerfectHTTP
import PerfectHTTPServer

// ๋ผ์šฐํŠธ ์„ค์ •
var routes = Routes()

// GET /hello ์—”๋“œํฌ์ธํŠธ
routes.add(method: .get, uri: "/hello") { request, response in
    response.setHeader(.contentType, value: "application/json")
    response.setBody(string: "{\"message\": \"Hello, Perfect! ๐Ÿš€\"}")
    response.completed()
}

// GET /hello/:name ์—”๋“œํฌ์ธํŠธ
routes.add(method: .get, uri: "/hello/{name}") { request, response in
    let name = request.urlVariables["name"] ?? "Guest"
    response.setHeader(.contentType, value: "application/json")
    response.setBody(string: "{\"message\": \"Hello, \(name)! ๐Ÿ‘‹\"}")
    response.completed()
}

// POST /user ์—”๋“œํฌ์ธํŠธ
routes.add(method: .post, uri: "/user") { request, response in
    do {
        guard let jsonData = request.postBodyString?.data(using: .utf8),
              let json = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
              let name = json["name"] as? String,
              let email = json["email"] as? String else {
            response.status = .badRequest
            response.setBody(string: "{\"error\": \"Invalid request\"}")
            response.completed()
            return
        }
        
        let responseData: [String: Any] = [
            "name": name,
            "email": email,
            "message": "User created successfully"
        ]
        
        let responseJson = try JSONSerialization.data(withJSONObject: responseData)
        response.setHeader(.contentType, value: "application/json")
        response.setBody(bytes: Array(responseJson))
        response.completed()
    } catch {
        response.status = .internalServerError
        response.setBody(string: "{\"error\": \"Server error\"}")
        response.completed()
    }
}

// ์„œ๋ฒ„ ์„ค์ •
let server = HTTPServer()
server.serverPort = 8080
server.addRoutes(routes)

do {
    try server.start()
    print("๐Ÿš€ Server started on port 8080")
} catch {
    print("โŒ Server failed to start: \(error)")
}

์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด:

swift run

๐Ÿ” Vapor vs Perfect ์ฝ”๋“œ ๋น„๊ต Perfect์˜ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด Vapor๋ณด๋‹ค ์ข€ ๋” ๋กœ์šฐ๋ ˆ๋ฒจ์ด๋ผ๋Š” ๊ฑธ ๋А๋‚„ ์ˆ˜ ์žˆ์–ด. response.completed()๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœํ•ด์•ผ ํ•˜๊ณ , JSON ์ฒ˜๋ฆฌ๋„ ์ง์ ‘ ํ•ด์•ผ ํ•˜์ง€.

๋ฐ˜๋ฉด Vapor๋Š” Content ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•ด์„œ ์ž๋™์œผ๋กœ ์ธ์ฝ”๋”ฉ/๋””์ฝ”๋”ฉ์„ ์ฒ˜๋ฆฌํ•ด์ค˜. ์–ด๋А ๊ฒŒ ๋” ์ข‹๋‹ค๊ธฐ๋ณด๋‹ค๋Š” ์Šคํƒ€์ผ์˜ ์ฐจ์ด์•ผ. Perfect๋Š” ๋” ๋งŽ์€ ์ œ์–ด๊ถŒ์„ ์ฃผ๊ณ , Vapor๋Š” ๋” ๋งŽ์€ ํŽธ์˜์„ฑ์„ ์ œ๊ณตํ•˜๋Š” ๊ฑฐ์ง€!
### ๐Ÿ’พ Perfect์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค Perfect๋„ ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ง€์›ํ•ด. PostgreSQL์„ ์‚ฌ์šฉํ•˜๋Š” ์˜ˆ์ œ๋ฅผ ๋ณด์ž:

๋จผ์ € ํŒจํ‚ค์ง€ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด:

dependencies: [
    .package(url: "https://github.com/PerfectlySoft/Perfect-PostgreSQL.git", from: "3.0.0")
]

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ฝ”๋“œ:

import PerfectPostgreSQL

// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์„ค์ •
let connection = PGConnection()
let status = connection.connectdb(
    "host=localhost port=5432 dbname=mydb user=postgres password=password"
)

guard status == .ok else {
    print("โŒ Database connection failed")
    exit(1)
}

// ์ฟผ๋ฆฌ ์‹คํ–‰ ํ•จ์ˆ˜
func executeQuery(_ query: String) -> PGResult? {
    return connection.exec(statement: query)
}

// ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋ผ์šฐํŠธ
routes.add(method: .post, uri: "/user") { request, response in
    guard let jsonData = request.postBodyString?.data(using: .utf8),
          let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
          let name = json["name"] as? String,
          let email = json["email"] as? String else {
        response.status = .badRequest
        response.completed()
        return
    }
    
    let query = """
        INSERT INTO users (name, email) 
        VALUES ('\(name)', '\(email)') 
        RETURNING id, name, email
    """
    
    guard let result = executeQuery(query),
          result.status() == .tuplesOK else {
        response.status = .internalServerError
        response.completed()
        return
    }
    
    // ๊ฒฐ๊ณผ ํŒŒ์‹ฑ
    let id = result.getFieldString(tupleIndex: 0, fieldIndex: 0) ?? ""
    let userName = result.getFieldString(tupleIndex: 0, fieldIndex: 1) ?? ""
    let userEmail = result.getFieldString(tupleIndex: 0, fieldIndex: 2) ?? ""
    
    let responseData: [String: Any] = [
        "id": id,
        "name": userName,
        "email": userEmail
    ]
    
    if let responseJson = try? JSONSerialization.data(withJSONObject: responseData) {
        response.setHeader(.contentType, value: "application/json")
        response.setBody(bytes: Array(responseJson))
    }
    
    response.completed()
}

โš ๏ธ SQL ์ธ์ ์…˜ ์ฃผ์˜!

์œ„ ์˜ˆ์ œ๋Š” ๊ต์œก ๋ชฉ์ ์ด์•ผ. ์‹ค์ œ ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์—์„œ๋Š” ์ ˆ๋Œ€ ๋ฌธ์ž์—ด ๋ณด๊ฐ„์œผ๋กœ SQL ์ฟผ๋ฆฌ๋ฅผ ๋งŒ๋“ค๋ฉด ์•ˆ ๋ผ! Prepared Statement๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด:

let query = "INSERT INTO users (name, email) VALUES ($1, $2)"
let result = connection.exec(statement: query, params: [name, email])
Perfect๋Š” ORM์ด ์—†์–ด์„œ SQL์„ ์ง์ ‘ ์ž‘์„ฑํ•ด์•ผ ํ•ด. ์ด๊ฒŒ ๋‹จ์ ์ผ ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ๋Š” ์˜คํžˆ๋ ค ์žฅ์ ์ด ๋  ์ˆ˜๋„ ์žˆ์–ด. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์™„์ „ํžˆ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ !
## ๐ŸŒ RESTful API ์„ค๊ณ„ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค ๋•Œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฑด ์ž˜ ์„ค๊ณ„๋œ API์•ผ. ํด๋ผ์ด์–ธํŠธ ๊ฐœ๋ฐœ์ž๋“ค์ด ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฝ๊ณ , ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์ข‹์€ API๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒŒ ํ•ต์‹ฌ์ด์ง€! ๐ŸŽฏ

### ๐Ÿ“‹ RESTful ์›์น™ REST(Representational State Transfer)๋Š” ์›น API ์„ค๊ณ„์˜ ํ‘œ์ค€์ด์•ผ. ๋ช‡ ๊ฐ€์ง€ ํ•ต์‹ฌ ์›์น™์„ ์‚ดํŽด๋ณด์ž:

๐ŸŽจ RESTful API ์„ค๊ณ„ ์›์น™ 1. ๋ฆฌ์†Œ์Šค ์ค‘์‹ฌ ์„ค๊ณ„
URL์€ ๋™์‚ฌ๊ฐ€ ์•„๋‹ˆ๋ผ ๋ช…์‚ฌ๋กœ ๊ตฌ์„ฑํ•ด. ๋ฆฌ์†Œ์Šค๋ฅผ ๋‚˜ํƒ€๋‚ด์•ผ ํ•ด.
โœ… GET /users
โŒ GET /getUsers

2. HTTP ๋ฉ”์„œ๋“œ ํ™œ์šฉ
โ€ข GET: ์กฐํšŒ
โ€ข POST: ์ƒ์„ฑ
โ€ข PUT/PATCH: ์ˆ˜์ •
โ€ข DELETE: ์‚ญ์ œ

3. ๊ณ„์ธต์  ๊ตฌ์กฐ
๋ฆฌ์†Œ์Šค ๊ฐ„์˜ ๊ด€๊ณ„๋ฅผ URL๋กœ ํ‘œํ˜„ํ•ด.
GET /users/123/posts - 123๋ฒˆ ์‚ฌ์šฉ์ž์˜ ๊ฒŒ์‹œ๊ธ€๋“ค
GET /users/123/posts/456 - 123๋ฒˆ ์‚ฌ์šฉ์ž์˜ 456๋ฒˆ ๊ฒŒ์‹œ๊ธ€

4. ์ ์ ˆํ•œ HTTP ์ƒํƒœ ์ฝ”๋“œ
โ€ข 200: ์„ฑ๊ณต
โ€ข 201: ์ƒ์„ฑ ์„ฑ๊ณต
โ€ข 400: ์ž˜๋ชป๋œ ์š”์ฒญ
โ€ข 401: ์ธ์ฆ ํ•„์š”
โ€ข 403: ๊ถŒํ•œ ์—†์Œ
โ€ข 404: ๋ฆฌ์†Œ์Šค ์—†์Œ
โ€ข 500: ์„œ๋ฒ„ ์—๋Ÿฌ

5. ๋ฒ„์ „ ๊ด€๋ฆฌ
API ๋ฒ„์ „์„ URL์— ํฌํ•จ์‹œ์ผœ.
/api/v1/users
/api/v2/users
### ๐ŸŽฏ ์‹ค์ „ API ์„ค๊ณ„ ์˜ˆ์ œ ์‹ค์ œ๋กœ ๋ธ”๋กœ๊ทธ API๋ฅผ ์„ค๊ณ„ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณด์ž. Vapor๋กœ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ:

import Vapor
import Fluent

// ๊ฒŒ์‹œ๊ธ€ ๋ชจ๋ธ
final class Post: Model, Content {
    static let schema = "posts"
    
    @ID(key: .id)
    var id: UUID?
    
    @Field(key: "title")
    var title: String
    
    @Field(key: "content")
    var content: String
    
    @Parent(key: "user_id")
    var user: User
    
    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?
    
    @Children(for: \.$post)
    var comments: [Comment]
    
    init() { }
}

// ๋Œ“๊ธ€ ๋ชจ๋ธ
final class Comment: Model, Content {
    static let schema = "comments"
    
    @ID(key: .id)
    var id: UUID?
    
    @Field(key: "content")
    var content: String
    
    @Parent(key: "post_id")
    var post: Post
    
    @Parent(key: "user_id")
    var user: User
    
    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?
    
    init() { }
}

// API ๋ผ์šฐํŠธ ์„ค์ •
func routes(_ app: Application) throws {
    let api = app.grouped("api", "v1")
    
    // ๊ฒŒ์‹œ๊ธ€ ๊ด€๋ จ ๋ผ์šฐํŠธ
    let posts = api.grouped("posts")
    
    // ๋ชจ๋“  ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ (ํŽ˜์ด์ง€๋„ค์ด์…˜ ํฌํ•จ)
    posts.get { req async throws -> Page<Post> in
        try await Post.query(on: req.db)
            .with(\.$user)
            .paginate(for: req)
    }
    
    // ํŠน์ • ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ
    posts.get(":id") { req async throws -> Post in
        guard let post = try await Post.find(req.parameters.get("id"), on: req.db) else {
            throw Abort(.notFound)
        }
        try await post.$user.load(on: req.db)
        try await post.$comments.load(on: req.db)
        return post
    }
    
    // ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ (์ธ์ฆ ํ•„์š”)
    let protected = posts.grouped(JWTAuthenticator()).grouped(User.guardMiddleware())
    
    protected.post { req async throws -> Post in
        let user = try req.auth.require(User.self)
        let post = try req.content.decode(Post.self)
        post.$user.id = try user.requireID()
        try await post.save(on: req.db)
        return post
    }
    
    // ๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •
    protected.put(":id") { req async throws -> Post in
        let user = try req.auth.require(User.self)
        guard let post = try await Post.find(req.parameters.get("id"), on: req.db) else {
            throw Abort(.notFound)
        }
        
        // ์ž‘์„ฑ์ž๋งŒ ์ˆ˜์ • ๊ฐ€๋Šฅ
        guard post.$user.id == user.id else {
            throw Abort(.forbidden)
        }
        
        let updatedPost = try req.content.decode(Post.self)
        post.title = updatedPost.title
        post.content = updatedPost.content
        try await post.save(on: req.db)
        return post
    }
    
    // ๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ
    protected.delete(":id") { req async throws -> HTTPStatus in
        let user = try req.auth.require(User.self)
        guard let post = try await Post.find(req.parameters.get("id"), on: req.db) else {
            throw Abort(.notFound)
        }
        
        guard post.$user.id == user.id else {
            throw Abort(.forbidden)
        }
        
        try await post.delete(on: req.db)
        return .noContent
    }
    
    // ๋Œ“๊ธ€ ๊ด€๋ จ ๋ผ์šฐํŠธ
    let comments = posts.group(":postId", "comments")
    
    // ํŠน์ • ๊ฒŒ์‹œ๊ธ€์˜ ๋Œ“๊ธ€ ์กฐํšŒ
    comments.get { req async throws -> [Comment] in
        guard let postId = req.parameters.get("postId", as: UUID.self) else {
            throw Abort(.badRequest)
        }
        
        return try await Comment.query(on: req.db)
            .filter(\.$post.$id == postId)
            .with(\.$user)
            .all()
    }
    
    // ๋Œ“๊ธ€ ์ž‘์„ฑ
    let protectedComments = comments.grouped(JWTAuthenticator()).grouped(User.guardMiddleware())
    
    protectedComments.post { req async throws -> Comment in
        let user = try req.auth.require(User.self)
        guard let postId = req.parameters.get("postId", as: UUID.self) else {
            throw Abort(.badRequest)
        }
        
        let comment = try req.content.decode(Comment.self)
        comment.$user.id = try user.requireID()
        comment.$post.id = postId
        try await comment.save(on: req.db)
        return comment
    }
}

๐Ÿ’ก ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ตฌํ˜„ํ•˜๊ธฐ

๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ๋ฐ˜ํ™˜ํ•˜๋ฉด ์„ฑ๋Šฅ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒจ. Vapor์˜ paginate() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‰ฝ๊ฒŒ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด:

// ์š”์ฒญ: GET /posts?page=2&per=20
// 2ํŽ˜์ด์ง€, ํŽ˜์ด์ง€๋‹น 20๊ฐœ ํ•ญ๋ชฉ

์‘๋‹ต ํ˜•์‹:
{
  "items": [...],
  "metadata": {
    "page": 2,
    "per": 20,
    "total": 150
  }
}
### ๐Ÿ“Š API ์‘๋‹ต ํ˜•์‹ ํ‘œ์ค€ํ™” ์ผ๊ด€๋œ ์‘๋‹ต ํ˜•์‹์„ ์‚ฌ์šฉํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ ๊ฐœ๋ฐœ์ด ํ›จ์”ฌ ์‰ฌ์›Œ์ ธ:

// ์„ฑ๊ณต ์‘๋‹ต
struct APIResponse<T: Content>: Content {
    var success: Bool
    var data: T?
    var message: String?
    
    static func success(data: T, message: String? = nil) -> APIResponse {
        APIResponse(success: true, data: data, message: message)
    }
}

// ์—๋Ÿฌ ์‘๋‹ต
struct APIError: Content {
    var success: Bool = false
    var error: ErrorDetail
    
    struct ErrorDetail: Content {
        var code: String
        var message: String
        var details: [String: String]?
    }
}

// ์‚ฌ์šฉ ์˜ˆ์ œ
posts.get { req async throws -> APIResponse<[Post]> in
    let posts = try await Post.query(on: req.db).all()
    return .success(data: posts, message: "Posts retrieved successfully")
}
## ๐Ÿงช ํ…Œ์ŠคํŠธ: ์•ˆ์ •์ ์ธ ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ์œผ๋ฉด ๋‹น์—ฐํžˆ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด์•ผ์ง€! ํ…Œ์ŠคํŠธ ์—†๋Š” ์„œ๋ฒ„๋Š” ์‹œํ•œํญํƒ„์ด๋‚˜ ๋งˆ์ฐฌ๊ฐ€์ง€์•ผ. ๐Ÿ˜ฑ

### ๐ŸŽฏ Vapor ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ Vapor๋Š” XCTest๋ฅผ ์‚ฌ์šฉํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง€์›ํ•ด. Tests ๋””๋ ‰ํ† ๋ฆฌ์— ํ…Œ์ŠคํŠธ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด๋ณด์ž:

import XCTVapor
@testable import App

final class UserTests: XCTestCase {
    var app: Application!
    
    override func setUp() async throws {
        app = Application(.testing)
        try configure(app)
        try await app.autoMigrate()
    }
    
    override func tearDown() async throws {
        try await app.autoRevert()
        app.shutdown()
    }
    
    // ์‚ฌ์šฉ์ž ์ƒ์„ฑ ํ…Œ์ŠคํŠธ
    func testCreateUser() async throws {
        let userData = User(name: "Test User", email: "test@example.com", age: 25)
        
        try app.test(.POST, "users", beforeRequest: { req in
            try req.content.encode(userData)
        }, afterResponse: { res in
            XCTAssertEqual(res.status, .ok)
            
            let user = try res.content.decode(User.self)
            XCTAssertEqual(user.name, "Test User")
            XCTAssertEqual(user.email, "test@example.com")
            XCTAssertNotNil(user.id)
        })
    }
    
    // ์‚ฌ์šฉ์ž ์กฐํšŒ ํ…Œ์ŠคํŠธ
    func testGetUser() async throws {
        // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
        let user = User(name: "Test User", email: "test@example.com", age: 25)
        try await user.save(on: app.db)
        
        let userId = try user.requireID()
        
        try app.test(.GET, "users/\(userId)", afterResponse: { res in
            XCTAssertEqual(res.status, .ok)
            
            let fetchedUser = try res.content.decode(User.self)
            XCTAssertEqual(fetchedUser.id, user.id)
            XCTAssertEqual(fetchedUser.name, user.name)
        })
    }
    
    // ์ธ์ฆ ํ…Œ์ŠคํŠธ
    func testAuthenticatedRoute() async throws {
        // ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋ฐ ๋กœ๊ทธ์ธ
        let user = User(name: "Test User", email: "test@example.com", age: 25)
        user.passwordHash = try Bcrypt.hash("password123")
        try await user.save(on: app.db)
        
        var token: String!
        
        try app.test(.POST, "login", beforeRequest: { req in
            try req.content.encode(LoginRequest(email: "test@example.com", password: "password123"))
        }, afterResponse: { res in
            let response = try res.content.decode(LoginResponse.self)
            token = response.token
        })
        
        // ์ธ์ฆ์ด ํ•„์š”ํ•œ ๋ผ์šฐํŠธ ํ…Œ์ŠคํŠธ
        try app.test(.GET, "profile", beforeRequest: { req in
            req.headers.bearerAuthorization = BearerAuthorization(token: token)
        }, afterResponse: { res in
            XCTAssertEqual(res.status, .ok)
        })
    }
    
    // ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ
    func testNotFound() async throws {
        try app.test(.GET, "users/00000000-0000-0000-0000-000000000000", afterResponse: { res in
            XCTAssertEqual(res.status, .notFound)
        })
    }
}

ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด:

swift test

๐ŸŽจ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ํŒ โ€ข Given-When-Then ํŒจํ„ด: ํ…Œ์ŠคํŠธ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌ์กฐํ™”ํ•ด
- Given: ์ดˆ๊ธฐ ์ƒํƒœ ์„ค์ •
- When: ํ…Œ์ŠคํŠธํ•  ๋™์ž‘ ์‹คํ–‰
- Then: ๊ฒฐ๊ณผ ๊ฒ€์ฆ

โ€ข ๋…๋ฆฝ์ ์ธ ํ…Œ์ŠคํŠธ: ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ์„œ๋กœ ์˜ํ–ฅ์„ ์ฃผ๋ฉด ์•ˆ ๋ผ
โ€ข ์˜๋ฏธ ์žˆ๋Š” ์ด๋ฆ„: ํ…Œ์ŠคํŠธ ์ด๋ฆ„๋งŒ ๋ด๋„ ๋ฌด์—‡์„ ํ…Œ์ŠคํŠธํ•˜๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด
โ€ข ์—ฃ์ง€ ์ผ€์ด์Šค: ์ •์ƒ ์ผ€์ด์Šค๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์—๋Ÿฌ ์ƒํ™ฉ๋„ ํ…Œ์ŠคํŠธํ•ด
### ๐Ÿ” ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ vs ๋‹จ์œ„ ํ…Œ์ŠคํŠธ
๊ตฌ๋ถ„ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ
๋ฒ”์œ„ ๊ฐœ๋ณ„ ํ•จ์ˆ˜/๋ฉ”์„œ๋“œ ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์˜ ์ƒํ˜ธ์ž‘์šฉ
์†๋„ ๋น ๋ฆ„ ๋А๋ฆผ
์˜์กด์„ฑ Mock/Stub ์‚ฌ์šฉ ์‹ค์ œ ์˜์กด์„ฑ ์‚ฌ์šฉ
๋ชฉ์  ๋กœ์ง ๊ฒ€์ฆ ์‹œ์Šคํ…œ ์ „์ฒด ๋™์ž‘ ๊ฒ€์ฆ
์‹คํ–‰ ๋นˆ๋„ ๋งค์šฐ ์ž์ฃผ ์ƒ๋Œ€์ ์œผ๋กœ ๋œ ์ž์ฃผ

์ด์ƒ์ ์œผ๋กœ๋Š” ๋‘ ๊ฐ€์ง€๋ฅผ ๋ชจ๋‘ ์ž‘์„ฑํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋กœ ๊ฐœ๋ณ„ ๋กœ์ง์„ ๊ฒ€์ฆํ•˜๊ณ , ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋กœ ์ „์ฒด ์‹œ์Šคํ…œ์ด ์ž˜ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฑฐ์ง€!
Testing Pyramid Unit Tests Fast, Many Integration Tests Medium Speed E2E Tests Slow, Few 70% Unit 20% Integration 10% E2E
## ๐Ÿš€ ๋ฐฐํฌํ•˜๊ธฐ: ์„œ๋ฒ„๋ฅผ ์„ธ์ƒ์— ๊ณต๊ฐœํ•˜์ž! ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์—ˆ์œผ๋ฉด ์ด์ œ ๋ฐฐํฌํ•ด์•ผ์ง€! ๋กœ์ปฌ์—์„œ๋งŒ ๋Œ๋ฆฌ๋ฉด ๋ฌด์Šจ ์†Œ์šฉ์ด์•ผ? ๐Ÿ˜„

### ๐Ÿณ Docker๋กœ ์ปจํ…Œ์ด๋„ˆํ™”ํ•˜๊ธฐ ์š”์ฆ˜์€ Docker๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐฐํฌ๊ฐ€ ํ‘œ์ค€์ด์•ผ. Vapor ํ”„๋กœ์ ํŠธ๋ฅผ Docker ์ด๋ฏธ์ง€๋กœ ๋งŒ๋“ค์–ด๋ณด์ž:

# Dockerfile
FROM swift:5.9-jammy as build

WORKDIR /build

# ์˜์กด์„ฑ ๋ณต์‚ฌ ๋ฐ ๋นŒ๋“œ
COPY ./Package.* ./
RUN swift package resolve

# ์†Œ์Šค ์ฝ”๋“œ ๋ณต์‚ฌ
COPY . .

# ๋ฆด๋ฆฌ์ฆˆ ๋นŒ๋“œ
RUN swift build -c release --static-swift-stdlib

# ๋Ÿฐํƒ€์ž„ ์ด๋ฏธ์ง€
FROM ubuntu:jammy

# ํ•„์š”ํ•œ ๋Ÿฐํƒ€์ž„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜
RUN apt-get update && apt-get install -y \
    libcurl4 \
    libxml2 \
    && rm -r /var/lib/apt/lists/*

# ๋นŒ๋“œ๋œ ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ณต์‚ฌ
WORKDIR /app
COPY --from=build /build/.build/release/App /app
COPY --from=build /build/Public /app/Public
COPY --from=build /build/Resources /app/Resources

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

# ์‹คํ–‰
ENTRYPOINT ["./App"]
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]

Docker ์ด๋ฏธ์ง€๋ฅผ ๋นŒ๋“œํ•˜๊ณ  ์‹คํ–‰ํ•ด๋ณด์ž:

# ์ด๋ฏธ์ง€ ๋นŒ๋“œ
docker build -t my-vapor-app .

# ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰
docker run -p 8080:8080 my-vapor-app

๐ŸŽฏ Docker Compose๋กœ ์ „์ฒด ์Šคํƒ ๊ด€๋ฆฌํ•˜๊ธฐ

์„œ๋ฒ„์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ํ•จ๊ป˜ ๊ด€๋ฆฌํ•˜๋ ค๋ฉด Docker Compose๋ฅผ ์‚ฌ์šฉํ•ด:

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_USERNAME: vapor
      DATABASE_PASSWORD: password
      DATABASE_NAME: vapor_db
      JWT_SECRET: your-secret-key
    depends_on:
      - db
    restart: unless-stopped
  
  db:
    image: postgres:15
    environment:
      POSTGRES_USER: vapor
      POSTGRES_PASSWORD: password
      POSTGRES_DB: vapor_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  postgres_data:

์‹คํ–‰:
docker-compose up -d
### โ˜๏ธ ํด๋ผ์šฐ๋“œ ๋ฐฐํฌ ์˜ต์…˜ ์—ฌ๋Ÿฌ ํด๋ผ์šฐ๋“œ ํ”Œ๋žซํผ์— ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์–ด. ๊ฐ๊ฐ์˜ ์žฅ๋‹จ์ ์„ ์‚ดํŽด๋ณด์ž:

1. Heroku - ๊ฐ€์žฅ ์‰ฌ์šด ๋ฐฐํฌ
Heroku๋Š” ์„ค์ •์ด ๊ฑฐ์˜ ํ•„์š” ์—†์–ด์„œ ์ดˆ๋ณด์ž์—๊ฒŒ ์ข‹์•„:

# Procfile ์ƒ์„ฑ
web: App serve --env production --hostname 0.0.0.0 --port $PORT

# ๋ฐฐํฌ
heroku create my-vapor-app
git push heroku main

2. AWS (Amazon Web Services)
ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉํ•ด. EC2, ECS, Lambda ๋“ฑ ๋‹ค์–‘ํ•œ ์˜ต์…˜์ด ์žˆ์–ด:

EC2 ๋ฐฐํฌ ์˜ˆ์ œ:
# EC2 ์ธ์Šคํ„ด์Šค ์ ‘์†
ssh -i your-key.pem ubuntu@your-ec2-ip

# Docker ์„ค์น˜
sudo apt-get update
sudo apt-get install docker.io docker-compose

# ์ฝ”๋“œ ํด๋ก  ๋ฐ ์‹คํ–‰
git clone your-repo
cd your-repo
docker-compose up -d

3. DigitalOcean - ๊ฐ€์„ฑ๋น„ ์ข‹์€ ์„ ํƒ
AWS๋ณด๋‹ค ์ €๋ ดํ•˜๊ณ  ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์›Œ. App Platform์„ ์‚ฌ์šฉํ•˜๋ฉด GitHub ์—ฐ๋™์œผ๋กœ ์ž๋™ ๋ฐฐํฌ๋„ ๊ฐ€๋Šฅํ•ด!

4. Vapor Cloud (์ข…๋ฃŒ๋จ)
์•„์‰ฝ๊ฒŒ๋„ Vapor ์ „์šฉ ํด๋ผ์šฐ๋“œ ์„œ๋น„์Šค๋Š” ์ข…๋ฃŒ๋์–ด. ํ•˜์ง€๋งŒ ์œ„์˜ ๋‹ค๋ฅธ ์˜ต์…˜๋“ค๋„ ์ถฉ๋ถ„ํžˆ ์ข‹์•„!

๐Ÿ’ก ๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋น„๋ฐ€๋ฒˆํ˜ธ, JWT ์‹œํฌ๋ฆฟ ๋“ฑ)
  • โœ… HTTPS ์„ค์ • (Let's Encrypt ์‚ฌ์šฉ ์ถ”์ฒœ)
  • โœ… ๋กœ๊น… ์„ค์ • (์—๋Ÿฌ ์ถ”์ ์„ ์œ„ํ•ด)
  • โœ… ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ • (์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ)
  • โœ… ๋ฐฑ์—… ์ „๋žต ์ˆ˜๋ฆฝ
  • โœ… CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•
  • โœ… ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ (ํŠธ๋ž˜ํ”ฝ์ด ๋งŽ์„ ๊ฒฝ์šฐ)
  • โœ… ์บ์‹ฑ ์ „๋žต (Redis ๋“ฑ)
### ๐Ÿ”„ CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•ํ•˜๊ธฐ GitHub Actions๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ž๋™ ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ์„ ๋งŒ๋“ค์–ด๋ณด์ž:

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Swift
        uses: swift-actions/setup-swift@v1
        with:
          swift-version: '5.9'
      
      - name: Run tests
        run: swift test
  
  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          push: true
          tags: your-username/my-vapor-app:latest
      
      - name: Deploy to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /app
            docker-compose pull
            docker-compose up -d

์ด์ œ main ๋ธŒ๋žœ์น˜์— ํ‘ธ์‹œํ•˜๋ฉด ์ž๋™์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ณ  ๋ฐฐํฌ๋ผ! ์™„์ „ ํŽธํ•˜์ง€? ๐ŸŽ‰
## โšก ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋น ๋ฅธ ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ ์„œ๋ฒ„๊ฐ€ ๋А๋ฆฌ๋ฉด ์‚ฌ์šฉ์ž๋“ค์ด ๋– ๋‚˜๊ฐ€. ์„ฑ๋Šฅ ์ตœ์ ํ™”๋Š” ํ•„์ˆ˜์•ผ! ๐Ÿš€

### ๐Ÿ“Š ์„ฑ๋Šฅ ์ธก์ •ํ•˜๊ธฐ ์ตœ์ ํ™”ํ•˜๊ธฐ ์ „์— ๋จผ์ € ์ธก์ •ํ•ด์•ผ ํ•ด. "์ธก์ •ํ•  ์ˆ˜ ์—†์œผ๋ฉด ๊ฐœ์„ ํ•  ์ˆ˜ ์—†๋‹ค"๋Š” ๋ง์ด ์žˆ์ž–์•„?

๐Ÿ” ์„ฑ๋Šฅ ์ธก์ • ๋„๊ตฌ

1. Apache Bench (ab)
๊ฐ„๋‹จํ•œ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ๋„๊ตฌ:
ab -n 1000 -c 10 http://localhost:8080/api/users
1000๊ฐœ ์š”์ฒญ์„ 10๊ฐœ ๋™์‹œ ์—ฐ๊ฒฐ๋กœ ๋ณด๋‚ด๋Š” ๊ฑฐ์•ผ.

2. wrk
๋” ๊ฐ•๋ ฅํ•œ HTTP ๋ฒค์น˜๋งˆํ‚น ๋„๊ตฌ:
wrk -t12 -c400 -d30s http://localhost:8080/api/users
12๊ฐœ ์Šค๋ ˆ๋“œ, 400๊ฐœ ์—ฐ๊ฒฐ, 30์ดˆ ๋™์•ˆ ํ…Œ์ŠคํŠธ.

3. Instruments (macOS)
Xcode์˜ Instruments๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜, CPU ์‚ฌ์šฉ๋Ÿ‰ ๋“ฑ์„ ์ž์„ธํžˆ ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์–ด.
### ๐ŸŽฏ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์ตœ์ ํ™” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ๋ณ‘๋ชฉ์ด ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„. ๋ช‡ ๊ฐ€์ง€ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์„ ์‚ดํŽด๋ณด์ž:

// โŒ N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ
let users = try await User.query(on: db).all()
for user in users {
    // ๊ฐ ์‚ฌ์šฉ์ž๋งˆ๋‹ค ๋ณ„๋„ ์ฟผ๋ฆฌ ์‹คํ–‰ (๋น„ํšจ์œจ์ !)
    let posts = try await user.$posts.get(on: db)
}

// โœ… Eager Loading ์‚ฌ์šฉ
let users = try await User.query(on: db)
    .with(\.$posts)  // ํ•œ ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ๋ชจ๋“  ๊ฒŒ์‹œ๊ธ€ ๊ฐ€์ ธ์˜ค๊ธฐ
    .all()

// โœ… ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ์„ ํƒ
let users = try await User.query(on: db)
    .field(\.$id)
    .field(\.$name)
    .field(\.$email)
    .all()

// โœ… ์ธ๋ฑ์Šค ํ™œ์šฉ
struct CreateUser: AsyncMigration {
    func prepare(on database: Database) async throws {
        try await database.schema("users")
            .id()
            .field("email", .string, .required)
            .field("name", .string, .required)
            .unique(on: "email")
            .create()
        
        // ์ž์ฃผ ๊ฒ€์ƒ‰ํ•˜๋Š” ํ•„๋“œ์— ์ธ๋ฑ์Šค ์ถ”๊ฐ€
        try await database.schema("users")
            .createIndex(on: "email")
            .run()
    }
}

### ๐Ÿ—„๏ธ ์บ์‹ฑ ์ „๋žต ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ์บ์‹œ์— ์ €์žฅํ•˜๋ฉด ์„ฑ๋Šฅ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋ผ. Redis๋ฅผ ์‚ฌ์šฉํ•ด๋ณด์ž:

import Redis

// Redis ์„ค์ •
app.redis.configuration = try RedisConfiguration(
    hostname: "localhost",
    port: 6379
)

// ์บ์‹œ ์‚ฌ์šฉ ์˜ˆ์ œ
func getUser(id: UUID, req: Request) async throws -> User {
    let cacheKey = "user:\(id)"
    
    // ์บ์‹œ์—์„œ ๋จผ์ € ํ™•์ธ
    if let cached = try await req.redis.get(cacheKey, as: User.self) {
        return cached
    }
    
    // ์บ์‹œ์— ์—†์œผ๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์กฐํšŒ
    guard let user = try await User.find(id, on: req.db) else {
        throw Abort(.notFound)
    }
    
    // ์บ์‹œ์— ์ €์žฅ (1์‹œ๊ฐ„ TTL)
    try await req.redis.setex(cacheKey, to: user, expirationInSeconds: 3600)
    
    return user
}

// ์บ์‹œ ๋ฌดํšจํ™”
func updateUser(id: UUID, req: Request) async throws -> User {
    let user = try await User.find(id, on: req.db)
    // ... ์—…๋ฐ์ดํŠธ ๋กœ์ง
    
    // ์บ์‹œ ์‚ญ์ œ
    try await req.redis.delete("user:\(id)")
    
    return user
}

๐ŸŽจ ์บ์‹ฑ ์ „๋žต ์„ ํƒํ•˜๊ธฐ Cache-Aside (Lazy Loading)
์œ„ ์˜ˆ์ œ์ฒ˜๋Ÿผ ํ•„์š”ํ•  ๋•Œ๋งŒ ์บ์‹œ์— ์ €์žฅ. ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ๋ฐฉ์‹.

Write-Through
๋ฐ์ดํ„ฐ๋ฅผ ์“ธ ๋•Œ ์บ์‹œ์™€ DB์— ๋™์‹œ์— ์ €์žฅ. ์ผ๊ด€์„ฑ์ด ์ค‘์š”ํ•  ๋•Œ ์‚ฌ์šฉ.

Write-Behind
์บ์‹œ์— ๋จผ์ € ์“ฐ๊ณ  ๋‚˜์ค‘์— DB์— ๋น„๋™๊ธฐ๋กœ ์ €์žฅ. ์“ฐ๊ธฐ ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•  ๋•Œ ์‚ฌ์šฉ.
### ๐Ÿ”„ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ํ™œ์šฉํ•˜๊ธฐ Swift์˜ async/await๋ฅผ ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•ด์„œ ๋™์‹œ์„ฑ์„ ๋†’์ด์ž:

// โŒ ์ˆœ์ฐจ ์ฒ˜๋ฆฌ (๋А๋ฆผ)
let user = try await fetchUser(id: userId, on: db)
let posts = try await fetchPosts(userId: userId, on: db)
let comments = try await fetchComments(userId: userId, on: db)

// โœ… ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ (๋น ๋ฆ„)
async let user = fetchUser(id: userId, on: db)
async let posts = fetchPosts(userId: userId, on: db)
async let comments = fetchComments(userId: userId, on: db)

let result = try await (user, posts, comments)

// โœ… TaskGroup ์‚ฌ์šฉ (๋” ๋งŽ์€ ์ž‘์—…์„ ๋ณ‘๋ ฌ๋กœ)
let userIds = [UUID(), UUID(), UUID()]

let users = try await withThrowingTaskGroup(of: User.self) { group in
    for id in userIds {
        group.addTask {
            try await fetchUser(id: id, on: db)
        }
    }
    
    var results: [User] = []
    for try await user in group {
        results.append(user)
    }
    return results
}

### ๐Ÿ“ฆ ์‘๋‹ต ์••์ถ• HTTP ์‘๋‹ต์„ ์••์ถ•ํ•˜๋ฉด ๋„คํŠธ์›Œํฌ ์ „์†ก๋Ÿ‰์ด ์ค„์–ด๋“ค์–ด:

// Vapor ์„ค์ •
app.http.server.configuration.responseCompression = .enabled

// ํŠน์ • ๋ผ์šฐํŠธ์—๋งŒ ์ ์šฉ
let compressed = app.grouped(FileMiddleware(publicDirectory: app.directory.publicDirectory))
compressed.get("large-data") { req async throws -> [String: Any] in
    // ํฐ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜
}
## ๐Ÿ”’ ๋ณด์•ˆ ๊ฐ•ํ™”: ์•ˆ์ „ํ•œ ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ ๋ณด์•ˆ์€ ์•„๋ฌด๋ฆฌ ๊ฐ•์กฐํ•ด๋„ ์ง€๋‚˜์น˜์ง€ ์•Š์•„. ํ•œ ๋ฒˆ์˜ ๋ณด์•ˆ ์‚ฌ๊ณ ๋กœ ๋ชจ๋“  ๊ฒŒ ๋ฌด๋„ˆ์งˆ ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿ›ก๏ธ

### ๐Ÿšซ ์ผ๋ฐ˜์ ์ธ ๋ณด์•ˆ ์œ„ํ˜‘๊ณผ ๋Œ€์‘
โš ๏ธ OWASP Top 10 ๋Œ€์‘ํ•˜๊ธฐ

1. SQL Injection
Fluent ORM์„ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ ๋ฐฉ์–ด๋ผ. ํ•˜์ง€๋งŒ Raw SQL์„ ์“ธ ๋•Œ๋Š” ์ฃผ์˜ํ•ด:
// โŒ ์œ„ํ—˜
let query = "SELECT * FROM users WHERE email = '\(email)'"

// โœ… ์•ˆ์ „ (Parameterized Query)
let query = "SELECT * FROM users WHERE email = $1"
let result = try await db.raw(query).bind(email).all()

2. XSS (Cross-Site Scripting)
์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ๊ทธ๋Œ€๋กœ ์ถœ๋ ฅํ•˜๋ฉด ์•ˆ ๋ผ:
// HTML ์ด์Šค์ผ€์ดํ”„ ์ฒ˜๋ฆฌ
import Vapor

extension String {
    var htmlEscaped: String {
        self.replacingOccurrences(of: "&", with: "&")
            .replacingOccurrences(of: "<", with: "<")
            .replacingOccurrences(of: ">", with: ">")
            .replacingOccurrences(of: "\"", with: """)
            .replacingOccurrences(of: "'", with: "'")
    }
}

3. CSRF (Cross-Site Request Forgery)
CSRF ํ† ํฐ์„ ์‚ฌ์šฉํ•ด์„œ ๋ฐฉ์–ด:
// CSRF ๋ฏธ๋“ค์›จ์–ด ๊ตฌํ˜„
struct CSRFMiddleware: AsyncMiddleware {
    func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
        if request.method == .POST || request.method == .PUT || request.method == .DELETE {
            guard let token = request.headers.first(name: "X-CSRF-Token"),
                  try request.application.jwt.verify(token, as: CSRFPayload.self) != nil else {
                throw Abort(.forbidden, reason: "Invalid CSRF token")
            }
        }
        return try await next.respond(to: request)
    }
}

4. Rate Limiting
๋ฌด์ฐจ๋ณ„ ๋Œ€์ž… ๊ณต๊ฒฉ์„ ๋ฐฉ์–ดํ•˜๊ธฐ ์œ„ํ•œ ์š”์ฒญ ์ œํ•œ:
import Redis

struct RateLimitMiddleware: AsyncMiddleware {
    let maxRequests: Int
    let windowSeconds: Int
    
    func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
        let ip = request.remoteAddress?.ipAddress ?? "unknown"
        let key = "rate_limit:\(ip)"
        
        let count = try await request.redis.incr(key)
        
        if count == 1 {
            try await request.redis.expire(key, after: windowSeconds)
        }
        
        if count > maxRequests {
            throw Abort(.tooManyRequests, reason: "Rate limit exceeded")
        }
        
        return try await next.respond(to: request)
    }
}

// ์‚ฌ์šฉ
app.middleware.use(RateLimitMiddleware(maxRequests: 100, windowSeconds: 60))
### ๐Ÿ” HTTPS ์„ค์ • ํ”„๋กœ๋•์…˜์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ HTTPS๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. Let's Encrypt๋กœ ๋ฌด๋ฃŒ SSL ์ธ์ฆ์„œ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด:

# Nginx ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ ์„ค์ •
server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com;
    
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    
    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

### ๐Ÿ”‘ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋ฉด ์•ˆ ๋ผ:

// .env ํŒŒ์ผ (์ ˆ๋Œ€ Git์— ์ปค๋ฐ‹ํ•˜์ง€ ๋ง ๊ฒƒ!)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=your-super-secret-key-here
REDIS_URL=redis://localhost:6379

// .env.example ํŒŒ์ผ (Git์— ์ปค๋ฐ‹)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=change-this-in-production
REDIS_URL=redis://localhost:6379

// ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉ
let databaseURL = Environment.get("DATABASE_URL") ?? "default-value"

๐Ÿšจ ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… ๋ชจ๋“  ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•ด์‹œํ™” (Bcrypt ์‚ฌ์šฉ)
  • โœ… JWT ํ† ํฐ์— ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ •
  • โœ… HTTPS ์‚ฌ์šฉ
  • โœ… CORS ์ •์ฑ… ์„ค์ •
  • โœ… Rate Limiting ๊ตฌํ˜„
  • โœ… ์ž…๋ ฅ ๊ฒ€์ฆ (Validation)
  • โœ… ์—๋Ÿฌ ๋ฉ”์‹œ์ง€์— ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋…ธ์ถœ ๊ธˆ์ง€
  • โœ… ์ •๊ธฐ์ ์ธ ์˜์กด์„ฑ ์—…๋ฐ์ดํŠธ
  • โœ… ๋กœ๊ทธ์— ๋ฏผ๊ฐํ•œ ์ •๋ณด ๊ธฐ๋ก ๊ธˆ์ง€
  • โœ… ๋ณด์•ˆ ํ—ค๋” ์„ค์ • (X-Frame-Options, X-Content-Type-Options ๋“ฑ)
## ๐ŸŒ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๊ฐ„๋‹จํ•œ ๋ธ”๋กœ๊ทธ API ๋งŒ๋“ค๊ธฐ ์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ๋ฐฐ์› ์œผ๋‹ˆ ์ด์ œ ์‹ค์ „ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ๊ฐ„๋‹จํ•œ ๋ธ”๋กœ๊ทธ API๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ. ๐ŸŽจ

### ๐Ÿ“‹ ํ”„๋กœ์ ํŠธ ์š”๊ตฌ์‚ฌํ•ญ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค ๋ธ”๋กœ๊ทธ API์˜ ๊ธฐ๋Šฅ๋“ค:
โ€ข ์‚ฌ์šฉ์ž ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ
โ€ข ๊ฒŒ์‹œ๊ธ€ CRUD
โ€ข ๋Œ“๊ธ€ ๊ธฐ๋Šฅ
โ€ข ์ข‹์•„์š” ๊ธฐ๋Šฅ
โ€ข ํƒœ๊ทธ ์‹œ์Šคํ…œ
โ€ข ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ

### ๐Ÿ—๏ธ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์„ค๊ณ„ ๋จผ์ € ํ•„์š”ํ•œ ๋ชจ๋ธ๋“ค์„ ์ •์˜ํ•˜์ž:

import Fluent
import Vapor

// ์‚ฌ์šฉ์ž ๋ชจ๋ธ
final class User: Model, Content {
    static let schema = "users"
    
    @ID(key: .id)
    var id: UUID?
    
    @Field(key: "username")
    var username: String
    
    @Field(key: "email")
    var email: String
    
    @Field(key: "password_hash")
    var passwordHash: String
    
    @Field(key: "bio")
    var bio: String?
    
    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?
    
    @Children(for: \.$author)
    var posts: [Post]
    
    @Children(for: \.$user)
    var comments: [Comment]
    
    init() { }
}

// ๊ฒŒ์‹œ๊ธ€ ๋ชจ๋ธ
final class Post: Model, Content {
    static let schema = "posts"
    
    @ID(key: .id)
    var id: UUID?
    
    @Field(key: "title")
    var title: String
    
    @Field(key: "content")
    var content: String
    
    @Field(key: "slug")
    var slug: String
    
    @Parent(key: "author_id")
    var author: User
    
    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?
    
    @Timestamp(key: "updated_at", on: .update)
    var updatedAt: Date?
    
    @Children(for: \.$post)
    var comments: [Comment]
    
    @Siblings(through: PostTag.self, from: \.$post, to: \.$tag)
    var tags: [Tag]
    
    @Children(for: \.$post)
    var likes: [Like]
    
    init() { }
}

// ๋Œ“๊ธ€ ๋ชจ๋ธ
final class Comment: Model, Content {
    static let schema = "comments"
    
    @ID(key: .id)
    var id: UUID?
    
    @Field(key: "content")
    var content: String
    
    @Parent(key: "post_id")
    var post: Post
    
    @Parent(key: "user_id")
    var user: User
    
    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?
    
    init() { }
}

// ํƒœ๊ทธ ๋ชจ๋ธ
final class Tag: Model, Content {
    static let schema = "tags"
    
    @ID(key: .id)
    var id: UUID?
    
    @Field(key: "name")
    var name: String
    
    @Siblings(through: PostTag.self, from: \.$tag, to: \.$post)
    var posts: [Post]
    
    init() { }
}

// ๊ฒŒ์‹œ๊ธ€-ํƒœ๊ทธ ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”
final class PostTag: Model {
    static let schema = "post_tags"
    
    @ID(key: .id)
    var id: UUID?
    
    @Parent(key: "post_id")
    var post: Post
    
    @Parent(key: "tag_id")
    var tag: Tag
    
    init() { }
}

// ์ข‹์•„์š” ๋ชจ๋ธ
final class Like: Model, Content {
    static let schema = "likes"
    
    @ID(key: .id)
    var id: UUID?
    
    @Parent(key: "post_id")
    var post: Post
    
    @Parent(key: "user_id")
    var user: User
    
    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?
    
    init() { }
}

### ๐ŸŽฎ ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„ ์ด์ œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•  ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“ค์ž:

import Vapor
import Fluent

struct PostController: RouteCollection {
    func boot(routes: RoutesBuilder) throws {
        let posts = routes.grouped("api", "v1", "posts")
        
        // ๊ณต๊ฐœ ๋ผ์šฐํŠธ
        posts.get(use: index)
        posts.get(":id", use: show)
        posts.get("search", use: search)
        posts.get("tag", ":tagName", use: byTag)
        
        // ์ธ์ฆ ํ•„์š” ๋ผ์šฐํŠธ
        let protected = posts.grouped(JWTAuthenticator()).grouped(User.guardMiddleware())
        protected.post(use: create)
        protected.put(":id", use: update)
        protected.delete(":id", use: delete)
        protected.post(":id", "like", use: toggleLike)
        protected.post(":id", "comments", use: addComment)
    }
    
    // ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ์กฐํšŒ (ํŽ˜์ด์ง€๋„ค์ด์…˜)
    func index(req: Request) async throws -> Page<Post.Public> {
        let posts = try await Post.query(on: req.db)
            .with(\.$author)
            .with(\.$tags)
            .sort(\.$createdAt, .descending)
            .paginate(for: req)
        
        return posts.map { $0.asPublic() }
    }
    
    // ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ ์กฐํšŒ
    func show(req: Request) async throws -> Post.Detail {
        guard let post = try await Post.find(req.parameters.get("id"), on: req.db) else {
            throw Abort(.notFound)
        }
        
        try await post.$author.load(on: req.db)
        try await post.$tags.load(on: req.db)
        try await post.$comments.load(on: req.db)
        try await post.$likes.load(on: req.db)
        
        return post.asDetail()
    }
    
    // ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ
    func create(req: Request) async throws -> Post.Public {
        let user = try req.auth.require(User.self)
        let input = try req.content.decode(Post.Create.self)
        
        let post = Post()
        post.title = input.title
        post.content = input.content
        post.slug = input.title.slugified()
        post.$author.id = try user.requireID()
        
        try await post.save(on: req.db)
        
        // ํƒœ๊ทธ ์ถ”๊ฐ€
        if let tagNames = input.tags {
            for tagName in tagNames {
                let tag = try await Tag.query(on: req.db)
                    .filter(\.$name == tagName)
                    .first() ?? Tag(name: tagName)
                
                if tag.id == nil {
                    try await tag.save(on: req.db)
                }
                
                try await post.$tags.attach(tag, on: req.db)
            }
        }
        
        try await post.$author.load(on: req.db)
        try await post.$tags.load(on: req.db)
        
        return post.asPublic()
    }
    
    // ๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •
    func update(req: Request) async throws -> Post.Public {
        let user = try req.auth.require(User.self)
        guard let post = try await Post.find(req.parameters.get("id"), on: req.db) else {
            throw Abort(.notFound)
        }
        
        // ์ž‘์„ฑ์ž๋งŒ ์ˆ˜์ • ๊ฐ€๋Šฅ
        guard post.$author.id == user.id else {
            throw Abort(.forbidden)
        }
        
        let input = try req.content.decode(Post.Update.self)
        post.title = input.title ?? post.title
        post.content = input.content ?? post.content
        
        try await post.save(on: req.db)
        try await post.$author.load(on: req.db)
        try await post.$tags.load(on: req.db)
        
        return post.asPublic()
    }
    
    // ๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ
    func delete(req: Request) async throws -> HTTPStatus {
        let user = try req.auth.require(User.self)
        guard let post = try await Post.find(req.parameters.get("id"), on: req.db) else {
            throw Abort(.notFound)
        }
        
        guard post.$author.id == user.id else {
            throw Abort(.forbidden)
        }
        
        try await post.delete(on: req.db)
        return .noContent
    }
    
    // ์ข‹์•„์š” ํ† ๊ธ€
    func toggleLike(req: Request) async throws -> Post.LikeStatus {
        let user = try req.auth.require(User.self)
        guard let postId = req.parameters.get("id", as: UUID.self) else {
            throw Abort(.badRequest)
        }
        
        let existingLike = try await Like.query(on: req.db)
            .filter(\.$post.$id == postId)
            .filter(\.$user.$id == try user.requireID())
            .first()
        
        if let like = existingLike {
            try await like.delete(on: req.db)
            return Post.LikeStatus(liked: false, count: try await countLikes(postId: postId, db: req.db))
        } else {
            let like = Like()
            like.$post.id = postId
            like.$user.id = try user.requireID()
            try await like.save(on: req.db)
            return Post.LikeStatus(liked: true, count: try await countLikes(postId: postId, db: req.db))
        }
    }
    
    // ๋Œ“๊ธ€ ์ถ”๊ฐ€
    func addComment(req: Request) async throws -> Comment.Public {
        let user = try req.auth.require(User.self)
        guard let postId = req.parameters.get("id", as: UUID.self) else {
            throw Abort(.badRequest)
        }
        
        let input = try req.content.decode(Comment.Create.self)
        
        let comment = Comment()
        comment.content = input.content
        comment.$post.id = postId
        comment.$user.id = try user.requireID()
        
        try await comment.save(on: req.db)
        try await comment.$user.load(on: req.db)
        
        return comment.asPublic()
    }
    
    // ๊ฒ€์ƒ‰
    func search(req: Request) async throws -> [Post.Public] {
        guard let query = req.query[String.self, at: "q"] else {
            throw Abort(.badRequest, reason: "Search query required")
        }
        
        let posts = try await Post.query(on: req.db)
            .group(.or) { group in
                group.filter(\.$title, .custom("ILIKE"), "%\(query)%")
                group.filter(\.$content, .custom("ILIKE"), "%\(query)%")
            }
            .with(\.$author)
            .with(\.$tags)
            .all()
        
        return posts.map { $0.asPublic() }
    }
    
    // ํƒœ๊ทธ๋ณ„ ์กฐํšŒ
    func byTag(req: Request) async throws -> [Post.Public] {
        guard let tagName = req.parameters.get("tagName") else {
            throw Abort(.badRequest)
        }
        
        guard let tag = try await Tag.query(on: req.db)
            .filter(\.$name == tagName)
            .first() else {
            return []
        }
        
        try await tag.$posts.load(on: req.db)
        
        for post in tag.posts {
            try await post.$author.load(on: req.db)
            try await post.$tags.load(on: req.db)
        }
        
        return tag.posts.map { $0.asPublic() }
    }
    
    private func countLikes(postId: UUID, db: Database) async throws -> Int {
        try await Like.query(on: db)
            .filter(\.$post.$id == postId)
            .count()
    }
}

// DTO (Data Transfer Object) ์ •์˜
extension Post {
    struct Create: Content {
        var title: String
        var content: String
        var tags: [String]?
    }
    
    struct Update: Content {
        var title: String?
        var content: String?
    }
    
    struct Public: Content {
        var id: UUID?
        var title: String
        var slug: String
        var content: String
        var author: User.Public
        var tags: [String]
        var createdAt: Date?
        var updatedAt: Date?
    }
    
    struct Detail: Content {
        var id: UUID?
        var title: String
        var slug: String
        var content: String
        var author: User.Public
        var tags: [String]
        var comments: [Comment.Public]
        var likeCount: Int
        var createdAt: Date?
        var updatedAt: Date?
    }
    
    struct LikeStatus: Content {
        var liked: Bool
        var count: Int
    }
    
    func asPublic() -> Public {
        Public(
            id: id,
            title: title,
            slug: slug,
            content: content,
            author: author.publicView,
            tags: tags.map { $0.name },
            createdAt: createdAt,
            updatedAt: updatedAt
        )
    }
    
    func asDetail() -> Detail {
        Detail(
            id: id,
            title: title,
            slug: slug,
            content: content,
            author: author.publicView,
            tags: tags.map { $0.name },
            comments: comments.map { $0.asPublic() },
            likeCount: likes.count,
            createdAt: createdAt,
            updatedAt: updatedAt
        )
    }
}

extension Comment {
    struct Create: Content {
        var content: String
    }
    
    struct Public: Content {
        var id: UUID?
        var content: String
        var user: User.Public
        var createdAt: Date?
    }
    
    func asPublic() -> Public {
        Public(
            id: id,
            content: content,
            user: user.publicView,
            createdAt: createdAt
        )
    }
}

extension String {
    func slugified() -> String {
        let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-"))
        return self.lowercased()
            .components(separatedBy: allowed.inverted)
            .filter { !$0.isEmpty }
            .joined(separator: "-")
    }
}

์™„์„ฑ์ด์•ผ! ์ด์ œ ์™„์ „ํžˆ ๋™์ž‘ํ•˜๋Š” ๋ธ”๋กœ๊ทธ API๊ฐ€ ์ƒ๊ฒผ์–ด. ๐ŸŽ‰

๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋ฐœ์ „์‹œํ‚ค๊ธฐ ์ด ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋” ๋งŽ์€ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด:
โ€ข ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ (AWS S3 ์—ฐ๋™)
โ€ข ์ด๋ฉ”์ผ ์•Œ๋ฆผ (SendGrid ๋“ฑ)
โ€ข ์†Œ์…œ ๋กœ๊ทธ์ธ (OAuth)
โ€ข ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ (WebSocket)
โ€ข ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ
โ€ข ํ†ต๊ณ„ ๋ฐ ๋ถ„์„

ํ˜น์‹œ Swift ์„œ๋ฒ„ ๊ฐœ๋ฐœ์— ๋Œ€ํ•ด ๋” ๋ฐฐ์šฐ๊ณ  ์‹ถ๊ฑฐ๋‚˜, ํ”„๋กœ์ ํŠธ์— ๋„์›€์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€๋“ค์˜ ๋„์›€์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด!
## ๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค์™€ ์ปค๋ฎค๋‹ˆํ‹ฐ Swift ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๊ฐœ๋ฐœ์„ ๊ณ„์† ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ๋ฆฌ์†Œ์Šค๋“ค์„ ํ™œ์šฉํ•ด๋ด! ๐Ÿ“š

### ๐Ÿ“– ๊ณต์‹ ๋ฌธ์„œ
๐Ÿ”— ํ•„์ˆ˜ ๋ฌธ์„œ๋“ค Vapor ๊ณต์‹ ๋ฌธ์„œ
https://docs.vapor.codes
๊ฐ€์žฅ ์ตœ์‹  ์ •๋ณด์™€ ํŠœํ† ๋ฆฌ์–ผ์ด ์žˆ์–ด. ์˜์–ด์ง€๋งŒ ์˜ˆ์ œ๊ฐ€ ๋งŽ์•„์„œ ๋”ฐ๋ผํ•˜๊ธฐ ์‰ฌ์›Œ.

Perfect ๊ณต์‹ ๋ฌธ์„œ
https://www.perfect.org/docs/
Perfect์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ๊ณผ API ๋ ˆํผ๋Ÿฐ์Šค๊ฐ€ ์ •๋ฆฌ๋˜์–ด ์žˆ์–ด.

Swift.org ์„œ๋ฒ„ ์›Œํ‚น ๊ทธ๋ฃน
https://www.swift.org/sswg/
Swift ์„œ๋ฒ„ ์ƒํƒœ๊ณ„์˜ ๋ฐฉํ–ฅ์„ฑ๊ณผ ํ‘œ์ค€์„ ๋…ผ์˜ํ•˜๋Š” ๊ณณ์ด์•ผ.
### ๐Ÿ’ฌ ์ปค๋ฎค๋‹ˆํ‹ฐ
๐ŸŒ ํ™œ๋ฐœํ•œ ์ปค๋ฎค๋‹ˆํ‹ฐ๋“ค Vapor Discord
์‹ค์‹œ๊ฐ„์œผ๋กœ ์งˆ๋ฌธํ•˜๊ณ  ๋‹ต๋ณ€๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๊ณณ. ๊ฐœ๋ฐœ์ž๋“ค์ด ์ •๋ง ์นœ์ ˆํ•ด!

Swift Forums
https://forums.swift.org
Swift ์ „๋ฐ˜์— ๋Œ€ํ•œ ๊นŠ์ด ์žˆ๋Š” ํ† ๋ก ์ด ์ด๋ฃจ์–ด์ง€๋Š” ๊ณต์‹ ํฌ๋Ÿผ.

Reddit r/swift
Swift ๊ด€๋ จ ๋‰ด์Šค์™€ ํ† ๋ก ์„ ๋ณผ ์ˆ˜ ์žˆ์–ด.

Stack Overflow
vapor, perfect, swift-server ํƒœ๊ทธ๋กœ ๊ฒ€์ƒ‰ํ•˜๋ฉด ๋งŽ์€ Q&A๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์–ด.
### ๐Ÿ“บ ํ•™์Šต ์ž๋ฃŒ
๐ŸŽฅ ์ถ”์ฒœ ๊ฐ•์˜์™€ ์ž๋ฃŒ Ray Wenderlich
Server-Side Swift with Vapor ์‹œ๋ฆฌ์ฆˆ๊ฐ€ ์ •๋ง ์ข‹์•„. ์œ ๋ฃŒ์ง€๋งŒ ๊ทธ๋งŒํ•œ ๊ฐ€์น˜๊ฐ€ ์žˆ์–ด.

YouTube
"Server-Side Swift", "Vapor Tutorial" ๊ฐ™์€ ํ‚ค์›Œ๋“œ๋กœ ๊ฒ€์ƒ‰ํ•˜๋ฉด ๋ฌด๋ฃŒ ๊ฐ•์˜๋“ค์ด ๋งŽ์•„.

GitHub
์‹ค์ œ ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ๋“ค์„ ๋ณด๋ฉด์„œ ๋ฐฐ์šฐ๋Š” ๊ฒŒ ์ตœ๊ณ ์•ผ. vapor/vapor ๋ ˆํฌ์ง€ํ† ๋ฆฌ์˜ ์ฝ”๋“œ๋ฅผ ์ฝ์–ด๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๊ณต๋ถ€๊ฐ€ ๋ผ.

๊ทธ๋ฆฌ๊ณ  ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์‹ค๋ฌด ๊ฒฝํ—˜์ด ์žˆ๋Š” ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ 1:1๋กœ ๋ฐฐ์šธ ์ˆ˜๋„ ์žˆ์–ด. ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์„ ๋ฐ”๋กœ๋ฐ”๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์–ด์„œ ํ•™์Šต ์†๋„๊ฐ€ ํ›จ์”ฌ ๋นจ๋ผ์ง€์ง€!
### ๐Ÿ“š ์ถ”์ฒœ ๋„์„œ ์‹ค๋ฌผ ์ฑ…์œผ๋กœ ๊ณต๋ถ€ํ•˜๋Š” ๊ฑธ ์„ ํ˜ธํ•œ๋‹ค๋ฉด:
โ€ข "Server-Side Swift" by Paul Hudson
โ€ข "Practical Server-Side Swift" by Tibor Bรถdecs
โ€ข "Vapor 4 Complete Guide" (์˜จ๋ผ์ธ eBook)

## ๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌ: Swift ์„œ๋ฒ„ ๊ฐœ๋ฐœ์˜ ๋ฏธ๋ž˜ ์ž, ์—ฌ๊ธฐ๊นŒ์ง€ Swift ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๊ฐœ๋ฐœ์— ๋Œ€ํ•ด ์•Œ์•„๋ดค์–ด! ๊ธธ์—ˆ์ง€? ๐Ÿ˜…

Swift๋กœ ์„œ๋ฒ„๋ฅผ ๊ฐœ๋ฐœํ•˜๋Š” ๊ฑด ์•„์ง์€ Node.js๋‚˜ Python๋งŒํผ ๋ฉ”์ธ์ŠคํŠธ๋ฆผ์€ ์•„๋‹ˆ์•ผ. ํ•˜์ง€๋งŒ ๋ช‡ ๊ฐ€์ง€ ํ™•์‹คํ•œ ์žฅ์ ์ด ์žˆ์–ด:

โœจ Swift ์„œ๋ฒ„ ๊ฐœ๋ฐœ์˜ ์žฅ์  ์ •๋ฆฌ 1. ํƒ€์ž… ์•ˆ์ •์„ฑ
์ปดํŒŒ์ผ ํƒ€์ž„์— ๋งŽ์€ ๋ฒ„๊ทธ๋ฅผ ์žก์•„๋‚ผ ์ˆ˜ ์žˆ์–ด. ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ๊ฐ€ ํ™• ์ค„์–ด๋“ค์ง€.

2. ์„ฑ๋Šฅ
C/C++ ์ˆ˜์ค€์˜ ์„ฑ๋Šฅ์„ ์ž๋ž‘ํ•ด. ํŠนํžˆ CPU ์ง‘์•ฝ์ ์ธ ์ž‘์—…์—์„œ ๋น›์„ ๋ฐœํ•ด.

3. ์ฝ”๋“œ ๊ณต์œ 
iOS ์•ฑ๊ณผ ์„œ๋ฒ„ ๊ฐ„์— ๋ชจ๋ธ ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์–ด. ๊ฐœ๋ฐœ ํšจ์œจ์ด ์—„์ฒญ๋‚˜๊ฒŒ ์˜ฌ๋ผ๊ฐ€.

4. ๋ชจ๋˜ํ•œ ๋ฌธ๋ฒ•
async/await, ์˜ต์…”๋„, ํ”„๋กœํ† ์ฝœ ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋“ฑ ๊ฐœ๋ฐœ์ž ์นœํ™”์ ์ธ ๊ธฐ๋Šฅ๋“ค์ด ๋งŽ์•„.

5. ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์ „์„ฑ
ARC ๋•๋ถ„์— ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๊ฐ€ ์ž๋™ํ™”๋˜๋ฉด์„œ๋„ ์•ˆ์ „ํ•ด.
### ๐Ÿ”ฎ ์•ž์œผ๋กœ์˜ ์ „๋ง Swift ์„œ๋ฒ„ ์‚ฌ์ด๋“œ๋Š” ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด. ํŠนํžˆ:
โ€ข **Swift Concurrency**: async/await๊ฐ€ ๋”์šฑ ์„ฑ์ˆ™ํ•ด์ง€๊ณ  ์žˆ์–ด
โ€ข **์„ฑ๋Šฅ ๊ฐœ์„ **: ๋งค ๋ฒ„์ „๋งˆ๋‹ค ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋˜๊ณ  ์žˆ์–ด
โ€ข **์ƒํƒœ๊ณ„ ํ™•์žฅ**: ๋” ๋งŽ์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ๋„๊ตฌ๋“ค์ด ๋‚˜์˜ค๊ณ  ์žˆ์–ด
โ€ข **๊ธฐ์—… ๋„์ž…**: ์ ์  ๋” ๋งŽ์€ ๊ธฐ์—…๋“ค์ด ๊ด€์‹ฌ์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด

ํŠนํžˆ iOS ์ค‘์‹ฌ์˜ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“œ๋Š” ํŒ€์ด๋ผ๋ฉด, Swift ์„œ๋ฒ„ ์‚ฌ์ด๋“œ๋Š” ์ •๋ง ๋งค๋ ฅ์ ์ธ ์„ ํƒ์ง€์•ผ. ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ๋ฅผ ๊ฐ™์€ ์–ธ์–ด๋กœ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑด ์—„์ฒญ๋‚œ ์ด์ ์ด๊ฑฐ๋“ !

### ๐Ÿš€ ์‹œ์ž‘ํ•ด๋ณด์ž! ์ด์ œ ๋„ˆ๋„ Swift๋กœ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ์ฒ˜์Œ์—” ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ๋”ฐ๋ผํ•˜๋‹ค ๋ณด๋ฉด ๊ธˆ๋ฐฉ ์ต์ˆ™ํ•ด์งˆ ๊ฑฐ์•ผ.

์ž‘์€ ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด๋ด. ๊ฐ„๋‹จํ•œ TODO API๋‚˜ ๋ฉ”๋ชจ ์•ฑ ๋ฐฑ์—”๋“œ ๊ฐ™์€ ๊ฑฐ ๋ง์ด์•ผ. ๊ทธ๋Ÿฌ๋‹ค ๋ณด๋ฉด ์ ์  ๋ณต์žกํ•œ ์‹œ์Šคํ…œ๋„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ๋  ๊ฑฐ์•ผ.

๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๊ณ , ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜๊ณ , ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์˜ ์ฝ”๋“œ๋ฅผ ์ฝ์–ด๋ด. ๊ทธ๋ฆฌ๊ณ  ๊ณ„์† ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒŒ ๊ฐ€์žฅ ์ค‘์š”ํ•ด!

๐Ÿ’ก ๋งˆ์ง€๋ง‰ ํŒ โ€ข ์™„๋ฒฝํ•œ ์ฝ”๋“œ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ์ž‘์„ฑํ•˜๋ ค๊ณ  ํ•˜์ง€ ๋งˆ. ์ผ๋‹จ ๋™์ž‘ํ•˜๊ฒŒ ๋งŒ๋“ค๊ณ  ๋ฆฌํŒฉํ† ๋งํ•ด
โ€ข ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ์Šต๊ด€์„ ๋“ค์—ฌ. ๋‚˜์ค‘์— ๋„ˆ๋ฅผ ๊ตฌํ•ด์ค„ ๊ฑฐ์•ผ
โ€ข ๋ณด์•ˆ์€ ์ ˆ๋Œ€ ์†Œํ™€ํžˆ ํ•˜์ง€ ๋งˆ. ํ•œ ๋ฒˆ์˜ ์‹ค์ˆ˜๊ฐ€ ํฐ ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ์–ด
โ€ข ์ปค๋ฎค๋‹ˆํ‹ฐ์— ๊ธฐ์—ฌํ•ด๋ด. ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ์— PR์„ ๋ณด๋‚ด๊ฑฐ๋‚˜ ๋ธ”๋กœ๊ทธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์•„
โ€ข ์ฆ๊ธฐ๋ฉด์„œ ํ•ด! ์ฝ”๋”ฉ์€ ์žฌ๋ฏธ์žˆ์–ด์•ผ ํ•ด ๐Ÿ˜Š
์ž, ์ด์ œ ๋„ˆ์˜ ์ฒซ Swift ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค๋Ÿฌ ๊ฐ€๋ณผ๊นŒ? ํ™”์ดํŒ…! ๐Ÿš€โœจ

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

Happy Coding! ๐ŸŽ‰
๋Œ“๊ธ€ ์ž‘์„ฑ

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

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

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