← 이력서
백엔드 케이스 포트폴리오 · 변승준
Backend Case Portfolio

변승준Byun Seungjun · Backend Engineer

실서비스를 만들고 운영하며 백엔드 기본기를 다진 엔지니어
Fine Play

축구 영상 분석 서비스 Fine Play를 백엔드·인프라부터 직접 만들어 운영하고 있습니다. 여기 담은 케이스는 모두 그 과정에서 실제 트래픽으로 마주한 것들입니다. 커넥션 풀 고갈, N+1, 트랜잭션 전파, 캐시를 원인부터 파고들어 해결했고, 각 케이스에 문제 → 원인 → 해결 → 결과를 담았습니다.

byean0106@naver.com  ·  github.com/ByunSeungJun  ·  byunseungjun.github.io
1 / 8
Fine Play 백엔드 케이스 포트폴리오
Overview
안정성
150+→40
커넥션 풀 고갈 장애를 역산 설계로 안정화 (MySQL 한계 151)
성능
3N→N+1
리스트 조회의 중복 요청 제거
트래픽
250
대회 순간 최대 동시접속을 무장애 운영
아키텍처
2→1
두 B2C 서비스를 단일 백엔드로 통합
System at a glance
사용자 Flutter 앱 iOS·Android·Web 운영자 React 어드민 운영·콘텐츠·결제 REST SSE 공유 Spring Boot 통합 JWT · 도메인 분리 admin·fineday·fineplay·global MySQL Redis S3 AWS EC2·ALB 외부 Matchday API — 서버 프록시로 감싸 키를 숨김
단일 Spring Boot 백엔드 위에 Flutter 앱과 React 어드민을 올린 풀스택 스포츠 데이터 플랫폼. 인증·결제·실시간(SSE)·외부 API 프록시까지 직접 다뤘습니다.
Contents
01단일 백엔드로 2개 B2C 서비스 운영무엇을 공유하고 무엇을 분리할지Architecture
02 DB 커넥션 풀 고갈 — 재시작 악순환 종결전체 합으로 자원 설계Reliability
03Redis 기반 인증·캐시 설계토큰 회전과 장애 격리Auth · Cache
04 리스트 조회 성능 최적화3N 요청을 N+1로Performance
05트랜잭션 전파 설계무엇을 같은 커밋에 둘지Transaction
A엔지니어링 하이라이트백엔드 · AI 활용Appendix
2 / 8
Fine Play 백엔드 케이스 포트폴리오
01 Architecture
단일 Spring Boot 백엔드로 2개 B2C 서비스를 한 팀이 운영
두 B2C 서비스를 운영하는 스포츠 데이터 플랫폼
역할백엔드·인프라 설계·구현   기간2024.10~   규모소수 창업팀
Spring BootJWTMySQLRedisAWSBlue-Green
문제
두 B2C 서비스를 소수 팀이 함께 운영

Fine Play(축구 분석)와 Fine Day(선수 매칭)를 운영하는데, 인증과 인프라, 배포를 서비스마다 따로 만들 여력이 없었습니다.

해결
백엔드는 공유하고 도메인만 분리
  • 단일 Spring Boot에 통합 JWT를 두고, 코드는 도메인 패키지(admin·fineday·fineplay·global)로 나눴습니다.
  • Fineday 합류로 백엔드 2명이 들어왔을 때, 어떤 데이터를 공유하고 어떤 걸 분리할지부터 정해 트랜잭션 경계를 잡았습니다.
  • AWS 위에 GitHub Actions와 Blue-Green으로 무중단 배포 파이프라인을 붙였습니다.
결과
인증·배포를 하나로 묶어 두 제품 운영

중복 인증과 배포를 없애 소수 팀이 두 서비스를 함께 굴릴 수 있게 됐고, iOS·Android·Web으로 출시했습니다(Apple 심사 반복 돌파).

배움
공유와 분리의 선을 먼저 그은 게 컸다

무엇을 합치고 무엇을 나눌지 초기에 정해두니, 이후 기능이 늘어도 운영 복잡도가 크게 오르지 않았습니다.

AWS Blue-Green 무중단 배포 구조
AWS Blue-Green 배포 아키텍처
GitHub Actions → ECR·S3 → CodeDeploy → Blue/Green Auto Scaling(EC2 T3a) 무중단 전환 · ALB · RDS(MySQL) · ElastiCache · Route 53 · ACM
3 / 8
Fine Play 백엔드 케이스 포트폴리오
02 Reliability 1사건과 진단
재시작 악순환에 빠진 프로덕션 장애 — DB 커넥션 풀 고갈
실서비스 운영 인프라 · 대회 트래픽 급증기
역할진단·설계·조치   시점2026.02 트래픽 급증기
HikariCPMySQLDockerAWS 다인스턴스
문제
재시작 루프로 전 인스턴스가 다운

