<?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, 22 Jun 2026 09:54:06 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[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>
        <item>
            <title><![CDATA[KT-AIVLE]]></title>
            <link>https://velog.io/@mi_nini/KT-AIVLE</link>
            <guid>https://velog.io/@mi_nini/KT-AIVLE</guid>
            <pubDate>Sun, 26 Apr 2026 07:09:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mi_nini/post/a833f549-89c6-4ad9-9a9e-297cb0f7c25f/image.png" alt=""></p>
<h3 id="들어가며">들어가며</h3>
<p>이번 글은 늦게나마 시작하게 된 에이블스쿨 합격 후기 글이자 에이블 스쿨에 회고록/배운내용정리를 쓰게 될 첫 글이다. 방학 동안 개인적으로 코딩 테스트 공부와 기업들에 채용공고들을 보며 나에게 부족한 점과 기업이 원하는 개발 언어, 툴, 경험을 쌓는 데에 초점을 두고 2026년에 계획을 세우고 있었다. 
<a href="https://aivle.kt.co.kr/home/main/indexMain">KT-에이블 스쿨</a>에 모집 글은 학교 내 포스터와 KT 디지털 인재 장학생을 마치며 단체 톡 방에도 모집 글이 올라왔었다. 나는 개발자 부트캠프에 참여했던 여러 선배들에 이야기를 듣기도 하고 구글링을 하며 XX부트캠프 1주차 회고록이라는 여러글도 봤었는데, 여러 부트 캠프 중 기업에 이름을 빌려 하는 부트 캠프들  - ( 삼성 SSAFY, 네이버 부스트 캠프, 우테코, LG Aimers, 카카오 테크 캠퍼스, 현대모비스 SW 아카데미, 포스코 AI · Big Data 아카데미 등등) 중 수료 후 KT 취업연계 지원과 우수 수료생일 경우 채용 지원 시 우대가 된다는 점, 그리고 과거 에이블 기수 후기에 코딩 테스트를 함께 리뷰하며 많이 성장한 후기, 여러 스터디 모임이 활성화되어 있다는 글을 보고 좋은 사람들과 함께 성장할 수 있는 좋은 기회라고 생각해 9기 모집에 지원했다. (글쓴이도 현재 공모전/코딩 테스트 스터디 모임에 활발히 참여 중입니다! 나중에 기록 남길게요..)</p>
<hr>
<h2 id="9기-지원-절차">9기 지원 절차</h2>
<table>
  <tr>
    <td><img src="https://velog.velcdn.com/images/mi_nini/post/81c08ea2-20a0-45a5-9c0f-6f7e0c9ece6e/image.png"/></td>
    <td><img src="https://velog.velcdn.com/images/mi_nini/post/c013aeda-ba48-43ea-bf3a-2ab089da2c3e/image.png"/></td>
  </tr>
</table>

<p>위 사진에서 보이듯 모집분야는 크게 AI 개발자(트랙)과 DX 컨설턴트(트랙)이 나눠져있다. 컨설팅에도 관심이 있었지만 지금까지 배워왔고 조금 더 익숙하다고 생각한 개발 쪽에 가까운 AI 개발자 트랙으로 지원하게 되었다. </p>
<ul>
<li><p><strong>서류절차</strong> : 최종학력/자격증/대외홛동/자신이 생각한 언어 레벨/지원 동기 등을 작성해야 했던 거 같다.
자신이 했던 활동들을 잘 녹여서 솔직하게 작성하는 것이 중요하다고 생각한다. AI도움받아서 글을 작성하지말고 내가 한 활동들과 지원동기를 자기 손으로 작성하자! (도움이 될지 모르겠지만 나는 내가 과거에 진행했던 활동들을 깃허브나 노션에 이미 정리가 돼있어 URL도 같이 제출했다.)
<img src="https://velog.velcdn.com/images/mi_nini/post/bf64e33f-dc00-4528-8e8e-c88bd07a14c5/image.png" alt=""></p>
</li>
<li><p><strong>역량검사</strong> : 서류 합격 후 등록한 이메일로 역량검사에 대한 정보를 자세히 설명해 준다. 안내된 사이트 중 <a href="https://www.jobda.im/acca/introduce">AI 역량검사</a>로 온라인 테스트로 진행하게 되는데 같은 환경에서 연습할 수 있는 기회도 5번 제공하니 테스트 진행 전 연습을 충분히 하고 테스트를 보면 된다. (역량검사 기간도 9일 정도 주기 때문에 시간, 공간 제약 없이 역량검사를 진행하자)</p>
</li>
<li><p><em>참고로 과거 에이블스쿨 AI트랙에 경우 따로 코딩테스트를 진행했어야했는데 9기 지원에서는 AI/DX 공통으로 AI역량검사만을 진행하면된다.*</em></p>
<blockquote>
<p>공부할때 참고한 유튜브지만 많은 자료가 있으니 더 참고해서 테스트 진행해보세요!
신유형 도형 기억하기 : <a href="https://www.youtube.com/watch?v=QMUhmIi_X2k&amp;t=194s">https://www.youtube.com/watch?v=QMUhmIi_X2k&amp;t=194s</a>
길 이어만들기 : <a href="https://www.youtube.com/watch?v=UDYLBG__Jeg">https://www.youtube.com/watch?v=UDYLBG__Jeg</a>
도형회전하기 : <a href="https://www.youtube.com/watch?v=ozWnwQifWPk&amp;t=149s">https://www.youtube.com/watch?v=ozWnwQifWPk&amp;t=149s</a>
마법약 만들기 : <a href="https://www.youtube.com/watch?v=qTKFQYPUg2Y">https://www.youtube.com/watch?v=qTKFQYPUg2Y</a></p>
</blockquote>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/818f6991-860b-47a8-83a3-294703a394e8/image.png" alt=""></p>
<ul>
<li><strong>합격발표/교육시작</strong> : 최종 결과 발표 후 이메일로 입교 정보, 에이블 스쿨 <a href="https://www.instagram.com/aivlestory/">공식인스타그램</a>, 오픈 카톡 방, K-DT 참여 규정 및 설명회를 진행하니 안내사항에 따라 잘 준비하고 교육을 시작하면 된다. (기간도 넉넉하고 안내사항을 이메일, 카카오톡, 문자로 안내 주시니 못따라가면 바보...)</li>
</ul>
<hr>
<h3 id="마치며">마치며</h3>
<p>KT-AIVLE을 지원하려고 하는 사람들에게 도움이 되었으면 해서 글을 작성했다. 큰 내용 없이 너무 단순한 글이지만 궁금한 점은 (기수 모집당시)AVILE 스쿨 오픈카톡방 또는 <a href="https://aivle.kt.co.kr/home/main/indexMain">공식 홈페이지</a> FAQ를 이용하면 도움을 받을 수 있을 것이다. (또는 댓글 남겨주시면 제가 알고 있는 지식에서 도와드리겠습니다)</p>
<blockquote>
<p>글쓴이는 9기 AI 트랙 수도권 5반에 배정받았습니다.</p>
</blockquote>
<hr>
<h3 id="부록">부록</h3>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/914366cc-b0a7-4f9a-85fc-d596f570f725/image.jpg" alt=""></p>
<p>AIVLE 스쿨에 입교하게 되면 교육기간 동안 사용할 노트북을 개인 모두에게 제공해 주니 참고! (교육 시 필요한 프로그램들을 설치 필요 없이 모두 준비돼있음)</p>
<blockquote>
<p>듀얼 모니터로 연결해 잘 사용 중입니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[늦은 2026 알림]]></title>
            <link>https://velog.io/@mi_nini/%EC%98%A4%EB%9E%9C%EB%A7%8C%EC%97%90-%EB%8F%8C%EC%95%84%EC%99%94%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@mi_nini/%EC%98%A4%EB%9E%9C%EB%A7%8C%EC%97%90-%EB%8F%8C%EC%95%84%EC%99%94%EC%8A%B5%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Wed, 01 Apr 2026 11:30:03 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>Velog에서 마지막글을 작성했던 2025년 6월 기점으로 약 10개월이 지나갔다. (옵시디언, 노션으로 기록을 했기 떄문에 밀렸던게 당연한가..) 그시간동안 많이 도전하고 실패도 느껴봤던거 같다. 졸업하기전 내가 원하던 목적지에 최종 도달하는데 실패했지만 하나씩 나의 기억을 정리하며 이제부터 다시 Velog로 돌아와  기록하고자 한다.</p>
