본문 바로가기
AI 오케스트레이션 캠프/일차별 회고

16일차 회고|회원 인증·예외 처리와 상품 CRUD 통합|AI 오케스트레이션 개발자 국비지원

by 랩보다 AI 더 잘해지기 2026. 7. 31.
728x90

들어가며

오늘은 FastAPI 백엔드와 Streamlit 프론트엔드를 연결해 회원가입·로그인 기능의 예외 처리를 보완하고, 기존에 나누어져 있던 상품 등록·조회·수정·삭제 기능을 하나의 Product Management 페이지로 통합했다.

기능을 단순히 실행하는 데서 그치지 않고, 잘못된 요청이나 중복 데이터가 발생했을 때 사용자에게 적절한 안내를 보여주는 과정까지 실습했다. 특히 프론트엔드에서 발생한 오류가 실제로는 응답 데이터 구조나 API 주소 불일치 때문에 발생할 수 있다는 점을 직접 확인했다.

회원가입 중복 검사와 예외 처리

회원가입 요청이 들어오면 바로 회원 정보를 저장하지 않고, 먼저 같은 아이디가 데이터베이스에 존재하는지 확인했다.

전체 흐름은 다음과 같다.

입력한 아이디로 회원 조회
→ 같은 아이디가 있으면 409 Conflict 반환
→ 같은 아이디가 없으면 회원정보 저장
→ 비밀번호를 제외한 공개 정보 반환

중복된 아이디가 있을 때는 FastAPI의 HTTPException을 사용했다.

raise HTTPException(
    status_code=409,
    detail="이미 사용 중인 아이디입니다.",
)

여기서 raise는 오류 응답을 보내는 역할만 하는 것이 아니다. raise가 실행되는 순간 아래 코드는 더 이상 실행되지 않기 때문에 중복된 회원정보가 데이터베이스에 저장되는 것도 막아준다.

화면에서 중복 여부를 검사할 수도 있지만, API는 Streamlit 이외의 프로그램에서도 직접 호출할 수 있다. 따라서 중요한 데이터 검증은 백엔드에서도 반드시 진행해야 한다는 점을 배웠다.

응답에서 비밀번호 제외하기

데이터베이스에서 회원정보를 조회하면 아이디, 이름, 비밀번호가 모두 포함될 수 있다. 하지만 로그인이나 회원가입 성공 응답에 비밀번호를 그대로 포함하면 보안상 위험하다.

이를 막기 위해 공개해도 되는 정보만 정의한 Pydantic 모델을 사용했다.

return AuthPublic.model_validate(customer)

model_validate()는 딕셔너리 형태의 데이터를 Pydantic 모델의 규칙에 맞게 검사하고 변환한다. AuthPublic 모델에 id와 name만 정의되어 있다면 pwd는 최종 응답에서 제외된다.

현재 실습에서는 기능의 흐름을 이해하기 위해 비밀번호를 단순한 형태로 다뤘지만, 실제 서비스에서는 비밀번호를 평문으로 저장하면 안 된다. 원래 비밀번호로 되돌릴 수 없는 단방향 해시 방식으로 안전하게 저장해야 한다.

로그인 검증 과정

로그인에서는 사용자가 입력한 아이디로 회원을 조회한 뒤 비밀번호가 일치하는지 확인했다.

입력한 아이디로 회원 조회
→ 회원이 없으면 로그인 실패
→ 비밀번호가 다르면 로그인 실패
→ 모두 일치하면 공개 가능한 회원정보 반환

아이디가 없는 경우와 비밀번호가 틀린 경우에는 같은 오류 메시지를 반환했다.

raise HTTPException(
    status_code=401,
    detail="아이디 또는 패스워드가 올바르지 않습니다.",
)

두 상황을 구분해서 알려주면 공격자가 실제로 가입된 아이디를 알아낼 수 있다. 따라서 로그인 실패 원인을 구체적으로 공개하지 않는 것이 더 안전하다.

프론트엔드에서 백엔드 오류 처리하기

백엔드가 401, 409 등의 오류 상태 코드를 반환하더라도 프론트엔드에서 이를 처리하지 않으면 Streamlit 앱 전체에 긴 오류 화면이 나타날 수 있다.

공통 API 요청을 담당하는 core/api_client.py에서는 백엔드 오류를 BackendAPIError로 변환하고, 각 Streamlit 페이지에서는 try-except로 오류를 잡도록 구성했다.

