Skip to content

급여 계산 조회·저장·응답 성능 검증 ​

2026-10-21에 runCompute의 세액표·조직 파라미터 중복 조회를 줄이고, 계산 셀 저장 SQL과 급여 화면의 행 응답을 추가로 줄였다. 계산식, 반올림, 적용 세액표 버전 선택, 저장기의 동시 입력 보호는 유지한다. 아래 1차 측정은 조회 재사용만, 추가 개선 측정은 세 변경을 모두 포함한다. 이 기록은 로컬 코드·테스트·PostgreSQL·개발 브라우저 측정의 근거이며 PR 머지·개발 배포·운영 체감 속도 확인을 뜻하지 않는다.

원인과 변경 범위 ​

세액 계산은 각 행에서 같은 조회 조건을 반복했고, 조직 파라미터는 전체 설정이 다른 묶음마다 공통 참조를 다시 조회했다. 기존의 완전히 같은 설정 묶기는 유지되지만, 배수만 다른 설정끼리 공유하는 단가 조회까지 제거하지는 못했다. 결과값만 검사하는 테스트는 이 비용 증가를 발견할 수 없다.

대상변경 전변경 후유지한 경계
세액표행마다 구간 조회한 모듈 입력 로딩에서 가족 수·조회 소득이 같은 조건 재사용조직·세액표 버전은 해당 로딩에서 고정, 행별 원천징수율·자녀 공제·실제 초과 소득은 각각 계산
조직 파라미터설정 묶음·참조 별칭마다 조회한 runCompute 안에서 전체 참조 경로별 재사용다른 계산·조직으로 공유하지 않으며 appliedParameters는 설정별로 생성

0·false·null도 조회 결과로 취급한다. 없는 세액 구간/파라미터도 같은 요청에서 다시 조회하지 않으며, DB 오류는 정상 누락값으로 바꾸지 않고 전파한다. 다음 계산은 값 수정·삭제·복원·새로운 추가를 다시 읽는다.

동일 참조가 계산 도중 변경되면 그 참조를 처음 읽은 값으로 해당 계산의 일관성을 유지하고 다음 실행에서 변경을 반영한다. 서로 다른 참조까지 하나의 DB 시점으로 고정하는 트랜잭션 스냅샷은 아니다. PostgreSQL의 기본 Read Committed 조회는 문장마다 새 스냅샷을 볼 수 있으므로 둘을 혼동하지 않는다. PostgreSQL 18 트랜잭션 문서

실제 PostgreSQL 측정 ​

  • 기준 코드: 6f7fb2bc3ef2970cd845fa04b8056d9406a40172의 compute.ts.
  • 환경: Apple M4 Pro, Node 24.15.0, Docker PostgreSQL 18.6, 로컬 TCP. 인위적 네트워크 지연은 넣지 않았다.
  • 데이터: 합성 수당·세액표·파라미터·시트 행. 실제 runCompute, TypeORM, PostgreSQL, 셀 저장기를 사용했다. 계산·DB 응답 mock은 없다.
  • 엔티티 메타데이터로 임시 스키마의 테이블·인덱스를 생성했다. FK와 일부 migration 전용 제약/인덱스는 운영과 다르므로 운영 스키마 전체의 재현이라고 보지 않는다.
  • 각 조건은 수정 전후 2회씩 준비 실행 후 12회씩 교차 측정했다. 신규 셀 저장과 기존 계산 셀 갱신을 분리하고 매번 동일한 초기 상태를 만들었다.
  • 측정 구간은 입력 조회부터 계산과 최종 셀 저장까지다. 데이터 준비·결과 확인 시간은 제외했다. HTTP·인증·브라우저 렌더링·Chrome Performance trace는 포함하지 않는다.
  • 매 실행의 계산 보고서, 모든 저장 셀의 값·종류·행/열 연결, 입력 스냅샷을 수정 전 결과와 깊게 비교했다. 생성 UUID·계산 시각 등 실행마다 달라지는 메타데이터는 비교 대상에서 제외했다.

아래는 신규 셀 저장의 중앙값이다. 전체 쿼리 수는 계산 구간에서 기록한 SQL 수이며, 세액표 횟수에는 버전 선택 1회가 포함된다.

