8.9 KiB
8.9 KiB
skill-email DB 연결 구현 완료 보고서
작업일: 2025-08-19
작업자: 희재
상태: 완료
1. 작업 개요
목적
skill-email 서비스가 Gmail OAuth 토큰을 파일이 아닌 PostgreSQL 데이터베이스에서 직접 조회하도록 변경
배경
- 기존: 파일 기반 토큰 관리 (FileCredentialsProvider)
- 변경: PostgreSQL gmail_tokens 테이블 기반 관리 (DBCredentialsProvider)
- 이유: 중앙집중식 토큰 관리, 보안 강화, 다중 서비스 통합
2. 구현 내용
2.1 의존성 추가
파일: /home/heejae/skill-email/requirements.txt
psycopg2-binary>=2.9.9 # PostgreSQL 연결용
2.2 환경 설정
파일: /home/heejae/skill-email/.env
# Skill Email Configuration
PORT=8501
# Token Provider Configuration
TOKEN_PROVIDER=database
# PostgreSQL Connection (SSH tunnel)
POSTGRES_CONNECTION_STRING=postgresql://robeings:robeings@localhost:5433/auth_db
# Service Name
SERVICE_NAME=skill-email
# Log Level
LOG_LEVEL=INFO
2.3 DBCredentialsProvider 구현
파일: /home/heejae/skill-email/services/db_credentials_provider.py
주요 기능
-
연결 풀 관리
- SimpleConnectionPool 사용 (최소 1개, 최대 5개 연결)
- 연결 재사용으로 성능 최적화
-
get_credentials() 메서드
def get_credentials(self, user_id: str) -> Optional[Credentials]: # gmail_tokens 테이블에서 조회 # is_equipped=true인 토큰만 조회 # token_data + oauth_config 컬럼 모두 활용- gmail_tokens 테이블 조회
- token_data 컬럼: access_token, refresh_token
- oauth_config 컬럼: client_id, client_secret, token_uri
- Google Credentials 객체로 변환
-
save_credentials() 메서드
def save_credentials(self, user_id: str, creds: Credentials) -> bool: # 갱신된 토큰을 DB에 저장 # access_token 필드로 저장 (DB 구조 호환) -
check_token_exists() 메서드
def check_token_exists(self, user_id: str) -> bool: # 사용자의 장착된 토큰 존재 여부 확인
특별 처리 사항
- 토큰 필드 매핑: DB의
access_token↔ Credentials의token - oauth_config 파싱: JSON 형식의 client credentials 추출
- is_equipped 필터: 장착된 토큰만 조회
2.4 main.py 수정
파일: /home/heejae/skill-email/main.py
변경 내용
# 전역 credentials provider
credentials_provider = None
def get_gmail_service() -> GmailService:
global credentials_provider
if credentials_provider is None:
if TOKEN_PROVIDER == "database":
if not POSTGRES_CONNECTION_STRING:
logger.error("POSTGRES_CONNECTION_STRING not set")
raise ValueError("Database connection string required")
logger.info("Using database credentials provider")
credentials_provider = DBCredentialsProvider(POSTGRES_CONNECTION_STRING)
else:
logger.info("Using file credentials provider")
credentials_provider = FileCredentialsProvider(TOKEN_BASE)
return GmailService(credentials_provider)
- TOKEN_PROVIDER 환경변수로 File/DB 모드 선택
- 의존성 주입 패턴 사용 (Depends)
- 기존 FileCredentialsProvider 호환성 유지
3. 데이터베이스 구조
gmail_tokens 테이블
CREATE TABLE gmail_tokens (
id SERIAL PRIMARY KEY,
user_id VARCHAR(100),
robeing_id VARCHAR(50),
token_data JSONB, -- access_token, refresh_token
oauth_config JSONB, -- client_id, client_secret, token_uri
scopes TEXT[],
metadata JSONB,
expiry TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_equipped BOOLEAN DEFAULT FALSE,
equipped_to VARCHAR(50)
);
데이터 예시
// token_data
{
"token_type": "Bearer",
"access_token": "ya29.a0AS3H6NxLyI1Ss1VJ-_WUkpR...",
"refresh_token": "1//0eZrT..."
}
// oauth_config
{
"client_id": "1044056803209-0h8n5kcl22rvl740mdgpejp58v27mlro.apps.googleusercontent.com",
"client_secret": "GOCSPX-...",
"token_uri": "https://oauth2.googleapis.com/token"
}
4. 테스트 결과
4.1 연결 테스트
cd /home/heejae/skill-email && python3 test_db.py
- ✅ PostgreSQL 연결 성공
- ✅ gmail_tokens 테이블 접근 가능
- ✅ 3개 토큰 확인 (heejae, test, unknown)
4.2 Provider 테스트
cd /home/heejae/skill-email && python3 test_integration.py
- ✅ DBCredentialsProvider 생성 성공
- ✅ heejae 토큰 로드 성공
- Access Token: ya29.a0AS3H6NxLyI1Ss1VJ-_WUkpR...
- Refresh Token: 있음
- Client ID: 1044056803209-0h8n5kcl22rvl740...
- Client Secret: 있음
- Token URI: https://oauth2.googleapis.com/token
- Scopes: ['https://www.googleapis.com/auth/gmail.send']
4.3 통합 테스트 스크립트
파일: /home/heejae/skill-email/test_integration.py
- DBCredentialsProvider 테스트
- GmailService 통합 테스트
- 토큰 존재 여부 확인
5. SSH 터널 설정
연결 구성
# SSH 터널 생성 (51123 서버 → 51124 서버)
sshpass -p "19800508" ssh -f -N -L 5433:localhost:5432 admin@124.55.18.179 -p 51123
- 로컬 포트 5433 → 원격 PostgreSQL 5432
- 51123 서버 (124.55.18.179)의 PostgreSQL 접근
- auth_db 데이터베이스 사용
6. 문제 해결
6.1 토큰 필드 불일치
- 문제: DB는
access_token, Credentials는token사용 - 해결:
token=token_data.get('access_token') or token_data.get('token')
6.2 is_equipped 필터
- 문제: 모든 토큰이 is_equipped=false 상태
- 해결:
UPDATE gmail_tokens SET is_equipped=true WHERE user_id='heejae'
6.3 oauth_config 누락
- 문제: client_id, client_secret이 없어 API 호출 불가
- 해결: oauth_config 컬럼에서 추가로 조회
cur.execute(""" SELECT token_data, oauth_config FROM gmail_tokens WHERE user_id = %s AND is_equipped = true """)
6.4 인코딩 오류
- 문제: .env 파일 UTF-8 디코딩 오류
- 해결: .env 파일 재작성
7. 코드 구조
/home/heejae/skill-email/
├── .env # 환경변수 설정
├── requirements.txt # 의존성 목록
├── main.py # FastAPI 앱 (수정됨)
├── services/
│ ├── gmail_service.py # 기존 Gmail 서비스
│ ├── db_credentials_provider.py # 새로 추가된 DB Provider
│ └── ...
├── test_db.py # DB 연결 테스트
├── test_integration.py # 통합 테스트
└── ...
8. 보안 고려사항
- 토큰 암호화: DB에 평문 저장 (추후 암호화 고려)
- 연결 풀: 최대 5개 연결로 제한
- 에러 처리: 토큰 정보 로그에 노출 방지
- SSH 터널: PostgreSQL 직접 노출 방지
9. 성능 최적화
- 연결 풀링: SimpleConnectionPool 사용
- JSON 파싱: 한 번만 파싱하여 재사용
- 로깅 레벨: INFO로 설정 (프로덕션에서는 WARNING 권장)
10. 남은 작업
즉시 필요
- 51124 서버 배포
- Docker 이미지 빌드 및 테스트
- rb8001과 통합 테스트
추후 개선
- 토큰 암호화 저장
- 토큰 자동 갱신 로직
- 연결 풀 모니터링
- 캐싱 레이어 추가
11. 호환성
기존 시스템과 호환
- ✅ FileCredentialsProvider 유지 (TOKEN_PROVIDER=file)
- ✅ 기존 API 인터페이스 변경 없음
- ✅ 동일한 GmailService 사용
새로운 기능
- ✅ 중앙집중식 토큰 관리
- ✅ 다중 robeing 지원 (equipped_to 필드)
- ✅ 감사 로그 준비 (gmail_audit_logs 테이블과 연동 가능)
12. 검증 체크리스트
- PostgreSQL 연결 성공
- gmail_tokens 테이블 조회
- 토큰 데이터 파싱
- Credentials 객체 생성
- oauth_config 통합
- 에러 처리 동작
- 연결 풀 정상 작동
- 환경변수 기반 Provider 선택
- 실제 Gmail API 호출
- 토큰 갱신 플로우
13. 참고 명령어
# DB 연결 테스트
cd /home/heejae/skill-email && python3 test_db.py
# Provider 테스트
cd /home/heejae/skill-email && python3 test_integration.py
# SSH 터널 재연결
sshpass -p "19800508" ssh -f -N -L 5433:localhost:5432 admin@124.55.18.179 -p 51123
# 토큰 상태 확인
python3 -c "
import psycopg2
conn = psycopg2.connect('postgresql://robeings:robeings@localhost:5433/auth_db')
cur = conn.cursor()
cur.execute('SELECT user_id, is_equipped FROM gmail_tokens')
for row in cur.fetchall():
print(f'{row[0]}: equipped={row[1]}')
"
작업 완료: 2025-08-19 다음 단계: rb8001 통합 (4번 작업)