<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>hanbin.dev</title>
    <link>https://hanbin8269.tistory.com/</link>
    <description>개와 개발을 좋아합니다</description>
    <language>ko</language>
    <pubDate>Tue, 21 Apr 2026 13:10:39 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>hanbindev</managingEditor>
    <image>
      <title>hanbin.dev</title>
      <url>https://tistory1.daumcdn.net/tistory/4480721/attach/140a5ed28caf40c9804f97893f68c3f9</url>
      <link>https://hanbin8269.tistory.com</link>
    </image>
    <item>
      <title>[PostgreSQL] Lock에 대해</title>
      <link>https://hanbin8269.tistory.com/65</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL에서는 동시성 제어를 위해 여러가지 모드의 lock을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 lock에도 여러가지 종류가 있고, 명시적으로 사용되는 경우/묵시적으로 사용되는 경우가 있는데, 간단하게 알아보자&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작하기 전 3줄요약&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Lock이 미치는 범위를 level로 나눈다&lt;/li&gt;
&lt;li&gt;Lock 모드별로 충돌하는 관계가 존재한다&lt;/li&gt;
&lt;li&gt;Lock은 트랜잭션 종료 시 혹은 롤백시에 풀린다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock이 미치는 범위를 Level로 나누는데, &lt;b&gt;Table-Level Lock&lt;/b&gt;, &lt;b&gt;Row-Level Lock&lt;/b&gt;, &lt;b&gt;Page-Level Lock&lt;/b&gt;, &lt;b&gt;Database-Level Lock&lt;/b&gt; 까지 다양하게 존재한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Table-Level Lock&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Table Level Lock은 테이블 수준에 락을 거는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 테이블 내에 100개의 로우가 있다고 하면 하나의 로우에 접근하는 동안 나머지 99개의 로우에 접근 할 수 없기 때문에, 다중 사용자 환경에서는 사용하지 않는 편이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(보통 테이블 전체 로우의 변경이 있는 DDL 구문과 함께 사용됨. (ex) &lt;code&gt;TRUNCATE&lt;/code&gt;, &lt;code&gt;ALTER&lt;/code&gt;))&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 Table Level Lock은 &lt;code&gt;LOCK&lt;/code&gt; 명령어를 이용해서 명시적으로 걸어줄 수도 있지만,&lt;br /&gt;우리가 특정 쿼리문을 사용할 때 마다 묵시적으로 걸리게 된다. 어느 상황에 어느 락이 걸리는지는 &lt;a href=&quot;https://www.postgresql.org/docs/current/explicit-locking.html#TABLE-LOCK-COMPATIBILITY&quot;&gt;해당 문서&lt;/a&gt;를 보면 알 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Row-Level Lock&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Row-Level Lock은 row 수준에 락을 거는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SELECT ~ FOR SHARE&lt;/code&gt;과 같은 DML 구문과 함께 가장 자주 사용되는 Lock이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이런 Lock의 종류가 하나밖에 없다면, 여러 종류의 트랜잭션에서 lock을 알맞게 사용하기 어려워지는데,&lt;br /&gt;이런 상황을 처리하기 위해 Lock에는 &lt;b&gt;Lock모드&lt;/b&gt;라는 것이 존재한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lock 모드?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lock 모드 별로 서로 충돌하는 Lock 모드가 있으며, 충돌한다면 해당 리소스(table, row)에 동시에 접근할 수 없게 된다다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ACCESS SHARE&lt;/code&gt;락은 &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; 락과 충돌한다,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ROW SHARE&lt;/code&gt;락은 &lt;code&gt;EXCLUSIVE&lt;/code&gt;, &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; 락과 충돌한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Table-Level Lock에는&lt;code&gt;ACCESS SHARE&lt;/code&gt;, &lt;code&gt;ROW SHARE&lt;/code&gt; 등,&lt;br /&gt;Row-Level Lock에는 &lt;code&gt;FOR UPDATE&lt;/code&gt;, &lt;code&gt;FOR KEY SHARE&lt;/code&gt; 등 여러가지 Lock 모드가 존재한다.&lt;br /&gt;이런 모드들의 이름은 &lt;b&gt;일반적으로 사용되는 경우&lt;/b&gt;를 나타내지만, 모드마다 기능적으로 &lt;b&gt;다른 점은 없다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 모드마다 차이점은 오직 &lt;b&gt;&quot;어떤 모드의 lock과 충돌하는가&quot;&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 lock 모드별 충돌되는 경우는 아래의 표를 보고 확인할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Table-Level Lock Mode&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/habiju/post/d4c523e8-942f-4787-8e9c-6669bb356152/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Row-Level Lock Mode&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/habiju/post/be4da1d9-6caf-475b-8fb0-2e3d91068370/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ref&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/mvcc.html&quot;&gt;Chapter 13. Concurrency Control&lt;/a&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;a href=&quot;https://www.postgresql.org/docs/current/explicit-locking.html&quot;&gt;13.3. Explicit Locking&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DB</category>
      <author>hanbindev</author>
      <guid isPermaLink="true">https://hanbin8269.tistory.com/65</guid>
      <comments>https://hanbin8269.tistory.com/65#entry65comment</comments>
      <pubDate>Sun, 2 Nov 2025 01:06:33 +0900</pubDate>
    </item>
    <item>
      <title>[Elasticsearch] 유저가 원하는 검색결과 보여주기 (analyzer)</title>
      <link>https://hanbin8269.tistory.com/64</link>
      <description>&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://academy.elice.io&quot;&gt;https://academy.elice.io&lt;/a&gt; 여기서 써볼수있다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;유저가 겪는 문제&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;163&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWfjty/btsPB04qB6Y/BkLP7c9GClWuSZlv19lkGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWfjty/btsPB04qB6Y/BkLP7c9GClWuSZlv19lkGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWfjty/btsPB04qB6Y/BkLP7c9GClWuSZlv19lkGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWfjty%2FbtsPB04qB6Y%2FBkLP7c9GClWuSZlv19lkGK%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;1200&quot; height=&quot;163&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;163&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 유저는 이름이 &quot;LangChain~&quot;으로 시작하는 상품을 검색하기 위해 보통 &quot;Lang&quot;까지만 검색해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우리의 통합검색 기능은 &quot;Lang&quot;을 입력했을때 &quot;LangChain~&quot; 상품을 보여주지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이런 현상이 일어나는 것일까? elasticsearch의 analyzer 동작 원리를 잠깐 살펴보고 가자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;elasticsearch analyzer 동작 원리&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;elasticsearch는 문자열이 들어오면 analyzer를 통해 &lt;b&gt;character filter -&amp;gt; tokenizer -&amp;gt; token filter 순서로 전처리&lt;/b&gt;를 한다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 &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;&quot;&amp;lt;strong&amp;gt;LangChain&amp;lt;/strong&amp;gt;으로 나만의 업무&quot;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;라는 문장이 있다고 가정해보자&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 character filter는 토큰화 이전에 문자열을 가공하는 역할을 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;character filter는 문자열을 어떻게 정리할 것인지에 따라 HTML 태그 제거, 패턴 기반 치환등 여러 종류가 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 HTML 태그 제거 character filter를 사용한다면 &quot;&lt;b&gt;LangChain으로 나만의 업무&lt;/b&gt;&quot; 와 같은 문자열로 변환된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음으로 tokenizer는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;문자열을 토큰화(분리)하는 역할을 한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;tokenizer는 문장을 어떻게 나눌 것인지에 따라 공백 기반 분할, 패턴 기반 분할 등 여러 종류가 있다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;공백 기반 Tokenizer를 사용한다면 &lt;b&gt;[&quot;LangChain으로&quot;, &quot;나만의&quot;, &quot;업무&quot;]&lt;/b&gt; 와 같이 문자열을 분리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 token filter는 토큰화된 문자들을 다시 가공하는 역할을 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;token filter 또한 문자열을 어떻게 정리할 것인지에 따라 대소문자 변환, 중복 제거 등 여러 종류가 있다&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대소문자 변환 token filter를 사용한다면 &lt;b&gt;[&quot;langchain으로&quot;, &quot;나만의&quot;, &quot;업무&quot;] &lt;/b&gt;와 같이 가공한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이처럼&lt;b&gt; analyzer는 원본 텍스트를 검색엔진(elasticsearch)이 이해할 수 있는 형태로 변환하는 역할&lt;/b&gt;을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 원인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 유저가 겪던 문제로 돌아가서 원인을 파악해보았는데, 아래와 같았다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;공백 기준으로 토큰화하는&lt;/span&gt; tokenizer를 사용하고 있다 (standard tokenizer)&lt;/li&gt;
