# 이메일 인프라 구현 계획서 (v1.0) ## 1. 개요 ### 1.1 목표 - B2B SaaS 환경에서 **자동 이메일 발송 시스템** 구축 - Google Workspace (개인용 Gmail 대체) 기반 SMTP 연동 - 비동기 태스크 기반 논블로킹 발송 - Phase 1: Google SMTP → Phase 2: AWS SES 마이그레이션 준비 ### 1.2 현재 상황 - **개발 단계**: 개인 Gmail 계정 (umsangdon@gmail.com) 사용 - **상용화 단계**: Google Workspace Business 또는 AWS SES로 전환 예정 - **타겟**: 회원가입, 로그인, 팀원 승인/거부 등 자동 메일 --- ## 2. 이메일 발송 유형 및 우선순위 ### 2.1 필수 (🔴) - Phase 1에서 구현 | 메일 유형 | 수신자 | 발송 타이밍 | 용도 | |----------|--------|-----------|------| | **OTP 인증** | 가입자/로그인자 | 가입/로그인 직후 | 이메일 소유권 확인 | | **가입 확인** | 신규 가입자 | 가입 완료 직후 | 가입 성공 안내 | | **팀원 승인 요청** | 마스터 | 팀원 가입 신청 시 | 승인/거부 요청 | | **팀원 승인/거부 결과** | 팀원 | 마스터 승인/거부 직후 | 결과 안내 | ### 2.2 중요 (🟠) - Phase 2에서 구현 | 메일 유형 | 수신자 | 발송 타이밍 | 용도 | |----------|--------|-----------|------| | **3개월 재인증 알림** | 모든 사용자 | 주기적 (크론) | 보안 점검 | | **비밀번호 변경 알림** | 해당 사용자 | 변경 직후 | 보안 알림 | | **Q&A 답변 회신** | 문의자 | 관리자 처리 후 | 고객 지원 | ### 2.3 선택사항 (🟡) - Phase 3 이후 | 메일 유형 | 수신자 | 발송 타이밍 | 용도 | |----------|--------|-----------|------| | **세션 만료 알림** | 활동 중인 사용자 | 만료 5분 전 | UX 개선 | | **주간 활동 리포트** | 마스터 | 매주 월요일 | 분석 정보 | | **결제/구독 안내** | 마스터 | 결제 시기 | 구독 관리 | --- ## 3. 아키텍처 설계 ### 3.1 전체 흐름도 ``` ┌─────────────────────────────────────────────────────┐ │ FastAPI 웹 서버 (Uvicorn) │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ API 라우터 │ │ │ │ - POST /api/auth/register │ │ │ │ - POST /api/auth/login │ │ │ │ - POST /api/auth/join-request │ │ │ │ - POST /api/admin/approve-join │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ │ ↓ (즉시 응답) │ │ ┌──────────────────────────────────────────────┐ │ │ │ 백그라운드 비동기 작업 │ │ │ │ (asyncio.create_task) │ │ │ │ │ │ │ │ ├─ OTP 생성 + 암호화 저장 │ │ │ │ ├─ 이메일 템플릿 렌더링 │ │ │ │ └─ SMTP 발송 시작 │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ │ ↓ │ │ ┌──────────────────────────────────────────────┐ │ │ │ 이메일 발송 모듈 │ │ │ │ (common_util/common_util_email.py) │ │ │ │ │ │ │ │ ├─ SMTP 클라이언트 (Google) │ │ │ │ ├─ 재시도 로직 (최대 3회) │ │ │ │ ├─ 로깅 및 모니터링 │ │ │ │ └─ 발송 이력 DB 저장 │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ └────────────────────┼────────────────────────────────┘ ↓ ┌──────────────────────────────┐ │ Google Gmail SMTP │ │ (smtp.gmail.com:587) │ │ + TLS 암호화 │ └──────────────────────────────┘ │ ↓ ┌──────────────────────────────┐ │ 수신자 이메일 서버 │ │ (Gmail, Naver, etc) │ └──────────────────────────────┘ ``` ### 3.2 개발 단계 상세 흐름 ``` 1단계: 사용자가 회원가입 버튼 클릭 ↓ 2단계: FastAPI 라우터 수신 - 이메일/비밀번호 검증 (Pydantic) - 중복 확인 - DB에 임시 사용자 생성 (status = PENDING) ↓ 3단계: 백그라운드 비동기 작업 시작 (즉시 응답) - OTP 코드 생성 (6자리 난수) - OTP 해시값 DB 저장 + 만료시간 기록 (5분) - 이메일 HTML 템플릿 렌더링 ↓ 4단계: 이메일 발송 (재시도 로직 포함) - 시도 1: SMTP 연결 및 발송 - 실패 시 2초 대기 후 재시도 - 3회 모두 실패 → 로그 기록 + 관리자 알림 ↓ 5단계: 클라이언트는 즉시 응답 받음 - "인증 코드를 이메일로 발송했습니다" - 사용자는 이메일 확인하여 OTP 입력 ``` --- ## 4. 개발 단계 설정 (Phase 1) ### 4.1 Google Gmail SMTP 설정 #### 단계 1: 앱 비밀번호 생성 ``` Google 계정 (umsangdon@gmail.com) 접속 ↓ Google 계정 관리 페이지 (myaccount.google.com) ↓ 왼쪽 메뉴: "보안" (Security) ↓ 2단계 인증 활성화 (이미 되어있으면 스킵) ↓ 앱 비밀번호 (App passwords) ├─ 앱: "메일(Mail)" ├─ 기기: "Windows" └─ [생성] 버튼 ↓ 16자리 비밀번호 생성 (예: abcd efgh ijkl mnop) ↓ .env 파일에 저장 ``` #### 단계 2: .env 파일 작성 ```bash # config/.env (개발용, .gitignore에 포함) # ===== Email Infrastructure (Phase 1) ===== SMTP_PROVIDER=google SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_USERNAME=umsangdon@gmail.com SMTP_PASSWORD=abcd efgh ijkl mnop # 위에서 생성한 앱 비밀번호 SMTP_USE_TLS=true # 발송 이메일 주소 EMAIL_FROM_ADDRESS=umsangdon@gmail.com EMAIL_FROM_NAME=Aislo Support EMAIL_NOREPLY_ADDRESS=umsangdon+noreply@gmail.com # 답장 불가 계정 # 이메일 발송 설정 EMAIL_RETRY_COUNT=3 EMAIL_RETRY_DELAY_SECONDS=2 EMAIL_OTP_VALID_MINUTES=5 EMAIL_NOTIFICATION_ENABLED=true # 관리자 이메일 (에러 발생 시 수신) ADMIN_EMAIL=umsangdon@gmail.com ``` #### 단계 3: config_system.py 수정 ```python # config/config_system.py import os from dotenv import load_dotenv load_dotenv() # ===== Email Configuration ===== SMTP_PROVIDER = os.getenv("SMTP_PROVIDER", "google") SMTP_HOST = os.getenv("SMTP_HOST", "smtp.gmail.com") SMTP_PORT = int(os.getenv("SMTP_PORT", "587")) SMTP_USERNAME = os.getenv("SMTP_USERNAME", "umsangdon@gmail.com") SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "") SMTP_USE_TLS = os.getenv("SMTP_USE_TLS", "true").lower() == "true" EMAIL_FROM_ADDRESS = os.getenv("EMAIL_FROM_ADDRESS", "umsangdon@gmail.com") EMAIL_FROM_NAME = os.getenv("EMAIL_FROM_NAME", "Aislo Support") EMAIL_NOREPLY_ADDRESS = os.getenv("EMAIL_NOREPLY_ADDRESS", "umsangdon+noreply@gmail.com") EMAIL_RETRY_COUNT = int(os.getenv("EMAIL_RETRY_COUNT", "3")) EMAIL_RETRY_DELAY_SECONDS = int(os.getenv("EMAIL_RETRY_DELAY_SECONDS", "2")) EMAIL_OTP_VALID_MINUTES = int(os.getenv("EMAIL_OTP_VALID_MINUTES", "5")) EMAIL_NOTIFICATION_ENABLED = os.getenv("EMAIL_NOTIFICATION_ENABLED", "true").lower() == "true" ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "umsangdon@gmail.com") ``` --- ## 5. 이메일 발송 모듈 (common_util_email.py) ### 5.1 기본 구조 ```python # common_util/common_util_email.py import smtplib import asyncio import logging from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from datetime import datetime from config.config_system import ( SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_USE_TLS, EMAIL_FROM_ADDRESS, EMAIL_FROM_NAME, EMAIL_RETRY_COUNT, EMAIL_RETRY_DELAY_SECONDS, ) logger = logging.getLogger(__name__) class EmailService: """Google SMTP를 통한 비동기 이메일 발송""" @staticmethod async def send_email( to_email: str, subject: str, body_html: str, body_text: str = None, retry_count: int = None ) -> bool: """ 비동기 이메일 발송 (재시도 로직 포함) Args: to_email: 수신자 이메일 subject: 제목 body_html: HTML 본문 body_text: 텍스트 본문 (선택) retry_count: 재시도 횟수 (기본값: 환경변수) Returns: 발송 성공 여부 (True/False) """ if retry_count is None: retry_count = EMAIL_RETRY_COUNT for attempt in range(retry_count): try: # 메시지 구성 msg = MIMEMultipart("alternative") msg["From"] = f"{EMAIL_FROM_NAME} <{EMAIL_FROM_ADDRESS}>" msg["To"] = to_email msg["Subject"] = subject msg["Date"] = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000") # 텍스트 본문 추가 (메일 클라이언트가 HTML 미지원 시) if body_text: msg.attach(MIMEText(body_text, "plain", "utf-8")) # HTML 본문 추가 msg.attach(MIMEText(body_html, "html", "utf-8")) # 별도 스레드에서 SMTP 발송 (논블로킹) await asyncio.to_thread( EmailService._send_smtp_blocking, msg, to_email ) logger.info(f"✅ 이메일 발송 성공: {to_email} (제목: {subject})") return True except Exception as e: logger.warning( f"⚠️ 이메일 발송 실패 (시도 {attempt+1}/{retry_count}): " f"{to_email} - {str(e)}" ) # 마지막 시도가 아니면 대기 후 재시도 (지수 백오프) if attempt < retry_count - 1: delay = EMAIL_RETRY_DELAY_SECONDS * (2 ** attempt) await asyncio.sleep(delay) continue logger.error(f"❌ 이메일 발송 최종 실패: {to_email} (제목: {subject})") return False @staticmethod def _send_smtp_blocking(msg, to_email): """ 동기 SMTP 발송 (asyncio.to_thread에서 실행) Args: msg: MIMEMultipart 메시지 객체 to_email: 수신자 이메일 """ with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: if SMTP_USE_TLS: server.starttls() server.login(SMTP_USERNAME, SMTP_PASSWORD) server.send_message(msg) ``` ### 5.2 이메일 템플릿 모듈 (common_util_email_templates.py) ```python # common_util/common_util_email_templates.py def get_otp_email_html(otp_code: str, valid_minutes: int = 5) -> tuple[str, str]: """ OTP 인증 이메일 반환 (HTML + Text) Returns: (html_body, text_body) """ html_body = f"""