운영 중 Too many connections로 앱이 못 뜨고, Docker가 자동 재시작하며 커넥션이 더 쌓이는 악순환으로 전 인스턴스가 내려갔습니다. 대회로 트래픽이 몰리던 시점이었습니다.

진단
가설을 하나씩 지워 원인을 좁힘
  • 관측Threads_connected가 151(max_connections)에 닿음SHOW STATUS로 확인 · 재시작마다 계단식으로 증가
  • 반증“트래픽 급증으로 정상 커넥션이 폭증”은 아님앱 풀 상한 합은 4대 × 10 = 40. 정상 경로만으로는 151에 닿을 수 없음
  • 확증비정상 종료 시 이전 커넥션이 회수되지 않고 누적재시작마다 직전 컨테이너의 커넥션이 DB에 잔존 → 계단식으로 쌓여 한계 도달
인시던트 흐름
① 커넥션 고갈 ② 재시작 악순환 ③ 근본원인 진단 ④ 해결 (다음 장)
커넥션 수: 한계까지 계단식 급증 (관측)
시간 (재시작 반복) → 커넥션 MySQL 한계 151 재시작마다 계단식 급증 ↑ 한계 도달
정상 트래픽이라면 앱 풀 상한 합(4×10=40)을 넘을 수 없는데 151에 닿았습니다. 재시작할 때마다 잔존 커넥션이 쌓인 것이 원인이었습니다.
다음 장 · 구조적 해결과 결과
4 / 8
Fine Play 백엔드 케이스 포트폴리오
02 Reliability 2해결과 결과
같은 장애, 구조로 해결하고 결과로 증명 — HikariCP 전체-합 역산
앞 장의 커넥션 풀 고갈 사건에서 이어짐
역할설계·조치   해결HikariCP 총량 역산
HikariCPmax-lifetime역산 설계
해결
즉시 정리 후, 총량을 역산해 고정
  • 전 인스턴스 컨테이너를 강제 종료해 잔존 커넥션을 회수하고 재시작 루프를 끊었습니다.
  • HikariCP 풀 상한과 커넥션 수명을 명시해 클러스터 전체 커넥션을 DB 한계 아래로 고정했습니다.
결과
커넥션 150+ → 40~50로 안정 · 재시작 악순환 종결
배움
자원 한도는 인스턴스 하나가 아니라 전체 합으로
실제 효과 대회 트래픽 급증기에 전 인스턴스가 내려가 서비스가 멈추던 상황을 무중단 운영으로 회복했습니다. 사용자 로그인·조회 실패가 사라졌습니다.
구조적 해결: HikariCP 총량 역산
# application.yml — 클러스터 전체 합을 DB 한계 아래로 고정
spring:
  datasource:
    hikari:
      maximum-pool-size: 10      # 4 인스턴스 × 10 = 40 ≤ 151
      max-lifetime: 300000       # 커넥션 수명 제한 → 잔존이 오래 남지 않게
      connection-timeout: 3000
      keepalive-time: 120000
역산: Σ(인스턴스 수 × 풀 상한) ≤ max_connections. 원인을 없애는 게 아니라 재발해도 총량이 한계 아래가 되게 통제하고, 수명 제한으로 잔존 회수를 앞당김.
Before~151한계 도달 · 재시작마다 누적 · 전 인스턴스 다운
After40~50풀 밖 접속 포함 안정치 · 재시작 악순환 종결
·
Fine Play 백엔드 케이스 포트폴리오
03 Auth · Cache
Redis 기반 인증·캐시 설계 — 토큰 회전과 장애 격리
통합 JWT 인증 시스템
역할인증·캐시 설계·구현
RedisJWTRTR분산락
문제
토큰이 탈취돼도, Redis가 죽어도 버티게

토큰이 탈취·재사용돼도 피해를 막고 싶었고, 동시에 Redis가 멈춰도 인증과 서비스가 함께 죽지 않게 하고 싶었습니다.

해결
상태를 저장하고, 장애를 흡수하도록 설계
  • 리프레시 토큰 회전(RTR)으로 토큰마다 상태(VALID·USED·REVOKED)를 Redis에 두고, 이미 쓴 토큰이 다시 오면 재사용으로 보고 해당 유저의 토큰을 일괄 무효화했습니다.
  • 동시에 들어오는 회전 요청은 setIfAbsent 분산락으로 하나만 통과시켰습니다.
  • 로그아웃한 액세스 토큰은 남은 만료시간만큼 TTL을 걸어 블랙리스트에 넣어 자동 소멸시켰습니다.
  • Redis 장애 시 캐시 실패를 흡수해 인증·프록시가 계속 동작하도록(graceful degradation) 했습니다.
