<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Jwhy's Blogitory</title>
    <link>https://jwhy-study.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 19 Aug 2026 00:58:04 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Jwhy</managingEditor>
    <image>
      <title>Jwhy's Blogitory</title>
      <url>https://tistory1.daumcdn.net/tistory/4952786/attach/bf98dfee558048f4a59b38f09cf9dc30</url>
      <link>https://jwhy-study.tistory.com</link>
    </image>
    <item>
      <title>Spring - SEV 장애 대응</title>
      <link>https://jwhy-study.tistory.com/151</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ 개발 환경&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java: 21&lt;/li&gt;
&lt;li&gt;Spring Boot: 3.4.4&lt;/li&gt;
&lt;li&gt;DB: RDS + MariaDB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  상황 설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 합류한 회사는 개발 조직이 최근에 만들어진 곳이었다. 빠르게 서비스를 만들다 보니 인프라에 부족한 부분이 많았다. 그래서 가장 먼저 손을 댄 일은 에러 알림을 슬랙으로 보내는 것이었다. 그 동안 아무도 몰랐던 에러들이 하나씩 수면 위로 올라왔고, 하나씩 정상화를 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 체계를 갖추고 얼마 지나지 않아 순식간에 엄청 많은 에러 메세지가 쏟아졌다. 사이트를 확인해보니 동작은 하지만 정말 느렸고, 로그를 확인해보니 커넥션 풀 고갈이 문제였다. 하지만 실제 원인을 분석해본 결과 커넥션 풀 고갈은 표면적인 문제였다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;웹 훅이 사용하는 커넥션 풀을 지정하지 않았다.&lt;/li&gt;
&lt;li&gt;순식간에 많은 웹 훅이 쏟아지면서 커넥션 풀을 모두 사용했다.&lt;/li&gt;
&lt;li&gt;실사용자의 요청 마저도 실패하게 되었다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어드민 페이지에서도 비슷한 에러가 발생했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;커넥션 풀이 고갈된 상황에서 무거운 집계 요청이 왔다.&lt;/li&gt;
&lt;li&gt;커넥션을 어떻게든 하나 잡아서 집계 쿼리를 DB에 날렸다.&lt;/li&gt;
&lt;li&gt;서버 및 DB에는 Timeout 설정이 없었다.&lt;/li&gt;
&lt;li&gt;어드민은 데이터 로딩이 느리다보니 새로고침을 했다.&lt;/li&gt;
&lt;li&gt;그 때마다 무거운 집계 쿼리가 DB에 쌓이게 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 타임아웃이 없어 무거운 집계 쿼리가 끊기지 않고 계속 쌓였고, 그만큼 CPU 부하가 누적되며 결국 99%까지 치솟은 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 해결 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. SEV-2 응급 조치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상황은 핵심 기능에 대한 부분 장애였고, 최대 30분 내에 수정을 해야 했다. 지금 당장 할 수 있는 것은 두 가지였다. &lt;b&gt;길게 잡혀 있던 폭주 쿼리를 강제로 끊고&lt;/b&gt;, 더 이상 쌓이지 않도록 &lt;b&gt;타임아웃을 걸고 HikariCP 풀을 2배로 늘리는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  datasource:
    hikari:
      maximum-pool-size: 30
    readonly:
      hikari:
        data-source-properties:
          socketTimeout: 60000
mybatis:
  configuration:
    default-statement-timeout: 15&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 Green에 배포해 스위칭하고, 기존에 DB에서 길게 커넥션을 물고 있던 집계 쿼리를 강제로 Kill 했다. 그 결과 &lt;code&gt;Connection is not available&lt;/code&gt; 에러는 잦아들었고, 사이트도 정상적으로 동작함을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 풀을 늘린 것은 &lt;b&gt;급한 불을 끄는 임시 조치일 뿐 근본 해결이 아니었다.&lt;/b&gt; CPU가 포화된 상황에서 풀을 늘리는 건 폭주 쿼리를 더 많이 동시에 받아주는 꼴이라, 오히려 독이 될 수도 있다. 진짜 해결은 그 뒤에 이어졌다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 이 응급 조치 중 타임아웃 도입은, 사고 직후 포스트모템을 정리한 뒤 별도 PR로 반영한 재발 방지책이다. 당장의 불은 폭주 쿼리 Kill과 풀 확대로 껐고, 타임아웃은 &quot;다시는 폭주가 쌓이지 않게&quot; 만드는 방어선이었다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 쿼리 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 내 존재하는 모든 조회 쿼리에 대해 최적화를 진행했다. 대부분의 테이블에 100만 ~ 500만 건 이상의 데이터가 존재하였고, JOIN 남발, 부족한 인덱싱 등 여러 문제가 많음을 발견했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중 가장 무거웠던 집계 쿼리를 우선적으로 손봤다. 이 쿼리는 상태별 집계값을 뽑기 위해 &lt;b&gt;같은 테이블을 상태마다 다시 스캔하는 파생 테이블 4개&lt;/b&gt;를 만들고, 그걸 또 메인 테이블에 &lt;code&gt;LEFT JOIN&lt;/code&gt;하고 있었다. 옵티마이저는 같은 테이블이라고 해서 한 번만 읽어 주지 않는다. 즉, 같은 테이블을 4번 풀스캔하고, 집계도 4번, 조인도 4번 수행한다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;LEFT JOIN (SELECT ..., COUNT(v.id) AS total_submitted ...) submitted_sum ON ...
