<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://dongzoolee.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://dongzoolee.github.io/" rel="alternate" type="text/html" /><updated>2025-09-03T04:10:31+00:00</updated><id>https://dongzoolee.github.io/feed.xml</id><title type="html">dongzoolee</title><subtitle>Minimal Jekyll theme for storytellers</subtitle><author><name>dongzoolee</name><email>me@leed.at</email></author><entry><title type="html">Tython: Python에 Typing 얹기</title><link href="https://dongzoolee.github.io/2023-09-04/tython" rel="alternate" type="text/html" title="Tython: Python에 Typing 얹기" /><published>2023-09-04T00:00:00+00:00</published><updated>2023-09-04T00:00:00+00:00</updated><id>https://dongzoolee.github.io/2023-09-04/tython</id><content type="html" xml:base="https://dongzoolee.github.io/2023-09-04/tython"><![CDATA[<p><img src="/assets/post-images/2023-09-04/tython/tython.png" alt="" width="20%" />
<br /></p>

<p>Python은 강타입 언어입니다. 서로 다른 타입 간의 연산이 금지되어 있고, 타입 변환을 위해서는 명시적으로 형변환을 해주어야 합니다.</p>

<p>하지만 C언어와 같이 변수의 데이터 타입을 명시적으로 지정하는 <code class="language-plaintext highlighter-rouge">정적 타이핑 언어</code>(Static Typed)가 아닙니다. 파이썬은 <code class="language-plaintext highlighter-rouge">동적 타이핑 언어</code>(Dynamically Typed)로, 한 변수에 할당된 데이터와 다른 타입을 가진 데이터를 언제든지 재할당할 수 있습니다.</p>

<p><code class="language-plaintext highlighter-rouge">a</code> 라는 변수에 string 데이터를 할당한 이후 언제든지 int, string[] 등의 다른 타입의 데이터를 재할당할 수 있다는 것입니다.</p>

<p><br />
타입을 지키지 않아도 코드 실행에 문제가 없다는 장점 덕분에 극한의 생산성을 발휘해내야 하는 해커톤, 빠르게 제품을 런칭해야 하는 스타트업에서 사용하기 좋은 특성을 가지고 있습니다.</p>

<p>하지만 서비스를 안정적으로 운영해야 하는 환경에 놓여 있다면, Python의 <code class="language-plaintext highlighter-rouge">동적 타이핑</code> 특성은 너무나 큰 장애물입니다. 실제로 문제가 있는 코드라면 런타임 환경에 가서야 터지기 때문이죠.</p>

<p><br /></p>

<h2 id="그래서-저희-코드-베이스는">🧐 그래서 저희 코드 베이스는..</h2>

<p><code class="language-plaintext highlighter-rouge">Any</code> type이 넘쳐나고, 2,000개가 넘는 파일에서 약 20,000개의 type error가 발생하고 있었습니다.</p>

<p>Python은 parameter와 return type의 type definition을 강제하지 않으니, 깨진 유리창은 계속 깨져만 갈 뿐이었습니다.</p>

<p>다른 개발자가 정의해놓은 type definition이 있으면 신나게 Type Hint의 도움을 받아 코드를 작성하기도 했지만, 잘못된 type definition을 작성해두었거나 outdated 정보가 적혀있는 경우도 많았습니다.</p>

<p><br />
실제 argument로 넘겨주는 데이터의 type과 parameter에 정의된 type이 달라도 에러가 발생하지 않았습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">execute_meeting</span><span class="p">(</span><span class="nb">id</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
	<span class="k">pass</span>

<span class="n">execute_meeting</span><span class="p">(</span><span class="mi">11</span><span class="p">)</span> <span class="c1"># actual: [int], expected: [str]
</span></code></pre></div></div>

<p>실제 return하는 데이터의 type과 return type이 달라도 에러가 발생하지 않았습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">extract_company_name</span><span class="p">(</span><span class="n">foo</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
	<span class="k">return</span> <span class="n">re</span><span class="p">.</span><span class="n">match</span><span class="p">(</span><span class="sa">r</span><span class="s">"bar"</span><span class="p">,</span> <span class="n">foo</span><span class="p">)</span> <span class="c1"># actual: [str | None], expected: [str]
</span></code></pre></div></div>

<p>단순한 오타도 인해 존재하지 않는 속성에 접근하는 경우도 마찬가지였습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">dataclass</span>
<span class="k">class</span> <span class="nc">User</span><span class="p">:</span>
	<span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
	<span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span>

<span class="n">dongzoo</span> <span class="o">=</span> <span class="n">User</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">full_name</span><span class="o">=</span><span class="s">"dongzoolee"</span><span class="p">)</span>
<span class="n">dongzoo</span><span class="p">.</span><span class="n">full_naem</span> <span class="c1"># "User" has no attribute "full_naem"
</span></code></pre></div></div>

<p><br /></p>

<p>그리고, 이 오류들을 전부 사람이 완벽하게 판단해내거나, 리뷰어가 육안으로 발견해내기에는 어려움이 있었습니다.</p>

<p>저희 팀은 Static Type Checker 도입의 필요성을 뼈저리게 느꼈습니다.</p>

<p><br /></p>

<h2 id="1️⃣-mypy-도입의-시작">1️⃣ mypy 도입의 시작</h2>

<p>1년 전 이 날은 객체에 존재하지 않는 속성에 접근하는 코드로 인해 버그가 발생하여 핫픽스 패치를 넣은 날이었습니다.</p>

<p>QA 단계에서 잡히지 않았다는 점도 문제이긴 했지만, 저는 다른 Static Typed Language라면 발생하지 않았을 문제라고 생각하여 바로 Static Type Checking 툴을 찾아보았습니다.</p>

<p><br />
그리고 mypy 라이브러리를 도입하기로 결정했습니다.</p>

<p>그 당시에는 아직 <code class="language-plaintext highlighter-rouge">0.9xx</code> 버전 밖에 릴리즈되지 않은 상태였지만, Python 재단에서 직접 관리하는 라이브러리라는 점이 큰 신뢰감을 안겨주었습니다.</p>

<p><img src="/assets/post-images/2023-09-04/tython/Untitled.png" alt="" width="80%" /></p>

<p><br />
mypy를 dev dependency에 추가하고, 저희 프로젝트에 맞는 config를 작성하여 도입 준비를 마쳤습니다.</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># pyproject.toml</span>

<span class="nn">[tool.mypy]</span>
<span class="py">python_version</span> <span class="p">=</span> <span class="s">"3.11"</span>

<span class="py">exclude</span> <span class="p">=</span> <span class="s">"DIRECTORIES_TO_EXCLUDE"</span>

<span class="py">plugins</span> <span class="p">=</span> <span class="nn">["mypy_django_plugin.main"]</span>
<span class="py">enable_incomplete_feature</span> <span class="p">=</span> <span class="nn">["Unpack"]</span>

<span class="py">ignore_missing_imports</span> <span class="p">=</span> <span class="kc">true</span>
<span class="py">disallow_untyped_defs</span> <span class="p">=</span> <span class="kc">true</span>
<span class="py">check_untyped_defs</span> <span class="p">=</span> <span class="kc">true</span>
<span class="py">local_partial_types</span> <span class="p">=</span> <span class="kc">true</span>
<span class="py">warn_unused_ignores</span> <span class="p">=</span> <span class="kc">true</span>
<span class="py">warn_redundant_casts</span> <span class="p">=</span> <span class="kc">true</span>
</code></pre></div></div>
<p><br /></p>

<p>VSC를 사용하고 있는 저희 팀원들 모두 mypy extension을 설치하여 에러를 IDE에서 직접 눈으로 확인할 준비까지 마쳤습니다.</p>

<p><img src="/assets/post-images/2023-09-04/tython/Untitled%201.png" alt="" width="80%" /></p>

<p>당시만 해도 Microsoft 공식 mypy extension이 없었는데 올해 4월에 Microsoft에서 공식 extension을 출시했습니다. 사용해보지는 않아서 두 extension의 차이는 잘 모르겠지만 아직 <code class="language-plaintext highlighter-rouge">Preview</code> 뱃지가 붙어 있네요.</p>

<p><img src="/assets/post-images/2023-09-04/tython/Untitled%202.png" alt="" width="80%" />
<br /></p>

<h3 id="난항">난항</h3>

<p>위에서 언급했다시피, 저희 코드 베이스에서 mypy 에러가 발생하는 파일의 수는 2,000개가 넘었습니다.</p>