조건수정 전수정 후계산 구간 SQL대상 조회
세액 1행6.06ms6.09ms15 → 15세액표 2 → 2
세액 500행, 동일 조건232.79ms60.23ms526 → 27세액표 501 → 2
세액 500행, 50개 조건231.45ms73.59ms526 → 76세액표 501 → 51
세액 500행, 모두 다른 조건210.60ms205.50ms526 → 526세액표 501 → 501
공통 파라미터 1개 설정2.42ms2.25ms6 → 6파라미터 1 → 1
공통 파라미터 30개 설정14.91ms4.08ms35 → 6파라미터 30 → 1
500행, 세액 50개 조건 + 파라미터 30개 설정950.35ms799.31ms781 → 302세액표 501 → 51, 파라미터 33 → 4

기존 셀 갱신 중앙값은 동일 세액 500행에서 227.10 → 60.28ms, 파라미터 30개 설정에서 13.20 → 3.80ms, 혼합 500행에서 1064.98 → 880.26ms였다. 저장 작업 비중이 큰 혼합 조건에서는 조회 감소율만큼 전체 시간이 줄지는 않았다.

중복이 없는 세액 500행의 첫 갱신 측정은 216.03 → 227.78ms로 수정 후가 5.4% 높았다. 같은 조건을 각 버전 36회 교차 재측정하니 신규 212.46 → 209.04ms, 갱신 221.16 → 221.58ms였다. 이 조건은 조회 감소나 성능 개선으로 계산하지 않는다. 전후 범위·전체 표본은 원시 기록에 남긴다.

전체 원시 측정 · 중복 없는 조건 재측정

운영의 데이터 분포·지연·동시 부하와 다른 로컬 실측이다. 이 수치를 전체 급여 화면의 개선율로 확대하지 않는다. 별도로 같은 시트·데이터를 사용한 브라우저 측정 결과는 다음 절에 기록한다.

1차 개발 브라우저 비교: 조회 재사용만 ​

동일한 500행 시트에서 계산 클릭부터 새 결과가 표시되는 프레임까지 중앙값은 1,724 → 1,592ms로 7.6% 줄었다. 계산 HTTP 요청은 1,197 → 997ms로 16.7% 줄었지만, 렌더링 자체의 개선은 확인되지 않았다. 각 버전 6회 측정 범위는 겹치므로 이 비율을 운영 환경의 개선 보장이나 통계적 유의성으로 해석하지 않는다.

환경과 측정 경계 ​

  • 로컬 개발 환경: Apple M4 Pro, Node 24.15.0, Vite 개발 서버, ts-node API, Docker PostgreSQL 17.7, HeadlessChrome 155, 1440×1000, DPR 1, CPU 1배속, 네트워크 속도 제한 없음. 위 서버 단독 시험의 PostgreSQL 18.6과 환경이 다르므로 두 표의 절대 시간을 직접 비교하지 않는다.
  • 기존 로컬 DB의 스키마만 복제한 독립 DB에 미적용 마이그레이션 77개를 적용했다. 실제 FK·인덱스·제약을 유지하고 테스트 조직·계정·직원·수당을 새로 생성했다. 배포된 개발 서버나 운영 DB를 측정한 것이 아니다.
  • 한 시트에 직원 500명, 세액 조회 조건 50개, 세액 열 2개, 같은 단가를 참조하는 서로 다른 설정의 수당 열 30개를 구성했다. 계산 대상은 16,000셀이다. 모든 반복에서 같은 시트 ID·행 ID·입력·설정을 사용하고 계산 셀을 -1로 되돌린 뒤 화면에도 -1이 표시되는지 확인했다.
  • 기준은 위 커밋의 compute.ts, 수정 후는 해당 파일의 조회 재사용 변경이다. API를 정지한 상태에서 이 파일만 교체했으며 프런트엔드는 동일했다. 측정 종료 후 수정 파일의 SHA-256이 원래 최적화 파일과 같음을 확인했다.
  • 순서는 수정 전 → 수정 후 → 수정 후 → 수정 전이다. 각 블록마다 API를 재시작하고 준비 실행 2회 후 본 측정 3회를 기록했다. 준비 8회와 본 측정 12회 모두 별도로 저장했으며, 표는 본 측정만 사용한다. 모든 본 측정의 두 핵심 HTTP 요청은 상태 200이고 캐시 응답이 아니었다.
  • 실제 메뉴의 계산 실행 버튼 클릭 이벤트에서 시작했다. 화면에 렌더링된 소득세 셀 전부가 50,000으로 바뀐 것을 관찰한 뒤 두 번째 requestAnimationFrame을 종료점으로 삼았다. 이는 DOM 갱신만으로 종료하지 않고 페인트 기회를 포함하는 보수적인 화면 표시 지표다. 모든 trace에서 해당 구간의 Paint 이벤트도 확인했고 결과 스크린샷을 검토했다. 물리 디스플레이의 표시 시점을 직접 측정한 것은 아니다.
  • Chrome DevTools Performance 원본 trace를 매회 수집했다. 네트워크 시간·바이트는 trace의 실제 요청·응답·전송 완료 이벤트에서 추출했다. 출처가 다른 API에 대한 Resource Timing의 크기 0을 실제 0바이트로 해석하지 않았다.

