<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>삼일오일사</title>
    <link>https://tofof.tistory.com/</link>
    <description>삼만천오백십사</description>
    <language>ko</language>
    <pubDate>Sat, 30 May 2026 14:49:47 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>31514</managingEditor>
    <image>
      <title>삼일오일사</title>
      <url>https://tistory1.daumcdn.net/tistory/7319046/attach/904a9f4ccca04ffb9c8f7e2e8a46b4f5</url>
      <link>https://tofof.tistory.com</link>
    </image>
    <item>
      <title>사용자가 원하는 구독 서비스를 쉽고 빠르게 찾아보자!</title>
      <link>https://tofof.tistory.com/120</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 2025 ProfitLab 해커톤에 참여한 뒤로 팀원들과 계속 소통하면서 앱 개발을 하고 있다.&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;그 결과 PostgreSQL을 사용하면 pg_trgm extension과 GIN 인덱스를 사용하여 목표를 달성할 수 있다는 사실을 알게되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 Elasticsearch 같은 것을 사용하여 형태소 분석, 오타 교정 등을 할 수 있지만 우리 서비스에는 오버엔지니어링이라고 판단했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자가 타이핑을 하면 어떤 순서로 검색될까?&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Prefix Match&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`LIKE '검색어%'` &amp;rarr; 인덱스 탐&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. In-fix Match&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`ILIKE '%검색어%'` &amp;rarr; pg_trgm 인덱스 덕분에 %가 앞에 있어도 빠름&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Fuzzy Match&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 결과가 아예 없을 때, &amp;ldquo;혹시 이걸 찾으시나요?&amp;rdquo;를 위한 오타 교정용&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;준비&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- pg_trgm 생성
CREATE EXTENSION pg_trgm;

-- GIN 인덱스 생성
CREATE INDEX idx_services_search_trgm ON subscription_services 
USING gin (name gin_trgm_ops, english_name gin_trgm_ops);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GIN 인덱스란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GIN(Generalized Inverted Index)은 &amp;lsquo;일반화된 역색인&amp;rsquo;이라고 부르며, 주로 배열(Array), JSONB, 그리고 Full-Text Search(텍스트 검색)에 특화된 인덱스이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. GIN의 핵심 개념: 역색인(Inverted Index)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`netflix`라는 단어가 들어간 페이지를 다 가져와라 &amp;rarr; 키워드 목록에서 netflix를 찾고, 그것이 포함된 모든 행의 ID(포인터)를 한꺼번에 반환한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. GIN 인덱스의 내부 동작 (Trigram 기준)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pg_trgm과 GIN이 만나면 다음과 같이 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, name 컬럼에 `netflix`, `net`이라는 두 데이터가 있다고 가정해 보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;분해: 각 단어를 Trigram 조각으로 쪼갠다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;`netflix` &amp;rarr; { n, new, net, etf, tfl, &amp;hellip; }&lt;/li&gt;
&lt;li&gt;`net` &amp;rarr; { n, ne, net, et, t }&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;인덱스 생성: 각 조각이 어느 행에 있는지 기록한다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;`n`: [Row 1, Row 2]&lt;/li&gt;
&lt;li&gt;`etf`: [Row 1]&lt;/li&gt;
&lt;li&gt;`et`: [Row 2]&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;검색: 사용자가 `net`을 검색하면, GIN 인덱스는 { n, ne, net } 조각을 가진 행들의 리스트를 가져와서 교집합을 구한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. B-Tree vs GIN&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 94px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;B-Tree&lt;/td&gt;
&lt;td&gt;GIN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;주 용도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;일치(=), 범위(&amp;gt;, &amp;lt;), 전방 일치(LIKE 'A%')&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;포함(@&amp;gt;), 요소 검색, 중간 일치(LIKE '%A%'), 유사도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;검색 속도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;매우 빠름&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;B-Tree보다는 느리지만 Full Scan보다 압도적으로 빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;삽입/수정 속도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;빠름&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;느림&lt;/b&gt; (데이터 하나 추가 시 수십 개의 조각을 인덱스에 업데이트해야 함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;인덱스 크기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;작음&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;큼&lt;/b&gt; (조각을 많이 생성할수록 커짐)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 주의사항&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;쓰기 성능 저하: 데이터가 `INSERT`될 때마다 수많은 Trigram 조각을 인덱스에 넣어야 하므로 쓰기 작업이 무겁다. 하지만 조회는 많고 수정은 적은 테이블에는 최적의 선택이다.&lt;/li&gt;
&lt;li&gt;`gin_pending_list_limit`: GIN은 쓰기 성능을 보완하기 위해 임시 리스트에 모았다가 한꺼번에 인덱스에 반영한다. 대량의 데이터를 넣을 때는 이 설정을 튜닝하기도 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용할 수 있는 쿼리&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *,
    CASE 
        -- 1순위: 정확히 일치하거나 접두사가 일치 (가장 높은 점수)
        WHEN name LIKE '냇플%' THEN 3
        -- 2순위: 중간에 포함됨 (pg_trgm 인덱스 활용)
        WHEN name ILIKE '%냇플%' THEN 2
        -- 3순위: 오타가 있거나 유사함 (유사도 기반)
        WHEN name % '냇플' THEN 1
        ELSE 0
    END AS search_priority
FROM SUBSCRIPTIONS
WHERE 
    name ILIKE '%넷플%' -- 인덱스 활용을 위한 조건
    OR name % '넷플'    -- 오타 대응용 조건
ORDER BY search_priority DESC, sort_order ASC
LIMIT 10;
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발</category>
      <category>Gin</category>
      <category>pg_trgm</category>
      <author>31514</author>
      <guid isPermaLink="true">https://tofof.tistory.com/120</guid>
      <comments>https://tofof.tistory.com/120#entry120comment</comments>
      <pubDate>Tue, 13 Jan 2026 15:27:39 +0900</pubDate>
    </item>
    <item>
      <title>MinIO Site Replication</title>
      <link>https://tofof.tistory.com/119</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 MinIO의 고가용성을 챙길 수 있는 방법 중 하나인 Site Replication에 대해 알아보고 구축해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Site Replication은 여러 독립된 MinIO 클러스터(사이트)를 하나로 묶어 데이터뿐만 아니라 설정까지 통째로 동기화하는 기능입니다.&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 data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,0,0&quot;&gt;전체 동기화:&lt;/b&gt; 데이터(Object)는 물론이고 IAM 사용자, 그룹, 정책(Policy), 버킷 설정까지 모든 사이트가 동일하게 유지됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,1,0&quot;&gt;Active-Active 지원:&lt;/b&gt; 기본적으로 모든 사이트에서 읽기/쓰기가 가능하며, 한곳에서 변경된 내용은 다른 모든 사이트로 자동 전송됩니다. (질문하신 Active-Passive 구성도 이를 통해 구현됩니다.)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,2,0&quot;&gt;자동 장애 복구:&lt;/b&gt; 특정 사이트가 다운되었다가 복구되면, 중단된 동안의 변경 사항을 자동으로 감지하여 다시 동기화(Resync)합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,3,0&quot;&gt;간편한 확장:&lt;/b&gt; mc admin replicate add 명령 한 번으로 새로운 사이트를 기존 복제 그룹에 추가할 수 있습니다.&lt;/li&gt;
&lt;/ul&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;저는 두 개의 서버를 이용하여 구축해보았습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;A 서버 docker-compose.yml 파일&lt;/h4&gt;
&lt;pre id=&quot;code_1766989655395&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  minio:
    image: minio/minio:RELEASE.2025-09-07T16-13-09Z
    container_name: minio-main
    hostname: minio-main
    restart: unless-stopped

    ports:
      - &quot;9000:9000&quot;
      - &quot;9002:9002&quot;

    volumes:
      - /volume1/minio-data:/data
      - /volume1/docker/minio/logs:/logs

    environment:
      MINIO_ROOT_USER: minio
      MINIO_ROOT_PASSWORD: minio123
      MINIO_SITE_NAME: main-site
      MINIO_SERVER_URL: &quot;http://&amp;lt;A서버 HOST 주소&amp;gt;:9000&quot;
      MINIO_BROWSER: &quot;on&quot;

    command: server /data --console-address &quot;:9002&quot;

    healthcheck:
      test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:9000/minio/health/live&quot;]
      interval: 30s
      timeout: 10s
      retries: 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;B 서버 docker-compose.yml 파일&lt;/h4&gt;
