[업무일지] 빈 문자열이 통과시킨 계좌 검증
예금주명 검증이 특정 사업자에서만 계속 틀려서 원인을 찾다가, 옆에 있던 걸 봤습니다. 접두어와 공백을 지운 결과가 빈 문자열이 되면
contains가 항상 참이라 통과하면 안 되는 건이 통과하고 있었습니다. 실패가 아니라 통과라서 로그에도 안 남았고요.
계좌를 등록할 때 사용자가 입력한 이름과 은행에서 조회한 예금주명이 같은지 확인합니다. 같은 회사인데도 표기가 제각각이라 그대로 비교할 수가 없거든요. 주식회사 가상상사, ㈜가상상사, 가상상사 가 다 들어옵니다.
그래서 접두어와 공백을 지우고 비교합니다. Java 21 환경입니다.
1
2
3
String cleanedUser = removePrefix(removeSpace(userName));
String cleanedOwner = removePrefix(removeSpace(bankOwnerName));
return cleanedUser.contains(cleanedOwner) || cleanedOwner.contains(cleanedUser);
전각 공백을 찾다가 로직을 다시 봤다
원래 잡으려던 건 다른 문제였습니다. 특정 사업자만 계속 불일치가 났는데 로그를 찍어보면 두 값이 똑같이 나왔습니다. 주식회사 가상상사 하고 주식회사 가상상사. 내가 생각한 거랑 좀 다른데.
코드포인트로 찍어보니 공백이 달랐습니다. 한쪽은 0x20, 다른 쪽은 U+3000 전각 공백이었습니다. 그리고 자바 정규식의 \s 는 [ \t\n\x0B\f\r] 만 잡습니다. U+3000 은 여기에 없습니다. 저는 \s 가 그냥 모든 공백인 줄 알고 있었고요.
1
2
// 전각 공백(U+3000) 포함
private static final Pattern REMOVE_EMPTY_SPACE_PATTERN = Pattern.compile("[\\s\\u3000]+");
이건 금방 끝났습니다. 문제는 테스트 케이스를 채우면서 위 비교 로직을 다시 읽었을 때였습니다.
빈 문자열은 모든 문자열에 포함된다
접두어와 공백을 다 지우고 나서 아무것도 안 남으면 어떻게 되나.
1
"가상상사".contains("") // true
자바에서 빈 문자열은 모든 문자열에 포함됩니다. String.contains 는 내부적으로 indexOf 를 쓰고 빈 문자열의 첫 등장 위치는 0 이니까 당연한 결과입니다. 특별한 예외 처리가 있는 게 아니라 정의상 그런 겁니다.
그러니까 은행에서 내려온 예금주명이 주식회사 처럼 접두어만 들어오면 정규화 후에 빈 문자열이 되고, 그 다음 줄에서 검증이 무조건 통과합니다. 계좌 검증인데요. 통과하면 안 되는 게 통과합니다.
실패가 아니라 통과라서 안 보였다
이게 이 버그의 성격입니다. 검증 로직이 터지면 에러 로그가 남고 알림이 옵니다. 그런데 이건 통과합니다. 예외도 안 나고 응답도 정상이고 로그에 아무것도 안 남습니다. 지표로도 안 잡히죠. 검증 실패율이 올라가는 게 아니라 오히려 내려가니까요.
원래 잡으려던 전각 공백 버그는 반대였습니다. 검증이 실패하니까 사용자가 막히고, 막히니까 문의가 들어오고, 그래서 제가 보게 된 겁니다. 증상이 있는 쪽이 먼저 발견되고 조용한 쪽은 그대로 남아 있었던 거죠.
가드를 넣고 테스트로 고정했습니다.
1
2
3
if (cleanedUser.isEmpty() || cleanedOwner.isEmpty()) {
return false;
}
false 로 막는 게 맞는지 잠깐 고민했습니다. 예금주명이 접두어만 온 건 은행 응답이 이상한 상황이라 예외를 던져서 드러내는 쪽도 됩니다. 그런데 이 경로는 검증 실패를 사용자에게 안내하는 흐름이 이미 있어서 새 예외를 끼우면 그 처리를 다 손봐야 했습니다. 그래서 실패로 떨어뜨리는 쪽을 골랐습니다.
남은 생각
전각 공백은 검색하면 나오는 얘기입니다. 제가 실제로 얻은 건 이쪽입니다.
입력을 깎아내는 로직은 다 깎여서 아무것도 안 남는 경우를 항상 봐야 합니다. trim, replaceAll, 접두어 제거처럼 문자열을 줄이는 처리를 붙일 때마다 그렇고요. 결과가 빈 문자열일 수 있고, 그 빈 값이 contains·startsWith·endsWith 에서 전부 참으로 통과합니다. 셋 다 같은 방식으로 뚫립니다.
그리고 버그를 고칠 때 주변을 한 번 더 보는 게 값이 있다는 걸 알았습니다. 증상이 있는 버그는 어차피 누가 발견합니다. 사용자가 막히면 문의가 오니까요. 조용히 통과하는 쪽은 아무도 안 옵니다. 그래서 이미 코드를 열어놓은 그때가 그걸 볼 수 있는 거의 유일한 시점입니다.
한계도 남았습니다. 같은 정규화 유틸을 쓰는 다른 검증 경로가 몇 군데 더 있는데 이번에 고친 건 계좌 검증 하나뿐입니다. 가드를 유틸 안쪽으로 옮겨서 한 번에 막는 방법도 있었지만, 그러면 정규화 함수가 빈 문자열을 어떻게 다뤄야 하는지를 호출부마다 다르게 원할 수 있어서 일단 판단을 미뤘습니다. 아직 그대로입니다.