DOCS/journey/troubleshooting/250822_happybell80_Gmail통합_UUID변환_문제해결.md
happybell80 0252dd1a7f fix: 51123 서버 IP 주소 업데이트 (성수 이전)
192.168.219.45 → 192.168.0.100 일괄 변경

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:52:26 +09:00

10 KiB

Gmail 통합 시스템 UUID 변환 문제 해결

날짜: 2025-08-22 (수정: 2025-08-28)

작성자: happybell80 & Claude

관련 서비스: robeing-monitor, robeing-gateway, skill-email

상태: ⚠️ 부분 해결 (rb8001 미수정 - 2025-08-31)


1. 문제 상황 요약

⚠️ 2025-08-31 업데이트: rb8001 → skill-email UUID 전달 문제 지속. 250831_skill-email_UUID_inconsistency_URGENT.md 참조

초기 발견

  • 프론트엔드에서 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_token 테이블의 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 타입 불일치

증상

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

# 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
    
    # 51123 매핑 API 호출로 변경됨 (UUID5 사용 중단)
    # 실제 DB의 slack_user_mapping 테이블 사용
    return call_mapping_api(user_id)  # 실제 UUID 반환

# 모든 엔드포인트에 적용
@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

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

@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_namename 변경

3.3 Phase 3: skill-email UUID 변환 및 타입 호환성

수정 파일 1: skill-email/services/db_credentials_provider.py

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 = call_51123_mapping_api(user_id)  # UUID5 대신 매핑 API
        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

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))

환경변수 설정

# docker-compose.yml
environment:
  - TOKEN_PROVIDER=database
  - POSTGRES_CONNECTION_STRING=${POSTGRES_CONNECTION_STRING:-postgresql://robeings:robeings@192.168.0.100: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 변환 구현 (UUID5 중단, 매핑 API 사용)

서비스 함수/메서드 입력 출력
robeing-gateway get_user_by_username() username UUID (DB 조회)
robeing-monitor 51123 매핑 API 호출 Slack ID UUID (매핑 테이블)
skill-email 51123 매핑 API 호출 Slack ID UUID (매핑 테이블)

4.3 표준 UUID 조회 방식 (UUID5 중단)

# 51123 매핑 API 사용 (2025-08-28부터)
# GET /api/slack/mapping/{slack_user_id}
# 실제 DB의 slack_user_mapping 테이블에서 UUID 반환

5. 테스트 결과

5.1 UUID 변환 테스트

# 테스트 케이스
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 변환

문서 끝