DOCS/300_architecture/uuid_conversion_system.md
happybell80 9eaa83a76e 아키텍처 문서 대규모 업데이트: JWT/UUID 변환 체계 정립
- JWT 검증 플로우: Gateway 내부 처리로 변경
- Username → UUID 변환 메커니즘 문서화
- UUID5 체계: Slack 사용자용 결정적 UUID 생성
- Gateway 프록시 패턴 상세 문서화
- 데이터베이스: gmail_tokens, robeing 스키마 추가
- 서비스 포트 매핑 및 역할 명확화
- auth_db → main_db 마이그레이션 반영
2025-08-22 20:12:35 +09:00

9.8 KiB

UUID 변환 시스템 아키텍처

작성일: 2025-08-21

작성자: Claude (51123 서버 관리자)


1. 개요

RO-BEING 시스템의 UUID 변환 체계 문서입니다. 일반 사용자는 UUID4, Slack 사용자는 UUID5를 사용하여 일관된 사용자 식별 체계를 유지합니다.


2. UUID 타입별 용도

2.1 UUID 타입 구분

타입 생성 방식 용도 특징
UUID4 랜덤 생성 Google OAuth 사용자 매번 다른 값 생성
UUID5 네임스페이스 + 이름 해시 Slack 사용자 같은 입력에 같은 출력 (결정적)

2.2 네임스페이스

# DNS 네임스페이스 (표준 UUID)
NAMESPACE = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')

3. 사용자별 UUID 생성

3.1 Google OAuth 사용자 (UUID4)

flowchart LR
    A[Google 로그인] --> B[이메일 확인]
    B --> C{기존 사용자?}
    C -->|Yes| D[기존 UUID 사용]
    C -->|No| E[uuid4() 생성]
    E --> F[DB 저장]
    F --> G[JWT 생성]

구현 코드

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)

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[기존 사용]

구현 코드

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 변환 플로우

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 변환 구현

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

-- 테스트 사용자 (고정 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 예시

# 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 마이그레이션

-- 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 검증 함수

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

@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 디버깅 엔드포인트

@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 디버깅 쿼리

-- 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 노출 최소화

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를 모르면 생성 불가능
  • 네임스페이스 비공개 유지 (소스코드에서만 관리)

문서 끝