이코에코(Eco²)
이코에코(Eco²) MQ 도입 전, 코드 품질 개선: My API 리팩토링
mango_fr
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 해결
문제: GrantCharacter gRPC에서 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