<p>감당할 수 없는 에러의 숫자로 IDE는 빨간불에 뒤덮히기 시작했습니다.</p>

<p><img src="/assets/post-images/2023-09-04/tython/blurred-captain.png" alt="" width="80%" /></p>

<p>거의 모든 파일과 line에 빨간불이 들어오니 절대로 해결할 수 없는 문제들처럼 보였고, mypy 또한 성능 저하를 일으켜 당장에 작성하고 있는 코드의 type error를 확인하는 데에도 약 10-20초 이상이 소요되었습니다.</p>

<p>심지어 vim에서는 렉이 너무 심하여 작업이 아예 불가능했습니다.</p>

<p>팀원들은 하나 둘 mypy extension을 제거하기 시작했습니다.</p>

<p><br /></p>

<h3 id="점진적-리팩토링의-실패">점진적 리팩토링의 실패</h3>

<p>에러 코드별로 에러가 발생하는 파일들의 목록을 정리해놓고 팀원들과 매일매일 파일 1개씩 담당하여 type error를 수정하기로 했습니다.</p>

<p>수정이 완료된 에러 코드들을 하나씩 CI Check에서 활성화하여, 궁극적으로는 모든 에러 코드들을 CI에서 활성화시키자는 취지였습니다.</p>

<p>약 3달 간 리팩토링을 진행했지만 승산은 없었습니다.</p>

<p>type error를 수정하려면 각 파일의 로직을 이해하고 있거나 이해하는 데에 시간을 사용해야 했는데, 이 비용은 팀원들에게 꽤 비쌌습니다.</p>

<p>근본적으로는 작업량이 너무 방대했습니다. 점진적 리팩토링은 실패로 돌아갔습니다.</p>

<p><br /></p>

<h3 id="이전-것들은-묻고-새로-시작하자">이전 것들은 묻고, 새로 시작하자</h3>

<p>현재 발생하는 모든 type error를 전부 묻어버리고, 앞으로 추가되는 코드부터 type error를 검사하자는 발상입니다. 지금이라도 mypy를 활성화하지 않으면 type error가 평생 무한히 늘어나기만 할 것이기 때문이죠.</p>

<p>그런데 이 <strong>묻어버리</strong>는 과정을 어떻게 실행할 것인가가 문제였습니다.</p>

<p>Mypy는 type error를 무시하는 방법으로 아래 세 가지를 제공합니다.</p>

<ol>
  <li>검사에서 제외할 directory를 config file에 정의하기</li>
  <li>검사에서 제외할 파일의 상단에 <code class="language-plaintext highlighter-rouge"># mypy: ignore-errors</code> comment 추가하기</li>
  <li>
    <p>에러가 발생하는 line에 <code class="language-plaintext highlighter-rouge"># type: ignore</code> comment 추가하기</p>

    <p><code class="language-plaintext highlighter-rouge"># type: ignore</code>는 Python에서 type error ignore를 도와주는 magic comment입니다.</p>

    <p>에러가 발생하는 line의 오른쪽에 <code class="language-plaintext highlighter-rouge"># type: ignore</code> comment를 붙여주면 mypy가 해당 line의 type error를 검사하지 않습니다.</p>

    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="s">"string"</span> <span class="o">+</span> <span class="mi">1_000</span> <span class="c1"># type: ignore
</span></code></pre></div>    </div>

    <p><code class="language-plaintext highlighter-rouge">[]</code> 에 에러 코드를 넣어주면 해당 에러 코드만 검사하지 않도록 할 수도 있습니다.</p>

    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="s">"string"</span> <span class="o">+</span> <span class="mi">1_000</span> <span class="c1"># type: ignore[wrong-concatenation]
</span></code></pre></div>    </div>
  </li>
</ol>

<p>회사 동료인 <a href="https://github.com/tonynamy">@tonynamy</a>는 3번의 방법에 주목했습니다. 바로 type error가 발생하는 모든 line에 각 에러 코드에 맞는 <code class="language-plaintext highlighter-rouge"># type: ignore[...]</code> comment를 달아버리는 것입니다.</p>

<p>Mypy 실행 결과로 출력되는 에러가 발생한 파일 경로, 줄 번호, 에러 코드를 파싱하여 해당 위치에 <code class="language-plaintext highlighter-rouge"># type: ignore[...]</code> comment를 붙여주는 스크립트를 작성하여, 저희 코드 베이스에 실행하셨습니다.</p>

<p><img src="/assets/post-images/2023-09-04/tython/Untitled%203.png" alt="" width="80%" /></p>

<p>제가 작업한 스크립트를 오픈소스로 공개해달라고 요청했는데, 아직 공개하시진 않았습니다 😅</p>

<p><br /></p>

<h3 id="good-boy-scouts-">Good Boy scouts 😎</h3>

<p>위 패치가 머지된 이후, CI에서 모든 Mypy 에러를 검사하기 시작했습니다. 이제는 Mypy 에러가 발생하는 코드가 master 브랜치에 머지될 수 없습니다. 깨어진 유리창이 더 조각나는 일은 더이상 발생하지 않는 것이죠.</p>

<p>그리고 본인이 다루는 레거시 코드에 <code class="language-plaintext highlighter-rouge"># type: ignore</code> comment가 붙어 있다면, 일단 해당 부분의 type error들을 전부 해결하고 작업을 시작하고 있습니다.</p>

<p>착한 보이 스카우트 팀원들 덕분에 <code class="language-plaintext highlighter-rouge"># type: ignore</code> comment는 매일매일 정말 빠른 속도로 줄어들고 있습니다.</p>

<p><br /></p>

<h2 id="2️⃣구원자-typed-graphene">2️⃣ 구원자 typed-graphene</h2>

<p><a href="https://github.com/graphql-python/graphene">Graphene</a>은 Python에서 GraphQL Schema와 Type을 편리하게 작성할 수 있도록 도와주는 라이브러리입니다.</p>

<p>그런데, 저희가 사용하고 있는 이 라이브러리의 제일 큰 단점이 있습니다.</p>

<p>바로 Python native type(str, int, [], …), dataclass, TypedDict를 사용하여 Schema를 정의할 수 없다는 점입니다. 다시 말해, Graphene에서 제공하는 Scalar class(String, Int, List, …)를 사용하여 Schema를 정의해야 한다는 것인데, 이 Scalar class들은 Mypy에서 전부 <code class="language-plaintext highlighter-rouge">Any</code>로 평가되는 문제가 있습니다.</p>

<p>(사실 저희 코드 베이스에 <code class="language-plaintext highlighter-rouge">Any</code> type을 난무하게 만든 주범은 Graphene입니다)</p>

<p>관련 이슈도 있었지만, 타이핑 지원 계획은 전혀 없어 보였습니다.</p>

<p><a href="https://github.com/graphql-python/graphene/issues/966">https://github.com/graphql-python/graphene/issues/966</a></p>

<p>이를 해결하기 위해 한 개발자가 타입 패키지를 만들었지만, 해당 패키지 개발자는 고장난 패키지를 방치해둔채로 손을 떼버렸습니다.</p>

<p><a href="https://github.com/trialspark/graphene-stubs">https://github.com/trialspark/graphene-stubs</a></p>

<p><img src="/assets/post-images/2023-09-04/tython/Untitled%204.png" alt="" width="80%" /></p>

<p>Graphene의 문제를 완전히 해결한 Strawberry라는 라이브러리가 등장했지만, 현재 저희 프로젝트는 너무나 많은 Schema와 Type들이 Graphene으로 작성되어 있어 migration이 거의 불가능한 수준이었습니다.</p>

<p><a href="https://github.com/strawberry-graphql/strawberry">https://github.com/strawberry-graphql/strawberry</a></p>

<p>어느날 회사 동료인 <a href="https://github.com/tonynamy">@tonynamy</a>님께서 Graphene에서 Python native type, dataclass, TypedDict를 사용하여 GraphQL Schema를 만들 수 있도록 도와주는 typed-graphene이라는 패키지를 들고 왔습니다.</p>

<p><a href="https://github.com/tonynamy/typed-graphene">https://github.com/tonynamy/typed-graphene</a></p>

<p>아래 After 코드를 보면, graphene으로부터 아무 것도 import 하지 않고 Python native type만 정의해서 Schema를 작성해나갈 수 있음을 확인할 수 있습니다.</p>

<p><strong>Before: w/Graphene</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">graphene</span> <span class="kn">import</span> <span class="n">Field</span><span class="p">,</span> <span class="n">Int</span><span class="p">,</span> <span class="n">ObjectType</span><span class="p">,</span> <span class="n">String</span>

