개발 slecs

외부 채널 수수료 정산 버그 수정과 UI 개선

목차

외부 채널은 결제 플랫폼을 통해 서비스를 제공하는 사업자 단위다. 채널별로 수수료 설정이 따로 돌아가고, 메뉴 권한과 하위 채널 관리도 각자 독립적으로 동작한다. 이런 구조는 유연하지만, 설정 변경이 화면에 즉시 반영되는지, 권한 조건이 정확히 평가되는지 같은 세부 동작을 꼼꼼히 챙기지 않으면 운영 중에 조용히 어긋나기 시작한다.

이번 작업은 규모가 작은 편이었는데, 딱 그 "조용히 어긋나는" 류의 버그들이었다. 터지면 크게 드러나지 않지만 운영팀이 수수료 확인할 때마다 헷갈리는 것들. 감지 자체가 늦어지기 때문에 방치 기간이 길어지면 그만큼 신뢰 비용이 쌓인다.

버그 수정 - 세 가지 유형

포털 메뉴 노출 조건 오류, 파라미터 바인딩 오류, 화면 갱신 타이밍 문제. 세 개 다 성격이 달랐다.

메뉴 노출 조건은 채널 유형이나 권한 플래그를 평가하는 로직이 꼬여 있었다. 이 계열의 버그는 대부분 조건 체크 순서 문제거나, 값이 null/undefined일 때 처리가 빠져 있는 경우다. 특정 채널에서만 메뉴가 보이거나 안 보이는 증상으로 나타나서 재현 자체가 까다롭다. 운영 환경에서 "왜 나는 이 메뉴가 없냐"는 문의가 들어오기 전까지는 잘 모르고 지나치는 경우가 많다.

파라미터 바인딩 오류는 폼 데이터나 요청 파라미터가 서버 컨트롤러에 제대로 묶이지 않는 문제다. MyBatis라면 #{} 내 변수명 오타, 혹은 객체 타입 불일치로 조용히 null이 들어간다. UPDATE 쿼리가 실행은 됐는데 변경이 없는 상태가 되는 것. 에러 로그도 안 남고 응답도 200으로 오니까 발견이 더디다. 바인딩 쪽 버그는 쿼리 로그를 직접 찍어보는 게 제일 확실하다.

화면 갱신 타이밍 문제가 가장 흔하고, 이번에도 있었다. 수수료 수정 후 즉시 목록에 반영이 안 되는 것. 구조적으로는 간단하다 - AJAX 요청 성공 콜백 안에서 목록을 다시 조회하지 않으면 로컬 상태와 서버 상태가 분리된다.

// 수정 전 - 저장 후 별도 갱신 없음
$.post('/api/channel/fee/update', params, function(res) {
  alert('저장되었습니다.');
});

// 수정 후 - 성공 응답 후 목록 재조회
$.post('/api/channel/fee/update', params, function(res) {
  if (res.success) {
    loadFeeList(); // 목록 다시 불러옴
  }
});

별거 아닌 것 같지만 사용자는 저장했는데 화면이 안 바뀌면 "저장이 됐나?" 의심하고 다시 누른다. 그러면 중복 요청 문제로 번진다. 저장 버튼을 비활성화하거나 로딩 상태를 주는 처리도 같이 챙기는 게 맞다.

테이블 th/td 컬럼 수 불일치도 잡았다. 브라우저가 자동으로 렌더링을 맞춰주니까 육안으로는 멀쩡해 보이는데, DOM을 열어보면 열 개수가 맞지 않는다. 컬럼이 동적으로 추가·삭제되는 구조에서 헤더 쪽 colspan을 빠뜨리거나, 조건부 렌더링으로 td를 숨겼는데 th는 그대로 남아 있을 때 생긴다. "테이블이 이상하게 보인다"는 제보가 들어왔을 때 스타일 먼저 의심하지 말고 Elements 탭에서 DOM 구조부터 확인하는 게 빠르다. 브라우저의 자동 보정이 오히려 문제를 숨기는 경우가 이 케이스다.

정산 UI와 수수료 계산 로직

정산 화면에서 사용자 시선이 가장 오래 머무는 건 숫자다. 총 거래액, 수수료, 실수령액. 이 세 숫자가 명확하지 않으면 신뢰가 흔들린다. 정산은 틀리면 안 되는 도메인이기 때문에 UI도 "맞아 보이게" 만드는 게 중요하다.

금액 표기 원칙은 단순한데 일관성이 핵심이다.

항목 처리 방식
천 단위 구분 쉼표 구분자 (1,234,567)
통화 단위 '원' 고정 표기
마이너스 금액 빨간색 + 부호(-) 병기
소수점 원 단위에서는 표기 안 함

마이너스 처리를 색상만으로 구분하면 인쇄하거나 엑셀로 내보낼 때 사라진다. 부호를 색상과 함께 붙이는 이유가 여기 있다. 접근성 측면에서도 색상 단독 정보 전달은 피하는 게 맞다.

수수료 계산 로직은 올림·버림 하나가 전체 합산에 영향을 준다. 계층별로 요율이 다르게 적용될 때 각 단계 계산 결과를 어떻게 처리할지 명확히 정해야 한다. 일반적으로 수수료는 내는 사람 기준 내림 처리로 과다 납부를 막고, 받는 쪽도 실제 수령액이 계산 결과를 초과하지 않게 맞춘다. 이게 정산 분쟁을 줄이는 기본 방향이다.

구현 레벨에서 주의할 건 부동소수점이다. 수수료율이 0.035 같은 소수일 때 부동소수점 연산 오차가 생긴다. 금액 계산은 처음부터 BigDecimal로 고정해두는 게 낫다.

BigDecimal amount = new BigDecimal("150000");
BigDecimal rate = new BigDecimal("0.035");
BigDecimal fee = amount.multiply(rate)
    .setScale(0, RoundingMode.FLOOR); // 수수료 내림 처리

double로 중간 계산을 거치면 원 단위 오차가 생기고, 건수가 많아질수록 누적 차이가 커진다. 정산 도메인에서 이 오차가 쌓이면 나중에 손으로 맞추는 일이 생긴다.

정산 배치 멱등성

수수료 계산 로직을 건드리면 반드시 같이 확인해야 하는 게 정산 배치 멱등성이다. 이번 수정 직접 대상은 아니었지만, 이 영역이 보장되지 않으면 계산 로직이 맞아도 결과 데이터가 꼬인다.

멱등성이 깨지면 같은 기간 정산을 두 번 돌렸을 때 결과가 달라진다. 실수로 배치를 재실행했을 때, 또는 중간 실패 후 재시도했을 때 문제가 된다. 이미 처리된 건에 대해 완료 상태를 체크하고 스킵하거나, upsert로 처리해서 중복 삽입이 없게 하는 게 기본이다. 멱등하지 않은 배치는 운영 중에 DB를 손으로 고치는 상황을 만든다. 그게 쌓이면 데이터 신뢰도가 흔들리고, 그 다음부터는 숫자가 맞아도 의심받는다.

이번 작업이 작게 느껴졌던 건 건드린 파일 수가 적어서였는데, 정산 도메인 특성상 각각의 수정이 의도하지 않은 곳에 영향을 줄 수 있어서 범위를 짚어가며 진행했다. 수수료 계산, 화면 갱신, 테이블 렌더링이 서로 무관해 보여도 같은 화면 위에서 동작하는 이상 어느 하나가 어긋나면 전체 신뢰가 같이 떨어진다. 그래서 이런 잡무성 수정이 실제로는 덜 잡무스럽다.

댓글 0

첫 댓글 달아줘.