<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>독한 it</title>
    <link>https://hyunai.tistory.com/</link>
    <description>독하게 한다면 뭐든 할 수 있다.</description>
    <language>ko</language>
    <pubDate>Mon, 13 Jul 2026 07:27:06 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>hyunai</managingEditor>
    <image>
      <title>독한 it</title>
      <url>https://tistory1.daumcdn.net/tistory/8484028/attach/321717aaa82247118a5f5c413613d93a</url>
      <link>https://hyunai.tistory.com</link>
    </image>
    <item>
      <title>POST API</title>
      <link>https://hyunai.tistory.com/157</link>
      <description>&lt;pre id=&quot;code_1783669513183&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# request.py
from pydantic import BaseModel

class CreateToDoRequest(BaseModel):
    contents: str
    is_done: bool


# main.py
@app.post(&quot;/todos&quot;, status_code=201)
# FastAPI가 알아서 reqeustBody를 CreateToDoRequest에 넣어서 처리를 해준다.
def create_todo_handler(
        request: CreateToDoRequest,
        session: Session = Depends(get_db)
) -&amp;gt; ToDoSchema:
    todo: ToDo=ToDo.create(request=request) # id=None
    todo: ToDo=create_todo(session=session, todo=todo) # id=int
    return ToDoSchema.model_validate(todo)
    
    
# orm.py
    # 요청 받은 requestBody를 ORM 객체로 변환해주는 클래스 method
    @classmethod
    def create(cls, request : CreateToDoRequest) -&amp;gt; &quot;ToDo&quot;:
        return cls(
            contents=request.contents,
            is_done=request.is_done,
        )
        

# test_main.py
def test_create_todo(client, mocker):
    mocker.patch(
        &quot;main.create_todo&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True),
    )

    body = {
        &quot;contents&quot; : &quot;test&quot;,
        &quot;is_done&quot; : False,
    }
    response = client.post(&quot;/todos&quot;, json=body)

    assert response.status_code == 201
    assert response.json() == {&quot;id&quot;: 1, &quot;contents&quot;: &quot;todo&quot;, &quot;is_done&quot;: True}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;main.py의 &lt;b&gt;create_todo를 mocking&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;post 요청으로 create를 해주기 때문에 requestbody가 필요&lt;/b&gt;하다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;request.py 파일에 있는 객체 멤버변수인 contents, is_done 값을 body로 전달해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이러면 pytest를 했을 때 pass는 나지만 뭔가 이상한 점이 발견된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이상한 점?&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1783670092417&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    body = {
        &quot;contents&quot; : &quot;test&quot;,
        &quot;is_done&quot; : False,
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783670145090&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    mocker.patch(
        &quot;main.create_todo&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True),
    )    
...
    assert response.json() == {&quot;id&quot;: 1, &quot;contents&quot;: &quot;todo&quot;, &quot;is_done&quot;: True}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지금 requestbody는 contents는 test, is_done값은 False값을 넘기고 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;하지만 마지막 response 데이터에서는 contents 값이 todo이고 is_done값이 True이다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;다시 말해서 이 response가 실제로 응답해서 그걸 검증하는 부분이 mocking 하는 부분과는 일치하지만 요청한 requestbody 부분과는 지금 일치하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;요청 흐름 시각화&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;client.post(&quot;/todos&quot;, json=body)
body = {&quot;contents&quot;: &quot;test&quot;, &quot;is_done&quot;: False}
        &amp;darr;
create_todo_handler 실행
        &amp;darr;
ToDo.create(request=request)   &amp;larr; body로 ToDo 객체 생성
todo = ToDo(contents=&quot;test&quot;, is_done=False)  id=None
        &amp;darr;
create_todo(session, todo) 호출 시도
        &amp;darr;
mocker가 가로챔
&quot;DB에 저장하는 거 내가 대신할게&quot;
return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True)
        &amp;darr;
{&quot;id&quot;: 1, &quot;contents&quot;: &quot;todo&quot;, &quot;is_done&quot;: True} 반환&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;mocker가 대체하는 시점이 핵심&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;어디서 mocker가 개입하냐면&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;def create_todo_handler(request, session):
    todo = ToDo.create(request=request)
    # todo.contents = &quot;test&quot;    &amp;larr; body 값
    # todo.is_done = False      &amp;larr; body 값
    # todo.id = None            &amp;larr; 아직 DB 저장 전

    todo = create_todo(session=session, todo=todo)
    #      &amp;uarr;
    #   여기서 mocker가 가로챔!
    #   body 값은 무시되고
    #   return_value 값이 반환됨
    #   todo.id = 1
    #   todo.contents = &quot;todo&quot;
    #   todo.is_done = True

    return ToDoSchema.model_validate(todo)
    # mocker가 준 값으로 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;body랑 mocker 데이터가 달라도 pass인 이유&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각 데이터의 역할&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;데이터&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 역할 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 응답에 영향? &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;body = {&quot;contents&quot;: &quot;test&quot;, &quot;is_done&quot;: False}&lt;/td&gt;
&lt;td&gt;create_todo 호출 전까지만 사용&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mocker return_value = ToDo(id=1, contents=&quot;todo&quot;, is_done=True)&lt;/td&gt;
&lt;td&gt;create_todo 결과로 반환됨&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;body는 create_todo() 를 &lt;b&gt;호출하는 데 쓰이는 값&lt;/b&gt;이고, mocker는 create_todo() 의 &lt;b&gt;반환값을 대체&lt;/b&gt;하기 때문&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이 테스트가 실제로 검증하는 것&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 테스트의 진짜 목적&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 이 테스트가 검증하는 것:
# 1. POST /todos 요청이 201 상태코드를 반환하는가?
assert response.status_code == 201

# 2. create_todo()의 반환값을 ToDoSchema로 잘 변환해서 응답하는가?
assert response.json() == {&quot;id&quot;: 1, &quot;contents&quot;: &quot;todo&quot;, &quot;is_done&quot;: True}

# 이 테스트가 검증하지 않는 것:
# - body 값이 실제 DB에 잘 저장되는가? (mocker로 대체했으니까)
# - contents=&quot;test&quot;, is_done=False 가 제대로 전달되는가?&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;핵심 요약:&lt;/b&gt;&lt;br /&gt;body 값은 create_todo() 를 &lt;b&gt;호출할 때 쓰이는 입력값&lt;/b&gt;&lt;br /&gt;mocker는 create_todo() 의 &lt;b&gt;출력값을 대체&lt;/b&gt;&lt;br /&gt;이 테스트는 &quot;DB 저장이 잘 되는가&quot; 가 아니라&lt;br /&gt;&quot;create_todo 결과를 받아서 응답을 잘 만드는가&quot; 를 검증 것&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;mocker의 spy 사용해보기&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1783670723490&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def test_create_todo(client, mocker):
	# ToDo라는 인자의 create라는 함수를 spy할 것이다. 라는 의미로 변수로 할당
    create_spy = mocker.spy(ToDo, &quot;create&quot;)
    mocker.patch(
        &quot;main.create_todo&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True),
    )

    body = {
        &quot;contents&quot; : &quot;test&quot;,
        &quot;is_done&quot; : False,
    }
    response = client.post(&quot;/todos&quot;, json=body)

    assert create_spy.spy_return.id is None
    assert create_spy.spy_return.contents == &quot;test&quot;
    assert create_spy.spy_return.is_done is False

    assert response.status_code == 201
    assert response.json() == {&quot;id&quot;: 1, &quot;contents&quot;: &quot;todo&quot;, &quot;is_done&quot;: True}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;spy_return을 할 때 ToDo와 연결이 되어 있다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;그래서 해당 &lt;b&gt;ToDo 코드가 호출이 되면 return 값이 원래는 확인을 못했지만 spy를 사용하여 spy_return 안에 복사&lt;/b&gt;가 된다.&lt;/li&gt;
&lt;li&gt;따라서 지금 이 spy 코드들은 ToDo가 잘 생성됐는지 검증해주는 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>FastAPI/테스트 코드</category>
      <author>hyunai</author>
      <guid isPermaLink="true">https://hyunai.tistory.com/157</guid>
      <comments>https://hyunai.tistory.com/157#entry157comment</comments>
      <pubDate>Fri, 10 Jul 2026 17:10:46 +0900</pubDate>
    </item>
    <item>
      <title>GET 단일 조회 API</title>
      <link>https://hyunai.tistory.com/156</link>
      <description>&lt;pre id=&quot;code_1783411704261&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# main.py
