docs: Gmail 통합 UUID 변환 문제 해결 트러블슈팅 추가
- robeing-monitor UUID 변환 누락 문제 해결 - Gateway username to UUID 변환 구현 - skill-email 타입 불일치 및 UUID 변환 추가 - 전체 시스템 ID 체계 통일 과정 문서화
This commit is contained in:
parent
06a4c94bb0
commit
1cdb721b77
331
troubleshooting/250822_happybell80_Gmail통합_UUID변환_문제해결.md
Normal file
331
troubleshooting/250822_happybell80_Gmail통합_UUID변환_문제해결.md
Normal file
@ -0,0 +1,331 @@
|
||||
# Gmail 통합 시스템 UUID 변환 문제 해결
|
||||
|
||||
## 날짜: 2025-08-22
|
||||
## 작성자: happybell80 & Claude
|
||||
## 관련 서비스: robeing-monitor, robeing-gateway, skill-email
|
||||
|
||||
---
|
||||
|
||||
## 1. 문제 상황 요약
|
||||
|
||||
### 초기 발견
|
||||
- 프론트엔드에서 Gmail 아이템 조회 시 빈 결과 반환
|
||||
- robeing-monitor API 직접 호출은 정상 작동
|
||||
- Gateway를 통한 호출만 실패
|
||||
|
||||
### 근본 원인
|
||||
시스템 전반에 걸친 사용자 ID 체계 불일치:
|
||||
- **Frontend**: JWT에 username 저장 (예: "happybell80")
|
||||
- **Slack**: Slack user_id 사용 (예: "U091UNVE41M")
|
||||
- **PostgreSQL**: UUID 저장 (예: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
|
||||
|
||||
---
|
||||
|
||||
## 2. 문제 분석
|
||||
|
||||
### 2.1 robeing-monitor UUID 변환 누락
|
||||
|
||||
#### 증상
|
||||
```
|
||||
asyncpg.exceptions.DataError: invalid input for query argument $1: 'U091UNVE41M'
|
||||
(invalid UUID 'U091UNVE41M': length must be between 32..36 characters, got 11)
|
||||
```
|
||||
|
||||
#### 원인
|
||||
- robeing-monitor가 Slack user_id를 UUID로 변환하지 않고 직접 DB 조회
|
||||
- PostgreSQL gmail_tokens 테이블의 user_id 컬럼은 UUID 타입
|
||||
|
||||
### 2.2 Gateway 프록시 문제
|
||||
|
||||
#### 증상
|
||||
- Gateway 통과 시 X-User-Id 헤더가 username으로 전달
|
||||
- robeing-monitor는 UUID 기대하지만 username 수신
|
||||
|
||||
#### 원인
|
||||
- JWT payload에 username만 있고 UUID 없음
|
||||
- Gateway가 username을 UUID로 변환하는 로직 부재
|
||||
|
||||
### 2.3 skill-email 타입 불일치
|
||||
|
||||
#### 증상
|
||||
```python
|
||||
AttributeError: 'Credentials' object has no attribute 'value'
|
||||
```
|
||||
|
||||
#### 원인
|
||||
- FileCredentialsProvider: `Result[Credentials, EmailError]` 반환
|
||||
- DBCredentialsProvider: `Optional[Credentials]` 반환
|
||||
- GmailService가 Result 타입만 처리
|
||||
|
||||
---
|
||||
|
||||
## 3. 해결 과정
|
||||
|
||||
### 3.1 Phase 1: robeing-monitor UUID 변환 추가
|
||||
|
||||
#### 수정 파일: `robeing-monitor/app/api/items.py`
|
||||
|
||||
```python
|
||||
# UUID 변환 함수 추가
|
||||
def convert_to_uuid(user_id: str) -> str:
|
||||
"""Slack user_id를 UUID로 변환"""
|
||||
try:
|
||||
uuid.UUID(user_id) # 이미 UUID인지 확인
|
||||
return user_id
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# UUID5로 변환 (DNS namespace 사용)
|
||||
namespace = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')
|
||||
return str(uuid.uuid5(namespace, user_id))
|
||||
|
||||
# 모든 엔드포인트에 적용
|
||||
@router.get("/gmail")
|
||||
async def get_gmail_items(x_user_id: Optional[str] = Header(None)):
|
||||
user_uuid = convert_to_uuid(x_user_id) # 변환 추가
|
||||
# ... DB 조회
|
||||
```
|
||||
|
||||
#### 테스트 결과
|
||||
- U091UNVE41M → b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0 ✅
|
||||
- test-user → a5f4fe64-dd02-5776-b5f9-59fccda849ee ✅
|
||||
|
||||
### 3.2 Phase 2: Gateway username → UUID 변환
|
||||
|
||||
#### 수정 파일 1: `robeing-gateway/app/database.py`
|
||||
|
||||
```python
|
||||
async def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
|
||||
"""username으로 사용자 정보 및 UUID 조회"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
query = text("""
|
||||
SELECT
|
||||
id::text as user_id,
|
||||
username,
|
||||
email,
|
||||
name
|
||||
FROM users
|
||||
WHERE username = :username
|
||||
LIMIT 1
|
||||
""")
|
||||
result = await session.execute(query, {"username": username})
|
||||
row = result.fetchone()
|
||||
|
||||
if row:
|
||||
return {
|
||||
'user_id': row.user_id, # UUID
|
||||
'username': row.username,
|
||||
'email': row.email,
|
||||
'name': row.name
|
||||
}
|
||||
return None
|
||||
```
|
||||
|
||||
#### 수정 파일 2: `robeing-gateway/app/main.py`
|
||||
|
||||
```python
|
||||
@app.api_route("/api/items/{path:path}", methods=["GET", "POST", "DELETE"])
|
||||
async def items_proxy(
|
||||
path: str,
|
||||
request: Request,
|
||||
x_user_id: str = Depends(get_verified_user)
|
||||
):
|
||||
monitor_url = os.getenv("MONITOR_URL", "http://192.168.219.52:9024")
|
||||
|
||||
# username을 UUID로 변환
|
||||
from app.database import get_user_by_username
|
||||
user_uuid = x_user_id
|
||||
|
||||
if x_user_id and x_user_id != "default":
|
||||
user_info = await get_user_by_username(x_user_id)
|
||||
if user_info:
|
||||
user_uuid = user_info['user_id'] # UUID 사용
|
||||
logger.info(f"Username to UUID: {x_user_id} → {user_uuid}")
|
||||
|
||||
# robeing-monitor에 UUID 전달
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.request(
|
||||
method=request.method,
|
||||
url=f"{monitor_url}/api/items/{path}",
|
||||
headers={"X-User-Id": user_uuid},
|
||||
content=await request.body() if request.method != "GET" else None
|
||||
)
|
||||
```
|
||||
|
||||
#### 문제 발견 및 수정
|
||||
- 초기 오류: `column "display_name" does not exist`
|
||||
- 원인: PostgreSQL users 테이블에는 `name` 컬럼 존재
|
||||
- 해결: `display_name` → `name` 변경
|
||||
|
||||
### 3.3 Phase 3: skill-email UUID 변환 및 타입 호환성
|
||||
|
||||
#### 수정 파일 1: `skill-email/services/db_credentials_provider.py`
|
||||
|
||||
```python
|
||||
class DBCredentialsProvider:
|
||||
def __init__(self, connection_string: str):
|
||||
self.connection_string = connection_string
|
||||
self.connection_pool = None
|
||||
# RFC 4122 DNS namespace - 모든 서비스 동일 사용
|
||||
self.uuid_namespace = uuid.NAMESPACE_DNS
|
||||
self._init_pool()
|
||||
|
||||
def _convert_to_uuid(self, user_id: str) -> str:
|
||||
"""Slack user_id를 UUID로 변환"""
|
||||
try:
|
||||
uuid.UUID(user_id)
|
||||
return user_id
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
user_uuid = str(uuid.uuid5(self.uuid_namespace, user_id))
|
||||
logger.debug(f"Converted {user_id} to {user_uuid}")
|
||||
return user_uuid
|
||||
|
||||
def get_credentials(self, user_id: str) -> Optional[Credentials]:
|
||||
# Slack user_id를 UUID로 변환
|
||||
user_uuid = self._convert_to_uuid(user_id)
|
||||
# ... DB 조회
|
||||
```
|
||||
|
||||
#### 수정 파일 2: `skill-email/services/gmail_service.py`
|
||||
|
||||
```python
|
||||
def _get_gmail_service(self, user_id: str) -> Result[Any, EmailError]:
|
||||
# Credentials 가져오기
|
||||
creds_result = self.creds_provider.get_credentials(user_id)
|
||||
|
||||
# DBCredentialsProvider는 Optional[Credentials] 반환
|
||||
if creds_result is None:
|
||||
return Err(EmailError(f"No credentials found for user: {user_id}"))
|
||||
|
||||
# Result 타입인지 확인 (FileCredentialsProvider)
|
||||
if hasattr(creds_result, 'value'):
|
||||
if isinstance(creds_result, Err):
|
||||
return creds_result
|
||||
creds = creds_result.value
|
||||
else:
|
||||
# DBCredentialsProvider - 직접 Credentials 반환
|
||||
creds = creds_result
|
||||
|
||||
try:
|
||||
service = build("gmail", "v1", credentials=creds, cache_discovery=False)
|
||||
self._service_cache[user_id] = service
|
||||
return Ok(service)
|
||||
except Exception as e:
|
||||
return Err(EmailError(f"Gmail 서비스 생성 실패: {str(e)}", e))
|
||||
```
|
||||
|
||||
#### 환경변수 설정
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
environment:
|
||||
- TOKEN_PROVIDER=database
|
||||
- POSTGRES_CONNECTION_STRING=${POSTGRES_CONNECTION_STRING:-postgresql://robeings:robeings@192.168.219.45:5432/main_db}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 최종 아키텍처
|
||||
|
||||
### 4.1 UUID 변환 체인
|
||||
|
||||
```
|
||||
Frontend (JWT) Gateway robeing-monitor PostgreSQL
|
||||
username: "happybell80" → DB 조회 → UUID → UUID로 조회 → Gmail 아이템
|
||||
|
||||
Slack (rb8001) skill-email PostgreSQL
|
||||
user_id: "U091UNVE41M" → UUID 변환 → UUID → 토큰 조회
|
||||
```
|
||||
|
||||
### 4.2 서비스별 UUID 변환 구현
|
||||
|
||||
| 서비스 | 함수/메서드 | 입력 | 출력 |
|
||||
|--------|------------|------|------|
|
||||
| robeing-gateway | `get_user_by_username()` | username | UUID |
|
||||
| robeing-monitor | `convert_to_uuid()` | Slack ID/username | UUID |
|
||||
| skill-email | `_convert_to_uuid()` | Slack ID | UUID |
|
||||
|
||||
### 4.3 표준 UUID 생성 규칙
|
||||
|
||||
```python
|
||||
# 모든 서비스 동일
|
||||
namespace = uuid.NAMESPACE_DNS # 6ba7b810-9dad-11d1-80b4-00c04fd430c8
|
||||
user_uuid = str(uuid.uuid5(namespace, user_id))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 테스트 결과
|
||||
|
||||
### 5.1 UUID 변환 테스트
|
||||
```python
|
||||
# 테스트 케이스
|
||||
U091UNVE41M → b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0 ✅
|
||||
test-user → a5f4fe64-dd02-5776-b5f9-59fccda849ee ✅
|
||||
happybell80 → aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa ✅
|
||||
```
|
||||
|
||||
### 5.2 시스템 통합 테스트
|
||||
- ✅ Frontend → Gateway → robeing-monitor: Gmail 아이템 조회 성공
|
||||
- ✅ Slack → rb8001 → skill-email: 이메일 처리 요청 수신
|
||||
- ⚠️ Gmail OAuth 토큰 만료로 실제 발송은 실패 (재인증 필요)
|
||||
|
||||
---
|
||||
|
||||
## 6. 교훈
|
||||
|
||||
### 6.1 ID 체계 설계의 중요성
|
||||
- **문제**: 서비스마다 다른 ID 체계 사용
|
||||
- **해결**: 중앙화된 UUID 변환 로직
|
||||
- **교훈**: 초기 설계 시 통일된 ID 체계 필요
|
||||
|
||||
### 6.2 타입 시스템의 가치
|
||||
- **문제**: Provider 인터페이스 불일치
|
||||
- **해결**: 런타임 타입 체크로 호환성 확보
|
||||
- **교훈**: TypeScript/타입 힌트 적극 활용 필요
|
||||
|
||||
### 6.3 디버깅 전략
|
||||
- **효과적**: 각 서비스 개별 테스트 후 통합
|
||||
- **핵심**: 로그를 통한 데이터 흐름 추적
|
||||
- **도구**: curl 직접 테스트로 문제 격리
|
||||
|
||||
### 6.4 협업의 중요성
|
||||
- 51123 서버팀: 실시간 테스트 및 로그 확인
|
||||
- 51124 서버팀: 서비스 재시작 및 배포
|
||||
- 로컬 개발: 코드 수정 및 Git 푸시
|
||||
|
||||
---
|
||||
|
||||
## 7. 향후 과제
|
||||
|
||||
### 즉시 필요
|
||||
- [ ] Gmail OAuth 토큰 자동 갱신 구현
|
||||
- [ ] rb10508_micro 이메일 처리 완성
|
||||
- [ ] robeing.contacts 테이블 활용
|
||||
|
||||
### 중기 개선
|
||||
- [ ] 타입 정의 통일 (Result vs Optional)
|
||||
- [ ] ID 변환 미들웨어 중앙화
|
||||
- [ ] 통합 테스트 자동화
|
||||
|
||||
---
|
||||
|
||||
## 8. 관련 파일
|
||||
|
||||
### 수정된 파일들
|
||||
- `robeing-monitor/app/api/items.py`
|
||||
- `robeing-gateway/app/database.py`
|
||||
- `robeing-gateway/app/main.py`
|
||||
- `skill-email/services/db_credentials_provider.py`
|
||||
- `skill-email/services/gmail_service.py`
|
||||
- `skill-email/docker-compose.yml`
|
||||
|
||||
### Git 커밋
|
||||
- robeing-monitor: `50ee489` UUID 변환 로직 추가
|
||||
- robeing-gateway: `0335f68` username → UUID 변환
|
||||
- skill-email: `d32a1ab` 타입 호환성 및 UUID 변환
|
||||
|
||||
---
|
||||
|
||||
**문서 끝**
|
||||
Loading…
x
Reference in New Issue
Block a user