Backend Engineer · 14년차

고은지 (Elin)

Node.js·NestJS 기반 백엔드 14년차입니다. 정산·결제·라이선스처럼 데이터의 정확성이 곧 서비스 신뢰와 직결되는 영역을 주로 다뤄왔습니다. 복잡한 비즈니스 요구사항을 데이터 모델과 시스템 구조로 풀어내고 운영 가능한 상태까지 책임지는 것을 일의 기준으로 삼습니다.

💼 경력 14년 2개월 · 현 백엔드 (2022.05~) 🎯 주력 정산/결제 도메인 · 대용량 아키텍처 · 장애 분석 🧭 지향 근본 원인 분석 · 운영 안정화

Profile

어떤 개발자인가

정산·결제 도메인

원 단위 정수 연산, 취소 후 재정산을 통한 불변성 유지, 감사 추적을 핵심 요구사항으로 두는 금융성 정산 시스템을 설계했습니다. 출판사·저자 다층 분배와 혼합결제 환불 역산 같은 복잡한 요구사항을 데이터 모델 차원에서 해결했습니다.

대용량 · 아키텍처

천만 건 규모의 정산 데이터를 조회 전용 Flat Table 기반의 CQRS 구조로 재설계하고 결제 임계경로에서 정산을 분리한 SQS 비동기 파이프라인을 구축했습니다. 읽기·쓰기 모델 분리와 멱등성 보장을 고려한 시스템 설계에 익숙합니다.

트러블슈팅 깊이

표면적인 현상보다 병목이 발생하는 레이어를 먼저 구분하고 데이터를 통해 원인을 좁혀가는 방식을 선호합니다. heap·external은 정상인데 RSS만 증가한 메모리 문제, 처리 속도는 정상인데 큐 대기 시간만 누적된 병목 현상이 그런 사례입니다.

운영과 안정화

오류를 체계적으로 분류하고 복구할 수 있도록 하는 자가복구 구조, 무중단 점진적 마이그레이션(Express → NestJS), 백오피스 공통화를 통해 반복적인 운영 비용을 줄여왔습니다. 한 시스템을 오래 책임지며 지속적으로 개선하는 일을 선호합니다.

Flagship

대표 시스템 — 분산 정산 플랫폼

교육 콘텐츠 마켓플레이스의 정산을 메인 백엔드(정산 producer·환불 오케스트레이션)정산 서버(정산 엔진·조회 최적화) 두 서비스로 나눠 맡았습니다. 결제(동기)와 정산(비동기)의 시간적 차이를 서비스 경계로 분리했고 정산 생성부터 조회 최적화까지 파이프라인 전반을 직접 설계하고 운영했습니다.

설계 배경과 의사결정은 정산 시스템 Deep Dive에 따로 정리했습니다.

Selected Work

대표 문제 해결 경험
Domain · 0→1 Design

① 수기 엑셀 정산을 시스템으로 — 정산 플랫폼 0→1 설계

사업이 성장하며 정산의 중요성이 커졌지만 당시 정산은 전용 시스템 없이 100% 수기 엑셀로 관리됐습니다. 출판사(원저작권)와 저자(2차 저작권) 권리가 동시에 얽혀 단일 수취인이 아닌 다중 분배가 필요했고 거래량이 늘수록 정합성 오류가 비례해 늘었습니다. 본질은 자동화 부족이 아니라 확장이 불가능한 구조였습니다.

설계 판단
권리 관계가 단순하지 않은 다중 분배 구조, 기간 기반 상품의 사용량 반영, 복합 결제수단처럼 서로 다른 정산 규칙을 데이터 모델 차원에서 분리해 설계했습니다.
구조
주문(Order) → 정산 기초 데이터(Basic) → 정산(Settlement)으로 단계를 분리하고 정산 오류를 에러 코드로 구조화해 운영자가 백오피스에서 직접 보정하되 원본은 수정하지 않는 자가복구 구조를 만들었습니다.
수기 작업을 시스템으로 전환했고, 이후 데이터가 천만 건대로 성장하는 동안의 정산 기반이 됐습니다. 이 설계가 뒤의 조회 최적화(②)와 분산 정산 아키텍처로 이어집니다.

전체 분석 읽기 →

Scale · Data Architecture

② 천만 건 정산 데이터 조회 구조 개선

정산 데이터 약 800만 건, 이용 로그 약 1,100만 건 규모에서 성수기 한 달의 무거운 정산 유형(약 166만 건) 조회가 약 19초까지 걸려, 엑셀 다운로드가 사실상 불가능했습니다.

분석 — 정공법을 차례로 시도하며 한계를 확인했습니다.

  1. 테이블 튜닝(VACUUM·REINDEX) — dead tuple·bloat는 해소됐지만 19초는 그대로였습니다.
  2. 월별 파티셔닝 — 프루닝은 정상 작동했으나, 단일 파티션 안의 대량 행 × 다중 조인 앞에서 옵티마이저가 비용 계산상 인덱스를 버리고 Seq Scan을 택하는 것이 진짜 원인이었습니다. 파티셔닝은 범위를 쪼갤 뿐 조인 비용을 줄이지 못한다는 것을 EXPLAIN ANALYZE로 확인했습니다.