<hr>
<h2 id="2025년-마지막-여름-방학">2025년 마지막 여름 방학</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/2ccc6e05-7ad9-4b38-be4c-82bf46cbee3b/image.png" width="300"/></td>
<td align="center"><img src="https://velog.velcdn.com/images/mi_nini/post/0739b861-1b74-45d7-b47f-639721f81797/image.png" width="300"/></td>
</tr>
</tbody></table>
<p>2025년에 여름방학은 나의 본가인 부천에서 보내지 않고 학교 연구실에 출근해 9 to 6을 지키려고 노력했다. 교수님이 방학 동안은 학부수업이 없으니 훨씬 여유롭고 자신이 집중하면 연구, 개인공부 등 하고싶은 공부를 하면 모든 것을 이뤄낼 수 있다고 응원도 해주셨다. (연구실에 있는 학부연구생, 석사생분들) 모든 사람들이 같이 학습하고 회의할 수 있는 장을 보낼 수 있게 RAG, LLM 공부 회의 및 논문세미나를 진행해 혼자 학습만을 하는 것이 아닌 함께 학습할 수 있어 보다 수 훨씬 하게 공부할 수 있었던 거 같다. </p>
<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/f1fefd19-dba2-44cd-bf80-182999704253/image.jpg" alt=""></td>
<td align="center"><img src="https://velog.velcdn.com/images/mi_nini/post/eba4dcb1-779a-4faf-8427-792e27b43bb6/image.jpg" alt=""></td>
</tr>
</tbody></table>
<hr>
<p>방학 동안에 있던 가장 뜻깊은 경험이라면 내가 너무 감사하게도 학교 등록금을 전액 받으면서 다닐 수 있었던 KT 디지털 인재 장학생으로써  KT 직원들과의 팀 프로젝트가 있었다. 조원은 학생들끼리만 배정되고 피드백을 각 독방에 멘토님이 계셔서 우리가 궁금한 점 어떤 점이 부족한가에 대한 가차없는 피드백을 받을 수 있었다. 우리 조는 총 4명으로 내가 조장으로써 좋은 성과를 내기위해 열심히 노려했던거 같다. <a href="https://github.com/KT-TeamProject-11">Notion-Team Project - 11</a> 천안재생센터와 미팅을 진행하며 요구사항을 듣고 우리가 천안재생센터가 필요하다고 생각하는 웹 또는 앱에 대한 주제를 선정해 결과물을 만들어 1박 2일 워크숍도 잘 다녀왔다. 내가 대학생인 신분에서 쉽게 경험하지 못할 이 감정을 선물해주신 KT-ESG 경영팀, 임직원분들, 그리고 멘토로 활동해주신 KT-Model Prototype, LLM, Cloud팀 멘토님들에게 감사인사를 전합니다.</p>
<blockquote>
<h4 id="추가로">추가로</h4>
</blockquote>
<ul>
<li>정보통신산업진흥원이 운영하는 2025-오픈소스 컨트리뷰션 아카데미 [Pytorch 문서화 번역] 파트 멘티에 참여하게 되어 2학기에 진행할 예정이다.</li>
<li>빅데이터분석기사 실기에 합격해 자격증 +1 </li>
<li>KT-AI 생성형AI 영상부문 공모전 입상 +1</li>
<li>백준 골드5 달성 </li>
</ul>
<hr>
<h2 id="2025년-마지막-학기를-보내며">2025년 마지막 학기를 보내며</h2>
<p>대학교 4-2학기인 마지막 학년을 보내며 주위친구들 중 이제 자리를 잡아가는 친구들도 하나둘씩 생기게 되었다. 대기업, 여러 인턴을 도전하는 친구들, 전공을 포기하고 다른 일자리를 찾으며 공인어학성적을 준비하는 친구들 중견, 중소에 취업해 졸업 전에 자기의 뜻을 이룬 친구들 등 나도 나 자신이 노력하고 있다고 생각하고 하루를 뜻깊게 살았지만 나보다 더 노력하고 열심히 하는 주위친구들이 많았었다고 생각한다. 나의 마지막 학기에 끝은 학부연구생을 하며 좋아하던 연구가 생겼었고 그 분야에 도전하기 위해 ist계열 대학원에 지원했었다. (서울권 대학원은 아무리 그래도 등록금에 대해 너무 부담스럽다고 생각했다.)</p>
<table>
<thead>
<tr>
<th align="center">면접</th>
<th align="center"><a href="https://github.com/KU-NoteFlow">졸업작품</a></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><img src="https://velog.velcdn.com/images/mi_nini/post/aea018d2-15c5-46df-8825-3090d19de662/image.jpg" width="300"/></td>
<td align="center"><img src="https://velog.velcdn.com/images/mi_nini/post/53fed67a-8cbe-4e76-b51e-1823b0f2e932/image.jpg" width="300"/></td>
</tr>
</tbody></table>
<p>(디지스트, 지스트, 유니스트)과학기술원에 도전하였고 지스트는 서류가 떨어졌지만 유니스트와 디지스트는 서류에 합격해 면접을 볼 좋은 기회가 생겼었다. 각 대구와 울산에 있으며 나의 학교에서 출발해도 하루를 학교 공결내고 왕복해도 부족할 만큼 거리가 좀 멀었던 기억이 남는다. 두 가지의 면접 모두 제공해준 지침과 구글링을 통해 어느 정도 감을 잡고 있었지만, 영어로 발표와 대답을 할 수 있다는 점이 나에게는 큰 한계였던 거 같다. 내 나름대로 짧은 기간 동안 모든 심혈을 기울여 집중하고 밤새고 대본 외우고 학교, 집에서 PT 연습을 많이 했었지만, 결과는 둘 다 떨어졌다. (교수님 컨택이 늦은 점도 있으며 면접 당시 1지망에 지원했던 교수님에게만 메일이 오지 않았지만, 면접에서 1지망 교수님이 있어서 매우 당황했던 기억이 있다.) 떨어지고 나서 미리 완성했었던 <a href="https://github.com/KKU-NoteFlow">졸업작품</a> 발표도 전시회도 그럭저럭 마무리하며 내가 2년 동안 함께했던 컴퓨터공학과 학생회 사람들과 나의 학교생활을 마무리했던 거 같다. (학부연구생 연구 할당에 집중하지 못한 내가 아쉽네..)</p>
<blockquote>
<h4 id="추가로-1">추가로</h4>
</blockquote>
<ul>
<li>정보처리기사 합격으로 자격증 +1 </li>
<li>학교 내 Mircro_Degreee - 인공지능전문가양성과정 수료</li>
<li>OSSCA-오픈소스-Pytorch 멘티 활동 수료</li>
<li>AI-FSESTA 컨퍼런스 참여<blockquote>
<ul>
<li>2025 GIT
<img src="https://velog.velcdn.com/images/mi_nini/post/e7a9719c-505a-43b9-853d-9b4eaa8f1a06/image.png" alt=""></li>
</ul>
</blockquote>
</li>
</ul>
<hr>
<h2 id="2026">2026</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/531d7395-e36a-427e-8056-c12bbafa8c70/image.jpg" width="500"/></td>
<td align="center"><img src="https://velog.velcdn.com/images/mi_nini/post/3bf02ce8-71e2-41aa-9df5-7dfc24df0461/image.jpg" width="500"/></td>
</tr>
</tbody></table>
<p>졸업식, 연구실정리, 부트캠프, 다시 대학원 컨택 등 내가 무엇을 해야할지, 나아갈 내 자신이 두려웠다. 사실 대학원에 떨어졌던 충격이 제일 크기도 했지만 내가 제일 잘하는 것이 무엇일까, 내가 노력했던 분야를 끝까지 물고가면 성공, 합격할 수 있을까 생각했다. 종강하고 3년동안 머물렀던 자취방을나와 오랜만에 나의 본가에 도착했다. 그동안 밀렸던 잠도 자고 국외여행도 다녀왔다. 졸업이기에 급한 마음도 있었지만 다시 오지 않을 26살이자 회사 또는 다른 일을 시작하게 된다면 이럴 시간도 없을 것이라 생각했다. (생각해보니 베짱이 마인드네) 놀기도 놀지만 매일 코딩테스트 문제나 내가 관심 있었던 머신 러닝, computer vision, 3D 그래픽 기사나 책도 가끔 읽는다. 이건 순순히 재밌기도 했고 밤새우면서 내가 무언가에 오랫동안 몰두했던 기억이 남아서이기도 한 거 같다. 하루를 낭비하지 않고 살아가는 것 그렇게 살기 위해 오늘 다시 시작해보려고 한다.</p>
<blockquote>
<p>포기하지말자</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[CART & Ensumble]]></title>
            <link>https://velog.io/@mi_nini/CART-Ensumble</link>
            <guid>https://velog.io/@mi_nini/CART-Ensumble</guid>
            <pubDate>Thu, 19 Jun 2025 17:19:56 GMT</pubDate>
            <description><![CDATA[<h1 id="비지도-학습-및-앙상블-주요-코드">비지도 학습 및 앙상블 주요 코드</h1>
<h2 id="목차">목차</h2>
<ul>
<li><a href="#%ED%8C%8C%ED%8A%B8-4-cart-%EB%B6%84%EB%A5%98-%ED%8A%B8%EB%A6%AC-classification-tree">파트 4: CART: 분류 트리 (Classification Tree)</a>  </li>
<li><a href="#%ED%8C%8C%ED%8A%B8-5-cart-%ED%9A%8C%EA%B7%80-%ED%8A%B8%EB%A6%AC-regression-tree">파트 5: CART: 회귀 트리 (Regression Tree)</a>  </li>
<li><a href="#%ED%8C%8C%ED%8A%B8-6-%EB%AA%A8%EB%8D%B8-%EC%9D%BC%EB%B0%98%ED%99%94-%ED%8F%89%EA%B0%80--%EA%B5%90%EC%B0%A8-%EA%B2%80%EC%A6%9D">파트 6: 모델 일반화 평가 &amp; 교차 검증</a>  </li>
<li><a href="#%ED%8C%8C%ED%8A%B8-7-bagging">파트 7: Bagging</a>  </li>
<li><a href="#%ED%8C%8C%ED%8A%B8-8-adaboost">파트 8: AdaBoost</a>  </li>
<li><a href="#%ED%8C%8C%ED%8A%B8-9-voting-classifier">파트 9: Voting Classifier</a>  </li>
<li><a href="#%ED%8C%8C%ED%8A%B8-10-random-forest-regressor">파트 10: Random Forest Regressor</a>  </li>
</ul>
<hr>
<h2 id="파트-4-cart-분류-트리-classification-tree">파트 4. CART: 분류 트리 (Classification Tree)</h2>
<pre><code class="language-python"># 페이지 15
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 데이터 분할 (80% train, 20% test)
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    stratify=y,
    random_state=1
)

# 모델 선언 &amp; 학습
dt = DecisionTreeClassifier(criterion=&#39;gini&#39;, random_state=1)
dt.fit(X_train, y_train)

# 예측 &amp; 평가
y_pred = dt.predict(X_test)
print(accuracy_score(y_test, y_pred))</code></pre>
<hr>
<h2 id="파트-5-cart-회귀-트리-regression-tree">파트 5. CART: 회귀 트리 (Regression Tree)</h2>
<pre><code class="language-python"># 페이지 20
from sklearn.tree import DecisionTreeRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error as MSE

# 데이터 분할 (80% train, 20% test)
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=3
)

# 모델 선언 &amp; 학습
dt = DecisionTreeRegressor(max_depth=4, min_samples_leaf=0.1, random_state=3)
dt.fit(X_train, y_train)

# 예측 &amp; 평가 (RMSE)
y_pred = dt.predict(X_test)
rmse = MSE(y_test, y_pred) ** 0.5
print(rmse)</code></pre>
<hr>
<h2 id="파트-6-모델-일반화-평가--교차-검증">파트 6. 모델 일반화 평가 &amp; 교차 검증</h2>
<pre><code class="language-python"># 페이지 21-23
from sklearn.tree import DecisionTreeRegressor
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import mean_squared_error as MSE

# 설정
SEED = 123

# 데이터 분할 (70% train, 30% test)
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.3,
    random_state=SEED
)

