전체 글 86

Spring Batch 커넥션 풀 고갈 해결 — 조회·계산·쓰기 3-Phase 분리

커넥션 풀 고갈 장애를 만나면 대부분 풀 사이즈를 늘리는 것부터 시작합니다. 하지만 이는 증상 완화에 가깝습니다. 실제로 운영 중인 배치에서 커넥션 풀 고갈로 배치가 멈추는 장애를 겪었고, 풀 사이즈를 늘려도 장애는 반복됐습니다. 근본 원인은 풀의 크기가 아니라 커넥션을 붙잡은 채 추가 커넥션을 획득하는 구조에 있었기 때문입니다.이 글에서는 장애의 원인을 분석하고, 배치를 조회 → 계산 → 쓰기 3-Phase로 분리해 커넥션 점유 자체를 없앤 재설계 과정을 정리합니다.1. 장애: 배치가 조용히 멈춘다외부에서 유입되는 이벤트에 점수를 매기는 배치가 있었습니다. 1분 주기로 미처리 이벤트를 조회해 점수를 계산하고, 결과를 저장한 뒤 누적 통계를 갱신하는 전형적인 배치입니다.어느 날부터 이 배치가 운영 환경에..

스프링 2026.09.01

복합 인덱스 설계의 오해와 실체: 선택도가 아니라 연산자가 기준이다

인덱스를 만들 때 "선택도(Selectivity)가 높고 데이터가 잘 분산된 칼럼을 앞에 두라"는 조언이 많지만, 이는 데이터베이스 내부 동작 원리를 고려하지 않은 오해에 가깝습니다. 복합 인덱스 칼럼 순서를 결정하는 진짜 기준을 정리합니다.1. 모든 조건이 등호(=)일 때: 순서는 상관없다두 칼럼을 모두 등호 조건으로 조회하는 경우, 칼럼 순서가 바뀌어도 성능 차이는 없습니다.SELECT * FROM employees WHERE employee_id = 123 AND subsidiary_id = 20;이유: B-Tree 인덱스는 왼쪽 칼럼부터 순서대로 다단 정렬됩니다. 두 조건 모두 명확한 값(=)을 지정했다면, 어느 칼럼이 앞에 오든 목적지 데이터로 향하는 탐색 경로와 비용은 동일합니다.진짜 기준: ..

데이터베이스 2026.07.02

Kotlin Coroutines의 핵심 원리와 Java 21 가상 스레드 활용 방안

최근 비동기 처리를 구현하면서 코루틴의 내부 동작과 활용법을 깊이 있게 검토해 보았습니다. 단순히 API를 사용하는 수준을 넘어, 코루틴의 작동 원리와 최신 Java 21 가상 스레드(Virtual Thread)와의 차이점, 그리고 실무적인 예외 처리 전략을 중심으로 학습한 내용을 정리해 보았습니다.1. 동시성 메커니즘의 비교 (Thread vs Coroutine vs Virtual Thread)효율적인 동시성 제어를 위해서는 각 메커니즘이 자원을 어떻게 관리하는지 파악하는 것이 우선이라고 생각합니다. 관리 주체와 비용에 따른 차이를 다음과 같이 정리했습니다.구분스레드 (Platform)가상 스레드 (Java 21)코루틴 (Kotlin)관리 주체OS 커널JVM (Project Loom)Kotlin 런타임..

[Java 21] 가상 스레드(Virtual Threads): 원리와 실무 적용 지침

Java 21에서 정식 도입된 가상 스레드(Virtual Thread)는 기존 플랫폼 스레드 모델의 자원 한계를 극복하기 위한 새로운 동시성 모델입니다. 본 포스팅에서는 가상 스레드의 설계 배경, 동작 원리, 그리고 실무 적용 시 반드시 알아야 할 핵심 개념을 정리해 보겠습니다.1. 가상 스레드의 설계 배경자바 애플리케이션의 성능은 오랫동안 운영체제(OS)의 스레드를 직접 사용하는 플랫폼 스레드(Platform Thread)에 종속되어 왔습니다. 하지만 플랫폼 스레드는 다음과 같은 물리적 제약이 존재합니다.메모리 점유: 스레드 하나당 약 1~2MB의 스택 메모리를 점유하므로 수천 개 이상의 스레드를 운용하기 어렵습니다.I/O 대기 손실: DB 조회나 외부 API 호출 시, 응답이 올 때까지 스레드는 자원..

Sealed Interface로 깔끔하고 안전한 API 응답 구조 만들기