<span class="k">class</span> <span class="nc">UserType</span><span class="p">(</span><span class="n">ObjectType</span><span class="p">):</span>
    <span class="nb">id</span> <span class="o">=</span> <span class="n">Int</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">full_name</span> <span class="o">=</span> <span class="n">String</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">age</span> <span class="o">=</span> <span class="n">Int</span><span class="p">()</span>

<span class="k">class</span> <span class="nc">UserQuery</span><span class="p">:</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="n">UserType</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="nb">id</span><span class="o">=</span><span class="n">Int</span><span class="p">())</span>

    <span class="k">def</span> <span class="nf">resolve_user</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">info</span><span class="p">,</span> <span class="o">**</span><span class="n">data</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">UserType</span><span class="p">:</span>
        <span class="n">data</span><span class="p">[</span><span class="s">"id"</span><span class="p">]</span>  <span class="c1"># [Any]
</span>        <span class="k">return</span> <span class="n">UserType</span><span class="p">(</span>
            <span class="nb">id</span><span class="o">=</span><span class="p">...,</span>
            <span class="n">full_name</span><span class="o">=</span><span class="p">...,</span>
            <span class="n">age</span><span class="o">=</span><span class="p">...,</span>
        <span class="p">)</span>
</code></pre></div></div>

<p><strong>After: w/TypedGraphene</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">TypedDict</span><span class="p">,</span> <span class="n">Unpack</span>

<span class="kn">from</span> <span class="nn">typed_graphene</span> <span class="kn">import</span> <span class="n">TypedField</span>

<span class="o">@</span><span class="n">dataclass</span>
<span class="k">class</span> <span class="nc">UserType</span><span class="p">:</span>
    <span class="nb">id</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">age</span><span class="p">:</span> <span class="nb">int</span> <span class="o">|</span> <span class="bp">None</span>

<span class="k">class</span> <span class="nc">UserFieldArguments</span><span class="p">(</span><span class="n">TypedDict</span><span class="p">):</span>
    <span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>

<span class="k">class</span> <span class="nc">UserQuery</span><span class="p">:</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">TypedField</span><span class="p">(</span><span class="n">UserType</span><span class="p">,</span> <span class="o">**</span><span class="n">UserFieldArguments</span><span class="p">.</span><span class="n">__annotations__</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">resolve_user</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">info</span><span class="p">,</span> <span class="o">**</span><span class="n">data</span><span class="p">:</span> <span class="n">Unpack</span><span class="p">[</span><span class="n">UserFieldArguments</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">UserType</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">UserType</span><span class="p">(</span>
            <span class="nb">id</span><span class="o">=</span><span class="p">...,</span>
            <span class="n">full_name</span><span class="o">=</span><span class="p">...,</span>
            <span class="n">age</span><span class="o">=</span><span class="p">...,</span>
        <span class="p">)</span>
</code></pre></div></div>

<p>typed-graphene 덕분에 GraphQL Schema의 모든 Argument와 Field의 타이핑이 가능해졌습니다.</p>

<p>약 4달 전부터 작성된 GraphQL Query, Mutation, Type들은 전부 typed-graphene 패키지를 사용하여 작성되고 있습니다. 타이핑이 100% 지켜진 상태로 말이죠.</p>

<h2 id="이외에도">😉 이외에도</h2>

<p>도저히 고쳐지지 않는 type error를 가진 외부 라이브러리를 직접 wrapping하여 강제로 type casting한 후 사용하기도 하고, 직접 외부 라이브러리의 stub 패키지를 제작하여 사용하기도 합니다.</p>

<h2 id="물론-아직은">😅 물론 아직은</h2>

<p>TypeScript와 동작이 완전히 동일하지는 않습니다. 저를 포함한 많은 팀원들이 엄격한 타입 체킹의 늪에서 매일매일 발버둥치고,, 또 발버둥치고 있습니다.</p>

<p><img src="/assets/post-images/2023-09-04/tython/Untitled%205.png" alt="" /></p>

<p><img src="/assets/post-images/2023-09-04/tython/Untitled%206.png" alt="" /></p>

<p><img src="/assets/post-images/2023-09-04/tython/Untitled%207.png" alt="" /></p>

<p>그리고 pip에 등록된 많은 라이브러리들이 타입 패키지(stub)를 공식적으로 지원하지 않습니다.</p>

<p>Mypy 또한 TypeGuard가 이제 막 지원되기 시작했고, TypedDict의 type narrowing 기능도 아직은 걸음마 수준입니다. 그렇지만 Mypy는 거의 2-3주에 한 번씩은 신규 버전을 출시하는 등, 굉장히 빠른 발전을 보이고 있어 이 정도 불편함은 잠시 안고 가도 괜찮겠다는 생각을 하고 있습니다.</p>

<h2 id="마무리하며">👋🏼 마무리하며</h2>

<p>TypeScript 혹은 Mypy의 type check를 통과하는 fully type-safe한 코드를 작성하기 위해서는 그렇지 않을 때보다 1.5배 정도의 노력을 더 써야합니다. 실제로 type error를 고치기 위해 고생하다가 개발 일정에 차질이 생기는 경우도 다반사입니다.</p>

<p>특히나 기존에 잘 작동하던 코드에 Generic을 추가하기라도 하면 여기저기에서 type error가 발생하는 것은 일도 아닙니다.</p>

<p>ZUZU에서는 JSX → TSX 마이그레이션, Graphene → TypedGraphene 마이그레이션, Mypy 타입 에러 수정 등 Type 관련 귀찮은 과제들을 해결하느라 꽤나 땀을 흘렸고 끝없이 땀흘릴 예정이지만, 제품 초기부터 완벽한 코드를 고집하며 개발을 했었다면 아마 제품을 시장에 내놓지도 못 하고 개발만 하다가 끝나 버렸을 수도 있습니다.</p>

<p>가끔은 밉기도 한 완벽하지 않은 코드들 덕분에 빠르게 서비스를 런칭하고, 생산성 높은 개발을 할 수 있지 않았나 싶습니다.</p>]]></content><author><name>dongzoolee</name></author><category term="일기" /><summary type="html"><![CDATA[동적 타이핑 언어에서 살아남기]]></summary></entry><entry><title type="html">Alpine Linux에 크게 데인 날 🥵</title><link href="https://dongzoolee.github.io/2023-05-12/alpine-linux-so-hot" rel="alternate" type="text/html" title="Alpine Linux에 크게 데인 날 🥵" /><published>2023-05-12T00:00:00+00:00</published><updated>2023-05-12T00:00:00+00:00</updated><id>https://dongzoolee.github.io/2023-05-12/alpine-linux-so-hot</id><content type="html" xml:base="https://dongzoolee.github.io/2023-05-12/alpine-linux-so-hot"><![CDATA[<p>오늘 아침부터 회사가 시끌시끌했습니다.</p>

<p>CI에서 체크하는 백엔드 테스트가 전부 실패했기 때문인데요, 분명 테스트 코드나 테스트 데이터에는 문제가 하나도 없었습니다.</p>

<p><img src="/assets/post-images/2023-05-12/alpine-linux-so-hot/2023-05-13-03-00-52.png" alt="" /></p>

<p>순간 머리 속으로 “제 2의 <a href="https://github.com/left-pad/left-pad/issues/4">left-pad 사건</a>인가..?” 하는 생각이 스쳐 지나갔습니다.. 그리고 결론적으로 나름? 유사한 사건이었습니다 😇</p>

<p>회사 동료인 <a href="https://github.com/StationSoen">@StationSoen</a>님과 함께 4시간을 쏟아 문제의 원인을 분석하고 해결한 과정을 기록해보려고 합니다.</p>

<h2 id="원인-찾기-">원인 찾기 🧐</h2>

<p>실패하는 테스트는 문서 스냅샷 테스트와 DB Call을 사용하는 일부 테스트였습니다.</p>

