공용 함수에 인자를 하나 추가했다. 호출부가 세 곳이라 세 곳 다 똑같이 바꿨다. 두 곳은 맞았고 한 곳은 틀렸다. 운영에 하루 반 나가 있었다. 틀린 곳은 그 인자가 가리키는 개념이 아예 없는 화면이었다. 조회 쿼리가 그 필드를 내려주지 않는다. 인자를 추가하는 것은 잠들어 있던 분기를 깨우는 일이다. 지금까지 안 돌던 코드가 갑자기 돈다. 그게 최근에 검증된 적 있는지는 아무도 모른다.
무슨 작업이었나
포인트 잔액 검사를 넣으면서 화면 쪽도 손봤다. 예약 목록을 그리는 공용 함수에 플래그를 하나 넘기는 작업이었다.
drawUserReserve(list, getPayFlag('isPayAmount'));
호출부가 세 곳이었다. 세 곳 다 똑같이 바꿨다.
세 번째 호출부
두 곳은 장바구니 화면이었다. 포인트 개념이 있는 경로다.
세 번째는 "남은 이용권 추가" 팝업이었다. 이미 산 이용권을 예약에 붙이는 화면이라 포인트라는 개념 자체가 없다.
그리고 그 화면의 조회 쿼리는 결제금액 필드를 아예 내려주지 않는다. 2023년 최초 생성 때부터 그랬다. 필요가 없으니까.
왜 이전에는 안 터졌나
인자를 안 넘기고 있었다. 그래서 함수 안에서 undefined였다.
function drawUserReserve(list, isPayAmount) {
if (isPayAmount) { // undefined → falsy → 안전한 분기
...
}
}
falsy라서 안전한 쪽으로 갔다. 잠들어 있던 분기였다.
내가 넘긴 헬퍼가 그걸 깨웠다.
function getPayFlag(key) {
var v = payFlags[key];
return v === undefined ? true : v; // ← 값이 없으면 true
}
값이 없으면 true를 반환하도록 만든 것이 문제였다. "설정이 없으면 켜진 걸로 본다"는 의도였는데, 그 결과 세 번째 경로가 없던 분기로 들어갔다.
그 안에서 이렇게 됐다.
if (row.payAmount != 0) { // undefined != 0 → true
html += commonMoneyFormat(row.payAmount); // TypeError
}
undefined != 0이 true다. "저장된 사용액이 있다"로 판정하고 undefined를 포맷하려다 죽었다.
세 가지가 겹쳤다
1. 기본값을 true로 둔 헬퍼. 없을 때 기존 동작(대개 falsy)을 유지하는 쪽이 안전하다. true는 없던 코드를 깨운다.
2. 느슨한 비교. x != 0은 undefined를 통과시킨다. 값 유무를 볼 거면 이렇게 쓴다.
if (Number(row.payAmount || 0) > 0) { ... }
3. 데이터 원천을 확인하지 않았다. 세 호출부가 각각 다른 쿼리에서 온 데이터를 그린다는 걸 생각 못 했다. 같은 함수를 부르니 같은 모양의 데이터일 거라 여겼다.
검증도 반쪽이었다
바꾼 뒤 장바구니를 열어 확인했다. 잘 됐다.
남은 이용권 팝업은 안 열어 봤다. 그게 세 번째 호출부였다.
호출부를 셋 바꿨으면 셋을 다 태워야 한다. 하나만 확인하고 넘어간 게 하루 반의 노출로 이어졌다.
그래서 규칙으로 정리했다
공용 함수의 호출부는 일괄로 바꾸지 않는다. 경로마다 하나씩 확인한 뒤 바꾼다.
확인할 것은 이렇다.
- 그 경로의 데이터 원천(쿼리)이 이 필드를 실제로 내려주는가
- 그 경로에 이 개념이 존재하는가 (포인트 없는 화면에 포인트 플래그를 넘기고 있지 않은가)
- 인자를 안 넘기던 곳이면, 지금까지 어떤 분기로 가고 있었는가
마지막이 핵심이다. 인자를 추가하는 것은 잠들어 있던 코드를 깨우는 일이다. 지금까지 안 돌던 분기가 갑자기 돈다. 그 분기가 최근에 검증된 적 있는지는 아무도 모른다.
그리고 검증은 바꾼 호출부 전부를 태운다. 하나라도 안 열어 보면 안 한 것과 같다.
같은 계열의 함정
이 프로젝트에서 비슷한 걸 하나 더 겪었다.
MyBatis 결과를 담는 Map 구현체가 put할 때 키를 변환한다. PAY_STATUS → payStatus로 잘 바꿔 주는데, 이미 camelCase인 키를 넣으면 통째로 소문자가 된다.
map.put("payStatus", x); // → "paystatus" 로 저장됨
map.put("PAY_STATUS", x); // → "payStatus" ← 이게 맞다
JSON으로 나가면 프론트가 못 읽는 키가 된다. 서버는 정상이고 화면만 안 그려진다.
공통점은 "같아 보이는데 다르게 동작하는 것"이다. 그리고 둘 다 에러가 아니라 조용한 오작동으로 나타났다.
정리
- 공용 함수 호출부를 일괄로 바꾸지 않는다. 경로마다 인자가 유효한지 본다.
- 인자 추가는 잠들어 있던 분기를 깨우는 일이다. 그 분기가 검증됐는지는 아무도 모른다.
- 기본값을
true로 두는 헬퍼는 위험하다. 없을 때 기존 동작을 유지하는 쪽이 안전하다. - JS의
x != 0은undefined를 통과시킨다.Number(x || 0) > 0을 쓴다. - 같은 함수를 부른다고 같은 데이터가 오는 게 아니다. 원천 쿼리를 본다.
- 바꾼 호출부는 전부 태워 본다. 하나 빼면 안 한 것과 같다.
'웹개발' 카테고리의 다른 글
| HTTPS 리다이렉트 무한 루프 — 앱이 원래 프로토콜을 모르기 때문이다 (0) | 2026.09.28 |
|---|---|
| 프론트 검증은 UX지 보안이 아니다 — 서버 검증 누락이 남긴 것 (0) | 2026.09.28 |
| 차단했는데 12시간을 더 쓴다 — 세션 인증에는 무효화가 없다 (0) | 2026.09.21 |
| 메서드 이름이 트랜잭션을 결정한다 — Spring AOP 일괄 트랜잭션의 함정 (0) | 2026.09.17 |
| npm ci와 npm install 차이 — 속도가 아니라 락 파일을 대하는 태도다 (0) | 2026.09.14 |
댓글