이코에코(Eco²)/Clean Architecture Migration

이코에코(Eco²) Clean Architecture #0: Auth 리팩토링.MD

mango_fr 2025. 12. 31. 01:10

작성일: 2025-12-31
Opus 4.5와의 문답, 자료조사를 거치며 디벨롭한 문서이며 리팩토링 전 과정의 초안이 됩니다.


1. 개요

1.1 목표

현재 domains/auth/ 구조를 Clean Architecture 원칙에 따라 apps/auth/로 리팩토링합니다.

1.2 주요 변경사항

항목 현재 목표
루트 경로 domains/auth/ apps/auth/
아키텍처 혼합된 레이어드 Clean Architecture
의존성 방향 양방향 단방향 (안쪽으로만)
ORM 결합 Entity = ORM 모델 Entity/ORM 분리

2. 현재 기능 분석

2.1 AuthService 메서드 → Use Case 매핑

현재 메서드 목표 Use Case 타입 파일
authorize() OAuthAuthorizeInteractor Command commands/oauth_authorize.py
login_with_provider() OAuthCallbackInteractor Command commands/oauth_callback.py
refresh_session() RefreshTokensInteractor Command commands/refresh_tokens.py
logout() LogoutInteractor Command commands/logout.py
get_current_user() ValidateTokenQueryService Query queries/validate_token.py

2.2 현재 서비스 → 목표 위치

현재 서비스 목표 위치 역할
TokenService application/common/ports/token_service.py (Port) 인터페이스
TokenService infrastructure/security/jwt_token_service.py (Adapter) 구현체
OAuthStateStore application/common/ports/state_store.py (Port) 인터페이스
OAuthStateStore infrastructure/persistence_redis/state_store_redis.py (Adapter) 구현체
TokenBlacklist application/common/ports/token_blacklist.py (Port) 인터페이스
TokenBlacklist infrastructure/persistence_redis/token_blacklist_redis.py (Adapter) 구현체
UserTokenStore application/common/ports/user_token_store.py (Port) 인터페이스
UserTokenStore infrastructure/persistence_redis/user_token_store_redis.py (Adapter) 구현체
ProviderRegistry infrastructure/oauth/registry.py OAuth 프로바이더 관리

2.3 현재 Repository → 목표 Gateway

현재 Repository 목표 Port 목표 Adapter
UserRepository UserCommandGateway user_data_mapper_sqla.py
UserRepository UserQueryGateway user_reader_sqla.py
LoginAuditRepository LoginAuditGateway login_audit_mapper_sqla.py

3. 목표 폴더 구조

