Portfolio

송영훈

Backend Developer with Infrastructure & AI Automation Experience

  • 20개 이상 병의원에서 운영되는 의료 시스템의 API, DB, 배포 환경을 다루며 데이터 정합성과 운영 안정성을 개선해왔습니다.
  • 컨트롤러·DB 접근 계층 분리, Redis 캐싱, HTTP Keep-Alive, 트랜잭션 적용처럼 운영 중인 백엔드의 구조적 병목을 직접 개선했습니다.
  • Docker·Kubernetes·Cloudflare·n8n 기반 개인 홈서버와 자동화 환경을 운영하며, 백엔드 개발을 실제 운영 흐름까지 연결해왔습니다.
[email protected]010-9143-5718

About

Tech Stack

backend

Node.jsKoa.jsJava Spring BootFastAPIREST API

infra

DockerKubernetesNginxGitHub ActionsCloudflare

db

MSSQLOracleMariaDBRedis

frontend

React.jsTypeScriptReact QueryRedux Toolkit

automation

OpenClawn8nClaude CodeShell Script

Certifications

  • SQLD
  • CSTS FL
  • 정보처리기사 (결과 대기 중)

Education

금오공과대학교

신소재공학부 · 2021.02 졸업

Projects

백엔드 설계, 데이터 정합성, 운영 안정성 중심의 프로젝트입니다.

Professional Experience

회사에서 수행한 실무 백엔드·운영 프로젝트입니다.

백엔드리팩토링DB 설계Node.js

PACS API 서버 Repository 패턴 도입

21개 레거시 컨트롤러 리팩토링 · MSSQL/Oracle DB 접근 계층 분리

2025.10 ~ 2026.02 · 단독 수행

(주)메디칼스탠다드 · 실무 프로젝트 · 소스 비공개

Problem

PACS API 서버의 컨트롤러에는 비즈니스 로직, Sequelize ORM 쿼리, Oracle Raw SQL, DBMS 분기 로직이 한 파일 안에 섞여 있었습니다. 기능 수정 시 어느 쿼리가 어떤 DBMS에서 실행되는지 추적하기 어려웠고, DB 관련 버그를 고치려면 컨트롤러 전체를 건드려야 하는 구조였습니다.

💡

Key Insight

단순히 파일을 나누는 것이 아니라, MSSQL과 Oracle을 동시에 지원하는 DB 접근 규칙을 먼저 정해야 했습니다. 공개 메서드는 DBMS 차이를 숨기고, 내부 구현에서만 Sequelize와 Oracle Raw SQL을 분기하도록 설계했습니다.

Process

01 DB 접근 계층 분리 규칙 수립
  • 컨트롤러의 쿼리, DBMS 분기, 비즈니스 로직을 분리할 기준 정의
  • 공개 메서드는 findAll, findOne, create처럼 도메인 동작 중심으로 통일
  • 내부 구현에서 _findAllSequelize, _findAllOracle 형태로 DBMS별 쿼리 분리
02 점진적 컨트롤러 리팩토링
  • 기존 repository가 있으면 메서드를 추가하고, 없으면 신규 repository 생성
  • 컨트롤러는 DBMS 세부 구현을 모르게 하고 repository 호출만 남기는 방식으로 교체
  • 두 DBMS 모두 호환되는 단순 쿼리는 공통 raw query로 유지해 중복 제거
03 반복 작업 자동화 및 검증
  • 반복 리팩토링 패턴을 Claude Code 커스텀 스킬로 정리해 일관성 확보
  • 리팩토링 전후 기능 동작이 동일한지 블랙박스 테스트 보고서 기반으로 검증

Result

컨트롤러

21개

Repository 패턴 적용

DBMS

2종

MSSQL / Oracle 지원

수정 범위

축소

DB 버그 수정 위치 명확화

Node.jsSequelizeOracle Raw SQLMSSQLRepository Pattern
백엔드트랜잭션데이터 정합성DB

다중 테이블 쓰기 연산 트랜잭션 원자성 보장

부분 실패로 인한 데이터 정합성 오염 방지 · 7개 핸들러 개선

2025년 · 단독 수행

(주)메디칼스탠다드 · 실무 프로젝트 · 소스 비공개

Problem

PACS API 서버의 일부 핸들러는 하나의 요청에서 2개 이상의 테이블에 INSERT, UPDATE, DELETE를 수행했지만 트랜잭션이 없었습니다. 중간 단계에서 예외가 발생하면 앞선 테이블 변경만 남아 데이터 정합성이 깨질 수 있는 구조였습니다.

💡

Key Insight

기능이 정상 동작하는 것과 실패 시 데이터가 안전하게 되돌아가는 것은 별개의 문제였습니다. 다중 테이블 쓰기 작업은 성공 경로보다 실패 경로를 기준으로 설계해야 했습니다.

Process

01 위험 핸들러 식별
  • INSERT, UPDATE, DELETE가 여러 테이블에 걸쳐 수행되는 API 핸들러 전수 확인
  • 부분 실패 시 남을 수 있는 데이터 불일치 시나리오를 기준으로 우선순위 선정
02 Sequelize 트랜잭션 적용
  • 각 핸들러에서 동일 트랜잭션 객체를 모든 쓰기 연산에 전달
  • 모든 연산 성공 시 commit, 하나라도 실패하면 rollback하도록 변경
  • 예외 처리 경로에서 rollback 누락이 없도록 공통 패턴으로 정리