@app.get(&quot;/todos/{todo_id}&quot;, status_code=200)
def get_todo_handler(
        todo_id: int,
        session: Session = Depends(get_db),
) -&amp;gt; ToDoSchema:
    todo: ToDo | None = get_todo_by_todo_id(session=session, todo_id=todo_id)
    if todo:
        return ToDoSchema.model_validate(todo)
    # 만약 todo가 없다면
    raise HTTPException(status_code=404, detail=&quot;ToDo Not Found&quot;)
    
# test_main.py
def test_get_todo(client, mocker):
    mocker.patch(
        &quot;main.get_todo_by_todo_id&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True),
    )
    response = client.get(&quot;/todos/1&quot;)
    assert response.status_code == 200
    assert response.json() == {&quot;id&quot;:1, &quot;contents&quot;:&quot;todo&quot;, &quot;is_done&quot;: True}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;mocker, client fixture를&lt;/b&gt; 사용하기 위해 인자로 추가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;mocking&lt;/b&gt;을 처리하여 ToDo 객체 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;에러 상황일 때 테스트&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1783412628121&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def test_get_todo(client, mocker):
    # 200
    mocker.patch(
        &quot;main.get_todo_by_todo_id&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True),
    )
    response = client.get(&quot;/todos/1&quot;)
    assert response.status_code == 200
    assert response.json() == {&quot;id&quot;:1, &quot;contents&quot;:&quot;todo&quot;, &quot;is_done&quot;: True}

    # 404
    mocker.patch(
        &quot;main.get_todo_by_todo_id&quot;,
        return_value=None,
    )
    response = client.get(&quot;/todos/1&quot;)
    assert response.status_code == 404
    assert response.json() == {&quot;detail&quot;: &quot;ToDo Not Found&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위의 main.py를 보면 raise 문에 에러가 났을 때 상황도 있다.&lt;/li&gt;
&lt;li&gt;return_value 값을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;None&lt;/b&gt;값을 주고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;status_code&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;값을 404로 바꾸고 에러가 났을 때 key와 value 값으로 바꿔준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;테스트를 하나씩 해보기&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1783412288910&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pytest tests/test_main.py::test_get_todo&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pytest 뒤에 파일 이름을 적고 :: 뒤에 함수 이름을 적게 되면 하나씩 테스트를 해볼 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1783412636910&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;(todos) PS C:\Users\user\Desktop\hyun\fastpai\todos\src&amp;gt; pytest
=============================================================== test session starts ===============================================================
platform win32 -- Python 3.14.6, pytest-9.1.1, pluggy-1.6.0
rootdir: C:\Users\user\Desktop\hyun\fastpai\todos\src
plugins: anyio-4.13.0, mock-3.15.1
collected 3 items                                                                                                                                  

tests\test_main.py ...                                                                                                                       [100%]

================================================================ warnings summary =================================================================
..\Lib\site-packages\fastapi\testclient.py:1
  C:\Users\user\Desktop\hyun\fastpai\todos\Lib\site-packages\fastapi\testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
    from starlette.testclient import TestClient as TestClient  # noqa

schema\response.py:6
  C:\Users\user\Desktop\hyun\fastpai\todos\src\schema\response.py:6: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.13/migration/
    class ToDoSchema(BaseModel):

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================================================== 3 passed, 2 warnings in 0.04s =========================================================&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그럼 이와 같이 테스트가 정상 동작한다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>FastAPI/테스트 코드</category>
      <author>hyunai</author>
      <guid isPermaLink="true">https://hyunai.tistory.com/156</guid>
      <comments>https://hyunai.tistory.com/156#entry156comment</comments>
      <pubDate>Tue, 7 Jul 2026 17:28:45 +0900</pubDate>
    </item>
    <item>
      <title>PyTest Fixture</title>
      <link>https://hyunai.tistory.com/155</link>
      <description>&lt;pre id=&quot;code_1783405602269&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def test_get_todos(mocker):
    # order = ASC

    # mocker를 쓰며 실제로 이 함수를 동작시키는 것이 아니라 이 함수가 동작한 것처럼 속이겠다 라는 의미
    # 그래서 DB에 들리지 않고 그냥 밑의 return_value 값으로 반환이 돼어 테스트하게 된다.
    mocker.patch(&quot;main.get_todos&quot;, return_value=[
        ToDo(id=1, contents=&quot;FastAPI Section 0&quot;, is_done=True),
        ToDo(id=2, contents=&quot;FastAPI Section 1&quot;, is_done=False),
    ])
    response = client.get(&quot;/todos&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;:True},
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: False},
        ]
    }

    # order = DESC
    response = client.get(&quot;/todos?order=DESC&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: False},
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;: True},
        ]
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지금 사용 중인 mocker를 보면 테스트 함수 안에 파라미터로 전달을 해서 mocker를 사용하고 있다.&lt;/li&gt;
&lt;li&gt;근데 생각해보면 &lt;b&gt;이 mocker를 따로 import를 하거나 하지 않았지만 pytest-mock이라는 라이브러리를 install 했을 때 그 라이브러리 안에 이런 mocker라는 fixture가 이미 정의가 되어 있기 때문에 테스트 코드에서 mocker를 바로 가져와서 사용할 수 있는 것&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1783405863003&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# test_main.py
# testclient import
from database.orm import ToDo
from fastapi.testclient import TestClient

# main.py에서 app import
from main import app

# 이 client 객체를 이용해서 여기에 어떤 API 요청을 보내보는 형태
client = TestClient(app=app)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지금 작성된 TestClient는 다른 테스트 파일이 추가된다고 할 때 그 파일들에서도 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;이런 식으로 &lt;b&gt;어떤 파일마다 이 객체를 생성하는게 아니라 미리 fixture로 등록을 해두게 되면 재사용 할 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1783406251764&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# conftest.py

import pytest

from fastapi.testclient import TestClient
from main import app

@pytest.fixture()
def client():
    return TestClient(app=app)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;tests의 python package 밑에 conftest라는 python 파일을 만든다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;함수 형태로 fixture&lt;/b&gt;를 만들어야한다.&lt;/li&gt;
&lt;li&gt;return으로 새로 생성된 &lt;b&gt;TestClient&lt;/b&gt; 객체를 넘겨주도록 한다.&lt;/li&gt;
&lt;li&gt;client를 fixture로 사용하기 위해 &lt;b&gt;pytest에 fixture&lt;/b&gt;라는 데코레이터를 추가해줘야된다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;그럼 pytest가 이 client란 함수를 fixture로 인식을 해서 테스트 코드 안에서 글로벌하게 사용할 수 있게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1783407001827&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# test_main.py

# testclient import
from database.orm import ToDo

def test_health_check(client):
    # get 요청의 응답을 response라는 변수로 할당
    response = client.get(&quot;/&quot;)
    assert response.status_code == 200
    assert response.json() == {&quot;ping&quot; : &quot;pong&quot;}

