<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>GW LABS</title>
    <link>https://gwlabs.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 9 May 2026 11:20:24 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>GeonWoo Kim</managingEditor>
    <image>
      <title>GW LABS</title>
      <url>https://tistory1.daumcdn.net/tistory/3015196/attach/b9d8271fdde4472a8749336f9836a4fd</url>
      <link>https://gwlabs.tistory.com</link>
    </image>
    <item>
      <title>읽은 글을 기억으로: 서버비 0원의 온디바이스 AI 퀴즈 앱 'Blank.' 개발기</title>
      <link>https://gwlabs.tistory.com/146</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf8CtM/dJMcabpJLHs/BAmqsP9pz6XoC63hRk3kik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf8CtM/dJMcabpJLHs/BAmqsP9pz6XoC63hRk3kik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf8CtM/dJMcabpJLHs/BAmqsP9pz6XoC63hRk3kik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf8CtM%2FdJMcabpJLHs%2FBAmqsP9pz6XoC63hRk3kik%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; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 매일 수많은 아티클과 뉴스레터를 읽습니다. 하지만 다음 날이면 내용이 가물가물해지곤 합니다. &amp;lsquo;스크랩만 해두고 다시 보지 않는 글들을 강제로 복습하게 만들 순 없을까?&amp;rsquo; 이 작은 고민에서 출발한 앱, &lt;b&gt;Blank.&lt;/b&gt;의 개발 과정을 공유합니다. Blank는 링크만 넣으면 AI가 핵심을 파악해 빈칸 채우기(Cloze Test) 문제를 만들어주는 안드로이드 앱입니다.&lt;/p&gt;
&lt;h1&gt;1. 왜 서버가 아니라 기기 내부(On-device)를 선택했나&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인화된 학습 도구를 만들 때 가장 큰 고민은 두 가지였습니다. 첫째는 트래픽에 비례해 증가하는 유지보수 비용(서버비)이었고, 둘째는 사용자가 어떤 글을 읽는지에 대한 프라이버시 문제였습니다. 이를 해결하기 위해 서버 기반의 API 대신 기기 내부에서 직접 AI를 구동하는 방식을 택했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;온디바이스 LLM (On-device LLM)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라우드 서버를 거치지 않고, 스마트폰 기기 내부의 연산 자원(AP)만으로 구동되는 소형 인공지능 모델(이 프로젝트에서는 Google Gemma 3 사용)입니다.&lt;/li&gt;
&lt;li&gt;외부로 데이터를 전송하지 않아 사용자의 프라이버시를 완벽히 보호하고, 개발자의 서버 유지 비용을 0원으로 만들기 위해 쓰입니다.&lt;/li&gt;
&lt;li&gt;데이터 보안이 민감한 메모 앱이거나, 비행기 모드 같은 오프라인 환경에서도 핵심 AI 기능이 동작해야 할 때 사용합니다.&lt;/li&gt;
&lt;li&gt;디자이너 관점 비교: 피그마(Figma)가 서버에 접속해야만 쓸 수 있는 클라우드 기반이라면, 온디바이스는 포토샵이나 일러스트레이터처럼 내 컴퓨터의 리소스를 직접 써서 오프라인에서도 돌아가는 독립적인 작업 환경과 같습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;2. 출시의 기쁨, 그리고 예상치 못한 암초&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초 버전을 완성한 뒤 긱뉴스(GeekNews)에 앱을 공개했고, 5일 만에 87대의 기기에서 다운로드되는 의미 있는 성과를 얻었습니다. 하지만 한 유저분의 뼈아픈 피드백이 도착했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;나무위키의 특정 인물 링크를 넣었더니, 7문제 중 5문제의 정답이 모두 그 인물 이름이었습니다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 핵심 경험을 해치는 치명적인 문제였습니다. 문서의 빈도수에 의존하다 보니 AI가 가장 많이 등장하는 단어에만 꽂히는 과적합(Overfitting) 현상이 발생한 것입니다.&lt;/p&gt;
&lt;h1&gt;3. 문제 해결 과정: AI의 고집 꺾기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 프롬프트에 &quot;이미 나온 단어는 쓰지 마세요&quot;라고 지시하거나, 중복된 정답이 나오면 다시 생성하게 만드는 재시도(Retry) 로직을 고려했습니다. 하지만 모바일 환경에서 수 초가 걸리는 추론을 여러 번 반복하는 것은 치명적인 UX 저하를 낳습니다. 속도를 희생하지 않으면서 확실한 통제 수단이 필요했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;텍스트 치환 (Input Substitution)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI에게 문장을 분석하라고 넘기기 직전, 이미 정답으로 출제된 단어들을 코드 레벨에서 원문으로부터 강제로 가려버리는(OOO 등으로 변경) 전처리 기법입니다.&lt;/li&gt;
&lt;li&gt;AI가 기존 정답 단어 자체를 아예 읽지 못하게 만들어 중복 추출을 원천 차단하고, 무거운 추론 과정을 단 1회로 끝내어 앱의 반응 속도를 방어하기 위해 쓰입니다.&lt;/li&gt;
&lt;li&gt;모바일 환경처럼 연산 비용이 비싸서 호출 횟수를 1회로 통제하면서 100% 확실하고 다양한 결과를 얻어야 할 때 사용합니다.&lt;/li&gt;
&lt;li&gt;작업자에게 &quot;파란색은 빼고 그려주세요&quot;라고 말로 지시하는 것(프롬프트)은 무의식적으로 파란색을 쓸 위험이 남습니다. 텍스트 치환은 애초에 팔레트에서 파란색 물감을 빼앗은 뒤 시안을 요구하는 것과 같습니다. 오류 가능성을 시스템적으로 차단합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식을 통해 퀴즈 생성 속도를 늦추지 않으면서 정답의 다양성을 확보할 수 있었습니다.&lt;/p&gt;
&lt;h1&gt;4. 다음 스텝을 향해&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;급한 불은 껐지만, 아직 전체 퀴즈 생성 속도를 더 끌어올려야 하는 과제가 남아있습니다. 텍스트의 유사도를 모두 계산하는 무거운 TextRank 알고리즘을 걷어내고, 문단 단위로 빠르게 핵심을 짚어내는 새로운 추출 로직을 도입할 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽은 것을 온전한 내 지식으로 만드는 경험, Blank.가 계속 만들어가겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://play.google.com/store/apps/details?id=com.shootsir.blank&quot;&gt;Google Play Store에서 Blank. 다운로드&lt;/a&gt;&lt;/p&gt;</description>
      <category>Programming</category>
      <category>gemma3</category>
      <category>LLM</category>
      <category>ondeviceai</category>
      <category>안드로이드개발</category>
      <category>최적화</category>
      <category>토이프로젝트</category>
      <category>프롬프트엔지니어링</category>
      <author>GeonWoo Kim</author>
      <guid isPermaLink="true">https://gwlabs.tistory.com/146</guid>
      <comments>https://gwlabs.tistory.com/146#entry146comment</comments>
      <pubDate>Mon, 23 Feb 2026 21:21:33 +0900</pubDate>
    </item>
    <item>
      <title>MySQL InnoDB: 백엔드 개발자를 위한 필수 가이드</title>
      <link>https://gwlabs.tistory.com/145</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwsrR6/btsQSGDrqlo/7CBeXj9UWkkAKX55UDhJD1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwsrR6/btsQSGDrqlo/7CBeXj9UWkkAKX55UDhJD1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwsrR6/btsQSGDrqlo/7CBeXj9UWkkAKX55UDhJD1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwsrR6%2FbtsQSGDrqlo%2F7CBeXj9UWkkAKX55UDhJD1%2Fimg.jpg&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;MySQL은 전 세계적으로 가장 많이 사용되는 오픈소스 데이터베이스 중 하나이며, 그 중심에는 &lt;strong&gt;InnoDB&lt;/strong&gt; 스토리지 엔진이 있습니다. InnoDB는 단순히 데이터를 저장하는 엔진을 넘어, 성능, 안정성, 확장성 측면에서 MySQL을 지탱하는 핵심 요소입니다. 이번 글에서는 백엔드 개발자라면 반드시 이해해야 할 MySQL InnoDB의 특징과 성능 튜닝 포인트를 정리해 보겠습니다.  &lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;InnoDB의 주요 특징&lt;/h2&gt;
&lt;h3&gt;1. 트랜잭션 지원 (ACID 보장)&lt;/h3&gt;
&lt;p&gt;InnoDB는 &lt;strong&gt;ACID(Atomicity, Consistency, Isolation, Durability)&lt;/strong&gt; 특성을 충실히 지원합니다.&lt;br&gt;이는 금융 서비스나 전자상거래처럼 데이터 무결성이 중요한 시스템에서 필수적인 기능입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 트랜잭션 예제
START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

