콘텐츠로 이동

HTTP 응답 개요#

2024 iThome 철인 대회

이 글은 Django Ninja 입문 가이드의 13번째 글이에요.

이 글부터 본격적으로 "HTTP 응답" 과정인 세 번째 섹션으로 들어갈 거예요.

이번 절에서는 총 4편의 글을 통해 Django Ninja가 어떻게 HTTP 응답을 처리하는지 소개할게요:

이 과정을 통해 Schema의 더 다양한 활용법을 배우게 될 텐데, 이러한 기술들을 활용하면 API의 출력 형식을 매우 정밀하게 제어할 수 있어요. 단일 객체 응답이든 복잡한 중첩 구조(nested structure)든, 앞으로 하나씩 차근차근 다뤄볼 거예요.

이번 글의 모든 코드 변경 사항은 이 PR에서 확인하실 수 있어요.

GitHub 예제 프로젝트#

👉 Django-Ninja-Tutorial


이 글에서는 쉬운 것부터 시작하여 복잡한 것까지, Django Ninja를 통해 HTTP 응답을 어떻게 생성하는지 한 걸음씩 단계별로 소개해 드릴게요.

또한 기존에 작성했던 3개의 API를 활용하여 시연을 진행할 거예요 (필요에 따라 각각에 새로운 내용을 덧붙일 거예요):

  1. 글 작성: 상태 코드가 포함된 간단한 응답을 보여줘요.
  2. 단일 글 조회: Schema 사용 및 response= 매개변수 정의가 필요한 단일 객체 응답을 시연해요.
  3. 글 목록 조회: 여러 개의 객체 응답을 어떻게 처리하는지 보여줘요.

시작해 볼까요!

1. 간단한 응답: 글 작성#

가장 단순한 응답 형식부터 살펴볼게요. 이 예제에서는 Python 딕셔너리를 어떻게 응답으로 반환하는지, 그리고 HTTP 응답 상태 코드를 수동으로 어떻게 설정하는지 보여드릴 거예요.

"글 작성" API를 예로 들어볼게요: (일부 코드는 생략했어요)

@router.post(path='/posts/')
def create_post(...) -> dict:
    ...
    return {'id': post.id, 'title': post.title}

여기서 반환되는 것은 Python 딕셔너리예요. 사실 "JSON으로 직렬화할 수 있는 어떠한 Python 데이터"든 return할 수 있어요. (그래서 Django 모델 객체 자체는 안 돼요. 직접 직렬화할 수 없거든요.)

따라서 다음과 같은 것들은 모두 return이 가능해요:

  • 단순한 문자열: "Hello World !"
  • Python 리스트: [1 , 2 , 3]
  • 중첩된 데이터 구조: {"name": "Alice", "age": 30, "hobbies": ["reading", "swimming"]}

이 모든 것들은 Django Ninja에 의해 자동으로 JSON 형식으로 직렬화되어 API의 응답으로 제공돼요:

{
    "id": 666,
    "title": "How to Be a Ninja"
}

응답에 HTTP 상태 코드 추가하기#

뷰 함수에서 응답을 처리할 때 종종 HTTP 상태 코드를 추가해야 하는 경우가 있어요. 특히 응답 상태가 다양할 때, 상태 코드를 통해 이를 구분해야 하죠.

방법은 아주 간단해요. 응답 내용 바로 앞에 숫자를 적어주기만 하면 된답니다:

return 201, {'id': post.id, 'title': post.title}

이렇게 하면 함수의 반환 타입이 기존의 dict에서 tuple로 바뀌게 돼요.

따라서 함수 시그니처의 type hints도 그에 맞춰 수정해야 해요:

def create_post(...) -> tuple[int, dict]:

만약 앞에 상태 코드 숫자를 넣지 않았다면, Django Ninja는 이를 기본값인 200으로 간주해요.

주의할 점은, 뷰 함수가 "200이 아닌" 응답을 반환해야 할 때, 반드시 router 데코레이터에 이를 명시해야 한다는 거예요:

@router.post(path='/posts/', response={201: dict})  # 바로 이 부분이에요

response={201: dict}가 이를 선언하는 방식으로, Python 딕셔너리를 이용해 상태 코드반환 내용의 형식을 일대일로 연결해 줘요.

글을 쓸 당시에는 이 부분의 예제 프로젝트 코드가 아직 추가되지 않았었기 때문에 이 API가 정상적으로 응답하지 않을 수 있어요 😅. 참고해 주세요.


첫 번째 응답 형태는 아주 단순했지만, 대부분의 API 응답은 이렇게 단순하지만은 않아요.

그럼 이제 두 번째 형태의 응답을 살펴볼게요.

2. 단일 모델 객체 응답: 단일 글 조회#

Django API를 개발할 때, 응답에 포함되는 데이터의 상당 부분은 Django 모델 객체를 직렬화한 것에서 비롯돼요.

하지만 데이터베이스의 모든 정보를 그대로 프론트엔드로 전달하는 경우는 흔치 않아요. 대신 우리는 필드 필터링, 검증 또는 형식 변환의 과정을 거치게 되죠.

