<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dae-hwa.log</title>
        <link>https://velog.io/</link>
        <description>대화로그</description>
        <lastBuildDate>Sat, 07 Feb 2026 16:50:14 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dae-hwa.log</title>
            <url>https://images.velog.io/images/dae-hwa/profile/dfc8d24f-b64e-4616-8389-259bdbbcdc79/social.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dae-hwa.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dae-hwa" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[SCD(slowly chainging dimesion)]]></title>
            <link>https://velog.io/@dae-hwa/SCDslowly-chainging-dimesion</link>
            <guid>https://velog.io/@dae-hwa/SCDslowly-chainging-dimesion</guid>
            <pubDate>Sat, 07 Feb 2026 16:50:14 GMT</pubDate>
            <description><![CDATA[<p>디멘젼/팩트 구성에 대한 방법론 (SCD)</p>
<p>요구되는 분석 관점에 따라 보관 방식을 선택한다.
(현재값만 유지할지, 시점 이력을 보존할지, 제한된 이력만 남길지)</p>
<p>각각의 Type으로 구분.</p>
<blockquote>
<p>Type4는 문헌에 따라 정의가 혼용되기도 한다. 여기서는 mini-dimension(킴볼 정의) 를 Type4로 두고, “현재+히스토리 테이블 분리” 패턴은 참고로 별도 표기함.</p>
</blockquote>
<hr>
<p>테이블 예시 시나리오</p>
<ul>
<li><p>고객 <code>C001</code>의 세그먼트가 변경됨</p>
<ul>
<li><code>2025-01-01 ~ 2025-02-14</code>: <code>SMB</code></li>
<li><code>2025-02-15 ~</code>: <code>ENT</code></li>
</ul>
</li>
<li><p>Fact(매출) 2건</p>
<ul>
<li><code>2025-02-10</code> 매출 100 (변경 전)</li>
<li><code>2025-03-01</code> 매출 200 (변경 후)</li>
</ul>
</li>
</ul>
<h2 id="type1-overwrite">Type1 (Overwrite)</h2>
<p>단순히 덮어쓰는(overwrite) 방식. 이력 유지는 되지 않는다.</p>
<p>이러면 업데이트 될때마다 속성 기준으로 미리 만들어 둔 집계(aggregate), 물리화 결과(MV/큐브)는 재계산이 필요할 수 있음.</p>
<p>과거의 데이터도 완전히 바뀌어야 하는 경우에 사용.
예를 들어 항상 최신 속성으로만 봐야 하는 경우(회원 정보에 문제나 오타가 있어 정정을 해야 하는경우)</p>
<h3 id="테이블-예시">테이블 예시</h3>
<p><strong>dim_customer (Type1)</strong></p>
<table>
<thead>
<tr>
<th>customer_id</th>
<th>segment</th>
</tr>
</thead>
<tbody><tr>
<td>C001</td>
<td>SMB</td>
</tr>
</tbody></table>
<p>변경 후(2025-02-15)</p>
<table>
<thead>
<tr>
<th>customer_id</th>
<th>segment</th>
</tr>
</thead>
<tbody><tr>
<td>C001</td>
<td>ENT</td>
</tr>
</tbody></table>
<blockquote>
<p>팩트는 C001 만 참조하면 되니 변함이 없음. 즉 기록에 대한 참조가 사라진다.</p>
</blockquote>
<hr>
<h2 id="type2">Type2</h2>
<p>변경 발생 시 새로운 행을 추가하고 대체키(surrogate key)를 부여한다.</p>
<p>유효한 시작일/종료일과 current 플래그를 두는 구성이 흔함.</p>
<p>하나의 테이블로 이력과 시점 추적이 가능(정석적이라고 함)</p>
<p>대신 변경이 잦을 경우 로우가 계속해서 증가하고, ETL 단계에서 매핑이 필요함.</p>
<p>Fact 적재 시점에 해당 일자/시점에 유효한 차원 버전(SK)을 찾아서 FK로 저장</p>
<h3 id="테이블-예시-1">테이블 예시</h3>
<p><strong>dim_customer (Type2)</strong></p>
<table>
<thead>
<tr>
<th align="right">customer_sk</th>
<th>customer_id</th>
<th>segment</th>
<th>valid_from</th>
<th>valid_to</th>
<th>is_current</th>
</tr>
</thead>
<tbody><tr>
<td align="right">101</td>
<td>C001</td>
<td>SMB</td>
<td>2025-01-01</td>
<td>2025-02-14</td>
<td>N</td>
</tr>
<tr>
<td align="right">102</td>
<td>C001</td>
<td>ENT</td>
<td>2025-02-15</td>
<td>9999-12-31</td>
<td>Y</td>
</tr>
</tbody></table>
<p><strong>fact_sales</strong></p>
<table>
<thead>
<tr>
<th>sales_dt</th>
<th align="right">amount</th>
<th align="right">customer_sk</th>
</tr>
</thead>
<tbody><tr>
<td>2025-02-10</td>
<td align="right">100</td>
<td align="right">101</td>
</tr>
<tr>
<td>2025-03-01</td>
<td align="right">200</td>
<td align="right">102</td>
</tr>
</tbody></table>
<blockquote>
<p>이미 쌓인 과거 Fact는 일반적으로 업데이트하지 않고, 과거 SK를 계속 참조하는 형태</p>
</blockquote>
<hr>
<h2 id="type3">Type3</h2>
<p>현재 값과 다른 컬럼에 이전값과 현재 값을 동시에 저장하는 것. 현재 값은 업데이트.</p>
<p>이걸 대체 현실(alternate reality)로 표현하기도 하며, 상대적으로 드물게 사용.</p>
<p>바로 직전 값과 현재 값만 비교해도 충분한 경우에 사용.
데이터가 커지진 않지만 직전 이력 이전의 이력은 동일하게 소실됨.</p>
<blockquote>
<p>개인적으로 e-value가 필요한 실시간 마트성 데이터가 이런 류가 아닌가 싶음</p>
</blockquote>
<h3 id="테이블-예시-2">테이블 예시</h3>
<p><strong>dim_customer (Type3)</strong></p>
<table>
<thead>
<tr>
<th>customer_id</th>
<th>segment_current</th>
<th>segment_prev</th>
</tr>
</thead>
<tbody><tr>
<td>C001</td>
<td>SMB</td>
<td>(null)</td>
</tr>
</tbody></table>
<p>변경 후(2025-02-15)</p>
<table>
<thead>
<tr>
<th>customer_id</th>
<th>segment_current</th>
<th>segment_prev</th>
</tr>
</thead>
<tbody><tr>
<td>C001</td>
<td>ENT</td>
<td>SMB</td>
</tr>
</tbody></table>
<hr>
<h2 id="type4-mini-dimension">Type4 (Mini-Dimension)</h2>
<p>변경이 잦은 속성 그룹은 mini-dimension으로 분리하는 방식.</p>
<p>차원폭증(rapidly changing monster dimension) 문제를 완화한다고 알려져 있음.</p>
<p>둘의 차이점은(여기서는 “mini-dimension” vs “history table 패턴”을 구분해서 적음)</p>
<ul>
<li><p>mini-dimension의 경우</p>
<ul>
<li>베이스 디멘젼은 자주 변하지 않는 속성으로 채우고, 자주 변하는 속성 그룹을 미니 디멘젼으로 만든다.</li>
<li>데이터 변경 시 Fact에 <strong>(base FK + mini FK)</strong> 를 함께 저장하는 형태가 흔함(팩트를 “나눈다”기보단 FK가 2개로 늘어나는 느낌)</li>
<li>미니 디멘젼은 “고객 개별 이력”이라기보단 <strong>속성 조합(프로파일) 카탈로그</strong> 성격이 강함</li>
</ul>
</li>
<li><p>히스토리 테이블 패턴(참고)</p>
<ul>
<li>“현재 테이블 + 이력 테이블”로 분리해서 운영하는 방식</li>
<li>Fact 시점과 valid 기간을 맞추는 range join이 필요한 경우가 많음</li>
</ul>
</li>
</ul>
<p>조인 복잡도가 상승하고 etl도 복잡해진다(당연하지만)</p>
<blockquote>
<p>나는 결제 내역과 결과를 분리할때 이런 방식을 사용했었다.</p>
</blockquote>
<h3 id="mini-dimension-테이블-예시">mini-dimension 테이블 예시</h3>
<p><strong>dim_customer_base</strong></p>
<table>
<thead>
<tr>
<th align="right">customer_sk</th>
<th>customer_id</th>
<th>name</th>
</tr>
</thead>
<tbody><tr>
<td align="right">501</td>
<td>C001</td>
<td>Alice</td>
</tr>
</tbody></table>
<p><strong>dim_customer_profile (mini-dimension)</strong></p>
<table>
<thead>
<tr>
<th align="right">profile_sk</th>
<th>segment</th>
</tr>
</thead>
<tbody><tr>
<td align="right">9001</td>
<td>SMB</td>
</tr>
<tr>
<td align="right">9002</td>
<td>ENT</td>
</tr>
</tbody></table>
<p><strong>fact_sales</strong></p>
<table>
<thead>
<tr>
<th>sales_dt</th>
<th align="right">amount</th>
<th align="right">customer_sk</th>
<th align="right">profile_sk</th>
</tr>
</thead>
<tbody><tr>
<td>2025-02-10</td>
<td align="right">100</td>
<td align="right">501</td>
<td align="right">9001</td>
</tr>
<tr>
<td>2025-03-01</td>
<td align="right">200</td>
<td align="right">501</td>
<td align="right">9002</td>
</tr>
</tbody></table>
<h3 id="참고-history-table-패턴-테이블-예시">(참고) history table 패턴 테이블 예시</h3>
<p><strong>dim_customer_current</strong></p>
<table>
<thead>
<tr>
<th>customer_id</th>
<th>segment_current</th>
</tr>
</thead>
<tbody><tr>
<td>C001</td>
<td>ENT</td>
</tr>
</tbody></table>
<p><strong>dim_customer_history</strong></p>
<table>
<thead>
<tr>
<th>customer_id</th>
<th>segment</th>
<th>valid_from</th>
<th>valid_to</th>
</tr>
</thead>
<tbody><tr>
<td>C001</td>
<td>SMB</td>
<td>2025-01-01</td>
<td>2025-02-14</td>
</tr>
<tr>
<td>C001</td>
<td>ENT</td>
<td>2025-02-15</td>
<td>9999-12-31</td>
</tr>
</tbody></table>
<hr>
<h2 id="나머지-타입-hybrid">나머지 타입 (Hybrid)</h2>
<p>아래 5/6/7은 “과거 이력을 보존”하면서도 “현재 속성 기준으로 과거 사실을 재분류해서 보고 싶다” 요구를 지원하기 위한 하이브리드로 알려져 있음.</p>
<h3 id="type5-type4--type1-outrigger">Type5 (Type4 + Type1 Outrigger)</h3>
<p>Type 4 + 1</p>
<p>mini-dimension을 쓰되, base dimension에 현재 mini-dimension 키를 Type1처럼 덮어써서 현재 속성 접근을 단순화하는 방식.</p>
<p><strong>dim_customer_base (Type5 느낌)</strong></p>
<table>
<thead>
<tr>
<th align="right">customer_sk</th>
<th>customer_id</th>
<th>name</th>
<th align="right">current_profile_sk</th>
</tr>
</thead>
<tbody><tr>
<td align="right">501</td>
<td>C001</td>
<td>Alice</td>
<td align="right">9002</td>
</tr>
</tbody></table>
<hr>
<h3 id="type6-type2--current-type1-overlay">Type6 (Type2 + Current Type1 Overlay)</h3>
<p>Type 2 + 1 (+3로도 설명됨)</p>
<p>행 추가는 Type2처럼 하되, 차원에 현재값 컬럼(Type1) 을 두고, 그 현재값 컬럼은 동일 customer_id의 모든 버전 행에 대해 덮어쓴다.</p>
<p>과거 행에는 history 속성 제공하고 현재의 값도 같이 제공한다.</p>
<blockquote>
<p>과거의 값과 현재값 대조/분석이 필요한 경우에 좋을듯 하다 → 추측임</p>
</blockquote>
<p><strong>dim_customer (Type6 예시)</strong></p>
<table>
<thead>
<tr>
<th align="right">customer_sk</th>
<th>customer_id</th>
<th>segment_hist</th>
<th>segment_current</th>
</tr>
</thead>
<tbody><tr>
<td align="right">101</td>
<td>C001</td>
<td>SMB</td>
<td>ENT</td>
</tr>
<tr>
<td align="right">102</td>
<td>C001</td>
<td>ENT</td>
<td>ENT</td>
</tr>
</tbody></table>
<hr>
<h3 id="type7-dual-type-1-and-type-2-perspectives">Type7 (Dual Type 1 and Type 2 Perspectives)</h3>
<p>팩트가 두 개의 FK를 가진다.</p>
<ul>
<li>Type2 버전으로 가는 surrogate key(당시 기준)</li>
<li>현재 행으로 가는 durable/natural key(현재 기준)</li>
</ul>
<p>타입 6에서 디멘젼의 업데이트 부담을 덜 수 있다(차원 과거행에 current 컬럼을 덮어쓰는 부담 회피).</p>
<p>(참조하는 팩트 테이블에 적재 시점에 두 키를 함께 저장하는 것)</p>
<p><strong>fact_sales (Type7 예시)</strong></p>
<table>
<thead>
<tr>
<th>sales_dt</th>
<th align="right">amount</th>
<th align="right">customer_sk</th>
<th>customer_id</th>
</tr>
</thead>
<tbody><tr>
<td>2025-02-10</td>
<td align="right">100</td>
<td align="right">101</td>
<td>C001</td>
</tr>
<tr>
<td>2025-03-01</td>
<td align="right">200</td>
<td align="right">102</td>
<td>C001</td>
</tr>
</tbody></table>
<hr>
<p>선택기준?</p>
<ul>
<li>이력 필요 없음(항상 최신만) → Type1</li>
<li>시점 이력 필요(정산/감사/시점 분석) → Type2</li>
<li>현재 vs 직전만 필요 → Type3</li>
<li>일부 속성군이 너무 자주 변해 Type2 폭증 → Type4(mini-dimension)</li>
<li>“as-was + as-is” 둘 다 편하게 → Type6/7</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[만들면서 배우는 클린아키텍쳐 내용 메모]]></title>
            <link>https://velog.io/@dae-hwa/%EB%A7%8C%EB%93%A4%EB%A9%B4%EC%84%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%ED%81%B4%EB%A6%B0%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%EB%82%B4%EC%9A%A9-%EB%A9%94%EB%AA%A8</link>
            <guid>https://velog.io/@dae-hwa/%EB%A7%8C%EB%93%A4%EB%A9%B4%EC%84%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%ED%81%B4%EB%A6%B0%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%EB%82%B4%EC%9A%A9-%EB%A9%94%EB%AA%A8</guid>
            <pubDate>Wed, 17 Jul 2024 16:51:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>책 내용 단순 나열에 가까운 기록용도의 글입니다. 특별한 내용은 거의 없습니다</p>
</blockquote>
<p><a href="https://github.com/wikibook/clean-architecture">https://github.com/wikibook/clean-architecture</a></p>
<h2 id="1-계층형-아키텍쳐의-문제는-무엇일까">1. 계층형 아키텍쳐의 문제는 무엇일까</h2>
<p>계층형 아케텍쳐는 선택의 폭을 넓히고 변화하는 요구사항과 외부요인에 빠르게 적응할 수 있게 해준다.</p>
<p>문제점은 코드에 나쁜 습관들이 스며들기 쉽게 만들고 시간이 지날수록 소프트웨어를 점점 더 변경하기 어렵게 만드는 허점들을 노출한다.</p>
<h3 id="데이터베이스-주도-설계를-유도한다">데이터베이스 주도 설계를 유도한다</h3>
<ul>
<li>도메인 계층은 영속성 계층에 의존하기 때문에 모든 것이 영속성 계층을 토대로 만들어진다.</li>
<li>비즈니스 관점에서는 도메인 로직을 먼저 만들 수 있어야 한다.</li>
<li>ORM을 사용하다보면 도메인 계층과 영속성 계층 사이 강한 결합이 생긴다. 도메인 계층에서 ORM 엔티티에 접근할 수 있기 때문<h3 id="지름길을-택하기-쉬워진다">지름길을 택하기 쉬워진다</h3>
</li>
<li>같은 계층에 있는 컴포넌트나 아래 계층에만 접근하도록 하는 규칙만 있어 우회하기가 쉬워진다.</li>
<li>예를 들어, 상위 계층의 컴포넌트에 접근하고 싶으면 해당 컴포넌트를 아래 계층으로 내려버리면 된다. 이러면 영속계층이 비대해진다.</li>
<li>한번 망가지기 시작한다면?... 깨진 유리창 이론<h3 id="테스트하기-어려워진다">테스트하기 어려워진다</h3>
</li>
<li>영속성 계층 형태 그대로 웹계층에서 사용하니 필드 하나만 바뀌어도 전체에 영향을 미친다. 책임이 섞이고 도메인 로직이 퍼져나갈 확률이 높아짐</li>
<li>웹 계층 테스트를 하는데 영속성 계층도 모킹해야 한다. 테스트 설정이 복잡해지면 테스트를 전혀 작성하지 않는 방향으로 갈 확률이 높아짐</li>
<li>실제 테스트를 작성하는 시간보다 설정시간이 더 오래걸림...<h3 id="유스케이스를-숨긴다">유스케이스를 숨긴다</h3>
</li>
<li>도메인 로직이 여러 계층에 존재할 확률이 높으니 새로운 기능이 어느 계층에 들어가야 할지 모호해진다.</li>
<li>넓은(여러 컨트롤러를 담당하는) 서비스 클래스들이 많아진다.<h3 id="동시-작업이-어려워진다">동시 작업이 어려워진다</h3>
</li>
<li>인터페이스를 먼저 따서 개발하면 상관 없지만, 대부분 영속 계층에 의존한 개발을 하기 때문에 불가능<h3 id="유지보수-가능한-소프트웨어를-만드는데-어떻게-도움이-될까">유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?</h3>
</li>
<li>엄격한 제약이 없으므로 잘못 흘러갈 가능성이 높다.</li>
<li>계층형 단점을 염두에 두고 만들어야 한다.</li>
</ul>
<h2 id="2-의존성-역전하기">2. 의존성 역전하기</h2>
<h3 id="단일-책임-원칙">단일 책임 원칙</h3>
<ul>
<li>컴포넌트를 변경하는 이유는 한 가지여야 한다.</li>
<li>하지만 변경할 이유는 컴포넌트 간의 의존성을 통해 전파된다.<h3 id="부수효과에-관한-이야기">부수효과에 관한 이야기</h3>
</li>
<li>핵심적인 컴포넌트를 건드렸을때 발생하던 부수효과때문에 그걸 회피하는 이상한 방식을 클라이언트에서 요구하더라는 썰풀<h3 id="의존성-역전-원칙">의존성 역전 원칙</h3>
</li>
<li>단일 책임 원칙을 고수준에서 적용하면 상위 계층이 하위 계층에 비해 변경할 이유가 더 많다</li>
<li>따라서 영속성 계층에 대한 도메인 계층의 의존성 때문에 영속성 계층을 바꿀때마다 도메인 계층도 변경해야 한다.</li>
<li>하지만 도메인 코드는 애플리케이션에서 가장 중요한 코드다. 변경의 영향을 최소화 하고 싶다.</li>
<li>이럴 때 의존성을 역전시키면 된다.<blockquote>
<p>레포지토리의 인터페이스를 도메인 계층에 넣어두고 구현은 영속 계층에서 하도록</p>
</blockquote>
<h3 id="클린-아키텍처로버트-마틴">클린 아키텍처(로버트 마틴)</h3>
</li>
<li>도메인 코드가 바깥으로 향하는 어떤 의존성도 없어야 하는 것(의존성을 역전시켜서)</li>
<li>클린 아키텍처에서 유스케이스는 서비스에 대응된다. 단일 책임을 갖기 위해 세분화 되는데 이를 이용해 넓은 서비스 문제를 해결할 수 있다.</li>
<li>도메인 코드에서 어떤 영속 프레임워크나 UI 프레임워크가 사용됐는지 알지 못한다. 따라서 특정 프레임워크에 종속된 코드를 가질 수 없고 비즈니스 규칙에 집중할 수 있다.</li>
<li>따라서 순수한 형태의 DDD를 적용할 수 있다. </li>
<li>트레이드 오프로 각 계층에서 엔티티에 대한 모델을 만들어야 한다. 그래서 계층을 넘나들때마다 DTO 변환작업이 필요해진다.</li>
<li>다소 추상적<h3 id="헥사고날-아키텍쳐알리스테어-콕번">헥사고날 아키텍쳐(알리스테어 콕번)</h3>
</li>
<li>육각형은 크게 의미 없다. 다른 시스템이나 어댑터와 연결되는 4개 이상의 면을 가질 수 있음을 보여주기 위해 사용했다 한다.</li>
<li>육각형에서 외부로 향하는 의존성이 없다. 따라서 클린아키텍쳐를 만족한다.</li>
<li>코어와 어댑터들간의 통신을 위해 코어가 포트를 제공해야 한다.<ul>
<li>driving 어댑터에게는 코어에 있는 유스케이스 클래스들에 의해 구현되고 호출되는 인터페이스가 포트<blockquote>
<p>내가 구현하면서 느껴본 바로는 의존방향과 제어 방향이 일치하므로 의존 역전이 필요 없을 수 있음</p>
</blockquote>
</li>
<li>driven 어댑터에게는 어댑터에 의해 구현되고 코어에 의해 호출되는 인터페이스가 포트</li>
</ul>
</li>
<li>이런 패턴때문에 포트와 어댑터 아케턱쳐라고 불리기도 함</li>
<li>유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?<ul>
<li>도메인 코드가 다른 바깥쪽 코드에 의존하지 않게 해서 코드를 변경할 이유의 수를 줄일 수 있다. 이러면 유지보수성이 좋아진다.</li>
<li>도메인 코드는 비즈니스 문제에 딱 맞도록 모델링 될 수 있다. 다른 영역은 고려하지 않아도 된다.</li>
</ul>
</li>
</ul>
<h2 id="3-코드-구성하기">3. 코드 구성하기</h2>
<h3 id="계층으로-구성">계층으로 구성</h3>
<ul>
<li>domain, persistence, web 으로 패키지를 나누기.<ul>
<li>저자는 domain에 persistence의 인터페이스를 두고 영속 영역에서 이를 구현하도록 했음</li>
<li>그럼에도 최적 구조가 아니라고 하는데,<ul>
<li>애플리케이션의 기능 조각(functional slice)이나 특성(feature)을 구분 짓는 패키지 경제가 없다. 이러면 서로 연관없는 클래스들끼리 부수효과를 발생시킬 가능성이 높아진다.(예시에서는 Account와 관련된 기능만 존재하던 프로젝트에 User 관련 기능이 추가되면 User와 관련된 컨트롤러, 서비스, 도메인, 레포지토리의 한 벌이 같이 섞이게 되는 것을 얘기함)</li>
<li>애플리케이션이 어떤 유스케이스를 제공하는지 파악할 수 없다.(아마 테이블 명이 Account니까 이를 관성적으로 따라가지 않았을까...)<blockquote>
<blockquote>
<p>이건 그냥 클래스 여러개 만들어서 관리하면 되는거 아닌가? 하는 생각도 든다</p>
</blockquote>
</blockquote>
</li>
<li>육각형 아키텍쳐를 따랐는지 확인하려면 세부 구조를 확인해야한다.(인커밍 포트와 아웃고잉 포트가, 각 어댑터들이 어디에 존재하고 있는지 직관적으로 파악하기 힘들다)</li>
</ul>
</li>
</ul>
</li>
<li>기능으로 구성하기<ul>
<li>기능으로 나누면 package private 접근 수준으로 패키지간 경계를 강화할 수 있다.</li>
<li>AccountService는 이미 account 패키지 아래에 있으니 SendMoneyService로 이름을 바꾼다.<blockquote>
<p>애플리케이션의 기능을 코드를 통해 볼 수 있게 만드는 것을 로버트 마틴은 소리치는 아키텍쳐라고 명명한다.</p>
</blockquote>
</li>
<li>그런데 이렇게하면 아키텍쳐의 가시성을 더 떨어뜨린다. 계층에 대한 구분이 없기 때문에</li>
</ul>
</li>
</ul>
<h3 id="아키텍처적으로-표현력-있는-패키지-구조">아키텍처적으로 표현력 있는 패키지 구조</h3>
<ul>
<li><p>육각형 아키텍처에서 구조적으로 핵심적인 요소는 엔티티, 유스케이스, 인커밍/아웃고잉 어댑터다.</p>
</li>
<li><p>아래는 구조의 각 요소들을 패키지에 매핑 시킨 구조</p>
<pre><code>buckpal
- account
  - adapter
    - in
      - web
        - AccountController
    - out
      - persistence
        - AccountPersistenceAdapter
        - SpringDataAccountRepository
- domain
  - Account
  - Activity
- application
  - SendMoneyService
  - port
    - in
      - sendMoneyUseCase
    - out
      - LoadAccountPort
      - UpdateAccountStatePort </code></pre><blockquote>
<p>Adapter들은 packege private으로 둬도 된다. 어차피 소통은 port의 in/out 인터페이스로만 하니까</p>
</blockquote>
</li>
<li><p>구조가 생소하다면 복잡해 보일 수 있지만, 의사소통에 크게 도움이 될 수 있다.</p>
</li>
<li><p>예를 들어, 만약에 서드파티 API 클라이언트를 변경해야 하면 adapter.out 에 있는걸 찾아서 바꾸면 된다.</p>
</li>
<li><p>이 구조로 아키텍처-코드 갭, 모델-코드 갭을 다룰 수 있다.</p>
<blockquote>
<p>TODO: <a href="https://www.georgefairbanks.com/software-architecture/model-code-gap/">https://www.georgefairbanks.com/software-architecture/model-code-gap/</a></p>
</blockquote>
</li>
<li><p>또한 DDD 개념에 직접적으로 대응 시킬 수 있다.</p>
</li>
<li><p>account 같은 상위 레벨 패키지는 다른 바운디드 컨텍스트와 통신할 전용 진입접과 출구(포트)를 포함하는 바운디드 컨텍스트에 해당한다.</p>
</li>
<li><p>완벽한 구조는 아니다. 지켜야 할 규칙이 있고, 패키지 구조가 적합하지 않아서 어쩔 수 없이 아키텍처-코드갭을 넓히고 아키텍처를 반영하지 않는 패키지를 만들어야 할 수도 있다.</p>
</li>
<li><p>그럼에도 표현력 있는 패키지구조는 아키텍처-코드 갭을 줄일 수 있게 도와주는 도구다</p>
<ul>
<li>의존성 주입의 역할</li>
</ul>
</li>
<li><p>클린 아키텍처의 본질적인 요건은 애플리케이션 계층이 인커밍/아웃고잉 어댑터에 의존하지 않는 것이다.</p>
</li>
<li><p>제어의 흐름과 의존성 방향이 다르다면 의존성을 역전 시켜야 한다.</p>
<ul>
<li>제어의 흐름 : <code>AccountController -&gt; &lt;I&gt;SendMoneyUseCase -&gt; SendMoneyService -&gt; &lt;I&gt;LoadAccountPort -&gt; AccountPersistenceAdapter</code></li>
<li>의존성 방향 : <code>AccountController -&gt; &lt;I&gt;SendMoneyUseCase -&gt; SendMoneyService &lt;- &lt;I&gt;LoadAccountPort &lt;- AccountPersistenceAdapter</code><blockquote>
<p>책의 구현은 SendMoneyUseCase를 SendMoneyService가 구현하도록 하고, SendMoneyService에서 LoadAccountPort를 사용하도록 한다.</p>
</blockquote>
<h3 id="유지보수-가능한-소프트웨어를-만드는데-어떻게-도움이-될까-1">유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?</h3>
</li>
</ul>
</li>
<li><p>이제 코드에서 아키텍처의 특정 요소를 찾으로면 패키지 구조를 보면 된다.
이러면 의사소통, 개발, 유지보수가 수월해진다.</p>
</li>
</ul>
<h2 id="4-유스케이스-구현하기">4. 유스케이스 구현하기</h2>
<ul>
<li>서론?<ul>
<li>이제 애플리케이션, 웹, 영속성 계층이 느슨하게 결합되어 있기 때문에 도메인 코드는 편하게 모델링 하면 된다.</li>
<li>DDD를 해도 되고, 리치 도메인, 어니믹 도메인(빈약한 도메인) 모델을 구현하거나 자기 나름의 방식을 사용해도 된다.</li>
<li>그럼에도 아래부터는 책에서 제시하는 방법을 설명한다.</li>
<li>육각형 아키텍처는 도메인 중심의 아키텍처에 적합하다. 따라서 유스케이스를 도메인 엔터티 중심으로 구현한다.<h3 id="도메인-모델-구현하기">도메인 모델 구현하기</h3>
</li>
</ul>
</li>
<li>한 계좌에서 디른 계좌로 송금하는 유스케이스를 구현한다.</li>
<li>객체지향적으로 모델링하는 한 가지 방법은 입금과 출금의 책임을 가진 Account 엔티티를 만들고 출금 게좌에서 돈을 출금해서 입금 계좌로 돈을 입금하는 것<blockquote>
<p><a href="https://github.com/wikibook/clean-architecture/blob/main/src/main/java/io/reflectoring/buckpal/account/domain/Account.java">https://github.com/wikibook/clean-architecture/blob/main/src/main/java/io/reflectoring/buckpal/account/domain/Account.java</a></p>
</blockquote>
<ul>
<li>Account 엔티티는 실제 계좌의 현재 스냅샷을 제공한다.</li>
<li>입금과 출금은 Activity 엔티티에 포착된다.</li>
<li>한 계좌의 모든 Activity를 미리 메모리에 올려두는 것은 비효율적이므로 ActivityWindow VO 에서 포착한 지난 며칠 혹은 몇 주간의 범위에 해당하는 활동만 보유한다.</li>
<li>현재 잔고 계산을 위해 baselineBalance가 있다. 현재 총 잔고는 baselineBalance에 활동창의 모든 활동들의 잔고를 합한 값이 된다.<pre><code class="language-java">Money.add(
          this.baselineBalance,
          this.activityWindow.calculateBalance(this.id)
      );</code></pre>
</li>
<li>입금과 출금은 각각 새로운 활동을 활동창에 추가하는 것을 의미한다. (withdraw() 와 deposit() 메서드)</li>
<li>출금하기 전에는 잔고를 초과하는 금액은 출금할 수 없도록 검사하는 비즈니스 규칙이 있다.</li>
</ul>
</li>
</ul>
<h3 id="유스케이스-둘러보기">유스케이스 둘러보기</h3>
<ul>
<li><p>유스케이스가 하는 일?</p>
<ol>
<li>입력을 받는다.</li>
<li>비즈니스 규칙을 검증한다.</li>
<li>모델 상태를 조작한다.</li>
<li>출력을 반환한다.<blockquote>
<p>저자는 유스케이스 코드가 도메인 로직에만 신경써야 한다고 생각한다. 유스케이스에 입력 유효성 검증이 들어가면 오염된다고 생각한다.</p>
</blockquote>
</li>
</ol>
</li>
<li><p>비즈니스 규칙을 충족하면 유스케이스는 입력을 기반으로 어떤 방법으로든 모델의 상태를 변경한다.</p>
<blockquote>
<p>일반적으로 도메인 객체의 상태를 바꾸고 영속성 어댑터를 통해 구현된 포트로 이 상태를 전달해서 저장될 수 있게 한다. 그리고 또 다른 아웃고잉 어댑터를 호출 할 수도 있다.</p>
</blockquote>
</li>
<li><p>넓은 서비스 문제를 피하려면 각 유스케이스별로 분리된 각각의 서비스를 만든다.</p>
</li>
<li><p><a href="https://github.com/wikibook/clean-architecture/blob/main/src/main/java/io/reflectoring/buckpal/account/application/service/SendMoneyService.java">https://github.com/wikibook/clean-architecture/blob/main/src/main/java/io/reflectoring/buckpal/account/application/service/SendMoneyService.java</a></p>
</li>
</ul>
<h3 id="입력-유효성-검증">입력 유효성 검증</h3>
<p>유스케이스 책임은 아니지만 애플리케이션 게층의 책임에는 해당한다.</p>
<p>어댑터가 유스케이스에 입력을 저날하기 전에 입력 유효성을 검증한다면? 이걸 모두 검증했다고 믿을 수 있나? 그리고 유스케이스는 하나 이상의 어댑터에서 호출되는데 유효성 검증이 각 어댑터마다 구현되어야 한다.</p>
<p>애플리케이션 게층에서 입력 유효성 검증 하는 이유</p>
<ul>
<li>애플리케이션 바깥쪽에서 유효하지 않은 입력값을 받으면 모델의 상태를 해치게 된다.</li>
</ul>
<p>그래서 입력 모델(input model)을 만든다.</p>
<ul>
<li>SendMoneyService 에서 받는 메소드가 sendMoney(SendMoneyCommand) 이런 식으로 생김</li>
<li>in port에 위치하므로 애플리케이션 계층임</li>
<li>쉽게 말하면 컨트롤러에서 서비스로 넘기는 DTO를 별도로 잡고 여기서 유효성 검증을 할 수 있도록 한 것.</li>
<li>이렇게 하면 유스케이스 구현체 주위에 오류 방지 계층(anti corruption layer; 하나의 바운디드 컨텍스트를 다른 바운디드 컨텍스트와 격리시키는 계층)을 만든 것</li>
</ul>
<h3 id="생성자의-힘">생성자의 힘</h3>
<p>SendMoneyCommand 는 생성자에 많은 책임을 부여했다. 클래스가 불변이고 유효성 검증까지 하고 있으니 유효하지 않은 상태를 만드는게 불가능</p>
<p>매개변수가 많으면 빌더로 만들어도 됨. 어차피 유효성 검사 하니까 빌더를 써도 잘못된 객체가 만들어지기 어려움.</p>
<p>그럼에도 빌더는 런타임에 감지가 되고 컴파일 타임에 감지를 하기 힘듦. </p>
<blockquote>
<blockquote>
<p>감지하려면 테스트 코드가 있어야 할 것이다</p>
</blockquote>
</blockquote>
<p>그래서 책에서는 생성자 사용을 권장</p>
<h3 id="유스케이스마다-다른-입력-모델">유스케이스마다 다른 입력 모델</h3>
<p>서로 다른 유스케이스에 같은 입력 모델을 사용하고 싶을 수 있다.</p>
<p>그런데 이건 코드 스멜일 가능성이 생긴다.</p>
<p>예를 들어, 같은 불변 커맨드 객체를 사용하지만 특정 유스케이스에서는 허용해야 하는게 다른 유스케이스에서는 불허해야 하는 경우가 있으니(e.g. nullable, not null)</p>
<p>따라서 유스케이스 전용 입력 모델을 만들어야 불필요한 부수효과가 생기지 않는다고 설명한다.</p>
<p>대신 입력 모델에 매핑하는 비용이 생긴다.</p>
<h3 id="비즈니스-규칙-검증하기">비즈니스 규칙 검증하기</h3>
<p>입력 유효성과 비즈니스 규칙 검증을 실용적으로 구분할 수 있는 방법은 도메인 모델의 현재 상태에 접근해야 하는지 여부</p>
<p>입력 유효성 검증은 <code>@NotNull</code> 같은거 붙이면 땡일 가능성이 높다.</p>
<p>책에서는 입력 유효성은 구문상의(syntactical) 유효성 검증이라 하고 비즈니스 규칙 검증은 의미적인(semantical) 유효성 검증이라 표현한다.</p>
<p>비즈니스 규칙 검증의 예시로 출금 계좌는 초과 출금 되어서는 안 된다는 조건을 얘기한다. 초과 출금 되지 않으려면 출금 계좌의 현재 상태에 접근해야 하므로 비즈니스 규칙이다.</p>
<p>반대로 송금되는 금액은 0보다 크다라는 조건은 모델에 접근하지 않아도 해결된다. 이러면 유효성 검증이다.</p>
<p>관점 따라 논쟁이 될 수 있다고 하는데 책에서는 코드의 일관성 측면에서 유지보수 하기 쉬운 방법이라 설명한다.</p>
<blockquote>
<blockquote>
<p>컨트롤러에서 받는 Request 모델에서 검증해야 하면 틀린 말일 수 있는데, 책에서 처럼 커맨드 객체를 만들어 사용하는거면 이게 맞는 것 같다.</p>
</blockquote>
</blockquote>
<p>그러면 비즈니스 규칙검증은 어떻게 하나? 도메인 객체에 넣어준다. 이러면 비즈니스 로직 옆에 존재하니 위치 고민도 없고 추론도 쉽다.</p>
<p>이게 애매하면 유스케이스 코드(서비스 구현체)에서 도메인 사용 전에 해준다.</p>
<h3 id="풍부한-도메인-모델-vs-빈약한-도메인-모델">풍부한 도메인 모델 vs 빈약한 도메인 모델</h3>
<h4 id="풍부한-도메인-모델">풍부한 도메인 모델</h4>
<p>풍부한 도메인 모델에서는 엔티티에서 가능한 많은 도메인 로직을 구현한다.</p>
<ul>
<li>유스케이스는 도메인 모델의 진입점이다. </li>
<li>유스케이스에는 사용자의 의도만 표현하고 실제 작업은 도메인 엔티티 메서드에서 수행한다.</li>
<li>비즈니스 규칙이 유스케이스 구현체가 아닌 엔티티에 위치하게 된다.</li>
</ul>
<h4 id="빈약한-도메인-모델">빈약한 도메인 모델</h4>
<p>엔티티 자체가 굉장히 얇다.</p>
<p>상태를 표현하는 필드와 getter, setter 메서드만 포함되고 도메인 로직을 가지지 않는다.</p>
<p>다른 말로 하면 도메인 로직이 유스케이스 클래스에 구현되어 있는 것</p>
<h4 id="뭐가-좋은가">뭐가 좋은가?</h4>
<p>상황과 필요에 따라서~</p>
<blockquote>
<blockquote>
<p>굳이 객체 상태를 다룰 필요성이 없는 아주 단순한 도메인이라면 리치 도메인이 없어도 된다. 개인적으론 그렇더라도 디미터 법칙은 지켰줬으면 하는 생각...</p>
</blockquote>
</blockquote>
<h3 id="유스케이스마다-다른-출력-모델">유스케이스마다 다른 출력 모델</h3>
<p>출력도 각 유스케이스에 맞게 구체적인이 좋다고 한다. Account를 직접 반환하는게 좋은 경우가 생길 수도 있긴 한데, 이건 계속 고민해야 하는 문제. 의심스러우면 가능한 적게 반환한다.</p>
<p>유스케이스들 간에 같은 출력 모델을 공유하면 유스케이스들도 강하게 결합된다.(단일 책임원칙 위반. 공유모델은 장기적으로 보면 무조건 커지게 되어있다.)</p>
<p>같은 이유로 도메인 엔티티를 출력 모델로 사용하는 것도 지양해야 한다. 도메인 엔티티를 변경해야 할 이유가 늘어나게 된다.</p>
<h3 id="읽기-전용-유스케이스는-어떨까">읽기 전용 유스케이스는 어떨까?</h3>
<p>책에서 하는 방법은 인커밍 전용 포트를 만들고 쿼리 서비스에서 구현하는 것.</p>
<p>CQRS 처럼 구현할 수도 있다.</p>
<p>조회 전용은 여러 계층에서 같은 모델을 사용한다면 지름길을 써보는 것을 고려해볼 수도 있다.</p>
<h3 id="유지보수-가능한-소프트웨어를-만드는데-어떻게-도움이-될까-2">유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?</h3>
<p>입출력 모델을 독립적으로 모델링하면 원치 않는 부수효과를 피할 수 있다.</p>
<p>대신 작업은 늘어난다. 매핑해줘야 하니까.</p>
<p>대신에 장기적으로 보면 유스케이스 이해도를 높일 수 있고 유지보수성도 높아진다. 서로 작업중인 유스케이스를 건드리지 않을 수 있으니.</p>
<p>지속 가능한 코드.</p>
<h2 id="5-웹-어댑터-구현하기">5. 웹 어댑터 구현하기</h2>
<h3 id="의존성-역전">의존성 역전</h3>
<p>웹 어댑터는 인커밍(주도하는) 어댑터.</p>
<p>웹 어댑터는 애플리케이션 계층에 있는 포트를 호출. 여기도 의존성을 역전시켜놨다.</p>
<p>제어의 흐름 : 컨트롤러 -&gt; 포트 -&gt; 서비스
의존성 방향 : 컨트롤러 -&gt; 포트 &lt;- 서비스</p>
<p>컨트롤러에서 서비스 직접 호출을 막은 이유는 포트를 외부와 통신하는 명세로 사용하기 때문</p>
<p>웹소켓은 인 아웃 포트에 모두 의존하도록 그림을 그려놨다.</p>
<p>웹소켓 컨트롤러 -&gt; 인 포트 &lt;- 서비스
            -&gt; 아웃포트 &lt;- 서비스</p>
<blockquote>
<blockquote>
<p>카프카 스트림즈 앱 구현하면서 난관?에 봉착했던 부분인데, in/out을 겸해야 하는 상황이다. 저렇게하면 순환 의존성은 확실히 피할 수 있을 것 같긴 한데, 스트림즈에 종속적인 코드가 어쩔 수 없이 나와야 하는 부분들은 어떻게 해야할지 아직도 고민이다.</p>
</blockquote>
</blockquote>
<h3 id="웹-어댑터의-책임">웹 어댑터의 책임</h3>
<ol>
<li>HTTP 요청을 자바 객체로 매핑</li>
<li>권한 검사</li>
<li>입력 유효성 검증</li>
<li>입력을 유스케이스의 입력 모델로 매핑</li>
<li>유스케이스 호출</li>
<li>유스케이스의 출력을 HTTP로 매핑</li>
<li>HTTP 응답을 반환</li>
</ol>
<p>유스케이스 입력모델과는 별개의 유효성 검증을 해야 한다고 한다. 웹 어댑터의 입력 모델을 유스케이스 입력 모델로 변환할 수 있는지를 확인하고, 이걸 방해하는 모든 것이 유효성 검증 에러라고 한다.</p>
<p>HTTP와 관련된 것이 애플리케이션 계층으로 침투하면 안된다. 만약 HTTP가 아닌 다른 방식을 사용한다면 동일한 도메인 로직을 수행할 수 있는 선택지를 잃게 된다.</p>
<p>도메인과 애플리케이션 계층부터 개발하면, 어댑터 보다 유스케이스를 먼저 구현하면 경계가 명확해진다고 한다.</p>
<h3 id="컨트롤러-나누기">컨트롤러 나누기</h3>
<p>컨트롤러도 너무 적은 것 보다는 너무 많은게 낫다고 한다(넓은 서비스 문제와 동일하게).</p>
<p>같은 리소스라고 하나의 컨트롤러에 묶어놓으면 아무리 메소드를 잘 나눠놓는다 해도 파악이 힘들어진다. 테스트도 비대해진다.</p>
<p>같은 dto 를 공유하게 되는 경우도 생기는데, 이게 진짜 해당 연산에서 필요한 것인지 생각해봐야 한다. </p>
<p>책에서 예시든건, Account와 User 객체가 1:N 관계일때 계좌를 생성하거나 업데이트 할때 User 객체도 필요한지에 관한 것. list 연산에 사용자 정보도 포함 시켜야 하는가?</p>
<p>전용 모델들을 패키지 private으로 선언하면 다른 곳에서 재사용 될 위험에서도 벗어나게 된다. </p>
<p>이렇게 나누면 동시 작업도 수월해진다.</p>
<h3 id="갈무리">갈무리</h3>
<p>웹 어댑터는 어떠한 도메인 로직도 수행하지 않는다. 애플리케이션 계층은 HTTP와 관련된 작업을 해서는 안 된다.</p>
<p>세분화된 컨트롤러는 처음에 공수가 더 들지만 유지보수하기 훨씬 좋다.</p>
<h2 id="6-영속성-어댑터-구현하기">6. 영속성 어댑터 구현하기</h2>
<h3 id="의존성-역전-1">의존성 역전</h3>
<p>영속성 어댑터는 아웃고잉(주도되는) 어댑터다.</p>
<p>어댑터에서 애플리케이션을 호출하지 않는다. 애플리케이션에게 호출 당한다.</p>
<p>코어의 서비스가 포트를 사용한다. 여기서 포트는 애플리케이션 레이어와 영속성 레이어 사이의 간접적 계층이다. 서비스에서 영속성 계층에 대한 코드 의존성을 없앨 수 있다.</p>
<h3 id="영속성-어댑터의-책임">영속성 어댑터의 책임</h3>
<ol>
<li>입력을 받는다.</li>
<li>입력을 데이터베이스 포맷으로 매핑한다.</li>
<li>입력을 데이터베이스로 보낸다.</li>
<li>데이터베이스 출력을 애플리케이션 포맷으로 매핑한다.</li>
<li>출력을 반환한다.</li>
</ol>
<p>포트 인터페이스에 의해 입력 메세지를 받으면 인터페이스가 지저한 특정 도메인 엔티티나 데이터베이스 연산 전용 객체가 입력 모델이 될 것이다.</p>
<p>영속성 어댑터는 이걸 데이터베이스에 쿼리 날리거나 변경하는데 사용할 수 있는 포멧으로 입력 모델을 매핑한다.</p>
<p>JPA를 사용하면 입력 모델을 JPA 엔티티로 변환시킬 것이다. 그런데 이건 가성비가 떨어질 수 있어서 매핑하지 않는 전략을 선택할 수도 있다.</p>
<p>영속성 어댑터의 입력 모델이 애플리케이션 코어에 존재한다. 따라서 영속성 어댑터 내부를 변경해도 코어에 영향을 주지 않는다.</p>
<p>출력 모델도 애플리케이션 코어에 위치해야 한다.</p>
<blockquote>
<blockquote>
<p>잘 생각해보면 당연한 얘기인데, 코어에서는 영속성 레이어를 몰라야 하기 때문에 애플리케이션 레이어에 있는걸 가져온 뒤 변환하거나, 좀 느슨하게 만들어서 그게 <code>@Entity</code> 객체라면 바로 사용하거나 하는 식으로 진행 될것이다.</p>
</blockquote>
</blockquote>
<h3 id="포트-인터페이스-나누기">포트 인터페이스 나누기</h3>
<p>특정 엔티티가 필요로 하는 모든 데이터베이스 연산을 하나의 리포지토리 인터페이스에 넣어두는게 일반적인 방법이다. 그런데 이러면 넓은 포트 인터페이스가 된다. 애플리케이션 레이어 입장에서 필요한 메소드 이외의 메소드도 알게 된다. 이러면 불필요한 의존성이 생긴다.</p>
<p>이러면 코드를 이해하고 테스트하기 힘들어진다. 어떤 메소드가 필요했던건지, 모킹을 어디까지 해야하는지 처음 보는 사람이 보면 모호해진다.</p>
<blockquote>
<p>로버트 마틴 : 필요없는 화물을 운반하는 무언가에 의존하고 있으면 예상하지 못했던 문제가 발생할 수 있다.</p>
</blockquote>
<p>이걸 해결하는 방법이 인터페이스 분리 원칙(Interface Segregation Principle; ISP)</p>
<p>포트(인터페이스)를 여러개 만들고 영속 어댑터에서 이걸 다 구현하도록 한다. 책에서는 플러그 앤 플레이로 비유한다.</p>
<blockquote>
<blockquote>
<p>굳이 이렇게까지? 싶긴 하다. 뒤를 보면 알겠지만 어차피 어댑터에서 레포지토리를 다시 호출해야 한다. 그렇지 않은 경우는 효과적일듯</p>
</blockquote>
</blockquote>
<h3 id="영속성-어댑터-나누기">영속성 어댑터 나누기</h3>
<p>영속성 어댑터는 도메인 클래스(혹은 DDD의 어그리거트) 하나당 하나의 영속성 어댑터를 구현하는 방식이 되어도 된다.</p>
<pre><code class="language-java">class AccountPersistenceAdapter implements LoadAccountPort, UpdateAccountStatePort {
  // ...
}</code></pre>
<p>이렇게 하면 영속성 어댑터들은 도메인 경계를 따라 자동으로 나눠진다. 나중에 여러개의 바운디드 컨텍스트의 영속성 요구사항을 분리하기도 좋아진다.</p>
<p>포트가 여러개면 경우에 따라 영속 레이어 구현 기술을 필요에 따라 선택해 사용하면 된다.</p>
<p>도메인 코드는 영속성 포트의 구현이 어떻게 됐는지에 대해 관심이 없기 때문이다. 포트 구현만 할 수 있으면 영속성 계층에서 하고싶은대로 하면 된다.</p>
<h3 id="스프링-데이터-jpa-예제">스프링 데이터 JPA 예제</h3>
<blockquote>
<p><a href="https://github.com/wikibook/clean-architecture/blob/main/src/main/java/io/reflectoring/buckpal/account/adapter/out/persistence/AccountPersistenceAdapter.java">https://github.com/wikibook/clean-architecture/blob/main/src/main/java/io/reflectoring/buckpal/account/adapter/out/persistence/AccountPersistenceAdapter.java</a></p>
</blockquote>
<p><code>updateActivities(Account account)</code> 를 보면 도메인 엔티티인 Account를 그대로 가져온다. 그 뒤 JPA 엔티티로 바꿔준다.</p>
<pre><code class="language-java">for (Activity activity : account.getActivityWindow().getActivities()) {
    if (activity.getId() == null) {
        activityRepository.save(accountMapper.mapToJpaEntity(activity));
    }
}</code></pre>
<p>이게 불필요하다고 판단해 매핑하지 않기 전략을 취할 수도 있다. 그런데 현재 Account 도메인의 요구사항을 지키려면 기본 생성자가 있으면 안 된다. 특정 기술을 위해 도메인 모델을 타협해야 하는 상황이고 책에서는 풍부한 도메인 모델을 원하기 때문에 매핑을 시켜줬다.</p>
<h3 id="데이터베이스-트랜잭션은-어떻게-해야-할까">데이터베이스 트랜잭션은 어떻게 해야 할까?</h3>
<p>영속성 어댑터는 본인이 어떤 유스케이스에 포함되는지 알지 못한다. 따라서 언제 트랜잭션을 열고 닫을지 결정할 수 없다. 따라서 트랜잭션에 관한 책임은 서비스에 위임해야 한다.</p>
<p>가장 쉬운 방법은 유스케이스를 구현한 서비스 클래스에 <code>@Transactional</code>을 붙이는 것인데, 이게 싫으면 aop를 이용해 위빙하면 된다.</p>
<h3 id="유지보수-가능한-소프트웨어를-만드는데-어떻게-도움이-될까-3">유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?</h3>
<p>영속성 어댑터를 플러그인처럼 동작하게 만들면 도메인 코드가 영속성 계층과 분리되어 풍부한 도메인 모델을 만들 수 있게 된다.</p>
<p>좁은 포트 인터페이스를 사용하면 포트 구현이 유연해진다.</p>
<h2 id="7-아키텍처-요소-테스트하기">7. 아키텍처 요소 테스트하기</h2>
<h3 id="테스트-피라미드">테스트 피라미드</h3>
<p><img src="https://martinfowler.com/bliki/images/testPyramid/test-pyramid.png" alt="테스트피라미드"></p>
<blockquote>
<p>출처 <a href="https://martinfowler.com/bliki/TestPyramid.html">https://martinfowler.com/bliki/TestPyramid.html</a></p>
</blockquote>
<p>시스템 테스트  -&gt; 통합 테스트 -&gt; 단위 테스트 순으로 이루어진다</p>
<p>비용이 적고 유지보수하기 쉽고 빨리 실행되고 안정적인 작은 크기의 테스트들에 대해 높은 커버리지를 유지해야 한다는 것</p>
<p>경계를 넘거나 결합하는 테스트는 만드는 비용이 비싸지고 실행이 느리고 깨지기 쉬워진다.</p>
<p>테스트 비용이 높으면 커버리지 목표를 낮게 잡아야 한다.</p>
<ul>
<li><p>단위 테스트</p>
<ul>
<li>피라미드의 토대</li>
<li>테스트 중인 클래스(Class Under Test -&gt; CUT 라고 한다고 한다)가 다른 크래스에 의존하면 해당 클래스들은 모킹</li>
</ul>
</li>
<li><p>통합 테스트</p>
<ul>
<li>연결된 여러 유닛을 인스턴스화 하고 시작점이 되는 클래스의 인터페이스로 데이터를 보낸 후 잘 동작하는지 검증</li>
<li>특정 시점에서는 목을 대상으로 수행해야 한다.</li>
</ul>
</li>
<li><p>시스템 테스트</p>
<ul>
<li>애플리케이션을 구성하는 모든 객체 네트워크를 가동시켜 특정 유스케이스가 전 계층에서 잘 동작하는지 검증</li>
</ul>
</li>
</ul>
<h3 id="단위-테스트로-도메인-엔티티-테스트하기">단위 테스트로 도메인 엔티티 테스트하기</h3>
<blockquote>
<p><a href="https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/account/domain/AccountTest.java">https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/account/domain/AccountTest.java</a></p>
</blockquote>
<p>단위테스트는 만들고 이해하는 것도 쉽고 아주 빠르게 실행된다. 비즈니스 규칙을 검증하기에 가장 적절한 방법이다. 도메인 엔티티는 다른 클래스에 거의 의존하지 않기 때문</p>
<h3 id="단위-테스트로-유스케이스-테스트하기">단위 테스트로 유스케이스 테스트하기</h3>
<blockquote>
<p><a href="https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/account/application/service/SendMoneyServiceTest.java">https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/account/application/service/SendMoneyServiceTest.java</a></p>
</blockquote>
<p>클래스 모킹 같은 세팅해야 될게 생기면서 가독성이 떨어지니 예제에서는 BDD 패턴으로 작성</p>
<p>테스트 대상이 외부 서버라 모킹을 했을 경우 상호작용 했는지 여부를 검증한다. 그런데 이러면 코드의 행동 변경 뿐만 아니라 구조 변경에도 취약해진다. 리팩토링 하면 테스트도 변경 될 확률이 높아진다.</p>
<p>따라서 모든 동작을 검증하기보단 꼭 필요한 상호작용만 테스트하는게 좋다. 그리고 이런 테스트는 사실상 통합테스트에 가깝다.</p>
<h3 id="통합-테스트로-웹-어댑터-테스트하기">통합 테스트로 웹 어댑터 테스트하기</h3>
<blockquote>
<p><a href="https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/account/adapter/in/web/SendMoneyControllerTest.java">https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/account/adapter/in/web/SendMoneyControllerTest.java</a></p>
</blockquote>
<p>예제 코드에서는 MockMvc를 이용해 모킹했기 때문에 실제로 HTTP 프로토콜로 테스트한 것은 아니다. 대신 JSON에서 커맨드 모델 객체로 매핑하는 과정을 검증한다.</p>
<p>또한 기대한 응답을 반환하는지도 확인한다.</p>
<p>단위테스트처럼 보일 수 있지만, 내부적으로 스프링 프레임워크와 관련된 작업이 이루어진다(<code>@WebMvcTest</code>를 사용했기 때문에). 프레임워크와 강하게 묶여 때문에 프레임워크와 통합된 상태로 테스트하는 것이 합리적이다.</p>
<p>매핑, 유효성 검증, HTTP 입력 검증 등</p>
<h3 id="통합-테스트로-영속성-어댑터-테스트하기">통합 테스트로 영속성 어댑터 테스트하기</h3>
<blockquote>
<p><a href="https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/account/adapter/out/persistence/AccountPersistenceAdapterTest.java">https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/account/adapter/out/persistence/AccountPersistenceAdapterTest.java</a></p>
</blockquote>
<p>비슷한 이유로 영속성 어댑터도 통합 테스트로 진행하는게 합리적이다. 데이터베이스 매핑도 검증해야 하기 때문에 <code>@DataJpaTest</code> 를 사용한다.</p>
<p>이 테스트에서는 데이터베이스를 모킹하지 않았는데, 커버리지와 별개로 실제 데이터베이스에서 구문 오류나 매핑 에러 등이 생길 수 있기 때문이다. 인메모리 테스트도 실용적이긴 하지만 같은 문제점이 있다. 테스트컨테이너 같은걸 사용하면 실제 환경과 같은 환경으로 진행할 수 있어 유용하다.</p>
<h3 id="시스템-테스트로-주요-경로-테스트하기">시스템 테스트로 주요 경로 테스트하기</h3>
<blockquote>
<p><a href="https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/SendMoneySystemTest.java">https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/SendMoneySystemTest.java</a></p>
</blockquote>
<p>싹다 띄워야 하니까 <code>SpringBootTest</code> 어노테이션 사용한다. TestRestTemplate을 이용해 실제 HTTP 통신을 한다.</p>
<p>실제 영속성 어댑터도 사용한다. 만약 서드파티 시스템을 실행해야 할 경우 해당 출력 포트 인터페이스들을 모킹해준다.</p>
<p>테스트 가독성을 위해 헬퍼 메서드를 만들었는데 이 헬퍼 메서드는 DSL(도메인인 특화 언어)를 형성한다.</p>
<p>적절한 어휘를 사용하면 도메인 전문가가 테스트에 대해 생각하고 피드백을 줄 수도 있다(큐큠버?).</p>
<p>위에서 작성한 통합테스트와 겹치는 부분도 있을 수 있는데, 계층간 매핑 문제 같은 통합테스트만으로 알 수 없는 문제도 알 수 있게 해주기 때문에 의미가 있다.</p>
<p>사용자가 애플리케이션을 사용하면서 거쳐갈 특정 경로로 시나리오를 만들어 커버할 수 있다면 최신 변경 사항들을 배포할 준비가 되었다고 확신할 수 있다.</p>
<h3 id="얼마만큼의-테스트가-충분할까">얼마만큼의 테스트가 충분할까?</h3>
<p>답하기 어려운 문제</p>
<p>라인 커버리지가 중요한 부분을 커버했다는 것을 알려주지는 않기 때문에 100퍼센트가 아니면 의미가 없을 수 있다.</p>
<p>저자는 마음 편하게 소프트웨어를 배포할 수 있는 것을 기준으로 삼는다고 한다. 그리고 배포 주기가 짧으면 테스트도 자주 돌아가니까 테스트를 더 신뢰할 수 있다고 한다.</p>
<p>따라서 처음에는 신뢰의 도약이 필요하다. 문제가 생기면 수정하고 이걸로 배우는 것을 우선순위로 삼으면 맞는 방향성이다.</p>
<p>테스트가 버그를 잡지 못한 이유를 생각하고 커버할 수 있는 테스트를 추가한다.</p>
<p>아예 테스트를 정의하는 것도 좋다.</p>
<ul>
<li>도메인 엔티티, 유스케이스 -&gt; 단위테스트</li>
<li>어댑터 -&gt; 통합테스트</li>
<li>중요한 애플리케이션 경로 -&gt; 시스템 테스트</li>
</ul>
<p>테스트가 개발 중에 이뤄진다면 귀찮은 작업이 아닌 개발 도구로 생각해볼 수 있다.</p>
<p>새로운 필드를 추가할때마다 테스트를 고치는데 한 시간을 써야 한다면 뭔가 잘못된 것</p>
<p>테스트가 코드의 구조적 변경에 너무 취약한 것이므로 개선해야 한다. 이러면 테스트로서의 가치를 잃는다.</p>
<blockquote>
<blockquote>
<p>내 생각에 테스트의 큰 장점 중 하나가 믿음을 가지고 변경이나 리팩토링할 수 있게 해준다는 점인데, 이 장점이 사라지기 때문인 것 같음</p>
</blockquote>
</blockquote>
<h3 id="유지보수-가능한-소프트웨어를-만드는데-어떻게-도움이-될까-4">유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?</h3>
<p>육각형 아키텍처는 도메인 로직과 바깥으로 향한 어댑터를 분리해놔서 내부는 단위테스트, 외부는 통합테스트로 명확하게 전략을 정할 수 있다.</p>
<p>입출력 포트는 뚜렷한 모킹 지점이 된다. 모킹할지 사용할지만 정하면 된다. 포트의 크기가 작으면 모킹하기 쉽고 어떤 메소드를 모킹해야할지 더 명확해진다.</p>
<p>모킹하는 것이 버거워지거나 코드의 특정 부분을 커버하기 위해 어떤 종류의 테스트를 사용해야 할지 모호하면 경고 신호다. 이런 측면에서 테스트코드가 카나리의 역할도 한다고 할 수 있다.</p>
<h2 id="8-경계간-매핑하기">8. 경계간 매핑하기</h2>
<p>매핑 찬성 : 두 계층 간 매핑을 하지 않으면 같은 모델을 사용해야 하기 때문에 두 계층이 강하게 결합됨</p>
<p>매핑 반대 : 보일러 플레이트 코드가 너무 많아지고, 대부분의 유스케이스들은 CRUD만 수행하고 계층에 걸쳐 같은 모델을 사용하기 때문에 과하다</p>
<p>둘 다 맞다. 장단을 따지고 상황에 따라 적합한 방법을 사용해야 한다.</p>
<h3 id="매핑하지-않기-전략">매핑하지 않기 전략</h3>
<p>입출력 모델로 도메인 모델을 사용하는 방법</p>
<p>도메인에 영속 계층과 JSON 직렬화를 위한 로직도 포함되므로 단일 책임 원칙을 위반한다.</p>
<p>하지만 간단한 CRUD에는 굳이 매핑을 할 필요가 없다.</p>
<p>모든 계층이 정확히 같은 구조의 같은 정보를 필요로 한다면 매핑하지 않기 전략은 완벽한 선택지다.</p>
<p>대신 애플리케이션 계층이나 도메인 계층에서 웹과 영속성 문제를 다루게 되면 다른 전략을 취해야 한다.</p>
<h3 id="양방향-매핑-전략">양방향 매핑 전략</h3>
<p>도메인 모델 외에 웹 모델과 영속성 모델을 별도로 둔다. 따라서 도메인 모델이 단일 책임 원칙을 만족한다.</p>
<p>개념적으로 매핑하지 않기 전략 다음으로 간단하다.</p>
<p>단점은 보일러 플레이트 코드가 많이 생긴다는 점. 매핑 프레임워크를 사용해도 시간이 들고 디버깅도 어렵다.</p>
<p>도메인 모델이 계층 경계를 넘어 사용된다(각 포트에서 도메인 모델을 입출력 값으로 사용). 따라서 도메인 모델이 바깥 계층의 변경에 취약해진다.</p>
<h3 id="완전-매핑-전략">완전 매핑 전략</h3>
<p>커맨드 모델 같이 각 계층을 넘나들때도 별도의 모델을 사용하도록 한다.</p>
<p>더 많은 코드가 필요하다. 대신 여러 유스케이스의 요구사항을 함께 다뤄야 하는 매핑에 비해 구현 및 유지보수하기가 쉽다.</p>
<blockquote>
<blockquote>
<p>여러 유스케이스의 요구사항을 함께 다뤄야 한다 -&gt; 매핑하지 않기나, 양방향은 하나의 도메인 모델이 여러 유스케이스의 입출력에 사용될 것</p>
</blockquote>
</blockquote>
<h3 id="단방향-매핑-전략">단방향 매핑 전략</h3>
<p>모든 계층의 모델들이 같은 인터페이스를 구현하도록 한다. 도메인 계층을 바깥으로 전달하고 싶을때도 매핑 없이 할 수 있다. 인터페이스로만 주고 받으면 되니까.</p>
<p>DDD의 팩터리(어떤 특정한 상태로부터 도메인 객체를 재구성할 책임을 가진다) 개념과 잘 어울린다.</p>
<p>계층 간의 모델이 비슷할때 효과적이다. 예를 들어 읽기 전용 연산의 경우</p>
<p>단점은 개념적으로 어렵다.</p>
<h3 id="언제-어떤-매핑전략을-사용할-것인가">언제 어떤 매핑전략을 사용할 것인가?</h3>
<p>각 매핑 전략은 저마다 장단을 가지고 있다.</p>
<p>요구사항에 비해 과하게 복자한 전략은 개발을 불필요하게 더디게 만든다. 어떤 매핑 전략도 철칙처럼 여겨져서는 안된다.</p>
<p>필요하다면 섞어쓴다. 특정 전략이 전역 규칙일 필요는 없다.</p>
<p>어떤 매핑 전략을 선택했더라도 나중에 언제든 바꿀 수 있다. 지금은 최선의 전략처럼 보여도 시간이 지나면 아닐 수 있다.</p>
<p>따라서 어떤 상황에서 어떤 매핑전략을 최우선으로 고려해야 하는지에 대한 가이드라인을 정해둬야 한다.</p>
<h4 id="책에서-제안하는-가이드라인">책에서 제안하는 가이드라인</h4>
<ul>
<li>변경 유스케이스<ul>
<li>웹 계층과 애플리케이션 계층 사이<ul>
<li>완전 매핑 전략을 먼저 고려한다.</li>
<li>이러면 유스케이스별 유효성 검증 규칙이 명확해지고 특정 유스케이스에서 필요하지 않은 필드를 다루지 않아도 된다.</li>
</ul>
</li>
<li>애플리케이션과 영속성 계층 사이<ul>
<li>매핑하지 않기 전략을 첫 번째 선택지로 둔다.</li>
<li>매핑 오버헤드를 줄이고 빠르게 코드를 짤 수 있다.</li>
<li>애플리케이션에서 영속성 문제를 다뤄야 하게 되면 양방향 매핑 전략으로 바꾼다.</li>
</ul>
</li>
</ul>
</li>
<li>쿼리 작업<ul>
<li>매핑하지 않기 전략을 첫 번째 선택지로 둔다.</li>
<li>애플리케이션 계층에서 영속성 문제나 웹 문제를 다뤄야 하게 양방향 매핑 전략으로 바꾼다.</li>
</ul>
</li>
</ul>
<h3 id="유지보수-가능한-소프트웨어를-만드는데-어떻게-도움이-될까-5">유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?</h3>
<p>좁은 포트를 사용하면 유스케이스마다 다른 매핑 전략을 사용할 수 있고, 다른 유스케이스에 영향을 미치지 않으면서 코드를 개선할 수 있다.</p>
<p>상황별로 매핑전략을 선택하면 더 어렵고 더 많은 커뮤니케이션을 필요로 할 것이다. 대신 코드가 정확히 해야하는 일만 수행하면서 더 유지보수하기 쉽게 될 것이다.</p>
<h2 id="9-애플리케이션-조립하기">9. 애플리케이션 조립하기</h2>
<p>최상위의 설정 관련된 것. 의존성 관리 프레임워크의 원리에 대해 간단하게 설명해줌. 뒷 내용 빌드업용인 것 같아 메모 X</p>
<h2 id="10-아키텍처-경계-정하기">10. 아키텍처 경계 정하기</h2>
<h3 id="경계와-의존성">경계와 의존성</h3>
<p>경계를 강제한다는 것</p>
<p>클린 아키텍처애서는 계층 경계를 넘는 의존성을 항상 안쪽으로 향하게 해야 한다. 이 규칙을 강제하는 것</p>
<h3 id="접근-제한자">접근 제한자</h3>
<p>경계를 강제하기 위해 자바에서 제공하는 가장 기본적인 도구</p>
<p>package-private(default)을 이용한다.</p>
<p>예를 들어, 각 인터페이스만 public으로 두고 그 구현체들은 package-private으로 만든다.</p>
<p>의존성 주입은 보통 리플렉션을 이용하니 package-private이어도 의존성 주입이 가능하다는 점을 이용. 대신 클래스 패스 스캐닝을 이용해야 함(직접 설정할 경우 인스턴스화 하기 위해 public 선언이 필요)</p>
<p>작은 모듈에서 효과적이다. 하지만 클래스가 특정 개수를 넘어가기 시작해 하위 패키지를 사용하게 된다면 불가능한 방법</p>
<h3 id="컴파일-후-체크">컴파일 후 체크</h3>
<p>public 제한자를 사용하면 컴파일 단계에서 의존 방향이 달라졌는지 체크하기 힘들다.</p>
<p>컴파일 후 체크(post-compile check)를 사용할 수 있다. 테스트할때 런타임 체크를 해준다.</p>
<p>ArchUnit 이라는 도구가 있다. JUnit과 같은 단위 테스트 프레임워크 기반에서 잘 동작한다. 의존성 규칙을 위반하면 테스트가 실패한다.</p>
<p>단점은 리팩토링에 취약하다. 항상 코드와 함께 유지보수돼야 한다.</p>
<h3 id="빌드-아티팩트">빌드 아티팩트</h3>
<p>maven이나 gradle 같은 자동화된 빌드 프로세스 이용</p>
<p>빌드 도구의 주요한 기능 중 하나는 의존성 해결. 모든 아티팩트가 사용 가능한지 확인해준다.</p>
<p>각 계층을 모듈로 나누고 각 모듈에서 의존할 수 있는 모듈을 강제하면 컴파일 에러를 발생시킬 수 있다.</p>
<p>어댑터 계층은 서로 의존하는걸 엄격하게 막지는 않지만 대부분의 경우 서로 격리시켜 유지하는 것이 좋다.</p>
<p>영속 계층이 웹 영역에 영향을 미치거나, 반대거나 서드파티에 영향을 받거나 하는 일이 발생할 수 있으므로.</p>
<p>모듈을 세분화 할 수록 의존성을 더 잘 제어할 수 있다. 대신 매핑이 그만큼 많이 필요하다.</p>
<p>빌드 도구로 나눴을때 장점</p>
<ul>
<li>순환 의존성을 막을 수 있다.</li>
<li>특정 모듈의 코드를 격리한 채로 변경할 수 있다. 예를 들어, 애플리케이션 계층에서 컴파일 에러가 발생해도 다른 계층에 영향이 없을 것<ul>
<li>극단적으로는 각 모듈을 서로 다른 레포지토리에서 관리할 수도 있음</li>
</ul>
</li>
<li>의존성을 새롭게 추가하려면 빌드 스크립트를 건드려야 하므로 의식적인 행동이 된다. 꼭 필요한건지 생각해보게 할 수 있다.</li>
</ul>
<h3 id="유지보수-가능한-소프트웨어를-만드는데-어떻게-도움이-될까-6">유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?</h3>
<p>소프트웨어 아키텍처는 아키텍처 요소간의 의존성을 관리해주는 것.</p>
<p>의존성이 꼬이면 아키텍처 역시 꼬인것</p>
<p>의존성이 올바른 방향으로 가고있는지 확인</p>
<p>세가지 접근 방식을 조합해서 사용할 수도 있음</p>
<h2 id="11-의식적으로-지름길-사용하기">11. 의식적으로 지름길 사용하기</h2>
<p>어떤 지름길이 있는지 알면 우발적으로 사용되는 지름길을 막고, 정당한 지름길이라면 그 효과를 택할 수도 있다.</p>
<h3 id="왜-지름길은-깨진-창문-같을까">왜 지름길은 깨진 창문 같을까</h3>
<p>깨진 창문 실험? 차의 유리창에 깨지니 좋은 동네에서도 행인들이 차를 망가뜨리기 시작했다.</p>
<ul>
<li>품질이 떨어진 코드에서 작업할 때 더 낮은 품질의 코드를 추가하기가 쉽다</li>
<li>코딩 규칙을 많이 어긴 코드에서 작업할 때 또 다른 규칙을 어기기도 쉽다</li>
<li>지름길을 많이 사용한 코드에서 작업할 때 또 다른 지름길을 추가하기도 쉽다.</li>
</ul>
<p>레거시라고 불리는 많은 코드의 품질은 시간이 가면서 심하게 낮아진다.</p>
<h3 id="깨끗한-상태로-시작할-책임">깨끗한 상태로 시작할 책임</h3>
<p>코드 작성할때도 심리적 영향을 받기 때문에 가능한 지름길을 쓰지 않고 기술 부채를 만들지 않은 채로 깨끗하게 시작하는 것이 중요하다.</p>
<p>만약 프로젝트를 마무리하지 못하고 다른 이들이 인계 받는다면, 인계받는 입장에서는 레거시이기 때문에 깨진 창문을 만들어 내기가 더 쉽다.</p>
<p>비교적 중요하지 않은 부분이거나 프로토 타이핑 작업 중이거나 경제적인 이유가 결부된다면 지름길을 취하는 것이 더 실용 적일 수도 있다.</p>
<p>대신 의도적인 지름길에 대해서는 잘 기록해둬야 한다.</p>
<h3 id="유스케이스-간-모델-공유하기">유스케이스 간 모델 공유하기</h3>
<p>유스케이스간 모델을 공유하는 것은 유스케이스들이 특정 요구사항을 공유할때 괜찮다 -&gt; 특정 세부사항을 변경했을때 두 유스케이스 모두에 영향을 주고 싶은 경우</p>
<p>여러 유스케이스가 같은 모델을 공유한다면 해당 모델은 변경할 이유가 여러개가 된다(SRP 위반).</p>
<p>따라서 두 유스케이스가 서로 독립적으로 진화해야 한다면 똑같은 모양을 사용하더라도 분리해서 시작해야 한다.</p>
<h3 id="도메인-엔티티를-입출력-모델로-사용하기">도메인 엔티티를 입출력 모델로 사용하기</h3>
<p>도메인 모델에 존재하지 않는 값을 추가하고 싶을때 다른 도메인이나 다른 바운디드 컨텍스트에 포함 시켜야 하는 경우에도 도메인에 값을 추가하게 될 수 있다.</p>
<p>간단한 생성이나 업데이트 유스케이스에서는 괜찮을 수 있다.</p>
<p>도메인 로직을 복잡하게 구현해야 한다면 유스케이스 전용 입출력 모델을 만들어야 한다. 유스케이스의 변경이 도메인 엔티티까지 전파될 수 있기 때문에</p>
<h3 id="인커밍-포트-건너뛰기">인커밍 포트 건너뛰기</h3>
<p>인커밍 포트는 의존성 역전에 필수 요소는 아니다.</p>
<p>인커밍 포트를 제거하면 추상화 계층을 줄일 수 있다.</p>
<p>대신 어떤 서비스 메서드를 호출해야 하는지와 같은 세부사항이나 내부 동작에 대해 잘 알아야 한다.</p>
<p>인커밍 포트를 유지하면 아키텍처를 강제할 수 있다. 인커밍 포트만 호출할 수 있도록 강제하면 인커밍 어댑터에서 호출하면 안 되는 메서드를 호출할 일이 없어진다.</p>
<h3 id="애플리케이션-서비스-건너뛰기">애플리케이션 서비스 건너뛰기</h3>
<p>도메인 로직이 어댑터에 추가 될 위험이 있다(한방쿼리?).이러면 도메인 로직이 흩어져서 도메인 로직을 찾거나 유지보수 하기 어려워진다.</p>
<p>단순한 전달만 하는 보일러플레이트를 줄일 수 있다. 이를 위해 간단한 CRUD 케이스에서는 애플리케이션 서비스를 건너 뛸 수도 있다.</p>
<p>대신 유스케이스가 단순 CRUD보다 더 많은 일을 해야 한다면 애플리케이션 서비스를 만들어야 한다.</p>
<h3 id="유지보수-가능한-소프트웨어를-만드는데-어떻게-도움이-될까-7">유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?</h3>
<p>경제적인 관점에서 지름길이 합리적일 때도 있다.</p>
<p>유스케이스가 단순한 CRUD에서 벗어나는 시점이 언제인지에 대해 팀이 합의하는 것이 매우 중요하다.</p>
<p>단순 CRUD 상태에서 벗어나지 않는 유스케이스는 이대로 두는게 더 경제적이다.</p>
<p>어떤 경우든 지름길을 선택한 이유를 기록해두고 나중에 다시 평가할 수 있도록 해야 한다.</p>
<h2 id="12-아키텍처-스타일-결정하기">12. 아키텍처 스타일 결정하기</h2>
<h3 id="도메인이-왕이다">도메인이 왕이다</h3>
<p>외부의 영향을 받지 않고 도메인 코드를 자유롭게 발전시킬 수 있다는 점이 육각형 아키텍처 스타일의 가장 중요한 가치다.</p>
<p>도메인을 중심에 두고 의존성을 역전시키지 않은 구조에서는 DDD를 제대로 할 가능성이 없다. 설계가 항상 다른 요소들에 의해 주도되기 때문에</p>
<p>다르게 말하면 도메인 코드가 애플리케이션에서 가장 중요한 것이 아니라면 이 아키텍처 스타일은 필요하지 않을 것이다.</p>
<h3 id="경험이-여왕이다">경험이 여왕이다</h3>
<p>계층형을 자주 사용했으니 편하다. 육각형 아키텍처도 편해지면 뭐가 더 좋은지 결정내리기 쉬울 것이다.</p>
<p>육각형 아키텍처를 작은 모듈에 적용해보고 편하게 느껴지는 스타일을 찾아보면 다음번 결정에 도움이 될 것이다.</p>
<h3 id="그때그때-다르다">그때그때 다르다</h3>
<p>어떤 소프트웨어를 만드느냐에 따라, 도메인 코드의 역할에 따라서도 다르다. 팀의 경험이나 내린 결정이 마음에 드느냐에 따라서도 다르다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Batch 4 -> 5 (boot 2 -> 3) 업그레이드 메모]]></title>
            <link>https://velog.io/@dae-hwa/Spring-Batch-4-5-boot-2-3</link>
            <guid>https://velog.io/@dae-hwa/Spring-Batch-4-5-boot-2-3</guid>
            <pubDate>Mon, 01 Jul 2024 07:45:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>도움이 됐던 자료들</p>
