PHP 8.2의 보안 지원은 2026년 12월 31일에 끝납니다(php.net 지원 버전 표 기준). 저희도 직접 운영하는 서비스 몇 곳의 PHP를 지난 10월 27일에 8.2에서 8.4(8.4.27)로 옮겼습니다. 그 준비 과정에서 에러도 경고도 없이 결과만 조용히 달라지는 곳을 하나 찾았습니다. 쇼핑몰 상품 가격을 일괄로 바꾸는 기능에서, 같은 입력인데 일부 가격이 1원 낮게 나왔습니다. 운영에 옮기기 전 시험에서 찾았고, 같은 날 코드를 고쳐 반영했습니다. 이 글은 그 경험을 정리한 것입니다.
어떻게 찾았나
대상은 저희가 운영하는 카페24 상품 관리 앱(셀러메이트)입니다. 선택한 상품의 가격을 퍼센트로 한꺼번에 올리거나 내리는 기능이 있고, 새 가격은 현재 가격에 (1 + 퍼센트/100)을 곱한 뒤 원 단위로 반올림해 구합니다.
이 앱에는 단위 시험이 따로 없었습니다. 그래서 운영과 같은 구성을 PC의 도커로 흉내 낸 시험 환경을 만들었습니다. 같은 설정으로 PHP 8.2.35와 8.4.27 두 벌을 만들고, 쇼핑몰 API와 메일 서버는 가짜 서버로 대신해 외부 호출도 운영 키도 없이 돌렸습니다. 설치, 상품 목록, 옵션, 일괄 수정, 되돌리기, CSV 올리기, 재고 알림 메일, 예약 작업까지 54단계 시나리오를 두 버전에 똑같이 실행해 결과를 비교했습니다.
8.2에서는 54단계가 모두 통과했습니다. 8.4에서는 53단계가 통과하고 1단계가 실패했습니다. 실패한 단계가 가격 일괄 변경이었습니다.
1원이 어긋난 계산
예를 들면 1,050원 상품의 가격을 33% 내리는 경우입니다. 수학으로는 1,050 × 0.67 = 703.5원이라 반올림하면 704원입니다. 그런데 컴퓨터가 이 곱셈을 이진 부동소수로 계산하면 정확히 703.5가 아니라 703.49999999999989에 가까운 값이 나옵니다. PHP 8.2의 round()는 이 값을 704로 올렸고, 수정 전 코드가 PHP 8.4에서는 703을 냈습니다.
부동소수가 정확하지 않다는 점은 PHP 매뉴얼에도 경고로 적혀 있습니다. 십진수 0.1 같은 값도 이진수로는 정확히 나타낼 수 없어서 계산 결과의 마지막 자리를 믿으면 안 된다는 내용입니다. 이번 일은 그 오차를 8.2의 round()가 대신 가려 주고 있었다는 점이 핵심입니다.
PHP 8.4에서 바뀐 것
PHP 8.4 업그레이드 안내(UPGRADING)에는 round()의 "사전 반올림(pre-rounding)"에 관한 항목이 있습니다. 예전 방식은 0.285처럼 실제로는 0.28499999999999998로 저장되는 값을 십진수 0.285로 보고 0.29로 올려 주었습니다. 그런데 이 방식이 일부 수를 잘못 반올림하는 문제가 있어서, 8.4는 사전 반올림을 없애고 수를 비교하는 방식을 바꿔 십진수로서 올바르게 반올림되도록 했다고 적혀 있습니다. 정수로 반올림하는 내부 구현도 새로 작성됐다고 합니다.
의도된 버그 수정이므로 8.4가 틀린 것은 아닙니다. 다만 저희 시험에서는 이 변경 때문에, 8.2에서는 사전 반올림이 흡수해 주던 부동소수 오차가 일부 가격 계산에서 결과 차이로 나타났습니다.
얼마나 자주 어긋나나
어긋남은 정확한 값이 딱 .5로 끝나는 경우에서 생겼습니다. 가격을 100원에서 299,100원까지 10원 단위로, 퍼센트를 정수(-50%에서 +100%)로 훑은 4,515,051가지 조합 중 정확히 .5로 끝나는 경우가 403,650가지였고, PHP 8.4(수정 전 코드)는 그중 21,325가지(약 5.3%)에서 1원 낮게 나왔습니다. 8.2는 0건이었습니다. 소수 퍼센트까지 넓힌 약 3,146만 가지 조합에서는 243,263가지(약 0.77%)가 정확한 계산과 달랐고, 8.2는 이때도 0건이었습니다.
비율은 작아 보여도 일괄 변경은 한 번에 여러 상품에 적용되는 기능이라 눈에 띄지 않는 1원 차이가 섞여 나갈 수 있습니다. 8.4로 바꿔도 오류나 경고는 나오지 않고 값만 달라집니다. 8.2와 8.4에 같은 입력을 넣어 결과를 비교했기 때문에 찾을 수 있었습니다.
어떻게 고쳤나
계산 결과를 먼저 소수 넷째 자리로 정리한 뒤 정수로 반올림하도록 바꿨습니다.
// 수정 전
$newPrice = max(0, (int)round($newPrice));
// 수정 후
$newPrice = max(0, (int)round(round($newPrice, 4)));
703.49999999999989를 소수 넷째 자리로 정리하면 703.5가 되고, 이를 정수로 반올림하면 8.2와 8.4 모두 704가 됩니다. 앞의 격자에서 8.2와 8.4 모두 정확한 계산과 어긋나는 경우가 0건이었고, 두 버전의 결과 기록(CRC)도 같았습니다. 54단계 시나리오도 8.4에서 54단계 모두 통과했고, 응답 본문까지 포함한 기록이 8.2와 같았습니다.
이 수정은 8.2와 같은 결과를 유지하기 위한 보정입니다. 처음부터 설계한다면 금액 계산을 부동소수 대신 정수로 하거나 정밀 연산 함수로 하는 방법도 있습니다. PHP 매뉴얼도 더 높은 정밀도가 필요하면 임의 정밀도 수학 함수(BCMath)와 gmp 함수를 쓰라고 안내합니다.
같이 나온 경고 하나
같은 시험에서 fgetcsv()의 escape 인자를 생략하면 8.4에서 Deprecated 경고가 나오는 것도 확인했습니다. 8.4 업그레이드 안내에 fputcsv(), fgetcsv(), str_getcsv()의 escape 기본값 사용이 deprecated라고 적혀 있습니다. 저희는 8.2의 기본값인 백슬래시 한 글자를 명시해 동작은 그대로 두고 경고만 없앴습니다. CSV 경계값 10종의 파싱 결과가 수정 전후, 8.2와 8.4의 네 조합에서 모두 같았습니다. 운영 설정은 deprecation 표시를 꺼 두고 있어서 화면에는 보이지 않았을 경고지만, 시험에서는 모든 경고를 켜서 로그로 확인했습니다.
교체와 코드 반영은 따로입니다
운영 쪽에서 배운 점도 있습니다. 컨테이너 이미지를 8.2에서 8.4로 바꾸는 절차(되돌릴 이미지를 먼저 남겨 두는 것 포함)는 앱 코드를 바꾸지 않습니다. 시험에서 찾은 코드 수정은 따로 서버에 반영해야 했습니다. 또 opcache가 파일 변경을 다시 확인하지 않도록 설정되어 있어서 PHP 컨테이너를 재시작해야 새 코드가 쓰였고, 작업을 처리하는 워커도 시작할 때 코드를 읽기 때문에 재시작했습니다. 저희는 같은 날 두 작업을 이어서 진행했습니다. 두 작업을 한 계획에 묶어 두지 않으면 이미지만 바꾸고 끝나는 일이 생길 수 있습니다.
8.4로 올리기 전에 확인할 것
- 금액, 수량, 점수를 round()로 반올림하는 코드를 찾습니다. 퍼센트 곱셈처럼 부동소수 곱셈 결과를 반올림하는 곳이 먼저입니다.
- .5로 끝나는 경계값을 격자로 만들어 8.2와 8.4에 같은 입력을 넣고 결과를 비교합니다. 시험이 통과하는지만 보면 이런 차이는 놓칠 수 있습니다.
- 8.4 업그레이드 안내(UPGRADING)를 읽고, 우리 코드가 쓰는 함수가 있는지 찾습니다.
- 시험할 때는 오류 표시를 전부 켜서 deprecation 경고를 로그로 모읍니다.
- 이미지 교체와 코드 반영을 같은 변경 계획에 넣고, 교체 전에 되돌릴 이미지를 남깁니다.
PHP 8.2 지원 종료에 맞춘 전체 순서는 PHP 8.2 지원 종료(2026년 12월 31일) 전에 끝내는 업그레이드 로드맵에 정리해 두었습니다.
PHP 버전을 올린 뒤 금액이나 수량 계산이 달라지지 않는지 확인하고 싶은데 시험이 부족하다면, 포르본허가 같은 입력으로 여러 버전을 비교하는 점검부터 전환, 롤백 계획까지 도와드릴 수 있습니다. 아래 문의 양식에 상황을 간단히 남겨 주세요.
참고 자료
- PHP 8.4 UPGRADING (round()의 pre-rounding 제거, escape 기본값 사용 deprecated)
- PHP 매뉴얼: Floating point numbers (부동소수 정밀도 경고)
- PHP 매뉴얼: fgetcsv() (8.4.0 변경 이력)
- PHP: Supported Versions