try:
    result = product_create(product)

except BackendAPIError as error:
    st.error(str(error))

전체적인 오류 처리 흐름은 다음과 같다.

Streamlit에서 API 요청
→ FastAPI에서 입력값과 데이터 검증
→ 문제가 있으면 HTTP 오류 반환
→ api_client.py가 BackendAPIError로 변환
→ Streamlit이 오류를 잡아 안내 문구 출력

이를 통해 오류가 발생해도 앱 전체가 중단되지 않고, 사용자는 현재 화면에서 문제의 원인을 확인할 수 있다.

Product API 클라이언트 분리

상품 페이지에서 직접 httpx를 호출하지 않고 clients/product_client.py를 별도로 만들었다.

from core.api_client import request

product_client.py에서는 공통 request() 함수를 재사용해 상품 API별 요청만 정의했다.

def product_create(product: dict) -> dict:
    return request(
        "POST",
        "/product/create",
        json=product,
    )


def product_select_all() -> dict:
    return request(
        "GET",
        "/product/getall",
    )


def product_update(product_id: str, product: dict) -> dict:
    return request(
        "PUT",
        f"/product/{product_id}",
        json=product,
    )


def product_delete(product_id: str) -> dict:
    return request(
        "DELETE",
        f"/product/delete/{product_id}",
    )

파일별 역할도 좀 더 명확해졌다.

  • core/api_client.py: 백엔드 주소, 공통 HTTP 요청 및 오류 처리
  • clients/product_client.py: 상품 API의 요청 방식과 경로 관리
  • 05_product_management.py: 입력창, 버튼, 목록 등 화면 구성

이렇게 역할을 나누면 백엔드 주소나 요청 처리 방법이 바뀌어도 여러 화면을 일일이 수정하지 않아도 된다.

Product Management 페이지 통합

기존에 따로 만들었던 상품 등록 페이지와 조회·수정·삭제 페이지를 하나의 Product Management 페이지로 합쳤다.

통합 페이지에서는 다음 기능을 한 화면에서 사용할 수 있게 구성했다.

  • 상품 이름과 가격 입력
  • 상품 등록
  • 전체 상품 목록 조회
  • 선택한 상품 수정
  • 삭제 전 확인
  • 수정·삭제 후 목록 자동 갱신
  • 서버 오류 메시지 출력

상품 등록에는 st.form()을 사용했다.

with st.form("product_create_form", clear_on_submit=True):
    product_name = st.text_input("NAME")

    product_price = st.number_input(
        "PRICE",
        min_value=1,
        step=1000,
    )

    create_submitted = st.form_submit_button("상품 등록")

clear_on_submit=True를 설정하면 등록 후 입력값을 초기화할 수 있다. 상품명에는 strip()을 사용해 앞뒤 공백을 제거하고, 빈 이름이 입력되지 않도록 검사했다.

상품 ID 입력 문제 확인

처음에는 상품 등록 화면에 ID 입력창을 만들었다. 같은 ID를 입력하면 중복 오류가 발생할 것으로 예상했지만, 실제 데이터베이스에는 상품이 계속 추가되었다.

데이터를 확인해보니 사용자가 입력한 ID가 저장된 것이 아니었다. 백엔드에서 날짜와 시간을 기반으로 새로운 상품 ID를 자동 생성하고 있었다.

20260731173107441221
20260731173116112010
20260731173158889193

즉, 화면에서 1을 여러 번 입력해도 백엔드가 매번 다른 ID로 바꾸어 저장하므로 중복이 발생하지 않았다.

이에 따라 상품 등록 화면에서는 ID 입력창을 제거하고 이름과 가격만 전달하도록 수정했다.

product = {
    "name": product_name.strip(),
    "price": int(product_price),
}

이번 문제를 통해 화면만 보고 데이터의 동작을 판단하기보다, 실제 데이터베이스에 어떤 값이 저장되었는지 확인하는 과정이 중요하다는 것을 알게 됐다.

API 응답 구조에 맞게 데이터 꺼내기

상품 등록과 전체 조회 API는 상품 데이터를 바로 반환하지 않고 다음과 같이 감싸서 반환했다.

{
    "success": True,
    "message": "요청에 성공했습니다.",
    "data": [...]
}

그런데 처음에는 반환 결과를 바로 반복문에 넣었다.

for product in result:

이 경우 실제 상품이 아니라 "success", "message", "data"라는 딕셔너리의 키를 하나씩 가져오게 된다. 결국 문자열에 product["name"]을 사용하면서 오류가 발생했다.

따라서 먼저 data 안의 실제 상품 목록을 꺼내야 했다.

result = product_select_all()
products = result.get("data", [])

이후에야 각 상품의 정보를 정상적으로 사용할 수 있었다.

for product in products:
    st.write(product["name"])
    st.write(product["price"])

API를 연결할 때는 요청 주소뿐만 아니라 서버가 반환하는 JSON 구조까지 정확히 확인해야 한다는 점을 배웠다.

상품별 수정·삭제 기능 구현

전체 상품 목록에는 각 상품마다 수정과 삭제 버튼을 배치했다. 자동 생성된 긴 ID를 사용자가 직접 입력할 필요 없이, 버튼을 누른 상품의 ID가 자동으로 전달되도록 만들었다.

수정 버튼을 누르면 기존 상품명과 가격이 입력된 대화상자가 표시된다.

@st.dialog("상품 수정")
def show_update_dialog(product: dict) -> None:
    update_name = st.text_input(
        "NAME",
        value=product["name"],
    )

삭제 버튼을 누르면 바로 삭제하지 않고 확인 대화상자를 보여주도록 했다.

@st.dialog("상품 삭제")
def show_delete_dialog(product: dict) -> None:
    st.warning("정말 삭제하시겠습니까?")

수정이나 삭제가 완료된 후에는 st.rerun()을 실행해 상품 목록을 다시 불러왔다. 덕분에 사용자가 페이지를 직접 새로고침하지 않아도 변경 결과를 바로 확인할 수 있었다.

상품 수정 API 주소 오류 해결

상품은 목록에서 정상적으로 조회됐지만, 수정을 실행하면 “존재하지 않습니다”라는 메시지가 나오는 문제가 발생했다.

상품 ID는 실제 데이터베이스에 존재했기 때문에 ID 자체의 문제처럼 보였지만, 코드를 비교한 결과 프론트엔드와 백엔드의 수정 API 주소가 서로 달랐다.

프론트엔드 요청: /product/update/{product_id}
실제 백엔드 주소: /product/{product_id}

잘못된 주소를 호출하고 있었기 때문에 백엔드는 해당 상품을 찾지 못한 것으로 처리했다. product_client.py의 수정 주소를 실제 백엔드 라우터와 동일하게 변경해 문제를 해결했다.

def product_update(product_id: str, product: dict) -> dict:
    return request(
        "PUT",
        f"/product/{product_id}",
        json=product,
    )

상품 ID는 매우 긴 값이므로 함수의 자료형도 int보다 str로 지정했다.

이 문제를 해결하면서 프론트엔드에서 “존재하지 않는다”는 메시지가 나왔다고 해서 반드시 데이터가 없는 것은 아니라는 사실을 알게 됐다. 요청 방식, API 경로, 전달한 ID, 백엔드 라우터를 함께 확인해야 정확한 원인을 찾을 수 있다.

오늘의 회고

오늘은 회원가입과 로그인 기능에 예외 처리를 추가하면서 백엔드 검증의 필요성과 안전한 오류 메시지 작성 방법을 배웠다. 또한 응답 모델을 이용해 비밀번호와 같은 민감한 정보를 제외하는 과정도 확인했다.

상품 관리 실습에서는 단순히 CRUD 기능을 한 화면에 모으는 것보다 프론트엔드와 백엔드의 약속을 정확히 맞추는 일이 더 중요했다. API 응답의 data 구조를 잘못 이해하거나 수정 주소가 조금만 달라도 화면에서는 전혀 다른 오류가 발생했다.

특히 상품 ID가 자동으로 생성된다는 사실을 데이터베이스에서 직접 확인하고, 화면 구조를 이에 맞게 수정한 과정이 기억에 남는다. 앞으로 API 오류가 발생하면 화면 코드만 반복해서 수정하기보다 다음 항목을 순서대로 확인해야겠다.

  • 실제 요청 주소와 HTTP 메서드
  • 백엔드 라우터에 정의된 주소
  • 요청으로 전달되는 데이터
  • 서버가 반환한 상태 코드와 JSON 구조
  • 데이터베이스에 실제 저장된 값

오늘 실습을 통해 프론트엔드, API 클라이언트, 백엔드, 데이터베이스가 서로 어떻게 연결되는지 한층 더 구체적으로 이해할 수 있었다.

댓글