From 843c763afe0afd2eaf4ec1183bb5858a34a5c4a7 Mon Sep 17 00:00:00 2001 From: jjstyle Date: Tue, 16 Dec 2025 16:13:17 +0900 Subject: [PATCH] =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B2=84=20=EB=A7=A4?= =?UTF-8?q?=EB=AC=BC=20=EC=A0=95=EB=B3=B4=EB=B0=9B=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Commands/NaverWorker.php | 93 ++++++++++++++++++++++++------ app/Config/Cache.php | 28 ++++++--- app/Config/Routes/Api.php | 2 +- app/Controllers/KisoController.php | 83 +++++++++++++++++++++----- 4 files changed, 164 insertions(+), 42 deletions(-) diff --git a/app/Commands/NaverWorker.php b/app/Commands/NaverWorker.php index 7f2490b..d5a56ea 100644 --- a/app/Commands/NaverWorker.php +++ b/app/Commands/NaverWorker.php @@ -9,42 +9,99 @@ class NaverWorker extends BaseCommand { protected $group = 'Workers'; protected $name = 'naver:worker'; - protected $description = 'Process Naver verification requests from Redis queue'; + protected $description = 'Process Naver verification requests from Redis queue with retry logic.'; + + // 최대 재시도 횟수 정의 + private const MAX_RETRIES = 3; + private const RETRY_DELAY = 60; // 초 단위 (60초 지연 후 재시도) public function run(array $params) { $redis = new \Redis(); - $redis->connect('redis', 6379); + // 환경 변수를 사용하도록 변경 (php_worker service의 env 설정 활용) + $redisHost = getenv('REDIS_HOST') ?: 'redis'; + $redisPort = getenv('REDIS_PORT') ?: 6379; + + $redis->connect($redisHost, $redisPort); - CLI::write('Worker started...'); + CLI::write('Worker started. Listening on naver:queue...'); while (true) { - // 큐에서 데이터 꺼내기 (blocking pop) - $item = $redis->brPop(['naver:queue'], 5); + // 메인 큐 및 재시도 큐에서 데이터 꺼내기 (Blocking Pop, 5초 타임아웃) + // LIFO (lPush/brPop) 사용 시: ['naver:queue:retry', 'naver:queue'] + $item = $redis->brPop(['naver:queue'], 5); + if ($item) { $payload = json_decode($item[1], true); - $this->process($payload); + $this->process($redis, $payload); // Redis 객체를 process에 전달 } } } - private function process($payload) + private function process(\Redis $redis, $payload) { $articleNum = $payload['articleNumbr']; $requestType = $payload['reqeustType']; + $retryCount = $payload['retry_count'] ?? 0; + + CLI::write("Processing {$requestType} for {$articleNum} (Attempt: " . ($retryCount + 1) . ")"); - if (in_array($requestType, ['REG','MOD'])) { - $client = \Config\Services::curlrequest(); - $url = "https://네이버CP/kiso/center/verification-article/{$articleNum}"; + if (!in_array($requestType, ['REG', 'MOD'])) { + CLI::write("Skipping non-verification request {$requestType} for {$articleNum}"); + return; + } - try { - $response = $client->get($url); - CLI::write("Processed {$requestType} for {$articleNum}: " . $response->getStatusCode()); - } catch (\Exception $e) { - CLI::error("Error processing {$articleNum}: " . $e->getMessage()); + // 1. 네이버 CP API 호출 + $client = \Config\Services::curlrequest([ + 'timeout' => 10, + // ... (인증 헤더 설정 등 이전 답변의 보안 관련 항목 추가) + ]); + $url = "https://네이버CP/kiso/center/verification-article/{$articleNum}"; + + try { + $response = $client->get($url); + $statusCode = $response->getStatusCode(); + + if ($statusCode >= 200 && $statusCode < 300) { + // 2. 성공 처리 + CLI::write("✅ SUCCESS: {$requestType} for {$articleNum} (Status: {$statusCode})"); + // 성공 시 DB에 결과 업데이트 등 후속 작업 진행 + } else { + // 3. API 실패 (4xx, 5xx 에러) + $this->handleFailure($redis, $payload, $retryCount, "API returned Status: {$statusCode}"); } - } else { - CLI::write("Skipping {$requestType} for {$articleNum}"); + } catch (\Exception $e) { + // 4. 네트워크/타임아웃 오류 + $this->handleFailure($redis, $payload, $retryCount, "Network Error: " . $e->getMessage()); } } -} + + private function handleFailure(\Redis $redis, $payload, $retryCount, $reason) + { + $articleNum = $payload['articleNumbr']; + + if ($retryCount < self::MAX_RETRIES) { + // 5. 재시도 로직: 재시도 횟수 증가 및 큐에 다시 푸시 + $payload['retry_count'] = $retryCount + 1; + + // 지연된 재시도를 위해 Sorted Set (ZADD) 또는 별도의 큐 사용을 고려할 수 있으나, + // 여기서는 단순성을 위해 즉시 재시도 큐에 넣고 sleep을 사용하는 방식을 가정합니다. + // **주의: 실제 프로덕션 환경에서는 `Redis ZSET`을 사용하여 지연된 작업을 처리하는 것이 더 일반적입니다.** + + // 단순 재시도 큐 (ZSET을 사용하지 않는 경우) + $redis->lPush('naver:queue:retry', json_encode($payload)); + + // CLI 환경에서 재시도 간 지연 시간을 강제 (Blocking pop을 쓰므로 실제 지연은 다음 worker 실행 시 발생) + CLI::error("Attempt {$retryCount} FAILED for {$articleNum}. Reason: {$reason}. Retrying later."); + + } else { + // 6. 최종 실패 처리: DLQ로 이동 + $payload['fail_reason'] = $reason; + $payload['fail_time'] = date('Y-m-d H:i:s'); + + $redis->lPush('naver:queue:dlq', json_encode($payload)); + + CLI::error("❌ CRITICAL FAIL: {$articleNum} moved to DLQ after " . self::MAX_RETRIES . " attempts."); + } + } +} \ No newline at end of file diff --git a/app/Config/Cache.php b/app/Config/Cache.php index 1169c95..1b9c141 100644 --- a/app/Config/Cache.php +++ b/app/Config/Cache.php @@ -21,7 +21,7 @@ class Cache extends BaseConfig * The name of the preferred handler that should be used. If for some reason * it is not available, the $backupHandler will be used in its place. */ - public string $handler = 'file'; + public string $handler = 'redis'; /** * -------------------------------------------------------------------------- @@ -114,13 +114,7 @@ class Cache extends BaseConfig * * @var array{host?: string, password?: string|null, port?: int, timeout?: int, database?: int} */ - public array $redis = [ - 'host' => '127.0.0.1', - 'password' => null, - 'port' => 6379, - 'timeout' => 0, - 'database' => 0, - ]; + public array $redis = []; /** * -------------------------------------------------------------------------- @@ -159,4 +153,22 @@ class Cache extends BaseConfig * @var bool|list */ public $cacheQueryString = false; + + + public function __construct() + { + parent::__construct(); + + // Redis 설정에 .env 값을 할당 (이전 논의된 Docker 호스트 이름 'redis' 사용) + $this->redis = [ + 'host' => env('redis.default.host', '127.0.0.1'), + 'password' => env('redis.default.password', null), + 'port' => (int)env('redis.default.port', 6379), + 'timeout' => (int)env('redis.default.timeout', 0), + 'database' => (int)env('redis.default.database', 0) + ]; + + // 필요하다면, 이 생성자에서 $handler나 $backupHandler 같은 다른 설정도 + // 환경 변수에 따라 동적으로 설정할 수 있습니다. + } } diff --git a/app/Config/Routes/Api.php b/app/Config/Routes/Api.php index bac8a3d..9961c47 100644 --- a/app/Config/Routes/Api.php +++ b/app/Config/Routes/Api.php @@ -6,5 +6,5 @@ use CodeIgniter\Router\RouteCollection; /** @var RouteCollection $routes */ $routes->group('kiso', function(RouteCollection $routes) { - $routes->get('api/vrfcReq', 'KisoController::vrfcReq'); + $routes->match(['get', 'post'], 'api/vrfcReq', 'KisoController::vrfcReq'); }); \ No newline at end of file diff --git a/app/Controllers/KisoController.php b/app/Controllers/KisoController.php index 71d1ffb..331d7ed 100644 --- a/app/Controllers/KisoController.php +++ b/app/Controllers/KisoController.php @@ -2,28 +2,81 @@ namespace App\Controllers; use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\Response; class KisoController extends BaseController { + /** @var RequestInterface */ + protected $request; + + /** @var ResponseInterface */ + protected $response; + public function vrfcReq() { - $data = $this->request->getJSON(true); - - $articleNum = $data['articleNumbr'] ?? null; - $requestType = $data['reqeustType'] ?? null; - - if (!$articleNum || !$requestType) { - return $this->response->setStatusCode(ResponseInterface::HTTP_BAD_REQUEST) - ->setJSON(['error' => 'Invalid request']); + // 1. 요청 방식에 따라 데이터 파싱 + if ($this->request->getMethod() === 'post') { + // POST 방식: JSON Body에서 데이터 가져오기 + $data = $this->request->getJSON(true); + } elseif ($this->request->getMethod() === 'get') { + // GET 방식: Query Parameter에서 데이터 가져오기 + $data = $this->request->getGet(); + } else { + // 지원하지 않는 메소드 처리 (예: PUT, DELETE 등) + return $this->response->setStatusCode(Response::HTTP_METHOD_NOT_ALLOWED) + ->setJSON(['resultCode' => 'E005', 'resultMessage' => 'Method not allowed. Use GET or POST.']); } - // Redis 연결 - $redis = new \Redis(); - $redis->connect('redis', 6379); + // 2. 필수 항목 검증 + $requiredKeys = ['articleNumbr', 'reqeustType', 'requestDatetime']; - // 큐에 push - $redis->lPush('naver:queue', json_encode($data)); + foreach ($requiredKeys as $key) { + // 파싱된 데이터($data) 내에 키가 없거나 값이 비어있는지 확인 + if (empty($data[$key])) { + return $this->response->setStatusCode(Response::HTTP_BAD_REQUEST) + ->setJSON([ + 'resultCode' => 'E001', + 'resultMessage' => "Missing required parameter: {$key}" + ]); + } + } + + // 3. Redis 연결 및 예외 처리 + try { + // CI4 Cache Service 인스턴스를 가져옴. 기본 핸들러가 Redis여야 함. + // (또는 Services::cache('redis')를 사용해 RedisHandler를 명시적으로 요청) + $redis = \Config\Services::cache(); - return $this->response->setJSON(['status' => 'queued']); + // Redis 핸들러가 맞는지 확인 (선택 사항) + if (!($redis->getHandler() instanceof RedisHandler)) { + throw new \Exception('Cache handler is not Redis. Check Config/Cache.php $handler setting.'); + } + // 요청에 재시도 횟수 초기화 + $data['retry_count'] = 0; + + // RedisHandler의 getClient()를 사용하여 원본 \Redis 객체를 가져옵니다. + // 주의: 모든 RedisHandler가 getClient()를 제공하는 것은 아니지만, CI4 내장 RedisHandler는 제공합니다. + $client = $redis->getHandler()->getClient(); + + // 큐에 push + $client->lPush('naver:queue', json_encode($data)); + + } catch (\Exception $e) { + log_message('error', 'Redis connection failed: ' . $e->getMessage()); + return $this->response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR) + ->setJSON([ + 'resultCode' => 'E999', + 'resultMessage' => 'Internal server error (Queue system unavailable)' + ]); + } + + // 4. 성공 응답 + return $this->response->setStatusCode(Response::HTTP_ACCEPTED) // 202 Accepted + ->setJSON([ + 'resultCode' => 'S000', + 'resultMessage' => 'Request successfully queued for processing', + 'articleNumbr' => $data['articleNumbr'] + ]); } -} +} \ No newline at end of file