FastAPI/데이터베이스

ORM 적용 - HTTP Response 처리

hyunai 2026. 6. 14. 16:50
# response.py

from typing import List

from pydantic import BaseModel

# BaseModel을 상속받는 스키마 클래스
class ToDoSchema(BaseModel):
    id: int
    contents : str
    is_done: bool

    # SQL alchemy를 읽어줄 수 있도록 하기 위한 옵션
    # Pydantic에 정의한 ORM 모드라는 걸 사용 가능
    # 따라서 ToDoSchema에 SQL alchemy ORM 객체를 주면 Pydantic이 이 ORM 객체를 해석해서 맞게 변경을 해준다.
    class Config:
        from_attributes = True

# 실제로 response에 활용을 할 클래스
class ListToDoResponse(BaseModel):
    todos: List[ToDoSchema]
  • 지금 ToDoSchema에 컬럼과 이미 ORM에 생성한 컬럼과 동일하다.
  • 근데 굳이 왜 response를 한번 더 분리하고 정의하냐?
    • 아직은 컬럼이 단순하고 이 구조가 단순하지만 응답의 컬럼 간의 연산이 있거나 이 객체를 뭔가 중첩된 구조로 반환을 한다거나 contents 값을 제외하고 return을 하고싶다 식의 다양한 usecase가 있기 때문에 response 객체를 분리를 해놓게 되면 훨씬 유연하게 코드 변경을 할 수가 있다.

class Config: from_attributes = True 이해하기


왜 이게 필요한지 - 문제부터 이해하기

FastAPI에서 데이터는 이렇게 흐른다.

데이터 흐름

MySQL DB
    ↓
SQLAlchemy ORM 객체 (ToDo)   ← Python 객체
    ↓
Pydantic 스키마 (ToDoSchema) ← 응답용 객체
    ↓
JSON 응답

여기서 중간에 "SQLAlchemy 객체 → Pydantic 스키마" 변환할 때 문제가 생긴다.


왜 변환이 안 되냐면

Pydantic은 기본적으로 딕셔너리만 읽을 수 있다.

Pydantic이 읽을 수 있는 것 vs 없는 것

# Pydantic이 기본으로 읽을 수 있는 것 (딕셔너리)
todo_dict = {
    "id": 1,
    "contents": "FastAPI 공부",
    "is_done": False
}
ToDoSchema(**todo_dict)  # 문제 없음

# Pydantic이 기본으로 읽을 수 없는 것 (SQLAlchemy 객체)
todo_orm = session.scalar(select(ToDo))  # SQLAlchemy 객체
ToDoSchema(???)  # 어떻게 넣어야 하지?

SQLAlchemy 객체가 뭔지 시각화

todo_orm 객체 내부
─────────────────────────────
todo_orm.id       = 1
todo_orm.contents = "FastAPI 공부"
todo_orm.is_done  = False
+ 그 외 SQLAlchemy 내부 정보 수십 가지
─────────────────────────────
Pydantic: "이게 뭔지 모르겠어, 읽는 방법을 몰라"

from_attributes = True 가 하는 일

from_attributes 설정 효과

from_attributes = False (기본값)
────────────────────────────────
Pydantic: "딕셔너리만 읽을 수 있어"
SQLAlchemy 객체 → 변환 불가

from_attributes = True (설정 후)
────────────────────────────────
Pydantic: "객체의 속성(attribute)도 읽을 수 있어"
SQLAlchemy 객체의 .id, .contents, .is_done → 변환 가능

쉽게 말하면:

"Pydantic한테 딕셔너리 말고 객체 속성도 읽는 법을 가르쳐주는 것"


실제로 어떻게 동작하냐면

from_attributes 동작 과정

# SQLAlchemy ORM 객체
todo_orm = session.scalar(select(ToDo))
# todo_orm.id = 1
# todo_orm.contents = "FastAPI 공부"
# todo_orm.is_done = False

# from_attributes = True 설정 덕분에
# Pydantic이 객체의 속성을 하나씩 읽어서 변환함
result = ToDoSchema.model_validate(todo_orm)

