<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>rose.log</title>
        <link>https://velog.io/</link>
        <description>신입 백엔드 개발자의 성장 기록</description>
        <lastBuildDate>Mon, 22 Jun 2026 15:40:52 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>rose.log</title>
            <url>https://velog.velcdn.com/images/dob-by/profile/4e8b5e8d-b9eb-423d-990a-2e05d8291703/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. rose.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dob-by" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[2026 상반기 회고: 신입 백엔드 개발자 4개월 차의 기록]]></title>
            <link>https://velog.io/@dob-by/2026-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0-%EC%8B%A0%EC%9E%85-%EA%B0%9C%EB%B0%9C%EC%9E%90-4%EA%B0%9C%EC%9B%94-%EC%B0%A8%EC%9D%98-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@dob-by/2026-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0-%EC%8B%A0%EC%9E%85-%EA%B0%9C%EB%B0%9C%EC%9E%90-4%EA%B0%9C%EC%9B%94-%EC%B0%A8%EC%9D%98-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Mon, 22 Jun 2026 15:40:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>조금 늦었을 뿐이고, 잠시 우선순위를 조정했을 뿐이다.</p>
</blockquote>
<p>2026년 상반기는 개발자로서의 첫 적응기였다.</p>
<p>새로운 환경에서 업무를 배우고, 내가 할 수 있는 것과 할 수 없는 것을 인정하는 방법을 배웠다. 계획했던 것들을 모두 이루지는 못했지만 그 과정에서 우선순위를 정하는 법과 꾸준함의 중요성을 다시 한 번 느낄 수 있었다.</p>
<p>잊어버리기 전에 2026년 상반기를 기록해두려고 한다.</p>
<h2 id="개발자로-이직-후-실무-적응">개발자로 이직 후 실무 적응</h2>
<p>자격증 공부와 실무 적응을 동시에 해내고 싶었다.</p>
<p>하지만 생각보다 업무가 빨리 끝나지 않았고, 야근이 잦아질수록 공부할 시간을 빼앗긴다는 생각에 답답하기도 했다.</p>
<p>결국 자격증은 잠시 내려놓고 일에 집중했다. 그 선택이 아쉽지 않도록 지금의 경험이 좋은 자산으로 남기를 바란다.</p>
<p>여러 번의 면접 끝에 연봉을 올려 새로운 회사에 왔다. 지금은 이 환경에 감사하며 배우는 데 집중해보려 한다.</p>
<h2 id="개발자로서의-첫-4개월">개발자로서의 첫 4개월</h2>
<p>상반기 가장 큰 변화는 개발자가 된 것이었다.</p>
<p>입사 후 통신사 빌링 시스템 개발 업무를 맡으며 처음으로 실무 개발 환경을 경험했다. 학교에서 학생들을 가르치던 시절과는 완전히 다른 환경이었다.</p>
<p>처음에는 개발자가 되면 코드를 잘 짜는 것이 가장 중요하다고 생각했다. 그런데 막상 일을 해보니 코드보다 먼저 이해해야 할 것들이 많았다.</p>
<p><span style='background-color: #FDECEF'>상품과 요금 구조를 이해해야 했고, 상품 변경 시 어떤 데이터가 어떻게 변경되는지 확인해야 했다.</span> 하나의 기능을 수정하더라도 기존 기능에 영향은 없는지, 예외 상황에서는 어떻게 동작하는지 확인해야 했다.</p>
<p>특히 최근 진행했던 개발 과제에서는 <strong>상품 변경과 관련된 여러 케이스를 검토하고 테스트하는 업무를 경험했다.</strong> 비슷해 보이는 요구사항도 실제로는 처리 기준이 달랐고, 어떤 데이터를 기준으로 판단해야 하는지 이해하는 데 시간이 많이 걸렸다. 이 과정에서 <span style='background-color: #FDECEF'>단순히 개발만 하는 것이 아니라 <strong>도메인을 이해하는 것</strong>이 중요하다는 것을 배웠다.</span></p>
<p>또 여러 부서와 협업하며 업무를 진행하는 과정도 쉽지 않았다. 같은 요구사항이라도 보는 관점이 달랐고, 내가 이해한 내용이 맞는지 계속 확인해야 했다. <strong>기술적인 문제보다 일정 조율이나 커뮤니케이션이 더 어렵다고 느낀 순간도 많았다.</strong></p>
<p>최근 개발 기간에는 &quot;퇴사할게요.&quot;라는 말이 목끝까지 차오르기도 했다. 업무 강도도 높았고 개인 시간이 사라진다는 생각에 힘들었다. 그런데 신기하게도 적응하는 시기라고 받아들이고 나니 조금씩 일이 재미있어지기 시작했다.</p>
<p>아직은 모르는 것이 훨씬 많다. 그래도 처음 입사했을 때와 비교하면 업무 흐름도 조금씩 보이기 시작했고, 내가 맡은 개발 건을 스스로 처리하는 범위도 넓어지고 있다. 실무 개발자로서 조금씩 성장하고 있다는 점에서 의미 있는 시간이었다.</p>
<h2 id="러닝과-체력-관리">러닝과 체력 관리</h2>
<p>상반기에는 스트레스 관리가 생각보다 쉽지 않았다.</p>
<p>퇴근 후 아무것도 하기 싫은 날도 많았고 운동을 미루는 날도 있었다. 예전 같았으면 그냥 쉬었을 텐데 어떻게든 러닝화를 신고 밖으로 나가려고 했다.</p>
<p><strong>그래도 결국 내 균형을 잡아준 것은 러닝이었다. 땀을 흘리고 나면 하루 종일 머릿속을 괴롭히던 생각들이 조금 정리되는 느낌이었다.</strong></p>
<p>매일 뛰지는 못했지만 끝까지 놓지 않았다는 것만으로도 다행이라고 생각한다.</p>
<h2 id="공부와-자격증">공부와 자격증</h2>
<p>결론은 빅데이터분석기사 실기시험을 또 미루고 말았다.</p>
<p>작년 하반기에는 SKCT를 준비한다는 이유로 미뤘는데, 또 미루게 될 줄은 몰랐다. 처음에는 스스로에게 실망하기도 했다.</p>
<p>하지만 지금 돌아보면 때가 아님을 인정하고 잠시 미뤄두는 것이 오히려 정신 건강에는 더 좋았다.</p>
<p>책도 많이 읽지 못했다. 대출 공부도 하겠다고 해놓고 많이 미뤘다. 눈앞의 일이 급급했다.</p>
<p>그럼에도 매일 출근길에 경제 기사와 부동산 관련 글을 읽었고, 월가 모닝브리핑도 꾸준히 들었다. 지금은 티가 나지 않더라도 이런 작은 습관들이 결국 자양분이 될 것이라고 믿는다.</p>
<h2 id="인간관계">인간관계</h2>
<p>원래 사람을 많이 만나는 편은 아니다. 운동하고 일하고 공부하고 나면 에너지가 거의 남지 않는다.</p>
<p>그런데 사람들을 조금 더 자주 만났던 시기가 있었다. 그때 느낀 건 좋은 사람들과 삶의 이야기를 나누는 것이 생각보다 큰 힘이 된다는 것이었다.</p>
<p>(그리고 내가 퇴사를 여러 번 외치며 불만을 쏟아낼 때 묵묵히 들어준 친구에게 정말 고맙고 미안하다. 사실 더 좋은 이야기를 나누었으면 좋았을 것 같다. 불만을 말한다고해서 해결되는 문제는 아니었다.)</p>
<p>반면 나를 지치게 만드는 관계도 있었다. 미련을 버리고 나니 오히려 후련했고 다시 내 루틴을 찾을 수 있었다.</p>
<p>좋은 관계는 에너지를 채워주고, 좋지 않은 관계는 생각보다 많은 에너지를 가져간다는 것을 배웠다.</p>
<h2 id="상반기에-포기한-것">상반기에 포기한 것</h2>
<h3 id="모든-사람에게-잘-보이려는-마음">모든 사람에게 잘 보이려는 마음</h3>
<p>입사 초반에는 대리님들에게도, 사원분들에게도 정말 많은 질문을 했다.</p>
<p>내가 들어간 개발팀에서는 직급보다는 소위말하는 “짬“이 더 중요했다. 질문을 너무 많이해서 이해도가 낮다는 이야기를 들은 적도 있다.</p>
<p>그동안 어디를 가도 1인분은 한다는 이야기를 듣고 살았는데, 그런 말을 들으니 내가 너무 바보 같기도 했다.</p>
<p>그래도 괜찮다.</p>
<blockquote>
<p>지금은 바보 대리를 하겠다.</p>
</blockquote>
<p>대신 1년 후에는 바보 대리가 되지 않기 위한 과정이라고 생각하겠다.</p>
<h3 id="완벽하게-하려는-강박">완벽하게 하려는 강박</h3>
<p>개발과제를 완벽하게 해내고 싶었다. 그러다 보니 주말에도 계속 일 생각을 했고, 잘하고 싶다는 마음이 오히려 부담으로 다가왔다.</p>
<p>개발자의 타이틀을 가진 지 4개월밖에 되지 않았는데 뭐가 그렇게 급했을까. </p>
<p>나에 대한 기대가 컸던 만큼 실망도 컸다.</p>
<h3 id="조급한-비교">조급한 비교</h3>
<p>&quot;저 사람은 사원인데 나보다 더 빨리 배우는 것 같다.&quot;</p>
<p>&quot;나는 1년 차가 되어도 저 정도는 못할 것 같다.&quot;</p>
<p>아직 오지도 않은 미래의 나를 스스로 낮게 평가하고 있었다.</p>
<p>나조차 나를 낮게 평가한다면 누가 나를 좋게 보겠는가.</p>
<h2 id="상반기에-배운-것">상반기에 배운 것</h2>
<ul>
<li>꾸준함이 재능보다 강하다.</li>
<li>체력이 곧 멘탈이다.</li>
<li>내 속도대로 가도 괜찮다.</li>
<li>방향성을 잃지 않는 것이 중요하다.</li>
</ul>
<p>나는 처음 배우는 과정에서 학습 속도가 빠른 사람은 아니다. 이해하고 내 것으로 만드는 데 시간이 오래 걸리는 편이다.</p>
<p>교관 시절에도 그랬다. 너무나도 당연하지만.. 교사 출신 선배님들은 수업 준비를 한두 시간 하면 되는 내용을 나는 몇 배의 시간을 들여 공부했다. </p>
<p><em>처음 가르쳤던 과목인 컴퓨터구조 교과서가 학생들 교과서보다 더 너덜너덜해지도록 봤던 기억이 있다.</em></p>
<p>대신 한 번 이해하고 나면 자신 있게 설명할 수 있을 때까지 반복했다.</p>
<p>지금도 크게 다르지 않은 것 같다.</p>
<p>회사는 내가 빨리 1인분이 되기를 바라겠지만 내가 더 조급해질수록 그 시기는 오히려 늦어질 수 있다.</p>
<p>내 속도를 유지하자. 그리고 방향성을 잃지 말자.</p>
<p>지금 회사에서 열심히 일하는 것도 중요하지만, 내가 어떤 개발자가 되고싶은지 끊임없이 고민하는 것도 중요하다.</p>
<h2 id="마무리하며">마무리하며</h2>
<p>2026년 상반기는 생각보다 훨씬 힘들었다. 개발자로서의 첫 적응기였고, 수없이 퇴사를 고민했다. 계획했던 것들을 여러 번 미루기도 했다. </p>
<p>그래도 포기하지는 않았다. 지금 생각해보면 실패한 상반기라기보다 우선순위를 다시 정했던 상반기에 가까운 것 같다. </p>
<p>모든 것을 다 가져가려고 했지만 현실은 그렇지 않았다.</p>
<p>무엇을 먼저 해야 하는지, 무엇을 잠시 내려놓아야 하는지 배웠다. 하반기에는 더 빨리 가기보다 꾸준히 가고 싶다.</p>
<p>그리고 2026년 12월의 내가 이 글을 다시 읽었을 때</p>
<blockquote>
<p>&quot;그래도 잘 버텼네.&quot;</p>
</blockquote>
<p>라고 웃으면서 말할 수 있었으면 좋겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LCEL로 RAG 파이프라인 구축하기]]></title>
            <link>https://velog.io/@dob-by/LCEL%EB%A1%9C-RAG-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dob-by/LCEL%EB%A1%9C-RAG-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 20 Sep 2025 09:09:01 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@dob-by/LCEL">지난 글</a>에서 LCEL이 뭔지, 그리고 왜 중요한지 개념적으로 정리해보았습니다. 간단히 복습하면 LCEL은 위 그림처럼 Runnable 단위를 쌓아올려서 최종적으로 체인이나 파이프라인을 만드는 구조입니다.</p>
<p>이번 포스팅에서는 이 구조를 실제 코드로 구현해보며 RAG 파이프라인 안에서 검색, 채팅, 메모리, 비동기 실행까지 직접 확인해보겠습니다.</p>
<h1 id="실습-목표">실습 목표</h1>
<p>이번 실습의 목표는 LCEL의 다양한 실행 방식과 블록들을 직접 체험하는 것입니다. </p>
<p>구체적으로는 다음을 확인해볼 것입니다.</p>
<blockquote>
<p><strong>1. 검색 모드</strong>: 벡터DB에서 관련 문서 찾기
<strong>2. 채팅 모드(Streaming)</strong>: 답변을 토큰 단위로 스트리밍 출력
<strong>3. 메모리 적용</strong>: RunnableWithMessageHistory를 활용하여 대화 맥락 유지
<strong>4. 배치 처리</strong>: 여러 질문을 한 번에 처리하는 batch/abatch 실행
<strong>5. 비동기 실행</strong>: ainvoke/astream으로 동시에 요청 처리</p>
</blockquote>
<p>정리하면 <span style="background-color:#FFE6E6"><strong>&quot;검색 → 답변 → 맥락 유지 → 병렬/비동기 처리라는 흐름을 하나의 파이프라인에서 확인하는 것&quot;</strong>이 핵심 목표</span>입니다.</p>
<h1 id="코드-구조-요약">코드 구조 요약</h1>
<p>이번 실습 코드는 기능별로 모듈을 나누어 구성했습니다.</p>
<blockquote>
<p>lcel_pipeline/
├── config.py                <span style="color:green"><small># 설정 및 상수</span></small>
├── models.py                <span style="color:green"><small># 데이터 모델(KeywordSection 등)</small></span>
├── document_processor.py    <span style="color:green"><small># 문서 로드 &amp; 섹션/청크 분리</small></span>
├── vector_store_manager.py  <span style="color:green"><small># 벡터스토어 구축 및 로드</small></span>
├── rag_chain_builder.py     <span style="color:green"><small># LCEL 기반 RAG 체인</small></span>
├── chat_history_manager.py  <span style="color:green"><small># 대화 히스토리 관리 (RunnableWithMessageHistory)</span></small>
├── main.py                  <span style="color:green"><small># RAGApplication (검색/채팅/비동기/배치 실행)</span></small>
│
├── run.py                   <span style="color:green"><small># 기본 실행 스크립트</small></span>
├── interactive_chat.py      <span style="color:green"><small># CLI 대화형 실행</small></span>
│
├── finance-keywords.txt     <span style="color:green"><small># 금융 키워드 데이터</small></span>
├── nlp-keywords.txt         <span style="color:green"><small># NLP 키워드 데이터</small></span>
└── faiss_keywords/          <span style="color:green"><small># 실행 시 생성되는 벡터스토어 디렉토리</small></span></p>
</blockquote>
<p><strong>1. 핵심 모듈</strong></p>
<ul>
<li>config.py: 모델명, 경로, 검색 파라미터 등 공통 설정</li>
<li>models.py: 데이터 모델 정의 (KeywordSection 등)</li>
<li>document_processor.py: 문서 로드 및 섹션/청크 분리</li>
<li>vector_store_manager.py: 임베딩 생성 &amp; 벡터스토어(FAISS) 관리</li>
<li>rag_chain_builder.py: LCEL 기반 RAG 체인 (Branch, Memory, Stream 포함)</li>
<li>chat_history_manager.py: 대화 히스토리 관리 (RunnableWithMessageHistory)</li>
<li>main.py: RAGApplication 클래스 → 검색, 채팅, 비동기, 배치 실행 총괄</li>
</ul>
<p><strong>2. 실행 스크립트</strong></p>
<ul>
<li>run.py: 기본 실행 스크립트</li>
<li>interactive_chat.py: CLI 기반 대화형 실행 (검색/채팅 모드 선택)</li>
</ul>
<p><strong>3. 데이터 파일</strong></p>
<ul>
<li>finance-keywords.txt: 금융 키워드 데이터</li>
<li>nlp-keywords.txt: NLP 키워드 데이터</li>
<li>faiss_keywords/: 실행 시 자동 생성되는 벡터스토어 디렉토리</li>
<li>index.faiss / index.pkl</li>
</ul>
<h1 id="데이터-플로우">데이터 플로우</h1>
<p><img src="https://velog.velcdn.com/images/dob-by/post/10f0dfc5-9f0f-4fc6-8252-ddb26e0ca257/image.png" alt=""></p>
<h1 id="파이프라인-구축">파이프라인 구축</h1>
<h2 id="앱-초기화">앱 초기화</h2>
<p>아래 함수 하나로 <strong><span style="background-color:#FFE6E6">문서 로드 → 청크 분할 → 벡터스토어 구축(FAISS) → retriever 생성 → RAG체인 연결 → 메모리</span>까지 한 번에 준비</strong>됩니다.</p>
<p>즉, <strong>질문을 던지면 바로 검색과 답변이 가능하도록 RAG 파이프라인 전체를 세팅하는 단계</strong>입니다.</p>
<pre><code class="language-python">def initialize(self) -&gt; None:
    print(&quot;문서를 로드하는 중...&quot;)
    docs = DocumentProcessor.load_documents(FILES)
    if not docs:
        raise ValueError(&quot;문서를 찾지 못했거나 섹션이 비어 있습니다.&quot;)

    print(&quot;문서를 청크로 분할하는 중...&quot;)
    split_docs = DocumentProcessor.split_long_docs(docs)

    print(&quot;벡터 스토어를 구축하는 중...&quot;)
    vs = VectorStoreManager.build_and_save_vectorstore(split_docs)
    self.retriever = vs.as_retriever(search_kwargs={&quot;k&quot;: DEFAULT_RETRIEVER_K})

    print(&quot;RAG 체인을 구축하는 중...&quot;)
    rag_chain = RAGChainBuilder.make_rag_chain(self.retriever)

    self.chain_with_history = RunnableWithMessageHistory(
        rag_chain,
        self.history_manager.get_session_history,
        input_messages_key=&quot;query&quot;,
        history_messages_key=&quot;history&quot;,
    )
    print(&quot;초기화 완료!&quot;)</code></pre>
<h3 id="코드-상세-설명">코드 상세 설명</h3>
<p><strong>1. 문서 로드</strong></p>
<pre><code class="language-python">def initialize(self) -&gt; None:
    print(&quot;문서를 로드하는 중...&quot;)
    docs = DocumentProcessor.load_documents(FILES)
    if not docs:
        raise ValueError(&quot;문서를 찾지 못했거나 섹션이 비어 있습니다.&quot;)</code></pre>
<p>텍스트 파일(<code>finance-keywords.txt</code>, <code>nlp-keywords.txt</code>)을 읽어 Document객체로 변환합니다. 만약 파일이 비어있다면 에러를 발생시켜 초기화가 멈추게합니다.</p>
<p><strong>2. 청크 분할</strong></p>
<pre><code class="language-python">    print(&quot;문서를 청크로 분할하는 중...&quot;)
    split_docs = DocumentProcessor.split_long_docs(docs)</code></pre>
<p>이 단계에서는 문서를 그대로 쓰지 않고 길이가 너무 긴 섹션은 자동으로 잘라냅니다.
실제로는 <code>DocumentProcessor.split_long_docs()</code> 안에서 <code>RecursiveCharacterTextSplitter</code>라는 유틸을 써서, 일정한 크기(chunk_size=800) 단위로 청크를 나누고, 겹치는 부분(chunk_overlap=120)도 남겨둡니다. 이렇게 해야 LLM이 문맥을 놓치지 않고 검색 정확도가 올라갑니다. </p>
<p>아래는 <code>split_long_docs</code> 함수를 정의한 부분입니다.</p>
<pre><code class="language-python"># document_processor.py
    @staticmethod
    def split_long_docs(docs: List[Document], chunk_size: int = DEFAULT_CHUNK_SIZE, 
                       chunk_overlap: int = DEFAULT_CHUNK_OVERLAP) -&gt; List[Document]:
        &quot;&quot;&quot;긴 문서들을 청크로 분할합니다.

        Args:
            docs: 분할할 Document 객체들의 리스트
            chunk_size: 청크 크기
            chunk_overlap: 청크 간 겹치는 부분의 크기

        Returns:
            분할된 Document 객체들의 리스트
        &quot;&quot;&quot;
        splitter = RecursiveCharacterTextSplitter(
            chunk_size=chunk_size,
            chunk_overlap=chunk_overlap,
            separators=TEXT_SPLITTER_SEPARATORS,
        )
        return splitter.split_documents(docs)</code></pre>
<p><strong>3. 벡터스토어 구축</strong></p>
<pre><code class="language-python">    print(&quot;벡터 스토어를 구축하는 중...&quot;)
    vs = VectorStoreManager.build_and_save_vectorstore(split_docs)
    self.retriever = vs.as_retriever(search_kwargs={&quot;k&quot;: DEFAULT_RETRIEVER_K})</code></pre>
<p>이 단계에서는 <strong>잘라낸 문서 청크들을 벡터(숫자 표현)로 변환</strong>한 후, 이를 <strong>FAISS라는 벡터 데이터베이스에 저장</strong>합니다. <span style="color:red">이 과정을 거쳐야 나중에 질문이 들어왔을 때, 관련성이 높은 문서들을 빠르게 찾아올 수 있습니다.</span></p>
<p>실제로는 <code>VectorStoreManager.build_and_save_vectorstore()</code>함수 안에서 다음 과정을 수행합니다.</p>
<pre><code class="language-python"># vector_store_manager.py
@staticmethod
def build_and_save_vectorstore(docs: List[Document], save_dir: Path = Path(&quot;./faiss_keywords&quot;)) -&gt; FAISS:
    embeddings = OpenAIEmbeddings(model=&quot;text-embedding-3-small&quot;)
    vectorstore = FAISS.from_documents(documents=docs, embedding=embeddings)
    save_dir.mkdir(parents=True, exist_ok=True)
    vectorstore.save_local(str(save_dir))
    return vectorstore</code></pre>
