← 문제 해결 기록

5인 팀 · 이전 프로젝트 · 2025.12 - 2026.02 (후속 개선 2026.03)

StoLink

외부 시스템의 대기가 내부 자원을 묶는 경계를 추적했습니다.

개요

기간
2025.12 - 2026.02 (후속 개선 2026.03)
참여 형태
5인 팀 · 이전 프로젝트
담당
장애 분석·결제 구현
목적
웹소설 작가가 인물·관계·사건을 직접 정리하지 않아도, 쓴 글에서 자동으로 분석해 그래프로 볼 수 있게 하는 것
대상
웹소설 작가와, 관계도와 함께 작품을 읽는 독자
사용 기술
Spring Boot · PostgreSQL · Testcontainers · React · Canvas
크래프톤 정글 5인 팀 프로젝트에서 백엔드와 프론트엔드 구현을 맡았습니다. 여기에는 장애 분석과 결제 경계 설계만 남겼습니다.

구현 기능과 기술 선택

  • 글에서 인물·관계·사건을 분석해 그래프로 시각화
  • 에디터와 문서 폴더 트리
  • 작품 열람·구매를 위한 크레딧 결제·충전
  • OAuth2 소셜 로그인과 JWT 인증·인가, 문서 폴더 트리 CRUD API, 크레딧 결제·충전 시스템 구현
  • 결제를 검증 → 트랜잭션 밖 PG 호출 → 결과 반영으로 분리, 잔액 경합에는 비관적 락·중복 요청에는 멱등키와 DB 제약 적용
  • Canvas API 기반 인물 관계도, Tiptap 기반 에디터, 폴더 트리 문서 사이드바 구현
비관적 락
같은 잔액 행을 동시에 읽고 저장하면 한쪽 차감이 사라질 수 있어, 잔액 경합에는 행 잠금을 적용했습니다.
멱등키와 DB 제약
같은 요청의 재시도는 락으로 막을 수 없는 별도의 문제라, 중복 처리 방지를 따로 두었습니다.
Testcontainers
락과 제약의 동작은 실제 PostgreSQL에서만 확인할 수 있어 인메모리 DB 대신 컨테이너를 썼습니다.
SVG 대신 Canvas
SVG는 노드마다 DOM 요소가 생겨 스타일 재계산과 리플로우가 프레임마다 반복됩니다. 요소 수와 무관하게 한 번에 그리는 Canvas로 바꿨습니다.

트러블슈팅

쓰레드 덤프에서 커넥션 점유까지 추적한 장애

문제 상황

전체 API가 간헐적으로 응답하지 않았지만 CPU·메모리는 정상이었습니다. 자원 부족으로는 설명되지 않는 장애였습니다.

해결 과정

서버를 키우는 대신 멈춘 순간의 쓰레드 덤프를 떴습니다. 외부 헬스체크가 Socket Read에서 기다리고 있었고, REQUIRES_NEW 트랜잭션이 DB 커넥션을 쥔 채 그 응답을 기다리는 흐름이었습니다. 타임아웃 설정이 있는데도 무한 대기한 이유는 @Value 필드 주입이 생성자 이후에 이뤄져 초기화 시점에 값이 비어 있었기 때문이었습니다. 타임아웃과 트랜잭션 경계를 따로 고쳤습니다. 생성자 주입으로 바꿔 외부 응답 대기를 5초로 제한하고, 결제는 검증 → 트랜잭션 밖 PG 호출 → 결과 반영으로 나눠 외부 대기 동안 커넥션을 잡지 않게 했습니다. 타임아웃만으로는 5초 동안의 점유가 남기 때문입니다.

결과

외부 연결 차단과 PG 지연을 주입한 검증에서도 일반 API가 정상 응답하는 것을 확인했습니다.

본인 판단: 전체 API 장애를 단순 자원 부족으로 판단하지 않고, 외부 호출·빈 초기화·트랜잭션 경계를 연결해 추적했습니다.

남은 실패 조건: PG 승인 후 DB 반영 실패는 재처리와 수동 처리 경로가 필요합니다.

