<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>kt.gmLOG</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 25 Aug 2026 15:52:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>kt.gmLOG</title>
            <url>https://velog.velcdn.com/images/kt_gml/profile/f59a162a-3422-477b-9dbc-385e05305dc8/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. kt.gmLOG. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kt_gml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[2박 3일간 강화학습 모델 49개 만들어서 AWS 대회 최우수상 받기]]></title>
            <link>https://velog.io/@kt_gml/2%EB%B0%95-3%EC%9D%BC%EA%B0%84-%EA%B0%95%ED%99%94%ED%95%99%EC%8A%B5-%EB%AA%A8%EB%8D%B8-49%EA%B0%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%84%9C-AWS-%EB%8C%80%ED%9A%8C-%EC%B5%9C%EC%9A%B0%EC%88%98%EC%83%81-%EB%B0%9B%EA%B8%B0</link>
            <guid>https://velog.io/@kt_gml/2%EB%B0%95-3%EC%9D%BC%EA%B0%84-%EA%B0%95%ED%99%94%ED%95%99%EC%8A%B5-%EB%AA%A8%EB%8D%B8-49%EA%B0%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%84%9C-AWS-%EB%8C%80%ED%9A%8C-%EC%B5%9C%EC%9A%B0%EC%88%98%EC%83%81-%EB%B0%9B%EA%B8%B0</guid>
            <pubDate>Tue, 25 Aug 2026 15:52:27 GMT</pubDate>
            <description><![CDATA[<h2 id="최우수상보다-먼저-남기고-싶은-것">최우수상보다 먼저 남기고 싶은 것</h2>
<p>2026년 8월 19일부터 21일까지 2박 3일간 진행한 <strong>경북대 × 전남대 Physical AI 경진대회</strong>에서
<strong>AWS DeepRacer 과제</strong>로 <strong>최우수상</strong>을 받았다.</p>
<p>결과만 적으면 <strong>“빠른 모델을 만들어 상을 받았다”</strong>로 끝날 수 있다. 하지만 이번
대회에서 더 오래 남기고 싶은 것은 <strong>가장 빠르다고 믿었던 모델이 실차에서 가장
먼저 실패했고, 그 실패를 해석하면서 전략을 완전히 바꾼 과정</strong>이다.</p>
<p>시뮬레이터에서 이론적으로 최적인 경로는 실제 차에서 최적이 아니었다. 실제
환경에서는 짧은 경로보다 <strong>카메라 지연을 흡수할 여유</strong>, 노면과 배터리 변화에 대한
안정성, 그리고 빠르게 반복할 수 있는 튜닝 방법이 더 중요했다.</p>
<hr>
<h2 id="한-번의-실험에-2시간-30분이-걸렸다">한 번의 실험에 2시간 30분이 걸렸다</h2>
<p>가장 큰 제약은 학습 시간이었다. 모델 하나를 학습하고 평가한 뒤 실차에서 확인할
때까지 약 <strong>2시간 30분</strong>이 필요했다. 하나의 모델을 만들고, 결과를 본 다음, 다시
조금 고치는 순차적인 방식으로는 하루에 몇 번밖에 실험할 수 없었다.</p>
<p>그래서 처음부터 가설을 넓게 벌렸다. 최적 궤적, 곡률 기반 감가속, 중앙선 추종,
조향 변화 억제처럼 성격이 다른 <strong>가설 10개</strong>를 세우고 <strong>약 50개 모델</strong>을 병렬로
검증했다. 그중 살아남은 <strong>후보 다섯 개</strong>만 추가 학습과 실차 테스트 대상으로
좁혔다.</p>
<p><strong>서로 다른 실패 방식을 가진 모델을 동시에 확보했기 때문에</strong>, 한 접근이 실차에서
무너졌을 때 다른 가설로 바로 전환할 수 있었다.</p>
<hr>
<h2 id="첫-번째-승자는-좌표-기반-최적-경로였다">첫 번째 승자는 좌표 기반 최적 경로였다</h2>
<p>첫 접근은 절대 좌표를 이용한 레이싱 라인이었다. 트랙 구조가 이미 주어졌다면
차가 시행착오로 모든 길을 탐색하게 하는 것보다 이동거리가 짧고 코너 반경이 큰
경로를 먼저 계산해 주는 편이 효율적이라고 판단했다.</p>
<p>A to Z Speedway의 센터라인 웨이포인트를 바탕으로 계산한 경로는 길이가
<strong>16.635m에서 15.409m로 약 7.4% 짧아졌다.</strong> 목표 좌표와 목표 속도를 보상 함수에
넣어 그 경로에 가까울수록 높은 점수를 주었다.</p>
<p>가상 환경에서는 예상이 맞았다. 좌표와 목표 속도를 함께 사용한 모델은 탈선 없이
안정적으로 수렴했고, 훈련 과정에서는 <strong>6초대 기록</strong>까지 나왔다. 평가에서도 좌표
모델이 중앙선 기반 모델보다 빨랐다. <strong>이때까지만 해도 최종 모델은 이미 정해졌다고
생각했다.</strong></p>
<hr>
<h2 id="시뮬레이터-6초대-모델이-실차에서-이탈했다">시뮬레이터 6초대 모델이 실차에서 이탈했다</h2>
<p><strong>실차 결과는 정반대였다.</strong> 좌표 기반 모델은 코너에서 계속 트랙 반대쪽으로
이탈했다. 반면 시뮬레이터 기록이 상대적으로 느렸던 중앙선 추종 모델은 실제
트랙을 안정적으로 완주했다.</p>
<p>처음에는 학습 시간이 부족했거나 조향각이 작아서라고 의심했다. 하지만 좌표
모델은 가상 평가에서 탈선이 없고 랩타임 편차도 작았다. 학습이 덜 된 정책이라면
시뮬레이터에서도 먼저 흔들렸어야 했다. 문제는 수렴 여부가 아니라 시뮬레이터와
현실 사이의 차이, 즉 <strong>Sim2Real 격차</strong>였다.</p>
<p>좌표 보상은 자동차가 현재 트랙의 어느 지점에 있는지 알아내고 안쪽 경로에
붙도록 요구한다. 하지만 카메라 한 대만 사용하는 모델이 가상 환경에서 그 위치를
구분하려면 배경과 텍스처 같은 시뮬레이터 고유 단서를 외우기 쉽다. <strong>실제 트랙에는
그 단서가 없었다.</strong></p>
<hr>
<h2 id="18cm의-여유와-100ms-동안의-20cm">18cm의 여유와 100ms 동안의 20cm</h2>
<p>계산된 최적 경로와 흰 선 사이의 최소 여유는 약 <strong>18cm</strong>였다. 지연이 없는
시뮬레이터에서는 충분해 보이는 거리였다.</p>
<p>하지만 자동차가 2.0m/s로 달릴 때 카메라 인식과 조향 사이에 <strong>100ms의 지연</strong>이
생기면, 판단이 반영되기 전까지 이미 <strong>20cm</strong>를 더 이동한다. 18cm의 여유보다 지연
중 이동거리가 더 길다. 코너 안쪽을 노린 정책이 늦게 꺾으면 바깥으로 밀려날
수밖에 없었다.</p>
<p>반면 센터라인의 여유는 약 <strong>44cm</strong>였다. 최단거리는 아니지만 지연과 오차를 흡수할
공간이 있었다.</p>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="right">거리</th>
<th align="left">의미</th>
</tr>
</thead>
<tbody><tr>
<td align="left">최적 경로의 최소 여유</td>
<td align="right"><strong>18cm</strong></td>
<td align="left">지연 중 이동거리보다 짧음</td>
</tr>
<tr>
<td align="left">100ms 동안 이동거리</td>
<td align="right"><strong>20cm</strong></td>
<td align="left">최적 경로의 여유를 초과</td>
</tr>
<tr>
<td align="left">센터라인의 여유</td>
<td align="right"><strong>44cm</strong></td>
<td align="left">지연과 오차를 흡수할 공간 확보</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>이론적인 최적 경로는 지연이 없는 세계에서만 최적이었다.</strong></p>
</blockquote>
<hr>
<h2 id="좌표를-버리고-중앙선으로-돌아오다">좌표를 버리고 중앙선으로 돌아오다</h2>
<p>최종 모델에서는 <strong>좌표를 한 줄도 사용하지 않았다.</strong> 트랙 폭 대비 중앙선에서 얼마나
벗어났는지와 조향이 얼마나 급격하게 변하는지만 보았다. 절대 위치 대신 카메라가
실차에서도 동일하게 관찰할 수 있는 상대적인 단서를 사용했다.</p>
<p>액션 공간도 실차 관측에 맞췄다. 코너를 더 잘 돌게 하려고 25도와 30도 조향을
시도했지만, 큰 조향각에서는 바퀴가 헛돌았고 직선에서도 좌우로 흔들렸다. 최종
조향은 <strong>±20도</strong>로 제한하고 큰 조향에서는 속도를 낮추고 직진에서는 속도를 높였다.</p>
<p><strong>더 많은 선택지를 주는 것이 항상 더 좋은 정책으로 이어지지는 않았다.</strong> 실제로
전이되는 입력과 차가 물리적으로 실행할 수 있는 행동만 남기는 것이 더 강했다.</p>
<hr>
<h2 id="학습-밖의-손잡이-스로틀과-출발-위치">학습 밖의 손잡이: 스로틀과 출발 위치</h2>
<p>최종 후보가 정해진 뒤에는 재학습보다 빠르게 반복할 수 있는 변수를 찾았다.
스로틀 배율은 즉시 바꿀 수 있었고 한 번의 주행은 수십 초면 끝났다. 같은 모델에서
배율을 2% 단위로 올리며 이탈하지 않는 경계선을 찾았다.</p>
<p>장소를 옮기자 같은 배율에서도 실제 속도가 달라졌다. 노면 마찰과 배터리 상태가
바뀌면 이전 장소의 숫자를 그대로 재사용할 수 없었다. 그래서 최종 주행에서는
배율 하나로 전체 랩을 묶지 않았다. 직선에서는 <strong>75%</strong>, 코너 진입 전에는 <strong>62%</strong>로
낮추고, 코너를 통과하면 다시 <strong>75%</strong>로 올렸다. 출발 위치도 뒤로 옮겨 직선에서
가속할 거리를 확보했다.</p>
<blockquote>
<p><strong>학습이 병목일 때 성능을 올리는 방법이 반드시 추가 학습인 것은 아니었다.</strong>
모델 밖에서 초 단위로 반복할 수 있는 손잡이를 찾는 것이 더 높은 기대값을 만들었다.</p>
</blockquote>
<hr>
<h2 id="최우수상-그리고-다시-한다면">최우수상, 그리고 다시 한다면</h2>
<p>결국 좌표 기반 최단 경로가 아니라 <strong>중앙선 기반의 안정적인 모델</strong>을 선택했고,
실차에서 속도와 조향을 다시 맞춘 전략으로 <strong>최우수상</strong>을 받았다.</p>
<p><img src="https://raw.githubusercontent.com/TaeHuiKKIM/deepracer-physical-ai/main/assets/awards/physical-ai-award.png" alt="Physical AI 경진대회 최우수상 수상"></p>
<p>다시 한다면 다음 원칙을 가장 먼저 적용할 것이다.</p>
<ol>
<li><strong>트랙 방향을 기본값에 맡기지 않고 학습 전에 확인한다.</strong></li>
<li><strong>시뮬레이터 순위를 실차 순위로 해석하지 않는다.</strong></li>
<li>절대 좌표보다 실제 센서에서도 유지되는 상대적 단서를 우선한다.</li>
<li><code>지연 × 속도</code>보다 큰 여유폭을 확보한다.</li>
<li>학습 시간이 병목이면 가설을 벌려 병렬로 검증한다.</li>
<li>스로틀, 출발 위치, 구간 제어처럼 학습 밖에서 빠르게 반복할 변수를 찾는다.</li>
</ol>
<p>이번 수상의 핵심은 처음부터 정답을 맞힌 것이 아니었다. 서로 다른 가설을
준비했고, <strong>가장 유력한 가설이 현실에서 실패했을 때 그 이유를 수치로 설명한 뒤
다른 접근으로 전환할 수 있었던 점</strong>이었다.</p>
<hr>
<h2 id="실험-기록과-코드">실험 기록과 코드</h2>
<p>보상 함수, 모델별 평가 결과, 실차 스로틀 튜닝 기록과 실패 원인은 아래 공개
저장소에 정리했다.</p>
<ul>
<li><strong>GitHub:</strong> <a href="https://github.com/TaeHuiKKIM/deepracer-physical-ai">https://github.com/TaeHuiKKIM/deepracer-physical-ai</a></li>
</ul>
<p>단순히 우승 모델의 코드만 남기기보다, 시뮬레이터에서 잘되던 접근이 왜 실차에서
실패했는지와 어떤 판단을 거쳐 최종 모델을 선택했는지를 함께 기록했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[브라우저는 HTML을 그대로 보여주지 않는다 ]]></title>
            <link>https://velog.io/@kt_gml/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EB%8A%94-HTML%EC%9D%84-%EA%B7%B8%EB%8C%80%EB%A1%9C-%EB%B3%B4%EC%97%AC%EC%A3%BC%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4-DOM-1970%EA%B0%9C%EA%B0%80-%ED%99%94%EB%A9%B4%EC%9D%B4-%EB%90%98%EA%B8%B0%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@kt_gml/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EB%8A%94-HTML%EC%9D%84-%EA%B7%B8%EB%8C%80%EB%A1%9C-%EB%B3%B4%EC%97%AC%EC%A3%BC%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4-DOM-1970%EA%B0%9C%EA%B0%80-%ED%99%94%EB%A9%B4%EC%9D%B4-%EB%90%98%EA%B8%B0%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Fri, 14 Aug 2026 01:57:46 GMT</pubDate>
            <description><![CDATA[<h2 id="dom-1970개가-화면이-되기까지">DOM 1,970개가 화면이 되기까지</h2>
<p><code>div</code> 몇 개 줄였는데 화면 흔들림이 사라지고 첫 클릭도 빨라졌다. 개미 투자 게임을 다듬을 때 실제로 겪은 일이다.</p>
<p>처음에는 “HTML 노드가 많아서 느리다” 정도로 이해했다. 하지만 DOM 수만 세어서는 왜 레이아웃이 흔들렸고, 왜 첫 입력에 긴 작업이 생겼는지 설명할 수 없었다. HTML은 화면이 아니다. 브라우저가 HTML·CSS·JavaScript를 읽고, 여러 중간 구조를 거쳐 픽셀로 바꾸는 과정을 알아야 숫자의 의미가 보였다.</p>
<h2 id="html이-픽셀이-되기까지">HTML이 픽셀이 되기까지</h2>
<p>브라우저는 네트워크에서 받은 HTML을 파싱해 DOM을 만든다. CSS는 CSSOM이 되고, 둘을 바탕으로 실제로 그릴 요소를 추린 렌더 트리가 만들어진다. 이후 대략 다음 단계가 이어진다.</p>
<ol>
<li><strong>Style</strong>: 어떤 CSS 규칙이 어떤 노드에 적용되는지 계산한다.</li>
<li><strong>Layout</strong>: 각 요소의 크기와 위치를 정한다.</li>
<li><strong>Paint</strong>: 텍스트, 배경, 테두리처럼 무엇을 그릴지 기록한다.</li>
<li><strong>Composite</strong>: 여러 레이어를 적절한 순서로 합성해 화면에 올린다.</li>
</ol>
<p>이 흐름을 한 번만 수행하고 끝나는 것은 아니다. JavaScript가 class를 바꾸거나, 이미지 크기가 뒤늦게 결정되거나, 폰트가 교체되면 일부 단계가 다시 필요할 수 있다.</p>
<p>그래서 “DOM 한 개를 추가했다”는 말에는 비용이 하나만 들어 있지 않다. 그 노드가 스타일 계산 범위를 넓히는지, 주변 배치를 다시 계산하게 하는지, 넓은 영역을 다시 그리게 하는지에 따라 결과가 달라진다.</p>
<h2 id="dom-개수만-줄이면-빨라질까">DOM 개수만 줄이면 빨라질까</h2>
<p>노드가 많으면 스타일과 레이아웃 후보도 많아질 수 있으니 DOM 크기는 유용한 신호다. 다만 만능 점수는 아니다.</p>
<ul>
<li>2,000개의 단순한 정적 노드와 200개의 복잡한 애니메이션 노드는 비용 구조가 다르다.</li>
<li><code>transform</code>으로 움직이는 요소와 레이아웃 속성을 계속 바꾸는 요소도 다르다.</li>
<li>보이지 않는 목록을 모두 렌더링하는 것과 화면 근처만 가상화하는 것은 노드 수 이상의 차이를 만든다.</li>
<li>selector 구조, 이미지 크기, 폰트, JavaScript 실행 시간도 함께 봐야 한다.</li>
</ul>
<p>DOM 개수는 원인 확정값이 아니라 “렌더링 경로를 더 살펴보라”는 계기판에 가깝다.</p>
<p>특히 <strong>CLS(Cumulative Layout Shift)</strong> 는 단순히 노드가 많다는 뜻이 아니다. 이미지나 광고의 공간을 미리 잡지 않았거나, 늦게 나타난 콘텐츠가 기존 요소를 밀어낼 때처럼 사용자가 예상하지 못한 위치 변경을 합산한 지표다. 노드가 적어도 큰 영역이 움직이면 CLS는 커질 수 있다.</p>
<h2 id="내-프로젝트에서-확인한-렌더링-병목">내 프로젝트에서 확인한 렌더링 병목</h2>
<p>개미 투자 게임의 v235 성능 기록에서는 로컬 Chromium의 정해진 시나리오 기준으로 다음 변화가 남아 있다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th align="right">변경 전</th>
<th align="right">변경 후</th>
</tr>
</thead>
<tbody><tr>
<td>DOM 노드</td>
<td align="right">1,970</td>
<td align="right">860</td>
</tr>
<tr>
<td>CLS 기록</td>
<td align="right">0.4926 / 0.2504</td>
<td align="right">0</td>
</tr>
<tr>
<td>첫 입력 Long Task</td>
<td align="right">약 223ms</td>
<td align="right">0~54ms</td>
</tr>
</tbody></table>
<p>이 숫자는 모든 기기와 모든 실행에서 보장되는 벤치마크가 아니다. 당시 로컬 환경에서 같은 흐름을 비교한 회귀 기록이다. 그래도 무엇을 함께 고쳤는지는 분명했다.</p>
<ul>
<li>시작부터 만들 필요가 없는 UI를 늦게 생성했다.</li>
<li>처음 보이는 화면의 구조를 단순화했다.</li>
<li>나중에 나타나는 요소가 기존 배치를 밀지 않도록 공간과 표시 순서를 조정했다.</li>
<li>첫 클릭 안에 몰려 있던 초기화 작업을 준비 단계와 작은 단위로 나눴다.</li>
</ul>
<p>DOM 1,110개를 지운 것만으로 CLS가 0이 된 것이 아니다. <strong>필요한 시점에 필요한 구조만 만들고, 뒤늦게 생길 요소의 자리를 예측 가능하게 만든 것</strong>이 함께 작용했다.</p>
<p>이 경험 이후에는 Lighthouse 점수 하나보다 Performance 패널의 Main thread, Layout Shift 영역, 긴 task 내부의 call stack을 먼저 본다. 숫자가 나빠졌다면 “React라서”, “DOM이 많아서”라고 결론 내리기 전에 어느 단계가 오래 걸렸는지 좁히는 편이 정확했다.</p>
<h2 id="렌더링-최적화에서-먼저-볼-순서">렌더링 최적화에서 먼저 볼 순서</h2>
<p>내가 다음 프로젝트에서 사용할 순서는 이렇다.</p>
<h3 id="1-사용자-시나리오를-고정한다">1. 사용자 시나리오를 고정한다</h3>
<p>첫 진입, 첫 클릭, 목록 필터, 모달 열기처럼 느리다고 느낀 행동을 하나 고른다. 서로 다른 행동의 수치를 섞으면 개선 전후가 비교되지 않는다.</p>
<h3 id="2-메인-스레드에서-긴-작업을-찾는다">2. 메인 스레드에서 긴 작업을 찾는다</h3>
<p>50ms를 넘는 task는 사용자 입력이나 다음 프레임을 오래 기다리게 할 수 있다. 그 안에서 스크립트 실행, 스타일, 레이아웃, 페인트 중 어디에 시간이 쓰였는지 본다.</p>
<h3 id="3-레이아웃-이동의-원인을-화면과-연결한다">3. 레이아웃 이동의 원인을 화면과 연결한다</h3>
<p>Layout Shift 항목을 클릭해 실제로 움직인 요소를 찾는다. 이미지 <code>width</code>·<code>height</code>, 비동기 콘텐츠 자리, 웹폰트, 삽입되는 배너를 확인한다.</p>
<h3 id="4-구조를-바꾼-뒤-같은-시나리오로-다시-잰다">4. 구조를 바꾼 뒤 같은 시나리오로 다시 잰다</h3>
<p>DOM 개수만 비교하지 않는다. 입력 응답, 긴 task, CLS, LCP를 함께 남기고 부작용도 기록한다.</p>
<h2 id="직접-해볼-실습">직접 해볼 실습</h2>
<p>아래처럼 높이가 없는 이미지와 늦게 삽입되는 배너를 가진 작은 페이지를 만든다.</p>
<pre><code class="language-html">&lt;main&gt;
  &lt;h1&gt;오늘의 상품&lt;/h1&gt;
  &lt;img src=&quot;large-product.jpg&quot; alt=&quot;상품&quot;&gt;
  &lt;section id=&quot;list&quot;&gt;&lt;/section&gt;
&lt;/main&gt;</code></pre>
<ol>
<li>Chrome DevTools Performance에서 새로고침을 기록한다.</li>
<li>Layout Shifts와 Main thread의 긴 task를 확인한다.</li>
<li>이미지에 실제 비율과 맞는 <code>width</code>, <code>height</code>를 준다.</li>
<li>1,000개 항목을 한 번에 넣는 버전과 50개만 넣는 버전을 비교한다.</li>
<li>DOM 수, CLS, scripting·layout 시간을 표로 남긴다.</li>
</ol>
<p>목표는 무조건 좋은 점수를 만드는 것이 아니라, 어떤 코드 변경이 어떤 렌더링 단계를 바꿨는지 한 문장으로 설명하는 것이다.</p>
<h2 id="공부하면서-확인할-질문">공부하면서 확인할 질문</h2>
<ul>
<li>DOM과 렌더 트리는 왜 같은 구조가 아닐까?</li>
<li><code>display: none</code>인 요소와 <code>visibility: hidden</code>인 요소는 렌더링 과정에서 어떻게 다를까?</li>
<li><code>transform</code> 애니메이션이 <code>top</code>·<code>left</code> 변경보다 유리할 수 있는 이유는 무엇일까?</li>
<li>CLS가 0이어도 첫 클릭이 느릴 수 있는 이유는 무엇일까?</li>
<li>노드 수를 줄였는데 성능이 그대로라면 다음에는 어떤 기록을 볼까?</li>
</ul>
<h2 id="공식-참고자료">공식 참고자료</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Critical_rendering_path">MDN — Critical rendering path</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/How_browsers_work">MDN — How browsers work</a></li>
<li><a href="https://web.dev/articles/optimize-long-tasks">web.dev — Optimize long tasks</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI가 코드를 다 짜주는 시대, 나는 뭘 공부해야 할까]]></title>
            <link>https://velog.io/@kt_gml/AI%EA%B0%80-%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%8B%A4-%EC%A7%9C%EC%A3%BC%EB%8A%94-%EC%8B%9C%EB%8C%80-%EB%82%98%EB%8A%94-%EB%AD%98-%EA%B3%B5%EB%B6%80%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@kt_gml/AI%EA%B0%80-%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%8B%A4-%EC%A7%9C%EC%A3%BC%EB%8A%94-%EC%8B%9C%EB%8C%80-%EB%82%98%EB%8A%94-%EB%AD%98-%EA%B3%B5%EB%B6%80%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Thu, 30 Jul 2026 07:48:54 GMT</pubDate>
            <description><![CDATA[<p>우선 지난 학기는 계절학기까지 정말 바쁘게 살았다.</p>
<ul>
<li>ADSP 취득</li>
<li>평균학점 4.21/4.3 달성</li>
<li>해달 해커톤 인기상 수상</li>
<li>CODE-MEDI 해커톤 최우수상 수상</li>
<li>자료구조 / 자료구조응용 / 컴퓨터구조 / 운영체제 / 알고리즘 실습 / SW진로설계 / 프로그래밍기초 한번에 수강 </li>
<li>각종 대회(모의투자 및 실전투자대회 4건 등) 및 세미나 참여 </li>
<li>카카오테크캠퍼스 4기 Agentic AI 과정 합격 후 1학기 수료</li>
<li>개발 동아리, 사진 동아리, 과생활, 벌크업 등등.....</li>
</ul>
<p>가장 어려운 과목들을 몰아듣고, 대외활동도 열심히 한만큼 정말 바쁜 1학기였다.</p>
<p>뿌듯하게 학기를 마치고 방학에 게임 제작, 각종 공모전 출전, 토이 프로젝트 등을 진행하면서 드는 생각을 적어본다.</p>
<p>요즘 가장 흔들리는 질문은 이것이다.</p>
<blockquote>
<p>AI가 코드를 이렇게 잘 짜는데, 지금 언어와 자료구조를 공부하는 게 맞을까?</p>
</blockquote>
<p>나도 AI에게 프로젝트를 맡겨 보면 놀랄 때가 많다. 화면을 만들고, 테스트를 추가하고, 배포 설정까지 이어 간다. 그러다 실행이 안 되는 한 줄을 만나면 더 크게 당황한다.</p>
<p>코드는 받았는데 왜 틀렸는지 설명하지 못하고, 어디까지 고쳐도 되는지 판단하지 못하면 속도는 빨라졌는데 실력은 남지 않는다.</p>
<p>그래서 내 결론은 “코딩을 덜 공부하자”가 아니다.</p>
<blockquote>
<p>문법 암기의 비중은 줄이고, 코드를 판단하고 검증하고 운영하는 공부를 늘리자.</p>
</blockquote>
<h2 id="ai가-잘-짜-준-코드가-터졌을-때-남는-사람">AI가 잘 짜 준 코드가 터졌을 때 남는 사람</h2>
<p>AI가 정상 경로를 빠르게 만드는 능력은 이미 좋다. 하지만 실제 프로젝트에서는 이런 문제가 남는다.</p>
<ul>
<li>같은 요청이 두 번 들어오면 결제가 두 번 되는가?</li>
<li>localStorage가 중간에 깨지면 사용자의 저장은 복구되는가?</li>
<li>이미지가 12MB인데 화면이 뜨기만 하면 끝인가?</li>
<li>외부 문서 안의 prompt injection이 도구 호출로 이어지는가?</li>
<li>테스트는 통과하지만 요구사항 자체를 잘못 이해한 것은 아닌가?</li>
</ul>
<p>이 질문은 “코드를 생성할 수 있나”보다 “시스템이 왜 이렇게 동작하나”에 가깝다.</p>
<p>운영체제, 네트워크, DB, 보안, 테스트를 알아야 AI가 만든 코드의 위험한 부분이 보인다. 컴퓨터 구조를 배우는 이유도 CPU 시험 문제를 맞히기 위해서만은 아니다. 캐시, 메모리 접근, 병렬성, ISA의 차이가 실제 성능과 런타임 선택으로 이어지는 지점을 보기 위해서다.</p>
<h2 id="내가-쌓을-다섯-층">내가 쌓을 다섯 층</h2>
<h3 id="1층-python으로-문제를-끝까지-푸는-힘">1층: Python으로 문제를 끝까지 푸는 힘</h3>
<p>코딩테스트 언어는 Python으로 고정할 생각이다.</p>
<p>언어를 여러 개 얕게 돌기보다 아래 범위를 반복한다.</p>
<ul>
<li>list, dict, set, heap, deque</li>
<li>정렬, 이분 탐색, 누적 합, 투 포인터</li>
<li>DFS/BFS, 최단 경로, union-find</li>
<li>greedy와 dynamic programming</li>
<li>시간·공간 복잡도 설명</li>
</ul>
<p>목표는 “문제를 많이 풀었다”가 아니라 풀이 뒤에 세 문장을 말할 수 있는 상태다.</p>
<pre><code class="language-text">왜 이 자료구조인가?
시간 복잡도는 얼마인가?
어떤 입력에서 깨지는가?</code></pre>
<p>주 5문제를 기준으로 잡되, 새 문제 3개와 복습 2개로 나눈다. 틀린 문제는 코드만 저장하지 않고 실패 원인과 다시 볼 날짜를 GitHub Issue에 남긴다.</p>
<h3 id="2층-컴퓨터-구조·운영체제를-코드와-연결하기">2층: 컴퓨터 구조·운영체제를 코드와 연결하기</h3>
<p>이번에 배운 RISC, CISC, EPIC 글을 쓰는 이유도 같다. 개념을 외우는 데서 끝내지 않고 최신 CPU와 OS까지 연결해 보려고 한다.</p>
<ul>
<li>ISA와 microarchitecture 구분</li>
<li>pipeline, branch prediction, cache locality</li>
<li>process, thread, context switch</li>
<li>virtual memory, page fault, mmap</li>
<li>lock, race condition, atomicity</li>
<li>file descriptor, socket, system call</li>
</ul>
<p>작은 실험도 같이 한다.</p>
<ul>
<li>Python list 순회와 NumPy 연산의 차이 측정</li>
<li>sequential access와 random access 비교</li>
<li>thread와 process의 CPU-bound 차이</li>
<li>파일을 한 번에 읽기와 streaming 비교</li>
</ul>
<p>개념 글 하나, 실험 코드 하나, 측정 결과 하나를 묶으면 면접에서도 “배웠다”보다 강한 증거가 된다.</p>
<h3 id="3층-백엔드와-데이터">3층: 백엔드와 데이터</h3>
<p>AI 프로젝트도 결국 사용자의 요청, 상태, 권한, 실패를 다루는 서비스다.</p>
<p>Python 기반으로 다음 조합을 깊게 가져가려고 한다.</p>
<ul>
<li>FastAPI</li>
<li>Pydantic</li>
<li>pytest</li>
<li>PostgreSQL과 SQL</li>
<li>Redis의 캐시·rate limit·queue 사용</li>
<li>Docker와 Linux 배포</li>
</ul>
<p>CRUD만 만드는 데서 끝내지 않고 아래를 프로젝트마다 하나씩 증명한다.</p>
<ul>
<li>idempotency</li>
<li>transaction</li>
<li>index 전후 query plan</li>
<li>pagination</li>
<li>timeout과 retry</li>
<li>migration과 rollback</li>
<li>structured logging</li>
</ul>
<h3 id="4층-ai를-제품으로-만드는-능력">4층: AI를 제품으로 만드는 능력</h3>
<p>모델 API를 한 번 호출하는 것과 AI 시스템을 만드는 것은 다르다.</p>
<ul>
<li>RAG 검색 품질</li>
<li>tool calling 계약</li>
<li>eval dataset</li>
<li>hallucination과 fallback</li>
<li>token·latency·cost</li>
<li>prompt injection과 권한</li>
<li>deterministic path와 model path 분리</li>
</ul>
<p>KB AI Challenge에서는 금융처럼 틀렸을 때 비용이 큰 도메인을 경험하고, 카카오테크캠퍼스 Agentic AI 과정에서는 도구 사용과 평가를 더 체계적으로 익히고 싶다.</p>
<p>모델 이름을 포트폴리오 전면에 쓰기보다 다음 숫자를 남길 것이다.</p>
<pre><code class="language-text">task success rate
tool-call accuracy
p50 / p95 latency
request당 token과 비용
fallback 발생률
위험 행동 차단 test 통과율</code></pre>
<h3 id="5층-읽히는-증거-만들기">5층: 읽히는 증거 만들기</h3>
<p>GitHub에 프로젝트가 많아도 채용 담당자가 무엇을 봐야 하는지 모르면 약하다.</p>
<p>그래서 README와 Velog의 역할을 나누려고 한다.</p>
<ul>
<li>README: 문제, 내 역할, 구조, 실행, 측정 결과</li>
<li>docs: 의사결정, 트러블슈팅, 실험 기록</li>
<li>Issue: 주간 학습 목표와 회고</li>
<li>Velog: 한 가지 판단을 깊게 설명</li>
<li>Demo: 3분 안에 핵심 흐름을 보여 줌</li>
</ul>
<p>“기술을 사용했다”보다 “왜 선택했고 무엇이 바뀌었는가”를 남기는 게 목표다.</p>
<h2 id="2026-하반기부터-1년-계획">2026 하반기부터 1년 계획</h2>
<h3 id="2026-여름-kb-ai-challenge">2026 여름: KB AI Challenge</h3>
<p>산출물 목표는 모델 결과만이 아니다.</p>
<ul>
<li>baseline과 개선 모델 비교</li>
<li>금융 안전 규칙</li>
<li>prompt·dataset 버전</li>
<li>offline eval</li>
<li>오류 사례 20개 이상 분류</li>
<li>데모와 3분 설명</li>
</ul>
<p>Challenge가 끝나면 성과를 “참가” 한 줄로 끝내지 않고, 공개 가능한 설계와 실패 사례를 글 2편으로 남긴다.</p>
<h3 id="2026-하반기-카카오테크캠퍼스-agentic-ai--종합설계">2026 하반기: 카카오테크캠퍼스 Agentic AI + 종합설계</h3>
<p>두 활동을 따로 놀게 하지 않을 생각이다.</p>
<p>Agentic AI에서 배운 tool calling, evaluation, security를 종합설계에 적용한다. 종합설계는 가능하면 연구실 또는 기업 연계 주제로 선택하고, 다음 중 하나를 분명히 잡는다.</p>
<ul>
<li>source-aware RAG</li>
<li>금융·의료처럼 규칙이 중요한 agent</li>
<li>온디바이스 또는 local-first AI</li>
<li>agent observability와 eval</li>
</ul>
<p>결과물은 논문 흉내보다 재현 가능한 실험이 우선이다.</p>
<pre><code class="language-text">가설 → baseline → 변경 → 측정 → 실패 분석</code></pre>
<h3 id="2026-겨울-해외-인턴·해외-탐방">2026 겨울: 해외 인턴·해외 탐방</h3>
<p>해외 경험도 “다녀왔다”보다 역할이 중요하다.</p>
<p>지원 전까지 영어 1분 자기소개, 프로젝트 3분 설명, 기술 질문 20개 답변을 준비한다. 
GitHub README 핵심 프로젝트 3개는 영문 요약을 붙인다.</p>
<p>교내 및 학과에서 지원하는 해외인턴 프로그램에 도전한다.</p>
<p>11월 초에는 경북대학교 대표 5명으로 선발되어 중국 청두의 여러 기관들을 탐방하는것이 계획되어있다.</p>
<h3 id="2027-상반기-인턴과-취업용-압축">2027 상반기: 인턴과 취업용 압축</h3>
<p>프로젝트를 더 늘리는 대신 대표 3개를 고친다.</p>
<ol>
<li>금융 agent: 안전한 tool use와 eval</li>
<li>종합설계: 연구·기업 문제와 측정</li>
<li>제품 프로젝트: 실제 사용자 UX와 성능</li>
</ol>
<p>각 프로젝트는 테스트, 배포, 문서, 장애 대응까지 갖춘 상태로 만든다.</p>
<h2 id="자격증은-무엇을-준비할까">자격증은 무엇을 준비할까</h2>
<p>우선순위는 다음과 같다.</p>
<ol>
<li><strong><del>SQLD/ADSP</del></strong>: DB·SQL 기본기를 증명하면서 백엔드 공부와 직접 연결</li>
<li><strong>정보처리기사</strong>: 국내 대기업·공기업 지원 범위를 넓히는 안전장치</li>
<li><strong>토익 갱신 / 오픽 AL</strong>: 해외 인턴 지원 자격과 면접 준비를 위해 필요한 수준 확보</li>
</ol>
<p>클라우드 자격증은 AWS 서비스를 실제 프로젝트에서 운영해 본 뒤 하나만 선택한다. 자격증 공부가 프로젝트의 Linux, Docker, 네트워크, DB 시간을 침범하면 미룬다.</p>
<p>스타트업·대기업 개발 직무가 우선이라면 자격증보다 코딩테스트, CS, 프로젝트 설명력이 앞선다. 공기업 가능성까지 열어 두고 싶을 때 정보처리기사와 영어가 보험 역할을 한다.</p>
<h2 id="프로젝트를-평가하는-내-점수표">프로젝트를 평가하는 내 점수표</h2>
<p>새 프로젝트를 시작하기 전 아래 질문에서 4개 이상 “예”가 아니면 보류한다.</p>
<ul>
<li>실제 사용자가 겪는 문제가 있는가?</li>
<li>기존 내 프로젝트와 다른 기술적 난점이 있는가?</li>
<li>전후를 측정할 수 있는가?</li>
<li>테스트로 고정할 계약이 있는가?</li>
<li>공개 가능한 데모나 문서가 남는가?</li>
<li>면접에서 5분 이상 깊게 설명할 수 있는가?</li>
</ul>
<p>AI 덕분에 프로젝트를 시작하는 비용은 낮아졌다. 그래서 더 중요한 것은 시작 개수가 아니라 완성 기준이다.</p>
<h2 id="느낀-점">느낀 점</h2>
<p>AI가 코드를 다 짜 준다는 말은 반은 맞다. 초안을 만드는 속도는 이미 사람이 따라가기 어렵다.</p>
<p>하지만 무엇을 만들지, 어떤 결과를 믿을지, 실패했을 때 어디까지 허용할지, 측정 없이 무엇을 주장하면 안 되는지는 여전히 사람의 몫이다.</p>
<p>내가 준비할 방향은 “AI보다 코드를 빨리 치는 사람”이 아니다.</p>
<blockquote>
<p>AI가 만든 코드를 시스템 안에서 검증하고, 안전하게 연결하고, 결과를 숫자로 설명하는 사람.</p>
</blockquote>
<p>앞으로 1년은 새 기술 이름을 수집하는 시기가 아니라 이 능력을 증거로 바꾸는 시기로 만들려고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LLM이 돈을 움직이지 않게 설계하기]]></title>
            <link>https://velog.io/@kt_gml/LLM%EC%9D%B4-%EB%8F%88%EC%9D%84-%EC%9B%80%EC%A7%81%EC%9D%B4%EC%A7%80-%EC%95%8A%EA%B2%8C-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0-%EA%B2%B0%EC%A0%95%EB%A1%A0-%EA%B7%9C%EC%B9%99%EA%B3%BC-%EC%8A%B9%EC%9D%B8-%ED%86%A0%ED%81%B0</link>
            <guid>https://velog.io/@kt_gml/LLM%EC%9D%B4-%EB%8F%88%EC%9D%84-%EC%9B%80%EC%A7%81%EC%9D%B4%EC%A7%80-%EC%95%8A%EA%B2%8C-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0-%EA%B2%B0%EC%A0%95%EB%A1%A0-%EA%B7%9C%EC%B9%99%EA%B3%BC-%EC%8A%B9%EC%9D%B8-%ED%86%A0%ED%81%B0</guid>
            <pubDate>Wed, 29 Jul 2026 06:34:36 GMT</pubDate>
            <description><![CDATA[<p>2026 KB AI Challenge를 준비하면서 시니어 사용자가 음성과 텍스트로 금융 업무를 이용할 수 있는 에이전트를 만들고 있다.</p>
<p>처음 아이디어를 생각했을 때 가장 먼저 떠오른 기능은 자연어 송금이었다.</p>
<pre><code class="language-text">&quot;딸에게 30만원 보내줘.&quot;</code></pre>
<p>사용자가 계좌번호와 메뉴를 찾지 않아도 말로 송금할 수 있다면 편리할 것 같았다. 하지만 금융 서비스에서 편리함만 생각할 수는 없었다.</p>
<p>LLM은 같은 입력에도 결과가 달라질 수 있고, 잘못된 계좌나 금액을 추정할 수 있다. 프롬프트를 아무리 자세하게 작성해도 모델의 판단만으로 실제 돈을 움직이게 하는 것은 위험했다.</p>
<p>그래서 프로젝트의 기준을 다음 한 문장으로 정했다.</p>
<blockquote>
<p>AI는 사용자의 말을 이해하고 설명하지만, 돈을 움직이는 결정은 사용자가 확인한 규칙과 서버 코드가 담당한다.</p>
</blockquote>
<p>이번 글에서는 자연어를 이해하는 AI와 금융 실행을 통제하는 코드를 어떻게 분리했는지 정리하려고 한다.</p>
<hr>
<h2 id="1-llm이-잘-판단하게-만드는-것만으로는-부족했다">1. LLM이 잘 판단하게 만드는 것만으로는 부족했다</h2>
<p>처음에는 프롬프트에 금융 사기 사례와 주의사항을 많이 넣으면 모델이 위험한 송금을 잘 찾아낼 수 있을 것이라고 생각했다.</p>
<p>하지만 프로젝트를 구체화하면서 세 가지 문제가 보였다.</p>
<h3 id="같은-입력에도-결과가-흔들릴-수-있다">같은 입력에도 결과가 흔들릴 수 있다</h3>
<p>LLM의 분류는 확률적이다. 정상 거래를 위험하다고 판단하거나, 위험 거래를 정상이라고 판단할 수 있다.</p>
<h3 id="모델이-근거를-만들어낼-수-있다">모델이 근거를 만들어낼 수 있다</h3>
<p>사용자가 말하지 않은 관계나 목적을 추정하면 그럴듯하지만 사실과 다른 설명이 만들어질 수 있다.</p>
<h3 id="모델-실패가-금융-실행-실패로-이어질-수-있다">모델 실패가 금융 실행 실패로 이어질 수 있다</h3>
<p>공급자 장애, timeout, JSON 형식 오류가 발생했을 때 송금 제어까지 함께 멈추거나 잘못된 상태로 진행되면 안 된다.</p>
<p>그래서 LLM의 성능을 높이는 것과 별개로, 모델이 틀려도 위험한 실행이 일어나지 않는 구조가 필요했다.</p>
<hr>
<h2 id="2-ai-규칙-실행의-역할을-나누기">2. AI, 규칙, 실행의 역할을 나누기</h2>
<p>전체 흐름을 단순화하면 다음과 같다.</p>
<pre><code class="language-text">사용자 음성·텍스트
  → AI 또는 결정론 파서가 거래안을 구조화
  → 결정론 머니룰이 ALLOW / HELD / BLOCKED 판정
  → 필요한 확인 단계 진행
  → 통과한 거래에만 일회용 승인 토큰 발급
  → 서버가 토큰을 한 번 소비한 뒤 모의 송금 실행</code></pre>
<p>각 구성요소의 역할을 명확하게 나누었다.</p>
<h3 id="ai가-하는-일">AI가 하는 일</h3>
<ul>
<li>사용자의 자연어에서 송금 의도와 슬롯 후보 찾기</li>
<li>자연어 규칙을 정해진 JSON 초안으로 변환</li>
<li>사용자에게 판정 이유와 다음 행동을 쉽게 설명</li>
<li>규칙을 만들 때 빠진 조건을 제안</li>
</ul>
<h3 id="ai가-하지-않는-일">AI가 하지 않는 일</h3>
<ul>
<li>규칙 자동 저장</li>
<li>송금 승인 토큰 발급</li>
<li>보류된 거래의 임의 해제</li>
<li>서버의 판정 덮어쓰기</li>
<li>실제 송금 실행</li>
</ul>
<h3 id="결정론-코드가-하는-일">결정론 코드가 하는 일</h3>
<ul>
<li>사용자가 확인한 규칙과 신고계좌 검사</li>
<li><code>ALLOW</code>, <code>HELD</code>, <code>BLOCKED</code> 판정</li>
<li>확인 단계와 승인 가능 여부 결정</li>
<li>승인 토큰 검증·소비</li>
<li>판정 근거를 규칙 ID와 사람이 만든 문장으로 반환</li>
</ul>
<p>AI가 만든 결과를 바로 실행하지 않고, 정해진 schema와 서버 규칙을 통과한 값만 다음 단계로 보낸다.</p>
<hr>
<h2 id="3-위험-점수만으로-돈을-막지-않은-이유">3. 위험 점수만으로 돈을 막지 않은 이유</h2>
<p>처음에는 위험 점수가 높으면 송금을 차단하는 방식이 단순해 보였다.</p>
<p>하지만 위험 신호는 항상 사기를 의미하지 않는다.</p>
<ul>
<li>처음 보내는 계좌</li>
<li>평소보다 큰 금액</li>
<li>여러 번 나누어 보내려는 요청</li>
<li>긴급하다는 표현</li>
</ul>
<p>이런 신호는 정상 거래에서도 나타날 수 있다. AI의 위험 점수만으로 돈을 막으면 정상 사용자가 자신의 금융 거래를 할 수 없게 된다.</p>
<p>그래서 위험 신호는 확인 강도를 높이는 데 사용하고, 강제 중단은 사용자가 미리 만든 규칙이나 신고계좌처럼 명시적인 근거가 있을 때만 수행하도록 했다.</p>
<pre><code class="language-text">위험 신호
  → 질문과 사람 확인을 강화

사용자 규칙 위반 / 신고계좌
  → 실행 중단</code></pre>
<p>모델이 사용자를 대신해 결정하는 것이 아니라, 사용자가 만든 안전 약속을 코드가 집행하는 구조다.</p>
<hr>
<h2 id="4-l0부터-l4까지-확인-단계를-나누기">4. L0부터 L4까지 확인 단계를 나누기</h2>
<p>모든 송금에 같은 수준의 마찰을 주면 정상 거래가 불편해진다. 반대로 모든 거래를 간단한 확인만으로 처리하면 위험 상황에서 충분한 보호가 되지 않는다.</p>
<p>그래서 거래 상황에 따라 확인 단계를 나누었다.</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>대표 상황</th>
<th>행동</th>
</tr>
</thead>
<tbody><tr>
<td>L0</td>
<td>등록 계좌·평소 금액</td>
<td>복창과 본인 확인 뒤 실행</td>
</tr>
<tr>
<td>L1</td>
<td>신규 계좌·일정 금액 이상</td>
<td>받는 사람과의 관계 질문</td>
</tr>
<tr>
<td>L2</td>
<td>위험 문구·여러 이상 신호</td>
<td>관계·목적 등 추가 질문</td>
</tr>
<tr>
<td>L3</td>
<td>신규 고액·분할 재시도</td>
<td>실행권을 주지 않고 사람 확인</td>
</tr>
<tr>
<td>L4</td>
<td>신고계좌·사용자 규칙 위반</td>
<td>규칙 근거를 보여주고 중단</td>
</tr>
</tbody></table>
<p>L1부터 L3는 AI가 돈을 차단하는 단계가 아니다. 필요한 확인 행동과 확인 주체를 강화하는 과정이다.</p>
<p>반면 L4는 사용자가 직접 정한 한도나 금지 수취인, 신고계좌처럼 명시적인 근거가 있기 때문에 승인 토큰 자체를 발급하지 않는다.</p>
<hr>
<h2 id="5-프론트엔드의-완료-상태를-믿지-않기">5. 프론트엔드의 완료 상태를 믿지 않기</h2>
<p>화면에서 사용자가 확인 버튼을 눌렀다는 사실만으로 송금을 실행하면 안 된다.</p>
<p>프론트엔드 상태는 조작되거나 오래된 탭에 남아 있을 수 있다. 네트워크 재시도로 같은 요청이 두 번 전달될 수도 있다.</p>
<p>그래서 서버가 일회용 승인 토큰을 발급하고, 실행할 때 다음을 검사하도록 했다.</p>
<ul>
<li>현재 거래와 연결된 토큰인가</li>
<li>필요한 질문과 사람 확인이 끝났는가</li>
<li>이미 사용된 토큰은 아닌가</li>
<li>규칙 판정이 바뀌지 않았는가</li>
</ul>
<p>토큰은 서버가 한 번 소비한 뒤 다시 사용할 수 없다.</p>
<pre><code class="language-text">화면에서 &quot;승인됨&quot;
        ≠
서버가 실행해도 되는 거래</code></pre>
<p>Agentic AI에서 중요한 것은 모델이 어떤 tool을 호출하는지뿐 아니라, tool이 실제로 실행될 수 있는 권한을 서버가 어떻게 통제하는지라는 생각이 들었다.</p>
<hr>
<h2 id="6-공급자-장애를-숨기지-않는-실행-모드">6. 공급자 장애를 숨기지 않는 실행 모드</h2>
<p>외부 LLM 공급자를 사용하는 기능은 네트워크와 API 상태에 영향을 받는다.</p>
<p>데모에서 온라인 AI가 실패했는데 결정론 응답으로 조용히 바뀌면 겉으로는 정상처럼 보일 수 있다. 하지만 어떤 엔진이 실제로 동작했는지 알 수 없고, 온라인 AI를 검증했다는 주장도 할 수 없다.</p>
<p>그래서 실행 모드를 세 가지로 구분했다.</p>
<h3 id="offline">Offline</h3>
<p>외부 LLM을 호출하지 않는다. 결정론 파서와 기본 안내로 전체 사용자 흐름을 재현한다.</p>
<h3 id="auto">Auto</h3>
<p>온라인 tool calling을 먼저 시도한다. 실패하면 결정론 경로로 폴백하지만 실제 실패 원인과 사용된 엔진 상태를 기록한다.</p>
<h3 id="live">Live</h3>
<p>시작할 때 실제 공급자 연결을 확인한다. 연결이 실패하면 시작 또는 요청을 실패 상태로 표시하고 offline 응답으로 위장하지 않는다.</p>
<p>이렇게 나누니 API 키가 없는 환경에서도 핵심 흐름을 재현하면서, 온라인 AI가 실제로 연결되었다는 주장과 오프라인 데모를 구분할 수 있었다.</p>
<hr>
<h2 id="7-기능-목록보다-평가-가능한-흐름-만들기">7. 기능 목록보다 평가 가능한 흐름 만들기</h2>
<p>대회 준비를 하다 보면 새로운 모델과 기능을 계속 추가하고 싶어진다.</p>
<p>파인튜닝, embedding 기반 RAG, 서버 STT까지 모두 넣으면 기술적으로 더 화려해 보일 수 있다. 하지만 실제로 완료하지 못한 기능을 README에 구현된 것처럼 적는 것은 피하고 싶었다.</p>
<p>그래서 먼저 다음 흐름을 끝까지 연결하는 것을 목표로 잡았다.</p>
<pre><code class="language-text">음성 또는 텍스트 입력
  → 자연어 규칙을 정해진 규칙으로 변환
  → 사용자가 복창 확인 후 저장
  → 정상 송금은 일반 확인
  → 의심 거래는 추가 질문·사람 확인
  → 규칙 위반·신고계좌는 실행 중단
  → 보호자에게 최소정보 알림
  → 대시보드에서 판정 근거와 평가 수치 확인</code></pre>
<p>그리고 골든셋으로 다음을 확인했다.</p>
<ul>
<li>자연어 규칙이 정해진 schema로 변환되는가</li>
<li>정상 거래를 과도하게 막지 않는가</li>
<li>사기·규칙 위반 거래에서 필요한 개입이 발생하는가</li>
<li>같은 입력에 결정론 판정이 재현되는가</li>
<li>사례 검색이 필요한 근거를 상위 결과에서 찾는가</li>
</ul>
<p>모델의 답변을 몇 번 눈으로 보는 것보다, 실패 유형을 정하고 같은 기준으로 반복 평가할 수 있는 구조가 더 중요했다.</p>
<hr>
<h2 id="8-구현한-것과-준비만-한-것을-구분하기">8. 구현한 것과 준비만 한 것을 구분하기</h2>
<p>현재 구현 범위를 정리하면서 표현을 특히 조심했다.</p>
<p>예를 들어 사례 검색은 현재 정해진 사례에서 관련 내용을 찾는 retrieval 중심 프로토타입이다. 검색 문서를 LLM에 넣어 새로운 답변을 생성하는 generation-grounded RAG까지 완성한 것은 아니다.</p>
<p>파인튜닝도 마찬가지다. 학습 코드와 설정, 소량의 SFT 데이터를 준비했지만 실제 GPU 학습과 학습 전후 성능 비교가 끝나지 않았다면 &quot;파인튜닝 완료&quot;라고 쓸 수 없다.</p>
<pre><code class="language-text">코드가 존재함
    ≠
학습·연결·평가가 완료됨</code></pre>
<p>이 경계를 README와 기술 설명서에 명확하게 남기는 것이 결과물을 덜 화려하게 보이게 할 수는 있다. 그래도 다른 사람이 저장소를 보고 5분 안에 실제로 재현할 수 있는 범위를 정확하게 알려주는 편이 더 중요하다고 생각했다.</p>
<hr>
<h2 id="9-이번-프로젝트에서-배운-점">9. 이번 프로젝트에서 배운 점</h2>
<p>이번 프로젝트를 시작하기 전에는 Agentic AI의 핵심을 tool을 많이 연결하고 모델이 스스로 판단하게 만드는 것이라고 생각했다.</p>
<p>지금은 권한의 경계를 설계하는 일이 더 중요하다고 생각한다.</p>
<pre><code class="language-text">LLM은 자연어에 강하지만 금융 실행 권한을 갖지 않는다.
결정론 코드는 예측 가능하지만 사용자 의도를 이해하지 못한다.
사용자는 최종 규칙과 중요한 거래를 확인한다.
서버는 그 확인이 실제 실행 조건을 만족하는지 검증한다.</code></pre>
<p>각 구성요소가 잘하는 일을 맡고, 한 구성요소의 실패가 바로 금융 실행으로 이어지지 않도록 경계를 만드는 것이 이번 프로젝트의 핵심이었다.</p>
<hr>
<h2 id="이번-프로젝트의-kpt">이번 프로젝트의 KPT</h2>
<h3 id="keep">Keep</h3>
<ul>
<li>AI와 결정론 코드의 역할을 문서와 코드에서 함께 구분한 점</li>
<li>외부 API 키 없이도 전체 흐름을 재현할 수 있게 만든 점</li>
<li>기능을 추가하기 전에 골든셋과 완료 기준을 먼저 정한 점</li>
<li>구현하지 않은 범위를 미완료라고 명확하게 표시한 점</li>
</ul>
<h3 id="problem">Problem</h3>
<ul>
<li>음성 인식과 실제 사용자 테스트가 아직 충분하지 않다.</li>
<li>합성 데이터 중심이라 실제 시니어 사용자의 표현을 모두 반영하지 못했다.</li>
<li>온라인 모델의 장애·회복 시나리오를 더 다양한 조건에서 검증할 필요가 있다.</li>
</ul>
<h3 id="try">Try</h3>
<ul>
<li>시니어 사용자 과업 테스트와 음성 실패 사례 수집</li>
<li>정상 거래 오차단률과 개입 단계별 과업 성공률 측정</li>
<li>공급자 timeout·잘못된 tool arguments·재시도 상황을 자동 테스트로 고정</li>
<li>대회 이후 공개 가능한 평가 코드와 합성 데이터 정리</li>
</ul>
<hr>
<blockquote>
<p>이 글은 2026 KB AI Challenge 제출 이후 공개 가능한 범위를 다시 확인하고 발행할 예정이다. 현재는 프로젝트의 설계 과정과 배운 점을 정리한 초안이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[검색 함수 하나면 되는 줄 알았는데 ChromaDB, SQLite, 대화 기억을 나눈 이유]]></title>
            <link>https://velog.io/@kt_gml/%EA%B2%80%EC%83%89-%ED%95%A8%EC%88%98-%ED%95%98%EB%82%98%EB%A9%B4-%EB%90%98%EB%8A%94-%EC%A4%84-%EC%95%8C%EC%95%98%EB%8A%94%EB%8D%B0-ChromaDB-SQLite-%EB%8C%80%ED%99%94-%EA%B8%B0%EC%96%B5%EC%9D%84-%EB%82%98%EB%88%88-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@kt_gml/%EA%B2%80%EC%83%89-%ED%95%A8%EC%88%98-%ED%95%98%EB%82%98%EB%A9%B4-%EB%90%98%EB%8A%94-%EC%A4%84-%EC%95%8C%EC%95%98%EB%8A%94%EB%8D%B0-ChromaDB-SQLite-%EB%8C%80%ED%99%94-%EA%B8%B0%EC%96%B5%EC%9D%84-%EB%82%98%EB%88%88-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Wed, 29 Jul 2026 06:16:19 GMT</pubDate>
            <description><![CDATA[<p>카카오테크캠퍼스 Agentic AI 프로젝트에서 개인 메이트 <code>나나(Nana)</code>의 기억 검색 기능을 구현했다.</p>
<p>지난주까지는 사용자의 자연어 요청을 구조화하고 SQLite에 저장한 뒤 다시 조회·수정·삭제하는 흐름을 만들었다. 이번에는 저장된 정보를 검색해 답변의 근거로 사용하는 RAG를 학습했다.</p>
<p>처음에는 검색 함수 하나를 만들고 질문을 넣으면 알아서 관련 정보를 찾아주는 구조를 생각했다.</p>
<p>하지만 실제 데이터는 한 곳에 있지 않았다.</p>
<ul>
<li>사용자가 미리 등록한 참고자료</li>
<li>SQLite에 저장한 일정·할 일·알림</li>
<li>예전에 나눈 채팅 대화</li>
</ul>
<p>세 데이터는 저장된 장소도 다르고 검색 방법도 달랐다. 같은 &quot;기억 검색&quot;이라고 부르더라도 하나의 함수로 뭉뚱그릴 수 없었다.</p>
<p>이번 글에서는 검색을 출처별 tool로 나누면서 알게 된 점과, 리뷰 피드백을 반영하며 audit log와 확정 데이터의 차이를 구분하게 된 과정을 정리하려고 한다.</p>
<hr>
<h2 id="1-rag는-하나의-마법-함수가-아니었다">1. RAG는 하나의 마법 함수가 아니었다</h2>
<p>RAG를 처음 접했을 때는 문서를 embedding한 다음 질문과 비슷한 내용을 찾아 LLM에게 전달하는 한 가지 흐름만 생각했다.</p>
<p>이번 과제에서 다룬 나나의 기억은 세 갈래였다.</p>
<pre><code class="language-text">개인 참고자료 ── ChromaDB Vector Search ─┐
                                          │
저장된 일정 ─── SQLite 조건 검색 ─────────┼─→ Agent가 출처에 맞는 tool 선택
                                          │
과거 대화 ───── SQLite → ChromaDB Sync ───┘</code></pre>
<p>각 출처에 다음 tool을 연결했다.</p>
<h3 id="개인-참고자료">개인 참고자료</h3>
<pre><code class="language-text">search_personal_references
  → PersonalReferenceStore
  → ChromaDB + OpenAI embedding</code></pre>
<p>미리 추가해 둔 참고자료를 의미 기반으로 검색한다. 질문의 단어가 문서와 완전히 같지 않아도 embedding 거리를 이용해 관련 내용을 찾을 수 있다.</p>
<h3 id="저장된-요청">저장된 요청</h3>
<pre><code class="language-text">search_saved_requests
  → AppSQLiteStore.search_saved_requests
  → SQLite 검색</code></pre>
<p>일정·할 일·알림처럼 이미 구조화된 데이터는 SQLite에 저장되어 있다. 이 데이터는 제목, 날짜와 요청 종류처럼 명확한 필드가 있으므로 모든 것을 벡터로 만들기보다 기존 저장소의 검색 기능을 사용하는 편이 자연스러웠다.</p>
<h3 id="과거-대화">과거 대화</h3>
<pre><code class="language-text">search_conversation_messages
  → ConversationRAGStore.sync_from_sqlite
  → ChromaDB search</code></pre>
<p>과거 대화 원문은 SQLite에 있지만 의미 기반으로 찾기 위해 검색 직전에 ChromaDB와 동기화한 후 벡터 검색을 실행한다.</p>
<p>처음에는 세 tool을 나누는 것이 코드만 늘리는 일처럼 보였다. 실제로 구현해 보니 출처별로 나누어야 어떤 데이터가 답변의 근거가 되었는지 설명할 수 있고, 저장소에 맞는 검색 방식을 선택할 수 있었다.</p>
<hr>
<h2 id="2-같은-검색이어도-반환-계약이-달랐다">2. 같은 검색이어도 반환 계약이 달랐다</h2>
<p>이번 과제에서는 tool마다 반환해야 하는 최상위 key가 정해져 있었다.</p>
<ul>
<li>참고자료 검색: <code>hits</code></li>
<li>저장 요청 검색: <code>rows</code></li>
<li>대화 검색: <code>hits</code>, <code>rows</code>, <code>context</code>, <code>rag_backend</code>, <code>sync</code></li>
</ul>
<p>이전 주차에는 tool 결과를 <code>ok</code>, <code>tool_name</code> 같은 공통 껍데기로 감싸 사용했다. 이번 계약은 <code>hits</code>와 <code>rows</code>가 top-level에 있어야 했다.</p>
<p>처음에는 기존 껍데기를 모두 버리고 과제에서 요구한 key만 남겨야 하나 고민했다. 그렇게 하면 이전 tool들과 응답 모양이 완전히 달라져 일관성이 깨질 것 같았다.</p>
<p>그래서 계약이 요구하는 key는 반드시 최상위에 두고, 기존의 <code>ok</code>와 <code>tool_name</code>도 옆에 유지하는 방식으로 정리했다.</p>
<pre><code class="language-json">{
  &quot;ok&quot;: true,
  &quot;tool_name&quot;: &quot;search_personal_references&quot;,
  &quot;hits&quot;: [
    {
      &quot;content&quot;: &quot;...&quot;,
      &quot;metadata&quot;: {
        &quot;title&quot;: &quot;...&quot;,
        &quot;tags&quot;: [&quot;...&quot;]
      }
    }
  ]
}</code></pre>
<p>참고자료 검색 결과도 store가 반환하는 모양과 tool 계약이 요구하는 모양이 달랐다. store는 <code>title</code>, <code>tags</code>를 hit의 최상위에 펼쳐 주었지만 tool 응답에서는 <code>metadata</code> 안에 들어가야 했다.</p>
<p>결국 store가 반환한 값을 그대로 노출하는 것이 아니라, tool이 외부에 제공하는 계약에 맞게 변환하는 계층이 필요했다.</p>
<hr>
<h2 id="3-현재-대화를-과거-기억처럼-검색하지-않기">3. 현재 대화를 과거 기억처럼 검색하지 않기</h2>
<p>대화 RAG에서 가장 신경 쓴 부분은 <strong>현재 대화 제외</strong>였다.</p>
<p>사용자가 방금 한 말을 검색 대상으로 바로 포함하면, agent가 현재 입력을 과거의 기억처럼 다시 가져올 수 있다.</p>
<p>예를 들어 사용자가 다음과 같이 말한다고 가정한다.</p>
<pre><code class="language-text">&quot;내일 병원 가는 거 기억해줘.&quot;</code></pre>
<p>현재 대화를 제외하지 않으면 검색 결과에서 방금 입력한 문장이 &quot;과거 대화에서 찾은 근거&quot;처럼 나타날 수 있다. 기능상 결과가 비슷해 보여도 출처의 의미가 잘못된다.</p>
<p>그래서 <code>conversation_id</code>가 별도로 주어지지 않은 경우 <code>current_session_scope()</code>에서 현재 대화 ID를 가져와 <code>exclude_conversation_id</code>로 전달하도록 했다.</p>
<p>반면 tool을 독립적으로 직접 호출하는 <code>DEFAULT_SESSION_SCOPE</code>에서는 제외할 현재 대화가 없기 때문에 조건을 다르게 처리했다.</p>
<p>이 부분을 따라가면서 현재 대화, 과거 대화, tool 직접 실행이 서로 다른 실행 문맥이라는 것을 배웠다.</p>
<hr>
<h2 id="4-검색-직전에-대화를-동기화하기">4. 검색 직전에 대화를 동기화하기</h2>
<p>과거 대화 원문은 SQLite에 저장되고, 의미 검색은 ChromaDB가 담당한다.</p>
<p>두 저장소가 따로 존재하면 새로운 대화가 SQLite에는 있지만 벡터 저장소에는 아직 없는 상황이 생길 수 있다.</p>
<p>그래서 <code>search_conversation_messages</code>는 검색 직전에 <code>sync_from_sqlite</code>를 호출한다.</p>
<pre><code class="language-text">SQLite 대화
   ↓ lazy sync
ChromaDB
   ↓ vector search
검색 결과</code></pre>
<p>동기화 결과에는 다음 정보가 포함됐다.</p>
<ul>
<li><code>upserted</code></li>
<li><code>skipped</code></li>
<li><code>deleted</code></li>
</ul>
<p>스모크 테스트에서 이 값들이 실제로 바뀌는 것을 확인했다. 저장과 검색 사이에 동기화 과정이 있다는 것을 코드로만 읽는 것보다 결과 숫자로 보는 것이 훨씬 이해하기 쉬웠다.</p>
<p>다만 검색할 때마다 동기화하는 방식이 데이터가 커져도 효율적인지는 별개의 문제다. 지금은 과제 규모에서 정확한 결과를 만드는 데 집중했지만, 이후에는 이벤트 기반 동기화나 변경분만 반영하는 구조도 비교해 보고 싶다.</p>
<hr>
<h2 id="5-top_k를-tool에서만-제한하면-충분할까">5. <code>top_k</code>를 tool에서만 제한하면 충분할까</h2>
<p>제출 후 리뷰에서 내부 helper의 <code>top_k</code> 처리에 대한 피드백을 받았다.</p>
<p>실사용 경로에서는 tool이 <code>top_k</code>를 안전한 범위로 보정한 뒤 helper를 호출했다. 그래서 현재 동작만 보면 문제가 없어 보였다.</p>
<p>하지만 내부 helper인 <code>search_conversation_message_rows</code>를 다른 코드가 직접 호출하면 보정되지 않은 값이 그대로 저장소에 전달될 수 있었다.</p>
<pre><code class="language-text">현재 경로가 안전함
        ≠
함수 자체가 항상 안전함</code></pre>
<p>그래서 helper에도 동일하게 <code>safe_limit</code>을 적용했다.</p>
<p>이 경험을 통해 안전장치를 호출하는 쪽 한 곳에만 둘지, 데이터를 실제로 사용하는 경계에도 둘지 생각하게 됐다. 재사용될 수 있는 내부 함수라면 함수 자체가 유효한 범위를 보장하는 편이 더 안전했다.</p>
<hr>
<h2 id="6-audit-log와-확정-일정은-다른-데이터였다">6. Audit Log와 확정 일정은 다른 데이터였다</h2>
<p>이번 리뷰에서 가장 많이 배운 부분이다.</p>
<p>호환용 통합 검색인 <code>search_nana_memory</code>는 저장된 일정의 근거를 <code>structured_requests</code>에서 가져오고 있었다.</p>
<p>처음에는 사용자가 입력한 요청이 모두 들어 있으니 근거로 사용해도 된다고 생각했다. 하지만 store를 다시 따라가 보니 <code>structured_requests</code>는 요청이 들어올 때마다 쌓이는 원본 audit log에 가까웠다.</p>
<p>같은 일정을 여러 번 요청하거나 수정하면 다음 문제가 생길 수 있다.</p>
<ul>
<li>같은 일정이 여러 행으로 중복됨</li>
<li>수정 전 값과 수정 후 값이 함께 검색됨</li>
<li>현재 확정된 일정이 무엇인지 판단하기 어려움</li>
</ul>
<p>반면 <code>schedules</code>는 정규화된 확정 일정이다. 일정이 수정되면 최신 상태로 갱신되고, 현재 사용자에게 보여줘야 하는 값 하나가 유지된다.</p>
<pre><code class="language-text">structured_requests
  → 사용자가 어떤 요청을 보냈는지 추적하는 audit log

schedules
  → 현재 확정된 일정 상태</code></pre>
<p>그래서 일정 검색 근거를 <code>find_schedules</code>로 변경했다.</p>
<p>두 테이블 모두 일정과 관련된 데이터를 갖고 있지만 목적은 달랐다. &quot;데이터가 있다&quot;는 이유만으로 검색 근거로 사용하지 않고, 어떤 질문에 답하기 위한 데이터인지 구분해야 했다.</p>
<hr>
<h2 id="7-직접-확인한-것">7. 직접 확인한 것</h2>
<p>이번 과제도 RAG와 ChromaDB가 익숙하지 않아 Claude Code의 도움을 많이 받았다.</p>
<p>대신 각 tool이 어느 저장소를 호출하고 어떤 형태로 결과를 돌려주는지는 직접 따라가려고 했다.</p>
<p>스모크 테스트에서는 다음을 확인했다.</p>
<ul>
<li>참고자료 검색 결과에 top-level <code>hits</code>가 있는가</li>
<li>저장기록 검색 결과에 top-level <code>rows</code>가 있는가</li>
<li>대화 검색에 <code>hits</code>, <code>rows</code>, <code>context</code>, <code>rag_backend</code>, <code>sync</code>가 있는가</li>
<li>참고자료의 <code>title</code>, <code>tags</code>가 <code>metadata</code> 안에 있는가</li>
<li>현재 대화가 검색 결과에서 제외되는가</li>
<li><code>top_k</code>가 helper를 직접 호출해도 제한되는가</li>
<li>통합 검색의 일정 근거가 audit log가 아니라 확정 일정에서 나오는가</li>
</ul>
<p>ChromaDB가 embedding을 만드는 내부 원리나 거리 함수에 대해서는 아직 모르는 부분이 많다. 현재는 distance가 작을수록 가까운 결과라는 정도와, 저장소·tool·응답 계약이 어떻게 연결되는지를 먼저 이해한 상태다.</p>
<hr>
<h2 id="8-이번-주에-바뀐-rag에-대한-생각">8. 이번 주에 바뀐 RAG에 대한 생각</h2>
<p>처음에는 RAG를 &quot;문서를 벡터 DB에 넣고 검색하는 기술&quot; 정도로 생각했다.</p>
<p>이번 과제를 마친 뒤에는 다음 요소를 함께 봐야 한다고 생각하게 됐다.</p>
<pre><code class="language-text">무엇을 검색하는가
어디에 저장되어 있는가
어떤 검색 방식이 맞는가
현재 문맥에서 제외할 데이터는 무엇인가
검색 결과를 어떤 계약으로 반환하는가
답변에 사용할 수 있는 최신 상태인가
출처를 설명할 수 있는가</code></pre>
<p>검색 정확도만큼 중요한 것은 출처와 데이터의 역할을 구분하는 일이었다.</p>
<p>앞으로는 현재 대화 제외와 <code>top_k</code> 보정을 스모크 테스트가 아니라 단위 테스트로 고정해 보고 싶다. 또한 검색된 결과가 최종 답변에 실제로 어떻게 포함되는지 agent trace를 더 자세히 확인할 생각이다.</p>
<hr>
<h2 id="이번-학습의-kpt">이번 학습의 KPT</h2>
<h3 id="keep">Keep</h3>
<ul>
<li>tool마다 실제로 호출하는 저장소를 따라가며 출처를 구분한 점</li>
<li>반환 JSON의 key를 스모크 테스트로 직접 확인한 점</li>
<li>리뷰 질문에 바로 답하지 않고 store를 다시 읽어 근거를 찾은 점</li>
</ul>
<h3 id="problem">Problem</h3>
<ul>
<li>embedding과 ChromaDB 내부 동작에 대한 이해가 아직 얕다.</li>
<li>호환용 통합 검색은 이전 trace가 없어 context 형식을 완전히 확신하기 어려웠다.</li>
<li>현재 대화 제외를 자동 테스트로 고정하지 못했다.</li>
</ul>
<h3 id="try">Try</h3>
<ul>
<li>현재 대화가 검색에서 빠지는지 fake store를 이용한 단위 테스트 작성</li>
<li>벡터 검색의 distance와 <code>top_k</code> 변화에 따른 결과 비교</li>
<li>검색 결과가 최종 답변의 어느 문장에 사용됐는지 trace로 확인</li>
<li>SQLite와 ChromaDB 동기화 비용 측정</li>
</ul>
<hr>
<h2 id="관련-학습-기록">관련 학습 기록</h2>
<ul>
<li><a href="https://github.com/TaeHuiKKIM/kakaotech-learning-log/issues/9">Week 08 — 구조화 결과 SQLite 저장/조회</a></li>
<li><a href="https://github.com/TaeHuiKKIM/kakaotech-learning-log/issues/10">Week 09 — 출처별 RAG tool</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[답변에서 저장까지 - Tool Call, Structured Output, SQLite로 이어진 3주]]></title>
            <link>https://velog.io/@kt_gml/%EB%8B%B5%EB%B3%80%EC%97%90%EC%84%9C-%EC%A0%80%EC%9E%A5%EA%B9%8C%EC%A7%80-Tool-Call-Structured-Output-SQLite%EB%A1%9C-%EC%9D%B4%EC%96%B4%EC%A7%84-3%EC%A3%BC</link>
            <guid>https://velog.io/@kt_gml/%EB%8B%B5%EB%B3%80%EC%97%90%EC%84%9C-%EC%A0%80%EC%9E%A5%EA%B9%8C%EC%A7%80-Tool-Call-Structured-Output-SQLite%EB%A1%9C-%EC%9D%B4%EC%96%B4%EC%A7%84-3%EC%A3%BC</guid>
            <pubDate>Wed, 29 Jul 2026 06:15:08 GMT</pubDate>
            <description><![CDATA[<p>카카오테크캠퍼스 1단계 프로젝트에서는 개인 메이트 <code>나나(Nana)</code>와 그룹 메이트 <code>카나(Kana)</code>를 만들고 있다.</p>
<p>처음 Agentic AI를 접했을 때는 LLM이 사용자의 말을 이해해서 일정을 알아서 저장해 주는 것이라고 단순하게 생각했다. 채팅창에서 &quot;내일 오후 3시에 팀 회의 잡아줘&quot;라고 말했을 때 자연스러운 답변이 돌아오면 기능이 잘 동작한다고 생각하기도 했다.</p>
<p>그런데 3주 동안 Tool Call, Structured Output, SQLite를 차례대로 붙여보니 실제 구조는 전혀 달랐다.</p>
<p>LLM은 일정을 직접 저장하지 않는다. 모델이 하는 일은 어떤 도구를 어떤 인자로 호출할지 결정하거나, 자연어를 프로그램이 읽을 수 있는 구조로 정리하는 것에 가깝다. 실제 실행과 저장은 우리가 작성한 코드와 데이터베이스가 담당한다.</p>
<pre><code class="language-text">사용자 자연어
  → LLM이 요청을 이해
  → Tool Call 또는 Structured Output 생성
  → Pydantic 스키마 검증
  → Python tool 실행
  → SQLite 저장
  → Tool Result를 모델에게 전달
  → 사용자에게 최종 답변</code></pre>
<p>이번 글에서는 이 흐름을 직접 구현하면서 무엇을 배웠고, 어떤 부분에서 생각이 바뀌었는지 정리하려고 한다.</p>
<hr>
<h2 id="1-모델의-답변과-실제-실행은-다른-일이었다">1. 모델의 답변과 실제 실행은 다른 일이었다</h2>
<p>첫 주에는 LangChain의 <code>@tool</code>을 사용해 개인 일정 생성·조회·삭제 기능을 구현했다.</p>
<ul>
<li><code>personal_create_schedule</code></li>
<li><code>personal_list_schedules</code></li>
<li><code>personal_delete_schedule</code></li>
</ul>
<p>사용자가 일정 생성을 요청하면 모델은 <code>personal_create_schedule</code>이라는 tool과 필요한 arguments를 만든다. 하지만 모델이 일정을 저장하는 것은 아니다. 실제 함수가 실행되어 저장소에 payload를 추가하고, 그 결과가 Tool Result로 다시 모델에게 전달된다.</p>
<p>처음에는 최종 답변만 자연스러우면 잘 동작한다고 생각했다. 하지만 Gradio의 trace JSON을 열어보니 답변과 실제 상태가 다를 수 있다는 것을 알게 됐다.</p>
<p>예를 들어 모델은 &quot;일정을 저장했어요&quot;라고 답할 수 있지만 다음과 같은 문제가 숨어 있을 수 있다.</p>
<ul>
<li><code>date</code>가 잘못 계산됨</li>
<li><code>attendees</code>가 누락됨</li>
<li>다른 대화의 일정이 함께 조회됨</li>
<li>삭제 tool이 잘못된 <code>schedule_id</code>를 받음</li>
<li>Tool Call은 만들어졌지만 저장소가 변경되지 않음</li>
</ul>
<p>그래서 이후부터는 최종 답변보다 먼저 아래 세 가지를 확인하게 됐다.</p>
<ol>
<li>어떤 <code>tool_name</code>이 선택됐는가</li>
<li>어떤 arguments가 전달됐는가</li>
<li>저장소의 실제 결과가 어떻게 바뀌었는가</li>
</ol>
<p>특히 일정이 대화마다 섞이지 않도록 현재 session 범위 안에서 생성·조회·삭제되게 처리하면서 scope가 왜 필요한지도 체감했다. 같은 저장소를 사용하더라도 어떤 사용자의 어느 대화에서 만들어진 데이터인지 구분하지 않으면, 기능은 동작해도 서비스로는 사용할 수 없기 때문이다.</p>
<hr>
<h2 id="2-자연어를-바로-실행하지-않고-구조화하기">2. 자연어를 바로 실행하지 않고 구조화하기</h2>
<p>다음 주에는 자연어 요청을 바로 tool로 실행하기보다 <code>StructuredRequest</code>라는 정해진 스키마로 변환했다.</p>
<p>일정 요청에는 생각보다 많은 정보가 들어간다.</p>
<pre><code class="language-text">제목 / 날짜 / 시작 시간 / 참석자 / 요청 종류 / 개인 또는 그룹 일정</code></pre>
<p>사람은 &quot;다음 주 화요일 세 시에 민수랑 회의&quot;라는 문장을 자연스럽게 이해하지만, 뒤쪽 코드가 이 문장을 그대로 처리하기는 어렵다. 표현이 조금만 달라져도 조건문이 계속 늘어날 수 있기 때문이다.</p>
<p>Pydantic 모델로 필드와 타입을 고정하면 뒤쪽 로직은 자연어 전체를 다시 해석하지 않고 정해진 값을 사용할 수 있다.</p>
<pre><code class="language-text">&quot;다음 주 화요일 세 시에 민수랑 회의&quot;

          ↓

kind: group_schedule
title: 회의
date: 2026-07-14
start_time: 15:00
members:
  - 민수</code></pre>
<p>처음에는 JSON 모양으로만 나오면 충분하다고 생각했다. 실제로는 상대 날짜와 분류 경계가 문제였다.</p>
<h3 id="상대-날짜는-기준일이-필요했다">상대 날짜는 기준일이 필요했다</h3>
<p>&quot;내일&quot;, &quot;다음 주 화요일&quot; 같은 말은 기준 날짜가 없으면 계산할 수 없다. 그래서 프롬프트에 <code>base_date</code>를 명확하게 전달하고, 기준일이 <code>2026-07-08</code>일 때 &quot;다음 주 화요일&quot;이 <code>2026-07-14</code>로 나오는지 직접 확인했다.</p>
<p>모델이 날짜를 잘 계산할 것이라고 믿는 것과, 기준을 주고 결과를 검증하는 것은 다른 일이었다.</p>
<h3 id="참석자가-있는데도-개인-일정으로-분류됐다">참석자가 있는데도 개인 일정으로 분류됐다</h3>
<p>참석자가 있는 일정인데도 결과의 <code>kind</code>가 가끔 개인 일정으로 나왔다. 모델이 먼저 선택한 tool 이름에 영향을 받아 그룹 일정을 개인 일정처럼 분류하는 경우가 있었다.</p>
<p>그래서 참석자가 한 명이라도 있으면 <code>group_schedule</code>로 판단하라는 규칙을 프롬프트에 추가했다. 수정 후에는 같은 시나리오를 세 번씩 실행해 결과가 흔들리지 않는지 확인했다.</p>
<p>한 번 성공한 결과는 모델의 안정성을 보장하지 않는다. 같은 입력을 반복해서 실행해 본 것이 이번 과제에서 가장 중요한 검증 중 하나였다.</p>
<h3 id="structured-output인데도-extra-data-오류가-났다">Structured Output인데도 <code>Extra data</code> 오류가 났다</h3>
<p>모델이 JSON 뒤에 &quot;일정 잡았어요&quot; 같은 문장을 붙이면 <code>StructuredOutputValidationError(&quot;Extra data&quot;)</code>가 발생했다.</p>
<p>처음에는 Structured Output을 사용했는데 왜 JSON 파싱 오류가 발생하는지 이해되지 않았다. 결국 구조화 데이터 뒤에 자연어가 추가되면서 스키마 검증을 방해한다는 것을 알게 됐다.</p>
<p>그래서 <code>ToolStrategy(StructuredRequestBatch)</code>를 사용해 구조화 결과를 tool call 인자로 받도록 바꾸었다. 이후 반복 실행에서 같은 오류가 발생하지 않는 것을 확인했다.</p>
<hr>
<h2 id="3-저장과-영속성은-다른-문제였다">3. 저장과 영속성은 다른 문제였다</h2>
<p>세 번째 주에는 구조화된 요청을 SQLite에 저장하고 다시 조회·수정·삭제하는 흐름을 붙였다.</p>
<p>1주차에는 일정이 메모리 저장소에 들어갔다. 앱을 켜둔 동안에는 잘 보였지만 프로세스를 다시 시작하거나 새 대화를 만들면 데이터가 사라질 수 있었다.</p>
<p>처음에는 &quot;리스트에 넣던 것을 DB에 넣으면 되는 것 아닌가&quot;라고 생각했다. 직접 해보니 이번 주차의 핵심은 저장 방식보다 <strong>영속성</strong>이었다.</p>
<pre><code class="language-text">현재 대화에서 저장됨
        ≠
앱을 다시 켜거나 새 대화를 시작해도 남아 있음</code></pre>
<p>구현한 주요 기능은 다음과 같다.</p>
<ul>
<li>구조화 요청 저장</li>
<li>저장된 요청 목록·단건 조회</li>
<li>일정 수정</li>
<li>조건을 이용한 삭제</li>
<li>레거시 payload 정규화</li>
</ul>
<h3 id="입력-검증-실행-저장의-역할을-나누기">입력 검증, 실행, 저장의 역할을 나누기</h3>
<p>이번에는 <code>@tool(args_schema=...)</code> 방식도 사용했다.</p>
<p>처음에는 tool 함수 안에서 값을 다시 Pydantic 모델로 만들어 한 번 더 검증해야 마음이 놓일 것 같았다. 하지만 코드를 따라가면서 역할이 다음처럼 나뉜다는 것을 이해했다.</p>
<pre><code class="language-text">Pydantic schema  → 입력 검증
Tool             → 실행 흐름
SQLite store     → 저장과 조회</code></pre>
<p>각 계층이 같은 검증을 반복하는 것이 아니라 자기 책임을 분명하게 갖는 구조였다.</p>
<h3 id="조건-없는-삭제는-막았다">조건 없는 삭제는 막았다</h3>
<p>가장 신경 쓴 부분은 삭제였다.</p>
<p><code>id</code>, 날짜, 제목처럼 삭제 대상을 좁히는 조건이 하나도 없는데 삭제 요청이 실행되면 저장된 일정이 전부 사라질 수 있다. 기능을 편하게 만드는 것보다 실수로 데이터를 통째로 지우지 못하게 만드는 것이 더 중요하다고 판단했다.</p>
<p>그래서 삭제 조건이 비어 있으면 아무것도 삭제하지 않도록 막았다.</p>
<p>이 경험을 통해 Agentic AI에서 안전장치는 모델에게 &quot;조심해서 실행해&quot;라고 말하는 것이 아니라, 위험한 동작이 코드 수준에서 실행되지 못하도록 만드는 것에 가깝다는 생각이 들었다.</p>
<hr>
<h2 id="4-새-대화를-만들어-직접-검증하기">4. 새 대화를 만들어 직접 검증하기</h2>
<p>SQLite 코드가 있다고 영속성이 증명되는 것은 아니다. 실제로 저장하고 새로운 대화에서 다시 읽히는지 확인해야 했다.</p>
<p>먼저 agent에 다음 요청을 보냈다.</p>
<pre><code class="language-text">&quot;내일 오전 10시에 개인 코칭 일정 저장해줘&quot;</code></pre>
<p>trace에서는 다음 순서로 tool이 호출됐다.</p>
<pre><code class="language-text">extract_schedule_request
  → save_structured_request
  → SQLite 저장</code></pre>
<p>그다음 agent를 새로 만들어 새 대화처럼 시작했다.</p>
<pre><code class="language-text">&quot;내가 저장한 일정 보여줘&quot;</code></pre>
<p>이전 대화에서 저장한 개인 코칭 일정이 그대로 조회되는 것을 확인했다. 단순히 DB 파일에 행이 생긴 것을 보는 것보다, 실제 사용자 흐름으로 저장과 조회가 연결되는 것을 확인했을 때 가장 뿌듯했다.</p>
<p>경계 조건도 함께 확인했다.</p>
<ul>
<li>없는 <code>id</code>를 조회했을 때 <code>row=None</code>이 나오는가</li>
<li>조건 없는 삭제가 거부되는가</li>
<li><code>None</code>인 필드가 불필요하게 저장되지 않는가</li>
<li>저장 → 목록 → 단건 조회 → 수정 → 삭제 순서가 모두 이어지는가</li>
</ul>
<hr>
<h2 id="5-ai를-사용하면서도-직접-확인하려고-한-것">5. AI를 사용하면서도 직접 확인하려고 한 것</h2>
<p>이번 과제들은 아직 Python과 LangChain이 익숙하지 않아 Claude Code의 도움을 많이 받았다.</p>
<p>AI가 제안한 코드를 활용한 것 자체를 숨기기보다, 그 코드를 어느 수준까지 이해하고 검증했는지가 더 중요하다고 생각했다.</p>
<p>그래서 다음 부분은 직접 확인하려고 했다.</p>
<ul>
<li>각 tool이 실제로 어떤 store 메서드를 부르는지 따라가기</li>
<li>반환 JSON과 trace의 key를 눈으로 확인하기</li>
<li>상대 날짜와 그룹 분류를 같은 입력으로 반복 실행하기</li>
<li>임시 DB에서 CRUD 흐름을 순서대로 실행하기</li>
<li>agent를 새로 만들어 영속성을 사용자 흐름으로 검증하기</li>
</ul>
<p>아직 Python 문법과 프레임워크 구조를 읽는 속도는 느리다. 다만 AI가 코드를 만들어주었다는 이유로 동작 원리를 건너뛰지 않고, 적어도 입력·실행·저장·결과가 어떻게 이어지는지는 설명할 수 있게 됐다.</p>
<hr>
<h2 id="6-3주-동안-달라진-생각">6. 3주 동안 달라진 생각</h2>
<p>처음에는 Agentic AI를 &quot;대답을 잘하는 챗봇&quot;에 가깝게 생각했다.</p>
<p>지금은 조금 다르게 보고 있다.</p>
<pre><code class="language-text">Agentic AI
= 자연어 이해
+ 구조화된 계약
+ 실제로 실행되는 코드
+ 저장소
+ 실패를 막는 안전장치
+ 결과를 확인하는 trace와 테스트</code></pre>
<p>자연스러운 답변은 사용자 경험에 중요하지만, 실제 데이터를 다루는 서비스에서는 tool arguments와 저장 결과가 더 먼저 검증되어야 한다.</p>
<p>앞으로는 tool 하나를 처음부터 끝까지 AI 도움 없이 구현해 보고 싶다. 그리고 현재는 스모크 테스트로 확인한 경계 조건들을 단위 테스트로 고정해, 나중에 코드를 수정해도 같은 문제가 다시 생기지 않도록 만들 생각이다.</p>
<hr>
<h2 id="이번-학습의-kpt">이번 학습의 KPT</h2>
<h3 id="keep">Keep</h3>
<ul>
<li>최종 답변보다 tool arguments와 저장소 결과를 먼저 확인한 점</li>
<li>한 번 성공한 결과를 믿지 않고 같은 시나리오를 반복 실행한 점</li>
<li>지난 회고에서 정한 &quot;작업 전 최신 main 반영&quot;을 실제 습관으로 옮긴 점</li>
</ul>
<h3 id="problem">Problem</h3>
<ul>
<li>Python과 LangChain이 익숙하지 않아 AI가 작성한 코드를 이해하는 데 시간이 많이 걸렸다.</li>
<li>Structured Output을 사용하면 항상 안정적인 결과가 나온다고 단순하게 생각했다.</li>
<li>스모크 테스트는 했지만 경계 조건을 자동으로 고정한 단위 테스트가 부족했다.</li>
</ul>
<h3 id="try">Try</h3>
<ul>
<li>tool 하나를 처음부터 끝까지 직접 구현하기</li>
<li>조건 없는 삭제와 현재 세션 범위를 단위 테스트로 고정하기</li>
<li>저장 결과가 최종 답변의 근거로 사용되는 과정까지 trace로 확인하기</li>
</ul>
<hr>
<h2 id="관련-학습-기록">관련 학습 기록</h2>
<ul>
<li><a href="https://github.com/TaeHuiKKIM/kakaotech-learning-log/issues/6">Week 06 — Agentic AI와 Tool Call</a></li>
<li><a href="https://github.com/TaeHuiKKIM/kakaotech-learning-log/issues/8">Week 07 — 자연어를 구조화된 요청으로 변환하기</a></li>
<li><a href="https://github.com/TaeHuiKKIM/kakaotech-learning-log/issues/9">Week 08 — 구조화 결과 SQLite 저장/조회</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2025년 정산 - SQLD 취득 / JSP 프로젝트 / 경진대회 수상 / 토익 편입 합격]]></title>
            <link>https://velog.io/@kt_gml/2025%EB%85%84-%EC%A0%95%EC%82%B0-SQLD-%EC%B7%A8%EB%93%9D%EA%B3%BC-%EC%88%98%EC%83%81-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%ED%86%A0%EC%9D%B5-%ED%8E%B8%EC%9E%85-4%EA%B4%80%EC%99%95</link>
            <guid>https://velog.io/@kt_gml/2025%EB%85%84-%EC%A0%95%EC%82%B0-SQLD-%EC%B7%A8%EB%93%9D%EA%B3%BC-%EC%88%98%EC%83%81-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%ED%86%A0%EC%9D%B5-%ED%8E%B8%EC%9E%85-4%EA%B4%80%EC%99%95</guid>
            <pubDate>Sat, 28 Feb 2026 17:34:10 GMT</pubDate>
            <description><![CDATA[<h1 id="2025년-정산">2025년 정산</h1>
<p>무휴학 편입 특성상 편입 직전의 집중도 있는 학습과 연속적인 편입 시험으로 너무 바빠서 한동안 벨로그를 작성하지 못했다.</p>
<p>그래서 지난 기간동안 어떤 성과를 냈는지 정리하고 넘어가려고 한다.</p>
<p>길고 길었던 편입 준비가 좋은 결과로 끝났고 새로운 학교에서는 백엔드와 더 깊이있는 코딩테스트 위주로 학습하면서 전공 공부에 매진할 생각이다. </p>
<p>앞으로는 배운 개념이나 이론 위주의 블로그 보다는 프로젝트나 학습하면서 느낀점들 위주로 작성해보려고 한다.</p>
<hr>
<h2 id="sqld-취득">SQLD 취득</h2>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/8c95e06d-36b5-45a5-97b0-d32bce65d9ad/image.png" alt=""></p>
<p>바쁜 와중에 <code>데이터베이스설계</code>와 <code>데이터관리</code> 수업을 듣고 SQL에 입문한 뒤 일주일 빠짝 공부해서 SQLD 취득에 성공했다.</p>
<p>어짜피 시험 범위였기에 미리 자격증을 따고 수월하게 A+를 받았다.</p>
<hr>
<h2 id="체육대전-농구-우승">체육대전 농구 우승</h2>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/bbe923c7-faa2-4a85-8100-563484f6b069/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/ce589da7-88e6-4aa2-8560-3acb3c913c8a/image.png" alt=""></p>
<p>경영학부의 에이스 선배들 덕분에 많이 뛰지도 않고 우승 트로피를 만졌다.</p>
<p>기여도 4%..? 경기 안뛸때는 벤치에서 응원 열심히 했다.</p>
<p>정작 열심히 한 배구는 예선 탈락했다는 사실..</p>
<hr>
<h2 id="교내-sw경진대회-장려상-수상">교내 SW경진대회 장려상 수상</h2>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/346770c5-73e7-4d7c-ad1c-3e63ab283a56/image.png" alt=""></p>
<p>Python, C++ 라이브러리를 활용하는 고학년들의 경쟁속이었다.</p>
<p>C랑 자료구조 알고리즘 공부하면서 백준 골드를 달성한 김에 C언어로 용감하게 대회에 신청하였다.</p>
<p>C언어로 큐를 직접 구현해가면서 문제를 푼 사람은 나밖에 없었던 것 같다.</p>
<p>어짜피 편입 시험 범위니까 공부한다는 생각으로 출전했는데 필요한 자료구조를 직접 구현하며 절반 정도 풀고나니 시간이 부족해서 더 풀 수가 없었다.</p>
<p>그래도 장려상이라도 받았으니 만족스럽다. </p>
<p>경북대학교에서는 Java로 전향하여 백준 플레티넘과 각종 알고리즘 대회 수상을 목표로 나아가려고한다.</p>
<hr>
<h2 id="시흥실록지리지로컬-창업-경진대회-장려상-수상">시흥실록지리지(로컬 창업 경진대회) 장려상 수상</h2>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/346fb4a4-08b8-455a-a5c5-478a4e4a45c2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/6c8ecbc2-c7ad-4029-8f24-41913d30096f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/9d300231-1bf8-4ec5-b3ae-4ad938eb19ce/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/2fa9fab8-062a-45f1-8993-77d6a8e9af29/image.png" alt=""></p>
<p>정말 많은 시간을 할애했지만 아쉽게도 가장 낮은 상을 수상하였다.</p>
<p>한국공학대학교라 그런건지 공학 관련 창업 아이템들이 상을 휩쓸었고 바이럴 마케팅이 메인 주제였던 우리의 서비스는 열띤 발표에도 불구하고 교수님들을 끝내 설득하지 못했다.</p>
<p>그래도 담당 공무원과 인터뷰를 잡고 직접 진행해보고, 각종 전문가들에게 문의하면서 견적내고, 각종 활동에서 여러 멘토님들도 만나면서 인사이트가 많이 넓어진것 같다. </p>
<p>또한 최고급 호텔 스위트룸에서 밤새 프로젝트에 몰두해보기도 하고, 창업 전문가들, 교수님들, 기자들, 수많은 학우들 앞에서 여러 차례 발표해볼 수 있는 좋은 경험이었다.</p>
<hr>
<h2 id="각종-대회-출전-경험">각종 대회 출전 경험</h2>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/fb1d2a0e-0090-48d0-8491-1bfe6b36feec/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/d5a45d94-3d18-405b-99b0-b23d98d29d7e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/082874b0-3bdd-4d7c-8a2c-a66a140f2e89/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/cb6c199c-0551-4c95-8648-03396bf61eb6/image.png" alt=""></p>
<p>창업 경진대회에 조원으로 참여하기도 하였고, 제주 공공데이터 활용 공모전에도 과감하게 출전하였으나 예선 탈락하였다.</p>
<hr>
<h2 id="jsp-쇼핑몰-프로젝트">JSP 쇼핑몰 프로젝트</h2>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/2777f37d-8ed1-4c53-bc6e-356fe26cbe13/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/f6aa2485-d37b-43ff-89a8-0dc8315fe1a9/image.png" alt=""></p>
<p><a href="https://github.com/TaeHuiKKIM/MERCI-JSP">GITHUB - MERCI JSP</a></p>
<p>JSP로 개발한 반응형 쇼핑몰 웹사이트 프로젝트이다.</p>
<p>상업적인 목적은 없으며 데이터베이스 연동 학습 목적으로 개발하였다.</p>
<p><a href="https://wellbeingexpress.com">https://wellbeingexpress.com</a></p>
<p>위 사이트의 메인 페이지 디자인을 참고하였으며 의류 이미지를 샘플 데이터로 사용하였다.</p>
<blockquote>
<p>*<em>구현한 기능 *</em>
물품 관리 - 등록/수정/삭제 - 품목 별 리뷰
고객 관리 - 회원가입 / 계정 삭제 / 로그인(소셜) 및 비밀번호 변경 - 찾기 / 상품 찜 기능 / 주소지 저장 및 불러오기 (도로명주소 API)
주문 관리 - 결제 확인 및 주문 완료 변경 / 리뷰 등록-수정-삭제
주문 및 결제(장바구니 및 대행사 활용) - AJAX 활용하여 사용 편의성 향상
문의 관리 - QNA 등록 수정 삭제
물품 카테고리별 나열 및 검색
소개 페이지 및 수정 페이지</p>
</blockquote>
<p>이외에도 디테일적인 부분들이 많고 결제는 대행사 테스트용 API를 활용했다.</p>
<hr>
<h2 id="2학년-수료-및-편입-시험-준비">2학년 수료 및 편입 시험 준비</h2>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/c811170e-a844-43cb-a6f3-2bacba13a229/image.png" alt=""></p>
<p>평균 평점 <strong>4.41</strong>로 <strong>한국공학대학교</strong> 2학년을 수료하였고 방학 동안은 편입 시험 공부에 집중하였다.</p>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/389a9e17-208d-448e-920b-f4133129c6c3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/7eb00f03-0831-4c88-94e8-c13a9b82aac8/image.png" alt=""></p>
<p><strong>레드 블랙 트리</strong>나 <strong>AVL 트리</strong>의 삭제 등 심화적인 부분까지 대비하였다. </p>
<p>처음엔 이해가 안갔는데 구현해본 코드를 외우고 설명할 정도까지 학습하고 나니까 이젠 쉽게 느껴진다.</p>
<p><strong>C++</strong>도 학습하며 STL 라이브러리의 활용과 각종 자료구조를 사용하는 코드를 구현하는걸 타이핑이 아니라 연필로 써가며 무한 반복하였다.</p>
<p>*<em>이산수학 / C / C++ / 자료구조 *</em>등 출제범위가 광범위하기에 내가 아무리 공부해도 모르는게 나와버리면 어떡하지라는 불안감속에서 끊임없이 반복학습하였다.</p>
<p>시험 직전에는 더 이상 공부할게 없었다.</p>
<p>이렇게까지 했는데 내가 못 풀면 남들도 못 푸는 문제다 라는 생각이 들 정도로 대비되어있었다. </p>
<p>다행히 큰 이변 없이 각종 면접과 시험에서 좋은 결과를 냈고 토익 편입으로 갈수 있는 모든 학교에 합격하였다.</p>
<p>그중 IT 분야 강자인 경북대학교로 진학하게 되었다.</p>
<hr>
<h2 id="앞으로는-">앞으로는 ?</h2>
<p>Java로 코딩테스트를 학습하면서 객체지향언어에 익숙해질것이다.</p>
<p>동시에 Spring을 깊이있게 학습해 대기업 백엔드 직군에 적합한 인재가 되어보려고 한다.</p>
<p>이 과정에서 바이브 코딩이나 openclaw 등을 활용하며 나의 깊은 지식과 AI의 생산성을 융합시켜 각종 대회에도 진출할 생각이다.</p>
<p>무엇보다도 동아리나 대외활동에 적극 참여하며 같은 분야에 속한 열정적인 학우들과 함께 성장하고싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UNIX] GDB(GNU Debugger) 사용법]]></title>
            <link>https://velog.io/@kt_gml/UNIX-GDBGNU-Debugger-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@kt_gml/UNIX-GDBGNU-Debugger-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Sat, 11 Oct 2025 13:52:47 GMT</pubDate>
            <description><![CDATA[<h1 id="gdbgnu-debugger-사용법">GDB(GNU Debugger) 사용법</h1>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/805e2085-51ee-4b13-89e8-674bcfad7e74/image.png" alt=""></p>
<p>코딩테스트 문제를 풀때 디버거를 잘 몰라서 printf와 같은 출력문을 반복문 사이사이에 넣어서 풀곤 했었다.</p>
<p>하지만 UNIX를 배우면서 <code>Make Utility</code>와 <code>GCC 컴파일러</code>에 대해 배우면서 <code>GDB</code>에 대해 배울 수 있었다.</p>
<p>이를 배우면서 흥미를 느껴 이에 대해 기록하고 앞으론 IDE에서도 디버거를 활용하려고한다.</p>
<hr>
<h2 id="디버거란-">디버거란 ?</h2>
<p><code>printf</code>를 코드 곳곳에 삽입하여 변수 값을 확인하는 방식은 간단하지만, 재귀와 같은 알고리즘이 쓰이면서 점점 복잡해지는 프로그램에서는 한계에 부딪힌다. </p>
<p>코드는 지저분해지고, 버그를 잡은 뒤에는 모든 <code>printf</code>를 다시 지워야 하는 번거로움도 존재한다.</p>
<p><code>GDB(GNU Debugger)</code>는 <code>리눅스(유닉스)</code> 환경에서 이런 원시적인 방법을 대체하는 강력한 디버깅 도구이다. </p>
<p>프로그램을 특정 지점에서 멈추게 하고, 변수 값을 실시간으로 들여다보며, 코드를 한 줄씩 실행하는 등 프로그램의 내부를 정밀하게 분석할 수 있게 해준다.</p>
<hr>
<h2 id="디버깅-정보-포함하여-컴파일하기">디버깅 정보 포함하여 컴파일하기</h2>
<p>GDB가 소스 코드의 내용을 제대로 인식하고 분석하려면, 컴파일할 때 <strong>디버깅 정보</strong>를 포함시켜야 한다. GCC 컴파일러에서 <code>-g</code> 옵션을 사용하면 된다.</p>
<pre><code class="language-bash"># -g 옵션을 추가하여 컴파일한다.
gcc -g -o factorial factorial.c</code></pre>
<p><code>-g</code> 옵션 없이 컴파일된 실행 파일은 GDB로 분석할 수는 있지만, 소스 코드의 몇 번째 줄인지 추적하거나 변수명을 확인하는 등의 핵심 기능을 제대로 사용할 수 없다. </p>
<p><strong>GDB</strong>를 사용하려면 <code>-g</code> 옵션을 꼭 사용해야한다.</p>
<hr>
<h2 id="버그가-있는-팩토리얼-프로그램">버그가 있는 팩토리얼 프로그램</h2>
<p>아래는 5의 팩토리얼(!5)을 계산하는 코드이지만 버그를 포함하고 있다. </p>
<p>5!는 120이어야 하지만, 이 프로그램은 잘못된 결과를 출력한다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

// n! (n 팩토리얼)을 계산하는 함수
int calculate_factorial(int n) {
    int result = 1;

    // 버그: i &lt;= n 이 되어야 하지만 i &lt; n 으로 되어있다.
    for (int i = 1; i &lt; n; i++) {
        result = result * i;
    }
    return result;
}

int main(void) {
    int number = 5;
    int result = 0;

    result = calculate_factorial(number);

    printf(&quot;%d의 팩토리얼 결과: %d\n&quot;, number, result);

    return 0;
}</code></pre>
<p>이 코드를 <code>-g</code> 옵션으로 컴파일하고 실행하면 <code>5의 팩토리얼 결과: 24</code>라는 오답이 나온다.</p>
<hr>
<h2 id="단계별-디버깅-과정">단계별 디버깅 과정</h2>
<p><strong>1) GDB 시작</strong>
터미널에 <code>gdb</code> 명령어와 함께 분석할 실행 파일명을 입력한다.</p>
<pre><code class="language-bash">gdb ./factorial</code></pre>
<p>GDB가 시작되면 <code>(gdb)</code> 프롬프트가 나타나며 명령어 입력을 기다린다.</p>
<p><strong>2) 브레이크포인트(Breakpoint) 설정</strong>
<strong>브레이크포인트</strong>는 프로그램 실행 중 의도적으로 멈추고 싶은 지점을 설정하는 것이다. </p>
<p>가장 먼저 <code>main</code> 함수가 시작되는 지점에 설정해보자.</p>
<pre><code class="language-gdb">(gdb) break main
Breakpoint 1 at 0x1169: file factorial.c, line 15.</code></pre>
<p><code>break [함수명]</code> 또는 <code>b [줄 번호]</code> 형식으로 설정할 수 있다.</p>
<p><strong>3) 프로그램 실행</strong>
<code>run</code> (축약형 <code>r</code>) 명령어로 프로그램을 실행한다. </p>
<p>프로그램은 실행되다가 이전에 설정한 브레이크포인트(<code>main</code> 함수)를 만나면 즉시 멈춘다.</p>
<pre><code class="language-gdb">(gdb) run
Starting program: /path/to/factorial

Breakpoint 1, main () at factorial.c:15
15        int number = 5;</code></pre>
<p>이제 프로그램은 15번째 줄이 실행되기 직전에 멈춰있다.</p>
<p><strong>4) 코드 한 줄씩 실행 : <code>next</code></strong>
<code>next</code> (축약형 <code>n</code>) 명령어는 현재 줄을 실행하고 바로 다음 줄에서 멈춘다. </p>
<p><code>main</code> 함수의 코드를 한 줄씩 따라가 보겠다.</p>
<pre><code class="language-gdb">(gdb) next
16        int result = 0;
(gdb) next
18        result = calculate_factorial(number);</code></pre>
<p><strong>5) 변수 값 확인 : <code>print</code></strong>
<code>print</code> (축약형 <code>p</code>) 명령어는 현재 시점의 변수 값을 보여준다.</p>
<pre><code class="language-gdb">(gdb) print number
$1 = 5
(gdb) print result
$2 = 0</code></pre>
<p><code>number</code> 변수에는 5가, <code>result</code> 변수에는 0이 올바르게 들어가 있음을 확인했다.</p>
<p><strong>6) 함수 내부로 들어가기: <code>step</code></strong>
다음 실행할 18번 라인은 <code>calculate_factorial</code> 함수 호출이다. </p>
<p>이 함수의 내부 동작을 보고 싶다면 <code>next</code> 대신 <code>step</code> (축약형 <code>s</code>)을 사용한다. </p>
<p><code>step</code>은 함수 호출을 만나면 그 함수 내부로 진입한다.</p>
<pre><code class="language-gdb">(gdb) step
calculate_factorial (n=5) at factorial.c:5
5        int result = 1;</code></pre>
<p><code>calculate_factorial</code> 함수 내부로 들어왔고, 파라미터 <code>n</code>에 5가 잘 전달된 것을 볼 수 있다.</p>
<p><strong>7) 버그 추적</strong>
이제 <code>for</code>문 안에서 변수들이 어떻게 변하는지 <code>next</code>와 <code>print</code>로 추적해보자.</p>
<pre><code class="language-gdb">(gdb) next
8        for (int i = 1; i &lt; n; i++) {
(gdb) next
9            result = result * i;
(gdb) print i
$3 = 1
(gdb) print result
$4 = 1
````next`를 반복하며 `i`와 `result`의 변화를 계속 관찰한다.

```gdb
# ... next를 계속 입력 ...

(gdb) next
9            result = result * i;
(gdb) p i
$7 = 4
(gdb) p result
$8 = 6
(gdb) next
8        for (int i = 1; i &lt; n; i++) {
(gdb) p result
$9 = 24
(gdb) next
11        return result;</code></pre>
<p><code>i</code>가 4일 때 <code>result</code>는 <code>6 * 4 = 24</code>가 되었다. </p>
<p>그리고 <code>i</code>가 5가 되자 <code>i &lt; n</code> (즉, <code>5 &lt; 5</code>) 조건이 거짓이 되어 루프를 빠져나왔다.</p>
<p>여기서 팩토리얼은 5까지 곱해야 하는데, <code>i</code>가 4일 때까지만 곱하고 루프가 종료되었음을 확인할 수 있다. </p>
<p><code>for</code>문의 조건이 <code>i &lt; n</code>이 아니라 <code>i &lt;= n</code>이 되어야 한다.</p>
<p><strong>8) 디버깅 종료</strong>
원인을 찾았으니 디버깅을 종료한다. <code>quit</code> (축약형 <code>q</code>) 명령어를 입력한다.</p>
<pre><code class="language-gdb">(gdb) quit
A debugging session is active.

    Inferior 1 [process 12345] will be killed.

Quit anyway? (y or n) y</code></pre>
<hr>
<h2 id="gdb-핵심-명령어-정리">GDB 핵심 명령어 정리</h2>
<table>
<thead>
<tr>
<th align="left">명령어</th>
<th align="left">축약형</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>break</code></td>
<td align="left"><code>b</code></td>
<td align="left">브레이크포인트를 설정한다. (예: <code>b main</code>, <code>b 12</code>)</td>
</tr>
<tr>
<td align="left"><code>run</code></td>
<td align="left"><code>r</code></td>
<td align="left">프로그램을 실행한다.</td>
</tr>
<tr>
<td align="left"><code>next</code></td>
<td align="left"><code>n</code></td>
<td align="left">다음 줄로 이동한다. (함수 안으로 들어가지 않음)</td>
</tr>
<tr>
<td align="left"><code>step</code></td>
<td align="left"><code>s</code></td>
<td align="left">다음 줄로 이동한다. (함수 안으로 들어감)</td>
</tr>
<tr>
<td align="left"><code>print</code></td>
<td align="left"><code>p</code></td>
<td align="left">변수의 값을 출력한다. (예: <code>p my_variable</code>)</td>
</tr>
<tr>
<td align="left"><code>list</code></td>
<td align="left"><code>l</code></td>
<td align="left">현재 위치 주변의 소스 코드를 보여준다.</td>
</tr>
<tr>
<td align="left"><code>continue</code></td>
<td align="left"><code>c</code></td>
<td align="left">다음 브레이크포인트까지 실행을 계속한다.</td>
</tr>
<tr>
<td align="left"><code>quit</code></td>
<td align="left"><code>q</code></td>
<td align="left">GDB를 종료한다.</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unix] Vi 편집기와 GCC 사용법]]></title>
            <link>https://velog.io/@kt_gml/UnixVim-Vi-%ED%8E%B8%EC%A7%91%EA%B8%B0%EC%99%80-GCC-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@kt_gml/UnixVim-Vi-%ED%8E%B8%EC%A7%91%EA%B8%B0%EC%99%80-GCC-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Sat, 11 Oct 2025 11:33:56 GMT</pubDate>
            <description><![CDATA[<h1 id="unix에서의-vi-편집기와-gcc-사용법">Unix에서의 Vi 편집기와 GCC 사용법</h1>
<p>Windows의 Visual Studio 같은 통합 개발 환경(IDE)은 매우 편리하지만, 많은 개발 환경, 특히 서버는 그래픽 인터페이스(GUI)가 없는 터미널 기반의 리눅스로 운영된다. </p>
<p>따라서 터미널 환경에서 코드를 작성하고 컴파일하는 능력은 개발자의 기본 소양이다.</p>
<p>이번에 강의에서 UNIX를 다루게 되면서 처음엔 복사 붙여넣기도 제대로 못하고 Vim 모드에서 벗어나기도 힘들었다.</p>
<p>이처럼 윈도우에 익숙해진 나에게 어려운 점이 많아서 오래 기억하고 두고두고 볼 수 있도록 글을 써보려고한다.</p>
<h2 id="vimvi-편집기-사용법">Vim(vi) 편집기 사용법</h2>
<p>Vim은 키보드만으로 모든 작업을 수행하는 터미널 기반의 강력한 텍스트 편집기이다. </p>
<p>최초의 유닉스용 화면 편집기이고 모든 유닉스/리눅스 시스템이 기본적으로 갖추고 있다.</p>
<p>Vim의 가장 큰 특징은 <strong>&#39;모드(Mode)&#39;</strong> 라는 개념이 있다는 것이다.</p>
<h3 id="vim의-3가지-모드">Vim의 3가지 모드</h3>
<p>Vim을 효과적으로 사용하려면 아래 3가지 모드를 이해해야 한다.</p>
<ol>
<li><p><strong>명령 모드 (Normal Mode)</strong>
Vim을 실행했을 때의 기본 상태이다.
키를 입력하면 텍스트가 써지는 것이 아니라, 커서 이동, 내용 복사/붙여넣기, 삭제 등의 <strong>명령</strong>이 실행된다.</p>
</li>
<li><p><strong>입력 모드 (Insert Mode)</strong>
실제로 텍스트를 입력하고 코드를 작성할 수 있는 모드이다.
명령 모드에서 <code>i</code>, <code>a</code>, <code>o</code> 등의 키를 눌러 진입할 수 있다.
이 모드에서 나가려면 <code>ESC</code> 키를 누르면 된다.</p>
</li>
<li><p><strong>마지막 행 모드 (Command-Line Mode)</strong>
명령 모드에서 <code>:</code>(콜론) 키를 눌러 진입한다.
화면 맨 아래 줄에 명령어를 입력하여 파일 저장, 종료, 검색 등 강력한 기능을 수행한다.</p>
</li>
</ol>
<hr>
<h3 id="vim으로-helloc-파일-작성하기">Vim으로 <code>hello.c</code> 파일 작성하기</h3>
<p><strong>1) Vim 실행 및 파일 생성</strong>
<code>vim</code>혹은 <code>vi</code> 명령어 뒤에 생성할 파일명을 입력한다. 파일이 존재하면 열리고, 없으면 새로 생성된다.</p>
<pre><code class="language-bash">vi hello.c</code></pre>
<p>실행하면 비어있는 화면이 나타나며, 현재는 <strong>명령 모드</strong> 상태이다.</p>
<p><strong>2) 입력 모드로 전환</strong>
코드를 작성하기 위해 키보드에서 <code>i</code> 키를 누른다. 화면 하단에 <strong><code>-- 끼워넣기 --</code></strong> 혹은 <strong><code>-- INSERT --</code></strong> 라는 문구가 나타나면 입력 모드로 전환된 것이다.</p>
<p><strong>3) 코드 작성</strong>
이제 자유롭게 C 코드를 작성할 수 있다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void)
{
    printf(&quot;Hello, World!! \n&quot;);
    return 0;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/8d40a521-84fd-43a1-8540-be636b8dcf8d/image.png" alt=""></p>
<p><strong>4) 저장 및 종료</strong>
코드 작성을 마쳤다면, 파일을 저장하고 Vim을 종료해야 한다.</p>
<ol>
<li><p><code>ESC</code> 키를 눌러 <strong>명령 모드</strong>로 돌아간다. (<code>-- INSERT --</code> 문구가 사라진다.)</p>
</li>
<li><p><code>:</code>(콜론)을 입력하여 <strong>마지막 행 모드</strong>로 전환한다.</p>
</li>
<li><p>원하는 명령어를 입력하고 <code>Enter</code>를 누른다.</p>
<p><code>:w</code> : (Write) 현재 파일을 <strong>저장</strong>한다.
<code>:q</code> : (Quit) Vim을 <strong>종료</strong>한다. (저장되지 않은 변경 사항이 있으면 오류가 발생한다.)
<code>:wq</code> : 파일을 <strong>저장하고 종료</strong>한다. (가장 일반적으로 사용)
<code>:q!</code> : 변경 사항을 저장하지 않고 <strong>강제로 종료</strong>한다.</p>
</li>
</ol>
<p>이제 터미널에 <code>ls</code> 명령어를 입력하면 <code>hello.c</code> 파일이 생성된 것을 확인할 수 있다.</p>
<h2 id="gcc-컴파일러-사용법">GCC 컴파일러 사용법</h2>
<p>GCC(GNU Compiler Collection)는 C, C++, Objective-C 등 다양한 언어를 컴파일할 수 있는 컴파일러 모음이며, 리눅스 환경의 표준 C 컴파일러이다. 컴파일이란 우리가 작성한 C언어 소스 코드(<code>.c</code> 파일)를 컴퓨터가 이해하고 실행할 수 있는 기계어 파일(실행 파일)로 변환하는 과정을 말한다.</p>
<h3 id="gcc로-helloc-컴파일-및-실행하기">GCC로 <code>hello.c</code> 컴파일 및 실행하기</h3>
<p><strong>1) 기본 컴파일</strong>
<code>gcc</code> 명령어 뒤에 컴파일할 소스 파일명을 입력한다.</p>
<pre><code class="language-bash">gcc hello.c</code></pre>
<p>컴파일 과정에서 오류가 없다면 아무런 메시지 없이 명령어 입력이 끝난다. <code>ls</code> 명령어로 확인해 보면 <code>a.out</code>이라는 파일이 새로 생성된 것을 볼 수 있다. 이 <code>a.out</code>이 바로 실행 파일이다.</p>
<p><strong>2) 프로그램 실행</strong>
리눅스 터미널에서 현재 디렉터리에 있는 실행 파일을 실행하려면 파일명 앞에 <code>./</code>를 붙여야 한다.</p>
<pre><code class="language-bash">./a.out</code></pre>
<p><strong>실행 결과:</strong></p>
<pre><code>Hello, World!</code></pre><p><strong>3) 원하는 이름으로 실행 파일 생성 (<code>-o</code> 옵션)</strong>
<code>a.out</code>이라는 기본 이름 대신 원하는 이름으로 실행 파일을 만들고 싶을 때는 <code>-o</code> (output) 옵션을 사용한다.</p>
<pre><code class="language-bash">#사용법: gcc -o [생성할 실행 파일명] [소스 파일명]
gcc -o hello hello.c</code></pre>
<p><code>ls</code> 명령어로 확인하면 <code>a.out</code> 대신 <code>hello</code>라는 이름의 실행 파일이 생성된 것을 볼 수 있다. 실행 방법은 동일하다.</p>
<pre><code class="language-bash">./hello```
**실행 결과:**</code></pre>
<p>Hello, World!</p>
<hr>
<h2 id="vim-모드별-핵심-명령어-정리">Vim 모드별 핵심 명령어 정리</h2>
<p>교수님께서도 처음에는 익숙하지 않을 수 있다고 적어놓고 쓰다보면 외워진다고 하셨다.</p>
<p>근데 평소엔 다른 IDE에서 개발하는 나에게는 실습때마다 도저히 안외워져서 정리해보겠다.</p>
<p>Vim을 처음 사용할 때 다른 것은 잊어버리더라도 아래 4가지만 기억하면 기본적인 문서 작성이 가능하다.</p>
<h3 id="helloc-컴파일에서-사용한-필수-명령어">hello.c 컴파일에서 사용한 필수 명령어</h3>
<table>
<thead>
<tr>
<th align="left">단계</th>
<th align="left">명령어</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>1. 파일 열기</strong></td>
<td align="left"><code>vim 파일명</code></td>
<td align="left">터미널에서 Vim을 실행하며 파일을 연다. (새 파일 생성)</td>
</tr>
<tr>
<td align="left"><strong>2. 입력 시작</strong></td>
<td align="left"><code>i</code></td>
<td align="left"><strong>입력 모드</strong>로 전환하여 텍스트를 입력한다.</td>
</tr>
<tr>
<td align="left"><strong>3. 입력 종료</strong></td>
<td align="left"><code>ESC</code></td>
<td align="left"><strong>명령 모드</strong>로 돌아와 명령을 내릴 준비를 한다.</td>
</tr>
<tr>
<td align="left"><strong>4. 저장 및 종료</strong></td>
<td align="left"><code>:wq</code></td>
<td align="left"><strong>마지막 행 모드</strong>에서 파일을 저장하고 Vim을 종료한다.</td>
</tr>
</tbody></table>
<hr>
<h3 id="명령-모드-normal-mode-명령어">명령 모드 (Normal Mode) 명령어</h3>
<p>Vim의 기본 모드로, 키보드 입력이 명령으로 인식된다. <code>ESC</code> 키를 누르면 언제나 명령 모드로 돌아온다.</p>
<h4 id="커서-이동">커서 이동</h4>
<table>
<thead>
<tr>
<th align="left">명령어</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>h</code>, <code>j</code>, <code>k</code>, <code>l</code></td>
<td align="left">왼쪽, 아래, 위, 오른쪽으로 한 칸씩 이동한다. (방향키 역할)</td>
</tr>
<tr>
<td align="left"><code>w</code></td>
<td align="left">다음 단어(word)의 시작으로 이동한다.</td>
</tr>
<tr>
<td align="left"><code>b</code></td>
<td align="left">이전 단어(word)의 시작으로 이동한다.</td>
</tr>
<tr>
<td align="left"><code>^</code> 또는 <code>0</code></td>
<td align="left">현재 줄의 맨 앞으로 이동한다.</td>
</tr>
<tr>
<td align="left"><code>$</code></td>
<td align="left">현재 줄의 맨 끝으로 이동한다.</td>
</tr>
<tr>
<td align="left"><code>gg</code></td>
<td align="left">파일의 맨 첫 번째 줄로 이동한다.</td>
</tr>
<tr>
<td align="left"><code>G</code></td>
<td align="left">파일의 맨 마지막 줄로 이동한다.</td>
</tr>
<tr>
<td align="left"><code>[숫자]G</code></td>
<td align="left">지정한 숫자의 라인으로 바로 이동한다. (예: <code>15G</code> -&gt; 15번째 줄로 이동)</td>
</tr>
</tbody></table>
<h4 id="편집-수정-및-삭제">편집 (수정 및 삭제)</h4>
<table>
<thead>
<tr>
<th align="left">명령어</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>x</code></td>
<td align="left">현재 커서 위치의 글자 하나를 삭제한다.</td>
</tr>
<tr>
<td align="left"><code>dw</code></td>
<td align="left">커서 위치부터 단어 하나를 삭제한다.</td>
</tr>
<tr>
<td align="left"><code>dd</code></td>
<td align="left">현재 줄 전체를 삭제한다.</td>
</tr>
<tr>
<td align="left"><code>[숫자]dd</code></td>
<td align="left">현재 줄부터 지정한 숫자만큼의 줄을 삭제한다. (예: <code>5dd</code> -&gt; 5줄 삭제)</td>
</tr>
<tr>
<td align="left"><code>r</code></td>
<td align="left">커서 위치의 글자 하나를 다른 글자로 변경한다. (<code>r</code> 누르고 바꿀 글자 입력)</td>
</tr>
<tr>
<td align="left"><code>cw</code></td>
<td align="left">커서 위치부터 단어 하나를 지우고 바로 입력 모드로 전환한다.</td>
</tr>
<tr>
<td align="left"><code>cc</code> 또는 <code>S</code></td>
<td align="left">현재 줄 전체를 지우고 바로 입력 모드로 전환한다.</td>
</tr>
</tbody></table>
<h4 id="복사--붙여넣기-yank--put">복사 &amp; 붙여넣기 (Yank &amp; Put)</h4>
<table>
<thead>
<tr>
<th align="left">명령어</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>yy</code> 또는 <code>Y</code></td>
<td align="left">현재 줄 전체를 복사(Yank)한다.</td>
</tr>
<tr>
<td align="left"><code>[숫자]yy</code></td>
<td align="left">현재 줄부터 지정한 숫자만큼의 줄을 복사한다. (예: <code>3yy</code> -&gt; 3줄 복사)</td>
</tr>
<tr>
<td align="left"><code>p</code></td>
<td align="left">복사한 내용을 현재 커서의 <strong>아래</strong>에 붙여넣기(Put)한다.</td>
</tr>
<tr>
<td align="left"><code>P</code></td>
<td align="left">복사한 내용을 현재 커서의 <strong>위</strong>에 붙여넣기(Put)한다.</td>
</tr>
</tbody></table>
<h4 id="실행-취소-및-반복">실행 취소 및 반복</h4>
<table>
<thead>
<tr>
<th align="left">명령어</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>u</code></td>
<td align="left">이전 명령을 취소한다. (Undo)</td>
</tr>
<tr>
<td align="left"><code>Ctrl</code> + <code>r</code></td>
<td align="left">취소했던 명령을 다시 실행한다. (Redo)</td>
</tr>
<tr>
<td align="left"><code>.</code></td>
<td align="left">바로 직전에 실행했던 명령을 반복한다.</td>
</tr>
</tbody></table>
<hr>
<h3 id="입력-모드-insert-mode-진입-및-탈출-명령어">입력 모드 (Insert Mode) 진입 및 탈출 명령어</h3>
<table>
<thead>
<tr>
<th align="left">명령어</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>i</code></td>
<td align="left">현재 커서 <strong>앞(왼쪽)</strong>에서 입력을 시작한다.</td>
</tr>
<tr>
<td align="left"><code>a</code></td>
<td align="left">현재 커서 <strong>뒤(오른쪽)</strong>에서 입력을 시작한다.</td>
</tr>
<tr>
<td align="left"><code>o</code></td>
<td align="left">현재 줄의 <strong>아래</strong>에 새 줄을 만들고 입력을 시작한다.</td>
</tr>
<tr>
<td align="left"><code>O</code></td>
<td align="left">현재 줄의 <strong>위</strong>에 새 줄을 만들고 입력을 시작한다.</td>
</tr>
<tr>
<td align="left"><code>ESC</code></td>
<td align="left"><strong>명령 모드</strong>로 돌아간다.</td>
</tr>
</tbody></table>
<hr>
<h3 id="마지막-행-모드-command-line-mode-명령어">마지막 행 모드 (Command-Line Mode) 명령어</h3>
<p>명령 모드에서 <code>:</code>(콜론)을 입력하여 진입한다. 화면 하단에 명령어를 입력하여 파일 제어, 검색, 설정 변경 등을 수행한다.</p>
<h4 id="파일-제어"><strong>파일 제어</strong></h4>
<table>
<thead>
<tr>
<th align="left">명령어</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>:w</code></td>
<td align="left">파일을 저장한다. (Write)</td>
</tr>
<tr>
<td align="left"><code>:w 파일명</code></td>
<td align="left">다른 이름으로 파일을 저장한다. (Save As)</td>
</tr>
<tr>
<td align="left"><code>:q</code></td>
<td align="left">Vim을 종료한다. (Quit)</td>
</tr>
<tr>
<td align="left"><code>:wq</code></td>
<td align="left">파일을 저장하고 종료한다.</td>
</tr>
<tr>
<td align="left"><code>:q!</code></td>
<td align="left">변경 내용을 무시하고 강제로 종료한다.</td>
</tr>
</tbody></table>
<h4 id="검색-및-치환"><strong>검색 및 치환</strong></h4>
<table>
<thead>
<tr>
<th align="left">명령어</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>/[검색어]</code></td>
<td align="left">아래 방향으로 검색어를 찾는다. <code>n</code> (다음 찾기), <code>N</code> (이전 찾기)</td>
</tr>
<tr>
<td align="left"><code>?[검색어]</code></td>
<td align="left">위 방향으로 검색어를 찾는다. <code>n</code>, <code>N</code> 키는 동일하게 동작한다.</td>
</tr>
<tr>
<td align="left"><code>:%s/[찾을내용]/[바꿀내용]/g</code></td>
<td align="left">파일 전체(<code>%</code>)에서 &#39;찾을내용&#39;을 &#39;바꿀내용&#39;으로 모두(<code>g</code>) 바꾼다.</td>
</tr>
<tr>
<td align="left"><code>:%s/[찾을내용]/[바꿀내용]/gc</code></td>
<td align="left">바꿀 때마다 사용자에게 확인(confirm)을 받는다.</td>
</tr>
</tbody></table>
<h4 id="에디터-설정"><strong>에디터 설정</strong></h4>
<table>
<thead>
<tr>
<th align="left">명령어</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>:set number</code></td>
<td align="left">줄 번호를 표시한다. (축약형: <code>:set nu</code>)</td>
</tr>
<tr>
<td align="left"><code>:set nonumber</code></td>
<td align="left">줄 번호를 숨긴다. (축약형: <code>:set nonu</code>)</td>
</tr>
<tr>
<td align="left"><code>:set autoindent</code></td>
<td align="left">자동 들여쓰기 기능을 활성화한다.</td>
</tr>
</tbody></table>
<hr>
<p>한번에 다 외울수는 없겠지만 실습때나 <code>Vi 편집기</code>를 사용할때 켜놓고 최대한 기능을 많이 써보며 <code>IDE</code>를 쓸때만큼 편리하게 쓸 수 있도록 여러 명령어들에 익숙해져야겠다.</p>
<p>사용할줄 아는 수준에서 더 나아가 여러 명령어들이 익숙해져 나중에는 <code>VIM 모드</code>가 더 편할 정도의 수준이 되어서 마우스 없이 나의 생각을 그 즉시 키보드로 빠르게 칠 수 있는 수준의 개발자가 되고싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 골드와 TOEIC 925점 달성 및 앞으로의 계획]]></title>
            <link>https://velog.io/@kt_gml/%EB%B0%B1%EC%A4%80-%EA%B3%A8%EB%93%9C%EC%99%80-TOEIC-925%EC%A0%90-%EB%8B%AC%EC%84%B1-%EB%B0%8F-%EC%95%9E%EC%9C%BC%EB%A1%9C%EC%9D%98-%EA%B3%84%ED%9A%8D</link>
            <guid>https://velog.io/@kt_gml/%EB%B0%B1%EC%A4%80-%EA%B3%A8%EB%93%9C%EC%99%80-TOEIC-925%EC%A0%90-%EB%8B%AC%EC%84%B1-%EB%B0%8F-%EC%95%9E%EC%9C%BC%EB%A1%9C%EC%9D%98-%EA%B3%84%ED%9A%8D</guid>
            <pubDate>Sat, 11 Oct 2025 07:34:53 GMT</pubDate>
            <description><![CDATA[<h2 id="백준-골드-달성">백준 골드 달성</h2>
<p><strong>전공 공부</strong>를 병행하면서 꾸준히 코딩테스트 문제를 풀다보니 목표 중 하나였던 <strong>백준 골드</strong>를 달성하게 되었다.</p>
<p><strong>실버</strong>를 달성까지 며칠밖에 안 걸렸어서 골드도 금방 달성할 수 있을 줄 알았지만 그건 내 기본기가 있기 때문이었다.</p>
<p>실버 문제부터는 <code>스택, 큐, 트리, 그래프</code>와 같은 자료구조나 <code>BFS, DFS, DP</code>와 같은 고급 알고리즘이 조금씩 사용되어 한 문제 한 문제 성장하면서 풀어야했기에 시간이 훨씬 오래 걸렸다.</p>
<p>티어를 올리기에 집중하기보단 C언어를 통해 문제에 필요한 자료구조들을 직접 구현해보면서 삽질도 해보고 시간을 많이 썼던 것 같다. </p>
<p>또한 문제를 풀때마다 <code>GitHub</code>에 코드들을 커밋했다. </p>
<p>시간이 너무 오래 걸리기도하고 풀이법이 잘 떠오르지도 않아 초반에는 내가 너무 못하는건 아닌가 걱정도 되었다.</p>
<p>하지만 무작위의 실버 수준의 문제들을 풀어낼 수준이 된다면 웬만한 기업의 코딩테스트는 뚫을 수 있다고 해서 걱정이 좀 줄어들었다.</p>
<p>이 정도면 C를 통한 삽질은 충분히 해봤다고 생각한다.</p>
<p>앞으로는 C++의 구현된 자료구조도 활용해보고 Java를 깊게파며 Spring 프레임워크를 활용한 백엔드 공부도 병행하면서 자료구조나 알고리즘의 구현보다는 활용하는 방법을 위주로 코딩테스트를 준비할 것이다.</p>
<p>이 템포라면 졸업할때쯤엔 플레티넘도 달성하고 부트캠프나 각종 기업의 코딩테스트는 가뿐히 넘을 수 있을 수준이 될 것이라고 생각한다. </p>
<hr>
<h2 id="toeic-925점-달성">TOEIC 925점 달성</h2>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/9f6f30c5-90ca-4e7e-a657-45c706484439/image.png" alt=""></p>
<p>RC가 문제라고 생각하고 RC를 보완하고자 ETS 기출문제 3권을 풀어보며 ETS만의 오답을 거르고 정답을 고르는 논리를 이해하고자 노력했다.</p>
<p>또한 어휘와 문법이 부족하다고 생각해 어휘와 문법을 깊게 학습했다.</p>
<p>예를들면 <code>due</code>라는 단어는 to와 함께 <code>~로인한</code> 이라는 뜻으로 알고있었는데 비즈니스 영어인 토익에서는 <code>membership due = 회비</code> 처럼 생각지도 못한 뜻으로 쓰이는 어휘들의 여러 쓰임들과 여러 뜻을 학습하려고 노력했다.</p>
<p>이뿐만아니라 <code>PART 5, 6</code> 위주로 문법을 처음부터 정리하고 엄청 많은 문제를 풀어보았다. </p>
<p>하지만 100문제 타임어택 독해 시험인 토익의 특성상 RC 성적이 그렇게 드라마틱하게 오르진 않았다.</p>
<p>RC 성적을 <strong>400점대</strong>에서 안정적인 <strong>430-440점대</strong>를 만드는데는 성공했지만 더 올리기는 어려웠다.</p>
<p>그래서 LC에 집중하여 LC 만점을 받기위해 노력했고 그 결과 목표와 가까운 <strong>925점</strong>에 달성하는데에 성공했다.</p>
<p><strong>경영학</strong>과 <strong>컴퓨터공학</strong>을 복수전공하며 공부하다보면 <code>영문 원서</code>로 수업하기도하고, 개인 프로젝트를 하다 생기는 오류나 궁금한점들을 찾아보다보면 양질의 자료들이 영어로 되어있는 경우가 많다.</p>
<p>이뿐만 아니라 요즘 미국주식에도 관심이 많은데 어닝콜을 보더라도 영어는 필수적이다. </p>
<p>LLM이나 파파고로 동시 번역도 가능한 요즘 시대에 영어 실력이 개발자가 되는데에 필수적인건 아니지만 영어를 잘하는 개발자가 여러 측면에서 유리하고 개발 분야뿐 아니라 앞으로 살아가면서도 더 빠르게 성장하는데에 꼭 필요한 능력이라고 생각한다. </p>
<p>졸업을 앞두고 오픽을 준비해서 가장 높은 성적인 <code>AL</code>도 받아보고싶다.</p>
<hr>
<h2 id="2학기-목표">2학기 목표</h2>
<ul>
<li><p>오래 기억하고 싶은 내용 VELOG 꾸준히 작성</p>
</li>
<li><p>인스타 키워서 양질의 의류 더 협찬 받기</p>
</li>
<li><p>데이터 관리 강의 활용 - SQLD 취득하기</p>
</li>
<li><p>시흥시 지역문제 경진대회 수상해서 해외연수 경험해보기 </p>
</li>
<li><p>제주시 데이터 활용 공모전 참가해보기</p>
</li>
<li><p>독서 및 투자 공부</p>
</li>
<li><p>운동 및 절주</p>
</li>
<li><p>자료구조 알고리즘 보완 -&gt; PCCP 취득 및 교내 SW 경진대회 수상</p>
</li>
<li><p>웹프로젝트응용 수업 활용 - 운영중인 웹사이트 보완 및 유지보수</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C] 분할 정복 알고리즘과 시프트 연산자
(2630 - 색종이 만들기 / 1074 - Z )]]></title>
            <link>https://velog.io/@kt_gml/C-%EB%B6%84%ED%95%A0-%EC%A0%95%EB%B3%B5-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EA%B3%BC-%EC%8B%9C%ED%94%84%ED%8A%B8-%EC%97%B0%EC%82%B0%EC%9E%902630-%EC%83%89%EC%A2%85%EC%9D%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0-1074-Z</link>
            <guid>https://velog.io/@kt_gml/C-%EB%B6%84%ED%95%A0-%EC%A0%95%EB%B3%B5-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EA%B3%BC-%EC%8B%9C%ED%94%84%ED%8A%B8-%EC%97%B0%EC%82%B0%EC%9E%902630-%EC%83%89%EC%A2%85%EC%9D%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0-1074-Z</guid>
            <pubDate>Tue, 16 Sep 2025 07:55:35 GMT</pubDate>
            <description><![CDATA[<h1 id="분할-정복-알고리즘">분할 정복 알고리즘</h1>
<p><strong>분할 정복 알고리즘(Divide and conquer algorithm)</strong>은 그대로 해결할 수 없는 문제를 작은 문제로 분할하여 문제를 해결하는 방법이다. </p>
<p>대표적인 예로는 <code>퀵 정렬</code>이나 <code>합병 정렬</code>과  <code>이진 탐색</code> 등이 있다.</p>
<hr>
<h2 id="분할-정복-설계">분할 정복 설계</h2>
<p><strong>1) Divide</strong> </p>
<p> 원래 문제가 분할하여 비슷한 유형의 더 작은 하위 문제로 분할이 가능할 때 까지 나눈다.</p>
<p><strong>2) Conquer</strong></p>
<p> 각 하위 문제를 재귀적으로 해결한다. 하위 문제의 규모가 나눌 수 없는 단위가 되면 탈출 조건을 설정하고 해결한다.</p>
<p><strong>3) Combine</strong></p>
<p> <strong>Conquer</strong>한 문제들을 통합하여 원래 문제의 답을 얻어 해결한다.</p>
<hr>
<h2 id="2630---색종이-만들기">2630 - 색종이 만들기</h2>
<p><a href="https://www.acmicpc.net/problem/2630">백준 - 2630 : 색종이 만들기</a></p>
<h3 id="문제">문제</h3>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/2438cde7-6e5b-4971-9102-72c6e8018859/image.png" alt="">
<img src="https://velog.velcdn.com/images/kt_gml/post/d1af3429-6cc7-4066-9591-420ea12b6f04/image.png" alt="">
<img src="https://velog.velcdn.com/images/kt_gml/post/35061317-f994-4a5f-8637-a54bcc6b8847/image.png" alt=""></p>
<h3 id="코드">코드</h3>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int paper[128][128], zero, one;

int search(int x, int y, int n){
    int count = 0;
    for(int i=x; i&lt;x+n; i++){
        for(int j=y; j&lt;y+n; j++){
            if(paper[i][j]==1) count++; 
        }
    }
    if(count == n * n) one++;
    else if(count == 0) zero++;
    else{
        search(x, y, n/2);
        search(x, y+n/2, n/2);
        search(x+n/2, y, n/2);
        search(x+n/2, y+n/2, n/2);
    }
}

int main(void){
    int n;
    scanf(&quot;%d&quot;, &amp;n);
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            scanf(&quot;%d&quot;, &amp;paper[i][j]);
        }
    }
    search(0, 0, n);
    printf(&quot;%d\n%d&quot;, zero, one);
}</code></pre>
<p><strong>2차원 배열</strong>을 활용했고, 1개의 정사각형을 1, 2, 3, 4사분면으로 나누어 조건이 만족될때마다 count를 높여가며 재귀적으로 문제를 풀었다.</p>
<p>이때 count와 n을 비교한 값을 종료 조건으로 설정하고 n/2으로 절반씩 줄여나가는 방법을 사용했다. </p>
<p>같은 원리로 Z라는 문제를 접근했는데 시간 초과 문제가 생겼다.</p>
<hr>
<h2 id="1074---z">1074 - Z</h2>
<p><a href="https://www.acmicpc.net/problem/1074">백준 - 1074 : Z</a></p>
<h3 id="문제-1">문제</h3>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/a14732ad-52d1-4343-b31a-727af786df6b/image.png" alt="">
<img src="https://velog.velcdn.com/images/kt_gml/post/4c375d32-ece6-487a-9643-752c817955c4/image.png" alt=""></p>
<h3 id="코드-1">코드</h3>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int count = 0; // 전역변수 초기화
int r, c;

int search(int x, int y, int n) { 
    if (n == 1) {
        if (x == r &amp;&amp; y == c) {
            return 1; // 찾았을 때 1 반환
        }
    } else {
        int half = n / 2;

        // 1사분면
        if (r &lt; x + half &amp;&amp; c &lt; y + half) {
            return search(x, y, half);
        } else {
            count += half * half;
        }

        // 2사분면
        if (r &lt; x + half &amp;&amp; c &gt;= y + half) {
            return search(x, y + half, half);
        } else {
            count += half * half;
        }

        // 3사분면
        if (r &gt;= x + half &amp;&amp; c &lt; y + half) {
            return search(x + half, y, half);
        } else {
            count += half * half;
        }

        // 4사분면
        if (r &gt;= x + half &amp;&amp; c &gt;= y + half) {
            return search(x + half, y + half, half);
        } else {
            count += half * half;
        }
        return 0;
    }
}

int main() {
    int n;
    scanf(&quot;%d %d %d&quot;, &amp;n, &amp;r, &amp;c);

    int size = 1 &lt;&lt; n; // 2^n 계산하기 위한 비트연산

    search(0, 0, size); // 좌표 0, 0 부터 탐색

    printf(&quot;%d\n&quot;, count);
    return 0;
}</code></pre>
<p>처음엔 <code>색종이 만들기</code> 문제와 같이 모든 사분면을 조사해보며 함수가 호출 될때마다 <code>count</code>를 증가시키는 방식을 사용했다.</p>
<p>예제까지는 잘 실행됐지만 2의 지수꼴로 조사해야하는 범위가 늘어날수록 시간이 기하 급수적으로 늘어나다보니 시간 초과 문제가 생겼다.</p>
<p>그래서 이를 해결하고자 각 사분면을 하나하나 조사하는게 아닌 <code>count+=n*n</code>을 통해 시간을 엄청나게 줄일 수 있었다.</p>
<p>이렇게 분할 정복에다가 조사하지 않아도 되는 부분을 <code>n*n</code>과 같은 방법으로 넘기는 방법까지 더해지니 2의 지수꼴과 같이 규모가 커짐에 따라 얼마나 크게 실행 속도의 차이를 만들어 내는지 느낄 수 있었다. </p>
<p>이런 알고리즘과 자료구조를 기반으로 효율적으로 구현한 코드 하나하나가 적은 비용으로 효과적으로 운영할 수 있는 서비스의 기반이 되는 것이다.</p>
<hr>
<h2 id="시프트-연산자--를-활용한-지수-계산">시프트 연산자(&lt;&lt; / &gt;&gt;)를 활용한 지수 계산</h2>
<p><strong>시프트 연산</strong>은 좌항에 있는 피연산자를 우항에 있는 수만큼 비트 자리 이동하는 연산을 수행한다.</p>
<p><code>&lt;&lt;</code> 는 왼쪽 쉬프트 연산자이고 <code>&gt;&gt;</code> 는 오른쪽 쉬프트 연산자이다.</p>
<p>왼쪽 쉬프트 연산을 하면 좌항에 있는 피연산자의 값이 우항에 있는 수만큼 왼쪽으로 자리 이동하고 빈 자리는 0으로 채우게 된다.</p>
<p>이 특성을 이용한다면 2의 거듭제곱을 Z 문제의 코드처럼 쉽게 구할 수 있다.</p>
<pre><code class="language-c">    printf(&quot;%u\n&quot;, num1 &lt;&lt; 1);    //   2: 0000 0010: 2
    printf(&quot;%u\n&quot;, num1 &lt;&lt; 2);    //   4: 0000 0100: 2^2
    printf(&quot;%u\n&quot;, num1 &lt;&lt; 3);    //   8: 0000 1000: 2^3
    printf(&quot;%u\n&quot;, num1 &lt;&lt; 4);    //  16: 0001 0000: 2^4
    printf(&quot;%u\n&quot;, num1 &lt;&lt; 5);    //  32: 0010 0000: 2^5
    printf(&quot;%u\n&quot;, num1 &lt;&lt; 6);    //  64: 0100 0000: 2^6
    printf(&quot;%u\n&quot;, num1 &lt;&lt; 7);    // 128: 1000 0000: 2^7</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C] 컴파일러의 차이로 발생하는 오류(Undefined Behavior)]]></title>
            <link>https://velog.io/@kt_gml/C-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC%EC%9D%98-%EC%B0%A8%EC%9D%B4%EB%A1%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EC%98%A4%EB%A5%98Undefined-Behavior</link>
            <guid>https://velog.io/@kt_gml/C-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC%EC%9D%98-%EC%B0%A8%EC%9D%B4%EB%A1%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EC%98%A4%EB%A5%98Undefined-Behavior</guid>
            <pubDate>Thu, 17 Jul 2025 06:21:56 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/25501">백준 25501 - 재귀의 귀재</a></p>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/0920694b-8bbe-4238-9af2-f9493b3cda4d/image.png" alt=""></p>
<p><strong>재귀 알고리즘</strong> 문제를 풀다가 답안을 제출했는데 자꾸 오답처리가 되었다.</p>
<p>알고리즘에도 문제가 없고 내 컴파일러에서는 실행결과에도 아무런 문제가 없었다.</p>
<p>그래서 다른 사람들의 코드와 비교하다가 이유를 찾았다.</p>
<pre><code class="language-c">오답 코드

#define _CRT_SECURE_NO_WARNINGS
#include &lt;stdio.h&gt;
#include &lt;string.h&gt;

int recursion(const char *s, int l, int r, int *count) {
  (*count)++;
  if (l &gt;= r)
    return 1;
  else if (s[l] != s[r])
    return 0;
  else
    return recursion(s, l + 1, r - 1, count);
}

int isPalindrome(const char *s, int *count) {
  return recursion(s, 0, strlen(s) - 1, count);
}

int main() {
  int num, count, temp;
  char s[1002];
  scanf(&quot;%d&quot;, &amp;num);
  while (num--) {
    count = 0;
    scanf(&quot;%s&quot;, s);
    printf(&quot;%d %d\n&quot;, isPalindrome(s, &amp;count), count);
  }
  return 0;
}</code></pre>
<hr>
<pre><code class="language-c">정답 코드
#define _CRT_SECURE_NO_WARNINGS
#include &lt;stdio.h&gt;
#include &lt;string.h&gt;

int recursion(const char *s, int l, int r, int *count) {
  (*count)++;
  if (l &gt;= r)
    return 1;
  else if (s[l] != s[r])
    return 0;
  else
    return recursion(s, l + 1, r - 1, count);
}

int isPalindrome(const char *s, int *count) {
  return recursion(s, 0, strlen(s) - 1, count);
}

int main() {
  int num, count, temp;
  char s[1002];
  scanf(&quot;%d&quot;, &amp;num);
  while (num--) {
    count = 0;
    scanf(&quot;%s&quot;, s);
    temp = isPalindrome(s, &amp;count);
    printf(&quot;%d %d\n&quot;, temp, count);
  }
  return 0;
}</code></pre>
<hr>
<pre><code class="language-c">오류 부분
    printf(&quot;%d %d\n&quot;, isPalindrome(s, &amp;count), count);


수정 후    
    temp = isPalindrome(s, &amp;count);
    printf(&quot;%d %d\n&quot;, temp, count);</code></pre>
<p>결론부터 말하자면, <strong>채점 서버의 컴파일러가 함수 인자를 처리하는 순서가 사용자의 로컬 환경과 달랐기 때문</strong>에 발생한 오류였다.</p>
<hr>
<h2 id="함수의-인자-평가-순서">함수의 인자 평가 순서</h2>
<p>C 언어 표준에서는 함수에 여러 개의 인자(argument)를 전달할 때, <strong>컴파일러가 그 인자들의 값을 어떤 순서로 계산(평가)할지에 대해 정해두지 않았다.</strong></p>
<p>즉, 아래와 같은 코드가 있을 때,
*<em><code>printf(&quot;%d %d\n&quot;, isPalindrome(s, &amp;count), count);</code>
*</em>
컴파일러는 두 가지 방식 중 아무거나 선택할 수 있다.</p>
<p><strong>1. 인자를 오른쪽에서 왼쪽으로 평가 (많은 C 컴파일러의 일반적인 방식)</strong></p>
<ol>
<li><code>printf</code>의 두 번째 인자인 <code>count</code>를 먼저 평가한다. </li>
<li>그 다음, 첫 번째 인자인 <code>isPalindrome(s, &amp;count)</code>를 평가한다.</li>
<li><code>isPalindrome</code> 함수가 실행되면서 내부의 <code>recursion</code>이 호출되고, <code>count</code>의 값이 (예를 들어) <strong>3</strong>으로 변경된다. 함수는 팰린드롬 여부에 따라 1 또는 0을 반환한다.</li>
<li><code>printf</code> 함수는 최종적으로 (2)번 단계에서 반환된 값과 (1)번 단계에서 <strong>미리 계산해 둔 <code>count</code>의 값(0)</strong>을 가지고 출력한다.</li>
<li><strong>결과: <code>1 0</code> (오답)</strong></li>
</ol>
<p><strong>2. 인자를 왼쪽에서 오른쪽으로 평가</strong></p>
<ol>
<li><code>printf</code>의 첫 번째 인자인 <code>isPalindrome(s, &amp;count)</code>를 먼저 평가한다.</li>
<li>함수가 실행되면서 <code>count</code>의 값이 <strong>3</strong>으로 변경되고, 함수는 1 또는 0을 반환한다.</li>
<li>그 다음, 두 번째 인자인 <code>count</code>를 평가한다. 이 시점에서 <code>count</code>의 값은 (2)번 단계에서 변경된 <strong>3</strong>이다다.</li>
<li><code>printf</code> 함수는 (1)번 단계의 반환값과 (3)번 단계에서 계산된 <code>count</code>의 값(3)을 가지고 출력한다.</li>
<li><strong>결과: <code>1 3</code> (정답)</strong></li>
</ol>
<p>백준의 채점 서버의 컴파일러는 <strong>방식 1</strong>처럼 동작했고, 나의 컴파일러는 <strong>방식 2</strong>처럼 동작해서 생긴 오류였다. </p>
<p>이처럼 컴파일러나 환경에 따라 결과가 달라질 수 있는 코드는 <strong>정의되지 않은 동작(Undefined Behavior)</strong>에 의존하는 코드라고 부르며, 프로그래밍 시 반드시 피해야 한다.</p>
<hr>
<h2 id="temp-변수를-사용한-순서-보장">temp 변수를 사용한 순서 보장</h2>
<p><code>temp</code> 변수를 사용한 코드는 왜 항상 올바르게 동작할까 ?</p>
<pre><code class="language-c">temp = isPalindrome(s, &amp;count);       // 1번 라인
printf(&quot;%d %d\n&quot;, temp, count);      // 2번 라인</code></pre>
<p>C 언어에서 세미콜론(<code>;</code>)은 <strong>&quot;시퀀스 포인트(Sequence Point)&quot;</strong> 역할을 한다. </p>
<p>시퀀스 포인트는 이전 라인에서 발생한 모든 계산과 <strong>부수 효과(side effect)</strong>가 다음 라인으로 넘어가기 전에 반드시 완료됨을 보장한다.</p>
<ol>
<li><strong>1번 라인 실행</strong>: <code>isPalindrome(s, &amp;count)</code>가 호출됩니다. 이 함수의 실행이 <strong>완전히 끝날 때까지</strong> 프로그램은 다음으로 넘어가지 않는다.<ul>
<li>이 과정에서 <code>count</code>의 값은 최종값(예: 3)으로 업데이트된다.</li>
<li><code>isPalindrome</code>의 반환값은 <code>temp</code> 변수에 저장된다.</li>
</ul>
</li>
<li><strong>시퀀스 포인트(<code>;</code>)</strong>: 1번 라인의 모든 동작(count 값 변경 포함)이 완료되었음을 보장한다.</li>
<li><strong>2번 라인 실행</strong>: <code>printf</code>가 호출된다.<ul>
<li>이 시점에는 <code>temp</code>와 <code>count</code>의 값이 이미 <strong>모두 확정된 상태</strong>이다.</li>
<li><code>printf</code>의 인자 평가 순서가 어떻게 되든 상관없이, 이미 계산이 끝난 <code>temp</code>의 값과 <code>count</code>의 최종값을 가져와 출력하므로 항상 올바른 결과가 나온다.</li>
</ul>
</li>
</ol>
<hr>
<p>앞으론 세미콜론(시퀀스 포인트)을 통해 <strong>연산의 순서를 명확하게 강제</strong>하는 등 C의 동작과정을 더 이해하고 어떤 컴파일러 환경에서도 동일한 결과를 보장하는 코드를 작성해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C] 스택과 후위 표기식(1918 - 후위 표기식)]]></title>
            <link>https://velog.io/@kt_gml/C-%EC%8A%A4%ED%83%9D%EA%B3%BC-%ED%9B%84%EC%9C%84-%ED%91%9C%EA%B8%B0%EC%8B%9D1918-%ED%9B%84%EC%9C%84-%ED%91%9C%EA%B8%B0%EC%8B%9D</link>
            <guid>https://velog.io/@kt_gml/C-%EC%8A%A4%ED%83%9D%EA%B3%BC-%ED%9B%84%EC%9C%84-%ED%91%9C%EA%B8%B0%EC%8B%9D1918-%ED%9B%84%EC%9C%84-%ED%91%9C%EA%B8%B0%EC%8B%9D</guid>
            <pubDate>Mon, 14 Jul 2025 11:18:31 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-링크">문제 링크</h2>
<p><a href="https://www.acmicpc.net/problem/1918">백준 - 후위 표기식</a></p>
<hr>
<h2 id="문제-내용">문제 내용</h2>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/48219334-d10f-450f-b2b1-7441fdf632f6/image.png" alt=""></p>
<p><strong>C</strong>에는 스택 라이브러리가 없기 때문에 구현하여 사용해야한다.</p>
<p>그래서 스택을 구현한 김에 더 깊게 이해하고자 스택 문제를 여러 개 풀고 있다.</p>
<hr>
<h2 id="후위-표기식postfix-notation이란">후위 표기식(Postfix Notation)이란?</h2>
<p>우리는 1+2+3 처럼 <code>숫자(피연산자)</code>와 <code>숫자</code> 사이에 <code>연산자(+)</code>를 넣어서 식을 표현하는데 </p>
<p>이를 <strong>중위 표기식(Infix Notation)</strong>이라고 한다.</p>
<p><strong>후위 표기식(Postfix Notation)</strong>은 <code>연산자</code>가 <code>피연산자</code>의 뒤에 온다.</p>
<pre><code>1 + 2 + 3 (Infix)

1 2 + 3 + (Postfix)</code></pre><h3 id="컴퓨터가-후위-표기식을-사용하는-이유">컴퓨터가 후위 표기식을 사용하는 이유</h3>
<p><strong>후위 표기식</strong>은 앞에서부터 뒤로 가면서 연산자가 보이면 앞의 두 숫자를 피연산자로 계산하면 된다.</p>
<p><strong>중위 표기식</strong> <code>1+2×3</code>의 경우 <strong>후위 표기식</strong>으론 <code>123×+</code>이다.</p>
<p>앞에서부터 차례로 읽다가 연산자인 ×가 발견되었을 때 앞에 있는 2개의 피연산자 2 3를 대상으로 2×3=6을 실행한다.</p>
<p>그 뒤의 연산자인 +가 발견되었을 때 앞에 있는 2개의 피연산자 1 6을 대상으로 1+6=7을 실행한다.
<br></p>
<p>컴퓨터는 후위 표기식을 사용하는데, 이런 <strong>후위 표기식</strong>을 컴퓨터가 사용하는 근본적인 이유는 </p>
<p><strong>괄호</strong>도 필요없고 <strong>연산자 우선 순위를 고려</strong>할 필요도 없기 때문이다.</p>
<h3 id="후위-표기식과-스택">후위 표기식과 스택</h3>
<p>이러한 방식의 <strong>후위 표기식</strong>을 만들거나 <strong>후위 표기식</strong>을 <strong>중위 표기식</strong>으로 만들때 스택(Stack) 구조가 사용된다.</p>
<p>간단한 예를 들면 아까처럼 <code>123x+</code>을 중위 표기식으로 사용할때 <code>1/2/3</code>을 스택에 담고 <code>x</code>를 만나면</p>
<p><code>pop() x pop()</code> 후 연산한 값을 <code>push()</code>한다면 스택에는 <code>1/6</code>이 들어가 있을 것이다.</p>
<p>이 예시는 후위 표기식을 중위 표기식으로 만드는 과정이었고, 이 문제는 반대로 중위 표기식을 후위 표기식으로 만드는 문제이다.</p>
<hr>
<h2 id="중위-표기식---후위-표기식">중위 표기식 -&gt; 후위 표기식</h2>
<p>먼저 코드를 작성하기 전 4가지 규칙을 찾았다.</p>
<ol>
<li><p>피연산자는 출력한다.</p>
</li>
<li><p>&#39;(&#39;를 만난다면 push한다.</p>
</li>
<li><p>&#39;)&#39;를 만난다면 &#39;(&#39;가 나올때까지 pop을 진행하고 그 사이의 연산자들을 출력한다.</p>
</li>
<li><p>연산자를 만난다면 push하지만 이때 peek의 우선순위가 연산자의 우선순위보다 크거나 같다면 우선순위가 더 작아질때까지 pop 후 출력하고 연산자를 push한다.</p>
</li>
</ol>
<p>이 조건을 잘 신경쓴다면 문제없이 코드를 구현할 수 있다.</p>
<hr>
<h2 id="코드">코드</h2>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#define MAX_STACK_SIZE 100

typedef char element;

typedef struct{
  element data[MAX_STACK_SIZE];
  int top; 
}StackType;

void init_stack(StackType *s){
  s-&gt;top = -1;
}

int is_empty(StackType *s){
  return(s-&gt;top == -1);
}

int is_full(StackType *s){
  return(s-&gt;top == MAX_STACK_SIZE-1);
}

void push(StackType *s, element item){
  if(is_full(s)){
    return;
  }
  s-&gt;data[++(s-&gt;top)] = item;
}

element pop(StackType *s){
  if(is_empty(s)){
    return 0;
  }
  return s-&gt;data[(s-&gt;top)--];
}

int priority(char c){
  if(c == &#39;(&#39; || c==&#39;)&#39;)
    return 0; //&#39;()&#39;은 연산에 참가 안하고 따로 처리하므로 후순위로 둠
  else if(c == &#39;+&#39; || c == &#39;-&#39;) 
    return 1;
  else if(c == &#39;*&#39; || c == &#39;/&#39;)
    return 2;
}

int main(void){
  char input[100] = {0,};
  scanf(&quot;%s&quot;, input);
  StackType s;
  char temp;
  init_stack(&amp;s);
  for(int i=0; i&lt;strlen(input); i++){
    int ch = input[i];
    switch(ch){
      case &#39;+&#39;: case &#39;-&#39; : case &#39;*&#39; : case &#39;/&#39;:
        while(!is_empty(&amp;s) &amp;&amp; priority(s.data[s.top])&gt;=priority(ch)){ 
        //스택에 있는 연산자의 우선순위가 더 크거나 같으면 출력
          printf(&quot;%c&quot;, pop(&amp;s));
        }
        push(&amp;s, ch);
        break;

      case &#39;(&#39;: 
        push(&amp;s, ch);
        break;

      case &#39;)&#39;:
        temp = pop(&amp;s);
        while(temp !=&#39;(&#39;){
          printf(&quot;%c&quot;, temp);
          temp = pop(&amp;s);
        }
        break;

      default : //피연산자인 경우
        printf(&quot;%c&quot;, ch);
        break;
    }
  }
  while(!is_empty(&amp;s)){ //남은 연산자 출력
    printf(&quot;%c&quot;, pop(&amp;s));
  }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C] 1874 - 스택 수열]]></title>
            <link>https://velog.io/@kt_gml/C-1874-%EC%8A%A4%ED%83%9D-%EC%88%98%EC%97%B4</link>
            <guid>https://velog.io/@kt_gml/C-1874-%EC%8A%A4%ED%83%9D-%EC%88%98%EC%97%B4</guid>
            <pubDate>Sun, 13 Jul 2025 16:27:06 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-링크">문제 링크</h2>
<p><a href="https://www.acmicpc.net/problem/1874">백준 - 스택 수열</a></p>
<hr>
<h2 id="문제-내용">문제 내용</h2>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/3512fca4-2659-47ed-8ae4-369f8ff36962/image.png" alt=""></p>
<p><strong>C</strong>에는 스택 라이브러리가 없기 때문에 구현하여 사용해야한다.</p>
<p>그래서 이왕 스택을 구현한 김에 더 깊게 이해하고자 스택 문제를 여러개 풀고 있다.</p>
<p>아마 예시 입출력이 없었다면 아직도 문제를 이해하지 못했을 것이다.</p>
<p>쉽게 설명하자면 <strong>1부터 오름차순</strong>으로 스택에 <strong>push</strong> 할 수 있고 <strong>pop</strong>을 통해 입력된 수열을 만들 수 있는지 체크하고 만들 수 있다면 push와 pop의 과정들을 출력하는 문제이다.</p>
<p><strong>세가지 조건</strong>을 나누어서 해결했다. </p>
<p>우선 peek라고도 불리는 <strong>stack[top]</strong> 값과 <strong>temp(입력값)</strong>을 비교했다.</p>
<p><strong>1. stack[top]값이 더 큰 경우</strong>
&quot;NO&quot; 출력 후 프로그램 종료</p>
<p><strong>2. temp값이 더 큰 경우</strong>
 stack[top]과 temp가 같아질때까지 push 후 같아지면 pop 진행</p>
<p><strong>3. 두 값이 같은 경우</strong>
pop 진행</p>
<p>이 세 가지 경우로 나누어 코드를 짰다.</p>
<hr>
<h2 id="코드">코드</h2>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#define MAX_STACK_SIZE 100

typedef char element;

typedef struct{
  element data[MAX_STACK_SIZE];
  int top; 
}StackType;

void init_stack(StackType *s){
  s-&gt;top = -1;
}

int is_empty(StackType *s){
  return(s-&gt;top == -1);
}

int is_full(StackType *s){
  return(s-&gt;top == MAX_STACK_SIZE-1);
}

void push(StackType *s, element item){
  if(is_full(s)){
    return;
  }
  s-&gt;data[++(s-&gt;top)] = item;
}

element pop(StackType *s){
  if(is_empty(s)){
    return 0;
  }
  return s-&gt;data[(s-&gt;top)--];
}

int priority(char c){
  if(c == &#39;(&#39; || c==&#39;)&#39;)
    return 0; //&#39;()&#39;은 연산에 참가 안하고 따로 처리하므로 후순위로 둠
  else if(c == &#39;+&#39; || c == &#39;-&#39;) 
    return 1;
  else if(c == &#39;*&#39; || c == &#39;/&#39;)
    return 2;
}

int main(void){
  char input[100] = {0,};
  scanf(&quot;%s&quot;, input);
  StackType s;
  char temp;
  init_stack(&amp;s);
  for(int i=0; i&lt;strlen(input); i++){
    int ch = input[i];
    switch(ch){
      case &#39;+&#39;: case &#39;-&#39; : case &#39;*&#39; : case &#39;/&#39;:
        while(!is_empty(&amp;s) &amp;&amp; priority(s.data[s.top])&gt;=priority(ch)){
          printf(&quot;%c&quot;, pop(&amp;s));
        }
        push(&amp;s, ch);
        break;

      case &#39;(&#39;:
        push(&amp;s, ch);
        break;

      case &#39;)&#39;:
        temp = pop(&amp;s);
        while(temp !=&#39;(&#39;){
          printf(&quot;%c&quot;, temp);
          temp = pop(&amp;s);
        }
        break;

      default : //피연산자인 경우
        printf(&quot;%c&quot;, ch);
        break;
    }
  }
  while(!is_empty(&amp;s)){
    printf(&quot;%c&quot;, pop(&amp;s));
  }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JSP] DAO 패턴과 커넥션 풀을 활용한  회원가입 및 로그인 기능 구현]]></title>
            <link>https://velog.io/@kt_gml/JSP-DAO-%ED%8C%A8%ED%84%B4-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EB%B0%8F-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@kt_gml/JSP-DAO-%ED%8C%A8%ED%84%B4-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EB%B0%8F-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Thu, 10 Jul 2025 17:24:01 GMT</pubDate>
            <description><![CDATA[<h1 id="dao-패턴과-커넥션-풀">DAO 패턴과 커넥션 풀</h1>
<p>비즈니스 로직을 JSP에서 분리했더라도, 데이터베이스 연결(JDBC) 코드가 서블릿이나 JSP에 직접 포함되어 있다면 심각한 문제를 야기한다.</p>
<pre><code class="language-java">// 서블릿이나 JSP 파일 내부 - 좋지 않은 예시
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;

try {
    // 1. 드라이버 로딩
    Class.forName(&quot;oracle.jdbc.driver.OracleDriver&quot;);
    // 2. 커넥션 생성
    conn = DriverManager.getConnection(&quot;jdbc:oracle:thin:@localhost:1521:XE&quot;, &quot;user&quot;, &quot;pass&quot;);
    // 3. SQL 작성 및 실행
    String sql = &quot;SELECT * FROM member WHERE id = ?&quot;;
    pstmt = conn.prepareStatement(sql);
    pstmt.setString(1, request.getParameter(&quot;id&quot;));
    rs = pstmt.executeQuery();
    // 4. 결과 처리...
    // 이 모든 코드가 DB 작업이 필요한 모든 파일에 중복되어 삽입된다.
} catch(Exception e) {
    // 예외 처리
} finally {
    // 5. 자원 해제 (매번 반복)
    if(rs != null) rs.close();
    if(pstmt != null) pstmt.close();
    if(conn != null) conn.close();
}</code></pre>
<p>이러한 구조는 코드의 <strong>중복</strong>이 극심하고, DB 정보(URL, ID, PW)가 변경되면 관련된 모든 파일을 수정해야 하는 <strong>유지보수 재앙</strong>을 초래한다. </p>
<p>또한, 매 요청마다 DB 커넥션을 생성하고 해제하는 과정은 시스템에 엄청난 부하를 주어 <strong>성능 저하의 주범</strong>이 된다.</p>
<p>해결책으로 <strong>DAO(Data Access Object) 패턴</strong>을 도입하여 데이터 로직을 분리하고, <strong>커넥션 풀(Connection Pool)</strong>을 함께 사용하여 시스템의 효율을 극대화해야 한다.</p>
<hr>
<h2 id="dao-패턴">DAO 패턴</h2>
<p><strong>DAO</strong>는 데이터베이스에 접근하고 처리하는 로직(CRUD: Create, Read, Update, Delete)을 <strong>별도의 클래스로 완전히 분리</strong>하여 캡슐화하는 디자인 패턴이다. </p>
<p>이는 &#39;관심사의 분리(Separation of Concerns)&#39; 원칙을 충실히 따르는 방법이다.</p>
<p>서블릿이나 다른 서비스 클래스들은 복잡한 JDBC 코드나 SQL 문을 전혀 알 필요가 없다. </p>
<p>그저 DAO 객체에게 &quot;회원 정보 저장&quot;, &quot;ID가 &#39;user1&#39;인 회원 정보 조회&quot; 와 같이 <strong>의미 있는 기능 단위로 요청</strong>만 하면 된다.</p>
<hr>
<h2 id="dto-vo-그리고-자바빈javabean">DTO, VO 그리고 자바빈(JavaBean)</h2>
<p><strong>DTO (Data Transfer Object) / VO (Value Object)</strong> 
계층 간(Controller-Service-DAO) 데이터를 전달하기 위한 목적으로 만들어진 객체이다. 
예를 들면 <code>MemberDTO</code>는 회원 한 명의 정보를 온전히 담아 운반하는 &#39;데이터 상자&#39; 역할을 한다.</p>
<p><strong>자바빈 (JavaBean)</strong> 
앞서 다뤘던 자바빈은 특정 규칙을 따르는 자바 클래스를 지칭하는 <strong>기술 규약</strong>이다.
    1.  <code>private</code> 접근 제한자로 필드를 선언한다.
    2.  <code>public</code> 접근자로 <code>getter/setter</code> 메서드를 가진다.
    3.  인자 없는 기본 생성자(<code>default constructor</code>)를 가진다.</p>
<p><strong>하지만 여기서 DTO는 자바빈 규약에 따라 만드는 것이 일반적이다.</strong> </p>
<p>즉, <code>MemberDTO</code>는 &#39;데이터 전달&#39;이라는 <strong>역할(패턴)</strong>을 수행하며, 그 구현은 <strong>자바빈(기술 규약)</strong>을 따른다. </p>
<p>따라서 &quot;DTO는 자바빈이다&quot;라고 말할 수 있다. </p>
<p>JSP에서 <code>&lt;jsp:useBean&gt;</code> 액션 태그가 DTO 객체를 쉽게 다룰 수 있는 것도 DTO가 자바빈 규약을 따르기 때문이다.</p>
<hr>
<h2 id="dao-data-access-object">DAO (Data Access Object)</h2>
<p>실제 DB 작업을 수행하는 객체이다. </p>
<p>모든 JDBC 코드는 DAO 클래스 내부에만 존재한다.</p>
<pre><code class="language-java">// MemberDAO.java 의 예시
public class MemberDAO {
    // ... Connection Pool을 통해 Connection을 얻어오는 로직 ...

    public int insertMember(MemberDTO dto) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        String sql = &quot;INSERT INTO member (id, password, name, email) VALUES (?, ?, ?, ?)&quot;;
        int result = 0;
        try {
            conn = getConnection(); // 커넥션 풀에서 하나 빌려오기
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, dto.getId());
            pstmt.setString(2, dto.getPassword());
            pstmt.setString(3, dto.getName());
            pstmt.setString(4, dto.getEmail());
            result = pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(pstmt, conn); // 자원 반납
        }
        return result;
    }
    // getMember(id), updateMember(dto), deleteMember(id) 등 다른 메서드들...
}</code></pre>
<hr>
<h2 id="커넥션-풀connection-pool">커넥션 풀(Connection Pool)</h2>
<p>데이터베이스 커넥션을 맺고 끊는 작업은 네트워크 통신, 사용자 인증, 세션 설정 등이 포함된, 시스템에 상당한 부하를 주는 <strong>고비용 작업</strong>이다. </p>
<p>매 요청마다 이 작업을 반복하는 것은 극심한 성능 저하를 유발한다.</p>
<p><strong>커넥션 풀</strong>은 이 문제를 해결하기 위해 미리 일정 개수의 <strong>DB 커넥션을 생성하여 &#39;풀(Pool)&#39;이라는 저장 공간에 보관</strong>해두고, 필요할 때마다 빌려 쓰고 반납하는 기법이다.</p>
<p><strong>동작 원리:</strong></p>
<ol>
<li><p><strong>WAS 시작</strong> 
웹 애플리케이션 서버(EX:Tomcat)가 시작될 때, <code>context.xml</code>에 설정된 정보를 읽어 지정된 개수만큼 커넥션을 미리 생성하여 풀에 저장한다.</p>
</li>
<li><p><strong>DAO의 요청</strong> 
<code>MemberDAO</code>가 DB 작업이 필요하면, <code>DriverManager.getConnection()</code>을 직접 호출하는 대신, WAS가 제공하는 <strong>JNDI(Java Naming and Directory Interface)</strong> 서비스를 통해 커넥션 풀을 찾는다.</p>
</li>
<li><p><strong>대여(Borrow)</strong> 
JNDI를 통해 찾은 커넥션 풀(DataSource 객체)에게 <code>dataSource.getConnection()</code>을 호출하여 커넥션을 요청한다. 풀은 유휴 상태의 커넥션 하나를 빌려준다.</p>
</li>
<li><p><strong>작업 수행</strong> 
DAO는 빌린 커넥션을 사용하여 SQL 작업을 수행한다.</p>
</li>
<li><p><strong>반납(Return)</strong> 
작업이 끝나면 <code>connection.close()</code>를 호출한다. 
커넥션 풀을 통해 얻은 커넥션은 이 때 실제로 닫히는 것이 아니라, <strong>풀에 반납되어 &#39;대기&#39; 상태</strong>로 돌아간다.</p>
</li>
<li><p><strong>재사용</strong> 
다른 요청이 들어오면, 풀에 대기 중인 커넥션을 다시 빌려주어 커넥션 생성 비용 없이 즉시 DB 작업을 시작할 수 있다.</p>
</li>
</ol>
<p><strong>Tomcat <code>context.xml</code> 설정 예시</strong>
<code>META-INF/context.xml</code> 파일에 다음과 같이 DB 정보를 설정한다.</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;Context&gt;
    &lt;Resource name=&quot;jdbc/OracleDB&quot; auth=&quot;Container&quot;
              type=&quot;javax.sql.DataSource&quot;
              driverClassName=&quot;oracle.jdbc.driver.OracleDriver&quot;
              url=&quot;jdbc:oracle:thin:@localhost:1521:XE&quot;
              username=&quot;myuser&quot;
              password=&quot;mypassword&quot;
              maxTotal=&quot;20&quot;   &lt;!-- 최대 커넥션 개수 --&gt;
              maxIdle=&quot;10&quot;    &lt;!-- 유휴 상태로 유지할 최대 커넥션 개수 --&gt;
              maxWaitMillis=&quot;5000&quot; /&gt; &lt;!-- 커넥션을 기다리는 최대 시간(ms) --&gt;
&lt;/Context&gt;</code></pre>
<p><strong>커넥션 풀 사용 코드 (DAO 내부)</strong></p>
<pre><code class="language-java">// DAO에서 커넥션을 얻어오는 메서드
private Connection getConnection() {
    Connection conn = null;
    try {
        Context initContext = new InitialContext();
        // context.xml 에 설정한 &quot;jdbc/OracleDB&quot; 이름을 찾아 DataSource 객체를 얻어온다.
        DataSource ds = (DataSource) initContext.lookup(&quot;java:/comp/env/jdbc/OracleDB&quot;);
        conn = ds.getConnection(); // DataSource를 통해 풀에서 커넥션을 대여한다.
    } catch (Exception e) {
        e.printStackTrace();
    }
    return conn;
}</code></pre>
<p>DAO 패턴과 커넥션 풀의 조합은 현대적인 웹 애플리케이션 구축의 표준적인 방법이다.</p>
<hr>
<h2 id="회원가입-및-로그인-기능-구현">회원가입 및 로그인 기능 구현</h2>
<p>JSP/Servlet 기반 MVC 패턴을 사용하여 회원가입 및 로그인 기능을 구현하는 코드이다.</p>
<h3 id="db-테이블">DB 테이블</h3>
<p>데이터베이스에 회원 정보를 저장할 <code>member</code> 테이블을 생성한다.</p>
<pre><code class="language-sql">-- member 테이블 생성 SQL
CREATE TABLE member (
    id VARCHAR2(20) PRIMARY KEY,
    password VARCHAR2(20) NOT NULL,
    name VARCHAR2(30) NOT NULL,
    email VARCHAR2(50),
    regdate DATE DEFAULT SYSDATE
);</code></pre>
<h3 id="model-데이터-및-로직-처리">Model (데이터 및 로직 처리)</h3>
<p><strong><code>Member.java</code> (DTO)</strong></p>
<p>회원 정보를 담아 계층 간에 전달하는 객체. 자바빈 규약에 따라 작성한다.</p>
<pre><code class="language-java">// Member.java
package com.example.model;

import java.io.Serializable;
import java.sql.Date;

public class Member implements Serializable {
    private static final long serialVersionUID = 1L;

    // 필드 (DB 컬럼과 일치)
    private String id;
    private String password;
    private String name;
    private String email;
    private Date regdate;

    // 기본 생성자
    public Member() {
    }

    // Getter and Setter
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public Date getRegdate() { return regdate; }
    public void setRegdate(Date regdate) { this.regdate = regdate; }
}</code></pre>
<br>

<p><strong><code>MemberDAO.java</code> (DAO)</strong></p>
<p>실제 데이터베이스에 접근하여 CRUD 작업을 수행하는 객체. </p>
<p>커넥션 풀을 사용한다.</p>
<pre><code class="language-java">// MemberDAO.java
package com.example.model;

import java.sql.*;
import javax.naming.*;
import javax.sql.*;

public class MemberDAO {

    private DataSource dataSource;

    // 생성자에서 커넥션 풀(DataSource)을 찾아 초기화
    public MemberDAO() {
        try {
            Context context = new InitialContext();
            dataSource = (DataSource) context.lookup(&quot;java:comp/env/jdbc/OracleDB&quot;);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }

    // 회원가입 (Create)
    public int join(Member member) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        int result = 0;
        String sql = &quot;INSERT INTO member (id, password, name, email) VALUES (?, ?, ?, ?)&quot;;

        try {
            conn = dataSource.getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, member.getId());
            pstmt.setString(2, member.getPassword());
            pstmt.setString(3, member.getName());
            pstmt.setString(4, member.getEmail());
            result = pstmt.executeUpdate(); // 성공 시 1 반환
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (pstmt != null) pstmt.close();
                if (conn != null) conn.close(); // 커넥션을 풀에 반납
            } catch (SQLException e) {}
        }
        return result;
    }

    // 로그인 인증 (Read)
    public int loginCheck(String id, String password) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        int result = -1; // -1: 에러, 0: 비밀번호 불일치, 1: 성공
        String sql = &quot;SELECT password FROM member WHERE id = ?&quot;;

        try {
            conn = dataSource.getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, id);
            rs = pstmt.executeQuery();

            if (rs.next()) { // 아이디가 존재할 경우
                if (rs.getString(&quot;password&quot;).equals(password)) {
                    result = 1; // 로그인 성공
                } else {
                    result = 0; // 비밀번호 불일치
                }
            } else {
                result = -1; // 아이디 존재 안함
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (rs != null) rs.close();
                if (pstmt != null) pstmt.close();
                if (conn != null) conn.close();
            } catch (SQLException e) {}
        }
        return result;
    }
}</code></pre>
<h3 id="view-사용자-인터페이스">View (사용자 인터페이스)</h3>
<p><strong><code>join.jsp</code></strong></p>
<pre><code class="language-java">&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;title&gt;회원가입&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h2&gt;회원가입&lt;/h2&gt;
    &lt;form action=&quot;join.do&quot; method=&quot;post&quot;&gt;
        &lt;label for=&quot;id&quot;&gt;아이디:&lt;/label&gt;&lt;br&gt;
        &lt;input type=&quot;text&quot; id=&quot;id&quot; name=&quot;id&quot; required&gt;&lt;br&gt;&lt;br&gt;
        &lt;label for=&quot;password&quot;&gt;비밀번호:&lt;/label&gt;&lt;br&gt;
        &lt;input type=&quot;password&quot; id=&quot;password&quot; name=&quot;password&quot; required&gt;&lt;br&gt;&lt;br&gt;
        &lt;label for=&quot;name&quot;&gt;이름:&lt;/label&gt;&lt;br&gt;
        &lt;input type=&quot;text&quot; id=&quot;name&quot; name=&quot;name&quot; required&gt;&lt;br&gt;&lt;br&gt;
        &lt;label for=&quot;email&quot;&gt;이메일:&lt;/label&gt;&lt;br&gt;
        &lt;input type=&quot;email&quot; id=&quot;email&quot; name=&quot;email&quot;&gt;&lt;br&gt;&lt;br&gt;
        &lt;input type=&quot;submit&quot; value=&quot;가입하기&quot;&gt;
    &lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<br>


<p><strong><code>login.jsp</code></strong></p>
<pre><code class="language-java">&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;title&gt;로그인&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h2&gt;로그인&lt;/h2&gt;
    &lt;form action=&quot;login.do&quot; method=&quot;post&quot;&gt;
        &lt;label for=&quot;id&quot;&gt;아이디:&lt;/label&gt;&lt;br&gt;
        &lt;input type=&quot;text&quot; id=&quot;id&quot; name=&quot;id&quot; required&gt;&lt;br&gt;&lt;br&gt;
        &lt;label for=&quot;password&quot;&gt;비밀번호:&lt;/label&gt;&lt;br&gt;
        &lt;input type=&quot;password&quot; id=&quot;password&quot; name=&quot;password&quot; required&gt;&lt;br&gt;&lt;br&gt;
        &lt;input type=&quot;submit&quot; value=&quot;로그인&quot;&gt;
    &lt;/form&gt;
    &lt;br&gt;
    &lt;%-- 로그인 실패 시 에러 메시지 출력 --%&gt;
    &lt;%
        String error = (String) request.getAttribute(&quot;error&quot;);
        if (error != null) {
    %&gt;
        &lt;p style=&quot;color: red;&quot;&gt;&lt;%= error %&gt;&lt;/p&gt;
    &lt;%
        }
    %&gt;
    &lt;a href=&quot;join.jsp&quot;&gt;회원가입&lt;/a&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<br>


<p><strong><code>main.jsp</code></strong></p>
<pre><code class="language-java">&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt;
&lt;%
    // 페이지 상단에서 세션 검사
    String userId = (String) session.getAttribute(&quot;userId&quot;);
    if (userId == null) { // 세션에 로그인 정보가 없으면
        response.sendRedirect(&quot;login.jsp&quot;); // 로그인 페이지로 강제 이동
        return; // 현재 페이지의 나머지 코드 실행 중단
    }
%&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;title&gt;메인 페이지&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;&lt;%= userId %&gt;님, 환영합니다!&lt;/h1&gt;
    &lt;p&gt;성공적으로 로그인되었습니다.&lt;/p&gt;
    &lt;a href=&quot;logout.do&quot;&gt;로그아웃&lt;/a&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<h3 id="controller-요청-처리-및-흐름-제어">Controller (요청 처리 및 흐름 제어)</h3>
<p><strong><code>JoinServlet.java</code></strong></p>
<pre><code class="language-java">// JoinServlet.java
package com.example.controller;

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

import com.example.model.Member;
import com.example.model.MemberDAO;

@WebServlet(&quot;/join.do&quot;)
public class JoinServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 파라미터 인코딩 설정
        request.setCharacterEncoding(&quot;UTF-8&quot;);

        // 2. 파라미터 추출 및 Member 객체 생성
        Member member = new Member();
        member.setId(request.getParameter(&quot;id&quot;));
        member.setPassword(request.getParameter(&quot;password&quot;));
        member.setName(request.getParameter(&quot;name&quot;));
        member.setEmail(request.getParameter(&quot;email&quot;));

        // 3. DAO를 통해 DB에 회원 정보 저장
        MemberDAO dao = new MemberDAO();
        int result = dao.join(member);

        // 4. 결과에 따른 페이지 이동
        if (result == 1) { // 회원가입 성공
            response.sendRedirect(&quot;login.jsp&quot;);
        } else { // 회원가입 실패
            // 실제로는 에러 페이지로 보내거나 알림을 띄우는 것이 좋음
            response.sendRedirect(&quot;join.jsp&quot;);
        }
    }
}</code></pre>
<br>


<p><strong><code>LoginServlet.java</code></strong></p>
<pre><code class="language-java">// LoginServlet.java
package com.example.controller;

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

import com.example.model.MemberDAO;

@WebServlet(&quot;/login.do&quot;)
public class LoginServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 파라미터 추출
        String id = request.getParameter(&quot;id&quot;);
        String password = request.getParameter(&quot;password&quot;);

        // 2. DAO를 통해 인증 시도
        MemberDAO dao = new MemberDAO();
        int result = dao.loginCheck(id, password);

        // 3. 결과에 따른 처리
        if (result == 1) { // 로그인 성공
            // 세션 생성 및 사용자 정보 저장
            HttpSession session = request.getSession();
            session.setAttribute(&quot;userId&quot;, id);
            response.sendRedirect(&quot;main.jsp&quot;);
        } else { // 로그인 실패
            // 에러 메시지를 request에 저장
            request.setAttribute(&quot;error&quot;, &quot;아이디 또는 비밀번호가 올바르지 않습니다.&quot;);
            // 포워드를 통해 login.jsp로 이동하여 에러 메시지 출력
            RequestDispatcher dispatcher = request.getRequestDispatcher(&quot;login.jsp&quot;);
            dispatcher.forward(request, response);
        }
    }
}</code></pre>
<br>


<p><strong><code>LogoutServlet.java</code></strong></p>
<pre><code class="language-java">// LogoutServlet.java
package com.example.controller;

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet(&quot;/logout.do&quot;)
public class LogoutServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 세션 가져오기 (없으면 null 반환)
        HttpSession session = request.getSession(false);

        // 2. 세션이 존재하면 무효화
        if (session != null) {
            session.invalidate();
        }

        // 3. 로그인 페이지로 리다이렉트
        response.sendRedirect(&quot;login.jsp&quot;);
    }
}</code></pre>
<hr>
<p>이처럼 JSP/Servlet 기반 MVC 웹 애플리케이션에서는 각 구성 요소가 명확한 역할을 수행한다.</p>
<p><strong>요청은 컨트롤러(서블릿)</strong>가 받아서 흐름을 제어한다.
<strong>데이터 처리와 비즈니스 로직은 모델(DAO, DTO)</strong>이 담당한다.
<strong>화면 표시는 뷰(JSP)</strong>가 담당한다.</p>
<p>이러한 역할 분담은 코드의 재사용성을 높이고 유지보수를 용이하게 만든다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JSP] JavaBean & useBean & EL]]></title>
            <link>https://velog.io/@kt_gml/JSP-JavaBean-useBean-EL</link>
            <guid>https://velog.io/@kt_gml/JSP-JavaBean-useBean-EL</guid>
            <pubDate>Thu, 10 Jul 2025 15:16:10 GMT</pubDate>
            <description><![CDATA[<p><strong>JSP 페이지</strong> 안에서 <code>&lt;% ... %&gt;</code> 스크립틀릿으로 자바 코드를 직접 작성하는 것은, 코드가 길어질수록 가독성과 유지보수성을 급격히 떨어뜨린다. </p>
<p>특히** HTML나 JSP 폼(form)**에서 넘어온 많은 파라미터를 처리하는 코드는 순식간에 지저분해지고 관리가 어렵다.</p>
<pre><code class="language-java">&lt;%
    // 스크립틀릿을 사용한 지저분한 코드의 예
    request.setCharacterEncoding(&quot;UTF-8&quot;);
    String id = request.getParameter(&quot;id&quot;);
    String password = request.getParameter(&quot;password&quot;);
    String name = request.getParameter(&quot;name&quot;);
    String email = request.getParameter(&quot;email&quot;);

    // 넘어온 파라미터마다 객체를 생성하고 setter를 호출해야 한다.
    Person p = new Person(); 
    p.setId(id);
    p.setPassword(password);
    p.setName(name);
    p.setEmail(email);
%&gt;</code></pre>
<p>JSP의 본래 목적인 <strong>&#39;화면 표현(View)&#39;</strong>에 집중하기 위해, 이러한 자바 로직 코드를 분리할 필요가 있다. </p>
<p>이때 JSP <strong>액션 태그(Action Tag)</strong> 중 하나인 <strong><code>&lt;jsp:useBean&gt;</code></strong>이 강력한 해결책이 되어준다.</p>
<hr>
<h2 id="자바빈javabean이란-">자바빈(JavaBean)이란 ?</h2>
<p><code>&lt;jsp:useBean&gt;</code>을 사용하려면, 데이터를 담을 자바 객체가 특정 규칙을 따라야 한다. </p>
<p>이러한 객체를 <strong>자바빈(JavaBean)</strong>이라고 부른다.</p>
<p><strong>자바빈의 규칙</strong></p>
<ol>
<li>클래스는 <code>public</code>으로 선언되어야 한다.</li>
<li>멤버 변수(프로퍼티)는 <code>private</code>으로 선언하여 외부 접근을 막는다.</li>
<li>각 멤버 변수에 접근할 수 있는 <code>public</code> 접근자인 <strong><code>getter/setter</code></strong> 메서드가 있어야 한다. </li>
<li>인자(매개변수)가 없는 <strong>기본 생성자</strong>가 반드시 있어야 한다.</li>
</ol>
<h3 id="ex-personjava">EX: Person.java</h3>
<pre><code class="language-java">package com.example.bean; // 패키지 경로

// 1. public 클래스
public class Person {

    // 2. private 멤버 변수
    private String id;
    private String name;

    // 4. 기본 생성자
    public Person() {
    }

    // 3. public Getter/Setter 메서드
    public String getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}</code></pre>
<blockquote>
<p><a href="https://all-record.tistory.com/102">ECLIPSE에서 Getter Setter 쉽게 설정하는 법</a></p>
</blockquote>
<hr>
<h2 id="jspusebean이란"><code>&lt;jsp:useBean&gt;</code>이란?</h2>
<p><strong><code>&lt;jsp:useBean&gt;</code></strong>은 JSP 페이지에서 사용할 <strong>자바빈(JavaBean) 객체를 선언, 생성, 관리</strong>해주는 액션 태그이다. </p>
<p>스크립틀릿의 복잡한 코드를 아래와 같이 간결한 태그 몇 줄로 완벽하게 대체할 수 있다.</p>
<p><strong><code>&lt;jsp:useBean&gt;</code></strong>
자바빈 객체를 생성하거나, 지정된 scope에 이미 객체가 존재하면 그 객체를 가져온다.</p>
<p><strong><code>&lt;jsp:setProperty&gt;</code></strong>
<code>useBean</code>으로 준비된 객체의 필드(프로퍼티)에 값을 자동으로 설정한다.</p>
<p><strong><code>&lt;jsp:getProperty&gt;</code></strong> 
객체 필드의 값을 가져와 화면에 출력한다.</p>
<h3 id="usebean-사용-예-before--after">useBean 사용 예 (Before &amp; After)</h3>
<p><strong><code>personAction.jsp</code></strong></p>
<h3 id="before-스크립틀릿-사용"><strong>Before: 스크립틀릿 사용</strong></h3>
<pre><code class="language-java">&lt;%@ page import=&quot;com.example.bean.Person&quot; %&gt;
&lt;%
    request.setCharacterEncoding(&quot;UTF-8&quot;);

    Person p = new Person(); // 직접 객체 생성
    p.setId(request.getParameter(&quot;id&quot;)); // 파라미터 하나하나 직접 설정
    p.setName(request.getParameter(&quot;name&quot;));
%&gt;
아이디: &lt;%= p.getId() %&gt; &lt;br&gt;
이름: &lt;%= p.getName() %&gt;</code></pre>
<h4 id="after-jspusebean-액션-태그-사용"><strong>After: <code>&lt;jsp:useBean&gt;</code> 액션 태그 사용</strong></h4>
<pre><code class="language-java">&lt;%@ page contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt;
&lt;% request.setCharacterEncoding(&quot;UTF-8&quot;); %&gt;

&lt;%-- 1. Person 객체를 request 영역에서 찾거나, 없으면 새로 생성하여 &#39;p&#39;라는 이름으로 사용 --%&gt;
&lt;jsp:useBean id=&quot;p&quot; class=&quot;com.example.bean.Person&quot; scope=&quot;request&quot; /&gt;

&lt;%-- 2. p 객체의 프로퍼티(id, name)에 요청 파라미터 값을 자동으로 설정 --%&gt;
&lt;jsp:setProperty name=&quot;p&quot; property=&quot;*&quot; /&gt;

아이디: &lt;jsp:getProperty name=&quot;p&quot; property=&quot;id&quot; /&gt; &lt;br&gt;
이름: &lt;jsp:getProperty name=&quot;p&quot; property=&quot;name&quot; /&gt;</code></pre>
<h4 id="액션-태그-자세히-보기">액션 태그 자세히 보기</h4>
<p><strong><code>&lt;jsp:useBean id=&quot;p&quot; class=&quot;com.example.bean.Person&quot; scope=&quot;request&quot; /&gt;</code></strong></p>
<p><code>id</code>: JSP 페이지 내에서 사용할 객체의 변수 이름(<code>p</code>)을 지정한다.</p>
<p><code>class</code>: 사용할 자바빈의 전체 클래스 경로(패키지 포함)를 지정한다.</p>
<p><code>scope</code>: 객체가 저장되고 유지될 메모리 영역을 지정한다. 
(<code>page</code>, <code>request</code>, <code>session</code>, <code>application</code> 중 선택)
<br>
<strong><code>&lt;jsp:setProperty name=&quot;p&quot; property=&quot;*&quot; /&gt;</code></strong></p>
<p><code>name</code>: 값을 설정할 자바빈 객체의 <code>id</code>를 지정한다. 
(<code>useBean</code>의 <code>id</code>와 일치)</p>
<p><code>property=&quot;*&quot;</code>** 
요청 파라미터의 이름과 자바빈의 프로퍼티(멤버 변수) 이름이 일치하는 모든 항목에 대해, 해당 <code>setter</code> 메서드(<code>setId()</code>, <code>setName()</code> 등)를 <strong>자동으로 호출하여 값을 설정</strong>해준다.</p>
<hr>
<h2 id="왜-jspusebean을-사용해야-할까">왜 <code>&lt;jsp:useBean&gt;</code>을 사용해야 할까?</h2>
<p><code>&lt;jsp:useBean&gt;</code> 액션 태그를 사용하면 JSP 페이지에서 <strong>데이터 처리 로직(Java 코드)을 거의 완벽하게 제거</strong>할 수 있다. </p>
<p>이를 통해 몇가지 이점을 얻는다.</p>
<ol>
<li><strong>가독성 및 유지보수성 향상</strong>: HTML과 유사한 태그로 로직을 처리하므로 코드가 훨씬 깔끔하고 이해하기 쉬워집니다.</li>
<li><strong>역할 분리</strong>: JSP는 화면을 그리는 역할에, 자바빈은 데이터를 담는 역할에 집중할 수 있어 웹 애플리케이션의 구조가 명확해집니다.</li>
<li><strong>생산성 증가</strong>: <code>property=&quot;*&quot;</code> 속성 하나로 수많은 <code>request.getParameter()</code>와 <code>setter</code> 호출 코드를 대체하여 개발 시간을 단축시킵니다.</li>
</ol>
<p>이때 <code>&lt;jsp:getProperty&gt;</code>보다 훨씬 직관적이고 간결하게 객체의 값을 출력하는 방법이 있다.</p>
<p>바로 <strong>표현 언어(Expression Language, EL)</strong>를 사용하는 것이다.</p>
<p><code>{p.id}</code>와 같은 형태로 값을 가져오는 방법이 바로 EL의 기본 문법이다.</p>
<hr>
<h2 id="표현-언어el란">표현 언어(EL)란?</h2>
<p>EL은 JSP 2.0부터 기본으로 포함된 기능으로, JSP 페이지에서 <strong>데이터에 더 쉽게 접근하기 위해 만들어진 언어</strong>이다. </p>
<p><code>$</code> 기호와 중괄호 <code>{}</code>를 사용하여 표현한다. (예: <code>${표현식}</code>)</p>
<p>EL을 사용하면 <code>&lt;jsp:getProperty&gt;</code>나 스크립틀릿 표현식(<code>&lt;%= ... %&gt;</code>) 없이도 자바빈의 프로퍼티, 컬렉션, 배열 등의 값에 간단하게 접근할 수 있다.</p>
<h2 id="el을-사용한-개선-방법">EL을 사용한 개선 방법</h2>
<h3 id="before-jspgetproperty-사용"><strong>Before: <code>&lt;jsp:getProperty&gt;</code> 사용</strong></h3>
<pre><code class="language-jsp">아이디: &lt;jsp:getProperty name=&quot;p&quot; property=&quot;id&quot; /&gt; &lt;br&gt;
이름: &lt;jsp:getProperty name=&quot;p&quot; property=&quot;name&quot; /&gt;</code></pre>
<h3 id="after-표현-언어el-사용"><strong>After: 표현 언어(EL) 사용</strong></h3>
<pre><code class="language-jsp">아이디: ${p.id} &lt;br&gt;
이름: ${p.name}</code></pre>
<p>코드가 훨씬 간결하고 직관적으로 변한 것을 바로 확인할 수 있다. </p>
<p>또 자바스크립트에서 객체 속성에 접근하는 것과 유사하여 JS를 접해봤다면 이해하기도 쉽다.</p>
<hr>
<h2 id="el의-동작원리">EL의 동작원리</h2>
<ol>
<li><p><strong>객체 저장</strong>
<code>&lt;jsp:useBean id=&quot;p&quot; ... scope=&quot;request&quot; /&gt;</code> 태그는 <code>Person</code> 객체를 생성하여 <code>request</code>라는 메모리 영역(scope)에 <code>p</code>라는 이름으로 저장한다.</p>
</li>
<li><p><strong>객체 검색</strong> 
EL 표현식 <code>${p.id}</code>가 실행되면, JSP 컨테이너는 다음 순서로 <code>p</code>라는 이름의 객체(Attribute)를 자동으로 찾는다.
<code>page</code> 영역 -&gt; <code>request</code> 영역 -&gt; <code>session</code> 영역 -&gt;<code>application</code> 영역</p>
</li>
<li><p><strong>Getter 호출</strong>
<code>request</code> 영역에서 <code>p</code> 객체를 찾으면, EL은 <code>p.id</code> 부분을 해석하여 <code>p.getId()</code> 메서드를 자동으로 호출하고 그 반환값을 해당 위치에 출력한다.</p>
</li>
</ol>
<hr>
<h2 id="최종-완성-코드-usebean--setproperty--el">최종 완성 코드 (useBean + setProperty + EL)</h2>
<p>이 세 가지를 조합하면 가장 이상적이고 현대적인 JSP 코드가 완성된다.</p>
<pre><code class="language-java">&lt;%@ page contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt;
&lt;% request.setCharacterEncoding(&quot;UTF-8&quot;); %&gt;

&lt;%-- 1. useBean으로 Person 객체를 request 영역에 준비 --%&gt;
&lt;jsp:useBean id=&quot;p&quot; class=&quot;com.example.bean.Person&quot; scope=&quot;request&quot; /&gt;

&lt;%-- 2. setProperty로 폼 파라미터 값을 객체에 자동으로 채우기 --%&gt;
&lt;jsp:setProperty name=&quot;p&quot; property=&quot;*&quot; /&gt;

&lt;h3&gt;회원 정보 (EL 사용으로 깔끔해진 코드)&lt;/h3&gt;
&lt;ul&gt;
    &lt;li&gt;아이디: ${p.id}&lt;/li&gt;
    &lt;li&gt;이름: ${p.name}&lt;/li&gt;
&lt;/ul&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JSP / WEB] 쿠키(Cookie)와 세션(Session)]]></title>
            <link>https://velog.io/@kt_gml/JSP-%EC%BF%A0%ED%82%A4Cookie%EC%99%80-%EC%84%B8%EC%85%98Session</link>
            <guid>https://velog.io/@kt_gml/JSP-%EC%BF%A0%ED%82%A4Cookie%EC%99%80-%EC%84%B8%EC%85%98Session</guid>
            <pubDate>Thu, 10 Jul 2025 03:05:24 GMT</pubDate>
            <description><![CDATA[<h1 id="cookie--session">Cookie &amp; Session</h1>
<h2 id="쿠키cookie">쿠키(Cookie)</h2>
<p><img src="https://velog.velcdn.com/images/kt_gml/post/606c54dd-a91f-4521-9981-dc304ebdb4aa/image.png" alt=""></p>
<p>HTTP 프로토콜은 &#39;상태가 없는(Stateless)&#39; 특징을 가진다. </p>
<p>이는 서버가 각 요청을 독립적인 것으로 취급하여 이전 요청을 기억하지 못함을 의미한다. </p>
<p>&quot;방금 로그인한 사용자가 누구인지&quot; 알 수 없는 것이다. </p>
<p>이 문제를 해결하기 위해, 서버는 브라우저에 작은 정보를 남겨두는데 이것이 바로 <strong>쿠키(Cookie)</strong>다.</p>
<p>쿠키는 서버가 사용자의 <strong>브라우저(클라이언트)에 저장하는 작은 텍스트 조각</strong>이다. </p>
<p>브라우저는 서버에 요청을 보낼 때마다 이 쿠키를 함께 전송하여, 서버가 사용자를 식별할 수 있도록 돕는다.</p>
<hr>
<h3 id="쿠키의-주요-역할">쿠키의 주요 역할</h3>
<p><strong>상태 관리</strong> 
사용자의 로그인 상태를 유지(자동로그인)한다.
비로그인 상태의 장바구니를 관리한다.</p>
<p><strong>사용자 추적</strong> 
사용자의 방문 기록이나 행동 패턴을 분석하는 데 사용된다.</p>
<p><strong>개인화</strong> 
&quot;오늘 하루 이 창을 열지 않음&quot; 팝업, 언어 설정 등 사용자 맞춤형 경험을 제공한다.</p>
<hr>
<h3 id="jsp-쿠키-객체cookie-활용법">JSP 쿠키 객체(Cookie) 활용법</h3>
<p>JSP에서 쿠키를 다루는 방법은 매우 간단하다.</p>
<p><strong>1. 쿠키 생성 및 전송</strong>
<code>response</code> 내장 객체를 사용한다.</p>
<pre><code class="language-java">&lt;%
    // 1. &quot;userId&quot;라는 이름에 &quot;guest&quot;라는 값을 가진 쿠키 객체 생성
    Cookie cookie = new Cookie(&quot;userId&quot;, &quot;guest&quot;);

    // 2. 쿠키 유효기간 설정 (초 단위, 여기서는 24시간)
    cookie.setMaxAge(60 * 60 * 24);

    // 3. response 객체에 쿠키를 추가하여 클라이언트로 전송
    response.addCookie(cookie);
%&gt;</code></pre>
<p><strong>2. 쿠키 읽기</strong>
<code>request</code> 내장 객체를 사용한다. 
브라우저는 여러 쿠키를 가질 수 있으므로 배열 형태로 받아온다.</p>
<pre><code class="language-java">&lt;%
    // 1. request로부터 모든 쿠키를 배열로 가져온다.
    Cookie[] cookies = request.getCookies();
    String userId = &quot;&quot;;

    if (cookies != null) {
        // 2. 쿠키 배열을 순회하며 원하는 이름의 쿠키를 찾는다.
        for (Cookie c : cookies) {
            if (c.getName().equals(&quot;userId&quot;)) {
                userId = c.getValue();
                break; // 찾았으면 반복 종료
            }
        }
    }
%&gt;
&lt;p&gt;현재 사용자 ID는 &lt;%= userId %&gt; 입니다.&lt;/p&gt;</code></pre>
<p><strong>3. 쿠키 삭제</strong>
쿠키를 직접 삭제하는 메서드는 없다. 
대신, <strong>유효기간을 0으로 설정</strong>하여 브라우저가 즉시 삭제하도록 유도할 수 있다.</p>
<pre><code class="language-java">&lt;%
    Cookie cookie = new Cookie(&quot;userId&quot;, &quot;&quot;); // 값은 비워도 무방하다.
    cookie.setMaxAge(0); // 유효기간을 0으로 설정
    response.addCookie(cookie);
%&gt;</code></pre>
<p>쿠키는 클라이언트 측에 데이터를 저장하는 간단하고 유용한 방법이다.</p>
<p>하지만 보안에 민감한 정보나 큰 데이터를 저장하기에는 적합하지 않다. </p>
<p>이러한 단점을 보완하기 위해 서버 측 저장 방식인 <strong>세션</strong>을 사용한다.</p>
<hr>
<h2 id="세션session">세션(Session)</h2>
<p>쿠키는 브라우저에 저장되어 보안에 취약하고 저장 데이터의 양도 제한적이다. </p>
<p>로그인 정보나 장바구니처럼 중요하고 복잡한 정보는 <strong>세션(Session)</strong>을 이용해 처리해야 한다.</p>
<p>세션은 쿠키와 달리 <strong>데이터를 서버 쪽에 저장</strong>하는 방식이다. </p>
<p>서버는 각 클라이언트를 위한 고유한 저장 공간(세션)을 만들고, 클라이언트에게는 그 공간에 접근할 수 있는 <strong>&#39;열쇠(ID)&#39;</strong>만 쿠키 형태로 전달한다. </p>
<p>클라이언트는 요청 시마다 이 열쇠를 함께 보내고, 서버는 열쇠를 보고 맞는 세션 저장소의 데이터를 사용한다.</p>
<h3 id="세션의-동작-원리">세션의 동작 원리</h3>
<ol>
<li><p><strong>최초 접속</strong> 
클라이언트가 서버에 처음 접속하면, 서버는 고유한 <strong>세션 ID</strong>를 생성한다.</p>
</li>
<li><p><strong>세션 저장소 생성</strong> 
서버 메모리에 해당 세션 ID와 연결되는 저장 공간을 만든다.</p>
</li>
<li><p><strong>세션 ID 전송</strong> 
서버는 생성한 세션 ID를 클라이언트 브라우저에 <strong>쿠키</strong>(보통 <code>JSESSIONID</code> 이름) 형태로 저장시킨다.</p>
</li>
<li><p><strong>재접속</strong> 
클라이언트가 다시 요청하면, 브라우저는 자동으로 세션 ID 쿠키를 함께 보낸다.</p>
</li>
<li><p><strong>세션 데이터 활용</strong> 
서버는 전달받은 세션 ID를 통해 서버에 저장된 해당 클라이언트의 데이터를 찾아 사용한다.</p>
</li>
</ol>
<h3 id="jsp-세션session-내장-객체-활용법">JSP 세션(session) 내장 객체 활용법</h3>
<p>JSP는 세션을 편리하게 다루도록 <code>session</code>이라는 내장 객체를 기본으로 제공한다.</p>
<p><strong>1. 세션에 값 저장하기</strong>
<code>setAttribute(&quot;이름&quot;, 값)</code> 메서드를 사용한다. 값으로는 모든 종류의 객체(Object)를 저장할 수 있다.</p>
<pre><code class="language-java">&lt;%
    String userId = &quot;testuser&quot;;
    // &quot;loginId&quot; 라는 이름으로 userId 변수의 값을 세션에 저장
    session.setAttribute(&quot;loginId&quot;, userId);
%&gt;</code></pre>
<p><strong>2. 세션에서 값 불러오기</strong>
<code>getAttribute(&quot;이름&quot;)</code> 메서드를 사용한다. 
반환 타입이 Object이므로, 원래 타입으로 형 변환(Casting)이 필요하다.</p>
<pre><code class="language-java">&lt;%
    // &quot;loginId&quot; 라는 이름의 세션 값을 가져온다.
    Object value = session.getAttribute(&quot;loginId&quot;);
    String loginId = &quot;&quot;;
    if (value != null) {
        loginId = (String) value;
    }
%&gt;
&lt;p&gt;현재 로그인한 아이디: &lt;%= loginId %&gt;&lt;/p&gt;</code></pre>
<p><strong>3. 세션에서 특정 값 삭제하기</strong>
<code>removeAttribute(&quot;이름&quot;)</code> 메서드를 사용한다.</p>
<pre><code class="language-java">&lt;%
    // &quot;loginId&quot; 세션만 삭제
    session.removeAttribute(&quot;loginId&quot;);
%&gt;</code></pre>
<p><strong>4. 세션 전체 무효화 (로그아웃)</strong>
<code>invalidate()</code> 메서드를 사용한다. 해당 클라이언트의 세션 저장소 전체가 서버에서 삭제된다.</p>
<pre><code class="language-java">&lt;%
    // 세션을 완전히 종료시킨다 (로그아웃 시 사용).
    session.invalidate();
%&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JSP] JSP & Servlet]]></title>
            <link>https://velog.io/@kt_gml/JSP-JSP-Servlet</link>
            <guid>https://velog.io/@kt_gml/JSP-JSP-Servlet</guid>
            <pubDate>Wed, 09 Jul 2025 06:46:30 GMT</pubDate>
            <description><![CDATA[<h2 id="jsp와-servlet-무엇이-다를까-">JSP와 Servlet, 무엇이 다를까 ?</h2>
<p>Java로 웹 개발을 시작하면 가장 먼저 <strong>JSP(JavaServer Pages)</strong>와 <strong>서블릿(Servlet)</strong>을 만나게 된다.</p>
<p><strong>서블릿 (Servlet)</strong>
서블릿은 <strong>순수 자바(Java) 코드</strong>이다. 
서블릿은 사용자의 요청을 처리하고, 비즈니스 로직을 실행하며, 데이터를 제어하는 역할을 수행한다.</p>
<p><strong>JSP (JavaServer Pages)</strong>
JSP는 <strong>HTML 코드 안에 자바 코드를 삽입하여 사용한다.</strong> 
JSP는 사용자에게 최종적으로 보여질 화면의 구조와 내용을 담는다.</p>
<hr>
<h3 id="서블릿의-한계">서블릿의 한계</h3>
<p>초기 웹 개발에서는 서블릿만으로 모든 것을 처리했다. </p>
<pre><code class="language-java">out.println(&quot;&lt;html&gt;&lt;body&gt;&lt;h1&gt;안녕하세요&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;&quot;);</code></pre>
<p>위의 코드처럼 자바만을 이용해 한 줄 한 줄 HTML을 문자열로 출력하는 것은 비효율적이었다. </p>
<p>디자인을 조금만 변경해도 자바 코드를 다시 컴파일해야 하는 번거로움도 있었다.</p>
<p>이처럼 서블릿은 <strong>로직 처리에는 강력하지만, 화면을 구성하는 표현(Presentation)에는 매우 취약한 구조</strong>를 가졌다.</p>
<hr>
<h3 id="jsp--html-문서에-자바를-심다">JSP : HTML 문서에 자바를 심다</h3>
<p>이러한 불편을 해결하기 위해 JSP가 등장했다. </p>
<p>JSP는 반대로 HTML을 기본 구조로 삼고, 필요한 부분에만 자바 코드를 넣어 동적으로 데이터를 처리할 수 있도록 했다. </p>
<p>이로써 디자이너는 자바를 몰라도 HTML 구조를 쉽게 수정하고, 개발자는 필요한 부분에만 로직을 추가하는 역할 분담이 가능해졌다.</p>
<hr>
<h3 id="mvc-패턴">MVC 패턴</h3>
<blockquote>
<p><a href="https://velog.io/@kt_gml/JSP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0MVC-%ED%8C%A8%ED%84%B4">MVC 패턴 및 JSP 프로젝트 구조</a></p>
</blockquote>
<p>결론적으로 현대 웹 개발은 이 둘을 함께 사용하는 <strong>MVC(Model-View-Controller)</strong> 패턴으로 정착되었다.</p>
<p><strong>Controller(컨트롤러) : 서블릿</strong>
사용자의 모든 요청을 가장 먼저 받는다.
필요한 비즈니스 로직을 처리하고 데이터를 가공한다.
그 결과를 어떤 JSP에게 보여줄지 결정하여 전달한다.</p>
<p><strong>View(뷰) : JSP</strong>
컨트롤러(서블릿)로부터 전달받은 데이터를 화면에 그리는 역할에만 집중한다.</p>
<p><strong>Model(모델)</strong> 
데이터 그 자체나 데이터를 처리하는 자바 클래스(JavaBean, DTO, DAO 등)를 의미한다.</p>
<p><strong>흐름</strong> 
사용자 요청 → <strong>서블릿</strong>이 받아서 처리 
→ 결과를 <strong>JSP</strong>에 전달 
→ JSP가 화면을 그려서 사용자에게 응답</p>
<h3 id="jsp-핵심-문법-요약">JSP 핵심 문법 요약</h3>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">문법</th>
<th align="left">설명</th>
<th align="left">예시</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>지시어 (Directive)</strong></td>
<td align="left"><code>&lt;%@ ... %&gt;</code></td>
<td align="left">페이지 전체에 대한 설정 (인코딩, import 등)</td>
<td align="left"><code>&lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; %&gt;</code></td>
</tr>
<tr>
<td align="left"><strong>선언문 (Declaration)</strong></td>
<td align="left"><code>&lt;%! ... %&gt;</code></td>
<td align="left">멤버 변수나 메서드를 선언</td>
<td align="left"><code>&lt;%! int count = 0; %&gt;</code></td>
</tr>
<tr>
<td align="left"><strong>스크립틀릿 (Scriptlet)</strong></td>
<td align="left"><code>&lt;% ... %&gt;</code></td>
<td align="left">자바 로직 코드를 작성 (가장 많이 사용)</td>
<td align="left"><code>&lt;% for(int i=0; i&lt;5; i++) { ... } %&gt;</code></td>
</tr>
<tr>
<td align="left"><strong>표현식 (Expression)</strong></td>
<td align="left"><code>&lt;%= ... %&gt;</code></td>
<td align="left">변수 값이나 메서드 리턴 값을 출력 (<code>;</code> 사용 안 함)</td>
<td align="left"><code>안녕하세요, &lt;%= name %&gt;님!</code></td>
</tr>
<tr>
<td align="left"><strong>주석 (Comment)</strong></td>
<td align="left"><code>&lt;%-- ... --%&gt;</code></td>
<td align="left">JSP 주석 (HTML 소스 보기에도 나타나지 않음)</td>
<td align="left"><code>&lt;%-- 이 부분은 클라이언트에게 보이지 않는다. --%&gt;</code></td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JSP] 프로젝트 구조(MVC 패턴)]]></title>
            <link>https://velog.io/@kt_gml/JSP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0MVC-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@kt_gml/JSP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0MVC-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Mon, 07 Jul 2025 14:34:39 GMT</pubDate>
            <description><![CDATA[<h1 id="프로젝트-구조-왜-중요할까">프로젝트 구조, 왜 중요할까?</h1>
<p>나도 그랬듯이 초보자들은 프로젝트를 시작할 때, 모든 코드를 파일 하나에 다 넣으려고 한다. </p>
<p>하지만 JSP를 예로 들었을때 HTML, CSS, JavaScript, 자바 로직, 데이터베이스 연결 코드까지 한 파일에 뒤섞이면 처음에는 빠르게 결과물이 나오는 것처럼 보인다. </p>
<p>하지만 프로젝트가 조금만 커져도, 그 코드는 누구도 손대고 싶지 않은 <strong>&#39;스파게티 코드&#39;</strong>가 되어버린다.</p>
<p>잘못된 프로젝트 구조는 유지보수를 악몽으로 만들고, 팀원과의 협업을 불가능하게 한다. </p>
<p>프로젝트에서 왜 구조가 중요한지, 그리고 가장 표준적으로 사용되는 <strong>MVC 패턴</strong>을 기반으로 어떻게 깔끔한 구조를 만들 수 있는지 설명하겠다.</p>
<hr>
<h2 id="왜-프로젝트-구조가-중요한가">왜 프로젝트 구조가 중요한가?</h2>
<p><strong>역할 분담</strong> 
디자이너는 화면(View)에, 개발자는 로직(Controller, Model)에 집중할 수 있게 한다.</p>
<p><strong>유지보수성</strong> 
&quot;로그인 로직을 수정해야지&quot; 라고 생각했을 때, <code>LoginServlet.java</code>와 <code>MemberDAO.java</code>만 보면 되도록 만들어준다. 
수많은 JSP 파일을 뒤질 필요가 없다.</p>
<p><strong>재사용성</strong> 
잘 만들어진 데이터베이스 처리 로직(<code>MemberDAO</code>)은 회원 정보가 필요한 어떤 페이지에서든 재사용할 수 있다.</p>
<p><strong>협업 효율성</strong> 
모든 팀원이 파일이 어디에 위치하고 어떤 역할을 하는지 예측할 수 있어, 코드 충돌을 줄이고 개발 속도를 높인다.</p>
<hr>
<h2 id="jsp-프로젝트의-표준-구조-mvc-패턴">JSP 프로젝트의 표준 구조: MVC 패턴</h2>
<p>JSP/Servlet 웹 애플리케이션의 구조를 잡는 가장 표준적인 방법은 <strong>MVC 패턴</strong>이다. 이 패턴은 프로젝트를 세 가지 역할로 명확하게 나눈다.</p>
<p><strong>Model</strong> 
데이터 그 자체(DTO/VO)와 데이터를 처리하는 비즈니스 로직(Service, DAO). 순수 자바 클래스로 구성된다.</p>
<p><strong>View</strong> 
사용자에게 보여지는 화면. <strong>JSP 파일</strong>이 이 역할을 전담한다.</p>
<p><strong>Controller</strong> 
사용자의 요청을 받고, 모델과 뷰를 연결하는 중재자. <strong>서블릿(Servlet)</strong>이 이 역할을 맡는다.</p>
<h3 id="예시-기본적인-mvc-프로젝트-폴더-구조">예시: 기본적인 MVC 프로젝트 폴더 구조</h3>
<p>이클립스의 &#39;Dynamic Web Project&#39; 기준으로, 다음과 같은 구조를 따르는 것이 일반적이다.</p>
<pre><code>my-web-project/
├── src/main/java/          # 순수 자바 코드가 위치하는 곳 (Model, Controller)
│   └── com/example/
│       ├── controller/     # 1. Controller (서블릿)
│       │   ├── LoginServlet.java
│       │   └── JoinServlet.java
│       ├── model/          # 2. Model (데이터 관련 클래스)
│       │   ├── dao/        #    - DAO (Data Access Object)
│       │   │   └── MemberDAO.java
│       │   └── dto/        #    - DTO (Data Transfer Object)
│       │       └── MemberDTO.java
│       └── util/           # 3. 유틸리티 클래스
│           └── DBManager.java  (DB 커넥션 풀 관리 등)
│
├── src/main/webapp/        # 웹 콘텐츠가 위치하는 곳 (View)
│   ├── WEB-INF/
│   │   ├── web.xml         #    - 서블릿 매핑 등 웹 애플리케이션 설정 파일
│   │   └── lib/            #    - 외부 라이브러리 (ex: ojdbc.jar)
│   ├── member/             # 기능별로 폴더를 나누면 좋음
│   │   ├── login.jsp
│   │   └── join.jsp
│   ├── main.jsp
│   ├── index.jsp
│   └── css/
│       └── style.css
│
└── pom.xml                 # (Maven 프로젝트인 경우) 의존성 관리 파일</code></pre><h3 id="각-폴더의-역할">각 폴더의 역할</h3>
<ol>
<li><p><strong><code>src/main/java</code>
백엔드 로직의 핵심</strong>
<strong><code>controller</code> 패키지</strong> 
사용자의 모든 <code>HTTP</code> 요청(<code>GET</code>, <code>POST</code>)을 가장 먼저 받는 서블릿들이 위치한다. 서블릿은 요청을 분석해서 어떤 모델을 사용할지, 어떤 뷰를 보여줄지 결정하는 &#39;교통경찰&#39; 역할을 한다.</p>
<p><strong><code>model.dao</code> 패키지</strong> 
<code>MemberDAO</code>처럼 데이터베이스에 직접 접근하여 <code>INSERT</code>, <code>SELECT</code>, <code>UPDATE</code>, <code>DELETE</code> (CRUD) 로직을 전담하는 클래스들이 모여있다.</p>
<p><strong><code>model.dto</code> 패키지</strong> 
<code>MemberDTO</code>처럼 데이터베이스 테이블의 정보를 행(Row) 단위로 담기 위한 객체(JavaBean)들이 위치한다. 계층 간 데이터 전송을 위한 &#39;데이터 그릇&#39;이다.</p>
</li>
<li><p><strong><code>src/main/webapp</code></strong></p>
</li>
</ol>
<p><strong>사용자의 눈에 보이는 모든 것</strong>
    내가 앞으로 공유할 코드들과 나의 Github에 있는 코드들은 교수님께 배운대로 모두 경로를 WebContents 바꿔놨다.</p>
<p>  이클립스 기준으로 프로젝트 생성시에 경로를 바꿀 수 있다.</p>
<p>   이 폴더 아래의 파일들은 URL을 통해 직접 접근이 가능하다. (단, <code>WEB-INF</code> 폴더 제외)</p>
<p>   <strong><code>.jsp</code> 파일 (View)</strong> 
   컨트롤러(서블릿)로부터 전달받은 데이터를 화면에 동적으로 그리는 역할을 한다. 자바 로직은 최소화하고, 화면 표시에만 집중해야 한다.</p>
<p>   <strong><code>css</code>, <code>js</code>, <code>images</code> 폴더</strong> 
   정적인 리소스 파일들을 위치시킨다.</p>
<ol start="3">
<li><strong><code>WEB-INF</code></strong> </li>
</ol>
<p><strong>보안이 필요한 핵심 설정 공간</strong>
    이 폴더 안의 파일들은 클라이언트가 URL로 직접 요청할 수 없어 보안에 유리하다.</p>
<p>   <strong><code>web.xml</code></strong> 
   배포 서술자(Deployment Descriptor)라고 부른다. 어떤 URL 요청을 어떤 서블릿으로 연결할지(서블릿 매핑), 필터, 리스너 등 웹 애플리케이션의 핵심 설정을 정의한다.</p>
<p>   <strong><code>lib</code></strong> 
   <code>jdbc.jar</code> 같은 외부 라이브러리(JAR 파일)를 위치시키는 곳이다.</p>
<h3 id="파일이-함께-동작하는-방식-로그인-과정-예시">파일이 함께 동작하는 방식 (로그인 과정 예시)</h3>
<ol>
<li><p><strong>사용자 요청</strong> 
사용자는 <code>login.jsp</code>에서 아이디와 비밀번호를 입력하고 &#39;로그인&#39; 버튼을 누른다. 폼의 <code>action</code>은 <code>/login</code>을 가리킨다.</p>
</li>
<li><p><strong>Controller (서블릿) 처리</strong> 
<code>web.xml</code>에 <code>/login</code> 요청을 처리하도록 매핑된 <code>LoginServlet</code>이 요청을 가로챈다.</p>
<p><code>LoginServlet</code>은 <code>request.getParameter()</code>로 사용자가 입력한 아이디와 비밀번호를 받는다.</p>
<p><code>MemberDAO</code> 객체를 생성하고, <code>dao.loginCheck(id, pw)</code> 메서드를 호출하여 DB에 해당 사용자가 있는지 확인한다.</p>
</li>
<li><p><strong>Model (DAO, DTO) 작업:</strong>
<code>MemberDAO</code>는 커넥션 풀에서 DB 커넥션을 얻어와 <code>SELECT</code> 쿼리를 실행한다.</p>
<p>결과가 있다면 로그인 성공, 없으면 실패로 판단하고 그 결과를 <code>LoginServlet</code>에 반환한다.</p>
</li>
<li><p><strong>View (JSP)로 전달 및 표시:</strong></p>
<p><strong>로그인 성공 시:</strong> <code>LoginServlet</code>은 <code>session.setAttribute(&quot;userId&quot;, id)</code>로 세션에 로그인 정보를 기록한 뒤, <code>response.sendRedirect(&quot;main.jsp&quot;)</code>를 통해 메인 페이지로 사용자를 보낸다.</p>
<p><strong>로그인 실패 시:</strong> 에러 메시지를 <code>request</code>에 담아 <code>RequestDispatcher</code>를 이용해 다시 <code>login.jsp</code>로 포워딩한다. <code>login.jsp</code>는 전달받은 에러 메시지를 화면에 표시한다.</p>
</li>
</ol>
<hr>
<p>이처럼 역할에 따라 <strong>프로젝트 구조</strong>를 명확히 나누는 것은 선택이 아닌 필수이다. </p>
<p><strong>MVC 패턴</strong>은 복잡한 웹 애플리케이션을 여러 개발자가 협업하여 만들고, 오랫동안 안정적으로 유지보수할 수 있게 하는 가장 검증되고 안정적인 방법이다.</p>
<p>실무에서 쓰이는 개념들을 하나하나 익힐수록 개인 프로젝트도 관리가 편해지고 뭔가 효율적이고 깔끔한 코딩을 하게되는 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git] Git & GitHub의 개념과 기초 사용법]]></title>
            <link>https://velog.io/@kt_gml/Git-Git-GitHub%EC%9D%98-%EA%B0%9C%EB%85%90%EA%B3%BC-%EA%B8%B0%EC%B4%88-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@kt_gml/Git-Git-GitHub%EC%9D%98-%EA%B0%9C%EB%85%90%EA%B3%BC-%EA%B8%B0%EC%B4%88-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Mon, 07 Jul 2025 07:49:32 GMT</pubDate>
            <description><![CDATA[<h2 id="개발자의-필수-도구-git--github">개발자의 필수 도구, Git &amp; GitHub</h2>
<p><code>최종_보고서.hwp</code>
<code>진짜최종_보고서.hwp</code>
<code>진짜진짜최종_수정본_보고서.hwp</code>
<code>교수님께_보낼파일_최종.hwp</code></p>
<p>우리는 모두 파일의 버전을 관리하느라 고통받아 본 경험이 있다. </p>
<p>개발 프로젝트는 수십, 수백, 수만 개의 파일로 이루어져 있고, 수많은 사람이 동시에 작업한다. </p>
<p>위와 같은 방식으로 파일 버전을 관리하며 이메일로 파일을 주고받기엔 한계가 있다.</p>
<p>이때 사용하는 도구가 바로 <strong>Git</strong>과 <strong>GitHub</strong>이다.</p>
<hr>
<h2 id="git--github-무엇이-다른가">Git &amp; GitHub, 무엇이 다른가?</h2>
<p>가장 먼저 이 둘의 관계를 명확히 해야 한다. </p>
<p><strong>Git은 &#39;도구&#39;이고, GitHub는 그 도구를 활용하는 &#39;서비스&#39;이다.</strong></p>
<blockquote>
<p><strong>Git (소프트웨어):</strong> 버전 관리 <strong>&#39;소프트웨어(도구)&#39;</strong> </p>
</blockquote>
<p>내 컴퓨터에 설치하여 코드의 변경 이력을 사진 찍듯이 저장하고, 언제든 과거로 돌아갈 수 있는 타임머신 역할을 한다. </p>
<p>인터넷 없이도 내 컴퓨터에서 모든 버전 관리가 가능하다.</p>
<blockquote>
<p><strong>GitHub (웹 서비스):</strong> Git으로 관리하는 프로젝트를 올려두는 <strong>&#39;웹 호스팅 서비스&#39;</strong> </p>
</blockquote>
<p>Git이라는 도구만으로는 힘든 <strong>백업, 공유, 협업</strong>을 가능하게 해준다.</p>
<hr>
<h2 id="왜-반드시-함께-사용해야-하는가">왜 반드시 함께 사용해야 하는가?</h2>
<ol>
<li><strong>완벽한 버전 관리 (Git) + 안전한 클라우드 백업 (GitHub)</strong> 
Git으로 내 코드의 모든 변경사항을 기록하고, GitHub에 올려두면 내 컴퓨터가 고장 나도 코드는 안전하다.</li>
<li><strong>체계적인 개인 프로젝트 관리 (Git) + 훌륭한 공개 포트폴리오 (GitHub)</strong> 
Git으로 내 프로젝트를 관리하고 GitHub에 꾸준히 올리는 것은, &quot;나는 이렇게 코딩하고 성장해왔다&quot;를 보여주는 가장 확실한 이력서가 된다.</li>
<li><strong>강력한 독립 작업 (Git Branch) + 체계적인 팀 협업 (GitHub Pull Request)</strong> 
Git의 브랜치 기능으로 내 작업을 독립적으로 수행하고, GitHub의 Pull Request 기능으로 팀원들에게 코드 리뷰를 요청하며 안전하게 코드를 병합할 수 있다.</li>
</ol>
<hr>
<h2 id="기초-사용법">기초 사용법</h2>
<h3 id="1-최초-설정-컴퓨터에-딱-한-번만">1. 최초 설정 (컴퓨터에 딱 한 번만)</h3>
<p>Git을 설치한 후, 커밋에 기록될 내 정보를 설정한다.</p>
<p>Git Bash를 설치하고 다음을 입력한다. </p>
<p>이때 복사와 붙여넣기는 <code>ctrl + c</code> <code>ctrl + v</code> 대신 <code>ctrl + insert</code> <code>shift + insert</code>로 사용한다.</p>
<pre><code class="language-bash">git config --global user.name &quot;Your Name&quot;
git config --global user.email &quot;your.email@example.com&quot;</code></pre>
<h3 id="2-로컬-프로젝트-생성-및-github에-올리기">2. 로컬 프로젝트 생성 및 GitHub에 올리기</h3>
<p><strong>1단계: 내 컴퓨터에서 Git 관리 시작</strong></p>
<pre><code class="language-bash"># 프로젝트 폴더를 만들고 이동
mkdir my-project
cd my-project

# 이 폴더를 Git으로 관리 시작!
git init</code></pre>
<p>아니면 vscode에서 터미널 창에서 bash를 선택하거나 프로젝트 폴터에서 우클릭 후 이 경로에서 bash 실행하기를 통해 생략할 수 있다.</p>
<p><strong>2단계: 코드 수정 및 로컬에 버전 저장 (add &amp; commit)</strong></p>
<pre><code class="language-bash"># 변경된 모든 파일을 스테이징(장바구니에 담기)
git add .

# 장바구니에 담긴 내용을 하나의 버전으로 저장(커밋)
git commit -m &quot;Initial commit: Add README.md&quot;</code></pre>
<p><strong>3단계: GitHub 원격 저장소 생성 및 연결</strong></p>
<ol>
<li><p><strong>GitHub</strong>에 로그인하여 <code>New repository</code> 버튼으로 새로운 저장소를 만든다. </p>
</li>
<li><p>생성된 저장소의 URL(예: <code>https://github.com/YourName/my-project.git</code>)을 복사한다.</p>
</li>
<li><p>내 컴퓨터 터미널에서 아래 명령어로 로컬 저장소와 원격 저장소를 연결한다.</p>
</li>
</ol>
<pre><code class="language-bash"># 원격 저장소 주소를 &#39;origin&#39;이라는 이름으로 추가
git remote add origin https://github.com/YourName/my-project.git

# 로컬의 main 브랜치를 원격 저장소(origin)로 업로드(push)
git push -u origin main</code></pre>
<p>이제 <code>my-project</code>는 내 컴퓨터와 GitHub 양쪽에 모두 존재하며 서로 연결되었다.</p>
<h3 id="3-협업의-기본-사이클-pull-→-branch-→-commit-→-push-→-pull-request">3. 협업의 기본 사이클: Pull → Branch → Commit → Push → Pull Request</h3>
<p>팀 프로젝트에 참여할 때의 일반적인 작업 흐름이다.</p>
<p><strong>1단계: 작업 시작 전, 항상 최신 코드로 동기화</strong></p>
<p>다른 팀원이 수정했을지 모르는 최신 코드를 원격 저장소에서 내 컴퓨터로 가져온다.</p>
<pre><code class="language-bash">git pull origin main</code></pre>
<p><strong>2단계: 나만의 작업 공간(브랜치) 만들기</strong></p>
<p><code>main</code> 브랜치를 직접 건드리지 않고, 새로운 기능을 개발하기 위한 독립된 브랜치를 만든다.</p>
<pre><code class="language-bash"># &#39;feature/login&#39; 이라는 브랜치를 만들고 그곳으로 이동
git checkout -b feature/login</code></pre>
<p><strong>3단계: 기능 개발 및 커밋</strong></p>
<p>이제 <code>feature/login</code> 브랜치에서 마음껏 코드를 수정하고, <code>add</code>와 <code>commit</code>으로 작업 단위를 저장한다.</p>
<pre><code class="language-bash"># (열심히 코딩...)
git add .
git commit -m &quot;Feat: Implement user login logic&quot;</code></pre>
<p><strong>4단계: 내 브랜치를 원격 저장소에 업로드</strong></p>
<p>내가 만든 <code>feature/login</code> 브랜치를 팀원들이 볼 수 있도록 GitHub에 올린다.</p>
<pre><code class="language-bash">git push origin feature/login</code></pre>
<p><strong>5단계: Pull Request(PR) 생성</strong></p>
<p><strong>가장 중요한 협업 과정이다.</strong></p>
<ol>
<li><strong>GitHub</strong> 사이트로 이동하면 &quot;<code>feature/login</code> 브랜치에 변경사항이 있으니 Pull Request를 생성할래요?&quot; 라는 초록색 버튼이 보인다.</li>
<li>버튼을 눌러 내가 작업한 내용과 리뷰를 요청하는 메시지를 작성하고 PR을 생성한다.</li>
<li>이제 팀원들은 내 코드를 보고 의견을 남길 수 있다. 리뷰가 끝나고 승인이 나면, 관리자가 <code>Merge</code> 버튼을 눌러 내 코드를 <code>main</code> 브랜치에 안전하게 합친다.</li>
</ol>
<hr>
<p>Git과 GitHub는 더 이상 선택이 아닌 <strong>필수</strong>라고 한다.</p>
<p>협업할때 뿐만 아니라 개인 프로젝트를 할때도 버전을 효율적으로 관리하고 포트폴리오로도 활용하기 위해 Git이 중요한 것 같다.</p>
<p>이전에 진행하였던 나의 프로젝트도 GitHub에 업로드해서 수정사항들을 더 체계적으로 관리하고 새로운 업데이트가 생겼을때 bash를 이용해 더욱 편하게 관리할 수 있을 것이다.</p>
<p>앞으론 터미널의 claude code와 Mcp를 활용해보며 교수님이 학기중에 수도 없이 말씀하신 바이브 코딩 능력을 길러봐야겠다.</p>
]]></description>
        </item>
    </channel>
</rss>