<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ooleem-generator.log</title>
        <link>https://velog.io/</link>
        <description>개발 / 성장 노트</description>
        <lastBuildDate>Mon, 19 Jan 2026 12:28:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ooleem-generator.log</title>
            <url>https://velog.velcdn.com/images/ooleem-generator/profile/7ea9d8a6-e281-4206-b236-9b487d4572f2/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ooleem-generator.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ooleem-generator" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[데이터베이스] 트랜잭션 격리 수준(Isolation Level) vs. 락(Lock)]]></title>
            <link>https://velog.io/@ooleem-generator/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80Isolation-Level-vs.-%EB%9D%BDLock</link>
            <guid>https://velog.io/@ooleem-generator/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80Isolation-Level-vs.-%EB%9D%BDLock</guid>
            <pubDate>Mon, 19 Jan 2026 12:28:33 GMT</pubDate>
            <description><![CDATA[<p>DeepQuest TRD 리뷰 중 언급되었던 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock), 그리고 데이터베이스의 트랜잭션 격리 수준에 대해 좀 더 알아보고자 한다. 또한, 이번에는 왜 낙관적 락을 선택했는지에 대해서도 좀 더 자세히 서술하고자 한다.</p>
<h2 id="트랜잭션-격리-수준-isolation-level">트랜잭션 격리 수준 (Isolation Level)</h2>
<h3 id="트랜잭션-격리-수준이란">트랜잭션 격리 수준이란?</h3>
<ul>
<li>&quot;다른 사람이 작업 중인 데이터를 어디까지 보여 줄 것인가?&quot; &quot;내가 읽을 때 남의 작업이 보이느냐 마느냐?&quot;에 초점을 맞춘 <strong>읽기 일관성</strong> 전략</li>
<li>여러 사람이 동시에 같은 데이터를 보거나 고칠 때 <strong>&quot;어디까지 허용해 줄 것인가&quot;</strong>에 대한 규칙</li>
<li>데이터베이스에서 자체적으로 결정</li>
</ul>
<h3 id="1-read-uncommited-가장-낮은-단계">1. READ UNCOMMITED (가장 낮은 단계)</h3>
<blockquote>
<p>&quot;남이 작업 중인 임시 데이터도 다 보여!&quot;</p>
</blockquote>
<p>이 수준에서는 다른 사람이 수정 중이고 아직 &#39;저장(Commit)&#39;하지 않은 데이터도 읽을 수 있다.</p>
<h4 id="발생하는-문제-dirty-read-오염된-읽기">발생하는 문제: Dirty Read (오염된 읽기)</h4>
<p>상황: 철수가 통장 잔액 10,000원을 0원으로 바꾸는 작업 시작 (아직 완료 버튼은 안 누름)</p>
<p>사건: 그 찰나에 영희가 철수의 잔액을 조회하니 0원으로 보임</p>
<p>반전: 철수가 마음이 변해 작업 취소(Rollback), 철수의 잔액은 다시 10,000원이 됨</p>
<p>결과: 영희는 존재하지도 않았던 &#39;0원&#39;이라는 가짜 데이터를 본 셈</p>
<h3 id="2-read-commited-가장-많이-쓰는-단계">2. READ COMMITED (가장 많이 쓰는 단계)</h3>
<blockquote>
<p>&quot;확정된(Commit) 데이터만 보여줄게. 하지만 내 마음은 갈대 같아&quot;</p>
</blockquote>
<p>가장 일반적인 설정으로, 누군가 수정을 완료해서 &#39;저장&#39; 버튼을 누른 데이터만 읽을 수 있다. 하지만 한 트랜잭션 내에서 같은 데이터를 두 번 조회할 때 결과가 달라질 수 있다.</p>
<h4 id="발생하는-문제-non-repeatable-read-반복-불가능한-읽기">발생하는 문제: Non-Repeatable Read (반복 불가능한 읽기)</h4>
<p>상황: 영희가 철수의 잔액을 조회, 10,000원이 출력됨</p>
<p>사건: 그 사이 철수가 편의점에서 과자를 사 먹고 잔액을 5,000원으로 바꾼 뒤 &#39;저장&#39;</p>
<p>문제: 영희가 같은 화면에서 새로고침을 누르니 갑자기 5,000원으로 바뀜</p>
<p>결과: &quot;어? 아까 분명히 만 원이었는데 왜 바뀌었지?&quot; -&gt; 한 작업 안에서 일관성 깨짐</p>
<h3 id="3-repeatable-read-mysql-기본값">3. REPEATABLE READ (MySQL 기본값)</h3>
<blockquote>
<p>&quot;내가 한 번 본 데이터는 내가 끝날 때까지 절대 안 변해!&quot;</p>
</blockquote>
<p>트랜잭션이 시작될 때의 데이터 상태를 기억해서, 그 트랜잭션이 끝날 때까지는 남이 데이터를 백번 고쳐도 나에게는 똑같은 모습으로 보인다.</p>
<h4 id="발생하는-문제-phantom-read-유령-읽기">발생하는 문제: Phantom Read (유령 읽기)</h4>
<p>상황: 영희가 &#39;우리 반 전체 학생 수&#39;를 조회, 10명 출력</p>
<p>사건: 그 사이 선생님이 전학생 1명을 추가하고 &#39;저장&#39;</p>
<p>문제: 영희가 다시 학생 수를 조회하면 여전히 10명으로 보이지만, 학생 명단을 하나씩 수정하려고 하니 <strong>처음엔 없었던 11번째 학생(유령)</strong>이 갑자기 나타나 수정되는 등의 기괴한 현상 발생</p>
<p>결과: 데이터의 &#39;값&#39;은 고정되지만, &#39;새로 생기거나 사라지는 레코드&#39;까지는 막지 못함</p>
<h3 id="4-serializable-가장-높은-단계">4. SERIALIZABLE (가장 높은 단계)</h3>
<blockquote>
<p>&quot;한 줄 서기! 앞사람 끝날 때까지 아무도 손대지 마!&quot;</p>
</blockquote>
<p>가장 엄격한 단계로, 누군가 데이터를 읽기만 해도 다른 사람은 그 데이터를 수정하거나 추가할 수 없다.</p>
<h4 id="발생하는-문제-매우-성능이-낮아짐">발생하는 문제: 매우 성능이 낮아짐</h4>
<p>위에서 말한 모든 문제(Dirty, Non-Repeatable, Phantom Read)가 해결되긴 하지만, 한 사람이 데이터를 조회하고 있을 경우 다른 사람은 데이터 조회조차 불가능해지기 때문에 성능이 매우 느려짐</p>
<h2 id="락이-필요한-이유--두-번의-갱신-분실-문제-lost-update">락이 필요한 이유 : 두 번의 갱신 분실 문제 (Lost Update)</h2>
<p>앞서 설명했듯, 트랜잭션 격리 수준은 <strong>읽기 일관성</strong>에만 관여하기 때문에, 두 사용자가 한 대상을 동시에 수정할 경우에 대해서는 방어해주지 않는다.</p>
<h3 id="lost-update가-발생하는-상황">Lost Update가 발생하는 상황</h3>
<p>현재 재고가 10개인 상품이 있고, 사용자 A와 B가 동시에 이 상품을 1개씩 사려고 할 경우를 생각해 보자.</p>
<h4 id="단계별-흐름">단계별 흐름</h4>
<p>사용자 A (T1 시작): 재고 조회 -&gt; 10개 출력</p>
<p>사용자 B (T2 시작): 재고 조회 -&gt; 10개 출력 (둘 다 10개라고 믿게 됨)</p>
<p>사용자 A: 1개를 샀으니 10 - 1 = 9로 계산하고, 재고를 9로 업데이트 후 커밋</p>
<p>사용자 B: 1개를 샀으니 10 - 1 = 9로 계산하고, 재고를 9로 업데이트 후 커밋</p>
<h4 id="결과">결과</h4>
<p>두 사람이 각각 1개씩 샀으므로 재고는 8개가 되어야 하지만, 최종 데이터베이스에는 9개가 남게 된다. 사용자 A가 수정한 내용이 사용자 B의 덮어쓰기에 의해 <strong>분실(Lost)</strong>된 것이다.</p>
<h3 id="왜-격리-수준은-이걸-못-막는가">왜 격리 수준은 이걸 못 막는가?</h3>
<p>DB는 보통 업데이트를 할 때 현재 시점의 실제 값을 기준으로 처리하는 게 아니라, 어플리케이션이 계산해서 보낸 값(SET stock = 9)을 그대로 쓰게 된다.
REPEATABLE READ의 경우 &quot;트랜잭션을 시작했을 때 본 데이터를 끝까지 유지&quot;하므로, 사용자 B는 트랜잭션 내내 재고를 10으로 보게 되며, 따라서 10-1=9로 계산하여 업데이트하게 된다.
READ COMMITTED의 경우 &quot;커밋된 데이터만 읽기&quot;이기 때문에, 위 상황에서 사용자 B가 만약 업데이트를 날리기 직전에 다시 한번 조회를 했다면, A가 커밋한 직후이므로 9를 보게 되며, &quot;어? 바뀌었네?&quot; 하고 8로 다시 계산할 기회라도 생기게 된다. 하지만 보통 다른 사람이 동시에 업데이트를 하고 있는지 알 방법이 없으므로 곧바로 업데이트를 날리게 되고, 그대로 Lost Update가 발생하게 된다.</p>
<h2 id="락-lock">락 (Lock)</h2>
<h3 id="락이란">락이란?</h3>
<ul>
<li>&quot;내 작업이 남의 작업을 덮어쓰느냐 마느냐&quot;에 초점을 맞춘 <strong>쓰기 일관성</strong> 전략</li>
<li>동일한 데이터의 동시 수정을 막기 위한 전략</li>
<li>애플리케이션 또는 ORM 레벨에서 선택 (실제로 데이터베이스의 기능을 이용해 잠금을 거는 것이 아니라, 애플리케이션 레벨에서 논리적으로 관리하는 방식)</li>
</ul>
<h3 id="비관적-락-pessimistic-lock">비관적 락 (Pessimistic Lock)</h3>
<blockquote>
<p>&quot;데이터는 언제든 수정될 수 있어! 일단 문부터 잠그자.&quot;</p>
</blockquote>
<p>충돌이 발생할 것이라고 미리 가정하고, 데이터를 읽을 때부터 아예 <strong>잠금(Lock)</strong>을 걸어버리는 방식으로, 내가 데이터를 다 쓸 때까지 아무도 손대지 못하게 하는 것이다. (SERIALIZE와 비슷한 효과)</p>
<h4 id="특징">특징</h4>
<p>방법: 데이터베이스의 <code>SELECT FOR UPDATE</code> 구문 등을 사용하여 로우(Row)에 락을 검</p>
<p>장점: 데이터 정합성이 완벽하게 보장되므로, 충돌이 잦은 환경에서 안전함</p>
<p>단점: 다른 사용자가 대기해야 하므로 성능(처리량)이 떨어질 수 있고, 서로가 서로의 자원을 기다리는 데드락(Deadlock) 상태에 빠질 위험이 있음</p>
<h3 id="낙관적-락-optimistic-lock">낙관적 락 (Optimistic Lock)</h3>
<blockquote>
<p>&quot;대부분의 경우 별 일 없을 거야. 만약 충돌하면 그때 해결하자!&quot;</p>
</blockquote>
<p>낙관적 락은 실제로 데이터베이스의 기능을 이용해 잠금을 거는 것이 아니라, 어플리케이션 레벨에서 논리적으로 관리하는 방식이다. 충돌이 거의 없을 것이라고 가정해서 일단 수정하고, 마지막에 저장할 때 &quot;내가 처음에 봤던 그 데이터가 맞나?&quot;를 확인한다.</p>
<h4 id="특징-1">특징</h4>
<p>방법: 데이터에 version 컬럼을 추가하여 관리</p>
<ol>
<li>데이터를 읽을 때 version이 1인 것을 확인</li>
<li>수정 후 저장할 때 WHERE version = 1 조건으로 업데이트</li>
<li>그 사이 누군가 수정해서 version이 2가 되었다면 업데이트 실패</li>
</ol>
<p>장점: 실제로 락을 걸지 않으므로 비관적 락보다 성능이 좋음</p>
<p>단점: 충돌이 빈번하게 발생하면 계속해서 재시도(Retry) 로직을 수행해야 하므로 오히려 성능이 저하될 수 있음</p>
<h2 id="핵심--왜-이번에는-낙관적-락을-선택했는가">핵심 : 왜 이번에는 낙관적 락을 선택했는가?</h2>
<p>보통 결제 기능의 경우 데이터 정합성이 매우 중요하므로 비관적 락을 선택한다고 알려져 있고, 그렇게만 알고 이번 포인트 시스템 TRD를 작성할 때 비관적 락을 적용하도록 하였다.
하지만, 지금 상황과 맥락에서는 비관적 락이 적합하지 않은 부분들이 있었다.</p>
<p><strong>1. Prisma의 경우 FOR UPDATE를 지원하지 않으며, 락 순서가 꼬여 데드락이 발생하는 경우도 감지해주지 않는다</strong>
: Prisma의 철학은 &quot;DB에 의존적인 기능보다는 어플리케이션 레벨에서 안전하게 처리하는 것&quot;에 가깝다고 한다. 만약 굳이 비관적 락을 적용하고자 한다면, 다음과 같이 복잡하게 작성해야 한다.</p>
<pre><code class="language-ts">const result = await prisma.$transaction(async (tx) =&gt; {
  // 1. Raw SQL로 특정 로우에 락을 겁니다.
  const [product] = await tx.$queryRaw&lt;Product[]&gt;`
    SELECT * FROM &quot;Product&quot; WHERE id = ${id} FOR UPDATE
  `;

  // 2. 이후 비즈니스 로직 수행 (여기서 다른 트랜잭션은 해당 로우 접근 불가)
  if (product.stock &gt; 0) {
    await tx.product.update({
      where: { id },
      data: { stock: product.stock - 1 }
    });
  }
});</code></pre>
<p><strong>2. Supabase를 사용하는 Serverless 환경에서는 Lock으로 잠기는 상황이 많아질 경우 connection pool 고갈 위험을 생각해야 한다</strong>
: 글이 너무 길어져 connection pool 관련 글은 따로 작성할 예정..</p>
<p><strong>3. 결정적으로, &quot;동시 수정이 이루어지는 경우가 있는가?&quot;를 생각해봐야 한다</strong>
: 기술적인 내용보다 중요한 것은, 지금 적용하고자 하는 기능의 맥락을 생각해야 한다는 것이었다. 
지금 MVP 단계에서 구현하고자 하는 기능은 &quot;사용자가 계좌이체를 통해 금액을 입금하면, 운영자가 수동으로 포인트를 충전한다&quot;이므로, 비관적 락을 적용할 때 얻는 이점보다 성능 손해가 더 크다는 것이 명백했다.</p>
<h2 id="결론">결론</h2>
<ul>
<li>특정 기술을 적용할 때의 이유는, 철저히 &quot;지금 만들고 있는 프로젝트&quot;의 맥락에서 생각해야 한다. &quot;일반적으로 <del>한 상황에서 좋기 때문에&quot;가 아니라, &quot;지금 이 프로젝트는 ~</del>한 상황이고, 이럴 때 좋기 때문에&quot;라고 말할 수 있어야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[오픈소스 기여] Supabase 대시보드 크래시 원인 분석 및 해결 (Zod safeParse 도입)]]></title>
            <link>https://velog.io/@ooleem-generator/%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC-Supabase-Issue-41698-%ED%95%B4%EA%B2%B0-%EA%B3%BC%EC%A0%95-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@ooleem-generator/%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC-Supabase-Issue-41698-%ED%95%B4%EA%B2%B0-%EA%B3%BC%EC%A0%95-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Sat, 17 Jan 2026 10:42:49 GMT</pubDate>
            <description><![CDATA[<h2 id="오픈소스-기여모임-10기">오픈소스 기여모임 10기</h2>
<p>작년 11월 K-DEVCON 2025 밋업에서 오픈소스 기여모임의 존재를 처음 알게 되었고, 10기 모집 소식을 보자마자 바로 참여하기로 결정했다. 개발자들이 보고한 다양한 문제들을 같이 고민하고 해결하는 과정이야말로 개발자로서의 문제 해결력을 기르는 것과 동시에, 오픈소스 생태계에 기여함으로써 개발자로서 세상에 흔적을 남길 수 있는 좋은 활동이라고 생각했다.</p>
<h2 id="오픈소스-이슈-선정">오픈소스, 이슈 선정</h2>
<p>다음과 같은 기준으로 오픈소스와 이슈를 선정했다.</p>
<ol>
<li>내가 사용해 봤거나, 활발히 사용할 예정인 오픈소스
: 간단해 보이는 오픈소스도 막상 소스 코드를 보면 만만치 않다. 적어도 어떤 용도인지, 어떻게 사용하는지 정도는 알고 있어야 기여하기 수월할 것이라 생각했다.</li>
<li>이슈에 대해 메인테이너의 응답이 빠르고, PR merge가 활발히 이루어지는 오픈소스
: 사실 FastAPI를 제일 먼저 찾아봤지만, FastAPI의 경우 거의 대부분 메인테이너가 올린 PR만 merge되고 있었다. 몇 가지 더 찾아본 끝에, Supabase로 결정했다.</li>
<li>단순 문서 편집보다는 직접적으로 코드를 수정해서 해결해야 하는 이슈
: &quot;오픈소스에 기여했다&quot;라는 스펙만 얻고 싶다면 문서 수정이 제일 빠르겠지만, 첫 기여부터 문서 수정으로 남기고 싶지는 않았다. 조금이라도 코드를 수정해서 내 코드가 해당 오픈소스에 남고, 모든 사용자들이 겪고 있던 문제를 해결해주는 보람을 느끼고 싶었다. </li>
</ol>
<h3 id="supabase-issue-41698-문제-상황"><a href="https://github.com/supabase/supabase/issues/41698">Supabase Issue #41698</a> 문제 상황</h3>
<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/1d1faa12-a718-4617-acba-75d717832111/image.png" alt=""></p>
<p>로컬 개발 환경에서 Supabase Studio의 Authentication 페이지에 접근하면, 다음과 같은 에러 메세지가 표시된다.
<img src="https://velog.velcdn.com/images/ooleem-generator/post/1abd4392-3eee-42ba-9469-edb1b3b0ab83/image.png" alt=""></p>
<h2 id="pr-생성까지의-과정">PR 생성까지의 과정</h2>
<h3 id="이슈-검증-에러-재현">이슈 검증 (에러 재현)</h3>
<p>예상대로 Supabase는 정말 거대한 프로젝트였다. 공식 문서에서 제공하는 아키텍처 구조도는 다음과 같다.
<img src="https://velog.velcdn.com/images/ooleem-generator/post/5d1258ef-18eb-418e-93e5-57183da2b7a3/image.png" alt=""></p>
<p>로컬 환경 설정하고, 무사히 돌아가게 만드는 데만 한 세월이 걸렸다. 그 와중에 동시에 돌려야 하는 도커 이미지가 하도 많다 보니 맥북이 버티질 못하고 뻗어버리기 일쑤였다.. 
기여 가이드를 꼼꼼히 읽어야 했다. 공식 문서를 읽는 능력이 얼마나 중요한지 새삼 깨달았다.
한참 고생한 끝에 동일한 에러를 확인했고, 로그에 어떻게 출력되는지도 확인할 수 있었다.
<img src="https://velog.velcdn.com/images/ooleem-generator/post/d0a1289d-ffc0-455f-ac1f-3b5de9cb03a4/image.png" alt="">
<img src="https://velog.velcdn.com/images/ooleem-generator/post/89be6331-2e0b-490c-ba9b-3679fcf09afd/image.png" alt=""></p>
<p>최대한 스스로 원인을 찾고 싶었지만, 에러 메세지가 암시하는 힌트가 너무 적었다. 에러 메세지를 긁어다가 검색해봐도 도저히 원인을 알 수 없었다. 어쩔 수 없이 이번에는 클로드의 도움을 받았다.</p>
<h3 id="원인-발견">원인 발견</h3>
<p><strong>Docker 디렉터리에 있는 .env에는 PG_META_CRYPTO_KEY라는 환경변수가 있는데, app/studio 디렉터리에 있는 .env에는 해당 키가 존재하지 않았다.</strong></p>
<p>studio에서 pg-meta (Postgres) 쪽으로 쿼리 요청을 보낼 때, 다음과 같이 DB 연결 정보
<code>postgresql://${postgresUser}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DATABASE}</code>를 암호화한다.</p>
<pre><code class="language-ts">export function encryptString(stringToEncrypt: string): string {
  return crypto.AES.encrypt(stringToEncrypt, ENCRYPTION_KEY).toString()
}</code></pre>
<p>그리고 암호화된 connectionStringEncrypted를 x-connection-encrypted 헤더로 보낸다.</p>
<pre><code class="language-ts">const response = await fetch(`${PG_META_URL}/query`, {
  method: &#39;POST&#39;,
  headers: constructHeaders({
    ...headers,
    &#39;Content-Type&#39;: &#39;application/json&#39;,
    &#39;x-connection-encrypted&#39;: connectionStringEncrypted,
  }),
  body: JSON.stringify(requestBody),
})</code></pre>
<p>constants.ts에 이 때 사용되는 암호화 키 <strong>ENCRYPTION_KEY = .env에 있는 PG_META_CRYPTO_KEY</strong>로 정의되고, 없을 경우 다른 기본값을 사용하도록 정의되어 있었다.</p>
<pre><code>export const ENCRYPTION_KEY = process.env.PG_META_CRYPTO_KEY || &#39;엉뚱한 값&#39;</code></pre><p>문제는 해당 값이 app/studio/.env에 없었고, 뒤에 &#39;엉뚱한 값&#39;이 docker 쪽에 있는 키 값과 달랐다.
따라서 pg-meta 서버에서 DB 연결 정보를 복호화하는 데 실패하고, <code>{&quot;message&quot;: &quot;failed to get connection string&quot;}</code>이라는 에러 응답을 반환하게 된다.
그런데, studio가 데이터베이스 에러를 받았으니 databaseErrorSchema라는 zod 스키마를 통해 적절하게 파싱해야 하는데, 스키마에 이런 유형의 에러가 정의되지 않았다.</p>
<pre><code class="language-ts">export const databaseErrorSchema = z.object({
  message: z.string(),
  code: z.string(),        // ← pg-meta 에러에 없음
  formattedError: z.string(), // ← pg-meta 에러에 없음
})</code></pre>
<p>따라서 이 databaseErrorSchema에서 string을 기대하는데, code와 formattedError 없이 message만 딸랑 왔으니(undefined), string을 기대했는데 undefined를 받았다는 오류가 뜨게 된 것이다. </p>
<h3 id="해결-방향">해결 방향</h3>
<p>당연히 개발자 문서에 이와 관련된 언급이 있는지 두번 세번 체크했고, 정말로 누락된 것이 맞다는 걸 확실히 했다.
일단 studio 쪽 .env에 PG_META_CRYPTO_KEY를 docker와 동일한 값으로 넣었다.
이것만으로도 일단 당장 500 에러가 뜨는 건 막을 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/9d5fd7bb-3692-473f-b3f0-8f4809c472fe/image.png" alt="">
<img src="https://velog.velcdn.com/images/ooleem-generator/post/683cb28e-3387-484e-8a5e-d600c719f870/image.png" alt=""></p>
<p>하지만 이것만으로는 부족했다. zod 검증 실패 메시지가 studio 화면에 출력되는 건 바람직하지 않기 때문에, 적절하게 코드를 수정해야 했다.
처음 클로드가 제시한 해결책은 암호화 관련 코드가 있는 util.ts에 따로 EncryptionKeyError라는 에러를 새로 정의하고, ENCRYPTION_KEY가 비어 있거나 &#39;엉뚱한 값&#39;일 때 에러를 미리 내뱉도록 하는 것이었다.</p>
<p>그런데, 아무리 생각해 봐도 그냥 constant.ts에 있는 &#39;엉뚱한 값&#39;이라는 이 기본값만 docker에 있는 제대로 된 값으로 수정하면 해결될 문제인 것 같았다.
어차피 .env에 PG_META_CRYPTO_KEY 항목은 추가해 줄 것이고, 나중에 prod 환경일 때 .env에서 값을 변경하면 그 값으로 반영될 것이기 때문이다. (docker에 있는 값도 예시로 들어간 placeholder였다)</p>
<p>일반적으로도 그렇지만, 특히 오픈소스 코드는 영향을 받는 사람이 많기 때문에 불필요한 수정은 지양해야 한다. 그냥 기본값을 docker 쪽과 일치시켜서 해결했다.</p>
<p>대신 만약 실수로 키가 양쪽이 달라질 경우를 대비해서, zod 검증이 실패하면 500 에러로 서버가 터져버리는 parse() 대신 safeParse()를 쓰도록 query.ts를 변경했다.</p>
<pre><code class="language-ts">if (!response.ok) {
      // Use safeParse to avoid throwing on schema mismatch
      const parsed = databaseErrorSchema.safeParse(result)

      if (parsed.success) {
        const { message, code, formattedError } = parsed.data
        const error = new PgMetaDatabaseError(message, code, response.status, formattedError)
        return { data: undefined, error }
      }

      // Flexibly extract error message when schema doesn&#39;t match (e.g., encryption key issues)
      const message =
        result?.message ?? result?.msg ?? result?.error ?? &#39;An unexpected error occurred&#39;
      const code = result?.code ?? &#39;UNKNOWN_ERROR&#39;
      const formattedError = result?.formattedError ?? message

      const error = new PgMetaDatabaseError(
        String(message),
        String(code),
        response.status,
        String(formattedError)
      )
      return { data: undefined, error }
    }</code></pre>
<p>이렇게 조치하면, 키가 일치하지 않을 때 zod 에러 대신 다음과 같이 보다 분명한 에러 메세지가 출력된다.
<img src="https://velog.velcdn.com/images/ooleem-generator/post/05440757-f3ed-475c-83a6-a81dd993e4ca/image.png" alt=""></p>
<p><strong>API 응답과 같은 외부 서비스 응답을 검증할 때는 parse() 대신 safeParse()를 써야 서버가 터지지 않는다는 걸 확실히 알게 되었다.</strong></p>
<h3 id="pr-생성"><a href="https://github.com/supabase/supabase/pull/41980#ref-pullrequest-3828237227">PR 생성</a></h3>
<p>PR을 생성하기 전 관련 가이드라인을 두번 세번 거듭 확인한 다음, 마침내 첫 번째 오픈소스 기여를 담은 PR을 요청했다. (pnpm format으로 linting하는 것도 잊지 않았다)
<img src="https://velog.velcdn.com/images/ooleem-generator/post/252ba088-247b-44a2-8128-9c432f59d522/image.png" alt=""></p>
<p>이제 기다림만 남았다.. merge되면 업데이트할 예정!</p>
<h2 id="느낀-점">느낀 점</h2>
<ul>
<li>좋은 의미로나 나쁜 의미로나, 오랜만에 핀토스를 다시 하는 기분이었다.
문서를 아주 꼼꼼히 읽어야 하고, 별개로 이 거대한 구조가 왜 이렇게 되어 있는지, 각각이 어떤 역할을 하는지를 빠르게 파악하는 능력이 딱 핀토스 때 요구되던 것과 똑같았다.
개인적으로는 핀토스를 굉장히 재미있게 했기 때문에, 이번에도 문제의 원인이 뭘까 고민하고 머리를 싸맸지만 즐겁게 진행했다. 이번에는 클로드의 도움을 받았지만, 다음에 또 오픈소스 기여에 도전한다면 그때는 꼭 내가 스스로 원인까지 찾아내고 싶다.</li>
<li>문제의 근본적인 원인은 AI가 찾아내더라도, 어떤 방향으로 해결할지 고민하는 건 여전히 개발자의 몫이다. 문제에 대한 해결책이 여러 방향으로 나올 수 있고, 상황과 맥락을 종합적으로 고려하여 판단해야 한다.
특히 요즘 오픈소스 기여라는 타이틀만 보고 AI 돌려서 검증도 안 해보고 PR을 찍어내는 행태가 심각한 문제라고 한다. 자기 자신을 위해서도, 오픈소스 생태계를 위해서도 절대 하지 말아야 할 행동이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Deep Quest 개발노트] 2026.01.04 TRD 리뷰 기록]]></title>
            <link>https://velog.io/@ooleem-generator/Deep-Quest-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-2026.01.04-TRD-%EB%A6%AC%EB%B7%B0-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@ooleem-generator/Deep-Quest-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-2026.01.04-TRD-%EB%A6%AC%EB%B7%B0-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Sun, 04 Jan 2026 08:53:05 GMT</pubDate>
            <description><![CDATA[<p>정글에서 모의면접 시스템을 만들었던 경험이 계기가 되어, 정글 선배님의 프로젝트에 공동 개발자로 참여하게 되었다.</p>
<p><a href="https://www.deepquest.app/">https://www.deepquest.app/</a></p>
<p>&quot;Deep Quest&quot;라는 기술면접 대비 서비스이고, 유료화와 연동된 포인트 시스템 도입부터 참여하게 되었다.
작성되어 있는 포인트 시스템 PRD를 바탕으로 TRD를 처음으로 직접 작성해봤고(물론 클로드가), 나름 꼼꼼히 검토한 다음에 제출했다고 생각했지만 피드백 받은 부분이 상당히 많았다.</p>
<p>TRD 작성, 리뷰 한 번 왔다갔다 한 것만으로도 엄청나게 배우는 부분들이 많다. 기록해본다.</p>
<h2 id="trd-초안-작성검토">TRD 초안 작성/검토</h2>
<h3 id="검토해서-수정한-내용">검토해서 수정한 내용</h3>
<ul>
<li>클로드가 먼저 작성한 내용을 나름 꼼꼼히 검토했다.</li>
<li>PRD에는 중복 클릭 방지를 위한 멱등성 키가 <strong>반드시 필요하다</strong>고 언급되어 있었는데, TRD 초안에는 선택사항인 것처럼 기재되어 있었다. 이 부분은 바로 수정 요청했다.</li>
<li>또한 초안에서는 멱등성 키를 클라이언트에서 생성한 다음에 서버로 보내는 방식으로 구현되어 있었는데, 이렇게 되면 클라이언트에서 위변조 가능성이 있기 때문에 서버에서 생성하도록 수정했다.</li>
<li>일단 이 정도까지만 짚어낼 수 있었고, 수정해서 제출했다.</li>
</ul>
<h3 id="작성하면서-알게-된-내용">작성하면서 알게 된 내용</h3>
<h4 id="프론트엔드-캐싱-전략">프론트엔드 캐싱 전략</h4>
<ul>
<li><p>유저의 현재 포인트 잔액, 또는 거래 내역을 불러오는 API의 경우 시간을 정해 두고(staleTime) React Query로 캐싱할 수 있으며, 트랜젝션이 발생하면 자동으로 해당 캐시에 대해 invalidate하도록 할 수 있다.</p>
<h4 id="not-in-서브쿼리-문제">NOT IN (서브쿼리) 문제</h4>
</li>
<li><p>다음 마이그레이션 코드에서,</p>
<pre><code class="language-sql">INSERT INTO user_points (user_id, balance, created_at, updated_at)
SELECT id, 0, NOW(), NOW()
FROM users
WHERE id NOT IN (SELECT user_id FROM user_points);</code></pre>
</li>
<li><p>서브쿼리 결과에 NULL이 하나라도 있으면, 연산 결과가 UNKNOWN이 되어 메인 쿼리에서 데이터가 반환되지 않는다</p>
</li>
<li><p>또한 서브쿼리 결과를 메모리에 전부 로드하므로 성능도 좋지 않다</p>
</li>
<li><p>NOT EXIST, 또는 LEFT JOIN + IS NULL을 대신 사용하는 게 좋다</p>
<pre><code class="language-sql">INSERT INTO user_points (user_id, balance, created_at, updated_at)
SELECT u.id, 0, NOW(), NOW()
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM user_points up
WHERE up.user_id = u.id
);</code></pre>
<p>또는 PostgreSQL의 경우 UNIQUE 제약이 있는 경우에 다음과 같이 사용할 수도 있다.</p>
<pre><code class="language-sql">INSERT INTO user_points (user_id, balance, created_at, updated_at)
SELECT id, 0, NOW(), NOW()
FROM users
ON CONFLICT (user_id) DO NOTHING;
</code></pre>
</li>
</ul>
<pre><code>
참고 : https://velog.io/@hskhyl/NOT-IN-NOT-EXISTS-LEFT-JOIN-IS-NULL-%EC%84%B1%EB%8A%A5%EB%B9%84%EA%B5%90

## 리뷰 받은 부분
### 피드백 받은 내용
#### 멱등성 키 생성 규칙
- 실제 서비스 흐름에 따라서 멱등성 키가 겹칠 수 있으므로, 생성 규칙을 만들 땐 꼭 흐름을 같이 생각해야 한다
: 이 부분은 내가 안이했다.. 좀 더 꼼꼼히 확인했어야 했다.

#### 상황과 맥락을 고려한 Lock 전략 선택
- Prisma의 경우 FOR UPDATE를 지원하지 않는다
: Pessimistic Lock을 선택할 경우 Raw SQL문을 사용해야 한다
- Supabase를 사용하는 Serverless 환경에서는 Lock으로 잠기는 상황이 많아질 경우 connection pool 고갈 위험을 생각해야 한다
: 데이터베이스 아키텍쳐, 티어마다 connection pool 최대 제한이 달라질 수 있음을 염두해 둬야 한다
- 결제/포인트 시스템에서는 일반적으로 보수적인 접근을 해야 하므로 Pessimistic Lock을 선택하라는 조언이 많긴 하다
: 하지만 우리는 MVP 단계이기도 하고, 특히 지금 상황에서는 **특정 유저의 포인트를 여러 사람이 접근해서 차감/증가시킬 일이 없다**는 걸 생각했어야 한다
: 따라서 지금 상황에는 Optimistic Lock이 더 적절했다

#### 서버에서 결정할 수 있는 값은 절대 클라이언트에서 받지 말 것
- 서비스 요청 시 요청 금액 cost를 클라이언트에서 받도록 구현되어 있었는데, 이 경우에도 멱등성 키와 마찬가지로 위변조의 위험이 있다. **클라이언트를 믿지 마라!**
- 서비스 금액 정보는 서버의 환경 변수로 관리하는 것이 좋다


### 새로 공부하게 된 내용
#### Optimistic Lock vs. Pessimistic Lock

#### Saga Pattern

#### Connection Pool

#### Feature Flag (Phased Rollout, Canary Deployment)

분량이 하나같이 많다.. 각각 따로 글을 써볼 생각이다</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[고찰] NestJS vs. FastAPI : 싱글톤 패턴으로 작성된 클래스의 export]]></title>
            <link>https://velog.io/@ooleem-generator/%EA%B3%A0%EC%B0%B0-NestJS-vs.-FastAPI-%EC%8B%B1%EA%B8%80%ED%86%A4-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EC%9E%91%EC%84%B1%EB%90%9C-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%9D%98-export</link>
            <guid>https://velog.io/@ooleem-generator/%EA%B3%A0%EC%B0%B0-NestJS-vs.-FastAPI-%EC%8B%B1%EA%B8%80%ED%86%A4-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EC%9E%91%EC%84%B1%EB%90%9C-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%9D%98-export</guid>
            <pubDate>Thu, 11 Dec 2025 07:15:33 GMT</pubDate>
            <description><![CDATA[<p>요즘 크래프톤 정글 최종 프로젝트에서 내가 개발했던 AI 모의면접 기능만 배포하려고 다시 개발중인데, 백엔드를 NestJS에서 FastAPI로 변경해서 개발하는 과정에서 얻은 지식을 적어본다.</p>
<h3 id="궁금증을-느끼게-된-계기">궁금증을 느끼게 된 계기</h3>
<ul>
<li>FastAPI에서 환경변수 설정을 담당하는 config.py는 보통 다음과 같이 작성한다.<pre><code class="language-py"># config.py
from pydantic_settings import BaseSettings
</code></pre>
</li>
</ul>
<p>class Settings(BaseSettings):
    DB_HOST: str
    DB_PORT: int = 5432
    SECRET_KEY: str</p>
<pre><code>class Config:
    env_file = &quot;.env&quot;</code></pre><p>settings = Settings()</p>
<pre><code>
그리고 이제 다른 파일, 예를 들면 app.py에서
```py
from fastapi import FastAPI
from config import settings

app = FastAPI()

@app.get(&quot;/debug&quot;)
def debug():
    return {
        &quot;db_host&quot;: settings.DB_HOST,
        &quot;secret&quot;: settings.SECRET_KEY
    }</code></pre><p>이렇게 가져와서 사용하게 된다.
그런데 여기서, import &#39;settings&#39; 대신 import &#39;Settings&#39;를 쓰면 안 되나 하는 생각이 갑자기 들었다.
결국, <code>settings = Settings()</code> 이 부분이 왜 필요한지 궁금했다.</p>
<h3 id="클래스를-직접-가져오면-안-되는-이유">클래스를 직접 가져오면 안 되는 이유</h3>
<p><code>Settings</code>를 쓸 경우 그 줄이 읽힐 때마다 Settings 객체를 다시 생성하면서, 환경변수 전체를 다시 로딩한다. 앱 내에의 여러 곳에서 Settings()를 호출하는 것은 당연히 매우 비효율적이고, 불필요한 오버헤드가 발생한다.
그래서 Settings 클래스의 인스턴스로써 <code>settings</code>를 딱 한 번만 생성한 뒤(<code>settings = Settings()</code>), 그 인스턴스를 다른 곳에서 import하여 사용하게 된다.
그러면 앱 전체에서 전역적으로 단일 settings 인스턴스를 유지한다. 이것이 싱글턴을 쓰는 이유이기도 하다.</p>
<h3 id="어-그런데-예전에-nestjs로-개발했을-때는">어? 그런데 예전에 NestJS로 개발했을 때는..</h3>
<p>&#39;저런 과정 없었던 거 같은데..? 인스턴스를 굳이 만들 필요가 없지 않았나..?&#39;
분명 그 때는 export class <del>~</del> 라고 정의만 해 놓고 다른 곳에서 import했던 걸로 기억했다.
내 기억이 확실한지, 그리고 맞다면 NestJS와 FastAPI에 무슨 차이가 있는지 궁금했다.</p>
<h3 id="nestjs에서는-내부-di-컨테이너가-인스턴스를-관리해준다">NestJS에서는 내부 DI 컨테이너가 인스턴스를 관리해준다</h3>
<p>NestJS에서는 보통 다음과 같이 싱글톤 패턴을 작성한다.</p>
<pre><code class="language-ts">// users.service.ts
@Injectable()
export class UsersService {
  // ...
}</code></pre>
<p>그리고 다른 곳에서 다음과 같이 주입하여 사용하게 된다.</p>
<pre><code class="language-ts">// users.controller.ts
@Controller(&#39;users&#39;)
export class UsersController {
  constructor(
    private readonly usersService: UsersService,  // 👈 여기
  ) {}
}</code></pre>
<p>그냥 클래스를 정의하고, 다른 파일에서 import해서 써먹기만 하면 됐던 것이다.
<code>UsersService = new UsersService()</code> 이런 작업 없이 말이다.</p>
<p>이 작업을 NestJS가 대신 해 준다고 생각하면 된다.
NestJS가 내부 DI 컨테이너에서 UsersService를 한 번만 생성하고, 그 인스턴스를 각각의 Controller/다른 Service에 주입해 주는 것이다.</p>
<p>(FastAPI의 DI는 클래스 기반이 아니라 함수 기반이라고 한다(<code>Depends()</code>). 이건 나중에 더 자세히 알아봐야겠다.)</p>
<h3 id="fastapi에서-유사하게-흉내내는-방법">FastAPI에서 유사하게 흉내내는 방법</h3>
<p>lru_cache를 이용해서, 처음 호출된 이후로는 캐시된 같은 인스턴스를 리턴하는 방법이 있다.</p>
<pre><code class="language-py">from functools import lru_cache
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    DB_HOST: str
    DB_PORT: int = 5432

    class Config:
        env_file = &quot;.env&quot;

@lru_cache
def get_settings():
    # 이 함수가 처음 호출될 때만 Settings()를 만들고
    # 그 이후로는 캐시된 같은 인스턴스를 리턴
    return Settings()</code></pre>
<p>그리고 라우터 쪽에서는, 앞서 말했던 Depends를 이용하여 다음과 같이 사용한다.</p>
<pre><code class="language-py">from fastapi import Depends

@app.get(&quot;/items&quot;)
def read_items(settings: Settings = Depends(get_settings)):
    return {&quot;db_host&quot;: settings.DB_HOST}</code></pre>
<p>하지만 굳이 이러느니.. 그냥 인스턴스 하나 만드는 게 낫지 않을까 싶다.</p>
<h3 id="결론">결론</h3>
<p>사실 NestJS의 내부 DI 관리 컨테이너에 대해서는 예전에 들은 적이 있지만, 이번에 확실히 무슨 역할을 하는지 알게 되었다.
이런 식으로 다른 언어와 프레임워크로 마이그레이션하면서 배울 수 있는 것들이 많을 것 같다.
개발하면서 하나씩 이렇게 정리하다 보면 좋은 공부가 될 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[251211 코테연습 기록]]></title>
            <link>https://velog.io/@ooleem-generator/251211-%EC%BD%94%ED%85%8C%EC%97%B0%EC%8A%B5-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@ooleem-generator/251211-%EC%BD%94%ED%85%8C%EC%97%B0%EC%8A%B5-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Thu, 11 Dec 2025 05:02:27 GMT</pubDate>
            <description><![CDATA[<h2 id="유기농-배추-백준">유기농 배추 (백준)</h2>
<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/1cd96029-384f-462e-bca0-99d315077d66/image.png" alt=""></p>
<ul>
<li>문제 유형 : DFS 또는 BFS</li>
<li>BFS로 일단 해봤으나 시간초과</li>
<li>항상 DFS/BFS 맵 탐색 유형은 더 효율적인 풀이가 없을지 생각해야 한다..
(하지만 사실 이 문제는 효율의 문제가 아니었음)</li>
</ul>
<h3 id="제출-코드-시간초과">제출 코드 (시간초과)</h3>
<pre><code class="language-py">import sys
input = sys.stdin.readline

from collections import deque

direction = [(1, 0), (-1, 0), (0, 1), (0, -1)]

t = int(input())
for _ in range(t):
    m, n, k = map(int, input().split())  # m -&gt; c, n -&gt; r
    cabbage = [[0] * m for _ in range(n)]

    for _ in range(k):
        a, b = map(int, input().split())
        cabbage[b][a] = 1

    count = 0
    queue = deque()
    for i in range(n):
        for j in range(m):
            if cabbage[i][j] == 1:
                queue.append((i, j))
                while queue:
                    r, c = queue.popleft()
                    cabbage[r][c] = 0
                    for dr, dc in direction:
                        if 0 &lt;= r + dr &lt; n and 0 &lt;= c + dc &lt; m:
                            if cabbage[r + dr][c + dc] == 1:
                                queue.append((r + dr, c + dc))
                count += 1
    print(count)</code></pre>
<p>모든 좌표를 일단 체크하는 부분이 문제일 것 같아서, 미리 배추 좌표를 리스트에 모아뒀다가 그 좌표들만 순회하면 어떨까 하고 수정했지만, 여전히 시간초과</p>
<p>결국 원인은.. 큐에 일단 넣고 뺄 때 방문처리(0으로 만들기)한 게 문제였다
고질적인 실수 유형 하나 찾았다</p>
<h3 id="정답-코드-bfs-해당-부분만-수정">정답 코드 (BFS, 해당 부분만 수정)</h3>
<pre><code class="language-py">    for i, j in cabbage:
        # for j in range(m):
        if cabbage_map[i][j] == 1:
            queue.append((i, j))
            cabbage_map[i][j] = 0
            while queue:
                r, c = queue.popleft()
                #cabbage_map[r][c] = 0
                for dr, dc in direction:
                    if 0 &lt;= r + dr &lt; n and 0 &lt;= c + dc &lt; m:
                        if cabbage_map[r + dr][c + dc] == 1:
                            queue.append((r + dr, c + dc))
                            cabbage_map[r+dr][c+dc] = 0
            count += 1
    print(count)</code></pre>
<p>이 문제는 DFS로도 다음과 같이 풀 수 있다. 다만 재귀 제한을 해제해야 한다.</p>
<h3 id="정답-코드-dfs">정답 코드 (DFS)</h3>
<pre><code class="language-py">import sys

input = sys.stdin.readline
sys.setrecursionlimit(10**6)

direction = [(1, 0), (-1, 0), (0, 1), (0, -1)]

t = int(input())
for _ in range(t):
    m, n, k = map(int, input().split())  # m -&gt; c, n -&gt; r
    cabbage_map = [[0] * m for _ in range(n)]
    cabbage = []

    for _ in range(k):
        a, b = map(int, input().split())
        cabbage_map[b][a] = 1
        cabbage.append((b, a))

    count = 0

    def dfs(r, c):
        if 0 &lt;= r &lt; n and 0 &lt;= c &lt; m:
            if cabbage_map[r][c] == 0:
                return

            cabbage_map[r][c] = 0

            for dr, dc in direction:
                dfs(r + dr, c + dc)

        else:
            return

    for i, j in cabbage:
        if cabbage_map[i][j] == 1:
            dfs(i, j)
            count += 1

    print(count)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[251127 코테연습 기록]]></title>
            <link>https://velog.io/@ooleem-generator/251127-%EC%BD%94%ED%85%8C%EC%97%B0%EC%8A%B5-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@ooleem-generator/251127-%EC%BD%94%ED%85%8C%EC%97%B0%EC%8A%B5-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Thu, 27 Nov 2025 04:31:07 GMT</pubDate>
            <description><![CDATA[<h2 id="길-찾기-게임-프로그래머스">길 찾기 게임 (프로그래머스)</h2>
<ul>
<li>어제 못풀었던 문제</li>
<li>클래스로 노드 구현하는 법은 알았으나, 그거만 가지고 안 됨</li>
<li>노드 클래스는 만들었는데, 그래서 그걸 어떻게 써먹을지 바로 구상이 안 됨</li>
<li>유사한 문제를 몇 번 풀어보고 다시 도전해야할듯</li>
</ul>
<h2 id="가장-긴-팰린드롬-프로그래머스">가장 긴 팰린드롬 (프로그래머스)</h2>
<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/4a40e53d-761c-4ac4-b200-f2d424d3836a/image.png" alt=""></p>
<ul>
<li>제한 시간 : 30분</li>
<li>접근 방식 : DP</li>
<li>풀이 성공.. 하지만 비효율적</li>
</ul>
<h3 id="제출-코드">제출 코드</h3>
<pre><code class="language-py">def solution(s):
    dp = [0]*len(s)
    string = &quot;&quot;
    for i in range(len(s)):
        string += s[i]
        for j in range(len(string)):
            target = string[j:]
            #print(string[j::-1])
            if target == target[::-1]:
                dp[j] = max(dp[j], len(target))

    #print(dp)

    return max(dp)
</code></pre>
<ul>
<li>초반에 좀 당황하긴 했는데, DP로 풀렸다 (각 글자마다 그 글자부터 시작해서 문자열 끝까지를 뒤집어서 같은지 확인)</li>
<li>이번에는 방심하지 않고 테스트 케이스 몇 개 더 만들어서 코드를 수정할 수 있었다</li>
<li>슬라이싱한 문자열을 뒤집으려고 할 때 <code>string[j::-1]</code> 이런식으로 쓰면 이상해져서 따로 변수에다 할당하고 <code>target[::-1]</code> 이렇게 비교했다</li>
<li>다만 문자열 s의 길이가 2500까지인 거 보고 이렇게 풀었는데, 다행히 효율성 테스트는 통과했지만 2초 정도 걸린다</li>
<li>아무래도 찝찝해서 찾아보니까 이 방법은 거의 O(n^3)에 가까울 정도로 비효율적이다.. 투 포인터로 푸는 게 정석이었다</li>
<li>다음에 다시 한번 풀어봐야겠다</li>
</ul>
<h2 id="가장-먼-노드-프로그래머스">가장 먼 노드 (프로그래머스)</h2>
<ul>
<li>이미 풀었던 문제 (다익스트라)</li>
<li>다만 주의할 점 하나 발견</li>
</ul>
<pre><code class="language-py">def solution(n, edge):
    answer = 0
    graph = [[] for _ in range(n+1)]
    shortest = [10**15]*(n+1)
    for i,j in edge:
        graph[i].append(j)
        graph[j].append(i)

    shortest[0] = 0
    shortest[1] = 0

    queue = []
    heappush(queue, [0,1])
    while queue:
        accum_dist, node = heappop(queue)
        for next_node in graph[node]:
            if accum_dist + 1 &gt;= shortest[next_node]:
                continue
            shortest[next_node] = accum_dist + 1
            heappush(queue, [accum_dist+1, next_node])

    #print(shortest)
    #shortest.sort()

    return shortest.count(max(shortest))</code></pre>
<p>여기서 <code>accum_dist + 1 &gt;= shortest[next_node]:</code> 이 부분 등호 안넣어주면 일부 테스트케이스에서 queue가 비질 않아서 무한루프 돌게되는 현상 확인!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[251126 코테연습 기록]]></title>
            <link>https://velog.io/@ooleem-generator/251126-%EC%BD%94%ED%85%8C%EC%97%B0%EC%8A%B5-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@ooleem-generator/251126-%EC%BD%94%ED%85%8C%EC%97%B0%EC%8A%B5-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Wed, 26 Nov 2025 03:09:00 GMT</pubDate>
            <description><![CDATA[<h2 id="베스트앨범-프로그래머스">베스트앨범 (프로그래머스)</h2>
<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/a2894b83-687c-49bd-b071-e589b4c6ce07/image.png" alt=""></p>
<ul>
<li>문제 유형 : 해시</li>
<li>제한 시간 : 30분</li>
<li>아이디어 구상, 구현 완료</li>
<li>엣지케이스 걸러내지 못함 (제출 후 채점하기에서 걸림)</li>
</ul>
<h3 id="엣지케이스-걸러내지-못한-이유">엣지케이스 걸러내지 못한 이유</h3>
<ul>
<li>3번 조건 확인 안함</li>
<li>조건 확인하고 테스트케이스 추가해서 확인해보니까 <code>pop()</code>을 사용했기 때문에 내림차순으로 정렬해야 함</li>
</ul>
<h3 id="내-풀이-최종-수정">내 풀이 (최종 수정)</h3>
<pre><code class="language-py">def solution(genres, plays):
    #songdata = list(zip(plays, list(range(len(plays)))))
    total_play_count = {}
    songs_by_genre = {}

    data = list(zip(genres, plays, list(range(len(plays)))))
    for genre, play, idx in data:
        if not total_play_count.get(genre):
            total_play_count[genre] = play
            songs_by_genre[genre] = [(play, idx)]
        else:
            total_play_count[genre] += play
            songs_by_genre[genre].append((play, idx))

    #print(total_play_count)
    #print(songs_by_genre)
    answer = []
    total_play_count = list(total_play_count.items())
    total_play_count.sort(key=lambda x:-x[1])
    for total in total_play_count:
        song_list = songs_by_genre[total[0]]
        song_list.sort(key=lambda x:(x[0],-x[1]))
        for _ in range(2):
            if not song_list:
                break
            play, idx = song_list.pop()
            answer.append(idx)

    return answer</code></pre>
<ul>
<li>풀면서 든 생각 : 아 아이디어는 쉬운데 구현이 너무 복잡하네</li>
</ul>
<h3 id="다른-사람의-깔끔한-풀이">다른 사람의 깔끔한 풀이</h3>
<pre><code class="language-py">def solution(genres, plays):
    answer = []

    dic1 = {}
    dic2 = {}

    for i, (g, p) in enumerate(zip(genres, plays)):
        if g not in dic1:
            dic1[g] = [(i, p)]
        else:
            dic1[g].append((i, p))

        if g not in dic2:
            dic2[g] = p
        else:
            dic2[g] += p

    for (k, v) in sorted(dic2.items(), key=lambda x:x[1], reverse=True):
        for (i, p) in sorted(dic1[k], key=lambda x:x[1], reverse=True)[:2]:
            answer.append(i)

    return answer</code></pre>
<h3 id="놓친-부분-정리">놓친 부분 정리</h3>
<ul>
<li>for 문에서 튜플이나 배열 같은거 섞어서 지칭할 경우 형식을 동일하게 맞춰 줄 것!
<code>for i, (g, p) in enumerate(zip(genres, plays)):</code></li>
<li>sorted()를 쓰면 <code>total_play_count.sort(key=lambda x:-x[1])</code> 이런 작업을 줄일 수 있음
<code>for (k, v) in sorted(dic2.items(), key=lambda x:x[1], reverse=True):</code> 이렇게</li>
<li>그리고 지금 보니까 in 뒤에는 리스트로 안만들어도 되니까 (이터러블이면 되니까) dic2.items()가 그냥 그대로 들어간걸 확인할수있음</li>
<li>곡이 하나밖에 없으면 하나만 넣는다 &lt;- 이 로직을 나는 조건분기로 처리했는데, [:2] 이걸로 해버리면 너무 간단하게 해결 가능했음..</li>
</ul>
<h2 id="길-찾기-게임-프로그래머스">길 찾기 게임 (프로그래머스)</h2>
<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/6c6267e7-eb95-475c-a7f4-e1ce825b22b4/image.png" alt="">
<img src="https://velog.velcdn.com/images/ooleem-generator/post/bd36ca95-5702-49f3-a295-825c2d43cf40/image.png" alt=""></p>
<ul>
<li>문제 유형 : 트리</li>
<li>제한 시간 : 30분</li>
<li>아이디어 구상, 구현 실패</li>
<li>파이썬에서 클래스 만들어서 트리 구현하는법 다시 찾아보고 재시도할 것 </li>
</ul>
<h2 id="숫자-타자-대회-프로그래머스">숫자 타자 대회 (프로그래머스)</h2>
<ul>
<li>아이디어 구상 실패</li>
<li>검색해보니 3차원 dp가 섞인 백트래킹이라고 한다..</li>
<li>또는 가중치 정보를 일일이 노가다로 입력한 다음에 해결하는 경우도 있다..</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AI 엔지니어링] 1장. 파운데이션 모델을 활용한 AI 애플리케이션 입문]]></title>
            <link>https://velog.io/@ooleem-generator/AI-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-1%EC%9E%A5.-%ED%8C%8C%EC%9A%B4%EB%8D%B0%EC%9D%B4%EC%85%98-%EB%AA%A8%EB%8D%B8%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-AI-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%9E%85%EB%AC%B8</link>
            <guid>https://velog.io/@ooleem-generator/AI-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-1%EC%9E%A5.-%ED%8C%8C%EC%9A%B4%EB%8D%B0%EC%9D%B4%EC%85%98-%EB%AA%A8%EB%8D%B8%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-AI-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%9E%85%EB%AC%B8</guid>
            <pubDate>Mon, 17 Nov 2025 07:04:43 GMT</pubDate>
            <description><![CDATA[<img src="https://velog.velcdn.com/images/ooleem-generator/post/3e21971a-5777-4fb6-8951-cc45107730b1/image.png" width=400>

<p>책에 없는 내용(따로 찾아본 내용, 또는 개인적인 메모 등)은 기울임체로 표기했다.</p>
<h1 id="11-ai-엔지니어링의-부상">1.1 AI 엔지니어링의 부상</h1>
<h2 id="111-언어-모델에서-대규모-언어-모델로">1.1.1 언어 모델에서 대규모 언어 모델로</h2>
<h3 id="언어-모델-language-model">언어 모델 (Language model)</h3>
<ul>
<li>하나 이상의 언어에 대한 통계 정보를 인코딩하여, 주어진 컨텍스트에서 나타날 단어를 예측
영어를 모델링하는 방법에 대한 연구 : Prediction and Entropy of Printed English (1951)</li>
</ul>
<h4 id="토큰-token">토큰 (Token)</h4>
<ul>
<li>언어 모델의 기본 단위, 모델에 따라 문자, 단어, 또는 단어의 일부가 될 수 있음
(예를 들어 영어에서의 -tion, <em>한국어에서는 형태소? 정도로 이해하면 될 듯</em>)<img src="https://miro.medium.com/1*wOCxyFINp7GxvOTs56jlNQ.png"></li>
<li>GPT-4의 경우, 토큰 하나의 평균 길이는 단어의 약 3/4 정도 (100토큰 = 약 75개의 단어, <em>아마 영어 기준?</em>)</li>
</ul>
<h4 id="어휘-vocabulary">어휘 (Vocabulary)</h4>
<ul>
<li>모델이 다룰 수 있는 모든 토큰의 집합
알파벳의 몇 글자를 사용해 많은 단어를 만들 수 있듯, 소수의 토큰만 사용하여 많은 고유 단어를 만들 수 있음
(어휘 크기 예시 : 믹스트랄 8x7B 모델 - 32,000개, GPT-4 - 100,256개)</li>
</ul>
<blockquote>
<h3 id="언어-모델이-단어나-문장이-아닌-토큰을-사용하는-이유"><strong>언어 모델이 단어나 문장이 아닌 토큰을 사용하는 이유</strong></h3>
<p><strong>1. &quot;의미&quot;를 담을 수 있는 최소 단위이기 때문</strong>
<strong>2. 고유한 토큰의 수가 고유한 단어의 수보다 적기 때문에 모델의 어휘 크기를 줄일 수 있음</strong>
<strong>3. 모델이 알려지지 않은 단어(예시: chatgpting <em>또는 이런 방식으로 만들어진 신조어</em>)를 처리할 때, 그 구조를 이해하는 데 도움이 됨</strong></p>
</blockquote>
<h4 id="언어-모델의-유형">언어 모델의 유형</h4>
<ul>
<li><p>마스크 언어 모델(Masked language model)
: 누락된 토큰 전후 컨텍스트를 사용해, 시퀀스의 어느 위치에서든 누락된 토큰을 예측하도록 학습됨
감정 분석, 텍스트 분류처럼 새로운 텍스트를 만들지 않거나, 코드 디버깅처럼 앞뒤 코드를 모두 이해해야 하는, 전체적인 컨텍스트 이해가 필요한 작업에도 유용함
대표적인 예시로 BERT가 있음
관련 논문 : BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding (2018)</p>
</li>
<li><p>자기회귀 언어 모델 (인과 언어 모델, Autoregressive language model)
: 이전 토큰들만 보고 시퀀스의 다음 토큰을 예측하도록 학습됨
토큰을 하나씩 순차적으로 생성할 수 있으며, 이 특성 때문에 텍스트 생성 분야의 대세로 자리잡고 있음</p>
<img src="https://miro.medium.com/0*XaCA7dIIlVSegefD" width=400>

</li>
</ul>
<blockquote>
<h4 id="생성형-ai-generative-ai라는-용어의-유래">생성형 AI (Generative AI)라는 용어의 유래</h4>
<p>언어 모델은 정해진 유한한 어휘만을 사용해서 무한히 다양한 결과물을 만들어 낼 수 있다. (출력에 제한이 없다)
이처럼 정해진 답 없이 개방형 출력을 생성하는 모델을 생성 모델(generative model)이라 부르며, 생성형 AI라는 용어는 여기에서 비롯되었다.</p>
</blockquote>
<h3 id="자기-지도-학습-self-supervised-learning">자기 지도 학습 (Self-supervised learning)</h3>
<ul>
<li><p>지도 학습(Supervised learning)
: 레이블이 있는 데이터를 사용해 ML 알고리즘을 학습하는 과정, 데이터를 분류해서 레이블을 붙이는 과정이 필요하므로 비용과 시간이 오래 걸림
예를 들어, CT 스캔 사진에서 암의 징후가 있는지 레이블링하는 것은 어마어마한 돈이 들 것</p>
</li>
<li><p>자기 지도 학습
: 언어 모델링의 경우 모델이 입력 데이터에서 레이블을 추론할 수 있기 때문에 명시적인 레이블이 필요하지 않음
각 입력 시퀀스가 레이블(예측할 토큰)과 예측에 필요한 컨텍스트를 모두 제공
따라서 언어 모델은 별도의 레이블링 작업 없이 텍스트 시퀀스만으로 학습할 수 있으며, 텍스트 시퀀스는 어디에나 존재하기 때문에 방대한 양의 학습 데이터를 구축하여 LLM으로 확장될 수 있었음</p>
</li>
<li><p>BOS, EOS : 문장의 시작과 끝을 의미하는 일종의 특수 토큰
BOS = Beginning of Sequence, EOS = End of Sequence</p>
</li>
</ul>
<blockquote>
<h4 id="모델의-크기">모델의 크기</h4>
<p>일반적으로 모델의 크기는 학습 과정을 통해 업데이트되는 모델 내 변수인 파라미터의 수로 측정됨
엄밀하게는 모델 가중치(weight)와 모델 편향(bias)을 통틀어 파라미터로 정의하지만, 일반적으로는 모델 가중치를 파라미터로 지칭함</p>
</blockquote>
<h2 id="112-대규모-언어-모델에서-파운데이션-모델로">1.1.2 대규모 언어 모델에서 파운데이션 모델로</h2>
<h3 id="대규모-멀티모달-모델-large-multimodal-model-lmm">대규모 멀티모달 모델 (Large Multimodal Model, LMM)</h3>
<ul>
<li>멀티모달 모델 : 둘 이상의 데이터 형태를 처리할 수 있는 모델</li>
<li>일반적으로 생성형 멀티모델을 대규모 멀티모달 모델로 지칭함</li>
<li>언어 모델의 경우 텍스트 토큰에 기반해 다음 토큰을 생성하지만, 멀티모달 모델은 텍스트와 이미지 토큰, 또는 모델이 지원하는 다른 모달리티를 기반으로 다음 토큰을 생성할 수 있음</li>
</ul>
<h4 id="멀티모달-모델의-자기지도학습-사례-언어-이미지-모델-clip-openai-2021">멀티모달 모델의 자기지도학습 사례: 언어 이미지 모델 CLIP (OpenAI, 2021)</h4>
<ul>
<li>자연어 지도(Natural language supervision)라는 자기지도학습 기법을 사용</li>
<li>각 이미지에 대한 레이블을 수동으로 생성하는 대신, 인터넷에서 함꼐 발견되는 (이미지, 텍스트) 쌍을 수집</li>
<li>CLIP은 생성 모델이 아닌 임베딩 모델로, 텍스트와 이미지를 함께 임베딩하는 방식
이러한 멀티모달 임베딩 모델은 Flamingo, LLaVA, Gemini와 같은 생성형 멀티모달 모델의 핵심</li>
</ul>
<h3 id="파운데이션-모델-foundation-model">파운데이션 모델 (Foundation model)</h3>
<ul>
<li><em>자기지도학습을 통해 별도의 레이블링 작업 없이 광범위한 데이터셋을 사전 학습하여, 그 과정에서 일반화 가능하고 적응 가능한 데이터 표현을 학습하는 모든 모델</em> </li>
<li>특정 작업에 맞춘 모델에서 범용 모델로 전환됨, 즉 하나의 모델로 여러 작업을 할 수 있음</li>
<li>만약 특정 작업에 대한 성능을 올리고 싶다면, 파인튜닝이 필요</li>
<li>직접 자체 모델(작업 특화 모델, task-specific model)을 만들 것인가? 파운데이션 모델을 파인튜닝할 것인가?</li>
</ul>
<p>참고 글 : <a href="https://yumdata.tistory.com/400">https://yumdata.tistory.com/400</a></p>
<h2 id="113-파운데이션-모델에서-ai-엔지니어링으로">1.1.3 파운데이션 모델에서 AI 엔지니어링으로</h2>
<ul>
<li>전통적인 ML 엔지니어링 : 모델 자체를 개발하는 것</li>
<li>AI 엔지니어링 : 이미 존재하는 모델을 활용하는 것</li>
</ul>
<blockquote>
<h3 id="ai-엔지니어링의-빠른-성장을-위한-이상적인-조건을-만드는-세-가지-요인">AI 엔지니어링의 빠른 성장을 위한 이상적인 조건을 만드는 세 가지 요인</h3>
<p>강력한 파운데이션 모델의 이용 가능성과 접근성이 다음 세 가지 요인으로 이어짐
<strong>1. 범용 AI 능력</strong>
: 더 많은 작업을 수행할 수 있기 때문에, 이전에 상상할 수 없었던 애플리케이션 서비스가 계속해서 등장하고 있고, 사용자 수와 AI 애플리케이션에 대한 수요가 크게 늘어남
<strong>2. AI 투자 증가</strong>
: AI에 대한 투자가 급격히 증가하면서, 점점 더 많은 기업이 AI를 자사 제품과 프로세스에 통합하고 있음
<strong>3. AI 애플리케이션 개발에 대한 낮아진 진입 장벽</strong>
: 단일 API 호출을 통해 강력한 모델에 접근할 수 있으며, 최소한의 코딩으로 애플리케이션 개발이 가능해짐</p>
</blockquote>
<h1 id="12-파운데이션-모델-활용-사례">1.2 파운데이션 모델 활용 사례</h1>
<ul>
<li>다음 분류는 깃허브에서 500개 이상의 별을 받은 오픈 소스 AI 애플리케이션을 분류한 것</li>
</ul>
<h3 id="코딩">코딩</h3>
<ul>
<li>웹 페이지와 pdf에서 구조화된 데이터 추출 : AgentGPT</li>
<li>자연어-코드 변환 : DB-GPT, SQL chat, PandasAI</li>
<li>디자인이나 스크린샷을 주면, 주어진 이미지처럼 보이는 웹사이트로 변환하는 코드 생성 : screenshot-to-code, draw-a-ui</li>
<li>프로그래밍 언어/프레임워크 번역(변환) : GPT-migrate, AI Code Translator</li>
<li>문서 작성 : Autodoc</li>
<li>테스트 만들기 : PentestGPT</li>
<li>커밋 메세지 만들기 : AI Commits</li>
</ul>
<h3 id="이미지-및-동영상-제작">이미지 및 동영상 제작</h3>
<ul>
<li>이미지 생성 : Midjourney</li>
<li>사진 편집 : Adobe Firefly</li>
<li>영상 생성 : Runway, Pika Labs, Sora</li>
</ul>
<h3 id="글쓰기-교육-대화형-봇-정보-집계-데이터-체계화-워크플로우-자동화">글쓰기, 교육, 대화형 봇, 정보 집계, 데이터 체계화, 워크플로우 자동화</h3>
<h1 id="13-ai-애플리케이션-기획">1.3 AI 애플리케이션 기획</h1>
<h2 id="131-활용-사례-평가">1.3.1 활용 사례 평가</h2>
<ul>
<li>제일 중요한 질문 : AI 애플리케이션을 <strong>왜</strong> 만들고 싶은가?</li>
<li>기업이 AI 애플리케이션을 만들기로 결정한 이유를 위험 순서대로 나열하자면..</li>
<li>AI를 가진 경쟁사에 밀려 생존을 위협받을 수 있거나</li>
<li>이익과 생산성 증대를 위한 기회를 포착하거나</li>
<li>구체적 활용법은 불확실해도, 기술 흐름에서 뒤처지는 것이 불안하거나</li>
</ul>
<h3 id="애플리케이션에서-ai와-사람의-역할">애플리케이션에서 AI와 사람의 역할</h3>
<h4 id="제품에-ai를-활용하는-방법">제품에 AI를 활용하는 방법</h4>
<ul>
<li>핵심적 또는 보완적
: 앱이 AI 없이도 작동할 수 있다면, AI는 그 앱에 보완적
앱에서 AI가 핵심적이라면, AI 부분이 더 정확하고 신뢰할 수 있어야 함</li>
<li>반응형 또는 선제형
: 반응형은 사용자의 응답이나 특정 행동에 &#39;응답&#39;하는 방식으로 작동 (예시 : 챗봇)
선제형 기능은 사용자에게 유용하다고 판단하는 &#39;적절한 시점&#39;에 먼저 정보를 제시 (예시 : 구글 맵스의 교통 알림)</li>
<li>동적 또는 정적
: 동적 방식은 사용자 피드백(자신의 데이터)을 통해 지속적으로 업데이트(파인튜닝)되어 자신만의 모델이 만들어지게 됨 (예시 : 페이스 ID)
정적 방식은 여러 사용자가 하나의 공유 모델을 함께 사용하므로, 주기적으로만 업데이트 (예시 : 구글 포토의 객체 탐지 모델)</li>
</ul>
<h4 id="애플리케이션에서-가능한-ai의-역할-범위">애플리케이션에서 가능한 AI의 역할 범위</h4>
<ul>
<li>AI는 사람을 뒤에서 지원하는 데에만 사용</li>
<li>AI는 단순한 요청만 처리, 복잡한 요청은 사람에게 전달</li>
<li>AI가 모든 요청을 처리</li>
</ul>
<h4 id="human-in-the-loop--ai가-의사결정-과정에-사람을-참여시키는-것">human-in-the-loop : AI가 의사결정 과정에 사람을 참여시키는 것</h4>
<h3 id="ai-제품-방어-가능성">AI 제품 방어 가능성</h3>
<ul>
<li>만약 AI 애플리케이션을 독립형 제품으로 판매한다고 할 경우, 방어 가능성을 고려해야 함</li>
<li>경쟁사, 또는 거대 대기업이 쉽게 따라 만들 수 없도록 하는 경쟁 우위가 필요 (기술력, 데이터, 유통력..)</li>
<li>더 큰 제품의 기능이 될 법한 제품으로도 성공한 사례는 꽤 많음
예시 : 캘린들리 - 구글 캘린더, 메일침프 - 지메일, 포토룸 - 구글 포토..
<em><strong>어떻게 이 스타트업들은 경쟁사를 뛰어넘었는가?</strong></em></li>
</ul>
<h2 id="132-기대치-설정">1.3.2 기대치 설정</h2>
<h3 id="ai-애플리케이션이-비즈니스에-어떤-영향을-미치는가---챗봇-예시">AI 애플리케이션이 비즈니스에 어떤 영향을 미치는가 - 챗봇 예시</h3>
<ul>
<li>자동화하고 싶은 고객 메시지의 비율</li>
<li>얼마나 더 많은 메세지를 처리할 수 있는지</li>
<li>얼마나 더 빨리 응답할 수 있는지</li>
<li>얼마나 많은 인력을 절감할 수 있는지</li>
</ul>
<h3 id="최소-성능의-기준---챗봇-예시">최소 성능의 기준 - 챗봇 예시</h3>
<ul>
<li>응답의 품질을 측정하는 품질 지표</li>
<li>TTFT(첫 토큰까지 걸리는 시간), TPOT(출력 토큰당 시간), 전체 지연 시간을 포함하는 지연 시간 지표</li>
<li>추론 요청당 비용 등 비용 지표</li>
<li>해석 가능성, 공정성 등 기타 지표</li>
</ul>
<h2 id="133-마일스톤-계획">1.3.3 마일스톤 계획</h2>
<ul>
<li>측정 가능한 목표 설정 후, 달성 계획이 필요</li>
<li>우선 이미 존재하는 모델의 성능을 평가해 그 능력을 파악해야 함</li>
<li>파운데이션 모델의 성능이 꽤 좋아서 초기 데모가 괜찮게 나오더라도, 최종 제품을 만드는 일은 훨씬 어려움</li>
</ul>
<h2 id="134-유지보수">1.3.4 유지보수</h2>
<ul>
<li>매우 빠른 기술의 변화를 항상 예의주시하면서, 각 기술 투자에 대한 비용-편익 분석 필요</li>
<li>지식재산권/AI 기술/관련 자원에 대한 규제 또한 계속 변하고 있음</li>
</ul>
<h1 id="14-ai-엔지니어링-스택">1.4 AI 엔지니어링 스택</h1>
<h2 id="141-ai의-세-가지-계층">1.4.1 AI의 세 가지 계층</h2>
<h3 id="애플리케이션-개발">애플리케이션 개발</h3>
<ul>
<li>실제 애플리케이션 개발 (최상위 계층)</li>
<li>모델에 적절한 프롬프트와 필요한 컨텍스트 제공</li>
<li>좋은 애플리케이션에는 좋은 인터페이스가 필요</li>
<li>AI 인터페이스, 프롬프트 엔지니어링, 컨텍스트 구성, 평가...<h3 id="모델-개발">모델 개발</h3>
</li>
<li>모델을 개발하기 위한 도구 제공</li>
<li>데이터가 모델 개발의 핵심이므로, 데이터셋 엔지니어링도 포함</li>
<li>추론 최적화, 데이터셋 엔지니어링, 모델링과 학습, 평가...<h3 id="인프라">인프라</h3>
</li>
<li>컴퓨팅 관리, 데이터 관리, 서빙, 모니터링...</li>
</ul>
<h2 id="142-ai-엔지니어링-대-ml-엔지니어링">1.4.2 AI 엔지니어링 대 ML 엔지니어링</h2>
<blockquote>
<h3 id="전통적인-ml-엔지니어링과의-차이점">전통적인 ML 엔지니어링과의 차이점</h3>
</blockquote>
<ol>
<li>전통적인 ML은 필요한 모델을 직접 학습시켜야 했지만, AI 엔지니어링은 모델을 가져다 씀</li>
<li>전통적인 ML보다 더 크고, 더 많은 컴퓨팅 자원을 소모하며, 더 높은 지연시간을 발생시키는 컴퓨팅 집약적인 모델을 다루면서, GPU와 대규모 클러스터를 다룰 줄 아는 엔지니어에 대한 수요가 늘어남</li>
<li>개방형 출력을 생성할 수 있는 모델을 다루면서, 평가가 더 어려워짐</li>
</ol>
<ul>
<li><strong>결국, AI 엔지니어링은 모델 개발보다 모델 조정과 평가에 더 중점을 둔다는 것이 차이점</strong></li>
</ul>
<h3 id="모델-조정-기법">모델 조정 기법</h3>
<ul>
<li>프롬프트 기반 기법(프롬프트 엔지니어링을 포함) - 모델 가중치를 업데이트하지 않음
: 모델 자체를 바꾸는 대신, 지시와 컨텍스트를 제공하여 모델의 반응을 원하는 방향으로 유도</li>
<li>파인튜닝 - 모델 가중치를 업데이트
: 모델 자체를 변경해 새로운 작업에 맞게 조정</li>
</ul>
<h3 id="모델-개발-1">모델 개발</h3>
<h4 id="모델링과-학습">모델링과 학습</h4>
<ul>
<li>모델 아키텍처를 고안하고, 학습하고, 파인튜닝하는 과정
도구의 예시 : 텐서플로(구글), 트랜스포머(허깅페이스), 파이토치(메타)</li>
<li>사전 학습(pre-train) : 모델을 처음부터 학습하는 것, 모델 가중치가 무작위로 초기화됨
많은 자원과 시간이 필요하며, 사전 학습 중의 작은 실수도 상당한 재정적 손실 및 프로젝트 지연을 초래할 수 있음</li>
<li>파인튜닝(fine-tuning) : 이미 학습된 모델을 추가로 학습하는 것
사전 학습보다 적은 자원을 필요로 함</li>
<li>사후 학습(post-train) : 기술적으로는 파인튜닝과 거의 같은 과정이며, 보통 모델 제공업체가 수행하는 경우 사후 학습, 애플리케이션 개발자가 수행하면 파인튜닝이라 칭함</li>
</ul>
<h4 id="데이터셋-엔지니어링">데이터셋 엔지니어링</h4>
<ul>
<li>AI 모델의 학습과 조정에 필요한 데이터를 선별하고, 생성하며, 주석을 다는 과정</li>
<li>전통적인 ML 엔지니어링에서 대부분의 활용 사례는 &quot;폐쇄형&quot;이었으나, 파운데이션 모델은 &quot;개방형&quot;이므로, 데이터 주석은 AI 엔지니어링에서 훨씬 더 큰 과제</li>
<li>AI 엔지니어링은 비정형 데이터를 다루며, 중복 제거, 토큰화, 컨텍스트 검색, 민감 정보 및 유해 데이터 제거를 포함한 품질 관리에 중점을 둠</li>
</ul>
<h4 id="추론-최적화">추론 최적화</h4>
<ul>
<li>모델을 더 빠르고 저렴하게 만드는 것</li>
<li>파운데이션 모델은 종종 자기회귀적인 특성을 보임(예시 : 토큰의 순차적 생성)</li>
</ul>
<h3 id="애플리케이션-개발-1">애플리케이션 개발</h3>
<ul>
<li>파운데이션 모델을 사용하는 경우 많은 팀이 같은 모델을 사용하므로, 애플리케이션 개발 과정을 통해 차별화를 이루어야 함</li>
</ul>
<h4 id="평가">평가</h4>
<ul>
<li>N-shot : 모델에게 N개의 예시를 미리 보여주는 기법</li>
<li>생각의 사슬(Chain of Thought, CoT) : 예시에 정답뿐만 아니라 사고 과정을 포함시켜 추론 능력을 높이는 기법</li>
<li>N-shot CoT : N개의 CoT 예시를 프롬프트에 포함하는 기법</li>
<li>CoT@N (예시 : CoT@32) : 하나의 문제에 대해 모델이 N개의 다른 풀이 과정을 생성하게 한 뒤, 가장 많이 나온 답을 채택하여 응답의 안정성을 높이는 기법</li>
</ul>
<h4 id="프롬프트-엔지니어링-및-컨텍스트-구성">프롬프트 엔지니어링 및 컨텍스트 구성</h4>
<h4 id="ai-인터페이스">AI 인터페이스</h4>
<h2 id="143-ai-엔지니어링-대-풀스택-엔지니어링">1.4.3 AI 엔지니어링 대 풀스택 엔지니어링</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[[객체지향의 사실과 오해] 7장+부록A]]></title>
            <link>https://velog.io/@ooleem-generator/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%9D%98-%EC%82%AC%EC%8B%A4%EA%B3%BC-%EC%98%A4%ED%95%B4-7%EC%9E%A5%EB%B6%80%EB%A1%9DA</link>
            <guid>https://velog.io/@ooleem-generator/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%9D%98-%EC%82%AC%EC%8B%A4%EA%B3%BC-%EC%98%A4%ED%95%B4-7%EC%9E%A5%EB%B6%80%EB%A1%9DA</guid>
            <pubDate>Fri, 07 Nov 2025 15:52:30 GMT</pubDate>
            <description><![CDATA[<h1 id="7장-함께-모으기">7장. 함께 모으기</h1>
<h2 id="코드와-세-가지-관점">코드와 세 가지 관점</h2>
<ul>
<li>개념 관점(Conceptual Perspective)
: 도메인 안에 존재하는 개념과 개념들 사이의 관계를 표현</li>
<li>명세 관점(Specification Perspective)
: 객체가 협력을 위해 &#39;무엇&#39;을 할 수 있는가를 표현</li>
<li>구현 관점(Implementation Perspective)
: 객체들이 책임을 수행하는 데 필요한 동작하는 코드를 작성</li>
</ul>
<p>이 순서대로 구현하라는 것이 아니라, 동일한 코드를 각기 다른 세 가지 관점에서 볼 수 있다는 것을 의미한다. &quot;이 코드는 무슨 일을 하고 있나요?&quot;라는 질문에 도메인(비즈니스 로직)의 관점에서 답할 수도 있고, 객체들의 역할/책임/협력 관점에서 답할 수도 있고, 순수하게 구현된 코드의 동작으로 답할 수도 있을 것이다.</p>
<blockquote>
<p><strong>인터페이스와 구현을 분리하라!</strong></p>
</blockquote>
<p>이 책에서 거듭 강조하는 표현이다. 예시를 들어 설명하자면 이렇다.</p>
<p>예를 들어, 차종이 달라져도 같은 자동변속기 차량이라면 대부분의 사람들은 운전하는 데 별 문제가 없다. 차량 내부의 구현 방식은 달라도, 운전자 입장에서는 굳이 구현 방식이 어떻게 달라졌는지 알 필요가 없다. 자동변속기라는 인터페이스가 같다면, 운전자는 같은 행위를 통해 차량을 운전할 수 있다.</p>
<p>만약 인터페이스와 구현이 명확히 분리되어 있지 않다면, 구현이 변경되어야만 하는 상황에 놓였을 때 인터페이스도 변경될 수 있으며, 사용자는 변경된 인터페이스에 맞춰서 행위를 변경해야 한다.</p>
<h1 id="부록a-추상화-기법">부록A. 추상화 기법</h1>
<p>사실 부록이지만 이 책에서 말하는 핵심 내용들이 부록에 다 담겨 있는 것 같다.
&quot;무엇&quot;을 추상화하느냐에 따라 아래 세 가지 기법 중 하나를 선택할 수 있다.</p>
<blockquote>
<p>각 추상화 기법은 공통적으로, 복잡성을 낮추기 위해 사물의 특정한 측면을 감춘다.</p>
</blockquote>
<h2 id="분류와-인스턴스화">분류와 인스턴스화</h2>
<ul>
<li>분류 : 객체의 구체적인 세부 사항을 숨기고 인스턴스 간에 공유하는 공통적인 특성을 기반으로 범주를 형성하는 과정</li>
<li>인스턴스화 : 분류의 역과정, 범주로부터 객체를 생성하는 과정</li>
</ul>
<p>객체지향에 대해 처음 공부했을 당시에 객체, 클래스, 인스턴스 등 처음 접하게 되는 개념들이 많아서 헷갈렸는데, 특히 &#39;인스턴스&#39;처럼 영어 단어를 그대로 음차한 단어 같은 경우 한 번에 뜻이 잘 와닿지 않는 경우가 많은 것 같다.</p>
<p>인스턴스를 확실하게 이해하게 됐던 계기는, 문득 영어 시간에 &quot;For instance&quot;가 &quot;예를 들어&quot; 였던 게 생각난 것이었다. 저 문장 앞에 어떤 개념에 대한 설명이 있고, 그 개념의 예시를 들 때 &quot;For instance, ~~&quot;라고 하니까, &quot;앞에서 말한 개념의 실제 사례&quot;라는 의미로 연결됐다.
여기서 한 번 더 든 생각이, &quot;EC2 인스턴스&quot;라는 표현이 있었는데, 이 때 인스턴스도 저런 의미에서 사용된 것인지 궁금해져서 GPT한테 물어봤더니, 내 생각이 맞았다.</p>
<blockquote>
<p>EC2 인스턴스: AMI/런치 템플릿(기본 이미지+설정)을 바탕으로 띄운 구체적인 가상 서버 한 대. 시작할 때마다 서로 다른 인스턴스 ID(i-...)를 가진 별개의 머신.</p>
</blockquote>
<p>덕분에 AMI/런치 템플릿이라는 게 있었구나도 다시 짚게 되었다.</p>
<h3 id="타입">타입</h3>
<ul>
<li>심볼(symbol) : 타입을 가리키는 간략한 이름이나 명칭</li>
<li>내연(intension) : 타입의 완전한 정의</li>
<li>외연(extension) : 타입에 속하는 모든 객체들의 집합</li>
</ul>
<p>마치 우리가 어떤 새로운 개념을 접했을 때 사전에서 그 뜻을 찾아보면, 그 정의가 내연이고, 예시가 외연이며, 그러한 정의에 해당하는 것을 부르는 단어를 심볼이라고 생각하면 될 것이다.</p>
<h3 id="클래스">클래스</h3>
<p>이 책에서 거듭 강조하는 또 다른 말은 클래스와 타입이 같은 개념이 아니라는 것이다. &#39;타입&#39;이라는 개념을 구현하기 위한 방법 중의 하나일 뿐이라는 것이다. 
실제로 자바스크립트의 경우 클래스 대신 프로토타입이라는 개념을 이용해 타입을 구현한다. (ES6에서 클래스도 도입되었다)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 최소비용 구하기]]></title>
            <link>https://velog.io/@ooleem-generator/%EB%B0%B1%EC%A4%80-%EC%B5%9C%EC%86%8C%EB%B9%84%EC%9A%A9-%EA%B5%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ooleem-generator/%EB%B0%B1%EC%A4%80-%EC%B5%9C%EC%86%8C%EB%B9%84%EC%9A%A9-%EA%B5%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 02 Nov 2025 10:21:31 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-python"># Practice : dijkstra

import heapq

n = int(input())
m = int(input())
graph = [[] for _ in range(n + 1)]
visited = [False] * (n + 1)
costs = [float(&quot;inf&quot;)] * (n + 1)  # 각 노드까지 가는 데 드는 비용 테이블


for _ in range(m):
    i, j, cost = map(int, input().split())
    graph[i].append((j, cost))

# print(graph)


def dijkstra(start):
    global costs
    queue = []
    costs[start] = 0
    heapq.heappush(queue, (0, start))
    while queue:
        for _ in range(len(queue)):
            accum_cost, present_node = heapq.heappop(queue)
            if costs[present_node] &lt; accum_cost:
                continue

            for next_node, cost in graph[present_node]:
                heapq.heappush(queue, (accum_cost + cost, next_node))
                if costs[next_node] &gt; accum_cost + cost:
                    costs[next_node] = accum_cost + cost
            # print(costs)


start, end = map(int, input().split())

dijkstra(start)

# print(costs)

print(costs[end])
</code></pre>
<p>메모리 초과가 나는 이유는?</p>
<ol>
<li><p><code>heapq.heappush(queue, (accum_cost + cost, next_node))</code> 위치가 잘못됐음
저 위치에 넣으면 cost가 갱신되든 안되든 일단 넣는다는 의미니까 힙이 불필요하게 커짐</p>
</li>
<li><p>float 대신 int쓰는게 좋음 (보통 10**15 많이씀) 1e15도 float이니까 조심</p>
</li>
<li><p>추가 팁) 지금 이 문제처럼 특정 도착지를 명시한 경우, 다음과 같이 도착지가 등장하자마자 return해버려도 됨</p>
<pre><code class="language-python">def dijkstra(start, end):
 global costs
 queue = []
 costs[start] = 0
 heapq.heappush(queue, (0, start))
 while queue:
     accum_cost, present_node = heapq.heappop(queue)
     if present_node == end:
         return accum_cost

     if costs[present_node] &lt; accum_cost:
         continue

     for next_node, cost in graph[present_node]:
         heapq.heappush(queue, (accum_cost + cost, next_node))
         if costs[next_node] &gt; accum_cost + cost:
             costs[next_node] = accum_cost + cost

</code></pre>
</li>
</ol>
<p>start, end = map(int, input().split())</p>
<p>dijkstra(start, end)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 용어 영단어 시리즈]]></title>
            <link>https://velog.io/@ooleem-generator/%EA%B0%9C%EB%B0%9C-%EC%9A%A9%EC%96%B4-%EC%98%81%EB%8B%A8%EC%96%B4-%EC%8B%9C%EB%A6%AC%EC%A6%88</link>
            <guid>https://velog.io/@ooleem-generator/%EA%B0%9C%EB%B0%9C-%EC%9A%A9%EC%96%B4-%EC%98%81%EB%8B%A8%EC%96%B4-%EC%8B%9C%EB%A6%AC%EC%A6%88</guid>
            <pubDate>Sun, 02 Nov 2025 08:36:53 GMT</pubDate>
            <description><![CDATA[<p>진짜 그냥 메모장
개발 용어 중에서 마땅히 번역할 말이 없거나, 또는 다른 단어가 같은 한국어로 번역되서 헷갈리는 경우 모음집</p>
<h3 id="정리하게-된-계기">정리하게 된 계기</h3>
<p>객체지향을 본격적으로 공부하기 시작해 보니까, 추상화된 개념을 이해해야 하는 부분이 많아서(거의 철학..) 번역된 용어 말고 원문 영단어에서 나오는 뉘앙스 차이를 이해해야겠다고 생각함</p>
<ul>
<li>instance
클래스, 객체, 인스턴스 어쩌구 할때 &quot;인스턴스&quot;라고 그대로 음차한 단어를 보통 써서 헷갈리는 경우가 많은 것 같다. 처음 들었을 땐 나도 헷갈렸었는데, 영어 시간에 &quot;For instance&quot;가 &quot;예를 들어&quot; 였던 게 생각나니까 갑자기 해결이 됐다. 저 문장 앞에 어떤 개념에 대한 설명이 있고, 그 개념의 예시를 들 때 &quot;For instance, ~~&quot;라고 하니까, &quot;앞에서 말한 개념의 실제 사례&quot;라는 의미로 연결됐다.
근데.. 생각해보니까 인스턴스라는 말이 EC2에서도 있었던거 같은데..?
그래서 GPT한테 물어봤더니 내 생각이 맞았다.<blockquote>
<p>EC2 인스턴스: AMI/런치 템플릿(기본 이미지+설정)을 바탕으로 띄운 구체적인 가상 서버 한 대. 시작할 때마다 서로 다른 인스턴스 ID(i-...)를 가진 별개의 머신.</p>
</blockquote>
</li>
</ul>
<p>덕분에 AMI/런치 템플릿이라는 게 있었구나도 다시 짚게 됨.</p>
<ul>
<li><p>attribute vs. property
<a href="https://inpa.tistory.com/entry/%F0%9F%8C%90-attribute-property-%EC%B0%A8%EC%9D%B4">https://inpa.tistory.com/entry/%F0%9F%8C%90-attribute-property-%EC%B0%A8%EC%9D%B4</a></p>
</li>
<li><p>entity</p>
</li>
<li><p>status vs. state</p>
</li>
<li><p>heuristic</p>
</li>
<li><p>ontology</p>
</li>
<li><p>semantic</p>
</li>
<li><p>literal</p>
</li>
</ul>
<p>나중에 하나씩 정리해야지.. 언젠가..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[타입스크립트 디자인 패턴] 프롤로그 + 1장]]></title>
            <link>https://velog.io/@ooleem-generator/%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%ED%94%84%EB%A1%A4%EB%A1%9C%EA%B7%B8-1%EC%9E%A5</link>
            <guid>https://velog.io/@ooleem-generator/%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%ED%94%84%EB%A1%A4%EB%A1%9C%EA%B7%B8-1%EC%9E%A5</guid>
            <pubDate>Sun, 02 Nov 2025 07:53:22 GMT</pubDate>
            <description><![CDATA[<p>원래는 &quot;객체지향의 사실과 오해&quot; 완독 스터디를 하면서 나만무 최종 프로젝트와 연결지어서 공부를 하려고 했는데.. 문제는 내가 타입스크립트와 NestJS, Next.js에 대한 이해가 너무 부족한 것 같았다.</p>
<p>과제가 아예 진행이 안 되서, 이대로는 안 되겠다 생각하고 타입스크립트부터 먼저 공부해야겠다 싶어서 고른 책, &quot;타입스크립트 디자인 패턴&quot;이다.</p>
<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/3de3e2d6-5733-4e0e-931b-308c2dfce397/image.png" alt=""></p>
<p>개인 블로그를 따로 만들어놓긴 했는데, 아무래도 이거 읽으면서는 각 잡힌 글로 정리하기 힘들 것 같아서 여기에 올려본다. 나중에 정리된 글을 쓸 정도로 이해도가 높아지면 다듬어보려고 한다.</p>
<h1 id="1장-도구와-프레임워크">1장. 도구와 프레임워크</h1>
<p>사실 이 책이 2017년도 책이라 이 부분은 그대로 받아들이기 어려운 부분들이 좀 있는 것 같다. &quot;이런 게 있었구나&quot;하고 새로운 키워드를 알게 되는 데 집중해야겠다.</p>
<h2 id="타입스크립트-프로젝트-구성">타입스크립트 프로젝트 구성</h2>
<h3 id="tsconfigjson">tsconfig.json</h3>
<h4 id="예시-실제로-goodjob_backend-리포지토리의-tsconfigjson">예시) 실제로 GoodJob_backend 리포지토리의 tsconfig.json</h4>
<pre><code class="language-typescript">{
    &quot;compilerOptions&quot;: {
        &quot;module&quot;: &quot;nodenext&quot;,
        &quot;moduleResolution&quot;: &quot;nodenext&quot;,
        &quot;resolvePackageJsonExports&quot;: true,
        &quot;esModuleInterop&quot;: true,
        &quot;isolatedModules&quot;: true,
        &quot;declaration&quot;: true,
        &quot;removeComments&quot;: true,
        &quot;emitDecoratorMetadata&quot;: true,
        &quot;experimentalDecorators&quot;: true,
        &quot;allowSyntheticDefaultImports&quot;: true,
        &quot;target&quot;: &quot;ES2023&quot;,
        &quot;lib&quot;: [&quot;ES2023&quot;, &quot;DOM&quot;],
        &quot;sourceMap&quot;: true,
        &quot;outDir&quot;: &quot;./dist&quot;,
        &quot;baseUrl&quot;: &quot;./&quot;,
        &quot;paths&quot;: {
            &quot;@/*&quot;: [&quot;src/*&quot;],
            &quot;@/config/*&quot;: [&quot;src/config/*&quot;],
            &quot;@/auth/*&quot;: [&quot;src/auth/*&quot;],
            &quot;@/database/*&quot;: [&quot;src/database/*&quot;],
            &quot;@/lib/*&quot;: [&quot;src/lib/*&quot;],
            &quot;@/modules/*&quot;: [&quot;src/modules/*&quot;],
            &quot;@/onboarding/*&quot;: [&quot;src/onboarding/*&quot;],
            &quot;@/ai/*&quot;: [&quot;src/ai/*&quot;],
            &quot;@/stt/*&quot;: [&quot;src/stt/*&quot;],
            &quot;@/tts/*&quot;: [&quot;src/tts/*&quot;],
            &quot;@/types/*&quot;: [&quot;src/types/*&quot;]
        },
        &quot;incremental&quot;: true,
        &quot;skipLibCheck&quot;: true,
        &quot;strictNullChecks&quot;: true,
        &quot;forceConsistentCasingInFileNames&quot;: true,
        &quot;noImplicitAny&quot;: false,
        &quot;strictBindCallApply&quot;: false,
        &quot;noFallthroughCasesInSwitch&quot;: false,
        &quot;typeRoots&quot;: [&quot;./node_modules/@types&quot;, &quot;./src/types&quot;]
    },
    &quot;include&quot;: [&quot;src/**/*&quot;, &quot;test/**/*&quot;, &quot;scripts/**/*&quot;],
    &quot;exclude&quot;: [&quot;node_modules&quot;, &quot;dist&quot;]
}
</code></pre>
<h4 id="컴파일러-옵션-compileroptions">컴파일러 옵션 (compilerOptions)</h4>
<ul>
<li>target: 타입스크립트 파일을 어떤 버전의 자바스크립트로 바꿔줄지 정하는 부분
예시) es5로 세팅하면 es5 버전 자바스크립트로 컴파일</li>
<li>module: 자바스크립트 파일간 import 문법을 구현할 때 어떤 문법을 쓸지 정하는 곳
책에서는 commonjs 쓰라고 하는 것 같은데, 우리는 nodenext를 사용
모듈 관련 아티클 -&gt; <a href="https://toss.tech/article/commonjs-esm-exports-field">https://toss.tech/article/commonjs-esm-exports-field</a></li>
<li>declaration: 활성화하면 자바스크립트 출력과 함께 .d.ts 선언 파일을 생성 (현재 사용하는 모든 타입이 저장됨)</li>
<li>strictNullChecks: </li>
</ul>
<p>(나중에 계속 업데이트 예정..)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[251021 오늘한거]]></title>
            <link>https://velog.io/@ooleem-generator/251021-%EC%98%A4%EB%8A%98%ED%95%9C%EA%B1%B0</link>
            <guid>https://velog.io/@ooleem-generator/251021-%EC%98%A4%EB%8A%98%ED%95%9C%EA%B1%B0</guid>
            <pubDate>Mon, 20 Oct 2025 16:50:36 GMT</pubDate>
            <description><![CDATA[<ul>
<li>파이썬 3.11 가상환경이 필요해서 알아보니까 pyenv라는 버전관리 패키지가 있다해서 설치</li>
<li>참고한 글 - <a href="https://www.daleseo.com/python-pyenv/">https://www.daleseo.com/python-pyenv/</a> <a href="https://raoneli-coding.tistory.com/172">https://raoneli-coding.tistory.com/172</a></li>
<li>원하는 파이썬 버전을 기본으로 설정하려면
<code>pyenv global [버전]</code></li>
<li>특정 위치에서 원하는 파이썬 버전을 사용하려면 - 위치로 이동 후
<code>pyenv local [버전]</code></li>
<li>가상환경 설정 : pyenv-virtualenv라는 패키지가 필요</li>
<li>참고한 글 : <a href="https://dandyrilla.github.io/2024-06-05/pyenv-virtualenv/">https://dandyrilla.github.io/2024-06-05/pyenv-virtualenv/</a></li>
<li>패키지 설치 후 <code>pyenv virtualenv [버전] [가상환경명]</code> 으로 가상환경 추가</li>
<li>그 다음에 <code>pyenv activate [가상환경명]</code> 하면 된다는데..
오류 : Failed to activate vertualenv.</li>
<li>참고한 글 - <a href="https://velog.io/@limdongyoung0/pyenv-Failed-to-activate-virtualenv">https://velog.io/@limdongyoung0/pyenv-Failed-to-activate-virtualenv</a></li>
<li>vi 에디터 오랜만에 다시
<code>vi ~/.zshrc</code> 로 zsh 설정 파일 열어서 수정
i 누르면 insert 모드로 바뀌면서 수정 가능해짐
맨 마지막 줄에 <code>eval &quot;$(pyenv init -)&quot;</code> 추가 후 esc 눌러서 insert 모드 빠져나오고, :wq로 저장 후 종료</li>
<li>이제 <code>pyenv activate [가상환경명]</code> 하니까 가상환경 잡힌다</li>
</ul>
<ul>
<li>파이썬 입력 인자 처리 : argparse 이용</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[251016 모각코테]]></title>
            <link>https://velog.io/@ooleem-generator/251016-%EB%AA%A8%EA%B0%81%EC%BD%94%ED%85%8C</link>
            <guid>https://velog.io/@ooleem-generator/251016-%EB%AA%A8%EA%B0%81%EC%BD%94%ED%85%8C</guid>
            <pubDate>Thu, 16 Oct 2025 07:56:05 GMT</pubDate>
            <description><![CDATA[<h1 id="1-문자열-압축">1. 문자열 압축</h1>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/60057">https://school.programmers.co.kr/learn/courses/30/lessons/60057</a></p>
<p>쉬워보였지만 문제를 잘못 이해하기 쉬운 문제
역시 한번에 제대로 읽어야 한다
시간 아낀다고 문제 대충 읽다가 풀이방법 잘못생각해서 날리는 시간이 훨씬 많다</p>
<p>문제 유형 자체는 빡구현이었고, 특별한 알고리즘을 이용하는 문제는 아니었음</p>
<h3 id="내가-작성한-코드-통과">내가 작성한 코드 (통과)</h3>
<pre><code class="language-python">def solution(s):
    zipped = &quot;&quot;
    zipped_list = []
    #snippet = []
    for i in range(1,len(s)+1):
        zipped = &quot;&quot;
        snippet = []
        for j in range(0, len(s), i):
            if j+i &lt; len(s):
                if not snippet:
                    snippet.append([1, s[j:j+i]])
                else:
                    if snippet[-1][1] == s[j:j+i]:
                        snippet[-1][0] += 1
                    else:
                        snippet.append([1, s[j:j+i]])
            else:
                if not snippet:
                    snippet.append([1, s[j:]])
                else:
                    if snippet[-1][1] == s[j:]:
                        snippet[-1][0] += 1
                    else:
                        snippet.append([1, s[j:]])
        #print(snippet)

        for num, val in snippet:
            if num != 1:
                zipped += str(num)
            zipped += val
        zipped_list.append(zipped)

    return min(list(map(len, zipped_list)))
</code></pre>
<ul>
<li>range 세번째 인자 이용</li>
<li>이중 for문으로 j+i가 len(s) 넘어가면 마지막 꼬다리라고 판단</li>
<li>바로 이전 스니펫이 같은거면 숫자 카운트, [숫자, 스니펫] 형태로 리스트에 추가하도록 함</li>
</ul>
<h3 id="생각보다-구현하기-까다로웠던-부분-바로-생각-안났던거">생각보다 구현하기 까다로웠던 부분 (바로 생각 안났던거)</h3>
<ul>
<li>길이 1부터 시작해서 문자열 길이까지 앞에서부터 잘라내는 작업</li>
<li>마지막에 남은 꼬다리(?) 처리 어떻게하지 싶었음</li>
<li>결국 if/else문으로 나눠서 구현</li>
</ul>
<h3 id="처음에-문제-잘못-이해했던-부분">처음에 문제 잘못 이해했던 부분</h3>
<ul>
<li>스니펫이 몇 번 등장하는지 카운트하는 딕셔너리로 접근했다가 잘못된걸 깨달음</li>
<li>예를들어 aabbaa면 길이 1로 잘랐을때 4a2b가 아니라, 2a2b2a가 나와야함</li>
<li>그나마 바로 알았어서 다행, 리스트의 [-1]을 이용해서 어찌저찌 다시 구현</li>
</ul>
<h1 id="2-순위-검색">2. 순위 검색</h1>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/72412">https://school.programmers.co.kr/learn/courses/30/lessons/72412</a></p>
<p>간단한듯 하면서 안 간단했던 문제
딕셔너리와 세트를 이용한 빡구현으로 해결이 되겠지 싶었지만..</p>
<h3 id="내가-제출한-코드-정확성-전부-통과--효율성-전부-실패">내가 제출한 코드 (정확성 전부 통과 / 효율성 전부 실패)</h3>
<pre><code class="language-python">def solution(info, query):
    dictionary = {&quot;-&quot;:[]}
    score_only = []

    for i, v in enumerate(info):
        info_parsed = v.split(&quot; &quot;)
        for j in range(4):
            if not dictionary.get(info_parsed[j]):
                dictionary[info_parsed[j]] = [i]
            else:
                dictionary[info_parsed[j]].append(i)
        score_only.append(int(info_parsed[4]))
        dictionary[&quot;-&quot;].append(i)

    #print(score_only)
    #info_parsed = [i.split(&quot; &quot;) for i in info]
    query_parsed = [q.split(&quot; and &quot;) for q in query]

    answer = []

    for q in query_parsed:
        result = set(dictionary[&quot;-&quot;])
        soulfood, score = q.pop().split(&quot; &quot;)
        score = int(score)
        q.append(soulfood)
        for cond in q:
            if cond == &quot;-&quot;:
                continue
            else:
                matched = set(dictionary[cond])
                result = set.intersection(result, matched)

        #print(score)

        answer.append(len([i for i in result if score_only[i] &gt;= score]))

    return answer</code></pre>
<h3 id="추측">추측</h3>
<ul>
<li>먼저 이분탐색으로 스코어를 걸러낸 다음에 검색해야하나 싶음</li>
<li>근데 그러면 처음에 딕셔너리 만드는 부분에서 문제가 생김 (매번 딕셔너리가 달라져야 되니까)</li>
</ul>
<h3 id="정답">정답</h3>
<p>이 문제는 2021년 카카오 신입공채 1차 코딩테스트 문제였음
<a href="https://tech.kakao.com/posts/420">https://tech.kakao.com/posts/420</a></p>
<h1 id="3-광고-삽입">3. 광고 삽입</h1>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/72414#fn1">https://school.programmers.co.kr/learn/courses/30/lessons/72414#fn1</a></p>
<h3 id="실패">실패</h3>
<p>분명히 백준에서 비슷한 유형을 본 거 같은데.. 이건 꼭 찾아봐야겠다
이분탐색으로 푼 거 같은데..</p>
<h3 id="정답-1">정답</h3>
<p>이 문제도 2021년 카카오 신입공채 1차 코딩테스트 문제였음
<a href="https://tech.kakao.com/posts/420">https://tech.kakao.com/posts/420</a></p>
<h1 id="오늘의-후기">오늘의 후기</h1>
<ul>
<li>차라리 구현하면서 미리 중간중간에 print()를 넣는건 어떨까 싶음</li>
<li></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[251003]]></title>
            <link>https://velog.io/@ooleem-generator/251003</link>
            <guid>https://velog.io/@ooleem-generator/251003</guid>
            <pubDate>Fri, 03 Oct 2025 17:30:09 GMT</pubDate>
            <description><![CDATA[<p>오늘 인스타그램 스토리에 한 매거진 글을 공유했다.</p>
<p>“취향이 실력이 되는 사회”</p>
<p>정말 공감하는 이야기라서 같이 읽었으면 좋겠다는 마음이었다. 이 글의 본문 중에 다음과 같은 내용이 있다.</p>
<p>“음악을 직접 만들지 않아도 클릭 몇 번이면 완성된 곡을 얻을 수 있다.”</p>
<p>확실히 Suno AI나 그림 생성 AI 같은 예술 활동을 해내는 AI들을 보면, 무언가를 창작한다는 개념이 사실 인류가 지금까지 만들어 놓은 예술이라는 빅데이터 안에서 생성하는 것에 지나지 않은가 하는 생각도 든다. 하지만, 결국 이 글에서 말하고 싶은 결론은 다음과 같다.</p>
<p>“기술이 아니라 취향이 실력이 되는 사회”</p>
<p>AI가 지금까지 인류가 만들어낸 모든 예술 작품들을 저장하고 있다 하더라도, 그 중에서 무엇이 좋다, 별로다라고 말할 수 있을까? 적어도 AI가 인간과 동일한 자아를 갖기 전까지는, “예술가들도 이제 끝이다”라는 소리가 들려올 정도로 AI가 범람하는 시대에서 예술가라는 자아를 지키는 방법은 자신만의 취향을 확고하게 다듬는 것이라고 생각한다.</p>
<p>사실 이런 생각을 3년 전쯤, 한창 열심히 음원을 내기 위해 음악에 몰두하고 있을 때 했었는데, 개발자로 전업을 준비하다 보니까 개발자의 관점에서 바라본 다른 생각이 든다.</p>
<p>음악과 같은 예술의 관점에서는 원문 그대로 말할 수 있다면, 개발에서는 “취향” 대신 “의도”라고 단어 하나만 살짝 바꿔서 말할 수 있지 않을까 싶다. </p>
<p>IDE의 위치를 프롬프트가 대체할 날이 곧 온다고 하면, “나는 과연 어떤 의도와 생각을 가지고 AI에게 명령할 것인가?” 이 고민이 곧 실력이 되지 않을까 하는 생각이 든다.</p>
<p>세상에는 정말 좋은 코드가 많고, 같은 문제에 대한 해법을 더 좋은 코드로 누군가 이미 만들어 놓았을 수도 있다. 하지만 내가 지금 눈앞에 직면한 문제를 나의 배경 지식을 바탕으로 내가 생각하는 최선의 방법대로 해결하고자 했을 때, 산출되는 코드는 누군가의 코드와 100% 일치하지 않는, 고유한 것이 될 거라고 생각한다. 그리고 그 코드에 담긴 생각과 의도가 날카로워질수록, 개발자의 “실력”이 더 늘어가는 것이라고 생각한다.</p>
<p>내 취향을 담는 음악가, 내 의도를 담는 개발자가 되어야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1991 트리 순회]]></title>
            <link>https://velog.io/@ooleem-generator/1991-%ED%8A%B8%EB%A6%AC-%EC%88%9C%ED%9A%8C</link>
            <guid>https://velog.io/@ooleem-generator/1991-%ED%8A%B8%EB%A6%AC-%EC%88%9C%ED%9A%8C</guid>
            <pubDate>Wed, 04 Jun 2025 14:55:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/462bb1a8-8425-4c65-b02a-fe6ca9515d0e/image.png" alt="">
드디어 마주한  거대한 장벽.. 그래프..</p>
<h2 id="아이디어-풀이법">아이디어 (풀이법)</h2>
<p>요구사항을 그대로 구현만 하면 된다.</p>
<h2 id="입력받기-전처리">입력받기 (전처리)</h2>
<p>각 노드 - 왼쪽 자식 노드 - 오른쪽 자식 노드 순서이므로,</p>
<pre><code>n = int(input())

tree = {}
preorder_stack = []
inorder_stack = []
postorder_stack = []

for _ in range(n):
    parent, left, right = input().split()
    visited = False
    tree[parent] = [left, right, visited]
</code></pre><p>트리는 일단 딕셔너리로 구현. (해당 노드 - 자식 노드 관계로 주어져서 직관적으로 딕셔너리로 해야겠다는 생각이 들었다)
스택은 하다 보니까 필요할 것 같아서 추가.</p>
<h2 id="구현">구현</h2>
<p>전위 / 중위 / 후위 각각</p>
<pre><code>
# 트리 방문 초기화
def tree_clear(tree):
    for node in tree.keys():
        tree[node][2] = False


# 전위 순회
def preorder(tree, node):
    if not tree[node][2]:
        tree[node][2] = True
        preorder_stack.append(node)

    if tree[node][0] != &quot;.&quot;:
        preorder(tree, tree[node][0])

    if tree[node][1] != &quot;.&quot;:
        preorder(tree, tree[node][1])


# 중위 순회
def inorder(tree, node):
    if tree[node][0] != &quot;.&quot;:
        inorder(tree, tree[node][0])

    if not tree[node][2]:
        tree[node][2] = True
        inorder_stack.append(node)

    if tree[node][1] != &quot;.&quot;:
        inorder(tree, tree[node][1])


# 후위 순회
def postorder(tree, node):
    if tree[node][0] != &quot;.&quot;:
        postorder(tree, tree[node][0])

    if tree[node][1] != &quot;.&quot;:
        postorder(tree, tree[node][1])

    if not tree[node][2]:
        tree[node][2] = True
        postorder_stack.append(node)


# 출력
preorder(tree, &quot;A&quot;)
print(&quot;&quot;.join(preorder_stack))
tree_clear(tree)
inorder(tree, &quot;A&quot;)
print(&quot;&quot;.join(inorder_stack))
tree_clear(tree)
postorder(tree, &quot;A&quot;)
print(&quot;&quot;.join(postorder_stack))
</code></pre><p>풀어놓고 나중에 확인해보니 - 스택을 이용한 재귀 -&gt; 이건 DFS다
라는 것을 알수있었다. (전위/중위/후위는 함수 내에서 순서만 달랐음)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1654 랜선 자르기]]></title>
            <link>https://velog.io/@ooleem-generator/1654-%EB%9E%9C%EC%84%A0-%EC%9E%90%EB%A5%B4%EA%B8%B0</link>
            <guid>https://velog.io/@ooleem-generator/1654-%EB%9E%9C%EC%84%A0-%EC%9E%90%EB%A5%B4%EA%B8%B0</guid>
            <pubDate>Wed, 28 May 2025 07:14:26 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/f5de5536-3b6d-4c51-8259-b679f1c34686/image.png" alt="">
이분탐색 연습문제 - 유독 이분탐색 유형이 어려워서 따로 정리하였다.</p>
<h2 id="오답-맨-처음-구현">오답) 맨 처음 구현</h2>
<pre><code>k, n = map(int, input().split())  # k는 10,000까지, n은 1,000,000까지
cables = [int(input()) for _ in range(k)]

cables.sort()

left = 0
right = cables[0]

while True:
    if left &gt; right:
        print(right)
        break
    mid = (left + right) // 2
    divided_cables = [cable // mid for cable in cables]  # 개수
    if (
        sum(divided_cables) &lt; n
    ):  # 너무 몇개 안나온다 싶으면 -&gt; 자르는 단위를 줄여야 한다
        right = mid - 1
    else:
        left = mid + 1</code></pre><h3 id="️-틀린-부분-1--left를-0으로-잡은-것">‼️ 틀린 부분 1 : left를 0으로 잡은 것</h3>
<p>이분탐색 대표 예시에서는 이분 탐색의 대상이 특정 리스트의 포인터였기 때문에, left를 0으로 설정한 거였음.
그거만 기억나서 left를 0으로 설정했으나, 이번에는 특정한 수를 찾는 것이기 때문에 left를 0으로 설정할 경우 ZeroDivisionError가 발생할 수 있음.
<img src="https://velog.velcdn.com/images/ooleem-generator/post/5ec7b47c-bcaf-4e2e-a1e1-02db2c86b24e/image.png" alt=""></p>
<h3 id="️-틀린-부분-2--right를-주어진-랜선의-최솟값으로-잡은-것">‼️ 틀린 부분 2 : right를 주어진 랜선의 최솟값으로 잡은 것</h3>
<p>이건 문제 이해를 잘못한 거였음.. 반드시 모든 선을 잘라야 할 필요는 없다..
<img src="https://velog.velcdn.com/images/ooleem-generator/post/e80498c9-009a-4bc9-9b0c-9f48fd042e63/image.png" alt="">
<img src="https://velog.velcdn.com/images/ooleem-generator/post/89b507cc-9c6f-4a3f-b3e5-8ca257bebf35/image.png" alt=""></p>
<h2 id="정답-위의-틀린-부분들-수정">정답 (위의 틀린 부분들 수정)</h2>
<pre><code>k, n = map(int, input().split())  # k는 10,000까지, n은 1,000,000까지
cables = [int(input()) for _ in range(k)]

cables.sort()

left = 1
right = cables[-1]

while True:
    if left &gt; right:
        print(right)
        break
    mid = (left + right) // 2
    divided_cables = [cable // mid for cable in cables]  # 개수
    if (
        sum(divided_cables) &lt; n
    ):  # 너무 몇개 안나온다 싶으면 -&gt; 자르는 단위를 줄여야 한다
        right = mid - 1
    else:
        left = mid + 1</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[3190 뱀]]></title>
            <link>https://velog.io/@ooleem-generator/3190-%EB%B1%80</link>
            <guid>https://velog.io/@ooleem-generator/3190-%EB%B1%80</guid>
            <pubDate>Wed, 28 May 2025 02:06:37 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/bb7505fc-bf28-4693-a5d4-d0f3a2d10b9e/image.png" alt="">
<img src="https://velog.velcdn.com/images/ooleem-generator/post/b7d53785-444f-4357-851c-11185ac8dcfc/image.png" alt=""></p>
<h2 id="아이디어">아이디어</h2>
<p>이번 문제는 특별한 아이디어를 요구하지 않았다. 얼마나 구현을 잘 하느냐의 문제</p>
<h2 id="구현">구현</h2>
<pre><code>from collections import deque
import sys

input = sys.stdin.readline

second = 0
snake = deque([(1, 1)])  # 첫번째가 행, 두번째가 열
apple = []
rotation = deque([(0, 1), (1, 0), (0, -1), (-1, 0)])
r_order = {}

n = int(input())
k = int(input())
for _ in range(k):
    a, b = map(int, input().split())
    apple.append((a, b))
l = int(input())
for _ in range(l):
    a, b = input().split()
    r_order[int(a)] = b

while True:
    second += 1
    snake_next = (snake[-1][0] + rotation[0][0], snake[-1][1] + rotation[0][1])

    if (n + 1 in snake_next) or (0 in snake_next) or (snake_next in snake):
        print(second)
        break

    snake.append(snake_next)

    if snake[-1] in apple:
        apple.remove(snake[-1])
    else:
        snake.popleft()

    if second in r_order:
        if r_order[second] == &quot;D&quot;:
            rotation.rotate(-1)
        else:
            rotation.rotate(1)</code></pre><p>임팩트가 있었던 부분은 deque의 rotate 메소드를 이용해서 방향 전환을 구현한 것 정도?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2812 크게 만들기]]></title>
            <link>https://velog.io/@ooleem-generator/2812-%ED%81%AC%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@ooleem-generator/2812-%ED%81%AC%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 27 May 2025 12:23:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/e42b6463-6569-45db-a3c6-93271c431d1a/image.png" alt=""></p>
<h2 id="아이디어">아이디어</h2>
<h3 id="아이디어-1">아이디어 1</h3>
<p>카테고리 분류가 그리디랑 스택이길래, 순간 &quot;맨 앞자리가 제일 큰 수를 <em><strong>선택</strong></em> 하면 되지 않을까?&quot; 라는 생각을 해버림..</p>
<h3 id="아이디어-2-챗지피티-스포일러">아이디어 2 (챗지피티 스포일러...)</h3>
<p>챗지피티가 스포해버림.. 결국 지워가는 게 맞았다 (지운 횟수를 카운트하면서)
지운 횟수 = pop을 한 횟수</p>
<h2 id="구현">구현</h2>
<h3 id="제출-1-아이디어-1으로-구현-메모리-초과">제출 1 (아이디어 1으로 구현, 메모리 초과)</h3>
<pre><code>n, k = map(int, input().split())
target = input()
choose_count = n - k
answer = []

def choose(idx, iter_count):
    global target
    global choose_count
    global answer
    searching_range = target[idx + 1 : len(target) - (choose_count - iter_count)]
    # print(searching_range)
    chosen_num = max(searching_range)
    answer.append(chosen_num)
    if choose_count == iter_count:
        return
    idx_iter = searching_range.index(chosen_num) + idx + 1 
    return choose(idx_iter, iter_count + 1)

choose(-1, 1)

[print(ans, end=&quot;&quot;) for ans in answer]</code></pre><p>앞에서부터 제일 큰 수를 골라서 추가하는 로직으로 가다 보니, 탐색할 범위를 슬라이싱해서 찾고 max 함수도 쓰고.. 난리가 났다.</p>
<h4 id="️-메모리-초과의-결정적-원인---재귀함수에서-슬라이싱-사용">‼️ 메모리 초과의 결정적 원인 - 재귀함수에서 슬라이싱 사용</h4>
<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/45b50b26-4cea-4b0f-862c-ba5548f48798/image.png" alt=""></p>
<p>슬라이싱은 대상의 특정 부분을 &#39;복사&#39;해서 새로 만들게 된다 -&gt; 저장공간이 새로 필요함!!</p>
<h4 id="-재귀함수-호출-자체도-메모리를-먹는다---함수-호출-스택">(+ 재귀함수 호출 자체도 메모리를 먹는다 -&gt; 함수 호출 스택)</h4>
<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/22658162-c17a-4a53-a89c-c7c60533fce7/image.png" alt=""></p>
<h3 id="제출-2-제출-1-수정-시간-초과">제출 2 (제출 1 수정, 시간 초과)</h3>
<pre><code>import sys

input = sys.stdin.readline

n, k = map(int, input().split())
number = input()
choose_count = n - k
chosed_count = 0
stack = []
idx = -1

while chosed_count &lt; choose_count:
    for i in range(idx + 1, len(number) - choose_count + chosed_count + 1):
        if i == idx + 1:
            stack.append((i, number[i]))
        else:
            if number[i] &gt; stack[-1][1]:
                stack.pop()
                stack.append((i, number[i]))
    idx = stack[-1][0]
    print(stack[-1][1], end=&quot;&quot;)
    chosed_count += 1</code></pre><p>쌩고생하면서 재귀함수도 빼고 슬라이싱도 빼고 했더니만.. 이번엔 시간초과..</p>
<h4 id="️-시간-초과의-원인---while문-안의-for-문-이중-순회">‼️ 시간 초과의 원인 - while문 안의 for 문 (이중 순회)</h4>
<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/aa3e93a7-e564-4ce8-ae36-9fb58125298c/image.png" alt=""></p>
<p>이제는 n에 가까운 범위를 이중탐색하는 방법은 웬만해선 안먹힌다고 봐야 한다..</p>
<p>이러고 나서.. GPT가 스포함..</p>
<h3 id="제출-3-아이디어-2로-구현-정답일-줄-알았으나-오답">제출 3 (아이디어 2로 구현, 정답..일 줄 알았으나 오답)</h3>
<pre><code>n, k = map(int, input().split())
number = input()

stack = []
erase_count = 0

for num in number:
    if erase_count == k:
        stack.append(int(num))
        # print(stack)
    else:
        if stack == []:
            stack.append(int(num))
            # print(stack)
        else:
            while True:
                if stack == [] or stack[-1] &gt;= int(num) or erase_count == k:
                    break
                stack.pop()
                erase_count += 1
                # print(stack)
            stack.append(int(num))
            # print(stack)

[print(numb, end=&quot;&quot;) for numb in stack]</code></pre><p>아이디어 그대로 구현해보니까 생각보다 테스트케이스에서 오답이 많이 나왔다.
특히 모든 과정에서 erase_count를 계속 확인하도록 하는 작업이 포인트였다.
(그래서 while문 안에 있는 조건문에 erase_count == k 조건이 달렸음)</p>
<p>그렇게 테스트케이스도 다 정답 나왔고 이제 제출하면 정답!! 인 줄 알았으나..</p>
<h3 id="제출-4-정답">제출 4 (정답)</h3>
<pre><code># 위 코드 마지막 print문 바로 위에
while erase_count &lt; k:
    stack.pop()
    erase_count += 1</code></pre><p>이걸 안넣어주면 연속된 숫자만 있는 경우 지우지를 않게 된다!!
그래서 맨 마지막에 erase_count가 k보다 작을 경우 안 지운 횟수만큼 stack에서 pop하는 과정을 추가.
드디어 정답.. 힘겹다..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2493 탑]]></title>
            <link>https://velog.io/@ooleem-generator/2493-%ED%83%91</link>
            <guid>https://velog.io/@ooleem-generator/2493-%ED%83%91</guid>
            <pubDate>Tue, 27 May 2025 11:51:09 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ooleem-generator/post/393bf3f1-94e6-434b-baa2-d0dde3e920e9/image.png" alt=""></p>
<h2 id="아이디어">아이디어</h2>
<p>이 문제가 왜 스택 문제인가?를 고민해야 아이디어가 보이는 문제.
탑이 맨 처음 것부터 하나하나 새로 들어온다고 생각하고 보면 아이디어가 보인다.
새로 탑이 지어질 경우, 만약 그 앞에 있는 탑이 새로 지어진 탑보다 작다면? 그 탑은 레이저를 맞을 일이 없게 된다 (새 탑이 가리니까)
이 점을 이용하여, 탑을 스택에 넣고, 스택 맨 위의 탑이 새로 들어올 탑보다 작다면 그 탑을 내보내는 작업을 반복하면 되겠다는 아이디어를 냈음.</p>
<h2 id="구현">구현</h2>
<h3 id="제출-1-시간-초과">제출 1 (시간 초과)</h3>
<pre><code>import sys

input = sys.stdin.readline

n = int(input())
towers = list(map(int, input().split()))
stack = [0]

for tower in towers:
    while stack[-1] &lt;= tower:
        stack.pop()
        if stack == []:
            print(0, end=&quot; &quot;)
            stack.append(tower)
            break
    else:
        print(towers.index(stack[-1]) + 1, end=&quot; &quot;)
        stack.append(tower)</code></pre><h4 id="️-시간-초과의-원인---index-함수">‼️ 시간 초과의 원인 - index 함수</h4>
<p>index 함수는 리스트를 전부 돌면서 해당하는 값의 인덱스를 찾아냄 : O(n)
따라서 O(n) 짜리 반복문 안에서 사용 시 바로 O(n^2)가 되어버린다. 조심!!</p>
<p>그렇다면 index 함수를 쓰지 않고도 인덱스를 찾아낼 수 있어야 한다.. 대안을 찾았다.</p>
<h3 id="제출-2-정답">제출 2 (정답)</h3>
<pre><code>import sys

input = sys.stdin.readline

n = int(input())
towers = list(map(int, input().split()))
stack = [0]

tower_idx_finder = {v: i for i, v in enumerate(towers)}

for tower in towers:
    while stack[-1] &lt;= tower:
        stack.pop()
        if stack == []:
            print(0, end=&quot; &quot;)
            stack.append(tower)
            break
    else:
        print(tower_idx_finder[stack[-1]] + 1, end=&quot; &quot;)
        stack.append(tower)</code></pre><h4 id="index-함수-회피-방법---딕셔너리-사용-value를-키-인덱스를-값으로">index 함수 회피 방법 -&gt; 딕셔너리 사용, value를 키, 인덱스를 값으로</h4>
<p>이 아이디어를 적용했더니 문제가 바로 풀렸다.
하지만 그때는 몰랐던 것이 있었으니...</p>
<h3 id="여담--사실-이-회피-방법은-완전한-방법이-아님">여담 : 사실 이 회피 방법은 완전한 방법이 아님</h3>
<h4 id="️-value를-키로-사용하기-때문에-value에-중복된-값이-있으면-제일-마지막에-할당한-인덱스로-덮어쓰게-됨">‼️ value를 키로 사용하기 때문에, value에 중복된 값이 있으면 제일 마지막에 할당한 인덱스로 덮어쓰게 됨!</h4>
<p>우연히 이번 문제는 높이가 같은 탑이 있을 경우 <strong><em>맨 오른쪽 탑에 레이저가 맞기 때문에</em></strong> 정답이 나올 수 있었다.
다른 회피 방법이 있을지는.. 계속 풀면서 생각해야겠다.</p>
]]></description>
        </item>
    </channel>
</rss>