この記事は現在、韓国語のみでご覧いただけます。
BUILT & IMPROVED · 2025.11
검증, 데이터 로딩, 비즈니스 규칙, 화면 상태 발행이 하나의 RxJS 흐름에 쌓여 있었습니다. 전체 순서를 조율하는 Orchestrator와 역할별 Service, 독립적인 오류 Handler로 책임을 나눴습니다.
게임 약관 동의 페이지의 초기화 로직은 오랜 기간 요구사항이 추가되며 단일 함수 416줄, 결과 처리 switch-case 200줄까지 커졌습니다.
검증과 API 호출, 비즈니스 규칙, 화면 상태 변경이 한 흐름에 섞여 있었고 대부분의 작업이 switchMap으로 작성되어 최대 6단계까지 중첩됐습니다.
전체 실행 순서는 Orchestrator가 담당하고, 세부 로직은 검증, 데이터 로딩, 비즈니스 규칙, 화면 상태 발행의 4개의 Service로 분리했습니다.
파이프라인 전체 데이터를 Context 객체에 누적해 각 단계의 입출력을 명시했습니다. 동기 변환은 map, API 호출은 switchMap, 부수효과는 tap으로 구분해 Operator의 의도도 드러냈습니다.
public execute(route: Route, subject: Subject<GameTermsListInfo>) {
return of(new GameTermsContext({query: route.query})).pipe(
// Phase 1: 검증
this.validationService.validateQuery(),
this.validationService.validateAuth(),
// Phase 2: 초기 데이터 로딩 (병렬)
this.dataLoaderService.loadInitialDataInParallel(),
// Phase 3: 약관 동의 체크
this.dataLoaderService.checkAgreement(),
this.validationService.validateAlreadyAgreed(),
// Phase 4: 본인인증 & 생년월일
this.validationService.validatePersonVerification(),
this.dataLoaderService.loadBirthDayInfo(),
// Phase 5: 연령 검증
this.dataLoaderService.loadGameRating(),
this.validationService.validateAge(),
// Phase 6: 약관 목록 로딩
this.dataLoaderService.loadTermsList(),
this.validationService.validateTermsListExists(),
// Phase 7: 비즈니스 로직
this.businessRuleService.determineTermsType(),
this.businessRuleService.setupUserForLogging(),
// Phase 8: 결과 발행
this.presenterService.sendViewLog(),
this.presenterService.emitTermsListInfo(subject),
this.presenterService.createSuccessResult()
);
}200줄의 switch-case는 오류 코드별 14개 Handler로 분리했습니다. Factory가 코드에 맞는 Handler를 선택하도록 해 각 케이스가 서로의 구현에 영향을 주지 않도록 했습니다.
여러 Handler에서 반복되던 오류 메시지 생성 로직은 공통 유틸리티로 옮겨 중복도 함께 제거했습니다.
처음에는 Factory 생성자에서 모든 오류 코드의 Handler를 미리 인스턴스화했습니다. 실제로는 한 번의 초기화에서 하나의 Handler만 사용된다는 리뷰 의견을 받아, 생성 함수만 등록해 두고 요청 시점에 지연 생성해 캐시하는 구조로 개선했습니다.
type HandlerFactory = (deps: HandlerDependencies) => InitResultHandler;
export class InitResultHandlerFactory {
// 생성된 Handler만 캐시
private handlerCache = new Map<IntegrationInitCode, InitResultHandler>();
// 인스턴스 대신 생성 함수만 등록
private readonly handlerFactories = new Map<IntegrationInitCode, HandlerFactory>([
[IntegrationInitCode.AbnormalAccess, (deps) => new AbnormalAccessHandler(deps)],
[IntegrationInitCode.NotLogin, (deps) => new NotLoginHandler(deps)],
// ... 나머지 오류 코드도 동일하게 등록
]);
public getHandler(code: IntegrationInitCode): InitResultHandler {
const cached = this.handlerCache.get(code);
if (cached) {
return cached;
}
// 필요한 시점에만 생성하고 캐시
const factory = this.handlerFactories.get(code);
const handler = factory ? factory(this.deps) : this.createDefaultHandler();
this.handlerCache.set(code, handler);
return handler;
}
}컴포넌트에서는 Factory를 첫 오류 처리 시점에 한 번만 생성해 재사용하고, 컴포넌트가 파괴될 때 참조를 해제하도록 했습니다.
패턴을 적용하는 것보다 각 단계와 분기가 독립적으로 이해되고 테스트될 수 있게 만드는 데 집중했습니다. 그 결과 전체 흐름은 Orchestrator에서 읽히고, 세부 변경은 해당 Service나 Handler 안에서 끝나게 됐습니다.