<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Back to the Basics</title>
    <link>https://sora9z.tistory.com/</link>
    <description>언제나 기초로 돌아가자</description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 14:10:48 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>9Jaeng</managingEditor>
    <image>
      <title>Back to the Basics</title>
      <url>https://tistory1.daumcdn.net/tistory/4755398/attach/0b9ffb6531b4497591e31c0fb55f8985</url>
      <link>https://sora9z.tistory.com</link>
    </image>
    <item>
      <title>개발을 시작하고 4년째가 된 개발자의 회고</title>
      <link>https://sora9z.tistory.com/156</link>
      <description>&lt;h2&gt;개발 4년, 그리고 2026년의 변화&lt;/h2&gt;
&lt;p&gt;2022년 4월에 개발을 시작했고, 2026년 4월이 되면 어느덧 딱 4년이 된다.&lt;br&gt;4년이라는 시간이 흘렀지만, 유독 2026년에 들어서면서 많은 것이 달라졌다고 느낀다. 이직을 했고, 생활 패턴도 바뀌었고, 무엇보다도 코드를 직접 작성하는 시간보다 프롬프트로 코드를 만들어 가는 시간이 훨씬 많아졌다.&lt;/p&gt;
&lt;p&gt;개발을 처음 시작했을 때만 해도 AI는 딥러닝, 비전, 자연어처리처럼 하나의 기술 분야에 가까웠다. 그러다 개발을 시작한 지 1년에서 1년 반쯤 지났을 무렵, 처음으로 LLM이라는 개념을 접했다. 그리고 얼마 지나지 않아, 그 LLM은 어느새 내 일상과 업무 한가운데로 들어왔다.&lt;br&gt;지금 돌아보면, 개발자로서 보낸 지난 4년 중에서도 요즘이 가장 큰 변화의 시기인 것 같다.&lt;/p&gt;
&lt;h2&gt;첫 시작과 이직, 그리고 또 이직&lt;/h2&gt;
&lt;p&gt;첫 회사를 약 2년 4개월 다녔고, 두 번째 회사를 1년 정도 다닌 뒤 지금의 회사로 옮겼다. 그렇게 지난 4년 동안 두 번의 이직을 했다.&lt;/p&gt;
&lt;p&gt;첫 번째 회사는 개발자로서의 커리어를 시작한 곳이었다. 믿음직스럽고 좋은 시니어분들이 많았고, 이것저것 배우기에 정말 좋은 환경이었다. 어느 정도 규모가 있는 프로젝트가 어떻게 돌아가는지, 일정 수준 이상의 인원이 있는 조직에서는 프로젝트 관리와 협업이 어떻게 이루어지는지 배울 수 있었던 곳이다.&lt;/p&gt;
&lt;p&gt;하지만 경기가 나빠지면서, 시리즈 C 투자까지 받았던 회사도 결국 구조조정을 하게 되었고, 나 역시 이직을 하게 되었다.&lt;/p&gt;
&lt;p&gt;두 번째 회사는 규모는 훨씬 작았지만 보다 단단한 회사였다. 공공부문 교육을 주로 다루는 에듀테크 회사였다. 첫 번째 회사보다 개발자 수는 적었지만, 자율성은 더 컸다. 첫 회사에서 쌓은 경험을 발판 삼아 이곳에서는 조금 더 주체적으로 일을 해나가야 했다. NCP도 이곳에서 처음 써봤고, 데브옵스 담당자가 나가면서 백엔드 개발자가 맡아야 할 역할도 자연스럽게 늘어났다. 첫 회사에서는 데브옵스 팀이 탄탄했기 때문에 인프라 영역까지 깊게 볼 일이 많지 않았는데, 개인적으로는 그런 점이 좋은 경험이 되었다.&lt;/p&gt;
&lt;p&gt;다만 아쉬운 점도 있었다. 팀 내부의 분열, 잦은 인원 교체, 사람을 뽑기 위한 릴레이 면접, 인원 부족으로 인한 개발 일정 지연이 반복됐다. 게다가 공공부문 교육이라는 도메인 특성상 기술 스택도 다소 제한적이었고, 일을 하면서 재미를 느끼기 어려웠다. 무엇보다 개발을 하면서 점점 무기력해지는 내 모습이 싫었다. 결국 퇴사를 결심하게 되었고, 마침 이사로 인해 통근 거리까지 멀어진 것도 적지 않은 이유가 됐다.&lt;/p&gt;
&lt;p&gt;그리고 지금의 세 번째 회사.&lt;br&gt;나는 원래부터 데이터에 관심이 많았고, AI를 많이 활용하거나 AI와 가까운 회사에서 일해보고 싶었다. 여기에 금융 도메인에 대한 관심도 늘 있었다. 그런 점에서 지금 회사가 하는 일은 내게 꽤 매력적으로 다가왔다. 게다가 재택근무가 가능하다는 점도 좋았다.&lt;/p&gt;
&lt;p&gt;막상 와보니 함께 일하는 개발자분들도 첫 번째 회사 못지않게 좋은 분들이었고, 배울 점도 많았다. 동시에 두 번째 회사에서처럼 스스로 더 주도적으로 움직여야 하는 환경이기도 했다. 내게는 첫 번째 회사와 두 번째 회사의 장점이 함께 있는 곳처럼 느껴졌다. 아직 부족한 점은 많지만, 내가 원하던 분야에 더 가까운 만큼 오래 일하고 싶다는 생각이 든다.&lt;/p&gt;
&lt;h2&gt;재택근무가 내게 가져온 변화&lt;/h2&gt;
&lt;p&gt;나는 원래 재택근무를 하면 오히려 일을 더 많이 하게 되는 편이라고 생각했는데, 지금도 역시 그렇다. 주말에도, 새벽에도 일하고 있는 나를 발견하곤 한다.&lt;/p&gt;
&lt;p&gt;내가 재택을 원했던 가장 큰 이유는 두 가지였다.&lt;br&gt;첫째는 통근 시간, 둘째는 효율성이다.&lt;/p&gt;
&lt;p&gt;최근 천안으로 이사를 오면서 서울로 출퇴근하는 것이 쉬운 일이 아니게 되었다. 도어 투 도어로 편도 2시간이 걸리는데, 그 시간이 너무 아깝게 느껴졌다. 버스를 타기 위해 새벽에 일어나야 했고, 아무리 일찍 잠들려고 해도 퇴근 후 집에 돌아와 저녁을 먹고 조금 쉬다 보면 어느새 새벽이 되었다. 자는 시간은 점점 늦어지고, 피로는 계속 쌓였다. 다크서클은 눈 밑까지 내려오고, 체력도 점점 부족해진다는 생각이 들었다.&lt;/p&gt;
&lt;p&gt;돌이켜보면 통근만으로도 하루에 써야 할 에너지의 절반 가까이를 쓰고 있었던 것 같다. 그러다 보니 주말에는 정말 잠만 자고 늘어져 있는 나를 자주 발견했다. 그런데 재택을 하면서 통근에 쓰이던 시간이 사라지자, 무기력했던 상태가 조금씩 회복되고 있다. 오히려 개발에 대해 더 많이 생각하게 되고, 생각 자체도 이전보다 훨씬 긍정적으로 바뀌는 느낌이 든다.&lt;/p&gt;
&lt;p&gt;두 번째 이유는 효율성이다.&lt;br&gt;오피스에서 생활하다 보면 동료들과의 반쯤 강제적인 티타임이나, 업무 외적인 대화에 쓰이는 시간이 생각보다 많다. 물론 그런 시간들이 관계를 쌓는 데 도움이 되지 않는 것은 아니다. 하지만 일을 해야 하는 시간에 집중이 흐트러지는 순간들이 분명히 존재한다. 반면 집에서는 비교적 온전히 업무에만 집중할 수 있다. 그래서인지 퇴근 이후에도 자연스럽게 일을 이어가고 있는 나를 발견할 때가 많다.&lt;/p&gt;
&lt;p&gt;물론 재택이 모두에게 정답이라고 생각하지는 않는다. 하지만 적어도 내게는 통근으로 소모되던 에너지를 줄여주고, 더 오래 집중할 수 있게 해주는 방식이다. 업무 시간을 줄이지 않으면서도 나름의 삶과 행복의 균형을 맞출 수 있게 해준다. 그래서 나는 재택이 정말 필요하다고 느낀다.&lt;br&gt;이 좋은 환경을 유지할 수 있도록 회사가 더 잘 성장했으면 좋겠고, 나도 그 안에서 더 열심히 일하고 싶다.&lt;/p&gt;
&lt;h2&gt;ADD(AI Driven Development)의 시대&lt;/h2&gt;
&lt;p&gt;2026년에 내 삶의 방식에서 제일 큰 변화가 아닐까싶다.&lt;br&gt;GPT가 처음 등장했을 때부터 꽤 열심히 사용해왔다. Cursor가 처음 나왔을 때도 주변 동료들보다 빠르게 써보면서 AI와 함께 개발하는 방식에 익숙해지려고 했다. 하지만 지금처럼 AI를 자주, 그리고 깊숙하게 사용하는 때는 없었던 것 같다.&lt;/p&gt;
&lt;p&gt;지금 다니는 회사는 조직 차원에서도 AI 사용을 매우 강조하고 있고, 여러 종류의 AI 도구를 지원해준다. 그러다 보니 요즘의 나는 AI가 작성한 코드를 읽고, 이해하고, 프롬프트로 수정 요청을 하는 일을 계속 반복하고 있다. 내가 처음부터 끝까지 직접 코드를 작성하는 시간은 예전에 비해 확실히 줄어들었다. &lt;/p&gt;
&lt;p&gt;그래서 요즘은 DDD도, TDD도, GDD도 아닌 ADD(AI Driven Development)를 하고 있는 것 같다는 생각이 든다. 가끔 인스타그램 릴스에서 “AI 이전의 개발자”와 “AI 이후의 개발자”를 비교하는 영상을 보면, 이후의 개발자는 AI가 코드를 써줄 때까지 기다리고 있는 모습으로 그려진다. 그걸 보면서 웃으면서도, 한편으로는 “어? 저거 난데?” 싶은 생각이 들었다.&lt;/p&gt;
&lt;p&gt;실제로 AI는 내가 직접 짜는 것보다 훨씬 빠르게 코드를 작성하기도 하고, 때로는 더 나은 방법을 제안하기도 한다. 불과 반년 전까지만 해도 계속해서 수정 요청을 해야 하는 경우가 많았는데, 요즘은 그런 빈도도 꽤 줄었다. 어떤 날은 코드를 읽고 수정 요청만 하다가, 정작 내가 직접 한 줄도 작성하지 않는 날도 있다.&lt;/p&gt;
&lt;p&gt;그럴 때면 한편으로는 불안해진다.&lt;br&gt;이렇게 하다 보면 나중에 직접 짜라고 했을 때 정말 잘 짤 수 있을까?&lt;br&gt;요즘은 간단한 작업조차 귀찮아서 AI에게 맡기곤 하는데, 이러다가 결국 내가 다 잊어버리는 건 아닐까 하는 생각도 든다.&lt;/p&gt;
&lt;p&gt;하지만 또 생각해보면, 기술 혁명이 일어나는 과도기에는 늘 비슷한 걱정이 있었을 것이다. 컴퓨터가 처음 등장했을 때도, 계산기가 처음 보급되었을 때도 분명 사람들은 계산 능력이 떨어질까 걱정했을 것이다. 실제로 예전보다 암산이 느려졌을 수는 있어도, 그렇다고 계산 능력 자체가 완전히 사라진 것은 아니다. 필요하면 여전히 할 수 있다.&lt;/p&gt;
&lt;p&gt;코딩도 비슷하지 않을까 싶다.&lt;br&gt;직접 코드를 빠르게 쳐내는 능력은 예전보다 조금 둔해질 수 있어도, 문제를 이해하고 구조를 파악하고 읽고 판단하는 능력까지 사라지는 것은 아닐 것이다. 오히려 중요한 것은 얼마나 빨리 코드를 타이핑하느냐보다, 무엇을 만들고 어떤 방식으로 검증할지를 판단하는 능력일지도 모른다.&lt;/p&gt;
&lt;p&gt;그렇게 생각하다 보면 결국 이 변화는 받아들여야 하는 흐름이라는 생각이 든다. 사실상 AI가 완전히 없는 환경이 아니라면, 앞으로도 우리는 어디서든 AI와 함께 일하게 될 가능성이 높다. 자연재해로 데이터센터나 클라우드 시스템이 모두 멈추는 극단적인 상황이 아니라면 말이다.&lt;/p&gt;
&lt;p&gt;요즘에는 글도 비슷하다.&lt;br&gt;대충 의미만 전달되도록 먼저 써놓고, AI로 교정하는 일이 정말 많아졌다. 거의 대부분 그렇다고 해도 과장이 아니다. 글쓰기 능력 역시 코딩과 비슷한 고민을 남긴다. 내가 직접 다듬는 힘이 줄어드는 건 아닐까 하는 생각. 하지만 이 역시 시대의 변화 속에서 자연스럽게 받아들여야 하는 부분일지도 모르겠다.&lt;/p&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;지난 4년 동안 두 번의 이직을 했고, 개발자로서의 환경도 많이 달라졌다.&lt;br&gt;함께 일하는 조직도 바뀌었고, 생활 방식도 바뀌었고, 일하는 도구 자체도 바뀌었다. 특히 2026년에 들어와서는 “개발을 한다”는 말의 의미가 예전과는 많이 달라졌다고 느낀다.&lt;/p&gt;
&lt;p&gt;예전에는 내가 직접 얼마나 많이 구현하느냐가 더 중요하게 느껴졌다면, 지금은 AI와 함께 얼마나 잘 문제를 정의하고, 읽고, 판단하고, 수정해 나가느냐가 점점 더 중요해지고 있다.&lt;/p&gt;
&lt;p&gt;아마도 앞으로의 개발자는, 코드를 잘 짜는 사람인 동시에 AI를 잘 활용하는 사람이어야 할 것이다.&lt;br&gt;나 역시 그 변화에 맞춰 조금씩 적응해 가는 중이다.&lt;/p&gt;</description>
      <category>Books &amp;amp; Reviews/회고획오</category>
      <category>2026년 회고</category>
      <category>Ai</category>
      <category>AI driven development</category>
      <category>개발회고</category>
      <category>회고</category>
      <author>9Jaeng</author>
      <guid isPermaLink="true">https://sora9z.tistory.com/156</guid>
      <comments>https://sora9z.tistory.com/156#entry156comment</comments>
      <pubDate>Sun, 29 Mar 2026 12:56:08 +0900</pubDate>
    </item>
    <item>
      <title>[python] Python의 비동기 동작과 event loop</title>
      <link>https://sora9z.tistory.com/155</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬의 event loop에 대해 알아보자. Nodejs의 event loop 기반의 논블로킹 싱글 스레드 기반의 환경과 어떻게 다른 지도 간단하게 정리하였다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Python에서 비동기를 작성하는 방법(문법 레벨)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NodeJs나 Python이나 async await이라는 문법이 존재한다. 두 언어에서 각각 비동기 함수를 정의하고 실행하는 역할을 한다는 공통점이 있다. 문법은 같은데 무엇이 다를까?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 NodeJs는 non-blocking이 기본 실행 환경이고 Python은 기본적으로 synchronous 실행 모델 이라는 실행 철학에 차이가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 async/ await 문법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python의 async/await은 구문은 파이썬 &quot;언어 차원&quot;에서 제공되는 문법이며(&lt;a href=&quot;https://peps.python.org/pep-0492/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PEP492&lt;/a&gt;) 이를 기반으로 동시성 코드를 작성할 수 있도록 지원한다. 이를 지원하는 대표적인 라이브러리가 asyncio 모듈이다 &lt;a href=&quot;https://docs.python.org/ko/3/library/asyncio.html&quot;&gt;Python의 asyncio 공식문서&lt;/a&gt; &amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;async await은 python 3.5부터&amp;nbsp;도입되었다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;async&lt;br /&gt;&lt;/b&gt;&lt;b&gt;async def&lt;/b&gt; 를 사용하여 함수를&amp;nbsp;&amp;nbsp;&lt;a href=&quot;https://docs.python.org/ko/3/glossary.html#term-coroutine&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;coroutine function(코루틴 함수)&lt;/a&gt;으로 정의한다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;async def로&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 정의한&amp;nbsp;&lt;/span&gt;&lt;/span&gt;코루틴 함수를 실행하면 실제 실행이 즉시 호출되는 것이 아니라 &lt;b&gt;coroutine object&lt;/b&gt;라는 것이 생성된다. 이 객체는 await 가능한 객체(awaitable)의 한 종류이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 coroutine이란 실행 도중 &lt;b&gt;await&lt;/b&gt; 지점에서 잠시 멈췄다가, event loop가 다른 작업을 처리한 뒤(event loop에 제어권을 넘기는 행위) 다시 이어서 실행될 수 있는 &lt;b&gt;함수 실행 단위이다.&lt;/b&gt; 코루틴에 대해서는 다음 세션에서 조금 더 확인해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;await&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;await 키워드(&lt;a href=&quot;https://docs.python.org/3/reference/expressions.html#await-expression&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;6.4. Await expression&lt;/a&gt;)는&amp;nbsp; awaitable 객체가 완료될 때까지 현재 coroutine의 실행을 일시 중단하며, coroutine function 내부에서만 사용할 수 있다.&lt;br /&gt;await을 만나면 현재 실행 중인 coroutine은 중단되고, 실행 제어권이 event loop로 넘어간다. event loop는 실행 가능한 다른 작업을 스케줄링하여 실행한다. 이후 await 대상 작업이 완료되면, 중단되었던 coroutine은 다시 실행 가능한 상태가 되어 event loop에 의해 재개된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;NodeJs와의 비교해 보자&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NodeJs에서 async 키워드는 해당 함수가 Promise를 반환하는 비동기 함수임을 의미한다. await 키워드는 Promise가 resolve 될 때까지 현재 async 함수의 실행을 일시 중단한다. 문법적인 형태는 매우 유사하다.&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;NodeJs에서는 async 함수가 호출되는 순간 Promise가 생성되고 함수 실행이 바로 시작된다(eager execution) 된다. 즉, 함수 호출 자체가 실행의 시작이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Python에서는 coroutine 함수를 호출해도 실제 실행은 바로 시작되지 않는다. 대신 couroutine object만 생성될 뿐이며, 이 객체는 await 되거나 event loop에 의해 스케줄링되기 전까지는 실행되지 않는다.&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;NodeJs&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773497357147&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function hello() {
  console.log(&quot;start&quot;); // 함수 호출 즉시 실행됨
  await Promise.resolve();
  console.log(&quot;end&quot;);
}