이렇게 함으로써 API의 출력 결과를 정밀하게 제어할 수 있을 뿐만 아니라, 데이터의 정확성과 보안을 확실히 할 수 있어요.

Django Ninja에서는 이러한 "필터링, 검증, 형식 변환"과 같은 요구사항을 모두 Schema를 통해 구현해요.

"단일 글 정보 가져오기" API에 맞게 Schema를 사용한 응답 형식을 설계해 볼까요:

# post/schemas.py
from datetime import datetime
...

class PostResponse(Schema):
    id: int
    title: str
    content: str
    author_id: int
    created_at: datetime
    updated_at: datetime

PostResponse Schema는 Post의 거의 모든 필드를 포함하고 있어요.

주의할 점은, Schema에 정의된 내용이 출력될 필드를 결정한다는 것이에요. 만약 Schema에 id 필드 하나만 있다면, 출력 결과도 해당 필드의 데이터만 나오게 돼요.

이제 이 Schema를 뷰 함수에서 사용해 볼게요:

@router.get(path='/posts/{int:post_id}/', response=PostResponse)
def get_post(request: HttpRequest, post_id: int) -> Post:
    """
    단일 글 가져오기
    """
    post = Post.objects.get(id=post_id)
    return post

단 한 줄만 바꿨어요! — router 데코레이터에 response=PostResponse를 추가해 주었죠.

response=PostResponse 설정 덕분에 Django Ninja는 함수가 반환하는 Post 모델 객체를 PostResponse로 넘겨 검증하고, 성공하면 이를 곧바로 JSON 형식으로 변환하여 프론트엔드로 보내줘요.

응답 결과를 확인해 볼까요:

// http://127.0.0.1:8000/posts/2/
{
    "id": 2,
    "title": "Alice's Django Ninja Post 1",
    "content": "Alice's Django Ninja Post 1 content",
    "author_id": 1,
    "created_at": "2024-09-12T02:28:16.801Z",
    "updated_at": "2024-09-12T02:28:16.801Z"
}

아주 잘 작동하네요!


3. 다중 모델 객체 응답: 글 목록 조회#

"리스트, 목록" 형식도 여러 건의 데이터를 포함하여 흔하게 쓰이는 API 응답 형태 중 하나예요.

방금 사용했던 PostResponse를 아무런 수정 없이 그대로 "글 목록 조회" API에 적용해 볼게요.

이번에도 딱 한 줄만 고치면 되지만, 이전과는 약간의 차이가 있어요:

@router.get(path='/posts/', response=list[PostResponse])

list[PostResponse]를 사용했는데, 이는 응답이 여러 PostResponse 객체들이 담긴 list가 될 것임을 의미해요.

Django Ninja의 Iterable 자동 처리#

흥미로운 점은, 이때 파이썬의 리스트를 "진짜로" return할 필요가 없다는 것이에요. 그냥 QuerySet을 반환하기만 하면 돼요. 객체의 반복(iteration)과 직렬화는 Django Ninja가 알아서 처리해 주거든요.

더욱 놀라운 건, 반환하는 대상이 iterable(반복 가능한 객체)이고 그 안에 있는 각각의 요소가 PostResponse의 검증을 통과하기만 한다면(형식이 일치한다면) 충분하다는 사실이에요!

결과를 살펴볼까요? 목록이 너무 길어서 스크린샷으로 준비했어요:

API 응답: 글 목록 조회


다중 상태 코드 응답#

앞서 살펴본 응답들은 200 아니면 201이었지만, 보통 API는 400, 401, 403, 심지어 500 같은 응답들도 반환하게 되죠. 이러한 상태 코드들 간의 대응 관계는 어떻게 처리해야 할까요?

네, 맞아요! response= 안의 딕셔너리를 더 확장해 주면 돼요. 공식 문서의 예제를 바로 살펴볼까요:

class Token(Schema):
    token: str
    expires: date

class Message(Schema):
    message: str

@api.post(
    path='/login',
    response={200: Token, 401: Message, 402: Message}
)
...

기억해 둘 만한 흥미로운 부분은, 딕셔너리의 key는 중복될 수 없지만 값(value)은 중복이 가능하다는 거예요! — Message두 번이나 등장했어요.

하지만 개인적으로 이 "다중 상태 코드 응답" 설정은 실무에서 그다지 유용하지 않다고 생각해요. 왜 그럴까요? 그 이유에 대해서는 나중에 다시 이야기해 볼게요.


소결#

이번 글에서는 가장 단순한 형태의 응답부터 시작하여, 단일 데이터와 다중 데이터를 응답에 어떻게 반환하는지 차근차근 알아보았고, 나아가 Django Ninja가 여러 상태 코드에 대한 응답을 어떻게 설정하는지까지 살펴보았어요.

다음 편에서는 응답 속에 들어 있는 복잡한 중첩 구조(nested structure)를 다루는 방법에 대해 논의하며, 우리의 API를 한층 더 견고하게 만들어 볼 거예요.