AI
글 45편
-
초당 85줄 로그가 그날의 매도 체결 기록을 지웠다
문제: 로그가 너무 많아서 로그가 사라졌다 자동매매 전략의 on_candle 함수는 종목마다, 매 틱마다 호출된다. 여기에 MDD(최대 낙폭) 차단이 걸릴 때마다 logger.info로 로그를 무조건 찍는 코드가 있었다. “차단 중”이라는 상태 하나를 매 틱마다 다시 알리는 셈이다...
-
ddl-auto: update의 빈틈을 보완 DDL 자동화로 메우기
Flyway를 걷어낸 뒤 남은 숙제 앞서 Flyway를 도입하고 3주 만에 걷어낸 이야기를 썼다. 걷어낸 뒤 스키마는 JPA ddl-auto와, 사람이 손으로 적용하는 SQL 파일 묶음(db/manual/*.sql)으로 관리해 왔다. 여기엔 두 가지 구멍이 있었다. stg 환경은 ...
-
종목마다 따로 놀던 최고점이 멀쩡한 계좌를 손절 모드로 밀어넣었다
문제: 손실이 없는데 손절 모드였다 자동매매 시스템에 equity_peak이라는 값이 있다. 계좌 자산이 지금까지 찍은 최고점을 기록해두고, 여기서 얼마나 떨어졌는지(MDD, Max Drawdown)를 계산해서 일정 이상 빠지면 신규 매수를 막는 용도다. 그런데 실제로는 이 값이 ...
-
체결 47건을 역산해서야 드러난 4.1배짜리 수수료 오차
시작은 작은 지적이었다 손익 화면에서 수수료와 세금이 하나의 값으로 합쳐져 표시되고 있다는 지적에서 이 작업이 시작됐다. 단순히 “두 값을 나눠서 보여주면 되겠지”라고 생각했는데, 확인해보니 합산 구조만 문제가 아니었다. 요율 자체가 틀려 있었다. 가정으로 두지 않고, 실제 체결 ...
-
설정 버그를 고쳤더니, 그다음엔 'ML을 켜는 게 맞나'라는 질문이 남았다
발단: 단순한 설정 누락 docker-compose.yml의 environment 항목은 어떤 환경변수를 컨테이너에 넘길지 명시적으로 나열하는 방식이다. 그런데 이 목록에 HYBRID_ENABLE_ML이 빠져 있었다. .env 파일에 값을 뭐라고 적어두든 컨테이너 안까지 전달되지 ...
-
죽은 파일이 살아있는 파일보다 최신이었다
감사의 목적은 번들 축소가 아니었다 죽은 코드 감사를 시작한 이유는 흔한 것 — 번들 크기나 파일 수 — 이 아니었다. “죽은 코드를 살아있다고 착각하고 거기에 작업하는 사고”를 막는 것이 목적이었다. 계기는 그 사고가 이미 여러 번 일어났기 때문이다. 조사해보니 2주 동안 같은 ...
-
같은 버그를 두 번 고친 날, V1/V2 이중 스택을 정리하기로 했다
계기: 같은 버그를 양쪽에서 따로 고쳤다 한 화면에 V1과 V2 두 벌의 구현이 공존하고 있었다. 신규 작성 화면은 V2를 쓰고, 월 업데이트 화면은 V1을 쓰는 식이었다. 카테고리 상태가 어긋나는(desync) 버그가 나왔을 때, 이걸 V1 쪽에서 고치고 며칠 뒤 V2 쪽에서 또...
-
매수 신호는 나오는데 체결이 0건 — 감으로 안 고치고 필터를 채점했다
문제: 신호는 나오는데 거래가 없다 2026년 7월 20~21일 라이브 로그를 보니 전략은 계속 BUY 신호를 내고 있는데, 실제 매수 체결은 하루종일 0건이었다. 신호 자체는 살아있는데 그게 전부 걸러지고 있다는 뜻이었다. 감으로 고치지 않고, 필터를 채점했다 어떤 필터가 범인인...
-
엑셀 라이브러리가 두 개였다 — xlsx에서 exceljs로 통일하기
문제 한 프로젝트 안에서 엑셀을 내보내는 경로가 두 개였고, 각각 다른 라이브러리를 쓰고 있었다. 경로 라이브러리 헤더 스타일 AG Grid export xlsx 불가 리포트 export exceljs 적용됨 xlsx(SheetJS)의 커뮤니티 버전은 셀 스타일을 지원하지 않는다....
-
AI 코드리뷰 봇 2종을 파이프라인에 넣고, 봇이 못 잡는 걸 배웠다
배경 한 프론트엔드 저장소에 AI 코드리뷰를 두 겹으로 붙였다. PR에 자동으로 달리는 Gemini 리뷰 봇, 그리고 작업 중간에 명시적으로 태우는 code-review 패스다. 목표는 “지적을 받는 것” 자체가 아니라, 지적사항을 실제 커밋으로 환류시키는 것이었다. 두 달 정도 ...
-
소프트 삭제 전환과 @Filter가 새는 곳
엔티티 14개를 하드 삭제에서 소프트 삭제로 보고서에서 참조하는 데이터가 삭제되면 과거 보고서가 깨진다. 그래서 삭제를 is_delete 플래그로 바꾸는 작업을 했다(PR #792). 개념은 단순한데 실제로 걸린 건 두 곳이었다 — 유니크 제약과 @Filter가 적용되지 않는 경로...
-
동기 30초 타임아웃을 webhook 비동기로
30초로는 부족했다 문서를 OCR로 파싱하는 기능이 있다. 내부 OCR 서비스에 HTTP로 요청하고 결과를 받아 응답하는 단순한 동기 구조였다. 두 가지가 겹쳐 30초 타임아웃에 계속 걸렸다. PaddleOCR 콜드스타트 — 모델 로딩에 시간이 걸린다. 한동안 요청이 없으면 첫 요...
-
PDF 다운로드가 7초 걸렸는데, 범인은 한글 폰트였다
문제 제품 배출량을 산정하는 B2B 웹앱에서 결과 리포트를 PDF로 내려받는 기능이 있었다. 버튼을 누르면 약 7초가 걸렸다. 그동안 화면은 로딩만 돌고 아무 일도 일어나지 않는다. 사용자가 “먹통이 됐나” 하고 다시 누르기 충분한 시간이다. 구현은 @react-pdf/render...
-
AI 에이전트 72개로 메모리 누수를 감사하고, 그 결과를 다시 검증했다
문제 오래 켜두는 SPA에서 메모리 누수는 재현이 어렵다. 특정 화면을 열고 닫기를 수십 번 반복해야 드러나고, 그때는 이미 어느 컴포넌트 탓인지 특정하기 어렵다. 그렇다고 수백 개 컴포넌트를 사람이 하나씩 훑는 건 현실적이지 않다. 그래서 접근을 바꿨다. 병렬 에이전트 72개를 ...
-
RTK Query 워터폴을 평탄화하고, 캐시를 켜자 stale이 드러났다
문제 대시보드 탭 하나를 여는 데 요청이 직렬로 줄줄이 이어졌다. summary 조회 → 첫 제품 자동 선택 → 해당 제품의 LCA 선택 → 상세 데이터 호출 각 단계가 앞 단계의 응답을 기다린 뒤에야 시작된다. 전형적인 워터폴이다. 게다가 이 API 그룹은 keepUnusedDa...
-
N+1 제거 4종 세트 — fetch join이 답이 아닐 때
N+1은 하나의 문제가 아니다 한 달 반 동안 대시보드 조회 성능을 고치면서 N+1을 여러 번 만났다. 매번 처방이 달랐다. “N+1이면 fetch join”이라는 반사가 통하지 않는 경우가 절반 이상이었다. (a) fetch join을 못 쓸 때 — @BatchSize f6fd2...
-
뷰를 물질화 테이블로 — 읽기 비용을 쓰기 시점으로 옮기기
읽을 때마다 5개 CTE를 풀스캔하고 있었다 대시보드 조회에 쓰이는 엔티티 하나가 @Subselect로 매핑된 뷰였다. CTE 5개를 엮은 집계 뷰인데, MySQL은 이 뷰를 실체화해두지 않으므로 조회할 때마다 전체를 다시 계산한다. 화면을 열 때마다 풀스캔이 도는 셈이다. 데이터...
-
AG Grid의 autoHeight는 행 가상화를 끈다
문제 입출력 데이터를 입력하는 그리드 화면이 있었다. 행이 수백 개인 상황에서 셀 하나를 편집할 때마다 눈에 띄게 버벅였다. 값을 타이핑하는 속도를 화면이 못 따라오는 수준이었다. 원인 1: 편집할 때마다 columnDefs가 통째로 재생성됐다 columnDefs를 useMemo로...
-
readOnly 트랜잭션에서 write를 잡아내는 테스트 가드레일
조회 API가 매번 write 트랜잭션을 만들고 있었다 대시보드 조회 API의 응답이 느려서 들여다봤는데, 조회 경로에서 write가 일어나고 있었다. f6fd21ef에서 확인한 내용이다 — eco-view의 배출원 조회 경로가 lcaStepService.updateStepOnly...
-
AI가 쓴 커밋 메시지가 거짓말을 했다
배경: 커밋의 절반이 AI 협업이다 같은 백엔드를 공유하는 쌍둥이 Next.js 프론트엔드를 한 달 남짓 집중적으로 만졌다. 두 저장소의 커밋 접두사는 압도적으로 fix다. 통합테스트와 QA 대응 국면이었다. 그 기간의 AI 협업 표기를 세어보면 이렇다. 관리자 포털 사용자 포...
-
Flyway를 도입하고 3주 만에 걷어낸 이야기
도입과 철수 사이 22일 2026년 5월 13일 b832f20d로 Flyway를 처음 도입했고, 6월 4일 9e3a7aaa로 걷어냈다. 그 사이 22일 동안 FlywayConfig는 221줄까지 자라났다가 통째로 삭제됐다. 성공담이 아니라 철수 회고다. 왜 도입했나 이 프로젝트는 ...
-
JWT를 걷어내고 Spring Session으로 — 그 뒤에 온 것들
전환 자체는 하루였다 a1837d49(PR #557)에서 JWT 기반 인증을 Redis 기반 Spring Session으로 바꿨다. JwtFilter, JwtProvider 등 클래스 6개 374줄이 삭제됐다. 커밋 하나로 끝난 작업이다. 정작 내용이 있는 건 그다음 3주였다. 세...
-
kubectl exec은 왜 멈추는가 — 셸 스크립트에 타임아웃과 재시도 넣기
문제: 스크립트가 멈춘다 이 프로젝트에서 Fabric 네트워크를 올리는 일은 결국 bash 스크립트가 kubectl을 순서대로 호출하는 것이다. 그런데 이 스크립트가 간헐적으로 멈췄다. 에러가 나는 게 아니라 그냥 멈춰 있는다. 폐쇄망에 kind 다중 클러스터 환경이라 네트워크 계...
-
TanStack Query에서 로그아웃은 토큰만 지우는 게 아니다
문제: 로그아웃했는데 이전 사용자 데이터가 보인다 같은 백엔드를 쓰는 쌍둥이 Next.js 프론트엔드 — 관리자 포털과 사용자 포털이다. 두 앱 모두 인증은 Zustand, 서버 상태는 TanStack Query로 나눠 관리한다. 이 분리 자체는 흔하고 합리적인 구성이다. 문제는 ...
-
비동기 API가 무조건 200을 반환하고 있었다
문제: “삭제 성공”이라는 거짓말 블록체인 네트워크를 만드는 데는 몇 분이 걸린다. Kubernetes에 리소스를 올리고 pod가 뜨기를 기다리고 채널을 만들고 peer를 조인시킨다. 그래서 생성도 삭제도 비동기로 처리한다. 컨트롤러는 작업을 등록만 하고 바로 응답을 준다. 문제는...
-
TOCTOU 한 건을 고치는 대신 버그 클래스를 없앴다
문제: SFTP 업로드가 동시에 들어오면 터졌다 이 프로젝트는 사용자가 웹에서 버튼을 누르면 Hyperledger Fabric·Besu 네트워크를 Kubernetes 클러스터에 올려주는 플랫폼이다. 인프라 계층은 JSch로 원격 서버에 SSH/SFTP 접속해 bash 스크립트를 밀...
-
localStorage에 남은 isAuthenticated: true가 로그인을 막았다
문제: 로그인 페이지가 “로딩 중…“에서 안 넘어간다 사용자 포털에서 올라온 버그다. 한동안 안 쓰다가 다시 들어오면 로그인 화면이 “로딩 중…“만 띄운 채 멈춘다. 폼이 안 나오니 로그인할 방법이 없다. localStorage를 비우면 정상으로 돌아왔다. localStorage를...
-
오픈소스 오퍼레이터 버그를 CRD 스키마 패치로 우회하기
문제: 체인코드 상태가 FAILED에서 안 나온다 이 플랫폼에서 체인코드를 배포하면 Kubernetes에 FabricChaincode라는 커스텀 리소스가 만들어진다. hlf-operator가 이 리소스를 보고 실제 체인코드 pod를 띄운다. 그런데 배포가 실제로는 잘 됐는데도 ku...
-
invalidateQueries를 refetchQueries로 바꿨더니
문제: mutation 후 목록이 즉시 안 바뀐다 관리자 포털과 사용자 포털 양쪽에서 같은 QA 피드백이 반복됐다. 항목을 삭제하거나 생성했는데 목록이 그대로다. 탭을 옮겼다 오거나 새로고침하면 그제야 반영된다. mutation 성공 후 invalidateQueries를 호출하고 ...
-
인증 필터가 매 요청 DB를 네 번 때리고 있었다
문제: TPS가 안 나온다 부하 테스트에서 목표 TPS에 못 미쳤다. 애플리케이션 로직 자체는 무겁지 않았다. 그래서 비즈니스 코드가 아니라 모든 요청이 반드시 지나가는 경로를 먼저 뜯어봤다. 이 프로젝트는 Spring Security + JWT 구성이다. 요청 하나가 컨트롤러에 ...
-
게이트웨이가 죽었는데 사용자가 F5를 누른다 — 브라우저 서킷 브레이커
문제: 새로고침이 서버를 한 번 더 때린다 같은 백엔드를 공유하는 쌍둥이 Next.js 프론트엔드가 있다. 관리자 포털과 사용자 포털. 게이트웨이가 502/503/504를 뱉으면 두 앱 모두 전역 오버레이를 띄워 “잠시 후 다시 시도해 주세요”라고 안내하고 있었다. 문제는 그 다음...
-
navigator.clipboard는 HTTPS에서만 동작한다
문제: 복사 버튼이 아무 반응이 없다 관리자 포털의 대시보드 화면에는 토큰을 복사하는 버튼이 있다. 로컬에서는 잘 됐다. 배포 환경에 올리니 버튼을 눌러도 아무 일도 일어나지 않았다. 에러 토스트도 없고 콘솔에도 아무것도 안 찍혔다. 원인은 코드가 아니라 배포 환경이다. 이 시스템...
-
탭 상태를 useState에 두지 마라 — URL 파생 상태로 바꾸기
문제: 사이드 메뉴로 이동하면 탭이 안 바뀐다 관리자 포털의 여러 목록 화면이 탭을 갖고 있다. 탭 상태는 ?tab= 쿼리 파라미터에 실려 있어서 링크 공유와 새로고침이 된다. 이걸 담당하는 게 useTabQuery 훅이었다. QA에서 올라온 증상은 이랬다. 사이드 메뉴에 ?tab...
-
사이드바 권한 분기 100줄을 지우고 ICON_MAP만 남기다
문제: 사이드바가 권한을 안다 사용자 포털의 사이드 메뉴는 상수 배열로 하드코딩돼 있었다. 두 그룹의 메뉴가 파일 안에 박혀 있고, 각 항목에 permission 문자열이 붙어 있다. const OAUTH_MENU_ITEMS = { IAM: { label: "IAM 관리", ico...
-
401을 성공으로 취급하는 헬스체크 — 배포 스크립트를 서브커맨드 오케스트레이터로 재설계
훅: 헬스체크가 401을 실패로 보고 있었다 배포 스크립트에 헬스체크가 있었다. 앱을 재시작하고 나서 /monitor/health를 찔러 응답이 오면 성공으로 본다. until "${SSH_CMD[@]}" "$REMOTE_USER@$REMOTE_HOST" \ "curl -fsS '...
-
AI에게 스펙을 쓰게 하고, 자기검토를 시키고, 그 다음 구현하기
커밋 히스토리가 곧 작업 기록이다 이 프로젝트에서 넉 달간 내가 남긴 커밋은 201건이다. 그중 185건, 92%에 AI 도구 트레일러가 붙어 있다. 트레일러 건수 Made-with: Cursor 147 Co-Authored-By: Claude Sonnet 4.6 25 Co-Aut...
-
엑셀 업로드에서 이름이 모호할 때 사용자에게 되묻기
문제 엑셀 파일로 장비 수천 행을 조직에 일괄 배정하는 기능이다. 사용자는 시트에 장비번호와 조직명을 적어 올린다. 서버는 조직명으로 조직을 찾아 매핑한다. 여기서 조직명이 유일하지 않다. 조직 트리에서 서로 다른 부모 아래에 같은 이름의 하위 조직이 있을 수 있다. 사용자가 "1...
-
정규화된 조인을 버리고 역정규화하기 — 백필 마이그레이션 포함
문제 장비 A가 어느 조직에 속하는지 알아내려면 두 단계 조인이 필요했다. A ──(point_sq)──> B ──(매핑 테이블)──> 조직 A는 조직을 직접 갖지 않는다. A는 고객 B에 붙어 있고, B가 조직 매핑 테이블을 통해 조직에 연결된다. 정규화 관점에서는 정...
-
권한 체크를 세션에 캐싱하고, 무효화 지점을 찾기
문제 권한 모델이 user_level 정수 하나였다. 숫자가 클수록 많은 걸 할 수 있다는 식이다. 이 모델은 요구가 단순할 때만 버틴다. “이 사람은 조회는 되는데 삭제는 안 되고, 대신 다른 화면에서는 승인 권한이 있다” 같은 요구가 들어오는 순간 정수 하나로는 표현이 안 된다...
-
GIN trigram 인덱스를 만들었는데 왜 안 빨라지지?
인덱스를 만든 날과 인덱스가 실제로 쓰인 날이 13일 떨어져 있다 2026년 3월 12일 커밋 361bc9e에서 GIN trigram 인덱스를 넣었다. 그런데 그 인덱스가 실제로 쿼리 플랜에 등장하기 시작한 건 3월 25일 커밋 4fb9cbd부터다. 그 사이 13일 동안 인덱스는 ...
-
depth < 10 방어 코드가 버그를 감추고 있었다
문제의 코드 계층형 조직 트리를 다루는 서비스에 조상 조직 ID를 전부 모아오는 메서드가 있다. 원래 구현의 핵심은 이 한 줄이었다. while (currentId != null && depth < 10) { ancestorIds.add(0, currentId)...
-
PostgreSQL 바인드 파라미터 65,535개 한도에 부딪히다
문제 엑셀로 장비 매핑 정보를 일괄 업로드하는 기능이 있다. 수백 행 정도는 잘 돌았다. 그런데 실제 현장 데이터로 수만 행을 올리자 배치 INSERT에서 터졌다. MyBatis <foreach>로 만든 멀티 로우 INSERT였다. 행이 많아질수록 SQL 하나에 붙는 바...
-
무료 프록시로 봇 차단을 버티는 크롤러 설계하기
문제: 반복 크롤링이 IP 차단에 걸리기 시작했다 골프 부킹 정보를 10분 주기로 크롤링하는 대시보드를 운영하고 있었다. 문제는 이 반복 패턴 자체가 상대 서버 입장에서는 봇으로 보이기 딱 좋다는 점이었다. 코드에는 _direct_blocked라는 플래그와 “IP 차단 시” 분기가...
-
배포 직후 55분, 핫픽스 4연발 — 문제를 네 번 다시 정의한 하루
09:41 — 배포하자마자 터졌다 프록시 로테이션 기능을 배포한 직후, 실서버에서 크롤링이 작동하지 않았다. 로컬에서는 잘 되던 기능이 배포 환경에서만 실패하는 전형적인 상황이었다. 이후 55분 동안 커밋 네 개가 연달아 올라갔다 — 문제를 하나 고칠 때마다 다음 문제가 드러나는 ...
-
MCP 란
MCP 란 Model Context Protocol (MCP)는 LLM(대형 언어 모델)과의 상호작용을 위한 프로토콜입니다. MCP는 LLM이 특정 작업을 수행하기 위해 필요한 컨텍스트를 제공하는 데 중점을 둡니다. 이 프로토콜은 LLM이 더 나은 응답을 생성할 수 있도록 돕기 위...