LEFT JOIN (... WHERE v.status = 'APPROVED' ...) approved_sum  ON ...
LEFT JOIN (... WHERE v.status = 'PENDING'  ...) pending_sum   ON ...
LEFT JOIN (... WHERE v.status = 'DENIED'   ...) denied_sum    ON ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리를 &lt;code&gt;CASE WHEN&lt;/code&gt;을 사용한 조건부 집계로 바꿔, &lt;b&gt;한 번만 스캔&lt;/b&gt;하면서 상태별 카운트를 모두 뽑도록 했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;LEFT JOIN (
    SELECT ...,
        COUNT(v.id) AS total_submitted,
        COUNT(CASE WHEN v.status = 'APPROVED' THEN 1 END) AS total_approved,
        COUNT(CASE WHEN v.status = 'PENDING'  THEN 1 END) AS total_pending,
        COUNT(CASE WHEN v.status = 'DENIED'   THEN 1 END) AS total_denied
    ...
) ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 &lt;b&gt;같은 테이블 스캔을 4회에서 1회로 줄였고&lt;/b&gt;, 결과값은 완전히 동일했다. 이 외에도 여러 조회 쿼리를 같은 관점으로 최적화했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 레플리카 도입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블마다 수백만 건에 JOIN도 많아 조회가 무겁고, 실제로 이번 사고도 &lt;b&gt;읽기 부하가 CPU를 올린&lt;/b&gt; 경우였다. 읽기 트래픽을 원본에서 떼어내 &lt;b&gt;원본은 쓰기에 집중&lt;/b&gt;시키면, 무거운 조회가 전체 서비스를 흔드는 일을 구조적으로 줄일 수 있다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 읽기 복제본(replica) 인스턴스를 도입했다. 본 DB는 Master로 CRUD가 모두 가능하지만, 복제본인 Slave는 Read만 가능한 실시간 사본이다. Master는 가장 작은 축인 &lt;code&gt;db.t3.small&lt;/code&gt;을 쓰고 있지만, Slave는 조회가 훨씬 많고 무겁기 때문에 한 단계 위인 &lt;code&gt;db.t3.medium&lt;/code&gt;을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서는 &lt;code&gt;AbstractRoutingDataSource&lt;/code&gt;를 상속해 &lt;b&gt;&quot;읽기 전용 트랜잭션이면 복제본, 아니면 원본&quot;&lt;/b&gt; 이라는 규칙 하나만 구현하면 된다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class ReplicaRoutingDataSource extends AbstractRoutingDataSource {

    static final String WRITE = &quot;WRITE&quot;; // 원본(Master)
    static final String READ  = &quot;READ&quot;; // 복제본(Slave)

    @Override
    protected Object determineCurrentLookupKey() {
        return TransactionSynchronizationManager
            .isCurrentTransactionReadOnly() ? READ : WRITE;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해두면 조회 메서드에 &lt;code&gt;@Transactional(readOnly = true)&lt;/code&gt;만 붙여도 그 안의 &lt;code&gt;SELECT&lt;/code&gt;가 자동으로 Slave로 흘러간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 여기엔 주의할 점이 하나 있다. 이 라우터만 등록하면 &lt;b&gt;라우팅이 동작하지 않는다.&lt;/b&gt; 스프링은 트랜잭션을 시작할 때 보통 &lt;b&gt;커넥션부터 먼저 잡고&lt;/b&gt; 그 다음에 &lt;code&gt;readOnly&lt;/code&gt; 플래그를 세팅하는데, 위 라우터는 &quot;커넥션을 잡는 순간&quot;에 원본/복제본을 결정한다. 그래서 아직 &lt;code&gt;readOnly&lt;/code&gt; 표시가 안 된 상태로 판단해 &lt;b&gt;항상 원본으로 붙어버린다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하려면 데이터소스를 &lt;code&gt;LazyConnectionDataSourceProxy&lt;/code&gt;로 한 번 감싸야 한다. 이 프록시는 커넥션 획득을 &lt;b&gt;실제 쿼리가 나가는 순간까지 미루기&lt;/b&gt; 때문에, 그 사이에 &lt;code&gt;readOnly&lt;/code&gt; 플래그가 이미 세팅되어 올바르게 복제본으로 라우팅된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource routingDataSource(
        @Qualifier(&quot;dataSource&quot;) DataSource write,
        @Qualifier(&quot;replicaDataSource&quot;) DataSource read
    ) {
        ReplicaRoutingDataSource routing = new ReplicaRoutingDataSource();
        routing.setTargetDataSources(Map.of(
            ReplicaRoutingDataSource.WRITE, write,
            ReplicaRoutingDataSource.READ,  read));
        routing.setDefaultTargetDataSource(write);
        return routing;
    }

    @Primary
    @Bean
    public DataSource primaryRoutingDataSource(
        @Qualifier(&quot;routingDataSource&quot;) DataSource routing
    ) {
        return new LazyConnectionDataSourceProxy(routing);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 기존 메서드들 중 내부에서 쓰기가 일어나지 않는 일반 조회 메서드를 모두 Slave에서 조회하도록 전환했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  회고&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Keep&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 큰 사고였음에도 빠르게 문제를 파악하고 순차적으로 배포하며 에러를 해결했다. 또한 CloudWatch, Replica 모두 미루지 않고 빠르게 도입한 것은 잘한 일이라고 생각한다. 이번 이슈 이후로 데이터 로딩까지 5s 이상 걸리던 것들이 대부분 50% 이상 개선되었고, 1s 내로 줄어든 페이지도 생겼다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Problem&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Replica를 미루지 않고 도입한 것은 좋았지만, 67개 서비스 중 일부 서비스의 2개 메서드에서 내부적으로 Update가 일어나는 부분을 놓쳐 에러가 발생했었다. 읽기 전용 트랜잭션이 복제본으로 라우팅되는데, 그 안에서 쓰기를 시도하니 복제본이 거부한 것이다. 아직 테이블 구조나 서비스 플로우에 완벽하게 익숙하지 않기에 발생한 실수로 본다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Try&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 사용되는 메서드들은 서비스 코드 베이스를 더 깊게 들여다봐야 할 것 같다. 특히 &lt;code&gt;readOnly&lt;/code&gt; 트랜잭션 안에서 쓰기가 숨어 있는 경로를 사전에 걸러내는 감사 습관이 필요할 것 같다.&lt;/p&gt;</description>
      <category>Spring/트러블 슈팅</category>
      <category>RDS Repilica</category>
      <category>장애 대응</category>
      <author>Jwhy</author>
      <guid isPermaLink="true">https://jwhy-study.tistory.com/151</guid>
      <comments>https://jwhy-study.tistory.com/151#entry151comment</comments>
      <pubDate>Sat, 4 Jul 2026 17:58:16 +0900</pubDate>
    </item>
    <item>
      <title>[AI] Gemini CLI - 오픈 소스 기여 방법</title>
      <link>https://jwhy-study.tistory.com/149</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  개요&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;47&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oCIJc/dJMb997jBKS/Y5WumKEPDUtM8QrsacMnx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oCIJc/dJMb997jBKS/Y5WumKEPDUtM8QrsacMnx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oCIJc/dJMb997jBKS/Y5WumKEPDUtM8QrsacMnx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoCIJc%2FdJMb997jBKS%2FY5WumKEPDUtM8QrsacMnx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;662&quot; height=&quot;47&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;47&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini CLI를 사용하다 보면 커맨드 이름 충돌이 발생하는 경우 경고 알림을 띄워준다. 충돌을 해결하려면 어떤 이름들이 이미 존재하는지 확인해야 하는데, 정작 모든 커맨드 목록을 한눈에 볼 수 있는 기능이 없었다. 그래서 직접 &lt;code&gt;/commands list&lt;/code&gt; 서브커맨드를 만들어서 기여해보기로 했고, 그 과정을 한 번 정리해보려고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  기여 방법&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확한 기여 방법은 &lt;a href=&quot;https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md&quot;&gt;공식 문서&lt;/a&gt;와 &lt;a href=&quot;https://github.com/google-gemini/gemini-cli/discussions/16706&quot;&gt;가이드라인 개선&lt;/a&gt;에 상세히 나와있습니다. 이 포스팅은 2026년 4월 9일 기준으로 작성하였으며, 기여 방법은 언제든 바뀔 수 있으니 참고용으로만 봐주시면 감사하겠습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름을 먼저 보자면 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;CLA 서명&lt;/li&gt;
&lt;li&gt;Issue 작성&lt;/li&gt;
&lt;li&gt;리포지토리 Fork&lt;/li&gt;
&lt;li&gt;기능 개발&lt;/li&gt;
&lt;li&gt;Pull Request 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. CLA 서명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, &lt;a href=&quot;https://cla.developers.google.com/about&quot;&gt;Google Open Source CLA&lt;/a&gt;에 들어가서 서명을 해야한다. CLA란 Contributor License Agreement를 의미하며, Google 오픈소스 프로젝트에 기여할 때 Google이 해당 기여물을 법적으로 사용 및 배포할 수 있도록 허가하는 계약이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 잘 만든 PR이라도 CLA 서명이 없으면 리뷰 자체가 진행되지 않거나 메인테이너가 CLA 서명을 해달라고 요청한다. CLA로 인해 첫 리뷰 기회를 놓칠 수 있으니 가장 먼저 처리하는게 좋다. 실제로 여러 PR을 돌아다니다 보면 CLA 미서명으로 묻혀있는 PR이 여럿 있는 걸 볼 수 있으니 빠르게 작성하는게 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 이슈 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Issues 탭에서 New issue 버튼을 누르면 아래와 같이 어떤 이슈를 생성할 것인지에 대한 분류가 나온다. 본인이 기여하고자 하는 카테고리를 클릭하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1160&quot; data-origin-height=&quot;637&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/odFiQ/dJMcadIBmoY/TK3uf1z0GC5PDkRz2emc20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/odFiQ/dJMcadIBmoY/TK3uf1z0GC5PDkRz2emc20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/odFiQ/dJMcadIBmoY/TK3uf1z0GC5PDkRz2emc20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FodFiQ%2FdJMcadIBmoY%2FTK3uf1z0GC5PDkRz2emc20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1160&quot; height=&quot;637&quot; data-origin-width=&quot;1160&quot; data-origin-height=&quot;637&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈를 구체적으로 작성해서 올리면, &lt;code&gt;need-triage&lt;/code&gt;라는 미분류 라벨이 붙게 된다. 이는 메인테이너가 해당 이슈를 살펴보고 내용이 부실하거나 방향이 맞지 않으면 &lt;code&gt;not planned&lt;/code&gt;로 Close가 되고, 가치 있다고 판단되면 &lt;code&gt;help-wanted&lt;/code&gt;를 달아준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점이 있다. &lt;code&gt;help-wanted&lt;/code&gt; 라벨이 달렸을 때, 다른 사람이 &lt;code&gt;/assign&lt;/code&gt; 코멘트를 달아버리면, 그 사람이 이 이슈를 가져갈 수 있게 된다. 때문에, 선빵필승 느낌으로 바로 &lt;code&gt;/assign&lt;/code&gt;을 달거나 이미 작업을 완료해서 &lt;code&gt;help-wanted&lt;/code&gt; 라벨을 기다리고 있다는 코멘트를 달아놓으면 좋다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 경우 이슈를 올리자마자 PR을 함께 올렸지만 큰 문제는 없었다. 하지만 Close 처리될 가능성이 있으니 바로 작업하지 않는 것이 좋다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이슈에 &lt;code&gt;gemini-cli&lt;/code&gt; 봇이 Found possible duplicate issues 코멘트와 함께 다른 이슈 목록을 보여줄 경우, 차분하게 분석해보면 된다. 태그된 이슈들이 정말 내가 해결하고자 하는 이슈와 동일한지 확인하고, 다를 경우 코멘트에 남겨놓으면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/evlSnm/dJMcaf0JIDE/0J4CeLPU13lmHkLtaI9UE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/evlSnm/dJMcaf0JIDE/0J4CeLPU13lmHkLtaI9UE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/evlSnm/dJMcaf0JIDE/0J4CeLPU13lmHkLtaI9UE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FevlSnm%2FdJMcaf0JIDE%2F0J4CeLPU13lmHkLtaI9UE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;787&quot; height=&quot;193&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;193&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Fork&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 리포지토리에서 Fork하면 된다. Fork한 리포지토리에서 통합 테스트까지 돌리려면 GitHub Repository Secret에 &lt;code&gt;GEMINI_API_KEY&lt;/code&gt;를 등록하고, Actions 탭에서 워크플로우를 활성화해야 한다. 하지만 필수는 아니기에 그냥 Fork만 해도 문제 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 기능 개발&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;브랜치 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에 Fork한 리포지토리를 로컬 환경에서 받아온 뒤 브랜치를 생성한다. 이름에 별도 규칙은 없지만, 어떤 작업인지 한눈에 파악할 수 있도록 작성하면 충분하다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;git checkout -b TYPE/SUMMARY
# 예: feat/add-commands-list, fix/oauth-timeout-leak&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;작업 진행&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에도 명시되어 있듯이, 하나의 PR에는 하나의 변경사항만 담는 것이 원칙이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;We favor small, atomic PRs that address a single issue or add a single, self-contained feature.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버그 수정과 기능 추가를 한 브랜치에 함께 담으면 메인테이너 입장에서 리뷰 부담이 커진다. 변경 범위는 &lt;code&gt;/packages&lt;/code&gt; 디렉터리 내에서 최대한 좁게 유지해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 노출되는 변경(새 커맨드 추가, 플래그 수정 등)이 있다면 &lt;code&gt;/docs&lt;/code&gt; 디렉터리의 관련 문서도 함께 수정해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;커밋&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 메시지는 &lt;a href=&quot;https://www.conventionalcommits.org/&quot;&gt;Conventional Commits&lt;/a&gt; 컨벤션을 따른다.&lt;/p&gt;
&lt;pre class=&quot;ldif&quot;&gt;&lt;code&gt;# 기본 형식
feat: 새 기능 추가
fix: 버그 수정

# scope 포함 (선택)
feat(cli): 사용자 인증 엔드포인트 추가
fix(core): 배열 파싱 오류 수정&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;검증 및 테스트&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 테스트에서 조금 애를 먹었다. 내가 수정하지 않은 파일인데 테스트 실패가 뜨는 경우가 분명 있을 것이다. main 브랜치에서의 테스트 결과를 먼저 저장해두고 내 브랜치 결과와 비교하면 구분하기 훨씬 쉽다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR 제출 전 아래 순서로 검증한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# 1. 로컬에서 직접 실행해보기
npm start

# 2. 단위 테스트 확인
npm run test

# 3. 최종 검증 (lint + prettier + 전체 테스트)
npm run preflight&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;preflight&lt;/code&gt;는 ESLint, Prettier, 테스트를 한 번에 돌리기 때문에 느린 편이다. 테스트 실패가 있는 상태에서 돌리면 중간에 끊겨서 다른 문제를 같이 못 보는 경우도 생긴다. 테스트를 먼저 통과시킨 뒤 &lt;code&gt;preflight&lt;/code&gt;로 최종 확인하는 순서가 효율적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Pull Request&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fork한 리포지토리의 &lt;code&gt;Pull requests&lt;/code&gt;에서 &lt;code&gt;New pull request&lt;/code&gt;를 눌러, 작업한 브랜치의 내용이 원본 레포의 &lt;code&gt;main&lt;/code&gt;으로 향하도록 설정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doPAMU/dJMcafzH7sn/Ge5gz0DkGF5vpCFFn2PJy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doPAMU/dJMcafzH7sn/Ge5gz0DkGF5vpCFFn2PJy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doPAMU/dJMcafzH7sn/Ge5gz0DkGF5vpCFFn2PJy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoPAMU%2FdJMcafzH7sn%2FGe5gz0DkGF5vpCFFn2PJy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1050&quot; height=&quot;156&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR 제목과 내용을 작성할 때는 다음 항목을 반드시 체크해야 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;처리하는 이슈를 &lt;code&gt;Fixes #이슈번호&lt;/code&gt; 형태로 명시한다.&lt;/li&gt;
&lt;li&gt;하나의 이슈, 하나의 기능만 담겼는지 점검한다.&lt;/li&gt;
&lt;li&gt;어떤 환경에서 테스트했는지 체크리스트를 채운다.&lt;/li&gt;
&lt;li&gt;문서 수정이 필요한 경우 변경 사항에 포함되어 있는지 확인한다.&lt;/li&gt;
&lt;li&gt;PR 제목이 Conventional Commits 형식을 따르는지 확인한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR을 올리면 &lt;code&gt;gemini-code-assist&lt;/code&gt; 봇에 의해 두 가지 이벤트가 발생한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;변경 사항(Summary of Changes)을 요약&lt;/li&gt;
&lt;li&gt;코드 리뷰&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰 피드백에는 우선 순위도 함께 작성해주는데, 우선 순위가 높은 것들을 먼저 수정한 뒤 &lt;code&gt;push&lt;/code&gt;까지 진행을 해두는게 좋다. 모든 반영이 끝나면, PR 코멘트에 &lt;code&gt;/gemini review&lt;/code&gt;를 남겨서 리뷰를 또 받아보는게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 거쳐서 리뷰에 큰 문제가 없으면, 깃허브 어플 알림이나 메일 등을 계속 살펴보면서 메인테이너가 내 PR에 관심을 가져줄 때까지 무한 대기를 하면 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 관심 받는데 거의 한 달이라는 시간이 걸렸다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인테이너가 리뷰를 달아주면, 그 기회를 놓치지 않고 코멘트를 남겨서 아직 여기 사람이 있다는 것을 어필해야 한다. 나의 경우 &quot;확인해줘서 고맙다&quot;, &quot;좋은 제안 감사하다&quot; 이런 내용의 시덥잖은 코멘트를 계속 남겼고, 그 이후로 꾸준히 리뷰도 받고, CI 과정까지 도달할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해보니 PR의 뒷 페이지부터 순차적으로 처리하고 있기 때문에 가장 최근 이슈나 PR의 응답은 꽤 걸리는 것 같다. 그렇게 메인테이너의 승인 처리를 받으면 다음 사진과 같이 CI 과정이 동작하게 되고, 첫 기여자의 경우 docs-pr-check 만 대기 상태로 남게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;746&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IXpow/dJMcac30T7e/xWcwC4uQaPZBcHlIz96zRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IXpow/dJMcac30T7e/xWcwC4uQaPZBcHlIz96zRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IXpow/dJMcac30T7e/xWcwC4uQaPZBcHlIz96zRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIXpow%2FdJMcac30T7e%2FxWcwC4uQaPZBcHlIz96zRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;746&quot; height=&quot;536&quot; data-origin-width=&quot;746&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음이다보니 메인테이너에게 &lt;code&gt;docs-pr-check&lt;/code&gt;는 어떻게 처리해야하는지 여쭤보니 &lt;code&gt;gemini-cli-docs&lt;/code&gt; 팀에 리뷰 요청을 보냈다는 답변이 돌아왔다. 즉, 기다리면 된다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속 기다리다보면, 메인테이너가 Merge Queue에 넣어준다. 만약 브랜치 충돌이 생기는 경우 바로 해결하면 되는데, &lt;code&gt;main&lt;/code&gt; 브랜치가 업데이트 되었으니 너가 만든 브랜치도 업데이트하라고 뜨면 그냥 무시하면 된다. 괜히 수정하면 CI가 초기화되니까 그냥 냅두고 기다리면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커맨드 목록을 보여주는 작은 기능 하나를 추가하는 데 거의 1.5개월 정도가 걸렸다. 이슈를 올리고 2주가 지나도록 아무 반응이 없어 &lt;code&gt;gemini-cli&lt;/code&gt; 봇이 PR을 자동으로 닫아버리는 상황도 겪었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 늦게나마 메인테이너가 이슈를 확인해주었고, &quot;사용자 가시성 향상에 중요한 가치를 하는 작은 기능&quot;이라는 평가를 받았다. 계속 진행할 의향이 있는지 묻는 코멘트를 달아주셔서, 새벽 4시에 급하게 답변을 달아 바로 Reopen 처리를 받을 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/orbJf/dJMcacwd39B/X1husKEQ19LOHwZ2FmRULK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/orbJf/dJMcacwd39B/X1husKEQ19LOHwZ2FmRULK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/orbJf/dJMcacwd39B/X1husKEQ19LOHwZ2FmRULK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2ForbJf%2FdJMcacwd39B%2FX1husKEQ19LOHwZ2FmRULK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;789&quot; height=&quot;224&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;224&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 꽤 걸린 작업이었지만, 일상의 불편함에서 시작한 작은 아이디어가 실제로 가치 있는 기여로 이어질 수 있다는 걸 몸으로 배웠다.&lt;/p&gt;</description>
      <category>Devlog/AI</category>
      <author>Jwhy</author>
      <guid isPermaLink="true">https://jwhy-study.tistory.com/149</guid>
      <comments>https://jwhy-study.tistory.com/149#entry149comment</comments>
      <pubDate>Sat, 2 May 2026 20:45:07 +0900</pubDate>
    </item>
    <item>
      <title>[AI] Gemini CLI - Conductor Extension, Plan Mode</title>
      <link>https://jwhy-study.tistory.com/147</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ 개발 환경&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Gemini CLI&lt;/b&gt;: v0.33.0 이상&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  업데이트 소개&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.googleblog.com/plan-mode-now-available-in-gemini-cli/&quot;&gt;Plan mode is now available in Gemini CLI&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 Agent를 써본 개발자라면 익숙한 &lt;b&gt;계획 모드&lt;/b&gt;가 드디어 Gemini CLI에도 정식으로 들어왔다. 솔직히 옆동네는 이미 한참 전부터 지원했는데 이제야 추가됐다는 건 좀 아쉬운 부분이다. 개인적으로 이 기능이 너무 부러워서 &lt;code&gt;/plan&lt;/code&gt; 커맨드를 직접 만들어서 쓰고 있었는데, 이제 공식 지원이 생겼으니 그 커맨드는 폐기 처리해야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 글을 읽으면서 충격적인 사실을 하나 알게 됐다. 내가 직접 만들어서 쓰던 &lt;code&gt;/init&lt;/code&gt;, &lt;code&gt;/plan&lt;/code&gt;, &lt;code&gt;/work&lt;/code&gt; 같은 커맨드들이 이미 공식 &lt;a href=&quot;https://github.com/gemini-cli-extensions/conductor&quot;&gt;Extension&lt;/a&gt;으로 존재했던 것이다. 개발자로서 이런 기술 소식을 제때 챙기지 못했다는 게 솔직히 좀 부끄럽긴 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 그래서 내가 몰랐던 &lt;b&gt;Conductor&lt;/b&gt;와, 이번에 공식으로 추가된 &lt;b&gt;Plan Mode&lt;/b&gt;, 이 두 가지를 함께 정리해보려고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conductor&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 12월 17일에 &lt;a href=&quot;https://developers.googleblog.com/conductor-introducing-context-driven-development-for-gemini-cli/&quot;&gt;Conductor: Introducing context-driven development for Gemini CLI&lt;/a&gt; 글이 올라왔다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시에는 AI Agent 자체를 잘 사용하지 않았어서 지나쳤는데, 지금 와서 보니까 내가 만들었던 커맨드들의 완성형이 이미 여기 있었다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Conductor의 핵심 철학은 &quot;&lt;b&gt;컨텍스트를 채팅창 밖으로 꺼내라&lt;/b&gt;&quot;**는 것이다. 즉, 대화 기록에 의존하는 대신, 프로젝트의 스펙과 계획을 프로젝트 내부에 직접 저장을 하는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini CLI가 설치된 환경이라면 다음 커맨드만 입력하면 끝난다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;gemini extensions install https://github.com/gemini-cli-extensions/conductor&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Conductor에서 지원하는 커맨드는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/conductor:status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/conductor:setup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/conductor:review&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/conductor:revert&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/conductor:newTrack&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/conductor:implement&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주요 기능들만 알아보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;setup&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 기반 컨텍스트를 정의한다. 이 프로젝트가 어떤 제품인지, 기술 스택은 무엇인지 등 프로젝트에 대한 정보를 미리 주입해두는 것이다. 한 번 세팅해두면 이후 작업 시 해당 문서를 참고해서 개발에 필요한 맥락을 빠르게 파악할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;newTrack&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 작업 단위(Track)를 시작한다. 코드를 바로 작성하는 게 아니라, 먼저 Spec(무엇을 왜 만드는가)과 Plan(단계별 실행 목록)을 작성한다. &lt;code&gt;setup&lt;/code&gt;을 먼저 진행한 상태라면 기존 컨텍스트를 바탕으로 초안을 제안해줘서 작성 속도가 훨씬 빠르다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;implement&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;newTrack&lt;/code&gt;으로 수립된 계획을 실제로 구현한다. 생성된 계획 문서를 토대로 태스크를 하나씩 체크해가며 진행하고, 중간에 멈춰도 진행 상태가 파일에 그대로 남아있어 컨텍스트를 이어받아 계속할 수 있다. 또한 새로운 기능이 추가되면 기존에 작성된 프로젝트 문서도 함께 업데이트해준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Plan mode&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 Conductor의 &lt;code&gt;newTrack&lt;/code&gt;과 유사한 기능이라고 보면 된다. 가장 큰 차이는 Conductor는 별도로 설치해야 하는 Extension인 반면, Plan Mode는 Gemini CLI에 기본으로 내장된 공식 기능이라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 특징은 &lt;b&gt;인터뷰 기반으로 동작한다&lt;/b&gt;는 것이다. 내가 무엇을 원하는지, 어떻게 개발해야 하는지를 처음부터 구체적으로 설명하기란 생각보다 훨씬 어렵다. 현실적으로 우리가 가장 많이 치는 프롬프트는 이 정도일 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;~에서 버그가 발생하고 있어. 수정해줘.&quot;&lt;/li&gt;
&lt;li&gt;&quot;~기능 만들어줘.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그나마 더 구체적으로 전달하려면 또 다른 AI한테 &quot;~ 내용을 Gemini CLI에 넘길 프롬프트로 만들어줘&quot;를 시키는 상황이 된다. 결국 이런 사이클이 반복된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;에러 발생, Agent에게 수정 요청&lt;/li&gt;
&lt;li&gt;추가 에러 발생 또는 의도와 다른 구현&lt;/li&gt;
&lt;li&gt;다시 수정 요청&lt;/li&gt;
&lt;li&gt;1번으로 복귀&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 &quot;&lt;b&gt;내가 아무리 개떡같이 말해도 너는 알아서 이해해주길 바라&lt;/b&gt;&quot;라는 마인드로 질문을 한다. 그래서 기능 개발보다 수정하는 시간이 훨씬 길어진다. Plan Mode는 이 사이클을 끊어내기 위해 사용하는 것이다. &lt;b&gt;&quot;코드 수정하기 전에 네가 뭘 할 건지 먼저 말해봐&quot;&lt;/b&gt; 라고 먼저 확인하는 것만으로도 방향이 틀어지는 빈도가 눈에 띄게 줄어들기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, Gemini CLI를 최신 버전으로 업데이트 해준다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;brew upgrade gemini-cli&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Gemini CLI에 접속해서 &lt;code&gt;Shift + Tab&lt;/code&gt;을 누르면 된다. 한 번 누르면 &lt;code&gt;auto-accept edits&lt;/code&gt; 모드가 되고, 한 번 더 누르면 &lt;code&gt;plan&lt;/code&gt; 모드로 전환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정은 &lt;code&gt;/settings&lt;/code&gt;에서 조정할 수 있다. 항상 계획 중심으로 접근하고 싶다면 Plan을 기본값으로 고정하면 되고, 빠른 편집 중심 워크플로우를 선호한다면 완전히 꺼버릴 수도 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Plan Mode를 켠 상태에서 &quot;~에러가 발생해. 수정해줘.&quot; 정도로만 입력하면, Gemini가 맥락을 파악하기 위해 &lt;code&gt;ask_user&lt;/code&gt; 도구로 인터뷰를 시작한다. 질문에 맞게 선택하거나 답을 입력하면, 아래와 같은 형태로 계획서를 만들어준다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Plan

CodeReviewController.kt 수정 계획:
1. ...

CodeReviewService.kt 수정 계획:
1. ...

이 계획대로 실행하겠습니다. 진행할까요? (다음 단계로 Execute를 곧바로 수행하겠습니다.)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 확인을 해줘야 비로소 코드 수정 단계로 넘어간다. AI가 혼자 추측해서 달려나가는 게 아니라, &lt;b&gt;내가 확인해준 내용을 바탕으로 계획을 완성&lt;/b&gt;하는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로, 외부 MCP 도구도 읽기 전용으로 연동된다. GitHub 이슈, PostgreSQL 스키마, Google Docs 같은 외부 컨텍스트도 계획 단계에서 안전하게 가져올 수 있어서, 로컬 코드 바깥의 맥락까지 계획에 포함할 수 있다는 게 의외의 강점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시라도 계획 수립 중에 파일이 수정되는 걸 걱정할 필요는 없다. Plan Mode에서는 &lt;b&gt;읽기 전용 MCP 도구만 활성화&lt;/b&gt;되기 때문에, 파일 수정은 애초에 불가능하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떻게 사용하는게 좋을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트 문서에 이런 내용이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;For more complex projects, Conductor may be a good fit. The Conductor for Gemini CLI will take full advantage of both &lt;b&gt;plan mode&lt;/b&gt; and the &lt;b&gt;&lt;code&gt;ask_user&lt;/code&gt;&lt;/b&gt; tool.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 복잡한 프로젝트에서는 Conductor를 사용하는 게 더 적합하고, 이 경우 Plan Mode와 &lt;code&gt;ask_user&lt;/code&gt; 도구까지 함께 활용하면 더 좋은 결과를 얻을 수 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 &lt;code&gt;/conductor:setup&lt;/code&gt; 이후에 Plan Mode 활성화 여부를 안내하는 문구가 등장하는 것도 이 맥락이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img-19.png&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHN6Hw/dJMcagLxrYK/1XhdJ2B0QAinfjkZTim6Zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHN6Hw/dJMcagLxrYK/1XhdJ2B0QAinfjkZTim6Zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHN6Hw/dJMcagLxrYK/1XhdJ2B0QAinfjkZTim6Zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHN6Hw%2FdJMcagLxrYK%2F1XhdJ2B0QAinfjkZTim6Zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;604&quot; height=&quot;382&quot; data-filename=&quot;img-19.png&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, 단순한 작업이라면 Plan Mode만으로도 충분하고, 규모가 크거나 레거시 코드베이스를 다루는 상황이라면 Conductor와 함께 쓰는 게 맞는 방향으로 보인다. 두 기능을 같이 써본 경험은 아직 많지 않아서, 충분히 사용해본 뒤에 따로 후기를 남겨볼 예정이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포스팅까지 올리면서 직접 만들었던 커맨드들이 이미 익스텐션으로 존재했다는 걸 뒤늦게 알았을 때, 솔직히 좀 허탈했다. 공식 문서와 업데이트 소식을 꼼꼼히 챙기지 못한 내 탓이니 어쩔 수 없다. 앞으로는 트렌드를 좀 더 부지런히 따라가야겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 얻은 것은 있다. MCP를 직접 적용해보고, 커스텀 커맨드가 어떻게 동작하는지 손으로 익힌 경험이 오히려 Conductor와 Plan Mode를 이해하는 데 도움이 됐다. 결국 직접 만들어봤기 때문에 이 기능들이 왜 이렇게 설계됐는지 더 잘 보이는 것 같기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 Plan Mode를 기본 진입점으로 두고 작업을 시작할 생각이다. 복잡한 작업에서 Conductor와 함께 쓰는 흐름도 익숙해지면 그때 다시 정리해보겠다.&lt;/p&gt;</description>
      <category>Devlog/AI</category>
      <category>Gemini CLI Conductor</category>
      <category>Gemini CLI Plan Mode</category>
      <category>Gemini CLI 계획 수립</category>
      <category>Gemini CLI 업데이트</category>
      <author>Jwhy</author>
      <guid isPermaLink="true">https://jwhy-study.tistory.com/147</guid>
      <comments>https://jwhy-study.tistory.com/147#entry147comment</comments>
      <pubDate>Sat, 14 Mar 2026 01:51:00 +0900</pubDate>
    </item>
    <item>
      <title>[AI] 무신사 AI Native Engineer 채용 회고</title>
      <link>https://jwhy-study.tistory.com/146</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  회고 주제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 달 전, 무신사의 AI Native Engineering 공고를 보고 지원하게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LcLgc/dJMcaaxKU5H/RdyuEBHJQPHasddp62vXik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LcLgc/dJMcaaxKU5H/RdyuEBHJQPHasddp62vXik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LcLgc/dJMcaaxKU5H/RdyuEBHJQPHasddp62vXik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLcLgc%2FdJMcaaxKU5H%2FRdyuEBHJQPHasddp62vXik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;684&quot; height=&quot;320&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qjxn7/dJMcahjmKn3/zxuiX0qRt8K2mM1j8MhEC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qjxn7/dJMcahjmKn3/zxuiX0qRt8K2mM1j8MhEC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qjxn7/dJMcahjmKn3/zxuiX0qRt8K2mM1j8MhEC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQjxn7%2FdJMcahjmKn3%2FzxuiX0qRt8K2mM1j8MhEC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;685&quot; height=&quot;236&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tvKzl/dJMcahQ98Qh/xKteY9GC8rcFZRHjKpNIr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tvKzl/dJMcahQ98Qh/xKteY9GC8rcFZRHjKpNIr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tvKzl/dJMcahQ98Qh/xKteY9GC8rcFZRHjKpNIr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtvKzl%2FdJMcahQ98Qh%2FxKteY9GC8rcFZRHjKpNIr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;670&quot; height=&quot;277&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 테스트까지 볼 수 있는 기회를 얻었지만, 결과적으로는 불합격했다. 이전 회사에서 경영난으로 권고사직을 받은 뒤, 새롭게 이직을 준비하면서 'AI가 코드를 짜는 시대에 백엔드 엔지니어로서 어떤 경쟁력을 가져야 할까?'라는 고민이 계속 머릿속을 맴돌아, 이력서를 수도 없이 수정했다. 그런 시기에 오랜만에 서류부터 코딩 테스트까지 밟아볼 수 있었기에, 결과와 무관하게 나에게는 값진 경험이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실, 회고를 해야겠다는 생각 자체를 잊고 있었는데, 최근에 EO Korea 채널에 올라온 &lt;a href=&quot;https://youtu.be/qEF-eUaTq0Y&quot;&gt;스탠포드에서 빅테크로 조용히 퍼지는 개발자 생존법&lt;/a&gt; 영상을 보고 2차 테스트에서 내가 놓쳤던 것들과 겹쳐 보이면서 생각이 났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 채용 후기가 아닌 회고이다. 무신사의 AI Native Engineer 2차 테스트 경험을 통해, 주니어 개발자로서 내가 AI 시대에 어떻게 나아가야 할지 톺아보도록 하자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  영상 요약&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtu.be/qEF-eUaTq0Y&quot;&gt;EO Korea - 스탠포드에서 빅테크로 조용히 퍼지는 개발자 생존법&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 시대에 주목받는 엔지니어상이 바뀌고 있다. 단순히 코드를 잘 짜는 것을 넘어, 다수의 AI Agent를 능숙하게 오케스트레이션하는 역량이 새로운 채용 기준이 되고 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 잘 하려면 두 가지가 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Agent들의 작업 흐름을 설계하는 제어 전략(Multi-Agent Workflow)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;점진적인 에이전트 도입&lt;/li&gt;
&lt;li&gt;컨텍스트 스위칭 전략&lt;/li&gt;
&lt;li&gt;훌륭한 관리자의 자질&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Agent가 혼란 없이 코드를 읽고 기여할 수 있는 코드베이스(Agent-Friendly Codebase)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명확한 컨트랙트(테스트) 제공&lt;/li&gt;
&lt;li&gt;문서와 코드의 일관성&lt;/li&gt;
&lt;li&gt;일관된 디자인 패턴&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흥미로운 건 이 변화가 주니어에게 유리하다는 것이다. 기존 방식에 고착되지 않은 주니어는 AI 워크플로우를 빠르게 실험하고, 수용할 수 있어 스타트업 생태계에서 오히려 강점이 된다고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  나의 문제점 분석 및 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 테스트는 단일 AI Agent(Gemini CLI)를 활용해 진행했다. 비록 다중 Agent를 동시에 다루지는 않았지만, 영상에서 강조된 AI 네이티브 엔지니어의 핵심 역량인 &lt;b&gt;'워크플로우 설계'&lt;/b&gt; 와 &lt;b&gt;'Agent-Friendly 코드베이스 구축'&lt;/b&gt; 관점에서, 나는 이 두 가지 역량 모두 부족했다. 하나씩 톺아보도록 하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 시스템 설계 위임과 컨텍스트 과부하&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  분석&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;표면적인 문제&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GEMINI.md&lt;/code&gt; 작성에 많은 시간을 투자했지만, 개발물의 품질과 개발 효율이 낮았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왜? &amp;rarr; 요구사항(&lt;code&gt;PROBLEM.md&lt;/code&gt;) 문서 분석을 내가 하지 않고, AI에게 전적으로 위임했다.&lt;/li&gt;
&lt;li&gt;왜? &amp;rarr; 제한된 시간 내에 빠르게 구현 단계로 넘어가야 한다는 압박감과 조급함이 있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;근본 원인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 설계와 컨텍스트 압축은 AI가 아닌 개발자의 영역이다. 영상에서 지적한 것처럼, &lt;b&gt;Agent는 문서와 코드가 불일치하거나 모호할 때, 사소한 오해를 바탕으로 빠르게 오류를 증폭&lt;/b&gt;시킨다. 즉, 작업을 지시하기 전, &lt;b&gt;명확한 도메인과 제약을 정의하는 시스템 설계자 역할&lt;/b&gt;을 수행하지 못했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;↩️ Before&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛 말에 틀린게 없다. &quot;바쁠수록 돌아가라&quot;라는 말을 간과했다. 초기 분석 없이 조급하게 AI가 도출한 제약 사항을 그대로 &lt;code&gt;GEMINI.md&lt;/code&gt;에 추가했다. 결과적으로 문서의 컨텍스트는 비대해졌고, 불필요하게 무거워진 제약사항과 핵심 비즈니스 로직 사이에서 Agent는 맥락을 잃기 시작했다. 이는 테스트 하네스 오류와 잘못된 코드 생성으로 이어졌다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;↪️ After&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GEMINI.md&lt;/code&gt;의 역할을 재정의해야 한다. 많은 정보를 주입하는 것보다, 오해 없이 실행할 수 있는 최소한의 계약서가 되어야 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;요구 사항을 내가 먼저 완독하고 핵심 요구사항을 직접 정리한다.&lt;/li&gt;
&lt;li&gt;그 중 Agent가 반드시 알아야 할 최소한의 내용만 &lt;code&gt;GEMINI.md&lt;/code&gt;에 넣는다.&lt;/li&gt;
&lt;li&gt;많을수록 좋은 게 아니라, 정확할수록 좋다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 검증 불가능한 모호한 워크플로우 분리&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  분석&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;표면적인 문제&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프라인을 Step 1 ~ 4로 나누었지만, 각 단계의 책임과 검증 기준이 크고 모호했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왜? &amp;rarr; 단계를 나누는 것 자체가 목적이 되어, 가장 중요한 완료 기준을 정의하지 않았다.&lt;/li&gt;
&lt;li&gt;왜? &amp;rarr; 단계를 잘 나누기만 하면, Agent가 스스로 잘 소화할 것이라고 과신했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;근본 원인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상에서 지적한 &lt;b&gt;점진적 오케스트레이션&lt;/b&gt; 원칙을 위배했다. 단일 작업의 완전한 수행과 검증 없이 다음 단계로 넘어갔기 때문에, 어디서부터 논리가 어긋났는지 추적 자체가 어려워졌다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;↩️ Before&lt;/h4&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;## Step 1: Prompt Log Generation (`prompts/`)
## Step 2: API Documentation Generation (`docs/`)
## Step 3: Code Implementation (`src/main/`)
## Step 4: Test Implementation (`src/test/`)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉보기에는 체계적으로 보이지만, 각 단계의 완료 기준이 없는 구조였다. 파일 생성이나 단순 컴파일 성공만으로는 비즈니스 요구사항 충족을 보장할 수 없다. 특히 코드를 먼저 작성하고 테스트를 나중에 구현하는 방식은, 영상에서 말한 '&lt;b&gt;테스트를 통한 명확한 컨트랙트 제공&lt;/b&gt;'에 어긋나며 Agent의 논리적 오류를 사전에 차단하지 못한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;↪️ After&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계에 명확한 검증 사이클을 통합해야 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;각 단계에서 완수해야 할 단일 기능과 범위 명시&lt;/li&gt;
&lt;li&gt;테스트 통과, 엔드포인트 응답 확인 등 검증 가능한 기준 설정&lt;/li&gt;
&lt;li&gt;다음 단계로 컨텍스트를 전환하기 전, 개발자가 직접 코드와 논리를 리뷰하는 체크포인트 확보&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Agent가 단일 작업을 완벽히 수행했는지 검증한 뒤 다음으로 넘어가는 것. 그것이 점진적 위임이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 테스트(2월 8일) 이후 약 한 달의 시간이 흘렀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 직후, 무엇이 부족했는지 바로 회고를 했다. 불합격 결과를 받고 이 시점에서 다시 읽어보니, 한 달 사이에 AI에 대하여 기술적으로나 방법론적으로 유의미한 변화가 있었음을 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 변화는 막연했던 AI 활용을 구조화했다는 점이다. &lt;code&gt;oh-my-opencode&lt;/code&gt;를 통해 Multi-Agent Orchestration 환경을 직접 경험했고, 이 과정에서 Single-Agent 환경에서도 적용할 수 있는 나만의 프로세스를 수립했다. 이 과정을 통해 Single-Agent 환경에서도 구조적으로 사용할 수 있는 &lt;code&gt;/init&lt;/code&gt;, &lt;code&gt;/plan&lt;/code&gt;, &lt;code&gt;/work&lt;/code&gt; 프로세스를 수립하여 검증 가능하고, 통제 가능한 워크플로우로 발전시킬 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상에서 말한 것처럼, &lt;b&gt;정답이 없는 AI 생태계에서는 직접 부딪히며 자신만의 워크플로우를 구축하는 실험 정신&lt;/b&gt;이 핵심이다. 나는 불합격 이후 한 달 동안 그걸 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불합격이라는 결과는 바꿀 수 없다. 하지만 이 결과가 지난 한 달간 내가 경험한 것들의 출발점이 됐다는 건 분명하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주니어의 강점은 &lt;b&gt;기존 방식에 고착되지 않는다는 것&lt;/b&gt;이다. &lt;b&gt;실패한 방법론은 빠르게 버리고, 더 나은 기술과 프로세스를 스펀지처럼 흡수&lt;/b&gt;하는 것. 지금 내가 AI 네이티브 엔지니어로 나아가는 데 있어 가장 강력한 무기는 바로 그 유연함이라고 생각한다.&lt;/p&gt;</description>
      <category>Devlog/회고</category>
      <category>AI 개발자 테스트 회고</category>
      <category>무신사 AI Native Engineer</category>
      <category>성장형 개발자</category>
      <category>주니어 개발자</category>
      <category>채용 회고</category>
      <author>Jwhy</author>
      <guid isPermaLink="true">https://jwhy-study.tistory.com/146</guid>
      <comments>https://jwhy-study.tistory.com/146#entry146comment</comments>
      <pubDate>Thu, 12 Mar 2026 17:08:55 +0900</pubDate>
    </item>
    <item>
      <title>[AI] Gemini CLI를 더 알차게 사용해보자</title>
      <link>https://jwhy-study.tistory.com/145</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ 개발 환경&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;  OS: MacOS&lt;/li&gt;
&lt;li&gt;  Github: &lt;a href=&quot;https://www.google.com/search?q=https://github.com/Jwhyee/gemini-cli-commands&quot;&gt;gemini-cli-commands&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⚽️ 목표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가난한 취준생은 오늘도 Google AI Pro 플랜을 알차게 사용하기 위해 고군분투 중이다. 평소에는 &lt;code&gt;oh-my-opencode&lt;/code&gt;를 통해 기초 개발을 진행하다가, 토큰 제한에 걸리면 &lt;code&gt;Gemini CLI&lt;/code&gt;로 넘어오고, 시각적인 뷰를 보면서 개발해야 할 때는 Google Antigravity로 넘어가는 방식을 섞어가며 힘들게 개발을 이어가고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 반복하다 보니 문득 &amp;lsquo;Gemini CLI 안에서 &lt;code&gt;oh-my-opencode(OmO)&lt;/code&gt;의 워크플로우를 어떻게든 흉내 내볼 수 없을까?&amp;rsquo;라는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OmO처럼 구체적인 계획을 먼저 작성시키고, 그 계획을 토대로 코드를 짜게 만드는 방식이 너무 마음에 들었기 때문이다. 그렇게 내 목표는 OmO의 핵심인 &lt;code&gt;Prometheus(Plan Builder)&lt;/code&gt;와 &lt;code&gt;Atlas(Plan Executor)&lt;/code&gt;의 역할을 Gemini CLI라는 단일 환경에서 간접적으로나마 구현해 보는 것으로 잡혔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  의문과 증명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;oh-my-opencode&lt;/code&gt; 개발팀은 24,000달러(한화 약 3,500만 원)어치의 어마어마한 토큰을 태워가며 토큰 최적화와 정확도를 높이기 위해 엄청난 노력을 기울였다고 한다. 과연 무엇이 토큰을 절약하고 성능을 끌어올렸는지 먼저 살펴보자. 공식 &lt;code&gt;README.md&lt;/code&gt; 파일에는 다음과 같은 기능이 명시되어 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;&lt;code&gt;/init-deep&lt;/code&gt;을 통해 프로젝트 전체에 걸쳐 계층적인 &lt;code&gt;AGENTS.md&lt;/code&gt; 파일을 자동 생성하여 토큰 효율과 에이전트 성능을 동시에 잡는다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 에이전트가 코드를 짤 때마다 매번 프로젝트 전체를 풀 스캔하도록 시키는 것이 아닌, 요약된 핵심 지식 문서만 참조하게 만들어 불필요한 컨텍스트 로드를 막는 최적화 전략이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, '계획 수립(Prometheus) -&amp;gt; 개발 진행(Atlas)'으로 이어지는 &lt;b&gt;역할 분담 구조&lt;/b&gt;는 어떨까? 이에 대한 해답은 공식 GitHub의 &lt;a href=&quot;https://github.com/code-yeongyu/oh-my-opencode/issues/1826&quot;&gt;Issue #1826&lt;/a&gt;에서 명확히 드러난다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q. 사용자 문제 제기&lt;/b&gt;&lt;br /&gt;&quot;에이전트 역할 간의 정보 전달(Hand-off)은 지연 시간을 늘릴 뿐만 아니라 토큰 소모를 배가시키고 있습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A. 메인테이너 답변&lt;/b&gt;&lt;br /&gt;&quot;전적으로 동의합니다. 이 설계는 의도적인 트레이드오프입니다. OMO는 복잡한 개발 워크플로우를 타겟으로 하기에, 전문화로 얻는 이점(정확도)이 토큰 오버헤드를 능가한다고 판단했습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면, 작업을 쪼개서 각기 다른 모델에게 지시사항과 컨텍스트를 전달하는 과정 자체는 오히려 더 많은 토큰을 소모한다는 것을 개발진도 인정하고 있다. 비용을 더 지불하더라도 &lt;b&gt;압도적인 꼼꼼함과 정확도&lt;/b&gt;를 얻겠다는 의도적인 설계인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Gemini CLI와 같은 단일 에이전트 환경은 에이전트 간의 '정보 전달' 과정이 필요 없다. 세션을 통해 컨텍스트를 캐싱하기 때문이다. 따라서 연속적인 작업에서는 단일 에이전트 방식이 토큰 소모량 측면에서 훨씬 유리할 수 있다는 가설이 세워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 이 가설이 맞는지 테스트를 진행해 보았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;테스트1 (OmO 방식)&lt;/b&gt;: 프로젝트 구조 제공 -&amp;gt; 계획 수립(PLANNING 생성) -&amp;gt; 계획을 바탕으로 코드 개발&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트2 (단일 프롬프트 방식)&lt;/b&gt;: 다짜고짜 프롬프트만 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에 사용된 요구사항은 다음과 같이 아주 단순하게 던져보았다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task 등록 폼에서 '인박스 보관' 버튼 클릭 시 인박스에 추가되지 않는 버그가 있어. 수정해 줘.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트1(OmO 플로우)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img-14.png&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;709&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lN83b/dJMcagSdhIt/zOivyVBLrSRyB3BrCNEIf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lN83b/dJMcagSdhIt/zOivyVBLrSRyB3BrCNEIf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lN83b/dJMcagSdhIt/zOivyVBLrSRyB3BrCNEIf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlN83b%2FdJMcagSdhIt%2FzOivyVBLrSRyB3BrCNEIf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;920&quot; height=&quot;709&quot; data-filename=&quot;img-14.png&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;709&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트2(프롬프트만 전달)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img-15.png&quot; data-origin-width=&quot;1067&quot; data-origin-height=&quot;690&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diwtN7/dJMcafMypSQ/KvttP5pg3690g01dpu3d80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diwtN7/dJMcafMypSQ/KvttP5pg3690g01dpu3d80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diwtN7/dJMcafMypSQ/KvttP5pg3690g01dpu3d80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiwtN7%2FdJMcafMypSQ%2FKvttP5pg3690g01dpu3d80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1067&quot; height=&quot;690&quot; data-filename=&quot;img-15.png&quot; data-origin-width=&quot;1067&quot; data-origin-height=&quot;690&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 결과 정리&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;Reqs&lt;/th&gt;
&lt;th&gt;Input Tokens&lt;/th&gt;
&lt;th&gt;Cache Reads&lt;/th&gt;
&lt;th&gt;Output Tokens&lt;/th&gt;
&lt;th&gt;총 연산 비용 (USD)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;테스트1&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;147,672&lt;/td&gt;
&lt;td&gt;743,994&lt;/td&gt;
&lt;td&gt;5,535&lt;/td&gt;
&lt;td&gt;&lt;b&gt;약 $0.1276&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;테스트2&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;176,802&lt;/td&gt;
&lt;td&gt;922,909&lt;/td&gt;
&lt;td&gt;2,306&lt;/td&gt;
&lt;td&gt;&lt;b&gt;약 $0.1415&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트1에서 캐싱된 내용을 테스트2에서 일부 재사용했다는 아쉬운 점은 있지만, 그럼에도 &lt;b&gt;단순 지시를 내린 테스트2가 입력 토큰을 20%가량 더 소모&lt;/b&gt;했다는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용뿐만 아니라 작업의 질에서도 큰 차이가 났다. 테스트1은 필요한 파일만 정확히 찾아 깔끔하게 문제를 해결한 반면, 뚜렷한 계획표가 없었던 테스트2 방식은 다음과 같은 문제점들을 노출했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;하네스(Harness) 문제&lt;/b&gt;: 작업 완료 후 테스트를 실행하지 않아서 직접 테스트 코드를 실행해보니, 통과하지 못하는 문제가 발생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비효율적인 탐색&lt;/b&gt;: 무얼 고쳐야 할지 몰라 &lt;code&gt;SearchText&lt;/code&gt;, &lt;code&gt;ReadFile&lt;/code&gt; 도구를 난사하며 불필요한 파일을 엄청나게 읽음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;작업 시간 지연&lt;/b&gt;: 명확한 지시 사항과 목표가 없다 보니 에이전트가 헤매는 시간이 길어졌다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하네스: 시스템이나 컴포넌트를 검증하기 위한 환경, 스크립트 등의 집합&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  구현 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가설 검증이 끝났으니 본격적인 구현에 들어가 보자. 앞선 테스트를 바탕으로 다음과 같은 플로우를 구축할 것이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프로젝트 구조 및 기반 지식을 문서화하여 주입 (&lt;code&gt;/init&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;구체적인 개발 계획 수립 (&lt;code&gt;/plan&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;수립된 계획을 순차적으로 개발 (&lt;code&gt;/work&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프로젝트 정보 주입 (&lt;code&gt;/init&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 OmO의 &lt;code&gt;/init-deep&lt;/code&gt;과 같은 역할을 하는 &lt;code&gt;/init&lt;/code&gt; 명령어를 구현해 보자. OmO는 프로젝트의 핵심 정보를 구조화된 마크다운으로 관리한다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;# PROJECT KNOWLEDGE BASE
## OVERVIEW
## STRUCTURE
## WHERE TO LOOK
## CODE MAP
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나도 이 구조를 차용하기 위해, 제일 먼저 &lt;code&gt;/init&lt;/code&gt; 커맨드 환경을 세팅했다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;mkdir -p ~/.gemini/commands
vim ~/.gemini/commands/init.toml&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Prompt에 대한 상세 내용은 &lt;a href=&quot;https://www.google.com/search?q=https://github.com/Jwhyee/gemini-cli-commands&quot;&gt;gemini-cli-commands&lt;/a&gt; 리포지토리에 정리해 두었습니다. 앞으로 따로 언급하지 않을 예정이니 참고 부탁드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트를 작성하고 폴더로 이동해 &lt;code&gt;gemini&lt;/code&gt;를 실행한 뒤 &lt;code&gt;/init&lt;/code&gt;을 입력해 보았다. 하지만 기대와 달리 화면에 마크다운 텍스트만 줄줄이 출력될 뿐 실제 파일은 생성되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 Gemini CLI의 &lt;code&gt;.toml&lt;/code&gt; 커스텀 커맨드가 단순히 LLM에게 &lt;b&gt;&quot;어떤 포맷으로 대답할지&quot;를 지시하는 프롬프트 템플릿&lt;/b&gt;에 불과하기 때문이다. 즉, LLM은 텍스트를 입력받아 텍스트를 뱉어내는 철저히 격리된 &lt;b&gt;뇌&lt;/b&gt; 역할만 할 뿐, 내 운영체제에 접근하여 디렉토리를 만들고 파일을 쓰는 &lt;b&gt;손&lt;/b&gt;이 없는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 '뇌'와 '손'을 연결하기 위해 반드시 도입해야 하는 기술이 바로 &lt;b&gt;MCP (Model Context Protocol)&lt;/b&gt;다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Model (모델)&lt;/b&gt;: 생각하고 코드를 짜는 AI (Gemini 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Context (컨텍스트/문맥)&lt;/b&gt;: AI가 갇혀있는 텍스트 박스 너머의 진짜 세상. 즉, 내 로컬 파일 시스템, Git 환경, 터미널 등 에이전트가 조작해야 하는 '실제 작업 환경'&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Protocol (프로토콜)&lt;/b&gt;: 이 격리된 모델(Model)이 외부 환경(Context)의 도구들을 안전하게 호출하여 사용할 수 있게 이어주는 '연결 규약'&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간략히 말해, MCP는 말만 하던 AI에게 내 로컬 환경을 직접 조작할 수 있는 권한과 도구를 쥐여주는 표준 인터페이스다. 이제 생각만 가능한 &lt;code&gt;toml&lt;/code&gt; 명령어에 진짜 손을 달아주기 위해 MCP를 적용해 보자.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;vim ~/.gemini/settings.json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일의 &lt;code&gt;general&lt;/code&gt; 블록이 끝나는 곳 뒤에 콤마(&lt;code&gt;,&lt;/code&gt;)를 찍고, 다음 라인에 아래 내용을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;  &quot;mcpServers&quot;: {
    &quot;local_filesystem&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-filesystem&quot;,
        &quot;{GIT_PROJECT_DIR}&quot;
      ]
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;{GIT_PROJECT_DIR}&lt;/code&gt;에는 &lt;code&gt;~/dev/git/&lt;/code&gt; 와 같이 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;권한을 부여할 프로젝트의 최상위 절대 경로를 넣으면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;~/&lt;/code&gt;와 같이 너무 많은 경로에 대한 권한을 줄 경우 문제가 생길 수 있으니 조심해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 저장하고 Gemini CLI를 재시작한 뒤 다시 &lt;code&gt;/init&lt;/code&gt;을 실행하면, AI가 알아서 프로젝트 내부에 있는 파일들을 분석하고 다음 파일들을 수정 및 생성해 준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/GEMINI.md&lt;/code&gt;: AI가 작업 전 반드시 숙지해야 할 시스템 가이드라인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/.gemini/docs/STRUCTURE.md&lt;/code&gt;: 프로젝트 구조, 기술 스택, 핵심 코드 맵핑 지식&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/.gemini/docs/DOCUMENT.md&lt;/code&gt;: 프로젝트 아키텍처 및 버전 히스토리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 계획 수립 (&lt;code&gt;/plan&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 기반 지식을 모두 주입했으니, 요구사항을 바탕으로 구체적인 계획을 수립할 차례다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역시 OmO의 포맷을 차용했다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;# Execution Plan
## 1. Goal
## 2. Scope (In-Scope / Out-of-Scope)
## 3. Architecture Impact
## 4. Execution Plan (Phase 1, 2, 3...)
## 5. Risk Mitigation
## 6. Final Verification Wave&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 생성해 내는 명령어 역시 리포지토리에 추가해 두었다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;vim ~/.gemini/commands/plan.toml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 구현하고 싶은 기능이나 버그 수정이 있다면, CLI 창에 대고 이렇게 질의하면 된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;/plan Task 등록 폼에서 인박스 보관 버튼 클릭 시 인박스에 추가되지 않는 버그가 있어. 수정해 줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령을 내리면 AI가 현재 프로젝트 경로를 기준으로 &lt;code&gt;/.gemini/docs/PLANNING.md&lt;/code&gt; 파일을 생성하고, 각 Phase별로 잘게 쪼개진 체크리스트(&lt;code&gt;- [ ]&lt;/code&gt;)를 작성해 준다. 본격적인 코딩을 시작하기 전, 작성된 계획 문서가 내 의도와 맞는지 한 번 검토하는 것을 강력히 추천한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 개발 진행 (&lt;code&gt;/work&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 수립된 &lt;code&gt;PLANNING.md&lt;/code&gt; 파일의 체크박스를 위에서부터 하나씩 지워가며 실제 코드를 작성할 &lt;code&gt;/work&lt;/code&gt; 명령어를 만들어 주면 파이프라인이 완성된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;vim ~/.gemini/commands/work.toml&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  전체 워크플로우 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구축된 환경에서의 개발 흐름은 다음과 같다. OAuth의 경우 그냥 사용하면 되지만, API Key를 사용하는 경우, 다음과 같이 구성하는게 좋다. 다음 모델을 수동으로 수정하기 귀찮을 경우, &lt;b&gt;plan.toml, work.toml 파일에 model 항목을 추가&lt;/b&gt;해주면 된다. 구체적인 내용은 리포지토리의 &lt;b&gt;auth-api&lt;/b&gt; 브랜치의 내용을 참고하면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;plan&lt;/b&gt;: gemini-3.1-pro
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구체적인 계획 수립이 필요하므로 고성능 모델을 할당하는 것이 좋음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;work&lt;/b&gt;: gemini-3-flash
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계획을 구체적으로 수립하였기에, 개발을 빠르게 진행하기 위해 flash 모델을 할당하는게 좋음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: center;&quot;&gt;테스트 해본 결과 OAuth로 로그인한 경우, toml 파일에 모델을 지정해도 그 모델을 사용하지 않으니 참고 바랍니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 프로젝트 정보 초기화 및 지식 베이스 구축 (최초 1회 또는 구조 변경 시)
/init
# 깨끗한 상태로 세션 초기화
/clear

# 생성된 /.gemini/docs/PLANNING.md 파일 내용 검토
/plan {요구 사항 작성}

# 컨텍스트 압축 또는 초기화로 토큰 확보
/compress (또는 /clear)
# 계획표에 따라 실제 코드 구현 및 체크박스 업데이트 반복
/work

# 작업 완료 후 변경된 내용을 도메인 단위로 묶어 자동 커밋 &amp;amp; 푸시
/git&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  주의사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 하네스(Harness)와 인간의 개입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 주의해야 할 점은 &lt;b&gt;결국 사람이 중간중간 개입하여 검토해야 한다는 것&lt;/b&gt;이다. 단일 에이전트는 세션 내내 동일한 컨텍스트를 유지하므로, 한 번 잘못된 방향으로 코드를 짜기 시작하면 그 '잘못된 지식'까지 캐싱되어 연쇄적인 오류를 낼 확률이 높다. 따라서 AI가 작성한 계획 문서는 &lt;code&gt;/work&lt;/code&gt;를 돌리기 전에 반드시 한 번씩 점검해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. &lt;code&gt;.gitignore&lt;/code&gt; 파일 무시 이슈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 AI 에이전트 도구(Tool)들은 기본적으로 &lt;code&gt;.gitignore&lt;/code&gt;에 등록된 규칙을 따른다. 프로젝트 루트의 &lt;code&gt;.gitignore&lt;/code&gt; 파일에 &lt;code&gt;.gemini/&lt;/code&gt; 폴더를 무심코 등록해 버리면, 에이전트가 애써 만든 &lt;code&gt;PLANNING.md&lt;/code&gt;나 &lt;code&gt;STRUCTURE.md&lt;/code&gt;를 스스로 읽지 못하는 불상사가 발생할 수 있으니 파일 관리 시 유의해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 에이전트 환경이 가지는 명확한 장점과 치명적인 단점을 동시에 깨달을 수 있는 뜻깊은 삽질(?)이었다. 특히 컨텍스트 캐싱(Caching)이라는 기술이 가져다주는 득과 실을 확실히 체험했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 언젠가 돈을 많이 벌게 되면 여러 AI 프로바이더를 동시에 구독해서, 각기 다른 역할을 수행하는 '나만의 완벽한 다중 AI 에이전트 팀'을 구성해 자율 개발을 시켜보고 싶다.&lt;/p&gt;</description>
      <category>Devlog/AI</category>
      <category>Gemini CLI Commands</category>
      <category>Gemini CLI 계획 작성</category>
      <category>Gemini CLI 잘쓰는법</category>
      <category>Gemini CLI로 oh-my-opencode 따라하기</category>
      <author>Jwhy</author>
      <guid isPermaLink="true">https://jwhy-study.tistory.com/145</guid>
      <comments>https://jwhy-study.tistory.com/145#entry145comment</comments>
      <pubDate>Sat, 7 Mar 2026 00:04:29 +0900</pubDate>
    </item>
    <item>
      <title>[AI] 바이브 코딩 후기(oh-my-opencode, gemini cli, antigravity)</title>
      <link>https://jwhy-study.tistory.com/144</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠숨고청(잠시 숨을 고르는 청년) 생활이 길어지면서, 내가 어떤 것을 해야 할지 체계적으로 정리하고 공부하기 위해 데스크톱 TODO 앱을 만들고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트를 개발하면서 바이브 코딩(Vibe Coding) 방식을 적극적으로 도입해 보았다. 구체적으로 &lt;b&gt;Gemini CLI&lt;/b&gt;, &lt;b&gt;oh-my-opencode&lt;/b&gt;, 그리고 &lt;b&gt;Google Antigravity&lt;/b&gt; 환경을 활용해 보았으며, 각 도구가 가진 장단점과 특징을 리뷰해 보려고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  주의 사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무료 API 키를 사용하거나 OAuth를 사용하여 로그인한 경우, Gemini CLI나 oh-my-opencode 사용 시 입력된 프롬프트, 코드 그리고 생성된 결과물은 &lt;a href=&quot;https://geminicli.com/docs/resources/quota-and-pricing/&quot;&gt;Google의 모델 개선&lt;/a&gt;에 사용될 수 있다. 따라서 무료 사용자의 경우 실무 프로젝트 개발 시 보안에 주의해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  AI Agent 프롬프팅 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Google AI Pro 플랜만 구독하여 사용 중이다. 각 AI 에이전트에게 작업을 지시할 때는 주로 아래와 같은 3단계 템플릿을 유지했다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;### [Objective]
(구현할 기능의 최종 목표와 비즈니스 로직 설명)

### [Execution Plan]
(프론트엔드/백엔드 관점에서의 단계별 실행 계획)

### [Validation Criteria]
(개발 완료 후 정상 작동 여부를 판단할 체크리스트)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 AI가 코드를 작성할 때 길을 잃지 않도록 돕는 명확한 &lt;b&gt;내비게이션 역할&lt;/b&gt;을 한다. 목표를 제시하고, FE/BE 흐름을 단계적으로 세분화하여 지시하며, 스스로 결과물을 검토할 수 있는 체크리스트를 제공함으로써 AI 환각(Hallucination)을 최소화하고 정확한 산출물을 얻어낼 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Gemini CLI&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 초반 환경을 빠르게 구성할 때 주력으로 사용한 방식이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;압도적인 접근성과 토큰 여유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini CLI를 OAuth로 인증한 경우, &lt;b&gt;하루 종일 사용해도 제한에 걸리지 않을 정도&lt;/b&gt;로 쿼터가 넉넉하다. 실제로 8시간 넘게 개발에 사용했음에도 제한에 도달하지 않았다. 구글에서 제공하는 정보에 따르면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;General Users (Personal Google Account):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Maximum 1,000 model requests per day&lt;/li&gt;
&lt;li&gt;Maximum 60 model requests per minute&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Gemini Code Assist Paid Subscribers:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Google AI Pro:&lt;/b&gt; Maximum 1,500 requests per day&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Google AI Ultra:&lt;/b&gt; Maximum 2,000 requests per day&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무료 가입자나 유료 가입자 모두 매우 여유롭게 사용할 수 있다. 그럼에도 효율적인 토큰 관리를 위해 &lt;code&gt;/compress&lt;/code&gt;를 사용하여 대화 내역을 압축하거나, &lt;code&gt;/clear&lt;/code&gt;를 통해 새로운 세션을 만드는 등 다양한 방식을 활용했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;빠른 속도로 초기 MVP 개발&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GEMINI.md&lt;/code&gt; 파일과 같이 Agent에게 가이드를 내리는 Rules(System Prompt) 설정은 선택이 아닌 필수다. 개인적으로 이 파일에 다음과 같은 내용으로 단계를 구성했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기획 문서 태그를 통해 프로젝트 콘텍스트 파악
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 내부에 &lt;code&gt;docs/&lt;/code&gt; 폴더 생성&lt;/li&gt;
&lt;li&gt;기획 문서 및 프로젝트 구조, 각 파일에 대한 간략한 설명 작성&lt;/li&gt;
&lt;li&gt;필요한 문서를 먼저 읽도록 지시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;개발 스타일 및 컨벤션에 대한 규칙
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용하는 언어에 대한 구체적인 코드 컨벤션 지시 (예: 라인 주석 대신 JavaDoc 등 문서화 주석 사용)&lt;/li&gt;
&lt;li&gt;파일 분기 시점 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테스트 및 빌드 통과 시 git 작업 진행
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구체적인 테스트 작성 방법 지시(실패 및 성공 테스트 작성)&lt;/li&gt;
&lt;li&gt;테스트 통과 및 빌드 성공 시 &lt;code&gt;git add, commit, push&lt;/code&gt; 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 단일 요청에 소모되는 토큰량은 많아질 수 있지만, AI의 실수로 인해 처음으로 롤백해서 다시 요청하거나 잘못 개발된 내용을 수정해달라고 요청하는 것보다 훨씬 효율적이다. 또한, 프로젝트의 구조와 특정 파일의 역할 등을 간략히 정리해 두면 AI가 전체 파일을 뒤지지 않고도 빠르게 작업을 진행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면, 명확한 룰을 작성하는 것만으로 원하는 기능을 빠르게 개발 및 검증하고, 필요한 내용을 수정할 수 있다. 이는 다른 Agent를 사용할 때도 마찬가지겠지만, 첫 번째 장점인 &lt;b&gt;압도적인 접근성과 토큰 여유&lt;/b&gt;가 맞물려 초기 MVP의 빠른 개발을 가능하게 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구체적인 계획의 누락&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 프롬프트에 너무 많은 지시 사항을 담아 계획을 짜라고 지시할 경우, 일부 내용이 누락되는 현상이 발생한다. 특정 파일을 생성해 계획을 관리하고 그 파일을 기반으로 체크하며 개발하도록 유도하면 어느 정도 해결될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;바바현사&lt;/b&gt;에서 매번 세세한 계획 파일까지 관리하며 지시하기에는 번거로움이 따른다. 다행히 사용량이 넉넉하므로 누락된 지시 사항은 다시 요청하면 그만이긴 하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. oh-my-opencode&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기대했던 기술이자, 오케스트레이션(Orchestration) 도구다. 여러 AI 에이전트가 팀을 이루어 역할을 분담하는 구조를 띠고 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;뛰어난 계획 수립&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 모델 도구에는 없는 '에이전트 간 협업을 통한 계획 수립' 기능을 갖추고 있다. &lt;code&gt;/init-deep&lt;/code&gt; 명령어를 통해 프로젝트에 대한 정보를 에이전트들이 참고할 수 있는 시스템 프롬프트 및 컨텍스트 문서들을 생성하게 되고, 이를 바탕으로 체계적인 개발을 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;oh-my-opencode&lt;/code&gt;를 사용하는 경우 크게 두 가지 개발 방식이 존재한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;Sisyphus&lt;/code&gt; 에이전트에게 &lt;code&gt;/ulw&lt;/code&gt; (Ultrawork) 모드로 자율 개발 시키기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Prometheus&lt;/code&gt; (Plan Builder)가 계획을 세우고 &lt;code&gt;Atlas&lt;/code&gt; (Plan Executor)가 계획을 실행하기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 장단점이 있지만, &lt;a href=&quot;https://github.com/code-yeongyu/oh-my-opencode/blob/dev/README.ko.md&quot;&gt;공식 문서&lt;/a&gt;의 설명이 아주 적절하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ultrawork&lt;/code&gt; (또는 &lt;code&gt;ulw&lt;/code&gt;) 치세요. 끝.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/ulw&lt;/code&gt; 모드로 &lt;code&gt;Sisyphus&lt;/code&gt;에게 지시를 내리면 스스로 해야 할 일들을 리스트업하고 TODO를 작성한 뒤 작업이 끝날 때까지 밀어붙인다. 다른 일을 하고 돌아오면 지시한 업무가 마무리되어 있는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 방식으로는 &lt;code&gt;Prometheus&lt;/code&gt;와 &lt;code&gt;Atlas&lt;/code&gt;를 연계하는 것이다. &lt;code&gt;Prometheus&lt;/code&gt;에게 원하는 개발 사항을 전달하면, 역으로 필요한 정보나 테스트 방식 등을 꼬치꼬치 묻는 '인터뷰'를 시전한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 인터뷰가 끝나고 구체적인 기획이 완료되면, 키보드의 &lt;code&gt;Tab&lt;/code&gt; 키를 통해 에이전트를 &lt;code&gt;Atlas&lt;/code&gt;로 변경하여 &lt;code&gt;/start-work&lt;/code&gt;로 실행을 지시한다. 그러면 &lt;code&gt;Prometheus&lt;/code&gt;가 세운 정교한 계획을 바탕으로 실제 코드를 작성한다. 이 과정은 마치 뛰어난 시니어 기획자/설계자와 함께 개발하는 듯한 느낌이 들게 만든다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;역할별 모델 최적화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 장점 중 하나다. 메인 오케스트레이터(&lt;code&gt;Sisyphus&lt;/code&gt;)나 아키텍처 리뷰어(&lt;code&gt;Oracle&lt;/code&gt;)에는 추론 능력이 뛰어난 최신 하이엔드 모델을, 단순 검색이나 코드 탐색(&lt;code&gt;Librarian&lt;/code&gt;, &lt;code&gt;Explore&lt;/code&gt;)에는 빠르고 가벼운 모델을 할당하는 등 자유로운 매핑이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 과정에서 OpenAI, Google 등 여러 API 인증 정보를 추가하면 역할에 맞는 모델을 자동으로 주입해 준다. 덕분에 복잡한 설정 없이 설치 직후 바로 최적화된 환경에서 개발에 돌입할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 작성된 단점은 Google AI Pro 단일 플랜만 구독하고 있어 발생하는 개인적인 한계일 수 있으니 참고용으로 봐주시면 감사하겠습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;API 리밋(Rate Limit) 병목&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 프로바이더(Provider)의 인증 정보를 추가하여 모델을 분산시키면 큰 문제가 없겠지만, 단일 플랜만 사용할 경우 각 에이전트 역할에 동일한 모델이 중복 할당된다. 결국 짧은 시간에 많은 요청이 몰리면서 TPM(Tokens Per Minute) 제한에 걸려 개발이 중간에 멈추는 병목 현상이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 &lt;code&gt;oh-my-opencode&lt;/code&gt; 내부적으로 API 요청 실패 시 스케줄링을 통해 일정 시간 뒤 재시도하는 로직이 있다. 즉, 시간에 구애받지 않는다면 단일 플랜으로도 개발을 완료할 수는 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;치밀한 계획 수립에 따른 오버헤드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;나무를 베는 데 6시간이 주어진다면, 나는 처음 4시간을 도끼를 가는 데 쓸 것이다.&quot;라는 링컨의 명언처럼, 좋은 결과물을 위해서는 계획 수립에 많은 시간을 투자해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;code&gt;Prometheus&lt;/code&gt;에게 계획 수립을 지시하면 체감상 꽤 오랜 시간이 소요된다. 30분씩 걸리는 것은 아니지만 답답함이 느껴질 때가 있다. AI가 계획을 세우는 동안 다른 업무를 보다가, AI의 질문(인터뷰) 알림이 울리면 다시 돌아와 응답해야 한다. 집중하고 있는 다른 업무가 있다면 약간의 Context Switching 비용이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 속도가 빠른 &lt;code&gt;Sisyphus&lt;/code&gt;에게 자율 작업을 시키면 되지 않을까? 이 역시 애매하다. 속도는 빠르지만 세부적인 검토 과정(인터뷰)이 생략된 채 독단적으로 코드를 수정하기 때문에 기획 의도와 다른 결과물이 나올 위험이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 목적에 따라 선택해야 한다. &lt;b&gt;시간이 걸리더라도 정교하고 높은 퀄리티의 결과물&lt;/b&gt;을 원한다면 &lt;code&gt;Prometheus -&amp;gt; Atlas&lt;/code&gt; 파이프라인을, &lt;b&gt;빠르게 결과물을 도출하고 검증&lt;/b&gt;하고 싶다면 &lt;code&gt;Sisyphus&lt;/code&gt;를 사용하는 것이 맞다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적인 견해로는 &lt;code&gt;Sisyphus&lt;/code&gt;를 사용하는게 더 사용 경험이 좋게 느껴진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Google Antigravity&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Google Antigravity&lt;/code&gt;는 Cursor, Windsurf 등과 같은 VS Code Fork 기반의 AI 에디터이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;종합 선물 패키지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 3월 3일 기준으로 Gemini 3.1 Pro, Gemini 3 Flash, Claude Sonnet 4.6, Claude Opus 4.6와 같은 다양한 모델을 자유롭게 선택하여 사용할 수 있다. 무료 계정은 주 단위로 한도가 갱신되며, Pro 및 Ultra 플랜은 5시간마다 초기화되어 플랜에 따라 더 많은 사용량을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 두 가지 개발 모드를 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Planning:&lt;/b&gt; 제공된 프롬프트에 대해 Task와 Implementation Plan을 수립한 후 개발을 진행한다. (&lt;code&gt;oh-my-opencode&lt;/code&gt;의 &lt;code&gt;Sisyphus&lt;/code&gt;와 유사하게 TODO를 계획하고 실행)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fast:&lt;/b&gt; 계획 수립 없이 제공된 프롬프트를 즉시 실행한다. (Gemini CLI와 유사)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;번외 수확 (의도치 않은 버그 픽스)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;oh-my-opencode&lt;/code&gt;로 개발한 뒤, Google Antigravity로 환경을 옮겨 다음 작업을 지시했다. 작업 완료 후 커밋 내역을 확인하다가 흥미로운 메모를 발견했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;번외 수확&lt;/b&gt;: 기존 백엔드 단위 테스트 5개가 &lt;code&gt;planning_memo&lt;/code&gt; 컬럼 누락으로 모두 실패하고 있었는데, 이번에 테스트 스키마도 함께 수정하여 &lt;b&gt;전체 9개 테스트가 통과&lt;/b&gt;합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해 보니 이전 도구에서 업무를 지시할 때 빌드 성공 여부만 확인하고 테스트 통과 여부를 꼼꼼히 체크하지 않은 것 같았다. 하지만 프로젝트에 세팅해 둔 &lt;code&gt;GEMINI.md&lt;/code&gt;의 &quot;테스트가 통과하는지 확인하고, 빌드 확인 후 git에 추가하라&quot;는 지시 사항 덕분에 Antigravity가 스스로 이전의 실패한 테스트를 발견하고 고친 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 다른 AI Agent도 가능한 일일 수 있지만, 지시하지도 않은 레거시 버그를 스스로 찾아 수정까지 한 경험은 처음이라 매우 놀라웠다. 의도치 않은 코드 변경에 토큰이 소모되는 것을 꺼리는 사람도 있겠지만, 필자 입장에서는 바이브 코딩 중 놓칠 수 있는 에러를 AI가 스스로 잡아주어 &lt;b&gt;오히려 땡큐&lt;/b&gt;였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;넉넉하지 않은 한도&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google AI 플랜 구독자는 무료 사용자보다 한도가 높고 5시간마다 초기화된다는 장점이 있지만, 그럼에도 한도 초과에 자주 부딪힌다. 긴 호흡으로 개발을 진행하기에는 제약이 느껴진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google AI Pro 플랜 기준으로 최신 하이엔드 모델을 2~3회 깊게 사용하면 금세 제한이 걸리는 느낌이다. 횟수 제한이 아닌 토큰 사용량 기준이라서 작업 크기를 예측하며 애매하게 사용해야 하는 불편함이 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 AI Agent를 활용한 개발에서 가장 핵심적인 이슈는 &lt;b&gt;하네스(Harness)&lt;/b&gt; 구축인 것 같다. 하네스는 본래 조종사나 등반가의 '안전벨트'를 뜻하지만, 소프트웨어 공학에서는 시스템이나 컴포넌트를 검증하기 위한 &lt;b&gt;환경, 데이터, 스크립트, 도구들의 집합&lt;/b&gt;을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 바이브 코딩의 성공 여부는 AI가 마음껏 코드를 작성하고 수정해도 시스템이 망가지지 않도록 보장해 주는 &lt;b&gt;자동화된 테스트 및 실행 환경(하네스)&lt;/b&gt;을 얼마나 잘 구성하느냐에 달려 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 김용성 님이 작성하신 &lt;a href=&quot;https://toss.tech/article/harness-for-team-productivity&quot;&gt;토스 테크 블로그 글&lt;/a&gt;을 인상 깊게 읽었다. 현재 많은 개발팀의 LLM 도입은 &lt;b&gt;각자도생&lt;/b&gt;에 가깝다고 한다. 프롬프트 컨텍스트를 잘 설계하는 엔지니어는 금방 결과를 얻지만, 그렇지 못한 엔지니어는 환각 현상을 바로잡느라 더 많은 시간을 허비한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이러한 개인 역량의 격차를 방치하지 말고, 터미널 도구나 내부 마켓플레이스 등을 통해 &lt;b&gt;팀 도메인에 최적화된 하네스를 구축하여 숙련된 팀원의 워크플로우를 조직 전체에 복제하는 시스템적 접근&lt;/b&gt;이 필요하다고 강조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 읽으며 &quot;나는 내가 사용하는 에이전트 환경에서 하네스를 완벽히 구축했는가?&quot; 되돌아보게 되었다. 프로젝트에 도메인 지식을 주입하고, 테스트와 빌드를 강제하는 등 나름대로 안전장치를 마련하기 위해 치열하게 고민해 왔다는 사실을 새삼 깨달았다. AI를 단순한 코드 생성기가 아닌 신뢰할 수 있는 개발 파트너로 만들기 위해, 앞으로도 시스템적인 하네스 구축과 프롬프팅 전략을 꾸준히 개선해야겠다.&lt;/p&gt;</description>
      <category>Devlog/AI</category>
      <category>antigravity 후기</category>
      <category>gemini cli 후기</category>
      <category>oh-my-opencode 후기</category>
      <category>바이브 코딩 후기</category>
      <author>Jwhy</author>
      <guid isPermaLink="true">https://jwhy-study.tistory.com/144</guid>
      <comments>https://jwhy-study.tistory.com/144#entry144comment</comments>
      <pubDate>Wed, 4 Mar 2026 00:45:59 +0900</pubDate>
    </item>
    <item>
      <title>[AI] oh-my-opencode 도입기</title>
      <link>https://jwhy-study.tistory.com/143</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ 개발 환경&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OpenCode : 1.2.14&lt;/li&gt;
&lt;li&gt;OS : Mac OS&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  OmO(oh-my-opencode)란&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/code-yeongyu/oh-my-opencode&quot;&gt;Github: oh-my-opencode&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 오픈 소스를 도입하기 전, AI를 활용한 코딩 도구가 어떻게 진화했는지 짚고 넘어갈 필요가 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 85px;&quot;&gt;&lt;b&gt;세대&lt;/b&gt;&lt;/th&gt;
&lt;th style=&quot;width: 77px;&quot;&gt;&lt;b&gt;핵심 개념&lt;/b&gt;&lt;/th&gt;
&lt;th style=&quot;width: 486px;&quot;&gt;&lt;b&gt;작동 방식 및 설명&lt;/b&gt;&lt;/th&gt;
&lt;th style=&quot;width: 202px;&quot;&gt;&lt;b&gt;대표 사례&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 85px;&quot;&gt;&lt;b&gt;1세대&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 77px;&quot;&gt;Auto Completion&lt;/td&gt;
&lt;td style=&quot;width: 486px;&quot;&gt;개발자가 코드를 입력하면, 문맥을 파악해 알맞은 코드를 제안&lt;/td&gt;
&lt;td style=&quot;width: 202px;&quot;&gt;GitHub Copilot (초창기)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 85px;&quot;&gt;&lt;b&gt;2세대&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 77px;&quot;&gt;Chat with AI&lt;/td&gt;
&lt;td style=&quot;width: 486px;&quot;&gt;웹이나 IDE 플러그인에서 질문하면 코드 스니펫이나 해결책을 응답&lt;/td&gt;
&lt;td style=&quot;width: 202px;&quot;&gt;ChatGPT, Claude 웹&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 85px;&quot;&gt;&lt;b&gt;3세대&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 77px;&quot;&gt;AI Agent&lt;/td&gt;
&lt;td style=&quot;width: 486px;&quot;&gt;터미널 환경에서 AI 에이전트가 직접 파일 시스템에 접근하여 코드를 작성하고 수정&lt;/td&gt;
&lt;td style=&quot;width: 202px;&quot;&gt;Gemini CLI, Claude Code, Cursor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 85px;&quot;&gt;&lt;b&gt;4세대&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 77px;&quot;&gt;&lt;b&gt;AI Agent Team&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 486px;&quot;&gt;여러 AI 모델을 조합해 기획, 구현, 검토 등 역할을 분담하여 자율적으로 프로젝트 개발&lt;/td&gt;
&lt;td style=&quot;width: 202px;&quot;&gt;oh-my-claudecode, oh-my-opencode 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;oh-my-opencode(OmO)&lt;/code&gt;는 현시점 최신 세대인 &lt;b&gt;AI Agent Team(멀티 에이전트 오케스트레이터)&lt;/b&gt;에 해당하는 강력한 오픈소스다. Gemini CLI나 Claude Code가 훌륭한 '단일 개발자'라면, &lt;code&gt;oh-my-opencode&lt;/code&gt;는 작업의 성격에 맞춰 각 분야의 최고 AI 모델을 적재적소에 배치하고 지휘하는 &lt;b&gt;수석 아키텍트이자 자동화된 개발 팀&lt;/b&gt;이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;❓ 왜 사용할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 AI Agent도 훌륭한데, 굳이 복잡해 보이는 AI Agent Team 프레임워크를 사용하는 이유는 무엇일까? 개인적으로는 세 가지 명확한 이점이 있다고 생각한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 컨텍스트 분리를 통한 API 토큰(비용) 최소화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 CLI 환경에서 AI와 대화를 이어가다 보면 이전 대화 기록이 누적되어 컨텍스트 윈도우(Context Window)가 비대해진다. 수시로 대화를 압축(&lt;code&gt;/compress&lt;/code&gt;)하지 않으면 불필요한 입력 토큰이 계속 소모된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OmO는 작업을 세분화하여, 특정 에이전트(예: 단순 코드 수정 담당)는 필요한 최소한의 파일과 문맥만 들고 실행된다. 불필요한 토큰 낭비를 막아 비용을 극적으로 절감한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 구체적인 계획 수립과 자율 실행 분리 (Prometheus &amp;amp; Sisyphus)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Agent에게 &quot;게시판 API 만들어줘&quot;라고 하면 곧바로 코드를 짜다가 길을 잃는 경우가 많다. 하지만 OmO는 다르게 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Prometheus&lt;/code&gt; 에이전트가 먼저 인터뷰 모드로 개발자에게 요구사항을 역질문하며 완벽한 계획 문서(Plan)를 수립한다. 이후 &lt;code&gt;Sisyphus&lt;/code&gt; 에이전트가 이 계획을 바탕으로 하위 에이전트들을 지휘하며 작업이 끝날 때까지 무한 루프(Ralph Loop)를 돌며 코드를 완성한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 안전한 코드 편집 (Hash-Anchored Edits &amp;amp; AST-Grep)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 AI 도구들의 가장 큰 문제는 코드를 덮어쓸 때 들여쓰기를 망치거나 엉뚱한 라인을 날려버리는 것이다. OmO는 각 코드 라인에 해시(Hash)를 부여하여, 원본이 변경되었거나 해시가 일치하지 않으면 편집을 거부한다. Kotlin/Spring Boot 처럼 패키지 구조가 깊고 파일 간 의존성이 복잡한 프로젝트를 리팩토링할 때 필수적인 안정성을 보장한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⚙️ 설치 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 과정은 직접 설정 파일을 건드리는 것보다 Agent에게 위임하는 것이 가장 확실하다. 터미널에서 기존에 사용하던 AI Agent(Gemini CLI, Claude Code 등)를 실행한 뒤 다음 프롬프트를 입력한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Install and configure oh-my-opencode by following the instructions here:
https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads/master/docs/guide/installation.md&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 README에도 &quot;사람은 설정하다 오타를 내니, Agent에게 시켜라(Let an agent do it)&quot;라고 명시되어 있다. Agent가 가이드 문서를 읽고 알아서 환경에 맞게 설치를 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 중 Provider(OpenAI, Anthropic, Google 등)를 선택하게 되는데, API 비용 최적화를 위해 Gemini를 메인으로 구성했다. (Google AI Studio에서 제공하는 무료 크레딧을 활용하면 초기 구축 비용을 크게 아낄 수 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경 변수에 API 키가 없다면 터미널 설정 파일에 추가해 준다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# zsh 사용 시
echo 'export GOOGLE_GENERATIVE_AI_API_KEY=&quot;YOUR_API_KEY&quot;' &amp;gt;&amp;gt; ~/.zshrc
source ~/.zshrc

# 정상 등록 확인
echo $GOOGLE_GENERATIVE_AI_API_KEY&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에 &quot;모든 에이전트는 해당 모델의 특장점에 맞춰 튜닝되어 있습니다. 수동으로 모델 바꿔가며 뻘짓하지 마세요.&quot;라고 나와있듯이, 모델을 따로 지정할 필요는 없다. 하지만 비용에 큰 부담이 있을 것 같다면 직접 사용할 모델을 수정해도 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;vim ~/.config/opencode/oh-my-opencode.json&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Agent 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 완료되면 터미널에 &lt;code&gt;opencode&lt;/code&gt;를 입력하여 실행한다. &lt;code&gt;tab&lt;/code&gt; 키를 누르면 프로젝트를 책임질 다양한 에이전트 목록을 확인할 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 207px;&quot;&gt;&lt;b&gt;이름&lt;/b&gt;&lt;/th&gt;
&lt;th style=&quot;width: 647px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 207px;&quot;&gt;&lt;b&gt;Sisyphus&lt;/b&gt; (Orchestrator)&lt;/td&gt;
&lt;td style=&quot;width: 647px;&quot;&gt;개발 팀의 리더. 하위 에이전트들에게 작업을 위임하고, 작업이 100% 완료될 때까지 집요하게 밀어붙인다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 207px;&quot;&gt;&lt;b&gt;Prometheus&lt;/b&gt; (Planner)&lt;/td&gt;
&lt;td style=&quot;width: 647px;&quot;&gt;아키텍트 역할. 코드를 건드리기 전, 인터뷰를 통해 시스템 구조와 예외 상황을 고려한 명확한 작업 계획서를 작성한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 207px;&quot;&gt;&lt;b&gt;Atlas&lt;/b&gt; (Executor)&lt;/td&gt;
&lt;td style=&quot;width: 647px;&quot;&gt;실제 코드를 작성하는 실무자. Prometheus가 세운 계획을 바탕으로 파일을 수정하고 로직을 구현한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 효율적인 작업 워크플로우는 다음과 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프로젝트 이동 및 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Agent Team에게 개발을 맡길 프로젝트로 이동한 뒤에, &lt;code&gt;opencode&lt;/code&gt;를 실행한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;cd {프로젝트_경로}
opencode&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 초기 컨텍스트 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 구조를 AI가 이해할 수 있도록 다음 명령어를 입력해 AI가 &lt;code&gt;AGENTS.md&lt;/code&gt; 파일을 생성하도록 한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;/init-deep&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9z1nD/dJMcahwKzkS/aSRE5wkbH7jQoK3hTv3eWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9z1nD/dJMcahwKzkS/aSRE5wkbH7jQoK3hTv3eWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9z1nD/dJMcahwKzkS/aSRE5wkbH7jQoK3hTv3eWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9z1nD%2FdJMcahwKzkS%2FaSRE5wkbH7jQoK3hTv3eWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;943&quot; height=&quot;540&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 기획 및 계획 수립&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Prometheus&lt;/code&gt;를 호출하여 만들고자 하는 기능을 설명한다. 사진과 같이 요구사항이 애매할 경우, AI가 사용자에게 질문을 던지게 된다. 이에 대해 구체적인 답변을 입력하면, 요구사항 명세서를 완성시킨다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;543&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXc5SL/dJMcah4zIT1/PisavQWYAlHcKinkP4zL11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXc5SL/dJMcah4zIT1/PisavQWYAlHcKinkP4zL11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXc5SL/dJMcah4zIT1/PisavQWYAlHcKinkP4zL11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXc5SL%2FdJMcah4zIT1%2FPisavQWYAlHcKinkP4zL11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;946&quot; height=&quot;543&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;543&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 개발 시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계획이 수립되면 &lt;code&gt;Atlas&lt;/code&gt;를 호출하여 &lt;code&gt;/start-work&lt;/code&gt; 명령어를 입력한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;/start-work&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 &lt;code&gt;Prometheus&lt;/code&gt;에게 맡겼던 계획 정보를 &lt;code&gt;Atlas&lt;/code&gt;가 이어 받아서 개발을 한다. 그러면 백그라운드에서 에이전트들을 스폰하여 파일 수정, 빌드, 오류 수정을 알아서 반복한다. 완료될 때까지 기다리면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I3Sjn/dJMcahjefNX/X5dwHTI9zdUAklBDMvbI4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I3Sjn/dJMcahjefNX/X5dwHTI9zdUAklBDMvbI4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I3Sjn/dJMcahjefNX/X5dwHTI9zdUAklBDMvbI4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI3Sjn%2FdJMcahjefNX%2FX5dwHTI9zdUAklBDMvbI4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;941&quot; height=&quot;540&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 자동 개발&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 계획 수립, 개발 시작을 내가 입력하지 않아도 알아서 하도록 만들고 싶다면, &lt;code&gt;Sisyphus&lt;/code&gt;에게 &lt;b&gt;/ulw&lt;/b&gt; 명령어를 사용하면 된다. 그러면 백그라운드에서 에이전트 팀을 스폰하여 작업이 끝날 때까지 멈추지 않고 개발을 진행한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Sisyphus&lt;/code&gt;의 자율 주행 성능은 정말 좋지만, 개인적으로는 &lt;code&gt;Prometheus&lt;/code&gt;로 탄탄하게 계획을 세운 뒤 &lt;code&gt;Atlas&lt;/code&gt;로 실행하는 워크플로우가 가장 안정적이고 깔끔하게 느껴진다. 단일 AI 에이전트(Claude Code, Gemini CLI 등)와 비교했을 때 속도 면에서는 느리지만, 그 단점을 충분히 상쇄할 만큼의 &lt;b&gt;압도적인 꼼꼼함&lt;/b&gt;을 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 내가 미처 고려하지 못한 엣지 케이스나 유스케이스까지 세밀하게 커버해 주는 점이 정말 놀라웠다. 이제는 단순히 코드를 직접 작성하는 시대를 넘어, 의도를 정확히 전달하는 프롬프트 엔지니어링(Prompt Engineering)의 역량이 개발자의 핵심 실력이 되었음을 실감한다.&lt;/p&gt;</description>
      <category>Devlog/AI</category>
      <category>oh-my-opencode</category>
      <category>oh-my-opencode 사용법</category>
      <category>ohmyopencode</category>
      <category>opencode 사용법</category>
      <category>오픈코드 사용법</category>
      <author>Jwhy</author>
      <guid isPermaLink="true">https://jwhy-study.tistory.com/143</guid>
      <comments>https://jwhy-study.tistory.com/143#entry143comment</comments>
      <pubDate>Thu, 26 Feb 2026 15:39:59 +0900</pubDate>
    </item>
    <item>
      <title>[백준][Kotlin] - 보물 찾기2(다익스트라와 0-1 BFS 차이)</title>
      <link>https://jwhy-study.tistory.com/142</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ️ 문제 레벨 : 골드3&lt;br /&gt;  문제 유형 : 다익스트라&lt;br /&gt;  풀이 언어 : Kotlin&lt;br /&gt; ️ 문제 링크 : &lt;a href=&quot;https://www.acmicpc.net/problem/27978&quot;&gt;백준 문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다익스트라는 대표적으로 두 가지 방법으로 풀이할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PriorityQueue를 사용한 일반 다익스트라&lt;/li&gt;
&lt;li&gt;Deque를 사용한 0-1 BFS&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 방식에 대해서 문제를 풀이하면서 한 번 알아보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 건덕이는 &lt;a href=&quot;https://www.acmicpc.net/problem/17489&quot;&gt;지난 보물찾기&lt;/a&gt;에서 보물을 찾는 데 성공했다. 이제는 배를 타고 세계 곳곳을 누비며 보물을 찾아 나서는 보물 탐사대가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;건덕이는 주변 섬들의 지형이 담긴 가로 &lt;code&gt;W&lt;/code&gt;칸, 세로 &lt;code&gt;H&lt;/code&gt;칸의 지도를 구했다. 지도에는 주변 바다의 지형이 나타나 있다. 바다와 암초로 이루어져 있는데, 배는 암초 위를 지나다닐 수 없다. 지도의 가장 왼쪽 위는 &lt;code&gt;(1, 1)&lt;/code&gt;, 오른쪽 아래는 &lt;code&gt;(H, W)&lt;/code&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;건덕이의 배는 매번 인접한 8칸 중 한 곳으로 이동할 수 있다. &lt;code&gt;(r, c)&lt;/code&gt;와 인접한 칸은 &lt;code&gt;max(|r-x|, |c-y|) = 1&lt;/code&gt;인 &lt;code&gt;(x, y)&lt;/code&gt; 이다. 안전한 항해를 위해 지도 바깥으로는 나가지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바다의 물살이 지도 기준 왼쪽에서 오른쪽으로 빠르게 흐르고 있어서, 물살을 타고 가는 데에는 연료가 들지 않지만, 그 외에는 한 칸당 1의 연료가 소모된다. 예를 들어, 건덕이가 현재 &lt;code&gt;(r, c)&lt;/code&gt; 위치에 자리 잡고 있다면 &lt;code&gt;(r-1, c+1), (r, c+1), (r+1, c+1)&lt;/code&gt;로는 연료 소모 없이 이동할 수 있고, 그 외의 칸으로는 &lt;code&gt;1&lt;/code&gt;의 연료를 소모해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;건덕이의 손에 보물지도가 주어졌다. 보물을 찾기까지 소모해야 하는 연료의 최솟값을 구해 주자!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  알고리즘 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 최소한의 비용을 들여 목적지에 도달해야하는 전형적인 다익스트라 문제이다. 이 문제의 핵심은 이동 방향에 따른 가중치 차이라고 보면 될 것 같다. 정답은 맞았지만, 제출 결과를 보고 위화감을 느꼈다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;메모리 : 148536 KB (약 145MB)
시간 : 708 ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바로 푼 다른 사람들의 결과(평균 30MB, 300ms)에 비해 내 코드는 메모리를 5배, 시간을 2배나 더 쓰고 있었다. 단순히 언어 차이라고 하기엔 격차가 너무 컸다. 왜 이런 결과가 나왔는지 Deep dive 해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시간 복잡도 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프 알고리즘에서 시간 복잡도를 논할 때, 정점의 개수인 V(Vertex), 간선의 개수인 E(Edge)를 의미한다. 즉, 이 문제인 격자판 환경은 다음과 같이 정의할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;V(정점의 수): 지도의 크기 (&lt;code&gt;H * W&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;E(간선의 수): 각 칸에서 8방향 이동 가능(약, &lt;code&gt;8 * H * w&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다익스트라&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다익스트라는 우선 순위 큐(&lt;code&gt;PriorityQueue&lt;/code&gt;)를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val pq = PriorityQueue&amp;lt;State&amp;gt;(compareBy { it.cost })

pq.add(State(ny, nx, nextCost))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐에 원소를 넣고(&lt;code&gt;add&lt;/code&gt;), 뺄 때(&lt;code&gt;poll&lt;/code&gt;)마다 재정렬(Heapify) 비용인 &lt;code&gt;log V&lt;/code&gt;가 들게 된다. 이 때, 모든 간선을 확인하기 때문에, 총 시간 복잡도는 &lt;code&gt;O(E log V)&lt;/code&gt;가 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;0-1 BFS&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;0-1 BFS&lt;/code&gt;는 단순하게 설명하면, 가중치가 0과 1일 때 사용할 수 있는 BFS 알고리즘이다. 이는, 우선 순위 큐 대신 &lt;code&gt;ArrayDeque&lt;/code&gt;를 사용한다. 기본적인 아이디어는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val deque = ArrayDeque&amp;lt;State&amp;gt;()

if (add == 0) {
    deque.addFirst(State(ny, nx, nextCost))
} else {
    deque.addLast(State(ny, nx, nextCost))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덱(디큐)는 위, 아래가 뚫려있는 통이라고 생각하면 된다. 가중치가 0인 경우 먼저 처리하고, 가중치가 1인 경우 나중으로 미루는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덱은 정렬 과정 없이 데이터를 앞 뒤로만 넣기 때문에, 삽입과 삭제는 &lt;code&gt;O(1)&lt;/code&gt;로 굉장히 빠르다. 이 알고리즘 또한, 8방향 모두 탐색을 하므로, 총 시간 복잡도는 &lt;code&gt;O(V + E)&lt;/code&gt;가 된다. 즉, 일반 다익스트라에 비해 log 항만큼 더 빠르다는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 사용량 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 생각해도 &lt;code&gt;PriorityQueue&lt;/code&gt;, &lt;code&gt;Deque&lt;/code&gt; 모두 8방향을 탐색해서 노드를 넣는데 왜 메모리가 크게 차이가 나는지 궁금해졌다. 이를 알아내면서 알게된 가장 큰 핵심은 바로 &lt;b&gt;큐에 존재하는 노드의 생명 주기&lt;/b&gt;와 &lt;b&gt;탐색 방식 차이&lt;/b&gt;였다. 다음 예시를 시뮬레이션하며 알아보자.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;. . . . .  
. . . . .  
. . K . .  
. . . . .  
. . . . *&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서 다음 방향을 찾기 위한 배열은 다음과 같이 정의되어 있으며, 1시, 3시, 5시 방향은 물살을 타기 때문에 연료 사용량(가중치)는 0이 되고, 나머지 방향은 1씩 사용한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 방향 순서: 12시, 1시, 3시, 5시, 6시, 7시, 9시, 11시
val dy = intArrayOf(-1, -1, 0, 1, 1, 1, 0, -1)  
val dx = intArrayOf(0, 1, 1, 1, 0, -1, -1, -1)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다익스트라&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다익스트라는 넓게 퍼진다. 즉, 현재 발견됭 경로보다 더 짧은 경로를 찾으면, 큐에 계속 추가하게 된다. 예를 들어 &lt;code&gt;(3, 3)&lt;/code&gt;에 가는 비용 10을 큐에 넣었는데, 나중에 비용 5인 경로를 찾으면 또 넣는다. 이때 비용 10인 '쓸모없는 노드'도 본인 차례가 와서 삭제될 때까지 큐 한구석을 차지하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 중요한 점은 바로 비용이 같은 노드가 여러 개인 경우이다. &lt;code&gt;Heap&lt;/code&gt; 구조 특성상 넣은 순서는 무시되고, 비용으로만 따지게 된다. 따라서, &lt;code&gt;poll()&lt;/code&gt;을 호출했을 때, 비용이 0인 노드 중에 어떤 노드가 튀어나올 지 모른다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;. . . . .    . . . . .    . . 1 1 0    . . 1 1 0    . . 1 1 0  
. . . . .    . 1 1 K .    . 1 1 0 0    . 1 1 0 0    . 1 1 0 0  
. . K . . =&amp;gt; . 1 0 0 . =&amp;gt; . 1 0 K 0 =&amp;gt; . 1 0 0 0 =&amp;gt; . 1 0 0 0  
. . . . .    . 1 1 0 .    . 1 1 0 .    . 1 1 K 0    . 1 1 0 0  
. . . . *    . . . . *    . . . . *    . . . . *    . . 1 1 K&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자는 각 좌표에 도달하기 위한 최소 연료이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시와 같이, 최소 연료가 0인 노드 중 한 곳을 랜덤으로 가게 된다. 따라서, 다익스트라를 사용했을 때, &lt;code&gt;PriorityQueue&lt;/code&gt;에 쌓일 수 있는 최대 노드의 수는 18개인 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;0-1 BFS&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;0-1 BFS&lt;/code&gt;는 &lt;code&gt;DFS&lt;/code&gt;와 유사하게 한 방향으로 우직하게 탐색한다. 위 설명에서 적은 것과 같이, 가중치가 0인 경우에는 &lt;code&gt;addFirst&lt;/code&gt;를 호출하게 된다. 즉, &lt;code&gt;dy&lt;/code&gt;, &lt;code&gt;dx&lt;/code&gt; 배열에서 1, 2, 3번 인덱스 순서대로 &lt;code&gt;addFirst&lt;/code&gt;를 하게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, &lt;code&gt;while&lt;/code&gt; 문을 돌면서 &lt;code&gt;removeFirst()&lt;/code&gt;를 호출하면 누가 나올까? 3번 인덱스로 추가된 노드가 나오게 된다. 즉, 이를 계속 반복하면, &lt;code&gt;0-1 BFS&lt;/code&gt;는 사실상 &lt;code&gt;DFS&lt;/code&gt;와 유사하게 한 방향으로 탐색을 할 수 있게 되는 것이다. 이 특성 때문에, &lt;code&gt;Deque&lt;/code&gt;에는 최소한의 노드들만 쌓일 수 있게 된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;. . . . .    . . . . .    . . . . .  
. . . . .    . 1 1 0 .    . 1 1 0 .   
. . K . . =&amp;gt; . 1 1 0 . =&amp;gt; . 1 1 0 0  
. . . . .    . 1 1 K .    . 1 1 0 0  
. . . . *    . . . . *    . . 1 1 K&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시와 같이, 가중치가 0인 노드 중 한 방향으로만 탐색하게 되는 것이다. 따라서, &lt;code&gt;0-1 BFS&lt;/code&gt;를 사용했을 때, &lt;code&gt;ArrayDeque&lt;/code&gt;에 쌓일 수 있는 최대 노드의 수는 14개인 것이다. 4개 정도의 차이 뿐이지만, 지도의 크기가 최대 &lt;code&gt;500 * 500&lt;/code&gt;임을 따지면 나중에는 굉장히 큰 차이가 나게 되는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 두 가지 방식으로 풀 수 있지만, 가중치가 0과 1과 같이 단순한 그래프에서는 0-1 BFS 알고리즘을 채택하는게 더 효율이 좋다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  최종 코드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 다익스트라&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;메모리 : 148536 KB
시간 : 708 ms&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;import java.util.PriorityQueue
import java.util.StringTokenizer

private const val SEA = '.'
private const val REEF = '#'
private const val TREASURE = '*'
private const val SHIP = 'K'

fun main() = with(System.`in`.bufferedReader()) {
    val (h, w) = StringTokenizer(readLine()).run {
        nextToken().toInt() to nextToken().toInt()
    }

    // 0: 배, 1: 보물
    val info = Array(2) { 0 to 0 }

    val map = Array(h) { i -&amp;gt;
        val line = readLine()
        CharArray(w) { j -&amp;gt;
            val mark = when (val cur = line[j]) {
                SHIP -&amp;gt; {
                    info[0] = i to j
                    SEA
                }

                TREASURE -&amp;gt; {
                    info[1] = i to j
                    cur
                }

                else -&amp;gt; cur
            }

            mark
        }
    }

    println(dijkstra(map, info, h, w))

    close()
}

private data class State(val y: Int, val x: Int, val cost: Int)

private fun dijkstra(
    map: Array&amp;lt;CharArray&amp;gt;,
    info: Array&amp;lt;Pair&amp;lt;Int, Int&amp;gt;&amp;gt;,
    h: Int,
    w: Int
): Int {
    // 12시부터 시계 방향으로 8방향
    val dy = intArrayOf(-1, -1, 0, 1, 1, 1, 0, -1)
    val dx = intArrayOf(0, 1, 1, 1, 0, -1, -1, -1)

    val pq = PriorityQueue&amp;lt;State&amp;gt;(compareBy { it.cost })
    val minCost = Array(h) { IntArray(w) { Int.MAX_VALUE } }

    val (entryY, entryX) = info[0]
    val (treasureY, treasureX) = info[1]

    minCost[entryY][entryX] = 0
    pq.add(State(entryY, entryX, 0))

    while (pq.isNotEmpty()) {
        val (y, x, cost) = pq.poll()

        if (cost &amp;gt; minCost[y][x]) {
            continue
        }

        if (y == treasureY &amp;amp;&amp;amp; x == treasureX) {
            return cost
        }

        for (dir in 0 until 8) {
            val ny = y + dy[dir]
            val nx = x + dx[dir]

            if (ny in 0 until h &amp;amp;&amp;amp; nx in 0 until w &amp;amp;&amp;amp; map[ny][nx] != REEF) {
                val add = when (dir) {
                    1, 2, 3 -&amp;gt; 0
                    else -&amp;gt; 1
                }
                val nextCost = cost + add

                if (minCost[ny][nx] &amp;gt; nextCost) {
                    minCost[ny][nx] = nextCost
                    pq.add(State(ny, nx, nextCost))
                }
            }
        }
    }
    return -1
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0-1 BFS&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;메모리 : 31592 KB
시간 : 372 ms&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;import java.util.ArrayDeque  
import java.util.StringTokenizer  

private const val SEA = '.'  
private const val REEF = '#'  
private const val TREASURE = '*'  
private const val SHIP = 'K'  

fun main() = with(System.`in`.bufferedReader()) {  
    val (h, w) = StringTokenizer(readLine()).run {  
        nextToken().toInt() to nextToken().toInt()  
    }  

    // 0: 배, 1: 보물  
    val info = Array(2) { 0 to 0 }  

    val map = Array(h) { i -&amp;gt;  
        val line = readLine()  
        CharArray(w) { j -&amp;gt;  
            val mark = when (val cur = line[j]) {  
                SHIP -&amp;gt; {  
                    info[0] = i to j  
                    SEA  
                }  

                TREASURE -&amp;gt; {  
                    info[1] = i to j  
                    cur  
                }  

                else -&amp;gt; cur  
            }  

            mark  
        }  
    }  
    println(dijkstra(map, info, h, w))  

    close()  
}  

private data class State(val y: Int, val x: Int, val cost: Int)  

private fun dijkstra(  
    map: Array&amp;lt;CharArray&amp;gt;,  
    info: Array&amp;lt;Pair&amp;lt;Int, Int&amp;gt;&amp;gt;,  
    h: Int,  
    w: Int  
): Int {  
    // 12시부터 시계 방향으로 8방향  
    val dy = intArrayOf(-1, -1, 0, 1, 1, 1, 0, -1)  
    val dx = intArrayOf(0, 1, 1, 1, 0, -1, -1, -1)  

    val deque = ArrayDeque&amp;lt;State&amp;gt;()  
    val minCost = Array(h) { IntArray(w) { Int.MAX_VALUE } }  

    val (entryY, entryX) = info[0]  
    val (treasureY, treasureX) = info[1]  

    minCost[entryY][entryX] = 0  
    deque.add(State(entryY, entryX, 0))  

    while (deque.isNotEmpty()) {  
        val (y, x, cost) = deque.removeFirst()  

        if (cost &amp;gt; minCost[y][x]) {  
            continue  
        }  

        if (y == treasureY &amp;amp;&amp;amp; x == treasureX) {  
            return cost  
        }  

        for (dir in 0 until 8) {  
            val ny = y + dy[dir]  
            val nx = x + dx[dir]  

            if (ny in 0 until h &amp;amp;&amp;amp; nx in 0 until w &amp;amp;&amp;amp; map[ny][nx] != REEF) {  
                val add = when (dir) {  
                    1, 2, 3 -&amp;gt; 0  
                    else -&amp;gt; 1  
                }  
                val nextCost = cost + add  

                if (minCost[ny][nx] &amp;gt; nextCost) {  
                    minCost[ny][nx] = nextCost  
                    if (add == 0) {  
                        deque.addFirst(State(ny, nx, nextCost))  
                    } else {  
                        deque.addLast(State(ny, nx, nextCost))  
                    }  
                }  
            }  
        }  
    }  
    return -1  
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>PS/DFS, BFS, 백트래킹, 다익스트라</category>
      <author>Jwhy</author>
      <guid isPermaLink="true">https://jwhy-study.tistory.com/142</guid>
      <comments>https://jwhy-study.tistory.com/142#entry142comment</comments>
      <pubDate>Fri, 23 Jan 2026 16:49:37 +0900</pubDate>
    </item>
    <item>
      <title>[백준][Kotlin] - 거울 설치(2151)</title>
      <link>https://jwhy-study.tistory.com/141</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ️ 문제 레벨 : 골드3&lt;br /&gt;  문제 유형 : 다익스트라&lt;br /&gt;  풀이 언어 : Kotlin&lt;br /&gt; ️ 문제 링크 : &lt;a href=&quot;https://www.acmicpc.net/problem/2151&quot;&gt;백준 문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채영이는 거울을 들여다보는 것을 참 좋아한다. 그래서 집 곳곳에 거울을 설치해두고 집 안을 돌아다닐 때마다 거울을 보곤 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채영이는 새 해를 맞이하여 이사를 하게 되었는데, 거울을 좋아하는 그녀의 성격 때문에 새 집에도 거울을 매달만한 위치가 여러 곳 있다. 또한 채영이네 새 집에는 문이 두 개 있는데, 채영이는 거울을 잘 설치하여 장난을 치고 싶어졌다. 즉, 한 쪽 문에서 다른 쪽 문을 볼 수 있도록 거울을 설치하고 싶어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채영이네 집에 대한 정보가 주어졌을 때, 한 쪽 문에서 다른 쪽 문을 볼 수 있도록 하기 위해 설치해야 하는 거울의 최소 개수를 구하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거울을 설치할 때에는 45도 기울어진 대각선 방향으로 설치해야 한다. 또한 모든 거울은 양면 거울이기 때문에 양 쪽 모두에서 반사가 일어날 수 있다. 채영이는 거울을 매우 많이 가지고 있어서 거울이 부족한 경우는 없다고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거울을 어떻게 설치해도 한 쪽 문에서 다른 쪽 문을 볼 수 없는 경우는 주어지지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제에 대한 오해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 처음 보고, '거울을 45도 기울여 설치한다.'는 문구 때문에, 빛 자체가 대각선 방향으로 이동하는 줄 알고, 아래와 같은 형태가 정답인 줄 알았다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;***#*
*...*
*!..*
*.!.*
*#***&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문제를 풀이하다가, '거울이 기울어져야 하는데 어떻게 저런 정답이 나오지'라는 생각을 하게 되었다. 때문에 예제를 가지고 머릿속으로 시뮬레이션을 해보니까 내가 생각한 루트가 정답이 아니라는 것을 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 빛 자체는 상하좌우만 흐르는 것이고, 거울의 설치 각도만 45도인 것이기 때문에, 문제에서 주어진 예제의 정답 형태는 다음과 같은 것이다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;***#*
*...*
*!.!*
*...*
*#***&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀이를 하고, 또 다른 테스트 케이스를 찾으려고 보니까 나처럼 생각한 사람이 꽤나 많은 것 같았다. 나만 당한게 아닌 것 같아서 한 편으로는 뭔가 안도감이 들었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;알고리즘 선정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 어떤 알고리즘을 사용해야할까? 우선, 문에서 문으로 가는 최단 경로를 찾아야하기 때문에 BFS인 것 같다. 하지만, 이 문제는 &lt;b&gt;거울을 최소한으로 사용&lt;/b&gt;해서 최단 경로를 찾는 것이다. 따라서, 설치한 거울의 개수라는 가중치가 존재하기 때문에, 다익스트라를 사용해야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFS는 '한 번 더 이동하면 무조건 비용이 증가한다'의 느낌이고, 다익스트라는 '멀리 가도 싸게 갈 수 있고, 가까워도 비쌀 수 있다'의 느낌이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 마디로, 다익스트라는 누적 가중치(비용)가 가장 작은 경로부터 우선 탐색하며, 어떤 노드에 대해 이미 더 작은 비용으로 도달한 최단 기록이 있다면 그보다 큰 비용으로 도달한 경로는 이후에 확장해도 최단경로가 될 수 없기 때문에 탐색 대상에서 제외시키는 알고리즘인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 대한 더 자세한 내용은 아래 내용들을 보면 이해가 될 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방향 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 어느 문을 출발지로 잡든 결과가 동일하다. 나는 배열을 돌면서 가장 처음 발견하는 문을 시작하는 문으로 정했다. 우선, 예제를 통해서 문제의 주인공인 채영이가 문을 열었을 때의 시야 경로를 시각화해보자.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;***#*      ***#*      ***#*      ***#*      ***#*  
*.!1*      *.!1*      *.!1*      *.!1*      *.!1*  
*!.!*  =&amp;gt;  *!.2*  =&amp;gt;  *!32*  =&amp;gt;  *432*  =&amp;gt;  *432*  
*.!.*      *.!.*      *.!.*      *.!.*      *5!.*  
*#***      *#***      *#***      *#***      *#***&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예시를 보면 알 수 있듯이, 현재 좌표(x, y), 거울 사용 개수 그리고 &lt;b&gt;방향&lt;/b&gt;이 필요하다. 때문에, 다음과 같은 클래스 구조를 가질 필요가 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private data class Sight(  
    val y: Int,  
    val x: Int,  
    val dir: Int,  
    val totalMirrorCount: Int  
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 다음 방향은 어떻게 설정할 수 있을까? 탐색 중 거울을 설치할 수 있는 곳에 위치하게 될 경우, 갈 수 있는 방향은 3개이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 바라보고 있는 방향 그대로 직진&lt;/li&gt;
&lt;li&gt;거울을 현재 바라보고 있는 방향의 왼쪽으로 45도 기울여 설치&lt;/li&gt;
&lt;li&gt;거울을 현재 바라보고 있는 방향의 오른쪽으로 45도 기울여 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방향을 틀지 않고 직진, 바라보고 있는 곳을 기준으로 왼쪽 혹은 오른쪽으로 틀기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 단순히 &lt;code&gt;Info&lt;/code&gt; 클래스 안에 &lt;code&gt;nextDir&lt;/code&gt; 함수로 넣어두면 편하게 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 0: 아래, 1: 왼쪽, 2: 위, 3: 오른쪽
private val dy = intArrayOf(1, 0, -1, 0)
private val dx = intArrayOf(0, -1, 0, 1)

private data class Info(
    ...
    val dir: Int
) {
    fun nextDir() = when(dir) {
        0 -&amp;gt; 1 to 3  
        1 -&amp;gt; 2 to 0  
        2 -&amp;gt; 3 to 1  
        3 -&amp;gt; 0 to 2  
        else -&amp;gt;  throw IllegalArgumentException()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수를 통해 나온 값들은 &lt;code&gt;dy&lt;/code&gt;, &lt;code&gt;dx&lt;/code&gt;에 대한 인덱스 정보이다. 예를 들어, 좌측 방향(1)을 바라보고 있을 때, 갈 수 있는 방향은 2번 방향(위)과 0번 방향(아래)이 되는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최소한의 거울 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 시작하는 문에서 다음 문까지의 시야를 확보할 때, &lt;b&gt;최소한의 거울을 사용&lt;/b&gt;하여 도달을 해야한다. 이를 간단한 예시를 통해 확인해보자.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;**!#*
*!!.*
*!.!*
*.!.*
*#***&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에 있는 &lt;code&gt;map[0][3]&lt;/code&gt;에 있는 문에서 시작해보자. 이 문에서 바라볼 수 있는 방향은 왼쪽과 아래쪽이다. 우선 왼쪽으로 시작할 경우에 시야가 다음 문까지 도달할 수 있는 루트를 시각화해보자.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;**1#*  =&amp;gt;  **1#*  =&amp;gt;  **1#*  =&amp;gt;  **1#*    
*!!.*  =&amp;gt;  *!2.*  =&amp;gt;  *32.*  =&amp;gt;  *32.*    
*!.!*  =&amp;gt;  *!.!*  =&amp;gt;  *!.!*  =&amp;gt;  *4.!*    
*.!.*  =&amp;gt;  *.!.*  =&amp;gt;  *.!.*  =&amp;gt;  *5!.*    
*#***  =&amp;gt;  *#***  =&amp;gt;  *#***  =&amp;gt;  *#***&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래쪽 방향을 바라본 경우에 대한 루트도 시각화해보자.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;**!#*  =&amp;gt;  **!#*  =&amp;gt;  **!#*  =&amp;gt;  **!#*  
*!!1*  =&amp;gt;  *!!1*  =&amp;gt;  *!!1*  =&amp;gt;  *!!1*  
*!.!*  =&amp;gt;  *!.2*  =&amp;gt;  *432*  =&amp;gt;  *432*  
*.!.*  =&amp;gt;  *.!.*  =&amp;gt;  *.!.*  =&amp;gt;  *5!.*  
*#***  =&amp;gt;  *#***  =&amp;gt;  *#***  =&amp;gt;  *#***&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 예시를 가지고, 중간 지점인 &lt;code&gt;map[2][1]&lt;/code&gt;을 기준으로 확인해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왼쪽 방향 시작 : 3개 설치(&lt;code&gt;map[0][2]&lt;/code&gt;, &lt;code&gt;map[1][2]&lt;/code&gt;, &lt;code&gt;map[1][1]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;아래쪽 방향 시작 : 2개 설치(&lt;code&gt;map[2][3]&lt;/code&gt;, &lt;code&gt;map[2][1]&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결과를 보면, 특정 거울에 위치할 때, 현재까지 몇 개의 거울을 사용했는지는 방향에 따라 달라지게 된다는 것을 알 수 있다. 즉, 이 정보를 알 수 있다면 더 많은 거울을 설치해서 특정 위치에 도달한 노드는 탐색하지 않고 제거(스킵)할 수 있게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 각 위치마다 어떤 방향을 바라 봤을 때, 총 몇 개의 거울을 사용했는지에 대한 정보를 저장하기 위한 배열을 선언해주어야 한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val dists = Array(n) { Array(n) { IntArray(4) { Int.MAX_VALUE } } }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  최종 코드&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;메모리 : 24848 KB
시간 : 172 ms&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;import java.util.PriorityQueue  

private const val WALL = '*'  
private const val EMPTY = '.'  
private const val DOOR = '#'  
private const val MIRROR = '!'  

private data class Sight(  
    val y: Int,  
    val x: Int,  
    val dir: Int,  
    val totalMirrorCount: Int  
) {  
    fun nextDir() = when(dir) {  
        0 -&amp;gt; 1 to 3  
        1 -&amp;gt; 2 to 0  
        2 -&amp;gt; 3 to 1  
        3 -&amp;gt; 0 to 2  
        else -&amp;gt; -1 to -1  
    }  
}  

fun main() = with(System.`in`.bufferedReader()){  
    val n = readLine().toInt()  

    val doors = ArrayList&amp;lt;Pair&amp;lt;Int, Int&amp;gt;&amp;gt;(2)  

    val map = Array(n) { i -&amp;gt;  
        val line = readLine()  
        CharArray(n) { j -&amp;gt;  
            if (line[j] == DOOR) {  
                doors.add(i to j)  
            }  
            line[j]  
        }  
    }  
    println(dijkstra(map, doors, n))  

    close()  
}  

private fun dijkstra(  
    map: Array&amp;lt;CharArray&amp;gt;,  
    doors: ArrayList&amp;lt;Pair&amp;lt;Int, Int&amp;gt;&amp;gt;,  
    n: Int  
): Int {  
    val dy = intArrayOf(1, 0, -1, 0)  
    val dx = intArrayOf(0, -1, 0, 1)  

    val pq = PriorityQueue&amp;lt;Sight&amp;gt;(compareBy { it.totalMirrorCount })  
    val dists = Array(n) { Array(n) { IntArray(4) { Int.MAX_VALUE } } }  

    val (entryY, entryX) = doors[0]  
    val (exitY, exitX) = doors[1]  

    for (dir in 0 until 4) {  
        dists[entryY][entryX][dir] = 0  
        pq.add(Sight(entryY, entryX, dir, 0))  
    }  

    while (pq.isNotEmpty()) {  
        val info = pq.poll()  
        val (y, x, dir, totalMirrorCount) = info  

        if (totalMirrorCount &amp;gt; dists[y][x][dir]) {  
            continue  
        }  

        val ny = y + dy[dir]  
        val nx = x + dx[dir]  

        if (ny in 0 until n &amp;amp;&amp;amp; nx in 0 until n &amp;amp;&amp;amp; map[ny][nx] != WALL) {  
            // 1. 거울을 설치하지 않고 직진하는 경우  
            if (dists[ny][nx][dir] &amp;gt; totalMirrorCount) {  
                dists[ny][nx][dir] = totalMirrorCount  
                pq.add(Sight(ny, nx, dir, totalMirrorCount))  
            }  

            // 2. 거울을 설치하고 방향을 바꾸는 경우  
            if (map[ny][nx] == MIRROR) {  
                val (opt1, opt2) = info.nextDir()  
                val nextMirrorCount = totalMirrorCount + 1  

                // 방향 전환 1
                if (dists[ny][nx][opt1] &amp;gt; nextMirrorCount) {  
                    dists[ny][nx][opt1] = nextMirrorCount  
                    pq.add(Sight(ny, nx, opt1, nextMirrorCount))  
                }  
                // 방향 전환 2
                if (dists[ny][nx][opt2] &amp;gt; nextMirrorCount) {  
                    dists[ny][nx][opt2] = nextMirrorCount  
                    pq.add(Sight(ny, nx, opt2, nextMirrorCount))  
                }  
            }  
        }  
    }  

    return dists[exitY][exitX].minOrNull() ?: -1  
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>PS/DFS, BFS, 백트래킹, 다익스트라</category>
      <category>다익스트라</category>
      <category>코틀린</category>
      <author>Jwhy</author>
      <guid isPermaLink="true">https://jwhy-study.tistory.com/141</guid>
      <comments>https://jwhy-study.tistory.com/141#entry141comment</comments>
      <pubDate>Thu, 22 Jan 2026 18:04:12 +0900</pubDate>
    </item>
    <item>
      <title>[백준][Kotlin] - 스티커 붙이기(18808)</title>
      <link>https://jwhy-study.tistory.com/140</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 소개&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ️ 문제 레벨 : 골드3&lt;br /&gt;  문제 유형 : 구현, 시뮬레이션, 브루트포스&lt;br /&gt;  풀이 언어 : Kotlin&lt;br /&gt; ️ 문제 링크 : &lt;a href=&quot;https://www.acmicpc.net/problem/18808&quot;&gt;백준 문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혜윤이는 최근에 다양한 대회를 참여하면서 노트북에 붙일 수 있는 스티커들을 많이 받았다. 스티커는 아래와 같이 사각 모눈종이 위에 인쇄되어 있으며, 스티커의 각 칸은 상하좌우로 모두 연결되어 있다. 또한 모눈종이의 크기는 스티커의 크기에 꼭 맞아서, 상하좌우에 스티커가 포함되지 않는 불필요한 행이나 열이 존재하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혜윤이는 자신의 노트북에 이 스티커들을 붙이기로 했다. 혜윤이의 노트북은 마침 직사각형 모양이고, 스티커가 인쇄된 모눈종이와 같은 간격으로 격자가 그려져 있다. 혜윤이는 스티커들을 먼저 받았던 것부터 차례대로 격자에 맞춰서 붙이려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혜윤이가 스티커를 붙이는 방법은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스티커를 회전시키지 않고 모눈종이에서 떼어낸다.&lt;/li&gt;
&lt;li&gt;다른 스티커와 겹치거나 노트북을 벗어나지 않으면서 스티커를 붙일 수 있는 위치를 찾는다. 혜윤이는 노트북의 위쪽부터 스티커를 채워 나가려고 해서, 스티커를 붙일 수 있는 위치가 여러 곳 있다면 가장 위쪽의 위치를 선택한다. 가장 위쪽에 해당하는 위치도 여러 곳이 있다면 그중에서 가장 왼쪽의 위치를 선택한다.&lt;/li&gt;
&lt;li&gt;선택한 위치에 스티커를 붙인다. 만약 스티커를 붙일 수 있는 위치가 전혀 없어서 스티커를 붙이지 못했다면, 스티커를 시계 방향으로 90도 회전한 뒤 2번 과정을 반복한다.&lt;/li&gt;
&lt;li&gt;위의 과정을 네 번 반복해서 스티커를 0도, 90도, 180도, 270도 회전시켜 봤음에도 스티커를 붙이지 못했다면 해당 스티커를 붙이지 않고 버린다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혜윤이는 스티커를 다 붙인 후의 노트북의 모습이 궁금해졌다. 노트북의 크기와 스티커들이 주어졌을 때 스티커들을 차례대로 붙이고 난 후 노트북에서 몇 개의 칸이 채워졌는지 구해보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 알고리즘 분석을 해봤다. 스티커가 주어지고, 그 스티커를 90도씩 돌려가면서 노트북에 붙여야하기 때문에 구현 + 시뮬레이션이 확실함을 느꼈다. 즉, 가능한 모든 경우의 수를 대입하면서 확인해야하는 브루트 포스를 사용해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1초에 약 1억 번의 연산이 가능하다고 가정하고, 문제의 조건을 분석해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스티커 최대 개수: 100&lt;/li&gt;
&lt;li&gt;노트북 최대 크기: 40 x 40&lt;/li&gt;
&lt;li&gt;스티커 최대 크기: 10 x 10&lt;/li&gt;
&lt;li&gt;회전 횟수: 4&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;100 * 4 * (40 * 40) * (10 * 10)&lt;/code&gt;으로 총 6,400,000번의 연산으로 2초 내에 충분히 통과할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이 문제를 풀기 위한 핵심 로직들을 나눠서 정리해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도형 회전&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 문제에서 나온 것과 같이 모눈 종이의 크기는 &lt;code&gt;r * c&lt;/code&gt; 형태이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val shape = Array(r) { IntArray(c) }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 예시 중 2번 도형을 가져와보자. 이 도형은 &lt;code&gt;-ㅜ&lt;/code&gt; 모양이며, 그래프로 나타낼 경우, &lt;code&gt;0, 1, 2, 3, 4, 8&lt;/code&gt;번이 스티커로 이루어진 모눈 종이이다. 이 스티커를 90도 뒤집으면서 회전 코드를 만들어보자.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;-- 원본 --

[0, 1, 2, 3, 4]
[5, 6, 7, 8, 9]

-- 90도 회전 후 --

[5, 0]
[6, 1]
[7, 2]
[8, 3]
[9, 4]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보는 것과 같이 배열의 구조 자체가 바뀌게 된다. 이를 코드로 나타내보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 원본
val shape = Array(r) { IntArray(c) }
val originRows = shape.size
val originCols = shape[0].size

// 회전된 배열
val rotated = Array(c) { IntArray(r) }
val rotatedRows = rotated.size
val rotatedCols = rotated[0].size&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 배열 구조는 위와 같이 바뀌게 된다. 그렇다면 데이터는 어떨까? 원본의 0번 행이 회전 후 각 행의 끝 열에 들어가게 되고, 원본의 1번 행은 각 행의 끝 열 - 1에 들어가게 된다. 즉, 식과 코드로 나타내면 다음과 같은 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원본: 0번 행 -&amp;gt; 회전: 각 행의 &lt;code&gt;originRows - 1&lt;/code&gt;번 열&lt;/li&gt;
&lt;li&gt;원본: 1번 행 -&amp;gt; 회전: 각 행의 &lt;code&gt;originRows - 1 - 1&lt;/code&gt;번 열&lt;/li&gt;
&lt;li&gt;원본: 2번 행 -&amp;gt; 회전: 각 행의 &lt;code&gt;originRows - 1 - 2&lt;/code&gt;번 열&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;for (r in 0 until originRows) {  
    for (c in 0 until originCols) {  
        newShape[c][originRows - 1 - r] = shape[r][c]  
    }  
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배치 가능 여부 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 스티커 범위 확인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 스티커를 노트북에 붙일 수 있는지 확인해야 한다. 만약, 스티커의 가로 크기가 노트북의 가로 크기를 벗어나거나, 스티커의 세로 크기가 노트북의 세로 크기를 벗어날 경우 회전을 시켜야 한다. 노트북의 크기가 &lt;code&gt;2(세로) * 4(가로)&lt;/code&gt;라고 가정해보자. 만약, 스티커가 &lt;code&gt;4(세로) * 1(가로)&lt;/code&gt;인 경우 회전을 하면 붙일 수 있기 때문에 꼭 필요한 로직이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;for (rotate in 0..3) {  
    if (sticker.rows &amp;gt; n || sticker.cols &amp;gt; m) {  
        sticker.rotate()  
        continue  
    }

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 스티커를 붙일 수 있는 좌표 탐색&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스티커를 붙일 수 있다면, 어디에 붙일지 확인을 해야한다. 하지만, 모든 범위를 다 검사하기에는 너무 많은 범위를 탐색하게 된다. 우리는 필요하지 않은 부분은 굳이 탐색하지 않도록 최적화를 시켜야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는, 문제에 힌트로 제공되어 있다. 스티커를 붙일 때는, 노트북의 왼쪽 위부터 붙인다는 조건이 나와있다. 즉, 우리가 탐색할 범위는 노트북의 왼쪽 위부터 스티커를 붙였을 때, 스티커가 완전히 붙을 수 있는 범위만 탐색하면 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 크기가 &lt;code&gt;3 * 3&lt;/code&gt;인 노트북에 &lt;code&gt;2 * 2&lt;/code&gt; 크기의 스티커를 붙인다고 가정해보자. 이 스티커를 붙일 수 있는 위치는 다음과 같을 것이다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[1, 1, 0] -&amp;gt; [0, 1, 1] -&amp;gt; [0, 0, 0] -&amp;gt; [0, 0, 0]  
[1, 1, 0] -&amp;gt; [0, 1, 1] -&amp;gt; [1, 1, 0] -&amp;gt; [0, 1, 1]  
[0, 0, 0] -&amp;gt; [0, 0, 0] -&amp;gt; [1, 1, 0] -&amp;gt; [0, 1, 1]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 노트북의 크기가 &lt;code&gt;n * m&lt;/code&gt;이고, 스티커의 크기가 &lt;code&gt;rows * cols&lt;/code&gt;일 때, 스티커의 왼쪽 상단 꼭지점이 위치할 수 있는 위치는 다음과 같이 제한적이게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세로 범위 : 0 ~ (n - rows)&lt;/li&gt;
&lt;li&gt;가로 범위 : 0 ~ (m - cols)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;for (i in 0..(n - sticker.rows)) {  
    for (j in 0..(m - sticker.cols)) {  
        ...
    }  
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 스티커 붙이기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스티커를 붙일 좌표를 선정했다면, 그 위치에 스티커를 붙일 수 있는지 확인하면 된다. 만약 붙이지 못할 경우 회전시켜서 다시 확인하면 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 앞에서 얻은 &lt;code&gt;(i, j)&lt;/code&gt;를 활용해서 스티커를 붙일 수 있는지 확인해보자. &lt;code&gt;Sticker&lt;/code&gt; 클래스에 있는 &lt;code&gt;shape&lt;/code&gt;는 사실상 모눈 종이이고, 그 위에 1로 된 데이터가 실제 스티커가 있는 위치이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 스티커를 붙일 노트북의 위치에 이미 스티커가 붙어있다면 바로 &lt;code&gt;false&lt;/code&gt;를 반환하면 되는 것이고, 만약 모두 붙일 수 있다면 &lt;code&gt;true&lt;/code&gt;를 반환해주면 되는 것이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun canPlace(
    laptop: Array&amp;lt;IntArray&amp;gt;, sticker: Sticker, i: Int, j: Int
): Boolean {  
    for (r in 0 until sticker.rows) {  
        for (c in 0 until sticker.cols) {  
            if (sticker.shape[r][c] == 1 &amp;amp;&amp;amp; laptop[i + r][j + c] == 1) {
                return false  
            }  
        }  
    }  

    return true  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 &lt;code&gt;canPlace&lt;/code&gt; 함수를 조금만 수정하면 &lt;code&gt;laptop&lt;/code&gt;에 스티커를 붙일 수 있게 된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun place(
    laptop: Array&amp;lt;IntArray&amp;gt;, sticker: Sticker, i: Int, j: Int
) {  
    for (r in 0 until sticker.rows) {  
        for (c in 0 until sticker.cols) {  
            if (sticker.shape[r][c] == 1) {  
                laptop[i + r][j + c] = 1  
            }  
        }  
    }  
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  최종 코드&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;메모리 : 26496 KB
시간 : 168 ms&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;import java.util.*  

private class Sticker(var shape: Array&amp;lt;IntArray&amp;gt;) {  
    var rows: Int = shape.size  
    var cols: Int = if (shape.isEmpty()) 0 else shape[0].size  

    fun rotate() {  
        if (rows == 0 || cols == 0) return  

        val newShape = Array(cols) { IntArray(rows) }  
        for (r in 0 until rows) {  
            for (c in 0 until cols) {  
                newShape[c][rows - 1 - r] = shape[r][c]  
            }  
        }  
        shape = newShape  
        rows = shape.size  
        cols = shape[0].size  
    }  
}  

fun main() = with(System.`in`.bufferedReader()) {  
    val (n, m, k) = StringTokenizer(readLine()).run {  
        Triple(
            nextToken().toInt(), nextToken().toInt(), nextToken().toInt()
        )
    }  

    val laptop = Array(n) { IntArray(m) { 0 } }  

    repeat(k) {  
        val (r, c) = StringTokenizer(readLine()).run {  
            nextToken().toInt() to nextToken().toInt()  
        }  
        val shape = Array(r) {  
            StringTokenizer(readLine()).run {  
                IntArray(c) { nextToken().toInt() }  
            }        
        }

        val sticker = Sticker(shape)  

        for (rotate in 0..3) {  
            if (sticker.rows &amp;gt; n || sticker.cols &amp;gt; m) {  
                sticker.rotate()  
                continue  
            }  

            var placed = false  

            for (i in 0..(n - sticker.rows)) {  
                for (j in 0..(m - sticker.cols)) {  
                    if (canPlace(laptop, sticker, i, j)) {  
                        place(laptop, sticker, i, j)  
                        placed = true  
                        break                    
                    }
                }  
                if (placed) break  
            }  

            if (placed) break  

            sticker.rotate()  
        }  
    }  

    println(laptop.sumOf { it.sum() })  
}  

private fun canPlace(
    laptop: Array&amp;lt;IntArray&amp;gt;, sticker: Sticker, i: Int, j: Int
): Boolean {  
    for (r in 0 until sticker.rows) {  
        for (c in 0 until sticker.cols) {  
            if (sticker.shape[r][c] == 1 &amp;amp;&amp;amp; laptop[i + r][j + c] == 1) {
                return false  
            }  
        }  
    }  

    return true  
}  

private fun place(
    laptop: Array&amp;lt;IntArray&amp;gt;, sticker: Sticker, i: Int, j: Int
) {  
    for (r in 0 until sticker.rows) {  
        for (c in 0 until sticker.cols) {  
            if (sticker.shape[r][c] == 1) {  
                laptop[i + r][j + c] = 1  
            }  
        }  
    }  
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>PS/구현, 시뮬레이션</category>
      <category>java</category>
      <category>kotlin</category>
      <category>구현</category>
      <category>백준</category>
      <category>스티커 붙이기</category>
      <category>자바</category>
      <category>코틀린</category>
      <author>Jwhy</author>
      <guid isPermaLink="true">https://jwhy-study.tistory.com/140</guid>
      <comments>https://jwhy-study.tistory.com/140#entry140comment</comments>
      <pubDate>Tue, 20 Jan 2026 14:40:55 +0900</pubDate>
    </item>
  </channel>
</rss>