<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>최지희 님의 블로그</title>
    <link>https://skoitart2e.tistory.com/</link>
    <description>최지희 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Sun, 21 Jun 2026 14:26:04 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>jiheechoi</managingEditor>
    <item>
      <title>LangChain + FAISS + Redis 기반 RAG 챗봇 구현</title>
      <link>https://skoitart2e.tistory.com/17</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 CrossAlpha를 개발하면서 RAG 기반 챗봇을 구현한 경험을 정리한 글이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CrossAlpha는 투자자가 &quot;왜 이 거래를 했는가&quot;를 기록하고, 행동 패턴을 분석하여 투자 습관을 개선할 수 있도록 돕는 트레이딩 저널 및 AI 기반 투자 행동 분석 서비스이다. CrossAlpha의 종목 상세 페이지에는 &lt;b&gt;차트 분석 AI 어시스턴트&lt;/b&gt; 기능이 있는데, 사용자가 현재 보유 종목의 기술적 지표(RSI, SMA, 볼린저 밴드 등)에 대해 자연어로 질문하면 AI가 답변해주는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 LLM을 호출하는 것이 아니라, 기술적 지표 문서를 기반으로 검색하고 현재 종목의 실시간 데이터를 결합하여 답변하는 &lt;b&gt;RAG(Retrieval-Augmented Generation)&lt;/b&gt; 구조로 구현했다. 여기에 Redis를 활용한 대화 히스토리 관리를 추가하여 이전 대화 맥락을 기억하는 챗봇을 만들었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 왜 RAG인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 LLM에 질문만 던지는 방식으로는 두 가지 문제가 있었다.&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;b&gt;문제 1 &amp;mdash; LLM이 기술적 지표를 틀리게 설명할 수 있다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RSI, 볼린저 밴드 같은 기술적 지표는 정확한 해석 기준이 있다. 하지만 LLM은 hallucination으로 잘못된 해석을 줄 수 있다. 예를 들어 RSI 70 이상이 과매수 구간이라는 기준을 틀리게 설명하거나, 볼린저 밴드 해석을 임의로 만들어낼 수 있다. 신뢰할 수 있는 문서를 기반으로 답변하도록 RAG 구조를 도입한 이유가 여기에 있다.&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;b&gt;문제 2 &amp;mdash; LLM은 현재 종목의 실시간 데이터를 모른다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;지금 NVDA RSI가 어때?&quot;라고 물어봐도 LLM은 실시간 데이터를 알 수 없다. 이건 RAG로 해결할 수 있는 문제가 아니라, 매 요청마다 현재 종목의 기술적 지표 스냅샷을 직접 context로 주입하는 방식으로 해결했다.&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;이러한 문제들로 인해 결국 이 두 접근이 결합된 구조를 선택하였다. RAG로 문서 기반의 정확한 해석을 제공하고, 스냅샷 주입으로 실시간 종목 데이터를 반영하는 방식이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 개발 환경 및 설정&lt;/h2&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div data-test-render-count=&quot;1&quot;&gt;
&lt;div data-is-streaming=&quot;false&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- 전체 스택&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CrossAlpha의 백엔드는 다음과 같은 환경으로 구성되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 118px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 17.209302%; height: 18px;&quot;&gt;백엔드&lt;/td&gt;
&lt;td style=&quot;width: 82.674419%; height: 18px;&quot;&gt;FastAPI (Docker 컨테이너)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 17.209302%; height: 18px;&quot;&gt;배포&lt;/td&gt;
&lt;td style=&quot;width: 82.674419%; height: 18px;&quot;&gt;Railway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 17.209302%; height: 18px;&quot;&gt;DB&lt;/td&gt;
&lt;td style=&quot;width: 82.674419%; height: 18px;&quot;&gt;PostgreSQL (Railway)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 17.209302%; height: 18px;&quot;&gt;대화 히스토리&lt;/td&gt;
&lt;td style=&quot;width: 82.674419%; height: 18px;&quot;&gt;Redis (Railway)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 17.209302%; height: 18px;&quot;&gt;LLM&lt;/td&gt;
&lt;td style=&quot;width: 82.674419%; height: 18px;&quot;&gt;GPT-4o-mini (OpenAI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 17.209302%; height: 18px;&quot;&gt;벡터스토어&lt;/td&gt;
&lt;td style=&quot;width: 82.674419%; height: 18px;&quot;&gt;FAISS (로컬 파일)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Railway 위에 FastAPI Docker 컨테이너, PostgreSQL, Redis가 같은 프로젝트 안에서 내부 네트워크로 연결되어 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- Railway에 Redis 서비스 추가하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Railway 대시보드에서 아래 순서로 Redis를 추가했다.&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;1. 프로젝트 대시보드 &amp;rarr; Add 클릭&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-22 오후 7.27.41.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyQ1Yh/dJMb99NebD9/mFTBGwPGZiyGlXvAiWaq8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyQ1Yh/dJMb99NebD9/mFTBGwPGZiyGlXvAiWaq8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyQ1Yh/dJMb99NebD9/mFTBGwPGZiyGlXvAiWaq8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyQ1Yh%2FdJMb99NebD9%2FmFTBGwPGZiyGlXvAiWaq8k%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2026-05-22 오후 7.27.41.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Database &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;rarr; &lt;/span&gt;Redis 선택&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-22 오후 7.27.48.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HkBBx/dJMcaipUvNo/Fg1HStMr6qLpTKW84rRFEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HkBBx/dJMcaipUvNo/Fg1HStMr6qLpTKW84rRFEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HkBBx/dJMcaipUvNo/Fg1HStMr6qLpTKW84rRFEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHkBBx%2FdJMcaipUvNo%2FFg1HStMr6qLpTKW84rRFEK%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2026-05-22 오후 7.27.48.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-22 오후 7.28.28.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nGxmE/dJMcadIOjTv/Hz7L3735ynoYxNEJ1hCakk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nGxmE/dJMcadIOjTv/Hz7L3735ynoYxNEJ1hCakk/img.png&quot; data-alt=&quot;대시보드에 Redis 추가 완료!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nGxmE/dJMcadIOjTv/Hz7L3735ynoYxNEJ1hCakk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnGxmE%2FdJMcadIOjTv%2FHz7L3735ynoYxNEJ1hCakk%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2026-05-22 오후 7.28.28.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대시보드에 Redis 추가 완료!&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 추가 완료 후 FastAPI 서비스 &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Variables&lt;span&gt;&amp;nbsp;&lt;/span&gt;탭 &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Add Variable로&lt;span&gt;&amp;nbsp;&lt;/span&gt;REDIS_URL 추가&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-22 오후 7.39.30.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QsKN8/dJMcacb9Cmj/mPP4XeDJdKL8WRBKGt8k51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QsKN8/dJMcacb9Cmj/mPP4XeDJdKL8WRBKGt8k51/img.png&quot; data-alt=&quot;Redis variables에서 REDIS_URL값 복붙하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QsKN8/dJMcacb9Cmj/mPP4XeDJdKL8WRBKGt8k51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQsKN8%2FdJMcacb9Cmj%2FmPP4XeDJdKL8WRBKGt8k51%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2026-05-22 오후 7.39.30.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Redis variables에서 REDIS_URL값 복붙하기&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Railway 내부에서 서비스끼리 통신할 때는&lt;span&gt;&amp;nbsp;&lt;/span&gt;private endpoint를 사용한다. Redis를 추가하면 두 가지 URL이 생성된다.&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;  변수 용도
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;REDIS_URL&lt;/td&gt;
&lt;td&gt;Railway 내부 통신 (배포 환경) &amp;mdash; 무료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REDIS_PUBLIC_URL&lt;/td&gt;
&lt;td&gt;외부 접근 (로컬 개발 환경) &amp;mdash; 소량 비용 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 환경에서는 Railway Variables에 설정한&lt;span&gt;&amp;nbsp;&lt;/span&gt;REDIS_URL이 자동으로 주입되고, 로컬 개발 환경에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;.env&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일에&lt;span&gt;&amp;nbsp;&lt;/span&gt;REDIS_PUBLIC_URL&lt;span&gt;&amp;nbsp;&lt;/span&gt;값을&lt;span&gt;&amp;nbsp;&lt;/span&gt;REDIS_URL로 넣어서 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- 패키지 설치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;requirements.txt에 아래 패키지를 추가했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;redis
langchain-redis&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cmake&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;pip install redis langchain-redis&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 전체 구조&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;gcode&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;사용자 질문
    &amp;darr;
FAISS에서 관련 기술적 지표 문서 검색 (Retrieval)
    +
현재 종목 기술적 지표 스냅샷 주입 (RSI, SMA, 볼린저 밴드 등)
    +
Redis에서 이전 대화 히스토리 로드
    &amp;darr;
LLM (GPT-4o-mini)
    &amp;darr;
답변 생성 + Redis에 대화 저장&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 기술적 지표 문서 벡터스토어 구성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG의 핵심은 검색할 문서를 벡터로 변환해서 저장하는 벡터스토어다. CrossAlpha에서는 RSI, SMA, 볼린저 밴드, 복합 신호 해석 문서를 마크다운 형태로 작성하고 FAISS에 저장했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;# build_vectorstore.py
from pathlib import Path
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings

DOCS_DIR = Path(&quot;rag/docs&quot;)
SAVE_DIR = &quot;rag/vectorstore&quot;

def build_vectorstore():
    docs = []
    for path in DOCS_DIR.glob(&quot;*.md&quot;):
        content = path.read_text(encoding=&quot;utf-8&quot;)
        docs.append(
            Document(
                page_content=content,
                metadata={&quot;source&quot;: path.name},
            )
        )

    embeddings = OpenAIEmbeddings(api_key=&quot;YOUR_API_KEY&quot;)
    vectorstore = FAISS.from_documents(docs, embeddings)
    vectorstore.save_local(SAVE_DIR)

if __name__ == &quot;__main__&quot;:
    build_vectorstore()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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: #000000; text-align: start;&quot;&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;문서를 추가하거나 수정할 때마다 이 스크립트를 실행해서 벡터스토어를 다시 빌드했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 종목 스냅샷 + RAG 결합한 체인 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangChain의&lt;span&gt;&amp;nbsp;&lt;/span&gt;RunnableParallel을 활용해서 세 가지 context를 병렬로 준비하고 LLM에 넘겼다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;lisp&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;chain = (
    RunnableParallel(
        {
            # 현재 종목 기술적 지표 스냅샷
            &quot;snapshot_context&quot;: RunnableLambda(_build_snapshot_context),

            # FAISS에서 관련 문서 검색
            &quot;retrieved_context&quot;: (
                RunnableLambda(lambda x: x[&quot;question&quot;])
                | retriever
                | RunnableLambda(_format_docs)
            ),

            # 사용자 질문
            &quot;question&quot;: RunnableLambda(lambda x: x[&quot;question&quot;]),

            # Redis에서 대화 히스토리 로드
            &quot;chat_history&quot;: RunnableLambda(lambda x: history.messages),
        }
    )
    | prompt
    | llm
    | StrOutputParser()
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;rarr; RunnableParallel을 쓰면 스냅샷 가져오기와 문서 검색이 동시에 실행되어 응답 속도가 빨라진다.&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;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;roboconf&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;def _build_snapshot_context(inputs: dict) -&amp;gt; str:
    snapshot = get_technical_summary_with_llm(ticker=ticker, rng=range_)
    return f&quot;&quot;&quot;
ticker: {snapshot.ticker}
rsi14: {snapshot.indicators.rsi14}
sma20: {snapshot.indicators.sma20}
sma50: {snapshot.indicators.sma50}
bollinger_upper: {snapshot.indicators.bollinger.upper}
bollinger_middle: {snapshot.indicators.bollinger.middle}
bollinger_lower: {snapshot.indicators.bollinger.lower}
&quot;&quot;&quot;.strip()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. Redis로 대화 히스토리 관리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단발성 질답이 아니라 이전 대화 맥락을 기억하는 챗봇을 만들기 위해 Redis를 도입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangChain의&lt;span&gt;&amp;nbsp;&lt;/span&gt;RedisChatMessageHistory를 사용하면 히스토리 저장/로드를 자동으로 처리해준다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;from langchain_redis import RedisChatMessageHistory

history = RedisChatMessageHistory(
    session_id=session_id,
    redis_url=REDIS_URL,
    ttl=60 * 60 * 24 * 7,  # 7일
)

# 히스토리 로드 (자동으로 Redis에서 가져옴)
print(history.messages)

# 답변 후 저장
history.add_user_message(question)
history.add_ai_message(answer)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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: #000000; text-align: start;&quot;&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;TTL을 7일로 설정하면 7일간 대화가 없으면 자동으로 만료된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. session_id로 사용자/종목별 대화 분리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 사용자가 여러 종목을 동시에 사용하기 때문에 대화가 섞이지 않도록 session_id로 분리했다. CrossAlpha에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;{user_id}:{ticker}&lt;span&gt;&amp;nbsp;&lt;/span&gt;조합으로 session_id를 구성했다.&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;  session_id의미
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;42:NVDA&lt;/td&gt;
&lt;td&gt;user 42번의 NVDA 대화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;42:TSLA&lt;/td&gt;
&lt;td&gt;user 42번의 TSLA 대화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99:NVDA&lt;/td&gt;
&lt;td&gt;user 99번의 NVDA 대화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&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;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;const sessionId = `${userId}:${ticker}`

await fetch(`/api/tickers/${ticker}/assistant`, {
    method: &quot;POST&quot;,
    body: JSON.stringify({
        question: question,
        range: &quot;3M&quot;,
        session_id: sessionId,
    })
})&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 테스트&lt;/h3&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;p data-ke-size=&quot;size16&quot;&gt;  테스트용 request body 작성&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;json&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;// 첫 번째 질문
{
    &quot;question&quot;: &quot;현재 RSI 기준으로 과매수 구간까지 몇 포인트 남았어?&quot;,
    &quot;range&quot;: &quot;3M&quot;,
    &quot;session_id&quot;: &quot;test:NVDA&quot;
}

// 두 번째 질문 &amp;mdash; 히스토리 없이는 답할 수 없음
{
    &quot;question&quot;: &quot;방금 말한 포인트 수치가 정확히 얼마였지?&quot;,
    &quot;range&quot;: &quot;3M&quot;,
    &quot;session_id&quot;: &quot;test:NVDA&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;/div&gt;
&lt;/div&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2769&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PAsD2/dJMcaaemFk0/uaO8mu5pLHCmiFmmQ2h6L0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PAsD2/dJMcaaemFk0/uaO8mu5pLHCmiFmmQ2h6L0/img.png&quot; data-alt=&quot;첫 번째 요청에 대한 실행 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PAsD2/dJMcaaemFk0/uaO8mu5pLHCmiFmmQ2h6L0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPAsD2%2FdJMcaaemFk0%2FuaO8mu5pLHCmiFmmQ2h6L0%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;2769&quot; height=&quot;482&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2769&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;첫 번째 요청에 대한 실행 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2766&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UZiu1/dJMcacDgmqz/z9o17eC5KRsEve9K4FOZi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UZiu1/dJMcacDgmqz/z9o17eC5KRsEve9K4FOZi1/img.png&quot; data-alt=&quot;두 번째 요청에 대한 실행 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UZiu1/dJMcacDgmqz/z9o17eC5KRsEve9K4FOZi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUZiu1%2FdJMcacDgmqz%2Fz9o17eC5KRsEve9K4FOZi1%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;2766&quot; height=&quot;471&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2766&quot; data-origin-height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;두 번째 요청에 대한 실행 결과&lt;/figcaption&gt;
&lt;/figure&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: #000000; text-align: start;&quot;&gt;두 번째 질문에서 첫 번째 답변의 수치를 정확히 기억하고 답하는 것을 확인하였다!&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>졸업 프로젝트</category>
      <author>jiheechoi</author>
      <guid isPermaLink="true">https://skoitart2e.tistory.com/17</guid>
      <comments>https://skoitart2e.tistory.com/17#entry17comment</comments>
      <pubDate>Mon, 25 May 2026 00:47:16 +0900</pubDate>
    </item>
    <item>
      <title>FinanceDataReader + GPT API로 주식 보조 지표 자동 해석하기</title>
      <link>https://skoitart2e.tistory.com/16</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;*작성일자: 2025-11-26 / 작성자: 컴퓨터공학과 2376302 최지희&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느덧 졸업을 1년 앞둔 컴공과 3학년 학부생이 된 나는, 이번 학기부터 학부 졸업 과제인 캡스톤 팀 프로젝트를 시작하게 되었다. 우리 팀이 진행하는 프로젝트의 주제는 &lt;b&gt;&amp;lt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;시간과 전문성이 부족한 개인 주식 투자자를 위한, 포트폴리오 기반 투자 행동 인사이트 &amp;amp; 학습&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;&lt;b&gt; 서비스&amp;gt;&lt;/b&gt;이다. 이 서비스는 투자에 관심은 있지만 스스로 시장을 해석하기 어렵거나, 자신의 투자 패턴을 객관적으로 파악하기 힘든 초보 투자자를 위한 도구를 만드는 것을 목표로 한다.&lt;br /&gt;이를 위해 우리 팀은 크게 세 가지 기능을 설계했다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-end=&quot;1671&quot; data-start=&quot;1541&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1578&quot; data-start=&quot;1541&quot;&gt;정성적 분석&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 섹터별 뉴스 맥락 해석 및 톤 분류&lt;/li&gt;
&lt;li data-end=&quot;1619&quot; data-start=&quot;1579&quot;&gt;정량적 분석&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 가격 기반 기술 지표 계산 및 해석 제공&lt;/li&gt;
&lt;li data-end=&quot;1671&quot; data-start=&quot;1620&quot;&gt;투자 행동 학습 로그&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 사용자의 매매 기록 기반 행동 분석 및 자기 피드백&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;1843&quot; data-start=&quot;1673&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이번 글에서는 그중에서도 내가 맡은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;정량적 분석 기능의 기술 요소를 기말 기술 검증 시연에 대비하여 구현&amp;middot;점검한 과정&lt;/b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;을 중심으로 정리한다. &lt;/span&gt;FinanceDataReader로 주가 데이터를 수집하고, SMA&amp;middot;RSI&amp;middot;볼린저밴드 등 핵심 지표를 계산한 뒤, 이를 기반으로 GPT 모델이 자연어 해석을 생성하는 전체 흐름을 코드와 함께 기록하고자 한다.&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;1. 준비하기(환경 설정 및 개발 준비 과정)&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;1-1) 개발 환경&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-25 오전 2.18.32.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5JpjJ/dJMcagRtBzd/m5tlmLF3BV6k7o8x4WEwrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5JpjJ/dJMcagRtBzd/m5tlmLF3BV6k7o8x4WEwrK/img.png&quot; data-alt=&quot;파이썬 3.13.9 버전 설치&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5JpjJ/dJMcagRtBzd/m5tlmLF3BV6k7o8x4WEwrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5JpjJ%2FdJMcagRtBzd%2Fm5tlmLF3BV6k7o8x4WEwrK%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-11-25 오전 2.18.32.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;파이썬 3.13.9 버전 설치&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-파이썬 3.13.9 버전, 파이참 다운로드&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;1-2) 필요한 라이브러리 설치&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1764152223170&quot; class=&quot;cmake&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;pip install finance-datareader pandas numpy python-dotenv 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;터미널에 다음과 같은 명령어를 입력하여 필요한 라이브러리들을 설치하였다.&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 style=&quot;color: #000000;&quot; data-end=&quot;1830&quot; data-start=&quot;1768&quot;&gt;FinanceDataReader&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 국내/해외 주가 데이터 수집을 위한 라이브러리&lt;/li&gt;
&lt;li style=&quot;color: #000000;&quot; data-end=&quot;1884&quot; data-start=&quot;1831&quot;&gt;Pandas&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 이동평균&amp;middot;볼린저밴드&amp;middot;RSI 계산 등 시계열 기반 지표 계산&lt;/li&gt;
&lt;li style=&quot;color: #000000;&quot; data-end=&quot;1925&quot; data-start=&quot;1885&quot;&gt;NumPy&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 수치 연산 최적화 및 Pandas 연산 보조&lt;/li&gt;
&lt;li style=&quot;color: #000000;&quot; data-end=&quot;1987&quot; data-start=&quot;1926&quot;&gt;python-dotenv&lt;span&gt;&amp;nbsp;&lt;/span&gt;: OpenAI API 키를&lt;span&gt;&amp;nbsp;&lt;/span&gt;.env에서 불러오는 역할&lt;/li&gt;
&lt;li style=&quot;color: #000000;&quot; data-end=&quot;2038&quot; data-start=&quot;1988&quot;&gt;openai&lt;span&gt;&amp;nbsp;&lt;/span&gt;: GPT 모델 호출을 통해 지표 해석과 주요 신호 요약에 활용&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;h3 data-ke-size=&quot;size23&quot;&gt;1-3) GPT API 키 발급&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-26 오후 7.56.22.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XpP8s/dJMcaiaG6jM/Owoodfwl4E4PHEGGnQ5Ms1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XpP8s/dJMcaiaG6jM/Owoodfwl4E4PHEGGnQ5Ms1/img.png&quot; data-alt=&quot;OpenAI 플랫폼에서 API key 발급하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XpP8s/dJMcaiaG6jM/Owoodfwl4E4PHEGGnQ5Ms1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXpP8s%2FdJMcaiaG6jM%2FOwoodfwl4E4PHEGGnQ5Ms1%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-11-26 오후 7.56.22.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OpenAI 플랫폼에서 API key 발급하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;GPT 모델을 통해 지표 해석(요약문 생성, 주요 신호 정리)을 생성하기 위해 OpenAI 플랫폼에서 API Key를 발급하고,&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;.env&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일로 관리하도록 구성하였다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;2. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;기술 검증 코드 및 결과&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;실제 서비스에서는 사용자가 포트폴리오에 보유 종목을 등록할 때 종목명을 기준으로 검색&amp;middot;선택하고, 내부적으로는 종목코드(티커)와 매핑하여 지표 계산 및 분석 로직이 동작하도록 설계할 예정이다.&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;다만 이번 글에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;핵심 기술 요소인 &amp;ldquo;가격 데이터 수집 &amp;rarr; 지표 계산 &amp;rarr; GPT 기반 해석&amp;rdquo; 흐름&lt;/b&gt;을 검증하는 것&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;에 집중하기 위해, 예시 종목을 하나 선정하여 해당 종목을 기준으로 정량 분석 파이프라인을 구현하고 테스트하였다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;2-1) 주가 데이터 수집 - FinanceDataReader를 활용한 최근 1년 OHLCV 수집&lt;/span&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def load_price(symbol, period=365):

    #오늘 기준 최근 period일 주가 데이터 로드.
    #FinanceDataReader 사용.

    end = datetime.today().date()
    start = end - timedelta(days=period)

    df = fdr.DataReader(symbol, start=start, end=end)
    df = df.rename(
        columns={
            &quot;Open&quot;: &quot;open&quot;,
            &quot;High&quot;: &quot;high&quot;,
            &quot;Low&quot;: &quot;low&quot;,
            &quot;Close&quot;: &quot;close&quot;,
            &quot;Volume&quot;: &quot;volume&quot;,
        }
    )
    return df[[&quot;open&quot;, &quot;high&quot;, &quot;low&quot;, &quot;close&quot;, &quot;volume&quot;]]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- DataReader()&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;를 사용해 &lt;/span&gt;오늘 기준 최근 1년(365일) 데이터&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;를 조회&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;2-2) 기술적 지표 계산 - SMA / RSI / Bollinger Band&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def calc_rsi(close, period=14):

    #기본형 RSI 계산 (단순 이동평균 기반).

    delta = close.diff()

    gain = delta.clip(lower=0)
    loss = -delta.clip(upper=0)

    avg_gain = gain.rolling(period).mean()
    avg_loss = loss.rolling(period).mean()

    rs = avg_gain / avg_loss
    rsi = 100 - (100 / (1 + rs))

    return rsi


