목록

2026.08.29 23:56
조회 11
0
0

안녕하세요. 예전이나 지금이나 항상 눈팅만 하고 있습니다.
운영진분들께는 항상 감사하고 있습니다.

여기에서 AI VIBE CODING을 알게 됐고 결제하고 이런 저런 필요한 것들을 만들어서 사용하고 있는데 참 편한 것 같습니다.
Claude 에서 코딩하고  Codex에서 다시 검증하고 다시  Claude에서 수정하는 식으로 합니다.

 

전 @에카님이 공개하신 https://rhymix.org/tip/1941047 이 걸 MCP서버로 해서 애드온이나 모듈을 만들고 었는데요.
여러개의 모듈 만들면서 자꾸 동일한 부분에서  AI가 자꾸 헤매서 라이믹스 함정이라는 문서를 따로 만들었는데 많은 분들이 AI VIBE CODING을 하시니 이런 것도 다른분들에게 도움이 될까 해서 써 봅니다.
md 파일은 첨부가 안되서 그냥 붙여넣었습니다.
 


# 36. 실전 함정 (Pitfalls)

 

문서화되지 않았거나 찾기 어려워 실제 개발 중 시간을 크게 잡아먹은 항목들. 증상 → 원인 → 해결 순으로 정리한다.

 

각 항목의 **증상** 줄에는 실제로 화면이나 로그에 나타나는 문자열을 그대로 적었다. 오류 메시지로 검색해 바로 찾을 수 있게 하기 위함이다.

 

---

 

## 모듈 구조

 

### 36.1 모듈 이름에 밑줄을 쓰면 클래스를 못 찾는다

 

**증상**: `Class "Rhymix\Modules\FooBar\Controllers\Base" not found`

 

**원인**: 오토로더는 `Rhymix\Modules\<모듈명>\Sub\Class` 를 `modules/<모듈명 소문자>/sub/Class.php` 로 매핑한다. 모듈 디렉토리가 `foo_bar` 인데 PHP 관례대로 네임스페이스를 `FooBar` 로 쓰면 `modules/foobar/` 를 찾으러 가서 실패한다.

 

`ModuleHandler` 는 `sprintf('Rhymix\\Modules\\%s\\%s', $this->module, $class_name)` 로 클래스명을 만든다. `$this->module` 은 디렉토리 이름 그대로다.

 

**해결**: **모듈 이름은 밑줄 없는 한 단어로.** `foo_bar` → `foobar`. `<namespaces>` 로 네임스페이스를 바꿔도 오토로더의 디렉토리 매핑은 그대로라 해결되지 않는다.

 

관련: `26-namespaces-and-autoload.md`

 

### 36.2 액션 이름에서 모듈명이 추출된다

 

**증상**: `ERR_ACT_NOT_FOUND` (`ModuleHandler.class.php:487`). 액션은 module.xml 에 분명히 있는데도 관리자 화면(`module=admin&act=...`)에서 못 찾는다.

 

**원인**: `ModuleHandler.class.php:478` 이 액션 이름에서 정규식으로 모듈명을 뽑아낸다.

 

```php

preg_match('/^[a-z]+([A-Z][a-z0-9\_]+).*$/', $this->act, $matches);

$module = strtolower($matches[1]);

```

 

대문자 하나 뒤에 **소문자만** 오므로 `dispFooBarAdminConfig` 는 `Foo` 에서 끊긴다. `foo` 모듈로 잘못 찾아간다.

 

**해결**: 액션명에 모듈명 전체를 **한 덩어리로** 넣는다. 모듈이 `foobar` 면 `dispFoobarAdminConfig`. 두 번째 단어의 첫 글자를 소문자로 쓰는 셈이라 어색하지만, 관리자 화면 진입이 이 규칙에 의존한다.

 

### 36.3 사이트맵 '메뉴 추가' 목록의 닭-달걀

 

**증상**: 모듈을 설치했는데 사이트 메뉴 편집 → 메뉴 추가 목록에 안 뜬다. 그래서 인스턴스(mid)를 만들 수 없다.

 

**원인**: `MenuAdminModel::getModuleListInSitemap()` 은 `instanceCount >= 1` 인 모듈만 목록에 넣는다. 인스턴스가 없으면 목록에 안 뜨고, 목록에 안 뜨면 인스턴스를 못 만든다.

 

**해결**: `menu.getModuleListInSitemap` **after 트리거**로 모듈 이름을 배열에 직접 추가한다. board 모듈도 같은 방식이다.

 