03 실패 경로 검증
  • 중간 연산 실패 시 앞선 테이블 변경이 남지 않는지 확인
  • 정상 경로와 실패 경로 모두에서 응답과 DB 상태를 검증

Result

핸들러

7개

트랜잭션 적용

부분 실패

rollback

정합성 오염 방지

쓰기 연산

원자화

다중 테이블 변경 보호

Node.jsSequelize TransactionMSSQLOracleData Consistency
백엔드데이터 설계성능 최적화Node.js

Windows·Web PACS 어노테이션 포맷 통합

이기종 렌더링 라이브러리 간 양방향 호환 · DB 스키마 통합

2025.01 ~ 2025.04 · 단독 수행

(주)메디칼스탠다드 · 실무 프로젝트 · 소스 비공개

Problem

Windows PACS(FODICOM)와 웹 PACS(Cornerstone.js)는 렌더링 라이브러리가 달라 어노테이션 데이터 포맷이 완전히 상이했습니다. Windows는 '13^|2,2|2,4|...' 형태의 구분자 문자열, 웹은 { handles: { x, y } } JSON 구조로 저장됩니다. 두 제품을 동시에 쓰는 병원에서 의료진이 한쪽에서 그린 측정·마킹이 반대쪽에서 보이지 않아 양쪽에서 중복 작업을 해야 했습니다. 두 DB 스키마는 별도로 운영 중이었고, 라이브러리 교체 없이 호환을 만드는 방법이 있는지 자체가 불분명한 상태였습니다.

💡

Key Insight

두 포맷을 직접 뜯어보니 — 표현 방식만 다를 뿐, 렌더링에 필요한 실질 데이터(좌표, 도형 종류)는 동일했습니다. 라이브러리를 교체하지 않고 변환 레이어만 추가하면 해결할 수 있었습니다.

Process

01 포맷 분석 및 변환 가능성 판단
  • Windows(FODICOM)와 Cornerstone.js 어노테이션 데이터 구조를 직접 분석
  • 좌표·도형 종류 등 핵심 데이터는 동일, 표현 방식만 다름을 확인
  • 렌더링 라이브러리 교체 없이 변환 레이어 추가로 해결 가능하다고 판단
02 양방향 변환기 구현 — 12종 어노테이션 타입
  • DBtoWEB: Windows 문자열 포맷 → Cornerstone JSON (DB → 웹 렌더링)
  • WEBtoDB: Cornerstone JSON → Windows 문자열 포맷 (웹 저장 → DB 기록)
  • Length, Angle, Rectangle, Circle, Polygon, FreehandRoi, CTR, CobbAngle 등 12종 지원
  • 일부 타입은 구버전·신버전 포맷 혼재 → dataBuffer 길이로 자동 분기 처리
03 N+1 쿼리 → 벌크 UPDATE로 성능 개선
  • 어노테이션 저장 시 이미지 수만큼 단건 UPDATE 순차 실행 → 응답 10초 이상 발생
  • PACS 특성상 검사 1건에 이미지 수천 장 → 쿼리 수가 응답 시간에 직결
  • CASE WHEN 기반 단일 벌크 UPDATE로 교체 → 즉시 응답으로 단축
04 DB 스키마 통합
  • Windows·웹이 각각 운영하던 별도 어노테이션 테이블을 단일 스키마로 통합
  • 데이터 일관성 확보 및 향후 유지보수 비용 절감

Result

저장 응답 시간

10초+ → 즉시

CASE WHEN 벌크 UPDATE

지원 타입

12종

양방향 변환 완전 지원

DB 스키마

2개 → 1개

데이터 일관성 확보

Architecture

▶ 개발 범위: 변환 레이어 + Web PACSWindows PACS기존 레거시 시스템FODICOM 라이브러리"13^|2,2|2,4|..."구분자 기반 문자열※ 코드 미수정DBMSSQL / OracleWindows 포맷으로 저장DBtoWEBDB → 웹 렌더링WEBtoDB웹 저장 → DB 기록변환 레이어convertAnnoData()12종 어노테이션 타입Length · Angle · Rectangle · CirclePolygon · FreehandRoi · CTR · CobbAngledataBuffer 길이 기반구버전 / 신버전 자동 분기렌더링 데이터저장 요청Web PACSCornerstone.js{ handles: {x, y} }JSON 구조
확대
Node.jsTypeScriptCornerstone.jsMSSQLOracle
인프라클라우드DockerKubernetes

오더연동서비스 클라우드 마이그레이션

네이버 클라우드 Kubernetes → IaaS · 1차 실패 후 재설계

2024년 말 · 단독 수행

(주)메디칼스탠다드 · 실무 프로젝트 · 소스 비공개

Problem

운영 중인 오더연동서비스를 NCP Kubernetes에서 회사 IaaS 서버로 이전해야 했습니다. 그런데 담당 개발자는 이미 퇴사한 상태였고, 소스코드 레포에는 버전별 폴더가 3개 있었는데 어느 것이 실제 운영 중인지 명세가 전혀 없었습니다. 테스트 명세 없음, 담당자 없음, 문서 없음 — 세 가지가 동시에 없는 상태에서 운영 중단 없이 이전을 완료해야 했습니다.

💡

Key Insight