결과
토큰 재사용 대응 + 단일 장애점 제거

재사용 공격에 대응하는 인증 구조를 갖췄고, Redis가 죽어도 서비스가 멈추지 않게 만들었습니다.

배움
보안은 “장애일 때”까지 설계해야 한다

정상 동작만이 아니라, 장애가 났을 때 무엇을 열고 무엇을 닫을지(페일오픈/클로즈)를 함께 정해야 한다는 걸 배웠습니다.

리프레시 토큰 회전 (RTR)
로그인 AT + RT 발급 RT 회전 old → USED · new → VALID 이미 쓴 토큰 재사용 시 유저 토큰 일괄 무효화
장애 격리: Redis가 죽어도 인증은 산다
// 캐시 실패를 흡수 — 단일 장애점 제거 (fail-open)
try {
    return redis.opsForValue().get(key);      // 정상: 캐시 조회
} catch (RedisConnectionFailureException e) {
    log.warn("Redis down — degrade", e);
    return loadFromSource();                   // 장애: 원본 폴백
}
함께 쓴 안전장치로 분산락(setIfAbsent)은 동시 회전을 차단하고, 블랙리스트 TTL은 로그아웃 토큰을 자동 소멸시킵니다. 정상일 때뿐만 아니라 장애일 때 무엇을 열고 닫을지까지 설계했습니다.
5 / 8
Fine Play 백엔드 케이스 포트폴리오
04 Performance 1문제와 측정
리스트가 커질수록 요청이 폭증 — 유저 목록·대시보드의 N+1
앱 유저 목록 · 어드민 대시보드 조회
역할성능 진단·개선   지표요청/쿼리 수
JPAMySQL지연 로딩캐시
문제
리스트가 길수록 화면당 요청이 폭증

유저 리스트가 길어질수록 화면 하나에 나가는 네트워크 요청·쿼리가 유저 수에 비례해 늘었습니다. 동작은 그대로 두고 비용만 줄여야 했습니다.

진단
셀 수 있는 지표로 원인을 특정
  • 관측화면 1개 요청 수가 유저 수(N)에 비례해 최대 3N리스트가 길수록 선형으로 늘어나는 셀 수 있는 지표
  • 유저 카드마다 프로필 이미지·즐겨찾기를 개별 호출목록 응답에 이미 있는 값을 두고 per-user로 다시 요청
  • 서버대시보드 목록에서 @ManyToOne 즉시 로딩항목마다 연관 엔티티 조회 쿼리가 붙는 전형적 N+1
요청 수: 유저 수(N)에 비례 (측정)
유저 수 N → 요청 수 최대 3N ↑ 카드마다 개별 호출
"느리다"가 아니라 요청 수(3N)라는 셀 수 있는 지표로 원인을 특정했습니다.
다음 장 · 해결과 결과
·
Fine Play 백엔드 케이스 포트폴리오
04 Performance 2해결과 결과
중복 호출 제거 · 지연 로딩 · DTO — 3N을 N+1로
앞 장에서 측정한 N+1을 구조로 해결
역할설계·개선   지표요청/쿼리 수
Fetch LAZYDTO Projection이미지 캐시
해결
목록 값 재사용 · 지연 로딩 · 공용 캐시
  • 목록 응답에 이미 있는 값을 재사용해 per-user 호출 제거 (3N→N+1), DTO 보강 시 →1.
  • 서버 @ManyToOne을 지연 로딩으로 바꿔 N+1 쿼리 제거.
  • 공용 이미지 컴포넌트로 디스크 캐시(1000개·60일)를 약 45곳에 일괄 적용.
결과
리스트 요청 3N → N+1 · 정적분석 0 error
실제 효과 느려지던 관리자 대시보드와 앱 유저 리스트의 조회 비용을 줄여, 목록이 길어져도 매끄럽게 스크롤·조회되게 했습니다.
핵심 코드: 지연 로딩 + DTO Projection
// 1) 즉시 로딩(N+1) → 지연 로딩
@ManyToOne(fetch = FetchType.LAZY)
private Team team;

// 2) 목록은 필요한 컬럼만 DTO로 한 번에 (→1)
@Query("select new UserCardDto(u.id, u.name, u.imageUrl) from User u")
List<UserCardDto> findCards();
엔티티 전체를 가져와 카드마다 연관을 다시 조회하는 대신, 화면에 필요한 필드만 한 쿼리로. 요청도 쿼리도 N에서 상수로.
Before3N카드마다 이미지·즐겨찾기 개별 호출 + @ManyToOne N+1
AfterN+1목록 값 재사용 · 지연 로딩 · DTO 보강 시 →1
·
Fine Play 백엔드 케이스 포트폴리오
05 Transaction
트랜잭션 전파 설계 — 무엇을 같은 커밋에 둘지
결제·구독 도메인
역할트랜잭션 경계 설계·디버깅
Spring TxJPAMySQL
문제
너무 많이 묶인 트랜잭션이 데이터를 삼켰다