&lt;li&gt;fuzziness 허용 편집거리는 1이다 (오타 1개까지 허용)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉 &quot;Lang_&quot; 까지 편집거리를 허용해주는데,&lt;br /&gt;실제 저장된 token인 &quot;LangChain으로&quot;는 편집거리가 훨씬 커서 매칭되지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항에 맞는 적절한 analyzer를 생성하여 해결하였다&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;analyzer&quot;: {
    &quot;camel_analyzer&quot;: {
      &quot;filter&quot;: [&quot;lowercase&quot;],
      &quot;char_filter&quot;: [&quot;camel_filter&quot;],
      &quot;tokenizer&quot;: &quot;nori_mixed&quot;
    }
  },
  &quot;char_filter&quot;: {
    &quot;camel_filter&quot;: {  // 1
      &quot;pattern&quot;: &quot;(?&amp;lt;=\\p{Lower})(?=\\p{Upper})&quot;,
      &quot;type&quot;: &quot;pattern_replace&quot;,
      &quot;replacement&quot;: &quot; &quot;
    }
  },
  &quot;tokenizer&quot;: {
    &quot;nori_mixed&quot;: {  // 2
      &quot;type&quot;: &quot;nori_tokenizer&quot;,
      &quot;decompound_mode&quot;: &quot;mixed&quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;CamelCase character filter 를 적용해 입력을 전처리 한다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[&quot;LangChain으로 나만의 업무&quot;] &amp;rarr; [&quot;lang chain으로 나만의 업무&quot;]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전처리 결과에 nori_tokenizer를 적용해 조사를 분리한다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[&quot;lang chain으로 나만의 업무&quot;] &amp;rarr; [&quot;lang&quot;, &quot;chain&quot;, &quot;으로&quot;, &quot;나&quot;, &quot;만&quot;, &quot;의&quot;, &quot;업무&quot;]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 analyzer를 index에 적용하고, reindex한 후에 &quot;lang&quot;으로 검색하면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[&quot;lang&quot;, &quot;chain&quot;, &quot;으로&quot;, &quot;나&quot;, &quot;만&quot;, &quot;의&quot;, &quot;업무&quot;]에 lang이 포함되어 있으므로&lt;/b&gt; 가중치가 높게 계산될 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;결과&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;423&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/paojF/btsPEtcRBTz/CaHgyjrb6gM038PmdyIkB0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/paojF/btsPEtcRBTz/CaHgyjrb6gM038PmdyIkB0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/paojF/btsPEtcRBTz/CaHgyjrb6gM038PmdyIkB0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpaojF%2FbtsPEtcRBTz%2FCaHgyjrb6gM038PmdyIkB0%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;1200&quot; height=&quot;423&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;423&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도한대로 &quot;Lang&quot;으로 검색하면 &quot;LangChain~&quot;이름을 가진 상품이 잘 나온다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Ref.&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.elastic.co/docs/manage-data/data-store/text-analysis/anatomy-of-an-analyzer&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.elastic.co/docs/manage-data/data-store/text-analysis/anatomy-of-an-analyzer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#fuzziness&quot;&gt;https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#fuzziness&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DB</category>
      <category>analyzer</category>
      <category>elasticsearch</category>
      <category>Tokenizer</category>
      <author>hanbindev</author>
      <guid isPermaLink="true">https://hanbin8269.tistory.com/64</guid>
      <comments>https://hanbin8269.tistory.com/64#entry64comment</comments>
      <pubDate>Thu, 31 Jul 2025 23:25:21 +0900</pubDate>
    </item>
    <item>
      <title>[PostgreSQL] 운영중인 DB 조용히 갈아끼우기</title>
      <link>https://hanbin8269.tistory.com/63</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLyDPF/btsPz1CVOED/TQxWKNkfVZmsaKrvruaKSK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLyDPF/btsPz1CVOED/TQxWKNkfVZmsaKrvruaKSK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLyDPF/btsPz1CVOED/TQxWKNkfVZmsaKrvruaKSK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLyDPF%2FbtsPz1CVOED%2FTQxWKNkfVZmsaKrvruaKSK%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;500&quot; height=&quot;368&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;368&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;회사의 모든 데이터베이스를 CNPG로 일원화하고자 하는 정책에 따라, &lt;b&gt;현재 운영 중인 Azure PostgreSQL 서비스를 자체 관리형 CloudNativePG(CNPG) 클러스터로 마이그레이션&lt;/b&gt;해야 하는 상황이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 운영 서비스와 연결되지 않은 데이터베이스였다면 단순히 dump &amp;amp; restore 방식으로 쉽게 해결할 수 있었겠지만&lt;b&gt;, 실제 서비스가 연결되어 있어 다른 접근 전략이 필요&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 내에서도 실시간으로 운영 중인 데이터베이스를 마이그레이션해본 경험이 없었기 때문에 팀원들과 충분히 논의하며 마이그레이션 계획을 수립했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검토 결과 두 가지 선택지가 있었다.&lt;/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 data-ke-size=&quot;size16&quot;&gt;우리 서비스의 &lt;b&gt;특성상 교육 시간대만 피하고 사전 공지를 한다면 짧은 다운타임이 발생해도 비즈니스에 큰 영향이 없었기&lt;/b&gt; 때문에, &lt;b&gt;데이터 정합성을 우선시하는 방향&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 실시간 복제 실행 &amp;rarr; 기존 앱 중단 &amp;rarr; 데이터 정합성 확인 &amp;rarr; 새로운 앱 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마이그레이션 방식 선택&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션 방식으로는 CDC와 Logical Replication 두 가지를 검토했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방식 모두 내부적으로 WAL(Write Ahead Logging)을 사용한다는 공통점이 있지만, CDC는 Kafka와 같은 추가 이벤트 스트림 컴포넌트가 필요하다는 차이점이 있다.&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;CDC&lt;/b&gt;: Source DB &amp;rarr; WAL &amp;rarr; Debezium &amp;rarr; Kafka &amp;rarr; Target(PostgreSQL, Elasticsearch 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Logical Replication&lt;/b&gt;: Source DB &amp;rarr; WAL &amp;rarr; Target DB&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 케이스에서는 Debezium, Kafka 등의 &lt;b&gt;추가 컴포넌트 설정이 불필요하고, 동일한 PostgreSQL 간의 단순 마이그레이션이므로 Logical Replication을 선택&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;Logical Replication 구현을 위해 별도의 서드파티 도구 없이 PostgreSQL 내장 기능만으로 동작하는 &lt;b&gt;pgcopydb&lt;/b&gt;(&lt;a href=&quot;https://github.com/dimitri/pgcopydb)%EB%A5%BC&quot;&gt;https://github.com/dimitri/pgcopydb)&lt;/a&gt;를 선택했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;docker compose로 진행했으며, source/target DB에 대해 postgresql.conf를 적절히 작성하였다&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;version: '3'
services:
    postgres_source:
        image: postgres:13
        environment:
            - POSTGRES_DB=source-db
            - POSTGRES_USER=hanbin
            - POSTGRES_PASSWORD=thisispassword
        volumes:
            - postgres_13-data:/var/lib/postgresql/data
            - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
        ports:
            - &quot;127.0.0.1:5432:5432&quot;
        shm_size: 1g
        command: [
            &quot;-c&quot;, &quot;config_file=/etc/postgresql/postgresql.conf&quot;,
         ]
    postgres_target:
        image: postgres:16
        environment:
            - POSTGRES_DB=target-db
            - POSTGRES_USER=hanbin
            - POSTGRES_PASSWORD=thisispassword
        volumes:
            - postgres_16-data:/var/lib/postgresql/data
            - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
        ports:
            - &quot;127.0.0.1:5433:5432&quot;
        shm_size: 1g
        command: [
            &quot;-c&quot;, &quot;config_file=/etc/postgresql/postgresql.conf&quot;,
        ]
volumes:
    postgres_source-data:
        driver: local
    postgres_target-data:
        driver: local&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 순서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 순서는 아래와 같다&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;1. Bastion VM pgcopydb 설치 및 연결 테스트
2. Source DB유저에게 모든 sequence 권한 부여
3. Target DB유저에게 임시로 superuser 권한 부여
4. DB 동기화
5. 복제 상황 모니터링
6. 복제 지연 최소화 대기 및 데이터 검증
7. api, worker, cron pod 제거
8. 복제 지연 없음 확인
9. pgcopydb 프로세스 종료
10. replication slot, origin 삭제
11. target DB유저 superuser 권한 제거
12. api, worker, cron 재배포&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서대로 차근차근 진행해보자&lt;/p&gt;
&lt;h5&gt;1. Bastion VM pgcopydb 설치 및 연결 테스트&lt;/h5&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sudo apt update
sudo apt install pgcopydb

mkdir -p pgcopydb-migration
cd pgcopydb-migration

export PGCOPYDB_SOURCE_PGURI=&quot;postgres://source-db:thisispassword@127.0.0.1:5432/source-db&quot;
export PGCOPYDB_TARGET_PGURI=&quot;postgres://target-db:thisispassword@127.0.0.1:5433/target-db&quot;

pgcopydb ping --source &quot;$PGCOPYDB_SOURCE_PGURI&quot; --target &quot;$PGCOPYDB_TARGET_PGURI&quot;
# 기대 출력
## INFO Successfully could connect to source database
## INFO Successfully could connect to target database&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2. Source DB유저에게 모든 sequence 권한 부여&lt;/h5&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- source-db 유저에게 모든 sequence 권한 부여
GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA public TO &quot;source-db&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3. Target DB유저에게 임시로 superuser 권한 부여&lt;/h5&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 임시로 superuser 권한 부여
ALTER USER &quot;target-db&quot; WITH SUPERUSER;&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;4. DB 동기화&lt;/h5&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;nohup pgcopydb clone --follow \
  --source &quot;$PGCOPYDB_SOURCE_PGURI&quot; \
  --target &quot;$PGCOPYDB_TARGET_PGURI&quot; \
  --no-owner \
  --no-acl \
  --table-jobs 1 \
  --index-jobs 1 \
  --verbose \
  --dir . \
  &amp;gt; ./pgcopydb-migration.log 2&amp;gt;&amp;amp;1 &amp;amp;

# 프로세스 ID 저장
echo $! &amp;gt; ./pgcopydb-migration.pid

# 실시간 로그 확인
tail -f ./pgcopydb-migration.log

# 진행 상황 확인
./replication_monitoring.sh&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;6. 복제 지연 최소화 대기 및 데이터 검증&lt;/h5&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;복제지연 확인
SELECT
    slot_name,
    active,
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) as lag_size,
    pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn) as lag_bytes
