<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sangwoo-sean</title>
        <link>https://velog.io/</link>
        <description>going up</description>
        <lastBuildDate>Sat, 04 Apr 2026 07:16:39 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>sangwoo-sean</title>
            <url>https://velog.velcdn.com/images/sangwoo-sean/profile/c99ccf27-4a9f-4893-8731-43361922f91c/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. sangwoo-sean. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sangwoo-sean" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[AI 와 함께 Pogstres 데이터베이스 테이블 파티셔닝하기]]></title>
            <link>https://velog.io/@sangwoo-sean/AI-%EC%99%80-%ED%95%A8%EA%BB%98-Pogstres-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%85%8C%EC%9D%B4%EB%B8%94-%ED%8C%8C%ED%8B%B0%EC%85%94%EB%8B%9D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sangwoo-sean/AI-%EC%99%80-%ED%95%A8%EA%BB%98-Pogstres-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%85%8C%EC%9D%B4%EB%B8%94-%ED%8C%8C%ED%8B%B0%EC%85%94%EB%8B%9D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 04 Apr 2026 07:16:39 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>2억 건짜리 테이블이 있었다. 인덱스가 11개였고, 크기는 데이터(25 GB)의 3.3배인 <strong>82 GB</strong>였다. 어느 시점부터 이 테이블의 쿼리 CPU 사용량과 읽기 대기 시간이 점점 증가하는 게 모니터링에 잡히기 시작했다. 데이터는 앞으로도 계속 늘어날 예정이었다.</p>
<p>비즈니스 로직 특성상 한 유저는 자신이 지금까지 생성한 모든 데이터를 조회하고 페이지네이션 할 수 있어야 했다. 파티셔닝 없이는 그 쿼리가 수억 건의 데이터를 풀스캔할 수밖에 없었다.</p>
<p><code>user_id</code> 기준으로 HASH 파티셔닝을 적용하면 그 쿼리가 파티션 중 <strong>1개만</strong> 스캔하게 된다.</p>
<p>32개 파티션으로 파티셔닝을 진행했고, 인덱스도 불필요한 것들을 정리해 11개 -&gt; 5개로 줄였다.</p>
<p>결과적으로 DB 피크 부하가 <strong>50% → 30%</strong> 로 내려갔고, 부하 패턴이 불규칙했었는데 예측 가능해졌다. 앞으로 트래픽이 더 늘어도 감당할 수 있는 구조가 됐다. 이 글은 그 과정을 기록한다.</p>
<p>데이터베이스 테이블명은 회사 정보 유출 방지를 위해 대체한 이름으로 표기했다.</p>
<hr>
<h2 id="ai에게-물었다">AI에게 물었다</h2>
<p>관계형 데이터베이스 테이블들의 구조를 관찰하다가 AI에게 물었다.</p>
<blockquote>
<p>&quot;users, events, event_participants 테이블이 있고, 한 유저가 자신이 참여한 모든 이벤트를 조회해야 합니다. 1000만 명의 유저와 10만 개의 이벤트를 안정적으로 서빙하려면 어떤 구조로 데이터베이스를 설계해야 할까요?&quot;</p>
</blockquote>
<p><strong>&quot;HASH 파티셔닝&quot;</strong> 이라는 말이 눈에 들어왔다.</p>
<p>파티셔닝 자체는 알고 있었다. 서비스에서 이미 날짜 기준으로 쪼개는 테이블이 있었기 때문이다. 근데 그게 RANGE 파티셔닝이라는 걸 이번에 알았다. HASH 파티셔닝이 있다는 것도 이번에 처음 알았다.</p>
<p>개념은 간단했다. <code>user_id</code>를 해시해서 32개 파티션에 고르게 분산시킨다. 한 유저의 데이터는 항상 같은 파티션에만 들어간다. 그러면 그 유저의 이벤트를 조회할 때 32개 파티션 중 <strong>1개만</strong> 스캔하면 된다.</p>
<p>다음 문제는 어떻게 전환하느냐였다. 라이브 중인 테이블을 파티셔닝 구조로 변경하는 방법은 없다고 한다. 기존 테이블을 수정하는 게 아니라, <strong>새로운 파티션 테이블을 만들고 거기에 데이터를 옮겨야 한다.</strong> 서비스를 멈추지 않으면서.</p>
<hr>
<h2 id="사전준비">사전준비</h2>
<p>방향은 잡혔는데, 바로 실행에 옮기기 전에 현황을 제대로 파악해야 했다.</p>
<p>소스코드 전체를 훑어서 이 테이블에 접근하는 모든 쿼리를 취합했다.
AWS RDS 성능 개선 도우미와 Datadog을 사용하고 있었어서 실제로 얼마나 자주, 어떤 패턴으로 쿼리가 나가는지 분석하기 용이했다.
파티셔닝 후 프루닝이 제대로 작동하려면 파티션 키(<code>user_id</code>)가 WHERE 절에 직접 포함된 쿼리와 그렇지 않은 쿼리를 모두 파악해두어야 했다.</p>
<p>파티션 개수도 결정해야 했다. 16개와 32개를 놓고 고민했다.
파티션이 많을수록 각 파티션이 작아져 스캔 효율은 올라가지만, 인덱스 관리나 VACUUM 같은 유지보수 오버헤드도 같이 늘어난다.
현재 데이터 규모와 앞으로의 성장 속도를 고려했을 때 <strong>32개</strong>가 적정선이라고 판단했다.</p>
<hr>
<h2 id="계획">계획</h2>
<p>전체 과정을 8개 Phase로 나눴다.</p>
<pre><code>Phase 0: 사전 검증
Phase 1: 신규 테이블 생성 + 인덱스 설계
Phase 2: 백필 — 2억 건 이관
Phase 3: 듀얼라이팅 — 실시간 동기화 시작
Phase 4: 누락 데이터 재동기화
Phase 5: 정합성 검증 + 성능 벤치마크
Phase 6: 테이블 전환 (RENAME)
Phase 7: 롤백 계획 유지 (24시간)
Phase 8: 안정화 확인 후 정리</code></pre><p>실제 라이브 전환 전에 개발 환경에서 전체 과정을 먼저 돌려봤다.
그리고 예상치 못한 돌발 상황과 대응책을 AI와 상담하며 시나리오를 정리해뒀다.
라이브하는 서비스의 DB를 마이그레이션하는 작업이라 불안감을 줄이고 싶었고 실제로 도움이 많이 되었다.</p>
<hr>
<h2 id="실행">실행</h2>
<h3 id="인덱스는-파티션-생성-전에-만든다">인덱스는 파티션 생성 전에 만든다</h3>
<p>PostgreSQL에서 파티션 테이블에 인덱스를 정의하면 <strong>이후 생성되는 모든 파티션이 자동으로 그 인덱스를 상속한다.</strong> 파티션을 먼저 만들고 인덱스를 붙이려면 32개 파티션에 각각 붙여야 하므로, 순서가 중요하다. 빈 테이블이라 <code>CONCURRENTLY</code> 없이 해도 잠금 문제가 없다.</p>
<h3 id="백필은-야간에-10만-건씩">백필은 야간에 10만 건씩</h3>
<p>운영 DB에 무리가 가지 않도록 10만 건씩 배치로, 새벽 시간에만 돌렸다. 백필 진행 상황을 기록하는 임시 테이블(<code>migration_progress</code>)을 만들어서 배치가 끝날 때마다 처리한 범위와 건수를 기록했다. Replication lag가 임계값을 넘으면 자동으로 멈추고, 연결이 끊어지면 이 테이블을 참조해 마지막 성공 배치부터 이어갔다.</p>
<pre><code>2026-03-06 01:32:41 [INFO] [Batch 1853] +100,000 | 누적 1,100,000/201,229,454 (0.5%) | 64s
2026-03-06 01:32:44 [WARNING] Replication lag 121.0 MB — 60초 대기 후 재시도
2026-03-06 01:33:44 [INFO] DB 연결이 끊어짐 — 재연결합니다.
2026-03-06 01:34:51 [INFO] [Batch 1854] +100,000 | 누적 1,200,000/201,229,454 (0.6%) | 66s</code></pre><p>트래픽이 없는 야간에 자동으로 시작하고, 트래픽이 들어오는 아침시간이 되면 멈추도록 스크립트를 만들어서 며칠에 걸쳐 crontab으로 자동으로 진행했다.</p>
<h3 id="듀얼라이팅-트리거">듀얼라이팅 트리거</h3>
<p>백필이 끝나면 실시간 동기화를 시작해야 한다. 방법은 두 가지를 고려했다. 애플리케이션 레벨에서 두 테이블에 동시에 쓰거나, DB 트리거로 처리하거나.</p>
<p>애플리케이션 레벨 듀얼라이팅은 배치 스크립트나 직접 쿼리처럼 앱을 거치지 않는 쓰기는 자동으로 미러링되지 않는 점이 리스크였다. 그리고 본질적으로 데이터베이스를 마이그레이션하는데 애플리케이션이 서비스 레벨에서 깊에 관여한다는게 부자연스럽다고 생각했다.</p>
<p>DB 트리거는 원본 테이블에 쓰기가 발생하는 순간 같은 트랜잭션 안에서 새 테이블에도 반영된다. 경로를 일일이 찾을 필요 없이 DDL 하나로 모든 쓰기를 커버할 수 있었다. 그래서 DB 트리거가 더 좋은 선택이라고 판단했다.</p>
<h3 id="역방향-트리거로-롤백-안전망-확보">역방향 트리거로 롤백 안전망 확보</h3>
<p>전환(RENAME) 후 24시간 동안은 새 테이블 → 구 테이블로도 듀얼라이팅을 유지했다. 문제가 생겼을 때 RENAME을 되돌리기만 하면 되고, 데이터 손실이 없다.</p>
<hr>
<h2 id="실전에서-마주친-문제">실전에서 마주친 문제</h2>
<h3 id="파티션-프루닝이-안-되는-쿼리">파티션 프루닝이 안 되는 쿼리</h3>
<p>이 과정에서 <strong>파티션 프루닝</strong>이라는 개념을 처음 제대로 이해하게 됐다. 파티션 프루닝이란 쿼리가 실행될 때 PostgreSQL이 파티션 키 조건을 보고 관련 없는 파티션을 아예 스캔 대상에서 제외하는 최적화다. <code>user_id = $1</code> 조건이 WHERE 절에 있으면 32개 파티션 중 해당 유저의 파티션 1개만 읽는다. 이게 작동하지 않으면 파티셔닝을 했어도 32개를 전부 스캔하게 된다.</p>
<p>라이브 전환 직후, 모니터링에서 이상한 게 잡혔다. 특정 쿼리가 실행될 때마다 DB 부하가 튀었다. <code>BufferIO</code> 경합이 폭증하고 CPU가 치솟았다.</p>
<p>원인을 추적해보니 UPDATE 쿼리 하나가 <code>user_id</code> 없이 <code>id</code>만으로 행을 찾고 있었다.</p>
<pre><code class="language-sql">-- 이렇게 하면 32개 파티션 전체를 스캔한다
UPDATE event_participants SET status = &#39;completed&#39; WHERE id = $1;