# 모델 선언
dt = DecisionTreeRegressor(max_depth=4, min_samples_leaf=0.14, random_state=SEED)

# 10-fold CV MSE 계산
mse_cv = -cross_val_score(
    dt, X_train, y_train,
    cv=10,
    scoring=&#39;neg_mean_squared_error&#39;,
    n_jobs=-1
)

# 학습 &amp; 예측
dt.fit(X_train, y_train)
y_train_pred = dt.predict(X_train)
y_test_pred = dt.predict(X_test)

# 결과 출력
print(&#39;CV MSE:&#39;, mse_cv.mean())
print(&#39;Train MSE:&#39;, MSE(y_train, y_train_pred))
print(&#39;Test MSE:&#39;, MSE(y_test, y_test_pred))</code></pre>
<hr>
<h3 id="파트-7-bagging">파트 7. Bagging</h3>
<pre><code class="language-python"># 페이지 8-9, 15
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

SEED = 1

# 데이터 분할 (70% train, 30% test)
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.3,
    stratify=y,
    random_state=SEED
)

# 기본 모델 선언
dt = DecisionTreeClassifier(max_depth=4, min_samples_leaf=0.16, random_state=SEED)

# Bagging 모델 선언 (n_estimators=300)
bc = BaggingClassifier(
    base_estimator=dt,
    n_estimators=300,
    n_jobs=-1
)

# 학습 &amp; 예측
bc.fit(X_train, y_train)
y_pred = bc.predict(X_test)
print(&#39;Bagging Accuracy:&#39;, accuracy_score(y_test, y_pred))

# OOB 평가 (oob_score=True 설정 시)
# bc = BaggingClassifier(base_estimator=dt, n_estimators=300, oob_score=True, n_jobs=-1)
# bc.fit(X_train, y_train)
# print(&#39;OOB Accuracy:&#39;, bc.oob_score_)</code></pre>
<hr>
<h2 id="파트-8-adaboost">파트 8. AdaBoost</h2>
<pre><code class="language-python"># 페이지 8-9
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split

SEED = 1

# 데이터 분할 (70% train, 30% test)
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.3,
    stratify=y,
    random_state=SEED
)

# Decision Stump 선언
dt = DecisionTreeClassifier(max_depth=1, random_state=SEED)

# AdaBoost 모델 선언 (n_estimators=100)
adb_clf = AdaBoostClassifier(base_estimator=dt, n_estimators=100)

# 학습 &amp; 예측 확률
adb_clf.fit(X_train, y_train)
y_proba = adb_clf.predict_proba(X_test)[:, 1]

# 평가 (ROC AUC)
print(&#39;ROC AUC:&#39;, roc_auc_score(y_test, y_proba))</code></pre>
<hr>
<h2 id="파트-9-voting-classifier">파트 9: Voting Classifier</h2>
<pre><code class="language-python"># 1) 라이브러리 임포트
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import VotingClassifier

# 2) 데이터 분할
SEED = 1
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.3,
    random_state=SEED
)

# 3) 기본 분류기 선언
lr = LogisticRegression(random_state=SEED)
knn = KNeighborsClassifier()
dt = DecisionTreeClassifier(random_state=SEED)

# 4) 앙상블 메타 모델 선언 및 학습
voting_clf = VotingClassifier(
    estimators=[(&#39;lr&#39;, lr), (&#39;knn&#39;, knn), (&#39;dt&#39;, dt)],
    voting=&#39;hard&#39;
)
voting_clf.fit(X_train, y_train)

# 5) 예측 및 평가
y_pred = voting_clf.predict(X_test)
print(accuracy_score(y_test, y_pred))</code></pre>
<hr>
<h2 id="파트-10-random-forestregressor">파트 10: Random ForestRegressor</h2>
<pre><code class="language-python"># 1) 라이브러리 임포트
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error as MSE

# 2) 데이터 분할
SEED = 1
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.3,
    random_state=SEED
)

# 3) 모델 선언 및 학습
rf = RandomForestRegressor(
    n_estimators=400,       # 트리 개수
    min_samples_leaf=0.12,  # 리프 노드 최소 샘플 비율
    random_state=SEED
)
rf.fit(X_train, y_train)

# 4) 예측 및 평가 (RMSE)
y_pred = rf.predict(X_test)
rmse = MSE(y_test, y_pred) ** 0.5
print(f&#39;Test set RMSE of rf: {rmse:.2f}&#39;)
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[k-mean, 군집화]]></title>
            <link>https://velog.io/@mi_nini/Unserpervised-learningfinal-term</link>
            <guid>https://velog.io/@mi_nini/Unserpervised-learningfinal-term</guid>
            <pubDate>Thu, 19 Jun 2025 17:11:13 GMT</pubDate>
            <description><![CDATA[<h3 id="목차">목차</h3>
<ul>
<li><a href="#%ED%8C%8C%ED%8A%B8-1-k-means-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EB%A7%81">파트 1: K-Means 클러스터링</a>  </li>
<li><a href="#%ED%8C%8C%ED%8A%B8-2-%EA%B3%84%EC%B8%B5%EC%A0%81-%EA%B5%B0%EC%A7%91%ED%99%94--t-sne">파트 2: 계층적 군집화 &amp; t-SNE</a>  </li>
<li><a href="#%ED%8C%8C%ED%8A%B8-3-nmf-non-negative-matrix-factorization">파트 3: NMF (Non-Negative Matrix Factorization)</a>  </li>
</ul>
<hr>
<h2 id="파트-1-k-means-클러스터링">파트 1: K-Means 클러스터링</h2>
<h3 id="개념">개념</h3>
<ul>
<li>데이터를 k개의 군집으로 나누는 비지도 학습 알고리즘  </li>
<li>중심(centroid)을 반복 갱신하며 군집 할당  </li>
</ul>
<h3 id="코드-스니펫">코드 스니펫</h3>
<pre><code class="language-python"># 1) 라이브러리 임포트
import numpy as np
from sklearn.cluster import KMeans

# 2) 데이터 로드
samples = np.loadtxt(&#39;data.csv&#39;, delimiter=&#39;,&#39;)       # → 파일 경로/이름 수정

# 3) 모델 선언 및 학습
model = KMeans(n_clusters=3)                          # → k 값 수정
model.fit(samples)

# 4) 클러스터 할당
labels = model.predict(samples)
print(labels)                                         # 예: [0 0 1 1 0 1 …]

# 5) 새로운 샘플 예측
new_samples = np.array([[…], […], […]])              # → 예측할 데이터 입력
new_labels = model.predict(new_samples)
print(new_labels)

# 6) 클러스터 평가 (관성: inertia)
print(model.inertia_)                                 # 값이 낮을수록 응집도 높음

# 7) 시각화 (산점도)
import matplotlib.pyplot as plt
xs = samples[:, 0]                                     # → 사용할 차원 인덱스 조정
ys = samples[:, 1]
plt.scatter(xs, ys, c=labels)
plt.show()</code></pre>
<hr>
<h2 id="파트-2-계층적-군집화--t-sne">파트 2: 계층적 군집화 &amp; t-SNE</h2>
<p>개념
계층적 군집화: 덴드로그램으로 군집 구조 파악
t-SNE: 고차원 데이터를 2차원으로 축소해 시각화</p>
<ul>
<li>계층적 군집화<pre><code class="language-python"># 라이브러리 임포트
from scipy.cluster.hierarchy import linkage, dendrogram, fcluster
import matplotlib.pyplot as plt
</code></pre>
</li>
</ul>
<h1 id="병합-연산-linkage">병합 연산 (linkage)</h1>
<p>mergings = linkage(samples, method=&#39;complete&#39;)         # method: &#39;single&#39;,&#39;average&#39;,&#39;ward&#39; 등</p>
<h1 id="덴드로그램-시각화">덴드로그램 시각화</h1>
<p>dendrogram(
    mergings,
    labels=country_names,                             # → 레이블 리스트 입력
    leaf_rotation=90,
    leaf_font_size=6
)
plt.show()</p>
<h1 id="클러스터-레이블-추출">클러스터 레이블 추출</h1>
<p>labels = fcluster(mergings, 15, criterion=&#39;distance&#39;) # → 거리 컷오프 값 조정
print(labels)</p>
<pre><code>- t-SNE
```python
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt

# 모델 선언 및 변환
model = TSNE(learning_rate=100)                       # → learning_rate 조정 (50–200 권장)
transformed = model.fit_transform(samples)

# 시각화
xs = transformed[:, 0]
ys = transformed[:, 1]
plt.scatter(xs, ys, c=species)                        # → 군집 비교용 레이블 입력
plt.show()
</code></pre><hr>
<h2 id="파트-3-nmf-non-negative-matrix-factorization">파트 3: NMF (Non-Negative Matrix Factorization)</h2>
<p>개념
음수가 아닌 행렬 분해를 통해 잠재 요인(topic) 추출
문서·이미지 처리, 추천 시스템 등에 활용</p>
<pre><code class="language-python"># 1) 라이브러리 임포트
import numpy as np
from sklearn.decomposition import NMF
import matplotlib.pyplot as plt

# 2) 모델 선언 및 학습
model = NMF(n_components=2)                           # → 컴포넌트 수 수정
model.fit(samples)

# 3) 특징 행렬 변환
nmf_features = model.transform(samples)
print(nmf_features)

# 4) 컴포넌트 행렬 확인
print(model.components_)                              # (n_components, n_features)

# 5) 이미지 재구성 예시
bitmap = sample.reshape((height, width))              # → sample, 크기 수정
plt.imshow(bitmap, cmap=&#39;gray&#39;, interpolation=&#39;nearest&#39;)
plt.show()

# 6) 추천 시스템 적용 예시
nmf = NMF(n_components=6)                             # → 추천용 토픽 수 조정
nmf_features = nmf.fit_transform(articles)            # → articles: 문서×단어 행렬
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[학부연구생 2일차]]></title>
            <link>https://velog.io/@mi_nini/%ED%95%99%EB%B6%80%EC%97%B0%EA%B5%AC%EC%83%9D-2%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@mi_nini/%ED%95%99%EB%B6%80%EC%97%B0%EA%B5%AC%EC%83%9D-2%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 09 Jan 2025 07:10:54 GMT</pubDate>
            <description><![CDATA[<h1 id="이번주-목표">이번주 목표</h1>
<p>1/9
fastapi 공부(로그인 기능 x) 
-&gt; 파이썬 가상환경(.venv)연결해 예제 풀이
axios 공부
( 로컬에서 돌려보기)
mysql 공부(이걸로 서버 관리함)
-&gt; mysql 벤치 설치 후 연결 확인 
orm 이용해서 예제 풀어보기
orm을 사용하여 fastapi를 어떻게 할지 공부</p>
<p>-&gt; 
개념 넓고 얇게 공부하기</p>
<p>데이터 주고 받기 정도</p>
<h2 id="fastapi란">FastAPI란?</h2>
<p>FastAPI는 Python으로 작성된 최신 웹 프레임워크로, 빠르고 간단하며 효율적인 API를 개발할 수 있도록 설계되었습니다. 높은 성능과 사용자 친화적인 인터페이스를 제공하여 웹 개발자들 사이에서 인기를 얻고 있습니다.</p>
<hr>
<h2 id="fastapi의-주요-특징"><strong>FastAPI의 주요 특징</strong></h2>
<h3 id="1-고성능">1. <strong>고성능</strong></h3>
<ul>
<li><strong>ASGI(Asynchronous Server Gateway Interface) 기반</strong>: 비동기 처리를 지원하여 높은 처리 속도를 자랑.</li>
<li>Python의 <strong>Starlette</strong>와 <strong>Pydantic</strong>을 기반으로 제작되어 경량화와 안정성 보장.</li>
</ul>
<h3 id="2-자동-문서화">2. <strong>자동 문서화</strong></h3>
<ul>
<li>FastAPI는 OpenAPI와 JSON Schema를 기반으로 <strong>자동 API 문서화</strong>를 제공합니다.</li>
<li>API를 생성하면 <code>/docs</code> 또는 <code>/redoc</code> 경로에서 문서를 자동 생성 및 확인 가능.</li>
</ul>
<h3 id="3-유형-검사type-hint-활용">3. <strong>유형 검사(Type Hint) 활용</strong></h3>
<ul>
<li>Python의 <strong>Type Hint</strong>를 적극 활용하여 코드의 안정성과 가독성 향상.</li>
<li>Pydantic을 사용해 데이터 검증과 직렬화 지원.</li>
</ul>
<h3 id="4-비동기-지원">4. <strong>비동기 지원</strong></h3>
<ul>
<li><code>async</code>/<code>await</code> 문법을 기본적으로 지원하여 비동기 작업을 간단하게 구현 가능.</li>
<li>데이터베이스, 외부 API와의 비동기 통신에 최적화.</li>
</ul>
<h3 id="5-간결한-코드">5. <strong>간결한 코드</strong></h3>
<ul>
<li>직관적인 문법과 코드 구조로 빠르게 API 개발 가능.</li>
<li>코드 작성이 간소화되어 생산성 증가.</li>
</ul>
<hr>
<h2 id="fastapi-시작하기"><strong>FastAPI 시작하기</strong></h2>
<p>FastAPI를 시작하는 것은 간단하다. 아래의 명령어를 실행하여 FastAPI를 설치</p>
<h3 id="설치">설치</h3>
<pre><code class="language-bash">pip install fastapi uvicorn</code></pre>
<h3 id="간단한-예제-코드">간단한 예제 코드</h3>
<pre><code class="language-python">from fastapi import FastAPI

app = FastAPI()

@app.get(&quot;/&quot;)
def read_root():
    return {&quot;message&quot;: &quot;Hello, FastAPI!&quot;}

@app.get(&quot;/items/{item_id}&quot;)
def read_item(item_id: int, q: str = None):
    return {&quot;item_id&quot;: item_id, &quot;q&quot;: q}</code></pre>
<h3 id="실행">실행</h3>
<pre><code class="language-bash">uvicorn main:app --reload</code></pre>
<ul>
<li><code>http://127.0.0.1:8000</code>에서 API를 확인할 수 있습니다.</li>
<li>자동 생성된 문서는 <code>http://127.0.0.1:8000/docs</code>에서 확인 가능합니다.</li>
</ul>
<hr>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/54a5929f-c175-4156-9d76-c05a52d55ff2/image.png" alt=""></p>
<p>간단하게 html 연결해서 실행해보자
FastAPI로 간단한 대시보드 구현 프로젝트 설명
실행 환경</p>
<pre><code>Python 버전: 3.10 이상
FastAPI: 웹 프레임워크
Uvicorn: ASGI 서버
Jinja2: HTML 템플릿 렌더링
파일 구조:

프로젝트 폴더/
├── .venv/                   # 가상환경 폴더
│   ├── Include/
│   ├── Lib/
│   ├── Scripts/
│   └── 기타 가상환경 파일들
├── static/                  # 정적 파일 저장 폴더
│   └── style.css            # 스타일 파일
├── templates/               # 템플릿 파일 저장 폴더
│   └── dashboard.html       # Jinja2 기반 템플릿 파일
├── main.py                  # FastAPI 애플리케이션 파일
└── 기타 프로젝트 관련 파일</code></pre><p>코드 설명</p>
<pre><code class="language-python"># 1. main.py: FastAPI 애플리케이션 로직

from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

app = FastAPI()</code></pre>
<hr>
<h2 id="정적-파일-및-템플릿-설정">정적 파일 및 템플릿 설정</h2>
<pre><code class="language-py">app.mount(&quot;/static&quot;, StaticFiles(directory=&quot;static&quot;), name=&quot;static&quot;)
templates = Jinja2Templates(directory=&quot;templates&quot;)


@app.get(&quot;/&quot;, response_class=HTMLResponse)
async def read_dashboard(request: Request):
    data = {&quot;visitors&quot;: 123, &quot;page_views&quot;: 456}  # 샘플 데이터
    return templates.TemplateResponse(&quot;dashboard.html&quot;, {&quot;request&quot;: request, &quot;data&quot;: data})</code></pre>
<blockquote>
<p>핵심 기능:
        /static 경로에 정적 파일(style.css)을 매핑.
        /templates 디렉토리에서 Jinja2 HTML 템플릿(dashboard.html)을 로드.
        샘플 데이터 (visitors, page_views)를 템플릿으로 전달.</p>
</blockquote>
<hr>
<h2 id="dashboardhtml-템플릿-파일">dashboard.html: 템플릿 파일</h2>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;FastAPI Dashboard&lt;/title&gt;
    &lt;link rel=&quot;stylesheet&quot; href=&quot;/static/style.css&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;FastAPI 대시보드&lt;/h1&gt;
    &lt;div class=&quot;container&quot;&gt;
        &lt;p&gt;방문자 수: {{ data.visitors }}&lt;/p&gt;
        &lt;p&gt;페이지 조회 수: {{ data.page_views }}&lt;/p&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<blockquote>
<p>HTML 기능:
        {{ data.visitors }} 및 {{ data.page_views }}는 FastAPI가 전달한 데이터를 표시.
        /static/style.css에서 불러온 스타일을 적용.</p>
</blockquote>
<h2 id="stylecss-정적-css-파일">style.css: 정적 CSS 파일</h2>
<pre><code class="language-css">body {
    font-family: Arial, sans-serif;
    margin: 20px;
    background-color: #f4f4f9;
    color: #333;
}

.container {
    border: 1px solid #ccc;
    padding: 20px;
    border-radius: 5px;
    background: #fff;
    box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
}</code></pre>
<blockquote>
<p>디자인 기능:
        배경색: 연한 회색(background-color: #f4f4f9)
        텍스트 색상: 어두운 회색(color: #333)
        컨테이너: 박스 그림자와 둥근 모서리로 심미성 향상.</p>
</blockquote>
<h2 id="구현-과정">구현 과정</h2>
<blockquote>
<ul>
<li>Python 가상환경 생성 및 활성화
python -m venv .venv
.venv\Scripts\activate          # Windows</li>
<li>필수 라이브러리 설치
pip install fastapi uvicorn jinja2</li>
<li>FastAPI 서버 실행
uvicorn main:app --reload</li>
<li>웹 브라우저에서 확인
  URL: <a href="http://127.0.0.1:8000">http://127.0.0.1:8000</a>
  CSS가 적용된 대시보드와 방문자 및 페이지 조회 수 데이터가 출력됩니다.</li>
</ul>
</blockquote>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mi_nini/post/f07338a1-0e55-4a15-86c1-9b2535663045/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mi_nini/post/8fe5f04f-45d5-498c-8667-3cb6feaf8c0f/image.png" alt=""></th>
</tr>
</thead>
</table>
<hr>
<h1 id="추가">추가</h1>
<p><img src="https://velog.velcdn.com/images/mi_nini/post/77599989-1589-468b-97c0-6a49e4db0319/image.png" alt="">
Html, js, css를 이용해서 틀을 만들어주고 fastapi를 이용해서  <a href="http://127.0.0.1:8000/">http://127.0.0.1:8000/</a> 로컬서버에 돌린 결과이다. 우리는 결과적으로 React와 fastapi를 연결하는 것이 중요하기에 이부분을 중심적으로 다시 공부해보고 연결해봐야 하겠다. </p>
<blockquote>
<p>참고하자
<a href="https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/">https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/</a>
<a href="https://blog.joonas.io/227">https://blog.joonas.io/227</a>
<a href="https://fastapi.tiangolo.com/ko/">https://fastapi.tiangolo.com/ko/</a>
<a href="https://github.com/reactjs/ko.react.dev">https://github.com/reactjs/ko.react.dev</a>
<a href="https://jongsky.tistory.com/17">https://jongsky.tistory.com/17</a></p>
</blockquote>
<hr>
<blockquote>
<p>이글을 끝으로 옵시디언을 활용해 글을 정리할 예정이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[항해99 잔디]]></title>
            <link>https://velog.io/@mi_nini/%ED%95%AD%ED%95%B499-%EC%9E%94%EB%94%94</link>
            <guid>https://velog.io/@mi_nini/%ED%95%AD%ED%95%B499-%EC%9E%94%EB%94%94</guid>
            <pubDate>Tue, 31 Dec 2024 05:41:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mi_nini/post/9afe0334-1817-4436-b660-3f2411e570ca/image.png" alt=""></p>
<p>안녕하세요! 깃허브를 통해 이번에 <a href="https://hanghae99.spartacodingclub.kr/campaign?utm_source=effic&amp;utm_medium=crm&amp;utm_campaign=%ED%95%AD%ED%95%B4&amp;utm_content=%EC%9D%B4%EB%B2%A4%ED%8A%B8%EC%B0%B8%EC%97%AC_branding&amp;utm_term=%EA%B0%9C%EB%B0%9C%EC%9E%90_241231">[항해99]</a>에서 진행하는 잔디 기부 캠페인에 참여했습니다. (위 이미지는 제가 기부를 통해 받은 인증서입니다. 🌱)</p>
<h2 id="📌-깃허브">📌 깃허브</h2>
<p>깃허브를 운영하며 올해 느낀 것은, 개발자 커뮤니티는 서로 돕고 영감을 나눌 때 가장 빛난다는 점입니다.
하나의 작은 코드가 누군가의 성장에 도움이 되고, 하나의 작은 기부가 더 나은 세상을 만드는데 기여할 수 있다는 점에서 큰 보람을 느낀 한 해였습니다.
이번 잔디 기부 캠페인은 이러한 가치를 몸소 실천할 수 있는 기회였기에, 저 역시 참여했습니다. </p>
<h2 id="📌-2024년을-마무리하며">📌 2024년을 마무리하며</h2>
<p>2024년은 제게 있어 여러 도전과 배움의 시간이었습니다.
개발자로서 그리고 깃허브 운영자로서, 더 나은 코드를 작성하고 더 많은 사람들과 함께 나눔의 가치를 실천하려 노력했던 한 해였죠.
돌아보면, 완벽한 해는 아니었지만 <strong>&quot;성장했다&quot;</strong>라는 뿌듯함이 남습니다.
그리고 올해를 마무리하며 느낀 가장 큰 교훈은, 작은 실천도 모이면 큰 변화를 만든다는 점입니다. 다가오는 2025년에도 저는 이 마음을 잊지 않고, 더 나은 개발자, 더 나은 사람으로 성장해 나가고 싶습니다. 🙌</p>
<h2 id="📌-마치며">📌 마치며</h2>
<p>작은 실천이라도 도전하고 실천하세요. 깃허브에 잔디를 심듯, 세상에 잔디를 심고, 나눔과 성장을 만들어가는 여정에 동참해보세요. 나눔은 나 혼자만의 힘으로 이루어지는 게 아니라, 우리가 함께할 때 진정한 의미를 갖는다고 생각합니다.</p>
<blockquote>
<p>#항해99 #잔디기부캠페인 #잔디기부</p>
</blockquote>
<p>2024년 한 해 동안 함께 성장하고 도전한 모든 분들께 감사드립니다.
새해에도 더 큰 나눔과 성장을 이룰 수 있기를 바라며, 따뜻하고 희망찬 2025년이 되길 기원합니다. 행복한 새해 맞이하세요! </p>
]]></description>
        </item>
    </channel>
</rss>