1차 실패 당일 밤, NCP Kubernetes 대시보드에 직접 접근해 실제 운영 중인 컨테이너 이미지를 확인했더니 — 소스의 최신 버전이 아닌 구버전 이미지가 돌고 있었습니다. 소스는 누군가 최신화했지만 배포는 그대로 방치된 상태였습니다. 소스를 새로 빌드하는 것이 아니라, 운영 이미지를 그대로 이식하는 것이 정답이었습니다.

Process

1차 시도
  • 소스의 최신 폴더를 운영 버전으로 가정하고 Dockerfile 새로 작성
  • 테스트 도메인 검증 후 운영 도메인으로 전환
  • 전환 직후 특정 엔드포인트에서 405 Method Not Allowed 오류 다수 발생
  • 실제 업무 장애 확인 → 즉시 도메인 롤백, NCP로 복구
02 원인 분석 (금~일 주말 독학)
  • 1차 실패 직후 금요일 밤부터 일요일까지 NCP 환경 전체를 독자적으로 파악
  • Kubernetes 대시보드 접근법, Bastion 호스트 개념, VPC 내부 접근 방법 독학
  • Bastion SSH → VPC 내부 노드 SSH → docker save로 운영 이미지 .tar 추출 → scp로 로컬 PC 복사
  • 이미지 확인 결과 소스 최신 버전과 달리 구버전 이미지가 운영 중임을 발견
  • 소스 코드가 최신화되어 있어도 이미지 빌드·배포는 구버전으로 유지된 상태였음
03 2차 시도 — 전략 전환으로 성공
  • 소스 재빌드 대신 운영 중인 Docker 이미지를 그대로 pull하여 IaaS로 이식
  • 테스트 명세가 없으므로, 운영 nginx 로그를 반나절 수집해 실제 요청 엔드포인트 전수 파악
  • 수집한 엔드포인트로 직접 테스트 명세 작성 → 전체 검증 완료 후 운영 전환

Result

소요 기간

1주

1차 실패 → 재설계 → 성공

운영 상태

무중단

이전 이후 현재까지

테스트 명세

직접 도출

운영 로그 기반

Architecture

BEFORE — NCP Kubernetes외부 요청 (example1/2.com)NCP Load BalancerIngress ControllerNginx (rewrite)example1.com → /api/{ver} → :5195example2.com → /core/api/{ver} rewrite → :5195그 외 도메인·경로 → 차단 (4xx)Pod 1:5195Pod 2:5195Pod Nreplicas마이그레이션이미지 그대로AFTER — IaaS 서버외부 요청 (example1/2.com)Nginx Proxy ManagerLet's Encrypt 자동 인증서 · 관리 UINginx (동일 rewrite 규칙 유지)example1.com → /api/{ver} → :5195example2.com → /core/api/{ver} rewrite → :5195그 외 도메인·경로 → 차단 (4xx)Docker Container오더연동서비스 :5195운영 이미지 그대로 이식unless-stopped · 무중단 유지
확대
DockerKubernetesNCPNginxIaaSBastion
인프라DockerNginxCI/CD

IaaS 인프라 구축 · 운영 및 CI/CD 파이프라인

Nginx gzip 17s→3s · MariaDB 튜닝 · Self-hosted Runner 보안 설계

2024.10 ~ 재직 중 · 단독 수행

(주)메디칼스탠다드 · 실무 프로젝트 · 소스 비공개

Problem

별도 인프라 담당자 없이 개발과 서버 운영을 병행하는 환경이었습니다. 서비스 이전 직후 쿼리 조회 속도가 기존 대비 현저히 느려지는 문제가 발생했고, GitLab CI를 IaaS에 연결하려면 내부망 포트를 외부에 노출하는 포트포워딩이 필요한 상황이었습니다. 간편한 방법이 있었지만 보안 문제가 명확했고, 그 판단과 대안 설득까지 직접 해야 했습니다.

💡

Key Insight

MariaDB는 설치 초기값(buffer pool 128MB) 그대로 운영 중이었습니다 — 설정 한 줄로 해결 가능한 문제였습니다. CI/CD는 GitLab 포트포워딩 대신 GitHub 조직 계정을 새로 개설해 git history 보존한 채 이전하는 방식으로 보안을 유지했습니다.

Process

01 MariaDB 성능 튜닝
  • 서비스 이전 후 쿼리 응답이 급격히 느려짐 → 설정 파일 직접 분석
  • innodb_buffer_pool_size가 기본값 128MB로 유지 중임을 확인
  • buffer pool 128MB → 4GB, max_connections 151 → 1000으로 조정 → 성능 정상화
02 Nginx gzip 압축으로 응답 시간 단축
  • 데이터 양이 많은 GET 요청에서 응답 시간 17초 이상 확인
  • Nginx gzip 압축 설정 적용 → 응답 시간 17초 → 3초로 단축
03 CI/CD 파이프라인 — 보안 우선 설계
  • 기존 사내 GitLab 레포를 Self-hosted Runner와 연결하려면 IaaS → GitLab 포트포워딩 필요
  • 외부 노출이 보안상 문제라고 판단 → 포트포워딩 방식 거부
  • GitHub 조직 계정 개설을 설득 → git history 보존한 채 GitLab에서 GitHub으로 이전
  • Self-hosted Runner 연결 → release 브랜치 push 시 Docker 빌드 + 자동 배포 파이프라인 구성

Result

응답 시간

17s → 3s

Nginx gzip 압축

DB 버퍼풀

128MB → 4GB

32배 확장