def test_get_todos(client, mocker):
    # order = ASC

    # mocker를 쓰며 실제로 이 함수를 동작시키는 것이 아니라 이 함수가 동작한 것처럼 속이겠다 라는 의미
    mocker.patch(&quot;main.get_todos&quot;, return_value=[
        ToDo(id=1, contents=&quot;FastAPI Section 0&quot;, is_done=True),
        ToDo(id=2, contents=&quot;FastAPI Section 1&quot;, is_done=False),
    ])
    response = client.get(&quot;/todos&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;:True},
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: False},
        ]
    }

    # order = DESC
    response = client.get(&quot;/todos?order=DESC&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: False},
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;: True},
        ]
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존에 있던 import 문들은 다 정리가 가능해진다.&lt;/li&gt;
&lt;li&gt;함수의 인자로 &lt;b&gt;client를 사용해서 pytest에 fixture를 사용할 수 있게 된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>FastAPI/테스트 코드</category>
      <author>hyunai</author>
      <guid isPermaLink="true">https://hyunai.tistory.com/155</guid>
      <comments>https://hyunai.tistory.com/155#entry155comment</comments>
      <pubDate>Tue, 7 Jul 2026 15:51:01 +0900</pubDate>
    </item>
    <item>
      <title>PyTest Mocking</title>
      <link>https://hyunai.tistory.com/154</link>
      <description>&lt;pre id=&quot;code_1783400463611&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def test_get_todos():
    # order = ASC
    response = client.get(&quot;/todos&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;:True},
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: True},
            {&quot;id&quot;: 3, &quot;contents&quot;: &quot;FastAPI Section 2&quot;, &quot;is_done&quot;: False},
        ]
    }

    # order = DESC
    response = client.get(&quot;/todos?order=DESC&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 3, &quot;contents&quot;: &quot;FastAPI Section 2&quot;, &quot;is_done&quot;: False},
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: True},
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;: True},
        ]
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이렇게 테스트 코드를 작성을 하다보면 함수가 두개가 있어 데이터 조회를 두번하게 된다.&lt;/li&gt;
&lt;li&gt;테스트 코드를 작성하다보면 이런 식으로 API를 여러 번 호출하게 되는데 이럴 때마다 실제 데이터베이스에 데이터 요청이 가게 된다.&lt;/li&gt;
&lt;li&gt;지금 같은 경우는 데이터를 조회하는 요청이기 때문에 크게 문제가 없을 수 있지만 데이터를 생성하거나 업데이트하거나 삭제하는 데이터를 변경하는 경우에는 실제 데이터베이스에 영향을 주게 된다.&lt;/li&gt;
&lt;li&gt;이때 사용되는 기술이 &lt;b&gt;mocking&lt;/b&gt;이라는 기술이다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;실제 데이터베이스에 요청을 하지 않지만 마치 데이터 베이스 요청을 한 것처럼 속이는, 흉내 내는 그런 기술&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;그렇다고 꼭 반드시 사용해야 되는 기술은 아니다.&lt;/li&gt;
&lt;li&gt;예를 들어, &lt;b&gt;외부 API를 이용한다거나 데이터베이스 요청을 한다거나 하는 오래 걸리는 어떤 동작이 있을 때&lt;/b&gt; 그 부분을 mocking을 사용해서 대체를 하게 되면 실제로 그 부분이 동작을 하지 않아서 테스트 코드가 훨씬 빨리 동작하게 된다.&lt;/li&gt;
&lt;li&gt;만약에 mocking을 사용하지 않고 매번 테스트마다 실제로 데이터베이스를 조회하게 됐을 때 데이터가 많다면 이 테스트 코드가 5~10분 1시간까지도 동작할수가 있다.&lt;/li&gt;
&lt;li&gt;테스트 코드의 목적이 그런식으로 실제 데이터를 검증하는 것은 아니기 때문에 mocking이라는 기술을 사용하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;라이브러리 install&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1783400904520&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pip install pytest-mock&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783403159171&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# main.py
@app.get(&quot;/todos&quot;, status_code=200)
def get_todos_handler(
    order: str | None = None,
    session: Session = Depends(get_db),
# 타입 힌트
) -&amp;gt; ToDoListSchema:
    # 타입 힌트
    todos: List[ToDo] = get_todos(session=session)

    if order and order == &quot;DESC&quot;:
        return ToDoListSchema(
            # todos: List[ToDoSchema] response.py에 이렇게 리스트 형식으로 작성해놨기 때문에 동일하게 []로 작성
            todos=[ToDoSchema.model_validate(todo) for todo in todos[::-1]]
        )
    return ToDoListSchema(
        todos=[ToDoSchema.model_validate(todo) for todo in todos]
    )
    

# test_main.py
def test_get_todos(mocker):
    # order = ASC

    # mocker를 쓰며 실제로 이 함수를 동작시키는 것이 아니라 이 함수가 동작한 것처럼 속이겠다 라는 의미
    # 그래서 DB에 들리지 않고 그냥 밑의 return_value 값으로 반환이 돼어 테스트하게 된다.
    mocker.patch(&quot;main.get_todos&quot;, return_value=[
        ToDo(id=1, contents=&quot;FastAPI Section 0&quot;, is_done=True),
        ToDo(id=2, contents=&quot;FastAPI Section 1&quot;, is_done=False),
    ])
    response = client.get(&quot;/todos&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;:True},
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: False},
        ]
    }

    # order = DESC
    response = client.get(&quot;/todos?order=DESC&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: False},
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;: True},
        ]
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;mocker를 쓸 함수 파라미터에 인자를 주입&lt;/b&gt;시켜 mocker를 작성해준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;mocking은 실제 코드가 동작하기 전에 적용을 해줘야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;그래서 mocking을 하는게 뭐냐면 &lt;b&gt;특정 함수를 가로채서 원하는 값을 대신 반환하게 만드는 것&lt;/b&gt;이다.&lt;b&gt;(return_value 값으로 반환&lt;/b&gt;을 해준다.)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;DB, 외부 API 등 내 코드가 아닌 것들을 가짜로 대체해서 내 로직만 순수하게 테스트할 수 있게 만들어주는 것&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;main에 있는 get_todos를 모킹할거다. 라고 작성해준것&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1783403808861&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(todos) PS C:\Users\user\Desktop\hyun\fastpai\todos\src&amp;gt; pytest
=============================================================== test session starts ===============================================================
platform win32 -- Python 3.14.6, pytest-9.1.1, pluggy-1.6.0
rootdir: C:\Users\user\Desktop\hyun\fastpai\todos\src
plugins: anyio-4.13.0, mock-3.15.1
collected 2 items                                                                                                                                  

tests\test_main.py ..                                                                                                                        [100%]

================================================================ warnings summary =================================================================
..\Lib\site-packages\fastapi\testclient.py:1
  C:\Users\user\Desktop\hyun\fastpai\todos\Lib\site-packages\fastapi\testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
    from starlette.testclient import TestClient as TestClient  # noqa

schema\response.py:6
  C:\Users\user\Desktop\hyun\fastpai\todos\src\schema\response.py:6: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.13/migration/
    class ToDoSchema(BaseModel):

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================================================== 2 passed, 2 warnings in 0.77s =========================================================&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그럼 이와 같이 테스트가 잘 된다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>FastAPI/테스트 코드</category>
      <author>hyunai</author>
      <guid isPermaLink="true">https://hyunai.tistory.com/154</guid>
      <comments>https://hyunai.tistory.com/154#entry154comment</comments>
      <pubDate>Tue, 7 Jul 2026 14:56:58 +0900</pubDate>
    </item>
    <item>
      <title>GET 전체 조회 API</title>
      <link>https://hyunai.tistory.com/153</link>
      <description>&lt;pre id=&quot;code_1783324928803&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# main.py
@app.get(&quot;/todos&quot;, status_code=200)
def get_todos_handler(
    order: str | None = None,
    session: Session = Depends(get_db),
# 타입 힌트
) -&amp;gt; ToDoListSchema:
    # 타입 힌트
    todos: List[ToDo] = get_todos(session=session)

    if order and order == &quot;DESC&quot;:
        return ToDoListSchema(
            # todos: List[ToDoSchema] response.py에 이렇게 리스트 형식으로 작성해놨기 때문에 동일하게 []로 작성
            todos=[ToDoSchema.model_validate(todo) for todo in todos[::-1]]
        )
    return ToDoListSchema(
        todos=[ToDoSchema.model_validate(todo) for todo in todos]
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dd2mOD/dJMcahEZMy1/1o1C7eGxlgIrWzKTJ0DK3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dd2mOD/dJMcahEZMy1/1o1C7eGxlgIrWzKTJ0DK3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dd2mOD/dJMcahEZMy1/1o1C7eGxlgIrWzKTJ0DK3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdd2mOD%2FdJMcahEZMy1%2F1o1C7eGxlgIrWzKTJ0DK3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;392&quot; height=&quot;392&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1783325087088&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# test_main.py
def test_get_todos():
	# 밑의 3줄은 테스트의 기본 구조
    response = client.get(&quot;/todos&quot;)
    assert response.status_code == 200
    assert response.json() == {
    
    	# 기존에 todos라는 list로 반환을 했기에 리스트 형식으로 작성해준다.
        &quot;todos&quot;: [
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;:True},
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: True},
            {&quot;id&quot;: 3, &quot;contents&quot;: &quot;FastAPI Section 2&quot;, &quot;is_done&quot;: False},
        ]
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783325350386&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(todos) PS C:\Users\user\Desktop\hyun\fastpai\todos\src&amp;gt; pytest
=============================================================== test session starts ===============================================================
platform win32 -- Python 3.14.6, pytest-9.1.1, pluggy-1.6.0
rootdir: C:\Users\user\Desktop\hyun\fastpai\todos\src
plugins: anyio-4.13.0
collected 2 items                                                                                                                                  

tests\test_main.py ..                                                                                                                        [100%]

================================================================ warnings summary =================================================================
..\Lib\site-packages\fastapi\testclient.py:1
  C:\Users\user\Desktop\hyun\fastpai\todos\Lib\site-packages\fastapi\testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
    from starlette.testclient import TestClient as TestClient  # noqa