&lt;pre id=&quot;code_1766989695047&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  minio:
    image: minio/minio:RELEASE.2025-09-07T16-13-09Z
    container_name: minio-slave
    hostname: minio-slave
    restart: unless-stopped

    ports:
      - &quot;9000:9000&quot;
      - &quot;9002:9002&quot;

    volumes:
      - /volume1/minio-data:/data
      - /volume1/docker/minio/logs:/logs

    environment:
      MINIO_ROOT_USER: minio
      MINIO_ROOT_PASSWORD: minio123
      MINIO_SITE_NAME: slave-site
      MINIO_SERVER_URL: &quot;http://&amp;lt;B 서버 HOST 주소&amp;gt;:9000&quot;
      MINIO_BROWSER: &quot;on&quot;

    command: server /data --console-address &quot;:9002&quot;

    healthcheck:
      test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:9000/minio/health/live&quot;]
      interval: 30s
      timeout: 10s
      retries: 3&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;두 도커 컴포즈 파일을 모두 실행한 뒤 A 서버에서 다음 명령어를 사용하여 사이트를 등록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`mc alias set minio-main http://&amp;lt;A 서버 HOST 주소&amp;gt;:9000 minio minio123`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`mc alias set minio-slave http://&amp;lt;B 서버 HOST 주소&amp;gt;:9000 minio minio123&lt;a&gt;`&lt;/a&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;` mc admin replicate add minio-main&amp;nbsp;minio-slave`&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;`mc admin replicate info minio-main`를 사용하여 잘 연결되었는지 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 연결되었다면, 저와 같이 기존의 A 서버에 이미 데이터가 존재하는 경우에는 다음 명령어를 사용하여 강제로 데이터 동기화를 진행하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`mc&amp;nbsp;admin&amp;nbsp;replicate&amp;nbsp;resync&amp;nbsp;start&amp;nbsp;minio-main&amp;nbsp;minio-slave`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 `mc admin replicate resync status minio-main minio-slave`를 통해 진행 상황을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마무리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 MinIO Site Replication에 대해 알아보고 직접 구축해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 외부 로드밸런서나 DNS 서비스를 사용하여 자동 장애 조치(Automated Failover)까지 적용해본다면 더욱 좋을 거 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 회사는 외부 사용자가 없다보니, 장애가 발생해도 크리티컬하지 않고 수동으로 갈아끼우면 되는 상황이기에 자동 장애 조치까지 다루지는 않겠습니다 :)&lt;/p&gt;</description>
      <category>개발</category>
      <category>minio</category>
      <category>site replication</category>
      <author>31514</author>
      <guid isPermaLink="true">https://tofof.tistory.com/119</guid>
      <comments>https://tofof.tistory.com/119#entry119comment</comments>
      <pubDate>Mon, 29 Dec 2025 15:45:01 +0900</pubDate>
    </item>
    <item>
      <title>2025 ProfitLab Hackathon 참가</title>
      <link>https://tofof.tistory.com/118</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;968&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RmRim/dJMcafrG81w/twSArjQ2dW0rvskLOai4mK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RmRim/dJMcafrG81w/twSArjQ2dW0rvskLOai4mK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RmRim/dJMcafrG81w/twSArjQ2dW0rvskLOai4mK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRmRim%2FdJMcafrG81w%2FtwSArjQ2dW0rvskLOai4mK%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;968&quot; height=&quot;668&quot; data-origin-width=&quot;968&quot; data-origin-height=&quot;668&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오후 8시부터 익일 오전 7시 30분까지 총 &lt;b&gt;11시간 30분&lt;/b&gt;을 몰입하여 프로덕트에 대해 고민하고 개발하는 경험을 할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 A라는 특정 인물의 불편함을 파악하고 문제를 해결하기 위해 다음과 같은 페르소나를 정의했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;커져가는 구독 시장, 다중 구독의 시대&lt;br /&gt;경제적이고 효율적으로 구독 서비스를 사용하기 위해 어떻게 할 수 있을까?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 따라 &quot;구독 관리 서비스&quot;를 제작하기로 결정했고, 경쟁 어플인 &quot;왓섭&quot;과의 차별점으로 총 2가지를 제시했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;2-30대가 더 경제적으로 구독 서비스를 이용할 수 있도록 &quot;구독팸&quot; 모집을 도와준다.&lt;/li&gt;
&lt;li&gt;서비스 이용자가 구독 서비스를 효율적으로 사용하고 있는지 판단을 도와주기 위해 연령대별, 직무별 구독 사용 현황을 알려준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 ERD 설계를 하면서 끊임없이 프로덕트가 더 나은 방향으로 갈 수 있도록 대화했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1738&quot; data-origin-height=&quot;1144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daLMlL/dJMcagYqzpl/o72aliFShKDKL0GXaQ3sT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daLMlL/dJMcagYqzpl/o72aliFShKDKL0GXaQ3sT1/img.png&quot; data-alt=&quot;ERD 설계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daLMlL/dJMcagYqzpl/o72aliFShKDKL0GXaQ3sT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaLMlL%2FdJMcagYqzpl%2Fo72aliFShKDKL0GXaQ3sT1%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;1738&quot; height=&quot;1144&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1738&quot; data-origin-height=&quot;1144&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ERD 설계&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;그리고 팀원들이 바로 데이터를 사용할 수 있도록 하기 위해서 Postgresql에 테이블을 생성하고, 더미 데이터를 삽입했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;1100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqmuWK/dJMcafrHbrj/NKxwm7NUTPxLeiEXxuueT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqmuWK/dJMcafrHbrj/NKxwm7NUTPxLeiEXxuueT1/img.png&quot; data-alt=&quot;DDL의 일부&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqmuWK/dJMcafrHbrj/NKxwm7NUTPxLeiEXxuueT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqmuWK%2FdJMcafrHbrj%2FNKxwm7NUTPxLeiEXxuueT1%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;1254&quot; height=&quot;1100&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;1100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DDL의 일부&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766892276394&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from faker import Faker
from sqlalchemy import create_engine, text
import time