배포

push → 자동

Self-hosted Runner CI/CD

Architecture

GitHubrelease 브랜치 pushGitHub ActionsSelf-hosted RunnerDocker Buildimage buildcompose up -dDocker Host (IaaS)Nginx Proxy Manager도메인별 리버스 프록시 · Let's Encrypt오더연동서비스컨테이너mc3컨테이너의료AI 커뮤니티컨테이너성능 튜닝Nginx gzip 압축17s → 3s대용량 GET 응답MariaDB 튜닝buffer 128MB → 4GBmax_connections 151 → 1000초기값 방치 → 설정 한 줄로 정상화CI/CD 보안 설계GitLab 포트포워딩 거부GitHub 조직 이전 → Self-hosted Runnergit history 보존
확대
DockerNginxNginx Proxy ManagerMariaDBGitHub ActionsSelf-hosted Runner
백엔드Redis캐싱성능 최적화

PACS DICOM 로딩 Redis 캐싱 최적화

동일 스토리지 설정 중복 조회 제거 · Cache-Aside 패턴 적용

2025년 · 단독 수행

(주)메디칼스탠다드 · 실무 프로젝트 · 소스 비공개

Problem

PACS 웹 뷰어는 CT/MRI 검사 로딩 시 수백~수천 장의 DICOM 슬라이스 이미지를 병렬로 요청합니다. 기존 백엔드는 각 이미지 파일을 읽을 때마다 스토리지 장비 정보(ADVSTORAGEMGRTAB)를 DB에서 조회했고, CT 3,000장 기준 동일한 SELECT 쿼리가 3,000번 반복 실행되는 구조였습니다. 이로 인해 대용량 검사 조회 시 DB 커넥션 풀 고갈과 대기 시간 누적 위험이 있었습니다.

💡

Key Insight

CT/MRI 한 검사에 포함된 모든 슬라이스 파일은 동일한 storageId에 저장됩니다. 즉, 문제의 본질은 쿼리 속도가 아니라 '같은 결과를 수천 번 조회하는 접근 패턴'이었습니다. 변경 빈도가 낮은 스토리지 설정 정보를 Redis에 캐싱하면 DB 부하를 구조적으로 줄일 수 있었습니다.

Process

01 중복 조회 원인 분석
  • DICOM 이미지 요청 흐름을 추적해 이미지마다 스토리지 설정 조회 쿼리가 반복 실행됨을 확인
  • 검사 단위의 모든 슬라이스가 동일 storageId를 사용한다는 도메인 특성 확인
  • DB 인덱싱보다 애플리케이션 레벨 캐싱이 근본 해결책이라고 판단
02 Redis Cache-Aside 패턴 구현
  • 캐시 키를 storage:mgr:{storageId} 형식으로 설계
  • 최초 요청 Cache Miss 시 DB에서 1회 조회 후 Redis에 저장
  • 이후 동일 storageId 요청은 Redis Cache Hit로 처리해 DB 접근 제거
  • TTL 10분을 설정해 스토리지 설정 변경 시에도 일정 시간 내 자동 갱신되도록 구성
03 운영 안정성 고려
  • Redis read/write 구간을 try-catch로 격리해 캐시 장애가 서비스 장애로 전파되지 않도록 처리
  • Redis 장애 시 DB 직접 조회로 폴백하도록 설계
  • 긴급 인프라 변경 시 특정 캐시 키를 수동 삭제할 수 있는 운영 방안 정리

Result

DB 조회

3000 → 1회

동일 스토리지 기준

쿼리 감소

99.97%

중복 SELECT 제거

장애 대응

Fallback

Redis 실패 시 DB 조회

Node.jsKoa.jsRedisMSSQLOracleDICOM
백엔드Node.js스트리밍아키텍처

DICOM 이미지 다운로드 — Non-blocking 스트리밍 구현

zip 스트리밍 전환 · Python FastAPI CPU 부하 분산

2025년 · 단독 수행

(주)메디칼스탠다드 · 실무 프로젝트 · 소스 비공개

Problem

의료진이 검사 1건에 포함된 모든 DICOM 이미지(.dcm)를 한 번에 다운로드하는 기능이 필요했습니다. 초기 구현은 서버가 zip을 모두 만든 뒤 blob으로 응답하는 방식이었는데, 검사 1건에 이미지가 수백~수천 장에 달하다 보니 서버 처리 시간 동안 UI가 완전히 블로킹되는 문제가 있었습니다. 의료진이 다운로드 버튼을 누른 뒤 다른 조작을 전혀 할 수 없는 상태였습니다.

💡

Key Insight

응답을 '다 만들고 나서 한 번에 보내는 것'이 아니라, '만들어지는 즉시 청크로 흘려보내는 것'으로 전환하면 됩니다. Node.js의 stream.PassThrough와 archiver를 pipe로 연결하면, zip이 완성되기 전에도 브라우저가 청크를 받기 시작합니다.

Process

1차 구현 — Blob 응답 방식
  • Axios로 요청 → 서버에서 zip 완성 후 blob으로 전체 응답
  • 파일 수가 많을수록 서버 처리 시간 증가 → UI 블로킹
  • 의료진이 다운로드 중 뷰어 조작 불가 → 사용성 문제 확인
