계좌번호로 은행을 맞춰보자

우빈·2025년 10월 13일
78
post-thumbnail

위 화면은 토스의 송금 화면 중 일부입니다.
계좌번호를 입력하면 해당 계좌의 은행 선택을 추천해주는 기능으로, 이체가 발생하는 기능에서
사용자가 간편하게 서비스를 이용할 수 있도록 돕는 유용한 기능입니다.

해당 기능을 서버나 머신러닝의 의존성 없이, 단순 자바스크립트 코드만으로 구현한 경험에 대해 공유합니다.

이 글에서는...

  • "계좌번호로 은행을 유추하는 게 그렇게 어려운 일일까?"라는 질문을 생각해봅니다
  • ML이나 서버 의존성 없이, 단순한 구현 코드만으로 해당 기능을 구현한 경험을 공유합니다

용어 정리

과목 코드: 각 은행에서 기관을 식별할 때 사용하는 코드로, 계좌번호에 2자리 또는 3자리로 포함됩니다.
e.g. 기업은행 XXX-YY-ZZZZZZ-C와 같은 형식인 경우, YY가 과목번호이며, 이는 01 또는 02이다.

계좌번호로 은행을 유추하는 게 어려운 일일까?

처음에는 각 금융기관마다 계좌번호 발급시 사용할 패턴이 있을 것이기에, 구현하는 데 난이도가 높진 않을 것이라고 생각했습니다.

그런데 다음과 같은 문제점이 있었습니다.

  1. 은행에 따라서 계좌번호 자릿수는 10~14자리가 될 수 있다.
  2. 은행별 계좌번호 패턴에 대한 공개 자료는 과목코드 단 하나를 제외하고 존재하지 않는다.
  3. 은행에 따라 과목코드는 2자리일 수도 있고 3자리일 수도 있다.
  4. 과목코드의 위치는 통일되어있지 않아, 과목코드가 은행에 따라 계좌번호의 앞/중간/뒤 등 다양한 곳에 위치한다.
  5. 은행끼리 과목코드가 겹치는 경우가 빈번하다.

제일 큰 블로커는, 공개 자료가 없기 때문에 14자리까지 올 수 있는 계좌번호에 들어가는 과목코드 달랑 2~3자리만으로 은행을 유추해야 한다는 점이었습니다.

또한 과목코드만으로 은행들을 식별하기에는 은행마다 위치가 다를 때도 있으며 같은 과목코드를 사용하는 곳 또한 존재합니다.

그런데, 이를 단점이 아닌 특징으로 바라보아 해결할 수는 없을까요?

과목코드로 은행 유추하기

먼저 금융결제원에서 제공하는 ‘참가기관별 CMS 계좌번호체계’ 자료를 조사했습니다.
해당 데이터는 각 은행이 어떤 과목코드를 사용하고 있는지를 유추시켜주는 자료입니다.

해당 자료에는 각 은행이 어떻게 과목 코드를 구성하는지, 각 과목 코드는 어떤 값인지에 대해 기록되어있습니다.

같은 은행에서도 계좌 유형이 어떻냐에 따라서 과목 코드가 변경되기 때문에, 다른 은행과 겹치거나 은행 안에서 과목 코드 위치가 다른 등의 이슈가 있습니다.

그런데 한 가지 좋은 소식은, 일부 은행의 계좌번호에는 과목코드를 제외하고도 특정한 규칙이 공개된 경우도 기재되어있다는 겁니다.
e.g. 토스뱅크는 일련번호의 첫자리가 토스머니는 8, 나머지는 0으로 고정된다.

확실히 이를 로직으로 구현하더라도 정확하게 계좌번호를 확인했을 때 '이 계좌번호는 이 은행거야!'라기엔 겹치는 은행이 많을 것 같네요.

그렇다면 해당 계좌번호체계를 정리해서 각 은행별 검증기를 만들고, 입력된 계좌번호를 각 검증기들이 확인하여 점수를 부여한다면, 높은 점수를 반환한 검증기의 은행이 맞을 확률이 높지 않을까요?