<ul>
<li><a href="https://github.com/spring-projects/spring-batch/wiki/Spring-Batch-5.0-Migration-Guide">https://github.com/spring-projects/spring-batch/wiki/Spring-Batch-5.0-Migration-Guide</a></li>
<li><a href="https://techblog.lycorp.co.jp/ko/how-to-migrate-to-spring-boot-3">https://techblog.lycorp.co.jp/ko/how-to-migrate-to-spring-boot-3</a></li>
<li></li>
</ul>
</blockquote>
<ul>
<li><p>JobBuilderFactory, StepBuilderFactory deprecated</p>
<ul>
<li><p><a href="https://github.com/spring-projects/spring-batch/wiki/Spring-Batch-5.0-Migration-Guide#jobbuilderfactory-and-stepbuilderfactory-bean-exposureconfiguration">https://github.com/spring-projects/spring-batch/wiki/Spring-Batch-5.0-Migration-Guide#jobbuilderfactory-and-stepbuilderfactory-bean-exposureconfiguration</a></p>
</li>
<li><p>이제는 Job이나 Step을 생성하면서 JobRepository와 TransactionManager를 넣어줘야 한다. JobRepository 자동 생성을 위해서는 Job 관리에 사용될 DataSource와 TransactionManager를 넣어줘야 하는데, <code>SpringBootBatchConfiguration</code> 에서 받아올 수 있도록 <code>@Primary</code> 를 붙여줘야 한다.</p>
</li>
<li><p>구조적으로 개선이 됐다고 생각되는데, 트랙잭션 매니저가 하나로 고정돼있어 소스와 타겟 DB가 다르면 별도의 작업을 해줘야 한다던가 하는 부분이 개선돼었다고 본다. 다만 이렇게 되면 청크단위로 가져올때 중단 지점을 어떻게 파악하고 트랜잭션을 되돌릴건지의 구현이 중요할 것 같은데 이건 따로 찾아봐야 할듯.</p>
</li>
</ul>
</li>
<li><p><code>BatchAutoConfiguration</code> 조건 변경 (이제는 <code>@EnableBatchProcessing</code> 를 제거해줘야 자동 설정이 된다.)</p>
<pre><code> @ConditionalOnMissingBean(
   value = {DefaultBatchConfiguration.class},
   annotation = {EnableBatchProcessing.class} // 이제는 @EnableBatchProcessing를 빼야 자동설정이 된다.
 )</code></pre><ul>