결과 ​

단위는 ms, 값은 중앙값이다. 각 구간의 중앙값을 더한 값은 전체 중앙값과 다를 수 있다. DOM 그리기 호출과 Layout·Paint 등은 겹칠 수 있어 합산하지 않는다.

측정 항목수정 전수정 후해석
클릭 → 결과 표시 프레임1,7241,5927.6% 감소
위 전체 시간 범위1,619–2,0221,390–1,768표본별 변동 존재
계산 HTTP 요청 전체1,19799716.7% 감소
계산 요청 TTFB1,195995개선은 대부분 응답 대기 구간
후속 행 재조회 HTTP 전체278293개선 확인 안 됨
행 재조회 TTFB246263서버 조회·응답 준비와 왕복 지연 포함
행 재조회 본문 다운로드2725로컬 전송 시간
행 응답 다운로드 완료 → 결과 표시 프레임227243브라우저 측 처리 비용이 남음
위 구간 중 SDK 결과 반환까지95100응답 처리·JSON 해석·콜백 대기 포함
renderToBody 호출5763화면 그리기 함수 자체는 빨라지지 않음
trace의 최장 메인 스레드 작업145164입력 반응을 막을 수 있는 긴 작업이 남음
계산 관련 HTTP 요청 수22계산 1회 + 행 재조회 1회
계산 응답 본문178바이트178바이트변화 없음
행 재조회 응답 본문9,778,471바이트9,778,471바이트약 9.78MB, 변화 없음

TTFB는 서버 처리와 네트워크 왕복을 함께 포함한다. 이번 변경이 회선 속도를 높이거나 HTTP 요청 수·전송량을 줄인 것은 아니다. 개발 API의 행 응답은 압축되지 않았으며 운영 프록시의 압축·대역폭·지연은 이번 측정에 포함하지 않았다. Chrome 네트워크 시간 정의, Performance 기록 방법

매회 DB에서 16,000셀의 계산값·종류·행/열 식별자·입력 및 적용 파라미터 스냅샷을 정렬해 비교했다. 값은 독립적으로 기대한 소득세 50,000원·지방소득세 5,000원·각 수당의 올림 결과와도 일치했다. 신규 기록 시각은 비교에서 제외했다. 준비·본 측정 전부 같은 SHA-256 1b218f34f8cdbdce7774ca910f487337aed2515eef263f4e9c399db39b4facc3였고, 매회 500행 처리·16,000셀 기록·충돌/수동 입력 건너뜀 0건이었다. 이는 해당 고정 데이터의 동일성 검증이며 앞 절의 경계·동시성 회귀를 대체하지 않는다.

브라우저 측정 전체 표본·요청 시간·바이트·trace 해시·별도 서버 프로파일