# 내부적으로 이렇게 동작하는 것과 같음
# ToDoSchema(
#     id = todo_orm.id,          ← 속성 읽기
#     contents = todo_orm.contents,  ← 속성 읽기
#     is_done = todo_orm.is_done     ← 속성 읽기
# )

print(result)
# ToDoSchema(id=1, contents='FastAPI 공부', is_done=False)

class Config 는 왜 클래스 안에 또 클래스가 있냐면

Config 클래스 역할

class ToDoSchema(BaseModel):
    # 실제 데이터 필드
    id: int
    contents: str
    is_done: bool

    # ToDoSchema의 동작 방식을 설정하는 곳
    class Config:
        from_attributes = True  # ORM 객체도 읽을 수 있게 허용

Config 클래스 비유

비유  설명
ToDoSchema 자동차
class Config 자동차 설정 메뉴
from_attributes = True "오프로드 모드 켜기"

자동차 자체는 ToDoSchema고, Config는 그 자동차의 동작 방식을 바꾸는 설정판


전체 흐름 한눈에 보기

전체 변환 흐름

DB에서 데이터 조회
        ↓
SQLAlchemy ORM 객체 (ToDo)
  todo.id = 1
  todo.contents = "FastAPI 공부"
  todo.is_done = False
        ↓
  model_validate(todo)  ← from_attributes = True 덕분에 가능
        ↓
Pydantic 스키마 (ToDoSchema)
  id = 1
  contents = "FastAPI 공부"
  is_done = False
        ↓
JSON 응답
  {
    "id": 1,
    "contents": "FastAPI 공부",
    "is_done": false
  }

💡 핵심 요약:
Pydantic은 기본적으로 딕셔너리만 읽을 수 있다.
from_attributes = True 설정을 하면 SQLAlchemy ORM 객체도 읽을 수 있게 된다.
class Config는 Pydantic 모델의 동작 방식을 설정하는 공간.


콘솔로 확인해보기

# 생성한 ToDoSchema import
from schema.response import ToDoSchema

# database의 orm에서 ToDo import
from database.orm import ToDo

# 임의의 ToDo를 하나 생성
# 이 때 생성된 객체는 데이터베이스에 저장되거나 이런 건 아니고 말 그대로 Python 객체
todo = ToDo(id=100, contents="test", is_done=True)

# ToDoSchema 객체 생성
ToDoSchema.model_validate(todo)
=====================================================
ToDoSchema(id=100, contents='test', is_done=True)

별도 작업을 해주지 않아도 ORM 모드를 True로 주게 된다면 Pydantic Schema로 쉽게 변환을 할 수 있다.

# main.py

# status_code만 적어주면 =200을 안 적어도 자동으로 200으로 매핑된다.
@app.get("/todos", status_code=200)
def get_todos_handler(
    order: str | None = None,
    session: Session = Depends(get_db),
# 타입 힌트
) -> ListToDoResponse:
    # 타입 힌트
    todos: List[ToDo] = get_todos(session=session)

    if order and order == "DESC":
        return ListToDoResponse(
            # todos: List[ToDoSchema] response.py에 이렇게 리스트 형식으로 작성해놨기 때문에 동일하게 []로 작성
            todos=[ToDoSchema.model_validate(todo) for todo in todos[::-1]]
        )
    return ListToDoResponse(
        todos=[ToDoSchema.model_validate(todo) for todo in todos]
    )
  • 이전 글까지만 해도 ORM 객체를 그대로 return을 해주어도 그대로 JSON 형태로 변환이 돼서 잘 응답을 해주긴했다.
  • 하지만 일반적으로는 데이터를 직접 return하지 않고 데이터가 표현되는 부분을 분리를 해서 처리를 한다.

리스트 컴프리헨션?

한 문장으로 표현하면:

"리스트의 각 요소를 하나씩 꺼내서 가공한 뒤, 새로운 리스트로 만드는 문법"


