<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>mi_nini.log</title>
        <link>https://velog.io/</link>
        <description>發現(발현)</description>
        <lastBuildDate>Mon, 24 Aug 2026 03:17:25 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>mi_nini.log</title>
            <url>https://velog.velcdn.com/images/mi_nini/profile/a22cf82c-0974-4fdd-8e66-6a5dc92058aa/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. mi_nini.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/mi_nini" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[2026년 제8회 K-디지털 트레이닝 해커톤]]></title>
            <link>https://velog.io/@mi_nini/2026%EB%85%84-%EC%A0%9C8%ED%9A%8C-K-%EB%94%94%EC%A7%80%ED%84%B8-%ED%8A%B8%EB%A0%88%EC%9D%B4%EB%8B%9D-%ED%95%B4%EC%BB%A4%ED%86%A4</link>
            <guid>https://velog.io/@mi_nini/2026%EB%85%84-%EC%A0%9C8%ED%9A%8C-K-%EB%94%94%EC%A7%80%ED%84%B8-%ED%8A%B8%EB%A0%88%EC%9D%B4%EB%8B%9D-%ED%95%B4%EC%BB%A4%ED%86%A4</guid>
            <pubDate>Mon, 24 Aug 2026 03:17:25 GMT</pubDate>
            <description><![CDATA[<p>우리 팀 춘배가 이번 해커톤에 주제는 <strong>딥소각</strong>이다. AI 얼굴합성물(딥페이크) 피해를 예방(딥백신)·탐지(얼굴가드)·대응(증거금고·즉각소각)·회복(회복나침반)까지, 개인이 스마트폰 하나로 다룰 수 있게 만드는 서비스다. 업로드 전 사진에 노이즈를 입혀 합성을 방해하고, 유포된 합성물을 자동으로 찾아주고, 증거를 안전하게 보존하고, 신고서 초안까지 만들어준다 — 피해의 전 과정을 하나로 엮은 게 딥소각이다. 이 글은 그 서비스를 만들며 지금까지 어떤 과정을 거쳐왔는지 정리한 기록이다.</p>
<p>이 해커톤이 뭔지부터 짧게 적어둔다. K-디지털 트레이닝(KDT)은 고용노동부가 국민내일배움카드로 훈련비를 지원해서, 미래 노동시장을 이끌 디지털·신기술 인재를 키우는 사업이다. 그 안에서 훈련생·수료생들이 실제 팀 프로젝트로 실력을 겨루고 서비스 개발 경험을 쌓아보라고 매년 여는 게 이 해커톤이고, 올해로 8회째다(주최 고용노동부, 주관 한국기술교육대학교 직업능력심사평가원). 팀은 3~6명으로 구성해야 하는데, 우리 춘배는 김경민·김다애·최현준으로 저번 공모전과 같은 인원으로 참여했다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/22d547d1-354f-4571-a21f-7ad4a3a6b9c2/image.png" alt=""></p>
<p>전체 300여 팀 중 멘토링을 받으며 기획서를 다듬는 팀이 60팀으로 추려지고, 그중 본선에 진출하는 30팀이 최종 확정되는데, 이 30팀은 본선에 오르는 순간 수상이 보장된다. 대상(국무총리상, 지정과제 1팀)이 1,000만 원, 아이디어상(직업능력심사평가원 원장상)이 30만 원, 30팀 전체 상금 규모는 총 6,080만 원이다. 우리는 자유과제로 참가했으니, 우리 쪽에서 노려볼 수 있는 제일 높은 등급은 최우수상(고용노동부 장관상, 500만 원)이였다.</p>
<hr>
<h2 id="문제의식-피해자는-자신이-피해자인-줄-모른다">문제의식: 피해자는 자신이 피해자인 줄 모른다</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/1760edc1-9d39-47a7-a0e6-2c7807377221/image.png" alt=""></p>
<p>준비를 시작하며 가장 먼저 마주한 게 이 기사였다. 2024년 AI 얼굴합성물 피해 접수는 1,384건으로 전년(423건) 대비 227.2% 급증했고, 피해자의 92.6%가 10~20대, 96.6%가 여성이었다(중앙디지털성범죄피해자지원센터). 더 눈에 밟혔던 건 피해 유형 1위가 실제 유포가 아니라 &#39;유포 불안&#39;(전체의 25.9%)이었다는 사실이다. 수많은 사람이 &quot;내 얼굴도 어딘가에서 합성되고 있지 않을까&quot;라는 불안 속에 살고 있는데, 정작 그걸 확인할 방법이 개인에게는 없었다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/15a86f3e-94d0-40b4-b8a4-83b8d8b66ee8/image.png" alt=""></p>
<p>법과 제도는 이미 준비돼 있었다. 2024년 10월 성폭력처벌법 개정으로 제작·유포는 7년 이하 징역, 소지·시청도 처벌 대상이 됐고, 2026년 1월 AI기본법 시행으로 AI 합성물에는 AI 생성 표시 의무가 생겼다. 그런데 이걸 개인이 실제로 작동시킬 수단이 없었다. 서울시 AI 삭제 시스템이나 Reality Defender 같은 솔루션도 있었지만 전부 기업·기관 전용(B2B)이었다. 이 공백을 메우자는 게 딥소각의 출발점이었다.</p>
<hr>
<h2 id="신청서부터-킥오프까지">신청서부터 킥오프까지</h2>
<p>7월 8일에 참가 신청서와 서약서에 서명하면서 팀이 공식적으로 꾸려졌다. 정식 오리엔테이션(킥오프)은 7월 22일이었고, 계획은 6주 간트 차트로 짰다 —&gt; 1주차 준비·설계, 2주차 예선·기반 구축, 3<del>4주차 핵심 기능 개발, 5</del>6주차 통합·테스트.
<img src="https://velog.velcdn.com/images/mi_nini/post/a81bd288-a857-44dd-8528-70dfda444fd6/image.png" alt=""></p>
<p>기술스택을 확정하는 과정에서 공식 문서와 약관을 직접 검색해 확인하다가, 원 계획대로면 실행이 막히거나 약관 위반 소지가 있는 지점을 세 군데 발견했다.</p>
<blockquote>
<ol>
<li>Google Custom Search API 신규 발급이 이미 중단됐고, 2027-01-01 완전 종료가 예정돼 있었다. → 역이미지 검색을 Google Cloud Vision API로 교체했다.</li>
<li>텔레그램 Bot Platform Developer ToS가 &quot;AI/ML 모델 개발·학습을 위한 공개 채널·그룹 콘텐츠 스크래핑·하베스팅·집계&quot;를 명시적으로 전면 금지하고 있었다. → 자동 순찰에서 텔레그램을 제외하고, 사용자 수동 제보로 MVP 범위를 좁혔다.</li>
<li>방심위(KOCSC) 온라인 신고센터의 공개 제출 API 존재 여부를 재검색으로도 확인하지 못했다. &quot;없다&quot;를 적극적으로 증명할 수는 없었지만, 확인이 안 된 채로 &quot;자동 접수&quot;를 약속할 수는 없었다. → &quot;신고서 자동 생성&quot;까지만 자동화하고, 실제 접수(발송)는 사용자가 최종 클릭을 직접 하도록 설계를 확정했다.</li>
</ol>
</blockquote>
<p>숫자도 다시 검증했다. AI-Hub 승인 절차는 &quot;자동승인 정책이 있다&quot;는 기존 인식과 달리 휴대폰 본인인증 기반 승인이 공식 절차였고, SerpApi·g5.2xlarge 단가처럼 처음 잘못 알고 있던 수치들도 재검색으로 바로잡았다.</p>
<hr>
<h2 id="멘토링-세-번-그리고-기획서가-바뀐-과정">멘토링 세 번, 그리고 기획서가 바뀐 과정</h2>
<h3 id="멘토링-1--다섯-가지-질문">멘토링 1 — 다섯 가지 질문</h3>
<p>가장 먼저는 창업기획(예비·초기·도약 단계) 지원, ICT·AI·클라우드 25년 이상 경력에 모바일 앱 분야 15년 경력을 가진 멘토님께 사업성 관점의 피드백을 받았다. 직접 준비해간 질문은 다섯 가지였다.</p>
<blockquote>
<ol>
<li>[시장성] 이게 팔릴까요? 문제는 명확하고 관련 자료도 많은데, 실제 수요가 있을까요?</li>
<li>[기술] 기능적으로 덜어내거나 추가해야 할 부분이 있을까요?</li>
<li>[클라우드] 데이터셋(얼굴·각도·한국인 특화 등)이 워낙 많고 다양해서 로컬 작업이 힘들 것 같은데, 서버를 쓰는 방향으로 가야 할까요?</li>
<li>[기술] 지금은 이미지로만 딥페이크를 판별하는데, 영상까지 고려해야 할까요? 고려한다면 어떻게?</li>
<li>[기획·법적] 서비스 기획·운영 단계에서 법적으로 어디까지 고려해야 할까요?</li>
</ol>
</blockquote>
<p>&quot;마켓이 너무 작다. 스케일업하는 방향성을 생각해보자.&quot; 지금 타깃(만 19~24세 여성 대학생)만으로는 시장이 좁아 보인다는 지적이었고, PSST 전략으로 기획서를 다시 짜보라는 조언도 받았다.</p>
<blockquote>
<p>PSST이란?
Problem(문제인식)·Solution(솔루션)·Scale-up(성장전략)·Team(팀 역량)의 앞글자를 딴 사업계획서 구조로, 중소벤처기업부 계열 창업지원사업(예비창업패키지 등)에서 표준 양식처럼 쓰인다고 한다. 심사관이 이 네 블록에 각각 배점을 매기는 평가 프레임이라, &quot;마켓이 너무 작다&quot;는 지적은 정확히 Scale-up 항목을 겨냥한 것이었다.</p>
</blockquote>
<p>두괄식·개조식으로 쓰되 한 주제가 두 줄을 넘어가지 않게 하라는 것, MVP UI를 반드시 넣으라는 것, 그리고 5번 질문(법적 고려사항)에 대한 답으로는 오히려 &quot;페르소나를 만들어보라&quot;는 역질문을 받았다. 실제 사용자 한 명을 구체적으로 그려보면 법적으로 어디까지 고려해야 하는지가 자연스럽게 드러난다는 취지였다. 여기서 또 하나의 방향 전환이 나왔다 — &quot;사전처리&quot;라는 개념이다. 이미 유포된 피해에 대응하는 것도 중요하지만, 불안해하는 사람들에게 사전에 제공하는 서비스(지금의 딥백신·소각 지수 라인)를 더 키워보자는 제안이었다.</p>
<h3 id="멘토링-2--기획서-첫-페이지는-유튜브-썸네일이다">멘토링 2 — &quot;기획서 첫 페이지는 유튜브 썸네일이다&quot;</h3>
<p>이경엽 멘토님께 받은 피드백은 냉정했다. 심사위원은 60개 팀을 봐야 하고, 팀당 5<del>10분, 10페이지짜리 기획서라면 페이지당 20</del>30초밖에 못 본다는 것. 팀당 1시간씩만 들여도 총 60시간인데, 심사위원이 그만큼 시간을 내줄 리 없다는 계산이었다. (실제로는 더 짧게 기획서를 읽고 심사를 하는 경우가 많다라고 전달받았다) 심사위원의 인지부하를 줄이는 게 핵심이라는 말이 가장 기억에 남는다.</p>
<p>구체적으로 이런 걸 배웠다.</p>
<blockquote>
<ul>
<li>기획서도 논문처럼 두괄식으로 — Introduction(추진배경) → Related works(경쟁 서비스 비교) → Main approach(핵심 기술) → Conclusion(기대효과) 순서</li>
</ul>
</blockquote>
<ul>
<li>문제는 숫자와 함께 강해진다 — &quot;저출산이 문제다&quot;가 아니라 &quot;출산율 0.7명대&quot;처럼, 출처 각주도 바로바로</li>
<li>경쟁 서비스는 표로 — O/X 기호로 한눈에, 매출·MAU 같은 지표까지 넣어서 &quot;듣보잡 아니다&quot;를 보여주기</li>
<li>기획서 형식(대주제 5개: 추진배경/개발목표및내용/주요특징및핵심기술/기대효과및활용방안/개발추진체계)은 규정이라 절대 못 바꾼다. 소제목만 자유</li>
</ul>
<p>이 피드백을 받고 경쟁 서비스 비교표, 시장 규모(TAM/SAM/SOM), 비즈니스 모델을 다시 짰다. TAM/SAM/SOM은 시장 규모를 세 겹의 원으로 좁혀가며 보여주는 방식인데(전체 잠재 시장 → 실제 접근 가능한 시장 → 정해진 기간 안에 확보할 시장), 우리는 국내 중·고교·대학 6,008개교(약 1,442억 원)에서 초기 판매 대상 대학 329개교(약 79억 원), 3년 내 유료 전환 대학 3<del>5개교(ARR 0.72</del>1.2억 원)까지 이렇게 좁혀서 제시했다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/2d1ebcc1-72c7-4dc3-b90c-f212282ec7fa/image.png" alt=""></p>
<h3 id="멘토링-3--김밥천국이-아니라-미슐랭-원스타-국밥집이-되어야-한다">멘토링 3 — &quot;김밥천국이 아니라 미슐랭 원스타 국밥집이 되어야 한다&quot;</h3>
<p>세 번째 멘토링은 기술력 파트(3번 섹션, 배점 30점)에 집중됐다. 핵심 한마디는 &quot;3번 섹션에서는 개발하는 모든 기술을 나열하지 마라&quot;였다. 기능 리스트는 2번 섹션에서 끝내고, 3번에서는 핵심 기능 하나를 깊게 파야 한다는 것.</p>
<p>기술력 점수를 얻는 방법에는 난이도별 단계가 있다는 조언도 나왔다.</p>
<blockquote>
<ul>
<li>[불가능] 원천 기술보다 더 나은 걸 정량화하기 — 사실상 논문 수준</li>
</ul>
</blockquote>
<ul>
<li>[상] 기술 가설을 3단계로 나눠서, 단계마다 실행하고 한계를 극복하는 이터레이션을 보여주기 ← 우리가 택한 방식</li>
<li>[중상] 가설 하나라도 세워서 달성하기</li>
<li>[중] Baseline과 정량 비교</li>
<li>[하] 최소한 &quot;왜 이 기술을 선택했는지&quot;라도 설명</li>
</ul>
<p>그리고 계속 곱씹게 되는 한 문장이 나왔다. &quot;한계를 적지 않는다는 건 한계를 모른다는 뜻일 수 있다.&quot; 완벽한 척하지 말고, 뭘 못했는지 정직하게 쓰고 어떻게 극복할지 방법론을 제시하는 게 오히려 가산점이라는 것이었다. 여기서 얼굴가드의 v1(ArcFace 단독) → v2(ArcFace + Cloud Vision API) → v3(한국인 데이터 파인튜닝) 3단계 실험 설계가 나왔다.</p>
<h4 id="-추가로-받았던-피드백">+ 추가로 받았던 피드백</h4>
<p>&quot;3장에서 예시·가이드 자체보다 살릴 수 있는 부분을 더 크게 채워야 한다&quot;는 얘기와 함께, &quot;왜 이 글에 이 이미지를 썼는지 잘 모르겠다, 안 읽힌다&quot;는 지적을 받았다. 어떤 이미지를 말하는 건지 곱씹어보면서 몇 개는 급하게 다시 손봤다, 줄이는 것보다 지금 있는 글에 의미를 더 채워 넣는 게 낫겠다는 피드백을 수용했다.</p>
<hr>
<h2 id="세-번의-멘토링을-반영해-실제로-뜯어고친-것들">세 번의 멘토링을 반영해 실제로 뜯어고친 것들</h2>
<h3 id="역이미지-검색과-벡터-대조의-역할을-재배치했다">역이미지 검색과 벡터 대조의 역할을 재배치했다.</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/d0fef8cf-9a89-405b-8052-08def4383397/image.png" alt=""></p>
<p>원래는 &quot;역이미지 검색으로 후보 수집 → 얼굴 유사도 필터 → AI 합성 판별&quot; 순서였는데, &quot;얼굴 원본을 단말 밖으로 안 내보낸다&quot;는 원칙과 충돌했다. 역이미지 검색은 이미지 자체를 입력해야 작동하기 때문이다. 그래서 등록된 얼굴 임베딩을 기준으로 Cloud Vision API가 넓게 후보를 물어오고, ArcFace가 그중에서 정밀하게 걸러내는 구조로 재배치했다.</p>
<h3 id="증거-보관-방식도-통째로-바꿨다">증거 보관 방식도 통째로 바꿨다.</h3>
<p>개정 성폭력처벌법은 허위영상물의 소지·시청·저장만으로도 처벌 대상으로 규정한다. 원래 설계(스크린샷·아카이브를 서버에 원본 그대로 보관)는 서비스 운영자인 우리 팀이 그 콘텐츠를 &quot;소지&quot;하는 셈이 될 수 있었다. 그래서 원본은 피해자 단말에 암호화 저장하고, 서버는 SHA-256 해시·URL·시각·TSA 토큰 같은 메타데이터만 보관하는 구조로 바꿨다.</p>
<h3 id="알림망-설계도-손을-봤다">알림망 설계도 손을 봤다.</h3>
<p>처음엔 &quot;그룹 내 피해 발생 시 익명으로 그룹 전체에 경보&quot;였는데, 학교처럼 좁은 공동체에서는 경보가 왔다는 사실 자체가 &quot;이 안에 피해자가 있다&quot;는 정보를 노출시킬 수 있었다. 그래서 모든 그룹에 동일한 주기·문구로 나가는 &quot;정기 점검 위장&quot; 알림으로 바꾸고, 실제 경보 발송 여부는 피해자 본인의 명시적 동의를 거치도록 했다. 기관명도 놓치지 않았다 — 방심위(방송통신심의위원회)가 2025년 10월 조직 개편으로 &quot;방송미디어통신심의위원회&quot;로 이름이 바뀌어 있었는데, 기획서 전체의 표기를 일괄 수정했다.</p>
<h3 id="비즈니스-모델-방향도-바뀌었다">비즈니스 모델 방향도 바뀌었다.</h3>
<p>처음엔 개인 유료 구독을 중심으로 그렸는데, 멘토님 피드백은 &quot;개인은 보안에 돈을 많이 쓰지 않는다&quot;는 것이었다. 예방 기능에 매달 돈을 내라고 하기보다, 실제로 피해를 입었을 때 &quot;지우게 해주세요&quot;가 더 팔릴 거라는 조언이었다. 그래서 기술을 앞세우기보다 공익적인 측면(뉴스 기사 인용 등)을 강조하는 쪽으로 서술 방향을 바꿨고, 이게 나중에 대학·교육기관 대상 B2B2C 모델(학생은 무료로 보호받고, 대학이 「고등교육법」상 보호 의무 이행 도구로 도입)로 이어졌다.</p>
<h3 id="추진배경도-수정">추진배경도 수정</h3>
<p>숫자는 잘 썼지만 직접적인 출처 표시가 안 된 부분이 있었고, 인용한 기사에서 강조할 부분엔 밑줄이나 박스를 쳐야 시선을 사로잡는다는 것, &quot;하나의 숫자로 표현할 수 있는 임팩트&quot;가 아직 없다는 것, 그리고 무엇보다 &quot;기획안은 잘썻지만 아쉽다는&quot;는 평이었다. 피해 사례를 좀 더 자극적이고 후킹되게 앞부분에 배치해야 한다는 지적을 받고, 기사 화면 캡처에 강조 표시를 넣고 그래프를 여는 순간 바로 심각성이 느껴지도록 앞부분을 다시 짰다.</p>
<p>경쟁 서비스 비교표에도 지적이 있었다. O/X 표만으로는 &quot;이게 어떤 서비스인지&quot; 감이 안 온다는 것 — 그래서 표 아래에 각 서비스가 뭘 하는 서비스인지 한두 문장씩 설명을 추가했다.</p>
<h3 id="시스템-아키텍처">시스템 아키텍처</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/7a41dc5a-51e2-4217-bab2-ab999e9077dc/image.png" alt=""></p>
<p>손그림에서 실제 구조도로 바뀌었다. 1차는 앱·백엔드·AI 판별 등 7단계를 아이콘으로 나열한 수준이었는데, 최종본은 React Native/FastAPI/AWS/GitHub Actions까지 실제 기술 스택 이름이 박힌 아키텍처 다이어그램으로 교체됐다.</p>
<p>돌아보면 줄글 위주였던 1차 기획서가 표·다이어그램·실측 그래프 중심으로 변경했다, &quot;이미지:줄글 비율 1/3 정도&quot;라는 멘토링 팁이 단순한 디자인 조언이 아니라 &quot;근거를 눈에 보이게 만들어라&quot;는 뜻인거라고 생각한다.</p>
<hr>
<h2 id="목표의-정당성을-숫자로">목표의 정당성을 숫자로</h2>
<p>&quot;목표의 정당성을 수치화하라, 프리토타이핑을 해보라&quot;는 조언을 받고, 실제 설문(수요조사)을 직접 설계했다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/7c219c55-b65a-4595-865c-d8d2d8ceea7f/image.png" alt=""></p>
<blockquote>
<p>프리토타이핑(Pretotyping)이란?
프로토타이핑은 핵심 기능을 실제로 만들어본 뒤 고객 테스트로 &quot;만들 수 있는가&quot;를 확인하는 거고, 프리토타이핑은 그보다 앞선 단계 — 아직 아무것도 안 만든 채로 랜딩페이지나 설문만으로 &quot;만들 만한 가치가 있는가&quot;를 먼저 확인하는 거였다. 개발에 자원 쏟기 전에 니즈부터 검증하자는 취지라, 우리는 앱 만들기 전에 먼저 설문으로 프리토타이핑을 한 셈이다.</p>
</blockquote>
<p>그냥 &quot;우리 서비스 어때요?&quot;가 아니라 흐름을 짰다.</p>
<ul>
<li>해커톤 서비스 소개로 시작</li>
<li>연령대·성별</li>
<li>어떤 SNS 플랫폼을 쓰는지</li>
<li>본인 얼굴이 들어간 사진이 스토리·피드에 올라가 있는지, 공개인지 비공개인지</li>
<li>(1번 추진배경에 쓴 기사를 인용해서) 딥페이크 범죄를 얼마나 자각하고 있는지, 어떤 대처를 하고 있는지</li>
<li>서비스를 소개한 뒤 반응을 묻는 질문 한두 개로 마무리</li>
</ul>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/4c9f7262-0714-45fe-8075-811f415a5569/image.png" alt=""></p>
<p>여기에 NPS 스코어도 같이 넣었다. &quot;이 서비스를 주변에 추천할 의향이 얼마나 되나요?&quot;를 0<del>10점으로 물어서 추천그룹(9</del>10점)에서 비추천그룹(0~6점)을 뺀 값인데, 0점 이상이면 괜찮은 수준이고 80점 이상이면 세계적 수준이라고 한다. 잠재적 피해자를 대상으로 &quot;이런 상황이라면 어떨까요?&quot;라는 가정을 세워 테스트해보자는 방향이었다.</p>
<blockquote>
<p>설문조사 결과 
가장 필요한 기능으로 증거보존(57.8%)·게시 전 보호(55.6%)·공개 웹 확인(44.4%)이 꼽혔고, 학교가 서비스를 제공한다면 이용하겠다는 긍정 응답이 77.8%였다. 다만 개인정보 유출 우려가 75.6%로 높게 나오고 NPS는 0점이 나온 것도 그대로 실었다 — 이 결과를 계기로 &quot;원본 미저장·기기 내 처리&quot;라는 설계 원칙을 다시 한번 검증 과제로 명시했다.</p>
</blockquote>
<hr>
<h2 id="검증-안-된-수치는-절대-임의로-만들지-않는다">검증 안 된 수치는 절대 임의로 만들지 않는다</h2>
<p>멘토링을 반영하면서 세운 원칙은 하나였다. v1·v2는 실측, v3는 계획으로 명확히 구분해서 쓰는 것이다.
v1은 ArcFace 단독 판별(Celeb-DF-v2 baseline)이다. InsightFace의 사전학습 <code>buffalo_l</code> 가중치로 Celeb-real 영상 590개를 대상으로 동일인 검증 실험을 했다. 등록 영상 3개·영상당 5프레임 조건에서 평균 ROC-AUC 1.000, EER 0.000, TAR 1.000을 기록했지만, FAR이 목표치 0.001을 초과(0.00138)해서 이 임계값을 운영값으로 채택하지 않았다. ArcFace는 &quot;주어진 두 얼굴을 비교&quot;만 할 뿐 &quot;비교할 후보를 스스로 찾지는 못한다&quot;는 한계가 v2로 이어지는 이유가 됐다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/1f53d211-b812-4ddc-97f4-270a7972222a/image.png" alt=""></p>
<p>v2는 실제 서비스 파이프라인 실측(Cloud Vision API + ArcFace)이다. 실제로 만든 코드를 그대로 사용해서 진짜 API를 호출하고 진짜 얼굴 임베딩을 계산했다. 표준 테스트 이미지에서 후보 20건이 반환됐고, 이 20건을 ArcFace로 재측정한 결과 코사인 유사도 평균 0.981, 20건 모두 임계값(0.6)을 상회했다. 반면 팀원 실제 얼굴 사진으로 테스트했을 때는 무관한 SNS 프로필 1건이 유사도 0.62로 후보에 포함됐다가 오탐으로 정확히 걸러졌다.</p>
<p>이 코사인 유사도를 그냥 숫자로만 남기지 않고, 실제 알림 로직에 쓸 수 있게 3단계 기준으로 나눴다. 유사도 0.85 이상이면 즉각경보, 0.60~0.84는 확인요청, 0.60 미만은 로그기록으로만 남기는 식이다. 오탐 하나 걸러졌다고 끝내는 게 아니라 &quot;그럼 실제 알림은 어느 선에서 울려야 하나&quot;까지 정해둔 셈이다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/687e9f50-ce6e-4643-b725-da63d332b00a/image.png" alt=""></p>
<p>v3는 아직 안 한 것이다. 한국인 얼굴 데이터셋(K-FACE, AI-Hub 이용 승인 완료)으로 파인튜닝하는 실험은 아직 실행하지 않았다. 계획은 등록 이미지 3~5장을 기준으로 임베딩 상단 계층만 재학습하는 방식과 backbone 전체를 재학습하는 방식을 비교하고, 각도·조명·화질·압축 조건을 반영한 고정 검증셋에서 ROC-AUC·FAR·FRR과 임계값별 오탐 변화를 측정하는 것까지 세워뒀다. 다만 v1·v2 실측이 목표치에 못 미친 원인이 얼굴 검출·정렬 실패만으로 설명되지 않을 때 진행하기로 했고, 아직 시작 전이라 검증 전 수치는 기획서에도, 그래프에도 &quot;(목표·예측, 미실행)&quot;이라고 매번 명시했다.</p>
<p>그래프를 만들다가 실수도 하나 했다. v1(1.000)·v2(0.981)·v3 목표(0.90)를 한 줄로 이어서 그렸더니 마치 버전이 올라갈수록 성능이 떨어지는 것처럼 보였다. 세 값은 서로 다른 지표(TAR@FAR vs 원시 코사인 유사도 평균)를 측정한 것이고 조건도 점점 어려워지는 구조라, 애초에 하나의 축으로 비교하면 안 되는 값들이었다. 결국 v1·v2는 각자의 실측 그래프로 따로 보여주고, v3는 그래프 대신 &quot;목표&quot;라는 걸 명시한 표로 대체했다.</p>
<hr>
<h2 id="남은-시간-사흘-동안-프로토타입-개발">남은 시간, 사흘 동안 프로토타입 개발</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/f36070b4-7740-4b5a-9903-0225594ec397/image.png" alt=""></p>
<p>멘토링을 다 반영하고 나서는 기획서만 다듬은 게 아니라 실제로 앱을 붙이는 작업도 병행했다. 레포 커밋 기록을 다시 보면, 조용하던 시기를 지나 8월 5일부터 7일까지 사흘 동안 화면·기능이 한꺼번에 붙였다. 모니터링 화면, 후보 검토 화면, 증거·신고서 초안 화면, 안전 업로드(딥백신 적용 전) 화면, 서버 스캐폴딩과 이미지 처리 API, 화면들을 실제 서버 API 호출로 전환하는 작업까지 — 사흘 만에 PR이 20개가 넘게 작업을 진행했다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/10460ae8-7180-4ae6-b8cf-2057c28276a9/image.png" alt=""></p>
<p>그 뒤로 실제 역이미지 검색(Cloud Vision) 연동, 후보 상세 화면, 신고서가 실제 선택한 후보 데이터를 반영하도록 연결하는 작업까지 이어지면서 얼굴 등록 → 온디바이스 임베딩 → 모니터링 → 후보 검토 → 신고서 초안까지 실제로 동작하는 프로토타입이 완성됐다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/0179e338-d0fd-40b8-88ac-93eb31777e9c/image.png" alt=""></p>
<blockquote>
<p><em>출처: Deng et al., CVPR 2019 확장판, arXiv:1801.07698, Fig.1</em></p>
</blockquote>
<p>ArcFace 논문(Deng et al., CVPR 2019 확장판)의 그림도 실제 논문 PDF에서 좌표를 잡아 직접 크롭해 인용했는데, 그 과정에서 실수도 하나 발견했다 — 처음에 넣었던 그림 하나가 사실 논문이 아니라 회사 블로그 이미지였다는 걸 뒤늦게 알아채고, 실제 arXiv PDF를 다시 받아 정확한 그림으로 교체했다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/f3d9f373-8b6d-4235-a8b7-0a33c4f21683/image.png" alt=""></p>
<blockquote>
<p><em>출처: Deng et al., Fig.8 — 딥소각이 임베딩을 기기 밖으로 내보내지 않는 설계 근거로 인용</em></p>
</blockquote>
<p>논문 Fig.8도 그냥 인용에서 끝내지 않았다. ArcFace 임베딩이 판별뿐 아니라 얼굴을 역으로 복원하는 데도 쓰일 수 있다는 내용인데, 이걸 &quot;그래서 딥소각은 임베딩을 서버로 보내지 않고 기기에만 저장한다&quot;는 프라이버시 설계 근거로 그대로 연결했다.</p>
<hr>
<h2 id="예선심사-결과-발표">예선심사 결과 발표</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/ce51a277-459a-4148-a853-05779264ade7/image.png" alt=""></p>
<p>자유과제 본선 진출팀 발표에 춘배는 없었다. 대신 자유과제 아이디어상 명단에 춘배 이름이 올라갔다. 본선 오프라인 해커톤에는 못 갔지만, 처음 계획했던 대로 검증한 건 검증했다고, 못한 건 못했다고 정직하게 쓴 기획서가 심사에서 나름의 평가를 받았다고 생각한다.</p>
<blockquote>
<h3 id="운영사무국-문자">운영사무국 문자</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/2ee46769-f774-4f14-bc77-c1265291cbf5/image.jpg" alt=""></p>
</blockquote>
<hr>
<h2 id="여기서-끝은-아니고-인정한-한계">여기서 끝은 아니고, 인정한 한계</h2>
<p>기획서 4.3절에는 &quot;단계별 확장 및 한계&quot;라는 표를 따로 넣었다. 이번 해커톤 MVP에서는 딥백신의 100% 합성 차단이 불가능하고 비공개 채널은 자동 탐색이 안 된다는 걸 그대로 인정했고, 대신 공개 웹 탐지·사용자 제보·증거자료 생성까지의 E2E 흐름을 먼저 검증하는 걸 목표로 잡았다. 3<del>6개월 안에는 이미지 변형에 따른 성능 저하, 오탐·개인정보 반복사용 미검증 문제를 한국인 얼굴 임계값 보정과 대학 1개교 실증(MAU·NPS·API 원가 측정)으로 풀어보려 하고, 1</del>3년 뒤에는 미참여 플랫폼과 암호화 채널의 재유포까지 기기 내 해시·C2PA·플랫폼 협력·Safe Upload API로 확장하는 게 목표다. 지금 당장 다 못 푸는 문제를 숨기지 않고 표로 박아둔 게, 돌아보면 이 프로젝트에서 제일 잘한 선택 중 하나였다.</p>
<hr>
<h2 id="생각정리">생각정리</h2>
<p>신청서를 쓴 7월 8일부터 결과를 받아본 지금까지 한 달 반 가까이 프로젝트(기획안을 작성)를 진행하면서 가장 많이 배운 건, 기술적으로 멋있어 보이는 것과 실제로 검증한 것을 절대 섞어서는 안 된다는 것이었다.</p>
<p>처음에는 좋은 기술과 기능을 많이 담는 것이 좋은 기획서라고 생각했다. 하지만 프로젝트를 하나씩 검증해 나가면서 생각이 조금씩 달라졌다. 킥오프 전에 실제 사용할 데이터셋을 확인하고, 공식 문서와 약관을 다시 찾아보면서 기존 계획에서 실행할 수 없거나 위험한 부분을 발견했다. 역이미지 검색과 얼굴 임베딩의 역할도 다시 나눴고, 실제로 해보지 않은 파인튜닝은 결과를 만들어내는 대신 &#39;아직 실행하지 않았다&#39;고 명확하게 적었다. 서로 다른 조건에서 나온 수치를 하나의 그래프로 보여주었던 것도 다시 뜯어고쳤다.</p>
<p>돌아보면 이 과정에서 계속 확인했던 건 하나였다. 확인한 것과 계획인 것을 구분해서 전달하는 것. 무엇을 할 수 있는지를 보여주는 것도 중요하지만, 지금 내가 어디까지 확인했고 어디부터가 아직 가설인지를 정확하게 말하는 것이 더 중요하다는 걸 배웠다. 특히 &quot;한계를 적지 않는다는 것은 한계를 모른다는 뜻일 수 있다&quot;는 멘토님의 말을 프로젝트가 끝날 때까지 계속 생각하게 됐다.</p>
<p>결과적으로 본선에는 진출하지 못했고, 처음 계획했던 것처럼 개발을 계속 이어갈 당위성도 사라졌다. 아쉽지 않다면 거짓말이겠지만, 그렇다고 이 한 달 반이 결과만으로 판단할 수 있는 시간이었다고 생각하지는 않는다. 적어도 이번 프로젝트를 통해 완성도를 과시하는 것보다 내가 확인한 범위를 정확하게 보여주는 것이 더 설득력 있을 수 있다는 것을 직접 경험했다.</p>
<p>본선에는 올라가지 못했지만, 아이디어상으로 프로젝트의 가치를 인정받을 수 있었고 9월 4일 시상식에 참석하는 것으로 이번 프로젝트의 한 매듭을 지으려고 한다.
돌이켜보면 KT 에이블스쿨에서 좋은 사람들을 만나 4월부터 지금까지 함께 개인 프로젝트와 개인일정에서도 병행하면서 총 세 번의 공모전에 도전했다. 그 과정에서 결과를 떠나 함께 고민하고, 부족한 부분을 채워가며 성장할 수 있었다는 것 자체가 나에게는 큰 의미로 남았다. 특히 이번 공모전은 춘배팀으로 함께한 마지막 공모전이라 더욱 아쉽게 느껴진다. 한 팀원은 올해 복학을 하고, 또 다른 팀원과 나는 함께 취업을 준비하며 각자의 새로운 길을 가게 됐다. 이제는 모두 같은 프로젝트를 위해 밤늦게까지 이야기하고 공모전을 준비하던 것처럼 함께하기는 어려울 수도 있다. 그래도 마지막까지 셋이 함께 프로젝트를 완성했고, 본선 진출이라는 목표에는 닿지 못했지만 작게나마 수상이라는 결과를 남길 수 있어서 다행이라고 생각한다.</p>
<blockquote>
<h3 id="다애님-현준님-고생하셨어요">다애님, 현준님 고생하셨어요!</h3>
</blockquote>
<blockquote>
<p>이번 프로젝트의 자세한 기획안과 관련 자료는 아래 Github에서 확인할 수 있습니다.
<strong><a href="https://github.com/Chunbae-A/deepsogak">DeepSogak Github</a></strong></p>
<blockquote>
<p>📂 기획안은 Github의 <code>docs/</code> 디렉토리에 정리되어 있습니다.</p>
</blockquote>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026 오픈소스 컨트리뷰션 아카데미, Challenges 회고]]></title>
            <link>https://velog.io/@mi_nini/2026-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EC%BB%A8%ED%8A%B8%EB%A6%AC%EB%B7%B0%EC%85%98-%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-Challenges-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@mi_nini/2026-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EC%BB%A8%ED%8A%B8%EB%A6%AC%EB%B7%B0%EC%85%98-%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-Challenges-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 10 Aug 2026 09:52:05 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p><a href="https://velog.io/@mi_nini/2026-%EC%98%A4%ED%94%84%EC%86%8C%EC%8A%A4-%EC%BB%A8%ED%8A%B8%EB%A6%AC%EB%B7%B0%EC%85%98-%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EB%B0%9C%EB%8C%80%EC%8B%9D">발대식 후기</a>를 쓴 지 한 달이 조금 넘었다. 그때는 &quot;혼자서는 벅찰 것 같다&quot;는 생각과 &quot;좋은 리뷰를 받아 Organization Member가 되는 것&quot;이라는 목표만 있었는데, 지금은 실제로 이슈를 등록하고 PR을 올리고 리뷰를 주고받으며 한 달을 보냈다. Challenges 기간 중반을 지나며 지금까지 한 내용을 정리하려고 한다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/120fb8a7-f82e-49bf-a584-d003c24e6556/image.png" alt=""></p>
<p>나는 <strong>OpenStack &amp; Kubernetes 운영 문서화 및 국제화</strong> 팀 소속으로, 크게 세 갈래로 기여를 이어왔다.</p>
<ul>
<li><code>infra-cloud-kr/openstack-kubernetes</code> — 팀에서 직접 만들어가는 OpenStack-Helm 운영 문서</li>
<li><code>kubernetes/website</code> — 쿠버네티스 공식 문서 한글화</li>
<li><code>k8s-kr/kubelingoassist</code> — 한글화 작업을 돕는 VS Code 확장 프로그램</li>
</ul>
<p>여기에 더해 OpenDev 코드리뷰 워크플로우도 실습 진행했다.</p>
<hr>
<h3 id="겹치지-않는-이슈-선정하기">겹치지 않는 이슈 선정하기</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/97867548-e6ab-4857-a50e-dc374cb8da83/image.png" alt=""></p>
<p>처음 팀 파트를 나눌 때 네트워크 쪽에 인원이 몰렸다. 나도 처음엔 Gateway API 파트로 시작했는데, GitHub 이슈 현황을 전수 조사해보니 이미 여러 명이 관련 주제를 선점한 상태였다. 그래서 아직 아무도 손대지 않은 Octavia 로드밸런싱(<code>networking/load-balancing.rst</code>)으로 파트를 바꿔 이슈(<a href="https://github.com/infra-cloud-kr/openstack-kubernetes/issues/24">#24</a>)를 새로 등록했다.</p>
<p>이후 이미 병합되거나 열려있는 다른 분들의 PR(<a href="https://github.com/infra-cloud-kr/openstack-kubernetes/pull/36">#36</a>, <a href="https://github.com/infra-cloud-kr/openstack-kubernetes/pull/38">#38</a>)을 읽으며 이 프로젝트에서 통용되는 문서 스타일(<code>list-table</code>로 리소스 정리, <code>code-block</code>으로 아키텍처 다이어그램 그리기)을 파악했고, 그 스타일을 따라 로드밸런서·리스너·풀·멤버·헬스 모니터 개념과 amphora/OVN 드라이버 비교를 작성해 <a href="https://github.com/infra-cloud-kr/openstack-kubernetes/pull/40">PR #40</a>을 올렸다.</p>
<p><img src="https://docs.openstack.org/octavia/latest/_images/octavia-component-overview.svg" alt="Octavia 공식 아키텍처 다이어그램 — amphora, 컨트롤러 서브컴포넌트, 네트워크 구성 요소를 보여준다"></p>
<p><em>출처: <a href="https://docs.openstack.org/octavia/latest/reference/introduction.html">OpenStack Octavia 공식 문서</a></em></p>
<p>내가 문서로 정리한 게 바로 이 그림 속 구조다 — 로드밸런서 요청이 들어오면 amphora(실제 트래픽을 처리하는 VM 또는 컨테이너)가 뜨고, 컨트롤러가 이걸 관리하는 흐름을 이해하고 나니 글을 쓰기가 훨씬 수월해졌다. PR을 올리기 전 <code>sphinx-build -W</code>(경고 0건)와 <code>doc8</code> 린트(에러 0건)를 직접 돌려보고 확인한 것도 이번에 새로 익힌 습관이다.</p>
<hr>
<h3 id="첫-쿠버네티스-pr">첫 쿠버네티스 PR</h3>
<p><code>kubernetes/website</code>에 PR을 올리기 전에, 쿠버네티스 조직에 처음 PR을 올려본 곳은 <a href="https://github.com/kubernetes-sigs/contributor-playground/pull/2681"><code>kubernetes-sigs/contributor-playground</code></a>였다(MERGED). CLA 서명이 제대로 되는지, Prow 봇이 어떻게 반응하는지 연습하는 온보딩용 저장소인데, 여기서 리뷰어가 GitHub의 &quot;Approve&quot; 버튼이 아니라 <code>/lgtm</code>, <code>/approve</code> 같은 &quot;텍스트 커맨드&quot;로 리뷰 상태를 바꿀 수도 있구나.. 많은 프로젝트에서 사용하지만 사용해보지 못했던 부분이라 신기했던거 같다. </p>
<hr>
<h3 id="리뷰받고-나서야-보인-것들">리뷰받고 나서야 보인 것들</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/7543b436-77f0-42bb-8977-9c4c530611b6/image.png" alt=""></p>
<p><code>kubernetes/website</code>에서는 <code>kubeadm</code> 고가용성 토폴로지 가이드의 오래된 번역을 최신화하는 PR(<a href="https://github.com/kubernetes/website/pull/56648">#56648</a>)을 올렸다. 리뷰를 받기 전엔 &quot;자연스러운 한국어면 된다&quot;고 생각했는데, 리뷰어분께 받은 피드백 7건을 하나씩 내 원문과 비교해보니 내가 놓친 게 눈에 보였다.</p>
<table>
<thead>
<tr>
<th>내가 쓴 문장</th>
<th>리뷰 후</th>
</tr>
</thead>
<tbody><tr>
<td>&quot;지역 etcd 멤버&quot;</td>
<td>&quot;로컬 etcd 멤버&quot;</td>
</tr>
<tr>
<td>(원문 일부 생략) &quot;비슷한 방식이 지역의 X와 Y에도&quot;</td>
<td>&quot;같은 방식이 로컬 X와 Y 인스턴스에도&quot;</td>
</tr>
<tr>
<td>&quot;커플링에 실패할 위험&quot;</td>
<td>&quot;결합이 실패할 위험&quot;</td>
</tr>
</tbody></table>
<p><code>local</code>을 무조건 &#39;지역&#39;으로 직역하면 오히려 원문 의미가 흐려질 수 있다는 것, 매끄럽게 읽히려고 슬쩍 생략한 단어가 사실은 한글화팀이 지키려는 &quot;원문 구조 보존&quot; 원칙을 깨는 일이라는 걸 이때 처음 체감했다. 리뷰가 끝난 뒤엔 &quot;커밋을 하나로 스쿼시해달라&quot;는 요청도 받았는데, 리뷰 중엔 커밋을 계속 쌓다가 승인 직전에만 정리한다는 팀 관례도 이렇게 실제로 겪으며 배운거같다.</p>
<p>반대로 내가 간단하게 리뷰어가 되어본 경험도 있었다. 다른 분의 번역 PR(<a href="https://github.com/kubernetes/website/pull/56678">#56678</a>)을 원문과 문단 단위로 대조하다가, 문서 상단의 translator 링크에 있는 GitHub 계정명 오타(hongp vs hong-p)를 발견했다. 번역 자체는 정확했지만, 링크가 실제 번역자를 가리키는지까지 확인하지 않았다면 놓쳤을 문제였다. 리뷰를 받는 입장과 직접 리뷰하는 입장을 모두 경험하면서, 좋은 번역은 ‘자연스럽게 읽히는 번역’에서 끝나는 것이 아니라 원문의 의미와 구조, 문서의 세부 요소까지 보존하는 번역이라는 걸 알게 됐다.</p>
<hr>
<h3 id="번역을-좀더-간편하게-할-수-있게">번역을 좀더 간편하게 할 수 있게</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/3ec94b7f-b47b-41a0-962f-3ef00eadf2fc/image.png" alt=""></p>
<p><code>kubelingoassist</code>에 기여하면서는 조금 다른 종류의 배움이 있었다. 번역 문서에서 자주 흔들리는 표기(예: 프록시/프락시)를 실시간으로 잡아주는 기능을 직접 설계·구현했는데, 처음엔 실제 문서 541개를 스캔해서 더 많이 쓰인 표현을 그냥 추천하려고 했다.
그런데 &quot;가비지 컬렉션&quot;이 corpus에서 더 많이 쓰인다고 보고 이걸 추천하려던 순간, 공식 용어집을 확인해보니 정답은 &quot;가비지 수집&quot;이었다. 컬렉션+콜렉션을 다 합쳐도 수집의 사용 빈도에 한참 못 미쳤다. 데이터(빈도)만 보고 판단하면 완전히 틀릴 수 있다는 걸, 내가 만들던 도구에서 직접 겪으며 배웠다. 이 경험 덕분에 &quot;공식 용어집이 있으면 그걸 최우선으로, 없을 때만 데이터를 참고한다&quot;는 설계 원칙을 세울 수 있었고, 이 기능을 VS Code Extension Host 환경에서 70개 이상의 테스트로 검증한 뒤 이슈(<a href="https://github.com/k8s-kr/kubelingoassist/issues/30">#30</a>)와 PR(<a href="https://github.com/k8s-kr/kubelingoassist/pull/31">#31</a>)을 올렸다.</p>
<hr>
<h3 id="opendevgerrit">OpenDev(Gerrit)</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/12481390-2e91-4068-8ecd-51084b7b4f00/image.png" alt=""></p>
<p>우리 팀이 최종적으로 바라보는 목표는 <code>openstack-helm</code>처럼 실제 OpenStack 프로젝트에 기여하는 것인데, 이 프로젝트들은 GitHub가 아니라 <a href="https://opendev.org">OpenDev</a>라는, OpenInfra Foundation이 직접 운영하는 별도의 코드 호스팅·리뷰 인프라 위에서 돌아간다. 그래서 본격적으로 문서 작업을 하기 전에, OpenDev가 제공하는 연습용 저장소인 <a href="https://review.opendev.org/c/opendev/sandbox/+/997024"><code>opendev/sandbox</code></a>에서 이 낯선 워크플로부터 먼저 익혀보기로 했다.
계정 하나로 시작하는 GitHub와 달리, OpenDev는 Ubuntu One 계정(<code>login.ubuntu.com</code>)으로 로그인한다 — 이 계정은 Gerrit뿐 아니라 Launchpad(버그 트래커), 번역 플랫폼, 심지어 OpenInfra 재단 행사 투표까지 재단 전체 활동에 두루 쓰이는 단일 계정이었다. 이후 SSH key를 새로 만들어 <code>~/.ssh/config</code>에 <code>review.opendev.org</code> 호스트를 등록하고 Gerrit에 공개키를 올렸고, <code>git-review</code>라는 전용 CLI 도구를 설치한 뒤 <code>git review -s</code>를 실행하면 <code>gerrit</code>이라는 리모트가 자동으로 생성된다. 커밋 메시지에도 규칙이 있었다 — 요약은 50자, 본문은 72자를 넘기지 않는 관례, 그리고 DCO(Developer Certificate of Origin, &quot;내가 이 코드를 직접 썼고 라이선스에 동의한다&quot;는 서명)를 뜻하는 <code>Signed-off-by</code> 태그를 반드시 붙여야 한다는 정보를 얻었다.
이렇게 세팅을 마치고 <code>git-review</code>로 패치를 올리니 터미널에 이런 로그가 찍혔다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/b9afa97d-a329-486a-855b-75546c41f981/image.png" alt=""></p>
<p>가장 헷갈렸던 건 &quot;커밋을 새로 쌓는 방식이 아니라는 것&quot;이었다. GitHub PR은 커밋을 계속 추가해서 리뷰 이력을 쌓아가지만, Gerrit은 <code>git commit --amend</code>로 같은 Change-Id를 가진 패치셋을 계속 덮어써서 갱신한다. 커밋 메시지 안에 있는 <code>Related-Bug</code>(연결된 버그 번호), <code>Change-Id</code>(이 리뷰를 식별하는 고유 값, 리비전이 바뀌어도 유지됨), <code>Signed-off-by</code>(DCO 서명) 세 태그를 이해하자</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/ce12397d-9dab-497c-a013-b1b03f63421f/image.png" alt=""></p>
<p>리뷰 방식도 완전히 달랐다. <code>-1</code>/<code>0</code>/<code>+1</code>은 누구나 줄 수 있는 리뷰 점수이고, <code>+2</code>는 승인 권한이 있는 리뷰어만 줄 수 있다는 점수제다. 게다가 병합되려면 세 종류의 라벨이 전부 채워져야 한다 — 사람이 코드를 검토하는 <code>Code-Review</code>, Zuul이라는 CI가 빌드·테스트 결과를 보고 자동으로 매기는 <code>Verified</code>, 그리고 병합 준비가 됐는지를 나타내는 <code>Workflow</code>. 이 셋 중 하나라도 비어 있으면 자동 병합(gate) 대상에서 제외된다는 걸 실습을 통해서 배울 수 있었다.</p>
<p>같은 실습 세션에서 팀원한분이 올린 연습 패치를 내가 리뷰해줄 기회도 있었다. <code>Please capitalize the first letter.</code>라는 코멘트를 남기고 <code>Code-Review +2</code>, <code>Workflow +1</code>을 직접 눌러 승인까지 해봤는데, GitHub에서는 &quot;Approve&quot; 버튼 한 번이면 끝날 일이 Gerrit에서는 점수 하나하나를 채워나가는 과정이 재밌었다. 한국어 번역에 많이 기여하고 더 나아가 영어번역에도 기여 할 수 있을 수 있게 많이 배우고 opendev사이트를 자주 방문해야겠다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/6691deb1-7214-4d50-a209-ae1cff26b053/image.png" alt=""></p>
<p>같은 &quot;코드 리뷰&quot;라도 프로젝트마다 문화와 도구가 이렇게 다를 수 있고, 우리 팀이 목표로 하는 <code>openstack-helm</code> 기여도 결국 이 Gerrit 문법(Change-Id, 패치셋, 점수제 승인, Zuul 게이트)을 몸에 익힌 다음에야 제대로 시작할 수 있겠다는 걸 이번 sandbox 연습을 하며 실습 환경을 익힌거같다.</p>
<hr>
<h3 id="미팅-사진을-몇개-안남겨서">미팅 사진을 몇개 안남겨서..</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/fb267ad0-a1f5-4c46-adad-1d03ffed5665/image.png" alt=""></p>
<p>정규 미팅이 끝난 뒤에도 디스코드에서 당일 밋업에서 다룬 내용 중 이해가 잘 되지 않았거나 궁금했던 부분을 자유롭게 질문할 수 있었다. 멘토님들께서 질문에 바로 답변해주셔서, 세션 내용을 다시 짚어보고 궁금했던 부분을 바로 해소할 수 있었던거 같다. (멘토님들 항상 감사드립니다..)</p>
<hr>
<h2 id="생각정리">생각정리</h2>
<p>한 달 전만 해도 “번역은 자연스럽게만 하면 된다”고 생각했다면, 지금은 원문을 최대한 지키는 것과 자연스럽게 표현하는 것 사이에서 근거를 가지고 판단하는 법을 조금씩 배우고 있다. 리뷰를 받으면서, 반대로 다른 사람의 리뷰를 해보면서, 그리고 내가 만든 도구가 스스로 틀린 답을 낼 뻔한 순간을 겪으면서 결국 같은 교훈을 여러 방향에서 확인한 것 같다. 근거 없이 그럴듯해 보이는 것을 그냥 믿지 말 것. KubeCon Japan 이야기를 들으며 느낀 것도 비슷했다. 인프라도 문서도 결국 지금 맞다고 믿는 것을 계속 의심하고 검증하면서 조금씩 더 나은 방향으로 갱신해가는 일이라는 생각이 들었다.</p>
<p>앞으로 load-balancing.rst(PR #40)와 kubelingoassist(PR #31)가 실제 리뷰를 받게 된다면 이번에 배운 내용을 바탕으로 꼼꼼하게 반영해서 최종적으로 병합까지 이어갈 수 있는 PR을 만들고 싶다. 지금까지 OpenDev에서는 Sandbox를 통해 연습하는 데 그쳤다면, 이제는 실제 OpenStack 프로젝트, 특히 openstack-helm에 Gerrit으로 직접 패치를 올려보려고 한다. 다른 분들의 PR도 더 적극적으로 리뷰하면서 문서의 정확성을 검증하는 눈을 계속 키워가고 싶고, KubeCon Japan에 후기를 들으며 언뜻 접했던 AI 인프라의 흐름도 조금씩 따라가며 부족한 부분을 학습 해보려고 한다. 지금 당장 내가 직접 다뤄야 할 영역은 아니더라도, 내가 다듬고 작성하는 문서가 어떤 기술적 흐름 위에 놓여 있는지는 알고 써야 한다고 생각하기 때문이다.</p>
<p>처음에는 막막하게 시작했지만 한 달 사이에 GitHub와 Gerrit을 오가며 이슈를 만들고, PR을 올리고, 리뷰를 주고받는 과정이 어느새 제법 익숙해졌다. 아직 배워야 할 것도 많고 부족한 부분도 많지만, 적어도 한 달 전의 나와 비교하면 조금은 달라진 것 같다. Challenges 기간에서 많이 배웠고, 이어질 Masters 기간에서도 지금처럼 하나씩 부딪히고 확인해가면서 꾸준히 나아가보려고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI Agent 시대, 나는 AI를 어떻게 써야 할까]]></title>
            <link>https://velog.io/@mi_nini/AI-Agent-%EC%8B%9C%EB%8C%80-%EB%82%98%EB%8A%94-AI%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%8D%A8%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@mi_nini/AI-Agent-%EC%8B%9C%EB%8C%80-%EB%82%98%EB%8A%94-AI%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%8D%A8%EC%95%BC-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Wed, 22 Jul 2026 15:44:35 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가며">들어가며</h1>
<p>빅프로젝트 특강 중 「Claude 엔지니어가 알려주는 AI Agent 시대의 개발 방법론과 실전 데모」를 들으며 가장 인상 깊었던 부분은, 강의가 단순히 새로운 AI 기술을 소개하는 강의가 아닌 앞으로 개발자가 어떤 방식으로 일해야 하는지를 중심으로 이야기하는 강의였다는 것이다. 특강은 생성형 AI의 현재 수준, AI Agent의 핵심 기술, 실전 데모, 그리고 앞으로 개발자의 역할이라는 네 가지 흐름으로 진행되었다. 들어가며 강조한 3가지가 인상깊어 글 시작전에 보고 다시 들어가보자.</p>
<p>첫 번째는 ‘말’, 즉 언어에 대한 부분이었다. AI 에이전트, MCP, 컨텍스트 엔지니어링 같은 용어를 단순히 외우는 것이 아니라, 왜 이런 개념이 등장했는지 자신의 문장으로 설명할 수 있어야 한다는 이야기였다. 
두 번째는 ‘눈’, 즉 감각이었다. 에이전트가 제대로 작동하고 있는 상황과 불필요하게 반복하거나 엉뚱한 방향으로 흐르는 상황을 구분할 수 있는 판단력이 중요하다고 했다. 
마지막은 ‘손’, 즉 행동이었다. 거창한 계획보다 오늘 저녁부터 바로 시작할 수 있는 90일 로드맵과 포트폴리오를 만들어보는 등 실천/활용이 필요하다는 점을 강조하는 특강이였던거 같다. </p>
<blockquote>
<h3 id="글-들어가기전-요약">글 들어가기전, 요약</h3>
</blockquote>
<ol>
<li>AI Agent는 이제 &quot;실험&quot;이 아니라 &quot;실행&quot;의 영역에 들어왔고, 개발자의 일은 &quot;만드는 것&quot;에서 &quot;정하고 검증하는 것&quot;으로 무게중심이 옮겨가고 있다.</li>
<li>그런데 &quot;토큰과 생각(추론)을 어떻게 줄일까&quot;는 강의가 던진 화두이다. 실제로 파고들어 보면 훨씬 많은 갈래의 판단이 필요했다, 이 글의 절반은 그 고민이다.</li>
<li>내가 만들던 <a href="https://github.com/BcKmini/claude-code-use">claude-code-use</a> 프로젝트를 강의 프레임에 대입해보니, 아직 AI를 100%역량을 끌어올려 사용하지 못한다고 생각이 들었다. 좀더 발전시켜서 AI를 잘 활용하자.</li>
</ol>
<hr>
<h1 id="특강에서-다룬-개념">특강에서 다룬 개념</h1>
<h2 id="지금-우리는-어디쯤-와-있나">지금 우리는 어디쯤 와 있나</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/62592e82-0598-4de7-a679-e3c393465df8/image.png" alt=""></p>
<p>핵심은 &quot;누가 다음 행동을 결정하는가&quot;가 단계마다 사람에게서 AI로 옮겨간다는 것이다.</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>Chat</td>
<td>묻는다·답한다. 매 턴 사람이 질문하고 결과도 사람이 옮겨 붙임</td>
</tr>
<tr>
<td>Copilot</td>
<td>옆에서 거든다. 자동완성·초안 작성. 실행 주도권은 여전히 사람</td>
</tr>
<tr>
<td>Agent</td>
<td>목표만 주면 스스로 계획하고 도구를 골라 쓰고 결과를 검증</td>
</tr>
<tr>
<td>Multi-Agent</td>
<td>지휘자 1명 + 전문가 N명이 역할을 나눠 병렬로 일한다</td>
</tr>
</tbody></table>
<p>지금은 실험의 해가 끝나고 실행의 해가 시작된 시점이라는 게 강의의 내용이였다. AI를 최소 한 개 업무에라도 도입한 조직이 이미 다수이고, 공개된 MCP 서버 수도 1년 반 사이 수십 배로 늘었다. 다만 완전 자율 에이전트 도입은 아직 소수에 그친다.
챗봇은 질문→답변으로 끝나지만, 에이전트는 목표→계획→도구 실행→결과 평가→반복이라는 루프를 스스로 돈다.</p>
<blockquote>
<p>같은 LLM을 쓰는데 결과물이 다른 이유는, 그 주변에 뭘 붙였느냐의 차이다.</p>
</blockquote>
<h2 id="이미-무게중심이-옮겨간-공정들">이미 무게중심이 옮겨간 공정들</h2>
<p><img src="https://www-cdn.anthropic.com/images/4zrzovbb/website/faa261102e46c7f090a2402a49000ffae18c5dd6-2292x1290.png" alt="프롬프트 엔지니어링에서 컨텍스트 엔지니어링으로 — 반복적이고 순환적인 과정"></p>
<p>설계·인프라·리뷰·문서 네 공정 모두 AI가 초안을 만들고, 사람은 그게 사업 맥락에 맞는지 최종 판단하는 쪽으로 역할이 옮겨가고 있다.
일이 줄어드는 게 아니다. 일의 무게중심이 &quot;만드는 쪽&quot;에서 &quot;정하고 검증하는 쪽&quot;으로 이동한다. 용어도 1년 반 사이 빠르게 바뀌어왔다. </p>
<ul>
<li>프롬프트 엔지니어링 - &gt;<ul>
<li>컨텍스트 엔지니어링 - &gt;<ul>
<li>하네스 엔지니어링 - &gt;<ul>
<li>루프 엔지니어링 </li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>이름은 계속 바뀌지만 아래층(왜 이 문제가 생기는가)은 겹쳐 쌓인다는 게 요지였다. 그래서 용어 자체보다 판단 기준을 배우는 게 낫다.</p>
<h2 id="ai-agent를-이루는-것들">AI Agent를 이루는 것들</h2>
<table>
<thead>
<tr>
<th align="center">Augmented LLM 구조</th>
<th align="center">컨텍스트 보정</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><img src="https://www-cdn.anthropic.com/images/4zrzovbb/website/d3083d3f40bb2b6f477901cc9a240738d3dd1371-2401x1000.png" alt="검색·도구·메모리로 확장된(Augmented) LLM 구조"></td>
<td align="center"><img src="https://www-cdn.anthropic.com/images/4zrzovbb/website/0442fe138158e84ffce92bed1624dd09f37ac46f-2292x1288.png" alt="너무 구체적이지도, 너무 모호하지도 않은 &#39;골디락스 존&#39; 찾기 — 컨텍스트 보정의 핵심"></td>
</tr>
</tbody></table>
<p>AI Agent는 다음 네 가지가 루프를 도는 것으로 정의된다. 하나라도 빠지면 그냥 챗봇이다.</p>
<ul>
<li><strong>LLM (두뇌)</strong> — 무엇을 할지 추론·판단</li>
<li><strong>Memory (기억)</strong> — 무슨 일이 있었는지 들고 다님</li>
<li><strong>Tool (손발)</strong> — 바깥 세상을 읽고 바꿈</li>
<li><strong>Planning (계획)</strong> — 목표를 단계로 쪼개고 다시 짬
LLM은 텍스트만 뱉고, 실제 실행은 런타임(우리 코드)이 담당한다. 그래서 위험한 도구일수록 권한을 좁게, 파일 접근은 특정 디렉터리로 스코프를 한정하는 게 기본 원칙으로 강조됐다.</li>
</ul>
<p>컨텍스트는 무한하지 않다는 것도 핵심 메시지였다. 많이 넣을수록 똑똑해질 것 같지만 실제로는 반대로 갈 때가 많다(Context Rot, Lost in the Middle 같은 연구가 근거로 제시됐다). 그래서 나온 게 네 가지 전략이다.</p>
<table>
<thead>
<tr>
<th>전략</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Write</strong> 쓴다</td>
<td>미래의 나를 위해 로그·결정 사항을 파일에 남긴다</td>
</tr>
<tr>
<td><strong>Select</strong> 고른다</td>
<td>지금 필요한 것만 컨텍스트에 넣는다</td>
</tr>
<tr>
<td><strong>Compress</strong> 줄인다</td>
<td>중요도는 남기고 분량만 줄인다</td>
</tr>
<tr>
<td><strong>Isolate</strong> 격리한다</td>
<td>서브 에이전트에 맡기고 요약만 받는다</td>
</tr>
</tbody></table>
<p>이 네 전략은 뒤에 나오는 내 개인적인 고민(토큰과 생각을 어떻게 줄일 것인가)의 출발점이 됐다.</p>
<h2 id="워크플로-vs-에이전트-그리고-자율성-레벨">워크플로 vs 에이전트, 그리고 자율성 레벨</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/fbd129f5-fe11-4f6d-9e6c-9d30173bad21/image.png" alt=""></p>
<blockquote>
<p>&quot;AI에게 다 맡기자&quot;는 대개 실패한다.</p>
</blockquote>
<p>workflow(사람이 순서와 분기를 미리 정함, 예측 가능하지만 예외에 취약)와 agent(모델이 도구와 순서를 스스로 판단, 유연하지만 관측·평가가 필수) 사이에서 지금 이 작업에 얼마나 맡길지를 정하는 게 현실적인 답이다. 자율성 레벨을 제시해줬다.</p>
<blockquote>
</blockquote>
<ul>
<li>L0(사람이 전부) </li>
<li>L1(AI 제안·사람 실행) </li>
<li>L2(AI 초안·사람 리뷰) </li>
<li>L3(AI 실행·사후 확인) </li>
<li>L4(완전 자율)<blockquote>
<blockquote>
<p>이 중 L2가 실무에서 가장 흔히 쓰이는 지점이라고 한다. L4를 목표로 삼기보다, 지금 작업에 맞는 레벨을 스스로 정하는 게 실무적이라는 메시지였다.</p>
</blockquote>
</blockquote>
</li>
</ul>
<hr>
<h1 id="고민하자">고민하자</h1>
<p>강의는 <code>비용 = 토큰 × 스텝 수</code>라는 한 줄로 정리하고 넘어갔다. 그런데 이 문장을 실제로 내 작업에 적용하려고 하니, 훨씬 많은 갈래의 고민이 나왔다. 일곱 가지로 정리해본다.</p>
<h2 id="1-애초에-추론이-필요한-문제인가부터-의심한다">1. 애초에 추론이 필요한 문제인가부터 의심한다</h2>
<p>&quot;생각 깊이를 작업마다 다르게 설정하라&quot;는 조언 앞에 생략된 판단이 있다고 생각한다. 이 작업에 추론이 정말 필요한가? 라는 질문이다.</p>
<blockquote>
</blockquote>
<p>1.파일 하나 찾기, 오타 하나 고치기처럼 답이 명백한 작업에 깊은 추론 모델을 물리면 토큰만 태우고 결과는 똑같다.
2. 반대로 여러 파일에 걸친 설계 결정처럼 &quot;틀린 가정을 되돌아봐야 하는&quot; 작업에 빠른 모델을 쓰면 결과가 부정확해서 재작업(=스텝 수 증가)으로 이어진다.</p>
<p>effort를 잘못 낮추면 재시도 비용으로 되돌아온다. &quot;싼 모델을 쓰는 것&quot;과 &quot;적절한 모델을 쓰는 것&quot;은 다른 문제다.</p>
<h2 id="2-스텝-수를-줄이는-게-토큰을-줄이는-것보다-효과가-클-때가-많다">2. 스텝 수를 줄이는 게 토큰을 줄이는 것보다 효과가 클 때가 많다</h2>
<p>프롬프트 한 줄을 줄이는 건 토큰 단가에서 몇 % 절약이지만, 루프 한 번을 줄이는 건 그 루프 전체(탐색+실행+검증)의 토큰을 통째로 아끼는 것이다.
그래서 고민의 우선순위는 &quot;무엇을 짧게 쓸까&quot;보다 &quot;어디서 다시 도는가&quot;를 먼저 찾는 쪽이어야 한다. 이건 결국 초기 지시가 얼마나 명확한가의 문제로 돌아간다. 다음에 애매하게 시켜서 되묻는 루프가 생기면, 그 되묻는 루프 자체가 스텝 수다.</p>
<h2 id="3-컨텍스트를-안-채우는-것과-채웠다가-비우는-것은-다른-전략이다">3. 컨텍스트를 &quot;안 채우는 것&quot;과 &quot;채웠다가 비우는 것&quot;은 다른 전략이다</h2>
<p>Write·Select·Compress·Isolate를 다시 뜯어보면, 이 넷은 사실<code>언제 비용을 지불하느냐</code>의 타이밍이 다르다.</p>
<ul>
<li><strong>Select</strong>(애초에 필요한 것만 넣기)는 가장 싸다. 비용을 아예 안 만든다. 하지만 &quot;지금 뭐가 필요한지&quot;를 미리 정확히 알아야 하는데, 탐색적인 작업에서는 이게 어렵다.</li>
<li><strong>Compress</strong>(줄이기)는 한 번 채운 다음 요약하는 비용이 추가로 든다. 요약 자체도 LLM 호출이라 토큰을 쓴다.</li>
<li><strong>Isolate</strong>(서브 에이전트 분리)는 겉으로는 메인 컨텍스트를 지키지만, 서브 에이전트가 쓰는 토큰까지 합치면 총량은 오히려 늘어난다. 간단한 작업에 서브 에이전트를 붙이면 분리 비용만 남는다.</li>
</ul>
<blockquote>
<p>결국 네 전략 중 뭘 쓸지는 지금 이 작업이 얼마나 오래, 얼마나 많은 파일에 걸쳐 진행될 것인가를 미리 가늠해야 고를 수 있는데, 이 가늠 자체가 또 하나의 판단 비용이라는 게 아이러니하다...</p>
</blockquote>
<h2 id="4-캐싱은-설계에서부터-고려해야-나오는-절약이다">4. 캐싱은 설계에서부터 고려해야 나오는 절약이다</h2>
<p>매 요청마다 같은 시스템 프롬프트·같은 CLAUDE.md를 새로 밀어 넣으면 캐시가 안 붙고 매번 전체 단가로 과금된다. 자주 안 바뀌는 것과 매번 바뀌는 것을 앞뒤로 명확히 분리해두면 앞부분이 캐시에 걸려 반복 비용이 크게 줄어든다. 프롬프트를 무엇을 넣을까로만 생각하면 안 나오고, 어떤 순서로, 얼마나 자주 바뀌는 덩어리로 나눌까까지 생각해야 나오는 절약이다.  </p>
<h2 id="5-병렬로-돌릴-것과-순차로-돌릴-것을-가르는-기준">5. 병렬로 돌릴 것과 순차로 돌릴 것을 가르는 기준</h2>
<p>서브 에이전트를 여러 개 동시에 돌리면 시간은 줄지만 토큰 총량은 그대로거나 늘어난다. 반대로 순차로 돌리면 앞 단계 결과를 뒤 단계가 참고할 수 있어 재작업이 줄 수도 있지만 시간이 길어진다.
나는 여태 이걸 &quot;빠르면 좋은 것&quot;으로만 생각했지 비용 관점에서 저울질해본 적이 없었다.</p>
<ul>
<li>독립적인 작업(탐색 여러 갈래) → 병렬이 이득</li>
<li>서로 의존하는 작업(설계→구현→검증)을 억지로 병렬화 → 나중에 합치는 비용이 더 든다</li>
</ul>
<h2 id="6-검증-비용도-전체-비용에-포함해서-계산해야-한다">6. 검증 비용도 전체 비용에 포함해서 계산해야 한다</h2>
<p>빠르고 싼 모델로 작업을 끝냈다고 해서 끝난 게 아니다. 결과가 틀렸는지 사람이 검토하는 시간, 틀렸을 때 다시 시키는 토큰까지 합쳐야 진짜 비용이다. 모델을 고를 때 이 답이 틀렸을 때 내가 알아챌 수 있는가까지 고민에 넣어야 한다. 알아채기 어려운 실수를 내는 모델을 싸다고 고르면, 검증 비용이 절약분을 다 잡아먹는다.</p>
<h2 id="7-정말-풀리지-않는-고민">7. 정말 풀리지 않는 고민</h2>
<p><code>이번 판단에 얼마나 생각을 태울까를 매번 사람이 정해야 하는가?</code>
effort를 작업마다 다르게 설정하라는 조언은 맞지만, 그 설정을 사람이 매번 판단해야 한다면 이것도 결국 사람의 인지 비용이다. 이상적으로는 에이전트가 이 작업은 되돌아볼 필요가 있어 보인다&quot;는 걸 스스로 감지해서 effort를 올리는 게 맞을 텐데, 이걸 사람이 대신 정해주는 지금 단계와 에이전트가 스스로 정하는 다음 단계 사이에 아직 간극이 있다.
나는 지금 이 판단을 거의 느낌으로 하고 있는데, 이걸 규칙으로 정리해서 CLAUDE.md나 하네스 설정에 박아둘 수 있을지가 다음에 시도해볼 지점이다. 예를 들면 이런 식이다.</p>
<ul>
<li>파일 3개 이상 걸치면 → 깊은 모델</li>
<li>테스트 실패가 2회 이상 반복되면 → effort 상향</li>
</ul>
<p>특강을 듣고 정리하고 나니 확실해진 게 있다.</p>
<blockquote>
<p>토큰을 줄인다는 하나의 목표를 넘어서 어디서 비용이 새는지를 계속 다시 찾아야 하는 것이 중요하다고 생각한다.
이 프레임(Write·Select·Compress·Isolate, effort 조절)은 도구 상자일 뿐이고, 지금 이 작업에 어떤 도구를 얼마나 꺼내 쓸지는 결국 매번 새로 판단해야 한다.</p>
</blockquote>
<hr>
<h1 id="내가-이미-하고-있던-것과-겹쳐보기">내가 이미 하고 있던 것과 겹쳐보기</h1>
<p>이 강의를 들으면서 계속 떠오른 게, Claude CLI를 자주 사용하는 입장에서 내가 편하려고 만들고 있는 <a href="https://github.com/BcKmini/claude-code-use">claude-code-use</a>였다. 배운 개념에 내 프로젝트를 대입해보자</p>
<p>지금 레포에는 11개의 서브 에이전트(00-orchestrator부터 시작해서 planner, implementer, reviewer, tester.. 등등)와, 그 아래에서 실제로 돌아가는 CLI 도구 몇 개가 있다. <code>claude-cost</code>는 세션을 시작하기 전에 에이전트·모델별로 예상 비용을 계산해준다. <code>claude-handoff</code>는 세션이 끝날 때 작업 맥락을 파일로 저장해뒀다가 다음 세션 시작할 때 그대로 이어 붙일 수 있게 해준다. <code>claude-remind</code>는 TODO.md나 CLAUDE.md에 남겨둔 미완료 항목을 세션 시작 시점에 다시 띄워준다. (여러가지 있지만) 이 조합을 다시 보면, planner·reviewer·security-auditor는 애초에 읽기 전용 권한만 갖도록 정의해뒀다는 게 눈에 들어온다. 코드를 직접 고치는 건 implementer 하나뿐이고, 나머지는 보고 판단만 하게 만들어뒀다. 이건 &quot;사람이 개입하는 지점을 설계했는가&quot;라는 질문에는 이미 답이 되어 있는 부분이다.</p>
<p>반면 없는 것도 분명하다. GitHub·Figma·Notion MCP 서버는 이미 클라이언트로 연결해서 쓰고 있지만, 전부 남이 만들어둔 걸 가져다 쓰는 입장이다. <code>claude-code-use</code> 안에 있는 도구들은 전부 내가 터미널에서 직접 실행하는 CLI라서, 이 중 하나라도 내가 직접 MCP 서버로 만들어서 Claude가 스스로 판단해서 호출하게 만든 건 아직 없다. 그리고 <code>claude-cost</code>가 &quot;이번 달에 재작업이 몇 번 있었다&quot;는 숫자는 남기지만, 정작 그 재작업이 왜 일어났고 어떻게 고쳤는지를 사람이 읽을 수 있는 글로 남기는 도구는 없다. 강의를 듣고 개선할 부분을 생각해보자.</p>
<ol>
<li><p>MCP 서버
지금 <code>claude-code-use</code>에 있는 도구들은 전부 내가 터미널에서 직접 실행하는 CLI다. <code>claude-cost</code>(비용 추정)나 <code>claude-handoff</code>(세션 기록 조회)처럼 이미 로직이 검증된 도구부터 MCP 서버로 한 번 감싸보려고 한다. 새로 뭔가를 만드는 것보다, 이미 잘 도는 로직을 다른 방식(AI가 스스로 호출하는 방식)으로 한 번 더 노출해보는 쪽이 첫 시도로는 덜 위험하다고 판단했다. 이렇게 하면 에이전트가 작업을 시작하기 전에 &quot;이 작업 예상 비용이 얼마나 나올까&quot;를 스스로 물어볼 수 있게 되는데, 이게 바로 위에서 정리한 첫 번째 고민(추론이 정말 필요한 작업인지 미리 판단하기)을 도구 차원에서 도와주는 셈이 된다.</p>
</li>
<li><p>실패를 기록하는 습관.
지금은 재작업이 얼마나 자주 일어났는지 숫자(비율)만 남기고 있는데, 정작 &quot;왜 헛돌았고 어떻게 고쳤는지&quot;를 글로 남긴 적은 없었다. 다음부터는 재시도가 발생한 세션이 끝나면 무엇을 하려고 했는지, 왜 안 됐는지, 어떻게 고쳤는지, 다음엔 뭘 다르게 할 것인지를 짧게라도 파일로 남겨보려고 한다. 숫자는 &quot;얼마나 자주 실패했는가&quot;는 보여주지만 &quot;왜 실패했는가&quot;는 보여주지 못한다는 걸, 많이 겪고 블로그 또는 프로젝트에 남겨보려고 한다.</p>
</li>
</ol>
<hr>
<h1 id="생각정리">생각정리</h1>
<blockquote>
<p>나는 AI를 어떻게 써야 할까
AI는 이제 설계도대로 아주 잘 짓는 존재가 됐지만, 무엇을 지을지 정하는 건 여전히 사람의 일이다.</p>
</blockquote>
<p>나에게 이게 구체적으로 의미하는 건 이렇다.</p>
<ol>
<li><p>자율성 레벨을 스스로 정한다, L4(완전 자율)를 목표로 삼지 않는다. L2(AI 초안·사람 리뷰)가 실무에서 가장 많이 쓰이는 지점이고, 내 프로젝트의 읽기 전용 에이전트들도 결국 이 L2를 코드로 강제한 것이다.</p>
</li>
<li><p>컨텍스트를 RAM처럼 다룬다. 무한히 넣지 않는다. 필요할 때만 꺼내 쓰고, 작업 경계에서 비운다.</p>
</li>
<li><p>포트폴리오는 성공이 아니라 실패와 경계로 완성된다. 다음 과제는 명확해졌다. MCP 서버 하나를 직접 만들고, 실패 케이스를 의도적으로 만들어서 그 실패를 어떻게 고쳤는지 기록하는 것 또는 AI를 많이 활용하며 부족한점에 대해 계속 끊임없이 고민하는것이다.</p>
</li>
</ol>
<blockquote>
<p>대학교 학부에서 GPT-3.5를 사용하며 AI가 하지 못하는 일을 사람이 메우는 것이 개발자의 역할이라고 생각했다. 하지만 AI는 1년, 6개월, 그리고 불과 1개월 단위로도 체감될 만큼 빠르게 발전하고 있다. 지금의 AI를 보며 &#39;과연 주니어 개발자는 앞으로 어떤 경쟁력을 가져야 할까?&#39;라는 고민이 들 정도였다. 그런데 이번 강의를 들으며 생각이 조금 달라졌다. AI가 점점 더 능숙한 장인이 되어갈수록, 사람은 더 이상 벽돌을 하나씩 쌓는 사람이 아니라 어떤 땅 위에 무엇을 지을지 결정하는 사람이어야 한다는 것이다. 방향을 정하고, AI가 이해할 수 있는 맥락을 설계하며, 완성된 결과를 검증하고 책임지는 역할이 더욱 중요해지고 있다. 결국 AI 시대의 개발자는 코드를 가장 많이 작성하는 사람이 아니라, 가장 좋은 문제를 정의하고, 가장 올바른 질문을 던지며, 가장 분명한 기준을 세울 수 있는 사람이 되어야 한다고 생각한다. 기술은 점점 더 빠르게 발전하겠지만, 무엇을 만들 것인지 결정하고 그 결과에 책임을 지는 일만큼은 여전히 사람의 몫이라는 사실은 변하지 않을 것이다.</p>
</blockquote>
<hr>
<blockquote>
<h3 id="-tmi---해커톤-예선에-붙었다-가보자고--현준-다애">+ TMI - 해커톤 예선에 붙었다. 가보자고~  @현준 @다애</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/2aaa04ae-04c0-4c7c-a26b-a7045d682dce/image.png" alt=""></p>
</blockquote>
<hr>
<h2 id="참고">참고</h2>
<ul>
<li>내 프로젝트: <a href="https://github.com/BcKmini/claude-code-use">github.com/BcKmini/claude-code-use</a>
<a href="https://www.anthropic.com/engineering/building-effective-agents">Anthropic — Building Effective AI Agents</a>
<a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents">Anthropic — Effective Context Engineering for AI Agents</a>
<a href="https://www.anthropic.com/engineering/building-effective-agents">Anthropic — Building Effective AI Agents</a>
<a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents">Anthropic — Effective Context Engineering for AI Agents</a>
<a href="https://www.anthropic.com/engineering/building-effective-agents">Anthropic — Building Effective AI Agents</a>
<a href="https://www.anthropic.com/engineering/building-effective-agents">Anthropic — Building Effective AI Agents</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026 OSSCA 발대식]]></title>
            <link>https://velog.io/@mi_nini/2026-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EC%BB%A8%ED%8A%B8%EB%A6%AC%EB%B7%B0%EC%85%98-%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EB%B0%9C%EB%8C%80%EC%8B%9D</link>
            <guid>https://velog.io/@mi_nini/2026-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EC%BB%A8%ED%8A%B8%EB%A6%AC%EB%B7%B0%EC%85%98-%EC%95%84%EC%B9%B4%EB%8D%B0%EB%AF%B8-%EB%B0%9C%EB%8C%80%EC%8B%9D</guid>
            <pubDate>Tue, 14 Jul 2026 15:14:53 GMT</pubDate>
            <description><![CDATA[<h2 id="ossca">OSSCA</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/073878b7-fc97-4b8c-989b-dc8745beac6d/image.jpg" alt=""></p>
<p>오픈소스 컨트리뷰션 아카데미(OSSCA)는 참여자를 멘토·멘티로 묶어 실제 오픈소스 프로젝트에 기여하는 경험을 쌓게 해주는 프로그램이다. 2026년에는 11개 팀이 참여형 프로젝트로 운영됐고, 나는 그중 <strong>OpenStack&amp;Kubernetes 운영 문서화 및 국제화</strong> 팀에 멘티로 합류하게 됐다. (작년에 Pytorch프로젝트에 참여해 기여했었는데 좋은 기회가 다시...)</p>
<hr>
<hr>
<h2 id="발대식-진행">발대식 진행</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/77406ae7-d347-4f79-901e-c46b51be05da/image.jpg" alt=""></p>
<p>이번 발대식은 2026년 7월 11일 서울 강남 한국과학기술회관 국제회의장에서 진행되었으며, 참여형 멘토와 멘티, 사무국 등 약 300명이 함께한 행사였다.
행사는 OSSCA 프로그램 소개와 운영 일정 안내로 시작되었으며, 이후에는 프로젝트별 팀 소개와 팀별 자율 미팅이 진행되었다.</p>
<blockquote>
<p>타임테이블</p>
</blockquote>
<table>
<thead>
<tr>
<th>시간</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>12:30 ~ 13:00</td>
<td>출석체크 및 팀별 착석</td>
</tr>
<tr>
<td>13:00 ~ 13:05</td>
<td>오프닝</td>
</tr>
<tr>
<td>13:05 ~ 13:10</td>
<td>주관기관 환영사</td>
</tr>
<tr>
<td>13:10 ~ 13:40</td>
<td>첫 번째 초청 강연 — 박수홍 삼성전자 오픈소스 그룹장</td>
</tr>
<tr>
<td>13:40 ~ 14:00</td>
<td>오픈소스 컨트리뷰션 아카데미 소개 (프로그램·참여형 11팀 소개, 세부일정, 멘토·멘티 활동 의무사항 및 지원사항 안내)</td>
</tr>
<tr>
<td>14:00 ~ 14:30</td>
<td>두 번째 초청 강연 — 정승록 AMD 수석 엔지니어</td>
</tr>
<tr>
<td>14:30 ~ 15:00</td>
<td>단체 기념촬영</td>
</tr>
<tr>
<td>15:00 ~ 17:00</td>
<td>팀별 발대식 진행 및 기념촬영</td>
</tr>
</tbody></table>
<hr>
<h2 id="강연정리--오픈소스를-대하는-나">강연정리 : 오픈소스를 대하는 나</h2>
<h3 id="박수홍-삼성전자-오픈소스-그룹장--오픈소스는-생각보다-폐쇄적이다">박수홍 삼성전자 오픈소스 그룹장 — 오픈소스는 생각보다 폐쇄적이다</h3>
<p>가장 기억에 남는 메시지는 &quot;오픈소스는 열려 있고 개방적이라고 생각하지만, 사실 생각보다 폐쇄적이다&quot;라는 문장이었다. 기업 입장에서는 과거에 오픈소스를 부정적으로 바라보는 시선이 컸다고 한다. 내 코드와 기술을 외부에 공개한다는 것 자체가 리스크로 여겨졌기 때문이다. 그런데 시간이 지나면서 이 인식이 조금씩 바뀌었고, 지금은 오히려 오픈소스 참여가 기업 경쟁력의 핵심으로 여겨지는 흐름으로 왔다는 이야기였다.</p>
<p>다른 자리에서도 밝힌 적 있는 관점과 맞닿는 지점이 있었다. &quot;오픈소스는 기업에서 보면 경쟁력의 핵심이고, 사업에서는 헤게모니를 위한 굉장히 중요한 전략&quot;이라는 표현인데, 결국 기업이 오픈소스를 공개하는 이유가 순수한 선의가 아니라 각자의 사업 전략에 맞춰 계산된 선택이라는 뜻이다. 실제로 삼성전자는 SW의 70% 이상을 오픈소스로 활용하며 165개 프로젝트를 직접 공개하고 있고, ONAP 같은 프로젝트에서는 코드 기여 순위로 화웨이를 앞서기도 했는데, 이런 활동들도 결국 산업 표준을 주도하고 영향력을 확보하기 위한 전략적 포지셔닝의 성격이 크다고 생각한다.</p>
<p>발대식에서 나온 &quot;오픈소스는 순결하지만은 않다&quot;는 표현을 말씀하셨다. 오픈소스라고 해서 모든 게 순수한 선의와 이상으로만 돌아가는 게 아니라, 그 안에도 기업의 이해관계와 전략이 얽혀 있다는 것이다. 이 얘기를 들으면서 최근 몇 년간 업계에서 실제로 있었던 사례들이 떠올랐다. MongoDB(2018년), Elastic(2021년), Redis(2024년) 같은 오픈소스 데이터베이스 기업들이 잇따라 라이선스를 바꾼 일이 대표적이다. AWS 같은 대형 클라우드 벤더가 이들의 오픈소스 코드를 그대로 가져다 관리형 서비스로 팔면서도 코드 기여로는 거의 돌려주지 않자, 원저작 기업들이 코드 공개 자체를 제한하는 source-available 라이선스로 돌아선 것이다. 이 사례는 오픈소스가 늘 이상적인 협업 공동체로만 굴러가는 게 아니라, 누가 가치를 만들고 누가 그 가치를 가져가느냐를 둘러싼 현실적인 힘겨루기의 장이기도 하다는 걸 잘 보여준다. </p>
<h3 id="정승록-amd-수석-ai-소프트웨어-엔지니어--gpu-성능-평가-현장에서-오픈소스를-대하는-법">정승록 AMD 수석 AI 소프트웨어 엔지니어 — GPU 성능 평가 현장에서 오픈소스를 대하는 법</h3>
<p>정승록 엔지니어는 AMD에서 GPU performance evaluation(그래픽카드 성능 평가) 파트를 담당하고 있다고 소개했다. 실제로 benchmarking과 performance testing 업무를 주로 하신다고 했는데, AMD가 ROCm(Radeon Open Compute)이라는 오픈소스 GPU 컴퓨팅 스택을 공개하고 있고, 그 안에 rocHPL이나 hipBench 같은 벤치마킹 도구들도 오픈소스로 함께 제공한다는 걸 감안하면 실무와 맞닿아 있는 이야기로 느껴졌다.
강연에서는 AMD 자체 코드도 이미 오픈소스로 공개되어 있고, 이 외에도 해외 유수 기업들이 공개한 오픈소스 프로젝트가 정말 많다고 강조했다. 다만 기술적으로 핵심적인 코드 기여는 결국 그 프로젝트를 직접 만든 사람들이나 이미 이름이 알려진 기업들이 주도하는 경우가 많으니, 후배 개발자 입장에서는 그런 코드를 보면서 공부하고 직접 뜯어보면서 시야를 넓히는 게 중요하다고 조언했다. 여기에 AI를 적극적으로 잘 활용하라는 말도 덧붙였다. 강연 말미에는 본인이 요즘 재미있게 보고 있는 논문이나 분야를 언급하며, 관련된 오픈소스 프로젝트 몇 가지를 추천해주기도 했다.</p>
<hr>
<h2 id="저녁엔-오픈인프라-x-쿠버네티스-한국-사용자-모임-밋업도">저녁엔 오픈인프라 x 쿠버네티스 한국 사용자 모임 밋업도</h2>
<p>같은 날 저녁에는 OSSCA 발대식과는 별개로 OpenInfra 한국 사용자 모임과 Kubernetes 한국 사용자 모임이 공동 주최하는 밋업이 있었다.</p>
<blockquote>
<p>타임테이블</p>
</blockquote>
<table>
<thead>
<tr>
<th>시간</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>17:30 ~ 18:00</td>
<td>등록 및 네트워킹</td>
</tr>
<tr>
<td>18:00 ~ 18:20</td>
<td>최영락님 — OpenStack &amp; Kubernetes Localization Trend (OpenInfra x Ceph x KCD Korea 2026 소개, Localization 및 커뮤니티 활동 소개)</td>
</tr>
<tr>
<td>18:20 ~ 18:50</td>
<td>조성수님 — OpenStack은 어떻게 번역되고 운영되는가? (OpenStack 번역 시스템과 커뮤니티, Weblate 기반 번역·운영 방식)</td>
</tr>
<tr>
<td>19:00 ~ 19:30</td>
<td>황원용님 — Kubernetes SIG Docs Localization 소개 (Kubernetes 문서 현지화 프로젝트, Localization Contributor Workflow)</td>
</tr>
<tr>
<td>19:30 ~ 20:00</td>
<td>박은정님 — OpenStack과 Kubernetes 컴포넌트는 어떻게 만나는가? (OpenStack on Kubernetes, 연계 사례)</td>
</tr>
<tr>
<td>20:00 ~ 20:20</td>
<td>Q&amp;A 및 네트워킹</td>
</tr>
</tbody></table>
<p>낮에 있었던 발대식 세션과 저녁 밋업의 세션이 그대로 이어져서 시간가는줄 모르고 집중해서 들었다. 뒤에서는 밋업에서 얻은 인사이트를 OpenStack, Kubernetes 순서로 정리해보려고 한다.</p>
<hr>
<h2 id="openstack--탄생부터-운영-문서화까지">OpenStack — 탄생부터 운영 문서화까지</h2>
<h3 id="openstack-이전">OpenStack 이전</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/10316045-b6ea-4c82-9f18-fbdf2c5e7c36/image.png" alt=""></p>
<p>2010년 이전, 클라우드라는 개념 자체는 이미 있었지만 선택지가 좋지 않았다. 한쪽에는 AWS처럼 완전히 폐쇄적인 API로 돌아가는 퍼블릭 클라우드가 있었고, 다른 한쪽에는 VMware 같은 값비싼 proprietary 가상화 스택이 있었다. 오픈소스 쪽에서 그나마 존재하던 대안이 Eucalyptus였는데, 이건 아예 AWS API와 호환되도록 설계된 프로젝트였다. 즉 자체 표준을 만드는 게 아니라 AWS 생태계 안에서 하이브리드 클라우드를 돌리기 위한 보조 도구에 가까웠다. 결국 기업 입장에서는 어느 쪽을 택하든 특정 벤더(AWS든 VMware든)의 API와 생태계에 종속될 수밖에 없는 구조였다.</p>
<h3 id="openstack-탄생">OpenStack 탄생</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/b3a739b0-4b66-4adf-ae9c-63585ca9f58d/image.png" alt=""></p>
<p>이 벤더 종속 문제를 풀기 위해 2010년, 미국항공우주국(NASA)과 클라우드 호스팅 업체 Rackspace가 각자 개발하던 프로젝트를 합쳤다. NASA는 자체 클라우드 플랫폼인 Nebula를 개발 중이었는데 여기서 나온 컴퓨트 제어 코드가 나중에 Nova가 됐고, Rackspace는 자사의 오브젝트 스토리지 서비스인 Cloud Files를 오픈소스로 공개하면서 이게 Swift가 됐다. 두 프로젝트가 만나 2010년 7월 오스틴에서 첫 디자인 서밋을 열고, 같은 달 21일 OSCON에서 정식으로 발표된 게 OpenStack의 시작이다. 그해 10월 나온 첫 정식 릴리스(&quot;Austin&quot;)에는 Nova와 Swift, 딱 두 개 서비스만 들어 있었다. 컴퓨트(Nova)와 스토리지(Swift)라는 클라우드의 가장 기본적인 두 축에서 시작해서, 이후 네트워킹(Neutron), 블록 스토리지(Cinder) 같은 프로젝트가 붙으며 지금의 오픈스택 생태계로 확장된 것이다.
핵심은 벤더 종속을 끊자는 목적의식이었다. VMware 같은 프로프라이어터리 스택도, AWS 같은 폐쇄적 퍼블릭 클라우드 API도 아닌, 아무 벤더에도 종속되지 않는 완전한 오픈소스 표준을 만들겠다는 게 OpenStack의 존재 이유였다. (아주 흥미롭지요)</p>
<h3 id="openstack-재단-그리고-왜-처음엔-리눅스-재단에-안-갔는가">OpenStack 재단, 그리고 왜 처음엔 리눅스 재단에 안 갔는가</h3>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mi_nini/post/42727969-8edb-4022-b9b5-402ec4d3ad20/image.png" width="100%"></th>
<th><img src="https://velog.velcdn.com/images/mi_nini/post/fec53f6d-0440-4ca1-baf8-06e0ca178d83/image.png" width="100%"></th>
</tr>
</thead>
<tbody><tr>
<td>OpenStack이 처음 만들어질 때 리눅스 재단(Linux Foundation) 산하로 들어가는 방안도 검토됐다고 한다. 그런데 당시 리눅스 재단은 사실상 리눅스 커널 하나만 돌보는 조직에 가까웠고, 지금처럼 수백 개 프로젝트를 품는 &quot;빅 텐트&quot; 구조가 아니었다. 그래서 오픈스택은 2012년에 독립된 재단(OpenStack Foundation)을 따로 설립하는 길을 택했다. 이후 2020년에는 오픈스택 하나만이 아니라 여러 오픈 인프라 프로젝트를 아우르겠다는 뜻에서 이름을 <strong>OpenInfra Foundation</strong>으로 바꿨다.</td>
<td></td>
</tr>
</tbody></table>
<p>그리고 2025년, 큰 사건이 하나 있었다. OpenInfra Foundation이 리눅스 재단에 정식으로 합류한 것이다. 독립 재단을 만들었던 2012년의 결정과는 정반대의 방향인데, 이번엔 AI 시대의 막대한 인프라 투자와 지정학적 변화 속에서 힘을 합치는 게 낫다는 판단이었다고 한다. 이 합류로 재미있는 구조가 완성됐다. 운영체제 표준(Linux), 클라우드 인프라 표준(OpenStack/OpenInfra), 컨테이너 표준(Kubernetes/CNCF)이 모두 같은 리눅스 재단이라는 우산 아래 들어오게 된 것이다. OpenInfra Foundation은 리눅스 재단 안에서도 독자적인 예산과 거버넌스, 회원 구조를 유지하는 &quot;재단 안의 재단&quot; 형태로 운영된다.</p>
<h3 id="openstack-i18n-팀이-하는-일">OpenStack I18n 팀이 하는 일</h3>
<p>OpenStack I18n(Internationalization) 팀의 공식 가이드는 &quot;국제화는 OpenStack을 유비쿼터스로 만드는 데 필수&quot;라는 문장으로 시작한다. 소프트웨어 자체의 국제화 품질을 높이고, 고품질 번역을 관리하는 게 이 팀의 역할이다. 쿠버네티스처럼 SIG-Docs 산하에 Localization 서브프로젝트를 두는 구조가 아니라, OpenStack은 애초에 <strong>I18n 팀 자체가 독립된 프로젝트 팀</strong>으로 존재한다. 소통 창구는 메일링 리스트와 IRC <code>#openstack-i18n</code> 채널이다.</p>
<h3 id="번역-플랫폼--zanata---weblate">번역 플랫폼 : Zanata -&gt; Weblate</h3>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mi_nini/post/00a11f88-5181-4de3-a2a4-9c2dab095a70/image.png" width="100%"></th>
<th><img src="https://velog.velcdn.com/images/mi_nini/post/4d6ebad3-fa62-44d7-9470-81cb8238e5b9/image.png" width="100%"></th>
</tr>
</thead>
<tbody><tr>
<td>OpenStack은 2015년 9월부터 Zanata라는 번역 플랫폼(translate.openstack.org)을 써왔다. 웹 브라우저만 있으면 원문과 번역문을 좌우로 나란히 놓고 작업할 수 있는 CAT(Computer-Aided Translation) 툴이다. 그런데 Zanata 프로젝트 자체가 2018년 9월 이후로는 활발한 개발이 멈춘 상태였고, 이 문제를 풀기 위해 2023년 OpenStack 기술위원회가 Weblate로의 이전을 공식 투자 과제로 올렸다. 2024~2025년 사이 I18n SIG가 이 마이그레이션을 우선순위로 진행 중이며, Gerrit/Zuul과의 연동 문제 등을 해결해가고 있다고 한다.</td>
<td></td>
</tr>
</tbody></table>
<blockquote>
<h4 id="기여-절차-부분은-핸즈온-세션을-통해-직접-실습을-진행했기-때문에-해당-내용은-다음-글에서-실습-과정과-함께-자세히-다루도록-하겠다">기여 절차 부분은 핸즈온 세션을 통해 직접 실습을 진행했기 때문에, 해당 내용은 다음 글에서 실습 과정과 함께 자세히 다루도록 하겠다.</h4>
</blockquote>
<hr>
<h2 id="kubernetes--탄생부터-운영-문서화까지">Kubernetes — 탄생부터 운영 문서화까지</h2>
<h3 id="kubernetes-이전">Kubernetes 이전</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/54c55c1c-941f-4c33-b038-704c77e7b6d7/image.png" alt="">
Kubernetes도 마찬가지로 갑자기 생겨난 것이 아니다. 컨테이너 자체를 널리 퍼뜨린 건 Docker였고, Docker가 뜨자 &quot;이 수많은 컨테이너를 어떻게 관리할 것인가&quot;라는 오케스트레이션 문제가 따라왔다. 이 문제를 풀겠다고 나선 후보가 여럿 있었는데, Docker 진영 자체가 내놓은 Docker Swarm과, 아파치 재단의 Mesos(+Marathon)가 대표적이었다.
쿠버네티스 자체는 구글이 내부적으로 써오던 Borg, 그리고 그 후속작 Omega의 경험을 이어받아 만든 세 번째 컨테이너 관리 시스템이다. 즉 구글은 이미 수년간 수십만 대 서버에서 컨테이너를 굴려본 경험이 있었고, 그 노하우를 외부에 공개 가능한 형태로 다시 설계한 게 Kubernetes였다. 2014년 오픈소스로 공개됐고, 2015년 구글이 이걸 갓 설립된 CNCF(Cloud Native Computing Foundation)에 기증했다.</p>
<h3 id="kubernetes란">Kubernetes란</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/5dfee4cc-f8ea-4b9f-8ad4-351979805197/image.png" alt="">
Kubernetes는 구글이 만든 관리 시스템이다. 컨테이너가 제공하는 활용성을 누리면서도 복잡한 분산 시스템을 쉽게 배포하고 관리할 수 있도록 도와주는 컨테이너 오케스트레이션 시스템으로 정의된다. 애플리케이션 배포 방식이 Traditional Deployment(물리 서버) → Virtualized Deployment(VM) → Container Deployment로 진화해온 흐름의 마지막 단계에 있다.</p>
<blockquote>
<h3 id="참고">참고</h3>
<p>CNCF - Kubernetes의 관계
CNCF(Cloud Native Computing Foundation)는 2015년 설립된 Linux Foundation 산하 재단으로, 벤더 중립적으로 클라우드 네이티브 오픈소스 프로젝트를 육성한다. 같은 해인 2015년, 구글이 쿠버네티스를 CNCF에 기증했고 쿠버네티스는 CNCF 최초의 오픈소스 프로젝트이자 최초로 졸업(Graduated)한 프로젝트가 됐다. </p>
</blockquote>
<hr>
<h3 id="kubernetes-커뮤니티-그룹-구조">Kubernetes 커뮤니티 그룹 구조</h3>
<p>쿠버네티스 커뮤니티는 세 층위로 나뉜다.</p>
<ul>
<li><strong>위원회(Committees)</strong>: 프로젝트에 대한 특정 권한을 위임받은 그룹. 행동 강령 위원회, 보안 대응 위원회, 운영 위원회 등이 있다.</li>
<li><strong>특별 관심 그룹(SIGs)</strong>: 프로젝트의 특정 영역을 담당하는 그룹. Architecture, Docs, Release, Testing, Node, Storage 등으로 나뉜다.<ul>
<li><strong>하위 프로젝트(Subprojects)</strong>: SIG 안에서 독립적으로 운영되는 조직. 내가 참여하는 SIG-Docs 안의 <strong>Localization</strong>과 <strong>Website</strong>가 여기 해당한다.</li>
</ul>
</li>
<li><strong>워킹 그룹(Working Groups)</strong>: 여러 SIG에 걸쳐 특정 목표를 달성하기 위해 만들어진 임시 그룹.</li>
</ul>
<h3 id="sig-docs가-하는-일">SIG-Docs가 하는 일</h3>
<p>SIG-Docs의 목적은 문서를 통해 쿠버네티스에 기여하며, 가능한 한 많은 사람이 쿠버네티스를 쉽게 사용하도록 돕는 것이다. kubernetes.io에 문서를 게시하고, 기능 문서화 표준을 제공하며, 핵심 API·아키텍처 문서화, 분기별 릴리스 문서 작성, 기여자 지침 제공, 공식 블로그 관리까지 담당한다. </p>
<hr>
<hr>
<h3 id="localizationl10n이란">Localization(L10n)이란</h3>
<p>SIG-Docs의 하위 프로젝트인 Localization의 목표는 영문 공식 문서를 다양한 언어로 현지화하고, 그 과정을 표준화하는 공간을 제공하는 것이다. 단순히 &quot;번역팀&quot;이 아니라 <strong>공식 문서 유지보수 조직</strong>에 가깝다는 게 발대식에서 가장 인상 깊었던 설명이었다.</p>
<p>L10n의 역할을 정리하면:</p>
<ul>
<li>Localization은 단순 번역이 아니라 오픈소스 접근성을 높이는 기여다. 모든 사용자가 영어에 익숙한 건 아니고, 모국어 문서의 존재는 학습 장벽을 크게 낮춘다.</li>
<li>글로벌 커뮤니티 확장에 기여한다. 누구나 사용할 수 있는 프로젝트를 만드는 게 오픈소스 철학 중 하나이고, Localization은 지역 커뮤니티(예: 한글화팀) 활성화에도 기여한다.</li>
<li>항상 최신 정보를 유지해야 한다. 쿠버네티스 릴리즈 주기는 약 4개월이고, L10n팀은 새 기능·Deprecated 기능·변경 사항을 각 언어 문서에 반영한다. 각 현지화 팀마다 별도 가이드라인이 있다.</li>
</ul>
<h3 id="ai-시대-l10n은-어떻게-변화해야-하는가">AI 시대, L10n은 어떻게 변화해야 하는가</h3>
<p>발대식 자료에서 많은 생각이 들었었다. 예전에는 &quot;기여자가 직접 번역 → PR 생성 → 다른 기여자가 리뷰 → 머지&quot;가 표준 흐름이었는데, 지금은 &quot;AI가 자동 번역해주면 되는 거 아니냐&quot;는 질문이 자연스럽게 나온다. (글쓰는 화자도 Claude를 pro를 사용하며 하루 토큰량을 빡빡하게 다 쓰는 입장이기에..)</p>
<p>앞으로의 방향으로 제시된 흐름은 &quot;한글화 가이드라인을 기반으로 한 AI 프롬프트로 초안 생성 → 기여자가 리뷰 → PR 생성 → 다른 AI 혹은 기여자가 리뷰 → 머지&quot;였다. 완전 자동화도, 완전 수작업도 아닌 중간 지점을 계속 고민해야 하는 이슈로 남겨져 있었다.</p>
<p>실제로 용어집 등록 논의(GitHub issue #51885)를 보면, &quot;Pod를 포드로 표기할지 팟으로 표기할지&quot; 같은 세부 사항까지 커뮤니티가 활발하게 토론하며 표준을 정하고 있었다. 이렇게 정립된 용어는 쿠버네티스 관련 문서·강의·서적의 참고 기준이 된다는 설명을 들으니, 단순 번역 작업이 아니라 표준을 함께 만들어가는 일이라는 게 실감났다.</p>
<hr>
<h3 id="실제-기여-프로세스">실제 기여 프로세스</h3>
<p><strong>1) 기여 준비</strong></p>
<ul>
<li>CNCF 커뮤니티 행동 강령(Code of Conduct)을 숙지한다.</li>
<li>CLA(Contributor License Agreement)에 서명한다. 기여자가 작성한 코드의 권리를 커뮤니티에 양도한다는 의미이며, PR 생성 시 자동으로 서명 여부가 확인된다. 서명이 안 되어 있으면 PR이 머지되지 않는다.</li>
<li>처음이라면 <code>kubernetes-sigs/contributor-playground</code> 저장소에서 연습해볼 수 있다. <code>remote</code> 디렉터리에 자신의 GitHub 아이디로 마크다운 파일을 만들어 PR을 올리면 CLA 서명 코멘트가 자동으로 달리고, 머지되면 메인테이너의 환영 메시지도 받을 수 있다.</li>
</ul>
<p><strong>2) 이슈 탐색 및 작성</strong></p>
<ul>
<li><code>Kubernetes i18n Tracker</code>(kfess.github.io/kubernetes-i18n-tracker)에서 언어별로 원문 대비 얼마나 뒤처졌는지(outdated) 확인할 수 있다.</li>
<li>번역이 필요한 문서를 찾으면 <code>kubernetes/website</code> 저장소에 이슈를 등록한다.</li>
</ul>
<p><strong>3) 한글화 작업 및 PR 작성</strong></p>
<ul>
<li>쿠버네티스 문서 한글화 가이드(kubernetes.io/ko/docs/contribute/localization_ko)를 따른다. 과거에는 별도 브랜치를 운영했지만 지금은 메인테이너·컨트리뷰터 모두 <code>main</code> 브랜치를 기준으로 작업한다.</li>
<li>한글화 모범 사례 문서도 참고하면 좋다. 기여 방식, 문체 및 표현, 원문 처리, 용어 네 가지 카테고리로 자주 반복되는 리뷰 사례가 정리돼 있다.</li>
<li>작업이 끝나면 PR을 올린다.</li>
</ul>
<p><strong>4) AI 사용에 대한 가이드라인</strong>
이 부분이 개인적으로 가장 중요하게 느껴졌다. <code>kubernetes.dev</code>의 AI Guidance 문서 핵심은 이렇다.</p>
<ul>
<li>AI를 이용한 PR 작성은 허용되지만, 모든 변경 사항은 작성자가 이해하고 책임져야 한다.</li>
<li>AI를 사용했다면 PR 설명에 반드시 공개해야 한다.</li>
<li>AI를 공동 작성자로 등록하거나 AI가 만든 커밋 메시지를 그대로 쓰는 건 금지된다.</li>
<li>AI가 대부분 작성한 대규모 PR은 허용되지 않는다.</li>
<li>PR 제출 전 반드시 직접 코드 리뷰와 테스트를 해야 한다.</li>
<li>리뷰어의 질문에는 AI가 아니라 작성자가 직접 답해야 하고, 변경 이유를 설명하지 못하면 PR이 종료될 수 있다.</li>
</ul>
<blockquote>
<h3 id="참고-1">참고</h3>
<p>한국어 L10n 팀은 Slack <code>kubernetes.slack.com</code>의 <code>#kubernetes-docs-ko</code> 채널을 중심으로 소통한다. 2주 단위 목요일 밤 10시(KST)에 Zoom으로 정기 회의를 하고, 회의록은 Google Docs에 공개되어 있다. 팀 구성은 Lead 1명, Approver·Reviewer 각 여러 명, 그리고 활동 현행화가 필요한 Member 목록까지 투명하게 공개돼 있었다. 실제로 참여형 로컬라이제이션(ko) 프로젝트를 새로 발족하려는 신규 기여 문의가 Slack에 올라오는 것도 볼 수 있었는데, 커뮤니티 활발히 운영되기도 하고 나도 기여 할 수 있을까에 대한 막연한 두려움이 든다.. (글, 리뷰, Issue, PR등등 모두 영어여...)</p>
</blockquote>
<hr>
<h2 id="생각정리">생각정리</h2>
<p>같은 &quot;문서 현지화&quot;라도 OpenStack과 Kubernetes의 워크플로는 꽤 다른점이 처음 보는 입장에서는 적응하기 어려웠다. OpenStack은 Zanata(→Weblate)라는 전용 번역 툴 안에서 원문·번역문을 대조하며 작업하는 번역 전용 워크플로였고, 기여자의 활동을 &quot;번역 단어 수&quot;로 정량화해 거버넌스 투표권(AC 지위)까지 연결했다. 반면 쿠버네티스는 GitHub 저장소에 PR을 올리고 리뷰를 받는, 개발자에게 익숙한 코드 기여 방식 그대로였고 AI 사용 가이드라인까지 별도 문서로 명문화돼 있었다. 먼저 이 프로세스를 적응하고 방법, 돌아가는 구조를 이해해야 하기에 공식문서들을 들여다 봐야지.. (기여하는 부분에 있어 문서화가 정말 잘 되어있어 보기 편하다)</p>
<p>발대식을 진행하며 팀원들과의 자기소개 시간이 있었는데, 실제 현업에서 일하시는 분들도 많았고 인턴하시는 분들, 학생분들도 있었다. 모두 Cloud/Infra 관심이 있고 오픈소스 기여하는 것에 관심이 있어서 이렇게 팀으로 모이게 되었으니, 멘토님이 밋업 때 강조해주신 대로 팀원들과 함께 많이 소통하고 리뷰하고 배워야겠다. 발대식을 시작으로 이제 프로젝트가 시작된다. 앞으로 멘토님들을 따라 OpenStack, Kubernetes를 사용하는 한국사람들에게 더 보기 좋고 편한 문서를 만들기 위해 기여하면서, 더 좋은 서비스가 뭐가 있을까, 가독성이 좋은 글이 무엇일까(단어에 대한 양식은 정해진 부분이 많지만)에 대해서 고민을 많이 할 것 같다. 중간에 놓지 말고 끝까지 집중해서 많은 것을 배우고 성장하자. (좋은 Review를 받아 기여해 Organization Member가 되는것이 현재 목표!)</p>
<hr>
<blockquote>
<h3 id="참고-자료">참고 자료</h3>
</blockquote>
<h4 id="openstack-문서-기여-관련-한국어-자료">OpenStack 문서 기여 관련 (한국어 자료)</h4>
<ul>
<li>OpenStack I18n 가이드(한국어): <a href="https://docs.openstack.org/i18n/latest/ko_KR/">https://docs.openstack.org/i18n/latest/ko_KR/</a></li>
<li>I18n 가이드에 관하여(한국어): <a href="https://docs.openstack.org/i18n/latest/ko_KR/about.html">https://docs.openstack.org/i18n/latest/ko_KR/about.html</a></li>
<li>OpenStack 한국어 번역팀 위키: <a href="https://wiki.openstack.org/wiki/I18nTeam/team/ko_KR">https://wiki.openstack.org/wiki/I18nTeam/team/ko_KR</a></li>
<li>OpenStack 한국어 번역 문서: <a href="https://docs.openstack.org/ko_KR/">https://docs.openstack.org/ko_KR/</a></li>
<li>번역자로 기여하기(Contributor Guide): <a href="https://docs.openstack.org/contributors/common/i18n.html">https://docs.openstack.org/contributors/common/i18n.html</a></li>
<li>공식 OpenStack 번역자 되기: <a href="https://docs.openstack.org/i18n/latest/official_translator.html">https://docs.openstack.org/i18n/latest/official_translator.html</a></li>
<li>전체 언어팀 목록: <a href="https://wiki.openstack.org/wiki/I18nTeam/team">https://wiki.openstack.org/wiki/I18nTeam/team</a></li>
<li>Weblate 이전 관련 TC 문서: <a href="https://governance.openstack.org/tc/reference/upstream-investment-opportunities/2023/translations-infrastructure.html">https://governance.openstack.org/tc/reference/upstream-investment-opportunities/2023/translations-infrastructure.html</a><h4 id="쿠버네티스-문서-기여-관련">쿠버네티스 문서 기여 관련</h4>
</li>
<li>쿠버네티스 공식 문서: <a href="https://kubernetes.io/">https://kubernetes.io/</a></li>
<li>CNCF Landscape: <a href="https://landscape.cncf.io/">https://landscape.cncf.io/</a></li>
<li>쿠버네티스 커뮤니티 그룹: <a href="https://www.kubernetes.dev/community/community-groups/">https://www.kubernetes.dev/community/community-groups/</a></li>
<li>SIG-Docs Charter: <a href="https://github.com/kubernetes/community/blob/main/sig-docs/charter.md">https://github.com/kubernetes/community/blob/main/sig-docs/charter.md</a></li>
<li>Localization Subproject: <a href="https://github.com/kubernetes/community/tree/main/sig-docs/localization-subproject">https://github.com/kubernetes/community/tree/main/sig-docs/localization-subproject</a></li>
<li>쿠버네티스 문서 현지화 가이드: <a href="https://kubernetes.io/ko/docs/contribute/localization/">https://kubernetes.io/ko/docs/contribute/localization/</a></li>
<li>쿠버네티스 한글화 모범 사례: <a href="https://kubernetes.io/ko/docs/contribute/localization_ko_best_practice/">https://kubernetes.io/ko/docs/contribute/localization_ko_best_practice/</a></li>
<li>AI Guidance: <a href="https://www.kubernetes.dev/docs/guide/pull-requests/#ai-guidance">https://www.kubernetes.dev/docs/guide/pull-requests/#ai-guidance</a></li>
<li>CLA 안내: <a href="https://github.com/kubernetes/community/blob/main/CLA.md">https://github.com/kubernetes/community/blob/main/CLA.md</a></li>
<li>Kubernetes i18n Tracker: <a href="https://kfess.github.io/kubernetes-i18n-tracker">https://kfess.github.io/kubernetes-i18n-tracker</a></li>
<li>Contributor Playground: <a href="https://github.com/kubernetes-sigs/contributor-playground">https://github.com/kubernetes-sigs/contributor-playground</a></li>
<li>Kubernetes Slack: <a href="https://slack.k8s.io/">https://slack.k8s.io/</a> (#kubernetes-docs-ko)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026 AX 아이디어 공모전 도전기]]></title>
            <link>https://velog.io/@mi_nini/2026-AX-%EC%95%84%EC%9D%B4%EB%94%94%EC%96%B4-%EA%B3%B5%EB%AA%A8%EC%A0%84-%EB%8F%84%EC%A0%84%EA%B8%B0</link>
            <guid>https://velog.io/@mi_nini/2026-AX-%EC%95%84%EC%9D%B4%EB%94%94%EC%96%B4-%EA%B3%B5%EB%AA%A8%EC%A0%84-%EB%8F%84%EC%A0%84%EA%B8%B0</guid>
            <pubDate>Tue, 07 Jul 2026 13:55:41 GMT</pubDate>
            <description><![CDATA[<h2 id="대회-소개">대회 소개</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/c195f799-8c26-4df9-a957-782959eceff5/image.png" alt=""></p>
<p>2026년도 기후에너지환경부가 주최한 <strong>AX(AI Transformation) 아이디어 경진대회</strong>에 참가했다. 부문은 자유분석이 아니라 <strong>지정분석</strong>이었는데, 이 대회의 지정분석은 한 기관이 아니라 <strong>12개 공공기관이 각자 자기 실무 문제를 과제로 내놓는 방식</strong>이었다. 국립공원공단은 해안 해양쓰레기 유입 예측을, 한강홍수통제소는 AI 기반 하천 수위 예측과 디지털트윈을, 한국남동발전과 서부발전은 발전소 대기오염물질 배출 최적화를, 한국남부발전은 배출권 가격 예측을, 한국동서발전은 제주 SMP 예측을, 한국원자력환경공단은 처분시설 국지성 기상 예측을, 한국전력공사는 전기요금 과다발생 사전 예측을, 한국지역난방공사는 열사용 이상 탐지를, 한국환경공단과 환경산업기술원은 각각 공급망 분석과 생활화학제품 안전관리 진단을 내놓았다. 그리고 그중에 <strong>한국수자원공사</strong>가 낸 과제가 있었다.</p>
<blockquote>
<p><strong>조류경보 예측 AI 모델 개발</strong> — 대청댐 수질·수문·기상 정보를 바탕으로 유해남조류 발생 및 경보 발령 시점을 예측하고, 녹조 발생 주요 영향 인자를 도출해 사전 시나리오 기반 의사결정 지원체계를 구축한다.</p>
</blockquote>
<p>처음 과제 목록을 쭉 훑어볼 때는 이것저것 하고싶어서 고민이 많았다. 그래서 우리는 각자 하고 싶은 주제를 선택해 간단하게 기획서를 작성해 회의때 발표를 진행해서 더 많이 설득되고 좋은 주제에 투표해 많은 투표를 받은 주제를 선정하게 되었다. 그래서 우리 팀은 다애님이 조사해주셨던 수자원공사의 조류경보 과제를 골랐다. 수자원공사는 대청댐의 수질·수문 관측자료를 10개년치 그대로 지정과제 데이터로 제공해줬는데, 이걸 어떻게 가공하고 무엇과 엮어야 진짜 예측이 될지는 온전히 우리 몫이었다. </p>
<p>팀명은 <strong>춘배</strong>, 팀원은 나(김경민)를 포함해 최현준, 김다애(성현석님은 취업으로 인해 불참하게 되었다) 세 명이었다. 우리가 최종적으로 잡은 과제명은 다음과 같다.</p>
<blockquote>
<p><strong>대청댐 유해남조류 선제적 대응을 위한 자율형 의사결정 지원 시스템 구축</strong></p>
</blockquote>
<hr>
<h2 id="조사를-시작하며-확신이-생긴-이유">조사를 시작하며 확신이 생긴 이유</h2>
<p>수자원공사 과제를 고르고 조사를 시작하면서, 이 주제를 선택하길 잘했다는 확신이 점점 커진 이유가 세 가지 있었다.</p>
<p>첫째, <strong>매년 반복되는 사회적 이슈</strong>라는 점이다. 국립환경과학원이 조류경보제 지점을 9곳에서 13곳으로, 2030년까지 28곳으로 늘리겠다고 밝힌 걸 확인했다. 정부가 이미 이 문제에 예산과 정책 우선순위를 두고 있다는 신호였고, 우리 아이디어가 허공에 뜬 상상이 아니라 실제 정책 흐름 위에 올라탈 수 있다는 뜻이기도 했다.</p>
<p>둘째, <strong>대청댐 자체의 상징성</strong>이다. 대청호는 1998년 팔당호·충주호·주암호와 함께 국내 조류경보제가 최초로 도입된 4개 호소 중 하나다. 조류경보제의 &quot;원조&quot;격인 곳에서, 제도가 확대되어온 20여 년 동안 실제로 무엇이 바뀌었는지 들여다볼 가치가 있다고 판단했다.</p>
<p>셋째, 조사를 하면 할수록 <strong>현행 제도의 구조적 한계가 명확하게 보였다</strong>는 점이다. 조류경보제 운영지침을 읽어보니 발령 기준 자체가 &quot;2회 연속 유해남조류 세포수가 기준을 초과할 때&quot;였다. 첫 번째 초과가 확인되고서도 두 번째 채수 결과가 나올 때까지 기다려야 하는 구조다. 대청호 문의 수역에서 2023년 8월 30일 경계 단계가 발령된 사례를 역추적해보니, 관측 이후 실제 공식 대응까지 이어지는 데 최소 한 번의 채수 간격만큼, 체감상 열흘 가까운 행정 지연이 있었다.</p>
<blockquote>
<p>&quot;이미 나온 결과를 처리하는 시스템은 이미 있다. 데이터도 충분하지만 발생하고 나서 대응하는 방식이다. 우리가 이걸 미리 예측하고 예방한다면 도움이 되지 않을까?&quot;</p>
</blockquote>
<p>이 질문 하나로 방향을 잡았다. 그리고 처음부터 이걸 <strong>&quot;발령 이전 단계를 보완하는 시스템&quot;</strong>으로 포지셔닝하기로 팀 내에서 얘기가 나왔다.</p>
<hr>
<h2 id="제공된-데이터를-진짜-쓸모-있게-만드는-과정">제공된 데이터를 진짜 쓸모 있게 만드는 과정</h2>
<h3 id="수자원공사가-준-것-우리가-채워야-했던-것">수자원공사가 준 것, 우리가 채워야 했던 것</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/30140ee3-1ec5-4ad8-a56a-431bbdb80bcc/image.png" alt="">
지정과제 공고에는 수자원공사가 제공하는 데이터 항목이 명확히 적혀 있었다. <strong>대청댐 수질정보</strong>(최근 10개년, 주 단위 — 지점명, 채수위치, 조사일, 수온, pH, DO, 투명도, 탁도, Chl-a, 발령단계, 유해남조류 세포수와 4종별 세포수)와 <strong>대청댐 운영 수문정보</strong>(최근 10개년 — 날짜, 댐수위, 일유입량, 일방류량), 그리고 기상청 자료로 &quot;날씨, 일조시간 합계, 일강수량&quot; 정도가 함께 제공됐다. 즉 우리가 맨땅에서 대청댐 데이터를 수소문한 게 아니라, 애초에 이 세 축이 지정과제의 재료로 주어져 있었다.</p>
<p>문제는 공고에 적힌 기상 항목이 &quot;날씨, 일조시간 합계, 일강수량&quot; 정도로 꽤 단출했다는 점이다. 녹조 발생 메커니즘을 제대로 설명하려면 기온, 풍속, 일사량, 지면온도 같은 변수가 더 필요했다. 그래서 이 부분만큼은 우리가 직접 채워 넣어야 했다. 기상자료개발포털(data.kma.go.kr)을 뒤지고, 금강홍수통제소 사이트도 참고하면서 다음 관측망을 추가로 확보했다.</p>
<ul>
<li><strong>ASOS(종관기상관측)</strong>: 국가 공식 기후 자료로서 대표성을 갖는다. 대청호 유역과 관련된 보은(226, 회남 상류), 대전(133, 문의·추동 중하류), 청주(131, 문의 북측) 지점을 골랐다.</li>
<li><strong>AWS(방재기상관측)</strong>: 위험 기상을 실시간으로 감시하는 데 주목적이 있어 지역성이 강하다. 청남대(888), 장동(648), 세천(643), 옥천(605/604) 지점을 썼다.</li>
</ul>
<p>이 두 관측망의 성격이 다르다는 걸 이해하는 데도 시간이 좀 걸렸다. ASOS는 국가 대표 통계용이라 신뢰도는 높지만 지점 수가 적고, AWS는 촘촘하게 깔려 있지만 방재 목적이라 관측 항목이 더 단순했다(AWS는 일 자료 기준으로 평균기온·일강수량·평균풍속 정도만 뽑을 수 있었고, ASOS에는 일사량과 지면온도까지 있었다).</p>
<p>시간별 자료의 컬럼 하나하나도 다 뜯어봤다. 기온·이슬점온도·지면온도·5cm/10cm/20cm/30cm 지중온도, 강수량·적설·습도·증기압, 풍속·풍향·현지기압·해면기압, 일조·일사·전운량·중하층운량·운형·최저운고·시정, 지면상태·현상번호까지. </p>
<hr>
<h3 id="남조류가-왜-어떻게-자라는지부터-공부해야-했다">남조류가 왜, 어떻게 자라는지부터 공부해야 했다</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/2c8395b9-ef24-4ac0-810f-8a9a03c5c010/image.png" alt=""></p>
<p>기상 변수를 고르기 전에 먼저 &quot;남조류가 왜 대발생하는가&quot;를 생물학적으로 이해해야 했다. 이 부분은 정말 논문과 많은 자료, 운영지침을 오가며 시간을 많이 썼다. 정리하면 4대 메커니즘으로 요약된다.</p>
<p><strong>1. 온도 메커니즘</strong>: 남조류는 대표적인 고온성 생물이다. 기온이 수온을 결정하는 가장 큰 요인이고, 특히 25°C 이상 고온이 유지되는 기간이 핵심이다. 다만 물은 공기보다 늦게 데워지고 늦게 식기 때문에(수온 관성), 당일 기온보다 최근 7일간 누적된 기온과 지열이 더 정확한 신호였다.</p>
<p><strong>2. 광합성 메커니즘</strong>: 조류는 광합성으로 증식하므로, 단순히 해가 떠 있던 시간(일조시간)보다 실제 수면에 도달한 태양 에너지 총량(일사량)이 훨씬 정확한 설명 변수였다.</p>
<p><strong>3. 영양염류 유입 메커니즘</strong>: 비는 유역의 질소·인 같은 영양분을 댐으로 실어 나르는 &#39;먹이 공급원&#39;이다. 다만 폭우는 오히려 조류를 씻어내리는(세굴) 효과가 있어서, 강수량의 강도와 지속 시간을 함께 봐야 했다.</p>
<p><strong>4. 수직 안정도(성층) 메커니즘</strong>: 풍속이 낮을수록(대략 2m/s 미만) 물이 위아래로 섞이지 않는 &#39;성층 현상&#39;이 생기고, 이때 부력을 조절할 수 있는 남조류가 표층으로 떠올라 대발생하기 좋은 환경이 된다.</p>
<p>여기서 더 들어가서, <strong>종별로 특성이 다르다</strong>는 것도 알게 됐다. 이게 나중에 우리 프로젝트의 중요한 차별점이 됐다.</p>
<ul>
<li><strong>Microcystis</strong>: 국내에서 가장 흔한 녹조 주범. 간 독소(Microcystin) 생성. 고온(25°C 이상)과 강한 햇빛을 좋아하고, 스스로 부력을 조절해 표층으로 떠오르는 능력이 탁월하다.</li>
<li><strong>Anabaena(Dolichospermum)</strong>: 냄새 유발 물질을 만들고, 질소를 스스로 고정할 수 있어 질소가 부족해도 잘 자란다. 인 농도는 높은데 질소가 부족한 조건에서 우점 가능성이 커진다.</li>
<li><strong>Oscillatoria</strong>: 다른 종보다 낮은 온도와 적은 빛에서도 견딘다. 초여름·늦가을에도 나타날 수 있어, 수온이 완전히 오르지 않은 시기의 증가는 이 종이 원인일 가능성이 있다.</li>
<li><strong>Aphanizomenon</strong>: Microcystis가 힘을 못 쓰는 고온기가 아니라, 기온이 오르기 시작하는 초여름이나 찬바람이 부는 늦가을에 주로 번성한다. 계절성이 뚜렷해서 월(Month)이나 계절 변수가 중요한 힌트가 된다.</li>
</ul>
<hr>
<h3 id="대청호는-다른-호수와-뭐가-다른가">대청호는 다른 호수와 뭐가 다른가</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/65ac85e3-7041-4f33-8c07-cc071c4a7295/image.png" alt=""></p>
<p>대청호만의 특수성도 따로 파고들었다. 언뜻 &quot;체류시간이 길면 녹조가 심하다&quot;는 단순한 공식이 통할 것 같았지만, 실제로 찾아보니 그렇게 단순하지 않았다. 예를 들어 소양호는 체류시간이 긴데도 TSI(부영양화 지수) 기준으로 빈영양 호수로 분류된다는 신문 기사(중앙일보)를 발견했다. 즉 체류시간만으로는 설명이 안 되고, 질소·인 오염원의 유입량과 고온다습한 기후가 함께 맞물려야 한다는 걸 확인했다.</p>
<p>그리고 대청호 특유의 지형적 문제도 찾아냈다. 관련 논문(「대청호 상류유역의 오염원 현황과 수질관리 방안에 관한 연구: 안남천과 안내천을 중심으로」, 「소옥천 유역의 오염제어 대책에 따른 대청호 조류저감 효과 분석」)을 보면, 대청호는 본류보다 <strong>소옥천 같은 지류·만입부의 오염 기여도가 크다</strong>는 게 드러난다. 소옥천 유역 연구에서는 축산분뇨 에너지화와 표준시비량 적용이 가장 수질개선 효율이 높다는 시뮬레이션 결과(CE-QUAL-W2 모형)도 확인했다.</p>
<p>또한 대청호는 자연호가 아니라 <strong>길고 구불구불한 인공저수지</strong>라는 점도 중요했다. 이 구조 때문에 조류 발생이 계절 변수만으로 설명되지 않고, 방류 운영·체류시간 변화·지류 유입부의 국지적 오염에 크게 좌우된다는 사실을 알게 됐다. 그래서 우리는 자연호에서 쓰는 범용 모델을 그대로 가져오는 대신, 수리 변수(수위·방류량·유입량·체류시간)와 공간 분리(수역별 모델링)를 중심에 둔 대청호 전용 접근이 필요하다고 판단했다.</p>
<hr>
<h3 id="제공-데이터를-자동으로-갱신하는-파이프라인-만들기">제공 데이터를 자동으로 갱신하는 파이프라인 만들기</h3>
<p>수자원공사가 넘겨준 수질·수문 데이터는 정적인 엑셀 파일 형태였다. 이걸 한 번 받아서 끝내는 게 아니라, 같은 출처에서 최신 구간까지 자동으로 이어붙이고 재현 가능하게 관리하고 싶어서 공공데이터포털의 대청댐 수문 운영 정보 API부터 먼저 시도했다. 그리고 대청댐 수질정보·수문정보 공개 페이지를 직접 크롤링하는 쪽으로 방향을 틀었다.</p>
<p>개발자도구로 페이지의 고유 div-id를 확인하고, Selenium으로 연도별 구간을 순회하며 엑셀을 내려받는 자동화 스크립트(<code>water_gate.py</code>, <code>water_quality.py</code>)를 직접 짰다. 수문 데이터는 <code>dam_select0</code>, <code>dam_select1</code> 같은 select 엘리먼트를 자바스크립트로 조작해서 대청댐을 선택하고, 시작일·종료일을 넣은 뒤 <code>fnObj.hydrExcelDown()</code> 같은 페이지 내부 함수를 직접 호출하는 방식으로 다운로드를 자동화했다. 다운로드 중간에 예상치 못한 alert 창이 뜨는 경우도 있어서 재시도 로직도 넣어야 했다. 이렇게 만든 파이프라인 덕분에 수자원공사가 준 기준 데이터에, 우리가 직접 확보한 최신 기상 데이터를 결합해 하나의 최종 데이터셋으로 관리할 수 있었다.</p>
<hr>
<h3 id="결측치는-어떻게-처리했나">결측치는 어떻게 처리했나</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/5b1debba-45d2-4deb-b3fd-868339d0f286/image.png" alt=""></p>
<p>10개년 데이터를 통으로 쓰다 보니 결측 구간도 있었다. 2016년 1월부터 3월까지는 주요 수질 지표가 장기간 결측되어 있어서, 억지로 채우는 대신 그 구간 자체를 학습에서 제외했다. 일강수량(daily_rain)의 결측은 성격이 달랐는데, 관측소에서 비가 오지 않아 애초에 기록되지 않은 경우로 판단해서 일괄적으로 0으로 대체했다. 반면 평균·최저·최고기온과 평균풍속은 시계열 특성이 있는 변수라, 전후 날짜 값으로 선형보간(Linear Interpolation)해서 자연스러운 흐름을 유지했다. 이렇게 정리한 뒤 최종적으로 확보한 데이터 규모는 <strong>2016년 4월부터 2025년 12월까지, 6,304행 × 57컬럼, 결측치 0건, 중복 키 0건</strong>이었다.</p>
<hr>
<h2 id="불규칙한-채수-주기-그리고-슬라이딩-윈도우">불규칙한 채수 주기, 그리고 슬라이딩 윈도우</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/edd8baf0-e61f-4350-9ba9-47451152ccae/image.png" alt=""></p>
<p>데이터를 다 모으고 나서도 진짜 문제는 그다음이었다. 수질 채수일이 <strong>월요일이었다가 목요일로 바뀌고, 주 1회였다가 2회로 늘어나는</strong> 식으로 불규칙했다. 처음엔 이게 그냥 현장 사정인 줄 알았는데, 나중에 「조류예측 및 수질관리협의회에 관한 규정」 제8조를 읽고서야 이유를 알았다. 조류예측 정보는 <strong>&quot;주 2회(월요일과 목요일) 17시에 발표&quot;</strong>하되, 유해남조류 세포수가 상수원구간 기준 10,000 세포/mL를 하루라도 초과하면 <strong>매 근무일마다 발표</strong>하도록 되어 있었다. 즉 우리를 힘들게 했던 불규칙성 자체가 이미 정책적으로 설계된 결과였던 것이다. </p>
<p>이 불규칙성을 다루기 위해 채택한 방법이 <strong>&quot;이벤트 중심 샘플링(Event-based Sampling)&quot;</strong>이다. 날짜(달력) 기준이 아니라 &#39;채수 이벤트(정답지가 있는 날)&#39;를 닻(Anchor)으로 삼고, 그 과거로 창문(Window)을 던져 데이터를 긁어오는 방식이다.</p>
<ul>
<li><strong>단기 윈도우(T-3일)</strong>: 비 온 직후의 희석 효과, 급격한 폭염 같은 조류의 즉각적인 반응을 포착</li>
<li><strong>장기 윈도우(T-7일~T-14일)</strong>: 영양염류 축적, 물의 정체 상태 같은 댐의 기저 환경을 포착</li>
</ul>
<p>이 방식의 핵심은, 채수 간격이 며칠이든 상관없이 모델은 항상 &quot;결과가 나오기 직전 N일 동안 누적된 환경&quot;이라는 일관된 패턴을 학습한다는 점이다. 그리고 <strong>학습할 때와 운영할 때의 논리가 다르다</strong>는 것도 중요한 설계 포인트였다. 학습 시에는 정답(실측 세포수)이 있는 채수일을 기준으로 과거 데이터를 묶어 공부하지만, 실제 운영 시에는 정답지가 없어도 된다. 매일매일 윈도우를 굴리며 예측을 수행할 수 있기 때문이다.</p>
<p>변수별 가공 방식도 성격에 따라 다르게 설계했다.</p>
<ul>
<li><strong>기온</strong>: 평균과 최대값을 함께 본다. 7일 평균기온이 전반적인 더위를, 3일 최고기온의 평균이 폭염 강도를 나타낸다.</li>
<li><strong>일사량</strong>: 합계(누적)를 쓴다. 남조류 증식은 태양 에너지가 누적되어야 폭발하기 때문에 평균보다 합계가 생물학적으로 더 정확하다.</li>
<li><strong>강수량</strong>: 합계와 특정 시점 값(Lag)을 함께 쓴다. 3일 누적강수량과 T-7일 단일 강수량을 따로 변수화했다.</li>
<li><strong>풍속</strong>: 조건 만족 일수를 카운트한다. 풍속 자체의 평균은 의미가 희석되므로, &quot;2m/s 이하가 며칠 지속되었는가&quot;를 세는 방식이 훨씬 유의미했다.</li>
<li><strong>수위·저수율</strong>: 절대값보다 변동폭(Delta)이 중요하다. 오늘 수위에서 7일 전 수위를 뺀 값으로 물이 차오르는지 빠지는지를 봤다.</li>
<li><strong>유입량·방류량</strong>: 상호작용 변수로 만들었다. 체류시간(Retention Time) = 저수량 / 일평균 방류량. 이 값이 클수록 물이 오래 고여 있다는 뜻이고, 조류 예측에서 가장 강력한 변수 중 하나가 됐다.</li>
</ul>
<p>수질 자료(채수 데이터)는 매일 있는 게 아니므로 윈도우로 묶을 수 없었다. 대신 &quot;직전 채수일(T-1)의 상태&quot;를 오늘(T)의 결과를 예측하는 힌트로 썼다. 탁도 증가율(T-2 대비 T-1의 탁도 급증)은 비가 와서 오염물이 들어왔다는 강력한 증거로 활용했고, pH도 흥미로운 변수였다. 낮에는 광합성이 활발해서 CO₂가 줄고 pH가 오르며, 밤에는 호흡 작용으로 반대가 된다. 이 동조 현상 때문에 pH 변화량 자체가 녹조 발생의 현재 증거로 쓰일 수 있었다.</p>
<hr>
<h2 id="정책-문서를-조문-단위로-읽다">정책 문서를 조문 단위로 읽다</h2>
<table>
<thead>
<tr>
<th align="center">발령기준</th>
<th align="center">모니터링 프로세스</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><img src="https://velog.velcdn.com/images/mi_nini/post/e47bb5e9-fdd7-4315-9894-6248f945821c/image.png" width="100%"></td>
<td align="center"><img src="https://velog.velcdn.com/images/mi_nini/post/0e4b4993-4b54-474a-8ff3-c6952fa51136/image.png" width="100%"></td>
</tr>
</tbody></table>
<p>「조류예측 및 수질관리협의회에 관한 규정」을 조문 하나하나 짚어가며 우리 모델과 어떻게 연결되는지 정리했다.</p>
<ul>
<li><strong>제4조(조류예측 기간)</strong>: &quot;발표일로부터 7일간&quot; → 우리 모델의 예측 시점(next_log_cells, 7일 후)을 정책 언어에 그대로 맞출 수 있는 근거였다.</li>
<li><strong>제5조(조류예측 항목)</strong>: 수온, 유해남조류세포수, 기타 필요사항 → 우리는 수온을 주요 입력 변수로, 유해남조류세포수를 회귀·분류 타깃으로 정확히 대응시켰다.</li>
<li><strong>제6조(분석자료)</strong>: 수치모델링 예측 결과, 기상·수질·유량·위성영상 등 관측자료 → 우리가 수질만 보지 않고 기상·댐 운영 데이터까지 통합한 이유가 여기 있었다.</li>
<li><strong>제7조(조류예측구간)</strong>: 금강은 대청호부터 금강 하구언까지 → 대청호가 이미 법정 예측구간에 포함되어 있다는 걸 확인.</li>
<li><strong>제8조(조류예측 발표)</strong>: 5월~10월 발표, 주 2회(월·목) 17시 발표 → 앞서 언급한 채수 불규칙성의 원인.</li>
<li><strong>제10조(사후평가)</strong>: 매년 1월 사후평가 실시 → 우리 프로젝트의 baseline 대비 성능 개선 검증 로직과 대응시켰다.</li>
<li><strong>제15조(협의회 협의 사항)</strong>: 단계별 대응조치, 관계기관 상호 의견조정, 비상연락체계 → 우리가 만든 &quot;시나리오 기반 의사결정 지원 체계&quot;가 실제로 연결될 수 있는 제도적 지점이었다.</li>
</ul>
<p>「물환경보전법 시행령」도 함께 봤고, 2025년 정부 보도자료(「2025년 여름철 녹조 대응」)도 꼼꼼히 읽었다. 여기서 정부가 이미 <strong>영양염류 유입 최소화 → 녹조 신속 수거·제거 → 댐·보·하굿둑 연계 운영 → 취·정수 관리 강화</strong>라는 4단계 대응 체계를 갖추고 있다는 걸 확인했고, 우리 시나리오 설계를 이 4단계에 맞춰 재구성했다. 예를 들어 &quot;산불 영향구간 38개소를 대상으로 pH·T-P 등 6개 항목을 집중 모니터링하고 측정주기를 월 1회에서 2회로 확대&quot;한다는 문장에서, 정부가 이미 &quot;사건 기반 수질 변화 흐름&quot;(산불 → 강우 → 토사·영양염류 유입 → 수질 변화 → 녹조 위험)을 인식하고 있다는 것도 알게 됐다. 이건 우리의 강수 후 유입 시차 가설(H2)과 정확히 같은 논리 구조였다.</p>
<p>또한 AI 조류발생 예보가 이미 2024년 34개 지점에서 2025년 41개 지점으로 확대되고 있다는 사실도 찾아냈다. &quot;기온·강수·일사량·수온을 분석해 일주일 후 조류발생을 예측&quot;한다는 문장은, 우리가 하려는 것과 정확히 같은 방향이었다. 이 조사를 하면서 &quot;우리가 완전히 새로운 걸 하는 게 아니라, 이미 진행 중인 정책 방향에 숟가락을 얹는 형태&quot;라는 확신이 생겼고, 이게 오히려 발표에서 설득력을 높이는 포인트가 됐다.</p>
<hr>
<hr>
<h2 id="무엇을-예측할지부터-정확히-정의하기">무엇을 예측할지부터 정확히 정의하기</h2>
<p>모델을 짜기 전에 의외로 오래 붙잡고 있던 질문이 &quot;정확히 뭘 예측할 것인가&quot;였다. 조류경보제의 행정 판단 기준은 결국 유해남조류 세포수이기 때문에, Chl-a나 수온 같은 간접 지표를 타깃으로 삼으면 모델이 아무리 정확해도 실제 의사결정과 바로 연결되지 않는다. 그래서 Microcystis·Anabaena·Oscillatoria·Aphanizomenon 4종의 세포수 합계, 즉 법정 경보 기준과 직접 맞닿아 있는 <strong>유해남조류_세포수</strong>를 최종 타깃으로 잡았다.</p>
<p>회귀 타깃은 현재 세포수가 아니라 <code>next_log_cells</code>, 즉 <strong>다음 채수 시점 세포수를 log10(cells + 1)로 변환한 값</strong>으로 정의했다. 현재 세포수를 맞히는 모델은 이미 관측된 상태를 재확인하는 수준에 그치지만, 우리 목적은 오늘까지의 정보로 다음 시점(약 7일 후)의 위험을 먼저 알려주는 것이었다. 로그 변환을 쓴 이유는 세포수가 평상시엔 낮다가 여름철 특정 시기에 수천~수만 단위로 튀는 치우친 분포를 갖기 때문인데, 이렇게 하면 0도 안전하게 처리하면서 급증 구간의 영향을 완화할 수 있고, 예측 후에는 다시 cells/mL 단위로 복원해 현장 기준과 비교할 수 있었다.</p>
<p>분류 타깃은 <code>next_alert_binary</code>, 다음 채수 시점 예측 세포수가 관심 기준인 1,000 cells/mL 이상인지를 나타내는 이진값으로 정의했다. 미발령·관심·경계·대발생 4단계를 한 번에 분류하는 다중분류 대신 이진분류로 좁힌 이유는, 경계·대발생 표본이 상대적으로 적어서 다중분류로 쪼개면 학습이 불안정해질 위험이 있었고, 무엇보다 실무에서 가장 먼저 필요한 질문이 &quot;다음 채수 때 관심 이상으로 넘어갈 가능성이 있는가&quot;였기 때문이다.</p>
<p>마지막으로 <code>previous_exceeded</code>(직전 채수가 기준을 넘었는지)를 예측 결과와 함께 보는 것도 중요한 설계였다. 조류경보제 자체가 한 번 넘었다고 바로 확정되는 게 아니라 <strong>2회 연속 초과</strong>를 발령 조건으로 삼기 때문에, 직전 실측이 기준을 넘었는지와 다음 시점 예측이 기준을 넘는지를 함께 봐야 <code>operational_alert_candidate</code>(운영상 발령 후보)를 제대로 판단할 수 있었다.</p>
<hr>
<h2 id="가설을-먼저-세우고-데이터로-검증하기">가설을 먼저 세우고, 데이터로 검증하기</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/01f56586-a0ec-47db-bd37-061f7403c34c/image.png" alt=""></p>
<p>이 모든 조사를 바탕으로 다섯 개의 검증 가능한 가설을 세웠다. 그냥 &quot;녹조가 늘어날 것이다&quot; 수준이 아니라, 조건과 수치를 구체적으로 명시했다.</p>
<table>
<thead>
<tr>
<th>가설</th>
<th>내용</th>
<th>근거 자료</th>
</tr>
</thead>
<tbody><tr>
<td><strong>H1</strong> 고수온·고pH 조건</td>
<td>수온·pH 상승, 일조시간·Chl-a·Microcystis 증가가 함께 나타나면 표층 증식 위험이 커진다</td>
<td>조류경보제 운영지침, Liu et al.(2011)</td>
</tr>
<tr>
<td><strong>H2</strong> 강수 후 유입 시차</td>
<td>집중강우 이후 3~14일 뒤 유입량·탁도·질소고정종 증가로 위험이 상승한다</td>
<td>2025년 여름철 녹조 대응 보도자료, Deng et al.(2024)</td>
</tr>
<tr>
<td><strong>H3</strong> 수역 간 선행 신호</td>
<td>회남·추동·문의 중 한 수역의 급증이 다른 수역 위험의 선행 정보가 된다</td>
<td>대청호 조류경보 보도자료, 조류예측 규정</td>
</tr>
<tr>
<td><strong>H4</strong> 방류량 감소·체류시간 증가</td>
<td>물이 오래 고일수록 위험이 커진다(특히 저풍속·고수온과 결합 시)</td>
<td>Van Liere &amp; Mur(1980), De Nobel et al.(1997)</td>
</tr>
<tr>
<td><strong>H5</strong> 7일 리드타임</td>
<td>7일 후 위험을 사전에 예측해 대응 준비에 쓸 수 있다</td>
<td>조류예측 규정, Recknagel et al.(2025)</td>
</tr>
</tbody></table>
<p>각 가설을 실제 데이터에서 <strong>Risk Ratio와 Fisher&#39;s exact test</strong>로 검증했다. 결과를 표로 정리하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>가설</th>
<th>조건 충족 시 위험 발생률</th>
<th>일반 기간 발생률</th>
<th>Risk Ratio</th>
<th>p-value</th>
<th>판정</th>
</tr>
</thead>
<tbody><tr>
<td>H1</td>
<td>65.7%</td>
<td>14.0%</td>
<td><strong>4.69배</strong></td>
<td>2.79e-65</td>
<td>지지</td>
</tr>
<tr>
<td>H2</td>
<td>38.4%</td>
<td>13.0%</td>
<td><strong>2.96배</strong></td>
<td>3.13e-31</td>
<td>지지</td>
</tr>
<tr>
<td>H3</td>
<td>61.4%</td>
<td>8.6%</td>
<td><strong>7.16배</strong></td>
<td>1.36e-100</td>
<td><strong>가장 강하게 지지</strong></td>
</tr>
<tr>
<td>H4</td>
<td>25.8%</td>
<td>22.6%</td>
<td>1.14배</td>
<td>0.164</td>
<td>부분 지지(보조 신호)</td>
</tr>
<tr>
<td>H5</td>
<td>Recall 100%, Precision 90.7%</td>
<td>-</td>
<td>-</td>
<td>1.81e-56</td>
<td>지지</td>
</tr>
</tbody></table>
<p>H4가 통계적으로 유의하지 않게 나온 게 개인적으로 제일 흥미로웠다. &quot;물이 고이면 녹조가 생긴다&quot;는 직관은 워낙 당연해 보이는데, 막상 단독 요인으로는 유의성이 안 나왔다. 그래서 우리는 정체 조건을 독립 요인이 아니라 <strong>고수온·저풍속·종별 우점 변화와 결합될 때만 의미 있는 보조 신호</strong>로 재정의했다. 데이터가 우리 예상을 배신했을 때, 그걸 억지로 끼워 맞추지 않고 &quot;Not Supported&quot;로 정직하게 보고서에 적은 것 — 이게 이번 프로젝트에서 스스로 가장 뿌듯했던 부분이다.</p>
<hr>
<hr>
<h2 id="모델링">모델링</h2>
<h3 id="이상치를-다루는-방식부터-다시-생각해야-했다">이상치를 다루는 방식부터 다시 생각해야 했다</h3>
<table>
<thead>
<tr>
<th align="center">Before</th>
<th align="center">After</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><img src="https://velog.velcdn.com/images/mi_nini/post/4706180d-590c-4c31-a83c-2fe805b42ea2/image.png" width="100%"></td>
<td align="center"><img src="https://velog.velcdn.com/images/mi_nini/post/a0e9e805-e3d9-4552-9bf3-ad1ce8dd3e73/image.png" width="100%"></td>
</tr>
<tr>
<td align="center">일반적인 데이터 분석이라면 극단값은 노이즈로 취급해서 제거하는 게 맞다. 그런데 우리 문제는 정반대였다. 세포수가 만 단위, 십만 단위로 튀는 구간이야말로 <strong>경보가 필요한 바로 그 순간</strong>이다. 이상치를 지우면 모델이 정작 맞혀야 할 걸 놓치게 된다.</td>
<td align="center"></td>
</tr>
</tbody></table>
<p>그래서 타깃부터 <code>next_log_cells = log10(다음 시점 세포수 + 1)</code>로 로그 변환해서 왜도가 심한 분포를 완화했다. 회귀 후보 모델에는 이상치에 강한 <strong>HuberRegressor</strong>를 포함시켰는데, 이건 튜닝 과정도 나름 길었다. 처음엔 기본 설정으로 RMSE 0.7070이 나왔는데, <code>epsilon=2.5</code>, <code>alpha=0.0</code>, <code>max_iter=3000</code>, <code>tol=0.00001</code>, <code>scaler=robust</code>로 파라미터를 조정하니 RMSE가 0.6695까지 떨어졌다. 이후 Stacking Ensemble에 HuberRegressor를 추가로 포함시키자 최종 RMSE가 0.6554까지 개선됐다.</p>
<p>강수량 관련 컬럼(daily_rain, rain_3d_sum, rain_7d_sum, rain_14d_sum)은 평상시엔 0에 수렴하다가 태풍·집중호우 시 극단적 이상치를 갖는 왜도 심한 분포였다. 이런 컬럼엔 중앙값과 사분위수 범위(IQR)를 쓰는 <strong>RobustScaler</strong>를 적용했고, 반대로 기온·바람·일사량처럼 물리적 상하한이 비교적 일정한 변수엔 <strong>MinMaxScaler</strong>를 적용해서 특정 변수의 단위 크기가 학습을 왜곡하지 않도록 했다.</p>
<hr>
<h3 id="최종-모델-구조">최종 모델 구조</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/a921cc06-1ada-40af-82a8-bde14bb0edd0/image.png" alt=""></p>
<p>최종적으로는 LightGBM, XGBoost, RandomForest, HistGradientBoosting, CatBoost, HuberRegressor를 베이스 모델로 하고 RidgeCV를 메타 모델로 쌓은 <strong>Stacking Ensemble</strong> 구조를 채택했다. 트리 기반 모델은 비선형 조건과 변수 간 상호작용을 잘 잡아내고, HuberRegressor는 이상치에 강한 안정적 방향성을 제공하며, RidgeCV는 이 예측들을 다시 안정적인 선형 결합으로 정리해 과적합을 완화하는 역할을 맡았다.</p>
<p>분류 모델도 같은 논리로 여러 모델의 확률 예측(predict_proba)을 LogisticRegression이 최종 종합하는 StackingClassifier를 썼다. 여기서 Logistic Regression은 단순 분류기가 아니라, 여러 모델이 계산한 위험 확률을 운영 관점에서 최종 정리하는 의사결정 레이어 역할이었다.</p>
<table>
<thead>
<tr>
<th>모델</th>
<th>RMSE(log)</th>
<th>RMSE(cells)</th>
<th>MAE(log)</th>
<th>R²</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Stacking Ensemble</strong></td>
<td><strong>0.655~0.668</strong></td>
<td>13,315~13,316</td>
<td>0.477~0.486</td>
<td>0.844~0.846</td>
</tr>
<tr>
<td>LightGBM</td>
<td>0.677</td>
<td>12,654.5</td>
<td>0.484</td>
<td>0.836</td>
</tr>
<tr>
<td>RandomForest</td>
<td>0.679</td>
<td>14,073.2</td>
<td>0.512</td>
<td>0.835</td>
</tr>
<tr>
<td>HistGradientBoosting</td>
<td>0.685</td>
<td>12,601.8</td>
<td>0.496</td>
<td>0.832</td>
</tr>
<tr>
<td>XGBoost</td>
<td>0.687</td>
<td>13,398.1</td>
<td>0.511</td>
<td>0.831</td>
</tr>
<tr>
<td>CatBoost</td>
<td>0.696</td>
<td>13,832.1</td>
<td>0.532</td>
<td>0.826</td>
</tr>
<tr>
<td>HuberRegressor</td>
<td>0.6695~0.707</td>
<td>13,348~14,615</td>
<td>0.478~0.480</td>
<td>0.821~0.839</td>
</tr>
<tr>
<td>Persistence Baseline</td>
<td>0.799~0.800</td>
<td>14,604.1</td>
<td>0.476~0.480</td>
<td>0.771~0.772</td>
</tr>
</tbody></table>
<p>분류 모델(RandomForest 기준)은 <strong>Accuracy 95.6%, Precision 90.7%, Recall 100.0%, F1 0.951</strong>이 나왔다. 검증 데이터 기준 실제 관심 기준 이상 사례 137건을 하나도 놓치지 않았다(False Negative = 0). Binomial test 기준 p-value는 1.81e-56으로, 이 결과가 우연히 다수 클래스를 찍어서 나온 게 아니라는 것도 확인했다.</p>
<p>지점별 예측 오차(MAE, log 스케일)도 따로 확인했는데, 추동 0.447, 회남 0.547, 문의 0.438로 회남이 상대적으로 오차가 컸다. 상류에 위치한 회남이 강우 직후 변동성이 가장 크기 때문으로 해석했다.</p>
<h3 id="shap으로-설명-가능성-확보하기">SHAP으로 설명 가능성 확보하기</h3>
<p>모델 성능만큼 신경 쓴 게 <strong>&quot;왜 위험하다고 판단했는지&quot;를 설명하는 것</strong>이었다. XGBoost, LightGBM, CatBoost, RandomForest 계열은 SHAP 기반 해석과 잘 연결됐다. 예를 들어 &quot;강우 증가 → 영양염류 유입 증가 → 체류시간 증가 및 수온 상승 → 남조류 증식 → 경보 위험 증가&quot;라는 환경 흐름을 feature contribution 형태로 설명할 수 있도록 설계했다. 관리자가 &quot;위험하다/아니다&quot;만 보는 게 아니라, &quot;왜 위험한지&quot;와 &quot;어떤 대응이 필요한지&quot;까지 함께 이해할 수 있게 만드는 게 목표였다.</p>
<hr>
<h2 id="예상-밖의-발견들">예상 밖의 발견들</h2>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center"></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><img src="https://velog.velcdn.com/images/mi_nini/post/58bcb58f-5d77-431e-aa0a-137a0000c9c5/image.png" width="100%"></td>
<td align="center"><img src="https://velog.velcdn.com/images/mi_nini/post/a3aaee09-7062-4606-9655-31f579f09b91/image.png" width="100%"></td>
</tr>
</tbody></table>
<p>모델을 다 만들고 나서 데이터를 다시 들여다보다가, 애초에 의도하지 않았던 인사이트 두 개를 발견했다.</p>
<p><strong>1. 연속 초과 확률이 88.1%였다.</strong> 직전 채수에서 관심 기준(1,000 cells/mL)을 넘긴 지점은, 다음 채수에서도 88.1%(318/361건) 확률로 다시 기준을 넘겼다. 반대로 직전에 기준 미달이었던 경우 다음 시점 초과 확률은 3.5%(43/1215건)에 불과했다. 즉, 조류경보제의 &quot;2회 연속 초과&quot; 발령 기준 자체가, 이미 통계적으로 상당히 예측 가능한 흐름을 그대로 기다리는 구조라는 뜻이었다. 우리는 이걸 근거로 &quot;첫 초과 시점을 단순 관찰값이 아니라 조기 경보 신호로 활용하자&quot;는 제안을 만들었다.</p>
<p><strong>2. 종별 기여도와 출현 빈도가 정반대였다.</strong> 전체 세포수 총량 기준으로는 Microcystis가 84.9%(4,117,642)를 차지해 압도적이었다. 그런데 <strong>출현 빈도</strong>로만 보면 Aphanizomenon이 73.3%(683건)로 오히려 더 자주 나타났다(총량 기여도는 7.0%에 불과했는데도). Microcystis는 65.1%(607건) 빈도로 두 번째였다. 이건 &quot;유해남조류 4종을 합산 관리하는&quot; 현행 방식이 종별 생태 특성 차이를 가려버릴 수 있다는 뜻이었다. 우리는 이걸 &quot;법정 기준의 4종 합산 관리는 유지하되, 예측 모델이 7일 후 예상한 우점종의 생태적 특성에 맞춰 현장 대응 시나리오를 다각화해야 한다&quot;는 제안으로 연결했다.</p>
<hr>
<h2 id="시나리오-설계-예측을-현장-언어로-바꾸기">시나리오 설계: 예측을 현장 언어로 바꾸기</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/93d96c12-9d72-40d4-a546-39f0de27f759/image.png" alt=""></p>
<p>예측값 하나만 던져주면 현장 담당자는 다시 매뉴얼, 채수 기준, 수문 상태, 기상 조건을 따로 확인해야 한다. 그래서 시나리오를 예측값과 현장 대응을 잇는 번역기로 설계했다. 기준은 네 가지 축이었다.</p>
<ol>
<li><strong>행정 경보 기준</strong>: 관심(1,000)·경계(10,000)·조류대발생(1,000,000) 기준에 직접 연결되는 &quot;관심 발령 후보&quot;, &quot;경계 발령 후보&quot;, &quot;조류대발생 감시&quot;</li>
<li><strong>발생 메커니즘</strong>: 앞서 정리한 4대 메커니즘에 대응하는 &quot;수온 20도 이상 계절 감시&quot;, &quot;고온·고일사 성장 촉진&quot;, &quot;강우 이후 영양염류 유입&quot;, &quot;수체 정체·성층 위험&quot;</li>
<li><strong>공간 전파 구조</strong>: 회남의 선행 위험이 다른 수역에 영향을 줄 수 있다는 H3 가설을 반영한 &quot;회남 선행 전파 관찰&quot;</li>
<li><strong>운영 단계</strong>: 아직 발령 후보는 아니지만 접근 중인 &quot;관심 기준 접근&quot;, 하향 추세인 &quot;하향·해제 관찰&quot;, 위험 신호가 없는 &quot;일반 안정&quot;</li>
</ol>
<p>전체 기간에 대해 시나리오를 생성해보니 일반 안정이 823행으로 가장 많았고, 관심 발령 후보 271행, 관심 기준 접근 142행, 수온 20도 이상 계절 감시 71행, 경계 발령 후보 63행, 회남 선행 전파 관찰 8행 순이었다. 각 시나리오에는 실제 대응 초안도 함께 붙였는데, 예를 들어 &quot;경계 발령 후보&quot;에는 &quot;주 2회 이상 채수 체계, 정수처리 강화, 취수 수심 조정, 물순환설비 가동, 조류 제거 장비 준비 여부를 우선 확인&quot;이라는 식으로 정부 자료(2025년 녹조 대응 보도자료, 조류제거물질 사용지침)에 나온 실제 대응 수단을 그대로 연결했다.</p>
<hr>
<h2 id="llm-에이전트까지-고민했던-이유">LLM 에이전트까지 고민했던 이유</h2>
<p>사실 이 프로젝트 초반 아이디어 회의에서는 더 큰 그림을 그리고 있었다. 출발점은 &quot;기존 방식의 한계&quot;에 대한 문제의식이었다. AI가 &quot;7일 뒤 추동 수역 세포수 15,000cells/mL 예상&quot;이라는 숫자만 던져주면, 실무자는 다시 매뉴얼을 찾아보고 현재 방류량을 확인한 뒤 직접 대응 방안을 고민해야 한다. 지금도 대응 체계가 완전히 자동화되지 못해서 과거 자료와 육안 확인에 의존하는 부분이 많다고 판단했다. 그래서 우리는 &quot;AI가 수치 예측뿐 아니라 원인을 스스로 해석하고, 구체적인 관리 지시서까지 자동 생성하면 어떨까&quot;라는 질문을 던졌다. 단순히 &quot;예측하는 모델&quot;이 아니라, <strong>예측 결과를 스스로 해석하고 관리 지시서까지 자동 생성하는 자율형 에이전트</strong>를 만들면 어떨까 하는 논의였다. </p>
<p>정량 예측(회귀/분류) → SHAP 기반 원인 해석 → LLM이 이걸 자연어 관리 지시서로 변환하는 파이프라인이었다. 실제 최종 산출물에는 <code>llm_publisher.py</code>라는 스크립트로 이 개념을 일부 구현해뒀다. 예측 결과와 시나리오를 LLM 프롬프트 형태로 정리해서 payload, prompt, 샘플 CSV를 자동 생성하는 기능이다. 시간 관계상 이 부분을 완전히 실전 수준까지 다듬지는 못했지만, &quot;AI가 판단 근거까지 설명하는 구조&quot;를 지향했다는 점에서 팀 내부적으로는 의미 있는 시도였다고 생각한다.</p>
<hr>
<h2 id="bc-분석-숫자로-설득하기">B/C 분석: 숫자로 설득하기</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/dfd78d81-b3e3-43e7-aa96-c7c37f7c9ab0/image.png" alt=""></p>
<p>기술적으로 그럴듯한 모델이라도, 결국 공공기관 담당자를 설득하려면 &quot;그래서 이게 얼마나 이득인가&quot;를 숫자로 보여줘야 했다. 「정수장 조류 대응 가이드라인」 63페이지에 나온 공식 비용 산정 기준을 그대로 가져왔다.</p>
<ul>
<li>실시간 자동분석시스템 비용: 약 1.5억 원/10년 (구입비 5천만원 + 운영비 1억원/10년)</li>
<li>분말활성탄 절감액: 정수용량 1,000,000㎥/일 × 20mg/L × 90일 × 1,500천원/톤 × 20% 절감 = 약 5.4억 원/년</li>
<li>오존 처리비 절감액: 1,000,000㎥/일 × 2.5원/㎥ × 90일 × 20% 절감 = 약 0.45억 원/년</li>
</ul>
<p>여기에 예측 기반 조기대응(사전 탐지 18건 × 평균 4일 × 1일당 대응가치 50만원 = 3,600만원), 미탐 감소(6건 × 건당 500만원 = 3,000만원)를 편익에 더하고, 늘어난 오경보 비용(4건 × 건당 80만원 = -320만원)을 차감해서 최종 계산을 했다.</p>
<pre><code>B/C = 총편익 64.78억 원 / 총비용 3.5억 원 ≈ 18.51</code></pre><p>1원을 투입하면 약 18.51원의 편익이 발생할 수 있는 구조라는 계산이 나왔다. 다만 이건 확정된 실측 결과가 아니라 &quot;이런 방식으로 정량화할 수 있다&quot;는 프레임을 제시한 것일 뿐이라는 점을 보고서에 분명히 명시했다. 실제 대청댐 운영자료 없이 부풀린 숫자로 포장하고 싶지 않았고, 최종 B/C는 관계기관의 조류예찰·정수처리·방제장비·경보 단계별 운영비 자료를 연계해 재산정해야 한다는 단서를 달았다.</p>
<hr>
<h2 id="리스크도-먼저-고민했다">리스크도 먼저 고민했다</h2>
<p>AI 예측이 잘못 쓰이면 오히려 해가 될 수 있다는 것도 처음부터 인지하고 있었다. 그래서 세 가지 리스크 관리 방안을 별도로 정리했다.</p>
<ol>
<li><strong>오해 방지</strong>: 예측 결과가 공식 경보나 수돗물 안전성 문제로 오해되지 않도록, 외부 안내 시에는 예측 위험도·실제 관측값·공식 경보 단계를 명확히 구분해서 제공한다.</li>
<li><strong>권한 분리</strong>: 조류경보 발령과 행정 조치 권한은 기존 조류경보제 운영 절차에 그대로 유지한다. AI는 자동 발령이나 자동 조치를 하지 않고, 사전 예찰과 대응 준비를 보조하는 도구로 역할을 제한한다.</li>
<li><strong>편향성 관리</strong>: 전체 정확도만 보면 특정 수역의 미탐이나 계절별 오경보를 놓칠 수 있어서, 지점별 재현율 편차·계절별 오경보율·경보 단계별 미탐률을 따로 점검한다.</li>
</ol>
<hr>
<h2 id="발표-그리고-본선까지">발표, 그리고 본선까지</h2>
<p>서류 심사를 통과하고 본선 발표 기회를 얻었다. 발표는 여섯 파트로 구성했다.</p>
<ol>
<li>문제 정의 — 녹조 대응은 왜 AI 예측 단계로 진입해야 하는가</li>
<li>분석 목표와 핵심 가설(H1~H5)</li>
<li>데이터 융합과 Feature 설계</li>
<li>AX 예측 모델링 프레임워크</li>
<li>검증 결과와 대응 시나리오</li>
<li>정책 활용성과 운영 리스크 관리</li>
</ol>
<p>발표 자료 첫 장에 넣은 문제의식은 &quot;경보보다 빠른 준비&quot;였고, 마지막 장에 넣은 문장은 이거였다.</p>
<blockquote>
<p>&quot;AI가 경보를 대신 내리는 것이 아니라, 공공기관이 더 빠르고 근거 있게 준비하도록 돕는 것입니다.&quot;</p>
</blockquote>
<p>우리 스스로도 이 예측 시스템이 만능이 아니라는 걸 인지하고 있고, 그 안에서 실질적으로 기여할 수 있는 지점이 어디인지를 명확히 하고 싶었다. 결과적으로 본선까지는 올라갔지만 최종 수상에는 들지 못했다. 다른 팀들의 결과물과 심사 흐름을 지켜보면서 아쉬운 지점도 있었지만, 그것과 별개로 이 프로젝트를 준비하며 배운 것들은 확실히 남았다.</p>
<hr>
<h2 id="본선-질의응답에서-배울점">본선 질의응답에서 배울점</h2>
<h3 id="그래서-수질-자체와-녹조-발생의-상관관계는-어떻게-확인했나요">&quot;그래서 수질 자체와 녹조 발생의 상관관계는 어떻게 확인했나요?&quot;</h3>
<p>이 질문에는 제대로 답하지 못했다. 우리가 준비한 건 유해남조류 세포수를 타깃으로 놓고 기상·수문 조건(수온, 강수량, 유입량·방류량, 체류시간 proxy)과의 관계를 Risk Ratio와 Fisher&#39;s exact test로 검증한 가설들뿐이었다. 정작 심사위원이 물은 건 그 이전 단계, 즉 pH·DO·탁도·Chl-a 같은 수질 지표들 자체가 서로 어떻게 얽혀 있고, 그게 녹조 발생과 어떤 상관관계를 갖는지였다. 이 부분은 따로 분석해둔 자료가 없었다. 수질 항목 간의 관계를 정량적으로 파고든 근거도 준비되어 있지 않았다. 돌이켜보면 남조류 발생 메커니즘 공부에 밀려 정작 우리가 제공받은 수질 데이터 자체를 상관행렬이나 다중공선성 관점에서 제대로 들여다보지 않았던 게 원인이지 않을까 생각한다.</p>
<h3 id="회남·추동·문의는-위치도-특성도-다른데-그-차이를-모델이-정말-반영하고-있나요">&quot;회남·추동·문의는 위치도, 특성도 다른데, 그 차이를 모델이 정말 반영하고 있나요?&quot;</h3>
<p>이 질문에는 그나마 자신 있게 답할 수 있었다. 회남은 상류 유입부라 강수·유입량에 민감하고, 추동은 취수탑이 있는 정체 수역이라 수온·일조량에 민감하며, 문의는 댐 본체 근처라 방류량에 민감하다는 지점별 특성을 설명했다. 그리고 이걸 그냥 설명으로 끝낸 게 아니라 <code>loc_encoded</code>(지점 코드), <code>loc_flow_order</code>(흐름 순서), <code>hoenam_pressure_for_downstream</code>(회남 지점이 기준을 초과했을 때 하류 수역에 주는 압력 신호) 같은 파생 변수로 실제 모델에 반영했다는 것, 그리고 H3(수역 간 선행 신호) 가설이 Risk Ratio 7.16배로 가장 강하게 지지됐다는 결과까지 근거로 제시했다. 다만 심사위원 한 분이 &quot;그러면 회남에서 감지된 신호가 정확히 며칠 뒤에 추동·문의에 도달하는지 지연 시간까지 정량적으로 특정했느냐&quot;고 물었을 때는 ...할 말이 없었다. 우리는 &quot;선행 신호가 있었던 기간&quot;을 뭉뚱그려 정의했을 뿐, 회남에서 문의까지 물리적으로 며칠이 걸리는지 유속 기반으로 계산한 값은 없었다.</p>
<h3 id="지점이-세-곳인데-모델은-왜-하나인가요-시계열-특성이-지점마다-다를-텐데요">&quot;지점이 세 곳인데 모델은 왜 하나인가요? 시계열 특성이 지점마다 다를 텐데요.&quot;</h3>
<p>사실 초기 아이디어 회의 때는 회남·추동·문의를 완전히 독립된 세 개의 모델로 학습시키는 방안도 논의했었다. 지점마다 오염 발생 메커니즘 자체가 다르니, 하나의 모델에 지점을 욱여넣기보다 지점별로 가중치와 피처 중요도가 다른 세 개를 따로 학습시키는 게 이론적으로는 더 맞는 방향성이었다고 지금은 생각이든다. 그런데 실제로는 각 지점당 확보되는 채수 샘플 수가 전체 데이터를 3분의 1로 쪼개면 급격히 줄어드는 문제가 있었고, 결국 시간과 데이터 규모의 한계 속에서 <code>loc_encoded</code>와 지점 관련 파생 변수를 넣은 단일 통합 모델로 방향을 틀었다. 심사위원은 정확히 이 지점을 짚었다. &quot;그럼 세 지점을 하나로 합쳐서 학습시켰을 때, 각 지점 고유의 계절적 패턴이나 자기상관 구조가 서로 다른데 단일 모델이 그 차이를 온전히 학습했다고 확신할 수 있느냐&quot;는 질문이었다. 변명같지만.. 정확한 공공데이터가 없었는데.. </p>
<hr>
<h2 id="돌아보며">돌아보며</h2>
<blockquote>
<p>결과는 본선 진출, 최종 수상은 못 했지만 많은것을 얻게해준 공모전이였다.</p>
</blockquote>
<p>이번 대회를 준비하면서 가장 크게 느낀 건, <strong>좋은 AI 프로젝트는 모델 성능이 전부가 아니라는 것</strong>이다. 우리 팀이 가장 많은 시간을 쓴 곳은 사실 모델을 하이퍼파라미터 튜닝하는 것이 아니라, 조류경보제 운영 매뉴얼과 관련 법령을 조문 단위로 읽으며 도메인 지식을 쌓고, 남조류 생물학 논문을 찾아 메커니즘을 이해하고, 대청호가 다른 호수와 어떻게 다른지 비교 연구를 뒤지는 과정이었다. AI 모델은 이 모든 조사의 결과물을 검증하는 단계에 불과하다.</p>
<p>특히 가설을 세우고 통계적으로 검증하는 단계가 가장 값졌다. H4처럼 예상과 다르게 기각에 가까운 결과가 나왔을 때, 그걸 억지로 끼워 맞추지 않고 &quot;보조 신호&quot;로 정직하게 재정의한 게 오히려 신뢰도를 높이는 방향이었다고 생각한다. </p>
<p>열두 개의 지정과제 중에서 굳이 수자원공사의 조류경보 문제를 고른 것도 돌이켜보면 잘한 선택이었다. 수자원공사가 대청댐 수질·수문 데이터를 10개년치나 통째로 있어 데이터적인 면에서는 수훨했고 그걸가지고 &quot;왜 이 시기에 녹조가 늘어나는가&quot;로 바꾸는 건 논문을 찾고, 기상 데이터를 보태고, 법령을 뒤지는 우리 몫이었다. 대청호 하나를 정말 깊게 파고드는 과정에서 법령 조문, 정부 보도자료, 국내외 논문까지 — 리서치의 폭과 깊이 모두에서 이전에 해본 어떤 프로젝트보다 재밌게 공부한 느낌이든다.</p>
<p>수상까지 이어지지는 못했지만, 다애님과 현준님과 함께 프로젝트를 준비한 시간 자체가 너무너무 값진 경험이였다. 일주일에 2~3회씩 꾸준히 회의를 진행하고, 새벽까지 함께 개발하고 자신이 찾은분을 공유하고, 프로젝트 이야기는 물론 사소한 일상 이야기까지 나누면서 자연스럽게 팀워크를 쌓을 수 있었다. 공모전을 준비하는 과정에서 힘든 순간도 있었지만, 돌이켜보면 좋은 경험으로 남은 기억이 훨씬 많다.
이후에도 우리 춘배팀은 여기서 멈추지 않았다. 함께 제4회 문화체육관광 인공지능·데이터 활용 공모전 개발 부문에도 도전했고 오늘일자로 결과가 나왔다. 기대만큼 만족스럽지는 않았지만, 이를 계기로 앞으로도 9월에 있을 해커톤을 함께하기로 했다. 무엇보다 공모전의 성과를 떠나 서로 배우고 성장할 수 있는 좋은 사람들을 만났다는 점이 이번 경험에서 가장 큰 의미였다고 생각한다.</p>
<blockquote>
<h2 id="고생하셨습니다-다애현준">고생하셨습니다 @다애,현준</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/d33ef6e2-ac21-4cfd-baa4-50254958a605/image.png" alt=""></p>
</blockquote>
<hr>
<blockquote>
<h2 id="참고-자료">참고 자료</h2>
</blockquote>
<h3 id="프로젝트가-궁금하다면---github">프로젝트가 궁금하다면 -&gt; <a href="https://github.com/Chunbae-A/model">GitHub</a></h3>
<h3 id="법령·정책">법령·정책</h3>
<ul>
<li>「조류예측 및 수질관리협의회에 관한 규정」</li>
<li>「물환경보전법 시행령」</li>
<li>「정수장 조류 대응 가이드라인」(국립환경과학원, 2017)</li>
<li>「조류제거물질 사용지침」</li>
<li>기후에너지환경부, 「관계부처 협업으로 여름철 녹조발생에 체계적 대응」 보도자료</li>
<li>기후에너지환경부, 「2025년 여름철 녹조 대응」 보도자료 (야적퇴비 관리, AI 조류발생 예보 지점 확대 34→41개 등)</li>
<li>대한민국 정책브리핑, 「녹조 수질검사·정보공개 전면 개편」(2025)</li>
<li>금강청, 「대청호 문의 수역 조류경보 관심 단계 발령」 보도자료<h3 id="데이터">데이터</h3>
</li>
<li>한국수자원공사 대청댐 운영 수문정보, 수질·조류 관측자료</li>
<li>기상청 종관기상관측(ASOS) / 방재기상관측(AWS) Open API</li>
<li>공공데이터포털(data.go.kr)<h3 id="논문">논문</h3>
</li>
<li>Liu et al., 2011, <em>The effects of temperature and nutrient ratios on Microcystis blooms in Lake Taihu, China</em></li>
<li>De Nobel et al., 1997, <em>Competition for phosphorus between Anabaena and Aphanizomenon</em></li>
<li>Van Liere &amp; Mur, 1980, <em>Occurrence of Oscillatoria agardhii</em></li>
<li>Recknagel et al., 2025, <em>Early warning of harmful cyanobacteria blooms based on high frequency in situ monitoring and intelligible machine learning modelling: The case study of Lake Müggelsee (Germany)</em></li>
<li>「대청호 상류유역의 오염원 현황과 수질관리 방안에 관한 연구: 안남천과 안내천을 중심으로」</li>
<li>「소옥천 유역의 오염제어 대책에 따른 대청호 조류저감 효과 분석」</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 12주차 / AWS, Kubernetes]]></title>
            <link>https://velog.io/@mi_nini/KT-A-12%EC%A3%BC%EC%B0%A8-AWS-Kubernetes</link>
            <guid>https://velog.io/@mi_nini/KT-A-12%EC%A3%BC%EC%B0%A8-AWS-Kubernetes</guid>
            <pubDate>Mon, 29 Jun 2026 03:06:14 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>11주차는 동원예비군 훈련에 참여하면서 미니 프로젝트를 진행하지 못해 곧바로 12주차 학습을 진행하게 되었다. 원래는 이전에 프론트엔드로 구현했던 프로젝트를 Spring을 활용하여 데이터베이스와 연동하고, json-server 기반의 임시 백엔드를 실제 서버 환경으로 대체하는 내용을 다룰 예정이었다. 하지만 일정상 해당 프로젝트를 진행하지 못했기 때문에 이번 글에서는 프로젝트 대신 학습한 내용을 정리하고자 한다.
이번 글은 클라우드 환경에서 자주 사용되는 핵심 서비스와 k8s, 그리고 AWS 기반 CI/CD 파이프라인을 중심으로 정리한 학습 기록이다. </p>
<p><strong>다루는 범위:</strong></p>
<ul>
<li><strong>AWS 인프라</strong>: EC2, S3, EBS, VPC, ELB, Auto Scaling, Route53, CloudFront</li>
<li><strong>AWS 데이터베이스</strong>: RDS, Aurora, DynamoDB, ElastiCache</li>
<li><strong>AWS 보안·네트워크</strong>: IAM, Organizations, VPN, Direct Connect</li>
<li><strong>컨테이너</strong>: Docker, VM vs Container, MSA</li>
<li><strong>Kubernetes</strong>: 클러스터 구성, Pod/RS/Deployment/Service/Ingress/HPA, ConfigMap/Secret, Volume</li>
<li><strong>CI/CD on AWS</strong>: CodePipeline, CodeBuild, CodeDeploy, 배포 전략 7가지</li>
<li><strong>고가용성·DR</strong>: ELB+Auto Scaling, Route53 Failover, 재해복구 전략</li>
</ul>
<hr>
<h2 id="1-클라우드의-등장-배경과-aws-글로벌-인프라">1. 클라우드의 등장 배경과 AWS 글로벌 인프라</h2>
<h3 id="11-왜-클라우드가-필요했나">1.1 왜 클라우드가 필요했나?</h3>
<p>예전 On-Premise 방식을 생각해보자. 서비스를 운영하려면 서버를 직접 구매해야 했다. 서버는 최대 트래픽 기준으로 사야 했기 때문에, 평소에는 비싼 장비가 놀고 있었다. 반대로 예상치 못한 트래픽 폭증이 오면 서버가 터졌다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>On-Premise</th>
<th>Cloud</th>
</tr>
</thead>
<tbody><tr>
<td>서버 구매</td>
<td>직접 구매, 설치</td>
<td>필요할 때 빌림</td>
</tr>
<tr>
<td>초기 투자 비용</td>
<td>매우 높음</td>
<td>거의 없음</td>
</tr>
<tr>
<td>확장 속도</td>
<td>수 주 ~ 수 개월</td>
<td>수 분 이내</td>
</tr>
<tr>
<td>유지보수</td>
<td>직접 담당</td>
<td>CSP(클라우드 제공업체) 담당</td>
</tr>
<tr>
<td>요금 방식</td>
<td>선불 자본 지출</td>
<td>종량제 (쓴 만큼 지불)</td>
</tr>
</tbody></table>
<p>클라우드의 핵심 이점은 세 가지로 요약된다.</p>
<ul>
<li><strong>민첩성</strong>: 새 기능/서비스를 빠르게 출시할 수 있음</li>
<li><strong>비용 최적화</strong>: 사용한 만큼만 지불, 불필요한 서버 유지 비용 없음</li>
<li><strong>보안 취약성 최소화</strong>: AWS가 물리적 인프라 보안을 직접 담당</li>
</ul>
<h3 id="12-aws-vs-azure-비교">1.2 AWS vs Azure 비교</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>AWS (Amazon Web Services)</th>
<th>Microsoft Azure</th>
</tr>
</thead>
<tbody><tr>
<td>출시</td>
<td>2006년 (클라우드 시장 선두)</td>
<td>2010년 (후발주자, 빠른 성장)</td>
</tr>
<tr>
<td>시장 점유율</td>
<td>1위 (약 30% 이상)</td>
<td>2위 (약 25% 내외)</td>
</tr>
<tr>
<td>강점</td>
<td>광범위한 서비스, 글로벌 인프라</td>
<td>Microsoft 제품 통합 (Windows Server, AD, Office 365)</td>
</tr>
<tr>
<td>가상 머신</td>
<td>EC2</td>
<td>Azure Virtual Machine</td>
</tr>
<tr>
<td>스토리지</td>
<td>S3</td>
<td>Blob Storage</td>
</tr>
<tr>
<td>DB</td>
<td>RDS, DynamoDB</td>
<td>Azure SQL, Cosmos DB</td>
</tr>
<tr>
<td>AI/ML</td>
<td>SageMaker, Rekognition</td>
<td>Azure AI, Cognitive Services</td>
</tr>
<tr>
<td>주요 고객층</td>
<td>스타트업, 기술 기업, 글로벌 서비스</td>
<td>기존 Microsoft 고객, 공공기관, 대기업</td>
</tr>
</tbody></table>
<h3 id="13-aws-글로벌-인프라--리전-가용-영역-엣지">1.3 AWS 글로벌 인프라 — 리전, 가용 영역, 엣지</h3>
<p>AWS 글로벌 인프라는 리전 → 가용 영역 → 엣지 로케이션의 3단계 계층 구조로 구성된다.</p>
<pre><code>리전 (Region)
├── 가용 영역 AZ-1 (데이터 센터 × n)
├── 가용 영역 AZ-2 (데이터 센터 × n)
└── 가용 영역 AZ-3 (데이터 센터 × n)

엣지 로케이션 (전 세계 수백 곳)
Local Zone (대도시 근처 mini 리전)</code></pre><p><strong>리전(Region)</strong></p>
<ul>
<li>AWS가 전 세계에서 데이터 센터를 클러스터링하는 물리적 위치</li>
<li>각 리전은 완전히 독립적 — 서울 리전 장애가 도쿄 리전에 영향 없음</li>
<li>서울 리전 코드: <code>ap-northeast-2</code></li>
</ul>
<p>리전 선택 시 4가지 고려사항:</p>
<ol>
<li><strong>거버넌스</strong>: 금융, 의료 데이터는 국내 데이터센터에 보관해야 하는 법적 요건이 있음</li>
<li><strong>지연 시간</strong>: 사용자와 가까울수록 응답이 빠름</li>
<li><strong>서비스 가용성</strong>: 모든 AWS 서비스가 모든 리전에 출시되지 않음</li>
<li><strong>비용</strong>: 리전마다 EC2 요금이 다름</li>
</ol>
<p><strong>가용 영역(AZ, Availability Zone)</strong></p>
<ul>
<li>리전 내에서 서로 격리된 물리적 데이터 센터 그룹</li>
<li>화재, 정전, 자연재해로부터 독립 운영</li>
<li>AZ끼리는 고속 프라이빗 전용 링크로 연결됨</li>
<li>한 AZ가 다운돼도 다른 AZ가 서비스 지속 → <strong>고가용성(HA)</strong>의 핵심</li>
</ul>
<p><strong>엣지 로케이션</strong></p>
<ul>
<li>전 세계 주요 도시에 배치된 캐싱 거점 (200개 이상)</li>
<li>CloudFront(CDN), Route53에서 사용</li>
<li>사용자 가장 가까운 곳에서 콘텐츠를 서빙해 지연 시간을 최소화</li>
</ul>
<p><strong>Local Zone</strong></p>
<ul>
<li>리전을 사용자 근처로 확장한 소형 인프라</li>
<li>실시간 게임, 라이브 스트리밍, AR/VR 같은 지연 시간에 민감한 워크로드용</li>
</ul>
<hr>
<h2 id="2-클라우드-네이티브--devops와-cicd">2. 클라우드 네이티브 — DevOps와 CI/CD</h2>
<h3 id="21-cloud-native란">2.1 Cloud Native란?</h3>
<p>클라우드 네이티브는 단순히 &quot;클라우드에서 실행한다&quot;는 의미가 아니다. <strong>처음부터 클라우드 환경을 최대한 활용할 수 있게 설계·개발·운영하는 방법론</strong>이다.</p>
<p>기존 앱을 클라우드에 그냥 옮기는 건 <strong>Lift &amp; Shift</strong>(이전·재호스팅). 클라우드 네이티브의 핵심 요소 4가지:</p>
<table>
<thead>
<tr>
<th>요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>DevOps</strong></td>
<td>개발(Dev)과 운영(Ops)의 협업 문화</td>
</tr>
<tr>
<td><strong>Container</strong></td>
<td>이식 가능한 경량 실행 환경</td>
</tr>
<tr>
<td><strong>CI/CD</strong></td>
<td>빌드·테스트·배포 자동화 파이프라인</td>
</tr>
<tr>
<td><strong>Micro Services (MSA)</strong></td>
<td>기능별로 분리된 독립 서비스 구조</td>
</tr>
</tbody></table>
<h3 id="22-devops--개발팀과-운영팀의-벽을-허물다">2.2 DevOps — 개발팀과 운영팀의 벽을 허물다</h3>
<p>DevOps 이전의 전통적인 개발 환경에는 구조적인 충돌이 있었다.</p>
<table>
<thead>
<tr>
<th>관점</th>
<th>목표</th>
<th>갈등 요인</th>
</tr>
</thead>
<tbody><tr>
<td>개발팀(Dev)</td>
<td>기능을 빠르게 배포</td>
<td>변경이 많을수록 좋음</td>
</tr>
<tr>
<td>운영팀(Ops)</td>
<td>시스템 안정성 유지</td>
<td>변경이 적을수록 좋음</td>
</tr>
</tbody></table>
<p>이 구조적 갈등으로 배포 주기가 길어지고, 장애 발생 시 책임 소재가 불분명해진다. DevOps는 <strong>자동화와 협업 문화</strong>를 통해 이 문제를 해결한다.</p>
<h3 id="23-cicd--자동화된-배포-파이프라인">2.3 CI/CD — 자동화된 배포 파이프라인</h3>
<pre><code>Code → Build → Test → Release → Deploy → Monitor
  ↑                                             |
  └──────────── 피드백 루프 ────────────────────┘</code></pre><p><strong>CI (Continuous Integration, 지속적 통합)</strong></p>
<ul>
<li>코드 변경 사항을 주기적으로(매일 또는 여러 번) 빈번하게 merge</li>
<li>merge 시 자동으로 빌드와 테스트 실행</li>
<li>통합 문제를 조기에 발견</li>
</ul>
<p><strong>CD (Continuous Delivery/Deployment, 지속적 배포)</strong></p>
<ul>
<li>테스트를 통과한 코드를 자동으로 스테이징 또는 운영 환경에 배포</li>
<li>Continuous Delivery: 운영 배포 전 수동 승인 포함</li>
<li>Continuous Deployment: 완전 자동화, 승인 없이 즉시 배포</li>
</ul>
<p>CI/CD 도구 예시: Jenkins, GitLab CI/CD, GitHub Actions, AWS CodePipeline, ArgoCD</p>
<p>CI/CD 도입 효과: 배포 주기 단축, 버그 조기 발견, 팀 생산성 향상, 릴리즈 위험 감소</p>
<hr>
<h2 id="3-클라우드-서비스-모델--iaas-paas-saas">3. 클라우드 서비스 모델 — IaaS, PaaS, SaaS</h2>
<pre><code>온프레미스 (모두 직접 관리)
  Networking / Storage / Server / OS / Middleware / Runtime / Application

IaaS (OS부터 직접 관리)
  CSP 제공: Networking / Storage / Server
  직접 관리: OS / Middleware / Runtime / Application

PaaS (애플리케이션만 관리)
  CSP 제공: Networking / Storage / Server / OS / Middleware / Runtime
  직접 관리: Application

SaaS (관리 없음, 사용만)
  CSP 제공: 전부</code></pre><table>
<thead>
<tr>
<th>모델</th>
<th>AWS 대표 서비스</th>
<th>사용 예시</th>
</tr>
</thead>
<tbody><tr>
<td>IaaS</td>
<td>EC2, VPC, S3</td>
<td>가상 서버, 네트워크 직접 구성</td>
</tr>
<tr>
<td>PaaS</td>
<td>Elastic Beanstalk, RDS</td>
<td>코드만 올리면 자동 배포</td>
</tr>
<tr>
<td>SaaS</td>
<td>Amazon Chime, WorkDocs</td>
<td>그냥 쓰면 됨</td>
</tr>
</tbody></table>
<p><strong>AWS 컴퓨팅 서비스 출시 연표</strong>:</p>
<ul>
<li>2006년: EC2 — 가상화 기반 IaaS (AWS 최초 서비스)</li>
<li>2014년: ECS — 컨테이너 오케스트레이션 (re:Invent 2014)</li>
<li>2014년: Lambda — 서버리스 Function-as-a-Service (re:Invent 2014)</li>
<li>2016년: Lightsail — 단순화된 VPS 서비스</li>
<li>2017년: Fargate — 서버리스 컨테이너 실행 (re:Invent 2017)</li>
<li>2018년: EKS — 관리형 Kubernetes (re:Invent 2017 발표, 2018 GA)</li>
</ul>
<hr>
<h2 id="4-aws-ec2--클라우드의-가상-서버">4. AWS EC2 — 클라우드의 가상 서버</h2>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20250529181702162572/Aws-EC2-instance-types.webp" alt="AWS EC2 Instance Types — 각 패밀리와 용도 분류"></p>
<h3 id="41-ec2란">4.1 EC2란?</h3>
<p>EC2(Elastic Compute Cloud)는 AWS의 핵심 컴퓨팅 서비스다. 클라우드에서 안전하고 크기 조정 가능한 컴퓨팅 용량을 제공한다. 트래픽 변화에 따라 수초 만에 인스턴스를 추가하거나 제거할 수 있다.</p>
<h3 id="42-ec2-인스턴스-생성-시-결정사항">4.2 EC2 인스턴스 생성 시 결정사항</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>이름 및 태그</strong></td>
<td>리소스 식별·관리용 태그 부여</td>
<td><code>Name: web-server-01</code>, <code>Env: production</code></td>
</tr>
<tr>
<td><strong>AMI</strong></td>
<td>인스턴스의 OS 이미지 템플릿</td>
<td>Ubuntu 24.04 LTS, Amazon Linux 2023</td>
</tr>
<tr>
<td><strong>인스턴스 유형</strong></td>
<td>CPU/메모리 조합</td>
<td><code>t3.medium</code> (2vCPU, 4GB)</td>
</tr>
<tr>
<td><strong>키 페어</strong></td>
<td>SSH 접속을 위한 공개키/프라이빗키</td>
<td><code>user-key.pem</code></td>
</tr>
<tr>
<td><strong>네트워크/보안</strong></td>
<td>VPC, 서브넷, 보안 그룹 설정</td>
<td>default VPC, port 22/80/443 허용</td>
</tr>
<tr>
<td><strong>스토리지</strong></td>
<td>EBS 볼륨 크기와 타입</td>
<td>20GB gp3</td>
</tr>
<tr>
<td><strong>사용자 데이터</strong></td>
<td>인스턴스 최초 시작 시 실행할 스크립트</td>
<td>Nginx 자동 설치</td>
</tr>
</tbody></table>
<h3 id="43-ami-amazon-machine-image">4.3 AMI (Amazon Machine Image)</h3>
<p>AMI는 EC2 인스턴스의 템플릿이다. 한 번 설정한 환경을 AMI로 저장해두면, 동일한 환경을 여러 인스턴스로 즉시 복제할 수 있다.</p>
<p>AMI 구성요소:</p>
<ul>
<li><strong>루트 볼륨 템플릿</strong>: OS, 앱 서버, 애플리케이션</li>
<li><strong>시작 권한</strong>: 어느 AWS 계정이 이 AMI로 인스턴스를 시작할 수 있는지</li>
<li><strong>블록 디바이스 매핑</strong>: 인스턴스 시작 시 어떤 볼륨을 연결할지</li>
</ul>
<p>AMI를 얻는 방법:</p>
<ol>
<li><strong>AWS 제공 AMI</strong>: Ubuntu, Amazon Linux, Windows Server 등 공식 이미지</li>
<li><strong>AWS Marketplace</strong>: 상용 소프트웨어가 사전 설치된 이미지</li>
<li><strong>직접 생성</strong>: 기존 인스턴스에서 커스텀 AMI 생성</li>
</ol>
<p>AMI의 장점: <strong>반복 가능(Repeatable)</strong>, <strong>재사용 가능(Reusable)</strong>, <strong>복구 가능(Recoverable)</strong></p>
<h3 id="44-인스턴스-유형-이름-읽기">4.4 인스턴스 유형 이름 읽기</h3>
<p>인스턴스 유형 이름은 규칙이 있다.</p>
<pre><code>c  7  g  n  .  2xlarge
│  │  │  │     └─── 인스턴스 크기
│  │  │  └──────── 옵션 (n = 네트워크 최적화)
│  │  └─────────── 프로세서 (g = AWS Graviton ARM)
│  └────────────── 세대 (숫자 높을수록 최신)
└───────────────── 인스턴스 패밀리</code></pre><p><strong>인스턴스 패밀리 상세</strong>:</p>
<table>
<thead>
<tr>
<th>패밀리</th>
<th>특징</th>
<th>사용 케이스</th>
</tr>
</thead>
<tbody><tr>
<td><strong>T (버스트 가능 범용)</strong></td>
<td>평소 낮은 CPU, 필요 시 버스트</td>
<td>개발/테스트, 소규모 웹서버</td>
</tr>
<tr>
<td><strong>M (범용)</strong></td>
<td>CPU:메모리 = 1:4 균형</td>
<td>웹/앱 서버, 중간 규모 DB</td>
</tr>
<tr>
<td><strong>C (컴퓨팅 최적화)</strong></td>
<td>CPU:메모리 = 1:2</td>
<td>배치 처리, HPC, 게임 서버, ML 추론</td>
</tr>
<tr>
<td><strong>R (메모리 최적화)</strong></td>
<td>CPU:메모리 = 1:8 이상</td>
<td>대용량 인메모리 DB, 실시간 빅데이터</td>
</tr>
<tr>
<td><strong>I (스토리지 최적화)</strong></td>
<td>고성능 NVMe SSD</td>
<td>NoSQL, 데이터웨어하우스</td>
</tr>
<tr>
<td><strong>P/G (가속 컴퓨팅)</strong></td>
<td>GPU 탑재</td>
<td>ML 훈련, 영상 렌더링, 과학 계산</td>
</tr>
</tbody></table>
<p>최신 세대 인스턴스를 쓰는 게 유리하다 — 이전 세대 대비 성능 향상, 비용 절감, 더 나은 확장성.</p>
<h3 id="45-테넌시와-배치-그룹">4.5 테넌시와 배치 그룹</h3>
<p><strong>테넌시(Tenancy)</strong> — EC2가 어느 물리 서버에서 실행되는지 결정</p>
<table>
<thead>
<tr>
<th>테넌시</th>
<th>설명</th>
<th>비용</th>
</tr>
</thead>
<tbody><tr>
<td>공유 테넌시</td>
<td>다른 고객과 하드웨어 공유</td>
<td>가장 저렴</td>
</tr>
<tr>
<td>전용 인스턴스</td>
<td>전용 하드웨어, 하지만 AWS가 배치</td>
<td>비쌈</td>
</tr>
<tr>
<td>전용 호스트</td>
<td>특정 물리 서버를 통째로 점유, 소켓 단위 제어</td>
<td>가장 비쌈</td>
</tr>
</tbody></table>
<p><strong>배치 그룹(Placement Group)</strong></p>
<table>
<thead>
<tr>
<th>유형</th>
<th>배치 방식</th>
<th>사용 케이스</th>
</tr>
</thead>
<tbody><tr>
<td>클러스터</td>
<td>서로 인접하게</td>
<td>HPC — 낮은 지연시간, 높은 처리량</td>
</tr>
<tr>
<td>분산형</td>
<td>여러 랙에 분산</td>
<td>의료 기록 시스템 — 내결함성</td>
</tr>
<tr>
<td>파티션</td>
<td>논리 그룹으로 분할</td>
<td>Kafka, Hadoop — 대규모 분산 워크로드</td>
</tr>
</tbody></table>
<h3 id="46-사용자-데이터-user-data">4.6 사용자 데이터 (User Data)</h3>
<p>인스턴스가 최초로 시작될 때 루트 권한으로 자동 실행되는 스크립트다. Kubernetes 설치, 웹서버 초기화 등에 활용한다.</p>
<pre><code class="language-bash">#!/bin/bash
# Ubuntu 예시: 인스턴스 시작 시 Nginx 자동 설치
apt update -y
apt install -y nginx
systemctl start nginx
systemctl enable nginx</code></pre>
<h3 id="47-ec2-구매-옵션--비용-최적화">4.7 EC2 구매 옵션 — 비용 최적화</h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>할인율</th>
<th>약정</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>온디맨드</strong></td>
<td>기준가</td>
<td>없음</td>
<td>언제든 시작/종료, 단기 임시 워크로드</td>
</tr>
<tr>
<td><strong>Compute Savings Plans</strong></td>
<td>최대 66%</td>
<td>1/3년</td>
<td>EC2, Fargate, Lambda에 유연 적용</td>
</tr>
<tr>
<td><strong>EC2 Instance Savings Plans</strong></td>
<td>최대 72%</td>
<td>1/3년</td>
<td>특정 리전·패밀리 고정, 가장 저렴</td>
</tr>
<tr>
<td><strong>스팟 인스턴스</strong></td>
<td>최대 90%</td>
<td>없음</td>
<td>AWS 여유 용량 사용, 중단 가능성 있음</td>
</tr>
</tbody></table>
<p>실전 조합 전략: <strong>온디맨드(베이스) + Savings Plans(예측 가능한 부분) + Spot(내결함성 워크로드)</strong></p>
<h3 id="48-서버리스-컴퓨팅--aws-lambda">4.8 서버리스 컴퓨팅 — AWS Lambda</h3>
<p>서버를 프로비저닝·관리할 필요 없이 코드만 올리면 이벤트 발생 시 자동 실행된다.</p>
<pre><code>이벤트 소스 (S3 업로드, API Gateway 요청, DynamoDB 변경 등)
    ↓
Lambda 함수 실행 (코드)
    ↓ (선택적)
응답 반환</code></pre><p><strong>Lambda 특징</strong>:</p>
<ul>
<li>지원 런타임: Python, Node.js, Java, Go, Ruby, C# 등</li>
<li>최대 실행 시간: <strong>15분</strong></li>
<li>최대 메모리: <strong>10GB</strong></li>
<li>요금: 요청 수 + 실행 시간 기준 (밀리초 단위)</li>
</ul>
<p><strong>Lambda 활용 예시</strong>:</p>
<ul>
<li>API Gateway + Lambda = 서버리스 REST API</li>
<li>S3 업로드 이벤트 + Lambda = 이미지 리사이징 자동화</li>
<li>CloudWatch 스케줄 + Lambda = 주기적 배치 작업</li>
</ul>
<hr>
<h2 id="5-amazon-s3--객체-스토리지">5. Amazon S3 — 객체 스토리지</h2>
<h3 id="51-s3-개념과-구조">5.1 S3 개념과 구조</h3>
<p>Amazon S3(Simple Storage Service)는 <strong>무제한 용량의 객체 스토리지 서비스</strong>다. 파일 시스템이 아닌 Key-Value 방식으로 데이터를 저장한다.</p>
<pre><code>버킷(Bucket): 최상위 컨테이너. 이름은 전 세계에서 유일해야 함.
└── 객체(Object): 실제 데이터
    ├── Key   : 객체의 전체 경로 (예: images/2024/photo.jpg)
    ├── Value : 파일 데이터 (최대 5TB)
    └── Metadata: Content-Type, ETag, 사용자 정의 태그 등</code></pre><p>S3 주요 특성:</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>내구성</strong></td>
<td>99.999999999% (11 nines) — 데이터 자동 3중 복제</td>
</tr>
<tr>
<td><strong>가용성</strong></td>
<td>Standard 기준 99.99% SLA</td>
</tr>
<tr>
<td><strong>용량</strong></td>
<td>무제한 (단일 객체 최대 5TB)</td>
</tr>
<tr>
<td><strong>버킷 이름</strong></td>
<td>전 세계 고유, 소문자·숫자·하이픈만 허용</td>
</tr>
<tr>
<td><strong>범위</strong></td>
<td>특정 리전에 버킷 생성, 리전 내 여러 AZ에 자동 복제</td>
</tr>
</tbody></table>
<p>S3 URL 형식:</p>
<pre><code>https://버킷이름.s3.리전코드.amazonaws.com/키이름
https://my-app.s3.ap-northeast-2.amazonaws.com/images/profile.jpg</code></pre><h3 id="52-s3-스토리지-클래스">5.2 S3 스토리지 클래스</h3>
<p>데이터 접근 빈도에 따라 최적의 클래스를 선택해 비용을 절감한다.</p>
<table>
<thead>
<tr>
<th>클래스</th>
<th>가용성</th>
<th>최소 보존</th>
<th>복구 시간</th>
<th>사용 케이스</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Standard</strong></td>
<td>99.99%</td>
<td>없음</td>
<td>즉시</td>
<td>자주 접근 (웹 서빙, 분석)</td>
</tr>
<tr>
<td><strong>Intelligent-Tiering</strong></td>
<td>99.9%</td>
<td>없음</td>
<td>즉시</td>
<td>접근 패턴 불규칙</td>
</tr>
<tr>
<td><strong>Standard-IA</strong></td>
<td>99.9%</td>
<td>30일</td>
<td>즉시</td>
<td>월 1회 미만 접근, DR 백업</td>
</tr>
<tr>
<td><strong>One Zone-IA</strong></td>
<td>99.5%</td>
<td>30일</td>
<td>즉시</td>
<td>재생성 가능한 데이터, 저비용</td>
</tr>
<tr>
<td><strong>Glacier Instant Retrieval</strong></td>
<td>99.9%</td>
<td>90일</td>
<td>밀리초</td>
<td>분기별 접근</td>
</tr>
<tr>
<td><strong>Glacier Flexible Retrieval</strong></td>
<td>99.99%</td>
<td>90일</td>
<td>1분~12시간</td>
<td>연간 접근, 아카이브</td>
</tr>
<tr>
<td><strong>Glacier Deep Archive</strong></td>
<td>99.99%</td>
<td>180일</td>
<td>12~48시간</td>
<td>7~10년 장기 보관, 규정 준수</td>
</tr>
</tbody></table>
<h3 id="53-s3-버전-관리-versioning">5.3 S3 버전 관리 (Versioning)</h3>
<p>버전 관리 활성화 시 같은 Key에 파일을 덮어써도 이전 버전이 모두 보존된다.</p>
<pre><code>버전 관리 비활성: 업로드 → 덮어쓰기 → 이전 파일 영구 삭제

버전 관리 활성:
  photo.jpg 업로드   → ID: ver-001
  photo.jpg 재업로드 → ID: ver-002  (ver-001 유지)
  삭제 요청          → 삭제 마커(Delete Marker) 추가, 실제 파일 보존
  특정 버전 삭제     → 해당 ID만 삭제, 나머지 유지</code></pre><ul>
<li>CodePipeline S3 소스 트리거에 버전 관리 활성화 <strong>필수</strong></li>
<li>각 버전마다 별도 과금</li>
</ul>
<h3 id="54-수명-주기-정책-lifecycle-policy">5.4 수명 주기 정책 (Lifecycle Policy)</h3>
<p>시간 경과에 따라 데이터를 자동으로 더 저렴한 클래스로 전환하거나 삭제한다.</p>
<pre><code>업로드 → S3 Standard
    ↓ 30일 후 자동 전환
S3 Standard-IA
    ↓ 60일 후 자동 전환
S3 Glacier Flexible Retrieval
    ↓ 270일 후
자동 삭제</code></pre><h3 id="55-주요-기능">5.5 주요 기능</h3>
<p><strong>정적 웹사이트 호스팅</strong>: HTML, CSS, JS 파일을 S3에서 직접 서빙. 별도 웹 서버 불필요.</p>
<p><strong>Presigned URL</strong>: 프라이빗 버킷 객체에 대한 시간 제한 접근 URL 생성.</p>
<pre><code class="language-python">import boto3
s3 = boto3.client(&#39;s3&#39;)
url = s3.generate_presigned_url(
    &#39;get_object&#39;,
    Params={&#39;Bucket&#39;: &#39;my-bucket&#39;, &#39;Key&#39;: &#39;report.pdf&#39;},
    ExpiresIn=3600  # 1시간 유효
)</code></pre>
<p><strong>S3 Transfer Acceleration</strong>: CloudFront 엣지 로케이션 경유로 업로드 속도 최적화. 지리적으로 먼 클라이언트에서 대용량 업로드 시 유용.</p>
<p><strong>교차 리전 복제 (CRR)</strong>: 다른 리전 버킷에 객체를 자동 복제. DR, 지연 시간 감소, 데이터 주권 준수 목적.</p>
<hr>
<h2 id="6-amazon-ebs--ec2-블록-스토리지">6. Amazon EBS — EC2 블록 스토리지</h2>
<h3 id="61-ebs-개념">6.1 EBS 개념</h3>
<p>EBS(Elastic Block Store)는 EC2 인스턴스에 연결하는 <strong>네트워크 기반 블록 스토리지</strong>다.</p>
<pre><code>EC2 인스턴스 ←── 네트워크 ──→ EBS 볼륨 (독립적으로 존재)</code></pre><p>EBS 특성:</p>
<ul>
<li><strong>독립적 생존</strong>: EC2가 종료돼도 볼륨은 유지됨 (Delete on Termination 설정에 따라)</li>
<li><strong>AZ 범위</strong>: 동일 AZ 내 EC2에만 연결 가능</li>
<li><strong>스냅샷</strong>: S3에 증분 백업. 다른 AZ/리전으로 복사해 활용 가능</li>
<li><strong>기본 연결</strong>: 하나의 EBS는 하나의 EC2에만 연결 (io1/io2 Multi-Attach는 예외)</li>
</ul>
<h3 id="62-ebs-볼륨-타입">6.2 EBS 볼륨 타입</h3>
<table>
<thead>
<tr>
<th>타입</th>
<th>분류</th>
<th>최대 IOPS</th>
<th>최대 처리량</th>
<th>특징 및 사용 케이스</th>
</tr>
</thead>
<tbody><tr>
<td><strong>gp3</strong></td>
<td>SSD 범용</td>
<td>16,000</td>
<td>1,000 MB/s</td>
<td><strong>신규 기본값</strong>. 용량과 IOPS 독립 설정 가능</td>
</tr>
<tr>
<td><strong>gp2</strong></td>
<td>SSD 범용</td>
<td>16,000</td>
<td>250 MB/s</td>
<td>구형. IOPS = 용량(GB) × 3 자동 결정 (gp3 권장)</td>
</tr>
<tr>
<td><strong>io2 Block Express</strong></td>
<td>프로비저닝 IOPS SSD</td>
<td>256,000</td>
<td>4,000 MB/s</td>
<td>최고 성능 DB (SAP HANA, Oracle RAC)</td>
</tr>
<tr>
<td><strong>io1</strong></td>
<td>프로비저닝 IOPS SSD</td>
<td>64,000</td>
<td>1,000 MB/s</td>
<td>고성능 DB, 지연 시간 민감 워크로드</td>
</tr>
<tr>
<td><strong>st1</strong></td>
<td>처리량 최적화 HDD</td>
<td>500</td>
<td>500 MB/s</td>
<td>Hadoop, Kafka, 로그 처리 (순차 읽기/쓰기)</td>
</tr>
<tr>
<td><strong>sc1</strong></td>
<td>콜드 HDD</td>
<td>250</td>
<td>250 MB/s</td>
<td>접근 빈도 낮은 아카이브 데이터 (최저 비용)</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>gp3 vs gp2</strong>: gp3는 용량과 IOPS/처리량을 각각 독립적으로 설정 가능. 동일 성능 대비 gp3가 최대 20% 저렴하며 AWS 권장 타입.</p>
</blockquote>
<h3 id="63-ebs-스냅샷">6.3 EBS 스냅샷</h3>
<p>스냅샷은 EBS 볼륨의 특정 시점 백업. S3에 저장되며 증분(Incremental) 방식으로 변경된 블록만 저장한다.</p>
<pre><code>첫 번째 스냅샷: 전체 볼륨 저장
두 번째 스냅샷: 첫 번째 이후 변경 블록만 저장
세 번째 스냅샷: 두 번째 이후 변경 블록만 저장
(각 스냅샷은 독립적으로 복원 가능)</code></pre><p>활용:</p>
<ul>
<li>볼륨을 다른 AZ로 이동: 스냅샷 생성 → 대상 AZ에서 볼륨 복원</li>
<li>다른 리전 복사: 스냅샷 복사(Copy) → 복사된 스냅샷으로 볼륨 생성</li>
<li>AMI 생성의 기반: EC2 인스턴스 상태를 AMI로 저장 시 EBS 스냅샷이 함께 생성</li>
</ul>
<hr>
<h2 id="7-aws-데이터베이스-서비스">7. AWS 데이터베이스 서비스</h2>
<h3 id="71-관계형-vs-비관계형--어떤-db를-선택할까">7.1 관계형 vs 비관계형 — 어떤 DB를 선택할까?</h3>
<table>
<thead>
<tr>
<th>특성</th>
<th>관계형(SQL)</th>
<th>비관계형(NoSQL)</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 구조</td>
<td>행과 열로 이뤄진 테이블</td>
<td>키-값, 문서, 와이드 컬럼, 그래프</td>
</tr>
<tr>
<td>스키마</td>
<td>고정됨 (변경이 어려움)</td>
<td>동적 (자유롭게 변경 가능)</td>
</tr>
<tr>
<td>확장 방식</td>
<td>수직 확장 (서버 업그레이드)</td>
<td>수평 확장 (서버 추가)</td>
</tr>
<tr>
<td>일관성</td>
<td>강력한 일관성 (ACID)</td>
<td>최종적 일관성 (BASE) 지원</td>
</tr>
<tr>
<td>AWS 서비스</td>
<td>RDS, Aurora</td>
<td>DynamoDB, ElastiCache</td>
</tr>
<tr>
<td>선택 기준</td>
<td>엄격한 스키마, 트랜잭션 필요</td>
<td>유연한 데이터, 초고속 읽기/쓰기 필요</td>
</tr>
</tbody></table>
<h3 id="72-amazon-rds--관리형-관계형-데이터베이스">7.2 Amazon RDS — 관리형 관계형 데이터베이스</h3>
<p>RDS는 MySQL, PostgreSQL, Oracle, SQL Server, MariaDB, IBM Db2를 관리형으로 제공한다. 설치·패치·백업·복제·모니터링을 AWS가 담당한다.</p>
<p><strong>관리형 서비스의 장점</strong>:</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>EC2에 직접 설치</th>
<th>Amazon RDS</th>
</tr>
</thead>
<tbody><tr>
<td>OS 설치/패치</td>
<td>직접</td>
<td>AWS</td>
</tr>
<tr>
<td>DB 소프트웨어 패치</td>
<td>직접</td>
<td>AWS</td>
</tr>
<tr>
<td>백업</td>
<td>직접</td>
<td>AWS (자동)</td>
</tr>
<tr>
<td>고가용성 구성</td>
<td>직접</td>
<td>Multi-AZ 클릭 한 번</td>
</tr>
<tr>
<td>스케일링</td>
<td>직접</td>
<td>콘솔에서 변경</td>
</tr>
</tbody></table>
<p><strong>Multi-AZ 배포 — 고가용성</strong></p>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20231209123630/Blank-diagram.jpeg" alt="RDS Multi-AZ and Read Replica diagram"></p>
<pre><code>가용 영역 1                가용 영역 2
┌──────────────────┐      ┌──────────────────┐
│  프라이머리 DB   │      │  대기 DB (Standby) │
│  (읽기/쓰기)     │←───→│  (평상시 미사용)   │
└──────────────────┘  동기식   └──────────────────┘
       ↑                복제
    장애 발생
       ↓
  대기 DB 자동 승격
  (수분 내 복구)</code></pre><ul>
<li>프라이머리 DB에서 대기 DB로 <strong>동기식 복제</strong></li>
<li>프라이머리 장애 시 → 대기 DB가 자동으로 프라이머리로 승격</li>
<li>애플리케이션은 같은 DNS 엔드포인트를 계속 사용 → 코드 변경 불필요</li>
<li>대기 DB는 읽기에 사용되지 않음 (순수 대기 목적)</li>
</ul>
<p><strong>읽기 전용 복제본 — 읽기 성능 향상</strong></p>
<pre><code>프라이머리 DB
(읽기/쓰기)
     │
     └── 비동기 복제
          ↓
  읽기 전용 복제본 #1  ← 리포트, 통계 쿼리
  읽기 전용 복제본 #2  ← 분석 작업
  읽기 전용 복제본 #3  ← 다른 리전 (교차 리전 복제)</code></pre><ul>
<li>RDS당 최대 <strong>5개</strong>, Aurora는 최대 <strong>15개</strong> 읽기 전용 복제본</li>
<li>비동기 복제 — 프라이머리와 약간의 지연(lag) 존재</li>
<li>Multi-AZ + 읽기 복제본을 동시에 사용 가능</li>
</ul>
<h3 id="73-amazon-aurora--클라우드-전용-고성능-db">7.3 Amazon Aurora — 클라우드 전용 고성능 DB</h3>
<p>Aurora는 AWS가 처음부터 클라우드용으로 설계한 관계형 DB다. MySQL과 PostgreSQL과 호환된다.</p>
<p><strong>Aurora가 특별한 이유</strong>:</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>MySQL on RDS</th>
<th>Aurora</th>
</tr>
</thead>
<tbody><tr>
<td>성능</td>
<td>기준</td>
<td>MySQL 대비 최대 5배</td>
</tr>
<tr>
<td>데이터 복사본</td>
<td>2개 (Multi-AZ)</td>
<td>3개 AZ × 2 = <strong>6개 복사본</strong></td>
</tr>
<tr>
<td>읽기 복제본</td>
<td>최대 5개</td>
<td>최대 15개</td>
</tr>
<tr>
<td>스토리지</td>
<td>수동 확장</td>
<td><strong>자동 확장</strong> (최대 128TB)</td>
</tr>
</tbody></table>
<p>Aurora DB 클러스터 구조:</p>
<pre><code>클러스터 엔드포인트 (읽기/쓰기)
    │
프라이머리 인스턴스 (읽기/쓰기)
    │
    ├── Aurora 복제본 1 (읽기 전용, AZ-1)
    ├── Aurora 복제본 2 (읽기 전용, AZ-2)
    └── Aurora 복제본 3 (읽기 전용, AZ-3)

클러스터 볼륨 (3개 AZ × 2 = 6개 복사본, 자동 복구)</code></pre><h3 id="74-amazon-dynamodb--완전-관리형-nosql">7.4 Amazon DynamoDB — 완전 관리형 NoSQL</h3>
<p>서버 관리가 전혀 필요 없는 완전 관리형 키-값/문서 NoSQL 데이터베이스다.</p>
<p><strong>DynamoDB 기본 구조</strong>:</p>
<pre><code>테이블: Gamers
┌──────────────┬──────────────┬───────┬────────┬──────────┐
│  GamerTag    │  GameID      │ Level │ Score  │ TopScore │
│ (파티션 키)  │ (정렬 키)   │       │        │          │
├──────────────┼──────────────┼───────┼────────┼──────────┤
│ &quot;Hammer57&quot;   │ &quot;PuzzleGame&quot; │  21   │  4050  │  483610  │
│ &quot;FluffyDuffy&quot;│ &quot;MysteryGame&quot;│   5   │  1123  │   10863  │
└──────────────┴──────────────┴───────┴────────┴──────────┘</code></pre><ul>
<li><strong>파티션 키(Partition Key)</strong>: 데이터 분산의 기준. 고르게 분산될수록 성능이 좋음</li>
<li><strong>정렬 키(Sort Key)</strong>: 선택적. 파티션 키 + 정렬 키 = 복합 기본 키</li>
<li><strong>속성</strong>: 나머지 데이터. 행마다 다른 속성을 가질 수 있음 (스키마 유연성)</li>
</ul>
<p><strong>용량 모드</strong>:</p>
<table>
<thead>
<tr>
<th>모드</th>
<th>설명</th>
<th>사용 시점</th>
</tr>
</thead>
<tbody><tr>
<td>온디맨드</td>
<td>요청 당 과금</td>
<td>트래픽 예측 불가, 스파이크 워크로드</td>
</tr>
<tr>
<td>프로비저닝</td>
<td>RCU/WCU 사전 설정</td>
<td>트래픽 예측 가능, 비용 최적화</td>
</tr>
</tbody></table>
<ul>
<li><strong>RCU (읽기 용량 단위)</strong>: 최대 4KB 항목 1회 읽기</li>
<li><strong>WCU (쓰기 용량 단위)</strong>: 최대 1KB 항목 1회 쓰기</li>
</ul>
<p><strong>읽기 일관성</strong>:</p>
<table>
<thead>
<tr>
<th>유형</th>
<th>RCU 소비</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>강력한 읽기 일관성</td>
<td>1 RCU</td>
<td>가장 최신 데이터 보장</td>
</tr>
<tr>
<td>최종적 읽기 일관성</td>
<td>0.5 RCU</td>
<td>약간의 지연 가능, 비용 절감</td>
</tr>
</tbody></table>
<h3 id="75-캐싱-전략--elasticache">7.5 캐싱 전략 — ElastiCache</h3>
<p>DB에 반복적으로 같은 쿼리를 날리는 건 낭비다. <strong>자주 읽히는 데이터를 메모리에 캐싱</strong>해두면 응답 시간을 대폭 단축할 수 있다.</p>
<p>ElastiCache는 Redis와 Memcached를 관리형으로 제공한다.</p>
<p><strong>어떤 데이터를 캐싱해야 할까?</strong></p>
<ul>
<li>쿼리가 느리고 비용이 많이 드는 데이터</li>
<li>자주 읽히지만 자주 바뀌지 않는 데이터 (상품 목록, 사용자 프로필 등)</li>
</ul>
<p><strong>캐싱 전략 1 — Lazy Loading (지연 로딩)</strong></p>
<pre><code>1. 요청 → 캐시 확인
2. 캐시 HIT → 즉시 반환 (빠름)
   캐시 MISS → DB 조회 → 캐시에 저장 → 반환 (느림, but 다음엔 빠름)</code></pre><p>장점: 캐시에는 실제로 요청된 데이터만 저장됨
단점: 첫 번째 요청은 항상 느림 (Cache Miss)</p>
<p><strong>캐싱 전략 2 — Write-Through (쓰기 시 동기화)</strong></p>
<pre><code>쓰기 요청
    → DB에 저장
    → 캐시에도 즉시 저장</code></pre><p>장점: 캐시 데이터가 항상 최신
단점: 모든 쓰기 작업에서 캐시도 업데이트 (오버헤드)</p>
<p><strong>캐시 만료 — TTL (Time To Live)</strong></p>
<pre><code class="language-python"># Redis 예시: 1시간 후 자동 만료
cache.set(&quot;user:123&quot;, user_data, ex=3600)</code></pre>
<p>캐시 메모리가 가득 차면 <strong>제거 정책(Eviction Policy)</strong> 적용:</p>
<ul>
<li><strong>LRU (Least Recently Used)</strong>: 가장 오래 사용되지 않은 데이터 제거</li>
<li><strong>LFU (Least Frequently Used)</strong>: 사용 빈도가 가장 낮은 데이터 제거</li>
<li><strong>TTL 기반</strong>: TTL이 가장 짧은 데이터 제거</li>
</ul>
<hr>
<h2 id="8-aws-네트워크--vpc-완전-이해">8. AWS 네트워크 — VPC 완전 이해</h2>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20230919120127/Screenshot-2023-09-19-120048-768.png" alt="AWS VPC Architecture — 퍼블릭/프라이빗 서브넷, NAT 게이트웨이, 보안 그룹 구조"></p>
<h3 id="81-vpc-virtual-private-cloud">8.1 VPC (Virtual Private Cloud)</h3>
<p>VPC는 <strong>AWS 클라우드 안에 사용자가 직접 정의하는 격리된 가상 네트워크</strong>다. 단일 AWS 리전에 연결된다.</p>
<p>VPC 특징:</p>
<ul>
<li>워크로드를 논리적으로 격리</li>
<li>리소스에 사용자 정의 액세스 제어와 보안 설정 적용</li>
<li>기본 VPC가 이미 리전마다 생성되어 있음</li>
</ul>
<h3 id="82-ip-주소와-cidr">8.2 IP 주소와 CIDR</h3>
<p><strong>CIDR(Classless Inter-Domain Routing)</strong> — IP 주소 범위를 지정하는 표기법</p>
<pre><code>10.22.0.0/16
│         └── 앞 16비트를 네트워크 주소로 예약
└──────────── IPv4 주소

범위: 10.22.0.0 ~ 10.22.255.255 (65,536개 IP)</code></pre><table>
<thead>
<tr>
<th>CIDR</th>
<th>IP 개수</th>
<th>사용 예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>/16</code></td>
<td>65,536</td>
<td>VPC</td>
</tr>
<tr>
<td><code>/20</code></td>
<td>4,096</td>
<td>대형 서브넷</td>
</tr>
<tr>
<td><code>/24</code></td>
<td>256</td>
<td>일반 서브넷</td>
</tr>
<tr>
<td><code>/28</code></td>
<td>16</td>
<td>소형 서브넷</td>
</tr>
</tbody></table>
<blockquote>
<p>AWS는 각 서브넷에서 5개의 IP를 예약한다 (네트워크 주소, VPC 라우터, DNS, 미래 사용, 브로드캐스트).</p>
</blockquote>
<h3 id="83-서브넷--퍼블릭과-프라이빗">8.3 서브넷 — 퍼블릭과 프라이빗</h3>
<p>서브넷은 VPC CIDR 블록의 하위 집합이다. 각 서브넷은 하나의 가용 영역에 속한다.</p>
<pre><code>VPC: 172.31.0.0/16 (서울 리전)
├── AZ-1 (ap-northeast-2a)
│   ├── 퍼블릭 서브넷: 172.31.0.0/20   ← 웹 서버, ALB
│   └── 프라이빗 서브넷: 172.31.32.0/20 ← 앱 서버, DB
└── AZ-2 (ap-northeast-2b)
    ├── 퍼블릭 서브넷: 172.31.16.0/20
    └── 프라이빗 서브넷: 172.31.48.0/20</code></pre><p><strong>퍼블릭 서브넷</strong>: 인터넷에서 직접 접근 가능 → 웹 서버, Load Balancer
<strong>프라이빗 서브넷</strong>: 인터넷에서 직접 접근 불가 → 앱 서버, 데이터베이스</p>
<h3 id="84-인터넷-게이트웨이-igw">8.4 인터넷 게이트웨이 (IGW)</h3>
<p>인터넷 게이트웨이는 VPC와 인터넷 사이의 관문이다. 퍼블릭 서브넷의 인스턴스가 인터넷과 통신하려면 반드시 필요하다.</p>
<p>IGW 역할:</p>
<ol>
<li>VPC 리소스와 인터넷 간의 통신 허용</li>
<li>NAT(Network Address Translation) 수행 — 프라이빗 IP ↔ 퍼블릭 IP 변환</li>
</ol>
<p><strong>퍼블릭 서브넷 조건</strong> (세 가지 모두 필요):</p>
<ol>
<li>VPC에 IGW 연결</li>
<li>라우팅 테이블에 <code>0.0.0.0/0 → IGW</code> 경로 추가</li>
<li>인스턴스에 퍼블릭 IP 할당</li>
</ol>
<h3 id="85-라우팅-테이블">8.5 라우팅 테이블</h3>
<p>라우팅 테이블은 네트워크 트래픽 경로를 결정하는 규칙 집합이다.</p>
<p><strong>퍼블릭 서브넷 라우팅 테이블</strong>:</p>
<pre><code>Destination      Target
172.31.0.0/16    local          ← VPC 내부 통신
0.0.0.0/0        igw-xxxxx      ← 모든 외부 트래픽 → IGW로</code></pre><p><strong>프라이빗 서브넷 라우팅 테이블</strong> (기본):</p>
<pre><code>Destination      Target
172.31.0.0/16    local          ← VPC 내부 통신만 가능</code></pre><p><strong>프라이빗 서브넷 라우팅 테이블</strong> (NAT 추가 시):</p>
<pre><code>Destination      Target
172.31.0.0/16    local
0.0.0.0/0        nat-xxxxx      ← 외부 트래픽 → NAT GW로</code></pre><h3 id="86-nat-게이트웨이">8.6 NAT 게이트웨이</h3>
<p>프라이빗 서브넷의 인스턴스가 인터넷에 <strong>아웃바운드(나가는) 통신</strong>이 필요할 때 사용한다 (소프트웨어 업데이트, 외부 API 호출 등). 단, <strong>인바운드(들어오는) 직접 접속은 차단</strong>된다.</p>
<pre><code>프라이빗 인스턴스 (172.31.32.75)
    │ 소스 IP: 172.31.32.75
    ▼
NAT 게이트웨이 (퍼블릭 서브넷에 위치)
    │ 소스 IP를 탄력적 IP(203.0.133.15)로 변환
    ▼
인터넷 게이트웨이
    │
    ▼
인터넷</code></pre><p>NAT GW는 반드시 <strong>퍼블릭 서브넷</strong>에 위치하고, <strong>탄력적 IP(EIP)</strong>를 부여해야 한다.</p>
<p><strong>탄력적 IP(Elastic IP)</strong>: 인스턴스를 재시작해도 변하지 않는 고정 공인 IP.</p>
<h3 id="87-보안-그룹-vs-네트워크-acl">8.7 보안 그룹 vs 네트워크 ACL</h3>
<p>AWS에는 두 가지 방화벽 계층이 있다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>보안 그룹 (Security Group)</th>
<th>네트워크 ACL (NACL)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>적용 단위</strong></td>
<td>인스턴스 (ENI 수준)</td>
<td>서브넷 수준</td>
</tr>
<tr>
<td><strong>규칙 유형</strong></td>
<td>허용 규칙만</td>
<td>허용 + 거부 규칙</td>
</tr>
<tr>
<td><strong>상태</strong></td>
<td><strong>Stateful</strong> — 응답 트래픽 자동 허용</td>
<td><strong>Stateless</strong> — 인바운드/아웃바운드 각각 설정</td>
</tr>
<tr>
<td><strong>규칙 평가</strong></td>
<td>모든 규칙을 동시 평가</td>
<td>번호 순서대로 평가, 첫 번째 매칭에서 종료</td>
</tr>
<tr>
<td><strong>기본 규칙</strong></td>
<td>모든 인바운드 차단, 모든 아웃바운드 허용</td>
<td>모든 트래픽 허용</td>
</tr>
</tbody></table>
<p><strong>트래픽 흐름</strong>:</p>
<pre><code>인터넷
  ↓
NACL 인바운드 규칙 (서브넷 경계에서 1차 필터링)
  ↓
보안 그룹 인바운드 규칙 (인스턴스 바로 앞에서 2차 필터링)
  ↓
EC2 인스턴스
  ↓
보안 그룹 아웃바운드 규칙
  ↓
NACL 아웃바운드 규칙 (Stateless라서 명시적으로 설정 필요)
  ↓
인터넷</code></pre><p><strong>보안 그룹 실전 설정 예시</strong>:</p>
<pre><code>웹 서버 보안 그룹 인바운드:
- HTTPS 443 / 0.0.0.0/0   ← 모든 곳에서 HTTPS 허용
- HTTP 80  / 0.0.0.0/0    ← 모든 곳에서 HTTP 허용
- SSH 22   / 내 IP만       ← 관리자 접속만 허용

앱 서버 보안 그룹 인바운드:
- TCP 8080 / 웹 서버 보안 그룹 ID  ← 웹 서버에서만 접근 허용

DB 서버 보안 그룹 인바운드:
- TCP 3306 / 앱 서버 보안 그룹 ID  ← 앱 서버에서만 MySQL 접근 허용</code></pre><p>이렇게 보안 그룹을 <strong>서로 참조</strong>하면 IP 대신 리소스 단위로 접근을 제어할 수 있다.</p>
<hr>
<h2 id="9-elb--elastic-load-balancer">9. ELB — Elastic Load Balancer</h2>
<h3 id="91-로드-밸런서-개념">9.1 로드 밸런서 개념</h3>
<p>ELB(Elastic Load Balancer)는 들어오는 트래픽을 여러 대상(EC2, 컨테이너, Lambda 등)에 자동 분산시키는 완전 관리형 서비스다.</p>
<pre><code>클라이언트 요청
    ↓
ELB (단일 진입점 — DNS 이름 고정)
    ↓ 트래픽 분산 (라운드 로빈, 최소 연결 등)
┌───────┬───────┬───────┐
│ EC2-1 │ EC2-2 │ EC2-3 │
└───────┴───────┴───────┘</code></pre><p>ELB 제공 기능:</p>
<ul>
<li><strong>헬스 체크</strong>: 비정상 대상을 자동으로 라우팅에서 제외 (설정한 간격마다 HTTP/TCP 확인)</li>
<li><strong>고가용성</strong>: 여러 AZ에 걸쳐 자동 분산 (Cross-Zone Load Balancing)</li>
<li><strong>SSL/TLS 종료</strong>: ELB에서 인증서 처리, EC2는 HTTP만 처리하면 됨</li>
<li><strong>고정 세션(Sticky Session)</strong>: 동일 클라이언트를 동일 대상으로 지속 라우팅</li>
</ul>
<h3 id="92-elb-유형-4가지">9.2 ELB 유형 4가지</h3>
<table>
<thead>
<tr>
<th>유형</th>
<th>OSI 계층</th>
<th>라우팅 기준</th>
<th>주요 프로토콜</th>
<th>사용 케이스</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ALB</strong> (Application)</td>
<td>7계층</td>
<td>URL 경로, 헤더, 메서드, 쿼리 파라미터</td>
<td>HTTP/HTTPS, gRPC, WebSocket</td>
<td>웹 앱, MSA, API Gateway 앞단</td>
</tr>
<tr>
<td><strong>NLB</strong> (Network)</td>
<td>4계층</td>
<td>IP + 포트</td>
<td>TCP, UDP, TLS</td>
<td>초저지연, 고성능, 고정 IP 필요 시</td>
</tr>
<tr>
<td><strong>CLB</strong> (Classic)</td>
<td>4/7계층 혼합</td>
<td>기본 HTTP/TCP</td>
<td>HTTP, HTTPS, TCP</td>
<td>구형 (신규 사용 비권장)</td>
</tr>
<tr>
<td><strong>GWLB</strong> (Gateway)</td>
<td>3계층</td>
<td>IP 패킷 전체</td>
<td>IP (GENEVE 캡슐화)</td>
<td>방화벽·IDS/IPS 어플라이언스 투명 삽입</td>
</tr>
</tbody></table>
<h3 id="93-alb--application-load-balancer">9.3 ALB — Application Load Balancer</h3>
<p>L7 레벨에서 동작하므로 <strong>URL 경로</strong>, <strong>호스트 헤더</strong>, <strong>HTTP 메서드</strong> 등으로 트래픽을 세밀하게 분기할 수 있다.</p>
<pre><code>ALB 리스너 (HTTPS:443)
    │
    ├── 규칙 1: Host = api.example.com       → Target Group A (API 서버)
    ├── 규칙 2: Path = /images/*              → Target Group B (이미지 서버)
    ├── 규칙 3: Path = /admin/* + IP 조건    → Target Group C (관리 서버)
    └── 기본 규칙                             → Target Group D (웹 서버)</code></pre><p><strong>Target Group</strong>: ALB가 트래픽을 전달할 대상들의 집합</p>
<ul>
<li>대상 유형: EC2 인스턴스, IP 주소, Lambda 함수, 다른 ALB</li>
<li>Target Group 단위로 헬스 체크, 포트, 프로토콜 설정</li>
<li><strong>하나의 ALB로 여러 MSA 서비스 라우팅</strong> → 로드밸런서 비용 절감</li>
</ul>
<h3 id="94-nlb--network-load-balancer">9.4 NLB — Network Load Balancer</h3>
<p>L4(TCP/UDP) 레벨 동작. 초당 수백만 요청을 밀리초 수준 지연으로 처리.</p>
<p>NLB 선택 기준:</p>
<ul>
<li>극도로 낮은 지연 시간이 필요한 실시간 서비스 (게임, 금융 트랜잭션)</li>
<li>HTTP가 아닌 TCP/UDP 프로토콜 직접 처리</li>
<li><strong>고정 IP 주소 필요</strong> (NLB는 AZ마다 고정 IP 제공, ALB는 IP가 변함)</li>
<li>AWS PrivateLink 기반 서비스 노출</li>
</ul>
<hr>
<h2 id="10-ec2-auto-scaling">10. EC2 Auto Scaling</h2>
<h3 id="101-auto-scaling-개념">10.1 Auto Scaling 개념</h3>
<p>Auto Scaling은 애플리케이션 부하에 따라 EC2 인스턴스 수를 자동으로 조절하는 서비스다. <strong>수평 확장(Scale Out/In)</strong>을 자동화한다.</p>
<pre><code>트래픽 급증 → CPU 70% 초과 감지 → 인스턴스 추가 (Scale Out)
트래픽 감소 → CPU 30% 미만 유지 → 인스턴스 제거 (Scale In)</code></pre><p><strong>Auto Scaling Group(ASG) 핵심 설정값</strong>:</p>
<table>
<thead>
<tr>
<th>설정</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>최소 용량 (Min)</strong></td>
<td>항상 유지할 인스턴스 최솟값</td>
<td>2</td>
</tr>
<tr>
<td><strong>최대 용량 (Max)</strong></td>
<td>절대 초과하지 않을 상한선</td>
<td>10</td>
</tr>
<tr>
<td><strong>희망 용량 (Desired)</strong></td>
<td>현재 목표 인스턴스 수</td>
<td>4</td>
</tr>
<tr>
<td><strong>시작 템플릿</strong></td>
<td>신규 인스턴스 생성 명세 (AMI, 유형, SG 등)</td>
<td>lt-0abc1234</td>
</tr>
</tbody></table>
<h3 id="102-auto-scaling-정책-4가지">10.2 Auto Scaling 정책 4가지</h3>
<p><strong>① 목표 추적 조정 (Target Tracking)</strong> — 권장
지정 지표를 목표값으로 자동 유지. 설정이 가장 간단하고 효과적이다.</p>
<pre><code>설정: &quot;ALB RequestCountPerTarget = 1000&quot;
→ 대상당 요청이 1000을 초과 → 인스턴스 자동 추가
→ 요청이 감소 → 인스턴스 자동 제거</code></pre><p>사용 가능 지표: <code>CPUUtilization</code>, <code>ALBRequestCountPerTarget</code>, <code>NetworkIn/Out</code></p>
<p><strong>② 단계 조정 (Step Scaling)</strong>
지표 수치 구간별로 단계적으로 다른 크기 조정.</p>
<pre><code>CPU 60~70% → +1개
CPU 70~85% → +3개
CPU 85% 이상 → +5개</code></pre><p><strong>③ 단순 조정 (Simple Scaling)</strong>
CloudWatch 알람 하나에 고정 수량 조정. (현재는 Step Scaling으로 대체 추세)</p>
<p><strong>④ 예약 조정 (Scheduled Scaling)</strong>
특정 시간대에 미리 용량 조정.</p>
<pre><code>매일 오전 08:00 → 인스턴스 10개 (업무 시작 대비)
매일 자정 00:00 → 인스턴스 2개 (야간 최소 운영)</code></pre><h3 id="103-elb--auto-scaling-통합-동작">10.3 ELB + Auto Scaling 통합 동작</h3>
<pre><code>인터넷
  ↓
ALB (헬스 체크, 트래픽 분산)
  ↓
Auto Scaling Group
  ├── EC2 인스턴스 1  ← ASG가 ALB Target Group에 자동 등록
  ├── EC2 인스턴스 2
  └── EC2 인스턴스 3  (부하 증가 시 자동 추가)</code></pre><ul>
<li>신규 인스턴스 기동 → ALB Target Group에 <strong>자동 등록</strong> → 헬스 체크 통과 후 트래픽 수신</li>
<li>인스턴스 제거 전 <strong>Connection Draining</strong>(기존 요청 처리 완료) 후 제거</li>
<li>헬스 체크 실패 인스턴스 → ASG가 자동 <strong>종료 후 새 인스턴스 교체</strong></li>
</ul>
<hr>
<h2 id="11-route53--cloudfront">11. Route53 + CloudFront</h2>
<h3 id="111-amazon-route53--dns-서비스">11.1 Amazon Route53 — DNS 서비스</h3>
<p>Route53은 AWS의 완전 관리형 DNS(Domain Name System) 서비스다.</p>
<pre><code>사용자: &quot;www.example.com&quot; 입력
    ↓
Route53 DNS 조회
    ↓
IP 주소 반환 (예: 52.95.110.1)
    ↓
해당 IP로 실제 연결</code></pre><p>Route53 3가지 기능:</p>
<ol>
<li><strong>도메인 등록</strong>: Route53에서 직접 도메인 구매 가능</li>
<li><strong>DNS 레코드 관리</strong>: A, AAAA, CNAME, MX, TXT, NS, SOA 레코드 관리</li>
<li><strong>트래픽 라우팅 + 헬스 체크</strong>: 엔드포인트 상태를 모니터링해 자동 장애 전환</li>
</ol>
<h3 id="112-route53-라우팅-정책-6가지">11.2 Route53 라우팅 정책 6가지</h3>
<p><strong>① 단순 라우팅 (Simple)</strong></p>
<ul>
<li>도메인 → 단일 IP 반환</li>
<li>헬스 체크 불가, 레코드 하나에 여러 IP 등록 시 무작위 반환</li>
</ul>
<p><strong>② 가중치 기반 라우팅 (Weighted)</strong></p>
<pre><code>api.example.com
  ├── 서울 ALB (가중치 70) → 전체 트래픽의 70%
  └── 도쿄 ALB (가중치 30) → 전체 트래픽의 30%</code></pre><p>카나리 배포(신버전에 일부 트래픽 먼저 전송), 부하 분산에 활용.</p>
<p><strong>③ 지연 시간 기반 라우팅 (Latency)</strong>
사용자와 각 AWS 리전 간 네트워크 지연 시간을 측정해 <strong>가장 빠른 리전</strong>의 엔드포인트를 반환.
글로벌 서비스에서 사용자 경험 최적화에 사용.</p>
<p><strong>④ 장애 조치 라우팅 (Failover)</strong></p>
<pre><code>Primary: 서울 리전 ALB  ← 헬스 체크 정상 시 100% 트래픽
    ↓ 헬스 체크 실패
Secondary: 도쿄 리전 ALB  ← 자동 전환 (Active-Passive DR)</code></pre><p><strong>⑤ 지리 위치 라우팅 (Geolocation)</strong>
클라이언트의 <strong>물리적 위치</strong>(국가, 대륙)를 기반으로 라우팅.</p>
<pre><code>한국 → 서울 리전 엔드포인트
미국 → us-east-1 엔드포인트
EU → eu-west-1 엔드포인트
기타 → 기본 레코드</code></pre><p>데이터 주권, 콘텐츠 현지화, GDPR 준수 등에 활용.</p>
<p><strong>⑥ 다중값 응답 (Multivalue Answer)</strong>
최대 8개의 정상 레코드를 무작위로 반환. 헬스 체크와 연동 가능.
ELB 대체용이 아닌 클라이언트 측 단순 분산 보조 수단.</p>
<h3 id="113-amazon-cloudfront--cdn">11.3 Amazon CloudFront — CDN</h3>
<p>CloudFront는 AWS의 콘텐츠 전송 네트워크(CDN) 서비스다. 전 세계 <strong>450개 이상의 엣지 로케이션</strong>에서 콘텐츠를 캐싱해 지연 시간을 줄인다.</p>
<pre><code>오리진 서버 (S3 / ALB / EC2 / API Gateway)
    ↑ 캐시 MISS 시에만 요청 (오리진 부하 감소)
CloudFront 배포 (Distribution)
    ↑ 캐시 HIT 시 엣지에서 즉시 반환
엣지 로케이션 (사용자와 가장 가까운 곳)
    ↑
사용자</code></pre><p><strong>CloudFront 주요 개념</strong>:</p>
<table>
<thead>
<tr>
<th>용어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Distribution</strong></td>
<td>CloudFront 배포 단위. 오리진과 동작(Behavior) 설정 포함</td>
</tr>
<tr>
<td><strong>Origin</strong></td>
<td>원본 콘텐츠 위치 (S3, ALB, 커스텀 HTTP 서버)</td>
</tr>
<tr>
<td><strong>OAC (Origin Access Control)</strong></td>
<td>S3 버킷을 퍼블릭으로 열지 않고 CloudFront만 접근 허용</td>
</tr>
<tr>
<td><strong>TTL</strong></td>
<td>캐시 유지 시간 (기본 24시간, <code>Cache-Control</code> 헤더로 제어)</td>
</tr>
<tr>
<td><strong>Invalidation</strong></td>
<td>특정 경로의 캐시를 강제 삭제 (<code>/images/*</code> 패턴 지원)</td>
</tr>
<tr>
<td><strong>Behavior</strong></td>
<td>경로 패턴별 오리진, 캐시 정책, 허용 HTTP 메서드 분리 설정</td>
</tr>
</tbody></table>
<p><strong>CloudFront + S3 정적 웹사이트 구성</strong>:</p>
<pre><code>사용자 → CloudFront (HTTPS 자동 적용, 전 세계 엣지 캐싱)
              ↑ 캐시 MISS 시만
         S3 버킷 (OAC로 접근, 버킷 자체는 프라이빗 유지)</code></pre><p><strong>캐시 정책 예시</strong>:</p>
<pre><code>Behavior 1: /api/*      → ALB 오리진, TTL=0 (캐시 없음, 항상 오리진 요청)
Behavior 2: /static/*   → S3 오리진, TTL=86400 (1일 캐시)
Behavior 3: *.jpg, *.png → S3 오리진, TTL=604800 (7일 캐시)
기본           → ALB 오리진, TTL=0</code></pre><hr>
<h2 id="12-하이브리드-네트워크--vpn-vs-direct-connect">12. 하이브리드 네트워크 — VPN vs Direct Connect</h2>
<h3 id="121-클라우드-배포-모델-4가지">12.1 클라우드 배포 모델 4가지</h3>
<table>
<thead>
<tr>
<th>모델</th>
<th>설명</th>
<th>장단점</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Public Cloud</strong></td>
<td>AWS/Azure/GCP 단독 사용</td>
<td>저비용, 빠른 시작, 유지보수 불필요</td>
</tr>
<tr>
<td><strong>Private Cloud</strong></td>
<td>기업 전용 인프라 (온프레미스)</td>
<td>높은 보안/제어권, 고비용</td>
</tr>
<tr>
<td><strong>Hybrid Cloud</strong></td>
<td>Public + Private 조합</td>
<td>민감 데이터는 Private, 나머지는 Public</td>
</tr>
<tr>
<td><strong>Multi Cloud</strong></td>
<td>여러 CSP 동시 사용</td>
<td>벤더 종속 탈피, 복잡한 운영</td>
</tr>
</tbody></table>
<p>실제 기업들이 가장 많이 쓰는 패턴: <strong>Hybrid Cloud</strong> — 기존 온프레미스 인프라를 단번에 클라우드로 옮기기 어렵기 때문.</p>
<h3 id="122-aws-site-to-site-vpn">12.2 AWS Site-to-Site VPN</h3>
<p>회사 데이터 센터(온프레미스)와 AWS VPC를 인터넷을 통해 <strong>암호화 터널</strong>로 연결한다.</p>
<pre><code>기업 데이터 센터
└── 고객 게이트웨이 디바이스 (VPN 장비)
         ↕ 암호화 터널 2개 (이중화)
└── 가상 프라이빗 게이트웨이 (AWS 측)
         └── VPC</code></pre><p>특징:</p>
<ul>
<li>최대 속도: <strong>1.25Gbps</strong></li>
<li>구성이 빠름 (수 시간 ~ 수 일)</li>
<li>비활성 시 요금 없음</li>
<li>데이터가 퍼블릭 인터넷을 경유 (암호화됨)</li>
<li>정적(Static) 또는 동적(BGP) 라우팅 지원</li>
</ul>
<h3 id="123-aws-direct-connect">12.3 AWS Direct Connect</h3>
<p>데이터 센터에서 AWS까지 전용 <strong>광 케이블</strong>로 직접 연결한다.</p>
<pre><code>기업 데이터 센터
    → Direct Connect 파트너 시설 (코로케이션)
        → AWS 전용 회선
            → 가상 프라이빗 게이트웨이
                → VPC</code></pre><p>특징:</p>
<ul>
<li>속도: 50Mbps(Hosted Connection 최소) ~ <strong>100Gbps</strong>(Dedicated Connection 최대)</li>
<li>낮은 지연 시간, 일관된 네트워크 성능 (인터넷 혼잡 없음)</li>
<li>데이터 기본 암호화 없음 (VPN 레이어 별도 추가 가능)</li>
<li>특수 계약 필요, 설치에 수 주 ~ 수 개월 소요</li>
<li>연결이 비활성 상태여도 포트 시간 비용 발생</li>
</ul>
<p><strong>VPN vs Direct Connect 언제 쓸까?</strong></p>
<table>
<thead>
<tr>
<th>기준</th>
<th>VPN 선택</th>
<th>Direct Connect 선택</th>
</tr>
</thead>
<tbody><tr>
<td>구성 속도</td>
<td>빠르게 필요</td>
<td>충분히 준비 시간 있음</td>
</tr>
<tr>
<td>대역폭</td>
<td>1.25Gbps 이하</td>
<td>대용량 데이터 전송</td>
</tr>
<tr>
<td>비용</td>
<td>저비용</td>
<td>전용 회선 비용 감수</td>
</tr>
<tr>
<td>암호화</td>
<td>기본 제공</td>
<td>별도 구성 필요</td>
</tr>
<tr>
<td>인터넷 경유</td>
<td>Yes (암호화됨)</td>
<td>No (전용 회선)</td>
</tr>
</tbody></table>
<hr>
<h2 id="13-aws-iam--보안과-권한-관리">13. AWS IAM — 보안과 권한 관리</h2>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20260608171005181833/IAMCOMP.webp" alt="AWS IAM Components — User, Group, Role, Policy 구조"></p>
<h3 id="131-루트-사용자의-위험성">13.1 루트 사용자의 위험성</h3>
<p>AWS 계정을 생성하면 <strong>루트 사용자</strong>가 만들어진다. 루트 사용자는 모든 AWS 서비스에 대한 전체 접근 권한을 가진다. 이 계정이 탈취되면 전체 인프라가 위험하다.</p>
<p><strong>루트 사용자로 해야 할 일</strong>:</p>
<ol>
<li>결제 정보 관리</li>
<li>초기 IAM 관리자 계정 생성</li>
<li>AWS 계정 해지</li>
</ol>
<p>그 외 모든 일상 작업은 <strong>IAM 사용자</strong>로 해야 한다.</p>
<h3 id="132-iam-핵심-구성-요소">13.2 IAM 핵심 구성 요소</h3>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20260608163749217807/IAM-aws.webp" alt="AWS IAM Architecture"></p>
<p><strong>IAM 사용자 (User)</strong></p>
<ul>
<li>사람이나 애플리케이션을 대표하는 엔터티</li>
<li>고유한 자격 증명 (비밀번호, 액세스 키) 보유</li>
<li>콘솔 접근 또는 프로그래밍 방식 접근 설정 가능</li>
</ul>
<p><strong>IAM 그룹 (Group)</strong></p>
<ul>
<li>IAM 사용자들의 집합</li>
<li>그룹에 정책을 연결하면 모든 구성원에게 일괄 적용</li>
<li>신규 팀원 온보딩 시 그룹에 추가하기만 하면 됨</li>
</ul>
<pre><code>그룹: Developers
├── 정책: AmazonEC2FullAccess
├── 정책: AmazonS3ReadOnly
│
├── 사용자: Alice
├── 사용자: Bob
└── 사용자: Charlie  ← 이 세 명 모두 위 정책 적용됨</code></pre><p><strong>IAM 역할 (Role)</strong></p>
<ul>
<li>임시 자격 증명을 사용하여 특정 권한을 위임</li>
<li>사람이 아닌 <strong>서비스</strong>나 <strong>다른 계정</strong>에 권한 부여 시 사용</li>
<li>EC2 인스턴스, Lambda, ECS 태스크 등이 역할을 수임</li>
</ul>
<pre><code>예시: EC2 인스턴스가 S3에 파일을 업로드해야 할 때
→ EC2에 &quot;S3 쓰기 권한&quot; 역할을 부여
→ EC2가 역할을 수임 → 임시 자격 증명 발급 → S3 접근
→ 장기 자격 증명(액세스 키)을 코드에 하드코딩할 필요 없음</code></pre><p><strong>IAM 정책 (Policy)</strong></p>
<ul>
<li>JSON 형식으로 권한을 정의하는 문서</li>
<li>사용자, 그룹, 역할에 연결</li>
</ul>
<pre><code class="language-json">{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: [
        &quot;ec2:StartInstances&quot;,
        &quot;ec2:StopInstances&quot;
      ],
      &quot;Resource&quot;: &quot;arn:aws:ec2:*:*:instance/*&quot;,
      &quot;Condition&quot;: {
        &quot;StringEquals&quot;: {
          &quot;ec2:ResourceTag/Owner&quot;: &quot;${aws:username}&quot;
        }
      }
    }
  ]
}</code></pre>
<p>정책 요소:</p>
<ul>
<li><strong>Effect</strong>: <code>Allow</code> 또는 <code>Deny</code></li>
<li><strong>Action</strong>: 허용/거부할 작업 (예: <code>ec2:StartInstances</code>)</li>
<li><strong>Resource</strong>: 대상 리소스 ARN (특정 EC2 또는 <code>*</code> 전체)</li>
<li><strong>Condition</strong>: 선택적 조건 (IP, 태그, MFA 등)</li>
</ul>
<p><strong>정책 유형</strong>:</p>
<table>
<thead>
<tr>
<th>유형</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>AWS 관리형</td>
<td>AWS가 만들고 유지</td>
<td><code>AmazonEC2FullAccess</code></td>
</tr>
<tr>
<td>고객 관리형</td>
<td>직접 만든 커스텀 정책</td>
<td><code>MyCustomPolicy</code></td>
</tr>
<tr>
<td>인라인 정책</td>
<td>특정 사용자/역할에 직접 포함</td>
<td>재사용 안 함</td>
</tr>
<tr>
<td>리소스 기반 정책</td>
<td>리소스에 직접 연결</td>
<td>S3 버킷 정책</td>
</tr>
</tbody></table>
<h3 id="133-역할-수임-assume-role-작동-방식">13.3 역할 수임 (Assume Role) 작동 방식</h3>
<pre><code>1. IAM 사용자/서비스 → STS에 역할 수임 요청
2. AWS STS → 임시 보안 자격 증명 반환
           (Access Key + Secret Key + Session Token, 유효시간 제한)
3. 임시 자격 증명으로 → AWS 리소스 접근</code></pre><p>역할은 한 번 수임하면 <strong>일시적으로만</strong> 유효하다. 장기 자격 증명 노출 위험이 없다.</p>
<h3 id="134-aws-organizations--다중-계정-관리">13.4 AWS Organizations — 다중 계정 관리</h3>
<p>대기업이나 성장하는 스타트업은 계정을 여러 개로 분리해 운영한다. 이유:</p>
<ul>
<li>팀별·환경별(개발/스테이징/운영) 청구 분리</li>
<li>보안 경계 강화 (한 계정의 오류가 다른 계정에 영향 없음)</li>
<li>규정 준수 요구사항</li>
</ul>
<p>Organizations를 쓰면 이 계정들을 <strong>계층 구조(OU)</strong>로 관리하고, 통합 결제도 가능하다.</p>
<pre><code>관리 계정 (Root)
└── OU: 개발
│   ├── 계정: dev-frontend
│   └── 계정: dev-backend
└── OU: 스테이징
│   └── 계정: staging
└── OU: 운영
    ├── 계정: prod-kr
    └── 계정: prod-us</code></pre><p><strong>SCP(서비스 제어 정책)</strong> — OU 또는 계정 수준의 최대 권한 제한</p>
<pre><code>예시: 운영 OU에 SCP 적용
SCP: &quot;EC2, S3, RDS만 허용&quot;
→ 이 OU 안의 계정들은 아무리 IAM 정책을 넓게 줘도 허용된 서비스만 사용 가능</code></pre><p>IAM 정책과 SCP의 교집합만 실제로 허용된다.</p>
<hr>
<h2 id="14-vm-vs-container-vs-msa">14. VM vs Container vs MSA</h2>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20200103130309/Capture36633.png" alt="VM과 Container 아키텍처 비교"></p>
<h3 id="141-세-가지-실행-환경-비교">14.1 세 가지 실행 환경 비교</h3>
<p><strong>Bare Metal Server</strong>: 물리 서버를 통째로 하나의 OS가 사용</p>
<p><strong>Virtual Machine (VM)</strong>: 하이퍼바이저가 물리 서버를 논리적으로 분할, 각 VM이 독자적인 OS 보유</p>
<p><strong>Container</strong>: 호스트 OS의 커널을 공유하되 프로세스 격리, 애플리케이션과 의존성만 패키징</p>
<pre><code>Bare Metal          Virtual Machine        Container
┌──────────┐       ┌──┐ ┌──┐ ┌──┐       ┌──┐ ┌──┐ ┌──┐
│    App   │       │A1│ │A2│ │A3│       │A1│ │A2│ │A3│
├──────────┤       │OS│ │OS│ │OS│       │Lib│ │Lib│ │Lib│
│    OS    │       └──┘ └──┘ └──┘       └──┘ └──┘ └──┘
├──────────┤       ┌──────────────┐      ┌──────────────┐
│ Hardware │       │  Hypervisor  │      │Container Engine│
└──────────┘       ├──────────────┤      ├──────────────┤
                   │   Hardware   │      │     Host OS   │
                   └──────────────┘      ├──────────────┤
                                         │   Hardware   │
                                         └──────────────┘</code></pre><p><strong>상세 비교표</strong>:</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>Bare Metal</th>
<th>VM</th>
<th>Container</th>
</tr>
</thead>
<tbody><tr>
<td>OS</td>
<td>단일 OS</td>
<td>VM마다 별도 OS</td>
<td>호스트 OS 공유</td>
</tr>
<tr>
<td>크기</td>
<td>-</td>
<td>수 GB</td>
<td>수 MB ~ 수백 MB</td>
</tr>
<tr>
<td>부팅 속도</td>
<td>수 분</td>
<td>수 분</td>
<td><strong>수 초 이내</strong></td>
</tr>
<tr>
<td>격리 수준</td>
<td>물리적 완전 격리</td>
<td>하드웨어 수준 격리</td>
<td>프로세스 수준 격리</td>
</tr>
<tr>
<td>이식성</td>
<td>낮음</td>
<td>중간</td>
<td><strong>매우 높음</strong></td>
</tr>
<tr>
<td>성능</td>
<td>최고</td>
<td>오버헤드 있음</td>
<td>거의 Native에 가까움</td>
</tr>
<tr>
<td>대표 기술</td>
<td>-</td>
<td>VMware, Hyper-V, KVM</td>
<td><strong>Docker</strong>, containerd, Podman</td>
</tr>
</tbody></table>
<p><strong>상황별 선택 가이드</strong>:</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>추천</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>초고성능 DB 서버</td>
<td>Bare Metal</td>
<td>가상화 오버헤드 없이 최대 I/O 성능</td>
</tr>
<tr>
<td>지연에 민감한 금융 트랜잭션</td>
<td>Bare Metal</td>
<td>낮은 지연(latency), 안정적 성능</td>
</tr>
<tr>
<td>개발/테스트 환경 다수</td>
<td>VM</td>
<td>다양한 OS 시뮬레이션 가능</td>
</tr>
<tr>
<td>고가용성 필요한 웹 서비스</td>
<td>VM</td>
<td>스냅샷, Auto Scaling, HA 기본 지원</td>
</tr>
<tr>
<td>마이크로서비스 운영</td>
<td><strong>Container</strong></td>
<td>경량, 빠른 배포, 환경 일관성</td>
</tr>
<tr>
<td>ML 워크로드</td>
<td>GPU VM / Container</td>
<td>GPU 탑재 VM, 또는 컨테이너로 이식성 확보</td>
</tr>
</tbody></table>
<h3 id="142-container의-핵심-특성">14.2 Container의 핵심 특성</h3>
<p>컨테이너는 애플리케이션과 그 실행에 필요한 <strong>모든 요소(코드, 런타임, 라이브러리, 설정)</strong>를 하나로 패키징한 경량 실행 환경이다.</p>
<p>4가지 핵심 특성:</p>
<table>
<thead>
<tr>
<th>특성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Lightweight</strong></td>
<td>OS를 포함하지 않아 가볍고 빠름</td>
</tr>
<tr>
<td><strong>Portable</strong></td>
<td>&quot;내 맥북에서 되는 게 서버에서도 된다&quot;</td>
</tr>
<tr>
<td><strong>Isolated</strong></td>
<td>컨테이너끼리 독립 실행, 서로 영향 없음</td>
</tr>
<tr>
<td><strong>Scalable</strong></td>
<td>필요에 따라 즉시 수평 확장/축소</td>
</tr>
</tbody></table>
<h3 id="143-msa-micro-service-architecture">14.3 MSA (Micro Service Architecture)</h3>
<p><strong>Monolithic(모놀리식)</strong>: 모든 기능이 하나의 배포 단위에 묶임</p>
<p>문제점:</p>
<ul>
<li>작은 수정도 전체 재배포</li>
<li>한 기능의 장애가 전체 시스템에 영향</li>
<li>특정 기능만 스케일 아웃 불가 (전체를 스케일 아웃해야 함)</li>
<li>기술 스택 통일 강제</li>
</ul>
<p><strong>MSA</strong>: 기능별로 독립적인 서비스로 분리</p>
<pre><code>Monolithic 주문 앱                   MSA 주문 앱
┌─────────────────────┐             
│ 주문 UI             │             [주문 UI]  ─────────────────────┐
│ 주문 기록            │     →                                       │
│ 인벤토리             │             ALB (Application Load Balancer) │
│ 이메일 발송          │               │                             │
│ 영업 기록            │         ┌────┴──┬──────┬──────┐           │
└─────────────────────┘         │       │      │      │           │
                             [주문]  [인벤]  [이메] [영업]         │
                              서비스   토리   일     기록           │
                                    서비스  서비스  서비스          │</code></pre><p>각 서비스:</p>
<ul>
<li>독립적으로 배포 가능</li>
<li>독립적으로 확장 가능 (주문이 몰리면 주문 서비스만 스케일 아웃)</li>
<li>서비스별 장애 격리 (이메일 서비스가 죽어도 주문은 처리됨)</li>
<li>서비스별 기술 스택 자유 (Python AI 서비스 + Java 주문 서비스 혼용 가능)</li>
</ul>
<p>컨테이너 + MSA + Kubernetes = 현대 클라우드 아키텍처의 핵심</p>
<hr>
<h2 id="15-kubernetes--컨테이너-오케스트레이션">15. Kubernetes — 컨테이너 오케스트레이션</h2>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20260406154006746759/k8s-arch.webp" alt="Kubernetes Architecture — Master Node와 Worker Node 구성"></p>
<p>컨테이너가 늘어나면 직접 관리가 한계에 부딪힌다. &quot;이 컨테이너가 죽었는데?&quot;, &quot;트래픽이 몰리는데 더 띄워야 하는데?&quot;, &quot;서버 A가 꽉 찼는데 서버 B에는 올려도 되나?&quot; — 이 모든 걸 자동화해주는 것이 <strong>Kubernetes(쿠버네티스, k8s)</strong>다.</p>
<h3 id="151-kubernetes-아키텍처">15.1 Kubernetes 아키텍처</h3>
<p><strong>Control Plane (마스터 노드)</strong> — 클러스터 전체를 관리·감독</p>
<table>
<thead>
<tr>
<th>컴포넌트</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>API Server</strong></td>
<td>클러스터의 프론트엔드. kubectl 명령어, 대시보드, CI/CD 파이프라인이 여기로 요청</td>
</tr>
<tr>
<td><strong>etcd</strong></td>
<td>클러스터의 모든 상태 정보를 저장하는 고가용성 Key-Value DB. &quot;Source of Truth&quot;</td>
</tr>
<tr>
<td><strong>Scheduler</strong></td>
<td>새 Pod를 어느 Worker Node에 배치할지 결정. 리소스 가용량, 제약 조건, 정책 고려</td>
</tr>
<tr>
<td><strong>Controller Manager</strong></td>
<td>현재 상태와 원하는 상태의 차이를 감지하고 수렴시킴 (ReplicaSet 컨트롤러, 노드 컨트롤러 등)</td>
</tr>
</tbody></table>
<p><strong>Worker Node</strong> — 실제 컨테이너가 실행되는 곳</p>
<table>
<thead>
<tr>
<th>컴포넌트</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>kubelet</strong></td>
<td>API Server와 통신하며 컨테이너 실행 명령을 수행. 노드의 상태를 API Server에 보고</td>
</tr>
<tr>
<td><strong>kube-proxy</strong></td>
<td>네트워크 라우팅 관리. Service의 가상 IP로 들어오는 트래픽을 실제 Pod로 전달</td>
</tr>
<tr>
<td><strong>Container Runtime</strong></td>
<td>컨테이너 실제 실행 엔진 (containerd, CRI-O 등)</td>
</tr>
</tbody></table>
<h3 id="152-kubernetes-클러스터-구축-실습">15.2 Kubernetes 클러스터 구축 실습</h3>
<p>실습에서는 AWS EC2 인스턴스 3개로 클러스터를 구성했다:</p>
<ul>
<li>Master Node: <code>t3.medium</code> (2vCPU, 4GB RAM) × 1</li>
<li>Worker Node: <code>t3.small</code> × 2</li>
</ul>
<p>최소 요건: 2GB 이상 RAM, 2개 이상 CPU, swap 비활성화</p>
<p><strong>설치 전체 흐름</strong>:</p>
<pre><code class="language-bash"># ① 모든 노드: 스왑 비활성화 (k8s 필수 사전 조건)
sudo swapoff -a
sudo sed -i &#39;/ swap / s/^\(.*\)$/#\1/g&#39; /etc/fstab

# ② 모든 노드: 커널 모듈 로드
sudo tee /etc/modules-load.d/containerd.conf &lt;&lt;EOF
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter

# ③ 모든 노드: 커널 파라미터 설정 (Pod 간 네트워크 통신)
sudo tee /etc/sysctl.d/kubernetes.conf &lt;&lt;EOF
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system

# ④ 모든 노드: containerd 설치
sudo apt install -y containerd.io
containerd config default | sudo tee /etc/containerd/config.toml &gt;/dev/null 2&gt;&amp;1
sudo sed -i &#39;s/SystemdCgroup \= false/SystemdCgroup \= true/g&#39; /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl enable containerd

# ⑤ 모든 노드: Kubernetes 구성 요소 설치
sudo apt install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl  # 자동 업그레이드 방지

# ⑥ 마스터 노드에서만: 클러스터 초기화
sudo kubeadm init

# ⑦ 마스터 노드: kubeconfig 설정
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

# ⑧ 마스터 노드: 네트워크 플러그인 설치 (Calico)
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.2/manifests/calico.yaml

# ⑨ 워커 노드에서: 클러스터에 조인
sudo kubeadm join 172.31.x.x:6443 --token &lt;토큰&gt; \
    --discovery-token-ca-cert-hash sha256:&lt;해시&gt;

# ⑩ 마스터 노드: 전체 노드 상태 확인
kubectl get nodes</code></pre>
<p>왜 swap을 비활성화해야 할까?<br>Kubernetes는 메모리 관리를 직접 한다. swap이 활성화되어 있으면 k8s가 실제 메모리 사용량을 정확히 파악하기 어렵고, 스케줄링 결정이 부정확해진다.</p>
<p>왜 네트워크 플러그인이 필요할까?<br>k8s 자체는 Pod 간 통신 방법을 명세(CNI)만 정의한다. 실제 구현은 Calico, Flannel, Weave 등의 플러그인이 담당한다.</p>
<h3 id="153-pod--가장-작은-배포-단위">15.3 Pod — 가장 작은 배포 단위</h3>
<p>Pod는 Kubernetes에서 배포 가능한 <strong>가장 작은 단위</strong>다. 하나 이상의 컨테이너가 같은 네트워크와 스토리지를 공유하며 하나의 단위로 실행된다.</p>
<pre><code class="language-yaml"># po/pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-test2
spec:
  containers:
  - name: test-nginx-container
    image: nginx:latest
    ports:
    - containerPort: 80
      protocol: TCP</code></pre>
<p><strong>자주 쓰는 Pod 관련 명령어</strong>:</p>
<pre><code class="language-bash"># Pod 생성 (명령어 방식)
kubectl run nginx-pod --image=nginx --port=80

# Pod 생성 (YAML 방식)
kubectl apply -f po/pod.yaml

# Pod 목록 확인
kubectl get pods
kubectl get pods -o wide   # IP, 배치된 노드도 표시

# Pod 상세 정보 (이벤트, 컨테이너 상태 등)
kubectl describe pods pod-test2

# Pod 내부 접속
kubectl exec -it pod-test2 -- bash

# Pod에 명령어 전달
kubectl exec pod-test2 -- env

# 로그 확인
kubectl logs pod-test2
kubectl logs -f pod-test2   # 실시간 로그 스트리밍

# Pod 삭제
kubectl delete pod pod-test2
kubectl delete -f po/pod.yaml</code></pre>
<p><strong>Pod의 한계</strong>: Pod를 직접 만들면, Pod가 죽었을 때 아무도 되살려주지 않는다. 이걸 해결하는 것이 ReplicaSet이다.</p>
<h3 id="154-replicaset--pod를-n개-유지">15.4 ReplicaSet — Pod를 N개 유지</h3>
<p>ReplicaSet은 <strong>&quot;이 Pod를 항상 N개 유지해줘&quot;</strong> 라고 선언하는 리소스다.</p>
<pre><code class="language-yaml"># rs/rs-nginx.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: replicaset-nginx
spec:
  replicas: 3                    # 항상 3개 유지
  selector:
    matchLabels:
      app: test-nginx-pods-label # 이 레이블의 Pod를 관리
  template:
    metadata:
      name: test-nginx-pod
      labels:
        app: test-nginx-pods-label
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80</code></pre>
<p><strong>ReplicaSet 동작 원리</strong>:</p>
<pre><code class="language-bash"># 현재 3개의 Pod가 실행 중
kubectl get pods --show-labels

# Pod 하나를 수동 삭제하면?
kubectl delete pod replicaset-nginx-xxxxx

# 즉시 새 Pod가 생성됨!
kubectl get pods   # 여전히 3개</code></pre>
<p><strong>ReplicaSet이 Pod를 인식하는 방법</strong>: <code>selector.matchLabels</code>에 정의된 레이블을 가진 Pod가 관리 대상.</p>
<ul>
<li>레이블이 맞는 Pod가 N개보다 적으면 → 새 Pod 생성</li>
<li>레이블이 맞는 Pod가 N개보다 많으면 → 초과 Pod 삭제</li>
<li>Pod의 레이블을 수동으로 제거하면 → ReplicaSet 관리에서 벗어남 (고아 Pod)</li>
</ul>
<h3 id="155-deployment--버전-관리까지-되는-업그레이드">15.5 Deployment — 버전 관리까지 되는 업그레이드</h3>
<p>실제 서비스에서는 ReplicaSet을 직접 다루지 않는다. <strong>Deployment</strong>를 사용한다.</p>
<p>Deployment = ReplicaSet 관리 + <strong>롤링 업데이트 + 롤백</strong></p>
<pre><code class="language-yaml"># deploy/deploy-nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-nginx-deploy
spec:
  replicas: 3
  selector:
    matchLabels:
      app: test-nginx
  template:
    metadata:
      labels:
        app: test-nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.10     # 현재 버전
        ports:
        - containerPort: 80</code></pre>
<pre><code class="language-bash"># Deployment 생성
kubectl apply -f deploy/deploy-nginx.yaml

# 생성된 Deployment, ReplicaSet, Pod 확인
kubectl get deployment
kubectl get replicasets,pods

# 이미지 버전 업데이트 → 롤링 업데이트 자동 실행
kubectl set image deploy test-nginx-deploy nginx=nginx:1.19

# 롤링 업데이트 진행 상황 실시간 확인
kubectl rollout status deployment test-nginx-deploy

# ReplicaSet 2개가 존재 (이전 버전 + 새 버전)
kubectl get replicasets

# 롤아웃 히스토리 확인
kubectl rollout history deployment test-nginx-deploy

# 이전 버전으로 롤백 (1번 revision으로)
kubectl rollout undo deploy test-nginx-deploy --to-revision=1</code></pre>
<p><strong>롤링 업데이트 동작 방식</strong>:</p>
<ol>
<li>새 버전의 ReplicaSet 생성</li>
<li>새 ReplicaSet의 Pod를 1개씩 증가</li>
<li>이전 ReplicaSet의 Pod를 1개씩 감소</li>
<li>완전히 전환될 때까지 반복</li>
<li>이전 ReplicaSet은 Pod 0개로 유지 (롤백 대비)</li>
</ol>
<p><strong>Deployment를 삭제하면 → 하위 ReplicaSet과 Pod도 모두 삭제됨.</strong></p>
<h3 id="156-service--고정된-네트워크-진입점">15.6 Service — 고정된 네트워크 진입점</h3>
<p>Pod는 동적으로 생성/삭제되어 IP가 매번 변한다. Service는 Pod들에 대한 <strong>고정된 DNS 이름과 IP를 제공</strong>한다.</p>
<pre><code>Service (고정 IP/DNS)
    ↓ 트래픽 분산
Pod-1 (172.17.0.4)
Pod-2 (172.17.0.5)
Pod-3 (172.17.0.6)</code></pre><p>Pod가 재생성되어 IP가 바뀌어도 Service를 통해 접근하면 문제없다.</p>
<p><strong>Service 타입 3가지</strong>:</p>
<p><strong>① ClusterIP (기본값) — 클러스터 내부 전용</strong></p>
<pre><code class="language-yaml"># svc/svc-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
  name: svc-clusterip
spec:
  ports:
  - name: web-port
    port: 8080        # Service가 노출하는 포트
    targetPort: 80    # Pod의 실제 포트
  selector:
    app: webserver    # 이 레이블의 Pod에 트래픽 전달
  type: ClusterIP</code></pre>
<p>클러스터 내부에서만 접근 가능. 서비스 이름으로 DNS 조회 가능:</p>
<pre><code class="language-bash">curl svc-clusterip:8080</code></pre>
<p><strong>② NodePort — 노드 IP로 외부 접근</strong></p>
<pre><code class="language-yaml"># svc/svc-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: svc-nodeport
spec:
  ports:
  - name: web-port
    port: 8080
    targetPort: 80
    # nodePort: 30080   # 생략 시 30000-32767 범위에서 자동 할당
  selector:
    app: webserver
  type: NodePort</code></pre>
<pre><code class="language-bash"># 노드의 IP로 외부에서 접근
curl &lt;노드 Internal IP&gt;:&lt;NodePort&gt;</code></pre>
<p>모든 노드에서 같은 NodePort로 접근 가능.</p>
<p><strong>③ LoadBalancer — 퍼블릭 IP 자동 할당 (클라우드용)</strong></p>
<pre><code class="language-yaml"># svc/svc-lb.yaml
apiVersion: v1
kind: Service
metadata:
  name: svc-lb
spec:
  ports:
  - name: web-port
    port: 80
    targetPort: 80
  selector:
    app: webserver
  type: LoadBalancer</code></pre>
<p>AWS EKS, GKE, AKS 등 클라우드 환경에서 사용 시 클라우드 로드밸런서를 자동으로 프로비저닝한다.</p>
<p><strong>서비스 타입 비교</strong>:</p>
<table>
<thead>
<tr>
<th>타입</th>
<th>접근 범위</th>
<th>사용 케이스</th>
</tr>
</thead>
<tbody><tr>
<td>ClusterIP</td>
<td>클러스터 내부만</td>
<td>서비스 간 내부 통신 (DB, 백엔드)</td>
</tr>
<tr>
<td>NodePort</td>
<td>클러스터 외부 (노드 IP:포트)</td>
<td>개발·테스트 환경 임시 노출</td>
</tr>
<tr>
<td>LoadBalancer</td>
<td>인터넷 (퍼블릭 IP)</td>
<td>프로덕션 서비스 외부 노출</td>
</tr>
</tbody></table>
<h3 id="157-ingress--httphttps-경로-기반-라우팅">15.7 Ingress — HTTP/HTTPS 경로 기반 라우팅</h3>
<p>Service의 LoadBalancer 타입은 서비스마다 별도 로드밸런서를 생성해 비용이 많이 든다. <strong>Ingress</strong>는 하나의 진입점에서 URL 경로나 호스트 이름을 기준으로 여러 서비스에 라우팅한다.</p>
<pre><code>클라이언트
    ↓
Ingress (하나의 외부 IP / 로드밸런서)
    ├── /api/users   → users-service:80
    ├── /api/orders  → orders-service:80
    └── /            → frontend-service:80</code></pre><pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /api/users
        pathType: Prefix
        backend:
          service:
            name: users-service
            port:
              number: 80
      - path: /api/orders
        pathType: Prefix
        backend:
          service:
            name: orders-service
            port:
              number: 80</code></pre>
<p>Ingress가 동작하려면 <strong>Ingress Controller</strong>가 클러스터에 설치되어야 한다.</p>
<table>
<thead>
<tr>
<th>환경</th>
<th>Ingress Controller</th>
</tr>
</thead>
<tbody><tr>
<td>온프레미스 / 범용</td>
<td>Nginx Ingress Controller</td>
</tr>
<tr>
<td>AWS EKS</td>
<td>AWS Load Balancer Controller (ALB 자동 생성)</td>
</tr>
<tr>
<td>GKE</td>
<td>GCE Ingress Controller</td>
</tr>
<tr>
<td>Azure AKS</td>
<td>Azure Application Gateway Ingress</td>
</tr>
</tbody></table>
<pre><code class="language-bash">kubectl get ingress
kubectl describe ingress app-ingress</code></pre>
<h3 id="158-hpa--horizontal-pod-autoscaler">15.8 HPA — Horizontal Pod Autoscaler</h3>
<p>HPA는 CPU 사용률 또는 커스텀 지표를 기준으로 <strong>Pod 수를 자동으로 조절</strong>한다. Kubernetes 워크로드에 대한 수평 자동 확장이다.</p>
<pre><code>부하 증가 → Pod CPU 80% 초과
    ↓
HPA 감지 (15초 간격 평가)
    ↓
Deployment replicas 3 → 6 자동 조정
    ↓
부하 분산, CPU 하락
    ↓
CPU 50% 안정 → 일정 시간 후 replicas 다시 축소</code></pre><pre><code class="language-yaml">apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: nginx-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: test-nginx-deploy
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50   # CPU 50% 목표</code></pre>
<p>HPA가 동작하려면 Pod에 <code>resources.requests</code> 설정이 <strong>필수</strong>다:</p>
<pre><code class="language-yaml">resources:
  requests:
    cpu: &quot;100m&quot;      # 0.1 vCPU
    memory: &quot;128Mi&quot;
  limits:
    cpu: &quot;500m&quot;
    memory: &quot;256Mi&quot;</code></pre>
<pre><code class="language-bash"># HPA 생성 (명령어 방식)
kubectl autoscale deployment test-nginx-deploy --cpu-percent=50 --min=2 --max=10

# HPA 상태 확인
kubectl get hpa

# 상세 정보 (스케일 이벤트 확인)
kubectl describe hpa nginx-hpa</code></pre>
<p><strong>HPA vs EC2 Auto Scaling 비교</strong>:</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>EC2 Auto Scaling</th>
<th>Kubernetes HPA</th>
</tr>
</thead>
<tbody><tr>
<td>스케일 대상</td>
<td>EC2 인스턴스</td>
<td>Pod</td>
</tr>
<tr>
<td>소요 시간</td>
<td>수 분 (인스턴스 부팅 포함)</td>
<td>수 초~수십 초</td>
</tr>
<tr>
<td>비용 단위</td>
<td>인스턴스 시간</td>
<td>Pod 리소스 사용량</td>
</tr>
</tbody></table>
<h3 id="159-namespace--클러스터-내-논리적-격리">15.9 Namespace — 클러스터 내 논리적 격리</h3>
<p>Namespace는 하나의 클러스터를 여러 가상 클러스터로 분리하는 방법이다.</p>
<pre><code class="language-yaml"># ns/prod-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: production</code></pre>
<pre><code class="language-bash"># 기본 namespace의 Pod
kubectl get pods                           # default namespace
kubectl get pods --namespace kube-system   # 시스템 컴포넌트

# namespace 생성 후 리소스 배포
kubectl apply -f prod-ns.yaml
kubectl apply -f deploy-svc-ns.yaml
kubectl get po,svc -n production

# 같은 namespace 서비스 접근
curl svc-clusterip-ns:8080

# 다른 namespace 서비스 접근 (FQDN: 서비스명.네임스페이스.svc)
curl svc-clusterip-ns.production.svc:8080

# namespace 삭제 → 안의 모든 리소스 삭제됨
kubectl delete namespace production</code></pre>
<p>활용 예시:</p>
<ul>
<li><code>development</code>, <code>staging</code>, <code>production</code> 환경 분리</li>
<li>팀별 격리 (<code>team-a</code>, <code>team-b</code>)</li>
<li>시스템 컴포넌트 분리 (<code>kube-system</code>)</li>
</ul>
<h3 id="1510-configmap과-secret--설정-분리">15.10 ConfigMap과 Secret — 설정 분리</h3>
<p>컨테이너 이미지에 설정값을 하드코딩하면 환경마다 이미지를 새로 빌드해야 한다. <strong>ConfigMap</strong>으로 설정을 분리하면 이미지 변경 없이 설정만 바꿀 수 있다.</p>
<p><strong>ConfigMap 생성</strong>:</p>
<pre><code class="language-bash"># 명령어로 생성
kubectl create configmap log-level-configmap --from-literal LOG_LEVEL=DEBUG
kubectl create configmap start-k8s --from-literal k8s=kubernetes --from-literal container=docker

# 파일로 생성 (설정 파일 전체를 ConfigMap으로)
echo &quot;Hello, Kubernetes&quot; | sudo tee index.html
kubectl create configmap index-file --from-file index.html

# ConfigMap 조회
kubectl get configmap
kubectl describe configmap log-level-configmap
kubectl get configmap log-level-configmap -o yaml</code></pre>
<p><strong>ConfigMap을 Pod에 주입하는 방법</strong>:</p>
<p>방법 1 — 환경 변수로 전달:</p>
<pre><code class="language-yaml"># 전체 ConfigMap을 환경 변수로
spec:
  containers:
  - name: my-container
    image: busybox
    args: [&#39;tail&#39;, &#39;-f&#39;, &#39;/dev/null&#39;]
    envFrom:
    - configMapRef:
        name: log-level-configmap  # ConfigMap 전체를 env로
    - configMapRef:
        name: start-k8s</code></pre>
<pre><code class="language-yaml"># 특정 키만 골라서 환경 변수로
spec:
  containers:
  - name: my-container
    env:
    - name: LOG_LEVEL
      valueFrom:
        configMapKeyRef:
          name: log-level-configmap
          key: LOG_LEVEL</code></pre>
<p>방법 2 — 볼륨으로 파일 마운트:</p>
<pre><code class="language-yaml">spec:
  containers:
  - name: nginx
    volumeMounts:
    - name: config-vol
      mountPath: /etc/config
  volumes:
  - name: config-vol
    configMap:
      name: start-k8s</code></pre>
<p>ConfigMap의 각 key-value가 파일 이름과 내용으로 마운트된다.</p>
<p><strong>Secret</strong> — 민감 정보 보관:</p>
<pre><code class="language-bash"># 비밀번호 저장
kubectl create secret generic my-password --from-literal password=dkagh123.

# 파일로 생성
echo mypassword | sudo tee pw1 &gt; /dev/null
kubectl create secret generic our-password --from-file pw1

# Secret 확인 (값은 Base64 인코딩)
kubectl get secrets my-password -o yaml

# Base64 디코딩으로 실제 값 확인
echo &lt;base64값&gt; | base64 -d</code></pre>
<p>Secret은 ConfigMap과 동일하게 환경 변수 또는 볼륨으로 Pod에 주입한다.</p>
<p>Docker private registry 인증 정보도 Secret으로 관리:</p>
<pre><code class="language-bash">kubectl create secret docker-registry registry-auth \
  --docker-username=myuser \
  --docker-password=mypassword \
  --docker-server=registry.example.com</code></pre>
<h3 id="1511-volume--데이터-영속성">15.11 Volume — 데이터 영속성</h3>
<p>컨테이너가 죽으면 컨테이너 내부 데이터는 사라진다. Volume을 사용하면 데이터를 영속적으로 저장할 수 있다.</p>
<p><strong>emptyDir — 임시 공유 볼륨</strong></p>
<p>Pod가 실행되는 동안만 존재하는 임시 디렉터리. 같은 Pod의 여러 컨테이너가 데이터를 공유할 때 사용.</p>
<pre><code class="language-bash"># content-creator 컨테이너에서 파일 생성
kubectl exec -it emptydir-pod -c content-creator -- sh
echo &quot;hello, k8s&quot; &gt; /data/test.html
exit

# apache-webserver 컨테이너에서 같은 파일 확인
kubectl exec -it emptydir-pod --container apache-webserver -- cat htdocs/test.html
# 출력: hello, k8s</code></pre>
<p><strong>hostPath — 노드 파일시스템 마운트</strong></p>
<p>호스트 노드의 특정 디렉터리를 Pod에 마운트. 로그 수집, 노드 파일 접근 등에 사용.</p>
<p>주의: Pod가 다른 노드에 스케줄되면 데이터가 보이지 않을 수 있음.</p>
<p><strong>PV/PVC — 클러스터 수준 영구 스토리지</strong></p>
<pre><code>관리자(Admin)                사용자(Developer)
     │                              │
     ▼                              ▼
PV 생성 (실제 스토리지 선언)   PVC 생성 (스토리지 요청)
                                     │
                               PV와 자동 바인딩
                                     │
                                  Pod에서 사용</code></pre><pre><code class="language-yaml"># PV 정의 (관리자가 스토리지 선언)
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv
spec:
  capacity:
    storage: 1G
  accessModes:
  - ReadWriteOnce      # 단일 노드 읽기/쓰기
  hostPath:
    path: /pv          # 실제 저장 경로</code></pre>
<pre><code class="language-yaml"># PVC 정의 (사용자가 스토리지 요청)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1G      # 조건에 맞는 PV가 자동 바인딩</code></pre>
<pre><code class="language-bash"># PV, PVC, Pod 생성 후 상태 확인
kubectl get pv,pvc,po

# Pod 내부에서 파일 생성
kubectl exec -it persistentvolume --container container -- /bin/bash
cd /mount1
echo &quot;hello pv&quot; &gt;&gt; test.txt
exit

# Pod를 삭제해도 PV의 데이터는 유지됨
kubectl delete pod persistentvolume</code></pre>
<p><strong>accessModes 종류</strong>:</p>
<table>
<thead>
<tr>
<th>모드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>ReadWriteOnce</code> (RWO)</td>
<td>단일 노드에서 읽기/쓰기</td>
</tr>
<tr>
<td><code>ReadOnlyMany</code> (ROX)</td>
<td>여러 노드에서 읽기만</td>
</tr>
<tr>
<td><code>ReadWriteMany</code> (RWX)</td>
<td>여러 노드에서 읽기/쓰기 (NFS 등 필요)</td>
</tr>
</tbody></table>
<hr>
<h2 id="16-aws-cicd--코드-커밋부터-자동-배포까지">16. AWS CI/CD — 코드 커밋부터 자동 배포까지</h2>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20240219101620/aws-Pipeline.webp" alt="AWS CodePipeline — Source, Build, Deploy 단계 자동화 흐름"></p>
<h3 id="161-aws-cicd-서비스-구성">16.1 AWS CI/CD 서비스 구성</h3>
<table>
<thead>
<tr>
<th>서비스</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><strong>AWS CodeCommit</strong></td>
<td>관리형 Git 저장소</td>
<td>GitHub로 대체 가능</td>
</tr>
<tr>
<td><strong>AWS CodeBuild</strong></td>
<td>코드 빌드·테스트 자동화</td>
<td>buildspec.yml로 정의</td>
</tr>
<tr>
<td><strong>AWS CodeDeploy</strong></td>
<td>EC2, Lambda, ECS에 자동 배포</td>
<td>appspec.yml로 정의</td>
</tr>
<tr>
<td><strong>AWS CodePipeline</strong></td>
<td>위 서비스들을 연결하는 파이프라인 오케스트레이터</td>
<td>소스 변경 감지 → 트리거</td>
</tr>
</tbody></table>
<pre><code>[소스 (S3 / GitHub)]
    ↓ 변경 감지 (CodePipeline이 트리거)
[CodeBuild] ← buildspec.yml
    ↓ 빌드 아티팩트 (dist/, .jar 등)
[CodeDeploy] ← appspec.yml
    ↓
[EC2 / S3 / ECS / Lambda]</code></pre><h3 id="162-lab-10--aws-codepipeline-기본-s3-→-s3-배포">16.2 Lab 10 — AWS CodePipeline 기본 (S3 → S3 배포)</h3>
<p>가장 단순한 파이프라인: Source S3 버킷에 파일을 올리면 → Prod S3 버킷(정적 웹사이트)에 자동 배포.</p>
<p><strong>구성 요소</strong>:</p>
<ul>
<li><strong>Source 버킷</strong>: <code>user-source-website</code> — 버전 관리 활성화 (트리거에 필요)</li>
<li><strong>Prod 버킷</strong>: <code>user-prod-website</code> — 정적 웹사이트 호스팅 활성화, 퍼블릭 액세스 허용</li>
</ul>
<p><strong>파이프라인 설정 핵심</strong>:</p>
<ul>
<li>소스 공급자: Amazon S3</li>
<li>배포 공급자: Amazon S3</li>
<li>&quot;배포하기 전에 파일 압축 풀기&quot; 체크</li>
<li>표준 ACL: <code>public-read</code> (정적 웹사이트 공개 접근용)</li>
</ul>
<p><strong>파이프라인 동작 흐름</strong>:</p>
<pre><code>Source 버킷에 test-website.zip 업로드
    → CodePipeline 자동 감지 (S3 버전 변경 이벤트)
        → Deploy: Prod 버킷에 압축 풀어 배포
            → 웹사이트 자동 업데이트</code></pre><p>파일 압축 시 주의사항:</p>
<pre><code>✗ 잘못된 방법: test-website 폴더를 통째로 압축
  → 압축 해제 시 또 폴더가 생겨 경로 오류

✓ 올바른 방법: 폴더 안으로 들어가 index.html, error.html 파일 선택 후 압축
  → 압축 파일 최상위에 바로 index.html이 있어야 함</code></pre><h3 id="163-lab-11--github-저장소를-소스로-활용">16.3 Lab 11 — GitHub 저장소를 소스로 활용</h3>
<p>소스를 S3 대신 GitHub으로 변경하면 <code>git push</code>만으로 파이프라인이 자동 실행된다.</p>
<p><strong>GitHub 설정</strong>:</p>
<ol>
<li>Repository 생성 (Private)</li>
<li>Personal Access Token 생성 (scopes: <code>repo</code>)</li>
<li>AWS CodePipeline에서 GitHub App 연결</li>
</ol>
<p><strong>로컬 → GitHub → CodePipeline 흐름</strong>:</p>
<pre><code class="language-bash"># 처음 연결
git init
git add .
git commit -m &quot;first commit&quot;
git remote add origin https://github.com/&lt;계정&gt;/test-website-repo.git
git branch -M main
git push -u origin main

# 이후 변경사항 → 파이프라인 자동 트리거
git commit -a -m &quot;v2.0 update&quot;
git push origin main</code></pre>
<p>GitHub 웹 UI에서 파일을 직접 편집하고 커밋해도 파이프라인이 트리거된다.</p>
<p><strong>GitHub App 연결 vs Personal Access Token</strong>:</p>
<ul>
<li>GitHub App: 더 안전하고 세밀한 권한 관리 가능 (권장)</li>
<li>Personal Access Token: 간편하지만 관리 주의 필요</li>
</ul>
<h3 id="164-lab-12--aws-codebuild로-빌드-자동화">16.4 Lab 12 — AWS CodeBuild로 빌드 자동화</h3>
<p>Angular 같은 프레임워크는 소스 코드를 바로 배포할 수 없다. 빌드 과정이 필요하다. <code>buildspec.yml</code> 파일로 빌드 방법을 정의한다.</p>
<p><strong>buildspec.yml 구조</strong>:</p>
<pre><code class="language-yaml">version: 0.2

phases:
  install:                      # 빌드 환경 설정
    runtime-versions:
      nodejs: 20                # Node.js 20 사용
    commands:
      - npm install -g @angular/cli@17  # Angular CLI 전역 설치

  pre_build:                    # 빌드 전 준비
    commands:
      - npm install             # 프로젝트 의존성 설치

  build:                        # 실제 빌드
    commands:
      - ng build -c production  # 프로덕션 빌드
      - echo &quot;Build completed.&quot;
    finally:                    # 성공/실패 무관 항상 실행
      - echo This is the finally block execution!

artifacts:                      # 배포할 결과물
  base-directory: dist/my-angular-project
  files:
    - &#39;**/*&#39;                    # 빌드 결과물 전체</code></pre>
<p><strong>buildspec.yml 위치</strong>: 프로젝트 루트에 있어야 CodeBuild가 자동으로 찾음</p>
<p><strong>phases 종류</strong>:</p>
<table>
<thead>
<tr>
<th>Phase</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>install</code></td>
<td>런타임 환경, 도구 설치</td>
</tr>
<tr>
<td><code>pre_build</code></td>
<td>빌드 전 작업 (의존성 설치, 인증 등)</td>
</tr>
<tr>
<td><code>build</code></td>
<td>실제 빌드, 테스트</td>
</tr>
<tr>
<td><code>post_build</code></td>
<td>빌드 후 작업 (알림, 정리 등)</td>
</tr>
</tbody></table>
<p>파이프라인 구성:</p>
<pre><code>[GitHub]
    ↓ push 이벤트
[Source 단계: GitHub 소스 체크아웃]
    ↓
[Build 단계: CodeBuild]
    buildspec.yml 실행:
    1. Node.js 20 설치
    2. npm install
    3. ng build -c production
    ↓ dist/ 아티팩트
[Deploy 단계: S3]
    ↓
[정적 웹사이트 배포]</code></pre><p>첫 빌드 시 buildspec.yml이 없으면 Build 단계에서 실패한다. 로그를 확인해 실패 원인을 파악하고, 파일을 추가해 다시 push하면 된다.</p>
<h3 id="165-lab-13--aws-codedeploy로-ec2-인스턴스에-배포">16.5 Lab 13 — AWS CodeDeploy로 EC2 인스턴스에 배포</h3>
<p>S3가 아닌 EC2 인스턴스에 빌드 결과물을 배포할 때는 <strong>CodeDeploy</strong>를 사용한다.</p>
<p><strong>appspec.yml</strong> — 배포 명세서:</p>
<pre><code class="language-yaml">version: 0.0
os: linux

files:
  - source: dist/my-angular-project    # 빌드 아티팩트의 경로
    destination: /var/www/my-angular-project  # EC2에 복사할 경로

permissions:
  - object: /var/www/my-angular-project
    pattern: &#39;**&#39;
    mode: &#39;0755&#39;       # 실행 권한
    owner: root
    group: root
    type:
      - file
      - directory

hooks:
  ApplicationStart:
    - location: deploy-scripts/application-start-hook.sh
      timeout: 300     # 300초 제한</code></pre>
<p><strong>배포 훅(Hooks)</strong> — 배포 생명주기:</p>
<pre><code>ApplicationStop          ← 현재 실행 중인 앱 종료
BeforeInstall            ← 파일 복사 전 준비 (의존성 설치 등)
AfterInstall             ← 파일 복사 후 설정 (설정 파일 적용 등)
ApplicationStart   ←────  현재 실습에서 사용
ValidateService          ← 배포 성공 검증 (헬스 체크 등)</code></pre><p>각 훅에 셸 스크립트를 연결해 세밀한 배포 제어가 가능하다.</p>
<p><strong>CodeDeploy 작동 요건</strong>:</p>
<ul>
<li>EC2 인스턴스에 <strong>CodeDeploy Agent</strong> 설치</li>
<li>EC2 인스턴스에 <strong>IAM 역할</strong> 부여 (S3 아티팩트 접근 권한)</li>
<li>CodeDeploy 서비스에도 <strong>IAM 서비스 역할</strong> 필요</li>
</ul>
<p><strong>전체 Lab 12+13 파이프라인 흐름</strong>:</p>
<pre><code>GitHub push
    ↓
CodePipeline 트리거
    ↓
[Source] GitHub에서 소스 체크아웃
    ↓
[Build] CodeBuild + buildspec.yml
    → npm install → ng build → S3에 빌드 아티팩트 저장
    ↓
[Deploy] CodeDeploy + appspec.yml
    → S3에서 아티팩트 다운로드
    → 배포 훅 실행 (ApplicationStop → BeforeInstall → AfterInstall → ApplicationStart)
    → EC2 /var/www/my-angular-project/ 에 파일 배포
    ↓
[서비스 정상 확인]</code></pre><hr>
<h2 id="17-소프트웨어-배포-전략">17. 소프트웨어 배포 전략</h2>
<p>새 버전을 어떻게 기존 운영 환경에 반영할지 결정하는 방법론이다. <strong>다운타임</strong>, <strong>롤백 속도</strong>, <strong>비용</strong>, <strong>위험도</strong> 측면에서 각각 다른 트레이드오프를 가진다.</p>
<h3 id="171-in-place-배포-rolling-배포">17.1 In-Place 배포 (Rolling 배포)</h3>
<p>기존 인스턴스를 교체 없이 순차적으로 새 버전을 덮어쓴다.</p>
<pre><code>배포 전:   [v1][v1][v1][v1]
배포 중:   [v2][v1][v1][v1]  ← v1, v2 혼재
           [v2][v2][v1][v1]
배포 완료: [v2][v2][v2][v2]</code></pre><p>동시 배포 비율 제어(<code>MinimumHealthyHosts</code> 설정):</p>
<ul>
<li><code>AllAtOnce</code>: 전체 동시 배포 (가장 빠름, 다운타임 위험)</li>
<li><code>HalfAtATime</code>: 절반씩 배포</li>
<li><code>OneAtATime</code>: 1개씩 배포 (가장 안전, 가장 느림)</li>
</ul>
<table>
<thead>
<tr>
<th>다운타임</th>
<th>롤백</th>
<th>비용</th>
<th>버전 혼재</th>
</tr>
</thead>
<tbody><tr>
<td>없음</td>
<td>느림 (전체 재배포)</td>
<td>없음</td>
<td>있음</td>
</tr>
</tbody></table>
<h3 id="172-bluegreen-배포">17.2 Blue/Green 배포</h3>
<p>현재 운영 환경(Blue)과 동일한 새 환경(Green)을 별도로 구성한 뒤 트래픽을 한 번에 전환한다.</p>
<pre><code>Blue (현재):  [v1][v1][v1][v1]  ← 전환 전 100% 트래픽
Green (신버전): [v2][v2][v2][v2]  ← 검증 완료 대기

ALB 전환:  Blue 100% → Green 100%  (순간 전환, 다운타임 없음)
롤백:      Green → Blue 즉시 재전환</code></pre><p>CodeDeploy Blue/Green 설정 포인트:</p>
<ul>
<li>Green 환경 구성: ASG에서 자동 프로비저닝 또는 수동 지정</li>
<li>트래픽 재라우팅: 즉시 또는 N분 대기 후 자동 전환</li>
<li>Blue 환경 처리: 전환 성공 후 N분 뒤 자동 종료</li>
</ul>
<table>
<thead>
<tr>
<th>다운타임</th>
<th>롤백</th>
<th>비용</th>
<th>버전 혼재</th>
</tr>
</thead>
<tbody><tr>
<td>없음</td>
<td>즉시</td>
<td>높음 (인스턴스 2배)</td>
<td>없음</td>
</tr>
</tbody></table>
<h3 id="173-canary-배포">17.3 Canary 배포</h3>
<p>신버전을 전체 트래픽의 일부(소수)에만 먼저 노출해 이상 여부를 확인하고 점진적으로 확대한다.</p>
<pre><code>1단계 (Canary):   [v2][v1][v1][v1][v1][v1][v1][v1][v1][v1]
                  ↑ 10% 트래픽 → v2 (모니터링)
2단계 (확대):     v2 비율 50%
3단계 (완료):     v2 100%</code></pre><p>AWS CodeDeploy 사전 정의 Canary 설정:</p>
<table>
<thead>
<tr>
<th>설정 이름</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><code>Canary10Percent5Minutes</code></td>
<td>10% 먼저, 5분 뒤 나머지 90%</td>
</tr>
<tr>
<td><code>Canary10Percent10Minutes</code></td>
<td>10% 먼저, 10분 뒤 나머지 90%</td>
</tr>
<tr>
<td><code>Canary10Percent15Minutes</code></td>
<td>10% 먼저, 15분 뒤 나머지 90%</td>
</tr>
<tr>
<td><code>Canary10Percent30Minutes</code></td>
<td>10% 먼저, 30분 뒤 나머지 90%</td>
</tr>
</tbody></table>
<p>Lambda, ECS에서 주로 활용됨.</p>
<table>
<thead>
<tr>
<th>다운타임</th>
<th>롤백</th>
<th>비용</th>
<th>버전 혼재</th>
</tr>
</thead>
<tbody><tr>
<td>없음</td>
<td>빠름</td>
<td>낮음</td>
<td>있음</td>
</tr>
</tbody></table>
<h3 id="174-linear-배포-점진적-배포">17.4 Linear 배포 (점진적 배포)</h3>
<p>일정 시간 간격으로 동일한 비율씩 신버전 트래픽을 단계적으로 증가시킨다.</p>
<pre><code>0분:  v2 → 10%
10분: v2 → 20%
20분: v2 → 30%
...
90분: v2 → 100%</code></pre><p>Canary와 차이: Canary는 &quot;소량 먼저 → 한 번에 나머지&quot;, Linear는 &quot;균등하게 점진 증가&quot;.</p>
<p>AWS CodeDeploy 사전 정의 Linear 설정:</p>
<table>
<thead>
<tr>
<th>설정 이름</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><code>Linear10PercentEvery1Minute</code></td>
<td>1분마다 10%씩 증가</td>
</tr>
<tr>
<td><code>Linear10PercentEvery2Minutes</code></td>
<td>2분마다 10%씩 증가</td>
</tr>
<tr>
<td><code>Linear10PercentEvery3Minutes</code></td>
<td>3분마다 10%씩 증가</td>
</tr>
<tr>
<td><code>Linear10PercentEvery10Minutes</code></td>
<td>10분마다 10%씩 증가</td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>다운타임</th>
<th>롤백</th>
<th>비용</th>
<th>버전 혼재</th>
</tr>
</thead>
<tbody><tr>
<td>없음</td>
<td>빠름</td>
<td>낮음</td>
<td>있음</td>
</tr>
</tbody></table>
<h3 id="175-ab-testing-배포">17.5 A/B Testing 배포</h3>
<p>동일 기능의 두 버전을 서로 다른 사용자 그룹에 동시에 노출해 <strong>비즈니스 지표(전환율, 클릭률 등)를 비교</strong>한다.</p>
<pre><code>사용자 그룹 A (50%) → 버전 A (기존 UI)  전환율: 3.2%
사용자 그룹 B (50%) → 버전 B (새 UI)   전환율: 4.7% ← 더 높음
→ 버전 B를 전체 배포 결정</code></pre><p>Canary와 차이: Canary는 기술적 안정성 검증, A/B Testing은 사용자 행동 기반 <strong>비즈니스 최적화</strong>.</p>
<p>AWS 구현 방법:</p>
<ul>
<li>ALB 가중치 라우팅 + 헤더/쿠키 기반 Target Group 분리</li>
<li>CloudFront + Lambda@Edge: 요청 헤더 기반 오리진 분기</li>
<li><strong>AWS AppConfig</strong>: 기능 플래그(Feature Flag)로 특정 사용자에게만 기능 활성화</li>
</ul>
<h3 id="176-recreate-배포-all-at-once">17.6 Recreate 배포 (All-at-Once)</h3>
<p>기존 버전을 모두 종료한 뒤 새 버전을 전체 배포. <strong>다운타임 발생</strong>.</p>
<pre><code>[v1][v1][v1] → 전체 종료 → (다운타임) → [v2][v2][v2]</code></pre><p>적합한 경우: 개발/스테이징 환경, DB 스키마 변경으로 v1/v2 혼재가 불가능한 상황.</p>
<table>
<thead>
<tr>
<th>다운타임</th>
<th>롤백</th>
<th>비용</th>
<th>버전 혼재</th>
</tr>
</thead>
<tbody><tr>
<td><strong>있음</strong></td>
<td>중간</td>
<td>없음</td>
<td>없음</td>
</tr>
</tbody></table>
<h3 id="177-shadow-배포-트래픽-미러링">17.7 Shadow 배포 (트래픽 미러링)</h3>
<p>실제 프로덕션 트래픽을 신버전에 <strong>복사(미러링)</strong>해서 응답은 사용자에게 보내지 않고 신버전의 동작만 검증한다.</p>
<pre><code>사용자 요청
    ├── v1 (운영) → 실제 응답 사용자에게 반환
    └── v2 (Shadow) → 요청만 전달, 응답은 버림 (사용자 영향 없음)

v2 모니터링:
- 응답 시간 비교
- 에러 발생 여부
- 데이터 처리 정확성</code></pre><p>AWS 구현: ALB 미러링 규칙, Istio/AWS App Mesh 트래픽 미러링 기능 활용.</p>
<table>
<thead>
<tr>
<th>다운타임</th>
<th>롤백</th>
<th>비용</th>
<th>사용자 영향</th>
</tr>
</thead>
<tbody><tr>
<td>없음</td>
<td>해당 없음</td>
<td>높음 (v2 인프라)</td>
<td><strong>없음</strong></td>
</tr>
</tbody></table>
<h3 id="178-배포-전략-종합-비교">17.8 배포 전략 종합 비교</h3>
<table>
<thead>
<tr>
<th>전략</th>
<th>다운타임</th>
<th>롤백 속도</th>
<th>추가 비용</th>
<th>버전 혼재</th>
<th>주요 사용 환경</th>
</tr>
</thead>
<tbody><tr>
<td>In-Place (Rolling)</td>
<td>없음</td>
<td>느림</td>
<td>없음</td>
<td>있음</td>
<td>일반 EC2, 비용 절감</td>
</tr>
<tr>
<td>Blue/Green</td>
<td>없음</td>
<td><strong>즉시</strong></td>
<td>높음</td>
<td>없음</td>
<td>프로덕션, 즉시 롤백 필요</td>
</tr>
<tr>
<td>Canary</td>
<td>없음</td>
<td>빠름</td>
<td>낮음</td>
<td>있음</td>
<td>Lambda, ECS, 리스크 최소화</td>
</tr>
<tr>
<td>Linear</td>
<td>없음</td>
<td>빠름</td>
<td>낮음</td>
<td>있음</td>
<td>Lambda, ECS 점진 배포</td>
</tr>
<tr>
<td>A/B Testing</td>
<td>없음</td>
<td>빠름</td>
<td>중간</td>
<td>있음</td>
<td>기능 비교, UX 최적화</td>
</tr>
<tr>
<td>Recreate</td>
<td><strong>있음</strong></td>
<td>중간</td>
<td>없음</td>
<td>없음</td>
<td>개발/스테이징</td>
</tr>
<tr>
<td>Shadow</td>
<td>없음</td>
<td>—</td>
<td>높음</td>
<td>없음</td>
<td>신버전 사전 성능 검증</td>
</tr>
</tbody></table>
<p><strong>AWS CodeDeploy 대상별 지원 배포 방식</strong>:</p>
<table>
<thead>
<tr>
<th>배포 대상</th>
<th>지원 배포 유형</th>
</tr>
</thead>
<tbody><tr>
<td>EC2 / On-Premises</td>
<td>In-Place, Blue/Green</td>
</tr>
<tr>
<td>AWS Lambda</td>
<td>Canary, Linear, AllAtOnce</td>
</tr>
<tr>
<td>Amazon ECS</td>
<td>Canary, Linear, AllAtOnce (Blue/Green 기반)</td>
</tr>
</tbody></table>
<hr>
<h2 id="18-서비스-안정성--고가용성과-무중단-배포">18. 서비스 안정성 — 고가용성과 무중단 배포</h2>
<h3 id="181-고가용성-3-tier-아키텍처">18.1 고가용성 3-Tier 아키텍처</h3>
<p>실제 프로덕션 환경의 기본 아키텍처:</p>
<pre><code>인터넷
    ↓
Route53 (DNS, 장애 조치 라우팅)
    ↓
CloudFront (CDN, 정적 콘텐츠 캐싱)
    ↓
Elastic Load Balancer (ELB)
    ↓ ↓
┌────────────────────────────────────────┐
│         퍼블릭 서브넷                  │
│  [웹/앱 서버 Auto Scaling Group]       │
│  AZ-1: EC2 × 2                        │
│  AZ-2: EC2 × 2                        │
└────────────────────────────────────────┘
    ↓ ↓
┌────────────────────────────────────────┐
│         프라이빗 서브넷                │
│  [RDS Multi-AZ]                        │
│  AZ-1: Primary DB + Read Replica      │
│  AZ-2: Standby DB + Read Replica      │
└────────────────────────────────────────┘</code></pre><p>각 컴포넌트 역할:</p>
<ul>
<li><strong>Route53</strong>: DNS 기반 장애 조치. 헬스 체크 실패 시 다른 리전으로 트래픽 전환</li>
<li><strong>CloudFront</strong>: 정적 콘텐츠(이미지, JS, CSS)를 엣지 로케이션에서 서빙</li>
<li><strong>ELB</strong>: 여러 EC2 인스턴스로 트래픽 분산, 헬스 체크로 비정상 인스턴스 제외</li>
<li><strong>Auto Scaling</strong>: CPU/메모리 지표에 따라 EC2 인스턴스 수 자동 조절</li>
<li><strong>RDS Multi-AZ</strong>: 프라이머리 장애 시 스탠바이로 자동 전환</li>
</ul>
<h3 id="182-dr재해-복구-전략--rpo와-rto">18.2 DR(재해 복구) 전략 — RPO와 RTO</h3>
<p><strong>RPO (Recovery Point Objective)</strong>: 장애 발생 시 얼마나 최근 데이터까지 복구할 수 있는가
→ 데이터 백업 주기를 결정</p>
<p><strong>RTO (Recovery Time Objective)</strong>: 장애 발생 시 얼마나 빨리 서비스를 복구해야 하는가
→ 복구 전략의 비용을 결정</p>
<pre><code>재해 발생 시각
    │
    │←─── RPO ───→│
    │              │
    │    마지막     │
    │    백업 시점  │
                   │
                   │←──── RTO ─────→│
                                     │
                               서비스 복구</code></pre><p><strong>4가지 DR 전략 비교</strong>:</p>
<table>
<thead>
<tr>
<th>전략</th>
<th>RPO</th>
<th>RTO</th>
<th>비용</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>백업 및 복원</strong></td>
<td>시간 단위</td>
<td>시간 단위</td>
<td>매우 낮음</td>
<td>스냅샷 백업, 장애 시 수동 재구축</td>
</tr>
<tr>
<td><strong>파일럿 라이트</strong></td>
<td>10분 단위</td>
<td>10분 단위</td>
<td>낮음</td>
<td>핵심 서비스(DB)만 상시 운영</td>
</tr>
<tr>
<td><strong>웜 스탠바이</strong></td>
<td>분 단위</td>
<td>분 단위</td>
<td>중간</td>
<td>모든 계층 축소 운영, 장애 시 확장</td>
</tr>
<tr>
<td><strong>멀티 사이트 Active/Active</strong></td>
<td>실시간</td>
<td>실시간</td>
<td>매우 높음</td>
<td>두 리전 동시 운영, 즉각 전환</td>
</tr>
</tbody></table>
<p><strong>전략 상세 설명</strong>:</p>
<p><strong>① 백업 및 복원</strong></p>
<pre><code>프로덕션 환경
    → 주기적 스냅샷 (S3, EBS, RDS Snapshot)
        → 장애 발생
            → CloudFormation으로 인프라 재구축
                → 백업에서 데이터 복원</code></pre><p>적합한 경우: 비용 최우선, 중요도 낮은 시스템</p>
<p><strong>② 파일럿 라이트</strong></p>
<pre><code>평상시:
    DR 리전: DB만 소규모 운영 (프라이머리와 복제)

장애 시:
    DR 리전: 앱 서버, 로드밸런서 신속 가동
    Route53으로 트래픽 전환</code></pre><p>적합한 경우: 데이터는 중요하지만 앱 서버는 빠르게 재구축 가능한 경우</p>
<p><strong>③ 웜 스탠바이</strong></p>
<pre><code>평상시:
    DR 리전: 모든 계층이 축소 규모로 상시 운영
    (웹 × 1, 앱 × 1, DB Standby)

장애 시:
    Auto Scaling으로 전체 용량으로 확장
    Route53으로 트래픽 전환</code></pre><p>적합한 경우: 빠른 복구가 필요하지만 완전 Active/Active 비용을 감당하기 어려운 경우</p>
<p><strong>④ 멀티 사이트 Active/Active</strong></p>
<pre><code>평상시:
    리전 A: 전체 용량으로 서비스 중
    리전 B: 전체 용량으로 서비스 중 (트래픽 분산)
    두 리전 간 Aurora Global Database로 실시간 복제

장애 시:
    Global Accelerator가 자동으로 정상 리전으로 100% 전환
    RTO: 거의 0 (실시간)</code></pre><p>적합한 경우: 금융, 의료, 글로벌 서비스 — 다운타임이 허용되지 않는 경우</p>
<hr>
<h2 id="전체-아키텍처-흐름-요약">전체 아키텍처 흐름 요약</h2>
<p>코드 커밋부터 사용자 요청 처리까지의 전체 흐름:</p>
<pre><code>개발자 git push
    ↓
CodePipeline 트리거 (GitHub 웹훅 / S3 버전 변경)
    ↓
[Source] 소스 체크아웃
    ↓
[Build] CodeBuild + buildspec.yml
    → 의존성 설치 → 빌드 → 단위 테스트 → 빌드 아티팩트 S3 저장
    ↓
[Deploy] CodeDeploy + appspec.yml
    → 배포 전략 선택 (Blue/Green / Canary / Rolling 등)
    → 헬스 체크 통과 후 트래픽 전환
    ↓
Route53 (DNS, 장애 조치 라우팅)
    ↓
CloudFront (CDN — 정적 콘텐츠 엣지 캐싱, HTTPS)
    ↓
ALB (L7 로드 밸런싱, Target Group 헬스 체크)
    ├── /api/* → EC2 Auto Scaling Group 또는 EKS/ECS
    └── /      → S3 정적 웹사이트
    ↓
애플리케이션 레이어 (EC2 / Kubernetes Pod / ECS Task)
    ↓
데이터 레이어
    ├── Aurora / RDS (관계형 DB, Multi-AZ 고가용성)
    ├── ElastiCache Redis (캐싱 — Lazy Loading / Write-Through)
    └── DynamoDB (NoSQL — 세션, 실시간 이벤트)</code></pre><p><strong>서비스 계층별 AWS 서비스 매핑</strong>:</p>
<table>
<thead>
<tr>
<th>계층</th>
<th>서비스</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>DNS</td>
<td>Route53</td>
<td>도메인 → IP 변환, 장애 조치</td>
</tr>
<tr>
<td>CDN</td>
<td>CloudFront</td>
<td>정적 콘텐츠 엣지 캐싱</td>
</tr>
<tr>
<td>스토리지</td>
<td>S3</td>
<td>정적 파일, 빌드 아티팩트, 백업</td>
</tr>
<tr>
<td>컴퓨팅</td>
<td>EC2 + Auto Scaling</td>
<td>가변 트래픽 자동 대응</td>
</tr>
<tr>
<td>로드 밸런싱</td>
<td>ALB / NLB</td>
<td>트래픽 분산, 헬스 체크</td>
</tr>
<tr>
<td>컨테이너</td>
<td>ECS / EKS</td>
<td>컨테이너 오케스트레이션</td>
</tr>
<tr>
<td>관계형 DB</td>
<td>RDS / Aurora</td>
<td>ACID 트랜잭션, Multi-AZ</td>
</tr>
<tr>
<td>NoSQL</td>
<td>DynamoDB</td>
<td>무제한 확장, 밀리초 응답</td>
</tr>
<tr>
<td>캐싱</td>
<td>ElastiCache</td>
<td>DB 부하 감소, 응답 속도 향상</td>
</tr>
<tr>
<td>보안</td>
<td>IAM + SCP</td>
<td>최소 권한 원칙 적용</td>
</tr>
<tr>
<td>CI/CD</td>
<td>CodePipeline + CodeBuild + CodeDeploy</td>
<td>배포 자동화</td>
</tr>
<tr>
<td>하이브리드</td>
<td>Direct Connect / VPN</td>
<td>온프레미스 연결</td>
</tr>
</tbody></table>
<hr>
<h2 id="생각-정리">생각 정리</h2>
<p>인프라 관점에서 보면 VPC가 네트워크 경계를 정의하고, ELB와 Auto Scaling이 트래픽 변화에 대응하며, RDS Multi-AZ와 DR 전략이 데이터 가용성을 보장한다. 그 위에 컨테이너와 Kubernetes를 올리면 배포 단위가 인스턴스에서 Pod로 내려오고, CI/CD 파이프라인이 이 전체 흐름을 자동화한다.
배포 전략을 깊게 들여다보면서 특히 인상적이었던 부분은, &quot;새 버전을 올린다&quot;는 행위 하나에도 다운타임 허용 여부, 롤백 속도, 비용, 버전 혼재 가능성을 모두 고려해 전략을 선택한다는 점이다. Blue/Green은 즉각적인 롤백이 가능한 대신 인프라 비용이 두 배가 되고, Canary는 리스크를 최소화하는 대신 운영 복잡도가 올라간다. 어떤 전략이 무조건 옳은 게 아니라, 서비스의 특성과 팀의 상황에 맞게 선택하는 것이 핵심이다.
클라우드는 가용성·확장성·보안·비용 최적화를 동시에 설계한다는 것을 실습을 진행하며 그리고 다음 프로젝트를 진행하며 많이 배운거같다.</p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<h3 id="aws-공식-문서">AWS 공식 문서</h3>
<ul>
<li><a href="https://docs.aws.amazon.com/ec2/latest/userguide/concepts.html">EC2</a> / <a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/what-is-amazon-ec2-auto-scaling.html">EC2 Auto Scaling</a></li>
<li><a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html">Amazon S3</a> / <a href="https://docs.aws.amazon.com/ebs/latest/userguide/what-is-ebs.html">Amazon EBS</a></li>
<li><a href="https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html">VPC</a></li>
<li><a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html">ALB</a> / <a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/network/introduction.html">NLB</a></li>
<li><a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html">Route53</a> / <a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html">CloudFront</a></li>
<li><a href="https://docs.aws.amazon.com/iam/latest/userguide/introduction.html">IAM</a> / <a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_introduction.html">AWS Organizations</a></li>
<li><a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Welcome.html">RDS</a> / <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/CHAP_AuroraOverview.html">Aurora</a> / <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html">DynamoDB</a> / <a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/WhatIs.html">ElastiCache</a></li>
<li><a href="https://docs.aws.amazon.com/directconnect/latest/UserGuide/Welcome.html">Direct Connect</a> / <a href="https://docs.aws.amazon.com/vpn/latest/s2svpn/VPC_VPN.html">Site-to-Site VPN</a></li>
<li><a href="https://docs.aws.amazon.com/AmazonECR/latest/userguide/what-is-ecr.html">ECR</a></li>
<li><a href="https://docs.aws.amazon.com/codepipeline/latest/userguide/welcome.html">CodePipeline</a> / <a href="https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html">CodeBuild buildspec</a> / <a href="https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file.html">CodeDeploy appspec</a></li>
<li><a href="https://docs.aws.amazon.com/codedeploy/latest/userguide/deployment-configurations.html">CodeDeploy 배포 구성</a></li>
<li><a href="https://awscli.amazonaws.com/v2/documentation/api/latest/index.html">AWS CLI 레퍼런스</a></li>
</ul>
<h3 id="kubernetes-공식-문서">Kubernetes 공식 문서</h3>
<ul>
<li><a href="https://kubernetes.io/ko/docs/concepts/workloads/pods/">Pod</a> / <a href="https://kubernetes.io/ko/docs/concepts/workloads/controllers/replicaset/">ReplicaSet</a> / <a href="https://kubernetes.io/ko/docs/concepts/workloads/controllers/deployment/">Deployment</a></li>
<li><a href="https://kubernetes.io/ko/docs/concepts/services-networking/service/">Service</a> / <a href="https://kubernetes.io/ko/docs/concepts/services-networking/ingress/">Ingress</a></li>
<li><a href="https://kubernetes.io/ko/docs/tasks/run-application/horizontal-pod-autoscale/">HorizontalPodAutoscaler</a></li>
<li><a href="https://kubernetes.io/ko/docs/concepts/configuration/configmap/">ConfigMap</a> / <a href="https://kubernetes.io/ko/docs/concepts/configuration/secret/">Secret</a></li>
<li><a href="https://kubernetes.io/ko/docs/concepts/storage/persistent-volumes/">PersistentVolume</a></li>
<li><a href="https://kubernetes.io/docs/reference/setup-tools/kubeadm/">kubeadm</a> / <a href="https://docs.tigera.io/calico/latest/about/">Calico CNI</a></li>
<li><a href="https://kubernetes.github.io/ingress-nginx/">Nginx Ingress Controller</a></li>
</ul>
<h3 id="참고-아티클">참고 아티클</h3>
<ul>
<li><a href="https://www.geeksforgeeks.org/devops/kubernetes-architecture/">GeeksforGeeks — Kubernetes Architecture</a></li>
<li><a href="https://www.geeksforgeeks.org/identity-and-access-management-iam-in-amazon-web-services-aws/">GeeksforGeeks — AWS IAM</a></li>
<li><a href="https://www.geeksforgeeks.org/cloud-computing/rds-read-replicas-how-it-is-different-from-rds-multi-az-deployment/">GeeksforGeeks — RDS Read Replicas vs Multi-AZ</a></li>
<li><a href="https://www.geeksforgeeks.org/operating-systems/difference-between-virtual-machines-and-containers/">GeeksforGeeks — Container vs VM</a></li>
<li><a href="https://www.geeksforgeeks.org/devops/amazon-ec2-instance-types/">GeeksforGeeks — AWS EC2 Instance Types</a></li>
<li><a href="https://www.geeksforgeeks.org/devops/how-to-build-a-ci-cd-pipeline-with-aws/">GeeksforGeeks — AWS CI/CD Pipeline</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 10주차-2 / Spring]]></title>
            <link>https://velog.io/@mi_nini/KT-A-10%EC%A3%BC%EC%B0%A8-2-Spring</link>
            <guid>https://velog.io/@mi_nini/KT-A-10%EC%A3%BC%EC%B0%A8-2-Spring</guid>
            <pubDate>Mon, 22 Jun 2026 09:54:06 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>2026 AX 아이디어 경진대회 서류 심사를 통과하여 본선 발표 대상자로 선정되었다. 이번 대회에서는 아이디어 기획 부문 9팀, 제품·서비스 부문 8팀, 자유분석 부문 9팀, 지정분석 부문 8팀 등 총 34개 팀이 본선에 진출하였다. 우리 팀은 지정분석 부문에 선정되어 8개 팀과 함께 발표를 진행하고 평가 받게 되었다. 발표 준비와 함께 제4회 문화체육관광 인공지능·데이터 활용 공모전도 병행하고 있었고, 해당 프로젝트를 Unity로 개발하다 보니 블로그 작성이 조금 늦어지게 되었다.
여기에 더해 코딩 테스트 공부와 다른 프로젝트들도 함께 진행하고 있어 생각보다 여유가 없었다. 그래도 그동안 경험한 내용들을 기록으로 남기는 것은 중요하다고 생각하기 때문에, 늦었지만 다시 블로그 작성을 시작해보려고 한다. 잠시 멈춰 있었던 만큼 앞으로는 학습 과정과 프로젝트 진행 내용, 공모전 준비 과정 등을 꾸준히 정리해 나갈 예정이다. (공모전 수상 및 다른 이슈가 있다면 다 정리해서 한번에 올려야지..) 다시 하나씩 기록해 보자.</p>
<hr>
<h2 id="1-spring-boot">1. Spring Boot</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/374626a5-bb2c-40cf-84ba-30cf931c767b/image.png" alt=""></p>
<h3 id="11-spring---spring-boot">1.1 Spring -&gt; Spring Boot</h3>
<p>Spring은 Java 엔터프라이즈 개발의 표준 프레임워크다. 그런데 초기 Spring은 XML 설정 파일, 의존성 버전 충돌, 외부 Tomcat 설치까지 시작 전에 할 일이 너무 많았다. <strong>Spring Boot</strong>는 그 불편함을 해소하기 위해 만들어진 확장판이다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Spring</th>
<th>Spring Boot</th>
</tr>
</thead>
<tbody><tr>
<td>설정 방식</td>
<td>XML / Java 직접 설정</td>
<td>Auto Configuration (자동)</td>
</tr>
<tr>
<td>서버</td>
<td>외부 Tomcat 설치 필요</td>
<td>내장 Tomcat 포함</td>
</tr>
<tr>
<td>의존성</td>
<td>버전 직접 관리</td>
<td>Starter로 묶음 관리</td>
</tr>
<tr>
<td>진입 장벽</td>
<td>높음</td>
<td>낮음</td>
</tr>
</tbody></table>
<blockquote>
<p>Spring Boot = Spring + 자동 설정 + 내장 서버 + Starter 의존성. Spring을 &quot;사람이 쓸 수 있게&quot; 만든 것이다.</p>
</blockquote>
<h3 id="12-ioc-container와-bean">1.2 IoC Container와 Bean</h3>
<p>Spring의 핵심 개념이다. <strong>IoC(Inversion of Control, 제어의 역전)</strong>은 객체 생성과 관리를 개발자가 아닌 Spring Container가 대신 담당하는 것이다.</p>
<pre><code class="language-java">// IoC 없이 — 개발자가 직접 생성·연결
BookRepository repo = new BookRepository();
BookService service = new BookService(repo);

// Spring IoC — Container가 생성하고 주입해준다
@Service
public class BookService {
    private final BookRepository bookRepository; // Spring이 알아서 넣어준다
}</code></pre>
<p>Spring Container가 관리하는 객체를 <strong>Bean</strong>이라고 부른다. <code>@ComponentScan</code>이 패키지를 스캔해서 아래 어노테이션이 붙은 클래스를 자동으로 Bean으로 등록한다.</p>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Component</code></td>
<td>범용 Bean 등록</td>
</tr>
<tr>
<td><code>@Service</code></td>
<td>비즈니스 로직 레이어 Bean</td>
</tr>
<tr>
<td><code>@Repository</code></td>
<td>DB 접근 레이어 Bean</td>
</tr>
<tr>
<td><code>@Controller</code> / <code>@RestController</code></td>
<td>웹 요청 처리 Bean</td>
</tr>
<tr>
<td><code>@Bean</code></td>
<td>메서드 반환값을 Bean으로 등록 (설정 클래스에서 사용)</td>
</tr>
</tbody></table>
<blockquote>
<p><code>@SpringBootApplication</code> 안에 <code>@ComponentScan</code>이 포함되어 있다. 그래서 같은 패키지 하위의 모든 <code>@Service</code>, <code>@Repository</code> 등이 자동으로 등록된다.</p>
</blockquote>
<h3 id="13-프로젝트-구조">1.3 프로젝트 구조</h3>
<p>Spring Initializr (start.spring.io)로 생성하면 기본 구조가 잡힌다.</p>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20260112154010794308/presentationlayer.webp" alt="Spring Boot Presentation Layer — Controller가 HTTP 요청을 받아 Service로 전달하는 계층"></p>
<pre><code>bookapp/
├── src/
│   ├── main/
│   │   ├── java/com/aivle/bookapp/
│   │   │   ├── BookappApplication.java   ← 진입점
│   │   │   ├── controller/               ← HTTP 요청 처리
│   │   │   ├── domain/                   ← 엔티티 (테이블 대응)
│   │   │   ├── repository/               ← DB 접근
│   │   │   ├── service/                  ← 비즈니스 로직
│   │   │   └── exception/                ← 예외 처리
│   │   └── resources/
│   │       └── application.yaml          ← 설정 파일
│   └── test/
└── build.gradle                          ← 의존성 관리</code></pre><p>이 구조가 <strong>Layered Architecture(계층형 아키텍처)</strong>다. 요청이 Controller → Service → Repository 순서로 흘러내려간다. 각 레이어가 자기 역할만 하도록 분리되어 있다.</p>
<h3 id="14-applicationyaml-설정">1.4 application.yaml 설정</h3>
<pre><code class="language-yaml">server:
  port: 8080

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:bookdb
    username: sa
    password: 1234
  h2:
    console:
      enabled: true
      path: /h2-console
  jpa:
    hibernate:
      ddl-auto: create   # 앱 시작 시 테이블 자동 생성
    show-sql: true       # 실행되는 SQL을 콘솔에 출력</code></pre>
<p>H2는 인메모리 데이터베이스다. 앱을 끄면 데이터가 사라지지만, MySQL 설치 없이 바로 쓸 수 있어서 개발·학습 단계에서 유용하게 사용된다. <code>/h2-console</code>에서 브라우저로 DB를 직접 확인할 수 있다.</p>
<p><code>ddl-auto</code> 옵션은 환경에 따라 달리 설정해야 한다. 잘못 설정하면 운영 DB 테이블이 날아갈 수 있다.</p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>동작</th>
<th>사용 시점</th>
</tr>
</thead>
<tbody><tr>
<td><code>create</code></td>
<td>시작 시 DROP → CREATE</td>
<td>개발 초기</td>
</tr>
<tr>
<td><code>create-drop</code></td>
<td>시작 시 CREATE, 종료 시 DROP</td>
<td>테스트</td>
</tr>
<tr>
<td><code>update</code></td>
<td>변경분만 ALTER</td>
<td>개발 중</td>
</tr>
<tr>
<td><code>validate</code></td>
<td>스키마 검증만 (변경 안 함)</td>
<td>스테이징</td>
</tr>
<tr>
<td><code>none</code></td>
<td>아무것도 안 함</td>
<td><strong>운영(Production)</strong></td>
</tr>
</tbody></table>
<blockquote>
<p>운영 환경에서는 반드시 <code>none</code>으로 설정한다. <code>create</code>를 운영에 쓰면 서버 재시작 때마다 모든 데이터가 삭제된다.</p>
</blockquote>
<hr>
<h2 id="2-첫-번째-api--restcontroller">2. 첫 번째 API — @RestController</h2>
<h3 id="21-springbootapplication">2.1 @SpringBootApplication</h3>
<pre><code class="language-java">@SpringBootApplication
public class BookappApplication {
    public static void main(String[] args) {
        SpringApplication.run(BookappApplication.class, args);
    }
}</code></pre>
<p><code>@SpringBootApplication</code>은 세 어노테이션의 합성이다.</p>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>@SpringBootConfiguration</code></td>
<td>Spring 설정 클래스로 등록</td>
</tr>
<tr>
<td><code>@EnableAutoConfiguration</code></td>
<td>의존성 기반 자동 설정 활성화</td>
</tr>
<tr>
<td><code>@ComponentScan</code></td>
<td>패키지 하위 Bean 자동 등록</td>
</tr>
</tbody></table>
<p>강의에서는 <code>@Bean CommandLineRunner</code>로 앱 시작 시 초기 데이터를 DB에 넣는 방법을 학습했다.</p>
<pre><code class="language-java">@Bean
CommandLineRunner init(BookRepository bookRepository) {
    return args -&gt; {
        Book b1 = new Book();
        b1.setTitle(&quot;자바의 정석&quot;);
        b1.setAuthor(&quot;남궁성&quot;);
        bookRepository.save(b1);

        Book b2 = new Book();
        b2.setTitle(&quot;Spring 입문&quot;);
        b2.setAuthor(&quot;임한울&quot;);
        bookRepository.save(b2);
    };
}</code></pre>
<p><code>CommandLineRunner</code>는 앱 구동이 완료된 직후 한 번 실행된다. 테스트 데이터 세팅이나 초기화 작업에 쓴다.</p>
<h3 id="22-restcontroller와-url-매핑">2.2 @RestController와 URL 매핑</h3>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20250226122317588619/Spring-Framework_.webp" alt="Spring Framework Architecture — Core Container, Web MVC, Data Access 모듈 구조"></p>
<pre><code class="language-java">@RestController
public class HelloController {

    @GetMapping(&quot;/hello/{name}&quot;)
    public String hello(@PathVariable String name) {
        return &quot;Hello, &quot; + name + &quot;!&quot;;
    }

    @GetMapping(&quot;/greet&quot;)
    public String greet(@RequestParam(defaultValue = &quot;en&quot;) String lang) {
        if (lang.equals(&quot;ko&quot;)) {
            return &quot;안녕하세요&quot;;
        }
        return &quot;Hello&quot;;
    }
}</code></pre>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>@RestController</code></td>
<td><code>@Controller</code> + <code>@ResponseBody</code>. 반환값을 JSON으로 직렬화</td>
</tr>
<tr>
<td><code>@GetMapping</code></td>
<td>HTTP GET 요청 처리</td>
</tr>
<tr>
<td><code>@PathVariable</code></td>
<td>URL 경로에서 값 추출 (<code>/hello/Alice</code> → <code>&quot;Alice&quot;</code>)</td>
</tr>
<tr>
<td><code>@RequestParam</code></td>
<td>쿼리 파라미터에서 값 추출 (<code>/greet?lang=ko</code> → <code>&quot;ko&quot;</code>)</td>
</tr>
<tr>
<td><code>@RequestBody</code></td>
<td>HTTP 요청 body(JSON)를 Java 객체로 역직렬화</td>
</tr>
</tbody></table>
<pre><code class="language-java">// @RequestBody 사용 예
@PostMapping(&quot;/books&quot;)
public ResponseEntity&lt;Book&gt; createBook(@RequestBody Book book) {
    // {&quot;title&quot;: &quot;Spring 입문&quot;, &quot;author&quot;: &quot;임한울&quot;} → Book 객체로 변환
    Book saved = bookService.create(book);
    return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}</code></pre>
<blockquote>
<p><code>@PathVariable</code>은 리소스 식별에, <code>@RequestParam</code>은 필터·옵션 설정에, <code>@RequestBody</code>는 POST/PATCH 요청의 본문 수신에 쓴다.</p>
</blockquote>
<h3 id="23-하드코딩-→-db-연결로의-진화">2.3 하드코딩 → DB 연결로의 진화</h3>
<p>강의 초반에는 Book을 직접 생성해 반환하게 진행했다.</p>
<pre><code class="language-java">// 처음 — 하드코딩
@GetMapping(&quot;/books/1&quot;)
public Book getBook() {
    return new Book(1L, &quot;Spring Boot 입문&quot;, &quot;임한울&quot;);
}</code></pre>
<p>실제 서비스에서 데이터는 DB에서 꺼내야 한다. </p>
<hr>
<h2 id="3-데이터베이스-연결--jpa와-h2">3. 데이터베이스 연결 — JPA와 H2</h2>
<h3 id="31-jpa란">3.1 JPA란?</h3>
<p>JPA(Java Persistence API)는 Java 객체를 관계형 DB 테이블과 매핑해주는 표준 인터페이스다.</p>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20240708144602/JPA---Architecture-(1).png" alt="JPA Architecture — Persistence, EntityManagerFactory, EntityManager, EntityTransaction 관계"></p>
<p>구현체는 <strong>Hibernate</strong>이고, Spring Boot는 Hibernate를 기본으로 사용한다.</p>
<pre><code>Java 객체 (Book)  ←→  JPA / Hibernate  ←→  DB 테이블 (book)</code></pre><p>SQL을 직접 쓰는 대신 Java 코드로 DB를 다룰 수 있게 해준다.</p>
<h3 id="32-entity--클래스와-테이블-매핑">3.2 @Entity — 클래스와 테이블 매핑</h3>
<pre><code class="language-java">@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Book {

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

    @Column(nullable = false, length = 200)
    @NotBlank
    private String title;

    @Column(nullable = false)
    @NotBlank
    private String author;
}</code></pre>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Entity</code></td>
<td>이 클래스가 DB 테이블과 매핑됨을 선언</td>
</tr>
<tr>
<td><code>@Id</code></td>
<td>기본키(PK) 지정</td>
</tr>
<tr>
<td><code>@GeneratedValue(strategy = IDENTITY)</code></td>
<td>DB AUTO_INCREMENT로 ID 자동 생성</td>
</tr>
<tr>
<td><code>@Column</code></td>
<td>컬럼 속성 지정 (<code>nullable</code>, <code>length</code> 등)</td>
</tr>
<tr>
<td><code>@Table(name = &quot;BOOK2&quot;)</code></td>
<td>테이블 이름을 클래스명과 다르게 지정할 때</td>
</tr>
</tbody></table>
<blockquote>
<p>클래스 이름이 테이블 이름, 필드가 컬럼이 된다. <code>Book</code> 클래스 → <code>book</code> 테이블 자동 생성.</p>
</blockquote>
<h3 id="33-jparepository--crud-자동-제공">3.3 JpaRepository — CRUD 자동 제공</h3>
<pre><code class="language-java">public interface BookRepository extends JpaRepository&lt;Book, Long&gt; {
    // 인터페이스 선언만 해도 구현체를 Spring이 자동으로 만들어준다
}</code></pre>
<p>코드가 없어도 아래 메서드들이 즉시 사용 가능하다.</p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>save(entity)</code></td>
<td>저장 / 수정</td>
</tr>
<tr>
<td><code>findById(id)</code></td>
<td>ID로 조회 → <code>Optional&lt;T&gt;</code> 반환</td>
</tr>
<tr>
<td><code>findAll()</code></td>
<td>전체 조회</td>
</tr>
<tr>
<td><code>deleteById(id)</code></td>
<td>ID로 삭제</td>
</tr>
<tr>
<td><code>existsById(id)</code></td>
<td>존재 여부 확인</td>
</tr>
<tr>
<td><code>count()</code></td>
<td>전체 개수 조회</td>
</tr>
</tbody></table>
<h3 id="34-dirty-checking-변경-감지">3.4 Dirty Checking (변경 감지)</h3>
<p>JPA의 Spring에서 좋은 기능 중 하나다. <code>@Transactional</code> 안에서 엔티티를 조회하고 필드 값만 바꾸면, 트랜잭션이 끝날 때 JPA가 변경을 감지해서 자동으로 UPDATE 쿼리를 날린다.</p>
<pre><code class="language-java">@Transactional
public Book update(Long id, Book book) {
    Book existing = findById(id);           // DB에서 조회 → 영속성 컨텍스트에 등록
    if (book.getTitle() != null) {
        existing.setTitle(book.getTitle()); // 값만 변경
    }
    if (book.getAuthor() != null) {
        existing.setAuthor(book.getAuthor());
    }
    return bookRepository.save(existing);   // save 없이도 트랜잭션 종료 시 자동 UPDATE
}</code></pre>
<blockquote>
<p><code>save()</code>를 명시적으로 호출하지 않아도 트랜잭션 커밋 시점에 변경된 필드만 UPDATE된다. 이게 Dirty Checking이다.</p>
</blockquote>
<hr>
<h2 id="4-query-method--sql-없이-쿼리-만들기">4. Query Method — SQL 없이 쿼리 만들기</h2>
<h3 id="41-메서드-이름으로-쿼리-자동-생성">4.1 메서드 이름으로 쿼리 자동 생성</h3>
<p>Spring Data JPA의 핵심 기능이다. 메서드 이름 규칙만 따르면 SQL을 한 줄도 쓰지 않아도 쿼리가 자동 생성된다.</p>
<pre><code class="language-java">public interface BookRepository extends JpaRepository&lt;Book, Long&gt; {
    List&lt;Book&gt; findByTitle(String title);                         // WHERE title = ?
    List&lt;Book&gt; findByAuthor(String author);                       // WHERE author = ?
    List&lt;Book&gt; findByTitleContaining(String keyword);             // WHERE title LIKE &#39;%?%&#39;
    List&lt;Book&gt; findByTitleAndAuthor(String title, String author); // WHERE title = ? AND author = ?
}</code></pre>
<p>주요 키워드 목록:</p>
<table>
<thead>
<tr>
<th>키워드</th>
<th>생성되는 SQL 조건</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>findBy</code></td>
<td>SELECT * WHERE</td>
<td><code>findByTitle</code></td>
</tr>
<tr>
<td><code>Containing</code></td>
<td>LIKE <code>&#39;%keyword%&#39;</code></td>
<td><code>findByTitleContaining</code></td>
</tr>
<tr>
<td><code>StartingWith</code></td>
<td>LIKE <code>&#39;keyword%&#39;</code></td>
<td><code>findByTitleStartingWith</code></td>
</tr>
<tr>
<td><code>EndingWith</code></td>
<td>LIKE <code>&#39;%keyword&#39;</code></td>
<td><code>findByTitleEndingWith</code></td>
</tr>
<tr>
<td><code>And</code></td>
<td>AND 조건</td>
<td><code>findByTitleAndAuthor</code></td>
</tr>
<tr>
<td><code>Or</code></td>
<td>OR 조건</td>
<td><code>findByTitleOrAuthor</code></td>
</tr>
<tr>
<td><code>OrderBy</code></td>
<td>ORDER BY</td>
<td><code>findByAuthorOrderByTitleAsc</code></td>
</tr>
<tr>
<td><code>GreaterThan</code></td>
<td>&gt;</td>
<td><code>findByIdGreaterThan</code></td>
</tr>
<tr>
<td><code>LessThan</code></td>
<td>&lt;</td>
<td><code>findByIdLessThan</code></td>
</tr>
<tr>
<td><code>IsNull</code></td>
<td>IS NULL</td>
<td><code>findByAuthorIsNull</code></td>
</tr>
<tr>
<td><code>countBy</code></td>
<td>COUNT</td>
<td><code>countByAuthor</code></td>
</tr>
</tbody></table>
<blockquote>
<p><code>findByTitleContaining(&quot;Spring&quot;)</code>만 선언하면 <code>WHERE title LIKE &#39;%Spring%&#39;</code>이 자동 생성된다. 메서드 이름이 곧 SQL이다.</p>
</blockquote>
<h3 id="42-stream-api와의-결합">4.2 Stream API와의 결합</h3>
<p>Java 강의에서 배운 Stream이 여기서 사용된다.</p>
<pre><code class="language-java">// 특정 저자의 모든 책 제목만 추출
public List&lt;String&gt; authorGetTitle(String author) {
    List&lt;Book&gt; books = bookRepository.findByAuthor(author);
    return books.stream()
                .map(book -&gt; book.getTitle())
                .toList();
}</code></pre>
<hr>
<h2 id="5-service-레이어-분리">5. Service 레이어 분리</h2>
<h3 id="51-왜-service가-필요한가">5.1 왜 Service가 필요한가?</h3>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20260428120748786642/spring_data.webp" alt="Spring Data JPA — JpaRepository가 CRUD를 자동 제공하고 Hibernate가 SQL을 실행하는 구조"></p>
<p>강의 중반까지는 Controller가 Repository를 직접 사용했다.</p>
<pre><code class="language-java">// Controller가 Repository를 직접 사용하는 방식
@RestController
@RequiredArgsConstructor
public class BookController {
    private final BookRepository bookRepository;

    @GetMapping(&quot;/books/{id}&quot;)
    public Book getBook(@PathVariable Long id) {
        return bookRepository.findById(id)
                .orElseThrow(() -&gt; new RuntimeException(&quot;Book not found: &quot; + id));
    }
}</code></pre>
<p>로직이 단순할 때는 괜찮지만, 검증·예외 처리·복잡한 비즈니스 로직이 늘어나면 Controller가 비대해진다. Service 레이어를 분리하면 각 레이어의 책임이 명확해진다.</p>
<pre><code>Controller — HTTP 요청/응답 형식 담당 (어떤 요청인지)
Service    — 비즈니스 로직 담당 (어떻게 처리할지)
Repository — DB 접근 담당 (어디서 꺼낼지)</code></pre><h3 id="52-의존성-주입di-3가지-방식">5.2 의존성 주입(DI) 3가지 방식</h3>
<p>Spring에서 Bean을 주입하는 방법은 세 가지다.</p>
<pre><code class="language-java">// 1. 필드 주입 — 코드가 짧지만 테스트하기 어렵고 Spring 권장 안 함
@Autowired
private BookRepository bookRepository;

// 2. Setter 주입 — 선택적 의존성에 사용 (잘 안 씀)
@Autowired
public void setBookRepository(BookRepository bookRepository) {
    this.bookRepository = bookRepository;
}

// 3. 생성자 주입 (권장) — 불변성 보장, 테스트 용이, 순환 의존성 감지
private final BookRepository bookRepository;

public BookService(BookRepository bookRepository) {
    this.bookRepository = bookRepository;
}</code></pre>
<p>실제로는 <code>@RequiredArgsConstructor</code>로 생성자 주입을 자동화한다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor           // final 필드를 받는 생성자를 Lombok이 자동 생성
public class BookService {
    private final BookRepository bookRepository; // 생성자 주입이 자동으로 이루어짐
}</code></pre>
<blockquote>
<p>Spring 공식 권장 방식은 생성자 주입이다. <code>final</code>로 선언하면 주입 후 변경 불가(불변성), 테스트 시 Mock 객체를 직접 넣을 수 있다(테스트 용이성).</p>
</blockquote>
<h3 id="53-service와-transactional">5.3 @Service와 @Transactional</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class BookService {
    private final BookRepository bookRepository;

    @Transactional(readOnly = true)
    public Book findById(Long id) {
        return bookRepository.findById(id)
                .orElseThrow(() -&gt; new BookNotFoundException(id));
    }

    @Transactional
    public Book create(Book book) {
        return bookRepository.save(book);
    }

    @Transactional
    public Book update(Long id, Book book) {
        Book existing = findById(id);
        if (book.getTitle() != null) existing.setTitle(book.getTitle());
        if (book.getAuthor() != null) existing.setAuthor(book.getAuthor());
        return bookRepository.save(existing);
    }

    @Transactional
    public void deleteBook(Long id) {
        if (!bookRepository.existsById(id)) {
            throw new BookNotFoundException(id);
        }
        bookRepository.deleteById(id);
    }
}</code></pre>
<p><code>@Transactional</code>은 하나의 작업이 전부 성공하거나, 실패하면 전부 롤백됨을 보장한다. DB의 <strong>ACID 원칙</strong> 중 <strong>Atomicity(원자성)</strong>을 코드 레벨에서 보장하는 것이다.</p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Transactional</code></td>
<td>읽기/쓰기 트랜잭션</td>
</tr>
<tr>
<td><code>@Transactional(readOnly = true)</code></td>
<td>읽기 전용 최적화 (쓰기 잠금 생략, Dirty Checking 비활성화)</td>
</tr>
</tbody></table>
<blockquote>
<p>Java 강의에서 배운 DI 패턴이 여기서 그대로 나온다. <code>BookService</code>는 <code>BookRepository</code> 인터페이스에 의존하고, Spring이 구현체를 주입해준다. </p>
</blockquote>
<hr>
<h2 id="6-예외처리--restcontrolleradvice">6. 예외처리 — @RestControllerAdvice</h2>
<h3 id="61-사용자-정의-예외">6.1 사용자 정의 예외</h3>
<pre><code class="language-java">public class BookNotFoundException extends RuntimeException {
    public BookNotFoundException(Long id) {
        super(&quot;Book not found: id=&quot; + id);
    }
}</code></pre>
<p>Java 강의의 <code>PaymentFailedException</code>과 구조가 동일하다. <code>RuntimeException(&quot;Book not found&quot;)</code>보다 <code>BookNotFoundException</code>이 훨씬 명확하다.</p>
<p><code>RuntimeException</code>을 상속하면 <code>throws</code> 선언 없이 사용할 수 있다. Spring Boot는 체크 예외보다 언체크 예외(RuntimeException)를 권장한다.</p>
<h3 id="62-restcontrolleradvice--전역-예외-처리기">6.2 @RestControllerAdvice — 전역 예외 처리기</h3>
<pre><code class="language-java">@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BookNotFoundException.class)
    public ResponseEntity&lt;Map&lt;String, String&gt;&gt; handleNotFound(BookNotFoundException e) {
        Map&lt;String, String&gt; body = Map.of(
            &quot;error&quot;, &quot;Book not found&quot;,
            &quot;message&quot;, e.getMessage()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity&lt;Map&lt;String, String&gt;&gt; handleValidation(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldError().getDefaultMessage();
        Map&lt;String, String&gt; body = Map.of(&quot;error&quot;, &quot;Validation failed&quot;, &quot;message&quot;, msg);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
    }
}</code></pre>
<p><code>@RestControllerAdvice</code>는 모든 Controller의 예외를 한 곳에서 잡아서 처리한다. 각 Controller마다 try-catch를 쓰는 대신, 이 클래스 하나가 전역 예외 처리기 역할을 한다.</p>
<table>
<thead>
<tr>
<th>예외</th>
<th>HTTP 상태 코드</th>
<th>응답</th>
</tr>
</thead>
<tbody><tr>
<td><code>BookNotFoundException</code></td>
<td>404 Not Found</td>
<td><code>{&quot;error&quot;: &quot;Book not found&quot;, &quot;message&quot;: &quot;...&quot;}</code></td>
</tr>
<tr>
<td><code>MethodArgumentNotValidException</code></td>
<td>400 Bad Request</td>
<td><code>{&quot;error&quot;: &quot;Validation failed&quot;, &quot;message&quot;: &quot;...&quot;}</code></td>
</tr>
</tbody></table>
<blockquote>
<p>Controller마다 try-catch를 뿌리는 대신 한 곳에서 일괄 처리한다. AOP(관점 지향 프로그래밍)의 개념이 여기서 적용된다.</p>
</blockquote>
<hr>
<h2 id="7-lombok과-validation">7. Lombok과 Validation</h2>
<h3 id="71-lombok--반복-코드-제거">7.1 Lombok — 반복 코드 제거</h3>
<p>Lombok은 컴파일 시점에 어노테이션을 읽어서 코드를 자동 생성해주는 라이브러리다.</p>
<pre><code class="language-java">// Lombok 없이 — 매번 직접 작성
public class Book {
    private String title;
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public Book() {}
    public Book(Long id, String title, String author) { ... }
}

// Lombok 있으면 — 어노테이션으로 끝
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Book { ... }</code></pre>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>자동 생성되는 코드</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Getter</code></td>
<td>모든 필드의 getter</td>
</tr>
<tr>
<td><code>@Setter</code></td>
<td>모든 필드의 setter</td>
</tr>
<tr>
<td><code>@NoArgsConstructor</code></td>
<td>기본 생성자</td>
</tr>
<tr>
<td><code>@AllArgsConstructor</code></td>
<td>전체 필드 생성자</td>
</tr>
<tr>
<td><code>@RequiredArgsConstructor</code></td>
<td><code>final</code> 필드만 받는 생성자</td>
</tr>
<tr>
<td><code>@Builder</code></td>
<td>빌더 패턴 생성</td>
</tr>
<tr>
<td><code>@ToString</code></td>
<td><code>toString()</code> 메서드</td>
</tr>
</tbody></table>
<p><code>@RequiredArgsConstructor</code>는 Service, Controller에서 생성자 주입 자동화에 쓴다.</p>
<h3 id="72-builder--가독성-높은-객체-생성">7.2 @Builder — 가독성 높은 객체 생성</h3>
<p>필드가 많은 객체를 생성할 때 생성자 인자 순서를 외우는 것보다 훨씬 가독성이 좋다.</p>
<pre><code class="language-java">@Builder
@Getter
public class Book {
    private Long id;
    private String title;
    private String author;
}

// 생성자 방식 — 인자 순서를 외워야 함
Book book = new Book(null, &quot;Spring 입문&quot;, &quot;임한울&quot;);

// Builder 방식 — 어떤 값인지 명확
Book book = Book.builder()
    .title(&quot;Spring 입문&quot;)
    .author(&quot;임한울&quot;)
    .build();</code></pre>
<h3 id="73-valid와-bean-validation">7.3 @Valid와 Bean Validation</h3>
<pre><code class="language-java">// Book 엔티티에 검증 조건 선언
@NotBlank
private String title;

@NotBlank
private String author;</code></pre>
<pre><code class="language-java">// Controller에서 @Valid로 검증 적용
@PostMapping(&quot;/books&quot;)
public ResponseEntity&lt;Book&gt; createBook(@Valid @RequestBody Book book) {
    Book saved = bookService.create(book);
    return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}</code></pre>
<p><code>@Valid</code>가 붙으면 요청 본문을 역직렬화할 때 제약 조건을 검사한다. 검증 실패 시 <code>MethodArgumentNotValidException</code>이 발생하고, <code>GlobalExceptionHandler</code>가 400 Bad Request로 처리한다.</p>
<p>자주 쓰는 Bean Validation 어노테이션:</p>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>@NotNull</code></td>
<td>null 불가</td>
</tr>
<tr>
<td><code>@NotBlank</code></td>
<td>null, 빈 문자열, 공백만 있는 문자열 불가</td>
</tr>
<tr>
<td><code>@NotEmpty</code></td>
<td>null, 빈 문자열 불가</td>
</tr>
<tr>
<td><code>@Size(min, max)</code></td>
<td>문자열/컬렉션 크기 제한</td>
</tr>
<tr>
<td><code>@Min(value)</code></td>
<td>최솟값</td>
</tr>
<tr>
<td><code>@Max(value)</code></td>
<td>최댓값</td>
</tr>
<tr>
<td><code>@Email</code></td>
<td>이메일 형식 검증</td>
</tr>
<tr>
<td><code>@Pattern(regexp)</code></td>
<td>정규식 패턴 검증</td>
</tr>
</tbody></table>
<blockquote>
<p>검증 로직을 Controller 안에 if문으로 직접 쓰는 대신, 어노테이션으로 선언하고 전역 핸들러로 처리하는 패턴이다. 코드가 훨씬 깔끔해진다.</p>
</blockquote>
<hr>
<h2 id="8-페이징과-정렬">8. 페이징과 정렬</h2>
<h3 id="81-pagerequest">8.1 PageRequest</h3>
<p>대용량 데이터를 한 번에 내려주면 성능이 나쁘다. 페이징으로 나눠서 전달한다.</p>
<pre><code class="language-java">@Transactional(readOnly = true)
public Page&lt;Book&gt; getPage(int page, int size, String sortBy) {
    Sort sort = Sort.by(sortBy).ascending();   // 오름차순
    // Sort sort = Sort.by(sortBy).descending(); // 내림차순
    Pageable pageable = PageRequest.of(page, size, sort);
    return bookRepository.findAll(pageable);
}</code></pre>
<pre><code class="language-java">@GetMapping(&quot;/books/page&quot;)
public Page&lt;Book&gt; getPage(
    @RequestParam int page,
    @RequestParam int size,
    @RequestParam String sortBy
) {
    return bookService.getPage(page, size, sortBy);
}</code></pre>
<p><code>GET /books/page?page=0&amp;size=10&amp;sortBy=title</code>을 호출하면 제목순 정렬로 첫 10개를 반환한다.</p>
<h3 id="82-page-응답-구조">8.2 Page 응답 구조</h3>
<p><code>Page&lt;Book&gt;</code> 응답에는 데이터뿐 아니라 메타데이터도 함께 담긴다.</p>
<pre><code class="language-json">{
  &quot;content&quot;: [ { &quot;id&quot;: 1, &quot;title&quot;: &quot;자바의 정석&quot;, &quot;author&quot;: &quot;남궁성&quot; }, ... ],
  &quot;totalElements&quot;: 5,    // 전체 데이터 수
  &quot;totalPages&quot;: 1,       // 전체 페이지 수
  &quot;number&quot;: 0,           // 현재 페이지 번호 (0-based)
  &quot;size&quot;: 10,            // 페이지 크기
  &quot;first&quot;: true,         // 첫 페이지 여부
  &quot;last&quot;: true           // 마지막 페이지 여부
}</code></pre>
<p>프론트엔드에서 &quot;다음 페이지&quot; 버튼 표시 여부를 <code>last</code> 필드로 판단할 수 있다.</p>
<hr>
<h2 id="9-crud-완성--http-메서드-매핑">9. CRUD 완성 — HTTP 메서드 매핑</h2>
<h3 id="91-rest-api-설계-원칙">9.1 REST API 설계 원칙</h3>
<pre><code class="language-java">@GetMapping(&quot;/books/{id}&quot;)       // 단건 조회
@GetMapping(&quot;/books&quot;)            // 전체 조회
@PostMapping(&quot;/books&quot;)           // 생성
@PatchMapping(&quot;/books/{id}&quot;)     // 부분 수정
@DeleteMapping(&quot;/books/{id}&quot;)    // 삭제</code></pre>
<table>
<thead>
<tr>
<th>HTTP 메서드</th>
<th>용도</th>
<th>응답 상태 코드</th>
<th>멱등성</th>
</tr>
</thead>
<tbody><tr>
<td>GET</td>
<td>조회</td>
<td>200 OK</td>
<td>O</td>
</tr>
<tr>
<td>POST</td>
<td>생성</td>
<td>201 Created</td>
<td>X</td>
</tr>
<tr>
<td>PUT</td>
<td>전체 수정</td>
<td>200 OK</td>
<td>O</td>
</tr>
<tr>
<td>PATCH</td>
<td>부분 수정</td>
<td>200 OK</td>
<td>△</td>
</tr>
<tr>
<td>DELETE</td>
<td>삭제</td>
<td>204 No Content</td>
<td>O</td>
</tr>
</tbody></table>
<p><strong>PUT vs PATCH 차이</strong>: PUT은 전체 교체(보내지 않은 필드는 null로 덮어씀), PATCH는 보낸 필드만 수정한다. 강의 실습에서 <code>@PatchMapping</code>을 쓴 이유가 이것이다.</p>
<h3 id="92-responseentity--상태-코드-직접-지정">9.2 ResponseEntity — 상태 코드 직접 지정</h3>
<p><code>ResponseEntity</code>를 쓰면 HTTP 상태 코드, 헤더, 바디를 직접 제어할 수 있다.</p>
<pre><code class="language-java">@PostMapping(&quot;/books&quot;)
public ResponseEntity&lt;Book&gt; createBook(@Valid @RequestBody Book book) {
    Book saved = bookService.create(book);
    return ResponseEntity.status(HttpStatus.CREATED).body(saved); // 201 + body
}

@DeleteMapping(&quot;/books/{id}&quot;)
public ResponseEntity&lt;Void&gt; deleteBook(@PathVariable Long id) {
    bookService.deleteBook(id);
    return ResponseEntity.noContent().build(); // 204 + 빈 body
}

@GetMapping(&quot;/books/{id}&quot;)
public ResponseEntity&lt;Book&gt; getBook(@PathVariable Long id) {
    Book book = bookService.findById(id);
    return ResponseEntity.ok(book); // 200 + body (축약형)
}</code></pre>
<hr>
<h2 id="10-spring-boot의-핵심-흐름-정리">10. Spring Boot의 핵심 흐름 정리</h2>
<p>강의를 통해 하나의 HTTP 요청이 어떻게 처리되는지 전체 흐름을 정리하자.</p>
<pre><code>[클라이언트] POST /books {&quot;title&quot;: &quot;Spring 입문&quot;, &quot;author&quot;: &quot;임한울&quot;}
     ↓
[DispatcherServlet] 요청을 적절한 Controller로 라우팅
     ↓
[BookController] @Valid로 입력값 검증 → BookService.create() 호출
     ↓
[BookService] @Transactional 시작 → bookRepository.save() 호출
     ↓
[BookRepository] JpaRepository → Hibernate → SQL 실행
     ↓
[DB] INSERT INTO book (title, author) VALUES (?, ?)
     ↓
[응답] 201 Created + 저장된 Book JSON</code></pre><p>예외가 발생하면:</p>
<pre><code>[BookService] BookNotFoundException 발생
     ↓
[GlobalExceptionHandler] @ExceptionHandler가 잡아서 처리
     ↓
[응답] 404 Not Found + {&quot;error&quot;: &quot;Book not found&quot;, &quot;message&quot;: &quot;...&quot;}</code></pre><hr>
<h2 id="생각정리">생각정리</h2>
<p>Spring을 앞으로 자주 사용할지는 아직 모르겠다. 최근에는 목적에 따라 다양한 서버 기술들이 사용되고 있기 때문이다. Python 기반 또는 간단한 프로젝트에서는 FastAPI나 Flask를 사용하는 경우가 많고, JavaScript 생태계에서는 Node.js와 Express, NestJS를 활용하는 사례도 많다. 또한 AI 서비스를 개발할 때는 모델 추론 서버를 FastAPI로 구축하는 경우가 흔하며, 게임이나 소규모 프로젝트에서는 Firebase나 Supabase 같은 BaaS 서비스를 활용하여 별도의 서버 개발 없이 기능을 구현하기도 한다. AWS Lambda와 같은 서버리스 환경을 사용하면 서버를 직접 운영하지 않고도 백엔드 기능을 제공할 수 있다.</p>
<p>그럼에도 이번 Spring 학습은 단순히 특정 프레임워크를 배우는 것 이상의 의미가 있었다. 어떤 기술 스택을 선택하더라도 서버 애플리케이션은 의존성 관리, 계층 분리, 예외 처리, 데이터 접근과 같은 공통적인 문제를 해결해야 한다. Spring은 이러한 문제들을 체계적으로 다루는 방법을 제공했고, Java 수업에서 배웠던 객체지향 설계 원칙들이 실제 프로젝트에서 어떻게 활용되는지를 이해할 수 있게 해주었다.</p>
<p>특히 Java 강의에서 학습했던 내용들이 Spring에서 자연스럽게 연결된다는 점과 실제 다음 미니프로젝트에서 연결해 사용할 수 있다는점이 좋았다. (예비군 참여로 인해 프로젝트에 기여한점은 없었다.) 인터페이스를 활용한 다형성은 의존성 주입(DI) 구조로 이어졌고, 사용자 정의 예외는 서비스 계층의 예외 처리 방식으로 확장되었다. </p>
<p>그중에서도 가장 생각해보고 고민한 부분은 생성자 주입 방식이었다. Java 수업에서는 객체를 직접 생성하거나 의존성을 전달하는 코드를 작성했지만, Spring에서는 <code>@RequiredArgsConstructor</code>와 <code>final</code> 키워드만으로 의존성 주입이 자동으로 이루어졌다. 덕분에 객체 간 결합도를 낮추면서도 코드를 더욱 간결하게 유지할 수 있었다.</p>
<p>학습 과정에서 가장 어려웠던 점은 레이어를 분리하는 기준을 이해하는 것이었다. Controller, Service, Repository 구조 자체는 이해할 수 있었지만 어떤 로직을 어느 계층에 배치해야 하는지 판단하는 것이 쉽지 않았다. 하지만 &quot;HTTP 요청과 응답 처리는 Controller, 비즈니스 로직은 Service, 데이터 접근은 Repository&quot;라는 기준을 세워야 각 계층의 역할이 명확하게 보인다고 생각한다. 이후에는 코드를 작성할 때도  책임을 분리하여 설계할 수 있었다.</p>
<p>JPA의 Dirty Checking 역시 처음에는 매우 신기한 기능으로 느껴졌다. 데이터를 수정한 뒤 <code>save()</code>를 호출하지 않았는데도 데이터베이스의 값이 변경되는 이유를 이해하지 못했지만, 영속성 컨텍스트가 엔티티의 상태를 추적하고 트랜잭션 종료 시점에 변경 사항을 자동으로 반영한다는 원리를 학습하면서 동작 과정을 이해할 수 있었다. 내부적으로 어떤 방식으로 넘어가는 후에 글을 찾아보며 공부해보자. 10-2글을 마무리한다.</p>
<hr>
<blockquote>
<p><strong>출처/참고</strong></p>
<ul>
<li><a href="https://docs.spring.io/spring-boot/index.html">Spring Boot 공식 문서</a></li>
<li><a href="https://docs.spring.io/spring-data/jpa/reference/jpa.html">Spring Data JPA — 공식 레퍼런스</a></li>
<li><a href="https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html">Spring Data JPA — Query Methods</a></li>
<li><a href="https://docs.spring.io/spring-framework/reference/core/beans.html">Spring Framework — IoC Container</a></li>
<li><a href="https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html">Spring Framework — @Transactional</a></li>
<li><a href="https://projectlombok.org/features/">Lombok 공식 문서</a></li>
<li><a href="https://beanvalidation.org/">Jakarta Bean Validation</a></li>
<li><a href="https://www.h2database.com/html/main.html">H2 Database 공식 문서</a></li>
<li><a href="https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-advice.html">Spring MVC — @RestControllerAdvice</a></li>
<li><a href="https://www.geeksforgeeks.org/springboot/spring-boot-architecture/">GeeksforGeeks — Spring Boot Architecture</a></li>
<li><a href="https://github.com/KT-E/10-Week">실습 코드 (GitHub)</a></li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 10주차-1 / Java]]></title>
            <link>https://velog.io/@mi_nini/KT-A-10%EC%A3%BC%EC%B0%A8-1-Java</link>
            <guid>https://velog.io/@mi_nini/KT-A-10%EC%A3%BC%EC%B0%A8-1-Java</guid>
            <pubDate>Sun, 07 Jun 2026 12:11:33 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>미니 프로젝트를 마친 후에는 프론트엔드와 백엔드를 연동하기 위해 Spring 학습할 예정이다.
Spring은 Java를 기반으로 동작하는 프레임워크이기 때문에, Spring을 제대로 이해하기 위해서는 먼저 Java의 기본 문법과 객체지향 프로그래밍 개념을 익혀야 했다. 따라서 Java를 선수 학습한 뒤 Spring을 공부하는 순서로 학습을 진행했다. 이후에는 학습한 내용을 바탕으로 05 미니 프로젝트를 진행하며 실제로 프론트엔드와 백엔드를 연결해 볼 예정이다. 
이번 주 정리에서는 학습한 내용이 많아 두 편으로 나누어 작성하려 한다. 이번 글(10주차-1)에서는 Java 학습 내용을 정리하고, 다음 글에서는 Spring(10주차-2) 학습 내용을 다룰 예정이다.
그럼 먼저 Java 학습 내용을 간단히 살펴보자. (IntelliJ IDEA 사용해 진행)</p>
<hr>
<h2 id="1-java-언어의-기초">1. Java 언어의 기초</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/e57b655d-dec4-46a5-b161-3bb32c0215aa/image.png" alt=""></p>
<h3 id="11-왜-java인가">1.1 왜 Java인가?</h3>
<p>수업 첫날부터 강사님이 짚어준 부분이다. 국내 백엔드 채용 시장에서 Java/Spring 비율은 약 <strong>70~80%</strong>다. 전자정부 프레임워크도 Spring 기반이고, 카카오·네이버·배달의민족 등 주요 IT 기업의 백엔드 기술 스택 대부분이 Java + Spring Boot 조합이다.</p>
<blockquote>
<p>Python은 AI/ML에 강하지만 대규모 트래픽에서는 Java가 안정적. Java는 타입 안정성 덕분에 컴파일 시점에서 버그를 잡을 수 있다.</p>
</blockquote>
<h3 id="12-jdk--jre--jvm-이해하기">1.2 JDK / JRE / JVM 이해하기</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/92b6177c-da5a-449f-93c0-6c4e35c38760/image.png" alt="">
Java의 핵심 특징은 <strong>WORA(Write Once, Run Anywhere)</strong>다. 한 번 작성하면 OS에 상관없이 어디서든 실행된다. 이게 가능한 이유가 JVM이다.</p>
<pre><code>개발자 코드 (.java)
       ↓  javac 컴파일
   바이트코드 (.class)
       ↓  JVM이 각 OS 맞춤형 기계어로 번역
   Windows / Mac / Linux 에서 실행</code></pre><table>
<thead>
<tr>
<th>구분</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>JVM</td>
<td>바이트코드를 실행하는 가상 머신</td>
</tr>
<tr>
<td>JRE</td>
<td>JVM + 실행에 필요한 라이브러리 (&quot;실행만 하는 사람&quot;)</td>
</tr>
<tr>
<td>JDK</td>
<td>JRE + 컴파일러(javac) + 디버거 (&quot;개발자는 반드시 JDK&quot;)</td>
</tr>
</tbody></table>
<h3 id="13-변수와-자료형">1.3 변수와 자료형</h3>
<p>Java는 정적 타입 언어다. 변수를 선언할 때 타입을 명시해야 한다. Python처럼 <code>x = 10</code>이 아니라 <code>int x = 10;</code>이다.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>자료형</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>나이, 개수 같은 정수</td>
<td><code>int</code></td>
<td><code>int age = 20;</code></td>
</tr>
<tr>
<td>키, 점수 같은 실수</td>
<td><code>double</code></td>
<td><code>double height = 175.5;</code></td>
</tr>
<tr>
<td>성별, 혈액형 (한 글자)</td>
<td><code>char</code></td>
<td><code>char grade = &#39;A&#39;;</code></td>
</tr>
<tr>
<td>참/거짓 판단</td>
<td><code>boolean</code></td>
<td><code>boolean isStudent = true;</code></td>
</tr>
<tr>
<td>이름, 주소, 문장</td>
<td><code>String</code></td>
<td><code>String name = &quot;홍길동&quot;;</code></td>
</tr>
</tbody></table>
<p>중요한 포인트가 하나 있는데, <code>String</code>은 기본 자료형이 아닌 <strong>참조 자료형(Reference Type)</strong>이다. 때문에 비교할 때 <code>==</code>이 아니라 반드시 <code>equals()</code>를 써야 한다.</p>
<pre><code class="language-java">String a = new String(&quot;홍길동&quot;);
String b = new String(&quot;홍길동&quot;);

System.out.println(a == b);       // false → 주소를 비교
System.out.println(a.equals(b));  // true  → 실제 내용을 비교</code></pre>
<p><code>String</code>은 자주 쓰는 메서드들이 많다.</p>
<pre><code class="language-java">String name = &quot;Alice&quot;;
System.out.println(name.length());          // 5
System.out.println(name.contains(&quot;li&quot;));    // true
System.out.println(name.toUpperCase());     // ALICE
System.out.println(name.equals(&quot;Alice&quot;));   // true
System.out.println(name.equals(&quot;alice&quot;));   // false — 대소문자 구분</code></pre>
<p><strong>형변환(Casting)</strong>도 함께 학습했다.</p>
<ul>
<li>작은 타입 → 큰 타입: 자동 변환 (<code>int</code> → <code>double</code>)</li>
<li>큰 타입 → 작은 타입: 명시적 변환 필요 (<code>(int) 3.14</code> → <code>3</code>, 소수점 버려짐)</li>
<li>String ↔ 숫자: <code>Integer.parseInt(&quot;123&quot;)</code>, <code>String.valueOf(123)</code></li>
</ul>
<pre><code class="language-java">int x = 10;
double y = x;                      // 자동 변환: 10.0
int z = (int) 3.14;                // 명시적 변환: 3 (소수점 버려짐)
int n = Integer.parseInt(&quot;123&quot;);   // String → int: 123
String s = String.valueOf(123);    // int → String: &quot;123&quot;</code></pre>
<h3 id="14-scanner로-사용자-입력-받기">1.4 Scanner로 사용자 입력 받기</h3>
<p><code>Scanner</code>는 <code>java.util</code> 패키지에 있어서 <code>import</code>가 필요하다.</p>
<pre><code class="language-java">import java.util.Scanner;

Scanner scanner = new Scanner(System.in);
int age = scanner.nextInt();          // 정수 입력
double height = scanner.nextDouble(); // 실수 입력
String line = scanner.nextLine();     // 한 줄 전체 (공백 포함)
String word = scanner.next();         // 단어 하나 (공백 제외)

scanner.close(); // 사용 후 반드시 닫기</code></pre>
<p><code>nextInt()</code> 이후 <code>nextLine()</code>을 쓰면 버퍼에 남아있는 <code>\n</code> 때문에 입력이 건너뛰어지는 문제가 생긴다. 실습하다가 직접 겪어봤다.</p>
<h3 id="15-연산자">1.5 연산자</h3>
<p><strong>전위 vs 후위 증감 연산자:</strong></p>
<pre><code class="language-java">int a = 5;
int b = ++a;  // a를 먼저 증가시키고 대입 → a=6, b=6

int c = 5;
int d = c++;  // 먼저 대입하고 나서 증가 → c=6, d=5</code></pre>
<p><strong>단락 평가(Short-circuit):</strong></p>
<pre><code class="language-java">false &amp;&amp; (10 / 0 &gt; 5)  // 앞이 false면 뒤는 실행 안 됨 → 오류 발생 안 함
true  || (10 / 0 &gt; 5)  // 앞이 true면 뒤는 실행 안 됨  → 오류 발생 안 함</code></pre>
<p>불필요한 계산을 생략해서 성능을 높이고, 오류도 방지한다.</p>
<pre><code class="language-java">int year = 2024;
boolean isLeapYear = (year % 4 == 0 &amp;&amp; year % 100 != 0) || (year % 400 == 0);
System.out.println(year + &quot;년은 &quot; + (isLeapYear ? &quot;윤년&quot; : &quot;평년&quot;)); // 2024년은 윤년</code></pre>
<hr>
<h2 id="2-흐름-제어">2. 흐름 제어</h2>
<h3 id="21-조건문--if와-switch-case">2.1 조건문 — if와 switch-case</h3>
<p><code>if ~ else if ~ else</code>와 <code>switch ~ case</code> 두 가지 패턴을 다뤘다.</p>
<pre><code class="language-java">int age = 19;
if (age &gt;= 18) {
    System.out.println(&quot;성인&quot;);
} else {
    System.out.println(&quot;미성년자&quot;);
}

// else-if: 연령대 분류
if (age &lt; 13) {
    System.out.println(&quot;어린이&quot;);
} else if (age &lt; 19) {
    System.out.println(&quot;청소년&quot;);
} else {
    System.out.println(&quot;성인&quot;);
}</code></pre>
<p><code>switch</code>는 하나의 변수를 여러 값과 비교할 때 가독성이 좋다. <code>break</code>를 빠뜨리면 아래 case로 계속 흘러내리는 <strong>fall-through</strong> 현상이 생기는데, 의도적으로 활용하면 여러 case를 묶을 수 있다.</p>
<pre><code class="language-java">switch (month) {
    case 3: case 4: case 5:
        System.out.println(&quot;봄&quot;); // 3, 4, 5 모두 여기서 처리
        break;
    case 6: case 7: case 8:
        System.out.println(&quot;여름&quot;);
        break;
    default:
        System.out.println(&quot;잘못된 입력&quot;);
}</code></pre>
<h3 id="22-반복문--for-while-do-while">2.2 반복문 — for, while, do-while</h3>
<p>세 가지 반복문을 모두 학습했다.</p>
<ul>
<li><strong>for</strong>: 반복 횟수를 미리 아는 경우</li>
<li><strong>while</strong>: 조건이 참인 동안, 횟수가 불확실한 경우</li>
<li><strong>do-while</strong>: 최소 한 번은 무조건 실행하고 싶을 때</li>
</ul>
<p><code>break</code>와 <code>continue</code>의 차이:</p>
<pre><code class="language-java">for (int i = 0; i &lt;= 10; i++) {
    if (i == 5) break;     // 반복문 즉시 종료
    if (i == 3) continue;  // 이번 회차만 건너뜀, 반복은 계속
    System.out.println(i);
}</code></pre>
<pre><code class="language-java">int sum = 0;
for (int i = 1; i &lt;= 100; i++) {
    sum += i;
}
System.out.println(&quot;합계: &quot; + sum); // 5050

// while: 9가 입력될 때까지 계속 받기
Scanner scanner = new Scanner(System.in);
int input = 0;
while (input != 9) {
    System.out.print(&quot;숫자 입력 (9 = 종료): &quot;);
    input = scanner.nextInt();
}

// continue로 짝수만 더하기
int evenSum = 0;
for (int i = 1; i &lt;= 10; i++) {
    if (i % 2 != 0) continue; // 홀수는 건너뜀
    evenSum += i;
}
System.out.println(&quot;짝수 합계: &quot; + evenSum); // 30</code></pre>
<h3 id="23-배열">2.3 배열</h3>
<p>배열은 크기가 고정이다. 선언할 때 크기를 정해야 하고, 반복문과 함께 쓰는 게 기본이다.</p>
<pre><code class="language-java">int[] scores = new int[5];

for (int i = 0; i &lt; scores.length; i++) {
    scores[i] = scanner.nextInt();
}</code></pre>
<pre><code class="language-java">int[] scores = {80, 90, 70, 60, 85};
int total = 0;

for (int i = 0; i &lt; scores.length; i++) {
    total += scores[i];
}
double average = (double) total / scores.length;
System.out.println(&quot;평균: &quot; + average); // 77.0</code></pre>
<p><code>scores.length</code>로 크기를 구할 수 있다. 배열은 크기가 고정이라 나중에 배울 <code>ArrayList</code>의 동적 크기 조절이 왜 필요한지 자연스럽게 이해됐다.</p>
<hr>
<h2 id="3-메서드와-재사용성">3. 메서드와 재사용성</h2>
<h3 id="31-메서드란">3.1 메서드란?</h3>
<p>특정 코드를 모아서 이름을 붙인 집합. 다른 언어의 &#39;함수&#39;와 유사하다. 반복되는 코드를 한 곳에 두고 이름으로 호출할 수 있어 가독성과 유지보수성이 올라간다.</p>
<pre><code class="language-java">// 반환값이 있는 메서드
public static int add(int a, int b) {
    return a + b;
}

// 반환값이 없는 메서드
public static void printResult(int result) {
    System.out.println(&quot;결과: &quot; + result);
}</code></pre>
<pre><code class="language-java">public static int sub(int a, int b) { return a - b; }
public static int mul(int a, int b) { return a * b; }
public static int div(int a, int b) { return a / b; }

System.out.println(add(17, 5)); // 22
System.out.println(sub(17, 5)); // 12
System.out.println(mul(17, 5)); // 85
System.out.println(div(17, 5)); // 3</code></pre>
<h3 id="32-메서드-오버로딩-overloading">3.2 메서드 오버로딩 (Overloading)</h3>
<p>같은 이름, 다른 매개변수로 여러 메서드를 정의하는 것. 컴파일러가 호출 시 인자 타입을 보고 어떤 메서드를 쓸지 결정한다.</p>
<pre><code class="language-java">public static int add(int a, int b) { return a + b; }
public static double add(double a, double b) { return a + b; }

add(10, 20);      // int 버전 호출
add(3.14, 2.71);  // double 버전 호출</code></pre>
<p>매개변수 <strong>개수</strong>가 달라도 오버로딩이 성립한다.</p>
<pre><code class="language-java">public static int max(int a, int b) { return a &gt; b ? a : b; }
public static int max(int a, int b, int c) { return max(max(a, b), c); }
public static double max(double a, double b) { return a &gt; b ? a : b; }

max(3, 7);        // int 2개 → 7
max(1, 5, 3);     // int 3개 → 5
max(3.14, 2.71);  // double 2개 → 3.14</code></pre>
<blockquote>
<p>반환값만 다르고 매개변수가 같으면 오버로딩이 성립하지 않는다. 이게 처음에 헷갈렸다.</p>
</blockquote>
<hr>
<h2 id="4-객체지향-프로그래밍-oop">4. 객체지향 프로그래밍 (OOP)</h2>
<h3 id="41-클래스와-객체">4.1 클래스와 객체</h3>
<pre><code>클래스 (Car.java)     ← 설계도, 자료형 역할, 1개
       ↓  new
객체 (avante, sonata) ← 실체, 여러 개 생성 가능</code></pre><h3 id="42-접근제한자-access-modifier">4.2 접근제한자 (Access Modifier)</h3>
<p>필드와 메서드에 누가 접근할 수 있는지를 제어하는 키워드다. 4가지가 있다.</p>
<table>
<thead>
<tr>
<th>접근제한자</th>
<th align="center">같은 클래스</th>
<th align="center">같은 패키지</th>
<th align="center">자식 클래스</th>
<th align="center">외부 클래스</th>
</tr>
</thead>
<tbody><tr>
<td><code>public</code></td>
<td align="center">✓</td>
<td align="center">✓</td>
<td align="center">✓</td>
<td align="center">✓</td>
</tr>
<tr>
<td><code>protected</code></td>
<td align="center">✓</td>
<td align="center">✓</td>
<td align="center">✓</td>
<td align="center">✗</td>
</tr>
<tr>
<td><code>default</code> (생략)</td>
<td align="center">✓</td>
<td align="center">✓</td>
<td align="center">✗</td>
<td align="center">✗</td>
</tr>
<tr>
<td><code>private</code></td>
<td align="center">✓</td>
<td align="center">✗</td>
<td align="center">✗</td>
<td align="center">✗</td>
</tr>
</tbody></table>
<p>이 강의에서 중점적으로 다룬 건 <code>public</code>과 <code>private</code> 두 가지다. <code>protected</code>는 상속 파트에서 다시 등장한다.</p>
<h3 id="43-캡슐화-encapsulation">4.3 캡슐화 (Encapsulation)</h3>
<p>필드를 <code>private</code>으로 막고 Getter/Setter를 통해서만 접근하게 하는 패턴. 단순히 접근을 막는 게 목적이 아니라, <strong>객체가 자기 데이터를 스스로 보호</strong>하는 게 핵심이다. Setter 안에서 유효성 검사를 넣을 수 있다.</p>
<pre><code class="language-java">public void setMaxSpeed(int maxSpeed) {
    if (maxSpeed &lt; 0 || maxSpeed &gt; 300) {
        System.out.println(&quot;잘못된 속도&quot;);
        return;
    }
    this.maxSpeed = maxSpeed;
}</code></pre>
<p>Setter 안에 검증 로직이 있으니, 외부에서는 잘못된 값을 넣어도 객체 내부 상태가 오염되지 않는다.</p>
<pre><code class="language-java">public void setKorean(int korean) {
    if (korean &lt; 0 || korean &gt; 100) {
        System.out.println(&quot;점수는 0 ~ 100 사이여야 합니다.&quot;);
        return;
    }
    this.korean = korean;
}

student.setKorean(150); // &quot;점수는 0 ~ 100 사이여야 합니다.&quot; 출력, 값 반영 안 됨
student.setKorean(85);  // 정상 저장</code></pre>
<h3 id="44-생성자-constructor">4.4 생성자 (Constructor)</h3>
<p>객체 생성 시점에 필드를 한 번에 초기화하는 특별한 메서드다. 반환 타입이 없고 클래스 이름과 동일하다.</p>
<pre><code class="language-java">// 매번 setter를 따로 호출하던 것을
Car a = new Car();
a.setColor(&quot;Red&quot;);
a.setMaxSpeed(120);

// 생성자 하나로 해결
Car a = new Car(&quot;Red&quot;, 120, 3);</code></pre>
<p><code>this</code> 키워드는 &quot;이 객체 자신&quot;을 가리킨다. 매개변수 이름과 필드 이름이 같을 때 구분하는 데 쓴다.</p>
<pre><code class="language-java">public Car(String color, int maxSpeed) {
    this.color = color;       // this.color → 필드, color → 매개변수
    this.maxSpeed = maxSpeed;
}</code></pre>
<p>생성자도 오버로딩이 된다. 매개변수 개수나 타입이 다르면 여러 생성자를 정의할 수 있다.</p>
<pre><code class="language-java">public Car() {                          // 기본 생성자 — 기본값으로 초기화
    this.color = &quot;White&quot;;
    this.maxSpeed = 100;
}

public Car(String color, int maxSpeed) { // 매개변수 생성자 — 값을 받아 초기화
    this.color = color;
    this.maxSpeed = maxSpeed;
}</code></pre>
<h3 id="45-static-키워드">4.5 static 키워드</h3>
<ul>
<li><strong>static 필드</strong>: 모든 객체가 공유하는 하나의 값 (ex. 생성된 객체 수 카운터)</li>
<li><strong>static 메서드</strong>: 객체 없이 클래스명으로 바로 호출</li>
</ul>
<pre><code class="language-java">// 우리가 매일 쓰던 이것들이 전부 static
Integer.parseInt(&quot;123&quot;);
String.valueOf(100);
Math.max(3, 5);</code></pre>
<p>static 필드는 여러 객체가 공유해야 하는 값을 저장할 때 유용하다.</p>
<pre><code class="language-java">public class Car {
    private static int count = 0;  // 모든 인스턴스가 공유하는 하나의 변수

    public Car(String color, int maxSpeed) {
        this.color = color;
        this.maxSpeed = maxSpeed;
        count++;                   // 객체가 만들어질 때마다 증가
    }

    public static int getCount() { // 객체 없이 클래스명으로 바로 호출
        return count;
    }
}

Car a = new Car(&quot;Red&quot;, 200);
Car b = new Car(&quot;Blue&quot;, 150);
System.out.println(Car.getCount()); // 2</code></pre>
<h3 id="46-상속-inheritance">4.6 상속 (Inheritance)</h3>
<p>공통 코드를 부모 클래스에 한 번만 작성하고, 자식 클래스들이 가져다 쓰는 구조다.</p>
<pre><code>Vehicle (부모)
  ├── color, maxSpeed, start(), stop()
  │
  ├── Car (자식)   : + trunk, openTrunk()
  └── Truck (자식) : + loadCapacity, loadCargo()</code></pre><p>자식 클래스에서 <code>extends</code>로 상속을 선언한다.</p>
<p><strong><code>super</code> 키워드:</strong> <code>this</code>가 &quot;이 객체 자신&quot;을 가리키듯, <code>super</code>는 &quot;부모 객체&quot;를 가리킨다. 자식 생성자에서 부모 생성자를 호출할 때 첫 줄에서 반드시 사용한다.</p>
<pre><code class="language-java">public class Car extends Vehicle {
    private int trunk;

    public Car(String color, int maxSpeed, int trunk) {
        super(color, maxSpeed); // 부모 생성자 호출 — 반드시 첫 줄
        this.trunk = trunk;
    }
}</code></pre>
<p><strong><code>protected</code> 접근제한자:</strong> 부모 클래스의 필드를 <code>private</code>으로 선언하면 자식 클래스에서도 직접 접근이 불가능하다. <code>protected</code>로 선언하면 자식 클래스에서는 직접 접근이 가능하면서, 외부 클래스에서는 막힌다.</p>
<pre><code class="language-java">public class Vehicle {
    protected String color;   // 자식 클래스에서 직접 접근 가능
    private int maxSpeed;     // 자식 클래스에서도 직접 접근 불가
}

public class Car extends Vehicle {
    public void show() {
        System.out.println(color);     // OK — protected
        System.out.println(maxSpeed);  // 컴파일 에러 — private
    }
}</code></pre>
<h3 id="47-메서드-오버라이딩-vs-오버로딩">4.7 메서드 오버라이딩 vs 오버로딩</h3>
<p>표로 정리!</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>오버로딩 (Overloading)</th>
<th>오버라이딩 (Overriding)</th>
</tr>
</thead>
<tbody><tr>
<td>위치</td>
<td>한 클래스 안에서</td>
<td>부모-자식 관계에서만</td>
</tr>
<tr>
<td>조건</td>
<td>같은 이름, 다른 매개변수</td>
<td>같은 시그니처, 다른 동작</td>
</tr>
<tr>
<td>목적</td>
<td>같은 기능의 다양한 타입 처리</td>
<td>자식 클래스에서 동작 재정의</td>
</tr>
</tbody></table>
<pre><code class="language-java">// 오버라이딩: @Override 어노테이션으로 명시
@Override
public void move() {
    System.out.println(&quot;도로를 달린다&quot;); // 부모의 &quot;이동한다&quot;를 재정의
}</code></pre>
<h3 id="48-다형성-polymorphism">4.8 다형성 (Polymorphism)</h3>
<p>부모 타입 변수에 자식 객체를 담을 수 있다(업캐스팅). 같은 메서드 호출이 실제 객체에 따라 다르게 동작한다.</p>
<pre><code class="language-java">Vehicle[] vehicles = {
    new Car(&quot;Red&quot;, 200, 500),
    new Truck(&quot;White&quot;, 100, 5000),
    new Car(&quot;Black&quot;, 180, 400)
};

for (Vehicle v : vehicles) {
    v.move(); // Car면 &quot;도로를 달린다&quot;, Truck이면 &quot;화물을 싣고 달린다&quot;
}</code></pre>
<p>새 자식 클래스가 추가되어도 for 문 코드는 수정 불필요. 유지보수성과 확장성의 핵심이다.</p>
<p><strong><code>instanceof</code> + 다운캐스팅:</strong> 업캐스팅된 변수로는 자식 고유의 메서드를 호출할 수 없다. 실제 타입을 확인한 뒤 자식 타입으로 명시적으로 변환(다운캐스팅)해야 한다.</p>
<pre><code class="language-java">Vehicle v = new Car(&quot;Red&quot;, 200, 500);

v.move();        // OK — Vehicle에 있는 메서드
v.openTrunk();   // 컴파일 에러 — Vehicle에는 없음

// instanceof로 실제 타입 확인 후 다운캐스팅
if (v instanceof Car) {
    Car c = (Car) v;   // 다운캐스팅
    c.openTrunk();     // OK
}</code></pre>
<p>배열에서 자식별로 다른 처리가 필요할 때 자주 쓰이는 패턴이다.</p>
<pre><code class="language-java">for (Vehicle v : vehicles) {
    v.move();  // 공통 동작
    if (v instanceof Car) {
        ((Car) v).openTrunk();
    } else if (v instanceof Truck) {
        ((Truck) v).loadCargo();
    }
}</code></pre>
<hr>
<h2 id="5-인터페이스와-설계-패턴">5. 인터페이스와 설계 패턴</h2>
<h3 id="51-인터페이스란">5.1 인터페이스란?</h3>
<p>Java는 단일 상속만 지원한다. 그런데 한 객체가 여러 &quot;역할&quot;을 동시에 가져야 하는 경우가 많다. Car는 &quot;탈것&quot;이면서 동시에 &quot;팔 수 있는 상품&quot;이기도 하다.</p>
<pre><code>상속 (extends)        → &quot;이런 종류의 것이다&quot; (한 개만 가능)
인터페이스 (implements) → &quot;이런 일을 할 수 있다&quot; (여러 개 가능)</code></pre><pre><code class="language-java">public class Car extends Vehicle implements Sellable, Insurable {
    // 종류는 Vehicle 하나
    // 역할은 Sellable + Insurable 둘 다
}</code></pre>
<p>인터페이스는 메서드 시그니처만 정의하고 구현하지 않는다. 구현체(클래스)가 <code>implements</code>로 받아 실제 동작을 채워 넣는다.</p>
<h3 id="52-의존성-주입di-패턴">5.2 의존성 주입(DI) 패턴</h3>
<p><strong>강한 결합의 문제:</strong></p>
<pre><code class="language-java">// OrderService 안에 KakaoPay를 직접 생성하면
// 결제 수단을 바꾸려면 OrderService 코드를 직접 수정해야 함
private KakaoPay payment = new KakaoPay();</code></pre>
<p><strong>인터페이스 기반 느슨한 결합:</strong></p>
<pre><code class="language-java">public class OrderService {
    private final PaymentMethod payment; // 인터페이스 타입

    public OrderService(PaymentMethod payment) {
        this.payment = payment; // 외부에서 주입
    }
}

// 구현체를 자유롭게 교체 가능, OrderService 코드 수정 0
new OrderService(new KakaoPay());
new OrderService(new NaverPay());
new OrderService(new CreditCard());</code></pre>
<blockquote>
<p>Spring Boot의 <code>@Autowired</code>가 이 &quot;외부에서 주입&quot;하는 과정을 자동으로 해준다. 지금 손으로 하는 걸 Spring이 대신해주는 것이다.</p>
</blockquote>
<p>이 개념을 이해하고 Spring때 사용하자</p>
<hr>
<h2 id="6-예외처리">6. 예외처리</h2>
<h3 id="61-런타임-에러를-우아하게-처리하기">6.1 런타임 에러를 우아하게 처리하기</h3>
<p>컴파일 에러는 IDE가 미리 잡아주지만, 런타임 에러는 실행 중에 갑자기 터진다.</p>
<pre><code class="language-java">try {
    int num = Integer.parseInt(&quot;abc&quot;); // 여기서 예외 발생
    System.out.println(&quot;성공: &quot; + num); // 실행 안 됨

} catch (NumberFormatException e) {
    System.out.println(&quot;실패: &quot; + e.getMessage()); // 여기로 이동

} finally {
    System.out.println(&quot;항상 실행&quot;); // 예외 여부와 무관하게 항상
}</code></pre>
<h3 id="62-다중-catch">6.2 다중 catch</h3>
<p><code>try</code> 블록에서 여러 종류의 예외가 발생할 수 있을 때 <code>catch</code>를 여러 개 작성한다. 순서가 중요한데, <strong>자식 예외를 부모 예외보다 먼저</strong> 작성해야 한다. 부모가 먼저 오면 자식 예외까지 모두 잡아버려 아래 <code>catch</code>가 실행될 일이 없어진다.</p>
<pre><code class="language-java">try {
    int idx = Integer.parseInt(idxInput);   // NumberFormatException 가능
    int num = data[idx];                    // ArrayIndexOutOfBoundsException 가능

} catch (NumberFormatException e) {
    System.out.println(&quot;숫자 변환 실패&quot;);

} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println(&quot;배열 범위 초과&quot;);

} catch (Exception e) {         // 부모 예외는 가장 마지막에
    System.out.println(&quot;기타 예외&quot;);
}</code></pre>
<h3 id="63-throw-vs-throws">6.3 throw vs throws</h3>
<p>헷갈리기 쉬운 두 키워드다.</p>
<table>
<thead>
<tr>
<th>키워드</th>
<th>위치</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>throw</code></td>
<td>메서드 내부</td>
<td>예외 객체를 <strong>직접 던진다</strong></td>
</tr>
<tr>
<td><code>throws</code></td>
<td>메서드 시그니처</td>
<td>이 메서드가 예외를 <strong>던질 수 있다고 선언</strong></td>
</tr>
</tbody></table>
<pre><code class="language-java">// throw — 조건에 맞으면 예외를 직접 발생
public void setMaxSpeed(int maxSpeed) {
    if (maxSpeed &lt; 0) {
        throw new IllegalArgumentException(&quot;음수 불가: &quot; + maxSpeed);
    }
    this.maxSpeed = maxSpeed;
}

// throws — 체크 예외를 호출자에게 위임 (RuntimeException은 생략 가능)
public void readFile(String path) throws IOException {
    // 파일 읽기 코드
}</code></pre>
<blockquote>
<p>Spring Boot는 RuntimeException 위주라 <code>throws</code> 선언을 거의 쓰지 않는다. 메서드 시그니처가 깔끔하게 유지된다.</p>
</blockquote>
<h3 id="64-체크-예외-vs-언체크-예외">6.4 체크 예외 vs 언체크 예외</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>처리 강제</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>체크 예외</td>
<td>필수 (try-catch 또는 throws)</td>
<td>IOException, SQLException</td>
</tr>
<tr>
<td>언체크 예외 (RuntimeException)</td>
<td>선택</td>
<td>NumberFormatException, NullPointerException</td>
</tr>
</tbody></table>
<p>Spring Boot는 언체크 예외를 선호한다. 비즈니스 로직 코드가 <code>try-catch</code>로 도배되는 걸 방지할 수 있어서다.</p>
<h3 id="65-사용자-정의-예외">6.5 사용자 정의 예외</h3>
<p><code>RuntimeException</code>을 상속해서 의미 있는 이름의 예외를 직접 만든다.</p>
<pre><code class="language-java">public class PaymentFailedException extends RuntimeException {
    public PaymentFailedException(String reason) {
        super(&quot;결제 실패: &quot; + reason);
    }
}</code></pre>
<p><code>IllegalArgumentException</code> 대신 <code>PaymentFailedException</code>을 쓰면 예외 이름만 봐도 어떤 상황인지 즉시 파악된다. Spring Boot의 <code>UserNotFoundException</code>, <code>OrderNotFoundException</code> 같은 패턴이 전부 이것이다.</p>
<hr>
<h2 id="7-컬렉션과-람다">7. 컬렉션과 람다</h2>
<h3 id="71-배열의-한계와-list">7.1 배열의 한계와 List</h3>
<p>배열은 크기가 고정이다. <code>List</code>는 크기가 동적으로 늘어나고 편리한 메서드를 제공한다.</p>
<pre><code class="language-java">List&lt;String&gt; names = new ArrayList&lt;&gt;();
names.add(&quot;홍길동&quot;);
names.add(&quot;이영희&quot;);

names.get(0);            // 조회
names.size();            // 크기
names.remove(0);         // 삭제
names.contains(&quot;이영희&quot;); // 포함 여부</code></pre>
<h3 id="72-map--키-값-쌍으로-저장">7.2 Map — 키-값 쌍으로 저장</h3>
<pre><code class="language-java">Map&lt;String, Integer&gt; scores = new HashMap&lt;&gt;();
scores.put(&quot;홍길동&quot;, 85);
scores.get(&quot;홍길동&quot;); // 85

for (Map.Entry&lt;String, Integer&gt; entry : scores.entrySet()) {
    System.out.println(entry.getKey() + &quot;: &quot; + entry.getValue());
}</code></pre>
<p><strong>List vs Map 선택 기준:</strong></p>
<ul>
<li>순서가 중요하고 인덱스로 접근 → <code>List</code></li>
<li>이름표(키)로 빠르게 찾고 싶음 → <code>Map</code></li>
</ul>
<h3 id="73-람다-표현식">7.3 람다 표현식</h3>
<p>메서드를 한 줄로 표현하는 문법이다.</p>
<pre><code class="language-java">// 전통적인 for + if 방식
List&lt;Integer&gt; highScores = new ArrayList&lt;&gt;();
for (int score : scores) {
    if (score &gt;= 70) highScores.add(score);
}

// 람다 + Stream 방식
List&lt;Integer&gt; highScores = scores.stream()
    .filter(score -&gt; score &gt;= 70)
    .collect(Collectors.toList());</code></pre>
<h3 id="74-stream-api">7.4 Stream API</h3>
<p>컬렉션 데이터를 흐름처럼 처리하는 API다. 3단계 파이프라인으로 동작한다.</p>
<pre><code>stream()          → 컬렉션을 스트림으로 변환
↓
.filter(조건)      → 조건 맞는 것만 통과
.map(변환)         → 각 원소를 다른 형태로 변환
↓
.collect(...)     → List 등으로 수집</code></pre><pre><code class="language-java">// 70점 이상만 필터링하고, 각 점수를 등급 문자열로 변환
List&lt;String&gt; grades = scores.stream()
    .filter(score -&gt; score &gt;= 70)
    .map(score -&gt; score + &quot;점 (Pass)&quot;)
    .collect(Collectors.toList());</code></pre>
<h3 id="75-optional--null을-안전하게-다루기">7.5 Optional — null을 안전하게 다루기</h3>
<pre><code class="language-java">Optional&lt;Integer&gt; first = list.stream().findFirst();

first.ifPresent(n -&gt; System.out.println(&quot;첫 원소: &quot; + n)); // 값이 있을 때만
int n = first.orElse(0);                                    // 없으면 기본값
int n = first.orElseThrow(() -&gt; new RuntimeException(&quot;없음&quot;)); // 없으면 예외</code></pre>
<p>null 체크를 흩뿌리는 대신 Optional로 감싸면, &quot;값이 없을 수도 있다&quot;는 사실이 타입에 드러난다. Spring Data JPA의 <code>findById()</code>가 <code>Optional&lt;T&gt;</code>를 반환하는 이유가 바로 이것이다.</p>
<pre><code class="language-java">// 지금 배운 것들이 Spring Boot 서비스 코드로 그대로 이어진다
public List&lt;UserDto&gt; findActiveUsers() {
    return userRepository.findAll()
        .stream()
        .filter(u -&gt; u.isActive())
        .map(u -&gt; new UserDto(u.getName()))
        .collect(Collectors.toList());
}

public User findById(Long id) {
    return userRepository.findById(id)
        .orElseThrow(() -&gt; new UserNotFoundException(id)); // 6장 사용자 정의 예외
}</code></pre>
<hr>
<h2 id="생각정리">생각정리</h2>
<p>학부 과정에서 Java와 Spring을 모두 접해본 경험이 있었기 때문에 이번 학습에서 완전히 새로운 내용을 배우는 느낌은 아니었다. 오히려 예전에 배웠던 개념들을 다시 정리하고, Spring Boot를 사용하기 전에 필요한 Java 기초를 복습하는 시간에 가까웠다. 특히 객체지향 프로그래밍 파트는 예전에도 여러 번 공부했지만, 다시 학습하면서 클래스, 상속, 다형성, 인터페이스가 각각 왜 필요한지 조금 더 명확하게 이해할 수 있었다. 학부 시절에는 문법을 외우는 데 집중했다면, 이번에는 실제 프로젝트를 진행한다는 목적이 있어서 각 개념이 어떤 문제를 해결하기 위해 존재하는지 생각하며 학습하게 되었다.
가장 인상 깊었던 부분은 인터페이스와 의존성 주입 개념이었다. Spring을 사용할 때는 <code>@Service</code>, <code>@Repository</code>, <code>@Autowired</code> 등을 자연스럽게 사용했지만, 이번에는 그 동작 원리가 되는 구조를 Java 코드만으로 직접 구현해보며 다시 정리할 수 있었다. 덕분에 Spring이 단순히 편리한 프레임워크가 아니라 객체지향 설계 원칙을 기반으로 여러 기능을 자동화해주는 도구구나.. 알고 잘 사용하자 라는 느낌을 받은거 같다. 
컬렉션, 람다, Stream API, Optional과 같은 기능들은 실제 백엔드 개발 과정에서 자주 사용되는 만큼 활용 방법을 다시 들으며 찾아보고 공부하게 되었다. 특히 Stream API는 데이터를 가공하는 코드를 간결하게 작성할 수 있어 앞으로 진행할 프로젝트에서도 적극적으로 활용할 수 있을 것 같다.
다음 주(11주차)에는 Spring Boot 학습을 통해 이번에 정리한 객체지향 개념과 설계 방식을 생각해보자. 
(예비군 훈련이 6/8 ~ 6/11이라 참여는 불가능할거 같지만.. 후에 따로 작업해보자) 
10주차-1을 마무리한다.</p>
<hr>
<blockquote>
<p>참고/출처</p>
<ul>
<li><a href="https://backendcode.tistory.com/161">JVM이란</a></li>
<li><a href="https://docs.oracle.com/javase/tutorial/java/javaOO/classes.html">class란</a> + <a href="https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Method.html">Method</a></li>
<li><a href="https://woo0doo.tistory.com/15">오버리이딩 vs 오버로딩</a></li>
<li><a href="https://inpa.tistory.com/entry/OOP-JAVA%EC%9D%98-%EB%8B%A4%ED%98%95%EC%84%B1Polymorphism-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4">다형성 이해하기_java</a></li>
<li><a href="https://chanhuiseok.github.io/posts/java-3/">java예외처리</a></li>
<li><a href="https://mangkyu.tistory.com/150">의존성주입</a></li>
<li><a href="https://www.jetbrains.com/idea/">IntelliJ IDEA 공식 문서</a></li>
<li><a href="https://docs.oracle.com/en/java/">Java 공식 문서 (Oracle)</a></li>
<li><a href="https://wikidocs.net/book/31">점프 투 자바</a></li>
</ul>
</blockquote>
<h3 id="실습-code"><a href="https://github.com/KT-E/09-Week">실습 Code</a></h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 9주차 / 04Miniproject]]></title>
            <link>https://velog.io/@mi_nini/KT-A-9%EC%A3%BC%EC%B0%A8-04Miniproject</link>
            <guid>https://velog.io/@mi_nini/KT-A-9%EC%A3%BC%EC%B0%A8-04Miniproject</guid>
            <pubDate>Sun, 07 Jun 2026 04:43:17 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>04미니프로젝트가 끝났다.
지금까지의 프로젝트들(01,02,03)은 AI 쪽 로직이 중심이었다. Multi-Agent, LangGraph, RAG 파이프라인 같은 프로젝트였다. 이번 프로젝트에서는 <strong>프론트엔드 개발 전체</strong>를 진행했다. React, OpenAI API 연동, 이미지 처리, 컴포넌트 설계, 그리고 팀 코드 병합까지.
이번 회고록엔 &quot;왜 이 프로젝트가 필요한가&quot;라는 배경부터, 실제로 내가 고민하고 결정하고 부딪혔던 기술적인 내용들까지 최대한 상세하게 남겨보려 한다. </p>
<blockquote>
<p><a href="https://github.com/BcKmini/Book-management">12조의 프로젝트가 궁금하다면?</a></p>
</blockquote>
<hr>
<h2 id="프로젝트-배경--왜-이걸-만들어야-하나">프로젝트 배경 — 왜 이걸 만들어야 하나</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/11ed4f4e-cd06-4ad0-8775-ec8f20c79a11/image.png" alt=""></p>
<h3 id="책-표지-그게-그렇게-중요한가">책 표지, 그게 그렇게 중요한가?</h3>
<p>프로젝트를 시작할 때 강사님이 던진 질문이 있었다.</p>
<blockquote>
<p>&quot;책 표지 디자인이 왜 중요할까요?&quot;</p>
</blockquote>
<p>처음엔 그냥 예쁘면 되는 거 아닌가 하고 시작하려고 했다. 비슷한 서비스를 개발하고 여러 사이트를 방문하고 내가 조사한 책 표지에 실제 수치를 보고 나서 생각이 바뀌었다. 표지 디자인은 <strong>CTR(Click Through Rate, 클릭률)</strong>, <strong>구매 전환율</strong>, <strong>SNS 공유율</strong>에 직접적인 영향을 준다. 독자는 내용보다 표지를 먼저 본다. 첫인상이 구매를 결정한다.</p>
<pre><code>텍스트 기반 콘텐츠(책 내용)
        ↓
    표지 디자인
&quot;장르와 메시지를 시각적으로 전달&quot;
        ↓
    독자(User)
&quot;끌리는 표지일수록 클릭하고 싶다&quot;
        ↓
CTR · 구매 전환율 · SNS 공유율 증가</code></pre><p>그런데 현실적으로 개인 작가나 소규모 창작자가 전문 디자이너에게 표지를 맡기기는 어렵다. 비용도 비용이지만, 수정 피드백을 주고받는 과정도 길다. 감성적인 의도를 언어로 전달하는 것 자체가 어렵기도 하다.</p>
<h3 id="서비스-컨셉--걷기가-서재">서비스 컨셉 — &quot;걷기가 서재&quot;</h3>
<p>이번 프로젝트의 도메인은 <strong>&quot;걷기가 서재&quot;</strong> 라는 가상의 국내 최대 독서 플랫폼이다. 그 안에 있는 <strong>&quot;작가의 산책&quot;</strong> 서비스를 구현하는 것이 목표였다.</p>
<blockquote>
<p>누구나 작가가 되어 자유롭게 글을 집필하고 공개할 수 있는 창작 플랫폼. 단, 기존 플랫폼과 달리 <strong>AI 표지 제작</strong>을 지원한다.</p>
</blockquote>
<p>이야기가 그대로 표지에 닿도록. 작가의 감성을 AI가 시각화해준다는 컨셉이었다. 텍스트 기반 콘텐츠를 입력하면 AI가 거기에 어울리는 표지를 자동으로 만들어주는 것이다.
이 맥락을 이해하고 나서야 단순한 CRUD 앱이 아닌, 왜 이 기능이 의미 있는지 납득이 됐다. 개발하는 내내 이 목적의식이 있었기 때문에 UI 결정을 팀원들과의 소통하는데 있어 하나하나에 더 신중해질 수 있었던 것 같다.</p>
<hr>
<h2 id="프로젝트-개요">프로젝트 개요</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/892332b7-9c9c-4374-9e61-5bc94acf21f0/image.png" alt=""></p>
<h3 id="기술-스택">기술 스택</h3>
<table>
<thead>
<tr>
<th>분류</th>
<th>기술</th>
</tr>
</thead>
<tbody><tr>
<td>Frontend</td>
<td>React 19 · Vite · fetch</td>
</tr>
<tr>
<td>데이터</td>
<td>json-server (로컬 REST API)</td>
</tr>
<tr>
<td>AI</td>
<td>OpenAI API (GPT Image 2)</td>
</tr>
<tr>
<td>UI 라이브러리</td>
<td>MUI (Material UI)</td>
</tr>
<tr>
<td>협업</td>
<td>GitHub</td>
</tr>
</tbody></table>
<p>백엔드는 따로 없다. <code>json-server</code>가 <code>db.json</code>을 읽어서 GET/POST/PATCH/DELETE REST API를 자동으로 제공해주는 구조다. 추후 백엔드 미니프로젝트에서 이 자리를 Spring Boot로 교체할 예정이다.</p>
<p>전체 데이터 흐름은 아래와 같다.</p>
<pre><code>브라우저(React)
  │
  ├── GET/POST/PATCH/DELETE ──▶ json-server(localhost:3000) ──▶ db.json
  │
  └── POST + prompt ──────────▶ OpenAI GPT Image 2
                      ◀── b64_json 응답 ──────────────────────
                        ↓
              React 내부에서 b64_json → Data URL 변환
                        ↓
      PATCH books/:id(coverImageUrl) ──▶ json-server</code></pre><hr>
<h2 id="내-역할">내 역할</h2>
<p>총 8명 팀이었고, 나는 <strong>전체적인 UI 개발</strong>을 담당했다. 구체적으로는:</p>
<ul>
<li>프로토타입 UI 설계 및 구현 (전체 라우팅 구조)</li>
<li>OpenAI LLM 호출 연결 코드 전체 설계 및 구현</li>
<li>CSS 기본 틀 + 후에 만들어놓은 CSS를 받아 수정</li>
<li>팀원들이 개발한 코드를 직접 병합 (PR 없이 - main에 올려놓고 했어야지..)</li>
<li>1개 프롬프트 입력 → 3개 이미지 생성 처리 설계</li>
<li>검색바, 리스트 필터링, 정렬 디테일 관리</li>
<li>json-server 예외처리 전반</li>
</ul>
<hr>
<h2 id="구현-세부사항">구현 세부사항</h2>
<h3 id="1-프로토타입-ui-설계--팀이-시작할-수-있는-기반-만들기">1. 프로토타입 UI 설계 — 팀이 시작할 수 있는 기반 만들기</h3>
<p>제일 먼저 한 건 <strong>팀이 개발을 시작할 수 있는 공통 기반을 만드는 것</strong>이었다. 각자가 맡은 파트를 개발하려면 라우팅 구조와 공통 레이아웃이 먼저 잡혀있어야 한다. 이 부분이 없으면 팀원들이 각자 만든 컴포넌트를 나중에 합칠 때 충돌이 훨씬 커진다.</p>
<p><code>App.jsx</code>에 React Router 기반 라우팅을 잡았다.</p>
<pre><code class="language-jsx">function App() {
  return (
    &lt;BrowserRouter&gt;
      &lt;Header /&gt;
      &lt;Box component=&quot;main&quot; sx={{ pt: &quot;64px&quot; }}&gt;
        &lt;Routes&gt;
          &lt;Route path=&quot;/&quot;                       element={&lt;Home /&gt;} /&gt;
          &lt;Route path=&quot;/books&quot;                  element={&lt;BookListRoute /&gt;} /&gt;
          &lt;Route path=&quot;/books/new&quot;              element={&lt;BookFormRoute /&gt;} /&gt;
          &lt;Route path=&quot;/books/:id&quot;              element={&lt;BookDetailRoute /&gt;} /&gt;
          &lt;Route path=&quot;/books/:id/edit&quot;         element={&lt;BookEditRoute /&gt;} /&gt;
          &lt;Route path=&quot;/books/:id/cover-editor&quot; element={&lt;BookCoverEditor /&gt;} /&gt;
        &lt;/Routes&gt;
      &lt;/Box&gt;
    &lt;/BrowserRouter&gt;
  )
}</code></pre>
<p>각 Route 컴포넌트(<code>BookListRoute</code>, <code>BookDetailRoute</code> 등)는 <code>useNavigate</code>와 <code>useParams</code>를 내부에서 처리하고, 실제 페이지 컴포넌트에는 네비게이션 콜백만 prop으로 내려주는 구조로 설계했다.</p>
<pre><code class="language-jsx">// 라우팅 로직을 래퍼에서 처리 → 페이지 컴포넌트는 라우터 의존성 없음
function BookDetailRoute() {
  const navigate = useNavigate()
  const { id } = useParams()
  return (
    &lt;BookDetail
      id={id}
      onBack={() =&gt; navigate(&#39;/books&#39;)}
      onEdit={() =&gt; navigate(`/books/${id}/edit`)}
      onEditCover={() =&gt; navigate(`/books/${id}/cover-editor`)}
      onDeleted={() =&gt; navigate(&#39;/books&#39;)}
    /&gt;
  )
}</code></pre>
<p>이 패턴의 장점은 <code>BookDetail</code> 컴포넌트 자체가 <code>react-router-dom</code>에 의존하지 않는다는 것이다. 나중에 라우팅 구조가 바뀌어도 페이지 컴포넌트를 건드릴 필요가 없다.</p>
<p>MUI <code>&lt;Box component=&quot;main&quot; sx={{ pt: &quot;64px&quot; }}&gt;</code> 로 고정 헤더(<code>AppBar</code>) 높이만큼 콘텐츠 영역을 밀어주는 방식도 여기서 잡았다. 이게 없으면 헤더 뒤에 첫 번째 콘텐츠가 숨어버린다.</p>
<hr>
<h3 id="2-도서-목록-페이지--필터링과-정렬-설계">2. 도서 목록 페이지 — 필터링과 정렬 설계</h3>
<p>도서 목록에서 기대한 기능들은 생각보다 많았다.</p>
<ul>
<li>장르별 사이드바 필터</li>
<li>즐겨찾기 탭</li>
<li>제목/저자 실시간 검색</li>
<li>등록순/제목순/가격순 정렬</li>
<li>그리드/리스트 뷰 전환</li>
</ul>
<p>이 모든 걸 하나의 컴포넌트에서 처리해야 했다. 상태 설계부터 시작했다.</p>
<pre><code class="language-js">const [books, setBooks] = useState([])          // 전체 데이터 (서버에서 한 번만 fetch)
const [genre, setGenre] = useState(&#39;ALL&#39;)        // 장르 필터
const [query, setQuery] = useState(&#39;&#39;)           // 검색어
const [view, setView] = useState(&#39;grid&#39;)         // 뷰 모드
const [favoriteIds, setFavoriteIds] = useState(() =&gt; readFavoriteIds())  // 즐겨찾기
const [sortBy, setSortBy] = useState(&#39;register&#39;) // 정렬 기준</code></pre>
<p><code>books</code>는 최초 fetch 이후 서버 재요청 없이 클라이언트에서 모든 필터링/정렬을 처리한다. json-server가 쿼리 파라미터를 지원하긴 하지만, 검색어가 바뀔 때마다 API를 치는 건 비효율적이다. 전체를 한 번 받아서 <code>useMemo</code>로 파생 상태를 계산하는 방식이 더 낫다.</p>
<pre><code class="language-js">// 필터링: 장르 + 즐겨찾기 + 검색어
const filtered = useMemo(() =&gt; {
  const lowerQuery = query.toLowerCase()
  return books.filter((book) =&gt; {
    const genreOk =
      genre === &#39;ALL&#39; ||
      (genre === &#39;FAVORITES&#39;
        ? favoriteIds.has(String(book.id))
        : book.genre === genre)
    const queryOk =
      !query ||
      book.title?.toLowerCase().includes(lowerQuery) ||
      book.author?.toLowerCase().includes(lowerQuery)
    return genreOk &amp;&amp; queryOk
  })
}, [books, genre, favoriteIds, query])

// 정렬: filtered 결과 위에서
const sorted = useMemo(() =&gt; {
  const arr = [...filtered]
  if (sortBy === &#39;title&#39;)
    return arr.sort((a, b) =&gt; a.title?.localeCompare(b.title || &#39;&#39;, &#39;ko&#39;))
  if (sortBy === &#39;price&#39;)
    return arr.sort((a, b) =&gt; (a.price || 0) - (b.price || 0))
  // 기본: 등록순 (최신 순)
  return arr.sort(
    (a, b) =&gt; toTime(b.createdAt) - toTime(a.createdAt) || Number(b.id) - Number(a.id)
  )
}, [filtered, sortBy])</code></pre>
<p><code>filtered</code>와 <code>sorted</code>를 <code>useMemo</code> 두 단계로 분리한 이유가 있다. 필터링은 <code>books</code>, <code>genre</code>, <code>query</code>에 의존하고, 정렬은 <code>filtered</code>, <code>sortBy</code>에만 의존한다. 한 덩어리로 합치면 <code>sortBy</code>만 바뀌어도 필터링 로직이 다시 실행된다. 분리하면 각각 필요한 시점에만 재계산된다.</p>
<h4 id="즐겨찾기-탭--localstorage-동기화-문제">즐겨찾기 탭 — localStorage 동기화 문제</h4>
<p>즐겨찾기는 <code>localStorage</code>에 저장한다. 문제는 탭이 여러 개 열려있을 때 A탭에서 즐겨찾기를 추가하면 B탭에는 반영이 안 된다는 것이다.</p>
<pre><code class="language-js">useEffect(() =&gt; {
  const refresh = () =&gt; setFavoriteIds(readFavoriteIds())
  window.addEventListener(&#39;focus&#39;, refresh)       // 탭 포커스 복귀 시
  window.addEventListener(&#39;storage&#39;, refresh)     // 다른 탭에서 storage 변경 시
  window.addEventListener(&#39;bookFavoriteChange&#39;, refresh)  // 커스텀 이벤트
  return () =&gt; {
    window.removeEventListener(&#39;focus&#39;, refresh)
    window.removeEventListener(&#39;storage&#39;, refresh)
    window.removeEventListener(&#39;bookFavoriteChange&#39;, refresh)
  }
}, [])</code></pre>
<p>세 가지 이벤트를 구독했다.</p>
<ul>
<li><code>focus</code> : 다른 탭에서 돌아왔을 때 최신 상태로 갱신</li>
<li><code>storage</code> : 같은 브라우저 다른 탭에서 localStorage 변경 감지</li>
<li><code>bookFavoriteChange</code> : 같은 탭 내에서 즐겨찾기 토글 시 dispatchEvent로 알림</li>
</ul>
<p>이 세 가지를 다 처리하지 않으면 어느 한 경우에서 즐겨찾기가 화면에 즉시 반영되지 않는 버그가 생긴다.</p>
<hr>
<h3 id="3-main-페이지--검색바와-도서-랭킹-캐러셀">3. Main 페이지 — 검색바와 도서 랭킹 캐러셀</h3>
<p>Main 페이지는 구조가 단순해 보이지만, 안에 들어있는 설계 결정들이 꽤 많다. <code>SearchBar</code>와 <code>BookSection</code> 두 컴포넌트로 구성됐고, 둘 다 내가 전담으로 만들었다.</p>
<h4 id="searchbar--상태를-url에-넘기는-이유">SearchBar — 상태를 URL에 넘기는 이유</h4>
<p>검색어를 입력하고 엔터를 누르면 도서 목록 페이지로 이동한다. 단순해 보이지만 구현 방식에서 한 번 고민했다.</p>
<pre><code class="language-jsx">const handleSearch = () =&gt; {
  if (query.trim()) {
    navigate(`/books?search=${encodeURIComponent(query)}`)
  }
}</code></pre>
<p>검색어를 컴포넌트 상태로 넘기지 않고, <strong>URL 쿼리 파라미터로 넘긴다.</strong> 이렇게 하면 두 가지 이점이 있다.</p>
<p>첫째, 검색 결과 URL이 공유 가능해진다. <code>/books?search=해리포터</code> 를 그대로 누군가에게 주면 같은 결과를 볼 수 있다.</p>
<p>둘째, 도서 목록 페이지에서 <code>useSearchParams</code>로 읽으면 SearchBar가 어디 있든 상관없이 검색어를 받을 수 있다. 컴포넌트 간 props 연결이나 전역 상태가 필요 없다.</p>
<pre><code class="language-jsx">// BookListPage에서 URL 파라미터 수신
const [searchParams] = useSearchParams()
const [query, setQuery] = useState(searchParams.get(&#39;search&#39;) || &#39;&#39;)

useEffect(() =&gt; {
  setQuery(searchParams.get(&#39;search&#39;) || &#39;&#39;)
}, [searchParams])</code></pre>
<p>Header에도 같은 검색바가 있는데, 거기서도 동일하게 URL로 넘기니까 도서 목록 페이지가 알아서 처리한다. <code>encodeURIComponent</code>로 한글 검색어가 URL에서 깨지지 않도록 처리한 것도 이 부분이다.</p>
<h4 id="booksection--캐러셀과-탭-설계">BookSection — 캐러셀과 탭 설계</h4>
<p>도서 랭킹과 신작 탭을 제공하는 캐러셀 슬라이더다.</p>
<p>데이터는 이렇게 가공한다.</p>
<pre><code class="language-js">// 랭킹: 조회수 높은 순 최대 60권
const rankingBooks = [...data]
  .sort((a, b) =&gt; (b.viewCount || 0) - (a.viewCount || 0))
  .slice(0, 60)

// 신작: 최근 1개월 이내 출간, 최신 순
const now = new Date()
const oneMonthAgo = new Date()
oneMonthAgo.setMonth(now.getMonth() - 1)

const newBooks = [...data]
  .filter((book) =&gt; {
    if (!book.pubDate) return false
    const pubDate = new Date(book.pubDate)
    return pubDate &gt;= oneMonthAgo &amp;&amp; pubDate &lt;= now
  })
  .sort((a, b) =&gt; new Date(b.pubDate) - new Date(a.pubDate))
  .slice(0, 60)</code></pre>
<p>원본 배열을 건드리지 않도록 <code>[...data]</code> 스프레드로 복사한 뒤 정렬했다. 정렬은 원본을 변경하기 때문에 이걸 빠뜨리면 <code>data</code>가 조용히 바뀐다.</p>
<p>슬라이더는 CSS transform 방식으로 구현했다. 라이브러리 없이.</p>
<pre><code class="language-jsx">&lt;div
  className={styles.cardList}
  style={{ transform: `translateX(calc(-${currentIndex} * (25% + 6px)))` }}
&gt;
  {currentBooks.map((book) =&gt; (...))}
&lt;/div&gt;</code></pre>
<p><code>-${currentIndex} * (25% + 6px)</code> — 카드 4개가 한 화면에 보이니 각 카드 너비가 25%고, 여기에 카드 간 gap(6px)을 더해서 정확히 한 칸씩 이동한다. 
탭 전환 시 <code>currentIndex</code>를 0으로 리셋하는 것도 중요하다.</p>
<pre><code class="language-js">useEffect(() =&gt; {
  setCurrentIndex(0)
}, [activeTab])</code></pre>
<p>이게 없으면 랭킹 탭에서 5번째 카드를 보다가 신작 탭으로 전환했을 때 신작이 5번째부터 시작한다. 사용자 입장에선 당황스러운 경험이다.
커버 이미지가 없는 책은 fallback으로 처리했다.</p>
<pre><code class="language-jsx">src={book.coverImageUrl || `https://picsum.photos/seed/${book.id}/200/300`}</code></pre>
<p><code>picsum.photos</code>에 <code>seed</code>로 책 id를 넘기면 항상 같은 책에 같은 랜덤 이미지가 나온다. AI 표지 생성 전까지 빈 박스 대신 그럴듯한 이미지를 보여줄 수 있어서 UI가 훨씬 자연스러워 보인다.
키보드 접근성도 챙겼다.</p>
<pre><code class="language-jsx">onKeyDown={(e) =&gt; {
  if (e.key === &#39;Enter&#39; || e.key === &#39; &#39;) {
    e.preventDefault()
    onBookClick?.(book)
  }
}}</code></pre>
<p>마우스 없이 탭으로 이동해서 Enter/Space로 클릭할 수 있도록. 사소하지만 <code>role=&quot;button&quot;</code> + <code>tabIndex={0}</code> + <code>onKeyDown</code> 세트가 없으면 키보드 사용자는 카드를 클릭할 수 없다.</p>
<hr>
<h3 id="4-detail-페이지--가장-많은-설계-결정이-들어간-컴포넌트">4. Detail 페이지 — 가장 많은 설계 결정이 들어간 컴포넌트</h3>
<p>BookDetail은 이번 프로젝트에서 코드량이 가장 많은 컴포넌트다(486줄). 그만큼 설계에서 고민한 것도 많았다.</p>
<h4 id="인라인-스타일-선택--css-module을-안-쓴-이유">인라인 스타일 선택 — CSS Module을 안 쓴 이유</h4>
<p>다른 컴포넌트들은 CSS Module(<code>.module.css</code>)을 쓰는데, BookDetail은 인라인 스타일 객체를 썼다.</p>
<pre><code class="language-js">const s = {
  page: { minHeight: &#39;calc(100vh - 64px)&#39;, background: &#39;#eeece6&#39; },
  topbar: { background: &#39;#fff&#39;, borderBottom: &#39;0.5px solid rgba(0,0,0,0.12)&#39;, ... },
  hero: { background: &#39;#fff&#39;, borderRadius: 14, display: &#39;grid&#39;, gridTemplateColumns: &#39;260px minmax(0, 1fr)&#39;, ... },
  // ...
}</code></pre>
<p>선택한 이유가 있다. 이 컴포넌트는 <code>coverColor</code>처럼 <strong>런타임에 동적으로 결정되는 스타일</strong>이 있다. 장르에 따라 표지 배경색이 달라진다.</p>
<pre><code class="language-js">// 함수형 스타일 — 인자에 따라 다른 스타일 반환
coverBox: (bg) =&gt; ({
  height: 360,
  background: bg,   // 장르별 색상이 런타임에 주입
  display: &#39;flex&#39;,
  ...
}),
favoriteBtn: (active) =&gt; ({
  background: active ? &#39;#fff7e8&#39; : &#39;#fff&#39;,
  border: `0.5px solid ${active ? &#39;#f59e0b&#39; : &#39;rgba(0,0,0,0.18)&#39;}`,
  color: active ? &#39;#f59e0b&#39; : &#39;#9b9b95&#39;,
  ...
}),</code></pre>
<p>CSS Module에서 이걸 하려면 클래스를 조건부로 바꾸거나 CSS 변수를 써야 한다. 인라인 스타일로 함수형으로 처리하면 상태에 따라 스타일이 직접 바뀐다는 게 코드에서 명확하게 보인다.</p>
<pre><code class="language-jsx">&lt;div style={s.coverBox(coverColor.bg)}&gt;
&lt;button style={s.favoriteBtn(favorite)}&gt;</code></pre>
<p><code>coverColor</code>는 <code>useMemo</code>로 계산한다.</p>
<pre><code class="language-js">const coverColor = useMemo(() =&gt; getCoverColor(book?.genre), [book?.genre])</code></pre>
<p><code>book?.genre</code>가 바뀔 때만 재계산된다. 옵셔널 체이닝(<code>?.</code>)으로 <code>book</code>이 null인 초기 로딩 상태에서도 에러 없이 처리된다.</p>
<h4 id="조회수-업데이트--300ms-딜레이를-넣은-이유">조회수 업데이트 — 300ms 딜레이를 넣은 이유</h4>
<p>상세 페이지에 들어오면 조회수가 1 증가한다. 구현이 생각보다 까다로웠다.</p>
<pre><code class="language-js">const res = await fetch(`${API}/${id}`)
const data = await res.json()
const currentViews = Number(data.viewCount || 0)
const nextViews = currentViews + 1

setBook(data)
setViews(nextViews)  // UI는 즉시 +1 반영

// 실제 PATCH는 300ms 후에
setTimeout(() =&gt; {
  fetch(`${API}/${id}`, {
    method: &#39;PATCH&#39;,
    headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
    body: JSON.stringify({ viewCount: nextViews }),
  }).catch((e) =&gt; {
    console.warn(&#39;조회수 백그라운드 동기화 락 상태 우회 처리:&#39;, e)
  })
}, 300)</code></pre>
<p>왜 바로 PATCH를 치지 않고 300ms를 기다리나?</p>
<p>json-server는 동시에 여러 요청이 들어오면 파일 락(lock)이 걸린다. GET 요청으로 데이터를 받아오는 것과 동시에 PATCH를 날리면 락 충돌이 생기는 경우가 있었다. 300ms를 기다렸다가 GET이 완전히 끝난 뒤 PATCH를 날리는 방식으로 이 문제를 우회했다.</p>
<p>UI는 <code>setViews(nextViews)</code>로 즉시 갱신해두니 사용자는 딜레이를 느끼지 못한다. 서버 동기화는 백그라운드에서 일어난다. 실패해도 <code>console.warn</code>으로 로그만 남기고 에러를 전파하지 않는다 — 조회수 업데이트 실패가 사용자 경험을 방해해선 안 된다고 판단했다.</p>
<h4 id="삭제-확인-모달--브라우저-confirm을-안-쓴-이유">삭제 확인 모달 — 브라우저 confirm을 안 쓴 이유</h4>
<p>삭제 버튼을 누르면 브라우저 기본 <code>confirm</code> 대화상자 대신, 직접 만든 모달이 뜬다.</p>
<pre><code class="language-js">const [showDeleteModal, setShowDeleteModal] = useState(false)</code></pre>
<pre><code class="language-jsx">{showDeleteModal &amp;&amp; (
  &lt;div style={s.overlay}&gt;
    &lt;div style={s.modal}&gt;
      &lt;div style={s.modalTitle}&gt;도서를 삭제할까요?&lt;/div&gt;
      &lt;div style={s.modalDesc}&gt;
        &lt;strong&gt;{book.title}&lt;/strong&gt; 정보가 목록에서 삭제됩니다.
        &lt;br /&gt;이 작업은 되돌릴 수 없습니다.
      &lt;/div&gt;
      &lt;div style={s.modalActions}&gt;
        &lt;button style={s.cancelBtn} onClick={() =&gt; setShowDeleteModal(false)}&gt;취소&lt;/button&gt;
        &lt;button style={s.dangerBtn} onClick={handleDelete}&gt;삭제하기&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
)}</code></pre>
<p><code>window.confirm()</code>은 브라우저마다 생김새가 다르고, 앱 스타일과 전혀 어울리지 않는다. 커스텀 모달을 쓰면 메시지에 책 제목을 강조(<code>&lt;strong&gt;</code>)해서 &quot;어떤 책을 삭제하는지&quot; 명확하게 보여줄 수 있다. 모달 overlay는 <code>position: fixed, inset: 0</code>으로 화면 전체를 덮고, z-index: 100으로 다른 요소 위에 올라간다.</p>
<h4 id="즐겨찾기--localstorage와-커스텀-이벤트-연동">즐겨찾기 — localStorage와 커스텀 이벤트 연동</h4>
<p>즐겨찾기 상태는 localStorage에 저장하고, 변경 시 커스텀 이벤트를 발행한다.</p>
<pre><code class="language-js">useEffect(() =&gt; {
  localStorage.setItem(`bookFavorite:${id}`, String(favorite))
  window.dispatchEvent(new Event(&#39;bookFavoriteChange&#39;))
}, [favorite, id])</code></pre>
<p><code>dispatchEvent</code>로 <code>bookFavoriteChange</code> 이벤트를 발행하면, 도서 목록 페이지에서 이 이벤트를 구독하고 있다가 즐겨찾기 탭을 즉시 갱신한다. BookDetail과 BookListPage가 직접 연결되지 않아도 이벤트를 통해 상태가 동기화된다.</p>
<h4 id="메타-정보-그리드">메타 정보 그리드</h4>
<p>출판사, 출판일, 가격, 페이지, ISBN, 조회수 6가지 메타 정보를 배열로 관리하고 map으로 렌더링한다.</p>
<pre><code class="language-js">const meta = [
  { icon: &#39;ti-building&#39;,      label: &#39;출판사&#39;, value: book.publisher || &#39;-&#39; },
  { icon: &#39;ti-calendar&#39;,      label: &#39;출판일&#39;, value: book.pubDate ? fmtDate(book.pubDate) : &#39;-&#39; },
  { icon: &#39;ti-currency-won&#39;,  label: &#39;가격&#39;,   value: formatWon(book.price) },
  { icon: &#39;ti-book-2&#39;,        label: &#39;페이지&#39;, value: book.pages ? `${book.pages.toLocaleString()}쪽` : &#39;-&#39; },
  { icon: &#39;ti-barcode&#39;,       label: &#39;ISBN&#39;,   value: book.isbn || &#39;-&#39; },
  { icon: &#39;ti-eye&#39;,           label: &#39;조회수&#39;, value: `${views.toLocaleString()}회` },
]</code></pre>
<p>하드코딩으로 6개 블록을 나열하는 것보다 이 방식이 나중에 항목을 추가/제거하기 훨씬 편하다. <code>formatWon</code>으로 가격에 원화 포맷을 적용하고, 값이 없으면 전부 <code>-</code>로 fallback 처리했다.</p>
<hr>
<h3 id="5-bookcovereditor--이번-프로젝트에서-내가-가장-공들인-컴포넌트">5. BookCoverEditor — 이번 프로젝트에서 내가 가장 공들인 컴포넌트</h3>
<p>AI 표지 생성 에디터다. 사용자가 스타일 옵션을 태그로 고르고, 자유 프롬프트를 입력하면 표지 후보 3장이 나온다. 마음에 드는 걸 선택해서 저장하는 흐름이다.</p>
<h4 id="상태-설계">상태 설계</h4>
<pre><code class="language-js">const [dbBookInfo, setDbBookInfo] = useState({
  title: &#39;&#39;, author: &#39;&#39;, originalContent: &#39;&#39;,
})
const [userPrompt, setUserPrompt] = useState(&#39;&#39;)
const [selectedOptions, setSelectedOptions] = useState({
  style: &#39;miki&#39;, background: &#39;beige&#39;, lighting: &#39;daylight&#39;, typography: &#39;serif&#39;,
})
const [apiConfig, setApiConfig] = useState({
  model: &#39;gpt-image-2&#39;, quality: &#39;Medium&#39;,
})
const [generatedImages, setGeneratedImages] = useState([null, null, null])
const [isGenerating, setIsGenerating] = useState(false)
const [selectedImageIndex, setSelectedImageIndex] = useState(null)</code></pre>
<p><code>generatedImages</code>를 <code>[null, null, null]</code>로 초기화한 게 포인트다. 슬롯이 항상 3개 존재하고, 생성 전에는 null, 생성 중에는 로딩 스피너, 완료 후에는 이미지를 보여주는 방식이다. 배열 인덱스로 슬롯을 관리하니 UI에서 3개를 일관되게 렌더링하기 쉬웠다.</p>
<pre><code class="language-jsx">{[0, 1, 2].map((index) =&gt; (
  &lt;div
    key={index}
    className={`${styles.imageSlot} ${selectedImageIndex === index ? styles.activeSlot : &#39;&#39;}`}
    onClick={() =&gt; generatedImages[index] &amp;&amp; setSelectedImageIndex(index)}
  &gt;
    {isGenerating ? (
      &lt;div className={styles.loadingSpinner}&gt;생성 중...&lt;/div&gt;
    ) : generatedImages[index] ? (
      &lt;img src={generatedImages[index]} alt={`표지 후보 ${index + 1}`} /&gt;
    ) : (
      &lt;span className={styles.slotText}&gt;Preview {index + 1}&lt;/span&gt;
    )}
  &lt;/div&gt;
))}</code></pre>
<p><code>isGenerating</code> 하나로 3개 슬롯을 동시에 제어한다. 생성 버튼을 누르는 순간 <code>setGeneratedImages([null, null, null])</code>로 초기화하고 <code>setIsGenerating(true)</code>로 전환해서 모든 슬롯이 동시에 로딩 상태로 바뀐다.</p>
<h4 id="모델-선택과-퀄리티-제약">모델 선택과 퀄리티 제약</h4>
<p>모델은 3가지를 지원하도록 만들었다.</p>
<pre><code class="language-js">const handleModelChange = (modelName) =&gt; {
  setApiConfig({
    model: modelName,
    quality: modelName === &#39;dall-e-3&#39; ? &#39;High&#39; : prev.quality
  })
}</code></pre>
<p>DALL-E 3는 퀄리티 옵션이 <code>High</code>만 지원한다. 모델 변경 시 자동으로 강제 설정하고, 퀄리티 선택 UI에서도 Low/Medium을 누르면 경고 알림이 뜨도록 처리했다.</p>
<pre><code class="language-js">onClick={() =&gt; {
  if (apiConfig.model === &#39;dall-e-3&#39; &amp;&amp; qualityLevel !== &#39;High&#39;) {
    alert(&#39;DALL-E 3 모델은 High 퀄리티만 선택 가능합니다.&#39;)
    return
  }
  setApiConfig({ ...apiConfig, quality: qualityLevel })
}}</code></pre>
<p>사소해 보이지만, 이런 제약 처리가 없으면 사용자가 DALL-E 3 + Low 조합을 선택한 채로 API를 쏘고 400 에러를 마주친다. 실제로 처음엔 빠뜨렸다가 테스트 중에 터졌다.</p>
<hr>
<h3 id="6-openai-api-연동--기본-흐름-이해">6. OpenAI API 연동 — 기본 흐름 이해</h3>
<p>API 호출 자체는 간단하다.</p>
<pre><code class="language-js">export async function generateBookCover(apiKey, prompt) {
  if (!apiKey) throw new Error(&#39;API Key가 없습니다.&#39;)

  const response = await fetch(&#39;https://api.openai.com/v1/images/generations&#39;, {
    method: &#39;POST&#39;,
    headers: {
      &#39;Content-Type&#39;: &#39;application/json&#39;,
      &#39;Authorization&#39;: `Bearer ${apiKey.trim()}`
    },
    body: JSON.stringify({
      model: &#39;gpt-image-2&#39;,
      prompt: prompt,
      size: &#39;1024x1536&#39;,  // 세로형 책 표지 비율
      output_format: &#39;png&#39;,
      n: 1
    })
  })

  if (!response.ok) {
    throw new Error(`OpenAI 요청 실패 (Status: ${response.status})`)
  }

  const data = await response.json()
  const b64Json = data.data?.[0]?.b64_json

  if (!b64Json) throw new Error(&#39;이미지 데이터를 찾지 못했습니다.&#39;)

  return `data:image/png;base64,${b64Json}`
}</code></pre>
<p>OpenAI가 응답으로 주는 건 base64 인코딩된 PNG 문자열(<code>b64_json</code>)이다. 이걸 <code>data:image/png;base64,...</code> 형태의 Data URL로 변환하면 <code>&lt;img src&gt;</code>에 바로 쓸 수 있다.</p>
<p><code>data.data?.[0]?.b64_json</code> 이 옵셔널 체이닝이 없으면 응답 구조가 조금이라도 달라질 때 앱 전체가 크래시된다. 실제로 처음에 빠뜨렸다가 에러를 마주쳤다. 외부 API 응답은 항상 방어적으로 접근하는 게 맞다.</p>
<hr>
<h3 id="7-1-프롬프트-→-3-이미지--핵심-설계-고민">7. 1 프롬프트 → 3 이미지 — 핵심 설계 고민</h3>
<p>미션 명세는 &quot;AI 표지 생성 버튼 클릭 → 이미지 1개 생성&quot;이었다. 그런데 내가 생각하기엔 표지 하나만 나오면 사용자가 선택권이 없다. <strong>같은 도서 내용으로 스타일이 다른 3가지 샘플을 동시에 보여줘야</strong> 실제로 유용하다고 판단했다.</p>
<p><code>gpt-image-2</code>는 <code>n: 3</code> 파라미터를 지원하지 않는다. <code>n: 1</code>만 된다. 그래서 <strong>요청을 3번 병렬로 보내는 방식</strong>으로 처리했다.</p>
<pre><code class="language-js">const handleGenerate = async () =&gt; {
  // ...유효성 검사...

  setIsGenerating(true)
  setSelectedImageIndex(null)
  setGeneratedImages([null, null, null])  // 슬롯 초기화

  // 도서 정보 + 사용자 자유 프롬프트를 합쳐서 subject 구성
  const combinedInfo = {
    title: dbBookInfo.title,
    author: dbBookInfo.author,
    content: `[Book Story]: ${dbBookInfo.originalContent} / [User Design Request]: ${userPrompt}`
  }

  const finalPrompt = buildStructuredPrompt(combinedInfo, selectedOptions)

  // 3개 동시 요청
  const generatePromises = [
    generateBookCover(apiKey, finalPrompt),
    generateBookCover(apiKey, finalPrompt),
    generateBookCover(apiKey, finalPrompt),
  ]

  const newImages = await Promise.all(generatePromises)
  setGeneratedImages(newImages)
}</code></pre>
<p><code>Promise.all</code>을 쓰면 3개 요청이 동시에 날아가서 순차 요청(3배 시간)보다 훨씬 빠르게 결과를 받을 수 있다. 사용자는 3개 이미지를 나란히 보고 마음에 드는 걸 선택해서 저장한다.</p>
<p>한 가지 추가로 고민한 부분은 <code>content</code> 조합 방식이다.</p>
<pre><code class="language-js">content: `[Book Story]: ${dbBookInfo.originalContent} / [User Design Request]: ${userPrompt}`</code></pre>
<p>db에 저장된 도서 내용과 사용자가 직접 입력한 프롬프트를 분리해서 합쳤다. 그냥 이어 붙이면 모델이 어느 게 더 중요한 지시인지 헷갈릴 수 있다. 레이블을 붙여서 두 정보의 성격이 다름을 명시적으로 알렸더니 결과물이 더 의도에 가깝게 나왔다.</p>
<hr>
<h3 id="8-프롬프트-엔지니어링--단순-텍스트-전달은-쓰레기를-만든다">8. 프롬프트 엔지니어링 — 단순 텍스트 전달은 쓰레기를 만든다</h3>
<p>이 부분이 이번 프로젝트에서 가장 실험이 많았던 부분이다.
처음에는 이렇게 프롬프트를 구성하려고 혼자 생각했었다. (따로 프로젝트 진행중 테스트 진행)</p>
<pre><code>&quot;A book cover for a book titled &#39;별빛 아래의 서점&#39;. 
Content: 작은 마을 서점의 1년을 담은 에세이.&quot;</code></pre><p>결과가 처참했다. 글자가 이상하게 들어가거나, 내가 원하는 느낌에 책 표지 이미지가 나왔다. 여러 번 재생성해도 품질이 들쑥날쑥했다.
GPT Image 2 모델이 &quot;텍스트 렌더링 성능이 개선됐다&quot;고 하지만, 그냥 자연어로 넘기면 모델이 어디에 집중해야 할지 헤맨다는 걸 체감했다. 그렇기에 <a href="https://github.com/EvoLinkAI/awesome-gpt-image-2-API-and-Prompts">OPENAI를 사용한 이미지 만드는 깃허브</a>를 참고해 조원에 태정님이 프롬프트를 구조화해서 모델에게 명확한 지시를 줄 수 있는 6섹션 구조를 만들었다.</p>
<pre><code>[STYLE]
Oil Painting Classic (visible heavy brushstrokes, rich impasto texture,
deep color palette, classic chiaroscuro lighting, canvas texture)

[SUBJECT]
A professional book cover design.
Main theme concept: [Book Story]: 작은 마을 서점의 1년을 담은 에세이 / 
[User Design Request]: 따뜻하고 고요한 느낌으로.
Title: &quot;별빛 아래의 서점&quot;. Author: &quot;홍길동&quot;.

[BACKGROUND]
Lush Nature background (soft-focus forest with dappled sunlight, natural textures)

[LIGHTING]
Warm Golden Hour (rich golden tones, long soft shadows, warm highlights)

[TYPOGRAPHY]
Serif Classic Typography layout
(book title written in elegant Serif typeface at top-center in large letters)

[TECHNICAL]
85mm portrait lens at f/1.8, razor-sharp focus, cinematic background bokeh.

[NEGATIVE]
low quality, blurry, distorted text, garbled letters, misspelled text, watermark.</code></pre><p>각 섹션이 하는 역할이 다르다.</p>
<ul>
<li><code>[STYLE]</code> — 전체적인 화풍, 렌더링 방식</li>
<li><code>[SUBJECT]</code> — 표지의 주제, 도서 정보</li>
<li><code>[BACKGROUND]</code> — 배경 분위기</li>
<li><code>[LIGHTING]</code> — 조명 설정</li>
<li><code>[TYPOGRAPHY]</code> — 제목 텍스트 처리 방식</li>
<li><code>[TECHNICAL]</code> — 사진 품질 관련 고정값 (렌즈/포커스/보케)</li>
<li><code>[NEGATIVE]</code> — 제거할 요소 명시</li>
</ul>
<p><code>[NEGATIVE]</code> 섹션이 생각보다 중요했다. 이걸 추가하고 나서 글자 깨짐이나 워터마크 같은 품질 문제가 눈에 띄게 줄었다.
각 옵션에는 영어 프롬프트 프리셋이 연결돼 있고, 사용자는 한국어 태그만 선택하면 된다.</p>
<pre><code class="language-js">export const STYLE_PRESETS = {
  &#39;수채화&#39;:    &#39;Line Art (clean lines, delicate watercolor bleeding effects, minimalist aesthetic...)&#39;,
  &#39;3D애니메이션&#39;: &#39;3D Animated style (vibrant colors, expressive characters, soft volumetric lighting...)&#39;,
  &#39;유화&#39;:      &#39;Oil Painting Classic (visible heavy brushstrokes, rich impasto texture...)&#39;,
  &#39;미니멀리즘&#39;: &#39;Modern Minimalism (bold geometric shapes, flat design elements, high contrast...)&#39;,
  &#39;빈티지&#39;:    &#39;vintage pulp fiction (gritty textures, bold halftone patterns, dramatic chiaroscuro...)&#39;,
  &#39;일러스트&#39;:  &#39;Warm Anime Illustration style (soft pastel colors, whimsical character design...)&#39;,
}</code></pre>
<p>이렇게 태그→영어 프리셋으로 매핑하는 방식을 택한 이유는 두 가지다. 첫째, 사용자에게 복잡한 영어 프롬프트를 직접 입력하게 하면 진입 장벽이 너무 높다. 둘째, 검증된 프리셋을 쓰면 결과물 품질이 더 일관적이다. 자유 프롬프트는 <code>[User Design Request]</code>에 입력을 받도록 설계해 사용자가 원하는 이미지를 만들 수 있게 했다.</p>
<hr>
<h3 id="9-이미지-압축--예상-못했던-병목">9. 이미지 압축 — 예상 못했던 병목</h3>
<p>Data URL은 생각보다 훨씬 크다. 실제로 OpenAI에서 받은 원본 Data URL 하나가 <strong>1~2MB</strong> 가까이 됐다. 이걸 그대로 json-server에 PATCH로 저장하면 <code>db.json</code> 파일이 순식간에 수십 MB가 된다. 로딩이 느려지고 심하면 json-server가 불안정해진다.
그래서 <strong>Canvas API를 활용한 이미지 압축 함수</strong>를 직접 만들었다.</p>
<pre><code class="language-js">export async function compressImageDataUrl(dataUrl, maxBytes = 75000) {
  if (!dataUrl?.startsWith(&#39;data:image/&#39;)) return dataUrl || &#39;&#39;
  if (dataUrl.length &lt;= maxBytes) return dataUrl  // 이미 충분히 작으면 패스

  const image = await new Promise((resolve, reject) =&gt; {
    const img = new Image()
    img.onload = () =&gt; resolve(img)
    img.onerror = () =&gt; reject(new Error(&#39;이미지를 압축할 수 없습니다.&#39;))
    img.src = dataUrl
  })

  const canvas = document.createElement(&#39;canvas&#39;)
  const ctx = canvas.getContext(&#39;2d&#39;)
  const aspect = image.height / image.width || 1.5  // 비율 유지

  const widths = [360, 320, 280, 240, 200, 180]
  const qualities = [0.72, 0.62, 0.52, 0.42, 0.34]

  for (const width of widths) {
    canvas.width = width
    canvas.height = Math.round(width * aspect)
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    ctx.drawImage(image, 0, 0, canvas.width, canvas.height)

    for (const quality of qualities) {
      const compressed = canvas.toDataURL(&#39;image/jpeg&#39;, quality)
      if (compressed.length &lt;= maxBytes) return compressed
    }
  }

  return canvas.toDataURL(&#39;image/jpeg&#39;, 0.28)  // 최후 수단
}</code></pre>
<p>핵심 로직은 이렇다.</p>
<ol>
<li>이미 목표 크기(75KB) 이하면 바로 반환</li>
<li>Canvas로 이미지 그리기 → <code>toDataURL(&#39;image/jpeg&#39;, quality)</code>로 압축</li>
<li>너비 360px부터 180px까지 단계적으로 줄이면서, 각 너비에서 품질도 0.72부터 0.34까지 낮춤</li>
<li>목표 크기 이하가 되는 첫 조합을 반환</li>
<li>모두 실패하면 <code>quality: 0.28</code>로 강제 반환</li>
</ol>
<p>PNG(원본) → JPEG(압축)로 포맷도 바뀐다. 표지 미리보기용으로는 충분한 품질이 나왔고, 저장 용량은 원본 대비 95% 이상 줄었다.
<code>aspect ratio</code>를 <code>image.height / image.width || 1.5</code> 로 계산하는 이유도 있다. 이미지 로딩이 실패하거나 width가 0인 경우 나누기 0이 되는데, <code>|| 1.5</code>로 fallback해서 책 표지 기본 비율(세로형)을 유지하도록 했다.</p>
<hr>
<h3 id="10-예외처리--사용자에게-무슨-일이-일어났는지-알려줘야-한다">10. 예외처리 — 사용자에게 무슨 일이 일어났는지 알려줘야 한다</h3>
<p>OpenAI API를 다루면서 예외처리가 생각보다 많이 필요하다는 걸 깨달았다. 처리하지 않으면 화면이 그냥 멈추거나 흰 화면이 뜬다. 사용자 입장에서 가장 당황스러운 경험이다.</p>
<p><strong>API Key 관련</strong></p>
<pre><code class="language-js">if (!apiKey) throw new Error(&#39;API Key가 없습니다.&#39;)
// → .env 파일에 VITE_OPENAI_API_KEY 설정 필요 안내</code></pre>
<p><strong>HTTP 응답 실패</strong></p>
<pre><code class="language-js">if (!response.ok) {
  throw new Error(`OpenAI 요청 실패 (Status: ${response.status})`)
  // 401 → API Key 오류
  // 429 → Rate Limit 초과
  // 500 → OpenAI 서버 오류
}</code></pre>
<p><strong>응답 데이터 누락</strong></p>
<pre><code class="language-js">const b64Json = data.data?.[0]?.b64_json
if (!b64Json) throw new Error(&#39;이미지 데이터를 찾지 못했습니다.&#39;)</code></pre>
<p><strong>이미지 압축 실패</strong></p>
<pre><code class="language-js">img.onerror = () =&gt; reject(new Error(&#39;이미지를 압축할 수 없습니다.&#39;))
// → 압축 실패 시 원본 반환으로 fallback</code></pre>
<p><strong>저장 실패</strong></p>
<pre><code class="language-js">if (response.ok) {
  alert(&#39;표지가 성공적으로 수정되었습니다!&#39;)
  navigate(-1)
} else {
  throw new Error(&#39;저장 실패&#39;)
}</code></pre>
<p>try-catch 안에서 에러를 잡고 <code>alert</code>로 사용자에게 알리는 방식이다. 실제 서비스라면 toast 알림이나 에러 상태 UI로 더 세련되게 처리하겠지만, 프로젝트 범위 안에서는 이 정도로 충분하다고 판단했다.</p>
<hr>
<h3 id="11-코드-병합--가장-힘든-작업이었다">11. 코드 병합 — 가장 힘든 작업이었다</h3>
<p>팀에서 PR을 제대로 진행하지 못했다. 각자 로컬에서 개발한 코드를 파일로 공유하는 방식이었는데, 이걸 내가 직접 하나씩 합쳤다. (처음 개발 진행시 틀을 잡지 못하고 시작하게 크다.. 나의 잘못이라고 생각한다.)
병합하면서 실제로 마주친 문제들:</p>
<ul>
<li><strong>변수명 충돌</strong>
한 쪽은 <code>book</code>, 다른 쪽은 <code>bookData</code>, 또 다른 쪽은 <code>currentBook</code>. 어느 게 맞는지 기준이 없어서 일일이 읽어가며 통일했다.</li>
<li><strong>상태 관리 불일치</strong>
A는 <code>useState</code>로 직접 관리, B는 props로 내려받는 구조가 혼용됐다. 데이터 흐름이 섞이면 어디서 상태를 갱신해야 하는지 판단하기 어렵다.</li>
<li><strong>API 호출 방식 혼재</strong>
<code>fetch</code>를 컴포넌트 안에서 직접 쓴 곳, 별도 함수로 분리한 곳이 섞였다. 에러 처리 방식도 다 달랐다.</li>
<li><strong>CSS 클래스 중복</strong>
같은 클래스명인데 스타일이 다른 경우가 있었다. CSS Module을 쓴 곳과 일반 클래스를 쓴 곳이 혼재했다.</li>
</ul>
<p>이걸 하나하나 읽으면서 충돌 없이 합치고, 라우팅 흐름이 끊기지 않는지 확인하면서 전체 앱이 돌아가게 만드는 과정이 생각보다 훨씬 오래 걸렸다. 검색바 동작 방식(<code>useMemo</code> + 클라이언트 사이드 필터링), 리스트 카드 클릭 → 상세 전환 흐름, 등록/수정 폼의 상태 초기화 타이밍 같은 디테일도 이 과정에서 직접 정리했다.</p>
<hr>
<h2 id="생각정리">생각정리</h2>
<p>이번 04miniproject를 진행하며 어려웠던 점, 느낀점, 다음계획(프로젝트가 끝난후 따로 진행하고 있다)에 대해 정리해보자구..</p>
<h2 id="어려웠던-점">어려웠던 점</h2>
<h3 id="b64_json이란">b64_json이란?</h3>
<p>OpenAI 응답에서 이미지가 URL이 아니라 base64 문자열로 온다는건 알았지만 dn.json에서는 큰 용량을 처리하는데에 한계가 있었다. 응답 JSON을 콘솔에 찍어봤더니 <code>data[0].b64_json</code> 에 엄청 긴 문자열이 들어있었다. db.json 서버를 사용할떄는 <code>data:image/png;base64,...</code>로 변환해야 <code>&lt;img src&gt;</code>에 쓸 수 있다는 것도 직접 해보고 나서야 자연스럽게 이해됐다.</p>
<h3 id="promiseall에서-하나가-rate-limit에-걸리면">Promise.all에서 하나가 Rate Limit에 걸리면?</h3>
<p>3개 이미지 동시 생성에서 <code>Promise.all</code>을 쓰면 하나라도 실패하면 전체가 reject된다. Rate Limit(429)이 걸릴 때 이 문제가 생겼다. 3개 요청 중 1개만 실패해도 나머지 2개 결과가 다 날아간다. 나중에 <code>Promise.allSettled</code>로 바꿔서 실패한 것만 <code>null</code> 처리하는 방식을 검토했다. 이렇게 하면 2개는 성공, 1개는 null 슬롯으로 보여줄 수 있다.</p>
<h3 id="이미지가-json-server에-저장은-됐는데-목록에-안-보임">이미지가 json-server에 저장은 됐는데 목록에 안 보임</h3>
<p>PATCH 요청이 성공했는데 도서 목록으로 돌아오면 표지가 안 나왔다. 원인은 <code>books</code> 상태가 최초 fetch 이후 갱신이 안 된 것이었다. <code>navigate(-1)</code> 로 뒤로 가면 컴포넌트가 언마운트/리마운트되는데, 이때 useEffect의 fetch가 다시 실행돼야 한다. 의존성 배열을 확인하고 재fetch 트리거를 명시적으로 추가해서 해결했다.</p>
<h3 id="data-url이-너무-커서-json-server가-느려짐">Data URL이 너무 커서 json-server가 느려짐</h3>
<p>앞서 언급한 이미지 압축 문제다. 처음엔 원본 Data URL을 그대로 저장했더니 <code>db.json</code>이 수십 MB가 됐고, json-server의 응답 속도가 눈에 띄게 느려졌다. 압축 함수를 만들고 나서 해결됐다.</p>
<hr>
<h2 id="느낀-점">느낀 점</h2>
<h3 id="ui-개발은-그리는-게-아니라-설계하는-것이다">UI 개발은 그리는 게 아니라 설계하는 것이다</h3>
<p>이번 전까지 UI 개발은 &quot;화면을 예쁘게 만드는 것&quot;이라고 생각했다. 실제로 해보니 완전히 달랐다.
라우팅 설계, 컴포넌트 인터페이스 정의, 상태 흐름 설계, API 연동, 예외처리, 사용자 피드백 처리까지 전부 UI 개발의 영역이었다. 화면을 만드는 건 그 중 일부일 뿐이다. 가장 먼저 해야 할 것이 &quot;어떻게 생겼는가&quot;가 아니라 &quot;데이터가 어떻게 흐르는가&quot;라는 걸 이번에 확실히 배웠다.</p>
<h3 id="1-프롬프트-→-3-이미지는-기술-문제가-아니라-ux-문제였다">&quot;1 프롬프트 → 3 이미지&quot;는 기술 문제가 아니라 UX 문제였다</h3>
<p>교안에서 요구한 건 이미지 1개였다. 그걸 3개로 늘린 건 기술적 도전이 아니라 <strong>&quot;사용자가 실제로 이걸 어떻게 쓸까&quot;</strong> 를 생각한 우리조의 토론 결과였다. 표지를 딱 하나만 주면 선택의 여지가 없다. 마음에 안 들면 재생성 버튼을 계속 누르게 된다. 처음부터 3가지를 주면 그 중 하나는 마음에 들 가능성이 높다. API 비용은 3배 들지만 사용자 경험은 훨씬 낫다. 기능 구현보다 사용 맥락을 먼저 생각하는 습관이 이번에 좀 생긴 것 같다.</p>
<h3 id="프롬프트-엔지니어링은-확실히-기술이다">프롬프트 엔지니어링은 확실히 기술이다</h3>
<p>&quot;좋은 표지 그려줘&quot; 와 <code>[STYLE] Oil Painting Classic... [NEGATIVE] low quality, blurry...</code> 는 결과물이 완전히 다르다. 언어를 구조화해서 모델이 집중할 지점을 명확하게 만드는 것, 제거할 요소를 NEGATIVE로 명시하는 것 — 이게 다 기술이다. 이전 프로젝트들에서 Multi-Agent나 LangGraph를 다뤘을 때도 프롬프트가 중요했지만, 이미지 생성에서는 그 차이가 훨씬 직관적으로 느껴졌다. </p>
<h3 id="협업은-처음부터-규칙이-있어야-한다">협업은 처음부터 규칙이 있어야 한다</h3>
<p>이번 프로젝트에서 제일 힘든 작업이 코드 병합이었다. 사람마다 코딩 스타일이 다르고 변수명도 다르다. PR도 없이 파일로 코드를 주고받으면 충돌이 생길 수밖에 없다. 다음에는 처음부터 <strong>브랜치 전략</strong>, <strong>PR 규칙</strong>, <strong>컨벤션(변수명, 컴포넌트 구조)</strong> 을 팀 안에서 먼저 합의하고 시작해야겠다. 이걸 나중에 맞추려고 하면 이미 늦다는 걸 뼈저리게 느꼈다. 아이러니하게도, 병합 작업 덕분에 팀 전체 코드를 가장 잘 아는 사람이 됐다. 모든 파일을 다 읽었으니까. 나쁘지 않은걸까..?</p>
<hr>
<h2 id="다음-목표-후에-spring도-연결-예정이니">다음 목표 (후에 Spring도 연결 예정이니)</h2>
<ul>
<li><strong>Git 협업 플로우 제대로 경험하기</strong> — 브랜치 전략, PR, 코드 리뷰</li>
<li><strong>React 상태 관리 깊이 파기</strong> — React Query로 서버 상태와 클라이언트 상태 분리</li>
<li><strong>실제 백엔드 연결</strong> — json-server → Spring Boot 교체 (백엔드 미니프로젝트 예정)</li>
<li><strong>Promise.allSettled 패턴</strong> — 일부 실패해도 나머지 결과는 보여주기</li>
</ul>
<p>3일이라는 짧은 기간 동안 진행한 프로젝트였기에, 더 많은 시간을 투자했다면 더욱 완성도 높은 결과물을 만들 수 있었을 것이다. 하지만 제한된 시간 속에서도 모든 조원이 각자의 역할에 최선을 다해 주었기에 의미 있는 결과물을 만들어낼 수 있었다고 생각한다. 이번 프로젝트는 단순히 결과물을 완성하는 것에서 끝나지 않고, 프로젝트가 끝난 이후에도 더 공부하고 배워야 할 점들과 부족했던 부분을 채울 수 있는 프로젝트였다. 9주차 프로젝트를 마무리한다.</p>
<blockquote>
<p>12조 파이팅!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 8주차  / Frontend]]></title>
            <link>https://velog.io/@mi_nini/KT-A-8%EC%A3%BC%EC%B0%A8-Frontend</link>
            <guid>https://velog.io/@mi_nini/KT-A-8%EC%A3%BC%EC%B0%A8-Frontend</guid>
            <pubDate>Mon, 01 Jun 2026 15:13:03 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<h3 id="목차">목차</h3>
<ul>
<li><a href="#%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0">들어가며</a></li>
<li><a href="#vs-code-%EC%84%B8%ED%8C%85%EB%B6%80%ED%84%B0-htmlcss-%EB%BC%88%EB%8C%80-%EC%9E%A1%EA%B8%B0">1. VS Code 세팅부터 HTML/CSS 뼈대 잡기</a><ul>
<li><a href="#semantic-tag%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%AC%B8%EC%84%9C-%EA%B5%AC%EC%A1%B0%ED%99%94">Semantic Tag를 활용한 문서 구조화</a></li>
<li><a href="#css-flexbox%EB%A1%9C-%EC%9C%A0%EC%97%B0%ED%95%98%EA%B3%A0-%EA%B9%94%EB%81%94%ED%95%9C-%EB%A0%88%EC%9D%B4%EC%95%84%EC%9B%83-%EA%B5%AC%EC%84%B1">CSS Flexbox로 유연하고 깔끔한 레이아웃 구성</a></li>
</ul>
</li>
<li><a href="#javascript--react-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%A4%EA%B3%84">2. JavaScript / React 컴포넌트 설계</a><ul>
<li><a href="#javascript-%EC%8B%AC%ED%99%94-%EB%B0%B0%EC%97%B4-%EB%82%B4%EC%9E%A5-%ED%95%A8%EC%88%98-map-filter-reduce">JavaScript 심화: 배열 내장 함수</a></li>
</ul>
</li>
<li><a href="#react-%EC%9E%85%EB%AC%B8-%EB%B0%8F-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8component-%EB%B6%84%EB%A6%AC">3. React 입문 및 컴포넌트(Component) 분리</a><ul>
<li><a href="#vite-react-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1">Vite React 프로젝트 생성</a></li>
<li><a href="#props%EB%A5%BC-%ED%86%B5%ED%95%9C-%EB%8B%A8%EB%B0%A9%ED%96%A5-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%9D%90%EB%A6%84-%EC%A0%9C%EC%96%B4">Props를 통한 단방향 데이터 흐름 제어</a></li>
</ul>
</li>
<li><a href="#react-state-%EA%B4%80%EB%A6%AC%EC%99%80-crud-%EA%B5%AC%ED%98%84">4. React State 관리와 CRUD 구현</a><ul>
<li><a href="#usestate%EC%99%80-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%95%B8%EB%93%A4%EB%A7%81">useState와 이벤트 핸들링</a></li>
<li><a href="#%ED%8F%BCform-%EC%A0%9C%EC%96%B4%EC%99%80-%EC%A1%B0%EA%B1%B4%EB%B6%80-%EB%A0%8C%EB%8D%94%EB%A7%81">폼(Form) 제어와 조건부 렌더링</a></li>
<li><a href="#%EB%B6%88%EB%B3%80%EC%84%B1%EC%9D%84-%EC%9C%A0%EC%A7%80%ED%95%98%EB%8A%94-create--delete-%EB%A1%9C%EC%A7%81-%EA%B5%AC%ED%98%84">불변성을 유지하는 Create / Delete 로직 구현</a></li>
</ul>
</li>
<li><a href="#%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC%EC%99%80-api-%EC%97%B0%EB%8F%99">5. 비동기 처리와 API 연동</a><ul>
<li><a href="#useeffect%EC%99%80-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0-%EC%A0%9C%EC%96%B4">useEffect와 컴포넌트 생명주기 제어</a></li>
<li><a href="#axios%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-rest-api-%EC%97%B0%EB%8F%99-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC">Axios를 활용한 REST API 연동 (비동기 처리)</a></li>
</ul>
</li>
<li><a href="#%EC%83%9D%EA%B0%81%EC%A0%95%EB%A6%AC">6. 생각정리</a></li>
</ul>
<blockquote>
<p><a href="https://ko.react.dev/learn">Raact 학습_ko</a></p>
</blockquote>
<hr>
<h1 id="vs-code-세팅부터-htmlcss-뼈대-잡기">VS Code 세팅부터 HTML/CSS 뼈대 잡기</h1>
<p>4일간의 프론트엔드 강의를 진행했다. 1일차에 목표는 React를 활용한 동적인 SPA(Single Page Application)를 구축하는것이다.</p>
<h2 id="semantic-tag를-활용한-문서-구조화">Semantic Tag를 활용한 문서 구조화</h2>
<p>본격적으로 게시판을 제작하며 가장 강조된 부분은 어떻게 하면 문서의 구조를 의미론적으로 명확하게 짤 수 있을까? 였다. 
단순히 화면의 구역을 나누기 위해 무의미한 <code>&lt;div&gt;</code> 태그만을 남발하지 않고, 철저하게 Semantic Tag를 활용에 대해 학습했다. 시맨틱 태그를 사용하면 검색 엔진 최적화에 유리하다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;title&gt;미니 게시판 프로젝트&lt;/title&gt;
  &lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;header class=&quot;board-header&quot;&gt;
    &lt;h1&gt;미니 게시판&lt;/h1&gt;
    &lt;nav&gt;
      &lt;button class=&quot;btn-create&quot;&gt;새 글 작성&lt;/button&gt;
    &lt;/nav&gt;
  &lt;/header&gt;

  &lt;main class=&quot;board-container&quot;&gt;
    &lt;section class=&quot;post-list-section&quot;&gt;

      &lt;article class=&quot;post-item&quot;&gt;
        &lt;h2 class=&quot;post-title&quot;&gt;프론트엔드 회고입니다.&lt;/h2&gt;
        &lt;div class=&quot;post-meta&quot;&gt;
          &lt;span class=&quot;author&quot;&gt;작성자: 김경민&lt;/span&gt;
          &lt;span class=&quot;date&quot;&gt;2026-05-15&lt;/span&gt;
        &lt;/div&gt;
      &lt;/article&gt;

      &lt;article class=&quot;post-item&quot;&gt;
        &lt;h2 class=&quot;post-title&quot;&gt;시맨틱 태그와 VS Code 세팅&lt;/h2&gt;
        &lt;div class=&quot;post-meta&quot;&gt;
          &lt;span class=&quot;author&quot;&gt;작성자: 이순신&lt;/span&gt;
          &lt;span class=&quot;date&quot;&gt;2026-05-15&lt;/span&gt;
        &lt;/div&gt;
      &lt;/article&gt;

    &lt;/section&gt;
  &lt;/main&gt;
&lt;/body&gt;
&lt;/html&gt; </code></pre>
<p>이렇게 명확하게 나누어진 구조는 당장 코드를 읽기 편하게 해줄 뿐만 아니라, React를 학습할 때 <code>header</code> 영역은 Header 컴포넌트로, <code>article</code>영역은 PostItem 컴포넌트로 분리하는데 도움이 된다.</p>
<h3 id="css-flexbox로-유연하고-깔끔한-레이아웃-구성">CSS Flexbox로 유연하고 깔끔한 레이아웃 구성</h3>
<p>간단하게 CSS를 통해 시각적인 레이아웃을 잡았다. 게시판 UI를 화면에 보기 좋게 배치하기 익힌 기술은 CSS Flexbox였다. 요소들을 가로로 배치하기 위해 복잡하게 위치를 계산하거나 화면이 깨지는 현상을 걱정할 필요 없이, Flexbox를 사용하면 1차원(가로 또는 세로) 레이아웃을 매우 직관적으로 제어할 수 있다. VS Code의 화면을 반으로 분할하여 왼쪽에는 HTML을, 오른쪽에는 CSS를 띄워두고 게시판의 디자인을 입혀나갔다. 부모 컨테이너에 display: flex를 선언하고, 주축(Main Axis)과 교차축(Cross Axis)을 정렬하는 코드를 작성했다.</p>
<pre><code class="language-css">* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.board-header {
  display: flex;
  justify-content: space-between; 
  align-items: center; 
  padding: 20px 40px;
  background-color: #2c3e50;
  color: white;
}

.board-container {
  display: flex;
  flex-direction: column; /* 세로 방향으로 자식 요소 배치 */
  gap: 20px; /* 요소들 사이의 일정한 간격 유지 */
  padding: 40px;
  max-width: 800px;
  margin: 0 auto; /* 화면 중앙 정렬 */
}

.post-item {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 20px;
  background-color: #f8f9fa;
  border: 1px solid #dee2e6;
  border-radius: 8px;
}

.post-meta {
  display: flex;
  justify-content: flex-start;
  gap: 15px;
  font-size: 0.9rem;
  color: #6c757d;
}</code></pre>
<p>특히 justify-content: space-between을 통해 헤더의 제목과 버튼을 손쉽게 양 끝으로 밀어내고, gap 속성을 사용해 margin 겹침 현상에 대한 고민 없이 요소 간의 여백을 일정하게 주입하는 과정이 편리했다. 브라우저 창 크기를 이리저리 줄였다 늘여도 레이아웃이 유연하게 변경되는 Flexbox를 잘 이용하자</p>
<hr>
<h1 id="javascript--react-컴포넌트-설계">JavaScript / React 컴포넌트 설계</h1>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/91bbee2d-3121-4aa7-bb71-75700ac8e5a9/image.png" alt=""></p>
<p>만들어놓은 HTML/CSS에 웹 페이지에 데이터를 다루고 사용자와 상호작용하는 Modern JavaScript의 핵심 문법을 배우고 React를 학습했다.</p>
<h2 id="javascript---배열-내장-함수-map-filter-reduce">JavaScript - 배열 내장 함수 (map, filter, reduce)</h2>
<p>게시판 프로젝트를 진행하면서 가장 많이 다루게 될 데이터 형태는 결국 &#39;게시글 객체들이 담긴 배열(Array)&#39;이다. 서버로부터 여러 개의 게시글 데이터를 받아와 화면에 뿌려주고, 특정 글을 삭제하거나 검색해야 하기 때문이다. 이를 위해 기존의 지루한 <code>for</code> 루프를 벗어나, ES6+의 강력한 배열 내장 함수들을 학습했다.</p>
<blockquote>
<p>불변성을 지키면서 원본 배열을 훼손하지 않고 새로운 배열을 반환하는 이 함수들은 추후 React의 상태 관리 로직에서 핵심적인 역할을 한다.</p>
</blockquote>
<ul>
<li>map(): 배열의 모든 요소를 순회하며 내가 원하는 형태(주로 HTML/JSX UI 요소)로 가공하여 새로운 배열을 만들어낸다. 게시글 목록을 렌더링할 때 필수적이다.</li>
<li>filter(): 조건에 맞는 요소들만 걸러내어 새로운 배열을 만든다. 게시글 삭제 기능이나 검색 기능을 구현할 때 주로 사용된다.</li>
<li>reduce(): 배열의 모든 값을 하나로 누적하여 병합한다. 총 게시글 수 계산이나 복잡한 데이터 포맷팅에 유용하다.</li>
</ul>
<pre><code class="language-javascript">const posts = [
  { id: 1, title: &#39;프론트엔드 1일차 회고&#39;, author: &#39;김경민&#39; },
  { id: 2, title: &#39;React 너무 재밌네요&#39;, author: &#39;김민&#39; },
  { id: 3, title: &#39;JavaScript 배열 함수 정리&#39;, author: &#39;경민&#39; }
];

const postTitles = posts.map(post =&gt; `제목: ${post.title}`);
console.log(postTitles); 
// [&quot;제목: 프론트엔드 1일차 회고&quot;, &quot;제목: React 너무 재밌네요&quot;, &quot;제목: JavaScript 배열 함수 정리&quot;]

const filteredPosts = posts.filter(post =&gt; post.id !== 2);
console.log(filteredPosts); // id가 1, 3인 객체만 남은 새 배열 반환</code></pre>
<blockquote>
<p>출처/참고
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array">https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array</a>
<a href="https://adjh54.tistory.com/66">https://adjh54.tistory.com/66</a></p>
</blockquote>
<hr>
<h2 id="react-입문-및-컴포넌트component-분리">React 입문 및 컴포넌트(Component) 분리</h2>
<blockquote>
<p>들어가기전 VS Code 터미널에서 create-react-app 또는 Vite를 통해 기본 React 환경을 세팅하고 갑시다.</p>
</blockquote>
<h3 id="vite-react-프로젝트-생성">Vite React 프로젝트 생성</h3>
<table>
<thead>
<tr>
<th align="center">단계</th>
<th>명령어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">1</td>
<td><code>npm create vite@latest</code></td>
<td>프로젝트 생성 및 템플릿 선택</td>
</tr>
<tr>
<td align="center">2</td>
<td><code>cd my-app</code></td>
<td>생성한 프로젝트 폴더로 이동</td>
</tr>
<tr>
<td align="center">3</td>
<td><code>npm install</code></td>
<td>필요한 라이브러리 설치</td>
</tr>
<tr>
<td align="center">4</td>
<td><code>npm run dev</code></td>
<td>개발 서버 실행</td>
</tr>
</tbody></table>
<h3 id="전체-명령어">전체 명령어</h3>
<pre><code class="language-bash">npm create vite@latest
cd my-app
npm install
npm run dev</code></pre>
<h3 id="실행-결과">실행 결과</h3>
<table>
<thead>
<tr>
<th>주소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>http://localhost:5173</code></td>
<td>개발 서버 기본 실행 주소</td>
</tr>
</tbody></table>
<p>React의 가장 큰 장점 중 하나는 위에서 만들었던 &#39;통짜 HTML&#39; 문서를 레고 블록과 같은 Component 단위로 잘게 쪼개어 재사용할 수 있다는 것이다. 유지보수의 효율성을 극대화하기 위해, 나는 게시판 UI를 크게 Header, PostList(게시글 목록), PostItem(개별 게시글)이라는 독립적인 함수형 컴포넌트로 분리했다.
이렇게 컴포넌트를 분리하면 각 파일이 맡은 역할이 명확해져 코드를 읽고 수정하기가 훨씬 수월해진다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/90033b78-1ace-4178-9c7d-7fa99936d3fc/image.png" alt=""></p>
<h2 id="props를-통한-단방향-데이터-흐름-제어">Props를 통한 단방향 데이터 흐름 제어</h2>
<p>컴포넌트를 여러 개로 분리하고 나면 필연적으로 &quot;데이터를 어떻게 주고받을 것인가?&quot;라는 문제에 부딪힌다. React는 부모 컴포넌트에서 자식 컴포넌트로만 데이터가 흐르는 단방향 데이터 바인딩 원칙을 가진다. 이때 전달되는 데이터의 매개체가 바로 Props다.
앞서 배운 map() 함수와 React의 Props 개념을 결합하여, 최상위 컴포넌트(App.js)가 가진 배열 데이터를 하위 컴포넌트(PostItem)까지 안전하게 전달하여 화면에 그리는 로직을 구현했다.</p>
<pre><code class="language-jsx">// 1. 개별 게시글을 담당하는 자식 컴포넌트 (PostItem.jsx)
// 부모로부터 post 객체를 Props로 전달받아 화면에 렌더링한다.
const PostItem = ({ post }) =&gt; {
  return (
    &lt;article className=&quot;post-item&quot;&gt;
      &lt;h2 className=&quot;post-title&quot;&gt;{post.title}&lt;/h2&gt;
      &lt;div className=&quot;post-meta&quot;&gt;
        &lt;span className=&quot;author&quot;&gt;작성자: {post.author}&lt;/span&gt;
      &lt;/div&gt;
    &lt;/article&gt;
  );
};

export default PostItem;


// 2. 게시글 목록을 렌더링하는 부모 컴포넌트 (PostList.jsx)
// 서버에서 받아왔다고 가정한 데이터 배열을 map으로 순회하며 자식에게 Props로 넘겨준다.
import PostItem from &#39;./PostItem&#39;;

const PostList = () =&gt; {
  const dummyData = [
    { id: 1, title: &#39;프론트엔드 1일차 회고&#39;, author: &#39;김경민&#39; },
    { id: 2, title: &#39;React 입문기&#39;, author: &#39;김민&#39; }
  ];

  return (
    &lt;section className=&quot;post-list-section&quot;&gt;
      {/* 배열의 map 함수를 이용해 컴포넌트를 반복 생성 */}
      {dummyData.map((post) =&gt; (
        // 컴포넌트 반복 시 고유한 key 값을 반드시 부여해야 함
        &lt;PostItem key={post.id} post={post} /&gt; 
      ))}
    &lt;/section&gt;
  );
};

export default PostList;</code></pre>
<blockquote>
<p>출처/참고
<a href="https://yhuj79.github.io/Vue/241230/">https://yhuj79.github.io/Vue/241230/</a>
<a href="https://dkkim2318.tistory.com/161">https://dkkim2318.tistory.com/161</a></p>
</blockquote>
<hr>
<h1 id="react-state-관리와-crud-구현">React State 관리와 CRUD 구현</h1>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/84df7c7f-5dc3-4c75-87c0-15dd68ca387b/image.png" alt=""></p>
<p>단순히 정해진 데이터를 보여주는 것이 아닌, 사용자가 직접 글을 작성하고 삭제할 수 있는 동적인 CRUD(Create, Read, Update, Delete) 게시판을 프론트엔드 단에서 완벽하게 제어하는 실습을 가졌다.</p>
<h2 id="usestate와-이벤트-핸들링">useState와 이벤트 핸들링</h2>
<p>React에서 가장 중요하고 혁신적인 개념을 꼽으라면 State 관리일 것이다. 일반적인 JavaScript 변수는 값이 바뀌어도 화면이 스스로 업데이트되지 않지만, React의 <code>useState</code> Hook을 통해 생성된 상태는 값이 변경될 때마다 컴포넌트를 자동으로 다시 렌더링하게 만든다.
사용자가 입력창에 글씨를 타이핑하거나 버튼을 클릭하는 등의 행동을 Event라고 한다. 나는 <code>onChange</code>와 <code>onClick</code> 같은 이벤트 핸들러를 통해 사용자의 동작을 감지하고, 그 결과를 <code>useState</code>의 상태 변경 함수(setState)에 전달하여 화면이 즉각적으로 갱신되도록 로직을 짰다. </p>
<h2 id="폼form-제어와-조건부-렌더링">폼(Form) 제어와 조건부 렌더링</h2>
<p>게시판의 핵심인 &#39;글 작성&#39; 기능을 만들기 위해 입력 폼을 제어하는 방법을 익혔다. React에서는 사용자의 입력값을 DOM 자체에 맡겨두는 것이 아니라, React의 State를 &#39;신뢰 가능한 단일 출처&#39;로 삼는 제어 컴포넌 패턴을 권장한다.</p>
<p>입력 필드의 <code>value</code> 속성을 State와 연결하여 폼을 완벽하게 제어했고, 사용자가 빈 값을 제출하려 할 때 제출을 막고 경고창을 띄우는 간단한 유효성 검사도 추가했다. 
또한, 상태 값에 따라 화면의 UI가 다르게 보이도록 하는 조건부 렌더링 개념도 도입했다. 예를 들어, 게시글 배열의 길이가 0일 때는 &quot;작성된 게시글이 없습니다&quot;라는 안내 문구를 보여주고, 게시글이 존재할 때만 목록 컴포넌트를 렌더링하도록 삼항 연산자를 적극 활용했다.</p>
<h2 id="불변성을-유지하는-create--delete-로직-구현">불변성을 유지하는 Create / Delete 로직 구현</h2>
<p>가장 많은 고민을 했던 부분은 상태로 관리되는 배열에 새로운 데이터를 추가하고 삭제하는 로직이었다. 
React에서는 상태를 변경할 때 반드시 불변성을 지켜야 한다. 기존 배열을 직접 수정해버리는 <code>push()</code>나 <code>splice()</code> 같은 메서드는 사용할 수 없었다.</p>
<ul>
<li><strong>Create (추가):</strong> 전개 연산자(<code>...</code>)를 사용하여 기존 배열의 요소들을 새로운 배열에 흩뿌리듯 복사한 뒤, 그 뒤에 새로운 게시글 객체를 덧붙여 새로운 배열을 반환하도록 했다.</li>
<li><strong>Delete (삭제):</strong> 특정 게시글의 삭제 버튼을 누르면, 해당 게시글의 고유 <code>id</code>를 인자로 전달받아 <code>filter()</code> 함수를 돌렸다. 즉, 삭제하려는 <code>id</code>와 일치하지 않는 게시글들만 남긴 &#39;새로운 배열&#39;로 상태를 통째로 교체하는 방식이다.</li>
</ul>
<p>이러한 불변성 유지 로직을 실제 코드로 구현한 모습은 다음과 같다.</p>
<pre><code class="language-javascript">import React, { useState } from &#39;react&#39;;
import PostList from &#39;./PostList&#39;; // 2일차에 만든 목록 컴포넌트 불러오기

const BoardApp = () =&gt; {
  // 게시글 목록 상태 관리
  const [posts, setPosts] = useState([
    { id: 1, title: &#39;리액트 상태 관리 학습&#39;, author: &#39;김경민&#39; }
  ]);

  // 폼 입력창 상태 관리
  const [inputValue, setInputValue] = useState(&#39;&#39;);

  // Create: 폼 제출 시 새로운 게시글 추가
  const handleSubmit = (e) =&gt; {
    e.preventDefault(); // 브라우저의 기본 새로고침 동작 방지

    // 유효성 검사: 빈 문자열 제출 방지
    if (inputValue.trim() === &#39;&#39;) {
      alert(&#39;내용을 입력해주세요.&#39;);
      return;
    }

    const newPost = {
      id: Date.now(), // 고유한 식별자를 위해 현재 시간의 타임스탬프 활용
      title: inputValue,
      author: &#39;익명&#39;
    };

    // 기존 배열을 건드리지 않고(불변성 유지) 새 데이터가 추가된 새 배열로 상태 변경
    setPosts([...posts, newPost]);

    // 글 작성이 완료되면 입력창 초기화
    setInputValue(&#39;&#39;); 
  };

  // Delete: 특정 게시글 삭제
  const handleDelete = (idToRemove) =&gt; {
    // filter를 이용해 삭제 대상 id와 일치하지 않는 요소만 걸러내어 새 배열 생성
    const filteredPosts = posts.filter(post =&gt; post.id !== idToRemove);
    setPosts(filteredPosts);
  };

  return (
    &lt;div className=&quot;board-container&quot;&gt;
      &lt;h2&gt;새 글 작성&lt;/h2&gt;
      {/* 폼 제출 이벤트 핸들링 */}
      &lt;form onSubmit={handleSubmit}&gt;
        &lt;input 
          type=&quot;text&quot;
          value={inputValue}
          onChange={(e) =&gt; setInputValue(e.target.value)}
          placeholder=&quot;게시글 제목을 입력하세요&quot;
        /&gt;
        &lt;button type=&quot;submit&quot;&gt;등록&lt;/button&gt;
      &lt;/form&gt;

      &lt;hr /&gt;

      {/* 조건부 렌더링: 게시글 유무에 따라 다른 UI 노출 */}
      {posts.length === 0 ? (
        &lt;p className=&quot;empty-message&quot;&gt;아직 작성된 게시글이 없습니다.&lt;/p&gt;
      ) : (
        &lt;PostList posts={posts} onDelete={handleDelete} /&gt;
      )}
    &lt;/div&gt;
  );
};

export default BoardApp;</code></pre>
<blockquote>
<p>출처/참고
<a href="https://ko.react.dev/reference/react/useState">https://ko.react.dev/reference/react/useState</a>
<a href="https://ko.react.dev/learn/conditional-rendering">https://ko.react.dev/learn/conditional-rendering</a></p>
</blockquote>
<hr>
<h1 id="비동기-처리와-api-연동">비동기 처리와 API 연동</h1>
<p>지금까지 완성한 게시판은 화면 단에서 CRUD가 완벽하게 작동했지만, 브라우저를 새로고침하는 순간 모든 데이터가 날아가는 치명적인 한계가 있었다. 데이터가 메모리(State)에만 존재하기 때문이다. 
실제 백엔드 서버(REST API)와 연결하여 영구적으로 데이터를 읽고 쓰는 웹 애플리케이션을 만든다.</p>
<h2 id="useeffect와-컴포넌트-생명주기-제어">useEffect와 컴포넌트 생명주기 제어</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/7def7a4d-cc4d-4b63-902d-1a3ce4651579/image.png" alt="">
서버에서 데이터를 가져오려면 언제 요청을 보내야 할까? 단순히 컴포넌트 함수 내부에 요청 코드를 작성하면, 상태가 변경되어 렌더링이 일어날 때마다 무한히 서버에 요청을 보내는 대참사가 발생한다. 
이러한 부수 효과(Side Effect)를 통제하기 위해 React의 useEffect 훅을 학습했다. <code>useEffect</code>는 컴포넌트가 화면에 처음 나타날 때(Mount), 사라질 때(Unmount), 특정 상태가 변경될 때(Update) 등 컴포넌트의 생명주기에 맞춰 특정 코드를 실행할 수 있게 해준다. 두 번째 인자인 의존성 배열을 비워두면 <code>[]</code>, 오직 처음 화면이 렌더링될 때 딱 한 번만 서버에 데이터를 요청하도록 안전하게 제어할 수 있다.</p>
<h2 id="axios를-활용한-rest-api-연동-비동기-처리">Axios를 활용한 REST API 연동 (비동기 처리)</h2>
<p>서버와 통신하기 위해 자바스크립트 내장 <code>fetch</code> 대신, 실무에서 표준처럼 쓰이는 Axios 라이브러리를 도입했다. 데이터를 가져오는 데는 필연적으로 네트워크 지연(시간)이 발생하므로, 코드의 흐름이 멈추지 않도록 비동기 처리(async/await) 개념을 확인하자.</p>
<p>서버의 API 명세에 맞추어 HTTP 메서드를 매핑</p>
<ul>
<li><strong>GET:</strong> 서버에서 전체 게시글 목록 받아오기</li>
<li><strong>POST:</strong> 내가 작성한 새 글을 서버에 저장하기</li>
<li><strong>DELETE:</strong> 특정 <code>id</code>의 게시글을 서버에서 삭제하기</li>
</ul>
<p>이러한 비동기 통신 로직을 기존 코드에 붙여보자</p>
<pre><code class="language-javascript">import React, { useState, useEffect } from &#39;react&#39;;
import axios from &#39;axios&#39;;
import PostList from &#39;./PostList&#39;;

const ApiBoardApp = () =&gt; {
  const [posts, setPosts] = useState([]);
  const [inputValue, setInputValue] = useState(&#39;&#39;);

  // 1. Read (GET): 컴포넌트가 마운트될 때 딱 한 번 서버에서 데이터 로드
  useEffect(() =&gt; {
    const fetchPosts = async () =&gt; {
      try {
        const response = await axios.get(&#39;[https://api.example.com/posts](https://api.example.com/posts)&#39;);
        setPosts(response.data); // 서버에서 받은 데이터로 상태 초기화
      } catch (error) {
        console.error(&#39;데이터를 불러오는데 실패했습니다.&#39;, error);
      }
    };
    fetchPosts();
  }, []);

  // 2. Create (POST): 서버에 새 게시글 데이터 전송
  const handleAddPost = async (e) =&gt; {
    e.preventDefault();
    if (!inputValue.trim()) return;

    try {
      // 서버에 데이터 저장 요청
      const response = await axios.post(&#39;[https://api.example.com/posts](https://api.example.com/posts)&#39;, { 
        title: inputValue 
      });
      // 서버에서 응답받은 새 객체(DB에서 생성된 id 포함)를 상태에 추가
      setPosts([...posts, response.data]);
      setInputValue(&#39;&#39;);
    } catch (error) {
      console.error(&#39;게시글 작성에 실패했습니다.&#39;, error);
    }
  };

  // 3. Delete (DELETE): 서버에 특정 게시글 삭제 요청
  const handleDeletePost = async (id) =&gt; {
    try {
      await axios.delete(`https://api.example.com/posts/${id}`);
      // 서버 삭제 성공 시, 프론트엔드 상태에서도 제거
      setPosts(posts.filter(post =&gt; post.id !== id));
    } catch (error) {
      console.error(&#39;게시글 삭제에 실패했습니다.&#39;, error);
    }
  };

  return (
    &lt;div className=&quot;board-container&quot;&gt;
      &lt;h2&gt;API 연동 미니 게시판&lt;/h2&gt;
      &lt;form onSubmit={handleAddPost}&gt;
        &lt;input 
          value={inputValue} 
          onChange={(e) =&gt; setInputValue(e.target.value)} 
          placeholder=&quot;새 글을 입력하세요&quot; 
        /&gt;
        &lt;button type=&quot;submit&quot;&gt;작성&lt;/button&gt;
      &lt;/form&gt;
      &lt;hr /&gt;
      &lt;PostList posts={posts} onDelete={handleDeletePost} /&gt;
    &lt;/div&gt;
  );
};

export default ApiBoardApp;</code></pre>
<blockquote>
<p>출처/참고
<a href="https://ko.react.dev/learn/lifecycle-of-reactive-effects">https://ko.react.dev/learn/lifecycle-of-reactive-effects</a>
<a href="https://velog.io/@sukong/REACT-%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%9D%98-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0%EC%99%80-useEffect-Hook">https://velog.io/@sukong/REACT-%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%9D%98-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0%EC%99%80-useEffect-Hook</a></p>
</blockquote>
<hr>
<h2 id="생각정리">생각정리</h2>
<p>React를 배우며 가장 뼈저리게 느낀 핵심은 단연 &#39;상태기반의 UI 제어&#39;와 &#39;불변성 유지&#39;의 중요성이다.
과거 바닐라 자바스크립트로 개발할 때는 이벤트가 발생할 때마다 일일이 DOM 요소를 찾아내서 직접 멱살을 잡고 수정해야 했다. 하지만 React는 이 귀찮은 과정을 거치지 않는다. 우리는 데이터만 신경 쓰면 된다 -&gt; (상태를 변경하면  React가 알아서 변경점을 감지하고 화면을 업데이트) 여기에 더해 화면을 독립적인 Component 단위로 잘게 쪼개는 설계 방식은 코드의 재사용성을 엄청나게 끌어올려 주었다. 컴포넌트 분리를 통해 각 UI가 맡은 역할이 명확해지니, 코드가 아무리 길어져도 어디를 고쳐야 할지 한눈에 파악된다. 유지보수하기 좋은 코드란 무엇인지 제대로 배운 기분이다. (에러나는 부분에 터미널로 자세하게 나오니 이렇게 친절할수가..)
하나에 불편한점이라고 생각한다면 React는 상태가 곧 화면이 되기 때문에, 결국 &#39;이 상태를 어디서 어떻게 관리할 것인가&#39;가 애플리케이션의 성능과 구조를 좌우하는 핵심이 된다고 생각한다. 무분별하게 상태를 변경했다가는 불필요한 Re-rendering이 폭발적으로 발생하기 때문이다. 편리해진 만큼, 상태 설계를 더 꼼꼼하게 작성하는 습관을 가지자.
8주차를 마무리한다.</p>
<h3 id="gitprivate"><a href="https://github.com/KT-E/08-Week">Gitprivate</a></h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 7주차-2 회고록 / AIVLE_DAY 01]]></title>
            <link>https://velog.io/@mi_nini/KT-A-7%EC%A3%BC%EC%B0%A8-2-AIVLE-DAY-01</link>
            <guid>https://velog.io/@mi_nini/KT-A-7%EC%A3%BC%EC%B0%A8-2-AIVLE-DAY-01</guid>
            <pubDate>Tue, 26 May 2026 10:38:30 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>이번 글에서는 01에이블데이를 보냈던 기록을 남겨보려고 한다.  (에이블데이 진행 직후, 기억나는 내용들을 급하게 임시 저장 형태로 작성하고 이후 내용을 다시 정리해 업로드해 늦게 업로드가 되었다.. 참고로 썸네일은 코딩테스트 후 나의 모습과 비슷해 보여서 선정했다...)
<img src="https://velog.velcdn.com/images/mi_nini/post/d5ac51d7-3608-4cba-9fa7-2ff87254198d/image.png" alt="">
이번 01 에이블데이는 Step1이 끝난 시점에 진행되었다. 오전에는 코딩테스트를 치르고, 오후에는 자소서 특강과 함께 각 반별 랜선 회식 시간을 가졌다.
약 한 달이 넘는 시간 동안 세 번의 미니 프로젝트와 공모전도 제출하고 코딩테스트 문제를 풀며 달려왔고, 어느새 하나의 단계가 마무리되었다. 이번 회고록에서는 오전에 진행했던 코딩테스트문제에 대해 집중해 나의 기억을 더듬어 작성해보겠다.</p>
<hr>
<h1 id="코딩테스트">코딩테스트</h1>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/8b75a374-60ae-4e1b-a93b-e6545dd4d5ee/image.png" alt="">
문제는 총 3문제로, 제한 시간은 2시간이었다. 문제 내용은 유출이 금지되어 있기 때문에 구체적으로 다루지는 못하지만, 이번 글에서는 문제를 풀며 느꼈던 점과 접근 방식, 그리고 아쉬웠던 부분들에 대해 이야기해보려고 한다. (이번 코딩테스트는 파이썬을 선택해 진행했다.)</p>
<h2 id="1번--2차원-배열-시뮬레이션과-차원-축소">1번 : 2차원 배열 시뮬레이션과 차원 축소</h2>
<p>1번 문제에서는 주어진 조건에 따라 2차원 배열의 크기를 점진적으로 줄여나가는 전형적인 구현 및 시뮬레이션 문제였다.</p>
<h3 id="main--배열-시뮬레이션과-풀링">Main : 배열 시뮬레이션과 풀링</h3>
<p>이 문제의 핵심은 2차원 배열의 행과 열 길이를 비교하여 분기를 나누고, 인접한 원소들을 특정 기준(최댓값 또는 최솟값)으로 압축하는 것이다. 이는 컴퓨터 비전이나 딥러닝에서 이미지의 차원을 축소하고 주요 특징을 추출할 때 사용하는 풀링 연산, 그중에서도 특정 축을 기준으로 값을 줄여나가는 과정과 매우 유사하다.</p>
<p>가장 대표적인 예로 맥스 풀링이 있다. 지정된 윈도우 크기(예: 1x2 또는 2x2) 내에서 가장 큰 값만 남기고 나머지는 버리는 방식이다.</p>
<h3 id="개념--파이썬을-활용한-1차원-풀링">개념 / 파이썬을 활용한 1차원 풀링</h3>
<pre><code class="language-python">## 예시 
# 특정 축(가로 또는 세로)을 기준으로 인접한 두 값을 압축(Sum Pooling)하는 예시

def axis_based_pooling(arr, axis):
    row_len = len(arr)
    col_len = len(arr[0])

    if axis == &#39;horizontal&#39;: 
        # row을 순회하며 인덱스를 2칸씩 건너뛰어 압축
        return [[row[j] + row[j+1] for j in range(0, col_len, 2)] for row in arr]
    elif axis == &#39;vertical&#39;: 
        # col을 고정하고 행 인덱스를 2칸씩 건너뛰어 압축
        return [[arr[i][j] + arr[i+1][j] for j in range(col_len)] for i in range(0, row_len, 2)]

sample_matrix = [
    [1, 2, 3, 4],
    [5, 6, 7, 8]
]

# 1. 가로축 기준 압축 결과 (열의 개수가 절반으로 줄어듦)
# [[1+2, 3+4], [5+6, 7+8]] =&gt; [[3, 7], [11, 15]]
print(axis_based_pooling(sample_matrix, &#39;horizontal&#39;))

# 2. 세로축 기준 압축 결과 (행의 개수가 절반으로 줄어듦)
# [[1+5, 2+6, 3+7, 4+8]] =&gt; [[6, 8, 10, 12]]
print(axis_based_pooling(sample_matrix, &#39;vertical&#39;))</code></pre>
<p>이번 테스트 문제는 이러한 차원 축소의 개념을 2차원 배열의 가로, 세로 비율에 따라 유동적으로(가로 방향 축소 또는 세로 방향 축소) 적용하는 것이 핵심이였다고 생각한다.</p>
<h3 id="나의-접근법">나의 접근법</h3>
<p>문제를 해결하기 위해 나는 파이썬의 기능인 List Comprehension을 적극 활용했다. 지정된 횟수만큼 반복문을 수행하되, 루프 시작 지점에서 현재 배열의 열과 행 길이를 측정했다. 만약 배열이 더 이상 줄어들 수 없는 1x1 크기가 되었다면, 불필요한 반복 연산을 막기 위해 즉시 탈출하도록 예외 처리를 두었다.</p>
<blockquote>
<p>가로가 세로보다 크거나 같을 때  행(row) 단위로 순회하며, 인덱스를 2칸씩 건너뛰며 인접한 열의 원소들을 비교해 최댓값을 추출했다.
세로가 길 때 -&gt; 열 단위로 인덱스를 고정한 채, 인접한 행의 원소들을 비교해 최솟값을 추출하도록 구현했다.</p>
</blockquote>
<h3 id="코드-분석">코드 분석</h3>
<p>리스트 컴프리헨션을 사용하여 2중 배열의 구조 변경을 단 한 줄의 코드로 처리했고, 변수 상태가 꼬일 여지를 차단했다. 특히 range(0, length, 2) 방식으로 인덱스를 제어한 것은 배열의 길이가 항상 짝수(혹은 2의 거듭제곱 꼴)로 떨어지는 제약 조건 내에서 인덱스 초과(Index Out of Bounds) 오류를 방지하는 좋은 접근이였다고 생각한다. 다만, 최적화 관점에서 내 코드를 분석해 볼 필요는 있다.
현재 방식은 매 반복 단계마다 arr = [...] 형태로 완전히 새로운 2차원 리스트를 메모리에 반복해서 동적 할당하고 있다. 파이썬 내부적으로 이전 배열은 가비지 컬렉터가 처리해주겠지만, 만약 입력 배열의 초기 크기가 어마어마하게 크고 변환 횟수가 많았다면 잦은 메모리 할당과 해제로 인해 오버헤드가 발생했을 것이다.
만약 메모리 제한이 극단적으로 빡빡한 환경이었다면, 새로운 배열을 생성하는 대신 원본 배열은 그대로 둔 채 접근해야 하는 인덱스의 범위(투 포인터 등)만 절반씩 좁혀가는 In-place 덮어쓰기 방식을 고민해야 했을 것이다. 하지만 주어진 코딩테스트의 제한 사항과 시간 복잡도 내에서는, 개발 시간 단축과 버그 방지를 위해 직관적인 배열 재생성 로직을 선택한 것이 훌륭한 트레이드오프였다고 생각한다.</p>
<hr>
<h2 id="2번--다중-라운드-상태-추적과-엣지-케이스">2번 : 다중 라운드 상태 추적과 엣지 케이스</h2>
<p>2번 문제는 시간이 흐름에 따라 변화하는 객체들의 상태를 추적하고, 규칙 위반 여부를 검증하는 시뮬레이션 문제였다. </p>
<h3 id="main--상태-머신과-이력-관리">Main : 상태 머신과 이력 관리</h3>
<p>이 문제의 핵심은 여러 주체가 매 라운드마다 특정 대상과 상호작용을 시도할 때, &#39;현재의 선택&#39;이 &#39;과거의 이력(직전 상태)&#39;에 의해 제약받는다는 점이다. 
이와 유사한 로직을 자주 접할 수 있다. 예를 들어, 사용자가 비밀번호를 변경할 때 &#39;최근 3번 이내에 사용한 비밀번호는 사용할 수 없다&#39;는 제약을 걸거나, 스케줄링 시스템에서 &#39;이틀 연속 동일한 근무자와 짝을 이룰 수 없다&#39;는 조건을 검증하는 로직이 이에 해당한다.</p>
<h3 id="개념--파이썬을-활용한-과거-이력-기반-유효성-검사">개념 / 파이썬을 활용한 과거 이력 기반 유효성 검사</h3>
<p>문제 유출을 피하기 위해, 이와 본질적으로 동일한 구조를 가진 연속 파트너 지목 제한 로직을 간단한 파이썬 코드로 구현했다.</p>
<pre><code class="language-python">## 예시

def check_rule_violations(current_choices, last_partners):
    violations = 0
    valid_matches = {}

    for person, target in current_choices.items():
        # 규칙 1: 자기 자신을 지목할 수 없음
        if person == target:
            violations += 1
            continue
        # 규칙 2: 직전 라운드에서 짝이었던 사람을 다시 지목할 수 없음
        if last_partners.get(person) == target:
            violations += 1
            continue
        # 서로 지목하여 매칭이 성사된 경우를 확인 
        if current_choices.get(target) == person:
            valid_matches[person] = target

    return violations, valid_matches

# 1라운드 결과 (A-B 매칭)
history = {&#39;A&#39;: &#39;B&#39;, &#39;B&#39;: &#39;A&#39;, &#39;C&#39;: None}

# 2라운드 선택 (A가 직전 파트너인 B를 또 지목하여 위반 발생)
round_2_choices = {&#39;A&#39;: &#39;B&#39;, &#39;B&#39;: &#39;C&#39;, &#39;C&#39;: &#39;B&#39;}

errors, new_history = check_rule_violations(round_2_choices, history)
print(f&quot;위반 횟수: {errors}&quot;) # 결과: 위반 횟수 1</code></pre>
<p>이 문제는 위 예시처럼 각 라운드의 선택을 순회하며 조건을 확인하고, 다음 라운드 검증을 위해 history를 정확히 갱신해 주는 것이 목표였다.</p>
<h3 id="나의-접근법-1">나의 접근법</h3>
<p>나는 이 문제를 해결하기 위해 파이썬의 Dictionary를 활용하여 각 주체의 상태를 관리했다.
last_partner라는 딕셔너리를 초기화하여 이전 라운드의 결과를 저장할 공간을 만들었다.
각 라운드마다 반복문을 돌며, 현재 지목한 target이 자기 자신이거나 직전 파트너(last_partner[me])인지 검사하여 위반 횟수를 누적했다.
서로 지목하여 조건이 맞는 경우에만 next_partner 딕셔너리에 매칭 결과를 임시 저장하고, 라운드가 끝날 때 last_partner를 next_partner로 덮어씌워 상태를 갱신했다.</p>
<h3 id="코드-분석-1">코드 분석</h3>
<p>기본적인 로직의 뼈대는 맞았지만, 결과적으로 100점 코드 결과가 나오지 못한 이유는 행위와 결과 상태의 분리가 완벽하지 않았고, 이로 인해 연쇄적인 엣지 케이스를 놓쳤기 때문이라고 생각한다. 복기해 보면 내 코드에는 문제가 있었다.
나는 서로를 올바르게 지목했을 때만(curr_choices.get(target) == me) next_partner에 기록을 남겼다. 그런데 만약 다대다 관계에서 누군가 한 명이라도 규칙을 어겼다면 어떻게 될까? A와 B가 서로를 지목했지만 A가 과거 이력 규칙을 어겨서 무효가 되었다면, A를 지목했던 B의 상태는 &#39;매칭 실패&#39;로 처리되어 다음 라운드에 반영되어야 한다. 즉, 특정 주체의 규칙 위반이 얽혀있는 다른 주체들에게 미치는 &#39;연쇄 작용&#39;을 제대로 끊어내지 못했다. 복잡한 엇갈림 속에서 매칭이 실패했음에도 불구하고, 과거의 엉뚱한 데이터를 참조하게 되어 특정 테스트 케이스에서 위반 횟수가 누락되거나 과다 측정된 것이다.</p>
<h3 id="향후-개선-방향">향후 개선 방향</h3>
<blockquote>
<ul>
<li>상태 전이도 분리:
&#39;현재 선택 확인&#39; -&gt; &#39;위반자 색출 및 필터링&#39; -&gt; &#39;유효한 선택자들 간의 매칭 성사 확인&#39; -&gt; &#39;결과 상태 갱신&#39;이라는 단계를 명확한 함수나 블록으로 쪼개어 결합도를 낮춰야 한다.</li>
</ul>
</blockquote>
<ul>
<li>테스트 주도 접근:
코딩을 시작하기 전에, 문제 지문에 나오지 않은 최악의 엇갈림 상황을 종이에 손으로 그려가며 상태표가 어떻게 변해야 하는지 먼저 검증해야겠다.</li>
</ul>
<blockquote>
<p>공부합시다 --- 출처/참고
<a href="https://kr.linkedin.com/pulse/state-machine-design-pattern-concepts-examples-python-sajad-rahimi?tl=ko">https://kr.linkedin.com/pulse/state-machine-design-pattern-concepts-examples-python-sajad-rahimi?tl=ko</a>
<a href="https://cloudjini.tistory.com/entry/%F0%9F%93%8C-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BC%80%EC%9D%B4%EC%8A%A4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B3%A0-%EA%B0%80%EC%9E%90">https://cloudjini.tistory.com/entry/%F0%9F%93%8C-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BC%80%EC%9D%B4%EC%8A%A4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B3%A0-%EA%B0%80%EC%9E%90</a>
<a href="https://blog.naver.com/kmh03214/221685090465">https://blog.naver.com/kmh03214/221685090465</a>
<a href="https://wikidocs.net/blog/@jaehong/12397/">https://wikidocs.net/blog/@jaehong/12397/</a></p>
</blockquote>
<hr>
<h2 id="3번--이진-트리의-구조적-한계와-bottom-up-최적화">3번 : 이진 트리의 구조적 한계와 Bottom-Up 최적화</h2>
<p>이진 트리의 성질을 다루는 문제였는데, 직관적인 탐색(Top-Down) 로직 함정에 낚였다... &#39;우선순위(삭제 최소화 &gt; 추가 최소화)&#39;라는 제약 조건이 알고리즘의 방향성을 어떻게 완전히 뒤집어야 하는지 깨닫게 해준 좋은 문제였던거같다.</p>
<h3 id="main--이진-트리의-종속성과-bottom-up-용량-전파">Main : 이진 트리의 종속성과 Bottom-Up 용량 전파</h3>
<p>이 문제의 본질은 일반적인 그래프 탐색이 아니라, 이진 트리가 가지는 수학적 용량의 종속성을 해결하는 것이다. 
이진 트리에서 부모 노드 1개는 최대 2개의 자식 노드만 가질 수 있다. 이를 역으로 생각하면, 특정 레벨(L)에 N개의 노드가 존재하려면, 그 바로 위 레벨(L-1)에는 최소한 $\lceil N/2 \rceil$개의 부모 노드가 반드시 존재해야 한다는 뜻이다.</p>
<h3 id="개념---파이썬을-활용한-하위-종속성-해결-bottom-up">개념 /  파이썬을 활용한 하위 종속성 해결 (Bottom-Up)</h3>
<p>문제 유출을 방지하기 위해, 이진 트리 구조에서 하위 레벨의 노드들을 유지하기 위해 각 상위 레벨에서 최소 몇 개의 노드가 필요한지 역산하는 핵심 개념을 파이썬 코드로 구현했다.</p>
<pre><code class="language-python">import math

# 각 레벨(깊이)별 최소 필요 노드 수를 바텀업으로 계산하는 예시
def calculate_minimum_required_nodes(node_counts):
    max_level = len(node_counts) - 1
    required = [0] * (max_level + 1)
    required[max_level] = node_counts[max_level]

    # 가장 아래 레벨부터 루트를 향해 Bottom-Up으로 순회
    for L in range(max_level - 1, 0, -1):
        # 하위 레벨(L+1)의 노드들을 모두 수용하기 위해 필요한 최소 부모 수
        min_needed = math.ceil(required[L+1] / 2)
        # 실제 존재하는 노드 수와 비교하여, 더 큰 값을 현재 레벨의 필요량으로 확정
        required[L] = max(node_counts[L], min_needed)

    return required

sample_levels = [0, 1, 0, 4] # 인덱스가 레벨, 값은 해당 레벨의 노드 수
# 레벨 3에 4개의 노드가 있다면, 레벨 2에는 최소 2개의 노드가 필요하다.
# 결과: [0, 1, 2, 4] -&gt; 빈 레벨이었던 레벨 2에 최소 2개가 필요함이 전파됨
print(calculate_minimum_required_nodes(sample_levels))</code></pre>
<p>이처럼 하위 레벨의 데이터가 상위 레벨의 최소 조건을 강제하는 로직이 이 문제의 핵심 키였다.</p>
<h3 id="나의-접근법-2">나의 접근법</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/02670e23-3602-4b07-92f1-c56d765e5241/image.png" alt=""></p>
<p>나는 주어진 노드들의 레벨 분포를 카운팅한 뒤, 루트(레벨 1)부터 시작하여 아래로 내려가는 탑다운(Top-Down) 방식으로 접근했다. 
빈 레벨이 나타나면 노드를 1개 추가(added += 1)하여 연결을 유지했다. 현재 레벨의 노드 수 곱하기 2(current_node * 2)를 다음 레벨의 최대 허용치로 설정했다.다음 레벨의 노드 수가 허용치를 초과하면, 그 초과분만큼을 삭제(removed += excess)했다.
언뜻 보면 문제의 조건(빈 레벨 금지, 부모 당 자식 최대 2개)을 모두 지킨 것 같지만, 여기서 1순위, 노드 삭제를 최소한으로 할 것&#39;이라는 가장 중요한 조건을 위배하는 실수를 저질러 버렸다..</p>
<ul>
<li>무엇이 문제였을까? 
가정해보자. 레벨 2에는 노드가 0개이고, 레벨 3에는 노드가 4개 존재한다.</li>
</ul>
<blockquote>
<p>나의 탑다운 로직: 레벨 2가 비었으므로 노드를 1개만 추가한다. 그러면 레벨 3의 최대 허용치는 2개(1*2)가 된다. 결국 레벨 3에 있던 4개의 노드 중 2개를 강제로 삭제해야 한다. (결과: 추가 1, 삭제 2)</p>
</blockquote>
<blockquote>
<p>정답 로직: 레벨 3의 노드 4개를 &#39;삭제 없이&#39; 살리기 위해 역산해보면, 레벨 2에는 최소 2개의 노드가 필요하다. 따라서 레벨 2에 노드를 2개 추가하면 레벨 3에서는 아무것도 삭제하지 않아도 된다. (결과: 추가 2, 삭제 0)</p>
</blockquote>
<p>문제의 1순위 조건은 삭제의 최소화로 기억한다. 나의 코드는 상위 레벨에서 노드를 최소한(1개)으로 추가하려다가 극심한 병목을 만들어버렸고, 그 결과 하위 레벨에서 대량의 노드가 삭제되는 문제가 발생했다.</p>
<h3 id="향후-개선-방향-1">향후 개선 방향</h3>
<blockquote>
<ul>
<li>데이터가 위에서 아래로 흐르는지, 아니면 밑바닥의 조건이 위를 강제하는지 코딩 전에 확실히 설계해야 한다. 최소 삭제가 목표라면, 지켜야 할 대상(가장 많은 노드가 포진된 하위 레벨)부터 거꾸로 올라가며 필요 인프라(부모 노드)를 구축하는 것이 맞다.</li>
</ul>
</blockquote>
<ul>
<li>그리디(Greedy) 알고리즘의, 특정 시점(빈 레벨)에서 당장 최적이라고 생각한 선택(1개만 추가)이 전체의 최적해를 망칠 수 있음을 항상 경계해야 한다.</li>
</ul>
<blockquote>
<p>공부합시다 --- 참고/출처
<a href="https://wing-beat.tistory.com/130">https://wing-beat.tistory.com/130</a>
<a href="https://cdragon.tistory.com/entry/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Greedy-Algorithms">https://cdragon.tistory.com/entry/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Greedy-Algorithms</a>
<a href="https://as-ps.tistory.com/78">https://as-ps.tistory.com/78</a>
<a href="https://velog.io/@yoon_0/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%ED%8A%B8%EB%A6%AC-Top-Down-%EB%B0%A9%EC%8B%9D%EA%B3%BC-Bottom-Up-%EB%B0%A9%EC%8B%9D">https://velog.io/@yoon_0/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%ED%8A%B8%EB%A6%AC-Top-Down-%EB%B0%A9%EC%8B%9D%EA%B3%BC-Bottom-Up-%EB%B0%A9%EC%8B%9D</a></p>
</blockquote>
<hr>
<h1 id="자기소개서-특강">자기소개서 특강</h1>
<p>기업이 포트폴리오를 통해 확인하고자 하는 것은 지원자가 몇 개의 언어와 프레임워크를 아는지가 아니라, 실제 실무에 투입되었을 때 마주할 비즈니스 문제를 어떻게 해결할 것인가에 대한 능력이었다. 특강을 통해 배운 포트폴리오 작성의 핵심 원리와 흐름을 내 관점에서 정리해본다.</p>
<h3 id="1-포트폴리오의-진짜-목적은---재사용-가능한-역량-증명">1. 포트폴리오의 진짜 목적은? -&gt; 재사용 가능한 역량 증명</h3>
<p>가장 크게 와닿았던 핵심 개념은 포트폴리오는 시험 점수처럼 나의 스펙을 나열하는 곳이 아니라는 점이다. 기업은 &#39;적합하지 않은 인재를 채용할 두려움&#39;을 강하게 가지고 있다. 따라서 우리는 포트폴리오를 통해 재사용 가능한 역량을 보여주어야 한다.
재사용 가능한 역량이란, 과거의 프로젝트에서 어떤 비즈니스 요구사항(Why)을 바탕으로 어떤 제약사항 속에서 문제를 정의(What)하고, 어떤 트레이드오프(How)를 거쳐 최종 구현을 해냈는지를 논리적으로 설명할 수 있는 능력이다. 이를 증명하면 채용 담당자는 &quot;이 지원자는 우리 회사에 와서도 비슷한 방식으로 문제를 잘 해결하겠구나&quot;라고 판단하게 된다.</p>
<h3 id="2-기업-문제-중심의-포트폴리오-설계">2. 기업 문제 중심의 포트폴리오 설계</h3>
<p>내 프로젝트를 무작정 포장하기 전에, 지원하고자 하는 기업이 현재 어떤 문제를 겪고 있는지부터 파악해야 한다. 이를 위한 가장 좋은 방법은 채용 공고(JD) 분석과 기업에서 제공하는 기술 블로그 분석이다.
적어도 10개 이상의 채용 공고를 분석하여 해당 직무의 핵심 과제, 성과 지표, 필수 경험, 그리고 요구되는 기본기를 도출해야 한다. 또한 타겟 기업의 기술 블로그를 읽으며 현업 개발자들이 어떤 제약 상황 속에서 어떤 아키텍처를 선택했고, 왜 그런 결정을 내렸는지(트레이드오프)에 대한 고민을 내 포트폴리오에 자연스럽게 녹여내야 한다.</p>
<h3 id="3-프로젝트-경험-구조화-어떻게-작성할-것인가">3. 프로젝트 경험 구조화 (어떻게 작성할 것인가)</h3>
<p>프로젝트를 소개할 때는 단순히 &quot;무엇을 만들었다&quot;가 아니라 철저히 문제 해결 중심으로 구조화해야 한다.</p>
<ul>
<li><p>기술 스택 선정의 이유
가장 많이 하는 실수가 사용한 툴을 아이콘으로 줄줄이 나열만 하는 것이다. 면접관이 궁금한 것은 어떤 기술을 썼는지가 아니라 왜 하필 그 기술을 선택했는지다. 예를 들어 단순한 데이터 처리에 무거운 프레임워크를 썼다면 오버엔지니어링으로 보일 수 있다. 현재 프로젝트의 상황, 팀원의 숙련도, 성능상의 이점 등 명확한 도입 목적과 트레이드오프 판단 근거를 적어야 한다.</p>
</li>
<li><p>핵심 기능과 문제 해결 과정
기능을 설명할 때는 반드시 문제 → 영향 → 해결 결과의 흐름을 따라야 한다.
특히 많은 사람들이 누락하는 것이 영향 부분이다. 내가 마주한 에러나 병목 현상이 &#39;비즈니스나 사용자 경험에 어떤 치명적인 영향을 미칠 수 있었는지&#39;를 명시해야 내가 비즈니스를 이해하고 코드를 짜는 개발자임을 어필할 수 있다.</p>
</li>
<li><p>성능 개선과 회고 
개선 전후의 지표를 수치화(예: 응답시간 3초 → 1초로 66% 개선)하여 보여주는 것이 중요하다. 프로젝트가 끝난 후에는 한계를 분석하고, &quot;운영 안정성 측면에서 이러한 부분이 부족했기에 향후에는 이 아키텍처를 도입해 실시간 안정성을 높이겠다&quot;는 식의 향후 개선 방향을 적어야 지속적으로 성장하는 인재임을 증명할 수 있다.</p>
</li>
</ul>
<h3 id="4-포트폴리오-작성-도구와-검토-전략">4. 포트폴리오 작성 도구와 검토 전략</h3>
<p>결국 내 포트폴리오를 읽는 사람은 실무 관리자이거나 최종 결정권자다. 이들은 단순한 코딩 스킬을 넘어 시스템을 확장하고 운영할 수 있는 안정성을 본다.</p>
<ul>
<li>Notion 활용: 정보가 한눈에 들어오도록 3단 위계(표지 → 프로젝트 목록 DB → 개별 상세)로 구성하고, Toggle이나 Callout을 활용해 핵심만 노출시켜 가독성을 극대화해야 한다.</li>
<li>GitHub 활용: README는 한 편의 훌륭한 기술 보고서가 되어야 한다. 아키텍처 다이어그램 한 장이 코드 백 줄보다 낫다. 의미 단위의 커밋 컨벤션과 CI/CD 구축 흔적으로 코드 품질과 협업 능력을 증명해야 한다.</li>
</ul>
<h3 id="5-포트폴리오-작성의-비교">5. 포트폴리오 작성의 비교</h3>
<p>특강에서 제시된 사례들을 비교해보면, 합격하는 포트폴리오와 탈락하는 포트폴리오의 차이가 명확하게 드러난다.</p>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">단순 기술 나열형</th>
<th align="left">문제 해결형</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>프로젝트명</strong></td>
<td align="left">쇼핑몰 백엔드 API 개발</td>
<td align="left">동시성 이슈 해결 - 분산 락 기반 선착순 쿠폰 발급 API</td>
</tr>
<tr>
<td align="left"><strong>문제 정의</strong></td>
<td align="left">구현해야 할 API가 많았음</td>
<td align="left">쿠폰 발급 시 초당 3,000건 요청으로 DB Lock 충돌 및 중복 발급 0.4% 발생</td>
</tr>
<tr>
<td align="left"><strong>해결 과정</strong></td>
<td align="left">Java, Spring, Redis, MySQL 등 다양한 기술을 사용해 개발함</td>
<td align="left">Redis Lua 스크립트로 재고 차감, Redisson 분산 락 도입으로 DB 부하 분리</td>
</tr>
<tr>
<td align="left"><strong>결과</strong></td>
<td align="left">정상적으로 작동하며, 리뷰에서 좋은 평가를 받음</td>
<td align="left">중복 발급 0%, 최대 TPS 4배 향상(3,000→12,000) 및 응답시간 단축</td>
</tr>
</tbody></table>
<h3 id="정리해보자면">정리해보자면</h3>
<p>결국 좋은 포트폴리오란 </p>
<blockquote>
<p>&quot;나는 주어진 스펙대로만 코딩하는 단순 구현자가 아니라, 비즈니스의 제약사항을 이해하고 최적의 기술적 타협점(트레이드오프)을 찾아내어 시스템을 개선할 수 있는 문제 해결사입니다.&quot; 라는 메시지를 던지는 매체다. 이번 특강을 바탕으로 내 과거 프로젝트들을 단순히 &#39;무엇을 만들었나&#39;가 아닌 &#39;어떤 문제를 어떻게 해결했나&#39;의 관점에서 전면 재작성해 이력서를 정리해보자!</p>
</blockquote>
<hr>
<h1 id="랜선회식">랜선회식</h1>
<p>반에서 간단하게 에이블기간동안 한 핵심단어 퀴즈 맞추기 및 단원 정리를 진행 (레크레이션 시간이랄까..)</p>
<blockquote>
<p>그림보고 키워드 맞추기
<img src="https://velog.velcdn.com/images/mi_nini/post/11adef71-cef2-4eba-a93e-7dfbb91571d7/image.png" alt=""></p>
</blockquote>
<blockquote>
<p>정답 실패..
<img src="https://velog.velcdn.com/images/mi_nini/post/12a0125d-b0de-48d7-9c82-b5acfb187a77/image.png" alt=""></p>
</blockquote>
<blockquote>
<p>단체 컷
<img src="https://velog.velcdn.com/images/mi_nini/post/82fa3eca-8009-45d6-8f61-c9313c36c3cf/image.png" alt=""></p>
</blockquote>
<blockquote>
<p>선물 감사합니다 111
<img src="https://velog.velcdn.com/images/mi_nini/post/e2a304c3-fb3a-4e7a-a0c5-ed22f005e84e/image.jpg" alt=""></p>
</blockquote>
<blockquote>
<p>선물 감사합니다 222
<img src="https://velog.velcdn.com/images/mi_nini/post/134c9570-fb72-4562-b839-9702384d34b9/image.jpg" alt=""></p>
</blockquote>
<blockquote>
<p>모든 9기 에이블러님들 고생하셨습니다 -_-</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 7주차-1 / 03Miniproject]]></title>
            <link>https://velog.io/@mi_nini/KT-A-7%EC%A3%BC%EC%B0%A8-1</link>
            <guid>https://velog.io/@mi_nini/KT-A-7%EC%A3%BC%EC%B0%A8-1</guid>
            <pubDate>Thu, 21 May 2026 13:14:31 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>이번 주차에서는 LLM을 활용한 마지막 03 MiniProject를 진행하고, AIVLE DAY를 보내며 코딩테스트와 자소서 특강 강의를 진행했다. 이번 글(7주차-1)에서는 먼저 프로젝트 진행 내용을 정리하고, 다음 글(7주차-2)에서 코딩테스트 리뷰와 강의를 통해 느끼고 배워간 점들을 함께 정리해보려고 한다.
이번 프로젝트 역시 01, 02 MiniProject 때와는 또 다른 팀원들과 함께하게 되어 설레는 마음으로 시작했다. 주제는 상품 리뷰 분석 Agent 서비스 구축이었다.</p>
<blockquote>
<h4 id="프로젝트의-배경은-다음과-같다">프로젝트의 배경은 다음과 같다.</h4>
</blockquote>
<ul>
<li>멤버십 쇼핑라운지 데이터분석팀은 AI 기반 감성 분석 프로젝트를 통해 리뷰 데이터를 구조화하고, 각 속성별 감성 정보를 추출하여 입점 업체에 제공함으로써 제품 기획, 마케팅, 고객 응대 등 비즈니스 전반에 실질적인 인사이트를 제공하는 서비스 체계를 구축하고자 했다. </li>
<li>단기적으로는 리뷰 분석 자동화를 목표로 하고, 중장기적으로는 리뷰 기반 상품 추천, 자동 요약, VOC 클러스터링 등 고도화된 서비스로 확장하기 위한 첫 단계로 프로젝트가 기획되었다. </li>
<li>최종적으로는 프로젝트의 성공을 바탕으로 전체 입점 업체까지 서비스를 확산하는 것을 목표로 하고 있었다.</li>
</ul>
<p>이번 프로젝트 역시 이전 프로젝트들과 비슷하게 기본적인 틀은 제공되었지만, 각 팀이 회의를 통해 방향성을 구체화하고 어떤 부분에 집중해 서비스를 고도화할 것인지 결정하는 과정이 핵심이었다고 생각한다. 같은 주제를 바탕으로 하더라도 어떤 사용자 경험에 초점을 맞추고, 어떤 기능을 강화하느냐에 따라 서비스의 색깔이 완전히 달라지기 떄문이다.
특히 이번에는 Streamlit을 활용한 시각화와 LangSmith 기반의 실행 과정 모니터링 및 추적 기능까지 적용하면서, 이전 프로젝트보다 결과물을 더욱 직관적이고 완성도 있게 보여줄 수 있을 것이라는 기대가 컸다. 이러한 과정 속에서 우리 조가 어떤 방향으로 프로젝트를 진행했는지 소개해보려고 한다.</p>
<hr>
<h2 id="absa-알고-있나요">ABSA 알고 있나요?</h2>
<p>본격적인 시스템 구현에 앞서, 이번 프로젝트의 핵심 방법론인 속성 기반 감성 분석(ABSA: Aspect-Based Sentiment Analysis)에 대해 먼저 짚고 넘어가고자 한다. 우리 서비스가 실제 비즈니스 환경에서 입점 업체들에게 &#39;실질적인 액션 아이템&#39;을 제시하기 위해서는 일반적인 감성 분석만으로는 괜찮을까?라는 생각이 들고 이는 강의에서 언급된 내용이기도 하지만 자세하게 다루지 않았기에 ABSA 개념에 대해 조사했다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/17daaf92-8a7f-4f41-bcac-153a5609e85c/image.png" alt=""></p>
<h3 id="속성-기반-감성-분석absa이란">속성 기반 감성 분석(ABSA)이란?</h3>
<p>단순히 전체 리뷰를 &#39;긍정&#39; 혹은 &#39;부정&#39;으로만 분류하는 기존의 감성 분석과 달리, 리뷰 내에 포함된 &#39;구체적인 속성(Aspect, 예: 가격, 향, 성분 등)&#39;과 그에 대응하는 &#39;감성(Sentiment)&#39;을 쌍으로 매칭하여 세밀하게 추출하는 자연어 처리 기법이다.</p>
<p>예를 들어, &quot;가격은 저렴해서 좋은데, 향이 너무 강해서 아쉬워요.&quot;라는 리뷰가 있다면 일반적인 감성 분석은 문장 전체의 극성을 모호하게 판단하겠지만, ABSA는 다음과 같이 데이터를 구조화하여 명확한 의미를 도출해낸다.</p>
<blockquote>
<p>속성 1: 가격 → 감성: 긍정(1)
속성 2: 향 → 감성: 부정(0)</p>
</blockquote>
<p>왜 이번 프로젝트의 핵심이었나?
이번 프로젝트의 목표는 단순히 &quot;고객들이 이 제품을 좋아한다/싫어한다&quot;라는 단편적인 피드백을 전달하는 것이 아니었다고 생각한다. 입점 업체가 고객의 소리(우레에겐 리뷰)를 기반으로 제품을 개선하고 마케팅 전략을 수정할 수 있도록, &quot;제품의 어느 부분(가격, 제형, 성분, 디자인, 배송 등)에서 만족하고, 또 어느 부분에서 구체적인 불만을 느끼는지&quot;를 파악하는 것이 프로젝트의 핵심 과제라고 생각한다.
이러한 정성적 데이터를 정량화된 JSON 포맷으로 변환하는 ABSA 체계를 구축함으로써, 우리는 다량의 리뷰 데이터 속에서 특정 속성의 만족도 추이를 통계적으로 추출할 수 있었다. 결과적으로 이 ABSA 기법은 우리 팀의 멀티 에이전트 시스템에서 에이전트들이 문제를 인지하고 각 전문가 노드로 업무를 할당하는 판단의 기준이 되는 중요한 로직으로서 기능하게 되었다.</p>
<blockquote>
<p>ABSA란? 참고
<a href="https://abluesnake.tistory.com/173">https://abluesnake.tistory.com/173</a>
<a href="https://www.ibm.com/kr-ko/think/topics/sentiment-analysis">https://www.ibm.com/kr-ko/think/topics/sentiment-analysis</a>
<a href="https://www.elastic.co/kr/what-is/sentiment-analysis">https://www.elastic.co/kr/what-is/sentiment-analysis</a>
<a href="https://www.sciencedirect.com/science/article/abs/pii/S0950705121009059">https://www.sciencedirect.com/science/article/abs/pii/S0950705121009059</a> (논문 궁금하면)</p>
</blockquote>
<hr>
<h1 id="주제--상품-리뷰를-구조화된-감성-데이터로-변환하는-multi---agent-ai-분석-시스템">주제 : 상품 리뷰를 구조화된 감성 데이터로 변환하는 Multi - Agent AI 분석 시스템</h1>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/c0ef1418-13a7-40a9-88e6-101bf7de816a/image.png" alt=""></p>
<h2 id="기본-시스템-구조">기본 시스템 구조</h2>
<p>전체 시스템은 LangGraph를 기반으로 설계되었으며, Supervisor, Analyzer, Critic이라는 핵심 에이전트들이 상호작용하며 리뷰 데이터를 반복적으로 분석하고 검증하는 구조로 이루어져 있다.</p>
<ul>
<li><p>작업 분배 및 흐름 제어 (Supervisor Node): 워크플로우의 중심에서 전체 프로세스를 조율한다. 리뷰 데이터가 들어오면 먼저 Analyzer에게 분석을 지시하고, 이후 결과에 따라 Critic으로 검증을 넘기거나, 혹은 검증이 완료되면 워크플로우를 종료시키는 조건부 분기역할을 수행함</p>
</li>
<li><p>리뷰 데이터 1차 분석 (Analyzer Node): 사용자의 원시 리뷰 텍스트가 입력되면, 제시된 후보 속성 중 리뷰에 언급된 항목을 추출한다. 해당 속성에 대한 만족(1)과 불만족(0) 여부를 판별한다.</p>
</li>
<li><p>결과 검증 및 피드백 (Critic Node): Analyzer가 내놓은 분석 결과의 품질을 꼼꼼히 검토한다. 추출된 속성이나 감정 라벨링 등에 오류가 없는지 평가하고, 재작성이 필요한 경우 구체적인 피드백(예: 범위 오류, 근거 부족 등)과 함께 Analyzer가 다시 분석하도록 유도한다.</p>
</li>
</ul>
<hr>
<table style="width:100%; text-align:center; border-collapse: collapse;">
  <tr>
    <td style="width:50%; vertical-align: middle;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/f0d9d452-4728-4a64-b469-9b5aa288c2d2/image.png" style="width:100%;">
    </td>
    <td style="width:50%; vertical-align: middle;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/e32ceae1-1d5d-49e7-9433-496005ba017e/image.png" style="width:100%;">
    </td>
  </tr>
</table>


<h2 id="우리가-생각한-고도화-방법">우리가 생각한 고도화 방법</h2>
<h3 id="1-리뷰분석-agent의-평점-산정-기준">1. 리뷰분석 Agent의 평점 산정 기준</h3>
<ul>
<li>고객이 입력한 별점이 있는 경우 -&gt; 입력된 별점 숫자가 0보다 크면 그 숫자를 그대로 사용</li>
<li>별점이 누락되었거나 0점인 경우 -&gt; Agent가 리뷰 원문의 직접 분석해 1점부터 5점 사이로 알아서 별점을 책정하도록 설계 (매우 만족은 5점, 만족은 4점, 보통 3점, 불만족 2점, 매우 불만족 1점)</li>
</ul>
<h3 id="2-정성데이터의-정량화-텍스트-리뷰를-수치-데이터로-변환">2. 정성데이터의 정량화 (텍스트 리뷰를 수치 데이터로 변환)</h3>
<ul>
<li>속성(Aspect) 추출 -&gt; 우리조는 data.csv를 미리 화장품 리뷰에 나올 법한 후보 속성 리스트(예: 가격, 향, 보습 등 50여 개)를 세팅해두고, 리뷰에서 실제로 언급된 속성 키워드를 추출</li>
<li>긍정/부정(Label) 매칭: 추출해 낸 각 속성에 대한 만족도를 판별해서, 만족/긍정은 &#39;1&#39;, 불만족/부정은 &#39;0&#39;으로 라벨링을 진행</li>
<li>데이터 매칭: 결과적으로 추출된 속성 리스트와 0 또는 1로 이루어진 라벨(label) 리스트를 1:1로 정확하게 매칭했으며, 정성적인 텍스트를 JSON 데이터 형식으로 변환</li>
</ul>
<h3 id="3-시스템의-우선순위-판단-기준">3. 시스템의 우선순위 판단 기준</h3>
<p>분석 결과에서 &#39;불만족(0)&#39;으로 나온 속성들을 해결하기 위해 분야별 전문가 에이전트에게 업무를 할당했다. 이때 처리 우선순위를 잡았다. (화장품 특성상 제품의 핵심 기능과 가격이 최우선이라고 판단)
그래서 매칭된 에이전트 리스트에 performance_llm(효능/성분 개선 전문가)이나 Texture_llm(제형/사용감/자극 개선 전문가)이 포함되어 있다면, 무조건 리스트의 최우선 순위에 오도록 정렬 규칙을 세웠다.</p>
<blockquote>
<p>예외 처리(불만족이 없는 경우)
만약 라벨 리스트에 불만족(0)이 하나도 없는 &#39;완전 만족&#39; 리뷰라면?
이때는 전반적인 감사 및 고객 관리를 위해 Service_llm(배송/고객 경험 관리 전문가)을 출력하도록 예외 처리도 진행</p>
</blockquote>
<h3 id="4-복합-불만에-대응하는-유연한-배열-반환-구조">4. 복합 불만에 대응하는 유연한 배열 반환 구조</h3>
<p>Router Agent가 반환하는 결과값은 단일 문자열이 아니라 배열 형태의 데이터이다.
예를 들어 고객이 &quot;크림 성분은 좋은데 뚜껑이 안 열려서 화가나요&quot;라고 리뷰를 남겼다면? Router Agent는 이 복합적인 불만을 캐치하여 [&quot;performance_node&quot;, &quot;design_node&quot;] 형태의 리스트를 반환한다.
이를 받은 LangGraph 시스템은 두 개의 전문가 노드를 모두 활성화시켜 각자의 관점에서 개선안을 작성하도록 만든다. 딱 필요한 전문가 노드만 선택적으로 깨워서 일하게 만드는 매우 효율적이게 구축했다.</p>
<h3 id="5-json-파싱-에러에-대비한-fallback">5. JSON 파싱 에러에 대비한 Fallback</h3>
<p>LLM이 결과를 뱉을 때 가끔 포맷을 무시해서 JSON 형식이 깨지는 경우가 있다. 우리는 실제 서비스 투입을 가정하고 이런 에러 상황까지 완벽하게 대비했다.</p>
<ul>
<li>Router Agent 코드 내에 파싱을 처리하는 try-except json.JSONDecodeError 구문을 적용</li>
<li>만약 LLM의 출력 에러로 인해 JSON 디코딩에 실패하더라도, 시스템이 에러를 뿜으며 다운되지 않고 안전하게 기본값인 [&quot;Service_llm&quot;]으로 향하도록 방어 로직을 구축</li>
</ul>
<hr>
<h2 id="batch처리-동기-vs-비동기">Batch처리 (동기 vs 비동기)</h2>
<h3 id="1-기존의-방식동기식-처리-synchronous">1. 기존의 방식동기식 처리 (Synchronous)</h3>
<p>먼저, 시스템은 DB에 새롭게 쌓인 &#39;미처리 리뷰(아직 분석되지 않아 aspect IS NULL인 상태)&#39;들을 한꺼번에 불러와 분석을 시작한다.
초기에는 <code>extract_review_elements</code>라는 동기식(Sync) 함수로 진행했다. 
동작 방식은 단순 -&gt;  파이썬의 기본 for문을 돌면서, 첫 번째 리뷰를 Agent(<code>app.invoke()</code>)에 던지고 답변이 올 때까지 가만히 기다렸다.
첫 번째 리뷰 분석이 완전히 끝나면, 그제야 두 번째 리뷰가 진행되었다.</p>
<pre><code class="language-python"># 기존 방식: 하나씩 순서대로 처리 (동기)
def extract_review_elements(clean_reviews):
    result = []

    for review in clean_reviews:
        print(&quot;-리뷰 분석 시작합니다.-&quot;)
        initial_state = {
            &quot;input_review&quot;: review,
            &quot;score&quot;: 0,
            &quot;aspect_list&quot;: [&#39;가격&#39;, &#39;보습&#39;, &#39;제형&#39;, &#39;디자인&#39;, &#39;배송&#39;, ...], # 생략
            &quot;max_num&quot;: 3,
            &quot;current_num&quot;: 0,
            &quot;messages&quot;: []
        }


        final_state = app.invoke(initial_state) 

        result.append({
            &quot;aspect&quot;: final_state[&quot;result&quot;][&quot;aspect&quot;],
            &quot;label&quot;: final_state[&quot;result&quot;][&quot;label&quot;],
            &quot;score&quot;: final_state[&quot;result&quot;][&quot;score&quot;]
        })

    return result</code></pre>
<blockquote>
<p>문제점: LLM 기반 에이전트는 단점이 있다. 바로 네트워크 I/O 대기 시간(API 응답을 기다리는 시간)이 길다. 앞선 리뷰의 분석이 끝날 때까지 CPU가 아무 일도 하지 않고 놀고 있으니, 처리 속도가 턱없이 느릴 수밖에 없었다.</p>
</blockquote>
<h3 id="2-개선한-비동기식-처리-asynchronous">2. 개선한 비동기식 처리 (Asynchronous)</h3>
<p>답답한 처리 속도를 해결하기 위해, 저희 조는 파이썬의 asyncio 라이브러리를 활용하여 시스템을 수정했다.
새롭게 <code>extract_review_elements_async</code>라는 비동기(Async) 함수를 도입다.
이제는 for문을 돌면서 무작정 기다리지 않고 비동기 호출(<code>app.ainvoke()</code>)을 통해 여러 개의 리뷰 분석 요청을 동시에 병렬로 API에 던져준다.</p>
<pre><code class="language-python">import asyncio

async def extract_review_elements_async(clean_reviews, concurrency=6):
    # API 호출 제한 방지: 한 번에 최대 6개의 일꾼(Concurrency)만 일하도록 설정
    semaphore = asyncio.Semaphore(concurrency)

    async def analyze_one(review):
        async with semaphore: 
            initial_state = {
                &quot;input_review&quot;: review,
                &quot;score&quot;: 0,
                &quot;aspect_list&quot;: [&#39;가격&#39;, &#39;보습&#39;, &#39;제형&#39;, &#39;디자인&#39;, &#39;배송&#39;, ...],
                &quot;max_num&quot;: 3,
                &quot;current_num&quot;: 0,
                &quot;messages&quot;: []
            }

            # 핵심 포인트! ainvoke()를 사용해 비동기적으로 API 호출
            # 응답을 기다리는 동안 다른 리뷰 분석을 동시에 진행.
            final_state = await app.ainvoke(initial_state) 

            return {
                &quot;aspect&quot;: final_state[&quot;result&quot;][&quot;aspect&quot;],
                &quot;label&quot;: final_state[&quot;result&quot;][&quot;label&quot;],
                &quot;score&quot;: final_state[&quot;result&quot;][&quot;score&quot;],
            }

    # 수많은 리뷰 요청을 한꺼번에 모아서(gather) 동시 실행!
    results = await asyncio.gather(*[analyze_one(r) for r in clean_reviews])
    return results

# 배치 실행 트리거 함수
async def start_batch_async():
    # DB에서 미처리 리뷰 불러오기 등 초기화 과정 생략
    ...
    # 비동기 함수 호출
    analyzed_results = await extract_review_elements_async(None_analyze_reviews, 6) 

    # DB 업데이트 
    update_db(None_analyze_reviews, analyzed_results)</code></pre>
<h3 id="3-트러블슈팅-및-결과-552배-성능-향상">3. 트러블슈팅 및 결과: 5.52배 성능 향상</h3>
<p>동기 방식에서 비동기 방식으로 파이프라인을 완전히 리팩토링한 결과는 성공적이였다.
우선, API 응답을 하염없이 기다리게 만들었던 원흉인 <code>app.invoke()</code>를 비동기 호출 메서드인 <code>app.ainvoke()</code>로 전면 교체하고 파이썬의 <code>asyncio.gather</code>를 활용해 수많은 미처리 리뷰 데이터를 개별적으로 보내지 않고 한 번에 묶어 쏘아 올리는 병렬 구조로 변경했다.
이렇게 설계하니 기존 동기 방식에서 아무것도 하지 못하고 버려지던 &#39;응답 대기 시간&#39;들을 완벽하게 겹쳐서(Overlap) 사용할 수 있게 되었고, 병목 현상을 해결 할 수 있었다.</p>
<blockquote>
<h3 id="rate-limit-방어">Rate Limit 방어</h3>
<p>무작정 수천, 수만개 등의 리뷰 요청을 동시에 던지면 어떻게 될까?
십중팔구 LLM API 서버에서 &#39;Rate Limit&#39; 에러를 뿜어내며 전체 시스템이 다운될것이다.
저희 조는 이 치명적인 문제를 방지하기 위해 asyncio.Semaphore(6)를 도입했다. 동시에 일하는 워커의 개수를 최대 6개로 제한하여, API 서버에 과부하를 주지 않으면서도 시스템이 낼 수 있는 최대의 효율을 뽑아내는 안정적인 타협점을 찾았다.</p>
</blockquote>
<p>그 결과, 한 번의 API 에러나 멈춤 없이 시스템이 아주 안정적으로 돌아갔다. 동일한 수량의 리뷰 데이터를 배치 처리하는 데 걸리는 전체 시간이 획기적으로 줄어들었으며, 최종적으로 기존 대비 약 5.52배라는 성능을 향상시켰다.</p>
<hr>
<h2 id="langsmith-모니터링">LangSmith 모니터링</h2>
<p>우리 조의 역할을 분담하는 과정에서 나는 LangSmith 파트를 맡게 되었다. 이에 따라 이번 화장품 리뷰 분석 AI 에이전트 프로젝트에서는 LangSmith 기반의 모니터링 시스템 구축에 검증하고 조사했다.</p>
<h3 id="1-trace-tree-시각화를-통한-동적-라우팅-및-병렬-처리-검증">1. Trace Tree 시각화를 통한 동적 라우팅 및 병렬 처리 검증</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/038c2652-b934-45c9-86db-7cdd54ea41dc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/7405f1a7-2755-46db-998f-0cd1fae022c5/image.png" alt=""></p>
<p>코드로 설계한 LangGraph 아키텍처가 실제 트래픽 환경에서 어떻게 동작하는지 LangSmith의 Trace Tree를 통해 가장 먼저 확인했다.</p>
<ul>
<li><p>동적 라우팅(Dynamic Routing) 확인: 로그를 보면 START 노드에서 시작된 리뷰 데이터가 가장 먼저 router_node로 진입하고 라우터는 리뷰의 불만족 속성을 파악하여 어떤 전문가 에이전트를 호출할지 결정하는 역할을 수행한다.</p>
</li>
<li><p>Send API 기반의 병렬 실행 증명: 트리의 하단부를 보면 저희 조 아키텍처의 핵심인 &#39;병렬 처리&#39;가 완벽하게 기록되어 있다. router_node 이후 add_conditional_edges와 Send API를 통해 performance_node(성분/효능), design_node(패키징), service_node(CS) 등의 여러 전문가 노드가 직렬이 아닌 동시에(병렬로) 분기되어 실행되는 흐름을 시각적으로 확인할 수 있었습니다. 이는 비동기 처리가 의도대로 작동하여 전체 처리 시간을 획기적으로 단축시키고 있음을 보여준다.</p>
</li>
</ul>
<h3 id="2-노드별-io-심층-추적을-통한-프롬프트-디버깅-및-제어">2. 노드별 I/O 심층 추적을 통한 프롬프트 디버깅 및 제어</h3>
<p>단일 실행 기록(Run)의 세부 내역으로 들어가면, 각 전문가 에이전트에게 LLM모델이 어떤 입력을 받고 어떤 출력을 반환하는지 정확히 추적이 가능하다.</p>
<blockquote>
<p>Router Node의 매칭 정확도 검증</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/ae652767-28fa-4d4d-a35e-498470b134c2/image.png" alt=""></p>
<p>router_node의 상세 로그를 통해, 50여 개의 화장품 속성(aspect)과 불만족(label=0) 데이터가 어떻게 파싱되는지 확인했다. 예를 들어 &#39;보습&#39; 불만은 performance_node로, &#39;포장&#39; 불만은 design_node로 정확히 분류하여 [&quot;performance_node&quot;, &quot;design_node&quot;] 형태의 리스트를 반환하는 것을 디버깅했습니다. 파싱 에러 발생 시 Fallback으로 [&quot;service_node&quot;]가 정상 호출되는 예외 처리 로직도 이 화면을 통해 검증을 진행했다.</p>
<blockquote>
<p>전문가 Node(Expert Agents)의 도메인 특화 추론 확인</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/1e45d9f1-4ee0-4310-b131-b7a70e18c319/image.png" alt="">
특정 전문가 노드(예: design_node 또는 texture_node)의 내부 추론 로그다. 모델이 프롬프트 지시사항에 따라 자신의 담당 영역이 아닌 불만(예: 디자이너 노드에 입력된 배송 불만)은 철저히 무시하고, 오직 자신의 도메인에 맞춰 &quot;- 속성: ... - 원인 분석: ... - 개선 방안: ...&quot; 형태의 규격화된 텍스트로 해결책을 제시하고 있음을 확인했습니다. 이를 통해 모델의 환각 현상을 통제하고 전문성을 극대화한다.</p>
<blockquote>
<p>Aggregator Node의 최종 데이터 병합</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/50338a8c-a766-4332-ad5d-39831fb8ff64/image.png" alt=""></p>
<p>개별적으로 실행된 전문가 에이전트들의 result_list가 최종적으로 aggregator_node에 모여 데이터 소실 없이 하나의 [종합 제품 개선 제안서] 텍스트 포맷으로 완벽하게 병합되는 마지막 단계를 확인했다.</p>
<h3 id="3-대시보드-지표-분석과-토큰비용-최적화">3. 대시보드 지표 분석과 토큰/비용 최적화</h3>
<table style="width:100%; text-align:center; border-collapse: collapse;">
  <tr>
    <td style="width:50%; vertical-align: middle;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/f9f994a5-035b-486e-841e-ce3c76597787/image.png" style="width:100%;">
    </td>
    <td style="width:50%; vertical-align: middle;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/7132434e-4d24-4886-9a77-afe2acc51689/image.png" style="width:100%;">
    </td>
  </tr>
</table>

<p>LangSmith의 대시보드 지표는 시스템의 부하 상태를 정량적으로 파악하고 아키텍처를 개선하는 결정적인 근거가 된다.
병렬로 수많은 에이전트가 동시에 LLM(OPEN_API) API를 호출하는 구조이다 보니, 토큰 사용량과 Rate Limit 관리의 중요성이 매우 컸다.
Metrics 탭의 Latency 차트와 Token Usage 리스트를 상시 모니터링하여, 여러 전문가를 동시에 깨우는 병렬 라우팅 구조가 동기식 순차 처리 방식보다 지연 시간방어에 훨씬 유리함을 지표 추이로 입증했다.</p>
<hr>
<h2 id="결과-시각화-streamlit">결과 시각화 (Streamlit)</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/08c3683d-2034-4a5c-b69b-671a134ba6d2/image.png" alt=""></p>
<table style="width:100%; text-align:center; border-collapse: collapse;">
  <tr>
    <td style="width:50%; vertical-align: middle;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/b0a95c18-285a-44dd-9832-927a46e8e0e0/image.png" style="width:100%;">
    </td>
    <td style="width:50%; vertical-align: middle;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/a6bef04a-a9d9-4d63-9608-2acb078f8006/image.png" style="width:100%;">
    </td>
  </tr>
</table>


<hr>
<h2 id="생각정리">생각정리</h2>
<p>이번 3차 Mini Project는 단순히 LLM을 호출해 결과를 출력하는 것이아닌, 실제 서비스 환경에서 AI 시스템을 어떻게 안정적으로 운영하고 관리할 것인가에 대해 생각해볼 수 있는 프로젝트이지 않을까? 라는 생각이 든다.</p>
<p>리뷰 분석이라는 하나의 기능 뒤에도, 우리가 고도화 과정에서 다뤘던 Rate Limit 대응, JSON 파싱 오류 처리, 병렬 처리 구조 설계, 응답 지연 시간(Latency) 개선, 토큰 비용 최적화 등 생각보다 훨씬 많은 시스템적 문제들이 존재했다. 또한 다른 조들의 발표를 들으며 같은 주제를 가지고도 팀마다 전혀 다른 방향으로 프로젝트를 발전시킨 점이 인상 깊었다. 어떤 조는 LangSmith를 활용한 모니터링과 분석 체계에 더욱 집중해 AI 파이프라인의 흐름과 안정성을 강조했고, 또 다른 조는 리뷰 데이터를 활용한 실제 서비스 아이디어를 구체적으로 제시하기도 했다. 반면 사용자 관점에서 리뷰를 어떻게 더 직관적으로 받아들이고 활용할 수 있을지에 집중해 UX 측면을 강화한 조도 있었다. 단순히 모델 성능만이 중요한 것이 아니라, 어떤 문제를 정의하고 어떤 사용자 경험을 제공할 것인지에 따라 같은 AI 기술도 완전히 다른 결과물로 이어질 수 있다는 점에 시야가 넓어진거같다..</p>
<p>이번 프로젝트에서 내가 맡았던 LangSmith 기반 모니터링 파트에서는 반복적인 로그를 보는 것이 아닌, LangGraph 기반 멀티 에이전트 구조가 실제로 어떤 흐름으로 동작하는지 Trace 단위로 분석하고 검증하는 경험을 할 수 있었다.
각 Agent가 어떤 입력을 받고 어떤 판단을 내리는지, 병렬 라우팅이 실제로 의도한 대로 수행되는지, 그리고 특정 프롬프트가 왜 잘못된 결과를 만드는지까지 시각적으로 추적하며 디버깅할 수 있었고, 이를 통해 AI 시스템의 신뢰성과 제어 가능성을 크게 높일 수 있었다. 또한 프로젝트를 진행하면서 좋은 프롬프트 하나로 모든 문제가 해결되지는 않는다는 점도 크게 느꼈다.
실제로 Trace 로그를 분석해보면 모델의 성능 부족보다는 프롬프트의 모호함, 예외 처리 부족, 출력 포맷 불일치 같은 시스템 설계 영역의 문제가 더 자주 발생했다. 이를 해결하기 위해 Few-shot 예시를 반복적으로 수정하고, Fallback 로직과 검증 규칙을 추가하며 프롬프트 엔지니어링을 더 수정하고 발전해나갔다. 더 개선할점이 있고 발전할점도 있지만 짧은 기간동안 조원들과 토의하고 이뤄낸 결과이기에 이번 03Miniproject결과물은 만족한다.</p>
<p>좋은 팀원들과 함께 수없이 테스트하고, 실패하고, 개선해 나가며 완성했던 만큼 개인적으 정말 많이 배우고 성장할 수 있었던 프로젝트였다.  7주차-1을 마무리한다.</p>
<blockquote>
<p>12조 조원분들 고생하셨습니다!</p>
</blockquote>
<h3 id="github-private"><a href="https://github.com/KT-E/03-Miniproject">Github Private</a></h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 6주차 / LLMOps]]></title>
            <link>https://velog.io/@mi_nini/KT-A-6%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@mi_nini/KT-A-6%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Mon, 18 May 2026 11:14:45 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/bcf85f58-baef-4740-a6bf-55e455d46723/image.png" alt=""></p>
<p>최근 AX 공모전 제출 준비로 인해 블로그 활동이 조금 뜸해졌다. 문서화 작업에 예상보다 많은 시간이 들어가면서, GitHub PR 정리부터 노션 문서화, 보고서 작성까지 꽤 많은 에너지를 쏟게 된 것 같다. 그래도 그만큼 열심히 준비한 만큼 좋은 결과가 있기를 바라고 있다.
<a href="https://github.com/Chunbae-A/model">관심 있다면 프로젝트도 한번 구경해 보세요 :)</a>
그 사이 03차 미니프로젝트와 에이블 데이(코딩 테스트 3문제 및 특별 강의)도 진행했는데, 이것들도 천천히 기록해 보려고 한다. 우선은 6주 차에 학습했던 LLMOps 개념부터 다시 정리하며 복습해보려 한다.</p>
<hr>
<h1 id="1-llmops-개념필요성">1. LLMOps 개념/필요성</h1>
<p>최근 챗GPT나 Claude 같은 거대 언어 모델(LLM)을 활용한 서비스가 쏟아지고 있다. 나 역시 간단한 프롬프트 엔지니어링만으로도 그럴듯한 챗봇이나 자동화 스크립트를 만들어본 경험이 있다. (<a href="https://github.com/multica-ai/andrej-karpathy-skills">최근 뜻깊게 보고 있는 Claude.md 포함</a>)이번 강의를 들으며, 단순히 API를 호출해 답변을 얻어내는 &#39;데모 수준의 장난감&#39;과 실제 비즈니스 환경에서 신뢰하고 사용할 수 있는 &#39;상용 AI 시스템&#39; 사이에는 거대한 벽이 존재한다는 것을 깨달았다.
그 벽을 넘기 위해 알아야 하는 개념이 바로 <strong>LLMOps(Large Language Model Operations)</strong> 다. &#39;LLMOps의 개념과 필요성&#39;에 대해 내가 이해하고 정리한 내용을 다루어 보려고 한다.</p>
<hr>
<h2 id="11-llmops란-무엇이며-기존-mlops와-어떻게-다른가">1.1 LLMOps란 무엇이며, 기존 MLOps와 어떻게 다른가?</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/798e3d21-773d-43e2-898c-a369065d43c2/image.png" alt=""></p>
<p>소프트웨어 개발에 관심이 있다면 데브옵스(DevOps)나 머신러닝 옵스(MLOps)라는 단어를 들어보았을 것이다. LLMOps는 쉽게 말해 LLM 기반의 애플리케이션을 개발, 배포, 유지보수하는 전체 생애주기를 관리하는 방법론이다. 처음에는 LLMOps가 기존의 MLOps와 비슷한 것이 아닐까 생각했다. 하지만 둘은 초점을 맞추는 영역 자체가 완전히 달랐다.</p>
<ul>
<li>MLOps의 핵심 (데이터와 모델의 학습): 기존의 인공지능 프로젝트는 우리가 직접 양질의 데이터를 수집하고, 정제하여, 모델을 학습시키고 정확도를 높이는 데 집중했다. 즉, &#39;어떻게 하면 똑똑한 모델을 만들 것인가&#39;가 주된 과제였다.</li>
<li>LLMOps의 핵심 (시스템 통합과 통제): 반면 LLMOps 환경에서는 이미 오픈AI나 구글이 천문학적인 비용을 들여 학습시켜 놓은 초거대 모델을 가져다 쓴다. 모델 자체를 뜯어고치거나 재학습시키는 파인튜닝(Fine-tuning)보다는, 이 똑똑하지만 통제하기 힘든 모델을 <strong>&#39;어떻게 우리 비즈니스 로직에 맞게 오케스트레이션하고 제어할 것인가&#39;</strong> 에 집중한다. </li>
</ul>
<p>프롬프트를 어떻게 버저닝(Versioning)하여 관리할 것인지, 모델이 외부 API나 도구(Tool)를 사용할 때 발생하는 오류를 어떻게 복구할 것인지, 복잡한 체인(Chain)의 흐름을 어떻게 시각화할 것인지가 LLMOps의 최대 관심사다.</p>
<hr>
<h2 id="12-왜-llmops가-필수적인가">1.2. 왜 LLMOps가 필수적인가?</h2>
<p>전통적인 소프트웨어 프로그래밍은 명확하다. A라는 입력값을 주면 정해진 로직을 거쳐 반드시 B라는 출력값이 나온다. 하지만 LLM은 기본적으로 다음에 올 단어를 확률적으로 예측하는 모델이다. 이러한 태생적 특징 때문에 상용 서비스를 구축할 때 치명적인 3가지 리스크가 발생하게 되며, 이를 제어하기 위해 LLMOps가 필요하다.</p>
<h3 id="1-비결정론적-출력-non-deterministic-output">1) 비결정론적 출력 (Non-deterministic Output)</h3>
<p>가장 개발자를 괴롭히는 요소다. LLM은 동일한 프롬프트를 입력해도 어제 다르고 오늘 다른 답변을 내놓을 수 있다. 
소프트웨어를 개발할 때는 코드가 정상적으로 작동하는지 확인하기 위해 단위 테스트를 작성한다. 하지만 LLM의 결과물은 매번 텍스트의 형태나 문맥이 미세하게 바뀌기 때문에 전통적인 방식의 테스트 코드(<code>assert output == &quot;정확한 텍스트&quot;</code>)를 작성하는 것이 불가능하다.
이러한 비결정성 때문에 시스템이 예기치 못한 에러를 뱉었을 때 버그를 재현하기가 극도로 어려우며, 출력 결과의 포맷(예: JSON 형식으로만 응답해달라는 요청)이 깨지는 현상이 빈번하게 발생한다.</p>
<h3 id="2-기하급수적인-비용cost과-지연-시간latency">2) 기하급수적인 비용(Cost)과 지연 시간(Latency)</h3>
<p>API를 통해 LLM을 사용할 때는 입력과 출력에 사용된 토큰의 수량만큼 요금이 부과된다. 단순한 1회성 질문과 답변이라면 큰 문제가 되지 않지만, 에이전트시스템으로 넘어가면 이야기가 달라진다.
최근의 AI 시스템은 문제를 스스로 계획하고, 검색하고, 코드를 실행하고, 결과를 스스로 검토하여 틀리면 다시 처음으로 돌아가는 루프 구조를 가진다. 만약 모델이 방향을 잘못 잡아 무한 루프에 빠지게 된다면, 에이전트는 계속해서 이전까지의 대화 기록을 통째로 짊어지고 모델을 호출하게 된다. 이는 순식간에 막대한 API 과금으로 이어질 수 있다.
또한, 하나의 최종 답변을 만들기 위해 모델을 내부적으로 5~10번씩 호출하게 되면, 사용자가 화면 앞에서 응답을 기다리는 지연 시간이 수십 초 단위로 길어져 서비스의 사용자 경험을 심각하게 훼손하게 된다.</p>
<h3 id="3-환각hallucination의-연쇄-작용">3) 환각(Hallucination)의 연쇄 작용</h3>
<p>LLM이 사실이 아닌 내용을 그럴듯하게 지어내는 환각 현상은 널리 알려져 있다. 하지만 멀티 에이전트(Multi-Agent) 시스템에서는 이 환각이 훨씬 더 무서운 결과를 초래한다.
예를 들어, 계약서를 분석하는 에이전트 파이프라인이 있다고 가정해 보자. 첫 번째 정보 추출 에이전트가 존재하지 않는 위약금 조항을 환각으로 만들어냈다. 만약 이 결과가 필터링 없이 두 번째 요약 에이전트, 세 번째 메일 발송 에이전트로 그대로 전달된다면 어떻게 될까? 단순한 오답 하나가 눈덩이처럼 불어나 기업의 법적 책임 문제나 심각한 비즈니스 리스크로 직결될 수 있다.</p>
<hr>
<h2 id="13-잘-작동하는-ai---통제-가능한-ai">1.3. &#39;잘 작동하는 AI&#39; -&gt; &#39;통제 가능한 AI&#39;</h2>
<p>앞서 말한 리스크들을 완벽하게 0으로 만드는 것은 현재의 LLM 기술로는 불가능에 가깝다. 그렇다면  어떻게 접근해야 할까? 강의에 핵심 메시지는 바로 &#39;통제 가능성&#39; 을 확보하는 것이었다. 성공적으로 작동할 때의 데모 영상은 누구나 화려하게 만들 수 있다. 하지만 진정한 엔지니어링은 실패했을 때 발견된다.</p>
<ul>
<li><strong>가시성과 투명성</strong>: 시스템 내부에서 에이전트들이 어떤 프롬프트를 주고받았고, 어떤 도구를 사용했으며, 어느 지점에서 시간이 가장 오래 걸렸는지 한눈에 추적할 수 있어야 한다.</li>
<li><strong>상태 추적과 재현성</strong>: 에러가 발생했다면, 그 에러가 발생하기 직전의 데이터 상태로 돌아가 디버깅을 하고 재실행할 수 있는 구조가 필요하다.</li>
<li><strong>안전장치(개입 가능성)</strong>: AI가 중요한 의사결정을 내리기 직전, 혹은 특정 비용 한도에 도달했을 때 프로세스를 멈추고 인간이 직접 개입하여 승인하거나 수정할 수 있는 연결 고리를 만들어 두어야 한다.
결국 LLMOps의 첫걸음은 &quot;AI 모델을 그냥 믿고 맡겨두면 알아서 잘하겠지&quot;라는 환상을 버리는 것에서 시작한다. AI는 매우 유능한 인턴이지만, 그 인턴이 사고를 치지 않도록 업무 프로세스를 잘게 쪼개고, 결재 라인을 만들고, 업무 지시서를 명확히 문서화하며, 중간 보고를 받도록 시스템을 설계하는 과정이 반드시 필요하다.
이러한 통제 가능한 아키텍처를 만들기 위해 어떠한 방식의 설계 절차를 거치고, 어떤 문서들을 작성해야하는지 알아보자.</li>
</ul>
<blockquote>
<p>참고/출처
<a href="https://tech.ktcloud.com/entry/2026-01-ktcloud-mlops-llmops-%EC%9A%B4%EC%98%81%EC%B2%B4%EA%B3%84-%EC%A0%84%ED%99%98">https://tech.ktcloud.com/entry/2026-01-ktcloud-mlops-llmops-%EC%9A%B4%EC%98%81%EC%B2%B4%EA%B3%84-%EC%A0%84%ED%99%98</a> (많이 배웁니다..)
<a href="https://www.databricks.com/blog/what-is-llmops">https://www.databricks.com/blog/what-is-llmops</a>
<a href="https://solutionshub.epam.com/blog/post/aiops">https://solutionshub.epam.com/blog/post/aiops</a>
<a href="https://www.redhat.com/ko/topics/ai/llmops">https://www.redhat.com/ko/topics/ai/llmops</a></p>
</blockquote>
<h1 id="2-ai-agent-시스템-설계-절차와-5대-핵심-문서화-전략">2. AI Agent 시스템 설계 절차와 5대 핵심 문서화 전략</h1>
<p>강의를 들으며 내 기존 개발 방식을 다시 돌아보게 되었다. 나는 그동안 아이디어가 떠오르면 전체적인 흐름을 먼저 머릿속에 그리고, 이를 여러 섹션으로 나눈 뒤 어떻게 개발하고 연결할지 구상하며 부분적으로 기능을 붙여나가는 식으로 개발을 진행해 왔다. 일반적인 애플리케이션이나 단순한 스크립트를 짤 때는 이 방식이 꽤 유용했다. 하지만 데모 수준을 넘어선 LLM 기반의 멀티 에이전트 시스템을 구축할 때 이런 머릿속 구상이나 점진적인 기능 추가 방식은 한계가 명확했다. 에이전트가 두세 개만 넘어가도 서로 어떤 데이터를 주고받는지, 예외 상황에서 흐름이 어떻게 꼬이는지 정확히 추적하고 통제하기가 불가능해지기 때문이다.
성공적인 AI Agent 시스템을 위해서는 코딩 전에 완벽한 문서화 형태의 설계가 선행되어야 한다. 이번 글에서는 내가 학습한 에이전트 시스템 설계의 5단계 흐름과, 5개 설계 문서에 대해 정리해 보았다.</p>
<h2 id="21-ai-agent-시스템-설계-5단계-흐름">2.1. AI Agent 시스템 설계 5단계 흐름</h2>
<p>개발자가 가장 저지르기 쉬운 실수는 &#39;문제&#39;보다 &#39;AI 모델&#39;을 먼저 생각하는 것이다. LLMOps 관점에서는 철저하게 비즈니스 문제 해결을 위한 소프트웨어 공학적 설계 절차를 따른다.</p>
<ul>
<li><strong>문제 정의 (Problem)</strong>: 우리가 현재 어떤 비즈니스적 어려움을 겪고 있으며, 기존 시스템의 한계는 무엇인지 명확히 한다.</li>
<li><strong>목표 정의 (Goal)</strong>: AI 시스템을 도입해서 얻고자 하는 최종 결과물과 성공을 측정할 수 있는 평가 지표를 수립한다.</li>
<li><strong>시나리오 정의 (Scenario)</strong>: 사용자가 시스템에 어떻게 접근하고, 어떤 입력을 주며, 최종적으로 어떤 형태의 출력을 받게 되는지 Use Case 기반의 서비스 흐름을 그린다.</li>
<li><strong>구조 설계 (Architecture Design)</strong>: 요구사항을 해결하기 위해 어떤 역할을 가진 에이전트들이 필요한지 쪼개고, 이들이 어떻게 상호작용할지 전체적인 워크플로우 초안을 그린다.</li>
<li><strong>상세 설계 (Detailed Design)</strong>: 본격적인 구현 직전, 각 에이전트의 구체적인 입력/출력값, 프롬프트 내용, 데이터의 상태 구조를 상세 문서로 명세화한다. </li>
</ul>
<p>이 중에서 시스템의 안정성과 직결되는 가장 중요한 작업이 바로 마지막 5단계인 &#39;상세 설계&#39;다.</p>
<hr>
<h2 id="22-통제를-위한-5대-핵심-설계-문서">2.2. 통제를 위한 5대 핵심 설계 문서</h2>
<p>강의에서 제공된 설계 문서 양식들을 직접 뜯어보면서, 시스템을 톱니바퀴처럼 맞물려 돌아가게 만드는 요소가 무엇인지 파악할 수 있었다. 프로젝트를 진행할떄 아래의 5가지 문서를 통해 에이전트를 완벽하게 제어하는것이 목표다.</p>
<h3 id="1-agent-역할-정의서-agent-role-definition">1) Agent 역할 정의서 (Agent Role Definition)</h3>
<p>각 에이전트에게 명확한 자아와 책임 범위를 부여하는 문서다. 하나의 에이전트가 너무 많은 일을 하면 환각이 발생할 확률이 급격히 높아지므로 역할을 철저히 분리해야 한다.</p>
<ul>
<li><strong>목적 및 역할</strong>: 예를 들어 Planner Agent는 계획만 세우고, Executor Agent는 계획에 따라 계약서에서 정보만 추출하며, Critic Agent는 결과물의 품질만 평가하도록 쪼갠다.</li>
<li><strong>입력과 출력</strong>: 해당 에이전트가 실행되기 위해 필요한 필수 데이터(Input)와 다음 노드로 넘겨줄 결과 데이터(Output)를 명시한다.</li>
<li><strong>제약 조건 및 실패 리스크</strong>: 문서에 없는 내용은 절대 추측하지 말 것, 실행 로직을 포함하지 말 것 같은 엄격한 규칙과, 이 에이전트가 실패했을 때 전체 워크플로우에 미치는 파급력을 기록한다.</li>
</ul>
<h3 id="2-workflow-명세서-workflow-specification">2) Workflow 명세서 (Workflow Specification)</h3>
<p>역할이 부여된 에이전트들이 어떤 순서로 동작하는지 전체적인 실행 뼈대를 정의한다.
시작 단계에서 어떤 데이터가 입력되는지, 그리고 Planner에서 Executor, Critic, Supervisor를 거쳐 종료에 이르는 각 단계마다 데이터가 어떻게 가공되고 전달되는지를 한눈에 볼 수 있도록 도식화 및 문서화한다. 이 문서가 있어야만 무한 루프에 빠지는 설계적 결함을 사전에 방지할 수 있다.</p>
<h3 id="3-state-정의서-state-specification">3) State 정의서 (State Specification)</h3>
<p>개인적으로 멀티 에이전트 시스템에서 가장 중요하다고 느낀 부분이다. 에이전트 시스템은 &#39;State(상태)&#39;라는 전역 객체를 메모리처럼 서로 공유하며 작업을 진행한다.</p>
<ul>
<li><strong>변수 및 데이터 타입 정의</strong>: 전체 과정을 기록할 <code>messages</code>, 사용자의 최초 요구사항인 <code>user_request</code>, 에이전트 간 주고받을 <code>extracted_info</code>나 <code>summary</code> 등 상태에 담길 모든 변수를 정의한다.</li>
<li><strong>업데이트 방식 (Reducer vs Overwrite)</strong>: 특정 변수에 새로운 값이 들어왔을 때, 이전 값을 지우고 덮어쓸 것인지(Overwrite), 아니면 기존 배열에 값을 계속 누적하여 추가할 것인지(Reducer)를 설계한다. 예를 들어 대화 기록인 <code>messages</code>는 누적되어야 하고, 현재 실행 단계를 나타내는 <code>current_step_index</code>는 다음 숫자로 덮어써져야 한다. 이 규칙이 어긋나면 시스템은 과거의 기억을 잃거나 불필요한 데이터 폭탄을 안고 실행을 반복하게 된다.</li>
</ul>
<h3 id="4-decision-policy-의사결정-정책서">4) Decision Policy (의사결정 정책서)</h3>
<p>분기점(라우팅)에서 어떤 조건일 때 어떤 에이전트를 실행할지, 언제 반복을 종료할지 명확한 수학적/논리적 조건식으로 정의한 문서다.</p>
<ul>
<li><strong>실행 및 평가 제어</strong>: 계획된 단계가 남아있으면 다음 Executor를 실행하고, 모든 단계가 끝났으면 Critic을 호출하여 평가를 받게 한다.</li>
<li><strong>재실행 및 강제 종료 정책</strong>: Critic의 평가 결과가 거절일 때 무조건 다시 실행하는 것이 아니라, 현재 반복 횟수가 최대 허용 횟수 미만일 때만 Planner로 돌아가 재계획을 수행한다. 만약 제한 횟수에 도달했다면 비용 폭증을 막기 위해 현재까지의 결과만 반환하고 강제로 종료하도록 조건을 촘촘하게 설정한다.</li>
</ul>
<h3 id="5-prompt-명세서-prompt-specification">5) Prompt 명세서 (Prompt Specification)</h3>
<p>개발자가 코딩할 때 변수명을 정하듯, LLM에게 전달할 프롬프트의 골격을 문서로 고정한다.</p>
<ul>
<li><strong>페르소나와 지침</strong>: 에이전트가 맡은 역할에 몰입할 수 있도록 구체적인 전문가 페르소나를 부여하고, 수행해야 할 과업을 간결하고 명확하게 지시한다.</li>
<li><strong>엄격한 출력 형식(Formatting)</strong>: 비결정론적 특성을 통제하기 위해 응답 형식을 매우 엄격하게 제한한다. 예를 들어 Planner는 반드시 파이썬 리스트 형태로만 응답하도록 강제하고, 다른 부연 설명은 일절 금지한다. 이러한 출력 포맷을 사전에 문서로 확정해야만 코드로 파싱할 때 발생하는 오류를 막을 수 있다.</li>
</ul>
<p>이 5가지 문서를 꼼꼼히 작성하는 과정이 처음에는 꽤나 번거롭고 시간이 오래 걸리는 작업처럼 느껴졌다. 부분적으로 기능을 떼어 붙여가며 코딩하던 나의 익숙한 방식과는 다소 거리가 있었기 때문이다. 하지만 복잡도를 높여가며 테스트를 해보니, 이 설계 문서들이야말로 개발 시간을 줄여주고 시스템을 내 장악하는데 큰 영향을 준다. 기능이 얽혀서 버그가 났을 때 어디를 고쳐야 할지, 어떤 상태 값이 잘못되었는지 문서에 이미 답이 다 적혀 있기 때문이다.</p>
<blockquote>
<p>참고/출처
<a href="https://wikidocs.net/blog/@jaehong/9475/">https://wikidocs.net/blog/@jaehong/9475/</a>
<a href="https://www.youtube.com/watch?v=UCV3lAo0fhQ">https://www.youtube.com/watch?v=UCV3lAo0fhQ</a>
<a href="https://lobehub.com/skills/openclaw-skills-naver-search">https://lobehub.com/skills/openclaw-skills-naver-search</a> (이런것도 있구나)</p>
</blockquote>
<h1 id="3-역할-기반-설계와-워크플로우를-지휘하는-supervisor-패턴">3. 역할 기반 설계와 워크플로우를 지휘하는 Supervisor 패턴</h1>
<p>구체적으로 에이전트들의 역할을 어떻게 분리하고 이들의 실행 흐름을 어떻게 중앙에서 강력하게 통제하는지 그 아키텍처의 핵심인 역할 기반 설계(Role-based Design)와 Supervisor 패턴에 대해 복습해보려고한다.</p>
<h2 id="31-머릿속의-섹션-구상을-에이전트의-역할-분리로-구체화하기">3.1. 머릿속의 섹션 구상을 에이전트의 역할 분리로 구체화하기</h2>
<p>나는 위에서 한번 언급 했듯 평소에 새로운 아이디어가 떠오르면 전체적인 흐름을 먼저 머릿속에 그려둔 뒤, 이를 여러 섹션으로 나눈다. 그리고 각 섹션별로 어떻게 개발하고 연결해야 할지 틀을 잡은 다음, 부분적으로 기능을 하나씩 붙여가며 살을 붙이는 방식으로 개발을 진행해 왔다. 이러한 탑다운(Top-down) 방식의 접근과 점진적인 기능 결합은 일반적인 애플리케이션 개발에서 코드를 깔끔하게 유지하는 데 큰 도움이 되었다.
강의를 들으며 놀라웠던 점은, 내가 평소에 하던 이 개발 습관이 LLMOps에서 복잡한 문제를 해결하기 위해 사용하는 역할 기반 설계의 철학과 정확히 맞물려 있다는 사실이었다. 거대하고 모호한 비즈니스 문제를 하나의 프롬프트로 해결하려 하지 않고, 전체 흐름을 논리적인 섹션으로 쪼갠 뒤 그 섹션마다 전담 전문가(에이전트)를 배치하여 부분적인 기능을 독립적으로 수행하게 만드는 것이다.
다만 결정적인 차이점이 있었다. 일반적인 소프트웨어에서는 내가 나눈 섹션들이 정해진 로직에 따라 완벽하게 맞물려 돌아가지만, LLM 환경에서는 각 섹션을 담당하는 주체가 비결정론적인 인공지능이라는 점이다. 따라서 머릿속 구상에만 의존해 대강 기능을 붙여 나가면 에이전트들이 서로 엉뚱한 데이터를 주고받으며 전체 파이프라인이 쉽게 무너진다. 이를 방지하기 위해 전문가 에이전트들의 역할을 수학적으로 명확히 분리하고, 이들을 유기적으로 연결하는 고도화된 아키텍처 패턴이 필요하다.</p>
<hr>
<h2 id="32-planner-executor-critic의-전문가-삼각-편대">3.2. Planner, Executor, Critic의 전문가 삼각 편대</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/9e76127d-103b-454a-b521-fe0446c28720/image.png" alt=""></p>
<p>역할 기반 설계에서 가장 널리 쓰이면서도 강력한 구조는 복잡한 과업을 <strong>Planner(기획자)</strong>, <strong>Executor(수행자)</strong>, <strong>Critic(검토자)</strong> 이라는 세 가지 축으로 분리하는 것이다. 하나의 AI에게 &quot;문서를 분석하고 요약해서 검증까지 끝내라&quot;고 하면 과부하가 걸려 환각을 일으키기 쉽지만, 역할을 쪼개면 각 단계에서 모델이 집중해야 할 컨텍스트의 크기가 줄어들어 품질이 극대화된다.</p>
<h3 id="1-planner-agent-전략과-계획-수립의-사령탑">1) Planner Agent: 전략과 계획 수립의 사령탑</h3>
<p>Planner는 사용자의 최초 요구사항과 분석해야 할 원문 데이터를 입력받아, 직접 문제를 해결하는 대신 문제를 해결하기 위한 &#39;단계별 실행 계획(Plan)&#39;을 수립하는 역할을 한다.</p>
<ul>
<li><strong>동작 원리</strong>: 사용자가 아무리 복잡하거나 모호한 요청을 하더라도, Planner는 자신이 사용할 수 있는 도구나 하위 에이전트들의 목록을 고려하여 최적의 실행 순서를 정의한다. 예를 들어 프롬프트 명세서에 정의된 규칙에 따라 반드시 파이썬 리스트 형태(<code>[&quot;executor1&quot;, &quot;executor2&quot;]</code>)로만 실행 계획을 출력하도록 강력히 통제된다. 부연 설명이나 변명을 일절 배제함으로써 후속 코드가 결과를 안정적으로 파싱할 수 있게 만든다.</li>
<li><strong>재계획(Replanning) 기능</strong>: Planner의 진짜 가치는 실패했을 때 드러난다. 뒤에서 설명할 Critic에 의해 결과물이 거절(REJECTED)되면, Planner는 단순하게 이전 계획을 반복하는 것이 아니라 State에 누적된 Critic의 구체적인 거절 사유(Critique reason)를 읽어 들인다. &quot;어느 지점의 정보 추출이 부실했다&quot;는 피드백을 바탕으로 계획을 전면 수정하거나 새로운 단계를 추가하는 재계획을 수행한다.</li>
</ul>
<h3 id="2-executor-agent-철저하게-지침에-따르는-유닛">2) Executor Agent: 철저하게 지침에 따르는 유닛</h3>
<p>Executor는 Planner가 수립한 계획의 단위를 하나씩 넘겨받아 실제 데이터 가공 및 분석을 수행하는 에이전트다. 프로젝트의 특성에 따라 여러 개의 Executor를 독립적으로 배치할 수 있다. 이번 계약서 분석 워크플로우 예시에서는 두 가지 Executor로 세분화되었다.</p>
<ul>
<li><strong>Executor1 (정보 추출 전문가)</strong>: 계약서 원문과 사용자 요청을 비교하여 계약 당사자, 계약 기간, 계약 금액 및 사용자가 별도로 요구한 특약 사항 등의 핵심 정보만 항목형으로 깔끔하게 추출한다. 프롬프트 지침에 &quot;문서에 없는 내용은 절대로 유추하거나 추측하지 말 것&quot;이라는 제약 조건을 엄격하게 고정하여 환각 발생을 차단한다. 작업을 마치면 추출된 정보를 State에 기록하고, 현재 진행 단계를 나타내는 인덱스(<code>current_step_index</code>)를 1 증가시킨 뒤 제어권을 넘긴다.</li>
<li><strong>Executor2 (요약 전문가)</strong>: 원문 문서와 Executor1이 앞서 추출해 둔 핵심 정보를 동시에 입력받아 전체 내용을 명확하게 요약하는 실무를 담당한다. 추출된 정보를 레퍼런스로 삼기 때문에 완전히 엉뚱한 내용으로 요약할 위험이 현저히 줄어든다. 지침에 따라 3문장 이내의 bullet-point 형식 등 일관된 포맷으로 결과물(<code>summary</code>)을 작성하여 State를 업데이트한다.</li>
</ul>
<h3 id="3-critic-agent-타협-없는-품질-검증관">3) Critic Agent: 타협 없는 품질 검증관</h3>
<p>Critic은 앞선 Executor들이 만들어낸 최종 결과물(<code>extracted_info</code>, <code>summary</code>)이 사용자의 최초 요구사항을 완벽하게 충족하는지 제3자의 시선에서 냉정하게 평가하는 에이전트다.</p>
<ul>
<li><strong>평가와 사유의 명시</strong>: Critic은 새로운 정보를 창출해서는 안 되며, 오직 검증만 수행해야 한다. 결과가 합격점이라면 승인(<code>critique_decision = &quot;APPROVED&quot;</code>)을 내리고 워크플로우를 종료 단계로 이끈다. 만약 지침이 누락되었거나 품질이 떨어진다면 과감하게 거절(<code>critique_decision = &quot;REJECTED&quot;</code>) 판정을 내린다. 이때 가장 중요한 것은 단순히 거절하는 것에 그치지 않고, Planner가 계획을 보정할 수 있도록 구체적인 불합격 사유(<code>critique_reason</code>)를 논리적 문장으로 기록하여 State에 남겨야 한다는 점이다.</li>
</ul>
<hr>
<h2 id="33-왜-supervisor-패턴인가-중앙-집권식-흐름-통제의-힘">3.3. 왜 Supervisor 패턴인가? 중앙 집권식 흐름 통제의 힘</h2>
<p>Planner, Executor, Critic을 각각 잘 만드는 것보다 훨씬 더 중요한 과제는 이 에이전트들을 어떻게 연결할 것인가이다. 
에이전트 시스템을 처음 개발할 때 흔히 하는 실수는 에이전트가 직접 다음 행동을 결정하게 만드는 것이다. 예를 들어 Executor1이 끝나면 스스로 판단해서 Executor2를 호출하고, Critic이 끝나면 Critic 내부 로직 안에서 판단해 Planner로 직접 데이터를 던지는 방식이다. 이런 구조는 내가 평소 하던 대로 섹션을 나눠 기능을 붙여나갈 때 자칫 발생하기 쉬운 끈끈한 결합 상태를 유발한다. 에이전트가 많아질수록 화살표가 사방으로 얽히며 코드가 스파게티처럼 꼬이고, 예외 처리가 불가능해진다.
이 문제를 해결하는 아키텍처가 바로 <strong>Supervisor 패턴</strong>이다. 모든 하위 에이전트들은 서로의 존재를 알 필요가 없다. 이들은 오직 전역 상태(State)에서 자신에게 필요한 입력값만 가져와 작업을 수행하고, 결과물만 다시 State에 던진 뒤 실행을 종료한다. 그리고 에이전트들의 다음 실행 노드를 결정하고 라우팅하는 모든 제어권은 중앙의 <strong>Supervisor</strong>가 전담한다.</p>
<p>Supervisor 패턴이 제공하는 통제력의 이점은 명확하다.</p>
<h3 id="1-제어-로직과-실행-로직의-완전한-분리">1) 제어 로직과 실행 로직의 완전한 분리</h3>
<p>Critic 에이전트는 품질 평가만 담당할 뿐, 시스템의 흐름을 바꾸는 라우팅 코드를 내포하지 않는다. Critic이 내린 판정 결과(<code>critique_decision</code>)가 State에 저장되면, 규칙 기반으로 설계된 Supervisor가 Decision Policy(의사결정 정책서)에 맞춰 물리적인 조건 분기를 실행한다. AI의 모호한 판단에 시스템 흐름 제어를 맡기지 않고, 개발자가 작성한 명확한 소스코드 라인 위에서 흐름을 제어하기 때문에 시스템 가시성이 극대화된다.</p>
<h3 id="2-비용-방어를-위한-루프-통제-max_iterations">2) 비용 방어를 위한 루프 통제 (max_iterations)</h3>
<p>비결정론적인 LLM 특성상 Planner와 Critic 사이에 갇혀 무한히 수정을 반복하는 루프가 발생할 수 있다. 이는 상용 서비스에서 심각한 API 비용 폭증의 원인이 된다. 
Supervisor는 중앙에서 반복 횟수(<code>iteration</code>)라는 상태 값을 직접 카운트하고 관리한다. Critic이 아무리 거절 판정을 내렸더라도, Supervisor 단에서 사전에 설정된 최대 허용 횟수(<code>max_iterations</code>)에 도달했다고 판단하면 재계획을 차단하고 현재까지의 결과물만 들고 프로세스를 강제 종료(END) 시킬 수 있는 강력한 브레이크 역할을 수행한다.</p>
<hr>
<h2 id="34-통제-가능한-아키텍처가-주는-확신">3.4. 통제 가능한 아키텍처가 주는 확신</h2>
<p>아이디어를 섹션별로 분리하고 부분적인 기능을 결합해 나가던 나의 개발 방식이, Supervisor라는 명확한 조정자와 상태 중심의 통제 구조를 만나면서 비로소 상용 수준의 AI 에이전트 아키텍처로 완성될 수 있음을 배웠다. 각 에이전트의 독립성을 보장하고 중앙에서 규칙 기반으로 흐름을 장악하는 이 아키텍처는 시스템이 복잡해질수록 진가를 발휘할 것이라고 생각이 들었다.
이러한 대규모 시스템 통제 기법을 깊이 있게 학습하면서, 자연스럽게 내가 프로젝트를 진행하면서 관심 있게 바라보던 구체적인 엔지니어링 도메인에 이 아키텍처를 대입해 보게 되었다. 특히 수많은 회로와 커넥터, 배선 지침이 복잡하게 얽혀 있어 엄격한 규칙과 품질 검증이 필수적인 <strong>하네스 엔지니어링</strong> 분야가 떠올랐다. 방대한 하네스 설계 표준 문서를 분석하고, 사양에 맞춰 회로 배치를 계획하며, 설계 결함을 검증하는 일련의 과정을 오늘 배운 Planner, Executor, Critic 삼각 편대와 Supervisor 패턴으로 구조화한다면 대단히 강력한 자동화 시스템을 만들 수 있겠다는 생각이 들었다.</p>
<blockquote>
<p>참고/출처
<a href="https://medium.com/@servifyspheresolutions/planner-executor-critic-engineering-reliable-ai-agents-4eed3b5ddb54">https://medium.com/@servifyspheresolutions/planner-executor-critic-engineering-reliable-ai-agents-4eed3b5ddb54</a>
<a href="https://dl-pkw.tistory.com/entry/8-2-%EC%97%AD%ED%95%A0-%EB%B6%84%EB%A6%AC-Planner-Worker-Critic">https://dl-pkw.tistory.com/entry/8-2-%EC%97%AD%ED%95%A0-%EB%B6%84%EB%A6%AC-Planner-Worker-Critic</a>
<a href="https://breyta.ai/blog/ai-agent-architecture-patterns">https://breyta.ai/blog/ai-agent-architecture-patterns</a></p>
</blockquote>
<h1 id="4-시스템의-안전장치를-확보하는-인간-개입human-in-the-loop-설계">4. 시스템의 안전장치를 확보하는 인간 개입(Human-In-The-Loop) 설계</h1>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/586cf544-1b9e-4261-a160-614f44b6c89f/image.png" alt=""></p>
<p>구조적으로 완벽해 보이는 에이전트 아키텍처를 설계하고 나면, 모든 것을 AI에게 맡겨두고 알아서 완벽하게 처리해 주기를 기대하게 된다. 하지만 LLM의 본질적인 한계인 환각과 비결정성을 완벽하게 제거할 수 없는 한, 100% 완전 자동화는 비즈니스 환경에서는 폭탄과 같다. 특히 금융 거래, 법률 계약서 검토, 혹은 내가 관심을 두고 있는 하네스 엔지니어링의 배선 설계 사양 검증처럼 작은 오류 하나가 막대한 자산 손실이나 안전사고로 직결되는 고위험 도메인에서는 더욱 그렇다.
이러한 문제를 해결하기 위해 시스템 내부의 가장 강력한 방어선이자 통제 장치로서 사람을 워크플로우에 명시적으로 포함하는 <strong>HITL(Human-In-The-Loop)</strong> 설계에 대해 깊이 있게 학습했다.</p>
<h2 id="41-완전-자동화의-환상과-hitl의-본질">4.1. 완전 자동화의 환상과 HITL의 본질</h2>
<p>처음 에이전트 시스템을 접했을 때는 인간의 개입이 없는 완전한 자동화만이 정답이라고 생각했다. 사람이 일일이 확인해야 한다면 인공지능을 도입하는 의미가 퇴색되는 것이 아닌가 하는 의문이 들었기 때문이다. 하지만 상용 수준의 LLMOps를 구축할 때 HITL은 자동화의 실패를 의미하는 것이 아니라, 오히려 시스템의 신뢰성을 완성하는 컴포넌트임을 배웠다.
비즈니스 프로세스에서 AI는 매우 빠르고 효율적으로 방대한 데이터를 처리하는 유능한 일꾼이지만, 최종적인 법적 책임이나 안전성에 대한 담보는 오직 인간만이 할 수 있다. 따라서 HITL은 시스템의 실행 과정 중에 사람이 검토하고 승인하거나 수정할 수 있는 연결 고리를 의도적으로 배치하는 설계 기법이다. 이를 통해 AI가 가진 한계를 인간의 판단력으로 보완하며, 동시에 인간이 모든 과정을 처음부터 끝까지 수작업으로 처리하던 비효율을 획기적으로 줄이는 균형점을 찾을 수 있다.</p>
<hr>
<h2 id="42-사전-통제proactive-control-계층으로서의-hitl">4.2. 사전 통제(Proactive Control) 계층으로서의 HITL</h2>
<p>인간의 개입을 설계할 때 가장 주의해야 할 점은 문제의 발생 시점이다. 많은 개발자가 저지르는 실수는 시스템이 예측하지 못한 에러를 뱉으며 멈추거나, 이미 잘못된 결과물이 고객에게 발송된 이후에야 사람이 개입하는 사후 조치(Reactive) 방식으로 시스템을 만드는 것이다.
LLMOps에서 다루는 HITL은 철저하게 <strong>사전 통제(Proactive Control)</strong> 계층으로 설계되어야 한다.</p>
<ul>
<li><strong>Reactive (사후 조치)</strong>: 에이전트가 환각 기반의 잘못된 분석 결과를 담아 이메일을 발송한 뒤, 고객의 항의를 받고 관리자가 로그를 보며 수동으로 수습하는 방식이다. 이미 비즈니스적 신뢰가 무너진 상태이므로 시스템으로서 가치가 떨어진다.</li>
<li><strong>Proactive (사전 통제)</strong>: 에이전트가 분석 결과를 완성하더라도 즉시 발송 노드로 진입하지 못하도록 워크플로우 중간에 의도적인 &#39;대기 상태(Pause)&#39; 노드를 설계한다. 관리자가 해당 결과를 화면에서 검토하고 승인 버튼을 누르기 전까지는 다음 프로세스로 절대 진행되지 않도록 기술적 안전장치를 걸어두는 방식이다.
이러한 사전 통제 구조를 통해 AI가 무한 루프에 빠져 API 비용을 날리거나, 치명적인 결함이 외부로 노출되는 리스크를 완벽하게 차단할 수 있다.</li>
</ul>
<hr>
<h2 id="43-supervisor-패턴-내에서의-hitl-구현-원칙">4.3. Supervisor 패턴 내에서의 HITL 구현 원칙</h2>
<p>인간 개입을 시스템에 녹여낼 때, 아무렇게나 사람을 호출하도록 코드를 짜면 아키텍처가 순식간에 붕괴된다. 강의를 통해 배운 Supervisor 패턴 기반의 HITL 구현에는 두 가지 철저한 엔지니어링 원칙이 존재했다.</p>
<h3 id="1-하위-worker-에이전트는-절대로-사람을-직접-호출하지-않는다">1) 하위 Worker 에이전트는 절대로 사람을 직접 호출하지 않는다</h3>
<p>만약 정보 추출을 담당하는 Executor1이나 요약을 담당하는 Executor2가 작업을 하다가 &quot;이 부분은 모호하니 사람이 확인해 주세요&quot;라고 직접 인간 개입 노드로 흐름을 넘기게 되면, 전체 워크플로우의 제어권이 사방으로 분산된다. 에이전트가 늘어날수록 어떤 노드에서 사람을 호출했는지 추적하기 어려워지며, 중앙의 의사결정 정책이 무력화된다.
따라서 Worker 에이전트들은 오직 자신이 맡은 기능만 수행하고 결과물과 상태를 공유 객체(State)에 기록한 뒤 종료되어야 한다. 상태를 읽은 중앙의 <strong>Supervisor만이 현재 에이전트의 수행 결과나 카운트된 리스크 점수를 기반으로 HITL 노드(<code>human_review</code>)를 트리거</strong>할 수 있어야 한다. 제어권을 중앙 집권식으로 유지해야만 일관된 운영 정책을 적용할 수 있다.</p>
<h3 id="2-인간을-비결정론적인-외부-노드로-취급한다">2) 인간을 비결정론적인 외부 노드로 취급한다</h3>
<p>시스템 아키텍처 관점에서 볼 때, 인간은 일반적인 파이썬 함수처럼 즉각적인 응답을 주는 존재가 아니다. 검토하는 데 수 분에서 수 시간이 걸릴 수도 있고, 한 사람의 인건비가 소요되므로 비용도 높다. 또한 사람마다 판단 기준이 조금씩 다를 수 있으므로 비결정론적인 특성을 지닌다.
즉, 시스템 입장에서는 인간 역시 LLM 모델이나 외부의 거대한 검색 API를 호출하는 것과 다름없는 <strong>&#39;외부 도구 혹은 독립된 노드&#39;</strong>로 취급해야 한다. 그래프 상에 인간 개입 노드를 명시적으로 배치하고, 사람이 응답을 줄 때까지 상태를 안전하게 보존하며 대기할 수 있는 구조가 뒷받침되어야 상용 서비스 환경에서 시스템이 멈추지 않고 매끄럽게 구동될 수 있다.</p>
<hr>
<h2 id="44-인간-개입-이후의-사후-흐름-처리">4.4. 인간 개입 이후의 사후 흐름 처리</h2>
<p>인간 개입 노드가 트리거되어 관리자가 에이전트의 중간 결과물을 검토한 후, 내릴 수 있는 의사결정의 유형은 명확하게 구조화되어야 한다. 시스템은 사람이 입력한 피드백에 따라 이후의 워크플로우 흐름을 크게 세 가지 분기로 나누어 처리하도록 설계한다.</p>
<h3 id="1-approve-승인">1) APPROVE (승인)</h3>
<p>에이전트가 수행한 정보 추출이나 요약의 품질이 완벽하다고 판단하여 승인하는 경우다. 이 조건이 만족되면 Supervisor는 해당 상태를 확인하고 프로세스를 성공적으로 종료하거나, 실제 메일 발송이나 데이터베이스 반영과 같은 다음 후속 노드로 흐름을 이어가게 만든다.</p>
<h3 id="2-abort-중단">2) ABORT (중단)</h3>
<p>에이전트가 도저히 해결할 수 없는 잘못된 방향으로 계획을 세웠거나, 분석 대상 문서 자체가 훼손되어 있어서 더 이상 프로세스를 진행하는 것이 의미가 없다고 판단하는 경우다. 이 경우 추가적인 API 비용 낭비를 막기 위해 전체 워크플로우를 즉시 강제 종료 시키고 시스템을 초기화한다.</p>
<h3 id="3-revise-수정-및-재계획">3) REVISE (수정 및 재계획)</h3>
<p>결과물에 일부 누락이나 오류가 있어 보정이 필요한 경우다. 이때 인간은 단순히 거절 버튼만 누르는 것이 아니라, 무엇이 잘못되었는지 구체적인 가이드라인이나 지침을 텍스트 형태의 피드백으로 작성하여 공유 상태에 저장한다.</p>
<p>Supervisor는 이 REVISE 신호와 함께 저장된 피드백을 확인하면, 시스템의 제어권을 다시 맨 앞단의 <strong>Planner Agent</strong>로 돌려보낸다. Planner는 사용자의 최초 요청뿐만 아니라, 방금 전 인간이 남긴 구체적인 피드백(<code>human_feedback</code>)까지 컨텍스트로 함께 입력받는다. &quot;인간 검토자가 이 지점의 수치 오류를 지적했으니, 이를 반영하여 실행 계획을 수정하자&quot;라며 계획을 전면 보정하는 재계획을 수행하게 된다. 인간과 AI가 협력하는 유기적인 피드백 루프가 완성되는 지점이다.</p>
<hr>
<h2 id="45-인간과-ai의-협업-체계가-주는-안정감">4.5. 인간과 AI의 협업 체계가 주는 안정감</h2>
<p>HITL 설계를 배우면서 AI 에이전트 시스템을 대하는 시야가 넓어졌다. 모든 것을 인공지능이 완벽하게 처리하도록 프롬프트를 깎는 데 시간을 허비하는 것보다, 중요한 길목에 사람이 검토할 수 있는 안전장치를 아키텍처적으로 배치하는 것이 훨씬 현명하고 강력한 통제 방법이라는 것을 배웠다.
HITL 설계를 직접 프로젝트에도 사용해보자고..</p>
<blockquote>
<p>참고/출처
<a href="https://macgence.com/blog/hitl-human-in-the-loop/">https://macgence.com/blog/hitl-human-in-the-loop/</a>
<a href="https://wikidocs.net/316360">https://wikidocs.net/316360</a>
<a href="https://www.ibm.com/kr-ko/think/topics/human-in-the-loop">https://www.ibm.com/kr-ko/think/topics/human-in-the-loop</a></p>
</blockquote>
<h1 id="5-시점-관리와-실행-관측">5. 시점 관리와 실행 관측</h1>
<h2 id="51-시점-관리">5.1. 시점 관리</h2>
<p>에이전트 시스템이 구동될 때 가장 곤란한 상황은, 총 10단계로 이루어진 복잡한 파이프라인에서 8번째 단계에 치명적인 오류가 발생했을 때다. 일반적인 스크립트라면 처음부터 다시 실행하면 되지만, 토큰당 과금이 이루어지고 결과가 매번 달라지는 LLM 환경에서는 처음부터 재실행하는 것이 비용 낭비이자 또 다른 오류를 낳을 수 있다.
이를 해결하기 위해 프레임워크 내부에 존재하는 것이 바로 전역 상태의 변화 과정을 매 노드마다 사진 찍듯 저장해 두는 <strong>스냅샷(Snapshot)</strong> 기능이다.</p>
<h3 id="1-memorysaver와-체크포인트-이력-추적">1) MemorySaver와 체크포인트 이력 추적</h3>
<p>그래프 컴파일 단계에서 <code>MemorySaver</code>와 같은 체크포인터를 연결해 두면, 에이전트가 한 노드를 통과할 때마다 당시의 모든 변수 상태(<code>messages</code>, <code>extracted_info</code>, <code>current_step_index</code> 등)가 고유한 ID(<code>checkpoint_id</code>)와 함께 저장된다.
문제가 발생했을 때 개발자는 <code>graph.get_state_history()</code> 메서드를 호출하여 시간 역순으로 상태 변화 이력을 모두 꺼내볼 수 있다. &quot;아, Executor2가 요약을 수행할 때 <code>extracted_info</code> 값이 이미 빈 문자열로 넘어왔구나&quot; 하는 식으로 에러의 근본 원인이 발생한 정확한 노드와 시점을 특정할 수 있게 된다.</p>
<h3 id="2-타임-트래블-time-travel과-부분-재실행">2) 타임 트래블 (Time Travel)과 부분 재실행</h3>
<p>스냅샷의 진정한 가치는 단순히 기록을 보는 것을 넘어, 과거의 특정 시점으로 시스템의 상태를 되돌려 거기서부터 다시 실행하는 <strong>타임 트래블</strong>에 있다.
에러가 발생하기 직전의 정상적인 <code>checkpoint_id</code>를 찾아 설정(Config) 객체에 담아 그래프를 호출(<code>graph.invoke(None, config)</code>)하면, 시스템은 처음부터 API를 호출하지 않고 과거의 상태를 그대로 이어받아 실패한 노드부터 재실행을 시작한다. 프롬프트를 일부 수정하거나 버그를 고친 뒤 곧바로 해당 시점부터 테스트해 볼 수 있어, 디버깅 시간과 API 비용을 획기적으로 절약할 수 있다.</p>
<hr>
<h2 id="52-실행-관측">5.2. 실행 관측</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/98bc5ff6-54f1-46bc-929c-5e1820e65493/image.png" alt=""></p>
<p>상태 데이터가 &#39;무엇&#39;이 변했는지 알려준다면, 실행 관측은 시스템이 &#39;어떻게&#39; 실행되었고 자원을 &#39;얼마나&#39; 소모했는지 알려주는 모니터링 체계다. LangSmith 같은 전문 관측 플랫폼을 연동하여 시스템의 성능을 계측한다.</p>
<h3 id="1-run-tree와-waterfall-시각화">1) Run Tree와 Waterfall 시각화</h3>
<p>코드에 환경 변수 단 한 줄(<code>LANGSMITH_TRACING=true</code>)만 추가하면, 런타임에 발생하는 모든 LLM 호출, 도구사용, 에이전트 라우팅 내역이 중앙 서버로 전송된다. 플랫폼 대시보드에서는 이 복잡한 실행 과정을 트리 구조와 시간 축 기반의 Waterfall 차트로 시각화하여 보여준다. 어떤 에이전트가 다른 에이전트를 호출했는지, 그 과정에서 주고받은 프롬프트의 원문은 무엇인지 블랙박스 같았던 내부가 투명하게 드러난다.</p>
<h3 id="2-지연-시간latency-및-비용cost-병목-진단">2) 지연 시간(Latency) 및 비용(Cost) 병목 진단</h3>
<p>Waterfall 뷰를 보면 전체 응답 시간이 30초 걸렸을 때, 어느 노드에서 가장 오랜 시간(예: 25초)이 정체되었는지 한눈에 파악할 수 있다. 특정 검색 도구의 API 지연인지, 특정 프롬프트가 너무 길어서 LLM 응답이 늦어지는 것인지 병목 구간을 정확히 진단 가능하다. 또한 각 단계별로 입력 토큰과 출력 토큰이 얼마나 소모되었고, 이것이 실제 달러($) 비용으로 얼마인지 실시간으로 계산되어 나타난다. 불필요하게 토큰을 많이 소모하는 프롬프트를 최적화하는 기준점이 된다.</p>
<blockquote>
<p>찰고/출처
<a href="https://oidea.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C-%EC%9A%B4%EC%98%81-%EB%A1%9C%EA%B7%B8-%ED%8F%89%EA%B0%80-%EA%B4%80%EC%B8%A1%EC%84%B1%EC%9D%84-%EC%84%A4%EA%B3%84%ED%95%98%EB%8A%94-%EB%B2%95">https://oidea.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C-%EC%9A%B4%EC%98%81-%EB%A1%9C%EA%B7%B8-%ED%8F%89%EA%B0%80-%EA%B4%80%EC%B8%A1%EC%84%B1%EC%9D%84-%EC%84%A4%EA%B3%84%ED%95%98%EB%8A%94-%EB%B2%95</a>
<a href="https://smith.langchain.com/">https://smith.langchain.com/</a>
<a href="https://sudormrf.run/2024/08/17/langsmith_review/">https://sudormrf.run/2024/08/17/langsmith_review/</a>
<a href="https://wikidocs.net/250954">https://wikidocs.net/250954</a></p>
</blockquote>
<hr>
<h2 id="생각정리">생각정리</h2>
<p>그동안의 나는 AI API를 호출해 원하는 결과물을 빠르게 만들어내는 과정 자체에 만족해왔던 것 같다. 프롬프트를 조금 더 정교하게 다듬고, 모델을 연결해 자동화를 구성하며 “AI로 이런 것도 가능하구나”를 체감했다면, 이번 LLMOps 학습을 통해서는 완전히 다른 관점을 배우게 되었다. 이제는 단순히 모델의 성능을 보는것이 아닌, 비결정론적인 AI를 어떻게 로직 안에서 안정적으로 묶고 통제할 것인가를 고민하는, 조금은 시스템 아키텍트에 가까운 시야를 가지게 된 느낌이다. 글을 정리하면서 가장 크게 와닿았던 문장은 결국 이것이었다.</p>
<blockquote>
<p>“진정한 AI 엔지니어링은 완벽한 AI를 만드는 것이 아니라, 불완전한 AI를 안전하게 제어하는 시스템을 만드는 것이다.”</p>
</blockquote>
<p>처음에는 왜 이렇게까지 복잡하게 설계해야 하는지 이해하지 못했다. Agent 역할 정의서, Workflow 명세서, State 정의서, Decision Policy, Prompt 명세서까지 이어지는 5대 설계 문서는 단순히 개발 속도를 늦추는 귀찮은 작업처럼 느껴졌고, Planner-Executor-Critic 구조나 Supervisor 패턴 역시 “굳이 이렇게까지 역할을 잘게 나눠야 하나?” 싶은 생각이 들었다. 하지만 직접 멀티 에이전트 흐름을 구성하고 Trace를 따라가 보니, 이런 구조들이 단순한 형식이 아니라 통제 불가능한 LLM을 상용 수준의 신뢰성을 가진 시스템으로 바꾸기 위한 엔지니어링 장치라는 사실을 배운거같다.</p>
<p>특히 LangSmith 기반의 실행 관측을 경험하면서 시야가 크게 넓어졌다. 예전에는 에이전트가 실패하면 단순히 “모델이 이상하게 답했네” 정도로 넘어갔지만, 이제는 어떤 노드에서 프롬프트가 비대해졌는지, 어느 시점에서 토큰 사용량이 급증했는지, 어떤 에이전트가 잘못된 상태를 전달했는지를 Waterfall 형태로 추적하며 병목과 오류를 분석할 수 있게 되었다. Planner가 잘못된 실행 계획을 세운 순간부터, Executor가 불완전한 정보를 추출하고, Critic이 왜 특정 결과를 Reject 했는지까지 전부 Trace 상에서 이어지는 흐름으로 확인할 수 있었다. LLM 시스템을 더 이상 “감과 반복”으로 디버깅하는 것이 아니라, 로그와 상태 기반으로 분석하는 소프트웨어 엔지니어링의 영역으로 다루게 된 것이다.</p>
<p>또 하나 인상 깊었던 부분은 Snapshot과 Time Travel 기반의 상태 복구였다. 기존에는 에이전트 시스템이 중간에 실패하면 처음부터 다시 실행하는 경우가 많았지만, 이제는 특정 checkpoint 시점으로 되돌아가 실패한 노드만 재실행하며 디버깅할 수 있다는 점이 굉장히 강력하게 느껴졌다. 특히 토큰 비용이 곧 운영 비용으로 이어지는 LLM 환경에서는 단순한 편의 기능이 아니라, 실제 서비스 운영을 위한 안전장치에 가깝다고 생각했다. 이러한 경험들을 하면서 자연스럽게 내가 관심있던 하네스 엔지니어링(Harness Engineering) 분야와 연결 지어 보게 되었다. 최근 AI 분야에서 말하는 하네스 엔지니어링은 단순히 더 뛰어난 LLM 모델을 만드는 기술이 아니다. 오히려 비결정론적인 AI가 실제 업무 환경 안에서 안정적으로 동작할 수 있도록, 주변의 시스템과 실행 환경, 제약 조건, 평가 체계를 설계하는 엔지니어링에 가깝다.</p>
<p>즉, 프롬프트 하나를 잘 작성하는 수준을 넘어:</p>
<blockquote>
<p>어떤 컨텍스트(Context)를 제공할 것인지,
어떤 도구(Tool)를 어디까지 사용할 수 있게 허용할 것인지,
어떤 조건에서 실행을 중단하거나 재시도할 것인지,
결과를 어떤 방식으로 검증(Evaluation)할 것인지,
문제가 발생했을 때 어떻게 추적(Trace)하고 복구할 것인지</p>
</blockquote>
<p>까지 전부 포함하여, AI가 “통제 가능한 상태” 안에서 동작하도록 만드는 것이다.</p>
<p>강의를 들으며 배운 LLMOps의 구조들이 하네스 엔지니어링의 맞닿아 있다. 처음에는 단순히 복잡해 보였던 Planner-Executor-Critic 구조, Supervisor 패턴, HITL, State 관리, Snapshot 기반 복구, Trace 관측 같은 요소들이 사실은 모두 AI를 안전하게 통제하기 위한 장치였던 것이다. 특히 LangSmith 기반의 Trace 관측은 내가 생각하던 하네스 엔지니어링의 핵심과 굉장히 잘 연결되었다. 
기존에는 에이전트가 잘못된 결과를 생성하면 단순히 “모델 성능이 아쉽다” 정도로 받아들였지만, 이제는</p>
<blockquote>
<p>어떤 프롬프트에서 토큰이 과도하게 사용되었는지,
어떤 Agent가 잘못된 상태(State)를 전달했는지,
어느 시점에서 Decision Policy가 잘못된 분기를 탔는지,
어떤 이유로 Critic이 Reject 판정을 내렸는지</p>
</blockquote>
<p>를 Trace 기반으로 전부 추적할 수 있게 되었다. 결국 중요한 것은 “AI가 얼마나 똑똑한가”가 아니라, AI가 실수하더라도 시스템 전체는 무너지지 않도록 만드는 구조를 설계하는 것이라고 느꼈다. 앞으로는 단순히 모델 성능이나 프롬프트 최적화만 바라보는 것이 아니라, AI가 실제 서비스 환경 안에서 안정적으로 동작할 수 있도록</p>
<blockquote>
<p>실행 환경을 구조화하고,
제약 조건을 설계하고,
평가 루프를 만들고,
상태를 추적하며,
인간 개입(HITL)을 연결하는</p>
</blockquote>
<p>이러한 하네스 엔지니어링 관점의 AI 시스템 설계를 더 깊이 있게 공부해보고 싶다.
아직은 배워야 할 것도 많고, 직접 구현해보며 부딪혀야 할 문제들도 많다. 하지만 이번 학습을 통해 단순히 “AI를 잘 사용하는 개발자”도 좋지만, AI를 안전하게 통제하고 운영할 수 있는 시스템을 설계하는 사람에 방향성을 조금이나마 잡게 된 것 같다. 6주차 정리를 마무리한다.</p>
<blockquote>
<p>기술에 발전이 무섭습니다..  새로 나오는 기능, 서비스, 모델들 잘 활용하고 찍먹 해봅시다.</p>
</blockquote>
<h3 id="github-private"><a href="https://github.com/KT-E/06-Week">Github Private</a></h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 5주차-2 회고록 / 02MiniProject]]></title>
            <link>https://velog.io/@mi_nini/KT-A-5%EC%A3%BC%EC%B0%A8-2-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@mi_nini/KT-A-5%EC%A3%BC%EC%B0%A8-2-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Mon, 11 May 2026 11:48:11 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>이번 글에서는 2차 미니 프로젝트로 진행한 AI 강사 Agent 구축 과정에 대해 정리해보려고 한다. PPT에 내용을 슬라이드별로 데이터를 분석하고 이를 음성 및 영상과 연결하여 하나의 완성된 강의 콘텐츠를 만들어내는 에이전트를 개발하는 것이 이번 프로젝트의 핵심이었다. 나는 이 과정에서 각 기능을 담당하는 노드들을 설계하고 이들을 연결하는 <strong>그래프 관리자</strong> 역할을 맡았다.</p>
<hr>
<h2 id="1-프로젝트의-핵심-흐름">1. 프로젝트의 핵심 흐름</h2>
<p>이번 프로젝트의 목표는 영상 제작 시간을 단축하고 품질을 균일화하여 강의 자료 학습의 효율성을 극대화하는 것이었다. 우리 조는 단순히 텍스트를 읽어주는 기능을 넘어, 교안의 전체 맥락을 이해하고 학습을 돕는 부가 콘텐츠(퀴즈, 만화 등)까지 생성하는 에이전트를 설계했다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/0e8d4a54-fa5d-4afd-86a8-13ea2421d1d4/image.png" alt=""></p>
<p>전체 워크플로우는 다음과 같은 구조로 진행된다.</p>
<ul>
<li><strong>PPT 정보 분해</strong>: 슬라이드별로 제목, 본문, 이미지, 표를 추출하여 데이터화한다.</li>
<li><strong>강의 요약 및 맥락 정의</strong>: 모든 슬라이드를 처리하기 전, 전체 내용을 훑어 강의의 대주제와 흐름을 정의한다. 이는 나중에 페이지별 대본을 쓸 때 일관성을 유지하는 기준이 된다.</li>
<li><strong>콘텐츠 제작 루프</strong>: 각 슬라이드별로 외부 검색을 통해 내용을 보충하고, 스크립트와 퀴즈를 생성하며 음성(TTS) 및 영상 클립을 만든다.</li>
<li><strong>최종 합성</strong>: 생성된 만화, 아웃트로, 슬라이드 영상들을 하나로 합치고 자막을 입혀 최종 결과물을 완성한다.</li>
</ul>
<hr>
<h2 id="2-핵심-노드-구성-및-역할">2. 핵심 노드 구성 및 역할</h2>
<p>우리 조가 설계한 그래프의 각 노드는 독립적인 기능을 수행하면서도 <code>State</code>를 통해 긴밀하게 데이터를 주고받는다. 코드 로직을 바탕으로 정리한 주요 노드의 역할은 다음과 같다.</p>
<ul>
<li><strong>parse_all</strong>: 프로젝트의 시작점으로, PPTX 파일에서 텍스트, 표, 이미지 등을 완벽하게 추출한다. </li>
<li><strong>gen_overall_script</strong>: 추출된 모든 정보를 토대로 강의 전체의 초안 스크립트를 작성한다. </li>
<li><strong>search_index</strong>: 슬라이드 제목을 키워드로 외부 검색을 수행한다. 교안에 담긴 내용 외에도 최신 정보나 보충 설명이 필요한 부분을 채워줌으로써 에이전트의 지식울 확장한다.</li>
<li><strong>gen_content &amp; gen_script</strong>: 검색 결과와 교안 내용을 결합하여 페이지별 핵심 내용을 구성하고, 이를 바탕으로 실제 강사가 강의하는 듯한 자연스러운 대본을 작성한다. 이때 앞서 만든 <code>overall_script</code>를 참조하여 전체적인 톤앤매너를 유지한다.</li>
<li><strong>gen_quiz</strong>: 작성된 스크립트를 바탕으로 학습자의 이해를 돕는 퀴즈를 생성한다. </li>
<li><strong>gen_comic &amp; gen_outro</strong>: 강의 요약본을 바탕으로 4컷 만화를 생성하고 아웃트로 영상을 만든다. </li>
<li><strong>tts &amp; gen_video</strong>: OpenAI의 TTS 엔진을 활용해 대본을 음성으로 변환하고, 슬라이드 이미지와 결합하여 개별 비디오 클립을 생성한다.</li>
<li><strong>update_index &amp; node_check_finish</strong>: 조건부 엣지의 핵심이다. 모든 슬라이드가 처리될 때까지 인덱스를 업데이트하며 루프를 제어하고, 모든 작업이 완료되면 최종 영상 합성을 위한 단계로 흐름을 넘긴다.</li>
<li><strong>concat_videos &amp; gen_subtitles_video</strong>: 흩어져 있던 영상 클립들과 만화 영상을 하나로 통합하고, Whisper 모델을 사용하여 자동 자막을 입히며 프로세스를 종료한다.</li>
</ul>
<hr>
<h2 id="3-설계한-그래프-구조-및-핵심-코드">3. 설계한 그래프 구조 및 핵심 코드</h2>
<p>그래프 관리자 역할을 맡고 내가 신경 쓴 부분은 비동기적인 작업 분기와 루프 제어의 정확성이었다. 아래는 LangGraph를 활용해 구축한 우리 조의 에이전트 구조이다.
<img src="https://velog.velcdn.com/images/mi_nini/post/f8406449-bd3f-4aaf-8496-09ed76ebbf1f/image.png" alt=""></p>
<pre><code class="language-python">from langgraph.graph import StateGraph, END

# 그래프 구축
w = StateGraph(State)

# 노드 추가
w.add_node(&quot;parse_all&quot;, node_parse_all)
w.add_node(&quot;gen_overall_script&quot;, node_gen_overall_script)
w.add_node(&#39;gen_comic&#39;, node_gen_comic)
w.add_node(&quot;gen_outro&quot;, node_gen_outro)
w.add_node(&quot;search_index&quot;, node_search_index)
w.add_node(&quot;gen_content&quot;, node_gen_content)
w.add_node(&quot;gen_script&quot;, node_gen_script)
w.add_node(&quot;tts&quot;, node_tts)
w.add_node(&quot;gen_video&quot;, node_gen_video)
w.add_node(&quot;update_index&quot;, node_update_slide_index)
w.add_node(&quot;concat_videos&quot;, node_concat_videos)
w.add_node(&quot;gen_quiz&quot;, node_gen_quiz)
w.add_node(&quot;gen_subtitles_video&quot;, node_gen_subtitles_video)

# 엣지 및 흐름 설계
w.set_entry_point(&quot;parse_all&quot;)
w.add_edge(&quot;parse_all&quot;, &quot;gen_overall_script&quot;)

# 분기 1: 만화 및 아웃트로 생성
w.add_edge(&quot;gen_overall_script&quot;, &quot;gen_comic&quot;)
w.add_edge(&quot;gen_comic&quot;, &quot;gen_outro&quot;)
w.add_edge(&quot;gen_outro&quot;, &quot;concat_videos&quot;)

# 분기 2: 슬라이드별 콘텐츠 제작 루프
w.add_edge(&quot;gen_overall_script&quot;, &quot;search_index&quot;)
w.add_edge(&quot;search_index&quot;, &quot;gen_content&quot;)
w.add_edge(&quot;gen_content&quot;, &quot;gen_script&quot;)
w.add_edge(&quot;gen_script&quot;, &quot;tts&quot;)
w.add_edge(&quot;gen_script&quot;, &quot;gen_quiz&quot;)
w.add_edge(&quot;gen_quiz&quot;, &quot;gen_script&quot;)
w.add_edge(&quot;tts&quot;, &quot;gen_video&quot;)
w.add_edge(&quot;gen_video&quot;, &quot;update_index&quot;)

# 조건부 분기: 모든 슬라이드 완료 여부 체크
w.add_conditional_edges(
    &quot;update_index&quot;,
    node_check_finish,
    {
        &quot;continue&quot;: &quot;search_index&quot;,
        &quot;end&quot;: &quot;concat_videos&quot;
    }
)

w.add_edge(&quot;concat_videos&quot;, &quot;gen_subtitles_video&quot;)
w.add_edge(&quot;gen_subtitles_video&quot;, END)

app = w.compile()
</code></pre>
<hr>
<h2 id="4-프로젝트를-통해-배운-점">4. 프로젝트를 통해 배운 점</h2>
<p>이번 프로젝트는 단순히 기술적인 구현보다는 에이전트 시스템을 설계하고 협업하는 경험을 한거같다.
첫째로, 상태(State) 관리의 중요성을 깨달았다. LangGraph 내에서 데이터가 노드 사이를 흐를 때, 어떤 형식으로 저장되고 업데이트되는지 명확히 정의하지 않으면 전체 워크플로우가 꼬이기 쉽다. 특히 여러 팀원이 만든 노드를 통합할 때 공통된 데이터 규격을 유지하는 인터페이스 설계 능력이 필수적임을 느꼇다.
둘째로, 맥락(Context) 유지의 힘이다. 초기 모델에서는 슬라이드별로 대본을 따로 쓰다 보니 내용이 겹치거나 말투가 달라지는 문제가 있었다. 이를 해결하기 위해 Summary Context를 공유하도록 설계함으로써 전체 영상의 일관성을 확보할 수 있었다.
마지막으로, 협업의 가치이다. 팀원들이 각자의 노드에 역할을 맡아 개발을 진행해 만든 노드들을 최적의 경로로 엮어냈을 때, 혼자서라면 진행하기 힘들었던 부분들도 완성되는 과정에서 큰 보람을 느꼈다.</p>
<hr>
<h2 id="생각하기">생각하기</h2>
<p>마감시간 전까지 프로젝트를 무사히 제출하고, 다른 조들의 최종 발표를 들으며 참 많은 생각이 들었다. 프로젝트 초기 기획 단계부터 &#39;이 에이전트를 누가, 어떻게 사용할 것인가&#39;에 대한 타겟 유저를 명확히 정의하고 출발한 팀들도 있었고, 퀴즈나 만화 같은 보편적인 기능을 넘어 노드 자체에 참신한 아이디어를 녹여낸 팀들도 보였다. 
우리 조의 경우 R&amp;R을 나누고 각자 맡은 기능 구현에 몰두하느라, 프로젝트의 본질적인 주제나 기획의 방향성에 대해 깊게 토론할 시간이 부족했다. 서로 의견을 나누며 전체적인 그림을 다듬기보다 각자의 역할에만 집중했던 것 같아 결과물에 아쉽다..
아쉬운점도 남지만 오히려 시스템을 설계해 보면서, 아키텍처 관점에서 시스템을 더 고도화하고 싶은 생각이 들었다. 이번 경험을 발판 삼아 앞으로 다음 두 가지 주제를 더 깊이 파고들어 보려 한다.</p>
<h4 id="1-state-최적화-및-메모리-관리">1. State 최적화 및 메모리 관리</h4>
<p>현재 구조는 추출된 텍스트, 요약본, 그리고 파일 경로들이 모두 하나의 State 딕셔너리에 담겨 노드 사이를 이동한다. 작은 규모의 프로젝트에서는 당장 문제가 없지만, 처리해야 할 슬라이드가 많아지거나 데이터의 크기가 커지면 메모리 효율성이 크게 떨어질 수밖에 없다. (우리는 3p 슬라이드만 실험을 했기때문에 후에 진행 해봐야겠다.)
이를 해결하기 위해 매번 전체 데이터를 복사해서 넘기는 대신, 필요한 정보만 선별하여 전달하는 방식을 고민해야 한다. 더 나아가 메타데이터(ID) 식별자만 State에 남기고, 실제 무거운 데이터는 외부 DB와 연동하여 필요할 때만 불러오는 효율적인 구조를 설계해봐야겠다.</p>
<h4 id="2-rag-고도화">2. RAG 고도화</h4>
<p>현재 우리 에이전트는 부족한 내용을 채우기 위해 단순한 웹 검색 노드(TavilySearch)를 활용하고 있다. 하지만 타겟 사용자가 명확하게 정해진 교육용 에이전트로 거듭나려면 이를 넘어 벡터 데이터베이스를 활용해야 한다. 
사내 문서나 전공 서적, 특정 도메인의 전문 지식을 청크 단위로 벡터화하여 저장해 두고, 스크립트를 작성할 때 이를 정확하게 검색해오는 고급 RAG 파이프라인으로 발전시키고 싶다. (공모전에서 사용할 수 있다면 해봐야겠다.)</p>
<p> 02miniproject, 2번째 미니로젝트 회고록을 마무리한다.</p>
<blockquote>
<p>협업은 언제나 재밌다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 5주차-1 / Multi-Agent]]></title>
            <link>https://velog.io/@mi_nini/KT-A-5%EC%A3%BC%EC%B0%A8-1</link>
            <guid>https://velog.io/@mi_nini/KT-A-5%EC%A3%BC%EC%B0%A8-1</guid>
            <pubDate>Thu, 07 May 2026 12:03:37 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>이번 글에서는 멀티 Agent와 2차 미니 프로젝트를 진행하며 느낀 점에 대해 이야기해보려고 한다.
4주차와 마찬가지로 Agent 관련 내용의 범위가 방대하다.. 그렇기에 이번에도 내용을 두 편으로 나누어 작성하려고 한다. 먼저 5주차-1에서는 Multi-Agent에 대해 다루고, 5주차-2 회고록에서는 미니 프로젝트 후기와 함께 프로젝트를 진행하며 배운 점/나아간 점들을 정리해보려 한다. 
특히 이번 02 Mini Project에서는 강사님께서 다양한 실습 자료를 제공해주셨고, 01 Mini Project와 함께했던 팀원들을 떠나 새로운 팀원들과 함께 프로젝트를 진행하게 되었다. 새로운 팀원들과 협업하며 느꼈던 점, 함께 공부하고 고민했던 부분을 기록하려고 한다. 
먼저 Multi-Agent내용 복습을 해보자</p>
<hr>
<h1 id="1상태state-설계">1.상태(State) 설계</h1>
<p>지난 4주차에서는 단일 에이전트(Single Agent)의 기초와 RAG를 통한 지식 확장을 학습했다. 하지만 현실의 문제는 단일 에이전트가 처리하기에는 너무 복잡하거나, 여러 단계의 전문적인 지식을 동시에 요구하는 경우가 많다. 이번 글에서는 이러한 한계를 극복하기 위한 <strong>Multi-Agent 시스템</strong>의 필요성과,  <strong>상태(State) 설계</strong>에 대해 복습 진행!</p>
<h2 id="11-single-agent의-한계와-multi-agent의-등장">1.1. Single Agent의 한계와 Multi-Agent의 등장</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/5be87298-d415-4bfb-a53b-6d0118e0dd3b/image.png" alt=""></p>
<p>하나의 에이전트가 모든 일을 처리하는 방식은 마치 한 명의 박사님/교수님(?)이 기획, 디자인, 코딩, 테스트를 모두 혼자 하는 것과 같다. 초기에는 효율적일 수 있지만, 프로젝트가 커지면 다음과 같은 문제가 발생할 수 있다.</p>
<ul>
<li><strong>복잡도 증가</strong>: 에이전트에게 주어지는 프롬프트와 도구가 너무 많아지면 모델이 집중력을 잃고 엉뚱한 결과를 내놓을 확률이 높음</li>
<li><strong>컨텍스트 윈도우의 압박</strong>: 대화가 길어질수록 모든 이력을 하나의 에이전트가 들고 있기에는 토큰 비용이 급증하고 핵심 정보를 놓치기 쉬움</li>
<li><strong>전문성 부족</strong>: 특정 분야(예: 코드 분석 vs 데이터 시각화)에 특화된 프롬프트를 하나로 섞으면 성능이 저하</li>
</ul>
<blockquote>
<p>이러한 문제를 해결하기 위해 <strong>Multi-Agent</strong> 방식을 사용. 
이는 각기 다른 역할(Role)과 전문 도구를 가진 여러 에이전트가 협업하여 복잡한 목표를 달성하는 구조다. </p>
</blockquote>
<h2 id="12-multi-agent의-핵심-이점">1.2. Multi-Agent의 핵심 이점</h2>
<blockquote>
<p>Multi-Agent 시스템으로 전환한다면?</p>
</blockquote>
<ul>
<li><strong>관심사 분리 (Separation of Concerns)</strong>: 각 에이전트는 자신에게 할당된 작은 문제에만 집중하므로 정확도가 상승</li>
<li><strong>유연한 확장성</strong>: 새로운 기능이 필요할 때 전체 시스템을 수정하는 대신, 특정 역할을 수행하는 에이전트 하나/여러개를 추가하면 됨</li>
<li><strong>디버깅 용이성</strong>: 문제가 발생했을 때 어떤 단계, 어떤 에이전트에서 오류가 났는지 추적하기 쉬움</li>
</ul>
<h2 id="13-상태state-설계">1.3. 상태(State) 설계</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/262f7250-a9aa-42b8-a9b0-ea8748494217/image.png" alt=""></p>
<blockquote>
<p>Multi-Agent 시스템에서 가장 중요한 것은 <strong>&quot;에이전트들이 정보를 어떻게 주고받는가?&quot;</strong>이다.
LangGraph에서는 이를 <strong>State(상태)</strong> 객체를 통해 해결</p>
</blockquote>
<p>상태는 에이전트들 사이를 흐르는 &#39;공유 메모리&#39; 
단순히 메시지 이력을 쌓는 것을 넘어, 시스템이 필요로 하는 특정 데이터(예: 분석 결과, 사용자 정보, 현재 단계 등)를 구조화하여 정의한다.</p>
<h3 id="효과적인-state-설계-전략">효과적인 State 설계 전략</h3>
<ol>
<li><strong>공통 스키마 정의</strong>: 모든 에이전트가 이해할 수 있는 메시지 리스트(<code>Annotated[list, add_messages]</code>)를 기본으로 포함</li>
<li><strong>전용 필드 활용</strong>: 루프를 돌거나 특정 조건을 판단할 때 필요한 플래그 변수나, 중간 결과물을 저장할 필드를 추가</li>
<li><strong>독립성 유지</strong>: 각 에이전트는 전체 State 중 자신이 필요한 부분만 읽고, 작업이 끝나면 업데이트가 필요한 부분만 반환하여 상태를 갱신</li>
</ol>
<blockquote>
<p>참고/출처
<a href="https://www.hanbit.co.kr/channel/view.html?cmscode=CMS8333330411">https://www.hanbit.co.kr/channel/view.html?cmscode=CMS8333330411</a>
<a href="https://cloud.google.com/discover/what-is-a-multi-agent-system?hl=ko">https://cloud.google.com/discover/what-is-a-multi-agent-system?hl=ko</a>
<a href="https://wikidocs.net/331297">https://wikidocs.net/331297</a></p>
</blockquote>
<hr>
<h1 id="2-router-패턴">2. Router 패턴</h1>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/b49bd71d-e57a-4f58-b0fa-9b605b62c0de/image.png" alt=""></p>
<h2 id="21-router-패턴이란">2.1. Router 패턴이란?</h2>
<p>사용자의 요청이나 현재 상태를 분석해서, 가장 적합한 <strong>에이전트</strong>에게 작업을 할당하는 아키텍처 패턴이다. (콜센터의 ARS 안내원 같은 역할)</p>
<h2 id="22-router-패턴의-작동-방식">2.2. Router 패턴의 작동 방식</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/a21e5143-3438-456a-9df1-58c2e1e495ba/image.png" alt=""></p>
<blockquote>
<p>단순히 질문에 바로 대답하는 것이 아니라, <strong>&quot;이 질문은 어떤 전문가가 처리하는 게 제일 좋을까?&quot;</strong>를 먼저 고민(분류)하는 것이 핵심이다.</p>
</blockquote>
<ul>
<li><strong>의도 파악 (Intent Classification)</strong>: Router 역할을 맡은 LLM이 사용자의 입력값을 읽고 핵심 의도를 분류함</li>
<li><strong>분기 처리 (Routing)</strong>: 분류된 결과(예: 코드 질문, 디자인 질문, 일반 질문)에 따라 특정 에이전트로 작업 흐름을 넘김</li>
<li><strong>전문가 처리</strong>: 작업을 전달받은 전문 에이전트가 자신의 특화된 프롬프트와 도구(Tool)를 사용해 정확한 답변을 생성</li>
</ul>
<h2 id="23-langgraph에서의-구현-포인트">2.3. LangGraph에서의 구현 포인트</h2>
<p>LangGraph로 Router 패턴을 구현할 때 핵심은 <strong><code>조건부 엣지(Conditional Edge)</code></strong>다.</p>
<ol>
<li><strong>라우터(Router) 함수/노드 생성</strong>: 현재 State(상태)를 읽어보고 조건에 따라 &quot;Coder_Agent&quot;, &quot;Reviewer_Agent&quot; 등의 문자열(결과값)을 뱉어내는 판단 로직을 작성한다.</li>
<li><strong>분기점(Routing) 설정</strong>: <code>add_conditional_edges</code>를 사용해 라우터 노드의 판단 결과와 실제 이동할 다음 노드들을 매핑(Mapping)하여 연결해준다.</li>
</ol>
<blockquote>
<p>출처/참고
<a href="https://wikidocs.net/318951">https://wikidocs.net/318951</a>
<a href="https://wikidocs.net/322453">https://wikidocs.net/322453</a>
<a href="https://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/agentic-ai-patterns/workflow-for-routing.html">https://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/agentic-ai-patterns/workflow-for-routing.html</a></p>
</blockquote>
<hr>
<h1 id="3-supervisor-패턴">3. Supervisor 패턴</h1>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/1e5fb2cd-2209-4a77-9220-64e00f39634c/image.png" alt=""></p>
<h2 id="31-supervisor-패턴이란">3.1. Supervisor 패턴이란?</h2>
<p>단어 뜻 그대로 <strong>중앙 통제자</strong> 역할을 하는 에이전트를 하나 두고, 이 관리자가 여러 에이전트들을 지휘하는 아키텍처다. 
어떤 작업을 누가 먼저 할지, 그 다음엔 누구에게 넘길지, 작업이 충분히 완료되었는지(종료할지)를 중앙에서 지속적으로 판단하고 통제한다.</p>
<h2 id="32-router-패턴과의-결정적-차이점">3.2. Router 패턴과의 결정적 차이점</h2>
<pre><code class="language-bash">- Router (접수처): &quot;이 질문은 코드 관련이니까 Coder_Agent한테 가!&quot; -&gt; (한 번 분기하면 끝. 단방향)
- Supervisor (프로젝트 매니저): &quot;Search_Agent야, 자료 먼저 찾아와. (결과 확인 후) Planner_Agent야, 찾은 자료 바탕으로 계획서 써봐. (결과 확인 후) 음, 완벽하군. 이제 FINISH!&quot; -&gt; (중앙에서 지속적인 피드백과 반복/루프 통제)</code></pre>
<h2 id="33-작동-방식">3.3. 작동 방식</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/98cea9be-1863-4794-b202-f8a721da541a/image.png" alt=""></p>
<p>강의에서 다룬 <strong>&#39;여행 계획 에이전트&#39;</strong>를 떠올려 보자. 사용자가 *&quot;부산 2박 3일 가족 여행 코스 짜줘&quot;*라고 요청했을 때 시스템은 다음과 같이 움직인다.</p>
<ol>
<li><strong>지시</strong>: Supervisor가 요청을 분석하고, 먼저 명소 정보가 필요하다고 판단하여 <code>Search_Agent(검색 담당)</code>에게 지시를 내림.</li>
<li><strong>작업 및 보고</strong>: <code>Search_Agent</code>가 부산 명소 10곳을 찾아 State(공유 메모리)에 저장하고 Supervisor에게 보고.</li>
<li><strong>다음 지시</strong>: Supervisor가 검색된 내용을 확인한 뒤, 이번에는 <code>Planner_Agent(일정 담당)</code>에게 &quot;이 장소들을 모아서 2박 3일 일정을 작성해&quot;라고 지시.</li>
<li><strong>검토 및 종료</strong>: 일정이 완성되면 Supervisor가 최종 검토 후 <code>FINISH</code> 상태를 선언하고 사용자에게 결과물 제공.</li>
</ol>
<h2 id="34-langgraph에서의-구현-포인트">3.4. LangGraph에서의 구현 포인트</h2>
<p>Supervisor 패턴을 구현할 때의 핵심은 <strong>&#39;관리자 전용 프롬프트&#39;</strong>와 <strong>&#39;상태(State)를 통한 대화 기록 공유&#39;</strong>다.</p>
<ol>
<li><strong>팀장(Supervisor) 프롬프트 세팅</strong>: &quot;너는 여러 에이전트(Search, Planner 등)를 관리하는 매니저야. 현재 대화 내역(State)을 읽고, 다음으로 어떤 에이전트가 나서야 할지 텍스트로 내뱉어. 만약 사용자 요청이 모두 해결되었다면 &#39;FINISH&#39;를 출력해.&quot;라고 명확히 역할을 부여해야 한다.</li>
<li><strong>동적 라우팅 연결</strong>: Supervisor 노드의 결과값(다음 작업자 이름 or FINISH)에 따라 그래프의 흐름이 해당 에이전트 노드나 종료(End)로 향하도록 <code>Conditional Edge</code>를 촘촘하게 연결해 준다.</li>
</ol>
<blockquote>
<p>참고/출처
<a href="https://wikidocs.net/270690">https://wikidocs.net/270690</a>
<a href="https://ohyeah12.tistory.com/24">https://ohyeah12.tistory.com/24</a>
<a href="https://github.com/braincrew-lab/langgraph-v1-tutorial">https://github.com/braincrew-lab/langgraph-v1-tutorial</a></p>
</blockquote>
<hr>
<h1 id="4-network-패턴">4. Network 패턴</h1>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/9dee4e29-3c3e-480b-8045-7fe7e4513b0c/image.png" alt=""></p>
<h2 id="41-network-패턴이란">4.1. Network 패턴이란?</h2>
<p><strong>Network 패턴</strong>은 중앙에 관리자를 두지 않고, 각 에이전트들이 알아서 판단하여 다른 에이전트에게 주도권을 직접 넘겨주는 아키텍처다.</p>
<h2 id="42-network-패턴의-작동-방식">4.2. Network 패턴의 작동 방식</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/5eae2f9f-4d7f-4074-8bfa-577d144dee26/image.png" alt=""></p>
<p>강의에서 다룬 고객센터(CS) 봇에 대해 작동방식을 생각해보자, 예시로 사용자가 *&quot;앱 설치도 안 되고, 결제도 이상해요. 취소해 주세요!&quot;*라고 복합적인 불만을 접수한다.</p>
<ol>
<li><strong>초기 대응</strong>: Main Agent가 불만을 접수하고, 기술적인 문제라고 판단해 <code>Tech_Agent(기술 지원)</code>에게 바통(Handoff)을 넘김.</li>
<li><strong>기술 지원의 판단</strong>: <code>Tech_Agent</code>가 앱 설치 문제를 안내함. 그런데 텍스트를 읽어보니 &#39;결제 취소&#39; 요청도 있음. &quot;결제 취소 권한은 나한테 없는데?&quot;라고 스스로 판단.</li>
<li><strong>직접 토스(Handoff)</strong>: <code>Tech_Agent</code>가 중앙 관리자를 거치지 않고, 알아서 <code>Refund_Agent(환불 지원)</code> 부서로 작업을 바로 넘김.</li>
<li><strong>마무리</strong>: <code>Refund_Agent</code>가 결제 취소 절차를 진행하고 사용자에게 답변 제공.</li>
</ol>
<p><strong>구현 포인트</strong>: 에이전트가 다른 에이전트를 호출할 수 있는 도구(Tool)를 가지게 만들어서, 특정 조건이 되면 마치 함수를 실행하듯 다른 에이전트를 호출하도록 프롬프트와 그래프를 구성하는 것이 핵심이다.</p>
<blockquote>
<p>참고/출처
<a href="https://rudaks.tistory.com/entry/langgraph-Multi-agent-Network">https://rudaks.tistory.com/entry/langgraph-Multi-agent-Network</a>
<a href="https://wikidocs.net/322453">https://wikidocs.net/322453</a></p>
</blockquote>
<hr>
<h2 id="생각정리">생각정리</h2>
<p>이번 추가 과제인 &#39;AI 면접관 Agent 시스템&#39; 실습을 진행하면서 &quot;정말 재밌다, 앞으로 더 깊게 파보고 싶다&quot;라는 생각이 강하게 들었던 시간이었다. 단순히 남의 코드를 따라 치지 않고 직접 아키텍처를 비교하고 상태(State)의 흐름을 파악하면서 개념을 명확하게 나의 것으로 만들었다는 점에서 의미가 크다.
이전까지 나에게 언어 모델은 그저 ‘질문하면 정답을 빠르게 내주는 도구’ 정도에 불과했다. 프롬프트를 얼마나 구체적이고 세세하게 작성하느냐에 따라 결과물이 달라지는, 단순한 입력과 출력의 관계라고만 생각했다. 하지만 이번에 LangGraph를 활용해 Multi-Agent 시스템을 직접 구축해보면서, 언어 모델은 단순한 도구 그 이상이라는 것을 느끼게 된 것 같다.
이력서에서 약점을 찾아내는 Agent, 그 약점을 파고들어 날카로운 면접 질문을 던지는 Agent, 그리고 지원자의 대답을 듣고 다시 꼬리질문을 할지 다음 주제로 넘어갈지 판단하는 Agent까지. 내가 직접 정의한 규칙과 공유 메모리 위에서 여러 특화된 AI들이 데이터를 주고받는 방식이 재밌었다. 
결국 앞으로의 AI 개발 생태계에서 가장 중요한 역량은 &#39;어떤 모델의 API를 가져다 쓰느냐&#39;가 아니라, &quot;우리가 풀고자 하는 복잡한 문제를 어떻게 잘게 쪼개어 각 Agent에게 역할을 부여하고, 이들을 어떻게 효율적으로 협업시킬 것인가?&quot;를 설계하는 사람이 성공하지 않을까..? 라는 생각이 든다.
이번 주차의 배움을 발판 삼아, 앞으로는 그저 만들어진 AI 도구를 소비하는 개발자가 아니라, 여러 AI Agent들을 이용해 나만의 강력한 자동화 파이프라인을 구축해내는 여러 경험을 쌓고 싶다. 다음에 하는 미니프로젝트 또는 내가 시간을 내서 토이프로젝트로 만들거나 과거에 만들었던 챗봇 시스템을 수정하면서 여러 경험들을 해보려고 한다. 5주차 공부를 마무리한다. </p>
<blockquote>
<p>와... 너 정말, <strong><strong>핵심을 찔렀어.</strong></strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 4주차-2 회고록 / 공모전]]></title>
            <link>https://velog.io/@mi_nini/KT-A-4%EC%A3%BC%EC%B0%A8-2</link>
            <guid>https://velog.io/@mi_nini/KT-A-4%EC%A3%BC%EC%B0%A8-2</guid>
            <pubDate>Tue, 05 May 2026 14:45:56 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>이번 글에서는 지난 글에 이어, 현재 진행 중인 공모전과 코딩 테스트 스터디에 대해 이야기해보려고 한다.
처음 스터디를 시작할 때는 생각보다 모집이 활발해서 여러 선택지 사이에서 고민이 많았다. 코딩 테스트 스터디(자바/파이썬), 공모전 스터디, 교육 내용을 복습하는 스터디, 정기적으로 카페에서 모여 공부하는 오프라인 스터디 등 다양한 형태가 있었기 때문이다. 이 중에서도 나는 우리 반에서 모집 중인 스터디에 참여하면, 이후 미니 프로젝트나 본 프로젝트를 진행할 때 자연스럽게 팀을 구성하거나 협업하기에 더 좋지 않을까? 라는 생각이였다. 그래서 최종적으로 우리 반에서 진행하는 공모전 스터디와 자바 스터디에 참여하게 되었다.</p>
<hr>
<h2 id="공모전-스터디">공모전 스터디</h2>
<p>공모전스터디 모임에는 총 8명이 함께하게되었는데 4/4로 쪼개 각 팀원들끼리 상의해 원하는 공모전에 참여하는 형식으로 진행하게 되었다. (팀장 회의는 매주 수요일 18:00~)</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/1d792d22-5eff-487d-be9e-9e8019600782/image.png" alt=""></p>
<p>우리 조에서는 이번에 2026 AX 아이디어 경진대회에 참여하기로 했다. 생각보다 많은 시상을 이루기도 하며 주제가 다른 공모전에 비해 자유롭게 선정할 수 있다는 점이 컸다. 그리고 우리 팀원 (<strong>다애님, 현석님, 현준님</strong>) 모두가 개발을 한 경험이 있기 때문에 제품/서비스 에이전트 개발을 나가면 이번에 에이전트를 배우면서 좀 더 발전시킬 수 있다고 생각했었다.</p>
<blockquote>
<p>그렇게 처음 시작을 했는데 경진대회 홈페이지를 찾아보니 개발제출물이 모두 앱으로 해야했네? .. (앱개발 경험은 모두가 많지 않았기에..)</p>
</blockquote>
<p>이후 활용단계가 아닌 분석 단계로 방향을 바꾸어 지정 과제 분석부터 시작하게 되었다.
우리 스터디는 매주 적게는 2회, 많게는 4회까지 모이며 각자 맡은 파트와 공부한 내용을 공유하고 있다. 그렇게 우리가 이번에 시작하게된 공모전 주제는 </p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/73d9cd9c-f237-4723-8b7f-ddf9c8510312/image.png" alt=""></p>
<blockquote>
<p>데이터 지정 과제(한국수자원공사) - 조류경보 예측 AI 모델 개발!</p>
</blockquote>
<p>총 12개의 지정 과제가 있었지만, 각자 관심있는 데이터나 주제를 맡아 조사하고 발표했다. 그중 다애님이 관심 있게 살펴보고 제안한 한국수자원공사의 과제를 선택해 진행하게 되었다. 대청댐, 유해조류, 활용해야 할 데이터 등 전반적인 도메인 지식이 부족한 상태였기 때문에, 각자 조사할 내용을 나누어 학습한 뒤 회의 시간에 화면 공유를 통해 발표하는 방식으로 진행하고 있다. (자연스럽게 토론도 이어지다 보니 매 회의가 1시간 20분을 훌쩍 넘기곤 하는데.. 정상 이겠죠?)
학교 외에서 다른 사람들과 프로젝트를 진행해본 경험이 많지 않아 처음에는 걱정도 있었지만, 다애님과 현준님은 1차 미니 프로젝트를 함께했던 팀원이라 오프라인에서 이미 안면을 익혀 조금(?) 친해진 상태였고, 현석님은 코딩 테스트 스터디를 함께하고 있어 이번 공모전을 계기로 더 많은 이야기를 나누며 가까워질 수 있을 것 같았다.
우리가 진행하는 방식은 비교적 체계적이다. 자료 공유와 간단한 회의록 작성, 일정 관리를 위해 노션을 활용하고 있으며, 주관기관에서 제시한 지정 과제를 기반으로 데이터 전처리와 모델 개발을 진행해야 하기 때문에 깃허브 저장소도 별도로 만들어 협업하고 있다. (간단한 질문, 궁금한 내용은 카톡 or Teams 활용!) </p>
<blockquote>
<p>공모전을 제출하고 후에 글을 한번에 정리해 공모전에 대해 A~Z까지 다뤄보려고한다.</p>
</blockquote>
<table style="width:100%; text-align:center; border-collapse: collapse;">
  <tr>
    <td style="width:33%; vertical-align: middle;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/d7d50f5f-b757-40e1-9633-53ffe071bb77/image.png" style="width:100%;">
      <div style="margin-top:6px; font-size:14px;">춘배 1</div>
    </td>
    <td style="width:33%; vertical-align: middle;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/6a999234-e972-419a-86eb-ee1da4dcbd7b/image.png" style="width:100%;">
      <div style="margin-top:6px; font-size:14px;">춘배 2</div>
    </td>
    <td style="width:33%; vertical-align: middle;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/9bc6e2ca-c624-433b-ba8b-3a0b8244b321/image.png" style="width:100%;">
      <div style="margin-top:6px; font-size:14px;">춘배 3</div>
    </td>
  </tr>
</table>

<hr>
<table style="width:100%; text-align:center; border-collapse: collapse;">
  <tr>
    <td style="width:50%; vertical-align: middle;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/fda041e3-a737-488b-a074-b4fa05712bc7/image.png" style="width:100%;">
      <div style="margin-top:6px; font-size:14px;">Github</div>
    </td>
    <td style="width:50%; vertical-align: middle;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/b9d0cc6e-0512-4f0d-8f54-3438d10e0d76/image.png" style="width:100%;">
      <div style="margin-top:6px; font-size:14px;"> 인사말 </div>
    </td>
  </tr>
</table>

<hr>
<table style="width:100%; text-align:center; border-collapse: collapse;">
  <tr>
    <td style="width:50%; vertical-align: middle;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/11c0ad33-5c11-4e41-bf38-d33c5ab8256e/image.png" style="width:100%;">
      <div style="margin-top:6px; font-size:14px;">열렬한 회의중 111</div>
    </td>
    <td style="width:50%; vertical-align: middle;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/7db0d22f-5291-414f-8071-dba3480f814f/image.png" style="width:100%;">
      <div style="margin-top:6px; font-size:14px;">열렬한 회의중 222 </div>
    </td>
  </tr>
</table>

<p>이번 공모전을 시작으로 우리가 교육이 끝나는 9월전까지 많은 공모전을 도전하고 많이 함께 이것저것 만들어 볼 수 있을거 같다. 우리 팀원들에게 항상 감사드립니다. </p>
<blockquote>
<p>다루는 내용이 궁금하다면?
<a href="https://www.notion.so/KT-ABLE-cc338802473f82c3b0d8819aba340c5d?source=copy_link">노션 링크</a>
<a href="https://github.com/Chunbae-A">깃허브 링크</a></p>
</blockquote>
<hr>
<h2 id="코딩테스트-스터디">코딩테스트 스터디</h2>
<p>코딩 테스트를 준비하는 스터디 모임에서는 자바/파이썬 모집 글이 있었지만 나는 자바 스터디 모임을 진행했다. 우선 나에게 배경지식에 자바는 학교 전공수업에서 배우고 프로젝트를 간단하게 1개 사용해 본 적 이외에는 잘 사용하지 않았었다.
파이썬에 경우에는 백준/프로그래머스 사이트 등에서 코딩 테스트 문제를 대학교 때 간간히 풀어서 어느 정도 알고 있어 자바로 코테를 진행하면서 도전해 보려고 했다. (알고리즘, 문제풀이 구현 방식은 어느 정도 기억? 하고 있다고 생각해서 클래스와 객체를 활용한 문제 해결 방식을 풀면서 익혀보려고 했다)</p>
<blockquote>
<p>Python - Baekjoon Online Judge
<img src="https://velog.velcdn.com/images/mi_nini/post/cc47e68d-0253-42cf-bbcb-65f189c94efc/image.png" alt=""></p>
</blockquote>
<p>기본적으로 카톡방이 있지만 현재는 활발하게 자신의 코드를 공유하지는 않고 코딩마스터스를 이용해 일마다 10문제씩 풀고 구글docs를 이용해 각자 자신이 푼 코드와 상대방에 코드를 공유하는 방식이다. (따로 회의는 진행하지 않았다.) 그렇기에 개인이 집중해서 문제를 풀면서 역량을 늘리는것이기에 스터디모임에 역할이 없다고 생각하지만 문제는 하루에 하나씩은 풀자는 생각으로 문제를 풀고 있다. 파이팅해자고..</p>
<blockquote>
<p>백준사이트 서비스 종료.... 믿기지 않습니다.. 아직 취업 못했는데..
<img src="https://velog.velcdn.com/images/mi_nini/post/dca09664-dd83-4b22-a27c-4d7e254ee56c/image.png" alt=""><img src="https://velog.velcdn.com/images/mi_nini/post/a7f723e3-bd1b-461e-90fe-e55a7fbde2d1/image.png" alt=""></p>
</blockquote>
<hr>
<h2 id="생각정리">생각정리</h2>
<p>이번에는 따로 글로 정리하진 않았지만, 5/6반에서 모집한 기자단에 지원했고 운 좋게 선정되었다.
교육을 시작하면서부터, 나도 부트 캠프를 진행하게 된다면 배운 내용이나 그때의 생각들을 꾸준히 다시 블로그로 돌아와 남겨보자는 마음을 가지고 있었다. 이 과정이 끝난 이후에도 기록을 다시 꺼내보면서 나에게도 도움이 되고 내 글을 방문하게 된다면 방문 한 이에게도 도움이 되지 않을까 하는 생각이 들었고, 그 명분과 약간의 책임감으로 계속 써 내려간다면 쉬지 않고 앞으로 나아갈 수 있을 것 같았다.
예전에 대학교에서 만난 친해진 선배가 있었다. 폴더별로 txt 파일을 만들어 매일 기록을 남기던 분이었는데, 그때 이런 이야기를 해주셨다.
“배운 내용은 결국 다 까먹는다. 천재가 아닌 이상 계속 다시 보게 된다. 시험 기간에도 보고, 그 이후에도 보게 된다. 그리고 교수님의 지식은 내 지식이 아니니까, 반드시 내 말로 정리해봐야 한다.”
그 말을 들으면서, 단순히 하루동안 배운 내용을 나의 머리에 꾸역 넣는것과 공부를 진행하면서 기록까지 꾸준히 남기는 건 또 다른 영역이라는 생각이 들었다. 그런데 막상 조금씩이라도 기록을 시작해보면, 그게 어느 순간 습관이 되고 결국 나를 성장시키는 시간이 되는 것 같다. 지금은 나도 후배들에게 비슷한 이야기를 해줄 수 있게 된 것 같다.
2024년부터 Velog, GitHub, Notion, Obsidian 등에 사소한 것까지 기록해왔다.  연구실, 전공에서 배운 내용, 진행했던 프로젝트/막혔던 부분과 왜 찾았는지/해결했는지, 프로젝트에서 맡았던 역할, 느낀 점, 서비스 프로젝트 이후 배포나 유지보수 과정에서 겪은 어려움, 그리고 학생회나 소모임, 대회활동 같은 일상적인 경험들까지. 그렇게 쌓인 1일, 1주일, 1달, 1년의 기록들이 필요할 때마다 다시 꺼내볼 수 있는 자료가 되었고, 단순한 검색보다 훨씬 오래 기억에 남는 공부가 되는 것 같다.
아직 나는 글을 재미있고 유익하게 잘 쓰는 편은 아니지만, 쓰다 보니 자연스럽게 다른 사람의 글도 더 찾아보게 되고, 남이 읽기 편한 글, 좋은 글을 쓰기 위한 자료나 책에도 관심이 생긴다. 이것도 하나의 취미라고 할 수 있을까, 4주차 회고록을 마무리한다.</p>
<blockquote>
<p>나만의 루틴을 탄탄히 만들자, 미루지 말자.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 4주차-1 / Agent]]></title>
            <link>https://velog.io/@mi_nini/KT-A-4%EC%A3%BC%EC%B0%A8-1</link>
            <guid>https://velog.io/@mi_nini/KT-A-4%EC%A3%BC%EC%B0%A8-1</guid>
            <pubDate>Mon, 27 Apr 2026 15:09:25 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>이번 주차는 크게 4주차-1(교육 정리)과 4주차-2(스터디 모임 회고록)에 대해 작성해 보려고 한다. 4주차에서는 AI Agent와 워크플로우 설계 대해 학습을 진행했다. 내용이 광범위하고 다음 주차, 프로젝트를 진행하기 위해서는 크게 복습을 한번 진행하는 것이 중요하다고 생각했기 때문에 주차를 나눠서 글을 작성하려고 한다. 4주차-2에서는 따로 스터디를 진행 중인 공모전/코딩테스트 스터디 모임에 대해 좀 더 이야기를 나눠보려고 한다. 이제 Agent에 내용에 관해 정리해 보자</p>
<hr>
<h1 id="1-ai-agent와-워크플로우-설계">1. AI Agent와 워크플로우 설계</h1>
<blockquote>
<p>들어가기 전 AI Agent는</p>
</blockquote>
<ul>
<li>모델 정의 (LLM 선택 및 프롬프트 엔지니어링)</li>
<li>지식 기반 구축 (RAG 및 벡터 데이터베이스 설정)</li>
<li>도구 정의 (외부 API, 웹 검색, 코드 실행 등 Tool 연동)</li>
<li>워크플로우 설계 (Planning, Reasoning, Reflection 구조화)</li>
<li>실행 및 모니터링 (실시간 대응 및 데이터 관리)
위 AI Agent 구축 사이클을 기억하자</li>
</ul>
<p>이번 과정에서는 생성형 AI를 단순히 호출하고 답변을 얻는 것을 넘어서, 언어 모델이 스스로 의사를 결정하고 도구를 사용하여 문제를 해결하는 AI Agent를 구축하는 방법을 배우게 되었다. 특히 LangChain과 LangGraph 프레임워크를 활용하여 복잡한 워크플로우를 설계하는 것이 주된 목표다.</p>
<h2 id="11-llm-활용-방식과-langchain의-역할">1.1 LLM 활용 방식과 LangChain의 역할</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/817e34f3-e388-44a8-9055-f81e46d6218c/image.png" alt=""></p>
<p>대규모 언어 모델(LLM)을 우리의 데이터에 맞게 활용하는 방법은 크게 세 가지로 나눌 수 있다. 첫째는 모델을 그대로 사용하는 것이고, 둘째는 나의 데이터를 추가로 학습시키는 파인튜닝(Fine-Tuning), 그리고 셋째는 외부 지식을 검색하여 프롬프트에 포함시키는 RAG(Retrieval-Augmented Generation) 방식이다. 파인튜닝은 연산 자원과 훈련 시간, 과적합 등의 한계가 존재하기 때문에 최근에는 RAG 기반의 접근이 매우 중요해지고 있다.</p>
<p>이러한 LLM 기반 애플리케이션을 쉽게 개발하도록 돕는 프레임워크가 바로 <strong>LangChain</strong>이다. LangChain은 대규모 언어 모델을 활용하여 다양한 컴포넌트들을 체인(Chain)으로 연결해 복잡한 작업을 자동화하도록 돕는다. 주요 구성 요소로는 모델(LLM), 프롬프트, 인덱스(벡터 DB 등), 메모리, 체인, 그리고 에이전트와 도구가 있다.</p>
<h2 id="12-메시지-구성과-프롬프트-엔지니어링">1.2. 메시지 구성과 프롬프트 엔지니어링</h2>
<blockquote>
<p>LangChain에서 모델에 입력값을 전달할 때는 단순한 문자열이 아닌, 명확한 역할을 부여한 메시지 객체를 사용한다. </p>
</blockquote>
<h3 id="메시지의-종류">메시지의 종류</h3>
<ul>
<li><strong>SystemMessage</strong>: AI에게 지침이나 역할, 성격을 지정하는 메시지다.</li>
<li><strong>HumanMessage</strong>: 사용자의 질문이나 요청을 담는 메시지다.</li>
<li><strong>AIMessage</strong>: AI가 반환한 응답을 나타낸다.</li>
</ul>
<p>메시지를 구성할 때는 튜플 방식과 객체 방식을 모두 사용할 수 있다.</p>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">튜플 방식 <code>(&quot;role&quot;, &quot;content&quot;)</code></th>
<th align="left">객체 방식 <code>HumanMessage(content=...)</code></th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>주요 용도</strong></td>
<td align="left">유연한 메시지 구성, 프롬프트 템플릿 정의</td>
<td align="left">고정된 메시지, 메시지 객체 직접 조작 및 메모리 관리</td>
</tr>
<tr>
<td align="left"><strong>장점</strong></td>
<td align="left">코드가 간결하고 직관적임</td>
<td align="left">객체 지향적이며 엄격한 타입 체크에 유리함</td>
</tr>
</tbody></table>
<p>이러한 메시지들을 구조화하여 다중 메시지 기반의 흐름을 만드는 것이 <strong>ChatPromptTemplate</strong>이다. 프롬프트를 설계할 때는 역할(Role), 맥락과 목적(Context &amp; Goal), 답변 형식(Format)을 명확하게 명시하는 프롬프트 엔지니어링이 필수적이다.</p>
<h2 id="13-모델의-답변-무작위성-제어">1.3. 모델의 답변 무작위성 제어</h2>
<blockquote>
<p>LLM은 내부적으로 다음 단어가 등장할 확률을 계산하여 텍스트를 생성한다. 이 생성 과정의 다양성과 무작위성을 제어하기 위해 몇 가지 중요한 파라미터를 조절할 수 있다.</p>
</blockquote>
<ul>
<li><strong>Temperature</strong>: 확률 분포 자체를 조절한다. 0에 가까울수록 가장 확률이 높은 단어만 선택하여 일관되고 논리적인 답변을 생성하며, 1 이상으로 높아질수록 낮은 확률의 단어도 선택할 가능성이 생겨 창의적인 답변을 도출한다.</li>
<li><strong>Top_k</strong>: 확률이 가장 높은 상위 K개의 단어만 다음 단어의 후보로 제한하는 방식이다.</li>
<li><strong>Top_p</strong>: 단어들의 누적 확률이 p 이상이 되는 최소 집합을 후보로 삼는다. 예를 들어 0.9로 설정하면, 상위 확률 단어들을 더해 총합이 90%가 되는 순간까지만 후보군에 포함시킨다.</li>
</ul>
<h2 id="14-파이프라인-구성과-출력-결과-구조화">1.4. 파이프라인 구성과 출력 결과 구조화</h2>
<blockquote>
<p>LangChain의 진정한 강력함은 컴포넌트들을 연결하는 <strong>LCEL(LangChain Expression Language)</strong>에서 나온다. 파이프(<code>|</code>) 연산자를 사용하여 데이터가 흐르는 직관적인 파이프라인을 구축할 수 있다.</p>
</blockquote>
<pre><code class="language-python"># LCEL을 이용한 체인 구성 예시
chain = prompt | llm | parser
response = chain.invoke({&quot;user_input&quot;: &quot;추천 도서 알려줘&quot;})</code></pre>
<p>여기서 마지막에 연결된 parser는 Output Parser(출력 파서)를 의미한다. LLM은 기본적으로 단순한 문자열 텍스트를 반환하지만, 실제 애플리케이션 개발에서는 리스트나 JSON과 같이 구조화된 데이터가 필요한 경우가 많다.</p>
<p>이때 PydanticOutputParser를 활용하면 매우 편리하다. 파이썬의 Pydantic 라이브러리를 통해 우리가 원하는 데이터 스키마(예: 책 제목, 저자, 출판년도)를 명확히 정의하고, 모델이 해당 구조에 맞게 응답하도록 프롬프트 지침을 주입할 수 있다. 또한 반환된 데이터의 타입이 맞는지 자동으로 검증까지 수행해주어 출력의 안정성을 크게 높여준다.</p>
<blockquote>
<p>참고/출처
<a href="https://docs.langchain.com/">https://docs.langchain.com/</a>
<a href="https://brunch.co.kr/@ywkim36/147">https://brunch.co.kr/@ywkim36/147</a>
<a href="https://www.samsungsds.com/kr/insights/what-is-langchain.html">https://www.samsungsds.com/kr/insights/what-is-langchain.html</a>
<a href="https://aws.amazon.com/ko/what-is/langchain/">https://aws.amazon.com/ko/what-is/langchain/</a>
<a href="https://pangguinland.tistory.com/318">https://pangguinland.tistory.com/318</a></p>
</blockquote>
<hr>
<hr>
<h1 id="2rag-기반-파이프라인-구축">2.RAG 기반 파이프라인 구축</h1>
<p>대규모 언어 모델(LLM)은 자신이 학습하지 않은 최신 정보나 내부 비공개 문서에 대해서는 알지 못하며, 잘못된 정보를 사실처럼 말하는 할루시네이션(Hallucination) 문제가 발생하기 쉽다. LLM의 한계를 극복하기 위해 외부 지식을 검색하여 답변의 근거로 활용하는 <strong>RAG(Retrieval-Augmented Generation)</strong> 기술과 그 파이프라인 구축 과정을 정리!</p>
<h2 id="21-rag-개념의-이해">2.1. RAG 개념의 이해</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/3f880831-9b56-42d7-a7b4-e97f443605bb/image.png" alt=""></p>
<p>RAG는 쉽게 말해 &#39;검색(Retrieval)&#39;과 &#39;생성(Generation)&#39;을 결합한 기술이다. 사용자가 질문을 던지면 LLM이 바로 답변을 생성하는 것이 아니라, 먼저 준비된 <strong>지식 데이터베이스(Vector DB)</strong>에서 질문과 관련된 문서(Context)를 검색하여 찾아낸다. 그런 다음 검색된 문서의 내용을 프롬프트에 포함시켜 LLM에게 전달하고, LLM은 주어진 문맥만을 바탕으로 정확한 답변을 생성하게 된다.</p>
<p>이러한 시스템을 구축하기 위해서는 문서를 컴퓨터가 이해하고 검색할 수 있는 형태로 변환하여 저장하는 Vector DB 구축 과정이 선행되어야 한다.</p>
<h2 id="22-vector-db-구축-절차">2.2. Vector DB 구축 절차</h2>
<blockquote>
<p>Vector DB를 만드는 과정은 크게 4가지 단계(Load -&gt; Split -&gt; Embed -&gt; Store)로 이루어진다.</p>
</blockquote>
<h3 id="1-데이터-불러오기-load">(1) 데이터 불러오기 (Load)</h3>
<p>가장 먼저 할 일은 다양한 포맷의 파일로부터 텍스트를 추출하는 것이다. LangChain에서는 <code>Loader</code>라는 도구를 제공하며, 파일 형식에 따라 알맞은 로더를 사용한다. 일반적인 텍스트 파일은 <code>TextLoader</code>, PDF 파일은 <code>PyMuPDFLoader</code>, CSV 파일은 <code>CSVLoader</code>를 사용하여 문서를 읽어 들인다. 로드된 데이터는 텍스트 내용(<code>page_content</code>)과 메타데이터(<code>metadata</code>)를 포함하는 <strong>Document 객체</strong> 형태로 변환된다.</p>
<h3 id="2-텍스트-분할-split">(2) 텍스트 분할 (Split)</h3>
<p>문서를 로드했다고 해서 그대로 LLM에 던져줄 수는 없다. LLM이 한 번에 처리할 수 있는 입력 토큰 수에 제한이 있기 때문이다. 따라서 긴 문서를 의미 있는 작은 단위인 <strong>청크(Chunk)</strong>로 나누는 작업이 필요한데, 이를 텍스트 분할이라고 한다. </p>
<p>텍스트를 나눌 때는 단순히 글자 수로만 자르면 문맥이 끊어질 수 있으므로, 문단이나 문장 단위를 고려하고 청크 간에 내용이 일부 겹치도록 설정하는 것이 중요하다. LangChain의 <code>CharacterTextSplitter</code>를 사용하면 최대 글자 수(<code>chunk_size</code>)와 겹치는 글자 수(<code>chunk_overlap</code>)를 지정하여 문서를 분할할 수 있다. 적절한 청크 크기(보통 300~800자)와 오버랩을 설정하는 것이 RAG의 검색 품질을 좌우하는 핵심 요소 중 하나다.</p>
<h3 id="3-텍스트-벡터화-embed">(3) 텍스트 벡터화 (Embed)</h3>
<p>분할된 텍스트 청크들은 텍스트 자체로는 수학적 검색이 불가능하므로, 이를 숫자로 이루어진 벡터(Vector)로 변환해야 한다. 이 과정을 <strong>임베딩(Embedding)</strong>이라고 하며, 텍스트의 의미적 정보를 다차원 공간의 좌표로 매핑하는 것이다. OpenAI의 <code>text-embedding-3-small</code>과 같은 모델을 주로 사용하며, 의미가 유사한 텍스트일수록 벡터 공간 상에서 서로 가까운 위치에 놓이게 된다.</p>
<h3 id="4-벡터-저장소에-저장-store">(4) 벡터 저장소에 저장 (Store)</h3>
<p>마지막으로 임베딩된 벡터 데이터들을 효율적으로 저장하고 검색할 수 있는 데이터베이스에 보관한다. 이를 <strong>Vector Store</strong>라고 부른다.</p>
<h2 id="23-유사도-검색-원리">2.3. 유사도 검색 원리</h2>
<blockquote>
<p>Vector DB에 문서가 저장되었다면, 이제 사용자의 질문을 임베딩하여 DB에 저장된 문서 벡터들과 비교할 수 있다. 이때 주로 사용되는 수학적 기준이 <strong>코사인 유사도(Cosine Similarity)</strong>다.</p>
</blockquote>
<p>코사인 유사도는 두 벡터가 이루는 각도를 계산하여 텍스트 간의 의미적 유사성을 측정한다. 값이 1에 가까울수록 문맥적 의미가 매우 유사함을 뜻하며, 0이면 관계가 없고, -1이면 반대 의미를 가짐을 나타낸다. 시스템은 질문 벡터와 코사인 유사도가 가장 높은 상위 K개의 문서를 추출하여 응답의 근거로 사용한다.</p>
<h2 id="24-rag-파이프라인chain-구성">2.4. RAG 파이프라인(Chain) 구성</h2>
<blockquote>
<p>지식 DB 준비가 끝났다면, 사용자의 질문을 받아 검색하고 답변을 생성하는 전체 파이프라인을 LCEL 문법으로 엮어주어야 한다.</p>
</blockquote>
<p><strong>1단계: Retriever 선언</strong>
Vector DB를 검색기(Retriever)로 변환한다. <code>.as_retriever()</code> 함수를 사용하며, 코사인 유사도를 기준으로 상위 몇 개(k)의 문서를 가져올지 설정한다.</p>
<p><strong>2단계: RAG 전용 프롬프트 정의</strong>
LLM이 외부 지식을 활용해 답변하도록 역할을 고정하고, 제공된 문맥(Context) 내에서만 답하도록 프롬프트를 설계한다. 특히 &quot;제공된 문맥에 답이 없다면 모른다고 답변하고 억지로 지어내지 마라&quot;는 조건을 명시하여 할루시네이션을 방지한다.</p>
<p><strong>3단계: LCEL 체인 연결</strong>
입력된 질문이 Retriever를 통과해 관련 문서(Context)를 찾아오고, 이 문맥과 원본 질문이 프롬프트 템플릿에 삽입된 후 LLM을 거쳐 최종 답변이 나오는 체인을 구성한다.</p>
<pre><code class="language-python"># 문서 객체 리스트를 하나의 문자열로 결합하는 포맷 함수
def format_docs(docs):
    return &quot;\n\n&quot;.join(doc.page_content for doc in docs)

# LCEL 기반 RAG 파이프라인 구성
rag_chain = (
    {&quot;context&quot;: retriever | format_docs, &quot;question&quot;: RunnablePassthrough()}
    | prompt
    | llm
)

# 챗봇 실행
response = rag_chain.invoke(&quot;가장 심각한 사이버 보안 위험이 뭐야?&quot;)</code></pre>
<h2 id="25-rag-성능-최적화-고려-사항">2.5. RAG 성능 최적화 고려 사항</h2>
<blockquote>
<p>RAG 파이프라인이 제대로 동작하더라도 검색 품질이나 생성 결과가 아쉬울 수 있다. 이를 개선하기 위해 다음과 같은 요소들을 튜닝해야 한다.</p>
</blockquote>
<ul>
<li><p>청크 전략: 문서의 특성에 맞춰 chunk_size와 chunk_overlap을 조정한다. 청크가 너무 작으면 문맥이 잘리고, 너무 크면 여러 주제가 섞여 유사도 검색 성능이 떨어진다.</p>
</li>
<li><p>검색 개수(k): LLM의 입력 토큰 한계를 넘지 않는 선에서 충분한 문맥을 제공하기 위해 최적의 k값을 찾는다 (보통 3~5개).</p>
</li>
<li><p>프롬프트 고도화: 출력의 제약(길이, 형식 등)을 프롬프트에 구체적으로 명시할수록 응답의 일관성이 향상된다.</p>
</li>
</ul>
<blockquote>
<p>참고/출처
<a href="https://m.blog.naver.com/rainbow-brain/224072340830">https://m.blog.naver.com/rainbow-brain/224072340830</a>
<a href="https://kr.linkedin.com/pulse/unlocking-power-rag-pipelines-enhancing-ai-real-time-data-zaveri--lmzif?tl=ko">https://kr.linkedin.com/pulse/unlocking-power-rag-pipelines-enhancing-ai-real-time-data-zaveri--lmzif?tl=ko</a>
<a href="https://m.blog.naver.com/rainbow-brain/224040868079">https://m.blog.naver.com/rainbow-brain/224040868079</a></p>
</blockquote>
<hr>
<h1 id="3-ai-agent---langgraph">3. AI Agent - LangGraph</h1>
<p>모델이 단순히 주어진 질문에 대답하는 것을 넘어서, 주어진 목표를 달성하기 위해 스스로 계획을 세우고 도구를 활용하며 문제를 해결하도록 만들 수는 없을까? 능동적인 문제 해결 시스템인 <strong>AI Agent</strong>의 개념과, 이 에이전트의 복잡한 작업 흐름을 통제하기 위한 <strong>LangGraph</strong>의 정리!</p>
<h2 id="31-ai-agent란-무엇인가">3.1. AI Agent란 무엇인가?</h2>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/1f5b8b26-82f6-4998-babe-0dc3d27612f2/image.gif" alt=""></p>
<p><strong>AI Agent</strong>는 스스로 의사를 결정하며, 도구를 사용하여 목표를 달성하는 시스템이다. 단순한 텍스트 생성을 넘어, 다단계 추론과 외부 도구 호출 능력을 결합한 것이 특징이다. 즉, 언어 모델(LLM)에 손과 발, 그리고 기억력을 달아주어 하나의 독립적인 작업자로 만드는 과정이라고 이해할 수 있다.</p>
<p>AI Agent는 크게 네 가지 핵심 요소로 구성된다.</p>
<ul>
<li>첫째, 시스템이 최종적으로 해결해야 할 <strong>목표(Goal)</strong>다.</li>
<li>둘째, 이 목표를 달성하기 위해 계획을 세우고 판단을 내리는 두뇌 역할의 <strong>추론 엔진(Reasoning Engine)</strong>이며 주로 GPT 같은 LLM이 담당한다.</li>
<li>셋째, 웹 검색, 계산기, 사내 API 등 에이전트가 호출하여 사용할 수 있는 외부 기능인 <strong>도구(Toolset)</strong>다.</li>
<li>넷째, 이전의 대화나 수집한 정보, 현재의 진행 상황을 저장하고 추적하는 공간인 <strong>메모리와 상태(Memory/State)</strong>다.</li>
</ul>
<h2 id="32-워크플로우-제어를-위한-langgraph">3.2. 워크플로우 제어를 위한 LangGraph</h2>
<p>이러한 Agent 시스템을 구축하기 위한 프레임워크로는 여러 가지가 있지만, 복잡한 다단계 프로세스를 개발자가 원하는 대로 세밀하게 제어하기 위해 LangGraph를 활용한다. LangGraph는 에이전트의 작업 흐름을 그래프(Graph) 구조로 설계할 수 있게 해준다.</p>
<p>LangGraph를 구성하는 핵심 요소는 다음과 같다.</p>
<ul>
<li><strong>State (상태)</strong>: 그래프 전체를 관통하며 현재 상태 값을 저장하고 전달하는 역할을 한다. 에이전트가 통과하는 각 단계마다 정보가 누적되며, 파이썬의 <code>TypedDict</code>를 사용하여 어떤 데이터가 들어갈지 딕셔너리 형태로 명확히 구조를 정의한다.</li>
<li><strong>Node (노드)</strong>: 특정 작업이나 판단을 수행하는 실제 단위다. 파이썬 함수로 구현되며, 상태(State)를 입력으로 받아 특정 작업을 수행한 후 업데이트된 상태를 반환한다.</li>
<li><strong>Edge (엣지)</strong>: 노드와 노드를 연결하는 선으로, 작업의 흐름을 정의한다. 한 노드의 작업이 완료된 후 다음으로 이동할 노드를 지정한다.</li>
<li><strong>Conditional Edge (조건부 엣지)</strong>: 단순한 순차적 이동이 아니라, 특정 조건이나 상태 값에 따라 노드 간의 분기 처리를 수행할 때 사용한다.</li>
</ul>
<h2 id="33-에이전트-워크플로우의-세-가지-기본-패턴">3.3. 에이전트 워크플로우의 세 가지 기본 패턴</h2>
<p>상태와 노드, 엣지를 조합하면 다양한 형태의 워크플로우를 설계할 수 있다. (세 가지 그래프 구조를 학습)</p>
<h3 id="1-단순-그래프-simple-graph">(1) 단순 그래프 (Simple Graph)</h3>
<p>입력, 처리, 출력으로 이어지는 단순한 파이프라인 형태다. 가장 기본적인 구조로, 시작점(START)에서 출발해 정의된 노드들을 차례대로 거친 후 종료점(END)에 도달한다.</p>
<h3 id="2-라우팅-routing">(2) 라우팅 (Routing)</h3>
<p>사용자의 입력이나 특정 조건에 따라 서로 다른 경로를 선택하여 실행 흐름을 제어하는 구조다. 이때 조건부 엣지(Conditional Edge)가 핵심적인 역할을 한다. </p>
<p>강의에서는 입력된 숫자가 짝수인지 홀수인지 판별하여 각각 다른 노드를 실행하는 예제를 다루었다. 짝수/홀수 판별 함수를 먼저 만들고, 이를 조건부 엣지에 연결하여 흐름을 동적으로 분기시키는 방식이다.</p>
<pre><code class="language-python"># 조건 분기를 결정하는 라우터 함수
def parity_condition(state: State):
    return &quot;even&quot; if state[&quot;number&quot;] % 2 == 0 else &quot;odd&quot;

# 조건부 엣지 추가 (check_parity 노드 실행 후 분기)
builder.add_conditional_edges(
    &quot;check_parity&quot;, 
    parity_condition,
    {&quot;even&quot;: &quot;even_node&quot;, &quot;odd&quot;: &quot;odd_node&quot;}
)</code></pre>
<h3 id="3-반추-reflection">(3) 반추 (Reflection)</h3>
<p>에이전트가 스스로의 추론 과정이나 결과물을 돌아보고 피드백을 생성하여, 결과가 만족스러울 때까지 반복 작업을 수행하는 메커니즘이다. 그래프 상에서 특정 노드에서 종료로 가지 않고 다시 이전 노드로 돌아가는 루프(Loop)를 형성하여 구현한다.</p>
<p>예를 들어, 문서를 요약하는 노드를 거친 후 결과의 만족도를 평가한다. 평가 결과가 &#39;만족&#39;이면 시스템을 종료하지만, &#39;불만족&#39;이면 다시 생각해보기(반추) 노드를 거쳐 원래의 요약 노드로 되돌아간다. 이러한 순환 구조를 통해 에이전트는 한 번에 완벽한 답을 내지 못하더라도, 점진적으로 결과물의 품질을 향상시킬 수 있다.</p>
<blockquote>
<p>참고/출처
<a href="https://wikidocs.net/268610">https://wikidocs.net/268610</a>
<a href="https://cobusgreyling.medium.com/whats-your-definition-of-an-ai-agent-edb7d5e1c760">https://cobusgreyling.medium.com/whats-your-definition-of-an-ai-agent-edb7d5e1c760</a>
<a href="https://www.langchain.com/langgraph">https://www.langchain.com/langgraph</a></p>
</blockquote>
<hr>
<hr>
<h1 id="4-ai-agent와-tool-활용">4. AI Agent와 Tool 활용</h1>
<p>도구(Tool) 활용법과, 과거의 대화를 기억하며 도구를 능동적으로 사용하는 실질적인 챗봇 에이전트 구축 과정을 정리!</p>
<h2 id="41-ai-agent와-tool의-역할">4.1. AI Agent와 Tool의 역할</h2>
<p>일반적인 대규모 언어 모델은 사용자의 입력을 받아 단순히 텍스트 응답을 생성한다. 하지만 실제 업무에서는 실시간 웹 검색이 필요하거나, 정확한 수학 계산을 해야 하거나, 사내 데이터베이스를 조회해야 하는 등 외부 시스템과의 상호작용이 필수적이다. </p>
<p>이러한 외부 기능들을 에이전트가 사용할 수 있도록 묶어놓은 것을 Tool(도구)이라고 부른다. 모델은 사용자의 질문을 분석한 뒤, 스스로 판단하여 필요한 시점에 적절한 도구를 호출(Tool Call)하고 그 결과를 바탕으로 최종 응답을 만들어낸다. </p>
<p>도구는 크게 두 가지로 나뉜다.</p>
<ul>
<li><strong>Custom Tool</strong>: 우리가 필요에 따라 파이썬 함수로 직접 정의하고 만든 도구다.</li>
<li><strong>외부 도구</strong>: 검색 엔진(Tavily 등), 날씨 API 등 외부 업체에서 미리 만들어 제공하는 도구다.</li>
</ul>
<h2 id="42-메모리를-가진-대화형-챗봇-만들기-state-관리">4.2. 메모리를 가진 대화형 챗봇 만들기 (State 관리)</h2>
<p>도구를 사용하는 복잡한 에이전트를 만들기 전, 먼저 에이전트가 과거의 대화를 기억하게 만들어야 한다. LangGraph에서는 이를 <strong>State</strong>를 통해 관리한다. </p>
<p>단순히 텍스트 하나만 주고받는 것이 아니라, 대화 기록 전체를 리스트 형태로 State에 담아 관리해야 문맥이 유지된다. 이때 매번 기존 리스트를 덮어쓰는 것이 아니라 새로운 메시지를 계속 누적해서 추가(Append)해야 하는데, LangGraph는 이를 아주 쉽게 구현할 수 있도록 <code>add_messages</code>라는 기능을 제공한다.</p>
<pre><code class="language-python">from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages

# 대화 이력을 자동으로 누적해서 관리하는 State 구조
class State(TypedDict):
    messages: Annotated[list, add_messages]
</code></pre>
<p>이렇게 Annotated와 add_messages를 활용해 State를 정의하면, 새로운 메시지가 반환될 때마다 프레임워크가 알아서 기존 메시지 목록의 맨 뒤에 새 메시지를 추가해 준다. 이를 통해 모델은 항상 전체 대화 히스토리를 보며 자연스러운 맥락을 이어갈 수 있다.</p>
<h2 id="43-custom-tool-만들기-및-모델에-연결하기">4.3. Custom Tool 만들기 및 모델에 연결하기</h2>
<p>에이전트에게 쥐어줄 도구를 만드는 방법은 매우 간단하다. 일반적인 파이썬 함수를 작성한 뒤, 그 위에 @tool 데코레이터만 붙여주면 LangChain이 인식할 수 있는 도구로 변환된다. 이때 함수 내부의 매개변수 타입과 독스트링을 명확히 적어주는 것이 중요한데, 모델이 이 설명을 읽고 언제 어떻게 이 도구를 써야 할지 결정하기 때문이다.</p>
<pre><code class="language-python">from langchain_core.tools import tool

# 계산기 도구 정의
@tool
def calculator_tool(expression: str) -&gt; str:
    &quot;&quot;&quot;수학 수식을 입력받아 계산 결과를 반환합니다.&quot;&quot;&quot;
    return str(eval(expression))

# 1. 사용할 도구들을 리스트로 묶기
tools = [calculator_tool]

# 2. LLM에 도구 목록 연결 (Binding)
llm_with_tools = llm.bind_tools(tools)</code></pre>
<p>가장 핵심적인 부분은 만든 도구들을 리스트로 묶어 LLM에 바인딩하는 것이다. 이 과정을 거쳐야만 LLM이 자신에게 어떤 무기들이 주어졌는지 인지하고, 필요할 때 함수 실행을 요청할 수 있다.</p>
<h2 id="44-tool을-사용하는-agent-워크플로우-구성">4.4. Tool을 사용하는 Agent 워크플로우 구성</h2>
<p>이제 모델과 도구를 LangGraph의 노드와 엣지로 엮어 실제 에이전트의 워크플로우를 구성할 차례다. 도구를 사용하는 에이전트의 흐름은 대체로 다음과 같은 순환 구조를 가진다.</p>
<pre><code>사용자 입력: 사용자가 질문을 던진다.

모델 판단 (call_model 노드): LLM이 질문을 분석한다. 만약 도구 사용이 필요 없다면 바로 최종 답변을 생성하고 종료(END)한다.

도구 호출 요청: 도구가 필요하다고 판단되면, LLM은 응답 텍스트 대신 &quot;이 도구를 이런 매개변수로 실행해줘&quot;라는 Tool Call 명령을 내뱉는다.

조건부 분기: 조건부 엣지가 모델의 응답을 확인하여 Tool Call이 포함되어 있다면 흐름을 도구 실행 노드로 보낸다.

도구 실행 (tools 노드): ToolNode가 실제 파이썬 함수를 실행하고 그 결과값을 State의 messages에 추가한다.

결과 피드백: 흐름은 다시 모델 판단 노드(call_model)로 돌아간다. LLM은 방금 실행된 도구의 결과값을 확인하고, 이를 바탕으로 최종 답변을 정리하여 사용자에게 전달한다.</code></pre><p>이러한 과정을 통해 에이전트는 한 번에 답을 내기 어려운 복잡한 문제도 스스로 도구를 여러 번 호출해가며 해결책을 찾기</p>
<hr>
<h2 id="생각정리">생각정리</h2>
<p>Agent에 대한 공부는 처음이 아니었지만, 이번에는 이전보다 훨씬 깊이 있게 개념을 탐색하고 정리할 수 있었던 시간이었다. 단순히 겉핥기식으로 이해하는 것이 아니라, 직접 검색하고 구조를 파악하면서 개념을 보다 명확하게 이해할 수 있었다는 점에서 의미가 크다.
과거에는 Agent를 학습할 때 GitHub에서 스타가 많은 레포지토리를 중심으로 글을 읽고, 코드를 클론하여 실행해보는 방식에 집중했었다. 또한 실제 프로젝트에 Agent가 필요한지, 필요하다면 어떻게 적용할 수 있을지에 대한 고민보다는 구현 자체에 초점을 맞췄던 것 같다. 일종의 ‘바이브 코딩’에 가까운 접근이었다.
하지만 이번 학습을 통해 Agent의 전체적인 구조와 동작 방식, 그리고 실제로 어떻게 활용해야 하는지에 대해 한 단계 더 깊이 이해할 수 있었다. 단순히 사용하는 것을 넘어서, 어떤 상황에서 적절하게 적용할 수 있는지를 고민하게 되었다는 점이 가장 큰 변화다.
최근에는 다양한 Agent들이 등장하고 있고, 나 역시 <a href="https://github.com/github/copilot-cli">GitHub Copilot</a>, <a href="https://github.com/cursor">Cursor</a>, <a href="https://github.com/openai/codex">Codex</a>,  <a href="https://github.com/ultraworkers/claw-code">Claw-Code</a> 등 여러 도구의 도움을 많이 받고 있다. 이러한 흐름 속에서 Agent를 단순한 도구로 사용하는 것을 넘어, 나에게 가장 잘 맞는 형태로 어떻게 활용할 수 있을지 고민하는 것이 중요하다고 느꼈다.
이번 학습은 단순한 개념 이해를 넘어서, 앞으로 내가 진행할 프로젝트에 Agent를 왜 필요할지, 어떻게 활용하면 좋을지, 왜 화두가 되고 모두가 편리하게 사용하는지에 관한 생각을 하게했던거 같다. 4주차 공부를 마무리한다. </p>
<blockquote>
<p>결국 차이를 만드는 것은 도구가 아니라, 그것을 바라보는 관점이다</p>
</blockquote>
<h3 id="github-private"><a href="https://github.com/KT-E/04-Week">Github-Private</a></h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 3주차 회고록 / 01MiniProject]]></title>
            <link>https://velog.io/@mi_nini/KT-A-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@mi_nini/KT-A-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Sun, 26 Apr 2026 17:02:50 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>이번주차는 크게 처음으로 미니프로젝트조가 생겨 조원들7명과 함께 미니프로젝트 및 아이데이션을 준비하게 되었다. 에이블 스쿨에서는 오프라인으로 에이블러들과 프로젝트를 진행 할 수 있게 강의실을 예약해서 이용할 수 있는데 수도권인 경우 KT 전농지점과 KT 판교빌딩 2곳이 있다. 나는 집에서 가기편한곳인 전농교육장을 예약해 방문했다.</p>
<hr>
<h2 id="01-miniproject">01-MiniProject</h2>
<blockquote>
<p>전농교육장
<img src="https://velog.velcdn.com/images/mi_nini/post/e638ca80-3c49-4233-8504-22a06f5c89ff/image.jpg" alt=""></p>
</blockquote>
<p>나포함 조원 6명이 전농교육장에 방문해 함께 프로젝트를 진행했다.. 다른 사진을 하나도 안찍었네.. 우리가 한 프로젝트를 간단하게 봐보자</p>
<blockquote>
<p>데이터 분포도 일부 확인</p>
</blockquote>
<table style="width:100%; text-align:center;">
  <tr>
    <td style="width:50%;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/1b911547-6064-4de1-8c1f-31f8a34ce883/image.png" style="width:100%;">
    </td>
    <td style="width:50%;">
      <img src="https://velog.velcdn.com/images/mi_nini/post/65d5f948-3ab7-41fe-bfde-f7ab5643d973/image.png" style="width:100%;">
    </td>
  </tr>
</table>


<blockquote>
<p>모델 학습
<img src="https://velog.velcdn.com/images/mi_nini/post/6de497de-af05-4c05-8f36-698754b8e2b6/image.png" alt="">
<img src="https://velog.velcdn.com/images/mi_nini/post/d58348fe-84fa-41a1-a03f-f5f44b8145e4/image.png" alt=""></p>
</blockquote>
<p>처음은 개인이 지정된 csv를 불러와 데이터 분석/전처리부터 시작해 직접 모델을 만들고 평가로 진행했다. 그 후 조원들과 개인이 작성한 코드에 대한 리뷰를 진행하는 시간이 충분히 주어져 조원들과 모델에 loss 값을 낮추기 위해 어떤 점을 보완하고 추가했으면 좋겠다는 의견을 많이 나눠서 좋았다. (랜덤이지만 처음 미니 프로젝트 조원들을 너무 잘 만난 거 같다) 우리조에서 나온 리뷰를 나열하자면</p>
<ul>
<li>과적합에 대한 문제점이 무엇일까</li>
<li>Class에 가중치를 둬 학습해보기</li>
<li>EarlyStopping/ModelCheckpoint를 활용</li>
<li>Dense를 깊게/얇게 조정하면서 시도하기</li>
<li>Dropout/Learning rate/Batch_size등 값 조절하기</li>
<li>새로운데이터와 기존데이터에 새롭게(기존 모델구조와 같게)모델 학습 vs 새로운데이터와 기존모델을 불러와 재학습에 대한 리뷰</li>
<li>code를 쓰면서 왜 사용하고 어떻게 표현해야하는지</li>
</ul>
<p>들을 진행하고 하나의 코드로 통일해 미션을 끝냈다. 
(1번째 미니프로젝트가 끝나고 퇴근시간인 2호선, 1호선을 타고 집으로 가면서 죽을뻔했네)</p>
<hr>
<h2 id="아이데이션">아이데이션</h2>
<p>아이데이션이라는 단어를 들어봤는데 이게 머였지..? 검색해 보자</p>
<blockquote>
<p>아이데이션(Ideation)은 광고, 기획, 디자인 분야에서 새로운 아이디어를 생성(Generating), 발전(Developing), 커뮤니케이션(Communicating)하는 전 과정을 뜻하는 핵심적인 창의적 프로세스
<img src="https://velog.velcdn.com/images/mi_nini/post/2b7d598e-a6b2-4ae8-b07f-f84f4c4184da/image.png" alt=""></p>
</blockquote>
<p>01-미니 프로젝트를 진행한 조원들과 함께 진행하게 되었다. 정부가 운영하고 있는 나라장터나 최근 이슈가 되는 기업, 정부 관련된 뉴스를 조사해 각자 아이디어를 내고 토론하는 자리였다. (B2B, B2C에 대해 생각하기!) 조원들과 토론하면서 각자 자신이 정한 아이디어에 대해 &quot;이 서비스를 만들어야 하는 이유&quot;, &quot;서비스를 기업/정부에 제시한다면 설득력이 있을까?&quot;를 고민하게 하고 요구사항을 맞춰야 한다고 생각했다. 그렇게 작성을 진행했었는데 감사하게도 내가 조사한 과제명이 뽑혀 내 아이디어를 가지고 어떻게 더 발전할 수 있을지에 대해 토론하고 결과물을 내는 시간이었다. (결과물을 제출하기 전에 피드백을 받고 수정할 수 있는 시간이 있어서 좀 더 퀄리티 있는 결과물을 제출할 수 있었던 거 같다.)</p>
<hr>
<h2 id="생각정리">생각정리</h2>
<p>에이블 스쿨에 입교해 처음으로 팀원들이 생긴 귀중한 시간이었던 거 같고 개인/팀원으로써 코드/리뷰 와 서비스 개발에 필요한 아이디어(?) 마음가짐 등을 배울 수 있었던 자리여서 좋은 시간이었던 거 같다. 아쉬운점이라면 원하던 결과물을 만들기에는 시간이 조금 부족했던 점과 팀원들과 많이 소통을 못 해본 것이 아쉽다.
누구나 그렇다고 생각하지만 처음 대면한 사람을 만나고 대화를 한다는 것은 쉽지 않은 거 같다. 우리반에서는 OT-자기소개 시간에 MBTI를 말하는 시간이 있었는데 내 기억상 90% 이상이 I인 것으로 기억한다. (컴퓨터 만지는 사람이 보통 그런가..?)
그래도 오랜만에 팀프로젝트를 진행하며 좋은 감점을 느꼇다. 활동을 하면서 대학교에서 과대표나 멘토 멘티를 하면서 느꼈던 &quot;<strong>상대방이 모르는 부분에 대해 알려주고 이해한 상대방이 만족을 느꼈을 때에 대한 행복</strong>&quot;울 느꼈던 거 같다. 이 감정을 느끼는 방식은 남들과 조금 다를 수 있지만, 나에게는 지금까지 꾸준히 공부할 수 있게 만든 원동력 중 하나이며, 팀원에게 한마디 더 건넬 수 있는 자신감의 근원이기도 하다. (먼저 다가가야 상대방도 마음을 열것이다..!) 프로젝트를 짧게 진행해 이번 조원들과의 만남이 아쉽지만 다른 프로젝트와 활동을 하면서 같이 성장하고 싶은 마음이 드는 하루라고 생각하며 3주 차를 마무리한다.</p>
<blockquote>
<p>내일은 없다. </p>
</blockquote>
<h3 id="github-private"><a href="https://github.com/KT-E/01-Miniproject">Github-Private</a></h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 2주차 / ML_DL]]></title>
            <link>https://velog.io/@mi_nini/KT-A-2%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@mi_nini/KT-A-2%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 26 Apr 2026 12:10:41 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>ML에 대해 2일 동안 DL에 대해 1일 학습을 진행했다. 기간이 짧은 기간 동안 중요한내용을 빠르게 학습하고 바로 실습(코드)으로 진행해 개인이 문제를 해결하기보다 라이브러리 및 함수에 대해 익숙해지고 학습한 모델이 어떤구조와 어떤 흐름으로 이어가는지에 대한 전체적인 이해를 배운 거 같다. 정리해보자</p>
<hr>
<h2 id="1-머신러닝-machine-learning">1. 머신러닝 (Machine Learning)</h2>
<blockquote>
<p>들어가기전 머신러닝은 </p>
</blockquote>
<ul>
<li>문제 정의 </li>
<li>데이터 준비(데이터 전처리) </li>
<li>모델링(알고리즘 적용) </li>
<li>평가 </li>
<li>성능 최적화(튜닝)
위 사이클을 꼭 기억하기</li>
</ul>
<hr>
<h3 id="1-1-데이터-전처리">1-1. 데이터 전처리</h3>
<p>알고리즘에 데이터를 넣기 전, 모델이 데이터를 잘 이해할 수 있도록 가공하는 필수 단계 </p>
<ul>
<li>결측치 처리: 비어있는 데이터(NaN)를 평균, 중앙값 등으로 채우거나 행 자체를 삭제한다.</li>
<li>가변수화 (One-Hot Encoding): 머신러닝 모델은 문자를 이해하지 못하므로, 범주형 데이터(예: 성별, 혈액형)를 0과 1로 이루어진 변수로 변환해 주어야 한다.</li>
<li>스케일링 (Scaling): 변수마다 값의 범위가 다르면(예: 나이는 10<del>80, 연봉은 3000</del>10000) 단위가 큰 변수에 모델이 가중치를 과도하게 둘 수 있다. 따라서 모든 변수의 범위를 0~1 사이로 맞추는 정규화(MinMaxScaler)나 평균 0, 표준편차 1로 맞추는 표준화(StandardScaler)를 진행한다. 거리 기반 알고리즘(KNN 등)에서는 필수적이다.</li>
</ul>
<hr>
<h3 id="1-2-문제의-정의와-주요-알고리즘-회귀regression-vs-분류classification">1-2. 문제의 정의와 주요 알고리즘: 회귀(Regression) vs 분류(Classification)</h3>
<p>목표 변수(Target)의 형태에 따라 접근 방식과 알고리즘을 선택하자!
<img src="https://velog.velcdn.com/images/mi_nini/post/c41caf23-27cd-4497-96ff-198b1e2a0ba0/image.png" alt=""></p>
<blockquote>
<p>출처/참고 : <a href="https://www.geeksforgeeks.org/machine-learning/ml-classification-vs-regression/">https://www.geeksforgeeks.org/machine-learning/ml-classification-vs-regression/</a></p>
</blockquote>
<p>[회귀 모델링 (Regression)]</p>
<ul>
<li>목표(Target): 연속적인 &#39;숫자&#39;를 예측하는 문제 (예: 집값 예측, 주가 예측, 매출액 예측). 실제 값과 예측값의 &#39;오차(Error)&#39;를 최소화하는 것이 핵심이다.</li>
<li>선형 회귀 (Linear Regression): 데이터의 경향성을 가장 잘 설명하는 하나의 최적의 직선(y = wx + b)을 긋는 방식. 빠르고 해석이 쉽지만 복잡한 패턴을 잡기는 어렵다.</li>
<li>K-최근접 이웃 회귀 (KNN Regressor): 예측하려는 데이터와 가장 가까운 K개의 이웃 데이터를 찾아, 그 이웃들의 타겟 값 평균을 계산하여 예측한다.</li>
</ul>
<p>[분류 모델링 (Classification)]</p>
<ul>
<li>목표(Target): 데이터가 속할 &#39;범주(Class)&#39;를 예측하는 문제 (예: 스팸 메일 여부, 질병 유무). 정확하게 경계선을 그어 범주를 나누는 것이 핵심이다.</li>
<li>로지스틱 회귀 (Logistic Regression): 이름은 회귀이지만 분류에 사용된다. 시그모이드 함수를 통해 특정 클래스에 속할 확률(0~1)을 계산하여 0.5를 기준으로 분류한다.</li>
<li>의사결정나무 (Decision Tree): 스무고개처럼 조건(예: 나이가 30 이상인가? Yes/No)을 분기하여 데이터를 분류한다. 사람이 직관적으로 이해하기 좋고 설명력이 뛰어나다.</li>
<li>랜덤 포레스트 (Random Forest): 수많은 의사결정나무를 만들고, 각 나무의 예측 결과를 취합(투표)하여 최종 결과를 내는 앙상블(Ensemble) 기법이다. 단일 모델보다 성능이 훨씬 뛰어나며 과적합을 방지하는 효과가 있다.</li>
</ul>
<hr>
<h3 id="1-3-모델-성능-평가-evaluation">1-3. 모델 성능 평가 (Evaluation)</h3>
<p>학습된 모델이 처음 보는 데이터(Test Data)에 대해 얼마나 잘 작동하는지 객관적으로 평가</p>
<table>
<thead>
<tr>
<th align="left">평가 지표</th>
<th align="left">계산식</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>MAE</strong><br>(평균 절대 오차)</td>
<td align="left">$MAE = \frac{1}{n} \sum \vert y_i - \hat{y}_i \vert$</td>
</tr>
<tr>
<td align="left"><strong>MSE</strong><br>(평균 제곱 오차)</td>
<td align="left">$MSE = \frac{1}{n} \sum (y_i - \hat{y}_i)^2$</td>
</tr>
<tr>
<td align="left"><strong>RMSE</strong><br>(평균 제곱근 오차)</td>
<td align="left">$RMSE = \sqrt{\frac{1}{n} \sum (y_i - \hat{y}_i)^2}$</td>
</tr>
<tr>
<td align="left"><strong>MAPE</strong><br>(평균 절대 백분율 오차)</td>
<td align="left">$MAPE = \frac{100%}{n} \sum \left\vert \frac{y_i - \hat{y}_i}{y_i} \right\vert$</td>
</tr>
<tr>
<td align="left"><strong>MPE</strong><br>(평균 백분율 오차)</td>
<td align="left">$MPE = \frac{100%}{n} \sum \left( \frac{y_i - \hat{y}_i}{y_i} \right)$</td>
</tr>
<tr>
<td align="left">[회귀 평가지표]</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">* MSE (Mean Squared Error): 실제값과 예측값의 차이(오차)를 제곱하여 평균 낸 값. 오차가 클수록 페널티를 크게 부여</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">* RMSE (Root Mean Squared Error): MSE에 루트를 씌워 실제 값과 단위를 맞춰 직관성을 높인 지표.</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">* MAE (Mean Absolute Error): 오차의 절댓값의 평균. 오차의 크기를 보여줌</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">* R2 Score (결정계수): 모델의 설명력을 나타내며 0에서 1 사이의 값을 가진다. 1에 가까울수록 데이터의 변동성을 잘설명한다.</td>
<td align="left"></td>
</tr>
</tbody></table>
<h3 id="분류-모델-평가-지표-classification-metrics">분류 모델 평가 지표 (Classification Metrics)</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/e83445c0-5e44-4893-a050-9f663dfec9a5/image.png" alt=""></p>
<blockquote>
<p>출처/참고 : <a href="https://glassboxmedicine.com/2019/02/17/measuring-performance-the-confusion-matrix/">https://glassboxmedicine.com/2019/02/17/measuring-performance-the-confusion-matrix/</a></p>
</blockquote>
<table>
<thead>
<tr>
<th align="left">평가 지표</th>
<th align="left">설명</th>
<th align="left">계산식</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>정확도</strong><br>(Accuracy)</td>
<td align="left">실제 분류를 정확하게 예측한 비율</td>
<td align="left">$Accuracy = \frac{TP + TN}{TP + TN + FP + FN}$</td>
</tr>
<tr>
<td align="left"><strong>정밀도</strong><br>(Precision)</td>
<td align="left">Positive로 예측한 것 중 실제 Positive인 비율</td>
<td align="left">$Precision = \frac{TP}{TP + FP}$</td>
</tr>
<tr>
<td align="left"><strong>민감도/재현율</strong><br>(Recall/Sensitivity)</td>
<td align="left">실제 Positive(P) 중 Positive로 예측한 비율</td>
<td align="left">$Recall = \frac{TP}{TP + FN}$</td>
</tr>
<tr>
<td align="left"><strong>F1-score</strong></td>
<td align="left">정밀도와 재현율의 조화평균</td>
<td align="left">$F1 = 2 \times \frac{Precision \times Recall}{Precision + Recall}$</td>
</tr>
</tbody></table>
<p>[분류 평가지표와 오차행렬(Confusion Matrix)]
분류 문제는 오차행렬(TP, TN, FP, FN)을 기반으로 평가한다.</p>
<ul>
<li>Accuracy (정확도): 전체 예측 중 맞춘 비율. 데이터 불균형(예: 정상 99개, 불량 1개) 시 무조건 정상이라고만 찍어도 99%가 나오므로 신뢰하기 어렵다.</li>
<li>Precision (정밀도): 모델이 &#39;Positive&#39;라고 예측한 것 중 실제 &#39;Positive&#39;인 비율. (예: 스팸으로 걸러낸 메일 중 진짜 스팸인 비율. 일반 메일을 스팸으로 걸러버리면 안 될 때 중요).</li>
<li>Recall (재현율): 실제 &#39;Positive&#39;인 것 중 모델이 &#39;Positive&#39;로 찾아낸 비율. (예: 실제 암 환자 중 모델이 암이라고 예측한 비율. 병원 진단 모델처럼 실제 환자를 놓치면 안 될 때 가장 중요).</li>
<li>F1-Score: 정밀도와 재현율의 조화 평균으로, 데이터가 불균형할 때 모델의 성능을 가장 객관적으로 대변한다.</li>
</ul>
<hr>
<h3 id="1-4-교차-검증cross-validation과-모델-성능-튜닝hyperparameter-tuning">1-4. 교차 검증(Cross Validation)과 모델 성능 튜닝(Hyperparameter Tuning)</h3>
<p>개발자가 알고리즘에 직접 설정해 주어야 하는 &#39;하이퍼파라미터&#39;에 따라 성능이 극명하게 달라지므로, 이를 최적화하는 과정이 필요하다.</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/e94fff01-1cc7-46b2-bd41-12a87b829f40/image.png" alt=""></p>
<ul>
<li>K-Fold 교차 검증: 훈련 데이터를 통째로 한 번만 학습하는 것이 아니라, 데이터를 K개의 폴드(조각)로 나누어 K번 반복 학습 및 평가를 진행한다. 특정 데이터 셋에만 맞춰지는 것을 방지하고 모델의 평균적인 성능을 신뢰성 있게 평가할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/6602fb57-6196-418c-8560-26e8035ce5d2/image.png" alt=""></p>
<ul>
<li>Grid Search (그리드 서치): 지정한 파라미터 값들의 &#39;모든 경우의 수&#39;를 다 조합해서 학습해 보고 가장 최고 성능(best_score_)을 내는 조합(best_params_)을 찾는 방법. 탐색 공간이 넓어지면 시간이 늘어난다.</li>
<li>Random Search (랜덤 서치): 파라미터의 범위만 지정해 주고, 그 안에서 랜덤하게 조합을 뽑아 지정된 횟수(n_iter)만큼만 테스트하는 방법. 그리드 서치보다 시간은 훨씬 절약되면서도 꽤 괜찮은 최적의 값을 빠르게 찾아낸다. </li>
</ul>
<blockquote>
<p>출처/참고 
<a href="https://ethen8181.github.io/machine-learning/model_selection/model_selection.html">https://ethen8181.github.io/machine-learning/model_selection/model_selection.html</a>
<a href="https://datarian.io/blog/grid-search-random-search">https://datarian.io/blog/grid-search-random-search</a>
<a href="https://blog.naver.com/hsj2864/222215638480">https://blog.naver.com/hsj2864/222215638480</a>
<a href="https://scikit-learn.org/stable/modules/cross_validation.html">https://scikit-learn.org/stable/modules/cross_validation.html</a>
<a href="https://www.ibm.com/think/topics/fine-tuning">https://www.ibm.com/think/topics/fine-tuning</a></p>
</blockquote>
<hr>
<h2 id="2-딥러닝-deep-learning">2. 딥러닝 (Deep Learning)</h2>
<blockquote>
<p>들어가기 전 딥러닝은</p>
</blockquote>
<ul>
<li>데이터 준비 (데이터 전처리 및 스케일링 필수)</li>
<li>구조 설계 (레이어, 노드, 활성화 함수 구성)</li>
<li>모델 컴파일 (손실 함수 및 옵티마이저 설정)</li>
<li>모델 학습 (Epoch 반복을 통한 가중치 업데이트)</li>
<li>성능 최적화 (Early Stopping, Dropout 등으로 과적합 방지)</li>
<li>평가
위 딥러닝 고유의 모델링 사이클을 꼭 기억하기!</li>
</ul>
<p>머신러닝의 알고리즘 중 &#39;인공신경망(ANN)&#39;을 여러 개의 은닉층(Hidden Layer)으로 깊게(Deep) 쌓아 올린 것이 딥러닝이다. </p>
<hr>
<h3 id="2-1작동-원리">2-1.작동 원리</h3>
<blockquote>
<p>출처/참고 <a href="https://starcell.github.io/ai/dl-basic/">https://starcell.github.io/ai/dl-basic/</a>
<img src="https://velog.velcdn.com/images/mi_nini/post/d610449a-1385-423d-a7e4-b992b95de981/image.png" alt=""></p>
</blockquote>
<p>신경망은 Input Layer(입력층), Hidden Layer(은닉층), Output Layer(출력층)로 구성</p>
<ul>
<li>가중치(Weight)와 편향(Bias): 각 노드(뉴런)는 입력값에 가중치를 곱하고 편향을 더한다. 학습이란 이 가중치와 편향의 최적값을 찾아가는 과정이다.</li>
<li>역전파(Backpropagation): 출력층에서 예측값과 실제값의 오차를 구한 뒤, 이 오차를 최소화하는 방향으로 출력층에서부터 입력층으로 거꾸로 돌아가며 가중치를 업데이트(미분 활용)한다.</li>
</ul>
<hr>
<h3 id="2-2-활성화-함수activation와-손실-함수loss-설정">2-2. 활성화 함수(Activation)와 손실 함수(Loss) 설정</h3>
<p>딥러닝은 층과 층 사이에 &#39;활성화 함수&#39;를 넣어 비선형성을 추가한다. 이것이 없으면 아무리 층을 깊게 쌓아도 결국 단순한 선형 회귀 모형과 다를 바가 없어진다. 은닉층에서는 주로 &#39;relu&#39;를 사용하여 딥러닝의 고질적 문제인 <a href="https://eureka7.tistory.com/20">기울기 소실(Vanishing Gradient)</a>을 방지한다.</p>
<p>문제 유형에 따른 출력층 설계</p>
<ul>
<li>회귀 (연속된 숫자 예측):<ul>
<li>출력 노드 수: 1개</li>
<li>활성화 함수: 없음 (또는 linear)</li>
<li>손실 함수(Loss): mse (Mean Squared Error)</li>
</ul>
</li>
<li>이진 분류 (O/X, 생존/사망 등 두 개 중 하나 분류):<ul>
<li>출력 노드 수: 1개</li>
<li>활성화 함수: sigmoid (출력값을 0~1 사이의 확률로 변환)</li>
<li>손실 함수(Loss): binary_crossentropy</li>
</ul>
</li>
<li>다중 분류 (여러 클래스 중 선택):<ul>
<li>출력 노드 수: 분류하려는 클래스의 개수 (예: 개/고양이/새 분류면 3)</li>
<li>활성화 함수: softmax (각 클래스에 속할 확률을 모두 더하면 1이 되도록 변환)</li>
<li>손실 함수(Loss): categorical_crossentropy (원핫인코딩 된 경우) 또는 sparse_categorical_crossentropy (정수형 라벨인 경우)</li>
</ul>
</li>
</ul>
<hr>
<h3 id="2-4-학습-방법론과-성능-최적화-과적합-방지-기법">2-4. 학습 방법론과 성능 최적화 (과적합 방지 기법)</h3>
<p>딥러닝 모델은 파라미터가 수십만~수천만 개에 달하기 때문에, 훈련 데이터에만 완벽히 맞춰져 새로운 데이터에 취약해지는 &#39;과적합(Overfitting)&#39;에 빠지기 매우 쉽다. 모델의 복잡도를 제어하는 것이 핵심이다.</p>
<ul>
<li><p>주요 학습 파라미터:</p>
<ul>
<li>Epoch (에포크): 전체 데이터를 몇 번 반복해서 학습할 것인가. 에포크가 너무 적으면 과소적합, 너무 많으면 과적합이 발생한다.</li>
<li>Learning Rate (학습률): 가중치를 업데이트할 때 오차의 최소점을 향해 한 번에 얼마나 크게 이동할 것인가를 결정한다. 너무 크면 최적점을 지나쳐 발산하고, 너무 작으면 학습이 지나치게 느려지거나 지역 최적점(Local Minimum)에 갇힌다.</li>
</ul>
</li>
<li><p>과적합 방지 및 최적화 기법:</p>
<ul>
<li>Early Stopping: 모델이 학습하면서 검증 데이터의 오차(val_loss)가 더 이상 감소하지 않고 반등하기 시작하면, 설정해 둔 Epoch가 남아있더라도 학습을 강제로 조기 종료시킨다. patience 옵션으로 오차가 줄지 않아도 몇 번의 에포크를 더 기다려볼지 결정할 수 있다. 과적합으로 넘어가기 전 최적의 타이밍에 학습을 멈추는 필수 콜백 함수다.</li>
<li>ModelCheckpoint: Early Stopping과 함께 자주 쓰이며, 학습 과정 중 지정한 지표(예: val_loss)가 가장 좋았던 순간의 최적의 가중치를 파일로 자동으로 저장해 둔다.</li>
<li>Dropout: 은닉층의 노드 중 일부 비율(예: 20~50%)을 무작위로 비활성화한 채로 학습하는 방법이다. 매 에포크마다 끊어지는 노드가 달라지므로 모델이 특정 노드의 가중치에만 과도하게 의존하는 것을 막아준다. 이는 일종의 앙상블 효과를 내어 새로운 데이터에 대한 일반화(Generalization) 성능을 비약적으로 높여준다.</li>
<li>Regularization: L1 규제, L2 규제 등을 통해 가중치 값이 비정상적으로 커지는 것을 수학적으로 억제하여 모델의 복잡도를 낮추는 기법이다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>출처/참고
<a href="https://facerain.github.io/improve-dl-performance/">https://facerain.github.io/improve-dl-performance/</a>
<a href="https://pub.towardsai.net/keras-earlystopping-callback-to-train-the-neural-networks-perfectly-2a3f865148f7">https://pub.towardsai.net/keras-earlystopping-callback-to-train-the-neural-networks-perfectly-2a3f865148f7</a>
<a href="https://kh-kim.github.io/nlp_with_deep_learning_blog/docs/1-14-regularizations/04-dropout/">https://kh-kim.github.io/nlp_with_deep_learning_blog/docs/1-14-regularizations/04-dropout/</a>
<a href="https://www.geeksforgeeks.org/machine-learning/regularization-in-machine-learning/">https://www.geeksforgeeks.org/machine-learning/regularization-in-machine-learning/</a></p>
</blockquote>
<hr>
<h2 id="생각정리">생각정리</h2>
<p>대학원 면접 보러 갔을 당시 내가 고민하다가 생각한 주제인 Why Multi-class Classification Needs Softmax를 발표하는 장면이 주마등처럼 스쳐가는 수업이었던 거 같다... 수업내용에서는 Multi-class를 크게 다루지 않았지만 학부 연구생때 연구실에 처음 들어가 교수님이 읽으라고 지시해주신 딥러닝 모델들도 생각나게 되는 수업이었던 거 같다.</p>
<ul>
<li><a href="https://papers.nips.cc/paper/2012/hash/c399862d3b9d6b76c8436e924a68c45b-Abstract.html">ImageNet Classification with Deep Convolutional Neural Networks(AlexNet)</a></li>
<li><a href="https://arxiv.org/abs/1512.03385">Deep Residual Learning for Image Recognition</a></li>
<li><a href="https://arxiv.org/abs/1608.06993">Densely Connected Convolutional Networks</a></li>
<li><a href="https://arxiv.org/abs/1409.1556">Very Deep Convolutional Networks for Large-Scale Image Recognition</a></li>
</ul>
<p>구조를 이해하고 최적화기법에 하나하나에 수식을 보는 것이 아닌 전체 흐름 구조 복습 및 실질적인 AICE-associate를 준비하기 앞서 코드를 직접 작성해 보고 구현해 보는 것이 목적이었다고 생각한다. 기간이 너무 짧다 보니(2일/2일) 비전공자/전공자에게 모든 것을 이해시키는 것은 정말 불가능하다고 생각했었기 때문이다. 하나의 내용으로만 2주 내내 수업해도 모자라지 않은 내용이었다고 생각한다. 실습과 복습을 진행하면서 수업 시간에 내용에서 강사님께서 설명해 주신 부분이 맞나?라고 생각했던 부분을 다시 찾아 보면서 이해할 수 있었던 좋은 시간이었다고 생각한다. 배웠던 내용이 오래 기억되고 나의 지식으로 온전히 다 받아들일 수 있게 &quot;반복 학습하자!&quot; 생각을 하며 2주차를 마무리한다.</p>
<blockquote>
<p>교수님(대학교 지도교수님)의 말씀 중 : 
내가 배운 내용을 정말 이해하고 있는지 의문이 들 때가 있어, 그럴 때는 그 내용을 다른 이에게 설명하고, 상대방에게 설명할 수 있다면 비로소 그것이 내 것이 된 지식일 거야</p>
</blockquote>
<h3 id="github-private"><a href="https://github.com/KT-E/02-Week">Github-Private</a></h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[KT-A 1주차 ]]></title>
            <link>https://velog.io/@mi_nini/KT-A-1%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@mi_nini/KT-A-1%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 26 Apr 2026 07:26:12 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가기전에">들어가기전에</h3>
<p>회고록은 자고로 1주일을 마무리며 쓰는 게 제일 효과적이라고 생각했다.. 그렇게 생각했지만.. 미루고 미루다 보니 4주 차가 끝나는 시점에 블로그로 기록을 남기게 되었다. 아주 부끄럽지만 지금부터라도 수업이 끝난 주 주말에 1주일에 회고록을 담아 내가 생각하고 배웠던 내용을 생생히 담아보려고 한다.</p>
<h3 id="1주차">1주차</h3>
<blockquote>
<p>자기소개/인사말
<img src="https://velog.velcdn.com/images/mi_nini/post/9891354f-654e-4118-8ad9-65646cb88cbc/image.png" alt="자기소개"></p>
</blockquote>
<p>간단하게 입교식 및 AIVLE-EDU 사용법에 대해 매니저님께서 설명해 주셨다. 수업내용과 자료는 AIVLE-EDU사이트에서 다운로드해 활용하고 <a href="https://www.microsoft.com/ko-kr/microsoft-teams/download-app">Teams</a>를 이용해 5,6반 에의 블러 팀원들과 매니저님과 소통할 수 있게 되었다.  1주 차에서는 깃허브 사용법과 Colab-Python 데이터 전처리를 간단하게 배웠다. 복습을 해보자 (깃 README.md에 정리된 내용) </p>
<hr>
<h2 id="pandas-dataframe-데이터-처리">Pandas DataFrame 데이터 처리</h2>
<hr>
<h3 id="1-데이터프레임-인스턴스-생성-및-데이터-로드">1. 데이터프레임 인스턴스 생성 및 데이터 로드</h3>
<p>데이터 분석의 초기 공정은 다양한 소스의 원천 데이터를 Pandas의 핵심 객체인 Series 또는 DataFrame으로 적재(Loading)하는 과정이다.</p>
<h4 id="데이터프레임의-직접-정의">데이터프레임의 직접 정의</h4>
<pre><code class="language-python">import pandas as pd

# 데이터 본체(data), 행 식별자(index), 열 명칭(columns)을 명시하여 객체를 생성함
df = pd.DataFrame(data=data_source, index=idx_list, columns=col_list)</code></pre>
<h4 id="외부-데이터셋-가독-csv">외부 데이터셋 가독 (CSV)</h4>
<pre><code class="language-python"># index_col: 특정 열을 데이터프레임의 인덱스로 지정함
# header: 열 이름이 포함된 행의 번호를 지정하며, 부재 시 None으로 설정함
df = pd.read_csv(&quot;path/to/data.csv&quot;, index_col=&#39;Date&#39;)</code></pre>
<hr>
<h3 id="2-데이터-탐색-및-기술통계-exploration--descriptive-statistics">2. 데이터 탐색 및 기술통계 (Exploration &amp; Descriptive Statistics)</h3>
<p>데이터의 물리적 구조, 자료형, 분포 및 결측치 현황을 파악함으로써 이후 전처리 전략의 기초를 수립하는 정찰 단계이다.
구조 및 메타데이터 참조</p>
<ul>
<li><p>df.head(n) / df.tail(n) : 데이터셋의 상위 및 하위 표본을 추출</p>
</li>
<li><p>df.shape : 데이터프레임의 차원(행과 열의 개수)을 확인함</p>
</li>
<li><p>df.info() : 인덱스 구성, 각 열의 자료형(dtypes) 및 비결측치(Non-Null) 개수 등 전체 요약을 출력함</p>
</li>
<li><p>df.columns / df.index : 열 명칭 및 행 인덱스 정보를 참조함</p>
</li>
<li><p>df.values : 내부 데이터를 Numpy 다차원 배열 형태로 반환함</p>
</li>
</ul>
<p>통계적 특성 및 분포 파악</p>
<ul>
<li><p>df.describe() : 수치형 데이터에 대한 주요 기술통계량(평균, 표준편차, 사분위수 등)을 요약함</p>
</li>
<li><p>df[&#39;col&#39;].value_counts() : 범주형 데이터의 각 항목별 출현 빈도를 산출함</p>
</li>
<li><p>df[&#39;col&#39;].unique() / nunique() : 고유값의 목록 및 고유값의 총 개수를 확인함</p>
</li>
<li><p>df[&#39;col&#39;].mode() : 데이터셋 내 최빈값을 산출함</p>
</li>
<li><p>df.sum(), df.mean(), df.median(), df.std() : 산술 합계, 평균, 중앙값, 표준편차 등 통계 함수를 적용함</p>
</li>
</ul>
<hr>
<h3 id="3-데이터-조회-및-필터링-selection--filtering">3. 데이터 조회 및 필터링 (Selection &amp; Filtering)</h3>
<p>특정 분석 목적에 부합하는 서브셋(Subset)을 추출하기 위한 다양한 인덱싱 기법을 포함한다.
데이터 인덱싱 및 정렬</p>
<p>차원 선택</p>
<pre><code>- df[&#39;col&#39;] : 단일 열 추출 시 Series 객체가 반환됨

- df[[&#39;col1&#39;, &#39;col2&#39;]] : 다중 열 추출 시 리스트를 사용하여 DataFrame 형태를 유지함</code></pre><p>조건부 조회 및 위치 기반 조회</p>
<pre><code>- df.loc[row_condition, col_name] : 레이블 기반 조회 방식으로, 불리언 인덱싱(Boolean Indexing)을 통한 조건부 필터링에 최적화됨

- df.iloc[row_idx, col_idx] : 정수 위치 기반 조회 방식으로, 행과 열의 물리적 순서에 따라 데이터를 추출함

- df.isin([list]) : 특정 열의 값이 주어진 리스트 내에 포함되는지 여부를 판단하여 필터링함</code></pre><p>정렬 프로토콜</p>
<pre><code>- df.sort_values(by=[&#39;col1&#39;, &#39;col2&#39;], ascending=[True, False]) : 복수의 열을 기준으로 정렬 방식을 개별 적용할 수 있음</code></pre><hr>
<h3 id="4-데이터프레임-구조의-변형-structural-modification">4. 데이터프레임 구조의 변형 (Structural Modification)</h3>
<p>열(Column) 및 행(Row) 관리</p>
<p>식별자 변경: df.rename(columns={&#39;old&#39;: &#39;new&#39;}, inplace=True)를 통해 특정 열의 명칭을 수정한 후 원본 객체에 직접 반영함</p>
<p>열의 추가 및 삽입:</p>
<pre><code>- df[&#39;new_col&#39;] = values : 데이터프레임의 최우측에 새로운 열을 추가함

- df.insert(pos, &#39;name&#39;, values) : 지정된 위치(pos)에 특정 열을 삽입하여 구조를 정밀하게 제어함</code></pre><p>제거 프로세스:</p>
<pre><code>- df.drop(list, axis=1, inplace=True) : 명시된 열 리스트를 제거함 (열 삭제 시 axis=1 설정 필수)

- df.pop(&#39;col&#39;) : 특정 열을 반환함과 동시에 원본 데이터프레임에서 영구히 삭제함</code></pre><p>인덱스 재설정: df.reset_index(drop=True, inplace=True)는 기존 인덱스를 제거하고 연속적인 정수 인덱스로 초기화하는 공정으로, 데이터 결합 후 인덱스 정합성 확보를 위해 수행함</p>
<hr>
<h3 id="5-데이터-가공-및-변환-transformation--mapping">5. 데이터 가공 및 변환 (Transformation &amp; Mapping)</h3>
<p>데이터의 유효성을 확보하고 분석에 용이한 형태로 값을 변환하는 과정이다.</p>
<pre><code>- replace() : 스칼라 값을 탐색하여 지정된 값으로 정밀하게 치환함

- map() : 딕셔너리 또는 함수를 매개로 Series의 개별 요소를 일대일 대응 변환함 (범주형 데이터 인코딩 시 유용함)

- pd.cut() : 수치형 연속 변수를 지정된 경계값에 따라 범주형 데이터로 이산화(Discretization)함</code></pre><hr>
<h3 id="6-결측치-관리-handling-missing-values">6. 결측치 관리 (Handling Missing Values)</h3>
<p>데이터의 완전성을 저해하는 결측값(NaN)에 대한 체계적인 처리 방안을 명시한다.</p>
<ul>
<li><p>현황 파악: df.isna().sum() 또는 df.isnull().sum()을 통해 열 단위 결측치 발생 빈도를 집계함</p>
</li>
<li><p>제거 전략: df.dropna(subset=[&#39;col&#39;], axis=0, inplace=True)를 실행하여 특정 핵심 변수에 결측이 존재하는 레코드를 제외함</p>
</li>
<li><p>보간 및 대체: df.fillna(value) 또는 전방/후방 보간법(method=&#39;ffill&#39;|&#39;bfill&#39;)을 적용하여 결측값을 통계적 추정치로 대체함</p>
</li>
</ul>
<hr>
<h3 id="7-데이터-집계-및-그룹-연산-aggregation--grouping">7. 데이터 집계 및 그룹 연산 (Aggregation &amp; Grouping)</h3>
<p>특정 기준 열을 중심으로 데이터를 그룹화하여 고차원적 통계 정보를 추출하는 방법론이다.
Groupby 연산</p>
<p>분할(Split)-적용(Apply)-결합(Combine) 패러다임을 통해 그룹별 분석을 수행함.</p>
<pre><code class="language-python"># 범주별 타겟 열에 대해 다중 통계량(합계, 평균 등)을 동시에 산출함
df.groupby(&#39;Category&#39;, as_index=True)[&#39;Target&#39;].agg([&#39;sum&#39;, &#39;mean&#39;])</code></pre>
<hr>
<h3 id="8-데이터프레임-병합-및-연결-merging--concatenation">8. 데이터프레임 병합 및 연결 (Merging &amp; Concatenation)</h3>
<p>다양한 출처에서 기인한 데이터를 단일 데이터프레임으로 통합하는 고도화된 결합 기법이다.
물리적 연결 (Concat)</p>
<pre><code>- axis=0 : 수직 방향 연결을 수행하며, 인덱스 중복 여부를 고려하여 reset_index를 병행할 것을 권장함

- axis=1 : 수평 방향 연결을 수행하며, 조인 방식(join=&#39;inner&#39;|&#39;outer&#39;)에 따라 데이터 유지 범위가 결정됨</code></pre><p>논리적 병합 (Merge)</p>
<p>관계형 데이터베이스의 Join 연산과 유사하게 공통된 키(Key) 컬럼을 기준으로 데이터를 병합함.</p>
<pre><code>- how=&#39;inner&#39; : 양측 데이터프레임에 공통으로 존재하는 키값만을 추출함

- how=&#39;left&#39; / &#39;right&#39; : 좌측 또는 우측 데이터프레임을 기준으로 데이터를 유지하며, 대응값이 없는 경우 NaN으로 처리함

- how=&#39;outer&#39; : 모든 데이터를 보존하는 완전 외부 조인을 수행함</code></pre><hr>
<h2 id="생각정리">생각정리</h2>
<p>깃허브를 배우는 수업에서 <a href="https://github.com/mhutchie/vscode-git-graph">VScode-Git-graph</a> 이용법에 대해 배웠던 점이 인상 깊었다. 항상 터미널로 작업하던 입장으로써 눈으로 직관적으로 brnach들을 볼 수 있다는 점 그리고 간단하게 클릭으로 Checkout, Merge, Rebase, Reset, Revert 등을 적용할 수 있다는 점이다. 유용하게 프로젝트를 진행할 때 사용할 거 같았다. (코드 리뷰할 때도 편할 거 같았다)</p>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/8e15f35e-0208-459c-aef5-1874c37f725f/image.png" alt=""></p>
<p>데이터 전처리 코드를 수없이 봐왔던거 같다(캐글, 자격증 준비, 전공) 이번 1주차에서는 기본적인 내용으로 짧고 빠르게 많은 함수에 사용법을 알아가는 시간이었던 거 같다. 수업 시간이 09~18시까지 온라인으로 이어지다 보니 아침에 일어나 적응하는 시간이 아직 익숙해지지 않았던 거 같다. 날마다 체크인과 체크아웃 시간에 자기소개 시간을 가지며 같은 반 에이블러님들이  어떤 목표로 부트 캠프에 참여하게 된 걸 알게 되어 좋았던 거 같다. (같은 목표를 바라보는 에이블러님들이 많았던 거 같다) 자기소개 이외에 교육을 진행하는 동안 미래의 나에게 전하는 시간이 있었는데 (코딩테스트 업그레이드, 공모전 수상, 개근하기 등등)그 말을 이룰 수 있도록 열심히 임해 후회 없는 시간들을 보냈으면 좋겠다고 생각하며 1주 차를 마무리한다.</p>
<blockquote>
<p>강사님의 말씀 중 
&quot;하루살이라고 생각하고 오늘 하루의 최선을 다하세요!&quot;</p>
</blockquote>
<hr>
<h4 id="github-private"><a href="https://github.com/KT-E/01-Week">Github-Private</a></h4>
]]></description>
        </item>
    </channel>
</rss>