def calc_indicators(df):

    #SMA20/50/120, RSI14, Bollinger Bands, Bollinger Width 계산.

    out = df.copy()

    # 이동평균 (SMA)
    out[&quot;sma_20&quot;] = out[&quot;close&quot;].rolling(20).mean()
    out[&quot;sma_50&quot;] = out[&quot;close&quot;].rolling(50).mean()
    out[&quot;sma_120&quot;] = out[&quot;close&quot;].rolling(120).mean()

    # RSI(14)
    out[&quot;rsi_14&quot;] = calc_rsi(out[&quot;close&quot;], 14)

    # Bollinger Band (20일 기준, &amp;plusmn;2표준편차)
    mid = out[&quot;close&quot;].rolling(20).mean()
    std = out[&quot;close&quot;].rolling(20).std()
    out[&quot;bb_mid&quot;] = mid
    out[&quot;bb_upper&quot;] = mid + 2 * std
    out[&quot;bb_lower&quot;] = mid - 2 * std

    # Bollinger Width
    out[&quot;bollinger_width&quot;] = (out[&quot;bb_upper&quot;] - out[&quot;bb_lower&quot;]) / out[&quot;bb_mid&quot;]

    return out.dropna()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;- &lt;/span&gt;&lt;/span&gt;지표 계산을 LLM에 맡길 경우 비용 증가 + 결과 일관성 저하 위험이 있어, 모든 수식 기반 계산 로직은 백엔드에서 직접 수행함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 기말 기술 검증 단계에서는 시간 제약으로 인해&lt;span&gt;&amp;nbsp;&lt;/span&gt;SMA20/50/120, RSI14, Bollinger Band(20일 &amp;plusmn;2&amp;sigma;) 등&lt;span&gt;&amp;nbsp;&lt;/span&gt;고정된 파라미터만 우선 적용하여 지표를 계산함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 향후 실제 서비스 개발 단계에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;EMA, MACD 등 추가 지표를 포함하고, 각 지표의 기간&amp;middot;파라미터 역시&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용자가 직접 선택할 수 있는 옵션 형태로 확장할 예정임.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;2-3) 규칙 기반 정량 분석&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def categorize_volatility(width_value, low_th, high_th):
    &quot;&quot;&quot;
    볼린저 폭 분위수를 기준으로 변동성 구간 라벨링.
    - 상위 33% 이상: '높음'
    - 하위 33% 이하: '낮음'
    - 나머지: '보통'
    &quot;&quot;&quot;
    if width_value &amp;gt;= high_th:
        return &quot;높음&quot;
    elif width_value &amp;lt;= low_th:
        return &quot;낮음&quot;
    return &quot;보통&quot;


def calc_support_resistance(df_calc, window=60):
    &quot;&quot;&quot;
    최근 window일 종가 기준:
    - 최저가 = 지지선
    - 최고가 = 저항선
    &quot;&quot;&quot;
    recent = df_calc.tail(window)
    support = float(recent[&quot;close&quot;].min())
    resistance = float(recent[&quot;close&quot;].max())
    return support, resistance&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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: #000000; text-align: start;&quot;&gt;변동성, 지지선 저항선은 모두 명확한 규칙 기반으로 계산할 수 있는 지표이므로, &lt;/span&gt;백엔드에서 직접 산출하는 방식&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;으로 구현함.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;- &lt;/span&gt;&lt;/span&gt;변동성은 Bollinger Band 폭을 활용해,&lt;span&gt;&amp;nbsp;&lt;/span&gt;33%&amp;middot;66% 분위수 기준으로 &amp;lsquo;높음/보통/낮음&amp;rsquo; 세 단계로 라벨링함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 지지선과 저항선은&lt;span&gt;&amp;nbsp;&lt;/span&gt;최근 60일 종가의 최저값&amp;middot;최고값을 기준으로 단순 계산하여, 초보 사용자도 이해하기 쉬운 형태로 구성함.&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;span&gt;&amp;nbsp;&lt;/span&gt;EMA, MACD, OBV 등 추가 지표를 활용하여 변동성&amp;middot;지지선&amp;middot;저항선 계산을 더욱 정교화할 계획임.&lt;/p&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 style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;2-4) &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;GPT에 전달할 Snapshot 구성&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def make_snapshot(symbol, df_calc, display_name=None):
    &quot;&quot;&quot;
    GPT에 전달할 Snapshot 생성.
    - symbol / name
    - date / price
    - volatility (낮음/보통/높음)
    - support / resistance
    - indicators (SMA20/50/120, RSI14, Bollinger Width)
    &quot;&quot;&quot;
    last = df_calc.iloc[-1]

    # 변동성 분위수 계산
    width_series = df_calc[&quot;bollinger_width&quot;].dropna()
    low_th = float(width_series.quantile(0.33))
    high_th = float(width_series.quantile(0.66))

    volatility = categorize_volatility(float(last[&quot;bollinger_width&quot;]), low_th, high_th)
    support, resistance = calc_support_resistance(df_calc)

    name = display_name if display_name is not None else symbol

    snapshot = {
        &quot;symbol&quot;: symbol,
        &quot;name&quot;: name,
        &quot;date&quot;: last.name.strftime(&quot;%Y-%m-%d&quot;),
        &quot;price&quot;: float(last[&quot;close&quot;]),
        &quot;volatility&quot;: volatility,
        &quot;support&quot;: support,
        &quot;resistance&quot;: resistance,
        &quot;indicators&quot;: {
            &quot;sma_20&quot;: float(last[&quot;sma_20&quot;]),
            &quot;sma_50&quot;: float(last[&quot;sma_50&quot;]),
            &quot;sma_120&quot;: float(last[&quot;sma_120&quot;]),
            &quot;rsi_14&quot;: float(last[&quot;rsi_14&quot;]),
            &quot;bollinger_width&quot;: float(last[&quot;bollinger_width&quot;]),
        },
    }
    return snapshot&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;p data-ke-size=&quot;size16&quot;&gt;- LLM이 해석해야 할 최소 정보만 전달하기 위해, 백엔드에서 지표 계산 후&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나의 snapshot(dict)&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로 구조화함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- snapshot은 종목코드, 날짜, 현재가, 변동성 라벨, 지지선/저항선, 핵심 지표(SMA&amp;middot;RSI&amp;middot;Bollinger)를 포함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 모든 수치는 이미 백엔드에서 검증된 값이기 때문에, LLM이 임의로 숫자를 생성하지 않고&lt;span&gt;&amp;nbsp;&lt;/span&gt;설명에만 집중하도록 설계함.&lt;/p&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 style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;2-5) 프롬프트 설계&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;SYSTEM_PROMPT = &quot;&quot;&quot;
당신은 초보 개인 투자자를 위한 기술적 지표 해설 전문가입니다.

반드시 지켜야 할 것:
- 모든 결과는 한국어로 작성합니다.
- 매수/매도, 종목 추천 등 직접적인 투자 조언을 하지 않습니다.
- 미래 가격을 단정적으로 예측하지 않습니다.
- 주어진 스냅샷(JSON)에 포함된 정보만 근거로 사용합니다.
- 초보자도 이해할 수 있도록, 전문 용어는 필요 시 괄호로 간단히 풀어서 설명합니다.

톤:
- 중립적이고 차분한 톤
- &quot;확실하다&quot;, &quot;반드시 오른다/내린다&quot;와 같은 표현은 사용하지 않습니다.
&quot;&quot;&quot;.strip()

USER_PROMPT_TEMPLATE = &quot;&quot;&quot;
아래는 사용자가 선택한 종목에 대한 기술적 지표 스냅샷(JSON)입니다.

요구사항:
1) &quot;주요 신호&quot; 섹션을 작성하세요.
   - SMA20/50/120, RSI14, Bollinger 폭 등을 기반으로
     핵심 신호를 3~4개 불릿 포인트로 정리합니다.
   - 단순 숫자 나열이 아니라, '해당 지표 상태가 무엇을 의미하는지'를 중심으로 설명합니다.

   예시:
     - 20일선이 50일선 위에 있어 단기 상승 흐름을 유지하고 있습니다.
     - 현재가 120일선 위에 위치해 장기 우상향 기조를 나타냅니다.
     - RSI가 50대 중반으로, 과열되지 않은 범위 내에서 상승 모멘텀이 이어지고 있습니다.
     - 볼린저 밴드 폭이 최근 확대되어 단기 변동성이 증가한 상황입니다.

2) &quot;차트 요약&quot; 섹션을 작성하세요.
   - 변동성, 지지선, 저항선 값을 활용하여
     현재 차트 상황을 2~4문장으로 종합적으로 설명합니다.
   - 예측이 아니라, 현재 상태를 기반으로 작성하세요.
   - 예시:
       - 최근 변동성이 확대되며 가격 움직임이 커진 상황입니다.
       - 현재 가격은 주요 지지선 위에 있어 단기적으로 방어력이 있는 위치입니다.
       - 다만 저항선과의 가격 차이가 좁아져 주의 깊은 관찰이 필요합니다.

형식 예시는 다음과 같습니다:

주요 신호:
- ...

차트 요약:
- ...

스냅샷(JSON):
{snapshot_json}