```xml

<eventHandler after="menu.getModuleListInSitemap"

              class="Controllers\Hook" method="triggerModuleListInSitemap" />

```

 

```php

public function triggerModuleListInSitemap(&$obj)

{

    if (is_array($obj) && !in_array('foobar', $obj, true))

    {

        $obj[] = 'foobar';

    }

    return new BaseObject;

}

```

 

`skins/` 디렉토리도 있어야 메뉴 항목으로 만들 수 있다.

 

---

 

## 액션과 라우팅

 

### 36.4 `proc*` 는 기본 POST 전용

 

**증상**: "이 요청에 사용할 수 없는 HTTP 메소드입니다" (`ModuleHandler.class.php:378`)

 

**원인**: `method=` 미지정 시 `proc*` 는 POST only 로 결정된다. 브라우저가 링크로 진입하는 자리(외부 인증 시작, OAuth 콜백 등)는 GET 이다.

 

**해결**: `method="GET"` 또는 `method="GET,POST"` 를 명시한다.

 

반대 방향도 있다. **`disp*` 에 `class=` 가 붙으면 기본이 GET only** 라 POST 폼을 제출하면 405 가 난다.

 

### 36.5 `disp*` 액션에서 `setRedirectUrl()` 이 동작하지 않는다

 

**증상**: 리다이렉트 코드를 넣었는데 그냥 화면이 렌더링된다.

 

**원인**: `disp` 는 화면을 그리는 흐름이라 리다이렉트 지시가 무시된다. `proc*` 에서만 유효하다.

 

**해결**: 조건부 이동이 필요하면 리다이렉트 대신 화면에 버튼/링크를 둔다. 사용자 입장에서도 자기가 어디 있는지 알 수 있어 더 낫다.

 

### 36.6 `standalone` 라우트는 action_forward 에 등록되어야 한다

 

**증상**: `route` 를 지정했는데 해당 URL 이 404.

 

**원인**: `standalone="true"`(따라서 `global_route="true"` 포함) 라우트는 `action_forward` DB 테이블에 등록되어야 mid 없는 URL 에서 탐색된다.

 

**해결**: 라우트를 추가·변경한 뒤 **관리자 모듈 목록에서 업데이트를 실행**한다 (`registerActionForwardRoutes()` 동기화). 등록 여부는 직접 확인할 수 있다.

 

```sql

SELECT * FROM rx_action_forward WHERE module='foobar';

```

 

`checkUpdate()` 에서 `ModuleModel::getActionForward($act)` 로 검사해 업데이트 버튼을 띄우면 이 실수를 예방할 수 있다.

 

### 36.7 `schemas/*.xml` 은 기존 테이블에 컬럼을 추가하지 않는다

 

**증상**: 스키마 XML 에 컬럼을 추가하고 모듈 업데이트를 실행했는데 `Unknown column 'xxx' in 'SET'`.

 

**원인**: `schemas/*.xml` 은 **테이블이 없을 때 생성**에만 쓰인다. 이미 설치된 사이트의 기존 테이블에는 자동으로 반영되지 않는다.

 

**해결**: install 클래스에서 직접 처리한다. 추가된 컬럼 목록을 상수로 관리하면 `checkUpdate()` 에서 업데이트 버튼을 띄우는 조건으로도 쓸 수 있다.

 

```php

const ADDED_COLUMNS = [

    ['table' => 'foo_items', 'name' => 'new_col', 'type' => 'number',

     'size' => '', 'default' => 0, 'notnull' => true],

];

 

// checkUpdate() 에서

foreach (self::ADDED_COLUMNS as $c) {

    if (!DB::getInstance()->isColumnExists($c['table'], $c['name'])) return true;

}

 

// moduleUpdate() 에서

DB::getInstance()->addColumn($c['table'], $c['name'], $c['type'],

                             $c['size'], $c['default'], $c['notnull']);

```

 

`DB::getInstance()` 의 `addColumn` / `modifyColumn` / `dropColumn` / `isColumnExists` 를 쓴다 (`14-database-and-queries.md`).

 

### 36.8 module.xml 을 고쳐도 업데이트 버튼이 안 뜬다

 

**증상**: 액션을 추가했는데 관리자 모듈 목록에 업데이트 버튼이 없어 반영할 방법이 없다.

 