02 zip 스트리밍으로 Non-blocking 전환
  • Node.js stream.PassThrough로 스트림 객체 생성
  • archiver(zip)를 PassThrough 스트림에 pipe로 연결
  • 파일이 추가되는 즉시 청크 단위로 클라이언트에 전송
  • 프론트엔드에서 Axios blob 방식 → <a> 태그 click() 방식으로 전환, 브라우저 기본 다운로드 엔진에 위임
03 DICOM → JPG 변환 부하 분산
  • Node.js에서 이미지 변환까지 처리할 경우 CPU 독점 우려
  • 이미지 변환 전용 Python FastAPI 서버를 별도로 구성해 변환 요청 위임
  • Node.js 메인 서버는 스트리밍과 라우팅에만 집중하는 구조 확립

Result

UX

Non-blocking

다운로드 중 뷰어 조작 가능

서버 부하

CPU 분산

변환 서버 분리

Architecture

브라우저<a>.click()다운로드 위임requeststream chunksNode.js APIstream.PassThroughpipe → archiver(zip)Non-blocking파일 준비 중 UI 조작 가능read files파일시스템.dcm 이미지JPG 변환 위임Python FastAPI이미지 변환 서버CPU 부하 분리Node.js 메인 서버 CPU 부하 분산
확대
Node.jsstream.PassThrougharchiverPythonFastAPI
백엔드성능 최적화HTTPNode.js

PACS Koa ↔ FastAPI 통신 Keep-Alive 성능 튜닝

대량 DICOM 변환 요청의 Port Exhaustion 방지 · TIME_WAIT 소켓 안정화

2025년 · 단독 수행

(주)메디칼스탠다드 · 실무 프로젝트 · 소스 비공개

Problem

PACS 백엔드는 Node.js(Koa)가 요청을 받고, DICOM 디코딩·JPG 변환 같은 무거운 이미지 연산은 Python FastAPI 서버에 위임하는 구조였습니다. 평소 수십 장 이미지는 문제가 없었지만, 1,000~3,000장 규모의 검사 로딩이나 일괄 다운로드에서는 처음엔 빠르다가 후반부로 갈수록 응답이 느려지고 EADDRINUSE 또는 소켓 연결 실패가 간헐적으로 발생했습니다.

💡

Key Insight

CPU 병목처럼 보였지만 실제 원인은 이미지마다 Axios가 새 TCP 연결을 생성·종료하는 통신 방식이었습니다. 로컬 통신이라도 수천 번 반복되면 TCP Handshake와 TIME_WAIT 소켓 누적으로 Port Exhaustion 위험이 생기므로, Node.js http.Agent 기반 Keep-Alive로 연결을 재사용해야 했습니다.

Process

01 병목 원인 재진단
  • Python 이미지 변환 서버의 CPU 사용량은 여유가 있는데 다운로드 후반부 지연이 커지는 현상 확인
  • Node.js ↔ FastAPI 사이에서 이미지 1장마다 TCP 연결 생성·종료가 반복되는 구조 파악
  • ss 명령으로 TIME_WAIT 소켓이 대량 누적되는 네트워크 레벨 지표 확인
02 HTTP Keep-Alive 적용
  • Node.js 전역에 http.Agent({ keepAlive: true, maxSockets: 100 }) 구성
  • DICOM 디코딩 API 호출부와 일괄 다운로드 JPG 변환 호출부의 Axios 옵션에 httpAgent 주입
  • FastAPI(Uvicorn)의 Keep-Alive 지원을 활용해 기존 연결을 재사용하도록 변경
03 운영 검증
  • 대량 이미지 다운로드 중 TIME_WAIT 소켓 수를 watch + ss로 실시간 추적
  • 소켓 수가 이미지 장수만큼 증가하지 않고 제한된 풀 안에서 관리되는지 확인
  • DICOM 다운로드 스트리밍 구조와 Python 변환 서버 분리 구조의 후속 병목을 제거

Result

TIME_WAIT

3000개 → 한 자릿수

연결 재사용으로 안정화

처리 속도

약 3배

체감 성능 향상

장애 위험

완화

EADDRINUSE / 포트 고갈 방지

Node.jsKoa.jsAxioshttp.AgentHTTP Keep-AlivePythonFastAPIDICOM
서버 상태APIReact Query데이터 정확성

워크리스트 서버 상태 분리 및 정렬 정확성 개선

Redux 비동기 상태 제거 · React Query 전환 · 서버사이드 정렬 API 적용

2025.07 ~ 2025.08 · 단독 수행

(주)메디칼스탠다드 · 실무 프로젝트 · 소스 비공개

Problem

PACS 워크리스트 화면은 서버에서 받아오는 검사 목록 데이터를 Redux Toolkit의 createAsyncThunk와 reducer로 직접 관리하고 있었습니다. fetch, loading, error, page 상태를 모두 수동으로 다뤄야 해 보일러플레이트가 많았고, 컬럼 정렬도 현재 화면에 로드된 일부 데이터만 클라이언트에서 정렬해 전체 데이터셋 기준 결과와 달라지는 문제가 있었습니다.

💡

Key Insight

서버에서 오는 데이터는 클라이언트 전역 상태가 아니라 서버 상태로 다뤄야 했습니다. 또한 정렬은 UI 편의 기능이 아니라 데이터 정확성 문제였기 때문에, 화면에 있는 50건이 아니라 전체 데이터셋을 기준으로 백엔드에서 처리해야 했습니다.

Process

