buildrisk-radar

코드 리뷰 · 개선 계획 (2026-10-11, 2차)

AWS 데모에 올라간 코드(main 491a95d — 서버 HEAD 와 같음을 확인)를 다시 리뷰했다. 1차 리뷰(2026-10-09, 계획) 뒤에 바뀐 코드(ADR-031 AWS 배포, 문제 169~171)와 1차가 놓친 곳이 대상이다. 문제 번호는 1차를 이어 쓴다(SEC-15~, DL-20~, FE-12~).

0. 요약

  High Medium Low 계
보안 · 의존성 (SEC · DEP) 3 3 6 12
데이터 정합성 · 신뢰성 (DL)   6 2 8
구조 · 성능 (ARC · PERF)     4 4
프론트엔드 (FE)   1 8 9

1. 현재 상태 (기준선, 2026-10-11)

항목 값
테스트 백엔드 313 · 웹 단위 44 · E2E 48 — 모두 통과. tsc · ESLint 0. 최근 CI 5회 성공
취약점 경고 npm audit --omit=dev 0 · Dependabot 열린 경고 0 — 단, Dependabot 이 Gradle(JVM) 의존성을 전혀 보지 못함(DEP-3)
DB 버전 로컬 · 테스트 PostgreSQL 16.11(이미지 2026-02 이후 갱신 없음) / 운영(RDS) 18.6
리뷰 방법 다섯 관점(보안 · 데이터/신뢰성 · 구조/성능 · 프론트엔드 · 버전/인프라)을 별도 에이전트가 읽고, 로컬 스택에 GET · SELECT · EXPLAIN 만 보내 확인. High · Medium 은 다시 코드 · 재현으로 확인

부수 확인(문제 없음): PG18 + Hikari 4(AWS 와 같은 풀)로 백엔드 313개 모두 통과, 풀 고갈 로그 0. 인가 표는 대소문자 · %61 · ..; · // 변형에서도 막힘. SQL 은 모두 바인딩.

2. 발견한 문제 (심각도순)

2.1 보안 · 의존성

ID 심각도 문제 확인 해결
SEC-15 High 경로를 퍼센트 인코딩하면(/%61pi/v1/…) 레이트리밋 · CSV 내려받기 한도(분당 30) · 감사 로그 · API CSP 헤더가 모두 빠진다. 인가와 MVC 는 디코딩한 경로로 맞추는데, 다섯 곳(RateLimitFilter:33,44 · AuditFilter:51-53 · SecurityConfig:85,110)은 원래 문자열 getRequestURI() 를 비교한다. 관리자 세션이나 서비스 토큰으로 규칙을 바꿔도 감사에 남지 않는다(ADR-013 위협 ③) 로컬 재현: GET /%61pi/v1/meta → 200 · CSP 없음, 레이트리밋 카운터 그대로 다섯 곳이 인가와 같은 매처(정규화한 경로) 를 쓰게 한다. 인코딩 경로의 집계 · 감사 회귀 테스트
DEP-1 High Tomcat 11.0.25 — 11.0.26 에서 고친 Important 4건(요청 헤더 뒤섞임 회귀 CVE-2026-86350 은 11.0.22~25 해당) tomcat.apache.org/security-11 useVersion("11.0.26")
DEP-2 High Jackson 2.22.1 / 3.1.5 — High(CVSS 7.5) DoS 등 GHSA-7hhh-6rmp-j9qf 외 BOM 2.22.3 / 3.1.7
SEC-16 Medium 기업별 캐시(company: · filings: · pf: · prices: · sites:c:)가 유니버스 43곳이 아니라 ref.company 전체 119,447곳의 코드마다 10분씩 저장된다. 세션 · 로그인 잠금과 같은 128 MB allkeys-lru Redis — 105 · 159 와 같은 길(159 는 ‘없는 코드’만 막음) 코드 · Redis 키 확인 대상 기업일 때만 저장(storeIf). 전후 키 수 측정
DEP-3 Medium Dependabot 의존성 그래프에 maven 0개(npm 507 · actions 8) — Gradle 은 정적 분석이 안 되어 JVM 취약점 경고가 오지 않는다. Trivy 게이트는 CRITICAL 만이라 DEP-1 · 2 가 조용히 남았다 GitHub SBOM CI 에 gradle/actions/dependency-submission 잡(그 잡만 contents: write). 함께: permissions: contents: read 명시, gitleaks 이미지 고정, Dependabot docker-compose 추가
DEP-4 Medium 테스트는 PG 16.11 · GEOS 3.11, 운영은 PG 18.6 — ST_CoverageSimplify(GEOS 3.12+) 경로는 CI 에서 한 번도 돌지 않음. -Dtest.pg.image 는 테스트 JVM 에 전달되지 않아 바꿔 돌릴 수도 없다 에이전트가 PG18 로 313/313 통과 확인 테스트 기본 이미지를 PG 18 + PostGIS 3.6(arm64 · amd64, 현재 갱신되는 태그)으로, 속성 전달 고침. 로컬 개발 DB 는 그대로(§7)
SEC-17 Low IPv6 는 주소 전체(/128)로 레이트리밋 · 로그인 잠금 키를 만든다 — /64 하나를 가진 쪽은 주소를 바꿔 한도를 비껴 가고, 주소마다 Redis 키가 생김. CloudFront 는 IPv6 가 기본으로 켜짐 코드 IPv6 는 /64 로 묶어 키를 만든다
SEC-18 Low 익명 배치 요청 목록의 message(worker 예외 메시지 원문 — DB 호스트 등이 들어갈 수 있음)를 가리지 않는다. 110 은 exitMessage 만 첫 줄로 줄였음 코드 익명이면 message 도 같은 규칙으로
SEC-19 Low afterMigrate.sql 의 DO 블록 본문에 런타임 DB 비밀번호가 평문으로 들어간다 — 블록이 실패하면(의도된 기동 중단 경로 포함) PostgreSQL 이 문장 전체를 서버 로그에 남김(RDS 로그는 세 프로젝트 공용) 코드 · log_min_error_statement=error 평문 대신 앱이 계산한 SCRAM-SHA-256 검증값을 넘김(PostgreSQL 이 그대로 저장)
SEC-20 Low AWS 에서 COOKIE_SECURE 가 false 로 풀릴 수 있음 — 배포 안내대로 make env 하면 .env.example 의 COOKIE_SECURE=false 가 복사되고, compose 의 :-true 기본값은 값이 있으면 쓰이지 않음 compose config 로 확인 compose.aws.yml 에 "true" 고정
DEP-5 Low redis 컨테이너가 root 로 돎(command: sh -c 가 공식 entrypoint 의 권한 내림을 건너뜀). AWS 도 같음 docker compose top user: 지정
DEP-6 Low web 이미지가 Debian 12(bookworm) — 정규 보안 지원은 2026-07 종료, 지금은 LTS 단계 debian.org trixie 기반으로