apps/auth/
│
├── domain/                                    # 🔵 Domain Layer (순수 Python)
│   ├── __init__.py
│   │
│   ├── entities/
│   │   ├── __init__.py
│   │   ├── base.py                            # Entity[T] 베이스
│   │   ├── user.py                            # User 엔티티
│   │   ├── user_social_account.py             # UserSocialAccount
│   │   └── login_audit.py                     # LoginAudit
│   │
│   ├── value_objects/
│   │   ├── __init__.py
│   │   ├── base.py                            # ValueObject 베이스
│   │   ├── user_id.py                         # UserId
│   │   ├── email.py                           # Email
│   │   ├── provider_user_id.py                # ProviderUserId
│   │   └── token_payload.py                   # TokenPayload
│   │
│   ├── enums/
│   │   ├── __init__.py
│   │   ├── oauth_provider.py                  # OAuthProvider
│   │   └── token_type.py                      # TokenType
│   │
│   ├── ports/
│   │   ├── __init__.py
│   │   ├── token_generator.py                 # TokenGenerator Protocol
│   │   └── user_id_generator.py               # UserIdGenerator Protocol
│   │
│   ├── services/
│   │   ├── __init__.py
│   │   └── user_service.py                    # 사용자 생성, 소셜 계정 연동
│   │
│   └── exceptions/
│       ├── __init__.py
│       ├── base.py                            # DomainError
│       ├── user.py                            # UserNotFoundError
│       └── auth.py                            # InvalidTokenError
│
├── application/                               # 🟢 Application Layer
│   ├── __init__.py
│   │
│   ├── commands/
│   │   ├── __init__.py
│   │   ├── oauth_authorize.py                 # OAuthAuthorizeInteractor
│   │   ├── oauth_callback.py                  # OAuthCallbackInteractor
│   │   ├── logout.py                          # LogoutInteractor
│   │   ├── refresh_tokens.py                  # RefreshTokensInteractor
│   │   └── revoke_all_tokens.py               # RevokeAllTokensInteractor
│   │
│   ├── queries/
│   │   ├── __init__.py
│   │   └── validate_token.py                  # ValidateTokenQueryService
│   │
│   └── common/
│       ├── __init__.py
│       │
│       ├── ports/
│       │   ├── __init__.py
│       │   ├── user_command_gateway.py
│       │   ├── user_query_gateway.py
│       │   ├── social_account_gateway.py
│       │   ├── login_audit_gateway.py
│       │   ├── token_service.py
│       │   ├── state_store.py
│       │   ├── token_blacklist.py
│       │   ├── user_token_store.py
│       │   ├── outbox_gateway.py
│       │   ├── flusher.py
│       │   └── transaction_manager.py
│       │
│       ├── services/
│       │   ├── __init__.py
│       │   └── oauth_client.py
│       │
│       ├── dto/
│       │   ├── __init__.py
│       │   ├── auth.py
│       │   └── token.py
│       │
│       └── exceptions/
│           ├── __init__.py
│           ├── base.py
│           └── auth.py
│
├── infrastructure/                            # 🟠 Infrastructure Layer
│   ├── __init__.py
│   │
│   ├── adapters/
│   │   ├── __init__.py
│   │   ├── user_data_mapper_sqla.py
│   │   ├── user_reader_sqla.py
│   │   ├── social_account_mapper_sqla.py
│   │   ├── login_audit_mapper_sqla.py
│   │   ├── main_flusher_sqla.py
│   │   ├── main_transaction_manager_sqla.py
│   │   ├── user_id_generator_uuid.py
│   │   └── types.py
│   │
│   ├── persistence_postgres/
│   │   ├── __init__.py
│   │   ├── session.py
│   │   ├── registry.py
│   │   └── mappings/
│   │       ├── __init__.py
│   │       ├── all.py
│   │       ├── user.py
│   │       ├── user_social_account.py
│   │       └── login_audit.py
│   │
│   ├── persistence_redis/
│   │   ├── __init__.py
│   │   ├── client.py
│   │   ├── state_store_redis.py
│   │   ├── token_blacklist_redis.py
│   │   ├── user_token_store_redis.py
│   │   └── outbox_redis.py
│   │
│   ├── security/
│   │   ├── __init__.py
│   │   ├── jwt_token_service.py
│   │   └── jwt_processor.py
│   │
│   ├── oauth/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── google.py
│   │   ├── kakao.py
│   │   ├── naver.py
│   │   └── registry.py
│   │
│   └── exceptions/
│       ├── __init__.py
│       ├── base.py
│       ├── gateway.py
│       └── oauth.py
│
├── presentation/                              # 🔴 Presentation Layer
│   ├── __init__.py
│   │
│   └── http/
│       ├── __init__.py
│       │
│       ├── controllers/
│       │   ├── __init__.py
│       │   │
│       │   ├── auth/
│       │   │   ├── __init__.py
│       │   │   ├── authorize.py
│       │   │   ├── callback.py
│       │   │   ├── logout.py
│       │   │   ├── refresh.py
│       │   │   ├── revoke.py
│       │   │   └── router.py
│       │   │
│       │   ├── general/
│       │   │   ├── __init__.py
│       │   │   ├── health.py
│       │   │   ├── metrics.py
│       │   │   └── router.py
│       │   │
│       │   ├── api_v1_router.py
│       │   └── root_router.py
│       │
│       ├── auth/
│       │   ├── __init__.py
│       │   ├── cookie_params.py
│       │   ├── dependencies.py
│       │   └── middleware.py
│       │
│       ├── errors/
│       │   ├── __init__.py
│       │   ├── handlers.py
│       │   └── translators.py
│       │
│       └── schemas/
│           ├── __init__.py
│           ├── auth.py
│           └── common.py
│
├── setup/                                     # 🟣 Setup Layer
│   ├── __init__.py
│   ├── config/
│   │   ├── __init__.py
│   │   ├── settings.py
│   │   ├── database.py
│   │   ├── redis.py
│   │   ├── security.py
│   │   └── oauth.py
│   ├── dependencies.py
│   ├── logging.py
│   ├── tracing.py
│   └── constants.py
│
├── workers/                                   # ⚙️ Workers
│   ├── __init__.py
│   └── consumers/
│       └── blacklist_relay.py
│
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── unit/
│   │   ├── domain/
│   │   ├── application/
│   │   └── factories/
│   └── integration/
│
├── main.py
├── Dockerfile
├── Dockerfile.relay
├── requirements.txt
└── README.md