FROM pg_replication_slots
WHERE slot_name = 'pgcopydb';

-- Result --
 slot_name | active | lag_size | lag_bytes
-----------+--------+----------+-----------
 pgcopydb  | t      | 541 kB   |    553920
(1 row)
데이터 일관성 검증
pgcopydb compare schema --verbose
pgcopydb compare data --verbose&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 541KB 만큼의 복제 지연이 있다는 의미이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 데이터 정합성 확인
pgcopydb compare data

# Output
16:11:44.270 162208 INFO   Running pgcopydb version 0.17-1.pgdg22.04+1 from &quot;/usr/bin/pgcopydb&quot;
16:11:44.357 162208 INFO   Using work dir &quot;/tmp/pgcopydb&quot;
16:11:44.358 162208 INFO   SOURCE: Connecting to &quot;postgres://target-db@127.0.0.1:5432/target-db?keepalives=1&amp;amp;keepalives_idle=10&amp;amp;keepalives_interval=10&amp;amp;keepalives_count=60&quot;
16:11:44.361 162208 INFO   Re-using catalog caches
16:11:44.365 162208 INFO   Starting 4 table compare processes
                    Table Name | ! |                      Source Checksum |                      Target Checksum
-------------------------------+---+--------------------------------------+-------------------------------------
                 public.user |   |  9136aaf4-0f32-cd2b-850f-49270ff7c03 |  9136aaf4-0f32-cd2b-850f-49270ff7c03
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 두 데이터가 다른 경우 Checksum이 다르고, 두번째 컬럼에 !가 나타난다&lt;/p&gt;
&lt;h5&gt;7. api, worker, cron pod 제거&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용중인 CD tool에서 직접 내렸다&lt;/p&gt;
&lt;h5&gt;8. 복제 지연 없음 확인&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6번 과정에서 진행한 작업을 반복하여 확인한다&lt;/p&gt;
&lt;h5&gt;9. replication slot, origin 삭제&lt;/h5&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- SOURCE DB에서
SELECT pg_drop_replication_slot('pgcopydb');

