플랫폼 내 공지 기능 풀스택 구현
#frontend#FastAPI#SQLAlchemy#Pydantic#PostgreSQL#pytest#React#TypeScript
Seoyoung Kim · Fullstack — API · 화면 · 릴리즈 단독 @ (주)썬더소프트코리아/System SW
2026-07-09 ~ 2026-07-21
공지사항(Notice) 기능 — API 설계·구현부터 화면·릴리즈까지

배경 — 왜 FE가 백엔드까지 했나
타 부서에서 요청한 기능이 FE는 끝났는데 API가 없어서 며칠씩 릴리즈가 밀리는 일이 반복됐다. 공지 기능은 CRUD 중심의 독립 도메인이라 기존 서비스와 결합이 거의 없었고, FE가 직접 구현해도 리스크가 낮다고 판단해 BE 팀과 합의한 뒤 단독으로 진행했다.
- 기간 — 2026.07.09 ~ 07.21 (2주), 기획 협의부터 운영 배포까지
- 범위 — FastAPI 라우터 · SQLAlchemy 모델 · Pydantic 스키마 · pytest, React 화면(공용 테이블 위젯 + 동적 폼 위), 기존 CI/CD로 배포
- 리뷰 — BE 팀 코드 리뷰 후 머지
핵심 구현 포인트
1. Soft Delete 정책
물리 삭제 대신 is_deleted 플래그를 사용했다. 모든 조회 쿼리에 is_deleted == False 필터를 기본 적용해 삭제된 데이터가 노출되지 않도록 했다. 운영 데이터 복구 가능성과 이력 추적을 고려한 결정.
2. 페이지네이션 + 검색
offset / limit기반 페이지네이션. 전체 개수는 count 서브쿼리로 별도 산출해total / limit / offset / result구조의 공통 응답(ResponsePagination[T], 제네릭)으로 반환- 키워드 검색은 필터 유틸로 분리해 쿼리 조립 로직과 검색 조건을 분리
3. N+1 쿼리 방지
공지마다 카테고리(관계 테이블)를 참조하는 구조라, 목록 조회 시 건별 쿼리가 발생하지 않도록 joinedload로 즉시 로딩을 적용했다. 작성자 정보는 outer join으로 한 번에 조회. 쿼리 로그로 목록 1회 조회 시 쿼리 수가 고정되는지 확인했다.
4. 동적 카테고리 매핑
카테고리는 하드코딩 enum이 아니라 DB에서 관리되는 동적 커스텀 필드다. 요청은 label(문자열)로 받고, 서비스 레이어에서 label → FK id로 변환해 저장한다. 카테고리 추가·변경 시 코드 배포 없이 대응된다.
5. 방어적 스키마 설계 (Pydantic)
- FE가 카테고리를 빈 문자열로 보내는 케이스를
field_validator로 흡수해 기본값("전체")으로 변환 — 클라이언트가 실제로 어떤 값을 보내는지 알고 있어서 넣을 수 있었던 방어 로직 - 마이그레이션 이전 데이터처럼 카테고리 FK가 비어 있는 레거시 데이터도 응답 변환 단계에서 기본값으로 처리해 500 없이 서빙
6. 동시성 제어 — 상단 고정 공지 중복 방지
상단 고정 공지는 한 번에 하나만 존재해야 하는데, 관리자 두 명이 동시에 고정 처리하면 둘 다 고정되는 경합이 생길 수 있다. 고정 처리 시 SELECT ... FOR UPDATE로 기존 고정 행을 잠근 뒤 해제·갱신하도록 해, 동시 요청에서도 고정 공지가 하나로 유지되도록 했다. pytest에서 동시 요청 케이스로 검증.
7. 화면 구현과 릴리즈
- 공지 목록·작성·상단 고정 관리 화면을 통합 플랫폼의 공용 테이블 위젯과 동적 폼 위에서 구현. 일반 사용자에게는 홈 화면 팝업으로 노출
- API와 화면을 함께 완성한 뒤 기존 CI/CD 파이프라인으로 배포
결과
- 착수부터 운영 배포까지 2주. 기존 흐름(FE 완료 → BE 대기 → 릴리즈)에서 대기 구간이 사라졌다
- BE 팀 리뷰를 통과해 기존 백엔드 코드베이스 규칙 안에서 머지
배운 점
- FE 경험이 API 설계에 직접 쓰인다. 클라이언트가 어떤 응답 형태를 원하는지, 어떤 비정상 입력을 보낼 수 있는지 알고 있어서 스키마와 방어 로직을 소비자 관점에서 설계할 수 있었다.
- ORM 로딩 전략(N+1), 트랜잭션과 락처럼 FE에서는 접하지 못했던 문제를 실제 코드로 다뤘다.
- 설계 결정(soft delete · row lock · eager loading · 방어적 스키마)을 먼저 세운 뒤 구현에 AI 코딩 도구를 활용했고, 생성된 코드는 pytest와 쿼리 로그로 N+1이 실제로 사라지는지, 락이 실제로 걸리는지 직접 확인한 뒤 배포했다.