방
INTRODUCE
병원 대상 의료 IT 서비스의 알림·CRM·통계 도메인을 담당해 온 4년차 풀스택 개발자입니다. Java·Spring Boot 백엔드가 주력이고, 운영자·병원이 쓰는 화면은 Vue 로 직접 만들었습니다. 알림 서버를 RabbitMQ 기반 별도 서비스로 분리하고 발송 채널을 확장했으며, 조회 성능과 락 경합 문제를 원인부터 좁혀 해결해 왔습니다. 운영에서 겪은 문제는 개인 실험으로 재현해 기술 선택의 근거를 확인합니다.
Seongmin
알림 서버 분리와 채널 확장
메인 서버에 붙어 있던 발송을 RabbitMQ 기반 별도 서비스로 분리하자고 제안하고 전환했습니다. 새 채널은 서비스 코드를 고치지 않고 확장 포인트만 추가하면 되는 구조로 정립했습니다. 채널 전환은 월 기준 2025-08 → 2026-07 에 SMS 35,194건 → 14,821건(−58%), 알림톡 0 → 4,517건이고, 알림톡 건당 단가는 SMS 대비 약 70% 낮습니다.
RabbitMQMSASystem Design
진료 통계 KPI 서비스 0 → 1
수기로 관리하던 진료 성과 지표를 서비스로 만들었습니다. 기획을 제안하고 사업팀과 지표를 정의한 뒤, 사용자가 기준일을 바꾸는 항목까지 담을 수 있는 집계 구조를 설계·구현했습니다. 제약 환경(1Core/1GB) 부하시험에서 동시 100명 평균 응답 26.7초 → 2.73초, 처리량 10.8배.
Data PipelineBatchProduct
락 경합의 원인을 커밋 시점까지 좁힘
트랜잭션을 잘게 나눠도 재발하던 lock wait timeout 을, 원인이 트랜잭션 길이가 아니라 락을 잡는 문장의 커밋 시점임을 확인해 주 2~3회에서 0건으로 없앴습니다. 이후 같은 장애를 실험실에서 재현해 처방별 효과를 측정으로 확인했습니다.
TransactionTroubleshootingMySQL
EXPERIENCE
TOTAL
총 3년 9개월
2023. 02 ~
(주) 플라잉닥터
풀스택 개발자
- 병원 대상 의료 IT 서비스의 알림·CRM·통계 도메인을 백엔드 중심으로 담당하고, 운영자·병원용 화면은 프론트까지 직접 구현
- 궤적 — 2023년 통합 클라이언트 구축과 화면 개발로 시작해, 2024년 Vue3·TypeScript 전환과 과금 기능을 거쳤고, 2025년부터 통계 서비스와 알림 아키텍처를 맡았다. 알림은 2023년 첫 작업부터 3년간 이어 온 도메인이다
- CRM 이벤트 기반 자동발송, 발송 이력 관리 및 포인트 기반 문자 과금 기능 개발
- 템플릿 데이터 구조를 설계하고 셀프서비스 편집 기능을 개발해, 이미지 제작 관련 CS 요청을 주 10건에서 2~3건으로 줄임
- 기획·사업팀과 통계 요구사항 및 지표를 정의하고, 집계 파이프라인과 조회 API 설계
- 운영 장애 분석, 데이터베이스 조회 최적화 및 처리 결과 모니터링 개선
- 주간 회의에서 설계 방향을 제안·합의하고, 배포 범위와 일정을 기획·디자인과 협의해 결정. 배포 전 변경 내용을 CS팀에 사전 공유하는 절차로 운영
- 2026년부터 담당 범위 일부를 다른 개발자에게 이관하고, 해당 프로젝트에는 멘토 성격으로 참여하는 구조로 조정
PROJECT
2025 ~
알림 서비스 분리 및 발송 채널 확장
(주) 플라잉닥터 · 서버 분리 제안·구현 / 채널 확장 설계·구현 주도- 맥락 — 알림 발송이 메인 서버에 붙어 있어 대량 발송 때마다 서비스 전체가 부하를 받았고, 발송 흐름이 서비스마다 중복 구현돼 채널을 바꾸려면 전 서비스를 손대야 하는 구조였다
- 규모 — SMS·알림톡·PUSH 합계 월 2만 건대(일 평균 496건). 오전 07~08시에 SMS 물량의 47.4%가 몰린다
- 대량 발송이 메인 서버에 주는 부하와 서비스별로 중복된 발송 흐름을 구조적으로 분리하고, 채널 확장이 가능한 발송 시스템으로 재설계
- 2025 · 알림 서버 분리
- 팀 리뷰를 거쳐 RabbitMQ 기반 별도 서비스로 분리하고, 외부 API 호출을 Virtual Thread로 병렬 처리
- 설계 판단 — 큐를 둔 이유는 처리량이 아니다. 이 규모는 초당 몇 건 수준이라 처리량만으로는 큐가 필요하지 않다. 대량 발송이 메인 서버를 흔드는 것을 막고, 실패를 격리해 재처리하려는 것이 목적이었다. 그래서 ACK·재처리·DLQ 를 기본으로 지원하는 RabbitMQ 를 골랐고 Kafka 는 이 요구에 과했다
- 설계 판단 — 발송이 외부 채널사 API 대기가 대부분인 I/O 바운드라는 점을 근거로, 스레드 수를 늘리지 않고 동시성을 확보하는 Virtual Thread 기반 구조 선택
- 건별 조회·저장을 묶음 조회와 일괄 처리로 전환해 배치 처리 구조 개선
- 2025.11 · 알림톡 채널 확장
- 기존 발송 호출부를 건드리지 않고 ChannelSender 확장 포인트로 알림톡 추가. 새 채널을 서비스 코드 수정 없이 도입하는 공통 구조로 정립
- 알림 유형별로 알림톡을 우선 발송하고 실패 시 SMS 로 폴백하는 채널 선택 정책 구현. 템플릿 변수 치환과 검증은 채널과 무관하게 공통화
- 알림톡 우선 발송으로 전환되는 구간에서 건당 단가가 SMS 대비 약 70% 낮음을 확인
- 전환 실측 (2025-08 → 2026-07, 월 기준) — SMS/LMS 35,194건 → 14,821건(−58%), 알림톡 0건 → 4,517건
- 2026 · 템플릿 셀프서비스
- 하드코딩된 템플릿을 DB 기반으로 전환하고, 병원이 등록한 템플릿을 운영자가 검수하는 셀프서비스 흐름으로 확장
- NCP 채널·템플릿을 조회 어댑터로 동기화하고, OTP 인증을 알림톡 우선(실패 시 SMS)으로 전환
- 2026 · CRM 알림톡 발송 — 초기 구현을 세 번 되돌림 (STAG 반영, 운영 배포 전)
- 맥락 — 병원 요청으로 CRM 개별·단체 문자 화면에서 승인된 템플릿으로 알림톡을 보낼 수 있게 했다. 관리 포인트가 늘어난다는 이유로 팀에서 보류해 온 건이어서, 화면을 새로 만들지 않고 기존 발송 화면에 수단을 추가하는 방향으로 합의했다
- 되돌림 1 — 결과 판정을 위해 발송을 동기 REST 로 바꿀 때 처음에는 모든 호출자를 한꺼번에 전환했다. OTP 와 기존 알림까지 큐의 버퍼링을 잃는 전역 변경이었고 합의된 범위도 아니어서 되돌렸다. 결과 판정이 필요한 CRM 발송만 동기로 두고 나머지는 큐를 유지
- 되돌림 2 — 발송 결과를 성공·실패 두 값으로 판정했다가, "안 나갔다" 와 "나갔는지 모른다" 를 구분하지 못해 개발 환경에서 같은 문자가 반복 발송됐다. SENT / NOT_SENT / UNKNOWN 3분류로 바꾸고 재시도는 안 나간 것이 확실한 경우만 하도록 제한. 모르는 건은 자동 복구하지 않고 사람이 판단하게 남겼다
- 되돌림 3 — 캠페인 전체를 한 트랜잭션으로 묶었더니 1건 실패가 이미 나간 나머지의 결과 기록까지 롤백시켰다. 예약 1건 = 트랜잭션 1개로 격리
- 사고 건수를 추정이 아니라 실측으로 정정했다 — 처음 48회로 봤으나 실제로는 9회였다. 무효로 판정한 실험 2건도 지우지 않고 기록에 남겼다. QA 점검표 104항목을 만들면서 "요구사항 통과가 배포 가능을 뜻하지 않는다" 를 배포 판정 기준으로 팀과 확정
- 배포 범위 — 기다리는 병원이 있어 범위를 쪼개서라도 일정을 앞당기자는 제안이 있었으나, 전송 결과를 확인할 화면 없이 발송 기능만 먼저 내보내는 것은 부적절하다고 보고 전송 내역과 함께 내자고 했다. 일정은 지키고 범위를 묶는 쪽으로 정리
- 2026 · 집계에서 빠져 있던 실패를 드러냄
- 발송 결과가 DB에는 있는데 조회 수단이 SQL뿐이라, 물량은 매번 사람이 쿼리를 짜고 실패는 고객 문의로 알던 상태였다. 13개월치를 직접 조사했다
- 성공 판정이 틀려 있었다 — 벤더가 거절한 141건도 상태가 COMPLETED 였다. 판정 기준을 상태값에서 응답 코드로 바꿨다. 상태 허용 목록에 빠져 있던 실패 242건도 13개월간 모든 집계에서 누락돼 있었고, 목록 밖 값을 세는 감시 쿼리를 짝으로 두는 규칙으로 정리
- 결과 콜백을 못 받아 미확정으로 남은 건이 435건, 가장 오래된 것은 442일 경과 상태였다
- 설계 판단 — 발송량 0을 실패율 0%로 그리지 않는다. SMS 물량의 47.4%가 오전 한 배치에 몰려 있어, 그 배치가 실패하면 발송 자체가 없어 실패율에는 아무것도 잡히지 않는다
2025. 02 ~ 2025. 07
통계 집계 및 KPI 리포트 서비스 개발
(주) 플라잉닥터 · 기획 제안 / 사업팀과 지표 정의 / 집계 설계·구현 주도- 맥락 — 병원은 직원·의사 성과를 수기로 관리했고, 재방문·이탈·피크타임은 감각으로만 판단하던 상황이었다
- 수기로 관리하던 진료 성과 지표를 집계 서비스로 대체
- 병원별·진료과목별 차이를 설정 값으로 모델링해 공통 집계 로직을 재사용
- EMR 직접 연동이 어려운 환경에서 Excel 데이터를 가져와 직원·의사별 지표를 산출하는 파이프라인 구현
- 통계 전용 데이터소스를 분리하고, 스트리밍 집계와 CompletableFuture 기반 병렬 처리 적용
- 설계 판단 — 진료 수·신규 환자 수·노쇼 수는 값이 고정되지만, 환자 수와 재방문 수는 사용자가 기준일을 바꾸므로 사전 집계가 불가능했다. 동적 항목까지 담을 수 있도록 진료 원본을 반영하는 집계 테이블을 따로 설계하고 조회 축에 맞춰 인덱스를 구성
- 제약 환경(JVM 1Core/1GB) 부하시험에서 동시 사용자 100명 기준 평균 응답 26.7초 → 2.73초, 처리량 3.33 → 35.9 TPS. 단일 호출 평균 응답은 0.76초 → 0.163초
- 같은 환경의 단계별 측정에서 일별 통계 집계 실패율 86% → 0% (개선 단위를 나눠 각 단계의 기여를 분리 측정). 통계 갱신 주기는 1일 1회 → 1시간으로 단축
2024 ~ 2025
홈페이지 이미지 편집 셀프서비스
(주) 플라잉닥터 · 기획·CS팀과 요구 정의 / 템플릿 구조 설계 / 편집기 구현- 맥락 — 병원이 홈페이지 이미지를 바꾸려면 CS팀이 매번 직접 만들어 전달해야 했고, 요청 한 건마다 며칠씩 기다리던 병목이 있었다
- 텍스트와 이미지를 분리한 템플릿 데이터 구조를 설계하고, 저장·조회 API 와 템플릿 버전 관리를 구현
- Fabric.js 기반 캔버스 편집기를 직접 구현. 렌더링과 편집 상태를 분리해 템플릿 추가를 지원하고, 변경된 객체만 다시 그리도록 구성
- 도입 후 이미지 제작 관련 CS 요청 주 10건 → 2~3건, 같은 기간 병원이 직접 하는 초기 셋업은 주 2~3건 → 5건
2024 ~ 2026
서비스 성능 및 안정성 개선
(주) 플라잉닥터 · 원인 분석·설계·구현 주도- 2024.11 · 리마인드 알림 조회 최적화
- EXPLAIN ANALYZE로 날짜 계산 조건의 풀 스캔을 확인하고, 범위 조건 전환으로 인덱스 Range Scan 복원
- 설계 판단 — 인덱스를 새로 추가하지 않고 카디널리티 분석으로 기존 복합 인덱스를 대체 설계해 쓰기 성능을 유지
- 리마인드 알림 조회 응답 시간을 약 5~6초에서 300~400ms로 단축
- 2025 · 조회 방식 셋을 부하테스트로 비교해 선택
- 맥락 — 통계의 인기 페이지 목록이 링크만 나열돼 어떤 페이지인지 알기 어려웠다. 각 링크의 대표 이미지를 썸네일로 붙이기로 하고, 가져오는 방식을 정해야 했다
- 요청 시점에 외부 페이지를 직접 파싱 / 테이블에 저장해 두고 조회 / 저장 + Redis 캐싱 셋을 같은 조건으로 측정하고, 캐시 자료구조도 String·Hash 로 나눠 비교
- 직접 파싱은 170건이 전량 타임아웃으로 성립하지 않았고, 저장 방식으로 바꾸자 실패가 사라졌다. Redis 를 더해 평균 633ms → 399ms. Hash 는 String 대비 이득이 없어 단순한 String 을 택함 (동시 사용자 10명 조건)
- 2026 · 푸시 발송 락 경합 대응
- 발송 배치가 콘텐츠 행을 잡은 채 커밋하지 않아 상세 조회가 lock wait timeout 으로 실패했다. 원인을 트랜잭션 길이가 아니라 완료 표시의 커밋 시점으로 좁혀 해당 갱신을 건별 커밋으로 분리
- 반복되던 락 경합 timeout 주 2~3회 → 0건 (발송 대상 평균 1천 명 규모)
- 2026 · 배치의 외부 호출을 worker 로 분리
- 맥락 — 일배치가 병원 도메인으로 HTTP 요청 약 440건을 순차로 보내 대표 이미지를 추출하면서 배치 스레드를 오래 점유했다. APM 으로 한 실행을 분해해 이 구간이 22초 이상임을 확인했다
- 외부 추출을 별도 worker 로 비동기 이관하고, 요청 원장 테이블과 결과 반영 잡을 분리한 2-잡 구조(enqueue / reconcile)로 재구성. 멱등 처리와 SSRF 가드를 넣어 재실행과 외부 호출을 안전하게 함
- 추출 92.5초 → 13.3초 (약 6.94배, stag APM 실측). 같은 기간 미배포 상태의 운영 환경은 149초대를 유지해 배포 효과임을 대조로 확인
- 2026 · 빨라진 원인을 벤치마크로 규명
- "Node 라서 빠르다" 는 해석이 맞는지 확인하기 위해 4변형(Java 동기·비동기 / Node 동기·비동기) × 페이지 수 × 동시성 6단계 × 5회, 420행을 측정. 추출 로직과 mock 서버를 고정해 동시성만 변수로 남김
- 언어는 변수가 아니었다 — Java 동기와 Node 동기가 0.6% 차이. 순차를 비동기로 바꾼 것만으로 90~105배였고, 같은 동시성 모델이면 언어가 달라도 결과가 같았다
- Node 비동기가 같은 처리량에서 CPU 를 약 37.7% 적게 썼다. worker 를 분리한 이유를 "언어" 가 아니라 동시성·격리·CPU 효율로 정리 (mock 서버 기준)
- 2026 · 실패로 잡히지 않는 누락을 관측 지표로
- 실패·폴백·stuck 카운트가 모두 0인데 적용률만 86~93% 에서 14~27% 로 떨어지는 구간을 발견. 요청 원장 11일치를 복원해 원인이 추출 결과가 빈 값으로 돌아오는 것임을 확인했고, 트리거는 worker 인스턴스 교체 시점과 일치했다
- 로그 보존 기간이 3.5일뿐이라 3일 창만 보면 평균 76% 로 건강해 보였다. 영구 보존되는 원장으로 소스를 바꿔 11일 평균 54% 를 드러냄
- 적용률(applied/processed)과 추출 실패율을 알람 지표로 설계. 대상 페이지 5,057건 중 4,825건에 썸네일이 적용된 상태(95.4%)
SIDE PROJECT
알림 발송 환경의 트랜잭션·락 경합 재현
- 운영에서 겪은 락 경합을 재현해, 트랜잭션 경계와 갱신 방식을 바꾸어 가며 처방별 차이를 비교한 개인 실험
- 알게 된 것 — 락을 잡는 문장의 커밋 시점이 문제였고, 처방 비교는 설계가 부족해 하나만 효과를 확인할 수 있었다. 무엇을 판정할 수 없는지까지 저장소에 적었다
- notification-reliability-lab · GitHub
Virtual Thread 적용 조건 측정
- 플랫폼 스레드와 가상 스레드를 조건 26개 × 3회로 비교하고, 원인을 확인하기 위해 JDK 와 JDBC 드라이버를 바꿔 가며 235회를 다시 측정
- 알게 된 것 — DB 를 거치지 않는 경로에서는 유리하지만 거치는 경로에서는 뒤집힌다. 원인은 드라이버 안의 pinning 이었고, 가설 4개 중 2개는 반증됐다
- virtual-thread-lab · GitHub
SKILL
Language
- Java
- TypeScript
Backend
- Spring Boot
- Spring Data JPA
- QueryDSL
- MyBatis
- Spring Batch
Database & Messaging
- MySQL
- RabbitMQ
Infrastructure & Ops
- Docker
- Kubernetes
- GitHub Actions
- Spring Boot Actuator
- Git
Frontend
- Vue.js 2·3
- TypeScript
- Fabric.js