<ul>
<li><strong>임베딩</strong>: OpenAI의 &quot;text-embedding-3-small&quot;모델을 사용해 각 문서를 벡터로 변환합니다.</li>
<li><strong>FAISS 저장</strong>: 변환된 벡터들을 FAISS에 저장하고, 로컬 디렉토리(faiss_keywords/)에도 파일(index.faiss, indel.pkl)로 남깁니다.</li>
<li><strong>retriever 변환</strong>: 마지막으로 <code>vs.as_retriever(k=DEFAULT_RETRIEVER_K)</code>를 통해 검색 인터페이스로 바꿉니다. 이제 사용자가 질문을 던지면, retriever가 FAISS에서 관련 문서 k개를 뽑아 RAG 체인에 넘깁니다.</li>
</ul>
<p><strong>4. RAG 체인 구축</strong></p>
<pre><code class="language-python">    print(&quot;RAG 체인을 구축하는 중...&quot;)
    rag_chain = RAGChainBuilder.make_rag_chain(self.retriever)</code></pre>
<p>이 단계에서는 검색기(retriever)를 받아서 RAG 파이프라인을 구성합니다. <code>RAGChainBuilder.make_rag_chain()</code>내부에서는 <span style="background-color:#FFE6E6">프롬프트 템플릿을 정의하고, LLM(ChatOpenAI)을 연결한 뒤, 검색 결과(retriever)에서 가져온 문서를 프롬프트에 포함시킵니다.</span> 이렇게하면 <strong>&quot;질문 → 관련 문서 검색 → 문맥을 붙여서 답변 생성&quot;</strong>의 RAG 흐름이 완성됩니다.</p>
<p><strong>5. 메모리 적용</strong></p>
<pre><code class="language-python">    self.chain_with_history = RunnableWithMessageHistory(
        rag_chain,
        self.history_manager.get_session_history,
        input_messages_key=&quot;query&quot;,
        history_messages_key=&quot;history&quot;,
    )</code></pre>
<p>마지막 단계에서는 체인에 대화 히스토리(메모리)를 붙입니다.</p>
<ul>
<li><code>RunnableWithMessageHistory</code>로 감싸면, 동일한 세션(session_id) 안에서는 이전 대화 내용이 자동으로 프롬프트에 포함됩니다.</li>
<li><code>input_messages_key="query"</code> : 사용자가 입력한 질문</li>
<li><code>history_messages_key="history"</code> : 저장된 대화 히스토리</li>
</ul>
<h2 id="검색-모드">검색 모드</h2>
<p>LLM을 붙이기 전에, 검색이 제대로 되는지 먼저 확인하는 모드입니다. 이 단계에서는 키워드를 입력하면, 관련 문서 섹션의 출처(source)와 제목(section_title)만 출력됩니다.</p>
<p>RAG를 튜닝할 때 첫번째 체크포인트입니다. 검색이 잘 안될 경우 답변 품질이 떨어지기 때문에 반드시 확인해야합니다.</p>
<pre><code class="language-python">def run_search(self, query: str) -&gt; None:
    items = self.retriever.invoke(query)
    print(&quot;\n[Search Results]&quot;)
    for d in items:
        print(&quot;-&quot;, d.metadata.get(&quot;source&quot;), d.metadata.get(&quot;section_title&quot;))</code></pre>
<p><img src="https://velog.velcdn.com/images/dob-by/post/07d901ac-de60-4224-9268-0b4a3661ec00/image.png" alt=""></p>
<h2 id="채팅-모드동기-스트리밍">채팅 모드(동기 스트리밍)</h2>
<p>채팅모드에서는 <code>stream()</code>을 사용해 답변이 토큰 단위로 출력됩니다. 기다리지 않고 첫 토큰부터 바로 보여주기때문에 실시간 채팅 환경을 제공합니다. </p>
<pre><code class="language-python">def run_chat(self, query: str, session_id: str = DEFAULT_SESSION_ID) -&gt; None:
    print(&quot;\n[Answer Streaming]&quot;)
    print(&quot;=&quot; * 50)
    response_chunks = []
    for chunk in self.chain_with_history.stream(
        {&quot;query&quot;: query},
        config={&quot;configurable&quot;: {&quot;session_id&quot;: session_id}}
    ):
        print(chunk, end=&quot;&quot;, flush=True)
        response_chunks.append(chunk)
    print(&quot;\n&quot; + &quot;=&quot; * 50)
    if not response_chunks:
        print(&quot;❌ 응답을 생성할 수 없습니다.&quot;)
    else:
        print(f&quot;✅ 응답 완료! (총 {len(&#39;&#39;.join(response_chunks))} 문자)&quot;)</code></pre>
<p>예를 들어, &quot;딥러닝이 뭐야?&quot;라고 물으면 한 글자씩 쌓이면서 답변이 완성됩니다.</p>
<p><img src="https://velog.velcdn.com/images/dob-by/post/cbe0fae3-e5b2-4040-9069-4d340ac23d83/image.png" alt=""></p>
<h3 id="코드-추가-설명">코드 추가 설명</h3>
<p><strong>1. 함수 진입</strong></p>
<pre><code class="language-python">def run_chat(self, query: str, session_id: str = DEFAULT_SESSION_ID) -&gt; None:</code></pre>
<p>인자로 query(사용자 질문)와 session_id(대화 세션 ID)를 받습니다. 세션 ID는 <code>RunnableWithMessageHistory</code>와 연결되어 있어서, 같은 세션 안에서는 대화 히스토리가 이어집니다. (여기서 히스토리 관리는 chat_history_manager.py의 <code>get_session_history()</code>함수가 담당합니다.)</p>
<p><strong>2. 스트리밍 출력 준비</strong></p>
<pre><code class="language-python">response_chunks = []
for chunk in self.chain_with_history.stream(
    {&quot;query&quot;: query},
    config={&quot;configurable&quot;: {&quot;session_id&quot;: session_id}}
):</code></pre>
<p>핵심은 <code>self.chain_with_history.stream(...)</code>입니다. <code>self.chain_with_history</code>는 <code>initialize()</code> 단계에서 만들어지는데, 사실은 이렇게 구성돼 있어요.</p>
<pre><code class="language-python">self.chain_with_history = RunnableWithMessageHistory(
    rag_chain,  # ← rag_chain은 rag_chain_builder.py에서 생성
    self.history_manager.get_session_history,
    input_messages_key=&quot;query&quot;,
    history_messages_key=&quot;history&quot;,
)</code></pre>
<p>즉 <strong>self.chain_with_history = RAG 체인 + 메모리</strong>입니다.</p>
<ul>
<li>RAG 체인은 rag_chain_builder.py의 <code>make_rag_chain()</code>에서 생성</li>
<li>메모리는 chat_history_manager.py의 <code>get_session_history()</code>로 관리</li>
<li><code>stream()</code> 메서드는 답변을 한 번에 기다리지 않고, 토큰 단위로 잘라서 실시간으로 전달해줍니다.</li>
</ul>
<p><strong>3. 출력 처리</strong></p>
<pre><code class="language-python">print(chunk, end=&quot;&quot;, flush=True)
response_chunks.append(chunk)</code></pre>
<p>각 토큰(chunk)을 바로 출력(print)하면서, 동시에 리스트에 저장합니다. <code>flush=True</code>옵션 덕분에 터미널에 바로 찍히고, 버퍼에 쌓이지 않습니다. 이렇게 모아둔 <code>response_chunks</code>는 마지막에 답변 전체를 합칠 때 사용됩니다.</p>
<p><strong>4. 최종 결과 출력</strong></p>
<pre><code class="language-python">if not response_chunks:
    print(&quot;❌ 응답을 생성할 수 없습니다.&quot;)
else:
    print(f&quot;✅ 응답 완료! (총 {len(&#39;&#39;.join(response_chunks))} 문자)&quot;)</code></pre>
<p>만약 스트리밍 도중 아무 토큰도 못 받으면 에러 메시지를 출력합니다. 정상적으로 답변이 생성되면 토큰을 전부 합쳐서 길이(문자 수)를 계산해 보여줍니다.</p>
<h2 id="메모리-적용">메모리 적용</h2>
<p>(별도 함수는 없고, 위 run_chat/run_chatasync에서 <code>session_id</code>로 동작합니다.)</p>
<pre><code class="language-python">for chunk in self.chain_with_history.stream(
    {&quot;query&quot;: query},
    config={&quot;configurable&quot;: {&quot;session_id&quot;: session_id}}
):
    ...</code></pre>
<p>같은 세션(<code>session_id</code>)에서는 이전 대화를 기억합니다. 예를 들어, 먼저 &quot;딥러닝이 뭐야?&quot;라고 물은 뒤에 &quot;예시를 들어줘&quot;라고 하면 모델이 앞 대화의 맥락을 기억하고 이어서 대답합니다. 이것은 <code>RunnableWithMessageHistory</code>덕분에 가능한 기능입니다.</p>
<p><code>session_id</code>는 사실상 “방 번호” 같은 역할을 합니다. 같은 방에서는 이전 대화가 그대로 이어지고, 새로운 <code>session_id</code>로 실행하면 완전히 새로운 대화 세션이 시작됩니다.</p>
<p><img src="https://velog.velcdn.com/images/dob-by/post/34986b9a-7f3b-4d3f-865d-98bd2bf5a132/image.png" alt=""></p>
<h2 id="배치-처리">배치 처리</h2>
<pre><code class="language-python">def run_batch(self, queries, session_id: str = DEFAULT_SESSION_ID):
    print(&quot;\n[Batch Processing]&quot;)
    results = self.chain_with_history.batch(
        [{&quot;query&quot;: q} for q in queries],
        config={&quot;configurable&quot;: {&quot;session_id&quot;: session_id}}
    )
    for idx, (q, res) in enumerate(zip(queries, results)):
        print(f&quot;\n[Query {idx+1}] {q}\n[Answer]&quot;)
        print(res)</code></pre>
<p><code>batch()</code>를 사용하면 여러 질문을 한 번에 처리할 수 있습니다. 예를 들어, [&quot;시장 변동성 정의&quot;, &quot;NLP 토큰화&quot;, &quot;딥러닝이 뭐야?&quot;]를 입력하면 각각의 답변이 순서대로 출력됩니다. 그리고 <span style="color:red">내부적으로 LCEL은 병렬 최적화를 지원</span>하기 때문에, 순차 실행보다 빠르게 결과를 얻을 수 있습니다. 출력도 [Query 1], [Query 2]처럼 깔끔하게 정리돼 나오기 때문에 <strong>부하 테스트나 대량 요청 시 매우 유용</strong>합니다.</p>
<p><strong>실행 방법</strong></p>
<pre><code class="language-bash"># ====== 1. 프로젝트 디렉토리 진입 ======
cd lcel_pipeline
python

# ====== 2. 파이썬 REPL에서 실행 ======
import asyncio
from main import RAGApplication

app = RAGApplication()
app.initialize()

# 비동기 실행
asyncio.run(app.run_chat_async(&quot;딥러닝이 뭐야?&quot;))</code></pre>
<p><img src="https://velog.velcdn.com/images/dob-by/post/efddc3ab-d505-4c24-95e7-c145f196dd86/image.png" alt=""></p>
<h2 id="비동기-스트리밍">비동기 스트리밍</h2>
<pre><code class="language-python">async def run_chat_async(self, query: str, session_id: str = DEFAULT_SESSION_ID) -&gt; str:
    print(f&quot;\n[Async Answer Streaming] 질문: {query}&quot;)
    print(&quot;=&quot; * 50)
    response = &quot;&quot;
    async for chunk in self.chain_with_history.astream(
        {&quot;query&quot;: query},
        config={&quot;configurable&quot;: {&quot;session_id&quot;: session_id}}
    ):
        print(chunk, end=&quot;&quot;, flush=True)
        response += chunk
    print(&quot;\n&quot; + &quot;=&quot; * 50)
    return response</code></pre>
<p><code>astream()</code>은 <code>stream()</code>의 비동기 버전입니다. 차이점은 <strong>“여러 요청을 동시에 처리할 수 있느냐”</strong>에 있습니다.</p>
<p>예를 들어, 동기 <code>stream()</code>은 한 사용자의 요청을 처리하는 동안 다른 요청은 대기해야 하지만, <code>astream()</code>은 동시에 여러 사용자의 질문을 받아 각각 독립적으로 스트리밍 답변을 보낼 수 있습니다.</p>
<p>서버 환경에서 동시 접속자가 많은 경우, 이 비동기 방식이 필수적입니다.</p>
<p><strong>실행 방법</strong></p>
<pre><code class="language-bash"># ====== 1. 프로젝트 디렉토리 진입 ======
cd lcel_pipeline
python

# ====== 2. 파이썬 REPL에서 실행 ======
from main import RAGApplication

app = RAGApplication()
app.initialize()

queries = [&quot;시장 변동성 정의&quot;, &quot;NLP 토큰화&quot;, &quot;딥러닝이 뭐야?&quot;]
app.run_batch(queries)</code></pre>
<p><img src="https://velog.velcdn.com/images/dob-by/post/5c3e4dbf-1989-48e8-8b90-d197a5bf6d96/image.png" alt=""></p>
<h1 id="마무리">마무리</h1>
<p>이번 실습에서는 <strong>LCEL의 다양한 실행 방식(stream, astream, batch, branch, memory)</strong>을 실제 코드에 적용해보면서 RAG 파이프라인을 확장해보았습니다.</p>
<p>검색만 검증할 수 있는 검증 모드, 토큰 단위로 답변을 보여주는 스트리밍, 맥락을 이어가는 메모리, 여러 질문을 동시에 처리하는 배치, 서버 환경에서 유용한 비동기 처리까지 동시에 확인할 수 있었습니다.</p>
<p>처음에는 복잡해보였지만, 결국 <strong>LCEL은 각 단계를 Runnable블록으로 나누고 파이프(|)로 연결한다라는 단순한 원리에 기반</strong>하는 것을 깨달았습니다.</p>
<h1 id="참고자료">참고자료</h1>
<p><a href="https://teddylee777.github.io/langchain/langchain-lcel/#google_vignette">LangChain Expression Language(LCEL) 원리 이해와 파이프라인 구축 가이드</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LCEL 쉽게 이해하기 (feat. 한강 라면)]]></title>
            <link>https://velog.io/@dob-by/LCEL</link>
            <guid>https://velog.io/@dob-by/LCEL</guid>
            <pubDate>Thu, 18 Sep 2025 01:58:59 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dob-by/post/fa8807cc-3ec6-45dc-80a3-6028ee0bcbc8/image.png" alt=""></p>
<p>여러분 혹시 한강라면 좋아하시나요? 저는 며칠 전에 한강에 갔다가 라면을 먹었는데, 대기 줄이 정말 길더라구요.</p>
<p>LCEL에 대해 공부하다가 이게 한강라면기계와 같은 것이 아닌가하는 생각이 들었습니다.(저도 왜 이런 생각을 했는지 모르겠네요😂 아마 배고파서 그런것 같아요.) 집에서 라면을 끓일 땐 물 계량, 불 세기, 면 넣는 타이밍까지 전부 직접 챙겨야 합니다. 그런데 한강 라면 기계는 그 과정을 다 자동화해주잖아요?</p>
<p>LCEL도 마치 그 한강 라면 기계와 비슷합니다.</p>
<p>우리가 <strong>&quot;무엇을 할지(What)&quot;만 선언하면 &quot;어떻게 할지(How)&quot;는 LangChain이 알아서 처리</strong>해줍니다. 덕분에 우리는 <strong>복잡한 세부 구현에 매달리지 않고, 큰 흐름만 설계</strong>하면 되는거죠.</p>
<p>이번 글에서는 위 특징을 가능하게해주는 핵심개념인 LCEL에 대해 정리해보겠습니다.</p>
<h1 id="lcellangchain-expression-language이란">LCEL(LangChain Expression Language)이란?</h1>
<p><a href="https://python.langchain.com/docs/concepts/lcel/?utm_source=chatgpt.com#benefits-of-lcel">공식문서</a>에는 LCEL에 대해 다음과같이 소개하고 있습니다.</p>
<blockquote>
<p><em>The LangChain Expression Language (LCEL) takes a declarative approach to building new Runnables from existing Runnables.
This means that you describe what should happen, rather than how it should happen, allowing LangChain to optimize the run-time execution of the chains.</em></p>
</blockquote>
<p>즉, LCEL은 <strong>기존 Runnable들을 조합해 새로운 체인을 선언적으로 표현하는 언어</strong>입니다.
중요한 점은 <span style="background-color:#FFE6E6">“무엇을 할지”를 기술하는 데 집중할 수 있고, 실행 과정의 최적화는 <strong><span style="color:red">LangChain이 자동으로 처리</strong></span>한다는 겁니다.</p>
<h1 id="lcel의-주요-특징">LCEL의 주요 특징</h1>
<ul>
<li><strong>모듈화(Modularity)</strong>: 모든 구성 요소가 Runnable 단위로 통일되어 있어 조합이 쉽습니다.</li>
<li><strong>일관된 실행 모델</strong>: 동기/비동기, 단일 입력/배치 입력, 스트리밍까지 모두 동일한 인터페이스로 실행 가능합니다.</li>
<li><strong>확장성(Extensibility)</strong>: 조건 분기, 사용자 정의 함수, 메모리 등 다양한 기능을 유연하게 추가할 수 있습니다.</li>
</ul>
<p>이 중에서 가장 핵심이 되는 키워드가 바로 <strong>*&quot;Runnable&quot;*</strong>입니다. <strong><span style="background-color:#FFE6E6">LCEL은 모든 요소를 Runnable단위로 <span style="color:red">추상화</span>하기 때문에, Runnable을 이해하는 것이 곧 LCEL을 이해하는 출발점</strong></span>이라고 할 수 있습니다.</p>
<h1 id="runnable">Runnable</h1>
<h2 id="개념">개념</h2>
<p>Runnable은 한마디로 <strong>입력을 받아 → 어떤 작업을 하고 → 출력을 내는 실행 단위</strong>입니다.
LCEL에서는 모든 구성 요소(프롬프트, LLM, 검색기, 변환기 등)가 Runnable로 표현됩니다.</p>
<h2 id="특징">특징</h2>
<p><strong>1. 입력 → 출력 구조</strong></p>
<blockquote>
<ul>
<li>&quot;무언가를 넣으면 결과가 나온다&quot;라는 점에서 함수와 유사합니다.</li>
</ul>
</blockquote>
<ul>
<li>ex) &quot;runnable&quot;를 넣으면 &quot;RUNNABLE&quot; 출력</li>
</ul>
<p><strong>2. 공통 실행 방식 제공</strong>
모든 Runnable은 동일한 메서드를 가집니다.</p>
<blockquote>
<ul>
<li>.invoke(input): 단일 입력 처리</li>
</ul>
</blockquote>
<ul>
<li>.batch([inputs]): 여러 입력 처리<ul>
<li>.stream(input): 토큰 단위 스트리밍</li>
<li>.ainvoke, .abatch, .astream: 비동기 버전</li>
</ul>
</li>
</ul>
<p><strong>3. 조합 가능</strong></p>
<blockquote>
<ul>
<li>Runnable들을 |(파이프)로 연결해서 체인(Chain)을 만듭니다.</li>
</ul>
</blockquote>
<ul>
<li>ex) 검색기 | 프롬프트 | LLM → RAG 체인</li>
</ul>
<p>LCEL의 특징 중 하나인 조합 기능을 보면 이런 의문이 들 수 있습니다.</p>
<p>*&quot;LCEL로 조립했을 때 뭐가 좋은거지?&quot;* 🤔 </p>
<p><span style="background-color:#FFE6E6">LCEL을 사용했을 때의 가장 큰 장점은 <strong>&quot;복잡한 기능을 단순하고 일관된 방법으로 제공한다&quot;</strong>라는 점입니다.</span> 병렬처리, 비동기, 스트리밍, 디버깅, 배포까지 모두 LCEL의 공통 인터페이스 안에서 쉽게 다룰 수 있다는 것이 장점입니다.</p>
<h1 id="주요-runnable-정리">주요 Runnable 정리</h1>
<p>이번에는 LCEL에서 자주 쓰이는 Runnable들을 정리해보겠습니다. 처음에는 &quot;이게 뭐야?&quot;싶었는데, 알고보면 되게 단순한 블록들입니다.</p>
<h3 id="runnablepassthrough">RunnablePassthrough</h3>
<p>가장 간단한 Runnable입니다. 입력을 그대로 통과시켜줍니다. 아무 일도 안 하지만, 디버깅이나 중간 데이터를 확인할 때 은근 유용합니다.</p>
<pre><code class="language-python">from langchain_core.runnables import RunnablePassthrough

passthrough = RunnablePassthrough()
print(passthrough.invoke(&quot;Hello&quot;))  
# 출력: &quot;Hello&quot;</code></pre>
<h3 id="runnablelambda">RunnableLambda</h3>
<p>python함수를 그대로 <code>Runnable</code>로 감싸는 기능입니다. 이를 통해 개발자는 자신만의 함수를 정의하고, 해당 함수를 <code>RunnableLambda</code>를 사용하여 실행할 수 있습니다.</p>
<pre><code class="language-python">from langchain_core.runnables import RunnableLambda

to_upper = RunnableLambda(lambda x: x.upper())
print(to_upper.invoke(&quot;hello&quot;))  
# 출력: &quot;HELLO&quot;</code></pre>
<p>예를 들어, 데이터 전처리, 계산, 또는 외부 API와의 상호작용과 같은 작업을 수행하는 함수를 정의하고 실행할 수 있습니다.</p>
<h3 id="runnablebranch">RunnableBranch</h3>
<p>조건에 따라 실행 경로를 분기하는 Runnable입니다. 예를 들어, 질문에 <code>"finance"</code>라는 단어가 있으면 금융 체인으로, &quot;nlp&quot;가 들어가 있으면 NLP 체인으로 보내는 식입니다.</p>
<pre><code class="language-python">from langchain_core.runnables import RunnableBranch, RunnableLambda

branch = RunnableBranch(
    (lambda x: &quot;finance&quot; in x, RunnableLambda(lambda x: &quot;금융 체인 실행&quot;)),
    (lambda x: &quot;nlp&quot; in x, RunnableLambda(lambda x: &quot;NLP 체인 실행&quot;)),
    RunnableLambda(lambda x: &quot;기본 체인 실행&quot;)  # fallback
)

print(branch.invoke(&quot;finance keyword&quot;))  </code></pre>
<h3 id="runnablewithmessagehistory">RunnableWithMessageHistory</h3>
<p>마지막은 대화형 시스템에서 정말 중요한 블록입니다. 이 <code>Runnable</code>을 사용하면 이전 대화 내용을 <strong>히스토리(history)</strong>로 저장해두고, 그 맥락을 이어받아 다음 대화를 할 수 있습니다.</p>
<p>예를 들어,</p>
<blockquote>
<p><em>사용자: &quot;노트북 추천 해줘.&quot;
모델: &quot;게임용이 필요한가요, 아니면 코딩을 하기 위해 필요한가요?&quot;
사용자: &quot;코딩용&quot;
모델: &quot;그럼 가볍고 키보드 타건감이 좋은 모델을 추천해드릴게요&quot;</em></p>
</blockquote>
<p>이렇게 앞에서 했던 대화를 기억하고 이어서 답변할 수 있는 것이 바로 <em>RunnableWithMessageHistory</em>덕분입니다.</p>
<h1 id="실행-방식">실행 방식</h1>
<p>Runnable은 기본적으로 <strong>입력 → 처리 → 출력 구조</strong>인데, 실행하는 방법이 다양합니다. 상황에 따라 적절히 골라 쓰면 됩니다.</p>
<p><strong>1. invoke</strong>
가장 기본적인 실행 방식입니다. 입력 하나를 동기적으로 처리합니다.</p>
<pre><code class="language-python">result = chain.invoke(&quot;딥러닝이 뭐야?&quot;)
print(result)</code></pre>
<p><strong>2. ainvoke</strong>
invoke의 비동기 버전입니다. asyncio같은 환경에서 동시에 여러 요청을 처리할 때 유용합니다.</p>
<pre><code class="language-python">import asyncio

result = await chain.ainvoke(&quot;딥러닝이 뭐야?&quot;)
print(result)</code></pre>
<p><strong>3. batch</strong>
여러 입력을 한꺼번에 넣고 병렬 처리합니다. 리스트를 넣으면 리스트 결과가 나옵니다.</p>
<pre><code class="language-python">queries = [&quot;딥러닝이 뭐야?&quot;, &quot;NLP가 뭐야?&quot;, &quot;금융 리스크란?&quot;]
results = chain.batch(queries)
print(results)</code></pre>
<p><strong>4. abatch</strong>
batch의 비동기 버전입니다. 비동기 서버 환경에서 동시에 수십 개 요청을 처리할 때 사용합니다.</p>
<pre><code class="language-python">queries = [&quot;딥러닝이 뭐야?&quot;, &quot;NLP가 뭐야?&quot;, &quot;금융 리스크란?&quot;]
results = await chain.abatch(queries)
print(results)</code></pre>
<p><strong>5. stream</strong>
체인 실행 결과를 토큰 단위로 스트리밍 출력합니다. 즉, 답변이 다 생성될 때까지 기다리지 않고, 하나씩 실시간으로 볼 수 있어요.</p>
<pre><code class="language-python">for chunk in chain.stream(&quot;딥러닝이 뭐야?&quot;):
    print(chunk, end=&quot;&quot;)</code></pre>
<p><strong>6. astream</strong>
stream의 비동기 버전입니다. 채팅 애플리케이션에서 토큰 단위로 실시간 출력할 때 자주 씁니다.</p>
<pre><code class="language-python">async for chunk in chain.astream(&quot;딥러닝이 뭐야?&quot;):
    print(chunk, end=&quot;&quot;)</code></pre>
<blockquote>
<p>•    invoke: 단일 입력 동기 실행
    •    ainvoke: 단일 입력 비동기 실행
    •    batch: 여러 입력 동기 실행
    •    abatch: 여러 입력 비동기 실행
    •    stream: 스트리밍 동기 실행
    •    astream: 스트리밍 비동기 실행
    👉 결국 상황에 따라 &quot;단일/여러 개&quot; + &quot;동기/비동기/스트리밍&quot; 조합으로 고르면 됩니다.</p>
</blockquote>
<h1 id="마무리">마무리</h1>
<p>오늘은 LCEL을 한강 라면 기계 비유로 풀어보고, Runnable의 개념부터 실행 방식, 그리고 주요 블록들까지 정리해봤습니다.</p>
<p>요약하자면..</p>
<ul>
<li>Runnable은 LCEL의 가장 작은 실행 단위(입력-&gt;처리-&gt;출력)</li>
<li>LCEL은 이 Runnable들을 레고 블록처럼 조합해서 체인을 만드는 언어</li>
<li>병렬 처리, 비동기, 스트리밍, 메모리 같은 기능을 쉽게 적용할 수 있다는 점이 강점</li>
</ul>
<p>👉 결국 LCEL을 쓰면 복잡한 구현을 단순한 흐름 설계로 바꿀 수 있다는 것이 핵심입니다.</p>
<p>다음 글에서는 실제로 LCEL을 활용해서 RAG 파이프라인을 직접 구축하는 실습을 다뤄볼 예정입니다. 실제 코드와 실행 결과를 보면서, 오늘 배운 내용이 어떻게 쓰이는지 확인해보는 시간을 가져봅시다.</p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://python.langchain.com/docs/concepts/lcel/?utm_source=chatgpt.com#benefits-of-lcel">LangChain 공식문서</a></li>
<li><a href="https://teddylee777.github.io/langchain/langchain-lcel/#google_vignette">LangChain Expression Language(LCEL) 원리 이해와 파이프라인 구축 가이드</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA] 즉시로딩과 지연로딩 (with N+1문제)]]></title>
            <link>https://velog.io/@dob-by/JPA-%EC%A7%80%EC%97%B0%EB%A1%9C%EB%94%A9%EA%B3%BC-%EC%A6%89%EC%8B%9C%EB%A1%9C%EB%94%A9-N1%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@dob-by/JPA-%EC%A7%80%EC%97%B0%EB%A1%9C%EB%94%A9%EA%B3%BC-%EC%A6%89%EC%8B%9C%EB%A1%9C%EB%94%A9-N1%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Wed, 27 Aug 2025 04:11:53 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dob-by/post/8b23898d-3707-4049-a463-eb6872da4396/image.png" alt=""></p>
<p>JPA에서 엔티티를 조회할 때 연관된 엔티티를 언제 불러올지는 중요한 주제입니다. 이를 결정하는 방식이 바로 <strong>지연 로딩</strong>과 <strong>즉시 로딩</strong>입니다. 이번 포스팅에서는 두 로딩 방식의 동작 차이를 코드 예제와 함께 살펴보고, 여기서 자주 발생하는 N+1문제까지 연결해서 정리해보겠습니다.</p>
<h1 id="1-지연-로딩-lazy를-사용하여-프록시로-조회">1. 지연 로딩 LAZY를 사용하여 프록시로 조회</h1>
<p>먼저 이번 예제에서 사용할 연관관계 구조를 그림으로 정리하면 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/dob-by/post/39142e45-f60a-4f10-8348-38c7487837e9/image.png" alt=""></p>
<p>이 구조를 코드로 매핑한 Member 엔티티는 다음과 같습니다.</p>
<h4 id="member엔티티-코드">Member엔티티 코드</h4>
<pre><code class="language-java">import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class Member extends BaseEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;MEMBER_ID&quot;)
    private Long id;

    @Column(name = &quot;USERNAME&quot;)
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private Team team;
}</code></pre>
<p>위 코드는 <code>fetch = FetchType.Lazy</code>로 설정한 경우입니다.</p>
<h4 id="실행-코드main">실행 코드(Main)</h4>
<pre><code class="language-java">package hellojpa;