1차 측정에서 찾은 추가 병목 ​

  1. 계산 후 전체 행과 셀 메타데이터 재전송. runCompute()가 끝나면 프런트엔드 engine.reload()가 queryRowsPage를 호출한다. 응답의 cellMeta 값만 8,860,500바이트로 전체의 약 **90.6%**다. resolver.ts는 셀 엔티티 전체를 담으며 감사 스냅샷뿐 아니라 반복되는 ID·시각·null 참조 필드도 포함한다. 실제 편집에 필요한 최소 필드와 상세 조회를 구분하고, 계산 후 변경 셀만 반환하는 경로를 검토할 가치가 있다. 이 시점에는 필드 생략의 안전성을 검증하지 않았다. 후속 작업에서 소비 경로를 확인하고 편집용 선택 응답과 회귀 테스트를 추가했다.
  2. 계산 셀 저장 비용. 주 비교와 분리한 수정 후 서버 계측 3회에서 runCompute는 매회 SELECT 62회, UPDATE 80회, START/COMMIT 각각 80회를 실행했다. 200셀씩 저장하므로 16,000셀에 80묶음이 생긴다. UPDATE 대기 중앙값은 약 543ms, 트랜잭션 시작·커밋 합계 중앙값은 약 177ms였다. 저장과 트랜잭션만 약 722ms로 해당 별도 계산 전체 중앙값 1,277ms의 약 57%였다. 계측을 추가한 별도 실행이므로 위 브라우저 비교 표본과 섞지 않는다. 묶음 크기나 SQL 형태를 바꾸려면 기존 xmin 조건부 저장·수동 입력 보존·부분 실패 순서·커밋 결과 불확실성 처리를 그대로 검증해야 한다.
  3. 큰 응답 처리와 DOM 재구축. 본문 수신이 끝나도 결과 프레임까지 약 243ms가 남고, 수정 후 최장 작업은 중앙값 164ms였다. renderToBody()는 약 63ms이며 데이터 재조회 후 행 처리, DOM 교체, 서식·표식 재적용이 이어진다. JSON 크기 축소와 변경 셀 갱신을 먼저 검토하고, 행 가상화만으로 응답 데이터 비용까지 해결됐다고 판단하지 않는다.

다음 최적화의 회귀 기준은 결과·감사 정보·편집 유형·잠금·권한·서식·정렬·필터·합계·동시 수정의 보존이다. CI에는 고정 데이터의 응답 바이트 상한과 요청 수·재조회 횟수처럼 안정적인 비용 계약을 추가하는 것이 적합하다. 시간 임계값은 공유 CI의 잡음 때문에 단독 합격 기준으로 삼지 않고 같은 개발 환경의 반복 trace와 함께 판단한다. 이 1차 조사까지는 위 후보의 제품 코드를 변경하지 않았다. 후속 구현과 재측정은 다음 절에 별도로 기록한다.

추가 개선: 저장 SQL과 화면 응답 ​

계산 셀 저장의 UPDATE를 반복 CASE/OR 조건에서 매개변수화한 UPDATE FROM VALUES로 바꿨다. 200셀 묶음, 트랜잭션, 중복 셀의 순차 처리, 실패 시 되돌림과 부분 저장 순서는 유지한다. 셀 ID와 xmin이 모두 일치하는 살아 있는 계산 셀만 갱신하므로 계산 중 발생한 직접 입력·수식·서식·삭제를 덮지 않는다. updatedAt 갱신과 계산 감사 스냅샷도 유지한다. 테이블과 스키마 이름은 엔티티 메타데이터에서 읽어 드라이버로 인용하고, 값은 바인딩한다. 입력 행이 같은 대상 셀에 중복 대응하지 않도록 기존 순차 경로를 유지했다. PostgreSQL UPDATE 문서

queryRowsPage에는 선택형 cellMetadata: "grid"를 추가했다. 기존 기본값 full은 전체 셀 정보를 유지한다. 급여 표의 첫 페이지와 후속 페이지는 grid를 요청해 셀 종류·직접 입력값·수식 표현식만 받는다. 값·정렬·필터·권한·집계는 기존 계산을 거치며, 응답 직전에만 메타데이터를 줄인다. 원본 엔티티, 저장된 감사 기록, 서식·이력의 별도 조회 경로는 그대로다. 이전 서버는 새 입력 옵션을 제거하고 전체 응답을 반환하므로 배포 순서에 따른 필수 응답 누락도 없다.

실제 개발 API의 500행 전체 응답과 경량 응답을 별도로 읽어 행 값·페이지 정보 전체 및 편집용 세 필드가 동일한 것을 확인했다. 단위 테스트만으로 API 연결을 추정하지 않았다. 다만 수정 후의 모든 편집·서식 UI 조합을 수동으로 실행한 것은 아니므로 아래 회귀 범위와 구분한다.

세 변경을 포함한 개발 브라우저 재측정 ​

