목록전체 글 (292)
아무튼, 쓰기
식당 문을 닫을 시간이 되었습니다. 손님들에게 ’지금 당장 나가!’라고 소리치시겠습니까, 아니면 ’마지막 주문은 끝났으니, 드시던 음식은 편안히 드시고 가세요’라고 안내하시겠습니까?Part 1. Why - 왜 우아하게 꺼야 하는가? 1. 탄생배경: 백화점 정전 사태여러분이 백화점 에스컬레이터를 타고 3층에서 4층으로 올라가는 중이라고 상상해 봅시다. 그런데 갑자기 전기가 뚝 끊깁니다. 에스컬레이터는 급정거하고, 사람들은 휘청거립니다. 계산대에서 카드를 긁던 손님은 “결제가 된 거야, 만 거야?”라며 불안해합니다.서버 배포도 마찬가지입니다. 우리는 하루에도 수십 번씩 새로운 코드를 배포합니다. 그때마다 실행 중인 애플리케이션을 종료하고(SIGKILL) 새 버전을 띄웁니다. 만약 아무런 대비 없이 프로세스를..
Version Note: 이 문서는 Resilience4j v2.x (v2.3 이상) 및 v3.0.0 소스 코드를 기준으로 작성되었습니다. (v1.x 버전과는 설정 변수명 등이 다를 수 있습니다.)본 글은 Resilience4j 라이브러리의 내부 코드를 분석하여, 각 모듈이 실제로 어떤 알고리즘과 데이터 구조를 사용하여 동작하는지 설명하고, 주요 설정의 기본값 과 동작 예시 레퍼런스를 제공합니다.1. 알아보기 전에 왜 Resilience4j여야 하는가?단순한 사용법을 넘어, 설계자의 시선에서 Resilience4j를 깊이 있게 분석해 봅니다.1-1. 탄생 배경과 문맥왜 Netflix Hystrix는 역사의 뒤안길로 사라지고 Resilience4j가 표준이 되었는가?Netflix Hystrix는 Java ..
최근 프로젝트에서 Spring Boot 3와 Lombok을 조합해 사용하던 중, 의아한 문제를 겪었습니다. 분명히 @Qualifier를 붙였는데, 엉뚱한 빈이 주입되는 현상이었습니다.단순히 생성자를 직접 만들어서 해결했다로 끝내기엔 찜찜했습니다. 도대체 왜 이런 일이 벌어지는지, 스프링 프레임워크 내부 코드를 뜯어보며 그 원리를 꼬리에 꼬리를 무는 질문으로 파헤쳐 보았습니다.상황: 16MB짜리 WebClient가 필요해@Configurationpublic class WebClientConfig { @Bean @Primary public WebClient webClient() { // 일반적인 요청용 (기본 설정) return WebClient.builder() ..
들어가며20만 건의 경매 데이터를 처리하는 과정에서 187초에서 6.65초로 약 28배의 성능 개선을 이뤄낸 경험을 바탕으로, Reader/Writer 선택부터 멀티스레드를 적용한 과정 사이에 겪은 트러블 슈팅이나 공유할만한 인사이트를 적어보겠습니다.1. Reader 선택1.1 Cursor vs Paging: 동작 방식의 차이배치 처리에서 가장 먼저 마주한 선택은 어떤 방식으로 데이터를 읽을 것인가입니다.CursorItemReaderDB와 하나의 커넥션을 유지하며 커서를 열어두고 Fetch 단위로 데이터를 스트리밍메모리 효율적: 한 건씩 처리하므로 힙 메모리 사용량이 일정하게 유지단일 커넥션 사용으로 멀티스레드 불가PagingItemReader페이지 단위로 DB 커넥션을 맺고 끊으며 데이터를 가져옴각 페..
들어가며독서 습관 형성 서비스에서 랭킹은 유저 참여도를 높이는 핵심 기능입니다. 유저들은 자신의 순위를 확인하고, 상위권 사용자들과 비교하며, 순위를 높이기 위해 다음 행동을 이어갑니다.현재 서비스는 100만 개 순위에서 평균 동시 접속자 100명, 피크 타임에는 500명이 접속하는 수준을 가정하고 로직을 작성하였고, RDB만으로 50ms 응답속도를 확보하였고 Range scan이라는 한계상 피크 타임 때 처리량이 한계가 보여서 ZSET을 도입하여, 처리량을 높였습니다. 초기 설계: 점수별 인원 집계 테이블설계 의도처음 랭킹 시스템을 설계할 때, MySQL을 기반으로 구현하였습니다. 랭킹 화면에는 Top 100과 내 순위가 함께 표시되는데, Top 100은 인덱스가 정렬되어 있어 빠르게 조회할 수 있습..
개요@Transactional(readOnly=true) 어노테이션을 사용해 마스터/슬레이브 DB로 자동 라우팅하는 기능이 LazyConnectionDataSourceProxy 없이 제대로 동작하지 않는 이유와 해결책을 정리한 내용입니다.문제 상황(readOnly=true여도 마스터 DB로 접속)AbstractRoutingDataSource를 구현해 TransactionSynchronizationManager.isCurrentTransactionReadOnly() 값으로 DB를 분기 처리해도, readOnly=true로 설정된 트랜잭션이 슬레이브 DB가 아닌 마스터 DB로 잘못 연결되고 있었습니다.왜 readonly가 주입이 안되는가? (Spring 트랜잭션의 동작 순서)이 문제의 원인은 Spring의 ..
[개요]디프만 프로젝트인 혼밥 식당 추천 서비스 밥토리에서 PostgreSQL의 pgvector 확장을 사용해 유사 상점 추천 기능을 구현했습니다.초기 프로토타이핑은 네이티브 쿼리(@Query(..., nativeQuery = true))로 진행했고, 엔티티 필드를 double[]로 사용했음에도 쿼리는 잘 작동했습니다.문제는 이 쿼리를 컴파일 시점의 안정성을 확보하기 위해 QueryDSL로 리팩토링하는 순간 발생했습니다. 잘 되던 double[] 타입이 갑자기 FunctionArgumentException을 일으켰습니다.이 글은 이 FunctionArgumentException의 원인을 추적하고, 네이티브 쿼리와 HQL(QueryDSL)의 동작 방식 차이를 이해하며 문제를 해결한 과정을 담은 트러블 슈팅..
[개요]AI 기반 퀴즈 생성 서비스에서 Google Gemini API를 활용하는 기능을 개발했습니다. 사용자가 챌린지를 생성하면, 백엔드 서버가 Gemini API를 호출하여 퀴즈를 생성하고 사용자에게 제공하는 것입니다.문제는 Gemini API가 퀴즈를 생성하는 데 약 1분의 긴 I/O 대기 시간을 소요한다는 점이었습니다. 이로 인해 동시 사용자 요청이 몰릴 경우, 시스템 전체의 처리량이 심각하게 저하되고 사용자 대기 시간이 기하급수적으로 증가하는 병목 현상이 발생했습니다.[요약]핵심 문제: AI 모델의 응답 시간(약 1분) 동안 @Async로 할당된 플랫폼 스레드가 블로킹되어, 한정된 스레드 풀(CorePoolSize: 4)이 금방 소진되어 병목 현상.분석 과정: Java 21 Virtual Thr..
[문제 발견: catch로 잡았는데 왜 500 에러가 발생할까?]좋아요 중복 요청 시 요구사항은 다음과 같았습니다.중복 요청 시 데이터베이스 트랜잭션은 롤백되어야 한다.이벤트 발행 등 후속 로직이 실행되지 않아야 한다.클라이언트에게는 200 OK를 응답해야 한다. (멱등성 보장)초기 코드는 단순한 try-catch 구조였습니다.@Transactionalpublic void createDiaryLike(Long memberId, Long diaryId) { final Member member = memberRepository.findByIdOrElseThrow(memberId); final ReadingDiary readingDiary = readingDiaryRepository.findById..