Files
2nd/10_Wiki/Topics/Architecture/엔터프라이즈 애플리케이션 및 점진적 리팩토링.md
T
2026-05-10 22:08:15 +09:00

7.3 KiB

id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
id title category status canonical_id aliases duplicate_of source_trust_level confidence_score verification_status tags raw_sources last_reinforced github_commit tech_stack
wiki-2026-0508-엔터프라이즈-애플리케이션-및-점진적-리팩토링 엔터프라이즈 애플리케이션 및 점진적 리팩토링 10_Wiki/Topics verified self
Incremental Refactoring
Strangler Fig
Brownfield Refactor
none A 0.9 applied
refactoring
enterprise
legacy
strangler-fig
2026-05-10 pending
language framework
typescript nestjs-nginx

엔터프라이즈 애플리케이션 및 점진적 리팩토링

매 한 줄

"매 big rewrite는 실패한다 — strangle 하라". 수년간 누적된 enterprise codebase는 한 번에 다시 쓸 수 없다. Martin Fowler의 Strangler Fig, branch by abstraction, parallel run 같은 점진 패턴으로 운영 중인 시스템 옆에 새 모듈을 자라게 하고, 기존을 천천히 제거.

매 핵심

매 점진적 vs Big Bang

  • Big bang rewrite: 6-24개월 dark, 새 기능 동결, 비즈니스 중단. 80% 실패 (Joel Spolsky의 "things you should never do").
  • 점진적: 매 PR 단위 가치 전달, rollback 가능, business risk 최소.

매 4 핵심 패턴

  • Strangler Fig: 기존 시스템 앞에 facade(예: API gateway, reverse proxy), traffic을 점차 새 구현으로 routing.
  • Branch by Abstraction: 코드 내부 abstraction 추가 → 두 구현 병존 → old 제거.
  • Parallel Run: 신·구 동시 실행, 결과 비교, 차이 발견 시 alert. 신뢰 확보 후 cutover.
  • Anti-Corruption Layer (DDD): legacy 모델이 새 도메인을 오염하지 않도록 translator layer.

매 응용

  1. Monolith → Microservice: route 별 strangler.
  2. DB migration: dual write + parallel read + cutover.
  3. Framework upgrade (e.g., Angular 1→latest): branch by abstraction per route.
  4. Algorithm replacement (legacy pricing → ML pricing): parallel run.

💻 패턴

Strangler Fig — Nginx routing