위 두 개 섹션만 출력하세요.
JSON, 코드 블록, 추가 설명 문구는 포함하지 마세요.
&quot;&quot;&quot;.strip()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 출력 포맷을 엄격하게 지정하여 주요 신호 3~4개, 2~4문장 요약 형태로만 결과가 나오도록 제어함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 투자 조언, 미래 예측, 강한 확신 표현 등을 전부 금지하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;모델 안전성&amp;middot;일관성&amp;middot;정책 준수를 확보함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 초보 사용자 대상 서비스 특성에 맞게 문장은 짧고, 필요 시 괄호를 통한 단어 풀이 등&lt;span&gt;&amp;nbsp;&lt;/span&gt;친절한 톤을 유지하도록 프롬프트 스타일을 제한함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- snapshot의 값이 프롬프트 내부에 그대로 주입되므로, LLM은 해석만 수행하고 계산은 하지 않도록 책임 분리가 이루어짐.&lt;/p&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 style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;2-6) GPT 호출 - &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;gpt-4o-mini 기반 해석 생성&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1764163019007&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def build_user_prompt(snapshot):

    #Snapshot(dict)를 JSON 문자열로 직렬화해서 USER_PROMPT_TEMPLATE에 삽입.

    snapshot_json = json.dumps(snapshot, ensure_ascii=False, indent=2)
    prompt = USER_PROMPT_TEMPLATE.format(snapshot_json=snapshot_json)
    return prompt


def explain_with_gpt(user_prompt):

    #OpenAI GPT API 호출하여 SYSTEM_PROMPT + USER_PROMPT로 차트 해석 텍스트 생성.

    response = client.chat.completions.create(
        model=&quot;gpt-4o-mini&quot;,
        messages=[
            {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: SYSTEM_PROMPT},
            {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_prompt},
        ],
        max_tokens=600,
    )
    return response.choices[0].message.content&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;- max_tokens를 600으로 지정하여 주요 신호 + 요약 문단이 안정적으로 생성될 수 있는 범위를 확보함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 응답은 문자열로 반환되며, 실제 서비스에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;JSON 응답 또는 구조화된 객체로 변경해 API와 연동할 수 있음.&lt;/p&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 style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;2-7) 전체 실행 흐름 및 실행 결과&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def demo(symbol=&quot;005930&quot;, name=&quot;삼성전자&quot;):

    df = load_price(symbol)
    df_calc = calc_indicators(df)
    snapshot = make_snapshot(symbol, df_calc, display_name=name)
    user_prompt = build_user_prompt(snapshot)
    explanation = explain_with_gpt(user_prompt)

    print(&quot;=== Snapshot (입력) ===&quot;)
    print(json.dumps(snapshot, ensure_ascii=False, indent=2))
    print(&quot;\n=== GPT 해석 (출력) ===\n&quot;)
    print(explanation)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- demo() 함수는 이번 기술 검증을 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;단일 종목(예: 삼성전자)&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 기준으로 전체 흐름을 단일 실행 스크립트에서 검증하기 위해 작성함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 실행 흐름은 다음과 같은 단계로 구성됨:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1805&quot; data-start=&quot;1544&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1584&quot; data-start=&quot;1544&quot;&gt;load_price()&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 최근 1년간 주가 데이터 불러오기&lt;/li&gt;
&lt;li data-end=&quot;1641&quot; data-start=&quot;1587&quot;&gt;calc_indicators()&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; SMA, RSI, Bollinger Band 계산&lt;/li&gt;
&lt;li data-end=&quot;1688&quot; data-start=&quot;1644&quot;&gt;make_snapshot()&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; LLM 전달용 snapshot 구성&lt;/li&gt;
&lt;li data-end=&quot;1726&quot; data-start=&quot;1691&quot;&gt;build_prompt()&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; 해석용 프롬프트 생성&lt;/li&gt;
&lt;li data-end=&quot;1767&quot; data-start=&quot;1729&quot;&gt;explain_with_gpt()&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ndash; GPT API 호출&lt;/li&gt;
&lt;li data-end=&quot;1805&quot; data-start=&quot;1770&quot;&gt;snapshot(원본 수치) + LLM 해석 결과 출력&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 검증 목적상 콘솔 출력 형태로 테스트했으나, 실제 서비스에서는 snapshot과 해석 결과를 모두&amp;nbsp;프론트엔드에 JSON 형태로 전달해 렌더링하는 방식으로 확장할 예정임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-01 오후 2.41.14.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPNYve/dJMcabimQOg/CHpDLX8BVk2BDvsJWVPkIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPNYve/dJMcabimQOg/CHpDLX8BVk2BDvsJWVPkIK/img.png&quot; data-alt=&quot;실행 결과. 주가 수집 &amp;amp;rarr; 지표 계산 및 차트 분석 요소 산출 &amp;amp;rarr; Snapshot 생성&amp;amp;rarr; GPT 해설 생성까지 전체 흐름이 기대한 대로 정상 작동함.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPNYve/dJMcabimQOg/CHpDLX8BVk2BDvsJWVPkIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPNYve%2FdJMcabimQOg%2FCHpDLX8BVk2BDvsJWVPkIK%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-12-01 오후 2.41.14.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실행 결과. 주가 수집 &amp;rarr; 지표 계산 및 차트 분석 요소 산출 &amp;rarr; Snapshot 생성&amp;rarr; GPT 해설 생성까지 전체 흐름이 기대한 대로 정상 작동함.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>졸업 프로젝트</category>
      <author>jiheechoi</author>
      <guid isPermaLink="true">https://skoitart2e.tistory.com/16</guid>
      <comments>https://skoitart2e.tistory.com/16#entry16comment</comments>
      <pubDate>Wed, 26 Nov 2025 23:13:14 +0900</pubDate>
    </item>
    <item>
      <title>[스프링 부트] 9장~11장</title>
      <link>https://skoitart2e.tistory.com/15</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;9. CURD와 SQL 쿼리 종합&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. &amp;nbsp;JPA 로깅 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-서버에서 데이터 생성, 조회, 수정, 삭제 등을 요청하면 JPA의 리파지터리가 DB에 해당 요청을 전달함. &amp;rarr;&amp;nbsp;요청을 받은 DB는 SQL로 쿼리를 작성해 테이블 속 데이터를 관리.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-쿼리란, DB에 정보를 요청하는 구문으로 INSERT문, SELECT문, UPDATE문, DELETE문이 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*JPA 로깅 레벨 설정하기(application.properties 수정)&lt;/p&gt;
&lt;pre id=&quot;code_1751040953730&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#JPA 로깅 설정
#디버그 레벨로 쿼리 출력
logging.level.org.hibernate.SQL=DEBUG&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;rarr; 로깅 레벨에는 7단계가 있고, 레벨을 설정하면 해당 레벨 이상의 로그가 출력됨. DEBUG로 설정하면 JPA가 동작할 때 수행되는 SQL 쿼리를 볼 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*그외 수정사항(application.properties에 추가)&lt;/p&gt;
&lt;pre id=&quot;code_1751041487022&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#JPA 로깅 설정
#디버그 레벨로 쿼리 출력
logging.level.org.hibernate.SQL=DEBUG
#쿼리 줄바꿈하기
spring.jpa.properties.hibernate.format_sql=true
#매개변수 값 보여주기
logging.level.org.hibernatetype.descriptor.sql.BasicBinder=TRACE