해당 논리를 기반으로 다이어그램을 간단히 그려보겠습니다.

  1. 모든 은행을 순회하며 입력받은 계좌번호에 대한 평가를 시작한다.
  2. 입력받은 계좌번호가 한 은행의 과목코드와 일치할 경우 해당 은행의 score를 과목코드의 길이만큼 증가시킨다.
  3. 입력받은 계좌번호가 한 은행의 계좌번호 규칙과 일치할 경우 해당 은행의 score를 0.5만큼 증가시킨다.
  4. 모든 은행의 score를 채점 후, score가 높은 순서대로 정렬한 리스트를 반환한다.

해당 알고리즘을 토대로, 서버나 ML의 의존성 없이 단순 코드만으로 기능을 구현해보겠습니다.

아키텍처 구상하기

각 은행사의 패턴을 생성해주는 클래스를 통해 플러그인을 생성하고, 이를 detectAccountNumber라는 감지기에 여럿 넣어주어 작동하는 식으로 설계했습니다.

해당 아키텍처를 기반으로 한 플로우는 다음과 같습니다.

  1. 모 은행의 과목코드 및 규칙을 GenerateDetector를 통해 생성한다.
  2. detectAccountNumber가 모든 detector를 순회하며 규칙을 평가하고 각 은행의 score를 매긴다.
  3. 모든 은행의 score를 채점 후, score가 높은 순서대로 정렬한 리스트를 반환한다.

이제 해당 알고리즘과 아키텍처를 기반으로 함수를 구현해보겠습니다.

알고리즘과 아키텍처를 기반으로 함수 구현하기

interface Detector {
  /**
   * 지원할 은행 종류입니다.
   * ex) kdb, kakao, toss
   */
  bank: string;
  basicRuleList: Array<BasicRule>;
}

interface BasicRule {
  /**
   * 계좌 번호 패턴입니다. 과목코드의 위치를 기준으로 판별합니다.
   * ex) YYYZZZZZZZZC인 경우, YYY를 기반으로 판단
   */
  patternList: Array<string>;
  /**
   * 과목 코드입니다.
   * from ~ to (YCodeRange) 입력 시 이에 해당하는 범위의 과목 코드를 허용합니다.
   * ex) { from: 100, to: 104 } => 100 ~ 104
   */
  yCodeList?: Array<string | YCodeRange>;
  /**
   * 각 은행 별로 계좌번호 형식에 따른 규칙을 설정합니다.
   * ex) 토스뱅크는 일련번호의 첫자리가 토스머니는 8, 나머지는 0으로 고정된다.
   * additionalRules: [(accountNumber) => accountNumber[3] === '8' || accountNumber[3] === '0'],
   */
  exceptionalRuleList?: Array<(accountNumber: string) => boolean)>;
}

해당 코드를 기반으로 각 은행의 계좌 패턴을 입력받아 플러그인을 만들어주는 generateDetector 함수를 작성합니다.

// generateDetector.ts
export function generateDetector({
  bank,
  basicRuleList,
  globalCustomRuleList,
}: Detector): (props: DetectorProps) => ScoreResult {

 // 설계한 알고리즘 기반으로 점수 계산하는 로직
 ...
 
 return { bank, bankCode, score };
}

generateDetector를 기반으로 아키텍처 설계에 따라 각 은행별로 플러그인을 생성합니다.

// 부산은행.ts
import { generateDetector } from '../src/generateDetector';

export const BNKDetectorPlugin = generateDetector({
  bank: '부산은행',
  basicRules: [
    {
      patterns: ['XXXYYYZZZZZC', 'ZYYYZZZZZZZZZC', 'YYYZZZZZZZZZC'],
      yCodes: ['107', '108', '109', '121', '123', '124', '122', '103', '101', '127', '716', '112'],
    },
  ],
});

이제 최종적으로, detectAccountNumber 함수에 해당 플러그인들을 주입하고, batch를 통해 플러그인 실행 후 높은 점수가 나온 은행을 반환하는 함수를 구현합니다.

