saveAll을 썼는데 insert가 1,000번 나간 이유

saveAll을 썼는데 insert가 1,000번 나간 이유

정산 배치에서 수천 건을 saveAll로 한 번에 저장하도록 짰습니다. 코드는 한 줄이라 당연히 묶어서 나갈 줄 알았는데, 쿼리 로그를 켜 보니 insert가 건수만큼 개별로 나가고 있었습니다. 1천 건이면 1천 번이었죠. List<Settlement> rows = build(…); // 1,000건 settlementRepository.saveAll(rows); // insert가 1,000번 saveAll은 배치를 보장하지 않는다 saveAll은 그냥 반복해서 저장을 호출하는 편의 메서드일 뿐, 그 자체로 JDBC … 더 읽기

OSIV를 껐더니 터진 LazyInitializationException

OSIV를 껐더니 터진 LazyInitializationException

커넥션이 오래 물리는 걸 줄이려고 spring.jpa.open-in-view를 false로 껐다. 이론은 알고 있었다. OSIV가 켜져 있으면 뷰를 렌더링할 때까지 영속성 컨텍스트와 커넥션이 살아 있으니, 트래픽이 몰릴 때 커넥션 점유가 길어진다. 그래서 껐는데, 끄자마자 여기저기서 예외가 터졌다. org.hibernate.LazyInitializationException: could not initialize proxy – no Session 지연 로딩할 세션이 이미 닫혔다 OSIV를 끄면 트랜잭션이 끝나는 순간 영속성 컨텍스트도 닫힌다. … 더 읽기

서버 시간은 맞는데 응답만 9시간 틀렸던 타임존 버그

서버 시간은 맞는데 응답만 9시간 틀렸던 타임존 버그

가입 시각을 응답으로 내려 주는데, 프런트에서 자꾸 9시간 전으로 표시된다는 제보가 왔습니다. 서버 시간은 정확했고 DB에 찍힌 값도 맞았습니다. 딱 9시간, 그러니까 한국과 UTC의 시차만큼이 어긋나 있었습니다. 어디서 9시간이 새는가 추적해 보니 DB에는 UTC 기준으로 시각이 저장돼 있었는데, 애플리케이션은 이 값을 그냥 한국 시각인 것처럼 다뤘습니다. LocalDateTime은 이름 그대로 타임존 정보가 없는 타입입니다. 같은 2026-07-01T00:00이라도 … 더 읽기

fetch join에 페이징 걸었다가 수만 건을 메모리로 퍼올린 이야기

fetch join에 페이징 걸었다가 수만 건을 메모리로 퍼올린 이야기

주문 목록을 주문항목까지 한 번에 가져오려고 fetch join에 페이징을 걸었다. 기능은 됐는데 데이터가 몇만 건 쌓이자 목록 API가 6초씩 걸리고 가끔 힙이 터졌다. 로그를 보니 조용히 경고가 찍혀 있었다. HHH000104: firstResult/maxResults specified with collection fetch; applying in memory 페이징을 메모리에서 하고 있었다 컬렉션을 fetch join 하면서 페이징을 걸면, DB에서는 limit을 못 건다. 조인 때문에 한 … 더 읽기

캐시를 켰는데 정각마다 DB가 터졌다 — 캐시 스탬피드

캐시를 켰는데 정각마다 DB가 터졌다 — 캐시 스탬피드

인기 상품 목록을 Redis에 캐싱하고 TTL을 10분으로 잡았습니다. 조회가 잦고 잘 안 바뀌는 데이터라 캐시로 DB 부하를 크게 덜 수 있었죠. 실제로 평소 DB는 아주 한가했습니다. 그런데 배포 며칠 뒤부터 매시 정각마다 DB CPU가 규칙적으로 튀는 이상한 패턴이 잡혔습니다. 그래프가 정확히 톱니 모양이었습니다. 규칙적이라는 게 결정적 단서였다 불규칙하게 튀면 트래픽 탓이라고 볼 텐데, 매시 정각에 … 더 읽기

신뢰성 있는 Webhook 전송은 어떻게 설계할까? — Transactional Outbox 패턴 (1편)

신뢰성 있는 Webhook 전송은 어떻게 설계할까? — Transactional Outbox 패턴 (1편)