schema\response.py:6
  C:\Users\user\Desktop\hyun\fastpai\todos\src\schema\response.py:6: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.13/migration/
    class ToDoSchema(BaseModel):

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================================================== 2 passed, 2 warnings in 0.75s ==========================================================
(todos) PS C:\Users\user\Desktop\hyun\fastpai\todos\src&amp;gt; pytest
=============================================================== test session starts ===============================================================
platform win32 -- Python 3.14.6, pytest-9.1.1, pluggy-1.6.0
rootdir: C:\Users\user\Desktop\hyun\fastpai\todos\src
plugins: anyio-4.13.0
collected 2 items                                                                                                                                  

tests\test_main.py ..                                                                                                                        [100%]

================================================================ warnings summary =================================================================
..\Lib\site-packages\fastapi\testclient.py:1
  C:\Users\user\Desktop\hyun\fastpai\todos\Lib\site-packages\fastapi\testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
    from starlette.testclient import TestClient as TestClient  # noqa

schema\response.py:6
  C:\Users\user\Desktop\hyun\fastpai\todos\src\schema\response.py:6: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.13/migration/
    class ToDoSchema(BaseModel):

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================================================== 2 passed, 2 warnings in 1.13s =========================================================&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 &lt;b&gt;/todos path&lt;/b&gt;로 작성된 코드로 조회를 하면 swagger와 같은 데이터들이 조회가 된다.&lt;/li&gt;
&lt;li&gt;그리고 테스트 코드를 위와 같이 작성하고 &lt;b&gt;pytest&lt;/b&gt;를 실행하면 위와 같이 테스트 함수가 성공했다는 것을 알 수 있다.&lt;/li&gt;
&lt;li&gt;밑에 2 passed는 실제 &lt;b&gt;동작한 함수의 개수&lt;/b&gt;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1783326027772&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# test_main.py
def test_get_todos():
	# 밑의 3줄은 테스트의 기본 구조
    response = client.get(&quot;/todos&quot;)
    assert response.status_code == 200
    assert response.json() == {
    
    	# 기존에 todos라는 list로 반환을 했기에 리스트 형식으로 작성해준다.
        &quot;todos&quot;: [
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;:True},
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: True},
            {&quot;id&quot;: 3, &quot;contents&quot;: &quot;FastAPI Section 2&quot;, &quot;is_done&quot;: True},
        ]
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존에 id가 3이었던 데이터 부분의 is_done 값을 False에서 True로 바꾸고 pytest를 실행하면&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1783326097187&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(todos) PS C:\Users\user\Desktop\hyun\fastpai\todos\src&amp;gt; pytest
=============================================================== test session starts ===============================================================
platform win32 -- Python 3.14.6, pytest-9.1.1, pluggy-1.6.0
rootdir: C:\Users\user\Desktop\hyun\fastpai\todos\src
plugins: anyio-4.13.0
collected 2 items                                                                                                                                  

tests\test_main.py .F                                                                                                                        [100%]

==================================================================== FAILURES =====================================================================
_________________________________________________________________ test_get_todos __________________________________________________________________

    def test_get_todos():
        # order = ASC
        response = client.get(&quot;/todos&quot;)
        assert response.status_code == 200
&amp;gt;       assert response.json() == {
            &quot;todos&quot;: [
                {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;:True},
                {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: True},
                {&quot;id&quot;: 3, &quot;contents&quot;: &quot;FastAPI Section 2&quot;, &quot;is_done&quot;: True},
            ]
        }
E       AssertionError: assert {'todos': [{'...one': False}]} == {'todos': [{'...done': True}]}
E         
E         Differing items:
E         {'todos': [{'id': 1, 'contents': 'FastAPI Section 0', 'is_done': True}, {'id': 2, 'contents': 'FastAPI Section 1', '[3[0m
E                                                                                                                                                   
E         ...Full output truncated (2 lines hidden), use '-vv' to show

tests\test_main.py:20: AssertionError
-------------------------------------------------------------- Captured stdout call ---------------------------------------------------------------
2026-07-06 17:21:09,947 INFO sqlalchemy.engine.Engine SELECT DATABASE()
2026-07-06 17:21:09,947 INFO sqlalchemy.engine.Engine [raw sql] {}
2026-07-06 17:21:09,949 INFO sqlalchemy.engine.Engine SELECT @@sql_mode
2026-07-06 17:21:09,950 INFO sqlalchemy.engine.Engine [raw sql] {}
2026-07-06 17:21:09,951 INFO sqlalchemy.engine.Engine SELECT @@lower_case_table_names
2026-07-06 17:21:09,951 INFO sqlalchemy.engine.Engine [raw sql] {}
2026-07-06 17:21:09,953 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2026-07-06 17:21:09,955 INFO sqlalchemy.engine.Engine SELECT todo.id, todo.contents, todo.is_done 
FROM todo
2026-07-06 17:21:09,955 INFO sqlalchemy.engine.Engine [generated in 0.00030s] {}
2026-07-06 17:21:09,963 INFO sqlalchemy.engine.Engine ROLLBACK
---------------------------------------------------------------- Captured log call ----------------------------------------------------------------
INFO     sqlalchemy.engine.Engine:base.py:1848 SELECT DATABASE()
INFO     sqlalchemy.engine.Engine:base.py:1848 [raw sql] {}
INFO     sqlalchemy.engine.Engine:base.py:1848 SELECT @@sql_mode
INFO     sqlalchemy.engine.Engine:base.py:1848 [raw sql] {}
INFO     sqlalchemy.engine.Engine:base.py:1848 SELECT @@lower_case_table_names
INFO     sqlalchemy.engine.Engine:base.py:1848 [raw sql] {}
INFO     sqlalchemy.engine.Engine:base.py:2712 BEGIN (implicit)
INFO     sqlalchemy.engine.Engine:base.py:1848 SELECT todo.id, todo.contents, todo.is_done 
FROM todo
INFO     sqlalchemy.engine.Engine:base.py:1848 [generated in 0.00030s] {}
INFO     sqlalchemy.engine.Engine:base.py:2715 ROLLBACK
================================================================ warnings summary =================================================================
..\Lib\site-packages\fastapi\testclient.py:1
  C:\Users\user\Desktop\hyun\fastpai\todos\Lib\site-packages\fastapi\testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
    from starlette.testclient import TestClient as TestClient  # noqa

schema\response.py:6
  C:\Users\user\Desktop\hyun\fastpai\todos\src\schema\response.py:6: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.13/migration/
    class ToDoSchema(BaseModel):

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
============================================================= short test summary info =============================================================
FAILED tests/test_main.py::test_get_todos - AssertionError: assert {'todos': [{'...one': False}]} == {'todos': [{'...done': True}]}
===================================================== 1 failed, 1 passed, 2 warnings in 1.35s ====================================================&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이렇게 실패가 나온다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783326131319&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tests\test_main.py:16 (test_get_todos)
{'todos': [{'contents': 'FastAPI Section 0', 'id': 1, 'is_done': True},
           {'contents': 'FastAPI Section 1', 'id': 2, 'is_done': True},
           {'contents': 'FastAPI Section 2', 'id': 3, 'is_done': False}]} != {'todos': [{'contents': 'FastAPI Section 0', 'id': 1, 'is_done': True},
           {'contents': 'FastAPI Section 1', 'id': 2, 'is_done': True},
           {'contents': 'FastAPI Section 2', 'id': 3, 'is_done': True}]}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;만약 위의 로그만으로 뭐가 잘못된지 잘 모르겠다면&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQddfx/dJMcahdZ6KH/RqHyL69YKVqLhVpbKw1knK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQddfx/dJMcahdZ6KH/RqHyL69YKVqLhVpbKw1knK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQddfx/dJMcahdZ6KH/RqHyL69YKVqLhVpbKw1knK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQddfx%2FdJMcahdZ6KH%2FRqHyL69YKVqLhVpbKw1knK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;507&quot; height=&quot;267&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 부분을 눌러서 실행을 시키면 위와 같이 로그에 어떤 부분이 틀렸는지 쉽게 볼 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;데이터 역정렬해서 테스트해보기&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1783326372565&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def test_get_todos():
    # order = ASC
    response = client.get(&quot;/todos&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;:True},
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: True},
            {&quot;id&quot;: 3, &quot;contents&quot;: &quot;FastAPI Section 2&quot;, &quot;is_done&quot;: False},
        ]
    }

    # order = DESC
    response = client.get(&quot;/todos?order=DESC&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 3, &quot;contents&quot;: &quot;FastAPI Section 2&quot;, &quot;is_done&quot;: False},
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: True},
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;: True},
        ]
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;윗쪽은 기존 코드 밑은 그 기존 코드에서 역정렬 내림차순한 데이터로 return되는 테스트하는 함수로 만든다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿼리스트링 부분에 기존 path 뒤에 ?order=DESC를 넣어준다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1783326694390&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(todos) PS C:\Users\user\Desktop\hyun\fastpai\todos\src&amp;gt; pytest
=============================================================== test session starts ===============================================================
platform win32 -- Python 3.14.6, pytest-9.1.1, pluggy-1.6.0
rootdir: C:\Users\user\Desktop\hyun\fastpai\todos\src
plugins: anyio-4.13.0
collected 2 items                                                                                                                                  