import jakarta.persistence.*;
import org.hibernate.Hibernate;

import java.util.List;

public class JpaMain {

    public static void main(String[] args) {

        System.out.println(&quot;MARKER &gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&quot;);

        EntityManagerFactory emf = Persistence.createEntityManagerFactory(&quot;hello&quot;);
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin(); //트랜잭션 시작

        try {
            Team team = new Team();
            team.setName(&quot;teamA&quot;);
            em.persist(team);

            Member member1 = new Member();
            member1.setUsername(&quot;member1&quot;);
            member1.setTeam(team);

            em.persist(member1);

            em.flush();
            em.clear();

            Member m = em.find(Member.class, member1.getId());

            System.out.println(&quot;m = &quot; + m.getTeam().getClass());

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
        emf.close();
    }

}
</code></pre>
<p><img src="https://velog.velcdn.com/images/dob-by/post/e6921617-2b64-4d2c-89d6-514e6df663a3/image.png" alt=""></p>
<p>Member만 먼저 조회되고(<code>select * from MEMBER …</code>), Team은 실제 속성 접근 시 쿼리가 나가며 초기화됩니다.
즉, “Member 조회 시 Member만 조회 / Team은 프록시로 보관 → 사용하는 순간 DB 조회”가 지연 로딩의 핵심 동작입니다.</p>
<p>그럼 Team은 언제 실제로 불러올까요? 🤔</p>
<p>아래처럼 실행 코드를 조금 바꿔서 getName()을 호출해보면, 이 시점에 프록시가 초기화되며 쿼리가 실행되는 것을 확인할 수 있습니다.</p>
<pre><code class="language-java">try {
            ...

            System.out.println(&quot;m = &quot; + m.getTeam().getClass());

            System.out.println(&quot;====================&quot;);
            m.getTeam().getName(); //초기화 시점
            System.out.println(&quot;====================&quot;);

            tx.commit();
        }</code></pre>
<p>team의 어떤 속성을 사용하는(<strong>실제 team을 사용하는 시점</strong>. team을 가져오는 시점이 아닙니다.) 시점에 프록시 객체가 초기화되면서 db에서 Team을 가져옵니다.</p>
<p><img src="https://velog.velcdn.com/images/dob-by/post/90281cec-d6a7-412d-934d-f22f10e01087/image.png" alt=""></p>
<h1 id="2-즉시-로딩eager을-사용해서-함께-조회">2. 즉시 로딩(EAGER)을 사용해서 함께 조회</h1>
<p>Member와 Team을 자주 함께 사용하는 경우에는 즉시로딩(EAGER)를 사용해서 함께 조회할 수 있습니다.</p>
<pre><code class="language-java">    @ManyToOne(fetch = FetchType.EAGER) //지연로딩(LAZY: team을 proxy로 조회)
    @JoinColumn
    private Team team; //연관관계 주인</code></pre>
<p><img src="https://velog.velcdn.com/images/dob-by/post/d9a8b59d-df51-4b15-8dbb-8db989c291ca/image.png" alt=""></p>
<p>즉시 로딩을 사용하는 경우, <code>em.find()</code>를 호출하면 Member와 Team을 한 번에 직접 조회합니다.</p>
<p>따라서 연관 엔티티가 프록시가 아닌 실제 엔티티로 로딩됩니다.</p>
<p><img src="https://velog.velcdn.com/images/dob-by/post/a4a09a9b-2252-4655-87d7-fa7348d66e24/image.png" alt=""></p>
<p><code>m.getTeam().getClass()</code>를 확인해보면 class hellojpa.Team이 출력되며, <code>m.getTeam().getName()</code>을 호출하면 지연 없이 바로 teamA가 출력됩니다.</p>
<p>즉, member1을 조회하면 곧바로 team1 엔티티까지 함께 가져옵니다.</p>
<p>즉시 로딩을 사용하는 경우 JPA 구현체는 다음과 같이 동작할 수 있습니다.</p>
<ul>
<li>조인을 사용하여 SQL 한 번에 함께 조회합니다.</li>
<li>또는 두 테이블을 각각 조회하여 데이터를 채울 수 있습니다.</li>
</ul>
<p>대부분의 경우 JPA 구현체는 가능하다면 조인을 사용해서 SQL 한 번에 조회하려고 합니다.</p>
<h1 id="3-프록시와-즉시로딩-주의">3. 프록시와 즉시로딩 주의</h1>
<h3 id="1-실무에서는-가급적-지연-로딩만-사용해야-합니다">1) 실무에서는 가급적 지연 로딩만 사용해야 합니다.</h3>
<p>즉시 로딩을 적용하면 예상하지 못한 SQL이 발생할 수 있습니다.</p>
<p>예를 들어, 연관된 테이블이 10개라면, 단순히 <code>find()</code> 한 번으로도 10개의 테이블이 전부 조인되어 성능이 급격히 떨어질 수 있습니다.</p>
<h3 id="2-즉시-로딩은-jpql에서-n1-문제를-일으킵니다">2) 즉시 로딩은 JPQL에서 N+1 문제를 일으킵니다.</h3>
<p>즉,</p>
<ul>
<li>1번: select * from Member</li>
<li>N번: 각 Member의 Team을 가져오기 위한 select * from Team where TEAM_ID = ?</li>
</ul>
<p>결과적으로 총 1 + N번의 쿼리가 발생하는 것이 바로 N+1 문제입니다.</p>
<h4 id="예제-코드-member-1명-team-1개--eager-가정">예제 코드 (Member 1명, Team 1개 / EAGER 가정)</h4>
<pre><code class="language-java">public class JpaMain {

        ...
        try {
            Team team = new Team();
            team.setName(&quot;teamA&quot;);
            em.persist(team);

            Member member1 = new Member();
            member1.setUsername(&quot;member1&quot;);
            member1.setTeam(team);

            em.persist(member1);

            em.flush();
            em.clear();

            // JPQL: 루트 엔티티(Member)목록 조회
            List&lt;Member&gt; members = em.createQuery(&quot;select m from Member m&quot;, Member.class).getResultList();

            tx.commit();
        } 
        ...
    }

}
</code></pre>
<p><img src="https://velog.velcdn.com/images/dob-by/post/c88bf1dc-4c2a-4fd0-acc5-e3f848da8b18/image.png" alt=""></p>
<h4 id="로그-해석">로그 해석</h4>
<ol>
<li>쿼리 1실행 (Member 조회)<pre><code class="language-sql">select * from Member;</code></pre>
</li>
</ol>
<blockquote>
<p>JPQL select m from Member m 가 그대로 Member 테이블 조회로 변환됩니다.</p>
</blockquote>
<ol start="2">
<li>쿼리 2실행 (Team 조회)<pre><code class="language-sql">select * from Team where TEAM_ID = xxx; //xxx에는 각 Member가 가진 TEAM_ID값이 바인딩</code></pre>
<blockquote>
<p>Member 엔티티의 team 필드가 EAGER로 지정되어 있기 때문에, 조회된 Member의 연관 Team을 즉시 초기화하기 위해 추가 SQL이 실행됩니다.</p>
</blockquote>
</li>
</ol>
<p>즉, Member 1번 + Team 1번 = 총 2번의 쿼리가 실행됩니다.
(여기서 Team이 프록시가 아닌 실제 엔티티로 채워집니다.)</p>
<h4 id="요약">요약</h4>
<blockquote>
<ul>
<li>Member 엔티티 목록을 조회하는 JPQL 한 줄이 실제로는 Member 쿼리 1번 + Team 쿼리 N번을 발생시킵니다.</li>
</ul>
</blockquote>
<ul>
<li>이 때문에 <strong>총 1 + N번의 쿼리(N+1 문제)</strong>가 실행됩니다.</li>
<li>즉, em.createQuery 결과 리스트를 만들 때 이미 Member와 Team이 모두 채워져 있어야 하므로 JPA가 자동으로 Team까지 가져오는 쿼리를 날리는 것입니다.</li>
</ul>
<h4 id="lazy로-지정했을-경우">LAZY로 지정했을 경우</h4>
<p>같은 JPQL을 실행하더라도 연관관계가 LAZY이면 처음에는 <code>select * from Member</code> 한 번만 실행됩니다.
Team은 실제로 <code>getTeam().getName()</code> 같이 Team의 속성을 사용하는 순간에 별도 쿼리가 나가며 초기화됩니다.</p>
<p>즉, 실제로 접근하기 전까지는 쿼리가 나가지 않는다는 점이 핵심입니다.  </p>
<h3 id="3-n1문제-해결-방법">3) N+1문제 해결 방법</h3>
<blockquote>
</blockquote>
<ul>
<li><strong>모든 연관관계 기본은 지연 로딩(LAZY)로 설정</strong>
(<code>@ManyToOne</code>, <code>@OneToOne</code>은 기본이 EAGER → 꼭 <code>fetch = FetchType.LAZY</code>로 변경)</li>
<li><strong>필요할 때만 fetch join으로 가져오기</strong>
: Member는 LAZY지만, JPQL에서 join fetch를 사용했으므로 Member와 Team을 한 번에 조인해서 가져옵니다.<pre><code class="language-java"># JpaMain.java
List&lt;Member&gt; members = em.createQuery(
  &quot;select m from Member m join fetch m.team&quot;, Member.class)
  .getResultList();</code></pre>