# PostgreSQL 연결
host = '****'
db_name = '****'
db_user = '****'
db_password = '****'
engine = create_engine(f'postgresql://{db_user}:{db_password}@{host}/{db_name}')

conn = engine.connect()

fake = Faker('ko_KR') # locale 정보 설정
Faker.seed() # 초기 seed 설정

insert_query = text('INSERT INTO users (nickname, email, password_hash, birth, age, created_at, updated_at, job_id) VALUES (:nickname, :email, :password_hash, :birth, :age, :created_at, :updated_at, :job_id)')

with conn.begin() as trans:
    for i in range(999):
        nickname = fake.name()
        # 고유한 email 생성 (인덱스와 타임스탬프를 사용자명에 추가)
        base_email = fake.email()
        email_parts = base_email.split('@')
        unique_email = f&quot;{email_parts[0]}_{i}_{int(time.time() * 1000000)}@{email_parts[1]}&quot;
        password = fake.password()
        birth = fake.date_of_birth(minimum_age=16, maximum_age=65)
        created_at = fake.date_time_between(start_date='-1y', end_date='now')
        updated_at = fake.date_time_between(start_date='-1y', end_date='now')
        job_id = fake.random_int(min=1, max=7)
        age = 2025 - int(str(birth)[:4])

        conn.execute(insert_query, {'nickname': nickname, 'email': unique_email, 'password_hash': password, 'birth': birth, 'age': age, 'created_at': created_at, 'updated_at': updated_at, 'job_id': job_id})
conn.close()&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;더미 데이터는 Faker 라이브러리를 사용하여 넣었고, 위 코드는 `users` 테이블에 들어가는 유저 정보를 생성하는 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 구독 서비스, 유저가 어떤 구독 서비스를 사용하고 있는지 등에 대한 더미 데이터를 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 연령대별, 직무별 통계를 내기 위해 약 4,000개의 데이터를 생성했다.&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;2412&quot; data-origin-height=&quot;1360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJptjd/dJMcafZwuCL/gfDGa5ryJl9p5a8m8ivLdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJptjd/dJMcafZwuCL/gfDGa5ryJl9p5a8m8ivLdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJptjd/dJMcafZwuCL/gfDGa5ryJl9p5a8m8ivLdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJptjd%2FdJMcafZwuCL%2FgfDGa5ryJl9p5a8m8ivLdk%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;2412&quot; height=&quot;1360&quot; data-origin-width=&quot;2412&quot; data-origin-height=&quot;1360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 우리는 담아(Dama)라는 구독 관리 &amp;amp; 커뮤니티 서비스를 제작했고, 사진에 보이는 것과 같은 애플리케이션을 만들었다.&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;ProfitLab 해커톤은 코드가 아닌 비즈니스 실현 가능성을 평가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 타서비스에서 이미 제공하고 있는 가계부 형태의 구독 관리 기능을 우리 서비스에서 다시 구현하는 것보다, &quot;구독팸 결성&quot;과 &quot;구독 서비스 통계&quot;에 집중하고 싶었다.&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;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;h4 data-ke-size=&quot;size20&quot;&gt;결과&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀은 아쉽게도 수상하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 그 원인은 앞서 말했듯 기능 개발에 초점을 둔 것과 비즈니스 모델을 산정할 때 터무니 없는 광고비 계산이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 좋은 사람들과 11시간 30분이라는 긴 시간동안 졸음을 이겨내며 프로덕트를 만들어내겠다는 생각 하나만으로 같이 시간을 보낸 것은 값진 경험이라고 말할 수 있을 거 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-12-28-12-10-25 001.jpeg&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;2338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vua8k/dJMcajt2onm/OvadF4J1ZgnflUPQTUhLc1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vua8k/dJMcajt2onm/OvadF4J1ZgnflUPQTUhLc1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vua8k/dJMcajt2onm/OvadF4J1ZgnflUPQTUhLc1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVua8k%2FdJMcajt2onm%2FOvadF4J1ZgnflUPQTUhLc1%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;1840&quot; height=&quot;2338&quot; data-filename=&quot;KakaoTalk_Photo_2025-12-28-12-10-25 001.jpeg&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;2338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-12-28-12-10-27 003.jpeg&quot; data-origin-width=&quot;3425&quot; data-origin-height=&quot;2569&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nerfL/dJMcaiIGk5C/k6naJY3UC9ZewNplUnhCuk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nerfL/dJMcaiIGk5C/k6naJY3UC9ZewNplUnhCuk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nerfL/dJMcaiIGk5C/k6naJY3UC9ZewNplUnhCuk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnerfL%2FdJMcaiIGk5C%2Fk6naJY3UC9ZewNplUnhCuk%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;3425&quot; height=&quot;2569&quot; data-filename=&quot;KakaoTalk_Photo_2025-12-28-12-10-27 003.jpeg&quot; data-origin-width=&quot;3425&quot; data-origin-height=&quot;2569&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>개발</category>
      <category>Hackathon</category>
      <category>ProfitLab</category>
      <author>31514</author>
      <guid isPermaLink="true">https://tofof.tistory.com/118</guid>
      <comments>https://tofof.tistory.com/118#entry118comment</comments>
      <pubDate>Sun, 28 Dec 2025 12:56:42 +0900</pubDate>
    </item>
    <item>
      <title>GitLab CI/CD 파이프라인 구축하기</title>
      <link>https://tofof.tistory.com/117</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. GitLab Settings에서 필요한 정보 확인하기&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;331&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k8FEU/dJMcaaKkHFW/Q1iAAtQSKMg3U5fWpEKKvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k8FEU/dJMcaaKkHFW/Q1iAAtQSKMg3U5fWpEKKvK/img.png&quot; data-alt=&quot;레포지토리에서 CI/CD 설정 클릭&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k8FEU/dJMcaaKkHFW/Q1iAAtQSKMg3U5fWpEKKvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk8FEU%2FdJMcaaKkHFW%2FQ1iAAtQSKMg3U5fWpEKKvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;455&quot; height=&quot;331&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;331&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;레포지토리에서 CI/CD 설정 클릭&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0QER6/dJMcahJssSf/hIewqRJj0u25R8dzq7IFc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0QER6/dJMcahJssSf/hIewqRJj0u25R8dzq7IFc0/img.png&quot; data-alt=&quot;URL과 token 값 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0QER6/dJMcahJssSf/hIewqRJj0u25R8dzq7IFc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0QER6%2FdJMcahJssSf%2FhIewqRJj0u25R8dzq7IFc0%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;1265&quot; height=&quot;704&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;URL과 token 값 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. CI/CD를 적용할 서버에서 gitlab-runner 설치하고 등록하기&lt;/h4&gt;