#DB URL 설정
#유니크 URL 생성하지 않기
spring.datasource.generate-unique-name=false
#고정 URL 설정하기
spring.datasource.url=jdbc:h2:mem:testdb&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. &amp;nbsp;SQL 쿼리 로그 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 데이터 생성시:INSERT문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-데이터 생성하기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.29.26.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VKss0/btsOVX1d28g/fMW5WK9sggVwv2ymMbl590/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VKss0/btsOVX1d28g/fMW5WK9sggVwv2ymMbl590/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VKss0/btsOVX1d28g/fMW5WK9sggVwv2ymMbl590/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVKss0%2FbtsOVX1d28g%2FfMW5WK9sggVwv2ymMbl590%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.29.26.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&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;-데이터 생성시 INSERT문 동작&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.31.13.png&quot; data-origin-width=&quot;2878&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KtesG/btsOVdiUKMp/tkjrH10rc3zXvL4DqXJvD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KtesG/btsOVdiUKMp/tkjrH10rc3zXvL4DqXJvD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KtesG/btsOVdiUKMp/tkjrH10rc3zXvL4DqXJvD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKtesG%2FbtsOVdiUKMp%2FtkjrH10rc3zXvL4DqXJvD0%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;2878&quot; height=&quot;622&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.31.13.png&quot; data-origin-width=&quot;2878&quot; data-origin-height=&quot;622&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;2) 데이터 조회 시: SELECT문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-localhost:8080/articles 페이지 접속해서 모든 데이터 조회하기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.34.06.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pancX/btsOVrPkwOD/ye5d7xDgdoE5C1BSJCulx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pancX/btsOVrPkwOD/ye5d7xDgdoE5C1BSJCulx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pancX/btsOVrPkwOD/ye5d7xDgdoE5C1BSJCulx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpancX%2FbtsOVrPkwOD%2Fye5d7xDgdoE5C1BSJCulx1%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.34.06.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-데이터 조회시 SELECT문 동작&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.35.00.png&quot; data-origin-width=&quot;2570&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/briGeR/btsOXm6JKHj/u925Yaz0oS5N7dtrkU2Ki0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/briGeR/btsOXm6JKHj/u925Yaz0oS5N7dtrkU2Ki0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/briGeR/btsOXm6JKHj/u925Yaz0oS5N7dtrkU2Ki0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbriGeR%2FbtsOXm6JKHj%2Fu925Yaz0oS5N7dtrkU2Ki0%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;2570&quot; height=&quot;336&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.35.00.png&quot; data-origin-width=&quot;2570&quot; data-origin-height=&quot;336&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;3) 데이터 수정시: UPDATE문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-id=4인 article을 수정&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.37.07.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d46fv1/btsOVikP4dz/CdSPHk7JqLovisaaKKNxO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d46fv1/btsOVikP4dz/CdSPHk7JqLovisaaKKNxO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d46fv1/btsOVikP4dz/CdSPHk7JqLovisaaKKNxO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd46fv1%2FbtsOVikP4dz%2FCdSPHk7JqLovisaaKKNxO0%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.37.07.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-데이터 수정시 UPDATE문 동작&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.37.43.png&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EX6LK/btsOVaAzewJ/Te1ZElwnq64KWoZOm2Pvvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EX6LK/btsOVaAzewJ/Te1ZElwnq64KWoZOm2Pvvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EX6LK/btsOVaAzewJ/Te1ZElwnq64KWoZOm2Pvvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEX6LK%2FbtsOVaAzewJ%2FTe1ZElwnq64KWoZOm2Pvvk%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;2548&quot; height=&quot;364&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.37.43.png&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;364&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;4) 데이터 삭제 시: DELETE문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-id=4인 article 삭제하기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.43.42.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k0QD2/btsOXiJ0qOW/mXfk7Kb5RAmf3L5fBUQRr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k0QD2/btsOXiJ0qOW/mXfk7Kb5RAmf3L5fBUQRr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k0QD2/btsOXiJ0qOW/mXfk7Kb5RAmf3L5fBUQRr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk0QD2%2FbtsOXiJ0qOW%2FmXfk7Kb5RAmf3L5fBUQRr1%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.43.42.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-데이터 삭제시 DELETE문 동작&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.44.09.png&quot; data-origin-width=&quot;2556&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Aseny/btsOVkbPi7u/4wMGYtASmbBwEIPmuazKFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Aseny/btsOVkbPi7u/4wMGYtASmbBwEIPmuazKFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Aseny/btsOVkbPi7u/4wMGYtASmbBwEIPmuazKFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAseny%2FbtsOVkbPi7u%2F4wMGYtASmbBwEIPmuazKFk%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;2556&quot; height=&quot;304&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.44.09.png&quot; data-origin-width=&quot;2556&quot; data-origin-height=&quot;304&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;h3 data-ke-size=&quot;size23&quot;&gt;3. 기본 SQL 쿼리 작성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) coffee 테이블 만들기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.48.38.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blHaK1/btsOWC3h7pl/5CKYsb4mgjVyNkF97OBuKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blHaK1/btsOWC3h7pl/5CKYsb4mgjVyNkF97OBuKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blHaK1/btsOWC3h7pl/5CKYsb4mgjVyNkF97OBuKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblHaK1%2FbtsOWC3h7pl%2F5CKYsb4mgjVyNkF97OBuKk%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.48.38.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&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;2) coffee 데이터 생성하기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.52.07.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buE7SX/btsOVdDej3N/dYdE4l0zpdeNak0z1dywS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buE7SX/btsOVdDej3N/dYdE4l0zpdeNak0z1dywS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buE7SX/btsOVdDej3N/dYdE4l0zpdeNak0z1dywS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuE7SX%2FbtsOVdDej3N%2FdYdE4l0zpdeNak0z1dywS0%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.52.07.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.52.25.png&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXqkkj/btsOWoKJP5k/0kKo62yH91NajMMem2loyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXqkkj/btsOWoKJP5k/0kKo62yH91NajMMem2loyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXqkkj/btsOWoKJP5k/0kKo62yH91NajMMem2loyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXqkkj%2FbtsOWoKJP5k%2F0kKo62yH91NajMMem2loyk%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;916&quot; height=&quot;648&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.52.25.png&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.55.05.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkR4cS/btsOWqPiXMO/J1knfcfYifbkeNq2GEpolk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkR4cS/btsOWqPiXMO/J1knfcfYifbkeNq2GEpolk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkR4cS/btsOWqPiXMO/J1knfcfYifbkeNq2GEpolk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkR4cS%2FbtsOWqPiXMO%2FJ1knfcfYifbkeNq2GEpolk%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.55.05.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&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;3) coffee 데이터 조회하기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.55.34.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qkWrM/btsOWpbN5Si/O11Un4hn9DqVbVqd6OgSG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qkWrM/btsOWpbN5Si/O11Un4hn9DqVbVqd6OgSG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qkWrM/btsOWpbN5Si/O11Un4hn9DqVbVqd6OgSG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqkWrM%2FbtsOWpbN5Si%2FO11Un4hn9DqVbVqd6OgSG0%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.55.34.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&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;4) coffee 데이터 수정하기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.59.37.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGaqrk/btsOVea3nYn/vKa4rU50zW2D5dtD7AuXy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGaqrk/btsOVea3nYn/vKa4rU50zW2D5dtD7AuXy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGaqrk/btsOVea3nYn/vKa4rU50zW2D5dtD7AuXy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGaqrk%2FbtsOVea3nYn%2FvKa4rU50zW2D5dtD7AuXy1%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.59.37.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.59.52.png&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QjstG/btsOXmZ1wqM/xq7VE4CCnRnE0pPiT5NNHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QjstG/btsOXmZ1wqM/xq7VE4CCnRnE0pPiT5NNHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QjstG/btsOXmZ1wqM/xq7VE4CCnRnE0pPiT5NNHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQjstG%2FbtsOXmZ1wqM%2Fxq7VE4CCnRnE0pPiT5NNHk%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;448&quot; height=&quot;386&quot; data-filename=&quot;스크린샷 2025-06-28 오전 1.59.52.png&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;386&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;5) coffee 데이터 삭제하기&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 2.01.01.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sciAz/btsOUXm4kor/OkjDNab105CYSdY2E3mBxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sciAz/btsOUXm4kor/OkjDNab105CYSdY2E3mBxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sciAz/btsOUXm4kor/OkjDNab105CYSdY2E3mBxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsciAz%2FbtsOUXm4kor%2FOkjDNab105CYSdY2E3mBxk%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 2.01.01.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 2.01.14.png&quot; data-origin-width=&quot;466&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W0P9u/btsOUd41BD8/96d6kpS0i65oBsmkjcpJBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W0P9u/btsOUd41BD8/96d6kpS0i65oBsmkjcpJBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W0P9u/btsOUd41BD8/96d6kpS0i65oBsmkjcpJBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW0P9u%2FbtsOUd41BD8%2F96d6kpS0i65oBsmkjcpJBK%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;466&quot; height=&quot;380&quot; data-filename=&quot;스크린샷 2025-06-28 오전 2.01.14.png&quot; data-origin-width=&quot;466&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;10. REST API와 JSON&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. REST API와 JSON의 등장배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-REST API는 서버의 자원을 클라이언트에 구애받지 않고 사용할 수 있게 하는 설계 방식임.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-REST API 방식에서는 HTTP 요청에 대한 응답으로 서버의 자원을 반환함. &amp;rarr; 서버에서 보내는 응답이 특정 기기에 종속되지 않도록 모든기기에서 통용될 수 있는 응답 데이터를 반환(JSON)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-JSON 데이터: key와 value로 구성된 정렬되지 않은 속성의 집합. 키는 문자열이므로 항상 큰따옴표로 감싸고, 값은 문자열인 경우에만 큰따옴표로 감쌈.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. REST API 동작 살펴보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) GET 요청하고 응답받기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-JSON placeholder 사이트에서 확인한 게시글, 댓글 등의 자원을 Talend API Tester 프로그램을 이용해 JSON 형식으로 받아오는 실습&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 5.08.12.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PdaoY/btsOWENEccj/07yHIgERDMUPsUgRJKHWKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PdaoY/btsOWENEccj/07yHIgERDMUPsUgRJKHWKk/img.png&quot; data-alt=&quot;모든 게시글 조회하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PdaoY/btsOWENEccj/07yHIgERDMUPsUgRJKHWKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPdaoY%2FbtsOWENEccj%2F07yHIgERDMUPsUgRJKHWKk%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 5.08.12.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모든 게시글 조회하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 5.10.33.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oZTY7/btsOV0cA9Rn/RGzm0jW0UG4PT6OSOFUFTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oZTY7/btsOV0cA9Rn/RGzm0jW0UG4PT6OSOFUFTK/img.png&quot; data-alt=&quot;1번 게시글 조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oZTY7/btsOV0cA9Rn/RGzm0jW0UG4PT6OSOFUFTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoZTY7%2FbtsOV0cA9Rn%2FRGzm0jW0UG4PT6OSOFUFTK%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 5.10.33.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;1번 게시글 조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ HTTP 상태 코드: 클라이언트가 보낸 요청이 성공했는지, 실패했는지 알려 주는 코드&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1XX(정보): 요청이 수신돼 처리 중임.&lt;/li&gt;
&lt;li&gt;2XX(성공): 요청이 정상적으로 처리됨.&lt;/li&gt;
&lt;li&gt;3XX(리다이렉션 메시지): 요청을 완료하려면 추가 행동이 필요함.&lt;/li&gt;
&lt;li&gt;4XX(클라이언트 요청 오류): 클라이언트의 요청이 잘못돼 서버가 요청을 수행할 수 없음.&lt;/li&gt;
&lt;li&gt;5XX(서버 응답 오류): 서버 내부에 에러가 발생해 클라이언트 요청에 대해 적절히 수행하지 못함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ HTTP 요청 메시지와 응답 메시지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-HTTP 메서드를 통한 데이터 조회 요청과 그에 대한 응답은 HTTP 메시지에 실려 전송됨. 요청할 때는 HTTP 요청 메시지에, 응답할 때는 응답 메시지에 내용이 실림.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-HTTP 메시지의 구성&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시작 라인: HTTP 요청 또는 응답 내용이 있음. 시작라인은 항상 한줄로 끝남.&lt;/li&gt;
&lt;li&gt;헤더: HTTP 전송에 필요한 부가 정보가 있음.&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;2) POST 요청하고 응답받기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 5.20.13.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdZnjt/btsOV64G2J2/LGC3PUJd3iGb0gtnsf3rP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdZnjt/btsOV64G2J2/LGC3PUJd3iGb0gtnsf3rP0/img.png&quot; data-alt=&quot;데이터 생성 요청&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdZnjt/btsOV64G2J2/LGC3PUJd3iGb0gtnsf3rP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdZnjt%2FbtsOV64G2J2%2FLGC3PUJd3iGb0gtnsf3rP0%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 5.20.13.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데이터 생성 요청&lt;/figcaption&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;3) PATCH 요청하고 응답받기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 5.22.07.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JEA88/btsOWIoD3i2/ywgmDYQhwJI05hkcRKN4eK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JEA88/btsOWIoD3i2/ywgmDYQhwJI05hkcRKN4eK/img.png&quot; data-alt=&quot;1번 게시글 수정 요청하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JEA88/btsOWIoD3i2/ywgmDYQhwJI05hkcRKN4eK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJEA88%2FbtsOWIoD3i2%2FywgmDYQhwJI05hkcRKN4eK%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 5.22.07.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;1번 게시글 수정 요청하기&lt;/figcaption&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;4) DELETE 요청하고 응답받기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-28 오전 5.23.38.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVJl2Q/btsOWhERolE/KyapD0fUCNjU22wUDwkKp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVJl2Q/btsOWhERolE/KyapD0fUCNjU22wUDwkKp0/img.png&quot; data-alt=&quot;10번 게시글 삭제 요청하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVJl2Q/btsOWhERolE/KyapD0fUCNjU22wUDwkKp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVJl2Q%2FbtsOWhERolE%2FKyapD0fUCNjU22wUDwkKp0%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-28 오전 5.23.38.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;10번 게시글 삭제 요청하기&lt;/figcaption&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;&lt;b&gt;11. HTTP와 REST 컨트롤러&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. REST API의 동작 이해하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;REST API 방식에서는 HTTP 요청에 대한 응답으로 서버의 자원을 반환함.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&amp;rarr; 서버에서 보내는 응답이 특정 기기에 종속되지 않도록 모든기기에서 통용될 수 있는 응답 데이터를 반환.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-REST API의 응답 표준으로 사용하는 것이 JSON.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-REST API를 잘 구현하면 클라이언트가 기기에 구애받지 않고 서버의 자원을 이용할 수 있으며, 서버가 클라이언트의 요청에 체계적으로 대응 가능함.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. REST API의 구현 과정&lt;/h3&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;조회 요청: /api/articles or /api/articles/{id}&amp;nbsp;&amp;rarr; GET 메서드로 Article 목록 전체 또는 단일 Article 조회&lt;/li&gt;
&lt;li&gt;생성 요청: /api/articles &amp;rarr; POST 메서드로 새로운 Article을 생성해 목록에 저장&lt;/li&gt;
&lt;li&gt;수정 요청: /api/articles/{id} &amp;rarr; PATCH 메서드로 특정 Article의 내용을 수정&lt;/li&gt;
&lt;li&gt;삭제 요청: /api/articles/{id} &amp;rarr; DELETE 메서드로 특정 Article 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- URL 요청을 받아 그 결과를 JSON으로 반환해 줄 컨트롤러 생성(REST 컨트롤러)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 응답할 때 적절한 상태의 코드를 반환하기 위해 ResponseEntity 클래스 활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. REST API 구현하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) REST 컨트롤러 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-FirstApiController.java 생성&lt;/p&gt;
&lt;pre id=&quot;code_1751056941290&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.firstproject.api;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FirstApiController {
    @GetMapping(&quot;/api/hello&quot;)
    public String hello() {
        return &quot;Hello World&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;-REST 컨트롤러와 일반 컨트롤러의 차이: REST 컨트롤러는 JSON이나 텍스트 같은 데이터를 반환하는 반면, 일반 컨트롤러는 뷰 페이지를 반환함.&amp;nbsp;&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;2) REST API: GET/POST/PATCH/DELETE 구현하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&lt;span style=&quot;text-align: center;&quot;&gt;FirstApiController.java&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package com.example.firstproject.api;

import com.example.firstproject.dto.ArticleForm;
import com.example.firstproject.entity.Article;
import com.example.firstproject.repository.ArticleRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Slf4j
@RestController
public class ArticleApiController { 
    @Autowired
    private ArticleRepository articleRepository;
    
    //GET
    @GetMapping(&quot;/api/articles&quot;)
    public List&amp;lt;Article&amp;gt; index() {
        return articleRepository.findAll();
    }

    @GetMapping(&quot;/api/articles/{id}&quot;)
    public Article show(@PathVariable Long id) {
        return articleRepository.findById(id).orElse(null);
    }
    
    //POST
    @PostMapping(&quot;/api/articles&quot;)
    public Article create(@RequestBody ArticleForm dto) {
        Article article = dto.toEntity();
        return articleRepository.save(article);
    }
    
    //PATCH
    @PatchMapping(&quot;/api/articlces/{id}&quot;)
    public ResponseEntity&amp;lt;Article&amp;gt; update(@PathVariable Long id, @RequestBody ArticleForm dto) {
        Article article = dto.toEntity();
        log.info(&quot;id: {}, article: {}&quot;, id, article.toString());
        Article target = articleRepository.findById(id).orElse(null);
        if (target == null || id != article.getId()) {
            log.info(&quot;잘못된 요청! id: {}, article: {}&quot;, id, article.toString());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
        }
        target.patch(article);
        Article updated = articleRepository.save(article);
        return ResponseEntity.status(HttpStatus.OK).body(updated);
    }
    
    //DELETE
    @DeleteMapping(&quot;/api/articles/{id}&quot;)
    public ResponseEntity&amp;lt;Article&amp;gt; delete(@PathVariable Long id) {
        Article target = articleRepository.findById(id).orElse(null);
        if (target == null) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
        }
        articleRepository.delete(target);
        return ResponseEntity.status(HttpStatus.OK).build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&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: #000000; text-align: center;&quot;&gt;Article.java 수정&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.example.firstproject.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@AllArgsConstructor
@NoArgsConstructor //기본 생성자 추가 어노테이션
@ToString
@Entity
@Getter
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //DB가 id 자동 생성
    private Long id;
    @Column
    private String title;
    @Column
    private String content;

    public void patch(Article article) {
        if (article.title != null) {
            this.title = article.title;
        }
        if (article.content != null) {
            this.content = article.content;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&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;</description>
      <category>ECC/스프링 부트</category>
      <author>jiheechoi</author>
      <guid isPermaLink="true">https://skoitart2e.tistory.com/15</guid>
      <comments>https://skoitart2e.tistory.com/15#entry15comment</comments>
      <pubDate>Sat, 28 Jun 2025 06:05:49 +0900</pubDate>
    </item>
    <item>
      <title>[스프링 입문] 6. 회원 관리 예제 ~ 8. AOP</title>
      <link>https://skoitart2e.tistory.com/14</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 회원 관리 예제 - 웹 MVC 개발&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 회원 웹 기능 - 홈 화면 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 홈 컨트롤러 추가(HomeCotroller 클래스)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 7.33.13.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fgrnk/btsOLdJ643y/PT6IAdw85UfG7ITOREFWp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fgrnk/btsOLdJ643y/PT6IAdw85UfG7ITOREFWp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fgrnk/btsOLdJ643y/PT6IAdw85UfG7ITOREFWp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFgrnk%2FbtsOLdJ643y%2FPT6IAdw85UfG7ITOREFWp1%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-20 오후 7.33.13.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&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;2) 회원 관리용 홈(home.html)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 7.33.28.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T9oB9/btsOLvp8r6v/xCVDC2tj7zkKrLlefFDeEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T9oB9/btsOLvp8r6v/xCVDC2tj7zkKrLlefFDeEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T9oB9/btsOLvp8r6v/xCVDC2tj7zkKrLlefFDeEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT9oB9%2FbtsOLvp8r6v%2FxCVDC2tj7zkKrLlefFDeEk%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-20 오후 7.33.28.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 회원 웹 기능 - 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 회원 등록 폼 컨트롤러(MemberController 클래스 수정)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 9.36.19.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ABEpA/btsOLBDXwRA/8VK05wACGIIqo5KRuzy041/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ABEpA/btsOLBDXwRA/8VK05wACGIIqo5KRuzy041/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ABEpA/btsOLBDXwRA/8VK05wACGIIqo5KRuzy041/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FABEpA%2FbtsOLBDXwRA%2F8VK05wACGIIqo5KRuzy041%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-20 오후 9.36.19.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&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;2) 회원 등록 폼 html(createMemberForm.html)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 9.36.31.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbGtzH/btsOMQ089RH/lFwyeGPXNnhAh1Kwk8eNMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbGtzH/btsOMQ089RH/lFwyeGPXNnhAh1Kwk8eNMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbGtzH/btsOMQ089RH/lFwyeGPXNnhAh1Kwk8eNMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbGtzH%2FbtsOMQ089RH%2FlFwyeGPXNnhAh1Kwk8eNMk%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-20 오후 9.36.31.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&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;3) 웹&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;등록&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;화면에서&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;데이터를&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;전달&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;받을&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;폼&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;객체(MemberForm 클래스)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 9.36.39.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cia8lU/btsOLy1v29Y/2hRcAv3et61Vc6SZ2VXvT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cia8lU/btsOLy1v29Y/2hRcAv3et61Vc6SZ2VXvT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cia8lU/btsOLy1v29Y/2hRcAv3et61Vc6SZ2VXvT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcia8lU%2FbtsOLy1v29Y%2F2hRcAv3et61Vc6SZ2VXvT0%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-20 오후 9.36.39.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&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;4) 회원&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;컨트롤러에서&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;회원을&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;실제&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;등록하는&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;기능(MemberController 클래스 수정)&lt;/p&gt;
&lt;pre id=&quot;code_1750423098718&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping
    public String create(MemberForm form) {
        Member member = new Member();
        member.setName(form.getName());

        memberService.join(member);

        return &quot;redirect:/&quot;;
    }&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;3. 회원 웹 기능 - 조회&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 회원&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;컨트롤러에서&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;조회&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;기능(MemberController 클래스 수정)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 9.55.59.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxcdJ4/btsOKF71ao8/n8oiporQTnZGkbuMTwAczK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxcdJ4/btsOKF71ao8/n8oiporQTnZGkbuMTwAczK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxcdJ4/btsOKF71ao8/n8oiporQTnZGkbuMTwAczK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxcdJ4%2FbtsOKF71ao8%2Fn8oiporQTnZGkbuMTwAczK%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-20 오후 9.55.59.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&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;2) 회원 리스트 html(memberList.html)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 9.56.08.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YLJXC/btsOLu5UIG9/wmgkcoRhMJKjGCZZvNWFBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YLJXC/btsOLu5UIG9/wmgkcoRhMJKjGCZZvNWFBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YLJXC/btsOLu5UIG9/wmgkcoRhMJKjGCZZvNWFBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYLJXC%2FbtsOLu5UIG9%2FwmgkcoRhMJKjGCZZvNWFBk%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-20 오후 9.56.08.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&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;&lt;b&gt;7. 스프링 DB 접근 기술&lt;/b&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. H2 데이터베이스 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 다운로드 후 터미널에서 권한 부여('chmod 755 h2.sh') 후 &lt;span&gt;실행 ('&lt;/span&gt;&lt;span&gt;./h2.sh')&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 10.05.24.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhJh7C/btsOLEtV1rq/ki4alcSlpcAZrZUwjTwoA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhJh7C/btsOLEtV1rq/ki4alcSlpcAZrZUwjTwoA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhJh7C/btsOLEtV1rq/ki4alcSlpcAZrZUwjTwoA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhJh7C%2FbtsOLEtV1rq%2Fki4alcSlpcAZrZUwjTwoA1%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-20 오후 10.05.24.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;2) member 테이블 생성 후 insert문 실행해보기&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 11.07.31.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqu6bO/btsOMVnRgO2/z1Yazv8MpaZGl9GKuCuGK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqu6bO/btsOMVnRgO2/z1Yazv8MpaZGl9GKuCuGK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqu6bO/btsOMVnRgO2/z1Yazv8MpaZGl9GKuCuGK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbqu6bO%2FbtsOMVnRgO2%2Fz1Yazv8MpaZGl9GKuCuGK1%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-20 오후 11.07.31.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-20 오후 11.09.24.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSHjtf/btsOLygbbv0/4HKFNolHG9fWhg11DYrZ40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSHjtf/btsOLygbbv0/4HKFNolHG9fWhg11DYrZ40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSHjtf/btsOLygbbv0/4HKFNolHG9fWhg11DYrZ40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSHjtf%2FbtsOLygbbv0%2F4HKFNolHG9fWhg11DYrZ40%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-20 오후 11.09.24.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 순수 JDBC&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 환경설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- jdbc 회원 리포지토리&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class JdbcMemberRepository implements MemberRepository {
    private final DataSource dataSource;
    public JdbcMemberRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    @Override
    public Member save(Member member) {
        String sql = &quot;insert into member(name) values(?)&quot;;
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql,
                    Statement.RETURN_GENERATED_KEYS);
            pstmt.setString(1, member.getName());
            pstmt.executeUpdate();
            rs = pstmt.getGeneratedKeys();
            if (rs.next()) {
                member.setId(rs.getLong(1));
            } else {
                throw new SQLException(&quot;id 조회 실패&quot;);
            }
            return member;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    @Override
    public Optional&amp;lt;Member&amp;gt; findById(Long id) {
        String sql = &quot;select * from member where id = ?&quot;;
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setLong(1, id);
            rs = pstmt.executeQuery();
            if(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong(&quot;id&quot;));
                member.setName(rs.getString(&quot;name&quot;));
                return Optional.of(member);
            } else {
                return Optional.empty();
            }
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    @Override
    public List&amp;lt;Member&amp;gt; findAll() {
        String sql = &quot;select * from member&quot;;
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            rs = pstmt.executeQuery();
            List&amp;lt;Member&amp;gt; members = new ArrayList&amp;lt;&amp;gt;();
            while(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong(&quot;id&quot;));
                member.setName(rs.getString(&quot;name&quot;));
                members.add(member);
            }
            return members;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    @Override
    public Optional&amp;lt;Member&amp;gt; findByName(String name) {
        String sql = &quot;select * from member where name = ?&quot;;
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, name);
            rs = pstmt.executeQuery();
            if(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong(&quot;id&quot;));
                member.setName(rs.getString(&quot;name&quot;));
                return Optional.of(member);
            }
            {
            }
            return Optional.empty();
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    private Connection getConnection() {
        return DataSourceUtils.getConnection(dataSource);
    }
    private void close(Connection conn, PreparedStatement pstmt, ResultSet rs)
    {
        try {
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
        e.printStackTrace();
        }
        try {
            if (pstmt != null) {
                pstmt.close();
            }
        } catch (SQLException e) {
        e.printStackTrace();
        }
        try {
            if (conn != null) {
                close(conn);
            }
        } catch (SQLException e) {
        e.printStackTrace();
        }
}

private void close(Connection conn) throws SQLException {
    DataSourceUtils.releaseConnection(conn, dataSource);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 스프링 설정 변경(SpringConfig.java 수정)&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package hello.hello_spring.service;

import hello.hello_spring.repository.JdbcMemberRepository;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.repository.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class SpringConfig {
    private final DataSource dataSource;
    public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 스프링 통합 테스트(MemberServiceIntegrationTest 클래스)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;color: #000000;&quot; data-end=&quot;106&quot; data-start=&quot;35&quot;&gt;@SpringBootTest&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 스프링 컨테이너를 띄워서 실제 애플리케이션 환경과 동일하게 테스트할 수 있게 해준다.&lt;/li&gt;
&lt;li style=&quot;color: #000000;&quot; data-is-last-node=&quot;&quot; data-end=&quot;202&quot; data-start=&quot;107&quot;&gt;@Transactional&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 테스트 실행 시 트랜잭션을 시작하고 완료 후 자동으로 롤백하여 DB에 데이터가 남지 않게 해 다음 테스트에 영향을 주지 않게 해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName(&quot;hello&quot;);

        //When
        Long saveId = memberService.join(member);

        //Then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }

    @Test
    public void 중복_회원_예외() throws Exception {
        //Given
        Member member1 = new Member();
        member1.setName(&quot;spring&quot;);
        Member member2 = new Member();
        member2.setName(&quot;spring&quot;);

        //When
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -&amp;gt; memberService.join(member2));//예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo(&quot;이미 존재하는 회원입니다.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;h3 data-ke-size=&quot;size23&quot;&gt;4. 스프링 JdbcTemplate&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 스프링 JdbcTemplate 회원 리포지토리&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;

import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class JdbcTemplateMemberRepository implements MemberRepository {

    private JdbcTemplate jdbcTemplate;

    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName(&quot;member&quot;).usingGeneratedKeyColumns(&quot;id&quot;);

        Map&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;&amp;gt;();
        parameters.put(&quot;name&quot;, member.getName());

        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        member.setId(key.longValue());
        return member;
    }

    @Override
    public Optional&amp;lt;Member&amp;gt; findById(Long id) {
        List&amp;lt;Member&amp;gt; result = jdbcTemplate.query(&quot;select * from member where id = ?&quot;, memberRowMapper(),id);
        return result.stream().findAny();
    }

    @Override
    public Optional&amp;lt;Member&amp;gt; findByName(String name) {
        List&amp;lt;Member&amp;gt; result = jdbcTemplate.query(&quot;select * from member where name = ?&quot;, memberRowMapper(), name);
        return result.stream().findAny();
    }

    @Override
    public List&amp;lt;Member&amp;gt; findAll() {
        return jdbcTemplate.query(&quot;select * from member&quot;, memberRowMapper());
    }

    private RowMapper&amp;lt;Member&amp;gt; memberRowMapper(){
        return (rs, rowNum) -&amp;gt; {

            Member member = new Member();
            member.setId(rs.getLong(&quot;id&quot;));
            member.setName(rs.getString(&quot;name&quot;));
            return member;
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. JPA&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;JPA는 SQL을 직접 작성하지 않아도 객체 중심으로 데이터를 다룰 수 있게 해 개발 생산성과 유지보수성을 높여주는 기술임.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;1) build.gradle&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;파일에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;JPA, h2&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;데이터베이스&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;관련&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;라이브러리&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;추가&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750436751938&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	runtimeOnly 'com.h2database:h2'testImplementation('org.springframework.boot:spring-boot-starter-test') {
	exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}&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;2) 스프링 부트에 JPA 설정 추가&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;spring.application.name=hello-spring
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) JPA 엔티티 매핑(Member 클래스)&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package hello.hello_spring.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) JPA 회원 리포지토리(JpaMemberReposityory 클래스)&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import jakarta.persistence.EntityManager;

import java.util.List;
import java.util.Optional;

public class JpaMemberRepository implements MemberRepository {

    private final EntityManager em;

    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }
    public Member save(Member member) {
        em.persist(member);
        return member;
    }
    public Optional&amp;lt;Member&amp;gt; findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }
    public List&amp;lt;Member&amp;gt; findAll() {
        return em.createQuery(&quot;select m from Member m&quot;, Member.class)
                .getResultList();
    }
    public Optional&amp;lt;Member&amp;gt; findByName(String name) {
        List&amp;lt;Member&amp;gt; result = em.createQuery(&quot;select m from Member m where m.name = :name&quot;, Member.class)
                .setParameter(&quot;name&quot;, name)
                .getResultList();
        return result.stream().findAny();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 스프링 데이터 JPA&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;스프링 부트와 JPA 위에 스프링 데이터 JPA를 사용하면 반복적인 CRUD 코드 없이 인터페이스만으로도 개발이 가능해져 생산성이 비약적으로 향상되고, 개발자는 핵심 비즈니스 로직에 집중할 수 있음.&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;1) 스프링&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;데이터&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;JPA&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;회원&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;리포지토리(SpringDataJpaMemberRepository 클래스)&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface SpringDataJpaMemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt;, MemberRepository {

    @Override
    Optional&amp;lt;Member&amp;gt; findByName(String name);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2)&amp;nbsp;스프링&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;데이터&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;JPA&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;회원&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;리포지토리를&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;사용하도록&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;스프링&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;설정&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;변경&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4; text-align: start;&quot;&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package hello.hello_spring.service;

import hello.hello_spring.repository.*;
import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class SpringConfig {
    private final MemberRepository memberRepository;

    @Autowired
    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }
    //@Bean
    //public MemberRepository memberRepository() {
        // return new MemoryMemberRepository();
        // return new JdbcMemberRepository(dataSource);
        //return new JdbcTemplateMemberRepository(dataSource);
        //return new JpaMemberRepository(em);
   // }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;8. AOP&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. AOP가 필요한 상황&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;115&quot; data-start=&quot;64&quot; data-ke-size=&quot;size16&quot;&gt;-회원 가입, 회원 조회 등 모든 메소드의 실행 시간을 측정하고 싶다면 AOP가 필요함.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-end=&quot;321&quot; data-start=&quot;117&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;174&quot; data-start=&quot;117&quot;&gt;시간 측정은 공통 관심 사항이며, 비즈니스 로직(핵심 관심 사항)과는 분리되어야 함.&lt;/li&gt;
&lt;li data-end=&quot;228&quot; data-start=&quot;175&quot;&gt;시간 측정 코드를 각 메소드에 직접 작성하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;유지보수가 어렵고 중복이 많아짐.&lt;/li&gt;
&lt;li data-end=&quot;275&quot; data-start=&quot;229&quot;&gt;로직 수정 시&lt;span&gt;&amp;nbsp;&lt;/span&gt;모든 코드를 찾아서 변경해야 하는 번거로움이 생김&lt;/li&gt;
&lt;li data-end=&quot;321&quot; data-start=&quot;276&quot;&gt;OOP만으로는 메소드 실행 전후 처리를&lt;span&gt;&amp;nbsp;&lt;/span&gt;효율적으로 분리하기 어려움.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;384&quot; data-start=&quot;323&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt; AOP를 사용하면 실행 시간 측정과 같은 공통 로직을&lt;span&gt;&amp;nbsp;&lt;/span&gt;핵심 로직과 분리해 깔끔하게 관리 가능.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. AOP 적용&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오전 11.41.56.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btkytO/btsOLheOWYD/gtFR2TeX58qxkTkdaEOGo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btkytO/btsOLheOWYD/gtFR2TeX58qxkTkdaEOGo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btkytO/btsOLheOWYD/gtFR2TeX58qxkTkdaEOGo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtkytO%2FbtsOLheOWYD%2FgtFR2TeX58qxkTkdaEOGo0%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-06-21 오전 11.41.56.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>ECC/Spring 입문</category>
      <author>jiheechoi</author>
      <guid isPermaLink="true">https://skoitart2e.tistory.com/14</guid>
      <comments>https://skoitart2e.tistory.com/14#entry14comment</comments>
      <pubDate>Sat, 21 Jun 2025 11:43:11 +0900</pubDate>
    </item>
    <item>
      <title>[스프링부트] 2. MVC 패턴 이해와 실습</title>
      <link>https://skoitart2e.tistory.com/13</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 뷰 템플릿과 MVC 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 뷰 템플릿이란&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;-1장에서 스프링 부트 프로젝트를 만들 때 추가한 머스테치가 바로 뷰 템플릿을 만드는 도구에 해당.&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;2) MVC 패턴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-MVC 패턴: 웹페이지를 화면에 보여주고(view), 클라이언트의 요청을 받아 처리하고(controller), 데이터를 관리하는(model) 역할을 나누는 기법&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;h2 data-ke-size=&quot;size26&quot;&gt;2. MVC 패턴을 활용해 뷰 템플릿 페이지 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 뷰 템플릿 페이지 만들기: src &amp;gt; main &amp;gt; resources &amp;gt; templates 에 greetings.mustache 작성&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.29.31.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XyEbS/btsN9a7EtWQ/kUlJf64GBtIFYdEHZsKc9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XyEbS/btsN9a7EtWQ/kUlJf64GBtIFYdEHZsKc9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XyEbS/btsN9a7EtWQ/kUlJf64GBtIFYdEHZsKc9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXyEbS%2FbtsN9a7EtWQ%2FkUlJf64GBtIFYdEHZsKc9k%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.29.31.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&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;(2) 컨트롤러 만들고 실행하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-컨트롤러는 src &amp;gt; main &amp;gt; java 디렉터리에 있는 기본 패키지 아래에 하나의 패키지로 만듦.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-만든 controller 패키지 밑에 FirstController 클래스를 만들고 이 클래스가 컨트롤러임을 선언하는 @Controller 어노테이션을 작성함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-반환형이 문자열인 niceToMeetYou() 메서드를 선언하여 앞에서 만든 greetings.mustache 페이지를 반환하도록 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-niceToMeetYou 메서드 앞에 @GetMapping()을 추가하고 괄호 안에 URL 주소인 &quot;/hi&quot;를 넣음 -&amp;gt; 페이지(greetings.mustache)를 반환해 달라는 URL 요청을 접수하는 부분임. 웹 브라우저에서 localhost:8080/hi로 접속하면 greetings.mustache 파일을 찾아 반환하라는 뜻.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.38.38.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdRSuw/btsN9Hc4D39/PqtZGImgQFB57ruZs4wNK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdRSuw/btsN9Hc4D39/PqtZGImgQFB57ruZs4wNK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdRSuw/btsN9Hc4D39/PqtZGImgQFB57ruZs4wNK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdRSuw%2FbtsN9Hc4D39%2FPqtZGImgQFB57ruZs4wNK1%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.38.38.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.41.39.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0Z2Yw/btsN90DyfKE/KkiCinGtsq6ARdmAytksMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0Z2Yw/btsN90DyfKE/KkiCinGtsq6ARdmAytksMk/img.png&quot; data-alt=&quot;localhost:8080/hi&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0Z2Yw/btsN90DyfKE/KkiCinGtsq6ARdmAytksMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0Z2Yw%2FbtsN90DyfKE%2FKkiCinGtsq6ARdmAytksMk%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.41.39.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;localhost:8080/hi&lt;/figcaption&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;(3) 모델 추가하기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.46.42.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MEXT3/btsN9JhMjJl/S7lvMJDoB8ISwPXik5LHw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MEXT3/btsN9JhMjJl/S7lvMJDoB8ISwPXik5LHw1/img.png&quot; data-alt=&quot;greetings.mustache에 변수 추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MEXT3/btsN9JhMjJl/S7lvMJDoB8ISwPXik5LHw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMEXT3%2FbtsN9JhMjJl%2FS7lvMJDoB8ISwPXik5LHw1%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.46.42.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;greetings.mustache에 변수 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.45.50.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUlbTw/btsN91vIMSk/3Vn9KBKtyU8AB6q39oZS21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUlbTw/btsN91vIMSk/3Vn9KBKtyU8AB6q39oZS21/img.png&quot; data-alt=&quot;컨트롤러의 메서드에서 매개변수로 모델을 받아도록 수정하고 username 변수 등록&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUlbTw/btsN91vIMSk/3Vn9KBKtyU8AB6q39oZS21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUlbTw%2FbtsN91vIMSk%2F3Vn9KBKtyU8AB6q39oZS21%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.45.50.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컨트롤러의 메서드에서 매개변수로 모델을 받아도록 수정하고 username 변수 등록&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. MVC의 역할과 실행 흐름 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-웹 서비스는 클라이언트-서버 구조로 동작하고, 스프링 부트는 서버의 역할을 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-서버는 모델, 뷰, 컨트롤러가 유기적으로 역할을 분담해 클라이언트의 요청을 처리함. -&amp;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;1) /hi 페이지의 실행 흐름(FirstController.java)&lt;/p&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;클라이언트로부터 &quot;/hi&quot;라는 요청을 받아 접수&lt;/li&gt;
&lt;li&gt;&quot;/hi&quot;라는 요청을 받음과 동시에 niceToMeetYou() 메서드를 수행함.&lt;/li&gt;
&lt;li&gt;뷰 템플릿 페이지에서 사용할 변수를 등록하기 위해 모델 객체를 매개변수로 가져옴.&lt;/li&gt;
&lt;li&gt;모델에서 사용할 변수를 등록함. 변숫값에 따라 서로 다른 뷰 템플릿 페이지 출력됨.&lt;/li&gt;
&lt;li&gt;메서드를 수행한 결과로 greetings.mustache 파일 반환함. return 문에는 파일 이름만 작성하면 됨.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_A7166C4AB50B-1.jpeg&quot; data-origin-width=&quot;1344&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IM1Uz/btsN8ZezJi1/85gcNygCA5CkSVK5r2Xcf1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IM1Uz/btsN8ZezJi1/85gcNygCA5CkSVK5r2Xcf1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IM1Uz/btsN8ZezJi1/85gcNygCA5CkSVK5r2Xcf1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIM1Uz%2FbtsN8ZezJi1%2F85gcNygCA5CkSVK5r2Xcf1%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;1344&quot; height=&quot;550&quot; data-filename=&quot;IMG_A7166C4AB50B-1.jpeg&quot; data-origin-width=&quot;1344&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) /bye 페이지의 실행 흐름&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.58.27.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVW67r/btsN87KefsR/TtgzvZ9XMbY5cyQ0ktb20K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVW67r/btsN87KefsR/TtgzvZ9XMbY5cyQ0ktb20K/img.png&quot; data-alt=&quot;bye.mustache&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVW67r/btsN87KefsR/TtgzvZ9XMbY5cyQ0ktb20K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVW67r%2FbtsN87KefsR%2FTtgzvZ9XMbY5cyQ0ktb20K%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.58.27.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;bye.mustache&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.59.24.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBbOgZ/btsN9hFGczX/HmAWgld4ggiSRhUjXg4KeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBbOgZ/btsN9hFGczX/HmAWgld4ggiSRhUjXg4KeK/img.png&quot; data-alt=&quot;FirstController 수정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBbOgZ/btsN9hFGczX/HmAWgld4ggiSRhUjXg4KeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBbOgZ%2FbtsN9hFGczX%2FHmAWgld4ggiSRhUjXg4KeK%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-05-23 오전 1.59.24.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FirstController 수정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 뷰 템플릿 페이지에 레이아웃 적용하기&lt;/h2&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;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;size16&quot;&gt;1) /hi 페이지에 헤더-푸터 레이아웃 적용하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*greetings.mustache 페이지를 쉽고 빠르게 꾸미기 위해 부트스트랩을 사용하자.(부트스트랩이란 웹 페이지를 쉽게 만들 수 있도록 작성해 놓은 코드 모음으로 각종 레이아웃, 버튼, 입력창 등 디자인을 미리 구현해 놓은 것)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-greetings.mustache 코드 작성&lt;/p&gt;
&lt;pre id=&quot;code_1747934256456&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;!-- Required meta tags --&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&amp;gt;

    &amp;lt;!-- Bootstrap CSS --&amp;gt;
    &amp;lt;link href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot; integrity=&quot;sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC&quot; crossorigin=&quot;anonymous&quot;&amp;gt;

    &amp;lt;title&amp;gt;Hello, world!&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;!-- navigation --&amp;gt;
    &amp;lt;nav class=&quot;navbar navbar-expand-lg navbar-light bg-light&quot;&amp;gt;
        &amp;lt;div class=&quot;container-fluid&quot;&amp;gt;
            &amp;lt;a class=&quot;navbar-brand&quot; href=&quot;#&quot;&amp;gt;Navbar&amp;lt;/a&amp;gt;
            &amp;lt;button class=&quot;navbar-toggler&quot; type=&quot;button&quot; data-bs-toggle=&quot;collapse&quot; data-bs-target=&quot;#navbarSupportedContent&quot; aria-controls=&quot;navbarSupportedContent&quot; aria-expanded=&quot;false&quot; aria-label=&quot;Toggle navigation&quot;&amp;gt;
                &amp;lt;span class=&quot;navbar-toggler-icon&quot;&amp;gt;&amp;lt;/span&amp;gt;
            &amp;lt;/button&amp;gt;
            &amp;lt;div class=&quot;collapse navbar-collapse&quot; id=&quot;navbarSupportedContent&quot;&amp;gt;
                &amp;lt;ul class=&quot;navbar-nav me-auto mb-2 mb-lg-0&quot;&amp;gt;
                    &amp;lt;li class=&quot;nav-item&quot;&amp;gt;
                        &amp;lt;a class=&quot;nav-link active&quot; aria-current=&quot;page&quot; href=&quot;#&quot;&amp;gt;Home&amp;lt;/a&amp;gt;
                    &amp;lt;/li&amp;gt;
                    &amp;lt;li class=&quot;nav-item&quot;&amp;gt;
                        &amp;lt;a class=&quot;nav-link&quot; href=&quot;#&quot;&amp;gt;Link&amp;lt;/a&amp;gt;
                    &amp;lt;/li&amp;gt;
                    &amp;lt;li class=&quot;nav-item dropdown&quot;&amp;gt;
                        &amp;lt;a class=&quot;nav-link dropdown-toggle&quot; href=&quot;#&quot; id=&quot;navbarDropdown&quot; role=&quot;button&quot; data-bs-toggle=&quot;dropdown&quot; aria-expanded=&quot;false&quot;&amp;gt;
                            Dropdown
                        &amp;lt;/a&amp;gt;
                        &amp;lt;ul class=&quot;dropdown-menu&quot; aria-labelledby=&quot;navbarDropdown&quot;&amp;gt;
                            &amp;lt;li&amp;gt;&amp;lt;a class=&quot;dropdown-item&quot; href=&quot;#&quot;&amp;gt;Action&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                            &amp;lt;li&amp;gt;&amp;lt;a class=&quot;dropdown-item&quot; href=&quot;#&quot;&amp;gt;Another action&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                            &amp;lt;li&amp;gt;&amp;lt;hr class=&quot;dropdown-divider&quot;&amp;gt;&amp;lt;/li&amp;gt;
                            &amp;lt;li&amp;gt;&amp;lt;a class=&quot;dropdown-item&quot; href=&quot;#&quot;&amp;gt;Something else here&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                        &amp;lt;/ul&amp;gt;
                    &amp;lt;/li&amp;gt;
                    &amp;lt;li class=&quot;nav-item&quot;&amp;gt;
                        &amp;lt;a class=&quot;nav-link disabled&quot; href=&quot;#&quot; tabindex=&quot;-1&quot; aria-disabled=&quot;true&quot;&amp;gt;Disabled&amp;lt;/a&amp;gt;
                    &amp;lt;/li&amp;gt;
                &amp;lt;/ul&amp;gt;
                &amp;lt;form class=&quot;d-flex&quot;&amp;gt;
                    &amp;lt;input class=&quot;form-control me-2&quot; type=&quot;search&quot; placeholder=&quot;Search&quot; aria-label=&quot;Search&quot;&amp;gt;
                    &amp;lt;button class=&quot;btn btn-outline-success&quot; type=&quot;submit&quot;&amp;gt;Search&amp;lt;/button&amp;gt;
                &amp;lt;/form&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/nav&amp;gt;

    &amp;lt;!-- content --&amp;gt;
    &amp;lt;div class=&quot;bg-dark text-white p-5&quot;&amp;gt;
        &amp;lt;h1&amp;gt;{{username}}님, 반갑습니다!&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- site info --&amp;gt;
    &amp;lt;div class=&quot;mb-5 container-fluid&quot;&amp;gt;
        &amp;lt;hr&amp;gt;
        &amp;lt;p&amp;gt; CloudStudying : &amp;lt;a href=&quot;#&quot;&amp;gt;Privacy&amp;lt;/a&amp;gt; : &amp;lt;a href=&quot;#&quot;&amp;gt;Terms&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js&quot; integrity=&quot;sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM&quot; crossorigin=&quot;anonymous&quot;&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-23 오전 2.17.00.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BspKw/btsOan6fBUe/aEtJ85jNECkKdcP6SyzDU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BspKw/btsOan6fBUe/aEtJ85jNECkKdcP6SyzDU1/img.png&quot; data-alt=&quot;/hi 페이지에 헤더-푸터 레이아웃 적용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BspKw/btsOan6fBUe/aEtJ85jNECkKdcP6SyzDU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBspKw%2FbtsOan6fBUe%2FaEtJ85jNECkKdcP6SyzDU1%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-05-23 오전 2.17.00.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;/hi 페이지에 헤더-푸터 레이아웃 적용&lt;/figcaption&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;2) /bye 페이지에 헤더-푸터 레이아웃 적용하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-앞선 방식과 똑같이 수행한다면 코드가 너무 길고 비효율적이므로 /hi 페이지를 템플릿화해서 사용함. -&amp;gt; 템플릿화란, 코드를 하나의 틀로 만들어 변수화한다는 뜻임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-특정 코드 영역을 변수화해 사용하려면 각 영역을 발췌하여 템플릿 파일로 만들어야 함(templates 디렉터리)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;뷰 템플릿 파일을 불러올 떄는 일반 변수를 {{변수명}}으로 사용하는 것과 달리 {{&amp;gt;파일명}}으로 작성함.greetins.mustache 파일의 내비게이션 바 부분을 잘라내 templates &amp;gt; layouts 디렉터리에 header.mustache로 만들고, 푸터 부분 역시 잘라내 footer.mustache에 붙여넣음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-23 오전 2.32.27.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckLBOR/btsN8qKqprD/CNdLSJTa4Hh0T3A81mPni1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckLBOR/btsN8qKqprD/CNdLSJTa4Hh0T3A81mPni1/img.png&quot; data-alt=&quot;greetings.mustache 수정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckLBOR/btsN8qKqprD/CNdLSJTa4Hh0T3A81mPni1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckLBOR%2FbtsN8qKqprD%2FCNdLSJTa4Hh0T3A81mPni1%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-05-23 오전 2.32.27.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;greetings.mustache 수정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrB26/btsN8Wa4uP6/Z0TXK4dtJ1U1eIvJqf5RzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrB26/btsN8Wa4uP6/Z0TXK4dtJ1U1eIvJqf5RzK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-05-23 오전 2.32.34.png&quot; data-widthpercent=&quot;50&quot; style=&quot;width: 49.418605%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrB26/btsN8Wa4uP6/Z0TXK4dtJ1U1eIvJqf5RzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrB26%2FbtsN8Wa4uP6%2FZ0TXK4dtJ1U1eIvJqf5RzK%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;2880&quot; height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;goodbye.mustache 수정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>ECC/스프링 부트</category>
      <author>jiheechoi</author>
      <guid isPermaLink="true">https://skoitart2e.tistory.com/13</guid>
      <comments>https://skoitart2e.tistory.com/13#entry13comment</comments>
      <pubDate>Fri, 23 May 2025 02:35:07 +0900</pubDate>
    </item>
    <item>
      <title>[스프링부트] 1. 스프링 부트 개요</title>
      <link>https://skoitart2e.tistory.com/12</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 스프링 부트란&lt;/h2&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;개발 환경 설정을 간소화: 스프링은 버전에 따라 동작하는 외부 라이브러리를 일일이 찾아 연동해야 했지만, 스프링 부트는 미리 설정된 스타터 프로젝트로 외부 라이브러리를 최적화해 제공하므로 사용자가 직접 연동할 필요가 x&lt;/li&gt;
