leo.dev
backend

여러 증권사를 묶는 일임계약 시스템 설계

공모주 일임 서비스의 핵심은 “고객 계좌를 대신 운용하는 계약”이다. 그런데 그 계약의 실체는 우리 DB가 아니라 증권사 API에 있다. 계좌 개설, 본인인증, 계약 등록·갱신·해지가 전부 증권사 쪽에서 일어나고, 우리는 그 위에 얹혀 상태를 추적한다. 증권사는 6곳이고, 각자 규격도 상태도 업무 시간도 다르다. 이 글은 그걸 하나의 시스템으로 묶으면서 내린 설계 결정들이다. 더 자세한 맥락은 케이스 스터디에 있다.

증권사별로 다른데 하나처럼 다뤄야 한다

증권사마다 계약 단계가 다르다. 어디는 1원 인증이 있고 어디는 SMS, 어디는 신분증 검증 순서가 다르다. 이걸 한 덩어리 if (증권사 === '하나') ... else if ...로 짜면 증권사가 늘 때마다 그 거대한 분기가 부푼다.

그래서 증권사별 모듈로 갈랐다. 계약 진행이라는 공통 인터페이스를 두고, 그 뒤에서 증권사별 상태기계가 각자의 단계를 돈다. 담당한 하나·한국투자·KB·삼성 네 곳이 각각의 상태 전이를 갖되, 바깥에서 보는 계약의 모양은 같다. 증권사 차이는 모듈 안에 가두고, 상위 로직은 “지금 어느 상태인가”만 본다.

상태가 곧 고객 화면이다

계약 상태에 따라 고객이 보는 화면이 다르다. “인증 대기”, “계약 진행 중”, “오류”, “완료” 각각에 다른 UI가 붙는다. 여기서 위험한 건 화면이 말하는 정보와 실제 데이터가 어긋나는 것이다. 화면은 “완료”인데 증권사 쪽 계약은 아직 안 끝났으면 사고다.

그래서 증권사·에러 케이스별로 상태→뷰 매핑 테이블을 설계했다. 계약의 현재 상태가 정확히 어떤 화면·어떤 정보로 내려갈지를 데이터로 고정해, 상태와 화면이 갈라지지 않게 했다. 에러 케이스도 국제 표준 규격으로 응답을 정규화해, 증권사마다 제각각인 실패를 일관된 상태로 받았다.

동시성은 DB 락이 아니라 Redis로

계약 진행은 여러 단계가 비동기로 오가고, 어드민도 동시에 들여다본다. 같은 계약을 동시에 건드리면 상태가 꼬인다. 처음엔 DB 락으로 막으려 했는데, 진행 과정을 DB 행에 걸어 잠그니 어드민 조회까지 그 락에 걸려 성능이 주저앉았다.

그래서 진행 과정의 동시성 제어를 Redis로 옮겼다. 계약 진행 단위를 키로 점유하고, 그 키가 살아 있는 동안은 같은 계약의 중복 진행을 막는다. 점유는 만료 시간이 있어 프로세스가 죽어도 영영 잠기지 않고, 점유 중인 요청은 짧게 폴링하다 타임아웃으로 떨어진다. DB는 최종 상태를 적는 자리로 두고, “지금 누가 진행 중인가”라는 휘발성 판단은 Redis가 맡는다. 잠금의 책임을 데이터 저장소에서 분리하니 조회 성능이 돌아왔다.

증권사가 문을 닫으면 큐가 받는다

증권사 API에는 업무 시간이 있다. 장 마감 후나 점검 시간엔 호출이 실패한다. 그 실패를 그 자리에서 에러로 끝내면 계약이 거기서 멈춘다.

그래서 증권사 업무 중단으로 실패한 작업은 큐에 대기값으로 쌓았다. Bull Queue와 Redis로 Producer-Consumer 구조를 만들어, 중단 중엔 작업을 큐에 두고, 증권사 업무가 재개되면 큐에서 꺼내 API로 resolve한다. 10여 개 서버 사이의 통신을 이 큐로 잇고, 외부로 나가는 통신은 AWS SQS로 처리했다. 실패가 곧 종료가 아니라 “나중에 다시”가 되게 만든 것이다.

그래서

이 시스템 설계의 절반은 “내 통제 밖에 있는 것”을 다루는 일이었다. 계약의 진실은 증권사에 있고, 증권사는 제각각이고 가끔 문을 닫는다. 그 위에서 할 수 있는 건, 차이를 모듈 뒤에 가두고, 상태와 화면을 데이터로 묶고, 잠금을 빠른 저장소로 옮기고, 실패를 큐로 미뤄두는 것이었다. 외부 의존이 강한 시스템일수록 설계는 “어떻게 잘 호출하나”가 아니라 “호출이 실패하고 늦고 제각각일 때 어떻게 버티나”가 된다.

↑↓ 이동 열기esc 닫기