01 Redux 비동기 상태 제거
  • 워크리스트 조회 로직을 useInfiniteQuery 기반 커스텀 훅으로 분리
  • fetchWorklist, fetchMoreWorklist, loading, hasMore 등 비동기 상태용 Redux 액션·리듀서 제거
  • 검색 필터는 Redux에 유지하고, 필터 객체를 queryKey에 포함해 조건 변경 시 자동 재조회되도록 설계
02 무한스크롤과 캐시 전략 적용
  • Intersection Observer로 테이블 하단 감지 시 다음 페이지 자동 요청
  • 뷰어 이동 후 복귀 시 캐시 데이터와 스크롤 위치를 활용해 탐색 흐름 유지
  • 검색 결과는 최신성을 우선해 staleTime을 짧게 두고, 복귀 UX를 위해 gcTime을 설정
03 서버사이드 정렬 API 적용
  • 컬럼 헤더 클릭 시 sortBy, sortOrder를 쿼리 키에 포함해 1페이지부터 재조회
  • 백엔드 워크리스트 API에 정렬 파라미터를 추가하고 쿼리 레이어에서 정렬 수행
  • 부분 데이터 클라이언트 정렬에서 전체 데이터셋 기준 정렬로 전환

Result

Redux 제거

4개 thunk

관련 액션·리듀서 정리

정렬 기준

전체 데이터

서버사이드 정렬

UX

무한스크롤

More 버튼 제거

ReactTypeScriptReact QueryRedux ToolkitREST API
인프라Docker배포멀티플랫폼

PACS 뷰어 Linux Docker 배포 체계 구축

Windows Electron 전용 구조에서 Linux 서버 Docker 배포 추가 · 해외 사이트 배포

2026.04 · 단독 수행

(주)메디칼스탠다드 · 실무 프로젝트 · 소스 비공개

Problem

PACS 뷰어는 Windows Server와 Electron GUI 앱을 전제로 배포되던 제품이었습니다. 해외 사이트에서 Linux 서버 배포 요구가 들어왔지만, 기존 구조는 Electron 실행 방식과 Windows 환경에 강하게 묶여 있었고 DICOM 처리 바이너리 의존성까지 함께 고려해야 했습니다.

💡

Key Insight

기존 Windows 배포를 깨지 않고 Linux 배포 경로만 추가해야 했습니다. Alpine의 가벼움보다 glibc 호환성이 더 중요했기 때문에 Ubuntu 24.04 기반 멀티스테이지 Dockerfile로 방향을 잡았습니다.

Process

01 Linux 실행 환경 분석
  • DICOM 이미지 처리용 dcm-worker 바이너리가 glibc에 의존한다는 점 확인
  • Alpine(musl libc) 대신 Ubuntu 24.04를 프로덕션 베이스 이미지로 선택
  • Oracle Instant Client와 libaio.so.1 심볼릭 링크 등 런타임 의존성 정리
02 Electron / Node 실행 분기 유지
  • Electron 실행 시 GUI 대시보드, Node 직접 실행 시 서버 기동이라는 기존 구조 보존
  • process.argv 기반 실행 환경 분기로 Docker(node) 환경에서도 서버가 정상 기동되도록 처리
  • Windows Electron 배포와 Linux Docker 배포가 단일 코드베이스를 공유하도록 구성
03 운영 배포 패키징
  • Redis 포함 Docker Compose 구성 및 healthcheck 의존성 설정
  • 로그 볼륨, .env 환경변수, SSL 인증서 경로를 분리해 현장 배포 가능하게 정리
  • 서비스팀 엔지니어가 매뉴얼만으로 설치할 수 있도록 배포 문서 작성

Result

배포 환경

Windows + Linux

단일 소스 멀티플랫폼

해외 사이트

배포 완료

Linux 서버 운영

설치 방식

매뉴얼화

서비스팀 독립 설치

DockerDocker ComposeUbuntu 24.04Node.jsOracle Instant ClientRedisElectron
백엔드Spring Boot보안설계

mc3 고객 사이트·제품 배포 관리 WAS 개발

Spring Boot 기반 사내 REST API · Redis 세션 인증 · 배포 상태 추적

2024.10 ~ 진행 중 · 단독 설계 및 개발

(주)메디칼스탠다드 · 실무 프로젝트 · 소스 비공개

Problem

20개 이상 병의원 고객사의 기본 정보, 설치 제품, 배포 상태, 지원 이력을 체계적으로 관리하는 사내 시스템이 없었습니다. 담당자별 스프레드시트와 개인 기록에 의존해 고객·제품·배포 이력이 분산되는 상황이었습니다.

💡

Key Insight

단순 CRUD가 아니라 고객, 제품, 직원, 배포 단위가 서로 연결되는 운영 도메인을 먼저 모델링해야 했습니다. 권한 변경이 즉시 반영되는 세션 관리와 공통 응답·예외 처리까지 초기에 표준화했습니다.

Process

01 도메인 모델링
  • CustomMaster, PlayerMaster, ProductMaster를 중심으로 9개 엔티티 설계
  • 고객별 제품 배포 상태와 제품 배포 파일·공지·직원 로그까지 운영 단위로 모델링
  • 현장 설치 단위별 버전과 배포 상태를 추적할 수 있도록 복합 구조 정리
02 인증·권한 구조 구현
  • Spring Security + PlayerMaster 기반 CustomUserDetailsService 구현
  • level과 rights 필드로 관리자 및 복수 권한을 동적으로 부여
  • Spring Session Data Redis와 SessionService로 권한 변경 시 특정 직원 세션 강제 만료 구현
