AgentSkillsCN

domain-invariant-pattern

将领域不变式(Invariant)提取为可复用的辅助函数的模式。 适用场景:在实体设计时提取业务规则、在测试用例中识别条件表达式, 或在 UI/API/测试中需要重复使用相同逻辑时。

SKILL.md
--- frontmatter
name: domain-invariant-pattern
description: |
  도메인 불변식(Invariant)을 재사용 가능한 헬퍼 함수로 추출하는 패턴.
  Use when: Entity 설계 시 비즈니스 규칙 추출, 테스트 케이스에서 조건 식별,
  UI/API/테스트에서 동일 로직 재사용이 필요할 때.
globs:
  - "**/_models/**/*.ts"
  - "**/models/**/*.ts"
  - "**/domain/**/*.ts"

Domain Invariant Pattern

도메인 불변식을 헬퍼 함수로 추출하여 UI, API, 테스트에서 재사용하는 패턴.

헬퍼 함수 유형

접두사용도반환 타입예시
is*상태 조건 체크booleanisMaximizeConversionsBidding()
get*불변식 고려한 파생값도메인 타입getDailyBudget()
can*행동 가능 여부booleancanEditDailyBudget()
should*조건부 동작 체크booleanshouldShowBudgetWarning()

Given/When/Then에서 추출

code
Given 분석 → is* (상태 조건)
When 분석  → can* (가능 조건)
Then 분석  → get*, should* (파생값, 동작 조건)

예시:

#GivenWhenThen추출 헬퍼
1입찰 전략이 "전환수 최대화"일예산 수정 시도필드 비활성화isMaximizeConversionsBidding, canEditDailyBudget
2일예산 < 일소진액×3대시보드 진입경고 표시shouldShowBudgetWarning

의존성 순서 (Layer)

code
Layer 1 (Base): is* 함수들 (의존성 없음)
    ↓
Layer 2 (Derived): can*, get* (is* 의존)
    ↓
Layer 3 (Composite): should* (여러 함수 조합)
typescript
// Layer 1
function isMaximizeConversionsBidding(adGroup: AdGroup): boolean {
  return adGroup.biddingType === 'MAXIMIZE_CONVERSIONS';
}

// Layer 2 (Layer 1 사용)
function canEditDailyBudget(adGroup: AdGroup): boolean {
  return !isMaximizeConversionsBidding(adGroup);
}

function getDailyBudget(adGroup: AdGroup): number | null {
  if (isMaximizeConversionsBidding(adGroup)) return null;
  return adGroup.dailyBudget;
}

// Layer 3 (Layer 1, 2 사용)
function shouldShowBidSettings(adGroup: AdGroup, campaign: Campaign): boolean {
  return canEditDailyBudget(adGroup) && campaign.status !== 'PAUSED';
}

사용 위치별 예시

UI 렌더링

tsx
function DailyBudgetField({ adGroup }: Props) {
  const disabled = !canEditDailyBudget(adGroup);
  const value = getDailyBudget(adGroup);

  return (
    <NumberInput
      value={value}
      disabled={disabled}
      placeholder={disabled ? "자동 설정" : "금액 입력"}
    />
  );
}

API 요청 Body

typescript
function buildUpdateRequest(adGroup: AdGroup): UpdateRequest {
  return {
    id: adGroup.id,
    ...(shouldIncludeBidAmount(adGroup) && { bidAmount: adGroup.bidAmount }),
    dailyBudget: getDailyBudget(adGroup),
  };
}

테스트 코드

typescript
describe('일예산 수정', () => {
  it('전환수 최대화 입찰이면 수정 불가', () => {
    const adGroup = createAdGroup({ biddingType: 'MAXIMIZE_CONVERSIONS' });

    expect(canEditDailyBudget(adGroup)).toBe(false);
    expect(getDailyBudget(adGroup)).toBeNull();
  });
});

파일 구조

Entity 파일 내부에 헬퍼 함수를 함께 정의:

typescript
// src/domain/AdGroup.ts (또는 _models/AdGroup.ts)

// Entity 타입
export interface AdGroup {
  id: string;
  biddingType: BiddingType;
  dailyBudget: number;
}

// ===== Invariant Helpers =====

export function isMaximizeConversionsBidding(adGroup: AdGroup): boolean {
  return adGroup.biddingType === 'MAXIMIZE_CONVERSIONS';
}

export function canEditDailyBudget(adGroup: AdGroup): boolean {
  return !isMaximizeConversionsBidding(adGroup);
}

export function getDailyBudget(adGroup: AdGroup): number | null {
  if (isMaximizeConversionsBidding(adGroup)) return null;
  return adGroup.dailyBudget;
}

흔한 실수와 해결책

문제원인해결
헬퍼 함수 중복UI/API에서 각각 구현공통 invariants.ts에 정의
조건 불일치같은 규칙을 다르게 해석Single Source of Truth 원칙
테스트 누락헬퍼 함수를 테스트 안 함헬퍼 함수별 단위 테스트 필수
의존성 순환is* 함수가 can* 함수 호출Layer 구조 준수
과도한 추상화모든 조건을 헬퍼로 추출재사용되는 경우만 추출