donghakim.dev — zsh
[03]
dnghkm@donghakim.dev ~/blog $ ls -t ./blog/*.md

Blog / 블로그

백엔드 · 인프라 · 커리어에 대한 2년차의 기록

29 / 29 posts
2026.09.12·3분·backend

행이 크면 같은 버퍼풀에 50분의 1만 캐시됩니다

InnoDB는 16KB 페이지 단위로 메모리에 적재하기 때문에, 큰 원문 컬럼을 같은 테이블에 두면 버퍼풀 대부분을 원문 XML이 점유합니다.

원본 XML 을 한 컬럼에 적재하는 테이블의 보존기간을 결정하려고 용량을 계산했는데, 스토리지 비용은 보존기간을 단축할 근거가 되지 못한다는 결론이 먼저 도출됐습니다. 실제 논점은 버퍼풀이었습니다. 원문이 전체 바이트의 97.6% 이고 16KB 페이지에 한 행밖에 저장되지 않아서, 원문을 분리하면 같은 메모리로 50배 많은 행을 캐시합니다. DELETE 로는 Aurora 과금이 감소하지 않는다는 것도 같이 정리했습니다.

#aurora#buffer-pool#innodb#mysql#performance#schema-design
backend
2026.09.12·3분·backend

X-Forwarded-For 유무로 내부 전용 서비스의 외부 진입을 막았습니다

ALB 를 경유한 요청에만 추가되는 헤더를 신뢰 신호로 삼아, 서비스 안의 서블릿 필터 하나로 외부 진입을 차단했습니다

내부 호출만 수신한다고 전제한 서비스가 인터넷에 연결된 로드밸런서를 통해 도달 가능한 상태였습니다. 멱등키는 선택값이라 요청을 차단하는 수단이 아니었고, 보안그룹은 변경할 IAM 권한이 없었고, 공용 모듈 변경은 의존 서비스 전체에 파급됩니다. ALB 가 XFF 를 append 한다는 성질을 근거로 헤더 유무를 판정에 사용했고, 경고만 기록하는 관찰 모드로 먼저 배포했습니다.

#alb#aws#idempotency#security#servlet-filter#spring
backend
2026.09.12·2분·backend

Spring Security 에서 권한 목록에 없는 API 는 전부 공개되고 있었습니다

보호 목록에 접두사 하나가 추가되자 그 아래 있던 공개 API 가 경고 없이 403 을 반환하게 됐습니다

비회원도 사용해야 하는 API 가 403 을 반환하고 있었습니다. 인가 설정을 수정한 적이 없는데도 그랬습니다. 롤 목록에 없으면 마지막 permitAll 규칙에 매칭되는 구조라, 공개 여부는 선언으로 결정되지 않고 목록에 없다는 사실로만 결정됐습니다. permit 예외 규칙을 추가하는 대신 URL 을 인가 등급에 맞는 네임스페이스로 변경해, 보안 설정을 변경하지 않고 수정했습니다.

#api-design#authorization#spring-security#testing
backend
2026.09.12·3분·backend

인증번호 오입력 제한을 시간 윈도우로 세면 재발송으로 우회됩니다

재발송 시 오입력 횟수를 초기화하려면 이전 코드 폐기가 전제입니다

인증 API 에 서버측 제한을 도입하면서 재발송 시 오입력 횟수도 초기화해 달라는 요구를 받았습니다. 초기화가 안전한 근거는 재발송하면 정답이 변경된다는 것인데, 검증 쿼리가 코드 값으로 먼저 조회하고 있어서 발급한 코드가 전부 동시에 유효했습니다. 재발송할 때마다 정답이 변경되지 않고 유효한 정답 수가 증가하는 상태였습니다.

#auth#email#rate-limit#redis#security#sms
backend
2026.09.12·3분·infra

프론트 프록시를 지나면 백엔드는 클라이언트 IP 를 못 봅니다

Amplify rewrite 는 리다이렉트가 아니라 서버측 프록시이고, Spring Boot 는 ECS 를 감지하면 XFF 신뢰를 자동으로 활성화합니다

사내망 대역만 허용하는 기능이 사무실에서도 전부 거절됐습니다. 로그의 IP 는 매번 다른 AWS 주소였습니다. 원인은 두 가지였습니다. 프론트의 status 200 rewrite 가 서버측 프록시라 ALB 가 인식하는 상대는 브라우저가 아니었고, Spring Boot 는 ECS 를 감지하면 forward-headers-strategy 를 자동으로 활성화해서 XFF 의 첫 공인 IP(프록시 송신 IP)를 getRemoteAddr 에 덮어씁니다.

#amplify#aws#cors#ecs#spring-boot#troubleshooting#x-forwarded-for
infra
2026.09.06·2분·backend

트랜잭션 안에서 외부 HTTP 를 호출하면 응답을 기다리는 동안 커넥션이 점유된다

커넥션 풀 고갈, 중복검사 통과, 부분 실패가 같은 원인에서 발생합니다. 필수가 아닌 호출은 커밋 이후에 실행하도록 분리했습니다

@Transactional 메서드 안에서 외부 HTTP 를 동기 호출하면 외부가 느려진 시간만큼 DB 커넥션과 트랜잭션이 점유됩니다. 평소에는 확인되지 않다가 외부 장애 때 세 가지 문제가 발생합니다. 풀 고갈로 무관한 경로까지 실패하고, 커밋이 안 되는 동안 SELECT-then-INSERT 중복검사가 계속 통과하고, 외부 과금이 먼저 발생한 뒤 롤백되면 정합성이 훼손됩니다. 한 장애에서 세 가지가 모두 발생했고, best-effort 호출을 afterCommit 으로 분리해 수정했습니다.

#aftercommit#connection-pool#data-integrity#spring#transaction#webclient
backend
2026.09.06·1분·note

툴팁이 잘리면 툴팁이 아니라 조상의 overflow 를 본다

recharts 의 allowEscapeViewBox 와 overflow-x: auto, 두 경우 모두 조상의 클리핑 컨텍스트가 원인이었습니다

어드민에서 툴팁이 잘리는 문제를 두 번 겪었는데 원인이 같았습니다. DOM 상 툴팁이 어떤 컨테이너 안에 있으면 그 컨테이너 밖으로 나간 부분은 잘립니다. recharts 는 allowEscapeViewBox 를 활성화하는 순간 자동 반전이 비활성화돼 Card 의 overflow: hidden 에 잘렸고, CSS 말풍선은 overflow-x: auto 가 세로까지 클리핑 컨텍스트로 전환하면서 잘렸습니다.

#css#mui#react#recharts#ux
note
2026.09.06·1분·infra

surefire -Dtest 필터는 @Nested 안의 테스트를 못 찾는다

@Nested 클래스의 실제 이름이 FooTest$Bar 라서 FooTest 패턴과 매칭되지 않습니다. 패턴 끝에 와일드카드(*)를 추가합니다

CI 에서 Tests run: 0 과 함께 No tests were executed! 로 빌드가 실패했습니다. 컴파일은 됐고 target/test-classes 에 클래스 파일도 있었습니다. 모든 @Test 가 @Nested 안에 있는 테스트라서 -Dtest='FooTest' 패턴과 매칭되지 않은 것이었습니다. @Nested 의 실제 클래스명은 FooTest$Bar 입니다.

#ci#docker#junit#maven#surefire#troubleshooting
infra
2026.09.06·1분·backend

붙여넣은 토큰의 개행 때문에 fetch 가 요청을 보내기 전에 실패했다

The string did not match the expected pattern. 이라는 메시지에는 헤더도 토큰도 등장하지 않습니다

API 호출이 전부 실패하는데 에러 메시지가 The string did not match the expected pattern. 한 줄이었습니다. 네트워크 탭에는 요청이 아예 없었습니다. 브라우저가 요청을 전송하기 전에 던진 예외였고, Authorization 헤더에 추가한 토큰에 개행이 섞여 있던 게 원인이었습니다. 메시지 어디에도 헤더나 토큰이라는 말이 없어서 URL 문제로 오인하기 쉽습니다.

#auth#fetch#frontend#jwt#safari#troubleshooting
backend
2026.09.06·2분·backend

ON DUPLICATE KEY UPDATE 로 신규/중복을 구분했는데 항상 신규로 읽혔다

Connector/J 의 useAffectedRows 기본값이 false 라, 아무것도 변경하지 않은 UPDATE 도 1 을 반환합니다

외부에서 수신한 메시지를 멱등하게 적재하려고 INSERT ... ON DUPLICATE KEY UPDATE id = id 의 반환값으로 신규와 중복을 구분했습니다. 그런데 이미 있는 행에도 1 이 반환돼서 판별 결과가 항상 신규였습니다. MySQL 은 no-op UPDATE 에 0 을 반환하지만, Connector/J 가 기본값으로 CLIENT_FOUND_ROWS 를 활성화하면 서버가 변경된 행 대신 매칭된 행을 반환합니다. 예외나 경고 없이 반환값만 틀리게 나옵니다.

#duplicate-key#idempotency#jdbc#mysql#testcontainers#troubleshooting
backend
2026.09.06·3분·infra

배포 실패와 Lambda 권한 오류의 원인이 공개 번들에 인라인된 AWS 키였다

NEXT_PUBLIC_ 접두사는 브라우저에 노출해도 되는 값이라는 선언입니다. 브라우저에서 S3 를 직접 호출하는 구조가 그 접두사를 강제하고 있었습니다

CodePipeline 이 8초 만에 실패하고, 별개의 Lambda 가 IAM 권한 오류를 발생시키고, 외부에서 탈취된 키로 결제가 시도됐다는 통보가 같은 날 접수됐습니다. 세 가지가 한 원인이었습니다. 어드민의 공개 JS 번들에 AWS 액세스키가 평문으로 포함돼 있었고, AWS 가 노출된 키를 탐지해 계정 단위 제한을 적용한 것이었습니다. 브라우저에서 S3 를 직접 호출하는 구조가 NEXT_PUBLIC_ 접두사를 강제한 경위와, 카나리 값으로 배포 전에 노출을 확인하는 방법을 기술했습니다.

#aws#env#incident#nextjs#postmortem#s3#security
infra
2026.09.06·3분·backend

MAX+1 채번은 락 유무와 인덱스 유무로 세 가지 상태가 된다

FOR UPDATE 가 유일성을, 그 컬럼의 인덱스가 락 범위를 결정합니다. 하나만 확인해서는 판정이 안 됩니다

SELECT MAX(컬럼)+1 로 채번하는 코드를 검토할 때 FOR UPDATE 만 확인하고 넘어갔습니다. 그런데 락이 있어도 그 컬럼에 인덱스가 없으면 FOR UPDATE 가 스캔한 행을 전부 잠급니다. 운영 슬로우로그에는 테이블 62,134행 중 약 60,500행을 검사하고 lock time 652ms 로 기록돼 있었습니다. 락 없는 쪽은 복합 PK 라 중복이 에러 없이 통과해 187개 그룹이 누적되어 있었습니다. 두 축으로 구분되는 세 상태와, 세 상태 모두 방지하지 못하는 삭제 후 번호 재사용을 실측 수치와 함께 기록했습니다.

#index#innodb#mysql#race-condition#sequence#transaction#troubleshooting
backend
2026.09.06·2분·backend

JOIN ON col IN (상관컬럼, 상관컬럼) 은 인덱스 ref 액세스를 쓰지 못한다

EXPLAIN 에 Range checked for each record 가 표시되면 이 형태입니다. UNION ALL 두 갈래로 분리해 각각 단일 등가로 변환합니다

환불 거래를 원 거래에 매칭하면서, 티켓번호가 현재권이거나 원권 둘 중 하나와 일치하면 되니까 ON col IN (r.a, r.b) 로 묶었습니다. 인덱스가 분명히 있는데 EXPLAIN 의 key 가 NULL 이고 type 이 ALL 이었습니다. IN (컬럼, 컬럼) 은 단일 ref 등가조건으로 환원되지 않습니다. 두 갈래를 UNION ALL 로 분리해 각각 단일 등가로 변환하고 우선순위로 1건만 선택했습니다.

#explain#index#mysql#performance#query-optimization#sql
backend
2026.07.08·2분·infra

슬로우 쿼리 1위를 고치기 전에는 2위가 보이지 않는다

Aurora 슬로우 로그를 CloudWatch 비용 없이 활성화하고, 튜닝→배포→재진단을 슬로우 후보가 없어질 때까지 반복했습니다

장애의 부하원을 특정하려면 슬로우 쿼리 로그가 필요한데, CloudWatch 로그 export 는 과금 대상입니다. Aurora 자체 슬로우 로그를 파일로만 기록하고 RDS API 로 다운로드하면 추가 비용 없이 같은 로그를 확보합니다. 더 중요한 건 진단 방식이었습니다. 슬로우 시간은 소수 쿼리에 크게 집중되어서, 1위(전체의 97%)를 수정해 배포하기 전에는 2위가 로그에 확인되지 않았습니다. 튜닝하고 배포하고 로그를 다시 다운로드하는 과정을 네 번 반복해 서비스 쿼리의 슬로우 후보가 없어질 때까지 진행한 과정을 정리했습니다.

#aurora#monitoring#mysql#performance#pt-query-digest#rds#slow-query-log#troubleshooting
infra
2026.07.08·2분·infra

오픈하지 않은 기능의 호출이 트래픽의 97%를 차지해 결제가 실패했다

커넥션 풀 고갈의 원인이 풀 크기가 아닐 때가 있습니다. writer 는 1.8ms 로 정상이었고 reader CPU 가 99%였습니다

프로모션 트래픽이 집중된 날, 결제는 됐는데 예약이 안 되는 고객이 발생하기 시작했습니다. 로그에는 커넥션 풀 타임아웃이 4시간에 9,400건 기록됐습니다. 그런데 포인트 기능은 아직 오픈도 안 했는데, pay 트래픽의 97%가 포인트 조회였습니다. 프론트가 헤더 잔액 표시용으로 전 페이지에서 호출하고 있었고, 무거운 내역 쿼리가 reader CPU 를 99%로 고정시키면서 조회 하나가 커넥션을 30초씩 점유했습니다. 풀 고갈이라는 증상에서 풀 크기를 확대하는 대응이 왜 적절하지 않았는지, 원인과 결과를 어떤 지표로 추적했는지 기록했습니다.

#aurora#connection-pool#hikari#incident#payment#performance#reader#troubleshooting
infra
2026.07.08·2분·backend

회원 한 명 조회가 전체 예약 41,135건을 집계하고 있었다

WHERE 1=1 파생테이블은 바깥 필터와 무관하게 테이블 전체를 materialize 합니다. 바깥의 회원 필터를 파생테이블 안에도 추가했습니다.

마이페이지 예약목록이 회당 3.8초씩 소요되며 슬로우 로그의 97%를 점유하고 있었습니다. EXPLAIN ANALYZE 를 실행해 보니 파생테이블이 조회 회원과 무관하게 전체 예약 41,135건을 GROUP BY 하고, 회원 필터는 그 바깥에서 나중에 적용되고 있었습니다. 바깥의 동적 필터를 파생테이블 안에 그대로 추가하는 방식으로 재작성해 materialize 를 41,135행에서 87행으로 축소했고, 같은 모양의 쿼리 세 개에 같은 기법을 적용해 13배~1,400배 빨라졌습니다. 인덱스만 먼저 추가했을 때는 거의 효과가 없었습니다.

#aurora#derived-table#explain-analyze#index#mysql#performance#query-optimization#troubleshooting
backend
2026.07.08·3분·backend

트랜잭션 안의 느린 외부 호출 때문에 중복검사가 동작하지 않을 때

커밋되지 않은 INSERT 는 다음 요청의 SELECT 에 보이지 않습니다

트래픽이 집중된 날, 소셜 가입에서 한 사용자에게 계정이 4개 생성됐습니다. 가입 코드에는 중복검사가 분명히 있었고, 동시성 race 도 아니었습니다. 제출 간격은 30~40초였는데도 전부 통과했습니다. 원인은 가입 트랜잭션 안에서 호출하던 외부 API 가 58초 만에 실패하면서 트랜잭션이 80초 동안 커밋되지 않았고, 커밋된 행만 조회하는 중복검사가 그동안 계속 0건을 반환한 것이었습니다. 외부 호출을 커밋 이후로 분리해 해결한 과정과, DB UNIQUE 제약을 추가할 수 없었던 이유를 정리했습니다.

#aftercommit#connection-pool#duplicate#mysql#signup#spring#transaction#troubleshooting
backend
2026.07.05·3분·infra

항공권 조회 봇을 브라우저 증명(PoW) 토큰으로 걸러낸 이야기

조회 대비 예약 비율(look-to-book)이 기준을 초과하면 과금되는 구조라, 예약 없이 조회 수만 증가시키는 봇이 문제였습니다. PoW 토큰 게이트로 봇 요청을 차단하고, 벤더 호출량(앱 로그)과 캐시 hit율(DB)로 효과를 검증했습니다.

항공권 조회는 조회 대비 실제 예약 비율(look-to-book)이 기준을 초과하면 과금되는 구조입니다. 어느 날부터 봇이 조회 요청을 정상의 100~200배로 전송하기 시작했는데, 봇이 헤더를 얼마든지 위조할 수 있어 헤더 검사로는 차단할 수 없었습니다. 브라우저에 계산 문제를 풀게 해서 실제 브라우저인지 확인하는 PoW 토큰 게이트로 봇을 차단하고, 벤더 호출량과 캐시 hit율로 효과를 검증한 이야기입니다.

#bot#cache#cost#monitoring#pow#proof-of-work#security#troubleshooting
항공권 조회 봇을 브라우저 증명(PoW) 토큰으로 걸러낸 이야기 · cover
2026.07.05·2분·backend

모든 예외를 RuntimeException 으로 감쌌더니 '매진' 이 '알 수 없는 오류' 로 나갔다

의미 있는 에러 코드를 generic 예외로 재포장하면, 호출자는 재시도할지 포기할지 판단할 정보를 상실합니다

외부 파트너가 예약 API 에서 '관리자에게 문의하세요(UNKNOWN_ERROR)' 만 수신하고 같은 요청을 반복하던 문제를 다룹니다. 실제 원인은 좌석 매진이었는데, 컨트롤러가 모든 예외를 RuntimeException 으로 감싸는 바람에 의미 있는 에러 코드가 공통 핸들러의 catch-all 코드로 대체됐습니다.

#api#cache#error-handling#exception#fail-fast#troubleshooting
모든 예외를 RuntimeException 으로 감쌌더니 '매진' 이 '알 수 없는 오류' 로 나갔다 · cover
2026.06.07·2분·infra

배포 직후 5분만 정상이고 이후 빈 페이지가 되던 Amplify SSR

콘솔 환경변수는 빌드 시점에만 주입되고 SSR 런타임에는 주입되지 않습니다

Next.js + Amplify Hosting SSR 사이트가 "배포 직후 5분 정상, 5분 후 빈 페이지"를 반복했습니다. server-only 환경변수가 빌드 시점엔 있지만 SSR Lambda 런타임의 process.env 엔 없어서, ISR 백그라운드 갱신이 빈 결과로 캐시를 덮어쓰고 있었습니다. 런타임 로그에 환경변수 존재 여부를 출력해 원인을 확인하고, .env.production 커밋으로 수정한 과정을 기록했습니다.

#amplify#env#isr#lambda#nextjs#process-env#serverless#ssr#troubleshooting
배포 직후 5분만 정상이고 이후 빈 페이지가 되던 Amplify SSR · cover
2026.06.07·2분·infra

모듈 싱글톤 커넥션 풀이 서버리스에서 DB 커넥션을 고갈시킬 때

Amplify SSR 런타임은 배포당 하나가 아니라 인스턴스마다 풀을 새로 생성합니다

운영 RDS 가 간헐적으로 커넥션 부족을 유발했는데, 누수가 아니라 idle 연결이 전체 커넥션 수의 대부분을 점유하는 형태였습니다. mysql2 풀을 모듈 싱글톤으로 생성해 뒀지만, Amplify Hosting 의 SSR 런타임에서 그 싱글톤은 배포 전체에 하나가 아니라 런타임 인스턴스마다 하나라, connectionLimit 이 인스턴스 수만큼 곱해집니다. 서버리스에서 "전역 싱글톤" 이라는 가정이 성립하지 않는 사례입니다.

#amplify#aurora#connection-pool#lambda#mysql2#nextjs#rds#serverless#ssr#troubleshooting
모듈 싱글톤 커넥션 풀이 서버리스에서 DB 커넥션을 고갈시킬 때 · cover
2026.05.16·4분·ai

Claude 로 개인 블로그를 만든 과정

Claude Design 과 Claude Code 로 디자인부터 배포까지 진행한 과정입니다.

디자인 시안부터 미니 PC 배포까지, 코드를 직접 작성하지 않고 개발한 사이드 프로젝트 기록입니다. AI 가 코드를 작성하기 시작한 뒤 사람이 담당한 일(의도 결정, 경계 조건 확인, 검수)을 기록했습니다.

#ai#claude-code#devops#docker#nextjs#self-hosting#side-project#spring
Claude 로 개인 블로그를 만든 과정 · cover
2026.05.08·2분·backend

mysql2 의 timezone 과 dateStrings 가 같이 켜져 있으면 저장할 때마다 9 시간씩 늘어난다

두 옵션은 따로는 합리적이지만 함께 활성화하면 round-trip 마다 +9h 가 더해집니다. KST 환경의 Node 백엔드에서 자주 발생하는 경우입니다

사용자가 18:01 을 입력해 저장하면 새로고침에 18:10, 다시 저장하면 19:10 이 됐습니다. 매 저장 round-trip 마다 9 시간씩 증가하는 원인을 확인해 보니 mysql2 풀의 timezone + dateStrings 두 옵션의 상호작용이었습니다.

#datetime#kst#mysql2#nodejs#timezone#troubleshooting#utc
mysql2 의 timezone 과 dateStrings 가 같이 켜져 있으면 저장할 때마다 9 시간씩 늘어난다 · cover
2026.05.07·2분·infra

.dockerignore 한 줄 때문에 Docker 빌드에서 mvn test 가 실행되지 않았다

CodeBuild 에서 "No tests were executed!" 가 출력되면 의존성·JUnit 보다 먼저 .dockerignore 를 확인해야 합니다

로컬 mvn test 도 통과하고 의존성도 정상인데 CodeBuild 만 "No tests were executed!" 로 실패했습니다. 컴파일·surefire·JUnit 가설을 하나씩 한 시간씩 검증하다가, 원인이 .dockerignore 한 줄이라는 것을 확인한 기록입니다.

#ci#codebuild#docker#dockerignore#maven#surefire#troubleshooting
.dockerignore 한 줄 때문에 Docker 빌드에서 mvn test 가 실행되지 않았다 · cover
2026.05.07·3분·backend

Lettuce cluster 의 SCAN cursor 는 값이 아니라 인스턴스였다

cursor 객체를 새로 생성하면 라우팅 정보가 소실돼 첫 shard 만 조회됩니다

Redis cluster 에서 SCAN 루프가 dev 에선 정상인데 prod 에서만 매칭이 0건이었습니다. spring-data-redis 를 우회해 native Lettuce API 로 전환한 코드에서 cursor 를 새 인스턴스로 복사한 한 줄이 원인이었고, 이전 수정이 새 문제를 유발한 사례입니다.

#cluster#java#lettuce#redis#scan#spring-data-redis#troubleshooting#valkey
Lettuce cluster 의 SCAN cursor 는 값이 아니라 인스턴스였다 · cover
2026.05.07·5분·note

팀 지식 저장소를 중앙화한 이야기

AI 에게 일을 잘 시키기 위한 팀 공유 컨텍스트

분산돼 있던 팀의 암묵지를 한 곳에 집약하고, Claude Code 가 작업 시작과 커밋 시점 양쪽에서 자동으로 참조·기록하도록 강제하는 사내 공유 저장소를 구축했습니다.

#ai#claude-code#devops#hooks#knowledge-management
팀 지식 저장소를 중앙화한 이야기 · cover
2026.05.07·7분·backend

Valkey Serverless의 SCAN cursor가 Java long 범위를 넘어 NPE로 이어진 과정

spring-data-redis가 SCAN cursor를 long으로 변환하는 코드 때문에 결제는 승인되고 주문은 생성되지 않는 사고가 발생했습니다.

캐시 무효화에 사용하던 SCAN의 cursor 값이 Java long 범위를 초과하면서 NumberFormatException이 발생했고, scanKeys가 null을 반환하면서 호출자의 addAll(null)이 NPE로 이어졌습니다. 결제는 승인됐는데 주문이 생성되지 않는 사고로 보였던 한 줄짜리 버그와, 두 곳에 적용한 핫픽스, 그리고 Redis SCAN cursor가 큰 값을 갖는 이유를 기록했습니다.

#lettuce#npe#redis#scan#spring-data-redis#troubleshooting#valkey
Valkey Serverless의 SCAN cursor가 Java long 범위를 넘어 NPE로 이어진 과정 · cover
2026.05.06·5분·backend

JVM 배치 서비스의 14일 메모리 누수, JSch SFTP가 Session을 닫지 않고 있었다

heap dump를 분석해 SFTP connection thread가 누적되는 원인을 발견하기까지의 진단 과정을 기록했습니다.

운영 배치 컨테이너의 메모리가 14일 동안 +351 MiB 단조 증가하다 OOM Kill로 종료됐습니다. CloudWatch 메트릭, ECS Exec, jcmd heap dump, Eclipse MAT 분석을 차례로 진행해, JSch SFTPUtils.destroy()가 Channel만 종료하고 Session(과 그 connection thread)을 종료하지 않는 한 줄짜리 누수를 발견했습니다. 같은 dump에서 AspectJ ShadowMatchCache와 JVM reflection inflation 클래스도 정상 baseline보다 많이 누적돼 있었습니다.

#aws-ecs#eclipse-mat#heap-dump#jsch#jvm#memory-leak#sftp#troubleshooting
JVM 배치 서비스의 14일 메모리 누수, JSch SFTP가 Session을 닫지 않고 있었다 · cover
2026.05.06·4분·backend

항공권 검색 캐시 전략: 무엇을 캐시하고, 언제 비우는가

외부 GDS 호출 비용을 절감하면서 stale 좌석 정보는 회피하는 무효화 설계입니다.

항공권 조회는 외부 GDS·NDC 호출 단가가 비싸서 검색 결과를 Valkey에 캐시해 재사용합니다. 다만 좌석은 수시로 변경되는 데이터라 캐시가 stale해지면 곧장 사용자 불만으로 이어집니다. 캐시 키를 어떻게 설계했고, 어떤 트리거로 어떤 범위를 비우는지, 그리고 무효화 단계를 본 흐름과 결합할지 분리할지를 정리했습니다.

#airline#architecture#cache#cache-invalidation#redis#valkey
항공권 검색 캐시 전략: 무엇을 캐시하고, 언제 비우는가 · cover