03 API·배포 기반 구성
  • ApiResponse, ErrorCode, BusinessException, GlobalExceptionHandler로 응답·예외 형식 통일
  • 고객 검색, 지원 이력, 설치 제품, 직원 CRUD, 비밀번호 변경 API 구현
  • Dockerfile과 GitHub Actions Self-hosted Runner로 release 브랜치 push 시 자동 배포 구성

Result

도메인

9개 엔티티

고객·제품·배포 관리

인증

Redis Session

세션 강제 만료

배포

push → 자동

CI/CD 연동

Java 17Spring BootSpring SecuritySpring SessionRedisMariaDBDockerGitHub Actions
백엔드운영 개선로깅장애 대응

PACS 서버 로깅 시스템 재설계

장애 원인 추적 가능한 Winston Logger 체계 구축

2024.11 · 단독 수행

(주)메디칼스탠다드 · 실무 프로젝트 · 소스 비공개

Problem

현장 배포 후 일부 API가 동작하지 않는 문제가 발생했지만, 기존 로그는 Unix 마이크로초 타임스탬프, HTTP 요청, DB 쿼리, 에러가 한 파일에 섞여 있었습니다. 에러 스택도 남지 않아 로그만으로 장애 원인을 추적하기 어려운 상태였습니다.

💡

Key Insight

장애를 한 번 해결하는 것보다, 다음 장애를 로그만 보고 좁힐 수 있는 구조를 만드는 것이 더 중요했습니다. 보안 사이트처럼 원격 접속이 제한되는 환경에서는 로컬 로그의 품질이 곧 대응 속도였습니다.

Process

01 로그 문제 재정의
  • HTTP 요청, DB 쿼리, 에러 로그가 섞여 원인 추적이 어려운 구조 확인
  • 에러 스택 미출력, 읽기 어려운 타임스탬프, 콘솔 색상 코드 오염 문제를 함께 식별
02 Winston 기반 로거 재작성
  • 타임스탬프를 YYYY-MM-DD HH:mm:ss 형식으로 변경
  • 전체 로그, 에러 로그, uncaughtException 로그를 파일 단위로 분리
  • 에러 발생 시 stack trace를 자동 포함하고, 30일 보관 및 gzip 압축 정책 적용
03 운영 장애 분석에 활용
  • 동일 현장 재방문 시 에러 스택에서 Oracle 스키마 구버전 설치 문제를 즉시 특정
  • JWT 토큰 재발급 API가 중복 호출되는 패턴을 HTTP 로그에서 발견해 프론트 인터셉터 버그를 선제 수정

Result

로그 분리

3종

전체 / 에러 / 예외

보관 정책

30일

gzip 압축 아카이브

장애 분석

로그 기반

원격 대응 가능성 향상

Node.jsWinstonMorganwinston-daily-rotate-fileOracle

Personal Backend Projects

GitHub에 공개 가능한 개인 백엔드 프로젝트입니다.

자동화LLMn8nSelf-hosted

LLM 대화 자동 위키 인제스트 파이프라인

n8n Form → Gemini 요약 → SSH 저장 → Claude Code 인제스트 자동화

2026.05 · 단독 설계 및 구축

개인 프로젝트

Problem

LLM과 나눈 대화에서 커리어·기술 회고 소재가 계속 쌓였지만, 원문 저장과 요약, 위키 문서화 과정을 매번 수동으로 처리해야 했습니다. 개인 지식관리 흐름에 자동 인제스트 파이프라인이 필요했습니다.

💡

Key Insight

브라우저나 n8n Code 노드 안에서 모든 일을 처리하려 하기보다, 각 계층의 보안 제약을 인정하고 Form Trigger, Gemini API, SSH, Claude Code CLI를 역할별로 분리하는 편이 안정적이었습니다.

Process

01 파이프라인 설계
  • n8n Form Trigger로 대화 내용을 입력받고 Gemini 2.5 Flash로 요약
  • SSH 노드로 호스트 파일시스템의 raw/ 디렉터리에 타임스탬프 기반 원문 저장
  • Claude Code CLI 비대화형 모드로 raw 문서를 wiki 문서로 인제스트