&lt;li&gt;웹 애플리케이션 서버를 내장: 스프링 부트는 내부에 톰캣이라는 웹 애플리케이션 서버를 내장하고 있어 웹 서비스를 jar 파일로 간편하게 배포 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 스프링 부트 개발 환경 설정하기&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;JDK 설치하기&lt;/li&gt;
&lt;li&gt;IDE 설치하기&lt;/li&gt;
&lt;li&gt;스프링 부트 프로젝트 만들기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) Spring Initializr 페이지에 접속하여 스프링 부트 프로젝트를 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-23 오전 12.17.13.png&quot; data-origin-width=&quot;2610&quot; data-origin-height=&quot;1636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nTQ6F/btsN71qSQv9/evKjCqirRUiZ2u5Ezy0SQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nTQ6F/btsN71qSQv9/evKjCqirRUiZ2u5Ezy0SQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nTQ6F/btsN71qSQv9/evKjCqirRUiZ2u5Ezy0SQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnTQ6F%2FbtsN71qSQv9%2FevKjCqirRUiZ2u5Ezy0SQ0%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;2610&quot; height=&quot;1636&quot; data-filename=&quot;스크린샷 2025-05-23 오전 12.17.13.png&quot; data-origin-width=&quot;2610&quot; data-origin-height=&quot;1636&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;(2)다운로드 받은 프로젝트의 압축을 풀고 인텔리제이에서 실행한다. 모든 빌드가 끝나면 BUILD SUCCESSFUL 메시지가 나온다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-23 오전 12.24.00.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1794&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F8lng/btsN8ww9OAZ/ohS1bHhsJi98kyV9oUXu70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F8lng/btsN8ww9OAZ/ohS1bHhsJi98kyV9oUXu70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F8lng/btsN8ww9OAZ/ohS1bHhsJi98kyV9oUXu70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF8lng%2FbtsN8ww9OAZ%2FohS1bHhsJi98kyV9oUXu70%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;2880&quot; height=&quot;1794&quot; data-filename=&quot;스크린샷 2025-05-23 오전 12.24.00.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1794&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;(3) 메인 메서드 실행해 프로젝트 동작해보기(헬로 월드! 출력하기)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-23 오전 12.31.19.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TBKGa/btsN8FOdwo8/ByjH9JJEWLMwZzvNNSCAR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TBKGa/btsN8FOdwo8/ByjH9JJEWLMwZzvNNSCAR0/img.png&quot; data-alt=&quot;localhost:8080/hello.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TBKGa/btsN8FOdwo8/ByjH9JJEWLMwZzvNNSCAR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTBKGa%2FbtsN8FOdwo8%2FByjH9JJEWLMwZzvNNSCAR0%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;2880&quot; height=&quot;1800&quot; data-filename=&quot;스크린샷 2025-05-23 오전 12.31.19.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;localhost:8080/hello.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 웹 서비스의 동작 원리 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 클라이언트-서버 구조&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;-서버: 서비스를 제공하는 프로그램 또는 컴퓨터&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;2) localhost:8080/hello.html의 의미&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-localhost: 실행 중인 서버의 주소 중 특별한 주소인 '내 컴퓨터'를 의미함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-8080: 포트번호를 의미함. 스프링 부트 프로젝트는 톰캣에 담겨 8080에서 기본 실행됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-hello.html: 서버에 요청하는 파일을 의미함.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-웹 브라우저에서&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;localhost:8080/hello.html로 접속하면 내 컴퓨터의 8080번에서 수행되는 서버에 hello.html 파일을 요청함. 이렇게 파일을 직접 지정할 경우 스프링 부트는 기본적으로 src &amp;gt; main &amp;gt; resources &amp;gt; static 디렉터리에서 파일을 찾음.&lt;/span&gt;&lt;/p&gt;</description>
      <category>ECC/스프링 부트</category>
      <author>jiheechoi</author>
      <guid isPermaLink="true">https://skoitart2e.tistory.com/12</guid>
      <comments>https://skoitart2e.tistory.com/12#entry12comment</comments>
      <pubDate>Fri, 23 May 2025 00:39:57 +0900</pubDate>
    </item>
    <item>
      <title>[Spring 입문] 4. 회원 관리 예제 - 백엔드 개발</title>
      <link>https://skoitart2e.tistory.com/11</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 비즈니스 요구사항 정리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터: 회원 ID, 이름&lt;/li&gt;
