Project Portfolio

Popping-community — 커뮤니티 서비스

개인 프로젝트 (2025.03 ~ 현재) · EC2 실 배포 · GitHub
테스트 환경: EC2 t2.micro (애플리케이션 + MySQL 공유) · JMeter 100 VUser · DAU 1만 명 가정
데이터 규모: 서비스 성장 시 병목 사전 검증을 위해 게시글 100만 · 댓글 1,000만 · 좋아요 450만 건 적재

1. 인기 게시글 댓글 트리 반복 생성으로 응답 지연 발생, 인메모리 캐시(Caffeine) Cache-Aside 패턴 적용으로 댓글 조회 응답시간 152ms → 43ms 및 CTE 실행 횟수 6,642건 → 458건 감소 Blog

Problem
Analyze
검토 방식검토 결과판단
Write-Through 커뮤니티 댓글은 새 댓글이 달린 직후 다른 사용자에게 몇 초 뒤에 보여도 사용자 경험에 문제가 없어 즉시 최신 데이터를 보장할 필요가 없는 도메인. 동시 쓰기 시 느린 재조립이 최신 캐시를 덮어쓰는 정합성 문제도 존재 기각
부분 무효화 불변 응답 구조를 사용해 일부 노드만 바꿔도 상위 경로 전체를 다시 만들어야 했기 때문에, 부분 갱신의 비용 이점이 거의 없었음 기각
Cache-Aside 댓글 트리 구조는 모든 사용자에게 동일하고 댓글 생성/삭제 시에만 변해 캐시 효율이 높음. 반면 좋아요 수는 수시로 변해 캐시에 넣으면 무효화가 잦아지고, 개인 반응(likedByMe)은 사용자마다 달라 공통 캐시에 넣을 수 없으므로 조회 시점에 별도 합성 채택
Action
Client → [Application] ↓ cache.get(postId) ├ HIT → 캐시 데이터 반환 + 좋아요/개인 반응 별도 합성 └ MISS → 계층형 조회 1회 실행 → 캐시 저장 → 반환 [댓글 작성/삭제] ↓ DB 커밋 완료 ↓ 커밋 이후 후속 처리 ↓ cache.evict(postId)
Result
인기 게시글 댓글 조회152 ms43 ms
CTE 실행 횟수 (10분)6,642건458건
캐시 히트율평균 ~80%
JMeter 100 VUser, 10분, 게시글 100만 건·댓글 1,000만 건, EC2 실서버 기준

2. 동시 좋아요 요청 시 유니크 제약 예외 발생, ON DUPLICATE KEY 기반 멱등 처리로 동시 20 VUser 요청에서 예외 0건 달성 Blog

Problem
Analyze
검토 방식검토 결과판단
비관적 락 Like row가 없으면 잠글 대상이 없어 Post에 락을 걸어야 함. 인기 게시글이 hot row가 되어 전체 직렬화로 처리량 급감 기각
예외 무시 후
계속 진행
트랜잭션 내부에서 쓰기 실패가 발생하면 이후 로직까지 실패가 전파될 수 있어, 상위 계층에 숨은 실패를 남길 위험이 있었음 기각
중복 키 기반 삽입
+ 영향 행 수 판정
사전 조회 없이 먼저 저장을 시도하고, 중복이면 no-op 처리. 실제 반영 여부를 영향 행 수로 판별해 count를 증감 채택
Action
// 멱등 삽입: 먼저 저장을 시도하고, 중복이면 변경 없이 종료 INSERT ... ON DUPLICATE KEY UPDATE id = id; // 실제로 새 행이 만들어진 경우에만 집계값 증가 if (affectedRows > 0) { incrementCount(...); }
Result
동일 사용자 JMeter 20 VUser 동시 요청 (ramp 1s)유니크 제약 예외 다수예외 0건, 멱등 처리

3. likes 450만 건에서 집계+개인화 단일 쿼리 풀스캔으로 응답시간 10초·에러율 15% 발생, 쿼리 분리+비정규화로 응답시간 10,460ms → 167ms·에러율 15.53% → 0% Blog

Problem
Before (단일 쿼리) SELECT COUNT(*), EXISTS(...) ← likes 450만 건 풀스캔 FROM likes WHERE target_type = ? AND target_id = ? ← target_type: 게시글/댓글 구분, target_id: 대상 ID ↑ 기존 유니크 인덱스의 선두 조건과 맞지 않아 인덱스 범위 탐색이 어려움
Analyze
검토 방식검토 결과판단
집계 전용
인덱스 추가
에러 즉시 해소(163ms, 에러 0%). 그러나 집계·개인화를 한 쿼리에서 처리하는 구조적 문제를 인덱스로 덮은 것에 불과. 데이터 증가 시 별도 인덱스 유지 비용도 함께 증가 한계
기존 유니크 인덱스
순서 변경
집계 조회와 사용자별 반응 조회의 선두 조건이 달라 인덱스 하나로 두 요구를 모두 만족시키기 어려웠음. 어느 방향이든 별도 인덱스가 추가로 필요 기각
쿼리 구조 분리 +
비정규화 칼럼
집계는 각 테이블의 비정규화 칼럼에서 직접 읽고, 개인화만 사용자 기준 유니크 인덱스를 활용 — 별도 집계 인덱스 없이 해결 가능 채택
Action
After (분리) [1] 집계값 ← 각 테이블의 like_count (비정규화 칼럼, 인덱스 불필요) [2] 개인 반응 ← SELECT ... FROM likes WHERE user_id = ? AND target_id IN (...) ↑ 사용자 기준 유니크 인덱스 활용, 인덱스 범위 탐색
Result
평균 응답시간10,460 ms167 ms
에러율15.53%0%
JMeter 100 VUser, 10분, likes 450만 건, EC2 실서버 기준. 별도 집계 인덱스 없이 기존 UK만으로 달성