하나의 트랜잭션에 일을 너무 많이 묶으면, 뒤에서 난 예외가 앞의 성공까지 되돌립니다. 어긋나면 안 되는 데이터를 지켜야 했습니다.

해결
같은 커밋에 둘 것과 분리할 것을 나눔
  • 구독 강등 처리가 상위 트랜잭션 롤백에 휩쓸려 유실되거나 재청구가 중복되던 경로를, 후처리를 REQUIRES_NEW로 분리해 독립 커밋되게 했습니다.
  • “결제는 성공했는데 커밋 후 후처리 예외로 500이 나던” 문제는, 커밋 이후 단계(알림·외부 호출)를 트랜잭션 밖으로 빼 원인을 없앴습니다.
  • 무엇을 같은 커밋에 두고 무엇을 분리할지를 기준으로 트랜잭션 경계를 다시 그었습니다.
결과
강등 유실·중복 청구·성공 후 500 제거

롤백에 데이터가 휩쓸리는 경로가 사라졌고, 결제 성공 뒤 500이 재발하지 않았습니다.

배움
다 묶는다고 안전한 게 아니다

트랜잭션은 크게 묶을수록 안전한 게 아니라, 같은 커밋에 둘 것과 분리할 것을 나누는 설계라는 걸 배웠습니다.

트랜잭션 경계: 묶기 vs 분리
Before 하나의 트랜잭션 { 결제 · 강등 · 후처리 } 후처리 예외 → 전체 롤백 · 강등 유실 / 중복 재청구 경계 분리 After 결제 · 강등 후처리 REQUIRES_NEW 알림·외부호출 AFTER_COMMIT 독립 커밋 · 롤백에 휩쓸리지 않음
핵심 코드: 전파 속성으로 경계 분리
// 강등: 상위 롤백에 안 휩쓸리게 독립 커밋
@Transactional(propagation = REQUIRES_NEW)
public void demote(Subscription s) { ... }

// 알림·외부 호출: 커밋 성공 후에만 → 성공 후 500 방지
@TransactionalEventListener(phase = AFTER_COMMIT)
public void onPaid(PaidEvent e) { notify(e); }
정합성이 필요한 일은 같은 커밋에, 외부 호출·알림은 커밋 밖으로. 트랜잭션 경계를 나누는 게 곧 정합성 설계입니다.
7 / 8
Fine Play 백엔드 케이스 포트폴리오
Appendix
엔지니어링 하이라이트 — 백엔드, 그리고 AI 활용
백엔드 엔지니어링 · AI 활용 (에이전트 개발 · 학내·사이드 프로젝트)
결제 시스템 — 토스 빌링키 정기·단건
Payments
서버가 금액을 확정해 변조를 막고, 웹훅 서명 검증 + PG 재조회로 이행 누락을 이중 방어. 결정적 주문키 멱등성으로 중복 결제를 차단했습니다.
TossWebhook멱등성
매치데이 프록시 — 외부 API 보안
Security
외부 API 키를 서버 프록시로 격리해 클라에 노출하지 않고, 경로 화이트리스트로 검증. 업스트림 실패 시 502로 끊고 Redis TTL 캐시로 완화했습니다.
프록시화이트리스트TTL 캐시
SSE 실시간 알림 — 상태 전파
Realtime
서버-센트 이벤트(SSE)로 공지·알림 등 상태 변화를 실시간으로 클라이언트에 전파하는 채널을 설계했습니다.
SSE이벤트
협업 · 문서화 — 이어받을 수 있는 코드
Process
PR·커밋·이슈 템플릿을 정립하고, 약 268개 REST API를 문서화. 아키텍처 결정 기록(ADR)으로 “누구든 이어받을 수 있게” 남겼습니다.
PR 컨벤션API 문서화ADR
AI 활용 개발 — 병행 세션 워크플로우
AI Workflow
Claude Code 멀티세션을 구현·리뷰·점검으로 나눠 병행하고, Obsidian에 아키텍처·결정 기록을 두어 컨텍스트로 주입. 스펙·검증·판단은 직접 했습니다.
Claude Code멀티세션Obsidian
AI 활용 프로젝트 — FirstAid · StyleRobo · iCatch
LLM · CV
GPT로 증상→응급처치 구조화 출력(FirstAid), 태그→LLM 문구·이미지 생성(StyleRobo), YOLO·MediaPipe 홈캠 경량화(iCatch). LLM·생성·CV를 실제 결과물에 활용했습니다.
OpenAI GPTLLMYOLOv8OpenCV
8 / 8