4. 기능 검증 매트릭스

4.1 엔드포인트 매핑

Method Endpoint 현재 목표
GET /{provider}/authorize AuthService.authorize() OAuthAuthorizeInteractor
GET /{provider}/callback AuthService.login_with_provider() OAuthCallbackInteractor
POST /logout AuthService.logout() LogoutInteractor
POST /refresh AuthService.refresh_session() RefreshTokensInteractor
POST /revoke (신규) RevokeAllTokensInteractor
GET /health health.py general/health.py
GET /metrics metrics.py general/metrics.py

4.2 기능 체크리스트

기능 현재 위치 목표 위치 상태
OAuth 인증 URL 생성 AuthService.authorize() commands/oauth_authorize.py
OAuth 콜백 (로그인/회원가입) AuthService.login_with_provider() commands/oauth_callback.py
토큰 갱신 AuthService.refresh_session() commands/refresh_tokens.py
로그아웃 AuthService.logout() commands/logout.py
토큰 검증 get_current_user dependency queries/validate_token.py
JWT 발급/검증 TokenService infrastructure/security/
OAuth 상태 관리 OAuthStateStore persistence_redis/state_store_redis.py
토큰 블랙리스트 TokenBlacklist persistence_redis/token_blacklist_redis.py
사용자 토큰 저장 UserTokenStore persistence_redis/user_token_store_redis.py
Outbox 패턴 RedisOutbox persistence_redis/outbox_redis.py
Blacklist Relay workers/blacklist_relay.py workers/consumers/blacklist_relay.py
사용자 생성/조회 UserRepository adapters/user_*.py
로그인 감사 LoginAuditRepository adapters/login_audit_mapper_sqla.py
쿠키 관리 AuthService._*_cookie*() presentation/http/auth/cookie_params.py

5. 리팩토링 단계

Phase 1: 기반 구조 생성

# 1.1 새 디렉토리 구조 생성
mkdir -p apps/auth/{domain,application,infrastructure,presentation,setup,workers,tests}

# 1.2 Domain Layer 기초
mkdir -p apps/auth/domain/{entities,value_objects,enums,ports,services,exceptions}

# 1.3 Application Layer 기초
mkdir -p apps/auth/application/{commands,queries,common}
mkdir -p apps/auth/application/common/{ports,services,dto,exceptions}

# 1.4 Infrastructure Layer 기초
mkdir -p apps/auth/infrastructure/{adapters,persistence_postgres,persistence_redis,security,oauth,exceptions}
mkdir -p apps/auth/infrastructure/persistence_postgres/mappings

# 1.5 Presentation Layer 기초
mkdir -p apps/auth/presentation/http/{controllers,auth,errors,schemas}
mkdir -p apps/auth/presentation/http/controllers/{auth,general}

# 1.6 Setup Layer 기초
mkdir -p apps/auth/setup/config

# 1.7 Workers
mkdir -p apps/auth/workers/consumers

# 1.8 Tests
mkdir -p apps/auth/tests/{unit,integration}
mkdir -p apps/auth/tests/unit/{domain,application,factories}

Phase 2: Domain Layer 구현

