이석영 웹 풀스택 개발자
About Me
이석영 프로필 사진

이석영

Lee Seokyoung

근본 원인에서 출발해 구조로 답하는 웹 풀스택 개발자

서로 다른 스택으로 프로덕션 서비스를 만들고 운영해 왔습니다. 기능을 동작시키는 데서 멈추지 않고 병목의 근본 원인까지 파고들어, 확장 가능한 구조로 다시 설계하는 일에 강점이 있습니다.

'어떻게'보다 '왜'를 먼저 묻고, 모르는 부분은 피하지 않습니다. 맡은 서비스의 가치를 높이는 것을 넘어, 기술로 조직에 기여하는 개발자로 성장하려 합니다.

2년+
프로덕션 개발 · 운영
4
아키텍처 설계 · 주도 프로젝트
4.13/4.5
학점 · 컴퓨터공학
정보처리기사
국가기술자격 · 2024
Education & Credentials
학력
우석대학교 · 컴퓨터공학과
공학사 · 2019.03 – 2025.02 졸업
4.13 / 4.5
자료구조 A+ 알고리즘 A+ 데이터베이스 A+ 컴퓨터네트워크 A+ 웹프로그래밍 A+ 프로그래밍언어 A+ 오픈소스 SW 응용 A0 캡스톤디자인 P
자격 · 문서
Tech Stack

Backend

NestJSTypeScript JavaSpring BootSpring Batch Node.jsREST APIWebSocket · SSE

Frontend

ReactNext.js JavaScriptThymeleafAjax

Data · Messaging

MariaDBMySQLMongoDB RedisRabbitMQBullMQ TypeORM · Drizzle

Infra · DevOps

DockerKubernetesGo AWS EC2NginxJenkins CI/CD MinIOPlaywright · Puppeteer
Experience
주식회사 라인월드 재직중 2026.06 ~ 현재
S/W팀

개발

  • 통합 보안 SaaS 플랫폼(Vulinder) 전체 아키텍처 설계
  • 통합 보안 SaaS 플랫폼(Vulinder) 프론트엔드·API 개발 (React·NestJS)
  • 7종 스캐너 연동 비동기 스캔 파이프라인 구축 (RabbitMQ·Redis)
  • 실시간 스캔 상태 스트리밍 구현 (WebSocket)
  • Kubernetes 백업 관리 시스템 개발 (React·Go)

인프라 · 데이터

  • MariaDB 3NF 데이터 모델링
  • 멀티 파드 배포 환경 구성 (Docker·Kubernetes)
  • 사설 클러스터 터널링 개발 (WireGuard·Go)
개인 프로젝트 · SITE NOTE 2025.12 ~ 현재
1인 개발

개발

  • 캡처·협업 SaaS 플랫폼(SITE NOTE) 전체 아키텍처 설계
  • 캡처·협업 SaaS 플랫폼(SITE NOTE) 프론트엔드·API 개발 (Next.js·NestJS)
  • 비동기 워커·큐 기반 캡처 파이프라인 구축 (BullMQ·Redis)
  • 초대형 이미지 타일링·스트리밍 처리 구현 (Sharp)
  • 브라우저 확장 기반 전체 페이지 캡처 개발

인프라

  • Docker 컨테이너 환경 구성 및 운영
  • AWS EC2·Nginx 인프라 구축 및 운영
주식회사 마루온 2024.07 ~ 2025.11 · 1년 5개월
S/W팀

개발

  • 에너지 모니터링 시스템(EMS) 프론트엔드·API 개발 (Thymeleaf·JS·Spring Boot)
  • Spring Batch 대용량 데이터 배치 개발
  • 창고 관리 시스템(WMS) 초기 아키텍처 설계 및 개발
  • 사내 JS 라이브러리 개발 (트리 시각화·Table Editor·Time Bar)

인프라 · CI/CD

  • AWS EC2·Nginx 인프라 구축 및 운영
  • Jenkins 멀티 모듈 빌드·배포 파이프라인 구축

