Skip to content

✍️ 필사 모드: 프레임워크별 디버깅 실전: Spring Boot · Django/FastAPI · React/Next.js에서 브레이크포인트, 실행, 프로파일링

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

이 글은 디버깅 실전 시리즈 5편 중 2편이다.

  1. 언어별 디버깅 가이드
  2. 프레임워크별 디버깅 실전 ← 현재 글
  3. IDE별 디버깅 완전정리
  4. 언어×프레임워크 장애 사례집
  5. 원격 디버깅 실전 가이드

프레임워크 디버깅의 핵심

언어보다 먼저 봐야 할 건 프레임워크의 실행 흐름이다.

  • 요청 진입점(Controller/View/Route)
  • 미들웨어/필터
  • 데이터 접근 계층
  • 외부 API 호출
  • 렌더링/하이드레이션

디버깅은 이 흐름의 어느 단계에서 데이터가 깨지는지 찾는 작업이다. 프레임워크마다 이 흐름의 구조와 관례가 다르기 때문에, 디버깅 진입점도 달라진다.


프레임워크별 디버깅 진입점 요약

항목Spring BootDjangoFastAPIReactNext.js
요청 진입점@Controller/@RestControllerView 함수/CBVRouter 함수Component renderServer Component / Route Handler
미들웨어/필터Filter, InterceptorMiddleware 클래스Middleware (Starlette)-middleware.ts
데이터 접근JPA RepositoryDjango ORMSQLAlchemy/Tortoise-Prisma/Drizzle (서버)
에러 핸들링@ExceptionHandlerMiddleware / DRF exceptionexception_handler 데코레이터ErrorBoundaryerror.tsx / not-found.tsx
프로파일링 도구JFR, MicrometerDjango Debug Toolbar, py-spypy-spy, OpenTelemetryReact DevTools ProfilerNext.js build analyzer
디버그 로그 설정application.yml logging.levelLOGGING dict in settings.pylogging.basicConfig()React DevToolsnext.config.js logging

프레임워크별 흔한 장애 유형과 도구

프레임워크흔한 장애 유형진단 도구핵심 해결 패턴
Spring BootN+1 쿼리, 트랜잭션 경계 오류, Bean 순환 의존Hibernate SQL 로그, Micrometer, JFR@EntityGraph, @Transactional 범위 조정
DjangoN+1 쿼리, 시리얼라이저 유효성 오류, 마이그레이션 충돌django-debug-toolbar, Silk, assertNumQueriesselect_related/prefetch_related, 마이그레이션 squash
FastAPIPydantic 유효성 에러, 비동기/동기 혼용 블로킹, 의존성 주입 순서py-spy, OpenTelemetry, loggingrun_in_executor, Depends 체이닝 설계
React불필요 리렌더, 상태 관리 복잡도, 메모리 누수 (구독 해제 누락)React DevTools Profiler, why-did-you-rendermemo/useMemo, useEffect cleanup
Next.jsHydration 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

브레이크포인트 추천 위치

  1. Controller 진입: 요청 파라미터와 헤더 확인
  2. Service 비즈니스 분기: 핵심 로직과 조건 분기 확인
  3. Repository 쿼리 직전: 쿼리 파라미터와 결과 확인
  4. ExceptionHandler: 예외 원인과 응답 매핑 확인
  5. 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

자주 터지는 이슈

  1. 서버/클라이언트 렌더 불일치(hydration mismatch)
  2. API route 에러 삼킴: try/catch 없이 500 응답
  3. Edge/Node.js runtime 차이: Edge에서 지원 안 되는 API 사용
  4. 이미지/캐시 정책 오해: 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 지연 원인 분리

프레임워크 공통 디버깅 플레이북

  1. 요청 ID 강제: 프론트백엔드DB까지 하나의 ID로 추적. 분산 시스템에서는 필수다.
  2. 경계에서 검증: 입력 DTO, 외부 응답, DB write 직전. 데이터가 깨지는 지점을 찾아라.
  3. 환경 분리 테스트: dev만 되는 버그 제거. 환경변수/시크릿/네트워크 차이를 점검한다.
  4. 관측 우선: 로그보다 메트릭/트레이싱 먼저 붙이기. 로그는 검색이 어렵다.
  5. 회귀 테스트화: 잡은 버그는 테스트로 고정. 같은 버그를 두 번 겪지 않기 위해.

종합 디버깅 체크리스트

장애 초기 대응

  • 에러 메시지/스택 트레이스를 정확히 읽었는가?
  • 최근 배포/변경사항과의 관련성을 확인했는가?
  • 동일 입력으로 로컬에서 재현되는가?
  • 요청 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 Bootlogging.level.org.hibernate.SQL=DEBUG 확인, Tomcat access log/actuator/metrics/http.server.requests p99 확인jcmd <PID> JFR.start duration=60s
Djangodjango-debug-toolbar SQL 패널, runserver 콘솔 로그Silk 요청별 응답시간 순위py-spy top --pid <PID>
FastAPIuvicorn --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, 브라우저 Consolenext 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배 이상 차이나는 이유를 정리했다. 실전 장애 사례가 궁금하다면 언어×프레임워크 장애 사례집을 참고한다.

현재 단락 (1/341)

언어보다 먼저 봐야 할 건 **프레임워크의 실행 흐름**이다.

작성 글자: 0원문 글자: 13,825작성 단락: 0/341