<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>rootTiket</title>
    <link>https://root-2707.tistory.com/</link>
    <description>꾸준하게, 얕지않게</description>
    <language>ko</language>
    <pubDate>Wed, 13 May 2026 05:39:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>rootTiket</managingEditor>
    <item>
      <title>[시스템 설계] 커머스 도메인 주문-결제 분산 트랜잭션 설계</title>
      <link>https://root-2707.tistory.com/10</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;0.&lt;/span&gt; 설계 경계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설계하고자 하는 분산 트랜잭션 서비스는 글로벌 한정판 스니커즈 Drop 커머스다.&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;100만 명&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;판매 재고는 &lt;b&gt;1,000족&lt;/b&gt;뿐이다.&lt;/li&gt;
&lt;li&gt;선착순 공정성을 보장해야 한다.&lt;/li&gt;
&lt;li&gt;1족도 초과 판매되면 안 된다.&lt;/li&gt;
&lt;li&gt;주문서 진입 후 &lt;b&gt;5분 안에 결제가 끝나지 않으면&lt;/b&gt; 재고를 돌려줘야 한다.&lt;/li&gt;
&lt;li&gt;사용자는 최대 &lt;b&gt;10분 안에&lt;/b&gt; 성공 또는 실패 피드백을 받아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 대기열이 100만 명을 그대로 주문 서버로 보내지 않고, 입장 순서와 트래픽을 제어한다고 본다. 즉, 주문 API에는 대기열을 통과한 사용자만 들어온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 재고 서비스가 초과 판매를 막기 위해 재고를 먼저 선점한다고 본다. 내부 구현은 Redis 분산 락이든 DB 조건부 업데이트든 가능하지만, Part 3에서는 재고 서비스가 다음 명령을 원자적으로 처리할 수 있다고 가정한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;ReserveInventory(orderId, skuId, ttl=5m)
ConfirmReservation(orderId)
ReleaseReservation(orderId)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 설계의 관심사는 그 다음이다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;주문 생성
&amp;rarr; 재고 예약
&amp;rarr; 외부 PG 결제 승인 요청
&amp;rarr; 재고 확정
&amp;rarr; 주문 성공 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름은 Order, Inventory, Payment, 외부 PG까지 걸쳐 있다. 모두 다른 서비스이고, 각자 다른 DB를 가진다. 여기서 &lt;b&gt;&quot;한 번에 모두 성공하거나 모두 실패하는 것처럼 보이게 만드는 것&quot;&lt;/b&gt;이 이번 설계의 목표다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;1.&lt;/span&gt; 먼저 실패 시나리오부터 생각했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &quot;Saga 패턴을 쓰면 되지 않을까?&quot; 정도로 생각했다. 그런데 그렇게 시작하면 글이 너무 추상적이 된다.&lt;/p&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;주문 서비스가 Order DB에 주문을 저장했다. 그런데 Kafka에 &lt;code&gt;PaymentApproveRequested&lt;/code&gt; 이벤트를 보내기 직전에 서버가 죽었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 DB에는 주문이 있는데, 결제 서비스는 이 주문을 모른다. 사용자는 주문을 눌렀지만 결제는 시작되지 않는다. 이런 주문은 운영에서 가장 곤란하다. 실패도 아니고 성공도 아닌 상태로 남기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 단순 재시도로 해결되지 않는다. 서버가 죽으면 재시도 로직도 같이 죽는다.&lt;/p&gt;
&lt;div class=&quot;highlight-box&quot;&gt;&amp;rarr; 그래서 &lt;b&gt;Outbox&lt;/b&gt;가 필요하다.&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 2. PG에서는 결제가 승인됐는데 우리 서버는 Timeout을 받는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Payment Service가 PG에 승인 요청을 보냈다. PG 내부에서는 결제가 승인됐다. 그런데 네트워크 문제로 응답이 유실됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 서버 입장에서는 Timeout이다. 하지만 이걸 실패로 처리하면 안 된다. 실제로는 돈이 빠져나갔을 수 있기 때문이다.&lt;/p&gt;
&lt;div class=&quot;highlight-box&quot;&gt;&amp;rarr; 그래서 FAILED가 아니라 &lt;b&gt;UNKNOWN&lt;/b&gt; 상태가 필요하다.&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 3. 사용자가 결제 버튼을 여러 번 누른다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한정판 구매 화면에서 사용자가 불안해서 결제 버튼을 여러 번 누를 수 있다. 브라우저 재시도, 앱 재시도, HTTP client retry, Kafka 재전달까지 생각하면 같은 요청은 여러 번 들어온다고 보는 게 맞다.&lt;/p&gt;
&lt;div class=&quot;highlight-box&quot;&gt;&amp;rarr; 그래서 모든 명령은 &lt;b&gt;멱등&lt;/b&gt;해야 한다.&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 4. 결제는 성공했는데 재고 확정이 실패한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PG 결제는 성공했다. 그런데 Inventory Service가 일시 장애라 &lt;code&gt;ConfirmReservation&lt;/code&gt;이 실패했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 바로 환불하면 안 된다. 이미 5분 TTL 안에 예약된 재고가 살아 있다면, 재고 확정을 재시도해서 주문 성공으로 처리시키는 게 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 예약이 이미 만료되어 다른 사용자에게 넘어갔다면, 그 결제는 취소 또는 환불해야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;정리하면 이번 설계의 핵심은 &quot;정상 흐름&quot;이 아니라 &quot;애매한 중간 상태를 어떻게 끝까지 책임질 것인가&quot;다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;2.&lt;/span&gt; 왜 2PC를 선택하지 않았나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 트랜잭션을 들으면 가장 먼저 2PC가 떠오른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2PC는 Coordinator가 각 서비스에 먼저 준비 가능한지 묻고, 모두 가능하다고 하면 한 번에 커밋을 지시하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;1. Prepare
2. Commit or Rollback&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;첫째, 외부 PG는 2PC에 참여하지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Order DB, Inventory DB는 우리가 통제할 수 있다. 하지만 PG사는 우리 트랜잭션 참여자가 아니다. &quot;prepare만 해두고 기다려줘&quot;, &quot;이제 commit 해줘&quot;, &quot;rollback 해줘&quot; 같은 식으로 지시할 수 없다. 결제 승인 이후 취소는 가능하지만, 그건 트랜잭션 롤백이 아니라 별도의 보상 트랜잭션이다. 이 지점에서 이미 2PC는 핵심 문제를 해결하지 못한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;둘째, 느린 외부 요청이 내부 트랜잭션을 붙잡는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2PC나 그와 비슷한 동기식 설계를 택하면, Order DB와 Inventory DB가 결제 응답을 기다리는 동안 리소스를 잡고 있을 가능성이 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 시스템의 핵심 주문/결제 API 목표 응답 시간은 &lt;b&gt;200ms 이하&lt;/b&gt;다. PG API가 1초만 느려져도 내부 트랜잭션, DB 커넥션, 애플리케이션 스레드가 같이 붙잡힌다. 100만 명이 몰리는 Drop 상황에서 이런 구조는 장애를 전파한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;셋째, MSA의 독립성을 깨뜨린다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 각 서비스가 자기 데이터에 대한 로컬 트랜잭션을 짧게 끝내야 한다. 2PC는 여러 서비스의 커밋 결정을 하나의 Coordinator에 강하게 묶는다. 한 서비스의 지연이 전체 지연이 되고, 한 서비스의 장애가 전체 장애가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;이번 설계에서는 실패를 보상하며 최종적으로 올바른 상태에 도달하는 구조가 더 적합하다고 봤다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;3.&lt;/span&gt; Saga 중에서도 Orchestration을 선택한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Saga는 하나의 큰 트랜잭션을 여러 로컬 트랜잭션으로 나누고, 실패하면 보상 트랜잭션을 실행하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Saga에는 두 가지 방식이 있다.&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;Choreography:&lt;/b&gt; 각 서비스가 이벤트를 보고 다음 행동을 스스로 결정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Orchestration:&lt;/b&gt; 중앙 Orchestrator가 전체 진행 상태와 보상 순서를 관리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 Orchestration을 선택했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 Choreography가 아니라 Orchestration인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Choreography는 단순한 이벤트 흐름에는 잘 맞는다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;OrderCreated
&amp;rarr; InventoryReserved
&amp;rarr; PaymentApproved
&amp;rarr; OrderCompleted&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;하지만 이번 주문-결제 흐름에서는 애매한 상태가 많다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결제 Timeout은 실패가 아니라 &lt;code&gt;UNKNOWN&lt;/code&gt;일 수 있다.&lt;/li&gt;
&lt;li&gt;5분 결제 제한 시간이 있다.&lt;/li&gt;
&lt;li&gt;결제 승인 후 재고 확정 실패 시 재시도와 환불 중 하나를 선택해야 한다.&lt;/li&gt;
&lt;li&gt;늦은 PG Webhook이 도착할 수 있다.&lt;/li&gt;
&lt;li&gt;사용자는 10분 이내에 최종 상태를 알아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 조건이 있으면 &quot;이 주문이 지금 정확히 어느 단계인가&quot;를 한 곳에서 추적하는 편이 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;OrderSagaOrchestrator&lt;/code&gt;를 둔다. 이 컴포넌트는 주문별 Saga 상태를 관리하고, 다음 명령을 내리고, 보상 트랜잭션을 실행한다.&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;Order Service:&lt;/b&gt; 주문 상태와 Saga 진행 상태 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Inventory Service:&lt;/b&gt; 예약, 확정, 해제 로컬 트랜잭션 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Payment Service:&lt;/b&gt; PG 승인 요청, 상태 조회, 취소/환불 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;4.&lt;/span&gt; 전체 구조&lt;/h2&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;User &amp;rarr; [Waiting Queue] &amp;rarr; [API Gateway] &amp;rarr; [Order Service / OrderSagaOrchestrator]
                                                     │
                              ┌──────────────────────┼──────────────────────┐
                              &amp;darr;                      &amp;darr;                      &amp;darr;
                         [Order DB]              [Kafka]              [Client Polling/SSE]
                                                /        \
                                    [Inventory Service]  [Payment Service]
                                        │    │                │    │
                                   [Inv DB] [Redis]      [Pay DB] [External PG]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름을 한 문장으로 말하면 이렇다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;대기열이 구매 기회를 제한하고, 재고 서비스가 5분 동안 재고를 예약하고, 주문 Orchestrator가 결제와 재고 확정을 끝까지 추적한다.&lt;/b&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; 외부 PG 호출은 절대 내부 DB 트랜잭션 안에서 하지 않는다는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;5.&lt;/span&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;반대로 주문서 진입 시점에 바로 판매 확정으로 차감해버리면, 결제 실패나 이탈 때 재고 복구가 복잡해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 중간 상태가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;AVAILABLE &amp;rarr; RESERVED &amp;rarr; SOLD