2.2 데이터 정합성 · 신뢰성

ID 심각도 문제 확인 해결
DL-20 Medium Job 이 도는 동안 DB 가 1분 넘게 끊기면 실행 요청 행이 RUNNING 으로 남아 그 Job 을 다시 요청할 수 없다(409, 취소 API 없음, worker 재시작 전까지). awaitFinished · finished() 의 예외가 executor Future 에 갇혀 로그도 없고, 고아 정리는 하트비트가 끊긴 worker 만 본다 코드 경로 확인 정리 주기에서 JobRepository 기준으로 맞춤(실행이 끝났거나 이 worker 가 들고 있지 않은 RUNNING 요청은 마무리), finished() 재시도, executor 예외 로그. 장애 주입 회귀 테스트
DL-21 Medium corpCodeSyncJob 이 적재 Step 에서 실패한 뒤 컨테이너가 바뀌면(AWS 는 매일 18:00 정지) restart 가 영원히 실패 — 내려받은 XML 이 tmpfs(/tmp)에서 사라졌는데 완료된 다운로드 Step 은 다시 돌지 않음 코드 · SB 6.0.5 동작 확인 다운로드 Step 을 restart 때도 돌게 하고 파일이 없으면 다시 받음(적재는 처음부터 — 5초). 회귀 테스트
DL-22 Medium 배치 화면의 ‘멈춘 실행 정리’가 하트비트가 살아 있는 실행도 확인 없이 FAILED 로 만든다 — 이어서 ‘이어서 실행’을 누르면 같은 JobInstance 가 두 스레드에서 돎(VERIFICATION 102 와 같은 위험). 버튼은 실행 중이면 늘 활성, recoverExecution 은 stale 여부를 보지 않음 코드 결정 D1
DL-23 Medium 관리자가 SEED 매핑 규칙과 같은 규칙을 추가하면 ON CONFLICT … DO UPDATE 가 SEED 행의 우선순위를 바꾸고 201 과 SEED 행 ID 를 돌려줌(그 ID 를 지우면 400, 다음 기동에 SEED 가 조용히 되돌림). sj_div 가 NULL 인 규칙(32개 중 5개)은 유니크가 NULL 을 구별해 같은 규칙이 중복으로 쌓임 코드. 현재 중복 0건(SELECT) 이미 있으면 SEED 는 400, USER 는 우선순위만. 유니크를 NULLS NOT DISTINCT 로(새 마이그레이션, 중복 0건이라 데이터 변경 없음)
DL-24 Medium 규칙(임계값 · 사용 여부)을 바꿔도 캐시를 비우지 않아 성적표 · 대시보드 요약이 최대 30분 옛 규칙으로 나감(성적표는 현재 규칙으로 다시 평가하는 화면). 매핑 규칙 추가 뒤 근거 계보의 reproduced 도 10분 옛 값. 무효화가 컨트롤러 몇 곳에 관례로만 흩어져 있음 코드 쓰는 app 서비스에서 커밋 뒤 무효화. IT
DL-25 Medium 같은 기업 화면에서 ‘위험 지역 수주 비중’이 셋으로 갈림 — 경보 R-X01 51.58%(최근 365일, 규칙 파라미터), 노출 화면 51.58%(365일 · 5 고정), 수주 · 보증 탭 카드 42.75%(화면이 직접 계산 — 기간 없음, 임계값 5 고정). 규칙 파라미터를 바꾸면 경보와 노출 화면도 갈라짐 실데이터 GET(00150828 · 00186939) 결정 D2
DL-26 Low api 와 batch 가 동시에 마이그레이션하면 afterMigrate 의 ALTER ROLE · GRANT 가 tuple concurrently updated 로 실패 — 진 쪽이 뜨지 않음(데이터 손실 없음). Flyway 는 콜백을 잠금 밖에서 돌림 실험: PG18 120회 중 50회 DO 블록에 pg_advisory_xact_lock. 동시 migrate IT
DL-27 Low 자본이 정확히 0 일 때 PF 지표(SQL)는 NEG_EQUITY, 재무 지표(Java)는 ZERO_DENOM — 경보에는 영향 없고 화면 · CSV · 근거 계보의 상태만 다름 코드 SQL 을 Java 와 맞춤 + 두 계산의 상태 분류 비교 테스트