일반 for문이랑 비교해보기

일반 for문 vs 리스트 컴프리헨션

# 일반 for문 방식
result = []
for todo in todos:
    result.append(ToDoSchema.model_validate(todo))

# 리스트 컴프리헨션 방식 (위랑 완전히 같은 동작)
result = [ToDoSchema.model_validate(todo) for todo in todos]

둘이 완전히 똑같이 동작. 리스트 컴프리헨션이 그냥 더 짧게 쓰는 방법


문법 구조 분해

리스트 컴프리헨션 구조

[ToDoSchema.model_validate(todo)   for   todo   in   todos]
 ↑                                  ↑     ↑      ↑     ↑
 각 요소에 할 작업                  for  하나씩  in  원본 리스트
 (todo를 Schema로 변환)                  꺼낼
                                        변수명

자연어로 읽으면:

"todos에서 todo를 하나씩 꺼내서, model_validate(todo)를 실행한 결과로 새 리스트를 만들어"


단계별로 동작 과정 보기

동작 과정 시각화

todos (SQLAlchemy 객체 리스트)
[
    ToDo(id=1, contents="섹션 0 수강", is_done=True),
    ToDo(id=2, contents="섹션 1 수강", is_done=False),
    ToDo(id=3, contents="섹션 2 수강", is_done=False),
]
        ↓ for todo in todos (하나씩 꺼냄)
        ↓ ToDoSchema.model_validate(todo) (변환)

결과 리스트
[
    ToDoSchema(id=1, contents="섹션 0 수강", is_done=True),
    ToDoSchema(id=2, contents="섹션 1 수강", is_done=False),
    ToDoSchema(id=3, contents="섹션 2 수강", is_done=False),
]

더 쉬운 예시로 이해하기

숫자로 이해하는 리스트 컴프리헨션

# 원본 리스트
numbers = [1, 2, 3, 4, 5]

# 각 숫자에 2를 곱한 새 리스트 만들기
doubled = [n * 2 for n in numbers]
print(doubled)  # [2, 4, 6, 8, 10]

# 문자열 리스트도 가능
names = ["alice", "bob", "charlie"]
upper_names = [name.upper() for name in names]
print(upper_names)  # ["ALICE", "BOB", "CHARLIE"]

지금 코드에 대입해서 읽기

실제 코드 자연어로 읽기

todos=[ToDoSchema.model_validate(todo) for todo in todos]
#      ↑                          ↑        ↑         ↑
#  ToDoSchema로 변환한 결과를    todo를  하나씩    todos에서
#  새 리스트에 담아               꺼내서   꺼내

"todos 리스트에서 todo를 하나씩 꺼내서,
ToDoSchema.model_validate(todo)로 변환한 결과들을
새로운 리스트로 만들어서 todos에 담아"


전체 맥락에서 보기

main.py 전체 흐름

return ListToDoResponse(
    todos=[ToDoSchema.model_validate(todo) for todo in todos]
    #      ↑ SQLAlchemy 객체들을 Pydantic 스키마로 변환한 리스트
)

# 최종적으로 이런 JSON이 반환됨
# {
#   "todos": [
#     {"id": 1, "contents": "섹션 0 수강", "is_done": true},
#     {"id": 2, "contents": "섹션 1 수강", "is_done": false},
#     {"id": 3, "contents": "섹션 2 수강", "is_done": false}
#   ]
# }

💡 핵심 요약:
[A for B in C] = "C에서 B를 하나씩 꺼내서 A를 실행한 결과로 새 리스트 만들기"
일반 for문을 한 줄로 압축한 문법


swagger ui로 확인

'FastAPI > 데이터베이스' 카테고리의 다른 글

파이참 - Refactoring  (0) 2026.06.16
ORM 적용 - GET 단일 조회 API  (0) 2026.06.16
ORM 적용 - GET 전체 조회 API / Python - Generator  (0) 2026.06.14
ORM 모델링  (1) 2026.06.09
데이터베이스 연결  (0) 2026.06.04