<h3 id="-스냅샷-테스트">📸 스냅샷 테스트</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">======================================================================</span>
<span class="n">FAIL</span><span class="p">:</span> <span class="n">test_xxx_document</span>
<span class="o">----------------------------------------------------------------------</span>
<span class="n">Traceback</span> <span class="p">(</span><span class="n">most</span> <span class="n">recent</span> <span class="n">call</span> <span class="n">last</span><span class="p">):</span>
  <span class="n">File</span> <span class="s">"/home/runner/work/captain/captain/zuzu/tests/xx/text_xx_document.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">111</span><span class="p">,</span> <span class="ow">in</span> <span class="n">test_xx_form_document</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">assertPdfEqual</span><span class="p">(</span><span class="n">pdf</span><span class="p">,</span> <span class="s">"option-consent-form.pdf"</span><span class="p">)</span>
  <span class="n">File</span> <span class="s">"/home/runner/work/captain/captain/zuzu/tests/xx/base.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">95</span><span class="p">,</span> <span class="ow">in</span> <span class="n">assertPdfEqual</span>
    <span class="k">raise</span> <span class="n">e</span>
  <span class="n">File</span> <span class="s">"/home/runner/work/captain/captain/zuzu/tests/xx/base.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">79</span><span class="p">,</span> <span class="ow">in</span> <span class="n">assertPdfEqual</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span>
<span class="nb">AssertionError</span><span class="p">:</span> <span class="mf">0.0006538025445077359</span> <span class="o">!=</span> <span class="mi">0</span> <span class="p">:</span> <span class="n">The</span> <span class="n">PDFs</span> <span class="n">are</span> <span class="n">different</span><span class="p">.</span>
<span class="o">----------------------------------------------------------------------</span>
</code></pre></div></div>

<p>보통 스냅샷 테스트가 실패하는 원인은 두 가지입니다.</p>

<ol>
  <li>테스트 시 생성하는 데이터가 실행할 때마다 다른 데이터가 생성되는 경우 (랜덤하게 실패하는 테스트)</li>
  <li>글자 크기나 자간 옵션을 고정하지 않아서 스타일이 살짝 다른 문서가 생성되는 경우</li>
</ol>

<p>아무리 찾아봐도 해당 문서를 생성하는 코드는 잘못이 없었습니다.</p>

<p>그리고 로컬에서는 해당 테스트를 아무리 실행해도 성공하고, CI 환경에서는 100% 실패했습니다.</p>

<p>문서 스냅샷 테스트의 비교 대상 스냅샷과 테스트 과정에 생성한 스냅샷의 diff 이미지를 출력해보니 다음과 같았습니다.</p>

<p><img src="/assets/post-images/2023-05-12/alpine-linux-so-hot/2023-05-13-03-01-20.png" alt="" /></p>

<p>이 정도면.. 두 문서의 차이는 없다고 봐야겠지요..?</p>

<p>아무리 봐도 알 수가 없어서 위 diff 이미지에 나와 있는 글자 중 하나를 크게 확대해서 확인해봤습니다.</p>

<div style="display:flex">
    <img width="45%" src="/assets/post-images/2023-05-12/alpine-linux-so-hot/2023-05-13-03-01-32.png" />
    <img width="45%" src="/assets/post-images/2023-05-12/alpine-linux-so-hot/2023-05-13-03-01-53.png" />
</div>

<p>차이가 보이시나요? 글자 주변에 번진 픽셀의 분포가 아주 미세하게 차이가 있습니다 😓</p>

<p>바로 저희가 Word 문서를 PDF로 변환할 때 사용하는 LibreOffice의 버전이 달라지지는 않았는지 확인하러갔습니다.</p>

<p>로컬이나 프로덕션 환경에서는 이미 빌드된 Docker image를 사용하기 때문에 영향은 없지만, CI 환경에서는 이미지를 매번 빌드하기 때문에 뭔가 다른 버전의 라이브러리가 설치되었을 가능성을 의심했습니다.</p>

<p>이미지를 로컬에서 직접 빌드해보니 역시나 마이너 버전이 업데이트된 LibreOffice 패키지가 설치되어 있었습니다.</p>

<p><strong>기존 이미지의 패키지 목록</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">/</span><span class="n">app</span> <span class="c1"># apk list
</span><span class="n">libreoffice</span><span class="o">-</span><span class="mf">7.3</span><span class="p">.</span><span class="mf">7.2</span><span class="o">-</span><span class="n">r0</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">base</span><span class="o">-</span><span class="mf">7.3</span><span class="p">.</span><span class="mf">7.2</span><span class="o">-</span><span class="n">r0</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">calc</span><span class="o">-</span><span class="mf">7.3</span><span class="p">.</span><span class="mf">7.2</span><span class="o">-</span><span class="n">r0</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">common</span><span class="o">-</span><span class="mf">7.3</span><span class="p">.</span><span class="mf">7.2</span><span class="o">-</span><span class="n">r0</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">connector</span><span class="o">-</span><span class="n">postgres</span><span class="o">-</span><span class="mf">7.3</span><span class="p">.</span><span class="mf">7.2</span><span class="o">-</span><span class="n">r0</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">draw</span><span class="o">-</span><span class="mf">7.3</span><span class="p">.</span><span class="mf">7.2</span><span class="o">-</span><span class="n">r0</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">impress</span><span class="o">-</span><span class="mf">7.3</span><span class="p">.</span><span class="mf">7.2</span><span class="o">-</span><span class="n">r0</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">lang</span><span class="o">-</span><span class="n">en_us</span><span class="o">-</span><span class="mf">7.3</span><span class="p">.</span><span class="mf">7.2</span><span class="o">-</span><span class="n">r0</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">math</span><span class="o">-</span><span class="mf">7.3</span><span class="p">.</span><span class="mf">7.2</span><span class="o">-</span><span class="n">r0</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">writer</span><span class="o">-</span><span class="mf">7.3</span><span class="p">.</span><span class="mf">7.2</span><span class="o">-</span><span class="n">r0</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreofficekit</span><span class="o">-</span><span class="mf">7.3</span><span class="p">.</span><span class="mf">7.2</span><span class="o">-</span><span class="n">r0</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
</code></pre></div></div>

<p><strong>새로 빌드한 이미지의 패키지 목록</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">/</span><span class="n">app</span> <span class="c1"># apk list
</span><span class="n">libreoffice</span><span class="o">-</span><span class="mf">7.5</span><span class="p">.</span><span class="mf">3.2</span><span class="o">-</span><span class="n">r2</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">base</span><span class="o">-</span><span class="mf">7.5</span><span class="p">.</span><span class="mf">3.2</span><span class="o">-</span><span class="n">r2</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">calc</span><span class="o">-</span><span class="mf">7.5</span><span class="p">.</span><span class="mf">3.2</span><span class="o">-</span><span class="n">r2</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">common</span><span class="o">-</span><span class="mf">7.5</span><span class="p">.</span><span class="mf">3.2</span><span class="o">-</span><span class="n">r2</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">connector</span><span class="o">-</span><span class="n">postgres</span><span class="o">-</span><span class="mf">7.5</span><span class="p">.</span><span class="mf">3.2</span><span class="o">-</span><span class="n">r2</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">draw</span><span class="o">-</span><span class="mf">7.5</span><span class="p">.</span><span class="mf">3.2</span><span class="o">-</span><span class="n">r2</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">impress</span><span class="o">-</span><span class="mf">7.5</span><span class="p">.</span><span class="mf">3.2</span><span class="o">-</span><span class="n">r2</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">lang</span><span class="o">-</span><span class="n">en_us</span><span class="o">-</span><span class="mf">7.5</span><span class="p">.</span><span class="mf">3.2</span><span class="o">-</span><span class="n">r2</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">math</span><span class="o">-</span><span class="mf">7.5</span><span class="p">.</span><span class="mf">3.2</span><span class="o">-</span><span class="n">r2</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreoffice</span><span class="o">-</span><span class="n">writer</span><span class="o">-</span><span class="mf">7.5</span><span class="p">.</span><span class="mf">3.2</span><span class="o">-</span><span class="n">r2</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
<span class="n">libreofficekit</span><span class="o">-</span><span class="mf">7.5</span><span class="p">.</span><span class="mf">3.2</span><span class="o">-</span><span class="n">r2</span> <span class="n">aarch64</span> <span class="p">{</span><span class="n">libreoffice</span><span class="p">}</span> <span class="p">(</span><span class="n">MPL</span><span class="o">-</span><span class="mf">2.0</span><span class="p">)</span> <span class="p">[</span><span class="n">installed</span><span class="p">]</span>
</code></pre></div></div>

<p>그런데 아무리 생각을 해봐도 리눅스 환경에서 버전이 고정된 패키지를 설치해본 적이 없습니다 🤔 오히려 패키지를 설치할 때마다 매번 습관적으로 <code class="language-plaintext highlighter-rouge">apk update</code>를 실행하던 기억 밖에 없고 말이죠.</p>

