로깅 필터를 붙였더니 컨트롤러가 요청 바디를 못 읽었다

로깅 필터를 붙였더니 컨트롤러가 요청 바디를 못 읽었다

API 요청/응답을 로깅하는 필터를 붙였다. 로그는 잘 남았는데, 그 뒤로 컨트롤러가 요청 바디를 못 읽는다며 빈 값으로 처리되기 시작했다. 요청 바디 스트림은 한 번만 읽힌다 HTTP 요청 바디는 InputStream이라 기본적으로 한 번 읽으면 끝이다. 로깅 필터에서 바디를 읽어 로그로 남기는 순간 스트림이 소진됐고, 그 뒤 컨트롤러가 다시 읽으려 하니 남은 게 없었다. 로그를 위해 데이터를 … 더 읽기

JSON 응답이 무한루프에 빠졌다 — 양방향 연관과 직렬화

JSON 응답이 무한루프에 빠졌다 — 양방향 연관과 직렬화

주문 엔티티를 그대로 JSON으로 내려 줬더니 응답이 안 끝나고 서버가 뻗었다. 로그엔 끝없이 반복되는 스택. com.fasterxml.jackson…JsonMappingException: Infinite recursion (StackOverflowError) 양방향 연관이 서로를 계속 부른다 Order는 Member를 참조하고, Member는 자기 Order 목록을 참조하는 양방향 관계였다. Jackson이 Order를 직렬화하려고 Member를 펼치고, Member를 펼치다 다시 Order를 만나고, 그 Order가 또 Member를 부르고… 무한 루프에 빠져 스택이 터진 거다. … 더 읽기

밤마다 뜨던 데드락, 잠그는 순서를 고정하니 사라졌다

밤마다 뜨던 데드락, 잠그는 순서를 고정하니 사라졌다

포인트 정산 배치가 밤마다 몇 건씩 실패했다. 로그엔 이 한 줄. Deadlock found when trying to get lock; try restarting transaction 재시도하면 성공하니 넘어갈 뻔했는데, 원인을 안 잡으면 트래픽 늘 때 폭발할 종류의 버그라 파고들었다. 두 트랜잭션이 서로 반대 순서로 잠갔다 정산 로직이 두 계좌 사이에 포인트를 옮기는 구조였는데, 어떤 트랜잭션은 A를 잠그고 B를 잠그려 … 더 읽기

@Transactional을 붙였는데 롤백이 안 됐던 이유

@Transactional을 붙였는데 롤백이 안 됐던 이유

대량 회원 데이터를 외부에서 받아 저장하는 배치를 만들었습니다. 요구사항은 단순했습니다. 한 건이 잘못돼도 나머지는 저장돼야 한다. 그래서 각 건을 독립된 트랜잭션으로 처리하려고 @Transactional(REQUIRES_NEW)를 붙였습니다. 그런데 운영에 올리고 보니, 중간에 한 건이 실패하면 그 배치 전체가 통째로 롤백되거나, 반대로 실패한 건까지 저장돼 버리는 이상한 일이 벌어졌습니다. @Service public class MemberImportService { public void importAll(List<MemberDto> rows) { … 더 읽기

OSIV를 껐더니 터진 LazyInitializationException

OSIV를 껐더니 터진 LazyInitializationException

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

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

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

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

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) 사이에서 … 더 읽기