행 응답 본문은 9,778,471 → 2,149,471바이트로 78.0% 줄었다. 계산 클릭부터 결과 표시 프레임까지의 관측 중앙값은 2,190 → 990ms였다. 다만 다른 데스크톱 앱의 높은 CPU 활동과 실행 간 변동이 관찰됐다. 이 시간 차이를 최적화만의 정확한 인과 효과나 운영 성능 보장으로 해석하지 않는다. 느린 표본도 제외하지 않았다.

앞의 브라우저 측정과 같은 종류의 로컬 환경(Apple M4 Pro, Node 24.15.0, PostgreSQL 17.7, Vite/ts-node, Chrome 155, 1440×1000, DPR 1, CPU·네트워크 제한 없음)을 사용했다. 기존 DB의 스키마만 복제하고 미적용 마이그레이션 77개를 적용한 새 독립 DB에 합성 데이터를 넣었다. 한 시트의 같은 직원 500명·32개 계산 열·16,000셀을 매회 -1로 초기화했다. 배포된 개발 서버의 실측이 아니다.

기준 커밋 6f7fb2bc3ef2970cd845fa04b8056d9406a40172의 계산기·저장기·급여 어댑터와 이번 세 변경을 비교했다. 선택형 응답 스키마·투영 코드는 양쪽 서버에 공통으로 존재하지만, 기준 어댑터는 옵션을 보내지 않아 기본 전체 응답이 그대로 반환된다. 코드 세 파일의 전후 해시가 서로 다른지 검사하고, 종료 후 최적화 파일 해시의 정확한 복원도 확인했다.

순서는 수정 전·후·후·전이며 각 블록마다 API 재시작과 페이지 재진입, 준비 실행 2회 후 본 측정 3회다. 각 버전 본 측정 6회, 준비 실행 8회를 분리했다. 로그인·시트 준비에 실패한 시도는 계산 측정 전에 중단했다. 실제 계산 버튼 클릭에서 시작해 렌더링된 소득세 37셀이 모두 50,000으로 바뀐 후 두 번째 requestAnimationFrame까지 측정했다. 본 측정 12개 trace 모두 그 사이 Paint가 존재했다. 물리 디스플레이 표시 시각을 직접 잰 것은 아니다.

측정 항목수정 전 중앙값수정 후 중앙값
클릭부터 결과 표시 프레임2,190ms990ms
위 전체 시간 범위1,557–4,270ms937–3,320ms
계산 HTTP 요청 전체1,605ms627ms
계산 요청 TTFB1,596ms623ms
후속 행 재조회 HTTP 전체304ms179ms
행 재조회 TTFB272ms174ms
행 응답 본문 다운로드34.3ms4.9ms
다운로드 완료부터 결과 표시 프레임263ms164ms
위 구간 중 SDK 결과 반환까지109ms27ms
renderToBody 호출69ms59ms
최장 메인 스레드 작업175ms126ms
계산 관련 HTTP 요청 수22
행 응답 본문9,778,471바이트2,149,471바이트

개별 블록의 클릭부터 표시까지 중앙값은 시간 순으로 수정 전 3,920ms / 수정 후 938ms / 수정 후 1,005ms / 수정 전 1,614ms다. 같은 버전에서도 부하 차이가 크다. 특히 DOM 그리기 호출은 전후 범위가 52–101ms / 51–89ms로 겹치므로 렌더러 자체가 확실히 빨라졌다고 판단하지 않는다. 응답 크기 감소는 모든 표본에서 같지만, 시간 결과는 이 부하 조건의 관측치로 남긴다. TTFB는 서버 처리와 네트워크 왕복이 합쳐진 값이며 회선 속도 개선이 아니다. 각 구간은 겹치거나 중앙값의 합이 다를 수 있어 더하지 않는다.

모든 본 측정에서 계산·재조회 요청은 200, 캐시 사용 없음, 요청 2회였다. 준비 실행을 포함한 20회 모두 DB의 16,000셀 값·종류·연결·입력 및 적용 파라미터 스냅샷 해시가 8a9c4c1b138b299d2e064c5cba2fa8afd4d4b9debd973936d6770ff72e53af52로 같았다. 매회 500행 처리·16,000셀 저장·충돌 및 수동 입력 건너뜀 0건을 확인했다. 시간 메타데이터는 비교에서 제외하고 소득세·지방소득세·수당 기대값을 별도로 검사했다.

전체 표본·네트워크 시간·바이트·원본 trace 해시