<p>Alpine Linux 패키지 목록 페이지에 가보아도 이전 버전에 대한 내용은 눈을 씻어도 찾기 힘들었습니다. 이전 버전의 패키지도 제공하는 npm이나 다른 패키지 매니저들과는 다르게 말이죠.</p>

<p><img src="/assets/post-images/2023-05-12/alpine-linux-so-hot/2023-05-13-03-02-05.png" alt="" /></p>

<p>찾아보니 Alpine Linux는 패키지 버전을 고정하거나, 이전 버전의 패키지를 설치하는 것을 지원하지 않았습니다.</p>

<p>Alpine Linux용 패키지를 배포할 때 어떤 버전(Branch)의 Alpine Linux에 link할지를 선택해서, 해당 버전의 branch에 push하면 해당 버전의 Alpine Linux에서 밖에 이용하지 못 하는 패키지가 되는 것입니다.</p>

<p>(참고: <a href="https://stschindler.medium.com/the-problem-with-docker-and-alpines-package-pinning-18346593e891">https://stschindler.medium.com/the-problem-with-docker-and-alpines-package-pinning-18346593e891</a>)</p>

<h3 id="첫-시도---실패-">첫 시도 - 실패 ❌</h3>

<p>처음에 이 사실을 깨닫고 OS의 레파지토리 목록에 <code class="language-plaintext highlighter-rouge">v3.17</code>을 추가하여 어떻게든 강제로 version fixing을 해보려고 시도해봤습니다.</p>

<div class="language-docker highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">RUN </span><span class="nb">echo</span> <span class="s2">"http://dl-cdn.alpinelinux.org/alpine/v3.17/main/"</span> <span class="o">&gt;&gt;</span> /etc/apk/repositories
<span class="k">RUN </span>apk add <span class="nt">-U</span> <span class="nt">--no-cache</span> <span class="s2">"libreoffice==7.5.3.2-r2"</span>
</code></pre></div></div>

<p>뭐, 결과는 당연했습니다. world가 맞지 않아 설치할 수 없답니다.</p>

<div class="language-docker highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ERROR: unsatisfiable constraints:
  libreoffice-7.5.3.2-r2:
    breaks: world[libreoffice=7.5.3.2-r2]
</code></pre></div></div>

<p>더 찾아보면 어떻게든 해결해볼 수는 있을 것 같았지만, Alpine Linux의 패키지 관리 원칙에 맞지 않는 우회책이라서 그냥 포기했습니다.</p>

<h3 id="두번째-시도---성공-">두번째 시도 - 성공 ✌🏼</h3>

<p>계속 <code class="language-plaintext highlighter-rouge">7.5.3.2-r2</code> 버전을 Alpine Linux 패키지 목록 사이트에서 찾아다니다보니, 결국 저희가 찾던 버전은 Alpine Linux 3.17 branch에 숨어 있는 것을 발견했습니다 😭</p>

<p><img src="/assets/post-images/2023-05-12/alpine-linux-so-hot/2023-05-13-03-02-38.png" alt="" /></p>

<p>그리고 저희의 Dockerfile에서도 Alpine Linux의 메이저 버전 3만 명시한 상태여서 Alpine Linux 3.18의 배포로 인해 자동으로 3.17 → 3.18을 사용하게 된 것입니다. 당연히 3.17 버전과 link된 LibreOffice 2.7.2 버전이 아니라 3.18 버전과 link된 LibreOffice 2.7.5가 설치된 것이구요.</p>

<p><img src="/assets/post-images/2023-05-12/alpine-linux-so-hot/2023-05-13-03-02-53.png" alt="" /></p>

<p>Alpine Linux 버전을 3.17로 고정시켜줌으로써 문제를 해결할 수 있었습니다.</p>

<h3 id="postgresql-db-call-오류">🤖 PostgreSQL DB Call 오류</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">File</span> <span class="s">"/home/runner/.local/share/virtualenvs/captain-CVyYfSri/lib/python3.11/site-packages/django/db/models/query.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">57</span><span class="p">,</span> <span class="ow">in</span> <span class="n">__iter__</span>
    <span class="n">results</span> <span class="o">=</span> <span class="n">compiler</span><span class="p">.</span><span class="n">execute_sql</span><span class="p">(</span>
              <span class="o">^^^^^^^^^^^^^^^^^^^^^</span>
  <span class="n">File</span> <span class="s">"/home/runner/.local/share/virtualenvs/captain-CVyYfSri/lib/python3.11/site-packages/django/db/models/sql/compiler.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">1361</span><span class="p">,</span> <span class="ow">in</span> <span class="n">execute_sql</span>
    <span class="n">cursor</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="n">params</span><span class="p">)</span>
  <span class="n">File</span> <span class="s">"/home/runner/.local/share/virtualenvs/captain-CVyYfSri/lib/python3.11/site-packages/django/db/backends/utils.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">67</span><span class="p">,</span> <span class="ow">in</span> <span class="n">execute</span>
    <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">_execute_with_wrappers</span><span class="p">(</span>
           <span class="o">^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span>
  <span class="n">File</span> <span class="s">"/home/runner/.local/share/virtualenvs/captain-CVyYfSri/lib/python3.11/site-packages/django/db/backends/utils.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">80</span><span class="p">,</span> <span class="ow">in</span> <span class="n">_execute_with_wrappers</span>
    <span class="k">return</span> <span class="n">executor</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="n">params</span><span class="p">,</span> <span class="n">many</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span>
           <span class="o">^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span>
  <span class="n">File</span> <span class="s">"/home/runner/.local/share/virtualenvs/captain-CVyYfSri/lib/python3.11/site-packages/django/db/backends/utils.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">84</span><span class="p">,</span> <span class="ow">in</span> <span class="n">_execute</span>
    <span class="k">with</span> <span class="bp">self</span><span class="p">.</span><span class="n">db</span><span class="p">.</span><span class="n">wrap_database_errors</span><span class="p">:</span>
  <span class="n">File</span> <span class="s">"/home/runner/.local/share/virtualenvs/captain-CVyYfSri/lib/python3.11/site-packages/django/db/utils.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">91</span><span class="p">,</span> <span class="ow">in</span> <span class="n">__exit__</span>
    <span class="k">raise</span> <span class="n">dj_exc_value</span><span class="p">.</span><span class="n">with_traceback</span><span class="p">(</span><span class="n">traceback</span><span class="p">)</span> <span class="k">from</span> <span class="n">exc_value</span>
  <span class="n">File</span> <span class="s">"/home/runner/.local/share/virtualenvs/captain-CVyYfSri/lib/python3.11/site-packages/django/db/backends/utils.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">89</span><span class="p">,</span> <span class="ow">in</span> <span class="n">_execute</span>
    <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">cursor</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="n">params</span><span class="p">)</span>
           <span class="o">^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span>
<span class="n">django</span><span class="p">.</span><span class="n">db</span><span class="p">.</span><span class="n">utils</span><span class="p">.</span><span class="n">OperationalError</span><span class="p">:</span> <span class="n">could</span> <span class="ow">not</span> <span class="n">load</span> <span class="n">library</span> <span class="s">"/usr/local/lib/postgresql/llvmjit.so"</span><span class="p">:</span> <span class="n">Error</span> <span class="n">relocating</span> <span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">postgresql</span><span class="o">/</span><span class="n">llvmjit</span><span class="p">.</span><span class="n">so</span><span class="p">:</span> <span class="n">LLVMBuildGEP</span><span class="p">:</span> <span class="n">symbol</span> <span class="ow">not</span> <span class="n">found</span>
</code></pre></div></div>

<p>느닷없이 DB call에 실패했습니다. 에러 로그를 보니 딱 봐도 Django의 탓은 아닙니다.</p>

<p>제일 먼저 저희가 사용하고 있는 <code class="language-plaintext highlighter-rouge">postgres:12-alpine</code>의 커밋 로그를 찾으러 떠났습니다. (위에서 이미 Alpine Linux 3.18으로 뒤통수를 한 대 맞았기 때문에, 이번에도 아주 쎄했습니다)</p>

<p>아니나다를까, 오늘 오전 3시에 Alpine Linux 3.17 → 3.18을 사용하도록 변경된 <code class="language-plaintext highlighter-rouge">postgres:12.15-alpine</code>이 출시되었고, CI에서 <code class="language-plaintext highlighter-rouge">postgres:12-alpine</code>를 사용 중이었던 저희는 자동으로 <code class="language-plaintext highlighter-rouge">12.14</code>에서 <code class="language-plaintext highlighter-rouge">12.15</code>를 사용하게 된 것입니다.</p>

<p><img src="/assets/post-images/2023-05-12/alpine-linux-so-hot/2023-05-13-03-03-06.png" alt="" /></p>

<p>그런데 뭔가가 이상합니다. Alpine Linux 3.17에서 3.18을 사용하게 되었다고 DB Call이 작동 안 할 이유가 있나요?</p>

<p>그래서 <a href="https://github.com/StationSoen">@StationSoen</a>님과 위 에러 로그의 <code class="language-plaintext highlighter-rouge">llvmjit.so</code> 바이너리, 그리고 이 바이너리 파일에서 사용하는 <code class="language-plaintext highlighter-rouge">LLVMBuildGEP</code> 함수가 무엇인지를 제일 먼저 찾아 보았습니다.</p>

<p>힘들게 찾아보니</p>

<ol>
  <li>
    <p>LLVM이라는 컴파일러의 16버전이 3월에 출시되었고, 이 버전에서 <code class="language-plaintext highlighter-rouge">LLVMBuildGEP</code> 함수가 <code class="language-plaintext highlighter-rouge">LLVMBuildGEP**2**</code>로 rename된 것을 확인했습니다.</p>

    <p><a href="https://releases.llvm.org/16.0.0/docs/ReleaseNotes.html#changes-to-the-c-api">https://releases.llvm.org/16.0.0/docs/ReleaseNotes.html#changes-to-the-c-api</a></p>
  </li>
  <li>
    <p>Alpine Linux 3.18 버전부터 LLVM 15 → 16을 사용하도록 변경되었던 겁니다.</p>
  </li>
</ol>

<p>결국 PostgreSQL에서는 업데이트된 LLVM의 인터페이스에 맞게 코드를 수정하지 않고 12.15 버전을 릴리즈한 것입니다 😢</p>

<p>(함수를 deprecate시키지 않고 rename해버린 LLVM 측도 너무하긴 했지만요..)</p>

<p>회사 동료인 <a href="https://github.com/StationSoen">@StationSoen</a>님과 “우리도 PostgreSQL 컨트리뷰터?” 라고 이야기하며 Pull Request를 만들어보자고 했었지만 ㅎ 일단 공식 레포지토리에 이슈만 생성해놓았습니다.</p>

<p>역시나 뿔이 난 전 세계 개발자들이 <a href="https://github.com/StationSoen">@StationSoen</a>님께서 만드신 이슈에 달려들어 분노를 표출해주셨습니다 👿</p>

<p><a href="https://github.com/docker-library/postgres/issues/1076">https://github.com/docker-library/postgres/issues/1076</a></p>

<p>12시간 밖에 안 되었는데 그는 벌써 글로벌 스타가 되었습니다 🔥</p>

<p><img src="/assets/post-images/2023-05-12/alpine-linux-so-hot/2023-05-13-03-03-22.png" alt="" /></p>

<p>해결책은 간단했습니다. CI에서 사용되는 PostgreSQL 이미지를 마이너 버전까지 fix하는 것으로 문제는 바로 해결되었습니다.</p>

<p><img src="/assets/post-images/2023-05-12/alpine-linux-so-hot/2023-05-13-03-03-34.png" alt="" /></p>

<h2 id="til">TIL</h2>

<ol>
  <li>지금까지 Alpine Linux는 “빠르고”, “가벼운” OS라는 생각만 하며 사용을 했었는데, 빠르고 가벼운 데에는 다 이유가 있었다.</li>
  <li>Alpine Linux 버전마다 설치할 수 있는 Package의 version이 fix되어 있다.</li>
  <li>의존성은 minor version까지 fix하는 것을 잊지 말자. (RC든 정식 Release version이든 테스트 전까지 믿을 게 못 된다..)</li>
</ol>

<p>마지막으로, 굳이 Alpine Linux가 이러한 방식으로 패키지를 관리하는 이유를 알고 계신 분이 있으시다면 댓글로 알려주세요 🤔 </p>

<p>패키지 개발자가 OS 버전에 link된 패키지 버전을 업데이트하면 Alpine Linux의 버전을 fix해도 언제든지 호환성 문제가 발생할 수 있는 risky한 구조가 아닌가 싶습니다.</p>

<p><strong>저와 하루종일 함께 건설적인 논의와 다양한 시도를 펼쳐주신 <a href="https://github.com/StationSoen">@StationSoen</a>님께 영광을 돌립니다 🎉</strong></p>]]></content><author><name>dongzoolee</name></author><category term="일기" /><summary type="html"><![CDATA[나는 아직 Alpine Linux의 A도 모른다.]]></summary></entry><entry><title type="html">2022년을 정산하며 👋🏼</title><link href="https://dongzoolee.github.io/2022-12-31/review-2022" rel="alternate" type="text/html" title="2022년을 정산하며 👋🏼" /><published>2022-12-31T00:00:00+00:00</published><updated>2022-12-31T00:00:00+00:00</updated><id>https://dongzoolee.github.io/2022-12-31/review-2022</id><content type="html" xml:base="https://dongzoolee.github.io/2022-12-31/review-2022"><![CDATA[<p><br />
2021년 12월 31일 오후 10시에 오피스에서 찍은 사진이에요.</p>