&lt;li&gt;기능: 회원 등록, 조회&lt;/li&gt;
&lt;li&gt;아직 DB는 선정되지 않았다는 시나리오&lt;/li&gt;
&lt;li&gt;클래스 의존관계는 다음과 같다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-10 오전 12.13.56.png&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;788&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vBcMc/btsNR7qddsF/fzSrQxURe3yyiQne0KQTNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vBcMc/btsNR7qddsF/fzSrQxURe3yyiQne0KQTNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vBcMc/btsNR7qddsF/fzSrQxURe3yyiQne0KQTNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvBcMc%2FbtsNR7qddsF%2FfzSrQxURe3yyiQne0KQTNk%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;583&quot; height=&quot;788&quot; data-filename=&quot;스크린샷 2025-05-10 오전 12.13.56.png&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;788&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;2. 회원 도메인과 리포지토리 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-회원 객체(Member 클래스)&lt;/p&gt;
&lt;pre id=&quot;code_1746806285615&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package hello.hello_spring.domain;

public class Member {
    private Long id;    //시스템이 데이터를 구분하기 위해 저장하는 임의의 아이디
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}&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;- 회원 리포지토리 인터페이스(MemberRepository 인터페이스)&lt;/p&gt;
&lt;pre id=&quot;code_1746806412251&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;

import java.util.List;
import java.util.Optional;

public interface MemberRepository {

