Ver2.0 ๐ Lumen์ผ๋ก ๊ฐ๋ณ๊ฒ ๋ ์์ค๋ฅด๋ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ ๊ตฌํ ๊ฐ์ด๋ ๐

๐ Lumen์ผ๋ก ๊ฐ๋ณ๊ฒ ๋ ์์ค๋ฅด๋ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ ๊ตฌํ ๊ฐ์ด๋ ๐
์๋ ํ์ธ์ ๊ฐ๋ฐ์ ์ฌ๋ฌ๋ถ! ์ค๋์ PHP ๊ฒฝ๋ ํ๋ ์์ํฌ์ ์จ์ ๋ณด์ ๐ Lumen์ ํ์ฉํด์ ์ด๊ฒฝ๋ ๋ง์ดํฌ๋ก์๋น์ค๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ํด ํจ๊ป ์์๋ณผ๊ฒ์! 2025๋ ํ์ฌ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๋ ๋์ด์ ์ ํ์ด ์๋ ํ์๊ฐ ๋์์ฃ . ๊ฐ๋ฒผ์ฐ๋ฉด์๋ ๊ฐ๋ ฅํ Lumen์ผ๋ก ์ฌ๋ฌ๋ถ์ ์๋น์ค๋ฅผ ๋ ๋ ตํ๊ฒ ๋ง๋ค์ด๋ด์! ๐
๐ ๋ชฉ์ฐจ
- Lumen๊ณผ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ ์๊ฐ
- Lumen ์ค์น ๋ฐ ํ๊ฒฝ ๊ตฌ์ฑํ๊ธฐ
- ์ฒซ ๋ฒ์งธ ๋ง์ดํฌ๋ก์๋น์ค ๋ง๋ค๊ธฐ
- ์๋น์ค ๊ฐ ํต์ ๊ตฌํํ๊ธฐ
- API ๊ฒ์ดํธ์จ์ด ๊ตฌ์ถํ๊ธฐ
- ์ธ์ฆ ๋ฐ ๋ณด์ ๊ตฌํํ๊ธฐ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋ ๋ฐ ์ต์ ํ
- ์๋น์ค ๋ฐฐํฌ ๋ฐ ๋ชจ๋ํฐ๋ง
- ์ค์ ํ๋ก์ ํธ ์ฌ๋ก ๋ถ์
- ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ์ ๋ฏธ๋
1. Lumen๊ณผ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ ์๊ฐ ๐
์์ฆ ๊ฐ๋ฐ ํธ๋ ๋ ๋ณด๋ฉด ๋ค๋ค ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ ์๊ธฐ๋ง ํ๋ ๊ฑฐ ๊ฐ์ฃ ? ใ ใ ใ ๊ทผ๋ฐ ์ง์ง ๋์ธ๊ฐ ๋ ์ด์ ๊ฐ ์์ด์! ํนํ PHP ์ํ๊ณ์์๋ Laravel์ ๋์๋ป์ธ Lumen์ด ๋ง์ดํฌ๋ก์๋น์ค ๊ตฌํ์ ์๋ฒฝํ ํํธ๋๋ก ์๋ฆฌ์ก๊ณ ์๋ต๋๋ค.
๐ค ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๊ฐ ๋ญ๊ธธ๋?
๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๋ ํ๋์ ํฐ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ๋ฌ ๊ฐ์ ์์ ๋ ๋ฆฝ์ ์ธ ์๋น์ค๋ก ๋ถ๋ฆฌํ๋ ๋ฐฉ์์ด์์. ๊ฐ ์๋น์ค๋ ์์ฒด ํ๋ก์ธ์ค์์ ์คํ๋๊ณ , API๋ฅผ ํตํด ๋ค๋ฅธ ์๋น์ค์ ํต์ ํด์. ์ด๋ ๊ฒ ํ๋ฉด ๊ฐ๋ฐ, ํ ์คํธ, ๋ฐฐํฌ๊ฐ ํจ์ฌ ์ฌ์์ง๊ณ , ํ์ฅ์ฑ๋ ์ข์์ง๋ต๋๋ค!
Lumen์ด ๋ญ๊ธธ๋ ์ด๋ ๊ฒ ํซํ๊ฑฐ์ฃ ? ๐ฅ
Lumen์ Laravel์ ์ฐฝ์์ Taylor Otwell์ด ๋ง๋ ์ด๊ฒฝ๋ PHP ๋ง์ดํฌ๋ก ํ๋ ์์ํฌ์์. Laravel์ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ์ ์ ์งํ๋ฉด์๋ ๋ถํ์ํ ๋ถ๋ถ์ ๊ณผ๊ฐํ ์ ๊ฑฐํด์ ์๋์ ํจ์จ์ฑ์ ๊ทน๋ํํ์ด์. 2025๋ ํ์ฌ Lumen 10.x ๋ฒ์ ๊น์ง ๋์๋๋ฐ, ์ฑ๋ฅ์ด ๋ฏธ์ณค๋ค๋๊น์? ๐ฒ
Lumen vs Laravel: ๋ญ๊ฐ ๋ค๋ฅธ๋ฐ? ๐คทโโ๏ธ
Laravel์ด ํ์คํ ํ๋ ์์ํฌ๋ผ๋ฉด, Lumen์ API ๊ฐ๋ฐ์ ํนํ๋ ๊ฒฝ๋ ๋ฒ์ ์ด์์. Laravel์ ๋ฌด๊ฑฐ์ด ๊ธฐ๋ฅ๋ค(์ธ์ , ๋ทฐ, ํ ํ๋ฆฟ ๋ฑ)์ ๊ณผ๊ฐํ ์ ๊ฑฐํด์ ์๋๋ฅผ ๊ทน๋ํํ์ฃ . ์ค์ ๋ก ๋ฒค์น๋งํฌ ํ ์คํธ์์ Lumen์ Laravel๋ณด๋ค ์ต๋ 2๋ฐฐ ๋น ๋ฅธ ์ฑ๋ฅ์ ๋ณด์ฌ์ค๋ต๋๋ค! ๐๏ธ๐จ
| ํน์ง | Laravel | Lumen |
|---|---|---|
| ์ฉ๋ | ํ์คํ ์น ์ ํ๋ฆฌ์ผ์ด์ | ๋ง์ดํฌ๋ก์๋น์ค, API |
| ํ์ผ ํฌ๊ธฐ | ๋ฌด๊ฑฐ์ (์ฝ 30MB+) | ๊ฐ๋ฒผ์ (์ฝ 1MB) |
| ์๋ | ๋ณดํต | ๋งค์ฐ ๋น ๋ฆ |
| ์ค์ ํ์ผ | ๋ง์ | ์ต์ํ |
| ํ ํ๋ฆฟ ์์ง | Blade ๋ด์ฅ | ๊ธฐ๋ณธ ์ ๊ณต ์ ํจ |
์ด์ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์ ๊ฐ๋ฐํ ๋๋ ๋ชจ๋๋ฆฌ์๋ณด๋ค๋ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๊ฐ ๋์ธ์ธ ์ด์ ๋ฅผ ์์๊ฒ ์ฃ ? ๐ ๊ฐ ๊ธฐ๋ฅ๋ณ๋ก ๋ ๋ฆฝ์ ์ธ ์๋น์ค๋ฅผ ๋ง๋ค๋ฉด ์ ์ง๋ณด์๋ ์ฝ๊ณ ํ์ฅ์ฑ๋ ์ข์์ง๋๊น์!
2. Lumen ์ค์น ๋ฐ ํ๊ฒฝ ๊ตฌ์ฑํ๊ธฐ ๐ ๏ธ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก Lumen์ ์ค์นํ๊ณ ๊ฐ๋ฐ ํ๊ฒฝ์ ๊ตฌ์ฑํด๋ณผ๊ฒ์! ์ง์ง ์ฌ์์ ๋๋๊ฑธ์? ใ ใ ใ
ํ์ ์๊ตฌ์ฌํญ ์ฒดํฌํ๊ธฐ โ
Lumen 10.x(2025๋ ์ต์ ๋ฒ์ )๋ฅผ ์ค์นํ๊ธฐ ์ํ ์๊ตฌ์ฌํญ์ ๋ค์๊ณผ ๊ฐ์์:
- PHP >= 8.2
- OpenSSL PHP Extension
- PDO PHP Extension
- Mbstring PHP Extension
- 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๋ณด๋ค ํจ์ฌ ๋จ์ํด์. ์ฃผ์ ๋๋ ํ ๋ฆฌ๋ ๋ค์๊ณผ ๊ฐ์์:
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);
์ด๋ ๊ฒ ํ๋ฉด ๊ฒฐ์ ์ฒ๋ฆฌ์ ์๋ฆผ ์ ์ก์ด ๋น๋๊ธฐ์ ์ผ๋ก ์ด๋ฃจ์ด์ ธ์! ์ฌ์ฉ์๋ ๊ฒฐ์ ์๋ฃ ์๋ต์ ๋ฐ๋ก ๋ฐ์ ์ ์๊ณ , ์๋ฆผ์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ฒ๋ฆฌ๋์ฃ . ๐
์๋น์ค ๋์ค์ปค๋ฒ๋ฆฌ ๊ตฌํํ๊ธฐ ๐
๋ง์ดํฌ๋ก์๋น์ค๊ฐ ๋ง์์ง๋ฉด ๊ฐ ์๋น์ค์ ์์น(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 ๊ฒ์ดํธ์จ์ด์ ์ฅ์
- ํด๋ผ์ด์ธํธ๋ ๋จ์ผ ์๋ํฌ์ธํธ๋ง ์๋ฉด ๋จ
- ์ธ์ฆ/์ธ๊ฐ๋ฅผ ์ค์์์ ์ฒ๋ฆฌ ๊ฐ๋ฅ
- ์์ฒญ/์๋ต ๋ณํ ๋ฐ ์ง๊ณ ๊ฐ๋ฅ
- ์๋ ์ ํ, ์บ์ฑ ๋ฑ ๊ณตํต ๊ธฐ๋ฅ ๊ตฌํ ๊ฐ๋ฅ
- ์๋น์ค ๋์ค์ปค๋ฒ๋ฆฌ ํตํฉ ๊ฐ๋ฅ
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์ ์ธ์ฆ ๋ฏธ๋ค์จ์ด๋ฅผ ์ถ๊ฐํด๋ณผ๊ฒ์:
<?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');
});
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);
์ด๋ ๊ฒ ํ๋ฉด ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ๋ณดํธํ ์ ์์ด์! ๐
โ ๏ธ ๋ณด์ ์ฃผ์์ฌํญ
- ํ๊ฒฝ ๋ณ์ ๋ณดํธ: .env ํ์ผ์ ์๋ ๋น๋ฐ ํค์ ์๊ฒฉ ์ฆ๋ช ์ ์์ ํ๊ฒ ๋ณด๊ดํ์ธ์.
- HTTPS ์ฌ์ฉ: ๋ชจ๋ API ํต์ ์ HTTPS๋ฅผ ํตํด ์ด๋ฃจ์ด์ ธ์ผ ํด์.
- ์ ๋ ฅ ์ ํจ์ฑ ๊ฒ์ฌ: ๋ชจ๋ ์ฌ์ฉ์ ์ ๋ ฅ์ ์ฒ ์ ํ ๊ฒ์ฆํด์ผ ํด์.
- ํ ํฐ ๋ง๋ฃ ์๊ฐ ์ค์ : JWT ํ ํฐ์๋ ์ ์ ํ ๋ง๋ฃ ์๊ฐ์ ์ค์ ํ์ธ์.
- ๋ก๊น ๊ณผ ๋ชจ๋ํฐ๋ง: ๋ณด์ ์ด๋ฒคํธ๋ฅผ ๋ก๊น ํ๊ณ ๋ชจ๋ํฐ๋งํ์ธ์.
7. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋ ๋ฐ ์ต์ ํ ๐พ
๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ๋ ๋งค์ฐ ์ค์ํ ๋ถ๋ถ์ด์์. ๊ฐ ์๋น์ค๊ฐ ๋ ๋ฆฝ์ ์ผ๋ก ๋์ํ๋ ค๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์ ์ ํ ๋ถ๋ฆฌ๋์ด์ผ ํ์ฃ . ์ด๋ป๊ฒ ๊ตฌํํ๋์ง ์์๋ณผ๊น์? ๐ง
๋ฐ์ดํฐ๋ฒ ์ด์ค ํจํด ์ ํํ๊ธฐ ๐
๋ง์ดํฌ๋ก์๋น์ค์์๋ ์ฃผ๋ก ๋ ๊ฐ์ง ๋ฐ์ดํฐ๋ฒ ์ด์ค ํจํด์ ์ฌ์ฉํด์:
- ๋ฐ์ดํฐ๋ฒ ์ด์ค per ์๋น์ค: ๊ฐ ๋ง์ดํฌ๋ก์๋น์ค๊ฐ ์์ฒด ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๊ฐ์ง
- ๊ณต์ ๋ฐ์ดํฐ๋ฒ ์ด์ค: ์ฌ๋ฌ ์๋น์ค๊ฐ ํ๋์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๊ณต์
์ผ๋ฐ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค per ์๋น์ค ํจํด์ด ๋ง์ดํฌ๋ก์๋น์ค ์ฒ ํ์ ๋ ๋ถํฉํด์. ๊ฐ ์๋น์ค๊ฐ ์์ ํ ๋ ๋ฆฝ์ ์ผ๋ก ๋์ํ ์ ์์ผ๋๊น์! ํ์ง๋ง ๋ฐ์ดํฐ ์ผ๊ด์ฑ ์ ์ง๊ฐ ์ด๋ ค์ธ ์ ์์ด์.
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);
}
// ๋ค๋ฅธ ๋ฉ์๋๋ค...
}
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ต์ ํ ๊ธฐ๋ฒ ๐
๋ง์ดํฌ๋ก์๋น์ค์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฑ๋ฅ์ ๋งค์ฐ ์ค์ํด์. ๋ค์์ ๋ช ๊ฐ์ง ์ต์ ํ ๊ธฐ๋ฒ์ด์์:
- ์ธ๋ฑ์ค ํ์ฉ: ์์ฃผ ์กฐํํ๋ ์ปฌ๋ผ์ ์ธ๋ฑ์ค๋ฅผ ์ถ๊ฐํ์ธ์.
- ์ฟผ๋ฆฌ ์ต์ ํ: N+1 ๋ฌธ์ ๋ฅผ ํผํ๊ธฐ ์ํด Eager Loading์ ์ฌ์ฉํ์ธ์.
- ์ปค๋ฅ์ ํ๋ง: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ์ ์ฌ์ฌ์ฉํ์ฌ ์ค๋ฒํค๋๋ฅผ ์ค์ด์ธ์.
- ์บ์ฑ: ์์ฃผ ์ฌ์ฉํ๋ ๋ฐ์ดํฐ๋ Redis๋ Memcached์ ์บ์ฑํ์ธ์.
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๋ฉ: ๋๊ท๋ชจ ๋ฐ์ดํฐ๋ฅผ ์ฌ๋ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ถ์ฐํ์ธ์.
์๋ฅผ ๋ค์ด, 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
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 ๊ธฐ๋ฐ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๊ฐ ์ด๋ป๊ฒ ํ์ฉ๋๋์ง ์์๋ณผ๊ฒ์! ๐ง
์ฌ๋ฅ ๊ณต์ ํ๋ซํผ ๋ง์ดํฌ๋ก์๋น์ค ๊ตฌํ ์ฌ๋ก ๐
์ฌ๋ฅ๋ท๊ณผ ๊ฐ์ ์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๋ก ๊ตฌํํ๋ค๋ฉด ์ด๋ป๊ฒ ๋ ๊น์? ๋ค์๊ณผ ๊ฐ์ ์๋น์ค๋ก ๋๋ ์ ์์ด์:
- ์ฌ์ฉ์ ์๋น์ค: ํ์๊ฐ์ , ๋ก๊ทธ์ธ, ํ๋กํ ๊ด๋ฆฌ
- ์ฌ๋ฅ ์๋น์ค: ์ฌ๋ฅ ๋ฑ๋ก, ์กฐํ, ๊ฒ์
- ์ฃผ๋ฌธ ์๋น์ค: ์ฌ๋ฅ ๊ตฌ๋งค, ์ฃผ๋ฌธ ๊ด๋ฆฌ
- ๊ฒฐ์ ์๋น์ค: ๊ฒฐ์ ์ฒ๋ฆฌ, ํ๋ถ
- ๋ฆฌ๋ทฐ ์๋น์ค: ๋ฆฌ๋ทฐ ์์ฑ, ์กฐํ
- ์๋ฆผ ์๋น์ค: ์ด๋ฉ์ผ, ํธ์ ์๋ฆผ
- ๊ฒ์ ์๋น์ค: ํตํฉ ๊ฒ์ ๊ธฐ๋ฅ
๊ฐ ์๋น์ค๋ Lumen์ผ๋ก ๊ตฌํํ๊ณ , API ๊ฒ์ดํธ์จ์ด๋ฅผ ํตํด ํด๋ผ์ด์ธํธ์ ํต์ ํด์. ์๋น์ค ๊ฐ ํต์ ์ HTTP API์ ๋ฉ์์ง ํ๋ฅผ ์กฐํฉํ์ฌ ๊ตฌํํ ์ ์์ด์.
์ฃผ์ ๊ธฐ๋ฅ ๊ตฌํ ์ฌ๋ก ๐ก
์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์ ์ฃผ์ ๊ธฐ๋ฅ์ ๋ง์ดํฌ๋ก์๋น์ค๋ก ์ด๋ป๊ฒ ๊ตฌํํ ์ ์๋์ง ์ดํด๋ณผ๊ฒ์:
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;
}
}
}
์ฑ๋ฅ ์ต์ ํ ์ฌ๋ก ๐
์ฌ๋ฅ๋ท๊ณผ ๊ฐ์ ํ๋ซํผ์์๋ ์ฑ๋ฅ ์ต์ ํ๊ฐ ๋งค์ฐ ์ค์ํด์. ๋ค์์ ์ค์ ํ๋ก์ ํธ์์ ์ ์ฉํ ์ ์๋ ์ฑ๋ฅ ์ต์ ํ ๊ธฐ๋ฒ์ด์์:
- API ์๋ต ์บ์ฑ: ์์ฃผ ์์ฒญ๋๋ API ์๋ต์ Redis์ ์บ์ฑํ์ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ถํ ๊ฐ์
- ์ด๋ฏธ์ง ์ต์ ํ: CDN์ ์ฌ์ฉํ์ฌ ์ด๋ฏธ์ง ๋ฐ ์ ์ ํ์ผ ์ ๊ณต ์๋ ํฅ์
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ฑ: ์์ฃผ ์กฐํ๋๋ ํ๋์ ์ธ๋ฑ์ค๋ฅผ ์ถ๊ฐํ์ฌ ์ฟผ๋ฆฌ ์ฑ๋ฅ ํฅ์
- ์ํ์ ํ์ฅ: ํธ๋ํฝ์ด ๋ง์ ์๋น์ค๋ ์ฌ๋ฌ ์ธ์คํด์ค๋ก ํ์ฅํ์ฌ ๋ถํ ๋ถ์ฐ
- ๋น๋๊ธฐ ์ฒ๋ฆฌ: ๊ฒฐ์ ํ์ธ, ์ด๋ฉ์ผ ์ ์ก ๋ฑ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ์์ ์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌ
๐ก ์ค์ ์ฌ๋ก - ์ฌ๋ฅ๋ท์ ์ฑ๋ฅ ๊ฐ์
์ฌ๋ฅ๋ท ํ๋ซํผ์ ์ด๊ธฐ์ ๋ชจ๋๋ฆฌ์ ์ํคํ ์ฒ๋ก ๊ตฌ์ถ๋์์ง๋ง, ์ฌ์ฉ์๊ฐ ์ฆ๊ฐํ๋ฉด์ ์ฑ๋ฅ ์ด์๊ฐ ๋ฐ์ํ์ด์. Lumen ๊ธฐ๋ฐ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๋ก ์ ํํ ํ ๋ค์๊ณผ ๊ฐ์ ๊ฐ์ ํจ๊ณผ๋ฅผ ์ป์์ต๋๋ค:
- API ์๋ต ์๊ฐ 60% ๊ฐ์
- ์๋ฒ ์์ ์ฌ์ฉ๋ฅ 40% ๊ฐ์
- ๋์ ์ฌ์ฉ์ ์ฒ๋ฆฌ ๋ฅ๋ ฅ 3๋ฐฐ ํฅ์
- ์๋ก์ด ๊ธฐ๋ฅ ์ถ์ ์ฃผ๊ธฐ 2์ฃผ์์ 3์ผ๋ก ๋จ์ถ
ํนํ ๊ฒ์ ์๋น์ค๋ฅผ ๋ณ๋๋ก ๋ถ๋ฆฌํ๊ณ Elasticsearch๋ฅผ ๋์ ํ ๊ฒฐ๊ณผ, ๋ณต์กํ ๊ฒ์ ์ฟผ๋ฆฌ์ ์๋ต ์๊ฐ์ด 2์ด์์ 0.2์ด๋ก ๋จ์ถ๋์์ต๋๋ค!
10. ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ์ ๋ฏธ๋ ๐ฎ
๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๋ ๊ณ์ ๋ฐ์ ํ๊ณ ์์ด์. 2025๋ ํ์ฌ์ ํธ๋ ๋์ ์์ผ๋ก์ ๋ฐ์ ๋ฐฉํฅ์ ์์๋ณผ๊ฒ์! ๐
ํ์ฌ ํธ๋ ๋ (2025๋ ) ๐
2025๋ ํ์ฌ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ์์ ์ฃผ๋ชฉ๋ฐ๊ณ ์๋ ํธ๋ ๋๋ ๋ค์๊ณผ ๊ฐ์์:
- ์๋ฒ๋ฆฌ์ค ์ํคํ ์ฒ: AWS Lambda, Google Cloud Functions ๊ฐ์ ์๋ฒ๋ฆฌ์ค ํ๋ซํผ์ ํ์ฉํ ๋ง์ดํฌ๋ก์๋น์ค ๊ตฌํ
- ๋ฉ์ ์ํคํ ์ฒ: ์๋น์ค ๋ฉ์(Istio, Linkerd)๋ฅผ ํ์ฉํ ์๋น์ค ๊ฐ ํต์ ๊ด๋ฆฌ
- GitOps: Git์ ์ค์ฌ์ผ๋ก ํ ์ธํ๋ผ ๋ฐ ์ ํ๋ฆฌ์ผ์ด์ ๋ฐฐํฌ ์๋ํ
- eBPF: ์ปค๋ ์์ค์ ๊ด์ฐฐ์ฑ๊ณผ ๋คํธ์ํน ์ต์ ํ
- AI ๊ธฐ๋ฐ ์ด์: ์ธ๊ณต์ง๋ฅ์ ํ์ฉํ ๋ง์ดํฌ๋ก์๋น์ค ๋ชจ๋ํฐ๋ง ๋ฐ ์ต์ ํ
Lumen๋ ์ด๋ฌํ ํธ๋ ๋์ ๋ง์ถฐ ๋ฐ์ ํ๊ณ ์์ด์. PHP 8.3์ ์ฑ๋ฅ ํฅ์๊ณผ ํจ๊ป Lumen์ ์๋์ ํจ์จ์ฑ์ด ๋์ฑ ๊ฐ์ ๋์์ด์!
์์ผ๋ก์ ๋ฐ์ ๋ฐฉํฅ ๐ญ
๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๋ ์์ผ๋ก ๋ค์๊ณผ ๊ฐ์ ๋ฐฉํฅ์ผ๋ก ๋ฐ์ ํ ๊ฒ์ผ๋ก ์์๋ผ์:
- ์์จ ๋ง์ดํฌ๋ก์๋น์ค: AI๊ฐ ์๋น์ค์ ํ์ฅ, ๋ณต๊ตฌ, ์ต์ ํ๋ฅผ ์๋์ผ๋ก ์ฒ๋ฆฌ
- ์ฃ์ง ์ปดํจํ ํตํฉ: ๋ง์ดํฌ๋ก์๋น์ค๋ฅผ ์ฌ์ฉ์์ ๊ฐ๊น์ด ์ฃ์ง์ ๋ฐฐํฌํ์ฌ ์ง์ฐ ์๊ฐ ์ต์ํ
- WebAssembly: ์ธ์ด์ ๊ตฌ์ ๋ฐ์ง ์๋ ๊ณ ์ฑ๋ฅ ๋ง์ดํฌ๋ก์๋น์ค ์คํ ํ๊ฒฝ
- ์์ ์ปดํจํ ํตํฉ: ํน์ ์ฐ์ฐ์ ์์ ์ปดํจํ ์ ํ์ฉํ ์ด๊ณ ์ ์ฒ๋ฆฌ
- ์ ๋ก ํธ๋ฌ์คํธ ๋ณด์: ๋ชจ๋ ์๋น์ค ๊ฐ ํต์ ์ ๊ฐ๋ ฅํ ์ธ์ฆ๊ณผ ์ํธํ ์ ์ฉ
PHP์ Lumen๋ ์ด๋ฌํ ๋ณํ์ ์ ์ํ๊ณ ์์ด์. JIT ์ปดํ์ผ๋ฌ์ ๋ฐ์ ๊ณผ ๋น๋๊ธฐ ํ๋ก๊ทธ๋๋ฐ ์ง์ ๊ฐํ๋ก ๋์ฑ ๋น ๋ฅด๊ณ ํจ์จ์ ์ธ ๋ง์ดํฌ๋ก์๋น์ค๋ฅผ ๊ตฌํํ ์ ์๊ฒ ๋ ๊ฑฐ์์!
Lumen์ ๋ฏธ๋ ๐
Lumen์ ๊ฒฝ๋ ๋ง์ดํฌ๋ก์๋น์ค ํ๋ ์์ํฌ๋ก์ ๊ณ์ํด์ ๋ฐ์ ํ๊ณ ์์ด์. ์์ผ๋ก์ Lumen์ ๋ค์๊ณผ ๊ฐ์ ํน์ง์ ๊ฐ์ง ๊ฒ์ผ๋ก ์์๋ผ์:
- ๋ ๋น ๋ฅธ ์ฑ๋ฅ: PHP 8.3+์ JIT ์ปดํ์ผ๋ฌ์ ์ต์ ํ๋ ์ฝ๋๋ก ๋์ฑ ๋น ๋ฅธ ์ฑ๋ฅ ์ ๊ณต
- ๋น๋๊ธฐ ์ง์ ๊ฐํ: ReactPHP, Swoole ๋ฑ๊ณผ์ ํตํฉ์ผ๋ก ๋น๋๊ธฐ ํ๋ก๊ทธ๋๋ฐ ์ง์ ๊ฐํ
- ์๋ฒ๋ฆฌ์ค ์ต์ ํ: AWS Lambda, Google Cloud Functions ๋ฑ ์๋ฒ๋ฆฌ์ค ํ๊ฒฝ์ ์ต์ ํ๋ ๋ฐฐํฌ ์ง์
- AI ํตํฉ: ์ธ๊ณต์ง๋ฅ ์๋น์ค์์ ํตํฉ์ ์ํ ๋๊ตฌ ๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๊ณต
- ๋ณด์ ๊ฐํ: ์ ๋ก ํธ๋ฌ์คํธ ๋ชจ๋ธ์ ์ง์ํ๋ ๋ณด์ ๊ธฐ๋ฅ ๊ฐํ
โ ๏ธ ์ฃผ์์ฌํญ
์๋ก์ด ๊ธฐ์ ์ ๋์ ํ ๋๋ ์์ ์ฑ๊ณผ ์ฑ์๋๋ฅผ ๊ณ ๋ คํด์ผ ํด์. ๋ชจ๋ ์ต์ ํธ๋ ๋๊ฐ ๋ชจ๋ ํ๋ก์ ํธ์ ์ ํฉํ ๊ฒ์ ์๋์์. ํ๋ก์ ํธ์ ์๊ตฌ์ฌํญ๊ณผ ํ์ ์ญ๋์ ๋ง๋ ๊ธฐ์ ์ ์ ํํ๋ ๊ฒ์ด ์ค์ํด์!
๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ์ ๋์ ๊ณผ์ ๐งฉ
๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๋ ๋ง์ ์ฅ์ ์ด ์์ง๋ง, ์ฌ์ ํ ํด๊ฒฐํด์ผ ํ ๋์ ๊ณผ์ ๋ ์์ด์:
- ๋ณต์ก์ฑ ๊ด๋ฆฌ: ์๋น์ค๊ฐ ๋ง์์ง์๋ก ์ ์ฒด ์์คํ ์ ๋ณต์ก์ฑ์ด ์ฆ๊ฐ
- ๋ถ์ฐ ํธ๋์ญ์ : ์ฌ๋ฌ ์๋น์ค์ ๊ฑธ์น ํธ๋์ญ์ ์ ์ผ๊ด์ฑ ์ ์ง
- ํ ์คํธ ๋ณต์ก์ฑ: ์ฌ๋ฌ ์๋น์ค ๊ฐ์ ํตํฉ ํ ์คํธ ๊ตฌํ ์ด๋ ค์
- ์ด์ ์ค๋ฒํค๋: ์ฌ๋ฌ ์๋น์ค๋ฅผ ๋ชจ๋ํฐ๋งํ๊ณ ๊ด๋ฆฌํ๋ ๋ถ๋ด
- ํ ์กฐ์ง: ๋ง์ดํฌ๋ก์๋น์ค์ ์ ํฉํ ํ ๊ตฌ์กฐ์ ํ์ ๋ฐฉ์ ํ์
์ด๋ฌํ ๋์ ๊ณผ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์๋น์ค ๋ฉ์, ๋ถ์ฐ ์ถ์ , ์๋ํ๋ ํ ์คํธ ๋ฑ์ ๋๊ตฌ์ ๋ฐฉ๋ฒ๋ก ์ด ๊ณ์ ๋ฐ์ ํ๊ณ ์์ด์!
๊ฒฐ๋ก ๐ฏ
Lumen์ ์ด์ฉํ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๋ ํ์ฅ์ฑ, ์ ์ฐ์ฑ, ๊ฐ๋ฐ ์๋๋ฅผ ์ค์์ํ๋ ํ๋ ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ์ ๋งค์ฐ ์ ํฉํด์. ํนํ ์ฌ๋ฅ๋ท๊ณผ ๊ฐ์ ํ๋ซํผ์์๋ ๊ฐ ๊ธฐ๋ฅ์ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฐ๋ฐํ๊ณ ๋ฐฐํฌํ ์ ์์ด ๋น ๋ฅด๊ฒ ๋ณํํ๋ ์์ฅ์ ๋์ํ ์ ์์ด์!
PHP์ Lumen์ด ๊ฐ๋ณ๊ณ ๋น ๋ฅด๋ค๋ ์ฅ์ ์ ์ด๋ ค ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๋ฅผ ๊ตฌํํ๋ค๋ฉด, ๋์ ์ฑ๋ฅ๊ณผ ๊ฐ๋ฐ ์์ฐ์ฑ์ ๋ชจ๋ ์ป์ ์ ์์ด์. ์์ผ๋ก๋ ๊ณ์ํด์ ๋ฐ์ ํ๋ ๋ง์ดํฌ๋ก์๋น์ค ๊ธฐ์ ๊ณผ ํจ๊ป Lumen๋ ๋์ฑ ๊ฐ๋ ฅํด์ง ๊ฒ์ผ๋ก ๊ธฐ๋๋ผ์! ๐
๐ก ๋ง์ง๋ง ํ!
๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๋ฅผ ๋์ ํ ๋๋ ์ ์ง์ ์ธ ์ ๊ทผ ๋ฐฉ์์ ์ทจํ๋ ๊ฒ์ด ์ข์์. ๋ชจ๋ ๊ฒ์ ํ ๋ฒ์ ๋ง์ดํฌ๋ก์๋น์ค๋ก ์ ํํ๊ธฐ๋ณด๋ค๋, ๊ฐ์ฅ ์ค์ํ๊ณ ๋ ๋ฆฝ์ ์ธ ๊ธฐ๋ฅ๋ถํฐ ์์ํ์ฌ ์ ์ฐจ ํ์ฅํด ๋๊ฐ์ธ์. ์ฌ๋ฅ๋ท๊ณผ ๊ฐ์ ํ๋ซํผ์์๋ ์ฌ์ฉ์ ๊ด๋ฆฌ, ๊ฒฐ์ , ๊ฒ์ ๋ฑ ํต์ฌ ๊ธฐ๋ฅ๋ถํฐ ๋ง์ดํฌ๋ก์๋น์ค๋ก ๋ถ๋ฆฌํ๊ณ , ๊ฒฝํ์ ์์๊ฐ๋ฉฐ ์ ์ฒด ์์คํ ์ ์งํ์ํค๋ ๊ฒ์ด ์ฑ๊ณต์ ์ด์ ์์!
๋ง๋ฌด๋ฆฌ ๐
์ง๊ธ๊น์ง Lumen์ ์ด์ฉํ ๊ฒฝ๋ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ ๊ตฌํ์ ๋ํด ์์๋ดค์ด์! ์ฒ์๋ถํฐ ๋๊น์ง ํจ๊ป ํด์ฃผ์ ์ ๊ฐ์ฌํด์. ์ด ๊ธ์ด ์ฌ๋ฌ๋ถ์ ๋ง์ดํฌ๋ก์๋น์ค ์ฌ์ ์ ๋์์ด ๋์๊ธธ ๋ฐ๋๋๋ค! ๐
์ฌ๋ฅ๋ท์์๋ ๋ค์ํ ๊ฐ๋ฐ ๊ด๋ จ ์ง์๊ณผ ๋ ธํ์ฐ๋ฅผ ๊ณต์ ํ๊ณ ์์ด์. ๊ฐ๋ฐ์๋ก์์ ์ฌ๋ฅ์ ๋๋๊ณ ์ถ๊ฑฐ๋, ํ์ํ ๊ฐ๋ฐ ์๋น์ค๋ฅผ ์ฐพ๊ณ ๊ณ์๋ค๋ฉด ์ฌ๋ฅ๋ท(https://www.jaenung.net)์ ๋ฐฉ๋ฌธํด๋ณด์ธ์!
๋ ๋ง์ ๊ฐ๋ฐ ๊ด๋ จ ์ง์์ ์ฌ๋ฅ๋ท์ '์ง์์ธ์ ์ฒ' ๋ฉ๋ด์์ ์ฐพ์๋ณด์ค ์ ์์ด์. ์ฌ๋ฌ๋ถ์ ๊ฐ๋ฐ ์ฌ์ ์ ํญ์ ํ์ด์ด ํจ๊ปํ๊ธธ ๋ฐ๋๋๋ค! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.