<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Official-Dev. blog</title>
    <link>https://jaehoney.tistory.com/</link>
    <description>예상과 추측을 넘어 반드시 데이터로 확인하는
서버 개발자입니다.</description>
    <language>ko</language>
    <pubDate>Wed, 8 Apr 2026 14:10:06 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>JaeHoney</managingEditor>
    <image>
      <title>Official-Dev. blog</title>
      <url>https://tistory1.daumcdn.net/tistory/4034457/attach/a0969b16e88c42eca05fb0b937dbdc54</url>
      <link>https://jaehoney.tistory.com</link>
    </image>
    <item>
      <title>Claude - Skills란 무엇인가? (Agent 잘 활용하기)</title>
      <link>https://jaehoney.tistory.com/442</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&amp;nbsp;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Skills는 특정 작업에서 &lt;b&gt;더 나은 성능&lt;/b&gt;을 발휘할 수 있도록 에이전트가 &lt;b&gt;동적&lt;/b&gt;으로 탐색하고 로드할 수 있는 지시문, 스크립트, 리소스의 정리된 폴더이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Claude 에이전트에게 특정 도메인이나 작업에 대한 &lt;b&gt;전문 지식&lt;/b&gt;을 제공하는 메커니즘이라고 생각할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 Skills를 사용할까?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude 에이전트가 특정 도메인의 작업을 수행할 때, 일반적인 지식만으로는 최적의 결과를 내기 어려울 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 프로젝트의 컨벤션이나 best practices&lt;/li&gt;
&lt;li&gt;복잡한 문제 해결을 위한 구체적인 단계&lt;/li&gt;
&lt;li&gt;특정 도메인의 전문적인 지식&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Skills의 해결책&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Skills를 정의함으로써 아래의 것들을 할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에이전트가 필요한 &lt;b&gt;순간에만 해당 지식을 로드&lt;/b&gt; (효율적인 토큰 사용)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관성 있는 결과&lt;/b&gt; 제공&lt;/li&gt;
&lt;li&gt;복잡한 작업을 &lt;b&gt;구조화된 방식&lt;/b&gt;으로 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 공통으로 지켜야 하는 개발 컨벤션의 경우에는 &lt;code&gt;rules&lt;/code&gt;에 정의할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;skills는 &lt;b&gt;특정 작업에 대한 전문 지식&lt;/b&gt;을 제공하는 데 초점을 맞추고 있다. 예를 들어, CVE 검사 및 수정과 같은 특정 작업에 대한 스킬을 정의할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Architecture&lt;/h2&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;폴더 구조&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;skills/
├── skill-1/
│   ├── SKILL.md
│   ├── script.py
│   └── template.json
├── skill-2/
│   ├── SKILL.md
│   └── guide.txt
└── skill-3/
    └── SKILL.md&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;skills 폴더&lt;/b&gt; 안에 &lt;b&gt;스킬 이름의 폴더&lt;/b&gt;를 생성하고, 그 안에 아래 파일을 구성한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SKILL.md&lt;/b&gt; (필수): 스킬의 메타데이터와 상세 설명&lt;/li&gt;
&lt;li&gt;관련 리소스 파일들: 스크립트, 템플릿, 가이드 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트는 사용자의 요청이 들어오면 아래와 같은 동작을 수행한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;필요한 SKILL을 선택&lt;/li&gt;
&lt;li&gt;해당 SKILL.md를 읽어 내용 파악&lt;/li&gt;
&lt;li&gt;필요시 관련 리소스 파일 로드&lt;/li&gt;
&lt;li&gt;해당 지식을 바탕으로 작업 수행&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;팁&lt;/b&gt;: 선택할 SKILL이 중복될 수 있기 때문에 &lt;b&gt;가능한 상세하고 명확하게&lt;/b&gt; 작성하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 명령어 목록에서 skills를 실제로는 활용하지 않았다면 위 부분을 체크하자.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SKILL.md 파일 포맷&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구조&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4tau0/dJMcabjmv11/rZkRIbjFRKkUhJSKPdhnJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4tau0/dJMcabjmv11/rZkRIbjFRKkUhJSKPdhnJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4tau0/dJMcabjmv11/rZkRIbjFRKkUhJSKPdhnJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4tau0%2FdJMcabjmv11%2FrZkRIbjFRKkUhJSKPdhnJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;536&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SKILL.md는 &lt;b&gt;Metadata&lt;/b&gt;와 &lt;b&gt;Body&lt;/b&gt; 두 가지 섹션으로 구성된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Metadata (YAML Front Matter)&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;---
name: &quot;CVE 검사 및 수정&quot;
description: &quot;프로젝트의 의존성에서 보안 취약점(CVE)을 감지하고 자동으로 수정하는 작업&quot;
input_schema:
  dependencies: &quot;패키지 목록 (package@version 형식)&quot;
  ecosystem: &quot;패키지 생태계 (npm, pip, maven 등)&quot;
target_model: &quot;claude-3-5-sonnet&quot;
---&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 필드&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;: 스킬의 간단한 이름&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt;: 스킬이 무엇을 하는지 명확하게 설명&lt;/li&gt;
&lt;li&gt;&lt;code&gt;input_schema&lt;/code&gt;: 스킬이 필요로 하는 입력값 정의&lt;/li&gt;
&lt;li&gt;&lt;code&gt;target_model&lt;/code&gt;: 이 스킬을 처리할 최적의 모델&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Body&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스킬의 &lt;b&gt;상세한 지시문&lt;/b&gt;과 &lt;b&gt;실행 방법&lt;/b&gt;을 기술한다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;## 개요
이 스킬은 프로젝트의 의존성에서 CVE를 검사하는 방법을 제공합니다.

## 단계별 처리 방법
1. 의존성 목록 파싱
2. 각 의존성별 CVE 조회
3. 취약점 발견시 패치 버전 제안
...

## 참고 파일
- `scripts/check-cve.py` - CVE 검사 스크립트
- `templates/report.json` - 보고서 템플릿&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일 참고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SKILL.md&lt;/code&gt;에서 다른 파일을 참고할 때는 &lt;b&gt;경로를 명확하게&lt;/b&gt; 작성해주면, 에이전트가 필요시 해당 파일을 로드할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;## 참고 파일
- `./scripts/deploy.sh` - 배포 스크립트
- `./templates/config.yaml` - 설정 템플릿&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Context Management: 효율적인 토큰 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Skills는 &lt;b&gt;3단계 레벨&lt;/b&gt;에서 Context를 다르게 관리함으로써 토큰 사용을 최적화한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;레벨&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;로드 시점&lt;/th&gt;
&lt;th&gt;토큰 사용량&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Metadata&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;SKILL.md의 YAML 헤더&lt;/td&gt;
&lt;td&gt;항상 로드&lt;/td&gt;
&lt;td&gt;~100 Tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Body&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;SKILL.md의 상세 내용&lt;/td&gt;
&lt;td&gt;스킬이 선택됨&lt;/td&gt;
&lt;td&gt;&amp;lt;5K Tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Bundle Files&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;참고 파일/리소스&lt;/td&gt;
&lt;td&gt;필요할 때만&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설계 의도&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;빠른 스킬 선택&lt;/b&gt;: Metadata만 로드하여 어떤 스킬이 필요한지 빠르게 판단&lt;/li&gt;
&lt;li&gt;&lt;b&gt;필요시만 상세 정보 로드&lt;/b&gt;: Body는 스킬이 확정되었을 때만 로드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연한 리소스 관리&lt;/b&gt;: 참고 파일은 에이전트가 필요시에만 로드&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 계층적 관리로 &lt;b&gt;컨텍스트 윈도우를 효율적으로 사용&lt;/b&gt;하면서도 &lt;b&gt;필요한 정보는 모두 제공&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 &quot;CVE를 확인해줘&quot;라고 요청하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Step 1&lt;/b&gt;: 모든 SKILL.md의 Metadata만 로드 &amp;rarr; CVE Remediator 스킬 발견 (~100 tokens)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Step 2&lt;/b&gt;: CVE Remediator의 Body 로드 &amp;rarr; 작업 방법 파악 (&amp;lt;5K tokens)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Step 3&lt;/b&gt;: 필요한 스크립트나 템플릿만 로드 &amp;rarr; 작업 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Deterministic (결정론적): 예측 가능한 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Skills의 주요 특성 중 하나는 &lt;b&gt;Deterministic&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결정론적(Deterministic)&lt;/b&gt;이란 동일한 입력에 대해 항상 동일한 출력을 생성한다는 뜻이다. 이는 &lt;b&gt;멱등성(Idempotency)&lt;/b&gt;과 유사한 개념이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 사례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스킬 없이&lt;/b&gt;: CVE 수정을 요청할 때마다 다른 패키지 버전을 제안할 수 있음&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;요청 1: react@16.0 &amp;rarr; react@18.2 제안
요청 2: react@16.0 &amp;rarr; react@17.5 제안 (다른 결과!)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스킬 사용&lt;/b&gt;: SKILL.md에서 명확한 지시문을 제공하므로 항상 같은 결과&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;요청 1: react@16.0 &amp;rarr; react@18.2 제안
요청 2: react@16.0 &amp;rarr; react@18.2 제안 (일관된 결과!)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 중요한가?&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;신뢰성&lt;/b&gt;: 사용자가 같은 작업을 반복해도 일관된 결과를 기대할 수 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디버깅&lt;/b&gt;: 문제가 발생했을 때 원인을 파악하기 쉽다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재사용성&lt;/b&gt;: 다른 팀이나 프로젝트에서도 동일한 결과를 기대할 수 있다&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Best Practices&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 명확한 설명 작성&lt;/h3&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;# 나쁜 예
description: &quot;코드 관련 스킬&quot;

# 좋은 예
description: &quot;프로젝트의 의존성에서 보안 취약점을 검사하고 자동으로 수정하는 스킬&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 구체적인 입력값 정의&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 나쁜 예
input_schema:
  packages: &quot;패키지들&quot;

# 좋은 예
input_schema:
  dependencies: &quot;package@version 형식의 의존성 목록 (예: react@16.0.0, vue@3.2.0)&quot;
  ecosystem: &quot;패키지 생태계 선택 (npm, pip, maven, go 등)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 단계별 명확한 지시문&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;## 처리 단계

1. **의존성 파싱**
   - package@version 형식 검증
   - 버전 범위 표준화

2. **CVE 데이터 조회**
   - 각 의존성별 알려진 취약점 검색
   - 심각도 분류

3. **패치 제안**
   - 최소 버전 제안
   - 호환성 검토&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 참고 파일의 경로를 명확하게&lt;/h3&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;더 자세한 내용은 다음을 참조하세요:
- `./scripts/validate.sh` - 검증 스크립트
- `./templates/report.json` - 보고서 템플릿&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스킬 선택 충돌&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 스킬의 설명이 비슷하면 에이전트가 어떤 스킬을 선택해야 할지 혼동할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결책&lt;/b&gt;: 각 스킬의 설명을 가능한 &lt;b&gt;구체적이고 유일하게&lt;/b&gt; 작성한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토큰 오버헤드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과도하게 큰 Body나 리소스 파일은 토큰을 낭비할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결책&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Body는 핵심 내용만 포함 (세부 사항은 참고 파일로 분리)&lt;/li&gt;
&lt;li&gt;리소스 파일은 필요한 것만 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Skills은 아래의 목적을 위해 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동적인 지식 제공&lt;/b&gt; 메커니즘&lt;/li&gt;
&lt;li&gt;&lt;b&gt;효율적인 토큰 관리&lt;/b&gt;로 비용 절감&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관성 있는 결과&lt;/b&gt; 보장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장 가능한 구조&lt;/b&gt;로 새로운 도메인 추가 용이&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 작업을 체계적으로 처리하고 싶다면 Skills를 활용해보자!&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Programming/AI Agent</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/442</guid>
      <comments>https://jaehoney.tistory.com/442#entry442comment</comments>
      <pubDate>Sat, 28 Mar 2026 16:27:10 +0900</pubDate>
    </item>
    <item>
      <title>DataBase - 시퀀스 전략 설계하기 (+ 성능 문제, 시퀀스 낭비 문제 등)</title>
      <link>https://jaehoney.tistory.com/441</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&amp;nbsp;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 데이터를 적재할 때 시퀀스의 개념을 사용할 때가 많다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ID(PK)&lt;/li&gt;
&lt;li&gt;Request Id&lt;/li&gt;
&lt;li&gt;카드번호&lt;/li&gt;
&lt;li&gt;사원번호&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스의 전략에 대해 알아보자. 예시 코드는 Hibernate를 사용했다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2313&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w3rwl/btsQIVUijub/zHTaAx3YdmDve6Gpk3j8R1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w3rwl/btsQIVUijub/zHTaAx3YdmDve6Gpk3j8R1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w3rwl/btsQIVUijub/zHTaAx3YdmDve6Gpk3j8R1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw3rwl%2FbtsQIVUijub%2FzHTaAx3YdmDve6Gpk3j8R1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2313&quot; height=&quot;642&quot; data-origin-width=&quot;2313&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AUTO_INCREMENT 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스는 DB에서 고유한 값을 순차적으로 생성하는 객체를 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 익숙한 것은 아래와 같이 DB의 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt;를 사용하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class  Member {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 설정하면 대 부분의 애플리케이션에 문제없이 돌아간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 추가로 고민해야 될 포인트가 있다. 성능적으로 문제가 없을 지이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능이 낭비되는 문제와 시퀀스 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AUTO_INCREMENT&lt;/code&gt; 전략을 사용하면 레코드를 생성할 때마다 매번 DB에 접근해서 다음 시퀀스 값을 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고성능 애플리케이션에서는 이런 부분을 주의해야 한다. 결제 시스템이 있고 1초 내에 반드시 결제를 마쳐야 한다고 가정이 있다면 지연을 조금이라도 줄여야 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 고민할 수 있는 게 &lt;code&gt;SEQUENCE&lt;/code&gt; 전략이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;order&quot;)
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = &quot;order_seq_gen&quot;)
    @SequenceGenerator(name = &quot;order_seq_gen&quot;, sequenceName = &quot;order_id_sequence&quot;, allocationSize = 100)
    private Long id;

    private String customerName;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 전략은 아래와 같이 동작한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DB에서 시퀀스를 100(allocationSize)만큼 증가시킨다.&lt;/li&gt;
&lt;li&gt;해당 시퀀스를 메모리에 보관한다.&lt;/li&gt;
&lt;li&gt;메모리에서 시퀀스를 순차적으로 사용한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당과 같이 별도 시퀀스를 사용해서 보관한다면 &lt;b&gt;미리 시퀀스를 확보&lt;/b&gt;해두고 실제 레코드가 생성되는 시점에 DB와의 통신을 대폭 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 미리 시퀀스를 50개든 100개든 확보해두고 100개의 시퀀스가 소진될 때 다시 DB에 접근해서 확보한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;317&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ei9IwV/btsQJw02VKZ/Mh0RQQtsls4MSJuX0TCtW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ei9IwV/btsQJw02VKZ/Mh0RQQtsls4MSJuX0TCtW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ei9IwV/btsQJw02VKZ/Mh0RQQtsls4MSJuX0TCtW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fei9IwV%2FbtsQJw02VKZ%2FMh0RQQtsls4MSJuX0TCtW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;692&quot; height=&quot;317&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;317&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SequenceGenerator&lt;/code&gt;가 사용하는 &lt;code&gt;PooledOptimizer&lt;/code&gt;를 보면 시퀀스의 현재 사용할 값과 채번한 마지막 값을 저장해두고 사용하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1331&quot; data-origin-height=&quot;1057&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHTFvz/btsQGQUj2mk/0Ab77idje4bOWpKkdxlcS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHTFvz/btsQGQUj2mk/0Ab77idje4bOWpKkdxlcS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHTFvz/btsQGQUj2mk/0Ab77idje4bOWpKkdxlcS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHTFvz%2FbtsQGQUj2mk%2F0Ab77idje4bOWpKkdxlcS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1331&quot; height=&quot;1057&quot; data-origin-width=&quot;1331&quot; data-origin-height=&quot;1057&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 시퀀스를 지원하지 않는 DBMS를 사용한다면 테이블 전략을 사용하면 유사한 효과를 누릴 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시퀀스가 낭비되는 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스 전략은 성능적으로 많이 향상되었지만 불필요하게 시퀀스가 낭비될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 현재 시퀀스로 100개를 채번했고, 80개 정도가 잔여한 상황에서 신규 코드 배포를 위해 애플리케이션을 셧다운했다고 가정하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 해당 애플리케이션에서 할당받은 80개의 시퀀스가 낭비된다. DB에는 기록되었지만 최종적으로 사용하지 않게 되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 카드번호는 카드사에 특정한 풀만큼 할당되기에 시퀀스의 낭비는 매우 비효율적인 요소일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럴 때는 &lt;code&gt;IdentifierGenerator&lt;/code&gt;를 구현해서 새로운 Custom Generator를 만들면 된다. 아래는 그 예시이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 애플리케이션 종료시 잔여 시퀀스 보관&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 &lt;code&gt;SequenceGenerator&lt;/code&gt; 동작과 더불어 아래처럼 동작하는 &lt;code&gt;IdentifierGenerator&lt;/code&gt;를 만든다면 시퀀스 낭비 문제를 해결할 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;애플리케이션이 종료될 때 잔여한 시퀀스를 레디스에 저장한다.&lt;/li&gt;
&lt;li&gt;시퀀스를 꺼낼 떄 레디스에 저장된 시퀀스가 있다면 그것을 먼저 확보한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 시퀀스 저장소로 레디스 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Hibernate&lt;/code&gt;의 &lt;code&gt;SequenceGenerator&lt;/code&gt;는 기본적으로 애플리케이션 메모리에 시퀀스를 보관하지만, Redis에 시퀀스를 보관하면 애플리케이션이 재기동되면서 시퀀스가 낭비되는 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 시퀀스는 특성상 중복이 발생하면 치명적인 문제가 생길 가능성이 크기에 Redis의 일관성 모델을 이해하고 구현해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가로 해결 가능한 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 기존 시퀀스 문제에서는 고려해볼 점이 한 가지 더 있는데, 실제 시퀀스를 확보하는 시점에서는 지연이 생길 수도 있는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 부분은 무시해도 크게 부담되진 않겠지만, 주기적으로 일정 부분 이상 소요되면 시퀀스를 확보하는 전략을 추가로 고려해볼 수도 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UUID 기반 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 고유한 ID 식별을 위한 경우처럼 순차적인 값을 보장하지 않아도 되는 경우라면 &lt;code&gt;UUID&lt;/code&gt; 값을 사용하는 것도 추천한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
public class Order {
    @Id
    @GeneratedValue(generator = &quot;uuid2&quot;)
    @GenericGenerator(name = &quot;uuid2&quot;, strategy = &quot;uuid2&quot;)
    @Column(columnDefinition = &quot;binary(16)&quot;)
    private UUID id;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID를 사용하면 분산 시스템에서도 충돌이나 성능저하 없이 시퀀스를 생성할 수 있지만 공간을 더 많이 차지한다는 단점이 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://vladmihalcea.com/hibernate-hidden-gem-the-pooled-lo-optimizer/&quot;&gt;https://vladmihalcea.com/hibernate-hidden-gem-the-pooled-lo-optimizer/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devcamus.tistory.com/16&quot;&gt;https://devcamus.tistory.com/16&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://techblog.woowahan.com/2663&quot;&gt;https://techblog.woowahan.com/2663&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Server/Spring JPA</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/441</guid>
      <comments>https://jaehoney.tistory.com/441#entry441comment</comments>
      <pubDate>Sun, 21 Sep 2025 19:29:13 +0900</pubDate>
    </item>
    <item>
      <title>Hibernate - 맵핑 타입 최적화</title>
      <link>https://jaehoney.tistory.com/440</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&amp;nbsp;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 번에 게시글로도 작성했었지만, DB에서 &lt;code&gt;Status&lt;/code&gt;와 같은 성격을 띄는 컬럼에 대해서 다뤄본적이 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;VARCHAR로 관리할 지 vs Enum vs Tinyint, ...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 Hibernate를 활용한 새로운 접근(?)을 알게 되었고 꽤 인상깊어서 공유하고자 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hibernate - Mapping Construct&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA, Hibernate에는 세 가지 매핑 구조가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Basic types (Integer, Long, String, CustomStatus)&lt;/li&gt;
&lt;li&gt;Embeddable types (@Embeddable 애노테이션처럼 여러 컬럼을 묶은 것)&lt;/li&gt;
&lt;li&gt;Entity types (테이블과 맵핑)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맵핑할 타입이 더 컴팩트할 수록 더 높은 성능을 낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령, Status라는 Enum이 있다면 String보다 Ordinal을 사용한다면 더 적은 컬럼 타입을 사용하여 이점을 누릴 수 있고, 반면 DB를 봤을 때 직관적이지 않다는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 보통 String을 선택하게 된다. 당연히 테이블, 인덱스 크기가 커지고 각 쿼리의 크기도 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소개할 방법은 &lt;code&gt;Status&lt;/code&gt;와 같은 테이블을 하나 만들면 된다. 아래 테이블과 맵핑한 엔터티를 보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity(name = &quot;PostStatusInfo&quot;)
@Table(name = &quot;post_status_info&quot;)
public class PostStatusInfo {
    @Id
    @Column(columnDefinition = &quot;tinyint&quot;)
    private Integer id;
    private String name;
    private String description;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 엔터티는 id로 Enum의 Ordinal Value를 저장하게 된다. 아래는 해당 상태를 실제로 사용하는 엔터티이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity(name = &quot;Post&quot;)
@Table(name = &quot;post&quot;)
public class Post {

    @Id
    private Long id;

    @Enumerated(EnumType.ORDINAL)
    @Column(columnDefinition = &quot;tinyint&quot;)
    private PostStatus status;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;status&quot;, insertable = false, updatable = false)
    private PostStatusInfo statusInfo;

    private String title;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하는 테이블에서는 Ordinal로 맵핑을 시킨다. &lt;code&gt;PostStatusInfo&lt;/code&gt;를 맵핑할 테이블에 설명을 저장한다면 테이블을 맵핑할 때 드는 비용을 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;description 정보가 필요할 때는 &lt;code&gt;PostStatusInfo&lt;/code&gt;를 조회하면 된다. 성능이 매우 중요한 환경에서는 이러한 기술을 사용한다면 이점이 있을 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.inflearn.com/courses/lecture?courseId=336847&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.inflearn.com/courses/lecture?courseId=336847&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Server/Spring JPA</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/440</guid>
      <comments>https://jaehoney.tistory.com/440#entry440comment</comments>
      <pubDate>Sun, 31 Aug 2025 21:01:35 +0900</pubDate>
    </item>
    <item>
      <title>Kotlin - 제너릭의 변성(Variance) 이해하기!</title>
      <link>https://jaehoney.tistory.com/439</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&amp;nbsp;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바나 코틀린에서 라이브러리를 개발할 때 제너릭 부분에서 컴파일 에러가 자주 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러한 문제를 효과적으로 해결하려면 &lt;b&gt;제너릭&lt;/b&gt;의 주요 개념 중 하나인 &lt;b&gt;변성(Variance)&lt;/b&gt;을 이해해야 한다. 관련해서 막연한 단어가 많지만, 예시를 들어 &lt;b&gt;이해가 가능하도록 포스팅 내용을 작성&lt;/b&gt;했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;변성의 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제너릭에서는 &lt;code&gt;List&amp;lt;Int&amp;gt;&lt;/code&gt; 타입이 있으면 &lt;code&gt;List&lt;/code&gt;를 기저 타입(Base Type), &lt;code&gt;Int&lt;/code&gt;를 타입 인자(Type argument)라고 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변성(Variance)은 기저 타입(Base type)이 같으면서 &lt;b&gt;타입 인자(Type argument)가 다른 경우&lt;/b&gt; 두 타입이 &lt;b&gt;서로 어떤 관계&lt;/b&gt;인지를 정의한 것이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m7RZ3/btsMkAfU0gj/0CK9Wf9enKe7EqB4Xx2XTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m7RZ3/btsMkAfU0gj/0CK9Wf9enKe7EqB4Xx2XTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m7RZ3/btsMkAfU0gj/0CK9Wf9enKe7EqB4Xx2XTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm7RZ3%2FbtsMkAfU0gj%2F0CK9Wf9enKe7EqB4Xx2XTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;460&quot; height=&quot;90&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;90&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서 &lt;code&gt;SomeClass&amp;lt;Int&amp;gt;&lt;/code&gt;가 &lt;code&gt;SomeClass&amp;lt;Number&amp;gt;&lt;/code&gt;의 하위 타입이라고 생각하기 쉽다. 그러나 그것은 개발자가 &lt;code&gt;SomeClass&lt;/code&gt;에 대한 변성을 어떻게 지정하느냐에 달린 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자는 변성을 적절하게 활용하면 확장성과 타입 안정성을 동시에 챙길 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서브 타입(Sub Type)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변성을 이해하기 앞서 &lt;b&gt;서브 타입(하위 타입)&lt;/b&gt;을 이해해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보자. 제너릭에 대해서 조금 이해한다면 &lt;code&gt;SomeClass&amp;lt;Int&amp;gt;&lt;/code&gt; 타입과 &lt;code&gt;SomeClass&amp;lt;Number&amp;gt;&lt;/code&gt; 타입은 엄연히 다른 것을 알 것이다. 타입에 의한 컴파일 에러가 발생할까?&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    val numbers: List&amp;lt;Int&amp;gt; = listOf(1, 2, 3)
    printList(numbers) // 컴파일 에러 X
}

private fun printList(list: List&amp;lt;Any&amp;gt;) {
    println(list.joinToString())
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 컴파일 에러가 발생하지 않는다. 코틀린에서 &lt;code&gt;List&amp;lt;Int&amp;gt;&lt;/code&gt;는 &lt;code&gt;List&amp;lt;Any&amp;gt;&lt;/code&gt;의 하위 타입이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    val numbers: MutableList&amp;lt;Int&amp;gt; = mutableListOf(1, 2, 3) // MutableList&amp;lt;Int&amp;gt;
    printList(numbers) // 컴파일 에러 O
}

private fun printList(list: MutableList&amp;lt;Any&amp;gt;) {
    println(list.joinToString())
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;List&lt;/code&gt;를 &lt;code&gt;MutableList&lt;/code&gt;로 바꿨을 뿐인데 컴파일 에러가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이런 차이가 발생할까?&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;서브 타입의 정의&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 타입의 모든 기능을 B 타입이 수행할 수 있을 때 B타입을 A타입의 &lt;b&gt;서브타입(하위 타입)&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;List&amp;lt;Int&amp;gt;&lt;/code&gt;는 &lt;code&gt;List&amp;lt;Any&amp;gt;&lt;/code&gt;의 모든 기능을 수행할 수 있다. 즉, &lt;code&gt;List&amp;lt;Int&amp;gt;&lt;/code&gt;는 &lt;code&gt;List&amp;lt;Any&amp;gt;&lt;/code&gt;의 하위 타입이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MutableList&amp;lt;Int&amp;gt;&lt;/code&gt;는 &lt;code&gt;MutableList&amp;lt;Any&amp;gt;&lt;/code&gt;의 모든 기능을 수행할 수 있을까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;MutableList&amp;lt;Any&amp;gt;&lt;/code&gt;는 &lt;code&gt;list.add(&quot;A&quot;)&lt;/code&gt;를 수행할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MutableList&amp;lt;Int&amp;gt;&lt;/code&gt;는 &lt;code&gt;list.add(&quot;A&quot;)&lt;/code&gt;를 수행할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시처럼 가변성이 있는 &lt;code&gt;MutableList&amp;lt;Int&amp;gt;&lt;/code&gt;는 &lt;code&gt;MutableList&amp;lt;Any&amp;gt;&lt;/code&gt;의 하위 타입이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 이 내용은 개념적인 부분이이다. 아래에서는 변성의 세 가지 유형과 함께 &lt;b&gt;코드&lt;/b&gt;로 변성을 지정하는 것을 예시로 보여준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;변성의 세 가지 유형&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변성에는 세 가지 유형이 있다. 공변성, 반공변성, 무공변성이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;out T&amp;gt;&lt;/code&gt;: 공변(Covariance)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;in T&lt;/code&gt;&amp;gt;: 반공변(Contravariance)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;: 무공변(invariance)&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 공변(Covariance)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;공변(Covariance)&lt;/b&gt;은 &lt;code&gt;Int&lt;/code&gt;, &lt;code&gt;Number&lt;/code&gt;처럼 A 타입이 B 타입의 하위 타입이고, &lt;code&gt;Covariance&amp;lt;Int&amp;gt;&lt;/code&gt;도 &lt;code&gt;Covariance&amp;lt;Number&amp;gt;&lt;/code&gt;의 하위 타입이 되는 경우를 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 &lt;code&gt;out&lt;/code&gt; 키워드를 사용해서 제너릭의 변성을 &lt;b&gt;공변&lt;/b&gt;으로 지정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class Covariance&amp;lt;out T: Number&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 예시에서 본 &lt;code&gt;List&lt;/code&gt; 클래스가 공변에 해당한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;695&quot; data-origin-height=&quot;161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2Khn0/btsMkcGAJuc/PeAv2DaruaWJ9nkn7oUni0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2Khn0/btsMkcGAJuc/PeAv2DaruaWJ9nkn7oUni0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2Khn0/btsMkcGAJuc/PeAv2DaruaWJ9nkn7oUni0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2Khn0%2FbtsMkcGAJuc%2FPeAv2DaruaWJ9nkn7oUni0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;695&quot; height=&quot;161&quot; data-origin-width=&quot;695&quot; data-origin-height=&quot;161&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;List&amp;lt;Any&amp;gt;&lt;/code&gt; 파라미터의 인자로 &lt;code&gt;List&amp;lt;Int&amp;gt;&lt;/code&gt;가 전달될 수 있었다. 공변을 활용하면 &lt;b&gt;읽기(Read)&lt;/b&gt;를 위한 제너릭 타입이 &lt;b&gt;타입 인자 간 관계와 동일한 관계&lt;/b&gt;를 가지는 게 가능해진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 무공변(Invariance)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변성을 지정하지 않으면 기본적으로 &lt;b&gt;무공변(Invariance)&lt;/b&gt; 상태가 된다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class Invariance&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MutableList&lt;/code&gt;는 무공변에 해당한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/om59C/btsMjK4P5gm/BHYxnaTtGKeesjwZjJ19iK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/om59C/btsMjK4P5gm/BHYxnaTtGKeesjwZjJ19iK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/om59C/btsMjK4P5gm/BHYxnaTtGKeesjwZjJ19iK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fom59C%2FbtsMjK4P5gm%2FBHYxnaTtGKeesjwZjJ19iK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;133&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;133&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;MutableList&amp;lt;Any&amp;gt;&lt;/code&gt; 파라미터의 인자로 &lt;code&gt;MutableList&amp;lt;Int&amp;gt;&lt;/code&gt;가 전달될 수 없었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 반공변(Contravariance)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;반공변(Contravariance)&lt;/b&gt;은 공변과 반대이다. &lt;code&gt;Int&lt;/code&gt;, &lt;code&gt;Number&lt;/code&gt;처럼 A 타입이 B 타입의 하위 타입이지만, &lt;code&gt;Contravariance&amp;lt;Int&amp;gt;&lt;/code&gt;가 &lt;code&gt;Contravariance&amp;lt;Number&amp;gt;&lt;/code&gt;의 상위 타입이 되는 경우를 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 &lt;code&gt;in&lt;/code&gt; 키워드를 사용해서 제너릭의 변성을 &lt;b&gt;반공변&lt;/b&gt;으로 지정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class Contravariance&amp;lt;in T&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 예시는 &lt;code&gt;Comparable&lt;/code&gt; 클래스이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;322&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLgwzX/btsMjSn3bXJ/LdMEdqGmrUFGrzq3ICQ2HK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLgwzX/btsMjSn3bXJ/LdMEdqGmrUFGrzq3ICQ2HK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLgwzX/btsMjSn3bXJ/LdMEdqGmrUFGrzq3ICQ2HK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLgwzX%2FbtsMjSn3bXJ%2FLdMEdqGmrUFGrzq3ICQ2HK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;322&quot; height=&quot;84&quot; data-origin-width=&quot;322&quot; data-origin-height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Comparable&amp;lt;Long&amp;gt;&lt;/code&gt;의 모든 기능은 &lt;code&gt;Comparable&amp;lt;Any&amp;gt;&lt;/code&gt;가 수행할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Comparable&amp;lt;Long&amp;gt;&lt;/code&gt;은&amp;nbsp;&lt;code&gt;compareTo(1)&lt;/code&gt;은 수행할 수 있지만, &lt;code&gt;compareTo(&quot;A&quot;)&lt;/code&gt;는 수행할 수 없다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Comparable&amp;lt;Any&amp;gt;&lt;/code&gt;는 &lt;code&gt;compareTo(1)&lt;/code&gt;도 수행할 수 있지만, &lt;code&gt;compareTo(&quot;A&quot;)&lt;/code&gt;도 수행할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반공변이 왜 필요한 지 이해해보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class AnimalComparable: Comparable&amp;lt;Animal&amp;gt; {
    override fun compareTo(other: Animal): Int {
        return 0
    }
}

// 반공변이므로, Dog에도 사용 가능
val dogComparable: Comparable&amp;lt;Dog&amp;gt; = AnimalComparable()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Comparable&amp;lt;Dog&amp;gt;&lt;/code&gt; 타입이 필요한 상황에서 &lt;code&gt;Comparable&amp;lt;Animal&amp;gt;&lt;/code&gt; 타입의 객체를 대신 사용할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결국 목적은 다형성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍 언어는 타입 안정성을 위해 제너릭을 지원한다. 다만, 무공변만으로는 다형성을 활용한 유연한 인터페이스를 만들기 어렵다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;List&amp;lt;Number&amp;gt;&lt;/code&gt; 필드에 &lt;code&gt;List&amp;lt;Int&amp;gt;&lt;/code&gt;를 할당할 수 없었다면 &lt;b&gt;유연성&lt;/b&gt;을 잃어버렸을 것&lt;/li&gt;
&lt;li&gt;&lt;code&gt;List&amp;lt;*&amp;gt;&lt;/code&gt;을 사용하면 &lt;code&gt;Number&lt;/code&gt;를 꺼내서 사용할 수 없어서, &lt;b&gt;as&lt;/b&gt;와 같은 키워드의 사용으로 &lt;b&gt;타입 안정성&lt;/b&gt;을 잃어버리게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 예시의 코드를 잘 보면 아래의 사실을 확인할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;공변(out T)&lt;/b&gt;은 메서드에서 T 타입의 파라미터가 없고, T 타입을 반환할 경우 사용할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;List, Set, Map, ...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;반공변(in T)&lt;/b&gt;은 메서드에서 T 타입의 파라미터가 있고, T 타입을 반환하지 않을 경우 사용할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Comparable, Comparator, Consumer, ...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기(Read)를 위한 클래스를 &lt;b&gt;공변&lt;/b&gt;으로 지정하면&lt;b&gt; 제너릭 타입에서도&lt;/b&gt; T 타입의 &lt;b&gt;다형성을 이어받아서&lt;/b&gt; 상위, 하위 타입으로 사용할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 및 연산(Write)을 위한 클래스를 &lt;b&gt;반공변&lt;/b&gt;으로 지정하면&lt;b&gt; T 타입의 상위 타입을 타입 인자로 가지는 제너릭 타입&lt;/b&gt;을 &lt;b&gt;하위 타입&lt;/b&gt;으로 사용할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변성을 잘 알고 사용한다면 넓은 의미의 인터페이스(Interface, Class, ...)를 안전하고, 유연하게 만들 수 있을 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://jaeyeong951.medium.com/kotlin-in-n-out-variance-%EB%B3%80%EC%84%B1-69204cbf27a1&quot;&gt;https://jaeyeong951.medium.com/kotlin-in-n-out-variance-변성-69204cbf27a1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.yes24.com/product/goods/55148593&quot;&gt;https://www.yes24.com/product/goods/55148593&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Language/Kotlin</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/439</guid>
      <comments>https://jaehoney.tistory.com/439#entry439comment</comments>
      <pubDate>Sat, 15 Feb 2025 23:40:40 +0900</pubDate>
    </item>
    <item>
      <title>로컬캐시 이해하기! (feat. 글로벌 캐시)</title>
      <link>https://jaehoney.tistory.com/438</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&amp;nbsp;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfl0mn/btsL3lEp5lV/wCqNhvnHTYNoE6GE6LYQ0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfl0mn/btsL3lEp5lV/wCqNhvnHTYNoE6GE6LYQ0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfl0mn/btsL3lEp5lV/wCqNhvnHTYNoE6GE6LYQ0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcfl0mn%2FbtsL3lEp5lV%2FwCqNhvnHTYNoE6GE6LYQ0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;930&quot; height=&quot;245&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;내가 속한 팀원이 사내 기술 블로그에 로컬 캐시 관련 포스팅을 했고, 기술 리뷰어로 나를 지정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원의 성장과 내용의 질을 위해 캐시와 관련된 학습을 하게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 로컬 캐시 vs 글로벌 캐시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 캐시는 조회에서의 &lt;b&gt;성능 향상&lt;/b&gt;과 &lt;b&gt;부하 방지&lt;/b&gt;를 위해 사용한다. 다수의 요청에서 연산을 위한 리소스나 DB 조회를 할 때 드는 비용 등을 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로컬 캐시&lt;/b&gt;는 각 서버의 메모리에 데이터를 저장하는 방식을 말한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션에서 &lt;code&gt;Map&lt;/code&gt;과 같은 자료구조에 저장하는 경우&lt;/li&gt;
&lt;li&gt;EhCache 라이브러리를 사용하는 경우&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 캐시의 특징은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;속도가 매우 빠르다.&lt;/li&gt;
&lt;li&gt;외부 인프라와 통신이 필요없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비용이 저렴하다.&lt;/li&gt;
&lt;li&gt;외부 통신의 리스크도 사라진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;분산 환경에서는 각 서버간 다른 데이터가 저장될 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동기화가 어렵다.&lt;/li&gt;
&lt;li&gt;데이터가 중복해서 저장될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;글로벌 캐시&lt;/b&gt;는 캐시 서버를 따로 분리하여 캐시 결과를 저장한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis, Memcached 등 외부 인프라에 데이터를 저장하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글로벌 캐시의 특징은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 통신을 하므로 속도가 로컬 캐시보다는 느리다. (일반적인 DB 보다는 훨씬 빠르다.)&lt;/li&gt;
&lt;li&gt;각 서버에서 동일한 데이터를 조회할 수 있다.&lt;/li&gt;
&lt;li&gt;전역적으로 데이터를 저장하므로 공간 효율이 비교적 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 캐시와 글로벌 캐시는 용도에 맞게 사용하는 것이 중요하다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;용도에 맞게 선택해보기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 유튜브 서비스를 예로 들어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국가의 목록을 조회할 때는 &lt;b&gt;로컬 캐시&lt;/b&gt;를 사용할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개수가 한정적이다. (각 서버에서 모든 데이터를 보관해도 문제가 없다.)&lt;/li&gt;
&lt;li&gt;변경이 잦지 않다.&lt;/li&gt;
&lt;li&gt;변경이 일어나도, 정확히 실시간 동기화가 반드시 필요하지는 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 유저의 정보를 조회하기 위해서는 &lt;b&gt;글로벌 캐시&lt;/b&gt;를 사용할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 로컬 캐시에 모든 데이터를 보관하기에는 공간이 부족하다.&lt;/li&gt;
&lt;li&gt;실시간 동기화가 필요하다. (최신화된 유저 정보를 조회할 수 있어야 한다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 로컬 캐시 + 글로벌 캐시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 서비스에서 특정 상황에서는 로컬 캐시가 적합하고, 다른 상황에서는 글로벌 캐시가 적합하다면 어떻게 하면 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘다 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 예로 들면 아래 기술들을 조합하면 로컬 캐시 + 글로벌 캐시를 사용할 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;spring-cache&lt;/li&gt;
&lt;li&gt;ehcache&lt;/li&gt;
&lt;li&gt;spring-data-redis&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는 spring-cache 프레임워크를 사용해서 캐시 데이터를 관리할 수 있다. 각 캐시 구현 기술을 구현하는 CacheManager를 빈으로 등록한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// Redis cache manager 빈 등록
@Bean(name = &quot;redisCacheManager&quot;)
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
  return RedisCacheManager.builder(connectionFactory).build();
}

// EhCache manager 빈 등록
@Bean(name = &quot;ehCacheManager&quot;)
public CacheManager cacheManager() {
  return new EhCacheManager();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 spring-cache에서 지원하는 애노테이션의 &lt;code&gt;cacheManager&lt;/code&gt; 속성에 사용할 CacheManager의 빈 이름을 명시해주면 된다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@Cacheable(key = &quot;'user:'+#id&quot;, cacheManager = &quot;redisCacheManager&quot;)
public User find(Integer id){
  return new User(id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 로컬캐시에서 실시간성 보장하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 언급했듯 분산 환경에서 로컬캐시를 사용한다면 각 서버에서 다른 데이터를 사용하는 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 캐시를 사용한다면 완벽한 실시간을 보장하려는 것은 사실상 불가능이지만, 준실시간을 보장할 수 있는 방법은 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 인기 유튜브 영상이 비공개로 변경되었다면, 해당 게시글을 노출하지 말아야 한다. 해당과 같은 요구사항에서 아래의 판단을 할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬캐시를 사용한다면 변경이 일어난 서버에서 각 서버에 전파가 필요할 것이다.&lt;/li&gt;
&lt;li&gt;모든 서버에서 완벽한 실시간을 보장할 필요까지는 없고, 준실시간 정도면 괜찮을 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 메시징 시스템을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 서버에서 메시징 시스템으로 이벤트를 전파하고, 다른 서버에서 이를 구독한다면 준실시간을 보장할 수 있다. 이러한 모델을 최종적 일관성(Eventual Consistency) 모델이라고 표현한다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;최종적 일관성(Eventual Consistency): 각 서버 간 데이터의 일시적 불일치를 허용하지만, 최종적으로는 모든 서버의 데이터 일관성이 맞춰지는 것&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@bonjugi/Cacheable-EhCache%EC%99%80-RedisCache-%EB%91%98%EB%8B%A4-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-CacheManager&quot;&gt;https://velog.io/@bonjugi/Cacheable-EhCache와-RedisCache-둘다-사용하기-CacheManager&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kk-programming.tistory.com/83&quot;&gt;https://kk-programming.tistory.com/83&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.kakaopay.com/post/local-caching-in-distributed-systems&quot;&gt;https://tech.kakaopay.com/post/local-caching-in-distributed-systems&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Operation/System Architecture</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/438</guid>
      <comments>https://jaehoney.tistory.com/438#entry438comment</comments>
      <pubDate>Fri, 31 Jan 2025 22:01:09 +0900</pubDate>
    </item>
    <item>
      <title>2024년 늦은 회고와 반성..? (feat. 해외 출장)</title>
      <link>https://jaehoney.tistory.com/437</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;425&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DMWM2/btsLSOeLOEi/c9ojlXAnVioRtb9AuKtax0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DMWM2/btsLSOeLOEi/c9ojlXAnVioRtb9AuKtax0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DMWM2/btsLSOeLOEi/c9ojlXAnVioRtb9AuKtax0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDMWM2%2FbtsLSOeLOEi%2Fc9ojlXAnVioRtb9AuKtax0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;504&quot; height=&quot;425&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;425&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;다들 2024년 회고를 하나씩 블로그에 포스팅하기에 더 늦기 전에 올려야겠다라는 생각이 들어 작성하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년은 가장 정신없게 보낸 한 해였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 블로그 작성에 소홀해진 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주변에서 블로그 작성에 너무 소홀해진 것 아니냐는 얘기를 종종 듣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사정을 이야기하자면 아래와 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;업무가 너무 많고 바빠졌다.&lt;/li&gt;
&lt;li&gt;여가 시간이 많이 줄어들었다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 보통 근무했던 시간은 08:00 ~ 19:00 이다. 근무 시간도 그렇고, 근무 일수도 4.5일에서 늘어났다. 디테일하게 다른 이유도 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;업무가 많은 편이고, 근무 시간도 많음&lt;/li&gt;
&lt;li&gt;B2C 서비스를 하고 있고, 외부 제휴사가 많은 부서라서 주말에도 장애에 민감&lt;/li&gt;
&lt;li&gt;실적 압박이 있는 편이고, 업무 관리가 굉장히 타이트한 편&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근무 시간이 많은 이유는 정말로 일이 많았다. 근무 시간도 많았지만, 커피를 마실 여유나 사내 게시판 등을 돌아볼 시간도 잘 없어 자리에서 일하거나 회의한 시간이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평일에는 집에 오면 씻고 바로 잠에 들었고, 주말에는 업무 로드로 인해 블로그까지 포스팅하기 쉽지 않았던 것 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 회사 얘기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 2024년에 무엇을 했나 돌이켜보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. 카드 결제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 합류한 팀에서는 카드 페이먼츠를 담당하고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이머니체크카드 개발&lt;/li&gt;
&lt;li&gt;머니 MST / 제로페이&lt;/li&gt;
&lt;li&gt;비가맹 결제&lt;/li&gt;
&lt;li&gt;트래블로그&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입사 후 전사적으로 2024년 가장 기대를 받는 프로젝트에 투입되었다.(입사 후 첫 프로젝트이기도 했다). 성과나 매출이 생각했던 것보다는 저조했지만, 확장성을 생각하면 나쁘지 않은 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카드 페이먼츠라는 도메인에 대해 자세히 알게되었기도 하다. (연말정산, 분리보관 등도 자세히 알게되었다). 결제 관련 도메인은 생각보다 복잡해서 구성원들이 도메인 전문가를 선호하는 이유를 알 것 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2. 해외 출장&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래블로그 연동을 하게 되면서 해외 결제 필드 테스트를 위해 후쿠오카 출장을 다녀왔다. 해당 시스템의 서버 개발이 내 담당이기도 했고, 결제쪽을 개발하기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 출장가는 것은 처음이었는데, 해외 출장이라서 한번 경험해보고 싶은 마음으로 가게 되었다. 생각보다 재밌었고, 많이 느낀 계기가 된 것 같다. 사진도 많이 찍어서 나중에 돌아보게 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해외 출장은 마스터카드, 하나카드, 카카오페이 각 업체의 담당자들이 모여서 10명 정도로 갔는데, 하나카드 측에서 자그마한 선물(트래블로그가 그려진 과잠, Contactless Payment 팔찌)도 주셨다. 그걸 보면 자꾸 생각나고, '내가 책임져야하는 프로젝트구나'라는 프로젝트에 대한 애착을 가지게 되기도 하는 것 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-3. 기타&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 생각해도 놀라운 점 중 하나는 팀에서 인당 1개 이상의 프로젝트를 맡고 있다. 그런데 프로젝트 1개당 볼륨이 장난이 아니다. (말도 안되는 수준인 것 같다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀에서는 애자일로 업무 관리를 하고 있는데, 개인당 퍼포먼스가 엄청난 것 같다. 단점은 힘들다. (진심)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문화데이(워크샵)를 몇번 챙겼는데, 팀원 분들이 만족도가 높으셔서 개인적으로 뿌듯했다. 맛집을 찾는 거나 계획을 짜는 것도 어느 정도 노하우가 생긴 것 같다.. ㅋㅋ&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 일상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 일에만 집중하고, 사소한 고민들이 조금 많았던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘은 심신이 불안정하다는 생각이 조금씩 들어서, 의식을 하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코스트코에서 인바디를 충동적으로 구매했는데, 2024년에 산 아이템 중에 가장 잘 산 것 같다. 건강 지표를 알 수 있으니 더 위기감을 느끼고 그 위기감이 액션으로 이어진다. 정신은 그냥 긍정적으로 생각하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재태크도 공부를 조금씩 하고 있지만, 블로그에서 다루는 것은 어려울 것 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2025년 목표&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 커리어 향상을 위한 결과를 내보기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연차가 올라가면 최소한 아래 두 가지 중 1가지는 보는 것 같다. (정말 좋은 곳은 둘 다 보는 것 같다.)&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;도메인 지식이 얼마나 방대한 지&lt;/li&gt;
&lt;li&gt;아웃풋이 얼마나 있는 지&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아웃풋은 사내 기술 블로그나, 컨퍼런스 발표, 외부 강의 등을 표현한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 글을 잘쓰시고, 정리를 잘하시는 탁월한 분들은 꼭 아웃풋을 내지 않더라도, 다른 곳에서 모셔갈 수 있는 것 같다. 난 그런 류(?)는 아니니까 활동이 조금 필요할 것 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 영어 공부&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자로 만 3년차가 되었다. (햇수로는 5년차다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연차가 쌓일수록 영어가 더 많이 중요해지는 것 같다. 문서 및 강연을 포함한 다양한 자료가 영어로 되어 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해에는 영어를 잘해보고자 휴대폰 설정, 맥북의 언어 설정을 모두 영어로 바꿔놔서 조금 불편하지만 적응해보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 건강 관리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평일에 헬스장을 가기는 어려운 상황이라서 집에서 하려고 풀업 &amp;amp; 스쿼트를 하고 싶었는데, 치닝디핑을 놓을 곳이 없어서 푸시업 &amp;amp; 스쿼트로 열심히 해봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;러닝을 다시 하고 싶다는 생각도 드는데, 무산소 운동도 해보고 싶고 발목도 다쳤기에 러닝은 쉽지 않을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긍정적인&amp;nbsp;생각을&amp;nbsp;위해&amp;nbsp;책도&amp;nbsp;자주&amp;nbsp;읽어야겠다.&amp;nbsp;2024년에는&amp;nbsp;종이&amp;nbsp;책은&amp;nbsp;거의(?)&amp;nbsp;안보고,&amp;nbsp;함께자라기&amp;nbsp;1권&amp;nbsp;읽은&amp;nbsp;것&amp;nbsp;같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 심기체를 다져보자.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Etc./개발 일기</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/437</guid>
      <comments>https://jaehoney.tistory.com/437#entry437comment</comments>
      <pubDate>Sun, 19 Jan 2025 20:41:38 +0900</pubDate>
    </item>
    <item>
      <title>코루틴에서 @Transactional을 사용하는 방법!</title>
      <link>https://jaehoney.tistory.com/436</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&amp;nbsp;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴에서 트랜잭션을 적용하는 데 어려움을 겪는 케이스가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 + JPA 환경에서 &lt;code&gt;@Transactional&lt;/code&gt; 애노테이션이 미동작했고, 해당 부분으로 인해 알게된 내용을 공유한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring I/O 2024에서 언급하는 내용도 참고했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 - 트랜잭션 관리 방식&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1199&quot; data-origin-height=&quot;369&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ud2nJ/btsKk0BiPkz/5XrOBBeTCrCc8MYiE6S8d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ud2nJ/btsKk0BiPkz/5XrOBBeTCrCc8MYiE6S8d0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ud2nJ/btsKk0BiPkz/5XrOBBeTCrCc8MYiE6S8d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUd2nJ%2FbtsKk0BiPkz%2F5XrOBBeTCrCc8MYiE6S8d0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1199&quot; height=&quot;369&quot; data-origin-width=&quot;1199&quot; data-origin-height=&quot;369&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 지원하는 트랜잭션 방식에 따라 크게 2가지로 방식이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선언적 트랜잭션&lt;/li&gt;
&lt;li&gt;프로그래밍적 트랜잭션&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선언적 트랜잭션(Declarative Transaction)이란 &lt;code&gt;@Transactional&lt;/code&gt;과 같은 애노테이션을 기반으로 트랜잭션을 처리하는 방식을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍적 트랜잭션은(Programmatic Transaction)은 실제 로직에서 트랜잭션을 수행하는 방식을 말한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;선언적 트랜잭션 vs 프로그래밍 트랜잭션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/tx-decl-vs-prog.html&quot;&gt;공식문서&lt;/a&gt;를 보면 아래와 같이 명시되어있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1231&quot; data-origin-height=&quot;447&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbLptG/btsKkoWLDPY/0MKXjOKacbOkMkHDMBXqB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbLptG/btsKkoWLDPY/0MKXjOKacbOkMkHDMBXqB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbLptG/btsKkoWLDPY/0MKXjOKacbOkMkHDMBXqB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbLptG%2FbtsKkoWLDPY%2F0MKXjOKacbOkMkHDMBXqB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1231&quot; height=&quot;447&quot; data-origin-width=&quot;1231&quot; data-origin-height=&quot;447&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 부분을 요약하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로그래밍적 트랜잭션&lt;/b&gt;은 트랜잭션 수가 적을 때에 한해서 좋다. 트랜잭션을 사용하는 곳이 적다면, 사용을 위해 스프링이나 프록시와 같은 기술을 신경쓰지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, 트랜잭션 작업이 많은 경우 &lt;b&gt;선언적 트랜잭션&lt;/b&gt;이 좋다. 선언적 트랜잭션 관리는 비즈니스 로직에 집중할 수 있게 도와주고 트랜잭션 처리가 비교적 간단하기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JPA와 코루틴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring JPA와 코루틴을 사용할 때 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나씩 알아보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 트랜잭션 미동작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 코드에서 Repository는 JpaRepository를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;interface OrderRepository : JpaRepository&amp;lt;Order, Long&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Service 클래스이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class OrderService(
    private val orderRepository: OrderRepository,
) {
    @Transactional
    suspend fun submit(
        id: Long,
        throwException: Boolean = false,
    ) {
        val order = orderRepository.findById(id).get()
        order.submit()
        orderRepository.save(order)
        if (throwException) {
            throw IllegalStateException(&quot;테스트 위한 에러&quot;)
        }
    }

    @Transactional
    fun submitNotSuspend(
        id: Long,
        throwException: Boolean = false,
    ) {
        val order = orderRepository.findById(id).get()
        order.submit()
        orderRepository.save(order)
        if (throwException) {
            throw IllegalStateException(&quot;테스트 위한 에러&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;throwException 파라미터에 true가 들어오면 예외를 발생할 것이고, 지난 변경사항을 롤백할 것이다. 2개 메서드는 동일하고 1개는 suspend 함수이고 나머지 1개는 일반 메서드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 데이터 Write가 정상적으로 수행되는 지와 예외가 발생했을 때 롤백이 되는 지에 대한 테스트 코드이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@SpringBootTest
class OrderServiceTest {
    @Autowired
    private lateinit var orderService: OrderService

    @Autowired
    private lateinit var orderRepository: OrderRepository

    @Test
    fun `정상 저장 테스트`() {
        // given
        val order =
            orderRepository.save(
                Order(
                    id = 1L,
                    status = OrderStatus.READY,
                ),
            )

        // when
        runBlocking(Dispatchers.IO) {
            orderService.submit(order.id)
        }

        // then
        val result = orderRepository.findById(order.id).get()
        assertThat(result.status).isEqualTo(OrderStatus.SUBMITTED)
    }

    @Test
    fun `롤백 동작 테스트`() {
        // given
        val order =
            orderRepository.save(
                Order(
                    id = 1L,
                    status = OrderStatus.READY,
                ),
            )

        // when
        runCatching {
            runBlocking(Dispatchers.IO) {
                orderService.submit(order.id, true)
            }
        }

        // then
        val result = orderRepository.findById(order.id).get()
        assertThat(result.status).isEqualTo(OrderStatus.READY)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 실행해보면 롤백 동작 테스트가 깨진다. Exception이 발생해도 롤백이 되지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBkGi7/btsKkqUCnRx/ojRZ8Xjk5utx36On9p0oF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBkGi7/btsKkqUCnRx/ojRZ8Xjk5utx36On9p0oF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBkGi7/btsKkqUCnRx/ojRZ8Xjk5utx36On9p0oF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBkGi7%2FbtsKkqUCnRx%2FojRZ8Xjk5utx36On9p0oF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1263&quot; height=&quot;362&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;suspend 메서드가 아닌 일반 메서드를 호출하면 테스트가 정상적으로 성공한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/efiuc2/btsKl2LGilg/Q1ZtyDAJbq5KkzdqC9PuQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/efiuc2/btsKl2LGilg/Q1ZtyDAJbq5KkzdqC9PuQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/efiuc2/btsKl2LGilg/Q1ZtyDAJbq5KkzdqC9PuQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fefiuc2%2FbtsKl2LGilg%2FQ1ZtyDAJbq5KkzdqC9PuQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1350&quot; height=&quot;838&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 Spring Data JPA는 기본적으로 동기 프로세스에 대해서만 지원하고, 코루틴에서의 Transaction을 지원하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 공식 Github에서 Spring MVC + JPA 환경에서 코루틴의 &lt;code&gt;@Transactional&lt;/code&gt;이 동작하지 않는다고 질문한 내용이 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;591&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmhtyM/btsKk35P8w0/xOwacyFoCf6j3hKKft7pIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmhtyM/btsKk35P8w0/xOwacyFoCf6j3hKKft7pIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmhtyM/btsKk35P8w0/xOwacyFoCf6j3hKKft7pIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmhtyM%2FbtsKk35P8w0%2FxOwacyFoCf6j3hKKft7pIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;915&quot; height=&quot;591&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;591&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답변은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7idDd/btsKlXQ8oD4/nY0ARFKKWNrJkSC8gRRzM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7idDd/btsKlXQ8oD4/nY0ARFKKWNrJkSC8gRRzM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7idDd/btsKlXQ8oD4/nY0ARFKKWNrJkSC8gRRzM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7idDd%2FbtsKlXQ8oD4%2FnY0ARFKKWNrJkSC8gRRzM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;932&quot; height=&quot;180&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 Reactive Transaction을 지원하기 때문에 MVC + JDBC가 아닌 WebFlux + R2DBC에서 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 &lt;code&gt;R2DBC&lt;/code&gt;를 사용하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwBXgw/btsKjOvdlVK/fatsyAaobhsukYyfdixPEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwBXgw/btsKjOvdlVK/fatsyAaobhsukYyfdixPEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwBXgw/btsKjOvdlVK/fatsyAaobhsukYyfdixPEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwBXgw%2FbtsKjOvdlVK%2FfatsyAaobhsukYyfdixPEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;322&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R2DBC는 쉽게 설명하면 아래의 부분과 대조되는 관계형 DB를 Reactive로 연결하는 라이브러리로 생각하면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDBC - R2DBC&lt;/li&gt;
&lt;li&gt;Spring Data JPA - Spring Data R2DBC&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data R2DBC는 코루틴 Repository를 포함해서 &lt;code&gt;CoroutineCrudRepository&lt;/code&gt;를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Repository
interface OrderRepository : CoroutineCrudRepository&amp;lt;Order, Long&amp;gt; {
    override suspend fun findById(id: Long): Order
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 모든 테스트를 통과한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wFYUx/btsKlGaZIFy/2YUJmxjVBBaGE7WrfPY4wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wFYUx/btsKlGaZIFy/2YUJmxjVBBaGE7WrfPY4wK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wFYUx/btsKlGaZIFy/2YUJmxjVBBaGE7WrfPY4wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwFYUx%2FbtsKlGaZIFy%2F2YUJmxjVBBaGE7WrfPY4wK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1274&quot; height=&quot;435&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;435&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;R2DBC&lt;/code&gt; 에서는 suspend 함수의 &lt;code&gt;@Transactional&lt;/code&gt; 애노테이션까지도 지원하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineCrudRepository와 R2DBC를 사용하면 아래 효과가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;suspend 함수에서도 AOP를 활용한 트랜잭션을 지원한다.&lt;/li&gt;
&lt;li&gt;데이터 접근 시 suspend 함수를 사용해서 Blocking I/O를 방지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 원하지 않는 범위의 롤백&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증이 필요한 부분이 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC의 Transaction에서는 ThreadLocal에 트랜잭션의 커넥션 정보를 저장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNOLxr/btsKlEEe9sx/sI9TlaKHTic7ByXMZK0GDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNOLxr/btsKlEEe9sx/sI9TlaKHTic7ByXMZK0GDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNOLxr/btsKlEEe9sx/sI9TlaKHTic7ByXMZK0GDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNOLxr%2FbtsKlEEe9sx%2FsI9TlaKHTic7ByXMZK0GDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1055&quot; height=&quot;444&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 코루틴 쓰레드는 1개의 요청만을 전담해서 처리하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴의 쓰레드는 여러 요청의 Job을 수행하기 때문에 롤백될 때 원하지 않는 것들까지 롤백될 수 있다. (JPA의 &lt;code&gt;@Transactional&lt;/code&gt;의 사용을 위해 Custom 하게 AOP를 동작하게 처리하다가 주로 발생한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 1개 쓰레드에서 여러 개의 Job을 동시다발적으로 처리할 때도 정상적으로 동작해야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
fun `동시성 테스트`() = runTest {
    // given
    repeat(2000) {
        orderRepository.save(Order(status = OrderStatus.READY))
    }

    // when
    val jobs = ArrayList&amp;lt;Job&amp;gt;()
    for (i in 1L..2000L) {
        val job = launch(Dispatchers.IO) {
            orderService.submit(
                id = i,
                throwException = i % 10 == 0L,
            )
        }
        jobs.add(job)
    }
    jobs.joinAll()

    // then
    val submittedOrders = orderRepository.findAll().toList().filter { it.status == OrderStatus.SUBMITTED }
    assertThat(submittedOrders).hasSize(1800)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드는 2천개의 작업 중 1800개는 주문을 제출하고, 200개는 롤백하게 코드를 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;200개의 주문을 롤백할 때 다른 주문까지 롤백된다면 주문된 결과가 1800건보다 적을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1307&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXZ1ym/btsKluokxYy/wMqba35T2lpQdnapWa6km1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXZ1ym/btsKluokxYy/wMqba35T2lpQdnapWa6km1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXZ1ym/btsKluokxYy/wMqba35T2lpQdnapWa6km1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXZ1ym%2FbtsKluokxYy%2FwMqba35T2lpQdnapWa6km1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1307&quot; height=&quot;430&quot; data-origin-width=&quot;1307&quot; data-origin-height=&quot;430&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 &lt;code&gt;CountDownLatch&lt;/code&gt;를 사용해서 복잡한 검증을 하는 것이 좋겠지만, 반복적으로 확인해봤을 때 문제가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 동시성 테스트도 무사히 통과했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트가 통과한 이유는 &lt;code&gt;R2DBC&lt;/code&gt;에서는 &lt;code&gt;PlatformTransactionManager&lt;/code&gt;가 아닌 &lt;code&gt;ReactiveTransactionManager&lt;/code&gt;를 사용하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ReactiveTransactionManager&lt;/code&gt;는 &lt;code&gt;ThreadLocal&lt;/code&gt;이 아닌 &lt;code&gt;Reactor&lt;/code&gt;의 &lt;code&gt;Context&lt;/code&gt;에 커넥션 정보를 보관한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1639&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bj8yAl/btsKkgdByLO/Ar4Wq4vJtxuGdexOO94Jnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bj8yAl/btsKkgdByLO/Ar4Wq4vJtxuGdexOO94Jnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj8yAl/btsKkgdByLO/Ar4Wq4vJtxuGdexOO94Jnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbj8yAl%2FbtsKkgdByLO%2FAr4Wq4vJtxuGdexOO94Jnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1639&quot; height=&quot;434&quot; data-origin-width=&quot;1639&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/enilqC/btsKlQxW124/HgtQLg3gjN4LHXyTNOmrz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/enilqC/btsKlQxW124/HgtQLg3gjN4LHXyTNOmrz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/enilqC/btsKlQxW124/HgtQLg3gjN4LHXyTNOmrz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FenilqC%2FbtsKlQxW124%2FHgtQLg3gjN4LHXyTNOmrz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;313&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Reactor&lt;/code&gt;의 &lt;code&gt;Context&lt;/code&gt;는 코루틴의 &lt;code&gt;Context&lt;/code&gt;와 호환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 Thread 1개가 여러 Job을 수행하더라도 각 Job 안에 커넥션 정보를 보관하기에 각 Job은 트랜잭션을 안전하게 보관할 수 있게 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로그래밍적 트랜잭션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍적 트랜잭션에 대해서도 간략하게 알아보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I8LwW/btsKkrzcGtk/i69FmAdGNy4hzPV6hn8pm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I8LwW/btsKkrzcGtk/i69FmAdGNy4hzPV6hn8pm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I8LwW/btsKkrzcGtk/i69FmAdGNy4hzPV6hn8pm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI8LwW%2FbtsKkrzcGtk%2Fi69FmAdGNy4hzPV6hn8pm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1184&quot; height=&quot;305&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 지원하는 프로그래밍적 트랜잭션은 크게 아래 2가지 방법이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TransactionTemplate&lt;/li&gt;
&lt;li&gt;TransactionalOperator&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;103&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WZDO6/btsKkIua35k/F5R05dzLcsrERv4kqm2b81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WZDO6/btsKkIua35k/F5R05dzLcsrERv4kqm2b81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WZDO6/btsKkIua35k/F5R05dzLcsrERv4kqm2b81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWZDO6%2FbtsKkIua35k%2FF5R05dzLcsrERv4kqm2b81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;701&quot; height=&quot;103&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;103&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TransactionTemplate&lt;/code&gt;은 PlatformTransactionManager를 기반으로 동작한다. 즉, 동기식 코드에 적합하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZKBsd/btsKkYQ26uD/gP7BJY1s0ReSQhT3VPUySK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZKBsd/btsKkYQ26uD/gP7BJY1s0ReSQhT3VPUySK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZKBsd/btsKkYQ26uD/gP7BJY1s0ReSQhT3VPUySK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZKBsd%2FbtsKkYQ26uD%2FgP7BJY1s0ReSQhT3VPUySK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;751&quot; height=&quot;118&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반응형 코드에서는 &lt;code&gt;TransactionalOperator&lt;/code&gt;를 권장한다. &lt;code&gt;TransactionalOperator&lt;/code&gt;는 &lt;code&gt;ReactiveTransactionManager&lt;/code&gt;를 기반으로 동작한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vos2A/btsKlmc3eFG/oUckKrAxWPV7NwEkrvuKh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vos2A/btsKlmc3eFG/oUckKrAxWPV7NwEkrvuKh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vos2A/btsKlmc3eFG/oUckKrAxWPV7NwEkrvuKh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvos2A%2FbtsKlmc3eFG%2FoUckKrAxWPV7NwEkrvuKh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;826&quot; height=&quot;137&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;137&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TransactionalOperator&lt;/code&gt;는 콜백 방식을 사용하고, &lt;code&gt;ReactiveStream&lt;/code&gt;을 기반으로 동작한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R2DBC를 활용하면 아래의 장점이 생긴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;suspend 함수에서도 선언적 트랜잭션 방식을 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;Transaction 내부에서도 코루틴 사용이 가능하다.&lt;/li&gt;
&lt;li&gt;데이터 접근을 비동기적으로 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선언적 트랜잭션 + AOP를 활용하기에 클래스 구조가 복잡해질 수 있다.&lt;/li&gt;
&lt;li&gt;비교적 높은 난이도&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 프로그래밍적 트랜잭션 방식을 유지하고, 반응형으로 코드를 처리하려면 TransactionalOperator를 사용하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/tx-decl-vs-prog.html&quot;&gt;https://docs.spring.io/spring-framework/reference/data-access/transaction/tx-decl-vs-prog.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=FXHv8ROsc-o&quot;&gt;https://www.youtube.com/watch?v=FXHv8ROsc-o&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devfunny.tistory.com/916&quot;&gt;https://devfunny.tistory.com/916&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;</description>
      <category>Server/Spring JPA</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/436</guid>
      <comments>https://jaehoney.tistory.com/436#entry436comment</comments>
      <pubDate>Sun, 29 Sep 2024 20:16:51 +0900</pubDate>
    </item>
    <item>
      <title>Spring I/O 2024 - 스프링으로 DDD 구현하기! (해석 및 리뷰)</title>
      <link>https://jaehoney.tistory.com/435</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbkxSU/btsJPBicwS7/urFtPskdLbfrsXuLsJ5qCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbkxSU/btsJPBicwS7/urFtPskdLbfrsXuLsJ5qCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbkxSU/btsJPBicwS7/urFtPskdLbfrsXuLsJ5qCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbkxSU%2FbtsJPBicwS7%2FurFtPskdLbfrsXuLsJ5qCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;290&quot; height=&quot;156&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;Spring I/O 2024에서 DDD(Domain Driven Design) 관련된 강연을 했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=VGhg6Tfxb60&quot;&gt;Implementing Domain Driven Design with Spring by Maciej Walkowiak @ Spring I/O 2024&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 강연은 DDD의 기본부터 Spring을 활용한 구현 방법을 설명한다. 아래는 해당 강연 내용을 해석한 내용이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DDD&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마틴파울러는 DDD를 아래와 같이 설명한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DDD(Domain-Driven-Design)&lt;/b&gt; is an approach to software development that centers the development on programming a &lt;b&gt;domain model&lt;/b&gt; that has a rich &lt;b&gt;understanding&lt;/b&gt; of the processes and rules of a domain&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD는 소프트웨어 개발에 대한 접근 방식으로, 도메인의 프로세스와 규칙에 대한 &lt;b&gt;풍부한 이해&lt;/b&gt;를 가진 &lt;b&gt;도메인 모델&lt;/b&gt;을 프로그래밍하는데 집중하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;풍부한 이해&lt;/b&gt;를 바탕으로 &lt;b&gt;도메인&lt;/b&gt;을 활용해서 도메인 모델을 만드는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD를 구현할 때 아래 과정을 거친다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 이해&lt;/li&gt;
&lt;li&gt;도메인을 서브 도메인으로 분리&lt;/li&gt;
&lt;li&gt;유비쿼터스 언어 개발&lt;/li&gt;
&lt;li&gt;도메인 모델 개발&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;도메인 이해&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뱅킹 소프트웨어를 만들기 위해서는 뱅킹과 은행업을 배우는 수 밖에 없다. 하지만 현실적으로는 시간이 부족해서 쉽지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 합리적인 방법은 사람들과 대화하는 것이다. 사람들과&amp;nbsp;대화하고&amp;nbsp;도메인을&amp;nbsp;이해해나가면서&amp;nbsp;전문가가&amp;nbsp;될&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;도메인을&amp;nbsp;서브&amp;nbsp;도메인으로&amp;nbsp;분리&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;도메인을 어느정도 이해하게 되면 해당 도메인은 일반적으로 복잡하다. 한 번에 해결하기 어렵기 때문에 문제를 더 작은 문제로 나누어야 한다. 그래서 가능한 독립적으로 문제를 해결하고 함께 동작하도록 만들어야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;유비쿼터스 언어 개발&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유비쿼터스 언어로 우리는 문서를 작성하고 해당 언어를 사용해서 커뮤니케이션한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그&amp;nbsp;결과&amp;nbsp;코드에&amp;nbsp;대한&amp;nbsp;내용을&amp;nbsp;공유해도&amp;nbsp;개발자가&amp;nbsp;아닌&amp;nbsp;관계자도&amp;nbsp;요점을&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;된다.&amp;nbsp;즉,&amp;nbsp;코드와&amp;nbsp;고객(관계자)&amp;nbsp;간의&amp;nbsp;격차가&amp;nbsp;줄여야&amp;nbsp;한다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;도메인 모델 개발&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이해한&amp;nbsp;도메인의&amp;nbsp;내용을&amp;nbsp;반영하는&amp;nbsp;코드를&amp;nbsp;작성한다는&amp;nbsp;의미이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도메인 모델&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 모델의 책임은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 캡슐화&lt;/li&gt;
&lt;li&gt;도메인 지식&lt;/li&gt;
&lt;li&gt;도메인 프로세스&lt;/li&gt;
&lt;li&gt;도메인 규칙&lt;/li&gt;
&lt;li&gt;제약&lt;/li&gt;
&lt;li&gt;행동&lt;/li&gt;
&lt;li&gt;상태 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 모델을 개발하는 데 사용할 수 있는 것 들은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Entities&lt;/li&gt;
&lt;li&gt;Value Objects&lt;/li&gt;
&lt;li&gt;Domain Services&lt;/li&gt;
&lt;li&gt;Factories&lt;/li&gt;
&lt;li&gt;Aggregates&lt;/li&gt;
&lt;li&gt;Repositories&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아키텍처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 모델은 프레임워크 등과 관계가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Onion Architecture, Hexagonal Architecture, Clean Architercture에서 가장 중요한 것은 동일하다. 도메인 모델이 중앙에 있고 종속성 화살표가 안쪽으로 향하는 것이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3kvZa/btsJPBCq15o/s5oAQFXOtwYWboBj6lRzN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3kvZa/btsJPBCq15o/s5oAQFXOtwYWboBj6lRzN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3kvZa/btsJPBCq15o/s5oAQFXOtwYWboBj6lRzN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3kvZa%2FbtsJPBCq15o%2Fs5oAQFXOtwYWboBj6lRzN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;345&quot; height=&quot;279&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Infrastructure Layer의 경우 Application Services와 Domain Services를 통해 DomainModel에 의존하지만 반대는 허용되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 DDD의 기본 개념에 대해 설명했다. Spring으로 어떻게 구현하는 지로 넘어가자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도메인 모델 핵심&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 구조에서는 패키지명으로 dto, entity, repository, service 등을 사용한다. 여기서 Entity는 데이터 컨테이너일 뿐이다. 모든 논리는 서비스에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 프로젝트는 소규모 서비스, 토이 프로젝트에 매우 적합할 수 있다. 하지만 성장하면 결국 수십 개의 서비스 클래스를 가지게 되고, 서비스가 여러 곳에서 사용되어 남용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 DDD는 어떤 식으로 구현해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 모델이 &lt;b&gt;User Story&lt;/b&gt;를 담아야 한다. 이는 사용자가 애플리케이션을 사용하여 수행하려는 실제 작업을 나타낸다. 도서관 시스템을 예를 들면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사서는 책의 사본을 등록한다.&lt;/li&gt;
&lt;li&gt;사용자는 책을 가질 수 있다.&lt;/li&gt;
&lt;li&gt;책에는 ISBN이 있고, 사본에는 바코드가 있다. 각 사본은 동일한 ISBN을 가진다.&lt;/li&gt;
&lt;li&gt;사용자는 책을 대여하고 반납할 수 있다.&lt;/li&gt;
&lt;li&gt;책은 카탈로그로 분류된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserStory는&amp;nbsp;유비쿼터스&amp;nbsp;언어를&amp;nbsp;사용해서&amp;nbsp;개발한다.&amp;nbsp;그리고&amp;nbsp;&lt;b&gt;UserStory를&amp;nbsp;기반으로&amp;nbsp;코드를&amp;nbsp;구현하는&amp;nbsp;데&amp;nbsp;집중&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현을 살펴보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예시 코드는 &lt;a href=&quot;https://github.com/maciejwalkowiak/implementing-ddd-with-spring-talk&quot;&gt;GitHub&lt;/a&gt;에서 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서 Spring Modulith를 사용한다. 요즘 Spring 관련 컨퍼런스에서 자주 보이는데 자세한 부분은 다음에 다루자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Modulith는 도메인 중심적인 Spring Boot Application을 구축할 수 있고, 각 모듈의 이벤트 기반 아키텍처를 구축할 수 있도록 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA를 사용할 경우 &lt;b&gt;경계&lt;/b&gt;를 잘못 설정하면 올바르게 변경하는 것이 매우 어렵다. 다른 DB에 저장될 경우 복잡한 마이그레이션을 수행해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 도메인 모델이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class Book {
    private BookId id;
    private String title;
    private Isbn isbn;

    public Book(String title, Isbn isbn) {
        Assert.notNull(title, &quot;title must not be null&quot;);
        Assert.notNull(isbn, &quot;isbn must not be null&quot;);
        this.id = new BookId();
        this.title = title;
        this.isbn = isbn;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 Book은 스스로 생성자에서 자신의 요구사항을 검증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Isbn 클래스를 먼저 보자.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public record Isbn(String value) {
    private static final ISBNValidator VALIDATOR = new ISBNValidator();

    public Isbn {
        if (!VALIDATOR.isValid(value)) {
            throw new IllegalArgumentException(&quot;invalid isbn: &quot; + value);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Isbn도 생성자를 통해 해당 객체가 가진 책임에 대한 내용을 검증하고 있다. 해당 객체는 Value-Object 이기 때문에 record는 최선의 선택이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 각 엔터티의 ID에 대한 전용 클래스를 만들어야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public record BookId(UUID id) {

    public BookId {
        Assert.notNull(id, &quot;id must not be null&quot;);
    }

    public BookId() {
        this(UUID.randomUUID());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 Repository를 보자. 도메인 모델을 만들 때에는 두 가지 옵션이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Entity를 도메인 모델으로 사용하는 것&lt;/li&gt;
&lt;li&gt;도메인 모델을 순수한 상태로 유지하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후자의 경우 많은 구현이 추가로 필요하다. 아래와 같이 Entity로의 변환을 통해 접근한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;@Component
public class JpaBookRepository implements BookRepository {
    private final BookEntityRepository bookEntityRepository;
    public JpaBookRepository(BookEntityRepository bookEntityRepository) {
        this.bookEntityRepository = bookEntityRepository;
    }
    @Override
    public Book save(Book book) {
        BookEntity entity = new BookEntity(book.getId().id(), book.getTitle(), book.getIsbn().value());
        bookEntityRepository.save(entity);
        return book;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헥사고날 아키텍처의 Adaptor와 유사하다. 이 부분은 상황에 따라 선택하면 될 것 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;서비스&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 구현의 경우 일반적으로 BookService, CopyService, LibraryService를 만드는 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 방법이지만 유지보수와 테스트를 유지하기 어렵다는 단점이 있는 것 같다. 그래서 클린아키텍처의 개념인 UseCase을 사용하는 것을 권한다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;@UseCase
public class RegisterBookCopyUseCase {
    private final CopyRepository copyRepository;

    public RegisterBookCopyUseCase(CopyRepository copyRepository) {
        this.copyRepository = copyRepository;
    }

    public void execute(@NotNull BookId bookId, @NotNull BarCode barCode) {
        copyRepository.save(new Copy(bookId, barCode));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UseCase는 단 1개의 메서드만을 가진다. 파라미터로 String이 아닌 Value Object를 받으므로 이점을 충분히 볼 수 있다. Kotlin을 사용한다면 invoke() 메서드를 사용하는 것도 좋은 선택일 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Infrastructure&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 의존이 필요한 경우에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 도메인 계층의 Service(Interface)가 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface BookSearchService {
    BookInformation search(Isbn isbn);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Infrastructure는 해당 Service(Interface)를 구현한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
class OpenLibraryBookSearchService implements BookSearchService {
    private final RestClient restClient;

    public OpenLibraryBookSearchService(RestClient.Builder builder) {
        this.restClient = builder
                .baseUrl(&quot;https://openlibrary.org/&quot;)
                .build();
    }

    public BookInformation search(Isbn isbn) {
        OpenLibraryIsbnSearchResult result = restClient.get().uri(&quot;isbn/{isbn}.json&quot;, isbn.value())
                .retrieve()
                .body(OpenLibraryIsbnSearchResult.class);
        return new BookInformation(result.title());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 InfraStructure의 클래스들은 package-private을 사용한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;도메인 모델의 파라미터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD에서 고민되는 점이 있다. 검증에서 데이터 조회가 필요한 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 경우 아래와 같이 UseCase에서 검증하는 방법이 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@UseCase
public class RentBookUseCase {
    private final LoanRepository loanRepository;
    public RentBookUseCase(LoanRepository loanRepository) {
        this.loanRepository = loanRepository;
    }
    public void execute(CopyId copyId, UserId userId) {
        if(loanRepository.isAvailable(copyId)) {
            throw new IllegalArgumentException();
        }
        loanRepository.save(new Loan(copyId, userId));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 UseCase보다는 도메인 모델이 더 코어한 영역이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 도메인 모델인 Loan의 생성자에서 Repository를 파라미터로 받아서 검증하는 방법을 권한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public Loan(CopyId copyId, UserId userId, LoanRepository loanRepository) {
    Assert.notNull(copyId, &quot;copyId must not be null&quot;);
    Assert.notNull(userId, &quot;userId must not be null&quot;);
    Assert.isTrue(loanRepository.isAvailable(copyId), &quot;copy with id = &quot; + copyId + &quot; is not available&quot;);
    this.loanId = new LoanId();
    this.copyId = copyId;
    this.userId = userId;
    this.createdAt = LocalDateTime.now();
    this.expectedReturnDate = LocalDate.now().plusDays(30);
    this.registerEvent(new LoanCreated(this.copyId));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 모든 애플리케이션에서 해당 검증을 놓치지 않을 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이벤트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다수의 도메인을 함께 사용하는 등의 처리가 필요할 때는 이벤트를 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트를 발행하는 쪽에서는 구독자 계층에 대해서는 신경쓰지 않는다. 그리고 사본 도메인에서는 이벤트를 구독하여 대여가 시작되면 해당 사본은 사용하지 못하는 상태로 변경하도록 처리한다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;@Component
public class DomainEventListener {

    private final CopyRepository copyRepository;

    public DomainEventListener(CopyRepository copyRepository) {
        this.copyRepository = copyRepository;
    }

    @ApplicationModuleListener
    public void handle(LoanCreated event) {
        Copy copy = copyRepository.findById(new CopyId(event.copyId().id())).orElseThrow();
        copy.makeUnavailable();
        copyRepository.save(copy);
    }

    @ApplicationModuleListener
    public void handle(LoanClosed event) {
        Copy copy = copyRepository.findById(new CopyId(event.copyId().id())).orElseThrow();
        copy.makeAvailable();
        copyRepository.save(copy);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;@ApplicationModuleListener&lt;/code&gt;는 아래의 애노테이션을 조합한 것과 동일한 역할을 한다. 즉 Save를 통해 해당 이벤트가 발행된 객체가 저장되면 반드시 구독이 수행된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 최종적 일관성을 보장하게 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Review&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD 관련 책이나 여러 강연을 봤기 때문에 생소한 내용이 많지는 않았다. Spring I/O에서 DDD 구현에 대한 강연을 한다는 것이 꼭 보고 싶은 이유가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 영어 공부와 더불어 오픈소스 코드 스타일을 배우고 싶다는 생각도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강연 내용은 한국에 번역한 도메인 주도 개발 책과 내용과 예제가 겨우 유사했다. 하지만 파라미터를 어떻게 다룰 지, 이벤트를 어떤 관점에서 바라볼 지, 등 핵심 개념 등을 자세히 설명해주는 것 같아 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 Spring Modulith에 대해 자세히 알아봐야겠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=VGhg6Tfxb60&quot;&gt;https://www.youtube.com/watch?v=VGhg6Tfxb60&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/relational/reference/jdbc/domain-driven-design.html&quot;&gt;https://docs.spring.io/spring-data/relational/reference/jdbc/domain-driven-design.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/barrymcauley/onion-architecture-3fgl&quot;&gt;https://dev.to/barrymcauley/onion-architecture-3fgl&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Server/Spring</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/435</guid>
      <comments>https://jaehoney.tistory.com/435#entry435comment</comments>
      <pubDate>Fri, 27 Sep 2024 21:03:57 +0900</pubDate>
    </item>
    <item>
      <title>DataGrip으로 Redis 조회하기!</title>
      <link>https://jaehoney.tistory.com/434</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 운영하다보면 Redis, MySQL, Mongo, ElasticSearch 등 다양한 DB를 사용해야 할 일이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL을 위한 MySQL-Workbench, MongoDB를 위한 MongoDB-Compass 등 다양한 도구를 익히면 중간에 컨텍스트 스위칭이 안되는 경험을 많이 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JetBrains DataGrip을 사용하면 대부분 종류의 DB를 동일한 도구에서 관리할 수 있어서 많이 사용한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis 지원&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataGrip 2022.3 이상 버전부터는 Redis 조회도 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 기능을 사용하면 아래와 같이 Redis 데이터도 편리하게 조회하거나 명령어를 수행할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;273&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yF34j/btsIUfGd8xA/2rVckyO10jcK07SUBfzoMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yF34j/btsIUfGd8xA/2rVckyO10jcK07SUBfzoMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yF34j/btsIUfGd8xA/2rVckyO10jcK07SUBfzoMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyF34j%2FbtsIUfGd8xA%2F2rVckyO10jcK07SUBfzoMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1274&quot; height=&quot;273&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;273&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 코드 자동완성 기능도 있어서 Command를 사용할 때보다 확실히 편리한 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;578&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJRde3/btsIS6Dqf9L/DzebSZ2fToom4EIAObO7K1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJRde3/btsIS6Dqf9L/DzebSZ2fToom4EIAObO7K1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJRde3/btsIS6Dqf9L/DzebSZ2fToom4EIAObO7K1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJRde3%2FbtsIS6Dqf9L%2FDzebSZ2fToom4EIAObO7K1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;578&quot; height=&quot;299&quot; data-origin-width=&quot;578&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;574&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpNZSq/btsIThLy8SD/9ZKKV17ij4PHktfFilvsf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpNZSq/btsIThLy8SD/9ZKKV17ij4PHktfFilvsf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpNZSq/btsIThLy8SD/9ZKKV17ij4PHktfFilvsf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpNZSq%2FbtsIThLy8SD%2F9ZKKV17ij4PHktfFilvsf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;574&quot; height=&quot;296&quot; data-origin-width=&quot;574&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구에 너무 익숙해지면 도구 없이 작업을 할 때 어려워진다는 단점이 있지만, 그만큼 생산성이 향상되기 때문에  적응해보는 것도 좋을 것 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.jetbrains.com/pages/datagrip-for-redis/&quot;&gt;https://www.jetbrains.com/pages/datagrip-for-redis/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database/NoSQL</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/434</guid>
      <comments>https://jaehoney.tistory.com/434#entry434comment</comments>
      <pubDate>Sat, 3 Aug 2024 13:49:19 +0900</pubDate>
    </item>
    <item>
      <title>RDB - 파티션을 사용할 때 고려할 부분! (feat. 공식문서)</title>
      <link>https://jaehoney.tistory.com/433</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파티션을 사용할 때 고려할 부분&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합류한 새로운 팀에서는 파티셔닝을 적극적으로 사용하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partitioning(파티셔닝)은 논리적으로 하나의 테이블이지만, 물리적으로는 여러 개의 파일 시스템에 분산하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티셔닝은 테이블과 인덱스 모두 적용된다. 파티셔닝을 사용하는 이유는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더 많은 데이터를 하나의 테이블에 저장할 수 있다.&lt;/li&gt;
&lt;li&gt;유용성을 잃은 데이터에 대해 특정 파티션을 제거하는 등 관리가 가능하다.&lt;/li&gt;
&lt;li&gt;검색에 대한 DB 부하를 감소하는 기능이다. (WHERE 절로 필요하지 않은 파티션은 자동으로 제외)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 공식 문서를 기반으로 파티셔닝을 사용할 때 고려해야할 부분에 대해 알아보자.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1608&quot; data-origin-height=&quot;769&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sbWcT/btsIQyFZoXL/fskcYu97z7QGfB1y4n0deK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sbWcT/btsIQyFZoXL/fskcYu97z7QGfB1y4n0deK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sbWcT/btsIQyFZoXL/fskcYu97z7QGfB1y4n0deK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsbWcT%2FbtsIQyFZoXL%2FfskcYu97z7QGfB1y4n0deK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;649&quot; height=&quot;769&quot; data-origin-width=&quot;1608&quot; data-origin-height=&quot;769&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 인덱스(Index)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티셔닝을 사용할 때 고려해야 할 것이 있다. 바로 Index이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티셔닝을 사용할 때 인덱스를 활용할 수 없다면 효율이 떨어질 것이다. 반면, 주로 사용하는 인덱스가 파티션 풀 스캔을 하는 경우에도 효율이 떨어진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;파티션 O 인덱스 O&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 이상적인 케이스는 파티션 스캔을 할 수 있으면서 원하는 인덱스를 타는 경우이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 주어진 파티션 내에서 인덱스로 스캔하므로 최상의 조건이라고 할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;파티션 X 인덱스 O&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 대표적인 예시로 PK 스캔을 하는 경우 예가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거래일자를 기반으로 파티션을 생성한 경우 PK 스캔을 하면 전체 파티션을 찾아야 하지만, PK를 사용해서 데이터를 빠르게 찾을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션의 개수가 많지 않다면 크게 문제되지는 않을 것이다. 다만, 파티션 수가 지나치게 많을 경우 가랑비에 옷이 젖는 것처럼 자원에 대한 문제가 생길 우려가 있다. 파티션을 사용하도록 유도하는 것이 더 좋을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션을 사용하려면 &lt;code&gt;WHERE&lt;/code&gt; 조건에서 파티션 키에 있는 모든 컬럼을 모두 사용해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;파티션 O 인덱스 X&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 특정 파티션만 읽으면 되지만 인덱스를 사용하지 않는 경우에 해당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 특정 파티션에 대해 Full Table Scan을 하기에 상당히 느린 쿼리가 발생한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;파티션 X 인덱스 X&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블의 모든 파티션을 검색하고 각 파티션을 Full Table Scan 해야 하기에 최악의 쿼리이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;좋은 설계..?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 스캔도 하고 인덱스도 타면 더할 나위 없이 좋다. 하지만 파티션과 인덱스의 기준은 요구사항에 따라 매우 다를 수 있다. 특정 서비스에서는 &lt;code&gt;일자&lt;/code&gt;가 파티션 키일 수 있고, 어떤 서비스에서는 &lt;code&gt;지역&lt;/code&gt;일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 서비스에서는 요구사항이 목록 조회만 있어서 파티션을 모든 경우에서 활용할 수 있을 수도 있고, 특정 서비스에서는 PK 조회, 파티션 키가 아닌 다른 키로 조회해야 하는 등 요구사항이 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 모든 서비스에서 Partition Key로 모든 요구사항에서 조회할 수 있기를 기대하는 것은 현실적으로 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Case에서는 파티션을 사용하지 않더라도 인덱스를 사용한다면 크게 무리가 있지는 않을 것이다. 즉, 인덱스를 사용하지 않는 것을 더 지양하기를 권한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튜닝이 가능할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배민시스템에서 &lt;code&gt;주문일시&lt;/code&gt;, &lt;code&gt;배달일시&lt;/code&gt; 컬럼이 있고 &lt;code&gt;배달일시&lt;/code&gt;를 조건으로 검색을 해야 한다. 하지만 &lt;code&gt;주문일시&lt;/code&gt; 컬럼이 파티션 키라고 가정하자. 이때는 &lt;code&gt;배달일시&lt;/code&gt;를 조건으로 조회하더라도 &lt;code&gt;주문일시&lt;/code&gt;까지 넉넉하게 이틀 정도를 조건으로 잡아주면 파티션 풀 스캔을 막을 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 락(Lock)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL을 예시로 5.6.6 이전버전에서는 SELECT를 시도했을 때 전체 테이블에 Lock이 걸린다. 아래 설명은 MySQL 5.6.6 이상의 버전을 기준으로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT의 경우 기본적으로 실제로 읽어야 하는 파티션을 대상으로 잠금을 수행한다. 만약 1개의 파티션을 대상으로 조회한다면 해당 파티션만 LOCK이 발생하고, 모든 파티션을 대상으로 조회한다면 모든 파티션에 LOCK이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UPDATE의 경우에는 파티셔닝 키 컬럼이 변경되지 않았을 경우 해당 파티션만 잠근다. 하지만, 파티셔닝 키 컬럼이 변경될 경우 MySQL에서는 어떤 파티션이 영향을 받을지 예측할 수 없으므로 모든 파티션을 잠궈 무결성을 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT는 삽입된 데이터의 파티셔닝 키에 따라 특정 파티션만 잠근다. 단, 파티셔닝 키가 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt;에 의해 생성된다면 모든 파티션을 잠근다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 파티셔닝된 테이블은 1개의 테이블로 관리될 때에 비해 의도치 않게 많은 영역에 대해 Lock을 걸게될 수 있다. 그래서 데드락을 유발하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 조회 조건이 특정 파티션만을 조회하도록 하고, 파티셔닝 키 컬럼을 변경할 때는 각별히 주의해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 파일 시스템 환경 변수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 MySQL 에서는 1개 테이블을 열 때 2~3개의 파일을 조회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 파티션이 1024개라면 1024 x 2~3 개의 파일을 조회할 수도 있다는 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서는 관련해서 아래의 환경 변수를 지원한다. 해당과 같은 환경 변수를 적절하게 조절해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;large_files_support: 대용량 파일 지원 여부 (여러 파티션을 조회한다면 활성화가 되어 있어야 한다.)&lt;/li&gt;
&lt;li&gt;open_files_limit: MySQL이 오픈할 수 있는 최대 파일 개수 (default = OS에 따라 조정됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. etc&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티셔닝된 테이블에서는 아래의 제약이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FK를 사용할 수 없다.&lt;/li&gt;
&lt;li&gt;Full-Text Index를 사용할 수 없다.&lt;/li&gt;
&lt;li&gt;Geometry(point, geometry, ...) 컬럼 타입을 사용할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/partitioning.html&quot;&gt;https://dev.mysql.com/doc/refman/8.4/en/partitioning.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/5.7/en/partitioning-limitations-locking.html&quot;&gt;https://dev.mysql.com/doc/refman/5.7/en/partitioning-limitations-locking.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Database/SQL</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/433</guid>
      <comments>https://jaehoney.tistory.com/433#entry433comment</comments>
      <pubDate>Sun, 28 Jul 2024 18:26:52 +0900</pubDate>
    </item>
    <item>
      <title>Spring - Sentry 이해하기! (+ 모니터링 개선, Tag 사용하기!)</title>
      <link>https://jaehoney.tistory.com/432</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&amp;nbsp;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스의 모니터링이 점점 어려워지는 문제가 발생하고 있었다. 가장 큰 문제는 &lt;b&gt;불필요한 에러 Alert이 너무 많다는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 신경이 더 많이 사용되고 정말 받아야 하는 Alert이 왔을 때 놓치거나 무신경하게 대응할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스의 중요도나 위험도, 트래픽 등을 고려했을 때 &lt;b&gt;모니터링 개선&lt;/b&gt;이 반드시 필요해서 시간내서 학습하고 적용하게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Sentry&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentry는 에러 모니터링 및 성능 모니터링을 제공해주는 도구이다. 주로 에러 트래킹이나 Slack 등을 통한 Alert으로 많이 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시를 위해 SpringBoot 3.1.9 버전과 아래 라이브러리를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation 'io.sentry:sentry-spring-boot-starter-jakarta:7.5.0'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yml은 아래와 같이 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;sentry:
  dsn: https://fa1b1dc87e3eb8bee49cc2d25b06615e@o4506869937078272.ingest.us.sentry.io/4506869938913280&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 키는 Sentry에 로그인하면 발급받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 해보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
class Controller {

    @RequestMapping(&quot;/test&quot;)
    fun test() {
        throw RuntimeException(&quot;Error!!&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API를 호출하면 아래와 같이 Sentry로 예외가 전달된다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1889&quot; data-origin-height=&quot;683&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dVU458/btsIwV8X0JO/Pd6LjuFXk6P65RWWIK79U0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dVU458/btsIwV8X0JO/Pd6LjuFXk6P65RWWIK79U0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dVU458/btsIwV8X0JO/Pd6LjuFXk6P65RWWIK79U0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdVU458%2FbtsIwV8X0JO%2FPd6LjuFXk6P65RWWIK79U0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1889&quot; height=&quot;683&quot; data-origin-width=&quot;1889&quot; data-origin-height=&quot;683&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h2 data-ke-size=&quot;size26&quot;&gt;이벤트 발생 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 Exception을 catch하면 에러(Event)가 Sentry로 전달되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
class Controller {

    @RequestMapping(&quot;/test&quot;)
    fun test() {
        try {
            throw RuntimeException(&quot;catch!!&quot;)
        } catch (e: Exception) {
            println(e.message)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentry를 잘 사용하려면 Error를 전달(Capture)하는 원리와 기준에 대해 알아야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;작동 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Sentry 라이브러리에 있는 &lt;code&gt;SentryExceptionResolver&lt;/code&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;public class SentryExceptionResolver implements HandlerExceptionResolver, Ordered {

    private final int order;
    // .. 생략

    @Override
    public @Nullable ModelAndView resolveException(
        final @NotNull HttpServletRequest request,
        final @NotNull HttpServletResponse response,
        final @Nullable Object handler,
        final @NotNull Exception ex) {

        final SentryEvent event = createEvent(request, ex);
        final Hint hint = createHint(request, response);

        hub.captureEvent(event, hint);

        // null = run other HandlerExceptionResolvers to actually handle the exception
        return null;
    }

    @NotNull
    protected SentryEvent createEvent(
        final @NotNull HttpServletRequest request, final @NotNull Exception ex) {

        final Mechanism mechanism = new Mechanism();
        mechanism.setHandled(false);
        mechanism.setType(MECHANISM_TYPE);
        final Throwable throwable =
            new ExceptionMechanismException(mechanism, ex, Thread.currentThread());
        final SentryEvent event = new SentryEvent(throwable);
        event.setLevel(SentryLevel.FATAL);
        event.setTransaction(transactionNameProvider.provideTransactionName(request));

        return event;
    }

    // org.springframework.core.Ordered 인터페이스의 메서드
    @Override
    public int getOrder() {
        return order;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 클래스는 &lt;code&gt;HandlerExceptionResolver&lt;/code&gt;를 구현한다. &lt;code&gt;HandlerExceptionResolver&lt;/code&gt;는 스프링 웹에서 발생된 예외를 핸들링 할 수 있는 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 예외를 핸들링하는 방식으로 동작하기 때문에 catch한 경우에는 Error가 Sentry로 전달되지 않았던 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Order&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@ControllerAdvice&lt;/code&gt;를 사용할 경우 Exception을 터트리지 않고 객체를 반환하도록 하는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 &lt;code&gt;ExceptionHandlerExceptionResolver&lt;/code&gt;를 검색하면서 &lt;code&gt;AnnotationAwareOrderComparator&lt;/code&gt;를 사용해서 정렬한다. 해당 클래스는 Order를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래 속성으로 Order를 명시할 수 있다. default가 1이라서 &lt;code&gt;@ExceptionHandler&lt;/code&gt;가 먼저 동작한다. (낮을수록 먼저 동작한다.)&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;sentry.exception-resolver-order: 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentry는 ExceptionResolver 뿐만 아니라 다양한 방법으로 비동기로 Sentry 서버에 이벤트를 전달한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SentryExceptionResolver&lt;/li&gt;
&lt;li&gt;SentrySpringFilter&lt;/li&gt;
&lt;li&gt;SentryWebExceptionHandler&lt;/li&gt;
&lt;li&gt;SentryCaptureExceptionParameterAdvice&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정은 프로퍼티나 YML 말고도 JVM 언어 등 프로그래밍 언어로도 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 &lt;code&gt;.properties&lt;/code&gt;를 사용한 예시이다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# 추적할 Event의 비율을 설정한다. 1.0이면 100%를 캡처한다.
# (너무 높으면 리소스를 많이 사용한다.)
sentry.traces-sample-rate=1.0
# Event를 전송할 확률을 설정한다. 1.0이면 100%를 캡처한다.
# 발생시킬 Event의 태그를 지정
sentry.tags.first_tag=first-tag-value
# 무시할 Exception 정의
sentry.ignored-exceptions-for-type=java.lang.RuntimeException,java.lang.IllegalStateExceptio
# 디버그 모드 (콘솔에 정보 출력)
sentry.debug=true
# 디버그 모드의 로그 레벨 설정 (debug, info, warning, error, fatal - default: debug)
sentry.diagnosticLevel=debug
# 로컬에 저장할 envelopes 수
sentry.maxCacheItems=30
# Stack Trace를 모든 메시지에 첨부
sentry.attachStacktrace=true
# HTTP 요청 본문 캡처 여부 (never, small, medium, alwways)
sentry.maxRequestBodySize=never
# SDK가 Sentry에 이벤트를 보낼 지 여부
sentry.enable=true
# 이벤트를 전송하기 전에 호출할 함수
sentry.beforeSend=null
# 예외 해결 순서 지정, -2147483647로 설정하면 Spring 예외 처리기에서 처리된 오류는 무시한다.
sentry.exception-resolver-order=0
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당&amp;nbsp;프로퍼티들은&amp;nbsp;특정&amp;nbsp;예외를&amp;nbsp;무시하도록&amp;nbsp;하거나&amp;nbsp;Event를&amp;nbsp;전송할&amp;nbsp;비율&amp;nbsp;등을&amp;nbsp;설정할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;필터링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentry는 올바른 정보와 합리적인 양을 가장 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;beforeSend&lt;/code&gt;는 이벤트가 서버로 전송되기 직전에 호출되기 때문에 &lt;b&gt;이벤트를 편집&lt;/b&gt;하거나 &lt;b&gt;아예 전송하지 않을 수&lt;/b&gt; 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BeforeSendCallback&lt;/code&gt;을 구현해서 빈으로 등록한다. 아래는 이벤트의 &lt;code&gt;serverName&lt;/code&gt; 필드를 null로 세팅하기 위한 설정이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class CustomBeforeSendCallback : SentryOptions.BeforeSendCallback {
    override fun execute(event: SentryEvent, hint: Hint): SentryEvent? {
        // Example: Never send server name in events
        event.serverName = null
        return event
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exception 정보로 분기를 하는 등 조건 처리도 가능하다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class CustomBeforeSendCallback : SentryOptions.BeforeSendCallback {
    override fun execute(event: SentryEvent, hint: Hint): SentryEvent? {
        if (event.throwable is SQLException) {
            return null
        }
        return event
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event를 보내지 않도록 설정하면 Sentry 서버의 부담을 줄일 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서버에서 Ignore 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentry 서버에서 Ignore 처리하는 방법도 있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;945&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HzbqV/btsIweId23j/9F6VT3m8UPhBDeSPKep3aK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HzbqV/btsIweId23j/9F6VT3m8UPhBDeSPKep3aK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HzbqV/btsIweId23j/9F6VT3m8UPhBDeSPKep3aK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHzbqV%2FbtsIweId23j%2F9F6VT3m8UPhBDeSPKep3aK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;945&quot; height=&quot;206&quot; data-origin-width=&quot;945&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;클라리언트에서는 이벤트를 발행하고 서버에서는 적재되므로 클라이언트에서 필터링하는 것에 비해 성능이 낭비된다는 점이 있다. 분류가 명확하지 않다면 원하지 않는 내용까지 Ignore 될 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Tag 활용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;앞에서 작성한 부분은 개선을 위해 공식문서를 학습한 내용&lt;/b&gt;이라면 &lt;b&gt;문제 해결을 위해 적용한&lt;/b&gt; &lt;b&gt;주요 내용&lt;/b&gt;이 &lt;b&gt;이 부분&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Exception 구조를 예시로 만든 것이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class BaeminException(val code: ErrorCode) : RuntimeException(code.message)

enum class ErrorCode(val message: String) {
    USER_NOT_FOUND(&quot;유저가 존재하지 않습니다.&quot;),
    ORDER_NOT_FOUND(&quot;주문이 존재하지 않습니다.&quot;),
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 Controller로 요청해보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
class Controller {

    @RequestMapping(&quot;/order&quot;)
    fun order(): Unit = throw BaeminException(ErrorCode.USER_NOT_FOUND)

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SentryEvent를 보면 아래와 같이 type은 &lt;code&gt;BaeminException&lt;/code&gt;, value는 &lt;code&gt;유저가 존재하지 않습니다.&lt;/code&gt; 이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;532&quot; data-origin-height=&quot;633&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUNSgj/btsIvRzLAIf/sK4hAvwu5H9k1XMdBDWofK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUNSgj/btsIvRzLAIf/sK4hAvwu5H9k1XMdBDWofK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUNSgj/btsIvRzLAIf/sK4hAvwu5H9k1XMdBDWofK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUNSgj%2FbtsIvRzLAIf%2FsK4hAvwu5H9k1XMdBDWofK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;532&quot; height=&quot;633&quot; data-origin-width=&quot;532&quot; data-origin-height=&quot;633&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 해당 에러가 왔을 때 Alert을 사용하기 위해서는 &lt;b&gt;메시지 문자열&lt;/b&gt;로 분기를 해야 하는 상황이 발생한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1071&quot; data-origin-height=&quot;767&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RWi2T/btsIwf1rbGJ/R6aPjqh39dBrtg2BSNinEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RWi2T/btsIwf1rbGJ/R6aPjqh39dBrtg2BSNinEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RWi2T/btsIwf1rbGJ/R6aPjqh39dBrtg2BSNinEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRWi2T%2FbtsIwf1rbGJ%2FR6aPjqh39dBrtg2BSNinEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1071&quot; height=&quot;767&quot; data-origin-width=&quot;1071&quot; data-origin-height=&quot;767&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;해당 &lt;b&gt;메시지&lt;/b&gt;는 &lt;b&gt;충분히 변경될 수 있는 내용&lt;/b&gt;이다. 그래서 아래 문제가 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지가 변경될 때마다 Sentry의 Alert에 동기화해야 한다.&lt;/li&gt;
&lt;li&gt;다른 메시지가 추가되거나 변경될 때 겹쳐서 의도치 않는 결과가 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Error에 대한 분류가 쉽지 않는 경우도 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래와 같이 &lt;code&gt;BeforeSendCallback&lt;/code&gt;을 활용해서 &lt;code&gt;tags&lt;/code&gt;에 원하는 값을 세팅해줄 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class BaeminBeforeSendCallback : SentryOptions.BeforeSendCallback {
    override fun execute(
        event: SentryEvent,
        hint: Hint,
    ): SentryEvent? {
        val exception = event.throwable
        if (exception is BaeminException) {
            event.setTag(ERROR_CODE_TAG, exception.code.name)
        }
        return event
    }
}

const val ERROR_CODE_TAG = &quot;errorCode&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentry 이벤트에 아래와 같이 &lt;code&gt;tags&lt;/code&gt;에 Key-value가 추가된다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dPMEkz/btsIvwigCIp/jSXwDjNSJWwVmJSxkbsQWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dPMEkz/btsIvwigCIp/jSXwDjNSJWwVmJSxkbsQWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dPMEkz/btsIvwigCIp/jSXwDjNSJWwVmJSxkbsQWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdPMEkz%2FbtsIvwigCIp%2FjSXwDjNSJWwVmJSxkbsQWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;762&quot; height=&quot;896&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 아래와 같이 &lt;code&gt;message&lt;/code&gt;가 아닌 &lt;code&gt;errorCode&lt;/code&gt;로 분류할 수 있게 된다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;733&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsFGcp/btsIwpW3NqN/M8T8k4j3rkwDZxDXyLIbD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsFGcp/btsIwpW3NqN/M8T8k4j3rkwDZxDXyLIbD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsFGcp/btsIwpW3NqN/M8T8k4j3rkwDZxDXyLIbD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsFGcp%2FbtsIwpW3NqN%2FM8T8k4j3rkwDZxDXyLIbD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1054&quot; height=&quot;733&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;733&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;이제 해당 Code로 조건을 분기해서 &lt;b&gt;지속가능한 Alert&lt;/b&gt;를 만들 수 있게 되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불필요한 Alert의 경우 해당 조건으로 제거가 가능해졌다.&lt;/li&gt;
&lt;li&gt;반드시 필요한 Alert의 경우 해당 조건으로 별도 웹훅을 세팅할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exception이 계층화되지 않은 경우 Sentry의 1개 이슈에 다수의 에러 내용이 포함되는 경우가 있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1323&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqP8GH/btsIvkI8zKG/z6y9178rhCsB8Ez3KtRhy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqP8GH/btsIvkI8zKG/z6y9178rhCsB8Ez3KtRhy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqP8GH/btsIvkI8zKG/z6y9178rhCsB8Ez3KtRhy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqP8GH%2FbtsIvkI8zKG%2Fz6y9178rhCsB8Ez3KtRhy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1323&quot; height=&quot;487&quot; data-origin-width=&quot;1323&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;해당 상황에서 &lt;b&gt;ErrorCode 별로 필터링해서 검색&lt;/b&gt;하는 것도 가능해졌다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그 통합&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외가 전파되면 Sentry 이벤트를 발행하겠지만, 로그 통합 기능을 사용할 수 있는 방법도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 의존성을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'io.sentry:sentry-logback:7.6.0'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 아래 설정을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 에러 수준이 error 이상일 때 이벤트 발행
sentry.logging.minimum-event-level=error
# 에러 수준이 debug 이상인 대상만 탐색에 포함
sentry.logging.minimum-breadcrumb-level=info&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;logback-spring.xml&lt;/code&gt;로 로그 설정을 통합할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
  &amp;lt;include resource=&quot;org/springframework/boot/logging/logback/defaults.xml&quot;/&amp;gt;
  &amp;lt;include resource=&quot;org/springframework/boot/logging/logback/console-appender.xml&quot; /&amp;gt;

  &amp;lt;appender name=&quot;SENTRY&quot; class=&quot;io.sentry.logback.SentryAppender&quot;&amp;gt;
      &amp;lt;minimumEventLevel&amp;gt;ERROR&amp;lt;/minimumEventLevel&amp;gt;
      &amp;lt;minimumBreadcrumbLevel&amp;gt;INFO&amp;lt;/minimumBreadcrumbLevel&amp;gt;
  &amp;lt;/appender&amp;gt;


  &amp;lt;root level=&quot;info&quot;&amp;gt;
    &amp;lt;appender-ref ref=&quot;CONSOLE&quot; /&amp;gt;
    &amp;lt;appender-ref ref=&quot;SENTRY&quot; /&amp;gt;
  &amp;lt;/root&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 Logback을 사용해서 &lt;code&gt;SentryAppender&lt;/code&gt;를 구성할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Throwable 전달하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Sl4fj&lt;/code&gt;를 사용한다면 아래와 같이 로그를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;log.error(&quot;유저 찾기 실패. ${e.message}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 로그에 trace가 없을 뿐더러 &lt;b&gt;Sentry Event에 Exception을 담을 수 없다.&lt;/b&gt; 위에서 설명한 Tag를 활용한 BeforeSendCallback도 Exception이 없으므로 ErrorCode가 없어서 적용되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래와 같이 Log에 Exception을 전달해주는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;log.error(&quot;유저 찾기 실패. ${e.message}&quot;, e)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/java/guides/spring-boot&quot;&gt;https://docs.sentry.io/platforms/java/guides/spring-boot&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Operation/Monitoring</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/432</guid>
      <comments>https://jaehoney.tistory.com/432#entry432comment</comments>
      <pubDate>Thu, 11 Jul 2024 21:23:29 +0900</pubDate>
    </item>
    <item>
      <title>Spring - 통합(인수) 테스트에서 Web 영역 Mocking하기(+ WireMock)</title>
      <link>https://jaehoney.tistory.com/431</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&amp;nbsp;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;해당 포스팅에서는 테스트 시 Web 영역을 어떻게 Mocking 할 지에 대해 다룬다.&lt;/span&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;통합테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘은 단위 테스트를 넘어서 통합테스트 / 인수테스트 / E2E테스트를 많이 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API를 호출하는 코드가 있다. 해당 로직을 Mocking할 때 어떻게 할 지 생각해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주문 시스템 (예시 코드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시를 위해 제작한 주문 시스템의 코드를 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 첫 주문 기능의 진입점인 주문 컨트롤러이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
class OrderController(
    private val orderService: OrderService,
) {
    @PostMapping(&quot;/order&quot;)
    fun order(
        @RequestBody orderRequest: OrderRequest,
    ): OrderResponse {
        orderService.order(orderRequest.userId, orderRequest.productId)
        return OrderResponse(isSuccess = true)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 주문 서비스이다. 주문 서비스는 결제 서비스를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class OrderService(
    private val productService: ProductService,
    private val paymentService: PaymentService,
) {
    fun order(
        userId: Long,
        productId: Long,
    ) {
        val product = productService.get(productId)
        productService.verify(product)
        val paymentResult = paymentService.payment(userId, product.amount)
        if (!paymentResult.isSuccess) {
            throw PaymentException(&quot;주문이 실패했습니다.&quot;)
        }
        // 주문 로직
        println(&quot;주문 완료!&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 결제 서비스이고, 외부 API를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class PaymentService(
    private val paymentApiClient: PaymentApiClient,
) {
    fun payment(
        userId: Long,
        amount: Amount,
    ): PaymentResult {
        val request = PaymentRequest(userId, amount)
        return paymentApiClient.payment(request)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드는 아래와 같다. 통합&amp;nbsp;테스트를&amp;nbsp;할&amp;nbsp;때&amp;nbsp;MockMvc,&amp;nbsp;RestAssured,&amp;nbsp;Cucumber&amp;nbsp;등&amp;nbsp;다양한&amp;nbsp;도구를&amp;nbsp;사용할&amp;nbsp;수도&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에서는 RestAssured를 사용해서 인수 테스트를 구성했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class OrderAcceptanceTest : BaseAcceptanceTest() {
    @Test
    @DisplayName(&quot;사용자는 상품을 주문할 수 있다.&quot;)
    fun order() {
        val response = 뿌링클을_주문한다()
        val 응답_데이터 = 주문_응답(response)
        assertThat(응답_데이터.isSuccess).isEqualTo(true)
    }

    fun 뿌링클을_주문한다(): ExtractableResponse&amp;lt;Response&amp;gt; = invokePost(&quot;/order&quot;, 뿌링클_1마리_주문_요청())

    fun 주문_응답(response: ExtractableResponse&amp;lt;Response&amp;gt;) = response.`as`(OrderResponse::class.java)
}

fun 뿌링클_1마리_주문_요청(): OrderRequest = OrderRequest(testUserId, 1L)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 테스트 클래스가 상속하는 &lt;code&gt;BaseAcceptanceTest&lt;/code&gt;는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class BaseAcceptanceTest {
    @LocalServerPort
    val port: Int? = null

    @MockBean
    lateinit var paymentApiClient: PaymentApiClient

    @BeforeEach
    fun setup() {
        RestAssured.port = port!!

        given(paymentApiClient.payment(any(PaymentRequest::class.java)))
            .willReturn(PaymentResult(true))
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 위 코드처럼 외부 API를 Mocking할 때 Service/Adaptor를 모킹하는 경우가 많다. 그런데 한 가지 의문점이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'이게 필요한 커버리지를 정말 보장하는 테스트 코드인가..?'&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 커버리지가 100%라도 안전하지 못할 수 있는 이유도 여기서 나온다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Web Layer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Web 영역이라고 하면 아래 영역을 포함한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Request를 직렬화&lt;/li&gt;
&lt;li&gt;Response를 역직렬화&lt;/li&gt;
&lt;li&gt;네트워크 통신&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 저런 방식으로 Mocking 하면 세가지 영역 중 어느것도 커버되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Product 코드에는 아래 설정이 존재한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class ObjectMapperConfig {
    @Bean
    fun objectMapper(): ObjectMapper = ObjectMapper()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjectMapper의 Naming 전략은 현재 CamelCase이다. 즉, 외부 API와 통신도 CamelCase로 통신을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjectMapper의 Naming 전략을 변경해보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class ObjectMapperConfig {
    @Bean
    fun objectMapper(): ObjectMapper =
        ObjectMapper()
            .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 ObjectMapper가 통신할 때 SnakeCase로 통신한다. PaymentApi는 CamelCase를 사용하고 있으므로 테스트가 깨져야 정상이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc6cW3/btsIgDh18BG/vuMc1yV6pTK18mlkWjPazK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc6cW3/btsIgDh18BG/vuMc1yV6pTK18mlkWjPazK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc6cW3/btsIgDh18BG/vuMc1yV6pTK18mlkWjPazK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc6cW3%2FbtsIgDh18BG%2FvuMc1yV6pTK18mlkWjPazK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1190&quot; height=&quot;336&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 테스트는 정상적으로 통과한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApiAdaptor를 Mocking 하면서 &lt;b&gt;테스트 단계에서 더 이상 직렬화 / 비직렬화가 진행되지 않았고&lt;/b&gt;, 해당 영역에 대한 결함을 테스트 코드로 잡을 수 없게 되었기 때문이다. (거짓 음성)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WireMock&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WireMock을 사용하면 특정 빈을 모킹하지 않고도 테스트할 수 있도록 Stub Server를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 실제로 직렬화/역직렬화, 네트워크 요청까지도 테스트에 커버할 수 있다는 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;WireMock&lt;/code&gt;은 &lt;code&gt;Spring Cloud&lt;/code&gt;에서도 스프링 환경에서 WireMock 사용을 위해 라이브러리를 지원하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 해당 라이브러리의 의존성을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation(&quot;org.springframework.cloud:spring-cloud-contract-wiremock:4.1.3&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로퍼티 설정에서 API 호출 url을 아래와 같이 세팅한다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;apis:
  payment:
    url: localhost:${wiremock.server.port}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud Contract WireMock은 Stub 서버의 포트를 wiremock.server.port 에 바운드한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 외부 API( &lt;code&gt;/payment&lt;/code&gt;)를 호출했을 때 응답을 아래 폴더에 정의한다. 예시의 경우&amp;nbsp;&amp;nbsp;&lt;code&gt;test/resources/__files/payload/payment-response.json&lt;/code&gt;에 정의한다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;success&quot;: true
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에서 Mocking 서버를 설정하는 메서드를 정의한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class WireMockContext {
    companion object {
        fun setupPaymentApi() {
            stubFor(
                WireMock
                    .post(WireMock.urlMatching(&quot;/payment&quot;))
                    .willReturn(
                        WireMock
                            .aResponse()
                            .withStatus(HttpStatus.OK.value())
                            .withHeader(&quot;Content-Type&quot;, MediaType.APPLICATION_JSON_VALUE)
                            .withBodyFile(&quot;payload/payment-response.json&quot;),
                    ),
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;PaymentApiClient&lt;/code&gt;를 Mocking하는 부분을 없애고 해당 메서드를 호출하기만 하면 된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@AutoConfigureWireMock(port = 0)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class BaseAcceptanceV2Test {
    @LocalServerPort
    val port: Int? = null

    @BeforeEach
    fun setup() {
        RestAssured.port = port!!

        WireMockContext.setupPaymentApi()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제로 테스트를 실행해보면 &lt;code&gt;PaymentApiClient&lt;/code&gt;를 실제로 호출해서 요청을 한다. 그래서 직렬화 전략이 SnakeCase일 경우 아래와 같이 테스트가 실패한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;isSuccess&lt;/code&gt; 필드가 역직렬화에 실패해서 기본 값인 false가 들어가 테스트가 실패한 것이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1751&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLC7oU/btsIioKfooc/4WXO1vLrC0l55BqdzjDHW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLC7oU/btsIioKfooc/4WXO1vLrC0l55BqdzjDHW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLC7oU/btsIioKfooc/4WXO1vLrC0l55BqdzjDHW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLC7oU%2FbtsIioKfooc%2F4WXO1vLrC0l55BqdzjDHW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1751&quot; height=&quot;340&quot; data-origin-width=&quot;1751&quot; data-origin-height=&quot;340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;WireMock을 사용함으로써 직렬화 / 역직렬화 및 네트워크 통신에서의 문제도 잡을 수 있게 된 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;번외 - 통합 테스트의 범위&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말한 것은 통합테스트의 범위에 따라 다르다. 만약 직렬화/역직렬화 및 네트워크 통신을 포함한 범위를 테스트 하고자 한다면 WireMock을 사용해서 서버 자체를 Stub 하는 방식이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 고려해야 할 점은 해당 프로젝트 자체의 Input/Output도 고려해야 한다는 점이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class OrderAcceptanceTest : BaseAcceptanceTest() {
    @Test
    @DisplayName(&quot;사용자는 상품을 주문할 수 있다.&quot;)
    fun order() {
        val response = 뿌링클을_주문한다()
        val 응답_데이터 = 주문_응답(response)
        assertThat(응답_데이터.isSuccess).isEqualTo(true)
    }

    fun 뿌링클을_주문한다(): ExtractableResponse&amp;lt;Response&amp;gt; = invokePost(&quot;/order&quot;, 뿌링클_1마리_주문_요청())

    fun 주문_응답(response: ExtractableResponse&amp;lt;Response&amp;gt;) = response.`as`(OrderResponse::class.java)
}

fun 뿌링클_1마리_주문_요청(): OrderRequest = OrderRequest(testUserId, 1L)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 테스트는 Input, Output 모두 특정 Object를 사용하고 있고, 직렬화/역직렬화는 빈으로 정의된 ObjectMapper를 따를 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 ObjectMapper의 Case 전략이 바뀐다면 Product/Test 환경 모두 전략이 일치할테니 버그를 잡을 수 없고, 실제로 클라이언트나 외부 시스템에서 호출할 때는 실패할 것임에도 테스트는 성공할 것이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class OrderAcceptanceV2Test : BaseAcceptanceV2Test() {
    @Test
    fun order() {
        val response = 뿌링클을_주문한다()

        val 응답_데이터 = response.`as`(Map::class.java)
        assertThat(응답_데이터.get(&quot;is_success&quot;)).isEqualTo(true)
    }

    fun 뿌링클을_주문한다(): ExtractableResponse&amp;lt;Response&amp;gt; = invokePost(&quot;/order&quot;, 뿌링클_1마리_주문_요청())

    fun 뿌링클_1마리_주문_요청() {
        val map = HashMap&amp;lt;String, Any&amp;gt;()
        map.put(&quot;user_id&quot;, testUserId)
        map.put(&quot;product_id&quot;, 1)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 인수 테스트를 할 때는 &lt;code&gt;Map&lt;/code&gt; 등을 사용해서 Input, Output을 통신하는 데이터 그대로 사용하는 것이 더 안전한 테스트가 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ATDD를 할 때 별도의 프로덕트 코드 없이 테스트를 먼저 작성할 때도 큰 도움이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reference&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://wiremock.org/docs/stubbing/&quot;&gt;https://wiremock.org/docs/stubbing/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://memo-the-day.tistory.com/10&quot;&gt;https://memo-the-day.tistory.com/10&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Server/JUnit, Spock</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/431</guid>
      <comments>https://jaehoney.tistory.com/431#entry431comment</comments>
      <pubDate>Sat, 29 Jun 2024 14:35:38 +0900</pubDate>
    </item>
    <item>
      <title>코프링 - runBlocking 모두 제거해야 하나?</title>
      <link>https://jaehoney.tistory.com/430</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;runBlocking {} 제거해야 할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린과 코루틴을 처음 접하는 경우가 많을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린이 익숙하지 않은 프로젝트를 보면 대부분 Controller 메서드의 시작이 &lt;code&gt;runBlocking()&lt;/code&gt;이 되어있고, 사실상 코루틴은 &lt;code&gt;runBlocking()&lt;/code&gt;, &lt;code&gt;runCatching()&lt;/code&gt; 밖에 존재하지 않는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 투입된 프로젝트의 코드도 동일한 상황이었고 그랬고, &lt;b&gt;'코루틴을 사용할 때의 이점을 하나도 못누리고 있는 것은 아닐까..?&lt;/b&gt;' 하는 의심을 가지게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;runBlocking&lt;/code&gt;은 왜 문제이며, 어떻게 개선할 수 있는 지 알아보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;runBlocking&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 &lt;code&gt;runBlocking&lt;/code&gt;에 대한 docs의 일부이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Runs a new coroutine and blocks the current thread interruptibly until its completion.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 docs를 읽어보면 &lt;code&gt;runBlocking&lt;/code&gt;은 실행한 Thread를 작업이 완료할 때까지 Blocking 한다는 것을 알 수 있다. 기존의 동기코드와 동일하게 동작한다는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Problem&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 코루틴을 사용하는 이유는 병렬 프로그래밍을 쉽고 편리하게 하기 위해서이다. &lt;code&gt;runBlocking&lt;/code&gt; 만을 사용한다면 동기 코드를 더 복잡하게만 만드는 행위일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 공식 문서의 설명이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The name of &lt;code&gt;runBlocking&lt;/code&gt; means that the thread that runs it (in this case &amp;mdash; the main thread) gets &lt;b&gt;blocked&lt;/b&gt; for the duration of the call, until all the coroutines inside &lt;code&gt;runBlocking { ... }&lt;/code&gt; complete their execution. You will often see &lt;code&gt;runBlocking&lt;/code&gt; used like that at the very top-level of the application and quite rarely inside the real code, as threads are expensive resources and blocking them is inefficient and is often not desired.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 부분은 아래와 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;쓰레드는 값비싼 자원이고, 이를 차단하는 것은 일반적으로 비효율적이다. 그렇기 때문에 최상위 수준에서 사용되는 &lt;code&gt;runBlocking&lt;/code&gt;은 실제로 거의 사용되지 않는다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;runBlocking&lt;/code&gt;을 Controller method처럼 상위 레벨에서 사용하는 것은 코루틴의 이점을 활용하지 못한다는 것을 의미한다. 게다가 오히려 잘못된 사용을 야기할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시를 보자. 아래의 OrderController가 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/order&quot;)
class OrderController(
    private val orderService: OrderService
){

    @GetMapping
    fun order() = runBlocking {
        orderService.order()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 OrderController는 아래의 OrderService의 order()를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class OrderService {

    suspend fun order() = runBlocking(Dispatchers.Order) {
        delay(100)
        println(&quot;주문이 완료되었습니다.&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드를 보면 Main Thread는 어차피 Dispatchers.Order가 관리하는 쓰레드의 작업이 종료될 때까지 다른 작업을 수행할 수 없고 기다려야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Thread가 1개만 필요한 상황에서 불필요하게 Thread를 2개 사용하는 상황이 된 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring MVC (+Webflux)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Webflux가 Controller의 suspend 메서드를 지원하는 것은 익히 알고 있다. Spring Webflux를 사용한다면 Controller 메서드에서는 &lt;code&gt;runBlocking&lt;/code&gt; 메서드를 호출할 필요가 없다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/hello&quot;)
class HelloController {
    private val log = logger&amp;lt;HelloController&amp;gt;()

    @GetMapping
    suspend fun hello() {
        log.info(&quot;context: {}&quot;, coroutineContext)
        log.info(&quot;thread: {}&quot;, Thread.currentThread().name)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 컨트롤러에 요청을 보내보면 아래 로그가 찍힌다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;34:31 [reactor-http-nio-2] - context:
    [Context1{reactor.onDiscard.local= reactor.core.publisher.Operators$$Lambda/0x0000000123657b60@7bbfcea9}, 
    MonoCoroutine{Active}@35a52e8a, Dispatchers.Unconfined]
34:31 [reactor-http-nio-2] - thread: reactor-http-nio-2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 &lt;code&gt;spring-web&lt;/code&gt; 라이브러리를 보면 suspend 함수에 대해서 아래와 같이 &lt;code&gt;invoke&lt;/code&gt;를 통해 처리하고 있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPqDI0/btsH1MLi6dq/ZLjpSvwfqnJ2PN0Sc3yND1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPqDI0/btsH1MLi6dq/ZLjpSvwfqnJ2PN0Sc3yND1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPqDI0/btsH1MLi6dq/ZLjpSvwfqnJ2PN0Sc3yND1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPqDI0%2FbtsH1MLi6dq%2FZLjpSvwfqnJ2PN0Sc3yND1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;534&quot; height=&quot;315&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;invokeSuspendingFunction()&lt;/code&gt;는 내부적으로 &lt;code&gt;Mono&lt;/code&gt;로 감싸서 함수를 처리하게 된다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baKqu1/btsHZLAROD5/wba3RKmTUmi2jR2mGJ744k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baKqu1/btsHZLAROD5/wba3RKmTUmi2jR2mGJ744k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baKqu1/btsHZLAROD5/wba3RKmTUmi2jR2mGJ744k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaKqu1%2FbtsHZLAROD5%2Fwba3RKmTUmi2jR2mGJ744k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;616&quot; height=&quot;282&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Spring Webflux에서 쓰레드를 관리해주는 역할을 책임지는 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Spring MVC&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Spring MVC에서는 어떻게 될까..? 요청을 보내보면 아래의 에러가 발생한다!&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;java.lang.ClassNotFoundException: org.reactivestreams.Publisher
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) ~[na:na]
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) ~[na:na]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Reactive Stream, Reactor 등이 필요한 비동기 환경으로의 변경 없이 컨트롤러 메서드에서 &lt;code&gt;suspend&lt;/code&gt;를 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보면 당연한 결과다. Spring MVC는 thread-per-request 모델이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각 및 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring WebFlux가 아닌 Spring MVC 상황에서는 비즈니스 로직에서의 &lt;code&gt;suspend&lt;/code&gt; 호출을 위해 &lt;code&gt;runBlocking&lt;/code&gt;은 존재할 수 밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC와 Coroutine은 다소 Fit 하지 않는(어울리지 않는) 느낌이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 병렬 프로그래밍을 제대로 하고 싶다면 Spring Webflux 로의 전환을 추천한다. 대부분은 Spring Webflux를 고려해서 suspend 처리를 하는 것이 더 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 단순히 '&lt;code&gt;runBlocking&lt;/code&gt;은 사용하면 안돼'라고 생각해서 Controller에 다른 쓰레드를 할당하는 등의 옵션은 Spring MVC의 매커니즘을 손상할 수 있다. 주어진 환경이 Spring MVC 라면 꼭 Controller Method가 아니더라도 &lt;code&gt;runBlocking&lt;/code&gt;이 필요하고 자연스러울 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구조화된 동시성 + 비동기 처리 등을 위 &lt;code&gt;suspend&lt;/code&gt; 메서드의 호출은 바람직하기 때문에 &lt;code&gt;suspend&lt;/code&gt; 메서드는 필요하다.&lt;/li&gt;
&lt;li&gt;Spring MVC 환경에서 해당 메서드를 호출하기 위해 &lt;code&gt;runBlocking&lt;/code&gt;을 사용하는 것은 자연스럽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 코드 내 대부분의 &lt;code&gt;runBlocking&lt;/code&gt;을 제거하기 위해서는 Spring MVC에서 WebFlux로의 전환이 필요하다는 것이다. WebFlux로 전환하면 Controller에서 suspend 키워드를 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC 환경에서는 &lt;code&gt;runBlocking&lt;/code&gt;이 필요할 수 있으며 반드시 제거해야 한다고 보기는 어렵다. 단, 쓰레드를 블락한다는 사실을 주의해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kotlinlang.org/docs/coroutines-basics.html&quot;&gt;https://kotlinlang.org/docs/coroutines-basics.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Server/Spring</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/430</guid>
      <comments>https://jaehoney.tistory.com/430#entry430comment</comments>
      <pubDate>Sun, 16 Jun 2024 17:26:08 +0900</pubDate>
    </item>
    <item>
      <title>Spring - Curcuit Breaker 이해하기!</title>
      <link>https://jaehoney.tistory.com/429</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Cloud&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud는 안정적인 Micro Service Architecture를 만들고 외부 환경에 대해 신경을 할애하지 않고 내부 로직에만 집중할 수 있게 도와주는 라이브러리이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud에서 지원하는 기능 중 Curcuit breaker에 대해 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Circuit breaker&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Circuit breaker는 전기 회로의 차단기와 같은 역할을 하는 디자인 패턴을 말한다. 즉, CurcuitBreaker는 기술이 아닌 패턴을 말한다. 주요 목적은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부의 장애를 격리하고 시스템 안정성을 유지할 수 있다.&lt;/li&gt;
&lt;li&gt;장애 복구 시간을 확보할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 서비스의 과부하나 장애가 발생했을 때 복구가 될 때까지 추가적인 요청을 차단해서 시스템의 안정성을 유지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Reactive Systems의 Resilient(복원력) 지원한다고 보면 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud는 Spring Cloud Circuit Breaker라는 라이브러리를 지원한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Cloud Circuit Breaker의 구현체로는 Resilience4j와 Spring Retry, Sentinel을 제공한다.&lt;/li&gt;
&lt;li&gt;해당 포스팅에서는 Resilience4j를 활용하는 예제에 대해서 다룬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Circuit breaker 상태&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CircuitBreaker는 FSM을 통해 구현한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XpklD/btsHR8hGOx1/GMXWoCdbykOwEib4L70Vz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XpklD/btsHR8hGOx1/GMXWoCdbykOwEib4L70Vz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XpklD/btsHR8hGOx1/GMXWoCdbykOwEib4L70Vz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXpklD%2FbtsHR8hGOx1%2FGMXWoCdbykOwEib4L70Vz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;426&quot; height=&quot;164&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Circuit breaker는 3가지 상태가 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Closed: 정상적으로 요청을 받을 수 있는 상태 (Open으로 상태 이동 가능)&lt;/li&gt;
&lt;li&gt;Open: Curcuit breaker가 작동하여 목적지로 가는 트래픽, 요청을 막고 fallback을 반환 (Half Open으로 상태 이동 가능)&lt;/li&gt;
&lt;li&gt;Half Open: 트래픽을 조금씩 흘려보고 Open을 유지할지 Closed로 변경할 지 결정(Open, Closed로 상태 이동 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 2가지 특별한 상태가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Disabled: 항상 호출을 허용&lt;/li&gt;
&lt;li&gt;Forced Open: 항상 호출을 거부&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Closed&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Closed 상태는 가장 기본적인 상태이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;들어오는 모든 요청을 대상 메서드, 서비스에 전달&lt;/li&gt;
&lt;li&gt;서비스에 전달 후 응답이 느리거나 error가 발생한다면 fallback을 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Open&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Open 상태에서는 기존에 호출하던 대상을 절대로 더 이상 호출하지 않는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fallback을 실행하여 반환&lt;/li&gt;
&lt;li&gt;호출하는 서비스를 보호하고 복구할 수 있는 시간 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Half Open&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Open 상태에서 Half open 상태로 바뀌면서 Circuit breaker는 State transition 이벤트를 발행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Open 상태에서 Half open으로 가기 위해서는 2가지 방법이 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Circuit breaker API를 사용해서 직접 변경&lt;/li&gt;
&lt;li&gt;옵션을 지정하여 특정 시간이 지나면 자동으로 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령 60초에 한번씩 Open -&amp;gt; Half Open으로 자동 변경을 하게 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Half Open 상태가 되면 Close가 되기 위해 아래의 역할을 수행한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Half open 상태에서 N번의 동작에 대한 측정 결과를 저장한다.&lt;/li&gt;
&lt;li&gt;Failure rate가 임계점보다 높거나 같다면 Open으로, 낮다면 Close로 전환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Sliding window&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Circuit breaker는 대상이 되는 서비스 호출의 결과를 Sliding window 형태로 저장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b80abF/btsHR38BCOT/GaUkrG7drDM0slV18knpfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b80abF/btsHR38BCOT/GaUkrG7drDM0slV18knpfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b80abF/btsHR38BCOT/GaUkrG7drDM0slV18knpfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb80abF%2FbtsHR38BCOT%2FGaUkrG7drDM0slV18knpfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;472&quot; height=&quot;198&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CurcuitBreaker는 &lt;code&gt;Count-based sliding window&lt;/code&gt;와 &lt;code&gt;Time-based sliding window&lt;/code&gt;가 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Count-based sliding window:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;N개 만큼의 측정 결과를 저장&lt;/li&gt;
&lt;li&gt;1개의 요청마다 실패율 계산 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Time-based sliding window:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최근 N초의 실패율을 계산한다.&lt;/li&gt;
&lt;li&gt;Count-based sliding window의 성능 문제를 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전체 대비 실패 비율을 failure rate라고 한다.&lt;/li&gt;
&lt;li&gt;Failure rate가 설정한 임계치에 도달하는 순간 Open 상태로 변경된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Resilience4j&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;48&quot; data-origin-height=&quot;48&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Njxkh/btsHSS6ci8x/cdYuyQeDLOiyahljRZKlSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Njxkh/btsHSS6ci8x/cdYuyQeDLOiyahljRZKlSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Njxkh/btsHSS6ci8x/cdYuyQeDLOiyahljRZKlSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNjxkh%2FbtsHSS6ci8x%2FcdYuyQeDLOiyahljRZKlSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;48&quot; height=&quot;48&quot; data-origin-width=&quot;48&quot; data-origin-height=&quot;48&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resilience4j는 Java에서 Curcuit breaker를 지원하는 러이브러리이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle에 아래 의존을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;dependencies {
    // reactive
    implementation(&quot;org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j&quot;)
    // non-reactive
    // implementation(&quot;org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j&quot;)
}

dependencyManagement {
    imports {
        mavenBom(&quot;org.springframework.cloud:spring-cloud-dependencies:2021.0.8&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CircuitBreakerConfig&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 위 의존을 추가했다면 AutoConfiguration이 동작한다. (&lt;code&gt;spring.cloud.circuitbreaker.resilience4j.enabled&lt;/code&gt;를 false로 설정하면 Off 할 수 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 예시로 작성한 빈 기반 커스텀 설정이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
public Customizer&amp;lt;ReactiveResilience4JCircuitBreakerFactory&amp;gt; autoHalf() {
    var cbConfig = CircuitBreakerConfig.custom()
            .failureRateThreshold(50)
            .slidingWindowSize(100)
            .enableAutomaticTransitionFromOpenToHalfOpen()
            .waitDurationInOpenState(Duration.ofSeconds(5))
            .build();

    var targets = new String[]{&quot;money&quot;};
    return factory -&amp;gt; {
        factory.addCircuitBreakerCustomizer(
                getEventLogger(), targets);
        factory.configure(builder -&amp;gt; {
            builder.circuitBreakerConfig(cbConfig);
        }, targets);
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 프로퍼티가 의미하는 것은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;failureRateThreshold: 장애로 인해 Open으로 상태를 전환할 FailureRate의 임계치&lt;/li&gt;
&lt;li&gt;slidingWindowSize: 최근 몇 개의 요청을 측정할 지&lt;/li&gt;
&lt;li&gt;enableAutomaticTransitionFromOpenToHalfOpen: 자동으로 Open -&amp;gt; Half open 변경을 사용할 지&lt;/li&gt;
&lt;li&gt;waitDurationInOpenState: Open에서 몇 초 뒤 Half Open으로 상태를 변경할 지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에도 다양한 설정이 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ignoreExceptions: 서비스에서 Exception을 던질 경우 허용할 Excceptions 목록&lt;/li&gt;
&lt;li&gt;permittedNumberOfCallsInHalfOpenStatus: Half open 상태에서 허용할 호출 수&lt;/li&gt;
&lt;li&gt;maxWaitDurationInHalfOpenStatus: Hlf open 상태에서 대기할 수 있는 최대 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TimeLimitConfig&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 TimeLimitConfig를 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class Resilience4JConfig {
    @Bean
    public Customizer&amp;lt;Resilience4JCircuitBreakerFactory&amp;gt; globalCustomConfiguration(){
        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .failureRateThreshold(4)
                .waitDurationInOpenState(Duration.ofMillis(1000))
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
                .slidingWindowSize(2)
                .build();

        TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
                .timeoutDuration(Duration.ofSeconds(4))
                .build();

        return factory -&amp;gt; factory.configureDefault(id -&amp;gt; new Resilience4JConfigBuilder(id)
                .timeLimiterConfig(timeLimiterConfig)
                .circuitBreakerConfig(circuitBreakerConfig)
                .build()
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TimeLimitConfig는 아래 프로퍼티를 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cancelRunningFuture: Future가 진행 중인 경우 Cancel 여부&lt;/li&gt;
&lt;li&gt;timeOutDaration: Timeout 기준 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;yml 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CircuitBreakerConfig나 TimeLimiterConfig는 아래와 같이 yml 설정을 사용할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;resilience4j:
  circuitbreaker:
    instances:
      order:
        sliding-window-size: 1
        failure-rate-threshold: 75
        automatic-transition-from-open-to-half-open-enabled: false
        wait-duration-in-open-state: 5s
        permitted-number-of-calls-in-half-open-state: 6
        ignore-exceptions:
          - java.lang.ArithmeticException
        max-wait-duration-in-half-open-state: 30s
        slow-call-rate-threshold: 50
        slow-call-duration-threshold: 1s
      payment:
        sliding-window-size: 4
        failure-rate-threshold: 50
        automatic-transition-from-open-to-half-open-enabled: true
        wait-duration-in-open-state: 5s
      shipment:
        sliding-window-size: 4
        failure-rate-threshold: 50
        automatic-transition-from-open-to-half-open-enabled: true
        wait-duration-in-open-state: 3s
        permitted-number-of-calls-in-half-open-state: 6
    configs:
      default:
        register-health-indicator: true
        sliding-window-size: 4
        failure-rate-threshold: 75
      mini-window-size:
        sliding-window-size: 4
  timelimiter:
    instances:
      order:
        timeout-duration: 1s
        cancel-running-future: true
      payment:
        timeout-duration: 1s&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Circuit Breaker Group&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 yml은 Circuit Breaker Group 별로 구성이 되어있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Circuit Breaker Instance를 만들면서 Group을 지정할 수 임ㅆ다.&lt;/li&gt;
&lt;li&gt;아래 순서로 설정을 적용하게 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Instance id와 일치&lt;/li&gt;
&lt;li&gt;Group과 정확히 일치&lt;/li&gt;
&lt;li&gt;default 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CircuitBreaker 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 CircuitBreaker를 활용한 예시 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주입받은 &lt;code&gt;CircuitBreakerFactory&lt;/code&gt;를 사용해서 메서드를 실행할용수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
public class DemoControllerService {
    private ReactiveCircuitBreakerFactory cbFactory;
    private WebClient webClient;


    public DemoControllerService(WebClient webClient, ReactiveCircuitBreakerFactory cbFactory) {
        this.webClient = webClient;
        this.cbFactory = cbFactory;
    }

    public Mono&amp;lt;String&amp;gt; slow() {
        return webClient.get().uri(&quot;/slow&quot;).retrieve().bodyToMono(String.class).transform(
            it -&amp;gt; cbFactory.create(&quot;slow&quot;).run(it, throwable -&amp;gt; return Mono.just(&quot;fallback&quot;)));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 서비스 메서드에 해당과 같은 코드가 들어간다면 비즈니스 로직에 집중하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 AOP 기반 동작을 위한 &lt;code&gt;@CircuitBreaker&lt;/code&gt; 애노테이션을 제공한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
class OrderController {

    @GetMapping(&quot;/order&quot;)
    @CircuitBreaker(name = &quot;order&quot;, fallbackMethod = &quot;orderFallback&quot;)
    fun order(): String {
        throw RuntimeException(&quot;주문 시스템 장애 상황&quot;)
        return &quot;주문이 완료되었습니다.&quot;
    }

    fun orderFallback(e: Throwable): String {
        return &quot;잠시 후 다시 시도해주세요. cause: ${e.message}&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 API Call을 해보면 일정 실패 이후부터 아래와 같이 핸들링 되는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;379&quot; data-origin-height=&quot;157&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FFvAq/btsHSsz98Hc/EUKs4BL8WLCOiHKwcUeAZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FFvAq/btsHSsz98Hc/EUKs4BL8WLCOiHKwcUeAZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FFvAq/btsHSsz98Hc/EUKs4BL8WLCOiHKwcUeAZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFFvAq%2FbtsHSsz98Hc%2FEUKs4BL8WLCOiHKwcUeAZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;379&quot; height=&quot;157&quot; data-origin-width=&quot;379&quot; data-origin-height=&quot;157&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Timeout 테스트도 아래와 같이 진행해봤다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
class OrderController {

    @GetMapping(&quot;/order&quot;)
    @CircuitBreaker(name = &quot;order&quot;, fallbackMethod = &quot;orderFallback&quot;)
    fun order(): String {
        println(&quot;Order 요청&quot;)
        Thread.sleep(3000)
        return &quot;주문이 완료되었습니다.&quot;
    }

    fun orderFallback(e: Throwable): String {
        println(&quot;Fallback 호출&quot;)
        return &quot;잠시 후 다시 시도해주세요. cause: ${e.message}&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 주문이 잘 진행되다가 TimeOut의 발생 Rate가 설정한 수치가 넘어가면 &lt;code&gt;order()&lt;/code&gt; 자체를 실행시지 않고 바로 Fallback을 호출한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQABU2/btsHSx85hsI/JK6qtUncBG4YBAm17SaEq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQABU2/btsHSx85hsI/JK6qtUncBG4YBAm17SaEq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQABU2/btsHSx85hsI/JK6qtUncBG4YBAm17SaEq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQABU2%2FbtsHSx85hsI%2FJK6qtUncBG4YBAm17SaEq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;486&quot; height=&quot;288&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 Half Open 전환 설정을 하면 특정 개수만큼만 order 호출을 허용하면서 자동으로 복구할 수 있을 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-circuitbreaker&quot;&gt;https://docs.spring.io/spring-cloud-circuitbreaker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Server/Spring MSA</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/429</guid>
      <comments>https://jaehoney.tistory.com/429#entry429comment</comments>
      <pubDate>Sat, 8 Jun 2024 11:01:54 +0900</pubDate>
    </item>
    <item>
      <title>빠른 기능 오픈 vs 견고한 프로덕트 - feat. 일단 출시.. 코드 품질은 집착인가?</title>
      <link>https://jaehoney.tistory.com/428</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 이직을 하고 업무를 하면서 &lt;b&gt;가장 큰 고민&lt;/b&gt;이 있다. 코드 리뷰할 때 특히 고민되는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하면서 아래의 선택 중 어떤 것을 선택할 지에 대한 문제이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;우선 기능이 돌아가게만 만들어서 &lt;b&gt;빠르게 오픈하고 점진적으로 개선&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;초기에 프로젝트 설계나 테스트 코드 등에 자원을 할당&lt;/b&gt;한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재까지의 나의 업무로 보면은 2번을 선택해왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;철저한 테스트와 코드 아키텍처가 추후 유지보수 작업 공수를 줄이고, 견고하고 유연한 서비스가 회사와 서비스에 대한 이미지나 사용자 경험을 증대시킨다고 생각했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 핫하다는 회사나 일을 잘한다는 팀은 1번을 선호하는 것 같기도 하다. 실제로 충분히 합리적이라고 생각한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사업 vs 서비스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사업 담당자의 추구&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기능 추가&lt;/li&gt;
&lt;li&gt;기능 변경&lt;/li&gt;
&lt;li&gt;할인, 프로모션 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서비스 운영자의 추구&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리팩토링&lt;/li&gt;
&lt;li&gt;코드 정리&lt;/li&gt;
&lt;li&gt;테스트 코드 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 서비스를 개발하다보면 이러한 이해관계가 자주 충돌하는 것 같다. &lt;b&gt;사업 담당자의 실적&lt;/b&gt;은 보통 &lt;b&gt;주어진 기간 내에&lt;/b&gt; &lt;b&gt;고객을 얼마나 유치하고 실적을 달성하는 지&lt;/b&gt;로 측정될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서비스 관점&lt;/b&gt;에서는 기능 추가와 기능 변경 만으로 유지보수를 하게되면 장애 발생이 증가하고 장애 대응도 늦어지고 회사와 서비스가 신뢰를 잃게 되어 큰 범위에서는 피해를 끼치게 될 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내 생각&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 의견 모두 충분히 합리적이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 주어진 과제를 성실히 이행했을 때 말이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빠른 기능 오픈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠른 오픈과 기능이 우선되어도 된다. 단, &lt;b&gt;장애에 최대한 빠르게 대처&lt;/b&gt;할 수 있어야 한다. &lt;b&gt;잦은 장애나 늦은 대응으로 회사의 이미지에 타격을 주어서는 곤란하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;틈틈히 &lt;b&gt;리팩토링&lt;/b&gt;도 해야 한다. 코드는 집과 같다. &lt;b&gt;청소를 하지 않으면 나중에는 결국 살기가 어려워진다.&lt;/b&gt; 그래서 당장 외부에 티가 안나더라도 틈틈이 청소를 해야하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 지표를 보면 주기적인 리팩토링이 점진적으로 유지보수에 필요한 시간이 감소된다는 사실을 알 수 있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;329&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nEdVB/btsHvsGlBxh/4Wur70tJDLuy7IGK35capk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nEdVB/btsHvsGlBxh/4Wur70tJDLuy7IGK35capk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nEdVB/btsHvsGlBxh/4Wur70tJDLuy7IGK35capk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnEdVB%2FbtsHvsGlBxh%2F4Wur70tJDLuy7IGK35capk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;541&quot; height=&quot;288&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;329&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;코드 아키텍처가 나쁘면 기능 수정이나 버그 fix가 &lt;b&gt;또 다른 버그를 만들어 낼 가능성&lt;/b&gt;이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠른 기능 오픈을 선택한다면 아래의 과제가 선행 되어야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장애나 결함에 민첩하게 대응&lt;/b&gt;해야 한다.&lt;/li&gt;
&lt;li&gt;장애나 결함이 무서워서 방치하면 안되고, &lt;b&gt;주기적인 리팩토링과 정리&lt;/b&gt;가 되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;견고한 프로덕트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;견고한 프로덕트를 개발할 때는 아래의 부분에 대한 확인이 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정말 견고해야 하는 지 &lt;b&gt;고심&lt;/b&gt;해봐야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일정에 무리가 가선 안된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로덕트를 오래 유지할 필요가 없을 수도 있고, 사용자 경험으로 부터 멀리 있을 수도 있고, 전혀 리스키하지 않을 수도 있다. 그러한 프로젝트에 테스트 커버리지를 100% 이상을 달성하는 것은 돈을 벌어야하는 입장으로써 이해하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항에 따라 천차만별이다. 하지만 일반적으로는 코드의 질로 인해 &lt;b&gt;일정이 지체 되어선 안된다.&lt;/b&gt; 적시 오픈을 해서 시장 점유를 하는 것도 중요하고, 고객과의 약속 및 Stakeholder 의 업무 스케줄도 존중되어야 한다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;코드 품질이 사업에 미치는 여향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드 품질이 사업에 미치는 영향&lt;/b&gt;이라는 내용의 최범균님의 영상이 있는데 고품질 코드가 사업과 완전히 상반된 내용이 아님을 데이터를 통해 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 내용의 일부이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzCDBa/btsHtisdB1e/KlAQoeGga30O2Ack0QLET0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzCDBa/btsHtisdB1e/KlAQoeGga30O2Ack0QLET0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzCDBa/btsHtisdB1e/KlAQoeGga30O2Ack0QLET0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdzCDBa%2FbtsHtisdB1e%2FKlAQoeGga30O2Ack0QLET0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;501&quot; height=&quot;244&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1119&quot; data-origin-height=&quot;627&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buy0HY/btsHs2pz3jM/2U8i1xMT7mboypv0xi5Kbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buy0HY/btsHs2pz3jM/2U8i1xMT7mboypv0xi5Kbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buy0HY/btsHs2pz3jM/2U8i1xMT7mboypv0xi5Kbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbuy0HY%2FbtsHs2pz3jM%2F2U8i1xMT7mboypv0xi5Kbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;501&quot; height=&quot;281&quot; data-origin-width=&quot;1119&quot; data-origin-height=&quot;627&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 고민을 가지고 있다면, 영상을 꼭 한번 보기를 권해드린다!&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=lcvZ9kBn2_M&quot;&gt;https://www.youtube.com/watch?v=lcvZ9kBn2_M&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;현재의 나&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로는 &lt;code&gt;빠른 오픈:견고한 프로덕트&lt;/code&gt;에서 중요하게 여기는 비율로 &lt;code&gt;48:52&lt;/code&gt; 정도를 주고 있는 것 같다. 장애가 두려워서 인 것 같기도 하고, 오픈한 기능을 변경하는 것도 두려워서 인 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정 준수를 위해 &lt;b&gt;업무 시간을 늘려서&lt;/b&gt;&lt;s&gt;(야근)&lt;/s&gt; 일정과의 Gap을 줄이고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에는 내가 중요하게 여기는 비율이 바뀔 수도 있다고 생각한다. 지속적으로 고민을 해봐야겠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://m.yes24.com/Goods/Detail/125921718&quot;&gt;https://m.yes24.com/Goods/Detail/125921718&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=Zzylm4Xkfc8&quot;&gt;https://www.youtube.com/watch?v=Zzylm4Xkfc8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=lcvZ9kBn2_M&quot;&gt;https://www.youtube.com/watch?v=lcvZ9kBn2_M&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Etc./개발 일기</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/428</guid>
      <comments>https://jaehoney.tistory.com/428#entry428comment</comments>
      <pubDate>Sun, 19 May 2024 11:58:17 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] - 적절한 Scope Function 선택하기!</title>
      <link>https://jaehoney.tistory.com/427</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EoS5K/btsHblvVNXm/w0THGBNlTQHMh3BhBbeVK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EoS5K/btsHblvVNXm/w0THGBNlTQHMh3BhBbeVK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EoS5K/btsHblvVNXm/w0THGBNlTQHMh3BhBbeVK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEoS5K%2FbtsHblvVNXm%2Fw0THGBNlTQHMh3BhBbeVK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;466&quot; height=&quot;233&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin 표준 라이브러리에서 객체의 컨텍스트 안에서 특정 블록의 코드를 실행하는 것이 목적인 함수가 포함되어 있다. Kotlin에서는 &lt;b&gt;Scope Functions&lt;/b&gt;을 제공하고 목적에 맞게 선택하는 것을 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 대부분 만능에 가까운 &lt;code&gt;let&lt;/code&gt;을 사용했고, Scope Function을 선택하는 기준을 모르고 있었다. 해당 포스팅은 공식문서 기반으로 Scope Function을 선택하는 기준에 대한 내용이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spec&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 각 &lt;b&gt;Scope Function&lt;/b&gt;에 대해 정리한 것이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;Function&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;참조 객체&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;반환값&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;확장함수 여부&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;let&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;it&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Lambda result&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;run&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;this&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Lambda result&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;code&gt;run&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;-&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;Lambda result&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;N: 객체 Context 밖에서 실행된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;code&gt;with&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;code&gt;this&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;Lambda result&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;N: 객체 Context를 인수로 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;apply&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;this&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Context object&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;also&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;it&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Context object&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 용도&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에서 정의한 각 Scope Function의 용도는 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;let:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;nullable이 아닌 객체에 대한 람다 실행&lt;/li&gt;
&lt;li&gt;로컬 볌위의 변수로 표현식을 도입한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;apply
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체를 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;nbsp;run&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체를 생성하고, 특정 동작을 수행한다.&lt;/li&gt;
&lt;li&gt;표현식이 필요한 동작 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;also
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부가적인 효과&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;with
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체의 함수 그룹핑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scope Function을 사용하면 코드가 간결해질 수 있고, Kotlin을 잘 사용하는 듯한 느낌이 들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 공식문서에서는 &lt;b&gt;Scope Function은 코드를 읽기 어렵게&lt;/b&gt; 만들고, 현재 Context의 객체와 &lt;code&gt;this&lt;/code&gt;, &lt;code&gt;it&lt;/code&gt;의 &lt;b&gt;혼동으로 인한 오류를 야기&lt;/b&gt;할 수 있다고 한다. Scope Function의 &lt;b&gt;중첩은 피하고 연결 시 충분히 주의&lt;/b&gt;를 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린의 Scope Function의 가장 큰 2개의 차이는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Context Object&lt;/li&gt;
&lt;li&gt;Return Value&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 2가지로&amp;nbsp;&lt;b&gt;Scope Function&lt;/b&gt;을 선택하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기준 1. Context Object&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scope Function에 전달된 람다 내에서 Context Object에는 Reference로 접근할 수 있는 방법을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Scope Function에 따라 아래 두 가지 방법이 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Lambda receiver - this&lt;/li&gt;
&lt;li&gt;Lambda argument - it&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    val str = &quot;Hello&quot;

    str.run {
        println(&quot;The string's length: $length&quot;)
    }

    str.let {
        println(&quot;The string's length is ${it.length}&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;run&lt;/code&gt;의 경우 Lambda receiver를 사용하고 있고, &lt;code&gt;let&lt;/code&gt;의 경우에는 Lambda argument를 사용하고 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Lambda receiver&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;run&lt;/code&gt;, &lt;code&gt;with&lt;/code&gt;, &lt;code&gt;apply&lt;/code&gt;에서는 Lambda receiver를 사용해서 Context Object에 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lambda Receiver의 경우에는 대부분 &lt;code&gt;this&lt;/code&gt;를 생략해서 코드를 더 짧게 만들 수 있다. 그래서 외부 객체와의 구별에 주의해야 한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val adam = Person(&quot;Adam&quot;).apply { 
    age = 20
    city = &quot;London&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 객체의 기능을 호출하거나 필드에 값을 할당하는 동작에서는 Lambda receiver를 사용하는 것이 좋다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Lambda argument&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;let&lt;/code&gt;, &lt;code&gt;also&lt;/code&gt;의 경우는 Lambda argument를 사용한다. 인수의 이름은 지정할 수 있으며 기본적으로 &lt;code&gt;it&lt;/code&gt;이 사용된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun getRandomInt(): Int {
    return Random.nextInt(100).also {
        writeToLog(&quot;getRandomInt() generated value $it&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 자주 사용되거나 특정 함수의 인수로 사용될 때는 Lambda argument를 사용하는 것을 권장한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기준 2. Return Value&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lambda receiver를 사용할 지 Lambda argument를 사용할 지 정했으면 Return 값에 맞게 선택하면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;apply&lt;/code&gt;, &lt;code&gt;also&lt;/code&gt;는 Context Object를 반환한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;let&lt;/code&gt;, &lt;code&gt;run&lt;/code&gt;, &lt;code&gt;with&lt;/code&gt;은 Lambda Result를 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scope Function을 선택을 할 때는 첫 번째로 Context Object를 필요에 따라 선택하고, 이후에 Retrun Value를 선택하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;let&lt;/code&gt;의 경우 대부분 모든 상황에서 사용할 수 있다. 그렇지만 &lt;b&gt;적절한 Scope Function을 선택&lt;/b&gt;하면 코드의 가독성을 증대시키고 혼동을 줄일 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드 예시&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;let&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;let은 Lambda argument를 사용하고 Lambda result를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;let은 특히나 거의 대부분의 용도로 사용할 수 있기에 더 주의해야 한다. let은 아래와 같이 Lambda argument가 특정 함수의 인자로 사용할 경우 유용하게 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val numbers = mutableListOf(&quot;one&quot;, &quot;two&quot;, &quot;three&quot;, &quot;four&quot;, &quot;five&quot;)
numbers.map { it.length }.filter { it &amp;gt; 3 }.let(::println)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;let은 인수가 null이 아닌 경우에 사용하길 권장한다. 그래서 Safe call operator &lt;code&gt;?.&lt;/code&gt;와 같이 사용하는 경우가 많다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val str: String? = &quot;Hello&quot;
val length = str?.let { 
    println(&quot;let() called on $it&quot;)        
    processNonNullString(it)
    it.length
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;with&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;with은 Lambda receiver를 사용하고, Lambda result를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;with&lt;/code&gt;은 반환된 결과를 사용할 필요가 없을 때 사용하기를 권장한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val numbers = mutableListOf(&quot;one&quot;, &quot;two&quot;, &quot;three&quot;)
val firstAndLast = with(numbers) {
    &quot;The first element is ${first()},&quot; +
    &quot; the last element is ${last()}&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;with&lt;/code&gt;는 &lt;code&gt;with this object, do the following&lt;/code&gt;의 의미를 가지고 있다. 즉, object를 사용해서 후속 작업을 하는 경우에 유용하다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;run&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;run은 Lambda receiver를 사용하고, Lambda result를 반환한다. with와 매우 유사하지만 run은 확장 함수라는 차이가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;run&lt;/code&gt;은 객체를 초기화하고 연산 결과를 반환할 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val service = MultiportService(&quot;https://example.kotlinlang.org&quot;, 80)

val result = service.run {
    port = 8080
    query(prepareRequest() + &quot; to port $port&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;apply&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apply는 Lambda receiver를 사용하고, 해당 객체를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;apply&lt;/code&gt;는 값을 반환하지 않고, receiver를 위주로 동작하는 코드에서 사용하길 권장한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val adam = Person(&quot;Adam&quot;).apply {
    age = 32
    city = &quot;London&quot;        
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;also&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;also는 &lt;code&gt;Lambda argument&lt;/code&gt;를 사용하고, 해당 객체를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;also&lt;/code&gt;는 인수에 대한 추가적인 동작이 있을 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val numbers = mutableListOf(&quot;one&quot;, &quot;two&quot;, &quot;three&quot;)
numbers
    .also { println(&quot;The list elements before adding new one: $it&quot;) }
    .add(&quot;four&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apply와 차이는 Context Object가 Receiver(&lt;code&gt;this&lt;/code&gt;)인지 Argument(&lt;code&gt;it&lt;/code&gt;)인지의 차이이며, 앞서 설명했듯 객체의 필드 위주로 사용되면 전자, 해당 객체를 입력으로 동작을 시키는 경우 후자를 선택하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;번외 - takeIf, takeUnless&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scope Functions 이외에도 표준 라이브러리에 &lt;code&gt;takeIf&lt;/code&gt;와 &lt;code&gt;takeUnless&lt;/code&gt;가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;takeIf&lt;/code&gt;나 &lt;code&gt;takeUnless&lt;/code&gt;를 사용하면 &lt;code&gt;특정 조건에 맞는 데이터 또는 NULL&lt;/code&gt;을 반환합니다. 즉, 반환 타입은 &lt;code&gt;XClass?&lt;/code&gt;가 되는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val number = Random.nextInt(100)

val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println(&quot;even: $evenOrNull, odd: $oddOrNull&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 객체에 대한 &lt;code&gt;filter&lt;/code&gt;라고 생각하면 된다. 주로 아래와 같이 &lt;code&gt;?.let&lt;/code&gt;과 같이 사용하는 경우가 많다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun displaySubstringPosition(input: String, sub: String) {
    input.indexOf(sub).takeIf { it &amp;gt;= 0 }?.let {
        println(&quot;The substring $sub is found in $input.&quot;)
        println(&quot;Its start position is $it.&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kotlinlang.org/docs/scope-functions.html&quot;&gt;https://kotlinlang.org/docs/scope-functions.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Language/Kotlin</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/427</guid>
      <comments>https://jaehoney.tistory.com/427#entry427comment</comments>
      <pubDate>Mon, 6 May 2024 18:44:59 +0900</pubDate>
    </item>
    <item>
      <title>Reactive Streams를 테스트하는 방법 (reactor-test 라이브러리)</title>
      <link>https://jaehoney.tistory.com/425</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactive Programming을 사용할 때 Project Reactor를 주로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor는 비동기로 동작하기 때문에 일반적인 테스트 방식으로 검증하기 어렵다. 그래서 &lt;code&gt;Reactor-test&lt;/code&gt; 라이브러리를 제공한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dependency&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 예시 동작을 위해서 아래와 같은 Dependency를 추가해야한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;testImplementation 'io.projectreactor:reactor-test:3.6.5'&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reactive Streams 테스트가 어려운 이유&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 강제 동기화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 테스트 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void test() {
    // given
    var expected = IntStream.range(0, 10).boxed()
        .collect(Collectors.toList());

    // when
    Flux&amp;lt;Integer&amp;gt; result = Flux.range(0, 10)
        .delayElements(Duration.ofSeconds(1));

    // then
    assertIterableEquals(expected, result.collectList().block());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트의 통과를 위해 &lt;code&gt;result.collectList().block()&lt;/code&gt;와 같은 비동기 코드의 강제 동기화가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 코드의 이점을 못살리는 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 다양한 테스트가 어려움&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 테스트 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void test() {
    // when
    Flux&amp;lt;Integer&amp;gt; result = Flux.create(sink -&amp;gt; {
        for (int i = 0; i &amp;lt; 10; i++) {
            sink.next(i);
            if (i == 5) {
                sink.error(new RuntimeException(&quot;error&quot;));
            }
        }
        sink.complete();
    });

    // then
    result.collectList().blocking() // 복잡한 검증을 어떻게 ..
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 테스트의 then에서 1, 2, 3, 4가 잘 전달이 되었는 지, 기대했던 예외가 터졌는 지 등 복합적으로 테스트하기 어렵다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;reactor-test&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 문제를 해결하기 위한 &lt;b&gt;Reactor Test&lt;/b&gt;에서 제공하는 기능에 대해 알아보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;StepVerifier&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StepVerifier를 사용하면 Publisher가 제공하는 다양한 이벤트를 차례로 검증할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StepVerifier는 FirstStep, Step, LastStep으로 구성된다. 아래 코드를 보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FirstStep은 없거나 1개 구성할 수 있다.&lt;/li&gt;
&lt;li&gt;Step은 없거나 N개를 구성할 수 있다.&lt;/li&gt;
&lt;li&gt;LastStep은 최종 결과를 검증하고 반드시 1개를 구성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void test() {
    Flux&amp;lt;Integer&amp;gt; result = Flux.create(sink -&amp;gt; {
        for (int i = 0; i &amp;lt; 10; i++) {
            sink.next(i);
        }
        sink.complete();
    });

    StepVerifier.create(result)
            .expectSubscription()
            .expectNext(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
            .expectComplete()
            .verify();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StepVerifier를 create하면 테스트를 위한 환경이 준비된 것이다. 그것만으로는 동작이 발생하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;verify()&lt;/code&gt;를 호출하면 실제로 Flux가 실행되면서 &lt;code&gt;expectSubscription()&lt;/code&gt;, &lt;code&gt;expectNext()&lt;/code&gt;, &lt;code&gt;expectComplete()&lt;/code&gt;를 통해서 이벤트를 확인한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;First Step&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FirstStep은 StepVerifier의 정적 메서드인 &lt;code&gt;create()&lt;/code&gt;로 생성되는 인터페이스이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface StepVerifier {

    static &amp;lt;T&amp;gt; FirstStep&amp;lt;T&amp;gt; create(Publisher&amp;lt;? extends T&amp;gt; publisher) {
        return create(publisher, Long.MAX_VALUE);
    }

    static &amp;lt;T&amp;gt; FirstStep&amp;lt;T&amp;gt; create(Publisher&amp;lt;? extends T&amp;gt; publisher, long n) {
        return create(publisher, StepVerifierOptions.create().initialRequest(n));
    }

    static &amp;lt;T&amp;gt; FirstStep&amp;lt;T&amp;gt; create(Publisher&amp;lt;? extends T&amp;gt; publisher,
        StepVerifierOptions options) {
        return DefaultStepVerifierBuilder.newVerifier(options, () -&amp;gt; publisher);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 &lt;code&gt;create()&lt;/code&gt;에서 사용하는 파라미터 &lt;code&gt;StepVerifierOptions&lt;/code&gt;는 &lt;code&gt;StepVerifier&lt;/code&gt;의 아래 속성을 지정할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;initialRequest: Subscription에 전달할 request 수 지정&lt;/li&gt;
&lt;li&gt;withInitialContext: Context 지정&lt;/li&gt;
&lt;li&gt;scenarioName: 시나리오 이름 부여 (에러 발생 시 노출)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 FirstStep은 Subscription이 제대로 이루어졌는 지 등을 검증할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;interface FirstStep&amp;lt;T&amp;gt; extends Step&amp;lt;T&amp;gt; {
    Step&amp;lt;T&amp;gt; expectNoFusionSupport();
    Step&amp;lt;T&amp;gt; expectSubscription();
    Step&amp;lt;T&amp;gt; expectSubscriptionMatches(Predicate&amp;lt;? super Subscription&amp;gt; predicate);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FirstStep은 Step을 상속하므로 동작을 생략하고 바로 Step으로 넘어갈 수도 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Step&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step은 StepVerifier에 의해 생성되어서 체이닝되는 객체의 클래스이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;interface Step&amp;lt;T&amp;gt; extends LastStep {
    default Step&amp;lt;T&amp;gt; assertNext(Consumer&amp;lt;? super T&amp;gt; assertionConsumer) {
        return consumeNextWith(assertionConsumer);
    }
    Step&amp;lt;T&amp;gt; expectNext(T t);
    Step&amp;lt;T&amp;gt; expectNext(T... ts);
    Step&amp;lt;T&amp;gt; expectNextCount(long count);
    Step&amp;lt;T&amp;gt; expectNextSequence(Iterable&amp;lt;? extends T&amp;gt; iterable);
    Step&amp;lt;T&amp;gt; expectNextMatches(Predicate&amp;lt;? super T&amp;gt; predicate);
    Step&amp;lt;T&amp;gt; expectNoEvent(Duration duration);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step은 onNext로 전달되는 item을 하나씩 검증한다. 각 메서드의 역할은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;assertNext: Consumer로 item을 검증한다. 예외 미발생이면 통과&lt;/li&gt;
&lt;li&gt;expectNext: 한 개 이상의 item을 순서대로 비교한 후 동일하면 통과&lt;/li&gt;
&lt;li&gt;expectNextCount: onNext 이벤트가 발생한 횟수가 동일하면 통과&lt;/li&gt;
&lt;li&gt;expectNextSequence: Iterable의 Element들을 onNext로 전달되는 items와 일치면 통과&lt;/li&gt;
&lt;li&gt;expectNextMatches: 인자로 전달된 Predicate의 결과가 true이면 통과&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 해당 메서드를 조합한 예시이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;void test() {
    var flux = Flux.range(0, 7);

    StepVerifier.create(flux)
            .assertNext(i -&amp;gt; {
                assertEquals(0, i);
            })
            .expectNext(1, 2)
            .expectNextCount(2)
            .expectNextSequence(List.of(4, 5))
            .expectNextMatches(i -&amp;gt; i == 6)
            .expectComplete()
            .verify();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step은 다음의 특징을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드들은 대부분 실행 후 Step을 반환하기 때문에 체이닝하여 다양한 검증이 가능하다.&lt;/li&gt;
&lt;li&gt;LastStep을 구현하기 때문에 바로 LastStep의 메서드를 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;LastStep&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LastStep은 가장 마지막에 호출되어 최종 상태를 확인하는 기능을 제공한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;interface LastStep {
    StepVerifier expectError();
    StepVerifier expectError(Class&amp;lt;? extends Throwable&amp;gt; clazz);
    StepVerifier expectTimeout(Duration duration);
    StepVerifier expectComplete();
    Duration verifyError();
    Duration verifyError(Class&amp;lt;? extends Throwable&amp;gt; clazz);
    Duration verifyComplete();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LastStep은 아래 메서드를 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;expectError: onError가 전달되었는 지 검증&lt;/li&gt;
&lt;li&gt;expectTimeout: Duradation 동안 onNext 혹은 onComplete 이벤트가 발생하지 않는 지 검증&lt;/li&gt;
&lt;li&gt;expectComplete: onComplete가 전달되었는 지 검증&lt;/li&gt;
&lt;li&gt;verifyXX: expectXX.verify()와 동일&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 예시 코드이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void test() {
    StepVerifier.create(Mono.just(1))
            .expectNext(1)
            .verifyComplete();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LastStep의 expectXX 메서드는 StepVerifier를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 StepVerifier의 메서드 일부이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface StepVerifier {
    Duration verify() throws AssertionError;
    Duration verify(Duration duration) throws AssertionError;
    Assertions verifyThenAssertThat();
    Assertions verifyThenAssertThat(Duration duration);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환된 StepVerifier의 &lt;code&gt;verify()&lt;/code&gt;를 호출해서 Publisher에 대한 검증을 시작할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Duration을 입력하지 않으면 영원히 결과를 기다리게 된다.&lt;/li&gt;
&lt;li&gt;verfyThenAssertThat을 사용해서 추가적인 검증을 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;withVirtualTime&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StepVerifier의 &lt;code&gt;withVirtualTime&lt;/code&gt;을 사용하면 기존의 Scheduler 대신 VirtualTimeScheduler가 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Scheduler는 delay와 관련된 함수들을 실제로 대기하는 대신 건너뛸 수 있는 기능을 제공한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void test() {
    StepVerifier.withVirtualTime(() -&amp;gt; {
            return Flux.range(0, 3)
                .delayElements(Duration.ofMinutes(1));
        })
        .thenAwait(Duration.ofMinutes(1))
        .expectNextCount(1)
        .thenAwait(Duration.ofMinutes(2))
        .expectNextCount(2)
        .verifyComplete();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;57&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/od7Sd/btsHcnTlDLI/20i2emoRVAkEln0s5oZg2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/od7Sd/btsHcnTlDLI/20i2emoRVAkEln0s5oZg2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/od7Sd/btsHcnTlDLI/20i2emoRVAkEln0s5oZg2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fod7Sd%2FbtsHcnTlDLI%2F20i2emoRVAkEln0s5oZg2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;380&quot; height=&quot;57&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;57&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 동작은 3분 이상이 소요되겠지만, 테스트에서는 491ms만 소요되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TestPublisher&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subscriber의 동작을 검증하기 위해서 직접 Publisher를 구현해야 할 수 있다. &lt;code&gt;reactor-test&lt;/code&gt;는 &lt;code&gt;TestPublisher&lt;/code&gt;를 제공한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public abstract class TestPublisher&amp;lt;T&amp;gt; implements Publisher&amp;lt;T&amp;gt;, PublisherProbe&amp;lt;T&amp;gt; {
    public static &amp;lt;T&amp;gt; TestPublisher&amp;lt;T&amp;gt; create() {}
    public abstract TestPublisher&amp;lt;T&amp;gt; next(@Nullable T value);
    public final TestPublisher&amp;lt;T&amp;gt; next(@Nullable T first, T... rest) {}
    public final TestPublisher&amp;lt;T&amp;gt; emit(T... values); {}
    public final TestPublisher&amp;lt;T&amp;gt; error(Throwable t);

    public abstract TestPublisher&amp;lt;T&amp;gt; assertMinRequested(long n);
    public abstract TestPublisher&amp;lt;T&amp;gt; assertMaxRequested(long n);
    public abstract TestPublisher&amp;lt;T&amp;gt; assertSubscribers();
    public abstract TestPublisher&amp;lt;T&amp;gt; assertSubscribers(int n);
    public abstract TestPublisher&amp;lt;T&amp;gt; assertNoSubscribers();
    public abstract TestPublisher&amp;lt;T&amp;gt; assertCancelled();
    public abstract TestPublisher&amp;lt;T&amp;gt; assertCancelled(int n);
    public abstract TestPublisher&amp;lt;T&amp;gt; assertNotCancelled();
    public abstract TestPublisher&amp;lt;T&amp;gt; assertRequestOverflow();
    public abstract TestPublisher&amp;lt;T&amp;gt; assertNoRequestOverflow();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TestPublsher&lt;/code&gt;를 사용하면 개발자가 직접 다양한 Event를 발생시키는 &lt;code&gt;Publisher&lt;/code&gt;를 쉽게 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;assertXX&lt;/code&gt;를 사용하면 &lt;code&gt;Publisher&lt;/code&gt;의 상태를 검증을 사용할 수도 있다. 아래 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
void test() {
    TestPublisher&amp;lt;Integer&amp;gt; publisher = TestPublisher.create();

    publisher.subscribe(new Subscriber() {
        @Override
        public void onSubscribe(Subscription s) {
            s.request(5);
        }

        @Override public void onNext(Object o) { }
        @Override public void onError(Throwable t) { }
        @Override public void onComplete() { }
    });
    publisher.assertSubscribers(1);
    publisher.assertWasRequested();
    publisher.assertMinRequested(5);
    publisher.assertMaxRequested(5);

    publisher.emit(1, 2);
    publisher.assertNoSubscribers();
    publisher.assertWasNotCancelled();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;assertXX&lt;/code&gt;를 사용하면 &lt;code&gt;SubScriber&lt;/code&gt;가 구독하는 &lt;code&gt;TestPublisher&lt;/code&gt;의 상태를 검증을 사용할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://fastcampus.co.kr/courses/216172&quot;&gt;https://fastcampus.co.kr/courses/216172&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://recordsoflife.tistory.com/1335&quot;&gt;https://recordsoflife.tistory.com/1335&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Server/JUnit, Spock</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/425</guid>
      <comments>https://jaehoney.tistory.com/425#entry425comment</comments>
      <pubDate>Sat, 4 May 2024 19:15:11 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] 코루틴 - CoroutineScope 이해하기!</title>
      <link>https://jaehoney.tistory.com/424</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Coroutine은 AbstractCoroutine을 상속한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public abstract class AbstractCoroutine&amp;lt;in T&amp;gt;(
    parentContext: CoroutineContext,
    initParentJob: Boolean,
    active: Boolean
) : JobSupport(active), Job, Continuation&amp;lt;T&amp;gt;, CoroutineScope { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Coroutine의 특징이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Coroutine은 Job, Continuation, CoroutinScope를 구현한다.&lt;/li&gt;
&lt;li&gt;CoroutineScope: Coroutine builder로 자식 Coroutine을 생성하고 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 CoroutineScope에 대해 알아보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CoroutineScope&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope는 Coroutine들에 대한 Scope를 정의한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 Coroutine들은 Scope를 가진다.&lt;/li&gt;
&lt;li&gt;Scope는 자식 Coroutine들에 대한 생명주기를 관리한다.&lt;/li&gt;
&lt;li&gt;자식 Coroutine이 모두 완료되어야 Scope도 완료된다.&lt;/li&gt;
&lt;li&gt;내부 CoroutineContext에 Job을 반드시 포함되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope는 CoroutineContext를 가진다. Scope를 통해 자식 코루틴에게 CoroutineContext를 전파하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;413&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beH3CR/btsGN14aidV/GCMsj1N1HM44SuUkN6H5T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beH3CR/btsGN14aidV/GCMsj1N1HM44SuUkN6H5T1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beH3CR/btsGN14aidV/GCMsj1N1HM44SuUkN6H5T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeH3CR%2FbtsGN14aidV%2FGCMsj1N1HM44SuUkN6H5T1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;490&quot; height=&quot;280&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;413&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Coroutine Builder&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Coroutine Builder는 CoroutineScope로부터 Coroutine을 생성한다. 생성된 Coroutine은 비동기로 동작하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 예시로 launch가 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;launch&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;launch의 동작은 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -&amp;gt; Unit
): Job {
    // 새로운 Context를 생성
    val newContext = newCoroutineContext(context)
    // 새로운 Context로 코루틴 생성
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존&amp;nbsp;Context를&amp;nbsp;활용해서&amp;nbsp;새로운&amp;nbsp;Context를&amp;nbsp;만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    runBlocking {
        val cs = CoroutineScope(EmptyCoroutineContext)
        log.info(&quot;job: {}&quot;, cs.coroutineContext[Job])

        val job = cs.launch {
            // coroutine created
            delay(100)
            log.info(&quot;context: {}&quot;, this.coroutineContext)
            log.info(&quot;class name: {}&quot;, this.javaClass.simpleName)
            log.info(&quot;parentJob: {}&quot;, this.coroutineContext[Job]?.parent)
        }
        log.info(&quot;start&quot;)
        job.join()
        log.info(&quot;finish&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;40:43 [main] - job: JobImpl{Active}@229d10bd
40:43 [main] - start
40:43 [DefaultDispatcher-worker-1] - context: [StandaloneCoroutine{Active}@15439c7c, Dispatchers.Default]
40:43 [DefaultDispatcher-worker-1] - class name: StandaloneCoroutine
40:43 [DefaultDispatcher-worker-1] - parentJob: JobImpl{Active}@229d10bd
40:43 [main] - finish&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;launch를 실행한 Job과 launch 내부 Context Job의 부모가 동일함을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 아래 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    runBlocking {
        val parent = launch {
            launch {
                delay(100)
                log.info(&quot;finish sub1&quot;)
            }
            launch {
                delay(100)
                log.info(&quot;finish sub2&quot;)
            }
            launch {
                delay(100)
                log.info(&quot;finish sub3&quot;)
            }
        }

        log.info(&quot;parent start&quot;)
        parent.join()
        log.info(&quot;parent end&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 결과이다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;35:16 [main] - parent start
35:16 [main] - finish sub1
35:16 [main] - finish sub2
35:16 [main] - finish sub3
35:16 [main] - parent end&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sub 잡이 모두 실행된 후 &lt;code&gt;parent.join()&lt;/code&gt;이 완료로 처리된다. 이는 부모 Job이 자식 Job의 생명주기를 관리한다는 것을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;launch는 비동기적으로 동작한다. 하지만, &lt;code&gt;join()&lt;/code&gt;이 완료될 때까지 suspend가 되어서 end는 잡이 실행된 이후에 실행된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;async&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;launch와 유사한 메서드로 async가 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public fun &amp;lt;T&amp;gt; CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -&amp;gt; T
): Deferred&amp;lt;T&amp;gt; {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine&amp;lt;T&amp;gt;(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;launch와의 유일한 차이는 Job을 반환하는 것이 아니라 Deffered를 반환하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deffered 인터페이스는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public interface Deferred&amp;lt;out T&amp;gt; : Job {
    public suspend fun await(): T
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deffered는 &lt;code&gt;await()&lt;/code&gt;을 통해 원하는 시점 반환하는 값에 접근할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Structured concurrency&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 코드를 구조적으로 작성하는 &lt;b&gt;동시성 프로그래밍 패러다임&lt;/b&gt;을 Structured Concurrency라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineScope는 Structured concurrency를 적용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 비동기 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun nonStructured() {
    log.info(&quot;step 1&quot;)
    CompletableFuture.runAsync {
        Thread.sleep(1000)
        log.info(&quot;Finish run1&quot;)
    }
    log.info(&quot;step 2&quot;)
    CompletableFuture.runAsync {
        Thread.sleep(100)
        log.info(&quot;Finish run2&quot;)
    }
    log.info(&quot;step 3&quot;)
}

fun main() {
    log.info(&quot;Start main&quot;)
    nonStructured()
    log.info(&quot;Finish main&quot;)
    Thread.sleep(3000)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;18:39 [main] - Start main
18:39 [main] - step 1
18:39 [main] - step 2
18:39 [main] - step 3
18:39 [main] - Finish main
18:39 [ForkJoinPool.commonPool-worker-2] - Finish run2
18:40 [ForkJoinPool.commonPool-worker-1] - Finish run1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 코드는 실행하는 채로 흐름을 그대로 가져가고 있다. 주목해야 할 점은 &quot;Finish main&quot;보다 &quot;Finish run&quot;이 늦게 출력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 코루틴 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private suspend fun structured() = coroutineScope {
    log.info(&quot;step 1&quot;)
    launch {
        delay(1000)
        log.info(&quot;Finish launch1&quot;)
    }
    log.info(&quot;step 2&quot;)
    launch {
        delay(100)
        log.info(&quot;Finish launch2&quot;)
    }
    log.info(&quot;step 3&quot;)
}

fun main() = runBlocking {
    log.info(&quot;Start runBlocking&quot;)
    structured()
    log.info(&quot;Finish runBlocking&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 실행 결과이다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;20:54 [main] - Start runBlocking
20:54 [main] - step 1
20:54 [main] - step 2
20:54 [main] - step 3
20:54 [main] - Finish launch2
20:55 [main] - Finish launch1
20:55 [main] - Finish runBlocking&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 코드와 결과가 다른 점은 &quot;Finish runBlocking&quot;이 마지막에 출력된다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 여러 동시성 코드를 같은 생명주기를 갖게 만들고, 해당 동작들이 모두 완료되어야 다음 동작을 수행한다. 코루틴도 &lt;b&gt;자식 코루틴(별도 쓰레드의 동작들)이 모두 종료되어야 해당 코루틴이 끝난 것으로 처리&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &lt;b&gt;구조화된 동시성(Structured concurrency)&lt;/b&gt; 이라 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cancellation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조화된 동시성의 또 하나의 특징은 &lt;b&gt;cancel이 발생하면 자식 coroutine까지 전파&lt;/b&gt;한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private suspend fun structured() = coroutineScope {
    launch {
        try {
            delay(1000)
            log.info(&quot;Finish launch1&quot;)
        } catch (e: CancellationException) {
            log.info(&quot;Job1 is cancelled&quot;)
        }
    }

    launch {
        try {
            delay(500)
            log.info(&quot;Finish launch2&quot;)
        } catch (e: CancellationException) {
            log.info(&quot;Job2 is cancelled&quot;)
        }
    }

    this.cancel()
}

fun main() = runBlocking {
    log.info(&quot;Start runBlocking&quot;)
    try {
        structured()
    } catch (e: CancellationException) {
        log.info(&quot;Job is cancelled&quot;)
    }
    log.info(&quot;Finish runBlocking&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;structured()&lt;/code&gt;를 보면 마지막에 &lt;code&gt;cancel()&lt;/code&gt;을 호출하고 있다. 아래는 실행 결과이다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;26:57 [main] - Start runBlocking
26:57 [main] - Job1 is cancelled
26:57 [main] - Job2 is cancelled
26:57 [main] - Job is cancelled
26:57 [main] - Finish runBlocking&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 코루틴에서도 &lt;code&gt;CancellationException&lt;/code&gt;이 발생해서 로그가 찍힌 것을 볼 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방향&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job의 실패는 부모에서 자식으로는 전파되지만, 자식에서 부모로 전파되지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    runBlocking {
        val parentJob = launch {
            val job1 = launch {
                try {
                    delay(1000)
                    log.info(&quot;job1 Success&quot;)
                } catch (e: Exception) {
                    log.info(&quot;job1 Cancelled&quot;)
                }
            }

            launch {
                try {
                    delay(1000)
                    log.info(&quot;job2 Success&quot;)
                } catch (e: Exception) {
                    log.info(&quot;job2 Cancelled&quot;)
                }
            }
            delay(100)
            job1.cancel()
        }
        parentJob.join()
        log.info(&quot;job is cancelled: {}&quot;, parentJob.isCancelled)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;23:39 [main] - job1 Cancelled
23:40 [main] - job2 Success
23:40 [main] - job is cancelled: false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주목해야할 점은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Job1을 취소했을 때 Job2는 취소되지 않았다.&lt;/li&gt;
&lt;li&gt;Job1을 취소했을 때 ParentJob은 취소되지 않았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cancellation은 부모에서 자식으로는 전파되지만, 자식에서 부모로는 전파되지 않는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Exception&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cancellation은 자식에서 부모로 전파되지 않는다. 하지만 &lt;b&gt;예외가 터지는 경우&lt;/b&gt;는 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래&amp;nbsp;코드를&amp;nbsp;보면&amp;nbsp;parentJob이&amp;nbsp;job1,&amp;nbsp;job2를&amp;nbsp;가지고,&amp;nbsp;job1에서&amp;nbsp;예외를&amp;nbsp;발생시킨다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    lateinit var job2: Job
    runBlocking {
        val parentJob = CoroutineScope(Dispatchers.Default).launch {
            launch {
                delay(100)
                throw IllegalStateException()
            }

            job2 = launch {
                try {
                    delay(1000)
                    log.info(&quot;job2 success&quot;)
                } catch (e: Exception) {
                    log.info(&quot;job2 Exception&quot;)
                }
            }
        }

        parentJob.join()
        log.info(&quot;job2 is cancelled: {}&quot;, job2.isCancelled)
        log.info(&quot;parentJob is cancelled: {}&quot;, parentJob.isCancelled)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;50:44 [DefaultDispatcher-worker-2] - job2 Exception
Exception in thread &quot;DefaultDispatcher-worker-2&quot; java.lang.IllegalStateException
    at com.grizz.wooman.coroutine.scope.LeafCoroutineExceptionExampleKt$main$1$parentJob$1$1.invokeSuspend(LeafCoroutineExceptionExample.kt:14)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
    Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@7cbbb8d9, Dispatchers.Default]
50:44 [main] - job2 is cancelled: true
50:44 [main] - parentJob is cancelled: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job1에서 Exception이 발생해서 parentJob을 cancel 했고, parentJob의 자식인 Job2도 Cancel로 처리되었다. 자식 코루틴에서 Exception이 발생하면 부모까지 전파가되어서 Cancel이 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로&amp;nbsp;&lt;code&gt;SupervisorJob&lt;/code&gt;을 사용하면 Exception이 발생해도 Cancellation이 자식으로만 전파되고, 부모로는 전파되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게&amp;nbsp;&lt;b&gt;부모&amp;nbsp;Job과&amp;nbsp;자식&amp;nbsp;Job&amp;nbsp;간의&amp;nbsp;생명주기(완료&amp;nbsp;여부,&amp;nbsp;취소&amp;nbsp;여부&amp;nbsp;등)를&amp;nbsp;어떻게&amp;nbsp;설정할&amp;nbsp;지&amp;nbsp;명시&lt;/b&gt;하는&amp;nbsp;것이&amp;nbsp;&lt;b&gt;CoroutineScope&lt;/b&gt;로&amp;nbsp;이해하면&amp;nbsp;될&amp;nbsp;것&amp;nbsp;같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://fastcampus.co.kr/courses/216172&quot;&gt;https://fastcampus.co.kr/courses/216172&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Language/Kotlin</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/424</guid>
      <comments>https://jaehoney.tistory.com/424#entry424comment</comments>
      <pubDate>Sat, 20 Apr 2024 17:17:21 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] 코루틴 - CoroutineContext 이해하기!</title>
      <link>https://jaehoney.tistory.com/423</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코루틴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴(Coroutine)은 Co(함께, 서로) + routine(규칙적 작업의 집합) 2개가 합쳐진 단어로 함께 동작하며 규칙이 있는 작업의 집합을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 &lt;code&gt;Koroutine&lt;/code&gt;이 아니라 &lt;code&gt;Coroutine&lt;/code&gt;인지 의아할 수 있는데 코루틴은 코틀린만의 것이 아니다. Python, C#, Go, Javascript 등 다양한 언어에서 지원하는 개념이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS의 async, await도 동일한 개념이고 코루틴은 프로그래밍 초창기부터 존재하던 개념이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kotlin Coroutines&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 코루틴을 위한 공식 라이브러리(&lt;code&gt;kotlinx.coroutines&lt;/code&gt;)를 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Kotlin Coroutines의 특징이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시성을 위한 기능을 제공&lt;/li&gt;
&lt;li&gt;Async Non-blocking으로 동작하는 코드를 동기 방식으로 작성할 수 있도록 지원
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코틀린 컴파일러에서 바이트 코드를 비동기 방식으로 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CoroutineContext를 통해 Dispatcher, Error handling, ThreadLocal 등을 지원&lt;/li&gt;
&lt;li&gt;CoroutineScope를 통해 Structured concurrency, Cancellation 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CoroutineContext&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서 코틀린 컴파일러는 &lt;code&gt;suspend&lt;/code&gt; 키워드가 있는 함수를 &lt;code&gt;Continuation&lt;/code&gt; 인터페이스 기반의 CPS를 구현해준다고 했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public interface Continuation&amp;lt;in T&amp;gt; {
    public val context: CoroutineContext
    public fun resumeWith(result: Result&amp;lt;T&amp;gt;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Continuation&lt;/code&gt;은 1개의 CoroutineContext를 포함한다. CoroutineContext는 자식 코루틴에 상태와 동작을 전파한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Coroutine 이름&lt;/li&gt;
&lt;li&gt;CoroutineDispatcher&lt;/li&gt;
&lt;li&gt;ThreadLocal&lt;/li&gt;
&lt;li&gt;CoroutineExceptionHandler&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 그림을 보면 더 이해가 쉬울 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;603&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crFbzc/btsGA0TqWoq/41Heet0kf0ZxMnqkfk9tlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crFbzc/btsGA0TqWoq/41Heet0kf0ZxMnqkfk9tlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crFbzc/btsGA0TqWoq/41Heet0kf0ZxMnqkfk9tlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrFbzc%2FbtsGA0TqWoq%2F41Heet0kf0ZxMnqkfk9tlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;603&quot; height=&quot;303&quot; data-origin-width=&quot;603&quot; data-origin-height=&quot;303&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 코루틴이 코루틴 컨텍스트를 꺼내는 방법은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Scope를 통한 접근&lt;/li&gt;
&lt;li&gt;Continuation을 통한 접근&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 코드는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    runBlocking {
        // Scope를 통한 호출
        log.info(&quot;context: {}&quot;, this.coroutineContext)
        sub()
    }
}
private suspend fun sub() {
    // Continuation을 통한 호출
    log.info(&quot;context {}&quot;, coroutineContext)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineContext는 병합하거나 분해할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EmptyCoroutineContext: Element가 없는 상태&lt;/li&gt;
&lt;li&gt;Element: Element가 하나인 상태&lt;/li&gt;
&lt;li&gt;CombinedContext: Element가 2개 이상인 상태&lt;/li&gt;
&lt;li&gt;Key: Element를 구분할 때 사용하는 식별자&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 CoroutineContext 인터페이스이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public interface CoroutineContext {
    public operator fun &amp;lt;E : Element&amp;gt; get(key: Key&amp;lt;E&amp;gt;): E?

    public fun &amp;lt;R&amp;gt; fold(initial: R, operation: (R, Element) -&amp;gt; R): R

    public operator fun plus(context: CoroutineContext): CoroutineContext

    public fun minusKey(key: Key&amp;lt;*&amp;gt;): CoroutineContext

    public interface Key&amp;lt;E : Element&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 실행해보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    val handler = CoroutineExceptionHandler { _, e -&amp;gt;
        log.error(&quot;exception caught in handler&quot;)
    }

    val context1 = CoroutineName(&quot;custom name&quot;) +
            Dispatchers.IO +
            Job() +
            handler
    log.info(&quot;context: {}&quot;, context1)

    val context2 = context1.minusKey(CoroutineExceptionHandler)
    log.info(&quot;context2: {}&quot;, context2)

    val context3 = context2.minusKey(Job)
    log.info(&quot;context3: {}&quot;, context3)

    val context4 = context3.minusKey(CoroutineDispatcher)
    log.info(&quot;context4: {}&quot;, context4)

    val context5 = context4.minusKey(CoroutineName)
    log.info(&quot;context5: {}&quot;, context5)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 결과이다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;22:43 [main] - context: [CoroutineName(custom name), JobImpl{Active}@5c1a8622, com.grizz.wooman.coroutine.context.ContextMinusExampleKt$main$$inlined$CoroutineExceptionHandler$1@5ad851c9, Dispatchers.IO]
22:43 [main] - context2: [CoroutineName(custom name), JobImpl{Active}@5c1a8622, Dispatchers.IO]
22:43 [main] - context3: [CoroutineName(custom name), Dispatchers.IO]
22:43 [main] - context4: CoroutineName(custom name)
22:43 [main] - context5: EmptyCoroutineContext&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineContext는 각 &lt;code&gt;Element&lt;/code&gt;를 더하거나 제거할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 각 코루틴 컨텍스트의 요소들은 &lt;code&gt;Element&lt;/code&gt;를 상속하고, 내부적으로 가진 Key를 통해 관리된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;213&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjljVV/btsGC2PKuBG/qmvuU2w9AvmF1fF5U8wh4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjljVV/btsGC2PKuBG/qmvuU2w9AvmF1fF5U8wh4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjljVV/btsGC2PKuBG/qmvuU2w9AvmF1fF5U8wh4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjljVV%2FbtsGC2PKuBG%2FqmvuU2w9AvmF1fF5U8wh4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;674&quot; height=&quot;213&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;213&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 활용하면 코루틴 내부에서 값을 전달할 때 사용하는 것도 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 Job에 대해 알아보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Job&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineContext의 요소중 하나인 Job이다. Job은 Coroutine의 생명주기를 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public interface Job : CoroutineContext.Element {
    public companion object Key : CoroutineContext.Key&amp;lt;Job&amp;gt;

    public val parent: Job?

    public val isActive: Boolean

    public val isCompleted: Boolean

    public val isCancelled: Boolean

    public fun start(): Boolean

    public fun cancel(cause: CancellationException? = null)

    public val children: Sequence&amp;lt;Job&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineContext의 Job에 대한 설명은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Active, Completed, Cancelled와 같은 상태를 갖는다.&lt;/li&gt;
&lt;li&gt;명시적으로 시작이나 취소를 할 수 있다.&lt;/li&gt;
&lt;li&gt;child를 통해서 다른 Job의 생명주기를 관리한다.&lt;/li&gt;
&lt;li&gt;launch, async 등의 coroutine builder를 통해 자식 Job을 생성가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CoroutineDispatcher&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineDispatcher는 코루틴을 어떤 Thread에게 보낼 지 결정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;531&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/muXkX/btsGC291QIR/bNvu1Zn4lYP1iHrAA9Syxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/muXkX/btsGC291QIR/bNvu1Zn4lYP1iHrAA9Syxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/muXkX/btsGC291QIR/bNvu1Zn4lYP1iHrAA9Syxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmuXkX%2FbtsGC291QIR%2FbNvu1Zn4lYP1iHrAA9Syxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;621&quot; height=&quot;343&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;531&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는&amp;nbsp;코틀린&amp;nbsp;Coroutine에서&amp;nbsp;기본으로&amp;nbsp;지원해주는&amp;nbsp;Dispatcher&amp;nbsp;목록이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dispatcher.Default
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU 개수 만큼 스레드를 생성&lt;/li&gt;
&lt;li&gt;리스트를 정렬하거나 Json Parsing 등 가공 작업에 주로 사용&lt;/li&gt;
&lt;li&gt;CPU를 많이 사용하는 무거운 작업에 최적화&lt;/li&gt;
&lt;li&gt;현재는 CommonPool이 사용되며, 쓰레드 풀의 최대 크기가 시스템 코어수-1이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Dispatcher.Main
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면 UI 작업을 위해 사용&lt;/li&gt;
&lt;li&gt;Android 개발 모듈에서 주로 사용하고, 일반적으로 서버 개발에서는 사용할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Dispatcher.IO
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최대 64개까지 늘어나는 가변 크기의 쓰레드 풀을 가진다.&lt;/li&gt;
&lt;li&gt;네트워크 DB 작업할 경우 사용 (I/O 블로킹을 메인 쓰레드에서 격리시키기 위해 사용)&lt;/li&gt;
&lt;li&gt;읽기, 쓰기 작업에 최적화&lt;/li&gt;
&lt;li&gt;Thread를 Block할 필요가 있는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Default와 IO 디스패처 간 차이는 &lt;b&gt;쓰레드 풀의 쓰레드 개수&lt;/b&gt; 설정에 있다. 복잡한 연산의 경우 CPU를 많이 사용하므로 쓰레드 개수가 많이 필요하지 않다. 반면, I/O 작업의 경우 CPU를 많이 점유하지 않고 쓰레드를 많이 필요로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dispatcher는 작업의 특성에 맞게 적절히 선택해야 한다고 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(필요한 경우 ThreadPoolExecutor를 생성한 후 그걸 사용해서 Dispatcher를 생성하면 된다.)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Default와 IO가 쓰레드가 동일한 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dispatchers.Default와 Dispatchers.IO는 다른 쓰레드 풀을 사용할 것이라고 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 실행해보면 어떻게 될까?&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    runBlocking {
        withContext(Dispatchers.Default) {
            log.info(&quot;thread: {}&quot;, Thread.currentThread().name)
            log.info(&quot;dispatcher: {}&quot;, this.dispatcher())
        }

        withContext(Dispatchers.IO) {
            log.info(&quot;thread: {}&quot;, Thread.currentThread().name)
            log.info(&quot;dispatcher: {}&quot;, this.dispatcher())
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 결과이다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;11:42 [DefaultDispatcher-worker-1] - thread: DefaultDispatcher-worker-1
11:42 [DefaultDispatcher-worker-1] - dispatcher: Dispatchers.Default
11:42 [DefaultDispatcher-worker-1] - thread: DefaultDispatcher-worker-1
11:42 [DefaultDispatcher-worker-1] - dispatcher: Dispatchers.IO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dispatchers.Default와 Dispatchers.IO는 &lt;b&gt;동일한 쓰레드 풀&lt;/b&gt;을 사용한다. 대신 &lt;b&gt;동시에 수행 가능한 쓰레드 수&lt;/b&gt;가 다른 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ThreadLocal&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 실행해보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    val threadLocal = ThreadLocal&amp;lt;String&amp;gt;()
    threadLocal.set(&quot;hello&quot;)

    runBlocking {
        log.info(&quot;thread: {}&quot;, Thread.currentThread().name)
        log.info(&quot;threadLocal: {}&quot;, threadLocal.get())

        launch(Dispatchers.IO) {
            log.info(&quot;thread: {}&quot;, Thread.currentThread().name)
            log.info(&quot;threadLocal: {}&quot;, threadLocal.get())
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 실행 결과이다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;34:21 [main] - thread: main
34:21 [main] - threadLocal: hello
34:21 [DefaultDispatcher-worker-1] - thread: DefaultDispatcher-worker-1
34:21 [DefaultDispatcher-worker-1] - threadLocal: null&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;runBlocking&lt;/code&gt;의 경우 main 쓰레드가 동작하고, &lt;code&gt;launch&lt;/code&gt;에서는 &lt;code&gt;Dispatchers.IO&lt;/code&gt;륾 명시해서 &lt;code&gt;DefaultDispatcher-worker-1&lt;/code&gt; 스레드에서 코루틴이 실행되고 있다. 당연히 ThreadLocal에서 값을 꺼낼 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 쓰레드에서 코루틴을 실행할 때 threadLocal을 유지할 수 있는 방법이 있다. &lt;code&gt;ThreadLocalElement&lt;/code&gt;를 활용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 &lt;code&gt;Dispatcher.IO&lt;/code&gt;를 사용해서 별도 Thread에서 코루틴을 수행한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    val threadLocal = ThreadLocal&amp;lt;String&amp;gt;()
    threadLocal.set(&quot;hello&quot;)
    log.info(&quot;thread: {}&quot;, Thread.currentThread().name)
    log.info(&quot;threadLocal: {}&quot;, threadLocal.get())

    runBlocking {
        val context = CoroutineName(&quot;custom name&quot;) +
                Dispatchers.IO +
                threadLocal.asContextElement()

        launch(context) {
            log.info(&quot;thread: {}&quot;, Thread.currentThread().name)
            log.info(&quot;threadLocal: {}&quot;, threadLocal.get())
            log.info(
                &quot;coroutine name: {}&quot;,
                coroutineContext[CoroutineName]
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 결과는 코루틴 내부에서도 아래와 같이 threadLocal 값이 잘 할당되어 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;12:31 [main] - thread: main
12:31 [main] - threadLocal: hello
12:31 [DefaultDispatcher-worker-1] - thread: DefaultDispatcher-worker-1
12:31 [DefaultDispatcher-worker-1] - threadLocal: hello
12:31 [DefaultDispatcher-worker-1] - coroutine name: CoroutineName(custom name)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 부분은 &lt;code&gt;threadLocal.asContextElement()&lt;/code&gt;를 사용해서 &lt;code&gt;ThreadLocalElement&lt;/code&gt;를 만들어서 CoroutineContext에 추가했기 때문이다.&lt;br /&gt;&lt;code&gt;kotlinx.coroutines.ThreadContextElement&lt;/code&gt;를 사용하면 해당 ThreadLocal을 보존할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CoroutineExceptionHandler&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CoroutineExceptionHandler&lt;/b&gt;는 CoroutineContext 요소 중 하나이고, 코루틴 내부의 Exception을 핸들링하는 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    val handler = CoroutineExceptionHandler { _, e -&amp;gt;
        log.error(&quot;not caught maybe&quot;)
    }
    runBlocking {
        CoroutineScope(handler).launch {
            throw IllegalStateException(&quot;exception in launch&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 아래와 같이 잘 에러가 핸들링된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;24:40 [DefaultDispatcher-worker-1] - not caught maybe&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주의사항&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoroutineExceptionHandler는 아래의 주의사항이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;main runBlocking에서 실행되는 코루틴에서는 동작하지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자식 코루틴에서 Exception이 발생하면 부모 코루틴이 취소되어 Exception을 Handling 할 수 없다.&lt;/li&gt;
&lt;li&gt;자세한 내용은 &lt;a href=&quot;https://kotlinlang.org/docs/exception-handling.html#cancellation-and-exceptions&quot;&gt;공식 문서&lt;/a&gt;를 읽어보길 권장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;루트가 아닌 코루틴에 적용되는 핸들러는 무시된다.&lt;/li&gt;
&lt;li&gt;async에는 적용할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun main() {
    val handler = CoroutineExceptionHandler { _, e -&amp;gt;
        log.error(&quot;custom exception handle: {${e.javaClass}}&quot;)
    }
    runBlocking(handler) {
        throw IllegalStateException()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi8IXe/btsGBQvYcaO/o6h6Fz6XXVtby1uo3ky8dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi8IXe/btsGBQvYcaO/o6h6Fz6XXVtby1uo3ky8dK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi8IXe/btsGBQvYcaO/o6h6Fz6XXVtby1uo3ky8dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi8IXe%2FbtsGBQvYcaO%2Fo6h6Fz6XXVtby1uo3ky8dK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1047&quot; height=&quot;140&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exception이 핸들링 되지 않는다. &lt;b&gt;main runBlocking에서 실행되는 코루틴에서는 CoroutineExceptionHandler가 동작하지 않기&amp;nbsp;&lt;/b&gt;때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에서는 CoroutineScope를 로 별도로 추가해서 사용했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;runBlocking {
    val context = CoroutineName(&quot;custom name&quot;) +
            CoroutineExceptionHandler { _, e -&amp;gt;
                log.error(&quot;custom exception handle: ${e.javaClass}&quot;)
            }

    CoroutineScope(Dispatchers.IO).launch(context) {
        throw IllegalStateException()
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;22:10 [DefaultDispatcher-worker-1] custom exception handle:
        {class java.lang.IllegalStateException}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 이제는 Exception이 잘 핸들링 되는 것을 볼 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://fastcampus.co.kr/courses/216172&quot;&gt;https://fastcampus.co.kr/courses/216172&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kotlinlang.org/docs/coroutines-overview.html&quot;&gt;https://kotlinlang.org/docs/coroutines-overview.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://write.agrevolution.in/kotlin-coroutines-part-3-coroutine-context-bd5543389190&quot;&gt;https://write.agrevolution.in/kotlin-coroutines-part-3-coroutine-context-bd5543389190&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Language/Kotlin</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/423</guid>
      <comments>https://jaehoney.tistory.com/423#entry423comment</comments>
      <pubDate>Sun, 14 Apr 2024 22:17:20 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] 코루틴 - suspend 키워드 이해해보기!</title>
      <link>https://jaehoney.tistory.com/422</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코루틴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴(Coroutine)은 Co(함께, 서로) + routine(규칙적 작업의 집합) 2개가 합쳐진 단어로 함께 동작하며 규칙이 있는 작업의 집합을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 &lt;code&gt;Koroutine&lt;/code&gt;이 아니라 &lt;code&gt;Coroutine&lt;/code&gt;인지 의아할 수 있는데 코루틴은 코틀린만의 것이 아니다. Python, C#, Go, Javascript 등 다양한 언어에서 지원하는 개념이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS의 async, await도 코루틴의 일부이며, 코루틴은 프로그래밍 초창기부터 존재하던 개념이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;vs Thread&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 &lt;b&gt;경량 쓰레드&lt;/b&gt;라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 여러 개의 쓰레드로 여러 개의 작업을 실행하는 방식이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2684&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFrItt/btsGjZMHFUn/IKyLvU5naJfVJhKsDTqcV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFrItt/btsGjZMHFUn/IKyLvU5naJfVJhKsDTqcV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFrItt/btsGjZMHFUn/IKyLvU5naJfVJhKsDTqcV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFrItt%2FbtsGjZMHFUn%2FIKyLvU5naJfVJhKsDTqcV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2684&quot; height=&quot;702&quot; data-origin-width=&quot;2684&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 작업 하나하나에 Thread를 할당하는 것이 아니라 &lt;b&gt;Object&lt;/b&gt;를 할당한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2693&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crndF8/btsGiZzQnAo/pnPOzRILe9n2nbGmWQev80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crndF8/btsGiZzQnAo/pnPOzRILe9n2nbGmWQev80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crndF8/btsGiZzQnAo/pnPOzRILe9n2nbGmWQev80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrndF8%2FbtsGiZzQnAo%2FpnPOzRILe9n2nbGmWQev80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2693&quot; height=&quot;702&quot; data-origin-width=&quot;2693&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰레드가&amp;nbsp;Object를&amp;nbsp;스위칭함으로써&amp;nbsp;Context&amp;nbsp;Swiching&amp;nbsp;비용을&amp;nbsp;대폭&amp;nbsp;줄인다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kotlin Coroutines&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 코루틴을 위한 공식 라이브러리(&lt;code&gt;kotlinx.coroutines&lt;/code&gt;)를 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jetbrain에서는 멀티 쓰레딩 문제를 간소화된 방식으로 해결할 수 있도록 코틀린의 코루틴 라이브러리를 개발했다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 높은 러닝커브를 가지는 &lt;code&gt;RxJava&lt;/code&gt;와 같은 비동기 라이브러리보다 낮은 러닝커브로 동기적으로 코드를 작성할 수 있게 도움을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 &lt;code&gt;suspend&lt;/code&gt; 키워드를 사용해서 코루틴을 제공하고 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;suspend 키워드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;suspend는 coroutine 혹은 다른 suspend 함수에서 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDIevn/btsGkBR7xXz/wh2LBWHZG6ELvFEs73PH6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDIevn/btsGkBR7xXz/wh2LBWHZG6ELvFEs73PH6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDIevn/btsGkBR7xXz/wh2LBWHZG6ELvFEs73PH6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDIevn%2FbtsGkBR7xXz%2Fwh2LBWHZG6ELvFEs73PH6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;278&quot; height=&quot;140&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;suspend 키워드를 사용하면 해당 작업을 일시중단 시키고, 그 시간 동안 다른 작업에 Thread를 할당할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;suspend의 내부 구현을 이해하려면  코틀린 컴파일러와 CPS(Continuation passing style)를 알아야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CPS(Continuation passing style)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴에서 사용하는 Continuation passing style는 Direct style과 유사하다. Direct style의 특징은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Caller가 callee를 호출하는 상황에서 Callee는 값을 계산하여 반환&lt;/li&gt;
&lt;li&gt;Caller는 callee가 반환한 결과를 사용&lt;/li&gt;
&lt;li&gt;일반적인 동기 스타일&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Continuation passing style의 특징이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Caller가 callee를 호출하는 상황에서 Callee는 값을 계산하여 Continuation을 실행하고 인자로 값을 전달&lt;/li&gt;
&lt;li&gt;continuation은 callee 마지막에서 한 번만 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;object CpsCalculator {
    fun calculate(initialValue: Int, continuation: (Int) -&amp;gt; Unit) {
        initialize(initialValue) { initial -&amp;gt;
            plusOne(initial) { added -&amp;gt;
                double(added) { multiplied -&amp;gt;
                    continuation(multiplied)
                }
            }
        }
    }

    private fun initialize(value: Int, continuation: (Int) -&amp;gt; Unit) {
        continuation(value)
    }

    private fun plusOne(value: Int, continuation: (Int) -&amp;gt; Unit) {
        continuation(value + 1)
    }

    private fun double(value: Int, continuation: (Int) -&amp;gt; Unit) {
        continuation(value * 2)
    }
}

fun main() {
    CpsCalculator.calculate(5) { result -&amp;gt;
        log.info(&quot;Result: {}&quot;, result)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Continuation은 Callback과 유사한 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Callback은 추가로 무엇을 해야 하는 지를 호출하는 것이고 여러번 호출할 수 있다. 반면, Continuation은 최종적으로 로직의 제어를 넘기기 위해 한 번 호출된다는 차이가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴은 내부적으로 CPS를 이용해 구현된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Contiunation&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Kotlin coroutines에서 사용하는 &lt;code&gt;Continuation&lt;/code&gt; 인터페이스이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;public interface Continuation&amp;lt;in T&amp;gt; {
    public val context: CoroutineContext
    public fun resumeWith(result: Result&amp;lt;T&amp;gt;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 coroutineContext를 포함하고, &lt;code&gt;resumeWith()&lt;/code&gt;는 마지막 suspend 함수의 결과를 전달받을 수 있게 해주는 함수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린 컴파일러는 아래와 같은 &lt;code&gt;suspend&lt;/code&gt; 키워드가 있는 메서드가 있다고 가정했을 때&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun execute(userId: Long, productIds: List&amp;lt;Long&amp;gt;): Order {
    // 1. 유저 조회
    val user = userService.findUserFuture(userId)
        .await()

    // 2. 상품 목록 조회
    val products = productService
        .findProductFlowable(productIds)
        .toList().await()

    // 5. 주문
    val order = orderService.createOrderMono(
        user, products
    ).awaitSingle()

    return order
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러가 &lt;code&gt;Continuation&lt;/code&gt;를 활용한 CPS 구조의 코드로 변환하고, 일시 중단(suspend), 재개(resume)이 가능한 형태로 만든다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;suspend 키워드가 붙은 함수에 Continuation 인자 추가&lt;/li&gt;
&lt;li&gt;내부에서 다른 suspend 함수를 실행할 때 소유하고 있던 Continuation을 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 일시 중단과 재개 가능한 단위를 &lt;b&gt;코루틴(coroutine)&lt;/b&gt;이라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 위 변환 과정으로 인해 suspend가 없는 함수에서는 다른 suspend 함수를 호출할 수 없다. 전달할 Continuation이 없기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.charlezz.com/?p=45962&quot;&gt;https://www.charlezz.com/?p=45962&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://incheol-jung.gitbook.io/docs/study/undefined-4/1&quot;&gt;https://incheol-jung.gitbook.io/docs/study/undefined-4/1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Language/Kotlin</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/422</guid>
      <comments>https://jaehoney.tistory.com/422#entry422comment</comments>
      <pubDate>Tue, 2 Apr 2024 22:35:14 +0900</pubDate>
    </item>
    <item>
      <title>Connection Pool Deadlock 해결하기!  (feat. REQUIRES_NEW)</title>
      <link>https://jaehoney.tistory.com/421</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 포스팅은 서비스를 오픈하면서 겪었던 이슈와 해결에 대해 공유한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드 및 내용은 예시를 위해 만든 프로젝트이다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;송금 서비스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;송금 서비스에서 은행 API를 호출해서 송금을 한다고 가정해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 예시를 위한 코드이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class TransferService {
    private final TransferRepository transferRepository;
    private final BankingRequestRepository bankingRequestRepository;
    private final BankingApiAdaptor bankingApiAdaptor;

    @Transactional
    public void transfer(TransferRequest transferRequest) {
        // Transfer를 생성 후 저장
        Transfer transfer = new Transfer(
            transferRequest.senderAccountId(),
            transferRequest.receiverAccountId(),
            transferRequest.amount()
        );
        transferRepository.save(transfer);

        // BankRequest 생성 후 저장
        BankingRequest bankingRequest = BankingRequest.from(transfer);
        bankingRequestRepository.save(bankingRequest);

        // BankingAPIAdaptor 호출
        // BankingAPI에서 처리 후 BankingRequest의 상태를 변경
        BankingResponse response = bankingApiAdaptor.banking(bankingRequest);

        // Transfer 상태 조정
        if (response.isSuccess())
            transfer.setSuccess();
        else
            transfer.setFail();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언뜻보면 문제가 없어 보이지만, 해당 코드는 정상적으로 실행되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BankingAPI에서 아래 로그가 찍혀있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;MySQLTransasctionRollbackException:
    Lock wait timeout exceeded; try restarting transaction&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락으로 인한 문제는 아래 SQL로 확인이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;# 실행중인 락 조회
SELECT * FROM performance_schema.data_locks;
# 실행중인 트랜잭션 조회
SELECT * FROM performance_schema.innodb_trx;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인을 확인한 결과는 다음과 같았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1431&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tv9ZI/btsGd1R9Zoc/XjkKRfMKyYwFWWrukJ8jok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tv9ZI/btsGd1R9Zoc/XjkKRfMKyYwFWWrukJ8jok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tv9ZI/btsGd1R9Zoc/XjkKRfMKyYwFWWrukJ8jok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftv9ZI%2FbtsGd1R9Zoc%2FXjkKRfMKyYwFWWrukJ8jok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1431&quot; height=&quot;276&quot; data-origin-width=&quot;1431&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransferService에서 아직 &lt;code&gt;commit&lt;/code&gt;을 수행하기 전이므로 BankingService에서 BankingRequest에 대한 잠금을 획득할 수 없다. 즉, 상태 변경을 할 때 Lock wait timeout이 발생하는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REQUIRES_NEW (문제 상황)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 문제 때문에 결론이 난 것이 propagation으로 REQUIRES_NEW를 사용해서 BankingRequest를 먼저 commit하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Transactional&lt;/code&gt;을 제거하는 것은 기존 코드가 너무 복잡해서 할 수 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class TransferService {
    private final TransferRepository transferRepository;
    private final BankingRequestSavePort bankingRequestSavePort;
    private final BankingApiAdaptor bankingApiAdaptor;

    @Transactional
    public void transfer(TransferRequest transferRequest) {
        // Transfer를 생성 후 저장
        Transfer transfer = new Transfer(
            transferRequest.senderAccountId(),
            transferRequest.receiverAccountId(),
            transferRequest.amount()
        );
        transferRepository.save(transfer);

        // BankRequest 생성 후 저장
        BankingRequest bankingRequest = BankingRequest.from(transfer);
        bankingRequestSavePort.save(bankingRequest);

        // BankingAPIAdaptor 호출
        // BankingAPI에서 처리 후 BankingRequest의 상태를 변경
        BankingResponse response = bankingApiAdaptor.banking(bankingRequest);

        // Transfer 상태 조정
        if (response.isSuccess())
            transfer.setSuccess();
        else
            transfer.setFail();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BankingRequestRepository&lt;/code&gt;를 직접적으로 호출하지 않고, &lt;code&gt;BankingRequestSavePort&lt;/code&gt;를 사용해서 호출한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class BankingRequestSavePort {
    private final BankingRequestRepository repository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void save(BankingRequest bankingRequest) {
        repository.save(bankingRequest);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BankingRequestSavePort&lt;/code&gt;는 &lt;code&gt;Propagation.REQUIRES_NEW&lt;/code&gt;로 repository.save()를 호출하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기능이 정상적으로 동작한다. 인수 테스트도 통과를 했고 해당 코드로 배포를 했다. 그런데 &lt;b&gt;부하 테스트&lt;/b&gt;를 하는데 &lt;b&gt;성공하다가, 성공 건이 없이 블록되다가.. 성공하다가 블록되다가.. 를 반복&lt;/b&gt;했다. &lt;b&gt;아래 에러&lt;/b&gt;가 계속 올라왔다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1446&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPyWGJ/btsGgaNDKEF/nMDOu3zJY3I7EYuVtMKPaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPyWGJ/btsGgaNDKEF/nMDOu3zJY3I7EYuVtMKPaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPyWGJ/btsGgaNDKEF/nMDOu3zJY3I7EYuVtMKPaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPyWGJ%2FbtsGgaNDKEF%2FnMDOu3zJY3I7EYuVtMKPaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1446&quot; height=&quot;230&quot; data-origin-width=&quot;1446&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 데드락인 줄 의심했는데 생각해보면 데드락일 수가 없었다. 해당 쿼리는 단일 레코드에 대해 수행하는 쿼리였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 &lt;b&gt;진짜 이유&lt;/b&gt;는 아래 HikariCP 로그에서 내뿜고 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1745&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R8JK8/btsGe8W3C44/lotIzIoRniZtk5Blj044h0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R8JK8/btsGe8W3C44/lotIzIoRniZtk5Blj044h0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R8JK8/btsGe8W3C44/lotIzIoRniZtk5Blj044h0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR8JK8%2FbtsGe8W3C44%2FlotIzIoRniZtk5Blj044h0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1745&quot; height=&quot;232&quot; data-origin-width=&quot;1745&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariPool을 보면 &lt;code&gt;active&lt;/code&gt; 상태인 것이 10개이고, &lt;code&gt;idle&lt;/code&gt;상태인 것이 없는 것을 확인할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HikariCP 데드락 (with. Nested Transaction)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP Log를 통해 특정 DB의 커넥션이 꽉차있는 상태로 &lt;code&gt;Blocking&lt;/code&gt;이 걸리는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 다시 보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class BankingRequestSavePort {
    private final BankingRequestRepository repository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void save(BankingRequest bankingRequest) {
        repository.save(bankingRequest);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드의 트랜잭션은 &lt;b&gt;메인 트랜잭션과 무관하게 반드시 커밋해야 하는 트랜잭션&lt;/b&gt;이라서 &lt;b&gt;전파 수준(propagation)&lt;/b&gt; 을 &lt;code&gt;REQUIRES_NEW&lt;/code&gt;로 해서 &lt;b&gt;새로운 커넥션&lt;/b&gt;으로 DB에 쿼리를 날리고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 트랜잭션과 무관하게 반드시 커밋하는 이유는 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 트랜잭션 내부에서 BankingAPI로 요청을 하고 있었다.&lt;/li&gt;
&lt;li&gt;DB 트랜잭션에서 커밋을 하기 전에 BankingAPI에서 해당 레코드를 수정할 수 없었다.&lt;/li&gt;
&lt;li&gt;그래서 전파 수준을 &lt;code&gt;REQUIRES_NEW&lt;/code&gt;로 새로운 트랜잭션에서 저장하도록 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;651&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UFuJk/btsGcPkx5gH/k3gKtkPywsSD06emHkTE81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UFuJk/btsGcPkx5gH/k3gKtkPywsSD06emHkTE81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UFuJk/btsGcPkx5gH/k3gKtkPywsSD06emHkTE81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUFuJk%2FbtsGcPkx5gH%2Fk3gKtkPywsSD06emHkTE81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;596&quot; height=&quot;651&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;651&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 커넥션 풀의 &lt;b&gt;10개의 커넥션이 모두 커넥션을 하나씩 점유한 채로 하나의 커넥션을 더 요구하면서 CP 데드락&lt;/b&gt;이 생겼던 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;HikariPool - Connection is not available, required timed out after 30000ms&lt;/code&gt;나 &lt;code&gt;Lock wait timeout exceeded&lt;/code&gt; 에러가 중요한 지표였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해서는 아래의 방법들이 있었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Connection Pool 확장&lt;/li&gt;
&lt;li&gt;Connection Timeout 설정&lt;/li&gt;
&lt;li&gt;트랜잭션을 커밋한 후 BankingAPI에 요청&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 Reference를 보면 1개의 커넥션으로 실행할 수 없는 코드는 HikariCP 데드락을 유발할 수 있는 코드라고 설명한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://techblog.woowahan.com/2663/&quot;&gt;https://techblog.woowahan.com/2663/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;(Nested Transaction을 사용하지 말자!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 사실상 1번과 2번은 임시 방편에 불과했다. 비주류 기능이면 고려할 수 있겠지만, 메인 기능인 부분이라서 CP 데드락이 실제 운영 중에도 터질 수 있어서 3번을 선택했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Nested Transaction 제거&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확하게는 트랜잭션을 커밋한 후 BankingAPI에 요청을 하도록 프로세스를 분리한 것이 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 되는 메서드의 &lt;code&gt;propagation = REQUIRES_NEW&lt;/code&gt;를 제거하고 요청 방식을 동기에서 &lt;b&gt;비동기&lt;/b&gt;로 변경했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 방식이 가능한 이유는 원래 Exception을 던지지 않았다. Banking에서 Exception이 터져도 catch해서 무조건 커밋을 하고 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 애초에 동기로 실행할 이유가 없었던 코드이고, 어필을 하고 있던 상황이었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 걱정되는 것은 BankingAPI 프로세스가 돌아가는 시점에 DB에 커밋이 되어 있는 것을 보장할 수 있느냐는 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메인 트랜잭션에서 DB에 INSERT 하면서 락을 획득했고 &lt;b&gt;Banking API에서는 이를 기다려야 데이터를 수정&lt;/b&gt;할 수 있기 때문에 순서가 보장되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 API 호출을 비동기 방식으로 변경했고, &lt;b&gt;CP 데드락이 더 이상 발생하지 않았다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성능도 많이 향상&lt;/b&gt;되었다. &lt;b&gt;API 호출도 병목 지점 중 하나&lt;/b&gt;였던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Server/Spring JPA</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/421</guid>
      <comments>https://jaehoney.tistory.com/421#entry421comment</comments>
      <pubDate>Sat, 30 Mar 2024 20:33:56 +0900</pubDate>
    </item>
    <item>
      <title>무지성 byte[] 사용하지 않기! (feat. Apache Commons Email)</title>
      <link>https://jaehoney.tistory.com/420</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포스팅 제목을 무지성&lt;code&gt;byte[]&lt;/code&gt; 사용하지 않기라고 했는데 불필요하게 전체 byte[] 할당하지 않기라고 봐주면 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 포스팅에서는 서비스 운영 중 &lt;code&gt;byte[]&lt;/code&gt;로 인해 심각한 문제가 생겼고, 오픈소스 기여까지 하게된 내용을 작성한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TPS가 심각하게 낮음&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메일 파일(약 20MB)을 읽어서 파일의 내용 중 일부를 화면에 노출하는 기능을 개발했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 POD 1대의 TPS가 1.7 정도밖에 나오지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1907&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0mPSU/btsF03J4rbS/TWXqkxBK22dAfYpq11PxkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0mPSU/btsF03J4rbS/TWXqkxBK22dAfYpq11PxkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0mPSU/btsF03J4rbS/TWXqkxBK22dAfYpq11PxkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0mPSU%2FbtsF03J4rbS%2FTWXqkxBK22dAfYpq11PxkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1907&quot; height=&quot;260&quot; data-origin-width=&quot;1907&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 HTTP 트랜잭션은 5.42초였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;172&quot; data-origin-height=&quot;28&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt1NUW/btsF2DDkRz6/8hKWqdz02cUneBlmRUzNLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt1NUW/btsF2DDkRz6/8hKWqdz02cUneBlmRUzNLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt1NUW/btsF2DDkRz6/8hKWqdz02cUneBlmRUzNLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt1NUW%2FbtsF2DDkRz6%2F8hKWqdz02cUneBlmRUzNLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;172&quot; height=&quot;28&quot; data-origin-width=&quot;172&quot; data-origin-height=&quot;28&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래와 같이 각 로직의 수행 시간을 측정해봤다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;819&quot; data-origin-height=&quot;40&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nJjOK/btsF1LPv6jK/AeAsuWIISPgGkwJ4ZDkhvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nJjOK/btsF1LPv6jK/AeAsuWIISPgGkwJ4ZDkhvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nJjOK/btsF1LPv6jK/AeAsuWIISPgGkwJ4ZDkhvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnJjOK%2FbtsF1LPv6jK%2FAeAsuWIISPgGkwJ4ZDkhvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;819&quot; height=&quot;40&quot; data-origin-width=&quot;819&quot; data-origin-height=&quot;40&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 parse 로직이 3787ms가 걸린다는 것을 알 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;42&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ohVud/btsF0sXy8HK/es8JQar2UJ3bw42d18R2PK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ohVud/btsF0sXy8HK/es8JQar2UJ3bw42d18R2PK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ohVud/btsF0sXy8HK/es8JQar2UJ3bw42d18R2PK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FohVud%2FbtsF0sXy8HK%2Fes8JQar2UJ3bw42d18R2PK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;261&quot; height=&quot;42&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;42&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;parse&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;parse()&lt;/code&gt;의 경우 외부 라이브러리(&lt;code&gt;apache-commons-email&lt;/code&gt;)의 로직만을 담고 있었다. 상세한 확인으로 아래 부분이 문제가 되었음을 알 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;705&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx2LHj/btsF3djUDRD/wakKPA56t4QUsOTPOxK3h1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx2LHj/btsF3djUDRD/wakKPA56t4QUsOTPOxK3h1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx2LHj/btsF3djUDRD/wakKPA56t4QUsOTPOxK3h1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx2LHj%2FbtsF3djUDRD%2FwakKPA56t4QUsOTPOxK3h1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;705&quot; height=&quot;384&quot; data-origin-width=&quot;705&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 부분은 메일의 첨부파일을 ByteArrayDataSource에 저장하는 부분이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tyEY4/btsF1SA0nZl/IpmtKd0nKmD6kP32jfWXc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tyEY4/btsF1SA0nZl/IpmtKd0nKmD6kP32jfWXc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tyEY4/btsF1SA0nZl/IpmtKd0nKmD6kP32jfWXc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtyEY4%2FbtsF1SA0nZl%2FIpmtKd0nKmD6kP32jfWXc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;432&quot; height=&quot;410&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장한 ByteArrayDataSource는 맵에 보관한다. 크게 2가지 문제가 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일의 전체 바이너리 데이터를 메모리에 할당한다.&lt;/li&gt;
&lt;li&gt;첨부파일을 읽지 않아도 되는 경우 성능이 낭비된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2가지 문제를 해결하기 위해 ByteArrayDataSource가 아니라 단순히 InputStream 기반의 DataSource를 저장하도록 코드를 수정했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C47ey/btsF09wyXUz/cKZFn8vkB0rowjk7CwWLNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C47ey/btsF09wyXUz/cKZFn8vkB0rowjk7CwWLNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C47ey/btsF09wyXUz/cKZFn8vkB0rowjk7CwWLNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC47ey%2FbtsF09wyXUz%2FcKZFn8vkB0rowjk7CwWLNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;521&quot; height=&quot;358&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGHL67/btsF3lhUu6g/KzRys7mVKuUa3rmEgQpXk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGHL67/btsF3lhUu6g/KzRys7mVKuUa3rmEgQpXk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGHL67/btsF3lhUu6g/KzRys7mVKuUa3rmEgQpXk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGHL67%2FbtsF3lhUu6g%2FKzRys7mVKuUa3rmEgQpXk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;775&quot; height=&quot;207&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;207&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, FileInputStream이 들어오면 굳이 &lt;code&gt;byte[]&lt;/code&gt;에 옮겨담지 말고 그대로 Wrapping해서 DataSource를 만든것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TPS 변화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 그 결과이다. 첨부파일 데이터가 필요 없는 경우 해당 데이터를 &lt;code&gt;byte[]&lt;/code&gt;에 할당할 필요가 없어져서 처리량이 올라갔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1969&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mqcfw/btsF1KC2gY7/U2g5MEkEW34sK0Nt9CYSGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mqcfw/btsF1KC2gY7/U2g5MEkEW34sK0Nt9CYSGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mqcfw/btsF1KC2gY7/U2g5MEkEW34sK0Nt9CYSGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmqcfw%2FbtsF1KC2gY7%2FU2g5MEkEW34sK0Nt9CYSGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1969&quot; height=&quot;262&quot; data-origin-width=&quot;1969&quot; data-origin-height=&quot;262&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리 사용량&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 전체 첨부파일을 &lt;code&gt;byte[]&lt;/code&gt;에 옮겨담고 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1759&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ecWrfo/btsF1zBHkun/87XjUvgqYkTWOjOuprNNb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ecWrfo/btsF1zBHkun/87XjUvgqYkTWOjOuprNNb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ecWrfo/btsF1zBHkun/87XjUvgqYkTWOjOuprNNb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FecWrfo%2FbtsF1zBHkun%2F87XjUvgqYkTWOjOuprNNb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1759&quot; height=&quot;364&quot; data-origin-width=&quot;1759&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bd5bt/btsF2EPLD91/gIEvgDVKNpzipG1Y7sxkYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bd5bt/btsF2EPLD91/gIEvgDVKNpzipG1Y7sxkYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bd5bt/btsF2EPLD91/gIEvgDVKNpzipG1Y7sxkYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBd5bt%2FbtsF2EPLD91%2FgIEvgDVKNpzipG1Y7sxkYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;870&quot; height=&quot;414&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 수정한 후에는 아래와 같이 메모리를 사용하지 않게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3551&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEpCrq/btsF3FtKaqV/KKoX0Tlfr62cwzTDwKMTe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEpCrq/btsF3FtKaqV/KKoX0Tlfr62cwzTDwKMTe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEpCrq/btsF3FtKaqV/KKoX0Tlfr62cwzTDwKMTe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEpCrq%2FbtsF3FtKaqV%2FKKoX0Tlfr62cwzTDwKMTe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3551&quot; height=&quot;752&quot; data-origin-width=&quot;3551&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 총 개선 결과이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TPS: 1.7 -&amp;gt; 21.7&lt;/li&gt;
&lt;li&gt;메모리 사용량: 약 30MB -&amp;gt; 거의 X&lt;/li&gt;
&lt;li&gt;평균 소요 시간: 14.82s -&amp;gt; 3.003s&lt;/li&gt;
&lt;li&gt;90% 요청 소요 시간: 21.247s -&amp;gt; 5.523s&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리한 부분은 &lt;code&gt;apache-commons-email&lt;/code&gt; 라이브러리에 기여하게 되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/commons-email/pull/159&quot;&gt;https://github.com/apache/commons-email/pull/159&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 파일을 다룰 때는 &lt;code&gt;byte[]&lt;/code&gt;나 ByteArrayInputStream 등에 전체 데이터를 담지 않도록 조심해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java IO나 Java NIO를 사용할 때는 최대한 Buffer를 사용해서 데이터를 주고받아 하고, 정말 필요한 순간에 데이터를 흘려보내는 것이 훨씬 효율적이다.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Language/Java</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/420</guid>
      <comments>https://jaehoney.tistory.com/420#entry420comment</comments>
      <pubDate>Sun, 24 Mar 2024 13:15:10 +0900</pubDate>
    </item>
    <item>
      <title>FixtureMonkey 적용 검토해보기!</title>
      <link>https://jaehoney.tistory.com/419</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJX1gT/btsF3olhCS3/kYmuRUnNoKHnvMfmRkBS91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJX1gT/btsF3olhCS3/kYmuRUnNoKHnvMfmRkBS91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJX1gT/btsF3olhCS3/kYmuRUnNoKHnvMfmRkBS91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJX1gT%2FbtsF3olhCS3%2FkYmuRUnNoKHnvMfmRkBS91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;306&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 팀에서 테스트를 위한 객체를 생성하는 패턴이나 라이브러리가 필요하게 되었다. 팀원 분이 FixtureMonkey를 추천해주셔서 POC를 진행하게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FixtureMonkey&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FixtureMonkey&lt;/b&gt;는 &lt;b&gt;Naver&lt;/b&gt;에서 만든 테스트 객체를 쉽게 생성하고 조작할 수 있도록 도와주는 Java 및 Kotlin 라이브러리이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FixtureMonkey는 한국어 docs를 지원한다. 오픈소스 중에 볼륨도 작은 편이라 한번 읽어보는 것도 추천드린다. 포스팅 내용은 요약 및 검토 정도로 봐주면 좋을 것 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://naver.github.io/fixture-monkey/v1-0-0-kor/docs/introduction&quot;&gt;https://naver.github.io/fixture-monkey/v1-0-0-kor/docs/introduction&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이전에 사용하지 않았던 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FixtureMonkey는 2년 전에 한번 봤었고 적용한다는 블로그를 많이 봤다. 하지만 버전이 낮고 Stars도 작아서 Minor 하다고 판단해서 적용하지 않았었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 부분에 대한 의문이 해소되지 않았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체를 검증을 통과할 수 있는 상태로 생성하기 위해 코드가 복잡할 것 같음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검증 로직과의 동기화 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테스트가 멱등하지 않을 것 같다.&lt;/li&gt;
&lt;li&gt;가독성이 좋지 않을 것 같다. (코드가 짧아지지만 의미를 노출하지 못할 것 같다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 인기가 생기고 있고 요청도 있으니 한번 검토를 해보자!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨셉&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FixtureMonkey는 공식적으로 4가지 컨셉을 언급한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 간결함&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 객체 생성이 매우 간단해진다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Product actual = fixtureMonkey.giveMeOne(Product.class);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 재사용성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 테스트에서 빌더를 재사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;ArbitraryBuilder&amp;lt;Product&amp;gt; actual = fixtureMonkey.giveMeBuilder(Product.class)
    .set(&quot;id&quot;, 1000L)
    .set(&quot;productName&quot;, &quot;Book&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 랜덤성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무작위 값으로 테스트 객체를 생성하므로 엣지 케이스를 발견할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;ArbitraryBuilder&amp;lt;Product&amp;gt; actual = fixtureMonkey.giveMeBuilder(Product.class);
then(actual.sample()).isNotEqualTo(actual.sample());&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 다용도성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속 / 순환 참조 / 익명 객체 등 다양한 경우에서 모두 동작한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;interface Foo {
    Bar getBar();
}

Foo foo = FixtureMonkey.create().giveMeOne(Foo.class);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메서드&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;giveMeOne&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 타입의 인스턴스가 필요하다면 &lt;code&gt;giveMeOne()&lt;/code&gt;을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val product: Product = fixtureMonkey.giveMeOne()
val strList: List&amp;lt;String&amp;gt; = fixtureMonkey.giveMeOne()&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;giveMe&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 인스턴스가 필요하다면 &lt;code&gt;giveMe()&lt;/code&gt;을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val productList: List&amp;lt;Product&amp;gt; = fixtureMonkey.giveMe(3)
val productSequence: Sequence&amp;lt;Product&amp;gt; = fixtureMonkey.giveMe()&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;giveMeBuilder&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스를 커스텀할 경우 &lt;code&gt;giveMeBuilder()&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val productBuilder: ArbitraryBuilder&amp;lt;Product&amp;gt; = fixtureMonkey.giveMeBuilder()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌더는 아래와 인스턴스를 생성하는 데 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val product = productBuilder.sample()
val productList = productBuilder.sampleList(3)
val productStream = productBuilder.sampleStream()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌더를 잘 정의해서 const화 시키면 유효한 여러가지 Case의 객체를 손쉽게 만들 수 있을 것으로 보인다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생성자 / 팩토리 메서드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 오버로딩, 정적 팩토리 메서드 등 다양한 방식으로 클래스를 정의했을 수 있다. 아래와 같이 메서드를 지정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 생성자 지정
 val product = fixtureMonkey.giveMeBuilder&amp;lt;Money&amp;gt;()
     .instantiateBy { 
         constructor&amp;lt;Money&amp;gt; { 
             parameter&amp;lt;Long&amp;gt;() 
         } 
     }
    .sample()

// 생성자 사용할 필드 지정
val product2 = fixtureMonkey.giveMeBuilder&amp;lt;Money&amp;gt;()
    .instantiateBy {
        constructor&amp;lt;Money&amp;gt; {
            parameter&amp;lt;Long&amp;gt;(&quot;amount&quot;)
        }
    }
    .sample()

// 팩토리 메서드 지정
val product3 = fixtureMonkey.giveMeBuilder&amp;lt;Money&amp;gt;()
    .instantiateBy {
        factory&amp;lt;Money&amp;gt;(&quot;from&quot;)
    }
    .sample()&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;유효성 검증 동기화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FixtureMonkey는 &lt;code&gt;jakarta.validation.constraints&lt;/code&gt; 기반의 어노테이션을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 엔터티 객체가 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Value
public class Money {
    @Min(0)
    long amount;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 의존을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;testImplementation(&quot;com.navercorp.fixturemonkey:fixture-monkey-jakarta-validation:1.0.14&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 해당 Validation을 통과하는 범위의 객체를 생성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
void test() {
    // given
    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .objectIntrospector(ConstructorPropertiesArbitraryIntrospector.INSTANCE)
        .plugin(new JakartaValidationPlugin())
        .build();

    // when
    Product actual = fixtureMonkey.giveMeOne(Money.class);

    // then
    then(actual).isNotNull();
    then(actual.getPrice()).isMoreThanOrEqualTo(0);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자에서 호출하는&amp;nbsp;&lt;code&gt;verifyAmount()&lt;/code&gt;와 같은 메서드를 통과할 때는 활용하기 어려울 것 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;객체 생성 Rule 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 단위 테스트에 필요한 객체를 만들 때는 Builder의 &lt;code&gt;set()&lt;/code&gt;을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
void test() {
    // given
    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .objectIntrospector(ConstructorPropertiesArbitraryIntrospector.INSTANCE)
        .build();
    long amount = 10000;

    // when
    Product actual = fixtureMonkey.giveMeBuilder(Money.class)
        .set(&quot;amount&quot;, amount)
        .sample();

    // then
    then(actual.getAmount()).isEqualTo(1000);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 도메인에서 Setter를 사용하지 않기 때문에 FixtureMonkey에서도 내부적으로 Reflection을 사용한다. 그래서 문자열로 필드명을 주게 되는데 필드명이 바뀌면 테스트가 깨질 것이라서 다소 아쉬운 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 코틀린을 사용하면 다르다. 코틀린에서는 &lt;code&gt;setExp()&lt;/code&gt;로 프로퍼티를 참조해서 커스텀한 객체를 생성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
fun test() {
    // given
    val fixtureMonkey = FixtureMonkey.builder()
        .plugin(KotlinPlugin())
        .build();

    // when
    val actual = fixtureMonkey.giveMeBuilder&amp;lt;Money&amp;gt;()
        .setExp(Product::amount, 1000L)
        .sample()

    // then
    then(actual.amount).isEqualTo(1000L)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 필드명이 변경되어도 IDE를 사용하고 있다면 테스트에 반영이 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Size&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;size()&lt;/code&gt;를 사용하면 프로퍼티의 크기를 지정할 수 있다. 최소값이나 최대값 지정도 가능하다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fixtureMonkey.giveMeBuilder&amp;lt;Product&amp;gt;()
    .sizeExp(Product::options, 5) // size:5

fixtureMonkey.giveMeBuilder&amp;lt;Product&amp;gt;()
    .sizeExp(Product::options, 3, 5) // minSize:3, maxSize:5

fixtureMonkey.giveMeBuilder&amp;lt;Product&amp;gt;()
    .minSizeExp(Product::options, 3) // minSize:3

fixtureMonkey.giveMeBuilder&amp;lt;Product&amp;gt;()
    .maxSizeExp(Product::options, 5) // maxSize:5&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Nullable&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 필드를 Null로 설정하거나 NotNull을 보장하기 위해서는 아래와 같이 &lt;code&gt;setNull()&lt;/code&gt;, &lt;code&gt;setNotNull()&lt;/code&gt;을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fixtureMonkey.giveMeBuilder&amp;lt;Product&amp;gt;()
    .setNullExp(Product::id)

fixtureMonkey.giveMeBuilder&amp;lt;Product&amp;gt;()
    .setNotNullExp(Product::id)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Post Condition&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 비즈니스의 경우 아래와 같이 &lt;code&gt;Predicate&lt;/code&gt;를 전달해서 조건을 만족하는 Fixture 객체를 생성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fixtureMonkey.giveMeBuilder(Product::class.java)
    .setPostConditionExp(Product::id, Long::class.java) { it: Long -&amp;gt; it &amp;gt; 0 }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;thenApply&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 타입에 따라 달라야하는 필드도 있을 수 있다. &lt;code&gt;thenApply()&lt;/code&gt;를 사용하면 이를 해결할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fixtureMonkey.giveMeBuilder(Product::class.java)
    .thenApply{it, builder -&amp;gt; builder.setExp(Product::name, it.type.toString())}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;acceptIf&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 조건에 따라 필드를 조정할 경우 &lt;code&gt;acceptIf()&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;fixtureMonkey.giveMeBuilder&amp;lt;Product&amp;gt;()
    .acceptIf(
        { it.productType == ProductType.CLOTHING },
        { builder -&amp;gt; builder.setExp(Product::price, 1000) }
    )&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 데이터를 위한 Fixture를 어떻게 관리할 지 고민을 했었다. 사실 개인적으로는 되게 편한 라이브러리면서도 다소 아쉬운 부분이 존재하는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 의문이 들었던 부분에 대한 정리이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체를 검증을 통과할 수 있는 상태로 생성하기 위해 코드가 복잡할 것 같음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제로 다소 복잡한 것이 맞는 것 같다.&lt;/li&gt;
&lt;li&gt;Fixture와 실제 클래스 명세와 동기화 하는 것도 어려울 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테스트가 멱등하지 않을 것 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제로 그런 것 같다. 랜덤성의 경우 엣지 케이스를 잡을 수 있다는 장점이 있다. 하지만, 불완전한 테스트를 짜고 그 것이 나중에 발견되는 것이 큰 이점이 있을까..의 의문이 사라지진 않는 것 같다.&lt;/li&gt;
&lt;li&gt;그냥 테스트 커버리지를 100%로 맞추면 되는 것이 아닐까 싶기도 하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;가독성이 좋지 않을 것 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 부분은 private method를 잘 활용한다면 가독성이 괜찮게 관리될 수는 있을 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알게된 장점은 FixtureMonkey는 Interface, Generic, Self reference class, Seald class 등 대부분의 상황에서 Fixture 생성을 지원한다. 그런 부분들은 충분히 장점인 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 프로퍼티를 설정할 때 변수명(String)을 사용하지 않을 수 있어서 리팩토링 내성 부분도 개선되는 것 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://naver.github.io/fixture-monkey/v1-0-0-kor/docs/introduction/overview&quot;&gt;https://naver.github.io/fixture-monkey/v1-0-0-kor/docs/introduction/overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Server/JUnit, Spock</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/419</guid>
      <comments>https://jaehoney.tistory.com/419#entry419comment</comments>
      <pubDate>Sat, 23 Mar 2024 23:05:26 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux - ServerSentEvent 이해하기!</title>
      <link>https://jaehoney.tistory.com/418</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Server Sent Event&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server Sent Event는 서버에서 클라이언트에게 일방적으로 이벤트를 전달하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에서 서버의 이벤트를 구독하기 위해서는 Polling 방식을 주로 사용했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Polling&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Polling 방식은 Client에서 Server에게 특정 주기로 요청을 보내서 데이터를 조회하는 방식이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciZM6C/btsFEtWapN5/EcoGkOZTOlZESICB1O5luk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciZM6C/btsFEtWapN5/EcoGkOZTOlZESICB1O5luk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciZM6C/btsFEtWapN5/EcoGkOZTOlZESICB1O5luk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciZM6C%2FbtsFEtWapN5%2FEcoGkOZTOlZESICB1O5luk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;746&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현이 간단하지만, 실시간성이 떨어지고 불필요한 네트워크 요청을 지속적으로 하기 때문에 자원을 낭비하게 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Long Polling&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 Polling의 문제를 해결하기 위해 Long Polling 기법이 나왔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;673&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bogELZ/btsFFQXVx9N/4WaJOXaGn8yV9vbNbokq4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bogELZ/btsFFQXVx9N/4WaJOXaGn8yV9vbNbokq4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bogELZ/btsFFQXVx9N/4WaJOXaGn8yV9vbNbokq4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbogELZ%2FbtsFFQXVx9N%2F4WaJOXaGn8yV9vbNbokq4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;673&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;673&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Long Polling 방식은 서버가 요청을 받은 후 데이터가 생길 때까지 기다렸다가 응답을 보내는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 불필요한 요청 수를 줄이고 실시간성을 보장할 수 있게 된다. 하지만, Event의 빈도가 잦다면 여전히 연결이 아주 많이 발생하는 문제가 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Http Streaming&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Http Streaming 방식은 위 문제들을 해결한 방식이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;241&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5NOMD/btsFGofenib/YRQlKFgAIO6UeyI1e0iB8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5NOMD/btsFGofenib/YRQlKFgAIO6UeyI1e0iB8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5NOMD/btsFGofenib/YRQlKFgAIO6UeyI1e0iB8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5NOMD%2FbtsFGofenib%2FYRQlKFgAIO6UeyI1e0iB8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;241&quot; height=&quot;361&quot; data-origin-width=&quot;241&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 연결을 유지하고 데이터가 생길 때마다 전달할 이벤트, 데이터를 Chunk 단위로 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Http Streaming의 특징이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Transfer-Encoding 헤더
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Transfer-Encoding: chunked를 헤더에 추가한다.&lt;/li&gt;
&lt;li&gt;빈 Chunk를 전달하기 전까지 값을 읽는다.&lt;/li&gt;
&lt;li&gt;HTTP/1.1 이상에서만 사용할 수 있다. (Connection: keep-alive)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;EOF
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection: close를 헤더에 추가한다.&lt;/li&gt;
&lt;li&gt;서버가 연결을 종료할 때까지 들어오는 값을 읽는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Content-Length를 알 수 없다.&lt;/li&gt;
&lt;li&gt;데이터를 청크 단위로 전송하므로 서버 입장에서 효율적으로 튜닝이 가능해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게하면 이벤트를 클라이언트에게 효율적으로 내려줄 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Server Sent Event&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring WebFlux에서 ServerSentEvent를 사용해서 HTTP Streaming을 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring WebFlux에서는 Handler의 Return Type으로 &lt;code&gt;Flux&amp;lt;ServerSentEvent&amp;gt;&lt;/code&gt;, &lt;code&gt;Observable&amp;lt;ServerSentEvent&amp;gt;&lt;/code&gt;를 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로는 Chunked Transfer-Encoding 기반으로 아래와 같은 데이터를 전송한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;id:0
event:add
:comment-i
data:data-0

id:1
event:add
:comment-i
data:data-1

id:2
event:add
:comment-i
data:data-2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 데이터는 아래 특징을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;chunk 단위로 여러 줄로 구성된 문자열을 전달한다.&lt;/li&gt;
&lt;li&gt;new line으로 이벤트를 구분한다.&lt;/li&gt;
&lt;li&gt;문자열은 {field}:{value} 형태로 구성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드는 아래 값을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id: 이벤트의 id를 가리킨다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Client에서는 id를 저장해서 Last-Event-ID 헤더를 첨부하고, 서버는 해당 id 이후의 이벤트만 보낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;event: 이벤트의 타입&lt;/li&gt;
&lt;li&gt;data: 이벤트의 데이터, 데이터가 많으면 Multi line으로 구성한다.&lt;/li&gt;
&lt;li&gt;retry: reconnection을 위한 대기 시간을 클라이언트에게 전달&lt;/li&gt;
&lt;li&gt;comment(Empty): 정보를 남기기 위한 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ServerSentEventHttpMessageWriter&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring WebFlux는 Servlet Stack과 다르게 ~Converter가 아니라 ~Writer를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ServerSentEventHttpMessageWriter는 객체를 ServerSentEvent 형태로 encode해서 write하는 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;public class ServerSentEventHttpMessageWriter implements HttpMessageWriter&amp;lt;Object&amp;gt; {
    private static final MediaType DEFAULT_MEDIA_TYPE = 
            new MediaType(&quot;text&quot;, &quot;event-stream&quot;, StandardCharsets.UTF_8);
    private static final List&amp;lt;MediaType&amp;gt; WRITABLE_MEDIA_TYPES =
            Collections.singletonList(MediaType.TEXT_EVENT_STREAM);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ServerSentEventHttpMessageWriter는 MediaType으로 &quot;event-stream&quot;을 찾아서 핸들링한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부를 보면 ServerSentEvent가 input으로 들어오면 그대로 사용하고, 아니라면 ServerSentEvent로 변환한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8vTDU/btsFFPED1CK/kcdV2Ce4BSzjmnKmguylZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8vTDU/btsFFPED1CK/kcdV2Ce4BSzjmnKmguylZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8vTDU/btsFFPED1CK/kcdV2Ce4BSzjmnKmguylZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8vTDU%2FbtsFFPED1CK%2FkcdV2Ce4BSzjmnKmguylZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;796&quot; height=&quot;248&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Content-Type으로 &lt;code&gt;text/event-stream&lt;/code&gt;을 사용하면 ServerSentEvent로 응답을 내릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ServerSentEvent는 내부적으로 Flux의 값을 조금씩 흘려보낸다. 해당 부분에 대해 더 알아보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Controller 구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 Controller는 Content-Type으로 &lt;code&gt;text/event-stream&lt;/code&gt;을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Controller
public class SseController {
    @ResponseBody
    @GetMapping(path = &quot;/sse&quot;, produces = &quot;text/event-stream&quot;)
    Flux&amp;lt;String&amp;gt; sse() {
        return Flux.interval(Duration.ofMillis(1000))
                .map(i -&amp;gt; &quot;VioletBeach: &quot; + i);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 아래와 같이 초마다 1Line씩 데이터가 출력된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;352&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSyHL9/btsFD5utKDH/9VPkBySrS85eFCkMbvPWCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSyHL9/btsFD5utKDH/9VPkBySrS85eFCkMbvPWCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSyHL9/btsFD5utKDH/9VPkBySrS85eFCkMbvPWCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSyHL9%2FbtsFD5utKDH%2F9VPkBySrS85eFCkMbvPWCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;352&quot; height=&quot;376&quot; data-origin-width=&quot;352&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 상세한 응답을 하고 싶다면 &lt;code&gt;ServerSentEventBuilder&lt;/code&gt;를 사용해서 &lt;code&gt;ServerSentEvent&lt;/code&gt;를 생성해서 직접 반환하면 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface Builder&amp;lt;T&amp;gt; {
    Builder&amp;lt;T&amp;gt; id(String id);

    Builder&amp;lt;T&amp;gt; event(String event);

    Builder&amp;lt;T&amp;gt; retry(Duration retry);

    Builder&amp;lt;T&amp;gt; comment(String comment);

    Builder&amp;lt;T&amp;gt; data(@Nullable T data);

    ServerSentEvent&amp;lt;T&amp;gt; build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;알림 서버 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ServerSentEvent는 Chunk 단위의 데이터를 내려주는 것에도 의미가 있지만, 처음 언급했던 대로 알림 서버를 구현하는 데 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/notifications&quot;)
@RequiredArgsConstructor
public class NotificationController {
    private static AtomicInteger lastEventId = new AtomicInteger(1);
    private final NotificationService notificationService;

    @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux&amp;lt;ServerSentEvent&amp;lt;String&amp;gt;&amp;gt; getNotifications() {
        return notificationService.getMessageFromSink()
                .map(message -&amp;gt; {
                    String id = lastEventId.getAndIncrement() + &quot;&quot;;
                    return ServerSentEvent
                            .builder(message)
                            .event(&quot;notification&quot;)
                            .id(id)
                            .comment(&quot;this is notification&quot;)
                            .build();
                });
    }

    @PostMapping
    public Mono&amp;lt;String&amp;gt; addNotification(@RequestBody Event event) {
        String message = event.getType() + &quot;: &quot; + event.getMessage();
        notificationService.addMessage(message);
        return Mono.just(&quot;ok&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 사용자는 &lt;code&gt;GET /api/notifications&lt;/code&gt;를 1번 호출하는 것 만으로 이후의 Notification을 지속적으로 받아올 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Websocket API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE(Server Sent Event) 방식과 유사한 방법 방식으로 Websocket이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE는 서버에서 클라이언트로만 메시지를 보내는 반면, Websocket 방식은 양방향 통신이 가능하다.하지만, SSE는 HTTP 위에서 동작하고 효율적이라는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webflux에서는 Websocket을 매우 편리하게 구성하기 도구를 제공한다. 이 부분에 대해서는 아래 공식 문서를 참고하자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/web/webflux-websocket.html&quot;&gt;https://docs.spring.io/spring-framework/reference/web/webflux-websocket.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://fastcampus.co.kr/courses/216172&quot;&gt;https://fastcampus.co.kr/courses/216172&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/techieahead/http-short-vs-long-polling-vs-websockets-vs-sse-8d9e962b2ba8&quot;&gt;https://medium.com/techieahead/http-short-vs-long-polling-vs-websockets-vs-sse-8d9e962b2ba8&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Server/Spring Reactive</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/418</guid>
      <comments>https://jaehoney.tistory.com/418#entry418comment</comments>
      <pubDate>Sun, 10 Mar 2024 11:07:58 +0900</pubDate>
    </item>
    <item>
      <title>Ktlint에서 라인 생성 Rule을 Disabled하기!</title>
      <link>https://jaehoney.tistory.com/415</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;새로 합류한 팀에서 ktlint를 적용하다가 생긴 문제와 해결 방법에 대해 공유한다.&lt;/p&gt;
&lt;p&gt;현재 팀에서 &lt;strong&gt;IntelliJ의 Ktlint 플러그인&lt;/strong&gt;을 사용하고 있고 &lt;strong&gt;Actions on Save 기능&lt;/strong&gt;을 사용해서 코드를 저장할 때마다 코드 스타일을 반영한다.&lt;/p&gt;
&lt;p&gt;그런데 &lt;strong&gt;나한테만 기존 코드의 변경사항이 너무 많았다.&lt;/strong&gt; 자꾸 &lt;strong&gt;라인이 아래와 같이 추가&lt;/strong&gt;되고 있었다.&lt;/p&gt;
&lt;h2 id=&quot;라인-추가되는-이슈&quot;&gt;라인 추가되는 이슈&lt;/h2&gt;
&lt;p&gt;아래는 기존의 코드이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;kotlin language-kotlin&quot;&gt;fun getUserTypeCode(user: User): Any = when (user.type) {
    Type.MEMBER -&amp;gt; if (true) { 1 } else { 2 }
    Type.ADMIN -&amp;gt; if (true) { 2 } else { 1 }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다른 팀원들 모두 이슈가 없는데, 내가 저장하면 아래와 같이 코드가 변경된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;kotlin language-kotlin&quot;&gt;fun getUserTypeCode(user: User): String =
        when (user.type) {
            Type.MEMBER -&amp;gt;
                if (true) {
                    1
                } else {
                    2
                }

            Type.ADMIN -&amp;gt;
                if (true) {
                    2
                } else {
                    1
                }
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음에는 CodeStyle이 문제인 줄 알았는데, Ktlint를 끄니까 변경되지 않는 것을 확인했다.&lt;/p&gt;
&lt;p&gt;즉, Ktlint가 문제였다.&lt;/p&gt;
&lt;h2 id=&quot;원인-확인&quot;&gt;원인 확인&lt;/h2&gt;
&lt;p&gt;Ktlint 공식문서를 뒤져보다가 아래 부분을 발견했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pinterest.github.io/ktlint/0.49.1/rules/experimental/#multiline-expression-wrapping&quot;&gt;https://pinterest.github.io/ktlint/0.49.1/rules/experimental/#multiline-expression-wrapping&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;해당 기능이 내가 겪는 문제와 매우 유사했다. 우변이 Multi-line이면 NewLine에서 시작하게 해주는 기능이라고 한다.&lt;/p&gt;
&lt;p&gt;문서에도 아래와 같이 설명이 나와있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;kotlin language-kotlin&quot;&gt;// AS-IS
val foo = foo(
        parameterName = &quot;The quick brown fox &quot;
                .plus(&quot;jumps &quot;)
                .plus(&quot;over the lazy dog&quot;),
)

// TO-BE
val foo =
        foo(
                parameterName =
                &quot;The quick brown fox &quot;
                        .plus(&quot;jumps &quot;)
                        .plus(&quot;over the lazy dog&quot;),
        )&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ktlint 플러그인의 Mode를 Manual로 바꾸면 저장할 때 코드를 반영하지 않고, 경고를 표시해준다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btArg7/btsFkXRrvpX/KobFzqsogBNl2sBE2oUuHK/img.png&quot; alt=&quot;img_4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;경고를 보니까 실제로도 multiline-expression-wrapping 룰 때문에 문제가 되는 것을 알 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q1XVh/btsFs0L886r/fi0qM9kxlQ38hSyFvpxAa0/img.png&quot; alt=&quot;img_5.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;그런데 왜 문제가 나한테만 적용될까 하는 것이었다.&lt;/p&gt;
&lt;p&gt;나는 그 원인을 Ktlint의 Github Repository의 &lt;code&gt;CHANGELOG.md&lt;/code&gt;에서 찾았다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pinterest/ktlint/blob/master/CHANGELOG.md&quot;&gt;https://github.com/pinterest/ktlint/blob/master/CHANGELOG.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;버전-이슈&quot;&gt;버전 이슈&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;multiline-expression-wrapping&lt;/code&gt; 키워드로 검색한 결과 라이브러리 버전 &lt;strong&gt;&lt;code&gt;0.49.0&lt;/code&gt;&lt;/strong&gt;부터 해당 rule이 도입되었다는 것을 알 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ck3U8n/btsFm5HZWAq/jUVCqAcPz01hVEHfC1Qyrk/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;내가 사용하는 Ktlint 플러그인은 &lt;code&gt;0.20.0&lt;/code&gt; 버전이라서 라이브러리 기준 &lt;code&gt;1.1.0&lt;/code&gt; 버전이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwZq4B/btsFuIEtf3U/ukddJZoi0KhvoJryKR7bJk/img.png&quot; alt=&quot;img_1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;반면, 다른 팀원들이 사용하는 Ktlint 플러그인 버전은 &lt;code&gt;0.13.0&lt;/code&gt; 버전이라서 라이브러리 기준 &lt;strong&gt;&lt;code&gt;0.48.2&lt;/code&gt;&lt;/strong&gt; 버전이었다.&lt;/p&gt;
&lt;p&gt;팀원들의 Ktlint 버전은 라이브러리 기준 0.49.0 이전이라서 해당 옵션이 지원되지 않아 줄 변경이 없었던 것이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rDULo/btsFqJ5dAgL/4ABXuGGOMUlj53MJmMK9pk/img.png&quot; alt=&quot;img_2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;정리하면 IntelliJ 버전에 따른 Ktlint 플러그인의 버전 차이였다. 나는 신규 버전의 IntelliJ를 설치했었다.&lt;/p&gt;
&lt;h2 id=&quot;해결&quot;&gt;해결&lt;/h2&gt;
&lt;p&gt;해당 사항을 팀원들에게 공유했고, IntelliJ를 업그레이드 한 분도 있었고 유지하시는 분도 있었다.&lt;/p&gt;
&lt;p&gt;그래서 해당 Option을 끌 수 있는 방법을 찾기로 했다. 과거 버전을 쓰는 팀원을 위해서도 있지만, 애초에 newLine이 생소하고 가독성이 떨어진다는 의견이 많았다.&lt;/p&gt;
&lt;p&gt;Ktlint는 &lt;code&gt;.editorconfig&lt;/code&gt; 파일을 사용하므로 Project의 root에 해당 파일을 생성한다.&lt;/p&gt;
&lt;p&gt;그리고 공식 문서에 룰을 설정하는 방법이 있다. 해당 옵션의 경우 StandardRule 이라서 아래와 같이 작성하면 된다. &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VEisC/btsFqaBN26o/Y3tpaJ60YxJcrqtbVAdkH1/img.png&quot; alt=&quot;img_3.png&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[*.{kt,kts}]
# 우변이 Multi Line일 경우 새로운 Line에서 시작 (off)
ktlint_standard_multiline-expression-wrapping: disabled
# 함수 파라미터 각각 새로운 Line 할당 (off)
# 함수 정의 시 반환 값 Line을 새로운 Line에서 시작 (off)
ktlint_standard_function-signature: disabled&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;반영 후 더 이상 문제가 재현되지 않았다.&lt;/p&gt;
&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;
&lt;p&gt;협업하는 팀원들끼리의 Coding Convention을 위해 CodeStyle과 Ktlint 같은 도구를 많이 사용한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(혹시 사용하지 않고 있다면, 강력하게 추천한다!!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;버전의 차이로 인해서 팀원과 Rule이 다른 부분이 발생할 수 있다.&lt;/p&gt;
&lt;p&gt;이 부분은 &lt;code&gt;.editorconfig&lt;/code&gt;에서 Rule을 명시하거나 버전을 맞추면 해결할 수 있다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>Language/Kotlin</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/415</guid>
      <comments>https://jaehoney.tistory.com/415#entry415comment</comments>
      <pubDate>Sat, 2 Mar 2024 11:04:16 +0900</pubDate>
    </item>
    <item>
      <title>SQL - LEFT OUTER JOIN 쿼리 Split 하기!</title>
      <link>https://jaehoney.tistory.com/414</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;지난번 커버링 인덱스를 적용한 쿼리를 추가 개선한 이야기이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;커버링 인덱스 적용 - &lt;a href=&quot;https://jaehoney.tistory.com/333&quot;&gt;https://jaehoney.tistory.com/333&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;지난번 조회 Latency가 너무 커서 커버링인덱스를 조회해서 2~3배, 최대 10배 효율적으로 개선을 할 수 있었다.&lt;/p&gt;
&lt;p&gt;문제는 Latency는 큰 문제가 없음에도 TPS가 너무 안나왔다. HTTP 트랜잭션이 0.3s~0.4s 정도인 반면 TPS가 10.8 밖에 안나왔다.&lt;/p&gt;
&lt;p&gt;결과를 먼저 소개하자면 &lt;strong&gt;LeftOuterJoin 구을 Split&lt;/strong&gt;해서 &lt;strong&gt;TPS를 10.8에서 107.7로 개선&lt;/strong&gt;했다.&lt;/p&gt;
&lt;p&gt;아래는 문제 재현을 위해 임의로 구성한 환경에서 테스트를 진행한 부분이다.&lt;/p&gt;
&lt;h2 id=&quot;기존-코드&quot;&gt;기존 코드&lt;/h2&gt;
&lt;p&gt;먼저 기존 코드를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Repository
public class ArticleRepositoryImpl {
    private final JPAQueryFactory jpaQueryFactory;
    private final CategoryRepository categoryRepository;
    private final Querydsl querydsl;

    public ArticleRepositoryImpl(JPAQueryFactory jpaQueryFactory, CategoryRepository categoryRepository,
        EntityManager entityManager) {
        this.jpaQueryFactory = jpaQueryFactory;
        this.categoryRepository = categoryRepository;
        this.querydsl = new Querydsl(entityManager, new PathBuilderFactory().create(Article.class));
    }

    public Page&amp;lt;ArticleInfo&amp;gt; findList(String regionCode, Pageable pageable) {
        // 조회 대상 id 목록을 커버링 인덱스로 조회한다.
        JPAQuery&amp;lt;Long&amp;gt; idsQuery = jpaQueryFactory
            .select(article.articleId)
            .from(article)
            .leftJoin(category).on(category.categoryId.eq(article.categoryId))
            .where(
                article.regionCode.eq(regionCode),
                category.isPublic.eq(true).or(article.categoryId.isNull())
            );

        // 페이지 네이션 적용 (offset, limit, sort) 후 쿼리 실행
        List&amp;lt;Long&amp;gt; ids = querydsl.applyPagination(pageable, idsQuery).fetch();

        // 카운트 쿼리
        JPAQuery&amp;lt;Long&amp;gt; countQuery = createCountQuery(idsQuery.getMetadata().getWhere());

        // 실제 데이터 블록 조회 쿼리
        JPAQuery&amp;lt;ArticleInfo&amp;gt; dataQuery = jpaQueryFactory.select(new QArticleInfo(article, articleAuth))
            .from(article)
            .leftJoin(articleAuth)
            .on(articleAuth.articleId.eq(article.articleId))
            .where(
                article.articleId.in(ids)
            );

        List&amp;lt;ArticleInfo&amp;gt; result = querydsl.applySorting(pageable.getSort(), dataQuery).fetch();

        return PageableExecutionUtils.getPage(result, pageable, countQuery::fetchOne);
    }

    private JPAQuery&amp;lt;Long&amp;gt; createCountQuery(Predicate whereCondition) {
        return jpaQueryFactory.select(article.count())
            .from(article)
            .where(whereCondition)
            .leftJoin(category)
            .on(category.categoryId.eq(article.categoryId));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 쿼리를 정리하면 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;sql language-sql&quot;&gt;# 1. 커버링 인덱스 기반 id 리스트 조회
select article.article_id
from article
left outer join
     category on (category.category_id = article.category_id)
where
    article.region_code = 'JP' and
    (category.is_public = true or category.category_id is null)
order by article.article_id desc
    limit 0, 20;

# 2. 카운트 쿼리
select count(1)
from article
left outer join
     category on (category.category_id = article.category_id)
where
    article.region_code = 'JP' and
    (category.is_public = true or category.category_id is null)

# 3. 데이터 블록 조회
select article.article_id,
       article.subject,
       article.content,
       article_auth.article_auth_id
       # .. 생략
from article
left outer join
     article_auth on (article_auth.article_id = article.article_id)
where article.article_id in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
order by article.article_id desc&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;전체 HTTP 트랜잭션은 477ms 정도 소요되었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqiMfS/btsFkGPgHE9/73GRLSWIlo4GEGHtswIOak/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;TPS를 측정해보니 VUsers 50 기준 2.0이 나왔다. (예시를 위한 데이터를 구성해서 그런 지 더 안좋게 나왔다.)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AqGUR/btsFpTl2j9L/AGKkGqbTMPtxMNyDQmQvt0/img.png&quot; alt=&quot;img_1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;문제가 되는 주요 쿼리는 아래 쿼리이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;sql language-sql&quot;&gt;select count(1)
from article
left outer join
     category on (category.category_id = article.category_id)
where
    article.region_code = 'JP' and
    (category.is_public = true or category.category_id is null)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 카운트 쿼리의 실행계획을 한번 보자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H8Vi5/btsFoeKWclX/p8kzM9oNAI0P9hRmuksVj0/img.png&quot; alt=&quot;img_2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;그렇게 큰 문제가 없어 보인다. 인덱스도 잘 타고 있다.&lt;/p&gt;
&lt;p&gt;도대체 뭐가 문제인 걸까? 열심히 찾아보다가 article과 category가 조인하고 필터링하는 부분에서 성능이 저하됨을 알 수 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;sql language-sql&quot;&gt;left outer join
    category on (category.category_id = article.category_id)
where
    article.region_code = 'JP' and
    (category.is_public = true or category.category_id is null)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 쿼리는 의도와 정확하게 일치하는 쿼리이다. &lt;/p&gt;
&lt;p&gt;해당 경우는 Join할 레코드가 없으므로 &lt;code&gt;category.category_id is null&lt;/code&gt;로 체크한다. 요구사항을 정리하면 아래와 같다.&lt;br /&gt;
article이 기본 category와 매핑된 것도 있다. 기본 category는 테이블에 보관하지 않는다. categoryId가 default, notice, event인 경우이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;region_code가 'JP'인 레코드만 조회한다.&lt;/li&gt;
&lt;li&gt;category의 is_public이 true 거나 조인할 카테고리가 없는 데이터만 노출한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;중요한 점은 Left Outer Join으로 인해 아래의 문제&lt;/strong&gt;가 있었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Public Category를 필터링하는 과정이 복잡해진다.&lt;/li&gt;
&lt;li&gt;가상 테이블에서 Article과 Category를 매칭하는 과정 필요&lt;/li&gt;
&lt;li&gt;전체 SQL 과정이 다루는 데이터의 크기가 훨씬 커진다.&lt;/li&gt;
&lt;li&gt;쿼리 캐시를 사용하기 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;개선을 고민해보다가 &lt;code&gt;JOIN&lt;/code&gt; 쿼리를 분리해보게 되었다.&lt;/p&gt;
&lt;h2 id=&quot;쿼리-분리&quot;&gt;쿼리 분리&lt;/h2&gt;
&lt;p&gt;해당 쿼리를 아래와 같이 분리하면 나아질 수도 있을 것 같다는 생각이 들었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Public Category ID 목록만 추출 (Default Category 포함)&lt;/li&gt;
&lt;li&gt;Article을 질의할 때는 categoryId 조건만을 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;해당과 같이 분리하면 Join도 필요가 없었고, 필터링을 위한 조건도 필요가 없었다.&lt;/p&gt;
&lt;p&gt;결과적으로 코드가 아래와 같이 변경되었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Repository
public class ArticleRepositoryImpl {
    private final JPAQueryFactory jpaQueryFactory;
    private final CategoryRepository categoryRepository;
    private final Querydsl querydsl;
    private final List&amp;lt;String&amp;gt; defaultCategoryIdList = List.of(&quot;default&quot;, &quot;notice&quot;, &quot;event&quot;);

    public ArticleRepositoryImpl(JPAQueryFactory jpaQueryFactory, CategoryRepository categoryRepository,
        EntityManager entityManager) {
        this.jpaQueryFactory = jpaQueryFactory;
        this.categoryRepository = categoryRepository;
        this.querydsl = new Querydsl(entityManager, new PathBuilderFactory().create(Article.class));
    }

    public Page&amp;lt;ArticleInfo&amp;gt; findList(String regionCode, Pageable pageable) {
        // 카테고리 조회
        List&amp;lt;String&amp;gt; publicCategoryIds = categoryRepository.findAllByRegionCode(regionCode)
            .stream()
            .filter(Category::isPublic)
            .map(Category::getCategoryId)
            .collect(Collectors.toList());

        publicCategoryIds.addAll(defaultCategoryIdList);

        // 커버링 인덱스 기반 id 리스트 조회
        JPAQuery&amp;lt;Long&amp;gt; idsQuery = jpaQueryFactory
            .select(article.articleId)
            .from(article)
            .where(
                article.regionCode.eq(regionCode),
                article.categoryId.in(publicCategoryIds)
            );

        List&amp;lt;Long&amp;gt; ids = querydsl.applyPagination(pageable, idsQuery).fetch();

        // 카운트 쿼리
        JPAQuery&amp;lt;Long&amp;gt; countQuery = createCountQuery(idsQuery.getMetadata().getWhere());

        // 데이터 블록 쿼리
        JPAQuery&amp;lt;ArticleInfo&amp;gt; dataQuery = jpaQueryFactory.select(new QArticleInfo(article, articleAuth))
            .from(article)
            .leftJoin(articleAuth)
            .on(articleAuth.articleId.eq(article.articleId))
            .where(
                article.articleId.in(ids)
            );

        List&amp;lt;ArticleInfo&amp;gt; result = querydsl.applySorting(pageable.getSort(), dataQuery).fetch();

        return PageableExecutionUtils.getPage(result, pageable, countQuery::fetchOne);
    }

    private JPAQuery&amp;lt;Long&amp;gt; createCountQuery(Predicate whereCondition) {
        return jpaQueryFactory.select(article.count())
            .from(article)
            .where(whereCondition);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 쿼리는 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;sql language-sql&quot;&gt;# 1. PUBLIC 카테고리 전체 검색
select category_id
from category
where region_code = 'JP' and is_public = true

# 2. 커버링 인덱스 기반 id 리스트 조회
select article.article_id
from article
where
    article.region_code = 'JP' and
    article.category_id in ('1', '2', '3', '4', '5', 'default', 'notice', 'event')
order by article.article_id desc
limit 0, 20;

# 3. 카운트 쿼리
select count(1)
from article
where
    article.region_code = 'JP' and
    article.category_id in ('1', '2', '3', '4', '5', 'default', 'notice', 'event')

# 4. 데이터 블록 조회
select article.article_id,
       article.subject,
       article.content,
       article_auth.article_auth_id
       # .. 생략
from article
left outer join
     category on (category.category_id = article.category_id)
left outer join
     article_auth on (article_auth.article_id = article.article_id)
where article.article_id in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
        11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
order by article.article_id desc&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;필터링할 카테고리 id 목록 조회를 먼저한다. 이후 쿼리에서는 &lt;strong&gt;카테고리 조인과 카테고리 필터 조건을 제거&lt;/strong&gt;하고 &lt;strong&gt;필터링한 id 목록을 기반으로 in 조건&lt;/strong&gt;만 걸 수 있었다.&lt;/p&gt;
&lt;p&gt;전체 HTTP 트랜잭션은 421ms 정도 소요되었다. 쿼리 결과도 당연히 동일하다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctUdXo/btsFmmvScy4/tkWK5fCTGHkbQLuMqOyb70/img.png&quot; alt=&quot;img_3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;그러나 TPS를 측정해보니 VUsers 50 기준 11.5가 나왔다. 2.0에서 5배 이상 개선된 결과이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5t8j4/btsFn8xbUBI/KPfYmfLdLFKZ3ierWEx1lk/img.png&quot; alt=&quot;img_4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(TPS가 지나치게 낮다고 생각한다면 당연하다! Count 쿼리의 결과는 50만 건이다.)&lt;/p&gt;
&lt;p&gt;(참고) 인덱스는 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;category: &lt;code&gt;ix_region_code_category_id on category (region_code, category_id)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;article_auth: &lt;code&gt;ix_article_id on article_auth (article_id)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;article: &lt;code&gt;ix_region_code_category_id on article (region_code, category_id)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;LEFT OUTER JOIN과 필터링을 1개의 쿼리로 Split&lt;/strong&gt;하는 작업을 진행했고, &lt;strong&gt;TPS가 2.0에서 11.5로 개선&lt;/strong&gt;되었다.&lt;/p&gt;
&lt;p&gt;실제로 운영중인 서버에서는 &lt;strong&gt;TPS가 10.8에서 107.7로 개선&lt;/strong&gt;되었다. (레코드를 수백만 개 가지고 있는 테스트 계정이며, 레코드를 1만 개 정도 가진 계정의 경우 TPS가 780 가량 나온다.)&lt;/p&gt;
&lt;p&gt;조회 쿼리의 성능이 너무 안나올 때 불필요한 Join이 많지는 않은 지 한번 확인해보길 권장한다. &lt;strong&gt;복잡한 필터링도 많다면&lt;/strong&gt; 쿼리를 분리했을 때 &lt;strong&gt;성능이 높아질 가능성&lt;/strong&gt;이 충분히 있다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>Database/SQL</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/414</guid>
      <comments>https://jaehoney.tistory.com/414#entry414comment</comments>
      <pubDate>Tue, 27 Feb 2024 23:02:48 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux란 무엇인가?! - 3. Spring Webflux 이해하기!</title>
      <link>https://jaehoney.tistory.com/413</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;해당 포스팅은 Netty, Reactor를 넘어서 Spring Webflux에 대한 자세히 다룬다. &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cazB2k/btsEZSuAUsC/tTT017qRrQyfpXR1gUkLl0/img.png&quot; alt=&quot;i_1.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;spring-reactive-stack&quot;&gt;Spring Reactive Stack&lt;/h2&gt;
&lt;p&gt;1편에서 봤던 Spring Reactive Stack을 다시 살펴보고 넘어가자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAt7y0/btsEXvNX9de/W8eD3QKlBA8R1jttlb1ykk/img.png&quot; alt=&quot;img_1.png&quot; /&gt;&lt;/p&gt;
&lt;h4 id=&quot;webfluxautoconfiguration&quot;&gt;WebFluxAutoConfiguration&lt;/h4&gt;
&lt;p&gt;WebFluxAutoConfiguration은 SpringWebflux 사용을 위한 필수적인 AutoConfiguration이다.&lt;/p&gt;
&lt;p&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@AutoConfiguration(after = { ReactiveWebServerFactoryAutoConfiguration.class, CodecsAutoConfiguration.class,
        ReactiveMultipartAutoConfiguration.class, ValidationAutoConfiguration.class,
        WebSessionIdResolverAutoConfiguration.class })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass(WebFluxConfigurer.class)
@ConditionalOnMissingBean({ WebFluxConfigurationSupport.class })
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class WebFluxAutoConfiguration {
    // ..생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@AutoConfiguration&lt;/code&gt;의 after 프로퍼티에 ReactiveWebServerFactoryAutoConfiguration이 있다.&lt;/p&gt;
&lt;p&gt;해당 클래스를 들여다 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Import({ ReactiveWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
        ReactiveWebServerFactoryConfiguration.EmbeddedTomcat.class,
        ReactiveWebServerFactoryConfiguration.EmbeddedJetty.class,
        ReactiveWebServerFactoryConfiguration.EmbeddedUndertow.class,
        ReactiveWebServerFactoryConfiguration.EmbeddedNetty.class })
public class ReactiveWebServerFactoryAutoConfiguration&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;내부적으로 EmbeddedTomcat, EmbeddedJetty, EmbeddedUndertow, EmbeddedNetty를 Import한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E7cHh/btsEYPL0A37/1oZiFKmrNx3pZsSKvPq9S1/img.png&quot; alt=&quot;img_2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;살펴보면 &lt;code&gt;@ConditionalOnClass&lt;/code&gt;로 인해서 기본적으로 EmbeddedNetty만 빈으로 등록된다.&lt;br /&gt;
(spring-boot-starter-webflux 의존성은 spring-boot-starter-reactor-netty를 포함한다.)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcz7z4/btsEWyjXztK/Y0gWUUl5fxsNKLBcQIa7w0/img.png&quot; alt=&quot;img_3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;EmbeddedNetty가 등록되면 NettyReactiveWebServerFactory를 빈으로 등록하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;NettyReactiveWebServerFactory를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;import reactor.netty.http.HttpProtocol;
import reactor.netty.http.server.HttpServer;

public class NettyReactiveWebServerFactory
        extends AbstractReactiveWebServerFactory {
    @Override
    public WebServer getWebServer(HttpHandler httpHandler) {
        HttpServer httpServer = createHttpServer();
        ReactorHttpHandlerAdapter handlerAdapter = new ReactorHttpHandlerAdapter(httpHandler);
        NettyWebServer webServer =
                createNettyWebServer(httpServer, handlerAdapter, this.lifecycleTimeout, getShutdown());
        webServer.setRouteProviders(this.routeProviders);
        return webServer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HttpServer로 ReactorHttpHandlerAdapter를 만든다. import를 보면 HttpServer가 &lt;strong&gt;Reactor Netty&lt;/strong&gt;의 클래스임을 알 수 있다. 최종적으로는 NettyWebServer를 만들게 된다.&lt;/p&gt;
&lt;p&gt;앞서 Netty와 Project Reactor를 학습했었다. Reactor Netty는 무엇일까.&lt;/p&gt;
&lt;h2 id=&quot;reactor-netty&quot;&gt;Reactor Netty&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Reactor Netty&lt;/strong&gt;는 &lt;strong&gt;Reactor를 기반으로 Netty를 Wrapping한 라이브러리&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;Reactor Netty는 아래의 장점을 모두 제공한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Netty의 높은 성능&lt;/li&gt;
&lt;li&gt;Reactor의 조합성, 편의성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Consumer&amp;lt;HttpServerRoutes&amp;gt; routesConsumer = routes -&amp;gt;
        routes.get(&quot;/hello&quot;, (request, response) -&amp;gt; {
            var data = Mono.just(&quot;Hello World!&quot;);
            return response.sendString(data);
        });

HttpServer.create()
        .route(routesConsumer)
        .port(8080)
        .bindNow()
        .onDispose()
        .block();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reactor의 Mono나 Flux처럼 Reactive Streams API의 Publisher를 활용해서 요청을 처리할 수 있도록 구성되어 있다.&lt;/p&gt;
&lt;p&gt;Spring Webflux는 Netty와 Reactor를 활용한 Reactor Netty 기반 WebServer를 제공한다.&lt;/p&gt;
&lt;h2 id=&quot;reactiveadapterregistry&quot;&gt;ReactiveAdapterRegistry&lt;/h2&gt;
&lt;p&gt;Spring Webflux에서 Reactor Netty를 사용한다고 설명했다. 그러면 RxJava와 같은 다른 Reactive Streams API의 구현체는 사용하지 못하는걸까?&lt;/p&gt;
&lt;p&gt;Spring Webflux는 Reactor 뿐 아니라 &lt;strong&gt;RxJava, Mutiny, Coroutine도 모두 지원&lt;/strong&gt;한다.&lt;/p&gt;
&lt;p&gt;Spring Webflux에서는 ReactiveAdapterRegistry에 ReactiveAdapter를 등록해서 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI9239/btsEYC0sDzq/11DgYo9GQms0RWTeKGIJIk/img.png&quot; alt=&quot;img_4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;RxJava의 Flowable이 들어왔다고 가정해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;ReactiveAdapter adapter = getAdapterRegistry().getAdapter(null, attribute);
Mono.from(adapter.toPublisher(attribute));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 ReactiveAdapter에 의해 Flowable을 Publisher로 변환한 후 Mono로 변환한다.&lt;/p&gt;
&lt;p&gt;즉, Spring Webflux 환경에서 ReactiveStreams의 Publisher 혹은 Reactor의 Mono나 Flux 기준으로 개발만 한다면, ReactiveAdapter를 통해서 여러 라이브러리를 지원할 수 있게 된다.&lt;/p&gt;
&lt;h4 id=&quot;httphandler&quot;&gt;HttpHandler&lt;/h4&gt;
&lt;p&gt;아래는 &lt;code&gt;org.springframework.http.server.reactive&lt;/code&gt;의 HttpHandler 인터페이스이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public interface HttpHandler {
    Mono&amp;lt;Void&amp;gt; handle(ServerHttpRequest request, ServerHttpResponse response);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 인터페이스는 아래와 같이 구현한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ServerHttpRequest와 ServerHttpResponse를 전달받는다.&lt;/li&gt;
&lt;li&gt;Http 요청 처리가 끝나면 &lt;code&gt;Mono&amp;lt;Void&amp;gt;&lt;/code&gt;를 반환한다.&lt;ul&gt;
&lt;li&gt;ServerHttpResponse의 setComplete 혹은 writeWith가 Mono&lt;Void&gt;를 반환하므로 그대로 사용하는 경우가 많다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래 구현을 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;var httpHandler = new HttpHandler() {
    @Override
    public Mono&amp;lt;Void&amp;gt; handle(
            ServerHttpRequest request,
            ServerHttpResponse response) {
        String nameQuery = request.getQueryParams().getFirst(&quot;name&quot;);
        String name = nameQuery == null ? &quot;world&quot; : nameQuery;

        String content = &quot;Hello &quot; + name;
        log.info(&quot;responseBody: {}&quot;, content);
        Mono&amp;lt;DataBuffer&amp;gt; responseBody = Mono.just(
                response.bufferFactory()
                        .wrap(content.getBytes())
        );

        response.addCookie(ResponseCookie.from(&quot;name&quot;, name).build());
        response.getHeaders().add(&quot;Content-Type&quot;, &quot;text/plain&quot;);
        return response.writeWith(responseBody);
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 HttpHandler는 request의 값을 사용해서 응답을 만들어서 response에 write한다.&lt;/p&gt;
&lt;p&gt;구현한 HttpHandler는 Reactor Netty의 ReactorHttpHandlerAdapter로 Wrapping해서 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;var adapter = new ReactorHttpHandlerAdapter(httpHandler);
HttpServer.create()
        .host(&quot;localhost&quot;)
        .port(8080)
        .handle(adapter)
        .bindNow()
        .channel().closeFuture().sync();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 사용한 HttpServer도 Reactor Netty의 컴포넌트이다. HttpServer는 별도의 channel이나 eventLoopGroup을 명시하지 않아도 알아서 관리해준다.&lt;/p&gt;
&lt;h2 id=&quot;thread&quot;&gt;Thread&lt;/h2&gt;
&lt;p&gt;Spring Webflux는 내부적으로 Reactor Netty에서 제공하는 서버를 사용한다.&lt;/p&gt;
&lt;p&gt;즉, 기존 Spring MVC에서는 Thread per request 모델이었지만, Spring Webflux에서는 Reactor Netty의 EventLoop 기반으로 동작한다.&lt;/p&gt;
&lt;p&gt;참고로 SpringBoot에서 사용하는 Tomcat의 max thread-pool size는 default가 200이다. Spring WebFlux에서 Worker thread default size는 core 개수로 설정되어 있다.&lt;/p&gt;
&lt;p&gt;즉, 서버의 core 수가 4개라면 worker thread 4개로 트래픽을 감당하게 된다.&lt;/p&gt;
&lt;h2 id=&quot;httpwebhandleradapter&quot;&gt;HttpWebHandlerAdapter&lt;/h2&gt;
&lt;p&gt;WebHandler는 spring-web에서 다양한 기능을 제공하기 위한 컴포넌트이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public interface WebHandler {
    Mono&amp;lt;Void&amp;gt; handle(ServerWebExchange exchange);
}

public interface WebFilter {
    Mono&amp;lt;Void&amp;gt; filter(ServerWebExchange exchange, WebFilterChain chain);
}

public interface WebExceptionHandler {
    Mono&amp;lt;Void&amp;gt; filter(ServerWebExchange exchange, Throwable ex);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;공통적으로 사용하는 ServerWebExchange에 대해 알아보자.&lt;/p&gt;
&lt;h4 id=&quot;serverwebexchange&quot;&gt;ServerWebExchange&lt;/h4&gt;
&lt;p&gt;ServerWebExchange는 아래와 같이 요청과 응답 등을 꺼내 사용할 수 있는 메서드를 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public interface ServerWebExchange {
    ServerHttpRequest getRequest();
    ServerHttpResponse getResponse();
    Map&amp;lt;String, Object&amp;gt; getAttributes();
    Mono&amp;lt;WebSession&amp;gt; getSession();
    &amp;lt;T extends Principal&amp;gt; Mono&amp;lt;T&amp;gt; getPrincipal();
    Mono&amp;lt;MultiValueMap&amp;lt;String, String&amp;gt;&amp;gt; getFormData();
    Mono&amp;lt;MultiValueMap&amp;lt;String, Part&amp;gt;&amp;gt; getMultipartData();
    ApplicationContext getApplicationContext();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이를 활용하면 아래와 같이 WebHandler를 생성할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;var webHandler = new WebHandler() {
    @Override
    public Mono&amp;lt;Void&amp;gt; handle(ServerWebExchange exchange) {
        final ServerHttpRequest request = exchange.getRequest();
        final ServerHttpResponse response = exchange.getResponse();

        String nameQuery = request.getQueryParams().getFirst(&quot;name&quot;);
        String name = nameQuery == null ? &quot;world&quot; : nameQuery;

        String content = &quot;Hello &quot; + name;
        log.info(&quot;responseBody: {}&quot;, content);
        Mono&amp;lt;DataBuffer&amp;gt; responseBody = Mono.just(
                response.bufferFactory()
                        .wrap(content.getBytes())
        );

        response.addCookie(
                ResponseCookie.from(&quot;name&quot;, name).build());
        response.getHeaders()
                .add(&quot;Content-Type&quot;, &quot;text/plain&quot;);
        return response.writeWith(responseBody);
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WebHandler는 WebHttpHandlerBuilder를 사용해서 filters, exceptionHandlers, sessionManager 등을 조합한 후 HttpWebHandlerAdapter로 만들 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public HttpHandler build() {
    WebHandler decorated = new FilteringWebHandler(this.webHandler, this.filters);
    decorated = new ExceptionHandlingWebHandler(decorated,  this.exceptionHandlers);

    HttpWebHandlerAdapter adapted = new HttpWebHandlerAdapter(decorated);
    if (this.sessionManager != null) {
        adapted.setSessionManager(this.sessionManager);
    }
    if (this.codecConfigurer != null) {
        adapted.setCodecConfigurer(this.codecConfigurer);
    }
    if (this.localeContextResolver != null) {
        adapted.setLocaleContextResolver(this.localeContextResolver);
    }
    if (this.forwardedHeaderTransformer != null) {
        adapted.setForwardedHeaderTransformer(this.forwardedHeaderTransformer);
    }
    if (this.applicationContext != null) {
        adapted.setApplicationContext(this.applicationContext);
    }
    adapted.afterPropertiesSet();

    return (this.httpHandlerDecorator != null ? this.httpHandlerDecorator.apply(adapted) : adapted);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;HttpWebHandlerAdapter&lt;/strong&gt;는 위에서 봤던 &lt;strong&gt;HttpHandler를 구현&lt;/strong&gt;한다.&lt;/p&gt;
&lt;p&gt;Spring Webflux에서는 WebHandler, WebFilter, WebExceptionHandler를 만든 이후 HandlerAdapter를 만들어서 ReactorNetty 기반의 서버를 구성해준다고 보면 될 것 같다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;strong&gt;각 컴포넌트들이 통신하는 데이터가 ServerWebExchange&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;다음으로 DispatcherHandler의 역할을 알아보자.&lt;/p&gt;
&lt;h2 id=&quot;dispatcherhandler&quot;&gt;DispatcherHandler&lt;/h2&gt;
&lt;p&gt;Spring Webflux에서는 DispatcherServlet과 유사하게 DispatcherHandler를 사용한다.&lt;/p&gt;
&lt;p&gt;아래는 DispatcherHandler의 일부이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class DispatcherHandler 
        implements WebHandler, PreFlightRequestHandler, ApplicationContextAware {

    @Nullable
    private List&amp;lt;HandlerMapping&amp;gt; handlerMappings;

    @Nullable
    private List&amp;lt;HandlerAdapter&amp;gt; handlerAdapters;

    @Nullable
    private List&amp;lt;HandlerResultHandler&amp;gt; resultHandlers;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 DispatcherHandler가 요청을 처리하는 방식은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJL9Fk/btsE0gvjw5w/vOEFcd5d0rjuzfOMGJ2NZ1/img.png&quot; alt=&quot;img_5.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;각 컴포넌트는 아래 역할을 수행한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HandlerMapping: ServerWebExchange를 입력받은 후 요청을 처리할 Handler를 Mono로 반환한다.&lt;ul&gt;
&lt;li&gt;반환하는 handler: HandlerMethod, HandlerFunction, WebHandler, …&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;HandlerAdapter:&lt;ul&gt;
&lt;li&gt;support: HandlerMapping에서 전달받은 Handler를 지원하는 지 여부를 확인한다.&lt;/li&gt;
&lt;li&gt;handle: 실제 요청을 처리하고 HandlerResult를 Mono로 반환한다.&lt;/li&gt;
&lt;li&gt;ex. RequestMappingHandlerAdapter, SimpleHandlerAdapter, …&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;HandlerResultHandler:&lt;ul&gt;
&lt;li&gt;support: HandlerAdapter를 통해 받은 HandlerResult를 지원하는 지 여부를 확인한다.&lt;/li&gt;
&lt;li&gt;handleResult: ServerWebExchange와 result를 받아서 응답을 Write하고 &lt;code&gt;Mono&amp;lt;Void&amp;gt;&lt;/code&gt;를 반환한다.&lt;/li&gt;
&lt;li&gt;ex. ResponseEntityResultHandler, ResponseBodyResultHandler, …&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래 컨트롤러를 등록했다고 가정하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Controller
@RequestMapping(path = &quot;/hello&quot;)
public class HelloController {
    @ResponseBody
    @GetMapping(params = &quot;name&quot;)
    Mono&amp;lt;String&amp;gt; helloQueryParam(@RequestParam String name) {
        String content = &quot;Hello &quot; + name;
        return Mono.just(content);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ReactorNetty는 DispatcherHandler에게 요청을 보낸다. DispatcherHandler의 동작은 요약하면 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DispatcherHandler가 HandlerMapping를 찾는다.&lt;ul&gt;
&lt;li&gt;RequestMappingHandlerMapping에 의해 &lt;code&gt;helloQueryParam()&lt;/code&gt;을 호출하는 HandlerMethod가 반환된다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;DispatcherHandler가 HandlerAdapter를 찾는다.&lt;ul&gt;
&lt;li&gt;HandlerMapping에게 전달받은 것이 HandlerMethod이므로 RequestMappingHandlerAdapter가 반환된다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;RequestMappingHandlerAdapter의 &lt;code&gt;handle()&lt;/code&gt;을 실행해서 실제 핸들러(컨트롤러) 메서드를 수행한다.&lt;/li&gt;
&lt;li&gt;결과를 처리할 수 있는 HandlerResultHandler를 찾는다.&lt;ul&gt;
&lt;li&gt;ResponseBodyResultHandler를 반환한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;ResponseBodyResultHandler가 응답을 write하고 결과를 ReactorNetty에 넘겨준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Spring WebFlux에서 DispatcherHandler를 사용해서 요청을 처리하는 방법을 알아봤다. &lt;/p&gt;
&lt;h4 id=&quot;reactive-stack&quot;&gt;Reactive Stack&lt;/h4&gt;
&lt;p&gt;ServletStack에서 사용하는 객체와 Reactive Stack에서 사용하는 객체는 차이가 꽤 존재한다.&lt;/p&gt;
&lt;p&gt;아래는 Reactive Stack에서 사용하는 객체이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Method Argument&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ServerWebExchange:&lt;ul&gt;
&lt;li&gt;HttpServletRequest, HttpServletResponse 대신 사용한다.&lt;/li&gt;
&lt;li&gt;ServerHttpRequest, ServerHttpResponse만 사용할 수도 있다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;WebSession&lt;ul&gt;
&lt;li&gt;HttpSession 대신 WebSession을 지원한다.&lt;/li&gt;
&lt;li&gt;HttpSession과 다르게 새로운 Session 생성을 강제하지 않으므로 null이 될 수 있다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. Return&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rendering&lt;ul&gt;
&lt;li&gt;ModelAndView 대신 Rendering을 지원한다.&lt;/li&gt;
&lt;li&gt;view, model, status, header, redirect 등의 정보를 포함한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Reactive Stack에서는 HttpMessageConverter 대신 HttpMessageWriter를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;요청을 처리하는 부분에 대해서는 어느정도 이해가 되었을 것이다.&lt;/p&gt;
&lt;h2 id=&quot;spring-security-reactive&quot;&gt;Spring Security Reactive&lt;/h2&gt;
&lt;p&gt;Spring WebFlux를 설명하는 데 Spring Security에 대해 설명하는 이유는 중요한 근간이 있기 때문이다.&lt;/p&gt;
&lt;p&gt;SpringMVC에서 Spring Security의 구조에 대해 간단하게 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nwd7I/btsEXtQegzM/6ksKC78xdokpvmEgcDH0d0/img.png&quot; alt=&quot;img_6.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;아래는 각 동작에 대한 설명이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Servlet Stack에서는 Servlet filter를 사용&lt;/li&gt;
&lt;li&gt;Servlet filter에 DelegatingFilterProxy를 추가&lt;ul&gt;
&lt;li&gt;DelegatingFilterProxy는 SecurityFilterChain을 호출&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;SecurityFilterChain를 사용해서 인증 인가를 수행&lt;ul&gt;
&lt;li&gt;각 Filter와 컨트롤러에서는 SecurityContextHolder를 사용&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;문제는 SecurityContextHolder는 내부적으로 ThreadLocal을 사용한다는 점이다. &lt;strong&gt;Spring MVC&lt;/strong&gt;에서는 &lt;strong&gt;Thread per request 모델&lt;/strong&gt;이어서 문제가 없었지만, &lt;strong&gt;Spring Webflux에서는 1개 Thread가 여러 개의 요청을 처리&lt;/strong&gt;함으로 문제가 생긴다.&lt;/p&gt;
&lt;h4 id=&quot;securitywebfilterchain&quot;&gt;SecurityWebFilterChain&lt;/h4&gt;
&lt;p&gt;Spring Security Reactive에서는 &lt;strong&gt;SecurityWebFilterChain&lt;/strong&gt;을 사용한다.&lt;/p&gt;
&lt;p&gt;SecurityWebFilterChain에서는 SecurityContextHolder가 아닌 &lt;strong&gt;ReactiveSecurityContextHolder&lt;/strong&gt;를 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public final class ReactiveSecurityContextHolder {

    public static Mono&amp;lt;SecurityContext&amp;gt; getContext() {
        return Mono.subscriberContext()
                .filter(ReactiveSecurityContextHolder::hasSecurityContext)
                .flatMap(ReactiveSecurityContextHolder::getSecurityContext);
    }

    public static Function&amp;lt;Context, Context&amp;gt; clearContext() {
        return (context) -&amp;gt; context.delete(SECURITY_CONTEXT_KEY);
    }

    public static Context withSecurityContext(Mono&amp;lt;? extends SecurityContext&amp;gt; securityContext) {
        return Context.of(SECURITY_CONTEXT_KEY, securityContext);
    }

    public static Context withAuthentication(Authentication authentication) {
        return withSecurityContext(Mono.just(new SecurityContextImpl(authentication)));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 클래스는 ThreadLocal 대신 &lt;strong&gt;SecurityContext&lt;/strong&gt;라는 것을 사용한다.&lt;/p&gt;
&lt;p&gt;각 메서드의 역할은 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;getContext: SecurityContext를 Mono로 제공&lt;/li&gt;
&lt;li&gt;clearContext: SecurityContext를 clear&lt;/li&gt;
&lt;li&gt;withSecurityContext: SecurityContext를 Mono로 받아서 이를 포함하는 Context 반환&lt;/li&gt;
&lt;li&gt;withAuthentication: Authentication을 받아서 SecurityContext를 생성 후 Context 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;해당 클래스에서 사용하는 &lt;strong&gt;Context&lt;/strong&gt;는 이전 2편 Reactor 부분에서 봤던 &lt;code&gt;reactor.util.context&lt;/code&gt;의 Context이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://jaehoney.tistory.com/412&quot;&gt;https://jaehoney.tistory.com/412&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 다른 요청의 Context와 독립적으로 관리될 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/&quot;&gt;https://docs.spring.io/spring-framework/reference/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fastcampus.co.kr/courses/216172&quot;&gt;https://fastcampus.co.kr/courses/216172&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Server/Spring Reactive</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/413</guid>
      <comments>https://jaehoney.tistory.com/413#entry413comment</comments>
      <pubDate>Sat, 17 Feb 2024 22:17:37 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux란 무엇인가?! - 2. Reactor 이해하기!</title>
      <link>https://jaehoney.tistory.com/412</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;이번 포스팅에서는 Project Reactor의 사용 방법에 대해 다룬다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpn2j6/btsEtmdiYqb/XJikluWDhVPLdubdC12jeK/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;project-reactor&quot;&gt;Project Reactor&lt;/h2&gt;
&lt;p&gt;아래 포스팅에서 ReactiveProgramming에 대해 설명했고, Reactive Stream과 Project Reactor에 대해서도 간단하게 설명했었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://jaehoney.tistory.com/359&quot;&gt;https://jaehoney.tistory.com/359&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;개념 자체가 생소하다면 해당 포스팅(이전 편)을 참고하시길 추천한다.&lt;/p&gt;
&lt;p&gt;Reactive Stream을 이해하고 있다면 굳이 보지 않아도 괜찮다.&lt;/p&gt;
&lt;p&gt;첫 번째로 살펴볼 것은 Subscribe이다.&lt;/p&gt;
&lt;h2 id=&quot;subscribe&quot;&gt;Subscribe&lt;/h2&gt;
&lt;p&gt;Mono와 Flux가 구현하는 CorePublisher 인터페이스이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public interface CorePublisher&amp;lt;T&amp;gt; extends Publisher&amp;lt;T&amp;gt; { 
    void subscribe(CoreSubscriber&amp;lt;? super T&amp;gt; subscriber);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Publisher가 item을 전달하면 그 아이템을 받아서 처리하는 것을 subscribe라고 한다.&lt;/p&gt;
&lt;p&gt;중요한 점은 item이 있어도 subscribe되지 않으면 아무 일도 일어나지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.fromIterable(List.of(1, 2, 3, 4, 5))
        .doOnNext(value -&amp;gt; {
            log.info(&quot;value: &quot; + value);
        })
        // .subscribe()를 하지 않으면 아무 일도 일어나지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래는 Mono와 Flux가 사용하는 subscribe의 명세이다. 오버로딩된 3개 메서드가 있음을 볼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public Disposable subscribe()

public final Disposable subscribe
    @Nullable Consumer&amp;lt;? super T&amp;gt; consumer,
    @Nullable Consumer&amp;lt;? super Throwable&amp;gt; errorConsumer,
    @Nullable Runnable completeConsumer,
    @Nullable Context initialContext)

private final void subscribe(Subscriber&amp;lt;? super T&amp;gt; actual)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래는 오버로딩된 3개 메서드에 대한 설명이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consumer를 넘기지 않는 subscribe&lt;ul&gt;
&lt;li&gt;별도로 Consume을 하지 않고 최대한으로 요청&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;함수형 인터페이스 기반의 subscribe&lt;ul&gt;
&lt;li&gt;Disposable을 반환하고 반환된 객체를 통해 언제든지 연결 종료할 수 있다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Subscriber 기반의 subscribe&lt;ul&gt;
&lt;li&gt;Subscriber는 subscription을 받기 때문에 request와 cancel으로 backpressure를 조절할 수 있다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;1-consumer를-넘기지-않는-subscribe&quot;&gt;1. Consumer를 넘기지 않는 subscribe&lt;/h4&gt;
&lt;p&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.fromIterable(List.of(1, 2, 3, 4, 5))
        .doOnNext(value -&amp;gt; {
            log.info(&quot;value: &quot; + value);
        })
        .subscribe()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 코드는 별도의 Consumer를 넘기지 않는 subscribe이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;결과를 이용하기 보다는 아이템을 만드는 것이 중요한 경우 사용한다.&lt;/li&gt;
&lt;li&gt;결과를 확인하기 위해 &lt;code&gt;doOnNext()&lt;/code&gt;를 활용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-함수형-인터페이스-기반의-subscribe&quot;&gt;2. 함수형 인터페이스 기반의 subscribe&lt;/h4&gt;
&lt;p&gt;아래는 함수형 인터페이스 기반의 subscribe이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.fromIterable(List.of(1, 2, 3, 4, 5))
        .subscribe(new Consumer&amp;lt;Integer&amp;gt;() {
            @Override
            public void accept(Integer integer) {
                log.info(&quot;value: &quot; + integer);
            }
        }, new Consumer&amp;lt;Throwable&amp;gt;() {
            @Override
            public void accept(Throwable throwable) {
                log.error(&quot;error: &quot; + throwable);
            }
        }, new Runnable() {
            @Override
            public void run() {
                log.info(&quot;complete&quot;);
            }
        }, Context.empty());&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;subscribe에는 총 4가지 인자를 넘길 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;consumer: 값은 하나씩 인자로 받아서 처리&lt;/li&gt;
&lt;li&gt;errorConsumer: 에러가 발생했을 때 인자로 받아서 처리&lt;/li&gt;
&lt;li&gt;completeConsumer: 완료 후에 인자 없이 Runnable 실행&lt;/li&gt;
&lt;li&gt;initialContext: upstream에 전달할 context&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;당연히 람다식이나 메소드 레퍼런스도 사용할 수 있다. 이를 사용하면 Subscribe 결과에 따른 처리를 하는 등의 처리를 할 수 있다.&lt;/p&gt;
&lt;h4 id=&quot;3-subscriber-기반의-subscribe&quot;&gt;3. Subscriber 기반의 subscribe&lt;/h4&gt;
&lt;p&gt;두 방법은 모두 backpressure를 사용할 수 없다. 이 경우 Subscriber 기반의 subscribe를 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.fromIterable(List.of(1, 2, 3, 4, 5))
        .subscribe(new Subscriber&amp;lt;&amp;gt;() {
            @Override
            public void onSubscribe(Subscription s) {
                s.request(Long.MAX_VALUE);
            }

            @Override
            public void onNext(Integer integer) {
                log.info(&quot;value: &quot; + integer);
            }

            @Override
            public void onError(Throwable t) {
                log.error(&quot;error: &quot; + t);
            }

            @Override
            public void onComplete() {
                log.info(&quot;complete&quot;);
            }
    });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Subscriber 기반의 subscribe의 경우 &lt;code&gt;onSubscribe()&lt;/code&gt;로 backpressure를 조절할 수 있다.&lt;/p&gt;
&lt;p&gt;외부에서 Subscriber를 전달하는 경우 &lt;code&gt;request()&lt;/code&gt;를 직접 호출하거나 &lt;code&gt;cancel()&lt;/code&gt;을 처리하는 등의 제어도 가능하다.&lt;/p&gt;
&lt;h2 id=&quot;backpressure&quot;&gt;Backpressure&lt;/h2&gt;
&lt;p&gt;다음은 배압(Backpressure)을 조절하는 방법에 대해 알아보자.&lt;/p&gt;
&lt;h4 id=&quot;unbounded-request&quot;&gt;Unbounded Request&lt;/h4&gt;
&lt;p&gt;Unbounded Request란 &lt;code&gt;request(Long.MAX_VALUE)&lt;/code&gt;처럼 backpressure를 비활성화하고 가능한 빠르게 아이템을 전달해달라는 요청이다.&lt;/p&gt;
&lt;p&gt;Unbounded Request는 아래 상황에 발생한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;아무 것도 넘기지 않는 subscribe()&lt;/li&gt;
&lt;li&gt;람다 기반의 subscribe()&lt;/li&gt;
&lt;li&gt;block(), blockFirst(), blockLast() 등의 blocking 연산&lt;/li&gt;
&lt;li&gt;toIterable(), toStream() 등의 toCollect 연산자&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 별도의 설정이 없다면 기본적으로 모든 아이템을 빠르게 전달한다.&lt;/p&gt;
&lt;h4 id=&quot;buffer&quot;&gt;buffer&lt;/h4&gt;
&lt;p&gt;backpressure 조절과 함께 활용할 수 있는 게 buffer 연산이다.&lt;/p&gt;
&lt;p&gt;buffer(N)을 호출 시 N개 만큼 item을 모아서 List로 전달한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;var subscriber = new BaseSubscriber&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt;() {
    private Integer count = 0;

    @Override
    protected void hookOnSubscribe(Subscription subscription) {
        request(2);
    }

    @Override
    protected void hookOnNext(List&amp;lt;Integer&amp;gt; value) {
        if (++count == 2) cancel();
    }

    @Override
    protected void hookOnComplete() {
        log.info(&quot;complete&quot;);
    }
};

Flux.fromStream(IntStream.range(0, 10).boxed())
        .buffer(3)
        .subscribe(subscriber);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 코드에서는 buffer(3)을 호출 후 request(2)를 하기 때문에 3개가 담긴 List 2개가 Subscriber에게 전달된다.&lt;/p&gt;
&lt;h4 id=&quot;take&quot;&gt;take&lt;/h4&gt;
&lt;p&gt;take 연산은 subscriber 외부에서 최대 개수를 제한할 수 있다.&lt;/p&gt;
&lt;p&gt;take(n)는 정확히 n개만큼 요청 후 complete 이벤트를 전달한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;var subscriber = new BaseSubscriber&amp;lt;Integer&amp;gt;() {
    @Override
    protected void hookOnNext(Integer value) {
        log.info(&quot;value: &quot; + value);
    }

    @Override
    protected void hookOnComplete() {
        log.info(&quot;complete&quot;);
    }
};

Flux.fromStream(IntStream.range(0, 10).boxed())
        .take(5)
        .subscribe(subscriber);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드의 경우 5개의 값만 전달되고 완료로 처리된다.&lt;/p&gt;
&lt;p&gt;다음은 중요한 개념인 Sequence에 대해 알아보자.&lt;/p&gt;
&lt;h2 id=&quot;sequence&quot;&gt;Sequence&lt;/h2&gt;
&lt;p&gt;Sequence란 Reactor에서 통지할 데이터를 정의한 것을 말한다. 데이터의 흐름 Stream과 유사하다고 생각하면 된다.&lt;/p&gt;
&lt;p&gt;공식 문서를 보면 0/1/N 시퀀스에 대해 설명하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzeXQT/btsEy85aSZR/3il1dCO6AA236D7JbFbGLK/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;친절하게 설명에 나와있듯 Flux는 N개의 아이템을 담을 수 있고, Mono는 0개나 1개의 아이템을 담는 데 유용하다.&lt;/p&gt;
&lt;p&gt;시퀀스를 다루는 함수에 대해 알아보자.&lt;/p&gt;
&lt;h4 id=&quot;just&quot;&gt;just&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Mono.just()&lt;/code&gt;, &lt;code&gt;Flux.just()&lt;/code&gt;를 사용하면 주어진 객체를 subscriber에게 전달할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Mono.just(1)
        .subscribe(value -&amp;gt; {
            log.info(&quot;value: &quot; + value);
        });

Flux.just(1, 2, 3, 4, 5)
        .subscribe(value -&amp;gt; {
            log.info(&quot;value: &quot; + value);
        });&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;error&quot;&gt;error&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Mono.error()&lt;/code&gt;, &lt;code&gt;Flux.error()&lt;/code&gt;를 통해 subscriber에게 onError 이벤트만 전달한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Mono.error(new RuntimeException(&quot;mono error&quot;))
         .subscribe(value -&amp;gt; {
             log.info(&quot;value: &quot; + value);
         }, error -&amp;gt; {
             log.error(&quot;error: &quot; + error);
         });

Flux.error(new RuntimeException(&quot;flux error&quot;))
         .subscribe(value -&amp;gt; {
             log.info(&quot;value: &quot; + value);
         }, error -&amp;gt; {
             log.error(&quot;error: &quot; + error);
         });&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;empty&quot;&gt;empty&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Mono.empty()&lt;/code&gt;, &lt;code&gt;Flux.empty()&lt;/code&gt;를 통해 시퀀스를 생성할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Mono.empty()
        .subscribe(value -&amp;gt; {
            log.info(&quot;value: &quot; + value);
        }, null, () -&amp;gt; {
            log.info(&quot;complete&quot;);
        });

Flux.empty()
        .subscribe(value -&amp;gt; {
            log.info(&quot;value: &quot; + value);
        }, null, () -&amp;gt; {
            log.info(&quot;complete&quot;);
        });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 경우에는 subscriber에게 onComplete 이벤트만 전달한다.&lt;/p&gt;
&lt;p&gt;가운데 null은 에러 컨슈머를 null로 사용한 것이다. Mono.empty()를 사용하므로 실패 Consumer는 필요가 없다.&lt;/p&gt;
&lt;h4 id=&quot;fromxx&quot;&gt;fromXX&lt;/h4&gt;
&lt;p&gt;실무를 하다보면 더 복잡한 경우도 생길 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들면 Callable, Runnable 등을 실행한 결과를 Mono, Flux한테 넘기는 경우 등에서 fromXX를 사용할 수 있다.&lt;/p&gt;
&lt;h4 id=&quot;mono&quot;&gt;Mono&lt;/h4&gt;
&lt;p&gt;Mono의 경우 아래 함수를 지원한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fromCallable: Callable 함수형 인터페이스를 실행하고 반환 값을 onNext로 전달&lt;/li&gt;
&lt;li&gt;fromFuture: Future를 받아서 done 상태가 되면 반환 값을 onNext로 전달&lt;/li&gt;
&lt;li&gt;fromSupplier: Supplier 함수형 인터페이스를 실행하고 반환 값을 onNext로 전달&lt;/li&gt;
&lt;li&gt;fromRunnable: Runnable 함수형 인터페이스를 실행하고 onComplete 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Mono.fromCallable(() -&amp;gt; {
    return 1;
}).subscribe(value -&amp;gt; {
    log.info(&quot;value fromCallable: &quot; + value);
});

Mono.fromFuture(CompletableFuture.supplyAsync(() -&amp;gt; {
    return 1;
})).subscribe(value -&amp;gt; {
    log.info(&quot;value fromFuture: &quot; + value);
});

Mono.fromSupplier(() -&amp;gt; {
    return 1;
}).subscribe(value -&amp;gt; {
    log.info(&quot;value fromSupplier: &quot; + value);
});

Mono.fromRunnable(() -&amp;gt; {
    /* do nothing */
}).subscribe(null, null, () -&amp;gt; {
    log.info(&quot;complete fromRunnable&quot;);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이를 사용하면 함수형 인터페이스나 Future 등의 결과를 시퀀스로 보낼 수 있다.&lt;/p&gt;
&lt;h4 id=&quot;flux&quot;&gt;Flux&lt;/h4&gt;
&lt;p&gt;Flux의 from은 여러 개의 값을 받아서 onNext로 전달한다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fromIterable: Iterable를 받아서 각각의 item을 onNext로 전달&lt;/li&gt;
&lt;li&gt;fromStream: Stream을 받아서 각각의 item을 onNext로 전달&lt;/li&gt;
&lt;li&gt;fromArray: Array를 받아서 각각의 item을 onNext로 전달&lt;/li&gt;
&lt;li&gt;range(start, n): start부터 시작해서 1개씩 커진 값을 n개만큼 onNext로 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.fromIterable(
        List.of(1, 2, 3, 4, 5)
).subscribe(value -&amp;gt; {
    log.info(&quot;value: &quot; + value);
});

Flux.fromStream(
        IntStream.range(1, 6).boxed()
).subscribe(value -&amp;gt; {
    log.info(&quot;value: &quot; + value);
});

Flux.fromArray(
        new Integer[]{1, 2, 3, 4, 5}
).subscribe(value -&amp;gt; {
    log.info(&quot;value: &quot; + value);
});

Flux.range(
        1, 5
).subscribe(value -&amp;gt; {
    log.info(&quot;value: &quot; + value);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이를 활용하면 Flux를 사용한 시퀀스를 쉽게 생성할 수 있다.&lt;/p&gt;
&lt;h4 id=&quot;generate&quot;&gt;generate&lt;/h4&gt;
&lt;p&gt;Flux.fromXX를 사용하면 간단하게 sequence를 만들 수 있었다.&lt;/p&gt;
&lt;p&gt;그러나 복잡한 경우도 생길 수 있다. 조건문이 들어간다거나, 콜백을 실행한 후 값을 sequence에 넣어줘야 한다거나 하는 경우에서는 generate를 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static &amp;lt;T, S&amp;gt; Flux&amp;lt;T&amp;gt; generate(
        Callable&amp;lt;S&amp;gt; stateSupplier,
        BiFunction&amp;lt;S, SynchronousSink&amp;lt;T&amp;gt;, S&amp;gt; generator)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;generate는 아래의 작업을 수행한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;동기적으로 Flux를 생성&lt;/li&gt;
&lt;li&gt;stateSupplier: 초기 값을 제공하는 Callable&lt;/li&gt;
&lt;li&gt;generator:&lt;ul&gt;
&lt;li&gt;첫 번째 인자로 state를 제공, 변경된 state를 반환&lt;/li&gt;
&lt;li&gt;두 번째 인자로 SynchronousSink를 제공. 명시적으로 next, error, Complete 호출 가능&lt;/li&gt;
&lt;li&gt;한 번의 generator에서 최대 한 번만 next 호출 가능&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;잘 이해가 되지 않는다. 아래 코드를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.generate(
        () -&amp;gt; 0,
        (state, sink) -&amp;gt; {
            sink.next(state);
            if (state == 9) {
                sink.complete();
            }
            return state + 1;
        }
).subscribe(value -&amp;gt; {
    log.info(&quot;value: &quot; + value);
}, error -&amp;gt; {
    log.error(&quot;error: &quot; + error);
}, () -&amp;gt; {
    log.info(&quot;complete&quot;);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 코드의 동작은 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;초기 값(state)를 0으로 세팅했다.&lt;/li&gt;
&lt;li&gt;generator에서 현재 state를 next로 반환한다.&lt;/li&gt;
&lt;li&gt;state가 9라면 complete 이벤트를 전달한다.&lt;/li&gt;
&lt;li&gt;state + 1을 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;create&quot;&gt;create&lt;/h4&gt;
&lt;p&gt;한 번의 generate에서 next를 두 번이상 호출하면 에러가 발생한다.&lt;/p&gt;
&lt;p&gt;만약 next를 많이 호출해야 하거나 더 복잡한 케이스를 커버하려면 create를 활용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static &amp;lt;T&amp;gt; Flux&amp;lt;T&amp;gt; create(
        Consumer&amp;lt;? super FluxSink&amp;lt;T&amp;gt;&amp;gt; emitter)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;create는 아래 작업을 수행한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;비동기적으로 Flux를 생성&lt;/li&gt;
&lt;li&gt;FluxSink를 노출&lt;ul&gt;
&lt;li&gt;명시적으로 next, error, complete 호출 가능&lt;/li&gt;
&lt;li&gt;emitter 1번에서 next를 여러 번 호출 가능&lt;/li&gt;
&lt;li&gt;여러 thread에서 동시에 호출 가능&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.create(sink -&amp;gt; {
    var task1 = CompletableFuture.runAsync(() -&amp;gt; {
        for (int i = 0; i &amp;lt; 5; i++) {
            sink.next(i);
        }
    });

    var task2 = CompletableFuture.runAsync(() -&amp;gt; {
        for (int i = 5; i &amp;lt; 10; i++) {
            sink.next(i);
        }
    });

    CompletableFuture.allOf(task1, task2)
            .thenRun(sink::complete);
}).subscribe(value -&amp;gt; {
    log.info(&quot;value: &quot; + value);
}, error -&amp;gt; {
    log.error(&quot;error: &quot; + error);
}, () -&amp;gt; {
    log.info(&quot;complete&quot;);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래 코드는 아래 역할을 수행한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2개의 쓰레드에서 sink.next를 수행&lt;/li&gt;
&lt;li&gt;CompletableFuture의 allOf()를 사용하여 두 개의 작업이 끝난 후 complete 이벤트 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결과적으로 0~4와 5~9가 전달되고 각각 순서까지만 보장하고, 0~4와 5~9 사이에서는 순서가 보장되지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;25:38 [ForkJoinPool.commonPool-worker-19] - value: 0
25:38 [ForkJoinPool.commonPool-worker-19] - value: 5
25:38 [ForkJoinPool.commonPool-worker-19] - value: 6
25:38 [ForkJoinPool.commonPool-worker-19] - value: 7
25:38 [ForkJoinPool.commonPool-worker-19] - value: 8
25:38 [ForkJoinPool.commonPool-worker-19] - value: 9
25:38 [ForkJoinPool.commonPool-worker-19] - value: 1
25:38 [ForkJoinPool.commonPool-worker-19] - value: 2
25:38 [ForkJoinPool.commonPool-worker-19] - value: 3
25:38 [ForkJoinPool.commonPool-worker-19] - value: 4
25:38 [ForkJoinPool.commonPool-worker-19] - complete&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;handle&quot;&gt;handle&lt;/h4&gt;
&lt;p&gt;generate(), create()까지 사용하면 대부분의 경우 sequence를 만들 수 있다. &lt;/p&gt;
&lt;p&gt;여기서 추가로 값을 Intercept해서 특정 값을 필터링하거나 가공하는 등의 처리를 할 때 handle을 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public final &amp;lt;R&amp;gt; Flux&amp;lt;R&amp;gt; handle(
        BiConsumer&amp;lt;? super T, SynchronousSink&amp;lt;R&amp;gt;&amp;gt; handler)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;handle은 아래 동작을 수행한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;일종의 interceptor로 source의 item을 필터하거나 변경할 수 있다.&lt;/li&gt;
&lt;li&gt;독립적으로 sequence를 생성할 수 없고 존재하는 source에 연결한다.&lt;/li&gt;
&lt;li&gt;handler&lt;ul&gt;
&lt;li&gt;첫 번째 인자로 source의 item을 제공&lt;/li&gt;
&lt;li&gt;두 번째 인자로 SynchronousSink를 제공&lt;/li&gt;
&lt;li&gt;sink의 next를 이용해서 현재 주어진 item을 전달할 지 여부를 결정&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.fromStream(IntStream.range(0, 10).boxed())
        .handle((value, sink) -&amp;gt; {
            if (value % 2 == 0) {
                sink.next(value);
            }
        }).subscribe(value -&amp;gt; {
            log.info(&quot;value: &quot; + value);
        }, error -&amp;gt; {
            log.error(&quot;error: &quot; + error);
        }, () -&amp;gt; {
            log.info(&quot;complete&quot;);
        });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 코드에서는 짝수인 경우에만 sink의 next를 호출한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;34:58 [main] - value: 0
34:58 [main] - value: 2
34:58 [main] - value: 4
34:58 [main] - value: 6
34:58 [main] - value: 8
34:58 [main] - complete&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;handle의 sink를 사용해서 complete나 error를 더 일찍 전달하는 방식으로 사용할 수 있다는 점도 참고하자.&lt;/p&gt;
&lt;h4 id=&quot;delayelements&quot;&gt;delayElements&lt;/h4&gt;
&lt;p&gt;많이 사용하는 연산자 중에 delayElements라는 연산이 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;최소 delay 만큼 간격을 두고 onNext 이벤트 발행&lt;/li&gt;
&lt;li&gt;onNext 이벤트가 발행된 후 더 늦게 다음 onNext 이벤트가 전달되면 즉시 전파&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 이를 사용하면 처리량을 제한할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.create(
        sink -&amp;gt; {
            for (int i = 1; i &amp;lt;= 5; i++) {
                sleep(1000);
                sink.next(i);
            }
            sink.complete();
        })
        .delayElements(Duration.ofMillis(5000))
        .doOnNext(value -&amp;gt; log.info(&quot;doOnNext: &quot; + value))
        .subscribe();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 코드는 5초마다 onNext로 각 데이터가 전달된다.&lt;/p&gt;
&lt;h4 id=&quot;concat&quot;&gt;concat&lt;/h4&gt;
&lt;p&gt;concat을 사용하면 Publisher 들을 결합할 수 있다.&lt;/p&gt;
&lt;p&gt;내부 동작은 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이전의 Publisher가 onComplete 이벤트를 전달되면 다음 Publisher를 subscribe&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;코드를 보자&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;var flux1 = Flux.range(1, 3)
        .doOnSubscribe(value -&amp;gt; log.info(&quot;doOnSubscribe1&quot;))
        .delayElements(Duration.ofMillis(100));
var flux2 = Flux.range(10, 3)
        .doOnSubscribe(value -&amp;gt; log.info(&quot;doOnSubscribe2&quot;))
        .delayElements(Duration.ofMillis(100));

Flux.concat(flux1, flux2)
        .doOnNext(value -&amp;gt; log.info(&quot;doOnNext: &quot; + value))
        .subscribe();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래는 코드를 실행한 결과이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00:44 [main] - doOnSubscribe1
00:44 [parallel-1] - doOnNext: 1
00:44 [parallel-2] - doOnNext: 2
00:45 [parallel-3] - doOnNext: 3
00:45 [parallel-3] - doOnSubscribe2
00:45 [parallel-4] - doOnNext: 10
00:45 [parallel-5] - doOnNext: 11
00:45 [parallel-6] - doOnNext: 12&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Publisher의 내부 순서도 보장하고, 인자로 전달된 각 Publisher 간 순서도 보장된다.&lt;/p&gt;
&lt;p&gt;그래서 처리량은 다소 떨어지게 된다.&lt;/p&gt;
&lt;h4 id=&quot;merge&quot;&gt;merge&lt;/h4&gt;
&lt;p&gt;merge도 Publisher를 결합하는 연산이다.&lt;/p&gt;
&lt;p&gt;단, concat과 다르게 모든 Publisher를 바로 subscribe하고 각각의 Publisher의 onNext 이벤트가 동시에 도달된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;var flux1 = Flux.range(1, 3)
        .doOnSubscribe(value -&amp;gt; log.info(&quot;doOnSubscribe1&quot;))
        .delayElements(Duration.ofMillis(100));
var flux2 = Flux.range(10, 3)
        .doOnSubscribe(value -&amp;gt; log.info(&quot;doOnSubscribe2&quot;))
        .delayElements(Duration.ofMillis(100));

Flux.merge(flux1, flux2)
        .doOnNext(value -&amp;gt; log.info(&quot;doOnNext: &quot; + value))
        .subscribe();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래는 해당 코드 실행 결과이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;07:45 [main] - doOnSubscribe1
07:45 [main] - doOnSubscribe2
07:45 [parallel-1] - doOnNext: 1
07:45 [parallel-1] - doOnNext: 10
07:45 [parallel-3] - doOnNext: 11
07:45 [parallel-4] - doOnNext: 2
07:45 [parallel-5] - doOnNext: 12
07:45 [parallel-6] - doOnNext: 3
07:46 [main] - end main&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Publisher의 내부 순서는 보장하지만, 인자로 전달된 Publisher 간 순서를 보장하지 않는다.&lt;/p&gt;
&lt;p&gt;참고로 mergeSequential이라는 순서를 보장하는 연산자도 지원한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;동시에 실행된 결과를 내부적으로 재정렬하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;다양한-연산자-operators&quot;&gt;다양한 연산자 (Operators)&lt;/h2&gt;
&lt;p&gt;지금까지는 Sequence를 생성하는 방법에 대해 배웠다.&lt;/p&gt;
&lt;p&gt;아래는 Sequence를 처리하기 위해 대표적으로 사용되는 publisher가 가지는 연산이다. 대부분 Stream과 유사하기 때문에 간략하게만 소개한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;map:&lt;ul&gt;
&lt;li&gt;map: onNext 이벤트를 받아서 값을 변경하고 다음으로 전달&lt;/li&gt;
&lt;li&gt;mapNotNull: null인 경우 넘기지 않음. NPE를 방지할 수 있다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;flatMap:&lt;ul&gt;
&lt;li&gt;map은 &lt;code&gt;Mono&amp;lt;Mono&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;를 반환하지만, flatMap은 &lt;code&gt;Mono&amp;lt;T&amp;gt;&lt;/code&gt;를 반환 (Flux도 동일)&lt;/li&gt;
&lt;li&gt;연산을 수행하기 위해 Mono나 Flux의 값을 꺼낼 필요가 없어진다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;doOnXX: doOnSubscribe, doOnNext 등은 데이터 sequence에 영향을 전혀 주지 않고 로깅이나 추가작업을 수행할 수 있다.&lt;/li&gt;
&lt;li&gt;filter: onNext 이벤트를 받아서 true라면 onNext 이벤트를 전파하고, false라면 무시한다.&lt;/li&gt;
&lt;li&gt;take:&lt;ul&gt;
&lt;li&gt;take: n개까지 onNext 이벤트를 전파하고 n개에 도달하면 onComplete 이벤트를 발생시킨다.&lt;/li&gt;
&lt;li&gt;takeLast: onComplete 이벤트가 발생하기 직전의 n개의 아이템만 전파하고 나머지는 버린다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;skip:&lt;ul&gt;
&lt;li&gt;skip: 처음 n개의 onNext 이벤트를 무시하고 그 이후 onNext 이벤트를 전파한다.&lt;/li&gt;
&lt;li&gt;skipLast: onComplete 이벤트가 발생하기 직전 n개의 onNext 이벤트를 무시한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;collectList:&lt;ul&gt;
&lt;li&gt;next 이벤트가 전달되면 내부에 item을 저장한 후 complete 이벤트가 전달되면 저장했던 item을 list형태로 전달&lt;/li&gt;
&lt;li&gt;다음 Flux에서 나이가 가장 적은 유저를 뽑는다고 했을 때 전체 유저를 알아야 한다. 그래서 &lt;code&gt;Flux&amp;lt;User&amp;gt;&lt;/code&gt;가 아닌 &lt;code&gt;Mono&amp;lt;List&amp;lt;User&amp;gt;&amp;gt;&lt;/code&gt;가 필요하다. 그때 사용할 수 있다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;cache: 처음 subscribe에만 publisher를 실행하고, 이후 subscribe에서는 캐싱한 event를 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그냥 '이런 연산들이 있구나.' 라고 생각하고 필요할 때 찾아보면 된다.&lt;/p&gt;
&lt;p&gt;다음은 Thread와 Scheduler에 대해 알아보자.&lt;/p&gt;
&lt;h2 id=&quot;thread&quot;&gt;Thread&lt;/h2&gt;
&lt;p&gt;Reactor에서의 subscribe랑 sequence 개념과 사용 방법에 대해 익혔다.&lt;/p&gt;
&lt;p&gt;subscribe를 하면 어떤 쓰레드에서 실행되는 걸까?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;기본적으로&lt;/strong&gt;는 Publisher랑 subscribe가 같은 쓰레드에서 실행된다. 즉, 기본적으로는 동기적으로 동작한다고 볼 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;별도의 설정이 없다면 subscribe를 호출한 caller의 쓰레드에서 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래 코드를 실행해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static void main(String[] args) {
    var executor = Executors.newSingleThreadExecutor();
    try {
        executor.submit(() -&amp;gt; {
            log.info(&quot;start!&quot;);
            Flux.create(sink -&amp;gt; {
                for (int i = 1; i &amp;lt;= 5; i++) {
                    log.info(&quot;next: {}&quot;, i);
                    sink.next(i);
                }
            }).subscribe(value -&amp;gt; {
                log.info(&quot;value: &quot; + value);
            });
        });
    } finally {
        executor.shutdown();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과 아래와 같이 동일한 쓰레드를 사용함을 알 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;26:36 [pool-2-thread-1] - start!
26:36 [pool-2-thread-1] - next: 1
26:36 [pool-2-thread-1] - value: 1
26:36 [pool-2-thread-1] - next: 2
26:36 [pool-2-thread-1] - value: 2
26:36 [pool-2-thread-1] - next: 3
26:36 [pool-2-thread-1] - value: 3
26:36 [pool-2-thread-1] - next: 4
26:36 [pool-2-thread-1] - value: 4
26:36 [pool-2-thread-1] - next: 5
26:36 [pool-2-thread-1] - value: 5&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;shceduler&quot;&gt;Shceduler&lt;/h2&gt;
&lt;p&gt;Scheduler로 Publish 혹은 Subscribe에 task를 실행하는 쓰레드 풀을 설정할 수 있고, Task를 언제 수행할 지 설정할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ImmediateScheduler&lt;ul&gt;
&lt;li&gt;subscribe를 호출한 caller 쓰레드에서 즉시 실행한다.&lt;/li&gt;
&lt;li&gt;별도 Scheduler를 넘기지 않는다면 기본으로 사용된다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;SingleScheduler&lt;ul&gt;
&lt;li&gt;캐싱된 1개 크기의 쓰레드 풀을 제공&lt;/li&gt;
&lt;li&gt;모든 publish, subsscribe가 하나의 쓰레드에서 실행&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;ParallelScheduler&lt;ul&gt;
&lt;li&gt;캐싱된 n개 크기의 쓰레드 풀을 제공&lt;/li&gt;
&lt;li&gt;기본적으로 CPU 코어 수만큼의 크기를 갖는다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;BoundedElasticScheduler&lt;ul&gt;
&lt;li&gt;캐싱된 고정되지 않은 크기의 쓰레드 풀을 제공&lt;/li&gt;
&lt;li&gt;재사용할 수 있는 쓰레드가 있다면 사용하고, 없으면 새로 생성&lt;/li&gt;
&lt;li&gt;특정 시간(default  = 60s) 사용하지 않으면 제거&lt;/li&gt;
&lt;li&gt;생성 가능한 최대 쓰레드 수는 CPU 코어 수 x 10&lt;/li&gt;
&lt;li&gt;I/O Blocking 작업을 수행할 때 적합&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Scheduler는 subscribeOn()으로 설정할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.create(sink -&amp;gt; {
    for (int i = 1; i &amp;lt;= 5; i++) {
        log.info(&quot;next: {}&quot;, i);
        sink.next(i);
    }
}).subscribeOn(
        Schedulers.single()
).subscribe(value -&amp;gt; {
    log.info(&quot;value: &quot; + value);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;schuedlernewxx&quot;&gt;Schuedler.newXX&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Schedulers.single()&lt;/code&gt;를 사용한다면 해당 스케줄러를 사용하는 작업들이 스레드 풀을 공유한다.&lt;/p&gt;
&lt;p&gt;매번 새로운 쓰레드 풀을 할당하거나 중요한 작업들을 위해 별도의 쓰레드 풀을 할당해야 한다면 Scheduler.newXX를 사용할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;newSingle()&lt;/li&gt;
&lt;li&gt;newParallel()&lt;/li&gt;
&lt;li&gt;newBoundedElastic()&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래 코드는 Schedulers.newSingle()을 사용해서 쓰레드 풀을 다른 작업과 공유하지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class SingleService {
    Scheduler newSingle = Schedulers.newSingle(&quot;single&quot;);

    void singleSchedulerTest(int idx) {
        Flux.create(sink -&amp;gt; {
            log.info(&quot;next: {}&quot;, idx);
            sink.next(idx);
        }).subscribeOn(
                newSingle
        ).subscribe(value -&amp;gt; {
            log.info(&quot;value: &quot; + value);
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;fromexecutorservice&quot;&gt;fromExecutorService&lt;/h4&gt;
&lt;p&gt;ExecutorService 사용에 익숙하다면 아래와 같이 Scheduler 인스턴스를 생성할 수도 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Schedulers.fromExecutorService(executorService)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;publishon&quot;&gt;publishOn&lt;/h4&gt;
&lt;p&gt;subscribeOn으로 스케줄러를 조정할 수 있었는데, publishOn을 사용해서 이후에 &lt;strong&gt;추가되는 연산자들의 스케줄러를 설정&lt;/strong&gt;할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;publishOn은 subscribeOn과 다르게 위치가 중요하다.&lt;/li&gt;
&lt;li&gt;적용 이후 다른 publishOn이 적용되면 추가된 Scheduler로 실행 쓰레드 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래 예시를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.create(sink -&amp;gt; {
    for (var i = 0; i &amp;lt; 3; i++) {
        log.info(&quot;next: {}&quot;, i);
        sink.next(i);
    }
}).publishOn(
        Schedulers.single()
).doOnNext(item -&amp;gt; {
    log.info(&quot;doOnNext: {}&quot;, item);
}).subscribe(value -&amp;gt; {
    log.info(&quot;value: &quot; + value);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과는 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;19:20 [main] - next: 0
19:20 [main] - next: 1
19:20 [main] - next: 2
19:20 [single-1] - doOnNext: 0
19:20 [single-1] - value: 0
19:20 [single-1] - doOnNext: 1
19:20 [single-1] - value: 1
19:20 [single-1] - doOnNext: 2
19:20 [single-1] - value: 2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래는 publishOn과 subscribeOn을 같이 사용한 예시이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.create(sink -&amp;gt; {
    for (var i = 0; i &amp;lt; 3; i++) {
        log.info(&quot;next: {}&quot;, i);
        sink.next(i);
    }
}).publishOn(
        Schedulers.single()
).doOnNext(item -&amp;gt; {
    log.info(&quot;doOnNext: {}&quot;, item);
}).publishOn(
        Schedulers.boundedElastic()
).doOnNext(item -&amp;gt; {
    log.info(&quot;doOnNext2: {}&quot;, item);
}).subscribeOn(Schedulers.parallel()
).subscribe(value -&amp;gt; {
    log.info(&quot;value: &quot; + value);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과는 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;28:46 [parallel-1] - next: 0
28:46 [parallel-1] - next: 1
28:46 [parallel-1] - next: 2
28:46 [single-1] - doOnNext: 0
28:46 [single-1] - doOnNext: 1
28:46 [single-1] - doOnNext: 2
28:46 [boundedElastic-1] - doOnNext2: 0
28:46 [boundedElastic-1] - value: 0
28:46 [boundedElastic-1] - doOnNext2: 1
28:46 [boundedElastic-1] - value: 1
28:46 [boundedElastic-1] - doOnNext2: 2
28:46 [boundedElastic-1] - value: 2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;동작을 설명하면 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;subscribeOn이 소스에 영향을 주기 때문에 소스가 parallel로 동작을 한다.&lt;/li&gt;
&lt;li&gt;이후 동작부터는 publishOn으로 인해 single로 동작을 한다.&lt;/li&gt;
&lt;li&gt;이후 동작부터는 새로운 publishOn으로 인해 boundedElastic으로 동작한다. &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음은 에러 핸들링에 대해 알아보자.&lt;/p&gt;
&lt;h2 id=&quot;에러-핸들링&quot;&gt;에러 핸들링&lt;/h2&gt;
&lt;p&gt;Reactive streams에서 onError 이벤트가 발생하면 onNext, onComplete 이벤트를 생산하지 않고 onError 이벤트를 아래로 쭉 전파하고 종료한다.&lt;/p&gt;
&lt;p&gt;onError 이벤트는 기본적으로 아래의 방식으로 처리할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;고정된 값을 반환&lt;/li&gt;
&lt;li&gt;publisher를 반환&lt;/li&gt;
&lt;li&gt;onComplete 이벤트로 변경&lt;/li&gt;
&lt;li&gt;다른 에러로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;에러-핸들링이-없을-경우&quot;&gt;에러 핸들링이 없을 경우?&lt;/h4&gt;
&lt;p&gt;에러 핸들링이 없으면 내부적으로 onErrorDropped를 호출하게 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static void onErrorDropped(Throwable e, Context context) {
    Consumer&amp;lt;? super Throwable&amp;gt; hook = context.getOrDefault(Hooks.KEY_ON_ERROR_DROPPED,null);
    if (hook == null) {
        hook = Hooks.onErrorDroppedHook;
    }
    if (hook == null) {
        log.error(&quot;Operator called default onErrorDropped&quot;, e);
        return;
    }
    hook.accept(e);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;내부적으로 log.error()를 사용해서 로그를 출력한다.&lt;/p&gt;
&lt;h4 id=&quot;errorconsumer&quot;&gt;ErrorConsumer&lt;/h4&gt;
&lt;p&gt;에러 핸들링의 가장 쉬운 방법 중 하나가 subscribe의 두번째 인자인 errorConsumer를 활용하는 방법이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.error(new RuntimeException(&quot;error&quot;))
        .subscribe(value -&amp;gt; {
            log.info(&quot;value: &quot; + value);
        }, error -&amp;gt; {
            log.info(&quot;error: &quot; + error);
        });&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;onerrorreturn&quot;&gt;onErrorReturn&lt;/h4&gt;
&lt;p&gt;ErrorConsumer는 특정 Action만 수행하지 결과를 반환하기 어렵다.&lt;/p&gt;
&lt;p&gt;이때 onErrorReturn을 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.error(new RuntimeException(&quot;error&quot;))
        .onErrorReturn(0)
        .subscribe(value -&amp;gt; {
            log.info(&quot;value: &quot; + value);
        });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;onErrorReturn을 사용하면 고정된 값을 반환할 수 있다. 단, onErrorReturn에는 함수를 전달할 수 없다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;인자로 함수의 결과를 전달한다면 Subscribe도 되기 전에 동작할 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;onerrorresume&quot;&gt;onErrorResume&lt;/h4&gt;
&lt;p&gt;onErrorReturn은 함수형 인터페이스를 전달 받을 수 없었다.&lt;/p&gt;
&lt;p&gt;onErrorResume은 함수형 인터페이스를 전달 받아서 에러가 발생한 경우 함수형 인터페이스의 결과를 다음 subscribe에 전달할 수 있다. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.error(new RuntimeException(&quot;error&quot;))
        .onErrorResume(throwable -&amp;gt; Flux.just(0, -1, -2))
        .subscribe(value -&amp;gt; {
            log.info(&quot;value: &quot; + value);
        });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;onErrorResume을 사용하면 실제로 에러가 발생한 경우에만 함수형 인터페이스를 실행하게 된다.&lt;/p&gt;
&lt;h4 id=&quot;onerrorcomplete&quot;&gt;onErrorComplete&lt;/h4&gt;
&lt;p&gt;onErrorComplete는 onError 이벤트를 onComplete 이벤트로 변경한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.create(sink -&amp;gt; {
    sink.next(1);
    sink.next(2);
    sink.error(new RuntimeException(&quot;error&quot;));
}).onErrorComplete()
        .subscribe(
                value -&amp;gt; log.info(&quot;value: &quot; + value),
                null,
                () -&amp;gt; log.info(&quot;complete&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음 2번은 sink.next(n)으로 정상적으로 Consumer가 동작하고, 원래라면 3번째에는 sink.error()로 인해 ErrorConsumer가 동작했다.&lt;/p&gt;
&lt;p&gt;실제로는 onErrorComplete()로 인해 onComplete 이벤트로 변경되므로 CompleteConsumer가 동작한다.&lt;/p&gt;
&lt;h4 id=&quot;onerrormap&quot;&gt;onErrorMap&lt;/h4&gt;
&lt;p&gt;IOException을 커스텀 비즈니스 익셉션으로 핸들링 하는 경우 아래와 같이 onErrorMap을 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.error(new IOException(&quot;fail to read file&quot;))
        .onErrorMap(e -&amp;gt; new CustomBusinessException(&quot;custom&quot;))
        .subscribe(value -&amp;gt; log.info(&quot;value: &quot; + value),
                e -&amp;gt; log.info(&quot;error: &quot; + e));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;onError는 예외를 다른 예외로 변환한다.&lt;/p&gt;
&lt;h4 id=&quot;doonerror&quot;&gt;doOnError&lt;/h4&gt;
&lt;p&gt;에러를 변환할 필요가 없고, ErrorConsumer까지 전달되기 전에 처리가 필요하다면 doOnError를 활용할 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.error(new RuntimeException(&quot;error&quot;))
        .doOnError(error -&amp;gt; log.info(&quot;doOnError: &quot; + error))
        .subscribe(value -&amp;gt; log.info(&quot;value: &quot; + value), 
                error -&amp;gt; log.info(&quot;error: &quot; + error));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;context&quot;&gt;Context&lt;/h2&gt;
&lt;p&gt;각 연산에서 Background를 공유해야 하는 환경에서는 어떻게 할까?&lt;/p&gt;
&lt;p&gt;ThreadLocal을 떠올릴 수 있지만 다른 쓰레드에서 접근할 수 없으므로 파이프라인 안에서 쓰레드가 변경되면 공유가 불가능해진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public interface ContextView {
    &amp;lt;T&amp;gt; T get(Object key);
    boolean kasKey(Object key);
    boolean isEmpty();
    int size();
}

public interface Context extends ContextView {
    Context put(Object key, Object value);
    Context delete(Object key);
    Context putAll(Context context);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Context와 ContextView는 아래 역할을 수행한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Context는 파이프라인 내부 어디서든 접근 가능한 key-value 저장소&lt;/li&gt;
&lt;li&gt;Context는 구독이 발생할 때마다 하나의 Context가 생긴다.&lt;/li&gt;
&lt;li&gt;Context는 쓰기를 할 수 있고, ContextView는 읽기 전용이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Context에 접근하기 위해서 아래의 메서드가 제공된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public final Mono&amp;lt;T&amp;gt; contextWrite(
        Function&amp;lt;Context, Context&amp;gt; contextModifier) {}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래는 예시이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.just(1)
        .flatMap(v -&amp;gt; ContextLogger.logContext(v, &quot;1&quot;))
        .contextWrite(context -&amp;gt;
                context.put(&quot;name&quot;, &quot;violet&quot;))
        .flatMap(v -&amp;gt; ContextLogger.logContext(v, &quot;2&quot;))
        .contextWrite(context -&amp;gt;
                context.put(&quot;name&quot;, &quot;beach&quot;))
        .flatMap(v -&amp;gt; ContextLogger.logContext(v, &quot;3&quot;))
        .subscribe();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;contextWrite를 사용할 때 주의할 점이 있다. 아래 결과를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;31:47 [main] - name: 1, context: Context1{name=violet}
31:47 [main] - name: 2, context: Context1{name=beach}
31:47 [main] - name: 3, context: Context0{}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;마지막 flatMap에서는 Context에 아무것도 들어있지 않은 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;contextWrite는 subscribe부터 Upstream으로 위로 올라가며 Write를 실행하고 위 연산자에 전달한다. 즉, contextWrite 위에 있는 flatMap에 영향을 끼쳤고, 마지막 flatMap은 아래에 contextWrite가 없으므로 비어있는 상태인 것이다.&lt;/p&gt;
&lt;p&gt;Context는 인증 정보와 같이 독립적인 데이터를 전달하기에 적합하다.&lt;/p&gt;
&lt;p&gt;read는 &lt;code&gt;deferContextual()&lt;/code&gt;를 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;위 코드에서 결과를 출력한 ContextLogger도 내부적으로 아래와 같이 작성되어 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class ContextLogger {
    public static &amp;lt;T&amp;gt; Mono&amp;lt;T&amp;gt; logContext(T t, String name) {
        return Mono.deferContextual(c -&amp;gt; {
            log.info(&quot;name: {}, context: {}&quot;, name, c);
            return Mono.just(t);
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아니면 연산에서 sink를 사용해서 직접 꺼내는 방법도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Flux.create(sink -&amp;gt; {
    var name = sink.contextView().get(&quot;name&quot;);
    log.info(&quot;name in create: &quot; + name);
    sink.next(1);
}).contextWrite(context -&amp;gt;
        context.put(&quot;name&quot;, &quot;violet&quot;)
).subscribe(null, null, null, initialContext);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://fastcampus.co.kr/courses/216172&quot;&gt;https://fastcampus.co.kr/courses/216172&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://projectreactor.io/&quot;&gt;https://projectreactor.io/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reactor/reactor-core&quot;&gt;https://github.com/reactor/reactor-core&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Server/Spring Reactive</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/412</guid>
      <comments>https://jaehoney.tistory.com/412#entry412comment</comments>
      <pubDate>Mon, 5 Feb 2024 21:57:28 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux란 무엇인가?! - 1. Netty 이해하기!</title>
      <link>https://jaehoney.tistory.com/406</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;reactive-programming-with-spring-boot&quot;&gt;Reactive Programming with Spring Boot&lt;/h2&gt;
&lt;p&gt;아래는 Spring 공식문서에 나와있는 Spring MVC와 Spring Webflux에 대한 설명이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bM420C/btsD09FzrzQ/t47fvxYdZscBTTJynqjjMk/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Spring MVC는 동기 블로킹 기반의 서블릿 API와 request-per-thread 모델을 제공하고, Spring Webflux는 대량의 동시 커넥션이 가능한 Non-Blocking 웹 프레임워크이다. &lt;/p&gt;
&lt;p&gt;해당 부분을 조금 더 풀어서 알아보자.&lt;/p&gt;
&lt;h2 id=&quot;spring-webflux&quot;&gt;Spring Webflux&lt;/h2&gt;
&lt;p&gt;Spring MVC는 request-per-thread 모델이기 때문에 만약 1만개 이상의 요청이 동시에 들어온다면 쓰레드가 부족하게 된다.&lt;/p&gt;
&lt;p&gt;Spring Webflux는 &lt;strong&gt;쓰레드를 가능한 최소한으로 사용하는 모델&lt;/strong&gt;과 &lt;strong&gt;리액티브 프로그래밍&lt;/strong&gt; 라이브러리를 제공한다고 생각하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kWQkG/btsD5weH5do/SSJGxxYSykPGEKlSWU0eKK/img.png&quot; alt=&quot;i_1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Spring Webflux는 &lt;strong&gt;비동기 + 리액티브 프로그래밍&lt;/strong&gt;을 기본으로 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;요청을 Event-Driven 방식으로 처리한다.&lt;/li&gt;
&lt;li&gt;작업이 완료될 때까지 다른 일을 하다가, 처리가 완료되면 Callback 메서드를 통해 응답을 반환한다.&lt;/li&gt;
&lt;li&gt;비동기 + 논블로킹&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Webflux는 작업이 끝날 때까지 기다리지 않기 때문에 cpu, thread, memory의 자원을 최대한 낭비하지 않고 효율적으로 동작하는 고성능 애플리케이션 개발에서 사용한다.&lt;/p&gt;
&lt;p&gt;토비님의 세미나에서는 &lt;strong&gt;서비스 간 호출이 많은 마이크로 서비스 아키텍처에 적합&lt;/strong&gt;하고, 함수형 프로그래밍의 이점이 있는 것도 Webflux를 선택하기에 충분한 이유가 된다고 설명한다.&lt;/p&gt;
&lt;h2 id=&quot;netty&quot;&gt;Netty&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIlyDN/btsD4U7NA33/u17LwrQtWOEqHHZi4dr6Mk/img.png&quot; alt=&quot;i_2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Spring Webflux를 사용하면 요청을 받는 내장 서버로 기본적으로 &lt;strong&gt;Netty&lt;/strong&gt;를 사용한다.&lt;/p&gt;
&lt;p&gt;Netty는 Async / NIO(Non-Blocking IO)에 초점을 둔 이벤트 기반 네트워크 애플리케이션 프레임워크이다. Netty는 유지보수를 고려한 고성능 프로토콜 서버나 클라이언트를 개발할 때 주로 사용한다.&lt;/p&gt;
&lt;p&gt;Netty의 장점은 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;비동기 이벤트 기반 네트워킹(Event Driven)을 지원&lt;/li&gt;
&lt;li&gt;Tomcat과 다르게 자원이 항상 스레드를 점유하고 Block을 유지하지 않으므로 처리량 대폭 증가&lt;/li&gt;
&lt;li&gt;스레드 수가 적다.&lt;/li&gt;
&lt;li&gt;Context switching 오버헤드 감소 (1개 Thread에서 쌓인 Event Queue를 기반으로 Non-Blocking으로 동작하기 때문!)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;event-loop&quot;&gt;Event Loop&lt;/h3&gt;
&lt;p&gt;Netty에서 핵심은 Event Loop이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgqupQ/btsD3axaG9o/SWF43rKW4WIpeHByiURFSK/img.png&quot; alt=&quot;i.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Event Loop에는 아래의 컴포넌트가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Channel:&lt;ul&gt;
&lt;li&gt;하나의 이벤트 루프에 등록된다.&lt;/li&gt;
&lt;li&gt;Channel에서 이벤트가 발생하면 해당 이벤트 루프의 이벤트 큐에 등록된다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Event Loop: 이벤트 큐에서 이벤트를 꺼내어서 작업을 비동기로 실행 (1개의 Thread는 여러개의 Event Loop 가질 수 있다.)&lt;/li&gt;
&lt;li&gt;Pipeline: 이벤트를 받아서 Handler로 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래 코드를 보자.&lt;/p&gt;
&lt;p&gt;EventLoopGroup은 &lt;code&gt;io.netty.channel&lt;/code&gt;에 있는 Netty가 사용하는 EventLoopGroup이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Netty에서는 EventLoop를 직접 사용할 수 없다. 그래서 EventLoopGroup를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static void main(String[] args) {
    EventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);

    for (int i = 0; i &amp;lt; 10; i++) {
        final int idx = i;
        eventLoopGroup.execute(() -&amp;gt; {
            log.info(&quot;i: {}&quot;, idx);
        });
    }

    eventLoopGroup.shutdownGracefully();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;EventLoop는 1개의 쓰레드에서 동작하기 때문에 아래와 같이 들어간 순서가 보장된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XTldm/btsD4THOFAA/EsrOQG73ohM83OcQunw3t1/img.png&quot; alt=&quot;img_1.png&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;event-loop-group&quot;&gt;Event Loop Group&lt;/h3&gt;
&lt;p&gt;Netty의 EventLoopGroup은 여러 개의 EventLoop를 포함할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static void main(String[] args) {
    EventLoopGroup eventLoopGroup = new NioEventLoopGroup(5);

    for (int i = 0; i &amp;lt; 12; i++) {
        final int idx = i;
        eventLoopGroup.execute(() -&amp;gt; {
            log.info(&quot;i: {}&quot;, idx);
        });
    }

    eventLoopGroup.shutdownGracefully();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과는 아래와 같다. &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmB9e1/btsD0Mjlb7B/9hZIK13mIg2JqEK52MxZK1/img.png&quot; alt=&quot;img_2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;다른 EventLoop 간 쓰레드가 동일함을 보장하지 않으므로 순서를 보장하지 않는다.&lt;/p&gt;
&lt;p&gt;단, 로그를 자세히 보면 &lt;strong&gt;동일한 EventLoop 간은 순서가 보장&lt;/strong&gt;된다.&lt;/p&gt;
&lt;h3 id=&quot;channel&quot;&gt;Channel&lt;/h3&gt;
&lt;p&gt;Netty는 Java NIO의 Channel과 유사한 자체적인 Channel을 만들어서 사용한다. 차이점은 Pipeline이나 Future와 같은 추가적인 기능을 제공한다.&lt;/p&gt;
&lt;h4 id=&quot;abstractchannel&quot;&gt;AbstractChannel&lt;/h4&gt;
&lt;p&gt;Netty의 AbstractChannel 내부적으로 Pipeline을 갖는다.&lt;/p&gt;
&lt;p&gt;Netty에서 제공하는 모든 Channel은 Pipeline을 사용한다.&lt;/p&gt;
&lt;h4 id=&quot;nioserversocketchannel&quot;&gt;NioServerSocketChannel&lt;/h4&gt;
&lt;p&gt;Netty 서버를 구성할 때 NioServerSocketChannel을 주로 사용한다.&lt;/p&gt;
&lt;p&gt;NioServerSocketChannel은 AbstractNioChannel을 상속하고, AbstractNioChannel은 내부적으로 자신이 등록된 Selector와 SelectableChannel을 가진다.&lt;/p&gt;
&lt;p&gt;그래서 Selector와 SelectableChannel을 사용해서 더 쉽게 현재 상태를 파악할 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;channelfuture&quot;&gt;ChannelFuture&lt;/h3&gt;
&lt;p&gt;아래는 ChannelFuture 인터페이스의 일부이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public interface ChannelFuture extends Future&amp;lt;Void&amp;gt; {
    Channel channel();

    @Override
    ChannelFuture addListener(GenericFutureListener&amp;lt;? extends Future&amp;lt;? super Void&amp;gt;&amp;gt; listener);

    @Override
    ChannelFuture removeListener(GenericFutureListener&amp;lt;? extends Future&amp;lt;? super Void&amp;gt;&amp;gt; listener);

    @Override
    ChannelFuture sync() throws InterruptedException;

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ChannelFuture는 아래 역할을 수행한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Channel I/O 작업이 완료되면 isDone이 ture가 되는 Future&lt;/li&gt;
&lt;li&gt;FutureListener를 등록/삭제하여 Write이 완료되었을 때 비동기 처리 가능&lt;/li&gt;
&lt;li&gt;addListener: Channel I/O 작업이 완료되면 수행할 Listener 등록&lt;/li&gt;
&lt;li&gt;removeListener: 등록된 Listener 제거&lt;/li&gt;
&lt;li&gt;sync: 작업이 완료될 때까지 Blocking&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;channelpipeline&quot;&gt;ChannelPipeline&lt;/h3&gt;
&lt;p&gt;Netty에서 매우 중요한 역할을 하는 것 중 하나가 ChannelPipeline이다.&lt;/p&gt;
&lt;p&gt;아래 그림을 보자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OotRm/btsD4unYRTh/17hOPEFaYOF8IsKtIfgSo0/img.png&quot; alt=&quot;img_3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;해당 그림을 설명하면 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;EventLoop는 Channel의 Inbound I/O를 감시한다.&lt;/li&gt;
&lt;li&gt;EventLoop는 I/O가 준비되면 ChannelPipeline으로 이벤트를 전파한다.&lt;/li&gt;
&lt;li&gt;ChannelPipeline은 I/O 이벤트를 처리한다.&lt;/li&gt;
&lt;li&gt;ChanelPipeline은 처리가 완료되면 Channel에 결과를 Outbound I/O로써 write 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, &lt;strong&gt;ChannelPipeline을 어떻게 구성하는 지&lt;/strong&gt;가 핵심이 된다.&lt;/p&gt;
&lt;p&gt;ChannelPipeline의 내부는 아래와 같이 구성된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GM3vL/btsD1pnXBNb/ZhSh6NhCxAxeYpngZm7J31/img.png&quot; alt=&quot;img_6.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;ChannelPipeline은 여러 개의 ChannelHandler를 가지고, 각 ChannelHandlerContext와 연결된 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;다음은 ChannelHandlerContext와 ChannelHandler에 대해 알아보자.&lt;/p&gt;
&lt;h2 id=&quot;channelhandlercontext&quot;&gt;ChannelHandlerContext&lt;/h2&gt;
&lt;p&gt;위 그림과 같이 pipeline의 ChannelHandlerContext는 LinkedList 형태로 next, prev를 통해 연결되어 있다.&lt;/p&gt;
&lt;p&gt;각 ChannelHandlerContext는 I/O 작업을 처리한 후 다음 Context에 넘기게 된다.&lt;/p&gt;
&lt;p&gt;다음은 ChannelHandlerContext의 내부이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzliE5/btsD28MU7p4/78QjoPzE7m8s4n1iAQN6C0/img.png&quot; alt=&quot;img_4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1개의 ChannelHandlerContext는 ChannelHandler를 가진다.&lt;/p&gt;
&lt;p&gt;그리고 ChannelHandlerContext는 EventExecutor를 가질 수 있다. EventLoop에 Blocking이 생기는 것을 막고 별도 쓰레드에서 I/O 작업을 수행하기 위해 EventExecutor를 사용한다.&lt;/p&gt;
&lt;h2 id=&quot;channelhandler&quot;&gt;ChannelHandler&lt;/h2&gt;
&lt;p&gt;ChannelHandler는 I/O 이벤트를 받아서 다음 Context에게 넘겨줄 수 있고, 직접 I/O 작업을 수행할 수도 있다.&lt;/p&gt;
&lt;p&gt;inbound I/O 이벤트는 ChannelInboundHandler가 처리하고, Outbound I/O 이벤트는 ChannelOutboundHandler가 처리한다.&lt;/p&gt;
&lt;p&gt;참고로 ChannelDuplexHandler는 ChannelInboundHandler, ChannelOutboundHandler 둘 다 구현한다.&lt;/p&gt;
&lt;h4 id=&quot;channelinboundhandler&quot;&gt;ChannelInboundHandler&lt;/h4&gt;
&lt;p&gt;ChannelInboundHandler는 다양한 경우에 핸들링할 수 있는 메서드를 제공한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;channelRegistered: Channel이 EventLoop에 등록된 경우&lt;/li&gt;
&lt;li&gt;channelUnregistered: Channel이 EventLoop에서 제거된 경우&lt;/li&gt;
&lt;li&gt;channelActive: Channel이 Active된 경우&lt;/li&gt;
&lt;li&gt;channelInactive: Channel이 Inactive된 경우&lt;/li&gt;
&lt;li&gt;channelRead: Channel로부터 메시지를 읽을 준비가 된 경우&lt;/li&gt;
&lt;li&gt;channelReadComplete: Channel로부터 모든 메시지를 읽은 경우&lt;/li&gt;
&lt;li&gt;channelWritabilityChanged: Channel이 Write가 가능한 상태가 변경된 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;channeloutboundhandler&quot;&gt;ChannelOutboundHandler&lt;/h4&gt;
&lt;p&gt;ChannelOutboundHandler도 Outbound I/O 작업을 가로채서 처리할 수 있는 메서드를 제공한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bind: serverSocketChannel에 bind 요청 시 호출&lt;/li&gt;
&lt;li&gt;connect: socketChannel이 connect 요청 시 호출&lt;/li&gt;
&lt;li&gt;disconnect: socketChannel이 disconnect 요청 시 호출&lt;/li&gt;
&lt;li&gt;deregister: eventLoop로부터 deregister되면 호출&lt;/li&gt;
&lt;li&gt;read: channel에 대한 read 요청 시 호출&lt;/li&gt;
&lt;li&gt;write: channel에 대한 write 요청 시 호출&lt;/li&gt;
&lt;li&gt;flush: flush 작업 수행 후 호출&lt;/li&gt;
&lt;li&gt;close: channel이 닫히면 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;예시-코드&quot;&gt;예시 코드&lt;/h4&gt;
&lt;p&gt;아래는 ChannelInboundHandlerAdapter를 상속받는 클래스이다. ChannelInboundHandlerAdapter는 ChannelInboundHandler 인터페이스에 대한 골격을 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class SampleChannelInboundHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        if (msg instanceof String) {
            // 다음 context로 이벤트를 전달하지 않고, outbound I/O 작업을 수행한 후 채널을 닫는다.
            ctx.writeAndFlush(&quot;Hello, &quot; + msg)
                    .addListener(ChannelFutureListener.CLOSE);
        } else if (msg instanceof ByteBuf) {
            // 메시지를 가공한 후 다음 context로 이벤트를 전달
            try {
                var buf = (ByteBuf) msg;
                var len = buf.readableBytes();
                var charset = StandardCharsets.UTF_8;
                var body = buf.readCharSequence(len, charset);
                ctx.fireChannelRead(body); // 다음 Context로 read 이벤트 전달
            } finally {
                ReferenceCountUtil.release(msg);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;받은 값을 그대로 bypass 하는 방식도 가능하다.&lt;/p&gt;
&lt;p&gt;아래는 ChannelOutboundHandler의 구현이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class SampleChannelOutboundHandler extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        if (msg instanceof String) {
            ctx.write(msg, promise);
        } else if (msg instanceof ByteBuf) {
            var buf = (ByteBuf) msg;
            var len = buf.readableBytes();
            var charset = StandardCharsets.UTF_8;
            var body = buf.readCharSequence(len, charset);
            ctx.write(body, promise);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 Handler는 String 타입일 경우 다음 OutboundHandler에게 패스하고, ByteBuf 타입이라면 가공한 후 write 한다.&lt;/p&gt;
&lt;h2 id=&quot;encoder-decoder&quot;&gt;Encoder, Decoder&lt;/h2&gt;
&lt;p&gt;Netty에서 외부에서 들어오는 데이터를 변환해주는 Decoder가 있고, 내부의 데이터를 변경해주는 Encoder를 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;Decoder의 경우 ChannelInBoundHandler를 구현하고, Encoder의 경우 ChannelOutBoundHandler를 구현한다.&lt;/p&gt;
&lt;p&gt;예를 들면 Netty는 아래의 클래스를 제공한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;StringDecoder: ByteBuf 객체를 String으로 변환하여 다음 Handler에게 제공한다.&lt;/li&gt;
&lt;li&gt;StringEncoder: String 객체를 ByteBuf로 변경하여 다음 handler에 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이를 사용하면 Handler의 구현을 간소화할 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;bootstrap&quot;&gt;Bootstrap&lt;/h2&gt;
&lt;p&gt;Netty는 Netty 서버나 클라이언트를 쉽게 만들 수 있게 Bootstrap이라는 클래스를 제공한다.&lt;/p&gt;
&lt;p&gt;Bootstrap은 다음의 메서드를 가진다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;group: EventLoopGroup 등록&lt;ul&gt;
&lt;li&gt;parent(accept 이벤트), child(read 이벤트)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;channel: Channel 클래스를 기반으로 인스턴스 생성&lt;/li&gt;
&lt;li&gt;childHandler: connect되었을 때 실행할 코드 &lt;/li&gt;
&lt;li&gt;bind: 특정 호스트, 포트에 bind하고 channelFuture 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래는 Bootstrap을 활용한 TCP 서버의 예시이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;EventLoopGroup parentGroup = new NioEventLoopGroup();
EventLoopGroup childGroup = new NioEventLoopGroup(4);

var bootstrap = new ServerBootstrap();
var executorGroup = new DefaultEventExecutorGroup(4);
var stringEncoder = new StringEncoder();
var stringDecoder = new StringDecoder();

var bind = bootstrap
        .group(parentGroup, childGroup)
        .channel(NioServerSocketChannel.class)
        .childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
            @Override
            protected void initChannel(SocketChannel ch) {
                ch.pipeline()
                        .addLast(executorGroup, new LoggingHandler(LogLevel.INFO))
                        .addLast(stringEncoder, stringDecoder, echoHandler());
            }
        })
        .option(ChannelOption.SO_BACKLOG, 128)
        .childOption(ChannelOption.SO_KEEPALIVE, true)
        .bind(8080);

bind.sync().addListener(future -&amp;gt; {
    if (future.isSuccess()) {
        log.info(&quot;Server bound to port 8080&quot;);
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bootstrap은 각 컴포넌트 조립을 수행해서 Netty 코드를 줄이는 데 큰 도움을 준다.&lt;/p&gt;
&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;
&lt;p&gt;지금까지 정리한 내용을 정리해보면 Netty의 전체적인 구성은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n6MBD/btsD4SPHAgJ/8vc7hyt7AKdznK3TiWODr1/img.png&quot; alt=&quot;img_7.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;이상으로 Netty에 대한 내용을 마치고 다음에는 Reactor에 대해서 자세하게 알아보자.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://thalals.tistory.com/381&quot;&gt;https://thalals.tistory.com/381&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://perfectacle.github.io/2021/02/28/netty-event-loop&quot;&gt;https://perfectacle.github.io/2021/02/28/netty-event-loop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://recordsoflife.tistory.com/1314&quot;&gt;https://recordsoflife.tistory.com/1314&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-webflux-concurrency&quot;&gt;https://www.baeldung.com/spring-webflux-concurrency&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/reactive/&quot;&gt;https://spring.io/reactive/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fastcampus.co.kr/courses/216172&quot;&gt;https://fastcampus.co.kr/courses/216172&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Server/Spring Reactive</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/406</guid>
      <comments>https://jaehoney.tistory.com/406#entry406comment</comments>
      <pubDate>Sun, 28 Jan 2024 20:28:04 +0900</pubDate>
    </item>
    <item>
      <title>자바 NIO 간략하게 알아보기!</title>
      <link>https://jaehoney.tistory.com/405</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;java-nio&quot;&gt;Java NIO&lt;/h2&gt;
&lt;p&gt;자바에서 &lt;code&gt;InputStream&lt;/code&gt;, &lt;code&gt;OutputStream&lt;/code&gt;과 같이 *Stream으로 통신하는 모델을 Java IO 모델이라고 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;패키지는 &lt;code&gt;java.io.*&lt;/code&gt;에 속한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;자바 NIO란 New Input/Output의 약자이다. 아래는 Java IO와 Java NIO의 차이이다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th id=&quot;구분&quot;&gt;구분&lt;/th&gt;
&lt;th id=&quot;io&quot;&gt;IO&lt;/th&gt;
&lt;th id=&quot;nio&quot;&gt;NIO&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;입출력 방식&lt;/td&gt;
&lt;td&gt;Stream&lt;/td&gt;
&lt;td&gt;Channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 단위&lt;/td&gt;
&lt;td&gt;Byte, Character&lt;/td&gt;
&lt;td&gt;Buffer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 흐름&lt;/td&gt;
&lt;td&gt;단방향&lt;/td&gt;
&lt;td&gt;양방향&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;논블로킹 지원&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;가장 큰 차이는 Java IO는 Stream 기반, NIO는 Channel 기반으로 동작한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NIO는 &lt;code&gt;java.nio.*&lt;/code&gt; 패키지에 속한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NIO는 아래와 같이 Buffer를 통해 데이터를 읽거나 써서 파일과 통신한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciCDoL/btsDRbV8PZu/KU7CEGjtd4ajtVkGDNCksK/img.png&quot; alt=&quot;img_7.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Java NIO의 모든 IO는 &lt;code&gt;Channel&lt;/code&gt;로 시작한다. Channel을 통해 버퍼에서 데이터를 읽거나 버퍼에 데이터를 쓴다.&lt;/p&gt;
&lt;p&gt;Buffer는 아래와 같이 다양한 타입을 제공한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ably4/btsDLsEpfCa/vmVmZafz36EXK89qU3biR1/img.png&quot; alt=&quot;img_8.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;다음으로 Buffer의 위치 속성을 살펴보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;capacity: Buffer가 저장할 수 있는 데이터의 최대 크기&lt;/li&gt;
&lt;li&gt;position: Buffer에서 현재 가르키는 위치&lt;/li&gt;
&lt;li&gt;limit: 데이터를 읽거나 쓸 수 있는 마지막 위치  (읽기 모드 시 주로 사용)&lt;/li&gt;
&lt;li&gt;mark: reset() 호출 시 position을 mark로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;해당 속성은 아래의 관계를 가진다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;0 &amp;lt;= mark &amp;lt;= position &amp;lt;= limit &amp;lt;= capacity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음으로 ByteBuffer의 종류에 대해 알아보자.&lt;/p&gt;
&lt;h4 id=&quot;directbytebuffer&quot;&gt;DirectByteBuffer&lt;/h4&gt;
&lt;p&gt;DirectByteBuffer는 Native Memory에 저장되는 ByteBuffer이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Native Memory(off-heap)에 저장&lt;/li&gt;
&lt;li&gt;커널 메모리에서 복사를 하지 않으므로 read/write 속도가 빠르다.&lt;/li&gt;
&lt;li&gt;비용이 많이 드는 System call을 사용하므 allocate, deallocate가 느리다. (Pool로 만들어서 사용할 수 있다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;heapbytebytebuffer&quot;&gt;HeapByteByteBuffer&lt;/h4&gt;
&lt;p&gt;HeapByteBuffer는 JVM Heap에 저장되는 ByteBuffer이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JVM Heap Memory에 저장된다. (byte[] 래핑)&lt;/li&gt;
&lt;li&gt;커널 메모리에서 복사가 일어나므로 read/write 속도가 느리다.&lt;/li&gt;
&lt;li&gt;gc에서 관리되므로 allocate, deallocate가 빠르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래는 예시 코드이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;var file = new File(FileChannelReadExample.class
        .getClassLoader()
        .getResource(&quot;hello.txt&quot;)
        .getFile());

try (var fileChannel = FileChannel.open(file.toPath())) {
    var byteBuffer = ByteBuffer.allocate(1024);
    fileChannel.read(byteBuffer);
    byteBuffer.flip();

    var result = StandardCharsets.UTF_8.decode(byteBuffer);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DirectByteBuffer를 사용하려면 &lt;code&gt;allocate()&lt;/code&gt;대신 &lt;code&gt;ByteBuffer.allocateDirect()&lt;/code&gt;를 사용하면 된다.&lt;/p&gt;
&lt;h3 id=&quot;configureblocking&quot;&gt;configureBlocking&lt;/h3&gt;
&lt;p&gt;아래는 &lt;code&gt;SelectableChannel&lt;/code&gt; 이라는 추상 클래스의 &lt;code&gt;configureBlocking()&lt;/code&gt; 메서드이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmhzzV/btsDQYCxYhD/2WcGXrSy6uKQDYDaKUKtDK/img.png&quot; alt=&quot;img_9.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;해당 메서드의 설명은 &quot;Adjusts this channel's blocking mode.&quot; 직역하면 Blocking 모드를 조정한다.&lt;/p&gt;
&lt;p&gt;아래는 &lt;code&gt;SelectableChannel&lt;/code&gt;을 상속하는 &lt;code&gt;ServerSocketChannel&lt;/code&gt;을 비동기로 사용하는 예제이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;try (var serverChannel = ServerSocketChannel.open()) {
    var address = new InetSocketAddress(&quot;localhost&quot;, 8080);
    serverChannel.bind(address);
    serverChannel.configureBlocking(false);

    var connected = serverChannel.connect(address);
    assert !connected; // 통과
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;반면 &lt;code&gt;FileChannel&lt;/code&gt;의 경우 &lt;code&gt;SelectableChannel&lt;/code&gt;을 상속받지 않는다. 그렇기 때문에 Non-Blocking으로 설정할 수 없다.&lt;/p&gt;
&lt;p&gt;여기서 알 수 있듯 &lt;strong&gt;Java NIO의 모든 IO가 Non-Blocking하게 동작할 수 있지는 않다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;aionio2&quot;&gt;AIO(NIO2)&lt;/h3&gt;
&lt;p&gt;Java AIO(Asynchronous Non-Blocking I/O)에서는 Callback 기반의 Channel을 제공해준다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NIO2 라고도 부른다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSnftA/btsDQ7GdFZA/OKiWpd4dcUUkL7rts8cM40/img.png&quot; alt=&quot;img_10.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;자바 NIO에서 Non-Blocking IO에서 결과를 받으려면 &lt;code&gt;while&lt;/code&gt;문으로 완료 여부를 확인 등의 처리가 필요하다. 자바 AIO에서는 콜백을 잡아서 처리할 수 있다.&lt;/p&gt;
&lt;p&gt;추가로 Non-Blocking 기반의 다양한 클래스를 지원한다. (자바 AIO의 &lt;code&gt;AsynchronousFileChannel&lt;/code&gt;의 경우 Non-Blocking을 지원한다.)&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kouzie.github.io/java/java-NIO&quot;&gt;https://kouzie.github.io/java/java-NIO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://codevang.tistory.com/154&quot;&gt;https://codevang.tistory.com/154&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Language/Java</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/405</guid>
      <comments>https://jaehoney.tistory.com/405#entry405comment</comments>
      <pubDate>Mon, 22 Jan 2024 23:31:33 +0900</pubDate>
    </item>
    <item>
      <title>첫 회사에서의 2년 회고 및 이직</title>
      <link>https://jaehoney.tistory.com/404</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;최근 이직을 하게 되어서 나름의 생각 정리와 궁금해하실 분들을 위해 공유하는 목적으로 글을 쓰게 되었다.&lt;/p&gt;
&lt;h2 id=&quot;2년의-업무를-하며&quot;&gt;2년의 업무를 하며&lt;/h2&gt;
&lt;p&gt;회사 2년 동안 많은 업무를 했다. 너무 다양한 업무를 해서 어디서부터 적어야 할 지 모르겠다.. (정말 많다.)&lt;/p&gt;
&lt;p&gt;그래서 그냥 &lt;strong&gt;가장 잘한 것&lt;/strong&gt;과 &lt;strong&gt;가장 못한 것&lt;/strong&gt;을 기록하기로 했다.&lt;/p&gt;
&lt;h4 id=&quot;가장-잘한-것&quot;&gt;가장 잘한 것&lt;/h4&gt;
&lt;p&gt;업무나 프로젝트와 해결한 문제 중 소개하고 싶은 것들이 많이 있다. 그렇지만 내가 가장 크게 기여한 부분은 &lt;strong&gt;좋은 영향&lt;/strong&gt;이라고 생각한다.&lt;/p&gt;
&lt;p&gt;영향력은 크게 두 가지로 나눌 수 있었다.&lt;/p&gt;
&lt;h5 id=&quot;1-문화적-영향력&quot;&gt;1. 문화적 영향력&lt;/h5&gt;
&lt;p&gt;팀원 분들께서도 내가 기술 블로그를 꾸준히 작성하고, 업무 외 시간에 공부하는 것을 너무 잘 알고 있다.&lt;/p&gt;
&lt;p&gt;출근도 1시간 30분 ~ 2시간 정도 일찍 와서 학습했다. 유연 근무제이지만 2년 동안 9-6 출퇴근만 하며 출근을 8시 이후에 해본적이 거의 없다.&lt;/p&gt;
&lt;p&gt;업무를 하며 해결한 문제나 이슈는 &lt;strong&gt;팀 전체&lt;/strong&gt;에게 잘 정리해서 공유했고, 코드 리뷰는 최대한 깔끔하게 정리해서 의견을 나누었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;조금이라도 복잡한 문제는 반드시 &lt;strong&gt;자세히 정리했다.&lt;/strong&gt; 그래서 소통은 정리한 것을 기반으로 나눌 수 있었다.&lt;ul&gt;
&lt;li&gt;eg. 각 의존 설계(3~4 가지)를 그린 후 장단점을 정리하고 의견을 공유드리거나 제안&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;당연하지만 절대로 강요하지 않았다. 의견을 말씀드릴 때는 &lt;strong&gt;&quot;고민이 있습니다.&quot;&lt;/strong&gt; 라는 치트키로 시작했고 상대방의 의견을 인정하고 내 의견을 말씀드렸다.&lt;ul&gt;
&lt;li&gt;억지로 공감한 것이 아니다. 개발하면서 생기는 95%의 문제는 정답이 없다고 생각한다. 그래서 상대 의견도 충분히 공감이 되었다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;기술적 세미나에서 발표도 진행했었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DB 샤딩 라이브러리 개발 이야기&lt;/li&gt;
&lt;li&gt;신입사원 온보딩 문서 구축 (+ Confluence 문서 개편)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이외에도 좋은 개발 문화를 만드려고 노력을 많이 했었던 것 같다.&lt;/p&gt;
&lt;h5 id=&quot;2-코드적-영향력&quot;&gt;2. 코드적 영향력&lt;/h5&gt;
&lt;p&gt;내가 입사하고 팀에서 자바를 처음으로 사용하게 되었다. 그래서 팀에서 자바 스프링에 대한 충분한 지식이 없었다.&lt;/p&gt;
&lt;p&gt;나는 &lt;strong&gt;업무 이외의 시간에 충분히 학습&lt;/strong&gt;한 것들을 &lt;strong&gt;회사에 하나씩 도입&lt;/strong&gt;할 수 있었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DDD&lt;/li&gt;
&lt;li&gt;ATDD (+ TDD)&lt;/li&gt;
&lt;li&gt;Clean Architecture&lt;/li&gt;
&lt;li&gt;Event Driven&lt;/li&gt;
&lt;li&gt;Auto Configuration&lt;/li&gt;
&lt;li&gt;OOP (+ 객체지향 생활체조)&lt;/li&gt;
&lt;li&gt;QueryDsl (+ Covered Index)&lt;/li&gt;
&lt;li&gt;DLQ&lt;/li&gt;
&lt;li&gt;멀티 모듈&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;공유하려고 &lt;strong&gt;의식했던 것은 아니다&lt;/strong&gt;. 어느 순간 사내 동료들이 &lt;strong&gt;따라서&lt;/strong&gt; 적용을 하고 있었다.&lt;/p&gt;
&lt;p&gt;Clean Architecture의 경우 나는 &lt;strong&gt;&quot;안해도 된다.&quot;&lt;/strong&gt; 라고 설명을 드렸는데, 동료들이 대부분 따라서 적용을 하고 있었다. 물론 &quot;안해도 된다&quot; 뒤에 &quot;그런데 이것도 좋고, 저것도 좋고, 이게 진짜 좋고, …&quot; 이렇게 설명을 드리긴 했었다.. ㅋㅋ&lt;/p&gt;
&lt;p&gt;입사하실 때는 테스트 코드를 잘 모르시던 분도, 내가 퇴사할 때 쯤에는 정말 충실하게 테스트를 짜고 있었다.&lt;br /&gt;
테스트 관련해서 정말 많이 여쭤봐주셨고 대답해 드리는 과정에서 &lt;strong&gt;나도 많이 성장&lt;/strong&gt;할 수 있었다.&lt;/p&gt;
&lt;p&gt;동료 분들의 코드가 매우 깔끔해지고 테스트 코드도 늘어가면서 나 역시 매우 큰 &lt;strong&gt;동기부여&lt;/strong&gt;가 되었다.&lt;/p&gt;
&lt;p&gt;이러한 &lt;strong&gt;문화적, 코드적 영향력&lt;/strong&gt;이 &lt;strong&gt;가장 잘한 부분&lt;/strong&gt;이라고 생각한다.&lt;/p&gt;
&lt;h4 id=&quot;가장-힘들었던-때&quot;&gt;가장 힘들었던 때&lt;/h4&gt;
&lt;p&gt;신입 때는 입사하고 나서 시스템의 전반적인 부분들에 대해 개선하고 싶었다.&lt;/p&gt;
&lt;p&gt;DB에는 불필요한 컬럼이 너무 너무 많았으며, 비합리적인 시스템 구조, 이해하기 어려운 프로세스가 너무 너무 많았다.&lt;/p&gt;
&lt;p&gt;그래서 그런 것들에 대해서 해결하자고 말씀을 많이 드렸다. 문제는 내가 &lt;strong&gt;신뢰자산&lt;/strong&gt;이 쌓이기 전이었다. &lt;strong&gt;나는 기본기도 부족했고 간단한 업무를 수행하기에도 모르는 것이 너무 많은 상태&lt;/strong&gt;였다.&lt;/p&gt;
&lt;p&gt;그 상태에서 &lt;strong&gt;의견 어필&lt;/strong&gt;을 자꾸 하니까 &lt;strong&gt;전부 반려&lt;/strong&gt;를 당했었다. 잘하는게 20이고 못하는 것이 80인데 잘하는 20에 대해서만 자꾸 &lt;strong&gt;'내 말이 맞는데.. 왜 자꾸 못하게 하시지..'&lt;/strong&gt; 생각을 하게 되었다.&lt;/p&gt;
&lt;p&gt;나는 이 문제를 해결하고자 &lt;strong&gt;레거시에 충실&lt;/strong&gt;하자고 결심했다. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;팀원 분들이 어려워 하시는 문제를 도맡아서 해결&lt;/strong&gt;하고, &lt;strong&gt;이해하기 싫었던 기술을 이해&lt;/strong&gt;하고, &lt;strong&gt;이해가 가지 않는 프로세스를 이해하려고 다이어그램과 플로우차트를 그려가며 열심히 정리&lt;/strong&gt;했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;그래서 다양한 문제를 파악할 수 있었고 많은 문제를 해결할 수 있게 되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그 결과, 팀 내에서 &lt;strong&gt;굉장히 많은 업무를 담당&lt;/strong&gt;하게 되었고, 팀장님도 내가 무슨 말을 해도 &lt;strong&gt;매우 긍정적으로 검토&lt;/strong&gt;해주시게 되었다!&lt;/p&gt;
&lt;h2 id=&quot;이직을-결심&quot;&gt;이직을 결심&lt;/h2&gt;
&lt;p&gt;해결하고 싶은 문제가 여전히 너무 많았다. 그러나 &lt;strong&gt;주어진 환경에서는 해결할 수 없었다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;포스팅에서 자세하게 문제들에 대해 언급하거나 자세히 다루기는 어렵다. 그러나 &lt;strong&gt;문화적&lt;/strong&gt;으로 &lt;strong&gt;기술적&lt;/strong&gt;으로 &lt;strong&gt;문제를 근본적으로 해결&lt;/strong&gt;하고 싶었으나 &lt;strong&gt;한계가 분명히 있었다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이슈 대응에 대해서도 B2B 특성 상 논리적 결함이 존재해도 꼭 해결할 필요가 없는 문제도 있었다. 고객 수가 적으니 문제가 생겨도 그냥 수동으로 대응을 하면 되었다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;strong&gt;더 다양한 문제에 도전&lt;/strong&gt;하고 싶고, 고객께 &lt;strong&gt;최고의 사용자 경험&lt;/strong&gt;을 드리고 싶어서 이직을 결심하게 되었다.&lt;/p&gt;
&lt;h4 id=&quot;나름의-기준&quot;&gt;나름의 기준&lt;/h4&gt;
&lt;p&gt;이직을 시도할 때 주로 고려했던 것은 아래와 같다. (너무 많아서 정말 심플하게 정리했다.)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;트래픽이 많이 발생하는 환경&lt;ul&gt;
&lt;li&gt;충분한 TPS를 요구하는 환경&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;훌륭한 동료와 성장 가능한 환경&lt;/li&gt;
&lt;li&gt;기술적인 도전&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;기술적으로 풀고 싶은 문제는 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MSA 잘 다루기&lt;ul&gt;
&lt;li&gt;데이터 쿼리&lt;/li&gt;
&lt;li&gt;신뢰성 보장&lt;/li&gt;
&lt;li&gt;트랜잭션과 정합성&lt;/li&gt;
&lt;li&gt;모니터링&lt;/li&gt;
&lt;li&gt;분리 (성능 및 장애 결합도 제거)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;대량의 트래픽 핸들링 (+ 데이터 배치)&lt;/li&gt;
&lt;li&gt;충분한 캐싱&lt;/li&gt;
&lt;li&gt;외부 시스템 연동&lt;/li&gt;
&lt;li&gt;보안 문제&lt;/li&gt;
&lt;li&gt;자동화&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;정리하면 다니던 회사보다 &lt;strong&gt;문화적으로 더 좋은 환경&lt;/strong&gt;과 &lt;strong&gt;기술적으로 더 많이 도전할 수 있는 환경&lt;/strong&gt;을 원했다.&lt;/p&gt;
&lt;p&gt;목표로 했던 회사가 많지는 않았던 것 같다.&lt;/p&gt;
&lt;h2 id=&quot;결과&quot;&gt;결과&lt;/h2&gt;
&lt;p&gt;1월 8일부터 카카오페이 &lt;strong&gt;금융서비스플랫폼 팀&lt;/strong&gt;으로 가게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o2vYE/btsC5XEkL43/OufajuAkdtZMhOrhQB2kwK/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;'&lt;strong&gt;3년 이상&lt;/strong&gt; 서버 개발 경력을 가지신 분을 찾고 있어요.' 라고 적혀있었지만, &lt;strong&gt;1년 11개월의 경력&lt;/strong&gt;으로 지원했고 &lt;strong&gt;최종 합격&lt;/strong&gt;했다.&lt;/p&gt;
&lt;p&gt;처우 협상의 경우 &lt;strong&gt;얼마로 오퍼를 주시던&lt;/strong&gt; 콜하기로 결심했어서 바로 콜했다. (내가 부족하기도 하고, 지원한 이유의 99%가 직무 때문이라서)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;참고로 다니던 회사도 복지나 처우는 충분히 만족헀다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;새로운 환경에서 경험하고 싶었던 것들을 경험할 수 있다는 생각에 설렌다.&lt;/p&gt;
&lt;p&gt;실제로 2차 면접때 면접관님께서 &lt;strong&gt;'입사하시면 ㅇㅇ님이 경험하시고 싶으셨던 것들은 전부 경험 하실 수 있을 거예요.'&lt;/strong&gt; 라고 얘기해주셨다.&lt;/p&gt;
&lt;p&gt;(1차 면접관 분들도 너무 젠틀하셨고, 내 역량을 최대한 어필할 수 있게 도와주셨다.)&lt;/p&gt;
&lt;h2 id=&quot;이직-팁&quot;&gt;이직 팁&lt;/h2&gt;
&lt;p&gt;팁 같은 것은 안타깝지만(?) 없다.&lt;/p&gt;
&lt;p&gt;이직하시는 개발자 분들을 보면서 이런 것을 느꼈다. &lt;strong&gt;'전부 이유가 있었다.'&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;좋은 문화가 있고 다른 분들이 가고 싶어 하시는 곳을 가시는 분의 Github을 보거나 블로그를 보면 &lt;strong&gt;다 이유를 찾을 수 있었다.&lt;/strong&gt;&lt;br /&gt;
평소에도 공부를 굉장히 열심히 하셨고, 정리하신 자료의 퀄리티를 보면 그 이유를 알 수 있었다. &lt;strong&gt;절대 운으로 되지 않는다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;꾸준히 하나씩 학습하셔서 &lt;strong&gt;1레벨씩 올리시길 추천&lt;/strong&gt;드린다. 그러다 보면 &lt;strong&gt;원하시는 기업에서 1%의 확률로 붙을 수 있는 레벨&lt;/strong&gt;이 될 것이고, &lt;strong&gt;레벨이 올라가면서 확률도 더 높아질 것&lt;/strong&gt;이다. &lt;/p&gt;
&lt;p&gt;내 생각은 그렇다.&lt;/p&gt;
&lt;h2 id=&quot;무수한-감사&quot;&gt;무수한 감사&lt;/h2&gt;
&lt;p&gt;운이 좋게도 다니던 회사에서 &lt;strong&gt;고마운 분들&lt;/strong&gt;이 정말 정말 많다.&lt;/p&gt;
&lt;p&gt;함께 매일같이 대화하고, 리뷰를 주고 받으면서 &lt;strong&gt;성장을 도와주신 매우 훌륭한 동료 분&lt;/strong&gt;도 계시고,&lt;/p&gt;
&lt;p&gt;팀장님께서는 피드백을 많이 주셔서 내가 설명을 드리거나 설득하는 과정에서 &lt;strong&gt;생각도 많이 정리&lt;/strong&gt;할 수 있었다.    &lt;/p&gt;
&lt;p&gt;나를 &lt;strong&gt;인정해 주시고&lt;/strong&gt; 이것저것 모르는 것을 매일같이 여쭤봐 주셔서 설명드리면서 다시 한번 생각을 정리하게 해주신 분도 계신다. (그 분께 &lt;strong&gt;Soft-Skill&lt;/strong&gt;도 정말 많이 배웠다. 성장하시는 모습을 보고 &lt;strong&gt;동기부여&lt;/strong&gt;도 정말 많이 되었다!)&lt;/p&gt;
&lt;p&gt;최근에는 엄청난 실력의 신규 입사자 분들도 들어와서 무척이나 기대가 되었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;리뷰에 대해 &quot;감사합니다. 열심히 하겠습니다!&quot;라고 말씀을 해주시는 등 업무에서 가장 중요한 &lt;strong&gt;기술적 겸손함&lt;/strong&gt;까지 겸비하셨다. 정말 기대가 많이 되었다!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;옆 팀에도 고민을 나누거나 업무 중 만난 문제를 공유하기도 하고 일상 얘기도 많이 하는 등 편하게 회사를 다닐 수 있게 해주신 분들이 정말 많다.&lt;/p&gt;
&lt;p&gt;부족하지만 &lt;strong&gt;좋게 봐주시고 아껴주신 고마운 분들께&lt;/strong&gt; &lt;strong&gt;무수한 감사&lt;/strong&gt; 를 드린다. (언젠가 다른 모습으로 다시 만나길 정말 진심으로 기대한다!)&lt;/p&gt; &lt;/article&gt;</description>
      <category>Etc./개발 일기</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/404</guid>
      <comments>https://jaehoney.tistory.com/404#entry404comment</comments>
      <pubDate>Wed, 3 Jan 2024 15:16:28 +0900</pubDate>
    </item>
    <item>
      <title>Axon Framework로 Orchestration-based Saga 구현하기!</title>
      <link>https://jaehoney.tistory.com/403</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 포스팅은 아래 강의 내용의 예시 코드를 포함하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://fastcampus.co.kr/dev_online_projectmsa&quot;&gt;https://fastcampus.co.kr/dev_online_projectmsa&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 코드를 따라하면서 작성한 부분으로 이해해주시면 감사하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Axon Framework란?&lt;/h2&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/4JfQ8/btsEpm3B2XI/LvFKWQySfok3yDnYNKL2cK/img.png&quot; alt=&quot;img.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axon Framework의 Document의 설명을 보면 첫줄은 아래와 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Axon Framework is a framework for building evolutionary, message-driven microservice systems based on the principles of Domain-Driven Design (DDD), Command-Query Responsibility Separation (CQRS), and Event Sourcing.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직역해보면 Axon Framework는 DDD, CQRS, EDA의 구현을 쉽게하기 위한 Open Source Framework 이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Axon Framework와 Axon Server&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AxonFramework는 기본적으로 Orchestration-based Saga를 구현한다. 즉, 별도의 Orchestrator를 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Choregraphy-based Saga도 구현할 수 있도록 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AxonFramework는 내부적으로 아래의 컴포넌트로 나눌 수 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2QwEY/btsEku944UF/c8muBL9fVWXcmzxG3YUTHk/img.png&quot; alt=&quot;img_1.png&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Axon Framework&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axon Framework는 Event Sourceing 기반으로 데이터를 관리할 수 있는 도구를 제공한다. (CQRS, DDD를 구현할 수 있도록 도와준다.)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Event Sourcing이란 모든 데이터의 변화를 Event로 발행하는 기법이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Axon Server&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axon Server는 아래의 역할을 수행한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Axon Framework를 사용하는 애플리케이션들로부터 발행된 Event를 저장&lt;/li&gt;
&lt;li&gt;각 애플리케이션의 상태를 관리하고 이벤트 큐의 유량을 관리&lt;/li&gt;
&lt;li&gt;고가용성에 집중된 내부 구현이 되어 있고, 사용자 편리성을 위한 Observability를 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axon에 대해 간단히 정리하면 Saga 구현을 위한 환경을 제공해준다고 생각하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Axon Framework 동작 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axon Framework는 EDA(Event Driven Architecture)를 구현하기 위한 다양한 기능을 구현하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axon Framework는 아래의 동작 방식을 가진다. (Axon Framework로 CQRS를 구축하는 예제의 이미지이다.)&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/lfxvG/btsEm8LQkQs/odkr4CkHm0zByVlUgYa9q1/img.png&quot; alt=&quot;img_2.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 컴포넌트의 역할은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Command Bus: Command가 지나갈 수 있는 통로&lt;/li&gt;
&lt;li&gt;Command Handler: Command Bus로부터 받은 Command를 처리&lt;/li&gt;
&lt;li&gt;Aggregate: Command를 할 수 있는 도메인의 단위 (DDD)&lt;/li&gt;
&lt;li&gt;Event Bus: Event가 지나갈 수 있는 통로&lt;/li&gt;
&lt;li&gt;Event Handler: Event Bus로부터 받은 Event를 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axon은 위 컴포넌트들을 기반으로 동작한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;적용 절차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 절차로 Axon Framework에서 Orchestration-based Saga를 적용할 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Axon Server를 구축(Axon Framework's Orchestrator)&lt;/li&gt;
&lt;li&gt;각 서비스에 Axon Framework 의존성 추가&lt;/li&gt;
&lt;li&gt;Event Sourcing 방식을 사용해서 Event Driven Model 적용&lt;/li&gt;
&lt;li&gt;Axon Framework가 지원하는 Saga를 구현&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IntelliJ를 사용 중이라면 Axon Framework 플러그인을 설치하는 것을 권장한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFXXW6/btsEj6hfElw/ooz7t0EYUcaehoKPGEJIxk/img.png&quot; alt=&quot;img_3.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 플러그인은 아래와 같이 Axon Framework의 동작 구조 파악을 위한 시각화 및 링크 기능을 제공한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/b77xtV/btsEquAn9mH/XVQEn0KyV66er0ECEgefWk/img.png&quot; alt=&quot;img_4.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 Axon 기반의 코드 구조에 대한 검사하는 기능도 제공한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 머니 충전 로직을 예시로 보자.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;1. 고객 정보가 정상인지 확인 (멤버)
2. 고객의 연동된 계좌가 있는지, 고객의 연동된 계좌의 잔액이 충분한지도 확인 (뱅킹)
3. 법인 계좌 상태도 정상인지 확인 (뱅킹)
4. 증액을 위한 &quot;기록&quot;. 요청 상태로 MoneyChangingRequest 를 생성한다.
5. 펌뱅킹을 수행하고 (고객의 연동된 계좌 -&amp;gt; 법인 계좌) (뱅킹)
6-1. 결과가 정상적이라면. 성공으로 MoneyChangingRequest 상태값을 변동 후에 리턴
6-2. 실패 시 펌뱅킹 수행 (법인 계좌 -&amp;gt; 고객의 연동된 계좌) 후 MoneyChangingRequest 상태값 변동
6-3. 성공 시 멤버의 MemberMoney 값 증액&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 비즈니스를 Axon Framework를 활용한 Saga 패턴으로 &lt;b&gt;예시 코드&lt;/b&gt;를 구현해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머니 충전 Saga를 포함한 Axon Framework를 위한 구현은 아래 패키지에서 수행했다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/vFqGS/btsEkCNKTvY/9xPoz5KNiaAQKopMKnKGr0/img.png&quot; alt=&quot;img_7.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 부분은 Money Service의 구현이고 BankingService, RemittanceService, Common 에서도 Axon Framework의 Saga를 위한 구현이 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Saga 실행&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axon Framework와 기본 코드 구조의 가장 큰 차이점은 서버가 요청이 들어오면 동기식으로 메서드를 호출하는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service까지는 동기로 호출하지만 Service에서 &lt;b&gt;비즈니스를 수행(머니 조회)&lt;/b&gt;한 후 아래와 같이 AxonServer의 &lt;b&gt;CommandBus에 Command를 보낸다.&lt;/b&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/G5qKf/btsEnZVaW3N/RY4cORBP5pQ4K8KzpxVW3K/img.png&quot; alt=&quot;img_5.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머니 &lt;b&gt;Aggregate&lt;/b&gt;는 해당 Command를 받아서 &lt;b&gt;머니 충전 요청 생성 Event&lt;/b&gt;를 발행한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/pOYQ6/btsElIf3Wte/B94tuDceLR8N3JBqk9GQeK/img.png&quot; alt=&quot;img_8.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Command Handling을 완료하면 최초 서비스 코드를 실행한 쓰레드는 종료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MoneyRechargeSaga&lt;/code&gt;는 해당 Event를 구독하여 트리거되고, &lt;code&gt;@StartSaga&lt;/code&gt;에 의해 Saga가 시작됨을 알린다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsU3AN/btsEon9yEpB/KBODs7TxBtX5ikkGGfEfd0/img.png&quot; alt=&quot;img_9.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@SagaEventHandler(associationProperty = &quot;rechargingRequestId&quot;)&lt;/code&gt;는 구독할 Saga를 정하고 식별자를 지정하는 구문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service에서 Command를 생성할 때 &lt;code&gt;rechargingRequestId&lt;/code&gt;에 유일한 키를 넣었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Saga -&amp;gt; 뱅킹 서비스 (검증 요청)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MoneyRechargeSaga&lt;/code&gt;는 CommandGateway를 통해 &lt;b&gt;뱅킹 계좌를 검증하는 Command&lt;/b&gt;를 보낸다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjqwGj/btsEmsjAvRz/ajLCmKQBvUqZa61nz7uur1/img.png&quot; alt=&quot;img_10.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Command를 핸들링하는 &lt;code&gt;Banking Service&lt;/code&gt;의 &lt;b&gt;계좌 Aggregate&lt;/b&gt;가 검증 비즈니스 로직을 호출한 후 &lt;b&gt;계좌 검증 완료 Event&lt;/b&gt;를 발행한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/7xqtB/btsElkF54XD/sS47KsrB2ymd4kXF2lm3ek/img.png&quot; alt=&quot;img_11.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Event는 다시 &lt;code&gt;MoneyRechargeSaga&lt;/code&gt;가 받는다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs3nYE/btsEkvnDcuw/mOBjnwIvYoZNkKaPQZviQK/img.png&quot; alt=&quot;img_12.png&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Saga -&amp;gt; 뱅킹 서비스 (펌뱅킹 요청)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MoneyRechargeSaga&lt;/code&gt;는 검증이 성공했다면 CommandGateway를 통해 &lt;b&gt;펌뱅킹을 요청하는 Command&lt;/b&gt;를 보낸다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi9ViL/btsEniAImQT/SKFqG0JCKRkUemJ3EmyXDk/img.png&quot; alt=&quot;img_13.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;펌뱅킹 Aggregate&lt;/b&gt;는 해당 비즈니스를 수행한 후 &lt;b&gt;펌뱅킹 완료 Event&lt;/b&gt;를 발행한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/SlhmD/btsEmEEtYyR/eQPKC9rqI69wG5rS2HdMEk/img.png&quot; alt=&quot;img_14.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Event는 다시 &lt;code&gt;MoneyRechargeSaga&lt;/code&gt;가 받는다. (두 번째 파라미터는 로직 수행을 위해 주입받아야 하는 빈을 명시한 것이다.)&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgrBGP/btsEnTOoD93/T5p3fuJUvCJyEaZPSivKrK/img.png&quot; alt=&quot;img_15.png&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;머니 증액 / 보상 트랜잭션 시작&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;펌뱅킹이 완료되면 머니를 증액한다. 증액이 완료되면 Saga를 종료한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 머니 증액이 실패하면 펌뱅킹으로 송금했던 부분을 다시 롤백해야 한다. Saga는 이를 위해 CommandGateway로 &lt;b&gt;펌뱅킹을 롤백하는 Command&lt;/b&gt;를 보낸다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMjJ2W/btsEkvOAojM/WGAeKYvnDK9F77QOJ6w7rK/img.png&quot; alt=&quot;img_16.png&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;펌뱅킹 롤백&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;펌뱅킹 Aggregate&lt;/b&gt;는 해당 펌뱅킹을 롤백하는 비즈니스를 수행한 후 &lt;b&gt;펌뱅킹 롤백 완료 Event&lt;/b&gt;를 발행한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGUPLf/btsEngiBP69/EyLMA7tEnSWspHVRVeiGKK/img.png&quot; alt=&quot;img_17.png&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Saga 종료&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Event는 &lt;code&gt;MoneyRechargeSaga&lt;/code&gt;가 받고 &lt;code&gt;@EndSaga&lt;/code&gt;로 Saga를 종료한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/dP2WP0/btsEon2LTCd/yde1XT5QjqqG0bRAl8ptYK/img.png&quot; alt=&quot;img_18.png&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Orchestration-based Saga&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 Axon으로 Orchestration Saga를 구현할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Choregraphy Saga였다면 &lt;b&gt;비즈니스 로직&lt;/b&gt;에서 &lt;b&gt;이벤트/롤백 이벤트를 발행&lt;/b&gt;해서 비즈니스 로직이 지저분해졌을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 코드에서는 &lt;b&gt;Orchestrator(Saga)가 직접 CommandGateway를 통해 서비스의 메서드를 호출&lt;/b&gt;했다. 즉, Saga(Orchestrator)에서 비즈니스나 롤백 비즈니스를 직접 실행해서 트랜잭션을 집중해서 관리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래의 이점이 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 서비스는 각자의 비즈니스만을 구현하면 되어서 코드가 간단해지고 테스트가 용이해졌다.&lt;/li&gt;
&lt;li&gt;트랜잭션의 상태를 추적하거나 파악하기가 용이해졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Observability&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Saga가 호출하는 각 서비스의 상태는 Axon Server에서 제공하는 UI에서 확인할 수 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/c70CS8/btsEkgYGEUy/62pCCxvsjeDUyX8I6H8nkk/img.png&quot; alt=&quot;img.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 각 Command가 몇 번 수행되었는 지도 확인할 수 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bN7GrM/btsEnaQoMhv/Hmk6ijRUd2BuuYXdQR5Ba1/img.png&quot; alt=&quot;img_1.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발행된 이벤트를 모니터링해서 추적하기도 용이하다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/5qNJh/btsEkhDhub2/dXnKWeSKe3kFEJlzk823q1/img.png&quot; alt=&quot;img_2.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 Axon Server는 Saga 인스턴스 관리나 최근 N분 통계 등 사용자 편의성을 위한 다양한 기능을 제공한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 코드는&amp;nbsp;아래에서 확인할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/violetbeach/payment-service&quot;&gt;https://github.com/violetbeach/payment-service&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.axoniq.io/reference-guide&quot;&gt;https://docs.axoniq.io/reference-guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nexocode.com/blog/posts/smooth-implementation-cqrs-es-with-sping-boot-and-axon&quot;&gt;https://nexocode.com/blog/posts/smooth-implementation-cqrs-es-with-sping-boot-and-axon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@positiveb16/choreography-based-saga-using-axon-framework-438a2d03b9ab&quot;&gt;https://medium.com/@positiveb16/choreography-based-saga-using-axon-framework-438a2d03b9ab&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Server/Spring MSA</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/403</guid>
      <comments>https://jaehoney.tistory.com/403#entry403comment</comments>
      <pubDate>Tue, 26 Dec 2023 22:18:00 +0900</pubDate>
    </item>
    <item>
      <title>Hibernate ORM 공식문서 읽어보기!</title>
      <link>https://jaehoney.tistory.com/401</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;Spring JPA 기반에서 개발하다보면 아래 라이브러리를 사용하게 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Jakarta Persistence API&lt;/li&gt;
&lt;li&gt;Hibernate ORM&lt;/li&gt;
&lt;li&gt;Spring Data JPA&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Jakrta Persistence API&lt;/code&gt;는 &lt;strong&gt;명세&lt;/strong&gt;에 해당한다. 실제 구현하는 기술은 &lt;code&gt;Hibernate ORM&lt;/code&gt;에 있다.&lt;/p&gt;
&lt;p&gt;해당 포스팅에서는 &lt;code&gt;Hibernate ORM&lt;/code&gt;에 대해 공식문서를 읽고 학습 테스트를 진행하면서 &lt;strong&gt;알아두면 좋을 내용&lt;/strong&gt;에 대해 소개한다.&lt;/p&gt;
&lt;h2 id=&quot;hibernate-orm&quot;&gt;Hibernate ORM&lt;/h2&gt;
&lt;p&gt;Hibernate ORM에서 소개하는 목표는 아래와 같다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Hibernate’s design goal is to relieve the developer from 95% of common data persistence-related programming tasks by eliminating the need for manual, hand-crafted data processing using SQL and JDBC.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;직역하면 아래와 같다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Hibernate의 설계 목표는 SQL과 JDBC를 사용하여 수작업으로 데이터를 처리할 필요가 없도록 함으로써 개발자의 데이터 영속성 관련 프로그래밍 작업의 95%를 덜 수 있도록 하는 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Hibernate는 ORM을 사용해서 개발자가 SQL이나 JDBC를 사용하는 부분을 대신 해결해준다. (JPA의 설명과 상통한다.)&lt;/p&gt;
&lt;p&gt;Hibernate ORM은 Stored Procedure를 기반으로 하는 데이터 중심 애플리케이션에는 적합하지 않고, Java의 객체 지향 프로그래밍 모델 및 비즈니스 로직에서 가장 적합하다고 한다.&lt;/p&gt;
&lt;h2 id=&quot;statistics&quot;&gt;Statistics&lt;/h2&gt;
&lt;p&gt;N+1 문제를 해결했다고 가정하자. N+1 문제가 안터지게 되는 것은 테스트 코드로 어떻게 검증할 수 있을까?&lt;/p&gt;
&lt;p&gt;이때 &lt;code&gt;org.hibernate.stat.Statistics&lt;/code&gt;를 활용할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFyDgw/btsCiKk2YiW/k5pZp7rLbbKNkSPAyVKkWK/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SessionFactory&lt;/code&gt; 별로 &lt;code&gt;Statistics&lt;/code&gt; 인스턴스를 가지고 있다. 그래서 나는 아래의 유틸 클래스를 만들었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class QueryCountUtil {
    public static long getEntityLoadCount(EntityManager entityManager) {
        return entityManager.unwrap(org.hibernate.Session.class).getSessionFactory().getStatistics()
            .getEntityLoadCount();
    }

    public static long getSelectQueryCount(EntityManager entityManager) {
        return entityManager.unwrap(org.hibernate.Session.class).getSessionFactory().getStatistics()
            .getQueryExecutionCount();
    }

    public static void clearAllCount(EntityManager entityManager) {
        entityManager.unwrap(org.hibernate.Session.class).getSessionFactory().getStatistics()
            .clear();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Statistics&lt;/code&gt;는 &lt;code&gt;Hibnerate&lt;/code&gt;에서 실행하는 쿼리의 개수나 시간 등을 측정하여 기록한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;공식 문서 참고: &lt;a href=&quot;https://docs.jboss.org/hibernate/orm/6.4/userguide/html_single/Hibernate_User_Guide.html#statistics&quot;&gt;https://docs.jboss.org/hibernate/orm/6.4/userguide/html_single/Hibernate_User_Guide.html#statistics&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 테스트 코드에서는 아래와 같이 작성할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m3ia9/btsCf0IWr3p/g9ZX9xiM1kkUSN5ZmnAMKK/img.png&quot; alt=&quot;img_3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;해당 테스트에서는 &lt;code&gt;Eager loading&lt;/code&gt; 엔터티를 조회할 시 지연 로딩으로 인한 엔터티 로딩을 사용하지 않는다는 것을 검증한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;N+1 문제가 터졌다면 Product 1개당 1번의 지연로딩이 발생할 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TestContext를 재활용하면서 SessionFactory에 카운트가 쌓이는 문제는 아래와 같이 해결할 수 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@BeforeEach
void setup() {
    QueryCountUtil.clearAllCount(em);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;elementcollection&quot;&gt;@ElementCollection&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;@OneToMany&lt;/code&gt;를 사용하는 대신 &lt;code&gt;@ElementCollection&lt;/code&gt;을 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Getter
@Entity
@Table(name = &quot;PERSON_TABLE&quot;)
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ElementCollection
    @CollectionTable(name = &quot;ADDRESS_TABLE&quot;,
            joinColumns = @JoinColumn(name= &quot;person_id&quot;, referencedColumnName = &quot;id&quot;)
    )
    private List&amp;lt;String&amp;gt; addresses = new ArrayList&amp;lt;&amp;gt;();

    public void setAddresses(List&amp;lt;String&amp;gt; addresses) {
        this.addresses = addresses;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@ElementCollection&lt;/code&gt;은 아래의 특징을 가진다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;단순한 값의 컬렉션을 나타낸다. &lt;/li&gt;
&lt;li&gt;컬렉션만 조회, 삭제 등 어떤 행위도 할 수 없고, &lt;strong&gt;반드시 부모를 통해 쿼리가 실행&lt;/strong&gt;된다.&lt;/li&gt;
&lt;li&gt;컬렉션은 엔터티가 아니므로 &lt;strong&gt;ID 생성 전략을 사용할 수 없다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;ID를 가지지 않으므로 컬렉션 값이 변경될 시 &lt;strong&gt;전체 삭제&lt;/strong&gt; 후 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;반면 &lt;code&gt;@OneToMany&lt;/code&gt;의 경우 Many 측이 단순한 컬렉션이 아닌 &lt;strong&gt;자식 엔터티&lt;/strong&gt;로 인정을 받게 된다. &lt;/p&gt;
&lt;p&gt;엔터티로써 인정받을 필요가 없고, 엔터티의 속성일 뿐이라면 &lt;code&gt;@ElementCollection&lt;/code&gt;만 사용하는 것이 &lt;strong&gt;Fit한 처리&lt;/strong&gt;일 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;softdelete&quot;&gt;@SoftDelete&lt;/h2&gt;
&lt;p&gt;기존에는 &lt;code&gt;SoftDelete&lt;/code&gt;를 사용하려면 JPA에서 &lt;code&gt;@Where&lt;/code&gt;와 &lt;code&gt;@SQLDelete&lt;/code&gt;와 같은 애노테이션을 작성해야 했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Entity
@Table(name = &quot;BOARD&quot;)
@NoArgsConstructor
@Where(clause = &quot;is_deleted = false&quot;)
@SQLDelete(sql = &quot;UPDATE board SET is_deleted = true WHERE id = ?&quot;)
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private boolean isDeleted;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hibernate 6.4 부터는 &lt;code&gt;@SoftDelete&lt;/code&gt; 애노테이션을 지원한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Entity
@Table(name = &quot;BOARD&quot;)
@NoArgsConstructor
@SoftDelete(columnName = &quot;is_deleted&quot;)
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만약 반대로 &lt;code&gt;is_active&lt;/code&gt; 컬럼이 있다면 &lt;code&gt;@SoftDelete(columnName = &quot;is_active&quot;, strategy = SoftDeleteType.ACTIVE)&lt;/code&gt;  처럼 &lt;code&gt;strategy&lt;/code&gt; 옵션을 사용하면 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Y&lt;/code&gt;, &lt;code&gt;N&lt;/code&gt; 등 문자열 등의 경우 Converter도 활용할 수 있게 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;@SoftDelete&lt;/code&gt;를 활용하면 코드가 훨씬 깔끔해진다.&lt;/p&gt;
&lt;p&gt;추가로 Hibernate 6.0 부터는 QueryDsl을 활용한 &lt;code&gt;execute()&lt;/code&gt; 시에도 해당 애노테이션이 적용된다.&lt;/p&gt;
&lt;h2 id=&quot;inheritance&quot;&gt;@Inheritance&lt;/h2&gt;
&lt;p&gt;실제 서비스를 운영하다보면 아래의 경우가 자주 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Address라는 테이블이 있다.&lt;/li&gt;
&lt;li&gt;Address에는 type이 PRIVATE, SHARED로 나뉜다.&lt;/li&gt;
&lt;li&gt;각 타입마다 사용하는 컬럼이나 조인 테이블이 다르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 경우 Inheritance를 사용하면 객체 지향적인 설계가 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Getter
@Entity
@Table(name = &quot;ADDRESS_TABLE&quot;)
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn(name = &quot;type&quot;)
public abstract class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    private Long id;
}

@Entity
@DiscriminatorValue(&quot;P&quot;)
public class PrivateAddress extends Address {
}

@Entity
@DiscriminatorValue(&quot;S&quot;)
public class SharedAddress extends Address {
    private String sharedType;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JPA Repository는 아래와 같이 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public interface PrivateAddressRepository extends JpaRepository&amp;lt;PrivateAddress, Long&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러면 데이터 삽입에서 &lt;code&gt;type&lt;/code&gt;이 &lt;code&gt;P&lt;/code&gt;로 삽입되고, 조회 시 &lt;code&gt;type = P&lt;/code&gt; 조건이 들어간다.&lt;/p&gt;
&lt;p&gt;그래서 각 타입의 엔터티가 필요한 필드와 메서드만 가지도록 설계할 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;sqlrestriction&quot;&gt;@SQLRestriction&lt;/h2&gt;
&lt;p&gt;Client가 여러 타입의 Account를 가진다면 Entity를 어떻게 구성할 수 있을까?&lt;/p&gt;
&lt;p&gt;그리고 각 타입의 Account가 다른 필드와 메서드를 가진다면?&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@SQLRestriction&lt;/code&gt;을 사용하면 이를 풀어낼 수 있다.&lt;/p&gt;
&lt;p&gt;아래와 같이 타입별로 다른 필드로 매핑한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Getter
@Entity(name = &quot;CLIENT_TABLE&quot;)
@NoArgsConstructor
public class Client {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @SQLRestriction(&quot;account_type = 'DEBIT'&quot;)
    @OneToMany(mappedBy = &quot;client&quot;)
    private List&amp;lt;Account&amp;gt; debitAccounts = new ArrayList&amp;lt;&amp;gt;();

    @SQLRestriction(&quot;account_type = 'CREDIT'&quot;)
    @OneToMany(mappedBy = &quot;client&quot;)
    private List&amp;lt;Account&amp;gt; creditAccounts = new ArrayList&amp;lt;&amp;gt;();

    public void addAccount(Account account) {
        if(account.getType() == AccountType.CREDIT) {
            creditAccounts.add(account);
        } else {
            debitAccounts.add(account);
        }
        account.setClient(this);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래는 Account 클래스이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Getter
@Entity(name = &quot;Account&quot;)
@SQLRestriction(&quot;active = true&quot;)
@NoArgsConstructor
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne
    private Client client;
    @Column(name = &quot;account_type&quot;)
    @Enumerated(EnumType.STRING)
    private AccountType type;
    private Boolean active = true;

    public Account(AccountType type) {
        this.type = type;
    }

    public void setClient(Client client) {
        this.client = client;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 매핑을 하면 &lt;code&gt;Client&lt;/code&gt;를 조회에서 연관된 &lt;code&gt;Account&lt;/code&gt;를 조회할 때 아래의 조회 쿼리가 타입별로 실행된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;sql language-sql&quot;&gt;select
    ca1_0.client_id,
    ca1_0.id,
    ca1_0.active,
    ca1_0.account_type
from
    account ca1_0
where
    ca1_0.client_id=?
    and ca1_0.active = true
    and ca1_0.account_type = 'CREDIT'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 각각의 결과를 다른 필드에 매핑할 수 있다.&lt;/p&gt;
&lt;p&gt;참고로 &lt;code&gt;@JoinTable&lt;/code&gt;을 사용할 경우 &lt;code&gt;@SQLJoinTableRestriction&lt;/code&gt;을 사용할 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;cascadetype&quot;&gt;CascadeType&lt;/h2&gt;
&lt;p&gt;JPA의 CascadeType은 아래 타입을 지원한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ALL&lt;ul&gt;
&lt;li&gt;모든 엔터티 상태 전환 연산을 전파한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;PERSIST&lt;ul&gt;
&lt;li&gt;영속화를 전파한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;MERGE&lt;ul&gt;
&lt;li&gt;엔터티의 상태를 영속성 컨텍스트에 반영하는 연산을 전파한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;REMOVE&lt;ul&gt;
&lt;li&gt;삭제 연산을 전파한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;REFRESH&lt;ul&gt;
&lt;li&gt;Refresh(엔터티의 데이터를 DB 데이터 기반으로 다시 로드) 연산을 전파한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;DETACH&lt;ul&gt;
&lt;li&gt;비영속화를 전파한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 &lt;code&gt;Cascade.ALL&lt;/code&gt;을 사용하면 아래의 &lt;code&gt;org.hibernate.annotation.CascadeType&lt;/code&gt;도 포함한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SAVE_UPDATE&lt;ul&gt;
&lt;li&gt;SaveOrUpdate(Hibernate에서 지원하는 기능)을 전파한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;REPLICATE&lt;ul&gt;
&lt;li&gt;복제 연산(Slave DB에 데이터를 동기화하는 연산)을 전파한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;LOCK&lt;ul&gt;
&lt;li&gt;엔터티를 영속성 컨텍스트에 연결하는 연산을 전파한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;callback&quot;&gt;Callback&lt;/h2&gt;
&lt;p&gt;JPA는 다양한 Callbacks를 제공한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@PrePersist&lt;ul&gt;
&lt;li&gt;Persist를 수행하기 전에 실행&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;@PreRemove&lt;ul&gt;
&lt;li&gt;Remove를 수행하기 전에 실행&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;@PreUpdate&lt;ul&gt;
&lt;li&gt;DB Update 전에 실행&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;@PostPersist&lt;ul&gt;
&lt;li&gt;Persist를 수행한 후 실행&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;@PostRemove&lt;ul&gt;
&lt;li&gt;Remove를 수행한 후 실행&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;@PostUpdate&lt;ul&gt;
&lt;li&gt;DB Update 후 실행&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;@PostLoad&lt;ul&gt;
&lt;li&gt;엔터티가 영속성 컨텍스트에 로드되거나 Refresh 된 후 실행&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여러 엔터티에 공통으로 적용해야 한다면 아래와 같이 별도의 Listener로 사용하는 것도 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class LastUpdateListener {
    @PreUpdate
    @PrePersist
    public void setLastUpdate( Person p ) {
        p.setLastUpdate( new Date() );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Entity(name = &quot;PERSON_TABLE&quot;)
@EntityListeners( LastUpdateListener.class )
public static class Person {
    @Id
    private Long id;
    private Date lastUpdate;
    public void setLastUpdate(Date lastUpdate) {
        this.lastUpdate = lastUpdate;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Callback을 사용하면 도메인 모델의 변화를 감지해야 하는 작업(CQRS 등)에서 유용하게 사용할 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.jboss.org/hibernate/orm/6.4/userguide/html_single/Hibernate_User_Guide.html&quot;&gt;https://docs.jboss.org/hibernate/orm/6.4/userguide/html_single/Hibernate_User_Guide.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Server/Spring JPA</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/401</guid>
      <comments>https://jaehoney.tistory.com/401#entry401comment</comments>
      <pubDate>Tue, 19 Dec 2023 08:19:02 +0900</pubDate>
    </item>
    <item>
      <title>QueryDsl에서 Index Hint 사용하기!</title>
      <link>https://jaehoney.tistory.com/394</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 5.7을 사용하는 프로젝트의 &lt;b&gt;QueryDSL 동적 쿼리에서 특정 경우에 인덱스를 안타는 문제&lt;/b&gt;가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 중인 서비스에서 &lt;b&gt;커버링인덱스를 탈 수 있는 상황에서는 인덱스를 선택&lt;/b&gt;했지만, &lt;b&gt;인덱스에 없는 컬럼 정렬&lt;/b&gt; 등에서 &lt;b&gt;PK&lt;/b&gt;를 타서 쿼리가 밀리는 현상이 자주 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;Index Hint&lt;/code&gt;를 QueryDsl에서 사용할 수 있도록 조치가 필요했다. 아래는 해당 처리를 위해 길을 떠나면서 얻게된 방법들이다.&lt;/p&gt;
&lt;h2 id=&quot;1-jpasqlquery&quot; data-ke-size=&quot;size26&quot;&gt;1. JPASQLQuery&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SqlQueryFactory&lt;/code&gt;는 &lt;code&gt;JpaQueryFactory&lt;/code&gt;와 다르게 &lt;b&gt;Native Query&lt;/b&gt;를 동적으로 생성해주는 클래스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;querydsl-jpa&lt;/code&gt;는 &lt;code&gt;JPASQLQuery&lt;/code&gt;라는 것을 제공한다. 아래는 해당 클래스의 설명이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPASQLQuery is an SQLQuery implementation that uses JPA Native SQL functionality to execute queries&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;http://querydsl.com/static/querydsl/4.1.3/apidocs/com/querydsl/jpa/sql/JPASQLQuery.html&quot;&gt;http://querydsl.com/static/querydsl/4.1.3/apidocs/com/querydsl/jpa/sql/JPASQLQuery.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직역하면 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;JPASQLQuery는 JPA의 Native SQL 기능을 사용하는 Query이다.&lt;/span&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJooE5/btsCJjvH4kb/ZmohOEJ6ql5ADyzVnB2Xs1/img.png&quot; alt=&quot;img_8.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;JPASQLQuery&lt;/code&gt;의 &lt;code&gt;addJoinFlag()&lt;/code&gt;를 사용하면 &lt;b&gt;가장 최근에 추가한 Join(From) 이후&lt;/b&gt;에 SL을 삽입할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 아래와 같이 &lt;code&gt;Use Index&lt;/code&gt; 구문이 포함되어 잘 실행되었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/Yoq2u/btsCG2nrlF6/wOsLKPSzBocBmZPUQMogt1/img.png&quot; alt=&quot;img_9.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 &lt;code&gt;force index&lt;/code&gt;가 아니라 &lt;code&gt;use index&lt;/code&gt;를 사용한 이유는 &lt;b&gt;강제&lt;/b&gt;보다는 &lt;b&gt;권장&lt;/b&gt; 정도로 충분했기 때문이다.&lt;/p&gt;
&lt;h2 id=&quot;2-mysqlqueryfactory&quot; data-ke-size=&quot;size26&quot;&gt;2. MySQLQueryFactory&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 &lt;code&gt;querydsl-sql&lt;/code&gt;이라는 라이브러리를 사용하는 방법이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;implementation &quot;com.querydsl:querydsl-sql:{version}&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 &lt;code&gt;MySQLQueryFactory&lt;/code&gt; 클래스에 대한 설명이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL specific implementation of SQLQueryFactory&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직역하면 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;MySQL 명세의 SqlQueryFactory 구현이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MySQLQueryFactory&lt;/code&gt;를 사용하면 &lt;code&gt;MySQL&lt;/code&gt;에서 지원하는 문법들을 메서드로 제공한다. 역시 SqlQueryFactory를 상속하므로 Native SQL을 사용한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpjDrr/btsCF7h7dBQ/DPwqA4paQo3f7CJlA40vZ0/img.png&quot; alt=&quot;img_11.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 실행해보자.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bChMIi/btsCMrs4hZ8/4Mm1fsU31Emb04KyKWzEmk/img.png&quot; alt=&quot;img_10.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 아래와 같이 인덱스 힌트가 정상적으로 들어갔다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/4z0wv/btsCSDsbuYp/IkxravxgVxK1M5YJ6raZwk/img.png&quot; alt=&quot;img_24.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SQLQueryFactory&lt;/code&gt;는 Class 기반으로 &lt;b&gt;Native Query&lt;/b&gt;를 생성하기 때문에 &lt;b&gt;DB 컬럼과 필드명이 정확히 일치&lt;/b&gt;한 새로운 엔터티 모델을 생성해야 한다.&lt;/p&gt;
&lt;h2 id=&quot;3-statementinspector-사용&quot; data-ke-size=&quot;size26&quot;&gt;3. StatementInspector 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번과 2번은 다소 불편하고 &lt;code&gt;JPQLQuery&lt;/code&gt;을 사용하지 않으므로 &lt;code&gt;QueryDsl&lt;/code&gt; 클래스와 같은 유틸 클래스를 활용하기 어렵고, 엔터티의 스펙을 테이블과 동일하게 설정해야 한다. 따라서 SQL Query를 사용하기 위한 엔터티를 별도로 구성하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 기존의 &lt;code&gt;JpaQueryFactory&lt;/code&gt; 방식을 유지하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;StatementInspector&lt;/code&gt;는 Hibernate에서 실행하는 쿼리의 일부를 대체하거나 전체를 교체할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 SQL 문에서 &lt;code&gt;{tablename} {alias}&lt;/code&gt;을 찾아서 뒤에 인덱스 힌트를 추가한 문자열로 Replacing 할 수 있다. (테이블 명이 포함되지 않은 SELECT 절이나 WHERE 절은 그대로 유지된다.)&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/eoytL3/btsCQsq8xQ7/wtEaCTO8vw92qdeWmaPX7K/img.png&quot; alt=&quot;img_12.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에서는 &lt;code&gt;HibernatePropertiesCustomizer&lt;/code&gt;를 사용해서 &lt;code&gt;HibernateProperties&lt;/code&gt;에 &lt;code&gt;StatementInspector&lt;/code&gt;를 등록한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3rJKK/btsCG46ExUi/fKi8a08UCTX9hksYOsYKM1/img.png&quot; alt=&quot;img_13.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 아래와 같이 인덱스 힌트를 사용할 수 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpqpXT/btsCP4cNFUR/2x9svNh2FMFrq0Dg56Vvk1/img.png&quot; alt=&quot;img_14.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로도 잘 적용이 되는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만 &lt;b&gt;해결해야 될 문제&lt;/b&gt;가 있다.&lt;/p&gt;
&lt;h4 id=&quot;모든-쿼리에서-적용되는-문제&quot; data-ke-size=&quot;size20&quot;&gt;모든 쿼리에서 적용되는 문제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Member&lt;/code&gt; 엔터티를 사용하는 단건 조회에서도 해당 &lt;code&gt;StatementInspector&lt;/code&gt;가 특정 인덱스를 유도하면 안될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;QueryDsl&lt;/code&gt;의 특정 로직에서만 &lt;b&gt;Alias&lt;/b&gt;를 걸 수 있는 방법을 찾아봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 &lt;code&gt;new QMember(Member.class, &quot;member_filtering&quot;)&lt;/code&gt;로 생성한 &lt;b&gt;alias&lt;/b&gt;의 경우 &lt;code&gt;hql&lt;/code&gt;에는 반영이 되었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBsT9I/btsCJhYXxQS/Y7KithvcHTQSKKlyOlPc30/img.png&quot; alt=&quot;img_15.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만 실제 &lt;code&gt;sql&lt;/code&gt;에는 &lt;code&gt;alias&lt;/code&gt;가 반영되지 않았다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVhkLs/btsCOzD2Epa/lRGtPRVK8k5YeovFokE0P1/img.png&quot; alt=&quot;img_16.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;alias&lt;/code&gt;를 설정하는 로직은 &lt;code&gt;Hibernate-ORM&lt;/code&gt;과 같은 영속성 라이브러리 및 &lt;b&gt;버전&lt;/b&gt;에 따라 다르다. 사용하는 버전에 맞게 &lt;b&gt;어떻게든&lt;/b&gt; &lt;code&gt;StatementInspector&lt;/code&gt;와 &lt;code&gt;Entity&lt;/code&gt; 클래스를 맞춰야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;StatementInspector&lt;/code&gt;가 &lt;code&gt;Hibernate-ORM&lt;/code&gt;에 논리적으로 강하게 의존해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 1번과 2번은 사용하기가 어려운 상황이라서 이 방법을 사용하기로 했다.&lt;/p&gt;
&lt;h3 id=&quot;hibernate-orm-56-에서는&quot; data-ke-size=&quot;size23&quot;&gt;Hibernate ORM 5.6 에서는&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate ORM 5.6 에서는 &lt;code&gt;FromElementFactory&lt;/code&gt;에서 &lt;code&gt;tableAlias&lt;/code&gt;를 세팅한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/9a5WN/btsCSc9fxVY/ff5xefU65ktZrTOAZKkXlK/img.png&quot; alt=&quot;img_18.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 &lt;code&gt;FromElement&lt;/code&gt;를 생성할 때 &lt;code&gt;tableAlias&lt;/code&gt;를 null로 그냥 넣어버린다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/vhQvw/btsCKtkB5am/m32loHqXDzmWRv82RnWn00/img.png&quot; alt=&quot;img_19.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;tableAlias&lt;/code&gt;가 없을 때 타는 로직인 &lt;code&gt;AliasGenerator&lt;/code&gt;에 의존적으로 구현하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AliasGenerator&lt;/code&gt;는 다음의 룰을 따른다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Entity&lt;/code&gt; 클래스명을 소문자로 변경&lt;/li&gt;
&lt;li&gt;앞에서 10자를 자른다.&lt;/li&gt;
&lt;li&gt;끝에 &lt;code&gt;{count}_&lt;/code&gt;를 붙인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;AliasGenerator&lt;/code&gt;는 Entity명에 종속적이므로 &lt;b&gt;Filtering을 위한 Entity 클래스&lt;/b&gt;를 한 개 더 만든다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/qgCEe/btsCK9zp3QW/hYDvK7kgcxrQOTwzXO4KQ0/img.png&quot; alt=&quot;img_21.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;code&gt;AliasGenerator&lt;/code&gt;에 의존해서 &lt;code&gt;StatementInspector&lt;/code&gt;를 구현한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/7mTEl/btsCMyll6V5/iLQwJAqABsuaGhBHhLnknk/img.png&quot; alt=&quot;img_20.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 해당 Index를 타야하는 순간에만 해당 Entity를 사용하면 된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/d3thYW/btsCQqNCKlg/A1f9QhnC42wDwOmJm9fzP0/img.png&quot; alt=&quot;img_22.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 아래와 같이 인덱스를 잘 탈 수 있게 되었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cioUz8/btsCOG4jeNA/IYMPPCm8Xw7KnrGXO0WTdK/img.png&quot; alt=&quot;img_23.png&quot; /&gt;
&lt;h2 id=&quot;4-comment--statementinspector&quot; data-ke-size=&quot;size26&quot;&gt;4. Comment + StatementInspector&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate의 주석과 StatementInspector를 활용한 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 주석을 사용하려면 아래 프로퍼티를 설정해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;spring.jpa.properties.hibernate.use_sql_comments=true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;4-1-use-index-정규식-inspect&quot; data-ke-size=&quot;size20&quot;&gt;4-1. USE INDEX 정규식 Inspect&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 주석으로 IndexHint 구문을 삽입한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/sLtGS/btsCQ5oZRc7/pIdG8DFCH51lKUuApDn0o1/img.png&quot; alt=&quot;img_25.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Inspector를 구현해서 주석의 IndexHint 구문을 Where 절 앞으로 옮기면 된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckTUeZ/btsCQ5oZRdr/Hx0d3uDmUc4r6vAZWcTWm0/img.png&quot; alt=&quot;img_26.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 아래와 같이 IndexHint가 잘 적용된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/UWneg/btsCRhv8sDO/qhKuMSihmUDnPgbF5z7fIk/img.png&quot; alt=&quot;img_27.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문법적으로 잘못된 경우 Replacing이 동작하지 않으므로 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 정규식을 파악하기 어렵고, 구문 사용에 제약이 있다는 단점이 있다.&lt;/p&gt;
&lt;h4 id=&quot;4-2-start-end-정규식-inspect&quot; data-ke-size=&quot;size20&quot;&gt;4-2. START, END 정규식 Inspect&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 방법은 START 구문과 END 구문을 만든 후 가운데를 Grouping 하는 것이다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/syOq8/btsCP44W9aH/KhUOi401THXhXgTZIe3830/img.png&quot; alt=&quot;img_29.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;queryFactory에서는 Inspector의 &lt;code&gt;getInspectorIndexHint()&lt;/code&gt;를 사용해서 주석을 삽입하면 된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/T1Z1x/btsCK7hiCgo/bOKbjdDIJVRJTtXdTSC1Q0/img.png&quot; alt=&quot;img_28.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법도 IndexHint가 잘 적용된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/pE5hQ/btsCIGEvhHS/NsAIdI2LfgjD5jYbWouXok/img.png&quot; alt=&quot;img_30.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;USE INDEX&lt;/code&gt;를 정규식으로 잡는 방법과 다르게 정규식을 파악하기 쉽고, 구문 사용도 자유롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, 잘못된 구문이 들어갈 경우 그대로 실행된다는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 두 방법 모두 &lt;b&gt;전체 쿼리에 영향이 생긴다는 단점&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;spring.jpa.properties.hibernate.use_sql_comments=true&lt;/code&gt;로 인해 모든 쿼리에 주석 생기는 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JOIN 확장이 어려운 문제&lt;/b&gt;도 있다. (더 자세히 정규식으로 잡으면 From 절의 인덱스만 처리할 수 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 상황에 맞게 사용하길 권장한다.&lt;/p&gt;
&lt;h2 id=&quot;정리&quot; data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 문제를 해결하려고 &lt;code&gt;QueryDSL&lt;/code&gt;에서 &lt;code&gt;MySQL&lt;/code&gt;의 &lt;code&gt;Index Hint&lt;/code&gt;를 줄 수 있는 방법을 알아봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;QueryDSL&lt;/code&gt;이 해당 부분에 대한 지원이 커져서 쉽게 반영할 수 있게 되었으면 좋겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 좋은 것은 이렇게 IndexHint를 먹이는 상황이 안나오는거겠지만, 피치 못할 사정이 있을 때도 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조건과 정렬이 너무 다양한 경우&lt;/li&gt;
&lt;li&gt;그로 인해 Optimizer가 잘못된 선택을 하는 경우&lt;/li&gt;
&lt;li&gt;Index 확장이 어려운 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;어쩔수 없는 상황&lt;/b&gt;에 여기서 고민했던 방법들을 선택지에 놓고 &lt;b&gt;상황에 맞게 선택&lt;/b&gt;하면 좋을 것 같다.&lt;/p&gt;
&lt;/article&gt;</description>
      <category>Server/Spring JPA</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/394</guid>
      <comments>https://jaehoney.tistory.com/394#entry394comment</comments>
      <pubDate>Fri, 8 Dec 2023 10:39:24 +0900</pubDate>
    </item>
    <item>
      <title>Spring 공식 문서 정독 후 몰랐던 것 정리!</title>
      <link>https://jaehoney.tistory.com/393</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;최근에 Spring 기술을 정확하게 모른다는 생각이 들었다.&lt;/p&gt;
&lt;p&gt;그래서 Spring에 대해 더 자세히 알고 사용하고 싶고 &lt;strong&gt;더 깊은 레벨로 문제를 해결&lt;/strong&gt;하고 싶어서 공식 문서를 정독하기로 했다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/erwV6P/btsBh3gjCse/zvox4tdTYKNByIuzIpR3jK/img.png&quot; alt=&quot;img_1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;아래는 Spring 공식 문서를 정독하여 얻은 지식을 기록한 것이다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;공유&lt;/strong&gt;보다는 &lt;strong&gt;기록&lt;/strong&gt;이 목적이라 가독성이 매우 좋지 않다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;새롭게-알게-되었거나-리마인드할-필요가-있는-것-정리&quot;&gt;새롭게 알게 되었거나 리마인드할 필요가 있는 것 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;빈은 정적 팩토리 메서드로도 생성할 수 있다.&lt;/li&gt;
&lt;li&gt;빈은 2개 이상의 이름을 가질 수 있다. &lt;code&gt;@Bean&lt;/code&gt; 애노테이션을 사용할 경우 &lt;code&gt;name&lt;/code&gt; 옵션을 &lt;code&gt;,&lt;/code&gt;로 연결하면 된다.&lt;/li&gt;
&lt;li&gt;지연 초기화를 사용하면 특수한 경우 성능을 Safe 할 수 있다. (DI도 지연이 가능하다.)&lt;/li&gt;
&lt;li&gt;final class는 프록시를 생성할 수 없다. (final method가 있어서도 안된다.)&lt;/li&gt;
&lt;li&gt;Lookup Method Injection을 사용하면 싱글톤 Bean에서 프로토타입 범위의 Bean을 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;RequestScope Bean을 사용하면 Spring MVC에서 ThreadLocal을 대체할 수 있다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SpringContext.getBean(T)&lt;/code&gt;에서 리플렉션을 사용하므로 훨씬 느리다. &lt;/li&gt;
&lt;li&gt;Context Switching 비용은 줄일 수 있다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;AOP에서 빈을 프록시로 래핑하는 것은 BeanPostProcessor이다.&lt;/li&gt;
&lt;li&gt;BeanPostPrecssor는 &lt;code&gt;InitializingBean.afterPropertiesSet()&lt;/code&gt; 같은 컨테이너 초기화 메서드가 실행되기 전후에 콜백을 받게 된다.&lt;/li&gt;
&lt;li&gt;스프링은 &lt;code&gt;JSR-250&lt;/code&gt; 기반의 &lt;code&gt;@Resource&lt;/code&gt; 등을 지원한다. (&lt;code&gt;@PostConstruct&lt;/code&gt;, &lt;code&gt;@PreDestroy&lt;/code&gt;)도 JSR-250 이다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Autowired&lt;/code&gt;는 필드 타입을 기준으로, &lt;code&gt;@Resource&lt;/code&gt;는 필드 이름을 기준으로 빈을 찾는다.&lt;/li&gt;
&lt;li&gt;생성자 Injection도 필드 타입을 기준으로 실행된다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;스프링은 &lt;code&gt;JSR-330&lt;/code&gt; 기반의 &lt;code&gt;@Inject&lt;/code&gt;, &lt;code&gt;@Named&lt;/code&gt;, &lt;code&gt;@Singleton&lt;/code&gt; 등을 지원한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Qualifier&lt;/code&gt;는 빈의 이름과 다르다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Qualifier&lt;/code&gt;를 상속하면 카테고리 문제를 푸는 데 도움이 된다. &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/autowired-qualifiers.html&quot;&gt;Link&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Qualifier&lt;/code&gt;는 &lt;code&gt;@Primary&lt;/code&gt;보다 우선권이 높다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Controller&lt;/code&gt;, &lt;code&gt;@Repository&lt;/code&gt;, &lt;code&gt;@Service&lt;/code&gt; 등의 애노테이션은 &lt;code&gt;@Component&lt;/code&gt; 애노테이션 이외에도 예외 자동 변환 등의 기능을 제공한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Lazy&lt;/code&gt;는 제한적이기 때문에 &lt;code&gt;ObjectProvider&amp;lt;Bean&amp;gt;&lt;/code&gt; 방식을 권장한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Bean&lt;/code&gt; 애노테이션은 &lt;code&gt;@Component&lt;/code&gt; 애노테이션과 다르게 &lt;code&gt;CGLIB&lt;/code&gt; 프록시를 생성한다.&lt;ul&gt;
&lt;li&gt;그래서 &lt;code&gt;private&lt;/code&gt; 또는 &lt;code&gt;final&lt;/code&gt; 메서드를 사용할 수 없다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Component&lt;/code&gt; 애노테이션 표준 자바 시멘틱을 가진다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lite mode&lt;/code&gt;란 CGLib 방식이 아닌 방식을 말한다. 스프링 빈의 싱글톤을 보장하지 않는다. &lt;a href=&quot;https://hyojabal.tistory.com/25&quot;&gt;Link&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AnnotationConfigApplicationContext&lt;/code&gt; 구현으로 &lt;code&gt;@Bean&lt;/code&gt;, &lt;code&gt;@Compoennt&lt;/code&gt;, &lt;code&gt;@Configuration&lt;/code&gt;, &lt;code&gt;JSR-330&lt;/code&gt; 등의 애노테이션을 인식한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Configuration&lt;/code&gt; 애노테이션도 &lt;code&gt;@Component&lt;/code&gt;를 포함한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Profile&lt;/code&gt;, &lt;code&gt;@ActiveProfiles&lt;/code&gt; 애노테이션에서는 &lt;code&gt;!&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;|&lt;/code&gt; 등을 활용한 복잡한 표현식을 지원한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MessageCodesResolver&lt;/code&gt;를  구현하면 단계적인 검증 에러 메시지를 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BeanWrapper&lt;/code&gt;를 사용하면 빈을 조작할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TypeDescriptor&lt;/code&gt;를 활용하면 제너릭을 추출할 수 있다. &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/validation/convert.html&quot;&gt;Link&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Formatter&lt;/code&gt;를 사용하면 Date 등과 String 사이를 변환하는 것을 전역적으로 적용할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SpEL&lt;/code&gt;은 수식 및 메서드 등 생각하는 대부분의 표현이 전부 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EvaluationContext&lt;/code&gt;를 사용하면 &lt;code&gt;Context&lt;/code&gt;에 변수를 생하는 등 표현식을 더 폭넓게 사용할 수 있다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;student?.name&lt;/code&gt;과 같은 훨씬 다양한 처리가 가능하다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Spring AOP는 순수 java로 구현된다.&lt;/li&gt;
&lt;li&gt;Spring은 &lt;code&gt;AspectJ&lt;/code&gt;를 비롯한 대부분의 프레임워크가 비즈니스 및 도메인 모델에 침해하지 않도록 구현되었다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Spring AOP&lt;/code&gt;는 여러 개를 사용하고 우선 순위를 지정할 수 있다.&lt;/li&gt;
&lt;li&gt;Spring은 Proxy 매커니즘에서 자체 호출 문제에 대한 방향을 제시하고 있다. &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/aop/proxying.html&quot;&gt;Link&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Spring에서 제공하는 &lt;code&gt;Null-Safety Annotation&lt;/code&gt;을 사용하면 외부의 도움을 받아 NPE를 방지할 수 있다.&lt;/li&gt;
&lt;li&gt;Spring은 Java에서 NIO를 위해 지원하는 &lt;code&gt;ByteBuffer&lt;/code&gt; 외에도 버퍼 재사용 및 성능에 도움되는 &lt;code&gt;DataBuffer&lt;/code&gt;를 제공한다.&lt;/li&gt;
&lt;li&gt;Spring은 &lt;code&gt;AOT&lt;/code&gt; 최적화를 지원한다.&lt;ul&gt;
&lt;li&gt;정확한 빈 타입 노출 등을 통해 &lt;code&gt;AOT&lt;/code&gt;를 잘 제공할 수 있는 예시를 제공한다. (&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/aot.html#aot.bestpractices&quot;&gt;Best Practices&lt;/a&gt;를 제공한다.)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;META-INF&lt;/code&gt;는 &lt;code&gt;spring.factories&lt;/code&gt; 이외에도 &lt;code&gt;spring.handlers&lt;/code&gt;, &lt;code&gt;spring.schemas&lt;/code&gt; 등을 사용해서 문제를 풀거나 최적화할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SpringBoot&lt;/code&gt; 3.0부터는 &lt;code&gt;spring.factories&lt;/code&gt;와 &lt;code&gt;..AutoConfiguration.imports&lt;/code&gt;를 혼용할 수 없다.&lt;/li&gt;
&lt;li&gt;Spring 팀도 공식적으로 TDD를 지지한다. (IoC를 제대로 사용하면 단위 테스트와 통합 테스트가 용이하다고 한다.)&lt;ul&gt;
&lt;li&gt;&lt;code&gt;org.springframework.mock&lt;/code&gt;, &lt;code&gt;org.springframework.test&lt;/code&gt; 패키지에서 매우 다양하면서 &lt;strong&gt;충분한 지원&lt;/strong&gt;을 하고 있다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Spring은 단위 테스트 뿐만 아니라 End-to-End 통합 테스트의 필요성도 지지한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TestExecutionListener&lt;/code&gt;를 사용하면 테스트를 격리 시키기 용이하다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TestContext&lt;/code&gt;에 대해서도 매우 자세히 다루고 있다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TestContext&lt;/code&gt;는 static 변수에 저장된다. 최대 크기는 32이고 LRU를 사용한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MockRestServiceServer&lt;/code&gt;를 사용해서 특정 endpoint에 대한 API를 Mocking할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HtmlUnit&lt;/code&gt;을 사용하면 js문법으로 뷰를 검증할 수 있다.&lt;/li&gt;
&lt;li&gt;테스트의 트랜잭션은 &lt;code&gt;TransactionalTestExecutionListener&lt;/code&gt;에 의해 롤백된다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ThreadLocal&lt;/code&gt;에 현재 트랜잭션 상태를 관리한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;JPAEntity를 안전하게 테스트하려면 명시적인 &lt;code&gt;flush()&lt;/code&gt;를 호출해야 한다.&lt;/li&gt;
&lt;li&gt;Spring은 &lt;code&gt;WebFlux&lt;/code&gt;도 그렇고 &lt;code&gt;Mono&lt;/code&gt;와 &lt;code&gt;Flux&lt;/code&gt; 같은 Reactor는 메이저하게 다루고 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Composed Annotations&lt;/code&gt;를 사용해서 트랜잭션 매니저별로 다른 애노테이션을 적용하면 유용할 수 있다.&lt;/li&gt;
&lt;li&gt;트랜잭션의 label을 사용하면 공통적인 처리를 할 수 있다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Transactional(label = &quot;retryable&quot;)&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Spring R2DBC&lt;/code&gt; 종류도 이미 &lt;code&gt;JDBC&lt;/code&gt;와 함께 메이저한 스택으로 작성되어 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Transactional&lt;/code&gt;의 &lt;code&gt;isolation&lt;/code&gt; 중 &lt;code&gt;REQUIRES_NEW&lt;/code&gt;의 CP 데드락 이슈는 공식문서에서도 다루고 있다.&lt;/li&gt;
&lt;li&gt;Spring Jdbc는 &lt;code&gt;Stored Procedure&lt;/code&gt;를 위한 &lt;code&gt;SimpleJdbcCall&lt;/code&gt; 클래스를 지원한다.&lt;/li&gt;
&lt;li&gt;Context의 Layer를 설정하는 것도 공식적으로 다루고 있다. &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet/context-hierarchy.html&quot;&gt;Link&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;WebSocket 방식도 공식적으로 지원한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reactor Netty&lt;/code&gt;, &lt;code&gt;Undertow&lt;/code&gt;, &lt;code&gt;Tomcat&lt;/code&gt;, &lt;code&gt;Jetty&lt;/code&gt;를 &lt;strong&gt;ServletContainer&lt;/strong&gt;라고 명칭한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@CrossOrigin&lt;/code&gt; 애노테이션으로 특정 Controller 혹은 Controller 메서드에만 모든 origins, headers, http methods를 허용하는 등의 처리를 할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Spring-Kafka&lt;/code&gt;, &lt;code&gt;ActiveMQ&lt;/code&gt;등은 JMS의 표준을 따른다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Spring&lt;/code&gt;은 캐시 추상화를 제공한다. 특정 &lt;code&gt;CacheManager&lt;/code&gt;을 지정해서 캐싱 방식을 부여할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;strategy-패턴과-template-method-패턴&quot;&gt;Strategy 패턴과 Template Method 패턴&lt;/h2&gt;
&lt;p&gt;공식 레퍼런스를 보면서 스프링 프레임워크가 지금까지도 &lt;strong&gt;잘 관리되는 비결&lt;/strong&gt;에 대해 &lt;strong&gt;나름대로 정리&lt;/strong&gt;할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;스프링 프레임워크&lt;/strong&gt;가 지속 가능한 이유 중 가장 큰 이유로 &lt;code&gt;Strategy&lt;/code&gt; 패턴과 &lt;code&gt;Template Method&lt;/code&gt; 패턴이라고 생각하게 되었다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Spring에서는 훨씬 더 다양한 디자인 패턴이 존재한다.&lt;/p&gt;
  &lt;p&gt;Strategy, Adapter, Decorator, Proxy, Composite, Facade, Observer, Singleton, …&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;1-strategy-패턴&quot;&gt;1. Strategy 패턴&lt;/h4&gt;
&lt;p&gt;스프링은 수많은 개념을 &lt;strong&gt;추상화&lt;/strong&gt;하고 인터페이스로 사용한다. 인터페이스는 추상적인 개념만 담고 있다.&lt;/p&gt;
&lt;p&gt;구현체는 &lt;strong&gt;Interface&lt;/strong&gt;를 가지고 있고, 그것에 대한 구현을 다시 &lt;strong&gt;전략 패턴&lt;/strong&gt;으로 주입을 받아서 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfPUmD/btsBims8mzp/kkmFGVhHnTrv66gPwMC540/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;스프링은 &lt;code&gt;Servlet&lt;/code&gt; 1개의 작업을 처리하기 위해 연계된 작업들도 대부분 &lt;strong&gt;Interface&lt;/strong&gt;로 추상화를 하고있다.&lt;br /&gt;
&lt;code&gt;Servlet&lt;/code&gt;을 구현한 &lt;code&gt;DispatcherServlet&lt;/code&gt;을 만들었다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DispatcherServlet&lt;/code&gt;은 다시 전략패턴을 사용해서 아래의 &lt;strong&gt;추상화된 개념에만 의존&lt;/strong&gt;한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HandlerMapping&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HandlerAdapter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ViewResolver&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ThemeResolver&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LocaleResolver&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ThemeResolver&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;해당 &lt;strong&gt;인터페이스의 구현체들은 다시 추상화된 개념(Interface)에만 의존&lt;/strong&gt;한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ErrorResponse&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HttpRequestHandler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExceptionHandler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MultipartResolver&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RequestCondition&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MultipartFile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HttpResource&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;전략 패턴의 장점은 &lt;strong&gt;기존 구조가 되는 코드를 변경하지 않고&lt;/strong&gt;, &lt;strong&gt;무수한 기능의 확장&lt;/strong&gt;과 &lt;strong&gt;기술의 발전&lt;/strong&gt;이 가능하다.&lt;/p&gt;
&lt;p&gt;이러한 설계는 SOLID 원칙 중 다음의 원칙을 만족시킨다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SRP (단일 책임 원칙)&lt;/li&gt;
&lt;li&gt;OCP (개방 폐쇄 원칙)&lt;/li&gt;
&lt;li&gt;DIP (의존 역전 원칙)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-template-method-패턴&quot;&gt;2. Template method 패턴&lt;/h4&gt;
&lt;p&gt;아래는 Template method의 구조이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yriul/btsBiU36kYV/jSs0NFO7k4t7LHKNonoC2k/img.png&quot; alt=&quot;img_2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;스프링은 수많은 개념을 추상화하고 인터페이스로 사용한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BeanFactory&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BeanPostProcessor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CacheManager&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JdbcTemplate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JmsOperations&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Controller&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Expression&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Message&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 각 라이브러리나 사용자가 해당 메서드를 구현하면서 조립할 수 있게 한다.&lt;/p&gt;
&lt;p&gt;즉, 구현 클래스들은 &lt;strong&gt;추상적인 개념에만 의존&lt;/strong&gt;하기 때문에 얼마든지 &lt;strong&gt;교체나 확장이 가능하다&lt;/strong&gt;. 낡은 클래스는 보수하거나 새로운 클래스로 교체하면 된다.&lt;/p&gt;
&lt;p&gt;만약 인터페이스에서 &lt;strong&gt;특정 부분의 구현만 확장&lt;/strong&gt;하고 싶다면 &lt;strong&gt;추상 골격 클래스&lt;/strong&gt;를 사용한다.&lt;/p&gt;
&lt;p&gt;Spring에서는 아래의 추상 골격 클래스들을 사용한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AbstractApplicationContext&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AbstractResourceReolsver&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AbstractRoutingDataSource&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AbstractCacheManager&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AbstractEncoder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AbstractController&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AbstractCacheResolver&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AbstractSqlParameterSource&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;해당 클래스들 역시 &lt;strong&gt;추상적인 개념&lt;/strong&gt;인 인터페이스를 구현하고 있다. 즉, 템플릿 메서드 패턴은 &lt;strong&gt;특정한 부분에 대한 골격을 제공&lt;/strong&gt;한다.&lt;/p&gt;
&lt;p&gt;스프링은 &lt;strong&gt;전략 패턴과 템플릿 메서드 패턴의 활용&lt;/strong&gt;으로 &lt;strong&gt;지속 가능한 소프트웨어&lt;/strong&gt;로 발전시킬 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;'추상화가 무조건 좋다'가 아니다.&lt;/strong&gt; &lt;strong&gt;올바른 추상화&lt;/strong&gt;는 &lt;strong&gt;지속 가능한 소프트웨어&lt;/strong&gt;에 도움이 된다는 것이다. (Interface로 추출했어도 &lt;strong&gt;논리적&lt;/strong&gt;으로 구현에 의존하고 있다면 &lt;strong&gt;잘못된 추상&lt;/strong&gt;화이다.) &lt;/p&gt;
&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;공식문서&lt;/strong&gt;를 다시 &lt;strong&gt;정독&lt;/strong&gt;하면서 모르는 걸 하나씩 정리하면서 &lt;strong&gt;스프링 전체 생태계에 대한 지식&lt;/strong&gt;이 더 쌓이는 것을 느꼈다.&lt;/p&gt;
&lt;p&gt;스프링 생태게 뿐만 아니라 &lt;strong&gt;다양한 패턴의 활용이나, 라이브러리에서 뭘 고려해야 하는 지&lt;/strong&gt; 등 시각이 조금 넓어지는 것 같다.&lt;/p&gt;
&lt;p&gt;처음 공식 문서를 봤을 때는 잘 이해가 안되었는데, &lt;code&gt;아는 만큼 보인다&lt;/code&gt;고 지금은 더 많이 보이게 되었다.&lt;/p&gt;
&lt;p&gt;1년 뒤에는 지금보다 더 많이 보일 것이다.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference&quot;&gt;https://docs.spring.io/spring-framework/reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://coding-factory.tistory.com/712#google_vignette&quot;&gt;https://coding-factory.tistory.com/712#google_vignette&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@dvmflstm/Review-소프트웨어-개발의-지혜-TEMPLATE-METHOD-및-STRATEGY-패턴-상속과-위임&quot;&gt;https://velog.io/@dvmflstm/Review-소프트웨어-개발의-지혜-TEMPLATE-METHOD-및-STRATEGY-패턴-상속과-위임&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Etc./개발 일기</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/393</guid>
      <comments>https://jaehoney.tistory.com/393#entry393comment</comments>
      <pubDate>Sat, 2 Dec 2023 09:46:42 +0900</pubDate>
    </item>
    <item>
      <title>MySQL - 인덱스를 사용해도 느린 이유! (key_len, filtered)</title>
      <link>https://jaehoney.tistory.com/392</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;Optimizer가 원하는 Index를 사용했으면 최적의 쿼리인걸까?!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;그렇지 않다!&lt;/strong&gt; 예시를 통해 알아보자. 아래는 예시를 위해 생성한 Index이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mMebD/btsBj9zCwtm/Yj2hWNP07vovUz6HcKVK1k/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;아래의 쿼리를 실행시키면 어떻게 될까?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;sql language-sql&quot;&gt;explain select * from employees
where office_id = 1 and money = 50;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;key&lt;/code&gt;로 &lt;code&gt;custom_index&lt;/code&gt;가 선택되었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHR8RO/btsBhWOTrEI/dDLGFOn2EOkkgtMbLsihak/img.png&quot; alt=&quot;img_5.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;글자가 너무 작아서 중요한 부분을 확대했다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc3H6m/btsBh5kYReC/fwGKKh9jLcITrajwuW484K/img.png&quot; alt=&quot;img_1.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;실행-시간&quot;&gt;실행 시간&lt;/h2&gt;
&lt;p&gt;해당 쿼리의 실행 시간은 &lt;strong&gt;548ms&lt;/strong&gt; 이다. &lt;strong&gt;Index도 원하는 대로 탔고&lt;/strong&gt;, &lt;strong&gt;매우 간단한 데 쿼리임에도 불구하고 매우 오래걸린다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GG6ex/btsBf9uz493/5M8Hwc1NwkZ5QcgiLTZ180/img.png&quot; alt=&quot;img_2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;뭐가 잘못된 걸까..?&lt;/p&gt;
&lt;h2 id=&quot;1-key_len&quot;&gt;1. key_len&lt;/h2&gt;
&lt;p&gt;다시 실행 계획을 보자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Aml63/btsBfMsvfGj/7LhM77MyNKEveT0UQy1ylk/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;여기서 가장 중요한 지표는 &lt;code&gt;key_len&lt;/code&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;key_len&lt;/strong&gt;은 &lt;strong&gt;쿼리 조건으로 사용된 인덱스 컬럼들의 총 Byte 수&lt;/strong&gt;이다. key_len을 보면 8이다. &lt;code&gt;office_id&lt;/code&gt; 컬럼의 byte 수인 8이다. (bigint)&lt;/p&gt;
&lt;p&gt;즉, &lt;code&gt;office_id&lt;/code&gt; 까지는 인덱스 기반 검색을 했다. 그렇지만 &lt;code&gt;user_id&lt;/code&gt;와 &lt;code&gt;money&lt;/code&gt;는 &lt;strong&gt;전체 탐색을 해서 필터링&lt;/strong&gt;을 한 것이다.&lt;/p&gt;
&lt;p&gt;그래서 막대한 성능 저하가 일어났다.&lt;/p&gt;
&lt;h2 id=&quot;2-filtered&quot;&gt;2. filtered&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;key_len&lt;/code&gt;만 봐도 원인은 찾은 상황이다. 하지만 &lt;strong&gt;중요한 1가지 지표가 더&lt;/strong&gt; 있다. &lt;code&gt;filtered&lt;/code&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;filtered&lt;/code&gt;는 &lt;strong&gt;MySQL 엔진에 의해 필터링되고 남은 비율&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7sQKF/btsBfAeSb4F/DivVUSGnm6cR6tUCLfDHB0/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;filtered&lt;/code&gt;가 10이라는 것은 &lt;strong&gt;스토리지 엔진에 의해 인덱스 기반으로 검색하고 남은 결과&lt;/strong&gt;를 &lt;strong&gt;10%만 남기고 모두 필터링&lt;/strong&gt;했다는 뜻이다.&lt;/p&gt;
&lt;p&gt;인덱스는 첫 번째 인덱스 컬럼인 &lt;code&gt;office_id&lt;/code&gt;까지만 탔었다. 즉, &lt;code&gt;office_id&lt;/code&gt; 조건이 일치하는 모든 레코드를 대상으로 하나씩 &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;money&lt;/code&gt;로 &lt;strong&gt;90%를 필터링&lt;/strong&gt;한 것이다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(통계 정보에 기반한 예측 값이다. 수치는 정확하지 않다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;조건을-바꾸면&quot;&gt;조건을 바꾸면&lt;/h2&gt;
&lt;p&gt;그러면 &lt;code&gt;user_id&lt;/code&gt;를 Where에 추가해보자. 더미 데이터로 넣은 user_id는 &lt;strong&gt;1 ~ 10 범위&lt;/strong&gt;를 가진다. 즉, 아래에서 추가한 user_id 조건은 사실상 &lt;strong&gt;전체&lt;/strong&gt;이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;sql language-sql&quot;&gt;explain select * from employees
where office_id = 1 and user_id in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) and money = 50;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;explain의 결과는 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgoSV1/btsBh6c73JP/Fj0Y4rBYNoIl6eLH6YVkU1/img.png&quot; alt=&quot;img_7.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;동일한 범위의 데이터 검색임에도 Explain의 결과가 다르다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;key_len이 3개 컬럼의 바이트 값을 더한 24가 정확히 나온다. (마지막 컬럼까지 인덱스를 탈 수 있었다.)&lt;/li&gt;
&lt;li&gt;filtered가 100이 나온다. (Index 스캔을 기반으로 하는 스토리지 엔진만으로 모두 필터링했고, MySQL 엔진에서는 필터링을 하지 않아서 100%가 남았다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;실행 결과는 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H3FAt/btsBjCINhul/VyWt7JsKKJ8FrQlTgOhMk1/img.png&quot; alt=&quot;img_4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;쿼리 실행 시간도 &lt;strong&gt;548ms -&gt; 7ms&lt;/strong&gt;로 개선된 것을 볼 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;인덱스-스킵-스캔&quot;&gt;인덱스 스킵 스캔&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;MySQL 8.0&lt;/strong&gt; 부터는 선행 인덱스 컬럼을 건너 뛸 수 있는  &lt;code&gt;Skip Scan Access Method&lt;/code&gt;라는 것을 지원한다.&lt;/p&gt;
&lt;p&gt;예를 들면 아래 쿼리를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;sql language-sql&quot;&gt;explain select id from employees
where office_id = 1 and money = 50;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음 문제가 되었던 쿼리에서 &lt;code&gt;select&lt;/code&gt;절만 &lt;code&gt;*&lt;/code&gt;에서 &lt;code&gt;id&lt;/code&gt;로 바꿨다. 그래서 &lt;strong&gt;Index 만으로 조회 쿼리를 실행&lt;/strong&gt;할 수 있게 되었다.&lt;/p&gt;
&lt;p&gt;실행 계획은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLwCVD/btsBfxJdiKn/xvv857lVPgcV2GckJ27Ka0/img.png&quot; alt=&quot;img_8.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;인덱스 스킵 스캔&lt;/strong&gt;을 사용해서 &lt;strong&gt;특정 인덱스 컬럼을 건너 뛰고&lt;/strong&gt; 검색할 수 있다. &lt;code&gt;key_len&lt;/code&gt;과 &lt;code&gt;filtered&lt;/code&gt;를 볼 때 정상적으로 인덱스를 탈 수 있음을 알 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;결론&quot;&gt;결론&lt;/h2&gt;
&lt;p&gt;특정 &lt;strong&gt;인덱스를 사용했어도 후반 인덱스 컬럼을 활용하지 못하는 경우&lt;/strong&gt;도 생길 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;key_len&lt;/code&gt;과 &lt;code&gt;filtered&lt;/code&gt;를 알고 있다면 문제를 인식할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이때는 아래의 처리 중 고민해야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;신규 인덱스를 생성&lt;/strong&gt; 또는 &lt;strong&gt;기존 인덱스 교체&lt;/strong&gt; (중간 인덱스 컬럼 제거)&lt;/li&gt;
&lt;li&gt;중간 인덱스 컬럼에서도 &lt;strong&gt;인덱스를 탈 수 있도록 유도&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;중간 인덱스 컬럼 WHERE 조건 추가&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;MySQL 8.0 이상이라면 Index Skip Scan을 활용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cheese10yun.github.io/mysql-explian&quot;&gt;https://cheese10yun.github.io/mysql-explian&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wisdom-and-record.tistory.com/137&quot;&gt;https://wisdom-and-record.tistory.com/137&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Database/SQL</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/392</guid>
      <comments>https://jaehoney.tistory.com/392#entry392comment</comments>
      <pubDate>Mon, 27 Nov 2023 22:27:05 +0900</pubDate>
    </item>
    <item>
      <title>SQL - WHERE 절, ON 절 제대로 이해하기!</title>
      <link>https://jaehoney.tistory.com/391</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;최근에 팀원 분이 LEFT OUTER JOIN의 &lt;strong&gt;ON 절에 일반 조건&lt;/strong&gt;이 포함된 쿼리를 작성하신 것을 봤다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;sql language-sql&quot;&gt;SELECT *
FROM team t
LEFT OUTER JOIN member m
    ON t.id = m.team_id
    AND m.team_id = 4;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 부분이 예상대로 동작하지 않을 것을 예상하고 리뷰를 드리면서, 생각보다 잘 모르시는 분이 많으실 것 같아 정리하게 되었다. &lt;/p&gt;
&lt;p&gt;미리 말하지만 &lt;strong&gt;해당 SQL은 의도대로 동작하지 않는다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ON 절과 WHERE 절의 차이&lt;/strong&gt;에 대해 알아보자.&lt;/p&gt;
&lt;h2 id=&quot;sample-data-삽입&quot;&gt;Sample Data 삽입&lt;/h2&gt;
&lt;p&gt;테스트를 위해 데이터를 삽입했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Team을 5개 삽입&lt;/li&gt;
&lt;li&gt;각 팀별 멤버를 2개 삽입&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;where&quot;&gt;WHERE&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;sql language-sql&quot;&gt;SELECT *
FROM team t
LEFT OUTER JOIN member m
    ON t.id = m.team_id
WHERE m.team_id = 4;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where 조건의 의미는 말 그대로 필터링이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;m.team_id&lt;/code&gt;가 4인 것만 가져오라는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ewXI2W/btsEkfSY4BQ/g0kEzyg8kiOkttxcxaYKO1/img.png&quot; alt=&quot;img_2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;결과도 예상과 같이 수행된다.&lt;/p&gt;
&lt;h2 id=&quot;on&quot;&gt;ON&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;sql language-sql&quot;&gt;SELECT *
FROM team t
LEFT OUTER JOIN member m
    ON t.id = m.team_id
    AND m.team_id = 4;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ON절은 아래와 같이 예상하지 못한 결과가 수행된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuPAbd/btsEnQKUoZD/rUfKwICZDXOyHxierB3d1k/img.png&quot; alt=&quot;img_1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;왜냐하면 ON절에 조건을 명시하면 &lt;strong&gt;조건에 맞는 데이터만 JOIN 하겠다는 뜻&lt;/strong&gt;이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FROM 테이블은 전체가 노출&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;조건에 맞는 경우 OUTER JOIN 수행&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, LEFT OUTER JOIN의 ON절을 걸면 메인 테이블과 ON절의 조건과 &lt;strong&gt;차집합&lt;/strong&gt;을 반환한다.&lt;/p&gt;
&lt;h2 id=&quot;inner-join-에서는&quot;&gt;Inner Join 에서는&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Inner Join&lt;/code&gt;에서는 &lt;code&gt;WHERE&lt;/code&gt;과 &lt;code&gt;ON&lt;/code&gt; 모두 동일한 결과를 가져올 수 있다. &lt;/p&gt;
&lt;p&gt;ON절의 가장 중요한 개념은 &lt;strong&gt;true가 아니면 조인을 하지 않는다는 것&lt;/strong&gt;이다. &lt;code&gt;Inner Join&lt;/code&gt;의 경우 Join할 데이터가 존재하지 않으면 &lt;code&gt;FROM&lt;/code&gt; 테이블 데이터도 반환하지 않는다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;WHERE&lt;/code&gt;절과 &lt;code&gt;ON&lt;/code&gt;절이 동일한 것처럼 동작한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;실제 용도와 의미는 분명히 다르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;
&lt;p&gt;정리하면 &lt;code&gt;LEFT OUTER JOIN&lt;/code&gt;에서 &lt;code&gt;WHERE&lt;/code&gt; 절에 일반 조건을 명시하면 아래 결과가 나온다. &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZOTom/btsEmT2rWTH/YJyRsEOJwBV92UiB3QqVD0/img.png&quot; alt=&quot;img_3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;반면 &lt;code&gt;ON&lt;/code&gt; 절에 일반 조건을 명시하면 아래 결과가 나온다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn9vkQ/btsEkx6K9Es/C0d3AQTRSVFRXjJXQFmudK/img.png&quot; alt=&quot;img_4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WHERE&lt;/code&gt;과 &lt;code&gt;ON&lt;/code&gt;은 &lt;strong&gt;용도와 의미가 명백히 다르며&lt;/strong&gt; &lt;strong&gt;의미에 맞게 사용&lt;/strong&gt;해야 한다.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://gibles-deepmind.tistory.com/entry/Oracle-SQL-JOIN%EC%8B%9C-WHERE-%EC%A0%88%EA%B3%BC-ON-%EC%A0%88%EC%9D%98-%EC%B0%A8%EC%9D%B4where-clause-vs-on-clause&quot;&gt;https://gibles-deepmind.tistory.com/entry/Oracle-SQL-JOIN%EC%8B%9C-WHERE-%EC%A0%88%EA%B3%BC-ON-%EC%A0%88%EC%9D%98-%EC%B0%A8%EC%9D%B4where-clause-vs-on-clause&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Database/SQL</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/391</guid>
      <comments>https://jaehoney.tistory.com/391#entry391comment</comments>
      <pubDate>Tue, 21 Nov 2023 08:51:00 +0900</pubDate>
    </item>
    <item>
      <title>JPA - OSIV 제대로 이해하기!</title>
      <link>https://jaehoney.tistory.com/390</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;osivopen-session-in-view&quot;&gt;OSIV(Open Session In View)&lt;/h2&gt;
&lt;p&gt;OSIV(Open Session In View)는 &lt;strong&gt;영속성 컨텍스트&lt;/strong&gt;를 &lt;strong&gt;뷰까지 개방&lt;/strong&gt;하는 기능이다.&lt;/p&gt;
&lt;p&gt;이를 사용하면 트랜잭션이 종료되어도 영속성 컨텍스트가 관리될 수 있다.&lt;/p&gt;
&lt;p&gt;여기까지는 사실 다 아는 내용이고 &lt;strong&gt;OSIV의 동작 원리&lt;/strong&gt;에 대해 알아보자.&lt;/p&gt;
&lt;h2 id=&quot;openinview&quot;&gt;OpenInView&lt;/h2&gt;
&lt;p&gt;Spring 환경에서 보통 아래의 property로 OSIV 설정에 접근한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;spring.jpa.open-in-view&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래는 &lt;code&gt;JpaWebConfiguration&lt;/code&gt; 클래스의 애노테이션이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0HCyr/btsEkxMrN91/0hbXKzZi8OLJKxPRCk5ll1/img.png&quot; alt=&quot;img_5.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;open-in-view&lt;/code&gt; 속성에 따라 해당 Configuration이 등록된다는 것을 알 수 있다. 이를 통해 알 수 있는 놀라운 사실은 '&lt;strong&gt;Spring JPA도 웹과 연관이 있다.&lt;/strong&gt;'는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LTryQ/btsElhWUQXW/dRZEsXt04qam8WTwOkkeVK/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JpaProperties&lt;/code&gt;의 &lt;code&gt;openInView&lt;/code&gt;에 대한 설명을 번역하면 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OpenEntityManagerInViewInterceptor&lt;/code&gt;를 등록&lt;/li&gt;
&lt;li&gt;JPA EntityManager를 Thread에 바인딩&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;뭔가 조금 깊게 들어가봐야 정확히 알 수 있을 것 같다.&lt;/p&gt;
&lt;h2 id=&quot;openentitymanagerinviewinterceptor&quot;&gt;OpenEntityManagerInViewInterceptor&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;OpenEntityManagerInViewInterceptor&lt;/code&gt;에 대한 설명을 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btAzIx/btsEmVzdJoE/Rv1zcu2OYZVAOlDDBKAU3k/img.png&quot; alt=&quot;img_1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;번역을 하면 &lt;code&gt;OpenEntityManagerInViewInterceptor&lt;/code&gt;의 역할을 아래와 같이 설명하고 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;쓰레드에 JPA EntityManager를 바인딩하는 &lt;strong&gt;Web Request Interceptor&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;트랜잭션이 완료되었더라도 웹 뷰에서 Lazy Loading을 허용하기 위함&lt;/li&gt;
&lt;li&gt;현재 쓰레드를 통해 JPA EntityManagers를 사용할 수 있게 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;해당 클래스의 몇 개 메서드를 살펴보자.&lt;/p&gt;
&lt;h3 id=&quot;prehandle&quot;&gt;preHandle()&lt;/h3&gt;
&lt;p&gt;아래는 &lt;code&gt;OpenEntityManagerInViewInterceptor&lt;/code&gt;의 &lt;code&gt;preHandle&lt;/code&gt; 메서드이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bH35NZ/btsEmC7KAHZ/VYtAeNPS8i9EsqkPkF392k/img.png&quot; alt=&quot;img_3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;해당 메서드에서는 요청이 들어오면 새로운 &lt;code&gt;entityManager&lt;/code&gt;를 생성한다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;code&gt;TransactionSynchronizationManager.bindResource(emf, emHolder);&lt;/code&gt;를 호출해서 &lt;code&gt;ThreadLocal&lt;/code&gt;에 &lt;code&gt;EntityManager&lt;/code&gt;를 바인딩한다.&lt;/p&gt;
&lt;h3 id=&quot;aftercompletion&quot;&gt;afterCompletion()&lt;/h3&gt;
&lt;p&gt;응답이 완료되면 아래와 같이 &lt;code&gt;TransactionSynchronizationManager&lt;/code&gt;에 &lt;strong&gt;저장된 리소스를 언바인드&lt;/strong&gt;한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uYreN/btsEnOl1ah0/g7srtoFAWbidYkSUl10tI1/img.png&quot; alt=&quot;img_4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;그리고 생성했던 &lt;code&gt;entityManager&lt;/code&gt;를 &lt;code&gt;close()&lt;/code&gt; 처리한다.&lt;/p&gt;
&lt;p&gt;해당 메서드는 &lt;code&gt;preHandle&lt;/code&gt;이 성공적으로 수행되었을 때만 호출된다.&lt;/p&gt;
&lt;h2 id=&quot;entitymanager-재활용&quot;&gt;EntityManager 재활용&lt;/h2&gt;
&lt;p&gt;아래는 &lt;code&gt;JpaTransactionManager&lt;/code&gt;의 &lt;code&gt;doGetTransaction()&lt;/code&gt; 메서드이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPUlAE/btsEoKQ2nsG/YDEkoXzTAb4C1ZKkUpFgJ0/img.png&quot; alt=&quot;img_6.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;해당 코드를보면 &lt;code&gt;TransactionSynchronizationManager.getResource()&lt;/code&gt;를 호출해서 쓰레드에 할당된 &lt;code&gt;emHolder&lt;/code&gt;를 찾아 &lt;strong&gt;재활용&lt;/strong&gt;한다는 사실을 알 수 있었다.&lt;/p&gt;
&lt;p&gt;즉, OSIV를 사용하면 &lt;code&gt;EntityManager&lt;/code&gt;를 &lt;strong&gt;매번 새로 생성하지 않고 재활용&lt;/strong&gt;할 수 있다는 장점이 있다.&lt;/p&gt;
&lt;h3 id=&quot;디버깅해보기&quot;&gt;디버깅해보기&lt;/h3&gt;
&lt;p&gt;OSIV 설정에 따라 &lt;strong&gt;EntityManager의 생명주기가 변하는 것이 사실인 지 확인&lt;/strong&gt;해보자.&lt;/p&gt;
&lt;p&gt;이해한 내용을 정리하면 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OSIV가 on일 때 여러 트랜잭션에서도 EntityManager를 재활용&lt;/strong&gt;해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OSIV가 off일 때 EntityManager를 트랜잭션이 실행될 때마다 생성&lt;/strong&gt;되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;실제로 확인해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Service
@RequiredArgsConstructor
public class ServiceA {
    private final MemberRepository memberRepository;

    public void execute() {
        memberRepository.save(new Member(&quot;test&quot;));
        memberRepository.save(new Member(&quot;test&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;먼저 OSIV를 켰을 때(default) &lt;code&gt;EntityManager&lt;/code&gt;의 구현체인 &lt;code&gt;SessionImpl&lt;/code&gt;의 주소가 두 개의 메서드 호출에서 동일하다.&lt;br /&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/o3dSh/btsEni1KB3D/Cl8obD4YoMz86a1TeBQCY0/img.png&quot; alt=&quot;img_9.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q4F0p/btsEmXw1hDz/Og1KiHVgm9tonE0YDMWK51/img.png&quot; alt=&quot;img_10.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;OSIV를 끄면 아래의 결과가 나온다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWrOh1/btsEnh9CQzY/KPde9sZ9UGnCfT5K4fq1h1/img.png&quot; alt=&quot;img_7.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/THdun/btsEpnhav3N/O0yhu2PRIfFvJMaqc1uN9K/img.png&quot; alt=&quot;img_8.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;주소를 보면 두 EntityManager가 다른 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;즉, OSIV 설정을 켜면 &lt;strong&gt;EntityManager를 재활용&lt;/strong&gt;할 수 있게 된다.&lt;/p&gt;
&lt;p&gt;실제로 OSIV를 끄고 디버깅해보면 &lt;code&gt;JpaTransactionManager&lt;/code&gt;의 &lt;code&gt;doGetTransaction()&lt;/code&gt;을 보면 두 번의 메서드 호출 모두에서 &lt;code&gt;emHolder&lt;/code&gt;가 null인 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6mLNF/btsElLjyT5R/6YfUsyiuDmjVacLoZs8Z50/img.png&quot; alt=&quot;img_11.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;
&lt;p&gt;기존에는 무조건 &lt;code&gt;open-in-view&lt;/code&gt;속성을 끄는 것이 바람직하다고 생각했었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;요청 전체에서 영속성 컨텍스트를 관리하는 것이 비효율적이기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OSIV의 기본 설정값이 &lt;code&gt;true&lt;/code&gt;인 이유는 Transaction 밖에서도 연관 엔터티 지연로딩 등을 편리하게 사용하기 위함이라고만 알고 있었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;영속성 컨텍스트 밖에서 지연로딩을 사용하면 &lt;code&gt;LazyInitializationException&lt;/code&gt;가 발생한다.&lt;/li&gt;
&lt;li&gt;DB Connection을 오래 유지하는 문제도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그러나 OSIV는 Lazy Loading 이외에도 이점이 있었다. &lt;strong&gt;영속성 컨텍스트&lt;/strong&gt;의 &lt;strong&gt;생명주기&lt;/strong&gt;와 &lt;strong&gt;재활용&lt;/strong&gt;과 밀접하게 관련이 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;트랜잭션 밖에서 재조회 시 영속성 컨텍스트를 활용할 수 있다.&lt;/li&gt;
&lt;li&gt;요청 1개당 1개의 &lt;code&gt;EntityManager&lt;/code&gt;만을 생성하고 재사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;느낀 점이 OSIV는 무조건 끄는 게 좋은 것이 아니라 &lt;strong&gt;Trade-Off&lt;/strong&gt; 관계이다.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://brunch.co.kr/@anonymdevoo/58&quot;&gt;https://brunch.co.kr/@anonymdevoo/58&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Server/Spring JPA</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/390</guid>
      <comments>https://jaehoney.tistory.com/390#entry390comment</comments>
      <pubDate>Sun, 19 Nov 2023 19:46:35 +0900</pubDate>
    </item>
    <item>
      <title>Spring에서의 Proxy, AOP 동작원리 이해하기!</title>
      <link>https://jaehoney.tistory.com/389</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;지난번 Spring AOP를 적용하면서 생겼던 문제에 대해 소개했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://jaehoney.tistory.com/375&quot;&gt;https://jaehoney.tistory.com/375&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이번 포스팅에서는 실무보다는 &lt;strong&gt;기본 개념&lt;/strong&gt;에 대해 집중적으로 알아보자.&lt;/p&gt;
&lt;p&gt;해당 포스팅은 &lt;strong&gt;김영한님의 스프링 핵심 원리 - 고급편&lt;/strong&gt;의 프록시 관련 내용을 정리한 것이며, &lt;strong&gt;프록시와 AOP의 동작의 기본 개념&lt;/strong&gt;이라고 보면 된다.&lt;/p&gt;
&lt;h2 id=&quot;프록시-패턴&quot;&gt;프록시 패턴&lt;/h2&gt;
&lt;p&gt;프록시 패턴에서는 프록시가 너무 많이 생기는 문제가 있다.&lt;/p&gt;
&lt;p&gt;아래는 GOF 프록시 패턴의 예시이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRiX6C/btsAyVJj3tg/JQRQXa1A0QSicortH6oGb1/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;예로 들면 &lt;code&gt;Repository&lt;/code&gt; 1개마다 전부 프록시 클래스를 생성해야 한다. 프록시를 적용할 클래스가 100개라면 100개의 프록시를 적용하는 코드를 만들어야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;즉, &lt;strong&gt;단일 책임 원칙&lt;/strong&gt;에 어긋나고 기능 변경 시 다수의 클래스에 변경이 전파된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 문제를 해결하기 위한 기술이 &lt;strong&gt;Dynamic proxy&lt;/strong&gt; 이다.&lt;/p&gt;
&lt;h2 id=&quot;다이나믹-프록시&quot;&gt;다이나믹 프록시&lt;/h2&gt;
&lt;p&gt;프록시 패턴에서는 대상 클래스 1개마다 클래스를 1개 추가해야 한다는 단점이 필요하다.&lt;/p&gt;
&lt;p&gt;프록시 1개만 사용해서 모든 클래스에 프록시를 적용할 수 없을까? 이걸 해결하는게 &lt;strong&gt;동적 프록시(Dynamic Proxy)&lt;/strong&gt; 방식이다.&lt;/p&gt;
&lt;p&gt;동적 프록시 중에서 JDK 동적 프록시를 이해하기 위해 &lt;strong&gt;리플렉션&lt;/strong&gt;에 대해 가볍게 살펴보자.&lt;/p&gt;
&lt;h4 id=&quot;리플렉션&quot;&gt;리플렉션&lt;/h4&gt;
&lt;p&gt;리플렉션을 사용하면 &lt;strong&gt;프록시를 적용할 코드 1개로&lt;/strong&gt; &lt;strong&gt;프록시 객체를 많이 생성&lt;/strong&gt;할 수 있다.&lt;/p&gt;
&lt;p&gt;아래와 같이 코드를 작성하면 &lt;code&gt;dynamicCall()&lt;/code&gt;이라는 공통 메서드를 추출할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Test
void reflection() throws Exception {
    Class classHello = Class.forName(&quot;jaehoney.proxy.jdkdynamic.ReflectionTest$Hello&quot;); 
    Hello target = new Hello();

    Method method = classHello.getMethod(&quot;print&quot;);
    dynamicCall(method, target);
}

private void dynamicCall(Method method, Object target) throws Exception {
    log.info(&quot;Hello&quot;);
    Object result = method.invoke(target);
    log.info(&quot;result={}&quot;, result);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;jdk-다이나믹-프록시&quot;&gt;JDK 다이나믹 프록시&lt;/h2&gt;
&lt;p&gt;아래는 JDK 다이나믹 프록시를 사용한 예시이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;java.lang.reflect.InvocationHandler&lt;/code&gt;를 구현한 클래스를 생성한다. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Slf4j
public class TimeInvocationHandler implements InvocationHandler {

    private final Object target;

    public TimeInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long startTime = System.currentTimeMillis();

        Object result = method.invoke(target, args);

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info(&quot;TimeProxy 종료 resultTime={}&quot;, resultTime);
        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 모든 클래스에 동일한 코드로 프록시를 적용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;AInterface target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
AInterface proxy =
        (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), new class[] {AInterface.class}, handler);
proxy.call();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;주의할 점은 이 방식(Jdk Dynamic Proxy)은 &lt;strong&gt;Interface&lt;/strong&gt;가 있어야만 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beHk8h/btsAuhNXJbp/hOskNuVeVzJvXPTXtrrF2k/img.png&quot; alt=&quot;img_1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;JDK Dynamic Proxy를 사용하면 필요한 프록시 인스턴스 수는 N으로 동일하지만, 프록시를 적용할 클래스는 1개만 만들면 된다. 그래서 부가 기능은 &lt;code&gt;InvocationHandler&lt;/code&gt;의 구현체에서만 관리해주면 된다.&lt;/p&gt;
&lt;p&gt;즉, &lt;strong&gt;단일 책임 원칙&lt;/strong&gt;을 지킬 수 있게 되었다.&lt;/p&gt;
&lt;h2 id=&quot;cglib&quot;&gt;CGLib&lt;/h2&gt;
&lt;p&gt;JDK Dynamic Proxy는 &lt;strong&gt;Interface&lt;/strong&gt;가 꼭 있어야만 동작한다.&lt;/p&gt;
&lt;p&gt;그래서 클래스만 있는 경우에는 &lt;strong&gt;CGLib&lt;/strong&gt;이라는 &lt;strong&gt;바이트코드를 조작&lt;/strong&gt;하는 라이브러리를 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;org.springframework.cglib.proxy.MethodInterceptor&lt;/code&gt;를 구현하면 CGLib 기반 프록시로 동작시킬 수 있다. DynamicProxy와 다르게 java 패키지가 아닌 스프링 패키지에 있다.&lt;/p&gt;
&lt;p&gt;MethodInterceptor의 구현체를 아래와 같이 정의한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {

    private final Object target;

    public TimeMethodInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        long startTime = System.currentTimeMillis();

        Object result = methodProxy.invoke(target, args);

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info(&quot;TimeProxy 종료 resultTime={}&quot;, resultTime);
        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 아래와 같이 프록시를 만들어서 실행시킬 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;AClass target = new AClass();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(AClass.class);
enhancer.setCallback(new TimeMethodInterceptor(target));
AClass proxy = (AClass) enhancer.create();
proxy.call();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CGLib&lt;/code&gt;에서는 &lt;code&gt;JDK Dynamic Proxy&lt;/code&gt;와 다르게 &lt;strong&gt;구체 클래스를 상속&lt;/strong&gt;받아서 프록시를 생성한다. 그래서 &lt;strong&gt;Interface가 없어도 동작&lt;/strong&gt;하도록 구현되었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mQjEK/btsABCbe23s/OPKXWNR3JDgeuyi7NzOyj0/img.png&quot; alt=&quot;img_2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Spring은 기본적으로 Interface가 있는 경우  &lt;code&gt;JDK Dynamic Proxy&lt;/code&gt;를 만들고, Interface가 없는 경우 &lt;code&gt;CGLib Proxy&lt;/code&gt;를 만든다. Spring-Boot 2.0 부터는 &lt;code&gt;CGLib&lt;/code&gt; 기반 프록시가 기본으로 채택되었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CGLib의 주의사항&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.&lt;/li&gt;
&lt;li&gt;클래스에 final 키워드가 붙으면 예외가 발생한다.&lt;/li&gt;
&lt;li&gt;메서드에 final 키워드가 붙으면 프록시 로직이 동작하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;프록시-팩토리&quot;&gt;프록시 팩토리&lt;/h2&gt;
&lt;p&gt;스프링에서 인터페이스가 있을 경우에는 JDK 다이나믹 프록시를 적용하고, 인터페이스가 없을 때는 CGLib를 적용해야 한다.&lt;/p&gt;
&lt;p&gt;스프링은 동적 프록시를 통합해서 편리하게 만들어주는 &lt;strong&gt;프록시 팩토리&lt;/strong&gt;를 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmZ2yI/btsAvjdqCKh/P8KxtVt3ORxDH3cme8764K/img.png&quot; alt=&quot;img_3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;스프링은 아래와 같이 &lt;code&gt;adviceInvocationHandler&lt;/code&gt;나 &lt;code&gt;adviceMethodInterceptor&lt;/code&gt;는 &lt;strong&gt;Advice&lt;/strong&gt;를 호출한다.&lt;/p&gt;
&lt;p&gt;그래서 개발자는 프록시 전후에 실행되어야 하는 로직을 가진 &lt;strong&gt;Advice&lt;/strong&gt;만 만들면 동적 프록시를 적용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nVXTa/btsACkhiQik/WBavAs5Gb74dRWUEQ89Ik0/img.png&quot; alt=&quot;img_4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;프록시 팩토리는 이 뿐만 아니라 &lt;strong&gt;중요한 역할&lt;/strong&gt;이 하나 더있다.&lt;/p&gt;
&lt;p&gt;인스턴스 1개에 여러 개의 AOP를 적용할 때 프록시는 &lt;strong&gt;1개만 생성&lt;/strong&gt;된다. 그 이유는 프록시 팩토리가 여러 개의 Advisor를 가지기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n7esb/btsAyLG0eM8/6X5Ac2X8QLFxKfuvkIfkCK/img.png&quot; alt=&quot;img_5.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Advisor는 1개의 Advice, 1개의 Pointcut을 가진다. &lt;strong&gt;실제 동작&lt;/strong&gt;은 Advisor의 &lt;strong&gt;Advice&lt;/strong&gt;에서 하므로 프록시를 Advisor 수 만큼 생성할 이유가 없다. 해당 Advice로의 참조만 가지면 된다.&lt;/p&gt;
&lt;p&gt;그래서 스프링에서는 &lt;strong&gt;대상 클래스 1개당 1개의 프록시&lt;/strong&gt;만 만들어서 사용한다.&lt;/p&gt;
&lt;h2 id=&quot;beanpostprocessor&quot;&gt;BeanPostProcessor&lt;/h2&gt;
&lt;p&gt;앞서 설명했듯, Jdk Dynamic Proxy나 CGLib Proxy 모두 &lt;strong&gt;1개의 클래스로 여러 프록시를 생성&lt;/strong&gt;할 수 있도록 동작한다.&lt;/p&gt;
&lt;p&gt;AOP는 이를 &lt;code&gt;BeanPostProcessor&lt;/code&gt; 를 사용해서 이를 해결하고 있다.&lt;/p&gt;
&lt;p&gt;스프링에서 빈을 &lt;strong&gt;생성 후&lt;/strong&gt; &lt;strong&gt;등록하기 직전&lt;/strong&gt;에 &lt;strong&gt;조작하고 싶다면&lt;/strong&gt; &lt;strong&gt;BeanPostProcessor&lt;/strong&gt;를 사용하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BeanPostProcessor&lt;/strong&gt;는 빈을 생성한 후 등록하기 전에 &lt;strong&gt;객체를 조작&lt;/strong&gt;하거나, &lt;strong&gt;완전히 다른 객체로 바꿔치기&lt;/strong&gt; 하는 등을 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/exPLXM/btsAxHE5Ps0/9mKOwxZPthOk6lyQk6p7s0/img.png&quot; alt=&quot;img_7.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;만약 A 객체를 B 객체로 바꿔치기 하면 B 빈이 스프링 컨테이너에 등록된다.&lt;/p&gt;
&lt;h4 id=&quot;postconstruct&quot;&gt;@PostConstruct&lt;/h4&gt;
&lt;p&gt;우리가 사용하는 &lt;code&gt;@PostConstruct&lt;/code&gt;도 빈 후처리기를 사용한 기술이다.&lt;/p&gt;
&lt;p&gt;스프링은 애플리케이션 실행 시 &lt;code&gt;CommonAnnotationBeanPostProcessor&lt;/code&gt;라는 후처리기를 자동으로 등록한다. 해당 후처리기에서 &lt;code&gt;@PostConstruct&lt;/code&gt; 애노테이션이 붙은 메서드를 호출해준다.&lt;/p&gt;
&lt;h3 id=&quot;aop&quot;&gt;AOP&lt;/h3&gt;
&lt;p&gt;아래의 &lt;code&gt;BeanPostProcessor&lt;/code&gt;를 살펴보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class PackageLogTracePostProcessor implements BeanPostProcessor {

    private final String basePackage;
    private final Advisor advisor;

    public PackageLogTracePostProcessor(String basePackage, Advisor advisor) {
        this.basePackage = basePackage;
        this.advisor = advisor;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        //프록시 적용 대상이 아니면 원본을 그대로 진행
        String packageName = bean.getClass().getPackageName();
        if (!packageName.startsWith(basePackage)) {
            return bean;
        }

        //프록시 대상이면 프록시를 만들어서 반환
        ProxyFactory proxyFactory = new ProxyFactory(bean);
        proxyFactory.addAdvisor(advisor);

        Object proxy = proxyFactory.getProxy();
        return proxy;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;bean을 가져와서 원하는 조건으로 체크를 한 후 &lt;strong&gt;프록시로 Wrapping해서 반환&lt;/strong&gt;한다. 생성된 모든 빈에 대해 해당 메서드를 실행하므로 프록시를 일괄적으로 적용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spring-AOP&lt;/strong&gt;에서도 크게 다르지 않다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpC6mP/btsAws2tTVJ/s5c48DKnsjefbHSg8l9lK1/img.png&quot; alt=&quot;img_6.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Spring-AOP는 &lt;code&gt;@Aspect&lt;/code&gt;를 전부 찾아서 &lt;code&gt;Advisor&lt;/code&gt;로 변환한다. 찾은 &lt;code&gt;Adivsor&lt;/code&gt; 하나당 등록된 모든 빈을 대상으로 &lt;strong&gt;Pointcut을 보고 대상을 체크&lt;/strong&gt;한다. 대상은 &lt;strong&gt;Advice&lt;/strong&gt;를 적용한 프록시로 빈을 &lt;strong&gt;Wrapping&lt;/strong&gt;한다. &lt;/p&gt;
&lt;p&gt;그래서 &lt;strong&gt;일괄적으로 프록시를 적용&lt;/strong&gt;할 수 있다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>Server/Spring</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/389</guid>
      <comments>https://jaehoney.tistory.com/389#entry389comment</comments>
      <pubDate>Thu, 16 Nov 2023 08:52:51 +0900</pubDate>
    </item>
    <item>
      <title>Hibernate @Where 애노테이션이 동작하지 않는 이유!</title>
      <link>https://jaehoney.tistory.com/388</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot;&gt;
&lt;h2 id=&quot;where-애노테이션이-동작하지-않는-이유&quot; data-ke-size=&quot;size26&quot;&gt;@Where 애노테이션이 동작하지 않는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 Learning 테이블 레코드를 일괄 수정 시 인덱스를 타지 않는 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 &lt;code&gt;@Where(clause = &quot;del_flag = 'N'&quot;)&lt;/code&gt;을 했는데, QueryDsl을 사용한 조회에서는 &lt;b&gt;해당 조건을 수행하지 않아서&lt;/b&gt; 인덱스를 탈 수 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 Hibernate 및 JPA에 대한 관심이 부쩍 늘어서 이 문제에 접근해보면 경험치를 얻을 수 있을 것 같아서 디버깅해봤다.&lt;/p&gt;
&lt;h2 id=&quot;문제-파악&quot; data-ke-size=&quot;size26&quot;&gt;문제 파악&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 코드에 대한 영향 없이 확인해야 하기 때문에 새로 프로젝트를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 JPA Entity를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Where(clause = &quot;name = 'violet'&quot;)
public class Learning {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;

    public Learning(String name) {
        this.name = name;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래의 QueryDsl 테스트를 돌렸다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@DataJpaTest
@Transactional
public class LearningTest {
    @Autowired
    EntityManager em;
    JPAQueryFactory queryFactory;

    @BeforeEach
    void before() {
        queryFactory = new JPAQueryFactory(em);
        Learning learning = new Learning(&quot;violet&quot;);
        em.persist(learning);

        Learning learning2 = new Learning(&quot;violet2&quot;);
        em.persist(learning2);
    }

    @Test
    void select() {
        Learning entity = queryFactory
            .select(learning)
            .from(learning)
            .fetchOne();

        assertThat(entity.getName()).isEqualTo(&quot;violet&quot;);
    }

    @Test
    void update() {
        long updated = queryFactory
            .update(learning)
            .set(learning.name, &quot;updated&quot;)
            .execute();

        assertThat(updated).isEqualTo(1);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Select는 예상대로 잘 실행 되었다. &lt;code&gt;@Where&lt;/code&gt;애노테이션의 &lt;code&gt;clause&lt;/code&gt;에 명시한 조건이 추가되어 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z2pL2/btszKznGgkJ/3DKf5QVhbZ0SLfexvbZkAK/img.png&quot; alt=&quot;img_1.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 Update는 테스트가 실패했다. 조건이 들어갔다면 1개만 변경되었을 텐데 2개 row가 모두 영향을 받았다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/B0hz1/btszLnfGLYF/7A8kPrXuu8SOkKHlEkkz9K/img.png&quot; alt=&quot;img_2.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Where&lt;/code&gt;의 조건이 수행되지 않았다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvXbAF/btszK8iODdW/t1ABiscuzWJUIzca1mb7W0/img.png&quot; alt=&quot;img_3.png&quot; /&gt;
&lt;h2 id=&quot;왜-동작하지-않을까&quot; data-ke-size=&quot;size26&quot;&gt;왜 동작하지 않을까..?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;hibernate-core&lt;/code&gt;의 &lt;code&gt;AnnotationBinder&lt;/code&gt;는 &lt;code&gt;@Where&lt;/code&gt;을 가져와서 &lt;code&gt;EntityBinder&lt;/code&gt;에 세팅한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/pYMYW/btszM2PFPAS/W6UKXyZmCuYHM4Qg2kud9K/img.png&quot; alt=&quot;img_7.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 부분이 사용되는 곳은 &lt;code&gt;AbstractEntityPersister&lt;/code&gt;이다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVTMOl/btszKA08o8L/pU6duyjoAM7RpV016f5LfK/img.png&quot; alt=&quot;img_6.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 부분은 &lt;code&gt;Persist(조회 및 영속화)&lt;/code&gt;에서만 사용되고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Querydsl&lt;/code&gt;의 &lt;code&gt;update()&lt;/code&gt;는 영속화한 후 &lt;code&gt;save&lt;/code&gt;하는 방식이 아니라, 영속성과 관계없이 바로 &lt;code&gt;Update&lt;/code&gt;를 날려버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 동작하지 않았다.&lt;/p&gt;
&lt;h2 id=&quot;modifying&quot; data-ke-size=&quot;size26&quot;&gt;@Modifying&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 의문이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Hibernate&lt;/code&gt;가 업데이트 쿼리할 때 &lt;code&gt;@Where&lt;/code&gt;을 적용하지 않는다면, &lt;code&gt;JPA&lt;/code&gt;의 &lt;code&gt;@Modifying&lt;/code&gt;은 어떻게 &lt;code&gt;@Where&lt;/code&gt;을 가져와서 동작시킬까?&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface LearningRepository extends JpaRepository&amp;lt;Learning, Long&amp;gt; {

    @Modifying
    @Query(&quot;UPDATE Learning l SET l.name = :name&quot;)
    int updateName(String name);

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그걸 확인해보자. 아래의 테스트를 실행시키면 어떻게 될까?&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@DataJpaTest
class LearningRepositoryTest {

    @Autowired
    LearningRepository learningRepository;

    @Test
    void test() {
        learningRepository.updateName(&quot;1J&quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Querydsl&lt;/code&gt;로 Update할 때와 마찬가지로 &lt;code&gt;@Where&lt;/code&gt; 애노테이션이 동작하지 않는다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmlER8/btszQCwg36o/82kGHdZEZeyjcsz6lHzBrK/img.png&quot; alt=&quot;img_4.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;JPA&lt;/code&gt;에서도 &lt;code&gt;@Modifying&lt;/code&gt;을 사용한 쿼리에서는 &lt;code&gt;@Where&lt;/code&gt;이 동작하지 않았다.&lt;/p&gt;
&lt;h2 id=&quot;그럼-어떻게-할까&quot; data-ke-size=&quot;size26&quot;&gt;그럼 어떻게 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈를 공유하고 의견을 제시하려고 &lt;code&gt;Hibernate&lt;/code&gt;의 최신버전 프로젝트를 다운받았는데 갑자기 모든 것이 잘 통과했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Querydsl&lt;/code&gt;의 업데이트에서도 아래와 같이 &lt;code&gt;@Where&lt;/code&gt; 애노테이션이 잘 적용되었고&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/8wHSU/btszM38SU6o/pFo789cxkRDGLTbPAuQyG0/img.png&quot; alt=&quot;img_10.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;JPA&lt;/code&gt;의 &lt;code&gt;@Modifying&lt;/code&gt;에서도 정상적으로 동작했다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0oCnR/btszOOczcsl/Ov7ZZk51MZMxpkrYbtqyX0/img.png&quot; alt=&quot;img_11.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 이후 &lt;code&gt;@Where&lt;/code&gt; 애노테이션에 대한 실제 적용을 &lt;code&gt;org.hibernate.sql.ast.spi&lt;/code&gt; 쪽에서 하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&lt;code&gt;Hibernate&lt;/code&gt; 6.0 이전 버전에서는 해당 패키지에 아무것도 없었다.)&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/zS1H1/btszNTyh2QF/NUKIQWba3VqHwjFN2AKu50/img.png&quot; alt=&quot;img_12.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6.0 이후 버전에서는 아래와 같이 &lt;code&gt;BaseSqmToSqlAstConverter&lt;/code&gt;에서 &lt;code&gt;applyBaseRestrictions()&lt;/code&gt;를 실행하고,&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/3BUXO/btszH5UNQjy/btFOmrO64bkbyFGqXwPYP1/img.png&quot; alt=&quot;img_14.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;applyBaseRestrictions()&lt;/code&gt;는 &lt;code&gt;whereClauseRestrictions&lt;/code&gt;에 아래와 같이 &lt;code&gt;SingleTableEntityPersister&lt;/code&gt;의 &lt;code&gt;sqlWhereStringTemplate&lt;/code&gt;을 사용한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/YHtTD/btszH8D3GJz/xvfyP5pXiC1AxIezbeZ5ok/img.png&quot; alt=&quot;img_15.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;code&gt;additionalRestrictions&lt;/code&gt;가 반영된 &lt;code&gt;UpdateState&lt;/code&gt;를 생성한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/yNpi7/btszKecAQiF/AKOeKiVIBR1YYmKxovDUQk/img.png&quot; alt=&quot;img_16.png&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 Hibernate 6.0 버전 이후에서는 &lt;code&gt;@Where&lt;/code&gt; 애노테이션의 내용이 적용된 &lt;code&gt;UpdateState&lt;/code&gt;를 생성해서 &lt;code&gt;AST&lt;/code&gt;를 생성하고 실행한다. 그래서 &lt;code&gt;@Where&lt;/code&gt; 애노테이션이 정상적으로 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6.0 버전 이전에는 이러한 과정이 없으며 Update에서는 인자로 들어온 주어진 JPQL만 실행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 &lt;code&gt;@Where&lt;/code&gt; 애노테이션은 6.3 부터 &lt;b&gt;Deprecated&lt;/b&gt;되고 &lt;code&gt;@SQLRestriction&lt;/code&gt;를 사용하라고 권장한다.)&lt;/p&gt;
&lt;h2 id=&quot;마무리&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6.0 이전 버전의 &lt;code&gt;@Where&lt;/code&gt;의 버그에 대한 내용은 &lt;code&gt;release notes&lt;/code&gt;에서 찾지는 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경사항은 &lt;code&gt;6.0.0&lt;/code&gt;에 적용되었다. 버그에 대해 직접적으로 언급하거나 다루지는 않았지만 &lt;code&gt;HQL&lt;/code&gt;에서 &lt;code&gt;SQM&lt;/code&gt;을 지원하게 되었다. &lt;code&gt;SQM&lt;/code&gt;에 대해서는 아래에서 기술하고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hibernate/hibernate-orm/blob/main/design/sqm.adoc&quot;&gt;https://github.com/hibernate/hibernate-orm/blob/main/design/sqm.adoc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SQM&lt;/code&gt;의 지원과 &lt;code&gt;AST&lt;/code&gt;가 개선 되면서 &lt;code&gt;@Where&lt;/code&gt; 애노테이션이 동작하지 않는 문제도 &lt;b&gt;해결&lt;/b&gt;된 것으로 보인다.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot; data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hibernate/hibernate-orm/blob/main/changelog.txt&quot;&gt;https://github.com/hibernate/hibernate-orm/blob/main/changelog.txt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hibernate.org/orm/releases/6.0&quot;&gt;https://hibernate.org/orm/releases/6.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/article&gt;</description>
      <category>Server/Spring JPA</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/388</guid>
      <comments>https://jaehoney.tistory.com/388#entry388comment</comments>
      <pubDate>Sun, 5 Nov 2023 15:31:29 +0900</pubDate>
    </item>
    <item>
      <title>Spring의 Servlet에 X-Forwarded-For 헤더가 왜 안들어올까?!</title>
      <link>https://jaehoney.tistory.com/387</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&amp;nbsp;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;아래는 실무 중에 만난 이슈와 해결 과정에 대해 다룬다.&lt;/span&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정확한 원인&lt;/b&gt;과 &lt;b&gt;해결 방법&lt;/b&gt;보다는 &lt;b&gt;풀어나간 과정&lt;/b&gt;에 집중되어 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원인과 해결 방안이 궁금한 분은 가장 아래로 내리시면 될 것 같습니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;헤더에 X-Forwarded-For이 안들어오는 문제 발견&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 공통 라이브러리의 코드의 일부 코드를 요약한 것이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class HeaderUtil {

    static public HeaderInfo initHeaderInfo(HttpServletRequest request){
        return HeaderInfo.builder()
                .token(request.getHeader(HeaderInfo.JWT_HEADER))
                .clientIp(request.getHeader(&quot;X-Forwarded-For&quot;))
                .referer(request.getHeader(&quot;Referer&quot;))
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter에서 HeaderInfo에 있는 &lt;code&gt;clientIp&lt;/code&gt; 정보를 &lt;code&gt;UserDetails&lt;/code&gt;의 구현체에 넣어서 사용한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class SecurityUser implements UserDetails {
    // ...
    private String clientIp;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 해당 유저 정보에서 &lt;code&gt;clientIp&lt;/code&gt;를 꺼내서 저장하는데 아래와 같이 null이라고 나온다. 로컬, 실서버 모두 null으로 나왔다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;21&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m6yNu/btsJpqAaI6I/USeRQcXvUxd4wOKVxou3g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m6yNu/btsJpqAaI6I/USeRQcXvUxd4wOKVxou3g1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m6yNu/btsJpqAaI6I/USeRQcXvUxd4wOKVxou3g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm6yNu%2FbtsJpqAaI6I%2FUSeRQcXvUxd4wOKVxou3g1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;772&quot; height=&quot;21&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;21&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;clientIp&lt;/code&gt;를 사용하던 기존의 소스를 실서버에서 디버깅해봤는데, 전부 Null 또는 Empty로 처리되어 저장되고있었다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;물어보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동료에게 이슈를 알린 후 여쭤봤는데, 왜 안되는 지는 모르지만 다른 헤더 값을 뒤져서 Ip를 추출하고 있었다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;'다른 사람들이 불편을 겪기 전에 내가 해결하자!'&lt;/b&gt; 라고 생각을 하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 다른 헤더가 들어오고 있어서 그거에 맞게 헤더만 수정하면 해결은 되었지만, 확인해본 결과 &lt;code&gt;X-Forwarded-For&lt;/code&gt;를 사용해야 한다고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인 지점 파악&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 필터를 디버깅 용도로 추가했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class HeaderLoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        Enumeration&amp;lt;String&amp;gt; headerNames = req.getHeaderNames();
        while(headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            String headerValue = req.getHeader(headerName);
            System.out.println(&quot;Header Name: &quot; + headerName + &quot;, Header Value: &quot; + headerValue);
        }
        chain.doFilter(request, response);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로깅 지점을 정리하면 아래와 같다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1077&quot; data-origin-height=&quot;443&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKoU4R/btsJoNQosev/SQ6Q0sVjqCUtqIhfccaenK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKoU4R/btsJoNQosev/SQ6Q0sVjqCUtqIhfccaenK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKoU4R/btsJoNQosev/SQ6Q0sVjqCUtqIhfccaenK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKoU4R%2FbtsJoNQosev%2FSQ6Q0sVjqCUtqIhfccaenK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1077&quot; height=&quot;443&quot; data-origin-width=&quot;1077&quot; data-origin-height=&quot;443&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 문제 원인 후보를 두 가지로 나눌 수 있었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 컨테이너 설정이 문제인 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 컨테이너 설정이 다른 경우 원인이 될 수도 있어 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만 Node와 PHP로 작성된 코드의 경우는 &lt;code&gt;X-Forwarded-For&lt;/code&gt;이 잘 들어왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프로젝트들도 CI/CD가 Java 기반 서버와 거의 동일해서 실마리를 찾지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리버스 프록시도 동일한 서버를 사용하고 있었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. ServletContainer가 문제인 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 Logging한 지점은 Servlet(Filter)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ServletContainer(Tomcat)이 헤더에 관여한다면 &lt;code&gt;X-Forwarded-For&lt;/code&gt;이 없는 것도 고려할 수 있는 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만 &lt;code&gt;Tomcat&lt;/code&gt;이 헤더에 관여할 이유가 없다고 생각했다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지옥의 디버깅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번의 경우 문제가 될 수 잇는 지점들을 하나씩 증명하면서 제거해갔는데 해결이 안되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서블릿 컨테이너로 들어오기 전 HTTP 원문을 까보기 어려움&lt;/li&gt;
&lt;li&gt;리버스 프록시에서 헤더가 정말 나온 것이 맞는 지 의심이 커져감
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔지니어 분께 보여달라고 요청 드려봤으나 다른 프로젝트(Node, PHP)도 설정이 동일한데 잘 나온다고만 말씀해주셨다. (확인이 어려우신가 보다..)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 2번인 ServletContainer(Tomcat)을 의심해봤는데 로컬에서는 헤더가 잘 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;2번은 절대 아닐 것 같다고 생각&lt;/b&gt;했고, 실제로 적용할 수 있는 거의 모든 옵션을 적용해봤는데 해결이 안되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;server.use-forward-headers: true/false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server.forward-headers-strategy: FRAMEWORK/NATIVE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이것저것 삽질 중 &lt;b&gt;정말 중요한 단서&lt;/b&gt;를 하나 찾았다!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Local에서는 잘 되는데, k8s에서는 안되는 현상&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 &lt;code&gt;curl&lt;/code&gt;을 통해 로컬에서 띄운 서버(&lt;code&gt;localhost&lt;/code&gt;)로 요청을 보냈다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;curl --location --request PATCH 'http://localhost:8080/product/123456' \
--header 'X-Forwarded-For: 99.99.99.99' \
--header 'Custom: 22.22.22.22' \
--header 'X-Forwarded-Jerry: 11.11.11.11' \
--header 'Content-Type: application/json' \
--data '{
  &quot;memo&quot;: &quot;Test Memo&quot;,
}'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 &lt;code&gt;X-Forwarded-For&lt;/code&gt; 헤더까지 잘 나왔다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;419&quot; data-origin-height=&quot;81&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br8OqE/btsJnGEtqEs/j67FOY8KjQU6MWuSdEMeh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br8OqE/btsJnGEtqEs/j67FOY8KjQU6MWuSdEMeh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br8OqE/btsJnGEtqEs/j67FOY8KjQU6MWuSdEMeh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr8OqE%2FbtsJnGEtqEs%2Fj67FOY8KjQU6MWuSdEMeh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;419&quot; height=&quot;81&quot; data-origin-width=&quot;419&quot; data-origin-height=&quot;81&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 &lt;b&gt;Container&lt;/b&gt;에서는 달랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 서버에서 &lt;code&gt;localhost&lt;/code&gt;로 동일한 curl을 날리니까&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;41&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgYfng/btsJm8OYtuF/5vTecaFA9JxLIidPEPAYF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgYfng/btsJm8OYtuF/5vTecaFA9JxLIidPEPAYF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgYfng/btsJm8OYtuF/5vTecaFA9JxLIidPEPAYF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgYfng%2FbtsJm8OYtuF%2F5vTecaFA9JxLIidPEPAYF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;536&quot; height=&quot;41&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;41&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;X-Forwarded-For&lt;/code&gt;만 &lt;b&gt;사라진 것&lt;/b&gt;을 확인할 수 있었다!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원인이 될만한 지점 파악&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 환경 모두 동일한 소스코드 였기에 아래의 것들을 의심했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jar 실행 스크립트&lt;/li&gt;
&lt;li&gt;jdk 파일&lt;/li&gt;
&lt;li&gt;컨테이너 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지 모두 확인해봤으나, 범인이 아니었고 쟤네가 Request Header에 관여를 할 것 같다는 생각도 전혀 안들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;Tomcat&lt;/b&gt; 관련 설정을 모두 디버깅해보다가 결국 원인을 찾을 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;server.tomcat.remoteIp.remoteIpHeader&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 &lt;code&gt;server.tomcat.remoteIp.remoteIpHeader&lt;/code&gt; 프로퍼티에 대한 설명인데 &lt;code&gt;X-Forwarded-For&lt;/code&gt; 헤더에 대한 내용이 나왔다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdzZXj/btsJpuo2oTV/Q8DRvtY66En2KwpKH0C1g0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdzZXj/btsJpuo2oTV/Q8DRvtY66En2KwpKH0C1g0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdzZXj/btsJpuo2oTV/Q8DRvtY66En2KwpKH0C1g0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdzZXj%2FbtsJpuo2oTV%2FQ8DRvtY66En2KwpKH0C1g0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;853&quot; height=&quot;278&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;직역하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;원격 IP를 추출할 HTTP 헤더의 이름입니다. 예를 들어 'X-FORWARDED-FOR'입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 살펴보니 &lt;code&gt;TomcatWebServerFactoryCustomizer&lt;/code&gt;는 해당 프로퍼티가 존재한다면 &lt;code&gt;RemoteIpValue&lt;/code&gt;에 세팅하고 있었다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;793&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TQiWO/btsJooiZxDA/mqjLu7yiWIrwHkRYYKKwR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TQiWO/btsJooiZxDA/mqjLu7yiWIrwHkRYYKKwR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TQiWO/btsJooiZxDA/mqjLu7yiWIrwHkRYYKKwR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTQiWO%2FbtsJooiZxDA%2FmqjLu7yiWIrwHkRYYKKwR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;899&quot; height=&quot;793&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;793&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;RemoteIpValve&lt;/code&gt;의 &lt;code&gt;remoteIpHeader&lt;/code&gt; 필드의 기본 값이 &lt;code&gt;X-Forwarded-For&lt;/code&gt; 이었다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EPjKu/btsJnJnHMtI/0gEWDzL7PWfPBEcRTuLxk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EPjKu/btsJnJnHMtI/0gEWDzL7PWfPBEcRTuLxk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EPjKu/btsJnJnHMtI/0gEWDzL7PWfPBEcRTuLxk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEPjKu%2FbtsJnJnHMtI%2F0gEWDzL7PWfPBEcRTuLxk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;117&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;117&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;해당 클래스의 &lt;b&gt;공식 문서&lt;/b&gt;를 들어가서 해당 필드에 대한 설명을 봤다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/catalina/valves/RemoteIpValve.html&quot;&gt;https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/catalina/valves/RemoteIpValve.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 필드에 대한 기본값은 &lt;code&gt;X-Forwarded-For&lt;/code&gt;이고&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2519&quot; data-origin-height=&quot;455&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eqnuzA/btsJpqmFuFk/FHRDlaaCTy9jrgACKu0FXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eqnuzA/btsJpqmFuFk/FHRDlaaCTy9jrgACKu0FXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eqnuzA/btsJpqmFuFk/FHRDlaaCTy9jrgACKu0FXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeqnuzA%2FbtsJpqmFuFk%2FFHRDlaaCTy9jrgACKu0FXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2519&quot; height=&quot;455&quot; data-origin-width=&quot;2519&quot; data-origin-height=&quot;455&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Valve&lt;/code&gt;를 거치기 전에는 Header에 값이 있다가, &lt;b&gt;Valve를 거친 후에는 Header의 값을 삭제&lt;/b&gt;한다고 한다!!! 헤더 값은 &lt;b&gt;request.remoteAddr에 저장한다.&lt;/b&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1830&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b39Eq3/btsJoA4B5ZL/fpPptkAlb3kgR0BkIayNX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b39Eq3/btsJoA4B5ZL/fpPptkAlb3kgR0BkIayNX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b39Eq3/btsJoA4B5ZL/fpPptkAlb3kgR0BkIayNX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb39Eq3%2FbtsJoA4B5ZL%2FfpPptkAlb3kgR0BkIayNX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1830&quot; height=&quot;378&quot; data-origin-width=&quot;1830&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Deque에 처리할 Remote IP 헤더가 존재하지 않을 경우 &lt;code&gt;removeHeader()&lt;/code&gt;를 통해 삭제하고 있었다!&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;643&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHGcla/btsJnXstifK/UK1QDpi3PtP88bjQkkgLk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHGcla/btsJnXstifK/UK1QDpi3PtP88bjQkkgLk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHGcla/btsJnXstifK/UK1QDpi3PtP88bjQkkgLk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHGcla%2FbtsJnXstifK%2FUK1QDpi3PtP88bjQkkgLk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;643&quot; height=&quot;138&quot; data-origin-width=&quot;643&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 원인은 &lt;b&gt;Servlet Container&lt;/b&gt;가 맞았다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬에서는 잘 되던 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TomcatWebServerFactoryCustomizer&lt;/code&gt;는 &lt;code&gt;customizeRemoteIpValue&lt;/code&gt;의 로직을 아래 조건이 통과하지 않으면 실행하지 않는다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1315&quot; data-origin-height=&quot;501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQAKpe/btsJnrtZPMz/hyt4xrObRNVgodDQhRtCfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQAKpe/btsJnrtZPMz/hyt4xrObRNVgodDQhRtCfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQAKpe/btsJnrtZPMz/hyt4xrObRNVgodDQhRtCfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQAKpe%2FbtsJnrtZPMz%2Fhyt4xrObRNVgodDQhRtCfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1315&quot; height=&quot;501&quot; data-origin-width=&quot;1315&quot; data-origin-height=&quot;501&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;getOrDeduceUseForwardHeaders()&lt;/code&gt;는 아래와 같이 &lt;code&gt;CloudPlatform&lt;/code&gt;이 실행중이면 무조건 true를 반환한다. (&lt;code&gt;platform.isUsingForwardedHeaders()&lt;/code&gt;는 무조건 true를 반환한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CloudPlatform&lt;/code&gt;은 클라우드 여부에 대한 감지를 위한 Enum 이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/scYAa/btsJnGYLgHK/WDSoL4K1DO2qRtPtK6yW50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/scYAa/btsJnGYLgHK/WDSoL4K1DO2qRtPtK6yW50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/scYAa/btsJnGYLgHK/WDSoL4K1DO2qRtPtK6yW50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FscYAa%2FbtsJnGYLgHK%2FWDSoL4K1DO2qRtPtK6yW50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;840&quot; height=&quot;152&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서는 클라우드 플랫폼이 아니므로 &lt;code&gt;false&lt;/code&gt;가 되어서 &lt;code&gt;Valve&lt;/code&gt;가 동작하지 않았던 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 내가 예상은 했지만 아닐 것이라고 믿었던 &lt;b&gt;ServletContainer(Tomcat)&lt;/b&gt;가 문제였고, 거기서 헤더를 지우는 것이 맞았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결은 코드의 &lt;code&gt;request.getHeader(&quot;X-Forwarded-For&quot;)&lt;/code&gt;를 &lt;code&gt;request.getRemoteAddr()&lt;/code&gt;로 교체해서 간단하게 해결할 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class HeaderUtil {

    static public HeaderInfo initHeaderInfo(HttpServletRequest request){
        return HeaderInfo.builder()
                .token(request.getHeader(HeaderInfo.JWT_HEADER))
                .clientIp(request.getRemoteAddr())
                .referer(request.getHeader(&quot;Referer&quot;))
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적용 후 clientIp가 잘 찍히는 것을 확인&lt;/b&gt;할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 &lt;code&gt;X-Forwarded-For&lt;/code&gt; 헤더가 없을 경우 아래의 로직을 타게 된다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kRKDV/btsJnglVKKG/pkvKAAVaEK7LQLKAWLnkI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kRKDV/btsJnglVKKG/pkvKAAVaEK7LQLKAWLnkI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kRKDV/btsJnglVKKG/pkvKAAVaEK7LQLKAWLnkI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkRKDV%2FbtsJnglVKKG%2FpkvKAAVaEK7LQLKAWLnkI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;680&quot; height=&quot;468&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;해당 부분은 &lt;code&gt;SocketWrapper&lt;/code&gt;에서 &lt;b&gt;출발지 IP&lt;/b&gt;를 가지고 온다. 실제로 서버에서 &lt;code&gt;X-Forwarded-For&lt;/code&gt;가 없을 때 GateWay 서버의 IP가 찍히는 것을 확인했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/catalina/valves/RemoteIpValve.html&quot;&gt;https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/catalina/valves/RemoteIpValve.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/tree/main/spring-web&quot;&gt;https://github.com/spring-projects/spring-framework/tree/main/spring-web&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/tomcat&quot;&gt;https://github.com/apache/tomcat&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>Server/Spring</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/387</guid>
      <comments>https://jaehoney.tistory.com/387#entry387comment</comments>
      <pubDate>Thu, 26 Oct 2023 08:29:05 +0900</pubDate>
    </item>
    <item>
      <title>단위 테스트 대상 분리하기!</title>
      <link>https://jaehoney.tistory.com/386</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;아래는 최범균님의 유튜브를 보고 느낀 점을 나름대로 정리한 것이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=qZ1R0C_iiV4&quot;&gt;https://www.youtube.com/watch?v=qZ1R0C_iiV4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;테스트-불가능한-문제&quot;&gt;테스트 불가능한 문제&lt;/h2&gt;
&lt;p&gt;아래의 코드가 테스트가 불가능한 문제가 있었다고 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;ResultBuilder builder = ...;
InputPeriod realPeriod = mapper.selectPeriod(param); // 1. DB에서 읽음
if (realPeriod == null) { // 2. 없으면 다른 값 읽음
    InputPeriod expectedPeriod = mapper.selectExpectedPeriod(otherParam);
    builder.period(expectedPeriod).type(EXPECTED);
} else if (realPeriod.getStart() == null || realPeriod.getEnd() == null) { // 3. 데이터는 있는데 실제 값이 없으면
    builder.period(null).type(EXPECTED);
} else {
    // 4. 데이터의 값이 있고 아래 조건을 충족하면
    if((today.isEqual(real.getStart()) || today.isEqual(real.getEnd())) ||
        (today.isAfter(real.getStart()) &amp;amp;&amp;amp; today.isBefore(real.getEnd()))
    ) {
        builder.period(realPeriod).type(SCHEDULED);
    } else { // 5. (4) 조건이 아니면
        builder.period(realPeriod).type(EXPECTED);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;realPeriod, expectedPeriod, today 값에 따라 사용할 period와 type이 달라지기 때문&lt;/p&gt;
&lt;h2 id=&quot;단위-테스트&quot;&gt;단위 테스트&lt;/h2&gt;
&lt;p&gt;단위 테스트를 방해하는 요소는 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;중간에 섞여 있는 DB 연동&lt;/li&gt;
&lt;li&gt;ResultBuilder를 만드는 과정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 아래의 로직으로 분리&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DB에서 InputPeriod를 읽어오는 코드&lt;/li&gt;
&lt;li&gt;realPeriod, expectedPeriod, today로 사용할 period와 type 구하기&lt;/li&gt;
&lt;li&gt;코드 분리를 위한 타입 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;데이터-타입-추가&quot;&gt;데이터 타입 추가&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class InputPeriods {
    private InputPeriod real;
    private InputPeriod expected;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;db에서-읽어오는-코드-분리&quot;&gt;DB에서 읽어오는 코드 분리&lt;/h2&gt;
&lt;p&gt;DB에서 읽어오는 부분을 아래와 같이 분리했다. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;private InputPeriods selectPeriod(Param param, OtherParam otherParam) {
    InputPeriod realPeriod = mapper.selectPeriod(param);
    if(realPeriod != null) return new InputPeriods(realPeriod, null);
    InputPeriod expectedPeriod = mapper.selectExpectedPeriod(otherParam);
    return new InputPeriods(null, expectedPeriod);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과적으로 코드가 아래와 같이 변경되었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;ResultBuilder builder = ...;
// InputPeriod realPeriod = mapper.selectPeriod(param);
InputPeriods periods = selectPeriod(param, otherParam);
if (realPeriod == null) {
    // InputPeriod expectedPeriod = mapper.selectExpectedPeriod(otherParam);
    // builder.period(expectedPeriod).type(EXPECTED);
    builder.period(periods.getExpected()).type(EXPECTED);
} else if (realPeriod.getStart() == null || realPeriod.getEnd() == null) {
    builder.period(null).type(EXPECTED);
} else {
    if((today.isEqual(real.getStart()) || today.isEqual(real.getEnd())) ||
        (today.isAfter(real.getStart()) &amp;amp;&amp;amp; today.isBefore(real.getEnd()))
    ) {
        builder.period(realPeriod).type(SCHEDULED);
    } else {
        builder.period(realPeriod).type(EXPECTED);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;period-type-구하는-로직을-inputperiods-클래스로-이동&quot;&gt;period, type 구하는 로직을 InputPeriods 클래스로 이동&lt;/h2&gt;
&lt;p&gt;period, type을 build하는 과정을 삭제하기 위해 로직을 Data 객체에 위임했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class InputPeriods {
    private InputPeriod real;
    private InputPeriod expected;

    public InputPeriod getPeriod() {
        if(real == null) return expected;
        if(real.getStart() == null || real.getEnd() == null) return null;
        return expected;
    }

    public PeriodType getType(LocalDate today) {
        if(real == null) return EXPECTED;
        if(realPeriod.getStart() == null || realPeriod.getEnd() == null) return EXPECTED;
        if((today.isEqual(real.getStart()) || today.isEqual(real.getEnd())) || 
            (today.isAfter(real.getStart()) &amp;amp;&amp;amp; today.isBefore(real.getEnd()))
        ) return SCHEDULED;
        return EXPECTED;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 LocalDate로 today라는 파라미터를 내부에서 수행하지 않는 이유는 제어가 가능해야 하기 때문이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://jojoldu.tistory.com/683&quot;&gt;https://jojoldu.tistory.com/683&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;변경-후&quot;&gt;변경 후&lt;/h2&gt;
&lt;p&gt;결과적으로 아래와 같이 깔끔한 코드가 되었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;ResultBuilder builder = ...;
InputPeriods periods = selectPeriod(param, otherParam);
builder.period(periods.getPeriod()).type(periods.getType(today));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;결과&quot;&gt;결과&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;코드가 단순해졌음&lt;/li&gt;
&lt;li&gt;로직에 대한 단위 테스트가 쉬워짐&lt;ul&gt;
&lt;li&gt;DB 연동 필요 없음 (Mocking 필요 X)&lt;/li&gt;
&lt;li&gt;실제 테스트하고 싶은 대상에만 초점&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;로직 변경/기능/추가가 쉬워짐&lt;ul&gt;
&lt;li&gt;관련 로직이 한 곳에 모임 -&amp;gt; 변경할 곳도 한 곳으로 모임&lt;/li&gt;
&lt;li&gt;데이터가 한 클래스에 있음 -&amp;gt; 데이터(expectedPeriod, realPeriod)와 관련된 기능 추가는 해당 클래스&lt;/li&gt;
&lt;li&gt;코드가 흩어지는 문제를 방지&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;
&lt;p&gt;나도 개발하면서 영속성 계층에서 (저 만큼 복잡하지는 않았지만) 로직을 수행해서 가져와야 하는 경우가 일부 있었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ALTER(Utf8mb4 적용)를 할 수 없어서 임시 테이블에 데이터가 존재 시 해당 테이블에서 데이터를 꺼내와야 했던 문제 &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;나도 JPA Entity에서 로직을 구현해서 테스트를 했었다. 내가 한건 처음 코드를 작성한 것이었고 기존의 코드를 리팩토링하는 것은 조금 더 어려운 것 같다.&lt;/p&gt;
&lt;p&gt;로직이 수행이 많아서 단위 테스트를 작성하기가 어렵다면 수행할 로직에서 &lt;strong&gt;테스트할 대상이 정말 1가지가 맞는 지 확인&lt;/strong&gt;하고, &lt;strong&gt;2가지 이상이라면 테스트할 대상을 분리&lt;/strong&gt;해보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQL을 실행하는 부분에서는 가능한 로직을 담지 말고 &lt;strong&gt;저장소의 역할만&lt;/strong&gt; 해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=qZ1R0C_iiV4&quot;&gt;https://www.youtube.com/watch?v=qZ1R0C_iiV4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jojoldu.tistory.com/683&quot;&gt;https://jojoldu.tistory.com/683&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Programming/Refactoring</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/386</guid>
      <comments>https://jaehoney.tistory.com/386#entry386comment</comments>
      <pubDate>Mon, 23 Oct 2023 08:48:36 +0900</pubDate>
    </item>
    <item>
      <title>Accept 헤더가 포함된 REST API에서 에러를 내려주는 방법!</title>
      <link>https://jaehoney.tistory.com/385</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;이번 포스팅은 회사에서 메일 원문 다운로드 API를 개발하면서 생긴 이슈에 대해 공유한다.&lt;/p&gt;
&lt;p&gt;이해하기 쉽도록 메일 원문 대신 &lt;strong&gt;첨부 파일&lt;/strong&gt;로 재해석해서 작성했다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;요즘은 파일 스토리지로 외부 인프라를 많이 사용해서 직접 설계해야 되는 상황이 드물긴 하다. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;파일-접근&quot;&gt;파일 접근&lt;/h2&gt;
&lt;p&gt;게시판의 첨부파일 기능을 생각해보자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BqCC1/btsEjIt8cah/ZtCzZ74aQVlqtTQanKBWVk/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;메타 데이터에는 빨간 네모와 같이 파일명과 확장자, 파일 사이즈, 다운로드 경로 등이 저장될 것이다. 다운로드를 클릭하면 실제 파일 스토리지에 접근해서 파일을 Binary 형태로 가져올 것이다.&lt;/p&gt;
&lt;p&gt;게시판을 노출하기 위해서 메타 데이터를 가져올 때 파일 정보까지 모두 가져오면 오버헤드가 발생할 것이다. 그래서 2개의 End-point로 분리했다.&lt;/p&gt;
&lt;h2 id=&quot;accept&quot;&gt;Accept&lt;/h2&gt;
&lt;p&gt;REST API에서는 url에 자원에 대한 경로를 지정한다.&lt;/p&gt;
&lt;p&gt;아래의 예시를 보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GET /boards/:boardId/attachments/:attachmentId&lt;/li&gt;
&lt;li&gt;POST /board/:boardId/attachments/:attachmentId/download&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;좋아보일 수도 있지만 &lt;strong&gt;REST API&lt;/strong&gt;에서 &lt;strong&gt;좋은 설계는 아니다.&lt;/strong&gt; url에 행위에 대한 설명이 들어있기 때문이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;참고: &lt;a href=&quot;https://meetup.nhncloud.com/posts/92&quot;&gt;https://meetup.nhncloud.com/posts/92&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;REST API에서는 &lt;strong&gt;헤더를 활용&lt;/strong&gt;하기를 권장한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GET /boards/:boardId/attachments/:attachmentId&lt;/li&gt;
&lt;li&gt;GET /boards/:boardId/attachments/:attachmentId Accept: application/octet-stream&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Accept 헤더&lt;/strong&gt;에 &lt;code&gt;application/octet-stream&lt;/code&gt;을 넣으면 &lt;strong&gt;해당 포맷의 데이터로 응답해달라&lt;/strong&gt;는 뜻이다.&lt;/p&gt;
&lt;h2 id=&quot;예시-코드&quot;&gt;예시 코드&lt;/h2&gt;
&lt;p&gt;아래는 스프링 애플리케이션에서 해당 요구사항을 적용한 예시이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@RestController
@RequestMapping(&quot;/attachments&quot;)
class AttachmentController {

    @GetMapping(&quot;/{attachmentId}&quot;)
    public ResponseEntity&amp;lt;Attachment&amp;gt; getAttachmentMeta(@PathVariable String attachmentId) {
        Attachment attachment = new Attachment(attachmentId, &quot;test.img&quot;, &quot;/path&quot;);
        return ResponseEntity.ok(attachment);
    }

    @GetMapping(value = &quot;/{attachmentId}&quot;, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public ResponseEntity&amp;lt;Resource&amp;gt; getAttachmentData(@PathVariable String attachmentId) throws IOException {
        InputStream is = new FileInputStream(getFile(attachmentId));
        Resource resource = new InputStreamResource(is);
        return ResponseEntity.ok(resource);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제로 요청을 해보면 아래와 같이 &lt;strong&gt;JSON&lt;/strong&gt; 형태의 메타 데이터 응답이 잘 나온다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bekX7q/btsEqxqlHfu/0DV4SRH3zve4AK1kp3Nwf0/img.png&quot; alt=&quot;img_2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;아래와 같이 Accept 헤더를 넣었을 때도 &lt;strong&gt;바이너리 데이터&lt;/strong&gt;가 잘 나오는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kZbTI/btsEj6VUzcs/fE4L5cFWJ6sbKfnsBmrHC0/img.png&quot; alt=&quot;img_3.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;에러-핸들링-문제&quot;&gt;에러 핸들링 문제&lt;/h2&gt;
&lt;p&gt;그런데 &lt;strong&gt;에러 처리&lt;/strong&gt;가 되면 어떻게 될까? 각 컨트롤러 메서드에 아래 부분을 추가했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;if(attachmentId.isEquals(&quot;1&quot;)) {
    throw new RuntimeException();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;메타 데이터 조회의 경우 잘 핸들링이 되었지만&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GK6Ot/btsEmXDOSGj/Hg2USDKsKSIGux3WxxKP41/img.png&quot; alt=&quot;img_8.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;아래와 같이 파일 데이터 조회의 경우 &lt;strong&gt;응답 Payload가 전혀 없었다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YJ0T6/btsElHIeGei/i1SkKV5xp3mzFDzcvDmQiK/img.png&quot; alt=&quot;img_9.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;'PostMan의 문제가 아닐까..?' 생각했지만 아래와 같이 &lt;strong&gt;서버에서 내려주는 Content-Length가 0&lt;/strong&gt;이었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BdnCx/btsEmSoMJLQ/RhNhZTPzYfDzO0AuahCgR1/img.png&quot; alt=&quot;img_7.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;abstractmessageconvertermethodprocessor&quot;&gt;AbstractMessageConverterMethodProcessor&lt;/h2&gt;
&lt;p&gt;위 두 가지 End-point를 각각 호출 후 디버깅한 결과 아래 사실을 알 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Accept&lt;/code&gt;가 &lt;code&gt;*/*&lt;/code&gt; 또는 &lt;code&gt;application/json&lt;/code&gt;으로 보낸 경우에는 &lt;code&gt;AbstractMessageConverterMethodProcessor&lt;/code&gt;의 &lt;code&gt;writeWithMessageConverters&lt;/code&gt; 동작 중 &lt;code&gt;compatibleMediaTypes&lt;/code&gt;의 size가 2가 나와서 예외가 터지지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJFEtr/btsEmMIO4GZ/kk4MKkokJ6Eh9KsJyGof90/img.png&quot; alt=&quot;img_12.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;compatibleMediaTypes&lt;/code&gt;는 아래와 같았다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duTZiF/btsEmWZbhnU/3T8WJh8TjYjvvZkuuKVSVK/img.png&quot; alt=&quot;img_13.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;결과적으로 아래와 같이 &lt;strong&gt;Body에 에러 메시지를 쓰고&lt;/strong&gt; &lt;strong&gt;flush&lt;/strong&gt; 할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IQt4y/btsEmWZbhn8/L48z1gD7ukpTGuv2j6KSkk/img.png&quot; alt=&quot;img_11.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;accept-applicationoctet-stream&quot;&gt;Accept: application/octet-stream&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Accept: application/octet-stream&lt;/code&gt;으로 요청을 보낸 것을 디버깅 한 결과 &lt;code&gt;compatibleMediaTypes&lt;/code&gt;가 비어있어서 예외가 터지고 Body에 데이터를 쓰지 못했다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bM0UMS/btsEm8rxkyd/1qFc623K2k3QP9smDFhCAk/img.png&quot; alt=&quot;img_14.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;compatibleMediaTypes&lt;/code&gt;는 &lt;code&gt;Accept&lt;/code&gt; 헤더로 들어온 값 (&lt;code&gt;application/octet-stream&lt;/code&gt;)이 &lt;strong&gt;Body 데이터와 호환 되는 타입&lt;/strong&gt;의 리스트를 반환한다. &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pRual/btsEkvOAurv/dKhTcKACUfcaUck8v0jVD0/img.png&quot; alt=&quot;img_15.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;반환된 결과로 &lt;code&gt;application/octet-stream&lt;/code&gt;으로 &lt;strong&gt;Body를 나타낼 수 없다고 판단&lt;/strong&gt;했기에 Spring이 데이터를 내려주지 않은 것이다.&lt;/p&gt;
&lt;h2 id=&quot;해결---applicationproblemjson를-추가&quot;&gt;해결 - application/problem+json를 추가&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Accept&lt;/code&gt; 헤더는 복수 개를 명시할 수 있다. 여기서 &lt;code&gt;application/problem+json&lt;/code&gt;을 사용한다면 이를 해결할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;+&lt;/code&gt;는 RFC 6838에서 지원하는 &lt;code&gt;suffix&lt;/code&gt;이며 MediaType의 기본 구조를 지정한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;problem&lt;/code&gt; 뿐 아니라 Custom한 다른 용어를 사용해도 정상적으로 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8VAMI/btsEm70r9Rb/Pp5ZZOwOcX1NJFHAV6Sxyk/img.png&quot; alt=&quot;img_17.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d0z7Gj/btsEmPFzGhV/3XlZWK8lwypvozZ8za6dKK/img.png&quot; alt=&quot;img_16.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;application/problem+json&lt;/code&gt;은 RFC 7807의 스펙이며, &lt;code&gt;application/json&lt;/code&gt;과 호환된다.&lt;/p&gt;
&lt;p&gt;Accept에 정상적일 때 받을 동작과, 에러 메시지로 받을 형식을 둘다 명시해서 에러 메시지도 API가 내려줄 수 있도록 정보를 전달해서 해결할 수 있었다.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://meetup.nhncloud.com/posts/92&quot;&gt;https://meetup.nhncloud.com/posts/92&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6838&quot;&gt;https://datatracker.ietf.org/doc/html/rfc6838&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7807&quot;&gt;https://datatracker.ietf.org/doc/html/rfc7807&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Server/Spring</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/385</guid>
      <comments>https://jaehoney.tistory.com/385#entry385comment</comments>
      <pubDate>Fri, 20 Oct 2023 08:14:19 +0900</pubDate>
    </item>
    <item>
      <title>자바에서 동시성을 다룰 때 주의할 점!</title>
      <link>https://jaehoney.tistory.com/384</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;아래는 이펙티브 자바의 내용 중 &lt;strong&gt;동시성&lt;/strong&gt;에 대한 부분을 정리한 것이다.&lt;/p&gt;
&lt;p&gt;Effective Java는 동시성을 사용할 때의 몇가지 주의사항과 가이드라인을 제시한다.&lt;/p&gt;
&lt;h2 id=&quot;동기화된-메서드-설계-시-주의할-점&quot;&gt;동기화된 메서드 설계 시 주의할 점&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;동기화된 메서드&lt;/strong&gt;를 작성할 때 중요한 것은 &lt;strong&gt;재정의할 수 있는 메서드&lt;/strong&gt;를 호출해선 안되고 클라이언트가 넘겨준 &lt;strong&gt;함수 객체&lt;/strong&gt;도 사용하면 안된다는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class ObservableSet&amp;lt;E&amp;gt; extends ForwardingSet&amp;lt;E&amp;gt; {

    public void addObserver(SetObserver&amp;lt;E&amp;gt; observer) {
        synchronized (observers) {
            observers.add(observer);
        }
    }

    public boolean removeObserver(SetObserver&amp;lt;E&amp;gt; observer) {
        synchronized (observers) {
            return observers.remove(observer);
        }
    }

    private void notifyElementAdded(E element) {
        synchronized (observers) {
            for(SetObserver&amp;lt;E&amp;gt; observer : observers) {
                observer.added(this, element);
            }
        }
    }

    @Override
    public boolean add(E element) {
        boolean added = super.add(element);
        if(added) {
            notifyElementAdded(element);
        }
        return added;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 코드를 보면 &lt;code&gt;add()&lt;/code&gt; 내부적으로 &lt;code&gt;notifyElementAdded()&lt;/code&gt; 메서드를 호출하고 있다.&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;add()&lt;/code&gt; 입장에서 보면 &lt;code&gt;notifyElementAdd()&lt;/code&gt;는 바깥 세상에서 온 외계인 영역이다. 그 메서드가 무슨 일을 할 지도 모르고 통제할 방법도 없다.&lt;/p&gt;
&lt;p&gt;외부 메서드는 동기화된 영역의 일관성을 해치거나 교착상태에 빠지게할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static void main(String[] args) {
    ObservableSet&amp;lt;Integer&amp;gt; set = new ObservableSet&amp;lt;&amp;gt;(New HashSet&amp;lt;&amp;gt;());

    set.addObserver(new SetObserver&amp;lt;Integer&amp;gt;() {
        public void added(ObservableSet&amp;lt;Integer&amp;gt; s, Integer e) {
            System.out.println(e);
            if (e == 23) s.removeObserver(this);
        }
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 함수를 실행할 경우 23번째 반복에서 &lt;code&gt;ConcurrentModificationException&lt;/code&gt;이 발생한다. &lt;code&gt;added()&lt;/code&gt;를 호출한 시점이 &lt;code&gt;notifyElementAdded&lt;/code&gt;가 &lt;code&gt;observers&lt;/code&gt;를 순회하고 있는 지점이기 때문이다.&lt;/p&gt;
&lt;p&gt;동기화 영역에서는 가능한 일을 적게 해야 하며, 외부 메서드를 호출해서는 안된다.&lt;/p&gt;
&lt;p&gt;가변 클래스를 작성할 때는 두 선택지 중 한 가지를 선택하고, 선택한 부분을 문서화를 해야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;동기화를 하지 않고 그 클래스를 사용하는 클래스가 동기화하게 만들기&lt;/li&gt;
&lt;li&gt;동기화를 내부에서 수행하기&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;동시성-유틸리티-사용하기&quot;&gt;동시성 유틸리티 사용하기&lt;/h2&gt;
&lt;p&gt;자바에서 &lt;code&gt;wait()&lt;/code&gt;과 &lt;code&gt;notify()&lt;/code&gt; 등 스레드 동기화를 위한 메서드를 제공한다. 그러나 이는 &lt;strong&gt;올바르게 사용하기 어렵다.&lt;/strong&gt; 대신에 고수준 &lt;strong&gt;동시성 유틸리티&lt;/strong&gt;를 사용할 수 있다.&lt;/p&gt;
&lt;h4 id=&quot;동시성-컬렉션&quot;&gt;동시성 컬렉션&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;ConcurrentHashMap&lt;/code&gt;을 사용한다면 내부에서 동시성을 제어할 수 있다. 여기서도 주의해야 할 것이 단순히 &lt;code&gt;put()&lt;/code&gt;, &lt;code&gt;isEmpty()&lt;/code&gt;를 활용해서 코드 블록에서 작성한다면 동기화되지 않는 부분이 생길 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;private static final ConcurrentMap&amp;lt;String, String&amp;gt; map = new ConcurrentHashMap&amp;lt;&amp;gt;();

public static String intern(String s) {
    String result = map.get(s); // 최적화: 필요할 때만 putIfAbsent()를 호출
    if (result == null) {
        result = map.putIfAbsent(s, s);
        if (result == null)
            result = s;
    }
    return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자바 8부터는 &lt;code&gt;putIfAbsent(key, value)&lt;/code&gt; 메서드와 같은 원자적인 동작을 보장하는 메서드를 지원한다. &lt;/p&gt;
&lt;h4 id=&quot;동기화-장치&quot;&gt;동기화 장치&lt;/h4&gt;
&lt;p&gt;동기화 장치로는 대표적으로 &lt;code&gt;CountDownLatch&lt;/code&gt;가 있다. 하나 이상의 스레드가 또 다른 하나 이상의 스레드 작업이 끝날 때까지 기다리게 한다.&lt;/p&gt;
&lt;p&gt;아래는 주어진 동작들을 모두 마친 후에 처리 시간을 출력하는 메서드이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static long time(Executor executor, int concurrency,
                            Runnable action) throws InterruptedException {
    CountDownLatch ready = new CountDownLatch(concurrency);
    CountDownLatch start = new CountDownLatch(1);
    CountDownLatch done = new CountDownLatch(concurrency);

    for (int i = 0; i &amp;lt; concurrency; i++) {
        executor.execute(() -&amp;gt; {
            // 타이머에게 준비가 됐음을 알린다.
            ready.countDown();
            try {
                // 모든 작업자 스레드가 준비될 때까지 기다린다.
                start.await();
                action.run();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                // 타이머에게 작업을 마쳤음을 알린다.
                done.countDown();
            }
        });
    }

    ready.await(); // 모든 작업자가 준비될 때까지 기다린다.
    long startNanos = System.nanoTime();
    start.countDown(); // 작업자들을 깨운다.
    done.await(); // 모든 작업자가 일을 끝마치기를 기다린다.
    return System.nanoTime() - startNanos;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;concurrency&lt;/code&gt;는 동시에 작업을 수행할 스레드의 개수이다. 위 코드는 동시에 여러 개의 작업을 모두 대기시킨 후, 동시에 작업을 수행하고 전체 실행시간을 측정한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;concurrency&lt;/code&gt;의 개수만큼 스레드를 생성하지 못하면 데드락이 생기는 점을 주의해야 한다.&lt;/p&gt;
&lt;h2 id=&quot;스레드-안전성-수준-문서화&quot;&gt;스레드 안전성 수준 문서화&lt;/h2&gt;
&lt;p&gt;API 문서에 &lt;code&gt;synchronized&lt;/code&gt;가 보인다고 스레드가 안전하다는 말은 사실이 아니다.&lt;/p&gt;
&lt;p&gt;스레드 안전성에도 수준이 나뉜다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;불변: 클래스의 인스턴스가 마치 상수와 같아서 외부 동기화도 필요 없다.&lt;/li&gt;
&lt;li&gt;무조건적 스레드 안전: 이 클래스의 인스턴스는 가변이나 내부에서 충실히 동기화하고 있다.&lt;/li&gt;
&lt;li&gt;조건부 스레드 안전: 일부 메서드를 사용하려면 외부 동기화가 필요하다.&lt;/li&gt;
&lt;li&gt;스레드 안전하지 않음: 이 클래스의 인스턴스는 수정될 수 있다. 동기화가 필요하다면 외부에서 직접 수행해야 한다.&lt;/li&gt;
&lt;li&gt;스레드 적대적: 이 클래스는 외부에서 동기화가 불가능하다. (정적 데이터를 내부에서 마음대로 수정하는 경우)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;예시로 &lt;code&gt;Collections.synchronizedMap()&lt;/code&gt;을 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K3tI9/btsyDLhvWgz/jsqpXCt008m1n8dkgEQma1/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;설명에는 아래 내용이 기술되어 있다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;synchronizedMap이 반환한 맵의 컬렉션 뷰를 순회하려면 반드시 그 맵을 락으로 사용해 수동으로 동기화하라.&lt;/p&gt;
  &lt;p&gt;코드대로 따르지 않으면 동작을 예측할 수 없다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;멀티스레드 환경에서 API를 안전하게 사용하려면 반드시 &lt;strong&gt;스레드 안전성 수준&lt;/strong&gt;을 명시해야 한다.&lt;/p&gt;
&lt;h4 id=&quot;번외---final&quot;&gt;번외 - final&lt;/h4&gt;
&lt;p&gt;만약 멀티쓰레드 환경에서 공유하는 객체를 사용하고자 한다면 final 키워드를 반드시 사용해야 한다.&lt;/p&gt;
&lt;p&gt;예시로 Singleton을 사용하는 경우에서 우리는 에러를 조기에 확인하기 위해 &lt;strong&gt;생성자 주입&lt;/strong&gt;과 &lt;strong&gt;final 키워드&lt;/strong&gt;를 활용한다.&lt;/p&gt;
&lt;p&gt;에러 조기 확인뿐 아니라 &lt;strong&gt;final 키워드&lt;/strong&gt;를 사용해야 공유하는 &lt;strong&gt;인스턴스가 교체되는 것을 막을 수 있다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;스케줄러에-기대지-않기&quot;&gt;스케줄러에 기대지 않기&lt;/h2&gt;
&lt;p&gt;아래는 busy waiting을 하는 좋지 않은 예시이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class SlowCountDownLatch {
    private int count;

    public SlowCountDownLatch(int count) {
        if (count &amp;lt; 0)
            throw new IllegalArgumentException(count + &quot; &amp;lt; 0&quot;);

        this.count = count;
    }

    public void await() {
        while (true) {
            synchronized(this) {
                if (count == 0)
                    return;
            }
        }
    }

    public synchronized void countDown() {
        if (count != 0)
            count--;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 코드에서는 공유 객체의 상태가 바뀔 때까지 쉬지 않고 검사를 한다. 개발자는 '스케줄러가 어떻게든 최적화 해주지 않을까..?' 기대하지만 실상은 자바의 &lt;code&gt;CountDownLatch&lt;/code&gt;보다 1000배 가량 느리다.&lt;/p&gt;
&lt;p&gt;스레드가 당장 처리할 작업이 없다면 실행하지 않는 매커니즘으로 구현해야하고, 스레드 우선순위는 자바에서 이식성이 가장 나쁜 특성이므로 바꾸려 해서는 안된다.&lt;/p&gt;
&lt;p&gt;잘 모른다면 그냥 공식적으로 제공하는 API를 사용하는 것이 바람직하다.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001033066&quot;&gt;https://product.kyobobook.co.kr/detail/S000001033066&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Language/Java</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/384</guid>
      <comments>https://jaehoney.tistory.com/384#entry384comment</comments>
      <pubDate>Tue, 17 Oct 2023 08:28:17 +0900</pubDate>
    </item>
    <item>
      <title>Java 검사(Checked) 예외와 비검사(Unchecked) 예외</title>
      <link>https://jaehoney.tistory.com/383</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;아래는 이펙티브 자바의 내용 중 &lt;strong&gt;예외&lt;/strong&gt;에 대한 부분의 일부이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;del&gt;이펙티브 자바도 그렇고, 예외도 그렇고 포스팅할 내용이 너무 너무 많아서 다 정리할 수 없어서 아쉽다..!&lt;/del&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;일반적으로 검사 예외와 비검사 예외를 &lt;strong&gt;CheckedException&lt;/strong&gt;, &lt;strong&gt;UncheckedException&lt;/strong&gt;이라고 명명하지만, 해당 포스팅에서는 &lt;strong&gt;Effective Java&lt;/strong&gt;의 명칭대로 &lt;strong&gt;검사 예외&lt;/strong&gt;와 &lt;strong&gt;비검사 예외&lt;/strong&gt;라고 명칭한다.&lt;/p&gt;
&lt;h2 id=&quot;검사-예외와-비검사-예외&quot;&gt;검사 예외와 비검사 예외&lt;/h2&gt;
&lt;p&gt;Throwable 클래스를 상속하는 것은 &lt;strong&gt;Error&lt;/strong&gt;와 &lt;strong&gt;Exception&lt;/strong&gt;이 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcGFR5/btsyaUl6IRv/J7RECYr872yFa0z3MN8oDK/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Exception을 상속하면서 RuntimeException을 상속하지 않으면 &lt;strong&gt;검사 예외&lt;/strong&gt;이고,  RuntimeException을 상속하면 &lt;strong&gt;비검사 예외&lt;/strong&gt;라고 부른다.&lt;/p&gt;
&lt;p&gt;참고로 &lt;strong&gt;Error&lt;/strong&gt;는 &lt;code&gt;Throwable&lt;/code&gt;을 상속하고 &lt;code&gt;OutOfMemoryError&lt;/code&gt;나 &lt;code&gt;StackOverflowError&lt;/code&gt;와 같이 복구할 수 없는 경우 터트린다.&lt;/p&gt;
&lt;h2 id=&quot;선택하는-기준&quot;&gt;선택하는 기준&lt;/h2&gt;
&lt;p&gt;검사 예외의 경우 무지성으로 핸들링해서 비검사 예외로 발생시켰다. 좋은 습관이 아니었다.&lt;/p&gt;
&lt;p&gt;Effective Java에서는 &lt;strong&gt;호출하는 쪽에서 처리할 수 있다면&lt;/strong&gt; 검사 예외를, 그렇지 않다면 비검사 예외를 사용하라고 한다.&lt;/p&gt;
&lt;p&gt;아래는 &lt;code&gt;javax.mail&lt;/code&gt;의 &lt;code&gt;InternetAddress&lt;/code&gt; 클래스의 생성자다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public InternetAddress(String address) throws AddressException {
    // use our address parsing utility routine to parse the string
    InternetAddress a[] = parse(address, true);

    this.address = a[0].address;
    this.personal = a[0].personal;
    this.encodedPersonal = a[0].encodedPersonal;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;입력 &lt;code&gt;address&lt;/code&gt;가 잘못된 경우 &lt;code&gt;parse()&lt;/code&gt;에서 검사 예외가 발생하고 그것을 외부로 그대로 던지고 있다.&lt;/p&gt;
&lt;p&gt;사용자 측은 &lt;code&gt;address&lt;/code&gt;가 잘못된 경우 로그를 찍고 넘어가던지, 다른 방법으로 생성하던지, 쓰레드를 종료하던지 등 &lt;strong&gt;원하는 처리&lt;/strong&gt;를 할 수 있게 된다.&lt;/p&gt;
&lt;h2 id=&quot;검사-예외-회피&quot;&gt;검사 예외 회피&lt;/h2&gt;
&lt;p&gt;검사 예외를 사용하면 그 API 사용자는 &lt;strong&gt;try-catch 블록을 추가&lt;/strong&gt;해야되고 &lt;strong&gt;스트림&lt;/strong&gt;에서 사용하지 못하게 된다.&lt;/p&gt;
&lt;p&gt;그래서 검사 예외를 안 던질 수 있는 방법이 필요하다.&lt;/p&gt;
&lt;p&gt;가장 쉬운 방법은 적절한 결과 타입을 담는 옵셔널을 반환하는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;return Optional.ofNullable(resource);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이러면 사용자는 복잡한 절차나 제약 없이 처리가 가능하다.&lt;/p&gt;
&lt;p&gt;다른 방법으로는 아래와 같이 예외가 터질 여부를 미리 검사하는 방법이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;if (actionPermitted(request)) {
    action(request);
} else {
    // 예외 상황 대처
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 두 사항을 적용하기가 명확하지 않고 애매할 때 검사 예외를 사용한다.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;p&gt;향로님 블로그를 보면 &lt;strong&gt;프로젝트 전체 관점&lt;/strong&gt;에서 예외를 어떻게 다루는 지와 몇 가지 &lt;strong&gt;안티패턴&lt;/strong&gt;을 소개한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://jojoldu.tistory.com/734&quot;&gt;https://jojoldu.tistory.com/734&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;매우 재미있으니 꼭 읽어보길 추천한다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>Language/Java</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/383</guid>
      <comments>https://jaehoney.tistory.com/383#entry383comment</comments>
      <pubDate>Thu, 12 Oct 2023 08:12:52 +0900</pubDate>
    </item>
    <item>
      <title>Spring - Bean은 어디에 저장되나?</title>
      <link>https://jaehoney.tistory.com/382</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;스프링 공식 문서를 읽다가 &lt;strong&gt;Spring Bean&lt;/strong&gt;은 &lt;strong&gt;어떤 자료구조에 저장&lt;/strong&gt;되어 있고 &lt;strong&gt;어떤 과정으로 찾아서 의존을 주입하는 지&lt;/strong&gt;가 궁금해졌다.&lt;/p&gt;
&lt;p&gt;해당 부분을 찾아가면서 알게된 결과에 대해 다룬다. 아래에서 말하는 Bean은 &lt;strong&gt;싱글톤 빈&lt;/strong&gt;임을 가정한다.&lt;/p&gt;
&lt;h4 id=&quot;들어가기-전에&quot;&gt;들어가기 전에&lt;/h4&gt;
&lt;p&gt;Spring의 경우 &lt;strong&gt;템플릿 메서드 패턴과 전략 패턴을 충분히 활용&lt;/strong&gt;해서 &lt;strong&gt;수 많은 인터페이스에 책임을 위임하고 있고 구현체도 아주 많다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그러다보니 자세히 다루면 양이 너무 방대해지는 문제가 있어서 &lt;strong&gt;어디까지 다룰 것인가&lt;/strong&gt;에 대한 고민이 있었다.&lt;/p&gt;
&lt;p&gt;너무 Deep한 문제는 다루지 않고 &lt;strong&gt;내용을 다루기에 이해가 필요한 부분까지&lt;/strong&gt;만 다룰 것이니 겁먹지 마시고 봐주시면 좋겠다!&lt;/p&gt;
&lt;h2 id=&quot;getbean&quot;&gt;getBean()&lt;/h2&gt;
&lt;p&gt;아래를 보면 &lt;code&gt;ApplicationContext&lt;/code&gt;의 &lt;code&gt;refresh()&lt;/code&gt;가 수행되면 &lt;code&gt;invokeBeanFactoryProcessors()&lt;/code&gt;를 수행한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIj48g/btsEnWj3xfe/QcrdEgJIQ3zu7GCEC4GjT1/img.png&quot; alt=&quot;img_2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;해당 메서드에서는 &lt;code&gt;getBean()&lt;/code&gt; 메서드를 호출한다. 해당 포스팅에서 주요하게 다룰 메서드이다!&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getBean()&lt;/code&gt;은 아래와 같이 &lt;code&gt;doGetBean()&lt;/code&gt;를 호출한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HME3r/btsEpn9ifvv/HwG12FzKxQSwFD882kz5f1/img.png&quot; alt=&quot;img_3.png&quot; /&gt;&lt;/p&gt;
&lt;h4 id=&quot;map에서-싱글톤-오브젝트를-조회&quot;&gt;Map에서 싱글톤 오브젝트를 조회&lt;/h4&gt;
&lt;p&gt;메서드에서 가장 먼저 실행되는 부분은 Bean의 &lt;strong&gt;이름&lt;/strong&gt;으로 싱글톤 인스턴스를 가져오는 부분이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PRdQF/btsEorjNIcP/AEQBesYDqJPTWZZMJuwXGK/img.png&quot; alt=&quot;img_7.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;해당 메서드 내부를 살펴보면 기본적으로 &lt;code&gt;this.singletonObjects&lt;/code&gt;라는 인스턴스 필드에서 &lt;code&gt;get()&lt;/code&gt;을 호출해서 객체를 조회하고 있고 &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ct4Zuo/btsEnOTR2BW/WhBc9R34eEofjWhCRtucg1/img.png&quot; alt=&quot;img_5.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;해당 메서드에서 인스턴스 필드는 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DkqO8/btsEpomP3ik/4DyBAGVAFgmBvYrkYkWplK/img.png&quot; alt=&quot;img_6.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;인스턴스 필드들을 보면 기본적으로 Map에 저장된 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;메서드의 동작을 간략히 정리해봤다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;singletonObjects에서 빈을 조회한다.&lt;/li&gt;
&lt;li&gt;존재하지 않는다면 earlySingletonObjects에서 빈을 조회한다.&lt;/li&gt;
&lt;li&gt;synchronized 키워드와 함께 위 과정을 다시 한 번 수행한다.&lt;/li&gt;
&lt;li&gt;singletonFactories에서 해당 빈이 존재하는 지 조회한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;빈은 Key를 빈의 이름, Value가 인스턴스&lt;/strong&gt;인 &lt;strong&gt;Map&lt;/strong&gt;에다가 저장해두고 조회하는 방식을 사용한다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eEfDPH/btsEmDZTru4/buax1mJIs6Cr5oWYbAkf51/img.png&quot; alt=&quot;img_8.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;싱글톤은 위와 같이 타입을 변환해서 반환한다.&lt;/p&gt;
&lt;h2 id=&quot;번외&quot;&gt;번외&lt;/h2&gt;
&lt;p&gt;싱글톤 빈이 어디에 저장되고 어떻게 찾는 지도 알 수 있었다.&lt;/p&gt;
&lt;p&gt;그러면 &lt;strong&gt;언제 어떻게 저장이 될까?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;디버깅을 위해 &lt;code&gt;TestConfig&lt;/code&gt; 클래스를 생성하고 &lt;code&gt;@Configuration&lt;/code&gt;으로 등록했다. Breaking Point는 &lt;strong&gt;생성자 호출&lt;/strong&gt;로 잡았다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQu96l/btsEjIAWseY/FeaKoF7y0BejEQEJJxmF11/img.png&quot; alt=&quot;img_10.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;디벼깅 결과 아래 부분이 수행됨을 알 수 있었고, 해당 부분은 &lt;code&gt;AbstractApplicationContext&lt;/code&gt;의 &lt;code&gt;refresh()&lt;/code&gt;의 일부이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 메서드를 파고 가보면 &lt;code&gt;getBean()&lt;/code&gt;을 호출한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EwF0r/btsEm9KKfil/zsvAcBh15yh8HKBxk6VwPk/img.png&quot; alt=&quot;img_11.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;신기한 점은 디버깅을 위해 생성한 클래스의 &lt;strong&gt;생성자가 호출되기 전에&lt;/strong&gt; &lt;code&gt;getBean()&lt;/code&gt;이 호출되었다.&lt;/p&gt;
&lt;p&gt;여기서도 중요한 것은 &lt;code&gt;doGetBean()&lt;/code&gt;에 있었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JyNTc/btsEnVk9jnd/VhummaKXbFd1LAuqNfAGbK/img.png&quot; alt=&quot;img_12.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;빨간 색으로 표시한 부분은 아까 싱글톤 메서드를 가져올 때 생략했던 부분이다.&lt;/p&gt;
&lt;p&gt;해당 부분에서는 &lt;strong&gt;singleton&lt;/strong&gt;이 아닌 범위에 대한 분기도 포함하지만, &lt;strong&gt;빈이 존재하지 않을 때&lt;/strong&gt;의 분기도 포함한다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMh82Y/btsEnYPw8AM/tEAkosjJZuoAkXuV7ceerk/img.png&quot; alt=&quot;img_13.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;빈이 존재하지 않는다면 빨간 사각 부분이 실행된다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;createBean() 호출&lt;/li&gt;
&lt;li&gt;getSingleton() 호출&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;createBean()&lt;/code&gt;는 복잡하지만 내부적으로 아래의 로직을 수행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
return instanceWrapper.getWrappedInstance();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 인스턴스를 생성해서 빈을 반환하고&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getSingleton()&lt;/code&gt; 메서드는 내부적으로 아래 로직을 수행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;synchronized (this.singletonObjects) {
    this.singletonObjects.put(beanName, singletonObject);
    this.singletonFactories.remove(beanName);
    this.earlySingletonObjects.remove(beanName);
    this.registeredSingletons.add(beanName);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정리하면 싱글톤으로 등록된 모든 빈의 &lt;code&gt;getBean()&lt;/code&gt; 메서드가 실행되고, 해당 메서드가 실행될 때 Bean이 내부 자료구조에 존재하지 않는다면 &lt;strong&gt;인스턴스를 생성&lt;/strong&gt;하고 &lt;strong&gt;Map에 넣는다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 과정을 우리는 &lt;strong&gt;빈을 등록&lt;/strong&gt;한다고 부르는 것 같다.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference&quot;&gt;https://docs.spring.io/spring-framework/reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework&quot;&gt;https://github.com/spring-projects/spring-framework&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Server/Spring</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/382</guid>
      <comments>https://jaehoney.tistory.com/382#entry382comment</comments>
      <pubDate>Mon, 9 Oct 2023 11:38:06 +0900</pubDate>
    </item>
    <item>
      <title>Java - switch 대신 Enum을 검토해보자!</title>
      <link>https://jaehoney.tistory.com/381</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;아래 내용은 Effective Java 내용에 기반한다.&lt;/p&gt;
&lt;h2 id=&quot;상수-대신-enum&quot;&gt;상수 대신 Enum&lt;/h2&gt;
&lt;p&gt;아래 코드를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 코드의 영향은 어떤 것이 있을까..?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상수의 값이 바뀌면 반드시 다시 컴파일해야 한다.&lt;/li&gt;
&lt;li&gt;추적이 어렵다. (0, 1, 2로 저장되니까)&lt;ul&gt;
&lt;li&gt;추적이 어렵다는 이유로 int 대신 String을 사용한다면 추적은 쉬워지겠지만 불안정한 시스템이 된다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;분기를 수행하기 어렵다.&lt;/li&gt;
&lt;li&gt;순회하기 어렵다.&lt;/li&gt;
&lt;li&gt;상수의 이름을 중복할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;반면 아래 예시를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public enum Apple {FUJI, PIPPIN, GRANNY_SMITH}
public enum Orange {NAVEL, TEMPLE, BLOOD}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서는 컴파일 타입 안정성이 제공된다.&lt;/p&gt;
&lt;p&gt;APPLE 값으로 다른 값이 들어오면 컴파일 에러가 난다.&lt;/p&gt;
&lt;p&gt;Enum은 새로운 상수를 추가하거나 순서를 바꿔도 다시 컴파일하지 않아도 된다.&lt;/p&gt;
&lt;h2 id=&quot;상수-내재&quot;&gt;상수 내재&lt;/h2&gt;
&lt;p&gt;Enum은 특정 데이터와 연관지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.&lt;/p&gt;
&lt;p&gt;Enum의 생성자는 해당 클래스 내부적으로만 동작하게 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Getter
public enum Planet {
     MERCURY(3.302e+23, 2.439e6),
     VENUS(4.869e+24, 6.052e6),
     EARTH(5.975e+24, 6.378e6),
     MARS(6.419e+23, 3.393e6),
     JUPITER(1.899e+27, 7.149e7),
     SATURN(5.685e+26, 6.027e7),
     URANUS(8.683e+25, 2.556e7),
     NEPTUNE(1.024e+26, 2.447e7);

     private final double mass;                // 질량(단위: 킬로그램)
     private final double radius;             // 반지름(단위: 미터)
     private final double surfaceGravity;  // 표면중력(단위: m / s^2)

     // 중력상수 (단위: m^3 / kg s^2)
     private static final double G = 6.67300E-11;

     Planet(double mass, double radius) {
          this.mass = mass;
          this.radius = radius;
          this.surfaceGravity = G * mass / (radius * radius);
     }

     public double surfaceWeight(double mass) {
          return mass * surfaceGravity;
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 클래스를 사용하면 어떤 행성의 지구에서의 무게를 입력받아, 해당 행성에서의 지구의 무게를 출력하는 것도 간단히 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public classs WeightTable {
     public static void main(String[] args) {
          double earthWeight = Double.parseDouble(args[0]);
          double mass = earthWeight / Planet.EARTH.surfaceGravity();
          for (Planet p : Palanet.values()) 
                System.out.println(&quot;%s에서 무게는 %f이다. %n&quot;, p, p.surfaceWeight(mass));
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;enum의-기본-메서드&quot;&gt;Enum의 기본 메서드&lt;/h4&gt;
&lt;p&gt;참고로 위에서 몇가지 Enum의 메서드를 사용했다. 지원하는 메서드는 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;values(): Enum에 정의된 상수들을 배열에 담아 반환&lt;/li&gt;
&lt;li&gt;valueOf(): 상수 이름을 입력받아 해당 상수를 반환&lt;/li&gt;
&lt;li&gt;toString(): 상수 이름을 문자열로 반환&lt;/li&gt;
&lt;li&gt;fromString(): &lt;code&gt;toString()&lt;/code&gt;이 반환하는 문자열을 해당 열거 타입 상수로 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 메서드는 재정의도 가능하기에 다양하게 활용할 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;switch-대신-enum&quot;&gt;switch 대신 Enum&lt;/h2&gt;
&lt;p&gt;만약 상수별로 동작이 다른 경우는 아래와 같이 구현할 수 있을 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public enum Operation {
     PLUS, MINUS, TIMES, DIVIDE
}

public double apply(double x, double y) {
     switch(this) {
          case PLUS: return x + y;
          case MINUS: return x - y;
          case TIMES: return x * y;
          case DIVIDE: return x / y;
     }
     throw new AssertionError(&quot;알 수 없는 연산: &quot; + this);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 코드는 아쉽게도 문제점이 여러가지 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;연산이 추가될때마다 비즈니스 코드인 &lt;code&gt;apply()&lt;/code&gt;를 수정해야 한다.&lt;/li&gt;
&lt;li&gt;기술적으로 case에 도달할 수 있기 때문에 default 값이나 throw가 강제된다.&lt;/li&gt;
&lt;li&gt;실수로 case를 추가하지 않으면 에러가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 문제를 추상 메서드를 사용하면 유용하게 해결할 수 있다. 아래의 각 열거형 요소는 추상 메서드를 구현한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public enum Operation {
     PLUS {public double apply(double x, double y) {return x + y;}},
     MINUS {public double apply(double x, double y) {return x + y;}},
     TIMES {public double apply(double x, double y) {return x + y;}},
     DIVIDE {public double apply(double x, double y) {return x + y;}};

     public abstract double apply(double x, double y);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다른 방법으로는 함수형 인터페이스를 사용할 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public enum Operation {
     PLUS((x, y) -&amp;gt; x + y),
     MINUS((x, y) -&amp;gt; x - y),
     TIMES((x, y) -&amp;gt; x * y),
     DIVIDE((x, y) -&amp;gt; x / y);

     private final DoubleBinaryOperator operator;

     Operation(DoubleBinaryOperator operator) {
          this.operator = operator;
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;나머지 문제가 있다. 입력으로 들어온 &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt;를 Enum으로 변환해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public enum Operation {
      PLUS('+', (x, y) -&amp;gt; x + y),
      MINUS('-', (x, y) -&amp;gt; x - y),
      TIMES('*', (x, y) -&amp;gt; x * y),
      DIVIDE('/', (x, y) -&amp;gt; x / y);

      private final char symbol;
      private final DoubleBinaryOperator operator;

      Operation(char symbol, DoubleBinaryOperator operator) {
            this.symbol = symbol;
            this.operator = operator;
      }

      public double apply(double x, double y) {
            return operator.applyAsDouble(x, y);
      }

      public char getSymbol() {
            return symbol;
      }

      private static final Map&amp;lt;String, Operation&amp;gt; stringToEnum = 
                 Stream.of(values()).collect(toMap(Object::toString, e -&amp;gt; e));

      public static Optional&amp;lt;Operation&amp;gt; fromString(String symbol) {
            return Optional.ofNullable(stringToEnum.get(symbol));
      }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 symbol 필드를 추가하고, &lt;code&gt;fromString()&lt;/code&gt;를 구현해주면 된다. 여기서는 Invalid 값이 들어왔을 때 예외를 터트리기보다는 상위 모듈에서 처리가 가능하도록 &lt;code&gt;Optional&lt;/code&gt;을 사용했다.&lt;/p&gt;
&lt;h3 id=&quot;마무리&quot;&gt;마무리&lt;/h3&gt;
&lt;p&gt;Enum은 상수보다 뛰어나며 더 읽기 쉽고 강력하다. Enum을 활용하면 생각보다 많은 것을 할 수 있다.&lt;/p&gt;
&lt;p&gt;Enum 요소의 필드로 다른 타입의 Enum 요소를 가질 수 있고 이를 활용한다면 아래와 같은 전략 열거 패턴을 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;enum PayrollDay {
     MONDAY, TUESDAY, WEDSDAY, THURSDAY, FRIDAY, 
     SATURDAY(PayTyoe.WEEKEND), SUNDAY(PayType.WEEKEND);

     private final PayType payType;

     PayrollDya(PayType payTyoe) {this.payType = payType;}

     int pay(int minutesWorked, int payRate) {
         return payType.pay(minutesWorked, payRate);
     }

     enum PayType {
          WEEKDAY {
                int overtimePay(int minusWorked, int payRate) {
                     return minusWorked &amp;lt;= MINS_PER_SHIFT ? 0 :
                     (minusWorked - MINS_PER_SHIFT) * payRate / 2;
                }
          },
          WEEKEND {
                int overtimePay(int minusWorked, int payRate) {
                     return minusWorked * payRate / 2;
                }
          };

          abstract int overtimePay(int mins, int payRate);
          private static final int MINS_PER_SHIFT = 8 * 60;

          int pay(int minsWorked, int payRate) {
                int basePay = minsWorked * payRate;
                return basePay + overtimePay(minsWorked, payRate);
          }
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Enum이 매우 유용하긴 하나 당연히 남용해서는 안된다. Effective Java에서는 Enum을 사용하는 기준을 &lt;strong&gt;컴파일 중에 필요한 원소를 모두 알 수 있는 상수의 집합&lt;/strong&gt;이라면 Enum을 사용할 것을 권고한다.&lt;/p&gt;
&lt;p&gt;상수가 추가되거나 제거되는 경우 사용하지 말아야 된다는 말이 아니다. Enum은 바이너리 수준에서 요소의 추가, 제거를 지원한다.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@new_wisdom/Effective-java-item-34.-int-상수-대신-열거-타입을-사용하라&quot;&gt;https://velog.io/@new_wisdom/Effective-java-item-34.-int-상수-대신-열거-타입을-사용하라&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.yes24.com/Product/Goods/65551284&quot;&gt;https://www.yes24.com/Product/Goods/65551284&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Language/Java</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/381</guid>
      <comments>https://jaehoney.tistory.com/381#entry381comment</comments>
      <pubDate>Thu, 5 Oct 2023 08:46:11 +0900</pubDate>
    </item>
    <item>
      <title>분산 시스템 설계 - 유튜브 설계해보기!</title>
      <link>https://jaehoney.tistory.com/380</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;요즘 너무 설계 내용만 포스팅해서 유튜브 설계 내용은 포스팅하지 않겠다고 생각했는데..&lt;/p&gt;
&lt;p&gt;유튜브 설계 내용이 생각외로 되게 재밌어서 또 포스팅하게 됬다!&lt;/p&gt;
&lt;p&gt;이번 포스팅은 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/strong&gt;에 기반한 내용이다.&lt;/p&gt;
&lt;h2 id=&quot;개략적-설계&quot;&gt;개략적 설계&lt;/h2&gt;
&lt;p&gt;유튜브 시스템은 언뜻 보기에는 간단해 보일 수 있지만 실제로는 굉장히 복잡하다. 아래는 유튜브에 대한 통계 자료이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MAU: 21억&lt;/li&gt;
&lt;li&gt;매일 재생되는 비디오 수: 50억&lt;/li&gt;
&lt;li&gt;5천만 명의 창작자&lt;/li&gt;
&lt;li&gt;모바일 인터넷 트래픽 가운데 37%를 점유&lt;/li&gt;
&lt;li&gt;2019년 기준 연간 광고 수입이 150억 달러&lt;/li&gt;
&lt;li&gt;80개 언어로 이용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;기본적으로 유튜브는 단순히 비디오를 생성하고 보는 것 이외에도 댓글, 공유, 좋아요, 재생목록, 채널, 구독 등과 같은 다양한 기능을 제공한다. 해당 기능들을 이번 포스팅에서 다 다루기는 어려렵다. 아래 설계에서는 비디오를 업로드하고 재생하는 것에 집중한다.&lt;/p&gt;
&lt;h4 id=&quot;요구사항&quot;&gt;요구사항&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;가장 중요한 기능: 비디오를 올리고 시청하는 기능&lt;/li&gt;
&lt;li&gt;클라이언트: 모바일 앱, 웹 브라우저, 스마트 TV&lt;/li&gt;
&lt;li&gt;일간 능동 사용자 수: 500만 명&lt;/li&gt;
&lt;li&gt;10% 사용자가 하루에 1개의 비디오 업로드&lt;/li&gt;
&lt;li&gt;다국어 지원: 모든 언어로도 이용 가능해야 함&lt;/li&gt;
&lt;li&gt;비디오 해상도: 현존하는 대부분을 지원&lt;/li&gt;
&lt;li&gt;암호화 필요: O&lt;/li&gt;
&lt;li&gt;비디오 파일 크기: 최대 1GB로 제한 (크지 않은 Size에 집중)&lt;/li&gt;
&lt;li&gt;AWS, GCP, Azure의 클라우드 서비스 활용 가능 여부: O&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래는 성능. 안정성에 대한 요구사항 명세이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;빠른 비디오 업로드&lt;/li&gt;
&lt;li&gt;원활한 재생&lt;/li&gt;
&lt;li&gt;재생 품질 선택 기능&lt;/li&gt;
&lt;li&gt;낮은 인프라 비용&lt;/li&gt;
&lt;li&gt;높은 가용성과 규모 확장성, 안정성&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;클라우드-비용-산정&quot;&gt;클라우드 비용 산정&lt;/h4&gt;
&lt;p&gt;아래는 클라우드 비용을 산정한 것이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;일간 능동 사용자 수는 500만 명, 한 사용자는 하루 평균 5개의 비디오 시청, 10% 사용자가 하루에 1개의 비디오 업로드&lt;/li&gt;
&lt;li&gt;비디오 평균 크기는 300MB&lt;/li&gt;
&lt;li&gt;비디오 저장을 위해 매일 새로 요구되는 저장 용량 = 500만 x 10% x 300MB = 150TB&lt;/li&gt;
&lt;li&gt;CDN 비용 (미국에서 발생을 가정): 500만 x 5비디오 x 0.3GB x $0.02 = $150,000&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 추정 결과에 따르면 CDN을 통해 비디오를 서비스하면 비용이 엄청나다. 위 비용을 줄일 수 있는 방안도 상세 설계에서 고민해보자.&lt;/p&gt;
&lt;h2 id=&quot;개략적-설계-1&quot;&gt;개략적 설계&lt;/h2&gt;
&lt;p&gt;아래는 쉽게 생각할 수 있는 설계이다. &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cw2coO/btsEnjsQrwL/2AHJmPyYsNOMt4osl9mAQk/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;각 컴포넌트의 역할은 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;단말: 컴퓨터, 폰, 스마트 TV 등을 통해 유튜브를 시청할 수 있다.&lt;/li&gt;
&lt;li&gt;CDN: 비디오는 CDN에 저장한다. 재생을 하면 CDN으로부터 스트리밍이 이루어진다.&lt;ul&gt;
&lt;li&gt;넷플릭스도 AWS를 사용하고 페이스북도 아카마이의 CDN을 사용한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;API 서버: 비디오 스트리밍을 제외한 모든 요청은 API 서버가 처리한다. 피드 추천, 비디오 업로드 URL 생성, DB 및 캐시 갱신, 사용자 가입 등을 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;스트리밍의 경우 CDN 서버에서 스트리밍을 요청하면 된다. 비디오 업로드 부분을 설계해보자.&lt;/p&gt;
&lt;h4 id=&quot;비디오-업로드&quot;&gt;비디오 업로드&lt;/h4&gt;
&lt;p&gt;최초 비디오 업로드 설계안은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sRJdO/btsEnY23tKD/mnQDXbN3nbGOlWEYnuWvG0/img.png&quot; alt=&quot;img_1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;정리하면 아래의 두 프로세스가 병렬적으로 수행된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;비디오 업로드&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;비디오를 원본 저장소에 저장&lt;/li&gt;
&lt;li&gt;트랜스코딩 서버는 원본 저장소에서 비디오를 가져와 트랜스코딩을 수행&lt;/li&gt;
&lt;li&gt;트랜스코딩이 완료되면 아래 두 절차를 병렬로 수행&lt;ul&gt;
&lt;li&gt;완료된 비디오를 트랜스코딩 저장소로 업로드한다.&lt;/li&gt;
&lt;li&gt;트랜스코딩이 끝난 비디오를 CDN에 올린다.&lt;/li&gt;
&lt;li&gt;트랜스코딩 완료 이벤트를 트랜스코딩 완료 큐에 넣는다.&lt;/li&gt;
&lt;li&gt;완료 핸들러가 이벤트를 큐에서 꺼낸다.&lt;/li&gt;
&lt;li&gt;메타데이터 DB와 캐시를 갱신한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;API 서버가 단말에게 스트리밍 준비가 되었다고 알림&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;메타데이터 갱신&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;단말은 병렬적으로 비디오 메타데이터 갱신 요청을 API 서버에 보낸다.&lt;ul&gt;
&lt;li&gt;파일 이름, 크기, 포맷 등&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;API 서버는 이 정보로 메타데이터 DB와 캐시를 업데이트한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;각 컴포넌트는 아래 역할을 수행한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 서버: 비디오 스트리밍을 제외한 다른 모든 요청을 처리한다.&lt;/li&gt;
&lt;li&gt;메타데이터 DB: 비디오의 메타데이터를 보관한다.&lt;ul&gt;
&lt;li&gt;샤딩(Sharding)과 복제(Replication)를 적용해서 성능 및 가용성 요구사항을 충족한다. &lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;메타데이터 캐시: 성능을 높이기 위해 비디오 메타데이터와 사용자 객체는 캐시한다.&lt;/li&gt;
&lt;li&gt;원본 저장소: 원본 비디오를 보관할 대형 이진 파일 저장소(BLOB) 시스템이다. &lt;/li&gt;
&lt;li&gt;트랜스코딩 서버: 비디오의 포맷(MPEG, HLS 등)을 변한해서 단말이나 대역폭에 맞는 최적의 비디오 스트림을 제공하기 위해 필요하다.&lt;/li&gt;
&lt;li&gt;트랜스코딩 비디오 저장소: 트랜스코딩이 완료된 비디오를 저장하는 BLOB 저장소이다.&lt;/li&gt;
&lt;li&gt;CDN: 비디오를 캐시하는 역할&lt;/li&gt;
&lt;li&gt;트랜스코딩 완료 큐: 비디오 트랜스코딩 완료 이벤트를 보관할 메시지 큐&lt;/li&gt;
&lt;li&gt;트랜스코딩 완료 핸들러: 트랜스코딩 완료 큐에서 이벤트를 꺼내서 메타데이터 캐시와 DB를 갱신할 작업 서버&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;비디오-스트리밍&quot;&gt;비디오 스트리밍&lt;/h4&gt;
&lt;p&gt;비디오 스트리밍은 비교적 간단하며 CDN을 통해서 이루어진다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BSlzj/btsEmFi4Tp1/KPGJAzKAsn7JAtDPWCseR1/img.png&quot; alt=&quot;img_2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;각 단말기마다 가장 가까운 CDN을 사용하기 때문에 전송지연에 대한 문제는 크게 일어나지 않는다.&lt;/p&gt;
&lt;p&gt;주의해야 할 점은 프로토콜마다 지원하는 비디오 인코딩과 플레이어가 다르다는 것이다.&lt;/p&gt;
&lt;h2 id=&quot;상세-설계&quot;&gt;상세 설계&lt;/h2&gt;
&lt;h4 id=&quot;비디오-트랜스코딩&quot;&gt;비디오 트랜스코딩&lt;/h4&gt;
&lt;p&gt;특정 단말에서 생성한 비디오가 다른 단말에서도 원활하게 재생되려면, 다른 단말과 호환되는 비트레이트로 저장되어야 한다.&lt;/p&gt;
&lt;p&gt;비디오 트랜스코딩은 다음 이유로 아주 중요하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;가공되지 않은 원본 비디오는 저장 공간을 아주 많이 차지한다.&lt;/li&gt;
&lt;li&gt;상당 수의 단말과 브라우저는 특정 종류의 비디오 포맷만 지원하기 때문에 호환성 문제를 해결하려면 1개의 비디오를 여러 포맷으로 인코딩해야 한다.&lt;/li&gt;
&lt;li&gt;사용자의 네트워크 대역폭, 인터넷 속도에 따라 끊김 없는 재생을 보장하기 위해 360p, 720p, 1080p 등 다양한 화질로 인코딩해두어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;dag&quot;&gt;DAG&lt;/h5&gt;
&lt;p&gt;비디오에서 워터마크를 표시하고 싶은 사용자와 섬네일 이미지를 직접 제작하고 싶은ㅇ 사용자, 고화질 비디오를 선호하는 사람 등 다양하다.&lt;/p&gt;
&lt;p&gt;페이스북의 스트리밍 비디오 엔진은 DAG(Directed Acyclic Graph) 모델을 도입한다. &lt;br /&gt;
그래서 아래와 같이 적절한 추상화를 도입한 모델이 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/py602/btsEmEYOQNi/K8KTsKxptjsruCkAQGmf20/img.png&quot; alt=&quot;img_3.png&quot; /&gt;&lt;/p&gt;
&lt;h5 id=&quot;인코딩&quot;&gt;인코딩&lt;/h5&gt;
&lt;p&gt;아래는 비디오 인코딩을 한 결과이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zT7Hm/btsEnPrHTPe/kL7QjdPjenPEKxlZbBUsZK/img.png&quot; alt=&quot;img_4.png&quot; /&gt;&lt;/p&gt;
&lt;h5 id=&quot;아키텍처&quot;&gt;아키텍처&lt;/h5&gt;
&lt;p&gt;위 내용을 바탕으로 작성한 트랜스코딩의 아키텍처는 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boKRXY/btsEnc1AOHG/jfjQ1uKcxhSReGsRJOwd31/img.png&quot; alt=&quot;img_5.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;각 컴포넌트의 역할은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;전처리기&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;비디오 분할: GOP(Group of Pictures) 단위로 비디오를 쪼개는 것을 오래된 단말이나 브라우저에서 지원하지 않는다. 그 경우 비디오 스트림을 몇 초 단위로 잘게 쪼갠다.&lt;/li&gt;
&lt;li&gt;DAG 생성: 클라이언트의 설정에 따라 DAG를 만든다.&lt;/li&gt;
&lt;li&gt;데이터 캐시: 분할된 비디오의 메타데이터를 임시 저장소에 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;DAG 스케줄러&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DAG 그래프를 몇 개의 단계로 분할한 다음 각 작업을 해당 작업 관리자의 작업 큐에 넣는다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZfATu/btsEmFclT32/zwvjQEibt5CZjX7UQMEpN0/img.png&quot; alt=&quot;img_6.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;자원 관리자&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;자원 배분을 효과적으로 수행한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNnYIL/btsEmFJ9NAD/MstOcgIHmUDdfREIRHjfSK/img.png&quot; alt=&quot;img_7.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;아래는 각 컴포넌트의 역할이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;작업 큐: 실행할 작업이 보관된 우선순위 큐&lt;/li&gt;
&lt;li&gt;작업 서버 큐: 작업 서버의 가용 상태 정보가 보관된 우선순위 큐이다.&lt;/li&gt;
&lt;li&gt;실행 큐: 현재 실행 중인 작업 및 작업 서버 정보가 보관되어 있는 큐이다.&lt;/li&gt;
&lt;li&gt;작업 스케줄러:&lt;ul&gt;
&lt;li&gt;최적의 작업/서버를 골라 해당 작업 서버가 작업을 수행하도록 지시한다.&lt;/li&gt;
&lt;li&gt;해당 작업이 어떤 서버에게 할당 되었는지에 관한 정보를 실행 큐에 넣는다.&lt;/li&gt;
&lt;li&gt;작업이 완료되면 해당 작업을 실행 큐에서 제거한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;작업 서버&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;작업 서버는 DAG에 정의된 작업을 수행한다. 작업 종류에 따라 작업 서버도 구분하여 관리한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvcKNL/btsEmCUetwN/ClpAYh7MIkaE5tzJmxMS40/img.png&quot; alt=&quot;img_8.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;임시 저장소&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;임시 저장소로 어떤 시스템을 선택할 것이냐는 저장할 데이터의 특징에 따라 달라진다. 메타 데이터는 작업 서버가 빈번히 참조하기 때문에 메모리에 캐시해두면 좋을 것이다.&lt;/p&gt;
&lt;p&gt;비디오/오디오 데이터는 BLOB 저장소에 두는 것이 바람직하다. 임시 저장소에 보관한 데이터는 프로세싱이 끝나면 삭제한다.&lt;/p&gt;
&lt;h3 id=&quot;최적화&quot;&gt;최적화&lt;/h3&gt;
&lt;h5 id=&quot;병렬-업로드&quot;&gt;병렬 업로드&lt;/h5&gt;
&lt;p&gt;비디오 전부를 한 번에 업로드하면 비효율적이다.&lt;/p&gt;
&lt;p&gt;비디오는 작은 GOP들로 분할할 수 있다.&lt;/p&gt;
&lt;p&gt;분할한 GOP를 병렬적으로 업로드하면 설사 일부가 실패해도 빠르게 업로드를 재개할 수 있으며 업로드 속도를 증가시킬 수 있다.&lt;/p&gt;
&lt;h5 id=&quot;병렬-프로세싱&quot;&gt;병렬 프로세싱&lt;/h5&gt;
&lt;p&gt;느슨하게 결합된 시스템은 Latency(응답 속도)를 감소하고 자원을 효율적으로 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;비디오를 원본 저장소에서 CDN으로 옮기기까지의 과정을 MQ를 사용하면 각 컴포넌트의 느슨한 결합을 보장할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uC81o/btsEnXiLHf0/gb4k9QTlVCkvrbNLZe3z5k/img.png&quot; alt=&quot;img_9.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;이제 인코딩 모듈은 다운로드 모듈의 작업이 끝나기를 기다리지 않아도 된다. 그리고 각 MQ의 메시지가 보존되므로 각자의 할일만 잘하면 된다.&lt;/p&gt;
&lt;p&gt;MQ는 이벤트를 발행하고, 구독하는 형태에서만 사용하기 적합하다고 생각했었다. 즉, 구독하는 컨슈머 그룹이 2개 이상이어야 MQ를 사용하기 적합한 환경이라고 생각했다. 그렇지만 &lt;strong&gt;느슨한 결합&lt;/strong&gt;을 위해서도 MQ를 활용할 수 있었다.&lt;/p&gt;
&lt;h5 id=&quot;비용-최적화&quot;&gt;비용 최적화&lt;/h5&gt;
&lt;p&gt;CDN은 비싸다. 비용을 최적화하기 위해 아래 방법을 고민할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;인기 비디오만 CDN을 통해 재생하고 그렇지 않은 비디오들은 원본 그대로 재생한다.&lt;/li&gt;
&lt;li&gt;짧은 비디오라면 필요할 때 인코딩해서 재생한다.&lt;/li&gt;
&lt;li&gt;특정 비디오는 특정 지역의 CDN에만 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 최적화는 컨텐츠 인기도, 이용 패턴, 비디오 크기에 근거한다. 그래서 로그를 통해 시청 패턴을 분석하는 것은 중요하다.&lt;/p&gt;
&lt;h2 id=&quot;에러-처리&quot;&gt;에러 처리&lt;/h2&gt;
&lt;p&gt;대형 시스템에서 에러는 불가피하다. 시스템 에러에는 두 종류가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;회복 가능 에러: 특정 작업이 실패한 경우 몇 번 재시도하면 해결 된다. 계속해서 실패한다면 클라이언트에게 적절한 에러 코드를 반환해야 한다.&lt;/li&gt;
&lt;li&gt;회복 불가능 에러: 비디오 포맷이 잘못되었거나 하는 경우 해당 작업을 중단하고 클라이언트에게 적절한 에러 코드를 반환해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;각 시스템 컴포넌트에서 발생할 수 있는 에러는 아래의 경우가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;업로드 오류: 몇 회까지 재시도한다.&lt;/li&gt;
&lt;li&gt;비디오 분할 오류: 클라이언트에서 분할이 실패한 경우 전체 비디오를 서버로 전송하고 서버에서 분할한다.&lt;/li&gt;
&lt;li&gt;트랜스코딩 오류: 재시도한다.&lt;/li&gt;
&lt;li&gt;전처리 오류: DAG 그래프를 재생성한다.&lt;/li&gt;
&lt;li&gt;DAG 스케줄러 오류: 작업을 다시 스케줄링한다.&lt;/li&gt;
&lt;li&gt;작업 관리자 큐에 장애 발생: 사본(replica)를 이용한다.&lt;/li&gt;
&lt;li&gt;작업 서버 장애: 다른 서버에서 해당 작업을 재시도한다.&lt;/li&gt;
&lt;li&gt;API 서버 장애: API 서버는 무상태 서버이므로 신규 요청은 다른 API 서버에서 수행한다.&lt;/li&gt;
&lt;li&gt;메타데이터 캐시 서버 장애: 데이터는 다중화되어 있으므로 다른 노드에서 데이터를 가져온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;마무리&quot;&gt;마무리&lt;/h3&gt;
&lt;p&gt;실제로 유튜브가 이러한 설계를 기반했는 지는 알 수 없지만 위 설계 내용이 굉장히 탄탄하다고 느꼈다. 가정한 트래픽이 매우 높았고, 가정한 기능도 다양했기 때문에 이러한 설계도 할 수 있었던 것 같다.&lt;/p&gt;
&lt;p&gt;다시 한 번 가정의 필요성을 느끼게 되었다.&lt;/p&gt;
&lt;p&gt;위 내용 이외에도 API 계층이나 DB 계층과 규모 확장, 샤딩 등을 논의해볼 수 있을 것 같다.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@kyy00n/대규모-시스템-설계-기초-유튜브-설계&quot;&gt;https://velog.io/@kyy00n/대규모-시스템-설계-기초-유튜브-설계&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Operation/System Architecture</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/380</guid>
      <comments>https://jaehoney.tistory.com/380#entry380comment</comments>
      <pubDate>Wed, 4 Oct 2023 08:11:54 +0900</pubDate>
    </item>
    <item>
      <title>DB 인덱스에 대한 오해 (컬럼 1개 vs 2개!)</title>
      <link>https://jaehoney.tistory.com/379</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;어느 날 신입 분이 나한테 찾아와서 물었다.&lt;/p&gt;
&lt;p&gt;'메일 조회를 할 때 권한이 없으면 404가 아니라 401 또는 403이 나와야 하는 것 아닌가요?'&lt;/p&gt;
&lt;p&gt;&lt;del&gt;(인덱스 얘기한대놓고 무슨 권한 얘기인지..)&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;나도 쌩신입때 선배분께 동일한 질문을 했었다.&lt;/p&gt;
&lt;h2 id=&quot;인덱스-오해&quot;&gt;인덱스 오해&lt;/h2&gt;
&lt;p&gt;아래 코드는 비즈니스에서 Mail을 조회하기 위해 사용되던 코드이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;mailRepository.findByUserIdAndId(userId, id)
        .orElseThrow(() -&amp;gt; new EntityNotFoundException(&quot;Mail&quot;, id));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 내가 생각한 문제는 3가지가 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;쿼리에 비즈니스 로직이 들어간다.&lt;ul&gt;
&lt;li&gt;가독성이 나빠지고 객체지향적인 설계가 불가능해진다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;프로그래밍 초식의 쿼리에서 로직 빼기&lt;/code&gt; 부분을 참고하자.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;자신의 소유가 아닌 메일에 대해서 404(Not Found)가 내려온다.&lt;ul&gt;
&lt;li&gt;REST 스펙에 따르면 인가와 권한의 문제는 403 Forbidden을 내려주는 것이 바람직한 것 같다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;인덱스가 불필요하게 추가될 위험이 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;그래서 나는 &lt;code&gt;findById()&lt;/code&gt;만 호출한 후 소유권자를 로직으로 비교하고 403 Forbidden으로 처리되어야 하지 않느냐는 질문을 했고&lt;/p&gt;
&lt;p&gt;여기서 동료(선배) 분이 '&lt;strong&gt;그렇게 하면 인덱스를 안타잖아요.&lt;/strong&gt;' 라는 답변을 주셨다. 완전 쌩신입이던 나는 우선적으로 동의했다.&lt;/p&gt;
&lt;p&gt;테이블의 인덱스는 아래와 같았다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기본 키(Primary Key): &lt;code&gt;id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;인덱스(Non-cluster Key): &lt;code&gt;user_id, id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;정말-그럴까&quot;&gt;정말 그럴까?&lt;/h2&gt;
&lt;p&gt;동료 분의 의견은 Index를 하나라도 많은 컬럼을 태워야 성능이 향상된다는 것이다.&lt;/p&gt;
&lt;p&gt;현재의 나는 Real MySQL이나 공식 문서를 정독한 경험이 있어서 그렇지 않다고 확신한다.&lt;/p&gt;
&lt;p&gt;아래는 Primary Key의 동작 방식을 그린 것이다.&lt;/p&gt;
&lt;h4 id=&quot;1-primary-key&quot;&gt;1. Primary Key&lt;/h4&gt;
&lt;p&gt;만약 id가 5인 row를 조회한다고 하자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDV8jm/btsEnOl1gKd/vT3kbnPFoGu61Q5UBLtdz0/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;아래와 같이 트리 순회를 하게 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;B+ Tree 구조의 특징과 Clustered-Key의 특징은 생략했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;2-index&quot;&gt;2. Index&lt;/h2&gt;
&lt;p&gt;그러면 Index로 user_id, id를 걸고 그걸로 조회를 하면 더 속도가 빠를까?&lt;/p&gt;
&lt;p&gt;user_id가 3이고, id가 1인 데이터를 조회하는 상황을 가정해보자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rKgwN/btsEmpAkWvn/EVjvrGlJKbKCdg1vhFRMyK/img.png&quot; alt=&quot;img_1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;위와 마찬가지로 &lt;strong&gt;트리 순회&lt;/strong&gt;를 해서 찾게 된다. PK(컬럼 1개)로 조회하던 2개 컬럼의 Index를 사용하던 어차피 결과는 &lt;strong&gt;트리 순회&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;즉 PK를 사용하는 거에 비해서 Index를 사용하는 것의 이점이 없었다. 물론 유저별 메일을 검색할 때의 이점은 충분히 있다.&lt;/p&gt;
&lt;p&gt;단건 조회 시 user_id까지 where 조건에 넣어야할 이유는 없다. 오히려 아래의 단점만 생긴다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clustered Key를 사용하지 않고 Non-Clustered Key를 사용하므로 데이터를 매핑할 때의 오버헤드가 발생한다.&lt;/li&gt;
&lt;li&gt;Index의 동작을 파악하기가 더 복잡하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;
&lt;p&gt;정리하자면 &lt;code&gt;Index 컬럼을 하나라도 더 태워야 성능 상 이점이 있다&lt;/code&gt;는 것은 사실이 아니다!&lt;/p&gt;
&lt;p&gt;&lt;code&gt;user_id, id&lt;/code&gt; 두개의 Index로 조회하지 않고 PK를 사용하면 아래의 이점이 있었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;쿼리를 로직으로 노출할 수 있다.&lt;/li&gt;
&lt;li&gt;DB 예외가 발생하는 것이 아니라 앱에서 예외 처리가 가능하다.&lt;/li&gt;
&lt;li&gt;Non-Clustered Index의 매핑 비용을 없앨 수 있다.&lt;/li&gt;
&lt;li&gt;불필요한 인덱스가 생성될 위험이 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그렇다고 &lt;strong&gt;무조건 단건 조회시 PK를 사용해야 한다는 것은 아니다.&lt;/strong&gt; 예를 들어 정책 상 &lt;code&gt;해당 유저의 엔터티가 아니라면 존재하지 않는 것&lt;/code&gt;이라 한다면 404를 내려주는 것도 그리 잘못된 것은 아니다.&lt;/p&gt;
&lt;p&gt;이유를 정확히 알고 사용하자는 것이다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>Database/SQL</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/379</guid>
      <comments>https://jaehoney.tistory.com/379#entry379comment</comments>
      <pubDate>Sat, 16 Sep 2023 18:07:47 +0900</pubDate>
    </item>
    <item>
      <title>잘못된 학습 하지않기! (feat. UncheckedException)</title>
      <link>https://jaehoney.tistory.com/378</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;자바에서 CheckedException과 UncheckedException의 차이를 아는가? 구글에 CheckedException의 UncheckedException의 차이에 대해서 검색해봤다.&lt;/p&gt;
&lt;p&gt;상위 7~8개 정도의 블로그 모두 동일한 표가 있었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CmyKh/btswBCt0SGv/2KR8gYOqaJr4Y7eIrYvgz1/img.png&quot; alt=&quot;img.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;여기서 의아한 점이 &lt;strong&gt;UncheckedException은 예외가 발생 시 트랜잭션이 롤백된다는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;나는 이 부분이 잘못된 &lt;strong&gt;학습의 폐해&lt;/strong&gt;라고 말하고 싶다.&lt;/p&gt;
&lt;h4 id=&quot;트랜잭션&quot;&gt;트랜잭션&lt;/h4&gt;
&lt;p&gt;UncheckedException이 발생하면 트랜잭션이 롤백된다고 한다.&lt;/p&gt;
&lt;p&gt;트랜잭션은 MessageQueue 트랜잭션도 있고 DB 트랜잭션도 있고 다른 의미의 트랜잭션도 있을 수 있다. 그래서 &lt;strong&gt;트랜잭션&lt;/strong&gt;을 롤백한다는 것은 말이 안된다.&lt;/p&gt;
&lt;h4 id=&quot;데이터베이스-트랜잭션&quot;&gt;데이터베이스 트랜잭션&lt;/h4&gt;
&lt;p&gt;만약 데이터베이스 트랜잭션이라고 가정했을 때도 말이 안된다. &lt;strong&gt;자바 Exception&lt;/strong&gt;의 &lt;strong&gt;Level(수준)&lt;/strong&gt;에서 &lt;strong&gt;데이터베이스 트랜잭션&lt;/strong&gt;을 알 리가 없다.&lt;/p&gt;
&lt;p&gt;패키지 관점에서도 &lt;code&gt;java.lang.RuntimeException&lt;/code&gt;이 &lt;strong&gt;데이터베이스 트랜잭션&lt;/strong&gt;의 롤백 여부를 결정하는 것도 말도 안된다.&lt;/p&gt;
&lt;p&gt;DB 트랜잭션에서 &lt;strong&gt;롤백을 언제 실행할 지는 개발자가 정하는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;그러면 저런 문장의 기원이 뭘까..?&lt;/p&gt;
&lt;h2 id=&quot;오해의-기원---spring-transaction&quot;&gt;오해의 기원 - Spring Transaction&lt;/h2&gt;
&lt;p&gt;이 오해의 기원은 &lt;code&gt;Spring Transaction&lt;/code&gt;에서 시작된 것이다.&lt;/p&gt;
&lt;p&gt;아래 Spring 공식문서를 보면 &lt;code&gt;Spring Transaction&lt;/code&gt;에서 기본적으로 &lt;code&gt;RuntimeException&lt;/code&gt;및 그 하위 Exception이 발생할 경우 &lt;strong&gt;트랜잭션을 롤백&lt;/strong&gt;한다고 적혀있다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/rolling-back.html&quot;&gt;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/rolling-back.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfPsuu/btsw6EjQOPo/FhrA24DpOicXzQvU3yA1C0/img.png&quot; alt=&quot;img_1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;spring-tx&lt;/code&gt;의 &lt;code&gt;@Transactional&lt;/code&gt; 애노테이션에서도 아래와 같이 &lt;code&gt;rollbackFor&lt;/code&gt;에 대한 &lt;strong&gt;기본 동작&lt;/strong&gt;을 문서로 제공하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpTNUF/btsw6DZy0vj/cEFgjHR0uVESFqedYdzjO1/img.png&quot; alt=&quot;img_2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;매우 작은 범위(&lt;code&gt;Spring&lt;/code&gt; - &lt;code&gt;DB Transaction&lt;/code&gt; - &lt;code&gt;default configuration&lt;/code&gt;)안에서 발생하는 현상&lt;/strong&gt;을 가지고 &lt;code&gt;Java&lt;/code&gt;의 &lt;code&gt;UncheckedException&lt;/code&gt;의 개념인 것처럼 누군가 블로그를 작성했고, 그것이 &lt;strong&gt;Copy &amp; Paste로 와전&lt;/strong&gt;된 것이다.&lt;/p&gt;
&lt;p&gt;UncheckedException이 발생했을 때 Rollback이 발생하는 현상는 &lt;code&gt;Spring&lt;/code&gt;에서 제공하는 &lt;code&gt;Transaction&lt;/code&gt;의 &lt;strong&gt;기본 동작&lt;/strong&gt;일 뿐이고 롤백을 직접 수행하거나 Framework의 설정을 얼마던지 변경할 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;
&lt;p&gt;결론은 위 사례와 같은 &lt;strong&gt;무지성 학습&lt;/strong&gt;을 하면 안된다는 점이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;짧게 보면 지식 1개만 잘못되는 것&lt;/strong&gt;이지만 &lt;strong&gt;꼬리를 물고 물어&lt;/strong&gt; &lt;strong&gt;방향 자체가 틀어질 가능성&lt;/strong&gt;이 있다.  무지성 학습은 &lt;strong&gt;깊이 있게 지식을 확장해나가기 어렵다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;블로그를 참고하는 것은 좋지만 &lt;strong&gt;공식적이고 신뢰성이 충분히 확보&lt;/strong&gt;된 &lt;strong&gt;공식 레퍼런스&lt;/strong&gt;를 &lt;strong&gt;메인으로 학습&lt;/strong&gt;해야 하고, 빅테크 기업의 기술 블로그까지는 그나마 믿을만하다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;개인이 운영하는 검증되지 않는 블로그&lt;/strong&gt;는 정보가 부족할 때 &lt;strong&gt;단순 참고용&lt;/strong&gt;으로만 사용하자.&lt;/p&gt;
&lt;h2 id=&quot;번외---chatgpt-함정-수사&quot;&gt;번외 - ChatGPT 함정 수사&lt;/h2&gt;
&lt;p&gt;ChatGPT를 상대로 이 부분을 함정 수사하면 걸릴 것만 같았다.&lt;/p&gt;
&lt;p&gt;그래서 ChatGPT한테 아래와 같이 물어봤다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpAGHW/btswwSDTAxi/8HBkEfc1kDmWFiDPp2DkR1/img.png&quot; alt=&quot;img_3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;내 예상과 다르게 ChatGPT는 똑똑했고.. 내 질문에 낚이지 않았다!&lt;/p&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=_WkMhytqoCc&quot;&gt;https://www.youtube.com/watch?v=_WkMhytqoCc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>Etc./개발 일기</category>
      <author>JaeHoney</author>
      <guid isPermaLink="true">https://jaehoney.tistory.com/378</guid>
      <comments>https://jaehoney.tistory.com/378#entry378comment</comments>
      <pubDate>Fri, 15 Sep 2023 23:01:43 +0900</pubDate>
    </item>
  </channel>
</rss>