2.3 구조 · 성능

ID 심각도 문제 해결
ARC-1 Low ‘상태를 바꾸는 SQL 은 기능 data 에만’(ADR-029) 검사가 기능 패키지만 본다 — adapters/seed/SeedLoader 가 기능 테이블 5개에 직접 쓰고, risk.rule · ref.account_map 의 INSERT 가 두 곳에 따로 있다(DL-23 의 원인) 검사를 모든 패키지로 넓히고 ops 테이블 쓰기는 명시 허용 목록. SeedLoader 의 쓰기를 각 기능 저장소로(리팩터링, 동작 같음)
ARC-2 Low 배치 조회 응답이 Map<String,Object> 이고 익명 가림이 SQL 별칭 문자열로 동작 — 별칭 하나만 바꿔도 사람 · 호스트 값이 다시 노출(110 수정이 깨지기 쉬움), OpenAPI 스키마도 없음 응답 모양을 먼저 테스트로 고정 → record 로 바꾸고 withoutActor()(RuleView · AlertDetail 과 같은 방식)
PERF-1 Low /batch/jobs 가 Job 14개마다 따로 조회(호출 1회에 테이블 스캔 약 240회 · 70~100 ms) — 익명으로 부를 수 있고 배치 화면이 3초마다 부름 한 문장(DISTINCT ON)으로. 전후 측정해 좋아질 때만
PERF-2 Low 캐시 무효화 직후 같은 무거운 계산(성적표 0.4~0.7 s)을 동시 요청이 각자 다시 함 — AWS 풀 4 에서 연결을 나눠 먹음 프로세스 안 키별 단일 계산. 측정해 좋아질 때만

2.4 프론트엔드