**원인**: `checkUpdate()` 가 `false` 를 반환하면 버튼이 안 뜬다. 코어는 module.xml 변경을 자동으로 감지하지 않는다.

 

**해결**: install 클래스에 XML 버전 상수를 두고 module.xml 을 고칠 때마다 올린다.

 

```php

const XML_VERSION = 3;   // module.xml 을 고칠 때마다 +1

 

public function checkUpdate(): bool

{

    return (int) Config::get('xml_version') !== self::XML_VERSION;

}

```

 

CLI 강제 실행: `php index.php module.updateAllModules`

 

---

 

## 권한

 

### 36.9 mid 없는 요청은 사이트 기본 모듈의 권한을 물려받는다

 

**증상**: `permission="guest"` 인데도 비로그인 요청이 `/member` 로그인 화면으로 리다이렉트된다. `msg_not_permitted`.

 

**원인**: `ModuleObject::proc()` 이 액션 실행 직전에 `module_srl && !grant->access` 를 검사한다. `permission` 속성(액션 권한)과 **별개**다.

 

mid 없이 들어온 요청은 사이트 기본 모듈(예: `page` 인스턴스)의 `module_info` 와 grant 를 물려받는다. 기본 페이지의 `access` grant 가 `-1`(로그인 회원)이면 그 즉시 막힌다.

 

**해결**: 공개 엔드포인트는 **전용 mid 인스턴스**를 만들고 그 인스턴스의 접근 권한만 `모든 방문자` 로 연다. 사이트 전체 정책은 그대로 유지된다.

 

`.well-known` 같은 고정 경로가 필요하면 nginx 에서 mid 를 주입한다 (36.14 참고).

 

### 36.10 로그인 리다이렉트 애드온은 권한 검사보다 앞단이다

 

**증상**: mid 를 공개로 열었는데도 여전히 로그인 화면으로 튕긴다.

 

**원인**: `xe-login-redirect` 류 애드온이 `before_module_init` 에서 동작한다. 모듈 권한 검사보다 훨씬 앞이라 mid 를 열어도 소용없다.

 

**해결**: 애드온 설정의 **예외 mid** 에 별도로 등록한다. "대상" 설정에서 아무것도 체크하지 않으면 **모든 대상에 적용**되는 점도 주의.

 

---

 

## 트리거

 

### 36.11 `display` 트리거로 HTML 을 주입하면 사이트가 죽는다

 

**증상**: `PHP Fatal error: Cannot redeclare class Rhymix\Modules\Foo\Controllers\Hook (previously declared in .../Hook.php:9) in .../Hook.php on line 9` → 전 페이지 HTTP 500

 

**원인**: `before display` 핸들러를 등록하면 해당 클래스 파일이 두 번 include 된다. `display` 는 매 요청마다 돌므로 사이트 전체가 죽는다. 관리자 화면도 함께 죽어 복구 경로가 막힌다.

 

**해결**: `display` 트리거로 출력 HTML 을 조작하지 않는다. 스킨에 직접 넣거나 위젯을 쓴다.

 

**응급 복구** (관리자 화면조차 안 열릴 때):

 

```sql

DELETE FROM rx_module_trigger WHERE module='foobar';

```

 

---

 

## 입출력

 

### 36.12 요청 변수는 HTML 이스케이프되어 들어온다

 

**증상**: URL 파라미터로 받은 값의 `&` 가 `&amp;` 로 바뀌어 있다. 문자 단위 비교가 실패한다.

 

**원인**: `Context::get()` 은 화면 출력 안전을 위해 요청 변수를 이스케이프해서 돌려준다.

 

**해결**: 프로토콜 파라미터처럼 **원문이 그대로여야 하는 값**은 `QUERY_STRING` 을 직접 파싱한다.

 

```php

parse_str((string) ($_SERVER['QUERY_STRING'] ?? ''), $query);

$raw = $query['redirect_uri'] ?? '';

```

 

OAuth/OIDC 의 `redirect_uri`, `state`, `nonce`, `code_challenge` 가 모두 여기 해당한다. 서명·해시 검증 대상 값은 전부 원문으로 읽어야 한다.

 

### 36.13 `Context::setResponseMethod('JSON')` 은 규격 응답에 쓸 수 없다

 

**증상**: 응답 JSON 에 `error`, `message` 필드가 멋대로 섞여 외부 클라이언트가 거부한다.

 