<li><code>JobLauncherApplicationRunner</code> 를 직접 만들어서 해결한 사람도 있던데, 현재는 이렇게 까지 할 필요가 없어 <code>@EnableBatchProcessing</code> 를 빼주는 식으로 작업했다.</li>
</ul>
</li>
<li><p>job parameter 포멧 변경</p>
<ul>
<li><p><code>parameterName=parameterValue,parameterType,identificationFlag</code> 혹은 <code>parameterName=&#39;{&quot;value&quot;: &quot;parameterValue&quot;, &quot;type&quot;:&quot;parameterType(default: String)&quot;, &quot;identifying&quot;: &quot;booleanValue(default: true)&quot;}&#39;</code> 와 같은 형식으로 변경됐다.</p>
<pre><code> $ --job.name=myJob param1=one param2=2024-05-31T00:00:00,java.time.LocalDateTime</code></pre><p> 위 예시에서 param2는 LocalDateTime으로 바로 받을 수 있다. </p>
</li>
</ul>
</li>
</ul>
<ul>
<li><p>테스트 실행(혹은 코드로 직접 생성)시 <code>JobParameter(value: T, class&lt;T&gt;)</code> 사용</p>
<ul>
<li>ItemWriteListener 의 메소드들의 타입 변경</li>
</ul>
<pre><code>// void beforeWrite(java.util.List&lt;? extends S&gt; items)
default void beforeWrite(Chunk&lt;? extends S&gt; items)
// void afterWrite(java.util.List&lt;? extends S&gt; items)
default void afterWrite(Chunk&lt;? extends S&gt; items)
//void onWriteError(java.lang.Exception exception, java.util.List&lt;? extends S&gt; items)
default void onWriteError(Exception exception, Chunk&lt;? extends S&gt; items)</code></pre><p>Chunk는 이렇게 생겼다</p>
<pre><code>public class Chunk&lt;W&gt; implements Iterable&lt;W&gt;, Serializable {
    // ...

    @SafeVarargs
    public Chunk(W... items) {
        this(Arrays.asList(items));
    }

    @SafeVarargs
    public static &lt;W&gt; Chunk&lt;W&gt; of(W... items) {
        return new Chunk(items);
    }

    public Chunk(List&lt;? extends W&gt; items) {
        this(items, (List)null);
    }

    public Chunk(List&lt;? extends W&gt; items, List&lt;SkipWrapper&lt;W&gt;&gt; skips) {
        // ...
    }

    public void add(W item) {
        this.items.add(item);
    }

    public void addAll(List&lt;W&gt; items) {
        this.items.addAll(items);
    }

    public void clear() {
        this.items.clear();
        this.skips.clear();
        this.userData = null;
    }

    // ...
</code></pre></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[spring boot 2.7.x -> 3.2.x 업그레이드 하며 맞이했던 문제들 메모]]></title>
            <link>https://velog.io/@dae-hwa/spring-boot-2.7.x-3.2.x-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%ED%95%98%EB%A9%B0-%EB%A7%9E%EC%9D%B4%ED%96%88%EB%8D%98-%EB%AC%B8%EC%A0%9C%EB%93%A4-%EB%A9%94%EB%AA%A8</link>
            <guid>https://velog.io/@dae-hwa/spring-boot-2.7.x-3.2.x-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%ED%95%98%EB%A9%B0-%EB%A7%9E%EC%9D%B4%ED%96%88%EB%8D%98-%EB%AC%B8%EC%A0%9C%EB%93%A4-%EB%A9%94%EB%AA%A8</guid>
            <pubDate>Mon, 01 Jul 2024 07:20:51 GMT</pubDate>
            <description><![CDATA[<p>한번에 넘어가려다 잘 안 돼서 단계별로 수정</p>
<blockquote>
<p>배치는 <a href="https://velog.io/@dae-hwa/Spring-Batch-4-5-boot-2-3">https://velog.io/@dae-hwa/Spring-Batch-4-5-boot-2-3</a> 참고</p>
</blockquote>
<h2 id="2718-로-버전업">2.7.18 로 버전업</h2>
<pre><code>2024-05월 기준 최신버전</code></pre><p><code>io.spring.dependency-management</code> 
<code>1.0.15.RELEASE</code> 로 버전 업
(<a href="https://docs.spring.io/spring-boot/docs/2.7.18/reference/html/dependency-versions.html">https://docs.spring.io/spring-boot/docs/2.7.18/reference/html/dependency-versions.html</a>)</p>
<ul>
<li><code>mysql:mysql-connector-java</code> -&gt; <code>com.mysql:mysql-connector-j</code> 변경</li>
</ul>
<h2 id="java-17-로-버전-업">Java 17 로 버전 업</h2>
<p>부트 3부터 java 17이 최소버전이기 때문에 미리 버전업 후 테스트</p>
<pre><code class="language-org.mybatis.spring.MyBatisSystemException:">### Error querying database.  Cause: java.lang.reflect.InaccessibleObjectException: Unable to make public boolean java.util.Arrays$ArrayList.contains(java.lang.Object) accessible: module java.base does not &quot;opens java.util&quot; to unnamed module @a4102b8
### Cause: java.lang.reflect.InaccessibleObjectException: Unable to make public boolean java.util.Arrays$ArrayList.contains(java.lang.Object) accessible: module java.base does not &quot;opens java.util&quot; to unnamed module @a4102b8</code></pre>
<p>에러 발생</p>
<p><a href="https://github.com/mybatis/mybatis-3/issues/2383#issuecomment-971649861">https://github.com/mybatis/mybatis-3/issues/2383#issuecomment-971649861</a></p>
<pre><code>That error may occur if you write &lt;if test=&quot;x.isEmpty()&quot;&gt;.
Try &lt;if test=&quot;x.empty&quot;&gt; or &lt;if test=&quot;x.isEmpty&quot;&gt; instead.</code></pre><p>따라해봐도 되지 않는다.</p>
<p>OGNL 버전 문제라고 한다(mybatis 3.5.10 부터 클리어)</p>
<pre><code>원래 Apache commons 프로젝트였는데, 관리가 되고 있지 않는 듯 하다.
이런 프로젝트들을 orphan software라는 곳에서 관리중...
Home for projects that lost their maintainers 라고 적혀있다.
- OGNL? https://jehuipark.github.io/java/mybatis_ognl 이게 읽기 쉬웠다</code></pre><p>그래서 <code>mybatis-spring-boot-starter</code> 2.3.x로 버전 업</p>
<h2 id="30x-로-버전업">3.0.x 로 버전업</h2>
<p><code>io.spring.dependency-management</code> <code>1.1.x</code> 로 업그레이드</p>
<p><a href="https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide">https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide</a></p>
<ul>
<li><p>RMI 사용 시 대안을 찾아야 한다(현재 micrometer + actuator 사용 중).</p>
</li>
<li><p>httpClient4 -&gt; httpClient5로 변경</p>
<ul>
<li><a href="https://hc.apache.org/httpcomponents-client-5.3.x/migration-guide/preparation.html">https://hc.apache.org/httpcomponents-client-5.3.x/migration-guide/preparation.html</a></li>
<li><code>org.apache.httpcomponents:httpclient</code> -&gt; <code>org.apache.httpcomponents.client5:httpclient5</code></li>
<li>RetryHandler -&gt; RetryStrategy</li>
<li>커넥션 관련 설정은 ConnectionManager를 사용</li>
<li>readTimeout 설정이 사라졌으므로 확인 필요</li>
<li>javax -&gt; jarkarta<ul>
<li><code>jakarta.persistence</code>, <code>javax.servlet</code>, <code>jakarta.validation</code></li>
<li><code>com.querydsl:querydsl-jpa::jakarta</code>, <code>kapt(group = &quot;com.querydsl&quot;, name = &quot;querydsl-apt&quot;, classifier = &quot;jakarta&quot;)</code><ul>
<li><code>A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask$KaptExecutionWorkAction  java.lang.reflect.InvocationTargetException (no error message)</code> 에러가 발생하고 trace 추적 시 <code>Caused by: java.lang.NoClassDefFoundError: javax/persistence/Entity</code> 인 경우 확인<ul>
<li>sentry boot starter -&gt; <code>sentry-spring-boot-starter-jakarta</code> 로 변경</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>type level에서 constructor binding이 필요 없어짐</p>
<ul>
<li>기본 동작이 setter에서 constructor로 변경된걸로 보인다.<ul>
<li>따라서 생성자와 매개변수 개수가 안 맞는데 기본값이 없으면 <code>Failed to instantiate [...]: Illegal arguments for constructor</code> 발생</li>
<li><a href="https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide#constructingbinding-no-longer-needed-at-the-type-level">https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide#constructingbinding-no-longer-needed-at-the-type-level</a></li>
<li><a href="https://github.com/spring-projects/spring-boot/issues/33471">https://github.com/spring-projects/spring-boot/issues/33471</a></li>
</ul>
</li>
</ul>
</li>
<li><p>spring rest docs requestParameters -&gt; queryParameters or formParameters</p>
<ul>
<li>epgaes:restdocs-api-spec 은 17 이후 버전으로</li>
</ul>
</li>
<li><p>spring cloud <code>2022.0.x</code></p>
<ul>
<li><a href="https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2022.0-Release-Notes">https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2022.0-Release-Notes</a></li>
</ul>
</li>
<li><p><code>MySQLDialect</code> 의 하위 버전들이 dperecated(최소버전 8)</p>
</li>
</ul>
<p>++ aws 관련 라이브러리 사용할 경우 slf4j 관련 버전 충돌이 발생할 수 있다. 나는 athena jdbc를 사용할때 gradle에서 별도 저장소의 jar 파일을 직접 가져와 사용하고 있는데 이 부분에서 문제가 생겼다.</p>
<h2 id="31x-로-버전업">3.1.x 로 버전업</h2>
<p><a href="https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.1-Release-Notes">https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.1-Release-Notes</a></p>
<ul>
<li>gradle 최소버전 7.5 <ul>
<li>8.x 마이그레이션 가이드 <a href="https://docs.gradle.org/current/userguide/upgrading_version_7.html">https://docs.gradle.org/current/userguide/upgrading_version_7.html</a></li>
</ul>
</li>
</ul>
<ul>
<li><p>spring-boot-docker-compose 가 추가됐다. <a href="https://docs.spring.io/spring-boot/docs/3.1.0/reference/html/features.html#features.docker-compose">https://docs.spring.io/spring-boot/docs/3.1.0/reference/html/features.html#features.docker-compose</a></p>
</li>
<li><p>Testcontainers를 스프링부트에서 정식 지원한다. <a href="https://docs.spring.io/spring-boot/docs/3.1.0/reference/html/features.html#features.testing.testcontainers">https://docs.spring.io/spring-boot/docs/3.1.0/reference/html/features.html#features.testing.testcontainers</a></p>
</li>
</ul>
<p>뜬금없이 Unable to start web server 에러 발생 시</p>
<ul>
<li><code>ServletWebServerApplicationContext.selfInitialize</code> 단계에서 <code>dispatcherServlet</code> 두 번 등록하는 것이 확인 될 경우</li>
<li><code>ServletRegistrationBean</code> 수동 등록 중인지 확인<ul>
<li><a href="https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.1-Release-Notes#servlet-and-filter-registrations">https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.1-Release-Notes#servlet-and-filter-registrations</a> 이 것과 관련 있을 것으로 추정</li>
</ul>
</li>
</ul>
<pre><code>Application run failed
org.springframework.context.ApplicationContextException: Unable to start web server
//...
Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
//...
Caused by: java.lang.IllegalStateException: Failed to register &#39;servlet dispatcherServlet&#39; on the servlet context. Possibly already registered?
//...
    at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:126)
    ... 13 common frames omitted</code></pre><h2 id="32x-로-버전업">3.2.x 로 버전업</h2>
<p><a href="https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes">https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes</a></p>
<ul>
<li><p>spring cloud 버전을 2023.0.2으로 올려야 한다. 
(<a href="https://github.com/spring-cloud/spring-cloud-release/wiki/Supported-Versions#supported-releases">https://github.com/spring-cloud/spring-cloud-release/wiki/Supported-Versions#supported-releases</a>)</p>
</li>
<li><p>영속 프레임워크 여러개 사용하는 경우 Bean 등록 중복되는 경우 체크 필요</p>
<ul>
<li>나의 경우 jpa + mybatis를 사용하고, 같은 패키지를 읽어오도록 할 경우 <code>ConflictingBeanDefinitionException</code> 발생</li>
<li>ClassPathBeanDefinitionScanner.checkCandidate 결과가 달라졌을 것으로 추정 (상속구조가 달라졌을 수도 있는데 추후 확인 해봐야 할듯) </li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[spring boot + batch, servlet web server를 호출할때?]]></title>
            <link>https://velog.io/@dae-hwa/spring-boot-batch-servlet-web-server%EB%A5%BC-%ED%98%B8%EC%B6%9C%ED%95%A0%EB%95%8C</link>
            <guid>https://velog.io/@dae-hwa/spring-boot-batch-servlet-web-server%EB%A5%BC-%ED%98%B8%EC%B6%9C%ED%95%A0%EB%95%8C</guid>
            <pubDate>Thu, 30 May 2024 10:29:43 GMT</pubDate>
            <description><![CDATA[<p>batch 혹은 web server 호출 이외에도 독립적인 스프링 부트 애플리케이션을 실행하고 싶은데, 웹서버를 찾을 경우</p>
<p>의존성에 아래 사항이 포함되어 있는지 확인 후 제거</p>
<pre><code class="language-java">    private static final String[] SERVLET_INDICATOR_CLASSES = new String[]{&quot;jakarta.servlet.Servlet&quot;, &quot;org.springframework.web.context.ConfigurableWebApplicationContext&quot;};
    private static final String WEBMVC_INDICATOR_CLASS = &quot;org.springframework.web.servlet.DispatcherServlet&quot;;
    private static final String WEBFLUX_INDICATOR_CLASS = &quot;org.springframework.web.reactive.DispatcherHandler&quot;;
    private static final String JERSEY_INDICATOR_CLASS = &quot;org.glassfish.jersey.servlet.ServletContainer&quot;;</code></pre>
<p>우회하려면 아래와 같이 webApplicationType을 강제로 넣어준다(SpringApplication 생성자 호출 이후 시점).</p>
<pre><code class="language-kotlin">SpringApplication.exit(runApplication&lt;AdPaymentBatchApplication&gt;(*args){
    // setWebApplicationType(WebApplicationType.NONE)
    webApplicationType = org.springframework.boot.WebApplicationType.NONE
}</code></pre>
<hr>
<p>삽질...</p>
<p><code>Check your application&#39;s dependencies for a supported servlet web server. Check the configured web application type.  org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.boot.web.context.MissingWebServerFactoryBeanException: No qualifying bean of type &#39;org.springframework.boot.web.servlet.server.ServletWebServerFactory&#39; available: Unable to start AnnotationConfigServletWebServerApplicationContext due to missing ServletWebServerFactory bean</code></p>
<p>Trace 추적하여
<code>SpringApplication.run</code> 확인</p>
<pre><code class="language-java">try {
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
    this.configureIgnoreBeanInfo(environment);
    Banner printedBanner = this.printBanner(environment);
    context = this.createApplicationContext();
    context.setApplicationStartup(this.applicationStartup);
    this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
    // 여기서 ConfigurableApplicationContext 가 ServletWebServerApplicationContext 로 세팅 돼있음
    this.refreshContext(context);
    this.afterRefresh(context, applicationArguments);
    Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
    if (this.logStartupInfo) {
        (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup);
    }

    listeners.started(context, timeTakenToStartup);
    this.callRunners(context, applicationArguments);
}</code></pre>
<p>batch job 등록: <code>AbstractApplicationContext.refresh</code>의 <code>finishBeanFactoryInitialization(beanFactory)</code></p>
<p>실제 Job 실행은 <code>JobLauncherApplicationRunner</code>가 callRunners 단계예서 실행된다.</p>
<p>그럼 확인해야할 것 </p>
<ul>
<li>context 올바르게 불러오는지<ul>
<li><code>WebApplicationType.NONE</code> 이어야 한다. 그래야 기본 컨텍스트를 가져온다(현재 문제는 서블릿 컨텍스트를 가져오는 것이기 때문에...)</li>
</ul>
</li>
<li>runner 잘 등록됐는지</li>
</ul>
<p>우선 원인은 <code>javax.servlet:javax.servlet-api</code> 가 의존성에 있었던 것...</p>
<p>의존성 제거해주거나 우회하거나 하면 되긴 한다</p>
<p>허나... 웹서버가 아닌 애플리케이션에서 참조하는 모듈에서 서블릿에 의존적인 코드를 작성한게 문제의 시발점이 아니었나 싶다.</p>
<pre><code>비동기 호출시 trace 추적 우회를 하고 싶었는데, 
https://jsonobject.tistory.com/468 이런 식으로 시도해본 사람도 있고
Spring Cloud Sleuth를 사용하는게 목적에 더 적합했을 수도 있겠다</code></pre><p>더 근본적으론 revert 먼저 하고 작업하지 않았던 점... 부끄럽지만 반성하자</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[spring hikaricp datasource properties]]></title>
            <link>https://velog.io/@dae-hwa/spring-hikaricp-datasource-properties</link>
            <guid>https://velog.io/@dae-hwa/spring-hikaricp-datasource-properties</guid>
            <pubDate>Thu, 25 Jan 2024 08:13:48 GMT</pubDate>
            <description><![CDATA[<p>url 뒤에 붙는 설정을 빼고 싶었음</p>
<p><code>HikariConfig()</code> 내부에 프로퍼티가 전달 돼야 한다.</p>
<p><code>HikariDataSource(HikariConfig configuration)</code>생성자 사용 시 <code>sealed = true</code> 상태로 바뀜 (<code>fastPathPool</code> 로 생성하면서 등록 해버린다)</p>
<p><code>HikariDataSource</code>에서 내부 프로퍼티 맵에 직접 접근 시 변경은 되지만 적용은 안 되는 것으로 보임
직접 접근이 아니라 <code>addDataSourceProperty</code> 메소드 사용 시 <code>sealed</code> 체크를 하기 때문에 <code>The configuration of the pool is sealed once started. Use HikariConfigMXBean for runtime changes</code> 와 같은 에러가 발생한다.</p>
<p>생각나는 방법은 세 가지 정도 있는데, </p>
<ol>
<li><p>데이터 소스를 직접 만들어준다</p>
<p> 생성자에 config를 넣어주지 않았을 경우 <code>fastPathPool</code>에 등록 되지 않고 당연히 <code>seal()</code>도 동작하지 않는다.
 대신 <code>getConnection()</code>을 할때 <code>sealed = true</code>로 변경된다.
 따라서 데이터 소스를 만들때 프로퍼티를 넣어주면 된다.</p>
<pre><code class="language-kotlin"> HikariDataSource().apply{
     addDataSourceProperty(&quot;zeroDateTimeBehavior&quot;, &quot;convertToNull&quot;)
 }</code></pre>
</li>
<li><p>HikariConfig를 동적으로 받아온다면 </p>
<ol>
<li><p>DataSource로 만들기 전에 넣어준다.</p>
<pre><code class="language-kotlin">   @ConfigurationProperties(prefix = &quot;spring.datasource.asdf&quot;)
   fun asdfDataSourceProperties(): HikariConfig = HikariConfig().apply {
       addDataSourceConfig(&quot;zeroDateTimeBehavior&quot;, &quot;convertToNull&quot;)
   })</code></pre>
</li>
<li><p>그냥 yml에 작성해버린다</p>
<pre><code class="language-yml"># ...
 driver-class-name: ...
 username: ...
 password: ...
 data-source-properties:
   zeroDateTimeBehavior: convertToNull
 ...</code></pre>
</li>
</ol>
</li>
</ol>
<p>아주 간단하게 테스트 해보자면</p>
<pre><code class="language-kotlin">JdbcTemplate(asdfDataSource).queryForMap(
    // zeroDateTimeBehavior 적용이 되지 않으면 에러 발생
    &quot;SELECT date(&#39;0000-00-00 00:00:00&#39;)&quot; 
)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Mockito Spy TooManyActualInvocations]]></title>
            <link>https://velog.io/@dae-hwa/Mockito-Spy-TooManyActualInvocations</link>
            <guid>https://velog.io/@dae-hwa/Mockito-Spy-TooManyActualInvocations</guid>
            <pubDate>Wed, 08 Mar 2023 09:12:31 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-java">verify(spyClass, timeout(..).times(..))....</code></pre>
<p>초기화시켜줘야 한다.</p>
<pre><code class="language-java">Mockito.reset(spyClass);</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kinesis Read Throughput Exceeded]]></title>
            <link>https://velog.io/@dae-hwa/Kinesis-ReadThroughputExceededException</link>
            <guid>https://velog.io/@dae-hwa/Kinesis-ReadThroughputExceededException</guid>
            <pubDate>Thu, 02 Mar 2023 14:37:08 GMT</pubDate>
            <description><![CDATA[<p>기존 스트림에서 크로스 어카운트로 데이터를 넘겨야 하는 작업이 필요한데, 람다에 키네시스 트리거를 붙이기만 하면 <code>getrecords iterator age</code> 와 <code>Read Throughput Exceeded</code> 수치가 오른다.</p>
<p><code>getrecords iterator age</code> 는 람다가 제대로 돌지 못하는거니 배치 사이즈와 윈도우 타임 조절로 해결이 가능했다. 하지만 <code>Read Throughput Exceeded</code>는 떨어지지 않는다.</p>
<p><a href="https://aws.amazon.com/ko/premiumsupport/knowledge-center/kinesis-readprovisionedthroughputexceeded/">https://aws.amazon.com/ko/premiumsupport/knowledge-center/kinesis-readprovisionedthroughputexceeded/</a></p>
<p>기존 스트림 한 대에 파이어호스가 다섯 대 붙어있는데 배치나 윈도우 사이즈 조절로는 해결을 하지 못해 이 것이 원인일 것이라 추정</p>
<p>향상된 팬아웃(enhanced fan-out)을 이용해 해결했다.</p>
<p><a href="https://docs.aws.amazon.com/ko_kr/streams/latest/dev/enhanced-consumers.html">https://docs.aws.amazon.com/ko_kr/streams/latest/dev/enhanced-consumers.html</a></p>
<p>다른 컨슈머와 경합하지 않고 독립적으로 끌어온다.</p>
<p><a href="https://docs.aws.amazon.com/kinesis/latest/APIReference/API_RegisterStreamConsumer.html">https://docs.aws.amazon.com/kinesis/latest/APIReference/API_RegisterStreamConsumer.html</a></p>
<p>를 이용하여 향상된 팬아웃 컨슈머를 생성한 뒤 트리거의 컨슈머로 선택해주면 된다.</p>
<p>사용법은 간단하지만 정확한 정보 찾는게 너무 힘들었다. AWS 제품들을 쓰다보면 잘 만들긴 했지만 사용법이 불친절한 경우가 많다. 숨겨져 있는걸 직접 찾아서 써야 한다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[mysql jdbc connection timezone]]></title>
            <link>https://velog.io/@dae-hwa/mysql-connection-timezone</link>
            <guid>https://velog.io/@dae-hwa/mysql-connection-timezone</guid>
            <pubDate>Tue, 27 Dec 2022 11:44:23 GMT</pubDate>
            <description><![CDATA[<p><a href="https://dev.mysql.com/doc/connector-j/en/connector-j-time-instants.html">https://dev.mysql.com/doc/connector-j/en/connector-j-time-instants.html</a></p>
<p>여기에 케이스 별로 설명이 나와있다</p>
<p>Connector/J 버전 8.0.23 이후부터 사용 가능</p>
<p><a href="https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-datetime-types-processing.html">https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-datetime-types-processing.html</a></p>
<p><code>prepareInstants</code>
<code>connectionTimeZone</code>
<code>forceConnectionTimeZoneToSession</code>
위 세개를 잘 조합해야 한다.</p>
<p>tbw</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[intellij gradle exclude bin directory]]></title>
            <link>https://velog.io/@dae-hwa/intellij-gradle-exclude-bin-directory</link>
            <guid>https://velog.io/@dae-hwa/intellij-gradle-exclude-bin-directory</guid>
            <pubDate>Tue, 27 Dec 2022 11:43:49 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-kotlin">// build.gradle.kts
plugins {
    idea
}

idea.module {
    excludeDirs.addAll(allprojects.map { it.file(&quot;bin&quot;) })
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring REST Docs에 DTO의 Validation 정보 담기]]></title>
            <link>https://velog.io/@dae-hwa/REST-Docs%EC%97%90-DTO%EC%9D%98-Validation-%EC%A0%95%EB%B3%B4-%EB%8B%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dae-hwa/REST-Docs%EC%97%90-DTO%EC%9D%98-Validation-%EC%A0%95%EB%B3%B4-%EB%8B%B4%EA%B8%B0</guid>
            <pubDate>Fri, 10 Dec 2021 10:49:55 GMT</pubDate>
            <description><![CDATA[<p>Spring을 이용해서 API의 제약사항을 표현할때 Bean Validation을 많이 사용한다. 필드에 표현은 쉽게 되는데, 이걸 REST Docs에 표현하자니 description에 적어줘야 할지... 애매해진다.</p>
<p>Validation 정보를 담기 위한 커스텀 템플릿과 <code>ConstraintDescriptions</code>을 이용해서 Bean Validation 정보를 가져오는 방법에 대해 알아보자.</p>
<blockquote>
<p>REST Docs에 대한 사용법은 시리즈의 이전 포스팅을 참고해주세요!</p>
</blockquote>
<h2 id="커스텀-템플릿-만들기">커스텀 템플릿 만들기</h2>
<p>일단은 스니펫 템플릿을 개조해본다.</p>
<pre><code class="language-adoc">// request-fields.snippet
|===
|Path|Type|Description|Constraints

{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}{{constraints}}{{/tableCellContent}}

{{/fields}}
|===
</code></pre>
<pre><code class="language-java">requestFields(
    fieldWithPath(&quot;name&quot;)
      .type(JsonFieldType.STRING)
      .description(&quot;이름&quot;)
      .attributes(key(&quot;constraints&quot;).value(&quot;not null&quot;))
);</code></pre>
<p><img src="https://images.velog.io/images/dae-hwa/post/ede0658b-54a2-4fef-a73f-3f139763d520/image-20211209175140617.png" alt="customtemplate1"></p>
<p>하지만 이걸로는 안 된다. not null 이라고 직접 입력한 것 뿐이다.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/9edaa197-5f41-4c5a-a1fb-7a7a423be2f3/image-20211209125633404.png" alt="customtemplate2"></p>
<p>필드 개수가 얼마 안 되면 어떻게 해보겠지만, 이렇게 필드가 많아지면 너무 힘들어진다. 자동화 시켜보자.</p>
<h2 id="validation-정보-불러오기">Validation 정보 불러오기</h2>
<h3 id="constraintdescriptions">ConstraintDescriptions</h3>
<p><code>spring-restdocs-core</code>에 <code>ConstraintDescriptions</code>가 들어있다. 특정 클래스에 제약사항이 있는지 확인해준다.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/e134f0cb-a062-4762-8eab-843b873e6e19/image-20211209181841196.png" alt="ConstraintDescriptions1"></p>
<pre><code class="language-java">ConstraintDescriptions simpleRequestConstraints = new ConstraintDescriptions(SimpleRequest.class);
List&lt;String&gt; nameDescription = simpleRequestConstraints.descriptionsForProperty(&quot;name&quot;);</code></pre>
<p>이렇게 하면 <code>SimpleRequest</code>의 필드에 붙은 제약사항을 불러온다. 이대로 넣어주기만 하면 된다.</p>
<pre><code class="language-java">requestFields(
    fieldWithPath(&quot;name&quot;)
      .type(JsonFieldType.STRING)
      .description(&quot;이름&quot;)
      .attributes(key(&quot;constraints&quot;).value(nameDescription))
)</code></pre>
<p>다만, attributes에 constraint가 없을 경우 `MustacheException$Context 예외가 발생할 수 있다. 스니펫 템플릿을 아래처럼 변경해주면 된다.</p>
<pre><code class="language-adoc">// request-fields.snippet
|===
|Path|Type|Description|Constraints

{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}{{#constraints}}{{.}} +
{{/constraints}}{{/tableCellContent}}

{{/fields}}
|===</code></pre>
<p><code>{{.}}</code>은 필드가 존재할때만 출력을 해주는 문법이다. <code>+</code>는 강제 개행인데, 한 줄씩 띄어준다. 쉼표같이 선호하는 표현 방식이 있으면 바꿔서 쓰면 된다.</p>
<h3 id="내부-동작">내부 동작</h3>
<p><code>org.springframework.restdocs.constraints</code> 패키지 안은 아래와 같이 구성된다.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/59f6461e-f2ac-457e-a824-f5f24e646890/image-20211210162316640.png" alt="ConstraintDescriptions2"></p>
<p><code>ConstraintDescriptions.descriptionsForProperty</code>에 프로퍼티 명을 매개변수로 넣으면 <code>ConstraintResolver</code>는 해당 프로퍼티에 맞는 <code>Constraint</code>를 가져온다. 기본 구현인 <code>ValidatorConstraintResolver</code>는 내부적으로 Bean Validation의 <code>Validator</code> 객체를 사용한다.</p>
<p>Bean Validation을 이용해서 해당 클래스의 필드에 붙은 <code>@NotNull</code>과 같은 constraint를 가져온다. Java Bean 규격을 따르기 때문에 필드명과 json field의 이름이 일치하지 않는 경우 주의해야 한다.</p>
<blockquote>
<p>만약 json field가 user_id일 경우 카멜케이스로 변경해서 넣어줘야 한다. 내부에 있는 <code>PropertyDescriptor</code>가 <a href="https://en.wikipedia.org/wiki/JavaBeans#JavaBean_conventions">Java Bean 컨벤션</a>에 따라 프로퍼티를 찾기 때문이다.</p>
</blockquote>
<p><code>ConstraintDescriptionResolver</code>는 <code>ConstraintResolver</code>가 찾아온 <code>Constraint</code>을 문자열로 변환해준다. 기본적으로 Bean Validator 2.0과 Hibernate Validator 스펙에 맞게 지원한다. 커스텀 제약사항이 있다면 해당 프로퍼티를 객체 생성시에 넣어주면 된다.</p>
<blockquote>
<p>참고 - <a href="https://docs.spring.io/spring-restdocs/docs/2.0.5.RELEASE/reference/html5/#documenting-your-api-constraints-describing">기본 지원 constraints</a></p>
</blockquote>
<h2 id="마치며">마치며</h2>
<p>공식문서에 깃허브 링크만 덜렁 보여주면서 따라해라고 돼있어 가이드를 작성해봤다. Attribute를 직접 넣어주는게 단점이지만, 반대로 Bean Validation을 사용하는 DTO라면 어디에든 넣어줄 수 있기 때문에 request parameter에도 똑같이 적용할 수 있다.</p>
<p><code>ConstraintDescriptions</code>도 쓰기 쉽게 잘 만들어 놓아서 Java Bean을 사용해야 한다는 사실만 숙지하면 쉽게 사용할 수 있다. 반복될 가능성이 높기 때문에 래핑해서 쓰는 것도 좋아보인다.</p>
<p>그럼에도 고민 되는 부분은 컬럼 항목이 많아지다보니 표가 복잡해진다는 것인데, <code>[%autowidth.stretch]</code>를 표에 붙여 타협하고 있다. 여유가 있으면 스니펫을 새로 만들면 되지만 시간을 꽤 투자해야 할 것 같아 망설여진다.</p>
<p>혹시 좋은 아이디어가 있으면 공유 부탁드립니다 ㅎㅎ</p>
<hr>
<h2 id="references">References</h2>
<ul>
<li><a href="https://docs.spring.io/spring-restdocs/docs/2.0.5.RELEASE/reference/html5/#documenting-your-api-constraints">https://docs.spring.io/spring-restdocs/docs/2.0.5.RELEASE/reference/html5/#documenting-your-api-constraints</a></li>
<li><a href="https://github.com/spring-projects/spring-restdocs/blob/main/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java">https://github.com/spring-projects/spring-restdocs/blob/main/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java</a></li>
<li><a href="https://github.com/spring-projects/spring-restdocs/blob/main/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java">https://github.com/spring-projects/spring-restdocs/blob/main/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring REST Docs 살펴볼만한 기능들]]></title>
            <link>https://velog.io/@dae-hwa/Spring-REST-Docs-%EC%82%B4%ED%8E%B4%EB%B3%BC%EB%A7%8C%ED%95%9C-%EA%B8%B0%EB%8A%A5%EB%93%A4</link>
            <guid>https://velog.io/@dae-hwa/Spring-REST-Docs-%EC%82%B4%ED%8E%B4%EB%B3%BC%EB%A7%8C%ED%95%9C-%EA%B8%B0%EB%8A%A5%EB%93%A4</guid>
            <pubDate>Wed, 08 Dec 2021 13:00:14 GMT</pubDate>
            <description><![CDATA[<h2 id="operation">Operation</h2>
<p>adoc파일에서 다른 adoc파일을 삽입하려면 <code>include</code> 문법을 사용해야 한다.</p>
<pre><code class="language-adoc">// index.adoc
= Rest Docs Example

== Simple Service

=== curl request

include::{snippets}/simple-read/curl-request.adoc[]

=== Path parameters

include::{snippets}/simple-read/path-parameters.adoc[]

=== Request parameters

include::{snippets}/simple-read/request-parameters.adoc[]

=== Response Fields

include::{snippets}/simple-read/response-fields.adoc[]

=== HTTP request

include::{snippets}/simple-read/http-request.adoc[]

=== HTTP response

include::{snippets}/simple-read/http-response.adoc[]</code></pre>
<p>많이 불편하다. 테스트 메소드 하나당 기본적으로 6개, 이것 저것 하다보면 10개에 가까운 adoc 파일이 생긴다. 테스트가 한 두개도 아니고 매 번 복붙하는데도 한계가 있다.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/c3ccbbb3-2f27-40d0-8c3c-13200e0729b1/image-20211102172516322.png" alt="Operation1"></p>
<p><code>operation</code>을 사용하면 include 여러개 하는 고통을 없애준다.</p>
<pre><code class="language-adoc">= Rest Docs Example

== Simple Service

operation::simple-read[]</code></pre>
<p>원하는 것만 골라서 가져올 수도 있다.</p>
<pre><code class="language-adoc">snippets=&#39;curl-request,path-parameters,request-parameters,response-fields,http-request,http-response&#39;</code></pre>
<p>테이블만 불러오는게 아니라, 헤더도 단계에 맞게 자동으로 생성해준다.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/ddf11d71-8f99-47a1-b8ea-5da96c9f203f/image-20211208181924595.png" alt="Operation2"></p>
<p>자동 생성되는 헤더가 마음에 안 든다면, 따로 정의할 수도 있다.</p>
<p><code>:operation-curl-request-title: Example request</code>와 같이 정의해주면, <code>curl-request-title</code> 의 헤더가 바뀐다.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/181c4fa5-7baa-4f03-88bf-64552c08265c/image-20211208182203204.png" alt="Operation3"></p>
<p>asciidoctor 의존성이 없으면 제대로 동작하지 않는다. 잘 안되면 확인해보자.</p>
<h2 id="pretty-print">pretty print</h2>
<p>기본적으로 요청과 응답의 body가 텍스트로 쭉 나열된다. 크기가 작으면 상관 없지만, json 객체가 커진다면 제대로 확인하기 힘들어진다.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/41ecb576-9afb-4870-bc3b-1bd346fd4335/image-20211208182505623.png" alt="pretty print1"></p>
<p>pretty print 기능은 말 그대로 예쁘게 포메팅해준다. </p>
<p>스니펫을 모아 <code>DocumentFilter</code>를 만들때 넣어주면 된다.</p>
<pre><code class="language-java">RestDocumentationFilter restDocumentationFilter = document(
        &quot;simple-read&quot;,
        preprocessRequest(prettyPrint()),
        preprocessResponse(prettyPrint()),
        simplePathParameterSnippet(),
        simpleRequestParameterSnippet(),
        simpleResponseFieldsSnippet()
);</code></pre>
<p>기본 설정으로 사용하려면 초기화할때 넣어준다.</p>
<pre><code class="language-java">@BeforeEach
void setUpRestDocs(RestDocumentationContextProvider restDocumentation) {
    Filter documentationConfiguration = documentationConfiguration(restDocumentation)
            .operationPreprocessors()
            .withRequestDefaults(prettyPrint())
            .withResponseDefaults(prettyPrint());

    this.spec = new RequestSpecBuilder()
            .addFilter(documentationConfiguration)
            .build();
}</code></pre>
<p><img src="https://images.velog.io/images/dae-hwa/post/b65cab5d-6ee5-4de4-bd0d-62a7c752b08f/image-20211208190159253.png" alt="pretty print2"></p>
<h2 id="매개변수화된-출력-디렉토리">매개변수화된 출력 디렉토리</h2>
<p><code>DocumentFilter</code>를 만들면서 어떤 디렉토리에 저장할지 정해진다.</p>
<pre><code class="language-java">RestDocumentationFilter restDocumentationFilter = document(
        &quot;simple-read&quot;, // 디렉토리명
        simplePathParameterSnippet(),
        simpleRequestParameterSnippet(),
        simpleResponseFieldsSnippet()
);</code></pre>
<p>일일이 이름을 정해줘야 한다. 개발자의 숙명이지만 항상 어렵다.</p>
<p>이런 고통을 덜어주는 기능이 있다. 미리 정해진 변수를 적어주면 클래스명과 메소드명 그리고 스텝 순서를 불러올 수 있다.</p>
<table>
<thead>
<tr>
<th align="left">Parameter</th>
<th align="left">Description</th>
</tr>
</thead>
<tbody><tr>
<td align="left">{methodName}</td>
<td align="left">메소드명</td>
</tr>
<tr>
<td align="left">{method-name}</td>
<td align="left">메소드명을 kebab-case로 포메팅 한다</td>
</tr>
<tr>
<td align="left">{meth</td>
<td align="left">메소드명을 snake_case로 포메팅 한다</td>
</tr>
<tr>
<td align="left">{ClassName}</td>
<td align="left">클래스명</td>
</tr>
<tr>
<td align="left">{class-name}</td>
<td align="left">클래스명을 kebab-case로 포메팅 한다</td>
</tr>
<tr>
<td align="left">{class_name}</td>
<td align="left">클래스명을 snake_case로 포메팅 한다</td>
</tr>
<tr>
<td align="left">{step}</td>
<td align="left">현재 테스트의 실행 순서를 불러온다</td>
</tr>
</tbody></table>
<p>테스트를 나눠서 애매한 경우도 처리 가능하다. 테스트 메소드를 기준으로 이름을 만들어주기 때문이다.</p>
<pre><code class="language-java">RestDocumentationFilter restDocumentationFilter = document(
        &quot;{class-name}/{method-name}&quot;,
        preprocessResponse(prettyPrint()),
        simplePathParameterSnippet(),
        simpleRequestParameterSnippet(),
        simpleResponseFieldsSnippet()
);

@Test
void test1() {
    RequestSpecification given = RestAssured.given(this.spec)
                .baseUri(BASE_URL)
                .port(port)
                .pathParam(&quot;id&quot;, 1)
                .queryParam(&quot;name&quot;, &quot;name&quot;)
                .filter(restDocumentationFilter);
}

@Test
void test2() {
    RequestSpecification given = RestAssured.given(this.spec)
                .baseUri(BASE_URL)
                .port(port)
                .pathParam(&quot;id&quot;, 1)
                .queryParam(&quot;name&quot;, &quot;name&quot;)
                .filter(restDocumentationFilter);
}</code></pre>
<p>위의 경우는 <code>클래스명/test1</code>과 <code>클래스명/test2</code>가 만들어진다.</p>
<p>MockMvc와 REST Assured에서만 사용 가능하고, WebTestClient는 사용 불가능하다.</p>
<h2 id="배열-표현">배열 표현</h2>
<p>배열은 <code>a.b[].c</code>와 같이 표현할 수 있다.</p>
<blockquote>
<p>이외에도 여러가지 표현식이 있는데, <a href="https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documenting-your-api-request-response-payloads-fields-json-field-paths">JSON Field Paths</a>를 참고하자.</p>
</blockquote>
<p>일반적인 경우에는 문제가 없겠지만, 배열을 동적으로 생성하거나 해야 하는 경우에 표현이 애매할 수 있다. 이럴 때 <code>a.*[].c</code>와 같이 와일드 카드로 처리해줄 수 있다.</p>
<h2 id="불필요한-헤더-제거">불필요한 헤더 제거</h2>
<p><img src="https://images.velog.io/images/dae-hwa/post/337ff399-c5a7-4410-9f7c-2d92186cb8cf/image-20211208205718179.png" alt="remove header1"></p>
<p>API문서에 불필요한 헤더가 포함되어 보인다. 이를 없애줄 수도 있다.</p>
<pre><code class="language-java">@BeforeEach
void setUpRestDocs(RestDocumentationContextProvider restDocumentation) {
    Filter documentationConfiguration = documentationConfiguration(restDocumentation)
            .operationPreprocessors()
            .withRequestDefaults(prettyPrint())
            .withResponseDefaults(
                    removeHeaders(
                            &quot;Transfer-Encoding&quot;,
                            &quot;Date&quot;,
                            &quot;Keep-Alive&quot;,
                            &quot;Connection&quot;
                    ),
                    prettyPrint()
            );

    this.spec = new RequestSpecBuilder()
            .addFilter(documentationConfiguration)
            .build();
}</code></pre>
<p><img src="https://images.velog.io/images/dae-hwa/post/5ba808c7-49b8-438b-9938-41d6bff77a9e/image-20211208205911864.png" alt="remove header2"></p>
<h2 id="템플릿-커스텀">템플릿 커스텀</h2>
<p>문서에 더 표현하고 싶은 정보가 있으면 템플릿을 커스텀할 수도 있다.</p>
<p>기본 템플릿은 <code>spring-restdocs-core</code>에 들어있는데, <code>org.springframework.restdocs.templates.asciidoctor</code>에 들어있다.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/83b16060-7e53-499d-87fc-21dc0e732bef/image-20211208210616856.png" alt="template1"></p>
<p><code>.snippet</code>파일이 템플릿이고 여기에 있는 템플릿을 오버라이딩 할 수도 있다.</p>
<p>오버라이딩하고 싶은 템플릿은 <code>src/test/resources/org/springframework/restdocs/templates/asciidoctor</code>에 넣어주면 된다. 예를 들어, <code>curl-request</code>를 새롭게 정의하고 싶으면 <code>src/test/resources/org/springframework/restdocs/templates/asciidoctor.curl-request.snippet</code>을 만들어주면 된다.</p>
<p>만드는 방법은 간단하다. <code>response-fields</code>에 <code>optional</code>을 표현하고 싶다면 해당 필드를 추가해주면 된다. AsciiDoc문법을 사용한다.</p>
<blockquote>
<p>참고 - <a href="https://docs.asciidoctor.org/asciidoc/latest/syntax-quick-reference/">AsciiDoc Syntax</a></p>
</blockquote>
<p>별도의 설정을 통해 마크다운을 사용할 수도 있는데, mustache와 함께 쓰기에는 adoc가 더 좋았다.</p>
<pre><code class="language-adoc">|===
|Path|Type|Description|Optional

{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}_{{optional}}_{{/tableCellContent}}

{{/fields}}
|===</code></pre>
<p>표의 컬럼에 Optional 항목을 추가해주고 row마다 optional을 불러오도록 했다.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/7a1646a7-d15e-47eb-b3e4-f32d2600977d/image-20211208212006077.png" alt="template2"></p>
<p>기본적으로 사용되는 옵션 외에 다른 항목을 넣어줄 수 있다. 스니펫을 만들때 attribute를 넣어주면 된다.</p>
<pre><code class="language-java">responseFields(
    fieldWithPath(&quot;id&quot;)
        .type(JsonFieldType.NUMBER)
         .description(&quot;아이디&quot;)
        .attributes(key(&quot;custom attribute&quot;).value(&quot;custom attribute value&quot;)),
    fieldWithPath(&quot;name&quot;)
        .type(JsonFieldType.STRING)
        .description(&quot;이름&quot;)
        .attributes(key(&quot;custom attribute&quot;).value(&quot;custom attribute value&quot;))
);</code></pre>
<pre><code class="language-adoc">|===
|Path|Type|Description|Optional|Custom attribute

{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}_{{optional}}_{{/tableCellContent}}
|{{#tableCellContent}}{{custom attribute}}{{/tableCellContent}}

{{/fields}}
|===</code></pre>
<p><img src="https://images.velog.io/images/dae-hwa/post/ac8ffac0-5dd1-49b2-915b-d852ccd8ddf3/image-20211208214857633.png" alt="template3"></p>
<p>원하는 스니펫이 없을 경우 아예 스니펫을 새로 만들 수도 있다. <a href="https://medium.com/@rfrankel_8960/generating-custom-templated-snippets-with-spring-rest-docs-d136534a6f29">Generating Custom Templated Snippets with Spring REST Docs</a>에 잘 정리돼있다.</p>
<h2 id="마치며">마치며</h2>
<p>이외에도 여러가지 기능들이 있는데, 지금까지 개발하며 유용하다고 느낀 기능들 위주로 소개해봤다.</p>
<p>다른 기능들은 <a href="https://docs.spring.io/spring-restdocs/docs/current/reference/html5">공식문서</a>에 예시와 함께 정리가 잘 돼있다. 그리고 공식문서도 REST Docs로 만들어서 어떤 식으로 문서를 구성할지 고민되면 참고해도 좋을 것 같다.</p>
<hr>
<h2 id="references">References</h2>
<ul>
<li><p><a href="https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#working-with-asciidoctor-including-snippets-operation">https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#working-with-asciidoctor-including-snippets-operation</a></p>
</li>
<li><p><a href="https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#customizing-requests-and-responses">https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#customizing-requests-and-responses</a></p>
</li>
<li><p><a href="https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documentating-your-api-parameterized-output-directories">https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documentating-your-api-parameterized-output-directories</a></p>
</li>
<li><p><a href="https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documenting-your-api-request-response-payloads-fields-json">https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documenting-your-api-request-response-payloads-fields-json</a></p>
</li>
<li><p><a href="https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#customizing-requests-and-responses-preprocessors-remove-headers">https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#customizing-requests-and-responses-preprocessors-remove-headers</a></p>
</li>
<li><p><a href="https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documenting-your-api-customizing">https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documenting-your-api-customizing</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring REST Docs 기본 설정과 API 문서 만들어보기]]></title>
            <link>https://velog.io/@dae-hwa/Spring-REST-Docs-%EA%B8%B0%EB%B3%B8-%EC%84%A4%EC%A0%95%EA%B3%BC-API-%EB%AC%B8%EC%84%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dae-hwa/Spring-REST-Docs-%EA%B8%B0%EB%B3%B8-%EC%84%A4%EC%A0%95%EA%B3%BC-API-%EB%AC%B8%EC%84%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 02 Nov 2021 09:09:00 GMT</pubDate>
            <description><![CDATA[<p>Spring REST Docs는 RESTful 서비스 문서화에 사용된다. 인수테스트나 컨트롤러 테스트와 같은 엔드포인트 테스트에서 사용할 수 있다. 각 테스트에서 스니펫이라고 하는 문서 조각을 만들어주는데 이를 모아서 하나의 문서로 꾸밀 수 있다.</p>
<p>문서는 adoc라는 확장자를 사용하며 AsciiDoc 문법을 따른다. 때문에 러닝커브가 존재하고 단순 구현에도 시간이 소요된다는 단점이 있다. 반면, 반드시 테스트를 통과해야 문서가 생성되기 때문에 서비스 자체의 신뢰도가 높아진다. 즉, 단순히 문서화에만 관심이 있다면 다른 도구를 사용하는 것이 더 나을 수 있다.</p>
<h2 id="기본-설정">기본 설정</h2>
<p>Gradle 6 + Spring Boot 2.5.3 + JUnit5 + REST Assured를 사용하는 환경에 적용시켰다. 이외에도 MockMvc나 WebTestClient와 조합하여 사용할 수 있다. JUnit5외의 테스트 프레임워크도 지원한다고 한다.</p>
<h3 id="buildgradle-설정">build.gradle 설정</h3>
<pre><code class="language-groovy">plugins {
    id &#39;org.springframework.boot&#39; version &#39;2.5.3&#39;
    id &#39;io.spring.dependency-management&#39; version &#39;1.0.11.RELEASE&#39;
    id &#39;org.asciidoctor.convert&#39; version &#39;1.5.9.2&#39;
    id &#39;java&#39;
}

group = &#39;com.example&#39;
version = &#39;0.0.1-SNAPSHOT&#39;
sourceCompatibility = &#39;11&#39;

repositories {
    mavenCentral()
}

ext {
    // 스니펫 생성 위치 변수에 저장
    set(&#39;snippetsDir&#39;, file(&quot;build/generated-snippets&quot;))
}

dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
    testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;
    testImplementation &#39;io.rest-assured:rest-assured&#39;

    /* Rest Docs */
    asciidoctor &#39;org.springframework.restdocs:spring-restdocs-asciidoctor&#39;
    testImplementation &#39;org.springframework.restdocs:spring-restdocs-restassured&#39;
}

test {
    outputs.dir snippetsDir
    useJUnitPlatform()
}


asciidoctor {
    inputs.dir snippetsDir
    dependsOn test
}

task copyAsciidoctor(type: Copy) {
    dependsOn asciidoctor
    from &quot;${asciidoctor.outputDir}/html5&quot;
    into &quot;${sourceSets.main.output.resourcesDir}/static/docs&quot;
}

bootRun {
    dependsOn copyAsciidoctor
}</code></pre>
<p>Gradle 버전은 6을 사용하는게 좋다. 7 버전은 문서 생성 과정에서 아래와 같은 에러가 발생한다. 다른 우회방법이 있지만 복잡하다. 반드시 7버전을 사용해야하는 것이 아니면, 공식적으로 대응해줄때까지 기다리는게 좋아보인다.</p>
<pre><code>Some problems were found with the configuration of task &#39;:asciidoctor&#39; (type &#39;AsciidoctorTask&#39;).
  - In plugin &#39;org.asciidoctor.convert&#39; type &#39;org.asciidoctor.gradle.AsciidoctorTask&#39; method &#39;asGemPath()&#39; should not be annotated with: @Optional, @InputDirectory.</code></pre><blockquote>
<p><code>./gradle/wrapper/gradle-wrapper.properties</code>에서 해당 프로젝트의 gradle 버전을 변경할 수 있다.</p>
<pre><code class="language-properties">distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-bin.zip // 이 부분을 바꿔준다.
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists</code></pre>
</blockquote>
<h3 id="테스트-클래스-설정">테스트 클래스 설정</h3>
<pre><code class="language-java">@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(RestDocumentationExtension.class)
class SimpleAcceptanceTest {

    private static final String BASE_URL = &quot;http://localhost&quot;;

    @LocalServerPort
    private int port;

    protected RequestSpecification spec;

    @BeforeEach
    void setUpRestDocs(RestDocumentationContextProvider restDocumentation) {
        this.spec = new RequestSpecBuilder()
                .addFilter(documentationConfiguration(restDocumentation))
                .build();
    }
}</code></pre>
<h2 id="스니펫">스니펫</h2>
<p>말 그대로 문서 조각이다. 이를 이용하여 문서에 사용될 객체의 구조를 정의하고 테스트 과정에서 확인한 뒤 adoc 파일을 만들어준다. 스니펫 객체가 실제 요청, 응답에 사용되는 객체와 다르면 테스트 통과가 되지 않는다. 즉, 엔드포인트에 사용되는 객체의 구조를 검증하는 동시에 테스트 문서를 만드는 것이다.</p>
<blockquote>
<p><a href="https://docs.spring.io/spring-restdocs/docs/2.0.5.RELEASE/reference/html5/#getting-started-documentation-snippets">참고</a></p>
</blockquote>
<h3 id="스니펫-객체-만들기">스니펫 객체 만들기</h3>
<p>아래와 같이 스니펫 객체를 만들 수 있다.</p>
<pre><code class="language-java">private Snippet simplePathParameterSnippet() {
    return pathParameters(parameterWithName(&quot;id&quot;).description(&quot;아이디&quot;));
}

private Snippet simpleRequestParameterSnippet() {
    return requestParameters(parameterWithName(&quot;name&quot;).description(&quot;이름&quot;));
}

private Snippet simpleResponseFieldsSnippet() {
    return responseFields(
            fieldWithPath(&quot;id&quot;)
                    .type(JsonFieldType.NUMBER)
                    .description(&quot;아이디&quot;),
            fieldWithPath(&quot;name&quot;)
                    .type(JsonFieldType.STRING)
                    .description(&quot;이름&quot;)
    );
}</code></pre>
<h3 id="documentation-필터-만들기">documentation 필터 만들기</h3>
<p>이를 이용해 Restassured documentation 필터를 만들어 테스트에 넣어주면, 명시한 디렉토리에 adoc파일을 만들어준다.</p>
<pre><code class="language-java">@Test
void read() {
    RestDocumentationFilter restDocumentationFilter = document(
            // identifier, 이를 이용해 adoc파일을 저장할 디렉토리를 생성한다 
            &quot;{class_name}/{method_name}/&quot;,
            simplePathParameterSnippet(),
            simpleRequestParameterSnippet(),
            simpleResponseFieldsSnippet()
    );

    RequestSpecification given = RestAssured.given(this.spec)
        .baseUri(BASE_URL)
        .port(port)
        .pathParam(&quot;id&quot;, 1)
        .queryParam(&quot;name&quot;, &quot;name&quot;)
        // 필터를 넣어준다.
        .filter(restDocumentationFilter);

    Response actual = given.when()
                           .get(&quot;/simple/{id}&quot;);

    actual.then()
          .statusCode(HttpStatus.OK.value())
          .log().all();
}</code></pre>
<h3 id="asciidoctor-실행">asciidoctor 실행</h3>
<p><code>build.gradle</code>에 만들어둔 <code>asciidoctor</code> task를 실행시킨다.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/52c5a480-7ab1-494f-a7df-b63ca23214e6/image-20211102172731021.png" alt="asciidoctor task"></p>
<p><code>build.gradle</code>에 지정해준 <code>snippetsDir</code>의 하위 디렉토리에 adoc파일이 생성됐다.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/f325c739-da9d-425c-a28a-41df996f38e6/image-20211102172516322.png" alt="snippets"></p>
<blockquote>
<p><a href="https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documenting-your-api-default-snippets">참고 - 기본 생성 스니펫</a></p>
</blockquote>
<h2 id="api-문서-만들기">API 문서 만들기</h2>
<p>스니펫은 말 그대로 문서 조각이기때문에 스니펫을 모아주는 문서를 작성해야 API문서가 완성된다. adoc 문서를 만들고 서버를 띄워서 확인해보자.</p>
<blockquote>
<p><a href="https://docs.spring.io/spring-restdocs/docs/2.0.5.RELEASE/reference/html5/#getting-started-using-the-snippets">참고</a></p>
</blockquote>
<h3 id="adoc-문서-작성">adoc 문서 작성</h3>
<p>adoc파일들을 모아서 문서를 작성해보자. 위에서 말했듯 adoc 파일은 AsciiDoc 문법을 따르는데, 마크다운과 유사하지만 include 기능을 가지고 있다. 단순히 링크를 거는 것이 아니라, 해당 페이지 내에 붙여넣을 수 있다.</p>
<p><code>src/docs/asciidoc/</code> 디렉토리를 만들어 adoc파일을 만들어보자.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/2cdd4d24-8693-4762-b18d-1ac86d1250f7/image-20211102173647861.png" alt="adoc document"></p>
<p><code>include::</code> 문법을 이용해 스니펫을 불러올 수 있다.</p>
<pre><code class="language-adoc">// index.adoc
= Rest Docs Example

== Simple Service

=== curl request

include::{snippets}/simple-read/curl-request.adoc[]

=== Path parameters

include::{snippets}/simple-read/path-parameters.adoc[]

=== Request parameters

include::{snippets}/simple-read/request-parameters.adoc[]

=== Response Fields

include::{snippets}/simple-read/response-fields.adoc[]

=== HTTP request

include::{snippets}/simple-read/http-request.adoc[]

=== HTTP response

include::{snippets}/simple-read/http-response.adoc[]
</code></pre>
<blockquote>
<p><a href="https://plugins.jetbrains.com/plugin/7391-asciidoc">AsciiDoc 플러그인</a>을 이용하면 편리하다.</p>
</blockquote>
<blockquote>
<p><code>asciidoctor: WARNING: dropping line containing reference to missing attribute: snippets</code> 와 같은 에러메세지가 뜨면 <code>spring-restdocs-asciidoctor</code> 의존성이 제대로 정의되어있는지 확인해보자.</p>
</blockquote>
<p><code>asciidoctor</code> task를 실행시키면 <code>build/asciidoc/html5/index.html</code>이 생성된다.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/11c07546-bc5a-4749-9faa-d24163319f91/image-20211102175345578.png" alt="html document"></p>
<blockquote>
<p>adoc 파일 명이 index.adoc이기 때문에 index.html로 생성된 것이다.</p>
</blockquote>
<h3 id="서버-실행">서버 실행</h3>
<p><code>build.gradle</code>에 <code>copyAsciidoctor</code>를 정의해놨다. 이는 위에서 만들어진 <code>index.html</code>을 정적 파일 디렉토리로 복사해주는 작업이다. <code>bootRun</code>에서 실행되게 해놨으니 <code>bootRun</code>으로 서버를 실행시켜보자</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/d6e7335a-96c7-4110-912c-ec37cae4b5b0/image-20211102175711678.png" alt="html in static dir"></p>
<p>빌드 결과의 <code>resources</code>에 <code>index.html</code>이 복사됐다. 이외에도 필요한 작업에 <code>copyAsciidoctor</code>를 실행시켜주는 등의 방법으로 복사해주면 된다.</p>
<p>디렉토리 구조를 보면 알겠지만, <code>/docs/index.html</code>로 요청하면 API문서를 볼 수 있다.</p>
<p><img src="https://images.velog.io/images/dae-hwa/post/ebd95256-47ae-4612-bc49-5bf9fa09726b/image-20211102175942033.png" alt="api document"></p>
<h2 id="그래서-좋은점은">그래서 좋은점은?</h2>
<p>따로 설정을 해줘야하고 문서를 만드는데 시간도 꽤 든다. 복잡한 구조라면 스니펫 만드는 것 부터 만만치 않다. 하지만 테스트를 통과해야만 문서가 작성된다는 점이 큰 장점이라 생각한다. 스웨거를 이용하면 쉽게 문서화시키고 테스트도 테스트 문서에서 바로 해볼 수 있다. 하지만 잘 생각해보면 포스트맨으로 일일이 확인하는거랑 크게 차이가 없다. 그리고 복잡한 구조 혹은 자세한 표현을 하려면 스웨거에도 복잡한 설정이 들어갈 수 밖에 없다.</p>
<p>현재 진행중인 프로젝트에서 인수테스트를 하고 있지만 응답으로 오는 json객체 구조를 검증하는게 쉽지 않았다. 예를 들어, 단순히 객체만으로 비교를 하는 경우 필드명이 스네이크케이스로 되어야 하는데 카멜케이스로 json객체를 만들고 있는 것을 인지하기 어렵다. 하지만, 스니펫을 이용하여 사전에 구조를 검증하게 되면 이런 불상사가 줄어든다. RestDocs를 사용하지 않더라도 구조 검증을 하려면 스니펫을 만드는 것과 비슷한 노력을 들여야 한다. 이왕 검증할거면 테스트 문서도 함께 만들 수 있다는 점이 큰 장점이라 생각된다.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://docs.spring.io/spring-restdocs/docs/2.0.5.RELEASE/reference/html5/">https://docs.spring.io/spring-restdocs/docs/2.0.5.RELEASE/reference/html5/</a></li>
<li><a href="https://github.com/Dae-Hwa/restdocs-example">예제코드</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[유클리드 알고리즘(유클리드 호제법)을 이용한 최대공약수와 최소공배수]]></title>
            <link>https://velog.io/@dae-hwa/%EC%9C%A0%ED%81%B4%EB%A6%AC%EB%93%9C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9C%A0%ED%81%B4%EB%A6%AC%EB%93%9C-%ED%98%B8%EC%A0%9C%EB%B2%95%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%B5%9C%EB%8C%80%EA%B3%B5%EC%95%BD%EC%88%98%EC%99%80-%EC%B5%9C%EC%86%8C%EA%B3%B5%EB%B0%B0%EC%88%98</link>
            <guid>https://velog.io/@dae-hwa/%EC%9C%A0%ED%81%B4%EB%A6%AC%EB%93%9C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9C%A0%ED%81%B4%EB%A6%AC%EB%93%9C-%ED%98%B8%EC%A0%9C%EB%B2%95%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%B5%9C%EB%8C%80%EA%B3%B5%EC%95%BD%EC%88%98%EC%99%80-%EC%B5%9C%EC%86%8C%EA%B3%B5%EB%B0%B0%EC%88%98</guid>
            <pubDate>Thu, 16 Sep 2021 18:05:56 GMT</pubDate>
            <description><![CDATA[<h2 id="최대공약수greatest-common-divisor-gcd">최대공약수(Greatest Common Divisor; GCD)</h2>
<p>공약수는 두 개 이상의 자연수의 약수 중 공통된 수다. 최대공약수는 공약수 중 가장 큰 수를 말한다.</p>
<blockquote>
<p>두 수의 최대공약수가 1이면 서로소(coprime; relatively prime)이라 한다.</p>
</blockquote>
<p>최대공약수를 구하는 가장 쉬운 방법은 두 수의 공약수 중 가장 큰 것을 선택하는 것이다. 예를 들어, 8과 12의 최대공약수를 구한다면, 각 수의 약수는 다음과 같다.</p>
<ul>
<li>8 : 1, 2, 4, 8</li>
<li>12 : 1, 2, 3, 4, 6, 12</li>
</ul>
<p>두 수의 공약수는 1, 2, 4 이다. 이중 가장 큰 수는 4이므로 4가 최대공약수다.</p>
<p>혹은 소인수 분해(prime factorial)의 결과의 차수 중 작은 것을 선택한 뒤 곱한다. 예를 들어 8의 소인수 분해 결과인 2³과 12의 소인수 분해 결과인 2² <em>3¹에서 작은 차수를 선택하면 2²</em> 3⁰ 이 된다. 따라서 최대공약수는 4가 된다.</p>
<h2 id="유클리드-알고리즘euclidean-algorithm-유클리드-호제법">유클리드 알고리즘(Euclidean algorithm; 유클리드 호제법)</h2>
<p>위의 방법들은 간단하게 생각하여 구할 수 있지만, 코드로 구현하면 계산이 많아진다는 문제점이 있다. 따라서 유클리드 알고리즘이 많이 쓰인다. 유클리드 알고리즘은 두 가지 사실을 바탕으로 만들어졌다.</p>
<ol>
<li><p><code>b|a</code>(b가 a의 인수나 약수이고, a가 b의 배수) 이면 <code>gcd(a, b) = b</code> 이다.</p>
</li>
<li><p><code>a = bt + r</code> 을 만족하는 정수 <code>t</code>와  <code>r</code>이 있으면 <code>gcd(a, b) = gcd(b, r)</code> 이다.</p>
</li>
</ol>
<p>우선 첫째로, a가 b의 배수라는 것은 a가 b로 나누어떨어진다는 것이다. 이 경우 <code>a = bk</code>를 만족하는 정수 <code>k</code> 가 존재한다. <code>bk</code>와 <code>b</code>의 최대공약수는 b이다. 즉, <code>gcd(bk, b) = b</code> 이기 때문에 1번이 성립한다.</p>
<p>2번 명제는 <code>gcd(a, b) = gcd(bt+r, b) = gcd(b, r)</code>로 정리해볼 수 있다. <code>bt</code>는<code>b</code>로 나누어떨어질 것이기 때문에 <code>b</code>의 모든 약수로 나눌 수 있다. 따라서 <code>a</code>와 <code>b</code>의 공약수는 <code>r</code>에 의해서 결정된다. 즉, a가 b로 나누어떨어지고, 나머지가 r일 경우 <code>gcd(b, r)</code>이 되는 것이다.</p>
<p>유클리드 알고리즘은 재귀적이다. 둘 중 큰 정수를 작은 정수로 나눈 나머지와 작은 정수의 최대 공약수를 구하는 것을 반복하기 때문이다. 다시 8과 12의 최대공약수를 구해보면 다음과 같은 과정을 거친다.</p>
<pre><code class="language-text">gcd(8, 12) &amp;=&amp; gcd(8, 12 % 8) = gcd(8, 4)
gcd(8, 4) &amp;=&amp; gcd(8 % 4, 4) = gcd(4, 0)</code></pre>
<p>따라서 <code>gcd(8, 12) = 4</code> 다.</p>
<h2 id="최소공배수least-common-multiple-lcm">최소공배수(Least Common Multiple; LCM)</h2>
<p>공배수는 두 개 이상의 자연수의 공통된 배수이며, 최소공배수는 공배수 중 가장 작은 것이다. 8과 12의 공배수를 찾아보기 위해 각각의 배수를 나열해보면 다음과 같다.</p>
<ul>
<li>8 : 8, 16, 24, 32, 40, <strong>48</strong>, 56, 64, 72, 80, 88, <strong>96</strong>, 104 ...</li>
<li>12 : 12, 24, 36, <strong>48</strong>, 60, 72, 84, <strong>96</strong>, 108 ...</li>
</ul>
<p>진하게 표시된 숫자가 공배수다. 그 중 최소공배수는 48이다.</p>
<p>최소공배수는 서로 다른 주기로 일어나는 두 사건이 동시에 일어나는 주기를 찾는 것과 같은 경우에 사용된다. 예를 들어, 대통령 선거(5년 주기)와 국회의원 선거(4년 주기)를 함께 치르는 주기는 <code>lcm(5, 4) = 20</code>이기 때문에 20년마다 한 번씩 찾아온다.</p>
<p>최소공배수는 최대공약수에서 살펴봤던 지수를 이용한 방법과 유사하게 구할 수 있지만, 최대공약수를 이용할 수도 있다. 최소공배수는 아래와 같은 성질을 가지고 있다.</p>
<pre><code class="language-text">max(x, y) &lt;= lcm(x, y) &lt;= xy</code></pre>
<p>최소공배수가 <code>xy</code>가 되는 상황은 최대공약수가 1이 되는 경우다. 이를 역으로 생각해보면 <code>lcm(x, y) * gcd(x, y) = xy</code>가 성립하는데, 이는 <code>lcm(x, y) = xy / gcd(x, y)</code>와 같이 정리할 수 있다.</p>
<h2 id="구현">구현</h2>
<p>위에서 살펴봤던 대로 재귀적으로 mod 연산을 하여 <code>r</code>을 구해주며 진행하면 된다.</p>
<pre><code class="language-java">int gcd(int x, int y) {
    int a = max(x, y);
    int b = min(x, y);
    int r = a % b;

    if(r == 0) return b;

    return gcd(b, r)
}

int lcm(int x, int y) {
    return x * y / gcd(x, y);
}</code></pre>
<p>자바의 경우 <code>BigInteger</code> 클래스에 <code>gcd</code>가 구현돼있다. 다음과 같이 이용할 수 있다.</p>
<pre><code class="language-java">import java.math.BigInteger;

public class GcdUtil {
    private GcdUtil() {}

    public static int gcd(int x, int y) {
        return BigInteger.valueOf(x)
               .gcd(BigInteger.valueOf(y))
               .intValue();
    }
}</code></pre>
<hr>
<h2 id="references">References</h2>
<ul>
<li><a href="https://mathbang.net/202">수학방 - 최대공약수</a></li>
<li><a href="https://www.hanbit.co.kr/media/books/book_view.html?p_code=B5937184860">Programming Challenges: 알고리즘 트레이닝 북</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT(JSON Web Token) 알아보기]]></title>
            <link>https://velog.io/@dae-hwa/JWTJSON-Web-Token-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dae-hwa/JWTJSON-Web-Token-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Thu, 16 Sep 2021 08:01:45 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<p>JWT를 찾아보면 명확하지 않은 설명들이 간간히 있었고 대부분의 블로그에서 비슷한 방식으로 설명해 의문점이 많이 생겼다. 그런 부분들을 해소하기 위해 조사하며 정리한 글이다. 따라서 간단한 컨셉을 파악하는 것은 <a href="https://jwt.io/introduction">https://jwt.io/introduction</a> 나 블로그에서 흔히 설명하는 방식의 간단한 글을 찾아보는 게 더 도움이 될 수 있다.</p>
<h2 id="jwtjson-web-token-개요">JWT(JSON Web Token) 개요</h2>
<p>JWT는 정보를 간결하게 주고받을 수 있는 기술이다. 간결(compact)하고 URL-safe하므로 HTTP 헤더나 URI 쿼리 파라미터와 같이 공간이 제한된 환경에서도 쉽게 사용할 수 있다. 이때, 주고받는 정보는 claim이라 한다.</p>
<blockquote>
<p>Claim은 어떠한 주제에 대해 주장하는 정보(A piece of information asserted about a subject)다. name/value 페어로 구성된다. 주제에 대해 주장한다는 말이 어색해 보이는데, asserted는 단언하여 주장하는 뉘앙스다. 그리고 JWT는 두 당사자(parties)에 관한 것이다. 따라서 어떤 주제에 대해 확신을 가지고 설명을 하는 느낌이고, claim 또한 어떠한 주제에 대해 확신할 수 있는 정보를 담았다는 뉘앙스로 생각된다. 예를 들어, 어떠한 토큰은 특정 주체가 발급했다고 확신할 수 있다는 식이다. claim은 암호화되거나 서명으로 무결성이 보장되기 때문이다.</p>
</blockquote>
<p>JWT는 인터페이스 역할을 해주는 추상적인 개념이고 실제 구현은 JWS와 JWE로 나누어진다.</p>
<h3 id="jwsjson-web-signature와-jwejson-web-encryption">JWS(JSON Web Signature)와 JWE(JSON Web Encryption)</h3>
<h4 id="jws-jwe-jwt-claims-set">JWS, JWE, JWT Claims Set</h4>
<p>Cliam은 서명을 하거나 암호화하여 사용한다. 디지털 서명을 하는 방식은 JWS(JSON Web Signature)방식이고 암호화 하는 방식은 JWE(JSON Web Encryption)다. 이러한 claim의 집합은 JSON 객체로 표현되어 전달되는데, 이를 JWT Claims Set이라 한다. 모든 JSON data type을 calim의 값으로 사용할 수 있다.</p>
<p>디지털 서명(signature)의 경우 claim의 내용이 노출되지만 서명을 이용하여 원본이 맞는지 무결성을 파악할 수 있다. 반면, 암호화(encryption) 방식은 claim 자체를 암호화시켜 내용을 파악할 수 없다. 보안은 당연히 암호화 방식이 좋지만, 클라이언트가 claim의 데이터를 사용하려면 디지털 서명 방식을 사용해야한다. 따라서 대부분 JWT라고 하면 JWS를 가리킨다. 이 글에서도 JWE보다는 JWS에 조금 더 무게를 실어서 설명을 할 것이다.</p>
<p>JWS와 JWE는 모두 base64URL로 인코딩되어 전달된다. 각 방식의 구성 요소들을 part라고 부르는데 이는 <code>.</code>을 구분자로 나누어진다.</p>
<pre><code class="language-text">eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
.
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk</code></pre>
<blockquote>
<p>구분자 . 사이의 줄 바꿈은 표현을 위해 임의로 넣은 것이다. 실제로는 한 줄로 붙어서 전송된다. 이렇게 <code>eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk</code></p>
</blockquote>
<h4 id="jwt는-독립적이다">JWT는 독립적이다</h4>
<p>JWT는 독립적(Self-contained)이라고 표현하기도 한다. 쉽게 얘기하면, 페이로드 안에 사용자에 대한 정보가 모두 들어있으면 데이터베이스 조회 작업이 줄어든다. 다른 측면에서 보면, 이러한 특성 때문에 데이터가 노출되는 JWS 방식의 경우 민감한 정보는 넣지 않는 것이 바람직하다.</p>
<h2 id="구조">구조</h2>
<p>앞서 말했듯, 주로 JWS의 구조에 대해 설명할 것이다. JWE의 예시는 <a href="https://datatracker.ietf.org/doc/html/rfc7516#section-3.3">Example JWE</a>에서 확인해볼 수 있다.</p>
<p>JWS의 구조는 JOSE Header, JWS Payload, JWS Signature로 구성된다. 각 구성요소를 part라고 하고, 각 part는 위에서 봤듯이 <code>.</code>으로 구분되어 표현된다.</p>
<blockquote>
<p>registered header와 claim의 값은 대부분 3글자 약어로 되어있는데, 이는 JWT의 주요 목표가 compact한 표현이기 때문이라 한다.</p>
</blockquote>
<h3 id="josejson-object-signing-and-encryption-header">JOSE(JSON Object Signing and Encryption) Header</h3>
<blockquote>
<p>Javascript Object Signing and Encryption이라 부르기도 하는데, 이 글에서는 <a href="https://datatracker.ietf.org/doc/html/rfc7515#section-2">JWS - 용어설명</a>에서 가져와 사용한다.</p>
</blockquote>
<p>JOSE Header part는 JWT의 구성요소이기 때문에 JWS와 JWE 둘 다 사용한다. 주로 JWT Claims Set에 적용될 암호화 작업을 설명해주고 옵셔널하게 그 외의 속성이 들어간다. JOSE Header에 들어가는 값들은 JWS와 JWE 중 무엇을 사용하느냐에 따라 달라진다.</p>
<blockquote>
<p>다시 말하면, JOSE Header에 들어가 있는 값이 JWS를 위한 것이면 JWT Claims Set이 JWS Payload가 되는 것이고, JWE를 위한 것이면 JWT Claims Set이 JWE에 의해 암호화된 plaintext가 되는 것이다.</p>
</blockquote>
<p>아래에서는 주요한 속성들만 설명한다. 나머지는 <a href="https://datatracker.ietf.org/doc/html/rfc7515#section-4">JWS - JOSE Header</a>에 자세히 나와있다.</p>
<h4 id="jwt-header">JWT Header</h4>
<ul>
<li>typ : type을 뜻한다. JWT가 아닌 객체가 존재할 수도 있을 때 구분하기 위해 사용한다. 옵셔널한 값이지만, 만약 사용해야 하고 JWT임을 나타내야 한다면 <code>JWT</code>를 값으로 넣는 것이 권장된다. 현재는 대소문자를 가리지 않지만, 레거시 중에 대문자만 인식하는 것이 있을 수도 있다고 한다.</li>
<li>cty : content type을 뜻한다. 일반적인 경우에는 사용하지 않는 것이 권장되고, 중첩된 구조에서는 반드시 사용되어야 한다고 한다. 이 경우에도 <code>JWT</code> 를 값으로 사용하는 것이 권장된다.</li>
</ul>
<h4 id="jws-jwe-header">JWS, JWE Header</h4>
<ul>
<li>alg : algorithm을 뜻한다. 필수값으로 반드시 넣어야 한다. 어떤 알고리즘을 사용할지 선택하는 것인데, JWS에서 사용 가능한 값은 <a href="https://datatracker.ietf.org/doc/html/rfc7518#section-3.1">JWA - alg for JWS</a>에서 확인할 수 있다.</li>
<li>jwk : Json Web Key를 뜻한다. JWS에 디지털 서명하는데 해당하는 공개키다. 형식의 예시는 <a href="https://datatracker.ietf.org/doc/html/rfc7517#section-3">Example JWK</a>에서 확인할 수 있다.</li>
<li>kid : 키에 대한 힌트를 나타낸다. 이를 이용하여 키 변경을 명시적으로 나타낼 수 있다.</li>
</ul>
<p>이외에도 JWS와 JWE헤더는 public, private 헤더를 지원한다.</p>
<h3 id="jws-payload">JWS Payload</h3>
<p>JOSE Header에 JWS를 위한 값들이 들이었으면 JWT Claims Set이 JWS Payload가 된다. 따라서 JWS Payload는 JWT Claims Set의 규칙을 따른다.</p>
<blockquote>
<p>당연히 JWE를 위한 값들이 들어있으면 JWT Claims Set은 plaintext로 암호화된 JWE가 된다.</p>
</blockquote>
<h3 id="claim">Claim</h3>
<p>Claim은 name/value의 쌍으로 이루어져 있고, JWT Claims Set 안에 있는 name은 unique한 값이어야 한다. 그렇지 않을 경우 파싱이 거부되거나 마지막 name만 파싱된다. 이러한 동작은 구현체에 따라 다르기 때문에 확인을 해봐야 하는데, 근본적으로 하지 않는 것이 좋다. 또한 JWT가 이해할 수 없는 claim은 무시된다.</p>
<p>JWT Claim Names는 세 가지로 분류되는데, registered, public, private이다.</p>
<h4 id="registerd-claim-names">Registerd Claim Names</h4>
<ul>
<li><p>iss : Issuer, 발급 주체</p>
</li>
<li><p>sub : subject, JWT의 주제(제목). JWT의 내용은 일반적으로 sub에 대한 설명이다.</p>
<blockquote>
<p>sub를 단순히 제목으로 사용하기보다는, user ID와 같이 전체를 포괄하는 내용을 담아주는 용도로 많이 사용하는 듯하다.</p>
</blockquote>
</li>
<li><p>aud : Audience, 받는 주체</p>
</li>
<li><p>exp : Expiration, 만료시간. NumericDate value를 포함하는 숫자여야 한다.</p>
</li>
<li><p>nbf : Not Before, 토큰의 활성 시작 시간. 예를 들어 nbf가 내일이면 오늘은 해당 토큰을 사용할 수 없다.  NumericDate value를 포함하는 숫자여야 한다.</p>
</li>
<li><p>iat : Issued At, 토큰이 발급된 시간. age를 계산하는 데 사용할 수 있다.  NumericDate value를 포함하는 숫자여야 한다.</p>
</li>
<li><p>jti : JWT ID, JWT의 유니크한 ID값. 중복 값이 생성될 확률이 무시할 수 있는 정도로 낮은 방법을 택해야 한다. 토큰이 재사용되는 것을 막아주는 용도로 사용할 수 있다.</p>
</li>
</ul>
<blockquote>
<p>위에 나온 registered claim names의 자세한 내용은 <a href="https://datatracker.ietf.org/doc/html/rfc7519#section-4.1">JWT - Registered Claim Names</a>에서 확인할 수 있다.</p>
</blockquote>
<h5 id="반드시-써야하나">반드시 써야하나?</h5>
<p>필수적으로 사용해야 하는 registered claim이 있는 것은 아니지만, 이를 사용해서 보다 유용하고 상호운용성이 있도록 만들 수 있다. 또한 발급 주체의 구현이나 상황(context)에 따라 반드시 포함해야 하는 claim을 정해놨을 수도 있다.</p>
<h4 id="public-private-calims">public, private calims</h4>
<p>public은 충돌이 일어나지 않는 이름(Collision-Resistant Name)이어야 하고, private은 registered나 public이 아닌 claim을 뜻한다.</p>
<h3 id="jws-signature">JWS Signature</h3>
<p>JWS Signature는 아래와 같은 작업들을 수행하여 만들어진다.</p>
<ol>
<li><p>JOSE Header를 넣어 JSON 객체를 만든 뒤 BASE64URL 방식으로 인코딩한다.</p>
</li>
<li><p>JWS Payload로 사용할 컨텐츠를 만든 뒤 BASE64URL 방식으로 인코딩한다.</p>
</li>
<li><p>1과 2에서 만들었던 내용들을 사용해서 JWS Signature를 만든다.</p>
<p>3-1. <code>BASE64URL(JOSE Header).BASE64URL(JWS Payload).</code>가 Signature 계산의 인풋이 된다.</p>
<p>3-2. 알고리즘은 JOSE Header의 alg 파라미터의 값으로 정해진다. 따라서 위에서도 살펴봤듯, alg 파라미터의 값은 반드시 존재해야 하며 JWS Signature에서 사용할 수 있는 알고리즘이어야 한다.
3-3. 3-1의 값을 3-2의 알고리즘을 이용하여 디지털 서명으로 바꿔준 뒤, BASE64URL 방식으로 인코딩한다.</p>
</li>
</ol>
<blockquote>
<p>따라서, JWS를 사용하는 JWT의 결과는 <code>BASE64URL(JOSE Header).BASE64URL(JWS Payload).BASE64URL(JWS Signature)</code>와 같은 형태가 된다.</p>
</blockquote>
<p>여기서 3-2의 알고리즘에 따라 Message Signature 방식과 MAC 방식으로 구분된다. 간단히 설명하면, MAC는 대칭키 방식으로 암호화하는 것이고, Message Signature 방식은 비대칭 키 방식으로 암호화하는 것이다. MAC는 대칭 키이기 때문에 클라이언트에서 검증 작업을 하기 어렵다. 따라서 서버에서 모든 것을 처리할 수 있거나, 간단한 작업에 사용되며 로그인 인증 토큰으로 많이 사용된다.</p>
<blockquote>
<p><a href="https://connect2id.com/products/nimbus-jose-jwt/algorithm-selection-guide">How to select a JOSE / JWT cryptographic algorithm for your application</a>에 각 방식의 비교와 도입 전 고려해야할 부분들이 정리돼있다. JWE도 함께 비교한다.</p>
</blockquote>
<p>어찌 됐건, JWS는 암호화된 Signature 파트를 비밀키로 검증한다. JOSE Header와 JWS Payload를 암호화하므로 해당 부분들에 변조가 있었는지 파악할 수 있다. 즉 데이터는 노출되지만, 데이터가 바뀔 위험이 없다. 따라서 로그인 인증 토큰으로 사용할 수 있는 것이다.</p>
<h2 id="사용-예시">사용 예시</h2>
<h3 id="인증authenticaition">인증(Authenticaition)</h3>
<ul>
<li><p>사용자가 자신의 인증 정보(credentials)를 이용하여 성공적으로 로그인하면 ID토큰을 반환된다.</p>
<blockquote>
<p>OAuth2를 기반으로 하는 인증 방식인 <a href="https://openid.net/specs/openid-connect-core-1_0.html#IDToken">OIDC(OpenID Connect)</a>스펙은 항상 JWT를 ID 토큰으로 사용하도록 되어 있다.</p>
</blockquote>
</li>
</ul>
<h3 id="권한-확인authorization">권한 확인(Authorization)</h3>
<ul>
<li><p>가장 많이 쓰는 시나리오. 사용자가 로그인하면 다음 요청부터는 JWT를 포함하여 서버에 요청을 보낸다. 서버는 해당 토큰이 유효한지 검증하고 접근할 수 없는 곳을 요청하면 거부할 수 있다.</p>
</li>
<li><p>오버헤드가 적고 여러 도메인에서 쉽게 사용할 수 있기 때문에 SSO(Single Sign On)에 JWT가 널리 사용된다.</p>
<blockquote>
<p>다른 방법으로 SAML과 SWT가 있다. 아래에서 간단히 비교한다.</p>
</blockquote>
</li>
</ul>
<h3 id="정보-교환">정보 교환</h3>
<ul>
<li><p>이 경우 비대칭 키 방식을 사용하면 더 좋을 것이다. 그러면 받는 사람도 안전하게 서명을 검증해 유효한지 확인할 수 있다. 즉, 받는 사람이 JWT에 있는 보낸 사람과 데이터가 변하지 않았다는 것을 믿을 수 있다.</p>
<blockquote>
<p>대칭 키 방식을 사용하면 비밀 키를 주고받아야 하기 때문에 보안에 좋지 않을 수 있다.</p>
</blockquote>
</li>
</ul>
<h2 id="장점과-단점">장점과 단점</h2>
<h3 id="장점">장점</h3>
<ul>
<li><p>사용자 편의성이 증가한다.</p>
<blockquote>
<p>JWT만의 장점은 아니다. 세션과 쿠키를 사용해도 같은 효과를 낼 수 는 있다.</p>
</blockquote>
</li>
<li><p>서버 비용이 줄어든다.</p>
</li>
<li><p>서버에 고정되는 것이 아니기 때문에 유연하다. 따라서 scale in/out에 대응하기 쉽고, SSO 구현에도 좋다.</p>
</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li><p>아무래도 보안적 측면이 좋다고 하기 힘들다. 토큰이 탈취되면 해당 토큰을 무력화하기 힘들 수 있다. 또한 대부분 JWS 방식이기 때문에 내용이 다 노출된다.</p>
</li>
<li><p>추가적인 보안책을 마련하려면 결국 서버의 도움을 받아야 한다.</p>
</li>
<li><p>세션처럼 사용한다면 클라이언트에서 업데이트하기 전까지 서버의 변동사항이 반영되지 않는다. 또한 토큰이 커지면 주고받아야 할 데이터의 양이 많아진다.</p>
</li>
</ul>
<h2 id="stateless하게-데이터를-주고받는-다른-방법들">Stateless하게 데이터를 주고받는 다른 방법들</h2>
<p>세션과 쿠키를 제외한 토큰 방식 중 JWT이외에도 SWT(Simple Web Token)과 SAML(Security Assertion Markup Language tokens)있다.</p>
<blockquote>
<p>SWT는 찾아도 잘 안 나온다. 궁금하신 분들은 <a href="https://docs.microsoft.com/en-us/previous-versions/azure/azure-services/hh781551(v=azure.100)?redirectedfrom=MSDN#swt-example">SWT Example</a> 참고</p>
</blockquote>
<h3 id="swt">SWT</h3>
<p>SWT는 Java의 properties와 유사한 형태다. key value형태로 사용하지만 JSON처럼 다양한 타입을 사용하기 힘들어 자유도가 떨어진다. 또한, SWT는 HS256만 사용할 수 있어 보다 강력한 보안이 요구될 때는 사용할 수 없다. 내 생각에 단순한 토큰으로는 문제가 없어 보인다. 그럼에도 잘 사용하지 않는 이유는 JSON이 더 다루기 쉽기 때문인 것으로 보인다.</p>
<h3 id="saml">SAML</h3>
<p>반면, SAML은 이름에서 유추할 수 있듯이 XML기반이다. 그래서 기본적으로 써줘야 할 게 많다. 즉 인코딩 시 크기가 매우 커서 불리하다. XML이 익숙한 사람에게는 가독성이 더 좋아 보일 수도 있지만, 역시나 JSON이 더 다루기 쉽다고 느끼는 사람이 훨씬 많은 것으로 생각된다. Ajax도 XML을 고려해서 만들었지만, 결국 대부분 JSON을 이용해서 사용하게 됐다.</p>
<h2 id="마치며">마치며</h2>
<p>명확하지 않은 설명이 싫어서 글을 쓰게 됐는데 나도 많이 건너뛰게 됐다. JWE, JWK, JWA도 있고, JWT만해도 중첩 구조나 protected, unprotected와 같이 다루지 않은 내용도 많다. 그럼에도 생각보다 글이 엄청 길어졌다.</p>
<p>하지만 이런 부분까지 몰라도 라이브러리의 도움을 받으면 사용에 큰 어려움이 없을 수 있다. 그래서 간단한 컨셉만 나타낸 글이 많은 것 같고, 그만큼 사용하기 쉽게 잘 만든 것 같다. 그래서 인기도 많은가보다.</p>
<hr>
<h2 id="references">References</h2>
<ul>
<li><a href="https://datatracker.ietf.org/doc/html/rfc7519">https://datatracker.ietf.org/doc/html/rfc7519</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc7515">https://datatracker.ietf.org/doc/html/rfc7515</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc7516">https://datatracker.ietf.org/doc/html/rfc7516</a></li>
<li><a href="https://medium.facilelogin.com/jwt-jws-and-jwe-for-not-so-dummies-b63310d201a3">https://medium.facilelogin.com/jwt-jws-and-jwe-for-not-so-dummies-b63310d201a3</a></li>
<li><a href="https://meetup.toast.com/posts/239">https://meetup.toast.com/posts/239</a></li>
<li><a href="https://jwt.io/introduction">https://jwt.io/introduction</a></li>
<li><a href="https://auth0.com/learn/json-web-tokens/">https://auth0.com/learn/json-web-tokens/</a></li>
<li><a href="https://connect2id.com/products/nimbus-jose-jwt/algorithm-selection-guide">https://connect2id.com/products/nimbus-jose-jwt/algorithm-selection-guide</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>