# 1단계: 100% legacy
location /api/orders { proxy_pass http://legacy-monolith; }

# 2단계: new endpoint만 신규로
location /api/orders/v2 { proxy_pass http://order-service; }

# 3단계: feature flag로 점진 cutover
location /api/orders {
  set $backend "legacy-monolith";
  if ($cookie_canary = "v2") { set $backend "order-service"; }
  proxy_pass http://$backend;
}

# 4단계: 100% 신규 → legacy 제거
location /api/orders { proxy_pass http://order-service; }

Branch by Abstraction (코드 내부)

// 1단계: 추상화 추출
export interface PricingEngine {
  price(cart: Cart): Promise<Money>;
}

// 2단계: 기존 구현을 interface 뒤로
export class LegacyPricing implements PricingEngine {
  async price(cart: Cart) { /* 10년 묵은 코드 */ }
}

// 3단계: 신규 구현 추가, feature flag로 선택
export class NewPricing implements PricingEngine {
  async price(cart: Cart) { /* 새 도메인 모델 */ }
}

@Injectable()
export class PricingFactory {
  constructor(private readonly flags: FeatureFlags) {}
  get(userId: string): PricingEngine {
    return this.flags.isOn('new-pricing', userId)
      ? new NewPricing()
      : new LegacyPricing();
  }
}

Parallel Run + diff alarm

@Injectable()
export class PricingShadow implements PricingEngine {
  constructor(
    private readonly legacy: LegacyPricing,
    private readonly fresh:  NewPricing,
    private readonly metrics: Metrics,
  ) {}

  async price(cart: Cart): Promise<Money> {
    const [oldP, newP] = await Promise.allSettled([
      this.legacy.price(cart),
      this.fresh.price(cart),
    ]);

    if (oldP.status === 'fulfilled' && newP.status === 'fulfilled'
        && !oldP.value.equals(newP.value)) {
      this.metrics.inc('pricing.diff', { sku: cart.skus.join(',') });
      log.warn({ legacy: oldP.value, fresh: newP.value }, 'pricing diff');
    }

    return oldP.status === 'fulfilled' ? oldP.value : (newP as any).value;
  }
}

DB migration: dual write + verify + cutover

// Phase A: dual write (legacy = source of truth)
async function saveOrder(o: Order) {
  await legacyDb.orders.insert(toLegacy(o));
  try { await newDb.orders.insert(toNew(o)); }
  catch (e) { metrics.inc('dual-write.new.fail'); /* not fatal yet */ }
}

// Phase B: read from new + verify against legacy (shadow read)
async function getOrder(id: string): Promise<Order> {
  const [legacy, fresh] = await Promise.all([
    legacyDb.orders.find(id), newDb.orders.find(id),
  ]);
  if (legacy && fresh && !equal(legacy, fresh)) {
    metrics.inc('dual-read.diff');
  }
  return fromLegacy(legacy);  // 아직 legacy가 truth
}

// Phase C: cutover — new = source of truth, legacy = read shadow
// Phase D: legacy stop

Anti-Corruption Layer

// legacy/order-record.ts — 외부 모델 (변경 불가)
interface LegacyOrderRecord {
  ord_id: string;
  cust_no: number;
  itm_lst: string;   // pipe-delimited!
  amt_krw: number;
}

// domain/order.ts — 새 도메인 모델
export class Order { /* clean */ }

// acl/order-acl.ts — translator
export function fromLegacy(r: LegacyOrderRecord): Order {
  return new Order({
    id: r.ord_id,
    customerId: `legacy-${r.cust_no}`,
    items: r.itm_lst.split('|').map(parseItem),
    total: Money.krw(r.amt_krw),
  });
}

Test characterization (Working Effectively with Legacy Code)

// Refactor 전: 기존 동작을 test로 pin
test('legacy pricing — golden', async () => {
  for (const fixture of loadFixtures('pricing/*.json')) {
    expect(await legacy.price(fixture.cart)).toEqual(fixture.expected);
  }
});
// 이후 refactor가 동작을 깨면 즉시 감지

매 결정 기준

상황 Approach
모놀리스 → 서비스 분리 Strangler Fig (proxy level)
동일 process 내부 모듈 교체 Branch by Abstraction
위험한 알고리즘 교체 Parallel Run
Legacy 모델 격리 Anti-Corruption Layer
DB 교체 Dual write → shadow read → cutover

기본값: 매 refactor는 매 PR 단위 deploy 가능해야 함 — 며칠 이상 머지 안 되는 long-lived branch 금지.

🔗 Graph

🤖 LLM 활용

언제: 대규모 legacy 시스템 refactor 계획, 마이그레이션 step decomposition. 언제 X: 파일 < 1000줄, 작은 internal tool — 그냥 다시 쓰는 게 빠름.

안티패턴

  • Big bang rewrite: 동결 기간 동안 비즈니스 정체 + cutover 시 폭발.
  • Long-lived feature branch: refactor 가지가 main에서 분기되어 영원히 conflict.
  • Test 없이 refactor: 회귀 감지 불가 → 결국 rollback.
  • Strangler 도중 멈춤: 신·구 system 영구 공존 → 복잡도 2배.

🧪 검증 / 중복

  • Verified (Martin Fowler, "StranglerFigApplication"; Michael Feathers, Working Effectively with Legacy Code, 2004).
  • 신뢰도 A.

🕓 Changelog

날짜 변경
2026-05-08 Phase 1
2026-05-10 Manual cleanup — strangler·BBA·parallel run·ACL 패턴