-- TARGET DB에서
SELECT pg_replication_origin_drop('pgcopydb');&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;10. target DB유저 superuser 권한 제거&lt;/h5&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- superuser 권한 제거
ALTER USER &quot;target-db&quot; WITH NOSUPERUSER;&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;11. api, worker, cron 재배포&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용중인 CD tool에서 직접 다시 올렸다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 진행할때는 개발환경에서 권한과 postgresql 버전 등등을 정확히 동일하게 세팅 하지 않았어서 운영환경에 반영할때 잡음이 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀더 꼼꼼하게 개발환경을 세팅했으면 좋았을텐데 아쉬웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 내에서 경험이 없던 작업이라 문서화에 신경을 많이 썼는데, 문서화가 제일 피곤했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 정합성과 다운타임을 모두 잡는 해결책이 있을지 궁금해졌다.&lt;/p&gt;</description>
      <category>DB</category>
      <category>PostgreSQL</category>
      <author>hanbindev</author>
      <guid isPermaLink="true">https://hanbin8269.tistory.com/63</guid>
      <comments>https://hanbin8269.tistory.com/63#entry63comment</comments>
      <pubDate>Sun, 27 Jul 2025 20:26:42 +0900</pubDate>
    </item>
    <item>
      <title>[Prometheus] 애플리케이션 성능 모니터링 구축기 - Metric</title>
      <link>https://hanbin8269.tistory.com/62</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYrV2X/btsOZFrCGal/7VRnPw1yBcNoQkC3Ai2yEk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYrV2X/btsOZFrCGal/7VRnPw1yBcNoQkC3Ai2yEk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYrV2X/btsOZFrCGal/7VRnPw1yBcNoQkC3Ai2yEk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYrV2X%2FbtsOZFrCGal%2F7VRnPw1yBcNoQkC3Ai2yEk%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;400&quot; height=&quot;235&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;235&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;유저: 실습 기능이 너무 느린것 같아요.
개발팀: 저희는 재연이 안되어서... 혹시 어떻게 하셨나요?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 일이 벌써 몇 번째였다. &lt;b&gt;항상 유저가 먼저 문제를 발견하고, 리포트해주기를 기다리는 수동적인 상황&lt;/b&gt;에 놓여있었다. 서비스에 문제가 생겨도 개발자는 알 수 없는 구조였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 보니 자연스레 유저가 서비스를 신뢰하지 못하게 되고, &lt;b&gt;개발자들은 예측이 안되는 이슈에 예정된 일을 내팽개치고 달려나와&lt;/b&gt; 해결해야 하는 상황에 놓이게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메트릭부터, 빠르게 구축하자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;APM(Application Performance Monitoring) 시스템을 한번에 도입하려 했지만, 복잡한 인프라 세팅과 러닝 커브가 부담스러웠다. 우리에게 당장 필요한 &lt;b&gt;현재 서버 상태를 대략적으로라도 파악할 수 있는 메트릭&lt;/b&gt;부터 구축하기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메트릭만 제대로 도입해도 위에서 설명한 문제들을 해결 할 수 있다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;http_requests_total (요청 수)&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;라벨: path, method, status_code&lt;/li&gt;
&lt;li&gt;엔드포인트별 트래픽 패턴과 에러율 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;http_duration (요청 시간)&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;라벨: path, method&lt;/li&gt;
&lt;li&gt;각 요청의 처리 시간 분포 측정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 우리의 대부분 MSA는 FastAPI로 구현되어 있는데, http 요청 메트릭을 기록하는 Middleware를 구현하였다&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def setup_apm(app: FastAPI, service_name: str, version: str) -&amp;gt; MetricsManager:
    middleware = APMMiddleware(service_name, version)
    app.add_middleware(BaseHTTPMiddleware, dispatch=middleware)

    @app.get(
        &quot;/metrics&quot;,
        include_in_schema=False,
        response_class=PlainTextResponse,
        tags=[&quot;monitoring&quot;],
    )
    async def metrics_endpoint() -&amp;gt; PlainTextResponse:
        return PlainTextResponse(
            middleware.metrics.generate_metrics(),
            media_type=&quot;text/plain; version=0.0.4; charset=utf-8&quot;,
        )

    return middleware.metrics&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시각화 (대시보드 구축)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시각화는 grafana를 이용했다.&lt;br /&gt;grafana는 JSON 기반 대시보드 설정이 가능해서, claude와 대화 몇번 주고받으면서 대시보드를 완성시켰다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEBHvD/btsOYwJegav/Q95WhywgkjGV8fKAzKYBsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEBHvD/btsOYwJegav/Q95WhywgkjGV8fKAzKYBsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEBHvD/btsOYwJegav/Q95WhywgkjGV8fKAzKYBsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEBHvD%2FbtsOYwJegav%2FQ95WhywgkjGV8fKAzKYBsK%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;3000&quot; height=&quot;1555&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1555&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;&lt;b&gt;Request Per Seconds&lt;/b&gt;.&lt;br /&gt;서비스 전체의 트래픽 패턴을 모니터링한다. 어느 기능에 요청이 몰리는지 파악할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Request Duration (99p/90p/50p)&lt;/b&gt;&lt;br /&gt;응답 시간의 백분위수를 추적하는 가장 중요한 메트릭이다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;50p (median): 일반적인 사용자 경험&lt;/li&gt;
&lt;li&gt;90p: 대부분의 사용자 경험&lt;/li&gt;
&lt;li&gt;99p: 최악의 사용자 경험&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Slowest Endpoint&lt;/b&gt;&lt;br /&gt;가장 느린 엔드포인트가 무엇인지 알 수 있다. 성능 개선이 필요한 기능의 우선순위를 정하는 데 도움이 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&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;1458&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GJ15f/btsOYwvJJAn/wxmAYRjlPgRcC2j13uPdek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GJ15f/btsOYwvJJAn/wxmAYRjlPgRcC2j13uPdek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GJ15f/btsOYwvJJAn/wxmAYRjlPgRcC2j13uPdek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGJ15f%2FbtsOYwvJJAn%2FwxmAYRjlPgRcC2j13uPdek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;517&quot; height=&quot;197&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포하고 1시간만에 안되는 조회 Endpoint에서 99p와 90p, 50p의 격차가 넓은것을 확인했다.&lt;br /&gt;외부호출이 있는 엔드포인트 였는데 특정 상황에 지연이 발생하고 있어서, 내일 해결해봐야 겠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;느낀점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이걸 왜 이제 했을까..&lt;/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;대시보드를 세팅할때 처음에는 Request Duration의 평균값만 기록하였는데, 모든 기능의 지연시간이 고만고만해보여서 아무런 문제가 없는줄 알았다.&lt;/li&gt;
&lt;li&gt;의심스러워서 99p/90p/50p등의 백분위수를 도입하고 다니 평균에 가려져있던 문제들이 드러나게 되었다.&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;대부분의 개발자는 목표지향적인 성격을 가지고 있다고 생각한다&lt;/li&gt;
&lt;li&gt;그런 그들에게 OO기능 50% 성능 개선과 같은 정량적 목표를 부여한다면 달성했을때 더 큰 보람을 스스로 느낄 것이다&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>hanbindev</author>
      <guid isPermaLink="true">https://hanbin8269.tistory.com/62</guid>
      <comments>https://hanbin8269.tistory.com/62#entry62comment</comments>
      <pubDate>Mon, 30 Jun 2025 23:48:50 +0900</pubDate>
    </item>
    <item>
      <title>애플리케이션 성능 모니터링</title>
      <link>https://hanbin8269.tistory.com/61</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;227&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNBR5F/btsOOLtRVyx/c4Yw6q9K5rd8S6DdaTeYY0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNBR5F/btsOOLtRVyx/c4Yw6q9K5rd8S6DdaTeYY0/img.jpg&quot; data-alt=&quot;콩쥐야.. 조때써&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNBR5F/btsOOLtRVyx/c4Yw6q9K5rd8S6DdaTeYY0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNBR5F%2FbtsOOLtRVyx%2Fc4Yw6q9K5rd8S6DdaTeYY0%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;227&quot; height=&quot;222&quot; data-origin-width=&quot;227&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;콩쥐야.. 조때써&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;xxx-api CPU가 계속 스파이크를 찍네요, 이유가 무엇인가요?&amp;rdquo;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;위 질문에 답변을 못한다는 것을 장애가 터지고 깨달았다, 사실 인지는 했지만 외면해두고 있었다&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;최근 배포한 기능들을 뒤져보며 문제가 될 만한 부분을 찾아 급한 불은 우선 껐지만, 현 구조가 장애 대응 능력이 부족하다는 것이 느껴졌다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;또한 사용자와 서비스 규모가 늘어나면서, 장애 상황에서의 빠른 대응이 팀의 우선순위가 되었다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;무엇이 원인인지&amp;ldquo; 더욱 빠르게 파악할수 있는 해결책이 필요하다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; 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; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-node-block=&quot;true&quot;&gt;