🌲 Aislo

이메일 인증

안녕하세요!

아래 인증 코드를 입력하여 계정을 확인해주세요:

{otp_code}

이 코드는 {valid_minutes}분간 유효합니다.

⚠️ 주의: 본인이 요청하지 않았다면 이 이메일을 무시해주세요.

Aislo는 절대로 귀하의 인증 코드를 요청하지 않습니다.

""" text_body = f""" Aislo - 이메일 인증 안녕하세요! 아래 인증 코드를 입력하여 계정을 확인해주세요: {otp_code} 이 코드는 {valid_minutes}분간 유효합니다. 주의: 본인이 요청하지 않았다면 이 이메일을 무시해주세요. --- Aislo Support https://aislo.company """ return html_body, text_body def get_join_approval_request_email_html( user_email: str, user_name: str, company_name: str, approval_url: str ) -> tuple[str, str]: """ 팀원 가입 승인 요청 이메일 (마스터 수신) """ html_body = f"""

새로운 팀원 가입 신청

안녕하세요 관리자님,

{user_email}{company_name}에 가입을 신청했습니다.

신청자 정보:
이메일: {user_email}
이름: {user_name}
회사: {company_name}

아래 버튼을 클릭하여 승인 또는 거부해주세요:

승인/거부 하기

버튼이 작동하지 않으면 아래 링크를 직접 열어주세요:
{approval_url}