연구개발

  • 국가 연구개발사업 수행 (지원금 1,000만원 규모 · 계획서·보고서·성과 프로그램 C# 개발)
Projects

Vulinder

통합 보안 SaaS 2026.06 ~ 현재
guide.vulinder.com

소스코드 · 컨테이너 이미지 · 웹 서비스 · 서버 인프라 · 런타임까지 하나로 묶은 보안 취약점 관리 SaaS. SAST · SCA · DAST · SBOM · IaC · Secret · License 분석을 한곳에서 제공합니다.

역할 · 서비스 전체 아키텍처 설계 및 풀스택 개발 (1인 주도)

TypeScriptNestJSReact MariaDBTypeORMRabbitMQ RedisWebSocketMinIO DockerKubernetesGo

시스템 아키텍처

KUBERNETES WS 실시간 작업·결과 consume 저장 아티팩트 이벤트 webhook 사용자 WebReact AdminReact IngressNginx APINestJS RabbitMQ Scan WorkersSAST·DAST·SCA·IaC·Secret Redis MariaDB MinIO Falco고객 런타임

제품 화면

Vulinder 통합 대시보드
통합 대시보드 · 심각도 · 컴플라이언스 · 보안 뉴스
Vulinder 취약점 관리 인박스
취약점 관리 · 서버사이드 페이징 · 트리아지
Vulinder 통합 컴플라이언스
통합 컴플라이언스 · OWASP · CWE · KISA 커버리지
Vulinder SAST 상세 드로어
취약점 상세 드로어 · 취약 코드 · 근거 · 참고 링크

문제 해결 기록

CASE 01

수만 건 스캔 결과의 조회 성능과 멀티 파드 상태 정합성

!문제

스캐너 7종을 하나로 통합하자 초기 구조가 그대로는 버티지 못했습니다. 스캐너마다 출력 형식이 제각각이고, 결과는 수만 건 단위로 쏟아졌으며, 서비스는 여러 파드로 나뉘어 떠 있었습니다. 결국 두 숙제를 동시에 풀어야 했습니다. 많은 결과를 느리지 않게 보여주는 것, 그리고 여러 파드에서도 상태가 어긋나지 않게 하는 것.

?원인

화면이 느린 걸 처음엔 쿼리 탓이라 생각했지만, 뜯어보니 두 지점 모두 구조가 근본 원인이었습니다.

  • 상세·목록이 원본 결과를 매 조회마다 다시 파싱했고,
  • 파드 간 실시간 상태가 공유되지 않아 이벤트가 어긋나고 작업이 중복됐습니다.

읽기 경로와 이벤트 전파, 두 설계 모두에 문제가 있었습니다.

결정 · 개선
  • 저장 시점 정규화 · 조회할 때마다 원본을 다시 가공하던 방식을 버리고, 스캔이 끝나는 시점에 3NF로 정규화·투영해 저장했습니다. 조회는 조립된 행을 읽기만 하면 되어 상세·목록 응답이 함께 좋아졌습니다.
  • Redis 이벤트 브리지 · 파드 간 이벤트를 Redis Pub/Sub로 잇고 분산 락으로 중복 작업을 막았으며, 이벤트를 조직 단위로 스코핑해 파드가 늘어도 알림이 섞이거나 어긋나지 않게 했습니다.
관통한 원칙

두 문제의 답이 결국 하나였습니다. "읽을 때 가공"에서 "저장할 때 정규화"로. 비싼 일을 읽기 경로에서 걷어내 쓰기 경로로 옮기는 이 원칙을 대시보드 등 제품 전반에 반복 적용했습니다.

성과
  • 조회 성능
    저장 시점 정규화로 상세·목록 개선
  • 멀티 파드
    이벤트 정합성 · 작업 중복 해소
CASE 02

사설 클러스터 VPN 스캔의 TLS 핸드셰이크 실패와 MTU 조정

!문제

VPN으로 접속하는 사설 클러스터에서 리소스 목록만 가볍게 가져오는 스캔은 잘 되는데, 클러스터 전체 설정을 동시에 훑는 무거운 병렬 검사만 TLS handshake timeout으로 실패했습니다. 작은 패킷을 주고받는 TCP 연결은 멀쩡히 통과하는데, 인증서를 교환하느라 수 KB짜리 대형 레코드를 보내는 TLS 핸드셰이크에서만 멈추는, 재현조차 까다로운 증상이었습니다.

?원인

유저스페이스 WireGuard가 외부 UDP 경로 위에 트래픽을 다시 감싸면서, 내부 MTU 1420에 WG 오버헤드가 더해져 외부 경로의 실제 전송 한계(PMTU)를 넘겼습니다. 그 결과 큰 패킷만 조용히 버려졌습니다. "연결은 되는데 TLS 핸드셰이크만 타임아웃"이라는 조합이 결정적 단서였습니다. 이건 MTU 드롭의 전형적인 신호였습니다.

결정 · 개선

터널 MTU를 1420 → 1280으로 낮췄습니다. 여기에 한 방향 EOF에 양쪽 소켓을 즉시 닫아 대용량 응답이 중간에 잘리던 파이프 버그를 전이중 half-close(CloseWrite)로 고치고, 연결이 한꺼번에 몰릴 때의 일시적 실패를 1회 재시도로 흡수했습니다. 무거운 병렬 검사에는 검사 대상을 네임스페이스 단위로 좁혀 동시 핸드셰이크 폭주 자체를 줄였습니다.

왜 1280인가

1280은 어떤 외부 경로에서도 조각·드롭이 없는 IPv6 최소 보장 MTU입니다. 증상이 MTU 드롭의 전형이라, 리스크가 가장 작은 최소 수정을 1순위로 택했습니다.

성과
  • TLS 핸드셰이크
    70초 타임아웃안정 통과
  • 사설 클러스터 스캔
    2,895건 E2E 성공
  • 대용량 응답
    중간 잘림 제거
CASE 03

호스트 OS 패키지 스캔의 OOM 실패와 SBOM 파이프라인 전환

!문제

운영 서버의 OS 패키지 취약점 스캔이 항상 0건으로 돌아왔습니다. 결과가 비는 데 그치지 않고, 재조정 로직이 "이번엔 결과가 없다"고 판단해 이전에 쌓아둔 1,208건까지 삭제했습니다. 스캔이 실패하고 있다는 신호도 남지 않았습니다.

?원인

접속 경로(VPN·SSH)도 스캐너도 정상이었지만, 이 서버의 rpm 패키지 DB가 181MB로 비대했습니다. 스캐너가 이를 파싱하다 메모리 2.9GB를 넘겨 OOM으로 강제 종료됐고, 수집본 전체를 한 번에 훑는 구조라 한 곳의 실패가 스캔 전체를 0건으로 만들었습니다. 결과가 비는 것과 기존 데이터가 지워지는 두 증상이 여기서 함께 나왔습니다.

결정 · 개선

무거운 원본 DB를 통째로 전송하는 대신, 호스트에서 설치된 패키지 목록만 추출해 표준 형식(CycloneDX SBOM)으로 변환한 뒤 그 목록을 스캐너로 분석하도록 파이프라인을 전환했습니다. 단일 결과만 생성되어 OOM도, 기존 결과 덮어쓰기도 사라졌습니다.

왜 원본 대신 SBOM인가

원본 rpm DB는 서버마다 크기가 제각각이라 언제든 OOM을 다시 부를 수 있습니다. 설치 목록을 표준 SBOM으로 정규화하면 입력 크기가 예측 가능해지고, 스캐너는 검증된 SBOM 분석 경로만 타므로 대규모 환경에서도 안정적입니다.

성과
  • OS 패키지 취약점
    0건5,706건 탐지
  • 기존 데이터
    1,208건 오삭제 재발 방지
  • 스캐너 메모리
    OOM(2.9GB+) 제거
CASE 04

실시간 스캔 알림의 완료 이벤트 유실과 WebSocket heartbeat 도입

!문제

스캔이 DB·큐상 분명히 완료됐는데도, 프론트의 상태 배지는 계속 '분석중'에 머물렀습니다. 로그를 초 단위로 맞춰보니, 완료 이벤트를 보내는 그 순간 소켓은 분명히 연결(clients=1)돼 있었는데도 브라우저가 그것을 받지 못했습니다.

?원인

WebSocket에 ping/pong keepalive가 없어, 피어가 끊겨도 서버는 TCP 타임아웃(~60초) 전까지 그 사실을 모릅니다. 그 사이 소켓은 readyState가 여전히 OPEN이라, 완료 이벤트가 살아있어 보이는 죽은 소켓(half-open)으로 send되어 유실됐습니다. 브라우저는 새 소켓으로 재연결하지만, 지나간 이벤트는 다시 오지 않았습니다.

결정 · 개선

서버에 30초 주기 heartbeat를 두어, 직전 주기에 pong이 없던 소켓을 연결 목록에서 제거하고 terminate했습니다. 죽은 소켓을 60초가 아니라 30초 안에 감지해 유실 창을 좁혔고, 여기에 프론트 재연결 시 진행 중 스캔을 다시 조회해 유실 창에 떨어진 완료까지 복구했습니다.

왜 2겹인가

heartbeat만으론 정확히 그 창에 떨어진 이벤트를 여전히 놓칠 수 있고, reconcile만으론 좀비 소켓이 오래 남아 매 완료마다 유실과 복구를 반복합니다. 유실 창을 줄이는 쪽과 놓쳐도 복구하는 쪽을 함께 둬, 완료 신호가 결국 도달하게 했습니다.

성과
  • 좀비 소켓 감지
    ~60초~30초
  • 완료 신호
    유실 제거 · 확실히 도달
  • '분석중' 잔류
    heartbeat + 재연결 reconcile 2겹

SITE NOTE

캡처·협업 SaaS 개인 프로젝트 2025.12 ~
sitenote.net

웹 페이지를 통째로 캡처해 그 위에 캔버스로 주석을 달고, 태그·컬렉션으로 정리하며 저장한 페이지에 AI로 질문까지 하는 협업 SaaS. 브라우저 확장으로 전체 페이지를 수집하고 무거운 캡처·이미지 처리는 비동기 워커·큐로 분리했으며, 조직·권한과 구독 결제, 3개 언어(한·영·일)를 갖춘 풀스택 프로덕션입니다.

역할 · 서비스 전체 아키텍처 설계 및 개발 (1인)

TypeScriptNext.jsNestJS MySQL 8Drizzle ORMRedis BullMQOpenAIPlaywright SharpFabric.jsDocker NginxAWS EC2

시스템 아키텍처

DOCKER HOST · NGINX REVERSE PROXY WORKER · NESTJS HTTP 작업 등록 consume 저장 저장·조회 사용자 브라우저 확장 Nginx WebNext.js APINestJS MySQL 8Drizzle ORM RedisBullMQ Playwright Sharp Storage타일 이미지

제품 화면

SITE NOTE 워크스페이스 라이브러리
워크스페이스 · 컬렉션 · 태그로 저장한 캡처 관리
SITE NOTE 캡처 주석 편집기
전체 페이지 캡처 위에 캔버스 주석 · 필기

문제 해결 기록

CASE 01

동적 웹 페이지의 전체 캡처 안정화와 서버 병합 파이프라인

!문제

"전체 페이지를 그대로 저장한다"는 제품의 핵심 가치는 실제 웹사이트 앞에서 가장 자주 깨졌습니다. 스크롤할 때 따라다니는 sticky 헤더가 화면마다 중복으로 찍히고, lazy-load 이미지는 회색 자리표시자로 비어 나오며, 스크롤 애니메이션 라이브러리는 요소를 반쯤 나타난 상태로 얼렸습니다. 사이트마다 깨지는 양상이 달라, 재현과 대응이 가장 까다로운 영역이었습니다.

?원인

단순히 페이지 높이만큼 스크롤하며 이어 붙이는 방식으로는 현대 웹의 동적 렌더링을 담을 수 없었습니다. 진짜 스크롤 컨테이너가 body가 아니라 내부 요소일 때가 많았고(오버플로 컨테이너·Perfect Scrollbar), position:fixed/sticky 요소는 뷰포트에 고정돼 프레임마다 재등장했으며, 이미지·콘텐츠는 스크롤이 닿아야 비로소 로드됐습니다. 게다가 캡처 도중 페이지 전체 높이가 늘어나 좌표가 밀리기도 했습니다.

결정 · 개선

브라우저 확장의 콘텐츠 스크립트에서 캡처 파이프라인을 단계로 나눠 이 변수들을 하나씩 통제했습니다.

  • DOM을 훑어 실제 스크롤 컨테이너를 탐색(메인 문서 / 내부 오버플로 / Perfect Scrollbar, 화면 밖·초소형 요소 필터)하고,
  • 캡처 직전 sticky→relative 전환·스크롤바 숨김·스크롤 애니메이션 라이브러리(AOS·Skrollr 등) 무력화·사이트별 예외 처리를 적용했습니다.
  • 본 캡처 전 페이지를 빠르게 훑는 warmup 스크롤로 lazy-load를 미리 트리거하고,
  • 프레임마다 fixed 요소를 처리하며, 캡처 중 높이가 변하면 위치 배열을 재계산·재큐잉했습니다.
왜 브라우저가 아니라 서버에서 병합했나

브라우저 Canvas에는 약 32,767px 높이 제한이 있어, 긴 페이지를 클라이언트에서 하나로 합치면 잘립니다. 그래서 확장은 개별 프레임과 좌표 메타데이터만 수집하고 병합은 서버에 맡겼습니다. 덕분에 사실상 무제한 길이의 페이지를 담을 수 있고, 뒤이은 타일링·스트리밍 재설계와도 자연스럽게 맞물립니다.

성과
  • sticky·fixed 중복
    프레임별 처리로 제거
  • lazy-load 이미지
    warmup 선트리거로 온전히 캡처
  • 내부 스크롤·동적 높이
    컨테이너 탐색 · 좌표 재계산으로 대응
  • 페이지 길이
    Canvas 한계 우회 · 사실상 무제한
CASE 02

고비용 캡처 작업의 비동기 워커 분리

!문제

초기 구조는 사용자가 캡처를 요청하면 API 서버가 직접 Playwright를 띄워 대상 페이지에 접속·렌더링·스크린샷·저장까지 전부 처리했습니다. 캡처 1건당 평균 30~40초가 걸렸고, 그동안 서버 자원이 묶여 다른 요청까지 느려졌습니다.

?원인

Node.js는 싱글 스레드입니다. CPU·I/O 집약적인 캡처가 이벤트 루프를 오래 점유하면서, 실시간 응답·브라우저 렌더링·파일 저장이라는 성격이 다른 세 가지 일을 한 서버가 동시에 떠안는 구조적 병목이 근본 원인이었습니다.

결정 · 개선

고비용 작업(캡처·업로드)을 별도 Worker App으로 분리하고, 그 사이를 메시지 큐로 이었습니다. 요청 처리와 실제 작업 수행을 떼어내고, Page Capture Queue와 Snapshot Processor Queue를 나눠 캡처와 이미지 후처리가 서로 독립된 파이프라인에서 돌게 했습니다.

왜 BullMQ였나

Kafka · RabbitMQ · BullMQ를 비교했지만, 이미 캐시로 Redis를 운영 중이었습니다. 별도 브로커 인프라를 새로 세우지 않고 가장 빠르게 적용할 수 있는 Redis 기반 큐 라이브러리 BullMQ를 선택했습니다.

성과
  • API 체감 응답 시간
    97% 단축
  • 확장
    Worker 추가만으로 선형 확장
  • 사용자 경험
    processing/completed 실시간 표시
CASE 03

초대형 이미지 처리의 OOM 해결과 타일링·스트리밍 재설계

!문제

긴 페이지를 캡처하면 전체를 하나의 초대형 이미지로 병합한 뒤 후처리했습니다. 100MB가 넘는 이미지에서 최대 2GB 이상 메모리를 쓰다 OOM으로 프로세스가 강제 종료됐고, 32,000px 이상 페이지는 포맷·Canvas 한계로 잘렸습니다.

?원인

전체를 먼저 병합한 뒤 타일로 자르는 순서 자체가 문제였습니다. 초고해상도 이미지를 단일 객체로 메모리에 통째로 올려 압축·최적화하는 동안 대형 버퍼가 오래 상주했고, 모든 처리가 메모리 위에서 동기적으로 일어났습니다.

결정 · 개선

순서를 뒤집어 병합 단계를 없애고, 캡처된 프레임에서 필요한 영역만 바로 타일로 생성하도록 파이프라인을 재설계했습니다. 이미지 전체를 적재하는 대신 Sharp로 필요한 부분만 스트리밍하고, 프레임 전처리는 Promise.all 병렬, 타일 업로드도 순차 → 병렬로 바꿨습니다. 포맷은 PNG → WebP(품질 92)로 전환했습니다.

성과
  • 피크 메모리
    2GB600MB
  • 처리 시간
    45초15초
  • 동시 처리
    2개10개 (5배)
  • 타일 용량
    2MB350KB (82%↓)
CASE 04

웹훅 재시도·중복에도 어긋나지 않는 구독 결제

!문제

구독 결제를 붙이자, 완료를 알리는 웹훅이 재시도·중복으로 도착할 수 있었습니다. 게다가 팀 결제는 결제는 성공했는데 뒤이은 조직 생성이 실패하면 "돈은 빠졌는데 상태는 없는" 최악이 가능했고, 업그레이드는 즉시·다운그레이드는 다음 주기부터여야 하는 상태 전이도 까다로웠습니다.

?원인

결제 완료 이벤트는 정확히 한 번을 보장하지 않았고(at-least-once), 외부 결제 API는 확인 도중 실패할 수 있었습니다. 클라이언트가 결제 금액을 중간에 바꿀 여지도 있었고, 다운그레이드를 즉시 반영하면 남은 기간만큼 손해였습니다.

결정 · 개선
  • 웹훅 멱등 · 이벤트 식별자에 UNIQUE를 걸고 처리 전에 게이팅해, 같은 웹훅이 몇 번 와도 상태가 두 번 바뀌지 않게 했습니다.
  • 금액 위변조 차단 · 결제 확인 직전, 서버가 발급한 대기 주문(30분 TTL)의 기대 금액과 대조해 클라이언트가 조작한 금액을 거부했습니다.
  • 원자적 팀 체크아웃 · 외부 API 호출과 DB 쓰기를 분리하고 조직·구독·결제 기록을 한 트랜잭션으로 묶어, 실패하면 자동 환불로 롤백(환불까지 실패하면 이벤트를 발행해 수동 개입)했습니다.
  • 안전한 상태 전이 · 업그레이드는 즉시, 다운그레이드·취소는 주기 말에 반영하고, 조직당 활성 구독은 항상 하나만 유지했습니다.
돈은 절대 어긋나면 안 된다

결제는 한 번의 오차도 신뢰를 무너뜨립니다. 그래서 정확성은 DB UNIQUE 제약과 트랜잭션 경계로 강제하고, 되돌리기 어려운 실패(외부 결제)는 자동 환불 + 수동 개입 신호로 감쌌습니다.

성과
  • 웹훅 중복·재시도
    DB UNIQUE로 정산 1회 강제
  • 팀 결제 실패
    자동 환불로 원자적 롤백
  • 결제 실패
    상태 무오염 · 재시도 가능
  • 금액 조작
    서버 대조로 거부

에너지 모니터링 시스템 (EMS)

실시간 모니터링 2024.08 ~ 2025.10

분산된 전력·태양광 계측기에서 Modbus TCP로 데이터를 수집해 실시간으로 저장·집계하는 에너지 모니터링 플랫폼. 원시 데이터는 MongoDB에, 1·15·60분 집계는 MariaDB에 나눠 담고, 사용자·관리자 웹과 실시간 알림(SSE)까지 풀스택으로 개발했습니다.

역할 · 사용자/관리자 시스템 풀스택 개발 및 운영 (2인)

JavaSpring BootSpring Batch JavaScriptThymeleafMariaDB MongoDBAWSJenkins

시스템 아키텍처

EC2 · NGINX (단일 서버) HTTPS · SSE REST 조회 실시간 저장 1·15·60분 배치 집계 적재 Modbus TCP HTTP 전송 사용자 전력 · 태양광계측기 · 인버터 MEMS-ETLPython · Modbus Web · Thymeleaf관리자 · 사용자 APISpring Boot MSA MariaDB집계 RDB MongoDB실시간 수집 Spring Batch1·15·60분 주기

문제 해결 기록

CASE 01

600만 건 배치 성능 개선, Paging에서 Cursor로

!문제

전력 데이터의 백업·삭제 배치가 대량 구간에서 비정상적으로 느려졌습니다. 삭제 로직을 제외한 테스트로 대량 데이터 처리 자체가 병목임을 먼저 좁혔습니다.

?원인

처음엔 Chunk Size로 인한 메모리·I/O 병목을 의심했습니다. 그런데 코드를 뜯어보니 Reader가 MongoPagingItemReader였던 게 진짜 원인이었습니다. 이 방식은 skip + limit 기반이라, 500만 번째 데이터를 읽으려면 앞선 499만 개를 모두 조회한 뒤 건너뜁니다. 데이터가 많을수록 조회 시간이 기하급수적으로 늘어나는 구조였습니다.

결정 · 개선

Reader를 MongoCursorItemReader로 교체해 커서 기반 스트리밍으로 전환, skip/limit이 만들던 불필요한 스캔을 제거했습니다. 배치 종료 시 커서를 명시적으로 닫아 리소스 누수도 막았습니다. 첫 가설(Chunk Size)에 머무르지 않고 Reader 구현까지 내려가 근본 원인을 확인한 것이 핵심이었습니다.

성과
  • 약 600만 건 처리
    5일+20분
  • 안정성
    대량에서도 일정한 성능 확보
CASE 02

두 시스템 사이의 데이터 불일치를 막는 동기화 설계

!문제

관리자 시스템에서 장치를 CRUD하면 그 결과가 서로 다른 두 시스템(관리자 DB · 장치 시스템)에 함께 반영돼야 했습니다. 그런데 한쪽은 성공하고 다른 쪽은 실패하는 상황이 생기면서, 두 시스템의 데이터가 어긋나는 심각한 정합성 문제가 있었습니다.

?원인

시스템 간 통신이 각각 개별적으로 처리돼, 요청 성공/실패 여부를 일관되게 관리할 체계가 없었습니다. 게다가 장치 시스템은 장치 정보를 JSON 파일로 저장하는 구조라, DB 반영 순서를 제어하지 않으면 언제든 불일치가 날 수 있었습니다.

결정 · 개선
  • ResponseMessage 공통화 · 공통 모듈에 응답 객체를 정의하고 두 시스템 간 모든 통신을 이 객체로 주고받게 해, 요청 성공/실패를 명확하게 판별할 수 있게 했습니다.
  • 요청 순서 재설계 · 파일 기반이라 되돌리기 어려운 장치 시스템을 먼저 처리하고, 그게 성공했을 때만 DB에 최종 반영하도록 순서를 뒤집었습니다. 실패 시에는 DB 반영을 차단해 불일치를 원천 차단했습니다.
왜 장치 시스템을 먼저인가

DB 트랜잭션은 롤백이 쉽지만, JSON 파일로 상태를 들고 있는 장치 시스템은 되돌리기 어렵습니다. 실패 가능성이 높고 되돌리기 어려운 쪽을 먼저 시도하고, 그 성공을 조건으로 쉬운 쪽(DB)을 커밋하는 순서가 정합성에 가장 안전했습니다.

성과
  • 두 시스템 정합성
    데이터 불일치 원천 차단
  • 에러 추적
    성공/실패 명확화로 대응 용이
CASE 03

Spring Batch × MongoDB의 Timezone 오차, 하루가 밀린 삭제

!문제

모든 데이터는 MongoDB에 UTC 기준으로 저장되고, 배치 서버는 KST(UTC+9)를 쓰고 있었습니다. 원래 의도는 KST 자정(UTC 15:00:00) 이전 데이터를 삭제하는 것이었는데, 실제로는 UTC 06:00:00까지의 데이터가 삭제되는, 하루가 밀린 현상이 나타났습니다.

?원인

시각 계산에 ZonedDateTime.withZoneSameInstant(UTC).toLocalDateTime()을 썼는데, toLocalDateTime()에서 타임존 정보가 사라지자 Spring Data MongoDB가 그 값을 다시 서버 기본 타임존(KST)으로 재해석했습니다. 그 결과 UTC 15:00:00 값이 한 번 더 변환되며 경계가 밀렸습니다. 눈에 안 보이는 타임존 이중 변환이 범인이었습니다.

결정 · 개선

toLocalDateTime() 대신 Instant를 그대로 사용해 타임존 손실 자체를 없앴습니다. 쿼리도 Criteria.where("createDate").lt(zonedDateTime.toInstant())처럼 UTC Instant를 직접 전달해, 컨버터가 올바른 UTC로 변환하도록 고쳤습니다.

성과
  • 삭제 경계
    정확히 KST 자정 기준으로 동작
  • 타임존 손실
    근본 해결 · 다양한 Timezone 확장 가능

창고 관리 시스템 (WMS)

창고 관리 2024.07 ~ 2024.08

종이·대면으로 처리하던 창고 입·출고와 자재·재고 관리를 웹으로 전산화한 시스템. 자재 마스터와 재고를 DB로 옮기고 입·출고를 트랜잭션으로 기록하는 초기 아키텍처를 설계하고 배포까지 주도했습니다.

역할 · 초기 시스템 아키텍처 설계 및 풀스택 개발 · 업무 처리 시간 50%+ 단축

JavaSpring BootSpring Data JPA Spring SecurityThymeleafJavaScript MySQLAWSJenkins

문제 해결 기록

CASE 01

종이·대면 입·출고의 웹 전산화와 트랜잭션 기반 재고 관리

!문제

입·출고가 종이 서류와 담당자 간 대면 확인에 의존했습니다. 처리에 시간이 오래 걸렸고, 무엇보다 기록된 수량과 실제 재고가 자주 어긋나 재고를 믿기 어려웠습니다.

?원인

자재 정보가 수기·종이로만 관리돼 실시간 집계와 이력 추적이 불가능했습니다. 입·출고 내역이 사람의 기록과 대면 인수인계에 묶여 있어, 누락·중복·오기입이 나면 어디서 틀어졌는지 되짚을 근거 자체가 없었습니다.

결정 · 개선

오프라인 대면 프로세스를 웹으로 전산화했습니다. 품목 정보(품명·규격·단가 등)를 DB로 옮기고, 재고 수량은 입고에서 출고를 뺀 값으로 계산했습니다. 모든 입·출고를 트랜잭션으로 기록했기 때문에 재고가 언제나 정확하고 일관되게 맞아떨어졌습니다. 처리·조회를 화면에서 바로 할 수 있게 만들어 대면 확인 단계 자체를 없앴습니다.

전산화가 곧 정확성

핵심은 모든 입·출고가 빠짐없이 한곳에 기록되게 만든 것입니다. 수량 관리가 사람의 종이에서 시스템으로 넘어오자, 재고가 어긋날 여지 자체가 사라졌습니다.

성과
  • 업무 처리 시간
    50%+ 단축
  • 재고 수량 정확도
    입고-출고 계산으로 항상 일치
  • 이력 추적
    입·출고 전 과정 기록·조회 가능