COMMIT;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 예제에서 두 개의 UPDATE가 모두 성공해야 최종 COMMIT이 이루어집니다. 실패 시 ROLLBACK으로 데이터 무결성이 보장됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. 클러스터형 인덱스 (Clustered Index)&lt;/h3&gt;
&lt;p&gt;InnoDB는 Primary Key를 기준으로 데이터를 물리적으로 정렬하는 클러스터형 인덱스를 사용합니다.&lt;br&gt;따라서 Primary Key를 잘 설계하는 것이 성능에 큰 영향을 줍니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;장점: Primary Key 기반 조회 속도가 매우 빠름&lt;/li&gt;
&lt;li&gt;단점: 잘못된 PK 설계 시 데이터 재배치 비용이 증가&lt;br&gt;```&lt;/li&gt;
&lt;li&gt;&lt;ul&gt;
&lt;li&gt;효율적인 PK 설계 예시&lt;br&gt;CREATE TABLE orders (&lt;br&gt; order_id BIGINT AUTO_INCREMENT PRIMARY KEY,&lt;br&gt; user_id BIGINT NOT NULL,&lt;br&gt; created_at DATETIME NOT NULL,&lt;br&gt; INDEX (user_id),&lt;br&gt; INDEX (created_at)&lt;br&gt;) ENGINE=InnoDB;&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;3. 외래 키(Foreign Key) 지원&lt;/h3&gt;
&lt;p&gt;MyISAM과 달리, InnoDB는 외래 키 제약조건을 지원합니다. 이는 데이터 무결성을 DB 레벨에서 보장할 수 있게 해줍니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 외래 키 제약조건 예시
CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL
) ENGINE=InnoDB;

CREATE TABLE posts (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT,
    content TEXT,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;MySQL 성능 튜닝과 InnoDB&lt;/h2&gt;
&lt;h3&gt;1. InnoDB Buffer Pool 최적화&lt;/h3&gt;
&lt;p&gt;InnoDB의 핵심은 Buffer Pool입니다. 이는 메모리에 데이터와 인덱스를 캐싱하여 디스크 I/O를 줄여줍니다.&lt;br&gt;대규모 트래픽 환경에서는 innodb_buffer_pool_size를 서버 메모리의 60~70% 수준으로 설정하는 것이 일반적입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# my.cnf 설정 예시
[mysqld]
innodb_buffer_pool_size = 8G
innodb_log_file_size = 1G
innodb_flush_log_at_trx_commit = 1&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;2. 트랜잭션 격리 수준 조정&lt;/h3&gt;
&lt;p&gt;InnoDB는 4가지 트랜잭션 격리 수준을 지원합니다.&lt;br&gt;기본값은 REPEATABLE READ이지만, 서비스 특성에 맞게 조정할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 현재 세션에서 격리 수준 변경
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;READ COMMITTED: 대부분의 웹 서비스에 적합, 잠금 경합 줄임&lt;/li&gt;
&lt;li&gt;REPEATABLE READ: 기본값, 팬텀 리드 방지&lt;/li&gt;
&lt;li&gt;SERIALIZABLE: 가장 강력하지만 성능 저하&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;백엔드 개발자가 알아야 할 InnoDB 핵심 포인트&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;PK 설계 중요성: 자동 증가 값이나 짧은 정수형 PK 권장&lt;/li&gt;
&lt;li&gt;Buffer Pool 튜닝: 성능의 70% 이상을 좌우&lt;/li&gt;
&lt;li&gt;트랜잭션 이해: 단순한 쿼리 작성이 아니라, 데이터 무결성을 보장하는 로직 설계 필요&lt;/li&gt;
&lt;li&gt;Deadlock 처리: InnoDB는 자동 Deadlock 감지를 지원하므로, 애플리케이션 레벨에서 재시도 로직을 구현해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# Python 예시: Deadlock 발생 시 재시도 로직
import MySQLdb
import time

def execute_with_retry(cursor, query, params=()):
    for attempt in range(3):
        try:
            cursor.execute(query, params)
            return
        except MySQLdb.OperationalError as e:
            if e.args[0] == 1213:  # Deadlock
                time.sleep(0.5)
            else:
                raise&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h3&gt;결론&lt;/h3&gt;
&lt;p&gt;InnoDB는 단순한 스토리지 엔진이 아니라, MySQL의 성능과 안정성을 좌우하는 핵심 기술입니다.&lt;br&gt;백엔드 개발자는 트랜잭션 관리, PK 설계, Buffer Pool 튜닝 같은 부분을 이해하고 적용해야 실제 서비스 환경에서 안정적이고 빠른 데이터베이스를 운영할 수 있습니다.&lt;/p&gt;
&lt;p&gt;  요약: InnoDB를 제대로 이해하면, MySQL의 잠재력을 최대한 끌어낼 수 있습니다.&lt;/p&gt;</description>
      <category>Infrastructure</category>
      <category>Database</category>
      <category>db성능튜닝</category>
      <category>DB튜닝</category>
      <category>innodb</category>
      <category>mysql</category>
      <category>데이터베이스</category>
      <category>성능개선</category>
      <author>GeonWoo Kim</author>
      <guid isPermaLink="true">https://gwlabs.tistory.com/145</guid>
      <comments>https://gwlabs.tistory.com/145#entry145comment</comments>
      <pubDate>Mon, 29 Sep 2025 08:14:16 +0900</pubDate>
    </item>
    <item>
      <title>AWS DynamoDB Deep Dive: Key-Value DB와 NoSQL 모델링 완벽 이해</title>
      <link>https://gwlabs.tistory.com/144</link>
      <description>&lt;h1&gt;AWS DynamoDB Deep Dive: Key-Value DB와 NoSQL 모델링 완벽 이해&lt;/h1&gt;
&lt;h2&gt;서론&lt;/h2&gt;
&lt;p&gt;AWS DynamoDB는 완전 관리형(fully managed) NoSQL 데이터베이스 서비스로, &lt;strong&gt;Key-Value 및 Document 모델을 지원&lt;/strong&gt;하면서도 초당 수백만 건의 요청을 처리할 수 있는 확장성을 제공합니다.&lt;br&gt;많은 기업이 DynamoDB를 선택하는 이유는 서버 관리 부담을 줄이고, 고성능 애플리케이션을 안정적으로 운영할 수 있기 때문입니다. 이번 글에서는 &lt;strong&gt;DynamoDB의 특징, 기본 사용법, 데이터 모델링 전략, 그리고 RDBMS와의 차이점&lt;/strong&gt;을 심도 있게 다루겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;본론&lt;/h2&gt;
&lt;h3&gt;1. DynamoDB의 주요 특징&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;완전 관리형&lt;/strong&gt;: 서버 인프라, 스케일링, 보안, 백업을 AWS가 관리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key-Value + Document 저장 모델&lt;/strong&gt;: 단순 키-값 조회부터 JSON 기반 복잡한 구조까지 지원&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자동 확장성&lt;/strong&gt;: 온디맨드(On-Demand) 또는 프로비저닝(Provisioned) 용량 모드 선택 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;고가용성&lt;/strong&gt;: 다중 AZ(Availability Zone)에 자동 데이터 복제&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;서버리스 통합&lt;/strong&gt;: AWS Lambda, API Gateway와 쉽게 연동되어 실시간 애플리케이션 구축에 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;2. DynamoDB 기본 사용법&lt;/h3&gt;
&lt;p&gt;DynamoDB는 &lt;strong&gt;테이블(table)&lt;/strong&gt;, &lt;strong&gt;아이템(item)&lt;/strong&gt;, &lt;strong&gt;속성(attribute)&lt;/strong&gt; 구조를 기반으로 동작합니다.&lt;br&gt;기본 키(primary key)는 두 가지 방식으로 정의할 수 있습니다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;단일 파티션 키 (Partition Key Only)&lt;/strong&gt;&lt;br&gt;→ 단순 Key-Value 조회용&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;복합 키 (Partition Key + Sort Key)&lt;/strong&gt;&lt;br&gt;→ 동일 파티션 키 내에서 정렬된 데이터 관리 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;예제: 테이블 생성 (AWS CLI)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aws dynamodb create-table \
    --table-name Orders \
    --attribute-definitions \
        AttributeName=OrderId,AttributeType=S \
        AttributeName=CreatedAt,AttributeType=N \
    --key-schema \
        AttributeName=OrderId,KeyType=HASH \
        AttributeName=CreatedAt,KeyType=RANGE \
    --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;예제: 데이터 삽입 (Python Boto3)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import boto3
from datetime import datetime

dynamodb = boto3.resource(&amp;#39;dynamodb&amp;#39;)
table = dynamodb.Table(&amp;#39;Orders&amp;#39;)

response = table.put_item(
   Item={
        &amp;#39;OrderId&amp;#39;: &amp;#39;ORD1234&amp;#39;,
        &amp;#39;CreatedAt&amp;#39;: int(datetime.now().timestamp()),
        &amp;#39;CustomerName&amp;#39;: &amp;#39;홍길동&amp;#39;,
        &amp;#39;TotalAmount&amp;#39;: 35000
    }
)
print(&amp;quot;PutItem succeeded:&amp;quot;, response)&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;예제: 데이터 조회 (Python Boto3)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;response = table.get_item(
    Key={
        &amp;#39;OrderId&amp;#39;: &amp;#39;ORD1234&amp;#39;,
        &amp;#39;CreatedAt&amp;#39;: 1694001123
    }
)
print(response[&amp;#39;Item&amp;#39;])&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h3&gt;3. DynamoDB 데이터 모델링 전략&lt;/h3&gt;
&lt;p&gt;DynamoDB는 RDBMS와 달리 정규화보다 액세스 패턴 중심 설계가 핵심입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;단일 테이블 설계 (Single-Table Design)&lt;ul&gt;
&lt;li&gt;여러 엔터티를 하나의 테이블에 저장하고, 파티션 키/정렬 키 조합으로 액세스 패턴을 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Secondary Index 활용&lt;ul&gt;
&lt;li&gt;GSI(Global Secondary Index): 파티션 키 및 정렬 키를 새롭게 정의해 보조 조회 지원&lt;/li&gt;
&lt;li&gt;LSI(Local Secondary Index): 동일 파티션 키 내에서 다른 정렬 키로 조회 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;핫 파티션 방지&lt;ul&gt;
&lt;li&gt;키 분포가 특정 값에 치우치지 않도록 설계 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;예제: GSI 생성 (AWS CLI)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;aws dynamodb update-table \
    --table-name Orders \
    --attribute-definitions AttributeName=CustomerName,AttributeType=S \
    --global-secondary-index-updates \
        &amp;quot;[{\&amp;quot;Create\&amp;quot;:{\&amp;quot;IndexName\&amp;quot;:\&amp;quot;CustomerNameIndex\&amp;quot;,\&amp;quot;KeySchema\&amp;quot;:[{\&amp;quot;AttributeName\&amp;quot;:\&amp;quot;CustomerName\&amp;quot;,\&amp;quot;KeyType\&amp;quot;:\&amp;quot;HASH\&amp;quot;}],\&amp;quot;Projection\&amp;quot;:{\&amp;quot;ProjectionType\&amp;quot;:\&amp;quot;ALL\&amp;quot;},\&amp;quot;ProvisionedThroughput\&amp;quot;:{\&amp;quot;ReadCapacityUnits\&amp;quot;:5,\&amp;quot;WriteCapacityUnits\&amp;quot;:5}}}]&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h3&gt;4. DynamoDB와 RDBMS의 차이점&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;DynamoDB&lt;/th&gt;
&lt;th&gt;RDBMS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;데이터 모델&lt;/td&gt;
&lt;td&gt;Key-Value, Document&lt;/td&gt;
&lt;td&gt;테이블, 행(Row), 열(Column)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스키마&lt;/td&gt;
&lt;td&gt;스키마리스 (유연)&lt;/td&gt;
&lt;td&gt;엄격한 스키마&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;확장성&lt;/td&gt;
&lt;td&gt;수평적 확장 (샤딩 자동화)&lt;/td&gt;
&lt;td&gt;수직적 확장 위주&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;트랜잭션&lt;/td&gt;
&lt;td&gt;제한적 지원 (ACID 일부)&lt;/td&gt;
&lt;td&gt;완전한 ACID 보장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설계 방식&lt;/td&gt;
&lt;td&gt;액세스 패턴 중심&lt;/td&gt;
&lt;td&gt;정규화 중심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용 사례&lt;/td&gt;
&lt;td&gt;IoT, 로그 처리, 세션 관리&lt;/td&gt;
&lt;td&gt;ERP, 회계, 관계형 데이터 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;이번 포스팅에서는 AWS DynamoDB의 특징, 기본 사용법, 데이터 모델링 전략, 그리고 RDBMS와의 차이점을 살펴보았습니다.&lt;br&gt;핵심은 DynamoDB는 스키마리스 구조와 수평 확장성 덕분에 고성능 분산 애플리케이션에 최적화되어 있으며, 전통적인 RDBMS와는 완전히 다른 설계 사고방식이 필요하다는 점입니다.&lt;/p&gt;
&lt;p&gt;  이 글을 통해 독자분들은 DynamoDB를 단순한 NoSQL 서비스가 아닌, 실무에 최적화된 강력한 데이터베이스로 이해할 수 있습니다.&lt;/p&gt;</description>
      <category>Infrastructure</category>
      <category>AWS DynamoDB</category>
      <category>dynamoDB</category>
      <category>key-value</category>
      <category>NoSQL</category>
      <category>다이나모DB</category>
      <author>GeonWoo Kim</author>
      <guid isPermaLink="true">https://gwlabs.tistory.com/144</guid>
      <comments>https://gwlabs.tistory.com/144#entry144comment</comments>
      <pubDate>Sun, 7 Sep 2025 09:05:02 +0900</pubDate>
    </item>
    <item>
      <title>Gemma3 270M으로 배우는 sLLM 파인튜닝과 Edge Device 서빙 전략</title>
      <link>https://gwlabs.tistory.com/143</link>
      <description>&lt;h2&gt;서론&lt;/h2&gt;
&lt;p&gt;최근 LLM(Large Language Model)의 경량화 버전인 &lt;strong&gt;sLLM(Small Language Model)&lt;/strong&gt; 이 빠르게 주목받고 있습니다. 거대한 모델을 직접 서빙하기 어려운 환경에서는 sLLM과 같은 소형 모델을 적절히 파인튜닝해 활용하는 것이 매우 유효한 전략입니다. 본 포스팅에서는 &lt;strong&gt;Gemma3 270M&lt;/strong&gt; 모델을 활용하여 &lt;strong&gt;LoRA 기반 파인튜닝&lt;/strong&gt;을 진행하고, 학습된 PyTorch 모델을 &lt;strong&gt;TensorFlow Lite 변환&lt;/strong&gt; 후 &lt;strong&gt;Mediapipe 기반 Edge Device 서빙&lt;/strong&gt;까지 이어지는 전체 워크플로우를 정리합니다.  &lt;/p&gt;
&lt;p&gt;이 글은 실무 환경에서 sLLM을 파인튜닝하거나 모바일·엣지 디바이스에 배포하려는 개발자에게 최적화된 가이드를 제공합니다.  &lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;본론&lt;/h2&gt;
&lt;h3&gt;1. Gemma3 270M과 sLLM 파인튜닝&lt;/h3&gt;
&lt;p&gt;Gemma3 270M은 Google이 공개한 소형 언어 모델로, &lt;strong&gt;경량·저비용·실시간 응답성&lt;/strong&gt;이라는 특징을 가집니다. 하지만 특정 도메인 데이터에 맞게 활용하려면 &lt;strong&gt;파인튜닝&lt;/strong&gt;이 필수적입니다.  &lt;/p&gt;
&lt;p&gt;가장 널리 쓰이는 접근은 &lt;strong&gt;LoRA(Low-Rank Adaptation)&lt;/strong&gt; 를 적용하는 방식입니다. LoRA는 전체 파라미터를 학습하지 않고 일부 저차원 행렬만 업데이트하기 때문에, 적은 리소스로도 높은 성능 향상을 기대할 수 있습니다.  &lt;/p&gt;
&lt;h4&gt;LoRA Config 예시&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from peft import LoraConfig

lora_config = LoraConfig(
    r=8,                      # 랭크
    lora_alpha=16,            # LoRA scaling
    target_modules=[&amp;quot;q_proj&amp;quot;, &amp;quot;v_proj&amp;quot;],  # 적용할 레이어
    lora_dropout=0.05,        # 드롭아웃
    bias=&amp;quot;none&amp;quot;,
    task_type=&amp;quot;CAUSAL_LM&amp;quot;
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 설정은 Gemma3 270M 같은 소형 모델에서 적절한 학습/메모리 균형을 제공합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;파인튜닝 예제&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer

model_name = &amp;quot;google/gemma-3-270m&amp;quot;
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)

# 데이터 준비
train_dataset = ...

# 학습 설정
training_args = TrainingArguments(
    output_dir=&amp;quot;./outputs&amp;quot;,
    per_device_train_batch_size=16,
    gradient_accumulation_steps=2,
    learning_rate=2e-4,
    num_train_epochs=3,
    fp16=True,
    logging_steps=50,
    save_steps=500,
    save_total_limit=2,
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
)

trainer.train()
&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h3&gt;2. Torch 모델을 TensorFlow Lite 변환&lt;/h3&gt;
&lt;p&gt;학습이 완료된 모델을 &lt;strong&gt;Edge Device&lt;/strong&gt;에 배포하기 위해서는 &lt;strong&gt;TFLite&lt;/strong&gt; 변환이 필요합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;변환 절차&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PyTorch → ONNX&lt;/li&gt;
&lt;li&gt;ONNX → TensorFlow SavedModel&lt;/li&gt;
&lt;li&gt;SavedModel → TFLite&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;코드 예제&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch

# 1. Torch -&amp;gt; ONNX
dummy_input = torch.randint(0, 100, (1, 128))  # 시퀀스 입력
torch.onnx.export(model, dummy_input, &amp;quot;gemma3.onnx&amp;quot;, input_names=[&amp;quot;input_ids&amp;quot;], output_names=[&amp;quot;logits&amp;quot;])

# 2. ONNX -&amp;gt; TensorFlow (onnx_tf 활용)
!onnx-tf convert -i gemma3.onnx -o ./gemma3_tf

# 3. TensorFlow -&amp;gt; TFLite
import tensorflow as tf

converter = tf.lite.TFLiteConverter.from_saved_model(&amp;quot;./gemma3_tf&amp;quot;)
converter.optimizations = [tf.lite.Optimize.DEFAULT]  # 양자화 가능
tflite_model = converter.convert()

with open(&amp;quot;gemma3_270m.tflite&amp;quot;, &amp;quot;wb&amp;quot;) as f:
    f.write(tflite_model)
&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h3&gt;3. Mediapipe 기반 Edge Device 서빙&lt;/h3&gt;
&lt;p&gt;변환된 .tflite 모델은 Mediapipe Tasks API를 활용하면 모바일 환경에서 효율적으로 서빙할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Python 예제&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import mediapipe as mp

BaseOptions = mp.tasks.BaseOptions
TextEmbedder = mp.tasks.text.TextEmbedder
TextEmbedderOptions = mp.tasks.text.TextEmbedderOptions

options = TextEmbedderOptions(
    base_options=BaseOptions(model_asset_path=&amp;quot;gemma3_270m.tflite&amp;quot;)
)

with TextEmbedder.create_from_options(options) as embedder:
    result = embedder.embed(&amp;quot;sLLM 활용 예제입니다.&amp;quot;)
    print(result)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Mediapipe는 GPU 가속, 멀티 플랫폼(Android, iOS, Web) 지원이 가능하기 때문에 실제 서비스에서 경량 AI 모델을 배포할 때 강력한 선택지가 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;이번 포스팅에서는 Gemma3 270M sLLM을 LoRA로 파인튜닝하고, 학습된 모델을 TFLite 변환 후 Mediapipe로 서빙하는 전체 과정을 살펴보았습니다.&lt;/p&gt;
&lt;p&gt;핵심은 작은 모델도 적절한 파인튜닝과 최적화 파이프라인을 거치면, 모바일과 Edge 환경에서도 충분히 실용적인 AI 서비스를 구현할 수 있다는 점입니다.&lt;/p&gt;
&lt;p&gt;  이 과정을 따라가면 저비용·고효율의 맞춤형 sLLM 서비스를 누구나 구축할 수 있습니다.&lt;/p&gt;</description>
      <category>MachineLearning</category>
      <category>fintuning</category>
      <category>gemma3</category>
      <category>LLM</category>
      <category>Lora</category>
      <category>Mediapipe</category>
      <category>sLLM</category>
      <category>TensorFlow Lite</category>
      <category>torch</category>
      <category>개발</category>
      <category>파인튜닝</category>
      <author>GeonWoo Kim</author>
      <guid isPermaLink="true">https://gwlabs.tistory.com/143</guid>
      <comments>https://gwlabs.tistory.com/143#entry143comment</comments>
      <pubDate>Tue, 2 Sep 2025 08:53:37 +0900</pubDate>
    </item>
    <item>
      <title>개인 프로젝트 WordADay 출시 후기: 바이브 코딩과 Flutter로 만든 앱, 그리고 PlayStore 등록까지</title>
      <link>https://gwlabs.tistory.com/142</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t6Xj2/btsP87PbAG6/CmW8U4vXxTCDG2nmPDNJh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t6Xj2/btsP87PbAG6/CmW8U4vXxTCDG2nmPDNJh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t6Xj2/btsP87PbAG6/CmW8U4vXxTCDG2nmPDNJh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft6Xj2%2FbtsP87PbAG6%2FCmW8U4vXxTCDG2nmPDNJh0%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; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자로서 개인 프로젝트를 완성해 실제로 &lt;b&gt;Google PlayStore&lt;/b&gt;에 출시하는 경험은 값진 자산이 됩니다. 이번 포스팅에서는 제가 진행한 &lt;b&gt;WordADay&lt;/b&gt; 프로젝트를 소개하고, 개발 과정에서 활용한 &lt;b&gt;바이브 코딩(Vibe Coding)&lt;/b&gt; 방식과 &lt;b&gt;Flutter UI 구현&lt;/b&gt;, 마지막으로 &lt;b&gt;Google PlayStore 등록 시 유의할 점&lt;/b&gt;을 공유하고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  WordADay 앱 바로가기: &lt;a href=&quot;https://play.google.com/store/apps/details?id=com.gwlabs.wordaday&quot;&gt;Google PlayStore 다운로드&lt;/a&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WordADay 프로젝트와 바이브 코딩 접근법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개인 프로젝트 동기와 목표&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WordADay는 매일 새로운 영어 단어를 제공하는 간단한 학습 앱입니다.&lt;br /&gt;개인적으로 &amp;ldquo;매일 반복적으로 학습할 수 있는 작은 습관 앱&amp;rdquo;을 만들어 보고 싶었고, Flutter를 기반으로 빠르게 MVP를 개발했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 저는 &lt;b&gt;바이브 코딩(Vibe Coding)&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;정해진 설계 문서를 따르기보다는, &amp;ldquo;느낌과 직관&amp;rdquo;을 우선시하여 UI와 기능을 즉시 구현&lt;/li&gt;
&lt;li&gt;즉각적인 실행과 피드백을 통해 사용자 경험을 빠르게 조정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 특히 &lt;b&gt;사이드 프로젝트&lt;/b&gt;에서 동기부여를 유지하고, 빠른 출시로 이어지는 데 유효했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Flutter UI: 바이브 코딩으로 빠르게 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter의 가장 큰 장점은 &lt;b&gt;핫 리로드(Hot Reload)&lt;/b&gt; 기능을 통한 빠른 UI 반복입니다.&lt;br /&gt;WordADay UI는 최소한의 코드로 심플하게 구성했으며, 핵심은 &amp;ldquo;단어 표시 &amp;rarr; 뜻 &amp;rarr; 추가 학습 버튼&amp;rdquo;의 단순한 흐름입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, WordADay의 기본 카드 UI는 아래와 같은 방식으로 구현했습니다. 실제 UI 코드는 아니나, 아래와 같은 작은 UI 코드들은 지속적으로 AI로 만들면서 개선했습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;import 'package:flutter/material.dart';

class WordCard extends StatelessWidget {
  final String word;
  final String meaning;

  const WordCard({required this.word, required this.meaning, super.key});

  @override
  Widget build(BuildContext context) {
    return Card(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      elevation: 4,
      margin: const EdgeInsets.all(16),
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(word,
                style: Theme.of(context).textTheme.headlineMedium),
            const SizedBox(height: 12),
            Text(meaning,
                style: Theme.of(context).textTheme.bodyLarge),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                // TODO: 추가 학습 페이지 연결
              },
              child: const Text(&quot;Learn More&quot;),
            ),
          ],
        ),
      ),
    );
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 작은 단위 UI 위젯을 바이브 코딩으로 빠르게 만들어 나가면, 전체 앱의 뼈대를 짧은 시간 안에 완성할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Google PlayStore 등록 시 유의할 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WordADay를 배포하면서, PlayStore 등록 과정에서 다음과 같은 포인트를 반드시 체크해야 했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;앱 서명(App Signing)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Google Play App Signing을 활성화하면 배포 및 업데이트 관리가 훨씬 안정적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;앱 콘텐츠 정책 준수&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;학습 앱이라 하더라도 개인정보 처리방침(Privacy Policy) URL은 필수&lt;/li&gt;
&lt;li&gt;아동 보호, 광고 정책 관련 체크리스트를 반드시 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;앱 설명과 SEO 최적화&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설명 문구에 핵심 키워드 삽입: 영어 단어, 학습, WordADay, 개인 프로젝트, Flutter&lt;/li&gt;
&lt;li&gt;스크린샷은 실제 사용성을 보여주는 형태로 준비&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;4&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;테스트 및 검증&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부 테스트 트랙 &amp;rarr; 클로즈드 테스트 &amp;rarr; 프로덕션 순으로 진행&lt;/li&gt;
&lt;li&gt;실제 기기에서 UI/UX 흐름을 검증한 뒤 출시&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론: 개인 프로젝트에서 출시까지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 WordADay 앱은 바이브 코딩으로 직관적인 Flutter UI 개발 &amp;rarr; 빠른 MVP 완성 &amp;rarr; Google PlayStore 배포라는 과정을 거쳐 탄생했습니다.&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;Flutter UI는 빠르게 시각적 결과물을 얻는 데 최적화되어 있습니다.&lt;/li&gt;
&lt;li&gt;PlayStore 등록은 개발뿐 아니라 정책과 마케팅 측면까지 고려해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  독자 여러분도 작은 개인 프로젝트를 실제로 출시해보는 경험을 통해 개발자로서의 성장을 체감할 수 있습니다.&lt;/p&gt;</description>
      <category>Programming</category>
      <category>Flutter</category>
      <category>flutter UI</category>
      <category>wordaday</category>
      <category>구글플레이스토더</category>
      <category>바이브코딩</category>
      <category>사이드프로젝트</category>
      <author>GeonWoo Kim</author>
      <guid isPermaLink="true">https://gwlabs.tistory.com/142</guid>
      <comments>https://gwlabs.tistory.com/142#entry142comment</comments>
      <pubDate>Wed, 27 Aug 2025 20:47:25 +0900</pubDate>
    </item>
    <item>
      <title>XGBoost 회귀 완전 정복: RMSE, MAE부터 Tweedie와 Gamma까지 최적 Objective 선택법</title>
      <link>https://gwlabs.tistory.com/141</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clk7Qy/btsP2HYfI5I/PjqhxEmsJ06Vr95jBToduK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clk7Qy/btsP2HYfI5I/PjqhxEmsJ06Vr95jBToduK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clk7Qy/btsP2HYfI5I/PjqhxEmsJ06Vr95jBToduK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fclk7Qy%2FbtsP2HYfI5I%2FPjqhxEmsJ06Vr95jBToduK%2Fimg.png&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;서론&lt;/h2&gt;
&lt;p&gt;XGBoost는 분류 문제뿐 아니라 회귀 문제에서도 강력한 성능을 발휘하는 알고리즘입니다. 특히 비선형 데이터, 결측치 처리, 대용량 데이터셋에 효과적이라는 장점이 있습니다. 그러나 회귀 문제에서는 단순히 &lt;code&gt;reg:squarederror&lt;/code&gt; 같은 기본 Objective만 사용하는 경우가 많습니다. 실제로 데이터의 분포와 목적에 맞춰 Objective를 선택하고, 적절한 하이퍼파라미터를 조정하면 모델의 성능을 극대화할 수 있습니다.  &lt;/p&gt;
&lt;p&gt;이번 글에서는 &lt;strong&gt;XGBoost 회귀(Objective function) 활용법&lt;/strong&gt;, &lt;strong&gt;RMSE와 MAE의 차이&lt;/strong&gt;, &lt;strong&gt;Tweedie, Gamma 분포 회귀 적용법&lt;/strong&gt;, 그리고 &lt;strong&gt;주요 하이퍼파라미터 튜닝 방법&lt;/strong&gt;을 정리하겠습니다.  &lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;본론&lt;/h2&gt;
&lt;h3&gt;1. XGBoost 회귀 Objective와 평가 지표 선택&lt;/h3&gt;
&lt;p&gt;XGBoost는 다양한 회귀용 Objective를 제공합니다. 데이터 분포와 목적에 맞게 선택해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;reg:squarederror&lt;/code&gt;&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기본값. 평균제곱오차(MSE)를 최소화.&lt;/li&gt;
&lt;li&gt;RMSE(Root Mean Squared Error) 지표와 궁합이 좋음.&lt;/li&gt;
&lt;li&gt;예측값이 &lt;strong&gt;정규분포적 특성을 가질 때&lt;/strong&gt; 적합.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;reg:squaredlogerror&lt;/code&gt;&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;로그 스케일 오차 최소화.&lt;/li&gt;
&lt;li&gt;target 값이 &lt;strong&gt;지수적 성장 형태&lt;/strong&gt;(예: 매출액, 가격)일 때 유용.&lt;/li&gt;
&lt;li&gt;예측값이 음수가 되지 않는다는 점을 고려해야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;reg:gamma&lt;/code&gt;&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;양수 실수값&lt;/strong&gt;(보험료, 대기 시간, 수익 등)에 적합.&lt;/li&gt;
&lt;li&gt;분포가 &lt;strong&gt;오른쪽 꼬리가 긴 형태(skewed distribution)&lt;/strong&gt;일 때 추천.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;reg:tweedie&lt;/code&gt;&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tweedie 분포 기반 회귀.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0 값이 다수 존재하면서 동시에 양수 연속값도 있는 경우&lt;/strong&gt; 유용 (예: 보험 청구 건수, 날씨 데이터 강수량).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tweedie_variance_power&lt;/code&gt; 하이퍼파라미터로 분포 형태 제어 가능:&lt;ul&gt;
&lt;li&gt;1 &amp;lt; power &amp;lt; 2 → &lt;strong&gt;포아송+감마 혼합&lt;/strong&gt;  &lt;/li&gt;
&lt;li&gt;power = 1 → 포아송 근사  &lt;/li&gt;
&lt;li&gt;power = 2 → 감마 근사  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;평가 지표 선택&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RMSE (Root Mean Squared Error)&lt;/strong&gt;  &lt;ul&gt;
&lt;li&gt;큰 오차에 민감 → 이상치 영향 반영 필요할 때 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MAE (Mean Absolute Error)&lt;/strong&gt;  &lt;ul&gt;
&lt;li&gt;이상치에 둔감 → 예측값 분포가 고르게 퍼져 있고 극단값이 문제되지 않을 때 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Poisson Deviance / Tweedie Deviance&lt;/strong&gt;  &lt;ul&gt;
&lt;li&gt;&lt;code&gt;reg:tweedie&lt;/code&gt;, &lt;code&gt;reg:gamma&lt;/code&gt; 모델에서 활용 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;2. 회귀 학습에서 중요한 하이퍼파라미터&lt;/h3&gt;
&lt;p&gt;회귀 문제에서 성능을 좌우하는 주요 하이퍼파라미터는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;학습률(&lt;code&gt;eta&lt;/code&gt;)&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기본값: 0.3  &lt;/li&gt;
&lt;li&gt;너무 크면 과적합, 너무 작으면 학습이 느려짐. 일반적으로 0.01~0.1 권장.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;트리 깊이(&lt;code&gt;max_depth&lt;/code&gt;)&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모델 복잡도를 조절.  &lt;/li&gt;
&lt;li&gt;깊을수록 비선형 패턴을 잘 잡지만 과적합 위험 증가.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;정규화 파라미터(&lt;code&gt;lambda&lt;/code&gt;, &lt;code&gt;alpha&lt;/code&gt;)&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;L2(&lt;code&gt;lambda&lt;/code&gt;), L1(&lt;code&gt;alpha&lt;/code&gt;) 정규화 적용.  &lt;/li&gt;
&lt;li&gt;다중공선성이나 잡음을 줄이는 데 효과적.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;max_delta_step&lt;/code&gt;&lt;/strong&gt; (특히 &lt;code&gt;reg:logistic&lt;/code&gt;, &lt;code&gt;reg:gamma&lt;/code&gt;, &lt;code&gt;reg:tweedie&lt;/code&gt;에서 중요)  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모델의 수렴 안정성을 높이는 역할.  &lt;/li&gt;
&lt;li&gt;포아송/감마 계열 회귀에서는 &lt;code&gt;1~10&lt;/code&gt; 범위에서 조정.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;tweedie_variance_power&lt;/code&gt;&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reg:tweedie&lt;/code&gt; 전용.  &lt;/li&gt;
&lt;li&gt;1.1~1.9 범위에서 탐색 → 데이터 분포에 맞게 최적화.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;3. 예제 코드: 다양한 회귀 Objective 적용&lt;/h3&gt;
&lt;p&gt;아래는 사이킷런 API 기반의 XGBoost 회귀 예제입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import xgboost as xgb
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, mean_absolute_error

# 예시 데이터 생성
X, y = make_regression(n_samples=5000, n_features=20, noise=0.3, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# XGBoost 회귀 모델 (Tweedie objective 적용)
params = {
    &amp;quot;objective&amp;quot;: &amp;quot;reg:tweedie&amp;quot;,
    &amp;quot;tweedie_variance_power&amp;quot;: 1.5,
    &amp;quot;learning_rate&amp;quot;: 0.05,
    &amp;quot;max_depth&amp;quot;: 6,
    &amp;quot;n_estimators&amp;quot;: 300,
    &amp;quot;eval_metric&amp;quot;: &amp;quot;mae&amp;quot;
}

model = xgb.XGBRegressor(**params)
model.fit(X_train, y_train)

# 예측
y_pred = model.predict(X_test)

# 평가
rmse = mean_squared_error(y_test, y_pred, squared=False)
mae = mean_absolute_error(y_test, y_pred)

print(f&amp;quot;RMSE: {rmse:.4f}, MAE: {mae:.4f}&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;XGBoost 회귀에서 성능을 극대화하려면 데이터의 분포와 특성에 맞는 &lt;strong&gt;Objective&lt;/strong&gt;를 선택하는 것이 중요합니다. RMSE와 MAE를 적절히 비교하며 평가 지표를 선택하고, reg:gamma, reg:tweedie 같은 특수 목적 Objective를 활용하면 일반적인 reg:squarederror보다 훨씬 나은 성능을 얻을 수 있습니다.&lt;/p&gt;
&lt;p&gt;  정리하면, XGBoost 회귀의 핵심은 올바른 Objective 선택과 하이퍼파라미터 최적화에 있다는 점입니다.&lt;/p&gt;</description>
      <category>MachineLearning</category>
      <category>Gamma</category>
      <category>Machine Learning</category>
      <category>Mae</category>
      <category>regression</category>
      <category>RMSE</category>
      <category>Tweedie</category>
      <category>xgboost</category>
      <category>머신러닝</category>
      <category>하이퍼파라미터</category>
      <category>회귀</category>
      <author>GeonWoo Kim</author>
      <guid isPermaLink="true">https://gwlabs.tistory.com/141</guid>
      <comments>https://gwlabs.tistory.com/141#entry141comment</comments>
      <pubDate>Mon, 25 Aug 2025 10:00:34 +0900</pubDate>
    </item>
    <item>
      <title>파이썬 병렬처리: 멀티프로세싱으로 CPU 코어를 100% 활용하는 방법</title>
      <link>https://gwlabs.tistory.com/140</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tZN7o/btsPZquvrX6/KnkzRK8ppSaSvFOvfx4bmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tZN7o/btsPZquvrX6/KnkzRK8ppSaSvFOvfx4bmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tZN7o/btsPZquvrX6/KnkzRK8ppSaSvFOvfx4bmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtZN7o%2FbtsPZquvrX6%2FKnkzRK8ppSaSvFOvfx4bmk%2Fimg.png&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;서론: 파이썬, 느리다는 편견을 깨다&lt;/h2&gt;
&lt;p&gt;파이썬은 쉽고 빠르게 코드를 작성할 수 있어 많은 사랑을 받고 있지만, &amp;quot;느리다&amp;quot;는 오명을 종종 듣곤 합니다. 이는 파이썬의 &lt;strong&gt;GIL(Global Interpreter Lock)&lt;/strong&gt; 때문인데요. GIL은 한 번에 하나의 스레드만 파이썬 바이트코드를 실행할 수 있도록 허용하는 메커니즘으로, 멀티코어 환경에서 스레드를 여러 개 사용해도 실질적인 병렬 처리가 어렵게 만듭니다.&lt;/p&gt;
&lt;p&gt;하지만 데이터 처리, 웹 스크래핑, 대규모 연산과 같은 작업에서는 이러한 한계가 병목 현상을 유발합니다. 그래서 우리는 이 병목 현상을 극복하고 파이썬의 성능을 극대화할 수 있는 &lt;strong&gt;병렬처리 기법&lt;/strong&gt;에 대해 알아볼 필요가 있습니다. 이번 포스팅에서는 파이썬에서 병렬 처리를 구현하는 대표적인 방법인 &lt;strong&gt;멀티프로세싱(Multiprocessing)&lt;/strong&gt;을 중심으로, &lt;strong&gt;멀티쓰레딩(Multithreading)&lt;/strong&gt;과 비교하고, 나아가 데이터 처리의 필수 라이브러리인 &lt;strong&gt;판다스(Pandas)&lt;/strong&gt;를 병렬 처리하는 실용적인 방법까지 심도 있게 다뤄보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;본론: 파이썬 병렬처리, 제대로 이해하기&lt;/h2&gt;
&lt;h3&gt;1. 멀티쓰레딩 vs. 멀티프로세싱: GIL의 한계를 넘어서&lt;/h3&gt;
&lt;p&gt;파이썬에서 동시성을 다루는 대표적인 두 가지 방법은 &lt;strong&gt;멀티쓰레딩&lt;/strong&gt;과 &lt;strong&gt;멀티프로세싱&lt;/strong&gt;입니다. 이 둘의 가장 큰 차이점은 GIL의 적용 여부입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;멀티쓰레딩 (Multithreading):&lt;/strong&gt; 하나의 프로세스 내에서 여러 스레드를 생성하여 작업을 분할합니다. 스레드는 메모리를 공유하기 때문에 데이터 접근이 용이하지만, GIL 때문에 CPU를 많이 사용하는 연산 작업(CPU-bound tasks)에서는 진정한 병렬 처리가 불가능합니다. 주로 네트워크 요청이나 파일 입출력과 같은 I/O-bound 작업에 적합합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;멀티프로세싱 (Multiprocessing):&lt;/strong&gt; 여러 개의 독립적인 프로세스를 생성하여 작업을 수행합니다. 각 프로세스는 별도의 메모리 공간을 가지므로 GIL의 영향을 받지 않고, 여러 CPU 코어를 동시에 활용할 수 있습니다. 따라서 대규모 연산이나 복잡한 계산 등 CPU-bound 작업에 매우 효과적입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;간단한 예제 코드를 통해 두 기법의 차이를 직관적으로 살펴보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time
import multiprocessing
import threading

def cpu_bound_task():
    # 간단한 CPU-bound 작업 (루프를 돌며 계산)
    count = 0
    for _ in range(10**8):
        count += 1

def run_with_multiprocessing():
    start = time.time()
    processes = []
    for _ in range(4): # 4개의 프로세스 생성
        p = multiprocessing.Process(target=cpu_bound_task)
        processes.append(p)
        p.start()

    for p in processes:
        p.join() # 프로세스가 종료될 때까지 기다림
    print(f&amp;quot;멀티프로세싱 소요 시간: {time.time() - start:.2f}초&amp;quot;)

def run_with_threading():
    start = time.time()
    threads = []
    for _ in range(4): # 4개의 스레드 생성
        t = threading.Thread(target=cpu_bound_task)
        threads.append(t)
        t.start()

    for t in threads:
        t.join() # 스레드가 종료될 때까지 기다림
    print(f&amp;quot;멀티쓰레딩 소요 시간: {time.time() - start:.2f}초&amp;quot;)

if __name__ == &amp;#39;__main__&amp;#39;:
    run_with_multiprocessing()
    run_with_threading()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 코드를 실행하면 멀티프로세싱이 멀티쓰레딩보다 훨씬 빠르게 작업을 완료하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;2. 판다스(Pandas) 병렬처리: 대용량 데이터프레임 가공하기&lt;/h3&gt;
&lt;p&gt;데이터 분석가와 개발자에게 &lt;strong&gt;판다스&lt;/strong&gt;는 필수적인 라이브러리입니다. 하지만 &lt;code&gt;apply()&lt;/code&gt;나 &lt;code&gt;map()&lt;/code&gt; 함수로 대용량 데이터프레임을 처리할 때, 단일 코어만 사용해 시간이 오래 걸리는 경우가 많습니다. 이때 &lt;strong&gt;&lt;code&gt;pandarallel&lt;/code&gt;&lt;/strong&gt; 라이브러리를 사용하면 판다스의 연산을 간단하게 병렬 처리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;먼저 &lt;code&gt;pandarallel&lt;/code&gt;을 설치합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install pandarallel&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;사용법은 매우 간단합니다. 초기화 후 기존 &lt;code&gt;apply&lt;/code&gt; 함수 자리에 &lt;code&gt;.parallel_apply()&lt;/code&gt;를 사용하기만 하면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import pandas as pd
from pandarallel import pandarallel

# pandarallel 초기화
pandarallel.initialize(nb_workers=4) # 사용할 코어 수 지정

# 예제 데이터프레임 생성
df_size = 10**6
df = pd.DataFrame({
    &amp;#39;a&amp;#39;: range(df_size),
    &amp;#39;b&amp;#39;: range(df_size)
})

# 복잡한 연산 함수 정의
def complex_operation(row):
    return row[&amp;#39;a&amp;#39;] * row[&amp;#39;b&amp;#39;] + 100

# 병렬 처리로 데이터프레임 연산
start_parallel = time.time()
df[&amp;#39;result_parallel&amp;#39;] = df.parallel_apply(complex_operation, axis=1)
print(f&amp;quot;Pandarallel 병렬 처리 소요 시간: {time.time() - start_parallel:.2f}초&amp;quot;)

# 일반적인 판다스 apply 연산
start_serial = time.time()
df[&amp;#39;result_serial&amp;#39;] = df.apply(complex_operation, axis=1)
print(f&amp;quot;Pandas 일반 처리 소요 시간: {time.time() - start_serial:.2f}초&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;대용량 데이터프레임에서 &lt;code&gt;parallel_apply&lt;/code&gt;를 사용하면 일반 &lt;code&gt;apply&lt;/code&gt; 대비 연산 속도가 크게 향상되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;결론: 파이썬의 무한한 가능성을 열다&lt;/h2&gt;
&lt;p&gt;파이썬의 GIL은 단일 스레드 환경에서 메모리 관리를 효율적으로 해주는 장점도 있지만, 멀티코어 시대에 성능의 제약이 되는 것도 사실입니다. 하지만 이번 포스팅에서 살펴본 &lt;strong&gt;멀티프로세싱&lt;/strong&gt;과 &lt;strong&gt;&lt;code&gt;pandarallel&lt;/code&gt;&lt;/strong&gt;과 같은 전문 라이브러리를 활용하면 이러한 한계를 극복하고 파이썬의 진정한 병렬 처리 능력을 끌어낼 수 있습니다.&lt;/p&gt;
&lt;p&gt;이러한 기법들은 단순한 코드 최적화를 넘어, 여러분이 다루는 대규모 데이터와 복잡한 연산 작업의 처리 시간을 획기적으로 단축시켜 개발 생산성과 시스템 효율을 동시에 높여줄 것입니다.  &lt;/p&gt;</description>
      <category>Programming/Python</category>
      <category>multi processing</category>
      <category>multi threading</category>
      <category>pandas</category>
      <category>parallel_apply</category>
      <category>Python</category>
      <category>멀티쓰레딩</category>
      <category>파이썬</category>
      <category>파이썬 멀티프로세싱</category>
      <category>판다스</category>
      <author>GeonWoo Kim</author>
      <guid isPermaLink="true">https://gwlabs.tistory.com/140</guid>
      <comments>https://gwlabs.tistory.com/140#entry140comment</comments>
      <pubDate>Wed, 20 Aug 2025 16:39:43 +0900</pubDate>
    </item>
    <item>
      <title>Spring Rest API 캐싱전략 완벽 가이드: @Cacheable과 CacheManager 활용법</title>
      <link>https://gwlabs.tistory.com/139</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KvwDu/btsPXQezeUF/IF8vEIHJaI8Zd2Kkf5WvM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KvwDu/btsPXQezeUF/IF8vEIHJaI8Zd2Kkf5WvM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KvwDu/btsPXQezeUF/IF8vEIHJaI8Zd2Kkf5WvM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKvwDu%2FbtsPXQezeUF%2FIF8vEIHJaI8Zd2Kkf5WvM0%2Fimg.png&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;Spring Rest API 캐싱전략 완벽 가이드: @Cacheable과 CacheManager 활용법&lt;/h1&gt;
&lt;h2&gt;서론&lt;/h2&gt;
&lt;p&gt;Rest API 서버를 운영하다 보면 가장 큰 고민 중 하나는 &lt;strong&gt;성능과 응답 속도&lt;/strong&gt;입니다. 특히 조회 요청이 빈번한 API의 경우, 매번 DB를 조회하는 것은 서버 부담을 크게 증가시킵니다. 이를 효율적으로 해결하는 방법이 바로 &lt;strong&gt;캐싱(Caching)&lt;/strong&gt;입니다.&lt;br&gt;Spring은 &lt;code&gt;@Cacheable&lt;/code&gt;과 &lt;code&gt;CacheManager&lt;/code&gt;를 통해 강력하면서도 유연한 캐싱 기능을 제공하고 있으며, 상황에 맞는 캐싱 전략을 적용하면 성능 최적화 효과를 극대화할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 &lt;strong&gt;Spring Rest API 캐싱전략&lt;/strong&gt;, &lt;strong&gt;CacheManager 사용법&lt;/strong&gt;, &lt;strong&gt;@Cacheable 활용법&lt;/strong&gt;을 중심으로 실무 적용 방법을 정리하겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;본론&lt;/h2&gt;
&lt;h3&gt;1. Spring Rest API 캐싱전략&lt;/h3&gt;
&lt;p&gt;Spring에서 캐싱은 크게 다음과 같은 전략으로 나눌 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;읽기 중심 API 캐싱&lt;/strong&gt;: 자주 조회되지만 데이터 변경이 드문 API 응답을 캐싱.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;조건부 캐싱&lt;/strong&gt;: 특정 조건(예: 파라미터 값, 사용자 권한)에 따라 캐싱 여부 결정.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTL(Time-to-Live) 전략&lt;/strong&gt;: 데이터 유효기간을 설정하여 일정 시간 후 자동으로 캐시 무효화.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;분산 캐싱&lt;/strong&gt;: Redis, Hazelcast 등 외부 캐시 서버를 사용하여 다중 서버 환경에서도 일관된 캐시 유지.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;캐싱 전략 선택 시 고려해야 할 요소는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터의 변경 주기&lt;/li&gt;
&lt;li&gt;실시간성이 중요한지 여부&lt;/li&gt;
&lt;li&gt;API 호출 빈도&lt;/li&gt;
&lt;li&gt;인프라 구성(단일 서버 vs 다중 서버)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;2. CacheManager 사용법&lt;/h3&gt;
&lt;p&gt;Spring은 &lt;code&gt;CacheManager&lt;/code&gt;를 통해 다양한 캐시 구현체(EhCache, Redis, Caffeine 등)를 추상화합니다.&lt;/p&gt;
&lt;h4&gt;예제: Caffeine Cache 설정&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager(&amp;quot;users&amp;quot;, &amp;quot;products&amp;quot;);
        cacheManager.setCaffeine(
                Caffeine.newBuilder()
                        .expireAfterWrite(10, TimeUnit.MINUTES)
                        .maximumSize(1000)
        );
        return cacheManager;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드에서는 Caffeine 캐시를 사용하며, users, products 캐시 영역을 정의했습니다. TTL은 10분, 최대 캐시 사이즈는 1000개로 설정되어 있습니다.&lt;/p&gt;
&lt;h3&gt;3. @Cacheable 사용법&lt;/h3&gt;
&lt;p&gt;@Cacheable은 메서드 실행 결과를 캐싱하고, 동일한 파라미터로 호출 시 캐시된 결과를 반환합니다.&lt;/p&gt;
&lt;h4&gt;기본 사용법&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Cacheable(value = &amp;quot;users&amp;quot;, key = &amp;quot;#userId&amp;quot;)
    public User getUserById(Long userId) {
        simulateSlowService();
        return new User(userId, &amp;quot;User_&amp;quot; + userId);
    }

    private void simulateSlowService() {
        try {
            Thread.sleep(3000); // DB 조회 대기 시간 가정
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;주요 속성&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;value&lt;/strong&gt;: 캐시 이름 지정 (CacheManager에서 관리)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;key&lt;/strong&gt;: 캐시 키 정의 (SpEL 사용 가능)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;condition&lt;/strong&gt;: 조건부 캐싱 여부 지정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;unless&lt;/strong&gt;: 결과에 따라 캐싱 제외&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;조건부 캐싱 예시&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Cacheable(value = &amp;quot;products&amp;quot;, key = &amp;quot;#id&amp;quot;, condition = &amp;quot;#id &amp;gt; 10&amp;quot;)
public Product getProduct(Long id) {
    return new Product(id, &amp;quot;Product_&amp;quot; + id);
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;→ ID가 10보다 큰 경우에만 캐싱.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;Spring Rest API에서 캐싱은 단순한 성능 최적화 이상의 의미를 가집니다. 캐싱 전략을 올바르게 적용하면 DB 부하 감소, 응답 속도 향상, 서비스 안정성 강화라는 세 마리 토끼를 동시에 잡을 수 있습니다.&lt;/p&gt;
&lt;p&gt;정리하자면,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CacheManager로 다양한 캐시 구현체를 통합 관리할 수 있고,&lt;/li&gt;
&lt;li&gt;@Cacheable을 통해 선언형 캐싱을 손쉽게 적용할 수 있으며,&lt;/li&gt;
&lt;li&gt;상황별 캐싱 전략(TTL, 조건부 캐싱, 분산 캐싱)을 병행하면 실무에서 강력한 성능 최적화를 이끌어낼 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;  효율적인 캐싱 전략은 API 성능을 한 단계 끌어올리는 핵심 무기입니다.&lt;/p&gt;</description>
      <category>Programming/Java</category>
      <category>@Cachable</category>
      <category>cachemanager</category>
      <category>caffeine</category>
      <category>redis</category>
      <category>Rest API Cache</category>
      <category>Spring Boot</category>
      <category>Spring Cachable</category>
      <category>조건부캐시</category>
      <category>캐시</category>
      <category>캐싱</category>
      <author>GeonWoo Kim</author>
      <guid isPermaLink="true">https://gwlabs.tistory.com/139</guid>
      <comments>https://gwlabs.tistory.com/139#entry139comment</comments>
      <pubDate>Mon, 18 Aug 2025 15:55:55 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch 사용법: 대용량 데이터 처리를 위한 실무 가이드</title>
      <link>https://gwlabs.tistory.com/138</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7Az8w/btsPTEmwzcW/v5ERhPTYZctvoGyqOxK6b0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7Az8w/btsPTEmwzcW/v5ERhPTYZctvoGyqOxK6b0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7Az8w/btsPTEmwzcW/v5ERhPTYZctvoGyqOxK6b0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7Az8w%2FbtsPTEmwzcW%2Fv5ERhPTYZctvoGyqOxK6b0%2Fimg.png&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;Spring Batch 사용법: 대용량 데이터 처리를 위한 실무 가이드&lt;/h1&gt;
&lt;h2&gt;서론&lt;/h2&gt;
&lt;p&gt;대규모 데이터 처리 작업은 단순히 코드 몇 줄로 해결할 수 있는 문제가 아닙니다. 안정성, 트랜잭션 관리, 스케줄링, 장애 복구 등 고려해야 할 요소가 많습니다. &lt;strong&gt;Spring Batch&lt;/strong&gt;는 이러한 요구사항을 충족하기 위해 설계된 프레임워크로, 반복적이고 대량의 데이터 처리를 안정적으로 지원합니다.&lt;br&gt;본 포스팅에서는 &lt;strong&gt;Spring Batch의 핵심 개념&lt;/strong&gt;, &lt;strong&gt;설정 방법&lt;/strong&gt;, 그리고 &lt;strong&gt;실무에서 주의해야 할 사항&lt;/strong&gt;을 다루어, 개발자가 바로 활용할 수 있는 지식을 제공합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;본론&lt;/h2&gt;
&lt;h3&gt;1. Spring Batch의 핵심 개념&lt;/h3&gt;
&lt;p&gt;Spring Batch는 크게 세 가지 개념으로 나눌 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Job&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;배치 작업의 최상위 단위  &lt;/li&gt;
&lt;li&gt;하나의 Job은 여러 개의 Step으로 구성  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Step&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Job을 구성하는 개별 처리 단계  &lt;/li&gt;
&lt;li&gt;일반적으로 &lt;code&gt;읽기(Read) → 처리(Processing) → 쓰기(Write)&lt;/code&gt; 구조를 가짐  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ItemReader / ItemProcessor / ItemWriter&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ItemReader&lt;/strong&gt;: 데이터 소스로부터 데이터를 읽어옴 (예: DB, CSV, API)  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ItemProcessor&lt;/strong&gt;: 읽어온 데이터를 가공 및 변환  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ItemWriter&lt;/strong&gt;: 가공된 데이터를 목적지에 저장  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;즉, Spring Batch는 &lt;strong&gt;대량의 데이터를 작은 단위로 나누어 안정적으로 처리하는 구조&lt;/strong&gt;를 제공합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. Spring Batch 기본 설정&lt;/h3&gt;
&lt;p&gt;Spring Batch 프로젝트를 시작하기 위해서는 &lt;strong&gt;Spring Boot + Spring Batch Starter&lt;/strong&gt; 의존성을 추가합니다.&lt;/p&gt;
&lt;h4&gt;Maven 의존성&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-batch&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-jdbc&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.h2database&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;h2&amp;lt;/artifactId&amp;gt;
    &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;기본 Job &amp;amp; Step 구성&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableBatchProcessing
public class BatchConfig {

    @Bean
    public Job exampleJob(JobRepository jobRepository, Step exampleStep) {
        return new JobBuilder(&amp;quot;exampleJob&amp;quot;, jobRepository)
                .start(exampleStep)
                .build();
    }

    @Bean
    public Step exampleStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder(&amp;quot;exampleStep&amp;quot;, jobRepository)
                .&amp;lt;String, String&amp;gt;chunk(10, transactionManager)
                .reader(exampleReader())
                .processor(exampleProcessor())
                .writer(exampleWriter())
                .build();
    }

    @Bean
    public ItemReader&amp;lt;String&amp;gt; exampleReader() {
        return new ListItemReader&amp;lt;&amp;gt;(List.of(&amp;quot;A&amp;quot;, &amp;quot;B&amp;quot;, &amp;quot;C&amp;quot;));
    }

    @Bean
    public ItemProcessor&amp;lt;String, String&amp;gt; exampleProcessor() {
        return item -&amp;gt; item.toLowerCase();
    }

    @Bean
    public ItemWriter&amp;lt;String&amp;gt; exampleWriter() {
        return items -&amp;gt; items.forEach(System.out::println);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위 코드는 간단한 예시로, 문자열 리스트 [&amp;quot;A&amp;quot;, &amp;quot;B&amp;quot;, &amp;quot;C&amp;quot;]를 읽어 소문자로 변환 후 출력합니다. 실제 환경에서는 DB, CSV, API 연동 등을 Reader/Writer에 적용할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3. Spring Batch 사용 시 유의사항&lt;/h3&gt;
&lt;p&gt;Spring Batch를 실무에서 사용할 때는 다음과 같은 포인트를 반드시 고려해야 합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;트랜잭션 관리&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;각 Step은 기본적으로 트랜잭션 단위로 동작합니다.&lt;/li&gt;
&lt;li&gt;Chunk 크기(chunk-size)에 따라 롤백 단위가 달라지므로, 데이터 일관성을 고려해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;성능 최적화&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;대규모 데이터의 경우 멀티 스레드 Step 또는 파티셔닝 전략을 활용하면 처리 속도를 높일 수 있습니다.&lt;/li&gt;
&lt;li&gt;대용량 Batch는 Reader/Writer에서 I/O 최적화가 중요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;재시작 전략&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Spring Batch는 실패한 Job을 중단된 위치부터 재시작할 수 있습니다.&lt;/li&gt;
&lt;li&gt;이를 위해 JobRepository(DB 테이블)에 실행 이력이 저장되며, 프로덕션 환경에서는 H2 대신 MySQL, PostgreSQL 같은 RDBMS를 권장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;운영 모니터링&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Job 실행 로그, Step별 처리 건수, 에러 내역 등을 수집/모니터링하는 체계를 구축해야 장애 대응이 수월합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;Spring Batch는 대량 데이터 처리를 안정적이고 유연하게 지원하는 강력한 프레임워크입니다.&lt;br&gt;본 포스팅에서는 Spring Batch의 핵심 개념, 기본 설정 방법, 실무 적용 시 주의사항을 살펴보았습니다.&lt;/p&gt;
&lt;p&gt;  요약: Spring Batch를 올바르게 이해하고 활용하면, 대규모 데이터 처리 업무를 안정적이고 효율적으로 수행할 수 있습니다.&lt;/p&gt;</description>
      <category>Programming/Java</category>
      <category>batch</category>
      <category>java</category>
      <category>Spring</category>
      <category>Spring Batch</category>
      <category>대용량처리</category>
      <category>배치</category>
      <category>스프링</category>
      <category>스프링 배치</category>
      <author>GeonWoo Kim</author>
      <guid isPermaLink="true">https://gwlabs.tistory.com/138</guid>
      <comments>https://gwlabs.tistory.com/138#entry138comment</comments>
      <pubDate>Sat, 16 Aug 2025 14:23:22 +0900</pubDate>
    </item>
    <item>
      <title>Spring Kafka 완벽 가이드: KafkaTemplate와 @KafkaListener, 재시도 및 Dead Letter Queue 활용법</title>
      <link>https://gwlabs.tistory.com/137</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m6gBQ/btsPSmtfkWC/vSPyhkGJ0Na8k6CMnBrBg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m6gBQ/btsPSmtfkWC/vSPyhkGJ0Na8k6CMnBrBg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m6gBQ/btsPSmtfkWC/vSPyhkGJ0Na8k6CMnBrBg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm6gBQ%2FbtsPSmtfkWC%2FvSPyhkGJ0Na8k6CMnBrBg1%2Fimg.png&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;Spring Kafka 완벽 가이드: KafkaTemplate와 @KafkaListener, 재시도 및 Dead Letter Queue 활용법&lt;/h1&gt;
&lt;h2&gt;서론&lt;/h2&gt;
&lt;p&gt;Apache Kafka는 대규모 실시간 데이터 스트리밍과 비동기 메시징을 처리하는 데 최적화된 분산 이벤트 스트리밍 플랫폼입니다.&lt;br&gt;Spring Kafka는 이러한 Kafka의 기능을 Spring 환경에 자연스럽게 통합하여 개발자가 &lt;strong&gt;간결한 코드&lt;/strong&gt;로 안정적이고 확장 가능한 메시지 기반 애플리케이션을 만들 수 있도록 지원합니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 &lt;strong&gt;Spring Kafka의 핵심 개념&lt;/strong&gt;, &lt;strong&gt;KafkaTemplate 사용법&lt;/strong&gt;, &lt;strong&gt;@KafkaListener 활용법과 주의사항&lt;/strong&gt;, 그리고 &lt;strong&gt;재시도 로직 및 Dead Letter Queue(DLQ) 적용 방법&lt;/strong&gt;을 실무 중심으로 정리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;본론&lt;/h2&gt;
&lt;h3&gt;1. Spring Kafka 핵심 개념&lt;/h3&gt;
&lt;p&gt;Spring Kafka의 주요 컴포넌트는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;KafkaTemplate&lt;/strong&gt;&lt;br&gt;Kafka 프로듀서 역할. 메시지를 전송하고 결과를 비동기/동기로 받을 수 있음.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;@KafkaListener&lt;/strong&gt;&lt;br&gt;Kafka 토픽을 구독하고 메시지를 처리하는 어노테이션 기반 컨슈머.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Listener Container&lt;/strong&gt;&lt;br&gt;메시지 리스너 실행 환경을 관리하며 스레드, 오프셋 커밋, 재시도 정책 등을 제어.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ErrorHandler / SeekToCurrentErrorHandler&lt;/strong&gt;&lt;br&gt;메시지 처리 실패 시 재시도 및 DLQ 라우팅 설정 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;기본 의존성 예시&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.kafka&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-kafka&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. KafkaTemplate 사용법&lt;/h3&gt;
&lt;p&gt;KafkaTemplate은 메시지를 Kafka로 전송하는 핵심 클래스입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;프로듀서 설정&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class KafkaProducerConfig {

    @Bean
    public ProducerFactory&amp;lt;String, String&amp;gt; producerFactory() {
        Map&amp;lt;String, Object&amp;gt; config = new HashMap&amp;lt;&amp;gt;();
        config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, &amp;quot;localhost:9092&amp;quot;);
        config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        return new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(config);
    }

    @Bean
    public KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate() {
        return new KafkaTemplate&amp;lt;&amp;gt;(producerFactory());
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;메세지 전송&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class KafkaProducerService {

    private final KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate;

    public KafkaProducerService(KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void send(String topic, String message) {
        kafkaTemplate.send(topic, message)
            .addCallback(
                success -&amp;gt; System.out.println(&amp;quot;전송 성공: &amp;quot; + success.getRecordMetadata()),
                failure -&amp;gt; System.err.println(&amp;quot;전송 실패: &amp;quot; + failure.getMessage())
            );
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;@KafkaListener 사용법과 주의사항&lt;/h3&gt;
&lt;p&gt;@KafkaListener는 Kafka 메시지 소비를 단순화합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;컨슈머 설정&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class KafkaConsumerConfig {

    @Bean
    public ConsumerFactory&amp;lt;String, String&amp;gt; consumerFactory() {
        Map&amp;lt;String, Object&amp;gt; config = new HashMap&amp;lt;&amp;gt;();
        config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, &amp;quot;localhost:9092&amp;quot;);
        config.put(ConsumerConfig.GROUP_ID_CONFIG, &amp;quot;my-group&amp;quot;);
        config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(config);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; factory =
            new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
        factory.setConsumerFactory(consumerFactory());
        return factory;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;메세지 소비&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class KafkaConsumerService {

    @KafkaListener(topics = &amp;quot;test-topic&amp;quot;, groupId = &amp;quot;my-group&amp;quot;)
    public void consume(String message) {
        System.out.println(&amp;quot;수신 메시지: &amp;quot; + message);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;주의사항&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;groupId 필수: 같은 groupId를 사용하는 컨슈머끼리는 메시지를 파티션 단위로 분배 처리.&lt;/li&gt;
&lt;li&gt;파티션 수 고려: 병렬 컨슈머 수는 파티션 수 이상이 되어도 추가 컨슈머는 대기 상태.&lt;/li&gt;
&lt;li&gt;에러 처리 필수: 예외 발생 시 무한 재시도 방지를 위해 재시도 및 DLQ 설정 필요.&lt;/li&gt;
&lt;li&gt;오프셋 관리 전략: auto-commit 또는 수동 커밋 전략을 요구사항에 맞게 선택.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;4. 재시도 로직과 Dead Letter Queue 적용&lt;/h3&gt;
&lt;p&gt;메시지 처리 중 오류가 발생하면 무한 재시도를 피하기 위해 &lt;strong&gt;재시도 횟수 제한&lt;/strong&gt;과 &lt;strong&gt;DLQ&lt;/strong&gt;를 설정하는 것이 권장됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;재시도 &amp;amp; DLQ 설정 예시&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class KafkaErrorHandlerConfig {

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; factory =
            new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
        factory.setConsumerFactory(consumerFactory());

        // SeekToCurrentErrorHandler: 재시도 후 DLQ로 전송
        DeadLetterPublishingRecoverer recoverer =
            new DeadLetterPublishingRecoverer(kafkaTemplate(), (record, ex) -&amp;gt; 
                new TopicPartition(record.topic() + &amp;quot;.DLT&amp;quot;, record.partition()));

        factory.setErrorHandler(new SeekToCurrentErrorHandler(recoverer, 3)); // 최대 3회 재시도
        return factory;
    }

    @Bean
    public ProducerFactory&amp;lt;String, String&amp;gt; producerFactory() {
        Map&amp;lt;String, Object&amp;gt; config = new HashMap&amp;lt;&amp;gt;();
        config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, &amp;quot;localhost:9092&amp;quot;);
        config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        return new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(config);
    }

    @Bean
    public KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate() {
        return new KafkaTemplate&amp;lt;&amp;gt;(producerFactory());
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Dead Letter Queue 운영 팁&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.DLT 접미사를 사용해 DLQ 토픽 이름을 명확하게 구분.&lt;/li&gt;
&lt;li&gt;DLQ 데이터를 주기적으로 모니터링하여 장애 원인을 분석.&lt;/li&gt;
&lt;li&gt;필요 시 DLQ 메시지를 재처리하는 별도 컨슈머 운영.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;Spring Kafka는 복잡한 Kafka API를 추상화하여 개발자가 간결하게 메시지 기반 애플리케이션을 개발할 수 있도록 돕습니다.&lt;br&gt;KafkaTemplate과 @KafkaListener를 올바르게 조합하고, 재시도 및 DLQ 설정을 적용하면 안정적인 이벤트 스트리밍 아키텍처를 구축할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;요약:&lt;/strong&gt;&lt;br&gt;Spring Kafka를 이해하고 재시도·DLQ 패턴을 적용하면, 실무에서 안정적이고 예측 가능한 메시징 시스템을 구현할 수 있습니다.&lt;/p&gt;</description>
      <category>Programming/Java</category>
      <category>@KafkaListener</category>
      <category>Dead Letter Queue</category>
      <category>Kafka</category>
      <category>kafka consumer</category>
      <category>KafkaTemplate</category>
      <category>spring kafka</category>
      <category>이벤트시스템</category>
      <category>카프카</category>
      <category>카프카 컨슈머</category>
      <category>카프카 프로듀서</category>
      <author>GeonWoo Kim</author>
      <guid isPermaLink="true">https://gwlabs.tistory.com/137</guid>
      <comments>https://gwlabs.tistory.com/137#entry137comment</comments>
      <pubDate>Fri, 15 Aug 2025 09:05:49 +0900</pubDate>
    </item>
  </channel>
</rss>