Spring 백엔드 개발자의 Python 전환 가이드
Spring의 익숙한 개념을 FastAPI, Django, SQLAlchemy, Redis, Celery, Kafka에 대응해 보면서 Python 언어부터 비동기 처리, 트랜잭션, 작업 큐와 운영까지 익히는 전환 가이드.
Spring 백엔드 개발자의 Python 전환 가이드
한 줄 요약
API·마이크로서비스 중심이라면 FastAPI부터, 관리자·인증·권한·CRUD가 중심이라면 Django와 DRF부터 시작하는 편이 자연스럽습니다. 어느 경로든 Python의 런타임 타입 특성, 비동기 I/O, process 단위 상태, 명시적 transaction과 멱등한 작업 처리를 먼저 이해해야 합니다.
먼저 읽을 결론
스프링 개발자가 Python 백엔드로 전환할 때는 먼저 다음 두 경로를 구분하는 것이 좋습니다.
API·마이크로서비스·비동기 I/O 중심
→ FastAPI
관리자 화면·인증·ORM·CRUD가 통합된 업무 시스템
→ Django + Django REST Framework
스프링과 가장 비슷한 통합 프레임워크 경험은 Django가 제공하지만, 최근의 API 전용 백엔드 개발 방식과 가장 자연스럽게 연결되는 것은 FastAPI입니다.
전체 스택은 이렇게 대응할 수 있습니다.
Spring Boot
≈ FastAPI
+ Uvicorn
+ Pydantic
+ SQLAlchemy
+ Alembic
+ redis-py
+ Celery
+ confluent-kafka 또는 aiokafka
또는:
Spring Boot
≈ Django
+ Django REST Framework
+ Django ORM
+ Django Migration
+ Django Auth/Admin
+ Celery
+ Redis/Kafka Client
FastAPI는 Python 타입 힌트와 Pydantic을 기반으로 요청 검증과 OpenAPI 생성을 제공하고, Depends를 통한 의존성 그래프를 지원합니다. 다만 Depends는 Spring ApplicationContext와 같은 범용 IoC 컨테이너라기보다 요청 처리에 특화된 의존성 해결 시스템에 가깝습니다. (FastAPI)
왜 저장했나
Spring 개발자가 Python 백엔드로 옮겨 갈 때는 문법보다 프레임워크의 책임 범위와 런타임 차이를 먼저 파악해야 합니다. FastAPI의 Depends는 Spring IoC Container와 같지 않고, Python 타입 힌트는 외부 입력을 검증하지 않으며, async def도 내부 호출이 blocking이면 event loop를 막습니다.
이 문서는 FastAPI와 Django의 선택 기준부터 Pydantic, SQLAlchemy, Redis, Celery, Kafka, 테스트와 운영까지 한 흐름으로 묶습니다. 주문·결제·재고 시스템을 구현하는 전환 프로젝트도 포함해 각 개념을 실습으로 연결합니다.
정리한 질문
Spring 백엔드 개발자가 Python 기반 백엔드로 전환하려면 어떤 기본·심화·실무 개념을 익혀야 하는가? Spring과 대응되는 개념으로 FastAPI와 Django를 이해하되, SQLAlchemy·Redis·Celery·Kafka를 포함한 데이터 처리와 운영 설계까지 어떤 순서로 학습하면 좋은가?
1. FastAPI와 Django 중 무엇을 먼저 배워야 하는가
| 기준 | FastAPI | Django + DRF |
|---|---|---|
| 스프링에서 느껴지는 대응 | Spring MVC를 필요한 라이브러리와 조합하는 느낌 | Spring Boot + MVC + JPA + Security의 통합형 |
| 주 용도 | API, 마이크로서비스, AI 서비스, 비동기 I/O | 업무 시스템, 관리자 기능, 인증 중심 서비스, 모놀리스 |
| HTTP API | 기본 기능 | DRF 추가 |
| 요청 검증 | Pydantic | DRF Serializer |
| ORM | 별도 선택, 주로 SQLAlchemy | Django ORM 내장 |
| Migration | Alembic 등을 별도 사용 | 내장 |
| 인증·권한 | 직접 조합 | Django Auth와 DRF Permission |
| 관리자 화면 | 없음 | Django Admin 내장 |
| 의존성 주입 | Depends | 프레임워크 차원의 범용 DI 없음 |
| 비동기 지원 | 핵심 설계에 가까움 | 동기 기능과 비동기 기능이 혼재 |
| 구조 강제 | 약함 | Django App 단위 구조를 어느 정도 강제 |
| 학습 추천 | API 개발자는 우선 학습 | 업무·관리 시스템 개발자는 우선 학습 |
Django는 View, Middleware, ORM, Migration, Authentication, Session, Admin 등을 한 프레임워크 안에서 제공합니다. DRF를 추가하면 Serializer, APIView, ViewSet, Authentication, Permission, Throttling 등이 추가됩니다. DRF Serializer는 입력 역직렬화와 검증, 출력 직렬화를 모두 담당합니다. (Django Project)
실무에서는 이 순서로 전환하는 편이 효율적입니다.
Python 언어
→ FastAPI
→ Pydantic
→ SQLAlchemy
→ Redis
→ Celery
→ Kafka
→ 필요할 때 Django + DRF
2. Spring과 FastAPI·Django의 1:1 개념 대응
2.1 HTTP와 애플리케이션 구조
| Spring | FastAPI | Django + DRF |
|---|---|---|
| Spring Boot | FastAPI + Uvicorn | Django |
SpringApplication.run() | FastAPI() + ASGI Server 실행 | manage.py runserver 또는 ASGI/WSGI 서버 |
@RestController | APIRouter, Path Operation 함수 | APIView, ViewSet |
@GetMapping | @router.get() | get(), list() |
@PostMapping | @router.post() | post(), create() |
@RequestBody | Pydantic 모델 파라미터 | Serializer의 request.data |
@PathVariable | 함수 파라미터 | URL path converter |
@RequestParam | 함수 파라미터 또는 Query() | request.query_params |
ResponseEntity | 반환값 또는 Response | DRF Response |
@Valid | Pydantic 검증 | Serializer 검증 |
@ControllerAdvice | Exception Handler | DRF Exception Handler |
| Servlet Filter | ASGI Middleware | Django Middleware |
HandlerInterceptor | Middleware 또는 Dependency | Middleware 또는 APIView 정책 |
| Spring Security Filter Chain | Security Dependency | Authentication + Permission |
| Argument Resolver | Dependency 또는 사용자 정의 타입 | Parser, Serializer, View 로직 |
FastAPI Middleware는 요청 처리 전후에 실행할 공통 로직에 사용하고, 인증·권한은 Dependency로 분리하는 편이 자연스럽습니다. Django Middleware는 동기·비동기 요청을 모두 지원할 수 있지만, 동기와 비동기 사이의 변환이 발생하면 추가 비용이 생길 수 있습니다. (FastAPI)
2.2 DI와 객체 생명주기
| Spring | FastAPI·Python |
|---|---|
ApplicationContext | 정확한 1:1 대응 없음 |
| Spring Bean | 모듈 전역 객체, lifespan 객체, Dependency가 만든 객체 |
@Component, @Service | 일반 Python class |
@Bean | 팩토리 함수, lifespan 초기화 |
| Constructor Injection | 생성자 직접 호출 또는 Depends |
| Request Scope | 요청마다 실행되는 Dependency |
| Singleton Scope | 프로세스 단위 전역 인스턴스 |
ThreadLocal | ContextVar |
가장 큰 차이는 이것입니다.
FastAPI의
Depends는 애플리케이션의 모든 객체를 관리하는 Spring IoC Container라기보다, HTTP 요청에 필요한 객체를 만드는 의존성 그래프다.
동일 요청 안에서 같은 Dependency가 여러 번 필요하면 FastAPI는 기본적으로 한 번만 계산하여 요청 범위에서 재사용합니다. DB 세션처럼 정리 작업이 필요한 객체는 yield Dependency로 만들고, Redis·Kafka Producer와 같은 애플리케이션 자원은 lifespan에서 초기화하고 종료할 수 있습니다. (FastAPI)
2.3 데이터베이스와 미들웨어
| Spring | FastAPI·Python |
|---|---|
| JPA/Hibernate | SQLAlchemy ORM |
EntityManager | SQLAlchemy Session, AsyncSession |
| JPA Entity | SQLAlchemy mapped class |
| Spring Data Repository | 직접 작성한 Repository 또는 SQLAlchemy Query |
@Transactional | with session.begin() / async with session.begin() |
| Flyway/Liquibase | Alembic |
RedisTemplate | redis-py |
@Cacheable | 명시적인 Cache Service 또는 Decorator |
@Async | asyncio, FastAPI BackgroundTasks 또는 Celery |
@Scheduled | Cron, Celery Beat |
KafkaTemplate | Kafka Producer |
@KafkaListener | Kafka Consumer |
| Spring Batch | Celery Canvas, 별도 Batch Worker |
| MockMvc | FastAPI TestClient, HTTPX |
| JUnit | pytest |
2.4 Django 쪽 대응
| Spring | Django + DRF |
|---|---|
| JPA Entity | Django Model |
| Spring Data Repository | Manager, QuerySet |
@Transactional | @transaction.atomic |
@Valid DTO | DRF Serializer |
| Spring Security Authentication | DRF Authentication |
| Spring Security Authorization | DRF Permission |
@RestController | APIView, ViewSet |
| Flyway | Django Migration |
| Spring Session | Django Session |
| 운영 관리자 기능 | Django Admin |
| package-by-feature | Django App |
Django는 기본적으로 autocommit으로 동작하며, 명시적으로 transaction.atomic() 또는 @transaction.atomic을 사용해 트랜잭션 범위를 구성할 수 있습니다. (Django Project)
3. Python 기본 개념
3.1 Python 타입 힌트는 런타임 검증이 아니다
def create_user(email: str, age: int) -> None:
print(email, age)
다음 호출은 Python 런타임 자체에서는 자동 차단되지 않습니다.
create_user(email=123, age="twenty")
Python의 타입 어노테이션은 기본적으로 정적 타입 검사기, IDE, 린터를 위한 정보이며 런타임에서 함수 인자 타입을 강제하지 않습니다. (Python documentation)
두 도구를 구분해서 써야 합니다.
mypy / pyright
→ 코드 작성·CI 시점의 정적 타입 검사
Pydantic
→ HTTP, Kafka, Redis 등 외부 입력의 런타임 검증
Pydantic은 Python 타입 힌트를 기반으로 실제 입력 데이터를 검증하고 직렬화합니다. 기본 설정에서는 일부 타입 변환이 가능하기 때문에 중요한 경계에서는 strict mode 사용 여부도 검토해야 합니다. (pydantic.dev)
from pydantic import BaseModel, ConfigDict, EmailStr, Field
class CreateUserRequest(BaseModel):
model_config = ConfigDict(strict=True)
email: EmailStr
age: int = Field(ge=0)
Spring에 대응시키면 이렇습니다.
Java 컴파일러 타입 검사
→ mypy / pyright
Jackson 역직렬화
+ Bean Validation
→ Pydantic
3.2 Protocol은 Java interface와 비슷하다
from typing import Protocol
class OrderRepository(Protocol):
async def save(self, order: "Order") -> None:
...
async def find_by_id(self, order_id: str) -> "Order | None":
...
구현체가 명시적으로 OrderRepository를 상속하지 않아도 필요한 메서드 구조를 만족하면 정적 타입 검사상 호환될 수 있습니다.
class SqlAlchemyOrderRepository:
async def save(self, order: "Order") -> None:
...
async def find_by_id(self, order_id: str) -> "Order | None":
...
이는 Java의 명목적 타입 시스템보다 TypeScript interface에 가까운 구조적 타입 방식입니다. Python의 Protocol은 구조적 서브타이핑을 지원합니다. (Python documentation)
3.3 Python Decorator는 Java Annotation과 다르다
@router.get("/orders")
async def get_orders():
...
Java Annotation은 주로 메타데이터이고 Spring이 이를 읽어 동작합니다.
Python Decorator는 더 직접적입니다.
get_orders = router.get("/orders")(get_orders)
즉, Decorator는 함수를 받아 다른 함수나 객체로 변환하는 실행 가능한 코드입니다. import 시점에 Decorator 표현식이 실행될 수도 있으므로 순환 import와 초기화 순서를 주의해야 합니다. (Python documentation)
3.4 Mutable Default Argument
Python 초심자가 자주 만드는 버그입니다.
def add_item(item: str, items: list[str] = []):
items.append(item)
return items
기본값 객체는 호출 때마다 새로 만들어지는 것이 아니라 재사용됩니다.
add_item("A") # ["A"]
add_item("B") # ["A", "B"]
다음처럼 작성해야 합니다.
def add_item(
item: str,
items: list[str] | None = None,
) -> list[str]:
if items is None:
items = []
items.append(item)
return items
dataclass에서는 default_factory를 사용합니다.
from dataclasses import dataclass, field
@dataclass
class Order:
items: list[str] = field(default_factory=list)
Python 문서도 mutable 기본값 공유를 방지하기 위해 dataclass의 default_factory 사용을 설명합니다. (Python documentation)
3.5 모델을 세 종류로 분리한다
스프링에서도 Entity와 DTO를 분리하듯 Python에서도 다음을 구분하는 것이 좋습니다.
| 역할 | 추천 타입 |
|---|---|
| HTTP·Kafka 요청/응답 | Pydantic BaseModel |
| 도메인 객체·값 객체 | 일반 class 또는 dataclass |
| 데이터베이스 영속 객체 | SQLAlchemy mapped class 또는 Django Model |
# API DTO
class CreateOrderRequest(BaseModel):
product_id: str
quantity: int
# Domain
@dataclass(frozen=True)
class Order:
id: str
product_id: str
quantity: int
# Persistence
class OrderEntity(Base):
__tablename__ = "orders"
...
다음처럼 하나의 클래스에 모든 책임을 넣는 것은 피하는 편이 좋습니다.
Pydantic DTO
= Domain Entity
= SQLAlchemy Entity
= Kafka Event
초기에는 편하지만 API 스키마 변경, DB 스키마 변경, 이벤트 버전 변경이 서로 결합됩니다.
4. FastAPI의 DI를 Spring 관점으로 이해하기
4.1 DB Session Dependency
Spring에서는 EntityManager나 Repository를 Bean으로 주입받습니다.
FastAPI에서는 요청별 세션을 yield Dependency로 만듭니다.
from collections.abc import AsyncIterator
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
async_session_factory: async_sessionmaker[AsyncSession]
async def get_session() -> AsyncIterator[AsyncSession]:
async with async_session_factory() as session:
yield session
from typing import Annotated
from fastapi import Depends
SessionDep = Annotated[AsyncSession, Depends(get_session)]
@router.get("/orders/{order_id}")
async def get_order(
order_id: str,
session: SessionDep,
):
...
개념 흐름은 이렇습니다.
FastAPI가 요청 시작
→ AsyncSession 생성
→ endpoint와 하위 Dependency에 전달
→ 요청 처리
→ yield 이후 코드 실행
→ AsyncSession close
4.2 Service 생성
class OrderService:
def __init__(
self,
session: AsyncSession,
repository: OrderRepository,
) -> None:
self._session = session
self._repository = repository
async def create(
self,
command: CreateOrderCommand,
) -> Order:
async with self._session.begin():
order = Order.create(
product_id=command.product_id,
quantity=command.quantity,
)
await self._repository.save(order)
return order
def get_order_service(
session: SessionDep,
) -> OrderService:
repository = SqlAlchemyOrderRepository(session)
return OrderService(
session=session,
repository=repository,
)
OrderServiceDep = Annotated[
OrderService,
Depends(get_order_service),
]
@router.post("/orders")
async def create_order(
request: CreateOrderRequest,
service: OrderServiceDep,
) -> OrderResponse:
order = await service.create(
CreateOrderCommand(
product_id=request.product_id,
quantity=request.quantity,
)
)
return OrderResponse.from_domain(order)
Spring과 비교하면 이렇습니다.
Spring
ApplicationContext가 시작할 때 Bean graph 구성
FastAPI
요청이 들어올 때 필요한 Dependency graph 계산
대규모 프로젝트에서 서비스와 Repository 생성 코드가 지나치게 복잡해지면 별도 DI Container를 도입할 수 있지만, 무조건 필요한 것은 아닙니다. Factory 함수와 Depends만으로도 대부분의 API 애플리케이션을 충분히 구성할 수 있습니다.
5. Python 비동기와 동시성
5.1 Node.js와 비슷하지만 완전히 같지는 않다
FastAPI의 비동기 처리 기반은 Python의 asyncio입니다.
Node.js
→ V8 + libuv Event Loop
Python FastAPI
→ asyncio Event Loop + ASGI Server
asyncio는 async/await 문법으로 동시성 코드를 작성하기 위한 표준 라이브러리입니다. (Python documentation)
@router.get("/orders/{order_id}")
async def get_order(order_id: str):
order = await repository.find_by_id(order_id)
return order
await는 새로운 스레드를 만드는 것이 아닙니다.
DB 요청 시작
→ 현재 Coroutine 실행 양보
→ Event Loop가 다른 요청 처리
→ DB 응답 도착
→ 기존 Coroutine 재개
5.2 async def 안에서 동기 I/O를 호출하면 안 된다
잘못된 예:
import requests
@router.get("/external")
async def call_external():
response = requests.get("https://example.com")
return response.json()
requests.get()은 동기 blocking 호출입니다. 이 호출이 끝날 때까지 해당 worker의 event loop가 막힐 수 있습니다.
비동기 HTTP Client를 사용합니다.
import httpx
@router.get("/external")
async def call_external():
async with httpx.AsyncClient() as client:
response = await client.get(
"https://example.com",
timeout=3.0,
)
return response.json()
또는 사용하려는 라이브러리가 동기 API만 제공한다면 path operation을 일반 def로 작성하거나 thread offloading을 명시적으로 사용합니다. FastAPI는 await를 지원하는 라이브러리를 사용할 때 async def, 동기 blocking 라이브러리를 사용할 때 일반 def를 선택할 수 있도록 지원합니다. (FastAPI)
5.3 async def가 무조건 더 빠르지 않다
| 작업 | 권장 방식 |
|---|---|
| 비동기 DB·HTTP·Redis 호출 | async def + await |
| 동기 SDK·동기 DB Driver | 일반 def 또는 thread offloading |
| 짧은 CPU 계산 | 요청 worker에서 처리 가능 |
| 무거운 CPU 계산 | Process Worker 또는 Celery |
| 내구성 불필요한 작은 후처리 | FastAPI BackgroundTasks |
| 재시도·내구성이 필요한 작업 | Celery |
| 장기 비즈니스 이벤트 | Kafka |
CPU 집약적 계산은 multiprocessing이나 별도 worker process를 통해 분리하는 것이 일반적입니다. Python의 multiprocessing은 subprocess를 사용해 여러 CPU를 활용할 수 있습니다. (Python documentation)
5.4 여러 Uvicorn Worker는 메모리를 공유하지 않는다
uvicorn app.main:app --workers 4
이 설정은 하나의 애플리케이션에 스레드를 네 개 추가하는 것이 아니라, worker process를 여러 개 실행합니다. Uvicorn은 여러 worker process 실행을 지원하고, FastAPI 배포 문서도 멀티코어 활용을 위해 process replication을 설명합니다. (FastAPI)
다음 객체는 worker마다 별도로 존재합니다.
local_cache: dict[str, object] = {}
Worker 1 local_cache
Worker 2 local_cache
Worker 3 local_cache
Worker 4 local_cache
Spring 기준으로 보면 이렇습니다.
Spring singleton
→ 해당 JVM 안에서 하나
Python module-level singleton
→ 해당 Python process 안에서 하나
여러 process·pod에서 공유해야 하는 데이터는 Redis, DB, Kafka 같은 외부 저장소를 사용해야 합니다.
또한 worker가 8개이고 worker당 DB pool이 10이면 애플리케이션 하나가 최대 약 80개의 DB connection을 사용할 수 있습니다.
5.5 asyncio.gather()와 DB Session
await asyncio.gather(
repository.find_by_id("1"),
repository.find_by_id("2"),
repository.find_by_id("3"),
)
이 코드는 세 작업이 동일한 AsyncSession을 사용한다면 안전하지 않을 수 있습니다.
SQLAlchemy Session과 AsyncSession은 하나의 상태 있는 트랜잭션을 나타내며, 여러 thread나 asyncio task가 동일한 인스턴스를 동시에 공유하도록 설계되지 않았습니다. 기본 원칙은 Session per thread, AsyncSession per task입니다. (docs.sqlalchemy.org)
트랜잭션 안에서 SQL을 무조건 병렬 실행하려고 하지 않는 편이 좋습니다.
같은 트랜잭션 안의 DB 작업
→ 일반적으로 순차 실행
독립적인 병렬 작업
→ task마다 별도 Session과 별도 transaction
그리고 별도 세션을 여러 개 사용하면 connection pool을 더 많이 소비합니다. 실제로 빠른지는 반드시 측정해야 합니다.
5.6 ThreadLocal 대신 ContextVar
Request ID나 Trace ID를 하위 호출로 전달할 때 사용할 수 있습니다.
from contextvars import ContextVar
request_id_var: ContextVar[str | None] = ContextVar(
"request_id",
default=None,
)
@app.middleware("http")
async def request_context_middleware(request, call_next):
request_id = request.headers.get(
"X-Request-ID",
generate_request_id(),
)
token = request_id_var.set(request_id)
try:
return await call_next(request)
finally:
request_id_var.reset(token)
ContextVar는 비동기 실행 컨텍스트 안에서 context-local 상태를 관리하기 위한 표준 기능이며, 동시성 코드에서 threading.local()의 값이 예상하지 못한 곳으로 섞이는 것을 방지하는 용도로 사용됩니다. (Python documentation)
다만 도메인 로직에 중요한 값은 가능하면 명시적인 인자로 전달하고, ContextVar는 로깅·추적 정보에 주로 사용하는 편이 좋습니다.
6. SQLAlchemy를 JPA 기준으로 이해하기
6.1 개념 대응
| JPA/Hibernate | SQLAlchemy |
|---|---|
| Entity | Mapped Class |
EntityManager | Session / AsyncSession |
| Persistence Context | Session의 Identity Map과 Unit of Work |
| JPQL | SQLAlchemy select() |
| Criteria API | SQL Expression API |
| Lazy Loading | Relationship Lazy Loading |
| Fetch Join | joinedload() |
| Batch Relation Loading | selectinload() |
persist() | session.add() |
| Flush | session.flush() |
| Transaction Commit | session.commit() |
@Version | Version Column 설정 |
| Flyway | Alembic |
SQLAlchemy는 단순 Query Builder가 아니라 Session, Identity Map, Unit of Work를 갖는 ORM이므로 JPA와 공통점이 많습니다.
하지만 Spring처럼 @Transactional 프록시가 자동으로 모든 계층에 적용된다고 생각하면 안 됩니다.
6.2 Transaction
async def create_order(
self,
command: CreateOrderCommand,
) -> Order:
async with self._session.begin():
order = OrderEntity(
id=generate_id(),
product_id=command.product_id,
quantity=command.quantity,
)
self._session.add(order)
self._session.add(
OutboxEventEntity(
event_id=generate_id(),
aggregate_id=order.id,
event_type="order.created.v1",
payload={
"orderId": order.id,
},
)
)
return to_domain(order)
async with session.begin() 범위를 벗어날 때 성공하면 commit되고 예외가 발생하면 rollback됩니다.
중요한 점은 트랜잭션 중인 session을 모든 Repository가 공유해야 한다는 것입니다.
# 위험
async with session.begin():
await another_repository_using_other_session.save(order)
# 권장
async with session.begin():
repository = SqlAlchemyOrderRepository(session)
await repository.save(order)
Spring의 propagation에 의존하기보다 transaction context를 명시적으로 전달한다고 이해하면 쉽습니다.
6.3 Repository 구현
from sqlalchemy import select
class SqlAlchemyOrderRepository:
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def save(self, order: Order) -> None:
entity = OrderEntity.from_domain(order)
self._session.add(entity)
await self._session.flush()
async def find_by_id(
self,
order_id: str,
) -> Order | None:
statement = select(OrderEntity).where(
OrderEntity.id == order_id
)
entity = await self._session.scalar(statement)
if entity is None:
return None
return entity.to_domain()
Repository 내부에서 매번 commit하지 않는 것이 좋습니다.
Repository
→ SQL 실행과 Entity 변환
Application Service
→ transaction 경계 결정
Spring Data Repository의 save()가 transaction context 안에서 동작하는 것과 동일한 방향입니다.
6.4 N+1
SQLAlchemy:
statement = (
select(OrderEntity)
.options(selectinload(OrderEntity.items))
)
Django:
orders = Order.objects.prefetch_related("items")
DRF는 Serializer 관계를 보고 QuerySet을 자동으로 최적화하지 않으므로, 개발자가 select_related 또는 prefetch_related를 명시해야 합니다. (Django REST Framework)
7. Django와 DRF를 Spring 기준으로 이해하기
Django는 Entity 중심이라기보다 Active Record에 가까운 Model 중심 프레임워크입니다.
class Order(models.Model):
product_id = models.CharField(max_length=100)
quantity = models.PositiveIntegerField()
status = models.CharField(max_length=30)
조회:
order = await Order.objects.aget(id=order_id)
또는 동기 코드:
order = Order.objects.get(id=order_id)
DRF Serializer
from rest_framework import serializers
class CreateOrderSerializer(serializers.Serializer):
product_id = serializers.CharField()
quantity = serializers.IntegerField(min_value=1)
serializer = CreateOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
validated = serializer.validated_data
대응 관계는 이렇습니다.
Spring Request DTO
+ Jackson
+ Bean Validation
≈ DRF Serializer
APIView
from django.db import transaction
from rest_framework.response import Response
from rest_framework.views import APIView
class OrderView(APIView):
@transaction.atomic
def post(self, request):
serializer = CreateOrderSerializer(
data=request.data,
)
serializer.is_valid(raise_exception=True)
order = Order.objects.create(
product_id=serializer.validated_data[
"product_id"
],
quantity=serializer.validated_data[
"quantity"
],
status="CREATED",
)
return Response({
"id": order.id,
"status": order.status,
})
다음 요구가 많다면 Django가 특히 유리합니다.
사용자·그룹·권한
관리자 화면
CRUD
파일 업로드
세션 인증
백오피스
콘텐츠 관리
반대로 작은 API 서비스마다 Django의 전체 기능을 올리는 것이 과할 수 있다면 FastAPI가 더 적합합니다.
8. Redis
8.1 클라이언트 대응
RedisTemplate
→ redis-py
ReactiveRedisTemplate
→ redis-py asyncio API
redis-py는 asyncio 기반 non-blocking Redis 접근도 지원합니다. (Redis)
애플리케이션 시작 시 Redis Client를 만들고 종료 시 close합니다.
from contextlib import asynccontextmanager
from fastapi import FastAPI
from redis.asyncio import Redis
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.redis = Redis.from_url(
"redis://localhost:6379",
decode_responses=True,
)
yield
await app.state.redis.aclose()
app = FastAPI(lifespan=lifespan)
8.2 Cache-Aside
import json
from redis.asyncio import Redis
class OrderCacheRepository:
def __init__(self, redis: Redis) -> None:
self._redis = redis
async def get(
self,
order_id: str,
) -> OrderResponse | None:
value = await self._redis.get(
f"order:v1:{order_id}"
)
if value is None:
return None
return OrderResponse.model_validate_json(value)
async def set(
self,
order: OrderResponse,
) -> None:
await self._redis.set(
f"order:v1:{order.id}",
order.model_dump_json(),
ex=60,
)
async def delete(self, order_id: str) -> None:
await self._redis.delete(
f"order:v1:{order_id}"
)
조회:
async def find_order(
order_id: str,
) -> OrderResponse:
cached = await cache.get(order_id)
if cached is not None:
return cached
order = await repository.find_by_id(order_id)
if order is None:
raise OrderNotFound(order_id)
response = OrderResponse.from_domain(order)
await cache.set(response)
return response
수정:
1. DB transaction commit
2. Redis key 삭제
3. 다음 조회에서 DB로부터 cache 재구성
주의할 점은 Node.js와 동일합니다.
TTL
TTL jitter
negative cache
cache stampede
키 버전
직렬화 스키마 버전
Redis 장애 시 fallback
cache hit ratio
8.3 Redis, Celery, Kafka의 역할 차이
| 도구 | 핵심 목적 |
|---|---|
| Redis Cache | 반복 조회 가속 |
| Redis Session | 여러 애플리케이션 인스턴스 간 세션 공유 |
| Redis Pub/Sub | 일시적인 실시간 broadcast |
| Redis Streams | 비교적 단순한 내구성 스트림 |
| Celery | 실행해야 할 작업·명령 전달 |
| Kafka | 재처리 가능한 비즈니스 이벤트 로그 |
예를 들어:
회원 가입 후 이메일 발송
→ Celery Task
주문 생성 사실을 결제·재고·분석 서비스가 각각 소비
→ Kafka Event
상품 상세 조회 결과
→ Redis Cache
Celery를 Kafka 대신 사용하거나 Kafka를 Celery 대신 사용하는 것이 항상 올바른 것은 아닙니다.
Celery 메시지
→ “이 작업을 실행하라”
Kafka 이벤트
→ “이 사실이 발생했다”
9. Celery
9.1 Spring 개념 대응
@Async
+ Task Executor
+ Queue
+ Retry
+ Scheduler
≈ Celery
Celery의 일반적인 용도는 이렇습니다.
이메일·SMS 발송
이미지·영상 변환
PDF 생성
외부 API 연동
데이터 집계
주기적 Batch
AI 추론 작업
9.2 FastAPI BackgroundTasks와 Celery
FastAPI BackgroundTasks:
@router.post("/notifications")
async def send_notification(
background_tasks: BackgroundTasks,
):
background_tasks.add_task(
send_email,
"user@example.com",
)
return {"accepted": True}
이 작업은 응답 후 동일 애플리케이션 process에서 실행됩니다.
따라서 다음 보장은 없습니다.
프로세스가 종료되어도 반드시 실행
여러 서버에 작업 분산
영구적인 retry
작업 상태 추적
FastAPI 공식 문서도 무거운 연산이나 여러 process·server에 분산해야 하는 작업에는 Celery와 같은 별도 작업 큐 사용을 권장합니다. (FastAPI)
9.3 Celery Task
from celery import Celery
celery_app = Celery(
"worker",
broker="redis://localhost:6379/0",
)
@celery_app.task(
bind=True,
autoretry_for=(TemporaryExternalApiError,),
retry_backoff=True,
retry_jitter=True,
max_retries=5,
acks_late=True,
)
def send_order_email(
self,
order_id: str,
) -> None:
email_service.send_for_order(order_id)
acks_late=True를 사용하면 task 완료 후 acknowledgement하도록 구성할 수 있지만, worker 장애 시 task가 다시 실행될 가능성이 있으므로 task는 멱등해야 합니다. Celery 문서도 late acknowledgement를 사용할 때 task의 멱등성을 요구합니다. (Celery Documentation)
구현은 이렇게 가져갑니다.
나쁜 구현:
이메일 task 실행 때마다 무조건 발송
권장 구현:
notification_id에 unique constraint
→ 이미 발송했으면 종료
→ 미발송이면 상태 기록 후 발송
10. Python과 Kafka
10.1 클라이언트 선택
confluent-kafka
- librdkafka 기반
- Producer, Consumer, AdminClient 제공
- 높은 성능과 풍부한 Kafka 기능이 중요한 경우
- poll loop와 callback 모델을 이해해야 함
Confluent의 Python Client는 Producer, Consumer, AdminClient를 제공하는 Kafka 클라이언트입니다. (Confluent Docs)
aiokafka
asyncio기반 인터페이스- FastAPI와 같은 async 애플리케이션에서 이해하기 쉬움
async for기반 Consumer- manual commit과 rebalance 처리를 직접 설계
from aiokafka import AIOKafkaConsumer
consumer = AIOKafkaConsumer(
"order.created.v1",
bootstrap_servers="localhost:9092",
group_id="payment-service",
enable_auto_commit=False,
)
aiokafka는 asyncio 기반 Producer와 Consumer를 제공하며, manual commit 시 처리 완료 후 offset을 commit하도록 구성할 수 있습니다. (aiokafka.readthedocs.io)
실무에서는 조직의 Kafka 운영 표준과 지원 정책에 따라 선택하는 것이 좋습니다.
10.2 Kafka Consumer는 FastAPI HTTP 프로세스와 분리하는 것이 좋다
가능한 구조:
order-api
→ FastAPI HTTP Server
order-event-consumer
→ 별도 Python Process
celery-worker
→ 별도 Python Process
하나의 FastAPI process 안에서 HTTP Server와 Kafka Consumer를 모두 시작할 수도 있지만 다음 문제가 복잡해집니다.
worker 수만큼 Consumer가 생성됨
lifespan 시작·종료 관리
rebalance
graceful shutdown
HTTP 부하와 Consumer CPU 경쟁
배포 시 메시지 처리 중단
작은 서비스에서는 함께 둘 수 있지만, 운영에서는 실행 단위를 분리하는 편이 확장과 장애 격리에 유리합니다.
10.3 이벤트 런타임 검증
Python 타입 힌트만으로 Kafka payload는 검증되지 않습니다.
class OrderCreatedV1(BaseModel):
event_id: str
event_type: Literal["order.created.v1"]
occurred_at: datetime
order_id: str
version: Literal[1]
event = OrderCreatedV1.model_validate_json(
message.value
)
HTTP DTO와 Kafka Event를 같은 Pydantic Model로 무조건 재사용하기보다, 이벤트 계약을 별도 모델로 관리해야 합니다.
HTTP CreateOrderRequest
→ 현재 API 호출을 위한 입력
OrderCreatedV1
→ 장기간 유지해야 할 서비스 간 계약
10.4 Manual Commit
await consumer.start()
try:
async for message in consumer:
event = OrderCreatedV1.model_validate_json(
message.value
)
async with session_factory() as session:
async with session.begin():
inserted = await inbox.try_insert(
session=session,
event_id=event.event_id,
)
if inserted:
await payment_service.process(
session=session,
event=event,
)
await consumer.commit()
finally:
await consumer.stop()
처리 순서:
1. Kafka 메시지 수신
2. Pydantic schema 검증
3. DB transaction
4. Inbox 중복 검사
5. 비즈니스 로직 처리
6. DB commit
7. Kafka offset commit
DB commit 후 offset commit 전에 process가 종료되면 같은 이벤트가 다시 전달될 수 있습니다. 따라서 Inbox 또는 idempotency key가 필요합니다.
Kafka는 기본적으로 at-least-once 처리 모델을 제공하고, 외부 DB까지 포함한 exactly-once 결과에는 외부 시스템과의 협력이 필요합니다. (Apache Kafka)
aiokafka에서 manual commit을 사용할 때는 rebalance 중 아직 처리 중인 메시지와 offset commit을 안전하게 관리하기 위해 ConsumerRebalanceListener도 고려해야 합니다. (aiokafka.readthedocs.io)
10.5 Outbox와 Inbox
Python에서도 Node.js·Spring과 동일합니다.
async with session.begin():
session.add(order_entity)
session.add(
OutboxEventEntity(
event_id=event_id,
aggregate_id=order_id,
event_type="order.created.v1",
payload=event.model_dump(),
)
)
Order API
└─ PostgreSQL Transaction
├─ orders INSERT
└─ outbox_events INSERT
↓
Outbox Publisher
↓
Kafka
↓
Python Consumer
↓
Inbox + Business DB
Outbox publisher가 같은 이벤트를 중복 발행할 수 있으므로 Consumer는 event_id를 기준으로 멱등하게 처리해야 합니다.
11. 권장 FastAPI 프로젝트 구조
NestJS처럼 모듈 구조를 프레임워크가 강제하지 않으므로 팀에서 규칙을 정해야 합니다.
app/
main.py
common/
exceptions.py
middleware.py
context.py
logging.py
infrastructure/
database.py
redis.py
kafka.py
celery.py
orders/
api/
router.py
schemas.py
dependencies.py
application/
commands.py
service.py
domain/
order.py
repository.py
exceptions.py
infrastructure/
models.py
sqlalchemy_repository.py
cache_repository.py
event_publisher.py
payments/
...
Spring에 대응시키면 이렇습니다.
api
→ Controller + DTO
application
→ Application Service + Use Case
domain
→ Entity + Value Object + Repository Port
infrastructure
→ JPA Repository 구현체 + Redis + Kafka Adapter
FastAPI Router에는 HTTP 관련 코드만 둡니다.
@router.post("/orders")
async def create_order(...):
return await service.create(...)
다음은 Router에 두지 않습니다.
복잡한 트랜잭션
Redis 캐시 정책
Kafka 발행
외부 API retry
도메인 상태 전이
12. 테스트
12.1 단위 테스트
Spring의 JUnit + Mockito에 대응합니다.
import pytest
@pytest.mark.asyncio
async def test_create_order():
repository = FakeOrderRepository()
session = FakeTransactionManager()
service = OrderService(
session=session,
repository=repository,
)
order = await service.create(
CreateOrderCommand(
product_id="P-1",
quantity=2,
)
)
assert order.quantity == 2
Python에서는 Mock을 남용하기보다 간단한 Fake 구현체를 만드는 방식도 많이 사용합니다.
class FakeOrderRepository:
def __init__(self) -> None:
self.orders: dict[str, Order] = {}
async def save(self, order: Order) -> None:
self.orders[order.id] = order
12.2 API 테스트
import pytest
from httpx import ASGITransport, AsyncClient
@pytest.mark.anyio
async def test_create_order():
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as client:
response = await client.post(
"/orders",
json={
"product_id": "P-1",
"quantity": 2,
},
)
assert response.status_code == 201
FastAPI는 비동기 테스트에서 HTTPX AsyncClient와 AnyIO 기반 테스트 구성을 안내합니다. (FastAPI)
12.3 통합 테스트
다음은 실제 인프라로 검증하는 것이 좋습니다.
PostgreSQL transaction
Alembic migration
Redis TTL
Kafka serialization
Consumer offset
Celery retry
Unique constraint
Outbox/Inbox
Mock으로만 테스트하면 다음 문제를 발견하기 어렵습니다.
동일 transaction에 참여하지 않는 Repository
AsyncSession 동시 사용
실제 DB constraint 오류
Redis 직렬화 호환성
Kafka rebalance와 중복 처리
Celery task 재실행
13. 운영 개념
13.1 Lifespan
DB Engine, Redis, Kafka Producer 같은 자원은 startup 시 생성하고 shutdown 시 정리합니다.
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.redis = create_redis()
app.state.kafka = create_kafka_producer()
await app.state.kafka.start()
yield
await app.state.kafka.stop()
await app.state.redis.aclose()
await database_engine.dispose()
FastAPI는 lifespan context manager를 startup·shutdown 로직의 권장 방식으로 제공합니다. (FastAPI)
13.2 Graceful Shutdown
1. 신규 HTTP 요청 중단
2. 진행 중인 요청 완료
3. Kafka Consumer polling 중단
4. 처리 중인 메시지 DB commit
5. offset commit
6. Kafka Producer flush
7. Redis connection 종료
8. DB pool dispose
9. process 종료
13.3 관측성
Python 서비스에서는 아래 지표를 확인합니다.
| 계층 | 주요 지표 |
|---|---|
| HTTP | 요청 수, 오류율, p95·p99 latency |
| ASGI | active request, event-loop delay |
| Process | CPU, RSS, worker restart |
| DB | pool active/waiting, query latency |
| Redis | hit ratio, command latency, eviction |
| Kafka | consumer lag, rebalance, retry, DLT |
| Celery | queue length, active task, retry, failed task |
여러 worker가 있으면 각 process가 별도 heap과 DB pool을 갖는다는 점을 모니터링과 용량 계산에 반영해야 합니다.
14. Spring 개발자가 Python에서 자주 하는 실수
| 잘못된 생각 | 실제 동작 |
|---|---|
| 타입 힌트가 잘못된 입력을 차단한다 | 기본 Python 런타임은 타입을 강제하지 않는다 |
| Pydantic은 정적 타입 검사기다 | Pydantic은 런타임 검증 도구다 |
async def로 바꾸면 코드가 빨라진다 | 내부 호출도 non-blocking이어야 효과가 있다 |
async def 안에서 requests를 호출해도 된다 | event loop를 막을 수 있다 |
await는 별도 thread에서 실행한다 | Coroutine이 실행을 양보하는 것이다 |
| 전역 dict는 모든 worker가 공유한다 | process마다 별도 복사본이다 |
FastAPI의 Depends는 Spring Container와 같다 | 요청 중심 Dependency Resolver다 |
하나의 AsyncSession을 gather()에서 공유해도 된다 | concurrent task 간 공유하면 안전하지 않다 |
| Repository에서 매번 commit하면 편하다 | 유스케이스 transaction을 분리시킨다 |
BackgroundTasks는 작업을 보장한다 | process 종료 시 작업을 잃을 수 있다 |
| Celery task는 한 번만 실행된다 | retry와 장애로 중복 실행될 수 있다 |
| Kafka offset을 먼저 commit해도 된다 | 처리 전 commit하면 메시지를 잃을 수 있다 |
| Redis와 Kafka와 Celery는 모두 Queue다 | 각각 캐시·작업·이벤트라는 다른 목적이 있다 |
| Decorator는 Java Annotation과 같다 | Python Decorator는 실제 함수 변환 코드다 |
| ORM Model을 API 응답으로 바로 노출해도 된다 | API·도메인·DB 모델이 강하게 결합된다 |
items=[]는 호출마다 새 List다 | 같은 mutable 객체가 재사용된다 |
15. 추천 학습 순서
1단계: Python 언어
먼저 익힐 항목입니다.
list, dict, set, tuple
comprehension
iterator, generator
decorator
context manager
exception
dataclass
typing
Protocol
Generic
None
mutable object
module과 import
virtual environment
특히 다음 네 가지를 코드로 확인해야 합니다.
타입 힌트는 런타임 강제가 아님
mutable default argument
Decorator는 함수 변환
module 전역 상태는 process-local
2단계: FastAPI
APIRouter
Pydantic Request/Response
Depends
Exception Handler
Middleware
Security Dependency
Lifespan
HTTPX Test
기존 Spring CRUD API 하나를 FastAPI로 포팅하면 좋습니다.
3단계: SQLAlchemy
Mapped Class
AsyncSession
select()
Relationship
Transaction
Migration
N+1
Optimistic Lock
Connection Pool
4단계: Redis
Cache-Aside
TTL
Invalidation
Idempotency Key
Rate Limit
Session
Distributed Lock
5단계: Celery
Task
Broker
Worker
Retry
acks_late
Idempotency
Celery Beat
Task Monitoring
6단계: Kafka
Producer
Consumer
Partition
Key
Consumer Group
Offset
Manual Commit
Rebalance
Retry Topic
DLT
Outbox
Inbox
7단계: Django + DRF
다음 요구가 있는 프로젝트를 만들어 봅니다.
사용자 로그인
그룹·권한
관리자 화면
CRUD API
파일 업로드
검색·필터
Pagination
16. 전환용 프로젝트 구성
기존에 제안한 주문·결제·재고 프로젝트를 Python으로 구현하면 좋습니다.
order-api
- FastAPI
- Pydantic
- SQLAlchemy
- PostgreSQL
- Redis Cache
- Outbox
payment-consumer
- aiokafka 또는 confluent-kafka
- Inbox
- PostgreSQL
stock-consumer
- Kafka Consumer
- Optimistic Lock
- Retry/DLT
notification-worker
- Celery
- Redis 또는 RabbitMQ Broker
- 이메일·SMS
- 멱등 처리
admin
- Django + DRF
- 주문·결제·재고 운영 관리
전체 흐름:
Client
↓
FastAPI Order API
↓
Pydantic Validation
↓
Application Service
↓
SQLAlchemy Transaction
├─ orders
└─ outbox_events
↓
Outbox Publisher
↓
Kafka
┌────┴─────┐
↓ ↓
Payment Stock
Consumer Consumer
│ │
Inbox Inbox
└────┬─────┘
↓
상태 변경 Event
↓
Celery
↓
Email / SMS
조회:
Client → FastAPI → Redis → PostgreSQL
17. 최종 정리
스프링 개발자가 Python 백엔드로 전환할 때 가장 중요한 대응 관계는 이렇습니다.
Spring MVC
→ FastAPI APIRouter 또는 DRF APIView
Bean Validation
→ Pydantic 또는 DRF Serializer
Spring Bean
→ 일반 Python 객체 + Dependency Factory
ApplicationContext
→ 정확한 대응 없음
JPA EntityManager
→ SQLAlchemy Session
@Transactional
→ session.begin() 또는 transaction.atomic()
ThreadLocal
→ ContextVar
@Async
→ 단순 비동기 I/O는 asyncio
→ 내구성 있는 작업은 Celery
RedisTemplate
→ redis-py
KafkaTemplate / @KafkaListener
→ Kafka Producer / Consumer
전환 과정에서 가장 우선적으로 체득해야 하는 것은 여섯 가지입니다.
- Python 타입 힌트는 런타임 검증이 아니다.
async def안에서는 blocking I/O를 실행하지 않는다.- 전역 singleton은 process마다 별도로 존재한다.
AsyncSession은 요청 또는 task 단위로 관리한다.- Celery와 Kafka 작업은 항상 중복 실행 가능성을 고려한다.
- API 모델, 도메인 모델, DB 모델, 이벤트 모델을 분리한다.
API·마이크로서비스 중심으로 전환한다면 FastAPI → SQLAlchemy → Redis → Celery → Kafka 순서가 가장 자연스럽습니다. Django는 그다음에 학습하되, 관리자·인증·권한·CRUD가 핵심인 시스템에서는 처음부터 Django + DRF를 선택하는 것이 더 효율적입니다.
주의점과 불확실성
- Spring 대응표는 학습을 돕기 위한 비유입니다. 같은 칸의 기능이라도 객체 생명주기, 비동기 실행, transaction 전파와 확장 방식까지 같다는 뜻은 아닙니다.
- FastAPI, Django, Pydantic, SQLAlchemy, Celery와 Kafka client의 API·기본값은 버전에 따라 달라집니다. 설치한 버전의 공식 문서와 lockfile을 기준으로 구현해야 합니다.
- worker 수, DB pool, timeout, TTL과 retry 횟수는 예시입니다. 트래픽, 지연시간, DB 한도와 장애 예산을 측정한 뒤 조정해야 합니다.
async def의 효과는 사용하는 DB·HTTP·Redis client가 실제 non-blocking I/O를 제공할 때 나타납니다. 동기 라이브러리와 섞을 때는 thread offloading이나 별도 worker를 검토합니다.- Celery의 late acknowledgement와 Kafka manual commit은 중복 실행 가능성을 없애지 않습니다. task와 consumer는 idempotency key, unique constraint, outbox·inbox로 멱등성을 보완해야 합니다.
검증이 필요한 주장
- Django의 동기·비동기 전환 비용과 async ORM 지원 범위는 사용하려는 Django 버전과 middleware 조합에서 다시 확인해야 합니다.
- SQLAlchemy
AsyncSession의 transaction·동시 task 사용 규칙은 선택한 SQLAlchemy major version과 driver에서 재검증해야 합니다. - Pydantic strict mode의 coercion과 serialization 동작은 모델 설정 및 Pydantic 버전에 따라 달라질 수 있습니다.
confluent-kafka와aiokafka의 rebalance, manual commit, transaction 지원 범위는 운영 표준과 현재 client 버전을 기준으로 비교해야 합니다.- Redis 또는 RabbitMQ를 Celery broker로 선택할 때의 durability, visibility timeout, 운영 복잡도는 workload에 맞춰 별도 검토해야 합니다.
Source Fidelity Notes
- Preserved key numbers: HTTP timeout 3.0초, Uvicorn worker 4개, worker 8개 × DB pool 10개 = 약 80 connection, Redis TTL 60초, Celery
max_retries=5, graceful shutdown 9단계, 학습 로드맵 7단계, 최종 원칙 6개. - Preserved frameworks / models: FastAPI·Django·DRF 선택 기준, Pydantic runtime validation, Python
Protocol과 decorator,asyncio, Uvicorn multi-process,ContextVar, SQLAlchemy Session·Unit of Work, Django ORM, Redis cache-aside, Celery task model, Kafka manual commit, outbox·inbox. - Preserved templates / checklists: Spring 대응표, 모델 3분리, FastAPI dependency graph, transaction 예제, Redis cache repository, Celery idempotency, Kafka event schema와 처리 순서, 권장 프로젝트 구조, 테스트 목록, 관측 지표, 전환 프로젝트 아키텍처.
- Omitted or compressed: 기술 본문과 코드 예시는 생략하지 않았습니다. 공개 문서에는 frontmatter와 탐색용 요약을 추가했고, 코드 블록 밖의 heading hierarchy와 불필요한 UTM query parameter만 정리했습니다. 공개 한국어 산문은 기술 의미·수치·고유명사를 유지한 채 반복 접속사와 기계적인 문장만 다듬었습니다.
- Omission risk: 기술 본문이 모두 남아 있어 해석 손실 위험은 낮습니다. 원래 표현과 구조는 private source layer에서 그대로 확인할 수 있습니다.