hello(); // 호출하는 순간 &quot;start&quot;가 바로 출력됨
// await은 실행을 시작하는 역할이 아니라 Promise 결과를 기다리는 역할&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NodeJs에서는 async 함수를 호출하는 순간 함수 body가 실행되며, await을 만나면 Promise가 resolve 될 때까지 실행이 잠시 중단된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Python&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773497432908&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import asyncio

async def hello():
    print(&quot;start&quot;)  # 아직 실행되지 않음
    await asyncio.sleep(0)
    print(&quot;end&quot;)

coro = hello()  # hello() 호출 시 coroutine object만 생성된다

# 실제 실행은 event loop에서 시작됨
asyncio.run(hello())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python에서는 coroutine 함수 호출 시 실행이 시작되는 것이 아니라 &lt;b&gt;coroutine object만 생성된다.&lt;/b&gt;&lt;br /&gt;실제 실행은 await 하거나 asyncio.create_task() 등을 통해 event loop에 등록될 때 시작된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 coroutine은?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://docs.python.org/ko/3/glossary.html#term-coroutine&quot;&gt;coroutine(코루틴)&lt;/a&gt;은 실행 도중 중단되었다가 이후 다시 실행될 수 있는 함수 실행 단위이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 함수는 호출되면 끝까지 실행되지만, coroutine은 await 지점에서 실행을 잠시 멈추고 event loop에게 제어권을 넘길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;coroutine의 특성을 정리해 보면 아래와 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. coroutine은 &quot;중단 가능한 함수&quot;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. coroutine function은 호출하면 바로 실행되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 호출 결과로 coroutine object가 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 이 coroutine object는 await 되거나 event loop에 의해 스케줄링될 때 실행이 시작된다.&lt;/p&gt;
&lt;pre id=&quot;code_1773497906036&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async def fetch_data():
    print(&quot;start&quot;)
    return &quot;data&quot;

coro = fetch_data()  # 아직 실행되지 않음 (coroutine object 생성)

await coro           # 이 시점에 실제 실행 시작&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, coroutine은 &quot;실행 가능한 작업을 표현하는 객체&quot;에 가깝다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 coroutine 실행 등록 (Task, gather)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명한 것처럼 Python의 coroutine은 함수를 호출한다고 바로 실행되지 않는다. coroutine 함수 호출 시 &lt;b&gt;coroutine object만 생성&lt;/b&gt;되며, 실제 실행을 위해서는 event loop에 의해 스케줄링되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;asyncio.run()&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asyncio.run()은 coorutine을 실행하기 위해 가장 기본적인 진입점이다. 내부적으로 event loop를 생성하고 coroutine을 실행한 뒤 event loop를 종료한다.&lt;/p&gt;
&lt;pre id=&quot;code_1773555474858&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import asyncio

async def hello():
    print(&quot;start&quot;)
    await asyncio.sleep(1)
    print(&quot;end&quot;)

asyncio.run(hello())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서 hello()는 coorutine object를 생성하고, asyncio.run()이 이를 event loop에서 실행하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 Python asyncio 프로그램의 최상위 실행 지점에서 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;asyncio.create_task()&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.python.org/3/library/asyncio-task.html#creating-tasks&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;create_task()&lt;/a&gt;는 coroutine을 &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://docs.python.org/3/library/asyncio-task.html#asyncio.Task&quot;&gt;Task&lt;/a&gt;로 감싸고 event loop에 등록하여 실행을 시작하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task는 event loop가 스케줄링할 수 있는 실행 단위이다.&lt;/p&gt;
&lt;pre id=&quot;code_1773544471715&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import asyncio

async def work():
    print(&quot;start&quot;)
    await asyncio.sleep(5)
    print(&quot;end&quot;)

async def main():
    task = asyncio.create_task(work()) # coroutine을 task로 만들어 event loop에 등록
    await task

asyncio.run(main())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;create_task()를 사용하면 coroutine이 event loop의 스케줄링 대상이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;asyncio.gather&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.python.org/3/library/asyncio-task.html#asyncio.gather&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gather&lt;/a&gt;를 사용하여 여러 coroutine을 동시에 실행(concurrent execution) 하고 결과를 모으기 위해 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1773544791396&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;&quot;&quot;
여러 coroutine을 동시에 실행하여 결과를 모으기 위해 aynsio.gather()를 사용할 수 있다.
work(1),work(2),work(3)을 동시에 실행하고 모든 작업이 완료되면 리스트로 반환
&quot;&quot;&quot;

import asyncio

async def work(n):
    await asyncio.sleep(2)
    return n

async def main():
    results = await asyncio.gather(
        work(1),
        work(2),
        work(3)
    )
    print(results)

asyncio.run(main())&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;즉, Python에서는 아래와 같은 흐름을 가져간다.&lt;/p&gt;
&lt;pre id=&quot;code_1773555666848&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async def &amp;rarr; coroutine 생성
asyncio.run &amp;rarr; event loop 시작
create_task / gather &amp;rarr; coroutine을 실행 대상으로 등록
event loop &amp;rarr; scheduling&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;cooperative scheduling&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python의 asyncio는 cooperative scheduling 방식으로 동작한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업이 강제로 중단되는 것이 아니라 coroutine이 await 지점에서 스스로 제어권을 event loop에 반환한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행의 흐름은 아래와 같다&lt;/p&gt;
&lt;pre id=&quot;code_1773544975169&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;coroutine 실행
    &amp;darr;
await 만남
    &amp;darr;
event loop에게 제어권 반환
    &amp;darr;
다른 coroutine 실행
    &amp;darr;
await 작업 완료
    &amp;darr;
다시 실행(resume)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식 덕분에 하나의 thread에서도 여러 작업을 번갈아서 실행하면서 I/O작업을 효율적으로 처리할 수 있다.&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;2. Python 비동기의 내부 동작 (런타임 레벨)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서는 Python에서 비동기 코드를 작성하는 문법적인 방법을 다뤘다. 이번에는 실제고 asyncio가 런타임에서 어떻게 동작하는지 간단히 알아보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 루프(Event Loop)란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.python.org/3/library/asyncio-eventloop.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;event loop&lt;/a&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;event loop는 개념적으로 &lt;b&gt;while loop 형태의 스케줄러&lt;/b&gt;라고 볼 수 있다. while loop를 돌면서 task를 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1773558697358&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while tasks:
    for task in tasks:
        execute(task)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Task queue(Ready Queue)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;task queue는 실행될 준비가 된 task들이 들어있다. 이 task들은 실행될 준비가 되면 이 queue에 추가된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;439&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/esIMzq/dJMcaiigP5s/T772u2aMPULUzTrsrLlX8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/esIMzq/dJMcaiigP5s/T772u2aMPULUzTrsrLlX8k/img.png&quot; data-alt=&quot;https://python.plainenglish.io/build-your-own-event-loop-from-scratch-in-python-da77ef1e3c39&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/esIMzq/dJMcaiigP5s/T772u2aMPULUzTrsrLlX8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FesIMzq%2FdJMcaiigP5s%2FT772u2aMPULUzTrsrLlX8k%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;704&quot; height=&quot;221&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;439&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://python.plainenglish.io/build-your-own-event-loop-from-scratch-in-python-da77ef1e3c39&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Sleeping queue(scheduled heap)&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;1400&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2oCjY/dJMcagdH0Uy/alUenCj1JkRxjZuEowEsa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2oCjY/dJMcagdH0Uy/alUenCj1JkRxjZuEowEsa1/img.png&quot; data-alt=&quot;https://python.plainenglish.io/build-your-own-event-loop-from-scratch-in-python-da77ef1e3c39&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2oCjY/dJMcagdH0Uy/alUenCj1JkRxjZuEowEsa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2oCjY%2FdJMcagdH0Uy%2FalUenCj1JkRxjZuEowEsa1%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;710&quot; height=&quot;302&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://python.plainenglish.io/build-your-own-event-loop-from-scratch-in-python-da77ef1e3c39&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;timer가 있는 작업이 들어가는 queue도 있다. event loop가 돌 때마다 deadline이 완료된 작업이 있는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 타이머가 다 된 작업이 있다면 ready queue로 옮긴다.&lt;/p&gt;
&lt;pre id=&quot;code_1773559313592&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while ready or sleeping:
    if not ready:
       deadline, task = sleeping.pop()
       delta = deadline - time.time()
       if delta &amp;lt; 0 # check if deadline is over
	       ready.append(task)
    
    # execute task
    while ready:
        task = ready.popleft()
        task()&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;size18&quot;&gt;&lt;b&gt;Selector (I/O Multiplexing)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 비동기 프로그햄에서는 네트워크 요청, 파일 읽기 등의 I/O작업이 훨씬 많이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 I/O작업을 처리하기 위해 asyncio는 I/O multiplexing을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 blocking I/O 코드를 생각해 보자&lt;/p&gt;
&lt;pre id=&quot;code_1773559919545&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data = socket.recv(1024)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 데이터가 도착할 때까지 프로그램 실행을 멈춘다. 즉, blocking I/O이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 asyncio는 non-blocking socker과 selctor를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;selector의 역할은 특정 socket에 &lt;b&gt;읽기(read) 또는 쓰기(write) 이벤트가 발생하면 이를 감지하는 것&lt;/b&gt;이다. 내부적으로는 epoll, kqueue, select 등의 OS 기능을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pythob에는 이를 selctors 모듈이 제공한다.&lt;/p&gt;
&lt;pre id=&quot;code_1773560040437&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import selectors

selector = selectors.DefaultSelector()
selector.register(sock, selectors.EVENT_READ)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;event loop는 내부적으로 selctor를 사용하여 I/O 이벤트를 감지한다. 즉, I/O 이벤트가 발생하면 해당 coroutine이 다시 실행될 준비 상태가 된다 (ready queye로 등록)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AHAOy/dJMb99MnzrS/rpS5mQwNPykvdo6Oi4KwBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AHAOy/dJMb99MnzrS/rpS5mQwNPykvdo6Oi4KwBK/img.png&quot; data-alt=&quot;https://python.plainenglish.io/build-your-own-event-loop-from-scratch-in-python-da77ef1e3c39&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AHAOy/dJMb99MnzrS/rpS5mQwNPykvdo6Oi4KwBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAHAOy%2FdJMb99MnzrS%2FrpS5mQwNPykvdo6Oi4KwBK%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;698&quot; height=&quot;271&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://python.plainenglish.io/build-your-own-event-loop-from-scratch-in-python-da77ef1e3c39&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;asyncio event loop 내부 흐름&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;1400&quot; data-origin-height=&quot;671&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TEPjc/dJMcag5QiFA/41lG8aOKprTvYbaKrCkGHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TEPjc/dJMcag5QiFA/41lG8aOKprTvYbaKrCkGHk/img.png&quot; data-alt=&quot;https://python.plainenglish.io/build-your-own-event-loop-from-scratch-in-python-da77ef1e3c39&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TEPjc/dJMcag5QiFA/41lG8aOKprTvYbaKrCkGHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTEPjc%2FdJMcag5QiFA%2F41lG8aOKprTvYbaKrCkGHk%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;716&quot; height=&quot;343&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;671&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://python.plainenglish.io/build-your-own-event-loop-from-scratch-in-python-da77ef1e3c39&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;간단하게 정리하자면 event loop는 &lt;b&gt;timer, I/O 이벤트, 실행 가능한 task를 반복적으로 확인하면서 coroutine 실행을 스케줄링한다&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773560679088&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while True:

    # timer 확인
    move_scheduled_tasks_to_ready()

    # I/O 이벤트 확인
    events = selector.select()

    # ready queue 실행
    while ready:
        task = ready.popleft()
        task.run()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vent loop는 이 구조들을 계속 확인하면서 coroutine 실행을 조정한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python의 비동기 모델은 event loop, task scheduling, selector 기반 I/O 이벤트 감지가 동작하면서 구현된다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773561120623&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;coroutine
&amp;darr;
Task
&amp;darr;
event loop
&amp;darr;
ready queue / scheduled queue
&amp;darr;
selector (I/O 이벤트 감지)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;event loop는 이러한 구조를 반복적으로 확인하면서 실행 가능한 coroutine을 스케줄링하고, I/O 이벤트가 발생하면 해당 coroutine을 다시 실행한다.&lt;/p&gt;
&lt;p data-end=&quot;1885&quot; data-start=&quot;1797&quot; data-ke-size=&quot;size16&quot;&gt;이러한 방식 덕분에 Python에서는 NodeJs와 같이&amp;nbsp;&lt;b&gt;하나의 thread에서도 여러 I/O 작업을 효율적으로 처리할 수 있는 비동기 프로그래밍 모델&lt;/b&gt;을 구현할 수 있다.&lt;/p&gt;</description>
      <category>Programming Languages/Python</category>
      <category>asyncawait</category>
      <category>asyncio</category>
      <category>AsyncProgramming</category>
      <category>CooperativeScheduling</category>
      <category>coroutine</category>
      <category>EventLoop</category>
      <category>IOmultiplexing</category>
      <category>NonBlockingIO</category>
      <category>Python</category>
      <category>TaskScheduling</category>
      <author>9Jaeng</author>
      <guid isPermaLink="true">https://sora9z.tistory.com/155</guid>
      <comments>https://sora9z.tistory.com/155#entry155comment</comments>
      <pubDate>Sun, 15 Mar 2026 16:54:31 +0900</pubDate>
    </item>
    <item>
      <title>[typescript] Awaited type에 대해 알아보자</title>
      <link>https://sora9z.tistory.com/154</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 사용하게 된 Awaited type에 대해 알아보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서 링크 :&amp;nbsp;&lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/utility-types.html#awaitedtype&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.typescriptlang.org/docs/handbook/utility-types.html#awaitedtype&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1768647132163&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Documentation - Utility Types&quot; data-og-description=&quot;Types which are globally included in TypeScript&quot; data-og-host=&quot;www.typescriptlang.org&quot; data-og-source-url=&quot;https://www.typescriptlang.org/docs/handbook/utility-types.html#awaitedtype&quot; data-og-url=&quot;https://www.typescriptlang.org/docs/handbook/utility-types.html#awaitedtype&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/utility-types.html#awaitedtype&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.typescriptlang.org/docs/handbook/utility-types.html#awaitedtype&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Documentation - Utility Types&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Types which are globally included in TypeScript&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.typescriptlang.org&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;h3 data-ke-size=&quot;size23&quot;&gt;Awaited type이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Awaited type은 await 했을 때 실제로 얻는 값의 타입을 의미한다.&amp;nbsp; 이 타입은 typescript 4.5 버전에 새로 추가된 타입이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 4.5 버전 이전에는 await 했을 때 실제 얻는 값의 타입을 확인할 수 없었을까? 한다면 그것은 아니다. 아래의 예시와 같이 4.5 이후나 이전에도 중첩된 Promise까지 실제 resolve 되는 결괏값 추론은 정상적으로 되었다&lt;/p&gt;
&lt;pre id=&quot;code_1768651712761&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type E = Promise&amp;lt;number&amp;gt;;
type F = Promise&amp;lt;Promise&amp;lt;number&amp;gt;&amp;gt;;
type G = Promise&amp;lt;Promise&amp;lt;Promise&amp;lt;number&amp;gt;&amp;gt;&amp;gt;;
type H = Promise&amp;lt;PromiseLike&amp;lt;Promise&amp;lt;number&amp;gt;&amp;gt;&amp;gt;;

declare const e: E;
declare const f: F;
declare const g: G;
declare const h: H;

async function testFunc(){
	const r1 = await e; // type 추론 number
	const r2 = await f; // type 추론 number
	const r3 = await g; // type 추론 number
	const r4 = await h; // type 추론 number
}&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;이 타입이 추가되었던&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#the-awaited-type-and-promise-improvements&quot;&gt;4.5 버전의 릴리즈 노트&lt;/a&gt;를 확인해 보면 이 타입이 도입된 배경을 알 수 있는데, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Promise를 재귀적으로 완전히 해제(unwrapping)했을 때 실제로 남는 타입을 제대로 추론할 수 있도록 도입된 타입이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;릴리즈 노트의 예시를 4.4 버전과 4.5 버전에서 각각 확인을 해보면 4.4 버전은 아래와 같은 type errror를 확인할 수 있고 4.5 버전은 result의 타입 추론을 확인할 수 있다.&lt;/span&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;pre id=&quot;code_1768652020459&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;declare function MaybePromise&amp;lt;T&amp;gt;(
  value: T
): T | Promise&amp;lt;T&amp;gt; | PromiseLike&amp;lt;T&amp;gt;;