이 글은 “믿을 수 있는 Webhook 전달 서비스 만들기” 시리즈의 첫 편입니다. 외부로 이벤트를 전달하는 Webhook은 “그냥 HTTP 한 번 쏘면 되는 것” 처럼 보이지만, 실제로 신뢰성 있게 만들려고 하면 분산 시스템의 어려운 문제들을 정면으로 만나게 됩니다. 이번 편에서는 코드를 바로 짜기 전에 무엇이 어려운지와 왜 Transactional Outbox 패턴을 선택하는지를 먼저 정리합니다. 다음 편부터 실제로 … 더 읽기

DB 부하를 줄이는 Spring Cache와 Redis 기반 캐싱 전략

DB 부하를 줄이는 Spring Cache와 Redis 기반 캐싱 전략

데이터베이스 아키텍처나 JPA 영속성 컨텍스트를 공부하다 보면 결국 백엔드 성능 최적화의 최종 관문과 마주하게 됩니다. 바로 조회(Read) 성능 최적화입니다. 대규모 트래픽이 몰리는 서비스에서 매번 똑같은 데이터를 조회하기 위해 무거운 RDB(관계형 데이터베이스)에 쿼리를 날리는 것은 비효율적일 뿐만 아니라, 전체 시스템을 다운시키는 주범이 되기도 합니다. 오늘은 스프링이 제공하는 선언적 캐시 추상화(Spring Cache)를 이해하고, 실무에서 글로벌 캐시 저장소로 … 더 읽기

스프링 부트 실무 아키텍처 가이드: @Valid와 @RestControllerAdvice를 결합한 완벽한 데이터 검증 스펙 구축하기

스프링 부트 실무 아키텍처 가이드 @Valid와 @RestControllerAdvice를 결합한 완벽한 데이터 검증 스펙 구축하기

지난 포스팅에서는 @RestControllerAdvice를 활용해 애플리케이션 전역의 예외를 한곳에서 가로채는 중앙 집중식 예외 처리 시스템을 구축해 보았습니다. 하지만 백엔드 개발을 하다 보면 예외 처리만큼이나 자주 마주치고, 또 수많은 중복 코드를 양산하는 주범이 있습니다. 바로 클라이언트가 보낸 데이터가 유효한지 검증(Validation)하는 작업입니다. 예를 들어 회원가입 요청 데이터에서 이메일 형식이 맞는지, 비밀번호가 공백은 아닌지, 나이가 음수로 들어오진 않았는지 등을 … 더 읽기

실무형 스프링 예외 처리 패턴: @RestControllerAdvice와 전역 Exception Handler 구축

실무형 스프링 예외 처리 패턴 @RestControllerAdvice와 전역 Exception Handler 구축

웹 애플리케이션을 개발하다 보면 데이터베이스 조회 실패, 잘못된 파라미터 입력, 권한 부족 등 수많은 예외(Exception) 상황을 마주하게 됩니다. 백엔드 서버에서 이러한 예외를 제때 잡아내지 못하면, 클라이언트에게 내부 소스 코드나 톰캣 에러 페이지(500 Internal Server Error)가 그대로 노출되는 심각한 보안 및 사용자 경험 문제가 발생합니다. 전통적인 자바 웹 개발에서는 모든 컨트롤러 메서드마다 try-catch 블록을 떡칠하여 예외를 … 더 읽기

JPA의 심장: 영속성 컨텍스트의 4가지 이점과 쓰기 지연 내부 원리

JPA의 심장 영속성 컨텍스트의 4가지 이점과 쓰기 지연 내부 원리

스프링 부트(Spring Boot) 백엔드 개발에서 JPA를 사용할 때 우리는 단순히 repository.save()를 호출하거나 엔티티를 조회하곤 합니다. 하지만 복잡한 실무 환경에서 성능을 최적화하고 예기치 못한 데이터 정합성 오류를 방지하려면, JPA의 내부 메커니즘인 영속성 컨텍스트(Persistence Context)를 반드시 완벽하게 이해해야 합니다. 영속성 컨텍스트는 “엔티티를 영구 저장하는 환경”이라는 뜻을 가지고 있습니다. 눈에 보이지 않는 논리적인 영역이지만, JPA 프리프레임워크와 데이터베이스(DB) 사이에서 … 더 읽기