&lt;li data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-node-block=&quot;true&quot;&gt;서버 예외 sentry 로그&lt;/li&gt;
&lt;li data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-node-block=&quot;true&quot;&gt;k8s pod 시스템 성능 지표 (CPU, MEM, Net I/O, &amp;hellip;)&lt;/li&gt;
&lt;li data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-node-block=&quot;true&quot;&gt;db 시스템 성능 지표 (CPU, MEM, Net I/O, &amp;hellip;)&lt;/li&gt;
&lt;li data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-node-block=&quot;true&quot;&gt;nginx http 로그&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;시스템 레벨의 지표는 수집하고 있지만, &lt;b&gt;애플리케이션 레벨의 관찰 가능성이 부족&lt;/b&gt;하여 &amp;ldquo;어떤 코드에서 발생했는지&quot; 파악할 수 없는 상황이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;따라서 다음과 같은 요소들이 추가로 필요하다:&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Metric&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;어플리케이션의 상태 트렌드를 파악하기 위해 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;API 엔드포인트별 응답시간, 처리량(TPS), 에러율&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;비즈니스 로직별 실행시간 (예: 결제 처리, 데이터 조회 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Logging&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;metric에서 트렌드를 파악하고, 구체적으로 어떤 문제인지 파악하기 위해 필요하다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;nginx http 로그&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;애플리케이션 에러 로그 (stack trace, 예외 상황)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Trace&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;하나의 요청이 여러 서비스를 거쳐가는 과정을 추적하여, 어느 구간에서 지연이나 문제가 발생했는지 파악하기 위해 필요하다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;각 API 호출의 상세 실행 과정 로깅&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;마이크로서비스 간 호출 관계와 소요시간&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Alert&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;문제 발생 시 즉시 인지하고 빠른 대응을 위해 필요하다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;임계값 기반 실시간 알림 (응답시간 &amp;gt;3초, 에러율 &amp;gt;5% 등)&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;일정 시간 내 미응답 시 상위 담당자에게 자동 확대 알림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;현재 logging은 loki 기반으로 nginx http 로그를 수집하고 있어 어느정도 갖춰져 있는 상태이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;우선 어플리케이션 레벨 &lt;b&gt;metric을 수집하는 것에 초점&lt;/b&gt;을 맞춰보자&lt;/p&gt;</description>
      <category>APM</category>
      <category>monitoring</category>
      <author>hanbindev</author>
      <guid isPermaLink="true">https://hanbin8269.tistory.com/61</guid>
      <comments>https://hanbin8269.tistory.com/61#entry61comment</comments>
      <pubDate>Wed, 25 Jun 2025 02:03:09 +0900</pubDate>
    </item>
    <item>
      <title>[ETC] 대선토론 보다가 떠오른 아이디어로 1시간 만에 서비스 런칭하기</title>
      <link>https://hanbin8269.tistory.com/60</link>
      <description>&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;자세히 알지 못하니 내 가치관을 투영하며 보질 못하니까 재미가 반감되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;a href=&quot;https://policy.nec.go.kr/&quot;&gt;https://policy.nec.go.kr&lt;/a&gt; 해당 링크에서 공약을 찾아보며 대선토론을 시청했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 문득, &quot;공약만 보고 비교하여 나에게 맞는 후보자를 찾는 사이트가 있으면 어떨까?&quot; 생각이 들었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Project Rule 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;claude 프로젝트를 만들고 간단한 Rule를 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시나 큰일나면 안되니 선거법을 찾아보며 만들었다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;1. 정치적 중립성 유지
   - 특정 후보나 정당을 편향되게 지지하거나 비판하지 않기
   - 개인적 정치 성향이나 선호도 표현 금지

2. 사실 정확성 최우선
   - Github Repository에 명시된 중앙선관위 등록 공약만 사용

3. 선거법 준수
   - 선거 관련 법령 위반 요소 철저히 배제
   - 개인정보 수집이나 여론조작 요소 금지

4. 데이터 처리
   - 사용자 개인정보 수집 금지
   - 클라이언트 사이드에서만 동작
   - 결과 추적이나 분석 데이터 저장 금지

5. 면책 조항
   - 교육 목적임을 명시
   - 실제 투표 시 종합적 고려 권고&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 데이터 사전 준비&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Brave Search 등의 MCP가 존재하였지만, 정보의 형평성을 맞추기 위해 크롤링은 하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선관위 사이트(&lt;a href=&quot;https://policy.nec.go.kr&quot;&gt;https://policy.nec.go.kr&lt;/a&gt;)에서 10대 공약 데이터를 추출하여 레포지토리에 미리 올려두었다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hanbin8269/political-mbti/tree/main/src/data/politics&quot;&gt;https://github.com/hanbin8269/political-mbti/tree/main/src/data/politics&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 개발&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 올려둔 데이터를 기반으로 claude에게 개발해달라고 한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3718&quot; data-origin-height=&quot;1854&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqHYKM/btsOlVbxmqU/bmWrpBXdvpvko2b0zptSCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqHYKM/btsOlVbxmqU/bmWrpBXdvpvko2b0zptSCK/img.png&quot; data-alt=&quot;실행까지 해준다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqHYKM/btsOlVbxmqU/bmWrpBXdvpvko2b0zptSCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqHYKM%2FbtsOlVbxmqU%2FbmWrpBXdvpvko2b0zptSCK%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;3718&quot; height=&quot;1854&quot; data-origin-width=&quot;3718&quot; data-origin-height=&quot;1854&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실행까지 해준다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 프로토타입을 확인하고, 질문, 후보자 정보, 매칭 로직을 검토하여 점진적으로 개선해갔다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vercel로 배포했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주변에서 가장 많이 들어본게 vercel이고, 프로젝트가 next.js 기반이고, 후에 SEO 최적화도 쉽다고 들어서 선택했다.&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시간 만에 서비스를 만들고 배포까지 해보았다.&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;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;링크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;github: &lt;a href=&quot;https://github.com/hanbin8269/political-mbti&quot;&gt;https://github.com/hanbin8269/political-mbti&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;서비스 링크 : &lt;a href=&quot;https://political-mbti.vercel.app&quot;&gt;https://political-mbti.vercel.app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발후기</category>
      <author>hanbindev</author>
      <guid isPermaLink="true">https://hanbin8269.tistory.com/60</guid>
      <comments>https://hanbin8269.tistory.com/60#entry60comment</comments>
      <pubDate>Sat, 31 May 2025 23:12:59 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Springfox Whitelabel Error Page in SpringBoot 3.0.0</title>
      <link>https://hanbin8269.tistory.com/58</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot 3.0.0 환경에서 Swagger를 적용하고자 Spring fox 라이브러리를 사용했는데 아래와 같이 Whitelabel Error Page 가 나오며 매핑이 제대로 되지 않는 문제가 발생했다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dCJkdB/btrWRzzzPR0/eYlZBpmKDiFzcFlsJpUzP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dCJkdB/btrWRzzzPR0/eYlZBpmKDiFzcFlsJpUzP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dCJkdB/btrWRzzzPR0/eYlZBpmKDiFzcFlsJpUzP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdCJkdB%2FbtrWRzzzPR0%2FeYlZBpmKDiFzcFlsJpUzP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1204&quot; height=&quot;412&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;412&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;build.gradle.kts는 아래와 같이 세팅해두었다.&lt;/p&gt;
&lt;pre id=&quot;code_1674466772567&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
implementation(&quot;io.springfox:springfox-boot-starter:3.0.0&quot;)
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해&lt;b&gt; &quot;spring fox not working in spring boot 3.0.0&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Spring boot를 2.6.2 로 다운그레이드 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. springdoc-openapi 라이브러리를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나같은 경우 프로젝트 초기 세팅 단계라 1번 방법으로 해결하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직은 Spring boot3 레퍼런스가 부족하다는 것을 느껴 결정하게 되었다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>springboot 3.0.0</category>
      <category>springfox</category>
      <author>hanbindev</author>
      <guid isPermaLink="true">https://hanbin8269.tistory.com/58</guid>
      <comments>https://hanbin8269.tistory.com/58#entry58comment</comments>
      <pubDate>Mon, 23 Jan 2023 18:42:01 +0900</pubDate>
    </item>
    <item>
      <title>사수 퇴사 축하 Git Commit 피규어 만들기</title>
      <link>https://hanbin8269.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1년동안 잘 챙겨주신 사수분이 퇴사하신다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 인생 첫 사수고 정말 좋은 분이라 뭔가 재미있는 선물을 해드리고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러던 중 스쳐지나가면서 봤던 &lt;a href=&quot;https://skyline.github.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github skyline&lt;/a&gt; 이라는 사이트가 생각났고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1850&quot; data-origin-height=&quot;880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sSwL3/btrJ1YRPZMf/wRx87pjUAUKrvz8VRPRgxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sSwL3/btrJ1YRPZMf/wRx87pjUAUKrvz8VRPRgxk/img.png&quot; data-alt=&quot;부끄러운 나의 커밋 수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sSwL3/btrJ1YRPZMf/wRx87pjUAUKrvz8VRPRgxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsSwL3%2FbtrJ1YRPZMf%2FwRx87pjUAUKrvz8VRPRgxk%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;694&quot; height=&quot;330&quot; data-origin-width=&quot;1850&quot; data-origin-height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;부끄러운 나의 커밋 수&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이거를 3D 프린터로 뽑아서 드리면 그간의 노력도 볼 수 있고 되게 재미있을 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 실행에 옮겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보니 우리 회사는 gitlab을 써서 위 사이트를 이용할 수가 없었다. 그래서 아래 라이브러리를 찾아왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/felixgomez/gitlab-skyline&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/felixgomez/gitlab-skyline&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1660822109170&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - felixgomez/gitlab-skyline: Generate a 3D Skyline in STL format and a OpenSCAD file from Gitlab contributions&quot; data-og-description=&quot;Generate a 3D Skyline in STL format and a OpenSCAD file from Gitlab contributions - GitHub - felixgomez/gitlab-skyline: Generate a 3D Skyline in STL format and a OpenSCAD file from Gitlab contribut...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/felixgomez/gitlab-skyline&quot; data-og-url=&quot;https://github.com/felixgomez/gitlab-skyline&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://github.com/felixgomez/gitlab-skyline&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/felixgomez/gitlab-skyline&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;GitHub - felixgomez/gitlab-skyline: Generate a 3D Skyline in STL format and a OpenSCAD file from Gitlab contributions&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Generate a 3D Skyline in STL format and a OpenSCAD file from Gitlab contributions - GitHub - felixgomez/gitlab-skyline: Generate a 3D Skyline in STL format and a OpenSCAD file from Gitlab contribut...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 라이브러리는 1년 단위로만 뽑을 수 있었는데, 사수의 2년이 모두 담길 수 있도록 코드를 조금 수정했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;601&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vuQpJ/btrJ2axLv2w/8Mk6KqOQWPMLouqsEEH3QK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vuQpJ/btrJ2axLv2w/8Mk6KqOQWPMLouqsEEH3QK/img.png&quot; data-alt=&quot;2년은 역시 길다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vuQpJ/btrJ2axLv2w/8Mk6KqOQWPMLouqsEEH3QK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvuQpJ%2FbtrJ2axLv2w%2F8Mk6KqOQWPMLouqsEEH3QK%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;482&quot; height=&quot;318&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;601&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2년은 역시 길다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 로고도 하나 박아주자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1398&quot; data-origin-height=&quot;1112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kPDae/btrKGOVT2aG/kkisnKifvR79aL5JgZzk5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kPDae/btrKGOVT2aG/kkisnKifvR79aL5JgZzk5k/img.png&quot; data-alt=&quot;이쁘다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kPDae/btrKGOVT2aG/kkisnKifvR79aL5JgZzk5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkPDae%2FbtrKGOVT2aG%2FkkisnKifvR79aL5JgZzk5k%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;420&quot; height=&quot;334&quot; data-origin-width=&quot;1398&quot; data-origin-height=&quot;1112&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이쁘다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리하여 출력할 stl 파일은 준비되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 이를 프린팅 해줄 대행 업체를 찾아봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1661645198977&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;3D프린터 장인 갓재석- 가장 저렴한 3D프린팅 / 실시간 비용 확인 - 3D프린터장인 갓재석&quot; data-og-description=&quot;2021년 3D프린터 장인의 가장 저렴한 3D프린터 출력 서비스, 석고몰드, 비누몰드, 3D모델이 없어도 스케치,그림만으로 제작해드립니다. 다양한 재료와 후가공 서비스로 여러분의 꿈을 3d프린터 장&quot; data-og-host=&quot;13.209.51.203&quot; data-og-source-url=&quot;https://www.xn--739a021b5xe.com/&quot; data-og-url=&quot;https://13.209.51.203/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/4rxs6/hyPAocRL9Q/1q2JoNjkdKs2E8nKbQuFjk/img.png?width=100&amp;amp;height=100&amp;amp;face=0_0_100_100,https://scrap.kakaocdn.net/dn/dl01jL/hyPBLqJNBt/bHh6x9KcXjm7yAWzGsxr9K/img.png?width=120&amp;amp;height=120&amp;amp;face=0_0_120_120&quot;&gt;&lt;a href=&quot;https://www.xn--739a021b5xe.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.xn--739a021b5xe.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/4rxs6/hyPAocRL9Q/1q2JoNjkdKs2E8nKbQuFjk/img.png?width=100&amp;amp;height=100&amp;amp;face=0_0_100_100,https://scrap.kakaocdn.net/dn/dl01jL/hyPBLqJNBt/bHh6x9KcXjm7yAWzGsxr9K/img.png?width=120&amp;amp;height=120&amp;amp;face=0_0_120_120');&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;3D프린터 장인 갓재석- 가장 저렴한 3D프린팅 / 실시간 비용 확인 - 3D프린터장인 갓재석&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2021년 3D프린터 장인의 가장 저렴한 3D프린터 출력 서비스, 석고몰드, 비누몰드, 3D모델이 없어도 스케치,그림만으로 제작해드립니다. 다양한 재료와 후가공 서비스로 여러분의 꿈을 3d프린터 장&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;13.209.51.203&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;stl 파일을 업로드하여 소재 / 크기 / 색상 선택 후 출력 신청을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3222&quot; data-origin-height=&quot;1822&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqgU1k/btrKH0g3qDG/7trQP7R0Bpz6T1m9y1dy91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqgU1k/btrKH0g3qDG/7trQP7R0Bpz6T1m9y1dy91/img.png&quot; data-alt=&quot;stl 뷰어도 제공한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqgU1k/btrKH0g3qDG/7trQP7R0Bpz6T1m9y1dy91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqgU1k%2FbtrKH0g3qDG%2F7trQP7R0Bpz6T1m9y1dy91%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;607&quot; height=&quot;343&quot; data-origin-width=&quot;3222&quot; data-origin-height=&quot;1822&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;stl 뷰어도 제공한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;크기 &lt;/span&gt;18 &amp;times; 2 &amp;times; 2 cm, 플라스틱(PLA) 소재는 &lt;b&gt;15000원&lt;/b&gt; 정도로 출력이 가능했다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 성공적으로 주문을 했고, 배송이 올때까지 기다리기로 하였다. (&lt;b&gt;1~2주&lt;/b&gt;&amp;nbsp;소요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vWNLH/btrKMFbTuJU/cPJ3KH68IWH4BiCKAtXRdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vWNLH/btrKMFbTuJU/cPJ3KH68IWH4BiCKAtXRdk/img.png&quot; data-alt=&quot;!!!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vWNLH/btrKMFbTuJU/cPJ3KH68IWH4BiCKAtXRdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvWNLH%2FbtrKMFbTuJU%2FcPJ3KH68IWH4BiCKAtXRdk%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;410&quot; height=&quot;112&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;!!!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;느긋하게 배송을 기다리려 했는데, &lt;b&gt;사수의 퇴사일이 앞당겨졌다는 소식&lt;/b&gt;을 듣게되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손가락 빨고있다간 퇴사일에 못 맞출 것 같아서, 퇴근하고 직접 가서 받아와야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;판매자분께 사정 말씀드리니 감사하게도 밤 늦게까지 기다려주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 플라스틱 재질로 주문했는데 &lt;b&gt;피규어에 적합한 더 비싼 레진 재질로 출력&lt;/b&gt;해주시고,&lt;b&gt; 마감처리까지 무료&lt;/b&gt;로&amp;nbsp;해주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 주문이라 번거로우실텐데도 신경써주셔서 너무 감사했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 힘들게 만든 결과물을 만날수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VdBvU/btrKIyLny6U/1Uw002rLmxqioZAd0pfCbK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VdBvU/btrKIyLny6U/1Uw002rLmxqioZAd0pfCbK/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;3024&quot; data-filename=&quot;KakaoTalk_Photo_2022-08-28-09-39-31 001.jpeg&quot; width=&quot;298&quot; height=&quot;298&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VdBvU/btrKIyLny6U/1Uw002rLmxqioZAd0pfCbK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVdBvU%2FbtrKIyLny6U%2F1Uw002rLmxqioZAd0pfCbK%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;3024&quot; height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Nr0fG/btrKGOhjUna/dQOxash2w5JsbLgQik5DeK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Nr0fG/btrKGOhjUna/dQOxash2w5JsbLgQik5DeK/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;3024&quot; data-filename=&quot;KakaoTalk_Photo_2022-08-28-09-39-32 002.jpeg&quot; width=&quot;306&quot; height=&quot;263&quot; style=&quot;width: 263px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Nr0fG/btrKGOhjUna/dQOxash2w5JsbLgQik5DeK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNr0fG%2FbtrKGOhjUna%2FdQOxash2w5JsbLgQik5DeK%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;3024&quot; height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;두둥&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2년간 열심히 일하신 기록을 한눈에 볼수있다. 가끔씩 주말에 잔디가 깔린걸 보면 눈물이난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 부러질까봐 걱정했는데. 레진 소재가 탄성과 무게감이 있어 튼튼했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보라색으로 페인팅까지 하면 좋았을텐데, 시간이 부족했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;1002&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RF2bf/btrKIrMdfig/WSDKFMD0sDVUZRRFw7K7C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RF2bf/btrKIrMdfig/WSDKFMD0sDVUZRRFw7K7C1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RF2bf/btrKIrMdfig/WSDKFMD0sDVUZRRFw7K7C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRF2bf%2FbtrKIrMdfig%2FWSDKFMD0sDVUZRRFw7K7C1%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;509&quot; height=&quot;312&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;1002&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;다같이 퇴사를 축하한 후, 사수의 손으로 무사히 전달되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번거로운 짐이 되지 않을까 걱정했는데, 엄청 좋아하셔서 뿌듯했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^_ㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>github skyline</category>
      <category>skyline</category>
      <category>갓재석</category>
      <author>hanbindev</author>
      <guid isPermaLink="true">https://hanbin8269.tistory.com/56</guid>
      <comments>https://hanbin8269.tistory.com/56#entry56comment</comments>
      <pubDate>Sun, 28 Aug 2022 09:58:23 +0900</pubDate>
    </item>
    <item>
      <title>[Python] NotImplementedError: Don't know how to literal-quote value ~</title>
      <link>https://hanbin8269.tistory.com/54</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Don't know how to literal-quote value ~ : sqlalchemy에게 이 문자를 어떻게 컴파일 해야 하는지 알려줘&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하자면 Sqlalchemy 1.3.6 이상에서는 일어나지 않는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;gt;&amp;nbsp; &lt;a href=&quot;https://docs.sqlalchemy.org/en/14/changelog/changelog_13.html#change-d388c44ba6f010e81ba29941ce191863&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;  sqlalchemy changelog 링크&amp;nbsp;&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;문제 원인&lt;/h3&gt;
&lt;pre id=&quot;code_1658850723446&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ============== example ==============

query = db.session.query(m.Account).filter(m.Account.created_datetime == datetime.now())

query.statement.compile(
	dialect=postgresql.dialect(),
	compile_kwargs={'literal_binds': True},
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;statement를 컴파일 할 때 생기는 문제인데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sqlalchemy 1.3.6 버전 이하의 &lt;b&gt;postgresql에서 DateTime을 처리하는 literal processor가 구현되어 있지 않아 생긴 문제로&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;sqlalchemy 창시자 &lt;b&gt;mike bayer&lt;/b&gt; 가 손수 해결해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 데이터베이스 엔진에 대해 &lt;b&gt;Datetime객체를 ISO 8601 포멧으로 치환&lt;/b&gt;해주도록 변경한 듯 하다. &amp;gt; &lt;a href=&quot;https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/3744/6/lib/sqlalchemy/sql/sqltypes.py#703&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;  commit log&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1658853130255&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;https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/3744/6/lib/sqlalchemy/sql/sqltypes.py#703&quot; data-og-description=&quot;&quot; data-og-host=&quot;gerrit.sqlalchemy.org&quot; data-og-source-url=&quot;https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/3744/6/lib/sqlalchemy/sql/sqltypes.py#703&quot; data-og-url=&quot;https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/3744/6/lib/sqlalchemy/sql/sqltypes.py#703&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/3744/6/lib/sqlalchemy/sql/sqltypes.py#703&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/3744/6/lib/sqlalchemy/sql/sqltypes.py#703&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;https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/3744/6/lib/sqlalchemy/sql/sqltypes.py#703&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gerrit.sqlalchemy.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;</description>
      <category>Python</category>
      <category>sqlalchemy</category>
      <author>hanbindev</author>
      <guid isPermaLink="true">https://hanbin8269.tistory.com/54</guid>
      <comments>https://hanbin8269.tistory.com/54#entry54comment</comments>
      <pubDate>Wed, 27 Jul 2022 01:34:52 +0900</pubDate>
    </item>
    <item>
      <title>[Python] metaclass 란</title>
      <link>https://hanbin8269.tistory.com/53</link>
      <description>&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;br /&gt;또한, 파이썬의 객체는 &lt;b&gt;아이덴티티, 값, 타입&lt;/b&gt;을 가지고 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;아이덴티티 (&lt;code&gt;id&lt;/code&gt;)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;id()&lt;/code&gt;&lt;/b&gt; 함수를 통해 얻을 수 있으며 객체의 수명동안 유일하고 불변함이 보장되는 정수다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;값 (&lt;code&gt;value&lt;/code&gt;)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 타입에 따라 불변할 수 있고 가변할 수도 있다. &lt;code&gt;ex)tuple : 불변, list : 가변&lt;/code&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;타입 (&lt;code&gt;type&lt;/code&gt;)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 지원하는 연산들과 그 타입의 객체가 가질 수 있는 값(&lt;code&gt;ex) int : 1, list : [1,2]&lt;/code&gt;)들을 통해 객체의 특성을 정의한다. 객체의 타입은 &lt;code&gt;type()&lt;/code&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;code&gt;type()&lt;/code&gt; 빌트인 함수를 사용하면 객체의 타입을 알 수 있다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;class Test:
    pass
t = Test()
type(t)
# &amp;lt;class '__main__.Test'&amp;gt;

def hello():
    pass
type(hello)
# &amp;lt;class 'function'&amp;gt;

type(1)
# &amp;lt;class 'int'&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제를 보면 &lt;code&gt;Test&lt;/code&gt; 클래스의 인스턴스인 &lt;code&gt;t&lt;/code&gt;는 &lt;code&gt;Test&lt;/code&gt;가 타입이고, &lt;code&gt;hello&lt;/code&gt; 함수는 &lt;code&gt;function&lt;/code&gt;이 타입이며, 정수 1은 &lt;code&gt;int&lt;/code&gt;가 타입이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, &lt;code&gt;Test&lt;/code&gt; 클래스의 타입은 존재할까?&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class Test:
    pass
type(Test)
# &amp;lt;class 'type'&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;놀랍게도, &lt;code&gt;Test&lt;/code&gt; 클래스 객체의 타입이 존재한다.&lt;br /&gt;여기서 출력된 &lt;code&gt;type&lt;/code&gt;을 &lt;code&gt;Test&lt;/code&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;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class TestMeta(type):
    def __prepare__(mcs, *args, **kwarg): # 메타 클래스가 결정되었을 때 (mro가 구성된 후) 클래스 정의를 위해 호출된다.
        # mcs = metaclass
        print(&quot;__prepare__()&quot;)
        return super.__prepare__(mcs, *args, **kwarg)

    def __new__(mcs, *args, **kwargs):  # 클래스를 생성 할 때 호출됨
        # mcs = metaclass
        print(&quot;__new__()&quot;)
        return super().__new__(mcs, *args, **kwargs)

    def __init__(cls, *args, **kwargs):  # 클래스가 생성 된 후 호출됨
        print(&quot;__init__()&quot;)
        super().__init__(*args, **kwargs)

    def __call__(cls, *args, **kwargs):  # 클래스의 인스턴스를 생성할 때 호출됨
        print(&quot;__call__()&quot;)
        return super().__call__(*args, **kwargs)


class Test(metaclass=TestMeta):
    pass

# __prepare__()
# __new__()
# __init__()

t = Test()
# __call__()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 보면 &lt;code&gt;__prepare__&lt;/code&gt;, &lt;code&gt;__new__&lt;/code&gt;, &lt;code&gt;__init__&lt;/code&gt;, &lt;code&gt;__call__&lt;/code&gt; 메소드를 작성하고 사용하는 것을 볼 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;__prepare__&lt;/code&gt; 메소드는 메타 클래스가 결정되었을 때 호출되며, 클래스의 네임 스페이스를 준비한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__new__&lt;/code&gt; 메소드는 클래스 객체를 생성 할 때 호출된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__init__&lt;/code&gt; 메소드는 클래스가 생성 된 후 호출되어 클래스를 초기화 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__call__&lt;/code&gt; 메소드는 클래스의 인스턴스를 생성 할 때 호출 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 매직 메서드를 가지고 무슨 일을 할 수 있을까?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싱글톤 패턴 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 패턴은 클래스의 인스턴스화를 항상 하나의 개체로만 제한하는 설계 패턴이다.&lt;br /&gt;구현해 보자면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]


class SingletonClass(metaclass=SingletonMeta):
    pass

sl1 = SingletonClass()
sl2 = SingletonClass()

print(id(sl1))
# 1877212284048
print(id(sl2))
# 1877212284048&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스 생성에 관여하는 &lt;code&gt;__call__()&lt;/code&gt;메소드를 오버라이딩 해서 클래스를 key로 두고 인스턴스를 value로 만들어 클래스당 하나의 인스턴스를 가지도록 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애트리뷰트 검증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DRF의 &lt;b&gt;ModelSerializer&lt;/b&gt; 에는 내부 Meta 클래스가 없다면 오류를 일으킨다. 이에 대한 오류 검증을 DRF에서는 get_fields() 메소드에 구현해 두었는데, 이를 메타 클래스로 검증 할 수 있을 것 같다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def get_fields(self):
    ...

    assert hasattr(self, 'Meta'), (
        'Class {serializer_class} missing &quot;Meta&quot; attribute'.format(
            serializer_class=self.__class__.__name__
        )
    )
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 클래스 내부에 Meta 애트리뷰트가 있는지 확인하는 코드이다. 이를 메타클래스로 검증하는 코드를 짜보자&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class ModelSerializerMetaclass(SerializerMetaclass):
    def __new__(mcs, *args, **kwargs):
        name, bases, namespace = args
        if name not in (&quot;ModelSerializer&quot;,&quot;HyperlinkedModelSerializer&quot;):
            mcs._check_meta(name,namespace)

        return super().__new__(mcs, *args, **kwargs)

    def _check_meta(name,namespace):
        if not namespace.get(&quot;Meta&quot;, None):
            raise Exception(f'Class {name} missing &quot;Meta&quot; attribute')
        return


class ModelSerializer(Serializer, metaclass=ModelSerializerMetaclass):
    pass&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Python</category>
      <category>METACLASS</category>
      <category>python</category>
      <author>hanbindev</author>
      <guid isPermaLink="true">https://hanbin8269.tistory.com/53</guid>
      <comments>https://hanbin8269.tistory.com/53#entry53comment</comments>
      <pubDate>Mon, 6 Jun 2022 22:48:24 +0900</pubDate>
    </item>
  </channel>
</rss>