결제 관련 코드를 전부 찾아야 했다. 세 번 찾았는데 매번 다른 결과가 나왔다.
Table of contents
Open Table of contents
증상 — 세 번 찾아 세 번 다른 결과
세 번의 결과가 이랬다.
$ grep -rn "payment" --include=*.php src/ | wc -l
412
$ grep -rni "payment\|pay_\|결제" --include=*.php src/ | wc -l
1,204
$ grep -rni "payment\|pay\|결제\|billing" --include=*.php --include=*.js src/ | wc -l
1,882
찾는 말을 바꿀 때마다 결과가 달라졌고 어느 것이 전부인지 알 수 없었다.
세 결과를 겹쳐 보면 한 번만 나온 파일이 꽤 됐다. 마지막 것이 가장 많이 나왔다는 사실이 그것이 전부라는 근거는 되지 않는다.
찾는 방법을 정하지 않고 찾기 시작한 것이 문제였다. 방법이 매번 다르면 결과도 매번 달라진다.
payment 만 넣으면 pay_state 를 쓰는 파일이 빠지고 pay 까지 넣으면 paycheck 같은 무관한 말이 섞인다. 검색어를 넓히는 것과 정확해지는 것이 같은 방향이 아니었다.
무엇을 찾는지부터 정했다
결제 관련 코드라는 말이 무엇을 가리키는지부터 적었다.
찾는 것
1. 결제 API 를 부르는 자리
2. 결제 결과를 받는 자리
3. 결제 상태를 읽거나 쓰는 자리
4. 결제 금액을 계산하는 자리
넷으로 나누고 나니 각각 어디를 봐야 하는지가 달라졌다.
하나의 검색어로 넷을 한꺼번에 잡으려 했던 것이 앞의 실패였다. 부르는 자리와 계산하는 자리는 애초에 같은 말로 적혀 있지 않다.
PaymentClient 를 부르는 자리에는 그 클래스 이름이 적혀 있지만 금액을 더하는 자리에는 payment 라는 말이 없다. 넷 중 어느 것을 찾느냐에 따라 실마리가 되는 말이 아예 달랐다.
조치 — 넷을 각각 다른 방법으로
1번은 부르는 클래스 이름으로 찾았다.
$ grep -rn "PaymentClient\|PgGateway" --include=*.php src/
2번은 콜백이 들어오는 자리라서 라우팅에서 찾았다.
$ grep -rn "pay.*callback\|pay.*return\|pay.*notify" application/config/routes.php
3번은 컬럼 이름이 정해져 있어 그것으로 찾았다.
$ grep -rn "pay_state\|payment_no\|paid_amount\|pay_date" --include=*.php src/
세 가지는 각각 찾을 실마리가 분명했다. PgGateway 와 routes.php 와 pay_state 처럼 한 가지로만 적히는 이름이 있었기 때문이다.
4번이 어려웠다. 금액을 담는 변수 이름이 제각각이었다.
$ grep -rn "amount\|price\|금액" --include=*.php src/ | wc -l
3,102
3,102줄은 읽어서 가려낼 수 있는 양이 아니었다. 이름으로 찾는 길이 막혔으니 다른 실마리가 필요했다.
데이터에서 거꾸로 찾았다
코드 쪽 이름은 제각각이어도 표 이름은 하나다.
SELECT DISTINCT table_name FROM information_schema.columns
WHERE table_schema = 'order'
AND (column_name LIKE '%pay%' OR column_name LIKE '%amount%');
표 열한 개가 나왔고 그다음은 그 이름으로 코드를 뒤졌다.
$ for t in $(cat tables.txt); do
echo "=== $t"; grep -rln "$t" --include=*.php src/
done
변수 이름은 사람마다 다르게 짓지만 표 이름은 한 가지로 적힌다.
이쪽으로 찾으니 4번이 열한 개 표를 건드리는 파일 목록으로 좁혀졌다. 이름에서 자료로 실마리를 옮긴 것이 이번에 효과가 컸다.
information_schema 쪽은 컬럼 이름이 paid_amount 든 total_price 든 한 번에 잡힌다. 코드에서 amount 로 3,102줄을 받던 것이 표 열한 개와 그것을 읽는 파일 목록으로 정리됐다.
검증 — 돌 때 잡는 장치
찾는 것으로 끝내지 않고 놓친 것이 드러나게 했다.
// PaymentClient 를 안 거치고 직접 부르는 것을 잡는다
class HttpClient {
public function request(string $url, ...) {
if (str_contains($url, PG_HOST) && !$this->calledFromPaymentClient()) {
log_message('error', "결제 API 직접 호출: " . $this->callerInfo());
}
...
}
}
PG_HOST 로 나가는데 PaymentClient 를 안 거친 호출이면 기록에 남는다.
며칠 돌리니 두 곳이 걸렸는데 앞서 찾기에서 안 나온 자리였다.
$host = $config['pg_' . $env . '_host'];
$url = 'https://' . $host . '/pay';
주소를 문자열로 조립해서 부르고 있어 어떤 검색어로도 안 잡힌다.
정적으로 찾는 것에는 이런 한계가 있었다. 실제로 돌 때 잡는 것을 같이 둬야 그 구멍이 메워진다.
callerInfo 가 남긴 파일과 줄 번호가 있어서 걸린 자리를 바로 열어 볼 수 있었다. 기록에 호출자를 안 남겼으면 무언가 직접 부른다는 것만 알고 어디인지는 다시 찾아야 했다.
찾은 것과 찾은 방법을 남겼다
목록에 결과만 적지 않고 방법도 같이 적었다.
결제 관련 코드 목록 (2017-10-15)
API 호출 src/Payment/PaymentClient.php
src/Legacy/pay_direct.php ← 정리 대상
콜백 수신 application/controllers/PayCallback.php
legacy/pay/return.php
상태 읽기/쓰기 (14곳, 별첨)
금액 계산 src/Order/AmountCalculator.php
legacy/order/calc.php
찾은 방법
1. 클래스명 검색
2. 라우팅 검색
3. 컬럼명 검색
4. 결제 테이블을 건드리는 파일 역추적
5. 실행 중 직접 호출 감지 (3일간)
다음에 같은 방법으로 찾으면 같은 결과가 나온다는 것이 이 다섯 줄의 값어치였다.
pay_direct.php 처럼 정리 대상이라고 표시한 것도 그 자리에 함께 적었다. 목록을 다시 만드는 사람이 이미 판단이 끝난 항목을 두고 같은 고민을 하지 않게 된다.
목록은 새로 생기는 것이 있으면 그날부터 낡는다.
// PaymentClient 를 거치지 않으면 예외
if (!$this->isAllowedCaller()) {
throw new LogicException('결제 API 는 PaymentClient 를 통해 호출합니다.');
}
기록만 남기던 것을 예외로 바꿔서 새로 만들 때 그 자리에서 걸리게 했다.
예외로 바꾸기 전에 며칠 동안 기록만 남기게 두고 정상적인 호출이 걸리지 않는지 먼저 봤다. 바로 예외로 걸었으면 멀쩡한 결제가 막혔을 수도 있다.
정리
- 전부 찾아야 하는 일은 찾는 방법부터 정한다
- 안 정하면 검색어를 바꿀 때마다 결과가 달라진다
- 가장 많이 나온 결과가 전부라는 근거는 되지 않는다
- 무엇을 찾는지 나누면 각각 다른 방법이 필요한 것이 보인다
- 클래스명과 라우팅과 컬럼명은 실마리가 분명하다
- 이름이 제각각이면 자료에서 거꾸로 찾는다
- 변수 이름은 제각각이어도 표 이름은 하나다
- 문자열로 조립해 부르는 것은 검색으로 못 찾는다
- 실제로 돌 때 잡는 것을 같이 둬야 그 구멍이 메워진다
- 목록과 함께 어떻게 찾았는지를 적는다
- 예외로 바꾸기 전에 기록으로 며칠 먼저 본다