<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>D E Y </title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 13 Nov 2024 13:48:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. D E Y . All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kwon_yongil_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Distributed Locks with Redis (Redis docs)]]></title>
            <link>https://velog.io/@kwon_yongil_/Distributed-Locks-with-Redis-Redis-docs</link>
            <guid>https://velog.io/@kwon_yongil_/Distributed-Locks-with-Redis-Redis-docs</guid>
            <pubDate>Wed, 13 Nov 2024 13:48:59 GMT</pubDate>
            <description><![CDATA[<p><a href="https://redis.io/docs/latest/develop/use/patterns/distributed-locks/">https://redis.io/docs/latest/develop/use/patterns/distributed-locks/</a></p>
<h2 id="distributed-locks">Distributed Locks</h2>
<ul>
<li>많은 환경에서 다양한 프로세스에서 공유하는 자원을 상호 독점적으로 사용할 수 있게 해준다.</li>
</ul>
<h2 id="safety-and-liveness-guarantess">Safety and Liveness Guarantess</h2>
<ul>
<li>효과적인 방법으로 분산락을 사용하기 위해서는 아래 조건들을 만족해야 한다.<ul>
<li>Safety property<ul>
<li>어떤 순간에 하나의 클라이언트만 락을 가져야 한다.</li>
</ul>
</li>
<li>Liveness property<ul>
<li>교착상태가 없다.</li>
</ul>
</li>
<li>Liveness proprety<ul>
<li>대다수의 레디스 노드가 UP 상태라면, 클라이언트는 락을 획득하고 해제할 수 있다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="why-failover-based-implementations-are-not-enough">Why Failover-based Implementations Are Not Enough</h2>
<p>리소스에 락을 하는 가장 간단한 방법은 인스턴스에 키를 만드는 것이다. 키는 대개 제한된 시간동안에 살아있고, 레디스 만료 기능을 통해서 해제된다. 클라이언트가 리소스를 해제하면, 키는 삭제된다. 그렇지만 여기에는 문제가 있다. 마스터 노드가 죽는다면, 단일 실패 지점이 될 수 있다. 그렇다고 레플리카를 추가할 순 없다. 레디스 복제는 비동기적으로 이뤄지므로 <code>Safety property</code>를 위배할 수 있다.</p>
<p>위배하는 상황은 아래와 같다.</p>
<ol>
<li>클라이언트 A가 마스터에서 락을 획득한다.</li>
<li>키에 대한 쓰기를 레플리카로 보내기전에 마스터는 충돌한다.</li>
<li>복제본이 마스터로 승격된다.</li>
<li>클라이언트 B는 클라이언트 A가 락을 가진 리소스에 대해서 락을 가질 수 있게 된다. 이것은 <code>Safety property</code>를 위배한다.</li>
</ol>
<p>만약 락을 동시에 갖는 것이 문제가 되지 않는다면 이 모델을 사용할 수 있다.</p>
<h2 id="implementations">Implementations</h2>
<ul>
<li>Java중에서 Redisson(Java)</li>
</ul>
<h2 id="correct-implementation-with-a-single-instance">Correct Implementation with a Single Instance</h2>
<pre><code class="language-redis">    SET resource_name my_random_value NX PX 30000</code></pre>
<ul>
<li>키가 이미 존재하지 않는 경우에만 키를 설정(NX)</li>
<li>만료 시간은 30000 밀리초 (PX)</li>
<li>값(my_random_value)은 모두 클라이언트와 모든 잠금 요청에서 고유해야 한다.<pre><code>if redis.call(&quot;get&quot;,KEYS[1]) == ARGV[1] then
  return redis.call(&quot;del&quot;,KEYS[1])
else
  return 0
end</code></pre></li>
<li>random value는 안전한 방식으로 락을 해제하기 위해서 사용된다. 다른 클라이언트가 락을 삭제하는 것을 방지할 수 있다. 락의 만료시간보다 오래 사용한 클라이언트가 다른 클라이언트에 의해 사용되고 있는 락을 지워버릴 수 있기 때문이다. 그래서, 랜덤 값을 통해서 값이 같을때만 삭제하게 된다.</li>
</ul>
<h2 id="the-redlock-algorithm">The Redlock Algorithm</h2>
<p>N개의 레디스 마스터가 있다고 가정한다. 노드들은 모두 독립적이고, 레플리카를 사용하지 않았다. 위에서 싱글 인스턴스에 대해서 안전하게 락을 획득하고 해제하는 방법을 알고 있다. 5개 레디스 마스터를 독립적인 환경에서 실패할 수 있도록 각자 다른 가상 환경에서 수행시킨다.</p>
<p>락을 얻기 위해서 클라이언트는 아래 작업들은 수행한다.</p>
<ol>
<li>현재 시간을 밀리초로 가져온다.</li>
<li>같은 키와 랜덤 값으로 모든 인스턴스에서 순차적으로 락을 획득한다. 클라이언트에서는 락 해제시간보다 짧은 시간의 타임아웃을 사용하도록 한다. 이건 클라이언트에게 레디스 노드가 다운됐을 때 오랜 시간 기다리는 것을 막아준다.</li>
<li>클라이언트는 1번에서 가진 현재 시간을 기준으로 얼마나 많은 시간이 흘렀는지 계산한다. 만약에 클라이언트가 적어도 3개 이상에서 락을 획득할 수 있고, 획득한 전체 시간이 경과 시간보다 작다면 락을 획득한 것으로 여겨질 수 있다.</li>
<li>락이 획득됐다면, 처음 유효 시간에서 락을 얻기 위해 경과된 시간을 차감하면 된다.</li>
<li>만약에 클라이언트가 락을 획득하지 못했다면, 전체 인스턴스에 대해서 잠금을 해제한다.</li>
</ol>
<blockquote>
<p>N개의 레디스를 통해서 고가용성 및 일관성 확보</p>
</blockquote>
<h2 id="is-the-alogirhtm-asynchronous">Is the Alogirhtm Asynchronous?</h2>
<p>N개의 노드가 있으면, 노드의 환경에 따라서 만료 시간을 통한 시간 동기화 문제가 발생할 수 있다. 노드마다 락을 얻기 위해 경과된 시간이 다르면, 해제 시간이 만료될 때 문제가 발생할 수 있다. </p>
<blockquote>
<p>과반수를 통한 락 획득을 통해서 안정성을 유지할 수 있도록 한다.</p>
</blockquote>
<h2 id="retry-on-failure">Retry on failure</h2>
<p>클라이언트가 락을 획득하지 못하면, 랜덤한 딜레이 후에 재시도를 하게 된다. 대다수가 락을 획득하게 되면, 더 이상 나머지에 대해서는 재시도 하지 않아도 된다. 또한 잠금을 획득하지 못한 경우, 일부 획득한 잠금을 최대한 빨리 해제하는 것이 중요하다. 이렇게 빨리 잠금을 해제하면 키 만료를 기다릴 필요가 없어진다.</p>
<h2 id="releasing-the-lock">Releasing the Lock</h2>
<p>특정 인스턴스에 락을 획득하거나 획득하지 못했더라도 락을 해제하는 것은 간단하다.</p>
<h2 id="safety-arguments">Safety Arguments</h2>
<p>여러 개의 인스턴스에 락을 획득하려고 하면, 키는 다양한 시간대에 셋팅 될 수 있다. 만약에 T1을 가장 처음 락을 획득한 시간, T2를 가장 나중에 락을 획득한 시간으로 하면 아래의 식이 만족된다.</p>
<pre><code>MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT</code></pre><p>최소 유효 기간동안에는 동시에 락을 잡을 수 없음을 의미한다.</p>
<p>이미 <code>N/2+1</code> 노드가 락되어 있는 동안에, 다른 클라이언트가 <code>N/2+1</code>을 획득할 수 없다.</p>
<p>최대 만료 시간(<code>MAX_VALIDITY</code>)보다 크거나 가깝게 락을 획득한 클라이언트는 락은 무효화되고 인스턴스들에서 락을 해제한다. 그러므로, 만료 시간내에 락을 획득한 경우에만 동시에 락을 잡지 못하는지 고려하면 된다. 이 상황에 대해서는 이미 <code>MIN_VALIDITY</code>와 <code>N/2+1</code>로 고려되어 있다.</p>
<h2 id="liveness-arguments">Liveness Arguments</h2>
<ol>
<li>락의 자동 해제(키 만료 이후): 키는 다시 락될 수 있다.</li>
<li>락이 획득되지 못했을 때 락을 해제하거나 락이 획득되고 작업이 종료됐을 때 클라이언트들은 협력하므로, 우리는 락을 다시 얻기 위해서 키의 만료까지 기다릴 필요가 없다.</li>
<li>클라이언트가 락을 다시 시도할 때, 다수의 락을 획득하는 데 필요한 시간보다 충분히 긴 시간을 기다렸다가 시도하면, 자원 충돌 시 split brain 상황이 발생할 가능성을 낮출 수 있다.</li>
</ol>
<p>네트워크가 분할되면 TTL 시간만큼의 가용성 저하가 발생하며, 만약 네트워크 분할이 지속적으로 발생하면 이 가용성 저하는 무한히 계속될 수 있다. 이런 상황은 클라이언트가 락을 획득한 후, 락을 해제하기 전에 네트워크가 분할되어 해당 인스턴스와 연결이 끊겼을 때 발생한다.</p>
<h2 id="performance-crash-recovery-and-fsync">Performance, Crash Recovery and fsync</h2>
<p>TBD</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes Deployment]]></title>
            <link>https://velog.io/@kwon_yongil_/Kubernetes-Deployment</link>
            <guid>https://velog.io/@kwon_yongil_/Kubernetes-Deployment</guid>
            <pubDate>Sat, 09 Nov 2024 11:40:08 GMT</pubDate>
            <description><![CDATA[<h2 id="디플로이먼트">디플로이먼트</h2>
<ul>
<li>디플로이먼트는 파드와 레플리카셋에 대한 선언적 업데이트를 제공</li>
<li>디플로이먼트는 상태를 갖고 있고, 디플로이먼트 컨트롤러는 현재 상태에서 의도하는 상태로 비율을 조정하며 변경할 수 있다.</li>
<li>새 레플리카셋을 생성하는 디플로이먼트를 정의하거나 기존 디플로이먼트를 제거하고, 모든 리소스를 새 디플로이먼트에 적용할 수 있다.</li>
<li>3개의 Nginx 파드를 불러오기 위한 레플리카셋을 생성<pre><code class="language-yml">apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
  app: nginx
spec:
replicas: 3
selector:
  matchLabels:
    app: nginx
template:
  metadata:
    labels:
      app: nginx
  spec:
    containers:
    - name: nginx
      image: nginx:1.14.2
      ports:
      - containerPort: 80</code></pre>
</li>
</ul>
<h2 id="디플로이먼트-업데이트">디플로이먼트 업데이트</h2>
<ul>
<li>디플로이먼트의 파드 템플릿이 변경된 경우에만 디플로이먼트의 롤아웃이 트리거된다. 예를 들면 템플릿의 레이블이나 컨테이너 이미지가 업데이트된 경우이다. 디플로이먼트의 스케일링과 같은 다른 업데이트는 롤아웃을 트리거하지 말아야 한다.</li>
<li>최소 가용 파드 수 보장 (Max Unavailable)<ul>
<li>업데이트가 진행되는 동안, 의도한 파드 수의 75% 이상이 항상 동작하도록 보장합니다. 즉, 의도된 파드 수의 최대 25%까지만 중단될 수 있도록 합니다.</li>
<li>예: 의도한 파드 수가 4개라면, 업데이트 동안 최대 1개의 파드만 중단될 수 있습니다. 나머지 3개(4개의 75%)는 항상 가용 상태를 유지합니다.</li>
</ul>
</li>
<li>최대 추가 파드 수 제한 (Max Surge)
디플로이먼트는 의도한 파드 수를 초과하는 추가 파드의 수를 제한합니다. 기본 설정에서는 최대 의도한 파드 수의 125%까지 추가 파드가 생성될 수 있습니다.<ul>
<li>예: 의도한 파드 수가 4개라면, 최대 1개의 추가 파드(4개의 25%)가 생성될 수 있어 총 5개까지 생성될 수 있습니다.
즉, 디플로이먼트는 의도한 파드 수에 비해 75% 이상의 가용성을 유지하면서도, 필요 시 최대 25%의 추가 파드를 생성하여 안전하게 업데이트가 완료되도록 합니다</li>
</ul>
</li>
<li>의도한 파드 수가 4개일 때는, 최소 3개에서 최대 5개의 파드를 가질 수 있다.</li>
</ul>
<h2 id="롤오버">롤오버</h2>
<ul>
<li>디플로이먼트 컨트롤러는 각 시간마다 새로운 디플로이먼트에서 레플리카셋이 의도한 파드를 생성하고 띄우는 것을 주시한다.</li>
</ul>
<h2 id="스케일링">스케일링</h2>
<ul>
<li>레플리카셋에 의도한 파드수를 조정하는 작업</li>
</ul>
<h2 id="상태">상태</h2>
<h4 id="진행중">진행중</h4>
<ul>
<li>디플로이먼트로 새 레플리카셋을 생성.</li>
<li>디플로이먼트로 새로운 레플리카셋을 스케일 업.</li>
<li>디플로이먼트로 기존 레플리카셋을 스케일 다운.</li>
<li>새 파드가 준비되거나 이용할 수 있음(최소 준비 시간(초) 동안 준비됨).</li>
</ul>
<h4 id="완료">완료</h4>
<ul>
<li>디플로이먼트과 관련된 모든 레플리카가 지정된 최신 버전으로 업데이트 되었을 때. 즉, 요청한 모든 업데이트가 완료되었을 때.</li>
<li>디플로이먼트와 관련한 모든 레플리카를 사용할 수 있을 때.</li>
<li>디플로이먼트에 대해 이전 복제본이 실행되고 있지 않을 때.</li>
</ul>
<h4 id="실패">실패</h4>
<ul>
<li>할당량 부족</li>
<li>준비성 프로브(readiness probe)의 실패</li>
<li>이미지 풀 에러</li>
<li>권한 부족</li>
<li>범위 제한</li>
<li>애플리케이션 런타임의 잘못된 구성</li>
</ul>
<h2 id="롤링-업데이트">롤링 업데이트</h2>
<ul>
<li><p>디플로이먼트는 <code>.spec.strategy.type==RollingUpdate</code>이면 파드를 롤링 업데이트 방식으로 업데이트 한다. <code>maxUnavailable</code> 와 <code>maxSurge</code> 를 명시해서 롤링 업데이트 프로세스를 제어할 수 있다.</p>
</li>
<li><p>최대 불가 (Max Unavailable)</p>
<ul>
<li>목적: 업데이트 중에 중단될 수 있는 파드의 최대 수를 정해, 의도한 파드의 최소 수를 유지합니다.</li>
<li>설정 예시: 이 값을 30%로 설정하면, 의도한 파드의 70% 이상이 항상 가동 상태가 되도록 업데이트를 진행합니다. 즉, 의도한 파드가 10개라면, 최대 3개까지 중단될 수 있지만, 최소 7개는 항상 동작합니다.</li>
</ul>
</li>
<li><p>최대 서지 (Max Surge)</p>
<ul>
<li>목적: 업데이트 중에 생성할 수 있는 파드의 최대 수를 설정해, 의도한 파드보다 약간 많은 수의 파드가 일시적으로 실행될 수 있게 합니다.</li>
<li>설정 예시: 이 값을 30%로 설정하면 의도한 파드 수의 최대 130%까지 일시적으로 파드를 운영할 수 있습니다. 의도한 파드 수가 10개라면, 최대 13개까지 파드가 생성되어 서비스의 안정성을 높입니다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes Ingress]]></title>
            <link>https://velog.io/@kwon_yongil_/Kubernetes-Ingress</link>
            <guid>https://velog.io/@kwon_yongil_/Kubernetes-Ingress</guid>
            <pubDate>Sat, 09 Nov 2024 10:58:53 GMT</pubDate>
            <description><![CDATA[<p><a href="https://kubernetes.io/ko/docs/concepts/services-networking/ingress/">https://kubernetes.io/ko/docs/concepts/services-networking/ingress/</a></p>
<h2 id="ingress">Ingress</h2>
<ul>
<li>클러스터 내의 서비스에 대한 외부 접근을 관리하는 API 오브젝트이며, 일반적으로 HTTP를 관리함</li>
<li>인그레스는 부하 분산, SSL 종료, 명칭 기반의 가상 호스팅을 제공할 수 있음</li>
<li>클러스터 외부에서 클러스터 내부 서비스로 HTTP와 HTTPS 경로를 노출한다. 트래픽 라우팅은 인그레스 리소스에 정의된 규칙에 의해 컨트롤 된다.</li>
<li>인그레스 컨트롤러가 있어야 인그레스를 충족할 수 있다. 인그레스 리소스만 생성한다면 효과가 없다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kwon_yongil_/post/fc83a0e3-a912-4eb3-9746-45c13fe27934/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes Service]]></title>
            <link>https://velog.io/@kwon_yongil_/Kubernetes-Service</link>
            <guid>https://velog.io/@kwon_yongil_/Kubernetes-Service</guid>
            <pubDate>Sat, 09 Nov 2024 03:45:27 GMT</pubDate>
            <description><![CDATA[<h2 id="서비스">서비스</h2>
<ul>
<li>외부와 접하는 단일 엔드포인트 뒤에 있는 클러스터에서 실행되는 애플리케이션을 노출시키는 역할</li>
<li>셀렉터를 이용하여 쿠버네티스 파드에 대한 접근을 추상화<pre><code>apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
  app.kubernetes.io/name: MyApp
ports:
  - protocol: TCP
    port: 80
    targetPort: 9376</code></pre></li>
<li>이 명세는 <code>my-service</code>라는 새로운 서비스 오브젝트를 생성하고, app.kubernetes.io/name=MyApp 레이블을 가진 파드의 TCP 9376 포트를 대상으로 한다.</li>
<li>쿠버네티스는 이 서비스에 서비스 프록시가 사용하는 IP 주소를 할당한다.</li>
<li>서비스 셀렉터의 컨트롤러는 셀렉터와 일치하는 파드를 지속적으로 검색하고, <code>my-service</code>라는 엔드포인트 오브젝트에 대한 모든 업데이트를 POST한다.</li>
<li>외부 시스템이나 마이그레이션 도중에는 서비스 셀렉터없이 서비스를 생성할 수 있다.</li>
</ul>
<h2 id="엔드포인트">엔드포인트</h2>
<ul>
<li>네트워크 엔드포인트의 목록을 정의</li>
<li>일반적으로 트래픽이 어떤 파드에 보내질 수 있는지를 정의하기 위해 서비스가 참조</li>
<li>엔드포인트슬라이스<ul>
<li>특정 서비스의 하위 네트워크 엔드포인트 부분집합을 나타내는 오브젝트</li>
</ul>
</li>
</ul>
<h2 id="서비스-디스커버리">서비스 디스커버리</h2>
<ul>
<li>쿠버네티스는 서비스를 찾는 두 가지 기본 모드를 지원한다.<ul>
<li>환경 변수<ul>
<li>파드가 노드에서 실행될 때, kubelet은 각 활성화된 서비스에 대한 환경 변수 세트를 추가한다.</li>
<li>서비스 네임을 프리픽스로 하고, PORT, HOST, ADDR 등의 환경 변수를 추가한다.</li>
</ul>
</li>
<li>DNS<ul>
<li>서비스 이름과 네임 스페이스 조합으로 DNS 레코드를 만든다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="서비스-퍼블리싱">서비스 퍼블리싱</h2>
<ul>
<li>서비스를 클러스터 밖에 위치한 외부 IP 주소에 노출하기 위함</li>
</ul>
<h4 id="clusterip">ClusterIP</h4>
<ul>
<li>서비스를 클러스터-내부 IP에 노출시킨다. 이 값을 선택하면 클러스터 내에서만 서비스에 도달할 수 있다. 이것은 서비스의 type을 명시적으로 지정하지 않았을 때의 기본값이다.</li>
</ul>
<h4 id="nodeport">NodePort</h4>
<ul>
<li>고정 포트로 각 노드의 IP에 서비스를 노출시킨다. 노드 포트를 사용할 수 있도록 하기 위해, 쿠버네티스는 <code>ClusterIP</code>인 서비스를 요청했을 때와 마찬가지로 클러스터 IP 주소를 구성한다.</li>
</ul>
<h4 id="loadbalancer">LoadBalancer</h4>
<ul>
<li>클라우드 공급자의 로드 밸런서를 사용하여 서비스를 외부에 노출시킨다.</li>
</ul>
<h4 id="externalname">ExternalName</h4>
<ul>
<li>값과 함께 CNAME 레코드를 리턴하여, 서비스를 externalName 필드의 내용에 매핑한다. 어떠한 종류의 프록시도 설정되지 않는다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MongoDB Sharding (8.0)]]></title>
            <link>https://velog.io/@kwon_yongil_/MongoDB-Sharding8.0</link>
            <guid>https://velog.io/@kwon_yongil_/MongoDB-Sharding8.0</guid>
            <pubDate>Sat, 09 Nov 2024 02:36:27 GMT</pubDate>
            <description><![CDATA[<h2 id="sharding">Sharding</h2>
<ul>
<li>데이터를 여러 머신에 분산하는 방법</li>
<li>대규모 데이터 세트와 높은 처리량 작업의 배포를 지원</li>
<li>단일 지점 서버의 용량 한계의 문제점을 극복하기 위한 방법<ul>
<li>쿼리 속도가 높으면 서버의 CPU 용량 고갈 문제</li>
<li>시스템의 RAM보다 작업 세트 크기가 크면 디스크 드라이브의 I/O 용량에 부담</li>
</ul>
</li>
<li>단일 지점에 수직 확장을 하는 것이 아닌, 수평적 확장을 통해서 로드를 분산하고 서버를 추가하는 작업</li>
<li>각 머신이 전체 워크로드의 일부를 분산 처리하면서, 단일 고속 대용량 서버보다 더 나은 효율성을 제공하는 방법</li>
<li>컬렉션 수준에서 데이터를 샤딩하여 클러스터의 샤드 전체에 컬렉션 데이터를 분산</li>
</ul>
<h2 id="shard-key">Shard Key</h2>
<ul>
<li>샤드 키를 사용해서 샤드 전반에서 컬렉션 문서를 분산</li>
<li>샤드 키는 문서의 필드 하나 또는 여러 필드로 구성</li>
<li>샤드 키 선택<ul>
<li>Cardiniality<ul>
<li>밸런서가 생성할 수 있는 최대 청크 수를 결정</li>
<li>카디널리티가 높은 샤드 키를 선택해서 클러스터의 수평 규모 조정</li>
<li>데이터 모델에서 카디널리티가 낮은 키에 샤딩이 필요한 경우 샤드 키의 인덱싱된 필드 조합을 사용하여 카디널리티를 높이는 것이 중요</li>
</ul>
</li>
<li>Frequency<ul>
<li>지정된 샤드 키 값이 데이터에서 발생하는 빈도</li>
<li>특정 샤드에 몰리는 경우 병목 현상이 발생</li>
<li>특정 샤드에 골고루 데이터가 발생하는 것이 중요</li>
</ul>
</li>
</ul>
</li>
<li>단조롭게 변경되는 샤드 키 (증가 OR 감소)<ul>
<li>해시 샤딩을 적용해서 균일한 데이터 분배를 보장하도록 변경</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Transaction Isolation Levels (MySQL 8.4)]]></title>
            <link>https://velog.io/@kwon_yongil_/Transaction-Isolation-Levels-MySQL-8.4</link>
            <guid>https://velog.io/@kwon_yongil_/Transaction-Isolation-Levels-MySQL-8.4</guid>
            <pubDate>Fri, 08 Nov 2024 23:43:17 GMT</pubDate>
            <description><![CDATA[<p><a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html">https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html</a></p>
<h2 id="transactions">Transactions</h2>
<ul>
<li>ACID에서 I를 의미한다.(Isolation)</li>
<li>여러 개의 트랜잭션이 동시에 발생하기 위해서 고려되는 성능과 신뢰성, 일관성, 결과 재현성은 트레이드 오프 관계이다. </li>
<li>InnoDB는 기본적으로 REPEATABLE READ로 설정된다.</li>
<li><code>SET TRANSACTION</code>을 통해서, Statement의 격리 수준을 설정할 수 있다.</li>
</ul>
<h2 id="repeatable-read">REPEATABLE READ</h2>
<ul>
<li>InnoDB의 기본적인 모드</li>
<li>동일한 트랜잭션내에서는 첫번째 읽기에서 획득한 스냅샷을 사용한다.</li>
<li>기본적인 SELECT문에 대해서 비잠금 읽기(Nonlocking reads)가 적용된다.</li>
<li>잠금 읽기(<code>SELECT WITH FOR UPDATE OR FOR SHARE</code>)는 인덱스의 종류에 따라서 동작이 달라질 수 있다.<ul>
<li>Unique Index는 해당 레코드만 락한다. 다른 레코드들에 대해서 처리할 수 있도록 하여 성능을 보장할 수 있다.</li>
<li>인덱스의 특징에 따라서, Range나 Next-Key에 대해서도 락할 수 있다.</li>
<li><code>Range, Next-Key 잠금 읽기를 통해서, Phantom Read가 발생하지 않는 것처럼 느껴질 수도 있어보인다.</code></li>
</ul>
</li>
<li>잠금 읽기(<code>locking reads</code>, <code>SELECT with FOR UPDATE or FOR SHARE</code>)와 비잠금 읽기를 사용하는 것은 권장되지 않는다, 비잠금 읽기는 스냅샷이지만, 잠금 읽기는 최신의 스냅샷을 사용하므로 두 개의 다른 테이블을 바라보고 있다. 또한 잠금 읽기를 사용하는 것은 대부분 <code>SERIALIZABLE</code>인 격리성을 고려해야 한다.</li>
<li>트랜잭션내 전체 레코드에 대해서 잠금 읽기하는 상황</li>
</ul>
<pre><code class="language-sql">// Session A
x-lock(1,2); retain x-lock
x-lock(2,3); update(2,3) to (2,5); retain x-lock
x-lock(3,2); retain x-lock
x-lock(4,3); update(4,3) to (4,5); retain x-lock
x-lock(5,2); retain x-lock</code></pre>
<pre><code class="language-sql">// Session B
x-lock(1,2); block and wait for first UPDATE to commit or roll back</code></pre>
<h2 id="read-committed">READ COMMITTED</h2>
<ul>
<li>가장 최신의 스냅샷(fresh snapshot)을 획득한다.</li>
<li>잠금 읽기에 대해서는 오직 해당 레코드만 가능하고, 갭 락킹이 불가능하므로 팬텀 로우 문제(<code>Phantom Rows</code>)가 발생할 수 있다.<ul>
<li><code>Phantom Rows</code> : 트랜잭션 내에서 특정 쿼리를 여러번 했을 때, 시점에 따라 다른 결과가 나타나는 현상<pre><code class="language-sql">SELECT * FROM child WHERE id &gt; 100 FOR UPDATE;</code></pre>
</li>
</ul>
</li>
<li>오직 해당 레코드만 잠금 읽기하는 상황</li>
</ul>
<pre><code class="language-sql">x-lock(1,2); unlock(1,2)
x-lock(2,3); update(2,3) to (2,5); retain x-lock
x-lock(3,2); unlock(3,2)
x-lock(4,3); update(4,3) to (4,5); retain x-lock
x-lock(5,2); unlock(5,2)
</code></pre>
<pre><code class="language-sql">x-lock(1,2); update(1,2) to (1,4); retain x-lock
x-lock(2,3); unlock(2,3)
x-lock(3,2); update(3,2) to (3,4); retain x-lock
x-lock(4,3); unlock(4,3)
x-lock(5,2); update(5,2) to (5,4); retain x-lock</code></pre>
<h2 id="read-uncommitted">READ UNCOMMITTED</h2>
<ul>
<li>Dirty Read가 발생할 수 있음<ul>
<li>다른 트랜잭션에 의해 커밋되지 않은 데이터들이 노출되는 상황</li>
</ul>
</li>
<li>ACID의 원칙을 준수하지 않으므로, 매우 위험하다.<blockquote>
<p>커밋까지 시간이 오래 걸리는 경우, 데이터들을 추적하는 용도로 사용할 수 있어보인다. </p>
</blockquote>
</li>
</ul>
<h2 id="serializable">SERIALIZABLE</h2>
<ul>
<li>REPEATABLE_READ보다 더 엄격한 수준의 격리성</li>
<li>AUTOCOMMIT : ON<ul>
<li>SELECT 1개가 자신의 트랜잭션을 가지고 있으므로, 트랜잭션내에서 Phantom Read가 발생하지 않는다.</li>
</ul>
</li>
<li>AUTOCOMMIT : OFF<pre><code>- `SELECT ... FOR SHARE`가 적용된다.</code></pre><ul>
<li>잠금 읽기가 발생한다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Consistent Nonlocking Reads (MySQL 8.4)]]></title>
            <link>https://velog.io/@kwon_yongil_/Consistent-Nonlocking-Reads-MySQL-8.4</link>
            <guid>https://velog.io/@kwon_yongil_/Consistent-Nonlocking-Reads-MySQL-8.4</guid>
            <pubDate>Fri, 08 Nov 2024 23:13:38 GMT</pubDate>
            <description><![CDATA[<p><a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html">https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html</a></p>
<h2 id="consistent-read">Consistent read</h2>
<ul>
<li>InnoDB는 multi-versioning을 통해서, 데이터 베이스의 특정 시점의 쿼리를 표현한다.</li>
<li>트랜잭션에 의해서 수행된 쿼리는, 이후나 커밋되지 않은 트랜잭션들로 인해 영향받지 않는다.</li>
<li>오직 이전에 수행된 트랜잭션들에 의해서 영향을 받을 수 있다.</li>
<li>같은 트랜잭션내에서 발생된 앞선 변화들에 의한 예외는 존재하는데, 이걸 비일관성 문제라고 한다.<ul>
<li>예를 들어, 트랜잭션 A에서 테이블의 일부 행을 업데이트한 후 SELECT 문을 실행하면, 그 트랜잭션은 방금 업데이트한 행의 최신 상태를 볼 수 있습니다. 하지만 다른 행들에 대해서는 트랜잭션이 시작되었을 때의 일관된 스냅샷 상태를 읽게 됩니다. 같은 테이블에 대해 일부 행은 최신 버전, 다른 행은 이전 버전으로 보여줄 수 있어, 트랜잭션 시작 시점에 존재하지 않았던 혼재된 상태를 나타낼 수 있습니다.</li>
<li>만약 트랜잭션 A가 특정 행을 업데이트하고, 다른 트랜잭션 B가 같은 테이블의 다른 행을 수정했다면, 트랜잭션 A의 SELECT 쿼리는 트랜잭션 A의 최신 업데이트를 포함하면서도, 트랜잭션 B가 수정한 최신 행은 포함하지 않는, 실제로 존재하지 않는 혼합된 데이터 상태를 조회하게 됩니다.</li>
</ul>
</li>
</ul>
<h2 id="first-snapshot">First snapshot</h2>
<h4 id="repeatable_read">REPEATABLE_READ</h4>
<ul>
<li>하나의 트랜잭션 내에서 트랜잭션의 첫번째 읽기에 대한 스냅샷을 일관적으로 읽는 것이 가능하다.</li>
<li>읽고 있는 레코드들에 대해 다른 트랜잭션이 삭제하고, 수정, 삽입하는 경우에도 영향을 받지 않는다.</li>
<li>읽기 스냅샷은 <code>SELECT</code>에만 적용이 된다. <code>DML</code>에 대해서는 이미 커밋된(즉, 스냅샷이 아닌) 데이터를 수정할 수 있다.</li>
</ul>
<pre><code class="language-sql">SELECT COUNT(c1) FROM t1 WHERE c1 = &#39;xyz&#39;;
-- Returns 0: no rows match.
DELETE FROM t1 WHERE c1 = &#39;xyz&#39;;
-- Deletes several rows recently committed by other transaction.

SELECT COUNT(c2) FROM t1 WHERE c2 = &#39;abc&#39;;
-- Returns 0: no rows match.
UPDATE t1 SET c2 = &#39;cba&#39; WHERE c2 = &#39;abc&#39;;
-- Affects 10 rows: another txn just committed 10 rows with &#39;abc&#39; values.
SELECT COUNT(c2) FROM t1 WHERE c2 = &#39;cba&#39;;
-- Returns 10: this txn can now see the rows it just updated.</code></pre>
<h2 id="fresh-snapshot">Fresh snapshot</h2>
<h4 id="read_commited">READ_COMMITED</h4>
<ul>
<li>트랜잭션내에서 읽기가 발생하면, 새로운 스냅샷(fresh snapshot)을 읽을 수 있다.</li>
</ul>
<h4 id="locking-read">locking read</h4>
<pre><code>SELECT * FROM t FOR SHARE;</code></pre><ul>
<li>fresh rows를 쿼리한 트랜잭션이 끝날 때까지 잠금을 걸어 최신 데이터를 유지할 수 있다.</li>
</ul>
<h2 id="nonlocking">Nonlocking</h2>
<ul>
<li>일관적 읽기는, REAPEATABLE_READ와 READ_COMMITED에서 기본적인 모드이며, 락킹을 하지 않기 때문에 다른 세션들에서 자유롭게 테이블을 수정할 수 있다.</li>
</ul>
<h2 id="ddl">DDL</h2>
<ul>
<li>일관적인 읽기는 DROP_TABLE, ALTER_TALBE와 같은 DDL에서는 동작하지 않을 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백엔드 테스트 환경 개선기]]></title>
            <link>https://velog.io/@kwon_yongil_/%EB%B0%B1%EC%97%94%EB%93%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD-%EA%B0%9C%EC%84%A0%EA%B8%B0</link>
            <guid>https://velog.io/@kwon_yongil_/%EB%B0%B1%EC%97%94%EB%93%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD-%EA%B0%9C%EC%84%A0%EA%B8%B0</guid>
            <pubDate>Sun, 09 Jun 2024 00:13:08 GMT</pubDate>
            <description><![CDATA[<h1 id="백엔드-테스트-환경-개선기">백엔드 테스트 환경 개선기</h1>
<hr>
<p>안녕하세요, 광고공통개발팀에 yong입니다.</p>
<p>테스트를 작성하는 것은 코드의 품질과 서비스 안정성을 증가시키고, 위험한 코드가 운영 환경에 배포되지 않도록 도움을 줍니다. 소속한 팀에서는, 깃헙(Github)에 PR(Pull Request)가 발생하면, CI(Continuous Integration) 서버에서 전체 테스트 케이스를 수행하여 결과 메시지를 수신하는 구조로 되어 있습니다.</p>
<p>CI(Continuous Integration) 서버에서 실패된 테스트를 로컬 환경에서 재수행하여 통과되는 경우가 발생했습니다. 멱등성이 지켜지지 않는 테스트가 생기면서 결과에 대한 신뢰도가 떨어지자, CI(Continuous Integration) 서버의 경고를 무시하게 되고, 문제가 발생했을 때 놓칠 가능성이 높아졌습니다.</p>
<p>테스트 결과를 무시하는 상황은 코드 품질 저하와 소프트웨어의 안정성 문제로 이어질 수 있으며, 사용자 경험에 부정적인 영향을 미칠 수 있다는 생각이 들었습니다.</p>
<p>테스트의 멱등성을 보장하고 신뢰도를 높이기 위한 노력이 필요했습니다. 쾌적한 테스트 환경을 만들기 위한 과정에서 겪은 경험들을 소개하도록 하겠습니다.</p>
<hr>
<h2 id="간헐적으로-깨지는-테스트-케이스">간헐적으로 깨지는 테스트 케이스</h2>
<p>간헐적으로 깨지는 테스트 케이스들은 임베디드 데이터베이스를 사용하는 통합 테스트인 경우가 많았습니다. 데이터베이스의 상태를 확인하기 위한 Assertion을 사용한 코드가 실패하는 경우였습니다. 신기하게도, 실패한 테스트만 단독으로 수행시키면, 테스트가 성공하는 경우도 있습니다.</p>
<p>실패한 테스트를 단독으로 실행했을 때와 모듈 내의 모든 클래스들을 실행했을 때의 차이가 무엇인지 알아보기 위해서는 테스트 환경에서 스프링 컨텍스트가 동작하는 방식을 이해해야 합니다.</p>
<p>동일한 스프링 컨텍스트라면 재사용하기 위한 캐싱 기능이 제공됩니다. 동일한 키를 가진 컨텍스트가 재사용되면, 테스트 시간 단축도움이 되는 장점이 있습니다. <strong>하지만, 동일한 컨텍스트 공유로 인해 테스트 격리성이 지켜지지 않는 상황들이 존재할 수 있습니다.</strong></p>
<h3 id="caching-of-spring-context">Caching of spring context</h3>
<p>테스트 환경에서 스프링 컨텍스트를 매번 띄우는 비용을 최소화하기 위해 컨텍스트 캐싱이 지원되고 있습니다. 일반적으로 작성된 테스트 클래스에서 여러 @Test 메소드들이 동일한 컨텍스트를 재사용하는 것을 확인할 수 있습니다.</p>
<p>스프링 컨텍스트가 재사용되는 로그를 확인하기 위해 아래 프로퍼티를 추가해줍니다.</p>
<pre><code>logging.level.root=DEBUG</code></pre><p>아래 두 개의 테스트 클래스에서는 스프링 컨텍스트가 공유되지 않습니다. TestExample2에서 @MockBean을 사용했기 때문입니다.</p>
<pre><code class="language-java">@SpringBootTest
public class TestExample1 {

    @Test
    public void test() {
        System.out.println(&quot;test!&quot;);
    }
}</code></pre>
<pre><code class="language-java">@SpringBootTest
public class TestExample2 {
    @MockBean
    Repository repository;

    @Test
    public void test() {
        System.out.println(&quot;test!&quot;);
    }
}</code></pre>
<p>두 테스트가 캐싱된 컨텍스트를 사용하지 않았다는 사실은 로깅을 통해서 확인할 수 있습니다. <strong>비교를 위해 MockBean을 제거하고 두 테스트를 수행해보면, missCount가 줄어든 것을 볼 수 있습니다.</strong></p>
<pre><code>ApplicationContext cache statistics: [DefaultContextCache@19fc0ef7 size = 1, maxSize = 32, parentContextCount = 0, hitCount = 12, missCount = 2]</code></pre><p>동일한 컨텍스트로 판단되는 기준은 <a href="https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/ctx-management/caching.html">스프링 문서</a>를 통해서 자세히 확인하실 수 있습니다.</p>
<h3 id="dirtiescontext">DirtiesContext</h3>
<p>MockBean이 사용되지 않아도 독립적인 컨텍스트를 사용하고 싶을 때는 DirtiesContext 어노테이션을 통해 테스트 격리성을 보장할 수 있습니다.</p>
<pre><code class="language-java">@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
@SpringBootTest
public class ServiceTest {

}</code></pre>
<p>모든 테스트를 격리하기 위해서, DirtiesContext를 모든 클레스에 붙이게 되면 매번 컨텍스트를 띄우는 오버헤드가 발생합니다. 스프링 컨텍스트를 띄우는 횟수가 증가함에 따라 테스트 전체 수행 시간이 증가될 수 있습니다.</p>
<p>테스트 전체 수행 시간이 늘어나는 비용을 감당할 수 없다면, 컨텍스트 캐싱을 사용하면서 격리성을 유지할 수 있는 방법을 찾아야 합니다. 방법으로서 Transactional, BeforeEach, AfterEach 어노테이션을 사용해볼 수 있습니다.</p>
<h3 id="transactional">Transactional</h3>
<p><a href="https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/tx.html">테스트 컨텍스트에서의 트랜잭션</a>은 격리성과 롤백을 보장하여 다른 테스트에 영향을 주지 않기 위한 용도로 사용됩니다.</p>
<pre><code class="language-java">@SpringBootTest
@Transactional
class TransactionalTest {

    @Autowired
    UserRepository userRepository;

    @Test
    @Order(0)
    void create() {
        userRepository.save(UserMaker.create());
    }

    @Test
    @Order(1) 
    void isEmpty() {
        assertThat(userRepository.findAll().isEmpty()).isTrue();
    }
}</code></pre>
<p>테스트 롤백이 발생하면, 아래의 로그를 확인할 수 있습니다.</p>
<pre><code>org.springframework.test.context.transaction.TransactionContext org.springframework.test.context.transaction.TransactionContext endTransaction:139 : Rolled back transaction for test: </code></pre><p>동기 환경에서는 PlatformTransactionManager를 구현하고 있지만, 비동기 환경에서는 PlatformTransactionManager를 구현하고 있지 않기 때문에 해당 어노테이션을 사용할 수 없습니다.</p>
<p>비동기 환경이라면, <a href="https://github.com/spring-projects/spring-framework/issues/24226#issuecomment-671656319">Github</a>을 참고하여 테스트를 작성해볼 수 있습니다.</p>
<h3 id="beforeeach-aftereach">BeforeEach, AfterEach</h3>
<p>PlatformTransactionManager를 구현하지 않는 데이터베이스라면 Transactional 어노테이션을 사용할 수 없습니다. BeforeEach나 AfterEach로 리소스를 정리하는 코드를 추가하면 격리성을 유지할 수 있습니다.</p>
<pre><code class="language-java">@SpringBootTest
class TransactionalTest {

    @Autowired
    ReactiveMongoTemplate template;

    @BeforeEach
    void beforeEach() {
          template.dropCollection(&quot;test&quot;).block();
    }
}</code></pre>
<p>@Transactional을 사용하지 못하면 트랜잭션 격리성(Isolation)이 보장되지 않으므로, AfterEach, BeforeEach를 사용하여 테스트 데이터를 정리하는 경우에 빌드 --parallel 옵션을 사용하려면 아래 설정이 필요합니다.</p>
<pre><code class="language-java">    tasks.test {
        useJUnitPlatform()
        maxParallelForks = 1
    }</code></pre>
<h3 id="extendwith">ExtendWith</h3>
<p>ExtendWith 어노테이션을 사용하므로서 공유 리소스에 대해 데이터를 지우는 코드를 공통화할 수 있습니다.</p>
<pre><code class="language-java">@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(RedisTestContainerConfig.class)
@ExtendWith(RedisTestContainerRollbackExtension.class)
@ContextConfiguration(initializers = RedisTestContainerConfig.Initializer.class)
public @interface RedisTestContainer {
}</code></pre>
<p>테스트 컨테이너를 사용하는 경우에 아래의 BeforeEachCallback 구현체를 이용할 수 있습니다.</p>
<pre><code class="language-java">public class RedisTestContainerRollbackExtension implements BeforeEachCallback {

    @Override
    public void beforeEach(ExtensionContext extensionContext) throws Exception {
        RedisTestContainerConfig.REDIS_SERVER
                .execInContainer(&quot;redis-cli&quot;, &quot;flushall&quot;);
    }
}</code></pre>
<hr>
<h2 id="임베디드-데이터베이스-테스트-환경">임베디드 데이터베이스 테스트 환경</h2>
<p>통합 테스트를 진행하기 위해서 임베디드 데이터베이스를 사용하는 경우가 많습니다. 상용 데이터베이스를 임베디드 형태로 제공하는 라이브러리를 사용해서 쉽게 데이터베이스 환경을 구축할 수 있습니다.</p>
<p>쉽게 사용할 수 있다는 장점도 있지만, 실제 환경과 다르다는 단점이 있습니다. 대표적인 경우로 운영 환경에서 MySQL, Oracle를 사용하는 경우에는 테스트 환경에서 H2를 임베디드 형태로 사용하는 경우가 있습니다.</p>
<p>임베디드 데이터베이스 환경은 호스트 환경을 이용해서 프로세스를 띄우는 형태이므로 호스트 환경과 결합이 강합니다. 임베디드 몽고 라이브러리로 널리 사용되는 de.flapdoodle.embed.mongo의 특정 버전에서 호스트 환경에서 실행하기 위해서 아래 코드처럼 호스트 환경과 밀접한 코드가 필요한 경우도 있습니다.</p>
<pre><code class="language-java">static { System.setProperty(&quot;os.arch&quot;, &quot;i686_64&quot;); }</code></pre>
<h3 id="testcontainer-필요성">Testcontainer 필요성</h3>
<p>임베디드 데이터베이스 환경의 한계를 극복하기 위해, 실제 환경과 동일한 데이터베이스처럼 사용할 수 있는 Testcontainer가 있습니다. 테스트 컨테이너를 사용하면, 운영 환경과 동일한 설정들을 사용할 수 있습니다.</p>
<p><a href="https://testcontainers.com/guides/introducing-testcontainers/">Testcontainer</a>는 운영 환경의 데이터베이스 버전과 일치하는 이미지를 받아와서 도커 환경에서 컨테이너를 띄우게 됩니다. 임베디드와 다르게 호스트 환경에서 포트는 도커 환경과 통신하기 위한 용도로만 사용됩니다. 즉, 호스트 환경에 영향받지 않고 Testcontainer를 사용할 수 있는 장점이 있습니다.</p>
<h3 id="커스텀-어노테이션">커스텀 어노테이션</h3>
<p>모든 테스트에서 Testcontainer를 코드를 추가하지 않고, 하나의 어노테이션으로 Testcontainer를 사용할 수 있는 환경을 만들어 코드의 중복을 제거할 수 있습니다.</p>
<p>아래 어노테이션을 붙이면, 사용하는 테스트 클래스에서 테스트 컨테이너를 사용할 수 있는 상태가 될 수 있습니다.</p>
<pre><code class="language-java">@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(value = {MongoDBTestContainerConfig.class})
@ExtendWith(MongoDBTestContainerRollbackExtension.class)
@ContextConfiguration(initializers = TestContainerInitializer.Initializer.class)
public @interface MongoDBTestContainer {
}</code></pre>
<p>아래 클래스의 BeforeAll 메소드를 통해서 Testcontainer를 동작시키고, BeforeEach를 통해 리소스를 정리하는 작업을 통해서 테스트 격리성을 유지시켜 줄 수 있습니다.</p>
<pre><code class="language-java">class MongoDBTestContainerRollbackExtension implements BeforeAllCallback, BeforeEachCallback {

    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        ReactiveMongoTemplate reactiveMongoTemplate = ReactiveMongoTemplateHolder.INSTANCE;
        reactiveMongoTemplate.getCollectionNames()
                             .flatMap(
                                   collectionName -&gt;
                                         reactiveMongoTemplate.remove(
                                               new Query(),
                                               collectionName
                                         )
                             )
                             .blockLast();
    }

    @Override
    public void beforeAll(ExtensionContext extensionContext) throws Exception {
        if (MongoDBTestContainerConfig.MONGO_DB_CONTAINER.isRunning()) {
            return;
        }

        MongoDBTestContainerConfig.MONGO_DB_CONTAINER.start();
    }
}</code></pre>
<p>ReactiveMongoTemplate을 static 환경에서 사용하기 위한 ReactiveMongoTemplateHolder 클래스도 추가됩니다.</p>
<pre><code class="language-java">@Component
public class ReactiveMongoTemplateHolder {

    public ReactiveMongoTemplateHolder(@Autowired ReactiveMongoTemplate reactiveMongoTemplate) {
        this.INSTANCE = reactiveMongoTemplate;
    }

    public static ReactiveMongoTemplate INSTANCE;

}</code></pre>
<p>아래 MongoDBTestContainerConfig를 통해서, static한 MongoDBContainer를 제공할 수 있게 됩니다. </p>
<pre><code class="language-java">public class MongoDBTestContainerConfig {

    public static MongoDBContainer MONGO_DB_CONTAINER;

    static {
        if (!StringUtils.isEmpty(PropertiesExtractor.getMongoDBVersion())) {
            MONGO_DB_CONTAINER = new MongoDBContainer(PropertiesExtractor.getMongoDBVersion());
        }
    }
}
</code></pre>
<p>아래 PropertiesExtractor 클래스는 static 환경에서 프로퍼티를 제공하기 위해 만들어진 클래스입니다.</p>
<pre><code class="language-java">public class PropertiesExtractor {
    private static Properties properties = new Properties();

    static {
        try (InputStream input = PropertiesExtractor.class
              .getClassLoader()
              .getResourceAsStream(&quot;testcontainer.properties&quot;)) {
            properties.load(input);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public static String getMongoDBVersion() {
        return properties.getProperty(&quot;testcontainer.mongodb.version&quot;);
    }

    public static String getMongoDBPrefix() {
        return properties.getProperty(&quot;testcontainer.mongodb.prefix&quot;);
    }

    public static String getElasticsearchVersion() {
        return properties.getProperty(&quot;testcontainer.elasticsearch.version&quot;);
    }

    public static String getElasticsearchPrefix() {
        return properties.getProperty(&quot;testcontainer.elasticsearch.prefix&quot;);
    }
}</code></pre>
<p>멀티 모듈 환경에서 testcontainer.properties에서 존재하는 프로퍼티들을 오버라이드하여 필요한 형태로 사용할 수 있습니다.</p>
<pre><code># mongodb
testcontainer.mongodb.version=mongo:5.0.13
testcontainer.mongodb.prefix=spring.data.mongodb

# elasticsearch
testcontainer.elasticsearch.version=docker.elastic.co/elasticsearch/elasticsearch:7.17.16
testcontainer.elasticsearch.prefix=spring.data.elasticsearch</code></pre><p>ApplicationContextInitializer를 구현한 static 클래스에서 운영 환경에서 필요한 프로퍼티들을 주입합니다. 주입된 프로퍼티를 통해 테스트 환경과 운영 환경이 동일한 시스템 설정을 사용할 수 있게 해줍니다.</p>
<pre><code class="language-java">public class TestContainerInitializer {

    public static class Initializer implements ApplicationContextInitializer&lt;ConfigurableApplicationContext&gt; {
        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {
            initializeMongoDBProperty(applicationContext);
            initializerElasticsearchProperty(applicationContext);
        }

        private static void initializeMongoDBProperty(ConfigurableApplicationContext applicationContext) {
            if (MONGO_DB_CONTAINER != null &amp;&amp; MONGO_DB_CONTAINER.isRunning()) {
                MongoDBTestContainerConnectionInfo mongoDBTestContainerConnectionInfo =
                      new MongoDBTestContainerConnectionInfo(MONGO_DB_CONTAINER.getReplicaSetUrl());

                TestPropertyValues.of(
                                        PropertiesExtractor.getMongoDBPrefix() + &quot;.host=&quot; + mongoDBTestContainerConnectionInfo.hostPort(),
                                        PropertiesExtractor.getMongoDBPrefix() + &quot;.id=&quot;,
                                        PropertiesExtractor.getMongoDBPrefix() + &quot;.password=&quot;,
                                        PropertiesExtractor.getMongoDBPrefix() + &quot;.database=&quot; + mongoDBTestContainerConnectionInfo.database(),
                                        PropertiesExtractor.getMongoDBPrefix() + &quot;.autoIndexCreation=true&quot;
                                  )
                                  .applyTo(applicationContext);
            }
        }

        private static void initializerElasticsearchProperty(ConfigurableApplicationContext applicationContext) {
            if (ELASTIC_SEARCH_CONTAINER != null &amp;&amp; ELASTIC_SEARCH_CONTAINER.isRunning()) {
                TestPropertyValues.of(
                      PropertiesExtractor.getElasticsearchPrefix() + &quot;.host=&quot; + ELASTIC_SEARCH_CONTAINER.getHttpHostAddress()
                ).applyTo(applicationContext);
            }
        }
    }
}</code></pre>
<p>위의 코드들을 통해서, 최종적으로는 아래의 형태로 테스트 클래스를 작성할 수 있게 됩니다. 위에서 MongoDBTestContainer만 구현됐지만, ElasticSearchTestContainer 커스텀 어노테이션을 구현하는 것을 시도해보시길 바랍니다.</p>
<pre><code class="language-java">@MongoDBTestContainer
@ElasticSearchTestContainer
@SpringBootTest
public class ManagedBizWalletQueryServiceTest {</code></pre>
<h3 id="testcontainer-도입">Testcontainer 도입</h3>
<p>임베디드 데이터베이스는 어떤 호스트 환경에서 쉽게 사용할 수 있지만, Testcontainer는 CI(Continuous Integration) 서버에서 도커(docker)가 지원되어야 합니다. 운영 환경과 테스트 환경을 유사하게 만들어야 하는 상황이라면, TestContainer를 도입하는 것을 추천드립니다.</p>
<hr>
<h2 id="중복되는-테스트-클래스-제거">중복되는 테스트 클래스 제거</h2>
<p>멀티 모듈의 환경에서, 다른 모듈의 test에 존재하는 클래스에 대해서 의존성을 가질 수 없습니다. 모듈의 test에서 사용되는 클래스들을 특정 모듈의 main으로 이동시켜 클래스를 사용하는 경우도 있습니다.</p>
<p>테스트 환경을 위해 사용되는 클래스가 main에 존재하면, 다른 모듈의 main에서 테스트를 위한 용도의 클래스를 사용할 수 있는 가능성이 생기게 됩니다.</p>
<pre><code>a-module
- main
- test

b-moudle
- main
- test

test-only-module // 테스트만을 위한 모듈이지만, 공통 사용을 위해 main에 클래스 추가
- main</code></pre><p>테스트 환경에서 공통적으로 사용하기 위해서 만든 Config, Builder, Factory와 같은 클래스들은 testFixtures에 두면 main과 구분될 수 있습니다.</p>
<h3 id="testfixtures-사용하기">TestFixtures 사용하기</h3>
<p>testFixtures를 제공하려는 모듈에서, 아래 플러그인을 추가하고 의존성을 추가합니다. 플러그인이 추가되면, testFixtures에서 사용하는 의존성을 추가하는 문법인 testFixturesImplementation 사용할 수 있습니다.</p>
<pre><code>plugins {
    id(&quot;java-test-fixtures&quot;)
}

project(:&quot;a-module&quot;){
    testFixturesImplementation(&quot;org.springframework.boot:spring-boot-starter-data-redis&quot;)
    testFixturesImplementation(&quot;org.testcontainers:testcontainers:1.19.8&quot;)
    testFixturesImplementation(&quot;com.redis:testcontainers-redis:2.2.2&quot;)
    testFixturesImplementation(&quot;org.springframework.boot:spring-boot-starter-test&quot;)
}</code></pre><p>a-module의 testFixutres를 사용할 수 있게 되면, 아래와 같이 모듈이 구성된 것을 확인할 수 있습니다.</p>
<pre><code>a-module
- main
- test
- testFixtures</code></pre><p>이제 b-module에서 a-module의 testFixtures를 사용하려면, 아래 의존성을 추가하면 사용할 수 있습니다. 아래 의존성으로 b-module에서는 a-module의 testFixtures에 선언된 클래스를 공통으로 사용할 수 있게 됩니다.</p>
<pre><code>plugins {
    id(&quot;java-test-fixtures&quot;)
}

project(:&quot;b-module&quot;){
    testImplementation(testFixtures(project(&quot;:a-module&quot;)))
}</code></pre><p>testFixtures를 통해서 테스트 환경에서 코드의 중복을 줄일 수 있게 되어, 테스트 코드 작성에 필요한 시간을 줄일 수 있게 됩니다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>테스트 격리성 보장, 테스트 컨테이너, 테스트 코드 중복 제거 등의 방법들을 통해 테스트 환경을 개선할 수 있었습니다. 테스트 환경 개선에 관심이 있는 분들이 계시다면, 위의 방법들을 적용해서 테스트 환경이 개선되면 좋겠습니다. </p>
<p>감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠버네티스 스케줄링]]></title>
            <link>https://velog.io/@kwon_yongil_/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81</link>
            <guid>https://velog.io/@kwon_yongil_/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81</guid>
            <pubDate>Sat, 16 Mar 2024 01:24:15 GMT</pubDate>
            <description><![CDATA[<h3 id="스케줄링">스케줄링</h3>
<ul>
<li><p>컨테이너나 가상 머신과 같은 인스턴스를 새롭게 생성할 때, 그 인스턴스를 어느 서버에 생성할 것인지 결정하는 일을 뜻함</p>
</li>
<li><p>컨테이너를 생성하기 전에 특정 목적에 최대한 부합하는 워커 노드를 선택하는 작업이 스케줄링에 해당</p>
</li>
<li><p><code>ectd</code>는 분산 코디네이터라고 불리는 도구의 일종으로, 클라우드 플랫폼 등의 환경에서 여러 컴포넌트가 정상적으로 상호 작용할 수 있도록 데이터를 조정하는 역할을 담당</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kwon_yongil_/post/4ea10569-1e7c-4df1-a706-bc4b8271c773/image.png" alt=""></p>
<ul>
<li><p>인증, 인가, 어드미션 컨트롤러 등의 단계를 모두 거쳐 포드 생성 요청이 최종적으로 승인됐다면, API 서버는 <code>etcd</code>에 포드의 데이터를 저장하지만, <code>nodeName</code> 항목을 설정하지 않은 상태로 저장한다. 스케줄링이 수행되어야, <code>nodeName</code>이 지정될 수 있다.</p>
</li>
<li><p><code>kube-scheduler</code>는 <code>API</code> 서버의 <code>Watch</code>를 통해 <code>nodeName</code>이 비어 있는 포드 데이터가 저장됐다는 사실을 인지하고, 해당 포드를 스케줄링 대상으로 판단하고 포드를 할당할 적절한 노드를 선택한 다음  <code>API</code> 서버에게 해당 노드와 포드를 바인딩할 것을 요청한다.</p>
</li>
</ul>
<hr>
<h3 id="스케줄링-과정">스케줄링 과정</h3>
<h4 id="노드-필터링">노드 필터링</h4>
<p>포드를 할당할 수 있는 노드와 그렇지 않은 노드를 분리해 걸러내는 단계, 포드에 설정된 <code>CPU</code>나 메모리의 <code>Requests</code>만큼 가용 자원이 존재하지 않는 노드는 노드 필터링 단계에서 제외된다. 그 외에도 기본적으로 마스터 노드는 포드를 할당할 수 없는 노드로 취급되며, 장애가 발생해 <code>kubectl get nodes</code>에서 <code>STATUS</code>가 <code>Ready</code>가 아닌 워커 노드 또한 제외된다.</p>
<hr>
<h4 id="노드-스코어링">노드 스코어링</h4>
<p>노드 스코어링 단계에서는 쿠버네티스의 소스코드에 미리 정의된 알고리즘의 가중치에 따라서 노드의 점수를 계산, 포드가 사용하는 도커 이미지가 이미 노드에 존재할 때는 빠르게 포드를 생성할 수 있기 때문에 해당 노드의 점수가 증가될 수 있다(<code>Image Locaity</code>). 또는 노드의 가용 자원이 많을수록 노드의 점수가 노게 평가될 수 있다(<code>Least Requested</code>).</p>
<hr>
<h3 id="노드-필터링-방법">노드 필터링 방법</h3>
<h4 id="nodename과-nodeselector를-사용한-스케줄링-방법">nodeName과 nodeSelector를 사용한 스케줄링 방법</h4>
<ul>
<li><p>특정 워커 노드에 포드를 할당하는 가장 확실한 방법은 포드의 <code>YAML</code> 파일에 노드의 이름을 직접 명시하는 방법</p>
</li>
<li><p>노드의 이름을 고정으로 설정하면, 다른 환경에 유연하게 대처할 수 없음</p>
</li>
<li><p>노드의 이름보다는 노드의 라벨을 사용하는 것이 조금 더 나은 방법임</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kwon_yongil_/post/fac327fa-6c91-492c-864f-39e2e8eab198/image.png" alt=""></p>
<ul>
<li><p><code>kubectl label nodes &lt;노드 이름&gt; &lt;추가할 라벨&gt;</code> 명령어를 통해서, 노드에 라벨링을 할 수 있음</p>
</li>
<li><p>포드의 적용될 <code>Yaml</code> 변경</p>
</li>
</ul>
<pre><code class="language-yaml">spec:
    nodeSelector:
        key : value</code></pre>
<hr>
<h4 id="node-affinity를-이용한-스케줄링-방법">Node Affinity를 이용한 스케줄링 방법</h4>
<ul>
<li><p>단순한 라벨링보다 조금 더 개선된 노드 선택방법</p>
</li>
<li><p><code>required</code> 형태는 하나의 조건만 만족하면 선택될 수 있다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kwon_yongil_/post/7a82f8c7-f4d7-4d7e-9111-2b9196e02c2c/image.png" alt=""></p>
<ul>
<li><p>조건식에서 <code>In</code>외에도 <code>NotIn</code>, <code>Exists</code>, <code>DoesNotExist</code>, <code>Gt</code>, <code>Lt</code>를 사용할 수 있어서 <code>nodeSelector</code>보다 더욱 다양하게 활용할 수 있다는 것이 특징이다.</p>
</li>
<li><p><code>required</code>는 반드시 만족해야 하는 제약 조건을 정의할 때 쓰이므로, <code>nodeSelcetor</code>의 기능을 확장했다고 생각할 수 있다.</p>
</li>
<li><p><code>preferred</code>는 조건에 만족하는 해당 노드에 가중치를 부여할 수 있음</p>
</li>
<li><p><code>Ignored</code> 방식들은 포드를 할당할 당시에만 유효하므로, 런타임에서는 다른 노드로 포드가 옮겨가는 퇴거가 발생하지 않는다.</p>
</li>
</ul>
<hr>
<h3 id="pod-affinity를-이용한-스케줄링-방법">Pod Affinity를 이용한 스케줄링 방법</h3>
<ul>
<li><code>Node Affinity</code>가 특정 조건을 만족하는 노드를 선택하는 방법이라면, <code>Pod Affinity</code>는 특정 조건을 만족하는 포드와 함께 실행되도록 스케줄링</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kwon_yongil_/post/4e7bd8e2-09b7-4a1d-9282-83214097723c/image.png" alt=""></p>
<ul>
<li><p><code>topologyKey</code>를 통해서, 토폴리지가 1, 2로 분리되고 동일한 노드에 포드가 할당될 수 있지만, 동일한 그룹(토폴로지)에 속하는 다른 노드에 포드가 할당될 수 있으므로, 노드 A와 B에 포드가 스케줄링될 가능성이 존재한다.</p>
</li>
<li><p><code>required</code>를 사용하면 토폴로지마다 반드시 하나의 포드만 할당하게 되지만, <code>preferred</code>를 사용하면 각 토폴로지의 노드에 포드를 여러 개 할당할 수도 있다.</p>
</li>
</ul>
<hr>
<h3 id="pod-anti-affinity를-이용한-스케줄링-방법">Pod Anti-affinity를 이용한 스케줄링 방법</h3>
<ul>
<li><p><code>Pod Affinity</code>가 특정 포드와 동일한 토폴로지에 존재하는 노드를 선택한다면, <code>Pod Anti-affinity</code>는 특정 포드와 같은 토폴로지의 노드를 선택하지 않는 방법</p>
</li>
<li><p>이 원리를 잘 이용하면 고가용성을 보장하기 위해 포드를 여러 가용 영역 또는 리전에 멀리 퍼뜨리는 전략을 세울 수도 있음</p>
</li>
</ul>
<hr>
<h3 id="taints와-tolerations-사용하기">Taints와 Tolerations 사용하기</h3>
<h4 id="taints와-tolerations를-이용한-포드-스케줄링">Taints와 Tolerations를 이용한 포드 스케줄링</h4>
<ul>
<li><p>노드의 얼룩(<code>Taints</code>)을 용인(<code>Tolerations</code>)할 수 있는 포드만 해당 노드에 할당할 수 있도록 함</p>
</li>
<li><p>노드의 얼룩(<code>Taints</code>)은 쿠버네티스 이벤트에 의해서도 발생한다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kwon_yongil_/post/e1fa1b4a-af80-4e48-a1fb-6a4d6000cb4f/image.png" alt=""></p>
<ul>
<li><p>라벨과 유사하지만, 라벨과는 다른점은 <code>key=value</code> 뒤에 `효과를 추가로 명시할 수 있음</p>
</li>
<li><p><code>Taint</code> 효과는 <code>Taint</code>가 노드에 설정됐을 때 어떠한 효과를 낼지 결정할 수 있으며, 효과에는 <code>NoSchedule</code>, <code>NoExecute</code>, <code>PreferNoSchedule</code> 총 3가지가 존재한다.</p>
</li>
<li><p>쿠버네티스는 기본적으로 다양한 <code>Taint</code>를 노드에 설정하며, 마스터 노드가 아닌 워커 노드에 포드들이 생성되는 이유도 <code>Taint</code>를 통해서 포드가 할당되는 것을 방지하고 있음</p>
</li>
<li><p><code>NoExecute</code>는 <code>NoSchedule</code>과 달리 기존 포드를 모두 종료시킬 수 있다. </p>
</li>
<li><p>파드를 실행시켜보면, 기본적으로 설정되는 <code>Toleration</code>이 존재한다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kwon_yongil_/post/61ead5fd-92b2-4d75-bc5d-74f39bf3e208/image.png" alt=""></p>
<ul>
<li>파드는 <code>Toleration</code>에 대해서 용인할 수 있는 <code>Seconds</code>를 설정해서, 해당 노드가 문제가 발생하더라도 해당 <code>Seconds</code>동안은 다른 포드로 이동하지 않을 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kwon_yongil_/post/5d48e31b-dba8-4074-92d0-304f9920ede8/image.png" alt=""></p>
<h3 id="cordon-drain-및-poddistributionbudget">Cordon, Drain 및 PodDistributionBudget</h3>
<h4 id="cordon을-이용한-스케줄링-비활성화">cordon을 이용한 스케줄링 비활성화</h4>
<ul>
<li><p><code>kubectl cordon</code>을 통해서 해당 노드에 더 이상 포드가 스케줄링되지 않도록 설정할 수 있다.</p>
</li>
<li><p><code>cordon</code>을 사용하면, <code>STATUS</code>에 <code>SchedulingDisabled</code> 설정이 됐음을 확인할 수 있음</p>
</li>
<li><p><code>cordon</code> 명령어는 <code>NoExecute</code>가 아닌 <code>NoSchedule</code> 효과가 있는 <code>Taint</code>를 노드에 추가하기 때문</p>
</li>
</ul>
<hr>
<h4 id="drain-명령어로-노드-비활성화하기">drain 명령어로 노드 비활성화하기</h4>
<ul>
<li><p><code>drain</code>은 <code>cordon</code>처럼 해당 노드에 스케줄링을 금지한다는 것은 같지만, 노드에서 기존에 실행중이던 포드를 다른 노드로 옮겨가도록 퇴거(<code>Eviction</code>)를 수행한다는 점이 다름</p>
</li>
<li><p>커널 버전 업그레이드, 유지 보수 등의 이유로 인해 잠시 노드를 중지해야 할 때 유용하게 사용할 수 있는 명령어</p>
</li>
<li><p>디플로이먼트나 레플리카셋, 잡, 스테이트풀셋 등에 의해 생성되지 않은 포드, 즉 단일 포드가 노드에 존재할 때도 <code>drain</code> 명령어는 실패할 수 있다. <code>type: Pod</code>처럼 정의해 생성한 단일 포드는 어떠한 이유로 종료되더라도 다른 노드로 옮겨가 다시 생성되지 않기 때문이다.</p>
</li>
</ul>
<hr>
<h4 id="poddisruptionbudget으로-포드-개수-유지하기">PodDisruptionBudget으로 포드 개수 유지하기</h4>
<ul>
<li><code>drain</code>과 같이 포드 퇴거 작업이 수행될 때는 애플리케이션 중단이 될 수 있음</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kwon_yongil_/post/811b6467-4143-4b18-8393-e4588089d3bf/image.png" alt=""></p>
<ul>
<li><p>디플로이먼트에서 포드의 개수를 여러 개로 설정함으로써 일정 개수의 포드가 중지되더라도 애플리케이션에 장애가 발생하지 않도록 구성할 수도 있음</p>
</li>
<li><p>그렇지만, 포드의 개수가 줄어든다는 것은 처리할 수 있는 요청의 총량은 일시적으로 감소함</p>
</li>
<li><p>이러한 상황에 대처할 수 있는 <code>PodDistruptionBudget</code>이라는 기능을 제공</p>
</li>
<li><p><code>maxUnavailable</code>는 비활성화될 수 있는 포드의 최대 개수 또는 비율을 의미하며, <code>minAvailable</code>은 포드의 퇴거가 발생할 때, 최소 몇 개의 포드가 정상 상태를 유지해야 하는지를 의미</p>
</li>
</ul>
<hr>
<h3 id="커스텀-스케줄러-및-스케줄러-확장">커스텀 스케줄러 및 스케줄러 확장</h3>
<h4 id="커스텀-스케줄러-구현">커스텀 스케줄러 구현</h4>
<ul>
<li>쿠버네티스에서 기본적으로 지원하는 기본 스케줄러(<code>kube-scheduler</code>) 외에도 여러 개의 스케줄러를 동시에 사용할 수 있도록 지원</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[External API in @Transactional]]></title>
            <link>https://velog.io/@kwon_yongil_/External-API-in-Transactional</link>
            <guid>https://velog.io/@kwon_yongil_/External-API-in-Transactional</guid>
            <pubDate>Sat, 09 Mar 2024 00:08:43 GMT</pubDate>
            <description><![CDATA[<h3 id="situation">Situation</h3>
<ul>
<li><p><code>@Transactional</code>에서 <code>POST</code>, <code>PUT</code>, <code>DELETE</code>와 같이 시스템에 변경을 가하는 외부 API를 호출함</p>
</li>
<li><p>앱과 연결된 데이터베이스는 트랜잭션 롤백이 반영되지만, 외부 API 처리를 롤백해주기 위해서는 별도의 방안이 필요함</p>
</li>
<li><p><code>POST</code>, <code>PUT</code>, <code>DELETE</code>에 대해서 요청할 때, 특정 키를 기준으로 발생함</p>
</li>
<li><p><code>API</code> 호출에 대한 응답값을 통해서 사용되는 데이터는 존재하지 않음 (성공적으로 호출 완료 응답만 받는 것이 목적)</p>
</li>
</ul>
<hr>
<h3 id="solution">Solution</h3>
<h4 id="api-호출을-코드-마지막-부분에-위치"><code>API</code> 호출을 코드 마지막 부분에 위치</h4>
<ul>
<li><p><code>API</code> 호출에 대한 응답값을 받는 것이 목적이므로, <code>API</code> 호출은 항상 코드 마지막 부분에 위치시킬 것이라고 생각할 수 있다.</p>
</li>
<li><p><code>API</code> 호출이 한 개인 경우는 가능하지만, <code>API</code>를 여러 개 호출한다면, 이 방법은 적합하지 않다.</p>
</li>
<li><p>특정 메소드는 한 번의 <code>API</code>만을 마지막에 호출해야 한다는 제약이 생긴다.</p>
</li>
</ul>
<h4 id="transactionaleventlistener-사용">@TransactionalEventListener 사용</h4>
<pre><code class="language-java">@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void rollback(CustomEvent customEvent) {
    if (customEvent.failProcess1()) {
        // rollback Api server1 Process
    }

    if (customEvent.failProcess2()) {
        // rollback Api server2 Process
    }

    ...
}
</code></pre>
<ul>
<li><p>발행된 이벤트를 통해서, 어떤 API 호출에 대해서 롤백해야 하는지 명시적으로 파악할 수 있음</p>
</li>
<li><p><code>SAGA</code> 패턴에서도 보면 이벤트 발행 방식으로 데이터를 롤백하여 최종 일관성을 통해서 데이터를 맞추는 것을 목적으로 하고 있음</p>
</li>
</ul>
<hr>
<h3 id="opinion-of-event">Opinion of Event</h3>
<p>이번 개인적인 상황에서는, <code>AFTER_COMMIT</code> 이벤트 방식을 사용하지 않았는데, <code>SAGA</code>패턴에서는 트랜잭션 커밋이 발생하면 다른 시스템으로 이벤트를 발행하는 방식을 사용하고 있다.</p>
<p><code>@TransactionalEventListener</code>와 <code>SAGA</code> 패턴에서 같은 이벤트 방식으로 데이터를 처리하고 있는데, 이 방식은 데이터가 일관성이 깨지는 순간이 존재할 수 있다는 것도 의미한다.</p>
<blockquote>
<p>트랜잭션에서 외부 API를 호출하는 것은 bad practice일까?</p>
</blockquote>
<ul>
<li><p>데이터베이스의 커넥션 풀은 한정된 리소스이며, <code>MSA</code> 환경에서 시스템을 직접 호출하는 것은 두 시스템의 리소스에 모두 영향을 줄 수 있음</p>
</li>
<li><p>중요한 API 호출이라면, 트랜잭션에 포함될 수도 있다고 생각한다.</p>
</li>
</ul>
<blockquote>
<p>이벤트 방식으로 처리하면서, 데이터 일관성이 깨지는 것이 괜찮은가?</p>
</blockquote>
<ul>
<li>도메인에 따라서, 데이터 일관성을 어디까지 맞출 것인가 에 대해서 논의를 통해서 결정하는 것이 맞아보인다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DDD] GENERIC SUBDOMAIN (일반 하위 도메인)]]></title>
            <link>https://velog.io/@kwon_yongil_/DDD-GENERIC-SUBDOMAIN-%EC%9D%BC%EB%B0%98-%ED%95%98%EC%9C%84-%EB%8F%84%EB%A9%94%EC%9D%B8</link>
            <guid>https://velog.io/@kwon_yongil_/DDD-GENERIC-SUBDOMAIN-%EC%9D%BC%EB%B0%98-%ED%95%98%EC%9C%84-%EB%8F%84%EB%A9%94%EC%9D%B8</guid>
            <pubDate>Sun, 03 Mar 2024 14:28:18 GMT</pubDate>
            <description><![CDATA[<h4 id="식별">식별</h4>
<ul>
<li><p>현재 진행 중인 프로젝트를 위한 것이 아닌 응집력 있는 하위 도메인을 식별해야 한다. </p>
</li>
<li><p>이러한 하위 도메인에서 일반화된 모델 요소를 추출해서 별도 <code>MODULE</code>에 배치할 수 있다.</p>
</li>
<li><p>하위 도메인으로 분리되고 나면 해당 하위 도메인의 계속되는 개발에 대해서는 <code>CORE DOMAIN</code>보다 낮은 우선순위를 부여하고 그 일에 핵심 개발자를 배치하지 않는다.</p>
</li>
</ul>
<h4 id="기성-솔루션">기성 솔루션</h4>
<ul>
<li><p>구현된 제품을 구입하거나 오픈 소스를 이용할 수 있다.</p>
</li>
<li><p>개발할 코드가 적어지며, 유지보수 부담이 외부화되고, 코드가 좀더 성숙하고 다양한 곳에서 사용되므로 사내에서 개발된 코드에 비해 실수가 적고 완전하다.</p>
</li>
<li><p>사용하기 전에 평가하고 이해하는 시간이 필요하며, 올바르고 안정적일 거라 확신할 수 없다. 오버 스펙이거나 통합에 있어서 매끄럽지 않은 부분이 있을 수 있다.</p>
</li>
</ul>
<h4 id="공표된-설계나-모델">공표된 설계나 모델</h4>
<ul>
<li><p>사내 모델보다 더 성숙되고 많은 사람들의 통찰력을 반영한다. 즉각적이고 높은 품질의 문서화 상태로 존재한다.</p>
</li>
<li><p>요구사항에 딱 맞지 않거나 과도한 설계일 수 있다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DDD] CORE DOMAIN (핵심 도메인)]]></title>
            <link>https://velog.io/@kwon_yongil_/DDD-CORE-DOMAIN-%ED%95%B5%EC%8B%AC-%EB%8F%84%EB%A9%94%EC%9D%B8</link>
            <guid>https://velog.io/@kwon_yongil_/DDD-CORE-DOMAIN-%ED%95%B5%EC%8B%AC-%EB%8F%84%EB%A9%94%EC%9D%B8</guid>
            <pubDate>Sun, 03 Mar 2024 12:13:40 GMT</pubDate>
            <description><![CDATA[<h4 id="core-domain">CORE DOMAIN</h4>
<ul>
<li><p><code>CORE DOMAIN</code>을 찾아 그것을 지원하는 다수의 모델과 코드로부터 쉽게 구별할 수 있는 수단을 제공해야 한다.</p>
</li>
<li><p><code>CORE DOMAIN</code>에 가장 재능 있는 인력을 할당하고 그에 따라 인력을 채용해야 한다.</p>
</li>
<li><p>시스템의 비전을 수행하기에 충분한 심층 모델을 찾고 유연한 설계를 개발할 수 있께 <code>CORE</code>에 노력을 쏟아야 한다.</p>
</li>
</ul>
<h4 id="선택">선택</h4>
<ul>
<li><p>어떤 <code>CORE DOMAIN</code>을 선택하느냐는 본인의 관점에 달렸다.</p>
</li>
<li><p>예를 들어, 수많은 애플리케이션에는 다양한 통화와 환율, 환산을 나타내는 화폐에 관한 일반화된 모델이 필요하다.</p>
</li>
<li><p>어떤 애플리케이션에서는 <code>CORE DOMAIN</code>인 것이 다른 애플리케이션에서는 일반화된 보조 컴포넌트에 해당할 수 있다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DDD] CONFORMIST (준수자)]]></title>
            <link>https://velog.io/@kwon_yongil_/DDD-CONFORMIST-%EC%A4%80%EC%88%98%EC%9E%90</link>
            <guid>https://velog.io/@kwon_yongil_/DDD-CONFORMIST-%EC%A4%80%EC%88%98%EC%9E%90</guid>
            <pubDate>Sun, 03 Mar 2024 04:30:53 GMT</pubDate>
            <description><![CDATA[<h4 id="필요성">필요성</h4>
<ul>
<li><p>두 개발 팀이 상류/하류 관계를 맺고 있고 상류 팀이 하류 팀의 필요성을 충족시킬 충분한 동기를 느낄 수 없다면 팀은 속수무책으로 무력해질 수밖에 없다.</p>
</li>
<li><p>이타주의를 발판 삼아 상류 개발자들이 약속을 할 수는 있어도 그 약속을 이행할 가능성은 희박하다.</p>
</li>
<li><p>상류 팀의 선한 의도를 신뢰한 하류 팀은 결코 사용할 수 없는 기능을 기반으로 계획을 작성하게 된다.</p>
</li>
<li><p>위와 같은 이유로 상류 팀을 신뢰할 수 없으면, <code>SEPRATE WAY</code>로 진행되기도 하는데 품질이 그렇게 떨어지지 않고 합리적으로 두 형식을 모두 수용할 수 있다면 독립적인 모델을 포기하고 <code>CONFORMIST</code>로 진행된다.</p>
</li>
</ul>
<h4 id="준수가-항상-나쁜-것은-아니다">준수가 항상 나쁜 것은 아니다.</h4>
<ul>
<li><p>거대한 인터페이스를 제공하는 기성 컴포넌트를 사용할 경우 일반적으로 해당 컴포넌트에 포함된 암시적인 모델을 준수(<code>CONFORM</code>)해야 한다.</p>
</li>
<li><p>컴포넌트가 담당하는 범위는 좁기 때문에 더 발전된 지식이 존재할 수 있다.</p>
</li>
<li><p>모델을 준수하면서, 더 나은 설계로 이어질 수도 있다.</p>
</li>
</ul>
<h4 id="준수">준수</h4>
<ul>
<li><p>맹목적으로 상류 팀의 모델을 준수해서 <code>BOUNDED CONTEXT</code> 간의 번역에 따른 복잡도를 제거해야 한다.</p>
</li>
<li><p><code>CONFOMRIST</code>를 따를 경우 하류 팀 설계자들의 설계 형식이 상류 팀에 속박되고 애플리케이션을 위한 이상적인 모델을 만들지는 못해도 통합 자체는 매우 단순해질 수 있다.</p>
</li>
</ul>
<h4 id="shared-kernel">SHARED KERNEL</h4>
<ul>
<li><p><code>CONFORMIST</code>는 동일한 모델을 이용하는 영역과 추가를 통해 모델을 확잫안 영역, 다른 모델이 영향을 미치는 영역을 보유한다는 점에서 <code>SHARED KERNEL</code>과 유사</p>
</li>
<li><p>두 패턴 간의 차이점은 의사결정과 개발 과정에 있다. <code>SHARED KERNEL</code>이 밀접하게 조율하는 두 팀 간의 협력관계를 다룬다면 <code>CONFORMIST</code>는 협력에 관심이 없는 팀과의 통합 문제를 다룬다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DDD] PUBLISHED LANGUAGE (공표된 언어)]]></title>
            <link>https://velog.io/@kwon_yongil_/DDD-PUBLISHED-LANGUAGE-%EA%B3%B5%ED%91%9C%EB%90%9C-%EC%96%B8%EC%96%B4</link>
            <guid>https://velog.io/@kwon_yongil_/DDD-PUBLISHED-LANGUAGE-%EA%B3%B5%ED%91%9C%EB%90%9C-%EC%96%B8%EC%96%B4</guid>
            <pubDate>Sun, 03 Mar 2024 03:26:52 GMT</pubDate>
            <description><![CDATA[<h4 id="필요성">필요성</h4>
<ul>
<li><p>기존 도메인 모델로 직접 번역하거나 기존 모델에서 직접 번역해 오는 것은 좋은 해결책이 아닐지도 모른다.</p>
</li>
<li><p>그러한 모델은 지나치게 복잡하거나 아니면 제대로 도출된 것이 아닐 수도 있으며, 아마 문서화돼 있지 않을 것이다.</p>
</li>
<li><p>한 모델을 데이터 교환 언어로 사용한다면 해당 모델은 본질적으로 굳어질 테고 새로운 개발 요구사항에 대응하지 못할 것이다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DDD] OPEN HOST SERVICE (공개 호스트 서비스)]]></title>
            <link>https://velog.io/@kwon_yongil_/DDD-OPEN-HOST-SERVICE-%EA%B3%B5%EA%B0%9C-%ED%98%B8%EC%8A%A4%ED%8A%B8-%EC%84%9C%EB%B9%84%EC%8A%A4</link>
            <guid>https://velog.io/@kwon_yongil_/DDD-OPEN-HOST-SERVICE-%EA%B3%B5%EA%B0%9C-%ED%98%B8%EC%8A%A4%ED%8A%B8-%EC%84%9C%EB%B9%84%EC%8A%A4</guid>
            <pubDate>Sun, 03 Mar 2024 03:21:33 GMT</pubDate>
            <description><![CDATA[<h4 id="필요성">필요성</h4>
<ul>
<li><p>어떤 하위 시스템을 다른 여러 하위 시스템과 통합해야 할 경우 각 하위 시스템에 대한 번역기를 조정한다면 팀 전체가 교착 상태에 빠질 수 있다.</p>
</li>
<li><p>변경이 발생할 때는 유지보수하고 걱정해야 할 일이 더욱 많은 법이다.</p>
</li>
<li><p>하위 시스템에 일관성이 있다면 그와 같은 일관성을 다른 하위 시스템에 대한 공통의 요구사항을 포괄하는 일련의 <code>SERVICE</code>로 설명하는 것도 가능할 것이다.</p>
</li>
</ul>
<h4 id="구현">구현</h4>
<ul>
<li><p>프로토콜을 공개해서 개발 중인 시스템과 통합하고자 하는 모든 이들의 해당 프로토콜을 사용할 수 있게 한다.</p>
</li>
<li><p>새로운 통합 요구사항을 처리하게끔 프로토콜을 개선하고 확장하되 특정한 한 팀에서 요청해 오는 독특한 요구사항은 제외해야 한다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA ENTITY LISTENER]]></title>
            <link>https://velog.io/@kwon_yongil_/JPA-ENTITY-LISTENER</link>
            <guid>https://velog.io/@kwon_yongil_/JPA-ENTITY-LISTENER</guid>
            <pubDate>Sat, 02 Mar 2024 08:34:07 GMT</pubDate>
            <description><![CDATA[<h3 id="entitylistener">EntityListener</h3>
<ul>
<li>JPA의 Entity에서 발생하는 변화들을 인지하기 위한 역할로서 사용되는 클래스</li>
<li>Entity와 관련된 트랜잭션(라이프사이클)에 관여하여 감사(Audit)하기 위한 용도로 사용될 수 있음</li>
</ul>
<pre><code class="language-java">@Entity
@EntityListeners(EntityListener.class)
class Entity</code></pre>
<hr>
<h3 id="implementation">Implementation</h3>
<ul>
<li>@PostPersist, @PostRemove, @PostUpdate의 형태로 사용할 수 있음</li>
<li>JPA의 엔티티 라이프사이클과 관련된 행동들을 정의할 수 있음</li>
</ul>
<pre><code class="language-java">@Slf4j
public class EntityListener {    
    @PostPersist
    public void postPersist(Entity entity) {
        log.info(&quot;Auditing... &quot; + entity.getId());
    }
}</code></pre>
<hr>
<h3 id="autowired">Autowired</h3>
<ul>
<li>아래의 형태로, <code>Repository</code>에 <code>Operation</code>을 발생시키면,<code>ConcurrenetModificationException</code>이 발생하게 됨</li>
<li>데이터 조회 쿼리만 발생하는 경우도 해당됨</li>
<li>JPA 공식 Spec을 확인해보면, <code>EntityListener</code>에서는 <code>Repository</code>를 호출하지 않아야 함을 명시하고 있음
<a href="https://jakarta.ee/specifications/persistence/3.1/jakarta-persistence-spec-3.1#persisting-an-entity-instance:~:text=The%20following%20rules%20apply%20to%20lifecycle,entity%20on%20which%20it%20is%20invoked">https://jakarta.ee/specifications/persistence/3.1/jakarta-persistence-spec-3.1#persisting-an-entity-instance:~:text=The%20following%20rules%20apply%20to%20lifecycle,entity%20on%20which%20it%20is%20invoked</a>.</li>
</ul>
<blockquote>
<p>In general, the lifecycle method of a portable application should not invoke EntityManager or query operations, access other entity instances, or modify relationships within the same persistence context[44][45]. A lifecycle callback method may modify the non-relationship state of the entity on which it is invoked.</p>
</blockquote>
<pre><code class="language-java">public class EntityListener {
    private static AnotherEntityRepository anotherEntityRepository;

    @Autowired
    public void init(AnotherEntityRepository anotherEntityRepository) {
        this.anotherEntityRepository = anotherEntityRepository;
    }

    @PostPersist
    public void postPersist(Entity entity) {
        anotherEntityRepository.countAll();
    }
}</code></pre>
<hr>
<h3 id="event-programming">Event Programming</h3>
<ul>
<li><code>EntityListener</code>에서 추가적인 작업이 필요하면, 이벤트 프로그래밍을 통해서 작업할 수 있음</li>
</ul>
<pre><code class="language-java">@Component
public class WithdrawAccountSnapshotEntityListener {
    private static ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    public void init(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    @PostPersist
    public void onPostPersist(Entity entity) {
        applicationEventPublisher.publishEvent(new CustomEvent(entity.getId()));
    }
}</code></pre>
<ul>
<li>이벤트 핸들러를 통해서, 트랜잭션을 분리하고 비동기적으로 이벤트를 처리하도록 작성할 수 있음</li>
<li><code>EntityListener</code>에서는 메시지 발행에 대한 역할만 부여하고 이벤트 프로그래밍을 통해서 추가 로직에 대해서 구현</li>
<li>이벤트 핸들 로직에서는 도메인 로직을 포함하면 안된다. 포함하면, 도메인 로직이 여기저기 분산되는 문제가 발생할 수 있음</li>
<li>이벤트 핸들 로직에서는 기존 도메인에 영향을 주지 않는 코드들에 대해서만 작성하는 것이 좋아보임<ul>
<li>다른 시스템으로 노티 메시지 전송</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
@Slf4j
public class EntityEventHandler {
    private final AnotherEntityRepository anotherEntityRepository;
    private final MessageQueue messagequeue;

    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, classes = CustomEvent.class, fallbackExecution = true)
    @Transactional
    public void handle(CustomEvent customEvent) {
        anotherEntityRepository.countAll();
    }
}
</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DDD] ANTICORRUPTION LAYER (오류 방지 계층)]]></title>
            <link>https://velog.io/@kwon_yongil_/DDD-ANTICORRUPTION-LAYER-%EC%98%A4%EB%A5%98-%EB%B0%A9%EC%A7%80-%EA%B3%84%EC%B8%B5</link>
            <guid>https://velog.io/@kwon_yongil_/DDD-ANTICORRUPTION-LAYER-%EC%98%A4%EB%A5%98-%EB%B0%A9%EC%A7%80-%EA%B3%84%EC%B8%B5</guid>
            <pubDate>Wed, 28 Feb 2024 14:32:19 GMT</pubDate>
            <description><![CDATA[<h4 id="격리-계층">격리 계층</h4>
<ul>
<li><p>클라이언트 고유의 도메인 모델 측면에서 기능을 제공할 수 있는 격리 계층을 만들어야 한다.</p>
</li>
<li><p>격리 계층은 기존에 이미 존재하는 인터페이스를 거쳐 다른 시스템과 통신하므로 다른 시스템을 거의 수정하지 않아도 된다.</p>
</li>
<li><p>해당 계층에서는 내부적으로 필요에 따라 두 모델을 상대로 양방향으로 번역을 수행한다.</p>
</li>
</ul>
<h4 id="facade-구현">FACADE 구현</h4>
<ul>
<li><p>FACADE는 하위 시스템에 대한 클라이언트의 접근을 단순화하고 더 쉽게 하위 시스템을 사용할 수 있게 만들어주는 대안 인터페이스에 해당</p>
</li>
<li><p>FACADE는 다른 시스템의 BOUNDED CONTEXT에 속한다.</p>
</li>
</ul>
<h4 id="adapter-구현">ADAPTER 구현</h4>
<ul>
<li>행위를 구현하는 측에서 이해한 것과 다른 프로토콜을 클라이언트에서 사용할 수 있게 해주는 래퍼(wrapper)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kwon_yongil_/post/61d9d18d-bfba-418c-a07a-eb415121df8a/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DDD] SHARED KERNEL(공유 커널)]]></title>
            <link>https://velog.io/@kwon_yongil_/DDD-SHARED-KERNEL%EA%B3%B5%EC%9C%A0-%EC%BB%A4%EB%84%90</link>
            <guid>https://velog.io/@kwon_yongil_/DDD-SHARED-KERNEL%EA%B3%B5%EC%9C%A0-%EC%BB%A4%EB%84%90</guid>
            <pubDate>Wed, 21 Feb 2024 00:27:10 GMT</pubDate>
            <description><![CDATA[<h3 id="필요성">필요성</h3>
<ul>
<li><p>밀접하게 연관된 애플리케이션을 대사응로 작업 중인 팀 간의 협력이 조율되지 않는다면 잠시 동안은 작업을 진행할 수 있겠지만 각 팀이 만들어낸 결과물을 함께 조합하기는 쉽지 않을 것이다.</p>
</li>
<li><p>결국 처음부터 <code>CONTINUOUS INTEGRATION</code>을 적용했을 때보다 더 많은 시간을 번역 계층을 개발하고 구조를 개선하는 데 허비하게 될 것이며, 동시에 공통 <code>UBIQUITOUS LANGUAGE</code>를 구축하는 작업이 중복되고 <code>UBIQUITOU LANGUAGE</code>로 얻을 수 있는 이점을 잃어버릴 것이다. </p>
</li>
</ul>
<h3 id="공유">공유</h3>
<ul>
<li><p>두 팀간에 공유하기로 한 도메인 모델의 부분 집합을 명시해야 한다.</p>
</li>
<li><p>모델 요소와 연관된 코드나 데이터베이스 설계의 부분 집합까지도 포함된다.</p>
</li>
<li><p>명시적으로 공유하는 부분들은 특별한 상태를 가지며, 다른 팀과의 협의 없이는 변경할 수 없다.</p>
</li>
</ul>
<h3 id="shared-kernel"><code>SHARED KERNEL</code></h3>
<ul>
<li><p><code>CORE DOMAIN</code>이거나 <code>GENERIC SUBDOMAIN</code>의 일부 또는 양쪽인 경우가 대부분이지만 두 팀에 모두 필요한 부분이라면 모델의 어떤 부분이라도 <code>SHARED KERNEL</code>이 될 수 있다.</p>
</li>
<li><p>목표는 중복을 줄이고 두 하위 시스템 간의 통합을 비교적 용이하게 만드는 것이다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kwon_yongil_/post/9b83e612-95ba-4ebf-a515-e8663054ea64/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DDD] CONTEXT MAP(컨텍스트 맵)]]></title>
            <link>https://velog.io/@kwon_yongil_/DDD-CONTEXT-MAP%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%A7%B5</link>
            <guid>https://velog.io/@kwon_yongil_/DDD-CONTEXT-MAP%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%A7%B5</guid>
            <pubDate>Wed, 21 Feb 2024 00:07:39 GMT</pubDate>
            <description><![CDATA[<h3 id="context-map-작성-기준">CONTEXT MAP 작성 기준</h3>
<ul>
<li><p>의사 소통을 위해 컨텍스트 간의 번역에 대한 윤곽을 명확하게 표현하고 컨텍스트 간에 공유해야 하는 정보를 강조함으로써 모델과 모델이 만나는 경계 지점을 서술해야 한다.</p>
</li>
<li><p>각 컨텍스트의 현재 영역을 나타내는 지도를 작성해야 한다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kwon_yongil_/post/c201d706-25be-45a2-8c2a-f9ded127402a/image.png" alt=""></p>
<h3 id="서로-다른-context간에-번역">서로 다른 CONTEXT간에 번역</h3>
<ul>
<li><p>서로 다른 CONTEXT간에 모델들을 번역할 수 있는 별도의 객체가 필요하다.</p>
</li>
<li><p>이 객체는 양 팀에서 합심해서 유지해야 할 유일한 객체로 볼 수 있다.</p>
</li>
<li><p>모델의 컨텍스트는 언제나 존재하지만 계속해서 주의하지 않으면 중복이 일어나고 불안정해진다.</p>
</li>
<li><p><code>BOUNDED CONTEXT</code>와 <code>CONTEXT MAP</code>을 명시적으로 정의하면 팀은 모델을 단일화하고 뚜렷이 구분되는 것들을 연결하는 절차를 통제할 수 있을 것이다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kwon_yongil_/post/543fdca2-e5fe-4fd8-93b4-55193f6d888b/image.png" alt=""></p>
<h3 id="테스트">테스트</h3>
<ul>
<li><p>다른 <code>BOUNDED CONTEXT</code>와 접촉하는 지점은 테스트할 때 특히 중요하다.</p>
</li>
<li><p>대개 테스트는 컨텍스트의 경계에 존재하는 번역의 미묘한 차이와 낮은 수준의 의사소통을 보완하는 데 기여한다.</p>
</li>
</ul>
<h3 id="소통">소통</h3>
<ul>
<li><p>팀의 모든 구성원이 서로 동일한 방식으로 개념적 경계를 이해하도록 개념적 경계에 관해 활발히 의사소통하는 것이다.</p>
</li>
<li><p>최종 목표는, &quot;특정 팀에서 만든 것이 변경될 예정이므로 그에 따라 저희 것도 변경할 예정입니다.&quot;라고 말하느 대신 &quot;A 모델이 바뀌고 있습니다. 그러니 저희 B 컨텍스트에 대한 번역기를 변경할 예정입니다.&quot;라고 얘기할 수 있어야 한다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DDD] BOUNDED CONTEXT (제한된 컨텍스트)]]></title>
            <link>https://velog.io/@kwon_yongil_/DDD-%EB%AA%A8%EB%8D%B8%EC%9D%98-%EB%AC%B4%EA%B2%B0%EC%84%B1-%EC%9C%A0%EC%A7%80</link>
            <guid>https://velog.io/@kwon_yongil_/DDD-%EB%AA%A8%EB%8D%B8%EC%9D%98-%EB%AC%B4%EA%B2%B0%EC%84%B1-%EC%9C%A0%EC%A7%80</guid>
            <pubDate>Mon, 19 Feb 2024 23:43:51 GMT</pubDate>
            <description><![CDATA[<h3 id="bounded-context">BOUNDED CONTEXT</h3>
<h4 id="다수의-모델">다수의 모델</h4>
<ul>
<li><p>규모가 큰 프로젝트에서는 다수의 모델이 사용되기 마련이다. </p>
</li>
<li><p>그러나 개별적인 모델을 기반으로 작성된 코드가 한데 섞이면 많은 버그가 발생하고 신뢰성이 떨어지며 이해하기 힘든 소프트웨어가 만들어진다.</p>
</li>
</ul>
<h4 id="컨텍스트">컨텍스트</h4>
<ul>
<li><p>컨텍스트는 코드의 특정 부분일 수도, 개별 팀이 수행하는 업무 일 수도 있다.</p>
</li>
<li><p>브레인스토밍 회의를 거쳐 만들어진 모델의 경우, 회의에서 오간 대화로 컨텍스트를 국한시킬 수 있다.</p>
</li>
<li><p>다수의 모델 탓에 발생하는 문제를 해결하려면 하나의 모델이 적용되고 가능한 한 통일된 상태로 유지할 수 있는 소프트웨어 내의 제한된 부분으로 특정 모델의 범위를 명호가하게 정의할 필요가 있다.</p>
</li>
</ul>
<h4 id="명시적-정의">명시적 정의</h4>
<ul>
<li><p>모델이 적용되는 컨텍스트를 명시적으로 정의해야 한다.</p>
</li>
<li><p>컨텍스트의 경계를 팀 조직, 애플리케이션의 특정 부분에서의 사용법, 코드 기반이나 데이터베이스 스키마와 같은 물리적인 형태의 관점에서 명시적으로 설정해야 한다.</p>
</li>
<li><p>경계 내에서는 모델을 엄격하게 일관된 상태로 유지하고 경계 바깥의 이슈 때문에 초점이 흐려지거나 혼란스러워져서는 안 된다.</p>
</li>
</ul>
<h4 id="bounded-context는-module이-아니다">BOUNDED CONTEXT는 MODULE이 아니다.</h4>
<ul>
<li><p>어떤 두 객체 집합이 각기 다른 모델을 구성한다고 여겨지면 두 객체 집합은 거의 항상 서로 다른 개별 <code>MODULE</code>내에 위치한다.</p>
</li>
<li><p>그러나 <code>MODULE</code>은 단일 모델 내에 포함된 요소를 구성하는 데도 사용되며, 꼭 개별 <code>CONTEXT</code>에 의도를 전하는 것은 아니다.</p>
</li>
<li><p><code>BOUNDED CONTEXT</code>내에 <code>MODULE</code>이 만들어낸 개별 네임스페이스가 포함되면 우발적으로 발생하는 모델의 단편화를 파악하기가 어려워진다.</p>
</li>
</ul>
<h4 id="예약-컨텍스트">예약 컨텍스트</h4>
<ul>
<li><p>예약이 완료되면 해당 예약 내역은 레거시 화물추적 시스템으로 전달돼야 한다. </p>
</li>
<li><p>레거시 모델과 구분하기로 사전에 결정했으므로, 레거시 화물추적 시스템은 구획 밖에 존재한다.</p>
</li>
<li><p>새로운 모델과 레거시 사이에 필요한 번역은 레거시 유지보수팀의 몫이다. 번역 메커니즘은 모델에 의해 주도되지 않는다.</p>
</li>
<li><p>하나의 <code>BOUNDED CONTEXT</code>에서 여러 팀이 동시에 작업하려면, 단일화 모델을 위해서 설계 과정에 같이 참여해야 한다.</p>
</li>
<li><p>여러 팀이 동시에 작업할 때는 비공식적인 정보 공유가 발생하지 않게 하고, 정보를 공유를 위한 프로세스가 정립되어야 한다.</p>
</li>
<li><p><code>BOUNDED CONTEXT</code>밖에 <code>CONTEXT</code>는 모델에서 벗어나는 자유로음을 얻을 수 있다.</p>
</li>
<li><p>모델을 책임지는 팀에서는 영속화를 비롯한 각 객체의 전체 생명주기를 다룬다. 이 팀에서 데이터베이스 스키마를 통제하므로 그 팀에서 신중하게 객체 관계형 매핑을 명확하게 유지해 오고 있다. 다시 말해서, 스키마는 모델에 의해 주도되며, 따라서 구획안에 존재한다.</p>
</li>
</ul>
<h4 id="균열-인식">균열 인식</h4>
<ul>
<li><p>실제로 같은 개념을 나타내는 두 개의 모델요소가 존재하는 중복된 개념이 발생한다.</p>
</li>
<li><p>두 사람이 서로 같은 것을 이야기하고 있다고 생각하지만 실제로는 그렇지 않은 경우인 허위 동족 언어가 발생한다.</p>
</li>
<li><p>균열이 발생하면, 독립적으로 모델을 개발하거나 프로세스를 정재해서 단편화를 막아야 한다.</p>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>