ID 심각도 문제 해결
FE-12 Medium 오류 본문에 message 가 없으면 res.statusText 를 쓰는데 HTTP/2(CloudFront)에서는 빈 문자열 — 공개 데모의 /login 실패(앞단 403)가 아무 안내 없이 비밀번호 칸만 비워지고, 502 · 504 는 ‘ERROR’ 만 보임 메시지 대체 순서(message → error → HTTP 상태), 로그인 실패 기본 문구, 데모면 /login 에 ‘조회 전용’ 안내
FE-13 Low 배치 화면의 시각(heartbeat · lastSeenAt · 시작/끝)이 시간대 없이 오는데 dt() 가 브라우저 현지 시각으로 읽음 — KST 밖에서 9시간 어긋남(143 의 빈틈) 시간대 없는 ISO 는 KST 로 해석 + 단위 테스트
FE-14 Low 조회가 실패하면 오류 상자 아래 ‘불러오는 중’이 끝나지 않음 — 규칙 · 감사 · 매핑 · 주가 반응 · 성적표 · 알림 등 8곳(163 · 167 의 나머지) 공용 상태 표시 하나로(Hook 이 loading/error/ready 를 줌)
FE-15 Low 실행 중인 Job 의 상세를 열어 두면 목록은 ‘완료’로 바뀌는데 상세는 그때 숫자에 멈춤 폴링에 선택한 실행 상세 포함
FE-16 Low 검색어를 바꾸고 200 ms 안에 Enter(한글 IME 는 Enter 로 마지막 글자를 확정)를 누르면 이전 검색어의 첫 결과로 이동 결과가 지금 입력값의 것일 때만 이동
FE-17 Low ‘안 읽음’ 2쪽의 마지막 1건을 읽음 처리하면 ‘읽지 않은 알림이 없습니다’(머리글은 30건)가 보이고 쪽 넘김이 사라짐. 범위 밖 ?p= 이면 기업 목록이 ‘배치를 실행하세요’ 쪽 번호를 범위 안으로(순수 함수 + 테스트), 쪽 넘김은 빈 상태 밖에
FE-18 Low 경보 목록 행이 <tr tabIndex onClick> — 화면 읽기 프로그램이 고를 수 있는 항목으로 알지 못함(147 과 같은 유형) 147 처럼 버튼/링크 + 선택 상태
FE-19 Low 규칙 편집기를 연 채 로그아웃하면 익명에게 ‘새 버전으로 저장’이 남음(누르면 401) 편집기는 ADMIN 일 때만
FE-20 Low ESLint 경계 규칙이 상대 경로 import(../api/client)와 window.fetch · globalThis.fetch 를 막지 못함, 근거 계보 JSON 주소가 화면에 하드코딩 경로 기준 규칙 + 속성 금지, alertApi 로 옮김

3. 결정이 필요한 것

ID 질문 권장 대안 · 영향
D1 (DL-22) 살아 있는 실행에 ‘정리’를 누르면? 하트비트가 살아 있으면 409 로 거절하고 ‘멈춤’을 안내. 화면은 stale 일 때만 버튼을 보임. 정말 멈춘 실행은 하트비트가 10분 뒤 stale 이 되어 정리됨 v1 호환 정책상 관리자 API POST /batch/executions/{id}/recover 의 의미가 바뀜(지금은 늘 정리). 사용처는 화면뿐. 대안: force=true 일 때만 지금처럼 / 화면에 확인 창만 두고 API 는 그대로(위험 남음)
D2 (DL-25) 수주 · 보증 탭의 비중 카드는 무엇을 보여야 하나? 경보 R-X01 과 같은 숫자 — 서버가 최신 R-X01 파라미터(기간 · 임계값)로 계산해 내려 주고, 카드에 ‘최근 365일 · R-X01 기준’을 적음. 노출 화면 기본값도 R-X01 파라미터에서. 계약 수 카드는 지금처럼 전체 카드 숫자가 바뀜(예 00150828: 42.75% → 51.58%). 응답에는 필드만 더함(v1 호환). 대안: 지금 숫자를 유지하고 ‘전체 기간’이라고만 표시

답변(2026-10-11): 계획 승인 · D1 = 살아 있는 실행은 409 로 거절 · D2 = 경보 R-X01 과 같은 숫자.

4. 실행 계획

브랜치: improve/review-2026-10-11(main 491a95d 에서). 이번 요청대로 별도 브랜치에서 커밋하고(프로젝트 규칙은 main 직접 커밋), 끝나면 보고만 한다. main 병합 · push · AWS 서버 반영은 요청하실 때 한다(서버 반영 명령은 그때 buildrisk 폴더만 대상으로 준비).