&lt;pre id=&quot;code_1761717823444&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1. GitLab 공식 패키지 레포 등록
curl -LO https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh
sudo bash script.deb.sh

# 2. GitLab Runner 설치
sudo apt-get install gitlab-runner -y

# gitlab-runner 등록
sudo gitlab-runner register&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;gitlab-runner를 등록할 때 URL과 token은 위에서 얻은 정보를 기입합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tag도 필수로 입력해줘야 합니다.&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;3. gitlab-runner 서비스 실행&lt;/h4&gt;
&lt;pre id=&quot;code_1761717926174&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo systemctl enable gitlab-runner
sudo systemctl start gitlab-runner
sudo systemctl status gitlab-runner&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. .gitlab-ci.yml 파일 구성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 디렉토리에 `.gitlab-ci.yml` 파일을 구성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서는 &lt;a href=&quot;https://docs.gitlab.com/ci/yaml/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;를 통해 확인할 수 있고, 저는 다음과 같이 구성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761718077230&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stages:          # List of stages for jobs, and their order of execution
  - deploy
  - notify

deploy-job:      # This job runs in the deploy stage.
  stage: deploy  # It only runs when *both* jobs in the test stage complete successfully.
  tags:
    - algorithm0
  environment: production
  rules:
    - if: $CI_COMMIT_REF_NAME == &quot;main&quot;
  script:
    - echo &quot;Deploying application...&quot;
    # 실제 명령어
    - echo &quot;Application successfully deployed.&quot;

notify-slack:
  stage: notify
  tags:
    - algorithm0
  needs: [&quot;deploy-job&quot;]
  script:
    - echo &quot;Sending Slack notification...&quot;
    - |
      curl -X POST -H 'Content-type: application/json' \
        --data &quot;{\&quot;text\&quot;: \&quot;$MESSAGE\&quot;}&quot; \
        $SLACK_WEBHOOK
  rules:
    - if: $CI_COMMIT_REF_NAME == &quot;main&quot;
  when: on_success&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발</category>
      <category>CI/CD</category>
      <category>GitLab</category>
      <author>31514</author>
      <guid isPermaLink="true">https://tofof.tistory.com/117</guid>
      <comments>https://tofof.tistory.com/117#entry117comment</comments>
      <pubDate>Wed, 29 Oct 2025 15:08:30 +0900</pubDate>
    </item>
    <item>
      <title>데이터 품질(Data Quality)</title>
      <link>https://tofof.tistory.com/116</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Databricks에서 데이터 품질에 대해 설명한 글을 읽다가, 데이터 품질을 보장하기 위해 IDA(INSTITUTE FOR DEFENSE ANALYSES)에서 정의한 프레임워크에 대해 알게 되었습니다. 영문으로 되어 있는 문서를 번역해서 정리해보도록 하겠습니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;들어가면서&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요하고 복잡한 많은 C2 활동(지휘 &amp;amp; 통제)은 시간에 따라 변동하며, 다양한 수준의 품질(완결성, 정확성 등)을 지니고, 출처가 불분명한 이질적인 데이터 소스(구조화 &amp;amp; 비구조화)의 사용을 요구한다. 현재 이러한 이질적인 데이터를 처리하는 작업은 수작업이 많이 필요하고 비용이 많이 드는데, 이는 주로 데이터의 품질 문제와 신속한 처리 능력 부족 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 큐레이션은 자동화된 데이터 발견, 고급 검색 기능, 전반적인 데이터 품질 개선, 데이터 재사용 증대를 가능하게 한다. 그리고 데이터 큐레이션의 &quot;&lt;b&gt;7C&lt;/b&gt;&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;데이터 큐레이션 프로세스의 이점은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문제 해결 시간 단축&lt;/li&gt;
