Skip to Content
Backenddouble quote 인코딩
💻 Backend2023년 11월 14일

double quote 인코딩

#e-commerce#payment-expansion#backend#bugfix
Payment Expansion · Series 3 · Rebuild & Quality11 / 15Backend
상품명의 큰따옴표 때문에 발생한 결제 API 파싱 오류와 대응을 정리한다.

결제 연동 서비스 API에서 상품명에 큰따옴표(double quote)가 포함된 경우 파싱 에러가 발생하는 문제를 발견하고 수정한 경험을 기록한다. 처음에는 상품명에 저런 문자가 실제로 들어올 줄 몰랐지만, 프로덕션에서는 늘 예상 밖의 데이터가 들어온다. 이후 같은 유형의 문제가 다른 필드에서도 반복되었고, 특히 웹뷰를 거치는 구간이 얼마나 쉽게 깨질 수 있는지를 보여줬다.

왜 문자열 정책이 먼저였나

이 경험은 예상하지 못한 입력값이 꼭 들어온다는 전제로 API를 다뤄야 한다는 교훈을 남겼다. 특수 문자 처리를 뒤로 미루면, 꼭 프로덕션에서 실제 사용자 데이터로 되돌아온다.

큰따옴표가 왜 결제를 깨뜨렸나

결제 연동 서비스는 한국 결제 처리를 담당하는 API다. 주문 정보를 받아 결제 게이트웨이에 전달하는데, 이 과정에서 상품명(product name)을 포함한 JSON 페이로드를 구성한다. 문제는 상품명에 큰따옴표가 포함된 경우(예: ‘12” 맥북 프로’)였다.

JSON에서 큰따옴표는 문자열 구분자이므로, 문자열 내의 큰따옴표는 이스케이프(“로 변환)해야 한다. 이스케이프 처리 없이 큰따옴표가 포함된 상품명이 JSON에 들어가면, JSON 파서가 문자열의 끝으로 인식하여 파싱이 실패한다.

흥미로운 점은 모든 구간이 똑같이 위험한 것은 아니었다는 것이다. 앱과 앱이 직접 값을 주고받는 구간에서는 보통 직렬화/역직렬화 계층이 이런 문제를 어느 정도 흡수해준다. 하지만 웹뷰를 끼는 순간 이야기가 달라진다. 웹뷰 구간에서는 문자열이 중간 브릿지를 거치며 수동 조합되거나 다시 파싱되는 순간이 있었고, 그 지점이 이번 버그의 실제 약한 고리였다.

핵심 체크포인트

  • 첫 번째 발견은 2023년 9월이었다. 프로덕션에서 특정 주문의 결제가 실패하는 것이 모니터링에서 감지되었다. 로그를 확인하니 JSON 파싱 에러가 발생하고 있었다. 원인을 추적해보니 상품명에 큰따옴표가 포함된 상품이 있었고, 이 상품의 주문만 결제 실패가 발생했다.
  • 수정은 상품명을 JSON에 삽입하기 전에 큰따옴표를 이스케이프 처리하는 것이었다. 일단은 단순한 수정으로 빠르게 막았지만, 이런 종류의 문제는 언제든 다른 필드나 다른 브릿지 구간에서 다시 나타날 수 있다는 점을 함께 봐야 했다.
  • 이후 같은 유형의 문제가 다른 필드(item name)에서 다시 발생했다. 상품명은 수정했지만, 아이템명 필드에서는 동일한 이스케이프 처리가 빠져 있었다. 코드의 다른 위치에서 JSON을 구성하고 있었기 때문에, 첫 번째 수정이 자동으로 적용되지 않았다.

재발을 막기 위해 무엇을 바꿨나

첫 번째 발견은 2023년 9월이었다. 프로덕션에서 특정 주문의 결제가 실패하는 것이 모니터링에서 감지되었다. 로그를 확인하니 JSON 파싱 에러가 발생하고 있었다. 원인을 추적해보니 상품명에 큰따옴표가 포함된 상품이 있었고, 이 상품의 주문만 결제 실패가 발생했다.

수정은 상품명을 JSON에 삽입하기 전에 큰따옴표를 이스케이프 처리하는 것이었다. 일단은 단순한 수정으로 빠르게 막았지만, 이 방식만으로는 웹뷰처럼 문자열이 다시 조합되는 다른 구간까지 모두 안전해지는 것은 아니었다. 리뷰 과정에서도 같은 유형의 문제가 다른 필드나 다른 전달 경로에서 반복될 수 있다는 점을 함께 점검했다.

이후 같은 유형의 문제가 다른 필드(item name)에서 다시 발생했다. 상품명은 수정했지만, 아이템명 필드에서는 동일한 이스케이프 처리가 빠져 있었다. 코드의 다른 위치에서 JSON을 구성하고 있었기 때문에, 첫 번째 수정이 자동으로 적용되지 않았다.

이 반복 발생은 두 가지 문제를 드러냈다.

  • JSON 구성 로직이 여러 곳에 분산되어 있어 한 곳의 수정이 다른 곳에 자동으로 반영되지 않았다.
  • 앱 직렬화 구간에서는 잘 흡수되던 문자열 이슈도, 웹뷰 브릿지처럼 수동 조합이 섞인 구간에서는 다시 튀어나올 수 있었다.

실패하는 주문 데이터를 먼저 특정했다

어떤 문자열이 실제로 파싱 실패를 만드는지 재현 조건을 먼저 잡았다.

문자열 조합 지점을 줄였다

수동 조합 대신 직렬화 계층으로 책임을 모으는 방향으로 정리했다.

재발 방지 테스트를 붙였다

다른 필드에서도 같은 문자가 들어올 수 있다는 전제로 테스트를 추가했다.

직렬화 예시 코드

특수문자 이슈는 문자열 조합 대신 직렬화 레이어로 일원화하면 재발 위험을 줄일 수 있다.

java
sample/safe-json-payload-writer.java
public String buildPayload(Item item) throws JsonProcessingException { ObjectNode root = mapper.createObjectNode(); root.put("itemName", item.name()); root.put("itemCode", item.code()); return mapper.writeValueAsString(root); }
java
sample/safe-json-payload-writer-test.java
@Test void shouldEscapeDoubleQuoteInItemName() { String json = writer.buildPayload(new Item("A "Limited" Shoe", "SKU-1")); assertThat(json).contains("\"Limited\""); }

문자열 정책을 뒤로 미루면 생기는 일

이 문제는 버그 하나를 고쳤다는 데서 끝나지 않는다. 결제처럼 외부 시스템과 값을 주고받는 영역에서는 어디서 직렬화가 안전하게 처리되고, 어디서 문자열이 다시 조합되는지를 구간별로 봐야 한다. 특히 웹뷰 같은 경계에서는 예상하지 못한 입력이 언제든 문제를 만들 수 있고, 그 기준이 없으면 같은 유형의 이슈가 필드만 바꿔 다시 나타난다.

다음 글에서는 서비스별로 흩어져 있던 중복 결제 연동 코드를 공통 클라이언트 라이브러리로 묶은 과정을 이어서 본다.

Last updated on