    Member save(Member member); //회원을 저장소에 저장
    Optional&amp;lt;Member&amp;gt; findById(Long id); //저장소에서 id로 회원찾기
    Optional&amp;lt;Member&amp;gt; findByName(String name);   //저장소에서 name으로 회원찾기
    List&amp;lt;Member&amp;gt; findAll(); //지금까지 저장된 모든 회원 리스트를 반환
    
}&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;-회원 리포지토리 메모리 구현체(MemoryMemberRepository 클래스)&lt;/p&gt;
&lt;pre id=&quot;code_1746806481030&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository {

    private static Map&amp;lt;Long, Member&amp;gt; store = new HashMap&amp;lt;&amp;gt;();
    private static long sequence = 0L;

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional&amp;lt;Member&amp;gt; findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional&amp;lt;Member&amp;gt; findByName(String name) {
        return store.values().stream()
                .filter(member -&amp;gt; member.getName().equals(name))
                .findAny();
    }

    @Override
    public List&amp;lt;Member&amp;gt; findAll() {
        return new ArrayList&amp;lt;&amp;gt;(store.values());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 회원 리포지토리 테스트 케이스 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-개발한 기능을 실행하여 테스트 -&amp;gt; main 메소드를 통한 실행 OR 컨트롤러를 통한 실행으로 테스트 가능&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-그러나 이러한 방법은 오래 걸리고 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어려움 -&amp;gt; JUnit 프레임워크를 이용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-src/test/java 하위 폴더에 생성&lt;/p&gt;
&lt;pre id=&quot;code_1746808234214&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;

class MemoryMemberRepositoryTest {
	MemoryMemberRepository repository = new MemoryMemberRepository();
	@AfterEach
	public void afterEach() {
		repository.clearStore();
	}
	@Test
	public void save() {
		//given
		Member member = new Member();
		member.setName(&quot;spring&quot;);
		
        //when
		repository.save(member);
		
        //then
		Member result = repository.findById(member.getId()).get();
		assertThat(result).isEqualTo(member);
	}
	@Test
	public void findByName() {
		//given
		Member member1 = new Member();
		member1.setName(&quot;spring1&quot;);
		repository.save(member1);
		
        Member member2 = new Member();
		member2.setName(&quot;spring2&quot;);
		repository.save(member2);
		
        //when
		Member result = repository.findByName(&quot;spring1&quot;).get();
		
        //then
		assertThat(result).isEqualTo(member1);
	}
	@Test
	public void findAll() {
		//given
		Member member1 = new Member();
		member1.setName(&quot;spring1&quot;);
		repository.save(member1);
	
    	Member member2 = new Member();
		member2.setName(&quot;spring2&quot;);
		repository.save(member2);
		
        //when
		List&amp;lt;Member&amp;gt; result = repository.findAll();
		
        //then
		assertThat(result.size()).isEqualTo(2);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-end=&quot;585&quot; data-start=&quot;301&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;378&quot; data-start=&quot;301&quot;&gt;@AfterEach&lt;br /&gt;각 테스트 실행 후 저장소(repository)를 비워주는 메소드. 테스트 간 데이터 충돌 방지용.&lt;/li&gt;
&lt;li data-end=&quot;452&quot; data-start=&quot;380&quot;&gt;save()&lt;span&gt;&amp;nbsp;&lt;/span&gt;테스트&lt;br /&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;save()&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드로 회원을 저장한 뒤,&lt;span&gt;&amp;nbsp;&lt;/span&gt;findById()로 잘 저장됐는지 확인.&lt;/li&gt;
&lt;li data-end=&quot;519&quot; data-start=&quot;454&quot;&gt;findByName()&lt;span&gt;&amp;nbsp;&lt;/span&gt;테스트&lt;br /&gt;&amp;rarr; 이름으로 회원을 검색했을 때, 올바른 회원 객체가 반환되는지 검증.&lt;/li&gt;
&lt;li data-end=&quot;585&quot; data-start=&quot;521&quot;&gt;findAll()&lt;span&gt;&amp;nbsp;&lt;/span&gt;테스트&lt;br /&gt;&amp;rarr; 저장된 모든 회원 목록을 조회했을 때, 기대한 개수와 일치하는지 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 회원 서비스 개발&lt;/h2&gt;
&lt;pre id=&quot;code_1746842141134&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
    
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    
    /* 회원가입 */
    public Long join (Member member) {
        validateDuplicatemember(member);    //중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicatemember(Member member) {
        memberRepository.findByName(member.getName())
                        .ifPresent(m -&amp;gt; {
                            throw new IllegalStateException(&quot;이미 존재하는 회원입니다.&quot;);
                        });
    }
    
    /* 전체 회원 조회 */
    public List&amp;lt;Member&amp;gt; findMembers(){
        return memberRepository.findAll();
    }
    
    public Optional&amp;lt;Member&amp;gt; findOne(Long memberId){
        return memberRepository.findById(memberId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-end=&quot;579&quot; data-start=&quot;275&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;365&quot; data-start=&quot;275&quot;&gt;memberRepository:&lt;br /&gt;&amp;rarr; 회원 데이터를 저장하고 조회하는 리포지토리로, 현재는&lt;span&gt;&amp;nbsp;&lt;/span&gt;MemoryMemberRepository로 구현됨.&lt;/li&gt;
&lt;li data-end=&quot;460&quot; data-start=&quot;367&quot;&gt;join(Member member):&lt;br /&gt;&amp;rarr; 회원 가입 로직을 담당.&lt;br /&gt;&amp;rarr; 내부적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;중복 회원 검증을 먼저 수행한 후, 검증을 통과하면 저장.&lt;/li&gt;
&lt;li data-end=&quot;579&quot; data-start=&quot;462&quot;&gt;validateDuplicatemember(Member member):&lt;br /&gt;&amp;rarr; 저장된 회원 중 같은 이름이 있는지 확인.&lt;br /&gt;&amp;rarr; 존재할 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;IllegalStateException&lt;span&gt;&amp;nbsp;&lt;/span&gt;예외 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 회원 서비스 테스트&lt;/h2&gt;
&lt;pre id=&quot;code_1746842809655&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {
    MemberService memberService;
    MemoryMemberRepository memberRepository;
    
    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }
    
    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }
    
    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName(&quot;hello&quot;);
        
        //When
        Long saveId = memberService.join(member);
        
        //Then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }
    
    @Test
    public void 중복_회원_예외() throws Exception {
        //Given
        Member member1 = new Member();
        member1.setName(&quot;spring&quot;);
        Member member2 = new Member();
        member2.setName(&quot;spring&quot;);
        
        //When
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -&amp;gt; memberService.join(member2));//예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo(&quot;이미 존재하는 회원입니다.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>ECC/Spring 입문</category>
      <author>jiheechoi</author>
      <guid isPermaLink="true">https://skoitart2e.tistory.com/11</guid>
      <comments>https://skoitart2e.tistory.com/11#entry11comment</comments>
      <pubDate>Sat, 10 May 2025 12:23:42 +0900</pubDate>
    </item>
    <item>
      <title>[Spring 입문] 5. 스프링 빈과 의존관계</title>
      <link>https://skoitart2e.tistory.com/10</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 스프링 빈이란?&lt;/h2&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;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;h2 data-ke-size=&quot;size26&quot;&gt;2. 컴포넌트 스캔과 자동 의존관계 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-컴포넌트 스캔 원리&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@Component 애노테이션이 있으면 스프링 빈으로 자동 등록됨.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;@Controller, @Service, @Repositroy 도 @Component 애노테이션을 포함하므로 스프링 빈으로 자동 등록됨&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 4장 예제 스프링 빈 등록해보기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;-&amp;gt; 회원 서비스 스프링 빈 등록&lt;/p&gt;
&lt;pre id=&quot;code_1746845816675&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }&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; &amp;nbsp;-&amp;gt; 회원 리포지토리 스프링 빈 등록&lt;/p&gt;
&lt;pre id=&quot;code_1746845927138&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public class MemoryMemberRepository implements MemberRepository {

    private static Map&amp;lt;Long, Member&amp;gt; store = new HashMap&amp;lt;&amp;gt;();
    private static long sequence = 0L;
    ... }&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; &amp;nbsp; -&amp;gt; &amp;nbsp;MemberController 클래스&lt;/p&gt;
&lt;pre id=&quot;code_1746845987651&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package hello.hello_spring.controller;

import hello.hello_spring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 자바 코드로 직접 스프링 빈 등록하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-4장 예제 스프링 빈 등록해보기&lt;/p&gt;
&lt;pre id=&quot;code_1746846661364&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package hello.hellospring;

import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {
	@Bean
	public MemberService memberService() {
		return new MemberService(memberRepository());
	}
	@Bean
	public MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}	
}&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;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-end=&quot;545&quot; data-start=&quot;79&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;151&quot; data-start=&quot;79&quot;&gt;XML 설정 방식
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;151&quot; data-start=&quot;101&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;151&quot; data-start=&quot;101&quot;&gt;과거에는 XML로 설정했지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;최근에는 잘 사용하지 않음&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;생략 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;268&quot; data-start=&quot;153&quot;&gt;DI 방법 3가지
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;268&quot; data-start=&quot;175&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;186&quot; data-start=&quot;175&quot;&gt;필드 주입&lt;/li&gt;
&lt;li data-end=&quot;205&quot; data-start=&quot;190&quot;&gt;Setter 주입&lt;/li&gt;
&lt;li data-end=&quot;268&quot; data-start=&quot;209&quot;&gt;생성자 주입&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr; 권장 방식&lt;span&gt;&amp;nbsp;&lt;/span&gt;(의존관계가 실행 중 동적으로 변할 일이 거의 없기 때문)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;434&quot; data-start=&quot;270&quot;&gt;컴포넌트 스캔 vs 수동 등록
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;434&quot; data-start=&quot;297&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;367&quot; data-start=&quot;297&quot;&gt;컴포넌트 스캔 사용&lt;br /&gt;&amp;rarr; 정형화된 코드 (예: Controller, Service, Repository)&lt;/li&gt;
&lt;li data-end=&quot;434&quot; data-start=&quot;371&quot;&gt;설정을 통한 수동 등록&lt;br /&gt;&amp;rarr; 정형화되지 않거나, 상황에 따라 구현 클래스를 바꿔야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;545&quot; data-start=&quot;436&quot;&gt;@Autowired&lt;span&gt;&amp;nbsp;&lt;/span&gt;주의점
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;545&quot; data-start=&quot;463&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;510&quot; data-start=&quot;463&quot;&gt;@Autowired는&lt;span&gt;&amp;nbsp;&lt;/span&gt;스프링이 관리하는 객체(=스프링 빈)&lt;span&gt;&amp;nbsp;&lt;/span&gt;에서만 동작&lt;/li&gt;
&lt;li data-end=&quot;545&quot; data-start=&quot;514&quot;&gt;직접 new로 생성한 객체에서는 작동하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>ECC/Spring 입문</category>
      <author>jiheechoi</author>
      <guid isPermaLink="true">https://skoitart2e.tistory.com/10</guid>
      <comments>https://skoitart2e.tistory.com/10#entry10comment</comments>
      <pubDate>Sat, 10 May 2025 12:14:56 +0900</pubDate>
    </item>
    <item>
      <title>[SQL] 7. 복수의 테이블 다루기</title>
      <link>https://skoitart2e.tistory.com/9</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 집합 연산&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 집합과 SQL&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-SELECT 명령을 실행하면 결과로 몇 개의 행이 반환되는데, 이때 반환된 결과 전체를 하나의 집합이라고 생각하면 됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;rarr;즉, 데이터베이스에서는 테이블의 행이 집합의 요소에 해당함.&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;2) UNION으로 합집합 구하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-SQL에서 합집합을 계산할 경우에는 UNION 키워드를 사용함.&lt;/p&gt;
&lt;pre id=&quot;code_1744331855233&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT a FROM sample71_a
UNION
SELECT b FROM sample71_b;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr;UNION으로 두 개의 SELECT 명령을 하나로 연계해 질의 결과를 얻을 수 있음.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-이때 각각의 SELECT 명령의 열 내용은 서로 일치해야 함.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;열 이름이 다르더라도 열 개수와 자료형이 일치하면 됨.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-SELECT 명령들을 UNION 으로 묶을 때 나열 순서는 합집합의 결과에 영향을 주지 않음.&lt;/p&gt;
&lt;pre id=&quot;code_1744331786330&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT a FROM sample71_a
UNION
SELECT b FROM sample71_b
UNION
SELECT age FROM sample31;&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;-UNION으로 SELECT 명령을 결합해 합집합을 구하는 경우, 각 SELECT 명령에 ORDER BY를 지정할 수 없음. 즉, 마지막 SELECT 명령에만 지정할 수 있음.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-합집합의 결과를 정렬하는 것이기 때문에 ORDER BY 구를 사용하여 정렬할 때에는, 서로 동일한 별명을 붙여 이름을 일치시켜야 함.&lt;/p&gt;
&lt;pre id=&quot;code_1744331963580&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT a AS c FROM sample71_a
UNION
SELECT b AS c FROM sample71_b ORDER BY b;&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;-UNION은 합집합을 구하는 것이므로 두 개의 집합에서 겹치는 부분은 공통 요소가 됨.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-중복을 제거하지 않고 2개의 SELECT 결과를 그냥 합치고 싶은 경우, UNION ALL을 사용함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;DISTINCT나 ALL로 중복제거 여부를 지정할 수 있다는 점은 SELECT 명령과 동일하지만, UNION의 기본동작은 ALL이 아닌 DISTINCT임.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744332265772&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT a FROM sample71_a
UNION ALL
SELECT b FROM sample71_b;&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;3) 교집합과 차집합&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-SQL에서 교집합을 구하려면 INTERSECT를 이용하고, 차집합을 구하려면 EXCEPT를 이용함. (MySQL은 지원 X)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 테이블 결합&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 곱집합과 교차결합&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-집합 X 와 집합 Y의 곱집합은 집합 X의 요소에 집합 Y의 각 요소를 붙여 계산하는 것임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-FROM 구에 복수의 테이블을 지정하면 교차결합을 함. 교차결합은 두 개의 테이블을 곱집합으로 계산함.&lt;/p&gt;
&lt;pre id=&quot;code_1744333112356&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM sample72_x, sample72_y;&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;-UNION으로 합집합을 구하면 세로 방향으로 더해지게 되는 한편, FROM 구로 테이블을 결합할 경우 가로 방향으로 더해지게 됨.&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;2) 내부 결합&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-내부 결합이란, 교차결합으로 계산된 곱집합에서 원하는 조합을 검색하는 것임.&lt;/p&gt;
&lt;pre id=&quot;code_1744333374606&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT 상품.상품명, 재고.재고수 FROM 상품, 재고수 
 WHERE 상품.상품코드 = 재고.재고수
  AND 상품.상품분류 = '식료품';&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) INNER JOIN 으로 내부 결합하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-INNER JOIN으로 두 개 테이블을 가로로 결합할 수 있음.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;rarr;SYNTAX: SELECT * FROM 테이블명1 INNER JOIN 테이블명2 ON 결합조건&lt;/p&gt;
&lt;pre id=&quot;code_1744347038928&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT 상품.상품명, 재고수.재고수
 FROM 상품 INNER JOIN 재고수
  ON 상품.상품코드 = 재고수.상품코드
WHERE 상품.상품분류 = '식료품';&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;4) 내부결합을 활용한 데이터 관리&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;-자기결합: 테이블에 별명을 붙일 수 있는 기능을 이용해 같은 테이블끼리 결합하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;rarr;자기 자신의 기본키를 참조하는 열을 자기 자신이 가지는 데이터 구조로 되어 있을 경우에 자주 사용함.&lt;/p&gt;
&lt;pre id=&quot;code_1744347257030&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT S1.상품명, S2.상품명
 FROM 상품 S1 INNER JOIN 상품 S2
  ON S1.상품코드 = S2.상품코드;&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;5) 외부결합&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;pre id=&quot;code_1744347468232&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT 상품3.상품명, 재고수.재고수
 FROM 상품3 LEFT JOIN 재고수
  ON 상품3.상품코드 = 재고수.상품코드
WHERE 상품3.상품분류 = '식료품';&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;rarr;왼쪽 테이블을 기준으로 결합할 때: LEFT JOIN / 오른쪽 테이블을 기준으로 결합할 때: RIGHT JOIN&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-구식방법에서의 외부결합(Oracle): FROM 구에 결합 조건을 기술하지 않고 WHERE 구로 결합조건을 지정. 그냥 조건식을 지정하면 내부결합이 되어버리므로 외부결합으로 진행하고 싶은 경우에는 (+) 라는 특수 기호를 붙여서 조건식을 지정함.&lt;/p&gt;
&lt;pre id=&quot;code_1744347861779&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT 상품3.상품명, 재고수.재고수
 FROM 상품3.재고수
 WHERE 상품3.상품코드 = 재고수.상품코드 (+)
  AND 상품3.상품분류 = '식료품';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 관계형 모델&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 관계형 모델&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; &amp;rarr;관계형 모델의 릴레이션은 SQL에서 말하는 테이블에 해당됨.&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; &amp;rarr;관계형 모델의 속성은 SQL에서 말하는 열에 해당함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-SQL에서의 행은 관계형 모델에서 튜플이라고 불린다.&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; &amp;rarr;관계대수의 기본규칙&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;li&gt;연산을 중첩구조로 실행해도 상관없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2)관계형 모델과 SQL&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; &amp;rarr;SQL에서는 UNION에 해당함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-차집합: 차집합은 릴레이션끼리의 뺄셈을 말함&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;rarr;SQL에서는 EXCEPT에 해당함.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-교집합: 교집합은 릴레이션끼리의 공통부분을 말함.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;rarr;SQL에서는 INTERSECT에 해당함.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-곱집합: 곱집합은 릴레이션끼리의 곱셈을 말함.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;rarr;SQL에서는 FROM구에 복수의 테이블을 지정한 경우 곱집합으로 계산됨.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-선택: 선택은 튜플의 추출을 말하며, 제한이라고 불리기도 함.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;rarr;SQL에서 튜플은 행을 말하기 때문에, WHERE 구에 조건을 지정해 검색하는 것에 해당.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-투영: 선택은 속성의 추출을 말함.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;rarr;SQL에서 속성은 열을 말하기 때문에, SELECT 구에 결과로 반환할 열을 지정하는 것에 해당함.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-결합: 결합은 릴레이션끼리 교차결합해 계산된 곱집합에서 결합조건을 만족하는 튜플을 추출하는 연산&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;rarr;SQL에서는 내부결합에 해당함.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ECC/SQL 첫걸음</category>
      <author>jiheechoi</author>
      <guid isPermaLink="true">https://skoitart2e.tistory.com/9</guid>
      <comments>https://skoitart2e.tistory.com/9#entry9comment</comments>
      <pubDate>Fri, 11 Apr 2025 14:32:11 +0900</pubDate>
    </item>
    <item>
      <title>[SQL] 6. 데이터베이스 객체 작성과 객체</title>
      <link>https://skoitart2e.tistory.com/8</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 데이터베이스 객체&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 데이터베이스 객체&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;-SELECT나 INSERT 등은 클라이언트에서 객체를 조작하는 SQL 명령어이지, 객체가 아님.&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;기존 이름이나 예약어와 중복하지 않음&lt;/li&gt;