&lt;li&gt;데이터 품질 향상&lt;/li&gt;
&lt;li&gt;소요 시간 및 수작업 노력 감소&lt;/li&gt;
&lt;li&gt;복잡한 문제 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;군사 작전에서의 C2 활동(지휘 &amp;amp; 통제) 실패, 재난 대응, 테러 공격에 대한 대응, 이러한 실패는 일반적으로 데이터에 대한 접근 부족 또는 데이터를 필요로 하는 이해관계자들에게 전달하는 과정이 불안정하여 잘못된 것으로 나타났다. 적절한 품질의 데이터는 C2 활동의 효과적인 수행에 매우 중요하다. 하지만 데이터를 제공하는 것은 다음과 같은 원인으로 인해 점점 더 어려워 지고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 센서와 무인 시스템 증가&lt;/li&gt;
&lt;li&gt;소셜 미디어&lt;/li&gt;
&lt;li&gt;온라인 저장소&lt;/li&gt;
&lt;li&gt;다양한 통신 방식&lt;/li&gt;
&lt;/ul&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;C2 sensemaking 관련 기능 및 프로세스(추론, 계획, 의사결정, 협업 등)은 시간에 따라 달라지고, 품질 수준(완전성, 정확성 등)이 다양하며, 출처가 모호한 서로 다른 데이터 소스(구조화된 데이터, 비구조화된 데이터)를 사용해야 한다. 현재 이러한 데이터를 처리하는 것은 수작업으로 진행되고 비용이 많이 들며, 시간이 많이 소모된다. 그 이유는 대부분 &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;데이터 큐레이션은 예상치 못한 질문에 신속하게 답변할 수 있도록 데이터를 미리 준비하고, 자동 처리를 용이하게 하는 형태로 데이터를 유지함으로써 C2 sensemaking을 보다 잘 지원할 수 있다. 원본 데이터 소스는 메타데이터로 보강되어 주어진 목적에 대한 데이터의 유용성을 이해하고 판단하는 부담을 줄여준다.&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;li&gt;도메인별 데이터 저장소의 네트워크 가용성&lt;/li&gt;
&lt;li&gt;시각화 및 검색 기능 향상&lt;/li&gt;
&lt;li&gt;빅데이터 계산 방법(?)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;큐레이션할 데이터 유형&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 큐레이션은 구조화된 데이터, 반구조화된 데이터, 비구조화된 데이터에 모두 적용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비구조화된 데이터는 일반적으로 기술 보고서, 뉴스 기사 등에서 볼 수 있는 텍스트, 사진, 동영상이 포함된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조화된 데이터는 행(row)과 열(column)로 구성된 고정된 스키마를 따르는 데이터를 말하며, 우리가 흔히 알고 있는 RDBMS에서 다루는 데이터가 그 예이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반구조화된 데이터는 고정 스키마는 없지만, 데이터 내에 구조적 정보가 들어가 있는 형태를 말하며, JSON과 XML, YAML 등이 그 예이다.&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;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;C2 활동(지휘 &amp;amp; 통제)에 데이터 큐레이션 기술을 적용하려면 특정 문제를 해결하기 위해 개발된 방법과 과학적 및 상업적 분야에서 개발된 방법을 통합해야 한다. 메타데이터 형식의 표준화와 용어 및 그 의미는 자동화 또는 수동 작업과 관계없이 큐레이션의 성공적인 적용에 있어 중요한 측면이다. 공통된 용어와 개념을 채택하면 큐레이션의 7단계 각각에 메타데이터를 통합하는 데 도움이 되며, 이는 이전 단계의 결과를 이해하는 데 기초가 된다.&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;데이터 큐레이션 문제는 &quot;&lt;b&gt;7C&lt;/b&gt;&quot; 모델을 사용하여 설명할 수 있다.&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;데이터 큐레이션의 7C&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 큐레이션 프로세스는 데이터 품질을 향상시키고 데이터 공유, 처리 및 사용을 용이하게 하는 일련의 단계이다. 이 프로세스는 데이터 큐레이션의 &quot;7C&quot;를 통해 설명할 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;수집(&lt;b&gt;C&lt;/b&gt;ollect) - 데이터 소스에 연결하고 입력을 받아들인다.&lt;/li&gt;
&lt;li&gt;특성화(&lt;b&gt;C&lt;/b&gt;haracterize) - 사용 가능한 메타데이터를 추가한다.&lt;/li&gt;
&lt;li&gt;정리(&lt;b&gt;C&lt;/b&gt;lean) - 데이터 품질 문제를 식별하고 수정한다.&lt;/li&gt;
&lt;li&gt;맥락화(&lt;b&gt;C&lt;/b&gt;ontextualize) - 맥락 및 출처 정보를 제공한다.&lt;/li&gt;
&lt;li&gt;분류(&lt;b&gt;C&lt;/b&gt;ategorize) - 문제를 도메인에 맞게 분류한다.&lt;/li&gt;
&lt;li&gt;상관관계(&lt;b&gt;C&lt;/b&gt;orrelate) - 다양한 데이터 간의 상관관계를 분석한다.&lt;/li&gt;
&lt;li&gt;목록화(&lt;b&gt;C&lt;/b&gt;atalog) - 검색 및 분석을 위한 API를 사용하여 데이터와 메타데이터를 저장하고 접근 가능하게 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수집&lt;/b&gt; 단계는 구조화된 데이터 저장소(RDBMS)나 텍스트 문서를 저장할 수 있는 NoSQL2에 데이터를 형식화하여 자동으로 저장하는 절차를 포함한다. 데이터는 확장 가능한 마크업 언어(XML) 또는 JSON과 같은 일반적인 표준 형식으로 저장해야 한다.&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;단계는 데이터가 수집될 때 적용되며, 생성 시간, 수집 방법, 센서 설명 및 설정, 정확도, 정밀도, 위치 등과 같은 메타데이터가 데이터와 함께 제공되고 기록된다. 적절한 특성화 데이터는 해당 도메인과 용도에 달라진다. 이러한 수준의 표준화 활동은 다양한 분야, 특히&amp;nbsp;의학 및 생물학 연구 분야에 걸쳐 나타나기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정리&lt;/b&gt; 단계는 기본적인 데이터 품질 도구를 데이터에 적용하여 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;데이터의 문제를 식별하고 해결한다&lt;/span&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;li&gt;데이터가 불완전한 경우&lt;/li&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;데이터베이스 커뮤니티에 많은 데이터 정리 기술들이 잘 알려져 있으며, 종종 ETL 프로세스로 구현된다. 데이터를 정리하는 방법은 오타 수정부터 인공지능 기술을 사용하여 누락된 관계를 추론하거나 대체 표준 표현으로 변환하는 것까지 복잡성이 증가할 수 있다. 시간이 지남에 따라 데이터 문제를 해결하는 데 드는 비용이 증가하기 때문에 데이터 큐레이션 과정에서 가능한 빨리 데이터를 정리하는 것이 바람직하다. 이러한 방법들은 비정형 데이터에도 확장되고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;맥락화&lt;/b&gt; 단계는 맥락이나 특정 문제뿐만 아니라 데이터의 용도에 따라 달라진다. 이러한 측면은 인증 및 기타 출처 정보와 같은 추가 메타데이터가 필요한지 알려준다. 예를 들어, 정보기관 애플리케이션은 일상적인 물류 요청보다 더 높은 수준의 출처 정보가 필요할 수 있다. 도메인은 데이터의 가장 적합한 메타데이터의 특정 형식이나 표현을 지시할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;분류&lt;/b&gt; 단계는 데이터에서 주요 관심 속성을 더욱 명확하게 식별한다. 자연어 처리, 텍스트 분석, 머신 러닝을 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;반구조화 및 비구조화 데이터에 적용&lt;/span&gt;하여 주요 관심 속성을 식별하고 추출할 수 있다. 이미지 분석은 이미지나 비디오 파일의 주요 특징을 식별하는 데 사용할 수 있다. 추출된 특정 속성은 문제 영역에 따라 달라진다. 예를 들어, 감성 분석은 블로그 데이터에서 신제품이나 제안된 정책 이니셔티브에 대한 의견을 추출하는 데 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;카탈로그&lt;/b&gt; 단계는 데이터와 메타데이터를 수명 주기 동안 저장하고 보존하며, 데이터 저장소에 게시하거나 지정된 소비자에게 푸시하거나 신속한 검색을 위한 인덱싱과 같은 배포를 준비한다. 데이터의 검색, 추출 및 기본 분석을 위해 API를 제공할 수 있으며, 일반적으로 웹 서비스로 구현된다. 이 단계에서 &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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;과제&lt;/h3&gt;
&lt;p data-end=&quot;164&quot; data-start=&quot;68&quot; data-ke-size=&quot;size16&quot;&gt;현재 대부분의 데이터 큐레이션은 여전히 수작업에 크게 의존하고 있다. 그 결과, 많은 전술 상황에서는 이 과정이 너무 느려서 제때 효과를 내지 못할 수 있다. 전투원이 전술 상황을 파악하려 할 때, 처리해야 하는 디지털 텍스트 정보가 너무 많아 행동으로 옮기기 전에 제때 분석을 마치기 어렵다. 게다가 데이터는 우선순위가 매겨져 있지 않고, 출처(provenance)가 태그되지 않았으며, 요약 정보도 제공되지 않기 때문에 전투원은 정보를 걸러내는 데 도움을 받지 못하고 빠르게 데이터 과부하(data overload) 상태에 빠질 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;164&quot; data-start=&quot;68&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;513&quot; data-start=&quot;414&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하려면 자연어 처리(NLP)와 텍스트 분석 기법, 그리고 이미지&amp;middot;비디오 데이터를 분석하기 위한 기법들을 목적에 맞게 수정&amp;middot;확장해 적용해야 한다. 특히 실시간 정보 처리는 자동화가 필수적이다. 무인항공기(UAV)와 같은 센서에서 들어오는 비디오 및 다른 데이터 집약적 소스들은 데이터의 유효 수명 내에 처리할 수 없을 만큼 많은 데이터를 만들어낸다. 그러나 현재의 이미지 처리, 이미지 이해, 장면 분석 기술은 이러한 요구를 충족하기에는 여전히 부족하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;83&quot; data-ke-size=&quot;size16&quot;&gt;자동화된 디지털 데이터 큐레이션의 개념은 기본 데이터에 메타데이터를 추가해 데이터의 분석 활용도를 높이고 데이터 공유를 촉진하는 &lt;b&gt;7단계 과정&lt;/b&gt;으로 설명할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;83&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;83&quot; data-ke-size=&quot;size16&quot;&gt;현재 국방부(DoD), 미국 연방 정부, 그리고 과학 연구 커뮤니티 대부분에서 데이터 큐레이션은 여전히 &lt;b&gt;수작업 중심&lt;/b&gt;으로 이루어지고 있다. 하지만 디지털 데이터의 양과 빅데이터를 활용하는 애플리케이션의 수가 급증하는 상황에서 이 방식은 지속 가능하지 않다.&lt;br /&gt;원하는 메타데이터를 만들기 위해 사람이 직접 개입해야 하는 프로세스에서 발생하는 &lt;b&gt;병목현상&lt;/b&gt;을 해결하려면 추가적인 &lt;b&gt;자동화&lt;/b&gt;가 필요하다.&lt;/p&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;83&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;652&quot; data-start=&quot;446&quot; data-ke-size=&quot;size16&quot;&gt;데이터 큐레이션이 지휘&amp;middot;통제(Command &amp;amp; Control, C2)에 얼마나 효과적인지는 여전히 연구 중이다. 효과 자체는 분명하지만, 큐레이션 단계를 제때 완료할 수 있도록 &lt;b&gt;자동화&lt;/b&gt;하는 것은 여전히 주요 과제로 남아 있다. 따라서 실제 C2 환경에서 데이터 큐레이션 단계를 더 명확히 정의하고 자동화하기 위한 추가 연구가 필요하다.&lt;/p&gt;</description>
      <category>개발</category>
      <category>데이터 품질</category>
      <author>31514</author>
      <guid isPermaLink="true">https://tofof.tistory.com/116</guid>
      <comments>https://tofof.tistory.com/116#entry116comment</comments>
      <pubDate>Tue, 16 Sep 2025 16:18:45 +0900</pubDate>
    </item>
    <item>
      <title>YOLO 모델 서빙 코드 성능 향상 경험</title>
      <link>https://tofof.tistory.com/115</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 알고리즘 팀에서 작성한 모델 파일을 통해 이미지를 추론하여 그 결과를 가공하고 DB에 적재하는 업무를 하던 중 경험한 내용을 정리한 글입니다.&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;먼저 알고리즘 팀으로부터 모델 파일과 추론 결과값 형식을 제공받은 후 서빙 코드를 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1754447447909&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def inference_on_gpu(device_id: int, image_list: List[str]) -&amp;gt; None:
    try:
        # 모델 로드
        model = load_model(model_path=model_path, device_id=device_id, save_dir=save_dir, save_folder=&quot;run&quot;)

        for jpg_filename in image_list:
            # 이미지 하나씩 모델 실행
            results = model.predict(
                jpg_filename,
                save=True, # 추론 결과를 저장할지 여부 O
            )

            # 기타 로직 수행...

            # txt 파일에 결과 쓰기
            txt_filename = change_extension(jpg_filename, &quot;txt&quot;)
            with open(txt_filename, &quot;w&quot;) as f:
                for result in results:
                    f.write(...)
    except Exception as e:
        # 예외 처리

