Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

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

>

> 1. [언어별 디버깅 가이드](/blog/devops/2026-03-07-devops-debugging-by-language-python-javascript-go-java)

> 2. **[프레임워크별 디버깅 실전](/blog/devops/2026-03-07-devops-debugging-by-framework-spring-django-fastapi-react-nextjs)** ← 현재 글

> 3. [IDE별 디버깅 완전정리](/blog/devops/2026-03-07-devops-debugging-by-ide-vscode-intellij-pycharm)

> 4. [언어×프레임워크 장애 사례집](/blog/devops/2026-03-07-devops-debugging-casebook-language-framework-combos)

> 5. [원격 디버깅 실전 가이드](/blog/devops/2026-03-07-devops-remote-debugging-guide-ssh-tunnel-vscode-intellij-pycharm)

프레임워크 디버깅의 핵심

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

- 요청 진입점(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

브레이크포인트 추천 위치

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):

요청 진입 — 여기에 브레이크포인트

start = time.time()

response = self.get_response(request)

응답 직전 — 느린 요청 감지

duration = time.time() - start

if duration > 1.0: # 1초 초과 시 경고

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

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로 감싸기

@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 로깅 활성화

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)

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 (

{orders.map((order) => (

))}

)

}

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' // 반드시 파일 최상단에 선언

export function Counter() {

const [count, setCount] = useState(0)

return <button onClick={() => setCount((c) => c + 1)}>{count}</button>

}

// Route Handler (app/api/orders/route.ts)

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 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별 디버깅 완전정리](/blog/devops/2026-03-07-devops-debugging-by-ide-vscode-intellij-pycharm)를 다룬다. 동일한 장애라도 IDE 설정에 따라 진단 속도가 10배 이상 차이나는 이유를 정리했다.

> 실전 장애 사례가 궁금하다면 [언어×프레임워크 장애 사례집](/blog/devops/2026-03-07-devops-debugging-casebook-language-framework-combos)을 참고한다.

현재 단락 (1/338)

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

작성 글자: 0원문 글자: 13,777작성 단락: 0/338