Split View: 프레임워크별 디버깅 실전: Spring Boot · Django/FastAPI · React/Next.js에서 브레이크포인트, 실행, 프로파일링
프레임워크별 디버깅 실전: Spring Boot · Django/FastAPI · React/Next.js에서 브레이크포인트, 실행, 프로파일링
이 글은 디버깅 실전 시리즈 5편 중 2편이다.
프레임워크 디버깅의 핵심
언어보다 먼저 봐야 할 건 프레임워크의 실행 흐름이다.
- 요청 진입점(Controller/View/Route)
- 미들웨어/필터
- 데이터 접근 계층
- 외부 API 호출
- 렌더링/하이드레이션
디버깅은 이 흐름의 어느 단계에서 데이터가 깨지는지 찾는 작업이다. 프레임워크마다 이 흐름의 구조와 관례가 다르기 때문에, 디버깅 진입점도 달라진다.
프레임워크별 디버깅 진입점 요약
| 항목 | Spring Boot | Django | FastAPI | React | Next.js |
|---|---|---|---|---|---|
| 요청 진입점 | @Controller/@RestController | View 함수/CBV | Router 함수 | Component render | Server Component / Route Handler |
| 미들웨어/필터 | Filter, Interceptor | Middleware 클래스 | Middleware (Starlette) | - | middleware.ts |
| 데이터 접근 | JPA Repository | Django ORM | SQLAlchemy/Tortoise | - | Prisma/Drizzle (서버) |
| 에러 핸들링 | @ExceptionHandler | Middleware / DRF exception | exception_handler 데코레이터 | ErrorBoundary | error.tsx / not-found.tsx |
| 프로파일링 도구 | JFR, Micrometer | Django Debug Toolbar, py-spy | py-spy, OpenTelemetry | React DevTools Profiler | Next.js build analyzer |
| 디버그 로그 설정 | application.yml logging.level | LOGGING dict in settings.py | logging.basicConfig() | React DevTools | next.config.js logging |
프레임워크별 흔한 장애 유형과 도구
| 프레임워크 | 흔한 장애 유형 | 진단 도구 | 핵심 해결 패턴 |
|---|---|---|---|
| Spring Boot | N+1 쿼리, 트랜잭션 경계 오류, Bean 순환 의존 | Hibernate SQL 로그, Micrometer, JFR | @EntityGraph, @Transactional 범위 조정 |
| Django | N+1 쿼리, 시리얼라이저 유효성 오류, 마이그레이션 충돌 | django-debug-toolbar, Silk, assertNumQueries | select_related/prefetch_related, 마이그레이션 squash |
| FastAPI | Pydantic 유효성 에러, 비동기/동기 혼용 블로킹, 의존성 주입 순서 | py-spy, OpenTelemetry, logging | run_in_executor, Depends 체이닝 설계 |
| React | 불필요 리렌더, 상태 관리 복잡도, 메모리 누수 (구독 해제 누락) | React DevTools Profiler, why-did-you-render | memo/useMemo, useEffect cleanup |
| Next.js | Hydration mismatch, 서버/클라이언트 경계 혼동, 캐시 정책 오해 | 브라우저 콘솔, Next.js 로그, build analyzer | 'use client' 명시, revalidate 설정 |
1) Spring Boot
실행/디버그 시작
# Gradle
./gradlew bootRun
# Maven
./mvnw spring-boot:run
# 특정 프로파일로 실행
./gradlew bootRun --args='--spring.profiles.active=dev'
원격 디버그:
JAVA_TOOL_OPTIONS='-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005' ./gradlew bootRun
브레이크포인트 추천 위치
- Controller 진입: 요청 파라미터와 헤더 확인
- Service 비즈니스 분기: 핵심 로직과 조건 분기 확인
- Repository 쿼리 직전: 쿼리 파라미터와 결과 확인
- ExceptionHandler: 예외 원인과 응답 매핑 확인
- Filter/Interceptor: 인증/권한 체크 흐름 확인
미들웨어/인터셉터 디버깅
// HandlerInterceptor로 요청 흐름 추적
@Component
public class RequestTracingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 여기에 브레이크포인트 — 모든 요청의 첫 진입점
String requestId = request.getHeader("X-Request-Id");
MDC.put("requestId", requestId);
log.info("Incoming: {} {} requestId={}", request.getMethod(),
request.getRequestURI(), requestId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
// 여기에 브레이크포인트 — 응답 직전에 예외 확인
if (ex != null) {
log.error("Request failed: requestId={}", MDC.get("requestId"), ex);
}
MDC.clear();
}
}
데이터베이스 쿼리 디버깅 (SQL 로깅)
# application.yml — 개발/디버그 시 SQL 로깅 설정
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
# 실행되는 SQL 파라미터까지 확인
generate_statistics: true
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE # 바인딩 파라미터 출력
org.hibernate.stat: DEBUG # 쿼리 통계
org.springframework.transaction: TRACE # 트랜잭션 경계 확인
// N+1 감지: 쿼리 카운트 검증 테스트
@Test
void shouldNotCauseNPlusOne() {
// given
long queryCountBefore = getQueryCount();
// when
List<Order> orders = orderService.findAllWithItems();
// then
long queryCountAfter = getQueryCount();
assertThat(queryCountAfter - queryCountBefore)
.as("N+1 쿼리 발생 확인")
.isLessThanOrEqualTo(2); // SELECT orders + SELECT items
}
프로파일링
- Micrometer + Prometheus + Grafana: 요청/에러/지연시간 관측
- JFR/async-profiler: CPU/alloc 병목
- SQL 로그 + 실행계획: N+1/슬로우쿼리 확인
2) Django
실행
python manage.py runserver
# 특정 포트로 실행
python manage.py runserver 0.0.0.0:8080
# shell_plus로 인터랙티브 디버깅
python manage.py shell_plus --print-sql
브레이크포인트
def create_order(request):
breakpoint() # View 진입점에서 요청 객체 확인
serializer = OrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# ...
추천 위치:
- View 진입: request 데이터 확인
- Serializer 유효성 검사: validated_data 확인
- ORM 쿼리 직전/직후: 쿼리셋과 결과 확인
- Middleware 인증/세션 처리: 인증 흐름 추적
미들웨어 디버깅
# 커스텀 미들웨어로 요청/응답 추적
class RequestDebugMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 요청 진입 — 여기에 브레이크포인트
import time
start = time.time()
response = self.get_response(request)
# 응답 직전 — 느린 요청 감지
duration = time.time() - start
if duration > 1.0: # 1초 초과 시 경고
import logging
logger = logging.getLogger(__name__)
logger.warning(
f"Slow request: {request.method} {request.path} "
f"took {duration:.2f}s"
)
return response
데이터베이스 쿼리 디버깅
# settings.py — SQL 로깅 설정
LOGGING = {
'version': 1,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG', # 모든 SQL 쿼리 출력
'handlers': ['console'],
},
},
}
# 테스트에서 쿼리 수 검증 — N+1 방지
from django.test.utils import override_settings
class OrderQueryTest(TestCase):
def test_no_n_plus_one(self):
# 10개 주문 생성
create_test_orders(10)
# 쿼리 수 제한 검증
with self.assertNumQueries(2): # orders + items 2개 쿼리만 허용
orders = Order.objects.select_related('user') \
.prefetch_related('items').all()
# 결과를 평가해야 쿼리가 실행됨
list(orders)
N+1 대응:
# 나쁜 예: N+1 발생
orders = Order.objects.all()
for order in orders:
print(order.user.name) # 주문마다 user 쿼리 발생
print(order.items.count()) # 주문마다 items 쿼리 발생
# 좋은 예: 2개 쿼리로 해결
orders = Order.objects.select_related('user').prefetch_related('items').all()
for order in orders:
print(order.user.name) # 캐시에서 조회
print(order.items.count()) # 캐시에서 조회
프로파일링
- Django Debug Toolbar: 쿼리 수, 쿼리 시간, 템플릿 렌더링, 시그널
- Silk: 요청별 프로파일링, 느린 뷰 순위
- py-spy: gunicorn worker 단위 CPU 샘플링
3) FastAPI
실행
# 개발 모드 (자동 리로드)
uvicorn app.main:app --reload --log-level debug
# 프로덕션 모드
uvicorn app.main:app --workers 4 --host 0.0.0.0 --port 8000
브레이크포인트 추천
- Router 함수: 요청 파라미터와 의존성 주입 결과 확인
- Dependency injection 함수: 인증/DB 세션 등 공통 의존성 확인
- 외부 API 호출 함수: 요청/응답 페이로드 확인
- 예외 핸들러: 에러 응답 매핑 확인
미들웨어 디버깅
from starlette.middleware.base import BaseHTTPMiddleware
import time
import logging
logger = logging.getLogger(__name__)
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# 여기에 브레이크포인트 — 요청 진입점
start = time.time()
request_id = request.headers.get("X-Request-Id", "unknown")
try:
response = await call_next(request)
except Exception as exc:
# 미들웨어에서 잡히지 않는 예외 추적
logger.error(f"Unhandled error: {request_id} {exc}", exc_info=True)
raise
duration = time.time() - start
response.headers["X-Response-Time"] = f"{duration:.3f}s"
if duration > 1.0:
logger.warning(
f"Slow: {request.method} {request.url.path} "
f"{duration:.2f}s requestId={request_id}"
)
return response
비동기/동기 혼용 문제 디버깅
# 나쁜 예: async 함수에서 동기 I/O 호출 → 이벤트 루프 블로킹
@app.get("/users/{user_id}")
async def get_user(user_id: int):
# requests.get은 동기 호출 → 전체 서버 블로킹
response = requests.get(f"http://external-api/users/{user_id}")
return response.json()
# 좋은 예 1: httpx 비동기 클라이언트 사용
@app.get("/users/{user_id}")
async def get_user(user_id: int):
async with httpx.AsyncClient() as client:
response = await client.get(f"http://external-api/users/{user_id}")
return response.json()
# 좋은 예 2: 동기 코드를 run_in_executor로 감싸기
import asyncio
@app.get("/report")
async def generate_report():
loop = asyncio.get_event_loop()
# CPU-bound 작업을 스레드풀에서 실행
result = await loop.run_in_executor(None, heavy_computation)
return {"result": result}
데이터베이스 쿼리 디버깅
# SQLAlchemy SQL 로깅 활성화
import logging
logging.getLogger('sqlalchemy.engine').setLevel(logging.DEBUG)
# 또는 create_engine에서 직접 설정
from sqlalchemy import create_engine
engine = create_engine(DATABASE_URL, echo=True) # echo=True → SQL 출력
프로파일링
# py-spy로 PID 샘플링
py-spy top --pid <PID>
# flamegraph 생성
py-spy record -o profile.svg --pid <PID>
- OpenTelemetry로 endpoint/외부 호출 trace 연결
- pydantic validation 비용 체크: 대용량 payload에서 Pydantic V2의 성능 개선 확인
4) React
실행
npm run dev
# 또는
npx vite # Vite 프로젝트
브레이크포인트 전략
- 이벤트 핸들러(onClick/onSubmit): 사용자 인터랙션 시점 확인
- 상태 업데이트 직전(setState/useState): 상태 변경 원인 추적
- useEffect 의존성 경계: 무한 루프/누락된 의존성 확인
- API 응답 파싱 지점: 서버 데이터 형태 확인
리렌더 디버깅
// why-did-you-render로 불필요 리렌더 감지
// wdyr.ts (앱 진입점 전에 import)
import React from 'react'
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render')
whyDidYouRender(React, {
trackAllPureComponents: true,
logOnDifferentValues: true,
})
}
// 감시할 컴포넌트에 표시
const OrderList: React.FC<Props> = ({ orders }) => {
return (
<ul>
{orders.map((order) => (
<OrderItem key={order.id} order={order} />
))}
</ul>
)
}
OrderList.whyDidYouRender = true
// useEffect 무한 루프 디버깅
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null)
// 나쁜 예: 매 렌더마다 새 객체가 의존성에 들어감
// const options = { include: ['orders'] } // 렌더마다 새 참조
// useEffect(() => { fetchUser(userId, options) }, [userId, options])
// 좋은 예: useMemo로 참조 안정화
const options = useMemo(() => ({ include: ['orders'] }), [])
useEffect(() => {
fetchUser(userId, options).then(setUser)
}, [userId, options]) // options가 안정적이므로 무한 루프 방지
}
프로파일링
- React DevTools Profiler: 컴포넌트별 렌더 횟수와 시간 시각화
- why-did-you-render: 리렌더 원인 콘솔 출력
- Web Vitals(LCP/INP/CLS) 추적: 사용자 체감 성능 모니터링
체크 포인트:
- key 안정성: 배열 index 대신 고유 ID 사용
- memo/useMemo/useCallback: 남용하지 말되 측정 후 필요한 곳에 적용
- context 과도한 전파: context value 변경 시 구독자 전체 리렌더
5) Next.js
실행
# 개발 모드
npm run dev
# 프로덕션 빌드 + 실행
npm run build && npm run start
# Turbopack으로 빠른 개발 서버
npx next dev --turbopack
자주 터지는 이슈
- 서버/클라이언트 렌더 불일치(hydration mismatch)
- API route 에러 삼킴: try/catch 없이 500 응답
- Edge/Node.js runtime 차이: Edge에서 지원 안 되는 API 사용
- 이미지/캐시 정책 오해: ISR revalidate 설정 실수
App Router 전용 디버깅
// Server Component 디버깅 — console.log가 서버 터미널에 출력됨
// app/orders/page.tsx
export default async function OrdersPage() {
// 서버에서만 실행 — 브라우저 콘솔에 안 보임
console.log('[SERVER] Fetching orders...')
const orders = await db.order.findMany({
include: { user: true, items: true },
})
console.log(`[SERVER] Found ${orders.length} orders`)
return <OrderList orders={orders} />
}
// 'use client'를 빼먹으면 에러 — 서버 컴포넌트에서 훅 사용 불가
// "useState is not a function" 에러의 원인
'use client' // 반드시 파일 최상단에 선언
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
// Route Handler (app/api/orders/route.ts)
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const status = searchParams.get('status')
console.log('[API] GET /api/orders status=', status)
const orders = await db.order.findMany({
where: status ? { status } : undefined,
})
return NextResponse.json(orders)
} catch (error) {
// 에러 삼킴 방지 — 반드시 로그 남기기
console.error('[API] GET /api/orders failed:', error)
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
}
}
Hydration Mismatch 디버깅
// 흔한 원인 1: 서버/클라이언트에서 다른 값 렌더
// 나쁜 예
function Greeting() {
// Date.now()는 서버와 클라이언트에서 다른 값
return <p>Timestamp: {Date.now()}</p>
}
// 좋은 예: suppressHydrationWarning 또는 useEffect로 처리
function Greeting() {
const [timestamp, setTimestamp] = useState<number | null>(null)
useEffect(() => {
setTimestamp(Date.now())
}, [])
return <p>Timestamp: {timestamp ?? 'Loading...'}</p>
}
브레이크포인트 포인트
- App Router의 server component 데이터 fetch: 서버 측 console.log로 확인
- Route handler (
app/api/.../route.ts): VS Code 디버거 연결 - Client component 이벤트 핸들러: 브라우저 DevTools 브레이크포인트
- middleware.ts: Edge runtime에서 실행되므로 Node.js 디버거와 별도
프로파일링
# 번들 크기 분석
ANALYZE=true npm run build
# next.config.js에 @next/bundle-analyzer 설정 필요
- Next build output으로 각 페이지/라우트 크기 확인 (First Load JS)
- Chrome Performance로 hydration 비용 측정
- 서버 로그 + tracing으로 API 지연 원인 분리
프레임워크 공통 디버깅 플레이북
- 요청 ID 강제: 프론트
백엔드DB까지 하나의 ID로 추적. 분산 시스템에서는 필수다. - 경계에서 검증: 입력 DTO, 외부 응답, DB write 직전. 데이터가 깨지는 지점을 찾아라.
- 환경 분리 테스트: dev만 되는 버그 제거. 환경변수/시크릿/네트워크 차이를 점검한다.
- 관측 우선: 로그보다 메트릭/트레이싱 먼저 붙이기. 로그는 검색이 어렵다.
- 회귀 테스트화: 잡은 버그는 테스트로 고정. 같은 버그를 두 번 겪지 않기 위해.
종합 디버깅 체크리스트
장애 초기 대응
- 에러 메시지/스택 트레이스를 정확히 읽었는가?
- 최근 배포/변경사항과의 관련성을 확인했는가?
- 동일 입력으로 로컬에서 재현되는가?
- 요청 ID로 프론트
백엔드DB 전체 경로를 추적했는가?
브레이크포인트 배치
- 요청 진입점(Controller/View/Router)에 브레이크포인트를 설정했는가?
- 미들웨어/필터/인터셉터에서 요청이 정상 통과하는지 확인했는가?
- 데이터 접근 계층(Repository/ORM)에서 쿼리 결과를 확인했는가?
- 외부 API 호출 직전/직후를 확인했는가?
데이터베이스 디버깅
- SQL 로깅을 활성화했는가?
- N+1 쿼리가 발생하지 않는지 확인했는가?
- 슬로우 쿼리의 실행 계획(EXPLAIN)을 확인했는가?
- 트랜잭션 경계가 올바른지 확인했는가?
프론트엔드 (React/Next.js)
- 불필요 리렌더가 발생하지 않는지 React DevTools Profiler로 확인했는가?
- Hydration mismatch 경고가 없는지 확인했는가?
- 'use client' / 'use server' 경계가 올바른가?
- 번들 크기를 확인하고 불필요한 라이브러리를 제거했는가?
해결 후 후속 작업
- 근본 원인(root cause)을 문서화했는가?
- 회귀 테스트를 작성했는가?
- 모니터링/알림 규칙을 추가했는가?
- 팀에 postmortem을 공유했는가?
API 응답시간 이상 시 — 프레임워크별 첫 3분 행동
장애 발생 후 처음 3분은 원인 분류에 집중한다. 아래 표는 프레임워크별로 가장 먼저 실행해야 할 명령/확인 포인트를 정리한 것이다.
| 프레임워크 | 1분: 로그 확인 | 2분: 메트릭 확인 | 3분: 프로파일링 시작 |
|---|---|---|---|
| Spring Boot | logging.level.org.hibernate.SQL=DEBUG 확인, Tomcat access log | /actuator/metrics/http.server.requests p99 확인 | jcmd <PID> JFR.start duration=60s |
| Django | django-debug-toolbar SQL 패널, runserver 콘솔 로그 | Silk 요청별 응답시간 순위 | py-spy top --pid <PID> |
| FastAPI | uvicorn --log-level debug 출력, slow callback 경고 | OpenTelemetry trace 확인 | py-spy record -o flamegraph.svg --pid <PID> |
| React | 브라우저 Console 에러, Network 탭 응답시간 | React DevTools Profiler 렌더 횟수 | Lighthouse Performance 점수 |
| Next.js | 서버 터미널 console.log, 브라우저 Console | next build output (First Load JS 크기) | ANALYZE=true next build |
디버깅 도구 설치 원라이너
# Spring Boot — SQL 로깅 + 메트릭 (build.gradle에 추가)
# implementation 'io.micrometer:micrometer-registry-prometheus'
# implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
# Django — 디버그 도구 일괄 설치
pip install django-debug-toolbar django-silk nplusone
# FastAPI — 프로파일링 + 트레이싱
pip install py-spy opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-fastapi
# React — 리렌더 분석
npm install -D @welldone-software/why-did-you-render
# Next.js — 번들 분석
npm install -D @next/bundle-analyzer
결론
프레임워크 디버깅의 승패는 "어디에 브레이크포인트를 찍을지"에서 갈린다. 흐름을 이해하고 경계 지점을 잡으면, 복잡한 장애도 빠르게 분해할 수 있다.
문제는 프레임워크가 복잡해서 생기는 게 아니라, 실행 흐름이 보이지 않아서 커진다.
프레임워크의 요청 라이프사이클을 머릿속에 그릴 수 있으면, 브레이크포인트 한 개로 30분 삽질을 3분으로 줄일 수 있다.
다음 글에서는 IDE별 디버깅 완전정리를 다룬다. 동일한 장애라도 IDE 설정에 따라 진단 속도가 10배 이상 차이나는 이유를 정리했다. 실전 장애 사례가 궁금하다면 언어×프레임워크 장애 사례집을 참고한다.
Debugging by Framework: Spring Boot, Django/FastAPI, React/Next.js
This post is part 2 of a 5-part debugging series.
Why this guide matters
Most debugging failures are not caused by missing tools, but by missing workflow discipline:
- reproducibility (same input, same env, same version)
- observability (logs + metrics + traces + breakpoints)
- hypothesis-driven narrowing (one change at a time)
This article focuses on execution patterns that work under production pressure.
Practical workflow
- Freeze reproduction conditions.
- Add breakpoint/logpoints only on boundaries.
- Capture one profile (CPU or memory) before changing code.
- Verify fix with regression test + replay scenario.
- Record root cause and prevention rule in team docs.
Operational checklist
- Reproducible command/script exists.
- Failure signal is measurable (error rate/latency/memory).
- Profiling artifact is attached (flamegraph/heap/thread dump).
- Rollback strategy is prepared before risky deploy.
- Postmortem includes prevention action owner and due date.
Korean original
For deeper examples and Korean explanations, read the original:
Quiz
Q1: What is the main topic covered in "Debugging by Framework: Spring Boot, Django/FastAPI,
React/Next.js"?
Part 2 of the debugging series. A practical, copy-paste-friendly guide with reproducible steps, breakpoint strategy, profiling checkpoints, and team-level checklists.
Q2: Why this guide matters?
Most debugging failures are not caused by missing tools, but by missing workflow discipline:
reproducibility (same input, same env, same version) observability (logs + metrics + traces +
breakpoints) hypothesis-driven narrowing (one change at a time) This article focuses on execu...
Q3: Explain the core concept of Practical workflow.
Freeze reproduction conditions. Add breakpoint/logpoints only on boundaries. Capture one profile
(CPU or memory) before changing code. Verify fix with regression test + replay scenario. Record
root cause and prevention rule in team docs.
Q4: What are the key aspects of Operational checklist?
[ ] Reproducible
command/script exists. [ ] Failure signal is measurable (error rate/latency/memory). [ ] Profiling
artifact is attached (flamegraph/heap/thread dump). [ ] Rollback strategy is prepared before risky
deploy.
Q5: How does Korean original work?
For deeper examples and Korean explanations, read the original:
/blog/devops/2026-03-07-devops-debugging-by-framework-spring-django-fastapi-react-nextjs