**원인**: `JSONDisplayHandler` 가 `getVariables()` 전체를 내보내며 `error`/`message` 를 **항상** 덧붙인다. `RAW` 는 스킨 템플릿을 컴파일하는 구조라 JSON 생성에 부적합하다.

 

**해결**: 직접 출력하고 `exit`.

 

```php

while (ob_get_level() > 0) { ob_end_clean(); }

http_response_code($status);

header('Content-Type: application/json; charset=UTF-8');

header('Cache-Control: no-store');

echo json_encode($data, JSON_UNESCAPED_SLASHES);

exit;

```

 

**주의**: `exit` 하면 `DisplayHandler::printContent()` 의 세션 저장을 건너뛴다. 세션을 쓰는 액션에서는 절대 `exit` 하지 말 것. 반대로 세션이 불필요한 API 액션은 `session="false"` 를 걸어두는 편이 낫다.

 

---

 

## nginx / 인프라

 

### 36.14 `rewrite ... last` 로는 mid/act 를 주입할 수 없다

 

**증상**: nginx 에서 `rewrite ^ /index.php?mid=x&act=y last;` 로 보냈는데 라이믹스가 404 를 내거나 사이트 첫 페이지를 렌더링한다.

 

**원인**: `rewrite` 는 `$uri` 와 `$args` 만 바꾸고 **`$request_uri` 는 원본을 유지**한다. 라이믹스 라우터는 `REQUEST_URI` 의 경로 부분으로 mid/act 를 해석하므로 주입한 쿼리스트링이 무시된다.

 

**해결**: `fastcgi_param` 으로 직접 덮어쓴다. **`include fastcgi_params` 뒤에 와야** 이긴다.

 

```nginx

location = /.well-known/openid-configuration {

    fastcgi_pass  php:9000;

    include       fastcgi_params;

 

    fastcgi_param SCRIPT_FILENAME /var/www/html/index.php;

    fastcgi_param SCRIPT_NAME     /index.php;

    fastcgi_param DOCUMENT_URI    /index.php;

    fastcgi_param REQUEST_URI     /index.php?mid=x&act=y;

    fastcgi_param QUERY_STRING    mid=x&act=y;

    fastcgi_param HTTP_COOKIE     "";

    fastcgi_param HTTPS           on;

}

```

 

`HTTP_COOKIE ""` 는 공개 문서에서 세션 시작과 `Set-Cookie` 를 막는다.

 

**진단 요령**: 어느 파라미터가 유실되는지 모를 때는 프로브 파일을 두고 `SCRIPT_FILENAME` 만 그쪽으로 돌려 `$_SERVER` 와 `$_GET` 을 찍어본다.

 

**주의**: `nginx -s reload` 는 즉시 반영되지 않는다. 곧바로 curl 로 확인하면 옛 응답을 보고 엉뚱한 결론에 이른다. `sleep 1` 을 넣을 것.

 

### 36.15 `.well-known` 이 nginx dotfile 규칙에 막힌다

 

**증상**: `/.well-known/...` 이 403.

 

**원인**: 하드닝된 설정에 흔한 `location ~ /\. { deny all; }` 가 잡는다.

 

**해결**: 확장자를 명시하거나(`location ~ /\.(ht|git|env)`) `.well-known` 만 예외 처리한다. certbot 을 webroot 방식으로 쓰고 있다면 `acme-challenge` 예외만 있고 `.well-known` 전체는 아닐 수 있으니 확인이 필요하다.

 

---

 

## 화면

 

### 36.16 Blade 에 `@php` 디렉티브가 없다

 

**증상**: 템플릿에 쓴 `@php($x = ...)` 가 화면에 **문자열 그대로** 출력된다.

 

**원인**: 라이믹스의 Blade 구현에는 표준 Blade 의 `@php` 디렉티브가 없다. 인식하지 못한 디렉티브는 그냥 텍스트로 출력된다.

 

**해결**: 값 계산은 컨트롤러에서 하고 `Context::set()` 으로 넘긴다. 템플릿에서 꼭 계산해야 하면 표현식 안에서 함수를 호출한다.

 

```blade

{{-- 안 됨 --}}

@php($minutes = intdiv($sec, 60))

 

{{-- 됨 --}}

{{ intdiv($sec, 60) }}

```

 

확인된 사용 가능 범위: `{{ }}` `{!! !!}` `@if/@elseif/@else/@endif` `@foreach/@endforeach` `@selected` `@csrf` `@load` `@include`, 그리고 표현식 안의 PHP 함수 호출.

 