def main():
    # 기타 로직 수행...

    # 가용 가능한 GPU 수 파악
    gpu_list = [int(d.strip()) for d in args.gpus.split(&quot;,&quot;) if d.strip() != &quot;&quot;]

    # jpg 파일 검색
    jpg_paths = find_files_by_extension(directory_path=...)

    # jpg 파일을 GPU 수에 맞게 균등 분할
    partitions = partition_list(jpg_paths, len(gpu_list))
    
    # 가용 가능한 GPU 병렬 처리
    with ProcessPoolExecutor(max_workers=len(gpu_list)) as executor:
        futures = []
        for device, partition in zip(gpu_list, partitions):
            futures.append(
                executor.submit(inference_on_gpu, device, partition, ...)
            )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드의 프로세스에 대하여 간략하게 설명하면 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;가용 GPU 수 확인&lt;/li&gt;
&lt;li&gt;입력 이미지 수집&lt;/li&gt;
&lt;li&gt;이미지를 GPU 수에 따라 균등하게 분할&lt;/li&gt;
&lt;li&gt;GPU 병렬 처리로 YOLO 모델 추론&lt;/li&gt;
&lt;li&gt;추론 결과를 텍스트 파일로 저장&lt;/li&gt;
&lt;/ol&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;위 프로세스 중 1번부터 4번까지는 개선할 수 있는 포인트가 없다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 5번의 경우 여러 장의 이미지를 하나하나 순회하면서 모델을 추론하고 텍스트 파일에 결과를 저장하고 있어서, 개선 가능성을 찾기 시작했습니다.&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;GPU 유휴 시간 발생 : 추론이 끝난 후 CPU I/O 작업 완료까지 다음 추론이 대기&lt;/li&gt;
&lt;li&gt;GPU-CPU 메모리 전송 비용 : GPU 메모리에서 CPU 메모리로의 데이터 복사 오버헤드 발생&lt;/li&gt;
&lt;/ul&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;저는 GPU 유휴 시간을 최소화하기 위해 추론 결과 파일 쓰기를 비동기 처리해야겠다는 생각이 들었고, 그 과정에서 큐 자료구조와 스레드를 사용하여 더 효과적으로 성능을 개선했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3ejPA/btsPJfsL7p8/IkJ5bgP2Y8dpPh3JODma8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3ejPA/btsPJfsL7p8/IkJ5bgP2Y8dpPh3JODma8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3ejPA/btsPJfsL7p8/IkJ5bgP2Y8dpPh3JODma8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3ejPA%2FbtsPJfsL7p8%2FIkJ5bgP2Y8dpPh3JODma8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;748&quot; height=&quot;116&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 GPU에서 추론이 완료된 결과를 Queue에 보내고 바로 다음 이미지 추론을 시작하고, 스레드는 Queue에서 결과값을 꺼내어 쓰기 작업을 하는 방식입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Queue 관련 코드&lt;/h4&gt;
&lt;pre id=&quot;code_1754453618554&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;write_queue = Queue(maxsize=200)  # 큐 생성

for i, image_path in enumerate(image_list):
    # 이미지 추론
    write_queue.put((txt_filename, txt_lines), timeout=0.1) # 추론 결과 Queue에 삽입&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Thread 관련 코드&lt;/h4&gt;