""" text_body = f""" 새로운 팀원 가입 신청 안녕하세요 관리자님, {user_email}이 {company_name}에 가입을 신청했습니다. 신청자 정보: - 이메일: {user_email} - 이름: {user_name} - 회사: {company_name} 아래 링크에서 승인 또는 거부해주세요: {approval_url} --- Aislo Support """ return html_body, text_body ``` --- ## 6. 라우터 통합 예시 ### 6.1 회원가입 라우터 (A07_Register_Router.py) ```python # A07_Register/A07_Register_Router.py from fastapi import APIRouter, HTTPException from pydantic import BaseModel, EmailStr from common_util.common_util_email import EmailService from common_util.common_util_email_templates import get_otp_email_html import asyncio import secrets router = APIRouter() class RegisterRequest(BaseModel): email: EmailStr password: str full_name: str @router.post("/api/auth/register") async def register(req: RegisterRequest): """회원가입 엔드포인트""" # 1. 검증: 이미 가입된 사용자인지 확인 existing_user = await db.get_user_by_email(req.email) if existing_user: raise HTTPException(status_code=400, detail="이미 가입된 이메일입니다.") # 2. OTP 코드 생성 (6자리 난수) otp_code = str(secrets.randbelow(1000000)).zfill(6) # 3. DB에 임시 사용자 생성 (status='PENDING', otp_hash 저장) otp_hash = hash_password(otp_code) # bcrypt 사용 user_id = await db.create_user( email=req.email, password_hash=hash_password(req.password), full_name=req.full_name, status="PENDING", otp_hash=otp_hash, otp_expires_at=datetime.utcnow() + timedelta(minutes=5) ) # 4. 백그라운드 태스크로 이메일 발송 (논블로킹) html_body, text_body = get_otp_email_html(otp_code) asyncio.create_task( EmailService.send_email( to_email=req.email, subject="[Aislo] 이메일 인증 코드", body_html=html_body, body_text=text_body ) ) # 5. 즉시 응답 (이메일 발송은 백그라운드에서 계속) return { "status": "success", "message": "인증 코드를 이메일로 발송했습니다. 5분 이내에 입력해주세요.", "user_id": user_id } @router.post("/api/auth/verify-otp") async def verify_otp(user_id: int, otp_code: str): """OTP 검증""" user = await db.get_user_by_id(user_id) # OTP 만료 확인 if datetime.utcnow() > user.otp_expires_at: raise HTTPException(status_code=400, detail="인증 코드가 만료되었습니다.") # OTP 일치 확인 (hash 비교) if not verify_password(otp_code, user.otp_hash): raise HTTPException(status_code=400, detail="인증 코드가 일치하지 않습니다.") # 사용자 상태 업데이트 await db.update_user(user_id, { "status": "ACTIVE", "otp_hash": None, "otp_expires_at": None }) return { "status": "success", "message": "이메일 인증 완료되었습니다." } ``` --- ## 7. 모니터링 및 로깅 ### 7.1 이메일 발송 이력 저장 (선택) 향후 추가할 선택사항: ```python # DB 테이블 CREATE TABLE email_logs ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT, to_email VARCHAR(255), subject VARCHAR(255), email_type ENUM('OTP', 'JOIN_APPROVAL', 'RESULT', 'OTHER'), status ENUM('SUCCESS', 'FAILED'), sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, retry_count INT DEFAULT 0, error_message TEXT, FOREIGN KEY (user_id) REFERENCES users(id), INDEX (sent_at) ); ``` ### 7.2 로깅 설정 (logging) ```python # config/config_system.py에 추가 import logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('logs/email.log'), logging.StreamHandler() ] ) ``` --- ## 8. Phase 2 마이그레이션 계획 (AWS SES) ### 8.1 AWS SES로 전환 시 변경 사항 ```python # 미래: AWS SES로 변경 (최소 코드 수정) SMTP_PROVIDER = "aws_ses" # config에서만 변경 SMTP_HOST = "email-smtp.ap-northeast-2.amazonaws.com" SMTP_USERNAME = "AKIA..." # AWS Access Key SMTP_PASSWORD = "..." # AWS Secret Key ``` **장점**: - 높은 배송 성공률 (99%) - 빠른 발송 속도 - 대량 발송 지원 - 스팸 필터 회피 능력 --- ## 9. 구현 체크리스트 (Phase 1) ### 필수 항목 - [ ] .env 파일 작성 (Google 앱 비밀번호) - [ ] config_system.py 이메일 설정 추가 - [ ] common_util_email.py 구현 - [ ] common_util_email_templates.py 구현 - [ ] A07_Register_Router.py 수정 (이메일 발송 통합) - [ ] 테스트: 실제 Gmail 발송 확인 - [ ] logging 설정 추가 - [ ] .gitignore에 .env 추가 ### 선택 항목 (Phase 2) - [ ] email_logs DB 테이블 생성 - [ ] 발송 이력 저장 로직 - [ ] AWS SES 마이그레이션 준비 - [ ] 이메일 템플릿 관리 시스템 --- ## 10. 테스트 방법 ### 10.1 개발 단계 테스트 ```bash # 1. .env 파일 작성 확인 cat config/.env | grep SMTP # 2. Python 테스트 스크립트 실행 python -m pytest tests/test_email_service.py # 3. Gmail 계정에서 발송 이력 확인 # Gmail 웹 → 설정 → 계정 및 가져오기 → 다른 이메일 주소에서 메일 보내기 ``` ### 10.2 이메일 도착 확인 ``` 발송된 이메일: - 받는사람: [테스트 이메일] - 제목: "[Aislo] 이메일 인증 코드" - 본문: HTML 템플릿 포함, 6자리 OTP 코드 - 발신: "Aislo Support " ``` --- ## 11. 보안 주의사항 ### 11.1 .env 파일 보안 ```bash # .gitignore에 추가 (필수) config/.env config/.env.local .env.* # Git에서 실수로 푸시된 경우 git rm --cached config/.env git commit -m "Remove .env from git tracking" ``` ### 11.2 Gmail 앱 비밀번호 회전 - **매년 1회**: 앱 비밀번호 재생성 권장 - **의심 활동**: 즉시 재생성 - **개발 → 상용화**: 새로운 비밀번호로 변경 ### 11.3 SMTP 연결 암호화 - ✅ TLS 사용 (포트 587) - ❌ SSL 포트 465는 피할 것 (deprecated) --- ## 12. 다음 단계 1. ✅ 이 계획서 검토 및 피드백 2. 📝 .env 파일 작성 + Google 앱 비밀번호 생성 3. 💻 common_util_email.py 코드 작성 4. 🧪 실제 Gmail 발송 테스트 5. 📋 backend.md 업데이트 (이메일 API 섹션 추가) --- **작성일**: 2026-07-07 **담당자**: 시스템 설계 팀 **상태**: 초안 (v1.0)