김수민 포트폴리오

Projects

똑똑 — 상명대학교 동아리 리크루팅 서비스

2025.05 – 진행 중

엑셀로 흩어져 관리되던 동아리 홍보·지원·부원 선발을 하나로 통합한 서비스(FE 2·BE 2·디자이너 1). 관리자 API, DB 설계, CI/CD와 운영 인프라 담당

Java 17Spring Boot 3.5JPAQueryDSLPostgreSQLRedisFlywayDocker ComposeNginxk6PrometheusGrafana
90.14% → 0%조회수 동시성 유실률
1.93배처리량 (311.7 → 601.4 RPS)
12,995 / 0무중단 전환 구간 요청 / 비200 응답

GoLe — 브릭 중고거래 플랫폼

2026.08 – 진행 중

레고 시세·직거래·컬렉션 플랫폼(gole.co.kr). 홍보 자동화 LLM 에이전트와 백엔드 기능 개발

Java 21Spring Boot 4MongoDBRedisPythonLLM AgentGitHub ActionsNext.js
기록된 사례가 아직 없습니다
← 전체 프로젝트
똑똑 · 2026.07 · PR #347

동아리 조회수 동시성 유실 해결과 처리 방식별 트레이드오프 실측

동시 요청에서 조회수 증가분의 90.14%가 사라지던 문제를, 6가지 처리 방식을 같은 조건으로 실측 비교해 해결했다. 고른 방식은 유실 0%에 처리량까지 1.93배가 됐고, 같은 원자적 UPDATE라도 문장 위치만으로 1.84배 차이가 나는 원인을 PostgreSQL 락 보유 기간에서 찾았다.

90.14% → 0%조회수 동시성 유실률
1.93배처리량 (311.7 → 601.4 RPS)
1.84배같은 UPDATE를 트랜잭션 끝으로 옮긴 것만으로 생긴 처리량 차이

문제

동아리 상세 조회 시 조회수 증가가 read-modify-write(findById → viewCount += 1)라, 동시 요청에서 증가분이 유실됐다. 100 VU 부하에서 유실률이 90.14%였다.

원인

고민한 방법

측정 조건(로컬): 100 VU / 5,000 요청, 워밍업 2,000 요청 후 측정, 각 변형 1회.

처리량 (RPS, 높을수록 좋음) · 100 VU / 5,000 요청, 워밍업 2,000 후, 로컬
현행 read-modify-write 유실 90.14%
311.7
원자적 UPDATE · 증가 먼저
327.3
원자적 UPDATE · 증가 마지막
601.4
원자적 UPDATE · 트랜잭션 미사용
319.4
비관락 FOR UPDATE
245.9
낙관락 @Version 실패 67.36%
150.8
변형 유실 실패율 RPS p50 p95 p99
현행 (read-modify-write) 90.14% 0% 311.7 300.1 481.5 622.4
원자적 UPDATE — 증가 먼저 0% 0% 327.3 296.3 371.4 406.8
원자적 UPDATE — 증가 마지막 (채택) 0% 0% 601.4 157.5 244.8 325.0
원자적 UPDATE — 트랜잭션 미사용 0% 0% 319.4 299.2 472.5 586.0
낙관락 (@Version, 3회 재시도) 0% 67.36% 150.8 699.6 1011.5 1205.1
비관락 (SELECT … FOR UPDATE) 0% 0% 245.9 401.1 480.7 503.6
방법 장점 단점 판단
현행 read-modify-write 코드가 단순하다 동시 요청에서 갱신이 유실된다(90.14%) 버림 — 문제의 원인
원자적 UPDATE, 증가 먼저 유실 0%, 스키마 변경 없음 행 락이 뒤따르는 조회 2개 동안 유지돼 처리량이 거의 그대로(327.3 RPS) 버림
원자적 UPDATE, 증가 마지막 유실 0%, 락 보유 시간이 가장 짧아 처리량 1.93배 문장 순서가 성능을 좌우하는 암묵적 계약이 된다 채택 — 순서를 InOrder 테스트로 고정
원자적 UPDATE, 트랜잭션 미사용 유실 0% 처리량 319.4 RPS로 이득이 없다 버림
낙관락(@Version, 3회 재시도) 락을 잡지 않는다 충돌률 100% 워크로드라 5,000 요청 중 1,632건만 성공, 재시도 트랜잭션 16,297개 낭비. @Version 컬럼 필요 버림
비관락(SELECT … FOR UPDATE) 유실 0%, "읽고 판단해서 쓰기"에도 쓸 수 있다 처리량 245.9 RPS로 현행(311.7)보다도 느리다 버림 — 지원 정원 체크(maxApplyCount)처럼 판단이 필요한 곳에 맞다고 정리