<p>아무것도 모르던 인턴 시절, 아직은 회사라는 곳이 신기해서 연말에도 늦게까지 작업에 전전했던 기억이 나네요.</p>

<p><img src="/assets/post-images/2022-12-31/review-2022/mac.jpg" alt="맥북" width="60%" />
<br /></p>

<p>지난 1년 동안 저와 제 주변에 정말 많은 변화가 있었는데, 이 변화들을 그저 추억으로만 가져가기에는 너무 아쉬웠어요.</p>

<p>한 해를 간단히 돌아보고 새로운 다짐을 해보았어요.
<br /><br /></p>

<h2 id="2021년-12월-새로운-보금자리">2021년 12월, 새로운 보금자리</h2>
<p>저는 오전 10시부터 학교 동아리방에서 개발을 시작하고, 밤 11시에 개발을 마치고 집으로 돌아가서 새벽 5시까지 또 개발을 이어 하던 일상을 보내고 있었어요.</p>

<p>하루종일 죽어라 개발만 하던 일상이었지만, 저는 제가 세상에 내놓고 싶은 프로덕트를 만들어 가는 과정이 너무 행복하고 즐거웠어요.</p>

<p>(입사 직전까지 만들던 제품 ⬇️)
<img src="/assets/post-images/2022-12-31/review-2022/flit.jpg" alt="맥북" width="100%" />
<br /></p>

<p>열심히 개발을 하면 할 수록 저의 현업에 대한 갈망은 더 커져만 갔죠.</p>

<p>좋은 기회가 생겨 <a href="https://zuzu.network">ZUZU</a>라는 B2B SaaS 서비스를 만드는 <a href="https://kodebox.io">코드박스</a>의 소프트웨어 엔지니어 인턴으로 합류하게 되었어요.</p>

<p>저희 회사는 원래 3년 전 블록체인 회사였는데, 저는 여전히 블록체인을 연구하는 회사인 줄 알고 입사를 했어요 😅</p>

<p>입사 첫 날에 혼자 당황해있었는데, 이 이야기는 회사 동료들 사이에서 아직도 웃음거리예요 🙃🙃🙃</p>

<p>저의 회사에서의 첫 일주일은 정신없이 지나갔어요.
<br /><br /></p>

<h2 id="-1월">😵‍💫 1월</h2>
<p>인턴은 1월 한 달 내내 테스트 코드를 작성하며 회사의 코드 베이스에 익숙해져갔어요.</p>

<p><img src="/assets/post-images/2022-12-31/review-2022/test-codes.jpg" alt="맥북" width="100%" />
<br />
동시에 정말 크게 아팠어요.</p>

<p>편도선염이 찾아왔는데, 열이 40도까지 올라가서 3일 내내 병원에서 주사를 맞아도 잘 낫지 않았어요ㅜ</p>

<p><img src="/assets/post-images/2022-12-31/review-2022/emergency.jpg" alt="아파요" width="80%" /></p>

<p>지금 돌아보면 낯선 환경에서 인정 받고 싶어서 아둥바둥 애쓰는 과정에서 저도 모르게 몸이 부담을 많이 받았던 것 같아요.
<br /><br /></p>

<h2 id="-2월">🏢 2월</h2>
<p>저는 코드를 이해하는 속도가 빨라요.</p>