남은 비용은 계산 뒤 전체 행을 재조회하는 두 번째 요청, resolver의 전체 셀 엔티티 로딩, 200셀마다 유지되는 트랜잭션, DOM 재구축이다. 변경 셀만 반영하는 경로와 내부 조회 필드 축소는 후속 후보이며 이번 PR에 구현하지 않았다. 다음 변경도 기본 응답·수식·감사·서식·권한·동시 입력 보존을 먼저 검증해야 한다.

추가 회귀 및 최종 로컬 검증 ​

확인결과
실제 PostgreSQL타입 검사 및 92개 통과. 조회 재사용 29개, 새 저장 SQL 1개, 기존 경합·실패 회귀 62개
응답 경계새 테스트 5개 통과. 기본 전체 응답, 경량 값·수식·0·false·null, 원본 불변, 조직 범위, 응답 크기 상한
급여 어댑터첫 페이지와 후속 페이지의 경량 요청 및 수식 편집 정보 보존 테스트 통과
수정 전 대조 실행조회 회귀 13개 실패. 기존 응답 투영은 3개 실패, 라우터·급여 어댑터 연결은 각각 1개 실패
저장 SQL 비용 회귀기존 200셀 UPDATE는 29,849바이트로 24KB 상한 실패. 새 SQL은 상한 통과하며 200개의 서로 다른 값·계산 시각·감사 스냅샷·기존 서식도 확인
응답 크기 회귀고정 500×32셀 시험의 기존 응답 7,493,891바이트는 2MB 상한 실패. 경량 응답 통과. 브라우저 실측 시트와 별도 fixture이므로 크기를 혼용하지 않음
전체 lint오류 0, 기존 경고 1,752개 및 정보 2개
전체 빌드24개 작업 성공, 2개 캐시
전체 테스트30개 작업 성공. Vitest 8,467개 통과, docs 테스트 6개 통과. 일반 실행에서 건너뛴 PostgreSQL 92개는 위 별도 DB 실행으로 통과. 시스템 관리자 환경 의존 16개는 미실행
독립 코드 리뷰조회 범위·SQL 경합 보호·트랜잭션·응답 호환성 검토에서 P1/P2 지적 없음. 급여 관련 42개 테스트도 별도 통과

당시 CI는 새 단위 테스트와 *.postgres.test.ts를 자동 수집했다. PostgreSQL 테스트용 TypeScript 검사 목록에도 새 API 응답 테스트를 포함했다. 안정적인 조회 횟수·SQL 및 응답 크기를 검사해 비용 증가를 잡으며, 밀리초 임계값을 단독 합격 기준으로 삼지 않는다.

조회 재사용 회귀와 재발 방지 ​

packages/api/src/routers/sheet-rows/compute-lookups.postgres.test.ts가 다음을 실제 PostgreSQL로 검증한다.

  • 구간 경계, 부양가족 반올림/최소/최대, 비과세 차감, 원천징수율·자녀 수, 지방소득세, 천만원 초과 산식의 기존 결과.
  • 세액표 시행일·조직 분리·삭제 제외, 0원과 미계산의 구분, 값 변경 후 재계산.
  • 숫자·문자열·불리언·null·누락·잘못된 참조, 중첩 네임스페이스, 일반 상수와 참조 혼합, 원본 설정 불변 및 설정별 감사 스냅샷.
  • 동시 조직 계산 분리, 수동 입력 보존, 조회 오류 전파와 복구, 같은 계산의 첫 조회값 일관성.
  • 행/설정 수를 늘려도 중복된 조건 때문에 조회가 다시 늘지 않는 상한. 결과값도 함께 검사하므로 조회를 생략해 잘못된 기본값을 반환하는 구현은 통과하지 못한다.

2026-10-21 .github/workflows/check-code.yaml의 PR·develop 자동 실행 정의를 제거했다. PostgreSQL 회귀 테스트는 POSTGRES_TEST_DATABASE_URL을 지정하고 pnpm --filter @hereby/api test:postgres로 실행한다. 이 명령은 src/**/*.postgres.test.ts를 자동 수집하고 TypeScript 검사 후 실행하며, DB URL이 빠지면 실패한다. 기존 계산 저장·입력 경합 테스트도 함께 실행한다.

2026-10-21 조회 재사용 단계의 로컬 검증 결과(추가 개선 전):