&lt;pre id=&quot;code_1754453675164&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;&quot;&quot; Thread에서 실행될 파일 쓰기 로직 &quot;&quot;&quot;
def file_writer_thread(write_queue: Queue, device_id: int):
    written_count = 0
    
    while True:
        try:
            # 큐에서 작업 가져오기 (타임아웃 1초)
            item = write_queue.get(timeout=1)
            
            # 종료 신호 확인
            if item is None:
                break
                
            filename, lines = item
            
            # 파일 쓰기 실행
            try:
                with open(filename, &quot;w&quot;) as f:
                    f.writelines(lines)
                written_count += 1
                
            # 예외 처리 ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754453805794&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Thread에서 실행할 함수 지정
writer_thread = Thread(target=file_writer_thread, args=(write_queue, device_id))
writer_thread.daemon = True # 백그라운드 작업 진행
writer_thread.start() # Thread 시작 포인트&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-type=&quot;horizontalRule&quot; 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;2,048 x 12,000 사이즈의 이미지 12,780개를 처리하는데 1426.81초가 소요되던 원래 코드를 수정하여 672.75초로 단축시켜 약 `52.85%` 성능 개선을 달성했습니다.&lt;/p&gt;</description>
      <category>개발</category>
      <category>thread</category>
      <category>비동기</category>
      <author>31514</author>
      <guid isPermaLink="true">https://tofof.tistory.com/115</guid>
      <comments>https://tofof.tistory.com/115#entry115comment</comments>
      <pubDate>Wed, 6 Aug 2025 13:22:45 +0900</pubDate>
    </item>
    <item>
      <title>오픈 소스의 버그를 발견한 경험</title>
      <link>https://tofof.tistory.com/114</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 Superset이라는 오픈소스 BI 툴을 사용하던 중에 어이없는 오류를 발견했다.&lt;/p&gt;
&lt;pre id=&quot;code_1751422427104&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WITH user_cte AS (
    SELECT name, age
    FROM test
    WHERE age &amp;gt; 20
)
SELECT 
    name as &quot;사람 이름&quot;,
    age as &quot;나이&quot;,
    COUNT(*) as &quot;something else&quot;
FROM user_cte
GROUP BY name, age;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리를 사용하면 아래와 같은 오류가 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1751422452856&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Unexpected error
Custom SQL fields cannot contain sub-queries.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 아래 쿼리를 사용하면 오류가 발생하지 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1751422483331&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WITH user_cte AS (
    SELECT name, age
    FROM test
    WHERE age &amp;gt; 20
)
SELECT 
    name as &quot;사람 이름&quot;,
    age as &quot;나이&quot;,
    COUNT(*) as &quot;테스트 컬럼&quot;
FROM user_cte
GROUP BY name, age;&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;두 쿼리는 같아 보이지만, `COUNT(*)` 함수의 alias 컬럼명이 다르다.&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 `validate_adhoc_subquery`라는 함수에 문제가 있다는 것을 알았고, 수정을 시도했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 이때 회사에서 사용하는 Superset 코드를 뜯어보지 않고, Github에서 최신 버전을 `clone`하여 수정했다.&lt;/p&gt;
&lt;pre id=&quot;code_1751422727720&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def validate_adhoc_subquery(
    sql: str,
    database: Database,
    catalog: str | None,
    default_schema: str,
    engine: str,
) -&amp;gt; str:
    &quot;&quot;&quot;
    Check if adhoc SQL contains sub-queries or nested sub-queries with table.

    If sub-queries are allowed, the adhoc SQL is modified to insert any applicable RLS
    predicates to it.

    :param sql: adhoc sql expression
    :raise SupersetSecurityException if sql contains sub-queries or
    nested sub-queries with table
    &quot;&quot;&quot;
    parsed_statement = SQLStatement(sql, engine)
    if parsed_statement.has_subquery():
        if not is_feature_enabled(&quot;ALLOW_ADHOC_SUBQUERY&quot;):
            raise SupersetSecurityException(
                SupersetError(
                    error_type=SupersetErrorType.ADHOC_SUBQUERY_NOT_ALLOWED_ERROR,
                    message=_(&quot;Custom SQL fields cannot contain sub-queries.&quot;),
                    level=ErrorLevel.ERROR,
                )
            )

        # enforce RLS rules in any relevant tables
        apply_rls(database, catalog, default_schema, parsed_statement)

    return parsed_statement.format()&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;하지만 코드를 아무리 분석해 봐도 문제가 될 만한 부분을 찾지 못했고, Github Issue를 확인한 뒤 이 &lt;a href=&quot;https://github.com/apache/superset/issues/32541&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;페이지&lt;/a&gt;를 발견할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈 페이지의 마지막 부분을 살펴보면, `SQLParse`를 `SQLGlot`으로 대체했다는 글이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불과 3주 전에 작성되었던 글이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(SQLParse를 사용하는 옛날 코드는 더보기 버튼을 클릭)&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1751423523201&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def validate_adhoc_subquery(
    sql: str,
    database_id: int,
    engine: str,
    default_schema: str,
) -&amp;gt; str:
    &quot;&quot;&quot;
    Check if adhoc SQL contains sub-queries or nested sub-queries with table.

    If sub-queries are allowed, the adhoc SQL is modified to insert any applicable RLS
    predicates to it.

    :param sql: adhoc sql expression
    :raise SupersetSecurityException if sql contains sub-queries or
    nested sub-queries with table
    &quot;&quot;&quot;
    statements = []
    for statement in sqlparse.parse(sql):
        try:
            has_table = has_table_query(str(statement), engine)
        except SupersetParseError:
            has_table = True

        if has_table:
            if not is_feature_enabled(&quot;ALLOW_ADHOC_SUBQUERY&quot;):
                raise SupersetSecurityException(
                    SupersetError(
                        error_type=SupersetErrorType.ADHOC_SUBQUERY_NOT_ALLOWED_ERROR,
                        message=_(&quot;Custom SQL fields cannot contain sub-queries.&quot;),
                        level=ErrorLevel.ERROR,
                    )
                )
            # TODO (betodealmeida): reimplement with sqlglot
            statement = insert_rls_in_predicate(statement, database_id, default_schema)

        statements.append(statement)

    return &quot;;\n&quot;.join(str(statement) for statement in statements)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 회사에서 사용하는 Superset 버전은 `SQLParse`를 사용하는 3주 전의 버전이었던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 좀 허탈했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;며칠 동안 열심히 코드 분석을 했지만 물거품이 된 기분이었다.&lt;/p&gt;