### 36.17 `setMessage()` 는 리다이렉트를 넘지 못한다

 

**증상**: 관리자 폼에서 저장을 눌렀는데 아무 안내도 없다. 저장이 됐는지 알 수 없다.

 

**원인**: `setMessage()` + `setRedirectUrl()` 조합에서 메시지가 유실된다. ajax 폼(`rx_ajax`)이면 alert 으로 뜨지만, 일반 폼 POST 후 리다이렉트하면 사라진다.

 

**해결**: 세션 플래시로 직접 전달한다.

 

```php

// proc 액션

$_SESSION['myflash'] = ['text' => '...', 'type' => 'ok'];

 

// disp 액션

Context::set('flash', $_SESSION['myflash'] ?? null);

unset($_SESSION['myflash']);

```

 

메시지는 "저장되었습니다" 보다 **실제로 벌어진 일**을 적는 편이 낫다. 부작용이 있는 작업(정지·삭제·키 재발급 등)은 색이나 문구로 구분해 관리자가 방금 무슨 일을 했는지 알 수 있게 한다.

 

### 36.18 레이아웃은 코드가 아니라 mid 설정으로 끈다

 

**증상**: 팝업으로 띄우는 화면에 사이트 헤더·푸터가 같이 나온다.

 

**해결**: 사이트 메뉴 편집 → 해당 mid → **디자인 → 레이아웃 [레이아웃 사용 안 함]**. 컨트롤러에서 레이아웃을 조작하려 애쓸 필요가 없다.

 

레이아웃은 **mid 단위 설정**이므로, 같은 모듈이라도 화면 성격이 다르면 mid 를 나눠야 한다. 예: 인증 팝업용 mid(레이아웃 없음) + 회원 관리 화면용 mid(레이아웃 있음).

 

### 36.19 표준 로그인 폼의 복귀 주소는 스킨에 의존한다

 

**증상**: `success_return_url` 을 붙여 로그인 화면으로 보냈는데, 로그인 후 사이트 첫 페이지로 떨어진다.

 

**원인**: 복귀 주소를 폼에 담는 것은 **스킨의 몫**이다. 스킨마다 구현이 다르다.

 

```html

<input type="hidden" name="success_return_url" value="{$referer_url}" />

```

 

`$referer_url` 은 URL 파라미터가 아니라 **리퍼러**를 쓴다. 팝업으로 열렸거나 직접 접근이면 값이 비어 복귀 주소가 사라진다.

 

**해결**: 복귀 주소가 흐름의 일부인 기능(외부 인증 등)은 표준 로그인 폼에 의존하지 말고 **자체 로그인 화면**을 만들고 복귀 주소를 세션에 보관한다. 인증 자체는 `MemberController::doLogin()` 을 그대로 쓰면 코어의 계정 잠금·승인 대기 정책과 `member.doLogin` 트리거가 유지된다.

 

---

 

## 큐 / CLI

 

### 36.20 큐에는 재시도도 dead letter 도 없다

 

**증상**: 예약 작업이 조용히 실패하고 아무도 모른다.

 

**원인**: 작업은 dequeue 시점에 **실행 전 삭제**되고, 예외는 `error_log` 에만 남고 폐기된다.

 

**해결**:

- 작업을 **멱등**으로 짜고, 실패해도 상태가 나빠지지 않는 방향으로 설계한다.

- 상태 점검 CLI 를 따로 만들어 cron + 알림에 물린다. 큐 자체를 신뢰하지 말 것.

- 큐는 기본 비활성이다. `config.php` 의 `queue.enabled`, `queue.driver` 와 cron 워커(`* * * * * php index.php common.cron`)를 확인한다.

 

### 36.21 CLI 스크립트에서 `$argv` 오프셋은 신뢰할 수 없다

 

**증상**: `php index.php mymodule.script check` 를 실행했는데 기본 동작(`status`)이 수행되고 종료 코드가 0 이다. **모니터링이 조용히 무력화된다.**

 

**원인**: 라이믹스 CLI 가 스크립트를 include 하기 전에 `$argv` 를 조작한다. `$argv[2]` 같은 고정 오프셋이 어긋난다.

 

**해결**: 위치가 아니라 **값**으로 찾는다.

 

```php

$commands = ['status', 'rotate', 'check'];

$command = 'status';

foreach (($_SERVER['argv'] ?? []) as $arg)

{

    if (in_array($arg, $commands, true)) { $command = $arg; break; }

}

```

 