확인결과
새 PostgreSQL 회귀29개 통과
원래 코드로 같은 회귀 실행13개 실패·16개 통과. 조회 상한 12개와 계산 도중 같은 참조의 변경 일관성 1개가 실패
기존 PostgreSQL 회귀 포함타입 검사 및 91개 통과. 새 29개 + 기존 62개
전체 lint오류 0. 저장소 경고 1,752개는 남아 있으며 변경한 코드 파일의 별도 검사는 경고 0
전체 빌드24개 작업 성공(1개 캐시)
전체 테스트30개 작업 성공(9개 캐시). API의 PostgreSQL 91개는 일반 실행에서는 skip, 위 별도 DB 실행에서 전부 통과. 시스템 관리자 환경 의존 16개는 미실행
별도 코드 리뷰운영 코드 결함 지적 없음. 권고한 초과 소득 조회 상한·계산 도중 파라미터 변경 테스트 추가

새 회귀의 통과와 전체 테스트 작업 성공을 모든 운영 시나리오의 무결함 보장으로 해석하지 않는다. GitHub 원격 CI와 배포 후 검증은 이 로컬 기록에 포함하지 않는다.

재발을 막는 검토 기준은 다음과 같다.

  1. 반복문 안에 조회를 추가할 때 행 수 N, 서로 다른 조회 조건 U, 참조 키 수 P를 구분한다. 이번 경로의 목표는 세액표 조회 1 + U, 현재값 파라미터 조회 P 이하다. 날짜 기준 자녀 공제 조회는 별도 경로다.
  2. 캐시 범위를 함수 실행보다 넓히려면 조직·기간·버전·설정 변경 무효화부터 설계한다. 이번 변경에는 프로세스 전역 캐시나 TTL을 도입하지 않는다.
  3. 조회 최적화 시 반환값뿐 아니라 누락·0·false, 감사 이력, 재계산, 오류 전파를 함께 검증한다. 계산식이 정상이어도 어댑터의 조회 조건이 틀리면 금액이 바뀐다.
  4. CI에서는 안정적인 조회 횟수 상한을 검사한다. 경합으로 흔들리는 밀리초 임계값은 CI 통과 조건으로 쓰지 않고, 아래 도구로 같은 조건의 전후 실측을 유지한다.
  5. 결과가 전부 다른 입력과 1행도 측정한다. 중복이 많은 유리한 사례만으로 전체 개선을 주장하지 않는다.

재실행 ​

격리된 임시 PostgreSQL URL을 사용한다. 도구는 무작위 이름의 스키마를 만들고 종료 시 제거한다. API 의존 패키지를 먼저 빌드해 최신 엔티티를 사용한다.

bash
pnpm --filter '@hereby/api^...' build
export POSTGRES_TEST_DATABASE_URL='postgres://postgres:regression@localhost:15439/hereby_regression'
pnpm --filter @hereby/api test:postgres

# 기준 파일은 compute.ts의 상대 import를 유지할 수 있는 같은 디렉터리에 둔다.
git show 6f7fb2bc3ef2970cd845fa04b8056d9406a40172:packages/api/src/routers/sheet-rows/compute.ts > packages/api/src/routers/sheet-rows/compute.lookup-baseline.ts
export COMPUTE_LOOKUP_BASELINE="$PWD/packages/api/src/routers/sheet-rows/compute.lookup-baseline.ts"
export COMPUTE_LOOKUP_BENCHMARK_OUTPUT=/tmp/compute-lookups.json
pnpm --filter @hereby/api exec tsx src/scripts/benchmark-compute-lookups.ts

# 중복 없는 조건을 더 반복할 때
COMPUTE_LOOKUP_BENCHMARK_SCENARIO=tax-500-unique COMPUTE_LOOKUP_BENCHMARK_REPEATS=36 COMPUTE_LOOKUP_BENCHMARK_OUTPUT=/tmp/compute-lookups-unique.json pnpm --filter @hereby/api exec tsx src/scripts/benchmark-compute-lookups.ts
rm packages/api/src/routers/sheet-rows/compute.lookup-baseline.ts

실측 중에는 다른 테스트·빌드를 동시에 돌리지 않는다. 원본 코드로 새 회귀 테스트를 실행했을 때 예상한 조회 횟수 실패가 나는지도 확인하고, 기준 파일을 PR에 포함하지 않는다.