-- 이렇게 해야 1개 파티션만 스캔한다
UPDATE event_participants SET status = &#39;completed&#39; WHERE id = $1 AND user_id = $2;</code></pre>
<p>HASH 파티셔닝에서 파티션 프루닝은 WHERE 절에 파티션 키가 <strong>직접</strong> 있을 때만 작동한다. JOIN으로 연결된 다른 테이블의 <code>user_id</code> 조건은 프루닝에 작동하지 않는다. 사전에 쿼리를 전수 조사했음에도 이 쿼리를 놓쳤다.
빈번하게 일어나는 수정 쿼리였어서 부하가 올라오는걸 전환 직후 감지했고 바로 쿼리를 찾아 수정하고 배포해서 안정화 했다.</p>
<h3 id="알게-된-postgresql-제약">알게 된 PostgreSQL 제약</h3>
<p>파티션 테이블에는 <code>CREATE INDEX CONCURRENTLY</code>를 부모에 직접 쓸 수 없다. 이걸 모르면 운영 중에 인덱스를 추가할 방법이 없다고 오해하기 쉽다. 실제로는 3단계로 처리할 수 있다.</p>
<pre><code>1. 각 파티션에 CREATE INDEX CONCURRENTLY (32번)
2. 부모에 CREATE INDEX ON ONLY (데이터 스캔 없이 메타데이터만 생성)
3. 각 파티션 인덱스를 ALTER INDEX ... ATTACH PARTITION</code></pre><p>3단계에서 32개가 모두 ATTACH되면 부모 인덱스가 유효한 상태로 전환된다.</p>
<p>이 제약을 파악해두고, 전환 후 성능 모니터링 결과에 따라 추가 인덱싱이 필요한 경우를 대비해 위 절차를 시나리오로 미리 정리해두었다. 실제로 전환 후 쿼리 패턴을 보면서 인덱스를 조정할 가능성이 있었기 때문이다. (결국 이 시나리오가 발생하지는 않았지만)</p>
<hr>
<h2 id="결과">결과</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>전</th>
<th>후</th>
</tr>
</thead>
<tbody><tr>
<td>인덱스 수</td>
<td>11개</td>
<td>5개</td>
</tr>
<tr>
<td>인덱스 크기</td>
<td>82 GB</td>
<td>~25–30 GB</td>
</tr>
<tr>
<td>PK</td>
<td><code>id</code></td>
<td><code>(user_id, event_id)</code></td>
</tr>
<tr>
<td>DB 피크 부하</td>
<td>~50%</td>
<td>~35%</td>
</tr>
<tr>
<td>서비스 중단</td>
<td>—</td>
<td>약 5분</td>
</tr>
</tbody></table>
<p>서비스 중단 5분은 전환 시퀀스에 소요된 시간이다. 듀얼라이팅 트리거를 DROP하고, 테이블을 RENAME한 뒤, 역방향 듀얼라이팅 트리거를 CREATE하는 세 단계를 순서대로 진행했다. 역방향 트리거가 올라오는 것까지 확인한 후 쓰기를 재개했다.</p>
<p>다만 DB는 여러 요인이 복합적으로 작용하기 때문에 이 수치가 파티셔닝만의 효과라고 단정할 수는 없다. 인덱스 정리, 파티션 분산, 쿼리 패치가 함께 작동한 결과일 것이다.</p>
<hr>
<h2 id="배운-것">배운 것</h2>
<p><strong>파티션 키 호환성 점검은 전환 전 필수다.</strong> <code>id</code>만으로 접근하는 쿼리가 하나라도 있으면 전환 후 예상치 못한 성능 저하가 발생한다. 직접 겪었다.</p>
<p><strong>트리거 에러 처리는 반드시 non-blocking으로.</strong> 트리거가 실패했을 때 원본 트랜잭션까지 롤백되면 안 된다. EXCEPTION에서 잡아서 에러 테이블에 기록하고, 원본은 그대로 진행시킨다.</p>
<p><strong>역방향 트리거는 전환 직후의 안전망이다.</strong> 전환 후 문제가 생겼을 때 RENAME만 되돌리면 되도록 준비해두면, 판단하고 결정하는 시간이 생긴다.</p>
<p><strong>파티션 인덱스 추가는 <code>CONCURRENTLY → ON ONLY → ATTACH</code> 3단계다.</strong></p>
<p>AI 를 활용해 사전 준비부터 계획, 검증, 예상치 못한 돌발 시나리오 준비, 실제 실행, 모니터링까지를 안전하고 빠르게 진행할 수 있었던것 같다. 이 경험을 기반으로 다음에 또 파티셔닝할 일이 있을 때 써먹을 수 있을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Let’s Encrypt 인증 방식]]></title>
            <link>https://velog.io/@sangwoo-sean/Lets-Encrypt-%EC%9D%B8%EC%A6%9D-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@sangwoo-sean/Lets-Encrypt-%EC%9D%B8%EC%A6%9D-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Sun, 18 May 2025 12:34:22 GMT</pubDate>
            <description><![CDATA[<p>무료로 https 인증을 받을 수 있는 Let’s Encrypt의 인증 방식에는 3가지가 있다. 공식문서에도 나와있는 내용을 이번에 경험하게 되어 기록한다.</p>
<hr>
<h1 id="1-http-01-challenge">1. HTTP-01 Challenge</h1>
<ul>
<li>도메인 소유자가 웹 서버의 특정 경로(/.well-known/acme-challenge/)에 인증 토큰을 제공하여 도메인 소유권을 증명하는 방식입니다.</li>
<li>요구사항:<ul>
<li>포트 80에서 HTTP 요청을 처리할 수 있어야 합니다.</li>
<li>웹 서버가 외부에서 접근 가능해야 합니다.</li>
</ul>
</li>
<li>장점:<ul>
<li>설정이 간단하며 자동화가 용이합니다.</li>
<li>일반적인 웹 서버 환경에서 쉽게 구현할 수 있습니다.</li>
</ul>
</li>
<li>단점:<ul>
<li>와일드카드 인증서 발급이 불가능합니다.</li>
<li>ISP나 방화벽에 의해 포트 80이 차단된 경우 사용이 어렵습니다.</li>
</ul>
</li>
</ul>
<h1 id="2-dns-01-challenge">2. DNS-01 Challenge</h1>
<ul>
<li>도메인의 DNS 설정에 특정 TXT 레코드를 추가하여 도메인 소유권을 증명하는 방식입니다.</li>
<li>요구사항:<ul>
<li>DNS 설정을 변경할 수 있는 권한이 있어야 합니다.</li>
<li>DNS 변경 사항이 전파되기까지 시간이 소요될 수 있습니다.</li>
</ul>
</li>
<li>장점:<ul>
<li>와일드카드 인증서 발급이 가능합니다.</li>
<li>웹 서버가 없어도 인증이 가능합니다.</li>
</ul>
</li>
<li>단점:<ul>
<li>설정이 복잡하며 자동화를 위해 DNS 제공업체의 API 지원이 필요할 수 있습니다.</li>
<li>DNS 변경 사항의 전파 시간을 고려해야 합니다.</li>
</ul>
</li>
</ul>
<h1 id="3-tls-alpn-01-challenge">3. TLS-ALPN-01 Challenge</h1>
<ul>
<li>TLS의 ALPN 확장을 사용하여 도메인 소유권을 증명하는 방식입니다.</li>
<li>요구사항:<ul>
<li>포트 443에서 TLS 연결을 처리할 수 있어야 합니다.</li>
<li>특정 ALPN 프로토콜을 지원하는 서버 설정이 필요합니다.</li>
</ul>
</li>
<li>장점:<ul>
<li>HTTP를 사용하지 않고도 인증이 가능합니다.</li>
<li>일부 보안 요구사항을 만족시킬 수 있습니다.</li>
</ul>
</li>
<li>단점:<ul>
<li>설정이 복잡하며 일반적인 웹 서버 환경에서는 사용이 제한적일 수 있습니다.</li>
<li>와일드카드 인증서 발급이 불가능합니다.</li>
</ul>
</li>
</ul>
<hr>
<h1 id="❗️와일드카드-인증서-발급-시-dns-01-challenge만-가능한-이유">❗️와일드카드 인증서 발급 시 DNS-01 Challenge만 가능한 이유</h1>
<p>와일드카드 인증서(*.example.com)는 모든 하위 도메인에 대한 인증서를 한 번에 발급받을 수 있는 방식입니다. Let’s Encrypt에서는 보안상의 이유로 와일드카드 인증서 발급 시 DNS-01 Challenge만을 허용합니다.</p>
<p>이유:</p>
<ul>
<li>HTTP-01이나 TLS-ALPN-01 방식은 특정 서브도메인에 대한 소유권만을 증명할 수 있습니다.</li>
<li>와일드카드 인증서는 모든 하위 도메인에 대한 소유권을 증명해야 하므로, DNS 수준에서의 인증이 필요합니다.</li>
</ul>
<p>따라서, 와일드카드 인증서를 발급받기 위해서는 _acme-challenge.example.com에 특정 TXT 레코드를 추가하여 DNS-01 Challenge를 완료해야 합니다.</p>
<hr>
<p>📊 인증방식 비교</p>
<table>
<thead>
<tr>
<th>인증 방식</th>
<th>포트 요구사항</th>
<th>와일드카드 지원</th>
<th>자동화 용이성</th>
<th>주요 특징</th>
</tr>
</thead>
<tbody><tr>
<td>HTTP-01</td>
<td>80</td>
<td>❌</td>
<td>✅ 쉬움</td>
<td>웹 서버에 파일 생성으로 인증 수행</td>
</tr>
<tr>
<td>DNS-01</td>
<td>없음</td>
<td>✅</td>
<td>⚠️ 중간 (API 필요)</td>
<td>DNS에 TXT 레코드 추가로 인증</td>
</tr>
<tr>
<td>TLS-ALPN-01</td>
<td>443</td>
<td>❌</td>
<td>⚠️ 복잡</td>
<td>TLS 핸드셰이크로 인증 수행</td>
</tr>
</tbody></table>
<hr>
<p>✅ 결론 및 권장 사항</p>
<ul>
<li>일반적인 웹 서버 환경: HTTP-01 Challenge를 사용하는 것이 간단하고 효율적</li>
<li>와일드카드 인증서 필요: DNS-01 Challenge를 사용해야 하며, DNS 제공업체의 API를 활용한 자동화를 고려해야함</li>
<li>특수한 보안 요구사항: TLS-ALPN-01 Challenge를 사용할 수 있으나, 설정이 복잡하므로 충분한 검토가 필요</li>
</ul>
<p>각 인증 방식의 특성과 요구사항을 고려하여, 자신의 환경에 가장 적합한 방법을 선택하시기 바랍니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Slack Message API with shell script]]></title>
            <link>https://velog.io/@sangwoo-sean/Slack-Message-API-with-shell-script</link>
            <guid>https://velog.io/@sangwoo-sean/Slack-Message-API-with-shell-script</guid>
            <pubDate>Thu, 23 May 2024 23:12:28 GMT</pubDate>
            <description><![CDATA[<p>slack 으로 메세지를 보내는 API를 이용하고 싶어서 API 문서를 이용해 스크립트를 만드는 과정에서의 에러를 기록한다.</p>
<p>내가 이용하고자 했던 API는 <a href="https://api.slack.com/methods/chat.postMessage">이 Slack 문서</a>에 있는 것으로, <a href="https://api.slack.com/apps">Slack Apps</a> 페이지에서 App 을 만들고 봇의 권한을 설정한 후 토큰을 발급받아 진행했다.</p>
<p>목표는 mac terminal 의 shell script 로 메세지 전송을 하는 것이었다.
가장 간단한 형태로 구현하기 위해 터미널에서 curl 명령어로 테스트를 해보았다.</p>
<p>필요한 값들은</p>
<ul>
<li>bot token: <code>xoxb-your-bot-token</code></li>
<li>channel id: <code>your-channel-id</code></li>
</ul>
<p>이 값들을 이용해 curl 명령어를 아래와 같이 작성해 메세지 발송에 성공하였다.</p>
<pre><code>curl -X POST https://slack.com/api/chat.postMessage \
-H &#39;Authorization: Bearer xoxb-your-bot-token&#39; \
-H &#39;Content-Type: application/json&#39; \
-d &#39;{
    &quot;channel&quot;: &quot;your-channel-id&quot;,
    &quot;text&quot;: &quot;Hello, this is a test message from the Slack API!&quot;
}&#39;</code></pre><p>이 스크립트를 그대로 아래와같은 executable bash shell script 로 작성해 실행해보았다.</p>
<pre><code class="language-bash">#!/bin/bash

# Variables
OAUTH_TOKEN=&quot;xoxb-your-bot-token&quot;
CHANNEL_ID=&quot;your-channel-id&quot;
MESSAGE=&quot;Hello, this is a test message from the Slack API!&quot;

# Function to post a message to Slack
post_message_to_slack() {
  curl -X POST https://slack.com/api/chat.postMessage \
  -H &#39;Authorization: Bearer $OAUTH_TOKEN&#39; \
  -H &#39;Content-Type: application/json&#39; \
  -d &#39;{
    &quot;channel&quot;: &quot;$CHANNEL_ID&quot;,
    &quot;text&quot;: &quot;$MESSAGE&quot;
  }&#39;
}</code></pre>
<p>그런데 결과는 계속 <code>invalid_auth</code> 가 나왔다.</p>
<p>GPT에게 해답을 물었고, 원인은 single quote 와 double quote 의 차이였다. script 에서 header 를 넣어주는 부분을 쌍따옴표로 바꿔주니 성공했다.</p>
<pre><code class="language-bash">#!/bin/bash

# Variables
OAUTH_TOKEN=&quot;xoxb-your-bot-token&quot;
CHANNEL_ID=&quot;your-channel-id&quot;
MESSAGE=&quot;Hello, this is a test message from the Slack API!&quot;