판단
문제를 쿼리 레벨에서 데이터 모델 레벨로 옮겼습니다. 필요한 컬럼을 모두 비정규화한 조회 전용 Flat Table(조인 0), 읽기/쓰기 DB 물리 분리, 메시지 큐 기반 동기화(생성·확정 이벤트 → Flat Table upsert)로 읽기 경로를 분리했습니다(CQRS).
읽기 경로를 조회 전용 모델로 분리해 대량 데이터에서도 안정적으로 조회할 수 있는 구조로 전환했습니다. 1·2차 시도로 문제의 본질이 인덱스나 파티셔닝이 아니라 데이터 모델과 조인 복잡도에 있음을 확인했고 해결 레이어를 쿼리에서 데이터 모델로 옮겼습니다.

전체 분석 읽기 →

Async · Throughput

③ Bull Queue 워터마크 처리 병목 분석

개별 작업은 1~5초면 끝나는데 사용자 대기 시간이 최대 31분까지 치솟았습니다.

원인
"작업이 느린 것"과 "큐가 밀리는 것"을 분리하는 데서 출발했습니다. 문제는 실행 시간이 아니라 대기 시간이었고, 근본 원인은 concurrency 미설정(기본값 1)이었습니다. 인스턴스당 한 번에 한 건만 처리하는데 유입량이 처리 용량의 약 3.5배라 백로그가 쌓였습니다.

해결 — 단순 동시성 증설로 끝내지 않았습니다.

  • concurrency를 작업 성격(I/O bound)과 리소스 경합을 계산해 단계적으로 상향
  • producer가 worker 설정을 덮어쓰던 lockDuration 우선순위 함정 제거
  • stalled를 "막기"가 아니라 "멱등하게 처리하기"로 전환 (중간 결과 선저장 + onStalled 완료 확인)
  • SIGTERM 시 새 작업을 거부하고 처리 중인 작업을 기다리는 graceful shutdown
  • 대기 시간과 처리 시간을 분리 로깅, 오토스케일링 지표를 CPU 기준으로 교체(scale-up 민감·scale-down 보수)
처리 병목을 해소하고 stalled 중복 실행, 배포 중 작업 유실, 오토스케일링 오작동까지 함께 정비했습니다.

전체 분석 읽기 →

Debugging · Native Layer

④ Node.js 워커 RSS 메모리 누수 추적

이미지 처리 워커의 RSS가 시간당 수백 MB씩 우상향하다 spawn ENOMEM으로 죽고, 컨테이너가 반복 재시작됐습니다.

디버깅 — 가설을 차례로 기각하며 좁혀갔습니다.

  1. ArrayBuffer 누수 의심 → 수정했으나 효과 미미. 이때 heap도 external도 정상인데 RSS만 오른다는 사실에서 네이티브 레이어 문제로 방향을 잡았습니다.
  2. glibc 단편화(MALLOC_ARENA_MAX=2) → 상승 속도는 절반으로 줄었지만 방향은 그대로. 원인이 아니라 증상 완화임을 확인했습니다.
  3. 단편화를 억제하자 가려져 있던 heap의 완만한 상승이 드러났습니다. "heap이 정상"은 단편화에 가린 착시였습니다.
  4. 과거 OOM 때 --max-old-space-size를 올린 조치가 오히려 GC를 게으르게 만들어 RSS 누적을 가속하고 있었습니다. 인과가 거꾸로였음을 인식하고 limit을 낮춰 GC를 적극화했습니다.
근본 원인
heap과 external 지표로 설명되지 않는 RSS 증가 패턴을 추적해 문제 범위를 V8 heap 바깥의 네이티브 레이어로 좁혔고 node-canvas@napi-rs/canvas(Rust/skia)로 교체한 뒤 누수 패턴이 사라짐을 확인했습니다.
해결
Rust/skia 기반 @napi-rs/canvas로 교체(API 호환·prebuilt). 메모리 사용률을 약 40%에서 7% 수준으로 낮추고 이후 수평을 유지했으며, 처리량이 늘어난 피크 구간에서도 누수 패턴이 사라졌습니다. 검증 기준은 배포 직후가 아니라 피크를 통과한 뒤로 잡았습니다.
가설을 세우고 데이터로 원인을 좁혀가며 문제를 추적했고 라이브러리 교체 후 피크 구간까지 포함한 장기 관찰로 해결을 검증했습니다.

전체 분석 읽기 →

Deep Dive

기술 심화 문서

정산 시스템 Deep Dive

payout 레거시 진단, settlement_prep 2단계 파이프라인, Strategy/Template, CQRS, SQS FIFO 멱등성, advisory lock, 데이터 모델링까지 — 정산 도메인의 설계 배경과 의사결정을 깊이 있게 다룬 기술 심화 문서입니다. → 정산 시스템 Deep Dive 보기

ListBuilder — AdminJS 리스트 페이지 빌더

리스트 페이지마다 반복되던 조회·필터·정렬·페이지네이션·엑셀 로직을 설정 객체 하나로 선언하는 재사용 빌더로 공통화했습니다. 로직(useListPage 훅)과 표현(렌더러) 분리, 슬롯·훅 단독 사용이라는 탈출구 설계, 모달 z-index 같은 디테일까지 — 10개 이상의 관리자 페이지가 채택한 백오피스 공통화 사례입니다. → ListBuilder Deep Dive 보기

Tech Stack

기술 스택
Core
TypeScriptNestJSNode.jsRxJSPHP / Laravel
Data
PostgreSQLMySQLRedisTypeORMMulti-DB (CQRS)
Async & Cloud
AWS SQS FIFODLQS3BeanstalkCloudWatchBull Queue
Integration & Ops
Toss PaymentsSlack APIExcelJS (stream)AdminJSLocalStack