Backend
글 11편
-
소프트 삭제 전환과 @Filter가 새는 곳
엔티티 14개를 하드 삭제에서 소프트 삭제로 보고서에서 참조하는 데이터가 삭제되면 과거 보고서가 깨진다. 그래서 삭제를 is_delete 플래그로 바꾸는 작업을 했다(PR #792). 개념은 단순한데 실제로 걸린 건 두 곳이었다 — 유니크 제약과 @Filter가 적용되지 않는 경로...
-
동기 30초 타임아웃을 webhook 비동기로
30초로는 부족했다 문서를 OCR로 파싱하는 기능이 있다. 내부 OCR 서비스에 HTTP로 요청하고 결과를 받아 응답하는 단순한 동기 구조였다. 두 가지가 겹쳐 30초 타임아웃에 계속 걸렸다. PaddleOCR 콜드스타트 — 모델 로딩에 시간이 걸린다. 한동안 요청이 없으면 첫 요...
-
N+1 제거 4종 세트 — fetch join이 답이 아닐 때
N+1은 하나의 문제가 아니다 한 달 반 동안 대시보드 조회 성능을 고치면서 N+1을 여러 번 만났다. 매번 처방이 달랐다. “N+1이면 fetch join”이라는 반사가 통하지 않는 경우가 절반 이상이었다. (a) fetch join을 못 쓸 때 — @BatchSize f6fd2...
-
readOnly 트랜잭션에서 write를 잡아내는 테스트 가드레일
조회 API가 매번 write 트랜잭션을 만들고 있었다 대시보드 조회 API의 응답이 느려서 들여다봤는데, 조회 경로에서 write가 일어나고 있었다. f6fd21ef에서 확인한 내용이다 — eco-view의 배출원 조회 경로가 lcaStepService.updateStepOnly...
-
JWT를 걷어내고 Spring Session으로 — 그 뒤에 온 것들
전환 자체는 하루였다 a1837d49(PR #557)에서 JWT 기반 인증을 Redis 기반 Spring Session으로 바꿨다. JwtFilter, JwtProvider 등 클래스 6개 374줄이 삭제됐다. 커밋 하나로 끝난 작업이다. 정작 내용이 있는 건 그다음 3주였다. 세...
-
비동기 API가 무조건 200을 반환하고 있었다
문제: “삭제 성공”이라는 거짓말 블록체인 네트워크를 만드는 데는 몇 분이 걸린다. Kubernetes에 리소스를 올리고 pod가 뜨기를 기다리고 채널을 만들고 peer를 조인시킨다. 그래서 생성도 삭제도 비동기로 처리한다. 컨트롤러는 작업을 등록만 하고 바로 응답을 준다. 문제는...
-
TOCTOU 한 건을 고치는 대신 버그 클래스를 없앴다
문제: SFTP 업로드가 동시에 들어오면 터졌다 이 프로젝트는 사용자가 웹에서 버튼을 누르면 Hyperledger Fabric·Besu 네트워크를 Kubernetes 클러스터에 올려주는 플랫폼이다. 인프라 계층은 JSch로 원격 서버에 SSH/SFTP 접속해 bash 스크립트를 밀...
-
인증 필터가 매 요청 DB를 네 번 때리고 있었다
문제: TPS가 안 나온다 부하 테스트에서 목표 TPS에 못 미쳤다. 애플리케이션 로직 자체는 무겁지 않았다. 그래서 비즈니스 코드가 아니라 모든 요청이 반드시 지나가는 경로를 먼저 뜯어봤다. 이 프로젝트는 Spring Security + JWT 구성이다. 요청 하나가 컨트롤러에 ...
-
엑셀 업로드에서 이름이 모호할 때 사용자에게 되묻기
문제 엑셀 파일로 장비 수천 행을 조직에 일괄 배정하는 기능이다. 사용자는 시트에 장비번호와 조직명을 적어 올린다. 서버는 조직명으로 조직을 찾아 매핑한다. 여기서 조직명이 유일하지 않다. 조직 트리에서 서로 다른 부모 아래에 같은 이름의 하위 조직이 있을 수 있다. 사용자가 "1...
-
권한 체크를 세션에 캐싱하고, 무효화 지점을 찾기
문제 권한 모델이 user_level 정수 하나였다. 숫자가 클수록 많은 걸 할 수 있다는 식이다. 이 모델은 요구가 단순할 때만 버틴다. “이 사람은 조회는 되는데 삭제는 안 되고, 대신 다른 화면에서는 승인 권한이 있다” 같은 요구가 들어오는 순간 정수 하나로는 표현이 안 된다...
-
depth < 10 방어 코드가 버그를 감추고 있었다
문제의 코드 계층형 조직 트리를 다루는 서비스에 조상 조직 ID를 전부 모아오는 메서드가 있다. 원래 구현의 핵심은 이 한 줄이었다. while (currentId != null && depth < 10) { ancestorIds.add(0, currentId)...