순서 파일 내용
2.1 domain/entities/base.py Entity 베이스 클래스
2.2 domain/value_objects/base.py ValueObject 베이스 클래스
2.3 domain/enums/*.py OAuthProvider, TokenType
2.4 domain/value_objects/*.py UserId, Email, TokenPayload
2.5 domain/entities/*.py User, UserSocialAccount, LoginAudit
2.6 domain/exceptions/*.py DomainError, UserNotFoundError
2.7 domain/ports/*.py TokenGenerator, UserIdGenerator
2.8 domain/services/user_service.py 순수 도메인 로직

Phase 3: Application Layer 구현

순서 파일 내용
3.1 application/common/ports/*.py 모든 Port 인터페이스 정의
3.2 application/common/dto/*.py AuthorizeRequest, TokenPairDTO
3.3 application/common/exceptions/*.py ApplicationError, AuthenticationError
3.4 application/commands/oauth_authorize.py OAuthAuthorizeInteractor
3.5 application/commands/oauth_callback.py OAuthCallbackInteractor
3.6 application/commands/logout.py LogoutInteractor
3.7 application/commands/refresh_tokens.py RefreshTokensInteractor
3.8 application/queries/validate_token.py ValidateTokenQueryService

Phase 4: Infrastructure Layer 구현

순서 파일 내용
4.1 infrastructure/persistence_postgres/session.py AsyncSession 설정
4.2 infrastructure/persistence_postgres/registry.py mapper_registry
4.3 infrastructure/persistence_postgres/mappings/*.py ORM 매핑
4.4 infrastructure/adapters/user_*.py UserCommandGateway, UserQueryGateway 구현
4.5 infrastructure/persistence_redis/*.py Redis 기반 Port 구현
4.6 infrastructure/security/*.py JWT 토큰 서비스
4.7 infrastructure/oauth/*.py OAuth 프로바이더
4.8 infrastructure/adapters/main_*.py Flusher, TransactionManager

Phase 5: Presentation Layer 구현

순서 파일 내용
5.1 presentation/http/schemas/*.py HTTP Request/Response 스키마
5.2 presentation/http/auth/*.py 쿠키, 의존성, 미들웨어
5.3 presentation/http/errors/*.py 에러 핸들러, 변환기
5.4 presentation/http/controllers/auth/*.py Auth 컨트롤러들
5.5 presentation/http/controllers/general/*.py Health, Metrics
5.6 presentation/http/controllers/*_router.py 라우터 통합

Phase 6: Setup & 통합

순서 파일 내용
6.1 setup/config/*.py 설정 클래스들
6.2 setup/dependencies.py FastAPI DI 설정
6.3 setup/logging.py 로깅 설정
6.4 main.py FastAPI 앱 엔트리포인트
6.5 workers/consumers/blacklist_relay.py Worker 마이그레이션

Phase 7: 테스트 & 검증

순서 작업 내용
7.1 Unit Tests Domain, Application 레이어 테스트
7.2 Integration Tests 전체 플로우 테스트
7.3 E2E Tests 실제 OAuth 플로우 검증
7.4 기존 테스트 마이그레이션 tests/ 구조 업데이트

6. 주요 코드 변환 예시

6.1 현재 AuthService → 목표 OAuthCallbackInteractor

현재:

# domains/auth/application/services/auth.py
class AuthService:
    def __init__(
        self,
        session: AsyncSession = Depends(get_db_session),
        token_service: TokenService = Depends(TokenService),
        ...
    ):
        self.session = session
        self.user_repo = UserRepository(session)

    async def login_with_provider(self, provider_name: str, payload: OAuthLoginRequest, ...):
        # 모든 로직이 한 곳에 혼합
        provider = self._get_provider(provider_name)
        tokens = await provider.exchange_code(...)
        profile = await provider.fetch_profile(...)
        user, _ = await self.user_repo.upsert_from_profile(profile)
        token_pair = self.token_service.issue_pair(...)
        await self.session.commit()
        self._apply_session_cookies(response, ...)
        return User.model_validate(user)

목표:

# apps/auth/application/commands/oauth_callback.py
from dataclasses import dataclass
from typing import Protocol

@dataclass(frozen=True, slots=True)
class OAuthCallbackRequest:
    provider: str
    code: str
    state: str
    redirect_uri: str | None
    user_agent: str | None
    ip_address: str | None

@dataclass(frozen=True, slots=True)
class OAuthCallbackResponse:
    user_id: UUID
    access_token: str
    refresh_token: str
    access_expires_at: int
    refresh_expires_at: int

class OAuthCallbackInteractor:
    def __init__(
        self,
        user_service: UserService,                    # Domain Service
        user_command_gateway: UserCommandGateway,     # Port
        social_account_gateway: SocialAccountGateway, # Port
        login_audit_gateway: LoginAuditGateway,       # Port
        token_service: TokenService,                  # Port
        state_store: StateStore,                      # Port
        user_token_store: UserTokenStore,             # Port
        oauth_client: OAuthClientService,             # Application Service
        flusher: Flusher,                            # Port
        transaction_manager: TransactionManager,      # Port
    ) -> None:
        self._user_service = user_service
        self._user_command_gateway = user_command_gateway
        self._social_account_gateway = social_account_gateway
        self._login_audit_gateway = login_audit_gateway
        self._token_service = token_service
        self._state_store = state_store
        self._user_token_store = user_token_store
        self._oauth_client = oauth_client
        self._flusher = flusher
        self._transaction_manager = transaction_manager

    async def execute(self, request: OAuthCallbackRequest) -> OAuthCallbackResponse:
        """
        :raises AuthenticationError: 상태 검증 실패
        :raises OAuthProviderError: Provider API 오류
        :raises DataMapperError: DB 오류
        """
        # 1. 상태 검증
        state_data = await self._state_store.consume(request.state)
        if not state_data or state_data.provider != request.provider:
            raise AuthenticationError("Invalid or expired state")

        # 2. OAuth 토큰 교환 & 프로필 조회
        profile = await self._oauth_client.fetch_profile(
            provider=request.provider,
            code=request.code,
            state=request.state,
            redirect_uri=request.redirect_uri or state_data.redirect_uri,
            code_verifier=state_data.code_verifier,
        )

        # 3. 사용자 생성/조회
        user = await self._user_service.upsert_from_profile(
            profile=profile,
            user_gateway=self._user_command_gateway,
            social_account_gateway=self._social_account_gateway,
        )

        # 4. 토큰 발급
        token_pair = self._token_service.issue_pair(
            user_id=user.id_,
            provider=request.provider,
        )

        # 5. 토큰 저장
        await self._user_token_store.register(
            user_id=user.id_,
            jti=token_pair.refresh_jti,
            expires_at=token_pair.refresh_expires_at,
            device_id=state_data.device_id,
            user_agent=request.user_agent,
        )

        # 6. 로그인 감사
        self._login_audit_gateway.add(LoginAudit(
            user_id=user.id_,
            provider=request.provider,
            jti=token_pair.access_jti,
            ip_address=request.ip_address,
            user_agent=request.user_agent,
        ))

        # 7. 커밋
        await self._flusher.flush()
        await self._transaction_manager.commit()

        return OAuthCallbackResponse(
            user_id=user.id_.value,
            access_token=token_pair.access_token,
            refresh_token=token_pair.refresh_token,
            access_expires_at=token_pair.access_expires_at,
            refresh_expires_at=token_pair.refresh_expires_at,
        )

6.2 현재 User 모델 → Domain Entity + ORM 매핑 분리

현재 (혼합):

# domains/auth/domain/models/user.py
from sqlalchemy.orm import Mapped, mapped_column
from domains.auth.infrastructure.database.base import Base

class User(Base):  # ORM과 결합
    __tablename__ = "users"
    id: Mapped[uuid.UUID] = mapped_column(...)
    username: Mapped[str] = mapped_column(...)

목표 - Domain Entity (순수 Python):

# apps/auth/domain/entities/user.py
from apps.auth.domain.entities.base import Entity
from apps.auth.domain.value_objects.user_id import UserId
from apps.auth.domain.value_objects.email import Email

class User(Entity[UserId]):
    def __init__(
        self,
        *,
        id_: UserId,
        username: str | None,
        nickname: str | None,
        profile_image_url: str | None,
        phone_number: str | None,
        created_at: datetime,
        updated_at: datetime,
        last_login_at: datetime | None,
    ) -> None:
        super().__init__(id_=id_)
        self.username = username
        self.nickname = nickname
        self.profile_image_url = profile_image_url
        self.phone_number = phone_number
        self.created_at = created_at
        self.updated_at = updated_at
        self.last_login_at = last_login_at

목표 - ORM 매핑 (Infrastructure):

# apps/auth/infrastructure/persistence_postgres/mappings/user.py
from sqlalchemy import Table, Column, String, DateTime
from sqlalchemy.dialects.postgresql import UUID

from apps.auth.infrastructure.persistence_postgres.registry import mapper_registry
from apps.auth.domain.entities.user import User
from apps.auth.domain.value_objects.user_id import UserId

users_table = Table(
    "users",
    mapper_registry.metadata,
    Column("id", UUID(as_uuid=True), primary_key=True),
    Column("username", String(120)),
    Column("nickname", String(120)),
    Column("profile_image_url", String(512)),
    Column("phone_number", String(32), index=True),
    Column("created_at", DateTime(timezone=True), nullable=False),
    Column("updated_at", DateTime(timezone=True), nullable=False),
    Column("last_login_at", DateTime(timezone=True)),
    schema="auth",
)

def start_user_mapper() -> None:
    mapper_registry.map_imperatively(
        User,
        users_table,
        properties={
            "id_": users_table.c.id,
        },
    )

7. 의존성 주입 설정

7.1 FastAPI Depends 기반 (Dishka 미사용)

# apps/auth/setup/dependencies.py
from functools import lru_cache
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession

from apps.auth.infrastructure.persistence_postgres.session import get_db_session
from apps.auth.infrastructure.persistence_redis.client import get_redis
from apps.auth.infrastructure.adapters.user_data_mapper_sqla import SqlaUserDataMapper
from apps.auth.infrastructure.security.jwt_token_service import JwtTokenService
from apps.auth.application.commands.oauth_callback import OAuthCallbackInteractor

# Gateway Providers
async def get_user_command_gateway(
    session: AsyncSession = Depends(get_db_session),
) -> UserCommandGateway:
    return SqlaUserDataMapper(session)

async def get_user_query_gateway(
    session: AsyncSession = Depends(get_db_session),
) -> UserQueryGateway:
    return SqlaUserReader(session)

# Service Providers
@lru_cache
def get_token_service() -> TokenService:
    return JwtTokenService(...)

# Use Case Providers
async def get_oauth_callback_interactor(
    user_command_gateway: UserCommandGateway = Depends(get_user_command_gateway),
    token_service: TokenService = Depends(get_token_service),
    ...
) -> OAuthCallbackInteractor:
    return OAuthCallbackInteractor(
        user_command_gateway=user_command_gateway,
        token_service=token_service,
        ...
    )

8. 위험 요소 및 대응

위험 영향 대응
ORM 매핑 분리 시 관계 매핑 복잡성 Imperative Mapping 테스트 철저히
기존 API 호환성 HTTP 스키마 동일하게 유지
테스트 커버리지 감소 단계별 테스트 작성 병행
배포 중 다운타임 Canary 이미지로 패키징, 현 배포 클러스터 이미지와 태그로 분리

9. 배포 전략 (Canary)

9.1 개요

리팩토링된 코드는 Canary 배포로 안전하게 검증합니다.

참조: docs/blogs/deployment/01-canary-deployment-strategy.md

9.2 배포 흐름

┌─────────────────────────────────────────────────────────────────────┐
│                         Canary 배포 아키텍처                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   Client                                                             │
│     │                                                                │
│     ├─────────────────────────────────────────┐                     │
│     │  X-Canary: true                         │  (no header)        │
│     ▼                                         ▼                     │
│  ┌─────────────┐                        ┌─────────────┐             │
│  │  Istio      │                        │  Istio      │             │
│  │  Gateway    │                        │  Gateway    │             │
│  └──────┬──────┘                        └──────┬──────┘             │
│         │                                      │                     │
│         ▼ VirtualService                       ▼ VirtualService     │
│  ┌─────────────────┐                    ┌─────────────────┐         │
│  │ auth-api-canary │                    │   auth-api      │         │
│  │ (리팩토링 버전) │                    │   (Stable)      │         │
│  │ version: v2     │                    │   version: v1   │         │
│  └─────────────────┘                    └─────────────────┘         │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

9.3 Canary 이미지 태그

# Stable 이미지 (현재)
image: docker.io/mng990/eco2:auth-api-dev-latest

# Canary 이미지 (리팩토링)
image: docker.io/mng990/eco2:auth-api-dev-canary

9.4 테스트 방법

# 1. Stable 버전 테스트 (기존 코드)
curl https://api.example.com/api/v1/auth/health

# 2. Canary 버전 테스트 (리팩토링 코드)
curl -H "X-Canary: true" https://api.example.com/api/v1/auth/health

9.5 배포 단계

단계 작업 검증
1 리팩토링 코드 → canary 브랜치 로컬 테스트
2 CI/CD가 auth-api-dev-canary 이미지 빌드 이미지 푸시 확인
3 ArgoCD가 deployment-canary.yaml 동기화 Pod Running 확인
4 X-Canary: true 헤더로 E2E 테스트 모든 엔드포인트 검증
5 성공 시 canarydevelop 머지 Stable 이미지 업데이트
6 Canary Deployment 제거 또는 축소 리소스 정리

9.6 롤백

문제 발생 시 즉시 롤백 가능:

# Canary Pod 스케일 다운 (트래픽 차단)
kubectl scale deployment auth-api-canary -n auth --replicas=0

# 또는 Canary 헤더 없이 요청하면 자동으로 Stable로 라우팅

10. 참고 문서

  • docs/foundations/16-fastapi-clean-example-analysis.md - 아키텍처 상세 분석 (포스팅 완료)
  • docs/foundations/15-dependency-injection-comparison.md - DI 비교 (내부 문서)
  • docs/blogs/deployment/01-canary-deployment-strategy.md - Canary 배포 전략 (내부 문서)
  • fastapi-clean-example - 참조 프로젝트