# Function to post a message to Slack
post_message_to_slack() {
  curl -X POST https://slack.com/api/chat.postMessage \
  -H &quot;Authorization: Bearer $OAUTH_TOKEN&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &quot;{
    \&quot;channel\&quot;: \&quot;$CHANNEL_ID\&quot;,
    \&quot;text\&quot;: \&quot;$MESSAGE\&quot;
  }&quot;
}</code></pre>
<p>터미널에서는 curl 명령어에서 홑따옴표를 쌍따옴표로 바꿔 적용이 가능한 것 같은데, shell script 에서는 엄격하게 쌍따옴표로 해주어야 성공하는 것 같았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Database] Materialized View]]></title>
            <link>https://velog.io/@sangwoo-sean/Database-Materialized-View</link>
            <guid>https://velog.io/@sangwoo-sean/Database-Materialized-View</guid>
            <pubDate>Wed, 15 May 2024 13:24:28 GMT</pubDate>
            <description><![CDATA[<p><strong>View</strong>는 데이터를 저장하지 않지만 액세스할 때마다 기본 테이블에서 동적으로 결과를 생성하여 최신 데이터를 보장하지만 복잡한 쿼리의 경우 성능이 저하될 수 있는 가상 테이블입니다. 이와 대조적으로 <strong>Materialized View</strong>는 SQL 쿼리 결과를 물리적으로 저장하므로 데이터를 최신 상태로 유지하기 위해 정기적인 업데이트가 필요하고 추가 저장 공간을 소비하는 대신 더 빠른 데이터 액세스를 제공하여 쿼리 성능을 향상시킵니다. 둘 사이의 선택은 데이터 최신성, 시스템 성능 및 리소스 관리에 대한 특정 요구 사항에 따라 달라집니다.</p>
<h2 id="1-저장-및-실행">1. 저장 및 실행</h2>
<p><strong>View</strong>
기본적으로 가상 테이블입니다. 데이터를 물리적으로 저장하는 것이 아니라 SQL 쿼리를 정의로 저장합니다. View가 쿼리되면 데이터베이스는 매번 데이터베이스의 실제 테이블에 대해 기본 SQL 쿼리를 실행합니다. 즉, View는 항상 최신 데이터를 표시하지만 액세스할 때마다 쿼리를 처음부터 계산해야 합니다.</p>
<p><strong>Materialized View</strong>
SQL 쿼리의 결과를 데이터베이스에 물리적으로 저장합니다. 이 저장은 Materialized View가 생성되거나 새로 고쳐질 때 발생합니다. 결과가 미리 계산되어 저장되므로 Materialized View에서 데이터에 액세스하는 것이 View에서보다 훨씬 빠릅니다. 특히 복잡한 쿼리의 경우 더욱 그렇습니다.</p>
<h2 id="2-성능-및-효율성">2. 성능 및 효율성</h2>
<p><strong>View</strong>
표시되는 데이터가 항상 최신인지 확인하지만, 계산 비용이 많이 들거나 큰 데이터 세트가 포함된 쿼리의 경우에는 보기에 액세스할 때마다 이러한 작업을 수행해야 하므로 효율성이 떨어질 수 있습니다.</p>
<p><strong>Materialized View</strong>
읽기 작업에 더 나은 성능을 제공하며 기본 데이터가 자주 변경되지 않지만 빠르고 자주 액세스해야 하는 경우 유용합니다. 따라서 응답 시간이 중요한 보고서, 대시보드 및 데이터 분석 애플리케이션에 이상적입니다.</p>
<h2 id="3-데이터-최신성-및-유지-관리">3. 데이터 최신성 및 유지 관리</h2>
<p><strong>View</strong>
기본 데이터의 현재 상태를 실시간으로 반영하므로 항상 최신 상태입니다. 기본 테이블을 유지 관리하는 것 이상으로 데이터를 최신 상태로 유지하는 측면에서 유지 관리가 필요하지 않습니다.</p>
<p><strong>Materialized View</strong>
데이터의 정확성을 유지하려면 정기적인 업데이트 또는 새로 고침이 필요합니다. 새로 고침 전략은 일정에 따라(예: 야간) 발생하거나 특정 이벤트(예: 기본 테이블 업데이트)에 의해 트리거되도록 설정할 수 있습니다. 이러한 유지 관리에는 데이터 최신 상태와 시스템 성능 및 리소스 사용량의 균형을 맞추기 위한 세심한 관리가 필요합니다.</p>
<h2 id="요약">요약</h2>
<p>전반적으로 View 또는 Materialized View 사용 간의 선택은 데이터 최신성, 쿼리 성능 및 시스템 리소스 관리에 대한 특정 요구 사항에 따라 달라집니다. 각각은 서로 다른 목적으로 사용되며 다양한 조건에서 이점을 제공합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[성과를 측정하는 7가지 지표]]></title>
            <link>https://velog.io/@sangwoo-sean/%EC%84%B1%EA%B3%BC%EB%A5%BC-%EC%B8%A1%EC%A0%95%ED%95%98%EB%8A%94-8%EA%B0%80%EC%A7%80-%EC%A7%80%ED%91%9C</link>
            <guid>https://velog.io/@sangwoo-sean/%EC%84%B1%EA%B3%BC%EB%A5%BC-%EC%B8%A1%EC%A0%95%ED%95%98%EB%8A%94-8%EA%B0%80%EC%A7%80-%EC%A7%80%ED%91%9C</guid>
            <pubDate>Tue, 09 Jan 2024 13:36:39 GMT</pubDate>
            <description><![CDATA[<h1 id="1-암묵지valid-tacit-knowledge와-명목지valid-explicit-knowledge">1. 암묵지(Valid Tacit Knowledge)와 명목지(Valid Explicit Knowledge)</h1>
<p>그냥 지식이면 안된다. 유효(Valid)한 지식이어야 한다. 또한 분명히 드러나는 명목지 뿐 아니라 직관의 영역에 속하는 암묵지도 중요하다. </p>
<h1 id="2-수련practice">2. 수련(Practice)</h1>
<p>지식을 배우기만 하는 것이 아니라, 꾸준히 수련해서 습득해야한다.</p>
<h1 id="3-에너지energy">3. 에너지(Energy)</h1>
<p>지식을 배우고 수련했다면, 그걸 실제 업무에 적용할 에너지도 있어야 할 것이다. 학습을 할 에너지, 업무를 할 에너지, 소통 할 에너지 등 모두 포함이라고 볼 수 있다.</p>
<h1 id="4-습관habit">4. 습관(Habit)</h1>
<p>지식을 쌓고, 수련하며 더 나은 방향으로 나아가게끔 하는 습관이 필요하다. 그러려면 습관을 잘 설계하는 것이 중요하다.</p>
<h1 id="5-도구tool">5. 도구(Tool)</h1>
<p>흔히 생각하는 하드웨어, 소프트웨어 뿐만 아니라 메모나 머리속으로 정리, 회고하는 인지적인 방법도 포함한다. 그러한 도구들을 잘 다룰 수 있어야 한다.</p>
<h1 id="6-메타인지meta-cognition">6. 메타인지(Meta-cognition)</h1>
<p>지금 내 상황과 문제, 성과가 어떤지 객관화 하여 볼 수 있어야 한다. 그래야 정확히 측정하고 어떤 것에 개선이 필요한지 알 수 있기 때문이다.</p>
<h1 id="7-사회적-자본social-capital">7. 사회적 자본(Social Capital)</h1>
<p>내가 어려움을 겪고 있을 때 나를 도와줄 수 있는 사람이 주위에 있는가?
내가 어떤 일을 진행하려고 할 때 추진력을 더해주는 사람이 주위에 있는가?</p>
<hr>
<p>성과는 위 항목들을 곱해서 측정할 수 있고, 이를 성과 측정 공식이라고도 한다.
각 항목별로 나에게 점수를 매겨보고, 어떤 점을 보완해야하는지 생각해 보자.</p>
<p>ref)
<a href="https://www.ac2.kr/">AC2(애자일 코치 squared)</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Scala] WartRemover]]></title>
            <link>https://velog.io/@sangwoo-sean/Scala-WartRemover</link>
            <guid>https://velog.io/@sangwoo-sean/Scala-WartRemover</guid>
            <pubDate>Sat, 11 Nov 2023 06:12:25 GMT</pubDate>
            <description><![CDATA[<p>scala 의 lint tool 에는 대표적으로 Scalastyle, Wartremover, Scalafix 등이 있다.</p>
<p>그 중 Wartremover 에 대해 알아보자.</p>
<p><code>wart</code> 란 &#39;사마귀&#39; 라는 뜻이다.
사마귀 제거제 라는 이름부터가 lint 를 확실하게 해줄 것 같다.</p>
<blockquote>
<p>WartRemover: a flexible scala linter</p>
</blockquote>
<p><a href="https://www.wartremover.org/">공식 홈페이지</a>에 scala linter 라고 나와있다.
끔찍한 코드를 제거해서 코드작성시 고통을 덜어준다고 한다.</p>
<h1 id="setup">Setup</h1>
<p>wartremover 를 설치하려면
<code>project/plugins.sbt</code> 파일에 아래 라인을 추가해 플러그인을 추가해야 한다.
버전은 <a href="https://mvnrepository.com/artifact/org.wartremover/wartremover">메이븐 저장소</a>에서 확인 가능하다.</p>
<pre><code class="language-java">addSbtPlugin(&quot;org.wartremover&quot; % &quot;sbt-wartremover&quot; % &quot;3.1.5&quot;)</code></pre>
<p>설치는 끝났다. 이제 <code>built.sbt</code> 파일에 lint rule 을 설정할 차례다.</p>
<p>기본적으로 모든 error, warning 은 꺼져있다.</p>
<p>원하는 rule 을 원하는 level 로 사용하기 위해 아래와 같이 룰을 추가해주어야 한다.</p>
<pre><code class="language-java">wartremoverErrors ++= Warts.unsafe //unsafe 옵션을 error 레벨로 on

wartremoverErrors ++= Warts.all //모든 옵션을 error 레벨로 on

wartremoverWarnings ++= Warts.all //모든 옵션을 warning 레벨로 on</code></pre>
<p>옵션의 커스터마이징을 위해서 아래와 같은 설정도 할 수 있다.</p>
<pre><code class="language-java">wartremoverErrors ++= Warts.allBut(Wart.Any, Wart.Nothing, Wart.Serializable) // params 로 넘겨진 내용을 제외하고 모두 검사

wartremoverWarnings += Wart.Nothing // 모든 warning off

wartremoverWarnings ++= Seq(Wart.Any, Wart.Serializable) // Seq 안에 있는 옵션들만 on</code></pre>
<h1 id="사용">사용</h1>
<p>옵션은 켜고싶지만 특정 코드 라인에 대해서만 린팅을 안하고 싶을 때는 코드 라인 위에 어노테이션을 추가해 끌 수 있다.</p>
<pre><code class="language-java">@SuppressWarnings(Array(&quot;org.wartremover.warts.Var&quot;, &quot;org.wartremover.warts.Null&quot;))
var foo = null</code></pre>
<p><code>build.sbt</code> 파일에 특정 디렉토리를 검사하지 않도록 설정할 수 있다.</p>
<pre><code class="language-java">wartremoverExcluded += baseDirectory.value / &quot;src&quot; / &quot;main&quot; / &quot;scala&quot; / &quot;SomeFile.scala&quot;
wartremoverExcluded += sourceManaged.value</code></pre>
<p>var, while, return 등을 금지하는 다양한 옵션들이 있다.</p>
<p>모든 옵션들은 <a href="https://www.wartremover.org/doc/warts.html">공식 문서</a>에서 살펴볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[scala] Covariant vs Contravariant]]></title>
            <link>https://velog.io/@sangwoo-sean/scala-Covariant-vs-Contravariant</link>
            <guid>https://velog.io/@sangwoo-sean/scala-Covariant-vs-Contravariant</guid>
            <pubDate>Sat, 04 Nov 2023 07:36:10 GMT</pubDate>
            <description><![CDATA[<p>흔히 공변성, 반변성 이라고 번역되는 <code>covariant</code>, <code>contravaiant</code> 에 대해 알아보겠습니다.</p>
<h1 id="사전적-의미">사전적 의미</h1>
<p><code>variant</code> 의 사전적 뜻은 변종, 이형(異形) 입니다.</p>
<p><code>~와 같이</code> 라는 뜻의 <code>co</code> 와 붙은 <code>covariant</code> 는 같이 변한다(공변)는 뜻입니다.</p>
<p><code>~와 반대로</code> 라는 뜻의 <code>contra</code> 와 붙은 <code>contravariant</code>는 반대로 변한다(반변)는 뜻이겠죠.</p>
<h1 id="up-casting-down-casting">Up Casting, Down Casting</h1>
<p><img src="https://velog.velcdn.com/images/sangwoo-sean/post/62852dc5-56c1-4c1a-98c9-e96b409a9974/image.png" alt=""></p>
<p>스칼라 타입 다이어그램과 같이 보면</p>
<p>모든 타입의 SuperType(조상)인 Any가 있고, 모든 타입의 SubType(자손)인 Nothing 이 있습니다.</p>
<p>SuperType 으로 갈수록 <code>-</code>, SubType으로 갈수록 <code>+</code> 라고 표현하겠습니다.</p>
<p>객체를 캐스팅할 때 <code>-</code> 방향으로 캐스팅하면 Up Casting 이라고 하고, <code>+</code> 방향으로 캐스팅하면 Down Casting 이라고 하죠.</p>
<pre><code class="language-java">abstract class Food
case class Egg()       extends Food
case class BoiledEgg() extends Egg()

val food: Food = Egg() //upcasting
val boiledEgg: BoiledEgg = Egg() // downcasting error!</code></pre>
<p>음식, 계란, 삶은계란을 예로 들면
계란을 음식에 할당(업캐스팅)할 수 있지만
삶은계란은 (날)계란에 할당(다운캐스팅)할 수 없는게 일반적입니다.</p>
<h1 id="covariant">Covariant</h1>
<p>스칼라에서 Covariant 는 <code>[+T]</code> 라는 제네릭으로 표현됩니다.</p>
<p><code>covariant</code> 는 업캐스팅과 비슷해서 이해하기가 쉽습니다.
흔히 보는 <code>List[+T]</code>도 <code>covariant</code> 이기 때문에 직관적이죠.
객체와 제네릭이 같은 방향으로 변하면
즉, <code>T</code>를 업캐스팅해서 <code>T1</code> 이 되었을 때 <code>List[T]</code>가 <code>List[T1]</code> 이 된다면 <code>covariant</code> 한 것입니다.
코드로 예시를 들어보면 아래와 같습니다.</p>
<pre><code class="language-java">class Box[+T]

val upcast1: Box[Food] = new Box[Egg]
val upcast2: Box[Egg]  = new Box[BoiledEgg]
val upcast3: Box[Food] = new Box[BoiledEgg]
val downcast1: Box[Egg]       = new Box[Food] //error!
val downcast2: Box[BoiledEgg] = new Box[Egg] //error!
val downcast3: Box[BoiledEgg] = new Box[Food] //error!</code></pre>
<p>음식을 담는 상자는 계란을 담을 수 있지만,
삶은계란 전용 상자는 모든 음식을 담을 수 없는 것과 마찬가지죠.</p>
<h1 id="contravariant">Contravariant</h1>
<p>스칼라에서 Contravariant 는 <code>[-T]</code> 라는 제네릭으로 표현됩니다.</p>
<p><code>contravariant</code> 는 다운캐스팅과 유사합니다.
<code>T</code>를 업캐스팅해서 <code>T1</code> 이 되었을 때 <code>List[T1]</code>가 <code>List[T]</code> 이 된다면 <code>contravariant</code> 한 것입니다.
하지만 이는 직관적으로 이해하기가 어렵습니다.
코드 예시를 보겠습니다.</p>
<pre><code class="language-java">val upcast1: Eater[Food]        = new Eater[Egg] //error!
val upcast2: Eater[Egg]         = new Eater[BoiledEgg] //error!
val upcast3: Eater[Food]        = new Eater[BoiledEgg] //error!
val downcast1: Eater[Egg]       = new Eater[Food]
val downcast2: Eater[BoiledEgg] = new Eater[Egg]
val downcast3: Eater[BoiledEgg] = new Eater[Food]</code></pre>
<p>모든음식을 먹을 수 있는 사람은 삶은계란을 먹을(consume) 수 있지만,
삶은계란만 먹을 수 있는 사람이 모든 음식을 먹을 수 있는 건 아니죠.</p>
<p>이처럼 <code>contravariant</code> 는 consume 의 측면에서 생각하면 쉽습니다.</p>
<h1 id="언제-사용해야-할까">언제 사용해야 할까?</h1>
<p>개념적으로 접근하자면 클래스가 특정 타입의 출력을 생성하고 그 타입의 인스턴스를 변형하거나 소비(consume) 한다면 Contravariant를 선택할 수 있다. 이는 주로 매개변수 타입에서 일반적이다.
그렇지 않고 특정 타입의 인스턴스를 소비하지 않는다면 Covariant 를 선택할 수 있다. 이는 주로 반환 타입에서 일반적이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[맞춤법을 지켜야 하는 이유]]></title>
            <link>https://velog.io/@sangwoo-sean/%EB%A7%9E%EC%B6%A4%EB%B2%95%EC%9D%84-%EC%A7%80%EC%BC%9C%EC%95%BC-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@sangwoo-sean/%EB%A7%9E%EC%B6%A4%EB%B2%95%EC%9D%84-%EC%A7%80%EC%BC%9C%EC%95%BC-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Tue, 03 Oct 2023 13:54:48 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>저는 맞춤법에 대해서 주위 사람들보다 민감한 편입니다. 틀린 맞춤법을 보면 매우 신경이 쓰이죠. 친한 친구들과 있는 단체 채팅방에서는 꼭 친구들의 맞춤법을 교정해줍니다. 그럴 때면 항상 친구들에게 한소리를 듣곤 하죠.</p>
<p>맞춤법을 엄격하게 지키는것을 귀찮아하는 사람들이 많습니다. 그게 습관이 되어, 처음에는 귀찮아서 대충 쓰다가 나중에는 정말 몰라서 틀리게 되는 사람도 많습니다. 이렇게 귀찮은 맞춤법, 대충 써도 될 법 한데 왜 중요할까요?</p>
<h1 id="맞춤법이-중요한-이유">맞춤법이 중요한 이유</h1>
<p>만약 대표이사나 의사결정자들이 보는 보고서와 자료에 잘못된 내용(오타, 오기, 맞춤법/문법오류)이 기재되어 있다면, 그들이 그 보고서를 신뢰할 수 있을까요? 권위자가 쓴 책이나 글에 맞춤법이나 문법 오류가 있다면 아무리 권위가 높은 사람이 쓴 글이라도 신뢰가 생길까요? 극단적 예시로, 예상이익에 0 하나를 잘못해서 덧붙였다면 (1000000000 10000000000 , 표시 안할 경우) 잘못된 데이터를 근거로 내린 의사결정이 어떤 결과를 초래할지 생각만 해도 끔찍합니다.</p>
<p>맞춤법, 띄어쓰기 정확하다고 그게 자랑거리는 아닙니다. 그러나 그 반대의 경우, 즉 틀리게 쓰는 것을 치욕으로 생각하지 않고 더 나아가 틀린 것을 지적받으면 반발하는 심리가 문제죠. 그로 인해 틀린 것을 틀렸다라고 말하지 않는 분위기가 형성되고, 이런 현상은 비단 글쓰기만의 문제가 아니고 사회의 모든 부문에 광범위하게 번져가고 있는 듯 합니다.</p>
<p>주변 사람들이 쓴 글들의 맞춤법을 꼼꼼하게 고쳐주는 사람들은 어쩌면, 그 사람이 갖고 있는 문장의 무게를 지켜주고, 그 사람 자신의 무게를 지켜주고자 하는 상당히 이타적인 사람들, 그리고 자기중심성을 어느정도 이상 벗어나 있는 성숙한 사람들일지도 모릅니다.</p>
<p>다만 표준말이나 맞춤법 등의 언어사용법은 한 사회에서 글을 보다 많은 사람들이 이해할 수 있도록 하기 위해 만들어진 것이기 때문에 일률적으로 어문규정 자체에 집착하는 방향으로 가서는 안되겠습니다.</p>
<p>지나친 집착은 다채롭게 생성되고 변화하는 언어의 생동감을 훼손시킬 수도 있습니다. 때로는 맞춤법을 지키지 않음으로서 신조어가 창조되기도 하고, 새로운 문화가 생겨나기도 하며, 언어가 더 발전하는 결과를 가져오기도 합니다.</p>
<h1 id="맺음말">맺음말</h1>
<p>저는 모든 맞춤법을 알고 완벽하게 사용해야한다고 생각하지 않습니다. 저도 모르는 맞춤법이 많아요. 다만 맞춤법뿐 아니라 모든 분야에서 모르거나 틀린 것이 있을 때 지적하고 인정하는 것, 생소하거나 헷갈리는 것이 있을 때 그것에 대해 알아보는 것이 불편하지 않은 사람들이 주위에 더 많아졌으면 하는 바람입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[상상으로 만들어낸 부자]]></title>
            <link>https://velog.io/@sangwoo-sean/%EC%83%81%EC%83%81%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%82%B8-%EB%B6%80%EC%9E%90</link>
            <guid>https://velog.io/@sangwoo-sean/%EC%83%81%EC%83%81%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%82%B8-%EB%B6%80%EC%9E%90</guid>
            <pubDate>Sun, 17 Sep 2023 07:21:31 GMT</pubDate>
            <description><![CDATA[<p>우리는 뉴스, 유튜브, 인스타그램 등의 매체에서 부자들의 소비를 자연스럽게 접한다. 운동선수가 몇백억짜리 별장과 요트를 산다거나, 재벌2세가 고급 스포츠카를 몇대씩 소유했다는 얘기도 쉽게 접할 수 있다. 고급 호텔에서 호화로운 휴가를 즐긴다거나 명품 구매로 FLEX 했다는 뉴스들도 있다. 대부분의 사람들은 그런것이 &quot;부자들의 소비&quot; 라고 생각한다.</p>
<p>하지만 그런 소비는 사실 &quot;<strong>사람들이 상상으로 만들어낸 부자들</strong>&quot;의 생활방식이다. 사람들은 그런 소비가 정상이라고 여기도록 길들여진다. 그에 반해 흔히 보는 일반인의 평범한 소비는 시시하다고 여긴다. 그리고 부자들의 소비와 생활습관을 따라하려 안간힘을 쓴다. 결과적으로 부자가 되는 길에서 더 멀어진다. 사람들이 그런 소비를 하게 되는 이유는 <strong>&quot;경제적인 능력을 갖추는 것&quot; 보다는 &quot;경제 능력을 상징하는 물건을 구입하는 것&quot;이 훨씬 쉽기 때문이다</strong>.</p>
<p>사실 진짜 부자들은 그러한 소비를 하지 않는다. 부자들을 인터뷰하기 위해 고급 요리와 비싼 와인을 준비해두고 부자들을 초대했지만, 부자들은 고급 요리에는 손도 대지 않았다는 일화가 있다. 대부분의 부자들은 우리가 상상하는 부유층 사람들의 소비를 하지 않는다. 그들은 부를 축적하고 유지하기 위해 끊임없이 절약하고 투자한다.</p>
<p>매일 조깅하는 사람은 건강해져서 더이상 조깅을 할 필요가 없어보일 정도로 건강하다. 매일이 쌓여 그사람의 건강을 만든 것이다. 대부분의 우리는 건강을 위해 운동을 해야함을 알지만 실천에 옮길 만큼 절제력이 따라주지 않는다.</p>
<p>소비습관을 가꾸는 것도 마찬가지 아닐까. 재정 건강을 유지하기 위해 꾸준히 절약과 투자를 하는 습관을 기르는 것이 중요한 것이다. <strong>부자들은 이미 부유해서 더이상 절약할 필요가 없어보이지만, 그들의 소비습관이 부를 축적했고 유지하게 한다</strong>는 것을 잊지 말아야겠다.</p>
<hr>
<p>&quot;이웃집 백만장자&quot;를 읽고</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ZIO] .conf 변수 불러오기 - Config]]></title>
            <link>https://velog.io/@sangwoo-sean/ZIO-.conf-%EB%B3%80%EC%88%98-%EB%B6%88%EB%9F%AC%EC%98%A4%EA%B8%B0-Config</link>
            <guid>https://velog.io/@sangwoo-sean/ZIO-.conf-%EB%B3%80%EC%88%98-%EB%B6%88%EB%9F%AC%EC%98%A4%EA%B8%B0-Config</guid>
            <pubDate>Sat, 16 Sep 2023 06:33:26 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>프로젝트를 하다 보면 API Key와 같은 민감한 정보들을 따로 관리하는 파일에 저장하고 앱에서 불러와 사용하는 경우가 있다.</p>