&lt;li&gt;숫자로 시작할 수 없음&lt;/li&gt;
&lt;li&gt;언더스코어(_) 이외의 기호는 사용할 수 없음&lt;/li&gt;
&lt;li&gt;한글을 사용할 때는 더블쿼트(MySQL에서는 백쿼트)로 둘러쌈&lt;/li&gt;
&lt;li&gt;시스템이 허용하는 길이를 초과하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 스키마&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;p data-ke-size=&quot;size16&quot;&gt;-테이블과 스키마는 무엇인가를 담는 그릇 역할을 한다는 점과 각각의 그릇 안에서는 중복하지 않도록 이름을 정한다는 점이 유사함.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;rarr;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; 이처럼 이름이 충돌하지 않도록 기능하는 그릇을 '네임스페이스'라고 부름.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 테이블 작성/삭제/변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 테이블 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-SELECT, INSERT, DELETE, UPDATE는 SQL 명령 중에서도 DML(데이터 조작)로 분류되고, 스키마 내의 객체를 관리하는 명령은 DDL임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-테이블을 작성할 때 CREATE TABLE 명령을 사용함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;rarr;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; SYNTAX&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743777593208&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE 테이블명(
 열 정의1,
 열 정의2,
 ...
 )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;rarr;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; 열 정의 SYNTAX&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743777639857&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;열명 자료형 [DEFAULT 기본값] [NULL|NOT NULL]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743777692856&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE sample62(
 no INTEGER NOT NULL,
 a VARCHAR(30),
 b DATE);&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;2) 테이블 삭제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-테이블을 삭제할 떄에는 DROP TABLE 명령을 사용함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-SYNTAX: DROP TABLE 테이블명&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-많은 데이터베이스가 SQL 명령을 실행할 때 확인을 요구하지 않으므로 실수로 삭제하지 않도록 주의해야 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-테이블 정의는 그대로 둔채 데이터만 삭제할 때는 DELETE 명령을 사용함.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;rarr;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; 이때, WHERE 조건을 지정하지 않으면 모든 행을 삭제할 수 있음.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;-DELETE 명령은 행 단위로 여러가지 내부처리가 일어나므로 삭제할 행이 많으면 처리속도가 느림.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;rarr;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp;TRUNCATE TABLE 명령을 사용하면 모든 행을 삭제해아 할 때 빠른 속도로 삭제 가능함.(DDL 명령, 삭제할 행/WHERE 구 지정 불가)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;rarr;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; SYNTAX: TRUNCATE TABLE 테이블명&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;3) 테이블 변경&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;-테이블을 작성한 뒤 열 구성을 변경하고 싶다면 ALTER TABLE 명령을 사용함.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;-ALTER TABLE로 할 수 있는 일은 크게 두가지로 분류함.&lt;/span&gt;&lt;/span&gt;&lt;/span&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;-열 추가: ALTER TABLE에서 열을 추가할 떄는 ADD 하부명령을 통해 실행할 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;rarr;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; SYNTAX: ALTER TABLE 테이블명 ADD 열 정의&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743778048788&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE sample62 ADD newcol INTEGER;&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;-열 속성 변경: MODIFY 하부명령을 실행. 열 이름은 변경할 수 없고, 자료형/기본값 등의 속성은 변경 가능함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;rarr;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; SYNTAX: ALTER TABLE 테이블명 MODIFY 열 정의&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743778170637&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE sample62 MODIFY newcol VARCHAR(20);&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;-열 이름 변경: CHANGE 하부 명령. CHANGE는 열 이름뿐만 아니라 열 속성도 변경할 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;rarr; SYNTAX: ALTER TABLE 테이블명 CHANGE [기존 열 이름] [신규 열 정의]&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743778294786&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE sample62 CHANGE newcol c VARCHAR(20);&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;-열 삭제: DROP 하부 명령.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;rarr; SYNTAX: ALTER TABLE 테이블명 DROP 열명&lt;/p&gt;
&lt;pre id=&quot;code_1743778344886&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE sample62 DROP c;&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;4) ALTER TABLE 로 테이블 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-최대길이 연장: 문자열의 최대길이 연장&lt;/p&gt;
&lt;pre id=&quot;code_1743778459068&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE sample MODIFY col VARCHAR(30)&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;pre id=&quot;code_1743778508902&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE sample ADD new_col INTEGER&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 제약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 테이블 작성시 제약 정의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-CREATE TABLE로 테이블을 작성할 떄 제약을 같이 정의함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-열에 대해 정의하는 제약을 '열 제약'이라고 부르고, 한 개의 제약으로 복수의 열에 제약을 설명하는 경우를 '테이블 제약'이라고 부름.&lt;/p&gt;
&lt;pre id=&quot;code_1743778773250&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE sample631(
 a INTEGER NOT NULL,
 b INTEGER NOT NULL UNIQUE,
 c VARCHAR(30)
 );&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;-제약에 이름을 붙이면 나중에 관리하기가 쉬워지므로 CONTSTRAINT 키워드를 사용해서 이름을 지정해야 함.&lt;/p&gt;
&lt;pre id=&quot;code_1743778814141&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE sample632(
 a INTEGER NOT NULL,
 b INTEGER NOT NULL UNIQUE,
 c VARCHAR(30)
 CONSTRAINT pkey_sample PRIMARY KEY (no, sub_no)
 );&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;2) 제약 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-열 제약 추가: ALTER TABLE로 열 정의를 변경할 수 있음. 기존 테이블을 변경할 경우에는 제약을 위반하는 데이터가 있는지 먼저 검사함.&lt;/p&gt;
&lt;pre id=&quot;code_1743778973382&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE sample631 MODIFY c VARCHAR (30) NOT NULL;&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;&amp;nbsp;&amp;rarr; 만약 c 열에 NULL 값이 존재한다면 에러가 발생함.&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;-테이블 제약 추가: ALTER TABLE의 ADD 하부 명령으로 추가할 수 있음. 열 제약을 추가할 때와 마찬가지로 기존의 행을 검사해 추가할 제약을 위반하는 데이터가 있으면 에러 발생함.&lt;/p&gt;
&lt;pre id=&quot;code_1743779115631&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE sample631 ADD CONSTRAINT pkey_sample631 PRIMARY KEY(a);&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;3) 제약 삭제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-열 제약 삭제하기: MODIFY 명령어 이용하여 열 정의를 변경함.&lt;/p&gt;
&lt;pre id=&quot;code_1743779274979&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE sample631 MODIFY c VARCHAR(30);&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;-테이블 제약 삭제하기: DROP 하부 명령으로 삭제 가능&lt;/p&gt;
&lt;pre id=&quot;code_1743779347096&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE sample631 DROP CONSTRAINT pkey_sample631;&lt;/code&gt;&lt;/pre&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;p data-ke-size=&quot;size16&quot;&gt;4) 기본키&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-기본키 제약은 열을 기본키로 지정해 유일한 값을 가지도록 하는 구조임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-기본키로 지정할 열은 NOT NULL 제약이 설정되어 있어야 하고, INSERT/UPDATE 명령을 실행할 때 중복되는 값을 추가/갱신하려 한다면 기본키 제약에 위반되므로 에러가 발생함.&lt;/p&gt;
&lt;pre id=&quot;code_1743779630735&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE sample634(
 p INTEGER NOT NULL,
 a VARCHAR NOT NULL,
 CONSTRAINT pkey_sample634 PRIMARY KEY(p)
 );&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;h2 data-ke-size=&quot;size26&quot;&gt;4. 인덱스 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 인덱스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-인덱스는 테이블에 붙여진 색인이라고 할 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-인덱스의 역할은 SELECT 명령에 WHERE 구로 조건을 지정하고 그에 일치하는 행을 찾는 검색 과정에서의 속도 향상임.&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;&amp;nbsp;&amp;rarr; 데이터베이스의 인덱스에는 검색 시에 쓰이는 키워드와 대응하는 데이터 행의 장소가 저장되어 있음.&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;2) 검색에 사용하는 알고리즘&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; &amp;nbsp;&amp;rarr; 대량의 데이터를 검색할 떄는 이진탐색이 훨씬 빠름.&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;p data-ke-size=&quot;size16&quot;&gt;3) 유일성&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;h2 data-ke-size=&quot;size26&quot;&gt;5. 인덱스 작성과 삭제&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 인덱스 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-CREATE INTDEX 명령으로 인덱스를 작성함.&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;&amp;nbsp;&amp;rarr; Oracle, DB2: 스키마 객체 / SQL server, MySQL: 테이블 내의 객체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-SYNTAX: CREATE INDEX 인덱스명 ON 테이블명 (열명1, 열명2, ...)&lt;/p&gt;
&lt;pre id=&quot;code_1743780580819&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX isample65 ON sample62(no);&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; &amp;nbsp;&amp;rarr; sample62 테이블의 no열에 isample65라는 인덱스를 지정함.&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;2) 인덱스 삭제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-인덱스는 DROP INDEX 명령으로 삭제함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;rarr; SYNTAX1(인덱스가 스키마 객체인 경우) : DROP INDEX 인덱스명&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;rarr; SYNTAX1(인덱스가 테이블 내 객체인 경우) : DROP INDEX 인덱스명 ON 테이블명&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-인덱스는 테이블에 의존하는 객체이므로 테이블을 삭제하면 테이블에 작성된 인덱스도 자동으로 삭제됨. 인덱스만 삭제하는 경우에는 DROP INDEX 명령으로 삭제함.&lt;/p&gt;
&lt;pre id=&quot;code_1743780814559&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX isample65 ON sample62(a);&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;3) EXPLAIN&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-EXPLAIN 명령은 SQL 명령이 어떤 상태로 실행되는지 설명해주는 명령어임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;rarr; SYNTAX: EXPLAIN SQL 명령&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-다만 이 SQL 명령은 실제로는 실행되지 않고 어떻게 실행되는지를 설명해줄 뿐임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-EXPLAIN은 표준 SQL 에는 존재하지 않는 데이터베이스 의존형 명령임.&lt;/p&gt;
&lt;pre id=&quot;code_1743781048919&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN SELECT * FROM sample62 WHERE a = 'a';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.666667%;&quot;&gt;id&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 16.666667%;&quot;&gt;select_type&lt;/td&gt;
&lt;td style=&quot;width: 16.666667%;&quot;&gt;table&lt;/td&gt;
&lt;td style=&quot;width: 16.666667%;&quot;&gt;type&lt;/td&gt;
&lt;td style=&quot;width: 16.666667%;&quot;&gt;possible_keys&lt;/td&gt;
&lt;td style=&quot;width: 16.666667%;&quot;&gt;key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.666667%; text-align: right;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 16.666667%;&quot;&gt;simple&lt;/td&gt;
&lt;td style=&quot;width: 16.666667%;&quot;&gt;sample62&lt;/td&gt;
&lt;td style=&quot;width: 16.666667%;&quot;&gt;ref&lt;/td&gt;
&lt;td style=&quot;width: 16.666667%;&quot;&gt;isample65&lt;/td&gt;
&lt;td style=&quot;width: 16.666667%;&quot;&gt;ismple65&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;&amp;nbsp;&amp;rarr; possible_keys라는 곳에 사용될 수 있는 인덱스가 표시되며, key는 사용된 인덱스가 표시됨.&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;4) 최적화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-SELECT 명령을 실행할 떄 인덱스의 사용 여부를 선택하는 것은 데이터베이스 내부의 최적화에 의해 처리되는 부분임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-내부처리에서는 명령을 실행하기 앞서 실행계획을 세우는데, 이 실행계획을 확인하는 명령이 EXPLAIN 명령임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-실행 계획에서는 인덱스의 유무, 인덱스를 사용할 것인지 여부에 대해 데이터베이스 내부 최적화 처리를 통해 판단. 이때 판단 기준으로 인덱스의 품질도 고려함.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 뷰 작성과 삭제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 뷰&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-뷰란 본래 데이터베이스 객체로 등록할 수 없는 SELECT 명령을 객체로서 이름을 붙여 관리할 수 있도록 한 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-뷰를 참조하면 그에 정의된 SELECT 명령의 실행결과를 테이블처럼 사용가능함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-뷰를 작성하는 것으로 복잡한 SELECT 명령을 간략하게 표현할 수 있음.&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;&amp;nbsp;&amp;rarr; 테이블처럼 데이터를 쓰고 지울 수 있는 저장공간을 가지지 않기 때문에 SELECT 명령에서만 사용하는 것을 권장함.&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;2) 뷰 작성과 삭제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-뷰의 작성: CREATE VIEW로 뷰의 이름과 SELECT 명령을 지정함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;rarr; SYNTAX(뷰 작성): CREATE VIEW 뷰명 AS SELECT 명령&lt;/p&gt;
&lt;pre id=&quot;code_1743781960773&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE VIEW sample_view_67 AS SELECT * FROM sample54;

SELECT * FROM sample_view_67&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;&amp;nbsp;&amp;rarr; SYNTAX(열 지정하기): CREATE VIEW 뷰명 (열병1, 열명2, ...) AS SELECT 명령&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-뷰의 열 지정을 생략한 경우에는 SELECT 명령의 SELECT 구에서 지정하는 열 정보가 수집되어 자동적으로 뷰의 열로 지정됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-뷰 삭제: DROP VIEW 이용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;rarr; SYNTAX: DROP VIEW 뷰명&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;3) 뷰의 약점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-뷰는 대량의 저장공간을 필요로 하지 않는 대신, 계산능력을 필요로 하기 때문에 CPU 자원을 사용함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-뷰를 참조하면 뷰에 등록되어 있는 SELECT 명령이 실행되고, 실행결과는 일시적으로 보존되며 뷰를 참조할 때마다 SELECT 명령이 실행됨.&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;테이블에 보관하는 데이터 양이 많은 경우, 집계처리를 할 때 뷰가 사용되거나 뷰를 중첩해서 사용하는 경우 처리 속도가 저하됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 뷰에 지정된 테이블의 데이터가 자주 변경되지 않는 경우라면, 머티리얼라이즈드 뷰로 약점 보완 가능. (머티리얼라이즈드 뷰: 처음 참조되었을 때 데이터를 저장해두고 다시 참조할 떄 이전에 저장해 두었던 데이터를 사용하는 뷰)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부모 쿼리와 어떤 식으로든 연관된 서브쿼리의 경우, 뷰의 SELECT 명령으로 사용할 수 없음.(뷰를 구성하는 SELECT 명령은 단독으로도 실행할 수 있어야 하므로)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 함수테이블을 사용하여 보완가능함. (함수 테이블: 테이블을 결괏값으로 반환해주는 사용자 정의 함수. 함수는 인수를 지정할 수 있기 때문에 인수의 값에 따라 WHERE 조건을 붙여 결괏값을 바꿀 수 있음.)&amp;nbsp;&lt;/p&gt;</description>
      <category>ECC/SQL 첫걸음</category>
      <author>jiheechoi</author>
      <guid isPermaLink="true">https://skoitart2e.tistory.com/8</guid>
      <comments>https://skoitart2e.tistory.com/8#entry8comment</comments>
      <pubDate>Sat, 5 Apr 2025 01:02:10 +0900</pubDate>
    </item>
  </channel>
</rss>