const detectorList = Object.values(detectors);

export function detectAccountNumber({
  accountNumber,
  length,
  additionalRuleScore,
}: DetectAccountNumberProps): Array<{ bank: string; code: string }> {
  const results: Array<ScoreResult> = detectorList.map((detector) =>
    detector({ accountNumber, additionalRuleScore }),
  );
  const sorted = [...results].sort((a, b) => b.score - a.score);
  const banks = sorted.filter((result) => result.score !== 0);
  return banks.map(({ bank, code }) => ({ bank, code })).slice(0, length);
}

마지막으로 해당 함수를 호출하여 사용해주면 복잡한 로직 없이 계좌번호를 기반으로 유추된 은행을 얻을 수 있습니다!

import { detectAccountNumber } from 'detectAccountNumber';

const result = detectAccountNumber({ accountNumber: '7777015828112' }); 
// [ { bank: '카카오뱅크', bankCode: '090' } ]

테스트

jest 테스트 코드를 기반으로 해당 함수의 예외 처리와, 은행 유추 정확도가 높은지 테스트해보겠습니다.

import detectAccountNumber from './detectAccountNumber';

const testAccountList = [
  { name: "suhyup", accountList: ... },
  ...
];

describe("detectAccountNumber", () => {
  testAccountList.forEach(({name, accountList}) => {
    test(`계좌번호를 입력했을 때 해당 은행을 포함한 배열을 반환한다 : ${name} (${accountList.length})`, () => {
      accountList.forEach((accountNumber) => {
        const result = detectAccountNumber(accountNumber, 2);
        expect(result.includes(name)).toBe(true);
      })
    })
  })

  test("두 번째 인자에 값이 전달되었을 때 해당 값을 초과하지 않는 1개 이상의 배열을 반환한다", () => {
    expect(detectAccountNumber("3021822612521", 1).length).toBeLessThanOrEqual(1);
    expect(detectAccountNumber("100216268330", 2).length).toBeLessThanOrEqual(2);
    expect(detectAccountNumber("98206928101011", 3).length).toBeLessThanOrEqual(3);
  })

  test("해당하는 은행을 찾지 못했을 때에는 빈 배열을 반환한다", () => {
    expect(detectAccountNumber("475475475475").length).toBe(0)
  })

  test("잘못된 인자를 전달받았을 경우 빈 배열을 반환한다", () => {
    expect(detectAccountNumber("1234").length).toBe(0);
    expect(detectAccountNumber("ABCDEFGHIJKLMN").length).toBe(0);
    expect(detectAccountNumber("ㄱㄴㄷㄹㅁㅂㅅㅇㅈㅊㅋㅍ").length).toBe(0);
    expect(detectAccountNumber("999999999999999999").length).toBe(0);
    expect(detectAccountNumber("!@#$%^&*()_+").length).toBe(0);
  })
})

입력한 계좌번호의 은행에 맞게 결과가 반환됩니다.

마무리

그리 간단하지는 않지만, 무거운 서버나 머신러닝 등의 의존성 없이 간단 구현 코드만으로 유틸리티한 함수를 만들 수 있었습니다. 혹시나 저가의 비용으로 비슷한 기능을 구현해야 하는 요구사항이 생긴다면 금융결제원 CMS 계좌번호체계 기반 데이터로 시도해보시는 것을 추천드립니다.

참고 링크

송금할 때 은행 이름을 꼭 입력해야 할까요?

jhaemin/korea-financial-account-number-detector

profile
프론트엔드 공부중

5개의 댓글

comment-user-thumbnail
2025년 10월 14일

개발에는 쉬운게 없네요ㅜㅜ 토스로 송금보낼 때는 간단해보였던 것이 막상 구현해보려하니...

1개의 답글
comment-user-thumbnail
2025년 10월 19일

good idea thank you

답글 달기
comment-user-thumbnail
2025년 10월 20일

대단해요! Good Idea

답글 달기
comment-user-thumbnail
2025년 10월 21일

대단해요!

답글 달기