단계 내용 커밋
P0 준비 브랜치, 테스트 강제 재실행으로 기준선, §6 의 측정 기준값 —
P1 테스트 기반 DEP-4: 테스트 DB 를 운영과 같은 PG 18 로 · 속성 전달. PG18 에서만 드러나는 실패가 있으면 그 자체를 문제로 기록 test 1
P2 보안 SEC-15 → DEP-1 → DEP-2 → SEC-16 → SEC-17 → SEC-18 → SEC-19 → SEC-20 → DEP-3 항목마다 fix/build 1
P3 데이터 · 신뢰성 DL-20 → DL-21 → DL-22(D1) → DL-23 → DL-24 → DL-25(D2) → DL-26 → DL-27 항목마다 fix 1 (재현 테스트가 먼저 실패하는 것을 확인)
P4 구조 · 성능 ARC-1(검사 → 리팩터링) → ARC-2(모양 고정 테스트 → 리팩터링) → PERF-1 → PERF-2 refactor · perf 를 따로, perf 는 수치가 좋아질 때만
P5 프론트엔드 FE-12 ~ FE-20. 순수 로직은 단위 테스트 먼저, 화면 흐름은 E2E fix 와 refactor 를 따로
P6 버전 · 도구 DEP-5 · DEP-6 · ESLint 10(개발 의존성, 지원 종료 — 호환되지 않으면 보류) · k6 0.55 → 현행(측정이 끝난 뒤, 스크립트 호환 확인) build/chore 1씩
P7 마무리 같은 날 다시 측정 → 별도 에이전트가 전체 diff 를 처음 보는 리뷰어로 검토 → 정확성 · 요구사항 문제만 수정 → 문서 → 최종 보고 docs 1~2

문서: VERIFICATION 172 부터 이어 씀. 새 ADR 없이 기존 ADR 에 덧붙임 — 012(요청 마무리 · 실행 정리), 013(경로 정규화 · IPv6 키), 014 · 031(afterMigrate 잠금 · SCRAM · 테스트 PG18), 022(규칙 변경 무효화 · 대상 기업만 저장), 027(D1 · D2 의 v1 변경 기록), 029(쓰기 SQL 검사 범위), 030(ESLint 경계). README 의 숫자 · CLAUDE.md 의 함정(gitleaks 고정 명령, 테스트 DB 버전)을 맞춘다.

5. 커밋 · 검증 규칙

6. 측정 계획 (전후 같은 날, 같은 조건 — 다른 스택이 함께 돌면 적음)

대상 방법 채택 기준
SEC-16 캐시 키 비대상 기업 코드 1,000개 GET 전후 Redis 키 수 · used_memory 늘어나는 키 0
PERF-1 /batch/jobs 호출 1회당 테이블 스캔 수(pg_stat_user_tables 차이) · 100회 지연 중앙값 / p95 둘 다 줄어야 채택
PERF-2 동시 계산 무효화 직후 동시 20요청 — 계산 횟수 · p95 · 풀 대기(hikaricp_connections_pending), api 풀 4(AWS 와 같게) 계산 1회 · p95 감소일 때만
전체 회귀 make load(k6 50 VU · 90초) 전후 처리량 · p95 · 오류율 회귀 없음
웹 next build 라우트별 JS(참고) 회귀 없음

7. 하지 않는 것 (이유)

9. 결과 (2026-10-11)

모두 브랜치 improve/review-2026-10-11 에 커밋했다(main 병합 · push · AWS 반영은 요청 시). 문제 번호는 VERIFICATION 8차.

ID 결과 문제
SEC-15 · DEP-1 · DEP-2 · SEC-16 · SEC-17 · SEC-18 · SEC-19 고침 172 · 173 · 174 · 175 · 176 · 177 · 178
SEC-20 고쳤다가 되돌림 — AWS 의 관리자 로그인이 http://localhost(포트 포워딩)라 Secure 고정이 로그인을 깰 수 있음. ADR-031 에 기록 179
DEP-3 · DEP-4 고침(의존성 그래프 제출은 push 뒤 확인할 일) 180 · 181
DL-20 ~ DL-27 고침(D1 · D2 반영) 182 ~ 189
(작업 중 발견) 비상장 기업 주가 404 고침 190
ARC-1 고침(검사 + 리팩터링) 191
ARC-2 방식을 바꿔 고침 — record 대신 ‘보일 키’ 허용 목록(fail-closed). record 는 시각 표기가 바뀌어 동작을 같게 둘 수 없었음 192
PERF-1 · PERF-2 측정해 좋아져 채택 193 · 194
FE-12 ~ FE-20 고침 195 ~ 203
DEP-5 · DEP-6 · k6 고침 204 · 205 · 206
(독립 검토) 상세 가림 · 노출 화면 · CSV 기본값 · ESLint 경계 · seed 경로 잠금 · 없는 실행 기다림 고침(High 0 · Medium 2 · Low 3 — 모두 재현 테스트 먼저) 207 ~ 211
ESLint 10 보류 — eslint-config-next 가 쓰는 플러그인 셋(import · jsx-a11y · react)이 최신판에서도 ESLint 10 을 지원 범위에 넣지 않음 —