결과

근거

← 전체 프로젝트
똑똑 · 2026.08 · PR #378

AWS Elastic Beanstalk → 자체 서버 이관과 블루-그린 무중단 배포 컷오버

EB·RDS·ElastiCache·S3로 나뉜 운영 환경을 자체 서버 한 대의 docker-compose 스택으로 옮기고 블루-그린 배포를 구축했다. 전환 구간 12,995 요청 중 비200 0건. 검증 중 "배포는 성공인데 전 요청 502"가 나던 결함의 원인을 inode 수준까지 추적해 막았다.

12,995 / 0무중단 전환 구간 요청 / 비200 응답
151 / 151MinIO 오브젝트 HTTPS 200
AWS 의존 0EB·RDS·S3·SES 전부 제거

문제

운영 환경이 EB(앱) + RDS(PostgreSQL) + ElastiCache(Redis) + S3/CloudFront(파일)로 나뉘어 있었고, 이를 자체 우분투 서버 한 대의 docker-compose 스택으로 통합해야 했다. main의 배포 워크플로우는 EB 기준(runs-on: ubuntu-latest, beanstalk-deploy)이었다.

이관 검증 중에는 "배포는 성공인데 서비스가 죽는" 결함이 나왔다 (#376). 블루-그린 전환 구간은 무결했지만, 구 색 컨테이너를 정리하는 순간부터 1180건 연속 502가 났고 CI는 초록이었다.

이관을 결정한 동기(비용 등)는 원문에 없음.

원인

검증 중 발견한 결함 3건의 원인 (이슈 원문 기준):

고민한 방법

정량 비교는 원문에 없음. 결정이 여러 개라 결정별로 정리했다 (이슈 #354·#372·#374·#376 원문 기준):

결정 택한 방법 버린 방법과 이유
파일 스토리지 MinIO 자체 호스팅 + 기존 공개 도메인 유지 DB URL 치환 마이그레이션. URL이 단순 컬럼뿐 아니라 마크다운 본문·JSON·jsonb 안에도 있어 부분 치환 위험이 큼
메일 Postfix 릴레이 컨테이너 경유 → 기존 외부 릴레이 서버 직접 발송. 공인 IP에 PTR이 없고(NXDOMAIN) Spamhaus PBL 등재라 거절/스팸 처리됨
포트 nginx 80/443만 공개, 인프라는 127.0.0.1 바인딩 UFW로 차단. Docker가 삽입한 iptables 규칙보다 늦게 평가돼 published 포트를 막지 못함
DB 이관 기존 덤프 복원 Flyway로 신규 생성. 첫 스크립트가 ALTER TABLE로 시작해 빈 DB에서 불가
#372 권한 Dockerfile에서 UID/GID를 호스트와 같게 고정(1001:1003) 파일 권한 완화(0644). JWT 시크릿·Firebase 키라 권한을 넓히지 않음
#374 DNS 공개 리졸버(1.1.1.1 / 8.8.8.8 / 9.9.9.9)에 DoH로 조회 후 curl --resolve dig 사용. 의존성을 늘리지 않으려고 이미 쓰는 curl로 처리
#376 방어 setup.sh는 없을 때만 생성 + deploy.sh가 전환 직후 컨테이너 시야 확인, 어긋나면 구 색을 살린 채 배포만 실패 setup.sh만 수정. 편집기 저장·sed -i·파일 복사도 inode를 바꾸므로 배포 단계 확인이 필요

결과

측정 조건: 컷오버 전, 운영용 자체 서버에서 workflow_dispatch로 develop 기준 배포를 반복 실행해 확인 (PR #378). 요청 생성 방식·속도는 PR #378 원문에 없음 (이슈 #354 인수 조건은 "프록시에 지속 요청을 걸어둔 상태로 배포 실행 → 비200 응답 0"). 실사용자 트래픽 여부는 원문에 없음.

근거