Skip to content
isdnetworks
Go back

오래된 코드에 새 패턴 넣기

외부 호출이 코드 곳곳에 흩어져 있었다. 어떤 곳은 직접 소켓을 열고 어떤 곳은 curl_exec 을 쓰고 어떤 곳은 자체 래퍼를 쓴다.

Table of contents

Open Table of contents

상황 — 수백 개의 호출 지점

CURLOPT_TIMEOUT 도 재시도도 기록도 자리마다 달랐다. 이것을 공통 모듈로 묶으면 한 곳에서 관리할 수 있는데 grep 으로 세어 보니 부르는 자리가 수백 개였다.

한 자리에서 정하지 못하니 같은 외부를 부르는데도 어떤 곳은 5초에 끊고 어떤 곳은 무한정 기다렸다. 그런 차이가 있다는 것 자체를 curl_setopt 를 부르는 쪽에서 모르고 있었다.

기록도 마찬가지여서 error_log 를 뒤져도 어느 외부를 하루에 몇 번 부르는지 답할 수 없었다. 숫자가 없으니 무엇부터 손대야 하는지도 정할 수 없는 상태였다.

선택지 — 전면 교체와 방치

공통 모듈을 만들고 수백 곳을 한꺼번에 바꾸는 것이 가장 깔끔하고 동시에 가장 위험하다. PHPUnit 이 덮는 범위가 좁은 코드에서 그 규모로 바꾸면 무엇이 깨졌는지 알 수 없다.

반대로 새로 만드는 것만 공통 모듈을 쓰고 기존은 그대로 두면 안전하다. 대신 HttpClient 와 옛 방식이 영원히 함께 남아서 새로 온 사람이 어느 쪽을 따를지 모르게 된다.

둘 다 안 좋아서 그 사이에 경계를 하나 두기로 했다. 새 모듈을 만들되 기존 코드를 감싸는 층을 하나 두는 방식이었다.

조치 — 껍데기는 두고 안쪽만 교체

새 코드가 쓸 것과 기존 코드가 부를 것을 이렇게 갈랐다.

// 새 코드는 이걸 쓴다
$client = new HttpClient(['timeout' => 5, 'retry' => 3]);
$res = $client->get($url);

// 기존 코드는 그대로 두되, 내부에서 새 모듈을 쓰게 바꾼다
function legacy_http_get($url) {
    static $client = null;
    $client ??= new HttpClient(['timeout' => 30, 'retry' => 0]);  // 기존 동작 유지
    return $client->get($url)->getBody();
}

legacy_http_get 의 시그니처와 동작은 그대로이고 안쪽만 HttpClient 로 바뀐다.

기존 호출부에는 timeout 30에 재시도 없음을 그대로 줬다. 여기서 값을 함께 손대면 겉모양은 같은데 동작이 달라져서 무엇 때문에 났는지 가릴 수 없게 된다.

부르는 쪽을 한 줄도 안 고쳤는데 기록과 지표가 한 곳으로 모인다. 옮기는 일의 첫 목표가 통일이 아니라 그 수집이었다.

기록이 모이니 그다음이 보였다

며칠 지나니 http_call_log 에 자료가 쌓였고 그 자료로 순서를 정했다.

호출 지점호출 수/일평균 응답실패율
A240,000120ms0.1%
B8,0002,400ms12%
C15300ms0%

B 부터 손댔다. 호출 수는 적은데 느리고 자주 실패한다.

C 는 하루 열다섯 번이고 아무 문제가 없어서 건드리지 않았다. 옮겨서 얻을 것이 없는 자리에 시간을 쓰면 정작 B 가 뒤로 밀린다.

자료가 없었으면 A 부터 갔을 것이다. 호출이 가장 많으니 중요해 보이는데 실제로는 A 가 셋 중 가장 건강했다.

규칙 — 고칠 일이 있으면 그때 옮긴다

두 방식이 함께 있는 동안의 혼란을 줄이려고 세 줄을 적어 뒀다.

[외부 HTTP 호출]
새 코드      HttpClient 를 쓴다
기존 코드    legacy_http_get 을 그대로 둔다. 고칠 일이 있으면 그때 HttpClient 로 옮긴다
직접 호출    curl_* 를 직접 부르지 않는다. 발견하면 legacy_http_get 으로 바꾼다

세 줄 중 가운데가 핵심이었다. 일부러 일정을 잡아 전부 옮기지 않고 어차피 손댈 때 HttpClient 로 같이 옮긴다.

이러면 자주 고치는 코드부터 자연히 옮겨진다. 몇 년째 아무도 안 건드리는 코드는 안 옮겨지는데 안 건드리니까 문제도 생기지 않는다.

옮기는 비용을 따로 내지 않고 원래 내던 수정 비용에 얹는 셈이다. 그래서 이 일에는 마감이 없고 legacy_http_get 을 부르는 자리가 주는 것으로 진행을 봤다.

검사로 뒷받침했다

규칙을 문서에만 적어 두면 모르는 사람이 옛 방식을 계속 쓴다.

# 새로 추가된 코드에 curl_ 직접 호출이 있으면 경고
git diff --cached -U0 | grep '^+' | grep -q 'curl_init' && echo "경고: HttpClient 를 쓰세요"

curl_init 이 새로 들어오면 경고가 뜬다. 우회할 수 있으니 완벽하지는 않다.

대부분은 몰라서 쓰고 알면서 우회하는 사람은 이유가 있다. 앞쪽은 이 검사가 잡고 뒤쪽은 이야기해 보면 되는 일이었다.

공통 모듈에 넣고 싶은 것이 여럿 있었지만 회로 차단기도 분산 추적도 이번에는 넣지 않았다. 그것들을 넣으면 legacy_http_get 의 동작이 달라져 기존 동작 유지라는 전제가 깨진다.

옮기기와 기능 추가를 같이 하면 문제가 났을 때 어느 쪽 탓인지 가르지 못한다. 다 옮긴 뒤에 필요하면 그때 넣기로 했다.

정리


Share this post on:

Previous Post
조용히 실패하는 연동
Next Post
요청 밖으로 뺀 무거운 계산