Architecture

목표

월 단위 문자열 컬럼(YYYY, MM)을 가진 업무 조회를 합성 도메인으로 재현합니다. 핵심은 정합성을 먼저 고정한 뒤 index-friendly predicate를 유지하는 것입니다.

구조

ActivityQueryService
  -> YearMonthRange validation / normalization
  -> ActivitySnapshotMapper
       -> same year: one range branch
       -> cross year: start + middle + end UNION ALL branches
       -> shared rangeRows fragment
            |-> count
            `-> page + deterministic ORDER BY
  -> H2 synthetic schema/data

범위 분해

같은 연도

snapshot_year = startYear
AND snapshot_month BETWEEN startMonth AND endMonth

여러 연도

세 집합은 서로 겹치지 않습니다.

1. snapshot_year = startYear AND snapshot_month >= startMonth
2. snapshot_year > startYear AND snapshot_year < endYear
3. snapshot_year = endYear AND snapshot_month <= endMonth

이를 UNION ALL로 결합합니다. 중복 제거 연산에 의존하지 않고, 구간 자체를 상호 배타적으로 설계합니다.

인덱스와 입력 정규화

합성 스키마의 인덱스는 다음 순서입니다.

(tenant_id, snapshot_year, snapshot_month)

월은 애플리케이션에서 항상 두 자리 문자열로 정규화합니다. 따라서 mapper는 indexed column에 숫자 변환 함수를 적용할 필요가 없습니다.

이 설계는 인덱스를 사용할 수 있는 형태를 보존하려는 것이며, H2 테스트가 Oracle 운영 실행계획이나 성능 향상을 증명한다는 뜻은 아닙니다.

count/page 정합성

MyBatis <sql id="rangeRows">를 count와 page가 공유합니다. 필터 로직을 두 곳에서 따로 유지해 total count와 실제 목록의 조건이 벌어지는 회귀를 줄입니다.

결정적 pagination

동일 연월에 복수 row가 있을 수 있으므로 정렬 마지막에 id ASC를 둡니다.

snapshot_year DESC,
snapshot_month DESC,
id ASC

이 tie-breaker가 없으면 같은 조건의 반복 조회에서 page 경계가 불안정해질 수 있습니다.

공개 안전성

회사 원본의 SQL, schema, 데이터와 식별자를 사용하지 않습니다. 공개 코드는 activity_snapshot, alpha/beta tenant와 임의 점수 등 합성 fixture만 포함합니다.