FastAPI CRUD — 생성·조회·수정·삭제
앞 편에서 만든 User 모델과 DB 세션 위에, 데이터를 만들고 읽고 고치고 지우는 네 동작을 한 번에 완성한다.
7편에서 우리는 SQLAlchemy 로 User 모델을 정의하고, 요청마다 Depends(get_db) 로 DB 세션을 받아 쓰는 구조까지 만들었다. 이제 그 토대 위에 진짜 알맹이를 올린다 — CRUD, 즉 데이터를 만들고(Create)·읽고(Read)·고치고(Update)·지우는(Delete) 네 동작이다.
거의 모든 백엔드 API 는 결국 이 네 가지의 반복이다. 회원 가입은 Create, 프로필 조회는 Read, 닉네임 변경은 Update, 탈퇴는 Delete. 이번 편을 끝내면 User 한 테이블에 대해 작동하는 완결된 API 한 벌을 갖게 된다.
1. CRUD 가 뭔가 — 네 동작과 HTTP 메서드
CRUD 는 데이터를 다루는 네 가지 기본 동작의 머리글자다. 그리고 REST API 에서는 이 네 동작이 각각 정해진 HTTP 메서드에 대응한다. 외울 필요 없이 표 한 장이면 평생 쓴다.
| CRUD | 뜻 | HTTP 메서드 | 예시 경로 |
|---|---|---|---|
| Create | 새로 만든다 | POST | POST /users |
| Read | 읽어 온다 | GET | GET /users · GET /users/1 |
| Update | 고친다 | PUT / PATCH | PUT /users/1 |
| Delete | 지운다 | DELETE | DELETE /users/1 |
여기 깔린 규칙은 의외로 단순하다 — "무엇을 할지" 는 메서드가, "무엇에 할지" 는 경로가 말한다. 같은 /users/1 주소라도 GET 으로 부르면 조회, DELETE 로 부르면 삭제다. 행동(동사)을 URL 에 넣지 않고 메서드로 분리하는 것이 REST 의 핵심이다.
2. Create & Read — 만들고 읽기
먼저 전제를 다시 깔아 둔다. 앞 편에서 만든 것들을 그대로 가져온다 — SQLAlchemy User 모델, 세션을 주는 get_db, 그리고 3편에서 익힌 Pydantic 스키마다. Pydantic v2 에서는 ORM 객체를 그대로 응답으로 변환하기 위해 from_attributes=True (구버전의 orm_mode) 를 켠다.
입력과 출력 스키마를 나누는 건 일부러다. 클라이언트가 id 를 멋대로 정해 보내면 안 되고(그건 DB 가 부여한다), 응답에는 비밀번호 같은 필드를 숨길 수도 있어야 하기 때문이다. 이제 Create 와 Read 라우트를 적는다.
세 줄이 핵심이다 — db.add() 로 세션에 올리고, db.commit() 으로 DB 에 확정하고, db.refresh() 로 DB 가 부여한 id 를 객체에 되채운다. 생성 응답에는 status_code=201(Created)을 붙여 의미를 명확히 했다. 단건 조회에서 데이터가 없으면 HTTPException(status_code=404) 를 던진다 — 4편에서 배운 그 방식 그대로다. 빈 응답을 200 으로 돌려주는 것보다 정직하다.
3. Update & Delete — 고치고 지우기
나머지 두 동작도 패턴은 같다. 먼저 대상을 찾고(없으면 404), 그다음 손을 댄다. Update 는 찾은 객체의 필드를 새 값으로 바꾼 뒤 commit, Delete 는 db.delete() 후 commit 이다.
Update 에서 신기한 점 하나 — 우리는 SQL 의 UPDATE 문을 한 줄도 안 적었다. SQLAlchemy 세션이 객체의 변경을 추적하고 있다가, commit() 순간 바뀐 필드를 알아서 SQL 로 만들어 보낸다. 이걸 변경 추적(dirty tracking) 이라 부른다. Delete 응답은 status_code=204(No Content) — 삭제 후에는 돌려줄 데이터가 없으니 본문 없이 "처리됐다" 만 알린다.
db.add()·db.delete() 나 필드 변경까지만 하고 commit() 을 안 하면, 세션이 끝날 때 변경이 통째로 롤백돼 DB 에는 아무 일도 안 일어난다. "코드는 맞는데 DB 에 안 들어가요" 의 90% 는 빠진 commit 이다. 쓰기 동작 뒤엔 commit 을 습관으로.
4. crud.py 로 분리 — 라우트는 얇게
위 코드를 보면 db.get(...), 404 체크, db.add/commit 같은 DB 로직이 라우트 함수마다 반복된다. 라우트가 커지면 "HTTP 처리" 와 "DB 처리" 가 뒤엉켜 읽기도, 테스트하기도 어려워진다. 그래서 실무에서는 DB 로직을 crud.py 의 평범한 함수로 빼내고, 라우트는 그 함수를 부르기만 하게 만든다.
이제 라우트는 얇아진다. HTTP 의 일(404 를 던질지 말지)만 남고, DB 의 일은 crud 함수에 위임한다.
이렇게 나누면 같은 crud.get_user() 를 다른 라우트에서도 재사용할 수 있고, 테스트할 때 HTTP 서버를 띄울 필요 없이 함수에 세션만 넘겨 직접 검증할 수 있다. 6편에서 다룬 라우터 분리와 같은 정신이다 — 각 파일이 한 가지 일만 하게.
요약
이번 편에서 CRUD 한 벌을 완성했다. POST 로 만들고(add·commit·refresh), GET 으로 읽고(목록은 .all(), 단건은 없으면 404), PUT 으로 고치고(필드 변경 후 commit), DELETE 로 지운다(delete·commit, 204). 모든 쓰기 동작의 끝에는 commit() 이 있고, 대상이 없을 땐 정직하게 HTTPException(404) 를 던진다. 그리고 DB 로직은 crud.py 로 빼서 라우트를 얇게 유지했다. 이제 User 테이블 하나에 대한 API 가 완결됐다.
하지만 빠진 게 하나 있다 — 지금은 아무나 남의 유저를 수정하고 삭제할 수 있다. 다음 편에서 이 문을 잠근다.
다음 편 예고 — 9편: 인증, OAuth2 와 JWT
로그인한 사람만 자기 데이터를 다룰 수 있게. OAuth2 비밀번호 흐름과 JWT 토큰으로 API 에 자물쇠를 단다.