목록전체 글 (291)
아무튼, 쓰기
최근 프로젝트에서 Spring Boot 3와 Lombok을 조합해 사용하던 중, 의아한 문제를 겪었습니다. 분명히 @Qualifier를 붙였는데, 엉뚱한 빈이 주입되는 현상이었습니다.단순히 생성자를 직접 만들어서 해결했다로 끝내기엔 찜찜했습니다. 도대체 왜 이런 일이 벌어지는지, 스프링 프레임워크 내부 코드를 뜯어보며 그 원리를 꼬리에 꼬리를 무는 질문으로 파헤쳐 보았습니다.상황: 16MB짜리 WebClient가 필요해@Configurationpublic class WebClientConfig { @Bean @Primary public WebClient webClient() { // 일반적인 요청용 (기본 설정) return WebClient.builder() ..
프로젝트 소개안녕하세요! 디프만 15기에서 백엔드 개발자로 참여하여 밥토리(Bobtory)라는 혼밥 식당 추천 서비스를 개발한 경험을 공유하고자 합니다.밥토리 서비스: https://bobtory.com/home프로젝트 상세: https://www.behance.net/gallery/239027413/-Bobtory-혼자 밥 먹는 것이 어색하고 불편한 분들을 위해 기획된 밥토리는, 혼밥 난이도별로 식당을 추천하고 맞춤형 필터링 기능을 제공하는 서비스입니다. 사용자의 혼밥 레벨(Lv.1~4)에 따라 적합한 식당을 추천하며, 좌석 형태, 메뉴 카테고리, 가격대 등 다양한 커스텀 필터를 통해 나에게 딱 맞는 혼밥 장소를 찾을 수 있습니다.프로젝트의 핵심 기능혼밥 레벨별 맞춤 추천: 입문자(편의점 도시락, 패스..
들어가며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..
개요좋아요 기능을 추가했을 뿐인데, NotificationService와 StatisticService까지 수정해야 했습니다. OCP(개방-폐쇄 원칙)가 훼손되는 문제를 해결하기 위해 Spring의 이벤트 기반 아키텍처로 리팩토링했습니다.서비스 계층이 특정 구현체에 직접 의존하는 강한 결합 구조에서 Spring ApplicationEvent 기반의 비동기 이벤트 구조로 전환하는 것이 목표였습니다.그 과정 속에서 Fat Event라는 두 번째 문제를 만났고, 이 문제를 해결하기 위해 최소 ID 원칙을 도입했습니다. 최종적으로는 원칙의 순수성과 시스템 성능 사이에서 실용적인 트레이드오프를 선택하며 해결하였습니다.문제 정의: 왜 리팩토링이 필요했나 (강한 결합과 OCP 위반)기능 요구사항은 간단했습니다. "사..