<p>덕분에 회사와 제 포지션에 빠르게 적응해나갔고, 초등학생 때부터 차곡차곡 쌓인 잡 개발 지식은 스타트업의 기술적인 문제들에 해결책을 제시하는 데에 많은 도움이 되었던 것 같아요.</p>

<h3 id="인턴-딱지를-떼었습니다">인턴 딱지를 떼었습니다.</h3>
<p>1학기 수강신청 마감일이었어요. 좋은 제안을 주셔서 휴학 결정을 내리고 정식 소프트웨어 엔지니어로 계약을 맺게 되었어요.</p>

<p><img src="/assets/post-images/2022-12-31/review-2022/first-business-card.jpeg" alt="첫명함" width="100%" />
<br /><br /></p>

<h2>🌝</h2>
<p>쉬어가는 시간입니다. 친구들과 배우 황정민을 따라해보았습니다 🌝</p>

<p><img src="/assets/post-images/2022-12-31/review-2022/hjm.jpg" alt="황정민" width="100%" />
<br /><br /></p>

<h2 id="-5월">💪 5월</h2>
<p><a href="https://zuzu.network/pricing">ZUZU 구독 요금제</a>를 탄생시켰어요.</p>

<p>입사하기 전에 제 개인 프로젝트에 결제 기능을 추가하려고 사업자등록증도 발급 받고 PG 연동도 하고 애를 많이 먹었었는데, PG사 승인이 너무 오래 걸려서 결국 결제 기능을 추가하지 못 했었어요.<br />
(입사하니까 기적처럼 승인이 떨어졌.. 😓)</p>

<p>이런 상황에서 구독 기능이 제 담당으로 넘어오게 되었으니, 저는 거의 물 만난 물고기처럼 신나 있었죠 🎣
<br /><br /></p>

<p>길거리를 걸어다니면서도 한 손에는 리갈 패드를, 다른 한 손에는 펜을 잡고 설계에 매진했던 기억이 나네요.</p>

<p>물론 거의 1년이 다 되어가는 현 시점에서 보면 부족한 점이 정말 많은 기능이지만, 구독 고객사가 하나씩 하나씩 늘어나는 것을 보면 제 자식(?)을 보는 것마냥 정말 뿌듯해요.</p>

<p><img src="/assets/post-images/2022-12-31/review-2022/landing-subscription.jpg" alt="랜딩구독" width="100%" /></p>

<p>입사 후 처음으로 해보는 큰 프로젝트였던 만큼 그 때의 감정과 배운 점을 글로 기록해두었어야 했는데.. 하는 아쉬움이 많이 들어요 😭
<br /><br /></p>

<h2 id="-6월">🙏 6월</h2>
<p>부모님 생신이셨어요.</p>

<p>휴학하고 회사에 다니겠다고 말씀드릴 때 저를 믿어주셔서 정말 감사해요.</p>

<p>항상 감사합니다.</p>

<p><img src="/assets/post-images/2022-12-31/review-2022/cake.jpg" alt="부모님생일케익" width="100%" />
<br /><br /></p>

<h2 id="-8월">📗 8월</h2>
<p>너무 늦게 깨달았죠?</p>

<p>어렸을 때부터 책보다는 컴퓨터 공부를 좋아했던 저는 교양과 지식이 많이 부족해요.</p>

<p>그래서 대학에 와서는 개발 도서보다는 저의 부족한 교양과 지식을 채워줄 수 있는 독서만 하기로 마음을 먹었었죠.</p>

<p>최신 개발 기술, 최신 아티클들은 인터넷에 홍수처럼 흘러 넘치니, 이 정보들만 열심히 챙기면 되겠구나~ 생각하고 있었죠.</p>

<p>8월에 제가 맡은 프로젝트를 하던 중, 테크 리드님께 저의 <code class="language-plaintext highlighter-rouge">인터페이스 설계</code>의 문제점에 대하여 피드백을 받았는데, 이때 저의 <code class="language-plaintext highlighter-rouge">개발에 대한 본질적인 공부</code>가 많이 부족하다는 것을 깨닫고 많은 충격을 받았어요.
<br /><br /></p>

<p>주니어 개발자에게 중요한 것은 본질에 대한 학습이라고 생각해요.</p>

<p>지금 올바르게 공부해두지 않으면 나중에 시니어 엔지니어가 되어서 잘못된 개념을 마치 정답인 것 마냥 가르치게 될 수도 있다고 생각해요.</p>

<p>프로덕션 코드를 보면서 공부한 내용들을 적용해볼 수 있다는 것은 정말 최고의 환경이자, 저에게 큰 축복이에요.</p>

<p>그래서 더욱이 바쁜 와중에도 독서는 놓치지 않으려고 최선을 다해서 노력하고 있어요.
<br /><br /></p>

<h2 id="-9월">🫂 9월</h2>
<p>부산에 업비트 개발자 컨퍼런스 <a href="https://udc.upbit.com/">UDC</a>가 열려서 엔지니어 다같이 부산에 다녀왔어요.</p>

<p>세미나는 정말 유익했습니다.<br />
<del>솔직히,, 세미나 열심히 안 들었습니다,, 먹고, 마시고, 호텔에서 요양하고 왔어요~!</del></p>

<div style="display:flex">
    <img width="38.9%" src="/assets/post-images/2022-12-31/review-2022/udc-dinner-ticket.jpeg" />
    <img width="59.9%" src="/assets/post-images/2022-12-31/review-2022/signiel-sushi.jpeg" />
</div>
<p><br /></p>

<p>상진님 해운대에 노을에 풍선에 맥주에,, 멋진 사진 감사합니다~!<br />
(소주 아닙니다.)</p>

<p><img src="/assets/post-images/2022-12-31/review-2022/dongzoo-maczoo.jpg" alt="맥주샷" width="80%" />
<br /></p>

<p>그리고 EO에 출연해서 우리 서비스도 홍보하고, 평소에 가지고 있던 생각들을 공유하는 시간을 가졌어요.</p>

<p>좋은 기회를 주신 회사에도 피플팀에도 너무 감사드려요 😙</p>

<iframe style="margin: 0 auto; display: block; width: 100%;" height="315" src="https://www.youtube.com/embed/udbWjB4F8Dg?start=3624" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>
<p><br /><br /></p>

<h2 id="-10월">🎨 10월</h2>
<p>제가 입사하기 이전부터 Storybook이 고장 나 있었고, 항상 소생시키고 싶다는 생각을 많이 했어요.</p>

<p>우리가 관리하고 있는 UI 컴포넌트를 한 눈에 보여주는 페이지가 있으면 프로덕트 디자이너든, 엔지니어든 온보딩 과정에서 큰 도움을 얻을 수 있겠다는 생각을 입사 초기부터 하고 있었거든요.</p>

<p>사실 UI 컴포넌트가 굉장히 많다 보니, 기존 엔지니어들도 “이런 컴포넌트가 있었나..”하고 헷갈려 하는 경우도 종종 있어요 ㅎ
<br /><br /></p>

<p>망가진 Storybook을 이때 고쳐두었고, 저희 프로덕트 디자이너 분들께서 열심히 UI 컴포넌트들을 추가해주고 계세요 💪🏻</p>

<p>자랑스러운 디자인 시스템을 이제 외부에 공개할 일만 남았네요 😁</p>

<p><img src="/assets/post-images/2022-12-31/review-2022/storybook.jpg" alt="Storybook" width="100%" />
<br /></p>

<h2 id="-12월">📆 12월</h2>
<p>좋아하는 일을 하느라 매일매일 신나있던 저에게 1년이라는 시간은 무척 짧았네요.<br />
어느새 인턴 2개월, 정규직 10개월을 지내고 1년 차 스타트업 엔지니어가 되었습니다.</p>

<p>그리고, 저도 많이 바뀌었습니다.</p>

<p>Y Combinator 창업자인 폴 그레이엄의 명언 <code class="language-plaintext highlighter-rouge">Launch before you're ready.</code>가 이제야 이해가 됩니다.</p>

<p>더 이상 ZUZU 구독 기능을 만들 때처럼 <code class="language-plaintext highlighter-rouge">구독 전용 기능이 아직 완전하지 않은데 출시해야 한다고?</code> 라고 생각하며 불평하던 제가 아닙니다.
<br /><br /></p>

<p>어쩌면 지금까지 작업한 개인 프로젝트 중에서 아직도 다른 유저들에게 활발히 사용되는 서비스가 몇 없는 이유도 <code class="language-plaintext highlighter-rouge">Early Launch</code>를 몰랐었기 때문이라고 생각합니다.</p>

