-
이코에코(Eco²) MQ 도입 전, 코드 품질 개선: My API 리팩토링이코에코(Eco²) 2025. 12. 20. 12:58

My API는 사용자 프로필과 보유한 캐릭터 인벤토리를 관리하는 서비스입니다.
Character 도메인에서 gRPC로 캐릭터 지급 요청을 받아 처리합니다.
도메인 간 통신 흐름 (/user/me/charcter)

리팩토링 전 문제점
- Race Condition: gRPC
GrantCharacter에서 SELECT→INSERT 패턴 - gRPC 설정 하드코딩:
os.getenv직접 사용 - 테스트 부족: placeholder 테스트 1개만 존재
- 리소스 누수 가능성: gRPC 채널 close 미보장
실제 가치 있는 개선
항목 판정 이유 Race Condition ✅ 필수 Character→My gRPC 호출은 빈번, 동시성 이슈 가능 테스트 추가 ✅ 필수 핵심 비즈니스 로직 검증 필요 과잉 엔지니어링 주의
항목 판정 이유 Circuit Breaker ⚠️ 선택 My→Character gRPC는 1회성 조회, 실패해도 빈 목록 반환 Settings 이관 ⚠️ 미미 os.getenv가 이미 있었음, 형식만 변경lifespan 정리 ⚠️ 미미 앱 종료 시 어차피 정리됨
개선 사항
P0: Race Condition 해결
문제:
GrantCharactergRPC에서 SELECT 후 INSERT 패턴# AS-IS: Race Condition 발생 가능 existing = await session.execute(select(...)) if existing: return already_owned session.add(new_ownership) await session.commit()해결: Optimistic Locking + IntegrityError 핸들링
# TO-BE: UniqueConstraint로 동시성 보장 session.add(new_ownership) try: await session.commit() return success except IntegrityError: await session.rollback() return already_owned # 동시 요청으로 이미 지급됨P1: gRPC 설정 Settings 이관
# core/config.py class Settings(BaseSettings): # gRPC Client Settings character_grpc_host: str = Field( "character-api.character.svc.cluster.local", validation_alias=AliasChoices("CHARACTER_GRPC_HOST"), ) character_grpc_port: str = Field("50051") character_grpc_timeout: float = Field(5.0) # Circuit Breaker (선택적) circuit_fail_max: int = Field(5) circuit_timeout_duration: int = Field(30)P1: Circuit Breaker 적용
# rpc/character_client.py class CharacterClient: def __init__(self, settings: Settings | None = None): self.settings = settings or get_settings() self._circuit_breaker = CircuitBreaker( name="character-grpc-client", fail_max=self.settings.circuit_fail_max, timeout_duration=self.settings.circuit_timeout_duration, ) async def get_default_character(self) -> DefaultCharacterInfo | None: try: return await self._circuit_breaker.call_async(self._impl) except CircuitBreakerError: return None # fail-fast except grpc.aio.AioRpcError: return None # graceful degradation⚠️ 주의: 이 기능은 기본 캐릭터 조회 1회용이므로 과잉 엔지니어링일 수 있습니다.
테스트 전략
테스트 구조
tests/ ├── conftest.py # 공통 fixtures ├── test_my_service.py # 프로필 서비스 (33개) ├── test_character_service.py # 캐릭터 서비스 (8개) ├── test_character_client.py # gRPC 클라이언트 (9개) └── test_user_character_servicer.py # gRPC 서버 (4개)핵심 테스트 케이스
MyService 테스트:
- 프로필 조회/생성/수정/삭제
- 전화번호 정규화 (
010-1234-5678,+82) - 소셜 계정 선택 로직
- 닉네임/사용자명 fallback
gRPC 테스트:
GrantCharacter정상/중복 지급IntegrityError핸들링- Circuit Breaker 상태 전이
실측 데이터
Radon 복잡도 분석
실행: radon cc domains/my/ -a -s --total-average 결과: - 총 블록: 169개 - 평균 복잡도: A (2.27) - C등급 이상: 0개테스트 커버리지
실행: pytest domains/my/tests/ --cov=domains.my.services --cov=domains.my.rpc 결과: - services/my.py: 97% - services/characters.py: 89% - rpc/character_client.py: 78% - rpc/v1/user_character_servicer.py: 100% - 전체: 90%테스트 수
항목 개선 전 개선 후 단위 테스트 1개 55개 커버리지 0% 90%
결론
주요 성과
- 안정성 향상: Race Condition 해결 (Optimistic Locking)
- 테스트 커버리지: 0% → 90%
- 설정 관리: gRPC 설정 중앙화
회고
- 과잉 엔지니어링 주의: Circuit Breaker는 실제 필요성 대비 복잡성 증가
- 테스트가 핵심:
my.py로직 테스트를 보강해 마이그레이션 전 배포 서비스 안정성 강화
domains/my/ ├── core/config.py # gRPC + Circuit Breaker 설정 ├── main.py # lifespan gRPC 정리 ├── requirements.txt # aiobreaker 추가 ├── rpc/character_client.py # Settings 주입 + Circuit Breaker ├── rpc/v1/user_character_servicer.py # IntegrityError 핸들링 ├── services/characters.py # IntegrityError 핸들링 └── tests/ ├── conftest.py ├── test_my_service.py # 신규 (33개) ├── test_character_service.py ├── test_character_client.py └── test_user_character_servicer.py
Reference
GitHub
Service
'이코에코(Eco²)' 카테고리의 다른 글
이코에코(Eco²) Deployment Strategy: Canary Deployments (0) 2025.12.30 이코에코(Eco²) MQ 도입 전, 코드 품질 개선: Chat API 리팩토링 (0) 2025.12.21 이코에코(Eco²) MQ 도입 전, 코드 품질 개선: Character API 리팩토링 (0) 2025.12.20 이코에코(Eco²) 백엔드/인프라 코드 품질 분석기 도입 (1) 2025.12.20 [Dec.20.2025] 이코에코(Eco²) 백엔드/인프라 디자인 패턴 (0) 2025.12.20 - Race Condition: gRPC