백엔드 개발을 하다 보면 서비스 레이어의 처리 결과를 컨트롤러에 어떻게 전달할지 고민하게 됩니다. 데이터가 없으면 null을 보낼지, 아니면 Exception을 던져야 할지 모호한 경우가 많죠. 이럴 때 Sealed Interface를 활용하면 응답의 상태를 명확하게 강제할 수 있고, 코드의 안정성도 획기적으로 높아집니다.1. Sealed Interface 응답 구조 설계성공, 데이터 없음, 에러 상황을 하나의 인터페이스로 묶어 관리하는 방식입니다. 여기서 핵심은 상황에 따라 class와 object를 적절히 섞어 쓰는 것입니다.sealed interface AnalyticsResult { // 성공: 결과 데이터라는 '본문'이 있으므로 data class data class Success(v..

기타 2026.02.26

Spring Cache 깊게 파헤치기: Cache vs CacheManager

Spring에서 캐시를 사용할 때 @Cacheable 어노테이션 한 줄로 마법처럼 동작하는 배경에는 CacheManager와 Cache라는 두 핵심 인터페이스의 철저한 역할 분담이 있습니다. 이 둘의 관계를 이해하면 커스텀 캐싱 전략을 훨씬 유연하게 설계할 수 있습니다.1. CacheManager: 캐시를 찍어내는 "공장이자 관리소"CacheManager는 캐시 추상화의 최상위 진입점입니다. 애플리케이션 내의 모든 캐시 인스턴스를 생성하고 관리하는 역할을 합니다.주요 역할:캐시 생성 및 조회: "users", "products" 등 이름에 맞는 Cache 객체를 생성하거나 찾아줍니다.설정의 중심: 데이터 유효 기간(TTL), 최대 크기, 직렬화 방식 등 '어떻게(How)' 저장할지를 결정합니다.라이프사이..

스프링 2026.02.26

[Spring Boot/Kotlin] Spring Cache 어노테이션 심화 가이드

1. 개요Spring Cache 추상화의 가장 큰 장점은 비즈니스 로직에 캐시 코드를 섞지 않고, 어노테이션(Annotation) 만으로 선언적인 캐싱을 적용할 수 있다는 점입니다. 본 보고서는 Spring Cache의 핵심 어노테이션인 @Cacheable, @CachePut, @CacheEvict 등의 기능과 속성을 상세히 분석하고, Kotlin 환경(Data Class, Coroutines)에서의 구체적인 사용 패턴을 제시합니다.2. 핵심 어노테이션 상세 분석2.1 @Cacheable: 조회 및 저장의 자동화가장 빈번하게 사용되는 어노테이션으로, 캐시 룩어사이드(Look-aside) 전략을 수행합니다. 데이터가 캐시에 있으면 반환하고, 없으면 메서드를 실행 후 결과를 캐시에 저장합니다.2.1.1 기본..

스프링 2026.02.26

Redis MGET이란? 동작 원리와 네트워크 성능 최적화 방법 정리

MGET (Multi-Get)이란?MGET은 Redis와 같은 키-값(Key-Value) 저장소에서 여러 개의 키(Key)에 해당하는 값(Value)들을 단 한 번의 명령으로 가져오는 기능입니다.비효율적인 방법 (GET 반복): 필요한 물건 목록(우유, 계란, 빵)을 보고, 우유를 가지러 갔다가 계산하고, 다시 계란을 가지러 갔다가 계산하고, 또 빵을 가지러 가는 방식입니다. 물건 하나마다 계산대(네트워크)를 거치므로 매우 비효율적입니다.효율적인 방법 (MGET): 필요한 물건 목록(우유, 계란, 빵)을 장바구니에 한 번에 다 담아서 계산대에 딱 한 번만 가는 방식입니다. 이것이 바로 MGET의 원리입니다.MGET의 동작 원리와 과정MGET은 N+1 문제를 해결하기 위해 "N번의 요청을 1번의 요청으로"..

데이터베이스 2026.02.25

포스트 하나에 10초? 코루틴으로 AI 요약 파이프라인 병렬화하기

최근 기술 블로그 RSS를 스크래핑하고 AI로 내용을 요약하는 'Morning Commit' 프로젝트의 파이프라인을 개선했습니다. 구현 과정에서 마주친 성능 병목 지점과 이를 Kotlin Coroutines로 해결한 과정을 정리했습니다.1. 문제 상황: 순차 처리의 한계기존 방식은 RSS 피드에서 추출한 포스트들을 하나씩 순서대로 처리했습니다.프로세스: HTML 스크래핑 → AI 요약(LLM API 호출) → 데이터 가공 및 저장소요 시간: 포스트 당 평균 약 10초포스트 하나당 10초는 짧아 보일 수 있지만, 배치 작업의 특성상 누적 시간은 무시할 수 없습니다. 한 번에 처리해야 할 포스트가 30개만 되어도 5분이 소요되며, 연동하는 블로그 소스가 늘어날수록 전체 작업 완료 시간은 선형적으로 증가하는 ..

기타 2026.02.24

[Spring Boot/Kotlin] Redis 캐싱: PageImpl 직렬화 에러와 ClassCastException 완벽 해결 가이드

Spring Boot와 Redis를 연동하여 캐싱을 구현하다 보면, 특히 Kotlin 환경에서 페이징된 데이터(Page)를 캐싱할 때 두 가지 큰 장벽에 부딪히게 됩니다.InvalidDefinitionException: PageImpl은 기본 생성자가 없어 Jackson이 역직렬화를 못 함.ClassCastException: 캐시에서 꺼낸 데이터가 DTO가 아닌 LinkedHashMap으로 변환됨.이 글에서는 이 문제의 원인을 파헤치고, 커스텀 Serializer를 이용한 근본적인 해결법과 Wrapper 클래스를 이용한 간편한 해결법 두 가지를 소개합니다.문제의 원인1. PageImpl의 구조적 문제Spring Data의 PageImpl 클래스는 Jackson이 데이터를 복원할 때 필요한 기본 생성자(N..

스프링 2026.02.17