4. App CPU 200% 포화로 TPS 274/s 정체, HAProxy cookie insert 기반 수평 확장(App 2대)으로 TPS 274 → 527/s·응답시간 867ms → 32ms Blog

Problem
Analyze
검토 방식검토 결과판단
Scale Up
(CPU 증가)
구성 변경 없이 즉시 효과가 있지만, 단일 장애점이 남고 비용이 급증하며 물리적 한계가 존재 한계
Nginx ip_hash 같은 IP에서 오는 요청만 고정 가능. 부하 테스트처럼 동일 IP에서 다수 스레드가 유입되면 한쪽 인스턴스에 쏠림 기각
HAProxy
cookie insert
첫 요청은 라운드 로빈으로 균등 분산하고 응답에 SERVERID 쿠키를 자동 삽입하여 이후 요청은 같은 인스턴스로 고정. Redis 없이 세션 유지 가능 채택
Action
Before: Client → App (2cpu/2GB) → MySQL (1cpu/1GB) After: Client → HAProxy (LB) ├ cookie SERVERID insert (첫 요청: round-robin → 쿠키 발급) ├ App-1 (2cpu/2GB) → MySQL (1cpu/1GB) └ App-2 (2cpu/2GB) ↗
Result
TPS274/s527/s (+92.3%)
평균 응답시간867 ms32 ms (-96.3%)
P95 응답시간4,799 ms93 ms (-98.1%)
P99 응답시간7,511 ms201 ms (-97.3%)
JMeter 500 VUser, 10분, 댓글 505만·좋아요 735만 건. Scale Out 후 병목이 App CPU에서 MySQL CPU(100%)로 이동 확인

5. Scale Out 후 MySQL CPU 100% 포화(병목 이동), Read Replica + Sticky Primary(쿠키 3초) 적용으로 응답시간 32ms → 10ms·Replication Lag 104s → 26s Blog

Problem
Analyze
읽기/쓰기 분리 방식
검토 방식검토 결과판단
AbstractRouting
DataSource
트랜잭션 시작 시점에 커넥션을 획득하는데, 이때 readOnly 플래그가 아직 미확정이어서 별도 AOP + ThreadLocal이 필요 (~50줄) 기각
LazyConnection
DataSourceProxy
커넥션 획득을 첫 SQL 실행까지 지연하므로, readOnly 플래그가 확정된 시점에 자연스럽게 올바른 DataSource로 라우팅. AOP/ThreadLocal 불필요 (~20줄) 채택
Replication Lag 대응 전략
검토 방식검토 결과판단
Semi-sync
Replication
모든 쓰기에 Replica ACK 대기를 강제하여 쓰기 지연 증가. 커뮤니티처럼 “본인 쓰기만 즉시”이면 전체 일관성은 과잉 기각
병렬 복제 +
Sticky Primary
병렬 복제로 Lag 자체를 줄이고, 쓰기 직후 3초간만 Master에서 읽어 “내 글이 안 보이는” 문제만 정확히 해결. 나머지 읽기는 Replica 유지 채택
Action
Client → HAProxy → App-1/App-2 ↓ StickyPrimaryFilter (Cookie → ThreadLocal) ↓ LazyConnectionDataSourceProxy ├ @Transactional → Master (Write) └ @Transactional(readOnly) ↓ StickyAwareRoutingDataSource ├ sticky=true → Master (쓰기 후 3초간) └ sticky=false → Replica (평상시) [쓰기 커밋 완료] → StickyPrimaryAspect.afterCommit() → STICKY_PRIMARY=1 쿠키 발급 (maxAge=3s, HttpOnly)
Result
평균 응답시간32 ms10 ms (-67.1%)
P99 응답시간237 ms80 ms (-66.2%)
Master CPU100% (포화)~25%
Replication Lag 평균104 s26 s (-75%)
Replication Lag 최대348 s177 s (-49%)
읽기/쓰기 분리: JMeter 500 VUser, 10분 기준. Lag 완화: JMeter 750 VUser, 10분 기준. 병렬 복제 + Sticky Primary 적용 후

6. 수동 빌드·배포·모니터링으로 반복 시간 소모, GitHub Actions + Jib + Prometheus·Grafana로 배포~모니터링 자동화

Problem
Action
main push ↓ GitHub Actions ├ Gradle build ├ 컨테이너 이미지 생성 → Docker Hub push └ SSH → EC2 docker-compose up -d Prometheus → Grafana (모니터링)
Result

