타임존 검수 보고서
검수일: 2026-02-10
대상: o2o-castad-backend 전체 프로젝트
기준: 서울 타임존(Asia/Seoul, KST +09:00)
1. 타임존 설정 현황
1.1 FastAPI 전역 타임존 (config.py:15)
TIMEZONE = ZoneInfo(os.getenv("TIMEZONE", "Asia/Seoul"))
ZoneInfo를 사용한 aware datetime 기반
.env로 오버라이드 가능 (기본값: Asia/Seoul)
1.2 타임존 유틸리티 (app/utils/timezone.py)
def now() -> datetime:
return datetime.now(TIMEZONE) # aware datetime (tzinfo=Asia/Seoul)
def today_str(fmt="%Y-%m-%d") -> str:
return datetime.now(TIMEZONE).strftime(fmt)
- 모든 모듈에서
from app.utils.timezone import now 사용 권장
- 반환값은 aware datetime (tzinfo 포함)
1.3 데이터베이스 타임존
- MySQL
server_default=func.now() → DB 서버의 시스템 타임존 사용
- DB 생성 시 서울 타임존으로 설정됨 →
func.now()는 KST 반환
2. 검수 결과 요약
| 구분 |
상태 |
비고 |
datetime.now() 직접 호출 |
✅ 정상 |
앱 코드에서 bare datetime.now() 사용 없음 |
datetime.utcnow() 사용 |
✅ 정상 |
프로젝트 전체에서 사용하지 않음 |
app.utils.timezone.now() 사용 |
✅ 정상 |
필요한 모든 곳에서 사용 중 |
모델 server_default=func.now() |
✅ 정상 |
DB 서버 타임존(서울) 기준 |
| naive/aware datetime 혼합 비교 |
⚠️ 주의 |
now().replace(tzinfo=None) 패턴으로 처리됨 |
3. 모듈별 상세 검수
3.1 app/user/services/jwt.py — ✅ 정상
| 위치 |
코드 |
판정 |
| L27 |
now() + timedelta(minutes=...) |
✅ 토큰 만료시간 계산에 서울 타임존 사용 |
| L52 |
now() + timedelta(days=...) |
✅ 리프레시 토큰 만료시간 |
| L110 |
now().replace(tzinfo=None) + timedelta(days=...) |
✅ DB 저장용 naive datetime 변환 |
3.2 app/user/services/auth.py — ✅ 정상
| 위치 |
코드 |
판정 |
| L171 |
user.last_login_at = now().replace(tzinfo=None) |
✅ DB 저장용 naive 변환 |
| L226 |
db_token.expires_at < now().replace(tzinfo=None) |
✅ naive datetime끼리 비교 |
| L486 |
revoked_at=now().replace(tzinfo=None) |
✅ DB 저장용 naive 변환 |
| L511 |
revoked_at=now().replace(tzinfo=None) |
✅ DB 저장용 naive 변환 |
3.3 app/user/api/routers/v1/auth.py — ✅ 정상
| 위치 |
코드 |
판정 |
| L464 |
user.last_login_at = now().replace(tzinfo=None) |
✅ 테스트 엔드포인트, DB 저장용 |
3.4 app/social/services.py — ✅ 정상
| 위치 |
코드 |
판정 |
| L308 |
current_time = now().replace(tzinfo=None) |
✅ DB datetime과 비교용 |
| L506 |
current_time = now().replace(tzinfo=None) |
✅ 토큰 만료 확인 |
| L577 |
now().replace(tzinfo=None) + timedelta(seconds=...) |
✅ 토큰 만료시간 DB 저장 |
| L639 |
now().replace(tzinfo=None) + timedelta(seconds=...) |
✅ 신규 계정 토큰 만료시간 |
| L693 |
now().replace(tzinfo=None) + timedelta(seconds=...) |
✅ 토큰 업데이트 시 만료시간 |
| L709 |
account.connected_at = now().replace(tzinfo=None) |
✅ 재연결 시간 DB 저장 |
3.5 app/social/worker/upload_task.py — ✅ 정상
| 위치 |
코드 |
판정 |
| L74 |
upload.uploaded_at = now().replace(tzinfo=None) |
✅ 업로드 완료시간 DB 저장 |
3.6 app/utils/logger.py — ✅ 정상
| 위치 |
코드 |
판정 |
| L27 |
from app.utils.timezone import today_str |
✅ 로그 파일명에 서울 기준 날짜 사용 |
| L89 |
today = today_str() |
✅ {날짜}_app.log 파일명 |
4. 모델 created_at / updated_at 패턴 검수
모든 모델의 created_at, updated_at 컬럼은 server_default=func.now()를 사용합니다.
| 모델 |
파일 |
created_at |
updated_at |
판정 |
| User |
app/user/models.py |
func.now() |
func.now() + onupdate |
✅ |
| RefreshToken |
app/user/models.py |
func.now() |
— |
✅ |
| SocialAccount |
app/user/models.py |
func.now() |
func.now() + onupdate |
✅ |
| Project |
app/home/models.py |
func.now() |
— |
✅ |
| Image |
app/home/models.py |
func.now() |
— |
✅ |
| Lyric |
app/lyric/models.py |
func.now() |
— |
✅ |
| Song |
app/song/models.py |
func.now() |
— |
✅ |
| SongTimestamp |
app/song/models.py |
func.now() |
— |
✅ |
| Video |
app/video/models.py |
func.now() |
— |
✅ |
| SNSUploadTask |
app/sns/models.py |
func.now() |
— |
✅ |
| SocialUpload |
app/social/models.py |
func.now() |
func.now() + onupdate |
✅ |
func.now()는 MySQL 서버의 NOW() 함수를 호출하므로 DB 서버 타임존(서울)이 적용됩니다.
5. now().replace(tzinfo=None) 패턴 분석
이 프로젝트에서는 aware datetime → naive datetime 변환 패턴이 일관되게 사용됩니다:
now().replace(tzinfo=None) # Asia/Seoul aware → naive (값은 KST 유지)
이유: MySQL의 DateTime 타입은 타임존 정보를 저장하지 않으므로(naive datetime), DB에 저장하거나 DB 값과 비교할 때 tzinfo를 제거해야 합니다.
검증: 이 패턴은 now()가 이미 서울 타임존 기준이므로, .replace(tzinfo=None) 후에도 값 자체는 KST 시간을 유지합니다. DB의 func.now()도 KST이므로 비교 시 일관성이 보장됩니다.
| 사용처 |
목적 |
일관성 |
jwt.py:110 |
refresh token 만료시간 DB 저장 |
✅ |
auth.py:171 |
마지막 로그인 시간 DB 저장 |
✅ |
auth.py:226 |
refresh token 만료 여부 비교 |
✅ |
auth.py:486,511 |
token 폐기 시간 DB 저장 |
✅ |
auth.py(router):464 |
테스트 엔드포인트 로그인 시간 |
✅ |
social/services.py:308,506 |
토큰 만료 비교 |
✅ |
social/services.py:577,639,693 |
토큰 만료시간 DB 저장 |
✅ |
social/services.py:709 |
재연결 시간 DB 저장 |
✅ |
social/worker/upload_task.py:74 |
업로드 완료시간 DB 저장 |
✅ |
6. 최종 결론
✅ 전체 판정: 정상 (PASS)
프로젝트 전반에 걸쳐 타임존 처리가 일관되게 구현되어 있습니다:
- bare
datetime.now() 미사용 — 앱 코드에서 타임존 없는 datetime.now() 직접 호출이 없음
datetime.utcnow() 미사용 — UTC 기반 시간 생성 없음
app.utils.timezone.now() 일관 사용 — 모든 서비스/라우터에서 유틸리티 함수 사용
- DB 저장 시 naive 변환 일관 —
now().replace(tzinfo=None) 패턴 통일
- 모델 기본값
func.now() 통일 — DB 서버 타임존(서울) 기준으로 자동 설정
- 비교 연산 안전 — DB의 naive datetime과 비교 시 항상 naive로 변환 후 비교
주의사항 (현재 문제 아님, 향후 참고용)
- DB 서버 타임존 변경 주의:
func.now()는 DB 서버 타임존에 의존하므로, DB 서버의 타임존이 변경되면 created_at/updated_at 등의 자동 생성 시간이 영향을 받습니다.
- 다중 타임존 확장 시: 현재는 단일 타임존(서울)만 사용하므로 문제없지만, 다국적 서비스 확장 시 UTC 기반 저장 + 표시 시 변환 패턴으로 전환을 고려할 수 있습니다.
replace(tzinfo=None) 패턴: 값은 유지하면서 타임존 정보만 제거하므로 안전하지만, 코드 리뷰 시 의도를 명확히 하기 위해 주석을 유지하는 것이 좋습니다(현재 social/services.py:307에 주석 존재).