tests\test_main.py ..                                                                                                                        [100%]

================================================================ warnings summary =================================================================
..\Lib\site-packages\fastapi\testclient.py:1
  C:\Users\user\Desktop\hyun\fastpai\todos\Lib\site-packages\fastapi\testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
    from starlette.testclient import TestClient as TestClient  # noqa

schema\response.py:6
  C:\Users\user\Desktop\hyun\fastpai\todos\src\schema\response.py:6: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.13/migration/
    class ToDoSchema(BaseModel):

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================================================== 2 passed, 2 warnings in 1.21s =========================================================&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그럼 위와 같이 테스트가 잘 통과했다고 확인할 수 있다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>FastAPI/테스트 코드</category>
      <author>hyunai</author>
      <guid isPermaLink="true">https://hyunai.tistory.com/153</guid>
      <comments>https://hyunai.tistory.com/153#entry153comment</comments>
      <pubDate>Mon, 6 Jul 2026 17:32:12 +0900</pubDate>
    </item>
    <item>
      <title>PyTest란?</title>
      <link>https://hyunai.tistory.com/152</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트 코드 작성을 위한 Python 라이브러리&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;vs. Unittest&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;간결한 문법&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;assert 문&lt;/li&gt;
&lt;li&gt;함수 단위 테스트 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;fixture 지원&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;테스트 데이터 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;라이브러리 설치&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1783310328610&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pip install pytest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트를 하는 라이브러리&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1783310360440&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pip install httpx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FastAPI에서 테스트 client라는 기능을 사용하기 위해 내부적으로 사용하는 라이브러리&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Python Package 만들기&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;237&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cq9xUl/dJMcabricOj/WxvEtskJce7I9H1kCKYCEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cq9xUl/dJMcabricOj/WxvEtskJce7I9H1kCKYCEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cq9xUl/dJMcabricOj/WxvEtskJce7I9H1kCKYCEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcq9xUl%2FdJMcabricOj%2FWxvEtskJce7I9H1kCKYCEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1042&quot; height=&quot;237&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;237&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전까지는 기존 directory를 추가했었는데 지금은 반드시 &lt;b&gt;python package&lt;/b&gt;를 추가를 해야된다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;둘의 차이로는 python package는 기본적으로 &lt;b&gt;__init__.py&lt;/b&gt;가 생성된다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;새로운 python package 폴더 아래에 반드시 있어야 정상적으로 탐색할 수 있게 된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 &lt;b&gt;python package&lt;/b&gt;는 python이 경로를 찾을 때 &lt;b&gt;반드시 이 python 모듈인 경우에만 경로를 찾을 수 있어서&lt;/b&gt; 새로 생성하는 tests 패키지 아래에 테스트 파일들을 &lt;b&gt;테스트로 인지하고 테스트 코드를 실행하게 된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;198&quot; data-origin-height=&quot;111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn71FG/dJMcaicWHoO/c2JoA5ck4HeSgRaXJnyIj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn71FG/dJMcaicWHoO/c2JoA5ck4HeSgRaXJnyIj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn71FG/dJMcaicWHoO/c2JoA5ck4HeSgRaXJnyIj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn71FG%2FdJMcaicWHoO%2Fc2JoA5ck4HeSgRaXJnyIj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;198&quot; height=&quot;111&quot; data-origin-width=&quot;198&quot; data-origin-height=&quot;111&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사진과 같이 &lt;b&gt;test_main.py&lt;/b&gt;로 짓는다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;테스트하는 파일들의 이름도 &lt;b&gt;test_&lt;/b&gt; &lt;b&gt;이렇게 네이밍 컨벤션을 맞춰줘야 pytest가 이 파일을 찾아서 정상적으로 테스트를 동작시킬 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;test_main.py&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1783320679975&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# main.py
...
app = FastAPI()
...


# test_main.py
# testclient import
from fastapi.testclient import TestClient

# main.py에서 app import
from main import app

# 이 client 객체를 이용해서 여기에 어떤 API 요청을 보내보는 형태
client = TestClient(app=app)