<p>테스트 코드에서 이를 불러오는 테스트를 작성해 보았다.</p>
<h1 id="1-테스트-코드-내에-직접-세팅하기">1. 테스트 코드 내에 직접 세팅하기</h1>
<pre><code class="language-java">case class MyConfig(value: Option[String])

object MyConfig {
  val config: Config[MyConfig] = DeriveConfig.deriveConfig[MyConfig].nested(&quot;my&quot;, &quot;env&quot;)
}

object ConfigSpec extends ZIOSpecDefault {

  val testConfigProviderFromMap: ZLayer[Any, Nothing, Unit] =
    Runtime.setConfigProvider(
      ConfigProvider
        .fromMap(Map(&quot;my.env.value&quot; -&gt; &quot;this is from map&quot;)))

  override def spec: Spec[TestEnvironment with Scope, Any] =
    suite(&quot;config from map&quot;)(
      test(&quot;fetch value from map&quot;) {
        for {
          config &lt;- ZIO.config[MyConfig](MyConfig.config)
        } yield assertTrue(config.value.contains(&quot;this is from map&quot;))
      }
    ).provideLayer(testConfigProviderFromMap)
}</code></pre>
<p>위의 코드에서 <code>ConfigProvider</code> 를 test suite 에 <code>provideLayer</code> 를 통해 제공해 준다.
<code>ConfigProvider</code> 는 <code>.fromMap()</code> 메서드에 Map 형태로 세팅해서 사용할 수 있다.</p>
<h1 id="2-파일에서-불러오기">2. 파일에서 불러오기</h1>
<pre><code># conf/application.conf
my.env {
    value = &quot;I am a value from conf/application.conf&quot;
}

# test/resources/application.conf
my.env {
    value2 = &quot;I am a value from test/resources/application.conf&quot;
}

# main/resources/application.conf
my.env {
    value3 = &quot;I am a value from main/resources/application.conf&quot;
}</code></pre><blockquote>
<p>프로젝트 내의 각 경로에 위와 같이 .conf 파일을 준비해둔다.</p>
</blockquote>
<pre><code class="language-java">case class MyConfig(value: Option[String], value2: Option[String], value3: Option[String], nothing: Option[String])

object MyConfig {
  val config: Config[MyConfig] = DeriveConfig.deriveConfig[MyConfig].nested(&quot;my&quot;, &quot;env&quot;)
}

object ConfigSpec extends ZIOSpecDefault {