async function test(): Promise&amp;lt;[number, number]&amp;gt; {
  const result = await Promise.all([
    MaybePromise(1),
    MaybePromise(2),
  ]);

  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4.4 버전&lt;/p&gt;
&lt;pre id=&quot;code_1768651893156&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Type '[number | Promise&amp;lt;1&amp;gt;, number | Promise&amp;lt;2&amp;gt;]' is not assignable to type '[number, number]'.
  Type 'number | Promise&amp;lt;1&amp;gt;' is not assignable to type 'number'.
    Type 'Promise&amp;lt;1&amp;gt;' is not assignable to type 'number'.(2322)
const result: [number | Promise&amp;lt;1&amp;gt;, number | Promise&amp;lt;2&amp;gt;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4.5 버전&lt;/p&gt;
&lt;pre id=&quot;code_1768651998529&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const result: [number, number]&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;위와 같은 에러가 발생한 이유는 typescript 4.5 이전에는 Promise.all 타입 정의 자체가 튜플 내부 요소의 Promise 해제를 재귀적으로&amp;nbsp; 표현하지 못했기 때문이다. MaybePromise &amp;lt;T&amp;gt;는 런타임에서는 결국 T로 resolve 되지만, 당시의 TypeScript 타입 시스템은 Promise.all이 반환하는 튜플 구조의 객 요소까지 Promise를 해제하지 못했다. 그 결과 [number, number]가 아닌 [number | Promise&amp;lt;number&amp;gt;, number | Promise&amp;lt;number&amp;gt;] 로 추론되어 타입 에러가 발생하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typeScript 4.5에서는 Awaited&amp;lt;T&amp;gt; 타입이 도입되면서 &amp;ldquo;Promise를 완전히 해제했을 때의 최종 타입&amp;rdquo;을 재귀적으로 추론할 수 있게 되었고, 이를 기반으로 Promise.all, Promise.race 등의 타입 정의가 같이 개선되었다. &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/microsoft/TypeScript/pull/45350&quot;&gt;관련 PR&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 타입은 이전 typescript에서 추론하지 못했던 복잡한 Promise 구조를 재귀적으로 unwrapping 하여 최종적으로 얻는 값을 추론하기 위해 도입하였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언제 어떻게 사용하는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 사용하는 방식은 함수의 return type을 사용해야 할 때 이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 아래와 같이 함수가 있다고 했을 때&lt;/p&gt;
&lt;pre id=&quot;code_1768653793273&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function fetchUser() {
  return { id: 1, name: &quot;jaeng&quot; };
}

// 다른 곳에서 이 함수의 결과 타입이 필요
type User = ReturnType&amp;lt;typeof fetchUser&amp;gt;;
// ❌ 추론 타입 Promise&amp;lt;{ id: number; name: string }&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하게 되면 추론 타입은 Promise가 된다. 내가 원하던 추론 타입은 { id: number; name: string }이다. 이럴 때 Awaited를 사용하면 원하는 타입 추론이 가능하다&lt;/p&gt;
&lt;pre id=&quot;code_1768653914277&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type User = Awaited&amp;lt;ReturnType&amp;lt;typeof fetchUser&amp;gt;&amp;gt;;
// { id: number; name: string }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 TypeORM을 사용할 때, 레포지토리의 쿼리 결과 타입을 서비스 레이어에서 재사용하되 엔티티를 그대로 API DTO로 노출하지 않기 위해 레포지토리 반환 타입을 서비스 내부 타입으로 정의할 때에도 Awaited&amp;lt;ReturnType&amp;lt;&amp;hellip;&amp;gt;&amp;gt;를 유용하게 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같이 함수의 반환타입을 정의할 때에도 사용할 수 있고 DTO 등을 만들 때에도 사용할 수도 있겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/utility-types.html#returntypetype&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ReturnType&lt;/a&gt;이란, 함수 선언에 명시된 반환 타입을 가져온다. 링크된 공식문서의 예제를 확인해 보자..!&lt;/p&gt;</description>
      <category>Programming Languages/Typescript &amp;amp; NestJS</category>
      <category>Awaited</category>
      <category>JavaScript</category>
      <category>returntype</category>
      <category>Type</category>
      <category>typeScript</category>
      <category>타입스크립트</category>
      <author>9Jaeng</author>
      <guid isPermaLink="true">https://sora9z.tistory.com/154</guid>
      <comments>https://sora9z.tistory.com/154#entry154comment</comments>
      <pubDate>Sat, 17 Jan 2026 22:03:52 +0900</pubDate>
    </item>
    <item>
      <title>[NestJS] Monorepo로 구축하기 2 : 모노레포 구축 with NestJs 공식문서</title>
      <link>https://sora9z.tistory.com/153</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이전 포스팅에서는 &lt;a href=&quot;https://sora9z.tistory.com/150&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[[NestJS] Monorepo로 구축하기 1 : pnpm workspace]&lt;/a&gt; pnpm에 대하 알아보았다.&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://docs.nestjs.com/cli/monorepo&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.nestjs.com/cli/monorepo&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756650551957&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Documentation | NestJS - A progressive Node.js framework&quot; data-og-description=&quot;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&quot; data-og-host=&quot;docs.nestjs.com&quot; data-og-source-url=&quot;https://docs.nestjs.com/cli/monorepo&quot; data-og-url=&quot;https://docs.nestjs.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/UHwGG/hyZCZOQmfj/JEmHPnEQKcGMsMnW47wmVk/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/cli/monorepo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.nestjs.com/cli/monorepo&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/UHwGG/hyZCZOQmfj/JEmHPnEQKcGMsMnW47wmVk/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429');&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;Documentation | NestJS - A progressive Node.js framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.nestjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJs에서 모노레포를 만드는 것은 어렵지 않다. Monorepo로 만들기 전에 먼저 standard nest js 프로젝트를 만들어주어야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;nest project 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #18181a; color: #f7f7f7; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;nest new monorepo-project

# pnpm을 사용할 것이기 때문에 pnpm선택&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해당 프로젝트로 이동해서 프로젝트를 확인해보자&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1756650811726&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;├── eslint.config.mjs
├── nest-cli.json
├── package.json
├── pnpm-lock.yaml
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 &lt;b&gt;Standard Mode&lt;/b&gt;의 nest js 프로젝트 구조이다. 앱이 하나만 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;monorepo로 구성할 때와 tsconfig, nest-cli가 조금 다른데, 그것은 아래에서 설명하겠다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Workspace&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJs의 monorepo는 &lt;b&gt;workspace&lt;/b&gt;라는 개념이 있다. workspace는 여러 앱을 관리하는 하나의 최상위 컨테이너 개념이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 workspace의 구조는 아래와 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;application&lt;/b&gt; : 실행 가능한 완전한 nestjs앱. main.ts를 갖는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;library&lt;/b&gt; : 단독으로 실행 불가능한 공유 라이브러리들이 이곳에서 관리된다. main.ts는 없고 index.ts를 갖는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 그리고 공통 설정 파일인 package.json, tsconfig.json, nest-cli.json 등을 갖는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이제 여기에 우리가 모노레포 구조로 만들 application을 추가해 보자&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #18181a; color: #f7f7f7; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;nest generate app admin-app

nest generate app cyber-edue&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;auth라는 공통 라이브러리를 추가해 보았다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #18181a; color: #f7f7f7; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt; nest g library auth&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;library를 생성하면 prefix를 설정하라는 메시지가 뜬다. 기본은 app인데, 나는 @lib로 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 prefix(alias)는 나중에 다른 application에서 lib을 import 할 때 @lib/auth로 import할 수 있게 해 준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nest cli를 사용하여 구축하면 자동으로 프로젝트 구조를 아래와 같이 만들어준다&lt;/p&gt;
&lt;pre id=&quot;code_1756653205988&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;├── apps
│   ├── admin-app
│   │   ├── src
│   │   │   ├── admin-app.controller.spec.ts
│   │   │   ├── admin-app.controller.ts
│   │   │   ├── admin-app.module.ts
│   │   │   ├── admin-app.service.ts
│   │   │   └── main.ts
│   │   ├── test
│   │   │   ├── app.e2e-spec.ts
│   │   │   └── jest-e2e.json
│   │   └── tsconfig.app.json
│   ├── cyber-edue
│   │   ├── src
│   │   │   ├── cyber-edue.controller.spec.ts
│   │   │   ├── cyber-edue.controller.ts
│   │   │   ├── cyber-edue.module.ts
│   │   │   ├── cyber-edue.service.ts
│   │   │   └── main.ts
│   │   ├── test
│   │   │   ├── app.e2e-spec.ts
│   │   │   └── jest-e2e.json
│   │   └── tsconfig.app.json
│   └── monorepo-project
│       ├── src
│       │   ├── app.controller.spec.ts
│       │   ├── app.controller.ts
│       │   ├── app.module.ts
│       │   ├── app.service.ts
│       │   └── main.ts
│       ├── test
│       │   ├── app.e2e-spec.ts
│       │   └── jest-e2e.json
│       └── tsconfig.app.json
├── eslint.config.mjs
├── libs
│   └── auth
│       ├── src
│       │   ├── auth.module.ts
│       │   ├── auth.service.spec.ts
│       │   ├── auth.service.ts
│       │   └── index.ts
│       └── tsconfig.lib.json
├── nest-cli.json
├── package.json
├── pnpm-lock.yaml
├── tsconfig.build.json
└── tsconfig.json&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 Standard Mode의 설정 파일의 설정이 조금 다르다고 하였다. 무엇이 다른지 확인해 보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 보면 여러 설정파일이 있다. 컴파일 옵션, 빌드 옵션 등을 설정해 주는 &lt;b&gt;tsconfig.json&lt;/b&gt;과 nestJs 프로젝트의 프로젝트 구성, 빌드, 배포에 필요한 metadata를 보관하는 &lt;b&gt;nest-cli.json &lt;/b&gt;이 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[tsconfig.json]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Standard Mode&lt;/p&gt;
&lt;pre id=&quot;code_1756653471990&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;module&quot;: &quot;commonjs&quot;,
    &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;sourceMap&quot;: true,
    &quot;outDir&quot;: &quot;./dist&quot;,
    &quot;baseUrl&quot;: &quot;./&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
  }
}&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;- Monorepo Mode&lt;/p&gt;
&lt;pre id=&quot;code_1756653546238&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;module&quot;: &quot;commonjs&quot;,
    &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;sourceMap&quot;: true,
    &quot;outDir&quot;: &quot;./dist&quot;,
    &quot;baseUrl&quot;: &quot;./&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;paths&quot;: {      // ---&amp;gt; 이게 추가됨
      &quot;@lib/auth&quot;: [  // 정확한 경로 매핑
        &quot;libs/auth/src&quot;
      ],
      &quot;@lib/auth/*&quot;: [ // 하위 파일들도 매핑
        &quot;libs/auth/src/*&quot;
      ]
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;monorepo의 paths 옵션이 추가된 것을 볼 수 있다. (&lt;a href=&quot;https://www.typescriptlang.org/tsconfig/#paths&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.typescriptlang.org/tsconfig/#paths)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;path는 lib를 생선 하면서 prefix를 설정해 주었을 때 tsconfig에 자동으로 설정되는 옵션이다. paths 옵션을 사용하여 @lib/auth로 libs/auth/src를 import 하도록 alias를 추가해 준다. baseUrl이 있다면 baseUrl을 기준으로, 없다면 해당 taconfig가 있는 경로를 기준으로 조회 경로를 매핑한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 아래와 같이 경로를 깔끔하게 보이게 할 수 있고 공유 프로젝트임을 확실히 할 수 있다&lt;/p&gt;
&lt;pre id=&quot;code_1756654054478&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 길고 상대경로 대신
import { AuthService } from '../../../libs/auth/src/auth.service';

// 깔끔한 alias 사용
import { AuthService } from '@lib/auth';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[nest-cli.json]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Standard Mode&lt;/p&gt;
&lt;pre id=&quot;code_1756653459066&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;$schema&quot;: &quot;https://json.schemastore.org/nest-cli&quot;,
  &quot;collection&quot;: &quot;@nestjs/schematics&quot;,
  &quot;sourceRoot&quot;: &quot;src&quot;,
  &quot;compilerOptions&quot;: {
    &quot;deleteOutDir&quot;: true
  }
  // projects section 없음 &amp;larr; 단일 앱이므로 불필요
}&lt;/code&gt;&lt;/pre&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;- Monorepo Mode&lt;/p&gt;
&lt;pre id=&quot;code_1756653565412&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;$schema&quot;: &quot;https://json.schemastore.org/nest-cli&quot;,
  &quot;collection&quot;: &quot;@nestjs/schematics&quot;,
  &quot;sourceRoot&quot;: &quot;apps/monorepo-project/src&quot;,
  &quot;compilerOptions&quot;: {
    &quot;deleteOutDir&quot;: true,
    &quot;webpack&quot;: true,
    &quot;tsConfigPath&quot;: &quot;apps/monorepo-project/tsconfig.app.json&quot;
  },
  &quot;monorepo&quot;: true,
  &quot;root&quot;: &quot;apps/monorepo-project&quot;,
  &quot;projects&quot;: { // &amp;larr; 여기서 각 프로젝트 메타데이터 정의
    &quot;admin-app&quot;: {
      &quot;type&quot;: &quot;application&quot;,
      &quot;root&quot;: &quot;apps/admin-app&quot;,
      &quot;entryFile&quot;: &quot;main&quot;,
      &quot;sourceRoot&quot;: &quot;apps/admin-app/src&quot;,
      &quot;compilerOptions&quot;: {
        &quot;tsConfigPath&quot;: &quot;apps/admin-app/tsconfig.app.json&quot;
      }
    },
    &quot;auth&quot;: {
      &quot;type&quot;: &quot;library&quot;,
      &quot;root&quot;: &quot;libs/auth&quot;,
      &quot;entryFile&quot;: &quot;index&quot;,
      &quot;sourceRoot&quot;: &quot;libs/auth/src&quot;,
      &quot;compilerOptions&quot;: {
        &quot;tsConfigPath&quot;: &quot;libs/auth/tsconfig.lib.json&quot;
      }
    },
    &quot;cyber-edue&quot;: {
      &quot;type&quot;: &quot;application&quot;,
      &quot;root&quot;: &quot;apps/cyber-edue&quot;,
      &quot;entryFile&quot;: &quot;main&quot;,
      &quot;sourceRoot&quot;: &quot;apps/cyber-edue/src&quot;,
      &quot;compilerOptions&quot;: {
        &quot;tsConfigPath&quot;: &quot;apps/cyber-edue/tsconfig.app.json&quot;
      }
    },
    &quot;monorepo-project&quot;: {
      &quot;type&quot;: &quot;application&quot;,
      &quot;root&quot;: &quot;apps/monorepo-project&quot;,
      &quot;entryFile&quot;: &quot;main&quot;,
      &quot;sourceRoot&quot;: &quot;apps/monorepo-project/src&quot;,
      &quot;compilerOptions&quot;: {
        &quot;tsConfigPath&quot;: &quot;apps/monorepo-project/tsconfig.app.json&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 nest-cli.json을 비교해 보자 뭔가 많이 바뀐 것이 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nest-cli.json의 구성은 global section, 각 project의 metadata를 정의하는 projects section 두 가지로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;global section에서 차이를 보면 sourceRoot가 다르고 compile option 외에 여러 옵션이 추가되었다. 하나씩 간단하게 살펴보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;sourceRoot&lt;/b&gt;는 standard mode에서는 root source code를 가리키고, monorepo 구조에서는 default project를 가리킨다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nest build, nest start 등이 project name 없이 호출될 때 실행 할 default project를 명시하는 것이다. 필요 없다면 제거해도 된다. 대신 package.json에 nest build 프로젝트 네임을 명실해 주어야 한다&lt;/p&gt;
&lt;pre id=&quot;code_1756655240969&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 기본 설정이 있을 때
nest start  # &amp;rarr; sourceRoot의 프로젝트 실행

# 기본 설정 제거 후
nest start  # &amp;rarr; 에러! 프로젝트명 필요&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;compilerOptions에서는&lt;/b&gt; webpack, tsConfigPath 옵션이 추가되었다. webpack은 compiler를 webpack으로 사용한다는 의미이다. false이면 tsc를 기본으로 사용한다. 이 두 컴파일러의 차이는 아래에 간단하게 정리하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tsConfigPath 옵션은 sourceRoot와 비슷하게 monorepo only, nest build나 nest start 등이 project name 없이 노출될 때 가리키는 taconfig.json의 경로를 가리킨다. 이것 또한 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;필요 없다면 제거해도 된다. 대신 package.json에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;nest build 프로젝트 네임을 명실해 주어야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;root&lt;/b&gt;도 monorepo인 경우 사용되는 옵션이며 default project의 root project를 가리킨다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;webpack vs tsc&lt;br /&gt;-&amp;nbsp;tsc는&amp;nbsp;순수 typescript&amp;nbsp;compiler로&amp;nbsp;ts를&amp;nbsp;js로&amp;nbsp;컴파일한다&lt;br /&gt;-&amp;nbsp;파일&amp;nbsp;구조가&amp;nbsp;그대로&amp;nbsp;유지된다&lt;br /&gt;-&amp;nbsp;모듈&amp;nbsp;해석이&amp;nbsp;단순함&lt;br /&gt;-&amp;nbsp;tsc는&amp;nbsp;각&amp;nbsp;파일을&amp;nbsp;독립적으로&amp;nbsp;컴파일하므로&amp;nbsp;패키지&amp;nbsp;간&amp;nbsp;의존성&amp;nbsp;추적이&amp;nbsp;어렵다&lt;br /&gt;-&amp;nbsp;webpack은&amp;nbsp;번들링과&amp;nbsp;complie을&amp;nbsp;동시에&amp;nbsp;수행한다&lt;br /&gt;-&amp;nbsp;복잡한&amp;nbsp;모듈&amp;nbsp;해석(alias,&amp;nbsp;path&amp;nbsp;mapping 등)&lt;br /&gt;-&amp;nbsp;HMR(Hot&amp;nbsp;Module&amp;nbsp;Replacement) 지원&lt;br /&gt;- &amp;nbsp;@libs/common&amp;nbsp;등&amp;nbsp;복잡한&amp;nbsp;import문을&amp;nbsp;처리하는데&amp;nbsp;필요하다&lt;br /&gt;-&amp;nbsp;전체&amp;nbsp;dependency&amp;nbsp;graph를&amp;nbsp;분석해서&amp;nbsp;필요한&amp;nbsp;모든&amp;nbsp;모듈을&amp;nbsp;포함한다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;monorepo인 경우 &lt;b&gt;project section&lt;/b&gt;을 갖는다.&amp;nbsp; (참고로 각 옵션이 의미하는 바는 이 &lt;a href=&quot;https://docs.nestjs.com/cli/monorepo#global-compiler-options&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;NestJs 공식 문서&lt;/a&gt;를 참고하자) 각 프로젝트마다 위에서 해주었던 옵션들을 설정해 줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 &lt;b&gt;package.json&lt;/b&gt;도 확인해 보면 아래 jest 설정 부분이 다른 것을 볼 수 있다&lt;/p&gt;
&lt;pre id=&quot;code_1756655754828&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Standard Mode
&quot;jest&quot;: {
  &quot;rootDir&quot;: &quot;src&quot;,                    // &amp;larr; src만 테스트 대상
  &quot;coverageDirectory&quot;: &quot;../coverage&quot;   // &amp;larr; 상대 경로
}

// Monorepo Mode

&quot;jest&quot;: {
  &quot;rootDir&quot;: &quot;.&quot;,                      // &amp;larr; 프로젝트 전체 루트
  &quot;coverageDirectory&quot;: &quot;./coverage&quot;,   // &amp;larr; 절대 경로
  &quot;roots&quot;: [                           // &amp;larr; 테스트 탐색 범위
    &quot;&amp;lt;rootDir&amp;gt;/apps/&quot;,
    &quot;&amp;lt;rootDir&amp;gt;/libs/&quot;
  ],
  &quot;moduleNameMapper&quot;: {                // &amp;larr; alias 매핑
    &quot;^@lib/auth(|/.*)$&quot;: &quot;&amp;lt;rootDir&amp;gt;/libs/auth/src/$1&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 수동으로 alias를 변경해야 한다면 package.json의 jest 설정도 변경해 주어야 한다&lt;/p&gt;
&lt;pre id=&quot;code_1756655796190&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// tsconfig.json
&quot;paths&quot;: {
  &quot;@auth&quot;: [&quot;libs/auth/src&quot;],
  &quot;@auth/*&quot;: [&quot;libs/auth/src/*&quot;]
}

// package.json의 moduleNameMapper
&quot;moduleNameMapper&quot;: {
  &quot;^@auth(|/.*)$&quot;: &quot;&amp;lt;rootDir&amp;gt;/libs/auth/src/$1&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;library를 application에 적용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 만든 공통 라이브러리를 각 application에서 사용하는 방법은 이&lt;a href=&quot;https://docs.nestjs.com/cli/libraries#using-libraries&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 공식문서&lt;/a&gt;에 잘 설명되어있다. 여기서는 간략하게만 정리해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nestjs의 다른 모듈을 사용하는 것과 다를 것 없이 간단하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;library를 사용하고자 하는 application의 module.ts에서 import해주면 된다. 만약 그 application에서 전역적으로 사용하고자 한다면 root module인 app.module.ts 에서 Import를 해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1757225706822&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { AdminAppController } from './admin-app.controller';
import { AdminAppService } from './admin-app.service';
import { AuthModule } from '@lib/auth';

@Module({
  imports: [AuthModule],
  controllers: [AdminAppController],
  providers: [AdminAppService],
})
export class AdminAppModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 잘 보면 @lib/auth로 import가 되었다. nestjs는 tsconfig 를 통해 path mapping을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 각 application의 tsconfig에 path alias를 설정해주었는데 path mapping에 대해 잘 알아보지 않고 했다가 Import가 계속 깨지는 이슈가 있었다. 조심해서? 사용하자&lt;/p&gt;
&lt;pre id=&quot;code_1757225341768&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; &quot;paths&quot;: {
      &quot;@lib/auth&quot;: [
        &quot;libs/auth/src&quot;
      ],
      &quot;@lib/auth/*&quot;: [
        &quot;libs/auth/src/*&quot;
      ]
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;library 적용은 간단하고 직관적이고 쉽다(path mapping을 꼬이지 않게 잘 해준다면)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 Import를 하고 admin-app을 빌드 하면 nestJs는모듈 해석을 자동으로 처리하고 import를 한 AuthModule코드까지 빌드 산출문레 포함한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 모노레포의 기본 컴파일러서 webpack이면 단일 번들로(하나의 js로) 묶고, tsc라면 파일 단위로 출력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;libs/auth자체를 별도 패키지처럼 별도로 빌드를 해주지는 않지만 앱 번들에 포함되기 때문에 배포가 가능하다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;불필요 프로젝트 제거&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트 구조를 보면, 프로젝트 이름인 monorepo-project가 앱으로 구축되어 있다. 아마 모노레포 구조로 만들면서 생성된 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 이름을 갖는 app을 필요 없어서 제거하였다. 그럼, 위에서 설정되어 있던 global 설정을 변경해 주어야 한다 제거해도 되고, default로 실행할 app으로 대체할 수 있다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마치며&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이렇게 직접 공식문서를 보고 모노레포로 구축을 하였다. 그런데 운영을 하다보니 빌드 순서라던가, 소나큐브, 테스트 등을 추가하면서 빌드가 느려지는 현상들에 불편함을 느끼게 되었고 &quot;터보레포&quot; 라는 모노레포 관리 도구를 추가하게 되었다.&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;</description>
      <category>Programming Languages/Typescript &amp;amp; NestJS</category>
      <category>MonoRepo</category>
      <category>NestJS</category>
      <category>nestjs monorepo</category>
      <category>nestjs 모노레포</category>
      <category>tsconfig</category>
      <category>모노레포</category>
      <author>9Jaeng</author>
      <guid isPermaLink="true">https://sora9z.tistory.com/153</guid>
      <comments>https://sora9z.tistory.com/153#entry153comment</comments>
      <pubDate>Mon, 1 Sep 2025 01:03:58 +0900</pubDate>
    </item>
    <item>
      <title>[CONFERENCE] 2025-TOSS-MAKERS 정리</title>
      <link>https://sora9z.tistory.com/152</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;25년 토스 메이커스 컨퍼런스를 다녀왔다. 우리 팀에서 운 좋게 두 명이 추첨이 되어 다녀오게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 3일간 진행을 하였고 day1은 product day, day2는 design day day3는 engineering day이며 나는 day3에 참석하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 회사에서 하는 컨퍼런스는 2023 우아콘을 다음으로 두 번째 참석해 본다. 이번 컨퍼런스를 통해 토스의 개발 파트는 어떤 문제를 갖고 있는지, 이 문제를 해결하기 위해 어떻게 하는지 살짝 볼 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2896&quot; data-origin-height=&quot;1530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kWjeW/btsPDqWUV4n/rl1THI1gsT1on4Y08RzskK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kWjeW/btsPDqWUV4n/rl1THI1gsT1on4Y08RzskK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kWjeW/btsPDqWUV4n/rl1THI1gsT1on4Y08RzskK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkWjeW%2FbtsPDqWUV4n%2Frl1THI1gsT1on4Y08RzskK%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;2896&quot; height=&quot;1530&quot; data-origin-width=&quot;2896&quot; data-origin-height=&quot;1530&quot;/&gt;&lt;/span&gt;&lt;/figure&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;b&gt;1. 20년 레거시를 넘어 미래를 준비하는 시스템 만들기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;2. 현장 결제 서비스의 분산 트랜잭션 관리학 개론&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 수천개의 API/배치 서버를 하나의 설정 체계로 관리하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 가맹점은 변함없이 결제창 시스템 전면 재작성하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. LLM내제화 전략&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. 확장성과 회복 탄력성을 갖춘 결제 시스템 만들기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7. 레거시 정산 개편기 : 신규 시스템 투입 여정부터 대규모 배치 운영 노하우까지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세션 별 간략 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 토스 컨퍼런스는 우아콘에 비해 더욱 정신이 없었던 것 같다. 우아콘은 점심시간을 별도로 줬었는데 토스는 별도 점심시간을 위한 시간도 없었고 컨퍼런스를 들으랴 정리하랴 조금 정신이 없었다. 그래도 조금이라도 적었던 것을 정리해 보았다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 20년 레거시를 넘어 미래를 준비하는 시스템 만들기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세션은 20년이나 된 서비스를 인수받고 복잡한 구조를 개선했던 경험, 어떤 문제가 있었고 어떻게 해결했는지에 대한 내용을 다룬다.&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;토스 페이먼츠가 2020.08월에 20년이나 된 서비스를 인수받았을 때 아래와 같은 문제들이 있었다&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;소스코드 관리가 안되어있는 상태로, 어떤 코드가 최신 코드인지 불분명&amp;nbsp;&lt;/li&gt;
&lt;li&gt;인프라 자동화 도구 미존재로 수백 대의 서버 관리를 수작업으로 진행&amp;nbsp;&lt;/li&gt;
&lt;li&gt;네트워크 라우팅 구성 개선 필요
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 서버마다 라우팅 구성을 갖고 있어서 서버마다 구성을 해주어야 함&lt;/li&gt;
&lt;li&gt;서버마다 구성이 다른 경우가 발생함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 합류하는 팀원이 비즈니스 임팩트를 낼 수 있도록 구조 개선이 필요했음&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;해결&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인프라 자동화를 위한 AWS 사용 및 devops 팀을 구성하여 인프라 관리를 조직화&lt;/li&gt;
&lt;li&gt;문제를 단계적인 해결
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;github 도입을 통한 소스코드 관리&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 어떤 코드가 최신인지 알 수 없는 상황에서 깃헙을 바로 사용하는 것이 어려웠고 한다. 이를 해결하기 위해&amp;nbsp; 현재 배포되어 있는 코드가 최신코드라고 인식을 하고 disassemble 된 코드를 찾아서 배포를 하는 방식으로 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌드 표준화 및&amp;nbsp; MSA 아키텍처로 전환 쿠버네티스 도입
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결제서비스는 결제서버와 어드민 두 개면 되는 것 아닌가? 싶을 수 있지만, 150개의 service가 있었고&amp;nbsp; 개발 단계별 배포 환경 없었기 때문에 MSA의 전환이 필요했음 이를 통한 고가용성 인프라를 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;자동화된 코드 업데이트 체계 확보
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 변경을 감지하여 자동으로 pr을 만들어주도록 자동화&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서비스안정성 성능 고도화&lt;br /&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;1% 단위로 트래픽 보내도록 하였고 특정 조건에 따라 점차적으로 %를 늘려가는 방식으로 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IvtC4/btsPDF0PCkB/7aVe8zHffk6eOZKBFnRPX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IvtC4/btsPDF0PCkB/7aVe8zHffk6eOZKBFnRPX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IvtC4/btsPDF0PCkB/7aVe8zHffk6eOZKBFnRPX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIvtC4%2FbtsPDF0PCkB%2F7aVe8zHffk6eOZKBFnRPX0%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;445&quot; height=&quot;672&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라스트마일 성능을 높이기 위한 HTTP3, TLS3&lt;/li&gt;
&lt;li&gt;멀티 플랙싱, 멀티 캐싱을 사용한 성능 최적화&lt;/li&gt;
&lt;li&gt;&amp;nbsp;보안강화
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AI를 활용한 보안 이벤트 분석&lt;/li&gt;
&lt;li&gt;민감한 API E2E 암호화 및 HTTP3, TLS1.3&lt;/li&gt;
&lt;li&gt;컨테이너 런타임 보안&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W4nTG/btsPD0Keuvi/NrNxyraV26dKujJcJl808K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W4nTG/btsPD0Keuvi/NrNxyraV26dKujJcJl808K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W4nTG/btsPD0Keuvi/NrNxyraV26dKujJcJl808K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW4nTG%2FbtsPD0Keuvi%2FNrNxyraV26dKujJcJl808K%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;688&quot; height=&quot;598&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 약 1분 걸리던 쿼리 결과 -&amp;gt; 1초 이내에 응답&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 최근 3개월 데이터 조회 -&amp;gt; 최근 5년 이내의 데이터 조회 가능&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. &lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;현장 결제 서비스의 분산 트랜잭션 관리학 개론&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 세션은 토스 현장결제 서비스에서 어떻게 분산 트랜잭션을 관리하는지 소개한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;분산 환경에서 일관성과 정합성을 관리하는 방법에 대해 다룬다. 2 Phase commit과 같은 방식이 아닌 새로운 방식을 소개하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;해당 세션은 문제를 정의하고 이를 해결한 방안에 집중한 발표로 진행되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;483&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9314f/btsPDDIH6Xt/xF29Bck8B9NoxYz4jw8AD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9314f/btsPDDIH6Xt/xF29Bck8B9NoxYz4jw8AD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9314f/btsPDDIH6Xt/xF29Bck8B9NoxYz4jw8AD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9314f%2FbtsPDDIH6Xt%2FxF29Bck8B9NoxYz4jw8AD0%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;644&quot; height=&quot;351&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;483&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 해결을 위해 필요한 세 가지로 정의한다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 복잡한 상황을 나타낼 수 있는 표현모델 : 트리구조&lt;/h4&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-filename=&quot;blob&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cz12vP/btsPF7g6uwR/qteeTKVNKsDDx7mw9kgInk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cz12vP/btsPF7g6uwR/qteeTKVNKsDDx7mw9kgInk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cz12vP/btsPF7g6uwR/qteeTKVNKsDDx7mw9kgInk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcz12vP%2FbtsPF7g6uwR%2FqteeTKVNKsDDx7mw9kgInk%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;640&quot; height=&quot;418&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;564&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;read timeout , network유실 등 정의되지 않은 에러도 존재하기 때문에&lt;b&gt;성공, 실패, 알 수 없음으로&lt;/b&gt; 분류해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 Success/Fail로는 복잡성을 표현 할 수 없다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;일관성을 누가 어떻게 보장할까? :&amp;nbsp; 각 노드가 보장 정책을 갖는다&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;973&quot; data-origin-height=&quot;647&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XyVJv/btsPHdHVoze/Dp9XVx0LZ9oiLEdfhiKo4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XyVJv/btsPHdHVoze/Dp9XVx0LZ9oiLEdfhiKo4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XyVJv/btsPHdHVoze/Dp9XVx0LZ9oiLEdfhiKo4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXyVJv%2FbtsPHdHVoze%2FDp9XVx0LZ9oiLEdfhiKo4k%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;650&quot; height=&quot;432&quot; data-origin-width=&quot;973&quot; data-origin-height=&quot;647&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transaction이 늘어나면 분기는 더 많아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면, 트랜젝션이 한 개라면 성공/실패/알 수 없음 3가지 조합이 되겠고 transaction이 두 개 라면 3x3 = 9개가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜젝션 간 불일치도 문제가 된다. 위의 그림에서 본다면&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(내가 잘 이해했는지는 모르겠지만), 결제 승인이 났는데 토스 할인은 success이고 토스 포인트 사용은 fail이다. 토스 머니는 사용했는지 unknown이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토스 머니 UNKNOWN은 어떻게 처리해야 할까?&lt;/li&gt;
&lt;li&gt;포인트 사용 FAIL은 어떻게 처리해야 할까?&lt;/li&gt;
&lt;li&gt;누가 이런 결정을 내려야 할까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각&amp;nbsp;노드는&amp;nbsp;보정&amp;nbsp;정챙을&amp;nbsp;자체적으로&amp;nbsp;갖도록&amp;nbsp;해야 한다&lt;/b&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;942&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuXWfP/btsPGIuBotL/rePp54lvVC5seotoe659sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuXWfP/btsPGIuBotL/rePp54lvVC5seotoe659sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuXWfP/btsPGIuBotL/rePp54lvVC5seotoe659sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuXWfP%2FbtsPGIuBotL%2FrePp54lvVC5seotoe659sk%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;532&quot; height=&quot;491&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;942&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;일관성을 언제까지 보장해양할까? :ASAP 하지만 최종적 일관성 추구&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api 트랜젝션 안에서 일관성을 보장할 수 있을까?&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;&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;b&gt;모든 서비스는 추상화된 노드이다&lt;/b&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.&lt;span&gt;수천 개의 API/배치 서버를 하나의 설정 체계로 관리하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;단순 복사 붙여 넣기가 아닌 한 번의 클릭으로 개발자의 인프라 접근성을 높인 과정에 대한 발표 세션&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 문제: 설정 관리의 복잡성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;초기 문제 상황&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYJyz2/btsPFs6RBc2/e0wxGfyd64iLdHNKPrq90K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYJyz2/btsPFs6RBc2/e0wxGfyd64iLdHNKPrq90K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYJyz2/btsPFs6RBc2/e0wxGfyd64iLdHNKPrq90K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYJyz2%2FbtsPFs6RBc2%2Fe0wxGfyd64iLdHNKPrq90K%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;728&quot; height=&quot;399&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/li&gt;
&lt;li&gt;&lt;b&gt;인프라 장애까지 갈 뻔함&lt;/b&gt;: 설정 오류가 전체 시스템 장애로 확산될 위험&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;421&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJjloG/btsPEpcd4Um/uthgZYOm8TkHg3SvvfWXHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJjloG/btsPEpcd4Um/uthgZYOm8TkHg3SvvfWXHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJjloG/btsPEpcd4Um/uthgZYOm8TkHg3SvvfWXHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJjloG%2FbtsPEpcd4Um%2FuthgZYOm8TkHg3SvvfWXHk%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;602&quot; height=&quot;334&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;421&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결책 1: 오버레이 아키텍처&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;579&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfJOC8/btsPEuSbJHQ/eEOfosSzNy1xOeVpNijnt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfJOC8/btsPEuSbJHQ/eEOfosSzNy1xOeVpNijnt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfJOC8/btsPEuSbJHQ/eEOfosSzNy1xOeVpNijnt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfJOC8%2FbtsPEuSbJHQ%2FeEOfosSzNy1xOeVpNijnt1%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;748&quot; height=&quot;419&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오버레이 아키텍처의 핵심 개념&lt;/h3&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;/li&gt;
&lt;li&gt;&lt;b&gt;위에 있을수록 우선순위 낮게&lt;/b&gt;: 상위 레이어가 하위 레이어를 오버라이드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;요구사항에 따라 계속 바뀌고 구축한다&lt;/b&gt;: 동적 설정 변경 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한계점과 추가 해결책&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오버레이 아키텍처의 한계:&lt;/b&gt;&lt;/p&gt;
&lt;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;b&gt;중복 제거 힘듦&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특정 값만 환경에 따라&lt;/b&gt; 변경하기 어려움&lt;/li&gt;
&lt;/ul&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;1069&quot; data-origin-height=&quot;524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qPfPA/btsPEEmG5FA/q6A6tnxejgYg9AHUQBf0z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qPfPA/btsPEEmG5FA/q6A6tnxejgYg9AHUQBf0z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qPfPA/btsPEEmG5FA/q6A6tnxejgYg9AHUQBf0z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqPfPA%2FbtsPEEmG5FA%2Fq6A6tnxejgYg9AHUQBf0z1%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;681&quot; height=&quot;334&quot; data-origin-width=&quot;1069&quot; data-origin-height=&quot;524&quot;/&gt;&lt;/span&gt;&lt;/figure&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;모든 요구사항을 만족할 수 있는 유연한 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1081&quot; data-origin-height=&quot;597&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zc5lG/btsPEv4y1hp/mJu7JDLsd7Unyy90AZItNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zc5lG/btsPEv4y1hp/mJu7JDLsd7Unyy90AZItNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zc5lG/btsPEv4y1hp/mJu7JDLsd7Unyy90AZItNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZc5lG%2FbtsPEv4y1hp%2FmJu7JDLsd7Unyy90AZItNK%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;681&quot; height=&quot;376&quot; data-origin-width=&quot;1081&quot; data-origin-height=&quot;597&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;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;/li&gt;
&lt;li&gt;&lt;b&gt;단순이 최고다, 위험이 클수록 단순해야 한다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Jenkins 기반 솔루션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Jenkins 선택 이유:&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;1045&quot; data-origin-height=&quot;667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMD7aW/btsPErA3bev/UXN3plJ8GLCcMuWFX3YjFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMD7aW/btsPErA3bev/UXN3plJ8GLCcMuWFX3YjFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMD7aW/btsPErA3bev/UXN3plJ8GLCcMuWFX3YjFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMD7aW%2FbtsPErA3bev%2FUXN3plJ8GLCcMuWFX3YjFK%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;700&quot; height=&quot;447&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;667&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/li&gt;
&lt;li&gt;검증된 오픈소스 도구&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JobDSL 플러그인 어댑터 개발:&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;1326&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KSgNr/btsPFBW1DbZ/reNdxvTqOrLRpeHXBrklc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KSgNr/btsPFBW1DbZ/reNdxvTqOrLRpeHXBrklc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KSgNr/btsPFBW1DbZ/reNdxvTqOrLRpeHXBrklc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKSgNr%2FbtsPFBW1DbZ%2FreNdxvTqOrLRpeHXBrklc1%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;720&quot; height=&quot;774&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;774&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1307&quot; data-origin-height=&quot;741&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O6Md5/btsPGj9tHVv/CS8DQ8ZuuDWHCxQtQrAaPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O6Md5/btsPGj9tHVv/CS8DQ8ZuuDWHCxQtQrAaPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O6Md5/btsPGj9tHVv/CS8DQ8ZuuDWHCxQtQrAaPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO6Md5%2FbtsPGj9tHVv%2FCS8DQ8ZuuDWHCxQtQrAaPK%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;730&quot; height=&quot;414&quot; data-origin-width=&quot;1307&quot; data-origin-height=&quot;741&quot;/&gt;&lt;/span&gt;&lt;/figure&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;이미 제공되는 Jenkins의 오픈소스 플러그인&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;플러그인을 한번 더 감싸서&lt;/b&gt; 개발자가 사용하기 쉽게 만들었다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빌더 패턴으로 구현&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트를 적용하여&lt;/b&gt; 개발자의 실수를 줄이도록 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영 효율화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리소스 관리:&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;690&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blXY2I/btsPHd2epl3/GxxBmCa2qgfE7w959z8m70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blXY2I/btsPHd2epl3/GxxBmCa2qgfE7w959z8m70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blXY2I/btsPHd2epl3/GxxBmCa2qgfE7w959z8m70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblXY2I%2FbtsPHd2epl3%2FGxxBmCa2qgfE7w959z8m70%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;723&quot; height=&quot;411&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;690&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1303&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSD8nk/btsPEMkEXcE/rj3tIWSfrsuFg1AfqEBR6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSD8nk/btsPEMkEXcE/rj3tIWSfrsuFg1AfqEBR6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSD8nk/btsPEMkEXcE/rj3tIWSfrsuFg1AfqEBR6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSD8nk%2FbtsPEMkEXcE%2Frj3tIWSfrsuFg1AfqEBR6K%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;749&quot; height=&quot;439&quot; data-origin-width=&quot;1303&quot; data-origin-height=&quot;763&quot;/&gt;&lt;/span&gt;&lt;/figure&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;JAR 파일 경로 표준화&lt;/b&gt; 수행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적은 비용으로 가능해졌다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;협업 개선:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개발자와 데브옵스 간의 단일 업무 인터페이스 구축&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;579&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3HvAx/btsPEsfDfrf/BROY0p45gvGDcx975vlNLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3HvAx/btsPEsfDfrf/BROY0p45gvGDcx975vlNLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3HvAx/btsPEsfDfrf/BROY0p45gvGDcx975vlNLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3HvAx%2FbtsPEsfDfrf%2FBROY0p45gvGDcx975vlNLK%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;788&quot; height=&quot;500&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 철학: 진화하는 시스템&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가장 강력한 기능은 진화&lt;/h3&gt;
&lt;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;/li&gt;
&lt;li&gt;완벽한 설루션보다는 &lt;b&gt;지속적으로 개선 가능한 구조&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b14FHq/btsPES55OlR/ca9DFJDjmAfa4iHsZU4Yi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b14FHq/btsPES55OlR/ca9DFJDjmAfa4iHsZU4Yi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b14FHq/btsPES55OlR/ca9DFJDjmAfa4iHsZU4Yi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb14FHq%2FbtsPES55OlR%2Fca9DFJDjmAfa4iHsZU4Yi1%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;784&quot; height=&quot;497&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래부터는 정리한 내용을 AI에세 요약을 부탁하여 작성하였다.&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.&lt;span&gt;&lt;span&gt; 가맹점은 변함없이 결제창 시스템 전면 재작성하기&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세션은 토스 페이먼츠의 결제창 시스템을 전면 재편하였던 경험을 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;20년 된 레거시 시스템으로, 코드 중복과 2500줄에 달하는 조건문(if) 분기 처리&lt;/li&gt;
&lt;li&gt;결제창 핵심 로직과 외부 인터페이스의 강한 결합, 전역변수로 인한 추적 어려움&lt;/li&gt;
&lt;li&gt;SDP 내에서 DB 쿼리와 화면 조작이 혼재, 신규 팀원 온보딩 어려움&lt;/li&gt;
&lt;li&gt;PG와 토스 페이먼츠 간 암호화 통신 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결 방안:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;추상화 및 결합도 감소:&lt;/b&gt; 모든 외부 인터페이스를 단일 인터페이스로 통합, 프론트와 백엔드 간 플로우 정의&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그 기반 파라미터 식별:&lt;/b&gt; 한 달치 로그를 분석해 파라미터 식별&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Simple Request 도입:&lt;/b&gt; Preflight 요청 제거로 요청 단순화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;카나리 배포:&lt;/b&gt; 점진적 트래픽 전환으로 안정성 확보&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비즈니스 이해:&lt;/b&gt; 레거시 개편에서 비즈니스 연속성을 유지하기 위해 도메인 이해를 강조&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성과:&lt;/b&gt; 가맹점 경험을 유지하면서 시스템 안정성과 유지보수성을 개선&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5.&lt;span&gt;&lt;span&gt;&amp;nbsp; LLM내재화 전략&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스 페이먼츠의 LLM(대규모 언어 모델) 내재화 전략을 다룬다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상용 LLM(GPT, Claude 등)은 금융 도메인 특화 커스터마이징이 어려움&lt;/li&gt;
&lt;li&gt;실시간성이 덜 중요하며, 안정성과 도메인 특화가 우선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결 방안:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;도메인 특화 LLM:&lt;/b&gt; 한국어와 영어만 지원하고, 금융 도메인에 특화된 소규모 LLM 개발&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 파이프라인:&lt;/b&gt; 데이터 수집(상용 LLM 활용), 정제, 오픈소스 기반 학습, 평가로 구성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평가: Reward Model과 LLM as a Judge로 자동화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인프라:&lt;/b&gt; KubeFlow로 파이프라인 관리, 5-10명 규모의 엔지니어 팀 필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고객 맞춤:&lt;/b&gt; 토스증권 고객의 요구사항을 반영한 모델 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6. 확장성과 회복 탄력성을 갖춘 결제 시스템 만들기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제 원장 테이블의 문제를 해결하고 확장성과 회복 탄력성을 강화한 사례를 다룬다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비일관성 구조:&lt;/b&gt; 카드 결제/취소는 별도 테이블, 계좌이체는 단일 테이블로 운영난이도 증가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;강한 의존성:&lt;/b&gt; 도메인별 컬럼 의미 상이, 팀 간 협업 문제&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성 제약:&lt;/b&gt; 1:1 구조로 더치페이 같은 유연한 결제 불가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MySQL 선택 이유:&lt;/b&gt; 기존 Oracle 기반 테이블의 복잡성, MyBatis 유지보수 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결 방안:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;일관성 있는 구조:&lt;/b&gt; 공통 테이블과 개별 테이블로 분리, 결제/취소 시 insert로 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의존성 제거:&lt;/b&gt; Kafka 도입으로 도메인 간 직접적 의존성 제거&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성 강화:&lt;/b&gt; 결제와 승인 테이블 분리로 더치페이 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비동기 처리:&lt;/b&gt; 기존 원장에 선저장 후 신규 원장에 비동기 저장, 쓰레드풀 튜닝 및 graceful shutdown&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 정합성:&lt;/b&gt; 검증 배치로 적합성 보장, 카나리 배포로 점진적 트래픽 전환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영 이슈 대응:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 select 쿼리의 DB 부하(인덱스 미사용으로 풀 스캔) &amp;rarr; 롤백 및 인덱스 최적화&lt;/li&gt;
&lt;li&gt;구-신규 원장 불일치 및 응답 지연 &amp;rarr; 로깅 fallback으로 서버 저장&lt;/li&gt;
&lt;li&gt;이벤트 발행 누락 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성과:&lt;/b&gt; 확장성과 회복 탄력성을 갖춘 결제 시스템 구축, 신규 팀원 온보딩 시간 단축&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.&lt;span&gt;&lt;span&gt;&lt;span&gt; 레거시 정산 개편기 : 신규 시스템 투입 여정부터 대규모 배치 운영 노하우까지&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 정산 시스템 개편과 대규모 배치 운영 방안을 다룬다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;쿼리 종속성:&lt;/b&gt; 복잡한 수수료 계산 쿼리로 유지보수 비용 증가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 모델링 한계:&lt;/b&gt; 거래별 세밀한 데이터 관리 부족&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고해상도 데이터 문제:&lt;/b&gt; 데이터량 증가로 조회 성능 저하&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배치 성능:&lt;/b&gt; 단일 스레드 기반 Spring Batch로 처리량 한계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결 방안:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;쿼리 분해:&lt;/b&gt; 복잡한 쿼리를 비즈니스 로직으로 분해, 분할정복 방식으로 점진적 전환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 모델링 개선:&lt;/b&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고해상도 데이터 처리:&lt;/b&gt; 날짜 기반 레인지 파티셔닝, 복합 인덱스, 조회 전용 테이블 설계&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배치 최적화:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Batch 멀티 스레드 스텝, 모듈러 연산으로 데이터 분할 처리&lt;/li&gt;
&lt;li&gt;동적 프로비저닝 및 JobDSL로 배치잡 선언 코드화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증 및 안정성:&lt;/b&gt; 구-신규 정산 데이터 비교 검증, 카나리 배포로 안정적 전환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모니터링 강화:&lt;/b&gt; OOM, 쓰레드 블록, 성능 지연 탐지, 쓰레드 덤프로 병목 및 데드락 분석&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성과:&lt;/b&gt; 유지보수성 향상, 데이터 조회 성능 개선, 안정적인 배치 운영&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;후기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 세션을 들으면서 공통적으로 느꼈던 점이 몇 가지 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완벽보다는 실용성과 지속적인 개선을 중요시하는 것 같았다. 전통적인 2PC 대신 자체적인 분산 트랜젝션 도입, jenkins를 &quot;완벽하지는 않지만 충분하다&quot;는 판단 하에 선택하는 등 기존의 방식에 안주하지 않고 새로운 접근방법을 시도하는 부분이 인상적이었다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Books &amp;amp; Reviews/회고획오</category>
      <category>2025 토스 메이커스 후기</category>
      <category>backend</category>
      <category>toss makers conference</category>
      <category>토스메이커스</category>
      <author>9Jaeng</author>
      <guid isPermaLink="true">https://sora9z.tistory.com/152</guid>
      <comments>https://sora9z.tistory.com/152#entry152comment</comments>
      <pubDate>Sun, 3 Aug 2025 23:16:55 +0900</pubDate>
    </item>
    <item>
      <title>[NestJS] Monorepo로 구축하기 1 : pnpm workspace</title>
      <link>https://sora9z.tistory.com/150</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 프로젝트는 모든 기능이 하나의 단일 코드베이스에서 관리되는 모놀리식 아키텍처(Monolithic Architecture)를 사용하고 있는 거대한 레거시였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSAP 인증을 받기 위해 테이블구조도 큰 변경이 필요했고 서비스가 커지면서 개발 유지보수를 힘들게 만드는 현재의 구조를 변경하기 위해 모노레포 아키텍처로 코드베이스를 변경하기로 논의가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 블로그는 모노레포를 구성하면서 조사했던 것들에 대해 정리를 해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로, mono repo 구축을 위한 package workspace에 대해 간단히 정리를 해보았다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Monorepo를 구축하는 방법 : pnpm workspace&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;monorepo를 구축하는 방법으로는 yarn worskapce와 같은 package workspace 나 lerna , Turborepo 등과 같은 도구를 사용하는 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 그중 pnpm workspace를 선택하였는데, 이유는 다음과 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. pnpm workspace는 기본적으로 모노레포를 지원한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. yarn , npm은 phantom dependency문제가 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 현재 사용 중이라 러닝커브가 높지 않다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;phantom dependency란?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 phantom dependnency란, A워크스페이스에서 설치하지 않는 의존성을 사용할 수 있게 된다거나 원래 사용 중이던 의존성을 갑자기 찾지 못한다는 경우를 말한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;의존성 호이스팅 : yarn classic과 npm의 경우 package설치의 중복을 줄이기 위해 의존성이 호이스팅 된다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 그림을 보면, package-1 은 A, C를 의존성으로 갖고 A, C는 각각 하위 의존성으로 B(1.0), B(2.0)을 갖는다. yarn은 이런 의존성들을 호이스팅 하여 root node_modules에서 관리한다. (평탄화라고 부르는 것 같다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvyyku/btsOb5SQrxK/ufsHGd9HTeGbuNNcUoZr10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvyyku/btsOb5SQrxK/ufsHGd9HTeGbuNNcUoZr10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvyyku/btsOb5SQrxK/ufsHGd9HTeGbuNNcUoZr10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdvyyku%2FbtsOb5SQrxK%2FufsHGd9HTeGbuNNcUoZr10%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;821&quot; height=&quot;385&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;385&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 package-1 은 B(1.0)을 설치하지 않았지만 호이스팅 되어 B(1.0)을 사용할 수 있다. 만약 B(1.0)을 의존하던 것이 없어진다면 node_modules에 B(1.0)이 등록되지 않기 때문에 not found module 이슈가 발생할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm의 공식문서 &lt;a href=&quot;https://pnpm.io/pnpm-vs-npm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pnpm&amp;nbsp;vs&amp;nbsp;npm과&lt;/a&gt; 이 &lt;a href=&quot;https://medium.com/wraffle-tech/yarn-workspace%EC%99%80-pnpm-workspace-c118e8ba3260&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그를&lt;/a&gt; 참고해 보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면, pnpm의 구조는 어떤가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm은 symlink를 통해 package를 참조하는 방식을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pnpm.io/symlinked-node-modules-structure&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&amp;nbsp;Symlinked&amp;nbsp;`node_modules`&amp;nbsp;structure&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1748159480256&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Symlinked &amp;#96;node_modules&amp;#96; structure | pnpm&quot; data-og-description=&quot;This article only describes how pnpm's node_modules are structured when&quot; data-og-host=&quot;pnpm.io&quot; data-og-source-url=&quot;https://pnpm.io/symlinked-node-modules-structure&quot; data-og-url=&quot;https://pnpm.io/symlinked-node-modules-structure&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/euiiXD/hyYYBG2psn/WffRcM0rghRT0n1b1jNOrK/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200,https://scrap.kakaocdn.net/dn/Q8wOl/hyY0vMjxYP/gUgq94BhkdKsoJfW4RyUTK/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://pnpm.io/symlinked-node-modules-structure&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pnpm.io/symlinked-node-modules-structure&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/euiiXD/hyYYBG2psn/WffRcM0rghRT0n1b1jNOrK/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200,https://scrap.kakaocdn.net/dn/Q8wOl/hyY0vMjxYP/gUgq94BhkdKsoJfW4RyUTK/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200');&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;Symlinked `node_modules` structure | pnpm&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This article only describes how pnpm's node_modules are structured when&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pnpm.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 공식문서에 따르면, pnpm 은 symlink를 사용하여 중첩된 종속성 트리를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node_modules 안에 있는 모든 패키지의 모든 파일은 content-addressable stora라는 전역 저장소에 대한 hard link를 갖는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;hard link란, 파일시스템의 기능 중 하나로, 하나의 파일 데이터를 여러 위치에서 참조할 수 있도록 하는 방식. 즉, 동일한 파일 내용을 여러 경로에서 접근할 수 있게 하되, 실제로는 디스크 상에 하나의 데이터만 존재한다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRxKM2/btsOcUb1hmV/s6KKolVfiaBAxEZIqchHVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRxKM2/btsOcUb1hmV/s6KKolVfiaBAxEZIqchHVK/img.png&quot; data-alt=&quot;https://pnpm.io/symlinked-node-modules-structure&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRxKM2/btsOcUb1hmV/s6KKolVfiaBAxEZIqchHVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRxKM2%2FbtsOcUb1hmV%2Fs6KKolVfiaBAxEZIqchHVK%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;990&quot; height=&quot;310&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://pnpm.io/symlinked-node-modules-structure&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 하드 링크가 구축되면, 중첩된 종속성 그래프 구조를 구축하기 위해 simlink를 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pWCem/btsOcgmi57D/oWIdtaQwSYZyY4ImPPW5dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pWCem/btsOcgmi57D/oWIdtaQwSYZyY4ImPPW5dk/img.png&quot; data-alt=&quot;https://pnpm.io/symlinked-node-modules-structure&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pWCem/btsOcgmi57D/oWIdtaQwSYZyY4ImPPW5dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpWCem%2FbtsOcgmi57D%2FoWIdtaQwSYZyY4ImPPW5dk%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;977&quot; height=&quot;261&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://pnpm.io/symlinked-node-modules-structure&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 경우 foo는 bar를 의존하는데, node_modules/. pnpm/foo@1.0.0/node_modules의 bar는. pnpm에 있는 bar를 simlink로 참조한다.&amp;nbsp; 이런 방식을 사용함으로써 순환참조, self 의존의 경우도 문제없이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm이나 yarn classic과 같이 호이스팅을 하지 않아 phantom dependency는 발생하지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;pnpm의 saveing disk 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm은 위와 같은 content-addressable storage 방식을 사용함으로써 disk 공간 절약에도 좋다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;317&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kr4CJ/btsOaWJKzYV/HEIv2pgCkZSehdkg7QmVJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kr4CJ/btsOaWJKzYV/HEIv2pgCkZSehdkg7QmVJk/img.png&quot; data-alt=&quot;https://pnpm.io/motivation&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kr4CJ/btsOaWJKzYV/HEIv2pgCkZSehdkg7QmVJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKr4CJ%2FbtsOaWJKzYV%2FHEIv2pgCkZSehdkg7QmVJk%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;698&quot; height=&quot;317&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;317&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://pnpm.io/motivation&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 npm이라면, 하나의 의종성을 100개의 패키지에서 의존성으로 갖는다면, 100개가 복사될 것이지만 pnpm은 store에 한 번만 저장하고 다른 패키지들이 참조를 하도록 하여 디스크를 절약할 수 있다. 만약 의존성의 버전이 올라가면 올라간 버전이 새로 저장되는 방식을 취함으로써 버전별 관리가 가능하다는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 이유로 pnpm workspace를 사용하게 되었다.&lt;/p&gt;</description>
      <category>Programming Languages/Typescript &amp;amp; NestJS</category>
      <category>monorepo nestjs</category>
      <category>nestjs monorepo 구축</category>
      <category>pnpm</category>
      <category>pnpm workspace</category>
      <category>Workspace</category>
      <category>yarn workdpace</category>
      <category>모노레포</category>
      <author>9Jaeng</author>
      <guid isPermaLink="true">https://sora9z.tistory.com/150</guid>
      <comments>https://sora9z.tistory.com/150#entry150comment</comments>
      <pubDate>Sun, 25 May 2025 20:19:51 +0900</pubDate>
    </item>
    <item>
      <title>[NestJS] Module의 실행 과정과 종류</title>
      <link>https://sora9z.tistory.com/149</link>
      <description>&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;NestJS에서 모듈은 애플리케이션에서 비슷한 기능을 논리적으로 묶은 단위이다.&amp;nbsp; 유사한 기능을 하는 컴포넌트들을 한데 모아 관리하는 컨테이너 역할을 한다. &lt;br /&gt;만약 Auth 서비스(로그인, 회원가입 등), 사용자 관련 서비스(사용자 정보 조회, 사용자 정보 수정 등), 주문 관련 서비스(주문 생성, 주문조회 등)가 있다고 한다면 이들 각가이 하나의 모듈이 된다. 이런 특성으로 코드를 깔끔하게 정리할 수 있고 다른 모듈과 독립적으로 동작하도록 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;NestJS 모듈은 @Module() 데코레이터가 달린 단일 클래스이며 이 데코레이터를 통해 애플리케이션 구조를 구성하는데 필요한 메타데이터를 제공받는다. 제공받는 메타데이터는 아래와 같이 네 가지가 존재한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 70px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;imports&lt;/td&gt;
&lt;td&gt;다른 모듈에서 export하는 기능을 사용하기 위해 다른 모듈을 가져온다. 의존성 주입의 기본이 되는 부분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;controllers&lt;/td&gt;
&lt;td&gt;이 모듈에 속한 컨트롤러를 정의. HTTP 요청을 처리하는 부분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;providers&lt;/td&gt;
&lt;td&gt;모듈에서 사용 할 서비스를 등록하는 부분&lt;br /&gt;import한 다른 모듈에서 export한 provider를 여기에 등록하여 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exports&lt;/td&gt;
&lt;td&gt;다른 모듈에 제공하고자 하는 provider의 집합이다. export에 등록된 provider만이 다른 모듈에서 사용할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre id=&quot;code_1742124848672&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;@Module({
    imports: [CommonModule], // CommonModule에서 export하고 잇는 provider를 사용하기 위해 가져오는 module집합
    controllers: [UserController], // UserModule에서 정의된 모든 Controller 집합
    providers: [UserService, CommonService], // UserService와 CommonService를 이 모듈의 비즈니스로직에서 사용하기 위해 provider로 등록한다
    exports: [UserService], // UserService를 다른 모듈에서 사용할 수 있도록 제공
})
export class UserModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NestJS의 모듈 시스템 실행 과정&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;NestJS는 AppModule이라는 최소 하나의 모듈로 이루어져 있다. 그리고 대부분 Application은 다수의 모듈을 사용한다. 아래의 그림과 같이 AppModule 루트모듈로 Application의 시작점이며 AppModule에서 하위 모듈을 import 하여 그래프 구조를 갖는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HOHAf/btsMM5l7knc/nwx8kPlfe5tiozEJ1oqKfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HOHAf/btsMM5l7knc/nwx8kPlfe5tiozEJ1oqKfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HOHAf/btsMM5l7knc/nwx8kPlfe5tiozEJ1oqKfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHOHAf%2FbtsMM5l7knc%2Fnwx8kPlfe5tiozEJ1oqKfk%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;370&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;526&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1742124514511&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Module({
  imports: [AuthModule, UsersModule, OrderModule], // AppModule에서 하위 모듈을 모두 import
})
export class AppModule {}&lt;/code&gt;&lt;/pre&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;이렇게 등록된 모듈 시스템은 NestJs가 실행될 때 아래와 같은 순서로 작동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 애플리케이션 부트스트래핑과 의존성 그래프 구축&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 시작되면 main.ts의 &lt;span&gt;NestFactory&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;AppModule&lt;/span&gt;&lt;span&gt;) 을 호출하여 루트 모듈을 등록한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742125981747&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS 애플리케이션이 부트스트래핑 되면 AppModule에 등록된 하위 Module들은 &lt;b&gt;의존성 그래프를 구축&lt;/b&gt;하게 된다. 이 과정에서 아래와 같은 동작이 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. root module의 imports 배열에 선언된 모든 모듈을 재귀적으로 분석한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 각 모듈에 정의된 providers, controllers를 식별한다.&lt;/b&gt;&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;NestJS 코드 내부를 살펴보면,&lt;/p&gt;
&lt;div&gt;DependenciesScanner의 scanForModules 에서 reflectMetadata를 통해 @Module 데코레이터의 imports 메타데이트 키인&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MODULE_METADATA.IMPORTS를 사용하여 imports의 메타데이터를 가져오는 것을 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1742130404596&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; const modules = !this.isDynamicModule(
      moduleDefinition as Type&amp;lt;any&amp;gt; | DynamicModule,
    )
      ? this.reflectMetadata(
          MODULE_METADATA.IMPORTS,
          moduleDefinition as Type&amp;lt;any&amp;gt;,
        )
      : [
          ...this.reflectMetadata(
            MODULE_METADATA.IMPORTS,
            (moduleDefinition as DynamicModule).module,
          ),
          ...((moduleDefinition as DynamicModule).imports || []),
        ];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래로 더 내려가면 재귀적으로 innerModule에 대해 scanForModules를 호출하면서 재귀적으로 imports문에 선언된 모든 모듈을 분석한다&lt;/p&gt;
&lt;pre id=&quot;code_1742130639850&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    let registeredModuleRefs: Module[] = [];
    for (const [index, innerModule] of modules.entries()) {
      // In case of a circular dependency (ES module system), JavaScript will resolve the type to `undefined`.
      if (innerModule === undefined) {
        throw new UndefinedModuleException(moduleDefinition, index, scope);
      }
      if (!innerModule) {
        throw new InvalidModuleException(moduleDefinition, index, scope);
      }
      if (ctxRegistry.includes(innerModule)) {
        continue;
      }
      const moduleRefs = await this.scanForModules({
        moduleDefinition: innerModule,
        scope: ([] as Array&amp;lt;Type&amp;gt;).concat(scope, moduleDefinition as Type),
        ctxRegistry,
        overrides,
        lazy,
      });
      registeredModuleRefs = registeredModuleRefs.concat(moduleRefs);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;DependenciesScanner 클래스에서는 각 모듈의 providers, controllers, imports, exports를 식별하고 컨테이너에 등록하는 로직을 통해 각 의존성을 식별한다.&lt;/div&gt;
&lt;pre id=&quot;code_1742131858932&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  public async scanModulesForDependencies(
    modules: Map&amp;lt;string, Module&amp;gt; = this.container.getModules(),
  ) {
    for (const [token, { metatype }] of modules) {
      await this.reflectImports(metatype, token, metatype.name);
      this.reflectProviders(metatype, token);
      this.reflectControllers(metatype, token);
      this.reflectExports(metatype, token);
    }
  }&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;size18&quot;&gt;2. 공급자(Provider) 인스턴스화&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;의존성 그래프가 구축되면, NestJS는 아래와 같은 순서로 Provider를 인스턴스화한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 글로벌 공급자 먼저 인스턴스를 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 모듈별 공급자를 인스턴스화 한다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 각 공급자의 의존성을 주입한다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;글로벌 공급자는 ApplicationConfig에 등록된 글로벌 가드, 파이프, 인터셉터, 필터 등을 말한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코드는 &lt;a href=&quot;https://github.com/nestjs/nest/blob/master/packages/core/scanner.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;DependenciesScaner&amp;nbsp;&lt;/a&gt;의 applyApplicationProviders 부분과 &lt;a href=&quot;https://github.com/nestjs/nest/blob/master/packages/core/injector/instance-loader.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;instance-loader.ts&lt;/a&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;size18&quot;&gt;3. LifeCycle Hook 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/fundamentals/lifecycle-events&quot;&gt;https://docs.nestjs.com/fundamentals/lifecycle-events&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBfuNC/btsMK52lcxC/IpkdSRXJwRWxkg7gWKVng0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBfuNC/btsMK52lcxC/IpkdSRXJwRWxkg7gWKVng0/img.png&quot; data-alt=&quot;https://docs.nestjs.com/fundamentals/lifecycle-events&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBfuNC/btsMK52lcxC/IpkdSRXJwRWxkg7gWKVng0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBfuNC%2FbtsMK52lcxC%2FIpkdSRXJwRWxkg7gWKVng0%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;1314&quot; height=&quot;512&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.nestjs.com/fundamentals/lifecycle-events&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJs는 Provider 인스턴스화 후 아래와 같은 순서로 LifeCycle Hook를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. onModuleInit() - 해당 모듈의 모든 의존성이 해결된 후 호출&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. onApplicationBootstrap - 모든 모듈이 초기화된 후 호출(하지만 연결을 수신하기 전에 호출된다)&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;size18&quot;&gt;4. 컨트롤러 등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Provider가 인스턴스화되고 LifeCycle Hook이 실행된 후 NestJS는 각 모듈에 정의된 컨트롤러를 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 컨트롤러 및 route handler 분석&lt;/b&gt;&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;b&gt;3. HTTP요청 처리를 위한 라우팅 테이블 설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;code/nest-application.ts의 &lt;a href=&quot;https://github.com/nestjs/nest/blob/master/packages/core/nest-application.ts#L54&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;init&lt;/a&gt;을 보면 아래와 같이 NestJS의 LifeCycle Hook 실행 후 Router를 확인하는 함수가 실행되는 것을 볼 수 있다&lt;/p&gt;
&lt;pre id=&quot;code_1742132887304&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public async init(): Promise&amp;lt;this&amp;gt; {
    if (this.isInitialized) {
      return this;
    }

    this.applyOptions();
    await this.httpAdapter?.init?.();

    const useBodyParser =
      this.appOptions &amp;amp;&amp;amp; this.appOptions.bodyParser !== false;
    useBodyParser &amp;amp;&amp;amp; this.registerParserMiddleware();

    await this.registerModules();
    await this.registerRouter();
    await this.callInitHook();
    await this.registerRouterHooks();
    await this.callBootstrapHook();

    this.isInitialized = true;
    this.logger.log(MESSAGES.APPLICATION_READY);
    return this;
  }&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;이렇게 모든 모듈, 공급자, 컨트롤러가 초기화되면 애플리케이션은 HTTP 요청을 처리할 준비가 된 상태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NestJS 모듈의 종류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/modules&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.nestjs.com/modules&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJs의 모듈 종류는 여러 가지가 있다. 위에서 실행과정을 보면서도 nestjs의 모듈 종류에 따라 다르게 처리하는 부분들을 봤던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. Feature Module&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 NestJs를 사용하면 기본적으로 사용하는 모듈이다. 비슷한 기능, 관련된 기능 단위로 관리하며 관련된 컨트롤러, 서비스, 레포지토리 등을 묶는다. 기능별로 분리를 하고 독립적으로 동작하거나 다른 모듈에 의존할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1742133362557&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';

@Module({
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. Shared Module&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJs에서 모듈은 기본적으로 싱글톤이다. 이러한 특징으로 동일한 instance의 provider를 모듈 간 공유할 수 있다. 모든 모듈이 공유모듈이라고 할 수 있다.&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;3. Dynamic Module&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적모듈은 런타임에 설정을 동적으로 적용할 수 있는 모듈이다. 애플리케이션 실행 시 생성되는 정적 모듈과 다르게 모듈의 구성 요소를&amp;nbsp; 실행 시점에 정할 수 있어 유연성이 좋다. 비슷한 기능을 여러 모듈에서 사용할 때, 설정만 다르게 해서 동일한 모듈을 재활용할 수 있다는 점이 큰 장점이라고 생각한다.&lt;/p&gt;
&lt;div&gt;&lt;span&gt;forRoot()&lt;/span&gt;나 &lt;span&gt;forFeature()&lt;/span&gt; 같은 메서드를 통해 모듈을 생성하며, 외부 라이브러리나 설정값을 주입할 때 사용된다.&amp;nbsp;&lt;/div&gt;
&lt;div&gt;주로 환경 변수 관리(&lt;span&gt;ConfigModule&lt;/span&gt;), 데이터베이스 ORM 설정(&lt;span&gt;TypeOrmModule&lt;/span&gt;) 등에서 자주 활용된다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 코드는 동적 모듈로 로그 모듈을 생성하고 사용하는 모듈에서 로그 레벨을 동적으로 설정할 수 있음을 보여준다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1742133787993&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Module, DynamicModule } from '@nestjs/common';
import { LoggerService } from './logger.service';

// 정의
@Module({})
export class LoggerModule {
  static forRoot(options: { level: string }): DynamicModule {
    return {
      module: LoggerModule,
      providers: [
        { provide: 'LOG_LEVEL', useValue: options.level },
        LoggerService,
      ],
      exports: [LoggerService],
    };
  }
}

// 사용
@Module({
  imports: [LoggerModule.forRoot({ level: 'debug' })],
})
export class AppModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;4. Global Module&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글로벌 모듈은 @Global() 데코레이터를 사용하여 전역 범위에서 사용 가능한 모듈이다. 한 번 정의하면 다른 모듈에서 import 없이도 provider를 주입받을 수 있다.&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;pre id=&quot;code_1742134046633&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Module, Global } from '@nestjs/common';
import { CacheService } from './cache.service';

@Global()
@Module({
  providers: [CacheService],
  exports: [CacheService],
})
export class CacheModule {}&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅은 NestJS의 모듈에 대해 알아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 프로바이더의 내부 동작과 종류 그리고 언제 어떤 방식을 사용하면 좋을지에 대해 정리해 보겠다.&lt;/p&gt;</description>
      <category>Programming Languages/JavaScript &amp;amp; Node.js</category>
      <category>module의 실행</category>
      <category>nestjs dynamic module</category>
      <category>nestjs module</category>
      <category>다이나믹모듈</category>
      <author>9Jaeng</author>
      <guid isPermaLink="true">https://sora9z.tistory.com/149</guid>
      <comments>https://sora9z.tistory.com/149#entry149comment</comments>
      <pubDate>Sun, 16 Mar 2025 23:22:44 +0900</pubDate>
    </item>
    <item>
      <title>[REFACTORING - 2] CH02 리랙터링의 원칙 정리</title>
      <link>https://sora9z.tistory.com/148</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLViS6/btsMBiFyj1y/sXTDQgd7B5u2mmQeJO6CH0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLViS6/btsMBiFyj1y/sXTDQgd7B5u2mmQeJO6CH0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLViS6/btsMBiFyj1y/sXTDQgd7B5u2mmQeJO6CH0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLViS6%2FbtsMBiFyj1y%2FsXTDQgd7B5u2mmQeJO6CH0%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;353&quot; height=&quot;434&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1장에서는 리팩터링이 무엇인지 감을 잡기 위한 것이었다면, 2장은 리팩터링의 정의와 이유, 시기, 그리고 리팩터링 시 고려해야 할 다양한 측면들을 다룬다.&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;2장 요약&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 리팩터링의 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 정리하거나 구조를 바꾸는 작업을 재구성(restructuring)이라고 하며, 리팩터링은 이러한 재구성의 한 형태다. 리팩터링의 핵심 목적은&lt;b&gt; 코드를 더 이해하기 쉽고 수정하기 쉽게 만드는 것&lt;/b&gt;이다. 구체적으로 리팩터링은 &lt;b&gt;&quot;기본 동작은 그대로 유지한 채, 여러 기법들을 사용해서 소프트웨어를 재구성하는 것&quot;&lt;/b&gt;을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩터링은 작은 단계들을 통해 코드를 점진적으로 수정하고, 이러한 단계들이 연결되어 궁극적으로 큰 변화를 만들어낸다. 가장 중요한 점은 코드의 &lt;b&gt;구조는 변경하지만, 프로그램의 전반적인 기능은 그대로 유지&lt;/b&gt;해야 한다는 것이다. 리팩터링으로 인해 기능 변경이 발생해서는 안 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 리팩터링을 하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 개발자들이 코드에 기능을 추가하다 보면 리팩터링을 하고 싶은 욕구를 느낄 것 이라 생각한다. 책에서는 소프트웨어 개발 과정에서 기능 추가와 리팩터링을 &lt;b&gt;목적에 맞게 명확하게 구분&lt;/b&gt;해야 한다고 강조한다. 리팩터링을 진행할 때는 테스트 코드조차 추가하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩터링의 필요성은 대부분의 개발자가 인식하고 있다고 생각한다. 지속적인 리팩터링은 &lt;b&gt;소프트웨어 설계를 개선&lt;/b&gt;하고 &lt;b&gt;코드를 이해하기 쉽게&lt;/b&gt; 만든다. 코드가 읽기 쉬워지면 &lt;b&gt;버그도 더 빠르게 발견&lt;/b&gt;할 수 있고 유지보수도 용이해진다. 코드를 쉽게 파악할 수 있게 되면 &lt;b&gt;작업 속도 또한 자연스럽게 향상&lt;/b&gt;된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 리팩터링은 언제 해야할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책에서는 리팩터링을 세 가지 유형으로 분류한다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;준비를 위한 리팩터링&lt;/b&gt;: 기능 추가 직전에 작업을 더 쉽게 만들기 위해 수행하는 리팩터링&lt;/li&gt;
&lt;li&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;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;b&gt; 해당 코드 관련 작업을 할 때마다 점진적으로 개선해 나가는 방법&lt;/b&gt;을 추천한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.5 리팩터링 시 고려할 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩터링을 하면서 발생할 수 있는 대표적인 문제들은 다음과 같다:&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;새 기능 개발 속도 저하&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩터링은 얼핏 보면 새 기능 개발 속도를 저하시킨다는 인상을 줄 수 있다. 그러나 책에서는 &lt;b&gt;&quot;리팩터링의 궁극적인 목적은 개발 속도를 높여서 더 적은 노력으로 더 많은 가치를 창출하는 것&quot;&lt;/b&gt;이라고 설명한다. 기능을 추가하기 쉽게 코드를 만든 후 기능을 추가하는 것이 장기적으로는 더 빠른 방법이기 때문이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코드 소유권과 브랜치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 소유권이 명확하게 나뉘어 있으면 리팩터링에 방해가 될 수 있다. 내가 소유하지 않은 코드를 리팩터링할 때 다른 부분에 영향을 주지 않도록 하는 복잡성이 발생한다. 이를 해결하기 위해 코드 소유권을 팀 단위로 두고, 팀 구성원 모두가 코드를 수정할 수 있는 느슨한 방식을 채택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지속적 통합(Continuous Integration)&lt;/b&gt; 또는 &lt;b&gt;트렁크 기반 개발(Trunk-Based Development)&lt;/b&gt;을 도입하여 기능별 브랜치와 마스터 브랜치가 통합되는 기간을 최소화하는 것도 좋은 방법이다. 이러한 방식에서는 &lt;b&gt;구성원들이 하루에 최소 한 번은 마스터와 통합&lt;/b&gt;하도록 하여 브랜치 간 차이가 크게 벌어지지 않도록 한다. 이를 통해 병합의 &lt;b&gt;복잡도를 낮추고, 코드베이스 전반에 걸친 리팩터링으로 인한 충돌을 최소화&lt;/b&gt;할 수 있다. 여기서 통합이란, 마스터를 개인 브랜치로 pull 하고 작업 결과를 마스터에 올리는 push 하는 작업을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( 팀마다 다르겠지만 master가 아닌 코드 통합 및 테스트의 기본 브랜치(예를 들면 develop)일 것이라 생각한다 )&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;b&gt; 테스트 코드가 필수적&lt;/b&gt;이다. 테스트 코드는 리팩터링 과정에서 기능 변경 여부를 확인할 수 있는 중요한 도구가 된다. 또한 지속적 통합 과정에서 활용하여 코드 통합 시 발생할 수 있는 충돌을 효과적으로 식별할 수 있다.&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 레거시 시스템을 테스트 코드 없이 리팩터링하는 것은 매우 어렵다. 이런 경우에는 테스트 보강이 가장 효과적인 접근법이다. 서로 관련된 부분끼리 나누어 하나씩 체계적으로 개선해 나가는 것이 중요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.6 아키텍처와 YAGNI&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩터링이 아키텍처에 미치는 가장 큰 효과는 &lt;b&gt;요구사항 변화에 따라 자연스럽게 대응할 수 있도록 코드를 설계&lt;/b&gt;해 준다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 소프트웨어를 사용해 보고 나서야 무엇이 적합한지 알게 되는 경우가 대부분이다. 범용성을 고려한 유연성 메커니즘을 미리 구현할 수도 있지만, 이는 실제 요구사항 변화에 따라 달라지는 경우가 많다. 리팩터링은 미리 추측하지 않고 현재까지 파악한 요구사항만을 해결하도록 코드를 구축하도록 한다. &lt;b&gt;개발을 진행하면서 사용자의 요구사항을 더 깊이 이해하게 되면 그에 맞게 아키텍처도 리팩터링하여 변경&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YAGNI(You Aren't Going To Need It)는 &quot;&lt;b&gt;당장 필요한 기능만으로 최대한 간결하게 만들라&quot;&lt;/b&gt;는 원칙을 의미한다. 이는 나중에 문제를 더 깊이 이해하게 되었을 때 처리하는 진화형 아키텍처의 핵심 개념이기도 하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.7 리팩터링과 소프트웨어 개발 프로세스&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드와 지속적 통합, 리팩터링을 함께 적용하면 YAGNI 설계 방식으로 효과적인 개발을 진행할 수 있다.&lt;/li&gt;
&lt;li&gt;불확실한 추측에 기반한 유연성 메커니즘보다는 단순한 시스템이 변경하기가 훨씬 쉽다.&lt;/li&gt;
&lt;li&gt;이 세 가지 실천 방법(테스트, 지속적 통합, 리팩터링)을 조화롭게 활용하면 요구사항 변화에 신속하게 대응할 수 있다.&lt;/li&gt;
&lt;li&gt;지속적 배포는 소프트웨어를 언제든 릴리스할 수 있는 상태로 유지해 준다. 이를 통해 비즈니스 요구에 맞춰 릴리스 일정을 유연하게 계획할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.8 리팩터링과 성능&lt;/h3&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;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 장에서는 리팩터링 전반에 적용되는 다양한 원칙들을 살펴보았다. 리팩터링의 정의와 목적, 적용 시기, 그리고 리팩터링 시 고려해야 할 요소들, 소프트웨어 아키텍처와의 관계, 성능과의 관계 등을 종합적으로 다루었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 인상 깊었던 부분은 &lt;b&gt;&quot;당장 필요한 기능만으로 최대한 간결하게&quot;&lt;/b&gt;라는 의미의 YAGNI 원칙과 이를 통한 진화형 아키텍처 접근법이었다. 최근 진행 중인 레거시 프로젝트 마이그레이션에서 새로운 아키텍처 구성에 대해 고민하던 중이었는데, 책에서 언급한 것처럼 한 번에 모든 요구사항을 예측하는 것은 불가능하며, 모든 상황을 고려한 유연성 메커니즘을 구현하려다 보면 리팩터링 범위가 커질 수 있다는 점을 실감했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 작업 중인 레거시 코드는 새로운 기능이 필요할 때마다 기존 구조를 고려하지 않고 계속 코드를 추가하다 보니 복잡해진 코드가 많이 있다. 테스트 코드도 없는 상황에서 점진적으로 테스트를 추가하며 리팩터링과 마이그레이션을 진행하고 있는데, 이 과정에서 테스트 코드와 리팩터링의 중요성을 더욱 실감하고 있다. 물론 일정을 맞추기 위해 속도가 중요할 때도 있어 항상 트레이드오프를 고려해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 장을 통해 특별히 기억하고 싶은 세 가지 핵심 원칙은 다음과 같다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;테스트 코드를 작성하자&lt;/li&gt;
&lt;li&gt;수시로 리팩터링을 하자&lt;/li&gt;
&lt;li&gt;당장 필요한 기능만으로 최대한 간결하게 개발하자&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Books &amp;amp; Reviews</category>
      <category>refactoring</category>
      <category>refactoring 2nd edition</category>
      <category>리팩토링</category>
      <category>리팩토링 원칙</category>
      <category>리팩토링2판</category>
      <category>지속적 통합</category>
      <category>테스트코드</category>
      <author>9Jaeng</author>
      <guid isPermaLink="true">https://sora9z.tistory.com/148</guid>
      <comments>https://sora9z.tistory.com/148#entry148comment</comments>
      <pubDate>Sun, 2 Mar 2025 22:41:10 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰 - 단위 테스트의 기술] - 단위 테스트 제대로 알기!</title>
      <link>https://sora9z.tistory.com/147</link>
      <description>&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;a href=&quot;https://sora9z.tistory.com/141&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[TEST]테스트 원칙에 대하여 - 소프트웨어 테스트의 7원칙과 FIRST원칙&lt;/a&gt; 이라던가 &lt;a href=&quot;https://sora9z.tistory.com/142&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[TEST]테스트 코드를 어떻게 시작하면 좋을까&amp;nbsp;&lt;/a&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;[&lt;b&gt;길벗출판사] 26차 개발자 리뷰어&quot;를&lt;/b&gt; 모집한다는 광고를 보았는데, 그중에 &quot;단위 테스트의 기술&quot;이라는 책이 눈에 들어왔다.&amp;nbsp; 저번에 신청했을 때는 선정이 안되었었기 때문에 큰 기대 없이 신청서 작성을 하였다. 그런데 운 좋게도 이번엔 당첨이 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tRm1e/btsL6ITd6Mr/U5YmgaSjGDaFbzDNaioW40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tRm1e/btsL6ITd6Mr/U5YmgaSjGDaFbzDNaioW40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tRm1e/btsL6ITd6Mr/U5YmgaSjGDaFbzDNaioW40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtRm1e%2FbtsL6ITd6Mr%2FU5YmgaSjGDaFbzDNaioW40%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;594&quot; height=&quot;292&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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-filename=&quot;edited_IMG_9337.JPG&quot; data-origin-width=&quot;1301&quot; data-origin-height=&quot;1723&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7xpqK/btsL6sQVADL/kVPkQd2iBcz3VuDdZuQDiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7xpqK/btsL6sQVADL/kVPkQd2iBcz3VuDdZuQDiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7xpqK/btsL6sQVADL/kVPkQd2iBcz3VuDdZuQDiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7xpqK%2FbtsL6sQVADL%2FkVPkQd2iBcz3VuDdZuQDiK%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;455&quot; height=&quot;603&quot; data-filename=&quot;edited_IMG_9337.JPG&quot; data-origin-width=&quot;1301&quot; data-origin-height=&quot;1723&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&lt;b&gt;[목차]&lt;/b&gt;&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;b&gt;2. 느낀점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 마치여&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 책에 대하여&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EXyqv/btsL7KXbdDe/7hsT8mfpzyoLMAE4DMGO60/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EXyqv/btsL7KXbdDe/7hsT8mfpzyoLMAE4DMGO60/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EXyqv/btsL7KXbdDe/7hsT8mfpzyoLMAE4DMGO60/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEXyqv%2FbtsL7KXbdDe%2F7hsT8mfpzyoLMAE4DMGO60%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;458&quot; height=&quot;588&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;588&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 로이 오셔로브와 블라디미르 코리코프가 쓴 단위 테스트에 관한 책이다. TDD가 아닌 &quot;&lt;b&gt;단위 테스트&lt;/b&gt;&quot; 자체에 초점을 맞추고 있으며, 테스트 작성법부터 시작해 신뢰성 있고 유지보수가 쉬운 테스트 작성 방법, 단위 테스트 도입 노하우, 레거시 코드에 단위테스트를 도입하는 방법 등 실용적인 가이드를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초판은 C#으로 예제가 작성되었는데, 이번 3판에서는 &lt;b&gt;JavaScript와 TypeScript&lt;/b&gt;로 바뀌었다. JS의 동적 타입과 TS의 정적 타입 예시를 모두 보여주는데, 대부분의 테스트 관련 서적이 Java 기반인 것에 비해 내가 주로 쓰는 언어로 되어있어서 반가웠다. 물론 이런 기술 서적의 개념들은 특정 언어에 국한되지 않아서 어떤 언어든 적용할 수 있다..!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책은 총 4부로 구성되어 있고, 각 부분은 다음과 같은 내용을 다룬다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1부: 단위 테스트의 기본기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1장에서는 테스트 단위의 정의, 시작점과 종료점 구분, 단위/통합 테스트의 차이, TDD와의 관계 등을 다룬다. 2장에서는 Jest를 사용한 테스트 작성법, AAA 패턴, 테스트 이름 짓기와 USE 전략, 테스트 구조화 방법 등을 소개한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2부:&amp;nbsp;실전&amp;nbsp;테스트&amp;nbsp;작성과&amp;nbsp;리팩터링&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3장은 &lt;b&gt;의존성 종류에 따라&lt;/b&gt; 목과 스텁을 어떻게 다르게 사용하는지 다룬다. 특히 &lt;b&gt;스텁&lt;/b&gt;을 활용해 &lt;b&gt;내부로 들어오는 의존성을 분리&lt;/b&gt;하는 방법과 리팩터링 기술을 집중적으로 다룬다. 순수 함수 스타일과 객체지향형 스타일로 각각의 접근법을 보여주어 개발자의 이해를 돕는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4장에서는 반대로 &lt;b&gt;목&lt;/b&gt;을 사용해 &lt;b&gt;외부로 나가는 의존성을 분리&lt;/b&gt;하는 방법을 소개한다. 목과 스텁의 차이점을 명확히 설명하고, 다시 순수함수 스타일과 객체지향 스타일로 다양한 예시를 통해 개념을 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3, 4장에서 수동으로 목과 스텁을 만드는 방식은 중복 코드와 복잡성 문제를 야기할 수 있다. 이를 해결하기 위해 5장에서는 격리 프레임워크를 사용하는 방법을 소개한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6장은 비동기 코드에 대한 테스트 기법을 다루며, 실무에서 자주 마주치는 비동기 로직의 테스트 방법을 제시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3부: 신뢰할 수 있는 테스트, 테스트의 유지보수성에 대한 모범 사레들&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1, 2부에서 단위 테스트의 기본기와 핵심 기술을 배웠다면, 3부는 테스트의 품질을 높이는 방법론을 제시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7장은 신뢰할 수 있는 테스트에 대해 깊이 파고든다. 잘못된 테스트, 신뢰성 없는 테스트의 패턴과 문제점을 살펴본다. 7장은 나 자신이 지금까지 작성했던 테스트들을 돌아보게 만드는 내용이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8장에서는 테스트의 유지보수성을 높이기 위한 다양한 기술과 접근법을 소개한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로&lt;b&gt; 7, 8장이 가장 중요하다고 생각&lt;/b&gt;한다. 이전 회사 경험을 되돌아보면, 유지보수의 어려움 때문에 방치된 수많은 실패한 테스트들이 떠오른다. 지금 와서 생각해보면, 그때 작성했던 테스트들이 정말 신뢰할 수 있는 것들이었는지, 혹시 그저 형식적으로 의무감에서 작성한 것은 아니었는지 의문이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4부: 실무에 단위 테스트를 도입하는 여러 방법&lt;/b&gt;&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;9장에서는 테스트 코드의 가독성을 다룬다. 이름 짓기는 항상 중요하고 어려운 작업이지만, 테스트에서는 그 중요성이 더욱 크게 느껴진다. 무엇을, 어떻게, 왜 테스트하는지 명확히 알 수 있도록 작성한다면 테스트 코드 자체가 훌륭한 문서화 도구가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10장은 효과적인 테스트 전략 수립 방법을 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11장에서는 조직 내 단위 테스트 도입 과정에서 발생할 수 있는 현실적인 문제들과 초기 도입 시 겪는 어려움들을 구체적으로 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;12장은 테스트 코드가 전혀 없는 레거시 코드에 테스트를 도입하는 방법을 소개한다. 단위테스트가 없는 기존 코드에 테스트를 적용해야 한다면 11, 12장이 큰 도움이 될 것이다. 세부 상황에 대한 더 자세한 예시가 있었으면 좋겠지만, 그래도 접근 방식에 대한 가이드로는 충분하다고 생각한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;느낀 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책을 읽으면서 지금까지의 내 테스트 방식의 실수들을 깨달았다. 특히 목과 스텁을 명확히 구분하지 않고 무분별하게 사용했던 점, 하나의 테스트에 여러 검증을 넣었던 점, 의존성 해결을 위해 불필요하게 복잡한 목 객체를 만들었던 경험들이 떠올랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책을 통해 그동안 잘못된 방법으로 테스트를 해왔음을 인정하게 되었다. 앞으로 이 책을 다시 정리하며 읽으면서 모범 사례들을 꼼꼼히 정리해볼 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 8장의 유지보수성 부분에서 가장 인상 깊은 문장이 있었다. &lt;b&gt;&quot;테스트가 장기적으로 가치가 있으려면 정말로 비즈니스에서 중요하게 생각하는 부분에서만 납득 가능한 이유로 실패해야 한다&quot;&lt;/b&gt;는 문장이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트의 실패는 프로덕션 코드의 실제 버그를 발견하는 &lt;b&gt;&quot;실제 실패&quot;&lt;/b&gt;여야 한다. 그 외의 이유로 발생하는 테스트 실패는 모두 &lt;b&gt;&quot;거짓 실패&quot;&lt;/b&gt;다. 테스트 유지보수성의 중요성을 뼈저리게 느끼고 있는데, 과거 유지보수 힘든 테스트코드 때문에 결국 테스트코드를 포기했던 개발 문화를 떠올리게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 잘 작성하는 방법에 대한 갈망이 있었는데, 이 책이 그 가이드가 될 것 같다. 책에서 언급한 다양한 테스트 관련 도서들도 시간 날 때 읽어보고 싶다. 특히 마이클 C. 페더스의 &lt;b&gt;&quot;레거시 코드 활용 전략&quot;&lt;/b&gt;이 읽고 싶지만, 아직 읽을 책들이 산더미라 언제 읽을지는 미지수다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소에 관심 있던 주제에 대한 책이라 흥미진진해하며 읽었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 읽기엔 아쉬워서 두고두고 생각날 때마다 가이드처럼 읽어도 좋을 것 같다.&lt;/p&gt;</description>
      <category>Books &amp;amp; Reviews</category>
      <category>unit test</category>
      <category>길벗</category>
      <category>길벗 리뷰</category>
      <category>단위 테스트</category>
      <category>단위 테스트의 기술</category>
      <category>단위태스트의기술</category>
      <category>레거시코드활용전략</category>
      <category>유닛테스트란?</category>
      <author>9Jaeng</author>
      <guid isPermaLink="true">https://sora9z.tistory.com/147</guid>
      <comments>https://sora9z.tistory.com/147#entry147comment</comments>
      <pubDate>Tue, 4 Feb 2025 22:58:07 +0900</pubDate>
    </item>
    <item>
      <title>[코드트리 한 달 사용기] 체계적이고 기본에 충실한 코테준비</title>
      <link>https://sora9z.tistory.com/146</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 이직을 준비하다 보면 다른 직군에 비해 준비해야 할 것들이 유독 많다는 것을 실감하게 된다. 기계공학과를 졸업하고 관련 직종으로 취업을 준비했던 경험과 비교해보면 그 차이가 확연히 느껴진다.&lt;br /&gt;&lt;br /&gt;일반적인&amp;nbsp;채용&amp;nbsp;프로세스는&amp;nbsp;서류&amp;nbsp;-&amp;nbsp;1차&amp;nbsp;면접&amp;nbsp;-&amp;nbsp;2차&amp;nbsp;면접&amp;nbsp;-&amp;nbsp;(추가&amp;nbsp;면접)&amp;nbsp;-&amp;nbsp;협상&amp;nbsp;-&amp;nbsp;합격의&amp;nbsp;순서를&amp;nbsp;따르지만,&amp;nbsp;현재&amp;nbsp;개발자&amp;nbsp;채용&amp;nbsp;시장에서는&amp;nbsp;대부분의&amp;nbsp;기업이&amp;nbsp;코딩테스트나&amp;nbsp;과제전형을&amp;nbsp;필수로&amp;nbsp;요구한다.&amp;nbsp;심지어&amp;nbsp;둘&amp;nbsp;다&amp;nbsp;요구하는&amp;nbsp;경우도&amp;nbsp;적지&amp;nbsp;않다.&amp;nbsp;여기에&amp;nbsp;기술면접&amp;nbsp;준비와&amp;nbsp;사이드&amp;nbsp;프로젝트까지&amp;nbsp;병행하려면&amp;nbsp;시간이&amp;nbsp;매우 부족하다.&lt;br /&gt;&lt;br /&gt;이런 상황에서 코딩테스트 준비는 종종 서류 합격 이후에 급하게 이루어지곤 했다. 프로그래머스와 백준으로 꾸준히 감을 유지하려 했지만, 실제로는 쉽지 않았다. 매번 급하게 준비하고 나면 '미리 준비할걸' 하는 후회를 종종 하곤 했다.&lt;br /&gt;&lt;br /&gt;'어떻게 하면 코딩테스트를 체계적으로 준비할 수 있을까?' 고민하던 중 코드트리 한 달 사용권을 체험할 수 있는 기회를 얻게 되었다. 코드트리는 이전부터 관심 있게 지켜보던 플랫폼이었지만, 유료 서비스라 고민을 많이 하였는데 좋은 기회였다.&lt;br /&gt;&lt;br /&gt;한&amp;nbsp;달간의&amp;nbsp;코드트리&amp;nbsp;사용&amp;nbsp;경험을&amp;nbsp;통해&amp;nbsp;개인적으로&amp;nbsp;부족하다고&amp;nbsp;느꼈던&amp;nbsp;부분들을&amp;nbsp;상당히&amp;nbsp;보완할&amp;nbsp;수&amp;nbsp;있었다.&amp;nbsp;이&amp;nbsp;글에서는&amp;nbsp;실제&amp;nbsp;사용&amp;nbsp;경험을&amp;nbsp;바탕으로&amp;nbsp;느낀&amp;nbsp;점들을&amp;nbsp;공유하고자&amp;nbsp;한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;체계적으로 기본기를 다질 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비전공자로서 코딩테스트를 준비하면서 가장 아쉬웠던 점은 탄탄한 기본기의 부족이었다. 공대를 졸업했지만 컴퓨터공학 전공이 아니다 보니, 자료구조나 알고리즘과 같은 핵심 개념들을 체계적으로 학습할 기회가 없었다. &quot;이것이 코딩테스트다 with 파이썬&quot;과 같은 책으로 독학하고 블로그를 참고하며 깃허브에 정리도 해보았지만, 한정된 문제 수와 실전 적용 가이드의 부족함이 늘 아쉬웠다. 이러한 요구사항은 코드트리를 통해 해소될 수 있었다.&lt;br /&gt;&lt;br /&gt;코드트리는 총 6개의 Trail로 구성된 체계적인 커리큘럼을 제공한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1938&quot; data-origin-height=&quot;1510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SeMcQ/btsL3xrvEuS/9Fyp64lmFgeV96LWNvLlLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SeMcQ/btsL3xrvEuS/9Fyp64lmFgeV96LWNvLlLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SeMcQ/btsL3xrvEuS/9Fyp64lmFgeV96LWNvLlLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSeMcQ%2FbtsL3xrvEuS%2F9Fyp64lmFgeV96LWNvLlLK%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;435&quot; data-origin-width=&quot;1938&quot; data-origin-height=&quot;1510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Novice Low (Trail 1): 다양한 프로그래밍 언어의 기초 문법 학습&lt;br /&gt;-&amp;nbsp;Novice&amp;nbsp;Mid&amp;nbsp;(Trail&amp;nbsp;2):&amp;nbsp;기초적인&amp;nbsp;구현&amp;nbsp;능력&amp;nbsp;배양&lt;br /&gt;-&amp;nbsp;Novice&amp;nbsp;High&amp;nbsp;(Trail&amp;nbsp;3):&amp;nbsp;문제&amp;nbsp;해결에&amp;nbsp;필요한&amp;nbsp;핵심&amp;nbsp;알고리즘과&amp;nbsp;자료구조&amp;nbsp;학습&lt;br /&gt;-&amp;nbsp;Intermediate&amp;nbsp;Low&amp;nbsp;(Trail&amp;nbsp;4):&amp;nbsp;BFS,&amp;nbsp;DFS,&amp;nbsp;DP&amp;nbsp;등&amp;nbsp;기본&amp;nbsp;알고리즘&amp;nbsp;학습&lt;br /&gt;-&amp;nbsp;Intermediate&amp;nbsp;Mid&amp;nbsp;(Trail&amp;nbsp;5):&amp;nbsp;Two&amp;nbsp;Pointer,&amp;nbsp;이분&amp;nbsp;탐색&amp;nbsp;등&amp;nbsp;최적화&amp;nbsp;기법과&amp;nbsp;고급&amp;nbsp;자료구조&amp;nbsp;학습&lt;br /&gt;-&amp;nbsp;Intermediate&amp;nbsp;High&amp;nbsp;(Trail&amp;nbsp;6):&amp;nbsp;트리,&amp;nbsp;위상&amp;nbsp;정렬&amp;nbsp;등&amp;nbsp;심화&amp;nbsp;알고리즘&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;나는 기본기 강화에 초점을 맞추어 Novice High부터 학습을 시작했다. 시간복잡도부터 시작해 기본 자료구조, 그래프 알고리즘까지 체계적으로 구성된 커리큘럼을 따라가며 단계적으로 복습 및 학습을 할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;1324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOme03/btsL4WQ6luH/PwUZF0l23ZMzQOVA5aXjpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOme03/btsL4WQ6luH/PwUZF0l23ZMzQOVA5aXjpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOme03/btsL4WQ6luH/PwUZF0l23ZMzQOVA5aXjpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOme03%2FbtsL4WQ6luH%2FPwUZF0l23ZMzQOVA5aXjpk%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;356&quot; height=&quot;401&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;1324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trail이 올라갈수록 점점 더 고급 자료구조, 알고리즘에 대해 학습한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnPiZe/btsL3SoJPGM/BiRZTakw0TTheOGXp37Kx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnPiZe/btsL3SoJPGM/BiRZTakw0TTheOGXp37Kx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnPiZe/btsL3SoJPGM/BiRZTakw0TTheOGXp37Kx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnPiZe%2FbtsL3SoJPGM%2FBiRZTakw0TTheOGXp37Kx1%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;389&quot; height=&quot;262&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;808&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드트리의 가장 큰 장점은 반복 학습을 통한 개념 강화하고 생각한다. 단순히 알고리즘에 대한 이론적 설명에 그치지 않고, 중간중간 실전 문제들을 통해 배운 개념을 계속 상기하고 적용해볼 수 있다. 이러한 반복 학습을 통해 자연스럽게 알고리즘 활용 능력이 체화되는 것을 경험할 수 있었다.&lt;br /&gt;&lt;br /&gt;특히 주목할 만한 점은 상세한&lt;b&gt; '해설' 제공&lt;/b&gt;이다. 프로그래머스나 백준에서는 다른 사람의 풀이나 토론을 통해 간접적으로만 학습할 수 있었던 반면, 코드트리는 공식 해설을 통해 의도된 풀이 방법과 접근 방식을 직접 확인할 수 있다. 이는 학습 효율을 높여주는 차별화된 장점이라고 생각한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;꾸준히 풀 수 있도록 하는 경험치 전략과 자동으로 심어지는 잔디&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩테스트는 지속적인 연습이 성패를 좌우한다. 시험 직전의 벼락치기로는 한계가 명확했고, 실전에서 제 실력을 발휘하기 위해서는 꾸준한 문제 풀이를 통해 감각을 유지하는 것이 중요하다.&lt;br /&gt;&lt;br /&gt;코드트리는&amp;nbsp;이러한&amp;nbsp;꾸준함을&amp;nbsp;유도하기&amp;nbsp;위해&amp;nbsp;두&amp;nbsp;가지&amp;nbsp;흥미로운&amp;nbsp;전략을&amp;nbsp;채택하고&amp;nbsp;있다.&amp;nbsp;첫째는&lt;b&gt;&amp;nbsp;경험치&amp;nbsp;획득&amp;nbsp;시스템&lt;/b&gt;이고,&amp;nbsp;둘째는&amp;nbsp;깃&lt;b&gt;허브&amp;nbsp;잔디&amp;nbsp;자동&amp;nbsp;심기&amp;nbsp;기능&lt;/b&gt;이다.&amp;nbsp;깃허브와&amp;nbsp;연동해&amp;nbsp;놓으면&amp;nbsp;문제를&amp;nbsp;풀&amp;nbsp;때마다&amp;nbsp;자동으로&amp;nbsp;잔디가&amp;nbsp;심어지는데,&amp;nbsp;점점&amp;nbsp;무성해지는&amp;nbsp;잔디를&amp;nbsp;보며&amp;nbsp;의미&amp;nbsp;있는&amp;nbsp;시간을&amp;nbsp;보내고&amp;nbsp;있다는&amp;nbsp;성취감을&amp;nbsp;느낄&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZriTz/btsL3Aocsd0/J5F80iunyMRbbx84aAX3k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZriTz/btsL3Aocsd0/J5F80iunyMRbbx84aAX3k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZriTz/btsL3Aocsd0/J5F80iunyMRbbx84aAX3k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZriTz%2FbtsL3Aocsd0%2FJ5F80iunyMRbbx84aAX3k0%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;733&quot; height=&quot;184&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;특히 경험치 시스템은 일일 목표 설정을 통해 학습 동기를 부여한다. 하루 90XP라는 구체적인 목표가 있다 보니, 자연스럽게 일정 수준 이상의 문제를 풀게 되는 동기부여가 된다. 물론 이러한 요소들이 완벽한 해결책은 아니지만, 학습 의욕을 높이고 지속적인 연습을 이끌어내는 데 효과적인 장치로 작용함을 느꼈다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘과 자료구조 학습은 단순히 코딩테스트 통과에만 있지 않고 프로그래머로서의 전반적인 문제 해결 능력을 키울 수 있는 도구라고 생각한다. 꾸준한 문제 풀이는 프로그래밍 감각을 유지하는 데 도움이 되며, 여러 자료구조와 알고리즘 지식은 현업에서 마주치는 복잡한 비즈니스 로직을 효율적으로 설계하고 구현하는 데 도움이 될 수 있다&lt;br /&gt;&lt;br /&gt;한 달간의 코드트리 사용 경험을 통해, 비전공자로서 느꼈던 기본기에 대한 갈증을 상당 부분 해소할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 남은 Trail들을 완주하며 더 깊이 있는 학습을 이어나가고 싶다.&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;</description>
      <category>Books &amp;amp; Reviews</category>
      <category>CodeTree</category>
      <category>글또</category>
      <category>리뷰</category>
      <category>코드트리</category>
      <category>코드트리후기</category>
      <author>9Jaeng</author>
      <guid isPermaLink="true">https://sora9z.tistory.com/146</guid>
      <comments>https://sora9z.tistory.com/146#entry146comment</comments>
      <pubDate>Mon, 3 Feb 2025 01:09:20 +0900</pubDate>
    </item>
  </channel>
</rss>