&lt;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;blockquote data-ke-style=&quot;style2&quot;&gt;오픈 소스에 문제가 있다면 사용하고 있는 버전을 확인하고, Github에서 이 문제가 이미 해결되었는지 확인하자.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>SQL</category>
      <category>Superset</category>
      <author>31514</author>
      <guid isPermaLink="true">https://tofof.tistory.com/114</guid>
      <comments>https://tofof.tistory.com/114#entry114comment</comments>
      <pubDate>Wed, 2 Jul 2025 11:33:58 +0900</pubDate>
    </item>
    <item>
      <title>PostgreSQL에서 FLOAT 타입 제대로 알고 사용하기</title>
      <link>https://tofof.tistory.com/113</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 회사는 PostgreSQL에 실수 값이 `real(float4)` 타입으로 저장되어 있으며, 이 데이터를 JavaScript에서 `Math.round()`를 사용하여 반올림하여 통계를 제공하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 통계 방식을 위해 Superset을 도입했고, 이 과정에서 SQL의 `ROUND()` 함수를 사용하여 반올림했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 통계 수치나 비교 결과가 &lt;b&gt;불일치&lt;/b&gt;하는 문제가 발생했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;부동소수점의 한계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 얘기했듯, PostgreSQL에는 실수가 `real` 타입으로 저장되고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`real`과 `double precision` 같은 부동소수점 타입은 이진수로 표현할 수 있는 숫자만 정확히 저장할 수 있어서, 대부분의 10진 실수는 근사값으로 저장된다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 0.1::real;             -- 출력: 0.1 (처럼 보임)
SELECT 0.1::real::numeric(20,18);
-- numeric을 사용하여 정확한 수치 출력: 0.100000001490116119

SELECT 0.1::real = 0.1::numeric;  -- false
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 차이로 인해 `real` 타입으로 저장된 실수에 `ROUND` 함수를 적용하면 오차가 발생할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;서로 다른 반올림 방식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서 사용하는 `Math.round()` 함수는 사사오입 방식을 채택하고 있고, PostgreSQL은 Banker&amp;rsquo;s rounding 방식을 채택하고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사사오입 방식 - 0.5 이상이면 무조건 올림&lt;/li&gt;
&lt;li&gt;Banker&amp;rsquo;s rounding 방식 - .5일 때는 가장 가까운 짝수로 반올림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예)&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;값&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Math.round()&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;ROUND()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;1.5&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;2.5&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;3.5&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;4.5&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL이 Banker&amp;rsquo;s rounding 방식을 채택한 이유는 &lt;b&gt;한쪽으로 쏠리는 누적 오차를 줄이기 위함&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 부동소수점의 한계를 해결하기 위해 데이터를 `text`로 변환한 후에 `numeric`으로 변환했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후에 SQL의 `ROUND` 함수를 사용하지 않고, JavaScript에서 사용하는 사사오입 방식을 그대로 구현했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FLOOR(실수::text::numeric * 1000 + 0.5) / 1000
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>float</category>
      <category>Round</category>
      <category>SQL</category>
      <author>31514</author>
      <guid isPermaLink="true">https://tofof.tistory.com/113</guid>
      <comments>https://tofof.tistory.com/113#entry113comment</comments>
      <pubDate>Fri, 20 Jun 2025 11:32:15 +0900</pubDate>
    </item>
    <item>
      <title>.pyc 파일</title>
      <link>https://tofof.tistory.com/112</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;pyc 파일.jpg&quot; data-origin-width=&quot;1752&quot; data-origin-height=&quot;2477&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ISULe/btsOwbkImrI/rmB54eE1omKjp16BPuKOsk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ISULe/btsOwbkImrI/rmB54eE1omKjp16BPuKOsk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ISULe/btsOwbkImrI/rmB54eE1omKjp16BPuKOsk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FISULe%2FbtsOwbkImrI%2FrmB54eE1omKjp16BPuKOsk%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;1752&quot; height=&quot;2477&quot; data-filename=&quot;pyc 파일.jpg&quot; data-origin-width=&quot;1752&quot; data-origin-height=&quot;2477&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>개발</category>
      <category>python</category>
      <author>31514</author>
      <guid isPermaLink="true">https://tofof.tistory.com/112</guid>
      <comments>https://tofof.tistory.com/112#entry112comment</comments>
      <pubDate>Tue, 10 Jun 2025 17:37:22 +0900</pubDate>
    </item>
    <item>
      <title>Airflow 컨테이너를 새로운 우분투 유저로 올리면서 생긴 일</title>
      <link>https://tofof.tistory.com/111</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우분투 서버에 새로운 유저를 만들고, Airflow 컨테이너를 올리는 과정에 겪은 문제를 기록합니다.&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;rootless-docker를 사용했고, 유저명은 niscom입니다.&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://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Airflow 공식 홈페이지&lt;/a&gt;에서 docker-compose.yaml 파일을 다운로드 받습니다.&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.9.1 버전을 선택했고, Playwright 라이브러리를 사용해야 했기 때문에 `build .` 주석 처리를 해제합니다.&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;Airflow docker-compose.yaml 파일을 살펴보면 user와 관련된 설정이 있는데, Airflow UID:GID로 구성되어 있습니다.&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;새로운 유저로 아무런 설정 없이 `docker compose up -d`를 실행하면 다음과 같은 오류가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1747050504206&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;airflow-scheduler-1  | PermissionError: [Errno 13] Permission denied: '/opt/airflow/logs/dag_processor_manager/dag_processor_manager.log'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼핏 봐도 권한 문제인 걸 확인할 수 있고, 이를 해결하기 위해 다양한 시도를 해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;`logs/` 폴더의 권한을 755로 변경하기&lt;/li&gt;
&lt;li&gt;`logs/` 폴더의 소유자를 root로 변경하기&lt;/li&gt;
&lt;li&gt;airflow docker-compose.yaml 파일에서 user UID를 niscom 유저 id로 변경하기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 결국 해결되지 않았고, 문제를 각 폴더의 GID라는 것을 알게 되었습니다.&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;pre id=&quot;code_1747050630557&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

# 현재 UID / GID 확인
AIRFLOW_UID=$(id -u)
AIRFLOW_GID=$(id -g)

# .env 파일 생성 또는 업데이트
echo &quot;AIRFLOW_UID=${AIRFLOW_UID}&quot; &amp;gt; .env

# 폴더 목록
dirs=(&quot;logs&quot; &quot;dags&quot; &quot;plugins&quot; &quot;data&quot;)

# 각 폴더가 없다면 생성하고, 소유권 변경
for dir in &quot;${dirs[@]}&quot;; do
  mkdir -p &quot;$dir&quot;
  sudo chown -R &quot;${AIRFLOW_UID}:${AIRFLOW_GID}&quot; &quot;$dir&quot;
done

echo &quot;.env 및 디렉토리 권한 설정 완료 ✅&quot;

docker compose up -d

echo &quot;Airflow 컨테이너 실행 완료 ✅&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;위 스크립트는 Airflow가 사용하는 각 폴더의 유저 ID와 그룹 ID를 변경해주는 스크립트입니다.&lt;/p&gt;</description>
      <category>개발</category>
      <category>Airflow</category>
      <category>docker</category>
      <author>31514</author>
      <guid isPermaLink="true">https://tofof.tistory.com/111</guid>
      <comments>https://tofof.tistory.com/111#entry111comment</comments>
      <pubDate>Mon, 12 May 2025 20:51:53 +0900</pubDate>
    </item>
  </channel>
</rss>