아카이브
지금까지 쓴 글들을 시간순으로 모아봤어요.
-
saveAll() 이 1만 INSERT 가 되는 이유 — IDENTITY + Hibernate batch 비활성화의 구조적 함정
hibernate.jdbc.batch_size=50 으로 설정해도 1만 row 의 saveAll() 이 1만 INSERT 로 발사되는 이유. GenerationType.IDENTITY 는 매 INSERT 마다 LAST_INSERT_ID() 가 즉시 필요해서 Hibernate 가 batch 를 *구조적으로 비활성화* — Statement.RETURN_GENERATED_KEYS 가 batch 와 호환 안 됨. TABLE 전략 시뮬레이션 (애플리케이션이 ID 미리 발급) 은 batch 묶임 → 200 SQL. raw JDBC batchUpdate + rewriteBatchedStatements=true 는 multi-value INSERT 로 약 10 SQL — 가장 빠름. DZone 의 IDENTITY → SEQUENCE 100배 글은 PostgreSQL 기준 — MySQL 은 SEQUENCE 미지원 (TABLE 로 에뮬레이션). MySQL 환경의 진짜 답은 UUID / TableGenerator pooled-lo / Snowflake / raw JDBC batch 중 선택입니다.
-
JPA N+1 + JOIN FETCH 깊이 함정 4종 — MultipleBagFetchException, Pagination OOM, OneToOne LAZY 트랩
4-depth 도메인 (owner→merchant→rule→history) 에서 findAll() + 자식 접근하면 21 SQL. JOIN FETCH 한 단계로 1 SQL. 단 *두 collection 동시 fetch* 면 MultipleBagFetchException — Hibernate 가 List(Bag) 두 개의 cartesian product 를 거부. *JOIN FETCH + setMaxResults* 면 HHH000104 경고 + in-memory pagination — 1만 row 다 읽고 20개만 남기는 OOM. *@OneToOne non-owning LAZY* 는 프록시가 null 인지 알 방법이 없어서 *항상 fetch* — LAZY 무시. @BatchSize 가 N+1 → N/K+1 으로 완화하는 표준 처방. JPA 의 fetch 함정은 기능 한 줄이 아니라 *Bag/List/Set + 프록시 메커니즘 + Hibernate 의 cartesian 처리 정책* 의 상호작용입니다.
-
JPA Dirty Checking 의 진짜 비용 — readOnly / @DynamicUpdate / Query Plan Cache 누수
Hibernate 의 dirty checking 은 entity load 시점에 *snapshot 메모리 복사* + flush 시 *모든 managed entity 의 현재 상태 vs snapshot 비교* + 변경된 row 만 UPDATE. 1만 row update 에서 readOnly=true 가 snapshot 안 만들어 메모리 절감, @DynamicUpdate 는 변경 컬럼만 SET 하지만 *Query Plan Cache 사용량 증가* (plan_cache_max_size 안 잡으면 영구 heap leak), bulk JPQL @Modifying 은 가장 빠르지만 *영속성 컨텍스트 비일관* — clearAutomatically=true 가 표준. 1만 insert 시 clear() 50마다 패턴이 메모리 일정 유지의 정석. JPA 의 함정은 기능 한 줄이 아니라 *flush + cache + snapshot* 의 lifecycle 상호작용입니다.
-
JPA 낙관락 + retry stampede 의 함정 — @Version 만으론 부족한 6 시나리오
100 worker 가 같은 룰의 priority 를 +1 합니다. @Version 없으면 priority < 100 (Lost Update). @Version 적용하면 OptimisticLockException 만 던지고 처리는 호출자 책임 — 일부만 성공. @Retryable(3) 백오프 0 으로 retry 면 *동시에 retry 가 몰려서* 다시 충돌 (retry stampede). exponential + full jitter 가 retry 를 분산시켜 priority=100 도달. 그리고 측정 도중 만난 자기 Lost Update — 같은 트랜잭션 안에서 SELECT 두 번이 다른 객체 (JDBC) vs 같은 인스턴스 (JPA 1차 캐시 ==). 분산 환경 Lost Update 와는 완전히 별개의 함정. @Transactional + @Retryable AOP 순서, exponential backoff + jitter 의 이론적 근거 (AWS Architecture Blog) 까지 풀었습니다.
-
[JPA + Spring Mastery 01] 1차 캐시 · flush · 트랜잭션 라이프사이클 — readOnly 가 줄이는 set_option, dirty checking 의 진짜 비용
PersistenceContext 가 Identity Map 패턴 (Fowler PoEAA) 위에 어떻게 작동하는지, ActionQueue 4종이 SQL 발행 순서를 어떻게 결정하는지, AutoFlush 가 query 직전에 무엇을 보고 flush 하는지, dirty checking 이 reflection 과 bytecode enhancement 두 방식에서 얼마나 비용이 다른지 — Hibernate 6 의 DefaultFlushEventListener 부터 카카오페이의 readOnly + set_option QPS 58% 감소 회고까지, 1차 캐시와 트랜잭션 라이프사이클을 line 단위로 분해한 기록입니다. JPA 5 ways 측정 (raw JDBC 0.74 / JPA 0.99~1.95 ms) 의 +0.4ms baseline 비용이 어디서 오는지, 그리고 그 비용을 줄이기 위한 readOnly 의 *3단 효과* (Hibernate flush mode + Spring tx 마커 + MySQL Com_set_option 감소) 를 라인 단위로 풀어봅니다.
-
프로덕션이 'Check failed: node->IsInUse()' 한 줄로 죽었습니다 (2) — Datadog 프로파일러가 V8 청소부와 race를 만든 자리
1편에서 V8 GlobalHandles 의 어설션이 깨졌다는 것까지 확인했습니다. 그 어설션을 깬 범인을 추적합니다 — V8 CPU Profiler 의 WeakCodeRegistry, dd-trace 의 1분 타이머, 그리고 6주 전 v5.14에서 새로 기본 경로가 된 lazy stop-and-collect. 마지막으로 우리 팀이 출혈 차단을 위해 무엇을 껐고 항구적으로는 어떻게 처방할 것인가까지 정리합니다.
-
[JPA + Spring Mastery 08] 트랜잭션 분리 패턴 — Saga / Outbox / REQUIRES_NEW, 학술 기원부터 EXP-09b 9 시나리오 실측까지
트랜잭션 안에서 외부 API 호출하지 마라 - 격언은 들어봤지만 어떻게 풀어야 하는지는 잘 다뤄지지 않습니다. 본 글은 PROPAGATION 7종의 정확한 의미부터 2PC (XA) 의 한계, Garcia-Molina 의 1987년 Sagas 논문, Pat Helland 의 CIDR 2005 Data on the Outside, Vogels 의 ACM Queue 2008 Eventually Consistent 까지 학술 기원을 짚고, 토스 SLASH24 SAGA, 29CM/리디 Outbox 한국 운영 사례를 거쳐, EXP-09b 9 시나리오 실측 매트릭스 - 패턴 A/B/C × OFF/DB_FAIL/EXT_FAIL - 로 검증한 기록입니다. 결제 도메인은 Saga, 알림은 Outbox, 캐시류만 단순 분리 - 이 매핑의 학술 + 운영 + 실측 3박자.
-
[JPA + Spring Mastery 07] Spring AOP self-invocation — @Transactional 이 작동하지 않는 진짜 이유, TransactionInterceptor.invoke 6단계까지 분해
낙관락 측정에서 successes=100인데 잔액이 그대로 100. 코드 logic 은 멀쩡한데 잔액이 안 줄어들었습니다. 원인은 같은 클래스 내부 호출이 Spring AOP 프록시를 우회한 것 — @Transactional 이 발동하지 않아 flush 가 일어나지 않았습니다. 이 글은 TransactionInterceptor.invoke 의 6단계 / AOP Alliance MethodInvocation.proceed() 가 raw target 을 호출하는 위치 / 같은 함정에 걸리는 6 어노테이션 (@Async / @Cacheable / @Validated / @Retryable / @PreAuthorize) / 4 가지 워크어라운드 (분리 빈 / getBean(self) / AopContext.currentProxy / AspectJ weaving) 를 Spring 6 / Hibernate 6 소스 라인 단위로 분해한 기록.
-
프로덕션이 'Check failed: node->IsInUse()' 한 줄로 죽었습니다 (1) — V8 GlobalHandles 해부
프로덕션 컨테이너가 평범한 새벽에 V8 fatal 한 줄을 남기고 죽었습니다. 어플 코드는 단 한 줄도 안 들어간 스택, Datadog 프로파일러의 마지막 호출, 그리고 V8 내부의 'Check failed: node->IsInUse()'. 이게 무슨 뜻인지부터 풀어봅니다 — 1편: 사건 재현과 V8 GlobalHandles의 해부.
-
MySQL 크레딧 차감 락 4종 비교 — 비관락 180ms / 100% 정확, 그리고 측정 도중 발견한 self-invocation 함정
잔액 100 인 계정에서 100 worker 가 동시에 1씩 차감하는 흔한 시나리오. 4 락 (낙관/비관/MySQL GET_LOCK/Redisson) 의 결과가 모두 다릅니다 — 비관락 180ms / 100% / 잔액 0, 낙관락 549ms (contention 시 재시도 폭증), GET_LOCK 5015ms (advisory lock 의 cost), Redisson 53/100 (단일 인스턴스 한계). 그리고 측정 도중 발견한 self-invocation 함정 — successes=100 인데 잔액 그대로 유지된 case. JPA / Spring 의 진짜 함정은 logic 이 아니라 AOP proxy 우회였습니다. GET_LOCK 의 connection-bound 함정 4 시나리오까지 직접 시연한 기록.
-
RDB Mastery #3 — EXPLAIN ANALYZE 마스터: Push Down 함정과 Index Selection 의 진짜 메커니즘
EXPLAIN ANALYZE 의 연산자 트리 한 줄을 읽을 줄 알면 옵티마이저의 결정을 직접 검증할 수 있습니다. Filter vs Index Range Scan over 한 단어 차이가 push down 성공 vs 실패. ANSI SQL 표준 row constructor (a,b)<(?,?) 가 MySQL 옵티마이저의 whitelist 패턴에 안 맞아서 push down 실패 — Bug #16247 은 2006년에 등록된 오래된 known limitation (현재 트래커는 duplicate 처리). Index Selection 도 옵티마이저의 cost-based 판단 — Q2 역설 (LIMIT 5 의 작은 수에서 옵티마이저가 잘못된 인덱스 선택해서 인덱스 추가가 느려짐). 100% 의 시간 옵티마이저는 옳지 않습니다. 1,000만 row 에서 측정한 5개 EXPLAIN ANALYZE 출력을 한 줄씩 해석하면서 push down 메커니즘과 cost-based index selection 의 내부를 풀어봅니다.
-
RDB Mastery #2 — MySQL 인덱스의 종류: B-tree / Hash / Covering / Composite / Multi-valued / Functional, 그리고 언제 무엇을 고를 것인가
InnoDB 의 모든 인덱스가 B-tree 가 아닙니다. Hash (Memory engine), Spatial (R-tree), Full-text (역인덱스), Multi-valued (8.0+, JSON 배열), Functional (8.0.13+, 표현식). 그리고 같은 B-tree 안에서도 clustered vs secondary, covering 여부, composite 의 leftmost prefix, cardinality / selectivity 가 결정의 축이 됩니다. 1,000만 row 환경에서 5종 인덱스를 직접 만들어 cardinality + Q1~Q5 latency 변화로 언제 무엇을 고를지를 측정으로 결정. Q3 covering 2,476배 / Q5 composite 577배 / Q2 역설 (인덱스 추가가 느려지는 케이스 0.66ms → 13.5ms). 인덱스는 공짜가 아닙니다 — 쓰기 비용 5~6배 + storage 1.3GB. 9개 다이어그램으로 끝까지 풀어봅니다.
-
RDB Mastery #1 — InnoDB 인덱스 내부 구조: No-Index 부터 다중 인덱스까지 B-tree 가 그리는 진짜 그림
인덱스를 안 걸어도 InnoDB 안에서는 이미 B-tree 입니다. PK 가 clustered index = 테이블 자체. Secondary index 는 PK 를 가리키는 별도 B-tree. Covering index 는 PK 까지 안 가도 답이 있는 인덱스. Reverse scan 은 leaf 의 양방향 linked list 를 거꾸로 walk. OFFSET 이 건너뛸 수 없는 이유는 B-tree 가 row 카운터를 안 가지기 때문. Cursor 가 빠른 이유는 WHERE 가 binary search primitive 를 트리거하기 때문. 다중 인덱스 = 같은 테이블에 N 개 B-tree. 1,000만 row 환경에서 [실측] Q3 covering 2,476배 / Q5 composite 577배 / OFFSET 1M 171ms / cursor 0.30ms — 다이어그램 10개로 끝까지 풀어봤습니다.
-
JVM Thread Dump로 분해한 HikariCP 풀 고갈 — TIMED_WAITING (parked) 의 진짜 의미
풀 고갈 알람이 울렸을 때 애플리케이션 코드만 들여다보면 답이 안 나옵니다. jstack으로 받아본 thread dump가 진짜 증거 — 모든 worker thread가 HikariCP 안에서 TIMED_WAITING (parked) 상태로 멈춰 있습니다. JVM Thread State 머신, LockSupport.parkNanos, ConcurrentBag·SynchronousQueue 의 동작, 그리고 트랜잭션-안-외부-호출 풀 고갈 [실측] (timeout 5s 100% / 1s 16.7%)이 thread dump 한 줄과 정확히 어떻게 매핑되는지 — 운영 중 풀 고갈을 dump 한 장으로 진단하는 방법을 라인 단위로 풀어봤습니다.
-
MySQL No-Offset Cursor 페이지네이션 — 1,000만 row에서 OFFSET 1M이 171ms / Cursor 0.30ms, 그 사이의 500배 함정 한 줄까지 측정으로 풀어봤습니다
1,000만 row 환경에서 OFFSET 1M이 171ms / No-Offset Cursor가 0.30ms — 약 570배 차이를 측정으로 재현했습니다. 그런데 No-Offset 코드를 어떻게 쓰느냐에 따라 또 한 번 500배가 갈라집니다. ANSI SQL 표준의 row constructor `(a,b)<(?,?)` 는 의미상 OR 분리 형태와 같지만 MySQL 옵티마이저가 index range로 push down 못 합니다 (154ms — OFFSET과 거의 동일). EXPLAIN ANALYZE 의 Filter: vs Covering index range scan over 한 줄 차이가 본질입니다 — 프로덕션 회고와 학습 환경 재현을 같이 풀어봤습니다.
-
MySQL InnoDB 격리수준 — phantom read 를 4 격리수준 모두 [실측]하고 RR 이 ANSI 표준보다 강한 이유를 메커니즘으로 분해했습니다
ANSI SQL 표준의 RR 은 phantom read 차단을 보장하지 않습니다. 그런데 MySQL InnoDB 의 RR 은 차단합니다. 이 흔한 주장을 직접 측정으로 굳혔습니다 — RU/RC 는 phantom 발생 (A1=0 → INSERT → A2=1), RR 은 차단 (A2=0), SERIALIZABLE 은 INSERT 자체 wait (1.56초). 그리고 왜 MySQL RR 이 ANSI 표준보다 강한지 — consistent read snapshot / gap lock / MVCC undo log 세 메커니즘으로 분해해서 결제 도메인에는 RR 만으로 충분하다는 결론을 측정값으로 굳혔습니다.
-
트랜잭션 안에서 외부 API 호출 — 풀 고갈을 직접 재현하고, 단순 분리·Saga·Outbox 세 처방을 측정으로 비교했습니다
Spring + HikariCP 환경에서 트랜잭션 안의 외부 API 호출이 풀을 고갈시키는 메커니즘을 raw JDBC로 재현한 뒤, 단순 분리 / Saga / Outbox 세 가지 처방을 60 worker × 9 chaos 시나리오로 직접 측정했습니다. 단순 분리가 정합성을 깨는 순간을 60건 어긋남으로 잡아내고, Saga의 3중 안전망이 어떻게 차례로 동작하는지, Outbox의 사용자 응답 72ms와 처리 완료 평균 93초가 *같은 측정값에서 어떻게 갈리는지*까지 라인 단위로 풀어봤습니다.
-
INSTANT DDL인데 왜 timeout? - MySQL 메타데이터 잠금 실전 진단 가이드
ALGORITHM=INSTANT로 실행한 ALTER TABLE이 45초 만에 timeout으로 실패했다. INSTANT가 느린 게 아니다 - DDL이 시작조차 못하고 MDL 획득에서 막히고 있었다. 2-세션 진단 방법, Idle in Transaction 분석, 안전한 KILL 판단 기준까지.
-
작은 테이블에도 파티셔닝을 적용한 이유: DELETE 대신 DROP PARTITION으로 300배 빠른 아카이빙
2.5GB, 300만건 규모의 로그 테이블에 파티셔닝을 적용한 결정 과정. 데이터 크기가 아닌 DELETE 패턴이 파티셔닝 적용 기준이 되어야 하는 이유와, Redis 분산 락을 활용한 멀티 인스턴스 환경의 파티션 관리 자동화까지
-
N사 2FA 메타데이터 테이블 설계기: BIGINT vs UUID 선택부터 TypeORM GENERATED 컬럼 트러블슈팅까지
플랫폼 계정 2FA 상태 추적을 위한 메타데이터 테이블 설계 과정에서 겪은 PK 타입 선택, UNIQUE KEY 설계, Optimistic Lock 적용, 그리고 TypeORM과 MySQL의 GENERATED 컬럼 충돌 해결까지의 기록
-
운영 중 ALTER TABLE 한 줄이 서비스를 마비시킨 이유 - MySQL 메타데이터 잠금의 모든 것
DB Load가 180배 급증한 장애의 원인을 추적하며 배운 MySQL Metadata의 정의, Data Dictionary 아키텍처, MDL 내부 구조와 락 획득 알고리즘, Convoy Effect, INSTANT DDL, 그리고 빅테크 기업들의 무중단 DDL 전략까지
-
TypeORM과 NestJS에서 커넥션 풀 제대로 설정하기
현재 설정을 분석하고 네이버 D2의 Commons DBCP 가이드를 참고하여, TypeORM과 mysql2 환경에서 안정적인 커넥션 풀 설정을 구현하는 과정을 공유합니다. TPS 계산 공식을 적용하고, 실제 프로덕션 코드를 Before/After로 비교하며 각 옵션의 의미를 깊이 이해합니다.
-
Kotlin Coroutines 딥다이브: 내부 구조부터 프로덕션 패턴까지
suspend 키워드 뒤에 숨겨진 State Machine의 동작 원리를 파헤치고, RxJava, CompletableFuture, Project Loom과의 트레이드오프를 비교합니다. 실제 프로덕션에서 마주치는 에러 핸들링, 테스트, 디버깅 패턴까지 다룹니다.
-
equals/hashCode 재정의가 만든 장애: 중복 결제 사고의 기록
Updated:equals()만 재정의하고 hashCode()를 빠뜨려 중복 결제 장애를 겪은 경험. Kafka TopicPartition 분석과 함께 HashMap 내부 동작부터 코드 리뷰 체크리스트까지.
-
B+tree 인덱스와 Page Split: UUID가 당신의 INSERT를 죽이고 있다
왜 복합 인덱스의 컬럼 순서가 중요할까? UUID PK가 왜 INSERT 성능을 망칠까? InnoDB B+tree의 Page Split 메커니즘과 Big-O 시간복잡도를 파고들어, '감'이 아닌 '원리'로 인덱스를 설계합니다. Instagram, Shopify, 카카오, 배민의 실제 사례도 함께 다룹니다.
-
배달 플랫폼 스크래핑 대장정 Part 1: API에서 브라우저 자동화로
API 호출 방식의 한계를 넘어 Playwright 브라우저 자동화로 전환한 여정. CDP(Chrome DevTools Protocol) 통신 구조부터 세션 풀 설계까지, 대규모 스크래핑 시스템의 첫 번째 아키텍처를 설계하며 배운 것들을 기록합니다.
-
MySQL InnoDB 아키텍처 이해: 엔진을 알아야 설계가 보인다
Buffer Pool Hit Rate 85%인데 왜 쿼리가 3초나 걸렸을까? InnoDB 엔진의 내부 구조(Buffer Pool, Redo Log, Undo Log)를 파고들어 RDB 설계의 '왜'를 이해합니다.
-
대규모 브라우저 자동화 시스템의 메모리 누수 해결기: 3개의 정리 경로가 만든 완벽한 폭풍
50개의 Firefox 브라우저를 동시에 관리하는 자동화 시스템에서 발견한 메모리 누수. Promise.race와 finally 블록이 만든 이중 정리 문제, 그리고 이를 해결하기까지의 탐구 과정을 기록합니다.
-
멀티 플랫폼 연동을 위한 데이터베이스 설계: 확장 가능한 로깅 시스템 구축기
새로운 외부 플랫폼 연동을 위한 전용 테이블 설계부터 EXPLAIN 기반 검증, 인덱스 최적화, 파티셔닝 전략까지 - 팀 규모와 트래픽을 고려한 DB 설계 경험기
-
Kotlin 표준 라이브러리 toSet() 해부: 엔지니어링은 선택에 대한 설명이다
Kotlin의 toSet() 메서드를 JVM 메모리 모델부터 프로덕션 환경까지 깊이 있게 분석합니다. 표준 라이브러리의 설계 결정, 메모리 오버헤드, GC 영향, 그리고 대규모 트래픽 환경에서의 실전 가이드를 다룹니다.