# UUID 변환 시스템 아키텍처 ## 작성일: 2025-08-21 (수정: 2025-08-28) ## 작성자: Claude (51123 서버 관리자) --- ## 1. 개요 RO-BEING 시스템의 UUID 변환 체계 문서입니다. Google OAuth는 UUID4, Slack은 51123 DB 매핑 API를 사용합니다. --- ## 2. UUID 타입별 용도 ### 2.1 UUID 타입 구분 | 타입 | 생성 방식 | 용도 | 특징 | |------|----------|------|------| | UUID4 | 랜덤 생성 | Google OAuth 사용자 | auth-server에서 생성 | | UUID5 | 네임스페이스 해시 | robeing-monitor 사용 중 | namespace 확인 필요 | | DB 매핑 | slack_user_mapping | Slack 사용자 매핑 | 실제 사용 여부 확인 필요 | ### 2.2 네임스페이스 ```python # DNS 네임스페이스 (표준 UUID) NAMESPACE = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') ``` --- ## 3. 사용자별 UUID 생성 ### 3.1 Google OAuth 사용자 (UUID4) ```mermaid flowchart LR A[Google 로그인] --> B[이메일 확인] B --> C{기존 사용자?} C -->|Yes| D[기존 UUID 사용] C -->|No| E[uuid4() 생성] E --> F[DB 저장] F --> G[JWT 생성] ``` #### 구현 코드 ```python import uuid class GoogleAuthHandler: async def create_or_get_user(self, email: str, name: str): # 기존 사용자 확인 user = await self.db.fetchone( "SELECT * FROM users WHERE email = $1", email ) if user: return user['id'] # 신규 사용자 - UUID4 생성 new_user_id = str(uuid.uuid4()) await self.db.execute(""" INSERT INTO users (id, email, name, username, oauth_provider) VALUES ($1, $2, $3, $4, 'google') """, new_user_id, email, name, self.generate_username(email)) return new_user_id ``` ### 3.2 Slack 사용자 (UUID5) ```mermaid flowchart LR A[Slack 이벤트] --> B[Slack User ID] B --> C[UUID5 생성] C --> D[네임스페이스 + Slack ID] D --> E[SHA-1 해시] E --> F[결정적 UUID] F --> G{DB 확인} G -->|없음| H[신규 등록] G -->|있음| I[기존 사용] ``` #### 구현 코드 ```python import uuid class SlackUserHandler: NAMESPACE = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') def get_user_uuid(self, slack_user_id: str) -> str: """Slack User ID를 UUID5로 변환""" return str(uuid.uuid5(self.NAMESPACE, slack_user_id)) async def get_or_create_user(self, slack_user_id: str, slack_user_info: dict): # UUID5 생성 (항상 같은 결과) user_uuid = self.get_user_uuid(slack_user_id) # 기존 사용자 확인 user = await self.db.fetchone( "SELECT * FROM users WHERE id = $1", user_uuid ) if user: return user_uuid # 신규 사용자 등록 username = f"slack_{slack_user_id[:8]}" await self.db.execute(""" INSERT INTO users (id, username, email, name, oauth_provider) VALUES ($1, $2, $3, $4, 'slack') """, user_uuid, username, slack_user_info.get('email', ''), slack_user_info.get('real_name', '')) return user_uuid ``` --- ## 4. Gateway에서의 UUID 변환 ### 4.1 JWT Token → UUID 변환 플로우 ```mermaid sequenceDiagram participant Client participant Gateway participant DB participant Service Client->>Gateway: Request + JWT (username) Gateway->>Gateway: JWT 검증 Gateway->>Gateway: username 추출 Gateway->>DB: SELECT id FROM users WHERE username = ? DB-->>Gateway: UUID Gateway->>Service: Request + X-User-Id (UUID) Service-->>Gateway: Response Gateway-->>Client: Response ``` ### 4.2 변환 구현 ```python class GatewayUUIDConverter: def __init__(self, db_pool): self.db_pool = db_pool self.cache = {} # username -> uuid 캐시 async def username_to_uuid(self, username: str) -> str: """Username을 UUID로 변환 (캐싱 포함)""" # 캐시 확인 if username in self.cache: return self.cache[username] # DB 조회 async with self.db_pool.acquire() as conn: row = await conn.fetchrow( "SELECT id FROM users WHERE username = $1", username ) if not row: raise UserNotFoundError(f"User {username} not found") user_uuid = str(row['id']) # 캐시 저장 (5분 TTL) self.cache[username] = user_uuid asyncio.create_task(self.clear_cache_after(username, 300)) return user_uuid async def clear_cache_after(self, username: str, seconds: int): """일정 시간 후 캐시 제거""" await asyncio.sleep(seconds) self.cache.pop(username, None) ``` --- ## 5. 테스트 사용자 UUID ### 5.1 하드코딩된 테스트 UUID ```sql -- 테스트 사용자 (고정 UUID) INSERT INTO users (id, username, email, name) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'happybell80', 'goeun2dc@gmail.com', '김종태'), ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'eagle0914', '0914eagle@gmail.com', '전희재'), ('cccccccc-cccc-cccc-cccc-cccccccccccc', 'test_user', 'test@example.com', 'Test User'); ``` ### 5.2 Slack 테스트 사용자 UUID5 예시 ```python # Slack User ID 예시 slack_ids = { "U0925SXQFDK": "종태", # UUID5: 생성된 값 "U0123ABCDEF": "테스트" # UUID5: 생성된 값 } # UUID5 생성 예시 for slack_id, name in slack_ids.items(): user_uuid = uuid.uuid5(NAMESPACE, slack_id) print(f"{name} ({slack_id}): {user_uuid}") ``` --- ## 6. UUID 마이그레이션 ### 6.1 기존 VARCHAR user_id → UUID 마이그레이션 ```sql -- 1. 임시 컬럼 추가 ALTER TABLE gmail_tokens ADD COLUMN user_uuid UUID; -- 2. UUID 매핑 UPDATE gmail_tokens gt SET user_uuid = u.id FROM users u WHERE gt.user_id = u.username; -- 3. 기존 컬럼 제거 및 이름 변경 ALTER TABLE gmail_tokens DROP COLUMN user_id; ALTER TABLE gmail_tokens RENAME COLUMN user_uuid TO user_id; -- 4. 외래키 제약 추가 ALTER TABLE gmail_tokens ADD CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id); ``` --- ## 7. UUID 유효성 검증 ### 7.1 검증 함수 ```python import re def is_valid_uuid(uuid_string: str) -> bool: """UUID 형식 검증""" uuid_pattern = re.compile( r'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', re.IGNORECASE ) return bool(uuid_pattern.match(uuid_string)) def get_uuid_version(uuid_string: str) -> int: """UUID 버전 확인""" try: u = uuid.UUID(uuid_string) return u.version except ValueError: return None # 사용 예시 user_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" if is_valid_uuid(user_id): version = get_uuid_version(user_id) print(f"Valid UUID v{version}") ``` --- ## 8. UUID 관련 API 응답 ### 8.1 사용자 정보 API ```python @app.get("/api/users/me") async def get_current_user(request: Request): user_id = request.headers.get("X-User-Id") username = request.headers.get("X-Username") return { "id": user_id, # UUID "username": username, "uuid_version": get_uuid_version(user_id), "is_slack_user": get_uuid_version(user_id) == 5 } ``` ### 8.2 UUID 디버깅 엔드포인트 ```python @app.get("/api/debug/uuid/{username}") async def debug_uuid(username: str): """개발 환경에서만 사용""" user = await db.fetchone( "SELECT id, username, oauth_provider FROM users WHERE username = $1", username ) if not user: return {"error": "User not found"} return { "username": username, "uuid": str(user['id']), "uuid_version": get_uuid_version(str(user['id'])), "oauth_provider": user['oauth_provider'], "is_deterministic": get_uuid_version(str(user['id'])) == 5 } ``` --- ## 9. 트러블슈팅 ### 9.1 일반적인 문제 | 문제 | 원인 | 해결 방법 | |------|------|----------| | UUID 불일치 | Slack ID 변경 | UUID5는 결정적이므로 같은 Slack ID는 항상 같은 UUID | | 중복 UUID | UUID4 충돌 (매우 드물음) | 재생성 또는 UNIQUE 제약 확인 | | 변환 실패 | username 없음 | users 테이블 username 필드 확인 | | 캐시 불일치 | TTL 만료 전 DB 변경 | 캐시 무효화 또는 TTL 단축 | ### 9.2 디버깅 쿼리 ```sql -- UUID 버전별 사용자 수 SELECT CASE WHEN id::text LIKE '________-____-4___-____-____________' THEN 'UUID4 (Google)' WHEN id::text LIKE '________-____-5___-____-____________' THEN 'UUID5 (Slack)' ELSE 'Other' END as uuid_type, COUNT(*) as count FROM users GROUP BY uuid_type; -- Username-UUID 매핑 확인 SELECT username, id, oauth_provider FROM users WHERE username = 'happybell80'; -- Slack 사용자 UUID5 검증 SELECT username, id, id = uuid_generate_v5('6ba7b810-9dad-11d1-80b4-00c04fd430c8'::uuid, SUBSTRING(username FROM 7)) as is_valid_uuid5 FROM users WHERE oauth_provider = 'slack'; ``` --- ## 10. 보안 고려사항 ### 10.1 UUID 노출 최소화 ```python class SecureUUIDHandler: @staticmethod def mask_uuid(uuid_string: str) -> str: """UUID 일부 마스킹""" # aaaaaaaa-****-****-****-aaaaaaaaaaaa parts = uuid_string.split('-') return f"{parts[0]}-****-****-****-{parts[4]}" @staticmethod def should_expose_uuid(user_role: str) -> bool: """역할별 UUID 노출 여부""" return user_role in ['admin', 'developer'] ``` ### 10.2 UUID 추측 방지 - UUID4: 122비트 랜덤 엔트로피로 추측 불가능 - UUID5: Slack ID를 모르면 생성 불가능 - 네임스페이스 비공개 유지 (소스코드에서만 관리) --- **문서 끝**