<p>“아직 완벽하지 않으니, 아직 추가할 기능이 너무 많으니, 좀만 더 작업하고 출시하자.” 했던 마인드가 결국 몇만줄짜리 코드의 프로젝트를 <code class="language-plaintext highlighter-rouge">그저 소중한 경험</code>으로만 남게 만들었습니다.
<br /><br /></p>

<p>회사에서 몸으로 배운 스타트업 정신은 저의 소중한 재산이 되었습니다.</p>

<p>그리고, 앞으로 저만의 새로운 서비스를 만들어가는 과정에서 아주 큰 빛을 발해줄 것이라 믿습니다.
<!-- 아 그리고, 고객사 5,000개를 달성하는 순간을 함께 할 수 있어서 정말 정말 영광이에요. -->
<br /><br /></p>

<h2 id="앞으로-남은-여정-가운데에">앞으로 남은 여정 가운데에</h2>
<p>저는 2023년 11월 중 입대를 계획하고 있어요.</p>

<p>1년도 남지 않은 앞으로의 시간 동안에도 ZUZU라는 소프트웨어를 죽을 힘을 다해서 만들어 나가겠지만, 
올해는 조금 더 구체적인 목표를 가지고 남은 시간을 보내려고 해요.</p>

<h3 id="제-머릿-속에만-있던-기술-지식들">제 머릿 속에만 있던 기술 지식들</h3>
<p>이 블로그에 전부 글로 기록할게요. 한 달에 적어도 한 번은 기술 아티클을 발행할게요.</p>

<h3 id="일-때문에-바쁘다는-핑계로-방치했던-개인-프로젝트들">일 때문에 바쁘다는 핑계로 방치했던 개인 프로젝트들</h3>
<p>전부 저 없이도 작동하도록 개선해놓을게요.</p>

<h3 id="술-좀-줄일게요">술 좀 줄일게요.</h3>
<p>술 마실 시간에 글 쓸게요.
<br /><br /></p>

<h2 id="다짐하며">다짐하며</h2>

<p>저는 똑똑한 사람이 아니에요. 그래서 더욱 더 타인의 생각을 통해서 배우려고 노력해요.<br />
지난 1년 간 제가 경험을 통해 배운 점들을 공유해요.</p>

<h3 id="yes-man은-위험해요-️">Yes-Man은 위험해요 🙅🏼‍♀️</h3>
<p>어떤 상황에서도 자기 자신의 주관과 소신에 맞는 판단을 한 이후에 답변을 하는 것은 중요하다고 생각해요.</p>

<p>(적어도 현재는) 서로 다른 의견으로 치고 부딛히는 과정에서 각자의 논리가 어떤 점에서 틀렸는지를 알게 되고, 합리적인 결과가 도출된다고 믿기 때문이에요.</p>

<p><strong>소프트웨어를 만들어 가는 과정에 있어서도 마찬가지라고 생각해요.</strong></p>

<p>나의 코드에 대한 피드백을 받을 때 상대의 의견에 의심도 하지 않고 좋다고 답변을 일관하는 것은 좋지 않다고 생각해요.</p>

<p>천천히 생각도 해보고, 피드백도 한 번 냉정하게 바라본 이후에 판단할 필요가 있어요.</p>

<p><a href="https://youtu.be/udbWjB4F8Dg?t=5498">틀린 것을 틀렸다고 말할 수 있는</a> 우리 회사로 오시겠어요?</p>

<p><a href="https://career.zuzu.network">일단 서류 넣고 고민합시다.</a>
<br /><br /></p>

<h3 id="개발-실력만큼-중요한-커뮤니케이션-실력">개발 실력만큼 중요한 커뮤니케이션 실력</h3>
<p>저는 어쩌면 상황에 따라 후자가 훨씬 더 중요할 수도 있다고 생각해요.</p>

<p>제품은 나 혼자 만드는 것이 아니에요.</p>

<p>나 혼자 코드를 작성하더라도 해당 기능을 기획한 개발팀 전체, 다른 팀원들 모두가 제품에 대한 오너예요.</p>

<p>나의 현재 작업 상황을 지속적으로 공유할 필요가 있고, 애매한 지점을 혼자만의 판단으로 결정하지 않아야 해요.</p>

<p>작업이 예상 일정보다 훨씬 지연될 것 같으면 팀에 바로바로 공유해야 해요.</p>

<p>100점짜리 코드도 중요하지만, 좋은 제품을 일정 내에 만들어 내는 것도 중요하니까요.</p>

<p>이렇게 말은 했지만 저도 아직도 많이 부족한 부분이에요 😓
<br /><br /></p>

<h3 id="질문-좀-하자-질문">질문 좀 하자 질문!</h3>
<p>저는 매일 링크드인을 챙겨보는데, 최근에 신수정 KT Enterprise 부문장님께서 <a href="https://www.linkedin.com/posts/soojung-shin-30398b75_%EC%A7%88%EB%AC%B8%EB%A7%8C-%EC%9E%98%ED%95%B4%EB%8F%84-%EB%A6%AC%EB%8D%94%EC%97%AD%ED%95%A0%EB%A1%9C-%EC%B6%A9%EB%B6%84%ED%95%98%EB%8B%A4-1-%EB%B2%A4%EC%B2%98%EB%A5%BC-%EA%B3%B5%EB%8F%99%EC%B0%BD%EC%97%85%ED%95%98%EC%97%AC-%EC%88%98%EB%85%84%EA%B0%84-activity-7004657743512948736-GEm1?utm_source=share&amp;utm_medium=member_desktop">링크드인 글</a>을 하나 공유해주셨어요.</p>

<blockquote>
  <p>질문만 잘해도 리더역할로 충분하다—</p>

  <p>모르는것은 모른다고 해야지 오히려 어설픈 전문성으로 아는척하며 말도 안되는 가르침이나 지시를 하면 신뢰만 잃고 엉뚱한 방향으로 인도할 뿐이다.</p>
</blockquote>

<p>1년 간 일을 해보니, 질문을 잘하는 것은 실력이자 재능인 것 같아요.</p>

<p>질문을 하면 사람들이 나를 <code class="language-plaintext highlighter-rouge">이것도 모르는 사람!</code>이라고 판단할 것 같지만, 그렇지 않아요.
<br />
오히려 조직 문화에서는 더 나은, 오류 없는 프로덕트를 만들어 갈 수 있는 방법이라고 확신해요.</p>

<p>우리 회사에서는 지훈님, 진민님, 이한님이 정말 질문을 잘 하시는 것 같아요.</p>

<p>항상 부러워요.</p>

<p>질문을 잘 한다는 것은 내가 무엇을 알고 모르는지 정확히 알고 있다는 거니까요.
<br /><br /></p>

<h2 id="지금까지-나의-모습과-앞으로의-나의-모습">지금까지 나의 모습과 앞으로의 나의 모습</h2>

<p>참 이기적인 2022년이었어요.</p>

<p>항상 말로는 <code class="language-plaintext highlighter-rouge">개발팀이 함께 성장했으면 좋겠다.</code> 했지만, 결국 저는 제 개인적인 성장에만 목말라있었네요.</p>

<p><code class="language-plaintext highlighter-rouge">내 일하기에도 바빴다.</code> 이건 그저 합리화를 위한 저의 핑계일 뿐이었죠.</p>

<p><br />
2023년에는 개발팀 14명이 함께 성장할 수 있는 분위기를 제가 만들어갈게요.</p>

<ul>
  <li>제가 보고, 배우고, 경험한 것들을 혼자만 간직하지 않고 글로, 세미나로 공유할게요.</li>
  <li><code class="language-plaintext highlighter-rouge">이런 활동들을 좋겠다</code> 말만 하지 않고 제가 바로 행동으로 옮길게요.</li>
  <li><code class="language-plaintext highlighter-rouge">팀 블로그 관리해 줄 사람 어디 없나!</code> 불평 부리던 한 해였는데, 제가 주도적으로 관리할게요.</li>
  <li><a href="https://green-labs.github.io/pair-programming/">페어 프로그래밍</a>에 더 힘쓸게요.
<br /><br /></li>
</ul>

<p>올해도 저와 제 주변이 모두 행복한 한 해를 만들어 가겠습니다!</p>

<p>끝까지 읽어주셔서 정말 감사해요 😁</p>]]></content><author><name>dongzoolee</name></author><category term="일기" /><summary type="html"><![CDATA[새로운 환경에서의 1년을 돌아보며 새로운 한 해를 준비하는 나의 모습]]></summary></entry></feed>