동시 차감과 중복 요청을 다른 실패로 다루기

문제 상황

크레딧 결제에서 같은 잔액을 동시에 읽고 저장하면 한쪽 차감이 사라질 수 있고, 같은 요청이 재시도되면 두 번 차감될 수 있었습니다. AI 코드 리뷰가 제기한 위험을 먼저 테스트로 재현해 실제 문제임을 확인했습니다.

해결 과정

락은 동시 차감을 막지만 같은 요청의 재시도는 막지 못해, 두 문제를 따로 다뤘습니다. 잔액 경합에는 낙관적 락도 검토했지만, 같은 잔액에 요청이 몰리면 충돌한 요청들의 재시도가 더 큰 부하가 된다고 판단했습니다. 그래서 대기 지연을 감수하고 잔액 행에는 비관적 락을, 같은 요청의 재처리에는 멱등키와 DB 제약을 적용했고, 락을 쥐는 시간이 길어지지 않게 PG 호출은 트랜잭션 밖으로 뺐습니다. 락과 제약의 동작은 인메모리 DB로는 확인할 수 없어, Testcontainers로 실제 PostgreSQL을 띄워 검증했습니다.

결과

100개 스레드가 동시에 차감해도 잔액이 맞았고, 잔액이 절반뿐이면 정확히 50건만 성공해 음수가 되지 않았습니다.

본인 기여: 결제 경계·동시성 개선과 검증. AI 리뷰의 제안을 재현 테스트로 확인했습니다.

노드 50개에서 느려지던 관계도를 Canvas로 옮기기

문제 상황

핵심 기능인 인물 관계도는 D3.js SVG로 그렸는데, 노드가 50개만 넘어도 클릭·드래그 응답이 420ms까지 느려지고 FPS가 10 아래로 떨어졌습니다. 인물이 많은 소설일수록 쓸 수 없는 기능이 되는 상황이었습니다.

해결 과정

처음 맡은 프론트엔드라 원인부터 몰랐는데, DevTools Performance로 보니 병목은 스타일 재계산이었습니다. SVG는 노드마다 DOM 요소가 생겨 프레임마다 수백 개 요소의 스타일 재계산과 리플로우가 반복되는 구조였습니다. DOM 요소 수에 비례하는 비용을 없애려고 Canvas로 전면 전환했습니다. 대신 DOM 이벤트를 쓸 수 없어 클릭과 드래그는 좌표 기반 히트 테스트로 직접 구현했고, React의 불변 데이터와 물리 엔진의 가변 좌표가 충돌하는 문제는 물리 연산용 복사본과 useRef로 분리했습니다. 캐릭터 이미지는 Offscreen Canvas에 비트맵으로 캐싱했습니다.

결과

INP는 420ms에서 64ms로, FPS는 10 미만에서 60으로 올랐고, 노드 650개 이상에서도 끊김 없이 동작했습니다.

결과와 성과

  • CPU·메모리가 정상인 전체 API 무응답을 쓰레드 덤프로 추적, 외부 호출 무한 대기와 DB 커넥션 점유를 원인으로 확인해 타임아웃 5초와 트랜잭션 경계 분리로 해결
  • Testcontainers의 실제 PostgreSQL에서 100스레드 동시 차감 시나리오의 잔액 정합성 검증
  • 관계도 렌더링을 SVG에서 Canvas로 전환해 INP 420ms → 64ms, 650개 이상 노드에서 60FPS 유지

회고

배운 점
외부 시스템의 대기가 내부 자원을 묶는 경로를 쓰레드 덤프로 따라가는 방법을 익혔습니다. 타임아웃과 트랜잭션 경계 분리가 서로 다른 문제를 푼다는 점도 구분하게 됐습니다.
아쉬운 점
PG 승인 후 DB 반영이 실패하는 경우의 재처리 경로를 만들지 못했습니다. 동시성 테스트도 특정 시나리오에 한정됩니다.
보완 계획
승인 후 반영 실패에 대한 재처리와 수동 처리 경로를 추가하는 것이 남은 과제입니다.