02 운영 환경 제약 해결
  • 브라우저 CSP 차단으로 API 직접 호출 대신 n8n 서버사이드 처리로 전환
  • Cloudflare Access의 Webhook 차단을 Form Trigger + /form/* bypass로 해결
  • OrbStack 컨테이너의 호스트 접근 DNS 문제를 host.docker.internal 사용으로 정리
03 입력 안정화
  • 줄바꿈·특수문자 포함 대화 내용을 JSON.stringify로 직렬화해 파싱 오류 방지
  • 비대화형 SSH PATH 문제를 CLI 절대 경로 명시로 해결
  • 한글 파일명 인코딩 문제를 타임스탬프 기반 파일명으로 회피

Result

흐름

End-to-end

입력→요약→저장→위키

장애 계층

6종 해결

CSP·Access·DNS·PATH 등

운영

Self-hosted

n8n + SSH

n8nGemini 2.5 FlashClaude Code CLISSHCloudflare AccessOrbStackKubernetes
홈서버AI 에이전트Kubernetes자동화

개인 홈서버 기반 OpenClaw AI 에이전트 운영 환경

Mac mini · Kubernetes · Docker · Discord/Telegram 연동형 개인 자동화 환경

2026.05 ~ 운영 중 · 개인 운영 환경 구축 및 유지보수

개인 프로젝트

Problem

개인 학습, 커리어 문서 정리, 일정·작업 자동화를 지속적으로 실험하기 위해 단발성 API 호출이 아니라 실제 생활과 개발 흐름에 붙는 로컬 AI 에이전트 운영 환경이 필요했습니다.

💡

Key Insight

AI 도구는 채팅창 안에만 있으면 생산성이 제한됩니다. 파일 시스템, 일정, 메시징, 백그라운드 작업, Kubernetes 서비스 운영과 연결해야 실제 개인 워크플로우가 됩니다.

Process

01 OpenClaw 운영 환경 구성
  • workspace 기반 AGENTS.md, SOUL.md, USER.md, MEMORY.md로 장기 컨텍스트와 운영 규칙 관리
  • Discord/Telegram 대화, 로컬 파일 작업, 세션 관리, 백그라운드 작업 실행 흐름 통합
  • cron과 heartbeat 개념을 활용해 일회성 대화가 아닌 지속 운영형 에이전트로 구성
02 홈서버·컨테이너 운영 실습
  • Mac mini 홈서버에서 Docker/OrbStack/Kubernetes 기반 서비스 운영
  • kubectl로 리소스 조회, 상태 확인, 서비스 운영 흐름 실습
  • cloudflared, ingress-nginx, n8n, Nextcloud 등 Self-hosted 서비스 운영 흐름 확인
03 개인 작업 자동화 연결
  • my-career와 Younghun-Wiki를 연동해 커리어 문서 업데이트 자동화에 활용
  • 대화 기반 지시 → 파일 조회/수정 → 빌드 검증까지 이어지는 개인 작업 흐름 운영
  • 단순 설치에서 끝내지 않고 실제 문서화·일정·작업 위임에 반복 사용

Result

운영 방식

상시 홈서버

Mac mini 기반

자동화

파일·일정·세션

대화형 작업 흐름

Self-hosted

실습/운영

K8s·Docker 경험

OpenClawKubernetesDockerkubectlcloudflaredingress-nginxn8nShell Script

Earlier Projects

부트캠프 및 초기 학습 프로젝트입니다.

백엔드보안JWT팀장

OnStayHouse 인증 구조 개선

IDOR 취약점 발견 · XSS 대응 · HttpOnly Refresh Token · Silent Refresh

2023.11 ~ 2023.12 · 팀장 / 인증 담당

메가스터디 더조은컴퓨터학원

Problem

숙소 예약 플랫폼 클론 프로젝트 초기 인증 구조는 JWT와 사용자 정보를 localStorage에 저장하고, 요청 userId를 클라이언트가 직접 보내는 방식이었습니다. userId 조작으로 타인 정보가 조회되는 IDOR 취약점과 XSS 시 토큰 탈취 위험이 있었습니다.

💡

Key Insight

동작하는 로그인보다 공격 시나리오에서 안전한 인증 구조가 중요했습니다. 서버 검증, 인메모리 Access Token, HttpOnly Refresh Token, Silent Refresh를 단계적으로 적용해 보안과 UX의 균형을 맞췄습니다.

Process

01 취약점 직접 발견
  • localStorage의 userId를 다른 사용자 ID로 바꿔 타인 정보가 조회되는 IDOR 문제 확인
  • 서버에서 JWT와 요청 userId 일치 여부를 검증하도록 수정
  • DB에 사용자와 JWT 매핑을 저장해 요청 주체 검증 강화
02 토큰 저장 구조 재설계
  • XSS 공격 시 localStorage 토큰 탈취 가능성을 인식하고 Access Token을 Context API 인메모리로 이전
  • Refresh Token은 HttpOnly Cookie로 관리해 JavaScript 접근 차단
  • 새로고침 시 로그인 유지 문제를 Silent Refresh로 해결
03 RTR 및 환경변수 적용
  • Refresh Token Rotation으로 재발급 시 기존 토큰을 폐기해 탈취 토큰 재사용 차단
  • JWT Secret 노출 위험을 인식하고 dotenv 기반 환경변수 관리 도입
  • 마이페이지·관리자 페이지·아이디/비밀번호 찾기 기능 구현

Result

취약점

IDOR/XSS

직접 발견·개선

토큰

AT/RT 분리

HttpOnly + 메모리

UX

Silent Refresh

자동 재발급

Architecture

로그인POST /authServerAT 발급 (5분)RT 발급 (7일)DB 저장Access TokenRefresh TokenContext (인메모리)XSS 차단 · 스크립트 접근 불가HttpOnly CookieJS 접근 불가 · Secure 플래그Silent RefreshAT 만료 → 자동 재발급사용자 개입 없음RTR재발급 시 기존 RT 폐기탈취 토큰 재사용 차단IDOR 직접 발견XSS 대응
확대
ReactNode.jsExpress.jsMySQLJWTHttpOnly CookieAxios Interceptor

Career

2024.10 ~ 재직 중 · 정규직

(주)메디칼스탠다드

풀스택 개발 / 인프라 운영

2024.05 ~ 2024.10 · 파견직

TTA 한국정보통신기술협회 (파견)

GS인증 소프트웨어 테스팅

2022.07 ~ 2023.04 · 정규직

(주)스튜디오타이탄

게임 시스템 기획자