<ul>
<li><strong>엔티티 그래프(EntityGraph)</strong>
: 애노테이션이나 동적 API를 통해 특정 시점에 연관 엔티티까지 함께 조회하도록 설정 가능</li>
<li><strong>배치사이즈 방법</strong>
: 여러 개의 연관 엔티티를 한 번의 IN 쿼리로 묶어서 가져오도록 최적화</li>
</ul>
</li>
</ul>
<h1 id="실무-권장-사항">실무 권장 사항</h1>
<ul>
<li>모든 연관관계는 기본적으로 LAZY로 설정하는 것을 추천합니다.</li>
<li>실제로 함께 조회가 꼭 필요한 상황에서는 fetch join 또는 엔티티 그래프를 활용하는 것이 좋습니다.</li>
<li>즉시 로딩(EAGER)은 사용하지 말 것 → 의도하지 않은 조인이나 N+1 문제가 발생할 수 있습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] 왜 굳이 DTO를 써야 할까? 유효성 검증까지 한번에 이해하기 (feat. 회원가입)]]></title>
            <link>https://velog.io/@dob-by/Spring-Boot-%EC%99%9C-%EA%B5%B3%EC%9D%B4-DTO%EB%A5%BC-%EC%8D%A8%EC%95%BC-%ED%95%A0%EA%B9%8C-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%A6%9D%EA%B9%8C%EC%A7%80-%ED%95%9C%EB%B2%88%EC%97%90-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-feat.-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85</link>
            <guid>https://velog.io/@dob-by/Spring-Boot-%EC%99%9C-%EA%B5%B3%EC%9D%B4-DTO%EB%A5%BC-%EC%8D%A8%EC%95%BC-%ED%95%A0%EA%B9%8C-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%A6%9D%EA%B9%8C%EC%A7%80-%ED%95%9C%EB%B2%88%EC%97%90-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-feat.-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85</guid>
            <pubDate>Wed, 18 Jun 2025 10:58:49 GMT</pubDate>
            <description><![CDATA[<p>Spring을 공부하다 보면 한 번쯤 듣게 되는 개념, <strong>DTO (Data Transfer Object)</strong>.</p>
<p>처음에는 &quot;뭔가 계층 간 데이터를 전달하는 용도인가보다~&quot; 하고 지나쳤지만, 실제로 회원가입 기능을 만들면서 <strong>DTO가 왜 필요한지, 어떻게 써야 하는지</strong> 체감하게 되었습니다.</p>
<p>이 글에서는 직접 회원가입 기능을 구현하면서 </p>
<blockquote>
<ul>
<li>DTO를 왜 따로 만드는것이 좋은지 </li>
</ul>
</blockquote>
<ul>
<li>유효성 검증 과정에서 어떤 문제가 발생했는지</li>
<li>그리고 어떻게 해결했는지  </li>
</ul>
<p>제가 겪은 과정을 기반으로 정리해보려고 합니다.</p>
<hr>
<h1 id="dto란">DTO란?</h1>
<p><strong>DTO (Data Transfer Object)</strong>는 계층 간 데이터 전달을 위해 사용하는 <strong>순수 데이터 전송 객체</strong>입니다. 웹 애플리케이션에서는 <strong>사용자의 입력값을 안전하게 받기 위한 전용 폼 객체</strong>로 자주 사용됩니다.</p>
<h2 id="🤔-왜-굳이-dto를-써야-할까">🤔 왜 굳이 DTO를 써야 할까?</h2>
<p>처음엔 이렇게 생각했습니다.</p>
<blockquote>
<p>&quot;User 엔티티를 그냥 써도 되지 않나? 굳이 SignupFormDto 같은 걸 왜 만들어야 하지?&quot;</p>
</blockquote>
<hr>
<h1 id="회원가입-기능을-만들며-겪은-실제-흐름">회원가입 기능을 만들며 겪은 실제 흐름</h1>
<p>처음엔 이렇게 <code>User</code> 엔티티를 그냥 컨트롤러에 받아서 처리했습니다.</p>
<pre><code class="language-java">@PostMapping(&quot;/signup&quot;)
public String signup(User user) {
    userRepository.save(user);
    return &quot;redirect:/login&quot;;
}</code></pre>
<p>간단해 보이지만 몇 가지 문제가 있었습니다.</p>
<p>❌ <strong>User 엔티티를 바로 받으면 생기는 문제</strong></p>
<table>
<thead>
<tr>
<th>문제</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>비즈니스 목적과 불일치</td>
<td>회원가입 시 필요한 필드만 받으면 되는데, 불필요한 필드까지 열려있음</td>
</tr>
<tr>
<td>보안상 위험</td>
<td>User 내부에 있는 민감한 필드까지 클라이언트에서 보낼 수 있음</td>
</tr>
<tr>
<td>유지보수 어려움</td>
<td>나중에 User 엔티티가 바뀌면 폼 로직이 다 깨짐</td>
</tr>
</tbody></table>
<br>

<h2 id="그래서-등장한-dto">그래서 등장한 DTO</h2>
<p>회원가입에 필요한 필드만 딱 담은 전용 객체 SignupFormDto를 만들었습니다.</p>
<pre><code class="language-java">@Data
public class SignupFormDto {

    @NotBlank(message = &quot;아이디는 필수입니다.&quot;)
    private String userId;

    @NotBlank(message = &quot;비밀번호는 필수입니다.&quot;)
    private String password;

    @NotBlank(message = &quot;이름은 필수입니다.&quot;)
    private String username;

    @Email(message = &quot;이메일 형식이 올바르지 않습니다.&quot;)
    @NotBlank(message = &quot;이메일 입력은 필수입니다.&quot;)
    private String email;

    @NotBlank(message = &quot;소속 선택은 필수입니다.&quot;)
    private String affiliation; //학생대, 교육대

    @NotBlank(message = &quot;중대 선택은 필수입니다.&quot;)
    private String unit; //1~3중대
}</code></pre>
<p>오직 회원가입 시 사용자가 입력한 정보만 받도록 설계했습니다.</p>
<h2 id="그런데-검증이-안된다">그런데 검증이 안된다?</h2>
<p><code>@NotBlank</code>, <code>@Email</code>을 다 붙였는데 값 안넣고 제출해도 그대로 통과되는 문제가 발생했습니다.</p>
<p>그 이유는 바로 <strong>@Valid와 BindingResult의 위치와 사용 여부</strong> 때문이었습니다.</p>
<h2 id="유효성-검증이-동작하려면">유효성 검증이 동작하려면</h2>
<p>컨트롤러는 다음처럼 구성되어야 합니다.</p>
<pre><code class="language-java">@PostMapping(&quot;/signup&quot;)
    public String processSignup(@Valid @ModelAttribute(&quot;signupForm&quot;) SignupFormDto form, BindingResult bindingResult, Model model) {

        if (bindingResult.hasErrors()) {
            return &quot;signup&quot;; // 유효성 에러 처리
        }

        try {
            signupService.registerUser(form);
            return &quot;redirect:/login?signupSuccess&quot;; //회원가입 성공 시 로그인 페이지로
        } catch (IllegalArgumentException e) {
            model.addAttribute(&quot;error&quot;, e.getMessage());
            return &quot;signup&quot;;
        }
    }</code></pre>
<table>
<thead>
<tr>
<th>요소</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>@Valid</td>
<td>DTO에 붙은 @NotBlank 등의 어노테이션을 <strong>실제로 작동시킴</strong></td>
</tr>
<tr>
<td>BindingResult</td>
<td><strong>유효성 검사 결과를 잡아주는</strong> 바구니 역할</td>
</tr>
</tbody></table>
<p>❗️ 순서 주의: <strong>반드시 @Valid → BindingResult 순서로 선언</strong></p>
<blockquote>
<p>처음엔 try-catch문이 있는데 왜 유효성 에러처리가 안되지? 라고 생각했는데 <strong>@Valid 유효성 검사는 컨트롤러 메서드 호출 전에 실행</strong>되기 때문이었습니다.</p>
</blockquote>
<blockquote>
<p><strong>BindingResult가 없으면?</strong>
: 검증 실패 시 Spring이 MethodArgumentNotValidException을 던지고 예외로 끝냅니다 → try-catch 안으로 못 들어옵니다.</p>
</blockquote>
<h2 id="폼-페이지-thymeleaf">폼 페이지 (Thymeleaf)</h2>
<pre><code class="language-html">&lt;form th:action=&quot;@{/signup}&quot; th:object=&quot;${signupForm}&quot; method=&quot;post&quot;&gt;
  &lt;input type=&quot;text&quot; th:field=&quot;*{userId}&quot; /&gt;
  &lt;small th:if=&quot;${#fields.hasErrors(&#39;userId&#39;)}&quot; th:errors=&quot;*{userId}&quot;&gt;&lt;/small&gt;

  &lt;input type=&quot;text&quot; th:field=&quot;*{email}&quot; /&gt;
  &lt;small th:if=&quot;${#fields.hasErrors(&#39;email&#39;)}&quot; th:errors=&quot;*{email}&quot;&gt;&lt;/small&gt;

  &lt;select th:field=&quot;*{affiliation}&quot;&gt;
    &lt;option value=&quot;&quot;&gt;📌 선택하세요&lt;/option&gt;
    &lt;option value=&quot;학생대&quot;&gt;학생대&lt;/option&gt;
    &lt;option value=&quot;교육대&quot;&gt;교육대&lt;/option&gt;
  &lt;/select&gt;
  &lt;small th:if=&quot;${#fields.hasErrors(&#39;affiliation&#39;)}&quot; th:errors=&quot;*{affiliation}&quot;&gt;&lt;/small&gt;
&lt;/form&gt;
</code></pre>
<hr>
<p>자 그럼 이렇게 DTO를 쓸 경우 어떤 점이 좋아질까요?</p>
<h2 id="dto-사용-시-장점">DTO 사용 시 장점</h2>
<table>
<thead>
<tr>
<th>장점</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>유효성 검증 최적화</td>
<td><code>@NotBlank</code>, <code>@Email</code>, <code>@Size</code> 등을 자유롭게 붙여 검증 가능</td>
</tr>
<tr>
<td>보안성 강화</td>
<td>민감한 필드를 사용자에게 노출하지 않음</td>
</tr>
<tr>
<td>필요한 필드만 사용</td>
<td>회원가입, 로그인, 비밀번호 변경 등 용도에 맞는 DTO 설계 가능</td>
</tr>
<tr>
<td>구조적 유연성</td>
<td>DTO는 컨트롤러~뷰 전용 객체라, Entity가 바뀌어도 독립적으로 유지 가능</td>
</tr>
</tbody></table>
<hr>
<h1 id="정리">정리</h1>
<ul>
<li>회원가입 같은 폼에서는 User 엔티티를 직접 사용하지 않는 것이 좋습니다.</li>
<li>User엔티티는 DB 저장용 객체이고, 사용자의 입력값은 DTO로 분리하는 것이 좋습니다.</li>
<li>유효성 검증은 DTO에 어노테이션으로 붙이고 컨트롤러에서는 @Valid, BindingResult로 처리해야 합니다.</li>
<li>Thymeleaf에서는 th:field, th:errors를 제대로 써야 오류 메시지 표시가 가능합니다.</li>
</ul>
<hr>
<h1 id="결론">결론</h1>
<p>DTO는 단순한 데이터 전달용 객체가 아닙니다. <strong>사용자 입력을 안전하게 받고, 검증하고, 분리해서 처리</strong>하기 위한 <strong>아키텍처적 보호막</strong>이자, <strong>유지보수성과 보안성을 높이는 필수 도구</strong>입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[기술 노트 / WingITs] Spring Boot MVC 아키텍처와 백엔드 구조 설계]]></title>
            <link>https://velog.io/@dob-by/%EA%B8%B0%EC%88%A0-%EB%85%B8%ED%8A%B8-WingITs-Spring-Boot-MVC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EC%99%80-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B5%AC%EC%A1%B0-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dob-by/%EA%B8%B0%EC%88%A0-%EB%85%B8%ED%8A%B8-WingITs-Spring-Boot-MVC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EC%99%80-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B5%AC%EC%A1%B0-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Tue, 17 Jun 2025 08:15:26 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dob-by/post/4c4e4cbc-65b4-4dfc-8fcc-d8e994bc7a34/image.png" alt=""></p>
<blockquote>
<p>📌 이 글은 WingITs의 기술 구조 정리입니다. 이 프로젝트가 어떻게 시작되었는지 궁금하시다면
👉 <a href="https://velog.io/@dob-by/series/WingITs">개발일지 WingITs #1~#6</a></p>
</blockquote>
<h2 id="프로젝트-개요">프로젝트 개요</h2>
<p>제가 개발한 노트북 관리 시스템 WingITs는 <strong>Spring Boot 기반의 MVC 아키텍처</strong>로 설계되었습니다.</p>
<ul>
<li><strong>Controller</strong>가 사용자 요청을 받고,  </li>
<li><strong>Service</strong>에서 비즈니스 로직을 처리하며,  </li>
<li><strong>Repository (JPA 기반)</strong>를 통해 MySQL과 연동됩니다.</li>
</ul>
<p>프론트엔드는 Thymeleaf를 사용하여 렌더링되며, 사용자 역할에 따라 <strong>학생</strong>과 <strong>관리자</strong> 기능을 명확하게 분리하였습니다.</p>
<p>인증과 보안은 <strong>Spring Security</strong>를 기반으로 구현했고, <strong>Google/Kakao OAuth2 로그인도 지원</strong>합니다. 특히 관리자 계정은 <strong>승인 절차를 거쳐야 로그인</strong>할 수 있도록 설정하여 보안을 강화했습니다.</p>
<p>운영 환경은 <strong>Docker</strong> 기반으로 구성되어 있으며, 개발 초기에는 H2 DB를 사용하다가 배포 단계에서 MySQL로 전환하였습니다. 또한 초기 데이터 세팅을 위해 Excel 업로드 기능도 구현했습니다 (<strong>Apache POI</strong>).</p>
<hr>
<h2 id="프레임워크-및-라이브러리별-역할-정리">프레임워크 및 라이브러리별 역할 정리</h2>
<table>
<thead>
<tr>
<th>분류</th>
<th>사용 기술</th>
<th>목적 / 기능</th>
</tr>
</thead>
<tbody><tr>
<td>백엔드 프레임워크</td>
<td>Spring Boot</td>
<td>전체 애플리케이션 구성 (MVC 구조 포함)</td>
</tr>
<tr>
<td>템플릿 엔진</td>
<td>Thymeleaf</td>
<td>서버사이드 렌더링 기반 HTML 화면 구성</td>
</tr>
<tr>
<td>인증 / 보안</td>
<td>Spring Security</td>
<td>로그인, 로그아웃, 권한 제어 (학생 / 관리자)</td>
</tr>
<tr>
<td>소셜 로그인</td>
<td>Spring Security OAuth2</td>
<td>Google/Kakao 계정 연동</td>
</tr>
<tr>
<td>비밀번호 암호화</td>
<td>BCrypt</td>
<td>비밀번호 해시 처리</td>
</tr>
<tr>
<td>DB 연동</td>
<td>Spring Data JPA</td>
<td>Entity 기반 CRUD 처리</td>
</tr>
<tr>
<td>ORM 구현체</td>
<td>Hibernate</td>
<td>SQL 없이 객체 지향적으로 DB 처리</td>
</tr>
<tr>
<td>운영 DB</td>
<td>MySQL</td>
<td>실 서비스 운영용 RDBMS</td>
</tr>
<tr>
<td>개발용 DB</td>
<td>H2</td>
<td>개발/테스트용 가벼운 DB</td>
</tr>
<tr>
<td>Excel 업로드</td>
<td>Apache POI</td>
<td>초기 데이터 입력용 <code>.xlsx</code> 처리</td>
</tr>
<tr>
<td>배포 환경</td>
<td>Docker</td>
<td>MySQL 컨테이너 실행, 환경 일관성 확보</td>
</tr>
<tr>
<td>글로벌 데이터 처리</td>
<td>@ControllerAdvice</td>
<td>로그인 사용자 정보 전역 전달 (Thymeleaf)</td>
</tr>
</tbody></table>
<hr>
<h2 id="각-기술을-선택한-이유">각 기술을 선택한 이유</h2>
<h3 id="spring-boot"><strong>Spring Boot</strong></h3>
<ul>
<li><code>spring-boot-starter-web</code> 하나만 추가해도 필요한 웹 기능이 자동 설정됨  </li>
<li>복잡한 설정 없이 빠르게 개발 시작 가능  </li>
<li>내장 톰캣 서버 덕분에 별도 WAS 설정 없이 실행 가능 → 개발 생산성 ↑</li>
</ul>
<hr>
<h3 id="thymeleaf"><strong>Thymeleaf</strong></h3>
<ul>
<li>Spring MVC와의 자연스러운 통합 덕분에 별도 설정 없이 사용 가능  </li>
<li>컨트롤러에서 전달한 데이터를 템플릿에서 직관적으로 활용 가능  </li>
<li>서버사이드 렌더링에 적합한 직관적 문법</li>
</ul>
<hr>
<h3 id="spring-security"><strong>Spring Security</strong></h3>
<ul>
<li>Spring 생태계에 최적화된 보안 프레임워크  </li>
<li>인증/인가 설정 및 커스터마이징이 유연함  </li>
<li>별도 연동 없이도 강력한 보안을 쉽게 구현 가능</li>
</ul>
<hr>
<h3 id="oauth2"><strong>OAuth2</strong></h3>
<ul>
<li>Google, Kakao 등 외부 서비스와 손쉽게 연동 가능  </li>
<li>사용자 비밀번호를 직접 다루지 않아 보안상 유리  </li>
<li>정립된 인증 플로우 덕분에 구현이 간편하고 유지보수도 쉬움</li>
</ul>
<hr>
<h3 id="bcrypt"><strong>BCrypt</strong></h3>
<ul>
<li>솔트 자동 추가, 키 스트레칭 등 보안성이 뛰어난 해시 알고리즘  </li>
<li>Spring Security와의 연동이 자연스러워 쉽게 적용 가능  </li>
<li>널리 사용되는 방식으로 신뢰성과 적용 편의성이 높음</li>
</ul>
<hr>
<h3 id="spring-data-jpa--hibernate"><strong>Spring Data JPA + Hibernate</strong></h3>
<ul>
<li>객체지향적으로 DB를 다룰 수 있어 코드가 직관적이고 재사용성 높음  </li>
<li>SQL을 직접 작성하지 않아도 되므로 반복적인 CRUD 로직이 줄어듦  </li>
<li>Repository 인터페이스만으로 기본 CRUD 자동 처리 가능</li>
</ul>
<blockquote>
<p><strong>객체지향적으로 DB를 다룰 수 있다?</strong>
: 데이터베이스의 테이블을 자바 객체로 매핑해서, 마치 자바 객체를 다루듯 데이터를 조작할 수 있다는 의미. 즉, 개발자는 SQL쿼리를 직접 작성하는 수고를 덜고, 자바의 클래스를 통해 데이터베이스 레코드를 다룰 수 있음. 
👉 코드가 직관적, 재사용성 증가</p>
</blockquote>
<blockquote>
<p><strong>반복적인 CRUD 로직을 줄일 수 있다?</strong>
: 기본적인 데이터 조작 작업들(ex. 저장, 조회, 수정, 삭제 등)이 메서드 호출만으로 가능해지기 때문에 반복적인 코드를 줄이고 생산성을 높일 수 있음.</p>
</blockquote>
<hr>
<h3 id="mysql--h2"><strong>MySQL &amp; H2</strong></h3>
<ul>
<li>개발 초기에는 H2로 빠르게 작업  </li>
<li>운영 환경에서는 안정적인 MySQL을 사용해 데이터 영속성과 확장성을 확보</li>
</ul>
<hr>
<h3 id="apache-poi"><strong>Apache POI</strong></h3>
<ul>
<li><code>.xlsx</code> 엑셀 데이터를 Java에서 쉽게 처리 가능  </li>
<li>초기 사용자/노트북 등록 등 관리자 기능 구현에 활용</li>
</ul>
<hr>
<h3 id="docker"><strong>Docker</strong></h3>
<ul>
<li>MySQL을 Docker로 컨테이너화해 개발/운영 환경을 통일  </li>
<li>환경 차이로 인한 문제 최소화  </li>
<li>배포 자동화 및 협업에도 유리</li>
</ul>
<br>

<h2 id="마무리하며">마무리하며</h2>
<p>이처럼 WingITs 프로젝트는 Spring Boot를 중심으로 보안, 인증, 데이터 연동, 배포 등 <strong>실제 서비스를 운영하기 위한 필수 요소들을 직접 설계하고 구현한 경험이 집약된 시스템</strong>입니다.</p>
<p>이후 포스트에서는 아래 주제들을 다룰 예정입니다:</p>
<ul>
<li>Spring Security + OAuth2 로그인 처리 흐름</li>
<li>ControllerAdvice를 활용한 사용자 정보 처리</li>
<li>JPA 연관관계 매핑 설계 및 이슈</li>
<li>Docker-Compose로 구성한 로컬 개발 환경</li>
</ul>
<hr>
<p>아키텍처를 설계할 때 이 글이 도움이 되었으면 좋겠습니다. 🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] WingITs #6 - 마무리와 회고, 그리고 다음을 위하여]]></title>
            <link>https://velog.io/@dob-by/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-WingITs-6-%EB%A7%88%EB%AC%B4%EB%A6%AC%EC%99%80-%ED%9A%8C%EA%B3%A0-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%8B%A4%EC%9D%8C%EC%9D%84-%EC%9C%84%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@dob-by/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-WingITs-6-%EB%A7%88%EB%AC%B4%EB%A6%AC%EC%99%80-%ED%9A%8C%EA%B3%A0-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%8B%A4%EC%9D%8C%EC%9D%84-%EC%9C%84%ED%95%98%EC%97%AC</guid>
            <pubDate>Sun, 15 Jun 2025 02:38:25 GMT</pubDate>
            <description><![CDATA[<h2 id="🎯-프로젝트를-마무리하며">🎯 프로젝트를 마무리하며</h2>
<p>2025년 3월 말, 군복무 중 직접 겪은 불편함을 해결하고자 시작한 <strong>노트북 수리 요청 시스템 WingITs</strong>는 기획부터 구현, 디자인, 배포 직전 단계까지 약 3개월 간의 개발 여정을 거쳐 완성되었습니다. 사실 전역 준비와 이사 준비로 예상했던 것보다 더 오래 걸렸던 것 같네요.</p>
<hr>
<h2 id="🛠-아직-배포하지-않은-이유">🛠 아직 배포하지 않은 이유</h2>
<p>현재까지의 개발 결과물은 완성도 측면에서 <strong>핵심 기능이 대부분 구현되어 실사용이 가능한 수준</strong>이지만, 아직 <strong>외부 배포는 진행하지 않았습니다.</strong> </p>
<p>그 이유는...</p>
<ul>
<li><strong>군부대 특성상 실제 학생/교직원 정보를 외부 서버에 올릴 수 없음</strong></li>
<li>이 시스템은 <strong>실제 부대 내에서 사용될 목적으로 직접 개발한 것</strong>이기 때문에,<br>→ <strong>정보보호 및 보안에 대한 내부 검토가 반드시 필요</strong></li>
<li>아직 <strong>보안 기능이 충분히 안전한지 검토가 완료되지 않음</strong></li>
<li>현재는 <strong>임의의 테스트 데이터</strong>를 활용해 기능을 검증하고 있으며, <strong>실제 학생 및 교직원 정보를 포함한 운영 환경 적용은</strong> <strong>군 부대의 보안 검토 절차를 모두 마친 후</strong>, <strong>공식적인 허가가 떨어지는 경우에 한해 외부 배포를 진행할 예정</strong></li>
</ul>
<hr>
<h2 id="☁️-배포-방식에-대한-고민">☁️ 배포 방식에 대한 고민</h2>
<p>원래는 <strong>Render를 통해 간편하게 배포</strong>하고, 테스트 사용자 피드백을 빠르게 받을 계획이었습니다.</p>
<p>하지만 이후 고려한 몇 가지 요소로 인해 <strong>EC2 + Docker 기반 클라우드 배포 방식</strong>으로 방향을 바꾸게 되었습니다:</p>
<ul>
<li><strong>보안 설정 및 서버 제어권 확보가 필요</strong></li>
<li>단순 호스팅보다 <strong>직접 인프라 구성 경험을 쌓고 싶었음</strong></li>
<li><strong>Docker, EC2, Nginx, HTTPS 인증서 등 기술적 역량 향상 목적</strong></li>
</ul>
<p>따라서 현재는 배포를 보류하고 있지만, <strong>보안 검토 및 허가 절차가 완료되면 EC2 기반으로 정식 배포를 진행할 계획</strong>입니다.</p>
<hr>
<h2 id="🔭-향후-추가-예정-기능-및-개선-사항">🔭 향후 추가 예정 기능 및 개선 사항</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>✅ 관리자별 통계 분리</td>
<td>현재는 전체 관리자 통계만 제공 → <strong>로그인한 관리자 기준으로 분리 예정</strong></td>
</tr>
<tr>
<td>🔐 비밀번호 변경 이력 기록</td>
<td>보안 강화를 위해 <strong>비밀번호 변경 시점 및 이력 추적</strong> 기능 추가</td>
</tr>
<tr>
<td>📊 노트북 상태 통계</td>
<td>전체 노트북의 상태(보유/수리중/반납 등)를 <strong>시각적으로 확인</strong></td>
</tr>
<tr>
<td>⛓️ 소유권 이력 자동화</td>
<td>수리 상태 변경 시 자동으로 <strong>소유권 이전 기록 반영</strong></td>
</tr>
<tr>
<td>🧪 통합 테스트 코드 작성</td>
<td><strong>단위/통합 테스트 작성</strong>을 통해 코드 신뢰성 향상 예정</td>
</tr>
<tr>
<td>🏁 AWS EC2 배포</td>
<td>Docker + Nginx + MySQL 구성으로 <strong>클라우드 배포 예정</strong></td>
</tr>
</tbody></table>
<hr>
<h2 id="💭-이-프로젝트가-나에게-남긴-것">💭 이 프로젝트가 나에게 남긴 것</h2>
<ul>
<li><strong>실제 문제 해결의 전 과정을 경험</strong>하며 개발자로서의 자신감 얻음  </li>
<li>Spring Security, OAuth2, Docker 등 <strong>다양한 기술 스택을 실전에 적용</strong>  </li>
<li><strong>기획 → 개발 → 테스트까지의 경험</strong></li>
</ul>
<hr>
<h2 id="📌-다음-글부터는">📌 다음 글부터는?</h2>
<p>이번 시리즈에서는 마무리 단계 이후, <strong>보안 검토를 통과하거나 실제 운영에 필요한 기능이 추가되는 경우</strong>를 중심으로 <strong>기능 개선 및 후속 개발기</strong>를 다룰 예정입니다.</p>
<blockquote>
<ul>
<li>관리자 전용 통계 기능 고도화  </li>
<li>수리 요청 소유권 변경 로직 완성  </li>
<li>실사용자 기반 피드백 반영 등</li>
</ul>
</blockquote>
<hr>
<p>※ 개발 중 겪었던 다양한 시행착오와 기술적 문제들은  별도의 시리즈, <strong>&quot;개발 이슈 로그&quot;</strong>에서 보다 상세히 다루고 있습니다. 🔧</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] WingITs #5 - 소통의 장, 공지사항 · 게시판 · FAQ]]></title>
            <link>https://velog.io/@dob-by/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-WingITs-5-%EC%86%8C%ED%86%B5%EC%9D%98-%EC%9E%A5-%EA%B3%B5%EC%A7%80%EC%82%AC%ED%95%AD-%EA%B2%8C%EC%8B%9C%ED%8C%90-FAQ</link>
            <guid>https://velog.io/@dob-by/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-WingITs-5-%EC%86%8C%ED%86%B5%EC%9D%98-%EC%9E%A5-%EA%B3%B5%EC%A7%80%EC%82%AC%ED%95%AD-%EA%B2%8C%EC%8B%9C%ED%8C%90-FAQ</guid>
            <pubDate>Sun, 15 Jun 2025 02:24:11 GMT</pubDate>
            <description><![CDATA[<p>노트북 수리 요청 시스템을 개발하면서 느낀 또 하나의 필요성은, <strong>학생과 관리자가 서로 정보를 주고받을 수 있는 &#39;소통 창구&#39;</strong>였습니다.</p>
<p>기존에는 학생 대상 공지를 대부분 <strong>카카오톡 단체방</strong>을 통해 전달했지만..</p>
<ul>
<li>모든 선생님이 공지를 올리다 보니 <strong>중요한 정보가 금방 묻혀버리고</strong></li>
<li>나중에 다시 확인하려 해도 <strong>검색이 어려운 문제</strong>가 자주 발생했습니다.</li>
</ul>
<p>이를 해결하기 위해, <strong>공지사항 · 게시판 · FAQ를 웹 시스템 내에 별도로 구현</strong>하고 역할별 접근 권한, 검색 기능, 댓글 기능 등을 통해 <strong>효율적인 커뮤니케이션 구조</strong>를 만들고자 했습니다.</p>
<hr>
<h2 id="📢-공지사항-기능">📢 공지사항 기능</h2>
<p>공지사항은 관리자만 등록할 수 있으며, <strong>페이징 / 검색 / 파일 첨부</strong> 기능을 지원합니다.</p>
<ul>
<li>목록에서 제목 클릭 시 상세 보기</li>
<li>첨부 파일이 있는 경우 다운로드 가능</li>
<li><strong>최근 공지사항 5개는 메인 페이지에 미리보기로 표시됨</strong></li>
<li>학생은 열람만 가능, 관리자는 작성 가능</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dob-by/post/70435f2a-716f-415f-b94a-111bff7d8f2e/image.gif" alt=""></p>
<hr>
<h2 id="💬-게시판--댓글-기능">💬 게시판 &amp; 댓글 기능</h2>
<p>게시판은 <strong>학생들이 자유롭게 글을 작성하고 소통할 수 있는 공간</strong>입니다.<br>각 게시글에는 댓글 기능이 포함되어 있어 간단한 피드백이나 공감 표현이 가능합니다.</p>
<ul>
<li><strong>학생</strong>: 게시글 작성, 수정, 삭제 가능</li>
<li><strong>관리자</strong>: 게시글 작성은 불가, 학생 글 삭제 가능</li>
<li>댓글은 역할 상관없이 누구나 작성 가능</li>
<li>본인 댓글은 본인만 수정/삭제 가능, 관리자는 모든 댓글 삭제 가능</li>
<li>조회수 증가 기능</li>
<li><strong>최근 게시글 5개는 메인 화면에 미리보기로 표시됨</strong>
<img src="https://velog.velcdn.com/images/dob-by/post/587f634a-dfba-4f8e-92d1-f65a3e20cd0b/image.gif" alt=""></li>
</ul>
<hr>
<h2 id="❓-자주-묻는-질문-faq">❓ 자주 묻는 질문 (FAQ)</h2>
<p>자주 묻는 질문은 <strong>Bootstrap 기반 아코디언 UI</strong>로 구현해 가독성을 높였습니다.</p>
<ul>
<li>관리자가 등록</li>
<li>질문 클릭 시 답변이 펼쳐지는 구조</li>
<li>나중에 카테고리 필터 기능 추가 예정</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dob-by/post/be85c95a-62ef-499e-9faf-4eae9a148b8f/image.gif" alt=""></p>
<hr>
<h2 id="🔐-역할-기반-접근-제한">🔐 역할 기반 접근 제한</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>학생 (<code>ROLE_STUDENT</code>)</th>
<th>관리자 (<code>ROLE_MID_ADMIN</code> 이상)</th>
</tr>
</thead>
<tbody><tr>
<td>공지사항 보기</td>
<td>O</td>
<td>O</td>
</tr>
<tr>
<td>공지사항 작성</td>
<td>X</td>
<td>O</td>
</tr>
<tr>
<td>게시판 글 작성</td>
<td>O</td>
<td>X (학생 글 삭제만 가능)</td>
</tr>
<tr>
<td>게시판 댓글 작성</td>
<td>O</td>
<td>O</td>
</tr>
<tr>
<td>게시판 댓글 삭제</td>
<td>본인만 가능</td>
<td>전체 삭제 가능</td>
</tr>
<tr>
<td>FAQ 열람</td>
<td>O</td>
<td>O</td>
</tr>
<tr>
<td>FAQ 등록</td>
<td>X</td>
<td><code>ROLE_TOP_ADMIN</code>만 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="⏭-다음-글에서는">⏭ 다음 글에서는...</h2>
<blockquote>
<p><strong>개발 마무리와 회고</strong>, 그리고 앞으로 추가할 기능들에 대한 계획을 공유하며 이 프로젝트의 여정을 정리할 예정입니다. 특히 <strong>배포를 아직 하지 않은 이유</strong>, 그리고 향후 도입 예정인 <strong>보안 기능</strong>과 <strong>개선 계획</strong>에 대해서도 함께 다룰 예정입니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] WingITs #4 - 수리 요청 시스템, 학생과 관리자의 연결 고리]]></title>
            <link>https://velog.io/@dob-by/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-WingITs-4-%EC%88%98%EB%A6%AC-%EC%9A%94%EC%B2%AD-%EC%8B%9C%EC%8A%A4%ED%85%9C-%ED%95%99%EC%83%9D%EA%B3%BC-%EA%B4%80%EB%A6%AC%EC%9E%90%EC%9D%98-%EC%97%B0%EA%B2%B0-%EA%B3%A0%EB%A6%AC</link>
            <guid>https://velog.io/@dob-by/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-WingITs-4-%EC%88%98%EB%A6%AC-%EC%9A%94%EC%B2%AD-%EC%8B%9C%EC%8A%A4%ED%85%9C-%ED%95%99%EC%83%9D%EA%B3%BC-%EA%B4%80%EB%A6%AC%EC%9E%90%EC%9D%98-%EC%97%B0%EA%B2%B0-%EA%B3%A0%EB%A6%AC</guid>
            <pubDate>Sun, 15 Jun 2025 02:05:54 GMT</pubDate>
            <description><![CDATA[<p>수리 요청은 이 시스템의 핵심 기능입니다.</p>
<p>학생이 노트북 수리 요청을 보내고, 관리자가 그 요청을 검토하여 상태를 업데이트하는 과정은 곧 이 시스템이 해결하고자 했던 가장 중요한 ‘불편함’이었습니다.</p>
<hr>
<h2 id="🧑🎓-학생-→-수리-요청">🧑‍🎓 학생 → 수리 요청</h2>
<p>학생은 로그인 후 <strong>마이페이지에서 본인의 노트북 정보</strong>를 확인할 수 있습니다. 
이 페이지에서 직접 수리 요청을 작성할 수 있도록 구성했습니다.</p>
<p><img src="https://velog.velcdn.com/images/dob-by/post/3a1ea411-9f30-48a3-95a9-72507ba93b9a/image.gif" alt=""></p>
<hr>
<h2 id="📋-요청-내역-확인">📋 요청 내역 확인</h2>
<p>요청한 내역은 <strong>요청 일자, 상태, 문제 요약</strong>으로 리스트업되며, 학생은 이를 통해 현재 수리 진행 상황을 실시간으로 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/dob-by/post/612ef464-225e-442e-a966-88b4bce51b75/image.png" alt=""></p>
<hr>
<h2 id="🛠-관리자-→-수리-요청-관리">🛠 관리자 → 수리 요청 관리</h2>
<p>관리자는 모든 학생의 요청을 <strong>목록 형태로 한눈에 볼 수 있으며</strong>, 검색, 필터링, 정렬이 가능합니다.</p>
<p><img src="https://velog.velcdn.com/images/dob-by/post/a10402b5-6f5e-4e8d-86c3-edd337bf89c8/image.gif" alt=""></p>
<p>각 요청을 클릭하면 상세 정보가 나오고, 여기서 관리자는 수리 상태를 다음중 하나로 변경할 수 있습니다.</p>
<blockquote>
<ul>
<li>수리 요청됨, 수리 중, 수리 완료, 반려</li>
</ul>
</blockquote>
<p>반려의 경우 관리자가 사유를 입력하여 학생이 반려 사유를 인지할 수 있도록합니다.</p>
<p>(중간 관리자의 경우 총괄 관리자와 다르게 승인과 반려 둘 중에 하나만 선택할 수 있습니다.)</p>
<hr>
<h2 id="🧭-상태-변경-로직-예정">🧭 상태 변경 로직 (예정)</h2>
<p>향후에는 수리 요청 상태에 따라 <strong>노트북 소유자가 자동으로 전환</strong>되는 로직을 도입할 예정입니다.</p>
<p>예를 들어,  </p>
<ul>
<li><strong>수리 요청됨 / 수리 중</strong> 상태일 때 → 소유자를 <strong>관리자</strong>로  </li>
<li><strong>수리 완료</strong> 시점 → 다시 <strong>학생에게 소유권 복구</strong></li>
</ul>
<p>이러한 흐름을 통해 실제 현장에서 자주 들었던  </p>
<blockquote>
<p>“누가 노트북을 가지고 있는지 모르겠어요…”  </p>
</blockquote>
<p>라는 문제를 <strong>기술적으로 해결</strong>하는 것을 목표로 하고 있습니다.</p>
<blockquote>
<p>⚙️ 현재는 해당 로직을 구현하기 위한 엔티티/서비스 구조를 고민 중이며, 추후 기능 안정화 이후 단계적으로 도입할 계획입니다.</p>
</blockquote>
<hr>
<h2 id="✅-관리자-마이페이지에서-확인-가능">✅ 관리자 마이페이지에서 확인 가능</h2>
<p>추가적으로, 관리자는 마이페이지에서 <strong>수리 요청 현황</strong>을 한눈에 파악할 수 있도록 구성된 통계 카드들을 통해 확인할 수 있습니다.  </p>
<p>요청 수, 처리 상태, 이번 주 접수 건수 등을 <strong>시각적으로 보여주어 업무 효율을 높였습니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/dob-by/post/4048124a-d319-4e95-b248-9358fbe5fbed/image.png" alt=""></p>
<blockquote>
<p>⚙️ 현재는 전체 수리 요청 기준의 통계만 제공되며, 추후에는 로그인한 관리자의 개인 처리 현황까지 구분하여 보여주는 기능도 추가할 예정입니다.</p>
</blockquote>
<hr>
<h2 id="📌-요약">📌 요약</h2>
<table>
<thead>
<tr>
<th>역할</th>
<th>가능 작업</th>
<th>권한 조건</th>
</tr>
</thead>
<tbody><tr>
<td>학생</td>
<td>수리 요청 작성 / 내역 확인</td>
<td><code>ROLE_STUDENT</code></td>
</tr>
<tr>
<td>관리자</td>
<td>모든 요청 조회 및 상태 변경</td>
<td><code>MID_ADMIN</code> 이상</td>
</tr>
</tbody></table>
<hr>
<h2 id="⏭-다음-글에서는">⏭ 다음 글에서는...</h2>
<blockquote>
<p>공지사항, 게시판, 그리고 FAQ까지 <strong>학생과 관리자 모두가 정보를 쉽게 주고받을 수 있도록 만든 커뮤니케이션 기능들</strong>에 대해 소개합니다. 특히 댓글 기능, 검색/페이징 처리, 역할 기반 접근제한 등의 구현 과정을 다룰 예정입니다!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] WingITs #3 - 하나의 로그인, 여러 개의 권한]]></title>
            <link>https://velog.io/@dob-by/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-WingITs-3-%ED%95%98%EB%82%98%EC%9D%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%91%90-%EA%B0%9C%EC%9D%98-%EA%B6%8C%ED%95%9C</link>
            <guid>https://velog.io/@dob-by/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-WingITs-3-%ED%95%98%EB%82%98%EC%9D%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%91%90-%EA%B0%9C%EC%9D%98-%EA%B6%8C%ED%95%9C</guid>
            <pubDate>Sat, 14 Jun 2025 15:28:59 GMT</pubDate>
            <description><![CDATA[<h2 id="🔐-로그인은-하나-권한은-둘">🔐 로그인은 하나, 권한은 둘</h2>
<p>이 프로젝트를 설계하면서 가장 고민이 많았던 부분 중 하나는 바로<br><strong>“학생과 관리자가 동일한 로그인 페이지를 통해 접근하되, 서로 다른 권한을 갖게 하자”</strong>는 요구사항이었습니다.</p>
<p>처음에는 간단하게 생각했습니다.<br>“그냥 Role만 구분하면 되겠지?”</p>
<p>하지만 구현 단계에서는 여러 갈래의 고민이 이어졌습니다.</p>
<ul>
<li>학생은 OAuth2만 허용하고, 관리자는 로컬 로그인만?</li>
<li>관리자도 OAuth2를 쓸 수 있게 하려면 <strong>추가 정보 입력</strong>을 어떻게 처리하지?</li>
<li>로그인 후 역할에 따라 라우팅을 나눌까, 통합할까?</li>
<li>로그인 폼을 아예 나눌까, 하나로 통합할까?</li>
</ul>
<p>결국 제가 내린 결론은 이랬습니다:</p>
<hr>
<h3 id="👩🎓-학생">👩‍🎓 학생</h3>
<ul>
<li><strong>로그인 방식</strong>: 군번(ID) + <code>군번+생년월일 6자리</code> (초기 비밀번호)</li>
<li><strong>사전 등록된 DB 정보</strong> 기반으로만 로그인 가능</li>
<li>최초 로그인 시 비밀번호 <strong>즉시 변경 필수</strong></li>
<li>자동으로 <code>ROLE_STUDENT</code> 부여</li>
<li><strong>학생 전용 기능만 접근 가능</strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/dob-by/post/174e530f-87b2-4d9a-9576-b1c886352553/image.gif" alt=""></p>
<hr>
<h3 id="👩💼-관리자">👩‍💼 관리자</h3>
<h4 id="✅-로컬-로그인">✅ 로컬 로그인</h4>
<ol>
<li>회원가입 시 이름, 군번, 이메일 등 기본 정보 입력</li>
<li><code>ROLE_PENDING_ADMIN</code> 상태로 DB 저장 (로그인 불가)</li>
<li>총괄 관리자(<code>ROLE_TOP_ADMIN</code>)의 <strong>승인 후</strong></li>
<li><code>ROLE_MID_ADMIN</code> 권한 부여 → 로그인 가능</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dob-by/post/d659ee05-ec64-4f77-b138-67766fd9c895/image.gif" alt=""></p>
<h4 id="✅-oauth2-로그인-google--kakao">✅ OAuth2 로그인 (Google / Kakao)</h4>
<ol>
<li>OAuth2 로그인 성공 시 <code>ROLE_TEMP</code>으로 임시 등록</li>
<li>즉시 <strong>추가 정보 입력 페이지로 리디렉션</strong></li>
<li>입력 완료 시 <code>ROLE_PENDING_ADMIN</code>으로 상태 전환</li>
<li>총괄 관리자 승인 후 <code>ROLE_MID_ADMIN</code> 으로 변경 → 로그인 가능</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dob-by/post/db11ccb8-4635-44da-8a19-86900c73e4b7/image.gif" alt=""></p>
<hr>
<h2 id="🗝️-관리자-권한-접근-정책">🗝️ 관리자 권한 접근 정책</h2>
<h3 id="✅-role_temp-role_pending_admin">✅ <code>ROLE_TEMP</code>, <code>ROLE_PENDING_ADMIN</code></h3>
<ul>
<li><strong>로그인 가능</strong></li>
<li>하지만 <code>/admin/**</code> 경로는 <strong>모두 접근 차단</strong></li>
<li>단, 예외적으로 아래 경로는 접근 허용:<ul>
<li><code>/admin/profile</code> (추가 정보 입력용 페이지)</li>
</ul>
</li>
</ul>
<blockquote>
<p>🔒 관리 기능은 <strong>사용 불가</strong></p>
</blockquote>
<hr>
<h3 id="✅-role_mid_admin-role_top_admin">✅ <code>ROLE_MID_ADMIN</code>, <code>ROLE_TOP_ADMIN</code></h3>
<ul>
<li><code>/admin/**</code> 경로 포함한 <strong>모든 관리자 기능 접근 가능</strong></li>
<li>권한 설명:<ul>
<li><code>ROLE_MID_ADMIN</code>: 일반 관리자</li>
<li><code>ROLE_TOP_ADMIN</code>: 총괄 관리자 (다른 관리자 승인 가능)</li>
</ul>
</li>
</ul>
<hr>
<h2 id="🧑✈️-총괄-관리자-role_top_admin">🧑‍✈️ 총괄 관리자 (<code>ROLE_TOP_ADMIN</code>)</h2>
<ul>
<li><code>ROLE_TOP_ADMIN</code>은 <strong>시스템 초기 등록 시 1명만 존재</strong></li>
<li>승인 대기 관리자 조회/승인 기능 (<code>/admin/pending-admins</code>)은 <strong>TOP_ADMIN만 접근 가능</strong></li>
<li>추후 <strong>인수인계를 위한 <code>TOP_ADMIN</code> 권한 이관 기능</strong> 별도 개발 예정</li>
</ul>
<hr>
<h2 id="⚙️-설계-흐름">⚙️ 설계 흐름</h2>
<pre><code>로그인 요청
├── 학생 로그인 (로컬)
│   └── DB에 저장된 사전 등록 정보 확인 → ROLE_STUDENT 부여
│
└── 관리자 로그인
    ├── 로컬 회원가입 → ROLE_TEMP
    │   └── ROLE_TOP_ADMIN 승인 → ROLE_MID_ADMIN
    │
    └── OAuth2 로그인 → ROLE_TEMP
        └── 추가 정보 입력 → ROLE_PENDING_ADMIN
            └── ROLE_TOP_ADMIN 승인 → ROLE_MID_ADMIN</code></pre><hr>
<h2 id="🔁-로그인-폼-하나로-통합할-것인가">🔁 로그인 폼, 하나로 통합할 것인가?</h2>
<p>이 프로젝트에서 고민했던 포인트 중 하나는<br><strong>&quot;로그인 폼을 기술적으로 통합하면서도, 사용자 경험을 구분할 수 있을까?&quot;</strong>였습니다.</p>
<p>처음에는 <code>/login</code> 하나로 통합된 폼만 제공했지만, 학생과 교직원이 혼동하지 않도록 <strong>UI 상에서만 탭으로 구분</strong>하는 방식으로 개선했습니다.</p>
<ul>
<li><strong>기술적으로는 여전히 하나의 엔드포인트(<code>/login</code>)</strong></li>
<li>입력받은 <code>userId(군번)</code>을 기반으로 이메일을 조회</li>
<li>Spring Security 내부에서 <strong>이메일 기반 인증</strong> 수행</li>
</ul>
<p>덕분에 로그인 플로우는 단순하게 유지하면서도, <strong>학생 / 관리자의 구분은 <code>Role</code> 기반으로 유연하게 처리</strong>할 수 있었습니다.</p>
<hr>
<h2 id="✅-다음-글에서는">✅ 다음 글에서는...</h2>
<blockquote>
<p>다음 글에서는 가장 핵심 기능인 <strong>학생과 관리자가 주고받는 수리 요청 프로세스</strong>를 어떻게 시스템으로 구현했는지 소개할 예정입니다.<br>특히 수리 이력 관리, 상태 변경 흐름, 권한에 따른 요청 처리 방식까지  <strong>실제 현장 문제를 어떻게 해결했는지</strong> 상세히 풀어보려고합니다!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] WingITs #2 - 기능은 어떻게 정했을까?]]></title>
            <link>https://velog.io/@dob-by/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-WingITs-2-%EA%B8%B0%EB%8A%A5%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%95%ED%96%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@dob-by/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-WingITs-2-%EA%B8%B0%EB%8A%A5%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%95%ED%96%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Sat, 14 Jun 2025 13:53:37 GMT</pubDate>
            <description><![CDATA[<p>개발을 결심한 건 좋았지만, 막상 만들려고 보니 머릿속이 하얘졌습니다.</p>
<blockquote>
<p>“그래서… 이걸 뭐부터 만들어야 하지?”</p>
</blockquote>
<h2 id="📌-기능-목록은-어디서-나왔을까">📌 기능 목록은 어디서 나왔을까?</h2>
<p>시작은 아주 단순했습니다.<br>실제 업무 중 겪은 <strong>수리 요청 절차의 불편함</strong>을 ‘어떻게 하면 시스템화할 수 있을까’에서 출발했기 때문이죠.</p>
<p>그래서 처음엔 그냥 <strong>내가 자주 들었던 말들</strong>을 적어봤습니다.</p>
<ul>
<li>&quot;선생님, 제 노트북 수리 요청하고 싶은데요.&quot;</li>
<li>&quot;담당자분 누구셨어요?&quot;</li>
<li>&quot;수리 맡긴지 오래됐는데 지금 어디 있는지 모르겠어요.&quot;</li>
<li>&quot;어떤 학생 노트북이 고장나서 다른 노트북으로 교환했는데 지금 누가 사용하고있는지 모르겠네요.&quot;</li>
</ul>
<p>이런 대화 속에서 필요한 기능들이 자연스럽게 떠올랐습니다.</p>
<h3 id="🎯-최소-기능-목록">🎯 최소 기능 목록</h3>
<ul>
<li><strong>학생</strong><ul>
<li>수리 요청 등록</li>
<li>내가 맡긴 수리 내역 확인</li>
</ul>
</li>
<li><strong>관리자</strong><ul>
<li>수리 요청 목록 보기</li>
<li>요청 상태 변경 (접수됨 / 수리 중 / 완료 등)</li>
<li>수리 이력 관리</li>
</ul>
</li>
</ul>
<p>우선은 이 정도만 있어도 “입력 → 처리 → 이력 확인”의 흐름이 가능하다고 생각했습니다.</p>
<hr>
<h2 id="🔁-기능을-정하면서-고민했던-점들">🔁 기능을 정하면서 고민했던 점들</h2>
<p>처음엔 기능이 아주 단순할 거라고 생각했지만, 실제로 만들다 보니 하나하나에서 고민할 게 정말 많았습니다.</p>
<h3 id="1-수리-요청은-누구나-할-수-있을까">1. 수리 요청은 누구나 할 수 있을까?</h3>
<p>→ 아니다. <strong>로그인한 학생만</strong> 가능해야 했고, <strong>중복 요청</strong>은 방지해야 했습니다.<br>→ 그래서 상태가 &#39;처리 중&#39;인 요청이 있을 땐, 새 요청을 막는 로직을 넣었습니다.</p>
<h3 id="2-관리자-권한은-어떻게-나눌까">2. 관리자 권한은 어떻게 나눌까?</h3>
<p>→ 그냥 다 admin이면 될 줄 알았는데, 관리자도 <strong>중간 관리자 / 최고 관리자</strong>로 나눠야 했습니다. 
(관리자 인원이 증가할수록 수동으로 사용자를 변경하는 기능을 모든 관리자가 할 수 있다면 잘못 설정하는 일이 발생할 수 있다고 생각이 들었습니다.)</p>
<h3 id="3-수리-요청-상태는-어떻게-설계할까">3. 수리 요청 상태는 어떻게 설계할까?</h3>
<p>→ &#39;요청됨 → 수리 중 → 수리 완료&#39;처럼 간단해보이지만, 학생들이 요청서를 잘못 적는 경우도 있기 때문에 실제로는 &#39;반려됨&#39;같은 예외 상태도 필요했습니다.</p>
<p>→ 그래서 <code>enum</code>으로 <code>RepairStatus</code>를 설계하고, 뷰에서도 한눈에 상태별 UI를 바꿔줄 수 있도록 처리했습니다.</p>
<hr>
<h2 id="🧠-흐름도와-erd의-시작">🧠 흐름도와 ERD의 시작</h2>
<p>처음에는 아주 단순한 흐름만 그렸습니다.</p>
<ul>
<li>수리 요청 흐름: 학생 → 요청 등록 → 관리자 확인 → 처리</li>
<li>화면 구성: 로그인 → 메인 → 내역 확인 (학생) / 전체 요청 관리 (관리자)</li>
</ul>
<p>ERD도 초안은 금방 그렸지만, 개발을 진행하면서 기능이 늘어나고 흐름이 바뀌며 구조도 계속 수정됐습니다.</p>
<p>그래서 이 글에서는 구체적인 ERD보다는, 어떤 기능을 어떤 흐름으로 구현했는지에 초점을 맞춰 정리해보려 합니다.</p>
<hr>
<h2 id="🤔-정리하며">🤔 정리하며</h2>
<p>‘기획’이라고 해서 거창한 게 필요한 건 아니었습니다. 오히려 <strong>내가 겪은 불편함을 적어보는 것</strong>부터 시작하니 그 안에서 자연스럽게 기능들이 나왔습니다.</p>
<p>무조건 많이 만들기보다는, <strong>실제로 필요한 흐름을 작동시킬 수 있는 최소한의 기능</strong>에서 부터 출발하는 게 정말 중요하다고 느꼈습니다.</p>
<hr>
<h2 id="다음-글에서는">다음 글에서는...</h2>
<blockquote>
<p>✅ 개발 중 가장 복잡했던 <strong>&quot;학생과 관리자, 로그인은 같지만 권한은 다르게&quot;</strong><br>인증 방식과 권한 분리, 그리고 로그인 흐름 설계에 대해 자세히 풀어보려 합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] WingITs #1 - 불편함에서 시작된 개발, 그리고 나만의 첫 서비스]]></title>
            <link>https://velog.io/@dob-by/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-WingITs-%EA%B0%9C%EB%B0%9C%EA%B8%B0-1-%EB%B6%88%ED%8E%B8%ED%95%A8%EC%97%90%EC%84%9C-%EC%8B%9C%EC%9E%91%EB%90%9C-%EA%B0%9C%EB%B0%9C-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%B2%AB-%EC%99%84%EC%84%B1%ED%98%95-%EC%84%9C%EB%B9%84%EC%8A%A4</link>
            <guid>https://velog.io/@dob-by/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-WingITs-%EA%B0%9C%EB%B0%9C%EA%B8%B0-1-%EB%B6%88%ED%8E%B8%ED%95%A8%EC%97%90%EC%84%9C-%EC%8B%9C%EC%9E%91%EB%90%9C-%EA%B0%9C%EB%B0%9C-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%B2%AB-%EC%99%84%EC%84%B1%ED%98%95-%EC%84%9C%EB%B9%84%EC%8A%A4</guid>
            <pubDate>Sat, 14 Jun 2025 13:31:14 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dob-by/post/d201a382-121d-4bb2-baf1-fa59165a7002/image.png" alt="시스템 도입 전, 구두 안내 장면"><br><sub>시스템 도입 전, 400여 명의 학생에게 수리 절차를 구두로 설명하던 당시 상황.<br>비효율적인 수기 방식의 문제를 절실히 느끼게 된 계기였습니다.</sub></p>
<h2 id="🙋♀️-왜-시작했을까">🙋‍♀️ 왜 시작했을까?</h2>
<p>장교로 군 복무 중, 항공과학고에서 전산 교관으로 근무하면서  저는 학교 내 노트북의 <strong>소프트웨어 관리를 담당</strong>하고 있었습니다.</p>
<p>이 시기, 학생들의 수리 요청은 대부분  고장 난 노트북을 <strong>직접 들고 와서 구두로 설명</strong>하거나, <strong>종이에 간단히 적어</strong> 전달하는 방식이었습니다.</p>
<p>요청이 많아질수록 내용을 체계적으로 기록하거나 수리 이력을 관리하기엔 너무나 비효율적인 구조였습니다.<br>(심지어 하드웨어 담당자와의 공간적 분리로 인해  <strong>수리 중 노트북이 분실되는</strong> 일도 있었습니다.)</p>
<blockquote>
<p>“선생님, 제 노트북이 갑자기 꺼져요. 그래서 수리를 맡겼는데 노트북을 받지 못했어요.”<br>“그때 담당자는 누구셨죠?”<br>“음… 잘 모르겠는데요…?”</p>
</blockquote>
<p>문제가 생겨도 이력을 추적할 수 없었고, 수리 상태를 공유하거나 누가 어떤 노트북을 갖고 있는지도 알기 어려웠습니다. 모든 것이 너무 <strong>아날로그적</strong>이었습니다.</p>
<p>그래서 생각했습니다.  </p>
<p><strong>“이걸 시스템으로 만들 수 있지 않을까?”</strong></p>
<hr>
<h2 id="💡-처음엔-아주-단순했다">💡 처음엔 아주 단순했다</h2>
<p>처음엔 그저 ‘수리 요청서 작성 기능’ 하나만 만들 생각이었습니다. 그런데 막상 시작하고 보니 생각보다 고려할 게 많았습니다.</p>
<ul>
<li>누가 노트북을 보유 중인지 어떻게 판단할까?  </li>
<li>수리 중일 땐 소유권이 누구에게 있을까?  </li>
<li>관리자와 학생의 권한을 어떻게 분리할까?  </li>
<li>OAuth2 로그인도 붙일 수 있을까?  </li>
<li>어차피 하는 김에 공지사항, 댓글, 마이페이지도?</li>
</ul>
<p>하나씩 기능이 붙어가며, 단순한 요청서 시스템에서 <strong>자산 및 수리 이력 관리 시스템</strong>으로 확장됐습니다. 기획부터 개발까지 모든 걸 직접 하면서  <strong>첫 웹 서비스</strong>를 완성했습니다.</p>
<hr>
<h2 id="🧱-기술보다-사용자-경험">🧱 기술보다 사용자 경험</h2>
<p>이번 프로젝트에선 멋진 기술보다  <strong>“실제로 쓰기 편한가?”</strong>에 더 집중했습니다.</p>
<ul>
<li>학생은 간단히 수리 요청을 등록할 수 있어야 했고  </li>
<li>관리자는 전체 요청 내역과 현재 수리 상태를 한눈에 볼 수 있어야 했다  </li>
<li>관리자 승인 흐름, 역할 구분, 로그인 방식까지 고려해야 했다  </li>
<li>군 환경에 맞춘 OAuth2 로그인과 관리자 승인 로직도 직접 설계했다</li>
</ul>
<p>그 과정에서 Spring Security, Docker, MySQL, Excel 데이터 마이그레이션까지<br>정말 다 해봤고… 그만큼 삽질도 많이 했습니다 😇</p>
<hr>
<h2 id="🏁-문제를-발견하고-직접-만든-첫-개발-프로젝트">🏁 문제를 발견하고, 직접 만든 첫 개발 프로젝트</h2>
<p>아직도 고칠 점은 많습니다. 하지만 이 프로젝트는 문제를 스스로 발견하고 → 기획하고 → 설계하고 → 개발한  <strong>나만의 첫 번째 서비스</strong>입니다.</p>
<p>그리고 무엇보다, 진짜 재밌었습니다.</p>
<hr>
<h2 id="💬-마무리">💬 마무리</h2>
<p>제가 만들고 싶은 건 “대단한 기술”이 아니라 <strong>“실용적이고 사용자에게 필요한 서비스”</strong>입니다.</p>
<p>WingITs는 그 첫걸음이었고, 앞으로 더 나은 개발자가 되기 위한 여정은 계속될 것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[WingITs / Spring Boot] H2 → MySQL 전환 시 서버 재시작마다 DB 날아간다면?]]></title>
            <link>https://velog.io/@dob-by/Spring-Boot-H2-MySQL-%EC%A0%84%ED%99%98-%EC%8B%9C-%EC%84%9C%EB%B2%84-%EC%9E%AC%EC%8B%9C%EC%9E%91%EB%A7%88%EB%8B%A4-DB-%EB%82%A0%EC%95%84%EA%B0%84%EB%8B%A4%EB%A9%B4</link>
            <guid>https://velog.io/@dob-by/Spring-Boot-H2-MySQL-%EC%A0%84%ED%99%98-%EC%8B%9C-%EC%84%9C%EB%B2%84-%EC%9E%AC%EC%8B%9C%EC%9E%91%EB%A7%88%EB%8B%A4-DB-%EB%82%A0%EC%95%84%EA%B0%84%EB%8B%A4%EB%A9%B4</guid>
            <pubDate>Mon, 09 Jun 2025 01:19:53 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>로컬 개발 환경에서 H2 인메모리 DB를 사용하다가 MySQL로 전환하면서 발생했던 문제들과 그 해결 과정을 정리합니다. Docker와 TablePlus를 활용했으며, <code>data.sql</code> 자동 실행 및 <code>application-test.yml</code> 오작동 등 다양한 이슈들을 직접 해결했습니다.</p>
</blockquote>
<h2 id="❗️문제-정의">❗️문제 정의</h2>
<p>H2에서 MySQL로 데이터베이스를 전환한 후 <strong>회원가입을 하게 되면 정상적으로 데이터가 저장되지만</strong>, 서버를 재시작하면 저장된 데이터가 사라지는 문제가 발생했습니다. </p>
<h3 id="문제-재현-절차">문제 재현 절차</h3>
<ol>
<li><p>IntelliJ에서 서버 실행</p>
</li>
<li><p>브라우저에서 회원가입 진행</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dob-by/post/9086c148-b866-4e38-9a3f-9d7a65e33526/image.png" alt=""></p>
<ol start="3">
<li><p>TablePlus에서 <code>users</code> 테이블 확인<br><img src="https://velog.velcdn.com/images/dob-by/post/a20c7858-a8cc-4176-9cd9-8812d066ac6c/image.png" alt=""><em>▲ 회원가입 직후 TablePlus에 저장된 사용자 정보 확인 화면</em></p>
</li>
<li><p>IntelliJ에서 서버 종료 (DemoApplication 수동 종료)</p>
</li>
<li><p>Docker 컨테이너 종료  </p>
<pre><code class="language-bash">docker stop wingits-mysql</code></pre>
</li>
<li><p>Docker 컨테이너 다시 시작  </p>
<pre><code class="language-bash">docker start wingits-mysql</code></pre>
</li>
<li><p>IntelliJ에서 서버 재실행</p>
</li>
<li><p>다시 TablePlus에서 <code>users</code> 테이블 확인<br><img src="https://velog.velcdn.com/images/dob-by/post/3c870656-d7af-465d-8b3b-b91cfaf83e85/image.png" alt=""><em>▲ 서버 재실행 후 회원 정보가 사라진 상태</em></p>
</li>
</ol>
<hr>
<h2 id="🔍-사실-수집">🔍 사실 수집</h2>
<ul>
<li><code>spring.jpa.hibernate.ddl-auto: update</code>로 설정되어 있음</li>
<li>회원가입은 성공했고, TablePlus에서도 데이터 확인됨</li>
<li><code>docker volume</code>도 정상적으로 연결되어 있었음</li>
<li><code>CommandLineRunner</code> 안에서 <code>userRepository.deleteAll()</code> 사용</li>
<li><code>data.sql</code> 파일이 존재함</li>
<li><code>application-test.yml</code>에서 mysql을 사용하도록 되어 있었다는 사실을 발견함</li>
</ul>
<hr>
<h2 id="💡-원인-추론">💡 원인 추론</h2>
<ol>
<li><code>data.sql</code> 자동 실행
→ Spring Boot는 서버 시작 시 data.sql이 존재하면 이를 실행함. 이로 인해 MySQL DB의 기존 데이터가 매번 초기화됨.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dob-by/post/4b6ad389-ac08-48f9-af79-9c5dfbb008a9/image.png" alt=""><em>▲ MySQL로 전환한 뒤에도 <code>data.sql</code> 파일이 남아 있는 상태</em></p>
<ol start="2">
<li><p>CommandLineRunner에서 <code>deleteAll()</code> 호출
→ 개발 중 테스트용 코드가 실서버에서도 동작하며 회원 데이터를 모두 삭제해 문제를 악화시킴.</p>
</li>
<li><p><code>application-test.yml</code> 설정이 제대로 분리되지 않음
→ 테스트 환경에서도 실제 MySQL을 참조하게 되어 의도치 않게 실DB에 영향을 줄 수 있는 구조였음.</p>
</li>
</ol>
<hr>
<h2 id="해결-방법-결정">해결 방법 결정</h2>
<ol>
<li><p>운영 환경에서 유저 데이터가 삭제되지 않도록
•    CommandLineRunner 내 userRepository.deleteAll() 코드 제거</p>
</li>
<li><p>Spring Boot의 자동 SQL 실행 방지
•    data.sql, schema.sql 파일 완전 삭제</p>
</li>
<li><p>테스트 환경과 운영 환경의 설정 명확히 분리
•    운영은 MySQL, 테스트는 H2를 사용
•    application.yml과 application-test.yml을 분리하고, 테스트에서는 아래와 같은 H2 설정만 유지</p>
</li>
</ol>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
    driver-class-name: org.h2.Driver
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
  h2:
    console:
      enabled: true</code></pre>
<ol start="4">
<li>도커 볼륨 확인 및 MySQL 영속성 확보
•    Docker Volume이 /var/lib/mysql에 제대로 마운트되어 있는지 확인 (docker inspect 명령 사용)
•    컨테이너 재시작 시 데이터가 유지되는지 테스트 완료</li>
</ol>
<hr>
<h2 id="결론-요약">결론 요약</h2>
<table>
<thead>
<tr>
<th>원인</th>
<th>설명</th>
<th>조치</th>
</tr>
</thead>
<tbody><tr>
<td><code>data.sql</code> 자동 실행</td>
<td>서버 시작 시 실행되어 DB 초기화</td>
<td><code>data.sql</code> 삭제</td>
</tr>
<tr>
<td><code>CommandLineRunner.deleteAll()</code></td>
<td>운영 환경에서도 모든 유저 삭제 실행됨</td>
<td>해당 코드 제거 또는 조건 분기</td>
</tr>
<tr>
<td><code>application-test.yml</code> 설정 오류</td>
<td>테스트 DB가 MySQL로 설정됨</td>
<td>H2로 수정하여 테스트 분리</td>
</tr>
<tr>
<td>Docker 볼륨 마운트 불확실</td>
<td>데이터 유지가 안 되는 의심</td>
<td><code>docker inspect</code>로 확인, 문제 없음</td>
</tr>
</tbody></table>
<hr>
<h2 id="실수를-줄이기-위한-팁">실수를 줄이기 위한 팁</h2>
<ul>
<li><code>data.sql</code>파일은 <strong>운영 환경에서 사용하지 않으면 삭제</strong>해두는 것이 좋습니다. (실수로 DB가 초기화될 수 있습니다.)</li>
<li><code>CommandLineRunner</code>에서 초기화 코드(<code>deleteAll()</code> 등)는 <strong>테스트 전용 조건 분기</strong> 또는 <strong>@Profile(&quot;test&quot;)</strong> 설정으로 운영 환경과 분리하는 것이 좋습니다.</li>
<li><code>application.yml</code>, <code>application-test.yml</code>의 설정을 명확히 구분하고, 테스트 환경에서는 반드시 <strong>H2 같은 인메모리 DB</strong>를 사용합시다.</li>
<li><code>docker inspect</code> 명령어로 <strong>볼륨 마운트 상태를 꼭 확인</strong>해두면, 데이터 손실 여부를 빠르게 파악할 수 있습니다.</li>
<li>중요한 설정(<code>ddl-auto</code>, datasource url 등)은 <strong>코드 리뷰 전 꼭 double-check</strong>!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ [WingITs / Spring Security] 인증은 됐는데 왜 역할은 ANONYMOUS?]]></title>
            <link>https://velog.io/@dob-by/%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%97%AD%ED%95%A0%EC%9D%B4-%EC%86%90%EB%8B%98ANONYMOUS%EC%9C%BC%EB%A1%9C-%EB%9C%A8%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EA%B8%B0</link>
            <guid>https://velog.io/@dob-by/%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%97%AD%ED%95%A0%EC%9D%B4-%EC%86%90%EB%8B%98ANONYMOUS%EC%9C%BC%EB%A1%9C-%EB%9C%A8%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EA%B8%B0</guid>
            <pubDate>Sat, 17 May 2025 15:43:55 GMT</pubDate>
            <description><![CDATA[<h1 id="프로젝트-소개">프로젝트 소개</h1>
<p>현재 학교 내 노트북 수리 요청 및 사용자 관리 시스템을 개발 중입니다.
Spring Boot와 Spring Security를 이용해 <strong>로컬 로그인과 카카오/구글 OAuth 로그인을 모두 지원</strong>합니다.</p>
<p>사용자 역할(Role)에 따라</p>
<ul>
<li><strong>일반 학생(STUDENT)</strong>은 게시글 작성 및 수리 요청 가능</li>
<li><strong>관리자(MID_ADMIN, TOP_ADMIN)</strong>는 사용자/노트북 관리 권한을 갖습니다.</li>
</ul>
<h1 id="문제-상황">문제 상황</h1>
<p>상단 메뉴바에 현재 로그인 중인 사용자 정보를 표시합니다. 이때 관리자 계정으로 로그인이 되어있기 때문에 TOP_ADMIN으로 표시되어야하나, 역할이 ANONYMOUS(손님)으로 잘못 표시되는 문제가 발생했습니다.</p>
<blockquote>
<p>👉 문제는 <strong>게시글 상세 페이지(<code>/posts/{id}</code>)에서만 발생</strong>했어요.
다른 페이지에서는 정상적으로 <strong>TOP_ADMIN | 관리자</strong>로 표시되었죠.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dob-by/post/906b8ec5-f60c-4a26-b1c0-df558c225e23/image.png" alt=""></p>
<h1 id="🔍-원인-분석">🔍 원인 분석</h1>
<p>원인을 파악하기 위해 게시글 상세 페이지(/posts/{postId})와 관련된 컨트롤러에서 <code>viewPost()</code>메서드를 분석했습니다.</p>
<pre><code class="language-java">// 게시글 상세
    @GetMapping(&quot;/{postId}&quot;)
    public String viewPost(@PathVariable String postId,
                           Model model,
                           HttpServletRequest request) {

        postService.incrementViewCount(postId);
        Post post = postService.getPost(postId);
        model.addAttribute(&quot;post&quot;, post);
        model.addAttribute(&quot;comments&quot;, commentService.getCommentsByPostId(postId));

        // 현재 로그인한 사용자 정보를 가져오기 위한 인증 객체 획득
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        // 인증된 사용자의 경우
        if (authentication != null &amp;&amp; authentication.isAuthenticated()) {
            Object principal = authentication.getPrincipal();

            if (principal instanceof CustomOAuth2User user) {
                model.addAttribute(&quot;currentUserEmail&quot;, user.getEmail());
                model.addAttribute(&quot;userRole&quot;, user.getRole().name());
            } else if (principal instanceof org.springframework.security.core.userdetails.User user) {
                // 로컬 유저의 경우 DB에서 이메일/역할 가져오기 (‼️문제 발생 의심 지점‼️)
                System.out.println(&quot;✅ 로컬사용자&quot;); //출력 ❌
                String email = user.getUsername(); // email이 username에 담김
                String role = postService.findRoleByEmail(email); // PostService에서 사용자 role 조회하는 메서드 만들기

                model.addAttribute(&quot;currentUserEmail&quot;, email);
                model.addAttribute(&quot;userRole&quot;, role);
            } else if (principal instanceof org.springframework.security.core.userdetails.User user) {
                String email = user.getUsername();
                String role = postService.findRoleByEmail(email);
                model.addAttribute(&quot;currentUserEmail&quot;, email);
                model.addAttribute(&quot;userRole&quot;, role);

            } else if (principal instanceof String email) {
                model.addAttribute(&quot;currentUserEmail&quot;, email);
                model.addAttribute(&quot;userRole&quot;, &quot;USER&quot;);

            } else {
                model.addAttribute(&quot;currentUserEmail&quot;, null);
                model.addAttribute(&quot;userRole&quot;, &quot;ANONYMOUS&quot;);
            }
        } else {
            model.addAttribute(&quot;currentUserEmail&quot;, null);
            model.addAttribute(&quot;userRole&quot;, &quot;ANONYMOUS&quot;);
        }

        // CSRF
        CsrfToken csrfToken = (CsrfToken) request.getAttribute(&quot;_csrf&quot;);
        model.addAttribute(&quot;_csrf&quot;, csrfToken);

        return &quot;post/view&quot;;
    }</code></pre>
<p>콘솔 출력 결과 <code>authentication != null && authentication.isAuthenticated()</code>조건은 통과하지만 <strong>✅ 로컬 사용자</strong>가 출력되지 않았습니다. <code>principal instanceof org.springframework.security.core.userdetails.User user</code>조건이 false가 되는 것이 원인이었죠.</p>
<p><code>SecurityContextHolder.getContext().getAuthentication().getPrincipal()</code>을 통해 로그인 정보를 가져오는데, principal 객체 타입이 잘못되어 발생한 문제였습니다.</p>
<table>
<thead>
<tr>
<th>로그인 방식</th>
<th>principal 타입</th>
</tr>
</thead>
<tbody><tr>
<td>OAuth 로그인(구글, 카카오)</td>
<td>CustomOAuth2User</td>
</tr>
<tr>
<td>로컬 로그인</td>
<td>CustomUserDetails</td>
</tr>
</tbody></table>
<p>👉 그런데 저는 컨트롤러에서 <span style="background-color:#FFE6E6">CustomOAuth2User만 체크하고, CustomUserDetails는 체크하지 않아
userRole = ANONYMOUS로 떨어졌던 겁니다.</span></p>
<h1 id="✅-해결-방법">✅ 해결 방법</h1>
<p>viewPost() 메서드에서 principal의 타입을 명확히 분기했습니다.</p>
<pre><code class="language-java">if (principal instanceof CustomOAuth2User user) {
    // OAuth 사용자 처리
    ...
} else if (principal instanceof CustomUserDetails userDetails) {
    // 로컬 로그인 사용자 처리
    ...
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dob-by/post/768c7309-a4bc-45cb-8bfb-34a9a477e6d1/image.png" alt=""></p>
<p>이로써 로컬 로그인 사용자도 userRole, username이 정확하게 나왔답니다!!👏👏👏</p>
<h1 id="배운-점">배운 점</h1>
<ul>
<li>Spring Security의 Authentication.getPrincipal()은 로그인 방식에 따라 타입이 다르다.</li>
<li>로그인 방식을 혼합해서 사용할 땐, @ControllerAdvice나 개별 컨트롤러에서 instanceof 분기 처리를 꼭 하자!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] WHERE과 HAVING의 차이 비교]]></title>
            <link>https://velog.io/@dob-by/SQL-WHERE%EA%B3%BC-HAVING</link>
            <guid>https://velog.io/@dob-by/SQL-WHERE%EA%B3%BC-HAVING</guid>
            <pubDate>Tue, 28 Jan 2025 05:49:53 GMT</pubDate>
            <description><![CDATA[<h1 id="select문의-실행-순서">SELECT문의 실행 순서</h1>
<p>WHERE과 HAVING의 차이를 비교하기 전에 우선 SQL쿼리의 실행 순서를 알아야합니다.</p>
<pre><code class="language-sql">SELECT ename
FROM emp
WHERE empno=10
GROUP BY ename
HAVING count(*)&gt;=1
ORDER BY ename;</code></pre>
<p>위 쿼리에서 SELECT문의 실행 순서는 <code>FROM > <span style="color:red">WHERE > <strong>GROUP BY</strong> > HAVING</span> > SELECT > ORDER BY</code>입니다.</p>
<h1 id="where과-having">WHERE과 HAVING</h1>
<h2 id="where">WHERE</h2>
<pre><code class="language-sql">SELECT * FROM 테이블 WHERE 조건절;</code></pre>
<pre><code>•    역할: 데이터를 그룹화(GROUP BY) 이전에, 개별 행(row) 수준에서 조건을 필터링.
•    적용 대상: 테이블의 개별 행.
•    사용 위치: FROM 절 바로 다음에 실행.
•    집계 함수(예: SUM, AVG)는 사용할 수 없음.</code></pre><p>WHERE은 기본적인 조건절로 항상 FROM 뒤에 위치하며 다양한 비교 연산자로 구체적인 조건을 줄 수 있습니다.</p>
<h2 id="having">HAVING</h2>
<pre><code class="language-sql">SELECT * FROM 테이블 GROUP BY 필드 HAVING 조건절;</code></pre>
<pre><code>•    역할: 데이터를 그룹화(GROUP BY) 이후에, 그룹 전체를 대상으로 조건을 필터링.
•    적용 대상: 그룹화된 데이터(aggregate data).
•    사용 위치: GROUP BY 절 뒤에 실행.
•    집계 함수(예: SUM, AVG)를 사용할 수 있음.</code></pre><p>항상 GROUP BY 뒤에 위치하며 WHERE과 마찬가지로 다양한 비교 연산자로 조건을 줄 수 있습니다.</p>
<h3 id="공통점과-차이점">공통점과 차이점</h3>
<p>둘 다 필드에 조건을 줄 수 있다는 것은 동일하지만, <span style="background-color:#FFE6E6">WHERE은 기본적으로 모든 필드에 조건을 줄 수 있지만 HAVING은 <strong>GROUP BY로 그룹화 된 필드에 조건을 줄 수 있습니다</strong>.</span> 또한, <strong>HAVING에서 조건을 줄 필드는 <span style="color:red">SELECT에 반드시 명시</strong></span>되어 있어야 합니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th align="left">WHERE</th>
<th align="left">HAVING</th>
</tr>
</thead>
<tbody><tr>
<td>적용 시점</td>
<td align="left"><strong>그룹화(GROUP BY) 이전</strong></td>
<td align="left"><strong>그룹화(GROUP BY) 이후</strong></td>
</tr>
<tr>
<td>적용 대상</td>
<td align="left">테이블의 <strong>개별 행(row)</strong></td>
<td align="left">그룹화된 <strong>집계 데이터</strong></td>
</tr>
<tr>
<td>집계 함수 사용 가능 여부</td>
<td align="left"><strong>불가능</strong></td>
<td align="left"><strong>가능</strong></td>
</tr>
<tr>
<td>사용 위치</td>
<td align="left">GROUP BY 이전</td>
<td align="left">GROUP BY 이후</td>
</tr>
<tr>
<td>필터링 기준</td>
<td align="left">개별 행을 기준으로 조건 검사</td>
<td align="left">그룹화된 데이터를 기준으로 조건 검사</td>
</tr>
</tbody></table>
<h1 id="적용-예제">적용 예제</h1>
<p>우선 직업별 급여 합계 중에 급여 합계가 1000이상인 직업을 조회하는 상황을 가정해봅시다.</p>
<h2 id="1-where-사용-span-stylecolorred오류-발생span">1. WHERE 사용( <span style="color:red">오류 발생</span>)</h2>
<pre><code class="language-sql">SELECT job, SUM(salary) AS total_salary
FROM employees
WHERE SUM(salary) &gt;= 1000 -- 오류 발생
GROUP BY job;</code></pre>
<p><strong>SUM(salary)는 그룹화 이후에 계산</strong>되므로, WHERE(그룹화 이전에 적용됨)에서 사용할 수 없습니다.</p>
<h2 id="2-having-사용올바른-사용">2. HAVING 사용(올바른 사용)</h2>
<pre><code class="language-sql">SELECT job, SUM(salary) AS total_salary
FROM employees
GROUP BY job
HAVING SUM(salary) &gt;= 1000;</code></pre>
<blockquote>
</blockquote>
<p><STRONG>쿼리 실행 흐름: </STRONG>
•    1단계: <code>FROM employees</code> → 직원 테이블 로드.
•    2단계: <code>GROUP BY job</code> → 직업별로 데이터를 그룹화.
•    3단계: <code>SUM(salary)</code> → 각 그룹의 급여 합계 계산.
•    4단계: <code>HAVING SUM(salary) >= 1000</code> → 급여 합계가 1000 이상인 그룹만 필터링.</p>
<p>GROUP BY는 데이터를 그룹 단위로 묶는 작업을 합니다. <span style="background-color:#FFE6E6">각 그룹에 대해 집계함수(SUM, AVG, COUNT, MAX, MIN 등)가 적용되며, 이 작업은 <strong>그룹화 후에 수행</strong>됩니다.</span></p>
<p>HAVING절은 GROUP BY 이후에 실행됩니다. 따라서, 이미 그룹화가 끝난 상태에서 SUM(salary) 값을 조건으로 사용할 수 있습니다.</p>
<p>HAVING은 그룹화된 결과에서 집계 결과를 필터링하므로 올바르게 작동합니다.</p>
<h1 id="요약">요약</h1>
<ul>
<li>SUM(salary)는 그룹화된 결과를 기반으로 계산되기 때문에, 그룹화 이전의 WHERE절에서는 사용할 수 없습니다.</li>
<li><span style="background-color:#FFE6E6">그룹화가 끝난 후 조건을 적용하려면, <span style="color:red">반드시 HAVING절을 사용</span>해야 합니다.</span></li>
<li>WHERE는 개별 행을 대상으로 작동하지만, HAVING은 그룹 전체를 대상으로 작동합니다.    <ul>
<li>WHERE: 개별 행 수준의 필터링. <strong>집계 함수 사용 불가.</strong></li>
<li>HAVING: 그룹화된 데이터의 필터링. <strong>집계 함수 사용 가능.</strong></li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<p>참고자료</p>
<ul>
<li><a href="https://amaranth1ne.tistory.com/52">https://amaranth1ne.tistory.com/52</a></li>
<li><a href="https://velog.io/@rocknzero/having-where%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A1%B0%EA%B1%B4%EC%A0%88-%EB%B9%84%EA%B5%90">https://velog.io/@rocknzero/having-where%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A1%B0%EA%B1%B4%EC%A0%88-%EB%B9%84%EA%B5%90</a> </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[python/백준] 2644. 촌수 계산(S2)]]></title>
            <link>https://velog.io/@dob-by/python%EB%B0%B1%EC%A4%80-2644.-%EC%B4%8C%EC%88%98-%EA%B3%84%EC%82%B0S2</link>
            <guid>https://velog.io/@dob-by/python%EB%B0%B1%EC%A4%80-2644.-%EC%B4%8C%EC%88%98-%EA%B3%84%EC%82%B0S2</guid>
            <pubDate>Fri, 30 Aug 2024 16:09:42 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-문제-탐색하기">📌 문제 탐색하기</h2>
<p>👉 <a href="https://www.acmicpc.net/problem/2644">문제바로가기</a></p>
<blockquote>
<p>n: 전체 사람의 수(1 ≤ n ≤ 100)
a, b: 촌수를 계산해야하는 서로 다른 두 사람의 번호
m: 부모 자식들간의 관계의 개수 m
x, y: 부모 자식간의 관계를 나타내는 두 번호(x는 y의 부모)</p>
</blockquote>
<p>부모 자식들 간의 관계가 주어졌을 때, 이를 그래프로 구축하고 두 사람의 촌수(간선의 개수)를 구하는 문제입니다. 두 사람의 친척 관계가 전혀 없어 촌수를 계산할 수 없는 경우 -1을 출력해야합니다.</p>
<p>주어진 예제를 예시로 들어보겠습니다.
<img src="https://velog.velcdn.com/images/dob-by/post/2326bb8b-8f44-4383-a607-f28ec0709316/image.png" alt=""></p>
<blockquote>
<ul>
<li>총 9명이 있습니다.</li>
</ul>
</blockquote>
<ul>
<li>우리가 구해야하는 것은 7번과 3번의 촌수입니다.</li>
<li>부모 자식들 간의 관계의 개수는 7개입니다.</li>
<li>1번은 2번의 부모입니다.</li>
<li>1번은 3번의 부모입니다.</li>
<li>2번은 7번의 부모입니다.</li>
<li>2번은 8번의 부모입니다.</li>
<li>2번은 9번의 부모입니다.</li>
<li>4번은 5번의 부모입니다.</li>
<li>4번은 6번의 부모입니다.</li>
</ul>
<p>위 정보를 그림으로 표현하면 아래와 같습니다.</p>
<img src="https://velog.velcdn.com/images/dob-by/post/a463b7dc-6150-4bab-9b2d-7fd5f5b86371/image.png" width="550" height="50"/>

<p><span style="background-color:#FFE6E6">주어진 부모 자식 관계를 통해 그래프를 구축</span>하고, 3에서 시작하여 7까지 가는 <span style="color:red">간선의 수</span>를 구하면 되는 문제입니다!</p>
<h3 id="1️⃣-입력-및-변수-정의">1️⃣ 입력 및 변수 정의</h3>
<pre><code class="language-python">n = int(sys.stdin.readline())
a, b = map(int, sys.stdin.readline().split())
m = int(sys.stdin.readline())</code></pre>
<h3 id="2️⃣-노드정보-저장-및-그래프-기록">2️⃣ 노드정보 저장 및 그래프 기록</h3>
<pre><code class="language-python">graph = [[] for _ in range(n+1)]  #그래프 리스트 초기화
visited = [False] * (n+1)    #방문 여부

for _ in range(m):
    x, y = map(int, sys.stdin.readline().split())   #부모 자식 관계정보
    graph[x].append(y)
    graph[y].append(x)</code></pre>
<h3 id="3️⃣-dfs함수-정의">3️⃣ DFS함수 정의</h3>
<p>우리가 구해야하는 것은 한 정점에서 다른 노드까지의 간선 개수입니다. 따라서 dfs의 매개변수인 v에는 인자를 a로하여 호출하면 됩니다. </p>
<pre><code class="language-python">def dfs(v):
    global count
    visited[v] = True
    if v == b:
        print(count)
        exit()
    for i in graph[v]:
        if not visited[i]:
            count += 1 
            dfs(i)
            count -= 1 #재귀 호출이 끝나면 깊이 감소

dfs(s)
print(-1)    </code></pre>
<p><code>dfs()</code>에서 <code>v == b</code>인 경우 <code>exit()</code>를 만나 코드가 종료된다는 것은 우리가 찾고자하는 b를 만나면 자동으로 종료된다는 의미입니다. <code>dfs()</code>에서 코드가 종료되지 못하면.. 즉, <span style="background-color:#FFE6E6">for문을 모두 반복해도 원하는 값을 만나지 못하면 <code>print(-1)</code>이 실행됩니다.</span></p>
<h2 id="📌-알고리즘-선택">📌 알고리즘 선택</h2>
<p><span style="background-color:#FFE6E6">한 점을 정점으로 선택하고 해당 점과 연결된 노드들을 모두 탐색</span>하면서 원하는 값을 찾아야하기 때문에 <span style="background-color:#FFE6E6">DFS를 활용</span>할 수 있겠네요.</p>
<h3 id="시간복잡도">시간복잡도</h3>
<p>dfs함수가 한 번 호출되면, 현재 노드에서 가능한 모든 인접한 노드를 재귀적으로 탐색합니다. 모든 노드와 간선을 한 번씩 방문하므로 시간복잡도는 <code>O(n+m)</code>입니다. n의 최대값은 1000이고, <span style="background-color:#FFE6E6">노드가 n개인 경우 부모-자식의 관계(간선의 수)는 n-1</span>이므로 최대 999개가 됩니다. 따라서 최악의 경우 연산은 약 1999회가 수행되므로 이는 수행 시간 내에 충분히 연산이 가능합니다.</p>
<hr>
<h2 id="📌-코드-설계하기">📌 코드 설계하기</h2>
<ol>
<li>전체 사람의 수(n), 촌수를 계산해야하는 서로 다른 두 사람의 번호, 부모 자식들간의 관계의 개수(m)을 Input받습니다.</li>
<li>부모 자식간의 관계를 나타내는 두 번호(x, y)를 입력받으면서 그래프를 구축합니다.</li>
<li>DFS를 수행하면서 간선의 개수를 카운트합니다.</li>
<li>원하는 값을 가진 노드를 찾으면 <code>count</code>를 출력하고, DFS를 모두 수행했음에도 원하는 값을 찾지 못하면 -1을 출력합니다.</li>
</ol>
<hr>
<h2 id="📌-정답-코드">📌 정답 코드</h2>
<pre><code class="language-python">import sys

count = 0

def dfs(v):
    global count
    visited[v] = True
    if v == b:
        print(count)
        exit()
    for i in graph[v]:
        if not visited[i]:
            count += 1 
            dfs(i)
            count -= 1 #재귀 호출이 끝나면 깊이 감소

n = int(sys.stdin.readline())
a, b = map(int, sys.stdin.readline().split())
m = int(sys.stdin.readline())

graph = [[] for _ in range(n+1)]  #그래프 리스트 초기화
visited = [False] * (n+1)

for _ in range(m):
    x, y = map(int, sys.stdin.readline().split())
    graph[x].append(y)
    graph[y].append(x)

dfs(a)
print(-1)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[python/백준] 11742. 연결 요소의 개수(S2)]]></title>
            <link>https://velog.io/@dob-by/python%EB%B0%B1%EC%A4%80-11742.-%EC%97%B0%EA%B2%B0-%EC%9A%94%EC%86%8C%EC%9D%98-%EA%B0%9C%EC%88%98S2</link>
            <guid>https://velog.io/@dob-by/python%EB%B0%B1%EC%A4%80-11742.-%EC%97%B0%EA%B2%B0-%EC%9A%94%EC%86%8C%EC%9D%98-%EA%B0%9C%EC%88%98S2</guid>
            <pubDate>Fri, 30 Aug 2024 14:56:51 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-문제-탐색하기">📌 문제 탐색하기</h2>
<p>👉 <a href="https://www.acmicpc.net/problem/11724">문제바로가기</a></p>
<blockquote>
<ul>
<li>N: 정점의 개수(1 ≤ N ≤ 1,000)</li>
</ul>
</blockquote>
<ul>
<li>M: 간선의 개수(0 ≤ M ≤ N×(N-1)/2)</li>
<li>u, v: 간선의 양 끝점(1 ≤ u, v ≤ N, u ≠ v) </li>
</ul>
<p>노드와 노드의 쌍이 주어진 경우 연결 요소의 개수를 구하는 문제입니다. </p>
<p>주어진 예제를 예시로 들어보겠습니다.
<img src="https://velog.velcdn.com/images/dob-by/post/5909ab79-80e0-4745-8be7-4434487bc862/image.png" alt=""></p>
<blockquote>
<ul>
<li>노드는 6개이며, 간선은 5개입니다.</li>
</ul>
</blockquote>
<ul>
<li>1과 2가 연결되어 있습니다.</li>
<li>2와 5가 연결되어 있습니다.</li>
<li>5와 1이 연결되어 있습니다.</li>
<li>3과 4가 연결되어 있습니다.</li>
<li>4와 5가 연결되어 있습니다.</li>
</ul>
<p>위 정보를 그림으로 그려보면 아래와 같습니다.
<img src="https://velog.velcdn.com/images/dob-by/post/3776f6c9-414f-4f4d-bc27-2d10ccab9004/image.png" width="550" height="50"/></p>
<p>그림에서 볼 수 있듯이 1-2-5가 연결되어 하나의 요소를 이루고, 6-4-3이 연결되어 하나의 요소를 이룹니다. </p>
<p>이 문제를 보자마자 몇일 전 풀어보았던 <a href="https://velog.io/@dob-by/python%EB%B0%B1%EC%A4%80-10451.-%EC%88%9C%EC%97%B4-%EC%82%AC%EC%9D%B4%ED%81%B4S3">순열 사이클</a> 문제가 생각났습니다. (상당히 유사한 방법으로 풀 수 있겠네요ㅎㅎ)</p>
<p>요소의 개수를 구한다는 것은 <span style="background-color:#FFE6E6">한 점을 정점으로 선택하고 해당 점과 연결된 모든 노드들을 탐색해나가는 과정에서, <strong>선택된 정점의 개수를 count한다</strong>는 의미로 해석</span>하면 되겠네요.</p>
<p>결국 우리가 구해야 할 것은 <span style="color:red">정점의 개수</span>입니다!!</p>
<h3 id="1️⃣-입력-및-변수-정의">1️⃣ 입력 및 변수 정의</h3>
<p>먼저 정점의 개수(N)과 간선의 개수(M)를 공백을 사이에 두고 입력받습니다. 그래프 구축 전 그래프 리스트를 초기화하고 방문 여부를 담을 리스트도 정의합니다. </p>
<pre><code class="language-python">N, M = map(int, sys.stdin.readline().split())
graph = [[] for _ in range(N+1)]  #그래프 리스트 초기화
visited = [False] * (N+1)
count = 0</code></pre>
<h3 id="2️⃣-노드정보-저장-및-그래프-기록">2️⃣ 노드정보 저장 및 그래프 기록</h3>
<p>노드-노드 쌍을 공백을 사이에 두고 입력받은 후 인접리스트 방법으로 그래프에 저장합니다.</p>
<pre><code class="language-python">for _ in range(M):
    a, b = map(int, sys.stdin.readline().split())
    graph[a].append(b)
    graph[b].append(a)
</code></pre>
<h3 id="3️⃣-dfs함수-정의">3️⃣ DFS함수 정의</h3>
<pre><code class="language-python">def dfs(v):
    visited[v] = True
    for i in graph[v]:
        if not visited[i]:
            dfs(i)</code></pre>
<h3 id="4️⃣-연결-요소의-개수-구하기">4️⃣ 연결 요소의 개수 구하기</h3>
<p>1부터 시작해서 정점으로 잡고 연결된 모든 노드를 방문하며 <code>dfs()</code>를 수행합니다. 연결된 노드가 더이상 없을 경우 다른 정점을 찾아 <code>dfs()</code>를 다시 수행합니다. 정점을 하나 찾을 때 마다 count를 1씩 증가시킵니다.</p>
<pre><code class="language-python">for i in range(1, N+1):
    if not visited[i]:
        dfs(i)
        count += 1</code></pre>
<h2 id="📌-알고리즘-선택">📌 알고리즘 선택</h2>
<p><span style="background-color:#FFE6E6">한 점을 정점으로 선택하고 해당 점과 연결된 모든 노드들을 모두 탐색</span>하면 되기 때문에 <span style="background-color:#FFE6E6">DFS를 활용</span>할 수 있겠네요.</p>
<h3 id="시간복잡도">시간복잡도</h3>
<blockquote>
<ul>
<li>그래프 구축: <code>O(M)</code></li>
</ul>
</blockquote>
<ul>
<li>dfs탐색: <code>O(N+M)</code></li>
</ul>
<p>최악의 경우 DFS는 모든 노드와 간선을 정확히 한 번 방문합니다. 따라서 전체 시간복잡도는 <code><strong>O(N+M)</strong></code>입니다. N의 최댓값은 1000이고, M의 최댓값은 (1,000*999)/2이므로 약 500,000입니다. 따라서 연산은 최대 약 501,000번 수행되므로 주어진 시간 내에 충분히 가능한 연산입니다.</p>
<hr>
<h2 id="📌-코드-설계하기">📌 코드 설계하기</h2>
<ol>
<li>N, M값과 노드-노드 쌍을 입력받습니다.</li>
<li>노드 연결정보를 통해 그래프 리스트를 구축합니다.</li>
<li>DFS를 수행합니다.</li>
</ol>
<hr>
<h2 id="📌-정답-코드">📌 정답 코드</h2>
<pre><code class="language-python">import sys

sys.setrecursionlimit(10000)

def dfs(v):
    visited[v] = True
    for i in graph[v]:
        if not visited[i]:
            dfs(i)

N, M = map(int, sys.stdin.readline().split())
graph = [[] for _ in range(N+1)]  #그래프 리스트 초기화
visited = [False] * (N+1)
count = 0

for _ in range(M):
    a, b = map(int, sys.stdin.readline().split())
    graph[a].append(b)
    graph[b].append(a)

for i in range(1, N+1):
    if not visited[i]:
        dfs(i)
        count += 1

print(count)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[python/백준] 17204. 죽음의 게임(S3)]]></title>
            <link>https://velog.io/@dob-by/python%EB%B0%B1%EC%A4%80-17204.-%EC%A3%BD%EC%9D%8C%EC%9D%98-%EA%B2%8C%EC%9E%84S3</link>
            <guid>https://velog.io/@dob-by/python%EB%B0%B1%EC%A4%80-17204.-%EC%A3%BD%EC%9D%8C%EC%9D%98-%EA%B2%8C%EC%9E%84S3</guid>
            <pubDate>Fri, 30 Aug 2024 11:42:27 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-문제-탐색하기">📌 문제 탐색하기</h2>
<p>👉 <a href="https://www.acmicpc.net/problem/17204">문제바로가기</a></p>
<blockquote>
</blockquote>
<p>N: 게임에 참여하는 사람의 수(3 ≤ N ≤ 150)
K: 보성이의 번호(1 ≤ K ≤ N - 1)
a<small>i</small>: i번 사람이 지목하는 사람의 번호(0 ≤ i ≤ N - 1, 0 ≤ a<small>i</small> ≤ N - 1)</p>
<p>지목하는 사람의 번호를 입력받아 차례대로 다음 노드로 이동하여 보성이의 번호(K)를 찾는 문제입니다. 어떤 방법으로도 K를 찾지 못한다면 -1을 출력하면됩니다.</p>
<h2 id="📌-알고리즘-선택">📌 알고리즘 선택</h2>
<p><code>방법1</code>: 단순히 지목하는 번호를 리스트에 담고, 그 번호를 가진 사람이 지목하는 번호를 뽑아 K와 일치하는지 확인하면서 몇 번 불렀는지 카운트하면 됩니다.</p>
<p><code>방법2</code>: M번만에 보성이에게 도달하는 최단 경로를 찾는 문제이므로 bfs를 활용할 수도 있겠네요.</p>
<h3 id="시간복잡도">시간복잡도</h3>
<p>1번 방법에서 최악의 경우 시간복잡도는 <code><strong>O(N)</strong></code>입니다. N의 최댓값은 150이므로 <code>O(N)</code>의 시간복잡도를 가진 알고리즘으로도 충분히 시간 내에 연산이 가능합니다.</p>
<hr>
<h2 id="📌-코드-설계하기">📌 코드 설계하기</h2>
<ol>
<li>N, K를 공백을 두고 Input받습니다.</li>
<li>N줄에 걸쳐 각각 사람이 지목하는 사람의 번호를 Input받습니다.</li>
<li>0부터 시작해서 해당 번호의 사람이 지목한 번호의 인덱스로 가서 다음 사람의 번호를 구합니다. 지목할때마다 count를 1씩 증가시키면서 K를 찾으면 count를 출력 후 반복문을 종료시키고, N번만큼 반복하여도 K를 찾지 못하면 종료하고 -1을 출력합니다.</li>
</ol>
<hr>
<h2 id="📌-정답-코드">📌 정답 코드</h2>
<pre><code class="language-python">import sys

N, K = map(int, sys.stdin.readline().split())
numbers = [int(sys.stdin.readline()) for _ in range(N)]
count = 0
number = 0

for _ in range(N):
    if number != K:
        number = numbers[number]
        count += 1
    else:
        print(count)
        break
else:
    print(-1)            </code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[python/백준] 10451. 순열 사이클(S3)]]></title>
            <link>https://velog.io/@dob-by/python%EB%B0%B1%EC%A4%80-10451.-%EC%88%9C%EC%97%B4-%EC%82%AC%EC%9D%B4%ED%81%B4S3</link>
            <guid>https://velog.io/@dob-by/python%EB%B0%B1%EC%A4%80-10451.-%EC%88%9C%EC%97%B4-%EC%82%AC%EC%9D%B4%ED%81%B4S3</guid>
            <pubDate>Thu, 29 Aug 2024 03:27:05 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-문제-탐색하기">📌 문제 탐색하기</h2>
<p>👉 <a href="https://www.acmicpc.net/problem/10451">문제바로가기</a></p>
<blockquote>
<h4 id="입력">입력</h4>
<p>T: 테스트 케이스의 개수
N: 순열의 크기(2 ≤ N ≤ 1,000)
순열: 각 정수는 공백으로 구분</p>
</blockquote>
<p>순열의 크기를 지정하고 순열이 주어진 경우 순열 사이클의 개수를 구하는 문제입니다. <strong>한 숫자를 지정했을 경우 해당 숫자와 연결된 모든 노드를 탐색해야 합니다. </strong></p>
<p>우리가 구해야 하는 것은 사이클의 수이며 사이클마다 정점은 하나씩 존재합니다. <span style="background-color:#FFE6E6">결론적으로 우리는 <span style="color:red">정점의 갯수</span>를 구해주면 됩니다!!</span></p>
<p>문제에서 주어진 순열을 예시로 들어보면, 8개의 순열 (3, 2, 7, 8, 1, 4, 5, 6)이 있을 경우 배열을 이용해 다음과 같이 표현할 수 있습니다.</p>
<p align="center"><img src="https://velog.velcdn.com/images/dob-by/post/abdb23a1-fbfa-4c6e-878b-1e5698d05730/image.png" width="500" height="50"/><p/>

<blockquote>
<p>1) 1 &rarr; 3, 3 &rarr; 7, 7 &rarr; 5, 5 &rarr; 1
2) 2 &rarr; 2
3) 4 &rarr; 8, 8 &rarr; 6, 6 &rarr; 4</p>
</blockquote>
<p>위 순서를 참고하여 그림으로 나타내면 다음과 같습니다.</p>
<p align="center"><img src="https://velog.velcdn.com/images/dob-by/post/e7fbebf4-0c7c-4c08-9c25-5ac325261f99/image.png" width="300" height="50"/><p/>

<p><span style="background-color:#FFE6E6">결국 1~8까지의 배열과 순열 (3, 2, 7, 8, 1, 4, 5, 6)를 하나씩 매치한 화살표가 <strong>노드와 노드의 연결을 의미</strong>합니다. </span></p>
<p>노드와 노드의 연결쌍을 알았으면 이전에 풀었던 문제들처럼 그래프를 기록할 수 있습니다.</p>
<h3 id="1️⃣-입력-및-변수-정의">1️⃣ 입력 및 변수 정의</h3>
<p>먼저 테스트 케이스의 개수(T)와 순열의 크기(N)를 입력받습니다.</p>
<pre><code class="language-python">T = int(sys.stdin.readline())   #테스트 케이스 수
N = int(sys.stdin.readline())   #순열의 크기</code></pre>
<p>이후 순열의 크기만큼 순열을 입력받고 방문 횟수여부를 담은 리스트<code>visited</code>를 정의합니다. </p>
<pre><code class="language-python">    arr2 = list(map(int, sys.stdin.readline().split()))   #순열
    visited = [False] * (N+1)
    count = 0   #정점의 수</code></pre>
<p><code>arr1</code>은 순열과 매치하기 위한 1~8까지의 숫자 리스트이기 때문에 반복문을 이용하여 1부터 순열의 크기만큼 숫자를 append해줍니다.</p>
<pre><code class="language-python">arr1 = []

for i in range(1, N+1):
    arr1.append(i)</code></pre>
<h3 id="2️⃣-노드정보-저장-및-그래프-기록">2️⃣ 노드정보 저장 및 그래프 기록</h3>
<p>1~8까지를 저장한 리스트 <code>arr1</code>과 8개의 수로 이루어진 순열을 저장한 리스트 <code>arr2</code>를 활용하여 노드와 노드의 연결 정보를 리스트<code>newArray</code>에 저장합니다.</p>
<pre><code class="language-python">newArray = [[] for _ in range(N)]

for i in range(N):
    newArray[i].append(arr1[i])
    newArray[i].append(arr2[i])</code></pre>
<p>저장된 노드 정보를 활용하여 인접리스트를 활용해 리스트<code>graph</code>에 기록합니다. 이때 해당 리스트의 중복을 제거하고, 작은 수부터 시작하도록 정렬하기 위해 <code>sort()</code>를 사용합니다.</p>
<pre><code class="language-python">graph = [[] for _ in range(N+1)]

for i in range(N):
    graph[arr1[i]].append(arr2[i])
    graph[arr2[i]].append(arr1[i])

#graph의 각 요소들을 정렬
for i in range(N+1):
    graph[i] = list(set(graph[i]))   #중복제거
    graph[i].sort()    </code></pre>
<h3 id="3️⃣-dfs함수-정의">3️⃣ DFS함수 정의</h3>
<pre><code class="language-python">def dfs(start):
    visited[start] = True
    #현재 노드와 연결된 다른 노드를 재귀적으로 방문
    for i in graph[start]:
        if not visited[i]:
            dfs(i)</code></pre>
<h3 id="4️⃣-순열-사이클-개수-구하기">4️⃣ 순열 사이클 개수 구하기</h3>
<p>1부터 시작해서 아직 방문하지 않은 연결된 노드들을 방문합니다. 모두 탐색했으면 방문하지 않은 다른 숫자를 정점으로해서 반복합니다. <span style="background-color:#FFE6E6">정점이 하나 정해질때마다 count하여 정점의 개수를 구하고 출력합니다.</span></p>
<pre><code class="language-python">for i in range(1, N+1):
    if not visited[i]:
        dfs(i)
        count += 1

print(count)        </code></pre>
<h2 id="📌-알고리즘-선택">📌 알고리즘 선택</h2>
<p>한 시작점이 정해지면 <span style="background-color:#FFE6E6">해당 노드와 연결된 노드를 끝까지 다 탐색</span>해야 하므로 DFS를 활용하여 풀 수 있습니다.</p>
<h3 id="시간복잡도">시간복잡도</h3>
<blockquote>
<ul>
<li>그래프 구축: <code>O(N)</code></li>
</ul>
</blockquote>
<ul>
<li>중복 제거 및 <code>sort()</code>를 사용하여 정렬: <code>O(NlogN)</code></li>
<li>dfs탐색: <code>O(N+E)</code></li>
</ul>
<p>최악의 경우 시간복잡도를 구하기 위해 N의 최대값이 1000인 경우, 모든 노드가 다른 모든 노드와 연결된 경우를 고려합니다. 이경우 E는 최대 N이며 결론적으로 <code>E=N=1000</code>입니다. 따라서 전체 시간복잡도는 <code>O(N)+O(NlogN)+O(N)=O(NlogN)=O(1000*log1000)</code>입니다. </p>
<hr>
<h2 id="📌-코드-설계하기">📌 코드 설계하기</h2>
<ol>
<li>테스트 케이스의 개수(T)와 순열의 크기(N)를 Input받습니다.</li>
<li>노드의 정보를 저장하고 해당 정보를 활용해 그래프를 기록합니다.</li>
<li>DFS를 수행하면서 정점의 개수를 구하고 출력합니다. </li>
</ol>
<hr>
<h2 id="📌-정답-코드">📌 정답 코드</h2>
<pre><code class="language-python">import sys

def dfs(start):
    visited[start] = True
    #현재 노드와 연결된 다른 노드를 재귀적으로 방문
    for i in graph[start]:
        if not visited[i]:
            dfs(i)

T = int(sys.stdin.readline())   #테스트 케이스 수

for _ in range(T):
    N = int(sys.stdin.readline())   #순열의 크기

    arr1 = []   
    arr2 = list(map(int, sys.stdin.readline().split()))   #순열
    visited = [False] * (N+1)
    count = 0   #정점의 수

    for i in range(1, N+1):
        arr1.append(i)

    newArray = [[] for _ in range(N)]

    for i in range(N):
        newArray[i].append(arr1[i])
        newArray[i].append(arr2[i])

    graph = [[] for _ in range(N+1)]

    for i in range(N):
        graph[arr1[i]].append(arr2[i])
        graph[arr2[i]].append(arr1[i])

    #graph의 각 요소들을 정렬
    for i in range(N+1):
        graph[i] = list(set(graph[i]))   #중복제거
        graph[i].sort()    

    for i in range(1, N+1):
        if not visited[i]:
            dfs(i)
            count += 1

    print(count)            </code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[python/백준] 1012. 유기농 배추(S2)]]></title>
            <link>https://velog.io/@dob-by/python%EB%B0%B1%EC%A4%80-1012.-%EC%9C%A0%EA%B8%B0%EB%86%8D-%EB%B0%B0%EC%B6%94-S2</link>
            <guid>https://velog.io/@dob-by/python%EB%B0%B1%EC%A4%80-1012.-%EC%9C%A0%EA%B8%B0%EB%86%8D-%EB%B0%B0%EC%B6%94-S2</guid>
            <pubDate>Tue, 27 Aug 2024 14:51:06 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-문제-탐색하기">📌 문제 탐색하기</h2>
<p>👉 <a href="https://www.acmicpc.net/problem/1012">문제바로가기</a></p>
<blockquote>
<p>T: 테스트 케이스 개수
M: 배추밭의 가로 길이(1 ≤ M ≤ 50)
N: 배추밭의 세로 길이(1 ≤ N ≤ 50)
K: 배추가 심어져 있는 위치의 개수(1 ≤ K ≤ 2500)
K줄에 배추의 위치X(0 ≤ X ≤ M-1), Y(0 ≤ Y ≤ N-1)가 주어진다. </p>
</blockquote>
<p>배추가 심어져 있는 위치를 입력받고, 땅의 처음부터 끝까지 탐색하면서 정점의 개수를 찾는 문제입니다. 한 정점에 대해 탐색을 수행하면서 값이 1인 곳(배추가 심어져 있는 곳)의 방문 여부를 True로 변경시켜주면 됩니다.</p>
<h3 id="1️⃣-입력-및-변수-정의">1️⃣ 입력 및 변수 정의</h3>
<pre><code class="language-python">M, N, K = map(int, sys.stdin.readline().split())
    arr = [[0] * M for _ in range(N)]  # 배추의 위치
    visited = [[False] * M for _ in range(N)]
    count = 0  # 정점의 갯수</code></pre>
<h3 id="2️⃣-배추의-위치-설정">2️⃣ 배추의 위치 설정</h3>
<pre><code class="language-python">for _ in range(K):
    X, Y = map(int, sys.stdin.readline().split())
    arr[Y][X] = 1</code></pre>
<h3 id="3️⃣-위치-탐색">3️⃣ 위치 탐색</h3>
<pre><code class="language-python">for i in range(N):
        for j in range(M):
            if not visited[i][j] and arr[i][j] == 1:
                count += 1
                dfs(i, j) </code></pre>
<p>땅의 처음부터 끝까지 모두 탐색하면서 dfs()를 호출합니다. 한 번의 정점을 기준으로 탐색하고, 해당 탐색이 끝나면 다음 칸을 정점으로 잡고 다시 탐색합니다. 하나의 정점에 대해 탐색이 끝난 경우 배추흰지렁이는 더이상 이동할 수 있는 칸이 없으므로 1마리를 추가합니다. <span style="color:red">정점의 갯수가 우리가 구하고자하는 배추흰지렁이의 수가 되는 것입니다.</span></p>
<h3 id="4️⃣-dfs">4️⃣ DFS</h3>
<pre><code class="language-python">def dfs(i, j): 
    visited[i][j] = True
    # 아래쪽 탐색
    if i + 1 &lt; N and not visited[i + 1][j] and arr[i + 1][j] == 1:
        dfs(i + 1, j)
    # 위쪽 탐색
    if i - 1 &gt;= 0 and not visited[i - 1][j] and arr[i - 1][j] == 1:
        dfs(i - 1, j)
    # 오른쪽 탐색
    if j + 1 &lt; M and not visited[i][j + 1] and arr[i][j + 1] == 1:
        dfs(i, j + 1)
    # 왼쪽 탐색
    if j - 1 &gt;= 0 and not visited[i][j - 1] and arr[i][j - 1] == 1:
        dfs(i, j - 1)
    else:
        return    </code></pre>
<p>한 정점을 기준으로 사방(오른쪽, 왼쪽, 위쪽, 아래쪽)에 배추가 더 있는지 모두 탐색해야합니다. 배추가 더 있는 경우 탐색을 이어서 더 진행해야하기 때문에 재귀함수를 사용합니다.</p>
<hr>
<h2 id="📌-알고리즘-선택">📌 알고리즘 선택</h2>
<p>한 정점과 인접한 가능한 모든 노드를 탐색해야 하므로 DFS를 사용하였습니다.</p>
<h3 id="시간복잡도">시간복잡도</h3>
<p>for루프가 중첩되어있는 부분에서 전체 탐색에 대한 시간복잡도는 <code>O(N*M)</code>이며, 각 위치에 대한 DFS의 시간복잡도는 <code>O(N*M)</code>입니다. 따라서 전체 시간복잡도는 <code><strong>O(N*M)</strong></code>입니다. N, M의 최댓값은 50이므로 약 2500회의 연산이 수행됩니다. 이는 주어진 제한시간 1초 안에 충분히 가능한 연산입니다.</p>
<hr>
<h2 id="📌-코드-설계하기">📌 코드 설계하기</h2>
<ol>
<li>T, M, N, K를 Input받고 배추가 심어져있는 위치는 K줄에 Input받습니다.</li>
<li>입력받은 배추의 위치 좌표에 1을 대입하여 리스트값을 변경합니다.</li>
<li>땅 위치의 처음부터 끝까지 탐색하며 정점의 갯수를 구합니다.</li>
</ol>
<hr>
<h2 id="📌-시도회차-수정사항">📌 시도회차 수정사항</h2>
<h3 id="1회차">1회차</h3>
<p>테스트 케이스 개수를 고려하지 않았습니다.</p>
<h3 id="2회차-런타임-에러">2회차: 런타임 에러</h3>
<h4 id="런타임-에러란">런타임 에러란?</h4>
<ul>
<li>프로그램 실행 도중 비정상적으로 종료되는 것<h4 id="런타임-에러의-이유">런타임 에러의 이유</h4>
런타임 에러의 이유는 다양하지만.. 제가 작성한 코드에서의 런타임 에러 이유는 다음과 같습니다.<blockquote>
<ul>
<li>백준 채점 시스템에서 최대 재귀 깊이를 디폴트 값으로 1000으로 정해놓았습니다.</li>
</ul>
</blockquote>
</li>
<li><span style="color:red">런타임 에러는 그 최대 깊이를 초과하여 재귀 호출을 하기 때문에 발생</span>하는 것입니다.</li>
</ul>
<h4 id="해결방법">해결방법</h4>
<pre><code class="language-python">import sys
sys.setrecursionlimit(10000)</code></pre>
<p>위 코드를 사용하여 최대 재귀 깊이를 늘려주었습니다.</p>
<hr>
<h2 id="📌-정답-코드">📌 정답 코드</h2>
<pre><code class="language-python">import sys

sys.setrecursionlimit(10000)

def dfs(i, j): 
    visited[i][j] = True
    # 아래쪽 탐색
    if i + 1 &lt; N and not visited[i + 1][j] and arr[i + 1][j] == 1:
        dfs(i + 1, j)
    # 위쪽 탐색
    if i - 1 &gt;= 0 and not visited[i - 1][j] and arr[i - 1][j] == 1:
        dfs(i - 1, j)
    # 오른쪽 탐색
    if j + 1 &lt; M and not visited[i][j + 1] and arr[i][j + 1] == 1:
        dfs(i, j + 1)
    # 왼쪽 탐색
    if j - 1 &gt;= 0 and not visited[i][j - 1] and arr[i][j - 1] == 1:
        dfs(i, j - 1)
    else:
        return    

T = int(sys.stdin.readline())

for _ in range(T):
    M, N, K = map(int, sys.stdin.readline().split())
    arr = [[0] * M for _ in range(N)]  # 배추의 위치
    visited = [[False] * M for _ in range(N)]
    count = 0  # 정점의 갯수

    # 배추의 위치 입력
    for _ in range(K):
        X, Y = map(int, sys.stdin.readline().split())
        arr[Y][X] = 1

    # 모든 위치를 탐색하며 DFS 호출
    for i in range(N):
        for j in range(M):
            if not visited[i][j] and arr[i][j] == 1:
                count += 1
                dfs(i, j)     
    print(count)</code></pre>
<hr>
<h2 id="📌-다른-풀이">📌 다른 풀이</h2>
<p>아래 코드는 BFS를 활용한 코드입니다. (출처: <code>@why_dev_says_no</code>)</p>
<pre><code class="language-python">import sys

T = int(sys.stdin.readline())
dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]


# 2. 한 노드에서 BFS를 탐색하는 과정을 구현합니다.
def bfs(x, y):
    queue = [(x, y)]
    global board
    board[x][y] = 0

    while queue:
        x, y = queue.pop()
        for i in range(4):
            nx = x + dx[i]
            ny = y + dy[i]

            if nx &lt; 0 or nx &gt;= M or ny &lt; 0 or ny &gt;= N:
                continue
            if board[nx][ny] == 1:
                board[nx][ny] = 0
                queue.append((nx, ny))


# 1. 문제의 input을 받습니다.
for _ in range(T):
    M, N, K = map(int, sys.stdin.readline().split())
    board = [[0 for _ in range(N)] for _ in range(M)]
    for _ in range(K):
        x, y = map(int, sys.stdin.readline().split())
        board[x][y] = 1

    ans = 0
    # 3. 문제의 배열에 대해 BFS 탐색을 진행하며 지렁이의 개수를 구합니다.
    for x in range(M):
        for y in range(N):
            if board[x][y] == 1:
                bfs(x, y)
                ans += 1
    print(ans)
</code></pre>
]]></description>
        </item>
    </channel>
</rss>