  private val configProvider: ConfigProvider = TypesafeConfigProvider
    .fromHoconFilePath(&quot;conf/application.conf&quot;) // 여기에서 먼저 찾고
    .orElse(TypesafeConfigProvider.fromResourcePath()) // 그 다음 여기에서 찾기

  val testConfigProviderFromConfFile: ZLayer[Any, Nothing, Unit] =
    Runtime.setConfigProvider(configProvider)

  override def spec: Spec[TestEnvironment with Scope, Any] =
    suite(&quot;config from conf file&quot;)(
      test(&quot;fetch value from  conf/application.conf&quot;) {
        for {
          config &lt;- ZIO.config[MyConfig](MyConfig.config)
        } yield assertTrue(config.value.contains(&quot;I am a value from conf/application.conf&quot;))
      },
      test(&quot;fetch value from test/resources/application.conf&quot;) {
        for {
          config &lt;- ZIO.config[MyConfig](MyConfig.config)
        } yield assertTrue(config.value2.contains(&quot;I am a value from test/resources/application.conf&quot;))
      },
      test(&quot;fetch value from main/resources/application.conf&quot;) {
        for {
          config &lt;- ZIO.config[MyConfig](MyConfig.config)
        } yield assertTrue(config.value3.contains(&quot;I am a value from main/resources/application.conf&quot;))
      },
      test(&quot;fetch value doesn&#39;t exist&quot;) {
        for {
          config &lt;- ZIO.config[MyConfig](MyConfig.config)
        } yield assertTrue(config.nothing.isEmpty)
      },
    ).provideLayer(testConfigProviderFromConfFile)
}</code></pre>
<p>테스트코드를 보면 config 값을 찾는 순서는 아래와 같다.</p>
<ol>
<li>configProvider 에 지정해 둔 대로 &quot;conf/application.conf&quot; 에서 먼저 값을 찾는다.</li>
<li>없으면 &quot;test/resources/application.conf&quot; 에서 값을 찾는다.</li>
<li>없으면 &quot;main/resources/application.conf&quot; 에서 값을 찾는다.</li>
<li>없으면 없는것으로 핸들링한다.</li>
</ol>
<hr>
<h1 id="결론">결론</h1>
<p>테스트 코드에서 환경변수를 원하는 대로 세팅해서 테스트의 자유도를 높일 수 있게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Scala] ADT 에서 case class vs case object]]></title>
            <link>https://velog.io/@sangwoo-sean/Scala-sealed-trait-%EC%95%88%EC%97%90%EC%84%9C-case-class-vs-case-object</link>
            <guid>https://velog.io/@sangwoo-sean/Scala-sealed-trait-%EC%95%88%EC%97%90%EC%84%9C-case-class-vs-case-object</guid>
            <pubDate>Sun, 03 Sep 2023 13:37:30 GMT</pubDate>
            <description><![CDATA[<p>Scala에는 ADT 라는 개념이 있습니다. 저는 이걸 배우며 자바에서 흔히 쓰이는 enum이 연상되더라구요. ADT는 <code>Algebraic Data Types</code> 의 약자로, 직역하자면 <code>대수 데이터 타입</code> 입니다.</p>
<h1 id="adt를-사용하면-좋은-점">ADT를 사용하면 좋은 점</h1>
<ol>
<li><strong>표현력</strong>: ADTs를 사용하면 복잡한 도메인을 타입 안전하게 모델링할 수 있습니다.</li>
<li><strong>안전성</strong>: <code>sealed traits</code>와 함께 패턴 매칭에서 <strong>완전성</strong>을 검사할 수 있습니다. 패턴매칭에서 모든 패턴을 매칭하지 않으면 컴파일 에러가 나기 때문입니다.</li>
<li><strong>명료성</strong>: ADTs에 대한 패턴 매칭은 코드를 명확하고 유지 관리하기 쉽게 만듭니다.</li>
</ol>
<br>

<p>ADT는 scala 에서 데이터 타입을 정의할 때 <code>sealed trait</code> 을 정의하여 사용할 수 있습니다. <code>companion obejct</code> 안에 <code>case class</code>와 <code>case object</code>를 사용할 수 있습니다.</p>
<p>그렇다면 이 둘은 각기 용도와 특성이 어떻게 다르며, 언제 쓰일까요? 각 측면에서 비교해보겠습니다.</p>
<h1 id="case-class-vs-case-object">case class vs Case object</h1>
<h2 id="인스턴스화">인스턴스화</h2>
<h3 id="case-class">Case Class</h3>
<p>다양한 생성자 인수로 여러 번 인스턴스화할 수 있습니다. 일관된 구조를 가지면서 변할 수 있는 데이터를 나타낼 때 유용합니다.</p>
<pre><code class="language-java">sealed trait Animal
object Animal {
  case class Dog(name: String) extends Animal
  case class Cat(name: String) extends Animal
}
val myDog = Animal.Dog(&quot;Buddy&quot;)
val myCat = Animal.Cat(&quot;Whiskers&quot;)</code></pre>
<h3 id="case-object">Case Object</h3>
<p>단일, 싱글톤 인스턴스를 나타냅니다. 고정된 데이터 세트나 여러 인스턴스가 필요하지 않은 &quot;객체&quot;를 나타낼 때 유용합니다.</p>
<pre><code class="language-java">sealed trait 신호등
object 신호등 {
  case object 빨강 extends 신호등
  case object 초록 extends 신호등
  case object 노랑 extends 신호등
}</code></pre>
<h2 id="언제-사용할까">언제 사용할까?</h2>
<h3 id="case-class-1">Case Class</h3>
<p>데이터의 값이 다양할 때 사용됩니다.</p>
<h3 id="case-object-1">Case Object</h3>
<p>다양한 데이터를 포함할 필요가 없지만 다른 타입이나 상태를 나타내고 싶을 때 사용됩니다</p>
<h2 id="파라미터-여부">파라미터 여부</h2>
<h3 id="case-class-2">Case Class</h3>
<p>생성자 매개변수를 가질 수 있으며 각 인스턴스에 대해 다른 값을 캡슐화할 수 있습니다.</p>
<h3 id="case-object-2">Case Object</h3>
<p>생성자 매개변수를 가질 수 없습니다. 단일, 불변 인스턴스입니다.</p>
<h2 id="패턴-매칭">패턴 매칭</h2>
<p><code>case class</code>와 <code>case object</code>는 모두 패턴 매칭에 사용할 수 있습니다. 참고로 <code>sealed</code> 키워드는 모든 하위 유형이 같은 파일 내에서 정의되도록 하여 컴파일러가 완전한 패턴 매칭을 확인하도록 합니다.</p>
<pre><code class="language-java">def describeAnimal(a: Animal): String = a match {
  case Animal.Dog(name) =&gt; s&quot;A dog named $name&quot;
  case Animal.Cat(name) =&gt; s&quot;A cat named $name&quot;
}

def checkLight(l: Light): String = l match {
  case Light.Red =&gt; &quot;Stop&quot;
  case Light.Green =&gt; &quot;Go&quot;
  case Light.Yellow =&gt; &quot;Caution&quot;
}</code></pre>
<h2 id="메모리-할당">메모리 할당</h2>
<h3 id="case-class-3">Case Class</h3>
<p>모든 인스턴스화가 메모리에 새 객체를 생성합니다.</p>
<h3 id="case-object-3">Case Object</h3>
<p>단 하나의 인스턴스만 생성되므로 특정 개념이나 상태를 나타낼 때 메모리 효율적입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[챗 GPT 잘 쓰는법 (프롬프팅)]]></title>
            <link>https://velog.io/@sangwoo-sean/%EC%B1%97-GPT-%EC%9E%98-%EC%93%B0%EB%8A%94%EB%B2%95-%ED%94%84%EB%A1%AC%ED%94%84%ED%8C%85</link>
            <guid>https://velog.io/@sangwoo-sean/%EC%B1%97-GPT-%EC%9E%98-%EC%93%B0%EB%8A%94%EB%B2%95-%ED%94%84%EB%A1%AC%ED%94%84%ED%8C%85</guid>
            <pubDate>Sat, 02 Sep 2023 11:39:25 GMT</pubDate>
            <description><![CDATA[<h1 id="가이드라인">가이드라인</h1>
<p>GPT에게 가이드라인을 먼저 주고, 실제 질문은 따로 구분하여 던져주면 그 질문에 대해 먼저 제시한 가이드라인을 따라 답변을 해준다.</p>
<pre><code>Summarize the text delimited by triple backticks
into a single sentence.

\```
You should express what you want a model to do by
providing instructions that are as clear and
specific as you can possibly make them.
This will guide the model towards the desired output,
and reduce the chances of receiving irrelevant
or incorrect responses. Don&#39;t confuse writing a
clear prompt with writing a short prompt.
In many cases, longer prompts provide more clarity
and context for the model, which can lead to
more detailed and relevant outputs.
\```
</code></pre><p>위의 예시에서는 백틱으로 질문과 가이드라인을 분리했지만 구분이 명확하기만 하다면 <code>&quot;, &#39;, -, =</code> 등 어떤것으로 구분하더라도 상관 없다.</p>
<h2 id="답변-길이-제한하기">답변 길이 제한하기</h2>
<ul>
<li>Use at most 50 words.</li>
<li>Use at most 1000 charactors.</li>
<li>Use at most 3 paragraph.</li>
</ul>
<p>원하는 길이의 답변이 있다면 가이드라인에 길이 제한을 걸 수 있다.</p>
<h2 id="테이블로-보여주기">테이블로 보여주기</h2>
<ul>
<li>include a table that gives the X. </li>
<li>The table should have N columns.</li>
<li>In the first column include Y. </li>
<li>...</li>
</ul>
<p>원하는 정보를 테이블 형태로 보여주도록 지정할 수 있다.</p>
<h2 id="특정한-형식으로-답변하기">특정한 형식으로 답변하기</h2>
<ul>
<li>extract the following items from X<ul>
<li>A</li>
<li>B</li>
<li>C</li>
<li>...</li>
</ul>
</li>
</ul>
<p>답변을 특정 형식에 맞게 받고 싶을 때 사용할 수 있다. 각기 다른 데이터에 대한 답변을 일관적인 형태로 받고 싶은 경우에 유용할 수 있다.</p>
<hr>
<h1 id="패턴">패턴</h1>
<p>GPT에게 어떤 질문을 하기 전에 패턴과 같은 프롬프트를 먼저 제공하거나, 혹은 질문을 할 때 패턴과 같은 형태로 프롬프트를 제공하면 더 나은 답변을 얻을 수 있을 것이다.</p>
<h2 id="1-페르소나-패턴">1. 페르소나 패턴</h2>
<ul>
<li>Act as Persona X.</li>
<li>Perform task Y.</li>
</ul>
<p>특정 분야에 대한 질문을 하고 싶을 때 유용하게 쓰일 수 있다.
예를 들면 <code>Act as a Senior software engineer</code> 혹은 <code>Act as a Stock analyst</code> 와 같이 쓰일 수 있다. 답변의 주제를 좁힐 수 있어 답변의 퀄리티가 향상될 수 있다.</p>
<h2 id="2-청중-페르소나-패턴">2. 청중 페르소나 패턴</h2>
<ul>
<li>Explain X to me</li>
<li>Assume that I am Persona Y</li>
</ul>
<p>페르소나 패턴의 역발상 버전이라고 볼 수 있다.
예시로는 <code>Explain the importance of eating vegetables to me. Assume that I am a skeptical child.</code> 와 같이 사용할 수 있다.</p>
<h2 id="3-레시피-패턴">3. 레시피 패턴</h2>
<ul>
<li>I am trying to do a task.</li>
<li>I know I have to do A, B, and C to complete the task</li>
<li>provide a complete sequence of steps for me</li>
<li>fill in any missing steps</li>
<li>identify any unnecessary steps</li>
</ul>
<p>어떤 작업을 해야하는데, 재료는 가지고 있지만 어떤 디테일과 순서로 작업해야할 지 잘 모르는 경우 사용할 수 있다.</p>
<h2 id="4-리플렉션-패턴">4. 리플렉션 패턴</h2>
<ul>
<li>when you provide an answer, explain the reasoning and assumptions behind your answer.</li>
<li>explain your choices and address any potential limitations or edge cases.</li>
</ul>
<p>답변의 근거와 배경에 대한 정보를 같이 받아볼 수 있다.</p>
<h2 id="5-거부-차단기-패턴">5. 거부 차단기 패턴</h2>
<ul>
<li>Whenever you can&#39;t answer a question, explain why you can&#39;t.</li>
<li>provide one or more alternative wordings of the question that you could answer.</li>
</ul>
<p>GPT가 질문에 답변할 수 없다고 하는 경우가 있을 것이다. 그런 경우 이 패턴을 사용하면 답변을 얻을 수 있는 다른 질문을 제시해 준다.</p>
<h2 id="6-뒤집힌-상호작용-패턴">6. 뒤집힌 상호작용 패턴</h2>
<ul>
<li>I would like you to ask me questions to achieve X.</li>
<li>You should ask questions until condition Y is met or to achieve this goal.</li>
</ul>
<p>사용자가 질문을 생각해 내는 대신, GPT가 질문부터 생성해 내도록 하는 패턴이다. 어떤 작업을 해야 하는데, 무엇부터 해야 할 지 잘 모를 때 사용하면 유용할 것 같다. GPT 가 질문 목록을 생성해주면 그 중에서 필요한 질문들만 골라서 다시 답변하도록 하는 방식으로 사용할 수 있을 것 같다.</p>
<h2 id="7-질문-개선-패턴">7. 질문 개선 패턴</h2>
<ul>
<li>Whenever I ask a question, suggest a better version of the question to use instead.</li>
</ul>
<p>어떤 질문을 해야 하는데, 질문을 잘 하는건지 애매하거나 더 나은 질문이 떠오르지 않을 때 사용할 수 있을 것 같다.</p>
<h2 id="8-메타-언어-생성-패턴">8. 메타 언어 생성 패턴</h2>
<ul>
<li>Whenever I ask a question, suggest a better version of the question to use instead.</li>
</ul>
<p>예약어를 설정해두고 사용할 수 있는 방법이다. shell 의 alias 기능과 비슷하다고 볼 수 있다.
예를 들면 <code>When I say “tl;dr”, I mean “your response was too long, make it shorter”.</code> 와 같이 사용할 수 있다.</p>
<hr>
<p>이 외에도 유저들이 공유하고 있는 다양한 패턴들이 존재한다. 필요한 instruction 을 적절히 조합해서 사용한다면 GPT 를 생산성 향상을 위한 도구로 활용할 수 있을 것 같다.</p>
<p>ref)
<a href="https://learn.deeplearning.ai/chatgpt-prompt-eng/lesson/1/introduction">DeepLearning.AI</a>
<a href="https://www.youtube.com/watch?v=WRkig3VeRLY">노마드코더 Youtube Clip</a>
<a href="https://medium.com/@corraljrmiguel/21-prompt-patterns-you-should-know-636c931bba2a">Brunch</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ZIO] ZIO JSON Library]]></title>
            <link>https://velog.io/@sangwoo-sean/ZIO-jsonDerive</link>
            <guid>https://velog.io/@sangwoo-sean/ZIO-jsonDerive</guid>
            <pubDate>Fri, 01 Sep 2023 14:41:23 GMT</pubDate>
            <description><![CDATA[<p>ZIO JSON 이라는 ZIO Library 에서 사용할 수 있는 여러가지 기능이 있다.</p>
<h1 id="jsondiscriminator">@jsonDiscriminator</h1>
<pre><code>sealed trait Fruit
case class Banana(curvature: Double) extends Fruit
case class Apple(poison: Boolean) extends Fruit

object Fruit {
  implicit val encoder: JsonEncoder[Fruit] =
    DeriveJsonEncoder.gen[Fruit]
}

val banana: Fruit = Banana(0.5)</code></pre><p>위와 같은 코드에서 <code>banana.json</code> 은 <code>{&quot;banana&quot;:{&quot;curvature&quot;:0.5}}</code> 로 encoding 된다.</p>
<p>하지만 <code>sealed trait</code> 위에 <code>@jsonDiscriminator(&quot;type&quot;)</code> 를 붙여주면 encoding 방식이 아래와 같이 바뀐다.
<code>{&quot;type&quot;:&quot;banana&quot;, &quot;curvature&quot;:0.5}</code></p>
<h1 id="jsonderive">@jsonDerive</h1>
<p><code>@jsonDerive</code> 어노테이션을 사용하면 JsonDecoder/JsonEncoder 를 빌드타임에 만들어 준다.</p>
<pre><code>import zio.json._

@jsonDerive case class Watermelon(pips: Int)</code></pre><p>위의 코드는 아래와 같이 빌드된다.</p>
<pre><code>import zio.json._

case class Watermelon(pips: Int)

object Watermelon {
  implicit val codec: JsonCodec[Watermelon] =
    DeriveJsonCodec.gen[Watermelon]
}</code></pre><p>여기서 <code>Codec == Encoder + Decoder</code> 이다.</p>
<p>만약 Encoder 만 만들고싶은 상황이라면 <code>@jsonDerive(JsonDeriveConfig.Encoder)</code> 와 같이 옵션을 주어 사용하면 된다.</p>
<p>이는 아래와 같이 빌드 된다.</p>
<pre><code>import zio.json._

case class Watermelon(pips: Int)

object Watermelon {
  implicit val encoder: JsonEncoder[Watermelon] =
    DeriveJsonEncoder.gen[Watermelon]
}</code></pre><p>Decoder 만 만들고 싶은 경우에도 똑같이 적용된다.</p>
<p>ref)
<a href="https://zio.dev/zio-json/">ZIO Doc</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[구루에게 배우는 복리효과를 일으키는 코딩 습관]]></title>
            <link>https://velog.io/@sangwoo-sean/%EA%B5%AC%EB%A3%A8%EC%97%90%EA%B2%8C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%B3%B5%EB%A6%AC%ED%9A%A8%EA%B3%BC%EB%A5%BC-%EC%9D%BC%EC%9C%BC%ED%82%A4%EB%8A%94-%EC%BD%94%EB%94%A9-%EC%8A%B5%EA%B4%80</link>
            <guid>https://velog.io/@sangwoo-sean/%EA%B5%AC%EB%A3%A8%EC%97%90%EA%B2%8C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%B3%B5%EB%A6%AC%ED%9A%A8%EA%B3%BC%EB%A5%BC-%EC%9D%BC%EC%9C%BC%ED%82%A4%EB%8A%94-%EC%BD%94%EB%94%A9-%EC%8A%B5%EA%B4%80</guid>
            <pubDate>Fri, 01 Sep 2023 13:30:44 GMT</pubDate>
            <description><![CDATA[<p>선배 개발자와 페어프로그래밍 하며 받은 조언입니다.</p>
<h1 id="1-작업의-단위를-잘게-쪼개라">1. 작업의 단위를 잘게 쪼개라</h1>
<p>가능한 작업의 단위를 잘게 쪼개서 체크리스트를 만들어라. 각 작업의 단위는 5분~10분이면 좋다. 각각의 작업을 해결해 나갈 때 마다 과감히 체크리스트에서 지워내려가라. 타격감이 성취감으로 이어질 것이다. 만약 진행한 작업이 생각보다 길었다면 더 작은 단위로 쪼갤 수 있었는지 복기해봐라. 혹은 놓친 위험요소나 방해요소가 있지는 않았는지 복기해봐라.</p>
<h1 id="2-앞에서-하는-작업이-뒤에서-하는-작업에-도움이-되도록-쪼개라">2. 앞에서 하는 작업이 뒤에서 하는 작업에 도움이 되도록 쪼개라.</h1>
<p><code>A - B - C - D - E</code> 의 순서로 linear 하게 작업을 진행한다면, E 라는 마지막 결과물이 나오기 전까지 앞서 진행했던 A~D 의 결과를 확인할 수 없다. 결과적으로 도달하고 싶은 목표가 복잡할수록 이러한 방식은 사고 정지와 작업 지연을 불러 일으킬 확률이 높다. 또한 뒤의 작업을 진행하다가 앞의 작업을 수정해야한다거나, 필요없는 작업이었다거나 하는 경우에는 시간을 낭비했다는 것을 뒤늦게 깨닫게 될 수도 있다.</p>
<p><code>A1 - A2 - A3 - A4 - A5</code> 의 순서로 작업을 진행하자. 이게 무슨 말장난같은 소리인가? 설명하자면 이렇다. 우선 A1 만 해도 동작할 수 있는 최소한의 결과물을 만들자. 복잡한 기능은 다 떼고 가장 간단한 동작부터 시작해도 좋다. 그 후에 A2 를 만드는 그 다음 단계로 나아가자. 이 때 A1 에 기능을 수정하거나 추가하여 A2 를 만들 수 있도록 한다. 이렇게 버전 업그레이드를 해 나가는 느낌으로 A3, A4 를 만들어 나가면 훨씬 쉽고 빠르게 목표를 달성할 수 있을 것이다.</p>
<p>예시를 들자면, 하늘을 나는 자동차를 만들고 싶을 때
<code>타이어 - 몸체 - 날개 - 조립 - 완성</code> 의 과정과
<code>작동하는 엔진 - 엔진으로 움직이는 4륜 - 움직이는 자동차 - 뜨는 자동차 - 하늘을 나는 자동차</code>  의 과정 으로 비유할 수 있겠다. 맞는 비유인지는 모르겠지만..?</p>
<h1 id="3-가능한-빠르게-결과물이-나올-수-있도록-한다">3. 가능한 빠르게 결과물이 나올 수 있도록 한다.</h1>
<p>어떤 작업을 할 때, 그 작업으로 인한 결과물을 가능한 빠르게 확인할 수 있으면 좋다. 여기서의 결과물은 최종적으로 원하는 결과물이 아니라, 그보다 훨씬 단순한 형태여도 괜찮다. 결과물이 나왔다는 것은 목표 달성 조건이 명확하게 있었다는 것의 반증이다. 또한, 결과물을 달성할만큼의 작업만 하도록 집중할 수 있다. 이는 불필요한 작업을 방지하는 효과가 있다.</p>
<h1 id="4-프로젝트-코드가-빌드되지-않는-시간을-최소화해라">4. 프로젝트 코드가 빌드되지 않는 시간을 최소화해라.</h1>
<p>이 조언은 정적 타입 언어에만 적용될 수도 있다.
코드를 수정하면 기존 코드가 깨지기 시작하며 프로젝트 코드가 빌드되지 않을 것이다. 깨진 코드들을 전부 고치고 나서야 다시 빌드가 될 것이다. 이 사이의 시간이 길어질수록 좋지 않다. 작업이 길어지고있다는 증거다. 기능을 완벽하게 완성하지 못했다고 하더라도 (= 동작이 실패하는 경우가 있더라도) 일단 코드가 죽지 않고 동작할 수 있는 상태를 만들자. 다만, 동작이 실패한다면 의도하거나 예측한대로 실패하는지를 확인해가며 작업해야 할 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ZIO] ZIOAspect.annotated]]></title>
            <link>https://velog.io/@sangwoo-sean/ZIO-ZIOAspect.annotated</link>
            <guid>https://velog.io/@sangwoo-sean/ZIO-ZIOAspect.annotated</guid>
            <pubDate>Sat, 26 Aug 2023 12:25:27 GMT</pubDate>
            <description><![CDATA[<p><code>ZIOAspect.annotated</code> 를 사용하면 effect 에 몇 가지 컨텍스트 정보(ex. correlation_id)를 <strong>주석</strong>으로 달 수 있습니다.</p>
<p>이 정보는 동일한 부모 fiber 에서 생성된 모든 fiber 에 전파되는 <code>FiberRef</code> 내부에 저장됩니다.</p>
<p>각 fiber 에는 고유한 어노테이션 set 가 있습니다.</p>
<p>Fiber 내부에서 로깅할 때 로깅 서비스는 파이버의 특정 어노테이션을 사용하여 로그 메시지를 생성합니다.</p>
<pre><code class="language-java">import zio._

object MainApp extends ZIOAppDefault {

  def handleRequest(request: String) =
    for {
      _ &lt;- ZIO.log(s&quot;Received request.&quot;)
      _ &lt;- ZIO.unit // do something with the request
      _ &lt;- ZIO.log(s&quot;Finished processing request&quot;)
    } yield ()

  def run =
    for {
      _ &lt;- ZIO.log(&quot;Hello World!&quot;)
      _ &lt;- ZIO.foreachParDiscard(List((&quot;req1&quot;, &quot;1&quot;), (&quot;req2&quot;, &quot;2&quot;), (&quot;req3&quot;, &quot;3&quot;))){ case (req, id) =&gt;
        handleRequest(req) @@ ZIOAspect.annotated(&quot;correlation_id&quot;, id)
      }
      _ &lt;- ZIO.log(&quot;Goodbye!&quot;)
    } yield ()

}</code></pre>
<p>위 코드를 실행하면 결과 로그는 아래와 같이 출력됩니다.</p>
<pre><code class="language-console">message=&quot;Hello World!&quot;
message=&quot;Received request.&quot; correlation_id=2
message=&quot;Received request.&quot; correlation_id=1
message=&quot;Received request.&quot; correlation_id=3
message=&quot;Finished processing request.&quot; correlation_id=3
message=&quot;Finished processing request.&quot; correlation_id=1
message=&quot;Finished processing request.&quot; correlation_id=2
message=&quot;Goodbye!&quot;</code></pre>
<p>요약하자면, <code>ZIOAspect.annotated</code> 는 로그 뒤에 부가적으로 출력 될 정보를 설정해 둘 수 있는 기능입니다.</p>
<p>ref)
<a href="https://zio.dev/reference/state-management/fiberref/#use-cases">ZIO Doc</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SBT] sbt-revolver 로 빠르게 개발하기]]></title>
            <link>https://velog.io/@sangwoo-sean/SBT-sbt-revolver-%EB%A1%9C-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sangwoo-sean/SBT-sbt-revolver-%EB%A1%9C-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 20 Aug 2023 06:08:20 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>scala 로 개발을 하면 SBT 라는 빌드 툴을 이용하게 된다.</p>
<p>주로 <code>sbt run</code> 과 같은 키워드를 통해 애플리케이션 서버를 동작시키곤 한다.</p>
<p>그런데 터미널에서 <code>sbt run</code> 을 입력하면 해당 터미널에서 sbt 가 foreground 로 동작한다.</p>
<p>그 말은 즉, 다른 액션을 하려면 다른 터미널을 켜서 수행하거나 애플리케이션을 종료해야 한다는 것이다.</p>
<p>이러한 방법이 불편했던 건지 <a href="https://github.com/spray">spray</a> 라는 github organization 에서 <code>sbt-revolver</code> 라는 라이브러리(좀더 정확히는 플러그인) 을 오픈소스로 개발했다.</p>
<h1 id="sbt-revoler-를-쓰면-좋은-점">sbt-revoler 를 쓰면 좋은 점</h1>
<p>이 플러그인을 사용하면 애플리케이션을 동작시키고 나서 동일 터미널에서 여러가지 다른 동작이 가능하다.
예를 들면 애플리케이션을 다시 켜기 위해 종료하는 수고가 줄어들 수 있고,
특히 멀티 모듈 프로젝트에서는 여러 모듈들을 한 터미널에서 수행할 수 있다.
이런것이 가능한 이유는 애플리케이션을 forked JVM에서 실행시키기 때문이다.</p>
<h1 id="사용-방법">사용 방법</h1>
<p><code>project/plugins.sbt</code> 설정에 아래와 같이 추가해주면 된다. (만약 없다면 파일을 생성하자)
<code>addSbtPlugin(&quot;io.spray&quot; % &quot;sbt-revolver&quot; % &quot;0.10.0&quot;)</code>
이 플러그인은 auth plugin 이기 때문에 build.sbt 에 따로 추가설정을 해줄 필요가 없다.</p>
<p>설정 및 사용 준비가 끝났다면, sbt shell 에서 <code>reStart</code> 키워드를 입력하면 애플리케이션을 실행할 수 있다.</p>
<p><code>reStart</code> 를 입력하는 시점에 애플리케이션이 이미 실행중이었다면먼저 애플리케이션을 멈추고 다시 시작하게 된다.</p>
<p>애플리케이션을 완전히 멈추고싶다면 <code>reStop</code> 키워드를 입력하면 된다.</p>
<h1 id="ref">Ref.</h1>
<p><a href="https://github.com/spray/sbt-revolver">sbt-revolver github</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[책 읽으면 기분 좋아지는 이유]]></title>
            <link>https://velog.io/@sangwoo-sean/%EC%B1%85-%EC%9D%BD%EC%9C%BC%EB%A9%B4-%EA%B8%B0%EB%B6%84-%EC%A2%8B%EC%95%84%EC%A7%80%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@sangwoo-sean/%EC%B1%85-%EC%9D%BD%EC%9C%BC%EB%A9%B4-%EA%B8%B0%EB%B6%84-%EC%A2%8B%EC%95%84%EC%A7%80%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 13 Aug 2023 04:52:40 GMT</pubDate>
            <description><![CDATA[<p>책을 읽으면 기분이 좋아지는 이유에 대해 생각해 보았다.</p>
<p>그것은 내가 <strong>생산적인 일을 했다는 착각</strong>을 안겨주기 때문이 아닐까?</p>
<p>책을 읽기만 하고 실천하지 않는다면 무용지물인데</p>
<p>실천하기는 싫은 게으른 사람이 책만 읽고
(왜냐면 실천에는 고통이 수반되기 때문)</p>
<p>&quot;이제 실천만 하면 된다&quot;며 자위하는 것은 아닐까</p>
<p>실천 없는 독서를 경계해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Scala] Doobie 로 postgres 사용하기]]></title>
            <link>https://velog.io/@sangwoo-sean/Scala-Doobie-%EB%A1%9C-postgres-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@sangwoo-sean/Scala-Doobie-%EB%A1%9C-postgres-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Fri, 04 Aug 2023 16:20:52 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>이 포스팅은 스칼라를 처음 접하는 분들도 쉽게 따라할 수 있도록 하는것이 목적입니다.
scala에 대한 기본적인것을 몰라도 따라하기 쉽도록 작성해보았습니다.</p>
<h3 id="사용하는-기술-도구들">사용하는 기술, 도구들</h3>
<ul>
<li>scala</li>
<li>Doobie</li>
<li>postgres</li>
<li>Docker</li>
<li>IntelliJ</li>
</ul>
<h3 id="혹시-scala-가-처음이라면">혹시 scala 가 처음이라면?</h3>
<ul>
<li><a href="https://www.scala-lang.org/download/">scala 공식 사이트</a>에 가서 scala 를 다운받으세요.</li>
</ul>
<h3 id="소스코드">소스코드</h3>
<p>프로젝트 소스코드는 <a href="https://github.com/sangwoo-sean/scala-doobie-practice">깃허브</a>에 올려두었습니다. 참고하세요.</p>
<h1 id="0-doobie-란">0. Doobie 란?</h1>
<p>scala 프로젝트에서 사용할 수 있는 JDBC 라이브러리입니다.
이제부터 Doobie 를 이용해서 scala 프로젝트에서 postgres 와 커넥션을 맺고, 데이터를 조회하거나 저장하는 것을 해보겠습니다.</p>
<h1 id="1-scala-프로젝트-생성">1. scala 프로젝트 생성</h1>
<p>개발 환경은 intelliJ 를 이용하겠습니다.</p>
<p><code>File -&gt; New -&gt; Project</code> 를 누르고
<img src="https://velog.velcdn.com/images/sangwoo-sean/post/b8447af0-5d00-48c4-995b-2bc350ee6d16/image.png" alt="">
위와 같이 설정 후 <code>Creaet</code> 를 클릭하면 끝</p>
<h1 id="2-doobie-의존성-설정">2. Doobie 의존성 설정</h1>
<p>scala 는 sbt 라는 빌드 툴을 이용합니다.
Doobie 를 사용하기 위해 <code>build.sbt</code> 파일에 들어가서 의존성을 설정해줍니다.</p>
<pre><code class="language-sbt">val DoobieVersion = &quot;1.0.0-RC1&quot;
val NewTypeVersion = &quot;0.4.4&quot;

lazy val root = (project in file(&quot;.&quot;))
  .settings(
    name := &quot;HelloDoobie&quot;,
    libraryDependencies ++= Seq(
      &quot;org.tpolecat&quot; %% &quot;doobie-core&quot; % DoobieVersion,
      &quot;org.tpolecat&quot; %% &quot;doobie-postgres&quot; % DoobieVersion,
      &quot;org.tpolecat&quot; %% &quot;doobie-hikari&quot; % DoobieVersion,
      &quot;io.estatico&quot; %% &quot;newtype&quot; % NewTypeVersion
    )
  )
</code></pre>
<p>설정 후 reload 를 통해 의존성을 새로고침 해줍시다.</p>
<h1 id="3-db-준비">3. DB 준비</h1>
<p>Doobie 를 사용해서 프로젝트와 연결할 DB가 있어야겠죠?</p>
<p>간단한 환경설정을 위해 docker-compose 를 사용하겠습니다.</p>
<p>최상위 디렉토리에 <code>docker-compose.yml</code> 파일을 생성하고, 아래와 같이 내용을 작성합니다.</p>
<pre><code class="language-yaml">version: &#39;3.1&#39;

services:
  db:
    image: postgres
    restart: always
    volumes:
      - &quot;./initdb.sql:/docker-entrypoint-initdb.d/initdb.sql&quot;
    environment:
      - &quot;POSTGRES_USER=user&quot;
      - &quot;POSTGRES_PASSWORD=1111&quot;
      - &quot;POSTGRES_DB=hellodoobie&quot;
    ports:
      - &quot;5432:5432&quot;
  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080</code></pre>
<p>여기서 <code>volumes</code> 부분을 보면 <code>&quot;./initdb.sql:/docker-entrypoint-initdb.d/initdb.sql&quot;</code> 라고 되어있습니다.
<code>./initdb.sql</code> 를 docker container 안에 <code>docker-entrypoint-initdb.d/initdb.sql</code> 로 복사하겠다는 뜻입니다.
docker container 에서 <code>docker-entrypoint-initdb.d</code> 경로에 있는 <code>.sql</code> 스크립트는 컨테이너가 생성될때 같이 실행됩니다.</p>
<p>그리고 DB 실습을 위해 필요한 <code>initdb.sql</code> 파일 또한 <code>docker-compose.yml</code> 파일과 동일한 디렉토리에 만들고, 아래와 같이 내용을 채워줍니다.</p>
<pre><code class="language-sql">-- Directors
CREATE TABLE directors
(
    id        serial            NOT NULL,
    PRIMARY KEY (id),
    name      character varying NOT NULL,
    last_name character varying NOT NULL
);

-- Movies
CREATE TABLE movies
(
    id uuid NOT NULL,
    title              character varying NOT NULL,
    year_of_production smallint          NOT NULL,
    director_id        integer           NOT NULL
);

ALTER TABLE movies
    ADD CONSTRAINT movies_id PRIMARY KEY (id);
ALTER TABLE movies
    ADD FOREIGN KEY (director_id) REFERENCES directors (id);

-- Actors
CREATE TABLE actors
(
    id   serial            NOT NULL,
    PRIMARY KEY (id),
    name character varying NOT NULL
);

-- Link between movies and actors
CREATE TABLE movies_actors
(
    movie_id uuid NOT NULL,
    actor_id integer NOT NULL
);

ALTER TABLE movies_actors
    ADD CONSTRAINT movies_actors_id_movies_id_actors PRIMARY KEY (movie_id, actor_id);
ALTER TABLE movies_actors
    ADD FOREIGN KEY (movie_id) REFERENCES movies (id);
ALTER TABLE movies_actors
    ADD FOREIGN KEY (actor_id) REFERENCES actors (id);


-- Actors
INSERT INTO actors (name)
VALUES (&#39;Henry Cavill&#39;);
INSERT INTO actors (name)
VALUES (&#39;Gal Godot&#39;);
INSERT INTO actors (name)
VALUES (&#39;Ezra Miller&#39;);
INSERT INTO actors (name)
VALUES (&#39;Ben Affleck&#39;);
INSERT INTO actors (name)
VALUES (&#39;Ray Fisher&#39;);
INSERT INTO actors (name)
VALUES (&#39;Jason Momoa&#39;);

-- Directors
INSERT INTO directors (name, last_name)
VALUES (&#39;Zack&#39;, &#39;Snyder&#39;);

-- Movies
INSERT INTO movies (id, title, year_of_production, director_id)
VALUES (&#39;5e5a39bb-a497-4432-93e8-7322f16ac0b2&#39;, &#39;Zack Snyder&#39;&#39;s Justice League&#39;, &#39;2021&#39;, 1);

-- Actor-Movie link
INSERT INTO movies_actors (movie_id, actor_id)
VALUES (&#39;5e5a39bb-a497-4432-93e8-7322f16ac0b2&#39;, 1);
INSERT INTO movies_actors (movie_id, actor_id)
VALUES (&#39;5e5a39bb-a497-4432-93e8-7322f16ac0b2&#39;, 2);
INSERT INTO movies_actors (movie_id, actor_id)
VALUES (&#39;5e5a39bb-a497-4432-93e8-7322f16ac0b2&#39;, 3);
INSERT INTO movies_actors (movie_id, actor_id)
VALUES (&#39;5e5a39bb-a497-4432-93e8-7322f16ac0b2&#39;, 4);
INSERT INTO movies_actors (movie_id, actor_id)
VALUES (&#39;5e5a39bb-a497-4432-93e8-7322f16ac0b2&#39;, 5);
INSERT INTO movies_actors (movie_id, actor_id)
VALUES (&#39;5e5a39bb-a497-4432-93e8-7322f16ac0b2&#39;, 6);
</code></pre>
<p>docker-compose 파일의 내용을 정리하자면,</p>
<blockquote>
<p>postgres DB 컨테이너를 만들거고, 이름은 user, password는 1111, 포트는 5432로 접근할거야. 그리고 시작할 때 initdb.sql 스크립트를 실행시켜</p>
</blockquote>
<p>라는 뜻인거죠.</p>
<p>이제 터미널에 <code>docker-compose up -d</code> 를 입력해주면 컨테이너가 만들어지고, 5432번 포트로 접근이 되며, 데이터베이스 스키마와 데이터가 준비되어 있는 것을 확인할 수 있습니다.</p>
<h1 id="4-조회하기">4. 조회하기</h1>
<h2 id="41-기본">4.1. 기본</h2>
<p>DB 조회를 실행할 파일을 <code>main</code> 경로에 만들어주겠습니다.
저는 <code>HelloDoobie.scala</code> 라는 이름으로 만들어 아래와 같이 작성하겠습니다.</p>
<pre><code class="language-java">import cats.effect.{ExitCode, IO, IOApp}
import doobie.implicits._
import doobie.util.transactor.Transactor

object HelloDoobie extends IOApp { //1

  val xa: Transactor[IO] = Transactor.fromDriverManager[IO]( //2
    &quot;org.postgresql.Driver&quot;,
    &quot;jdbc:postgresql:hellodoobie&quot;,
    &quot;user&quot;, // username
    &quot;1111&quot; // password
  )

  def findAllActorsNames: IO[List[String]] = { //3
      val query = sql&quot;select name from actors&quot;.query[String] //4
    query.to[List].transact(xa)
  }

  override def run(args: List[String]): IO[ExitCode] =
    findAllActorsNamesProgram.map(println).as(ExitCode.Success) //5
}</code></pre>
<p>코드를 알아보겠습니다.
DB 커넥션이 잘 되는지 확인하기 위해 간단한 조회 쿼리를 만들었습니다.</p>
<blockquote>
<ol>
<li>cats 의 IOApp 을 extends 함으로서 실행 가능한 프로젝트를 만듭니다.</li>
<li><code>Transactor</code> 는 DB와 커넥션을 맺고 트랜잭션을 관리하도록 합니다.</li>
<li><code>findAllActorsNames</code> 에서 sql query를 만들고 <code>transact(xa)</code> 로 실행시킵니다.</li>
<li><code>sql&quot;select ...&quot;</code> 과 같이 sql 인터폴레이션을 사용하기 위해서는 <code>import doobie.implicits._</code>를 import 해주어야 함을 유의하세요.</li>
<li><code>run</code> 에서 조회 함수를 실행시키고  결과에 map(println) 을 해주어 결과를 출력할 수 있도록 합니다.</li>
</ol>
</blockquote>
<p>그러면 아래와 같이 조회 결과가 출력됩니다.
<code>List(Henry Cavill, Gal Godot, Ezra Miller, Ben Affleck, Ray Fisher, Jason Momoa)</code></p>
<p>간단한 조회를 성공했습니다.</p>
<p>위의 <code>findAllActorsNames</code> 메서드에서 query 를 만들고 <code>transact(xa)</code> 를 통해서 트랜잭션을 실행시켰습니다. 같은 방식으로 작동하는 다른 조회방법들을 살펴보겠습니다.</p>
<h2 id="42-find-by-id">4.2. find by Id</h2>
<p>id(혹은 특정 컬럼) 으로 데이터를 조회하는 방법입니다. 결과가 있을수도, 없을수도 있기 때문에 Option 형태로 가져옵니다.</p>
<pre><code class="language-java">  def findActorById(id: Int): IO[Option[String]] = {
    val query = sql&quot;select name from actors where id=$id&quot;.query[String]
    query.option.transact(xa)
  }</code></pre>
<p>전체조회와 동일하게 query 를 interpolation 을 이용하여 작성하고, <code>option.transact(xa)</code> 를 실행시켜 줍니다.</p>
<h2 id="43-find-all-stream">4.3. find all stream</h2>
<pre><code class="language-java">  def findActorsStream: IO[List[String]] = {
    sql&quot;select name from actors&quot;.query[String]
      .stream.compile.toList.transact(xa)
  }</code></pre>
<p>query에 <code>stream.compile.toList</code> 를 이용해 stream 방식으로 조회할 수 있습니다.</p>
<h2 id="44-find-by-name-with-hc-module">4.4. find by Name (with HC Module)</h2>
<pre><code class="language-java">  def findActorByName(name: String): IO[Option[Actor]] = {
    val queryString = &quot;select id, name from actors where name = ?&quot;
    HC.stream[Actor](
      queryString,
      HPS.set(name),
      100 //chunk size
    ).compile.toList.map(_.headOption).transact(xa)
  }</code></pre>
<p>HC(Highlevel Connection) 와 HPS(Highlevel PreparedStatement) 를 이용하여 조금 더 로우 레벨에서 직접 쿼리를 작성하는 방법입니다. HC 에 query 와 HPS, chunk size 를 설정해서 쿼리를 실행합니다.
이 때, 쿼리는 <code>?</code> 를 wildcard 로 사용하는 plain String 입니다.</p>
<h2 id="45-fragments">4.5. Fragments</h2>
<pre><code class="language-java">  def findActorsByInitial(letter: String) = {
    val selectPart = fr&quot;select id, name&quot;
    val fromPart = fr&quot;from actors&quot;
    val wherePart = fr&quot;where Left(name, 1) = $letter&quot;

    val statement = selectPart ++ fromPart ++ wherePart
    statement.query[Actor].stream.compile.toList.transact(xa)
  }</code></pre>
<p>fragment 는 SQL문의 조각들을 가지고 조합하여 전체 SQL문을 빌드하는 방법입니다.
<code>++</code> 연산자로 각 statement 를 조합하고, <code>stream.compile</code> 로 실행시킬 수 있습니다.</p>
<h1 id="5-저장하기">5. 저장하기</h1>
<h2 id="51-기본">5.1. 기본</h2>
<pre><code class="language-java">  def saveActor(id: Int, name: String) = {
    val query = sql&quot;insert into actors (id, name) VALUES ($id, $name)&quot;
    query.update.run.transact(xa)
  }</code></pre>
<p>저장하기도 조회 쿼리와 비슷합니다. 다른점은, <code>update.run</code> 을 통해 실행시켜준다는 것입니다.</p>
<h2 id="52-id-자동생성">5.2. id 자동생성</h2>
<pre><code class="language-java">  def saveActorAutoGenerated(name: String) = {
    val query = sql&quot;insert into actors (name) VALUES ($name)&quot;
    query.update.withUniqueGeneratedKeyswithUniqueGeneratedKeys[Int](&quot;id&quot;).transact(xa)
  }</code></pre>
<p><code>withUniqueGeneratedKeys</code> 메서드를 이용하여 지정된 컬럼의 유니크 값을 자동생성할 수 있습니다. 이때 제네릭에는 파라미터로 넘겨주는 컬럼의 자료형을 입력합니다.
<del>꼭 <code>withUniqueGeneratedKeys</code> 를 이용하지 않더라도, DB에 pk, auto increment 가 지정되어있는 컬럼은 생략하면 자동으로 생성됩니다.</del></p>
<h2 id="53-desugared-version">5.3. desugared version</h2>
<pre><code class="language-java">  def saveActors_v2(id: Int, name: String) = {
    val queryString = &quot;insert into actors (id, name) values (?, ?)&quot;
    Update[Actor](queryString).run(Actor(id, name)).transact(xa)
  }</code></pre>
<p><code>5.2.</code>와 같은 쿼리 방법은 사실  syntactic sugar 입니다. 해당 쿼리를 desugarize 한다면 위의 쿼리와 같이 됩니다. 이 방법으로도 동일하게 데이터를 삽입할 수 있습니다.
여러 건의 데이터를 한번에 저장하는 경우에는 desugared 방법으로 작성해야 하기 때문에 알아둘 필요가 있겠습니다.</p>
<h2 id="54-bulk-insert">5.4. bulk insert</h2>
<pre><code class="language-java">  def saveActorsBulk(names: List[String]) = {
    val queryString = &quot;insert into actors (name) values (?)&quot;
    Update[String](queryString)
      .updateManyWithGeneratedKeys[Actor](&quot;id&quot;, &quot;name&quot;)(names).compile.toList.transact(xa)
  }</code></pre>
<p><code>5.3.</code>  에서 살펴본 방법을 참고하여 만들어진 bulk insert 쿼리입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ZIO] Logging - SLF4J v2]]></title>
            <link>https://velog.io/@sangwoo-sean/ZIO-Logging-SLF4J-v2</link>
            <guid>https://velog.io/@sangwoo-sean/ZIO-Logging-SLF4J-v2</guid>
            <pubDate>Wed, 05 Jul 2023 12:16:56 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>ZIO 로 프로그래밍을 하던 중 서버를 기동할 때 마다 계속해서 거슬리는 아래와 같은 메세지가 출력되었었다.
<img src="https://velog.velcdn.com/images/sangwoo-sean/post/261b5acd-59d8-4f27-94ae-393ef75efcfa/image.png" alt=""></p>
<p>대충 로깅 설정을 하지 않아서 그러겠거니.. 하고 로깅 설정을 하러 <a href="https://zio.dev/zio-logging/slf4j2">ZIO 공식문서</a>를 찾아보았다.</p>
<blockquote>
<p>libraryDependencies += &quot;dev.zio&quot; %% &quot;zio-logging-slf4j2&quot; % &quot;2.1.13&quot;</p>
</blockquote>
<p>이걸 추가하면 된다고 해서 추가하고, <code>logback.xml</code> 파일도 공식문서에 있는 sample 파일을 그대로 classPath에 넣어주었다.</p>
<h1 id="문제-해결-다음-문제">문제 해결, 다음 문제</h1>
<p>sbt를 다시 load 하고 앱을 실행했더니 이번에는 아래와 같이 바뀐 로그가 나왔다.</p>
<p><img src="https://velog.velcdn.com/images/sangwoo-sean/post/2e08f3d3-4acf-4bab-8e4e-a58305ba8da8/image.png" alt=""></p>
<p>SLF4J provider 가 없으므로 default 인 NOP logger 의 구현으로 사용하겠다라...</p>
<p>여전히 거슬리므로 SLF4J 의 provider 를 찾아주도록 하자.</p>
<h1 id="두번째-문제-해결">두번째 문제 해결</h1>
<p>다른 provider 들이 많이 있겠지만, 공식문서의 <code>logback.xml</code> 파일에 보면 appender class 에 <code>class=&quot;ch.qos.logback.core.ConsoleAppender&quot;</code> 라는 것을 보았고, 겸사겸사 logback 을 찾아주도록 하자</p>
<blockquote>
<p>libraryDependencies += &quot;ch.qos.logback&quot; % &quot;logback-classic&quot;    % &quot;1.4.7&quot;</p>
</blockquote>
<p>sbt reload 이후 다시 앱을 기동시켜보면 아래와 같이 logger 의 log 가 제대로 찍히는걸 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/sangwoo-sean/post/fbaa110d-6aa6-40fa-95f3-b3b72787dc18/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ZIO] flyway 환경설정 에러 해결]]></title>
            <link>https://velog.io/@sangwoo-sean/ZIO-flyway-%ED%99%98%EA%B2%BD%EC%84%A4%EC%A0%95-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@sangwoo-sean/ZIO-flyway-%ED%99%98%EA%B2%BD%EC%84%A4%EC%A0%95-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Sat, 17 Jun 2023 15:08:57 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>ZIO 로 간단한 프로젝트를 따라하며 배우던 중, DB migration 툴인 flyway를 설정하다가 빌드가 되지 않아 원인을 찾느라 고생하고 있었다.</p>
<h1 id="원인">원인</h1>
<p><img src="https://velog.velcdn.com/images/sangwoo-sean/post/5bba00a7-c5be-4150-a38a-ffbaa148d6f6/image.png" alt=""></p>
<p>위의 사진과 같은 에러를 뿜고있었고, 에러 로그를 보며 처음에는</p>
<p><code>Exception in thread &quot;zio-fiber-8&quot; sttp.client3.DeserializationException: sttp.client3.DeserializationException: (expected &#39;[&#39; got &#39;&lt;&#39;)</code> 라고 써있는 빨간 글씨에 집중했지만, 이내 다른 곳을 보니 의미있는 문장을 찾을 수 있었다.</p>
<p><code>WARN  o.f.core.internal.command.DbValidate No migrations found. Are your locations set up correctly?</code></p>
<p>즉 migration 의 location 설정이 잘못되어있다는 것이다.</p>
<p>나는 분명 그대로 따라했었던것같은데..?</p>
<p>내 디렉토리 설정은 아래와 같이 되어있었다.</p>
<p><img src="https://velog.velcdn.com/images/sangwoo-sean/post/354cf9dc-1d10-46a3-8821-6dac6c1efb8e/image.png" alt=""></p>
<p><strong>설마 저 <code>resources.db.migration</code> 을 계층폴더로 인식 못하는걸까..</strong></p>
<h1 id="해결">해결</h1>
<p>가정이 맞았다. 사진과 같이 디렉토리를 제대로 설정해주고, resources 디렉토리로도 잘 지정해주었다. (이름을 바꾸니 자동으로 적용되었었다.)</p>
<p><img src="https://velog.velcdn.com/images/sangwoo-sean/post/e7c2aa7a-ab41-4355-ba24-c3a5318f7441/image.png" alt=""></p>
<p>다시 테스트코드를 작동시키니 아래와 같은 로그와 함께 migration이 잘 작동하는 것을 확인할 수 있었다.</p>
<pre><code class="language-terminal">00:05:51.196 [ZScheduler-6] INFO  o.f.core.internal.command.DbValidate Successfully validated 1 migration (execution time 00:00.009s)
00:05:51.206 [ZScheduler-6] INFO  o.f.core.internal.command.DbMigrate Current version of schema &quot;public&quot;: 001
00:05:51.208 [ZScheduler-6] INFO  o.f.core.internal.command.DbMigrate Schema &quot;public&quot; is up to date. No migration necessary.</code></pre>
<h1 id="기타">기타</h1>
<p>그나저나 flyway는 테스트코드로 동작시켰을 때는 테스트가 종료되기 전 롤백을 하나보다. 데이터 삽입 쿼리가 있는데 해당 데이터가 테스트 이후에 DB에서 조회되지 않는다.</p>
]]></description>
        </item>
    </channel>
</rss>