PoppingOps — 백엔드 운영 모니터링·장애 알림 자동화

개인 프로젝트 (2026.04) · Bash · Railway · Docker · GitHub Actions · GitHub

1. 로컬 PC 의존 모니터링으로 PC 종료 시 감시 불가, Railway 컨테이너 기반 SSH 메트릭 수집으로 최대 10분 이내 자동 알림 전환

Problem
Action
Railway Container └─ health-check.sh ── SSH ── EC2 actuator/node-exporter/mysqld-exporter ├─ 10분 snapshot 저장 ├─ counter delta 기반 트래픽 지표/응답시간/에러율 계산 ├─ 임계값 기반 상태 전이 감지 (Memory ≥80% WARN, ≥95% CRITICAL 등) └─ Discord Webhook 알림 + 대응 조치 매뉴얼(JSON runbook) 기반 권장 조치
Result
장애 인지 소요시간수동 확인 (PC 종료 시 불가)최대 10분 이내 자동 알림

2. 모든 감지에 AI 사용으로 비용 반복·장애 전파, 정기 감지는 규칙 기반 스크립트로 분리하고 AI는 해석·요약에만 호출하여 정상 상태 AI 비용 0원 유지 및 AI 장애 시에도 감지 동작

Problem
Analyze
검토 방식검토 결과판단
모든 감지를
AI 모델로 처리
정상 상태에서도 매 10분마다 호출 비용이 발생하고, 제공자 장애가 곧 모니터링 장애로 이어짐. 임계값 비교 같은 단순 판단에 AI는 과잉 기각
권장 조치를
매번 AI 호출
같은 종류·심각도의 알림마다 같은 권장 조치를 반복 생성. 알림 지연 발생 기각
임계값 기반 상태 판단 +
대응 조치 매뉴얼 우선 +
AI는 해석에만
정기 메트릭 수집과 임계값 비교는 스크립트가 처리하고, 권장 조치는 대응 조치 매뉴얼(JSON runbook)을 우선 적용하며, AI는 사용자 질문·리포트 작성·매뉴얼 미매칭 fallback처럼 해석이 필요한 경우에만 사용 채택
Action
Detection Layer (Bash, AI 없음) └─ health-check.sh: 임계값 비교 → 상태 전이 감지 ↓ State Layer (snapshot) └─ /tmp/health-check-alerts/status-current.env ↓ Recommendation Layer (대응 조치 매뉴얼, AI 없음) └─ JSON runbook: 알림 종류·심각도별 권장 조치 우선 적용 └─ 미매칭 시에만 AI fallback ↓ Analysis Layer (AI, 필요 시에만) └─ Daily Summary(9AM) / Full Report(6시간) / 사용자 질문 ↓ Notification Layer └─ Discord Webhook
Result
정상 상태 토큰 비용매 10분마다 소비0원 (bash 처리)
권장 조치 경로매번 AI 호출대응 조치 매뉴얼 우선, 미매칭만 AI fallback
AI 제공자 장애 시핵심 감지 경로 영향감지·1차 알림은 계속 동작

3. 모니터링 장애 시 정상으로 오인 위험, 유형별 실패 추적 + snapshot freshness 검증으로 “서버 장애”와 “감지 불능” 분리 알림

Problem
Analyze
Action
health-check.sh / heartbeat-context.sh ├─ SSH 연결 확인 → 실패 시 연결 실패 카운트 증가 ├─ 메트릭 수집/검증 → 파싱 실패 또는 필수 값 누락 시 resource_parse 카운트 증가 └─ snapshot 저장 → 저장 실패 시 snapshot_write 카운트 증가 [유형별 2회 연속 실패] → "모니터링 장애 알림" snapshot freshness 판정 ├─ <20분: OK ├─ 20~30분: WARN (오래됨) └─ >30분: CRITICAL (수집 중단 가능성) 필수 메트릭/핵심 값 검증 ├─ app/node/mysql metrics 누락 ├─ 핵심 값 비어 있음/0 이하 └─ 위 경우 모두 resource_parse로 처리하고 snapshot 갱신 중단
Result
장애 유형 구분서버/모니터링 장애 미구분"서버 장애" vs "감지 불능" 분리 알림
stale data 오인오래된 snapshot을 현재로 사용freshness 검증 (20분/30분 임계)
불완전 데이터exporter 누락 시 그대로 전달필수 메트릭 검증 후 차단

FINNA — 루틴 관리 서비스

팀 프로젝트 (4명) · 2024.01 ~ 2024.04 · EC2 실 배포 · 본인 담당: 백엔드 전체 · GitHub

1. 수동 API 명세 공유로 프론트엔드와 명세 불일치·재작업 발생, Spring REST Docs로 테스트 통과 시에만 문서 생성되는 자동화 파이프라인 구축

Problem
Analyze
Action
테스트 코드 작성 ↓ ./gradlew test ↓ asciidoctor (snippets → HTML 문서) ↓ copyDocument (static/docs에 배치) ↓ 테스트 통과 시에만 문서 갱신 → 문서 = 항상 최신 상태
Result