종료 코드로 판정하는 스크립트는 **실패 케이스를 반드시 실물로 검증**한다. 정상 케이스만 확인하면 위 증상을 못 잡는다.

 

---

 

## 알아두면 좋은 것

 

### 36.22 코어에 이미 있는 것들

 

- `firebase/php-jwt 6.4.0` 번들 — RS256 JWT 직접 구현 불필요 (`34-external-libraries.md`)

- `Rhymix\Framework\Session::login(int $member_srl)` — **비밀번호 없는 프로그래매틱 로그인**

- `Rhymix\Framework\Security::encrypt()` / `decrypt()` — 키가 `files/config` 에 있어 DB 만 유출되어서는 복호화 불가

- `Context::setCorsPolicy()` — CORS 헤더

 

### 36.23 코어에 없는 것들

 

- **소셜 로그인 드라이버 프레임워크가 없다.** "별도 플러그인 또는 외부 모듈에서 제공" 이며, 계정 매핑은 각자 구현해야 한다 (`28-modules/member.md`).

 

### 36.24 스키마 / 쿼리

 

- `type="number"` 는 크기와 무관하게 **항상 bigint** 다. `size` 는 무시된다.

- 시간 컬럼은 용도에 따라 고른다. 코어 관례는 `date`(char 14, `YYYYMMDDHHIISS`)지만, 외부 규격과 주고받는 값(JWT `exp`/`iat` 등)은 unix timestamp 를 `number` 로 두는 편이 변환이 없어 안전하다.

- `<navigation>` 을 넣으면 `list_count` 기본값에 따라 결과가 잘릴 수 있다. 전체 조회가 목적이면 넣지 않는다.

- 동시 요청이 겹칠 수 있는 갱신은 36.25 를 따른다.

 

### 36.25 원자적 갱신은 한 곳만 고치면 안 된다

 

**증상**: 동시 요청 두 개가 모두 성공한다. 순차 테스트로는 절대 재현되지 않는다.

 

**원인**: `SELECT` 로 상태를 확인하고 `UPDATE` 하는 코드는 두 요청이 같은 상태를 읽는 순간 무너진다. 토큰 회전, 1회용 코드 소비, 재고 차감, 중복 신청 방지처럼 "먼저 온 하나만 통과" 여야 하는 곳이 전부 해당한다.

 

**해결**: 조건을 `UPDATE` 문 안으로 넣고 **영향 행 수**로 판정한다.

 

```xml

<query id="consumeIfActive" action="update">

    <tables><table name="foo_items" /></tables>

    <columns>

        <column name="consumed_at" var="consumed_at" notnull="notnull" />

    </columns>

    <conditions>

        <condition operation="equal" column="item_hash" var="item_hash" notnull="notnull" />

        <condition operation="null" column="consumed_at" pipe="and" />

    </conditions>

</query>

```

 

```php

$output = executeQuery('foo.consumeIfActive', $args);

$affected = $output->get('_affected_rows');

$won = $affected === null ? true : ((int) $affected > 0);

```

 

NULL 비교는 `operation="null"` / `operation="notnull"` 을 쓴다. 이 두 연산자는 `var`/`default` 가 없어도 SQL 에서 생략되지 않는다 (`14-database-and-queries.md` 의 조건 포함 규칙 6번). 값으로 넘기려면 `NullValue` 인스턴스를 쓰면 `equal` 이 자동으로 `null` 로 치환된다.

 

**진짜 함정은 여기다.** 같은 패턴이 필요한 자리를 **전부** 찾았는지 확인해야 한다. 실제 사례에서 authorization code 소비에는 조건부 UPDATE 를 적용해 놓고 refresh token 회전에는 빠뜨려, 재사용 탐지가 무력화된 채로 배포 직전까지 갔다. 두 코드가 서로 다른 파일에 있었고 리뷰에서도 걸러지지 않았다.

 

점검 방법:

 

- "먼저 온 하나만" 이 필요한 지점을 목록으로 적고 하나씩 대조한다.

- 코드 리뷰로는 잘 안 잡힌다. **동시 요청 테스트를 회귀 테스트로 남긴다.** 스레드를 배리어로 묶어 동시에 출발시키면 순차 실행에서 안 보이던 것이 드러난다.

- 성공한 요청이 정확히 하나인지, 나머지가 의도한 오류 코드를 받는지 둘 다 확인한다.

 

아그네스 Lv. 3

댓글 0