def test_health_check():
    # get 요청의 응답을 response라는 변수로 할당
    response = client.get(&quot;/&quot;)
    assert response.status_code == 200
    assert response.json() == {&quot;ping&quot; : &quot;pong&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;assert란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 문장으로 표현하면:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;이 조건이 True가 아니면 테스트 실패로 처리해줘&quot;&lt;/b&gt; 라고 pytest한테 말하는 것&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;assert가 뭔지&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;assert 기본 문법&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;assert 조건식

# 조건식이 True  &amp;rarr; 통과, 아무 일도 안 일어남
# 조건식이 False &amp;rarr; AssertionError 발생, 테스트 실패
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;지금 코드 분해하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;assert 코드 완전 분해&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;response = client.get(&quot;/&quot;)
# &quot;/&quot; 엔드포인트에 GET 요청을 보내고 응답을 받음

assert response.status_code == 200
#      &amp;uarr;                     &amp;uarr;
#  응답의 상태코드가         200이어야 한다
#  (HTTP 상태코드)          (성공 코드)

assert response.json() == {&quot;ping&quot;: &quot;pong&quot;}
#      &amp;uarr;                   &amp;uarr;
#  응답의 JSON 바디가       이 값이어야 한다
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;어떻게 동작하는지&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;assert 동작 흐름&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;client.get(&quot;/&quot;) 요청
        &amp;darr;
응답 받음
        &amp;darr;
assert response.status_code == 200
        &amp;darr;
    200 맞음?
   ↙         ↘
True          False
  &amp;darr;             &amp;darr;
통과         테스트 실패 
              AssertionError&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;성공 vs 실패 예시&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;assert 성공 vs 실패 케이스&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 성공 케이스
# 실제 응답: status_code=200, json={&quot;ping&quot;: &quot;pong&quot;}
assert response.status_code == 200      # True &amp;rarr; 통과
assert response.json() == {&quot;ping&quot;: &quot;pong&quot;}  # True &amp;rarr; 통과

# 실패 케이스
# 실제 응답: status_code=404
assert response.status_code == 200
# False &amp;rarr; AssertionError!
# FAILED tests/test_main.py::test_health_check&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;assert를 왜 2번 쓰냐면&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각 assert가 검증하는 것&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;assert&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 검증 내용 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 의미 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;assert response.status_code == 200&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;HTTP 상태코드&lt;/td&gt;
&lt;td&gt;요청이 성공했는지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;assert response.json() == {&quot;ping&quot;: &quot;pong&quot;}&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;응답 바디&lt;/td&gt;
&lt;td&gt;올바른 데이터가 왔는지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 통과해야 테스트가 성공. 하나라도 실패하면 테스트 전체가 실패&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;핵심 요약:&lt;/b&gt;&lt;br /&gt;assert 조건 = &quot;이 조건이 True가 아니면 테스트 실패&quot;&lt;br /&gt;테스트 코드에서 &lt;b&gt;&quot;내가 기대하는 값&quot;&lt;/b&gt; 과 &lt;b&gt;&quot;실제 응답값&quot;&lt;/b&gt; 을 비교하는 용도&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;pytest 실행&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1783322184282&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pytest&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;터미널에 pytest라고 입력 후 실행
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;pytest가 &lt;b&gt;src 안에서 테스트 코드를 쭉 검색&lt;/b&gt;을 하게 된다.&lt;/li&gt;
&lt;li&gt;여기서 &lt;b&gt;test_main.py가 테스트 코드라는 걸 알게 되고 그 안에서 테스트로 시작한 함수를 테스트 코드로 해서 동작을 시키게 된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XEa5b/dJMb991lnnA/dNyDRJk8vskQ0yJvkTRzu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XEa5b/dJMb991lnnA/dNyDRJk8vskQ0yJvkTRzu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XEa5b/dJMb991lnnA/dNyDRJk8vskQ0yJvkTRzu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXEa5b%2FdJMb991lnnA%2FdNyDRJk8vskQ0yJvkTRzu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1770&quot; height=&quot;670&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그럼 위의 사진과 같이 test_main.py가 실행됐다는 걸 알 수 있다.&lt;/li&gt;
&lt;li&gt;하나의 테스트 코드가 정상적으로 통과했다는 것을 알 수 있게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqaFJv/dJMcajiyoOJ/HWfxZWXSWXPFw1KhVArUA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqaFJv/dJMcajiyoOJ/HWfxZWXSWXPFw1KhVArUA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqaFJv/dJMcajiyoOJ/HWfxZWXSWXPFw1KhVArUA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqaFJv%2FdJMcajiyoOJ%2FHWfxZWXSWXPFw1KhVArUA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;507&quot; height=&quot;267&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;굳이 터미널에 입력하지 않고 왼쪽의 화살표를 눌러 Run 버튼을 눌러도 된다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>FastAPI/테스트 코드</category>
      <author>hyunai</author>
      <guid isPermaLink="true">https://hyunai.tistory.com/152</guid>
      <comments>https://hyunai.tistory.com/152#entry152comment</comments>
      <pubDate>Wed, 1 Jul 2026 13:32:25 +0900</pubDate>
    </item>
    <item>
      <title>테스트 코드란?</title>
      <link>https://hyunai.tistory.com/151</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;테스트 코드&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템의 품질과 신뢰성을 검증하기 위한 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;'코드 변경 -&amp;gt; 기능 점검' 과정 자동화 =&amp;gt; 반복적인 과정을 줄여주어 생산성 향상&lt;/li&gt;
&lt;li&gt;개발자가 시스템에 대한 안정성 확신 =&amp;gt; 유연한 코드 변경 및 리펙토링&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;심화&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Unit Test / Integration Test / Regression Test / TDD / BDD 등 다양한 테스트 종류과 방법론 존재&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>FastAPI/테스트 코드</category>
      <author>hyunai</author>
      <guid isPermaLink="true">https://hyunai.tistory.com/151</guid>
      <comments>https://hyunai.tistory.com/151#entry151comment</comments>
      <pubDate>Wed, 1 Jul 2026 13:26:39 +0900</pubDate>
    </item>
    <item>
      <title>ORM 적용 - DELETE API</title>
      <link>https://hyunai.tistory.com/150</link>
      <description>&lt;pre id=&quot;code_1782457663215&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# repository.py

from sqlalchemy import select, delete

def delete_todo(session: Session, todo_id: int) -&amp;gt; None:
    session.execute(delete(ToDo).where(ToDo.id == todo_id))
    session.commit()&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;session.execute() 완전 이해하기&lt;/b&gt;&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;먼저 전체 맥락 이해하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 배운 SQLAlchemy 조회 방법들&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SQLAlchemy 실행 메서드 비교&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; 메서드 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;용도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 반환값 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;session.scalars()&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;조회 (여러 개)&lt;/td&gt;
&lt;td&gt;객체 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;session.scalar()&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;조회 (한 개)&lt;/td&gt;
&lt;td&gt;객체 하나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;session.execute()&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;뭐든 다 실행&lt;/td&gt;
&lt;td&gt;Result 객체&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;session.execute() 는 &lt;b&gt;조회/추가/수정/삭제 전부 다 실행할 수 있는 만능 메서드&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;session.execute() 구조 분해&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현재 코드 완전 분해&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;session.execute(delete(ToDo).where(ToDo.id == todo_id))
#       &amp;uarr;        &amp;uarr;              &amp;uarr;
#    실행해줘   무엇을?      어떤 조건으로?
#            ToDo 삭제     id가 todo_id인 것만
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자연어로 읽으면:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;ToDo 테이블에서 id가 todo_id인 행을 삭제하는 SQL을 실행&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;내부적으로 어떤 SQL이 실행되냐면&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 실행되는 SQL&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;# Python 코드
session.execute(delete(ToDo).where(ToDo.id == todo_id))

# SQLAlchemy가 자동으로 번역한 SQL
DELETE FROM todo WHERE todo.id = 1
#                             &amp;uarr;
#                          todo_id 값
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo=True 설정 덕분에 콘솔에 이런 로그가 찍힌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;scalars vs execute 차이&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;scalars vs execute 비교&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# scalars &amp;rarr; 조회 전용, 결과를 Python 객체로 바로 반환
todos = session.scalars(select(ToDo))
# [ToDo(id=1...), ToDo(id=2...)] &amp;larr; 바로 객체 리스트

# execute &amp;rarr; 만능, Result 객체 반환
result = session.execute(select(ToDo))
# &amp;lt;sqlalchemy.engine.result.ChunkedIteratorResult&amp;gt; &amp;larr; Result 객체
# .scalars(), .fetchall() 등으로 꺼내야 함

# 삭제할 때는 반환값이 필요 없으니까 그냥 실행만
session.execute(delete(ToDo).where(ToDo.id == todo_id))
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;delete() 문법 더 이해하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;delete() 문법 구조&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from sqlalchemy import delete

delete(ToDo)                        # ToDo 전체 삭제 (위험)
delete(ToDo).where(ToDo.id == 1)    # id가 1인 것만 삭제
delete(ToDo).where(ToDo.is_done == True)  # 완료된 것만 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;delete().where() 동작 흐름&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;delete(ToDo)
    &amp;darr;
&quot;todo 테이블에서 삭제할 준비&quot;
    &amp;darr;
.where(ToDo.id == todo_id)
    &amp;darr;
&quot;id가 todo_id인 것만&quot;
    &amp;darr;
session.execute()
    &amp;darr;
실제 SQL 실행
&quot;DELETE FROM todo WHERE id = 1&quot;
    &amp;darr;
session.commit()
    &amp;darr;
DB에 반영 완료
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;select / delete / update 비교&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CRUD 각각 어떻게 쓰는지&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;from sqlalchemy import select, delete, update

# 조회 (Read)
session.scalars(select(ToDo).where(ToDo.id == 1))

# 삭제 (Delete)
session.execute(delete(ToDo).where(ToDo.id == 1))

# 수정 (Update)
session.execute(
    update(ToDo)
    .where(ToDo.id == 1)
    .values(is_done=True)
)

# 추가 (Create) &amp;rarr; execute 대신 add 사용
session.add(ToDo(contents=&quot;할일&quot;, is_done=False))
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;핵심 요약:&lt;/b&gt;&lt;br /&gt;session.execute() = SQL 구문을 실행하는 만능 메서드&lt;br /&gt;delete(ToDo).where(조건) = 조건에 맞는 행 삭제 SQL 생성&lt;br /&gt;session.execute() 만으로는 DB에 안 반영되고, session.commit() 까지 해야 실제 반영.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;pre id=&quot;code_1782461404806&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# main.py

# delete 성공 상태 코드 : 204
# 204는 응답 받는 body가 없기 때문에 return이 없거나 빈 return을 줘도 된다.
@app.delete(&quot;/todos/{todo_id}&quot;, status_code=204)
def delete_todo_handler(
        todo_id: int,
        session: Session = Depends(get_db),):
    # 삭제하기 위해선 todo id가 유효한지 확인
    todo: ToDo | None = get_todo_by_todo_id(session=session, todo_id=todo_id)
    if not todo:
        raise HTTPException(status_code=404, detail=&quot;Todo Not Fount&quot;)

    delete_todo(session=session, todo_id=todo_id)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아이디를 먼저 조회를 해서 todo를 확인한 후 투두가 있으면 delete 요청을 하고 todo가 없으면 404를 리턴&lt;/li&gt;
&lt;li&gt;&lt;b&gt;204인 경우에는 별도로 return문을 적지 않아도 응답으로 반환할 데이터가 없기 때문에&lt;/b&gt; 먼저 처리를 해주면 에러를 발생시키고 그게 아닌 경우에는 데이터를 삭제해줄 수 있어 코드 가독성을 위해 에러문을 위로 처리(if no todo:)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;swagger 확인&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1719&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjYbRc/dJMcaay4cuG/SGaxKb466naCu8X4YV9pr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjYbRc/dJMcaay4cuG/SGaxKb466naCu8X4YV9pr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjYbRc/dJMcaay4cuG/SGaxKb466naCu8X4YV9pr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjYbRc%2FdJMcaay4cuG%2FSGaxKb466naCu8X4YV9pr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1719&quot; height=&quot;784&quot; data-origin-width=&quot;1719&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1441&quot; data-origin-height=&quot;799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGDZHG/dJMcaaZ5Eh7/Gr8WDQlUekNsRs8YwOYCbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGDZHG/dJMcaaZ5Eh7/Gr8WDQlUekNsRs8YwOYCbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGDZHG/dJMcaaZ5Eh7/Gr8WDQlUekNsRs8YwOYCbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGDZHG%2FdJMcaaZ5Eh7%2FGr8WDQlUekNsRs8YwOYCbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1441&quot; height=&quot;799&quot; data-origin-width=&quot;1441&quot; data-origin-height=&quot;799&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>FastAPI/데이터베이스</category>
      <author>hyunai</author>
      <guid isPermaLink="true">https://hyunai.tistory.com/150</guid>
      <comments>https://hyunai.tistory.com/150#entry150comment</comments>
      <pubDate>Fri, 26 Jun 2026 17:19:15 +0900</pubDate>
    </item>
    <item>
      <title>ORM 적용 - PATCH API</title>
      <link>https://hyunai.tistory.com/149</link>
      <description>&lt;pre id=&quot;code_1782375314258&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# orm.py

    def done(self) -&amp;gt; &quot;ToDo&quot;:
        self.is_done = True
        return self

    def undone(self) -&amp;gt; &quot;ToDo&quot;:
        self.is_done = False
        return self&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인스턴스 메소드로 정의&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이렇게 인스턴스 메소드를 정의해서 데이터를 업데이트 관리하는 이유는 &lt;b&gt;유지보수가 훨씬 편하기 때문이다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;예를 들어 만약 is_done을 True로 업데이트할 때 이메일을 발송한다고 하면 여기에 sendEmail 같은 식으로 코드를 구현하게 되면은 이 updateDone을 호출할 때마다 이 코드가 자연적으로 호출이 돼서 좀 더 코드 반복 없이 작업을 할 수 있게 된다.&lt;/li&gt;
&lt;li&gt;근데 만약 isDone을 true로 하는 업데이트 관련 코드가 밖에 있다면 여러 군데애 파편화되어 있어 반복되는 작업이 많아진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1782376330617&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# repository.py

def update_todo(session: Session, todo:ToDo) -&amp;gt; ToDo:
    session.add(instance=todo)
    session.commit()
    session.refresh(instance=todo)
    return todo&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;create 부분이랑 코드가 같다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;왜? 마찬가지로 업데이트된 todo 객체를 받아서 session에 추가를 해주고 session에 커밋을 하게 되면 이 때 데이터가 저장이 된다.&lt;/li&gt;
&lt;li&gt;여기선 refresh가 필수는 아니지만 최신 데이터를 DB에 잘 저장된 데이터를 가져오는지 확인 차 넣어준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1782376646393&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# main.py

@app.patch(&quot;/todos/{todo_id}&quot;, status_code=200)
def update_todo_handler(
    todo_id : int,
    is_done: bool = Body(..., embed=True),
    session: Session = Depends(get_db),
):
    # todo id를 조회해서 단일 todo를 조회
    todo: ToDo | None = get_todo_by_todo_id(session=session, todo_id=todo_id)
    # todo가 있다면
    if todo:
        # 밑의 일반 if문이랑 같은 의미 -&amp;gt; 삼항연산자
        # Patch 요청으로 is_done을 true를 보내면 True로 false를 보내면 False로 응답
        # 여기까지 하면 DB에 반영이 되진않는다.
        todo.done() if is_done else todo.undone()
        # if is_done is True:
        #     todo.done()
        # else:
        #     todo.undone()
        todo: ToDo = update_todo(session=session, todo=todo)
        return ToDoSchema.model_validate(todo)
    # 만약 todo가 없다면
    raise HTTPException(status_code=404, detail=&quot;Todo Not Fount&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ToDo id path를 통해서 먼저 접근하고자 하는 데이터가 유효한지 체크&lt;br /&gt;=&amp;gt; ToDo가 존재하는지 확인&lt;/li&gt;
&lt;li&gt;ToDo가 존재하면 update&lt;/li&gt;
&lt;li&gt;ToDo가 존재하는 경우에 is_done 값 요청이 true면 &lt;b&gt;done()&lt;/b&gt;를 통해 true로 update false면 &lt;b&gt;undone()&lt;/b&gt;을 통해 false로 update&lt;/li&gt;
&lt;li&gt;없다면 404 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;swagger 확인&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SCkfs/dJMcaaeKle4/BwZvkNqrzmfV7d8CTukzKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SCkfs/dJMcaaeKle4/BwZvkNqrzmfV7d8CTukzKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SCkfs/dJMcaaeKle4/BwZvkNqrzmfV7d8CTukzKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSCkfs%2FdJMcaaeKle4%2FBwZvkNqrzmfV7d8CTukzKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1810&quot; height=&quot;816&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1806&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CNCRG/dJMcafmP2dI/YpWpoAKnNGA5CHHXdrpPcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CNCRG/dJMcafmP2dI/YpWpoAKnNGA5CHHXdrpPcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CNCRG/dJMcafmP2dI/YpWpoAKnNGA5CHHXdrpPcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCNCRG%2FdJMcafmP2dI%2FYpWpoAKnNGA5CHHXdrpPcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1806&quot; height=&quot;700&quot; data-origin-width=&quot;1806&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1825&quot; data-origin-height=&quot;820&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9EtX9/dJMcacwQGA6/4IcACDUIpANgM9mj6yPXe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9EtX9/dJMcacwQGA6/4IcACDUIpANgM9mj6yPXe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9EtX9/dJMcacwQGA6/4IcACDUIpANgM9mj6yPXe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9EtX9%2FdJMcacwQGA6%2F4IcACDUIpANgM9mj6yPXe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1825&quot; height=&quot;820&quot; data-origin-width=&quot;1825&quot; data-origin-height=&quot;820&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1798&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hbdea/dJMcahZbxI8/M5DkPYmwLKjPMt35KKBkYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hbdea/dJMcahZbxI8/M5DkPYmwLKjPMt35KKBkYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hbdea/dJMcahZbxI8/M5DkPYmwLKjPMt35KKBkYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHbdea%2FdJMcahZbxI8%2FM5DkPYmwLKjPMt35KKBkYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1798&quot; height=&quot;700&quot; data-origin-width=&quot;1798&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>FastAPI/데이터베이스</category>
      <author>hyunai</author>
      <guid isPermaLink="true">https://hyunai.tistory.com/149</guid>
      <comments>https://hyunai.tistory.com/149#entry149comment</comments>
      <pubDate>Thu, 25 Jun 2026 17:42:39 +0900</pubDate>
    </item>
    <item>
      <title>ORM 적용 - POST API</title>
      <link>https://hyunai.tistory.com/148</link>
      <description>&lt;pre id=&quot;code_1782274887951&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# request.py
from pydantic import BaseModel


class CreateToDoRequest(BaseModel):
    contents: str
    is_done: bool


# orm.py
from schema.request import CreateToDoRequest

    # 요청 받은 requestBody를 ORM 객체로 변환해주는 클래스 method
    @classmethod
    def create(cls, request : CreateToDoRequest) -&amp;gt; &quot;ToDo&quot;:
        return cls(
            contents=request.contents,
            is_done=request.is_done,
        )&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 &lt;b&gt;classmethod&lt;/b&gt;는 request를 그대로 전달을 받는다.&lt;/li&gt;
&lt;li&gt;return에 cls에 맞는 데이터로 매핑을 해주면 되는데 앞서 id 값은 데이터베이스에 의해 자동으로 결정하도록 했기 때문에 별도로 지정해주지 않는다.&lt;/li&gt;
&lt;li&gt;Pydantic을 통해 전달 받은 Pydantic으로 request를 받아서 이제 &lt;b&gt;ORM 객체를 생성해&lt;/b&gt;주게 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;여기까지는 데이터베이스에 실제로 저장을 한 것은 아니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;@classmethod 와 cls&lt;/b&gt;&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;파이썬 메서드 종류부터 이해하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메서드 3종류 비교&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 84px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;종류&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt; 선언 방식 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt; 첫 번째 파라미터 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt; 접근 가능한 것 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;일반 메서드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;그냥 def&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;self (인스턴스)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;인스턴스 속성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;클래스 메서드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;@classmethod&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;cls (클래스)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;클래스 자체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;정적 메서드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;@staticmethod&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;둘 다 접근 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;self 랑 cls 차이부터 이해하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;self vs cls 비교&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class ToDo:
    # 일반 메서드 &amp;rarr; self = 만들어진 객체 자신
    def get_contents(self):
        return self.contents  # 객체의 속성에 접근
    
    # 클래스 메서드 &amp;rarr; cls = ToDo 클래스 자체
    @classmethod
    def create(cls, request):
        return cls(...)  # ToDo(...) 와 완전히 같은 말
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;self vs cls 시각화&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;self = 만들어진 붕어빵 (인스턴스)
cls  = 붕어빵 틀 (클래스 자체)

ToDo 클래스 (붕어빵 틀)
    &amp;darr; ToDo() 호출
todo 인스턴스 (붕어빵)
    &amp;darr;
todo.contents, todo.is_done 접근 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@classmethod 가 뭔지&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;classmethod 없을 때 vs 있을 때&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# classmethod 없이 객체 생성
todo = ToDo(
    contents=request.contents,
    is_done=request.is_done,
)

# classmethod로 객체 생성
todo = ToDo.create(request=request)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 결과는 같다. 차이는 &lt;b&gt;&quot;생성 로직을 클래스 안에 캡슐화했냐&quot;&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;cls(...) 가 왜 ToDo(...) 랑 같냐면&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cls 동작 원리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;class ToDo(Base):
    @classmethod
    def create(cls, request: CreateToDoRequest) -&amp;gt; &quot;ToDo&quot;:
        #        &amp;uarr;
        #    cls = ToDo 클래스 자체를 가리킴
        
        return cls(              # = ToDo() 랑 완전히 같음
            contents=request.contents,
            is_done=request.is_done,
        )

# 실제로 이렇게 호출하면
todo = ToDo.create(request=request)

# 내부적으로 이렇게 동작하는 것과 같음
todo = ToDo(
    contents=request.contents,
    is_done=request.is_done,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;왜 굳이 @classmethod로 만드냐면&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;classmethod 장점&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 밖에서 직접 생성하는 방식
@app.post(&quot;/todos&quot;)
def create_todo_handler(request: CreateToDoRequest):
    # 핸들러가 ToDo 생성 방법을 알아야 함
    todo = ToDo(
        contents=request.contents,
        is_done=request.is_done,
    )
    # 만약 나중에 필드가 추가되면?
    # 모든 핸들러를 다 찾아서 수정해야 함

# classmethod로 만드는 방식
@app.post(&quot;/todos&quot;)
def create_todo_handler(request: CreateToDoRequest):
    # 핸들러는 그냥 create()만 호출하면 됨
    todo = ToDo.create(request=request)
    # 나중에 필드 추가되면 create()만 수정하면 됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;classmethod 쓰는 이유&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 84px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이유&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt; 설명 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;캡슐화&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;생성 로직이 ToDo 클래스 안에 모여있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;유지보수&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;생성 방법 바뀌면 create()만 수정하면 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;가독성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;ToDo.create(request) 가 ToDo(contents=..., is_done=...) 보다 읽기 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;반환 타입 -&amp;gt; &quot;ToDo&quot; 에서 왜 따옴표가 붙냐면&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;따옴표 있는 타입 힌트&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;@classmethod
def create(cls, request: CreateToDoRequest) -&amp;gt; &quot;ToDo&quot;:
#                                              &amp;uarr;
#                                         따옴표로 감싼 이유:
#                                         자기 자신 클래스를 타입 힌트로
#                                         쓸 때 따옴표 필요

# 왜냐면 ToDo 클래스가 아직 완전히 정의되기 전에
# 자기 자신을 참조하려고 하면 에러가 나기 때문
# 따옴표로 감싸면 &quot;나중에 확인해줘&quot; 라는 의미
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;전체 흐름 한눈에 보기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;create() 전체 동작 흐름&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;POST /todos 요청
{
  &quot;contents&quot;: &quot;FastAPI 공부&quot;,
  &quot;is_done&quot;: false
}
        &amp;darr;
create_todo_handler(request=CreateToDoRequest)
        &amp;darr;
ToDo.create(request=request) 호출
        &amp;darr;
cls = ToDo 클래스 자체
cls(contents=&quot;FastAPI 공부&quot;, is_done=False)
        &amp;darr;
ToDo 인스턴스 생성
todo.contents = &quot;FastAPI 공부&quot;
todo.is_done  = False
        &amp;darr;
session에 저장
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;핵심 요약:&lt;/b&gt;&lt;br /&gt;@classmethod = 인스턴스 없이 클래스 이름으로 바로 호출할 수 있는 메서드&lt;br /&gt;cls = 클래스 자체 (self는 인스턴스, cls는 클래스)&lt;br /&gt;cls(...) = ToDo(...) 랑 완전히 같은 말&lt;br /&gt;쓰는 이유 = 생성 로직을 클래스 안에 모아서 유지보수를 쉽게 하기 위해&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;pre id=&quot;code_1782277400207&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# main.py

@app.post(&quot;/todos&quot;, status_code=201)
# FastAPI가 알아서 reqeustBody를 CreateToDoRequest에 넣어서 처리를 해준다.
def create_todo_handler(request: CreateToDoRequest):
    todo: ToDo=ToDo.create(request=request)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변환된 ORM 객체를 통해서 DB에 실제로 저장해달라는 요청을 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1782281230524&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# repository.py

# todo를 ORM 객체로 전달을 받도록 설정해주고 return을 해준다. 
def create_todo(session: Session, todo:ToDo) -&amp;gt; ToDo:
    # 생성한 ORM 객체를 Session Object에 먼저 추가를 해준다.
    session.add(instance=todo)
    # db save
    # 이때 DB상의 ToDo id 값은 할당이 되어 저장이 된다.
    session.commit()
    # db를 다시 read -&amp;gt;이때 지금 메모리 상의 todo_id 결정
    session.refresh(instance=todo)
    return todo&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1782373800598&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# request.py
from pydantic import BaseModel

class CreateToDoRequest(BaseModel):
	# id 값은 DB에서 자동으로 올려주기 때문에 id 없애기
    contents: str
    is_done: bool


# main.py

@app.post(&quot;/todos&quot;, status_code=201)
# FastAPI가 알아서 reqeustBody를 CreateToDoRequest에 넣어서 처리를 해준다.
def create_todo_handler(
        request: CreateToDoRequest,
        session: Session = Depends(get_db)
) -&amp;gt; ToDoSchema:
    todo: ToDo=ToDo.create(request=request) # id=None
    todo: ToDo=create_todo(session=session, todo=todo) # id=int
    return ToDoSchema.model_validate(todo)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫번째 todo는&lt;b&gt; 아직 id 값이 none이다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;두번째 todo에서는 &lt;b&gt;create_todo에서 정해진 id 값이 들어간 todo&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;session&lt;/b&gt;을 통해서 todo에 todo가 &lt;b&gt;실제로 DB에 저장&lt;/b&gt;이 되고 리프레쉬된 두번째 todo가 이 변수에 다시 할당이 되게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;swagger에서 확인&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1713&quot; data-origin-height=&quot;765&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FkaYg/dJMb99UoGDS/oq64g2lKurc8LWmSoKRrK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FkaYg/dJMb99UoGDS/oq64g2lKurc8LWmSoKRrK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FkaYg/dJMb99UoGDS/oq64g2lKurc8LWmSoKRrK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFkaYg%2FdJMb99UoGDS%2Foq64g2lKurc8LWmSoKRrK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1713&quot; height=&quot;765&quot; data-origin-width=&quot;1713&quot; data-origin-height=&quot;765&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1789&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dU2Zr5/dJMcaa6T5Wx/SuwUQRD3CKWix7DxAAA190/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dU2Zr5/dJMcaa6T5Wx/SuwUQRD3CKWix7DxAAA190/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dU2Zr5/dJMcaa6T5Wx/SuwUQRD3CKWix7DxAAA190/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdU2Zr5%2FdJMcaa6T5Wx%2FSuwUQRD3CKWix7DxAAA190%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1789&quot; height=&quot;654&quot; data-origin-width=&quot;1789&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>FastAPI/데이터베이스</category>
      <author>hyunai</author>
      <guid isPermaLink="true">https://hyunai.tistory.com/148</guid>
      <comments>https://hyunai.tistory.com/148#entry148comment</comments>
      <pubDate>Wed, 24 Jun 2026 15:11:17 +0900</pubDate>
    </item>
  </channel>
</rss>