RESERVED  &amp;rarr; RELEASED&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문서에 들어온 사용자는 재고를 &lt;b&gt;5분 동안 점유&lt;/b&gt;한다. 이 5분 안에 결제에 성공하면 SOLD로 확정한다. 결제 실패, 결제 포기, 만료가 발생하면 RELEASED로 되돌린다.&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;시스템은 결제 실패 시 재고를 다음 대기자에게 넘길 수 있다.&lt;/li&gt;
&lt;li&gt;초과 판매는 Inventory Service의 예약 원자성으로 막는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;6.&lt;/span&gt; 정상 흐름&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;User          OrderSagaOrchestrator    Inventory Service    Payment Service    External PG
 │                    │                       │                   │               │
 │─ POST /orders ────▶│                       │                   │               │
 │                    │─ Local TX: Order(CREATED)                 │               │
 │                    │─ ReserveInventory ───▶│                   │               │
 │                    │                       │─ Local TX: RESERVED               │
 │                    │◀─ InventoryReserved ──│                   │               │
 │                    │─ Local TX: Order(WAITING_PAYMENT)         │               │
 │◀─ orderId, expiresAt ─│                    │                   │               │
 │                    │                       │                   │               │
 │─ POST /payments/confirm ▶│                 │                   │               │
 │                    │─ Local TX: Order(PAYMENT_IN_PROGRESS) + Outbox            │
 │◀─ 202 Accepted ───│                       │                   │               │
 │                    │─ PaymentApproveRequested ────────────────▶│               │
 │                    │                       │                   │─ Approve() ──▶│
 │                    │                       │                   │◀─ Approved ───│
 │                    │                       │                   │─ Local TX: APPROVED + Outbox
 │                    │◀─ PaymentApproved ────────────────────────│               │
 │                    │─ ConfirmReservation ─▶│                   │               │
 │                    │                       │─ Local TX: RESERVED &amp;rarr; SOLD        │
 │                    │◀─ InventoryConfirmed ─│                   │               │
 │                    │─ Local TX: Order(PAID)                    │               │
 │◀─ SSE: success ───│                       │                   │               │&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;POST /payments/confirm&lt;/code&gt;은 결제 완료를 기다리지 않는다. 결제 승인 요청을 접수하고 &lt;b&gt;202 Accepted&lt;/b&gt;를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 PG 호출은 느릴 수 있기 때문이다. PG가 느려졌다고 해서 주문 API의 스레드와 DB 트랜잭션이 같이 느려지면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 이후 주문 상태 조회나 SSE를 통해 최종 결과를 받는다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /orders/{orderId}
{
  &quot;orderId&quot;: &quot;ord_123&quot;,
  &quot;status&quot;: &quot;PAYMENT_IN_PROGRESS&quot;,
  &quot;message&quot;: &quot;결제 승인 처리 중입니다.&quot;
}&lt;/code&gt;&lt;/pre&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;성공:&lt;/b&gt; &lt;code&gt;PAID&lt;/code&gt;, &lt;code&gt;COMPLETED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실패:&lt;/b&gt; &lt;code&gt;CANCELLED&lt;/code&gt;, &lt;code&gt;EXPIRED&lt;/code&gt;, &lt;code&gt;PAYMENT_FAILED&lt;/code&gt;, &lt;code&gt;REFUNDED&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;7.&lt;/span&gt; 외부 PG 호출을 내부 트랜잭션에서 분리한다&lt;/h2&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;@Transactional
fun confirmPayment(orderId: String) {
    orderRepository.updateStatus(orderId, PAYMENT_IN_PROGRESS)

    val result = pgClient.approve(orderId)   // &amp;larr; 위험

    paymentRepository.save(result)
    inventoryClient.confirm(orderId)
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;warn-box&quot;&gt;&lt;b&gt;문제:&lt;/b&gt; &lt;code&gt;pgClient.approve()&lt;/code&gt;가 3초 동안 응답하지 않으면 DB 트랜잭션도 3초 동안 열린다. PG Timeout은 내부 DB 커넥션, row lock, 애플리케이션 스레드를 같이 잡아둔다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음처럼 나눈다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Local TX:    Order status update + outbox insert      &amp;larr; 짧게 끝냄
No TX:       External PG HTTP call                   &amp;larr; 트랜잭션 밖
Local TX:    Payment result update + outbox insert   &amp;larr; 짧게 끝냄&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Order DB에 결제 진행 상태를 저장한다.&lt;/li&gt;
&lt;li&gt;같은 트랜잭션에서 Outbox에 &lt;code&gt;PaymentApproveRequested&lt;/code&gt;를 저장한다.&lt;/li&gt;
&lt;li&gt;트랜잭션을 커밋한다.&lt;/li&gt;
&lt;li&gt;Outbox Relay가 Kafka로 이벤트를 발행한다.&lt;/li&gt;
&lt;li&gt;Payment Service Consumer가 PG를 호출한다.&lt;/li&gt;
&lt;li&gt;PG 결과를 Payment DB에 저장하고 다시 이벤트를 발행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 PG가 느려져도 Order DB 트랜잭션은 영향을 받지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;8.&lt;/span&gt; PG 승인 Timeout은 실패가 아니라 UNKNOWN이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요하게 다룬 케이스다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Payment Service &amp;rarr; PG 승인 요청
PG 내부 승인 성공
PG &amp;rarr; Payment Service 응답 유실
Payment Service: Timeout 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 우리 서버가 받은 건 Timeout이다. 하지만 PG에서는 결제가 승인됐을 수 있다.&lt;/p&gt;
&lt;div class=&quot;warn-box&quot;&gt;Timeout을 &lt;code&gt;PAYMENT_FAILED&lt;/code&gt;로 처리하면 안 된다. 재고를 풀어버리고 주문을 취소했는데, 실제로는 고객 카드에서 돈이 빠져나간 상태가 될 수 있다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 케이스는 반드시 &lt;code&gt;PAYMENT_UNKNOWN&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;1160&quot; data-origin-height=&quot;962&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blAiVQ/dJMcabYiuiw/nfFrK8uyBnfZkqKSsVbqfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blAiVQ/dJMcabYiuiw/nfFrK8uyBnfZkqKSsVbqfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blAiVQ/dJMcabYiuiw/nfFrK8uyBnfZkqKSsVbqfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblAiVQ%2FdJMcabYiuiw%2FnfFrK8uyBnfZkqKSsVbqfK%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;962&quot; data-origin-width=&quot;1160&quot; data-origin-height=&quot;962&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;Timeout은 실패가 아니라 &lt;b&gt;UNKNOWN&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;승인 요청에는 반드시 &lt;b&gt;idempotencyKey&lt;/b&gt;를 붙인다.&lt;/li&gt;
&lt;li&gt;PG 상태 조회 API로 실제 승인 여부를 확인한 뒤 다음 단계로 간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;9.&lt;/span&gt; 5분 제한 시간이 지나면 어떻게 할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 요구사항은 명확하다. 주문서 진입 후 5분 이내에 결제가 완료되지 않으면 재고를 환원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Payment가 UNKNOWN이면 고민이 생긴다. 나는 이 정책을 선택했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;5분 안에 PG 조회로 &lt;code&gt;APPROVED&lt;/code&gt;가 확인되면 &lt;b&gt;주문 성공&lt;/b&gt; 처리한다.&lt;/li&gt;
&lt;li&gt;5분이 지났고 승인 내역이 확인되지 않으면 주문은 &lt;code&gt;EXPIRED&lt;/code&gt; 처리하고 &lt;b&gt;재고를 해제&lt;/b&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;style3&quot;&gt;&lt;b&gt;&quot;5분은 결제 완료 시간이 아니라 구매 권리의 유효 시간&quot;이다. 5분이 지난 뒤 결제가 확인되었다고 해서 이미 해제된 재고를 다시 뺏어오면 안 된다. 그 재고는 다음 대기자에게 넘어갔을 수 있다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;1128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byjQtJ/dJMcabxciC9/dkT5QyK6pzOzxvQxL6TmKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byjQtJ/dJMcabxciC9/dkT5QyK6pzOzxvQxL6TmKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byjQtJ/dJMcabxciC9/dkT5QyK6pzOzxvQxL6TmKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyjQtJ%2FdJMcabxciC9%2FdkT5QyK6pzOzxvQxL6TmKk%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;1204&quot; height=&quot;1128&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;1128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정책은 구매 전환율만 보면 아쉬울 수 있다. 하지만 이 도메인에서는 초과 판매 방지가 더 중요하다. 1,000족 한정 판매에서 이미 해제된 재고를 다시 성공 처리하면 다른 사용자의 선착순 기회를 침해할 수 있다.&lt;/p&gt;
&lt;div class=&quot;summary-box&quot;&gt;&lt;b&gt;보수적 정책: &lt;/b&gt;기한 내 확정되지 않은 결제는 구매 성공으로 인정하지 않는다. 늦게 결제가 확인되면 환불로 보상한다.&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;10.&lt;/span&gt; 상태 모델&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Orchestration Saga를 쓰려면 상태가 명확해야 한다. 상태가 흐릿하면 장애 복구 시 &quot;이 주문을 다시 진행해야 하는지, 취소해야 하는지, 환불해야 하는지&quot; 판단할 수 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Order 상태&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;CREATED
&amp;rarr; WAITING_PAYMENT
&amp;rarr; PAYMENT_IN_PROGRESS
&amp;rarr; PAID
&amp;rarr; COMPLETED

WAITING_PAYMENT &amp;rarr; EXPIRED
PAYMENT_IN_PROGRESS &amp;rarr; PAYMENT_FAILED &amp;rarr; CANCELLED
PAYMENT_IN_PROGRESS &amp;rarr; PAYMENT_UNKNOWN &amp;rarr; PAID or EXPIRED
EXPIRED + LatePaymentApproved &amp;rarr; REFUND_REQUESTED&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Payment 상태&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;READY
&amp;rarr; APPROVAL_REQUESTED
&amp;rarr; APPROVED

APPROVAL_REQUESTED &amp;rarr; FAILED
APPROVAL_REQUESTED &amp;rarr; UNKNOWN
UNKNOWN &amp;rarr; APPROVED / FAILED / REFUND_REQUIRED

APPROVED &amp;rarr; CANCEL_REQUESTED &amp;rarr; CANCELLED or REFUNDED&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Inventory Reservation 상태&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;RESERVED &amp;rarr; SOLD
RESERVED &amp;rarr; RELEASED
RESERVED &amp;rarr; EXPIRED&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태를 이렇게 쪼개는 이유는 운영 복구 때문이다. 장애가 난 뒤 재처리 Job이 돌 때도 &lt;b&gt;상태만 보고 다음 행동을 결정할 수 있어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;11.&lt;/span&gt; 보상 트랜잭션 정리&lt;/h2&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;처리&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;재고 예약 실패&lt;/td&gt;
&lt;td&gt;주문 취소, 결제 요청 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;결제 명시적 실패&lt;/td&gt;
&lt;td&gt;재고 예약 해제, 주문 취소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;결제 Timeout&lt;/td&gt;
&lt;td&gt;Payment UNKNOWN, PG 상태 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout 후 5분 내 승인 확인&lt;/td&gt;
&lt;td&gt;재고 확정, 주문 성공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout 후 5분 내 승인 미확인&lt;/td&gt;
&lt;td&gt;재고 해제, 주문 만료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주문 만료 후 늦은 승인 확인&lt;/td&gt;
&lt;td&gt;결제 취소/환불&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;결제 승인 후 재고 확정 실패&lt;/td&gt;
&lt;td&gt;재고 예약이 살아 있으면 재시도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재고 예약 만료 후 결제 승인 확인&lt;/td&gt;
&lt;td&gt;결제 취소/환불&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;warn-box&quot;&gt;&lt;b&gt;중요:&lt;/b&gt; 모든 실패를 똑같이 취소하지 않는다.&lt;br /&gt;결제 승인 후 재고 확정 실패는 바로 환불하면 안 된다. 재고 예약이 아직 살아 있으면 재시도해서 성공으로 수렴시키는 게 맞다.&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;12.&lt;/span&gt; 멱등성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 같은 메시지를 두 번 전달할 수 있다. PG Webhook도 중복으로 올 수 있다. 사용자의 결제 요청도 중복으로 들어올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 설계에서는 &lt;b&gt;&quot;같은 요청이 여러 번 들어온다&quot;를 기본값으로 둔다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;-- 중복 방지
orders:
  UNIQUE(drop_id, user_id)

inventory_reservations:
  UNIQUE(order_id)

payment_attempts:
  UNIQUE(order_id, attempt_no)
  UNIQUE(idempotency_key)

inbox_events:
  UNIQUE(event_id)

outbox_events:
  PRIMARY KEY(event_id)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PG 승인 요청에도 idempotency key를 붙인다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;idempotencyKey = payment:{orderId}:{attemptNo}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 전이도 조건부로 처리한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE inventory_reservation
SET status = 'SOLD'
WHERE order_id = :orderId
  AND status = 'RESERVED';   &amp;larr; 이미 SOLD면 UPDATE 안 됨&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;이미 SOLD인 상태에서 같은 요청이 한 번 더 와도 결과는 달라지지 않아야 한다. 이것이 Saga를 운영 가능한 구조로 만드는 핵심이다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;13.&lt;/span&gt; 사용자에게는 어떻게 보여줄까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 주문/결제 API는 평균 &lt;b&gt;200ms 이하&lt;/b&gt;를 목표로 한다. 따라서 결제 승인과 재고 확정까지 한 API에서 모두 기다리면 안 된다. API 응답은 접수와 완료를 분리한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주문서 진입&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;POST /orders
&amp;rarr; 200 OK
{
  &quot;orderId&quot;: &quot;ord_123&quot;,
  &quot;status&quot;: &quot;WAITING_PAYMENT&quot;,
  &quot;expiresAt&quot;: &quot;2026-04-26T12:05:00+09:00&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결제 승인 요청&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;POST /orders/{orderId}/payments/confirm
&amp;rarr; 202 Accepted
{
  &quot;orderId&quot;: &quot;ord_123&quot;,
  &quot;status&quot;: &quot;PAYMENT_IN_PROGRESS&quot;,
  &quot;message&quot;: &quot;결제 승인 처리 중입니다.&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상태 조회 (Polling / SSE)&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;GET /orders/{orderId}
&amp;rarr; 200 OK
{
  &quot;orderId&quot;: &quot;ord_123&quot;,
  &quot;status&quot;: &quot;PAID&quot;,
  &quot;paymentStatus&quot;: &quot;APPROVED&quot;,
  &quot;inventoryStatus&quot;: &quot;SOLD&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 중간에 &lt;code&gt;PAYMENT_IN_PROGRESS&lt;/code&gt; 또는 &lt;code&gt;PAYMENT_UNKNOWN&lt;/code&gt;을 볼 수 있다. 다만 &lt;b&gt;10분 안에는 성공 또는 실패 계열의 최종 상태&lt;/b&gt;로 안내한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 PG 확인이 계속 지연된다면, 구매는 실패로 확정하고 이후 결제 승인 사실이 확인될 경우 자동 환불한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;section-num&quot;&gt;14.&lt;/span&gt; 최종적으로 내가 선택한 설계&lt;/h2&gt;
&lt;div class=&quot;summary-box&quot;&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;주문 생성 시 Inventory Service가 재고를 5분간 &lt;code&gt;RESERVED&lt;/code&gt; 처리한다.&lt;/li&gt;
&lt;li&gt;결제 승인 요청 API는 PG 응답을 기다리지 않고 &lt;b&gt;202&lt;/b&gt;로 반환한다.&lt;/li&gt;
&lt;li&gt;PG 호출은 Payment Service가 비동기로 수행한다.&lt;/li&gt;
&lt;li&gt;PG Timeout은 &lt;code&gt;FAILED&lt;/code&gt;가 아니라 &lt;code&gt;UNKNOWN&lt;/code&gt;으로 저장한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UNKNOWN&lt;/code&gt;은 PG 상태 조회와 Webhook으로 확정한다.&lt;/li&gt;
&lt;li&gt;5분 안에 승인 확인 시 주문 성공, 확인 실패 시 주문 만료와 재고 해제.&lt;/li&gt;
&lt;li&gt;만료 이후 늦은 승인 확인은 주문 성공이 아니라 &lt;b&gt;환불&lt;/b&gt;로 보상한다.&lt;/li&gt;
&lt;li&gt;모든 이벤트와 명령은 Outbox, Inbox, Idempotency Key로 중복과 유실을 방어한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&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;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;외부 PG 장애가 내부 트랜잭션을 붙잡지 않는다.&lt;/li&gt;
&lt;li&gt;결제 Timeout을 섣불리 실패로 처리하지 않는다.&lt;/li&gt;
&lt;li&gt;주문은 최종적으로 성공 또는 실패로 수렴한다.&lt;/li&gt;
&lt;li&gt;사용자는 제한 시간 안에 결과를 받는다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>rootTiket</author>
      <guid isPermaLink="true">https://root-2707.tistory.com/10</guid>
      <comments>https://root-2707.tistory.com/10#entry10comment</comments>
      <pubDate>Sun, 26 Apr 2026 11:19:16 +0900</pubDate>
    </item>
    <item>
      <title>Claude Code를 모니터링 해보자!: claude-analytics 개발기</title>
      <link>https://root-2707.tistory.com/9</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3&quot;&gt;들어가며&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;지난 글에서는 초기 창업 팀이 파편화된 AI 사용에서 벗어나기 위해 manifest.md와 YAML 기반의 명세서를 도입하고, 팀의 컨벤션에 맞게 Claude Code를 통제하는 '컨텍스트 엔지니어링' 과정을 공유했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 나름의 규칙을 세우고 매번 수정하며 '컨텍스트 엔지니어링 회고'를 진행하고 있습니다. 하지만 회고를 진행할 때마다 /resume 으로 대화 흐름을 보고 피드백하기를 반복하는 과정이 불편했고, 생산성을 위해 오히려 더 번거로운 일을 하는 것은 아닌가? 하는 의문이 들었습니다. 이 글은 claude code의 대화 내용을 웹으로 직접 보기위한 대시보드를 만들기까지의 회고 입니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6&quot;&gt;1. 터미널 창을 벗어나고 싶다는 단순한 귀찮음&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;Claude Code는 기본적으로 CLI 기반으로 동작합니다. 그러다 보니 지난주 대화 기록을 보며 리뷰를 하려면 까만 터미널 창에서 마우스 휠을 한참 올리며 맥락을 파악해야 했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;게다가 대화 효율을 측정할 수 있는 유일한 단서인 토큰 사용량은 그저 &quot;제한량의 몇 %를 사용했다&quot; 정도로만 표기되었습니다. 매 세션마다 이 퍼센티지를 따로 기록하고 추적하는 건 Agent 도입으로 얻은 생산성을 되려 깎아먹는다고 생각했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&quot;터미널에 갇힌 대화 기록과 애매한 토큰 사용량을 웹에서 좀 편하게 볼 수 없을까?&quot; 하는 개발자 같은 귀찮음과 불편함이 이 프로젝트의 시작이었습니다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10&quot;&gt;2. 백엔드 개발자의 Antigravity 찍어 먹어보기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;저는 주로 Java와 Spring Boot 생태계에서 백엔드 개발을 해왔습니다. 그렇다 보니 Node.js 생태계나 IDE 환경에서 AI 에이전트가 코드를 직접 작성해 주는 워크플로우를 써볼 기회가 거의 없었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;프론트엔드 개발하시는 분들이 Cursor로 편하게 작업하시는 걸 항상 부럽게만 생각했는데, 이렇게 아이디어가 나온 김에 저도 Antigravity를 써보고자 했습니다. 학생 신분이기 때문에 추가 과금이 필요 없었고, Opus도 제약적이게나마 사용할 수 있다는 점이 매력적이었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;개발 자체는 수월했습니다. 대화 로그 포맷을 알려주고 룰을 잡아주니 에이전트가 훌륭하게 파싱 코드를 짜주었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3644&quot; data-origin-height=&quot;2368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doaVLs/dJMcachYNho/jeMkVsNOaMmf2aFyNQioz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doaVLs/dJMcachYNho/jeMkVsNOaMmf2aFyNQioz1/img.png&quot; data-alt=&quot;처음 개발했을때 모습입니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doaVLs/dJMcachYNho/jeMkVsNOaMmf2aFyNQioz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoaVLs%2FdJMcachYNho%2FjeMkVsNOaMmf2aFyNQioz1%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;682&quot; height=&quot;443&quot; data-origin-width=&quot;3644&quot; data-origin-height=&quot;2368&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;처음 개발했을때 모습입니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;생각보다 너무 빠르게 만들어줘서 놀랐습니다. 2시간도 걸리지 않고 만들었다는게 참 놀라웠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; 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;KakaoTalk_Snapshot_20260307_183733.png&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;1314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csnBSP/dJMcahXSK0V/kt7e0GmlTKD6yEX4toEzI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csnBSP/dJMcahXSK0V/kt7e0GmlTKD6yEX4toEzI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csnBSP/dJMcahXSK0V/kt7e0GmlTKD6yEX4toEzI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsnBSP%2FdJMcahXSK0V%2Fkt7e0GmlTKD6yEX4toEzI1%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;330&quot; height=&quot;597&quot; data-filename=&quot;KakaoTalk_Snapshot_20260307_183733.png&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;1314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;그런데 오히려 의외로 문제가 됐던 것이&amp;nbsp; '파일 경로'였습니다. 프로토타입을 만들자마자 지인에게 배포를 했었는데, 다른 PC에서는 .claude 폴더를 찾지 못해서 실행이 안 되는 이슈가 있었습니다. AI가 아무리 로직을 잘 짜준다고 한들, 결국 실행 환경을 제어하고 보장하는 것은 온전히 인간 개발자의 몫이라는 걸 다시금 깨달았던 순간이었습니다. (이 경로 문제는 어찌저찌 잘 해결했습니다.)&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14&quot;&gt;3. 컨텍스트가 짧으면 효율적인 걸까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;완성된 대시보드를 처음 로컬에 띄웠을 때, 대시보드에서 가장 먼저 보려고 했던 지표는 '컨텍스트의 크기'였습니다. 우리가 컨텍스트 엔지니어링을 잘했다면 불필요한 토큰 낭비가 줄었을 테니, 전체 컨텍스트 길이가 짧게 나올 것이라는 단순한 가설이었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;하지만 데이터를 시각화해서 보다 보니 이 지표에 치명적인 허점이 있었습니다. Claude가 생성해 내는 결과물(Output) 역시 컨텍스트 길이에 포함된다는 사실이었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;가령 AI가 요구사항을 한 번에 완벽하게 이해하고 수백 줄의 훌륭한 코드를 반환했다면 전체 컨텍스트 길이는 길어집니다. 길이는 길지만 효율은 좋은 상태죠. 반대로 대화는 짧게 끝났지만 엉뚱한 코드만 뱉어냈다면 수치상으로는 '효율적'인 것으로 오해할 수 있습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;결국 단순히 '숫자가 적다 = 효율적이다'라는 1차원적인 지표로는 이 복잡한 대화를 평가할 수 없다는 것을 깨달았습니다. 애매한 지표를 넣고 나니, 오히려 &quot;우리가 진짜 집중해야 할 지표가 무엇인가?&quot;를 다시 생각하게 된 경험이었습니다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;20&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20&quot;&gt;4. 점수표 대신 워크플로우 시각화하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 툴을 만든 원래의 목적은 충분히 달성하고 있었습니다. 바로 '시각화'입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;고심해서 만든 CLAUDE.md 문서에 적힌 대로 잘 로드되고 있는지, /feature나 /fix 같은 Skill들이 의도한 타이밍에 정확하게 워크플로우를 타고 작동하고 있는지. 이전에는 까만 터미널 창에서 ctrl + o를 눌러가며 일일이 확인해야 했던 것들을 이제는 대시보드를 통해 눈으로 직접 확인할 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 무조건적인 효율을 숫자로 쾅 찍어주는 절대적인 평가 도구가 아닙니다. 그보다는 우리가 주입한 컨텍스트들이 대화 속에서 어떻게 흘러가는지 궤적을 보여주는 시각화 도구에 가깝습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;최근에는 Anthropic에서 공개한 Claude Skill Building Guide 를 보며 턴 수를 줄이거나, 자율적으로 실행하는 시간의 비율을 측정하는 방식으로 지표를 업데이트 해 나가고 있습니다. 하지만 정량적인 측정 방식이 상대적으로 효율을 대변하기에는 어렵다고 생각하여 의도한대로 동작하는지 판단하는 모니터링 용도에 초점을 두고 개발해 나가고자 합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;25&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;25&quot;&gt;마치며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;사실 이 프로젝트를 깃허브에 퍼블릭으로 공개하고 이 글을 쓰는 지금도 마음 한편에는 불안함과 머쓱함이 있습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;저는 AI 프롬프트 전문가도 아니고, 데이터를 전문적으로 공부해보지도 않은 그냥 대학생이기 때문입니다. 개발을 하다가 &quot;이거 너무 불편한데?&quot;라는 생각에 AI의 힘을 빌려 뚝딱 만들어본 툴입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 툴을 공개해보는 이유는, 저희 팀과 똑같은 고민을 하고 있을 누군가에게 작은 도움이 되길 바라기 때문입니다. AI 코딩 어시스턴트를 실무에 도입하며 &quot;우리 팀, 지금 잘 쓰고 있는 걸까?&quot; 막막해하던 분들이 계신다면, 이 툴을 통해 여러분의 대화 로그를 한 번쯤 시각화해 보시길 추천합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;긴 글 읽어주셔서 감사합니다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3644&quot; data-origin-height=&quot;2324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbj7zA/dJMcabwAivQ/sJRprdK6zgK9nlvgFyJop1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbj7zA/dJMcabwAivQ/sJRprdK6zgK9nlvgFyJop1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbj7zA/dJMcabwAivQ/sJRprdK6zgK9nlvgFyJop1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcbj7zA%2FdJMcabwAivQ%2FsJRprdK6zgK9nlvgFyJop1%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;623&quot; height=&quot;397&quot; data-origin-width=&quot;3644&quot; data-origin-height=&quot;2324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;  &lt;b data-index-in-node=&quot;3&quot; data-path-to-node=&quot;29&quot;&gt;&lt;a href=&quot;https://github.com/rootTiket/claude-analytics&quot; data-ved=&quot;0CAAQ_4QMahgKEwj3_9iOn42TAxUAAAAAHQAAAAAQ3gk&quot; data-hveid=&quot;0&quot;&gt;GitHub - rootTiket/claude-analytics&lt;/a&gt;&lt;/b&gt; (직접 써보시고 많은 피드백을&amp;nbsp; 남겨주시면 큰 힘이 될 것 같습니다!)&lt;/p&gt;</description>
      <author>rootTiket</author>
      <guid isPermaLink="true">https://root-2707.tistory.com/9</guid>
      <comments>https://root-2707.tistory.com/9#entry9comment</comments>
      <pubDate>Sat, 7 Mar 2026 19:11:50 +0900</pubDate>
    </item>
    <item>
      <title>초기 창업 팀에서 Claude Code 를 도입한 방법</title>
      <link>https://root-2707.tistory.com/8</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 들어가며: 바이브 코딩의 유행과 현실적인 고민&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 개발 생태계를 뒤흔든 가장 뜨거운 화두는 단연 바이브 코딩이었을 것입니다. 초기 창업팀에서 프로젝트를 시작하는 개발자이자 본격적인 커리어를 준비하는 취업준비생 입장에서, 코드를 뚝딱 만들어내는 AI의 발전 속도는 경이로움을 넘어 일견 채용에 대한 두려움으로 다가오기도 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 막연한 불안감에 빠져있기보다는 시각을 조금 바꿔보기로 했습니다. &lt;b&gt;&quot;어떻게 하면 이 강력한 AI를 활용해 우리 팀의 개발 생산성을 극대화할 수 있을까?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에 AI를 도입하며 마주한 현실은 단순히 코드를 복사하는 것만큼 간단하지 않았습니다. 팀원 각자가 파편화된 방식으로 AI를 사용하다 보니, 오히려 팀의 코드 컨벤션을 맞추고 프로젝트 전체의 통일성을 유지하는 데 더 많은 리뷰와 수정 비용이 들 것 같다는 우려가 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신뢰할 수 있는 서비스를 만들기 위해서, AI의 결과물을 그대로 수용하기보다는 AI가 &lt;b&gt;우리 조직의 언어와 프레임워크 구조 안에서 '제대로' 동작하도록 규칙을 잡아주는 과정이 필요하다는&lt;/b&gt; 결론을 내렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 단순한 툴 사용을 넘어 조직 차원에서의 체계적인 &lt;b&gt;AI Co-Work&lt;/b&gt; 환경이 필요했습니다. 이 글에서는 우리 팀이 &lt;b&gt;Claude Code&lt;/b&gt;를 활용해 어떻게 팀 컨벤션에 맞춘 컨텍스트 엔지니어링을 수행했고, 파편화된 AI 사용의 한계를 극복하며 개발 생산성을 어떻게 끌어올리고 있는지 그 과정을 공유하고자 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 컨텍스트 엔지니어링의 목표&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;팀이 함께 사용할 수 있도록 해야한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 학생이거나, AI툴을 도입하기 시작한 초기 조직은 조직내 개인이 룰을 정하고 코드를 작성합니다. 이렇게 되면 같은 프로덕트를 만들더라도, 내리는 명령의 종류가 제각각이게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;b&gt;Before:&lt;/b&gt; 개별 팀원이 각자의 스타일로 프롬프팅 -&amp;gt; 결과물의 일관성 부족&lt;/li&gt;
&lt;li&gt;&lt;b&gt;After:&lt;/b&gt; 팀이 합의한 아키텍처, 네이밍 규칙, 기술 스택을 담은 '공통 컨텍스트' 주입 -&amp;gt; &lt;b&gt;누가 요청해도 동일한 규격의 코드 생성&lt;/b&gt;&lt;/li&gt;
&lt;/ul&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;흔히 LLM을 사용할 때 채팅하듯 여러 번 대화를 주고받으며 수정하는 방식을 사용합니다. 하지만 이 방식은 맥락을 잃고 효율적이지 못합니다.&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;맥락의 휘발:&lt;/b&gt; 대화가 길어질수록 AI는 초기 요구사항이나 전체 구조를 놓칠 가능성이 큽니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비용 문제:&lt;/b&gt; 수정 요청을 반복할수록 누적된 토큰 양이 늘어나 비용과 시간이 낭비됩니다.&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;3. AI를 팀원으로 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글에 들어가기 전 해당 글은 Toss의 &lt;a href=&quot;https://toss.tech/article/44539&quot;&gt;소프트웨어 3.0 시대를 맞이하며&lt;/a&gt; 라는 글이 큰 도움이 됐습니다. 초기에 구성하신다면 읽어보시길 추천드립니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아키텍처 설계: Toss의 레이어드 구조 적용&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;3&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,0,0&quot;&gt;Slash Command&lt;/b&gt;&lt;span&gt; = Controller 역할&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,1,0&quot;&gt;Sub-agent&lt;/b&gt;&lt;span&gt; = Service Layer (여러 기능 조합)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,2,0&quot;&gt;Skills&lt;/b&gt;&lt;span&gt; = 단일 책임 원칙을 따르는 컴포넌트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,3,0&quot;&gt;MCP&lt;/b&gt;&lt;span&gt; = 외부 시스템과의 Adapter&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,4,0&quot;&gt;CLAUDE.md&lt;/b&gt;&lt;span&gt; = 프로젝트 설정 파일 (package.&lt;/span&gt;&lt;span&gt;json 역할)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 개념을 프로젝트에 다음과 같이 적용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size20&quot;&gt;1. Boot Sequence: manifest.md가 부트로더 역할&lt;/h4&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;AI는 매 대화마다 어디서부터 읽어야 할지 모릅니다. 따라서 우리는 manifest.md를 부트로더로 설계했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1771143631943&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# manifest.md
boot_sequence:
  - order: 1
    file: .claude/core/essential-rules.yaml
    purpose: Load constraints &amp;amp; stack
  - order: 2
    file: .claude/memory.md
    purpose: Load context
  - order: 3
    file: .claude/CLAUDE.md
    purpose: Load index&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;AI는 실행 시 항상 manifest.md를 먼저 읽고, 정해진 순서대로 문서를 로드합니다. 이는 마치 애플리케이션의 main() 함수처럼 작동합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size20&quot;&gt;2. Lazy Loading: 필요한 것만 로드시키기&lt;/h4&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;CLAUDE.md는 변하지 않는 정보만 담아야 합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;이를 적용하여 문서를 세 단계로 분류했습니다:&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12&quot;&gt;Index&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,0,0&quot;&gt;1. Core (Must Read)&lt;/b&gt;: 반드시 읽어야 하는 변하지 않는 규칙
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;core/essential-rules.yaml: Non-negotiable Rules &amp;amp; Tech Stack&lt;/li&gt;
&lt;li&gt;memory.md: Current Context &amp;amp; Status&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,1,0&quot;&gt;2. Design (Read on Demand)&lt;/b&gt;: 설계/구현 시에만 필요한 문서
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;core/system-design.yaml: Architecture, API, Entity Rules&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,2,0&quot;&gt;3. References (Lazy Load)&lt;/b&gt;: 필요할 때 사용하는 상세 가이드
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;references/conventions/coding-style.md: 코딩 스타일 가이드&lt;/li&gt;
&lt;li&gt;references/data-model/domains/: 도메인 엔티티 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 불필요한 토큰 소비를 줄이고, AI가 핵심 컨텍스트에 집중할 수 있도록 했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;15&quot; data-ke-size=&quot;size20&quot;&gt;3. Skills: 단일 책임 원칙&lt;/h4&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;반복적인 작업을 Skills로 정의했습니다:&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17&quot;&gt;Skills&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;18&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/feature: 새로운 기능 구현&lt;/li&gt;
&lt;li&gt;/fix: 버그 수정&lt;/li&gt;
&lt;li&gt;/refactor: 리펙토링&lt;/li&gt;
&lt;li&gt;/docs: 문서화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;각 Skill은 명확한 책임을 가지며, 사용자는 /feature라고 입력하는 것만으로 정해진 워크플로우를 실행할 수 있습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;26&quot; data-ke-size=&quot;size23&quot;&gt;Ask Protocol 정의하기&lt;/h3&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;AI 에이전트는 구현, 혹은 수정 중 불확실한 상황에서 질문할 수 있습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;이를 적용하여 &quot;Ask Protocol&quot;을 정의했습니다:&lt;/p&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;29&quot;&gt;Agent Behavior (Protocol)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;30&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;30,0,0&quot;&gt;Ask Protocol&lt;/b&gt;: If requirements are ambiguous, risky (e.g., deletion), or deviate from conventions, &lt;b data-index-in-node=&quot;98&quot; data-path-to-node=&quot;30,0,0&quot;&gt;STOP and ASK&lt;/b&gt; the user immediately.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;AI는 다음 상황에서 반드시 사용자에게 질문해야 합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;32&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;p data-path-to-node=&quot;33&quot; data-ke-size=&quot;size16&quot;&gt;이는 모든 분기를 미리 정의할 필요 없이, 중요한 순간에 사용자 판단을 요청하는 방식으로 작동합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;33&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;35&quot; data-ke-size=&quot;size23&quot;&gt;Feature Spec: Docs First 워크플로우&lt;/h3&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;Toss 블로그에서는 &quot;변하지 않는 정보만 문서화하라&quot;고 했지만, 우리는 한 가지를 추가했습니다: Feature Spec입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;각 기능은 구현 전에 YAML 형식의 명세서를 먼저 작성합니다&lt;/p&gt;
&lt;p data-path-to-node=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;예를들어:&lt;/p&gt;
&lt;pre id=&quot;code_1771144185174&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# create_remote_waiting.yaml
meta:
  id: FEAT-WAITING-002
  title: 원격 웨이팅 등록 (Remote Waiting Registration)
  type: Feature

overview:
  summary: &amp;gt;
    앱 사용자가(Member)가 직접 특정 부스에 원격으로 웨이팅을 신청하는 기능입니다.
  user_story: &amp;gt;
    As a Member, I want to register for waiting remotely via the app,
    so that I can enjoy the festival without physically standing in line.

technical_spec:
  api:
    - method: POST
      path: /api/v1/booths/{boothId}/waitings
      auth: MEMBER

  validation_rules:
    - &quot;요청자는 로그인한 회원(MEMBER)이어야 한다.&quot;
    - &quot;부스가 존재하며, 운영 상태가 'OPEN'이어야 한다.&quot;
    - &quot;동일한 부스에 이미 'WAITING' 상태인 내역이 존재하면 안 된다.&quot;

  testing_strategy:
    happy_path:
      - &quot;로그인한 멤버가 원격 줄서기가 허용된 OPEN 부스에 웨이팅 등록 성공&quot;
    edge_cases:
      - &quot;부스가 CLOSED 상태일 때 예외 발생 (BOOTH_NOT_OPEN)&quot;
      - &quot;이미 웨이팅 중인 회원이 중복 등록 시도 시 예외 발생&quot;&lt;/code&gt;&lt;/pre&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문서 포맷은 yml로&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI는 구조화 된 포맷일 수록 맥락을 이해하기 쉽고, 토큰 효율적으로 구성할 수 있습니다. 흔히 AI 친화적인 포맷으로 md, xml 등을 꼽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 claude는 xml 형식을 잘 이해한다는 글을 읽고, xml 형식으로 spec 문서를 작성했습니다. 하지만 xml 포맷은 치명적인 단점이 있었는데요.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. XML은 인간에게 친화적이지 못하다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서는 기본적으로 인간이 작성할 수 있어야 한다고 생각했습니다. code first가 아닌 docs first로 요구사항을 문서에 먼저 반영하고 이를 구현하게 하는 것이 기본 흐름이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서는 편집하기 쉽고, 빠르게 변경해야 하기에 인간에게 친화적이지 못한 XML 포맷은 적절하지 못하다고 생각하여 다른 포맷으로 변경하기로 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;1046&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/booGHK/dJMcahJ9oEg/g8jHsmQkDGd3MKrAREVIO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/booGHK/dJMcahJ9oEg/g8jHsmQkDGd3MKrAREVIO1/img.png&quot; data-alt=&quot;초기 XML로 작성한 에러처리 spec&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/booGHK/dJMcahJ9oEg/g8jHsmQkDGd3MKrAREVIO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbooGHK%2FdJMcahJ9oEg%2Fg8jHsmQkDGd3MKrAREVIO1%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;558&quot; height=&quot;1046&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;1046&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;초기 XML로 작성한 에러처리 spec&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 마크다운은 자칫 컨텍스트를 나열할 수 있다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 AI가 선호하는 포맷은 마크다운이 있었습니다. 실제로 많은 프로젝트에서 마크다운을 사용하고 있는것으로 알고있었습니다. 또한 수정하기 용이하기 때문에 사용을 고려했으나, 모든 상황에서 사용하기에는 어려움이 있다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 불필요한 컨텍스트가 포함될 수 있다.&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;p data-ke-size=&quot;size16&quot;&gt;2. 위계를 한눈에 보기에 다소 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마크다운은 보통 # 을 이용해서 글의 맥락을 나타냅니다. 하지만 이 역시 3계층만 표현할 수 있다는 점에서 위계 표현에 제한이 있다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 위계를 한눈에 보기에 다소 불편하다고 생각했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 그래서 최종 선택은 YAML(yml)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 XML과 마크다운의 단점을 보완하면서도, AI와 팀원 모두에게 가장 효율적인 포맷을 고민한 끝 YAML을 컨텍스트 문서의 표준으로 도입하기로 결정했습니다. 그 이유는 다음과 같습니다.&lt;/p&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;YAML은 Key-Value 형태를 기반으로 합니다. 이는 문서를 작성하는 팀원으로 하여금 자연스럽게 '줄글 형태의 서술'이 아닌, 핵심 키워드와 '조건(명세)' 위주로 작성하도록 유도합니다. 불필요한 조사나 연결어가 배제되므로 토큰 사용량을 획기적으로 줄일 수 있고, AI 역시 애매한 문장 대신 명확한 속성값으로 맥락을 파악하기 때문에 할루시네이션이 크게 줄어듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 한눈에 들어오는 직관적인 위계(Hierarchy) 표현&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마크다운이 # 기호를 통해 다소 평면적인 계층 구조를 가졌다면, YAML은 들여쓰기(Indentation)만으로 복잡한 비즈니스 로직이나 엣지 케이스의 Depth를 무한하고 시각적으로 깔끔하게 표현할 수 있습니다. 덕분에 문서의 전체적인 뼈대와 세부 조건을 한눈에 파악하기 쉬워, 'Docs First'를 실천하며 문서를 리뷰하고 수정하는 과정이 훨씬 수월해졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;As-Is&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;1210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2el08/dJMcacovQWe/LecXBUJZMak471Ki1qAR6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2el08/dJMcacovQWe/LecXBUJZMak471Ki1qAR6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2el08/dJMcacovQWe/LecXBUJZMak471Ki1qAR6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2el08%2FdJMcacovQWe%2FLecXBUJZMak471Ki1qAR6K%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;517&quot; height=&quot;1210&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;1210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;To-Be&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;1198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPMcO1/dJMcaca0ouN/hnlUg3Z6TosgElVGTvPnF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPMcO1/dJMcaca0ouN/hnlUg3Z6TosgElVGTvPnF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPMcO1/dJMcaca0ouN/hnlUg3Z6TosgElVGTvPnF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPMcO1%2FdJMcaca0ouN%2FhnlUg3Z6TosgElVGTvPnF1%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;529&quot; height=&quot;1198&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;1198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 성과&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;성과: 모델 1회 요청당 토큰 사용량을 최대 25% 절감 (제한량 기준 20% &amp;rarr; 15% 수준으로 최적화)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 구현 단계에서 적용했을 때, 기존에 컨벤션이 맞지 않아 자주 변경해야 했던 것에 비해 확실히 컨벤션에 맞게 잘 작성해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처 설계 단계에서 의도했던 manifest 를 통한 라우팅도 적절히 이루어지고 있어요&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1692&quot; data-origin-height=&quot;1494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHa7gA/dJMcad1YM2i/kKVVes8ckgt881P7KXj2N1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHa7gA/dJMcad1YM2i/kKVVes8ckgt881P7KXj2N1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHa7gA/dJMcad1YM2i/kKVVes8ckgt881P7KXj2N1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHa7gA%2FdJMcad1YM2i%2FkKVVes8ckgt881P7KXj2N1%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;621&quot; height=&quot;548&quot; data-origin-width=&quot;1692&quot; data-origin-height=&quot;1494&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;엄밀하지 않지만 단순한 API 구현 기준으로 1회당, 20%이상 사용된 토큰 사용량이 10% 대에서 안정적으로 구현이 가능해졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 한 세션에서 한번의 slach command(Skill)을 사용하는것으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이전보다 수정 요청 빈도 감소 (평균 5회 -&amp;gt; 3회미만)&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;5. 회고&lt;/h2&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;현재 구현에서 불만족 스러운 부분은 있는가, spec 문서 중 중복되거나, 혹은 명확하지 않은 부분이 있는지 등을 회고하고 있어요&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;1762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JdPck/dJMcabiRPPv/9bZrUw7mlz3SQ5Z6sHCnz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JdPck/dJMcabiRPPv/9bZrUw7mlz3SQ5Z6sHCnz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JdPck/dJMcabiRPPv/9bZrUw7mlz3SQ5Z6sHCnz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJdPck%2FdJMcabiRPPv%2F9bZrUw7mlz3SQ5Z6sHCnz1%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;513&quot; height=&quot;1762&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;1762&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;1044&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JhWCg/dJMcaflfaWQ/u9b7Vy7Eu1moxnxRX7gUsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JhWCg/dJMcaflfaWQ/u9b7Vy7Eu1moxnxRX7gUsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JhWCg/dJMcaflfaWQ/u9b7Vy7Eu1moxnxRX7gUsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJhWCg%2FdJMcaflfaWQ%2Fu9b7Vy7Eu1moxnxRX7gUsK%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;545&quot; height=&quot;412&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;1044&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;부족한 점이 아직 많다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 문서를 기반으로 한 엔지니어링에 초점이 되어있으나, hook, mcp같은 고차원 적인 기능을 바로 도입하고 있지는 않습니다.&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;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://github.com/rootTiket/claude-analytics&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/rootTiket/claude-analytics&lt;/a&gt; 에서 완성된 프로덕트가 있으니 관심있으시다면 둘러봐주세요 :)&lt;/p&gt;
&lt;figure id=&quot;og_1771146871073&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - rootTiket/claude-analytics&quot; data-og-description=&quot;Contribute to rootTiket/claude-analytics development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/rootTiket/claude-analytics&quot; data-og-url=&quot;https://github.com/rootTiket/claude-analytics&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bAmFMx/dJMb85WPyZl/1qSYvv9UhJxoTjfaXr41Ak/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/ct1Cdx/dJMb88F1aqd/aWtoDsouzprbFeBcNfaAPk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/rootTiket/claude-analytics&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/rootTiket/claude-analytics&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bAmFMx/dJMb85WPyZl/1qSYvv9UhJxoTjfaXr41Ak/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/ct1Cdx/dJMb88F1aqd/aWtoDsouzprbFeBcNfaAPk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - rootTiket/claude-analytics&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to rootTiket/claude-analytics development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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-origin-width=&quot;3644&quot; data-origin-height=&quot;2370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzHUws/dJMcagqUGaY/hgt9L0h0X0rtq8LATMJfo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzHUws/dJMcagqUGaY/hgt9L0h0X0rtq8LATMJfo0/img.png&quot; data-alt=&quot;spec문서 사이의 관계를 시각화 해주는 웹&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzHUws/dJMcagqUGaY/hgt9L0h0X0rtq8LATMJfo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzHUws%2FdJMcagqUGaY%2Fhgt9L0h0X0rtq8LATMJfo0%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;582&quot; height=&quot;379&quot; data-origin-width=&quot;3644&quot; data-origin-height=&quot;2370&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;spec문서 사이의 관계를 시각화 해주는 웹&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3564&quot; data-origin-height=&quot;2290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AUHAd/dJMcahwB6ul/ctLovJbLBOV3ownDe2Lln0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AUHAd/dJMcahwB6ul/ctLovJbLBOV3ownDe2Lln0/img.png&quot; data-alt=&quot;세션 별 내용을 볼 수 있는 웹&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AUHAd/dJMcahwB6ul/ctLovJbLBOV3ownDe2Lln0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAUHAd%2FdJMcahwB6ul%2FctLovJbLBOV3ownDe2Lln0%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;638&quot; height=&quot;410&quot; data-origin-width=&quot;3564&quot; data-origin-height=&quot;2290&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;세션 별 내용을 볼 수 있는 웹&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;마치며&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금처럼 AI 와 human in loop으로 작업하기 시작한지 3주가 지나가고 있습니다.&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;1. 명확한 문서 작성을 통한 기획 - 개발 간 도메인 지식 격차 해소&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;부족하지만 긴 글 읽어주셔서 감사합니다!&lt;/p&gt;</description>
      <category>Claude</category>
      <category>claude code</category>
      <category>바이브코딩</category>
      <category>클로드</category>
      <category>클로드코드</category>
      <author>rootTiket</author>
      <guid isPermaLink="true">https://root-2707.tistory.com/8</guid>
      <comments>https://root-2707.tistory.com/8#entry8comment</comments>
      <pubDate>Sun, 15 Feb 2026 18:16:27 +0900</pubDate>
    </item>
    <item>
      <title>NCP Finance 환경에서 GitHub Actions로 CI/CD 구축하기</title>
      <link>https://root-2707.tistory.com/7</link>
      <description>&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;인턴때 진행한 NCP&amp;nbsp;Finance&amp;nbsp;환경에서 &lt;b data-index-in-node=&quot;22&quot; data-path-to-node=&quot;4&quot;&gt;Bastion Host를 따로 두지 않고, Cloud Functions을 활용해 CI/CD 파이프라인을 구축한 과정&lt;/b&gt;을 공유해보고자 합니다. 어쩌다가 NCP Finance 환경을 사용하게 되신 분들은 도움이 되셨으면 좋겠습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;NCP 뿐 만 아니라 private subnet 환경에서 CICD를 구축해보고 싶으신 분들도 한번 참고해보시면 좋을 것 같습니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5&quot;&gt;1. 구축 배경&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트의 인프라 환경은 다음과 같은 제약이 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;7&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,0,0&quot;&gt;환경:&lt;/b&gt; NCP Finance&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0&quot;&gt;네트워크:&lt;/b&gt; 모든 애플리케이션 서버는 &lt;b data-index-in-node=&quot;20&quot; data-path-to-node=&quot;7,1,0&quot;&gt;Private Subnet&lt;/b&gt;에 위치&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,2,0&quot;&gt;접근 제어:&lt;/b&gt; 외부 인터넷에서 내부 서버로의 SSH(22번 포트) 접근 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;보통 이런 환경에서 자동 배포를 구성하려면 두 가지 방법 중 하나를 선택합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;Bastion Host 사용:&lt;/b&gt; Public Subnet에 중계 서버를 띄워두고 터널링으로 접근합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;이 방법이 가장 정석적인 방법이 아닐까 생각합니다. 하지만 비용문제로 인해 사용할 수 없었습니다.&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;NCP SourcePipeline 사용:&lt;/b&gt; 클라우드 벤더가 제공하는 CI 도구를 사용합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,1,0,0&quot;&gt;문제:&lt;/b&gt; 확인해 보니 빌드 환경이 Ubuntu 16.04 기반이었습니다. 최신 라이브러리와 Python 3.10 이상을 사용해야 하는 우리 프로젝트와 호환되지 않았고, 코드 저장소를 별도로 관리해야 하는 번거로움이 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;우리는 익숙한 GitHub Actions를 그대로 쓰면서, 추가 비용 없이 내부망에 배포할 방법이 필요했습니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11&quot;&gt;2. 해결 아이디어: Serverless를 트리거로 사용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;해결책으로 떠올린건 Cloud Functions를 활용하는 것이었습니다(&lt;a href=&quot;https://www.ncloud.com/v2/product/compute/cloudFunctions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Cloud Functions 란?&lt;/a&gt;). Cloud Functions는 VPC 내부 리소스에 접근할 권한을 가질 수 있고, 호출될 때만 과금되므로 Bastion 서버보다 훨씬 경제적입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;전체적인 아키텍처 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;blockquote data-path-to-node=&quot;14&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-path-to-node=&quot;14,0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,0&quot;&gt;GitHub Actions (빌드/Push) -&amp;gt; API Gateway -&amp;gt; Cloud Functions (VPC 내부) -&amp;gt; Private Server (SSH 명령)&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;690&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JcQKc/dJMcahwvvjA/Etz0tbLzcKYLcjAk7X9Bk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JcQKc/dJMcahwvvjA/Etz0tbLzcKYLcjAk7X9Bk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JcQKc/dJMcahwvvjA/Etz0tbLzcKYLcjAk7X9Bk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJcQKc%2FdJMcahwvvjA%2FEtz0tbLzcKYLcjAk7X9Bk0%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;692&quot; height=&quot;690&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;690&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;16&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16&quot;&gt;3. 구축 과정&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-path-to-node=&quot;17&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17&quot;&gt;Step 1. 배포 대상 서버(VM) 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;먼저 배포 대상 서버에 배포 스크립트(deploy.sh)를 작성해 둡니다. 외부에서 복잡한 명령어를 주입하는 것보다, 내부 스크립트를 실행만 하는 방식이 관리하기 편하고 오류 가능성도 적습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;무엇보다 외부에서 명령어를 직접 운영서버에 injection 할 경우 보안적으로 위협이 된다고 생각했기에 Cloudfunctions는 최대한 단순한 행위만 할 수 있도록 구성했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19&quot;&gt;/home/ubuntu/deploy.sh&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjYjfbPirWSAxUAAAAAHQAAAAAQ2wE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;#!/bin/bash
set -e

# 1. 최신 이미지 Pull 
docker pull &amp;lt;레지스트리 주소&amp;gt;

# 2. 기존 컨테이너 종료 및 정리
echo &quot;Stopping containers...&quot;
cd &amp;lt;docker compose 파일 위치&amp;gt;
docker compose down

# 3. 서비스 재시작
echo &quot;Deploying new version...&quot;
docker compose up -d 

# 4. 불필요한 이미지 정리
docker image prune -a -f&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-path-to-node=&quot;21&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;21&quot;&gt;Step 2. Cloud Functions 생성&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;Cloud Functions가 Private Server와 통신하려면 &lt;b data-index-in-node=&quot;55&quot; data-path-to-node=&quot;22&quot;&gt;반드시 동일한 VPC&lt;/b&gt;에 배치되어야 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;23&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;23,0,0&quot;&gt;Subnet 생성:&lt;/b&gt; Cloud Functions용 Subnet을 생성할 때, 배포 대상 서버와 통신 가능한 대역(Private)을 선택합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;23,1,0&quot;&gt;Action 생성:&lt;/b&gt; Python 등의 런타임을 선택하고 코드를 작성합니다. 이 함수는 SSH로 대상 서버에 접속해 sh deploy.sh를 실행하는 역할만 수행합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;24&quot;&gt;설정 체크포인트:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;25&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;VPC: Target Server와 동일한 VPC&lt;/li&gt;
&lt;li&gt;ACG 설정: 배포 대상 VM의 AGC에서 Cloud Functions의 주소로 22번 포트를 열어주어야 합니다.&lt;br /&gt;cloudfunctions-vpc&amp;lt;VPC_ID&amp;gt; 형식으로, Cloud Functions를 만들었다면 자동완성 추천이 나오는 걸로 설정해줍니다.&lt;/li&gt;
&lt;li&gt;Zone은 KR-2로 설정합니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9&quot;&gt;Step3. Action 코드 작성 및 패키징&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;SSH 접속을 위해 Python의 paramiko 라이브러리를 사용합니다. NCP Cloud Functions는 기본 라이브러리 외의 외부 라이브러리(pip install 필요 항목)를 사용할 경우, &lt;b data-index-in-node=&quot;112&quot; data-path-to-node=&quot;10&quot;&gt;반드시 로컬에서 패키징하여 Zip 파일로 업로드&lt;/b&gt;해야 합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11&quot;&gt;디렉토리 구조:&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjYjfbPirWSAxUAAAAAHQAAAAAQ8QE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;deploy-function/
├── __main__.py      # 메인 실행 코드
├── requirements.txt # 의존성 목록
└── package/         # pip install로 설치된 라이브러리 폴더
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13&quot;&gt;__main__.py 작성 예시:&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjYjfbPirWSAxUAAAAAHQAAAAAQ8gE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import sys
import os
# 패키지 경로 추가
sys.path.append(os.path.join(os.path.dirname(__file__), 'package'))

import paramiko

def main(args):
    host = &quot;10.0.x.x&quot;  # Target Server의 Private IP
    port = 22
    username = &quot;root&quot;
    password = args.get(&quot;SERVER_PASSWORD&quot;) # 혹은 SSH Key 방식 사용

    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    try:
        # 1. SSH 접속
        client.connect(host, port=port, username=username, password=password)
        
        # 2. 배포 스크립트 실행
        stdin, stdout, stderr = client.exec_command(&quot;sh /home/ubuntu/deploy.sh&quot;)
        
        # 3. 결과 리턴
        result = stdout.read().decode()
        error = stderr.read().decode()
        
        if error:
             return {&quot;payload&quot;: {&quot;status&quot;: &quot;error&quot;, &quot;log&quot;: error}}
        return {&quot;payload&quot;: {&quot;status&quot;: &quot;success&quot;, &quot;log&quot;: result}}
        
    except Exception as e:
        return {&quot;error&quot;: str(e)}
    finally:
        client.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3&quot;&gt;Step 4. API Gateway 연결: 외부 트리거 생성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;Cloud Functions를 만들었다면, 이제 GitHub Actions가 이 함수를 호출할 수 있도록 해야합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;API Gateway 콘솔에서 처음부터 세팅할 수도 있지만, Cloud Functions의 &lt;b data-index-in-node=&quot;135&quot; data-path-to-node=&quot;4&quot;&gt;[트리거]&lt;/b&gt; 탭을 이용하면 훨씬 간편하게 연동할 수 있습니다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5&quot;&gt;1. 트리거 생성 (Cloud Functions Console)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;작성한 Action의 상세 화면에서 &lt;b data-index-in-node=&quot;20&quot; data-path-to-node=&quot;6&quot;&gt;[트리거]&lt;/b&gt; 탭으로 이동해 + 트리거 추가 버튼을 클릭합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;7&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,0,0&quot;&gt;타입:&lt;/b&gt; API Gateway&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0&quot;&gt;동작:&lt;/b&gt; 신규 생성 (기존에 만들어둔 Product가 있다면 선택해도 됩니다)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,2,0&quot;&gt;옵션:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;7,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,2,1,0,0&quot;&gt;인증:&lt;/b&gt; '사용(API Key 필요)' 체크&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,2,1,1,0&quot;&gt;스테이지:&lt;/b&gt; v1 또는 real 등 식별 가능한 이름 입력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8&quot;&gt;2. 경로(Path) 설정과 /{type+}&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;URL 경로를 설정할 때 주의할 점이 있습니다. 단순히 /deploy로 끝내는 것이 아니라, 뒤에 /{type+}를 붙여주어야합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;설정 예시:&lt;/b&gt; /deploy/{type+}&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0&quot;&gt;이유:&lt;/b&gt; API Gateway가 Cloud Functions로 데이터를 넘길 때, JSON 포맷(Body)을 온전히 전달받기 위한 예약어 설정입니다. 이 설정을 하지 않으면 GitHub Actions에서 보낸 Payload가 함수 내부로 제대로 전달되지 않는 경우가 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11&quot;&gt;3. API 배포 (Publish)&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,0,0&quot;&gt;NCP 콘솔 &amp;gt; API Gateway&lt;/b&gt; 메뉴로 이동합니다.&lt;/li&gt;
&lt;li&gt;방금 생성된 Product를 선택하고 &lt;b data-index-in-node=&quot;21&quot; data-path-to-node=&quot;13,1,0&quot;&gt;[API 배포]&lt;/b&gt; 버튼을 클릭합니다.&lt;/li&gt;
&lt;li&gt;앞서 설정한 스테이지를 선택하여 배포를 완료합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14&quot;&gt;4. API Key 및 URL 확인&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;GitHub Actions에 등록할 두 가지 정보를 확보합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;16&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,0,0&quot;&gt;Invoke URL:&lt;/b&gt; API Gateway 콘솔의 [스테이지] 메뉴에서 배포된 URL을 확인합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;16,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;형식: https://{random-id}.apigw.ntruss.com/deploy/v1/json&lt;/li&gt;
&lt;li&gt;&lt;i data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,0,1,1,0&quot;&gt;(참고: /{type+}를 설정했으므로 호출 시 끝에 /json 등을 명시해야 데이터가 정상 바인딩됩니다.)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,1,0&quot;&gt;API Key:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;16,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,1,1,0,0&quot;&gt;[API Gateway &amp;gt; API Keys]&lt;/b&gt; 메뉴로 이동하여 API Key 생성을 클릭합니다.&lt;/li&gt;
&lt;li&gt;생성된 키를 복사해 둡니다.&lt;/li&gt;
&lt;li&gt;API Key들은 별도 탭에서 삭제도 가능하므로, 혹시 유출되었다면, 변경 혹은 삭제 후 재생성을 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-path-to-node=&quot;27&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;27&quot;&gt;Step 4. API Gateway 및 GitHub Actions 연동&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;Cloud Functions는 기본적으로 내부망에 숨어있기 때문에, 외부에서 호출할 수 있도록 &lt;b data-index-in-node=&quot;53&quot; data-path-to-node=&quot;28&quot;&gt;API Gateway&lt;/b&gt;와 연결합니다. 그리고 GitHub Actions 워크플로우 마지막 단계에서 이 API를 호출하도록 설정합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;29&quot;&gt;.github/workflows/deploy.yml&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjYjfbPirWSAxUAAAAAHQAAAAAQ3AE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;&lt;span&gt;YAML&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest

    steps:
      - name: Trigger Deployment via API
        run: |
          curl -X POST \
            -H &quot;x-ncp-apigw-api-key: ${{ secrets.NCP_APIGW_API_KEY }}&quot; \
            -H &quot;Content-Type: application/json&quot; \
            -d '{}' \
            ${{ secrets.NCP_APIGW_API_URL }}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;보안을 위해 API Key와 URL은 GitHub Secrets에 저장하여 관리합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;35&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;36&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;36&quot;&gt;4. 마무리&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;36&quot;&gt;정석적인 방법의 인프라 구축과는 조금 거리가 있어보였지만, 제약된 상황에서 얻은 성과는 다음과 같았습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;38&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;38,0,0&quot;&gt;비용 절감:&lt;/b&gt; 상시 운영해야 하는 Bastion 서버 비용을 아끼고, 배포 시에만 실행되는 Serverless 비용으로 운영이 가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;38,1,0&quot;&gt;개발 생산성:&lt;/b&gt; SourcePipeline의 제약에서 벗어나, 최신 Ubuntu 환경의 GitHub Actions를 자유롭게 사용할 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;39&quot; data-ke-size=&quot;size16&quot;&gt;금융 클라우드나 보안이 엄격한 환경에서 CI/CD 구축을 고민하고 있다면, 별도의 서버 증설 없이 Serverless 리소스를 활용해 연결 고리를 만들어보는 방식을 추천합니다.&lt;/p&gt;</description>
      <category>Infra</category>
      <category>CICD</category>
      <category>cloudfunctions</category>
      <category>github actions</category>
      <category>ncp</category>
      <category>NCP Finance</category>
      <author>rootTiket</author>
      <guid isPermaLink="true">https://root-2707.tistory.com/7</guid>
      <comments>https://root-2707.tistory.com/7#entry7comment</comments>
      <pubDate>Sat, 31 Jan 2026 17:28:03 +0900</pubDate>
    </item>
    <item>
      <title>[Recap] CRAYON를 재출시하며</title>
      <link>https://root-2707.tistory.com/6</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2938&quot; data-origin-height=&quot;1836&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqo0e0/btsMcKCW77Y/wTjq4HCMyKSzVn0bdGd070/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqo0e0/btsMcKCW77Y/wTjq4HCMyKSzVn0bdGd070/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqo0e0/btsMcKCW77Y/wTjq4HCMyKSzVn0bdGd070/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcqo0e0%2FbtsMcKCW77Y%2FwTjq4HCMyKSzVn0bdGd070%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;2938&quot; height=&quot;1836&quot; data-origin-width=&quot;2938&quot; data-origin-height=&quot;1836&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;CRAYON?&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;CRAYON은 동아리 모집 올인원 솔루션이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동아리 원을 모집하고, 평가를 관리하는 과정에서 생기는 불편함을 해결하자는 목표로 기획되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style2&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이제 회고를 해?&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;나는 CRAYON에 24년 여름부터 참여해왔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9월 1차 출시, 그리고 현재 2차 출시를 해오며 매번 회고해야겠다는 생각을 했지만 회고의 방향을 잡기 어려워 미뤄왔었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 출시를 한 지금 CRAYON에는 많은 이야기가 쌓였고, 개발자로서, 이야기를 공유하고 함께 공감했으면 하는 생각으로 회고를 작성하기로 했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;1. CRAYON 시작&lt;br /&gt;2. 1차 출시, 그리고 실패&lt;br /&gt;3. 변화&lt;br /&gt;4. 현재&lt;br /&gt;5. 회고를 마무리하며&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;&quot;&gt;&lt;b&gt;CRAYON 시작&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정말 잘 가꿔진 화원인줄 알았는데...&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 개발이 진행중인 CRAYON에 참여하기 시작하며 처음에는 잘 개발되고 있는 서비스겠지? 하는 막연한 기대가 있었다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;282&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zlboO/btsMa4Qq7VZ/6glJ4dHmKkj153fxPQio20/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zlboO/btsMa4Qq7VZ/6glJ4dHmKkj153fxPQio20/img.webp&quot; data-alt=&quot;아 !구조를 새로짜면 되겠다!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zlboO/btsMa4Qq7VZ/6glJ4dHmKkj153fxPQio20/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzlboO%2FbtsMa4Qq7VZ%2F6glJ4dHmKkj153fxPQio20%2Fimg.webp&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;282&quot; height=&quot;384&quot; data-origin-width=&quot;282&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아 !구조를 새로짜면 되겠다!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그대로 진행하기 보다 db구조를 다시 정리하고, 아키텍처에 필요한 세팅을 문서화 해 두고 시작하는게 좋겠다고 생각했다.&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;h4 data-ke-size=&quot;size20&quot;&gt;우리 잘 가고 있는거 맞겠지?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;CRAYON은 &quot;완벽보다 완성&quot; 이었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리가 생각한 완벽보다, 빠르게 완성해서 사용자들에게 직접 피드백을 받고 수용하는 것이 우리의 목표였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;타협하더라도 동아리 모집기간에 늦지 않게 완성하고, 실제로 서비스하며 개선할 생각이었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위한 7,8월은 정말 몰입 몰입 몰입이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하루에 6시간 이상 개발에 몰두했고 기획과 서비스 방향성에 대해 고민했다.&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;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style2&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1차출시, 그리고 실패&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;냉정하게 말하자면 CRAYON의 1차출시는 실패였다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; 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;많은 동아리가 모집 중 == 기회라고 생각했지만 사실은 그렇지 않았다.&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;CRAYON은 모집 기간이 아닌 모집 전 미리 배포가 이루어졌어야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 동아리 모집은 사전에 준비가 필요하고, 그 과정에서 CRAYON의 필요성이 빛을 발할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동아리 모집이 이미 시작해버린 시점에서 기존의 모집 방식을 버리고 CRAYON을 선택 할 수 있는 동아리는 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;/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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;변화&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;517&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d5wHwc/btsMcKbP4kQ/7kPcgbripmIRFVZvqsgfm1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d5wHwc/btsMcKbP4kQ/7kPcgbripmIRFVZvqsgfm1/img.jpg&quot; data-alt=&quot;다시 도전하면 그만이야&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d5wHwc/btsMcKbP4kQ/7kPcgbripmIRFVZvqsgfm1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd5wHwc%2FbtsMcKbP4kQ%2F7kPcgbripmIRFVZvqsgfm1%2Fimg.jpg&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;850&quot; height=&quot;517&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;517&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다시 도전하면 그만이야&lt;/figcaption&gt;
&lt;/figure&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;25년 1월 2차 출시라는 새로운 목표를 세웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차출시에서 생각한 CRAYON의 부족한 점은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 어려운 UX&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 안정성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 느린 기능&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;h4 data-ke-size=&quot;size20&quot;&gt;성공적인 재단장&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UX개선&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;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;CRAYON의 회원가입은 로그인 -&amp;gt; 동아리 생성 -&amp;gt; 홍보페이지 생성 으로 꽤 많은 단계를 거쳐야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 사용자가 가입과정에서 이탈할 수도 있다는 생각이 들어 로그인 -&amp;gt; 동아리 생성만 해도 서비스를 이용할 수 있도록 개선했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 &quot;이거 만들어야 하는거야?(홍보페이지)&quot; 같은 질문은 많이 줄어들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; 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;이전에는 여러 동아리에 콜드메일을 보내고, 최대한 이름을 알리는데에 집중했다.&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;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;p data-ke-size=&quot;size16&quot;&gt;서비스에 대한 고민들을 공유하고 의견을 나눌 수 있다면, CRAYON은 더 발전할 수 있다고 믿는다.&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;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style2&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;회고를 마무리하며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; 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;교내동아리부터 연합동아리까지 다양한 사용자를 구하고 있고  개발만큼이나 홍보에도 열을 올리고 있다!&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;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;끝으로 처음으로 서비스에 대해 깊게 고민하며 참여해 본 프로젝트인 만큼 CRAYON은 내게 의미가 깊은 프로젝트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 부족한 부분도 많지만, 부족한 만큼 더 열심히 채워나가고자 한다.&lt;/p&gt;</description>
      <category>Recap</category>
      <category>CRAYON</category>
      <category>개발동아리</category>
      <category>개발자</category>
      <category>동아리</category>
      <category>연합동아리</category>
      <author>rootTiket</author>
      <guid isPermaLink="true">https://root-2707.tistory.com/6</guid>
      <comments>https://root-2707.tistory.com/6#entry6comment</comments>
      <pubDate>Mon, 10 Feb 2025 01:05:42 +0900</pubDate>
    </item>
    <item>
      <title>[Recap] 2024년 회고 : 방황하고 넘어지고, 다시일어나기</title>
      <link>https://root-2707.tistory.com/5</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_6847.jpg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chMIX1/btsLBj7ZsIz/Gm4TOJr2aePjkjP6c0iX01/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chMIX1/btsLBj7ZsIz/Gm4TOJr2aePjkjP6c0iX01/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chMIX1/btsLBj7ZsIz/Gm4TOJr2aePjkjP6c0iX01/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchMIX1%2FbtsLBj7ZsIz%2FGm4TOJr2aePjkjP6c0iX01%2Fimg.jpg&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;560&quot; data-filename=&quot;IMG_6847.jpg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;2024년은 어떤 한 해였어?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년을 한마디로 정의하자면 &amp;ldquo;성장통 가득한 한 해&amp;rdquo;라고 답하고 싶다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 첫 서비스 출시&lt;br /&gt;2. 갑자기 맞이한 휴식기&lt;br /&gt;3. 넘어지기&lt;br /&gt;4. 다시 일어서기&lt;br /&gt;5. 그래서 2025년에는&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;첫 서비스 출시 : Fling&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024 2월에 2023년 1월부터 약 두 달간 진행한 프로젝트 Fling을 출시했다.&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;지금 돌아보면 JPA나 자바의 기초도 잘 안되어 있어 메인 기능을 담당하기 꺼리고 있던 내게 첫 도전이었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;서비스 자체로는 조금 아쉬웠다&lt;/b&gt;&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;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;size18&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;커뮤니케이션의 중요성과, 약간의 프론트엔드 지식의 도움을 받아 원활히 서비스를 마무리했던 경험은 이후 프로젝트에서도 도움이 되었다고 생각한다.&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;깃허브 링크 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://github.com/Leets-Official/Fling-BE&quot;&gt;https://github.com/Leets-Official/Fling-BE&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1735897857506&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - Leets-Official/Fling-BE: 꽃다발을 통한 추억 공유 서비스, Fling  &quot; data-og-description=&quot;꽃다발을 통한 추억 공유 서비스, Fling  . Contribute to Leets-Official/Fling-BE development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Leets-Official/Fling-BE&quot; data-og-url=&quot;https://github.com/Leets-Official/Fling-BE&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/AEDLB/hyXWqMwVsZ/zt2qDhCKNmLCd3oZ4J0Nik/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/b7UhSC/hyXWweS38M/LEQtst3yonoTtw0IcwALa1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Leets-Official/Fling-BE&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Leets-Official/Fling-BE&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/AEDLB/hyXWqMwVsZ/zt2qDhCKNmLCd3oZ4J0Nik/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/b7UhSC/hyXWweS38M/LEQtst3yonoTtw0IcwALa1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - Leets-Official/Fling-BE: 꽃다발을 통한 추억 공유 서비스, Fling  &lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;꽃다발을 통한 추억 공유 서비스, Fling  . Contribute to Leets-Official/Fling-BE development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;갑자기 맞이한 휴식기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2023 하반기 부터 시작했던 개발은 초반에는 즐거움의 연속이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 학기내 토이프로젝트 두개, 학업 병행, 그리고 이어진 정규 프로젝트 Fling을 거치며 즐거움 보다는 부담감, 막막함만 남기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;내가 정말 백엔드 개발로 나아가는게 맞는것인가?&quot; &quot;프론트엔드로 다시 넘어가서 제대로 개발해볼까?&quot; 하는 생각에 사로잡혀서 무엇하나 제대로 시작하지 못한 3개월이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQNjmi/btsLD1k8RmV/YkkG26ddMXkd3LKmQTn4fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQNjmi/btsLD1k8RmV/YkkG26ddMXkd3LKmQTn4fk/img.png&quot; data-alt=&quot;실제로 Fling이후로 많은것을 하지는 않았다..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQNjmi/btsLD1k8RmV/YkkG26ddMXkd3LKmQTn4fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQNjmi%2FbtsLD1k8RmV%2FYkkG26ddMXkd3LKmQTn4fk%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;772&quot; height=&quot;258&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제로 Fling이후로 많은것을 하지는 않았다..&lt;/figcaption&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;size18&quot;&gt;&lt;b&gt;꼭 필요했던 시간&lt;/b&gt;&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;p data-ke-size=&quot;size16&quot;&gt;이 시간이 있었기에 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;다시 개발에 몰입할 수 있었다고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;마냥 쉬지는 않았다&lt;/span&gt;&lt;/b&gt;&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;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;1. 취미생활 다시 시작하기&lt;/b&gt;&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;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;2. 학업에 열중하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학업에 열중하기는 일단 개발을 하지 않는다면? 본업인 학업에 집중해보자! 라는 생각으로 제대로 했던 것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 당시에 공부법과 몰입하는 방법에 대한 고민을 많이 했고 스스로 시행착오를 겪어가며 나만의 공부 방법을 적립해 나갔던 시간이었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기록과 정리, 그리고 몰입의 중요성을 몸소 체험할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_DA2DE194814C-1.jpeg&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;996&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3F0Md/btsLDeyDtFN/utuFUbdHor4hdN5MUc9AWk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3F0Md/btsLDeyDtFN/utuFUbdHor4hdN5MUc9AWk/img.jpg&quot; data-alt=&quot;결과도 좋았다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3F0Md/btsLDeyDtFN/utuFUbdHor4hdN5MUc9AWk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3F0Md%2FbtsLDeyDtFN%2FutuFUbdHor4hdN5MUc9AWk%2Fimg.jpg&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;477&quot; height=&quot;403&quot; data-filename=&quot;IMG_DA2DE194814C-1.jpeg&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;996&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과도 좋았다&lt;/figcaption&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;&lt;b&gt;3. 자바 공부하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바공부도 멈추지 않았다. 이전 Fling에서 깨달았던 것은 나는 자바와 스프링을 정말 모르고 개발하고 있다는 사실이었다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;넘어지기&lt;/h3&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;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;1. 단기현장실습&amp;nbsp;&lt;/b&gt;&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;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;2. 연합동아리&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연합동아리는 기업은 아니지만, 다양한 개발자들과 함께 개발하며 새로운 인사이트를 얻을 수 있다는 점이 매력적으로 다가와 지원을 결심했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇보다 &quot;실제로 사용하는 서비스&quot;를 만들고 싶다는 열망이 컸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 프로젝트들을 통해 깨달은 점은 단순히 서비스를 출시하는 것을 넘어 실제로 서비스를 유지보수하는 경험이 필요하다는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 사용자들을 위한 서비스에는 디자인과 탄탄한 기획이 핵심이라고 판단했고, 이에 디자이너와의 협업이 가능한 NEXTERS와 DND에 관심을 갖고 지원하게 되었다.&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;포트폴리오를 정리하고 지원했으나 서류도 통과하지 못했다.&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;3. ICT 인턴십&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문을 계속 두드리면 언젠가 열리지 않을까 하는 생각에 지원하는 것을 멈추지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현장실습과 마찬가지로 학점과 동시에 실무 경험을 할 수 있는 ICT 인턴십을 다음 목표로 정했다.&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;ICT인턴십은 총 세개의 기업에 지원 할 수 있고 앞에 두 활동과는 다르게 코딩테스트가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특이하게 기업에서 각각 다른 코딩테스트를 보는 것이 아니라 ICT인턴십 공통 코딩테스트를 진행한다.&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;p data-ke-size=&quot;size16&quot;&gt;좀 꾸준히 코딩테스트를 준비해야할 필요성을 느꼈다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 다섯개 중에 결과는 4솔, 네 번째 문제에서 히든케이스를 통과하지 못해 완벽한 5솔은 하지 못했다.&amp;nbsp;&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;210&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beqD0F/btsLC01p6UN/krtw4AReRzKzISFUw7K5zK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beqD0F/btsLC01p6UN/krtw4AReRzKzISFUw7K5zK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beqD0F/btsLC01p6UN/krtw4AReRzKzISFUw7K5zK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeqD0F%2FbtsLC01p6UN%2Fkrtw4AReRzKzISFUw7K5zK%2Fimg.jpg&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;210&quot; height=&quot;210&quot; data-origin-width=&quot;210&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;지금 회고해 보자면 포트폴리오의 문제가 컸다고 생각한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지 해 온 것의 나열보다, 어떤 것을 이루어냈고 또 무엇을 개선했가에 대한&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이야기가 담겨있지 않았기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 그와 별개로 점점 자신감을 잃는 것은 어쩔 수 없었다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;제대로 된 분석 없이, 큰 개선 없이 여러번 반복해서 지원하고 실패한 것이 누군가에게는 미련한 일 처럼 보이겠지만&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;나에게는 넘어지는 연습과도 같았다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;어쩌면 성공보다도 더 값진 실패였다고 생각한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;다시 일어나기&lt;/h3&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;라인업지&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;친한 형과 헬스장에서 나눈 우연한 대화로 시작된 프로젝트.&lt;br /&gt;&quot;주점 웨이팅 서비스는 없을까?&quot;라는 질문에서 출발했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 정말 많은 회고거리를 남겼다.&lt;br /&gt;하나의 프로덕트가 많은 사용자에게 쓰이기까지 얼마나 어려운 과정을 거쳐야 하는지 뼈저리게 깨달았다.&lt;br /&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;br /&gt;5월에 기획을 시작한 프로젝트의 본격적인 개발은 7월이 되어서야 궤도에 올랐다.&lt;br /&gt;그 시기는 정말로 어려웠다. 그런데도 &quot;이 프로젝트가 그렇게 어려운 건 아니겠지?&quot;라는 안일한 생각을 했던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라인업지를 진행하며 가장 크게 얻은 것은 단연 팀워크였다.&lt;br /&gt;팀이 정말 잘 뭉치기가 얼마나 어려운지, 그리고 실제 사용자를 위한 프로덕트를 만들어내기 위해&lt;br /&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;5월에 시작하며 &quot;과연 잘 끝낼 수 있을까?&quot; 걱정이 많았던 라인업지를 성공적으로 서비스했다.&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;br /&gt;결국 오버스펙의 서버를 사용해 과도한 비용이 발생했다.&lt;br /&gt;더 많이 공부했더라면, 더 세심하게 신경 썼더라면 하는 후회가 남았지만,&lt;br /&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;a href=&quot;https://github.com/Gachon-Table/GachonTable-BE&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Gachon-Table/GachonTable-BE&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1735907060382&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - Gachon-Table/GachonTable-BE: 가천대학교 가을 축제 주점 웨이팅 서비스&quot; data-og-description=&quot;가천대학교 가을 축제 주점 웨이팅 서비스. Contribute to Gachon-Table/GachonTable-BE development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Gachon-Table/GachonTable-BE&quot; data-og-url=&quot;https://github.com/Gachon-Table/GachonTable-BE&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jMQo7/hyXWoBeamB/rZyKkmgERHOrw5iQw9eNK1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bfY9zw/hyXWsRbSkH/SXh7xR6O8VvSmH7tngTq80/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Gachon-Table/GachonTable-BE&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Gachon-Table/GachonTable-BE&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jMQo7/hyXWoBeamB/rZyKkmgERHOrw5iQw9eNK1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bfY9zw/hyXWsRbSkH/SXh7xR6O8VvSmH7tngTq80/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - Gachon-Table/GachonTable-BE: 가천대학교 가을 축제 주점 웨이팅 서비스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;가천대학교 가을 축제 주점 웨이팅 서비스. Contribute to Gachon-Table/GachonTable-BE development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Crayon 시작&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7월, Crayon 이라는 서비스의 합류 제안을 받았다.&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;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;/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;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;Crayon을 통해 스스로의 방향성을 다시 정립할 수 있었다.&lt;br /&gt;특히 협업 과정에서 &quot;어떻게 하면 좋은 팀원이 될 수 있을까?&quot;에 대해 고민하기 시작했다.&lt;br /&gt;작은 배려와 세심함 덕분에 동료들에게 &quot;너랑 같이 작업하니까 편하다.&quot;라는 말을 들었을 때,&lt;br /&gt;다른 사람과 일하기 좋은 개발자가 되고 싶다는 새로운 목표를 가지게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;첫 동아리 운영진과 컨퍼런스 개최&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1년 동안 몸담아온 동아리 Leets의 운영진으로 합류했다.&lt;br /&gt;애정이 깊은 첫 동아리에 기여하고 싶었고, 미숙하지만 새로운 도전에 나서고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동아리 운영은 정말 쉬운 일이 하나도 없었다.&lt;br /&gt;개인보다는 팀과 동아리 전체를 우선으로 생각해야 했고, 넓은 시야와 책임감이 필요했다.&lt;br /&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;이번 동아리 기수에서는 많은 새로운 시도가 있었다.&lt;br /&gt;그중 가장 큰 예시는 GDG on Campus와 공동 기획한 컨퍼런스 마일스콘&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주니어 개발자와 시니어 개발자의 이야기를 듣는 컨퍼런스는 외부에 많지만,&lt;br /&gt;작은 공간에서 연사님들의 이야기를 가까이에서 듣고 질문할 수 있는 자리를 마련하는 것은 쉬운 일이 아니었다.&lt;br /&gt;연사 섭외부터 장소 준비, 케이터링 규모와 시간 조정까지 고려할 것이 많았지만,&lt;br /&gt;각 동아리에서 참여해준 모든 분들과 하나씩 해나가며 성공적으로 마무리할 수 있었다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;우아한 테크코스 도전&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2023년에는 도전하지 않았던 우아한 테크코스에 도전했다.&lt;br /&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;프리코스에서의 4주는 정말 유익했다고 단언할 수 있다.&lt;br /&gt;이 시간은 그동안 개발하면서 생긴 나쁜 습관들을 고칠 수 있는 기회였기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각하며 코드를 짜는 습관은 프리코스 이후에도 실천하고 있고&lt;br /&gt;단일 책임 원칙, 올바른 메서드 이름 작성, 객체를 일하게 하는 코드 스타일 등을 Crayon에 적용하며 개선할 수 있었다.&lt;br /&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;또한, 메타인지에 대해서도 고민하게 되었다.&lt;br /&gt;내가 정말 하고 싶은 것은 무엇인지, 부족한 점은 무엇인지, 잘하는 것은 무엇인지 깊이 생각해봤다.&lt;br /&gt;이는 개발자로서뿐만 아니라 &quot;나&quot;라는 인간에 대해 고찰하며, 반년 넘게 해오던 고민을 조금이나마 해결할 수 있는 계기가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 7기에 합격하지는 못했지만, 앞으로 나아갈 길을 더 단단히 다질 수 있었던 소중한 4주였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;그래서 2025년에는&lt;/h3&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;1. 그냥 작동하는 코드 보다는 깊이있는 개발&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 한번 보고 끝내는 것이 아닌 적용하고 문서로 남기는 공부&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 개발하고 방치하는 것이 아니라 유지보수하는 경험&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 제발 이제는 합격해보기&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;2024는 도전과 실패의 연속이었다면 2025년에는 과정이 아닌 결과를 남기고 싶다.&lt;/p&gt;</description>
      <category>Recap</category>
      <category>2024 회고</category>
      <category>개발자 회고</category>
      <category>회고</category>
      <author>rootTiket</author>
      <guid isPermaLink="true">https://root-2707.tistory.com/5</guid>
      <comments>https://root-2707.tistory.com/5#entry5comment</comments>
      <pubDate>Fri, 3 Jan 2025 22:00:07 +0900</pubDate>
    </item>
    <item>
      <title>[Recap] 모던 자바 인 액션 스터디 회고</title>
      <link>https://root-2707.tistory.com/4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가면서&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3개월간 진행한 모던 자바 인 액션 스터디가 종료됐다.&lt;br /&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;a href=&quot;https://github.com/Leets-Official/modern-java-in-action&quot;&gt;https://github.com/Leets-Official/modern-java-in-action&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1722064085702&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - Leets-Official/modern-java-in-action: Leets 3기 자바 스터디&quot; data-og-description=&quot;Leets 3기 자바 스터디. Contribute to Leets-Official/modern-java-in-action development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Leets-Official/modern-java-in-action&quot; data-og-url=&quot;https://github.com/Leets-Official/modern-java-in-action&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/y9VrF/hyWGYcwhjz/KiY2e4ZMmZitGJXmWL57wk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Leets-Official/modern-java-in-action&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Leets-Official/modern-java-in-action&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/y9VrF/hyWGYcwhjz/KiY2e4ZMmZitGJXmWL57wk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - Leets-Official/modern-java-in-action: Leets 3기 자바 스터디&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Leets 3기 자바 스터디. Contribute to Leets-Official/modern-java-in-action development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 모던 자바 인 액션이었지?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모던 자바 인 액션을 고른 이유는 이 당시 스트림과 람다를 전혀 사용할 줄 몰랐기 때문이다.&lt;br /&gt;심지어 스트림과 람다를 쓴 코드를 거의 이해하지도 못했다. 스스로 사용 못하는 것 보다도 다른 사람의 코드를 읽지 못하는 것은 코드 리뷰나 앞으로 진행할 프로젝트에서 큰 걸림돌이라고 생각했고 고민도 없이 모던 자바 인 액션을 선택했다. 다행히 팀원들도 모두 동의해서 무사히 모던 자바 인 액션을 읽을 수 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스터디.. 어떻게 해야 하지?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디의 시작이 동아리 스터디였던 만큼 다른 스터디 팀들과 비슷한 방식으로 스터디를 진행하려 했다.&lt;br /&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 챕터들 안에서 문제를 만들어 이슈로 올리기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 정기 모임에서 다 같이 풀어보기&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;1. 책을 읽기보다 문제를 찾기 위해 집중하게 되지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 결국 공부는 혼자 하는 것, 정기모임이 공부의 주가 되면 안 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 아는 것으로 문제를 만들기보다 모르는 것을 질문하는 것이 더 유익하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 결국 숙제처럼 느껴진다면 혼자 읽는 것보다 좋은 방법이라고 생각하지 않는다.&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-27 오후 3.25.10.png&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;1002&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blgYda/btsIPuqNbUY/sUSiiWyJPzlC6FOfPI6KkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blgYda/btsIPuqNbUY/sUSiiWyJPzlC6FOfPI6KkK/img.png&quot; data-alt=&quot;노션에 제안서 까지 적었다..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blgYda/btsIPuqNbUY/sUSiiWyJPzlC6FOfPI6KkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblgYda%2FbtsIPuqNbUY%2FsUSiiWyJPzlC6FOfPI6KkK%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;1632&quot; height=&quot;1002&quot; data-filename=&quot;스크린샷 2024-07-27 오후 3.25.10.png&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;1002&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;노션에 제안서 까지 적었다..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-27 오후 3.27.03.png&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bl2TEY/btsIPTRd8Ms/W72zkuBot4zmfQmZdMnw9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bl2TEY/btsIPTRd8Ms/W72zkuBot4zmfQmZdMnw9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bl2TEY/btsIPTRd8Ms/W72zkuBot4zmfQmZdMnw9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbl2TEY%2FbtsIPTRd8Ms%2FW72zkuBot4zmfQmZdMnw9K%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;1616&quot; height=&quot;802&quot; data-filename=&quot;스크린샷 2024-07-27 오후 3.27.03.png&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;a href=&quot;https://pumped-marquess-04e.notion.site/42201c8659584c11acc92f45e1a6b7cd?pvs=74&quot;&gt;https://pumped-marquess-04e.notion.site/42201c8659584c11acc92f45e1a6b7cd?pvs=74&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1722062038085&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;스터디 학습 가이드라인  | Notion&quot; data-og-description=&quot;✏️&amp;nbsp;스터디&amp;hellip; 시작했는데 어떻게 정리해요?&quot; data-og-host=&quot;pumped-marquess-04e.notion.site&quot; data-og-source-url=&quot;https://pumped-marquess-04e.notion.site/42201c8659584c11acc92f45e1a6b7cd?pvs=74&quot; data-og-url=&quot;https://pumped-marquess-04e.notion.site/42201c8659584c11acc92f45e1a6b7cd&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bwhMPy/hyWGY4Fp0E/H7hcl3FZHd5oO3KJkQu5oK/img.png?width=2000&amp;amp;height=1054&amp;amp;face=0_0_2000_1054,https://scrap.kakaocdn.net/dn/cVf28f/hyWGVGRsol/uqx3Li0oAKCkUtdPQhG8Ak/img.png?width=2000&amp;amp;height=1054&amp;amp;face=0_0_2000_1054&quot;&gt;&lt;a href=&quot;https://pumped-marquess-04e.notion.site/42201c8659584c11acc92f45e1a6b7cd?pvs=74&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pumped-marquess-04e.notion.site/42201c8659584c11acc92f45e1a6b7cd?pvs=74&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bwhMPy/hyWGY4Fp0E/H7hcl3FZHd5oO3KJkQu5oK/img.png?width=2000&amp;amp;height=1054&amp;amp;face=0_0_2000_1054,https://scrap.kakaocdn.net/dn/cVf28f/hyWGVGRsol/uqx3Li0oAKCkUtdPQhG8Ak/img.png?width=2000&amp;amp;height=1054&amp;amp;face=0_0_2000_1054');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;스터디 학습 가이드라인 | Notion&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;✏️&amp;nbsp;스터디&amp;hellip; 시작했는데 어떻게 정리해요?&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pumped-marquess-04e.notion.site&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;좋았던 점&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;회고록 방식은 좋았다!&lt;/b&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;회고를 작성하면서 자신이 모르는 것을 스스로 공부하고 질문할 수 있는 분위기가 만들어지는 부분이 긍정적이게 다가왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획 당시 내가 원하는 스터디 또한 &quot;각자 공부하고 편하게 질문하자! &quot; 였기에 이 부분들이 잘 실현된 거 같아 뿌듯했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;하지만 아쉬움이 더 많았던 스터디&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;후순위로 밀려버려 아쉬워요&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞는 말이다.. 어쨌는 1주일에 회고록 한 개를 작성한다는 건 시험기간, 개인 프로젝트가 겹치는 시기를 지나며 점점 후순위로 밀려났다. 여유 있는 시기에는 즐거웠으나 시간이 지나며 점점 짐과 숙제처럼 다가왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;솔직히 재미없어요&lt;/b&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;초반은 각자 부족함이 눈에 보여 책을 읽을 때 즐거웠으나 시간이 지날수록 당장의 이슈, 그리고 흥미 있는 분야가 변하면서 자연스럽게 흥미가 떨어진 것으로 보인다.&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;그래서 이대로 끝내려고?&lt;/b&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;대신 새로운 스터디를 위해 앞으로 어떤 것을 바라는지 함께 회고해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;재밌는 스터디를 하고 싶어요!&lt;/b&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;그러나 6명이 되는 인원이 모두 흥미 있을만한 주제를 어떻게 찾기는 쉽지 않다.&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;스크린샷 2024-07-27 오후 4.38.41.png&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;1118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcIqC8/btsIQIajhFq/kRwspVLxBf1154iV8ku020/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcIqC8/btsIQIajhFq/kRwspVLxBf1154iV8ku020/img.png&quot; data-alt=&quot;앞으로 바라는 점&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcIqC8/btsIQIajhFq/kRwspVLxBf1154iV8ku020/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcIqC8%2FbtsIQIajhFq%2FkRwspVLxBf1154iV8ku020%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;1414&quot; height=&quot;1118&quot; data-filename=&quot;스크린샷 2024-07-27 오후 4.38.41.png&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;1118&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;앞으로 바라는 점&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;스터디에서 스터디메이트로&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 각자 목표를 정하고 결과를 확인해 보고 과정을 이야기해보는 스터디는 어떨까?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일주일 간 각자의 목표를 이야기하고 한 주의 과정을 함께 이야기하는 스터디를 진행해보려 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 각자의 일주일간 목표 정하기&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 목표 피드백 해주기&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. 결과도 함께 확인하기&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4. 실패 시 벌금(일정 금액이 모이면 다 같이 회식하기)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비록 같은 공부를 하는 스터디는 아니지만 공부할 때 서로 공유할 것이 많은 게 진정한 스터디의 장점이라고 생각한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;언제 까지라고 기한은 정하지 않았지만 위처럼 스터디를 진행해 보면서 얻은 경험을 나중에 회고해 보도록 하자.&lt;/p&gt;</description>
      <category>Recap</category>
      <category>개발자 스터디</category>
      <category>모던 자바 인 액션</category>
      <category>스터디</category>
      <author>rootTiket</author>
      <guid isPermaLink="true">https://root-2707.tistory.com/4</guid>
      <comments>https://root-2707.tistory.com/4#entry4comment</comments>
      <pubDate>Sat, 27 Jul 2024 16:34:03 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] MapStruct, Mapper 가 제대로 작동하지 않음</title>
      <link>https://root-2707.tistory.com/3</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Mapper가 제대로 작동이 안되어 팀원이 4시간이 넘는 트러블 슈팅을 겪었다&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;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;분명 Mapper 문법에 맞게 작성했는데 구현체가 생성되지 않고, 코드가 제대로 실행되지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하자면 Target 에 Setter가 누락되어서 생기는 오류였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mapper가 어떻게 작동하는지 부터 다시 되짚어보며 문제를 해결해봤다.&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;Mapper, 그럼 어떻게 구현되는가?&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Mapper
public interface TestMapper{
    void updateHuman(TestDto testDto, @MappingTarget Test test);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 코드를 작성하면 testDto 의 내용이 Test 객체로 매핑된다!&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;b&gt;공식 문서를 보자&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2003&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xhfK5/btsILRz66cs/NKb22GY4KCwQcLwLQP4lyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xhfK5/btsILRz66cs/NKb22GY4KCwQcLwLQP4lyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xhfK5/btsILRz66cs/NKb22GY4KCwQcLwLQP4lyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxhfK5%2FbtsILRz66cs%2FNKb22GY4KCwQcLwLQP4lyK%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;632&quot; height=&quot;633&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2003&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mapper는 구현될 때 target에 setter를 이용하여 매핑을 해준다는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 직접 setter를 이용해서 필드의 값을 하나씩 매핑하는 과정을 생략해주는 것이라고 볼 수 있다!&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;br /&gt;코드로 DTO를 target(엔티티)로 매핑하는 과정을 보면서 Setter가 있는경우와 없는 경우를 비교해 보자&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;testUse(Target이 될 클래스)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Getter
public class TestUse {
    private String f1;
    private String f2;
}
&lt;/code&gt;&lt;/pre&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;DTO&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public record Dto (
    String f1, 
    String f2
){}&lt;/code&gt;&lt;/pre&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;Mapper Interface 정의&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Mapper
public interface TestMapper {
    TestMapper INSTANCE = Mappers.getMapper(TestMapper.class);
    TestUse toTestUse(Dto dto);
}&lt;/code&gt;&lt;/pre&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;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class UserMapperTest {
    @Test
    public void testEntityToDTOWithoutSetters() {
        Dto dto = new Dto(&quot;f1&quot;,&quot;f2&quot;);
        TestUse testUse = TestMapper.INSTANCE.toTestUse(dto);
        System.out.println(testUse.getF1());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dto 를 생성해주고 TestUse와 매핑, 그 결과를 출력해보자&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;pre id=&quot;code_1721967474921&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;null&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상처럼 null이 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Setter를 정의해주지 않아 객체에 값을 넣어주지 못해 null을 반환하는 것.&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;pre id=&quot;code_1721967675421&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class TestMapperImpl implements TestMapper {

    @Override
    public TestUse toTestUse(Dto dto) {
        if ( dto == null ) {
            return null;
        }

        TestUse testUse = new TestUse();

        return testUse;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현체에 TestUse를 설정하는 코드가 생성되지 않는다!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 setF1, setF2를 해 주지 못해 빈 객체를 반환하게 된다.&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;Setter 대신 Builder로도 확인해볼까?&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1721969091513&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
    public TestUse toTestUse(Dto dto) {
        if ( dto == null ) {
            return null;
        }

        TestUse.TestUseBuilder testUse = TestUse.builder();

        testUse.f1( dto.f1() );
        testUse.f2( dto.f2() );

        return testUse.build();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Builder 어노테이션이 있다면 이를 감지하여 자동으로 구현체를 생성해준다.&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 id=&quot;code_1721969317996&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
    public TestUse toTestUse(Dto dto) {
        if ( dto == null ) {
            return null;
        }

        String f1 = null;
        String f2 = null;

        f1 = dto.f1();
        f2 = dto.f2();

        TestUse testUse = new TestUse( f1, f2 );

        return testUse;
    }&lt;/code&gt;&lt;/pre&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;MapStruct 사용하면 편하게 객체사이를 매핑 할 수 있지만 이 또한 자동이라고 생각하지 말자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MapStruct의 역할은 객체의 Setter, Builder, 생성자를 이용해 매핑을 자동화 해 주는 것이므로 매핑이 제대로 되지 않는것 같다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스에 Setter, Builder가 선언되어있는지 확인해 보면 좋을것 같다.&lt;/p&gt;</description>
      <category>SpringBoot</category>
      <category>Mapper</category>
      <category>mapstruct</category>
      <category>Spring boot</category>
      <author>rootTiket</author>
      <guid isPermaLink="true">https://root-2707.tistory.com/3</guid>
      <comments>https://root-2707.tistory.com/3#entry3comment</comments>
      <pubDate>Fri, 26 Jul 2024 13:56:32 +0900</pubDate>
    </item>
  </channel>
</rss>