[{"data":1,"prerenderedAt":1996},["ShallowReactive",2],{"navigation_docs_ko":3,"-ko-project-brobean-brobean-dev-log-13":267,"-ko-project-brobean-brobean-dev-log-13-surround":1991},[4,130,253],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":33},"Note (개발 지식)","emojione-v1:note-pad","\u002Fko\u002Fnote","ko\u002F1.note",[10,34,48,74,84,94,110,120],{"title":11,"icon":12,"path":13,"stem":14,"children":15,"page":33},"Spring","logos:spring-icon","\u002Fko\u002Fnote\u002Fspring","ko\u002F1.note\u002F0.spring",[16,21,25,29],{"title":17,"path":18,"stem":19,"icon":20},"Spring AI 핵심 개념과 LLM 애플리케이션 구조 이해","\u002Fko\u002Fnote\u002Fspring\u002Fspring-ai","ko\u002F1.note\u002F0.spring\u002F1.spring-ai","lucide:file-text",{"title":22,"path":23,"stem":24,"icon":20},"[Java] Record와 @Version으로 이해하는 불변 데이터와 동시성 제어","\u002Fko\u002Fnote\u002Fspring\u002Frecord-and-version-concurrency","ko\u002F1.note\u002F0.spring\u002F2.record-and-version-concurrency",{"title":26,"path":27,"stem":28,"icon":20},"[JPA] @PersistenceContext와 EntityManager로 이해하는 영속성 컨텍스트","\u002Fko\u002Fnote\u002Fspring\u002Fspring-persistence-context","ko\u002F1.note\u002F0.spring\u002F3.spring-persistence-context",{"title":30,"path":31,"stem":32,"icon":20},"[Java] YearMonth로 월 단위 날짜를 다루는 방법","\u002Fko\u002Fnote\u002Fspring\u002Fjava-year-month","ko\u002F1.note\u002F0.spring\u002F4.java-year-month",false,{"title":35,"icon":36,"path":37,"stem":38,"children":39,"page":33},"Javascript","skill-icons:javascript","\u002Fko\u002Fnote\u002Fjavascript","ko\u002F1.note\u002F1.javascript",[40,44],{"title":41,"path":42,"stem":43,"icon":20},"Promise.all과 구조 분해 할당을 활용한 데이터 처리 패턴","\u002Fko\u002Fnote\u002Fjavascript\u002Fdestructuring","ko\u002F1.note\u002F1.javascript\u002F1.destructuring",{"title":45,"path":46,"stem":47,"icon":20},"JavaScript 배열 고차 함수와 데이터 처리 패턴 이해","\u002Fko\u002Fnote\u002Fjavascript\u002Ffilter","ko\u002F1.note\u002F1.javascript\u002F2.filter",{"title":49,"icon":50,"path":51,"stem":52,"children":53,"page":33},"Nuxt4","logos:nuxt-icon","\u002Fko\u002Fnote\u002Fnuxt4","ko\u002F1.note\u002F2.nuxt4",[54,58,62,66,70],{"title":55,"path":56,"stem":57,"icon":20},"Nuxt 4를 선택한 이유와 아키텍처 및 실무 가이드","\u002Fko\u002Fnote\u002Fnuxt4\u002Fnuxt-info","ko\u002F1.note\u002F2.nuxt4\u002F1.nuxt-info",{"title":59,"path":60,"stem":61,"icon":20},"Nuxt useAsyncData 동작 원리와 SSR 데이터 페칭 최적화","\u002Fko\u002Fnote\u002Fnuxt4\u002Fnuxt-use_async_data","ko\u002F1.note\u002F2.nuxt4\u002F2.nuxt-use_async_data",{"title":63,"path":64,"stem":65,"icon":20},"Nuxt Content v3 데이터 모델과 Query 시스템 이해","\u002Fko\u002Fnote\u002Fnuxt4\u002Fnuxt-content-v3","ko\u002F1.note\u002F2.nuxt4\u002F3.nuxt-content-v3",{"title":67,"path":68,"stem":69,"icon":20},"Nuxt Auto-imports 내부 동작과 컴파일 과정 분석","\u002Fko\u002Fnote\u002Fnuxt4\u002Fnuxt-auto-import","ko\u002F1.note\u002F2.nuxt4\u002F4.nuxt-auto-import",{"title":71,"path":72,"stem":73,"icon":20},"Nuxt 렌더링 방식 이해: SSR, SSG, ISR, CSR 구현 방법","\u002Fko\u002Fnote\u002Fnuxt4\u002Fnuxt-rendering-strategy","ko\u002F1.note\u002F2.nuxt4\u002F5.nuxt-rendering-strategy",{"title":75,"icon":76,"path":77,"stem":78,"children":79,"page":33},"Nitro","unjs:nitro","\u002Fko\u002Fnote\u002Fnitro","ko\u002F1.note\u002F3.nitro",[80],{"title":81,"path":82,"stem":83,"icon":20},"Nitro 아키텍처와 서버 엔진 구조","\u002Fko\u002Fnote\u002Fnitro\u002Fnitro","ko\u002F1.note\u002F3.nitro\u002Fnitro",{"title":85,"icon":86,"path":87,"stem":88,"children":89,"page":33},"Devops","vscode-icons:file-type-pipeline","\u002Fko\u002Fnote\u002Fdevops","ko\u002F1.note\u002F4.devops",[90],{"title":91,"path":92,"stem":93,"icon":20},"서버사이드 WebAssembly(Wasm)와 차세대 실행 환경","\u002Fko\u002Fnote\u002Fdevops\u002Fwebassembly","ko\u002F1.note\u002F4.devops\u002F1.webAssembly",{"title":95,"icon":96,"path":97,"stem":98,"children":99,"page":33},"Tooling","lucide:wrench","\u002Fko\u002Fnote\u002Ftooling","ko\u002F1.note\u002F4.tooling",[100],{"title":101,"icon":102,"path":103,"stem":104,"children":105,"page":33},"Package Manager","devicon:npm","\u002Fko\u002Fnote\u002Ftooling\u002Fpackage-manager","ko\u002F1.note\u002F4.tooling\u002F1.package-manager",[106],{"title":107,"path":108,"stem":109,"icon":20},"Package Lock 파일의 역할과 의존성 관리 원리","\u002Fko\u002Fnote\u002Ftooling\u002Fpackage-manager\u002Fpackage-lock","ko\u002F1.note\u002F4.tooling\u002F1.package-manager\u002F1.package-lock",{"title":111,"icon":112,"path":113,"stem":114,"children":115,"page":33},"Web","streamline-plump:web","\u002Fko\u002Fnote\u002Fweb","ko\u002F1.note\u002F4.web",[116],{"title":117,"path":118,"stem":119,"icon":20},"웹 애플리케이션 렌더링 아키텍처 이해","\u002Fko\u002Fnote\u002Fweb\u002Frendering-modes","ko\u002F1.note\u002F4.web\u002F2.rendering-modes",{"title":121,"icon":122,"path":123,"stem":124,"children":125,"page":33},"SEO","icon-park-solid:seo","\u002Fko\u002Fnote\u002Fseo","ko\u002F1.note\u002F6.seo",[126],{"title":127,"path":128,"stem":129,"icon":20},"[SEO] 개발 블로그 검색 노출을 위한 robots.txt와 SEO 설정 가이드","\u002Fko\u002Fnote\u002Fseo\u002Fseo","ko\u002F1.note\u002F6.seo\u002F1.seo",{"title":131,"icon":132,"path":133,"stem":134,"children":135,"page":33},"Project (개발일지)","icon-park-solid:branch-two","\u002Fko\u002Fproject","ko\u002F2.project",[136,187],{"title":137,"icon":138,"path":139,"stem":140,"children":141,"page":33},"Brolog","custom:symbol","\u002Fko\u002Fproject\u002Fbrolog","ko\u002F2.project\u002F1.brolog",[142,146,150,154,158,162,166,170,174,179,183],{"title":143,"path":144,"stem":145,"icon":20},"[Brolog] 프로젝트 개요","\u002Fko\u002Fproject\u002Fbrolog\u002Foutline","ko\u002F2.project\u002F1.brolog\u002F1.outline",{"title":147,"path":148,"stem":149,"icon":20},"[Brolog] Nuxt Multi Layer 프로젝트 Vercel 배포 (3) - Runtime 기능 문제 분석과 AI Assistant 전환","\u002Fko\u002Fproject\u002Fbrolog\u002Fvercel-deploy3-ai-integration","ko\u002F2.project\u002F1.brolog\u002F10.vercel-deploy3-ai-integration",{"title":151,"path":152,"stem":153,"icon":20},"[Brolog] AI Assistant 적용 과정 - Agent 구조와 API 사용량 관리 시 고려사항","\u002Fko\u002Fproject\u002Fbrolog\u002Fai-assistant-application-process","ko\u002F2.project\u002F1.brolog\u002F11.ai-assistant-application-process",{"title":155,"path":156,"stem":157,"icon":20},"[Brolog] 검색 엔진 노출을 위한 SEO 적용 과정","\u002Fko\u002Fproject\u002Fbrolog\u002Fbrolog-seo","ko\u002F2.project\u002F1.brolog\u002F12.brolog-seo",{"title":159,"path":160,"stem":161,"icon":20},"[Brolog] 프로젝트 구조","\u002Fko\u002Fproject\u002Fbrolog\u002Fstructure","ko\u002F2.project\u002F1.brolog\u002F2.structure",{"title":163,"path":164,"stem":165,"icon":20},"[Brolog] Nuxt Content 네비게이션 아이콘 커스텀 빌드","\u002Fko\u002Fproject\u002Fbrolog\u002Fcustom-icon","ko\u002F2.project\u002F1.brolog\u002F3.custom-icon",{"title":167,"path":168,"stem":169,"icon":20},"[Brolog] Nuxt4 Auto-imports 동작 원리와 자동 import 시스템 이해","\u002Fko\u002Fproject\u002Fbrolog\u002Fnuxt-auto-import","ko\u002F2.project\u002F1.brolog\u002F4.nuxt-auto-import",{"title":171,"path":172,"stem":173,"icon":20},"[Brolog] 개발 블로그 메인 페이지 구현","\u002Fko\u002Fproject\u002Fbrolog\u002Fdevelop-main","ko\u002F2.project\u002F1.brolog\u002F5.develop-main",{"title":175,"path":176,"stem":177,"icon":178},"[Brolog] 브랜드 아이덴티티 적용 - Custom 404 페이지와 Default No Image 구현","\u002Fko\u002Fproject\u002Fbrolog\u002Fcustom-error-and-default-thumbnail","ko\u002F2.project\u002F1.brolog\u002F6.custom-error-and-default-thumbnail","lucide:image",{"title":180,"path":181,"stem":182,"icon":20},"[Brolog] Nuxt Multi Layer 프로젝트 Vercel 배포 (1)","\u002Fko\u002Fproject\u002Fbrolog\u002Fvercel-deploy1","ko\u002F2.project\u002F1.brolog\u002F8.vercel-deploy1",{"title":184,"path":185,"stem":186,"icon":20},"[Brolog] Nuxt Multi Layer 프로젝트 Vercel 배포 (2) - Build Pipeline 오류 분석과 Nuxt 실행 구조 이해","\u002Fko\u002Fproject\u002Fbrolog\u002Fvercel-deploy2-error-analyze","ko\u002F2.project\u002F1.brolog\u002F9.vercel-deploy2-error-analyze",{"title":188,"icon":189,"path":190,"stem":191,"children":192,"page":33},"Brobean","custom:brobean","\u002Fko\u002Fproject\u002Fbrobean","ko\u002F2.project\u002F2.brobean",[193,197,201,205,209,213,217,221,225,229,233,237,241,245,249],{"title":194,"path":195,"stem":196,"icon":20},"[BroBean] 연인과 함께 사용하는 가계부: BroBean - 프로젝트 기획과 이름의 탄생","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-01","ko\u002F2.project\u002F2.brobean\u002F1.brobean-dev-log-01",{"title":198,"path":199,"stem":200,"icon":20},"[BroBean] 연인과 함께 사용하는 가계부: BroBean 프로젝트 시작 - Nuxt 4 기반 프론트 개발 환경 구축","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-02","ko\u002F2.project\u002F2.brobean\u002F2.brobean-dev-log-02",{"title":202,"path":203,"stem":204,"icon":20},"[BroBean] BroBean 프로젝트 #3 - 모바일 앱 전환을 고려한 공통 레이아웃 설계","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-03","ko\u002F2.project\u002F2.brobean\u002F3.brobean-dev-log-03",{"title":206,"path":207,"stem":208,"icon":20},"[BroBean] BroBean 프로젝트 #4 - 홈 화면(Dashboard) 설계와 Mock 데이터 구성","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-04","ko\u002F2.project\u002F2.brobean\u002F4.brobean-dev-log-04",{"title":210,"path":211,"stem":212,"icon":20},"[BroBean] BroBean 프로젝트 #5 돈을 모으면 캐릭터도 성장한다 - 브로빈 성장 시스템 설계","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-05","ko\u002F2.project\u002F2.brobean\u002F5.brobean-dev-log-05",{"title":214,"path":215,"stem":216,"icon":20},"[BroBean] BroBean 프로젝트 #6 씨앗 심기와 밭 가꾸기 기능 설계","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-06","ko\u002F2.project\u002F2.brobean\u002F6.brobean-dev-log-06",{"title":218,"path":219,"stem":220,"icon":20},"[BroBean] BroBean 프로젝트 #7 성장 일지와 나무 키우기 화면 구현","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-07","ko\u002F2.project\u002F2.brobean\u002F7.brobean-dev-log-07",{"title":222,"path":223,"stem":224,"icon":20},"[BroBean] BroBean 프로젝트 #8 수확 정산으로 한눈에 보는 소비 흐름 - 소비 리포트 & 정산 화면 구현","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-08","ko\u002F2.project\u002F2.brobean\u002F8.brobean-dev-log-08",{"title":226,"path":227,"stem":228,"icon":20},"[BroBean] BroBean 프로젝트 #9 알림부터 마이페이지까지 - 사용자 편의 기능과 개인 농장 설정 화면 구현","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-09","ko\u002F2.project\u002F2.brobean\u002F9.brobean-dev-log-09",{"title":230,"path":231,"stem":232,"icon":20},"[BroBean] BroBean 프로젝트 #10 로그인부터 파트너 초대까지 - 계정 인증과 커플 연결 화면 구현","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-10","ko\u002F2.project\u002F2.brobean\u002F910.brobean-dev-log-10",{"title":234,"path":235,"stem":236,"icon":20},"[BroBean] BroBean 프로젝트 #11 개인과 커플을 모두 위한 모드 분리 - Pinia 기반 개인\u002F커플 모드 전환 구현","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-11","ko\u002F2.project\u002F2.brobean\u002F911.brobean-dev-log-11",{"title":238,"path":239,"stem":240,"icon":20},"[BroBean] BroBean 프로젝트 #12 인증 상태 복구와 Nuxt Plugin 실행 순서 제어","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-12","ko\u002F2.project\u002F2.brobean\u002F912.brobean-dev-log-12",{"title":242,"path":243,"stem":244,"icon":20},"[BroBean] BroBean 프로젝트 #13 개인·커플 모드와 가계부 API 구현 - 쿼리 성능을 다시 돌아보다","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-13","ko\u002F2.project\u002F2.brobean\u002F913.brobean-dev-log-13",{"title":246,"path":247,"stem":248,"icon":20},"[BroBean] BroBean 프로젝트 #14 순환 참조 오류를 ObjectProvider로 해결하기","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-14","ko\u002F2.project\u002F2.brobean\u002F914.brobean-dev-log-14",{"title":250,"path":251,"stem":252,"icon":20},"[BroBean] BroBean 프로젝트 #15 JPA 페이지네이션 적용, Page와 Slice의 차이","\u002Fko\u002Fproject\u002Fbrobean\u002Fbrobean-dev-log-15","ko\u002F2.project\u002F2.brobean\u002F915.brobean-dev-log-15",{"title":254,"icon":255,"path":256,"stem":257,"children":258,"page":33},"Error (트러블슈팅)","emojione:stop-sign","\u002Fko\u002Ftrouble-shooting","ko\u002F3.trouble-shooting",[259],{"title":137,"icon":138,"path":260,"stem":261,"children":262,"page":33},"\u002Fko\u002Ftrouble-shooting\u002Fbrolog","ko\u002F3.trouble-shooting\u002F1.brolog",[263],{"title":264,"path":265,"stem":266,"icon":20},"[Nuxt Content] Nuxt Icon 오류 해결과 Custom Collection 기반 오프라인 아이콘 구성","\u002Fko\u002Ftrouble-shooting\u002Fbrolog\u002Fnuxt-icon-restricted-network-troubleshooting","ko\u002F3.trouble-shooting\u002F1.brolog\u002F1.nuxt-icon-restricted-network-troubleshooting",{"id":268,"title":242,"body":269,"date":1971,"description":1972,"extension":1973,"links":1974,"meta":1975,"navigation":1986,"path":243,"seo":1987,"stem":244,"__hash__":1990},"docs_ko\u002Fko\u002F2.project\u002F2.brobean\u002F913.brobean-dev-log-13.md",{"type":270,"value":271,"toc":1966},"minimark",[272,1962],[273,274,276,281,290,293,301,312,319,322,326,329,332,342,345,351,354,361,363,367,370,377,380,408,411,414,417,420,426,429,432,434,438,441,444,932,935,938,1002,1005,1008,1019,1022,1025,1027,1031,1034,1040,1043,1049,1052,1059,1136,1139,1145,1148,1151,1200,1203,1230,1233,1240,1242,1246,1249,1259,1262,1268,1271,1277,1280,1286,1289,1292,1295,1298,1304,1307,1310,1317,1319,1323,1326,1329,1332,1335,1341,1344,1347,1353,1356,1359,1362,1368,1371,1374,1377,1383,1386,1388,1392,1395,1398,1446,1449,1475,1478,1481,1484,1487,1490,1496,1499,1505,1507,1511,1517,1581,1584,1591,1593,1599,1602,1605,1681,1684,1690,1693,1696,1702,1705,1707,1711,1714,1718,1724,1727,1730,1736,1739,1743,1749,1752,1759,1761,1765,1768,1774,1777,1825,1828,1831,1895,1898,1924,1927,1930,1936,1939,1942,1945,1952,1955],"u-page-card",{":spotlight":275},"true",[277,278,280],"h1",{"id":279},"개인커플-모드와-가계부-api-구현-그리고-쿼리-성능을-다시-돌아보다","🌱 개인·커플 모드와 가계부 API 구현, 그리고 쿼리 성능을 다시 돌아보다",[282,283,284,285,289],"p",{},"앞선 글에서는 BroBean을 개인 사용자도 사용할 수 있도록 ",[286,287,288],"strong",{},"개인 모드와 커플 모드를 분리하는 작업","을 진행했다.",[282,291,292],{},"이번에는 이 구조를 실제 백엔드 기능에 연결했다.",[282,294,295,296,300],{},"사용자가 어떤 모드를 사용하고 있는지 ",[297,298,299],"code",{},"UserSetting","을 통해 관리하고, 마지막으로 선택했던 모드와 관련 설정값을 저장할 수 있도록 작업했다.",[282,302,303,304,307,308,311],{},"그리고 본격적으로 가계부 기능을 백엔드와 연결하면서 수입·지출·저축을 등록하는 ",[286,305,306],{},"'씨앗심기'",", 등록된 내역을 조회하는 ",[286,309,310],{},"'성장일지'"," API를 구현하고 프론트까지 연결했다.",[282,313,314,315,318],{},"기능적으로는 이전보다 꽤 많은 부분이 진행됐지만, 이번 작업에서 개인적으로 가장 기억에 남은 부분은 기능 자체보다 ",[286,316,317],{},"QueryDSL로 조회 쿼리를 작성하면서 성능에 대해 다시 생각하게 된 과정","이었다.",[320,321],"hr",{},[277,323,325],{"id":324},"️-개인커플-모드를-실제-데이터와-연결하기","⚙️ 개인·커플 모드를 실제 데이터와 연결하기",[282,327,328],{},"개인과 커플이 모두 사용할 수 있도록 방향을 변경하면서 단순히 화면에 모드 전환 버튼만 추가해서는 해결되지 않았다.",[282,330,331],{},"농장뿐만 아니라 실제 데이터를 사용하는 대부분의 기능이 개인과 커플을 구분해야 하기 때문이다.",[333,334,340],"pre",{"className":335,"code":337,"language":338,"meta":339},[336],"language-text","농장\n가계부\n저축 목표\n챌린지\n예산\n고정지출\n","text","",[297,341,337],{"__ignoreMap":339},[282,343,344],{},"이런 데이터들이 현재 사용자의 모드에 따라 다르게 조회되어야 한다.",[282,346,347,348,350],{},"그래서 ",[297,349,299],{}," 영역에서 개인 \u002F 커플 모드와 마지막 전환 모드, 기타 사용자 설정값을 관리하고, 실제 데이터 조회 시에도 현재 모드를 기준으로 데이터를 구분할 수 있도록 백엔드 작업을 진행했다.",[282,352,353],{},"프론트에서는 Pinia Store가 현재 모드를 관리하고, 백엔드에서는 사용자 설정을 기준으로 상태를 유지하는 방식이다.",[282,355,356,357,360],{},"결국 하나의 모드 전환 기능이 단순한 UI 상태가 아니라 ",[286,358,359],{},"서비스 전체 데이터 조회의 기준","으로 확장되고 있다.",[320,362],{},[277,364,366],{"id":365},"씨앗심기와-성장일지-api","🌱 씨앗심기와 성장일지 API",[282,368,369],{},"가계부 기능도 본격적으로 백엔드와 연결했다.",[282,371,372,373,376],{},"BroBean에서는 수입·지출·저축을 추가하는 기능을 ",[286,374,375],{},"씨앗심기","라고 표현하고 있다.",[282,378,379],{},"실제 백엔드에서는 하나의 거래 데이터로 관리하며 타입을:",[333,381,385],{"className":382,"code":383,"language":384,"meta":339,"style":339},"language-java shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","EXPENSE\nINCOME\nSAVING\n","java",[297,386,387,396,402],{"__ignoreMap":339},[388,389,392],"span",{"class":390,"line":391},"line",1,[388,393,395],{"class":394},"spNyl","EXPENSE\n",[388,397,399],{"class":390,"line":398},2,[388,400,401],{"class":394},"INCOME\n",[388,403,405],{"class":390,"line":404},3,[388,406,407],{"class":394},"SAVING\n",[282,409,410],{},"으로 구분한다.",[282,412,413],{},"사용자가 입력한 거래는 개인 \u002F 커플 모드와 함께 저장되고, 이후 성장일지에서 해당 데이터를 다시 조회한다.",[282,415,416],{},"성장일지는 실제로는 가계부 내역 조회 기능이다.",[282,418,419],{},"예를 들어 사용자가 2026년 8월을 선택하면:",[333,421,424],{"className":422,"code":423,"language":338,"meta":339},[336],"지출   1,250,000원\n수입   3,500,000원\n저축     800,000원\n",[297,425,423],{"__ignoreMap":339},[282,427,428],{},"과 같은 월별 요약 정보와 함께 해당 월의 거래 내역을 보여준다.",[282,430,431],{},"이 부분까지 백엔드 API를 구현하고 프론트와 연결하면서 이제 실제로 데이터를 입력하고 다시 조회하는 흐름이 만들어졌다.",[320,433],{},[277,435,437],{"id":436},"월별-조회-쿼리를-작성하면서-생긴-고민","🔎 월별 조회 쿼리를 작성하면서 생긴 고민",[282,439,440],{},"성장일지에서 월별 요약 정보를 조회하기 위해 QueryDSL을 사용했다.",[282,442,443],{},"지출, 수입, 저축을 각각 별도로 조회하지 않고 하나의 쿼리에서 조건별 합계를 계산하도록 구성했다.",[333,445,447],{"className":382,"code":446,"language":384,"meta":339,"style":339},"queryFactory\n        .select(Projections.constructor(\n                TransactionDto.SummaryResponse.class,\n\n                \u002F\u002F 지출 합계\n                new CaseBuilder()\n                        .when(transaction.type.eq(TransactionType.EXPENSE))\n                        .then(transaction.amount)\n                        .otherwise(0L)\n                        .sum()\n                        .coalesce(0L),\n\n                \u002F\u002F 수입 합계\n                new CaseBuilder()\n                        .when(transaction.type.eq(TransactionType.INCOME))\n                        .then(transaction.amount)\n                        .otherwise(0L)\n                        .sum()\n                        .coalesce(0L),\n\n                \u002F\u002F 저축 합계\n                new CaseBuilder()\n                        .when(transaction.type.eq(TransactionType.SAVING))\n                        .then(transaction.amount)\n                        .otherwise(0L)\n                        .sum()\n                        .coalesce(0L)\n        ))\n        .from(transaction)\n        .where(\n                eqCoupleOrEmail(recordMode, couple, email)\n                        .and(transaction.date.between(startDate, endDate))\n        )\n        .fetchOne();\n",[297,448,449,455,480,498,505,512,525,562,582,598,608,623,628,634,643,673,690,703,712,725,730,736,745,775,792,805,814,827,833,847,857,881,915,921],{"__ignoreMap":339},[388,450,451],{"class":390,"line":391},[388,452,454],{"class":453},"sTEyZ","queryFactory\n",[388,456,457,461,465,468,471,474,477],{"class":390,"line":398},[388,458,460],{"class":459},"sMK4o","        .",[388,462,464],{"class":463},"s2Zo4","select",[388,466,467],{"class":459},"(",[388,469,470],{"class":453},"Projections",[388,472,473],{"class":459},".",[388,475,476],{"class":463},"constructor",[388,478,479],{"class":459},"(\n",[388,481,482,485,487,490,492,495],{"class":390,"line":404},[388,483,484],{"class":453},"                TransactionDto",[388,486,473],{"class":459},[388,488,489],{"class":453},"SummaryResponse",[388,491,473],{"class":459},[388,493,494],{"class":453},"class",[388,496,497],{"class":459},",\n",[388,499,501],{"class":390,"line":500},4,[388,502,504],{"emptyLinePlaceholder":503},true,"\n",[388,506,508],{"class":390,"line":507},5,[388,509,511],{"class":510},"sHwdD","                \u002F\u002F 지출 합계\n",[388,513,515,519,522],{"class":390,"line":514},6,[388,516,518],{"class":517},"s7zQu","                new",[388,520,521],{"class":463}," CaseBuilder",[388,523,524],{"class":459},"()\n",[388,526,528,531,534,536,539,541,544,546,549,551,554,556,559],{"class":390,"line":527},7,[388,529,530],{"class":459},"                        .",[388,532,533],{"class":463},"when",[388,535,467],{"class":459},[388,537,538],{"class":453},"transaction",[388,540,473],{"class":459},[388,542,543],{"class":453},"type",[388,545,473],{"class":459},[388,547,548],{"class":463},"eq",[388,550,467],{"class":459},[388,552,553],{"class":453},"TransactionType",[388,555,473],{"class":459},[388,557,558],{"class":453},"EXPENSE",[388,560,561],{"class":459},"))\n",[388,563,565,567,570,572,574,576,579],{"class":390,"line":564},8,[388,566,530],{"class":459},[388,568,569],{"class":463},"then",[388,571,467],{"class":459},[388,573,538],{"class":453},[388,575,473],{"class":459},[388,577,578],{"class":453},"amount",[388,580,581],{"class":459},")\n",[388,583,585,587,590,592,596],{"class":390,"line":584},9,[388,586,530],{"class":459},[388,588,589],{"class":463},"otherwise",[388,591,467],{"class":459},[388,593,595],{"class":594},"sbssI","0L",[388,597,581],{"class":459},[388,599,601,603,606],{"class":390,"line":600},10,[388,602,530],{"class":459},[388,604,605],{"class":463},"sum",[388,607,524],{"class":459},[388,609,611,613,616,618,620],{"class":390,"line":610},11,[388,612,530],{"class":459},[388,614,615],{"class":463},"coalesce",[388,617,467],{"class":459},[388,619,595],{"class":594},[388,621,622],{"class":459},"),\n",[388,624,626],{"class":390,"line":625},12,[388,627,504],{"emptyLinePlaceholder":503},[388,629,631],{"class":390,"line":630},13,[388,632,633],{"class":510},"                \u002F\u002F 수입 합계\n",[388,635,637,639,641],{"class":390,"line":636},14,[388,638,518],{"class":517},[388,640,521],{"class":463},[388,642,524],{"class":459},[388,644,646,648,650,652,654,656,658,660,662,664,666,668,671],{"class":390,"line":645},15,[388,647,530],{"class":459},[388,649,533],{"class":463},[388,651,467],{"class":459},[388,653,538],{"class":453},[388,655,473],{"class":459},[388,657,543],{"class":453},[388,659,473],{"class":459},[388,661,548],{"class":463},[388,663,467],{"class":459},[388,665,553],{"class":453},[388,667,473],{"class":459},[388,669,670],{"class":453},"INCOME",[388,672,561],{"class":459},[388,674,676,678,680,682,684,686,688],{"class":390,"line":675},16,[388,677,530],{"class":459},[388,679,569],{"class":463},[388,681,467],{"class":459},[388,683,538],{"class":453},[388,685,473],{"class":459},[388,687,578],{"class":453},[388,689,581],{"class":459},[388,691,693,695,697,699,701],{"class":390,"line":692},17,[388,694,530],{"class":459},[388,696,589],{"class":463},[388,698,467],{"class":459},[388,700,595],{"class":594},[388,702,581],{"class":459},[388,704,706,708,710],{"class":390,"line":705},18,[388,707,530],{"class":459},[388,709,605],{"class":463},[388,711,524],{"class":459},[388,713,715,717,719,721,723],{"class":390,"line":714},19,[388,716,530],{"class":459},[388,718,615],{"class":463},[388,720,467],{"class":459},[388,722,595],{"class":594},[388,724,622],{"class":459},[388,726,728],{"class":390,"line":727},20,[388,729,504],{"emptyLinePlaceholder":503},[388,731,733],{"class":390,"line":732},21,[388,734,735],{"class":510},"                \u002F\u002F 저축 합계\n",[388,737,739,741,743],{"class":390,"line":738},22,[388,740,518],{"class":517},[388,742,521],{"class":463},[388,744,524],{"class":459},[388,746,748,750,752,754,756,758,760,762,764,766,768,770,773],{"class":390,"line":747},23,[388,749,530],{"class":459},[388,751,533],{"class":463},[388,753,467],{"class":459},[388,755,538],{"class":453},[388,757,473],{"class":459},[388,759,543],{"class":453},[388,761,473],{"class":459},[388,763,548],{"class":463},[388,765,467],{"class":459},[388,767,553],{"class":453},[388,769,473],{"class":459},[388,771,772],{"class":453},"SAVING",[388,774,561],{"class":459},[388,776,778,780,782,784,786,788,790],{"class":390,"line":777},24,[388,779,530],{"class":459},[388,781,569],{"class":463},[388,783,467],{"class":459},[388,785,538],{"class":453},[388,787,473],{"class":459},[388,789,578],{"class":453},[388,791,581],{"class":459},[388,793,795,797,799,801,803],{"class":390,"line":794},25,[388,796,530],{"class":459},[388,798,589],{"class":463},[388,800,467],{"class":459},[388,802,595],{"class":594},[388,804,581],{"class":459},[388,806,808,810,812],{"class":390,"line":807},26,[388,809,530],{"class":459},[388,811,605],{"class":463},[388,813,524],{"class":459},[388,815,817,819,821,823,825],{"class":390,"line":816},27,[388,818,530],{"class":459},[388,820,615],{"class":463},[388,822,467],{"class":459},[388,824,595],{"class":594},[388,826,581],{"class":459},[388,828,830],{"class":390,"line":829},28,[388,831,832],{"class":459},"        ))\n",[388,834,836,838,841,843,845],{"class":390,"line":835},29,[388,837,460],{"class":459},[388,839,840],{"class":463},"from",[388,842,467],{"class":459},[388,844,538],{"class":453},[388,846,581],{"class":459},[388,848,850,852,855],{"class":390,"line":849},30,[388,851,460],{"class":459},[388,853,854],{"class":463},"where",[388,856,479],{"class":459},[388,858,860,863,865,868,871,874,876,879],{"class":390,"line":859},31,[388,861,862],{"class":463},"                eqCoupleOrEmail",[388,864,467],{"class":459},[388,866,867],{"class":453},"recordMode",[388,869,870],{"class":459},",",[388,872,873],{"class":453}," couple",[388,875,870],{"class":459},[388,877,878],{"class":453}," email",[388,880,581],{"class":459},[388,882,884,886,889,891,893,895,898,900,903,905,908,910,913],{"class":390,"line":883},32,[388,885,530],{"class":459},[388,887,888],{"class":463},"and",[388,890,467],{"class":459},[388,892,538],{"class":453},[388,894,473],{"class":459},[388,896,897],{"class":453},"date",[388,899,473],{"class":459},[388,901,902],{"class":463},"between",[388,904,467],{"class":459},[388,906,907],{"class":453},"startDate",[388,909,870],{"class":459},[388,911,912],{"class":453}," endDate",[388,914,561],{"class":459},[388,916,918],{"class":390,"line":917},33,[388,919,920],{"class":459},"        )\n",[388,922,924,926,929],{"class":390,"line":923},34,[388,925,460],{"class":459},[388,927,928],{"class":463},"fetchOne",[388,930,931],{"class":459},"();\n",[282,933,934],{},"여기서 처음 작성할 때는 월을 선택한다는 화면 요구사항을 그대로 쿼리에도 반영하려고 했다.",[282,936,937],{},"예를 들어:",[333,939,941],{"className":382,"code":940,"language":384,"meta":339,"style":339},".and(transaction.date.year().eq(year))\n.and(transaction.date.month().eq(month))\n",[297,942,943,973],{"__ignoreMap":339},[388,944,945,947,949,951,953,955,957,959,962,965,967,969,971],{"class":390,"line":391},[388,946,473],{"class":459},[388,948,888],{"class":463},[388,950,467],{"class":459},[388,952,538],{"class":453},[388,954,473],{"class":459},[388,956,897],{"class":453},[388,958,473],{"class":459},[388,960,961],{"class":463},"year",[388,963,964],{"class":459},"().",[388,966,548],{"class":463},[388,968,467],{"class":459},[388,970,961],{"class":453},[388,972,561],{"class":459},[388,974,975,977,979,981,983,985,987,989,992,994,996,998,1000],{"class":390,"line":398},[388,976,473],{"class":459},[388,978,888],{"class":463},[388,980,467],{"class":459},[388,982,538],{"class":453},[388,984,473],{"class":459},[388,986,897],{"class":453},[388,988,473],{"class":459},[388,990,991],{"class":463},"month",[388,993,964],{"class":459},[388,995,548],{"class":463},[388,997,467],{"class":459},[388,999,991],{"class":453},[388,1001,561],{"class":459},[282,1003,1004],{},"같은 방식이다.",[282,1006,1007],{},"논리적으로는 전혀 문제가 없다.",[282,1009,1010,1011,1014,1015,1018],{},"2026년 8월을 선택하면 ",[297,1012,1013],{},"year = 2026",", ",[297,1016,1017],{},"month = 8","을 조건으로 주면 해당 데이터를 가져올 수 있기 때문이다.",[282,1020,1021],{},"그런데 다시 DB 관점에서 생각해보니 굳이 데이터베이스에서 날짜의 연도와 월을 추출할 필요가 없었다.",[282,1023,1024],{},"화면에서 월을 선택하는 것과 DB가 데이터를 조회하는 방식은 별개의 문제이기 때문이다.",[320,1026],{},[277,1028,1030],{"id":1029},"월이라는-값을-날짜-범위로-바꾸기","📅 월이라는 값을 날짜 범위로 바꾸기",[282,1032,1033],{},"사용자가 선택한 값이:",[333,1035,1038],{"className":1036,"code":1037,"language":338,"meta":339},[336],"2026-08\n",[297,1039,1037],{"__ignoreMap":339},[282,1041,1042],{},"이라면 결국 DB에서 필요한 조건은:",[333,1044,1047],{"className":1045,"code":1046,"language":338,"meta":339},[336],"2026-08-01\n~\n2026-08-31\n",[297,1048,1046],{"__ignoreMap":339},[282,1050,1051],{},"이다.",[282,1053,1054,1055,1058],{},"그래서 Java의 ",[297,1056,1057],{},"YearMonth","를 사용해 애플리케이션에서 먼저 조회 범위를 만들도록 변경했다.",[333,1060,1062],{"className":382,"code":1061,"language":384,"meta":339,"style":339},"YearMonth ym = YearMonth.parse(month);\n\nLocalDate startDate = ym.atDay(1);\nLocalDate endDate = ym.atEndOfMonth();\n",[297,1063,1064,1089,1093,1118],{"__ignoreMap":339},[388,1065,1066,1068,1071,1074,1077,1079,1082,1084,1086],{"class":390,"line":391},[388,1067,1057],{"class":394},[388,1069,1070],{"class":453}," ym ",[388,1072,1073],{"class":459},"=",[388,1075,1076],{"class":453}," YearMonth",[388,1078,473],{"class":459},[388,1080,1081],{"class":463},"parse",[388,1083,467],{"class":459},[388,1085,991],{"class":453},[388,1087,1088],{"class":459},");\n",[388,1090,1091],{"class":390,"line":398},[388,1092,504],{"emptyLinePlaceholder":503},[388,1094,1095,1098,1101,1103,1106,1108,1111,1113,1116],{"class":390,"line":404},[388,1096,1097],{"class":394},"LocalDate",[388,1099,1100],{"class":453}," startDate ",[388,1102,1073],{"class":459},[388,1104,1105],{"class":453}," ym",[388,1107,473],{"class":459},[388,1109,1110],{"class":463},"atDay",[388,1112,467],{"class":459},[388,1114,1115],{"class":594},"1",[388,1117,1088],{"class":459},[388,1119,1120,1122,1125,1127,1129,1131,1134],{"class":390,"line":500},[388,1121,1097],{"class":394},[388,1123,1124],{"class":453}," endDate ",[388,1126,1073],{"class":459},[388,1128,1105],{"class":453},[388,1130,473],{"class":459},[388,1132,1133],{"class":463},"atEndOfMonth",[388,1135,931],{"class":459},[282,1137,1138],{},"그러면:",[333,1140,1143],{"className":1141,"code":1142,"language":338,"meta":339},[336],"2026-08\n\n↓\n\n2026-08-01\n2026-08-31\n\n↓\n\ntransaction.date.between(startDate, endDate)\n",[297,1144,1142],{"__ignoreMap":339},[282,1146,1147],{},"와 같은 흐름으로 조회할 수 있다.",[282,1149,1150],{},"결국 처음 생각했던:",[333,1152,1154],{"className":382,"code":1153,"language":384,"meta":339,"style":339},"transaction.date.year().eq(year)\ntransaction.date.month().eq(month)\n",[297,1155,1156,1178],{"__ignoreMap":339},[388,1157,1158,1160,1162,1164,1166,1168,1170,1172,1174,1176],{"class":390,"line":391},[388,1159,538],{"class":453},[388,1161,473],{"class":459},[388,1163,897],{"class":453},[388,1165,473],{"class":459},[388,1167,961],{"class":463},[388,1169,964],{"class":459},[388,1171,548],{"class":463},[388,1173,467],{"class":459},[388,1175,961],{"class":453},[388,1177,581],{"class":459},[388,1179,1180,1182,1184,1186,1188,1190,1192,1194,1196,1198],{"class":390,"line":398},[388,1181,538],{"class":453},[388,1183,473],{"class":459},[388,1185,897],{"class":453},[388,1187,473],{"class":459},[388,1189,991],{"class":463},[388,1191,964],{"class":459},[388,1193,548],{"class":463},[388,1195,467],{"class":459},[388,1197,991],{"class":453},[388,1199,581],{"class":459},[282,1201,1202],{},"대신:",[333,1204,1206],{"className":382,"code":1205,"language":384,"meta":339,"style":339},"transaction.date.between(startDate, endDate)\n",[297,1207,1208],{"__ignoreMap":339},[388,1209,1210,1212,1214,1216,1218,1220,1222,1224,1226,1228],{"class":390,"line":391},[388,1211,538],{"class":453},[388,1213,473],{"class":459},[388,1215,897],{"class":453},[388,1217,473],{"class":459},[388,1219,902],{"class":463},[388,1221,467],{"class":459},[388,1223,907],{"class":453},[388,1225,870],{"class":459},[388,1227,912],{"class":453},[388,1229,581],{"class":459},[282,1231,1232],{},"를 사용하게 됐다.",[282,1234,1235,1236,1239],{},"이렇게 하면 DB에는 연도와 월을 계산하라는 조건이 아니라 ",[286,1237,1238],{},"날짜 컬럼 자체에 대한 범위 조건","을 전달하게 된다.",[320,1241],{},[277,1243,1245],{"id":1244},"알고-있던-내용인데-왜-처음에는-놓쳤을까","📈 알고 있던 내용인데 왜 처음에는 놓쳤을까?",[282,1247,1248],{},"사실 이 부분이 이번 작업에서 가장 많이 생각해본 부분이다.",[282,1250,1251,1254,1255,1258],{},[297,1252,1253],{},"YEAR()","나 ",[297,1256,1257],{},"MONTH()"," 같은 함수를 컬럼에 적용하는 것보다, 인덱스를 고려한다면 컬럼 자체에 범위 조건을 사용하는 것이 유리할 수 있다는 것은 이미 알고 있던 내용이었다.",[282,1260,1261],{},"그런데 막상 실제 기능을 구현하다 보니:",[333,1263,1266],{"className":1264,"code":1265,"language":338,"meta":339},[336],"화면에서 월을 선택한다.\n\n↓\n\nyear와 month를 받는다.\n\n↓\n\n그대로 DB 조건으로 사용한다.\n",[297,1267,1265],{"__ignoreMap":339},[282,1269,1270],{},"라는 식으로 생각하고 있었다.",[282,1272,1273,1274,318],{},"돌이켜보면 너무 자연스럽게 ",[286,1275,1276],{},"화면의 요구사항을 DB 조회 조건으로 그대로 옮긴 것",[282,1278,1279],{},"하지만 실제로는:",[333,1281,1284],{"className":1282,"code":1283,"language":338,"meta":339},[336],"화면\n\n\"2026년 8월을 보여주세요.\"\n\n↓\n\n애플리케이션\n\n\"2026-08-01 ~ 2026-08-31로 변환\"\n\n↓\n\nDB\n\n\"이 날짜 범위의 데이터를 조회\"\n",[297,1285,1283],{"__ignoreMap":339},[282,1287,1288],{},"처럼 각 계층에서 필요한 형태로 변환해주는 것이 더 적절했다.",[282,1290,1291],{},"특히 날짜 컬럼에 인덱스가 존재한다면 범위 조건은 인덱스를 활용할 수 있는 형태가 된다.",[282,1293,1294],{},"반면 컬럼에 함수를 적용하는 조건은 DBMS와 실행 계획에 따라 인덱스 활용에 불리할 수 있다.",[282,1296,1297],{},"물론 이것을 단순히:",[333,1299,1302],{"className":1300,"code":1301,"language":338,"meta":339},[336],"YEAR \u002F MONTH = 나쁨\nBETWEEN = 좋음\n",[297,1303,1301],{"__ignoreMap":339},[282,1305,1306],{},"으로 이해해서는 안 된다.",[282,1308,1309],{},"실제 성능은 사용하는 DBMS, 인덱스 구성, 데이터 분포, 실행 계획 등에 따라 달라진다.",[282,1311,1312,1313,1316],{},"중요한 것은 ",[286,1314,1315],{},"쿼리가 실제 DB에서 어떤 형태로 실행되는지까지 생각하는 것","이라고 봤다.",[320,1318],{},[277,1320,1322],{"id":1321},"기능이-동작하는-것과-좋은-쿼리를-작성하는-것은-다르다","🧠 기능이 동작하는 것과 좋은 쿼리를 작성하는 것은 다르다",[282,1324,1325],{},"이번 경험을 통해 다시 느낀 부분이다.",[282,1327,1328],{},"처음 작성했던 쿼리도 기능적으로는 정상적으로 동작한다.",[282,1330,1331],{},"데이터도 원하는 결과가 나온다.",[282,1333,1334],{},"하지만:",[333,1336,1339],{"className":1337,"code":1338,"language":338,"meta":339},[336],"정상적으로 동작한다.\n\n≠\n\n가장 좋은 방식으로 동작한다.\n",[297,1340,1338],{"__ignoreMap":339},[282,1342,1343],{},"는 점을 다시 생각하게 됐다.",[282,1345,1346],{},"특히 개발을 하다 보면 기능을 먼저 완성하는 데 집중하면서:",[333,1348,1351],{"className":1349,"code":1350,"language":338,"meta":339},[336],"이 데이터가 원하는 대로 조회되는가?\n\n이 API가 정상적으로 응답하는가?\n\n프론트에서 제대로 표시되는가?\n",[297,1352,1350],{"__ignoreMap":339},[282,1354,1355],{},"까지만 확인하고 넘어가기 쉽다.",[282,1357,1358],{},"하지만 실제 서비스에서는 데이터가 계속 쌓인다.",[282,1360,1361],{},"지금은 거래 데이터가 몇 백 건에 불과하더라도 나중에는:",[333,1363,1366],{"className":1364,"code":1365,"language":338,"meta":339},[336],"수천 건\n수만 건\n수십만 건\n",[297,1367,1365],{"__ignoreMap":339},[282,1369,1370],{},"으로 늘어날 수 있다.",[282,1372,1373],{},"그때도 동일한 쿼리가 효율적으로 동작할지는 별개의 문제다.",[282,1375,1376],{},"그래서 기능 구현 단계에서도 최소한:",[333,1378,1381],{"className":1379,"code":1380,"language":338,"meta":339},[336],"어떤 조건으로 조회하는가?\n\n불필요한 계산을 DB에서 하고 있지는 않은가?\n\n인덱스를 활용할 수 있는 형태인가?\n\n조회 범위가 필요 이상으로 넓지는 않은가?\n",[297,1382,1380],{"__ignoreMap":339},[282,1384,1385],{},"정도는 함께 생각해야 한다는 것을 다시 느꼈다.",[320,1387],{},[277,1389,1391],{"id":1390},"작은-차이를-미리-고민하는-습관","🔬 작은 차이를 미리 고민하는 습관",[282,1393,1394],{},"이번 작업에서 특별히 복잡한 성능 최적화를 한 것은 아니다.",[282,1396,1397],{},"오히려:",[333,1399,1400],{"className":382,"code":1153,"language":384,"meta":339,"style":339},[297,1401,1402,1424],{"__ignoreMap":339},[388,1403,1404,1406,1408,1410,1412,1414,1416,1418,1420,1422],{"class":390,"line":391},[388,1405,538],{"class":453},[388,1407,473],{"class":459},[388,1409,897],{"class":453},[388,1411,473],{"class":459},[388,1413,961],{"class":463},[388,1415,964],{"class":459},[388,1417,548],{"class":463},[388,1419,467],{"class":459},[388,1421,961],{"class":453},[388,1423,581],{"class":459},[388,1425,1426,1428,1430,1432,1434,1436,1438,1440,1442,1444],{"class":390,"line":398},[388,1427,538],{"class":453},[388,1429,473],{"class":459},[388,1431,897],{"class":453},[388,1433,473],{"class":459},[388,1435,991],{"class":463},[388,1437,964],{"class":459},[388,1439,548],{"class":463},[388,1441,467],{"class":459},[388,1443,991],{"class":453},[388,1445,581],{"class":459},[282,1447,1448],{},"에서",[333,1450,1451],{"className":382,"code":1205,"language":384,"meta":339,"style":339},[297,1452,1453],{"__ignoreMap":339},[388,1454,1455,1457,1459,1461,1463,1465,1467,1469,1471,1473],{"class":390,"line":391},[388,1456,538],{"class":453},[388,1458,473],{"class":459},[388,1460,897],{"class":453},[388,1462,473],{"class":459},[388,1464,902],{"class":463},[388,1466,467],{"class":459},[388,1468,907],{"class":453},[388,1470,870],{"class":459},[388,1472,912],{"class":453},[388,1474,581],{"class":459},[282,1476,1477],{},"로 바꾼 정도다.",[282,1479,1480],{},"하지만 이런 작은 차이를 개발 단계에서 발견하고 고민하는 과정 자체가 의미 있다고 생각한다.",[282,1482,1483],{},"서비스 규모가 작을 때는 두 방식의 차이가 눈에 띄지 않을 수 있다.",[282,1485,1486],{},"하지만 데이터가 커지고 조회가 반복되기 시작하면 작은 쿼리 하나의 차이가 누적될 수 있다.",[282,1488,1489],{},"그래서 성능 최적화라는 것을 반드시:",[333,1491,1494],{"className":1492,"code":1493,"language":338,"meta":339},[336],"Redis\n캐싱\n분산 처리\nDB 샤딩\n비동기 처리\n",[297,1495,1493],{"__ignoreMap":339},[282,1497,1498],{},"같은 큰 작업으로만 생각할 필요는 없을 것 같다.",[282,1500,1501,1504],{},[286,1502,1503],{},"처음부터 DB가 효율적으로 데이터를 찾을 수 있는 형태로 쿼리를 작성하는 것 역시 성능을 고려한 개발","이라고 생각하게 됐다.",[320,1506],{},[277,1508,1510],{"id":1509},"️-날짜-타입에-따른-조회-범위도-확인","⚠️ 날짜 타입에 따른 조회 범위도 확인",[282,1512,1513,1514,1516],{},"이번 구현에서는 거래 날짜를 ",[297,1515,1097],{},"로 관리하고 있기 때문에:",[333,1518,1519],{"className":382,"code":1061,"language":384,"meta":339,"style":339},[297,1520,1521,1541,1545,1565],{"__ignoreMap":339},[388,1522,1523,1525,1527,1529,1531,1533,1535,1537,1539],{"class":390,"line":391},[388,1524,1057],{"class":394},[388,1526,1070],{"class":453},[388,1528,1073],{"class":459},[388,1530,1076],{"class":453},[388,1532,473],{"class":459},[388,1534,1081],{"class":463},[388,1536,467],{"class":459},[388,1538,991],{"class":453},[388,1540,1088],{"class":459},[388,1542,1543],{"class":390,"line":398},[388,1544,504],{"emptyLinePlaceholder":503},[388,1546,1547,1549,1551,1553,1555,1557,1559,1561,1563],{"class":390,"line":404},[388,1548,1097],{"class":394},[388,1550,1100],{"class":453},[388,1552,1073],{"class":459},[388,1554,1105],{"class":453},[388,1556,473],{"class":459},[388,1558,1110],{"class":463},[388,1560,467],{"class":459},[388,1562,1115],{"class":594},[388,1564,1088],{"class":459},[388,1566,1567,1569,1571,1573,1575,1577,1579],{"class":390,"line":500},[388,1568,1097],{"class":394},[388,1570,1124],{"class":453},[388,1572,1073],{"class":459},[388,1574,1105],{"class":453},[388,1576,473],{"class":459},[388,1578,1133],{"class":463},[388,1580,931],{"class":459},[282,1582,1583],{},"형태로 간단하게 처리할 수 있었다.",[282,1585,1586,1587,1590],{},"하지만 만약 거래 시간이 ",[297,1588,1589],{},"LocalDateTime","으로 관리된다면 월의 마지막 날짜를 처리할 때 조금 더 주의해야 한다.",[282,1592,937],{},[333,1594,1597],{"className":1595,"code":1596,"language":338,"meta":339},[336],"2026-08-31 15:30:00\n",[297,1598,1596],{"__ignoreMap":339},[282,1600,1601],{},"같은 데이터까지 포함해야 하기 때문이다.",[282,1603,1604],{},"이런 경우에는:",[333,1606,1608],{"className":382,"code":1607,"language":384,"meta":339,"style":339},"LocalDateTime start =\n        ym.atDay(1).atStartOfDay();\n\nLocalDateTime end =\n        ym.plusMonths(1).atDay(1).atStartOfDay();\n",[297,1609,1610,1620,1641,1645,1654],{"__ignoreMap":339},[388,1611,1612,1614,1617],{"class":390,"line":391},[388,1613,1589],{"class":394},[388,1615,1616],{"class":453}," start ",[388,1618,1619],{"class":459},"=\n",[388,1621,1622,1625,1627,1629,1631,1633,1636,1639],{"class":390,"line":398},[388,1623,1624],{"class":453},"        ym",[388,1626,473],{"class":459},[388,1628,1110],{"class":463},[388,1630,467],{"class":459},[388,1632,1115],{"class":594},[388,1634,1635],{"class":459},").",[388,1637,1638],{"class":463},"atStartOfDay",[388,1640,931],{"class":459},[388,1642,1643],{"class":390,"line":404},[388,1644,504],{"emptyLinePlaceholder":503},[388,1646,1647,1649,1652],{"class":390,"line":500},[388,1648,1589],{"class":394},[388,1650,1651],{"class":453}," end ",[388,1653,1619],{"class":459},[388,1655,1656,1658,1660,1663,1665,1667,1669,1671,1673,1675,1677,1679],{"class":390,"line":507},[388,1657,1624],{"class":453},[388,1659,473],{"class":459},[388,1661,1662],{"class":463},"plusMonths",[388,1664,467],{"class":459},[388,1666,1115],{"class":594},[388,1668,1635],{"class":459},[388,1670,1110],{"class":463},[388,1672,467],{"class":459},[388,1674,1115],{"class":594},[388,1676,1635],{"class":459},[388,1678,1638],{"class":463},[388,1680,931],{"class":459},[282,1682,1683],{},"처럼 다음 달의 시작을 종료 기준으로 잡고:",[333,1685,1688],{"className":1686,"code":1687,"language":338,"meta":339},[336],">= start\n\u003C end\n",[297,1689,1687],{"__ignoreMap":339},[282,1691,1692],{},"형태로 조회하는 방법도 고려할 수 있다.",[282,1694,1695],{},"날짜 조회는 단순해 보이지만 실제로는:",[333,1697,1700],{"className":1698,"code":1699,"language":338,"meta":339},[336],"LocalDate인가?\nLocalDateTime인가?\n\n시작과 종료를 어떻게 정의할 것인가?\n\n마지막 날을 포함해야 하는가?\n\n시간대가 필요한가?\n",[297,1701,1699],{"__ignoreMap":339},[282,1703,1704],{},"등을 데이터 모델에 맞게 함께 생각해야 한다.",[320,1706],{},[277,1708,1710],{"id":1709},"지금까지-진행된-brobean","🚜 지금까지 진행된 BroBean",[282,1712,1713],{},"이번 작업까지 진행하면서 주요 기능들이 실제 데이터와 연결되기 시작했다.",[1715,1716,299],"h3",{"id":1717},"usersetting",[333,1719,1722],{"className":1720,"code":1721,"language":338,"meta":339},[336],"개인 \u002F 커플 모드\n마지막 선택 모드\n사용자 설정값\n",[297,1723,1721],{"__ignoreMap":339},[282,1725,1726],{},"을 관리할 수 있도록 백엔드 작업을 진행했다.",[1715,1728,1729],{"id":375},"🌱 씨앗심기",[333,1731,1734],{"className":1732,"code":1733,"language":338,"meta":339},[336],"수입\n지출\n저축\n",[297,1735,1733],{"__ignoreMap":339},[282,1737,1738],{},"을 실제 Transaction 데이터로 저장하는 API를 구현했다.",[1715,1740,1742],{"id":1741},"성장일지","📖 성장일지",[333,1744,1747],{"className":1745,"code":1746,"language":338,"meta":339},[336],"거래 내역 조회\n월별 조회\n지출 합계\n수입 합계\n저축 합계\n",[297,1748,1746],{"__ignoreMap":339},[282,1750,1751],{},"를 제공하는 API를 구현하고 프론트와 연결했다.",[282,1753,1754,1755,1758],{},"그리고 개인 \u002F 커플 모드를 기준으로 실제 데이터 조회 범위까지 연결하면서 이제 단순한 화면 구현을 넘어 ",[286,1756,1757],{},"서비스의 데이터 구조와 조회 방식","을 함께 고민하는 단계로 넘어가고 있다.",[320,1760],{},[277,1762,1764],{"id":1763},"마무리","🎯 마무리",[282,1766,1767],{},"이번 작업에서 기능적으로 가장 큰 변화는 개인 \u002F 커플 모드가 실제 백엔드 데이터와 연결되고, 씨앗심기와 성장일지까지 실제 API를 통해 동작하기 시작했다는 점이다.",[282,1769,1770,1771,1051],{},"하지만 개인적으로 더 기억에 남은 것은 월별 조회 쿼리를 작성하면서 ",[286,1772,1773],{},"내가 알고 있던 기본적인 원칙을 실제 코드에서는 놓칠 수도 있다는 것을 다시 확인한 것",[282,1775,1776],{},"처음에는:",[333,1778,1779],{"className":382,"code":1153,"language":384,"meta":339,"style":339},[297,1780,1781,1803],{"__ignoreMap":339},[388,1782,1783,1785,1787,1789,1791,1793,1795,1797,1799,1801],{"class":390,"line":391},[388,1784,538],{"class":453},[388,1786,473],{"class":459},[388,1788,897],{"class":453},[388,1790,473],{"class":459},[388,1792,961],{"class":463},[388,1794,964],{"class":459},[388,1796,548],{"class":463},[388,1798,467],{"class":459},[388,1800,961],{"class":453},[388,1802,581],{"class":459},[388,1804,1805,1807,1809,1811,1813,1815,1817,1819,1821,1823],{"class":390,"line":398},[388,1806,538],{"class":453},[388,1808,473],{"class":459},[388,1810,897],{"class":453},[388,1812,473],{"class":459},[388,1814,991],{"class":463},[388,1816,964],{"class":459},[388,1818,548],{"class":463},[388,1820,467],{"class":459},[388,1822,991],{"class":453},[388,1824,581],{"class":459},[282,1826,1827],{},"가 당연하게 느껴졌다.",[282,1829,1830],{},"하지만 월이라는 화면상의 개념을 실제 날짜 범위로 변환해 생각해보니:",[333,1832,1833],{"className":382,"code":1061,"language":384,"meta":339,"style":339},[297,1834,1835,1855,1859,1879],{"__ignoreMap":339},[388,1836,1837,1839,1841,1843,1845,1847,1849,1851,1853],{"class":390,"line":391},[388,1838,1057],{"class":394},[388,1840,1070],{"class":453},[388,1842,1073],{"class":459},[388,1844,1076],{"class":453},[388,1846,473],{"class":459},[388,1848,1081],{"class":463},[388,1850,467],{"class":459},[388,1852,991],{"class":453},[388,1854,1088],{"class":459},[388,1856,1857],{"class":390,"line":398},[388,1858,504],{"emptyLinePlaceholder":503},[388,1860,1861,1863,1865,1867,1869,1871,1873,1875,1877],{"class":390,"line":404},[388,1862,1097],{"class":394},[388,1864,1100],{"class":453},[388,1866,1073],{"class":459},[388,1868,1105],{"class":453},[388,1870,473],{"class":459},[388,1872,1110],{"class":463},[388,1874,467],{"class":459},[388,1876,1115],{"class":594},[388,1878,1088],{"class":459},[388,1880,1881,1883,1885,1887,1889,1891,1893],{"class":390,"line":500},[388,1882,1097],{"class":394},[388,1884,1124],{"class":453},[388,1886,1073],{"class":459},[388,1888,1105],{"class":453},[388,1890,473],{"class":459},[388,1892,1133],{"class":463},[388,1894,931],{"class":459},[282,1896,1897],{},"로 애플리케이션에서 범위를 만들고,",[333,1899,1900],{"className":382,"code":1205,"language":384,"meta":339,"style":339},[297,1901,1902],{"__ignoreMap":339},[388,1903,1904,1906,1908,1910,1912,1914,1916,1918,1920,1922],{"class":390,"line":391},[388,1905,538],{"class":453},[388,1907,473],{"class":459},[388,1909,897],{"class":453},[388,1911,473],{"class":459},[388,1913,902],{"class":463},[388,1915,467],{"class":459},[388,1917,907],{"class":453},[388,1919,870],{"class":459},[388,1921,912],{"class":453},[388,1923,581],{"class":459},[282,1925,1926],{},"로 조회하는 것이 더 자연스러운 구조였다.",[282,1928,1929],{},"무엇보다 이번 일을 계기로 쿼리를 작성할 때 단순히 **\"원하는 결과가 나오는가\"**만 확인하지 않고,",[333,1931,1934],{"className":1932,"code":1933,"language":338,"meta":339},[336],"DB 입장에서는 이 조건을 어떻게 처리할까?\n\n인덱스를 활용할 수 있는 형태인가?\n\n불필요한 계산을 하고 있지는 않은가?\n\n데이터가 많아져도 괜찮은 구조인가?\n",[297,1935,1933],{"__ignoreMap":339},[282,1937,1938],{},"를 한 번 더 생각해보게 됐다.",[282,1940,1941],{},"아직 BroBean의 데이터 규모가 크지 않기 때문에 지금 당장 큰 성능 차이를 체감할 수 있는 단계는 아니다.",[282,1943,1944],{},"그래도 이런 부분을 나중에 문제가 발생한 뒤 고치는 것보다, 기능을 구현하는 순간부터 조금씩 신경 쓰는 것이 더 좋은 개발 습관이라고 생각한다.",[282,1946,1947,1948,1951],{},"결국 좋은 코드는 단순히 ",[286,1949,1950],{},"동작하는 코드","에서 끝나는 것이 아니라, 데이터가 늘어나고 서비스가 커졌을 때도 어떤 방식으로 동작할지를 생각한 코드라고 생각한다.",[282,1953,1954],{},"BroBean도 이제 화면을 하나씩 완성하는 단계를 넘어 실제 데이터가 쌓이고 여러 기능이 서로 연결되는 단계로 넘어가고 있다.",[282,1956,1957,1958,1961],{},"앞으로 기능이 많아질수록 단순한 구현보다 ",[286,1959,1960],{},"데이터 구조와 조회 성능까지 함께 고민하는 개발","을 계속해보려고 한다.",[1963,1964,1965],"style",{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":339,"searchDepth":398,"depth":398,"links":1967},[1968,1969,1970],{"id":1717,"depth":404,"text":299},{"id":375,"depth":404,"text":1729},{"id":1741,"depth":404,"text":1742},"2026-09-01","BroBean의 개인·커플 모드 전환 기능과 씨앗심기·성장일지 백엔드 및 API를 구현하고, 월별 가계부 조회 쿼리를 작성하는 과정에서 쿼리 성능을 다시 고민해본 경험을 정리합니다.","md",null,{"tags":1976,"image":1985},[1977,1978,1979,1980,1981,1982,1983,1984,299],"BroBean","가계부 앱","커플 가계부","Spring Boot","QueryDSL","JPA","API","성능 최적화","\u002Fimages\u002Fbrobean\u002Fog-image.png",{"icon":20},{"title":1988,"description":1989},"BroBean 개인·커플 모드와 가계부 API 구현 - QueryDSL 쿼리 성능 고민","BroBean의 개인·커플 모드와 씨앗심기, 성장일지 API를 구현하면서 QueryDSL의 월별 조회 조건을 날짜 범위 조회로 개선하고 쿼리 성능을 다시 고민한 과정을 정리합니다.","rMDjiTVtmw2DIwA9KtLO3ehrTC9OGND9Fmu9slCoNww",[1992,1994],{"title":238,"path":239,"stem":240,"description":1993,"icon":20,"children":-1},"회원가입과 로그인 백엔드 연동 과정에서 발생한 인증 상태 초기화 문제를 해결하고, Nuxt Plugin을 활용한 사용자 정보 복구와 플러그인 실행 순서 제어까지 구현한 과정",{"title":246,"path":247,"stem":248,"description":1995,"icon":20,"children":-1},"BroBean 커플 기능을 구현하던 중 AuthService와 CoupleService 사이에서 발생한 순환 참조 문제와 ObjectProvider를 활용해 의존성 주입 시점을 지연시키는 방법을 정리합니다.",1789947611440]