<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Hyeok의 웹 개발 블로그</title>
    <link>https://yy9611.tistory.com/</link>
    <description>초보 웹 개발 학습 일지 </description>
    <language>ko</language>
    <pubDate>Tue, 7 Apr 2026 02:04:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Yhyeok</managingEditor>
    <item>
      <title>&amp;lt;2025.08.07&amp;gt; Embedding 기능 및 VectorStore 기능</title>
      <link>https://yy9611.tistory.com/56</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1️⃣개요&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자의 자연어 요청을 단순 키워드가 아닌 의미 기반으로 분석하고, 이에 가장 적절한 강의를 추천하는 시스템&lt;/li&gt;
&lt;li&gt;이를 위해 OpenAi의 임베딩 모델을 사용해 강의 콘텐츠와 사용자 입력을 벡터로 변환하고, VecotorStore(PGVector)를 이용해 이 벡터 간의 유사도를 계산하여 가장 유사한 강의를 빠르게 찾는다.&lt;/li&gt;
&lt;li&gt;이 시스템은 정확한 추천뿐만 아니라, 사용자의 재질문이나 조건 변경에도 유연하게 대응할 수 있는 구조를 갖추고 있다.&lt;/li&gt;
&lt;li&gt;프로젝트 초기에는 벡터 저장 방식을 SimpleVectorStore를 사용했습니다. 하지만 프로젝트를 진행하며 PgVectorStore 로 변경하여 구현하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2️⃣&lt;/b&gt;기술 도입 배경&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  임베딩의 필요성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴퓨터가 이해할 수 있도록 텍스트를 수치화&lt;/li&gt;
&lt;li&gt;강의 설명을 벡터로 변환해서 DB에 저장 (float[])&lt;/li&gt;
&lt;li&gt;사용자가 입력한 검색어 또는 질문도 벡터로 변환&lt;/li&gt;
&lt;li&gt;둘의 유사도를 비교해서 가까운 강의를 추천 &amp;rarr; pgvector Store에서 유사도 검색&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; VectorStore 사용 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;텍스트를 벡터로 변환해 의미가 비슷한 내용 검색 가능&lt;/li&gt;
&lt;li&gt;빠른 유사도 검색 가능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코사인 유사도를 사용해 대규모 벡터에서 빠른 검색 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LLM과 연결성 뛰어남&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; SimpleVectorStore 에서 PgVectorStore 변경 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimpleVectorStore는 Spring AI 에서 기본으로 제공하는 인메모리 벡터 저장소로 설정이 간단하고 빠르게 기능 구현이 가능하다는 장점이 있습니다. 하지만 프로젝트가 운영환경으로 확장하고 진행됨에 따라 한계점이 발생&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SimpleVectorStore
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;벡터 수가 많이 질 수록 검색 기능이 낮아지고 저하되는 스케일링 한계&lt;/li&gt;
&lt;li&gt;다중 서버나 배포 환경에서 벡터 공유가 불가능하여 확장성과 안정성이 부족&lt;/li&gt;
&lt;li&gt;어플리케이션 재시작 시 저장된 벡터 데이터 모두 초기화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;PgVectorStore
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어플리케이션 재시작 시 벡터가 안전하게 보존&lt;/li&gt;
&lt;li&gt;유사도 검색 최적화 : pgvector의 벡터 연산기능을 통해서 cosine기반 고속 검색 가능&lt;/li&gt;
&lt;li&gt;다중서버간 데이터 공유가 가능하고 확장성과 유지보수가 뛰어남&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3️⃣ 주요 흐름 (초기 구조: 전체 응답 수신 방식)&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자 입력 수신
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 자연어로 요청을 보낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용자 입력을 입베딩 변환
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EmbeddingService를 통해 사용자 질문을 임베딩(벡터)으로 변환&lt;/li&gt;
&lt;li&gt;OpenAi의 Embedding API 사용 (Model : text-embedding-3-small)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application.yml에 Ai Model 밑에 Embedding model을 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;// application.yml
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      base-url: &amp;lt;https://api.openai.com&amp;gt;
      chat:
        options:
          model: gpt-4o-mini
      embedding:
        model: text-embedding-3-small
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변환 결과는 float[ ]형태의 벡터 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;VectorStore에 저장된 강의 데이터와 유사도 검색
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PGVector가 적용된 PostgreSQL 기반 VectorStore에서 미리 임베딩해둔 강의 정보와 입력 벡터를 비교&lt;/li&gt;
&lt;li&gt;Cosine Similarity를 통해 유사도 계산&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;가장 유사한 강의 추출
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유사도가 높은 순으로 정렬&lt;/li&gt;
&lt;li&gt;상의 N개의 강의 정보를 추출하여 결과 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;응답 생성 및 반환
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추천 결과를 사용자에게 자연스러운 문장으로 구성해 응답&lt;/li&gt;
&lt;li&gt;WebSocket 또는 REST API를 통해 프론트에 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r1O5n/btsPL09HVg1/M0M54oBA47FiCxZOXtUxDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r1O5n/btsPL09HVg1/M0M54oBA47FiCxZOXtUxDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r1O5n/btsPL09HVg1/M0M54oBA47FiCxZOXtUxDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr1O5n%2FbtsPL09HVg1%2FM0M54oBA47FiCxZOXtUxDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;532&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4️⃣&lt;/b&gt; 기술 구성 요소&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;SimpleVectorStore&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;PgvectorStore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;저장 방식&lt;/td&gt;
&lt;td&gt;메모리 기반&lt;/td&gt;
&lt;td&gt;PostgreSQL + pgvector 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 유지&lt;/td&gt;
&lt;td&gt;비영속성 (서버 꺼지면 사라짐)&lt;/td&gt;
&lt;td&gt;영속성 (DB에 저장됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;유사도 검색&lt;/td&gt;
&lt;td&gt;가능 (작은 규모에 적합)&lt;/td&gt;
&lt;td&gt;가능 (대규모 최적화됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설치&lt;/td&gt;
&lt;td&gt;없음 (간단하게 사용)&lt;/td&gt;
&lt;td&gt;PostgreSQL + pgvector 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성능&lt;/td&gt;
&lt;td&gt;적은 벡터 수에 빠름&lt;/td&gt;
&lt;td&gt;수천~수백만 벡터도 처리 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개발 용도&lt;/td&gt;
&lt;td&gt;로컬 테스트, 데모&lt;/td&gt;
&lt;td&gt;실제 운영 환경, 서비스 구축&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용하기 좋은 곳&lt;/td&gt;
&lt;td&gt;기능 시연, 빠른 테스트&lt;/td&gt;
&lt;td&gt;사용자 데이터 기반 추천 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5️⃣ 결과&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자의 질문을 의미 단위로 분석하여 가장 유사한 강의를 빠르고 정확하게 추천합니다.&lt;/li&gt;
&lt;li&gt;임베딩 기반 유사도 검색을 통해 사용자 질문에 맞는 강의를 의미적으로 매칭한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6️⃣ 회고&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기술적 회고
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순 GPT 호출하는 것이 전부인 줄 알았지만, 실제로 프롬프트 구성, 의미 파악, 후처리 등이 챗봇의 품질과 성능에 큰 영향으 미쳤다.&lt;/li&gt;
&lt;li&gt;Embedding 결과는 float 배열이라 가시성이 없어서, 유사도 기준 설정에 많은 실험이 필요&lt;/li&gt;
&lt;li&gt;LLM이 생성하는 답변이 항상 정답이 아니기 때문에, 추천 결과를 유연하게 해석하고 제어하는 로직 설계가 필요하다는 점을 알게됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;추후 개선 사항
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재는 키워드를 수집합니다. 수집한 키워드를 기반으로 강의를 추천해주고있습니다. 추후에는 키워드 뿐만아니라, 전체적인 대화 흐름을 지금보다 더 정확하게 분석하고 판단하여 강의를 추천하도록 개선&lt;/li&gt;
&lt;li&gt;사용자의 요청이 바뀔 때, 대화 흐름과 추천 상태를 어떻게 이어갈지에 대한 고민이 많았고, 이 부분에 대해서는 아직 개선 가능성이 많다 생각해서 추후 개선하면 성능 향상에 많은 도움이 될 것이라 생각&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL/AI</category>
      <author>Yhyeok</author>
      <guid isPermaLink="true">https://yy9611.tistory.com/56</guid>
      <comments>https://yy9611.tistory.com/56#entry56comment</comments>
      <pubDate>Thu, 7 Aug 2025 21:49:55 +0900</pubDate>
    </item>
    <item>
      <title>&amp;lt;2025.08.01&amp;gt; 프롬프트 (Zero-Shot, One-Shot, Few-Shot)</title>
      <link>https://yy9611.tistory.com/55</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ 샷 기반 프롬프팅?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 'Shot' 은 프롬프터에 포함된 예시의 수를 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Zero-Shot Prompting : 예시가 제공되지 않으며, 모델은 사전 훈련된 지식에 전적으로 의존해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- One-Shot Prompting : 모델의 작업을 명확히 하기 위해 단일 예를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Few-Shot Prompting : 두 개 이상의 예가 포함되어 있어 모델이 패턴을 인식하고 더 정확한 응답을 제공할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ Zero-Shot Prompting&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 제로샷 프롬프팅 은 가장 간단한 형태의 프롬프팅&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 모델에 예시나 데모를 제공하지 않고 작업을 수행하라는 직접적인 지시를 내린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 즉, 모델은 사전 훈련된 지식에 전적으로 의존하여 작업 완료 방법을 파악해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754030330870&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Zero-Shot Prompting 예시&amp;gt;

Prompt
&amp;ldquo;문장의 감정을 분류하세요. (긍정 / 부정)
문장: 오늘 날씨가 너무 끔찍해.&amp;rdquo;

모델 응답 (예시)
&amp;ldquo;부정&amp;rdquo;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ One-Shot Prompting&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 원샷 프롬프팅은 새로운 작업을 시작하기 전에 단일 예를 제공해서 프롬프팅을 향상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이를 통해 기대 사항을 명확히 하고 모델 성능을 개선하는데 도움&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754030573039&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;One Shot Prompting 예시&amp;gt;

Prompt
&amp;ldquo;문장의 감정을 분류하세요. (긍정 / 부정)
예시:
문장: 이 영화 진짜 재미있었어!
감정: 긍정

문장: 오늘 날씨가 너무 끔찍해.&amp;rdquo;

모델 응답 (예시)
&amp;ldquo;감정: 부정&amp;rdquo;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ Few-Shot Prompting&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 퓨샷 프롬프팅은 두 개 이상의 예시를 제공하여 모델이 패턴을 인식하고 더 복잡한 작업을 처리하는 데 도움을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 예시가 많을 수록 모델은 작업을 더 잘 이해하게 되어 정확도와 일관성이 향상됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754030732232&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Few-Shot Prompting 예시&amp;gt;

너는 사용자의 메시지가 '강의 재추천 요청'인지 판단하는 AI야.
		
		[판단 기준]
		- 사용자가 조건을 바꿔서 다시 추천을 요청하는 경우 (예: '다른 강의 추천해줘', '가격을 낮춰서 다시 추천', '조건을 바꿔서 추천해줘' 등)
		- 단순한 추가 질문, 강의 설명 요청 등은 재추천이 아님
		
		[응답 형식]
		- 반드시 아래 형식에 따를 것
		- 응답은 YES 또는 NO 중 하나
		
		[예시]
		사용자 메세지: 다른 강의도 추천해줘
		응답: YES
		
		사용자 메세지: 이 강의 설명 좀 더 해줘
		응답: NO
		
		사용자 메세지: 초급 강의로 다시 추천해줘
		응답: YES
		
		사용자 메세지: 이 강의는 무료인가요?
		응답: NO
		
		사용자 메세지: 다른 거 볼 수 있을까요?
		응답: YES
		
		사용자 메세지: 추천된 강의 등록은 어떻게 하나요?
		응답: NO
		
		사용자 메세지: 좀 더 저렴한 강의는 없나요?
		응답: YES
		
		사용자 메세지: 입문자용으로 다시 보여주세요.
		응답: YES
		&quot;&quot;&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Few-Shot Prompting 을 위한 모범 사례&amp;nbsp;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;몇가지 샷으로 구성된 프롬프트를 구성할 때 고려해야 할 사항
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;포함할 예제의 수&lt;/li&gt;
&lt;li&gt;예시의 순서와 관련성&lt;/li&gt;
&lt;li&gt;출력 형식 (예: 목록, JSON, YAML)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;몇 개의 샷으로 구성된 프롬프트 구성
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Few-Shot Prompt 를 구성하느 방식은 매우 중요하다. 여기서 중요한 점은 입력과 출력을 콜론( : ) 으로 구분 할 것인지,&amp;nbsp;&lt;br /&gt;INPUT/ OUTPUT 으로 구분 할 것인지 이다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1754032534925&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Q : 입력
A : 출력

INTPUT : 입력
OUTPUT : 출력&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Few-Shot Prompting 의 한계
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;단 몇 번의 샷 프롬프팅은 매우 효과적이지만 한계가 존재한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&amp;nbsp;컨텍스트 창 제약은 예제의 수를 제한한다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;예시가 너무 유사하면 과도한 일반화가 발생 할 수 있다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;모델은 작업을 이해하기보다는 피상적은 패턴에 초점을 맞출 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ 결론&amp;nbsp;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Few-Shot Prompting 은 AI 역량을 향상시키는 다재다능하고 강력한 기법&amp;nbsp;&lt;/li&gt;
&lt;li&gt;예시를 제공함으로써 모델이 정확하고 체계적인 출력을 생성하도록 유도 가능&lt;/li&gt;
&lt;li&gt;효과를 극대화 하려면 컨텍스트 창 크기 및 예시 선택과 같은 제약 조건을 고려하는 것이 중요!&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL/AI</category>
      <author>Yhyeok</author>
      <guid isPermaLink="true">https://yy9611.tistory.com/55</guid>
      <comments>https://yy9611.tistory.com/55#entry55comment</comments>
      <pubDate>Fri, 1 Aug 2025 01:03:45 +0900</pubDate>
    </item>
    <item>
      <title>&amp;lt;2025.06.04&amp;gt; 임베딩</title>
      <link>https://yy9611.tistory.com/53</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 임베딩&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;텍스트(문자)를 숫자 벡터로 변환하는 작업&lt;/li&gt;
&lt;li&gt;숫자로 변환된 벡터를 벡터 DB에 저장되어 나중에 유사한 질문 검색(RAG)등에 사용&lt;/li&gt;
&lt;li&gt;OpenAI의 대표 임베딩 모델 &amp;rarr; text-embedding-ada-002&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ Spring OpenAI 임베딩&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring AI는 OpenAI의 텍스트 입베딩 모델 지원&lt;/li&gt;
&lt;li&gt;OpenAI의 텍스트 임베딩은 텍스트 문자열의 연관성을 측정&lt;/li&gt;
&lt;li&gt;임베딩은 부동 소수점 숫자로 구성된 벡터&lt;/li&gt;
&lt;li&gt;두 벡터 간의 거리로 연관성 측정 &amp;rarr; 가까우면 연관성 높고, 멀면 연관성 낮다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 필수 조건&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OpenAI 임베딩 모델에 엑세스 하려면 OpenAI로 API를 만들어야 한다.&lt;/li&gt;
&lt;li&gt;OpenAI API 키 페이지 에서 토큰 생성&lt;/li&gt;
&lt;li&gt;Spring AI 프로젝트는 &lt;a href=&quot;http://openai.com&quot;&gt;openai.com&lt;/a&gt; 에서 얻은&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring.ai.openai.api-key값으로 설정해야 하는 구성 속성을 정의합니다.API Key&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 에서 이 구성 속성을 설정할 수 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.properties&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;spring.ai.openai.api-key=&amp;lt;your-openai-api-key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 키와 같은 민감한 정보를 처리할 때 보안을 강화하기위해 SPEL을 사용 하여 환경 변수 참조 가능&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# In application.yml
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# In your environment or .env file
export OPENAI_API_KEY=&amp;lt;your-openai-api-key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 어플리케이션 코드에서 프로그래밍 방식으로 설정 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// Retrieve API key from a secure source or environment variable
String apiKey = System.getenv(&quot;OPENAI_API_KEY&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 자동 구성&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring AI는 OpenAI 임베딩 모델에 대한 Spring Boot 자동 구성을 제공한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 Maven pom.xml 파일에 종속성 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.ai&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-ai-starter-model-openai&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;or Gradle build.gradle 파일에 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.ai:spring-ai-starter-model-openai'
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 임베딩 API를 위한 주소 설정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OpenAI 임베딩 API는 별도의 주소를 설정 X&lt;/li&gt;
&lt;li&gt;기본적으로 &lt;a href=&quot;https://api.openai.com/v1/embeddings&quot;&gt;https://api.openai.com/v1/embeddings&lt;/a&gt; 엔드포인트 사용, Spring AI에서는 이를 자동으로 처리 &amp;rarr; 별도의 주소값을 가져오지 않아도 무관&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 다음으로 컨트롤러와 서비스를 구현하면 별도의 WebClient 구성 없이도 OpenAI 임베딩 API와 자동으로 연동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 입력된 텍스트를 바로 벡터 (List&amp;lt;Double&amp;gt;)로 변환하여 사용가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이후 벡터를 PgVector DB에 저장하면 RAG에 활용할 수 있다.&lt;/p&gt;</description>
      <category>TIL/Spring</category>
      <author>Yhyeok</author>
      <guid isPermaLink="true">https://yy9611.tistory.com/53</guid>
      <comments>https://yy9611.tistory.com/53#entry53comment</comments>
      <pubDate>Wed, 4 Jun 2025 21:01:21 +0900</pubDate>
    </item>
    <item>
      <title>&amp;lt;2025.06.02&amp;gt; RAG (검색-증강 생성)</title>
      <link>https://yy9611.tistory.com/52</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ RAG (검색-증강 생성)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RAG 란?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RAG (Retrieval-Augmented Generation)는 대규모 언어 모델의 출력을 최적화하여 응답을 생성하기 전에 학습 데이터 소스 외부의 신뢰할 수 있는 지식 베이스를 참조하도록 하는 프로세스&lt;/li&gt;
&lt;li&gt;대규모 언어 모델 LLM 은 방대한 양의 데이터를 기반으로 학습되며 수십억 개의 매개 변수를 사용하여 질문에 대한 답변, 언어 번역, 문장 완성과 같은 작업에 대한 독창적인 결과를 생성&lt;/li&gt;
&lt;li&gt;RAG는 이미 강력한 LLM의 기능을 특정 도메인이나 조직의 내부 지식 기반으로 확장하므로, 모델을 다시 교육할 필요 X&lt;/li&gt;
&lt;li&gt;LLM 결과를 개선하여 다양한 상황에서 관련성, 정확성 및 유용성을 유지하기 위한 비용 효율적인 접근 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RAG 가 중요한 이유?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LMM &amp;rarr; 챗봇 / 자연어 처리 (NLP) 기능을 지원하는 AI 기술&lt;/li&gt;
&lt;li&gt;LMM 기술의 특성상 LLM 응답에 대한 예측 불가능&lt;/li&gt;
&lt;li&gt;LLM 훈련 데이터는 정적이며 보유한 지식에 대한 마감일을 도입한다.&lt;/li&gt;
&lt;/ul&gt;
&amp;rarr; LMM의 문제점
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;답변이 없을 때 허위 정보를 제공&lt;/li&gt;
&lt;li&gt;사용자가 구체적이고 최신의 응답을 기대할 때 오래되었거나 일반적인 정보를 제공&lt;/li&gt;
&lt;li&gt;신뢰할 수 없는 출처로부터 응답을 생성&lt;/li&gt;
&lt;li&gt;용어 혼동으로 인해 응답이 정확하지 않다.&lt;/li&gt;
&lt;li&gt;다양한 훈련 소스가 동일한 용어를 사용하여 서로 다른 내용을 설명&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;-  RAG는 이러한 문제 중 일부를 해결 하기 위한 한 가지 접근 방식이다.
-  LLM을 리디렉션하여 신뢰할 수 있는 사전 결정된 지식 출처에서 관련 정보를 검색
-  조직은 생성된 텍스트 출력을 더 잘 제어 할 수 있으며, 사용자는 LLM이 응답을 생성하는 방식에 대한 통찰력을 얻을 수 있다.&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;RAG의 이점?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비용 효율적인 구현
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;챗봇 개발 &amp;rarr; 파운데이션 모델 사용하여 시작&lt;/li&gt;
&lt;li&gt;RAG는 LMM에 새 데이터를 도입하기 위한 보다 비용 효율적인 접근 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;* 파운데이션 모델 &amp;rarr; 광범위한 일반화 데이터와 레이블이 지정되지 않은 데이터에 대해 훈련된   API 엑세스 가능 LLM
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;최신 정보
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RAG를 사용하여 LLM을 라이브 소셜 미디어 피드, 뉴스 사이트 또는 기타 자주 업데이트되는 정보 소스에 직접 연결할 수 있습니다. 그러면 LLM은 사용자에게 최신 정보를 제공할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용자 신뢰강화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RAG 는 LLM 소스의 저작자 표시를 통해 정확한 정보 제공 가능&lt;/li&gt;
&lt;li&gt;사용자는 추가 설명이나 세부 정보가 필요한 경우 소스 문서를 직접 찾아볼 수 있다.&lt;/li&gt;
&lt;li&gt;이를 통해 생성형 AI 솔루션에 대한 신뢰와 확신을 올릴 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;개발자 제어 강화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RAG를 사용하여 채팅 어플을 보다 효율적으로 테스트하고 개선 가능&lt;/li&gt;
&lt;li&gt;LLM의 정보 소스를 제어하고 변경하여 변화하는 요구 사항 또는 부서 간 사용에 적응 가능&lt;/li&gt;
&lt;li&gt;개발자는 민감한 정보 검색을 다양한 인증 수준으로 제한 하고, LLM이 적절한 응답을 생성하도록 가능&lt;/li&gt;
&lt;li&gt;광범위한 어플리케이션을위해 생성형 AI 기술을 보다 자신 있게 구현 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RAG 작동 방식
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부데이터 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LLM의 원래 학습 데이터 세트 외부에 있는 새 데이터를 &amp;lsquo;외부 데이터&amp;rsquo; 라고 한다.&lt;/li&gt;
&lt;li&gt;API, 데이터베이스 또는 문서 Repository와 같은 여러 데이터 소스에서 가져올 수 있다.&lt;/li&gt;
&lt;li&gt;데이터 &amp;rarr; 파일, 데이터베이스 레코드 or 긴형식의 텍스트와 같은 여러 형태로 존재 가능&lt;/li&gt;
&lt;li&gt;임베딩 언어 모델이라고 하는 또 다른 AI 기법은 데이터 수치로 변환 하고 벡터 DB에 저장&lt;/li&gt;
&lt;li&gt;프로세스는 생성형 AI 모델이 이해할 수 있는 지식 라이브러리를 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;관련 정보 검색
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 쿼리는 벡터 표현으로 변환 &amp;rarr; 벡터 DB와 매칭&lt;/li&gt;
&lt;li&gt;관련성은 수학적 벡터 계산 및 표현에 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LLM 프롬프트 확장
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RAG 모델은 검색된 관련 데이터를 컨텍스트에 추가하여 사용자 입력(or 프롬프트)을 보강&lt;/li&gt;
&lt;li&gt;신속한 엔지니어링 기술을 사용 &amp;rarr; LLM 과 효과적으로 통신&lt;/li&gt;
&lt;li&gt;확장된 프롬프트를 사용하면 대규모 언어 모델이 사용자 쿼리에 대한 정확한 답변 생성 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;외부 데이터 업데이트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최신 정보를 유지하기 위해 문서를 비동기적으로 업데이트 및 문서의 임베딩 표현 업데이트&lt;/li&gt;
&lt;li&gt;자동화된 실시간 프로세스 또는 주기적 배치 처리를 통해 수행&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpkb6F/btsOneW4xcZ/IR693tUdVfRcVS8biC3K51/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpkb6F/btsOneW4xcZ/IR693tUdVfRcVS8biC3K51/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpkb6F/btsOneW4xcZ/IR693tUdVfRcVS8biC3K51/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcpkb6F%2FbtsOneW4xcZ%2FIR693tUdVfRcVS8biC3K51%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;898&quot; height=&quot;532&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL/AI</category>
      <author>Yhyeok</author>
      <guid isPermaLink="true">https://yy9611.tistory.com/52</guid>
      <comments>https://yy9611.tistory.com/52#entry52comment</comments>
      <pubDate>Mon, 2 Jun 2025 23:46:53 +0900</pubDate>
    </item>
    <item>
      <title>&amp;lt;2025.05.19&amp;gt; 페이지네이션, 무한스크롤</title>
      <link>https://yy9611.tistory.com/48</link>
      <description>&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;  페이지네이션(Pagination)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-pm-slice=&quot;1 1 []&quot;&gt;많은 데이터를 한 번에 다 보여주면 느려지니까, 적당히 나눠서 보여주는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&amp;nbsp;  &lt;/span&gt;&lt;span&gt;&lt;b&gt;무한스크롤(Infinite Scroll)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스크롤을 내릴 때마다 자동으로 데이터를 더 보여주는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&amp;nbsp;  &lt;/span&gt;&lt;span&gt;&lt;b&gt;커서 기반 페이지네이션(Cursor Pagination)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;어디까지 봤는지 표시&quot;해서 그 다음 데이터를 보여주는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;  오프셋 기반&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이지 번호로 데이터를 나눠 보여줌&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT * FROM item ORDER BY created_at DESC LIMIT 10 OFFSET 100000000;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이런 쿼리는 앞에 1억 개 데이터를 &lt;/span&gt;&lt;span&gt;&lt;b&gt;먼저 읽고&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 그 다음 10개를 준다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;rarr; &lt;/span&gt;&lt;span&gt;&lt;b&gt;점점 느려진다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&amp;nbsp;커서 기반&lt;/span&gt;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&quot;마지막으로 본 아이디(ID)&quot;를 기억하고, 그 다음부터 보여준다.&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;1페이지: id 1~10 &amp;rarr; 마지막은 id 10&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;2페이지: &lt;/span&gt;&lt;span&gt;WHERE id &amp;gt; 10&lt;/span&gt;&lt;span&gt; &amp;rarr; id 11부터 다시 10개 보여줌!&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;즉, 이미 본 데이터는 건너뛰고 바로 다음 것만 보여줍니다. 훨씬 빠름&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Spring에서 커서 기반 무한스크롤 구현 방법&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1. 헬퍼 클래스 만들기 (ScrollPaginationCollection)&lt;/span&gt;&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class ScrollPaginationCollection&amp;lt;T&amp;gt; {
    private final List&amp;lt;T&amp;gt; itemsWithNextCursor; // 데이터 + 다음 거 1개
    private final int countPerScroll;

    public boolean isLastScroll() {
        return this.itemsWithNextCursor.size() &amp;lt;= countPerScroll;
    }

    public List&amp;lt;T&amp;gt; getCurrentScrollItems() {
        return isLastScroll() ? itemsWithNextCursor : itemsWithNextCursor.subList(0, countPerScroll);
    }

    public T getNextCursor() {
        return itemsWithNextCursor.get(countPerScroll - 1);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&amp;nbsp;이 클래스는 다음 스크롤이 있는지 확인하고, 현재 보여줄 데이터만 잘라주는 역할을 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;2. 서비스 로직에서 사용&lt;/span&gt;&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public GetFeedsResponse getFeeds(String userEmail, Long roomId, int size, Long lastFeedId) {
    PageRequest pageRequest = PageRequest.of(0, size + 1); // 다음 페이지 있는지 확인하려고 +1
    Page&amp;lt;Feed&amp;gt; page = feedRepository.findAllByRoomAndIdLessThanOrderByIdDesc(room, lastFeedId, pageRequest);

    ScrollPaginationCollection&amp;lt;Feed&amp;gt; feedsCursor = ScrollPaginationCollection.of(page.getContent(), size);
    return GetFeedsResponse.of(feedsCursor, ..., ...);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&amp;nbsp; lastFeedId는 &quot;지금까지 본 마지막 게시글 ID&quot;&lt;/li&gt;
&lt;li&gt;size + 1&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;은 다음 페이지가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;있는지 없는지 확인하려고 하나 더 가져온 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;3. 클라이언트 응답용 DTO&lt;/span&gt;&lt;/h3&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;public class GetFeedsResponse {
    private List&amp;lt;FeedsInfoResponse&amp;gt; contents;
    private long totalElements;
    private long nextCursor;

    public static GetFeedsResponse of(...) {
        if (feedsScroll.isLastScroll()) {
            return newLastScroll(...); // 다음 거 없음
        }
        return newScrollHasNext(...); // 다음 커서 포함
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;nextCursor == -1 &amp;rarr; 마지막 페이지라는 뜻&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;프론트에서는 이 값 보고 &quot;더 이상 요청 안 함&quot;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&amp;nbsp;예시 요청&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;최초 요청:&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /feed?roomId=1&amp;amp;size=1&amp;amp;lastFeedId=9223372036854775807&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;응답:&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{
  &quot;contents&quot;: [ { &quot;feedId&quot;: 20, ... } ],
  &quot;nextCursor&quot;: 20
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;다음 요청:&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /feed?roomId=1&amp;amp;size=1&amp;amp;lastFeedId=20&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;rarr; 이런 식으로 &lt;/span&gt;&lt;span&gt;&lt;b&gt;nextCursor &amp;rarr; lastFeedId&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 로 계속 요청하며 무한 스크롤이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL/Spring</category>
      <author>Yhyeok</author>
      <guid isPermaLink="true">https://yy9611.tistory.com/48</guid>
      <comments>https://yy9611.tistory.com/48#entry48comment</comments>
      <pubDate>Mon, 19 May 2025 21:55:22 +0900</pubDate>
    </item>
    <item>
      <title>&amp;lt;2025.05.08&amp;gt; TestCode - 단위 테스트</title>
      <link>https://yy9611.tistory.com/44</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅단위 테스트&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작은 코드 조각을 검증&lt;/li&gt;
&lt;li&gt;빠르게 수행&lt;/li&gt;
&lt;li&gt;격리된 방식으로 처리하는 자동화된 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt; 좋은 테스트&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 모델 및 알고리즘 ( Entity, Utill 클래스)&lt;br /&gt;- 노력 대비 단위 테스트의 가치가 매우 높다.&lt;br /&gt;- 해당 코드가 복잡하거나 중요한 로직을 수행해서 테스트의 회귀 방지 향상&lt;br /&gt;- 외부 의존성이 없어서 테스트 유지비를 낮추기 때문에 저렴&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  나쁜 테스트&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단한 코드 (간단한 DTO, Utill) , @Getter, @Setter, @Builder 등등&lt;br /&gt;- 이러한 테스트는 가치가 없어서 할 필요 X&lt;/li&gt;
&lt;li&gt;컨트롤러&amp;nbsp;- 엔드투 엔트 테스트가 적합- 단위 테스트를 적용 X&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1646&quot; data-origin-height=&quot;1145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MUm99/btsNQFmkeeD/Ex0boY1oabVVWAYwUVHtR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MUm99/btsNQFmkeeD/Ex0boY1oabVVWAYwUVHtR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MUm99/btsNQFmkeeD/Ex0boY1oabVVWAYwUVHtR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMUm99%2FbtsNQFmkeeD%2FEx0boY1oabVVWAYwUVHtR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;449&quot; height=&quot;1145&quot; data-origin-width=&quot;1646&quot; data-origin-height=&quot;1145&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 단위 테스트의 4가지 특성&lt;br /&gt;1. 회귀 방지 -&amp;gt; 기능 오류를 방지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 리팩토링 내성 -&amp;gt; 리팩토링을 해도 테스트가 깨지지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 빠른 피드백 -&amp;gt; 빠른 테스트 속도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 유지 보수성 -&amp;gt; 가독성과 재사용성이 좋고 실행되기 쉽게 작성되어야함.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 빠른 피드백&amp;nbsp;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존성이 없는 코드에 대한 테스트 작성 (POJO 테스트)&lt;br /&gt;- Entity, Utill 클래스&lt;/li&gt;
&lt;li&gt;의존성을 mock 처리하여 핵심 비즈니스 로직에 대한 테스트 코드만을 작성&lt;br /&gt;- 복잡한 비즈니스 로직이 포함된 Service 클래스&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ Mock&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mock은 테스트 할 때, 필요한 실제 객체와 동일한 모의 객체를 만들어 테스트의 효율성을 높이기 위해 사용하는 '가짜 객체'&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 사용 하는 경우&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;실제 객체를 만들기엔 시간과 비용이 많이 드는 경우&lt;/li&gt;
&lt;li&gt;의존성이 길게 걸쳐져있어서 테스트를 제대로 구현하기 어려운 경우&lt;/li&gt;
&lt;li&gt;테스트 작성을 위한 환경 구축이 어려운 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 종류 및 사용 방법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Mock : 특정 클래스 위에 선언하면, 해당 클래스를 가짜 객체로 만듬&lt;/li&gt;
&lt;li&gt;@InjectMocks : @Mock 으로 생성된 mock 객체를 자동으로 DI 해주는 어노테이션&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1746710806018&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class) // Mock Application Context Load
public class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;
    @Mock
    private ProductRepository productRepository;
    @Mock
    private OrderLineRepository orderLineRepository;

    @InjectMocks
    private OrderService orderService;
 
 
    @Test
		void test() {
			...
		}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL/Spring</category>
      <author>Yhyeok</author>
      <guid isPermaLink="true">https://yy9611.tistory.com/44</guid>
      <comments>https://yy9611.tistory.com/44#entry44comment</comments>
      <pubDate>Thu, 8 May 2025 20:28:34 +0900</pubDate>
    </item>
    <item>
      <title>&amp;lt;2025.05.07&amp;gt; 연관관계 N+1</title>
      <link>https://yy9611.tistory.com/43</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅단방향 연관관계&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단방향 연관관계는 아래와 같이 연관관계에 있는 두 객체 사이에 한 방향으로의 참조가 존재하는 상태&lt;br /&gt;- Team -&amp;gt; Member&lt;br /&gt;- Member -&amp;gt; Team&lt;br /&gt;Team 입장에서는 Member 와 1:N 연관관계를 가지지만, Member 입장에서는 Team 과 N:1 연관관계이고 어떤 방향이든 두 객체 사이의 한 방향의 참조만 존재한다면 모두 단방향 연관관계라 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔ @OneToOne&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@OneToOne 연관관계는 두 테이블 / 객체가 1:1 연관관계일 때 사용&lt;br /&gt;- 1:1연관관계에 있는 객체간 참조 방향을 어떻게할지 잘 결정해야함.&lt;br /&gt;- 물리적으로 분리되어야하는 테이블인지 고민 (데이터의 생명주기 혹은 사용 패턴을 토대로 고민 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔ @ManyToOne&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블상의 외래키 위치와 객체상의 참조 위치가 동일해 헷갈리지 않음&lt;/li&gt;
&lt;li&gt;별다른 설정 없이도 우리가 생각한대로 SQL 쿼리가 잘 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1746634501517&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Team {
	
	@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = &quot;team_id&quot;)
	private Long id;
	
	private String name;	
	private LocalDateTime createdAt;
	private LocalDateTime updatedAt;
}

@Entity
public class Member {

	@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = &quot;member_id&quot;)
	private Long id;
	
	private String name;
	private int age;
	
	@ManyToOne
	@JoinColumn(name = &quot;team_id&quot;)
	private Team team;
	
	private LocalDateTime createdAt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔ @OneToMany&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RDB 의 외래키의 위치와 객체 참조의 위치가 다르다.&lt;/li&gt;
&lt;li&gt;1번 문제로 인해 INSERT 쿼리 요청 시, 불필요한&amp;nbsp; UPDATE 쿼리가 발생.&lt;/li&gt;
&lt;li&gt;단방향 연관관계로 사용할 때, @JoinColum을 반드시 사용해야함. 사용 안했을땐, 불필요한 연결 테이블이 생성될 가능성있음&lt;/li&gt;
&lt;li&gt;연관관계에 있는 Entity(member)를 save() 해주지 않으면 제대로 저장 X&lt;br /&gt;이런 경우 반드시 cascade 옵션 중 PERSIST 를 사용해야함.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단방향 연관관계의 방향으로 @OneToMany 를 사용했을 때 주의해야할 것들이 존재할 뿐이지 명확한 단점이 있는 것은 아니라고 생각한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@OneToMany 연관관계는 사용하면 안돼! 가 아니라 이런 포인트에서의 주의해야할 부분이 있으니 Trade-Off 하자! 로 접근하는게 더 좋을 것&lt;/b&gt; 같다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;도메인을 정의할 때 더 자연스러운 방향으로 설계하는게 비즈니스 로직을 개발하는 과정에서 더 자연스럽고 더 잘 읽히는 코드를 만들 수 있다.&lt;/li&gt;
&lt;li&gt;항상 같이 조회되는 상황에서는 @OneToMany 로 조회했을 때 코드가 자연스러워 진다.&lt;/li&gt;
&lt;li&gt;Post - Hashtag 처럼 Hashtag &amp;rarr; Post 로 참조할 일이 없고 항상 Post &amp;rarr; Hashtag 로만 참조하는 조회패턴을 가진다면 @OneToMany 를 적극 고려할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 양방향 연관 관계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- 객체 사이에는 &quot;양방향&quot;이라는 개념 X&lt;br /&gt;&amp;nbsp;Team -&amp;gt; Member, Member -&amp;gt; Team 두 단방향 연관관계가 존재하는 상황을 양방향 연관관계라고 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✔ 양방향 연관관계를 지양해야하는 이유&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;양방향 연관관계를 지양해야한다는 말은 많이 들어봤을거라 생각한다! 그럼 왜 그럴까?
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;불필요한 연관관계로 인해 설계의 복잡도가 증가한다.&lt;/li&gt;
&lt;li&gt;양방향 의존성은 순환참조 및 데이터 동기화 등에서 문제가 생길 가능성이 존재한다.&lt;/li&gt;
&lt;li&gt;객체 사이의 과도한 결합으로 인해 서로 변경을 전파하게되어 변경에 유연하지 못한 구조가 된다. &amp;rdquo;OOP 의 의존성&amp;rdquo; 에 대한 이야기!&lt;/li&gt;
&lt;li&gt;객체의 독립성이 저하되어 테스트 코드를 작성하기 어렵다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✔ 단점만 있는 기술은 없다!&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;양방향 연관관계는 그럼 절대 쓰지말아야하는 걸까?&lt;/li&gt;
&lt;li&gt;그렇지 않다! &lt;b&gt;그럼 어떤 상황에서 양방향 연관관계를 고려해볼 수 있을까?&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이미 @ManyToOne 연관관계를 사용하고 있는 상황에서 @OneToMany 방향으로의 조회(객체 참조)패턴이 많이 필요한 상황 &amp;rArr; &amp;ldquo;단순 조회 목적!&amp;rdquo;&lt;/li&gt;
&lt;li&gt;@OneToMany 단방향 연관관계 매핑을 사용할 때 발생하는 &lt;b&gt;&amp;ldquo;INSERT 쿼리시 연관관계 처리를 위한 UPDATE 쿼리가 함께 발생하는 문제&amp;rdquo;&lt;/b&gt; 를 해소하기 위한 상황 (대안 존재함!)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;양방향 연관관계는 &amp;ldquo;필수&amp;rdquo; 가 아니라 &amp;ldquo;필요&amp;rdquo; 에 의해서 추가되어야한다는 것을 꼭 기억하자!&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;우선 하나의 단방향 연관관계로 시작하자!! 이후에 반대 방향의 객체 참조가 필요하다면 양방향 연관관계를 추가해도 테이블에 영향을 주지 않기 때문에 충분히 유연하다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ N+1&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Todo 와 Comment 는 1:N 연관관계를 가지는 테이블이다.&lt;/li&gt;
&lt;li&gt;Todo 와 Comment 객체는 양방향 연관관계 매핑을 맺고 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;양방향 연관관계를 사용한 이유는 @OneToMany, @ManyToOne 에 대한 N+1 문제를 하나의 예제코드에서 모두 살펴보기 위함이다!!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;두 연관관계 매핑에서 모두 FetchType.EAGER 를 적용했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1746634962606&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Todo {
	
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = &quot;todo_id&quot;)
  private Long id;
  
  private String contents;
  private LocalDate date = LocalDate.now();
  private boolean isComplete = false;
  
  @OneToMany(mappedBy = &quot;todo&quot;, fetch = FetchType.EAGER)
  public List&amp;lt;Comment&amp;gt; comments = new ArrayList&amp;lt;&amp;gt;();
}

@Entity
public class Comment {

  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = &quot;comment_id&quot;)
  private Long id;
  
  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = &quot;todo_id&quot;)
  private Todo todo;
  private String contents;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔ N+1 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N+1 문제는 JPA 를 이용해 연관관계에 있는 두 테이블의 데이터를 조회할 때 JOIN 을 이용한 SELECT 쿼리 1개가 발생할거라 기대했지만, &lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;1&quot;&gt;JOIN 을 이용하지 않은 1개의 SELECT 쿼리 + 연관관계에 있는 데이터를 조회하기 위한 N개의 SELECT 쿼리로 총 N+1 개의 SELECT 쿼리가 발생하는 문제&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;3&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot; data-token-index=&quot;3&quot;&gt;▶ ORM 이 가지는 고질적인 문제인 N+1 문제 발생 이유?&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&amp;nbsp;- JPA 에서 연관관계에 있는 두 테이블 JOIN 해서 가지고오고 싶은지 확신 X&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&amp;nbsp;- &quot;연관관계 = JOIN&quot; 이라고 가정, 너무 많은 연관관계 그래프가 한번에 조회 될 가능성 Up!!&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&amp;nbsp;- 어떤 시점에&amp;nbsp; 어떤 연관관계 그래프를 한번에 조회해야하는지 JPA 입장에서 알 수 없다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&amp;nbsp; &amp;nbsp; N+1 문제가 발생하고, Fetch Join, Entity Graph와 같은걸 통해 명시적으로 알려줌.&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- N+1 문제는 JPA가 객체인 Entity 기반으로 쿼리를 자동으로 만들어주기 때문에 발생하는 문제.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;✔ N+1 문제가 발생하는 이유는 EAGER ?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예제 코드에서 Todo, Comment 는 서로에 대한 연관관계 Fetch 전략을 모두 EAGER 로 설정했다.&lt;/li&gt;
&lt;li&gt;경우에 따라 Lazy Loading 이 발생하지 않는 로직일 경우, 즉 연관관계를 참조하지 않는 경우 N개의 쿼리가 생략될 수는 있다.&lt;/li&gt;
&lt;li&gt;하지만, &lt;b&gt;연관관계에 있는 Entity 를 사용하는 시점에 결국 N개의 SELECT 쿼리가 발생해 N+1 문제가 여전히 발생하는걸 알 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;즉, LAZY Fetch 전략을 통해 N+1 문제를 완전히 해결할 수는 없다!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;✔ N+1 문제 해결&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Batch Size 조절&lt;/li&gt;
&lt;li&gt;Fetch Join&lt;/li&gt;
&lt;li&gt;EntityGraph&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Batch Size&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- Batch Size 를 조절하는 것은 근본적인 해결 방법 X&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- N개의 쿼리가 많아 특정 개수씩 묶어 In Query를 날려서 쿼리 개수를 줄임.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Fetch Join&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- Fetch Join은 JPA에서 지원하는 JOIN 방식&lt;br /&gt;&amp;nbsp;- Fetch Join을 사용할 때, 연관관계에 있는 Entity 까지 한번에 조회해 영속화 해줌.&lt;br /&gt;&amp;nbsp;- Fetch Join은 JPQL로 JOIN FETCH 구문 사용&lt;/p&gt;
&lt;pre id=&quot;code_1746635954261&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public interface TodoRepository extends JpaRepository&amp;lt;Todo, Long&amp;gt; {

  @Query(&quot;SELECT todo FROM Todo todo JOIN FETCH todo.comments&quot;)
  List&amp;lt;Todo&amp;gt; findAllWithFetchJoin();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- EntityGraph&lt;br /&gt;&amp;nbsp;- @EntityGraph 를 이용해 해당 Entity의 연관관계에 대한 정보를 넣어주는 방식&lt;/p&gt;
&lt;pre id=&quot;code_1746635936783&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public interface TodoRepository extends JpaRepository&amp;lt;Todo, Long&amp;gt; {

	@EntityGraph(attributePaths = [&quot;comments&quot;])
  @Query(&quot;SELECT todo FROM Todo todo&quot;)
  List&amp;lt;Todo&amp;gt; findAllWithEntityGraph();
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL/Spring</category>
      <author>Yhyeok</author>
      <guid isPermaLink="true">https://yy9611.tistory.com/43</guid>
      <comments>https://yy9611.tistory.com/43#entry43comment</comments>
      <pubDate>Wed, 7 May 2025 20:32:53 +0900</pubDate>
    </item>
    <item>
      <title>&amp;lt;2025.05.01&amp;gt; 영속성 컨텍스트</title>
      <link>https://yy9611.tistory.com/39</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅영속성이란?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;영속성( 永(길게) 續(속하다) - Persistence)이란, 프로그램이 종료된 이후에도 데이터가 사라지지 않고 저장되는 성질을 말합니다.&lt;/li&gt;
&lt;li&gt;즉, 메모리(휘발성 공간)가 아닌 &lt;b&gt;디스크&lt;/b&gt;(비휘발성 저장소)에 데이터를 영구히 보존하겠다라는 강력한 의지가 나타내기 위한 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbR7mo/btsNIOJZGNS/W0myy0CwRbSICLKlLCipt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbR7mo/btsNIOJZGNS/W0myy0CwRbSICLKlLCipt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbR7mo/btsNIOJZGNS/W0myy0CwRbSICLKlLCipt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbR7mo%2FbtsNIOJZGNS%2FW0myy0CwRbSICLKlLCipt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;711&quot; height=&quot;612&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅JPA에서 영속성이란?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA는 객체(Entity)와 데이터베이스 테이블(Table)간의 중간자 역할을 하며, 자바 객체를 데이터베이스에 &amp;lsquo;영속&amp;rsquo;시키는 과정을 관리하기 위한 라이브러리에요.&lt;/li&gt;
&lt;li&gt;한명의 유치원 선생님이 여러 유치원생을 관리하려면 유치원이라는 환경이 필요하듯 여러 객체를 관리하기 위한 환경이 바로 &lt;b&gt;영속성 컨텍스트(Persistence Context)&lt;/b&gt; 라는 메모리 영역을 필요해요.&lt;/li&gt;
&lt;li&gt;그래서 JPA에서의 영속성은 &lt;b&gt;영속성 컨텍스트(Persistence Context)&lt;/b&gt; 에 등록한 객체의 추이를 관찰하다 DB에 어떻게 영구히 저장할 것인가를 정하는 과정인 것이죠.&lt;/li&gt;
&lt;li&gt;JPA에서 영속은 영속성 컨텍스트에 객체를 등록된 상태.&lt;br /&gt;- 영속 (managed)&lt;br /&gt;&amp;nbsp;- 영속성 컨텍스트에 등록된 상태&lt;br /&gt;&amp;nbsp;- find(),save()&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;- 비영속(new/transient)&lt;br /&gt;&amp;nbsp;- 영속성 컨텍스트에 등록되지 않은 상태&lt;br /&gt;&lt;br /&gt;- 준영속 (detached)&lt;br /&gt;&amp;nbsp;- 영속성 컨텍스트에 저장되었다가 분리된 상태&lt;br /&gt;&amp;nbsp;- 준영속 상태의 엔티티는 컨텍스트가 제공하는 기능을 사용X&lt;br /&gt;&amp;nbsp;- clear() , refresh()&amp;nbsp;&lt;br /&gt;&lt;br /&gt;- 삭제(removed)&lt;br /&gt;&amp;nbsp;- 영속성 컨텍스트에서 제외되며 SQL 저장소에 delete문을 저장&lt;br /&gt;&amp;nbsp;- delete()&lt;br /&gt;&lt;br /&gt;- 반영(flush)&lt;br /&gt;&amp;nbsp;- 현재 영속성 컨텍스트에 상태를 확인하여 SQL 생성 후 데이터베이스에 query를 전송&lt;br /&gt;&amp;nbsp;- 단, commit 전이기 때문에 데이터베이스에 실제로 저장되지 않은 상태&lt;br /&gt;&amp;nbsp;- flush()&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅영속성 컨텍스트에서 객체 관리&lt;br /&gt;&amp;nbsp;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EntityManager가 객체를 관리하고 배출하는 곳은 EntityManagerFactory 에서 나오고, 배출 되는 시점은 transaction이&lt;br /&gt;시작 될 때이다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;생성된 EntityManager는 스프링은 내부적으로 발행된 TransactionThread에 바인딩되기 때문에 해당 트랜잭션 동안&lt;br /&gt;&amp;nbsp;동일한 EntityManager 인스턴스를 재사용할 수 있게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dEBf8c/btsNHIDZplC/RSZ59MbkRyiQCyyuevMir1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dEBf8c/btsNHIDZplC/RSZ59MbkRyiQCyyuevMir1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dEBf8c/btsNHIDZplC/RSZ59MbkRyiQCyyuevMir1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdEBf8c%2FbtsNHIDZplC%2FRSZ59MbkRyiQCyyuevMir1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;657&quot; height=&quot;366&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅영속성 컨텍스트의 특징&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1차 캐시 &amp;amp; 동등성 보장&lt;br /&gt;- 영속성 컨텍스트 내부에는 캐시라는 이름의 Map&amp;lt;@Id, Entity&amp;gt; 형태의 자료 구조가 존재&lt;br /&gt;- 캐시는 동일 트랜잭션 내에서만 유효한 로컬 캐시&lt;br /&gt;- 같은 엔티티를 반복 조회할 때, DB 접근 횟수를 줄여줌&lt;br /&gt;- 실무 고려사항 : 대량의 데이터를 처리하는 배치 작업에서는 주기적으로 영속성 컨텍스트를 초기화 clear() 하여 메모리 사용량 관리 필요가 있어요. 다만 clear() 했다고해서 대량의 데이터가 stack memory에서 없어지는건 아니기 때문에 OutOfMemory를 조심해야해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쓰기 지연&lt;br /&gt;- JPA는 SQL을 즉시 실행하지 않고 영속성 컨텍스트 내의 쓰기 지연 SQL 저장소에 모아둔다.&lt;br /&gt;- transaction.commit() 시점에 모아둔 SQL을 한 번에 DB로 전송&lt;br /&gt;- 작동 과정&lt;br /&gt;&amp;nbsp; 1. em. persist(entity)호출 : 엔티티를 1차 캐시에 저장 + INSERT SQL 생성 후 쓰기 지연 SQL 저장소에 보관&lt;br /&gt;&amp;nbsp; 2. transaction.commti() 호출 : flush() 자동 호출 &amp;gt; 쓰기 지연 SQL 저장소의 쿼리들을 DB에 전공 &amp;gt; 실제 커밋&lt;br /&gt;- commit 전 flush()를 통해 DB에 반영되는 경우&lt;br /&gt;&amp;nbsp; - repository.flush() 직접 호출 시&lt;br /&gt;&amp;nbsp; - JPQL 쿼리 실행 전 (자동)&lt;br /&gt;&amp;nbsp; - 식별자 생성 전략이 GenerationType.IDENTITY인 경우 save() 호출 시 즉시 INSERT SQL 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변경 감지 (Dirty Checking)&lt;br /&gt;- 트랜잭션 내에서 엔티티 값이 변경되면, 커밋 시점에 자동으로 UPDATE SQL이 생성&lt;br /&gt;- 별도의 update() 메소드 호출이 필요 없는 것이 JPA의 큰 특징&lt;br /&gt;- 변경 감지의 동장 원리&lt;br /&gt;&amp;nbsp; 1. 트랜잭션 시작 시, 엔티티의 최초 상태 스냅샷을 저장&lt;br /&gt;&amp;nbsp; 2. 플러시 시점에 현재 엔티티와 스냅샷을 비교 하여 변경된 엔티티 탐지&lt;br /&gt;&amp;nbsp; 3. 변경된 엔티티가 있으면 UPDATE SQL 생성 후 DB에 전송&lt;br /&gt;&lt;br /&gt;- 변경 감지 최적화 관점&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA는 기본적으로 엔티티의 모든 필드를 업데이트하는 SQL을 생성해요.&lt;/li&gt;
&lt;li&gt;필드가 많은 엔티티의 경우 변경 필드만 업데이트하도록 설정 가능하죠.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>TIL/Spring</category>
      <author>Yhyeok</author>
      <guid isPermaLink="true">https://yy9611.tistory.com/39</guid>
      <comments>https://yy9611.tistory.com/39#entry39comment</comments>
      <pubDate>Thu, 1 May 2025 20:21:32 +0900</pubDate>
    </item>
    <item>
      <title>&amp;lt;2025.04.30&amp;gt; 동시성 제어</title>
      <link>https://yy9611.tistory.com/38</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  동시성 제어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;- DBMS가 다수의 사용자 사이에서 동시에 작용하는 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;다중 트랜잭션의 상호 간섭 작용에서 DataBase를 보호하는 것을 의미.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;- 일반적으로 어플리케이션의 다수의 사용자의 요청을 처리&lt;br /&gt;&amp;nbsp; &amp;nbsp; -&amp;gt; 이러한 요청에는 DB 접근을 필요로 하는 요청 포함&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;- 요청이 들어올 때, 트랜잭션 단위로 DB 접근하는데 접근을 동시적으로 허용하면,&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp;DB의 일관성과 무결성이 깨진다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; 이를 방지하기 위해, 동시성제어(병행 제어) 로 DB를 보호.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;169&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JHn5D/btsNFqkb0rt/6YukKX3jfRkmVtHVry4941/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JHn5D/btsNFqkb0rt/6YukKX3jfRkmVtHVry4941/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JHn5D/btsNFqkb0rt/6YukKX3jfRkmVtHVry4941/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJHn5D%2FbtsNFqkb0rt%2F6YukKX3jfRkmVtHVry4941%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;802&quot; height=&quot;169&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;169&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;- 동시성 제어를 해야 하는 이유&lt;br /&gt;&amp;nbsp; &amp;nbsp;- Race Condition&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;- 두 개 이상의 스레드가 동시에 같은 데이터를 접근하여 값을 변경하고자 할 때,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 데이터의 예상치 못한 변경이 발생 할 수 있다.&lt;br /&gt;&amp;nbsp; &amp;nbsp;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqjDNN/btsNHaGW1Sf/Yk9ydTnTgRbZgEPNTkhK01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqjDNN/btsNHaGW1Sf/Yk9ydTnTgRbZgEPNTkhK01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqjDNN/btsNHaGW1Sf/Yk9ydTnTgRbZgEPNTkhK01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqjDNN%2FbtsNHaGW1Sf%2FYk9ydTnTgRbZgEPNTkhK01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;652&quot; height=&quot;250&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 동시성 제어 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Synchronized&lt;br /&gt;- 프로세스에 여러 스레드가 동시에 접근하는 것을 방지하기 위한 기법&lt;br /&gt;&amp;nbsp; - 메서드에 synchronized 키워드 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1746010703768&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public synchronized void decrease() {
		entity.decrease();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;- @Synchronized 어노테이션 추가&lt;/p&gt;
&lt;pre id=&quot;code_1746010736862&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Synchronized
public void decrease() {
		entity.decrease();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;- 코드 내부에 synchronized 블럭 추가&lt;/p&gt;
&lt;pre id=&quot;code_1746010764645&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void decrease() {
		synchronized(this) {
			entity.decrease();
		}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB에서의 Lock 제어&lt;/li&gt;
&lt;li&gt;비관적 락 (Pessimistic Lock)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;현재 트랜잭션이 접근한 데이터에 row 단위로 Lock을 걸어 다른 트랜잭션이 읽기(Shared Lock) 혹은 쓰기(Exclusive Lock) 접근을 하지 못하게 하는 방법입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Shared lock (읽기 잠금, s-lock)&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;락을 획득한 트랜잭션에서만 대상 레코드를 수정, 삭제 할 수 있으며 락을 획득하지 못한 트랜잭션은 읽기만 허용하는 방법. (PESSIMISTIC_READ)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Exclusive Lock (쓰기 잠금, x-lock)&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;락을 획득하지 못한 트랜잭션에서 대상 레코드를 수정, 삭제 뿐 아니라 읽기도 허용하지 않는 방법. (PESSIMISTIC_WRITE)&lt;/li&gt;
&lt;li&gt;읽기를 허용하지 않기 때문에 한 번에 하나의 트랜잭션만 작업을 수행함을 보장.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;b&gt;장점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블에 락을 걸어 다른 트랜잭션에서의 접근을 하지 못하게 하기 때문에 데이터의 일관성이 보장됩니다&lt;/li&gt;
&lt;li&gt;트랜잭션에서 데이터를 사용하기 전 락을 걸기 때문에, 데이터를 변경 중에 다른 트랜잭션과 충돌 가능성이 낮습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;b&gt;단점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 일관성을 보장하지만 동시 접속자가 많은 환경에서는 락 대기 시간으로 인해 성능에 영향을 줄 수 있다.&lt;/li&gt;
&lt;li&gt;다수의 트랜잭션이 서로 다른 순서로 여러 데이터에 락을 요청하면 데드락이 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;낙관적 락 (Optimistic Lock)&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;version(혹은 시간 관련 등의 컬럼으로도 가능) 컬럼을 추가하여 버전이 다르면 업데이트가 불가능하게 하는 방법&lt;/li&gt;
&lt;li&gt;간단히 얘기하자면, 해당 테이블에&amp;nbsp;&lt;b&gt;변경사항(수정)이 생겼을 때 버전이 올라가는 것입니&lt;/b&gt;다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시 요청(많은 재시도 횟수가 아님)에 대해 DB에 락을 걸지 않기 때문에, 비관적 락보다 성능 향상에 이점이 있습니다.&lt;/li&gt;
&lt;li&gt;낙관적 락은&amp;nbsp;충돌이 자주 발생하지 않는다고 가정하기 때문에,&amp;nbsp;많은 사용자가 동시에 데이터에 접근할 수 있습니다. 즉, 처리량을 향상시킬수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시에 요청하여 데이터 충돌이 발생했을 때 이를 해결하기 위한 추가적인 로직(재시도 로직)이 필요하여 구현 복잡성이 있습니다.&lt;/li&gt;
&lt;li&gt;데이터의 변경 빈도가 높은 시스템에서는 충돌이 자주 발생하기 때문에, 이를 해결하기 위한 추가적인 시간이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 락 (Distributed Lock)&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;분산락이란 여러 서버가(프로세스) 공유 데이터를 제어하기 위한 기술 입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;락을 획득한&lt;/b&gt;&amp;nbsp;프로세스 혹은 스레드만이 공유 자원 혹은&amp;nbsp;&lt;b&gt;Critical Section&lt;/b&gt;&amp;nbsp;에 접근할 수 있도록 하는 것 입니다.&lt;/li&gt;
&lt;li&gt;분산락의 장점은 서버 분산 환경에서도 프로세스들의 원자적 연산이 가능한 것 입니다.(아래 그림처럼요.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Iq0f4/btsNGGs5DNo/sFKHTTzPHmf33iRAiatxIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Iq0f4/btsNGGs5DNo/sFKHTTzPHmf33iRAiatxIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Iq0f4/btsNGGs5DNo/sFKHTTzPHmf33iRAiatxIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIq0f4%2FbtsNGGs5DNo%2FsFKHTTzPHmf33iRAiatxIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;662&quot; height=&quot;390&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;종류&lt;br /&gt;- Lettuce&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;spring-data-redis의 기본 구현체&lt;/li&gt;
&lt;li&gt;기본적으로 Spin Lock을 사용한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 Lock을 대기하는 상황에서, Lock을 획득할 수 있는지 계속 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;따라서 Lock을 획득하려는 스레드가 많을 경우 Redis에 부하가 집중된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Lock에 대한 타임아웃이 없어, Unlock(잠금 해제) 호출을 하지 못한 경우 Dead Lock을 유발할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Redisson&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pub/sub 방식을 사용한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Lock을 당장 획득할 수 없으면 대기한다.&lt;/li&gt;
&lt;li&gt;Lock이 획득 가능할 경우 Redis에서 클라이언트로 획득 가능함을 알린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Lock의 lease time 설정이 가능하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 설정된 lease time이 지난 경우 자동으로 Lock의 소유권을 회수하여 Dead Lock을 방지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>Yhyeok</author>
      <guid isPermaLink="true">https://yy9611.tistory.com/38</guid>
      <comments>https://yy9611.tistory.com/38#entry38comment</comments>
      <pubDate>Wed, 30 Apr 2025 17:58:19 +0900</pubDate>
    </item>
    <item>
      <title>&amp;lt;2025.04.24&amp;gt; 테스트 코드</title>
      <link>https://yy9611.tistory.com/36</link>
      <description>&lt;h1 data-pm-slice=&quot;1 2 []&quot;&gt;&lt;span&gt; &amp;nbsp; 테스트 코드의 중요성과 작성법 정리&lt;/span&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;1️⃣ 테스트 코드는 무엇이고 왜 작성해야 할까?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;소프트웨어 테스트란 &lt;/span&gt;&lt;span&gt;&lt;b&gt;해당 소프트웨어가 기대한 대로 잘 동작하는지 확인하는 과정&lt;/b&gt;&lt;/span&gt;&lt;span&gt;입니다. 개발자라면 누구나 테스트라는 과정을 경험했을 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;테스트 코드를 작성하면:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;소프트웨어의 결함을 조기에 발견&lt;/b&gt;&lt;/span&gt;&lt;span&gt;할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;완성도 높은 소프트웨어&lt;/b&gt;&lt;/span&gt;&lt;span&gt;를 개발할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;반복적인 테스트를 자동화&lt;/b&gt;&lt;/span&gt;&lt;span&gt;할 수 있어 &lt;/span&gt;&lt;span&gt;&lt;b&gt;시간을 절약&lt;/b&gt;&lt;/span&gt;&lt;span&gt;할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;시간이 지날수록 &lt;/span&gt;&lt;span&gt;&lt;b&gt;소프트웨어의 안정성&lt;/b&gt;&lt;/span&gt;&lt;span&gt;이 높아집니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;테스트 코드 작성의 이점&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;테스트를 자주 실행할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;리팩터링 시 안정감&lt;/b&gt;&lt;/span&gt;&lt;span&gt;을 준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;버그를 조기에 발견&lt;/b&gt;&lt;/span&gt;&lt;span&gt;할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;테스트 코드의 &lt;/span&gt;&lt;span&gt;&lt;b&gt;FIRST 원칙&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원칙의미&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;F (Fast)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;테스트는 빠르게 실행돼야 한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;I (Isolated)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;테스트는 독립적이어야 한다. 외부 시스템에 의존 X&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;R (Repeatable)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;반복 실행해도 결과는 동일해야 한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;S (Self-validating)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;테스트는 스스로 검증 가능해야 한다. (출력 X, 검증 O)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;T (Timely)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;테스트 코드는 실제 코드보다 먼저 or 동시에 작성되어야 한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;2️⃣ Spring Boot에서 작성할 수 있는 테스트 종류&lt;/span&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;테스트 종류&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;단위 테스트 (Unit Test)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;개별 코드 단위 테스트 (순수 Java 코드 위주)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;통합 테스트 (Integration Test)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;여러 모듈 간 상호작용 테스트 (ex. Repository + Service)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;E2E 테스트 (End-to-End Test)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;API의 처음부터 끝까지 흐름 테스트&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;인수 테스트 (Acceptance Test)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;사용자의 시나리오 전체 플로우 검증 (요구사항 충족 여부 확인)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;3️⃣ 테스트 코드 작성 전 기억할 점&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;테스트의 본질&lt;/b&gt;&lt;/span&gt;&lt;span&gt;은 시나리오 검증이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;멋진 코드보단 &lt;/span&gt;&lt;span&gt;&lt;b&gt;명확한 시나리오&lt;/b&gt;&lt;/span&gt;&lt;span&gt;가 더 중요하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;BDD(Given-When-Then) 방식을 염두에 두자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;테스트를 작성하기 전에 &lt;/span&gt;&lt;span&gt;&lt;b&gt;한글로 시나리오를 적어보자.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;4️⃣ Mocking과 Stubbing&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Test Double이란?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;테스트에 필요한 가짜 객체&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Mock&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 테스트 전용 가짜 객체 (Dummy, Stub, Spy처럼 동작 가능)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Stub&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 특정 메서드 호출 시 미리 정의한 응답 반환&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Spy&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 메서드 호출 여부, 횟수 등을 기록&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Fake&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 간단한 가짜 로직 포함 (Stub보다 더 복잡)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Java에서는 Mockito 등의 라이브러리로 Test Double 구현&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Mockito 예시&lt;/span&gt;&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
class SocialMemberServiceTest {

    @Mock
    private SocialMemberRepository socialMemberRepository;

    @InjectMocks
    private SocialMemberService socialMemberService;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Mock 객체로 시나리오 테스트&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Stubbing&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;when(memberRepository.findByEmail(any()))
    .thenReturn(Optional.of(new Member(...)));&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Spy로 메서드 호출 검증&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;verify(memberRepository, times(1)).findByEmail(any());&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Captor로 파라미터 검증&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Captor ArgumentCaptor&amp;lt;String&amp;gt; emailCaptor;
verify(memberRepository).findByEmail(emailCaptor.capture());
assertThat(emailCaptor.getValue()).isEqualTo(&quot;example@email.com&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;void 메서드 Stubbing&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;doNothing().when(memberRepository).delete(any());&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;5️⃣ 자주 등장하는 객체별 Unit Test 요령&lt;/span&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;객체&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;특징 및 테스트 요령&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Domain Entity / POJO&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;순수 단위테스트. Mocking 불필요.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Service&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;비즈니스 로직 테스트. Mocking 필요. (Repository 등 의존성 대체)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Client&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;외부 서비스 호출. Mock Server 사용. (&lt;/span&gt;&lt;span&gt;MockWebServer&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;WireMock&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Repository&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;JPA 사용 시 필요 적음. 직접 쿼리(JPQL/QueryDSL) 작성 시 통합 테스트 권장&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Controller&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;@WebMvcTest를 이용한 슬라이스 테스트 대체 가능 (우선순위 낮음)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;단위 테스트 우선순위&lt;/span&gt;&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Domain Entity &amp;gt; Application Service &amp;gt; Client &amp;gt; POJO &amp;gt; Repository &amp;gt; Controller&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;6️⃣ Unit Test 작성 시 주의할 점&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;true&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;의존성 관점&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;Repository &amp;rarr; Service &amp;rarr; Controller 순서로 작성&lt;/span&gt;&lt;br /&gt;&lt;span&gt;(Mock 객체가 믿을만한지 보장 필요)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Test Double 사용 시 &lt;/span&gt;&lt;span&gt;&lt;b&gt;Stubbing 신뢰성&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 확보&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;7️⃣ JPA + H2 Database를 활용한 통합 테스트&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;통합 테스트는 &lt;/span&gt;&lt;span&gt;&lt;b&gt;가볍고 빠르게&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 실행돼야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;@DataJpaTest&lt;/span&gt;&lt;span&gt; 사용 &amp;rarr; 최소한의 Context 로드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;실제 운영 DB 사용 ❌ &amp;rarr; &lt;/span&gt;&lt;span&gt;&lt;b&gt;Isolated &amp;amp; Repeatable 원칙 위배&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;rarr; &lt;/span&gt;&lt;span&gt;&lt;b&gt;Embedded H2&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 사용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;8️⃣ 기타: 테스트 방법론&lt;/span&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;방법론&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;TDD (Test Driven Development)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;테스트 &amp;rarr; 코드 개발 반복&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;BDD (Behavior Driven Development)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;시나리오 기반 테스트 (Given-When-Then)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;ATDD (Acceptance Test Driven Development)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;인수 테스트 기반 개발&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;지금 중요한 건 방법론보다 &quot;일단 테스트 작성&quot;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;9️⃣ 마무리&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;true&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;테스트 코드에서 가장 중요한 것은 &lt;/span&gt;&lt;span&gt;&lt;b&gt;시나리오 검증&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;rarr; 코드를 읽는 사람이 &lt;/span&gt;&lt;span&gt;&lt;b&gt;쉽게 이해&lt;/b&gt;&lt;/span&gt;&lt;span&gt;할 수 있어야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>Yhyeok</author>
      <guid isPermaLink="true">https://yy9611.tistory.com/36</guid>
      <comments>https://yy9611.tistory.com/36#entry36comment</comments>
      <pubDate>Thu, 24 Apr 2025 22:59:53 +0900</pubDate>
    </item>
  </channel>
</rss>