<?xml version="1.0"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>coyled.com</title>
    <link>https://spoqa.github.io</link>
    <atom:link href="https://spoqa.github.io/rss.xml" rel="self" type="application/rss+xml" />
    <description>stuff from coyled</description>
    <pubDate>Mon, 23 Mar 2026 02:55:19 +0000</pubDate>
    <lastBuildDate>Mon, 23 Mar 2026 02:55:19 +0000</lastBuildDate>

    
    <item>
      <title>KMP/CMP 마이그레이션, 정말 프로덕션에서 가능할까? - 키친보드 앱 마이그레이션 도전기</title>
      <link>https://spoqa.github.io/2026/03/12/cmp-migration.html</link>
      <pubDate>Thu, 12 Mar 2026 00:00:00 +0000</pubDate>
      <author>김진우</author>
      <guid>/2026/03/12/cmp-migration</guid>
      <description>&lt;p&gt;안녕하세요. 스포카 FE팀의 Android 개발자 김진우입니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Kotlin/Compose Multiplatform(이하 KMP/CMP) 마이그레이션, 정말 프로덕션에서 가능할까?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이 질문에 대한 답을 찾기 위해 저희 팀은 키친보드 Android 앱의 KMP/CMP 마이그레이션에 도전했습니다.&lt;/p&gt;

&lt;p&gt;그 과정에서 가장 큰 난관은 Compose Navigation의 한계로 인한 WebView 화면 상태 유실 문제였습니다.&lt;/p&gt;

&lt;p&gt;이로 인해 마이그레이션 자체를 포기해야 하나 고민했지만, 포기하지 않고 커스텀 네비게이션 아키텍처를 설계하여 문제를 해결할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;이 글에서는 위 문제를 어떻게 해결했는지, 그리고 그 과정에서 얻은 인사이트를 공유드리고자 합니다.&lt;/p&gt;

&lt;h2 id=&quot;마이그레이션-배경&quot;&gt;마이그레이션 배경&lt;/h2&gt;

&lt;p&gt;키친보드는 요식업 매장과 식자재 유통사의 주문·결제·커뮤니케이션 관리를 통합 지원하는 B2B SaaS 플랫폼으로, Android와 iOS 네이티브 앱을 각각 운영해 왔습니다.&lt;/p&gt;

&lt;p&gt;서비스가 성장하면서 양쪽 플랫폼의 기능 격차를 최소화하고, 더 빠른 기능 배포가 필요한 상황이었습니다.&lt;/p&gt;

&lt;h3 id=&quot;기술-스택-선택-과정&quot;&gt;기술 스택 선택 과정&lt;/h3&gt;

&lt;p&gt;멀티플랫폼 전략을 수립하면서 여러 선택지를 검토했습니다:&lt;/p&gt;

&lt;table&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;strong&gt;현행 유지&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;안정적 운영&lt;/td&gt;
      &lt;td&gt;플랫폼별 개발 비용 2배&lt;/td&gt;
      &lt;td&gt;❌ 장기적 효율성 부족&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;React Native&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;성숙한 생태계 및 웹 개발자 활용 가능&lt;/td&gt;
      &lt;td&gt;기존 Android 코드 재활용 불가&lt;/td&gt;
      &lt;td&gt;❌ 전면 재작성 필요&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Flutter&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;성숙한 생태계&lt;/td&gt;
      &lt;td&gt;기존 Android 코드 재활용 불가&lt;/td&gt;
      &lt;td&gt;❌ 전면 재작성 필요&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;KMP/CMP&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;기존 Android 코드 70%+ 재활용&lt;/td&gt;
      &lt;td&gt;비교적 미성숙한 생태계&lt;/td&gt;
      &lt;td&gt;✅ &lt;strong&gt;채택&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;KMP/CMP를 선택한 핵심 이유:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;기존 코드 재활용&lt;/strong&gt;: 이미 Kotlin으로 작성된 Business Logic과 Compose로 작성된 UI 재활용 가능&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Kotlin/Compose 기반 개발&lt;/strong&gt;: 기존 Android 개발 경험과 노하우를 그대로 활용&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;네이티브 성능 보장&lt;/strong&gt;: 100% 네이티브 성능 유지 (WebView 방식과 달리)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;플랫폼 특화 기능 지원&lt;/strong&gt;: DeepLink, Push, MLKit 등 네이티브 기능 완전 지원&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;네이티브-기능-호환성-검증&quot;&gt;네이티브 기능 호환성 검증&lt;/h3&gt;

&lt;p&gt;마이그레이션 전, 현재 서비스의 핵심 네이티브 기능들이 KMP/CMP에서 지원 가능한지 검증했습니다:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;기능&lt;/th&gt;
      &lt;th&gt;KMP/CMP&lt;br /&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;DeepLink&lt;/td&gt;
      &lt;td&gt;✅ 완전 지원&lt;/td&gt;
      &lt;td&gt;Platform-specific 코드 구현&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Push Notification&lt;/td&gt;
      &lt;td&gt;✅ 완전 지원&lt;/td&gt;
      &lt;td&gt;Platform-specific 코드 구현&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;MLKit / VisionKit&lt;/td&gt;
      &lt;td&gt;✅ 완전 지원&lt;/td&gt;
      &lt;td&gt;Platform-specific 화면 구현&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;커스텀 카메라&lt;/td&gt;
      &lt;td&gt;✅ 완전 지원&lt;/td&gt;
      &lt;td&gt;Platform-specific 화면 구현&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;파일 시스템 접근&lt;/td&gt;
      &lt;td&gt;✅ 완전 지원&lt;/td&gt;
      &lt;td&gt;expect/actual 연동&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Third-Party SDKs&lt;br /&gt;(Sendbird SDK 등)&lt;/td&gt;
      &lt;td&gt;✅ 일부 지원&lt;/td&gt;
      &lt;td&gt;미지원 시,&lt;br /&gt;각 Platform SDK를 expect/actual 연동&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;검증 결과, 모든 핵심 기능이 KMP/CMP에서 구현 가능함을 확인했습니다.&lt;/p&gt;

&lt;h3 id=&quot;마이그레이션-목표&quot;&gt;마이그레이션 목표&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;기존 사용자 경험 100% 유지&lt;/strong&gt;: 기존 프로덕션 앱의 모든 기능과 UX를 그대로 유지&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;플랫폼 간 코드 공유 극대화&lt;/strong&gt;: Business Logic, UI 70% 이상 공유&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장기적 유지보수 효율성 확보&lt;/strong&gt;: 한 번의 개발로 양쪽 플랫폼 배포&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;해결해야-했던-핵심-과제&quot;&gt;해결해야 했던 핵심 과제&lt;/h2&gt;

&lt;p&gt;키친보드 앱의 핵심 화면은 WebView 기반의 주문서 작성 화면입니다.&lt;/p&gt;

&lt;p&gt;사용자가 주문서를 작성하는 중에 다른 화면(예: 상품 상세, 상품 검색)으로 이동했다가 다시 돌아왔을 때, 작성 중이던 내용이 그대로 유지되어야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;compose-navigation의-한계&quot;&gt;Compose Navigation의 한계&lt;/h3&gt;

&lt;p&gt;초기에는 CMP를 위해 Compose Navigation을 도입(기존엔 Multi-Activity 구조)하려 했습니다. 하지만 다음과 같은 문제가 발생했습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Compose Navigation 방식&lt;/span&gt;
&lt;span class=&quot;nc&quot;&gt;NavHost&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;navController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;startDestination&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;home&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;composable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;webview&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;WebViewScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 백스택에서 복귀 시 재구성(Recomposition)됨&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;composable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;detail&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;DetailScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Compose Navigation의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavHost&lt;/code&gt;는 화면 전환 시 destination을 재구성(Recomposition)하는 특성이 있습니다.&lt;/p&gt;

&lt;p&gt;이로 인해 WebView 객체가 초기화되어, 백스택에서 WebView 화면으로 복귀할 때 리로딩되는 문제가 발생했고, 이는 다음과 같은 사용자 경험 저하로 이어졌습니다.&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;/ul&gt;

&lt;p&gt;이러한 문제를 근본적으로 해결(Recomposition 회피)하기 위해, Compose Navigation 대신 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FragmentManager(Android)&lt;/code&gt; 및 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UINavigationController(iOS)&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;img src=&quot;/images/2026-03-12-cmp-migration/cmp-custom-navigation-architecture.png&quot; alt=&quot;cmp-custom-navigation-architecture&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;1-type-safety-화면-정의---navdestination--navscreen&quot;&gt;1. Type Safety 화면 정의 - NavDestination &amp;amp; NavScreen&lt;/h3&gt;

&lt;p&gt;네비게이션의 타입 안전성을 보장하기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavDestination&lt;/code&gt; abstract class와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavScreen&lt;/code&gt; enum을 설계했습니다.&lt;/p&gt;

&lt;h4 id=&quot;navdestination---화면-라우트-정의&quot;&gt;NavDestination - 화면 라우트 정의&lt;/h4&gt;

&lt;p&gt;각 화면의 Route 패턴과 파라미터를 Type Safety하게 정의합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// NavDestination.kt - Navigator Module&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;open&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;pathParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PathParam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;emptyList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;open&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;queryParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;QueryParam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;emptyList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Type Safety Route 생성&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createRoute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;pathArgs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;emptyList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;queryArgs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NavParam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;emptyMap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Route에서 Type Safety 데이터 파싱&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SavedState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;D&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;실제 사용 예시:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// WebViewDestination.kt&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;object&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebViewDestination&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;WebViewDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;host&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;webview&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;QueryParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;QueryParam&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;StringType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;TRANSITION&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;transition&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;StringType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;RESULT_KEY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;resultKey&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;StringType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createRoute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HORIZONTAL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createRoute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;queryArgs&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mapOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;QueryParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;URL&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;QueryParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;TRANSITION&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;webViewParam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;QueryParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;RESULT_KEY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?,&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SavedState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;QueryParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;about:blank&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;transition&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;QueryParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;TRANSITION&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fromWebViewParam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;QueryParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;RESULT_KEY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 🔑 Type Safety 데이터 전달&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;navigate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;route&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebViewDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createRoute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;https://kitchenboard.co.kr/order&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;주문서 작성&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;transition&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;VERTICAL&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 🔑 Type Safety 데이터 접근&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;WebViewScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebViewDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;AndroidView&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;loadUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;navscreen---화면-메타데이터-관리&quot;&gt;NavScreen - 화면 메타데이터 관리&lt;/h4&gt;

&lt;p&gt;모든 화면을 Enum으로 정의하여 모든 화면 메타데이터를 한 곳에서 관리합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// NavScreen.kt&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HORIZONTAL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;launchMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavLaunchMode&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavLaunchMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;STANDARD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;SPLASH&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;destination&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SplashDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SplashDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Screen&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SplashScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;transition&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NONE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;launchMode&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavLaunchMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SINGLE_TASK&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;HOME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;destination&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HomeDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HomeDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Screen&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HomeScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;transition&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;VERTICAL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;launchMode&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavLaunchMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SINGLE_TASK&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;WEBVIEW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;destination&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebViewDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebViewDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Screen&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebViewScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;2-플랫폼-추상화---navigator-인터페이스&quot;&gt;2. 플랫폼 추상화 - Navigator 인터페이스&lt;/h3&gt;

&lt;p&gt;CMP에서 플랫폼 독립적인 네비게이션을 구현하기 위해 Navigator Module에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Navigator&lt;/code&gt; 인터페이스를 정의했습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Navigator&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;navigate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;navOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;navigateUp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;expect&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getNavigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Navigator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이를 통해 비즈니스 로직 레이어에서는 플랫폼에 독립적으로 네비게이션 API를 사용할 수 있게 되었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Android Implementation&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getNavigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Navigator&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AndroidNavigator&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// iOS Implementation&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getNavigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Navigator&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;IOSNavigator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;android-구현---fragmentmanager-기반&quot;&gt;Android 구현 - FragmentManager 기반&lt;/h2&gt;

&lt;h3 id=&quot;1-fragment-화면-단위---screenfragment&quot;&gt;1. Fragment 화면 단위 - ScreenFragment&lt;/h3&gt;

&lt;p&gt;각 화면을 독립적인 Fragment로 관리합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// ScreenFragment.kt&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenFragment&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Fragment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;currentRoute&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutableStateOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&amp;gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lazy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ARG_TRANSITION&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;safeValueOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;onCreateView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;inflater&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LayoutInflater&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ViewGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;savedInstanceState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Bundle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ComposeView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;requireContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;apply&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;setContent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// 각 Fragment가 독립적인 Compose 트리 소유&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;Screen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentRoute&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ARG_ROUTE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 동적으로 Route 변경 가능 (SINGLE_TOP, SINGLE_TASK 모드에서 활용)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;updateRoute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;putString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ARG_ROUTE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;currentRoute&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;route&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Recomposition 트리거&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;핵심 포인트:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;각 Fragment = 하나의 독립적인 Compose 트리&lt;/li&gt;
  &lt;li&gt;Fragment가 백스택에 유지되면 Compose 상태도 함께 유지&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mutableStateOf&lt;/code&gt;로 Route 변경 시 필요한 부분만 Recomposition&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;2-네비게이션-로직---androidnavigator&quot;&gt;2. 네비게이션 로직 - AndroidNavigator&lt;/h3&gt;

&lt;p&gt;WebView 상태 유지의 핵심은 &lt;strong&gt;Compose Recomposition을 회피&lt;/strong&gt;하는 것입니다. Fragment를 백스택에 유지하여 destroy되지 않도록 합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// AndroidNavigator.kt&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;navigate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;navOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// LaunchMode 처리&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;targetScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;launchMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nc&quot;&gt;NavLaunchMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SINGLE_TOP&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;현재&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;화면과&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;동일한&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;화면이면&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;err&quot;&gt;기존&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Fragment&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;의&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;updateRoute&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;호출&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
         &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;nc&quot;&gt;NavLaunchMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SINGLE_TASK&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;err&quot;&gt;백스택을&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;역순으로&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;탐색하여&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;동일한&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;화면의&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Fragment&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;를&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;찾으면&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
               &lt;span class=&quot;err&quot;&gt;상위&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;모든&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Fragment&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;제거&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;후&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;찾은&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Fragment&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;의&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;updateRoute&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;호출&lt;/span&gt;
               &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
         &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// 새 Fragment 추가&lt;/span&gt;
   &lt;span class=&quot;n&quot;&gt;fragmentManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;beginTransaction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;apply&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;fragment&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenFragment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;newInstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;targetTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fragment_container&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fragment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;nf&quot;&gt;addToBackStack&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;}.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;commit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;ios-구현---uinavigationcontroller-기반&quot;&gt;iOS 구현 - UINavigationController 기반&lt;/h2&gt;

&lt;p&gt;Android의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FragmentManager&lt;/code&gt;와 동일한 개념으로, iOS에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UINavigationController&lt;/code&gt;를 활용하여 네비게이션을 구현했습니다.&lt;/p&gt;

&lt;h3 id=&quot;1-uiviewcontroller-화면-단위---screenviewcontroller&quot;&gt;1. UIViewController 화면 단위 - ScreenViewController&lt;/h3&gt;

&lt;p&gt;Android의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ScreenFragment&lt;/code&gt;와 동일한 역할을 하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ScreenViewController&lt;/code&gt;를 구현했습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// ScreenViewController.kt&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenViewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?,&lt;/span&gt;
   &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UIViewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nibName&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bundle&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

   &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;currentRoute&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableStateOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

   &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;composeViewController&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ComposeUIViewController&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nc&quot;&gt;Screen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentRoute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nf&quot;&gt;addChildViewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;composeViewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addSubview&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;composeViewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;composeViewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;didMoveToParentViewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;updateRoute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;currentRoute&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;핵심 포인트:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;각 ViewController = 하나의 독립적인 Compose 트리&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ComposeUIViewController&lt;/code&gt;로 Compose UI를 UIKit에 통합&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mutableStateOf&lt;/code&gt;로 Route 변경 시 Recomposition&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;2-ios-네비게이션-관리---iosnavigator&quot;&gt;2. iOS 네비게이션 관리 - IOSNavigator&lt;/h3&gt;

&lt;p&gt;Android의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FragmentManager&lt;/code&gt;와 동일한 역할로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UINavigationController&lt;/code&gt;를 활용합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// IOSNavigator.kt - 핵심 로직&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;navigate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;navOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;navigationController&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UIApplication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sharedApplication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findNavigationController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;nf&quot;&gt;ensureNavigationDelegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;navigationController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// LaunchMode 처리 (Android와 동일)&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;targetScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;launchMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// UINavigationController에 push&lt;/span&gt;
   &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;viewController&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenViewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;targetTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;n&quot;&gt;navigationController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pushViewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;viewController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;animated&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;플랫폼-비교&quot;&gt;플랫폼 비교&lt;/h2&gt;

&lt;h3 id=&quot;android-vs-ios-구현-비교&quot;&gt;Android vs iOS 구현 비교&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;항목&lt;/th&gt;
      &lt;th&gt;Android&lt;/th&gt;
      &lt;th&gt;iOS&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;네비게이션 관리&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;FragmentManager&lt;/td&gt;
      &lt;td&gt;UINavigationController&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;화면 단위&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Fragment&lt;/td&gt;
      &lt;td&gt;UIViewController&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;상태 유지&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Fragment 백스택 유지&lt;/td&gt;
      &lt;td&gt;ViewController 백스택 유지&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;화면 전환&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;FragmentTransaction&lt;/td&gt;
      &lt;td&gt;pushViewController&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;추가-기능-구현&quot;&gt;추가 기능 구현&lt;/h2&gt;

&lt;h3 id=&quot;1-동적-트랜지션-제어---overridependingtransition&quot;&gt;1. 동적 트랜지션 제어 - overridePendingTransition&lt;/h3&gt;

&lt;p&gt;각 화면은 기본적으로 정의된 Transition을 가지지만, 때로는 런타임에 동적으로 변경해야 하는 경우가 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;사용 사례:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;WebView 화면: transition 파라미터에 따라 다른 트랜지션 적용&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Navigator 인터페이스&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Navigator&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;overridePendingTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt;
   &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 사용 예시&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;overridePendingTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;VERTICAL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;navigate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;some_screen&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;코드 구현:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// AndroidNavigator.kt&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;pendingTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;overridePendingTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;n&quot;&gt;pendingTransition&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transition&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;navigate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;navOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NavOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// WebView 특수 처리&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;targetScreen&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Screen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;WEBVIEW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;webViewData&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebViewDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;c1&quot;&gt;// transition 파라미터에 전달된 트랜지션 사용&lt;/span&gt;
      &lt;span class=&quot;nf&quot;&gt;overridePendingTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;webViewData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// 🔑 핵심: pendingTransition이 있으면 우선 사용, 없으면 기본값&lt;/span&gt;
   &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;targetTransition&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pendingTransition&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;targetScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transition&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// 사용 후 초기화 (일회성)&lt;/span&gt;
   &lt;span class=&quot;nf&quot;&gt;overridePendingTransition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이를 통해 화면별 기본 트랜지션을 유지하면서도, 필요할 때만 동적으로 오버라이드할 수 있게 되었습니다.&lt;/p&gt;

&lt;h3 id=&quot;2-a--b--a-화면-간-데이터-전달---screenresult&quot;&gt;2. A &amp;gt; B &amp;gt; A 화면 간 데이터 전달 - ScreenResult&lt;/h3&gt;

&lt;p&gt;B 화면에서 A 화면으로 결과를 전달해야 하는 경우가 있습니다. 예를 들어 주소 검색 화면에서 선택한 주소를 A 화면으로 전달하거나, 문서 스캔 완료 후 A 화면을 새로고침해야 하는 경우입니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;코드 구현:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Pending → Commit 패턴 (2단계 전송)&lt;/strong&gt;
    &lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;internal&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;mutex&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Mutex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;_data&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableSharedFlow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&amp;gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;extraBufferCapacity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;internal&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Flow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;asSharedFlow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;pendingResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;hasPendingResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 1단계: emit - 결과 예약만 (즉시 전송 X)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;suspend&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;emit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withLock&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;pendingResult&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;hasPendingResult&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 2단계: commit - 실제 전송 (onDispose에서 자동 호출)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;internal&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;suspend&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;commit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withLock&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hasPendingResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;emit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pendingResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;pendingResult&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;hasPendingResult&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;

    &lt;p&gt;&lt;strong&gt;왜 2단계로 분리했는가?&lt;/strong&gt;&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;iOS의 &lt;strong&gt;Swipe Back Gesture&lt;/strong&gt; 대응: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;navigateUp()&lt;/code&gt; 명시적 호출 없이도 화면이 닫힐 수 있음&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;결과 전송 타이밍 보장&lt;/strong&gt;: Emitter의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DisposableEffect.onDispose&lt;/code&gt;에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;commit()&lt;/code&gt;을 호출하여, 화면이 완전히 종료되는 시점에 결과 전송&lt;/li&gt;
    &lt;/ul&gt;

    &lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rememberScreenResultEmitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;coroutineScope&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rememberCoroutineScope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;screenResult&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;remember&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;ScreenResultRegistry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getOrPut&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nc&quot;&gt;DisposableEffect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 진입 시: null로 초기화&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;coroutineScope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;launch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;screenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;emit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 🔑 종료 시: 자동으로 commit (NonCancellable로 보장)&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;onDispose&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;coroutineScope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;launch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;withContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NonCancellable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;screenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;commit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;screenResult&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;전역 Registry 패턴 (Singleton 저장소)&lt;/strong&gt;
    &lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;internal&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;object&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenResultRegistry&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SynchronizedObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;results&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutableMapOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;()&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getOrPut&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;synchronized&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getOrPut&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;synchronized&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;synchronized&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;

    &lt;p&gt;&lt;strong&gt;왜 전역 Registry가 필요한가?&lt;/strong&gt;&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;독립적인 Compose 트리 연결&lt;/strong&gt;: Collector와 Emitter가 서로 다른 Fragment/ViewController의 Composable 트리에 존재&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;resultKey 기반 공유&lt;/strong&gt;: UUID 기반 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resultKey&lt;/code&gt;로 같은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ScreenResult&lt;/code&gt; 인스턴스 보장&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;프로세스 재생성 대응&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resultKey&lt;/code&gt;가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SavedState&lt;/code&gt;에 저장되므로, 앱이 백그라운드에서 종료되었다가 복원되어도 결과 전달 가능&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;MutableSharedFlow 버퍼 전략&lt;/strong&gt;
    &lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;_data&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableSharedFlow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&amp;gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;extraBufferCapacity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;internal&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Flow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;asSharedFlow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;

    &lt;p&gt;&lt;strong&gt;왜 extraBufferCapacity = 1 인가?&lt;/strong&gt;&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;결과 유실 방지&lt;/strong&gt;: Collector가 아직 구독을 시작하기 전에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;commit()&lt;/code&gt;이 호출되어도 결과가 버퍼에 저장됨&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;타이밍 이슈 해결&lt;/strong&gt;: 화면 전환 애니메이션 중 Collector의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LaunchedEffect&lt;/code&gt;가 아직 실행되지 않은 상태에서도 안전&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Collector의 생명주기 관리&lt;/strong&gt;
    &lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rememberScreenResultCollector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;onResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;suspend&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;resultKey&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rememberSaveable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;screenResult&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;remember&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;ScreenResultRegistry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getOrPut&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ScreenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nc&quot;&gt;LaunchedEffect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;screenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;collect&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;onResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nc&quot;&gt;DisposableEffect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;onDispose&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;ScreenResultRegistry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;screenResult&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;

    &lt;p&gt;&lt;strong&gt;설계 핵심:&lt;/strong&gt;&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;UUID 기반 키 생성&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rememberSaveable&lt;/code&gt;로 프로세스 재생성 시에도 동일한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resultKey&lt;/code&gt; 유지&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;LaunchedEffect로 구독&lt;/strong&gt;: Collector가 활성 상태일 때만 Flow 구독&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;자동 정리&lt;/strong&gt;: Collector 화면이 종료되면 Registry에서 제거하여 메모리 누수 방지&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;NonCancellable Context (결과 전송 보장)&lt;/strong&gt;
    &lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;onDispose&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;coroutineScope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;launch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;withContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NonCancellable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;screenResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;commit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;

    &lt;p&gt;&lt;strong&gt;왜 NonCancellable이 필요한가?&lt;/strong&gt;&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Dispose 중 결과 전송 보장&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onDispose&lt;/code&gt; 내부의 코루틴이 취소되어도 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;commit()&lt;/code&gt;은 반드시 실행&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;실전 예시: 주소 검색&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 주소 입력 화면&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;AddressInputScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;navigator&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalNavigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;current&lt;/span&gt;
   &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;selectedAddress&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;remember&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutableStateOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&amp;gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// 🔑 주소 검색 결과 받기&lt;/span&gt;
   &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;addressResultCollector&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rememberScreenResultCollector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;address&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;selectedAddress&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;address&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;nc&quot;&gt;SFButton&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;주소 검색&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;onClick&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;n&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;navigate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;route&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebViewDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createRoute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
               &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;https://kitchenboard.co.kr/address&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
               &lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;addressResultCollector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 연결&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
         &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// WebViewScreen (주소 검색 웹뷰)&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;WebViewScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebViewDestination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;navigator&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalNavigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;current&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// 🔑 결과 보내기&lt;/span&gt;
   &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;addressResultEmitter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultKey&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;rememberScreenResultEmitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;c1&quot;&gt;// WebView JavaScript Interface&lt;/span&gt;
   &lt;span class=&quot;nc&quot;&gt;LaunchedEffect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;webView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;webView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addJavascriptInterface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;object&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{
         @&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JavascriptInterface&lt;/span&gt;
         &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;onAddressSelected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;coroutineScope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;launch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
               &lt;span class=&quot;n&quot;&gt;addressResultEmitter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;emit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
               &lt;span class=&quot;n&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;navigateUp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
         &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Android&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;인사이트-정리&quot;&gt;인사이트 정리&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. “사용자 경험은 타협할 수 없다”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;처음에는 Compose Navigation을 그대로 사용하려 했습니다. 하지만 WebView 리로딩 문제를 발견하고 과감히 커스텀 네비게이션을 구축했습니다.&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;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. “포기하지 않으면 불가능은 없다”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Compose Navigation의 한계에 부딪혔을 때, “이러면 CMP 마이그레이션이 불가능한가?”라는 생각이 들었습니다. 하지만 포기하지 않고 다른 방법을 찾았습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;문제를 마주했을 때의 선택:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;❌ “Compose Navigation으로 안 되니 CMP를 포기하자”&lt;/li&gt;
  &lt;li&gt;✅ “Compose Navigation 대신 다른 방법을 찾아보자”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;결과적으로 FragmentManager와 UINavigationController라는 네이티브 해법을 찾아냈고, 오히려 더 나은 사용자 경험을 제공할 수 있었습니다.&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;strong&gt;3. “작은 유틸리티가 큰 차이를 만든다”&lt;/strong&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;화면마다 다른 Transition이 필요했습니다&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;overridePendingTransition&lt;/code&gt;과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ScreenResult&lt;/code&gt; 같은 작은 유틸리티들이 사용자 경험의 완성도를 크게 높였습니다. 핵심 아키텍처만큼이나 중요한 것은 세부 UX를 지원하는 유틸리티 계층입니다.&lt;/p&gt;

&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;

&lt;p&gt;KMP/CMP 마이그레이션을 고민하고 계신가요?&lt;/p&gt;

&lt;p&gt;이 글을 쓰면서 전하고 싶었던 메시지는 단 하나입니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“기술 스택의 제약은 프로젝트의 성공을 막을 수 없습니다.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;생태계가 성숙하지 않은 KMP/CMP에서는 예상치 못한 문제들을 마주하게 되지만, 포기하지 않고 문제의 본질을 이해하면 네이티브 해법을 찾을 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;여러분도 마이그레이션 과정에서 분명 예상치 못한 문제를 만나게 될 것입니다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;그때 “KMP/CMP가 아직 성숙하지 않아서 안 되는구나”보다는, “다른 방법은 없을까?”라고 질문해 보시기 바랍니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;마이그레이션은 완벽한 조건에서 시작하는 것이 아닙니다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;중요한 것은 문제를 마주했을 때 포기하지 않고 해결책을 찾아내는 것입니다.&lt;/p&gt;

&lt;p&gt;이 글이 KMP/CMP 마이그레이션을 망설이고 계신 분들께 작은 용기가 되었으면 좋겠습니다.&lt;/p&gt;

&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>피그마 플러그인 만들어줘</title>
      <link>https://spoqa.github.io/2025/09/09/figma-plugin-design.html</link>
      <pubDate>Tue, 09 Sep 2025 00:00:00 +0000</pubDate>
      <author>김동환</author>
      <guid>/2025/09/09/figma-plugin-design</guid>
      <description>&lt;p&gt;오랜만에 인사드립니다. 스포카에서 제품 디자인을 하고 있는 김동환(Donny)입니다.&lt;/p&gt;

&lt;p&gt;이전에 &lt;a href=&quot;https://brunch.co.kr/@okid2318/18&quot;&gt;개인 블로그에 바이브 코딩으로 Swift 앱을 제작한 이야기&lt;/a&gt;를 공유한 적이 있었는데요, 이번에는 피그마 플러그인을 직접 제작하고, 업무에 적용한 경험을 나눠보고자 합니다.&lt;/p&gt;

&lt;p&gt;매일 2~3시간, 총 3일 정도 걸려 완성했습니다. 사실 이런 단순한 통신과 Mapping을 다루는 플러그인은 이미 흔한 사례지만, 대부분은 TF 단위로 기획되고 만들어집니다.&lt;/p&gt;

&lt;p&gt;그런 점에서 이번 이야기는 PD 혼자서 가볍게 기획하고 제작할 수 있다는 가능성에 더 큰 의미가 있다고 생각합니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;프로젝트-커틀러리&quot;&gt;프로젝트 커틀러리&lt;/h2&gt;

&lt;p&gt;시간적인 이유로 아래와 같은 화면으로 디자인/설계 공유를 하는 분들이 많으실 것 같습니다. 저도 마찬가지였습니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-09-08/1.png&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;더미 데이터는 공유받는 사람, UT 참여자의 진지한 몰입에 도움이 됩니다. 하지만, 한땀한땀 넣기에는 시간, 체력, 멘탈적인 문제가 있을 수밖에 없습니다.&lt;/p&gt;

&lt;p&gt;특히 B2B SaaS 제품 같은 경우, 테이블이 아주 많습니다. Column이 8개, Row가 30개만 되더라도 도저히 채울 엄두가 나지 않는 것이 사실입니다. 그래서 Mapping 플러그인을 구상하게 되었습니다.&lt;/p&gt;

&lt;p&gt;프로젝트명은 커틀러리(식기)입니다. 손으로 먹지 말고, 도구를 써서 먹자는 바람을 담았습니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-09-08/2.png&quot; /&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;어떻게-만들지&quot;&gt;어떻게 만들지?&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://www.figma.com/plugin-docs/plugin-quickstart-guide/&quot;&gt;피그마에서 제공하는 공식 가이드&lt;/a&gt;를 참고하면, 생각보다 손쉽게 기본 환경 설정을 마칠 수 있습니다.&lt;/p&gt;

&lt;p&gt;구조를 간단히 살펴보면, 플러그인의 로직을 담당하는 code.ts와 실질적인 화면을 구성하는 ui.html로 이루어져 있습니다.&lt;/p&gt;

&lt;p&gt;팁으로 피그마 플러그인 같이 특수한 룰이 있는 환경에서는 AI 답변의 품질 향상을 위해 별도의 룰 문서나 오답 노트를 .md로 만드시면 좋습니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-09-08/3.png&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;목표를 명확하게 세워 봅시다. 우선 문자열과 이미지로 구성된 더미 데이터 뭉치가 있어야 하고, 그것들을 피그마 플러그인을 통해 레이어에 주입해야 합니다.&lt;/p&gt;

&lt;p&gt;문자 데이터는 Google Spreadsheets, 이미지는 Google Drive에 넣어 GCP API로 연결하는 것을 머릿속에 그렸습니다.&lt;/p&gt;

&lt;p&gt;주입 방식은 텍스트 레이어 이름과 칼럼명을 대조해 일치한다면, 하위 row 데이터를 랜덤으로 전달하는 방식입니다.&lt;/p&gt;

&lt;p&gt;여기까지만 해도 저는 완벽하다고 생각했습니다. 후후…&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-09-08/4.png&quot; /&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;도전-1-cors-이슈&quot;&gt;도전 1. CORS 이슈&lt;/h2&gt;

&lt;p&gt;API를 이상 없이 연결했지만, 값이 불러와지지 않았습니다. 콘솔로 확인해보니 CORS 문제가 발생하는 것을 알 수 있었습니다. &lt;a href=&quot;https://techblog.woowahan.com/21850/&quot;&gt;우아한 분들의 사례&lt;/a&gt;를 보니 Figma iframe에서는 보안상 이유로 외부 API 사용이 불가능하다고 합니다. 로컬 환경에서 프록시 서버로 우회하는 방식을 시도해보았습니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-09-08/5.png&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;엔드포인트를 작성하고, 더미 데이터가 정상적으로 불러와지는 것을 확인할 수 있었습니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-09-08/6.png&quot; /&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;도전-2-이미지-불러오기&quot;&gt;도전 2. 이미지 불러오기&lt;/h2&gt;

&lt;p&gt;Google Drive의 이미지도 같은 방식으로 가져왔습니다. 그런데 테스트 중 403 에러가 미친 듯이 발생했습니다. 가난한 나머지 GCP 요청 한도(Limit)에 도달한 것이었습니다.&lt;/p&gt;

&lt;p&gt;플러그인을 최초 실행할 때 캐싱하는 방식으로 해결해보려고 했지만, 데이터는 계속 업데이트 되어야했고, 초기 로딩 시간도 오래 걸렸습니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-09-08/7.png&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;결국 S3를 구걸하러 백엔드 챕터에 방문했고, 이미지는 S3 URL로 Spreadseets에 첨부했습니다. 요청 Limit과 초기 로딩 속도 문제는 이렇게 해결했습니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-09-08/8.png&quot; /&gt;
&lt;/figure&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-09-08/1.gif&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;이미지 요청은 구현 방식이 다양했고, 결국 코드를 읽고 고민 해야 했습니다. 결정을 온전히 AI에게 맡기는 순간, 같은 자리를 맴도는 느낌을 받았습니다.&lt;/p&gt;

&lt;h2 id=&quot;도전-3-mapping-book&quot;&gt;도전 3. Mapping Book&lt;/h2&gt;

&lt;p&gt;함께 협업하기 위해서는 어떤 레이어명에 어떤 칼럼 데이터가 Mapping되는지, 더미 데이터 형식은 어떤지 정리한 문서가 필요했습니다. 다수의 디자이너와의 협업을 고려해 해당 내용을 빠르게 Mapping Book으로 정리했습니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-09-08/9.png&quot; /&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;결과배포&quot;&gt;결과/배포&lt;/h2&gt;

&lt;p&gt;이제 새로운 컴포넌트, 테이블을 만들 때 텍스트 레이어명만 Mapping Book을 보고 적는다면, 더미 데이터를 빠르게 채울 수 있습니다. 더미 데이터 원본은 공유 파일이기 때문에 여러 디자이너들이 편집하고, 새로운 데이터 셋을 추가할 수 있습니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-09-08/2.gif&quot; /&gt;
&lt;/figure&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-09-08/3.gif&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;품목명, 이미지, 규격, 단위가 하나의 세트로 출력되며, 품목명이 긴 경우(Ellipsis 케이스)를 처리할 수 있는 옵션을 추가하는 것이 후속 작업입니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;불가능보다-가능을-말하기&quot;&gt;불가능보다 가능을 말하기&lt;/h2&gt;

&lt;p&gt;이전 직장에서 UX 라이팅 플러그인을 만들려고 했으나, 진행 도중 TF 구성원 각자의 사정으로 흐지부지 끝나버린 경험이 있습니다. 이런 Extra mile의 업무는 필요성을 느끼는 구성원을 모으는 것부터 일정까지 조율해야 하는 부분이 제작보다 더 힘든 것 같습니다.&lt;/p&gt;

&lt;p&gt;하지만 이제는 필요함을 느낀다면 그것을 정의하고 가볍게 바로 만들어 보면 됩니다. 올바르게 만들었다면 자연스럽게 구성원들의 공감과 지지를 얻을 것입니다. &lt;strong&gt;지금의 가장 큰 장벽은 귀찮음과 막연한 두려움이 아닐까 싶습니다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;아직 가야할 길이 멉니다. 그래픽 리소스 생성, 맞춤법 및 UX Writing 검사, 자어 수 계산, 반응형 별 화면 생성 등 추가할 기능들이 많습니다. 다음번에 또 재미있는 사례로 찾아 뵙겠습니다.&lt;/p&gt;

&lt;p&gt;시간 내어 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>보법이 다른 B2B 디자인</title>
      <link>https://spoqa.github.io/2025/04/21/b2b-design.html</link>
      <pubDate>Mon, 21 Apr 2025 00:00:00 +0000</pubDate>
      <author>김동환</author>
      <guid>/2025/04/21/b2b-design</guid>
      <description>&lt;p&gt;안녕하세요, 스포카에서 제품 디자인을 하고 있는 김동환(Donny)입니다.&lt;/p&gt;

&lt;p&gt;저는 이전에 콜드체인 이커머스와 O2O 매칭 서비스 등 B2C 제품 위주로 경력을 쌓아 왔고, B2B SaaS는 스포카에서 처음 경험했습니다. 이곳에서 제품을 만들며 디자이너로서 느낀 경험과 몇 가지 생각을 공유하고자 합니다.&lt;/p&gt;

&lt;p&gt;비슷한 도메인이라도 데이터를 중시하는 조직이 있는가 하면, 직관을 더 선호하는 조직도 있을 것입니다. 사업 분야, 규모, 조직의 성향과 전략에 따라 개인의 경험은 크게 달라질 수 있기 때문에 제 경험이 모든 B2B 조직을 대변할 수는 없다는 점을 미리 말씀드립니다.&lt;/p&gt;

&lt;hr /&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;Vertical Solution답게 해결하려는 고객의 문제와 환경이 매우 특수했고, 서비스 정책 간의 상호 연관성과 깊이도 상당했습니다.&lt;/p&gt;

&lt;p&gt;예를 들어 키친보드에서는 전처리, 선주문, 레거시 ERP 호환 등 기존 식자재 매장의 특수성이 존재했으며, 운영 방식을 충분히 이해한 후에야 제품 개선이 가능했습니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-04-07/1.png&quot; /&gt;
&lt;/figure&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;B2B 제품은 특성상 기능이 많고 활용 형태도 다양했기 때문에, 입사 초기에는 이전 리서치 자료를 꼼꼼히 읽고, 다양한 방식으로 툴을 사용해 보며 제품을 익혔던 기억이 납니다.&lt;/p&gt;

&lt;p&gt;쉐도잉이 특히 도움이 되었습니다. 명확한 역할과 상황, 목표를 설정한 뒤, 메소드 연기처럼 진심을 담아 제품을 사용해 보는 체험 방식입니다.&lt;/p&gt;

&lt;p&gt;사실 그전까지는 일부러 사전 학습을 최소화한 채 제품을 탐색하는 것을 선호했습니다. 이해도가 낮은 이 시기에 Happy Case를 따라가며 제품을 경험하는 것이 기존 디자이너들과는 조금 다른 관점으로 제품을 바라볼 수 있는 기회라고 생각했기 때문입니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-04-07/2.png&quot; /&gt;
&lt;/figure&gt;

&lt;hr /&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;이 점은 Micro A/B 테스트를 진행하며 정량 데이터에 많은 가치를 두던 이전 조직의 환경과는 달랐기 때문에 개인적으로 인상 깊었던 부분이었습니다.&lt;/p&gt;

&lt;p&gt;제품 개발 방식이 이런 모습을 갖게 된 것에는 이유가 있었습니다.&lt;/p&gt;

&lt;h3 id=&quot;트래픽이-적습니다&quot;&gt;트래픽이 적습니다&lt;/h3&gt;

&lt;p&gt;B2C 서비스 같은 경우 보통 십만에서 백만 단위의 트래픽이 일반적이지만, &lt;strong&gt;B2B는 유저 수 자체가 굉장히 적습니다.&lt;/strong&gt; 대신 LTV나 CAC 관점에서 한 명 한 명의 유저가 훨씬 더 비싸고 가치 있는 편입니다.&lt;/p&gt;

&lt;p&gt;이런 환경에서는 트래픽 기반의 A/B 테스트 같은 실험을 반복하며 제품을 개선하기 어렵습니다. 설령 실험 기간을 길게 잡더라도 제품 내·외부의 변인을 통제하고 통계적으로 유의미한 결과를 얻기 힘든 조건입니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-04-07/3.png&quot; /&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;지표-왜곡이-일어납니다&quot;&gt;지표 왜곡이 일어납니다&lt;/h3&gt;

&lt;p&gt;단순화 한 사례로 말씀드리자면, 아래 그래프는 매장이 앱에 들어와 주문을 완료하기까지의 구간별 잔존율입니다. 그래프만 보면 본격적으로 상품 탐색이 시작된 이후에는 중도 이탈이 거의 없는 형태입니다.&lt;/p&gt;

&lt;p&gt;하지만 애초에 탐색부터 시작하는 구매 퍼널이 과연 유저의 80%를 끝까지 이끌 수 있을까요? 모두가 정말 만족하고 있는 걸까요?&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-04-07/4.png&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;유저 인터뷰와 VOC를 통해 주문 과정에서의 불만 사항을 확인할 수 있었습니다. 쉐도잉을 해보니 정말 헉 소리 날 정도로 불편하더군요. 그렇다면 왜 이런 불편함은 데이터로 보이지 않았을까요?&lt;/p&gt;

&lt;p&gt;바로 &lt;strong&gt;매장은 주문을 하지 않으면 장사를 할 수 없기 때문입니다.&lt;/strong&gt; 불편해도 매장은 생업을 위해 주문을 해야 합니다. 그래서 사용성에 상관없이 이탈이 거의 없었던 것입니다.&lt;/p&gt;

&lt;p&gt;“그냥 다른 커머스로 시켜버리면 되는 거 아니야?”라고 생각할 수 있지만 매장은 한 유통사와 오랜 시간 거래하며 신뢰를 쌓아 온 관계를 쉽게 바꾸지 않는 특성을 가지고 있습니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-04-07/5.png&quot; /&gt;
&lt;/figure&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;인터뷰 드리븐은 특히 디자이너에게 매력적인 환경인 것 같습니다. 숫자만으로 결정을 내리기 힘든 상황에서, 시각 산출물로서 논리를 이어갈 수 있다는 점에서 디자이너의 Super Power가 부각된다고나 할까요?&lt;/p&gt;

&lt;p&gt;그동안 0.1% 앞에서 얼마나 많은 논리와 디자인이 매몰차게 거부됐나요. &lt;strong&gt;데이터 드리븐 환경에서는 직관이 거의 죄악에 가까웠습니다.&lt;/strong&gt; 하지만 이곳에서는 유저 행동의 행간을 읽고, 여러 형태의 근거를 모아 판단합니다. 그리고 내가 만든 것의 가치가 온전히 전달되었는지 세심하게 관찰합니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-04-07/6.png&quot; /&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;제품-사용-설명서&quot;&gt;제품 사용 설명서&lt;/h2&gt;

&lt;p&gt;유저에게 제품을 이해시키는 방법으로 FAQ, Help 페이지를 적극적으로 활용하는 것 또한 신기한 점 중 하나였습니다.&lt;/p&gt;

&lt;p&gt;제품 특성상 기능이 많기 때문에 어중간하게 요약해 설명하거나 복잡도가 높은 화면을 만들기보다는, 과감하게 사용 설명서로 링크를 시키거나 온보딩을 담당하는 영업팀에 도움을 요청하는 방식으로 풀어나가는 점이 새로웠습니다.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/2025-04-07/7.png&quot; /&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;페르소나의-중요성&quot;&gt;페르소나의 중요성&lt;/h2&gt;

&lt;p&gt;사실 B2B, B2C를 떠나 우리의 제품과 기능이 누구를 위한 것인지에 대한 &lt;strong&gt;뾰족한 설정&lt;/strong&gt;은 너무나도 기본적인 부분입니다.&lt;/p&gt;

&lt;p&gt;하지만 초기 B2B 같은 경우, 고객사 하나하나가 소중하기 때문에 &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;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;본질&quot;&gt;본질&lt;/h2&gt;

&lt;p&gt;B2B든 B2C든 본질은 똑같았습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;누구에게, 무엇을, 어떻게, 왜 제공할지를 뾰족하게 정하고, 그것을 효과적으로 수행할 수 있는 방법을 탐구하는 것.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;쉬워 보이면 쉬운 일이고, 어렵게 느껴지면 너무나도 어려운 그 본질 자체는 같았습니다.&lt;/p&gt;

&lt;h2 id=&quot;시장-관점에서-b2b&quot;&gt;시장 관점에서 B2B&lt;/h2&gt;

&lt;p&gt;마치며, 여담으로 사실 B2B 도메인을 선택한 데에는 시장 관점의 판단도 있었습니다.&lt;/p&gt;

&lt;p&gt;거시적인 관점에서 바라본 B2B는, 쉬운 문제가 대부분 해결된 시장에서 여전히 성장 동력이 많이 남아 있는 영역으로 보였습니다. 고객 영업과 획득이 어렵지만, 그만큼 Lock-in도 강해 변동성이 큰 시장 상황에서도 현금 흐름을 예측할 수 있고, 하방이 비교적 단단하다는 장점이 있다고 생각했습니다.&lt;/p&gt;

&lt;p&gt;물론 우리가 열광했던 폭발적인 J 형태의 성장 곡선은 기대하기 어렵지만, 상대적으로 안정적인 구조임은 분명해 보였습니다. 지금처럼 저성장·긴축·불안정성이 높은 시장 분위기에서는 오히려 더 매력적인 포지션이 아닐까 싶었습니다.&lt;/p&gt;

&lt;p&gt;뭔가 주식 블로그처럼 마무리된 느낌이네요. 아무튼, 이런 도전적인 시기에도 유·무형의 가치를 만들기 위해 애쓰는 메이커 여러분 모두를 진심으로 응원하며 글을 마치겠습니다.&lt;/p&gt;

&lt;p&gt;시간 내어 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>단지 권한 기능을 추가해달라고 했을 뿐인데(feat. 인증 기능 개선)</title>
      <link>https://spoqa.github.io/2025/04/18/improve-auth.html</link>
      <pubDate>Fri, 18 Apr 2025 00:00:00 +0000</pubDate>
      <author>남경호</author>
      <guid>/2025/04/18/improve-auth</guid>
      <description>&lt;p&gt;안녕하세요. 스포카 백엔드팀 프로그래머 남경호입니다.&lt;/p&gt;

&lt;p&gt;개발자라면 누구나 오랫동안 미뤄두었던 과제가 하나쯤 있을 것입니다. 업무의 우선순위가 낮거나 긴급한 과제들에 밀려 지속적으로 백로그에 쌓여 있던 작업 말이죠. 최근 저희팀에서 왜 오랜 시간 미뤄두었던 인증 방식 개선 작업을 진행하게 되었는지, 그 과정에서 얻은 여러 경험을 여러분께 공유하고자 합니다.&lt;/p&gt;

&lt;h1 id=&quot;배경&quot;&gt;배경&lt;/h1&gt;

&lt;p&gt;스포카 블로그를 꾸준히 보신 분이라면, &lt;a href=&quot;https://spoqa.github.io/2022/04/15/all-new-server.html&quot; target=&quot;\_blank&quot;&gt;서버 언어 전환 이야기&lt;/a&gt; 글에서 &lt;a href=&quot;https://en.wikipedia.org/wiki/JSON_Web_Token&quot; target=&quot;\_blank&quot;&gt;JWT&lt;/a&gt; 관련 문제를 언급하며 향후 개선할 예정이라고 소개했던 내용을 기억하실 겁니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/improve-auth/jwt.png&quot; alt=&quot;jwt&quot; /&gt;&lt;/p&gt;

&lt;p&gt;약 3년이 흐른 지금, 드디어 저희가 인증 방식 개선을 진행하게 된 가장 큰 이유는 바로 키친보드 매장 앱에 권한관리 기능이 추가되었기 때문입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/improve-auth/request-work.png&quot; alt=&quot;request-work&quot; /&gt;&lt;/p&gt;

&lt;p&gt;키친보드 매장 앱은 식자재 주문부터 거래대금 결제까지 다양한 기능을 제공합니다. 이 과정에서 사장님은 직원이 매장의 월 거래 내역 등 민감한 정보를 조회하지 못하도록 권한을 제어할 필요가 생겼는데요. 기존 JWT 인증은 무 상태(stateless) 특성상 권한 변경 시 즉각적으로 클라이언트의 인증 상태를 관리할 수 없다는 한계가 있었습니다.&lt;/p&gt;

&lt;p&gt;그래서 저희는 권한 기능을 추가하기에 앞서 인증 방식을 먼저 개선하기로 하였습니다.&lt;/p&gt;

&lt;h1 id=&quot;인증-방식-개선-방법&quot;&gt;인증 방식 개선 방법&lt;/h1&gt;

&lt;h2 id=&quot;refresh-token-도입&quot;&gt;Refresh Token 도입&lt;/h2&gt;

&lt;p&gt;앞서 이야기했듯이, JWT 기반의 인증 방식은 서버가 사용자의 상태를 저장하지 않습니다. 덕분에 서버의 확장성이 높고 서버 부하를 줄일 수 있다는 장점이 있지만, 한번 발급된 토큰을 서버에서 직접 제어할 수 없다는 단점이 있습니다.&lt;/p&gt;

&lt;p&gt;이러한 특성은 보안 문제로 연결될 수 있는데요. 만약 인증을 통해 발급받은 토큰이 탈취된다면, 서버가 이 토큰을 제어할 수 없으므로 악의적인 사용자는 손쉽게 탈취된 토큰을 이용하여 정상 사용자처럼 서비스를 이용할 수 있게 됩니다. 보통 이러한 보안 위험을 방지하기 위해 Access Token의 만료 시간을 짧게 설정하지만, 이 경우 사용자가 자주 로그인해야 하는 번거로움이 발생하게 됩니다.&lt;/p&gt;

&lt;p&gt;이와 같은 문제를 해결할 수 있는 대표적인 방법의 하나가 바로 &lt;a href=&quot;https://auth0.com/docs/secure/tokens/refresh-tokens&quot; target=&quot;\_blank&quot;&gt;Refresh Token&lt;/a&gt; 의 도입입니다. Refresh Token은 Access Token과 달리 서버가 상태를 관리하는 토큰으로, Access Token을 갱신하는 데 사용됩니다. 앞서 말씀드린 대로, Access Token의 탈취 위험을 낮추기 위해 Access Token의 만료 시간을 짧게 설정하는 것이 좋은데요. 이때 Refresh Token을 활용하면 사용자가 Access Token의 만료 시점마다 다시 로그인하지 않아도 편리하게 새로운 Access Token을 발급받을 수 있습니다.&lt;/p&gt;

&lt;p&gt;다음 그림에서 Access Token과 Refresh Token의 인증 과정을 자세히 확인할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/improve-auth/auth-flow-with-refresh-token.png&quot; alt=&quot;auth-flow-with-refresh-token&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;사용자가 로그인을 요청하면 서버는 Access Token과 Refresh Token을 발급합니다.&lt;/li&gt;
  &lt;li&gt;사용자는 발급받은 유효한 Access Token을 이용해 API를 호출하고, 서버는 요청된 데이터를 정상적으로 응답합니다.&lt;/li&gt;
  &lt;li&gt;사용자가 만료된 Access Token을 가지고 API 요청을 하면 서버는 401 인증 에러를 반환합니다. 이때 클라이언트는 Refresh Token을 사용하여 새로운 Access Token을 발급받고, 갱신된 Access Token으로 API를 재요청하여 정상적으로 데이터를 받을 수 있습니다.&lt;/li&gt;
  &lt;li&gt;하지만 만약 사용자의 Refresh Token까지 만료된 상태라면, 서버는 최종적으로 401 인증 에러를 반환하여 사용자의 다시 로그인을 요구합니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;위 과정을 통해 일반적으로 Access Token의 만료 시간을 짧게 설정하여 Access Token의 탈취 위험을 최소화하고, Refresh Token을 통해 사용자 편의성 또한 유지할 수 있습니다.&lt;/p&gt;

&lt;p&gt;아래 그림을 통해 Access Token의 탈취로 인한 공격 시나리오로 Access Token의 만료시간이 짧으면 짧을수록 보안 위험도가 감소하게 됩니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/improve-auth/malicous-case1.png&quot; alt=&quot;malicous-case1&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이처럼 Refresh Token을 적절히 도입하고 관리하면 토큰 탈취로 인한 보안 위험을 효과적으로 감소시킬 뿐만 아니라 사용자가 매번 로그인해야 하는 문제도 해결할 수 있으므로 사용성도 함께 챙길 수 있게 됩니다.&lt;/p&gt;

&lt;h2 id=&quot;refresh-token-rotation&quot;&gt;Refresh Token Rotation&lt;/h2&gt;

&lt;p&gt;한편, Access Token에 대한 탈취 위험은 Refresh Token도 동일한 것 아닌가? 라는 질문을 할 수 있을 것 같습니다. 맞습니다. Refresh Token이 탈취당하면 Access Token을 갱신할 수 있고 갱신된 Access Token을 통해 악의적 사용자는 손쉽게 탈취한 사용자인 척 서비스를 이용할 수 있게 됩니다.&lt;/p&gt;

&lt;p&gt;이러한 문제를 해결하기 위해 우리는 &lt;a href=&quot;https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation&quot; target=&quot;\_blank&quot;&gt;Refresh Token Rotation&lt;/a&gt; 을 도입하기로 합니다. Refresh Token Rotation은 아래와 같이 Refresh Token을 이용해 Access Token을 갱신할 때 Refresh Token도 함께 갱신하여 Refresh Token 탈취 시 발생할 수 있는 위험을 회피합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/improve-auth/malicous-case2.png&quot; alt=&quot;malicous-case2&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;이슈&quot;&gt;이슈&lt;/h1&gt;

&lt;h2 id=&quot;클라이언트의-네트워크-이슈&quot;&gt;클라이언트의 네트워크 이슈&lt;/h2&gt;

&lt;p&gt;앞서 저희는 Refresh Token Rotation을 이용하여 Refresh Token 탈취에 대한 위험성을 회피하고자 하였습니다. 이렇게 하면 Refresh Token을 이용하여 Access Token을 갱신 요청할 때 요청한 Refresh Token도 새롭게 발급되어 더 이상 Refresh Token을 사용할 수 없게 되는데요. 보안 수준은 강화되었지만, 클라이언트 개발자분들이 한가지 우려 점을 제기해 주셨습니다.&lt;/p&gt;

&lt;p&gt;모바일 기기 특성상 지하실이나 엘리베이터안과같이 네트워크가 원활하지 않은 곳에서 사용할 가능성이 존재하는데요. 이때 아래 그림과 같이 첫 번째 요청한 Refresh Token을 재요청하는 경우가 발생할 수 있습니다. 하지만 Refresh Token을 매번 갱신하기 때문에 동일한 Refresh Token으로 여러번 Access Token을 갱신요청하게 된다면 두번째 요청부터는  인증 에러가 발생하게 됩니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/improve-auth/network-issue-case.png&quot; alt=&quot;network-issue-case&quot; /&gt;&lt;/p&gt;

&lt;p&gt;그래서 저희는 &lt;a href=&quot;https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation#automatic-reuse-detection&quot; target=&quot;\_blank&quot;&gt;Token Family 방식&lt;/a&gt; 을 사용하여 요청 시마다 기존 Refresh Token을 지우지 않고 과거 토큰을 저장해 두었다가 첫 번째 요청으로 새롭게 발급된 토큰 또는 모종의 이유로 인해 갱신하지 못한 기존 토큰으로 토큰 갱신 요청을 할 수 있도록 구현하여 Refresh Token을 재사용할 수 있도록 하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/improve-auth/token-family-flow.png&quot; alt=&quot;token-family-flow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이로써 클라이언트는 네트워크 이슈가 발생해도 Refresh Token을 갱신할 수 있게 되었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/improve-auth/resolved-network-issue-flow.png&quot; alt=&quot;resolved-network-issue-flow&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;인증-토큰의-하위-호환&quot;&gt;인증 토큰의 하위 호환&lt;/h2&gt;

&lt;p&gt;한편, 저희는 JWT를 다루는 라이브러리로 &lt;a href=&quot;https://github.com/spoqa/jjwt&quot; target=&quot;\_blank&quot;&gt;JJWT&lt;/a&gt; 를 사용하고 있습니다. 앞서 JWT는 상태를 가지지 않기 때문에 사용자가 사용하는 Access Token을 서버에서 제어할 수 없다고 말씀드렸었는데요. 그래서 Access Token의 만료 시간을 두어 새롭게 Access Token을 발급받도록 하여 우회적으로 제어할 수 있습니다. Access Token을 만료시키는 또 다른 방법은, 해당 토큰을 생성할 때 사용된 암호키를 변경하는 것입니다. 저희는 그래서 클라이언트에서 사용하는 Access Token을 만료시키고 새롭게 변경된 권한을 사용하는 Access Token으로 사용하도록 하기 위해 암호키를 바꾸기로 하였습니다. 다만 여기서 발생하는 문제가 바로 앱의 업데이트 타이밍이었습니다.&lt;/p&gt;

&lt;p&gt;개발자라면 다들 잘 아시겠지만, 서버와 앱은 동일한 시점에 개발이 완료되더라도 배포되는 시점이 다를 수 있습니다. 서버는 배포하는 즉시 배포가 되지만 앱은 심사 과정이 필요하고 배포가 되더라도 앱스토어에 배포된 버전이 전파되기까지 1일 이상 소요될 수 있습니다. 그러다 보니 Access Token을 변경하기 위해 키를 변경하게 되면 서버가 배포된 이후부터 앱이 업데이트되기 전까지 사용자가 서비스를 이용할 수 없다는 문제가 생길 수 있습니다. 그래서 저희는 과거 버전의 앱에서도 새롭게 배포된 서버의 인증을 문제없이 사용할 수 있도록 방법을 모색해야 했습니다.&lt;/p&gt;

&lt;h3 id=&quot;jjwt-버전-변경&quot;&gt;JJWT 버전 변경&lt;/h3&gt;

&lt;p&gt;한편, 저희는 비밀키를 바꾸는 김에, 과거에 사용하던 서명 알고리즘(HS256)에 비해 보안성이 강화된 서명 알고리즘(PS256)을 변경하기로 합니다. 그러다 보니 JJWT라이브러리 버전을 업그레이드해야 했는데요. Gradle에 아래처럼 동일한 라이브러리를 서로 다른 버전으로 사용하는 경우 패키지 충돌이 발생하여 신규 버전에서 제공하는 함수를 사용할 수 없게 됩니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 구버전&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;implementation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;io.jsonwebtoken:jjwt:0.9.1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 신규버전&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;implementation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;io.jsonwebtoken:jjwt-api:0.12.6&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;runtimeOnly&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;io.jsonwebtoken:jjwt-impl:0.12.6&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;runtimeOnly&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;io.jsonwebtoken:jjwt-jackson:0.12.6&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/images/improve-auth/jjwt-version-conflict.png&quot; alt=&quot;jjwt-version-conflict&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이러한 문제를 해결하기 위해 저희는 &lt;a href=&quot;https://jitpack.io/&quot; target=&quot;\_blank&quot;&gt;Jitpack&lt;/a&gt; 을 사용하기로 합니다. JitPack은 GitHub에 호스팅된 라이브러리를 쉽게 빌드하고 배포할 수 있게 해주는 Maven/Gradle 용 리포지터리 서비스입니다. GitHub 저장소를 바탕으로 라이브러리를 빌드하므로, 별도의 중앙 저장소(예: Maven Central) 등록 과정을 거치지 않아도 된다는 장점이 있습니다. 그리고 오픈소스 저장소에, 한에 무료로 사용할 수 있다는 점도 장점입니다.&lt;/p&gt;

&lt;p&gt;저희는 JJWT 라이브러리를 fork하여 &lt;a href=&quot;https://github.com/spoqa/jjwt&quot; target=&quot;\_blank&quot;&gt;Spoqa용 JJWT Github 저장소&lt;/a&gt; 를 생성하였습니다. 그런 다음 충돌 패키지 충돌이 발생하지 않도록 패키지명을 변경해 주었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/improve-auth/changed-package-commit.png&quot; alt=&quot;changed-package-commit&quot; /&gt;&lt;/p&gt;

&lt;p&gt;그런 다음 Release를 생성해 주면, 아래와 같이 Jitpack에서 조회할 수 있게 됩니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/improve-auth/jitpack.png&quot; alt=&quot;jitpack&quot; /&gt;&lt;/p&gt;

&lt;p&gt;마지막으로 아래와 같이 Gradle에 의존성을 추가해주면, 패키지명이 변경된 JJWT라이브러리를 사용할 수 있게 됩니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;implementation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;com.github.spoqa:jjwt:1.0.2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;implementation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;javax.xml.bind:jaxb-api:2.3.1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;implementation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;io.jsonwebtoken:jjwt-api:0.12.6&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;runtimeOnly&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;io.jsonwebtoken:jjwt-impl:0.12.6&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;runtimeOnly&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;io.jsonwebtoken:jjwt-jackson:0.12.6&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;composite-패턴-vs-tokenmanager&quot;&gt;Composite 패턴 vs TokenManager&lt;/h3&gt;

&lt;p&gt;저희는 Jitpack으로 생성한 과거버전의 JJWT를 의존하는 구현체를 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LegacyJwtProcessor&lt;/code&gt;로 변경하고 신규 버전을 사용하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JwtProcessor&lt;/code&gt;를 새롭게 생성하였습니다. 그런 다음 아래와 같이 인증 로직에 과거 버전의 Access Token과 신규 버전의 Access Token을 모두 수용할 수 있도록 구현하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtAuthenticationProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;legacyJwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LegacyJwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;jwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AuthenticationProvider&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;authenticate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(!&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;supports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;java&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;principal&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;jwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AuthenticationException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;legacyJwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtUserAuthenticationToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;supports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtPreAuthenticationToken&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;java&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 구현하면 앱이 배포되기 전에 서버가 먼저 배포되어도 기존 버전을 사용하는 사용자가 정상적으로 로그인을 유지할 수 있게 됩니다.&lt;/p&gt;

&lt;p&gt;한편, 인증 로직을 구현하는 곳 말고도 LegacyJwtProcessor를 사용하는 곳이 다수 존재하였는데요. 그러다 보니 새롭게 만들어진 JwtProcessor로 전환하는 것을 누락할 가능성이 존재하였습니다. 다행히 기능 테스트가 있어 놓친 구현을 바로잡을 순 있었지만, 코드의 응집성 측면에서는 좋은 코드는 아니라 생각하였습니다.&lt;/p&gt;

&lt;p&gt;그래서 Composite 패턴을 사용해서 아래와 같이 구현해 볼지 생각을 하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtProcessor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;generateToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserPrincipal&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CompositeJwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;newJwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;legacyJwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtProcessor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;generateToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserPrincipal&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;jwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AuthenticationException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;legacyJwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NewJwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtProcessor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;generateToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserPrincipal&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;legacy&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtProcessor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;generateToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserPrincipal&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CompositeJwtProcessor&lt;/code&gt;를 이용하면 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JwtAuthenticationProvider&lt;/code&gt;는 더 이상 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;legacyJwtProcessor&lt;/code&gt;를 알지 않아도 되고 추후 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;legacyJwtProcessor&lt;/code&gt;가 제거되어도 영향범위는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CompositeJwtProcessor&lt;/code&gt;로 한정되기 때문에 응집도 높은 코드를 유지할 수 있게 됩니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtAuthenticationProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;compositeJwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AuthenticationProvider&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;authenticate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(!&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;supports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;java&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;principal&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;compositeJwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtUserAuthenticationToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다른 방법으로는 TokenManager라는 상위 수준의 클래스를 만들어 응집도를 높이는 방법도 생각해 보았습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TokenManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;jwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;legacyJwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LegacyJwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;refreshTokenService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RefreshTokenService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserPrincipal&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;jwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AuthenticationException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;legacyJwtProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;generateAccessToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;userPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;generateRefreshToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RefreshToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;replaceRefreshToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RefreshTokenUserPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 하면 Token을 Composite 패턴을 사용한 것과 같이 하위호환을 지키는 코드와 함께 토큰과 관련된 다른 기능들도 해당 클래스로 모을 수 있어 응집도를 상당히 높일 수 있게 됩니다.&lt;/p&gt;

&lt;p&gt;어떤 방식이 더 나은 방식이라고 말씀드리긴 어려울 것 같습니다. 다만, 저희는 LegacyJwtProcessor는 앱 배포 이후에 제거될 클래스이므로 불필요하게 Composite 패턴을 사용하기보다 TokenManager를 생성하여 코드 응집도를 높이는 방법으로 결정하게 되었습니다.&lt;/p&gt;

&lt;h3 id=&quot;spring-security---preauthorize&quot;&gt;Spring Security - PreAuthorize&lt;/h3&gt;

&lt;p&gt;저희는 인증과 인가를 위해 Spring Security를 사용하고 있습니다. JWT를 통해 인증된 사용자는 UserPrincipal이라는 인증된 사용자로 변환되고 UserPrinciapl이 가진 authorities를 통해 권한 처리를 하고 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AccessTokenAuthenticationProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;tokenManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TokenManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AuthenticationProvider&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;authenticate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(!&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;supports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;java&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;principal&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokenManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AccessTokenAuthenticationToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;supports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authentication&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AccessTokenPreAuthenticationToken&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;java&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AccessTokenAuthenticationToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AbstractAuthenticationToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorities&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setAuthenticated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;principal&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getCredentials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;사용자는 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Secured&lt;/code&gt;를 통해 권한을 검증받고 API를 호출할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@DgsMutation&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Secured&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;STORE_ADMIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;STORE_MANAGER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;VENDOR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@InputArgument&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreateOrderSheetInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreateOrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Secured&lt;/code&gt;는 단순한 권한 Role 기반 접근을 제어하기에 적절합니다. 이전까지 키친보드는 관리자, 매장 사용자, 유통사 사용자로 명확하게 Role이 나뉘어져 있었기 때문에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Secured&lt;/code&gt;는 요구사항을 충분히 충족하면서 단순하게 구현할 방법이었습니다.&lt;/p&gt;

&lt;p&gt;하지만 새로운 요구사항이 추가되면서 매장 사용자는 매장 관리자, 매장 직원으로 권한이 분리되게 되었는데요. 이에 따라 매장 사용자 모두 접근을 할 수 있는 API에는 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Secured(STORE_ADMIN, STORE_MANAGER)&lt;/code&gt; 표현해야 하는 불편함이 있게 됩니다. 거기다 만약 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;STORE_INTERN&lt;/code&gt;이 추가된다면 매장 사용자 권한을 가져야 하는 API를 모두 찾아서 바꿔줘야 하니 상당히 번거로운 작업이 될 것이고 자칫 권한 변경을 누락할 수 있는 위험성 또한 내포하고 있습니다.&lt;/p&gt;

&lt;p&gt;이와 같은 문제를 해소하기 위해 Spring Security에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@PreAuthorize&lt;/code&gt;를 이용하여 유연하게 권한을 체크하는 기능을 제공합니다. 그래서 저희는 아래와 같이 매장 사용자 여부를 확인하는 서비스 함수를 만들어 &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/expressions.html&quot; target=&quot;\_blank&quot;&gt;SpEL&lt;/a&gt; 을 이용해 권한을 체크하도록 함으로써 권한을 일일이 나열하지 않고 새로운 권한이 생기더라도 유연하게 대처할 수 있도록 하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@PostMapping&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/replace-store&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@PreAuthorize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@authorizationExpressionHelper.isManager()&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;replaceStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;@RequestBody&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ReplaceStoreRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ReplaceStoreResponse&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AuthorizationExpressionHelper&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;isManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrincipalProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;userPrincipal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isManager&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;마무리&quot;&gt;마무리&lt;/h1&gt;

&lt;p&gt;지금까지 저희가 권한 기능을 추가하기 위해 인증 로직을 어떻게 개선하였고 개선하면서 겪었던 이슈들을 공유해 보았습니다. 단순히 권한을 추가해 달라는 요구사항에서 시작되었지만, 그동안 우리가 가지고 있던 기술 부채도 해결함과 동시에 기술적인 여러 고민을 할 수 있어서 개인적으로 배운 게 많은 프로젝트였습니다.&lt;/p&gt;

&lt;p&gt;모쪼록 인증 기능 구현에 관심이 있으시거나 예정인 분들께 도움이 되었으면 합니다.&lt;/p&gt;

&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>식자재 품목 검색을 더 쉽게! 검색 엔진 도입과 개선</title>
      <link>https://spoqa.github.io/2025/03/04/es-dev.html</link>
      <pubDate>Tue, 04 Mar 2025 00:00:00 +0000</pubDate>
      <author>이지민</author>
      <guid>/2025/03/04/es-dev</guid>
      <description>&lt;p&gt;안녕하세요. 스포카 백엔드팀 프로그래머 이지민입니다.&lt;/p&gt;

&lt;p&gt;스포카에서는 식당 점주분들이 식자재 주문을 더 편리하게 하기 위한 많은 노력들을 하고 있습니다.
그중에서도, 주문하려는 품목을 검색하여 원하는 품목을 빠르게 찾을 수 있도록 품목 검색 기능을 제공하고 있는데요.&lt;/p&gt;

&lt;p&gt;검색 엔진 도입부터 지금의 검색이 되기까지의 과정들을 이야기해보려고 합니다.&lt;/p&gt;

&lt;p&gt;도입 초기에는 검색 엔진에 대한 이해가 깊지 않아, 논리적인 의사결정보다는 다양한 테스트를 통해 더 나은 결과를 찾는 방식으로 기능을 결정, 구현하였습니다.
이 점을 고려해 읽어주시길 바라며, 이 글은 검색 엔진의 점진적인 발전 과정을 다루는 이야기이니, 순차적으로 읽어보시면 개선 과정이 더욱 잘 이해되실 것 같습니다!&lt;/p&gt;

&lt;h1 id=&quot;검색-엔진-도입-배경&quot;&gt;검색 엔진 도입 배경&lt;/h1&gt;
&lt;p&gt;품목 검색 기능 초기에는 Database의 LIKE 질의를 통한 검색만 제공되었습니다. 이로 인해 품목명에 띄어쓰기가 다르거나 맞춤법이 정확히 일치하지 않는 경우, 사용자가 원하는 결과를 찾기가 어려웠습니다.&lt;/p&gt;

&lt;p&gt;예를 들어, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;깐마늘&lt;/code&gt;을 검색했을 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;마늘/깐&lt;/code&gt; 이라고 저장되어 있는 유통사 품목은 검색되지 않아 점주들은 깐마늘을 유통사가 취급하지 않는다고 오해하는 상황이 발생하곤 했습니다.&lt;/p&gt;

&lt;p&gt;이와 같은 문제와 사용하는 점주의 수가 증가하고 품목의 종류가 다양해짐에 따라, DB 검색 기능의 한계가 더 드러나게 되었고 이를 해결하기 위해 검색 엔진 도입의 필요성이 대두되었습니다.&lt;/p&gt;

&lt;p&gt;이번 검색 엔진 도입이 스포카에서 최초 도입은 아닌데요.(하지만 제가 처음이에요.) (구)도도카트 서비스 운영 당시 많은 명세표 품목을 검색하는데 Elasticsearch 검색 엔진을 활용했었습니다.
우선 별도의 검색 품질에 대한 기준이 마련되어 있지 않았기 때문에 (구)도도카트 서비스의 검색 엔진 설정을 참고해 Elasticsearch(이하 ES)를 POC 해보기로 했습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/es-dev/es0-1.jpg&quot; alt=&quot;background&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;db-like-검색과-es-검색-비교-poc&quot;&gt;DB LIKE 검색과 ES 검색 비교 POC&lt;/h2&gt;
&lt;p&gt;품목 데이터는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;product&lt;/code&gt; 라는 인덱스에 다음과 같은 setting 으로 구성했습니다.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;product&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;mappings&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;properties&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;analyzer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;korean&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;fields&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;ngram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;analyzer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;korean_ngram&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;settings&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;index&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;analysis&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;edge_ngram_back&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;min_gram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;side&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;back&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;edge_ngram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;max_gram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;5&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;edge_ngram_front&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;min_gram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;side&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;front&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;edge_ngram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;max_gram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;5&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;analyzer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;korean&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;lowercase&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;trim&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;custom&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tokenizer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;nori_mixed&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;korean_ngram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;lowercase&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;edge_ngram_front&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;edge_ngram_back&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;trim&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;custom&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tokenizer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;nori_mixed&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tokenizer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;nori_mixed&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;nori_tokenizer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;decompound_mode&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;mixed&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;ul&gt;
  &lt;li&gt;품목명을 저장할 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; field, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name.ngram&lt;/code&gt; field 구현&lt;/li&gt;
  &lt;li&gt;한글 검색의 정확성과 유연성을 높이기 위해 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-nori-tokenizer.html&quot;&gt;Nori Tokenizer&lt;/a&gt;와 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-edgengram-tokenfilter.html&quot;&gt;Edge N-gram&lt;/a&gt; 필터를 활용해 띄어쓰기나 일부 단어만으로도 검색이 가능하도록 설정&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;또한, LIKE 질의를 사용하는 기존 DB 검색과 ES 검색의 결과를 비교한 POC 결과는 다음과 같습니다.
&lt;img src=&quot;/images/es-dev/es0-2.png&quot; alt=&quot;es-start&quot; /&gt;&lt;/p&gt;

&lt;p&gt;결과를 통해 볼 수 있듯이, 정확하지 않은 키워드로 검색했을 때도 기존 DB의 LIKE 질의보다 ES 검색이 훨씬 더 나은 결과를 제공하는 것을 확인할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;비용 증가와 관리 포인트가 늘어남에도 불구하고 앞서 언급한 문제들이 해소되었고 검색 품질을 크게 항상시킬 수 있을거 같아 검색 엔진 도입을 최종적으로 결정하게 되었습니다!&lt;/p&gt;

&lt;p&gt;다만, 이번 테스트는 전체 검색어가 아닌 일부 검색어를 대상으로 진행된 POC 였기 때문에, 실제 사용자의 피드백을 바탕으로 지속적인 수정과 개선이 필요할 것으로 예상하고 있었습니다.
이러한 부분을 미리 인지하고 마음의 준비(?)와 공부를 하고 있었죠.&lt;/p&gt;

&lt;h1 id=&quot;개선-작업&quot;&gt;개선 작업&lt;/h1&gt;
&lt;h2 id=&quot;1-가중치-조절-및-n-gram-조정&quot;&gt;1) 가중치 조절 및 N-gram 조정&lt;/h2&gt;
&lt;h3 id=&quot;이슈-및-원인-분석&quot;&gt;이슈 및 원인 분석&lt;/h3&gt;
&lt;p&gt;검색 엔진을 적용한 후에 아래와 같은 피드백이 들어왔습니다.
&lt;img src=&quot;/images/es-dev/es1-1.png&quot; alt=&quot;es-bacon-result&quot; /&gt;&lt;/p&gt;

&lt;p&gt;주문하려고 했던 품목은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통베이컨(에스푸드)&lt;/code&gt;였지만, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통베&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통베이&lt;/code&gt; 키워드로 검색했을 때 상위에 노출되지 않는다는 이슈였습니다.
이 문제를 해결하기 위해, 우선 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통베&lt;/code&gt;라는 검색어를 중심으로 원인을 분석해보았습니다.&lt;/p&gt;

&lt;p&gt;문제 원인 파악을 위해 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html&quot;&gt;_analyze&lt;/a&gt; API를 활용하여 name 필드에 적용된 분석기(analyzer)가 검색어를 어떻게 토큰화하는지 살펴보았습니다.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;product/_analyze&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;field&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;통베이컨(에스푸드)&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;response&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tokens&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;token&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;통&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;start_offset&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;end_offset&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;word&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;position&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;token&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;베이컨&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;start_offset&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;end_offset&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;word&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;position&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;token&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;에스&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;start_offset&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;end_offset&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;word&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;position&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;token&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;푸드&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;start_offset&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;end_offset&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;word&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;position&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;결과는 [&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;베이컨&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;에스&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;푸드&lt;/code&gt;] 로 예측 가능하게 나오네요.&lt;/p&gt;

&lt;p&gt;그러나 문제의 검색어인 통베는 다음과 같이 토큰화되었습니다:
[&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;베&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;베&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;어&lt;/code&gt;]&lt;/p&gt;

&lt;p&gt;잠깐 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;어&lt;/code&gt; 는 뭐지? 라고 생각하실 수 있는데요. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통베&lt;/code&gt;라는 단어 어디에도 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;어&lt;/code&gt; 라는 단어는 찾아볼 수 없기 때문이죠.&lt;/p&gt;

&lt;p&gt;이는 ES의 Nori Tokenizer 가 한국어 문장에서 어미를 추출하는 방식을 따라 토큰화하기 때문입니다.
예를 들어, “강아지가 밥을 먹습니다”라는 문장은 [&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;강아지&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;가&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;밥&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;을&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;먹&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;습니다&lt;/code&gt;]로 명사와 어미를 구분하여 토큰화됩니다.
따라서 정확히 알기는 어렵지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;어&lt;/code&gt;는 Nori Tokenizer 가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;베&lt;/code&gt;에서 어미로 분리한 결과로 예상하고 있어요.&lt;/p&gt;

&lt;p&gt;이런 토큰화 방식을 보고 Nori Tokenizer 의 토큰화는 단어별로 검색하는 패턴이 많은 저희 서비스에서 예측 불가능할 수 있겠다라는 깨달음을 얻었어요.
하지만 Nori를 아예 제거하기엔 Nori 가 해주는 명사 추출의 이점이 있을 수 있어 조심스러웠습니다.
결론적으로, Nori 기능을 완전히 제거하는 대신, 다른 접근을 시도하기로 했습니다.&lt;/p&gt;

&lt;h3 id=&quot;쿼리-가중치-조절-poc&quot;&gt;쿼리 가중치 조절 POC&lt;/h3&gt;
&lt;p&gt;문제 해결을 위해 쿼리의 가중치를 조절해보기로 했습니다.
기존엔 쿼리 가중치를 순수 Nori Tokenizer 가 적용된 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; 필드와 Nori Tokenizer 와 N-gram filter 가 적용된 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name.ngram&lt;/code&gt; 필드에 각각 10과 5 를 주고 있었는데요.
따라서 N-gram 에 의해 검색된 품목보다 순수 Nori 에 의해 검색된 품목의 유사도가 높아져 상위에 올라가게 됩니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통베&lt;/code&gt; 라고 검색했을때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통베이컨(에스푸드)&lt;/code&gt; 품목이 올라오기 위한 가중치 조절과 N-gram Filter 조정이 필요해보였습니다.
&lt;img src=&quot;/images/es-dev/es1-2.jpeg&quot; alt=&quot;sapjil&quot; /&gt;
최적의 가중치와 N-gram Filter 설정을 찾기 위해 통베이컨 품목을 기준으로 &lt;del&gt;삽질을&lt;/del&gt; 테스트를 아래와 같이 해보았는데요.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/es-dev/es1-3.png&quot; alt=&quot;es-ngram&quot; /&gt;
&lt;img src=&quot;/images/es-dev/es1-4.png&quot; alt=&quot;es-ngram&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Nori Tokenizer 에만 의존하기에는 무리가 있을거 같아 ES 기본 Tokenizer 인 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-standard-tokenizer.html&quot;&gt;Standard Tokenizer&lt;/a&gt; 도 추가해서 테스트 해봤습니다. 
가중치의 경우, 순수 Nori 인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nori&lt;/code&gt; 와 N-gram 을 적용한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ngram&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;standard&lt;/code&gt; 필드의 가중치를 조정해보며 각각 2.0, 3.0, 2.0 일 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통베이컨(에스푸드)&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;세척당근&lt;/code&gt;이 가장 잘 검색되는 것을 확인 했습니다.&lt;/p&gt;

&lt;p&gt;여기에서 Edge N-gram 에 대해서 간단히 설명드리자면요.&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;min_gram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;max_gram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;3&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;side&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;front&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;edge_ngram&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;ul&gt;
  &lt;li&gt;min_gram : 최소 토큰 길이&lt;/li&gt;
  &lt;li&gt;max_gram : 최대 토큰 길이&lt;/li&gt;
  &lt;li&gt;side : 단어의 어느 부분부터 토큰화 할지 설정(front/back)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;side 가 front 인 위 예시로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;안녕하세요&lt;/code&gt;를 토큰화해보면 [&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;안&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;안녕&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;안녕하&lt;/code&gt;]로 토큰화되고 side 가 back 일 경우엔 [&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;요&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;세요&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;하세요&lt;/code&gt;]로 토큰화 됩니다.
때문에 front 의 경우 주로 첫 글자부터 검색하는 자동완성과 같은 곳에서 사용하고 back 은 주로 뒷글자부터 검색하는 경우, 예를들면 영어로 ion 을 검색했을 때 action, station, evolution 같은 것들을 검색할 때 유용하게 사용할 수 있을거예요.&lt;/p&gt;

&lt;p&gt;저희는 식자재 검색라는 특성이 있어 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통베&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;세척당&lt;/code&gt;과 같이 앞글자부터 검색하는 경우가 많기 때문에 back 은 제거하고 front 만 남기기로 했습니다. max_gram 도 기존엔 5로 토큰화가 많이 되어 오히려 정확성을 떨어트리는 것을 발견했고 적절해보이는 3으로 조정했습니다.&lt;/p&gt;

&lt;h3 id=&quot;결론&quot;&gt;결론&lt;/h3&gt;
&lt;p&gt;결론적으로 아래 조정 작업으로 문제가 되었던 품목이 검색 상위에 안정적으로 노출되도록 검색 품질을 향상시켰습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;N-gram 조정: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_gram&lt;/code&gt; 값을 5 -&amp;gt; 3으로 하향 조정하고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;side: front&lt;/code&gt; 만 사용&lt;/li&gt;
  &lt;li&gt;가중치 조정: Nori, N-gram, Standard 분석기의 가중치를 적절히 분배&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;2-wildcard-검색&quot;&gt;2) Wildcard 검색&lt;/h2&gt;
&lt;h3 id=&quot;이슈-및-원인-분석-1&quot;&gt;이슈 및 원인 분석&lt;/h3&gt;
&lt;p&gt;위 작업을 배포하고 내부에서 아래와 같은 피드백을 받았습니다.
&lt;img src=&quot;/images/es-dev/es2-1.png&quot; alt=&quot;es2&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;칠성사이다/355ml*24캔&lt;/code&gt;라는 품목이 있는데도 불구하고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;사이다&lt;/code&gt; 라고 검색했을때 검색이 되지 않는 이슈였는데요.
각 분석기에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;칠성사이다/355ml*24캔&lt;/code&gt;이 토큰화된 결과는 다음과 같았습니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Nori: 칠성사, 칠, 성사, 이, 다, 355, ml, 24, 캔

N-gram: 칠, 칠성, 칠성사, 칠, 성, 성사, 이, 다, 3, 35, 355, m, ml, 2, 24, 캔

Standard: 칠성사이다, 355ml, 24캔
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;결과에서 확인할 수 있듯이, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;사이다&lt;/code&gt;라는 토큰이 생성되지 않아 검색 결과에서 제외된 것입니다.&lt;/p&gt;

&lt;p&gt;위에서 가중치와 N-gram 을 조정했는데도 불구하고 왜 사이다로 토큰화되지 않았을까요?
이는 N-gram 은 Filter 이기 때문에 Nori 분석기에서 생성된 토큰을 기반으로 토큰을 더 잘게 나누는 필터링을 수행하기 때문이에요.
즉, Nori 분석기가 사이다를 하나의 단어로 인식하지 못하고 어미(이, 다)로 나누어버렸기 때문에, 사이다라는 토큰 자체가 존재하지 않았던 것이죠,,&lt;/p&gt;

&lt;p&gt;만약 칠성고구마였다면 어떻게 되었을까요?&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;nori: [칠성, 고구마, 355, ml, 24, 캔]

ngram: [칠, 칠성, 고, 고구, 고구마, 3, 35, 355, m, ml, 2, 24, 캔]

standard: [칠성고구마, 355ml, 24캔]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/images/es-dev/es2-2.jpeg&quot; alt=&quot;huguma&quot; width=&quot;300&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이처럼 명사 단위로 토큰화하기 때문에 &lt;a href=&quot;https://bitbucket.org/eunjeon/mecab-ko-dic/src/master/&quot;&gt;Nori 명사 사전&lt;/a&gt;에 명사 존재 여부에 따라 토큰화가 다르게 됩니다. 명사 사전에 존재하는 품목의 경우, 고구마처럼 검색이 훨씬 매끄러울 수 있을거예요.
사이다도 명사 사전에 등록되어 있었다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;칠성사이다&lt;/code&gt;도 [&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;칠성&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;사이다&lt;/code&gt;] 로 토큰할 수 있었겠죠.&lt;/p&gt;

&lt;h3 id=&quot;user-dictionary&quot;&gt;User Dictionary&lt;/h3&gt;
&lt;p&gt;따라서, &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-nori-tokenizer.html&quot;&gt;사용자 사전(user_dictionary)&lt;/a&gt; 도입을 고려했었는데요. Nori Tokenizer 에게 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;사이다&lt;/code&gt;는 명사야, 혹은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;칠성사이다&lt;/code&gt;는 [&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;칠성&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;사이다&lt;/code&gt;] 라고 토큰화 해! 라고 인식할 수 있는 기준을 마련해줄수있는 방법이에요.&lt;/p&gt;

&lt;p&gt;하지만 몇가지 한계가 있었어요.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;관리 포인트 증가
    &lt;ul&gt;
      &lt;li&gt;관리해야 할 품목의 종류가 너무 많아 어려움이 발생&lt;/li&gt;
      &lt;li&gt;농산물, 곡류, 축산물, 수산물 등 수백에서 수천 가지 품목을 주기적으로 업데이트하기 어려운 환경&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;표준화되지 않은 품목명
    &lt;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;/p&gt;

&lt;h3 id=&quot;wildcard-field&quot;&gt;Wildcard Field&lt;/h3&gt;
&lt;p&gt;문제를 다시 분석한 결과, 검색어 자체가 포함된 품목을 반환하는 것이 핵심이라는 점을 확인했습니다.
이는 마치 DB의 LIKE 쿼리처럼 검색어가 포함된 품목을 반환하는 것이죠.
ES에서는 이러한 기능을 제공하는 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/7.17/keyword.html#wildcard-field-type&quot;&gt;Wildcard&lt;/a&gt; 필드를 활용할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;Wildcard 필드를 추가하는 방법은 간단합니다.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;mappings&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;properties&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;analyzer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;korean&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;fields&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;wildcard&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;wildcard&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//..&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Wildcard 필드는 역색인 구조가 아닌 패턴 매칭 방식을 사용하기 때문에 성능 문제가 발생할 수 있어서 신중히 사용해야 합니다. 모든 토큰을 검사해야 하기 때문에 데이터가 많아질수록 메모리 사용량이 많아지고 성능이 떨어질 수 있습니다.
따라서 Wildcard 필드 대신 정교한 N-gram 을 사용하거나 Query-String 쿼리를 권장합니다.&lt;/p&gt;

&lt;p&gt;하지만 저희는 데이터량이 많지 않고, 필터를 통해 조회되는 데이터 수를 제한할 수 있었기 때문에 성능 부담이 아직까진 크지 않아 Wildcard 필드를 사용하기로 결정했습니다.&lt;/p&gt;

&lt;p&gt;Wildcard 필드를 활용한 쿼리는 다음과 같이 구성했습니다:&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;query&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;bool&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;must&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;bool&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;should&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;wildcard&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name.wildcard&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;boost&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;100.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;wildcard&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;*사이다*&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;multi_match&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;fields&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;name^3.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;name.ngram^4.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;name.standard^3.0&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;query&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;사이다&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;검색어가 포함된 결과는 상단으로 올리되, 포함된 결과 내에서도 유사도에 맞게 정렬되도록 쿼리를 수정했습니다. 위처럼 할 경우 사이다가 포함된 단어는 100 점을 추가로 받고 match 되는 필드에 따라 점수를 추가로 더해지게 됩니다.&lt;/p&gt;

&lt;p&gt;예를들어 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;칠성사이다&lt;/code&gt; , &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;칠십성사이다&lt;/code&gt; , &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;칠성사이&lt;/code&gt; 라는 품목이 있을때, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;사이다&lt;/code&gt; 라고 검색하면 wildcard 에 의해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;칠성사이다&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;칠십성사이다&lt;/code&gt; 가 가장 상단으로 나오게 될테고, 품목의 이름이 더 짧아 유사도가 더 높은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;칠성사이다&lt;/code&gt; 가 최상단으로 나오게 될거예요.
기존의 가중치는 유지하되 검색어가 포함된 결과만 올리기 위한 쿼리입니다.&lt;/p&gt;

&lt;h3 id=&quot;결론-1&quot;&gt;결론&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;User Dictionary 도입을 고려했으나 유지보수에 대한 한계로 제외&lt;/li&gt;
  &lt;li&gt;Wildcard 필드와 쿼리로 검색어가 포함된 품목의 점수를 높임&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;3-초성-검색-feat-icu&quot;&gt;3) 초성 검색 feat. ICU&lt;/h2&gt;
&lt;p&gt;Wildcard 검색까지 구현하고 나니 검색 되지 않는 품목 없이 꽤 안정화된 검색 결과를 제공할 수 있었는데요. 더 편리한 검색을 위한 초성 검색 니즈가 들어 왔습니다.&lt;/p&gt;

&lt;h3 id=&quot;어떤-extension-을-사용할-것인가&quot;&gt;어떤 extension 을 사용할 것인가&lt;/h3&gt;
&lt;p&gt;초성검색을 위해 지금 시스템에 도입할 수 있고, 적당한 레퍼런스가 있는 두가지 extension으로 POC 를 진행해봤어요.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/netcrazy/elasticsearch-jaso-analyzer&quot;&gt;elasticsearch-jaso-analyzer&lt;/a&gt;(이하 JASO)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-icu.html&quot;&gt;analysis-icu&lt;/a&gt;(이하 ICU)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;결론적으로 ICU 를 선택했는데요, JASO 에 대한 설명이 너무 길어질거 같아 자세한 설정 방법과 설명은 위 주소에서 참고주시길 바랍니다.
두 분석기를 비교한 결과는 아래 표로 정리되었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/es-dev/es3-1.png&quot; alt=&quot;jaso-icu-analyzer&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;개발 난이도
    &lt;ul&gt;
      &lt;li&gt;JASO: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;chosung&quot;&lt;/code&gt; 옵션만 추가하면 간단히 초성 검색이 가능&lt;/li&gt;
      &lt;li&gt;ICU: 직접 초성 필터를 구현해야 하는 추가 작업 필요&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;유지보수 및 확장성
    &lt;ul&gt;
      &lt;li&gt;JASO: 커스텀 확장(extension)으로 기본 제공되지 않기 때문에, 사용하는 ES 버전과 플랜에 따라 제약이 있을 수 있음&lt;/li&gt;
      &lt;li&gt;ICU: 기본 확장(extension)으로 계속 지원되며, 다른 기능으로의 확장이 자유로움&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;버전 지원
    &lt;ul&gt;
      &lt;li&gt;JASO: Elasticsearch 8.6.2까지만 지원. 이후 버전은 직접 설정 필요&lt;/li&gt;
      &lt;li&gt;ICU: 최신 버전까지 지원&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;토큰 생성 방식
    &lt;ul&gt;
      &lt;li&gt;JASO: 영어 오타 교정, 쌍자음 분리 등 추가 기능 지원&lt;/li&gt;
      &lt;li&gt;ICU: 필요에 따라 초성 검색뿐만 아니라 다양한 확장 가능&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;JASO 가 더 많은 옵션을 제공한다는 이점이 있지만 불필요한 토큰이 생성되고 큰 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_gram&lt;/code&gt;을 주어 토큰을 많이 생성해야 된다는 점,
유지보수를 직접 해야된다는 점에서 ICU extension 을 직접 확장하여 사용하기로 하였습니다.&lt;/p&gt;

&lt;h3 id=&quot;icu-analyzer&quot;&gt;ICU Analyzer&lt;/h3&gt;
&lt;p&gt;그럼, ICU analyzer 의 설정을 좀더 자세히 살펴보겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;orderable_vendor_product_v4&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;aliases&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;orderable_vendor_product&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;mappings&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;properties&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;fields&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;icu&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;analyzer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;icu_analyzer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;search_analyzer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;icu_search_analyzer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;similarity&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;scripted_no_idf&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;template&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;settings&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;index&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;analysis&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;ngram_filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ngram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;min_gram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;max_gram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;token_chars&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;letter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;digit&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;analyzer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;icu_analyzer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;custom&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;lowercase&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ngram_filter&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;char_filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;nfd_normalizer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;make_chosung_filter&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tokenizer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;icu_tokenizer&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;icu_search_analyzer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;custom&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;lowercase&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ngram_filter&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;char_filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;chosung_only_filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                                &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;nfd_normalizer&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tokenizer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;icu_tokenizer&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;char_filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;nfd_normalizer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;mode&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;decompose&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;nfkc&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;icu_normalizer&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;make_chosung_filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;pattern_replace&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;pattern&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;[^&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\u&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;1100-&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\u&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;1112^0-9a-zA-Z가-힣ㄱ-ㅎ ㅏ-ㅑ]&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;replacement&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;chosung_only_filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;pattern_replace&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;pattern&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;[^ㄱ-ㅎa-zA-Z0-9]&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;replacement&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//..&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;Tokenizer&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 analzyer 를 그림으로 나타내면 아래와 같습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/es-dev/es3-2.png&quot; alt=&quot;icu-analzyer&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;icu&lt;/code&gt; field 를 보시면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;analyzer&lt;/code&gt; 와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search_analyzer&lt;/code&gt; 를 구분해서 설정해준걸 보실 수 있는데요.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Analyzer : 저장되는 document 에 대해서 토큰화하여 그림과 같이 역색인화 구조로 저장합니다.&lt;/li&gt;
  &lt;li&gt;Search Analyzer : 검색어에 대해서 토큰화를 수행해서 저장되어 있는 토큰을 검색해서 결과를 내는 역할을 합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;기존엔 Analyzer 와 Search Analyzer 를 구분해서 지정해줄 필요가 없었지만 초성 검색의 경우엔 초성으로 검색 했을 때만 초성 검색이 되길 바랬었는데요.
예를 들어 초성이 아닌 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통베이컨&lt;/code&gt;을 Analyzer 로 검색했을 경우, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ㅌㅂㅇㅋ&lt;/code&gt; 으로 초성화 될 것이고 사용자가 초성 검색을 하지 않았는데도 초성을 포함하는 품목이 많이 나오게 될 것 입니다. 
그래서 최대한 기존의 쿼리 score 에는 영향이 가지 않고 초성 검색을 했을때만 초성으로 품목을 찾기 위해서 초성을 제외한 글자는 모두 제거하는 필터를 넣는 Search Analyzer 를 따로 지정해주었습니다.
즉, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통베이컨&lt;/code&gt;이 Search Analyzer 를 거치게 되면 아무 토큰도 생성되지 않게 되어 초성 검색에 대해서는 수행이 되지 않게 되는거죠.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-icu-normalization-charfilter.html&quot;&gt;ICU_Normalizer&lt;/a&gt; 에 대한 설정은 문서를 보시면 더 자세히 볼 수 있을거예요.
요약하자면 유니코드의 정규화(Normalization)를 수행하는 역할을 합니다.&lt;/p&gt;

&lt;p&gt;저희는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;decompose&lt;/code&gt; 옵션을 사용하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통&lt;/code&gt;이라는 글자를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ㅌㅗㅇ&lt;/code&gt; 으로 문자를 분해할 수 있도록 했고, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NFKC(Normalization Form Compatibility Composition)&lt;/code&gt; 옵션을 사용하여 호환가능한 문자를 호환시키고, 조합 가능한 문자는 조합하도록 설정하였습니다.
즉, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;통베이컨™①é&lt;/code&gt;를 정규화하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ㅌㅗㅇㅂㅔㅇㅣㅋㅓㄴTM1é&lt;/code&gt;로 정규화 됩니다. 
초성검색 구현을 위해선 정규화 방식보다는 초성 분리를 위한 mode 를 잘 설정하는게 더 핵심이라고 할 수 있을거예요.&lt;/p&gt;

&lt;p&gt;요약하면 품목을 ICU Normalizer 를 활용하여 초성 토큰 형태로 저장하고, 초성 검색어에 대해서만 초성 검색을 수행할 수 있도록 Search Analyzer 를 구분하여 구현해주었습니다.&lt;/p&gt;

&lt;p&gt;ICU 는 JASO와 비교했을 때 초기 구현은 다소 복잡했지만, 유지보수와 추후 확장성 측면에서 더 적합한 선택이었으면 합니다!! (Extension 교체 작업만은 다시 하고 싶지 않아요..)&lt;/p&gt;

&lt;h3 id=&quot;idf-제외&quot;&gt;IDF 제외&lt;/h3&gt;
&lt;p&gt;이제 마지막 개선 작업이네요.
잘 운영하고 있던 중 아래와 같은 의견이 들어왔습니다.&lt;/p&gt;

&lt;h3 id=&quot;이슈-및-원인-분석-2&quot;&gt;이슈 및 원인 분석&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/es-dev/es4-1.png&quot; alt=&quot;es-sweetcorn&quot; /&gt;&lt;/p&gt;

&lt;p&gt;사용자가 스위트콘을 검색하려 했으나 스위트곤으로 오타가 발생한 경우, 기대했던 스위트콘이 아닌 곤약이 상위에 노출되는 문제가 있었습니다.
일반적으로 사용자가 기대하는 결과와는 다른 결과였죠.&lt;/p&gt;

&lt;p&gt;다행히도 로컬 테스트 환경에서 원인 분석을 해볼 수 있었는데요, 그런데 조금 충격적이게도 로컬에선 스위트콘이 더 상위노출 되었습니다. 이럴수가..&lt;/p&gt;

&lt;p&gt;그렇다는 것은 로컬 테스트코드와 실제 환경의 검색 결과가 다르다는 것이고, 지금까지의 테스트가 유효한게 맞을까.. 하는 생각이 들었는데요.
&lt;img src=&quot;/images/es-dev/es4-2.jpeg&quot; alt=&quot;es5&quot; /&gt;&lt;/p&gt;

&lt;p&gt;더 자세한 원인을 파악하기 위해 ES의 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/search-explain.html&quot;&gt;explain&lt;/a&gt; API를 사용해 점수 산출 과정을 분석했습니다.&lt;/p&gt;

&lt;p&gt;결과를 요약하자면 아래와 같은데요.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#곤약/면곤약&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;N-gram Boost : 8.8&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;IDF: 6.05&lt;/strong&gt;&lt;/li&gt;
      &lt;li&gt;TF: 0.78&lt;/li&gt;
      &lt;li&gt;8.8 * 6.05 * 0.78 = &lt;strong&gt;41.66&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;스위트콘/리치스/2.95kg&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;N-gram Boost : 8.8&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;IDF : 3.14&lt;/strong&gt;&lt;/li&gt;
      &lt;li&gt;TF : 0.81696963&lt;/li&gt;
      &lt;li&gt;8.8 * 3.14 * 0.81696963 = &lt;strong&gt;22.568493&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ES 에서 Boost * IDF * TF 계산 로직을 통해 score 를 산출하고 있었습니다. 곤약이 상위로 올라온 이유는 숫자를 보면 알수있듯 두배 가까이 차이나는 IDF 때문인 것을 파악할 수 있었는데요.&lt;/p&gt;

&lt;p&gt;우선 처음 보는 개념인 IDF 와 TF 가 무엇인지 알아보았습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;IDF(Inverse Document Frequency): 특정 단어가 &lt;strong&gt;전체 문서&lt;/strong&gt;에서 얼마나 드물게 나타나는지&lt;/li&gt;
  &lt;li&gt;TF(Term Frequency): 특정 단어가 문서 내에서 얼마나 자주 나타나는지&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;IDF 는 전체 문서를 기준으로 계산되기 때문에 테스트코드와 실제 환경의 결과가 다른 이유가 여기에 있었습니다.
테스트코드의 테스트를 위해 생성해놓은 문서는 상대적으로 너무나도 적은 양의 데이터이기 때문에 IDF 의 값이 대부분 동일하고 TF 값으로 대부분 유사도가 정해질거예요.
반면, 테스트코드에 비해 방대한 품목 데이터가 있는 실제 환경에선 곤약과 스위트콘처럼 IDF 의 차이가 클 수 있습니다.&lt;/p&gt;

&lt;p&gt;IDF 가 유의미한 결과를 내주기 위해서는 전체 문서가 모두 한 유통사의 품목으로, 품목명의 구조나 맥락이 동일해야 할 것 같은데요.
하지만 유통사마다 품목명이 모두 제각각인데도 IDF가 모든 유통사의 데이터를 포함한 전체 품목 데이터를 기준으로 계산되면서 오히려 유사도 계산에 역효과를 내고 있었습니다.
또한, 실제환경과 로컬 환경에서의 테스트 결과가 보장되지 않아 문제 재현 및 원인 파악이 어려워보였습니다.&lt;/p&gt;

&lt;p&gt;따라서, &lt;strong&gt;IDF 를 제외&lt;/strong&gt;하고 score 계산하는 방법을 알아봤습니다.&lt;/p&gt;

&lt;p&gt;IDF를 제외한 점수 계산을 위해 크게 세 가지 방법을 검토해봤습니다.&lt;/p&gt;

&lt;h3 id=&quot;1-점수-고정-constant-score&quot;&gt;1. 점수 고정 (Constant Score)&lt;/h3&gt;

&lt;p&gt;첫번째 방법은 아래와 같이 쿼리에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;boost&lt;/code&gt; 값을 명시하여 점수를 고정 시키는 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-constant-score-query.html&quot;&gt;Constant Score Query&lt;/a&gt; 를 활용한 방법입니다.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;query&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;bool&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;must&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;bool&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;should&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;constant_score&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;wildcard&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name.wildcard&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;wildcard&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;*스위트곤*&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;boost&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;100.0&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;constant_score&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;match&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;스위트곤&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;boost&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;3.0&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;constant_score&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;match&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name.ngram&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;스위트곤&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;boost&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;4.0&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;결과를 봤을 때, Boost 만으로 Scoring 되기 때문에 같은 필드에서 검색된 품목은 동일한 점수를 가지는 것을 확인했습니다. 
예를 들어 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name.ngram&lt;/code&gt; 로 검색된 품목들은 모두 Boost 4 인 동일한 점수로 정렬이 제대로 되지 않았습니다.&lt;/p&gt;

&lt;p&gt;결론적으로, 이 방식은 유사도 기반 정렬이 필요한 우리의 요구사항에 적합하지 않았습니다.&lt;/p&gt;

&lt;h3 id=&quot;2-유사도-모델-변경&quot;&gt;2. 유사도 모델 변경&lt;/h3&gt;
&lt;p&gt;기본적으로 ES는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Boost * IDF * TF&lt;/code&gt; 식을 사용하는 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-similarity.html#bm25&quot;&gt;BM25(Best Matching 25)&lt;/a&gt; 모델을 사용합니다. 이를 대신해 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-similarity.html#dfr&quot;&gt;DFR(Deviation From Randomness)&lt;/a&gt; 모델을 사용해 보았습니다.
DFR 은 현 document에 얼마나 드물게 등장하는지, 문서 길이에 따라 통계적 랜덤성 기반으로 검색됩니다. type 뿐만 아니라 아래처럼 옵션에 가중치나 옵션을 설정해줄수있습니다.
아래는 DFR 모델 설정 예시입니다.&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;settings&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;similarity&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;custom_similarity&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;DFR&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;basic_model&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;g&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;문서&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;내에&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;특정&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;용어가&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;얼마나&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;드물게&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;발생하는지&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;after_effect&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;b&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;검색어가&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;얼마나&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;여러번&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;등장하는지&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;normalization&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;h2&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;문서&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;길이에&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;따라&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;빈도&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;설정&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;보시다시피 DFR 모델에 대한 지식이 없으면 유지보수가 무척 어려워보입니다.
문제가 생기거나 개선의 여지가 생겼을 때 더 복잡한 계산식을 사용하는 모델이기 때문에 쉽게 커스텀하기가 힘들어보이고, 모델의 대한 이해뿐만 아니라 옵션에 대한 각 알고리즘도 알아야하기 때문에 유지보수가 정말 쉽지 않을거라 생각이 들었죠.&lt;/p&gt;

&lt;p&gt;우리 팀에 검색 엔진에 대한 전문적인 지식을 가진 분이 없었기 때문에 더 복잡한 모델로 변경하는건 과감하게 제외했습니다.&lt;/p&gt;

&lt;h3 id=&quot;3-scripted-similarity-사용&quot;&gt;3. Scripted Similarity 사용&lt;/h3&gt;

&lt;p&gt;IDF를 제외하고 직접 계산식을 정의하는 방법입니다. 아래는 Scripted Similarity 설정 예시입니다.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;setting&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;similarity&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;scripted_no_idf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;scripted&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;script&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;source&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;double tf = Math.sqrt(doc.freq); return query.boost * tf;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위처럼 setting 을 변경하고 쿼리의 explain 을 해보면 script 에 있는 식이 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idOrCode&lt;/code&gt;에 들어가 scoring 되는것을 볼 수 있어요.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt;Explain&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;결과&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;   
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;sum of:&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;details&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;details&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;score from ScriptedSimilarity(weightScript=[null], script=[Script{type=inline, lang=&apos;painless&apos;, idOrCode=&apos;double tf = Math.sqrt(doc.freq); return query.boost * tf;&apos;, options={}, params={}}]) computed from:&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;details&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                        &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 세가지 방법 중 가장 마지막 방법인 Scripted Similarity 로 계산식을 넣기로 했어요. Scripted Similarity 를 선택한 이유는 다음과 같습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;유연한 계산식 사용
    &lt;ul&gt;
      &lt;li&gt;계산식을 직접 조정 가능&lt;/li&gt;
      &lt;li&gt;기존 BM25 모델에서 IDF만 제거할 수 있는 계산식을 직접 부여 가능&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;유지보수 용이성
    &lt;ul&gt;
      &lt;li&gt;계산식이 명확히 노출되어 있어 비교적 수정이 간단&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;쿼리와 독립적
    &lt;ul&gt;
      &lt;li&gt;점수 계산이 쿼리와 독립적으로 이루어져 다른 쿼리 추가 시에도 영향을 받지 않음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;scripted-similarity-계산식-결정&quot;&gt;Scripted Similarity 계산식 결정&lt;/h3&gt;

&lt;p&gt;그후에는 Scripted Similarity 에 적용할 계산식 결정이 필요했습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;후보 1. 단순한 TF 계산식&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;double&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tf&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sqrt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;freq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boost&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;cm&quot;&gt;/* freq : 문서 내의 토큰 등장 수 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;후보 2. BM 모델과 동일한 TF 계산식&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;double&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;freq&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;freq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;double&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;k1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;double&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.75&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;double&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;double&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;avgdl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;double&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tf&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;freq&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;freq&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;k1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;avgdl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boost&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;cm&quot;&gt;/*
    freq : 문서 내의 토큰 등장 수
    k1: TF 영향도 가중치
    b : 문서 길이 보정 파라미터
    dl : 문서 길이
    avgdl : 전체 문서의 문서 길이 평균값
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/images/es-dev/es4-3.png&quot; alt=&quot;idf&quot; /&gt;
&lt;img src=&quot;/images/es-dev/es4-4.png&quot; alt=&quot;idf&quot; /&gt;
결과만 보았을 때 확실히 IDF 를 포함했을 때보다 IDF 를 제외했을 때 결과가 개선되긴 했지만, 여전히 단순 TF 계산식, BM 모델 계산식의 결과는 크게 차이가 없었습니다.&lt;/p&gt;

&lt;p&gt;하지만 위 결과의 정렬은 비슷한듯 하지만 실제 점수는 다릅니다. 단순 TF 의 경우 문서 내의 토큰 등장 수로만 TF 를 계산하기 때문에 동일한 점수를 가진 결과가 다수 나타나 매번 정렬이 달라질 수 있습니다.
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;국산쌀&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;찰떡(쌀 국산)&lt;/code&gt;는 단순 TF 계산식에 의하면 둘은 동일한 점수를 반환하게 됩니다.
반면 BM 모델과 동일한 TF 계산식의 경우, 문서 내 토큰 등장 수와 문서 길이를 함께 고려하기 때문에 저희가 흔히 생각한대로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;국산쌀&lt;/code&gt;의 유사도가 더 높은 결과로 나오게 됩니다.&lt;/p&gt;

&lt;p&gt;따라서, BM 모델에서 IDF 만 제거한, 후보2 계산식을 사용하기로 결정하였습니다.&lt;/p&gt;

&lt;p&gt;다만, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;avgdl&lt;/code&gt; 를 현재의 기준으로 7로 고정시켜 가중치를 조정했지만 이후 문서의 평균 길이가 변경될 경우엔 유지보수가 필요한 부분이 있을 수 있습니다.
하지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;avgdl&lt;/code&gt; 가 크게 변하지 않을 것이라는 가정과 점수 계산에 크게 영향을 주지 않는다고 생각해 고정된 계산식으로 가게 되었습니다.&lt;/p&gt;

&lt;p&gt;그럼에도 이후에 유지보수가 필요하거나 변경이 필요할 수 있습니다. 꽤 해석이 필요한 계산식을 가지고 있기 때문에
어떤 계기로 IDF를 제거하게 되었는지, Scripted Similarity 를 왜 적용하게 되었는지, 계산식은 어떻게 결정되었고 어떤 의미인지를 자세히 문서화하도록 노력하였습니다.&lt;/p&gt;

&lt;h2 id=&quot;결과&quot;&gt;결과&lt;/h2&gt;
&lt;p&gt;결과적으로, 아래와 같은 구조를 갖게 되었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/es-dev/es5-1.png&quot; alt=&quot;final&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이러한 구조를 통해 현재 검색 정확도는 어떨까요?&lt;/p&gt;

&lt;p&gt;아래는 매장 사이드 관련 데이터입니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;2025년 1월 기준
    &lt;ul&gt;
      &lt;li&gt;검색 결과 성공률: 검색 시 하나 이상의 품목이 결과에 노출되는 경우 → &lt;strong&gt;98.6%&lt;/strong&gt;&lt;/li&gt;
      &lt;li&gt;검색 목적 달성률: 검색한 품목을 선택한 후, 실제 액션(주문 등)을 수행하는 경우 → &lt;strong&gt;73.6%&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이를 통해 상당히 높은 검색 정확도와 달성률을 기록하고 있음을 확인할 수 있습니다.&lt;/p&gt;

&lt;p&gt;또한, 아래는 유통사에서 매장의 주문서를 생성할 때, ES 검색이 매우 편리했다는 피드백을 받은 메시지입니다.
키친보드의 검색 기능이 타 ERP와 달리 오타가 있어도 품목을 정확하게 검색할 수 있어 큰 편리함을 느꼈다고 합니다!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/es-dev/es5-2.png&quot; alt=&quot;final&quot; /&gt;
&lt;img src=&quot;/images/es-dev/es5-3.png&quot; alt=&quot;final&quot; /&gt;&lt;/p&gt;

&lt;p&gt;개발 과정에서 어려움이 있었지만 결국 사용자한테 좋은 영향을 주는 기능을 개발한거 같아 아주 뿌듯하네요!&lt;/p&gt;

&lt;h1 id=&quot;소소한-tip&quot;&gt;소소한 Tip&lt;/h1&gt;
&lt;h2 id=&quot;중단-시간을-줄인-reindexing&quot;&gt;중단 시간을 줄인 Reindexing&lt;/h2&gt;
&lt;p&gt;ES 의 비용을 최소화하기 위해서 최소한의 리소스로 구동하고 있습니다. 그러다보니 인덱스에 새로운 필드가 추가되거나 설정이 변경되는 개선이 될때마다 인덱스를 새롭게 동기화해야하기 때문에 중단이 불가피했는데요.
하지만 주문에서의 검색은 점주들이 꼭 해야만하는 중요한 기능이기 때문에 중단을 최소화하고 싶었습니다.&lt;/p&gt;

&lt;p&gt;알아보던 중 찾아낸 것은 별칭(Alias)를 이용한 배포입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/es-dev/es6-1.png&quot; alt=&quot;final&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Old Index Template&lt;/code&gt; 을 템플릿으로 하여 생성된 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Old Index&lt;/code&gt;(별칭: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product&lt;/code&gt;) 가 존재한다.&lt;/li&gt;
  &lt;li&gt;새로운 매핑 정보가 있는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;New Index Template&lt;/code&gt; 을 생성한다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;New Index Template&lt;/code&gt; 을 템플릿으로 하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;New Index&lt;/code&gt; 를 생성한다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Old Index&lt;/code&gt; 의 문서를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;New Index&lt;/code&gt; 로 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html&quot;&gt;reindex&lt;/a&gt; 한다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Old Index&lt;/code&gt; 를 삭제한다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;New Index&lt;/code&gt; 에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product&lt;/code&gt; alias 를 부여 한다.&lt;/li&gt;
  &lt;li&gt;서버에서 alias 기준으로 쿼리한다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;이렇게 되면 5번과 6번 사이, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product&lt;/code&gt; 를 가진 인덱스가 존재하지 않을 시에만 검색 중단이 발생하게 됩니다.
이 방법이 가장 좋은 방법이라고 표현하기는 어렵지만 기존 20만개의 품목을 마이그레이션 하느라 10분이 넘는 중단 시간을 3초 정도의 중단 시간으로 낮출 수 있는 방법이었습니다.&lt;/p&gt;

&lt;h2 id=&quot;대량-데이터-조회&quot;&gt;대량 데이터 조회&lt;/h2&gt;
&lt;p&gt;DB 의 품목 데이터와 ES 인덱스 동기화를 위해 하루에 한번씩 스케줄러가 실행됩니다.&lt;/p&gt;

&lt;p&gt;DB 품목 데이터가 삭제될 때 ES 데이터도 삭제되어야 하는데 어떠한 이유로 삭제되지 않는 경우가 있었습니다. 즉, 품목이 DB 데이터엔 없지만 ES 데이터에는 있는 경우죠.&lt;br /&gt;
스케줄러에서 전체 ES 인덱스 문서를 조회하고 DB 에 없으면 문서를 삭제해서 동기화해주려고 해요.&lt;/p&gt;

&lt;p&gt;다만 전체 문서를 조회하는 과정의 부하가 많이 걱정되긴 했는데요. 그래서 처음엔 페이징을 활용했습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;deleteOrphanDocuments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;pageSize&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;pageable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PageRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pageSize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Sort&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Sort&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;asc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;id.keyword&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;indexPage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;productIndexRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;documentProductIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;indexPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;dbProductIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;productRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAllById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;documentProductIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;documentIdsNotInDB&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;documentProductIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dbProductIds&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;productIndexRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deleteAllById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;documentIdsNotInDB&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;pageable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;indexPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hasNext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src=&quot;/images/es-dev/es6-2.png&quot; alt=&quot;page-query&quot; /&gt;&lt;/p&gt;

&lt;p&gt;ES야.. 죽지마.. 테스트서버에서 확인해보니 동기화 할 때마다 자꾸 ES 서버가 다운되더라고요..
페이징보다 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/scroll-api.html&quot;&gt;Scroll&lt;/a&gt; API 를 이용하는 것이 훨씬 성능상 좋다고 하여서 Scroll API 를 적용해봤습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;deleteOrphanDocuments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;toDeleteProductIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutableListOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;()&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;batchSize&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;documentIdsWithScroll&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;productIndexRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAllIdWithScroll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;batchSize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;documentIdsWithScroll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;productDocumentIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;isNotEmpty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;documentIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;documentIdsWithScroll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;productDocumentIds&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;dbProductIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;productRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAllById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;documentIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;toDeleteProductIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;documentIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dbProductIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;documentIdsWithScroll&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;productIndexRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAllIdByScrollId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;scrollId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;documentIdsWithScroll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nextScrollId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;productIndexRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clearScroll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;documentIdsWithScroll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nextScrollId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;productIndexRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deleteAllById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;toDeleteProductIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ES 에서 Scroll 을 생성할 때 설정한 시간만큼 Scroll 정보를 저장하고 있고 해당 Scroll ID 를 가지고 다음 데이터를 처리단위만큼 반환해줍니다. 
받은 데이터엔 또 다음 Scroll 조회를 위한 Scroll ID 를 반환하는 형식입니다.&lt;/p&gt;

&lt;p&gt;장점은 페이징 조회보다 계산과 조회가 빠르고 부하가 적은 것인데요. 다만, Scroll 정보를 저장하고 있어야한다는 단점이 있습니다.&lt;/p&gt;

&lt;p&gt;조회 후 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clearScroll&lt;/code&gt; 해주고 만료 시간을 적게 설정하면 큰 문제가 되지 않을거예요.&lt;/p&gt;

&lt;p&gt;변경 후엔 ES가 건강해졌습니다. 대량 조회가 필요할 땐 Scroll API 를 사용하는걸 추천드립니다.&lt;/p&gt;

&lt;h2 id=&quot;검색-품질-유지하기&quot;&gt;검색 품질 유지하기&lt;/h2&gt;
&lt;p&gt;추가적으로 쿼리나 스코어링 방식을 변경하다보면 현재의 요구사항에는 만족했지만 이전의 요구사항에 반할 때가 생길 수 있습니다.
그래서 테스트코드에 이전 요구사항에 관련된 테스트케이스를 꼭 남겨두고 현재 만족할 수 있는 테스트케이스를 더해서
이전과 현재 요구사항을 모두 만족할 수 있도록 구현하는걸 목표로 했습니다.&lt;/p&gt;

&lt;p&gt;특정 케이스에 모두 만족하는지 일일이 확인하는 것은 현실적으로 어려울 수 있으니 테스트를 든든하게! 잘 짜두는 것이 많은 도움이 될 겁니다!&lt;/p&gt;

&lt;h2 id=&quot;이후엔-어떤-것을-할-것인가&quot;&gt;이후엔 어떤 것을 할 것인가&lt;/h2&gt;
&lt;p&gt;Wildcard 검색, 초성 검색, IDF 제거 등 여러 개선들을 해왔는데요.
요구사항에 맞게, 문제상황에 맞게 조치하는 형식으로 개선해나가다보니 조금은 불필요한 옵션들로 검색을 무겁게 만든 부분들이 분명 있을 수 있을것 같아요.
또한 검색 엔진에 대한 전문적인 지식 없이 점차 학습하여 개발하다보니 초반과 후반에 대한 지식의 차이가 커서 초반에 했던 방법들이 논리보단 여러 검색어 테스트 POC 로 적용한 것들이 많아요.
그래서 조금 뚱뚱해진 ES 의 다이어트가 필요해보입니다.&lt;/p&gt;

&lt;p&gt;일단 생각나는 것은 부하를 줄 수 있는 Wildcard 를 없애는 것인데요. N-gram 과 Tokenizer 의 적절한 조합으로 Wildcard 없이 포함된 단어가 잘 조회될 수 있도록 방법을 찾아봐야할 것 같아요.&lt;/p&gt;

&lt;p&gt;그리고 이젠 품목 데이터가 많이 쌓여서 한국 식자재의 대부분 데이터가 존재할 것인데요. 이 데이터로 사용자 사전을 만들어서 검색의 정확도를 높이는 작업을 해볼 수 있을 겁니다.
추가되는 데이터에 맞춰 사전이 관리되어야 하기 때문에 개발자뿐만 아니라 타부서 팀원분들도 쉽게 관리하기 위한 시스템을 마련해야 할 것 같습니다.&lt;/p&gt;

&lt;p&gt;외래어를 포함하는 새로운 식자재를 등록하는 것도 중요하지만 “셀러드”처럼 식자재이지만 표기를 잘못한 경우도 있어서 이런 경우엔 품목명 자체의 수정이 필요합니다.
ERP에 등록된 품목명과 키친보드의 등록된 품목명의 강한 결합성을 끊을 수 있도록, 검색 엔진의 설정뿐만 아니라 키친보드 품목명의 데이터 클렌징이 필요할거예요.&lt;/p&gt;

&lt;h1 id=&quot;마무리&quot;&gt;마무리&lt;/h1&gt;
&lt;p&gt;끝으로, 저희의 지금 검색 엔진은 완성이 아닙니다. 지금도 많이 부족할 수 있지만 사용자의 보이스를 들으며 꾸준히 발전하고 있습니다.
키친보드를 사용하는 점주 모두가 검색이 너무 편해요! 라고 할때까지, 대충 검색해도 마음속으로 생각했던 품목이 결과에 나오는 그날까지, 발전은 계속 됩니다!&lt;/p&gt;

&lt;p&gt;ES 도입과 설정에 도움주시고 자기 일처럼 고민해주신 백엔드 개발자분들과 개선을 위한 데이터를 제공해주신 데이터팀분들, 검색 개선을 위해 피드백 주신 사업팀분들 모두 감사합니다!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/es-dev/chillguy.png&quot; alt=&quot;chillguy&quot; width=&quot;300&quot; /&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>스포카의 백엔드팀에서 코딩 컨벤션을 관리하는 방법</title>
      <link>https://spoqa.github.io/2024/11/18/coding-convention-story.html</link>
      <pubDate>Mon, 18 Nov 2024 00:00:00 +0000</pubDate>
      <author>남경호</author>
      <guid>/2024/11/18/coding-convention-story</guid>
      <description>&lt;p&gt;안녕하세요, 스포카 백엔드팀 프로그래머 남경호입니다.&lt;/p&gt;

&lt;p&gt;개발자라면 누구나 한 번쯤 더 나은 코드를 작성하고, 팀의 생산성과 유지보수성을 높이기 위해 고민해 보셨을 겁니다. 중복된 코드를 줄이고, 가독성을 높이며, 테스트 코드를 꼼꼼히 작성하거나, 알맞은 변수명을 고심하는 과정은 모두 그런 노력의 일환이죠. 하지만 이런 개선 작업이 효과적으로 이루어지려면, 팀 전체가 공통된 코딩 기준을 공유하고 지키는 것이 무엇보다 중요합니다.&lt;/p&gt;

&lt;p&gt;일관된 코딩 컨벤션은 단순한 규칙 이상의 역할을 합니다. 특히 여러 명의 개발자로 이루어진 팀에서는 코드의 가독성과 유지보수성을 높이고, 불필요한 논쟁을 줄이며, 협업의 효율성을 극대화하는 강력한 도구가 됩니다. 하지만 이러한 컨벤션을 설정하고, 이를 꾸준히 유지하는 과정은 절대 간단하지 않습니다. 문서로 정리한 규칙이 팀 내에서 제대로 적용되지 않거나, 시간이 지나며 점차 구식이 되는 문제를 겪어보신 분들도 많을 겁니다.&lt;/p&gt;

&lt;p&gt;우리 스포카 백엔드팀 역시 비슷한 과정을 겪었습니다. 코딩 컨벤션을 관리하기 위해 문서, 코드 리뷰, 그리고 자동화 도구를 활용하며, 더 나아가 Konsist라는 새로운 도구까지 도입하게 된 여정을 통해 다양한 시행착오를 경험했죠. 이 글에서는 단순한 도구 사용법을 넘어, 우리가 직면했던 문제들과 이를 어떻게 극복했는지에 대해 이야기해 보려 합니다.&lt;/p&gt;

&lt;p&gt;코딩 컨벤션 관리라는 쉽지 않은 도전이 어떻게 우리 팀의 개발 문화를 변화시켰는지, 그리고 여러분의 팀에도 어떤 인사이트를 줄 수 있을지 함께 살펴보시죠.&lt;/p&gt;

&lt;h1 id=&quot;코딩-컨벤션-관리의-시작-문서-작성&quot;&gt;코딩 컨벤션 관리의 시작: 문서 작성&lt;/h1&gt;

&lt;p&gt;모든 시작은 단순합니다. 우리 팀 역시 처음에는 README.md 파일을 통해 코딩 컨벤션을 관리하기 시작했습니다. 사내 문서 관리 도구인 Confluence를 사용하지 않은 이유는 간단했는데요. 코딩 컨벤션은 코드와 가장 밀접하게 연관되어 있기 때문에, 코드와 가장 가까운 곳에서 관리할 필요가 있다고 판단했기 때문입니다.&lt;/p&gt;

&lt;p&gt;README.md에는 서버 실행을 위한 준비 작업, IDEA 환경 설정, GIT 브랜치 전략, 그리고 코딩 컨벤션 등 백엔드 개발자가 우리 팀에서 알아야 할 모든 내용이 기재되어 있었습니다. 초기에는, 이 접근법이 꽤 효과적이었습니다. 새로운 팀원이 들어오더라도 문서를 참고해 작업 환경을 설정하거나 코드를 작성할 때 기준을 따를 수 있었으니까요.&lt;/p&gt;

&lt;p&gt;아래는 저희가 README.md에 작성했던 문서의 일부분입니다.&lt;/p&gt;

&lt;h4 id=&quot;일관된-eofend-of-file-설정을-위한-가이드&quot;&gt;일관된 EOF(End of File) 설정을 위한 가이드&lt;/h4&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gu&quot;&gt;#### EOF 설정&lt;/span&gt;

파일의 마지막 라인에 자동으로 개행이 되도록 하기 위한 설정입니다.
&lt;span class=&quot;p&quot;&gt;
1.&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`Editor`&lt;/span&gt; -&amp;gt; &lt;span class=&quot;sb&quot;&gt;`General`&lt;/span&gt;로 이동합니다.
&lt;span class=&quot;p&quot;&gt;2.&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`On Save`&lt;/span&gt; 색션에서 &lt;span class=&quot;sb&quot;&gt;`Ensure every saved file ends with a line break`&lt;/span&gt;를 체크합니다.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;ktlint-플러그인-설정-가이드&quot;&gt;Ktlint 플러그인 설정 가이드&lt;/h4&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gu&quot;&gt;## Ktlint Settings&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;### ktlint pre-commit 설정&lt;/span&gt;

아래 명령어를 실행하여 pre-commit hook을 등록합니다.

&quot;&quot;&quot;
./gradlew addKtlintCheckGitPreCommitHook
&quot;&quot;&quot;&lt;span class=&quot;sb&quot;&gt;


&lt;/span&gt;&lt;span class=&quot;gu&quot;&gt;### Formatting&lt;/span&gt;

만약 lint 위반 오류가 발생하는 경우 아래 명령어를 실행하여 자동으로 포메팅을 수행합니다.

&quot;&quot;&quot;
./gradlew ktlintFormat
&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이처럼 문서는 개발자가 코드 작성 시 참고할 수 있는 명확한 기준을 제시해 주었고, 코드 리뷰 과정에서도 사소한 논쟁을 줄이는 데 기여했습니다. 하지만 시간이 지나면서 문제점이 하나둘씩 드러나기 시작했습니다.&lt;/p&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;이러한 한계를 극복하기 위해 우리 팀은 문서만으로 코딩 컨벤션을 관리하는 데서 벗어나기 시작했습니다. 코딩 컨벤션을 더 효과적으로 지키기 위해 Lint와 같은 자동화된 도구, 그리고 코드 리뷰와 같은 방식을 도입했고, 이는 더 나은 방향으로의 첫걸음이 되었습니다.&lt;/p&gt;

&lt;h1 id=&quot;자동화의-첫걸음-ktlint의-도입&quot;&gt;자동화의 첫걸음: ktlint의 도입&lt;/h1&gt;

&lt;p&gt;문서로 코딩 컨벤션을 관리하는 데 한계를 느낀 우리 팀은 Lint라는 자동화 도구를 도입했습니다. Lint는 코드의 오류, 버그, 스타일 문제를 찾아주는 정적 코드 분석 도구로, 간단한 설정만으로도 코드 스타일을 일관성 있게 유지할 수 있게 해줍니다. 특히, Kotlin 언어에서는 ktlint가 대표적인 Lint 도구로 자리 잡고 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;lint-도구의-효과&quot;&gt;Lint 도구의 효과&lt;/h3&gt;
&lt;p&gt;Lint 도구를 활용하면서 코드 스타일 문제를 해결하는 과정이 크게 간소화되었습니다. 이전에는 코드 리뷰 과정에서 개행, Trailing commas, 공백 처리 같은 사소한 스타일 문제로 많은 시간을 할애해야 했습니다. 하지만 Lint를 도입한 이후, 이런 문제들은 더 이상 개발자들이 신경 써야 할 부분이 아니게 되었죠. CI/CD 파이프라인에 통합하면, 코드가 규칙을 위반할 경우 자동으로 알려주기 때문에 문제를 사전에 방지할 수 있었습니다.&lt;/p&gt;

&lt;h4 id=&quot;gradle에서-ktlint-설정하기&quot;&gt;Gradle에서 ktlint 설정하기&lt;/h4&gt;

&lt;p&gt;Gradle에서 ktlint를 설정하는 방법은 간단합니다. 먼저, Gradle 빌드 스크립트에 ktlint 플러그인을 추가합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;plugins&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;org.jlleitschuh.gradle.ktlint&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;{version}&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그 후, 특정 파일이나 디렉터리를 제외하고 싶은 경우, 다음과 같이 필터를 설정할 수 있습니다. 아래 예시는 다른 프레임워크에서 자동으로 생성된 코드들은 Lint의 검증 대상에서 제외하도록 하는 설정을 나타냅니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;ktlint&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;filter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;exclude&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;**/generated/**&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;마지막으로, 테스트 실행 전 lint 검사를 자동으로 수행하도록 구성할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;withType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;dependsOn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;withType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;KtLintCheckTask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;())&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;만약 ktlint에서 제공해 주는 Hook 기능을 사용한다면 Git에서 Commit 시 매번 스타일 오류를 수작업으로 수정하지 않아도, 코드를 저장하거나 테스트를 실행하는 과정에서 자동으로 스타일이 교정되거나 문제를 알려주도록 만들 수 있었습니다.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Git Commit 전 ktlint check를 통해 스타일 위반 사항이 있는지 확인
./graldew addKtlintCheckGitPreCommitHook

# Git Commit 전 kotlin format을 통해 스타일을 자동으로 보정
./graldew addKtlintFormatGitPreCommitHook
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;lint-도구의-한계&quot;&gt;Lint 도구의 한계&lt;/h3&gt;

&lt;p&gt;Lint는 코드 스타일을 유지하고 간단한 문제를 잡아내는 데 매우 유용하지만, 모든 문제를 해결해 주는 도구는 아닙니다. 개행이나 공백 처리 같은 기본적인 규칙을 강제할 수는 있지만, 코드의 중복 문제나 보안상의 허점, 복잡한 구조적 컨벤션은 감지하지 못합니다.&lt;/p&gt;

&lt;p&gt;Lint 도구를 사용하면서 우리는 “자동화로 해결할 수 있는 범위는 어디까지일까?”라는 고민을 하게 되었고, 이 고민은 더 정교한 도구와 방법을 찾는 과정으로 이어졌으며, 그 여정은 곧 SonarQube와 Konsist 같은 도구의 도입으로 확장되었습니다.&lt;/p&gt;

&lt;h1 id=&quot;코드-리뷰-사람의-눈은-여전히-중요하다&quot;&gt;코드 리뷰: 사람의 눈은 여전히 중요하다&lt;/h1&gt;

&lt;p&gt;자동화 도구를 더 이야기하기 전에, Lint와 같은 자동화 도구를 도입했음에도, 코드 리뷰는 여전히 코딩 컨벤션을 지키고 코드 품질을 높이는 데 필수적인 과정으로 남아 있습니다. 자동화 도구가 사소한 스타일 문제를 처리해 준다면, 코드 리뷰는 도메인 요구사항을 잘 충족하였는지, 자동화된 도구 잡아내기 힘든 컨벤션 위반을 발견해 주는 등의 더 깊이 있는 검토를 통해 팀의 코드 베이스를 개선하는 데 집중할 수 있습니다.&lt;/p&gt;

&lt;p&gt;아래 이미지는 Lint로는 잡아내기 힘든 컨벤션 위반을 코드리뷰를 통해 개선할 수 있었던 사례입니다. 우리 팀은 테스팅 라이브러리로 Kotest를 사용 중이며 테스트 코드 작성 시 아래와 같은 컨벤션을 지키도록 약속되어 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 테스트 코드 컨벤션&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SalesOrderFacadeTest&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UnitTestBase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ... 생략&lt;/span&gt;
    
    &lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;getSalesOrderById&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;아이디로 매출 전표를 조회한다&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;c1&quot;&gt;// ... 생략&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        
        &lt;span class=&quot;c1&quot;&gt;// ... 생략&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/images/coding-convention-story/code-review.png&quot; alt=&quot;code-review&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;코드-리뷰의-한계&quot;&gt;코드 리뷰의 한계&lt;/h3&gt;

&lt;p&gt;코드 리뷰는 수많은 회사에서 도입하고 있는 코드 품질을 높일 수 있는 수단임에도 불구하고 아래와 같이 몇 가지 한계가 있습니다.&lt;/p&gt;

&lt;p&gt;첫 번째로, 사람이 직접 코드를 검토하기 때문에 시간이 많이 소요된다는 점입니다. 특히, 개행, 공백, Trailing commas 같은 사소한 스타일 문제를 반복적으로 지적해야 한다면, 리뷰어와 작성자 모두에게 소모적인 작업이 될 수 있습니다. 이러한 한계는 앞서 소개한 Lint를 통해 해결할 수 있습니다.&lt;/p&gt;

&lt;p&gt;두 번째로, 리뷰 품질이 리뷰어에 따라 다를 수 있다는 문제도 있습니다. 어떤 리뷰어는 코드 설계나 성능 최적화에 집중할 수 있지만, 다른 리뷰어는 스타일 문제에 더 많은 시간을 할애할 수 있습니다. 이에 따라 팀 내에서 일관성이 떨어질 가능성이 있습니다.&lt;/p&gt;

&lt;p&gt;마지막으로, 코드 리뷰 과정에서 부정적인 피드백이 적절히 전달되지 않으면 팀원 간의 긴장감이 생길 수 있습니다. 이는 팀워크에 부정적인 영향을 미칠 수 있고, 오히려 협업의 효율성을 저해하는 결과를 초래할 수 있습니다.&lt;/p&gt;

&lt;p&gt;이러한 한계를 극복하기 위해 자동화 도구를 활용해 사소한 문제를 미리 해결하거나, 팀의 리뷰 가이드라인을 문서화하여 일관성을 유지하는 등의 보완책이 필요합니다. 코드 리뷰는 팀의 코드 품질과 협업 문화를 개선하는 중요한 과정인 만큼, 이러한 한계를 인지하고 효과적으로 관리하는 것이 중요합니다.&lt;/p&gt;

&lt;p&gt;“자동화된 도구가 더 많은 것을 해결해 줄 순 없을까?”라는 고민은 여기서 더욱 깊어졌습니다.&lt;/p&gt;

&lt;h1 id=&quot;sonarqube의-도입-더-넓은-관점에서의-코드-품질-관리&quot;&gt;SonarQube의 도입: 더 넓은 관점에서의 코드 품질 관리&lt;/h1&gt;

&lt;p&gt;Lint와 코드 리뷰를 통해 팀의 코딩 컨벤션을 어느 정도 유지할 수 있었지만, 여전히 놓치고 있는 문제들이 있었습니다. 스타일 문제나 사소한 규칙 위반은 Lint로 충분히 해결할 수 있었지만, 코드의 중복, 복잡성, 보안 취약점, 그리고 더 넓은 관점에서의 코드 품질 관리는 다른 도구가 필요했습니다. 이에 우리 팀은 SonarQube를 도입하게 되었습니다.&lt;/p&gt;

&lt;h3 id=&quot;sonarqube의-기능과-장점&quot;&gt;SonarQube의 기능과 장점&lt;/h3&gt;

&lt;p&gt;SonarQube는 코드 품질을 유지하고 개선하는 데 있어 매우 강력한 도구입니다. 가장 큰 장점은 코드의 다양한 품질 요소를 종합적으로 분석할 수 있다는 점입니다. 예를 들어, 코드에서 중복된 부분을 찾아내거나 복잡도가 높은 영역을 감지하며, 테스트 커버리지를 평가하고 버그나 보안 취약점을 자동으로 탐지할 수 있습니다. 이를 통해 개발자는 단순한 스타일 문제를 넘어서, 코드의 구조적 결함이나 유지보수성을 저해할 수 있는 장기적인 문제들을 사전에 파악할 수 있습니다.&lt;/p&gt;

&lt;p&gt;아래 사례는 Kotlin의 Data Class에서 Array 프로퍼티를 사용하는 경우 equals, hashCode, toString을 재정의해 주지 않아 발생한 SonarQube의 이슈 목록을 보여줍니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/coding-convention-story/sonarqube-issues.png&quot; alt=&quot;sonarqube-issues&quot; /&gt;&lt;/p&gt;

&lt;p&gt;또한, SonarQube는 오픈소스로 제공되기 때문에 로컬 환경이나 사내 서버에 설치해서 사용할 수 있습니다. 이 외에도 클라우드 기반으로도 운영이 가능하여 팀의 필요에 맞는 다양한 방식으로 유연하게 활용할 수 있습니다. 기본 제공되는 검사 규칙뿐만 아니라, 프로젝트의 특성에 따라 다양한 플러그인과 설정을 통해 맞춤형 규칙을 추가할 수도 있어 확장성도 뛰어납니다.&lt;/p&gt;

&lt;h3 id=&quot;운영-과정에서-마주한-도전-과제&quot;&gt;운영 과정에서 마주한 도전 과제&lt;/h3&gt;

&lt;p&gt;SonarQube의 강력함에도 불구하고, 실제 운영 과정에서는 몇 가지 불편한 점들이 존재했습니다.&lt;/p&gt;

&lt;p&gt;첫 번째로는 서버 관리와 관련된 문제입니다. SonarQube를 사용하려면 자체 서버를 설치하고 운영해야 하는데, 이 과정에서 추가적인 설정과 유지보수가 필요했습니다. 특히, 코드 분석 과정에서 소스코드를 서버에 업로드하고 검증하는 데 시간이 소요되었는데, 이는 Pull Request 단계에서 실시간 검증을 수행하기에는 부담으로 작용했습니다.&lt;/p&gt;

&lt;p&gt;두 번째 도전 과제는 사전 검증과 사후 검증 간의 균형을 맞추는 일이었습니다. 초기에는 코드 변경 사항이 병합되기 전에 Pull Request 단계에서 SonarQube를 통해 코드를 검증하려 했습니다. 그러나 검증 과정이 느리고 시간이 오래 걸리는 탓에, 개발 속도를 저하하는 문제가 있었습니다. 이에 따라 우리는 코드가 병합된 후에 Code Smell이나 품질 문제를 감지하는 방식으로 운영 방식을 조정했습니다. 이 방식은 코드 품질 문제를 감지하는 데 유연함을 제공했지만, 문제를 사전에 완전히 방지할 수는 없다는 한계도 있었습니다.&lt;/p&gt;

&lt;p&gt;SonarQube는 단순히 스타일을 검사하는 도구에서 벗어나, 팀이 더 넓은 관점에서 코드 품질을 관리할 수 있도록 도와주는 강력한 도구입니다. 비록 작업코드가 병합되기 전에 품질관리를 하기 어렵다는 불편함은 있지만 사후 검증을 통해서도 Lint와 코드 리뷰가 해결하지 못했던 복잡한 문제를 감지하고, 코드베이스의 장기적인 건강 상태를 유지하는 데 중요한 역할을 하고 있다는 점은 코드 컨벤션 관리를 통해 우리 팀이 더 나은 코드 품질을 유지하기 위한 노력에 잘 부합한다고 할 수 있습니다.&lt;/p&gt;

&lt;p&gt;다만, SonarQube를 도입한 이후에도 우리는 더 세부적인 규칙 검증과 구조적인 코딩 컨벤션 관리를 필요로 했고, 이 과정은 결국 Konsist와 같은 도구를 찾게 되는 계기가 되었습니다.&lt;/p&gt;

&lt;h1 id=&quot;konsist-코딩-컨벤션-관리의-새로운-도약&quot;&gt;Konsist: 코딩 컨벤션 관리의 새로운 도약&lt;/h1&gt;

&lt;p&gt;앞서 소개한 도구들(문서, Lint, SonarQube, 코드 리뷰)은 코딩 컨벤션을 유지하고 코드 품질을 관리하는 데 큰 도움이 되었지만, 여전히 해결되지 않는 문제들이 있었습니다. 예를 들어, 프로젝트의 구조적 규칙을 정의하거나, 클래스 네이밍 규칙, 의존성 방향과 같은 팀 고유의 복잡한 코딩 컨벤션을 자동으로 검증하는 것은 기존 도구로는 쉽지 않았습니다. 바로 이 지점에서 Konsist라는 새로운 도구가 빛을 발했습니다.&lt;/p&gt;

&lt;h3 id=&quot;konsist란-무엇인가&quot;&gt;Konsist란 무엇인가?&lt;/h3&gt;

&lt;p&gt;Konsist는 Kotlin 언어를 위해 설계된 정적 코드 분석 도구로, 기존의 Lint나 SonarQube가 다루기 어려운 구조적 규칙과 세세한 컨벤션을 검증할 수 있도록 설계되었습니다. Konsist의 가장 큰 특징은 API를 활용한 단위 테스트를 통해, 팀에서 정의한 코딩 규칙을 명확히 표현하고 자동화된 방식으로 검증할 수 있다는 점입니다.&lt;/p&gt;

&lt;p&gt;예를 들어, “Domain 레이어는 Application 레이어를 의존하지 않는다”와 같은 복잡한 구조적 규칙을 코드로 표현하고, 이를 테스트를 통해 검증할 수 있습니다. 이 도구는 기존 도구들의 한계를 보완하며, 코딩 컨벤션 관리의 새로운 가능성을 열어주었습니다.&lt;/p&gt;

&lt;h3 id=&quot;konsist의-주요-특징&quot;&gt;Konsist의 주요 특징&lt;/h3&gt;

&lt;h4 id=&quot;간단한-설정&quot;&gt;간단한 설정&lt;/h4&gt;

&lt;p&gt;Konsist는 Gradle이나 Maven에 의존성을 추가하고, JUnit이나 Kotest와 같은 테스트 프레임워크와 연동하면 바로 사용할 수 있습니다. 복잡한 설정 과정 없이 빠르게 적용할 수 있기 때문에 새로운 도구를 도입할 때 진입 장벽이 낮습니다. 우리 팀도 Konsist를 처음 접한 날 바로 프로젝트에 적용 테스트를 진행할 수 있을 만큼 사용이 간단했습니다.&lt;/p&gt;

&lt;h4 id=&quot;구조적-규칙-검증&quot;&gt;구조적 규칙 검증&lt;/h4&gt;

&lt;p&gt;Konsist는 기존 도구들이 다루기 어려운 프로젝트의 구조적 규칙을 검증할 수 있는 강력한 기능을 제공합니다. 예를 들어, 특정 레이어가 다른 레이어를 의존하지 않도록 규칙을 정의하거나, 클래스와 속성의 네이밍 규칙을 설정하고 이를 자동으로 검증할 수 있습니다. 이러한 기능은 프로젝트 구조와 일관성을 유지하는 데 큰 도움이 됩니다.&lt;/p&gt;

&lt;h4 id=&quot;효율성-향상&quot;&gt;효율성 향상&lt;/h4&gt;

&lt;p&gt;Konsist를 활용하면 코드 리뷰 과정에서 반복적으로 지적해야 했던 많은 규칙 위반을 자동화할 수 있습니다. 이는 코드 리뷰의 품질을 높이는 동시에, 리뷰어가 더 중요한 논의나 설계 검토에 집중할 수 있도록 해줍니다. 덕분에 팀의 생산성과 효율성이 크게 향상되었습니다.&lt;/p&gt;

&lt;h4 id=&quot;단순하면서도-강력한-구조&quot;&gt;단순하면서도 강력한 구조&lt;/h4&gt;

&lt;p&gt;Konsist는 복잡한 학습 없이 바로 사용할 수 있는 단순한 구조로 되어 있습니다. 개발자는 검증해야 할 코딩 규칙을 정의하는 데만 집중하면 되며, 불필요한 설정이나 복잡한 사용법으로 인해 시간을 낭비하지 않아도 됩니다. 이는 도구를 처음 접하는 개발자들에게도 사용이 용이하다는 점에서 큰 장점으로 작용합니다.&lt;/p&gt;

&lt;h3 id=&quot;konsist-활용-사례&quot;&gt;Konsist 활용 사례&lt;/h3&gt;

&lt;p&gt;이 글의 목적은 Konsist를 자세히 소개하는 데 있지 않습니다. Konsist를 시작하는 방법이나 설치 가이드는 &lt;a href=&quot;https://docs.konsist.lemonappdev.com/&quot; target=&quot;\_blank&quot;&gt;공식 홈페이지&lt;/a&gt; 에서 확인하실 수 있습니다. 대신, 이 글에서는 우리 팀이 Konsist를 활용하고 있는 몇 가지 사례를 통해, 이 도구를 어떻게 효과적으로 사용할 수 있을지 감을 잡을 수 있도록 도와드리고자 합니다.&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;/images/coding-convention-story/architecture.png&quot; alt=&quot;architecture&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Domain 레이어는 데이터 및 비즈니스 로직과 같은 도메인 로직을 처리하는 코드들이 위치하며,
Application 레이어는 표현 계층, Facade, 그리고 외부 리소스와의 연동을 담당하는 기반 코드들이 포함되어 있습니다.&lt;/p&gt;

&lt;p&gt;의존성 방향에 대한 규칙도 명확히 정의했습니다. Application 레이어는 Domain 레이어를 의존할 수 있지만, 반대로 Domain 레이어는 Application 레이어를 의존하지 않도록 컨벤션을 정했습니다.&lt;/p&gt;

&lt;p&gt;이러한 규칙을 코드로 표현하면 아래와 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;application 레이어는 domain 레이어를 의존한다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;Konsist&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;scopeFromProduction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;assertArchitecture&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;application&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Layer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Application&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;com.spoqa.cart.application..&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;domain&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Layer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Domain&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;com.spoqa.cart.domain..&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

            &lt;span class=&quot;n&quot;&gt;application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dependsOn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dependsOnNothing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;여담으로, 처음 구조 테스트를 실행했을 때 나름 코드 리뷰에서 꼼꼼하게 의존성 위반 여부를 점검했다고 생각했지만, 예상외로 다수의 의존성 위반 코드가 발견되었습니다. 이를 통해 코드 리뷰가 사람이 수행하는 작업인 만큼, 놓치는 부분이 생길 수밖에 없다는 점을 다시 한번 실감했습니다. 동시에, Konsist가 이런 문제를 효과적으로 해결할 수 있다는 점도 확실히 체감할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/coding-convention-story/architectural-test-result.png&quot; alt=&quot;architectural-test-result&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;class-컨벤션&quot;&gt;Class 컨벤션&lt;/h4&gt;

&lt;p&gt;또한 우리 팀에서는 Entity 클래스나 테스트 클래스들은 여러 이유로 인해 특정 클래스를 필수적으로 상속받도록 강제하고 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;amount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BigDecimal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BillType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;attachements&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BaseEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BaseEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// ... 생략&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetFacadeTest&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UnitTestBase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AdminMutationTest&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FunctionalTestBase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CartRepositoryTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;cartRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CartRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RepositoryTestBase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// ... 생략&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같은 컨벤션을 테스트하는 코드는 아래와 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Entity 클래스는 BaseEntity 클래스를 필수로 상속한다&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;Konsist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;scopeFromProduction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;classes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hasAnnotationOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;assertTrue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testName&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testCase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hasParentClassOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BaseEntity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;test 클래스는 Base 클래스 중 하나를 필수로 상속한다&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;Konsist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;scopeFromTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;classes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;assertTrue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testName&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testCase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hasParentClassOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;FunctionalTestBase&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;||&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hasParentClassOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UnitTestBase&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;||&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hasParentClassOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;RepositoryTestBase&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;||&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hasParentClassOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;IndexRepositoryTestBase&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;entity-property-컨벤션&quot;&gt;Entity Property 컨벤션&lt;/h4&gt;

&lt;p&gt;Kotlin으로 JPA를 사용하다 보면 종종 실수하기 쉬운 부분이 있습니다. 바로 JPA의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Column&lt;/code&gt; 어노테이션에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nullable&lt;/code&gt; 속성과 Entity 프로퍼티의 타입을 다르게 정의하는 경우입니다. 우리 팀은 이러한 실수를 방지하기 위해 아래와 같이 Declarations Test를 정의해 두었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Entity 클래스의 Column 프로퍼티가 non-nullable 타입이라면 &apos;@Column(nullable=false)&apos;가 선언되어야 한다&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;Konsist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;scopeFromProject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;classes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withAllAnnotationsOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withAllAnnotationsOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Column&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isNullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;assertTrue&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hasAnnotation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;annotation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;annotation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hasArgument&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;arg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;nullable&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;false&quot;&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이와 같이 테스트 코드를 작성하면 아래와 같이 어노테이션과 컬럼의 타입이 불일치하게 작성하는 경우를 효율적으로 방지할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BaseEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// ... 생략&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/images/coding-convention-story/property-test-result.png&quot; alt=&quot;property-test-result&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;마무리하며&quot;&gt;마무리하며&lt;/h1&gt;

&lt;p&gt;Konsist와 같은 도구는 팀이 코딩 컨벤션을 잘 지키도록 돕는 강력한 도구지만, 모든 문제를 해결할 수 있는 만능 솔루션은 아닙니다. 지나치게 세부적인 규칙은 생산성을 떨어뜨릴 수 있으며, 도구가 잡아내지 못하는 부분은 결국 사람이 검토해야 합니다.&lt;/p&gt;

&lt;p&gt;저희 백엔드팀은 Konsist, ktlint, SonarQube와 같은 도구를 적절히 조합하고, 코드 리뷰를 통해 이를 보완하면서 코딩 컨벤션을 지속적으로 발전시키고 있습니다. 이러한 과정은 단순히 코딩 규칙을 준수하는 것을 넘어, 팀의 협업 문화를 개선하고 더 나은 코드를 만드는 데 기여하고 있습니다.&lt;/p&gt;

&lt;p&gt;여러분의 팀은 코딩 컨벤션을 어떻게 관리하고 계신가요? 기회가 된다면 각자의 경험과 노하우를 공유하며 서로 배울 수 있는 시간이 있기를 기대합니다.&lt;/p&gt;

&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>AI를 활용한 주문서 생성 자동화: 카카오톡 주문을 키친보드 주문으로</title>
      <link>https://spoqa.github.io/2024/08/19/ai-order-sheet.html</link>
      <pubDate>Mon, 19 Aug 2024 00:00:00 +0000</pubDate>
      <author>남경호</author>
      <guid>/2024/08/19/ai-order-sheet</guid>
      <description>&lt;p&gt;안녕하세요. 스포카 백엔드팀 프로그래머 남경호입니다.&lt;/p&gt;

&lt;p&gt;최근 &lt;a href=&quot;https://chatgpt.com/&quot; target=&quot;\_blank&quot;&gt;ChatGPT&lt;/a&gt;를 비롯한 생성형 AI가 주목받으면서 저희 스포카에서도 AI 스터디를 진행했습니다. AI에 대해 들어본 적은 있었지만, 실제로 접해본 적은 없어서 스터디를 통해 &lt;a href=&quot;https://en.wikipedia.org/wiki/Prompt_engineering&quot; target=&quot;\_blank&quot;&gt;프롬프트 엔지니어링&lt;/a&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/Retrieval-augmented_generation&quot; target=&quot;\_blank&quot;&gt;RAG&lt;/a&gt;, &lt;a href=&quot;https://www.langchain.com/&quot; target=&quot;\_blank&quot;&gt;랭체인&lt;/a&gt; 등의 개념을 배우고 이를 어떻게 활용할 수 있는지에 대한 기초를 익힐 수 있었습니다.&lt;/p&gt;

&lt;p&gt;이번 글에서는 저희가 AI를 어떻게 제품에 활용했는지 소개해 드리고자 합니다. RAG를 사용하거나 복잡한 기술을 도입한 것은 아니지만, AI 초보 개발자도 사용자의 편의를 위해 제품에 AI를 효과적으로 활용할 수 있다는 점에서 의미가 있다고 생각합니다.&lt;/p&gt;

&lt;p&gt;모쪼록 재미있게 봐주시길 바랍니다.&lt;/p&gt;

&lt;h1 id=&quot;도입배경&quot;&gt;도입배경&lt;/h1&gt;

&lt;p&gt;키친보드를 사용하기 전, 매장의 점주님들은 유통사로 식자재를 주문할 때 주로 문자나 카카오톡을 이용했습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/ai-order-sheet/kakao-order.png&quot; alt=&quot;kakao-order&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;/li&gt;
&lt;/ul&gt;

&lt;p&gt;저희 키친보드는 이러한 불편을 해소하기 위해 주문톡 서비스를 운영하고 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;하지만 여전히 일부 매장은 카카오톡으로 유통사에 식자재를 주문하고 있습니다. 유통사는 키친보드의 편리함을 느끼고 이를 적극적으로 사용하지만, 몇몇 매장은 기존의 카카오톡 주문 방식을 고수하고 있는 것입니다.&lt;/p&gt;

&lt;p&gt;매장에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;피망 6개, 양파 2망, 올리브오일 2개 주세요&lt;/code&gt;와 같이 간단히 주문하는 것이 편리할 수 있습니다. 유통사가 알아서 품목을 해석하고 적절한 제품을 보내주기 때문입니다. 하지만 유통사는 매장의 주문을 해석하고 취합하는 데 많은 시간을 소모하게 됩니다. 더욱이 이러한 지식은 오랜 경험을 바탕으로 작업자들이 처리해 왔기 때문에, 작업자가 바뀌면 배송 실수가 발생할 가능성이 큽니다.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;그래서 저희 키친보드는 오래전부터 카카오톡으로 받은 주문을 어떻게 하면 손쉽게 주문톡의 주문서로 변환할 수 있을지 고민해 왔습니다. 주문톡 서비스를 처음 만들 때도 카카오톡 주문을 주문서로 생성하려는 시도도 물론 있었습니다. 일정한 패턴만 정의할 수 있다면 카카오톡 주문을 해석하고, 이를 기반으로 주문서를 생성하는 것이 가능할 것으로 생각했기 때문입니다.&lt;/p&gt;

&lt;p&gt;하지만 저희는 아래와 같이 매장에서 주문하는 다양한 패턴에 좌절하고 맙니다.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;피망 6개, 양파 2망, 올리브오일 2개 주세요
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;다진김치1  콩나물2개 깻잎2개 계란5판 청양고추1 깐마늘1 다진마늘1 쌈무4개 날치알3개 떡사리1개 모짜치즈1개 버섯1 만두2개 맛소금1 마요네즈1개
다진김치는 중국산으로 주세요 3kg
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;맛살-2
치즈떡-3
연근-3
메추리알-3
고구마떡-2
비엔나-3봉지
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;콩나물4개 깻잎4개 날치알4개 튀김고구마1개 만두4개 슬라이스치즈1개 오이 3숙주나물 0,5 청양고추1 스위트곤1개 다진마늘1참맛기름1개 설탕1개 계란5 쌈장1개 쌈무6개 당면1개 마요네즈1개 떡사리1개 모짜치즈2개 고추맛기름1 김가루1개 김2개 짜파게티5개입 2 사세바팔로윙스틱1개 미풍1개 물엿1 마카로니1개 감자튀김 1
레몬 4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;어떤 매장은 품목명과 개수를 띄어쓰기도 하고, 어떤 매장은 품목명과 개수 사이에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&lt;/code&gt;를 넣기도 합니다. 또 어떤 매장은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;00개&lt;/code&gt;라고 표기하거나, 같은 내용의 주문에서도 다른 표기법을 사용하기도 합니다.&lt;/p&gt;

&lt;p&gt;결국, 저희는 다양한 주문 패턴을 단순히 조건 처리로 해결하기에는 무리가 있다고 판단했고, 카카오톡 주문을 주문톡으로 자동 변환하는 과제를 이후로 미루기로 했습니다.&lt;/p&gt;

&lt;h1 id=&quot;ai-활용&quot;&gt;AI 활용&lt;/h1&gt;

&lt;p&gt;AI 스터디를 진행하면서 저희는 과거에 좌절했던 카카오톡 주문 해석 문제를 AI로 해결할 수 있다고 생각했습니다. AI를 활용하면 다양한 패턴의 문자 내용을 우리가 원하는 포맷으로 변환할 수 있고, 이를 바탕으로 주문서를 생성할 수 있을 것으로 생각했기 때문입니다.&lt;/p&gt;

&lt;p&gt;그래서 저희는 아래와 같이 몇 가지 사례를 기반으로 테스트를 진행했습니다.&lt;/p&gt;

&lt;h3 id=&quot;사례1&quot;&gt;사례1&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Prompt
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Convert the given &apos;text&apos; into JSON format with &apos;name&apos; and &apos;count&apos; as keys. Where &apos;name&apos; is of type String and &apos;count&apos; is of type integer.

text:
다진김치1  콩나물2개 깻잎2개 계란5판 청양고추1 깐마늘1 다진마늘1 쌈무4개 날치알3개 떡사리1개 모짜치즈1개 버섯1 만두2개 맛소금1 마요네즈1개
다진김치는 중국산으로 주세요 3kg
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;Output
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[
  { &quot;name&quot;: &quot;다진김치&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;콩나물&quot;, &quot;count&quot;: 2 },
  { &quot;name&quot;: &quot;깻잎&quot;, &quot;count&quot;: 2 },
  { &quot;name&quot;: &quot;계란&quot;, &quot;count&quot;: 5 },
  { &quot;name&quot;: &quot;청양고추&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;깐마늘&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;다진마늘&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;쌈무&quot;, &quot;count&quot;: 4 },
  { &quot;name&quot;: &quot;날치알&quot;, &quot;count&quot;: 3 },
  { &quot;name&quot;: &quot;떡사리&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;모짜치즈&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;버섯&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;만두&quot;, &quot;count&quot;: 2 },
  { &quot;name&quot;: &quot;맛소금&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;마요네즈&quot;, &quot;count&quot;: 1 }
]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;사례2&quot;&gt;사례2&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Prompt
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Convert the given &apos;text&apos; into JSON format with &apos;name&apos; and &apos;count&apos; as keys. Where &apos;name&apos; is of type String and &apos;count&apos; is of type integer.

text:
콩나물 8
깻잎 3
청양고추 2
깐마늘 2
다진마늘 2
홍고추 1
양배추 1
날치알 3
만두 2
갖다주세요
당면 3개추가좀 해주세요 ㅠ
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;Output
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[
  { &quot;name&quot;: &quot;콩나물&quot;, &quot;count&quot;: 8 },
  { &quot;name&quot;: &quot;깻잎&quot;, &quot;count&quot;: 3 },
  { &quot;name&quot;: &quot;청양고추&quot;, &quot;count&quot;: 2 },
  { &quot;name&quot;: &quot;깐마늘&quot;, &quot;count&quot;: 2 },
  { &quot;name&quot;: &quot;다진마늘&quot;, &quot;count&quot;: 2 },
  { &quot;name&quot;: &quot;홍고추&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;양배추&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;날치알&quot;, &quot;count&quot;: 3 },
  { &quot;name&quot;: &quot;만두&quot;, &quot;count&quot;: 2 },
  { &quot;name&quot;: &quot;갖다주세요&quot;, &quot;count&quot;: 0 },
  { &quot;name&quot;: &quot;당면&quot;, &quot;count&quot;: 3 },
  { &quot;name&quot;: &quot;개추가좀 해주세요 ㅠ&quot;, &quot;count&quot;: 0 }
]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;사례3&quot;&gt;사례3&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Prompt
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Convert the given &apos;text&apos; into JSON format with &apos;name&apos; and &apos;count&apos; as keys. Where &apos;name&apos; is of type String and &apos;count&apos; is of type integer.

text:
야채 유통사에 넣어주세요.
새송이-1박스
숙주나물 -1박스
깐마늘-2봉지
팽이버섯-1박스
대파-1단
치즈떡-1봉지
메추리알-1봉지
불고기햄-1봉지
맛소금-1봉지
토마토 케첩-봉지에 들은걸로 2.5킬로?? 그걸로 ?? 9봉지
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;Output
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[
  { &quot;name&quot;: &quot;새송이&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;숙주나물&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;깐마늘&quot;, &quot;count&quot;: 2 },
  { &quot;name&quot;: &quot;팽이버섯&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;대파&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;치즈떡&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;메추리알&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;불고기햄&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;맛소금&quot;, &quot;count&quot;: 1 },
  { &quot;name&quot;: &quot;토마토 케첩&quot;, &quot;count&quot;: 9 }
]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;개추가좀 해주세요 ㅠ&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;갖다주세요&lt;/code&gt; 와 같이 몇 가지 잘못 해석하는 부분은 있었지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;토마토 케첩-봉지에 들은걸로 2.5킬로?? 그걸로 ?? 9봉지&lt;/code&gt;를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;name&quot;=&quot;토마토 케첩&quot;, &quot;count&quot;=9}&lt;/code&gt;로 인식한다든지 패턴이 일정하지 않더라도 품목명과 주문수를 원하는 대로 구분해 주는 것들을 보면 AI를 통해 충분히 카카오톡 주문 내용을 해석하여 원하는 패턴으로 변환할 수 있을 것으로 생각했습니다.&lt;/p&gt;

&lt;h1 id=&quot;주문서-품목-선택&quot;&gt;주문서 품목 선택&lt;/h1&gt;

&lt;p&gt;앞에서 저희는 AI를 통해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;품목명&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;주문 건수&lt;/code&gt;와 같이 일관된 양식으로 변환할 수 있다는 것을 알게 되었습니다. 하지만 이번에는 해석된 품목명을 키친보드에 입력된 유통사의 품목으로 변환하는 데 문제가 있었는데요.&lt;/p&gt;

&lt;p&gt;키친보드에서 다루는 유통사의 주문 품목은 품목명만 있는 것이 아닌 규격과 단위가 존재합니다. 예를 들어 같은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;계란&lt;/code&gt;이라는 품목이 있더라도 규격은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;대란&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;중란&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;특란&lt;/code&gt;과 같이 나눠질 수 있고 단위도 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;6개입&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;개&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;판&lt;/code&gt;과 같이 나누어질 수 있습니다. 그래서 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;계란&lt;/code&gt;을 주문하려고 하면 다수의 품목이 검색되어 어떤 품목을 주문해야 할지 시스템이 판단하기 어렵다는 문제가 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/ai-order-sheet/eggs.png&quot; alt=&quot;eggs&quot; /&gt;&lt;/p&gt;

&lt;p&gt;또 다른 사례로, 매장에서는 유통사가 입력해 둔 정확한 품목명을 기억하고 주문하지 않는 경우가 많습니다. 예를 들어 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;깐마늘&lt;/code&gt;을 생각해 보겠습니다. 유통사는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;깐마늘&lt;/code&gt;을 그대로 저장하지 않고, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;마늘/깐(대)&lt;/code&gt;와 같이 유통사의 규칙에 따라 품목명을 저장해두고 주문을 받고 있습니다.&lt;/p&gt;

&lt;p&gt;이 때문에 키친보드에서는 주문톡을 사용하는 매장이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;깐마늘&lt;/code&gt;이라고 검색하더라도, 유통사에서 저장한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;마늘/깐(대)&lt;/code&gt; 품목을 조회할 수 있도록 유사도 검색 기능을 지원하고 있습니다. (다만 유사도 검색에는 원치 않는 품목이 함께 조회될 가능성이 있다는 단점도 존재합니다)&lt;/p&gt;

&lt;p&gt;예를 들어 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;깐마늘&lt;/code&gt;을 검색했을 때, 아래처럼 다양한 품목들이 조회되는 것을 볼 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/ai-order-sheet/search-products.png&quot; alt=&quot;search-products&quot; /&gt;&lt;/p&gt;

&lt;p&gt;그래서 저희는 주문할 품목 선택을 시스템이 자동으로 하기보다는 사용자에게 위임하기로 했습니다. 잘못된 품목으로 주문서가 생성되는 위험보다는 매장에서 원하는 품목을 한 번 더 확인하도록 하는 것이 낫다는 판단에서입니다.&lt;/p&gt;

&lt;p&gt;다만, 사용자 편의를 위해 입력한 품목명과 가장 유사한 품목을 자동으로 선택하고, 최근에 주문했던 품목을 우선 선택하도록 하여 최대한 편리하게 주문서를 생성할 수 있도록 했습니다.&lt;/p&gt;

&lt;h1 id=&quot;ai-주문서-생성-순서&quot;&gt;AI 주문서 생성 순서&lt;/h1&gt;

&lt;p&gt;위에서 말씀드린 내용을 기반으로 사용자가 AI 주문서를 생성하는 순서를 그려보면 아래와 같습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/ai-order-sheet/ai-order-flow.png&quot; alt=&quot;ai-order-flow&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;주문서-초안-생성&quot;&gt;주문서 초안 생성&lt;/h3&gt;

&lt;p&gt;먼저 사용자는 카카오톡 주문 내용을 복사해서 주문서 초안을 생성합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/ai-order-sheet/create-draft.png&quot; alt=&quot;create-draft&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;주문-품목-선택&quot;&gt;주문 품목 선택&lt;/h3&gt;

&lt;p&gt;앞서 “주문서 품목 선택 이슈”에서 말씀드린 바와 같이 주어진 품목명이 원하는 주문서 품목과 일치하는지 정확히 알기 힘들기 때문에 생성된 주문서 초안에서 원하는 품목을 선택합니다. 다만, 가장 많이 주문하는 품목이 먼저 선택되어 있도록 함으로써 사용자가 품목을 선택하는 행위를 최소화하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/ai-order-sheet/select-product.png&quot; alt=&quot;select-product&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;주문서-확인-및-주문&quot;&gt;주문서 확인 및 주문&lt;/h3&gt;

&lt;p&gt;사용자는 최종적으로 생성될 주문서를 확인하고 주문서를 생성할 수 있습니다. 이때 원치 않는 품목을 제거하거나 추가하여 AI가 생성해 준 품목을 토대로 기대한 주문서를 생성할 수 있게 됩니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/ai-order-sheet/check-order-sheet.png&quot; alt=&quot;check-order-sheet&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;구현&quot;&gt;구현&lt;/h1&gt;

&lt;p&gt;이제 기술적인 측면을 살펴보겠습니다. 사실 AI를 활용한 주문서 생성 기능은 기존의 주문서 생성 기능을 크게 변경하지 않았습니다. 앞서 설명해 드린 AI 주문서 생성 순서에서, 초안을 생성하는 부분만 추가되었을 뿐이죠.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/ai-order-sheet/ai-order-flow-for-dev.png&quot; alt=&quot;ai-order-flow-for-dev&quot; /&gt;&lt;/p&gt;

&lt;p&gt;그래서 주문서 초안을 생성하는 흐름도를 조금 더 자세히 그려보면 아래와 같이 그릴 수 있습니다. 조금 더 자세한 내용은 아래에서 다루어볼게요.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/ai-order-sheet/create-draft-diagram.png&quot; alt=&quot;create-draft-diagram&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;spring-ai&quot;&gt;Spring AI&lt;/h3&gt;

&lt;p&gt;AI 스터디를 진행하면서 읽었던 책은 &lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000212568407&quot; target=&quot;\_blank&quot;&gt;랭체인으로 LLM 기반의 AI 서비스 개발하기&lt;/a&gt;였습니다. 이 책은 LLM 기반의 AI 서비스를 예제로 쉽게 따라 할 수 있도록 가이드해 주어, AI 초보자인 저도 손쉽게 학습할 수 있었습니다. (광고 아닙니다!)&lt;/p&gt;

&lt;p&gt;처음 학습할 때 랭체인으로 시작했기 때문에, AI 모델에게 주문 내용을 해석해 주는 기능을 별도의 Python 서버로 구현하는 방안을 고민하기도 했습니다. 그러나 팀 정책상, 관리 포인트를 불필요하게 늘리지 않기 위해 키친보드 서비스의 서버에서 최대한 해결하는 것이 좋겠다고 판단했습니다. 그러던 중 발견한 프레임워크가 바로 Spring AI였습니다.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://spring.io/projects/spring-ai&quot; target=&quot;\_blank&quot;&gt;Spring AI&lt;/a&gt;는 AI 엔지니어링에 특화된 프레임워크로, 랭체인만큼 다양한 기능을 제공하지는 않지만, 저희가 구현하고자 했던 카카오톡 주문 내용을 원하는 형식으로 변환하는 데는 부족함이 없었습니다. 또한, 스프링과 쉽게 통합할 수 있었기에 더할 나위 없는 선택지였습니다. (자세한 사용법은 &lt;a href=&quot;https://docs.spring.io/spring-ai/reference/getting-started.html&quot; target=&quot;\_blank&quot;&gt;공식 문서&lt;/a&gt;를 참고해 주세요!)&lt;/p&gt;

&lt;h3 id=&quot;주문서-초안-생성-로직&quot;&gt;주문서 초안 생성 로직&lt;/h3&gt;

&lt;p&gt;추문서 초안 생성을 위한 로직을 보면 아래와 같습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/ai-order-sheet/create-draft-data-flow.png&quot; alt=&quot;create-draft-data-flow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;주문 내용을 해석해 줄 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessageInterpreter&lt;/code&gt;가 AI 서비스에 해석요청을 보내고 그 결과를 바탕으로 유통사의 품목을 조회해 초안을 생성해 준다는 것을 알 수 있습니다.&lt;/p&gt;

&lt;p&gt;저희는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessageInterpreter&lt;/code&gt;의 구현 클래스에서 Spring AI를 활용하였습니다. 구현 클래스인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AIMessageInterpreter&lt;/code&gt;를 보면 상당히 단순한 것을 볼 수 있습니다. Spring AI는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ChatClient&lt;/code&gt;를 통해 AI 서비스와의 Prompt 통신을 단순화한 것을 볼 수 있습니다. 또한, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;responseEntity&lt;/code&gt; 함수와 같이 응답 결과를 원하는 데이터 구조로 손쉽게 변환할 수 있도록 해주기 때문에 AI 채팅의 결과를 원하는 데이터 객체로 변환하기 위한 복잡하고 번거로운 코드를 작성하지 않아도 된다는 장점도 있습니다.&lt;/p&gt;

&lt;p&gt;Java가 아닌 Kotlin으로 Spring AI를 사용하시는 분들께 드리는 한가지 팁은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;responseEntity&lt;/code&gt;의 반환 타입인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OrderProduct&lt;/code&gt;를 정의할 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@JvmRecord&lt;/code&gt;를 선언해 주지 않으면 오류가 발생하니 이 부분은 기억해 두시면 좋겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MessageInterpreter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;interpretOrderMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AIMessageInterpreter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;openAiChatClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ChatClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;aiChatHistoryRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AIChatHistoryRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MessageInterpreter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;interpretOrderMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;promptMessage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
            |Convert the given &apos;text&apos; into JSON format with &apos;name&apos; and &apos;count&apos; as keys. Where &apos;name&apos; is of type String and &apos;count&apos; is of type integer.

            |text:
            |$message
            &quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trimMargin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;openAiChatClient&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;prompt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;promptMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;responseEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parameterizedTypeRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;())&lt;/span&gt;

        &lt;span class=&quot;nf&quot;&gt;saveHistory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@JvmRecord&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Float&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;주문-품목-병렬-조회&quot;&gt;주문 품목 병렬 조회&lt;/h3&gt;

&lt;p&gt;위에서 보여드린 예시처럼, 매장에서는 한 번에 여러 품목을 주문하는 경우가 많습니다. 예를 들어, 주문서 품목을 조회하는 데 1초가 걸린다고 가정하면, 30개의 품목을 조회하는 로직을 아래와 같이 구현했을 때, 주문서 초안을 생성하는 데 최소 30초가 소요될 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getOrderSheetDraftProducts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetDraftProductsSearchData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheetDraftProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderProducts&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aiMessageInterpreter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;interpretOrderMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderProducts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;products&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;searchOrderableProducts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;nc&quot;&gt;OrderSheetDraftProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;foundProducts&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;당연한 이야기겠지만, 주문서 초안을 생성하는 데 시간이 오래 걸리면 사용자는 불편함을 느껴 AI 주문서 기능을 더 이상 적극적으로 사용하지 않게 될 수 있습니다. 이를 방지하기 위해 저희는 병렬 처리 방식을 도입하여 주문서를 조회하고 초안을 생성함으로써, 최대한 짧은 시간 안에 사용자의 요청을 처리할 수 있도록 했습니다.&lt;/p&gt;

&lt;p&gt;아래와 같이 코드를 작성하면 다수의 품목을 조회하더라도 약 1초 만에 전체 품목의 조회 결과를 반환할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getOrderSheetDraftProducts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetDraftProductsSearchData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheetDraftProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;runBlocking&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderProducts&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aiMessageInterpreter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;interpretOrderMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;orderProducts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;products&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;searchOrderableProducts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

                &lt;span class=&quot;nc&quot;&gt;OrderSheetDraftProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;foundProducts&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;awaitAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;마무리&quot;&gt;마무리&lt;/h1&gt;

&lt;p&gt;지금까지 Spring AI를 활용하여 카카오톡 주문을 키친보드 주문서로 자동 생성하는 기능을 구현한 사례를 소개해 드렸습니다. 비록 이번 구현이 단순한 수준이어서 AI 도입 사례로 소개하기에 다소 민망할 수 있지만, 이 과정에서 저희가 가진 문제를 어떻게 해결할지 고민한 점과, AI 도입이 생각만큼 복잡하거나 어려운 일이 아니라는 점을 봐주시면 좋겠습니다.&lt;/p&gt;

&lt;p&gt;최근 OpenAI에서 &lt;a href=&quot;https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/&quot; target=&quot;\_blank&quot;&gt;GPT-4o-mini 모델이 출시&lt;/a&gt;되어, 저희가 구현한 것처럼 경량 모델이 필요한 경우에는 GPT-3.5 Turbo 모델보다 비용 효율적이고 다양한 기능을 사용할 수 있게 되었습니다. 그래서 앞으로 이미지 기반 주문서 생성 기능도 도입할 계획입니다. 이를 통해, 유통사에서 화이트보드나 손 글씨로 작성해 카카오톡으로 보내던 주문 요청을 키친보드로 쉽게 전환할 수 있도록 할 예정입니다. (생각보다 변환 결과가 괜찮았습니다!)&lt;/p&gt;

&lt;p&gt;또한, 아직 경험이 부족하여 키친보드의 데이터를 사전 학습시켜 주문서를 생성하는 RAG 방식을 도입하지는 못했지만, 더 나은 기능을 제공할 아이디어가 떠 오른다면, 조금 더 학습하여 RAG 방식을 도입해 AI를 조금 더 풍부하게 활용해 보고 싶습니다.&lt;/p&gt;

&lt;p&gt;또 다른 사용 사례가 생기면, 다음에 더 유익한 글로 찾아뵙겠습니다.&lt;/p&gt;

&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Querydsl에서 Kotlin JDSL 으로</title>
      <link>https://spoqa.github.io/2024/05/03/transfer-jdsl.html</link>
      <pubDate>Fri, 03 May 2024 00:00:00 +0000</pubDate>
      <author>남경호</author>
      <guid>/2024/05/03/transfer-jdsl</guid>
      <description>&lt;p&gt;안녕하세요.&lt;/p&gt;

&lt;p&gt;또다시 전환 이야기로 찾아온 스포카 백엔드팀 프로그래머 남경호입니다. (이번에는 제목을 다르게 지어봤어요.)&lt;/p&gt;

&lt;p&gt;최근 저희 백엔드팀에서는 &lt;a href=&quot;https://github.com/querydsl/querydsl&quot; target=&quot;\_blank&quot;&gt;Querydsl&lt;/a&gt;을 &lt;a href=&quot;https://github.com/line/kotlin-jdsl&quot; target=&quot;\_blank&quot;&gt;Kotlin JDSL&lt;/a&gt;로 전환하는 작업을 진행하였는데요. Querydsl은 Spring Framework를 사용하고 계신다면 누구나 알고 계실 유명한 쿼리 빌더 라이브러리입니다. 한편 Kotlin JDSL은 2021년 라인에서 공개한 쿼리빌더 라이브러리입니다.&lt;/p&gt;

&lt;p&gt;저희가 이렇게 유명한 라이브러리인 Querydsl을 Kotlin JDSL로 변경하게 된 계기는 무엇이며 변경하면서 저희가 겪었던 이슈나 전환 시 저희가 사용했던 여러 가지 팁들을 공유해 드리면서 전환 이야기를 나눠보도록 하겠습니다.&lt;/p&gt;

&lt;h1 id=&quot;전환-배경&quot;&gt;전환 배경&lt;/h1&gt;

&lt;h2 id=&quot;kapt를-요하는-querydsl&quot;&gt;kapt를 요하는 Querydsl&lt;/h2&gt;

&lt;p&gt;Kotlin에서 Querydsl을 사용하기 위해서는 kapt가 필요합니다. kapt는 Kotlin의 Annotation Processor를 활용하기 위한 도구로, 주로 Kotlin에서 작성된 코드의 Annotation을 처리하는 데 사용됩니다. 그러나 공식 문서에 따르면 kapt의 새로운 기능 지원이 중단되고 유지보수만 진행될 예정이라고 합니다. 앞으로는 Kotlin에 더 친화적이고 빌드 시간이 더 빠른 KSP를 권장할 것으로 예상됩니다. 이는 결국 kapt가 향후 Kotlin에서 지원되지 않을 가능성이 높다는 것을 의미합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/transfer-jdsl/kapt-warning.png&quot; alt=&quot;kapt-warning&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Kotlin에서 Querydsl를 사용하려면 Gradle에서 의존성을 추가할 때 다음과 같이 kapt가 필요합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// build.gradle.kts&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;plugins&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;kotlin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;kapt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;1.9.21&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;dependencies&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;implementation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;com.Querydsl:Querydsl-jpa:5.1.0:jakarta&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;kapt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;com.Querydsl:Querydsl-apt:5.1.0:jakarta&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;kapt를 사용하지 않는다면 KSP를 대안으로 고려할 수 있습니다. 그러나 &lt;a href=&quot;https://github.com/Querydsl/Querydsl/issues/3284&quot; target=&quot;\_blank&quot;&gt;[Kotlin] Migrate kapt to ksp&lt;/a&gt; 이슈에서 보면 Querydsl에서 KSP를 지원할지 여부는 아직 미지수인듯 합니다.&lt;/p&gt;

&lt;h2 id=&quot;querydsl-유지보수&quot;&gt;Querydsl 유지보수&lt;/h2&gt;

&lt;p&gt;Querydsl의 &lt;a href=&quot;https://github.com/Querydsl/Querydsl/releases&quot; target=&quot;\_blank&quot;&gt;릴리스 노트&lt;/a&gt;를 살펴보면 배포 주기가 상당히 길다는 사실을 확인할 수 있습니다. 2018년에는 2번, 2019년에는 1번, 2020년에는 2번, 2021년에는 두 번, 그리고 2024년에는 한 번의 릴리스가 있었습니다. 이렇듯 이슈는 꾸준히 제기되고 있지만, 실제로 적극적인 패치나 새로운 기능의 개발은 이루어지고 있지 않은 실정입니다.&lt;/p&gt;

&lt;p&gt;오픈소스를 선택할 때 때 큰 이유 중 하나는 이슈에 대한 적극적인 패치와 업데이트로 인한 신뢰성과 안정성을 기대하기 때문입니다. 그러나 이와 같은 상황은 Querydsl을 사용하는 경우 이슈에 대한 신속한 대응과 새로운 기능 개발을 기대하기 어렵다는 것을 보여줍니다. 따라서 우리는 Querydsl이 더 이상 오픈소스로서 안정성을 유지하는 데 어려움이 있다고 판단했습니다.&lt;/p&gt;

&lt;h1 id=&quot;왜-kotlin-jdsl인가&quot;&gt;왜 Kotlin JDSL인가?&lt;/h1&gt;

&lt;h2 id=&quot;쿼리-빌더-선택-비교&quot;&gt;쿼리 빌더 선택 비교&lt;/h2&gt;

&lt;p&gt;Kotlin 생태계에서는 Querydsl 외에도 여러 선택지가 있습니다. 아래는 Kotlin에서 사용할 수 있는 다양한 쿼리 빌더들 중에서 저희가 검토해 본 것들입니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Exposed&lt;/strong&gt;: Jetbrains사에서 만든 Kotlin 경량 ORM 입니다. 직관적이고 간결한 API를 제공합니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ktorm&lt;/strong&gt;: Kotlin을 위한 경량 ORM 라이브러리로, DSL을 통한 효율적인 쿼리 작성이 특징입니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;JOOQ&lt;/strong&gt;: Java의 또 다른 유명한 쿼리 빌더입니다. Kotlin에서도 사용할 수 있으며, 타입 안전성을 강조하고 SQL을 자동으로 생성합니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;JPA Criteria API&lt;/strong&gt;: 영속화를 위한 쿼리언어인 JPQL을 자바 코드로 작성할 수 있도록 지원하는 쿼리 빌더입니다. JPA를 사용한다면 추가로 의존성을 추가해 줄 필요 없이 사용할 수 있습니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Query Method&lt;/strong&gt;: Spring Data JPA의 Query Methods는 함수명을 분석하여 자동으로 SQL 쿼리를 생성해 주는 기능을 제공합니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Kotlin JDSL&lt;/strong&gt;: Kotlin으로 메타모델 없이 작성할 수 있는 쿼리 빌더입니다. JPQL을 기반으로 쿼리가 생성됩니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;exposed-ktorm&quot;&gt;Exposed, Ktorm&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/JetBrains/Exposed&quot; target=&quot;\_blank&quot;&gt;Exposed&lt;/a&gt;와 &lt;a href=&quot;https://github.com/kotlin-orm/ktorm&quot; target=&quot;\_blank&quot;&gt;Ktorm&lt;/a&gt;은 둘 다 ORM 라이브러리입니다. 저희는 이미 JPA를 ORM으로 사용하고 있으며, JPA의 엔티티로 반환되는 쿼리 결과 및 이를 활용한 Dirty Checking, Lazy Loading, Cascade 등의 비즈니스 로직이 많이 존재합니다. 따라서 Exposed와 Ktorm과 같은 ORM을 대체하는 옵션은 고려되지 않았습니다.&lt;/p&gt;

&lt;h3 id=&quot;jooq&quot;&gt;JOOQ&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/jOOQ/jOOQ&quot; target=&quot;\_blank&quot;&gt;JOOQ&lt;/a&gt;는 Java 생태계에서 새롭게 주목받는 유명한 쿼리 빌더입니다. 타입 안정성을 제공하여 MyBatis와는 달리 뛰어난 유지보수성을 가지고 있으며, ORM인 Hibernate와 비교하여 SQL 친화적이어서 최적화된 쿼리와 세밀한 쿼리 작성이 가능합니다.&lt;/p&gt;

&lt;p&gt;저희는 &lt;a href=&quot;https://www.jooq.org/doc/latest/manual/sql-execution/alternative-execution-models/using-jooq-with-jpa/&quot; target=&quot;\_blank&quot;&gt;JOOQ가 JPA와 함께 사용할 수 있다는 점&lt;/a&gt;에 주목하여 POC를 진행했습니다. 그러나 멀티 모듈 설정이나 메타모델 생성을 위한 복잡한 메커니즘 등으로 인해 JPA와 함께 사용하는 것에 대한 거부감을 느꼈습니다. 또한 JOOQ에서 권장하는 방식과 다르기 때문에 JPA와 함께 사용할 때 발생할 수 있는 이슈에 대한 공식적인 지원을 받기 어려울 것이라는 우려도 있었습니다.&lt;/p&gt;

&lt;p&gt;이러한 이유로 JOOQ도 전환 대상에서 제외하기로 했습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/transfer-jdsl/jooq-with-jpa.png&quot; alt=&quot;jooq-with-jpa&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;spring-data의-query-method&quot;&gt;Spring Data의 Query Method&lt;/h3&gt;

&lt;p&gt;Spring Data에서 제공해 주는 &lt;a href=&quot;https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html&quot; target=&quot;\_blank&quot;&gt;Query Method&lt;/a&gt;는 아래와 같이 쿼리빌더를 통해 쿼리를 세세하게 작성하지 않아도 원하는 쿼리를 실행시켜 준다는 장점이 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AdminRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpaRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByEmail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;만약 복잡한 쿼리를 생성해야 하는 경우에는 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Query&lt;/code&gt; 어노테이션을 활용하여 작성하기도 합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AdminRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpaRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;SELECT a FROM Admin a JOIN AdminMeta b on a.id = b.adminId WHERE b.isActive is true &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findActive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Query Method를 활용하면 추가된 라이브러리 없이 간편하게 쿼리를 작성할 수 있지만, 작성된 쿼리가 컴파일 시점에서 정상 작동하는지 확인하기 어렵다는 점이 있습니다. 이는 테스트 코드를 통해 보완할 수 있지만, 추가로 매개변수에 따라 동적으로 쿼리를 작성하는 기능을 활용하기 어려운 단점도 있기에 Spring Data의 Query Method사용은 Querydsl의 대체제로는 제외되었습니다.&lt;/p&gt;

&lt;h3 id=&quot;jpa의-criteria-api&quot;&gt;JPA의 Criteria API&lt;/h3&gt;

&lt;p&gt;JPA의 &lt;a href=&quot;https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#criteria&quot; target=&quot;\_blank&quot;&gt;Criteria API&lt;/a&gt;를 사용하면 Querydsl과 같이 쿼리를 코드로 생성할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findPrivateStores&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;cb&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;criteriaBuilder&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cb&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;java&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;apply&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;store&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;java&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

            &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;equal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;StoreType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;storeType&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;StoreType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultList&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;또한, Spring Data에서 지원하는 &lt;a href=&quot;https://docs.spring.io/spring-data/jpa/reference/jpa/specifications.html&quot; target=&quot;\_blank&quot;&gt;Specifications&lt;/a&gt;를 통해서 동적 쿼리도 작성할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;StoreRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpaRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpaSpecificationExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;StoreSpecification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;createdAtGte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OffsetDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?,&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;createdAtLte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OffsetDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Specification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;toPredicate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CriteriaQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CriteriaBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Predicate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;predicates&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutableListOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Predicate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;()&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;createdAtGte&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;predicates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;greaterThanOrEqualTo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;createdAt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;createdAtLte&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;predicates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;lessThanOrEqualTo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;createdAt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;and&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;predicates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toTypedArray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;test specification&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;storeRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;StoreSpecification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;createdAtGte&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;createdAtLte&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// select * from store s1_0 where 1=1;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;storeRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;StoreSpecification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;createdAtGte&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OffsetDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;createdAtLte&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// select * from store s1_0 where s1_0.created_at&amp;gt;=&apos;2021-01-01T01:00:00.000+0900&apos;;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;storeRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;StoreSpecification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;createdAtGte&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;createdAtLte&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OffsetDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// select * from store s1_0 where s1_0.created_at&amp;lt;=&apos;2024-04-26T23:04:53.156+0900&apos;;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;storeRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;StoreSpecification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;createdAtGte&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OffsetDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;createdAtLte&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OffsetDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// select * from store s1_0 where s1_0.created_at&amp;gt;=&apos;2024-04-26T23:04:53.162+0900&apos; and s1_0.created_at&amp;lt;=&apos;2024-04-26T23:04:53.162+0900&apos;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;코드를 살펴보면 조건문에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s.get&amp;lt;StoreType&amp;gt;(&quot;storeType&quot;)&lt;/code&gt;과 같이 문자열 매개변수를 사용하는 것을 알 수 있습니다. 이는 타입 안정성이 다소 떨어지기 때문에 유지보수에 대한 고민이 있었는데요. 이러한 문제를 해결하기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s.get&amp;lt;StoreType&amp;gt;(Store::storeType.name)&lt;/code&gt;과 같이 Entity의 Property로 정의하거나 Hibernate의 &lt;a href=&quot;https://docs.jboss.org/hibernate/jpamodelgen/1.0/reference/en-US/html_single/#whatisit&quot; target=&quot;\_blank&quot;&gt;메타모델&lt;/a&gt;을 활용하여 타입 안정성을 확보할 수 있습니다. 그러나 여전히 매개변수로 문자열을 사용할 수 있기 때문에 팀 내에서 엄격한 코딩 규칙을 정하고 사용을 제한하는 방법으로 타입 안정성을 보장할 수 있을 것으로 생각됩니다.&lt;/p&gt;

&lt;p&gt;이렇듯 JPA의 Criteria API는 타입 안정성을 유지하면서 쿼리를 작성하고, Specifications를 통해 동적 쿼리를 지원하기 때문에 Kotlin JDSL을 발견하기 전까지 가장 유력한 전환 후보였습니다.&lt;/p&gt;

&lt;h3 id=&quot;kotlin-jdsl&quot;&gt;Kotlin JDSL&lt;/h3&gt;

&lt;p&gt;라인의 개발팀에서 개발한 &lt;a href=&quot;https://github.com/line/kotlin-jdsl&quot; target=&quot;\_blank&quot;&gt;Kotlin JDSL&lt;/a&gt;은 Querydsl이나 JOOQ와는 다르게 메타모델을 필요로 하지 않고 쿼리를 쉽게 작성할 수 있는 라이브러리입니다. DSL을 이용하여 타입 안정성 있는 쿼리를 작성할 수 있으며, 동적 쿼리를 간편하게 생성할 수 있는 장점을 갖고 있습니다. 또한 JPQL을 기반으로 동작하기 때문에 JPA와의 호환성도 우수합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;StoreRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpaRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KotlinJdslJpqlExecutor&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;namePredicate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;JDSL 매장&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;storeRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;like&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;%$namePredicate%&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;비록 추가적인 의존성을 필요로 하기는 하지만 DSL을 활용하여 Criteria API에 비해 간결하고 직관적인 쿼리를 작성할 수 있고 타입 안정성을 기본적으로 지원하기에 저희는 최종적으로 Kotlin JDSL으로 전환하기로 하였습니다.&lt;/p&gt;

&lt;h2 id=&quot;비교&quot;&gt;비교&lt;/h2&gt;

&lt;p&gt;다시 한번 위에서 말한 쿼리빌더들을 표로 정리하면 아래와 같습니다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;이름&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;JPA 연동여부&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;타입 안정성&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;동적 쿼리&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;추가 의존성&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Exposed&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;X&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Ktorm&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;X&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;JOOQ&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;△&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Spring Data Query Method&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;X&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;X&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;X&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;JPA Criteria API&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;△&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;△&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Kotlin JDSL&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;△&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;O&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h1 id=&quot;전환-방법&quot;&gt;전환 방법&lt;/h1&gt;

&lt;p&gt;Querydsl에서 Kotlin JDSL로의 전환을 본격적으로 이야기해 보겠습니다.&lt;/p&gt;

&lt;p&gt;여러 제품 개발 프로젝트가 동시에 진행되는 상황에서 Querydsl을 Kotlin JDSL로 전환해야 했습니다. 이를 위해 효율적이고 리스크를 최소화할 방법을 고민한 결과, 아래와 같은 순서로 작업을 진행했습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;전환 대상 목록화&lt;/strong&gt;: 먼저 전환할 대상을 명확히 정리하고 우선순위를 결정했습니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;베이스 코드 작성&lt;/strong&gt;: 전환할 대상을 위한 환경 설정 및 베이스 코드를 작성하고 팀원들이 손쉽게 적응할 수 있도록 예시 코드를 작성했습니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;작업 방식 전파&lt;/strong&gt;: 전환된 코드와 작업 방식에 대한 교육과 트레이닝을 진행하여 팀 전체에 새로운 작업 방식을 전파했습니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;병렬 작업&lt;/strong&gt;: 여러 프로젝트를 수행하면서 동시에 전환 작업을 병렬적으로 진행하여 효율성을 극대화했습니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;전환-대상-목록화&quot;&gt;전환 대상 목록화&lt;/h2&gt;

&lt;p&gt;저희 팀은 Querydsl을 Kotlin JDSL로 전환하는 프로젝트와 같이 규모가 있는 리펙터링 작업인 경우 아래와 같이 작업 목록을 생성하여 진행합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/transfer-jdsl/transfer-epic.png&quot; alt=&quot;transfer-epic&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이처럼 작업 목록을 생성하고 리팩토링을 진행하면 다음과 같은 장점을 얻을 수 있습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;규모 파악&lt;/strong&gt;: 리팩토링을 위한 작업을 미리 Task로 등록하면 해당 프로젝트의 규모와 작업에 들 시간을 예상할 수 있습니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;영향 범위 파악&lt;/strong&gt;: 생성된 작업 목록을 통해 프로젝트의 영향 범위를 손쉽게 파악할 수 있습니다. 물론 작업 명을 명확하게 작성하는 것이 중요합니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;진행률 파악&lt;/strong&gt;: 에픽을 통해 프로젝트 진행률을 손쉽게 파악할 수 있습니다. 소스 코드를 탐색하지 않고도 개발 상황을 파악할 수 있습니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;병렬 작업&lt;/strong&gt;: 필요한 작업을 미리 생성하고 목록화하면 여유가 생길 때 해당 작업을 누구나 진행할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이러한 장점을 활용하기 위해 Kotlin JDSL 전환 작업을 위한 에픽을 생성하고, Querydsl을 사용한 코드를 모두 검토하여 작업 목록을 미리 생성하는 단계부터 시작했습니다.&lt;/p&gt;

&lt;h2 id=&quot;베이스-코드-작성&quot;&gt;베이스 코드 작성&lt;/h2&gt;

&lt;p&gt;베이스 코드는 여러 명의 개발자가 가능한 한 일관된 방식으로 전환할 수 있도록 가이드라인을 제공하는 역할을 합니다. 개발자들이 공식 문서를 참고하여 각자가 Querydsl에서 Kotlin JDSL로의 전환을 수행하는 것은 어렵지 않겠지만, 코드의 일관성이 부족하면 전체적인 생산성에 영향을 미칠 수 있습니다.&lt;/p&gt;

&lt;p&gt;따라서 저희는 먼저 아래와 같이 베이스 코드를 작성하여 팀원들이 일관된 방식으로 코드 작업을 수행할 수 있도록 하였습니다. 이를 통해 병렬로 코드 작업을 진행할 수 있도록 지원하였습니다.&lt;/p&gt;

&lt;h3 id=&quot;의존성-추가&quot;&gt;의존성 추가&lt;/h3&gt;

&lt;p&gt;전환 작업을 모두가 병렬적으로 수행하기 위해서는 우선 의존성 설정과 환경 설정 등을 위한 기반 코드를 작성해야 합니다. 따라서 Kotlin JDSL을 사용하기 위한 의존성을 먼저 추가했습니다. 또한, 저희는 Spring을 사용하고 있기 때문에 &lt;a href=&quot;https://kotlin-jdsl.gitbook.io/docs/v/ko-1/jpql-with-kotlin-jdsl/spring-supports&quot; target=&quot;\_blank&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spring-data-jpa-support&lt;/code&gt;&lt;/a&gt;도 함께 추가했습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;implementation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;com.linecorp.kotlin-jdsl:jpql-dsl:$jdslVersion&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;implementation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;com.linecorp.kotlin-jdsl:jpql-render:$jdslVersion&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;implementation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;com.linecorp.kotlin-jdsl:spring-data-jpa-support:$jdslVersion&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;테스트-코드-설정&quot;&gt;테스트 코드 설정&lt;/h3&gt;

&lt;p&gt;다음으로 Repository 테스트에서 Kotlin JDSL을 이용한 쿼리 테스트를 수행하기 위해서 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KotlinJdslAutoConfiguration&lt;/code&gt;도 추가해 줍니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@DataJpaTest&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Import&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;KotlinJdslAutoConfiguration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RepositoryTestBase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;예시-전환-코드-작성&quot;&gt;예시 전환 코드 작성&lt;/h3&gt;

&lt;p&gt;저희는 프로젝트 초기부터 Kotlin JDSL을 적용하는 것이 아니라 Querydsl에서 Kotlin JDSL로의 전환을 진행했기 때문에 공식 문서에서 안내하는 방식대로 코드를 작성하는 것이 어려웠습니다. 이는 해당 방식으로는 Service 코드를 변경해야 했기 때문입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AdminRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpaRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KotlinJdslJpqlExecutor&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AdminService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;bookRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BookRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getByFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SearchFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bookRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whereAnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;like&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;%${filter.email}%&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filterNotNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그래서 저희는 Querydsl에서 활용한 Spring Data가 제공하는 &lt;a href=&quot;https://docs.spring.io/spring-data/jpa/reference/repositories/custom-implementations.html&quot; target=&quot;\_blank&quot;&gt;Custom Repository Implementations&lt;/a&gt;을 그대로 활용하기로 했습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AdminRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpaRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomAdminRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByEmail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomAdminRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SearchFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomAdminRepositoryImpl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomAdminRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SearchFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whereAnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                        &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;like&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;%${filter.email}%&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filterNotNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AdminService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;adminRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AdminRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getByFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SearchFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;adminRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findByFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;해당 방식은 앞서 언급한 방식보다 코드양이 많아 보일 수 있지만, 우리가 개발할 때 추상화된 인터페이스를 사용하는 가장 큰 이유라고 생각합니다.&lt;/p&gt;

&lt;p&gt;이 방식의 가장 큰 장점은 구현 세부 사항의 변경(쿼리빌더가 Querydsl에서 Kotlin JDSL로 변경)이 비즈니스 계층인 Service의 변경을 일으키지 않는다는 것입니다. 따라서 쿼리빌더의 변경으로 인한 비즈니스 로직의 변경에 대해 걱정할 필요가 없습니다. 이는 코드를 유연하게 변경할 수 있음을 보여줍니다.&lt;/p&gt;

&lt;p&gt;실제로 저희가 Querydsl에서 Kotlin JDSL로 전환하는 동안 CustomRepository 외에 Service 코드를 변경한 커밋은 하나도 없었습니다. 이에 따라 Repository에서 사용된 쿼리 빌더의 변경에만 집중할 수 있었고, 전환된 쿼리 빌더에서 실행된 쿼리가 기존과 동일하게 동작하는지 확인하는 데 집중할 수 있었습니다.&lt;/p&gt;

&lt;h2 id=&quot;작업-방식-전파&quot;&gt;작업 방식 전파&lt;/h2&gt;

&lt;p&gt;베이스 코드를 작성한 후, 팀원들에게 전환 스타일에 대한 리뷰를 받고 작업 방식을 전파하면서 적용한 코드를 함께 리뷰하는 시간을 가졌습니다. 각자가 이해하고 있는 라이브러리에 대한 지식과 코딩 스타일이 다를 수 있기 때문에, 가능한 한 맞추되 어느 정도 이견에 대한 합의가 필요했습니다.&lt;/p&gt;

&lt;h2 id=&quot;병렬-작업&quot;&gt;병렬 작업&lt;/h2&gt;

&lt;p&gt;작업 방식에 대한 기본적인 합의가 이루어지면 각자가 본격적으로 전환 작업을 수행할 수 있습니다. 이 작업은 제품 개발 프로젝트보다는 우선순위가 낮았기 때문에, 특별한 일정 없이 작업시간이 여유로운 개발자가 자율적으로 작업을 할당받아 진행하는 칸반 방식을 채택했습니다.&lt;/p&gt;

&lt;h1 id=&quot;이슈&quot;&gt;이슈&lt;/h1&gt;

&lt;p&gt;아래는 저희가 Kotlin JDSL로 전환하면서 직면한 몇 가지 이슈를 소개해 드리려고 합니다.&lt;/p&gt;

&lt;p&gt;현재는 이미 해결된 이슈들도 있고 Kotlin JDSL의 이슈가 아니라 Spring Data JPA의 이슈인 것들도 있었습니다. Querydsl과 같이 수많은 사용자층이 있는 라이브러리가 아니기 때문에 구글링이나 GPT를 통한 이슈 해결은 어려워 주로 Github Issue를 통해 이슈를 문의하고 답변을 받아 이슈를 해결했습니다. 다행인 점은 Kotlin JDSL을 주로 관리하는 라인 측 개발자분께서 이슈를 남기면 대부분 하루 안에 친절한 설명과 함께 답변을 주셔서 큰 문제 없이 전환 작업을 완료할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;Kotlin JDSL을 적용 검토 중인 분들이라면 아래 소개하는 이슈들을 참고하시면 좋을 것 같습니다.&lt;/p&gt;

&lt;h3 id=&quot;spring-data-custom-repository-implementations-이슈&quot;&gt;Spring Data Custom Repository Implementations 이슈&lt;/h3&gt;

&lt;p&gt;이슈 링크: &lt;a href=&quot;https://github.com/line/kotlin-jdsl/issues/668&quot; target=&quot;\_blank&quot;&gt;Custom Repository Implementations is not available in spring-data-jpa-support&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;최초 도입 당시 버전인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3.3.1&lt;/code&gt; 버전에서 spring-data-jpa-support의존성을 추가한 후 Spring Data JPA의 Custom Repository Implementations를 사용하면 아래와 같이 오류가 발생하는 이슈가 있었습니다. 해당 이슈는 이슈를 제기한 당일 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3.3.2&lt;/code&gt; 버전에서 핫픽스로 빠르게 조치되어 해결되었습니다.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;java.lang.IllegalStateException: Failed to load ApplicationContext for [WebMergedContextConfiguration@662061e3 testClass = com.example.demo.ApplicationTests, locations = [], classes = [com.example.demo.Application], contextInitializerClasses = [], activeProfiles = [], propertySourceDescriptors = [], propertySourceProperties = [&quot;org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true&quot;], contextCustomizers = [org.springframework.boot.test.autoconfigure.actuate.observability.ObservabilityContextCustomizerFactory$DisableObservabilityContextCustomizer@1f, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizer@18a136ac, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@421bba99, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@31bcf236, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@253d9f73, org.springframework.boot.test.context.SpringBootTestAnnotation@3a387497], resourceBasePath = &quot;src/main/webapp&quot;, contextLoader = org.springframework.boot.test.context.SpringBootContextLoader, parent = null]
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:180)
	at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:130)
...
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name &apos;boardRepository&apos; defined in com.example.demo.BoardRepository defined in @EnableJpaRepositories declared on JpaConfig: Could not create query for public abstract java.util.List com.example.demo.CustomBoardRepository.findAllByFilter(com.example.demo.BoardFilter); Reason: Failed to create query for method public abstract java.util.List com.example.demo.CustomBoardRepository.findAllByFilter(com.example.demo.BoardFilter); No property &apos;filter&apos; found for type &apos;Board&apos;
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1786)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
...
Caused by: org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract java.util.List com.example.demo.CustomBoardRepository.findAllByFilter(com.example.demo.BoardFilter); Reason: Failed to create query for method public abstract java.util.List com.example.demo.CustomBoardRepository.findAllByFilter(com.example.demo.BoardFilter); No property &apos;filter&apos; found for type &apos;Board&apos;
	at org.springframework.data.repository.query.QueryCreationException.create(QueryCreationException.java:101)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.lookupQuery(QueryExecutorMethodInterceptor.java:115)
...
Caused by: java.lang.IllegalArgumentException: Failed to create query for method public abstract java.util.List com.example.demo.CustomBoardRepository.findAllByFilter(com.example.demo.BoardFilter); No property &apos;filter&apos; found for type &apos;Board&apos;
	at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.&amp;lt;init&amp;gt;(PartTreeJpaQuery.java:106)
	at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$CreateQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:124)
...
Caused by: org.springframework.data.mapping.PropertyReferenceException: No property &apos;filter&apos; found for type &apos;Board&apos;
	at org.springframework.data.mapping.PropertyPath.&amp;lt;init&amp;gt;(PropertyPath.java:90)
	at org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:443)
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;데이터베이스-잠금-설정-문의&quot;&gt;데이터베이스 잠금 설정 문의&lt;/h3&gt;

&lt;p&gt;이슈 링크: &lt;a href=&quot;https://github.com/line/kotlin-jdsl/issues/677&quot; target=&quot;\_blank&quot;&gt;Inquire how to use CRUD method metadata&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;아래와 같이 Querydsl로 작성된 기존의 코드 중에 동시성 이슈를 방지하고자 데이터베이스의 잠금 설정을 적용한 함수가 있었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findAllActive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;orderableVendorId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;storeIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jpaQueryFactory&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;selectFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;bill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendorId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;and&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;`in`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;and&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BillState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ACTIVE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setLockMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LockModeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PESSIMISTIC_WRITE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setHint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;org.hibernate.cacheable&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setHint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;jakarta.persistence.lock.timeout&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Kotlin JDSL의 공식 문서에는 잠금과 관련한 내용이 언급되어 있지 않아 어떻게 사용하면 좋을지 문의를 남겼었는데요. 아래와 같이 EntityManager를 이용하여 적용하는 방법을 제안받았습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomBillRepositoryImpl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;entityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;jpqlRenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomBillRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findAllActive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;orderableVendorId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;storeIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;query&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;jpql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whereAnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendorId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;customIn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BillState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ACTIVE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entityManager&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jpqlRenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;apply&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; 
                &lt;span class=&quot;n&quot;&gt;lockMode&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LockModeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PESSIMISTIC_WRITE&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;setHint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HINT_CACHEABLE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;setHint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HINT_SPEC_LOCK_TIMEOUT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultList&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;manytoone-fetch-join-이슈&quot;&gt;ManyToOne fetch join 이슈&lt;/h3&gt;

&lt;p&gt;이슈 링크: &lt;a href=&quot;https://github.com/line/kotlin-jdsl/issues/682&quot; target=&quot;\_blank&quot;&gt;Fetch join issue in pagination&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3.3.2&lt;/code&gt; 버전에서 아래와 같이 1:N 관계의 Entity가 존재할 때 페이징 쿼리에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ManyToOne&lt;/code&gt; 연관관계를 조회할 때 오류가 발생하는 이슈가 있었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Actor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Actor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpaRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomBoardRepository&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomBoardRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByActorName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actorName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomBoardRepositoryImpl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomBoardRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByActorName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actorName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;fetchJoin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Actor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actorName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.query.SemanticException: Query specified join fetching, but the owner of the fetched association was not present in the select list [SqmSingularJoin(com.example.demo.Board(Board).actor(Actor) : actor)]
	at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:371)
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:246)
...
Caused by: java.lang.IllegalArgumentException: org.hibernate.query.SemanticException: Query specified join fetching, but the owner of the fetched association was not present in the select list [SqmSingularJoin(com.example.demo.Board(Board).actor(Actor) : actor)]
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:143)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:167)
...
Caused by: org.hibernate.query.SemanticException: Query specified join fetching, but the owner of the fetched association was not present in the select list [SqmSingularJoin(com.example.demo.Board(Board).actor(Actor) : actor)]
	at org.hibernate.query.sqm.tree.select.SqmQuerySpec.assertFetchOwner(SqmQuerySpec.java:573)
	at org.hibernate.query.sqm.tree.select.SqmQuerySpec.validateFetchOwners(SqmQuerySpec.java:548)
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;해당 이슈의 원인은 Kotlin JDSL에서 사용되는 Legacy Query Parser의 페이지네이션과 Fetch Join 처리 메커니즘으로 인한 것으로 확인되었습니다. 이 이슈는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3.4.0&lt;/code&gt; 버전에서 해결되었습니다.&lt;/p&gt;

&lt;h3 id=&quot;selectdistinctnew-이슈&quot;&gt;selectDistinctNew 이슈&lt;/h3&gt;

&lt;p&gt;이슈 링크: &lt;a href=&quot;https://github.com/line/kotlin-jdsl/issues/694&quot; target=&quot;\_blank&quot;&gt;When use selectDistinctNew function in findPage, there are some paging error&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;해당 이슈는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;selectDistinctNew&lt;/code&gt;를 사용하여 페이징 쿼리를 호출하면 일부 페이지에서 오류가 발생하는 이슈인데요.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardStats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boardId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;viewCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpaRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KotlinJdslJpqlExecutor&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_findPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;pageable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PageRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;selectDistinctNew&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&amp;gt;&amp;gt;(&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BoardStats&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;viewCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;leftJoin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BoardStats&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BoardStats&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boardId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;nf&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;totalElements&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.query.SyntaxException: At 1:26 and token &apos;kotlin&apos;, no viable alternative at input &apos;SELECT count(DISTINCT NEW *kotlin.Pair(Board, BoardStats.viewCount)) FROM Board AS Board LEFT JOIN BoardStats AS BoardStats ON Board.id = BoardStats.boardId&apos; [SELECT count(DISTINCT NEW kotlin.Pair(Board, BoardStats.viewCount)) FROM Board AS Board LEFT JOIN BoardStats AS BoardStats ON Board.id = BoardStats.boardId]
	at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:371)
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:246)
...
Caused by: java.lang.IllegalArgumentException: org.hibernate.query.SyntaxException: At 1:26 and token &apos;kotlin&apos;, no viable alternative at input &apos;SELECT count(DISTINCT NEW *kotlin.Pair(Board, BoardStats.viewCount)) FROM Board AS Board LEFT JOIN BoardStats AS BoardStats ON Board.id = BoardStats.boardId&apos; [SELECT count(DISTINCT NEW kotlin.Pair(Board, BoardStats.viewCount)) FROM Board AS Board LEFT JOIN BoardStats AS BoardStats ON Board.id = BoardStats.boardId]
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:143)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:167)
...
Caused by: org.hibernate.query.SyntaxException: At 1:26 and token &apos;kotlin&apos;, no viable alternative at input &apos;SELECT count(DISTINCT NEW *kotlin.Pair(Board, BoardStats.viewCount)) FROM Board AS Board LEFT JOIN BoardStats AS BoardStats ON Board.id = BoardStats.boardId&apos; [SELECT count(DISTINCT NEW kotlin.Pair(Board, BoardStats.viewCount)) FROM Board AS Board LEFT JOIN BoardStats AS BoardStats ON Board.id = BoardStats.boardId]
	at org.hibernate.query.hql.internal.StandardHqlTranslator$1.syntaxError(StandardHqlTranslator.java:108)
	at org.antlr.v4.runtime.ProxyErrorListener.syntaxError(ProxyErrorListener.java:41)
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;해당 이슈에 대해 Kotlin JDSL 측에 문의해 보았으나, JDSL은 해당 기능을 사용할 때 Spring Data JPA API를 활용하기 때문에 Spring Data JPA 측에 문의하는 것이 좋을 것으로 답변을 받았습니다.&lt;/p&gt;

&lt;p&gt;따라서 Spring Data JPA 측에 이슈를 문의하기 전에, 혹시나 이전에 비슷한 이슈가 있었는지 확인하기 위해 검색을 진행했습니다. 그 결과 아래 링크에서 관련된 이슈가 이미 제기되어 있는 것을 발견하였습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-data-jpa/issues/3098&quot; target=&quot;\_blank&quot;&gt;Cannot compile SELECT with DISTINCT with pagination&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이슈를 요약하면, 기존에 동작하던 Count Query가 잘못된 방식으로 동작하고 있었으며, Distinct와 Count 쿼리를 함께 사용하면 의도치 않게 동작할 수 있으니 다른 방법으로 사용할 것을 권장한다는 내용입니다.&lt;/p&gt;

&lt;p&gt;따라서 저희도 쿼리를 다르게 작성하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;selectDistinctNew&lt;/code&gt;를 사용하여 페이지네이션을 하지 않는 방향으로 이 문제를 해결했습니다.&lt;/p&gt;

&lt;h3 id=&quot;limit-이슈&quot;&gt;limit 이슈&lt;/h3&gt;

&lt;p&gt;이슈 링크: &lt;a href=&quot;https://github.com/line/kotlin-jdsl/issues/695&quot; target=&quot;\_blank&quot;&gt;An issue where an error occurs when using limit with a custom serializer in JDSL&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;기존에 Querydsl을 사용하면서 특정 개수의 데이터만 조회하고자 할 때 주로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;limit&lt;/code&gt; 함수를 활용했습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findRecentOrderSheetsByStoreId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;orderBy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updatedAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;desc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toLong&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그래서 Kotlin JDSL로 전환하는 과정에서도 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;limit&lt;/code&gt; 함수를 사용해 보려 했으나, 아쉽게도 해당 함수가 존재하지 않았습니다. 따라서 아래와 같이 Custom DSL을 만들어 사용하게 되었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlLimit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;selectQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;QueryPart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;returnType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SelectQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;object&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlLimitSerializer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlSerializer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JpqlLimit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;handledType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JpqlLimit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlLimit&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;serialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;part&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlLimit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlWriter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;delegate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JpqlRenderSerializer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;serialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;part&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;selectQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot; LIMIT ${part.limit}&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomJpql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Jpql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;companion&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;object&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Constructor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlDsl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Constructor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CustomJpql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;newInstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomJpql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomJpql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;inline&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;reified&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;JpqlQueryable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SelectQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlLimit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlLimit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toLong&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findRecentOrderSheetsByStoreId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;orderBy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updatedAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;desc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filterNotNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;하지만 위와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orderBy&lt;/code&gt; 절을 함께 사용하는 경우 문제가 되지 않았지만, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orderBy&lt;/code&gt; 절 없이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;limit&lt;/code&gt; 함수를 사용하면 아래와 같이 오류가 발생하는 이슈가 있었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findRecentOrderSheetsByStoreId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filterNotNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;org.hibernate.query.SyntaxException: At 1:33 and token &apos;LIMIT&apos;, mismatched input &apos;LIMIT&apos;, expecting one of the following tokens: &amp;lt;EOF&amp;gt;, &apos;,&apos;, CROSS, FULL, GROUP, INNER, JOIN, LEFT, ORDER, OUTER, RIGHT, WHERE [SELECT Actor FROM Actor AS Actor LIMIT 1]
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.query.SyntaxException: At 1:33 and token &apos;LIMIT&apos;, mismatched input &apos;LIMIT&apos;, expecting one of the following tokens: &amp;lt;EOF&amp;gt;, &apos;,&apos;, CROSS, FULL, GROUP, INNER, JOIN, LEFT, ORDER, OUTER, RIGHT, WHERE [SELECT Actor FROM Actor AS Actor LIMIT 1]
	at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:371)
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:246)
...
Caused by: java.lang.IllegalArgumentException: org.hibernate.query.SyntaxException: At 1:33 and token &apos;LIMIT&apos;, mismatched input &apos;LIMIT&apos;, expecting one of the following tokens: &amp;lt;EOF&amp;gt;, &apos;,&apos;, CROSS, FULL, GROUP, INNER, JOIN, LEFT, ORDER, OUTER, RIGHT, WHERE [SELECT Actor FROM Actor AS Actor LIMIT 1]
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:143)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:167)
...
Caused by: org.hibernate.query.SyntaxException: At 1:33 and token &apos;LIMIT&apos;, mismatched input &apos;LIMIT&apos;, expecting one of the following tokens: &amp;lt;EOF&amp;gt;, &apos;,&apos;, CROSS, FULL, GROUP, INNER, JOIN, LEFT, ORDER, OUTER, RIGHT, WHERE [SELECT Actor FROM Actor AS Actor LIMIT 1]
	at org.hibernate.query.hql.internal.StandardHqlTranslator$1.syntaxError(StandardHqlTranslator.java:108)
	at org.antlr.v4.runtime.ProxyErrorListener.syntaxError(ProxyErrorListener.java:41)
...
org.hibernate.query.SyntaxException: At 1:33 and token &apos;LIMIT&apos;, mismatched input &apos;LIMIT&apos;, expecting one of the following tokens: &amp;lt;EOF&amp;gt;, &apos;,&apos;, CROSS, FULL, GROUP, INNER, JOIN, LEFT, ORDER, OUTER, RIGHT, WHERE [SELECT Actor FROM Actor AS Actor LIMIT 1]
java.lang.IllegalArgumentException: org.hibernate.query.SyntaxException: At 1:33 and token &apos;LIMIT&apos;, mismatched input &apos;LIMIT&apos;, expecting one of the following tokens: &amp;lt;EOF&amp;gt;, &apos;,&apos;, CROSS, FULL, GROUP, INNER, JOIN, LEFT, ORDER, OUTER, RIGHT, WHERE [SELECT Actor FROM Actor AS Actor LIMIT 1]
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:143)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:167)
...
Caused by: org.hibernate.query.SyntaxException: At 1:33 and token &apos;LIMIT&apos;, mismatched input &apos;LIMIT&apos;, expecting one of the following tokens: &amp;lt;EOF&amp;gt;, &apos;,&apos;, CROSS, FULL, GROUP, INNER, JOIN, LEFT, ORDER, OUTER, RIGHT, WHERE [SELECT Actor FROM Actor AS Actor LIMIT 1]
	at app//org.hibernate.query.hql.internal.StandardHqlTranslator$1.syntaxError(StandardHqlTranslator.java:108)
	at app//org.antlr.v4.runtime.ProxyErrorListener.syntaxError(ProxyErrorListener.java:41)
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;원인은 Hibernate의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;limit&lt;/code&gt; 쿼리가 필수적으로 orderBy 절을 요구하기 때문에 발생한 것입니다. 따라서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;limit&lt;/code&gt; 을 사용하려면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orderBy&lt;/code&gt; 절을 함께 사용하거나, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;limit&lt;/code&gt; 을 사용하지 않고 페이지네이션을 하는 방법, 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EntityManager&lt;/code&gt;의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setMaxResults&lt;/code&gt;를 사용하는 방법 중 하나를 선택하여 이 문제를 해결해야 했습니다.&lt;/p&gt;

&lt;p&gt;현재는 저희가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;limit&lt;/code&gt;을 적용한 함수에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orderBy&lt;/code&gt; 절이 함께 있어서 문제가 되지 않았지만, 무조건 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;limit&lt;/code&gt;을 사용할 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orderBy&lt;/code&gt; 절을 함께 사용하는 것은 DB 성능을 불필요하게 낭비하는 것이라 판단되어, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EntityManager&lt;/code&gt;의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setMaxResults&lt;/code&gt;를 사용하는 방법을 고려해 보고 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findRecentOrderSheetsByStoreId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;query&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;jpql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
              &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
              &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entityManager&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jpqlRenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;apply&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setMaxResults&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultList&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;활용팁&quot;&gt;활용팁&lt;/h1&gt;

&lt;p&gt;여기서는 Kotlin JDSL에서 공식적으로 지원하지 않지만, 저희 팀에서 좀 더 효율적으로 Kotlin JDSL을 사용하기 위한 여러 가지 팁들을 소개하고자 합니다.&lt;/p&gt;

&lt;h2 id=&quot;helper-클래스&quot;&gt;Helper 클래스&lt;/h2&gt;

&lt;p&gt;우선 저희는 여러 상황에서 발생하는 코드 중복을 줄이고 Repository의 의존성을 줄이고자 Helper 클래스를 두고 아래와 같이 Repository가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KotlinJdslJpqlExecutor&lt;/code&gt;가 아닌 Helper 클래스인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JdslExecutorSupport&lt;/code&gt;를 의존하도록 기존 코드를 리펙토링하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomOrderSheetRepositoryImpl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;jdslExecutorSupport&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JdslExecutorSupport&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomOrderSheetRepository&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그럼 어떤 상황들로 인해 Helper 클래스가 필요하게 되었는지 알아보겠습니다.&lt;/p&gt;

&lt;h3 id=&quot;데이터베이스-잠금&quot;&gt;데이터베이스 잠금&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KotlinJdslJpqlExecutor&lt;/code&gt;를 사용하면 우선 대부분의 쿼리를 작성하는 데에는 큰 문제가 없었습니다. 하지만 위에서 말씀드린 이슈 중에 데이터베이스의 잠금 기능을 사용해야 하는 경우 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EntityManager&lt;/code&gt;를 사용할 수밖에 없다는 것을 보셨을 텐데요.&lt;/p&gt;

&lt;p&gt;이러면 아래와 같이 Custom Repository에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EntityManager&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RenderContext&lt;/code&gt;를 추가적으로 의존성을 추가해 주어 사용해야 할 불편함이 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomBillRepositoryImpl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;entityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;jpqlRenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomBillRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByIdUsingPessimisticLock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;query&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;jpql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eneity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entityManager&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jpqlRenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;apply&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;lockMode&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LockModeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PESSIMISTIC_WRITE&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;setHint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HINT_CACHEABLE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;setHint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HINT_SPEC_LOCK_TIMEOUT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultList&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;firstOrNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ofNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Repository 입장에서는 쿼리를 실행하기 위한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KotlinJdslJpqlExecutor&lt;/code&gt;만 의존하는 것이 이상적일 것으로 보입니다. 반면 보다 세부적인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EntityManager&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RenderContext&lt;/code&gt;를 알아야 한다는 것은 의존성 구조를 복잡하게 만든다고 생각됩니다. 또한, 잠금을 사용하는 함수의 존재 여부에 따라 개발자가 의존성을 추가해야 하는 부분도 좋지 않은 영향을 미친다고 생각됩니다.&lt;/p&gt;

&lt;p&gt;LockMode를 설정하는 코드도 살펴보면, 이를 공통 함수로 추출한다면 중복된 LockMode와 Hint 설정을 줄일 수 있습니다. 이로써 개발자가 실수로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setHint(HINT_SPEC_LOCK_TIMEOUT, 10000)&lt;/code&gt;를 빼먹는 등의 오류를 줄일 수 있습니다.&lt;/p&gt;

&lt;p&gt;따라서 저희는 Helper 클래스에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findWithPessimisticLock&lt;/code&gt;를 만들어서 위와 같은 문제를 해결하려고 하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JdslExecutorSupport&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;entityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;jpqlRenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findWithPessimisticLock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlDsl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlQueryable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SelectQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entityManager&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;jpql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CustomJpql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jpqlRenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;apply&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;lockMode&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LockModeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PESSIMISTIC_WRITE&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;setHint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HINT_CACHEABLE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;setHint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HINT_SPEC_LOCK_TIMEOUT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultList&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;firstOrNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ofNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomOrderSheetRepositoryImpl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;jdslExecutorSupport&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JdslExecutorSupport&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomOrderSheetRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByIdUsingPessimisticLock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jdslExecutorSupport&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findWithPessimisticLock&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;selectFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CustomOrderSheetRepositoryImpl&lt;/code&gt;를 보시면 확실히 의존하는 클래스도 줄어들었고 함수도 단순하게 변경된 것을 볼 수 있습니다. 조금 더 응용하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findAllWithSkipLock&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findAllWithOptimisticLock&lt;/code&gt; 등과 같은 함수들도 만들 수 있겠죠.&lt;/p&gt;

&lt;h3 id=&quot;nullable-collection-처리&quot;&gt;Nullable Collection 처리&lt;/h3&gt;

&lt;p&gt;기본적으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KotlinJdslJpqlExecutor&lt;/code&gt;의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findAll&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findPage&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findSlice&lt;/code&gt;는 nullable 한 타입을 가진 Collection을 반환합니다. 이에 따라 매번 nullable 한 요소들을 non-nullable하도록 필터링해 주거나 nullable 한 Collection을 반환해야 하는 불편함이 있었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SearchFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whereAnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;like&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;%${filter.email}%&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filterNotNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Kotlin JDSL의 과거 &lt;a href=&quot;https://github.com/line/kotlin-jdsl/issues/608&quot; target=&quot;\_blank&quot;&gt;이슈&lt;/a&gt;를 보면 nullable 한 Collection을 반환하는 이유를 설명하고 있는데요.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/transfer-jdsl/kotlin-jdsl-null-type-issue.png&quot; alt=&quot;kotlin-jdsl-null-type-issue&quot; /&gt;&lt;/p&gt;

&lt;p&gt;저희는 nullable 한 항목을 반환하는 경우가 거의 없으므로 Helper 클래스에 nullable 한 항목을 필터링하여 non-nullable 한 Collection을 기본적으로 반환하는 함수를 새롭게 만들어 사용했습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JdslExecutorSupport&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;entityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;jpqlRenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlDsl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlQueryable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SelectQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CustomJpql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filterNotNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 함수를 통해 이제 더 이상 아래와 같이 매번 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filterNotNull&lt;/code&gt;함수를 적어주지 않아도 되게 개선되었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SearchFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jdslExecutorSupport&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whereAnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Admin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;like&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;%${filter.email}%&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;단일조회&quot;&gt;단일조회&lt;/h3&gt;

&lt;p&gt;Kotlin JDSL에서 제공하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KotlinJdslJpqlExecutor&lt;/code&gt;에서는 단일 조회함수가 별도로 존재하지 않습니다. 그래서 만약 단일 데이터를 조회하려면 아래와 같이 Kotlin에서 제공하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;List#firstOrNull&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;List#first&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;List#find&lt;/code&gt; 등의 함수를 사용해야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findActive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;orderableVendorId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whereAnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendorId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BillState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ACTIVE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;firstOrNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ofNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;매번 위와 같은 코드를 작성하는 것은 번거롭기 때문에 저희는 Helper 클래스를 통해 단일 조회 함수들을 만들어서 사용하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JdslExecutorSupport&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KotlinJdslJpqlExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;entityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;jpqlRenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Jpql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlQueryable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SelectQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getOrNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ofNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getOrNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Jpql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlQueryable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SelectQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kotlinJdslJpqlExecutor&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;firstOrNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findActive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;orderableVendorId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jdslExecutorSupport&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;selectFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whereAnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendorId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BillState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ACTIVE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;추가로, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JdslExecutorSupport#find&lt;/code&gt;나 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JdslExecutorSupport#getOrNull&lt;/code&gt; 함수를 사용하여 단일 데이터를 조회할 때 의도치 않게 1개 이상의 쿼리를 작성하여 원치 않는 조회 비용이 발생할 수 있는 우려가 있었습니다.&lt;/p&gt;

&lt;p&gt;이 문제를 회피하기 위해 쿼리에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;limit&lt;/code&gt; 구문을 추가하는 방법이 있지만, 앞서 언급한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;limit&lt;/code&gt; 쿼리 이슈로 인해 불필요하게 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orderBy&lt;/code&gt; 절을 사용해야 하거나, 매번 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;limit&lt;/code&gt; 구문을 추가하는 번거로움과 코드 중복을 유발한다는 문제가 있습니다. 이에 따라 저희는 단일 조회 시 1개의 row만 반환하도록 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setMaxResults&lt;/code&gt;를 활용하여 최적화를 시도했습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getOrNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Jpql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlQueryable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SelectQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entityManager&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jpqlRenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;apply&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setMaxResults&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resultList&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;firstOrNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;custom-dsl-활용&quot;&gt;Custom DSL 활용&lt;/h2&gt;

&lt;p&gt;Kotlin JDSL은 공식적으로 지원하는 DSL 외에도 개발자가 자신만의 DSL을 만들어 사용할 수 있도록 Custom DSL을 제공합니다. 저희도 아래와 같이 Custom DSL을 적용하여 사용하고 있는데요. 적용한 함수들이 어떤 목적으로 사용되었는지 소개해 드리도록 하겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomJpql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Jpql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;falsePredicate&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;booleanLiteral&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;booleanLiteral&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;truePredicate&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;booleanLiteral&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;booleanLiteral&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;companion&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;object&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Constructor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlDsl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Constructor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CustomJpql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;newInstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomJpql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomJpql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Expressionable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;arrayContains&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Predicate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlArrayContain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toExpression&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Expressionable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;jsonbExistsAny&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Predicate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlJsonbExistsAny&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toExpression&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toTypedArray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()))&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;S&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Expressionable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;`in`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;compareValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Collection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;S&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Predicate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;compareValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;isEmpty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;falsePredicate&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Predicates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;`in`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toExpression&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;compareValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Expressions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;inline&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;reified&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;selectFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SelectQueryWhereStep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;entity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;데이터베이스-함수-표현&quot;&gt;데이터베이스 함수 표현&lt;/h3&gt;

&lt;p&gt;저희는 PostgreSQL을 사용하고 있는데요. PostgreSQL의 함수들(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ARRAY_POSITION({0}, {1}) is not null&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FUNCTION(&apos;jsonb_exists_any&apos;, {0}, {1}) = true&lt;/code&gt;)을 DSL로 표현하여 사용하기 위해서 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arrayContains&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsonbExistsAny&lt;/code&gt; 함수를 만들어 활용하고 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlArrayContain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;(&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Expression&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;,&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;compareValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Predicate&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;object&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlArrayContainsSerializer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlSerializer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JpqlArrayContain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;handledType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JpqlArrayContain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlArrayContain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;serialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;part&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlArrayContain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlWriter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;delegate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JpqlRenderSerializer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// ARRAY_POSITION({0}, {1}) is not null&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ARRAY_POSITION(&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;serialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;part&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;, &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&apos;${part.compareValues.name}&apos;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;) IS NOT NULL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlJsonbExistsAny&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Expression&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;,&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;compareValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Expression&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Predicate&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;object&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlJsonbExistsAnySerializer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlSerializer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JpqlJsonbExistsAny&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;handledType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JpqlJsonbExistsAny&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlJsonbExistsAny&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;serialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;part&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlJsonbExistsAny&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlWriter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RenderContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;delegate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JpqlRenderSerializer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// FUNCTION(&apos;jsonb_exists_any&apos;, {0}, {1}) = true&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;FUNCTION(&apos;jsonb_exists_any&apos;, &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;serialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;part&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;, &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;serialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;part&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;compareValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;) = true&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Expressionable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;arrayContains&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Predicate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlArrayContain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toExpression&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Expressionable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;jsonbExistsAny&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Predicate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpqlJsonbExistsAny&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toExpression&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toTypedArray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()))&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 Custom DSL을 만들면 아래처럼 단순하게 복잡한 쿼리를 실행시킬 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findRecommendations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MajorTradeStoreCategory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;towns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Town&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jdslExecutorSupport&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;selectFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whereAnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;metaInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MetaOrderableVendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;advertisementEnabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;metaInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MetaOrderableVendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;productInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendorProductInfo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;majorTradeStoreCategories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;arrayContains&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;metaInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MetaOrderableVendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deliveryInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendorDeliveryInfo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deliveryTowns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;jsonbExistsAny&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;towns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;orderable_vendor&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;o1_0&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;join&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;meta_orderable_vendor&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;m1_0&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;m1_0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;o1_0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;meta_orderable_vendor_id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m1_0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;advertisement_enabled&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;array_position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m1_0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;major_trade_store_categories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;KOREAN&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;jsonb_exists_any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m1_0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;delivery_towns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{&quot;GANGNAM_GU&quot;}&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;custom-in&quot;&gt;Custom In&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in&lt;/code&gt; 절을 사용할 때, Querydsl은 매개변수가 빈 배열이어도 false로 처리하고 오류를 반환하지 않지만, Kotlin JDSL의 경우 오류를 반환하는 이슈가 있었습니다. 어느 구현이 맞다고 말하기 어렵다고 판단하여 별도로 이슈를 제기하지는 않았는데요.&lt;/p&gt;

&lt;p&gt;문제는 클라이언트에서 빈 배열을 보내는 형태로 로직이 구현되어 있다는 것이 이슈였습니다. 그래서 저희는 아래와 같이 3가지 방법을 고민하였습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;빈 배열이 들어올 때 예외를 발생시킵니다.&lt;/strong&gt; 명시적이라 잘못된 매개변수를 넘기는 경우에 대한 인지가 가능하다는 장점이 있지만 프론트에서 매번 예외 처리해야 하므로 번거로움이 있습니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;빈 배열이 들어오면 매개변수를 넘기지 않은 것과 같이 모든 데이터를 반환합니다.&lt;/strong&gt; 원치 않는 모든 데이터를 반환하므로 성능적인 이슈가 있을 수 있습니다. 빈 배열을 넘겼는데 모든 값이 넘어온다는 가정이 일반적인 동작 방식인가 고민해 보았을 때 일반적인 방식은 아니라고 판단했습니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;빈 배열이 들어오면 false로 처리합니다.&lt;/strong&gt; 즉, 일치하는 조건이 없으므로 데이터를 반환하지 않습니다. Querydsl은 이렇게 동작하도록 처리되어 있습니다. 일리가 있는 동작 방식이나 SQL 문법상으로는 알맞지 않은 것 같습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;결국 저희는 기존 동작 방식을 변경하지 않는다는 기본 전제조건을 지키기 위해서 3번으로 구현하기로 하였습니다.&lt;/p&gt;

&lt;p&gt;한편, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in&lt;/code&gt; 함수명에 대한 고민도 있었는데요. 왜냐하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CustomJpql&lt;/code&gt;의 부모 클래스인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Jpql&lt;/code&gt;에서 이미 인스턴스 확장함수로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in&lt;/code&gt;을 선언해 두었기 때문입니다. 그래서 처음에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;customIn&lt;/code&gt;이라는 함수명을 사용했으나 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in&lt;/code&gt; 절을 사용할 때 자칫 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;customIn&lt;/code&gt;을 사용하는 것을 잊어버리고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in&lt;/code&gt;함수를 사용할까 우려되었습니다. 거기다 확장함수라 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Jpql&lt;/code&gt;의 함수를 오버라이딩할 수 없었는데요. 다행히 &lt;a href=&quot;https://kotlinlang.org/docs/extensions.html#declaring-extensions-as-members&quot; target=&quot;\_blank&quot;&gt;Kotlin 확장함수 우선순위 정책&lt;/a&gt;에 따르면 동일한 함수명이 동일한 경우 맴버 함수가 우선된다는 점을 이용하여 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CustomJpql&lt;/code&gt;의 인스턴스 확장함수로 정의하였습니다.&lt;/p&gt;

&lt;p&gt;이로써 빈 배열이 들어오면 false로 처리되도록 조치되었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomJpql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Jpql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;falsePredicate&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;booleanLiteral&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;booleanLiteral&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;S&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Expressionable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;`in`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;compareValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Collection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;S&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Predicate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;compareValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;isEmpty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;falsePredicate&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Predicates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;`in`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toExpression&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;compareValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Expressions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findAllActive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;orderableVendorId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;storeIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jdslExecutorSupport&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAllWithPessimisticLock&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;selectFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whereAnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendorId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;`in`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BillState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ACTIVE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;빈 매장 ID 목록이 주어지면 빈 목록을 반환한다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;billRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAllActive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;orderableVendorId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;storeIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;shouldBeEmpty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;bill&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b1_0&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;b1_0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderable_vendor_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;018f39dd-e8f2-ee4f-7041-1d55cdea8fd6&apos;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b1_0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;ACTIVE&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;no&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;key&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;update&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;selectfrom&quot;&gt;selectFrom&lt;/h3&gt;

&lt;p&gt;솔직히 해당 팁은 너무 단순해서 소개할지 말지 고민하다가, 그래도 기왕에 구현한 것을 소개해 드리자고 생각해서 소개하고자 합니다.&lt;/p&gt;

&lt;p&gt;Kotlin JDSL 쿼리를 작성하다보면 상당수의 쿼리가 아래와 같이 Entity를 반환하는 경우가 많습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findAllActive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;pageRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jdslExecutorSupport&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pageRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whereAnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BillState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ACTIVE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;orderBy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;asc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그러다 보니 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;select(entity(Entity::Class)).from(entity(Entity::class))&lt;/code&gt; 코드가 반복되는 쿼리들이 보였습니다. 그래서 해당 코드의 중복도 줄이고 단순하게 작성할 수 있도록 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;selectFrom&lt;/code&gt;이라는 함수를 추가하여 해당 코드들에 적용하였습니다. (해당 함수는 Querydsl의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JPAQueryFactory#selectFrom&lt;/code&gt;함수를 참고하였습니다)&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;inline&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;reified&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;selectFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SelectQueryWhereStep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;entity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findAllActive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;pageRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jdslExecutorSupport&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pageRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;selectFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whereAnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BillState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ACTIVE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;orderBy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bill&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;asc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;manytomany-이슈&quot;&gt;ManyToMany 이슈&lt;/h2&gt;

&lt;p&gt;저희가 개발 중인 서비스의 수많은 Entity 중 N:M 관계를 맺은 Entity가 몇 개 있습니다. N:M 관계를 맺은 Entity들 중 일부는 아래와 같이 ManyToMany로 표현해서 사용하고 있는데요.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;
    
    &lt;span class=&quot;nd&quot;&gt;@ManyToMany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;mutableProducts&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;mutableTags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendorProductTag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableListOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutableTags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Tag&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;
    
    &lt;span class=&quot;nd&quot;&gt;@ManyToMany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PERSIST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MERGE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinTable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;product_tag_assoc&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;joinColumns&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;tag_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)],&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;inverseJoinColumns&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;product_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)],&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;mutableProducts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableListOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutableProducts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;문제는 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tag_id&lt;/code&gt;를 가진 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product&lt;/code&gt;를 조회하는 쿼리를 작성할 때 Kotlin JDSL로 위에 정의된 Entity로는 표현할 방법을 찾을 수 없었다는 것이었습니다.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;product&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;select&lt;/span&gt;
      &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;product_tag_assoc&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;018efa72-0b67-e9a3-29e1-a44576a66bba&apos;&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;product_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그래서 어쩔 수 없이 Kotlin JDSL 쿼리를 위한 관계 Entity를 아래와 같이 추가한 후 쿼리를 만들어서 해결하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@IdClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ProductTagAssocId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProductTagAssoc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProductTagAssocId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;product_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;productId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;productId&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;tag_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;tagId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tagId&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProductTagAssocId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;productId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;tagId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Serializable&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByTagId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;tagId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jdslExecutorSupport&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findAll&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;selectFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                        &lt;span class=&quot;nf&quot;&gt;selectFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ProductTagAssoc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whereAnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                                &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ProductTagAssoc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tagId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tagId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                                &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ProductTagAssoc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;productId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
                            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;asSubquery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같은 방식이 가장 좋은 방식인지는 잘 모르겠습니다. Kotlin JDSL의 문서나 예시 코드에도 위와 같은 사례를 찾아볼 순 없어서 일단 문제 해결에 초점을 맞추어 조치하였는데요. 해당 부분은 한번 Kotlin JDSL측에 문의해 보아야겠습니다.&lt;/p&gt;

&lt;h2 id=&quot;literals&quot;&gt;Literals&lt;/h2&gt;

&lt;p&gt;아래와 같이 특정 컬럼의 존재 여부에 따라서 정렬하고 싶은 경우 CASE 문과 함께 상숫값을 넣어줘야 하는 경우가 있었습니다.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;product&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;
  &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;price&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;asc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;asc&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;offset&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;rows&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;first&lt;/span&gt;
  &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;rows&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;only&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;처음에 아래와 같이 쿼리를 작성해 보았는데요.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendorCatalogProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jdslExecutorSupport&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;selectFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;orderBy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;caseWhen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;isNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;`else`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;asc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;asc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;해당 함수를 실행해 보면 아래와 같이 오류가 발생함을 볼 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Could not locate named parameter [param1], expecting one of []
org.springframework.dao.InvalidDataAccessApiUsageException: Could not locate named parameter [param1], expecting one of []
	at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:371)
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:246)
...
Caused by: java.lang.IllegalArgumentException: Could not locate named parameter [param1], expecting one of []
	at org.hibernate.query.internal.ParameterMetadataImpl.getQueryParameter(ParameterMetadataImpl.java:263)
	at org.hibernate.query.spi.AbstractCommonQueryContract.setParameter(AbstractCommonQueryContract.java:844)
...
Caused by: org.hibernate.query.UnknownParameterException: Could not locate named parameter [param1], expecting one of []
	... 519 more
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;원인은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;then(2)&lt;/code&gt;과 같이 매개변수를 전달하면 내부적으로는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;then(value(2))&lt;/code&gt;으로 동작하게 되고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt;는 JPQL로 변환할 때 쿼리 매개변수(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;param1&lt;/code&gt;)로 변경되는데 실제로 페이징 쿼리를 수행할 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;param1&lt;/code&gt;을 치환할 값이 없으니, 오류가 발생하는 것으로 추측됩니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;writeParam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;param${incrementer.getNext()}&quot;&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;writeParam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그래서 전달된 값 그대로 쿼리로 치환할 수 있도록 intLiteral을 사용하여 위와 같은 오류를 해결하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendorCatalogProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jdslExecutorSupport&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pageable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;selectFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;orderBy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;caseWhen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;isNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;intLiteral&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;`else`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;intLiteral&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;asc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;asc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;마치며&quot;&gt;마치며&lt;/h1&gt;

&lt;p&gt;지금까지 Querydsl에서 Kotlin JDSL으로 어떻게 전환하였고 전환하면서 겪었던 이슈들과 팁들을 소개해 드렸습니다. Kotlin JDSL에 대한 레퍼런스가 많지 않아 저희가 겪었던 경험을 최대한 소개해 드리려고 하다 보니 다소 내용이 길어진 감이 있는 점 양해바랍니다.&lt;/p&gt;

&lt;p&gt;개인적으로 Querydsl에서 Kotlin JDSL으로 전환은 만족스러운 결과를 얻었다고 생각합니다. 메타모델을 별도로 필요로 하지 않고 Entity의 Property를 통해 쿼리를 작성하기 때문에 쿼리를 위해 Entity에서 외부로 노출하지 않아도 될 Property를 어쩔 수 없이 노출해야 할 단점은 있었지만, 메타모델 생성을 위한 복잡한 설정을 하지 않아도 되고 DSL로 직관적이고 단순한 쿼리를 통해 손쉽게 쿼리를 생성할 수 있다는 부분에서 큰 매력으로 다가왔습니다.&lt;/p&gt;

&lt;p&gt;그리고 무엇보다 더 이상 kapt를 사용하지 않고 메타모델을 위한 Code Generation을 실행하지 않다 보니 컴파일 시간을 상당히 줄일 수 있다는 점에서도 큰 장점이라고 생각됩니다.&lt;/p&gt;

&lt;h4 id=&quot;전환-전---1분-22초&quot;&gt;전환 전 - 1분 22초&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/images/transfer-jdsl/as-is-transferr.png&quot; alt=&quot;as-is-transferr&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;전환-후---34초&quot;&gt;전환 후 - 34초&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/images/transfer-jdsl/to-be-transfer.png&quot; alt=&quot;to-be-transfer&quot; /&gt;&lt;/p&gt;

&lt;p&gt;모쪼록 이 글을 통해 Querydsl을 다른 쿼리 빌더로 전환할 계획을 세우시거나 새로운 프로젝트에 도입할 만한 쿼리 빌더를 고민하고 계신 분들께 작은 도움이 되었길 바라면서 마무리 짓도록 하겠습니다.&lt;/p&gt;

&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>발전하는 iOS와 Clean Swift Architecture</title>
      <link>https://spoqa.github.io/2024/03/06/clean-swift.html</link>
      <pubDate>Wed, 06 Mar 2024 00:00:00 +0000</pubDate>
      <author>박건우</author>
      <guid>/2024/03/06/clean-swift</guid>
      <description>&lt;p&gt;안녕하세요. 스포카 제품팀 iOS 개발자 박건우입니다. 🙂&lt;/p&gt;

&lt;p&gt;스포카 iOS 플랫폼에서는 Clean Swift 아키텍처를 기반으로 &lt;a href=&quot;https://apps.apple.com/kr/app/%ED%82%A4%EC%B9%9C%EB%B3%B4%EB%93%9C-%EC%8B%9D%EC%9E%90%EC%9E%AC-%EC%9C%A0%ED%86%B5%EC%97%85%EC%B2%B4-%ED%95%84%EC%9A%94%ED%95%A0%EB%95%8C/id1565918209&quot;&gt;키친보드&lt;/a&gt;와 &lt;a href=&quot;https://apps.apple.com/kr/app/%ED%82%A4%EC%B9%9C%EB%B3%B4%EB%93%9C-%EC%9C%A0%ED%86%B5%EC%82%AC/id1630018371&quot;&gt;키친보드 유통사&lt;/a&gt; iOS 앱을 개발하고 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://clean-swift.com&quot;&gt;Clean Swift&lt;/a&gt;란, Uncle Bob의 클린 아키텍처를 iOS, MacOS 플랫폼에 맞게 적용한 형태의 아키텍처입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/spoqa/CleanSwift-CIS/blob/main/Images/VIP.png?raw=true&quot; alt=&quot;drawing&quot; width=&quot;1000&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이 글에서는 스포카에서 Clean Swift 기반으로 iOS 앱을 개발하며 겪었던 어려움과 이를 어떻게 개선하였는지에 대한 내용을 다루어보겠습니다.&lt;/p&gt;

&lt;h2 id=&quot;왜-clean-swift를-채택하는가&quot;&gt;왜 Clean Swift를 채택하는가?&lt;/h2&gt;

&lt;p&gt;Clean Swift 아키텍처를 제품 개발에 꾸준히 사용한 이유는 아래와 같습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;키친보드 iOS 앱의 아키텍처는 언제든지 바뀔 수 있다.
    &lt;ul&gt;
      &lt;li&gt;스포카 모바일 챕터는 규모가 크지 않습니다. 그래서 추후 여러 사람과 공동으로 작업하게 되었을때 손쉽게 다른 아키텍처를 결합하거나, 현재의 아키텍처를 개선할 수 있어야한다고 보았습니다.&lt;/li&gt;
      &lt;li&gt;Clean Swift가 추구하는 프레임워크가 아닌 템플릿으로 만들어지는 아키텍처 구조는 특정 아키텍처에 강하게 결합되지 않도록 도와주었고 이는 개선에도 용이하였습니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;키친보드 iOS 앱은 로직의 복잡도가 높다.
    &lt;ul&gt;
      &lt;li&gt;키친보드 서비스는 무거운 도메인들과 관리되어야하는 케이스가 다소 존재하는 편입니다. 저희는 이러한 로직의 복잡도가 UI의 복잡도보다 높다고 판단하였습니다.&lt;/li&gt;
      &lt;li&gt;클린 아키텍처를 기반으로 한 Clean Swift는 iOS 플랫폼에 특화된 고수준과 저수준을 분리하는 방식을 제시합니다.&lt;/li&gt;
      &lt;li&gt;이러한 고수준 비즈니스 논리의 분리가 키친보드 iOS 앱 코드의 복잡도를 낮출 수 있었습니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;그러나 Clean Swift를 적용하며 마주한 문제가 있었습니다.&lt;/p&gt;

&lt;h2 id=&quot;발전하는-ios&quot;&gt;발전하는 iOS&lt;/h2&gt;

&lt;p&gt;저희가 마주한 Clean Swift의 문제점은 &lt;strong&gt;발전하는 iOS&lt;/strong&gt; 문제였습니다.&lt;/p&gt;

&lt;p&gt;iOS 플랫폼은 끊임없이 발전을 거듭했습니다. SwiftUI의 등장, 상태 관리의 중요성 대두, 새로운 비동기 처리 방식의 도입 등 Swift 언어와 패러다임은 다소 많은 변화를 거쳤습니다.&lt;/p&gt;

&lt;p&gt;iOS 플랫폼의 클린 아키텍처라는 말이 무색하게 2015년에 발표된 Clean Swift는 2024년의 iOS 플랫폼에는 맞지 않게 된 것이었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/spoqa/CleanSwift-CIS/blob/main/Images/iOS-World-CleanSwift.png?raw=true&quot; alt=&quot;drawing&quot; width=&quot;1000&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이러한 Clean Swift의 문제점을 마주하여 새로운 기술 도입에 어려움을 겪었고, 새로운 아키텍처로의 변경을 생각하기도 했었습니다.&lt;/p&gt;

&lt;p&gt;그러나 앞서 언급드린 프레임워크가 아닌 템플릿, 고수준과 저수준 분리 등 Clean Swift에서 제시하는 방향성은 여전히 유효하여 깊은 고민이 되었습니다.&lt;/p&gt;

&lt;h2 id=&quot;더-나은-방법&quot;&gt;더 나은 방법&lt;/h2&gt;

&lt;p&gt;Clean Swift를 만든 Raymond는 블로그에서 아래와 같은 말을 하였습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I am not perfect. No one is. It is very likely that there are &lt;strong&gt;better ways&lt;/strong&gt; to do things, especially as the Swift language continues to evolve. I look forward to connect with you so we can learn and improve together.
저는 완벽하지 않습니다. 아무도 완벽하지 않죠. 특히 Swift 언어가 지속적으로 발전하는 만큼, &lt;strong&gt;더 나은 방법&lt;/strong&gt;이 존재할 가능성이 큽니다. 여러분과 소통하며 서로 배우고 함께 성장하기를 기대합니다.&lt;/p&gt;

&lt;/blockquote&gt;

&lt;p&gt;Raymond의 말처럼, 저희는 Swift 언어가 지속적으로 발전하는 만큼 더 나은 방법을 찾아 나아가야한다고 보았습니다.
그래서 Clean Swift를 변형하여 언어와 패러다임의 발전에 맞추는 여러 시도들을 해보았습니다.&lt;/p&gt;

&lt;p&gt;이 글에서는 저희가 설계한 완벽하진 않지만 &lt;strong&gt;더 나은 방법(better ways)&lt;/strong&gt;의 Clean Swift를 소개해 드려 보고자 합니다.&lt;/p&gt;

&lt;h2 id=&quot;설계-목표&quot;&gt;설계 목표&lt;/h2&gt;

&lt;p&gt;먼저 개선한 Clean Swift의 설계 목표로 기존의 장점들을 포함한 다음의 4가지를 선정하였습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Single Responsibility (단일 책임)&lt;/strong&gt; : 컴포넌트를 세부적이고 명확하게 분리합니다. 세분화된 컴포넌트로 고수준과 저수준의 분리 또한 이루어지도록 합니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Testability (테스트 가능성)&lt;/strong&gt; : 컴포넌트간의 의존도를 낮추어 단위 테스팅 및 통합 테스팅을 쉽게 할 수 있도록 합니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Framework Agnostic (프레임워크 독립성)&lt;/strong&gt; : 외부 프레임워크에 종속된 아키텍처를 구성하지 않습니다. UI 혹은 Network Framework가 바뀌더라도, Swift 언어와 Foundation에서 제공하는 요소들만을 사용하여 구조화된 코드들은 영향을 받지 않습니다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Manageability of State (상태 관리 용이성)&lt;/strong&gt; : 단방향, 부수효과 등 여러 상태 관리 기법들을 통해 애플리케이션의 상태를 예측 가능하게 만듭니다. 명확하게 관리되는 상태는 시스템, UI의 현재 상태를 빠르게 파악할 수 있게 해줍니다. SwiftUI를 비롯한 다양한 신기술과 패러다임에서 중심이 되는 개념입니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이제 4가지 목표들을 달성하기 위해 개선한 Clean Swift를 알아보겠습니다.&lt;/p&gt;

&lt;h2 id=&quot;cis-controller-interactor-store&quot;&gt;CIS (Controller-Interactor-Store)&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/spoqa/CleanSwift-CIS/blob/main/Images/Main-Diagram.png?raw=true&quot; alt=&quot;drawing&quot; width=&quot;1000&quot; /&gt;&lt;/p&gt;

&lt;p&gt;상태 관리 개념과 클린아키텍처 논리를 중심으로 Clean Swift를 해석하고 각 컴포넌트의 역할을 분리 혹은 재정의하였습니다.
주요 컴포넌트는 5개로, View, Controller, Interactor, Store, Worker가 존재합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ViewController&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ 화면(View)과 유저 액션을 핸들링하는 로직(Controller)으로 분리하여 각각의 책임을 지게 하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Presenter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ 상태를 관리하고, 인터렉터의 결과를 반환받아 상태를 변경시키는 Store로 변화하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactor&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ 기존과 동일하게 고수준의 비즈니스 로직만을 담당할 수 있도록 하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worker&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ 부수효과(SideEffect)를 처리하는 컴포넌트로 확장되었습니다.&lt;/p&gt;

&lt;p&gt;Layer로는 3 Layer로 구분이 됩니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Domain&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;비즈니스 규칙이 존재하는 영역입니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Interface Adapter&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;domain과 infrastructure 사이의 연결을 도와줍니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;외부 세계를 담당합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;저희는 이것을 CIS 패턴으로 명칭을 정의하였습니다.&lt;/p&gt;

&lt;h2 id=&quot;템플릿과-예제&quot;&gt;템플릿과 예제&lt;/h2&gt;

&lt;p&gt;이해를 돕기 위해 CIS 패턴으로 작성된 템플릿과 예제를 첨부합니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/spoqa/CleanSwift-CIS&quot;&gt;https://github.com/spoqa/CleanSwift-CIS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/spoqa/CleanSwift-CIS/blob/main/Images/Templates.png?raw=true&quot; alt=&quot;drawing&quot; width=&quot;1000&quot; /&gt;&lt;/p&gt;

&lt;p&gt;템플릿은 Scene과 SPM 형태의 Scene, 그리고 각각의 컴포넌트를 만들수 있도록 구성하였습니다.&lt;/p&gt;

&lt;p&gt;예제는 키친보드 서비스의 기능중 하나인 견적 요청 기능입니다. 명세표 이미지를 첨부하고 요청 사항을 추가하여 견적을 요청하는 기본적인 플로우를 담았습니다.&lt;/p&gt;

&lt;p&gt;이제 각 컴포넌트별 부여된 역할과 세부적인 특징을 알아보겠습니다.&lt;/p&gt;

&lt;h2 id=&quot;view&quot;&gt;View&lt;/h2&gt;

&lt;p&gt;View는 &lt;strong&gt;어떻게 그려지는가&lt;/strong&gt;를 책임집니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;특징&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;View는 고수준의 책임에서 완전히 분리됩니다.
    &lt;ul&gt;
      &lt;li&gt;유저 행동의 시작점(Action)과 끝점(State). 그리고 그 과정은 View의 책임에서 분리합니다.&lt;/li&gt;
      &lt;li&gt;유저의 트리거가 발생되면 Controller의 액션을 호출하고, ObservableObject 형태인 Store가 변경되면 화면을 변화시킵니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;View에 어느 UI Framework를 사용하여도 고수준의 컴포넌트들에게 영향을 주지 않습니다.
    &lt;ul&gt;
      &lt;li&gt;UI에 대한 책임을 고수준에서 완전히 분리하기 때문에 UI Framework는 View 컴포넌트에서만 종속성을 띄게 됩니다.&lt;/li&gt;
      &lt;li&gt;UIKit, SwiftUI 등 다양한 UI Famework들에 대한 독립성을 이룰 수 있습니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptSwiftUIView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;controller&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptControllerable&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@ObservedObject&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptStore&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;onTapGesture&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;controller&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;imageAttachTapped&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptUIKitView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIViewController&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;controller&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptControllerable&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptStore&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sink&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;weak&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;updateImageScrollView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;in&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cancellables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;+상태를 기반으로 렌더링하는 구조가 아닌 UIKit 등의 경우에는 First Party Library인 Combine을 사용해서 상태 값을 바인딩하도록 하였습니다.&lt;/p&gt;

&lt;h2 id=&quot;controller&quot;&gt;Controller&lt;/h2&gt;

&lt;p&gt;Controller는 &lt;strong&gt;유저의 액션을 처리&lt;/strong&gt;합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;특징&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Controller는 유저 행위의 시작점에서 도메인 계층에 대한 게이트웨이 역할을 합니다.
    &lt;ul&gt;
      &lt;li&gt;비즈니스 로직을 실행하고자 한다면 Interactor의 유즈케이스를 호출합니다.&lt;/li&gt;
      &lt;li&gt;단순한 상태, UI의 변화를 실행하고자 한다면 Store의 뮤테이션을 호출합니다.&lt;/li&gt;
      &lt;li&gt;유저의 액션이 들어오면 이를 판단하여, 비즈니스 로직 혹은 상태 변화를 적절히 실행시킵니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Controller의 액션 함수는 순수 함수로 작성됩니다.
    &lt;ul&gt;
      &lt;li&gt;액션 함수는 상태에 의존하지 않고 변경하지 않습니다. 동일한 입력에 항상 동일한 출력을 반환합니다.&lt;/li&gt;
      &lt;li&gt;Controller를 부수효과에 영향이 없도록 만들어 예측 가능성과 모듈화 수준을 높입니다.&lt;/li&gt;
      &lt;li&gt;이를 위하여 Controller는 단방향으로 전달만 가능한 컴포넌트들에 의존합니다. 상태에 의존하지 않습니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptAction&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cameraCanceled&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;imagePicked&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptControllerable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;interactor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptInteractable&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;weak&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptMutatable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptController&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptAction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;cameraCanceled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dismissCamera&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;imagePicked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;interactor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;attachImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;AttachImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;imageUIData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dismissImagePicker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;interactor&quot;&gt;Interactor&lt;/h2&gt;

&lt;p&gt;Interactor는 &lt;strong&gt;비즈니스 로직&lt;/strong&gt;을 담당합니다.&lt;/p&gt;

&lt;p&gt;특징&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Interactor는 화면의 모든 유즈케이스를 캡슐화하고 고립시킵니다.
    &lt;ul&gt;
      &lt;li&gt;Interactor는 단방향 사이클에서 자연스럽게 고수준의 유즈케이스를 분리할 수 있게끔 해줍니다.&lt;/li&gt;
      &lt;li&gt;Request, Response 모델을 통해 각각의 유즈케이스를 고립시킵니다.&lt;/li&gt;
      &lt;li&gt;하나의 기능 변경이 다른 하나의 기능에 영향을 미치지 않도록 합니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/spoqa/CleanSwift-CIS/blob/main/Images/UseCase.png?raw=true&quot; alt=&quot;drawing&quot; width=&quot;1000&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// MARK: - UseCases&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AttachImage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        
        &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Request&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;imageUIData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ImageUIDataModel&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        
        &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;attachedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;
            
            &lt;span class=&quot;kd&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exceedImageCount&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;failDataMapping&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ol&gt;
  &lt;li&gt;Interactor는 State에서 분리되어 고수준으로 고립된 &lt;strong&gt;DomainState&lt;/strong&gt;를 사용하여 비즈니스 로직을 구현합니다.
    &lt;ul&gt;
      &lt;li&gt;상태의 목적에는 여러가지가 있을 수 있습니다. 
 (View를 렌더링 하기 위해, 테스트를 하기 위해, 로깅을 하기 위해, 로직에 활용하기 위해 ..)&lt;/li&gt;
      &lt;li&gt;이중에서 로직에 활용하기 위한 상태를 “도메인 상태”로 분리하고, Interactor는 이를 read only로 활용합니다.&lt;/li&gt;
      &lt;li&gt;상태 관리 측면에서도 저수준과 고수준을 분리하며, 더욱더 외부에 의존하지 않는 도메인 계층을 만들 수 있습니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/spoqa/CleanSwift-CIS/blob/main/Images/DomainState.png?raw=true&quot; alt=&quot;drawing&quot; width=&quot;1000&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptUseCase&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;attachImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;AttachImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;uploadImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;UploadImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;saveImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;SaveImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;showCamera&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ShowCamera&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;showGallery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ShowGallery&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptInteractor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptInteractable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptMutatable&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;HasUploadReceiptDomainState&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptWorkable&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptInteractor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;useCase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptUseCase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;useCase&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;attachImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;AttachImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;storedAttachedImages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;domainState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt;
            
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storedAttachedImages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;AttachImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;attachedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storedAttachedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exceedImageCount&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;imageData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mapToData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;imageUIData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;AttachImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;attachedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storedAttachedImages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;imageData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uiData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)],&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;nil&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;AttachImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;attachedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storedAttachedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;failDataMapping&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            
            &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mutateAttachImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;uploadImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;UploadImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;storedAttachedImages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;domainState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt;
            
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storedAttachedImages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isEmpty&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;UploadImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;uploadUrlObjectKeys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;emptyAttachedImage&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uploadUrls&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetchImageUploadUrls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;names&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storedAttachedImages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}))&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;requestImagesUpload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;attatchedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storedAttachedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;imageUploadUrls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uploadUrls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    
                    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;UploadImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                        &lt;span class=&quot;nv&quot;&gt;uploadUrlObjectKeys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uploadUrls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objectKey&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
                        &lt;span class=&quot;nv&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;nil&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;UploadImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                        &lt;span class=&quot;nv&quot;&gt;uploadUrlObjectKeys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt;
                        &lt;span class=&quot;nv&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            
            &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mutateUploadImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;store&quot;&gt;Store&lt;/h2&gt;

&lt;p&gt;Store는 &lt;strong&gt;상태를 관리하고 변경&lt;/strong&gt;시키는 역할을 담당합니다.&lt;/p&gt;

&lt;p&gt;특징&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Store는 상태를 한 곳에서 만들어지고 관리될 수 있도록 합니다.
    &lt;ul&gt;
      &lt;li&gt;상태에 대한 setter를 private으로 관리하여 외부에서 상태를 변경할 수 없도록 합니다.&lt;/li&gt;
      &lt;li&gt;뮤테이션 함수에서는 상태를 변경하는 대신, 업데이트된 복사본을 새로 만들어 기존 상태를 덮어씌웁니다.&lt;/li&gt;
      &lt;li&gt;상태 객체가 어디서든 변경될 수 있는 가능성을 제거하여 예측 가능한 동작을 하도록 합니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Store의 뮤테이션 함수는 외부 세계의 상태를 변화시켜야하는 상황에 대응합니다.
    &lt;ul&gt;
      &lt;li&gt;뮤테이션을 통해서 상태를 변화시키고자 하였으니, 상태가 화면의 상태가 아닌 경우가 존재합니다. 
 (토스트 메세지 표출 등)&lt;/li&gt;
      &lt;li&gt;뮤테이션 함수에서는 Worker 함수를 호출하여 이러한 외부 상태를 변경하는 부수효과를 처리합니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/spoqa/CleanSwift-CIS/blob/main/Images/Mutation.png?raw=true&quot; alt=&quot;drawing&quot; width=&quot;1000&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptMutation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutateAttachImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;AttachImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dismissCamera&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dismissImagePicker&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;@MainActor&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptState&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;DomainState&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;@MainActor&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptMutatable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;HasUploadReceiptDomainState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ObservableObject&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptWorkable&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@Published&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private(set)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptState&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptStore&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptMutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptState&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mutateAttachImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;exceedImageCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;showToast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;최대 5장까지 첨부할 수 있습니다&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    
                &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;failDataMapping&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;showToast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;사진 처리 과정에서 오류가 발생하였습니다&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;domainState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uiData&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// .. &lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;dismissCamera&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;showingCamera&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;
            
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;dismissImagePicker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;showingImagePicker&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;worker&quot;&gt;Worker&lt;/h2&gt;

&lt;p&gt;Worker는 &lt;strong&gt;부수효과(SideEffect)를 처리하고, 복잡도를 낮추는 역할&lt;/strong&gt;을 합니다.&lt;/p&gt;

&lt;p&gt;*부수효과(SideEffect)&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;상태 변화를 유발하지 않거나(예: 앱로깅 등), 복잡하고 예측 불가능한 작업(예: 네트워크 요청 등)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;특징&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Worker는 상태를 예측 가능하도록 만듭니다.
    &lt;ul&gt;
      &lt;li&gt;예측이 불가능한 작업인 부수효과를 처리하여 화면의 상태를 예측 가능하도록 만듭니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Worker는 모든 컴포넌트들을 순수하게 만들 수 있도록 합니다.
    &lt;ul&gt;
      &lt;li&gt;Worker가 주입되는 컴포넌트는 Interactor에 국한되지 않습니다.&lt;/li&gt;
      &lt;li&gt;Worker는 부수효과가 발생될 수 있는 모든 컴포넌트들에 주입되고 부수효과들을 제어합니다.&lt;/li&gt;
      &lt;li&gt;ex)
        &lt;ul&gt;
          &lt;li&gt;Controller : Logging&lt;/li&gt;
          &lt;li&gt;Interactor : Database, Server&lt;/li&gt;
          &lt;li&gt;Store : Toast Message&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/spoqa/CleanSwift-CIS/blob/main/Images/Worker.png?raw=true&quot; alt=&quot;drawing&quot; width=&quot;1000&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;protocol&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptWorkable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AnyObject&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;@MainActor&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;showToast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;requestImagesUpload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;attatchedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;imageUploadUrls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageUploadUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throws&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;requestCameraPermission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Bool&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;requestGalleryPermission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Bool&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptWorker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptWorkable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;imageUploadNetworkService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ImageUploadNetworkServiceProtocol&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptWorker&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;@MainActor&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;showToast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;Toast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;requestImagesUpload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;attatchedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;imageUploadUrls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageUploadUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;withThrowingTaskGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Void&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;imageUploadUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;attachedImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;imageUploadUrls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;attatchedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;addTask&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;imageUploadNetworkService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;putImageData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;imageUploadUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uploadUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;imageData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;attachedImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;waitForAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;requestCameraPermission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Bool&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;withCheckedContinuation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;continuation&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;AVCaptureDevice&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;requestAccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;video&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;granted&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;continuation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;resume&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;returning&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;granted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;requestGalleryPermission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Bool&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;withCheckedContinuation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;continuation&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;#available(iOS 14, *)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;PHPhotoLibrary&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;requestAuthorization&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;readWrite&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;continuation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;resume&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;returning&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorized&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;PHPhotoLibrary&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;requestAuthorization&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;continuation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;resume&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;returning&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;authorized&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;models&quot;&gt;Models&lt;/h2&gt;

&lt;p&gt;Model은 화면에서 사용되는 Entity와 ViewModel, UseCaseModel(Request, Response)을 명세합니다.&lt;/p&gt;

&lt;p&gt;특징&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Entity는 도메인 계층에서 사용되는 객체입니다.
    &lt;ul&gt;
      &lt;li&gt;비즈니스 로직에 활용되며 Interactor와 Worker의 계층간 이동에 사용됩니다.&lt;/li&gt;
      &lt;li&gt;비즈니스 로직을 담당하는 Interactor가 저수준에 의존하지 않도록 합니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;ViewModel은 화면 렌더링에 사용되는 객체입니다.
    &lt;ul&gt;
      &lt;li&gt;실질적으로 화면에 그려질 날것의 값들을 포함합니다.&lt;/li&gt;
      &lt;li&gt;Store의 뮤테이션 함수를 통해 만들어지며 Store와 View의 계층간 이동에 사용됩니다.&lt;/li&gt;
      &lt;li&gt;View에서 불필요한 매핑 로직을 작성하지 않도록 도와주고 화면을 그리는것에만 집중할 수 있도록 합니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;UseCaseModel은 각 UseCase의 Input과 Output 객체입니다.
    &lt;ul&gt;
      &lt;li&gt;Input인 Request는 Controller와 Interactor의 계층간 이동에 사용됩니다.&lt;/li&gt;
      &lt;li&gt;Output인 Response는 Interactor와 Store의 계층간 이동에 사용됩니다.&lt;/li&gt;
      &lt;li&gt;화면에서 발생할 수 있는 UseCase를 캡슐화하고 추적에 용이하게 만듭니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// MARK: - Entities&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timeIntervalSince1970&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Data&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uiData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ImageUIDataModel&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// MARK: - ViewModels&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;typealias&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ImageUIDataModel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIImage&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// MARK: - UseCases&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AttachImage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        
        &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Request&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;imageUIData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ImageUIDataModel&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        
        &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Response&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;attachedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;
            
            &lt;span class=&quot;kd&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exceedImageCount&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;failDataMapping&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;플로우&quot;&gt;플로우&lt;/h2&gt;

&lt;p&gt;주요 컴포넌트의 역할과 특징을 알아보았습니다.&lt;/p&gt;

&lt;p&gt;이해를 돕고자 명세표 이미지 첨부 플로우를 예시로 CIS 사이클의 동작 방식을 표현하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/spoqa/CleanSwift-CIS/blob/main/Images/Flow-Example.gif?raw=true&quot; alt=&quot;drawing&quot; width=&quot;1000&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;쓰레드-관리&quot;&gt;쓰레드 관리&lt;/h2&gt;

&lt;p&gt;구조의 안정성을 높이기 위해서는 쓰레드 관리 방식 또한 고려가 되어야 했습니다.&lt;/p&gt;

&lt;p&gt;가장 우선적으로 고려하였던 점은 아래와 같습니다&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Swift Concurrency를 사용합니다.
    &lt;ul&gt;
      &lt;li&gt;Swift Concurrency는 GCD와 비교하여 가독성과 성능이 우수한 API로써 앞으로의 iOS 동시성 프로그래밍 표준이라고 보았기에 이를 바탕으로 구상하였습니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;액션간의 독립성을 제공합니다.
    &lt;ul&gt;
      &lt;li&gt;유저의 액션은 각각 독립적으로 움직이며 서로의 액션이 서로에게 영향을 주지 않도록 합니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;상태 관리는 단일 쓰레드에서 될 수 있도록 합니다.
    &lt;ul&gt;
      &lt;li&gt;상태가 여러 쓰레드에서 수정 및 참조되어 동시성 문제가 발생되지 않도록 합니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 점들을 바탕으로 쓰레드 관리 방식을 설계하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/spoqa/CleanSwift-CIS/blob/main/Images/Thread.png?raw=true&quot; alt=&quot;drawing&quot; width=&quot;1000&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;유저의 액션이 발생되면 Task를 생성하고, 액션을 통해 실행되는 작업들은 하나의 Task로 묶이도록 합니다.&lt;/li&gt;
  &lt;li&gt;상태를 변경하거나 참조할때는 Main Thread로 다시 격리 시킵니다.&lt;/li&gt;
  &lt;li&gt;병렬화되어 실행되었던 유저 액션들이 다시 직렬화되어 상태를 관리합니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;설계한 쓰레드 관리 방식을 일관되게 행하기 위하여 각각의 컴포넌트의 실행 지점을 enum으로 명세하고 하나의 함수로 실행시키도록 하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Controller&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;protocol&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptControllerable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AnyObject&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@discardableResult&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptAction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Void&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Never&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Interactor&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;protocol&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptInteractable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;useCase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptUseCase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Store&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;protocol&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptMutatable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AnyObject&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@MainActor&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptMutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;테스팅&quot;&gt;테스팅&lt;/h2&gt;

&lt;p&gt;현재 스포카 iOS 플랫폼에서는 BDD 방식에 의거하여, 보편어로 테스트 시나리오를 명세하고 이를 통합 테스트 코드로 작성합니다.&lt;/p&gt;

&lt;p&gt;이러한 시나리오를 기반으로 하는 통합 테스트는 요구사항의 누락을 줄이며 이해도를 높히고 사양을 명확하게 할 수 있었습니다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;Scenario&lt;/th&gt;
      &lt;th&gt;Given (전제)&lt;/th&gt;
      &lt;th&gt;When (조건)&lt;/th&gt;
      &lt;th&gt;Then (결과)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;사진 첨부 수단 선택&lt;/td&gt;
      &lt;td&gt; &lt;/td&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;사진 첨부 버튼 클릭 &amp;gt; 사진 첨부 수단 시트 표출&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;사진 첨부 버튼 클릭&lt;/td&gt;
      &lt;td&gt;사진 첨부 수단 시트 표출됨&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;카메라 표출&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;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;사진 촬영하기 옵션 클릭 &amp;gt; 사진 첨부 수단 시트 미표출&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;사진 촬영하기 옵션 클릭&lt;/td&gt;
      &lt;td&gt;사진 첨부 수단 시트 표출되지 않음&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;…&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;이미지 피커 (갤러리) 표출&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;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;사진첩에서 가져오기 옵션 클릭 &amp;gt; 사진 첨부 수단 시트 미표출&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;사진첩에서 가져오기 옵션 클릭&lt;/td&gt;
      &lt;td&gt;사진 첨부 수단 시트 표출되지 않음&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;…&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;카메라 촬영&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;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;카메라 촬영 취소 &amp;gt; 카메라 미표출&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;카메라 촬영 취소&lt;/td&gt;
      &lt;td&gt;카메라가 표출되지 않음&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;…&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;갤러리 사진 선택&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;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;사진 선택 취소 &amp;gt; 이미지 피커 미표출&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;사진 선택 취소&lt;/td&gt;
      &lt;td&gt;이미지 피커가 표출되지 않음&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;…&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;이미지 업로드&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;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;다음 버튼 클릭 &amp;gt; 첨부된 사진 0개 &amp;gt; 토스트 메세지 표출&lt;/td&gt;
      &lt;td&gt;첨부된 사진이 0개이다.&lt;/td&gt;
      &lt;td&gt;다음 버튼 클릭&lt;/td&gt;
      &lt;td&gt;“견적 요청을 하기 위해서\n필수로 이미지를 첨부하여야 합니다” 토스트 메세지 표출됨&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;td&gt; &lt;/td&gt;
      &lt;td&gt;…&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;AS-IS 구조의 경우에는, 통합 테스트 코드를 작성할때 UI에 의존적인 컴포넌트인 ViewController에 접근해야 하였습니다.&lt;/p&gt;

&lt;p&gt;UI에 의존적인 통합 테스트 코드는 세부사항인 UI 및 외부 Framework의 변경이 전체 테스트 코드에 영향을 주었고, 이때문에 안정적인 시나리오 기반의 테스트 코드 작성에 어려움을 겪었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/spoqa/CleanSwift-CIS/blob/main/Images/ASIS-Test.png?raw=true&quot; alt=&quot;drawing&quot; width=&quot;1000&quot; /&gt;&lt;/p&gt;

&lt;p&gt;TO-BE 구조에서는, 유저의 액션과 상태가 UI에 분리되어 UI에 접근하지 않고도.&lt;/p&gt;

&lt;p&gt;State를 통해 Given을 정의할 수 있고, Controller를 통해 When을 실행할 수 있고, State를 통해 Then을 검증할 수 있습니다.&lt;/p&gt;

&lt;p&gt;이를 통해 외부에 의존적이지 않고 보편적인 시나리오에 집중할 수 있는 통합 테스트 코드를 작성할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/spoqa/CleanSwift-CIS/blob/main/Images/TOBE-Test.png?raw=true&quot; alt=&quot;drawing&quot; width=&quot;1000&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptSceneTests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;XCTestCase&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;controller&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptController&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@MainActor&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptState&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_사진선택완료_첨부된사진5개이상__토스트메세지표출됨&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;MainActor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mockWorker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mapToDataResult&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            
            &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;domainState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uiData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()),&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uiData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()),&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uiData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()),&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uiData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;configure&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;initialState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        
        &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;controller&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;imagePicked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageUIDataModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()))&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;
        
        &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;lastShowToastMessage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mockWorker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lastShowToastMessage&lt;/span&gt;
        
        &lt;span class=&quot;kt&quot;&gt;XCTAssertEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lastShowToastMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;최대 5장까지 첨부할 수 있습니다&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_사진선택완료_첨부된사진5개미만__첨부된이미지목록에추가됨&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;MainActor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mockWorker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mapToDataResult&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            
            &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;domainState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uiData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()),&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uiData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()),&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uiData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()),&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uiData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;configure&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;initialState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        
        &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;controller&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;imagePicked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceipt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;ImageUIDataModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()))&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;
        
        &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;domainAttachedImages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;domainState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;attachedImages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt;
        
        &lt;span class=&quot;kt&quot;&gt;XCTAssertEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;domainAttachedImages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;XCTAssertEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attachedImages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptSceneTests&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;MockWorker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptWorkable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;화면간의-통신&quot;&gt;화면간의 통신&lt;/h2&gt;

&lt;p&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;p&gt;먼저 화면이 그려지기 위한 초기값을 전달 받기 위해서 View가 생성될때 initialState를 받도록 하였습니다.&lt;/p&gt;

&lt;p&gt;상위 화면 상태에 전달할 값을 저장하고 하위 화면을 생성할때 initialState로 값을 전달합니다.&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 전달할 상태 값 저장&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptStore&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptMutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptState&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mutateUploadImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;receiptImageUploadUrlObjectKeys&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uploadUrlObjectKeys&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Data Passing&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isAddRequestsViewActive&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Active AddRequests View&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 상위 뷰&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptSwiftUIView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;NavigationView&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;NavigationLink&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;isActive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Binding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                        &lt;span class=&quot;nv&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isAddRequestsViewActive&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
                        &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;kt&quot;&gt;AddRequestsSwiftUIView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                            &lt;span class=&quot;nv&quot;&gt;initialState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                                &lt;span class=&quot;nv&quot;&gt;receiptImageUploadUrlObjectKeys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;receiptImageUploadUrlObjectKeys&lt;/span&gt;
                            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                            &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 하위 뷰&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsSwiftUIView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;initialState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다른 화면으로 위임되는 흐름은 부수효과(SideEffect)로 해석하였습니다.&lt;/p&gt;

&lt;p&gt;그래서 부수효과를 담당하는 Worker를 통해 서로 통신하도록 하였고 위임된 흐름은 Controller로 흘러가게 설계하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/spoqa/CleanSwift-CIS/blob/main/Images/Communication.png?raw=true&quot; alt=&quot;drawing&quot; width=&quot;1000&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Worker는 상위 화면에 흐름을 위임 시킬수 있도록 Delegate를 갖습니다. 그리고 하위 화면에 흐름을 위임 시킬수 있도록 하위 화면의 Controller를 갖습니다.&lt;/p&gt;

&lt;p&gt;Delegate는 View가 생성될 때 상위 화면에서 주입받도록 하였고, Controller는 inout 키워드를 통해 하위 화면이 생성되면 상위 화면에 할당하도록 하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 상위 화면의 Worker&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;protocol&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptWorkable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AnyObject&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;addRequestsController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsControllerable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 상위 화면에서 하위 화면에 의해 흐름을 위임 받는 로직&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsDelegate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;@MainActor&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;quotationRequestSuccessed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;successMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setIsActiveAddRequestsView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;isActive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;showMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;successMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;clearAttachedImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 상위 화면에서 하위 화면에게 흐름을 위임 시키는 로직&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptStore&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptMutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UploadReceiptState&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mutateSomthing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;addRequestsController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;something&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 하위 화면의 Delegate&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;protocol&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsDelegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AnyObject&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@MainActor&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;quotationRequestSuccessed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;successMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 하위 화면의 Worker&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;protocol&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsWorkable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AnyObject&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsDelegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 하위 화면에서 상위 화면에게 흐름을 위임 시키는 로직&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsStore&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsMutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsState&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mutateRequestQuotation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;quotationRequestSuccessed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;successMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;견적 요청을 성공하였습니다 :)&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 하위 뷰&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsSwiftUIView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;initialState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;controller&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;inout&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsControllerable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsDelegate&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;worker&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsWorker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;quotationNetworkService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;quotationNetworkService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;_controller&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AddRequestsController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;interactor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;interactor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;controller&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_controller&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ..&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;

&lt;p&gt;지금까지 스포카 iOS 플랫폼에서 Clean Swift에 겪었던 어려움과 이를 극복하기 위해 개선한 구조를 알아보았습니다.&lt;/p&gt;

&lt;p&gt;Clean Swift를 만든 Raymond의 말을 빌려, 
저희는 완벽하지 않습니다. 더 나은 방법이 존재할 가능성이 큽니다. 그러나 저희의 여정이 같은 고민을 가지고 있는 분들에게 조금이나마 도움이 되었으면 좋겠습니다.&lt;/p&gt;

&lt;p&gt;스포카는 매장의 식자재 걱정을 덜어주는 편리한 서비스를 만들어가고 있습니다. 모바일 챕터는 정답을 찾아가는 과정에서 발생되는 앱차원의 여러 문제들을 유연하고 단순하게 만들어야하는 책임을 가지고 있습니다.&lt;/p&gt;

&lt;p&gt;이러한 책임을 바탕으로 또 다른 문제를 해결한 경험을 가지고 다른 글로 찾아뵙도록 하겠습니다.&lt;/p&gt;

&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>키친보드 안드로이드 앱 Jetpack Compose 도입기</title>
      <link>https://spoqa.github.io/2023/10/30/android-jetpack-compose.html</link>
      <pubDate>Mon, 30 Oct 2023 00:00:00 +0000</pubDate>
      <author>김진우</author>
      <guid>/2023/10/30/android-jetpack-compose</guid>
      <description>&lt;p&gt;안녕하세요. 스포카 제품팀의 안드로이드 개발자 김진우입니다.&lt;/p&gt;

&lt;p&gt;드디어 이번에 키친보드 안드로이드 앱에 Jetpack Compose를 도입하게 되었습니다.&lt;br /&gt;
그동안의 Jetpack Compose 도입하기 위해 검토했던 부분과 소소한 팁들을 공유드릴 겸 기술 블로그를 올리게 되었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-10-30/android-compose-spoqa-foundation.png&quot; alt=&quot;android-compose-spoqa-foundation&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;도입-배경&quot;&gt;도입 배경&lt;/h2&gt;

&lt;p&gt;수년간 XML 기반의 Android 앱을 개발하면서 가장 불편했던 점은 빈번한 작업 컨텍스트 전환의 번거로움이었습니다.&lt;/p&gt;

&lt;p&gt;기본적으로 XML 기반의 UI를 개발하려면, res/layout 디렉토리에 XML Layout을 정의한 후, Java나 Kotlin 코드 레벨에서 View를 연결하여 UI를 핸들링합니다. 뿐만 아니라, Color나 Shape 속성을 반영하기 위해 res/color 및 res/drawable 디렉토리에 접근해야 하며, UI State(enabled, focused 등)를 세부적으로 표현하기 위해 selector를 정의하기도 합니다.&lt;/p&gt;

&lt;p&gt;또한 List UI 개발하려면, RecyclerView + Adapter + ViewHolder + XML Layout 구조를 각각 연동시켜야 하므로, 마찬가지의 빈번한 작업 컨텍스트 전환을 요구합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-10-30/android-xml-ui-development-flow.png&quot; alt=&quot;android-xml-ui-development-flow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이러한 일련의 과정들에 대한 작업 컨텍스트의 전환은 XML 기반의 앱 개발 과정에선 필수적이기에 개발자 입장에서는 피로를 느끼게 됩니다.&lt;/p&gt;

&lt;p&gt;그래서 이러한 불편함을 해결해 줄 수 있으며, 보다 직관적이고 단순하게 UI를 표현할 수 있는 Jetpack Compose를 도입하게 되었습니다.&lt;/p&gt;

&lt;p&gt;참고) &lt;a href=&quot;https://developer.android.com/jetpack/compose/why-adopt?hl=ko&quot;&gt;https://developer.android.com/jetpack/compose/why-adopt?hl=ko&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;선언형-ui란&quot;&gt;선언형 UI란?&lt;/h2&gt;

&lt;p&gt;선언형 UI는 상태에 따라 어떤 UI를 렌더링할지 정의하는 패러다임입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://miro.medium.com/v2/resize:fit:1400/1*QxZkcSfgUbASR8DJsuWBtw.png&quot; alt=&quot;declarative-ui&quot; /&gt;&lt;/p&gt;

&lt;p&gt;선언형 UI의 장점은 UI 상태와 연관된 UI 컴포넌트를 코드로 직접 설명하기 때문에 코드가 더욱 이해하기 쉬워집니다. 이에 따라 UI 상태 변경 시 자동으로 UI가 업데이트되어 복잡한 상태 관리 코드를 작성할 필요가 줄어듭니다. 그리고 UI 내에서 상태 변경을 직접 관리하지 않기 때문에, 버그 발생의 가능성이 줄어듭니다.&lt;/p&gt;

&lt;p&gt;선언형 UI의 단점은 UI 상태에 따라 UI가 자동으로 업데이트되므로, 개발자가 UI 업데이트를 미세 조정하거나 완전히 제어하는 것이 어려울 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;jetpack-compose란&quot;&gt;Jetpack Compose란?&lt;/h2&gt;

&lt;p&gt;Jetpack Compose는 Android 앱을 개발하기 위한 현대적인 선언형 UI 프레임워크로, 명령형으로 UI를 조작하지 않고 선언형으로 UI를 렌더링 할 수 있게 해주는 라이브러리입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-10-30/android-jetpack-compose.png&quot; alt=&quot;android-jetpack-compose&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;compose-ui의-장점을-정리해-보자면-아래와-같습니다&quot;&gt;Compose UI의 장점을 정리해 보자면 아래와 같습니다.&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;기존 XML UI와의 호환성&lt;/strong&gt; : 기존 XML UI와 Compose UI를 상호 운용할 수 있습니다.&lt;br /&gt;
참고) &lt;a href=&quot;https://developer.android.com/jetpack/compose/interop/interop-apis?hl=ko&quot;&gt;https://developer.android.com/jetpack/compose/interop/interop-apis?hl=ko&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;작업 컨텍스트 전환 최소화&lt;/strong&gt; : Compose UI는 Kotlin 기반이기 때문에 모든 UI 관련 코드는 Kotlin 디렉토리만 탐색하면 됩니다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;간단한 List UI 개발 방법&lt;/strong&gt; : Compose UI에서 List UI는 기본적으로 LazyList를 사용합니다. 이를 사용 시, Composable로 정의된 함수를 그대로 사용할 수 있기 때문에, 기존 XML 방식에 비해 보일러 플레이트 코드를 최소화시킬 수 있습니다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;선형적인 레이아웃 작성&lt;/strong&gt; : 기존 XML UI에선 중첩 LinearLayout에 대한 성능 저하 이슈로 인해 View 간의 제약 조건을 설정하여 레이아웃을 정의하는 ConstraintLayout 사용을 지향해야만 했습니다. 하지만 Compose UI는 설계 구조상 Row, Column를 중첩시켜도 성능에 큰 지장이 없기 때문에 가독성 좋은 선형적인 레이아웃 코드를 작성할 수 있습니다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;라이브러리 수정 용이&lt;/strong&gt; : 오픈소스 라이선스에서 허용한 범위 내에서 라이브러리 내 Composable 함수 코드를 가져와서 커스터마이징을 쉽게 할 수 있습니다. (ex. Image Picker, Calendar 등)&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;compose-ui의-단점을-정리해-보자면-아래와-같습니다&quot;&gt;Compose UI의 단점을 정리해 보자면 아래와 같습니다.&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;높은 러닝 커브&lt;/strong&gt; : 전통적인 Android UI 개발과는 매우 다른 접근 방식을 사용하기 때문에, 기존의 개발 경험을 가진 사람들도 Compose에 적응하기 위해 시간이 필요합니다. 선언적 UI 패러다임은 명령형 UI 패러다임과 다르게 동작하므로, 이해하고 효과적으로 사용하기 위해서는 새로운 사고 방식을 필요로 합니다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;초기 작업 속도&lt;/strong&gt; : Compose UI를 완벽하게 이해 및 적응하기 전까지는 작업 속도가 낮아질 걸로 예상됩니다. 하지만 적응만 된다면, 기존 XML 작업 속도보다 훨씬 빠른 UI 작업이 가능합니다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;정립되지 않은 아키텍쳐&lt;/strong&gt; : Compose UI의 장점을 극대화하기 위한 아키텍쳐는 현재까지도 개발자간의 많은 토론 주제로 남아있기에, 어떤 설계가 좋은 설계인지에 대한 연구와 고민이 필요합니다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Debug Mode 성능 이슈&lt;/strong&gt; : Debug Mode에서는 Compose UI의 성능이 저하됩니다. (애니메이션이 버벅거리는 현상 등)&lt;br /&gt;
참고) &lt;a href=&quot;https://developer.android.com/jetpack/compose/performance&quot;&gt;https://developer.android.com/jetpack/compose/performance&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;TextField MaxLength, InputFilter 기본 지원 안됨&lt;/strong&gt; : Compose UI의 TextField는 MaxLength 속성과 InputFilter 속성이 기본 지원하지 않기 때문에, 개발자의 커스텀 로직 개발이 필요합니다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;LazyList 스크롤바 기본 지원 안됨&lt;/strong&gt; : List UI의 스크롤바가 기본 지원하지 않기 때문에, 개발자의 커스텀 UI 개발이 필요합니다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;LazyList 스크롤 페이징 버그&lt;/strong&gt; : Android 12 OverScroll Stretch Effect와 LazyList 간의 호환성 버그로 인해 스크롤 페이징 인터랙션이 부자연스러운 결과가 나타납니다. (유저가 스크롤 행위를 종료했을 때만, 다음 페이지가 렌더링되는 현상)&lt;br /&gt;
임시 해결 방안) OverScroll Effect 비활성화&lt;br /&gt;
참고) &lt;a href=&quot;https://issuetracker.google.com/issues/233515751&quot;&gt;https://issuetracker.google.com/issues/233515751&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;jetpack-compose-도입-tip&quot;&gt;Jetpack Compose 도입 Tip&lt;/h2&gt;

&lt;h3 id=&quot;1-compose-ui의-presentation-layer-디자인-패턴은-mvimodel-view-intent-패턴을-채택하는-것이-좋습니다&quot;&gt;1. Compose UI의 Presentation Layer 디자인 패턴은 MVI(Model-View-Intent) 패턴을 채택하는 것이 좋습니다.&lt;/h3&gt;
&lt;p&gt;Google 공식 문서에 따르면 Compose UI는 단방향 데이터 흐름 설계(UDF)를 지향하고 있습니다. 단방향 데이터 흐름(UDF)은 상태는 아래로 이동하고 이벤트는 위로 이동하는 디자인 패턴입니다. 단방향 데이터 흐름을 따라 UI에 상태를 표시하는 컴포저블과 상태를 저장하고 변경하는 앱 부분을 서로 분리할 수 있습니다.&lt;br /&gt;
참고) &lt;a href=&quot;https://developer.android.com/jetpack/compose/architecture?hl=ko&quot;&gt;https://developer.android.com/jetpack/compose/architecture?hl=ko&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;2-compose-관련-라이브러리에-포함된-모든-composable-함수는-프로젝트-내에서-한번-wrapping-처리하여-사용하는-것이-좋습니다&quot;&gt;2. Compose 관련 라이브러리에 포함된 모든 Composable 함수는 프로젝트 내에서 한번 Wrapping 처리하여 사용하는 것이 좋습니다.&lt;/h3&gt;
&lt;p&gt;Compose 관련 라이브러리는 아직까지 실험적이고 불안정한 부분들이 많기 때문에 버전을 올리는 일이 빈번할 것인데, 자칫 파라미터 값들이 상당 부분 변경된다면 해당 함수를 사용한 프로젝트 모든 부분에서 빌드 에러를 낼 수 있기 때문입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;SFRow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modifier&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;horizontalArrangement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Arrangement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Horizontal&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Arrangement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;verticalAlignment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Alignment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Vertical&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Alignment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RowScope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Unit&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;modifier&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;horizontalArrangement&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;horizontalArrangement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;verticalAlignment&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;verticalAlignment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;SFColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modifier&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;verticalArrangement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Arrangement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Vertical&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Arrangement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;horizontalAlignment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Alignment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Horizontal&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Alignment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ColumnScope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Unit&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;modifier&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;verticalArrangement&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;verticalArrangement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;horizontalAlignment&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;horizontalAlignment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;3-유용한-modifier-확장-함수-구현-예제&quot;&gt;3. 유용한 Modifier 확장 함수 구현 예제&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;멀티 터치를 비활성화하는 Modifier 확장 함수&lt;/strong&gt; : Compose UI는 기본적으로 멀티 터치를 허용하기에, 여러개의 버튼을 동시에 누를 수 있습니다. 이를 제한하기 위해 아래 Modifier 확장 함수를 이용하여 전역적인 MaterialTheme 영역 단에서 멀티 터치를 비활성화 처리할 수 있습니다.
    &lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;disableMultiTouch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;composed&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;coroutineScope&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rememberCoroutineScope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;nf&quot;&gt;pointerInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;coroutineScope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;launch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;currentId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1L&lt;/span&gt;
          &lt;span class=&quot;nf&quot;&gt;awaitPointerEventScope&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                  &lt;span class=&quot;nf&quot;&gt;awaitPointerEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PointerEventPass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Initial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;changes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;forEach&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pointerInfo&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt;
                      &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                          &lt;span class=&quot;n&quot;&gt;pointerInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pressed&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1L&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pointerInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;
                          &lt;span class=&quot;n&quot;&gt;pointerInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pressed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;not&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pointerInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
                          &lt;span class=&quot;n&quot;&gt;pointerInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1L&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pointerInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;consume&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                          &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Unit&lt;/span&gt;
                      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
              &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;SFTheme&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nc&quot;&gt;MaterialTheme&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;nc&quot;&gt;SFBox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;modifier&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;disableMultiTouch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;nf&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;일정 시간 동안 연속 클릭/토글 제한하는 Modifier 확장 함수&lt;/strong&gt; : 사용자가 연속적으로 버튼을 클릭하면 서버에 동일한 API를 다수 요청하여 오류를 발생시킬 수 있습니다. (일명 “따닥 이슈”) 이를 해결하기 위해 아래 Modifier 확장 함수를 이용하여 일정 시간 동안 연속 클릭/토글 제한할 수 있습니다.
    &lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;throttleClickable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;throttleTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;interactionSource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableInteractionSource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;indication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Indication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;onClickLabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Unit&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;composed&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;lastClickTimestamp&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;remember&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableStateOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0L&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;coroutineScope&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rememberCoroutineScope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

  &lt;span class=&quot;nf&quot;&gt;clickable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;interactionSource&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;interactionSource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;indication&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;indication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;enabled&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;onClickLabel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;onClickLabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;role&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;onClick&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;currentTimestamp&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;currentTimeMillis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;currentTimestamp&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lastClickTimestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;throttleTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;coroutineScope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;launch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                  &lt;span class=&quot;nf&quot;&gt;withContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Dispatchers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                      &lt;span class=&quot;nf&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
              &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;lastClickTimestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentTimestamp&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;throttleToggleable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;throttleTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;interactionSource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableInteractionSource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;indication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Indication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;onValueChange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Unit&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;composed&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;lastClickTimestamp&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;remember&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableStateOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0L&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;coroutineScope&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rememberCoroutineScope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

  &lt;span class=&quot;nf&quot;&gt;toggleable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;interactionSource&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;interactionSource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;indication&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;indication&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;enabled&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;role&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;onValueChange&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;currentTimestamp&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;currentTimeMillis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;currentTimestamp&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lastClickTimestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;throttleTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;coroutineScope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;launch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                  &lt;span class=&quot;nf&quot;&gt;withContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Dispatchers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                      &lt;span class=&quot;nf&quot;&gt;onValueChange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
              &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;lastClickTimestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentTimestamp&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Figma의 Drop Shadow를 구현하는 Modifier 확장 함수&lt;/strong&gt; : 기존 XML UI에선 Figma의 Drop Shadow를 구현하는 방법이 까다로웠으나, Compose UI에선 아래 Modifier 확장 함수를 이용하여 Figma의 Drop Shadow를 쉽게 구현할 수 있습니다.
    &lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dropShadow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Color&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Black&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;borderRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Dp&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;offsetX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Dp&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;offsetY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Dp&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;blurRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Dp&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spreadRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Dp&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modifier&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modifier&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;drawBehind&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nf&quot;&gt;drawIntoCanvas&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;canvas&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;paint&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Paint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;frameworkPaint&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;paint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;asFrameworkPaint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;spreadPixel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spreadRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toPx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;leftPixel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0f&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spreadPixel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;offsetX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toPx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;topPixel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0f&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spreadPixel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;offsetY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toPx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;rightPixel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;width&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spreadPixel&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;bottomPixel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;height&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spreadPixel&lt;/span&gt;

          &lt;span class=&quot;n&quot;&gt;frameworkPaint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;color&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toArgb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

          &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;blurRadius&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;frameworkPaint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;maskFilter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BlurMaskFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                  &lt;span class=&quot;n&quot;&gt;blurRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toPx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
                  &lt;span class=&quot;nc&quot;&gt;BlurMaskFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Blur&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NORMAL&lt;/span&gt;
              &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

          &lt;span class=&quot;n&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;drawRoundRect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;leftPixel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;top&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;topPixel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rightPixel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;bottom&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bottomPixel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;radiusX&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;borderRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toPx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;radiusY&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;borderRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toPx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;paint&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;paint&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;3-효율적인-compose-ui-테스트-자동화-환경-구축하기-위해선-modifier의-semantics-와-testtag-확장-함수를-활용하는-것이-좋습니다&quot;&gt;3. 효율적인 Compose UI 테스트 자동화 환경 구축하기 위해선 Modifier의 semantics 와 testTag 확장 함수를 활용하는 것이 좋습니다.&lt;/h3&gt;
&lt;p&gt;아래와 같은 코드를 추가한다면, Appium / UIAutomator2 와 같은 UI Inspector에서 뷰에 접근할 때, Tree 구조의 XPath가 아닌 ResourceId로 명시적 접근이 가능하기에 효율적으로 테스트 코드를 작성할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@OptIn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ExperimentalComposeUiApi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;SFTheme&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;MaterialTheme&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;SFBox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;modifier&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;semantics&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testTagsAsResourceId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Composable&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;TestTagExample&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;SFTheme&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;SFButton&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;modifier&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modifier&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fillMaxWidth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;testTag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;btnConfirm&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ButtonType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PRIMARY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ButtonSize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LARGE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;stringResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;confirm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;onClick&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;onConfirmButtonClick&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;

&lt;p&gt;지금까지 저희 키친보드 안드로이드 앱에 Jetpack Compose를 도입했을 당시 고려했던 부분, 그리고 소소한 Jetpack Compose 도입 Tip들을 알아보았습니다.&lt;/p&gt;

&lt;p&gt;특히 안정적이고 익숙했던 XML 방식에 비해 아직까지는 불안정하고 익숙하지 않은 Compose 도입을 앞두고 많은 것들을 고려했기 때문에, Compose의 장점 및 단점 그리고 Compose로 가능한 것과 불가능한 것에 대한 부분들을 자세히 기술하였습니다.&lt;/p&gt;

&lt;p&gt;현재 Compose 도입을 검토 및 고려 중인 분들에게 조금이나마 도움이 되었으면 좋겠습니다.&lt;/p&gt;

&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>기능 테스트 전환 이야기</title>
      <link>https://spoqa.github.io/2023/10/20/functional-testing-converting-story.html</link>
      <pubDate>Fri, 20 Oct 2023 00:00:00 +0000</pubDate>
      <author>남경호</author>
      <guid>/2023/10/20/functional-testing-converting-story</guid>
      <description>&lt;p&gt;안녕하세요. 키친보드 제품팀의 백엔드 프로그래머 남경호입니다.&lt;/p&gt;

&lt;p&gt;2월에 작성한 &lt;a href=&quot;https://spoqa.github.io/2023/02/24/bill-payment-development-story.html&quot;&gt;청구/수납 서비스 개발기&lt;/a&gt;를 이후로 오랜만에 글을 작성하게 되었네요. 이번 글은 지난번과 같은 서비스에 대한 내용이 아닌 좀 더 기술적인 내용을 다루어 보려고 합니다.&lt;/p&gt;

&lt;p&gt;혹시 2022년에 작성했던 &lt;a href=&quot;https://spoqa.github.io/2022/04/15/all-new-server.html&quot;&gt;서버 언어 전환 이야기&lt;/a&gt; 글을 기억하실까요? 저희 백엔드 챕터에서는 언어 전환을 하면서 테스트 코드 작성에 대한 숙련도 문제, 데이터 초기화의 불편함 등과 같은 여러 이유로 인해 통합테스트를 선택하게 되었었고 이로 인해 아래와 같은 이슈들을 겪게 되었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-10-05/all-new-server-jira-issue.png&quot; alt=&quot;all-new-server-jira-issue&quot; title=&quot;JPA 이슈&quot; /&gt;&lt;/p&gt;

&lt;p&gt;그동안 저희 백엔드 챕터는 서비스를 발전 시켜가면서 수많은 테스트 코드를 작성하였고 자연스레 테스트 코드 작성에 대한 숙련도를 높일 수 있게 되었고 기능테스트 작성에 대한 난이도를 어느 정도 감수할 수 있다고 판단했습니다.&lt;/p&gt;

&lt;p&gt;그래서 통합테스트에 대한 단점을 보완하고 좀 더 변경에 대한 안정감을 느끼며 서비스를 개발하기 위해 그동안 묵혀두었던(?) 기능 테스트로의 전환을 아래와 같이 2023년 목표로 설정하고 제품을 개발할 때 틈나는 대로 전환 작업을 진행해 왔습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-10-05/2023-goal.png&quot; alt=&quot;2023-goal&quot; title=&quot;2023년 백엔드 목표&quot; /&gt;&lt;/p&gt;

&lt;p&gt;모두가 노력해 준 덕분에 올해 목표로 했던 기능 테스트로의 전환을 마무리할 수 있게 되었는데요. 그동안 저희가 통합 테스트에서 기능테스트로 왜 전환하게 되었는지, 전환하면서 어떤 이슈들을 겪었는지 소개해 드리고자 합니다.&lt;/p&gt;

&lt;h2 id=&quot;기능-테스트&quot;&gt;기능 테스트?&lt;/h2&gt;

&lt;p&gt;테스트 방법을 이야기할 때 &lt;a href=&quot;https://en.wikipedia.org/wiki/Unit_testing&quot;&gt;단위 테스트&lt;/a&gt;나 &lt;a href=&quot;https://en.wikipedia.org/wiki/Integration_testing&quot;&gt;통합 테스트&lt;/a&gt;는 수많은 서적이나 블로그 글에서 다루어질 만큼 개발자에게 익숙한 테스트 방법일 것으로 생각합니다. 하지만 기능 테스트는 여러 매체에서 언급되지 않을 정도로 익숙한 용어는 아닌 것 같습니다. 그래서 먼저 이 글에서 전반적으로 이야기할 기능 테스트라는 단어를 먼저 정의하고 시작해 볼까 합니다. 테스트는 여러 가지 의미로 해석됩니다. &lt;a href=&quot;https://www.browserstack.com/guide/end-to-end-testing&quot;&gt;E2E(End to End) 테스트&lt;/a&gt;로 보기도 하고 통합 테스트, 혹은 &lt;a href=&quot;https://en.wikipedia.org/wiki/System_testing&quot;&gt;시스템 테스트&lt;/a&gt;와 유사한 테스팅 기법으로 해석되기도 합니다.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Functional_testing&quot;&gt;위키피디아의 기능 테스트&lt;/a&gt;를 보면 아래와 같이 정의되어 있습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;In software development, functional testing is a quality assurance (QA) process and a type of black-box testing that bases its test cases on the specifications of the software component under test. (소프트웨어 개발에서 기능 테스트는 품질 보증(QA) 프로세스이며 테스트 대상 소프트웨어 구성요소의 사양을 바탕으로 테스트 사례를 작성하는 일종의 블랙박스 테스트를 말한다)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;특히, 아래 구문에서 기능 테스트가 E2E 테스트와 구분이 된다고 생각하는데요.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Functional testing does not imply that you are testing a function (method) of your module or class. Functional testing tests a slice of functionality of the whole system. (기능 테스트는 모듈 또는 클래스의 기능(방법)을 테스트한다는 의미가 아니다. 기능 테스트는 전체 시스템의 일부 기능을 테스트한다)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;E2E 테스트는 End to End 즉, 종단 간 테스트를 의미합니다. 보통 실제 사용자 관점에서 테스트를 수행하기 때문에 브라우저 환경에서 테스트하는 &lt;a href=&quot;https://www.selenium.dev/&quot;&gt;Selenium&lt;/a&gt;, &lt;a href=&quot;https://appium.io/&quot;&gt;appium&lt;/a&gt;과 같은 도구를 통해 테스트를 수행합니다. 하지만 저희는 서버의 API 범위에 한정해서 테스트하므로 E2E 테스트라고 보기 어렵다고 생각했습니다.&lt;/p&gt;

&lt;p&gt;한편, 누군가는 통합 테스트와 같은 의미이지 않은가 하고 이야기할 수 있을 것 같습니다. 통합 테스트가 단위 테스트를 거친 여러 모듈을 그룹화하여 테스트를 적용한다는 점과 단위 테스트와 시스템 테스트(혹은 E2E 테스트)의 사이에 진행한다는 점에서 같은 의미로 해석될 수 있다고 생각합니다.&lt;/p&gt;

&lt;p&gt;사실 통합 테스트를 좀 더 나은 테스트 방식으로 변경했다고 봐도 될 것 같긴 합니다. 다만 동일한 통합 테스트라고 명명하기보다 좀 더 명시적으로 구분할 수 있는 테스트 방법을 표현할 수 있는 명칭이 필요하다고 생각했습니다. 그리고 일부 모듈들의 집합을 테스트할 수 있는 통합 테스트에 비해 API의 끝점(Endpoint)에서 블랙박스로 테스트한다는 부분이 좀 더 강조되는 기능 테스트가 저희가 전환하는 목적성에 잘 부합한다고 생각해서 기능 테스트라고 부르기로 하였습니다.&lt;/p&gt;

&lt;h2 id=&quot;왜-기능-테스트로-전환하기로-하였나요&quot;&gt;왜 기능 테스트로 전환하기로 하였나요?&lt;/h2&gt;

&lt;p&gt;서버의 언어 전환을 진행한 이후로 백엔드 챕터에서는 모든 제품의 기능을 개발할 때 단위 테스트와 통합테스트를 작성하도록 하였습니다. 코드 리뷰를 할 때도 동료가 작성한 코드의 스타일 뿐만 아니라 누락된 테스트 케이스가 없는지 체크해 줌으로써 기능적인 결함이 없도록 모두가 노력하였고 그 덕인지 QA에서 발생하는 서버의 버그 비율은 10% 내외로 유지할 수 있게 되었습니다.&lt;/p&gt;

&lt;p&gt;저희는 여기에 만족하지 않고 좀 더 버그 비율을 줄일 방법이 있을지 찾아보았습니다. 백엔드 전체 버그 이슈의 절반은 관리자와 관련된 이슈이고 그 나머지 중 1/3은 불명확한 요구사항으로 인해서, 나머지 1/3은 코드 오류 또는 테스트 케이스 누락, 나머지 1/3은 통합테스트에서 발견하지 못한 버그로 인해 발생한 것이었습니다.&lt;/p&gt;

&lt;p&gt;불명확한 요구사항이나 개발자의 실수로 인한 버그는 백엔드 챕터 외적인 요소이거나 장기적으로 개발자의 실수를 줄일 방안을 찾아나가야 한다고 판단해서 제외하고 마지막 1/3인 통합테스트에서 발견되지 않은 버그로 인한 이슈에 집중하기로 하였는데요. 먼저 통합테스트로 인해 발견하지 못한 대표적인 사례들을 소개해 보면서 왜 통합 테스트에서 버그를 발견하지 못하게 되었는지 소개해 보겠습니다.&lt;/p&gt;

&lt;h3 id=&quot;사례&quot;&gt;사례&lt;/h3&gt;

&lt;h4 id=&quot;hibernate-lazy-loading&quot;&gt;Hibernate Lazy Loading&lt;/h4&gt;

&lt;p&gt;먼저 Hibernate의 Lazy Loading으로 인해 발생한 버그인데요. 구현된 버그가 발생한 코드를 먼저 보고 이야기를 이어가 보겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorProduct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToMany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;product&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ALL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orphanRemoval&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;singleProductBundle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendorProductBundle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableSetOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;productBundle&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;singleProductBundle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;firstOrNull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorProductUpdateData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;unit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unit&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;standard&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;standard&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;erpCode&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;erpCode&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;unitPrice&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unitPrice&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;isMarketPrice&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isMarketPrice&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;syncBundle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;productBundle&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;IllegalStateException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;묶음품목이 없는 품목은 묶음을 동기화할 수 없습니다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;bundleProduct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;productBundle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bundleProduct&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;bundleProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;bundleProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;standard&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;makeBundleStandard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;standard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;productBundle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unitCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;bundleProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;erpCode&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;erpCode&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorProductBundle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;insertable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;updatable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorProduct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorProductService&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendorProductId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorProductUpdateData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorProduct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 1. 품목 조회&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;product&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getOrderableVendorProductById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendorProductId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 2. 품목 데이터 업데이트&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 3. 묶음 품목 조회&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;productBundle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

            &lt;span class=&quot;c1&quot;&gt;// 4. 묶음 품목이 존재하면 동기화&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;syncBundle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Entity로는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;품목&lt;/code&gt;과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;묶음 품목&lt;/code&gt;이 존재하고 양방향 연관관계를 가지고 있는 것을 볼 수 있습니다. 그리고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OrderableVendorProductService#update&lt;/code&gt; 함수를 보시면 ID를 이용하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;품목을 조회&lt;/code&gt;하고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;품목 데이터를 업데이트&lt;/code&gt;하는데 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;묶음 품목이 존재&lt;/code&gt;하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;묶음 품목을 동기화&lt;/code&gt;해 주는 것을 볼 수 있습니다.&lt;/p&gt;

&lt;p&gt;만약 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;품목&lt;/code&gt;에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;묶음 품목&lt;/code&gt;이 존재하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; 함수가 기대했던 대로 동작할까요? 예상하셨겠지만 아쉽게도 기대했던 대로 동작하지 않고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;품목&lt;/code&gt;은 수정하기 전 상태 그대로 존재하게 됩니다. 왜일까요?&lt;/p&gt;

&lt;p&gt;이유는 Hibernate의 Lazy Loding과 영속 메커니즘 때문입니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; 함수가 수행되는 순서대로 차근차근 살펴보겠습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;품목 ID를 이용해서 품목을 조회합니다.&lt;/li&gt;
  &lt;li&gt;조회한 품목을 수정합니다.&lt;/li&gt;
  &lt;li&gt;조회한 품목의 묶음 품목을 조회합니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;묶음 품목&lt;/code&gt;의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FetchType&lt;/code&gt;이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Lazy&lt;/code&gt;이기 때문에 호출 시점에 묶음 품목을 조회합니다. 이때 묶음 품목의 연관관계인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;품목&lt;/code&gt;도 함께 조회됩니다.&lt;/li&gt;
  &lt;li&gt;품목 정보를 동기화합니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;syncBundle&lt;/code&gt; 함수를 보시면 묶음 품목의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;품목&lt;/code&gt; 정보를 조회하여 묶음 품목정보의 데이터를 업데이트해 준다는 것을 볼 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;문제는 3번에서 발생합니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;묶음 품목&lt;/code&gt;에서 조회한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;품목&lt;/code&gt; 정보의 ID는 1번에서 조회한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;품목&lt;/code&gt;의 ID와 동일합니다. (양방향 연관관계를 가졌으니까요!) 그러다 보니 Hibernate의 영속 메커니즘에 따라 캐시 된 Entity 정보를 조회하게 되고 2번에서 수정한 품목 정보는 3번에서 조회한 품목 정보에 의해 덮어씌워지게 되면서 수정한 데이터가 반영되지 않게 되는 것입니다.&lt;/p&gt;

&lt;p&gt;눈으로 직접 확인해 보기 위해 로그를 출력해 보았습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;======== 11111111&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;======== ${product.orderableVendorCatalogProduct}&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;======== 22222222&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;======== ${product.orderableVendorCatalogProduct}&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;productBundle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;======== 33333333&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;======== ${product.orderableVendorCatalogProduct}&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;syncBundle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;======== 44444444&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;======== ${product.orderableVendorCatalogProduct}&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;======== 11111111
======== com.spoqa.cart.domain.orderableVendor.OrderableVendorCatalogProduct@9671a783
======== 22222222
======== null
Hibernate: select s1_0.id,s1_0.bundle_product_id,s1_0.created_at,s1_0.unit_count from orderable_vendor_product_bundle s1_0 where s1_0.id=?
Hibernate: select o1_0.id,o1_0.created_at,o1_0.erp_code,o1_0.is_market_price,o1_0.name,o1_0.orderable_vendor_id,o1_0.orderable_vendor_catalog_product_id,o1_0.standard,o1_0.unit,o1_0.unit_price,o1_0.updated_at,o1_0.vat_included from orderable_vendor_product o1_0 where o1_0.id in(?,?)
======== 33333333
======== com.spoqa.cart.domain.orderableVendor.OrderableVendorCatalogProduct@9671a783
======== 44444444
======== com.spoqa.cart.domain.orderableVendor.OrderableVendorCatalogProduct@9671a783
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;2번 로그를 보시면 분명 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orderableVendorCatalogProduct&lt;/code&gt;를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;로 업데이트하였음에도 불구하고 3번 로그에서 다시 조회됨을 볼 수 있습니다.&lt;/p&gt;

&lt;h4 id=&quot;hibernate-flushing&quot;&gt;Hibernate Flushing&lt;/h4&gt;

&lt;p&gt;Hibernate와 관련해서 좀 더 단순한 다른 이슈를 더 보겠습니다. 이번에는 Hibernate의 Flushing 메커니즘과 관련한 이슈입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Reconciliation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;transactionDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;transactionDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transactionDate&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ReconciliationWriter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PaymentTransactionComparisonResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Reconciliation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;transactionDate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getTransactionDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 1. 거래일 기준 대사 삭제&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;reconciliationRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deleteByTransactionDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transactionDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;reconciliation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createReconciliation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transactionDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 2. 대사 저장&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;reconciliationRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reconciliation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 코드를 보시면 코드상으로는 문제가 없어 보입니다. 거래일 기준으로 대사 정보를 삭제하고 해당 거래일 기준으로 대사를 다시 생성한 후 저장합니다. 기존 데이터를 삭제하고 새로운 데이터를 생성하기 때문에 거래일이 유일 제약조건이 걸려있어도 문제없어 보입니다. 통합테스트에서도 정상적으로 동작합니다. 하지만 실제로 서버를 실행해서 기능을 수행해 보면 아래와 같이 오류가 발생합니다.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;58144180 [scheduling-1] ERROR:
ERROR: duplicate key value violates unique constraint &quot;reconciliation_transaction_date_uk&quot;
 Detail: Key (transaction_date)=(2023-02-01) already exists.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;데이터를 삭제하고 저장했는데 왜 유일키 제약조건 위반 오류가 발생할까요? 이유는 Hibernate의 Flushing 메커니즘 때문입니다.&lt;/p&gt;

&lt;p&gt;Hibernate의 &lt;a href=&quot;https://docs.jboss.org/hibernate/orm/current/javadocs/org/hibernate/event/internal/AbstractFlushingEventListener.html#performExecutions(org.hibernate.event.spi.EventSource)&quot;&gt;AbstractFlushingEventListener#performExecutions&lt;/a&gt;의 동작 방식을 보면 아래와 같이 적혀있습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Execute all SQL (and second-level cache updates) in a special order so that foreign-key constraints cannot be violated:&lt;/p&gt;
  &lt;ol&gt;
    &lt;li&gt;Inserts, in the order they were performed&lt;/li&gt;
    &lt;li&gt;Updates&lt;/li&gt;
    &lt;li&gt;Deletion of collection elements&lt;/li&gt;
    &lt;li&gt;Insertion of collection elements&lt;/li&gt;
    &lt;li&gt;Deletes, in the order they were performed&lt;/li&gt;
  &lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;그래서 위에 적힌 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ReconciliationWriter#write&lt;/code&gt;함수는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2. 대사 저장&lt;/code&gt; 후 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1. 거래일 기준 대사 삭제&lt;/code&gt;를 수행하는 순서로 실행되게 됩니다. 그래서 저장을 먼저 수행하고 데이터를 삭제하니 유일키 제약조건을 위반하는 것입니다.&lt;/p&gt;

&lt;h4 id=&quot;transactional-event-listener&quot;&gt;Transactional Event Listener&lt;/h4&gt;

&lt;p&gt;이번에는 Hibernate가 아닌 SpringFramework에서 제공하는 &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html&quot;&gt;TransactionalEventListener&lt;/a&gt;로 인해 발생했던 이슈를 살펴보겠습니다.&lt;/p&gt;

&lt;p&gt;아래 코드는 유통사 계정을 생성하는 Facade의 구현 코드입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorAccountFacade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createAccount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreateOrderableVendorAccountFacadeData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorAccount&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 1. 유통사 계정을 생성합니다&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;createdAccount&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderableVendorAccountService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createAccount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;createOrderableVendorAccountData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 2. 샌드버드 사용자를 생성합니다&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chatClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createOrderableVendorAccountChatUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;createdAccount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 3. 샌드버드 사용자 ID를 생성한 유통사 계정에 업데이트 합니다&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;orderableVendorAccountService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;updateSendbirdUserId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;orderableVendorAccountId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;createdAccount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;newSendbirdId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;createdAccount&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorAccountService&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createAccount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreateOrderableVendorAccountData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorAccount&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 1.1. 유통사 계정을 생성합니다&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderableVendorAccountRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;also&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;c1&quot;&gt;// 1.2. 유통사 계정 생성 이벤트를 발행합니다&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;eventPublisher&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;publishEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendorAccountCreatedEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ChatEventHandler&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@TransactionalEventListener&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorAccountCreatedEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 1.2.1. 유통사 계정 생성 이벤트를 받아 유통사의 모든 채팅방에 계정을 초대합니다&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;chatClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;inviteOrderableVendorAllChannels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ChatClient&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;inviteOrderableVendorAllChannels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorAccount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderChannels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;forEach&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// 1.2.1.1. 모든 주문채널에 계정을 초대합니다&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;queueMessageSender&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;inviteSendbirdChannelUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;SendbirdInviteChannelUserQueuePayload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;channelUrl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SendbirdGroupChannelInvitePayload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                        &lt;span class=&quot;n&quot;&gt;userIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sendbirdId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;queueMessageSender&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;inviteSendbirdChannelUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// 1.2.1.2. 문의 채널에 계정을 초대합니다.&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;SendbirdInviteChannelUserQueuePayload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;channelUrl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;INQUIRY_${account.orderableVendor.id}&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SendbirdGroupChannelInvitePayload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;userIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sendbirdId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;클래스가 분리되어 있다 보니 코드의 실행 순서를 따라가기 힘드실 거라 생각되어 위에 적힌 계정 생성 기능의 코드 순서를 아래와 같이 정리해 보았습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;유통사 계정을 생성합니다. (1.)&lt;/li&gt;
  &lt;li&gt;유통사 계정을 생성합니다. (1.1.)&lt;/li&gt;
  &lt;li&gt;유통사 계정 생성 이벤트를 발행합니다. (1.2.)&lt;/li&gt;
  &lt;li&gt;유통사 계정 생성 이벤트를 받아 유통사의 모든 채팅방에 계정을 초대합니다. (1.2.1.)&lt;/li&gt;
  &lt;li&gt;모든 주문 채널에 계정을 초대합니다. (1.2.1.1.)&lt;/li&gt;
  &lt;li&gt;문의 채널에 계정을 초대합니다. (1.2.1.2.)&lt;/li&gt;
  &lt;li&gt;샌드버드 사용자를 생성합니다. (2.)&lt;/li&gt;
  &lt;li&gt;샌드버드 사용자 ID를 생성한 유통사 계정에 업데이트합니다. (3.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;한날 어느 개발자가 유통사 계정을 생성하는 코드를 리펙터링하는 도중 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ChatEventHandler#handle&lt;/code&gt; 함수에 기재된 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@TransactionalEventListener&lt;/code&gt;를 모종의 이유로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@EventListener&lt;/code&gt;로 변경하게 되었습니다. 코드를 변경하고 나니 5~6번 항목인 채팅방에 계정을 초대하는 부분에서 오류가 발생하였는데요.&lt;/p&gt;

&lt;p&gt;이유를 살펴보니 아래와 같이 사용자 계정의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sendbirdId&lt;/code&gt;가 null 값이라 발생한 오류였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;SendbirdGroupChannelInvitePayload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;userIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sendbirdId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// &amp;lt;--- NullPointerException 발생&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;구현 코드를 변경한 게 아닌데도 단순히 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@TransactionalEventListener&lt;/code&gt;에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@EventListener&lt;/code&gt;로 변경했음에도 불구하고 왜 이런 오류가 발생하였을까요?&lt;/p&gt;

&lt;p&gt;코드의 구현 순서가 아닌 실제 동작하는 순서를 다시 한번 적어보겠습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;유통사 계정을 생성합니다. (1.)&lt;/li&gt;
  &lt;li&gt;유통사 계정을 생성합니다. (1.1.)&lt;/li&gt;
  &lt;li&gt;유통사 계정 생성 이벤트를 발행합니다. (1.2.)&lt;/li&gt;
  &lt;li&gt;샌드버드 사용자를 생성합니다. (2.)&lt;/li&gt;
  &lt;li&gt;샌드버드 사용자 ID를 생성한 유통사 계정에 업데이트 합니다. (3.)&lt;/li&gt;
  &lt;li&gt;유통사 계정 생성 이벤트를 받아 유통사의 모든 채팅방에 계정을 초대합니다. (1.2.1.)&lt;/li&gt;
  &lt;li&gt;모든 주문채널에 계정을 초대합니다. (1.2.1.1.)&lt;/li&gt;
  &lt;li&gt;문의 채널에 계정을 초대합니다. (1.2.1.2.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;앞서 소개해 드린 순서와 조금 다른 부분을 볼 수 있을 텐데요. 3번 항목인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;유통사 계정을 생성한 이벤트를 발행&lt;/code&gt;한 후 곧바로 이벤트를 소비하는 것이 아닌 7번 항목이었던 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;샌드버드 사용자 생성&lt;/code&gt;과 8번 항목이었던 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;샌드버드 사용자 ID를 생성한 유통사 계정에 업데이트&lt;/code&gt;하는 항목을 먼저 실행한 후 유통사 계정 생성 이벤트를 소비한다는 것을 알 수 있습니다.&lt;/p&gt;

&lt;p&gt;원인은 바로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@TransactionalEventListener&lt;/code&gt;의 동작 방식 때문입니다. &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html&quot;&gt;TransactionalEventListener 문서&lt;/a&gt;를 보면 아래와 같은 내용을 볼 수 있습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;… If a transaction is running, the event is handled according to its TransactionPhase.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;즉 이벤트 리스너의 함수에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@TransactionalEventListener&lt;/code&gt;를 정의하면 해당 리스너의 함수는 정해진 Transaction 단계에 따라 수행되며 위 예시 코드에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OrderableVendorAccountFacade#createAccount&lt;/code&gt; 함수에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Transactional&lt;/code&gt;이 선언되어 있으므로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;createAccount&lt;/code&gt; 함수가 끝난 시점에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ChatEventHandler#handle&lt;/code&gt;함수가 실행되는 것입니다.&lt;/p&gt;

&lt;p&gt;그래서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ChatEventHandler#handle&lt;/code&gt; 함수의 어노테이션을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@EventListener&lt;/code&gt;로 바꿔주면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;샌드버드 사용자를 생성&lt;/code&gt;하고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;샌드버드 사용자 ID를 생성한 유통사 계정에 업데이트&lt;/code&gt;하는 로직이 실행되기 전에 이벤트가 소비되므로 유통사 계정의 샌드버드 ID를 조회하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;account.sendbirdId!!&lt;/code&gt; 코드에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NullPointerException&lt;/code&gt;이 발생하게 되는 것입니다.&lt;/p&gt;

&lt;h3 id=&quot;원인&quot;&gt;원인&lt;/h3&gt;

&lt;p&gt;통합테스트에서 검출되지 못한 버그로 인한 이슈는 위에서 소개해 드린 사례 말고도 많은데요. 대부분 Hiberante + Transaction 또는 Client의 Mocking으로 인해 발생한 이슈들로 모을 수 있었습니다.&lt;/p&gt;

&lt;p&gt;통합테스트를 위해 저희는 테스트 간의 데이터를 손쉽게 격리하고 Mock Bean들을 원활하게 생성하기 위해 아래와 같이 테스트를 위한 서버를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MOCK&lt;/code&gt; 모드로 실행하고 Transaction 내에서 실행되도록 설정해 두었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@SpringBootTest&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Import&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;TestDatabaseConfiguration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;Fixture&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;ApplicationEventPublisherSpyConfiguration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@AutoConfigureMockMvc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;IntegrationTestBase&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BehaviorSpec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@MockkBean&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;s3BucketClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;S3BucketClient&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@MockkBean&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;chatClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ChatClient&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그러다 보니 Transaction 밖에서의 Hibernate의 실행 동작을 테스트 환경에서 온전히 재현하기 어려웠습니다. 그리고 모든 모듈에 대한 단위테스트가 되어있다 보니 통합테스트를 좀 더 편하게 하기 위해 API들을 추상화한 Client 클래스들을 Mocking 하였는데요. 이로 인해 위 사례와 같이 실제 동작에서 발견할 수 있는 버그를 발견하지 못하는 사례가 생기게 된 것입니다.&lt;/p&gt;

&lt;p&gt;그래서 기능 테스트에서는 더 이상 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MOCK&lt;/code&gt; 모드로 실행하는 것이 아닌 내장된 서버를 이용하여 테스트를 실행할 수 있도록 하고 MockBean 들을 모두 제거하여 테스트를 수행할 수 있도록 아래와 같이 베이스 클래스를 설정하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Import&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;FunctionalTestConfig&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@SpringBootTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;webEnvironment&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SpringBootTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;WebEnvironment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;RANDOM_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FunctionalTestBase&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FunSpec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;extensions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SpringExtension&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;mockServer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ClientAndServer&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;suspend&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;beforeSpec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Configuration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;configuration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;logLevel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Level&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;WARN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mockServer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ClientAndServer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;startClientAndServer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;configuration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;SENDBIRD_API_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;SLACK_API_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;DATA_GOV_API_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;NCLOUD_SENS_API_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;NICEPAY_WEB_API_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;NICEPAY_DATA_API_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;KAKAO_API_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;suspend&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;afterSpec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mockServer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;stop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;어떻게-전환-작업을-하였나요&quot;&gt;어떻게 전환 작업을 하였나요?&lt;/h2&gt;

&lt;p&gt;앞선 사례를 통해 통합테스트 환경에서 발견하지 못한 버그들을 살펴보았는데요. 본격적으로 기능 테스트로의 전환 이야기로 넘어가 보겠습니다. 테스트 코드를 보여주는 것은 통합 테스트에서나 기능 테스트에서나 큰 틀에서는 차이가 없을 것이므로 어떠한 전략과 방법을 사용하여 기능 테스트를 수행하였는지 이야기해 보는 게 좋을 것 같습니다.&lt;/p&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;&lt;img src=&quot;/images/2023-10-05/functional-testing-guide.png&quot; alt=&quot;functional-testing-guide&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;테스트-케이스&quot;&gt;테스트 케이스&lt;/h3&gt;

&lt;p&gt;아래 그림은 &lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000201055864&quot;&gt;이펙티브 소프트웨어 테스팅&lt;/a&gt;에서 소개된 테스트 피라미드입니다. 피라미드 상단으로 올라갈수록 복잡도는 올라가지만, 현실성이 높아짐을 볼 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-10-05/test-pyramid.jpg&quot; alt=&quot;test-pyramid&quot; /&gt;&lt;/p&gt;

&lt;p&gt;서버 언어를 전환하며 채택했던 테스트 전략에서와 같이 저희는 복잡한 비즈니스 요구사항에 대한 다양한 케이스들은 단위 테스트에서 모두 다루고 주요하거나 단위 테스트에서 발견하기 힘들다고 판단되는 케이스에 대해서 기능테스트를 작성하는 방식을 도입하였습니다.&lt;/p&gt;

&lt;p&gt;즉, 보다 단순하게 테스트할 수 있는 단위 테스트에서 대부분의 비즈니스 로직을 테스트하고 실제 환경과 가장 유사하지만 테스트하기에 복잡한 기능 테스트에서는 전체적인 기능이 잘 수행되는지 혹은 단위 테스트만으로 불안하다고 판단되는 부분을 확인하기 위한 테스트 코드를 작성하였습니다.&lt;/p&gt;

&lt;p&gt;아래는 정산 데이터를 생성하는 기능에 대한 단위 테스트와 기능 테스트의 테스트 케이스입니다. 단위 테스트는 각 모듈별로 여러 테스트 케이스가 존재하는 것을 볼 수 있지만 기능 테스트는 주요 기능에 대한 테스트 케이스만 존재하는 것을 볼 수 있습니다.&lt;/p&gt;

&lt;h4 id=&quot;정산-데이터-생성-단위-테스트들-중-테스트-케이스-일부&quot;&gt;정산 데이터 생성 단위 테스트들 중 테스트 케이스 일부&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-10-05/settlement-unit-testing.png&quot; alt=&quot;ettlement-unit-testing&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-10-05/calculator-unit-testing.png&quot; alt=&quot;calculator-unit-testing&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;주문서-생성-기능-테스트-케이스&quot;&gt;주문서 생성 기능 테스트 케이스&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-10-05/settlement-functional-testing.png&quot; alt=&quot;settlement-functional-testing&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;mockserver&quot;&gt;MockServer&lt;/h3&gt;

&lt;p&gt;통합 테스트에서 기능 테스트로 전환하는 여러 이유 중 하나가 바로 Client를 Mocking 함으로써 발견하지 못한 버그의 존재였습니다. 그래서 기능 테스트에서는 최대한 외부 API를 그대로 사용하고자 하였는데요. AWS와 같이 저희가 관리하는 외부 API는 테스트를 위한 인프라를 구성해 둘 수 있었지만 그렇지 못한 외부 API도 존재하였습니다.&lt;/p&gt;

&lt;p&gt;그렇다고 이전과 같이 Client를 그대로 Mocking 하는 것은 좋지 않다고 생각했는데요. 그래서 외부 API를 최대한 유사하게 사용하는 환경을 구성할 수 있는 &lt;a href=&quot;https://www.mock-server.com/&quot;&gt;MockServer&lt;/a&gt;를 활용하기로 하였습니다. 기능 테스트에서 MockServer를 사용하는 것에 대해 다소 논란이 있을 수 있겠지만 저희는 MockServer를 사용하게 되면서 아래와 같이 실제 외부 API를 사용할 때 발생할 수 있는 문제점을 해결할 수 있다는 부분이 더 매력적으로 다가와 MockServer를 채택하게 되었습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;만약 외부 API가 다운된다면 테스트를 할 수 없음&lt;/li&gt;
  &lt;li&gt;외부 리소스를 생성하거나 수정하는 경우 외부 API로 검증하지 못하는 상황이 있음&lt;/li&gt;
  &lt;li&gt;외부 API에 데이터를 전달하기 위한 사전 조건이 너무 방대한 경우 혹은 불가능한 경우&lt;/li&gt;
  &lt;li&gt;외부 API에 테스트 데이터를 함부로 넣으면 안 되는 경우&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;사실 MockServer를 사용함으로써 &lt;a href=&quot;https://en.wikipedia.org/wiki/Black-box_testing&quot;&gt;블랙박스 테스트&lt;/a&gt;의 장점을 많이 상쇄시킨다는 부분이 마음에 걸렸는데요. 여러 고민을 해본 결과 블랙박스 테스트의 장점을 상쇄시키는 것이 외부 API를 복잡하게 사용함으로써 기능 테스트 코드 작성에 어려움을 겪는 것보다 낫다고 판단해서 결국 MockServer를 사용하기로 하였습니다. 다만, 테스트 코드에서 Mocking 부분을 최대한 추상화된 함수로 사용함으로써 테스트 코드를 복잡하지 않게 사용하도록 노력하였습니다.&lt;/p&gt;

&lt;p&gt;한편, 왜 많은 Mock 서버 라이브러리 중 MockServer를 선택하였는지 궁금하실 수도 있겠는데요. MockServer를 선택한 이유는 단순히 저희가 이미 사용 중인 테스트 프레임워크인 &lt;a href=&quot;https://kotest.io/docs/extensions/mockserver.html&quot;&gt;Kotest에서 확장 기능&lt;/a&gt;을 제공해 주었기 때문입니다. 또한 저희는 Kotest의 가이드 문서와 같이 코드를 작성하지는 않고 최대한 기능 테스트 코드에서 Mocking에 대한 내용을 숨기기 위해 Base 클래스에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MockServer&lt;/code&gt; 및 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MockClient&lt;/code&gt;를 생성하고 Helper 클래스를 통해 Mocking 코드를 최대한 추상화하여 사용하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Import&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;FunctionalTestConfig&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@SpringBootTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;webEnvironment&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SpringBootTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;WebEnvironment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;RANDOM_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FunctionalTestBase&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FunSpec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;extensions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SpringExtension&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;mockServer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ClientAndServer&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;suspend&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;beforeSpec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Configuration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;configuration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;logLevel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Level&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;WARN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mockServer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ClientAndServer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;startClientAndServer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;configuration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;SENDBIRD_API_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;SLACK_API_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;NCLOUD_SENS_API_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@TestConfiguration&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FunctionalTestConfig&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sendbirdApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MockServerClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;localhost&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SENDBIRD_API_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;slackApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MockServerClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;localhost&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SLACK_API_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ncloudSensApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MockServerClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;localhost&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NCLOUD_SENS_API_PORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;sendbirdApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MockServerClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;slackApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MockServerClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ncloudSensApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MockServerClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Mockery&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;sendbirdApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;slackApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;ncloudSensApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createSendbirdUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sendbirdApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whenWithDefault&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;HttpRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withMethod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpMethod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withPath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/v3/users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;respond&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;HttpTemplate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;TemplateType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MUSTACHE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
            {
                &quot;statusCode&quot;: 200,
                &quot;body&quot;: {
                    &quot;user_id&quot;: &quot;{{#jsonPath}}$.user_id{{/jsonPath}}{{jsonPathResult}}&quot;
                }
            }
            &quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trimIndent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;verifyCreateSendbirdUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sendbirdApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;verify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;HttpRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withMethod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpMethod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withPath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/v3/users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withBody&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
                    {
                      &quot;user_id&quot;: &quot;$userId&quot;
                    }
                    &quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trimIndent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
                    &lt;span class=&quot;nc&quot;&gt;MatchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ONLY_MATCHING_FIELDS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;


&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;매장과 유통사를 연결하면 샌드버드 계정이 생성된다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createSendbirdUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clientBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;점주&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;토큰&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;executeQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;extractValueAsObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;connectOrderableVendor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;typeRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ConnectedOrderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actualStore&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;단일&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;매장&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;조회&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;actualStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;managersSendbirdIds&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldHaveSize&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;managerSendbirdId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actualStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;managersSendbirdIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;eventually&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;verifyCreateSendbirdUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;managerSendbirdId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;test-helper-클래스&quot;&gt;Test Helper 클래스&lt;/h3&gt;

&lt;p&gt;앞에서도 언급하였지만, 테스트를 작성하다 보면 기능을 수행하기 위한 값이나 상태를 만들기 위한 코드들이 필요합니다. 특히 기능 테스트에서는 단위 테스트에 비해 준비 코드들이 상당히 필요할 수 있는데요. 이러한 코드들을 모든 테스트에 하나하나 작성해 두기보다 Helper 클래스를 만들어서 사용하면 반복적인 코드를 상당히 줄일 수 있습니다. 그리고 의미 있는 함수명을 사용한다면 좀 더 읽기 쉬운 테스트 코드를 작성할 수 있기도 합니다.&lt;/p&gt;

&lt;p&gt;아래 코드를 보시면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;testHelper&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mockery&lt;/code&gt;라는 변수로 사용되는 모듈을 볼 수 있을 텐데요. 해당 모듈이 테스트를 위한 데이터를 생성해 주거나 검증을 위한 데이터를 가져오는 역할을 수행해줍니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;유통사 토큰으로 대량 메시지를 발송을 호출하면 대량 메시지 발송 이력이 저장되고, 메시지가 발송된다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderableVendor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;가능한&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;거래처&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderableVendorAccount&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;가능한&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;거래처&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;계정&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;store&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;매장&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;점주&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;관리&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;매장&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;추가&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;매장&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;가능&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;유통사&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;연결&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SendBulkChatInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;imageUrl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;imageUrl&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;storeIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;variables&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mapOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;input&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clientBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;가능한&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;거래처&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;토큰&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendorAccount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;executeQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;extractValueAsObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;sendBulkChat&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;typeRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SendBulkChat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;())&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;assertSoftly&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldBe&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;content&quot;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;imageUrl&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldBe&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;imageUrl&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;expectedStore&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;단일&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;매장&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;조회&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;expectedGroupChannelUrl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expectedStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getOrderChannelUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;eventually&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;verifySendUserMessageToGroupChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;groupChannelUrl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expectedGroupChannelUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;customType&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ORDERABLE_VENDOR_ANNOUNCEMENT&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;sendbirdId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderableVendorAccount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sendbirdId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;만약 Helper 클래스가 없다면 위에서 사용한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;주문_가능한_거래처_생성&lt;/code&gt;함수와 같은 코드를 매번 작성해 주어야 해 상당한 코드 중복이 발생할 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;주문&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;가능한&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;거래처&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;facade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getBean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderableVendorFacade&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;java&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorCreationData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;deliverableDayOfWeek&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deliverableDayOfWeek&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;erpConfiguration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ErpConfigurationCreateData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;erpType&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ErpType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NOT_SUPPORTED&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;billPaymentConfigurationCreateData&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BillPaymentConfigurationCreateData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;usable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;accountNumber&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;bank&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;accountHolder&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;virtualAccountBank&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;depositedMethod&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DepositMethod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BY_ORDERABLE_VENDOR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;feeRules&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;ecountErpConfigurationCreateData&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EcountErpConfigurationCreateData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;usable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;companyCode&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;apiCertKey&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;warehouseCode&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;businessInfo&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorBusinessInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;regNum&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;regNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;businessName&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;businessName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;businessType&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;businessType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;businessCondition&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;businessCondition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;businessAddress&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;businessAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;representative&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;representative&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;storageAddress&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storageAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;establishmentDate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;establishmentDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;officials&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;officials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;authenticationAttachments&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authenticationAttachments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;productInfo&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorProductInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;majorTradeStoreCategories&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;majorTradeStoreCategories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;majorProductCategories&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;majorProductCategories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;mainProducts&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mainProducts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;deliveryInfo&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorDeliveryInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;nextDayDeliveryDeadline&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nextDayDeliveryDeadline&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;regions&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deliveryRegions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;preferredRegions&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;preferredDeliveryRegions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;methods&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deliveryMethods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;deliveryTimeRange&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deliveryTimeRange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;paymentInfo&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorPaymentInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;availableMethods&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;availablePaymentMethods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;intervals&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;paymentIntervals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;minimumOrder&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;minimumOrder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;inquiryInfo&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderableVendorInquiryInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;chatInquirable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chatInquirable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;newStoreExtendable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newStoreExtendable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;inqurableTime&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;inqurableTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;inqurableDayOfWeek&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;inqurableDayOfWeek&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;memo&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;memo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;orderChannelWelcomeMessage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderChannelWelcomeMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;matchingEnabled&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;matchingEnabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;facade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createOrderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;테스트를-위한-open-entitymanager-in-view&quot;&gt;테스트를 위한 Open EntityManager in View&lt;/h3&gt;

&lt;p&gt;기능 테스트를 수행하다 보면 리소스를 생성하는 API를 수행한 후 주어진 데이터로 리소스가 잘 생성되었는지 검증하기 위해 단언(Assertion) 시 데이터를 조회하게 됩니다. 조회 API가 구현되어 있다면 해당 API를 사용하면 가장 이상적이겠지만 조회 API가 구현되어 있지 않는 경우에는 어쩔 수 없이 Service나 Repository를 이용하여 Entity를 조회해야 하는 경우가 발생합니다.&lt;/p&gt;

&lt;p&gt;이때 검증을 위해 조회한 Entity의 연관관계를 조회하면 아래와 같은 오류를 만나게 되는 경우가 있습니다. (Hibernate에 한정된 이슈입니다)&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;재무 담당자 권한과 거래일이 주어지면 대사 정보를 생성한다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 사전 데이터 생성&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;clientBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;재무&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;관리자&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;토큰&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;executeQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;extractValueAsObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;reconcile&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;typeRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Reconcile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;expected&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;단일&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;대사&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;조회&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transactionDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;assertSoftly&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transactionDate&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldBe&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transactionDate&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;estimatedSettlementDate&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldBe&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2023&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldBe&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ReconciliationState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SUCCESS&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;totalTransactionCount&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldBe&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;           &lt;span class=&quot;c1&quot;&gt;// &amp;lt;------ 연관관계 조회 시 오류 발생&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;preVendorSettlements&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldHaveSize&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;totalTransactionAmount&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldBe&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;25000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toBigDecimal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;failed to lazily initialize a collection of role: com.spoqa.cart.domain.reconciliation.Reconciliation._transactions: could not initialize proxy - no Session
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.spoqa.cart.domain.reconciliation.Reconciliation._transactions: could not initialize proxy - no Session
	at org.hibernate.collection.spi.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:635)
	at org.hibernate.collection.spi.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218)
	at org.hibernate.collection.spi.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:148)
	at org.hibernate.collection.spi.PersistentBag.size(PersistentBag.java:350)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이유는 Transaction 밖에서 Lazy 하게 연관관계를 조회하려고 하면서 발생한 오류인데요. (좀 더 자세히 알고 싶으시다면 &lt;a href=&quot;https://www.baeldung.com/hibernate-initialize-proxy-exception&quot;&gt;Hibernate could not initialize proxy – no Session&lt;/a&gt;글을 봐주세요) 테스트로 인해서 구현된 운영 코드를 바꿀 수 없으므로 테스트 코드에서 무언가 조치를 해야 할 필요가 있었습니다.&lt;/p&gt;

&lt;p&gt;해당 이슈에 대한 해결 방법으로 생각해 낸 것이 바로 OSIV(Open Session in View)로 알려진 Spring의 &lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#data.sql.jpa-and-spring-data.open-entity-manager-in-view&quot;&gt;Open EntityManager in View&lt;/a&gt;입니다.&lt;/p&gt;

&lt;p&gt;Spring으로 Web Application을 개발하시는 개발자라면 OSIV(Open Sesison in View)에 대해 잘 아실 것으로 생각합니다. OSIV 패턴은 Spring MVC에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OpenEntityManagerInViewInterceptor&lt;/code&gt;에 의해 적용되어야 하는데요. 해당 클래스를 참고해서 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TestHelper&lt;/code&gt; 클래스에서 Entity를 조회한 후 연관관계를 사용할 때 오류가 발생하지 않도록 조치하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;AnnotationTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CLASS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;annotation&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OpenEntityManager&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Aspect&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OpenEntityManagerAspect&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EntityManagerFactoryAccessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Around&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@within(com.spoqa.cart.fixture.OpenEntityManager).*(..)&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;openEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pjp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProceedingJoinPoint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;debug&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Opening JPA EntityManager&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;emf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EntityManagerFactory&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;obtainEntityManagerFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;em&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EntityManager&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;emHolder&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EntityManagerHolder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;em&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;TransactionSynchronizationManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bindResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;emf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;emHolder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PersistenceException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DataAccessResourceFailureException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Could not create JPA EntityManager&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pjp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;proceed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;loadAssociations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;finally&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;emHolder&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TransactionSynchronizationManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;unbindResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;emf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EntityManagerHolder&lt;/span&gt;

            &lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;debug&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Closing JPA EntityManager&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;nc&quot;&gt;EntityManagerFactoryUtils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;closeEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;emHolder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;loadAssociations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;declaredMemberProperties&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;forEach&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@TestComponent&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@OpenEntityManager&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestHelper&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;단일&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;대사&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;조회&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transactionDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Reconciliation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;facade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getBean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ReconciliationFacade&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;java&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;facade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;reconcile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transactionDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OpenEntityManagerAspect &lt;/code&gt;클래스를 보시면 Spring의 AOP를 사용해서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@OpenEntityManager&lt;/code&gt; 어노테이션이 선언된 클래스의 모든 함수에 OSIV를 적용한다는 것을 볼 수 있습니다. 특히 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadAssociations&lt;/code&gt; 함수를 보면 조회한 Entity의 모든 연관관계를 조회한다는 것을 볼 수 있는데요. 그 이유는 테스트를 수행하는 함수에서는 Transaction이 실행 중이지 않기 때문에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TestHelper&lt;/code&gt;의 함수에서 모든 연관관계를 먼저 조회하여 테스트 함수에서 연관관계 조회 시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LazyInitializationException&lt;/code&gt;이 발생하지 않도록 하기 위함이었습니다.&lt;/p&gt;

&lt;p&gt;이렇게 조치하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Transactional&lt;/code&gt;이 선언되어 있지 않은 테스트 코드에서도 운영 코드를 변경하지 않고 매번 Helper 클래스에서 연관관계를 명시적으로 조회하지 않고도 Entity의 연관관계를 손쉽게 조회할 수 있어 테스트 코드를 좀 더 손쉽게 작성할 수 있다는 장점이 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;기능-테스트에서의-단언assertion&quot;&gt;기능 테스트에서의 단언(Assertion)&lt;/h3&gt;

&lt;p&gt;통합 테스트에서 기능 테스트로 전환 시 가장 걱정했던 부분이 바로 테스트 케이스 간의 데이터 공유로 인한 간섭이었습니다. 테스트 코드를 작성하는 데 하나의 테스트 케이스가 다른 테스트 케이스에 영향을 받지 않도록 하는 것이 이상적이지만 CI 환경에서 효율적이고 빠르게 테스트를 수행하기 위해서는 어쩔 수 없이 데이터베이스나 Message Queue와 같은 자원들은 공유할 수밖에 없었습니다.&lt;/p&gt;

&lt;p&gt;그러다 보니 통합 테스트에서는 Transaction을 테스트마다 실행시켜서 테스트 종료 후 Rollback 하는 형태로 테스트간 간섭을 회피하였는데요. 기능 테스트에서는 테스트 케이스 함수에서 Transaction 사용으로 인한 문제점을 해결하려 하였기 때문에 통합 테스트의 방식을 사용할 수 없었습니다. 매 테스트 코드가 실행될 때마다 데이터베이스를 초기화해 주는 스크립트를 실행시켜 보자는 의견도 나왔었지만, 테스트가 실행될 때 준비 시간이 너무 늘어나는 이슈로 인해 해당 방법도 사용할 수 없었습니다.&lt;/p&gt;

&lt;p&gt;결국, 기능 테스트에서는 데이터베이스나 Message Queue를 공유하되 각 테스트 케이스의 단언을 아래와 같이 다른 테스트 케이스에 영향을 받지 않게끔 작성하도록 하였습니다.&lt;/p&gt;

&lt;h4 id=&quot;통합-테스트-단언-예시&quot;&gt;통합 테스트 단언 예시&lt;/h4&gt;

&lt;p&gt;통합 테스트에서는 각 테스트마다 데이터가 격리되기 때문에 주문서를 생성한 후 전체 주문서를 조회해도 기대하는 주문서를 이용해서 단언을 수행할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;유통사를 생성한다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderableVendor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;가능한&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;거래처&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clientBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;executeQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;extractValueAsObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;createOrderSheet.orderSheet&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;typeRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheetField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;())&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;expected&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;전체&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문서&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;조회&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expected&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldHaveSize&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldBe&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;기능-테스트-단언-예시&quot;&gt;기능 테스트 단언 예시&lt;/h4&gt;

&lt;p&gt;기능 테스트에서는 테스트마다 데이터가 격리되지 않기 때문에 각 테스트 케이스에서 생성한 유통사를 이용하여 기대하는 주문서를 조회한 후 단언을 수행하도록 하여 테스트의 거짓양성이 발생하지 않도록 하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;유통사를 생성한다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderableVendor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;가능한&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;거래처&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clientBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;executeQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;extractValueAsObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;createOrderSheet.orderSheet&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;typeRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheetField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;())&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;expected&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;유통사의&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문서&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;목록&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;조회&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expected&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldHaveSize&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldBe&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;비동기-코드-검증&quot;&gt;비동기 코드 검증&lt;/h3&gt;

&lt;p&gt;테스트의 단언(Assertion)의 연장선으로 통합 테스트에서 기능 테스트로 전환 시 고민했던 부분이 비동기 코드의 검증이었습니다. 백엔드에서는 주문서 생성 시 슬렉 메시지 전송과 같은 주요 로직이 아닌 부가적인 로직을 처리할 때나 처리 효율성을 이유로 비동기적으로 서버 요청을 처리해야 할 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Async&lt;/code&gt;를 활용하고 있습니다. (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Async&lt;/code&gt;와 관련한 자세한 내용은 &lt;a href=&quot;https://spring.io/guides/gs/async-method/&quot;&gt;Creating Asynchronous Methods&lt;/a&gt; 글을 참고해 주세요)&lt;/p&gt;

&lt;p&gt;통합 테스트에서는 Mocking을 이용하여 호출 여부만 판단하는 형태로 테스트 코드를 작성했었는데요. 그러다 보니 이벤트 처리에 대한 대부분의 코드가 대부분 단위테스트로만 검증되고 있었습니다. 그래서 비동기 코드가 최종적으로 어떻게 통합되어 수행되는지 검증하지 못한다는 단점이 있었는데요. 그래서 기능 테스트로 전환하면서 비동기적인 코드가 끝까지 실행되는지를 테스트하여 해당 기능에 대한 안정성을 좀 더 높이고자 하였습니다.&lt;/p&gt;

&lt;p&gt;하지만 비동기적으로 실행되는 코드를 검증하는 것은 동기적으로 실행되는 코드보다 복잡할 수 있는데요. 저희는 아래와 같은 선택지에서 고민하였습니다.&lt;/p&gt;

&lt;h4 id=&quot;threadsleep&quot;&gt;Thread#sleep&lt;/h4&gt;

&lt;p&gt;다소 아름답지(?) 못한 방식이지만 가장 단순하게 시도해 볼 수 있는 코드입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;주문서를 생성하면 주문 메시지를 발송한다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreateOrderSheetInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;variables&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mapOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;input&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clientBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;executeQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;extractValueAsObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;createOrderSheet&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;typeRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CreateOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;())&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;expected&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;유통사의&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문서&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;목록&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;조회&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expected&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldHaveSize&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldBe&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;

    &lt;span class=&quot;nc&quot;&gt;Thread&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;verifySendUserMessageToGroupChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 코드대로라면 비동기 코드가 언제 실행되든지 간에 테스트는 최소 1초 이상 실행될 것입니다. 거기다 만약 이벤트가 1초 이상 걸린다면 테스트는 실패하게 되겠죠. 테스트 코드를 작성하는 중에 제대로 테스트 코드를 작성하고 있는지 확인하기 위해 임시로 코드를 넣어볼 순 있겠지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Thread#sleep&lt;/code&gt;을 그대로 사용하는 것은 좋아 보이지 않습니다.&lt;/p&gt;

&lt;h4 id=&quot;synctaskexecutor&quot;&gt;SyncTaskExecutor&lt;/h4&gt;

&lt;p&gt;다음 방법으로는 &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/task/SyncTaskExecutor.html&quot;&gt;SyncTaskExecutor&lt;/a&gt;를 활용하는 것입니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SyncTaskExecutor&lt;/code&gt;는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Async&lt;/code&gt;로 선언된 코드를 동기적으로 수행되도록 해줍니다. 문서에도 쓰여있다시피 주로 테스트를 위해 사용됩니다.&lt;/p&gt;

&lt;p&gt;테스트 설정에서 아래와 같이 코드를 작성하면 사용할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@TestConfiguration&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@EnableAsync&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AsyncConfig&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AsyncConfigurer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getAsyncExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Executor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SyncTaskExecutor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 설정하면 이제는 더이상 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Thread.sleep(1000)&lt;/code&gt;과 같은 코드를 넣지 않고도 비동기 기능을 검증할 수 있게 됩니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;주문서를 생성하면 주문 메시지를 발송한다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreateOrderSheetInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;variables&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mapOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;input&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clientBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;executeQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;extractValueAsObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;createOrderSheet&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;typeRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CreateOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;())&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;expected&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;유통사의&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문서&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;목록&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;조회&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expected&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldHaveSize&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldBe&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;verifySendUserMessageToGroupChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;하지만 해당 설정에도 문제점이 존재하였는데요. 저희는 부가적인 로직(예를 들어 주문서 생성 알림을 위한 메시지 전송)에서 발생하는 오류가 주요 로직(예를 들어 주문서 생성)에 영향을 미치지 않았으면 하였는데요. 부가적인 로직을 비동기 함수로 처리하게 되면 이러한 요구사항을 충족시킬 수 있었습니다. 그래서 만약 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sendUserMessageToGroupChannel&lt;/code&gt; 함수에서 오류가 발생하더라도 주문서 생성은 문제없이 동작하는 것입니다. 그러나 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SyncTaskExecutor&lt;/code&gt;를 사용하면 이러한 요구사항을 충족시키지 못합니다. 부가적인 로직에서 발생한 오류가 전파되어 주요 로직에도 영향을 미치기 때문입니다.&lt;/p&gt;

&lt;p&gt;운영환경과 테스트환경에 차이가 있는 것은 어느 정도 불가피하다지만 이러한 주요 요구사항을 충족하지 못하는 부분은 중대하다고 판단해서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SyncTaskExecutor&lt;/code&gt;를 사용하지 않기로 하였습니다.&lt;/p&gt;

&lt;h4 id=&quot;eventually&quot;&gt;eventually&lt;/h4&gt;

&lt;p&gt;결국 저희는 Kotest에서 제공하는 &lt;a href=&quot;https://kotest.io/docs/assertions/eventually.html&quot;&gt;Eventually&lt;/a&gt;를 사용하기로 하였는데요. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Eventually&lt;/code&gt;는 실제 환경과 동일하게 테스트 환경을 구성함과 동시에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Thread.sleep(1000)&lt;/code&gt;을 사용하지 않도록 하는 가장 손쉬운 방법을 제공해 주었습니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Eventually&lt;/code&gt;는 제한된 시간 내에 기대하는 비동기 코드가 실행되는지 가장 짧은 시간 내에 알려줍니다. 그래서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Thread.sleep(1000)&lt;/code&gt;을 사용했을 때처럼 무조건 지정된 시간을 기다리지도 않고 비동기 코드 단언을 위한 복잡한 코드를 작성하지도 않아도 되었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;주문서를 생성하면 주문 메시지를 발송한다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreateOrderSheetInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;variables&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mapOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;input&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clientBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;executeQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;extractValueAsObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;createOrderSheet&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;typeRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CreateOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;())&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;expected&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;유통사의&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문서&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;목록&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;조회&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expected&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldHaveSize&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shouldBe&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
    
    &lt;span class=&quot;nf&quot;&gt;eventually&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;verifySendUserMessageToGroupChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;전환-시-이슈는-없었나요&quot;&gt;전환 시 이슈는 없었나요?&lt;/h2&gt;

&lt;p&gt;앞서 말씀드렸지만, 처음부터 기능 테스트로 테스트 코드를 작성하지 않고 통합 테스트로 테스트 코드를 작성할 만큼 기능 테스트에 대한 난이도에 대한 우려가 있었습니다. 아니나 다를까 기능테스트를 전환하면서 수많은 이슈를 겪게 되었는데요. 모두 소개해 드리면 좋겠지만 내용이 너무 길어질 수 있으므로 저희가 겪었던 대표적인 이슈들을 소개하고 어떻게 해결하였는지 이야기해 보겠습니다.&lt;/p&gt;

&lt;h3 id=&quot;flaky-tests&quot;&gt;Flaky tests&lt;/h3&gt;

&lt;p&gt;저희는 CI(Continuous Integration) 도구로 &lt;a href=&quot;https://circleci.com&quot;&gt;CircleCI&lt;/a&gt;를 사용합니다. CircleCI에서는 Insights라는 기능을 통해 테스트 케이스가 간헐적으로 실패하는 테스트를 알려줍니다. (&lt;a href=&quot;https://circleci.com/docs/insights-tests/#flaky-tests&quot;&gt;CircleCI 문서의 Flaky tests&lt;/a&gt;를 참고해 주세요)&lt;/p&gt;

&lt;p&gt;기능테스트로 전환하면서 CI에서 Flaky tests의 빈도가 증가하였는데요. 앞서 말씀드린 바와 같이 기능 테스트에서는 테스트간 상태 격리가 되지 않아 개발자의 실수 탓에 간헐적으로 실패가 발생하는 것이었습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;기능 테스트에서의 단언&lt;/code&gt; 부분에서 말씀드렸다시피 최대한 테스트 간에 영향을 받지 않게끔 코드를 작성함에도 간헐적인 테스트의 거짓양성이 발생할 수 있는 것은 어쩔 수 없다고 생각합니다. 그래서 저희는 완벽하게 간헐적인 테스트 실패를 막으려 하기보다 아래와 같이 CI의 Flaky tests 리포트를 자주 모니터링하면서 최대한 불안정한 테스트를 줄이도록 노력하고 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-10-05/flaky-tests-report.png&quot; alt=&quot;flaky-tests-report&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;mockserver-response-template&quot;&gt;MockServer Response Template&lt;/h3&gt;

&lt;p&gt;테스트 코드를 작성할 때 테스트를 어렵게 하는 요인 중 하나가 바로 현재시간, Random 데이터 생성 등 테스트 대상 내에서 생성하는 무작위 값이 존재할 때입니다. 좀 더 테스트하기 쉬운 코드를 작성하기 위해서 단위 테스트에서는 의존주입, 매개변수로 추출 등과 같은 기술을 이용하지만, 기능테스트에서는 이마저도 활용할 수 없는 경우가 많습니다.&lt;/p&gt;

&lt;p&gt;그래서 기능테스트에서는 검증하기 힘든 값들은 이미 단위 테스트에서 잘 검증했다는 가정하에 과감히 생략하기도 합니다. 하지만 이마저도 주요 로직에 포함되거나 모듈 간에 통합하는 부분에서 테스트가 필요한 경우 생략하지 못하는 상황이 존재합니다. 저희는 MockServer를 활용하면서 해당 이슈들을 겪었는데요. 대표적인 사례만 소개하고 넘어가 보겠습니다.&lt;/p&gt;

&lt;p&gt;아래 코드는 매장과 유통사를 연결하는 기능에 대한 테스트입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;관리자 권한으로 유통사와 매장을 연결하면 매장과 유통사가 연결된다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderableVendor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;가능한&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;거래처&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;store&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;매장&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ConnectStoreOrderableVendorInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;variables&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mapOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;input&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    
    &lt;span class=&quot;n&quot;&gt;mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getSendbirdUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clientBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;관리자&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;토큰&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;executeQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;extractValueAsObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;connectStoreOrderableVendor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;typeRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ConnectedStoreOrderableVendor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;eventually&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;verifyCreateSendbirdUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;createdSendbirdId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mockery#getSendbirdUser&lt;/code&gt; 함수를 보면 아래와 같이 외부 API를 Mocking하고 있는데요.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getSendbirdUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sendbirdApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whenWithDefault&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;HttpRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withMethod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpMethod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withPath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/v3/users/.+&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;respond&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;HttpResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withStatusCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withBody&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
                    {
                      &quot;user_id&quot;: &quot;${generateId()}&quot;,
                      &quot;nickname&quot;: &quot;${generateString()}&quot;
                    }
                    &quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trimIndent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt;를 랜덤한 값으로 Mocking 하게 되면서 테스트가 실패하거나 검증하지 못하는 상황이 발생하였습니다. 그래서 저희는 랜덤한 값을 생성하여 전송하더라도 운영 코드 변경 없이 테스트가 잘 수행되도록 할 방법을 모색하기 시작하였습니다.&lt;/p&gt;

&lt;p&gt;첫 번째는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mockery#getSendbirdUser&lt;/code&gt; 함수에 매개변수를 추가하는 방법이 논의되었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getSendbirdUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sendbirdApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whenWithDefault&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;HttpRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withMethod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpMethod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withPath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/v3/users/.+&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;respond&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;HttpResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withStatusCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withBody&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
                    {
                      &quot;user_id&quot;: &quot;$userId&quot;,
                      &quot;nickname&quot;: &quot;${generateString()}&quot;
                    }
                    &quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trimIndent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;가장 직관적이고 손쉽게 문제를 해결할 방법 같아 보이지만 API 서버 내부에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;userId&lt;/code&gt;를 랜덤하게 생성하는 경우에는 해당 값을 매개변수로 전달할 수 없기 때문에 해결 방법으로 사용할 수 없었습니다.&lt;/p&gt;

&lt;p&gt;두 번째 방법으로 &lt;a href=&quot;https://www.mock-server.com/mock_server/response_templates.html&quot;&gt;MockServer의 Response Template&lt;/a&gt; 을 활용하는 방법을 모색하였습니다. Response Template에는 여러 가지 포맷들이 있는데요. 그중 저희는 &lt;a href=&quot;https://www.mock-server.com/mock_server/response_templates.html#mustache_templates&quot;&gt;Mustache Response Templates&lt;/a&gt; 을 사용하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Mockery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getSendbirdUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sendbirdApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;whenWithDefault&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;HttpRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withMethod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpMethod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withPath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/v3/users/{userId}&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;withPathParameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;userId&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;.+&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;respond&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;HttpTemplate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;TemplateType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MUSTACHE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
            {
                &quot;statusCode&quot;: 200,
                &quot;body&quot;: {
                  &quot;user_id&quot;: &quot;{{ request.pathParameters.userId.0 }}&quot;,
                  &quot;nickname&quot;: &quot;${generateId()}&quot;
                }
            }
            &quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trimIndent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 코드를 보시면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{{ request.pathParameters.userId.0 }}&lt;/code&gt;부분이 보이실 텐데요. 해당 값은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;withPath(&quot;/v3/users/{userId}&quot;)&lt;/code&gt;의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;userId&lt;/code&gt; 값을 그대로 반환하도록 하는 문법입니다. 이를 통해 위에서 작성한 테스트가 실패하지 않고 올바르게 검증되도록 할 수 있었고 결국 저희는 두 번째 방법을 사용하기로 하였습니다.&lt;/p&gt;

&lt;h3 id=&quot;message-throttling&quot;&gt;Message Throttling&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://spoqa.github.io/2023/02/24/bill-payment-development-story.html&quot;&gt;청구/수납 서비스 개발기&lt;/a&gt;에서 Message Throttling과 관련한 이야기를 다루었었는데요. &lt;a href=&quot;https://github.com/bucket4j/bucket4j&quot;&gt;Bucket4j&lt;/a&gt;의 Message Throttling을 사용하기 위해서는 데이터베이스와 Message Queue가 필요합니다. 앞서 말씀드린 바와 같이 기능테스트에서는 데이터베이스와 Message Queue를 공유해서 사용하고 있는데요. 이에 따라 Throttling 된 메시지를 전송할 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eventually&lt;/code&gt;를 사용해 검증하지만 메시지 전송 단언(Assertion)이 되지 않는 이슈가 발생하였습니다.&lt;/p&gt;

&lt;p&gt;특이한 점은 단일 테스트 케이스를 실행해서 테스트하는 경우에는 성공하지만, 전체 테스트 케이스를 실행시키면 실패한다는 것이었습니다. 원인은 바로 Message Throttling이 문제였었는데요.&lt;/p&gt;

&lt;p&gt;하나의 테스트 케이스만 실행하는 경우 Throttling 대기열에 쌓여있는 메시지가 없으므로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eventually&lt;/code&gt;로 검증 시 제한된 시간 내 잘 검증이 되는 것을 볼 수 있었습니다. 하지만 전체 테스트를 실행하는 경우 Throttling 대기열에 다수의 테스트 케이스 메시지들이 쌓이게 되고 Throttling 되어 소비되기 때문에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eventually&lt;/code&gt;로 검증을 시도하더라도 제한된 시간 내 단언에 성공하지 못해 테스트가 실패하는 경우가 발생한 것입니다. (해당 이슈를 발견하기까지 상당히 애를 먹었네요 ^^;;)&lt;/p&gt;

&lt;p&gt;생각해 보면 Message Throttling을 구현한 이유가 외부 API의 제약조건 때문이었는데요. 이러한 외부요인을 회피하기 위해 저희는 MockServer를 활용하고 있으므로 Message를 Throttling 할 필요가 없습니다. 그래서 저희는 아래와 같이 Throttling이 걸려있는 Bucket 설정을 모두 Throttling이 걸리지 않도록 변경하여 문제를 해결했습니다.&lt;/p&gt;

&lt;h4 id=&quot;운영-환경-설정&quot;&gt;운영 환경 설정&lt;/h4&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Bucket4jConfig&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;bucketProxyManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dataSource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DataSource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PostgreSQLadvisoryLockBasedProxyManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PostgreSQLadvisoryLockBasedProxyManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SQLProxyConfiguration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dataSource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sendbirdUserMessageBucket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bucketProxyManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PostgreSQLadvisoryLockBasedProxyManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BucketProxy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1003L&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;bucketConfiguration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BucketConfiguration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addLimit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Bandwidth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;simple&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ofSeconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 1초에 5회로 Throttling&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bucketProxyManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bucketConfiguration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;테스트-환경-설정&quot;&gt;테스트 환경 설정&lt;/h4&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@TestConfiguration&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Bucket4jConfig&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;unlimited&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Bandwidth&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Bandwidth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;simple&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MAX_VALUE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ofNanos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MAX_VALUE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;bucketProxyManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dataSource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DataSource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PostgreSQLadvisoryLockBasedProxyManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PostgreSQLadvisoryLockBasedProxyManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SQLProxyConfiguration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dataSource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sendbirdUserMessageBucket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bucketProxyManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PostgreSQLadvisoryLockBasedProxyManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BucketProxy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1003L&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;bucketConfiguration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BucketConfiguration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addLimit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unlimited&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Throttling 설정하지 않음&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bucketProxyManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bucketConfiguration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;

&lt;p&gt;지금까지 저희 백엔드 챕터에서 기능 테스트로 전환 시 사용했던 여러 가지 방법들을 알아보았습니다.&lt;/p&gt;

&lt;p&gt;상황을 보다 이해가 잘되도록 하려다 보니 다소 세세한 부분까지 다루게 되었는데요. 기능 테스트를 전환할 때 어떠한 애로사항들이 있는지, 해당 애로사항들을 어떻게 풀어나갈 수 있는지와 같은 넓은 관점에서 바라봐 주시면 좋을 것 같습니다.&lt;/p&gt;

&lt;p&gt;모두의 노력 덕분에 저희는 올해 통합 테스트 코드를 모두 제거하고 온전히 기능 테스트로만 전체 기능을 테스트할 수 있게 되었습니다. 다만, 여전히 기능테스트에 대한 개선할 점들은 많이 있어 보이네요 ^^;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-10-05/remove-integration-testing-pr.png&quot; alt=&quot;move-integration-testing-pr&quot; /&gt;&lt;/p&gt;

&lt;p&gt;모쪼록 이번 이야기가 여러분의 테스트 코드 작성에 조금이나마 도움이 되었길 바라며, 기능 테스트를 작성하면서 다시 또 재미있는 이야깃거리가 있다면 소개해 드리는 글로 찾아뵙도록 하겠습니다.&lt;/p&gt;

&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>이벤트 로그 체계 구축 여정</title>
      <link>https://spoqa.github.io/2023/04/28/journey-to-building-an-event-log-system.html</link>
      <pubDate>Fri, 28 Apr 2023 00:00:00 +0000</pubDate>
      <author>양현승</author>
      <guid>/2023/04/28/journey-to-building-an-event-log-system</guid>
      <description>&lt;p&gt;안녕하세요. 스포카의 데이터 분석가 양현승(todd)입니다. 😄&lt;br /&gt;
키친보드 서비스(&lt;a href=&quot;https://play.google.com/store/apps/details?id=com.spoqa.ops&amp;amp;hl=en_US&quot;&gt;Android,&lt;/a&gt; &lt;a href=&quot;https://apps.apple.com/kr/app/%ED%82%A4%EC%B9%9C%EB%B3%B4%EB%93%9C-%EB%8F%84%EB%8F%84%EC%B9%B4%ED%8A%B8-%EB%B9%84%EC%9A%A9%EA%B4%80%EB%A6%AC-%EC%A3%BC%EB%AC%B8/id1565918209&quot;&gt;iOS&lt;/a&gt;) 이벤트 로그를 새로 설계하게 된 과정을 공유하려고 합니다.&lt;/p&gt;

&lt;p&gt;로그 설계의 필요성은 많이 느꼈지만, 경험이 없었기 때문에 어떻게 시작해야할지 막막하고 걱정이 앞섰습니다.&lt;br /&gt;
또한 이미 조직에 로그 체계가 있었기 때문에, 기존 로그 체계의 데이터를 새로운 체계로 이전하는 과정에서 데이터 무결성 및 기타 이슈들을 고려해야했습니다.&lt;br /&gt;
돌이켜보면, 동료들과 함께 패기와 열정으로 해낸 것 같습니다.&lt;br /&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;li&gt;스포카의 이벤트 로그 체계가 궁금하신 분&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;목차&quot;&gt;목차&lt;/h1&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#배경&quot;&gt;배경&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#1-부족한-데이터&quot;&gt;1. 부족한 데이터&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#2-분리된-문서&quot;&gt;2. 분리된 문서&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#3-디버깅의-어려움&quot;&gt;3. 디버깅의 어려움&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#이벤트-로그-설계-과정&quot;&gt;이벤트 로그 설계 과정&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#screen&quot;&gt;Screen&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#variable&quot;&gt;Variable&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#screen별-event&quot;&gt;Screen별 Event&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#event-total&quot;&gt;Event Total&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#update&quot;&gt;Update&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#로그-검증&quot;&gt;로그 검증&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#개선-효과&quot;&gt;개선 효과&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#회고&quot;&gt;회고&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#마무리&quot;&gt;마무리&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#추천-자료&quot;&gt;추천 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;배경&quot;&gt;배경&lt;/h1&gt;

&lt;p&gt;스포카는 firebase를 활용하여 로그를 수집하고 있습니다.&lt;br /&gt;
이벤트 로그 설계 및 관리는 PO, PM분들께서 구글 시트에서 해주셨습니다.&lt;br /&gt;
기존 설계 문서는 스크린(화면)과 이벤트 로그를 시트로 나눠서 기록하고 있었습니다.&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;스크린 정리 시트는 아래와 같이 크게 3가지가 기록되어 있습니다.
&lt;img src=&quot;/images/2023-04-28/old_design_screen_name.png&quot; alt=&quot;old_design_screen_name&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;스크린별 한글/영문명 (Camel case)&lt;/li&gt;
  &lt;li&gt;구현해야하는 대상 (웹/앱)&lt;/li&gt;
  &lt;li&gt;이벤트 저장 시점&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이벤트 로그 정리 시트는 아래와 같이 크게 6가지로 구성되어 있습니다.
&lt;img src=&quot;/images/2023-04-28/old_design_eventlog.png&quot; alt=&quot;old_design_eventlog&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;스크린 영문명 (Camel case)&lt;/li&gt;
  &lt;li&gt;action에 대한 설명&lt;/li&gt;
  &lt;li&gt;이벤트명 (snake_case)&lt;/li&gt;
  &lt;li&gt;이벤트 형식&lt;/li&gt;
  &lt;li&gt;이벤트 파라미터 (snake_case)&lt;/li&gt;
  &lt;li&gt;수집 목적&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;기존 로그 체계와 문서는 잘 구축되었지만, 3가지 불편함이 있었습니다.&lt;/p&gt;

&lt;h2 id=&quot;1-부족한-데이터&quot;&gt;1. 부족한 데이터&lt;/h2&gt;

&lt;p&gt;“&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;데이터 분석가님 이거 확인해 줄 수 있나요?&lt;/code&gt;”&lt;/p&gt;

&lt;p&gt;분석 요청이 들어오면 회사의 데이터 구조가 더 빨리 보입니다.&lt;br /&gt;
요청 사항을 시행하기 위해 로그 문서를 찾아보면 수집이 안된 경우가 많았습니다.&lt;/p&gt;

&lt;p&gt;추가적으로 필요한 데이터는 크게 3가지 종류였습니다.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;이벤트 버튼의 위치 (ex. 상/하단, 버튼 index 등)&lt;/li&gt;
  &lt;li&gt;서버에 저장된 테이블과 연동할 수 있는 id 값&lt;/li&gt;
  &lt;li&gt;누락된 이벤트 (ex. scroll, 특정 버튼 등)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;보통 당장 쓰일 수 있는 데이터만 수집하고, 나머지 데이터는 사용 목적을 모르거나 저장 공간 낭비로 여겨지기 때문에 누락되는 것 같습니다.&lt;br /&gt;
유저의 모든 행동 데이터를 기록해 두면 이탈이 발생한 지점에서 어떤 행동을 했는지 자세하게 알 수 있습니다.&lt;br /&gt;
또한 각 스크린별 유저의 행동을 추적하면서, 생각하지 못한 인사이트를 얻을 수도 있습니다.&lt;br /&gt;
서버 테이블과 연동할 수 있는 id 값이 있다면, 이상 지점에서 발생한 유저들의 행동을 쉽게 확인할 수 있습니다.&lt;/p&gt;

&lt;p&gt;“&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;어떻게 하면 분석을 더 잘 할 수 있는 데이터를 모을 수 있을까요?&lt;/code&gt;”&lt;/p&gt;

&lt;h2 id=&quot;2-분리된-문서&quot;&gt;2. 분리된 문서&lt;/h2&gt;

&lt;p&gt;“&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xx 화면에서 xx 버튼 클릭하는 로그가 어디있나요?&lt;/code&gt;”&lt;/p&gt;

&lt;p&gt;특정 기능을 하는 이벤트가 어느 화면에 어떤 부분인지 알기 어려운 경우가 많았습니다.&lt;br /&gt;
로그 문서와 Figma가 독립적으로 관리되고 있었기 때문입니다.&lt;/p&gt;

&lt;p&gt;신규 입사자와 같이 서비스에 익숙하지 않거나, Figma와 로그 문서에 익숙하지 않은 경우 헤매기 쉬울 것 같았습니다.&lt;br /&gt;
또한 잘 쓰지 않는 로그는 시간이 지나면 잊어버려서 다시 문서를 찾고 있었습니다.&lt;br /&gt;
이벤트 로그에 대한 질문이 있을 때마다, PO/PM과 PD가 시간을 할애해야 했다는 느낌이 들었습니다.&lt;br /&gt;
(너무 간단한 질문이라 조금 눈치도 보였습니다.. 😥)&lt;/p&gt;

&lt;p&gt;로그 관련 질문 프로세스는 보통 아래와 같습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-04-28/inquiry_process.png&quot; alt=&quot;inquiry_process&quot; /&gt;&lt;/p&gt;

&lt;p&gt;“&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;어떻게 하면 모두가 이해하기 쉽게 문서를 만들 수 있을까요?&lt;/code&gt;”&lt;/p&gt;

&lt;h2 id=&quot;3-디버깅의-어려움&quot;&gt;3. 디버깅의 어려움&lt;/h2&gt;

&lt;p&gt;“&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OO개발자님! XX유저는 이 시간대에 서버에는 데이터가 있는데 왜 이벤트 로그가 없을까요?&lt;/code&gt;”&lt;/p&gt;

&lt;p&gt;사실 이벤트 로그는 생각보다 손실이 많이 발생합니다.&lt;br /&gt;
유저의 네트워크, 단말 문제 등 알 수 없는 요인이 있습니다.&lt;br /&gt;
그렇다고, “아 그냥 누락되었나 보다”라고 생각하고 넘어갈 수는 없습니다.&lt;/p&gt;

&lt;p&gt;“&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OO개발자님! A이벤트는 B화면에서 발생해야 하는데 왜 C화면이라고 데이터가 저장되어 있나요?&lt;/code&gt;”&lt;/p&gt;

&lt;p&gt;또한 이벤트는 유저가 액션을 하는 타이밍 이슈에 따라 view 로그 데이터가 누락될 수 있습니다.&lt;br /&gt;
너무 빨리 버튼을 누르거나, 탭 전환을 하는 경우에는 view 로그를 남겨서 firebase에 보내기도 전에 다른 이벤트가 발생하기 때문입니다.&lt;br /&gt;
이벤트명을 보고 유추할 수 있지만, 다양한 스크린에서 공통적으로 쓰는 로그는 확인이 어려울 수 있습니다.&lt;/p&gt;

&lt;p&gt;“&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;어떻게 하면 특정 문제가 생겼을 때, 최대한 디버깅이 가능하도록 정보를 수집할 수 있을까요?&lt;/code&gt;”&lt;/p&gt;

&lt;h1 id=&quot;이벤트-로그-설계-과정&quot;&gt;이벤트 로그 설계 과정&lt;/h1&gt;

&lt;p&gt;불편했던 3가지를 해결할 수 있는 방향을 찾기로 했습니다.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;어떻게 하면 분석을 더 잘할 수 있는 데이터를 모을 수 있을까요?&lt;/li&gt;
  &lt;li&gt;어떻게 하면 모두가 이해하기 쉽게 문서를 만들 수 있을까요?&lt;/li&gt;
  &lt;li&gt;어떻게 하면 특정 문제가 생겼을 때, 최대한 디버깅이 가능하도록 정보를 수집할 수 있을까요?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;br /&gt;
스포카는 개발자(웹/앱)분들이 Firebase 로그를 보내면, GA4에서 데이터를 받아서 BigQuery로 전달하는 과정을 가집니다. Firebase 형식을 참고하고, 기본적으로 제공하는 정보를 고려해서 설계했습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://support.google.com/analytics/answer/9234069?hl=ko&quot;&gt;[GA4] 자동 수집 이벤트&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://support.google.com/firebase/answer/7029846?hl=ko#zippy=%2Cevent&quot;&gt;[GA4] BigQuery Export 스키마&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;br /&gt;
로그 설계 문서는 구글 시트를 활용했습니다. 구글 시트를 사용한 이유는 총 3가지입니다.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;다른 팀과 쉽게 공유할 수 있으며,&lt;/li&gt;
  &lt;li&gt;동시 수정 및 코멘트 기능이 있고,&lt;/li&gt;
  &lt;li&gt;수식을 활용하여 자동화하기 좋기 때문입니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;br /&gt;
시트는 크게 5가지 형식으로 구분되어 있습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Screen: 모든 스크린 정보 저장&lt;/li&gt;
  &lt;li&gt;Variable: 모든 변수 정보 및 데이터 타입 저장&lt;/li&gt;
  &lt;li&gt;Screen별 Event: 특정 스크린에서 발생하는 이벤트 파라미터 정보 저장&lt;/li&gt;
  &lt;li&gt;Event Total: 모든 이벤트 파라미터 정보 저장&lt;/li&gt;
  &lt;li&gt;Update: 이벤트 로그 관련 작업에 대한 요청사항을 정리&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;설계를 하면서 도움이 많이 되었던 내용은 아래 “추천 자료”에 기재해 두었습니다.&lt;br /&gt;
각 형식별 자세한 설명을 하겠습니다.&lt;/p&gt;

&lt;h2 id=&quot;screen&quot;&gt;Screen&lt;/h2&gt;

&lt;p&gt;Screen 시트는 app과 webview의 모든 스크린 정보를 모아두는 테이블입니다.&lt;br /&gt;
모든 스크린과 이벤트의 명칭은 snake case로 적었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-04-28/sheet_screen.png&quot; alt=&quot;sheet_screen&quot; /&gt;&lt;/p&gt;

&lt;p&gt;각 구성 요소의 의미는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;use: 스크린 사용 여부&lt;/li&gt;
  &lt;li&gt;screen_name: 스크린명&lt;/li&gt;
  &lt;li&gt;service_name: 서비스 영문명 (ex. KITCHENBOARD-STORE)&lt;/li&gt;
  &lt;li&gt;service_name_kr: 서비스 한글명 (ex. 키친보드 매장앱)&lt;/li&gt;
  &lt;li&gt;function: 기능 영문명 (ex. CHAT)&lt;/li&gt;
  &lt;li&gt;function_kr: 기능 한글명 (ex. 채팅)&lt;/li&gt;
  &lt;li&gt;platform: webview / web / app(native) 구분&lt;/li&gt;
  &lt;li&gt;firebase_screen_class: firebase용 스크린 영문명&lt;/li&gt;
  &lt;li&gt;firebase_screen: firebase용 스크린 한글명&lt;/li&gt;
  &lt;li&gt;sheet_name: “Screen별 Event” 시트명 및 링크&lt;/li&gt;
  &lt;li&gt;action_plan: 해당 스크린의 이벤트 로그를 어떻게 활용할 것인지 간략하게 정리&lt;/li&gt;
  &lt;li&gt;comment: 업데이트 날짜, 특이사항 기록&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;service_name, function과 screen_name은 이벤트 로그의 카테고리라고 생각하시면 될 것 같습니다.&lt;br /&gt;
service_name은 대분류, function은 중분류, screen_name은 소분류의 성격을 갖고 있습니다.&lt;/p&gt;

&lt;p&gt;firebase_screen_class와 firebase_screen은 firebase를 사용하기 때문에 전달하는 값입니다.&lt;br /&gt;
firebase에 두 값을 보내면, view 이벤트가 발생했을 때 이전 스크린의 정보 값을 알 수 있습니다.&lt;br /&gt;
(firebase_previous_class, firebase_previous_screen)&lt;/p&gt;

&lt;p&gt;각 스크린별 이벤트를 잡기 전에 해당 스크린을 어떻게 활용할 것인지 action_plan에 간략하게 정리하면, 활용 목적에 따라 어떤 이벤트 로그를 잡아야 하는지 명확해지고 히스토리로도 활용할 수 있습니다.&lt;/p&gt;

&lt;p&gt;sheet_name은 “service_name / function / screen_name”으로 구성했으며,&lt;br /&gt;
“Screen별 Event” 연결 링크를 첨부하여 이동이 편리하게 설정했습니다.&lt;/p&gt;

&lt;h2 id=&quot;variable&quot;&gt;Variable&lt;/h2&gt;
&lt;p&gt;Variable 시트는 모든 속성들의 정보를 저장하는 시트입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-04-28/sheet_variable.png&quot; alt=&quot;sheet_variable&quot; /&gt;&lt;/p&gt;

&lt;p&gt;구성 요소는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;category: 변수 카테고리&lt;/li&gt;
  &lt;li&gt;variable: 변수명&lt;/li&gt;
  &lt;li&gt;data_type: 변수 데이터 타입&lt;/li&gt;
  &lt;li&gt;description: 변수 설명&lt;/li&gt;
  &lt;li&gt;variable_size: 변수명 길이&lt;/li&gt;
  &lt;li&gt;comment: 업데이트 날짜, 특이사항 기록&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;br /&gt;
category는 이벤트의 용도를 그룹화해서 정보를 분리할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;action: 유저의 행동 (ex. view, click, scroll, search)&lt;/li&gt;
  &lt;li&gt;user_properties: 유저의 정보 (ex. 고객 id, 고객명, 주소, 업종 등)&lt;/li&gt;
  &lt;li&gt;object_section: 이벤트가 위치하고 있는 곳에 대한 정보 (ex. gnb, lnb, fnb 등)&lt;/li&gt;
  &lt;li&gt;object_type: 이벤트의 유형 (ex. button, banner 등)&lt;/li&gt;
  &lt;li&gt;object_id: 이벤트의 유형 id (ex. banner_id 등)&lt;/li&gt;
  &lt;li&gt;object_name: 이벤트 명칭 (ex. next, close, back 등)&lt;/li&gt;
  &lt;li&gt;object_position: 이벤트의 index (ex. 버튼이 세로로 4개가 있고, 첫 번째 버튼을 클릭한다면 0)&lt;/li&gt;
  &lt;li&gt;event_properties: 각 이벤트별 보유하고 있는 정보 (ex. 주문서 id, 주문 수량, 결제액 등)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Firebase에서 기본적으로 제공하는 값은 문서에서 제외했습니다.&lt;br /&gt;
대표적인 데이터는 앱 인스턴스 ID(user_pseudo_id), 이벤트 실행 시간(event_timtestamp),&lt;br /&gt;
유저 처음 앱 실행 시간(user_first_touch_timestamp), 디바이스 정보(브랜드명, 모델명 등),&lt;br /&gt;
앱 정보(버전 등) 등이 있습니다.&lt;/p&gt;

&lt;p&gt;firebase 정책 상, 변수의 길이는 24자 이하를 만족해야 로그가 쌓입니다. (&lt;a href=&quot;https://support.google.com/firebase/answer/9237506?hl=ko&quot;&gt;Firebase 참고 문서&lt;/a&gt;)&lt;br /&gt;
variable_size의 길이가 넘지 않도록 주의해야 하기 때문에, 조건부 서식 같이 눈에 보이는 장치를 추가하는 것이 좋습니다.&lt;/p&gt;

&lt;h2 id=&quot;screen별-event&quot;&gt;Screen별 Event&lt;/h2&gt;
&lt;p&gt;세부적인 이벤트들은 Screen별 분리해서 정리했습니다.&lt;br /&gt;
Screen의 Figma 디자인과 이벤트를 관리하기 편하기 때문입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-04-28/sheet_screen_event.png&quot; alt=&quot;sheet_screen_event&quot; /&gt;&lt;/p&gt;

&lt;p&gt;왼쪽 상단에는 바로가기 기능을 만들었습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Home: Screen 시트로 이동&lt;/li&gt;
  &lt;li&gt;Total: 모든 이벤트를 모아둔 시트(Event Total)로 이동&lt;/li&gt;
  &lt;li&gt;Figma: 해당 화면의 Figma 시안 링크&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;중앙에는 Figma 시안을 추가했습니다.&lt;br /&gt;
시안을 Figma에 접속해서 따로 확인하면서 로그를 설계할 필요가 없어졌습니다.&lt;/p&gt;

&lt;p&gt;하단에는 수집할 이벤트를 정리했습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;use: 이벤트 사용 여부&lt;/li&gt;
  &lt;li&gt;event_name: 이벤트명&lt;/li&gt;
  &lt;li&gt;scenario: 유저 시나리오&lt;/li&gt;
  &lt;li&gt;comment: 업데이트 날짜, 특이사항 기록&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;event_name은 아래와 같은 형식으로 구성됩니다.&lt;br /&gt;
“&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;screen_name + __ + action + __ + object_name&lt;/code&gt;”&lt;/p&gt;

&lt;p&gt;event_name은 자세하게 하는 방법과 축약해서 공통적으로 사용하는 방법이 있습니다.&lt;br /&gt;
각각 장단점이 있지만, 이벤트명을 보면 어떤 기능을 하는지 직관적으로 이해하기 위해 위 방식을 사용했습니다.&lt;br /&gt;
단, view는 screen_view로 통일된 명칭을 사용합니다.&lt;br /&gt;
Firebase를 활용하면서 앱 개발자가 firebase_screen_class를 업데이트하면 자동적으로 찍히기 때문에, Firebase에서 제공하는 명칭을 이용했습니다.&lt;/p&gt;

&lt;h2 id=&quot;event-total&quot;&gt;Event Total&lt;/h2&gt;
&lt;p&gt;모든 스크린의 이벤트를 모아둔 시트입니다. 샘플은 아래와 같습니다.
&lt;img src=&quot;/images/2023-04-28/sheet_event_total.png&quot; alt=&quot;sheet_event_total&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Screen별 Event 시트의 구성요소와 별개로 2가지 요소가 추가되어있습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;length_event_name: 이벤트명 길이&lt;/li&gt;
  &lt;li&gt;count_event_keys: event_properties에 포함된 key 개수&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Firebase는 이벤트명의 길이가 40자가 넘어가면 로그가 저장되지 않습니다.&lt;br /&gt;
따라서 이벤트명의 길이를 확인하는 과정이 별도로 필요합니다.&lt;br /&gt;
이 시트에서는 40자가 넘어가면 조건부 서식으로 행을 음영처리하여 작업자가 확인 가능하도록 세팅했습니다. (&lt;a href=&quot;https://support.google.com/firebase/answer/9237506?hl=ko&quot;&gt;Firebase 참고 문서&lt;/a&gt;)&lt;/p&gt;

&lt;h2 id=&quot;update&quot;&gt;Update&lt;/h2&gt;
&lt;p&gt;체계가 구축된 이후, 변경/추가/삭제가 필요한 로그는 update 시트에서 논의합니다.&lt;br /&gt;
샘플은 아래와 같습니다.
&lt;img src=&quot;/images/2023-04-28/sheet_update.png&quot; alt=&quot;sheet_update&quot; /&gt;&lt;/p&gt;

&lt;p&gt;구성 요소는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;done: 검증 완료 여부&lt;/li&gt;
  &lt;li&gt;dev (iOS, Android): 개발용 버전 로그 검증 여부&lt;/li&gt;
  &lt;li&gt;qa (iOS, Android): QA 버전 로그 검증 여부&lt;/li&gt;
  &lt;li&gt;요청자&lt;/li&gt;
  &lt;li&gt;작업자&lt;/li&gt;
  &lt;li&gt;version (app/web)&lt;/li&gt;
  &lt;li&gt;attribute: 추가/변경하는 속성 (ex. user_properties)&lt;/li&gt;
  &lt;li&gt;variable / event_name: 변수 or 이벤트명&lt;/li&gt;
  &lt;li&gt;purpose: 로그를 추가/변경/삭제하는 목적&lt;/li&gt;
  &lt;li&gt;comment: 업데이트/요청 날짜, 특이사항 기록&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;요청자(ex. 데이터 분석가, PM)는 변경할 작업에 대한 설명과 목적을 적은 후,&lt;br /&gt;
작업자(ex. 프론트엔드, 앱 개발자)에게 공지를 합니다.&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;개발자가 편리하게 작업의 내용을 확인할 수 있게 “variable / event_name” 칼럼에 링크를 걸어두는 것이 좋습니다.&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;작업자가 개발을 완료한 경우, 개발/QA 버전별 검증을 진행합니다.&lt;br /&gt;
모든 검증이 완료한 경우, done에 체크를 함으로써 해당 이벤트 작업이 종료됩니다.&lt;/p&gt;

&lt;h1 id=&quot;로그-검증&quot;&gt;로그 검증&lt;/h1&gt;
&lt;p&gt;기존 로그 체계를 새롭게 개편했기 때문에, 과거의 로그가 누락되지 않았는지 확인했습니다.&lt;br /&gt;
또한 모든 로그 데이터가 잘 수집되는지 검증을 해야 했습니다.&lt;br /&gt;
만약 X개의 로그가 쌓였다면, 총 2X개의 로그를 검증해야 합니다. (iOS, Android)&lt;br /&gt;
QA팀에서 정말 많은 도움을 주셔서 해결할 수 있었지만, 이 과정을 최대한 자동화하고 싶었습니다.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;
“&lt;em&gt;우리가 설계한 로그 스키마와 수집된 로그 데이터가 동일한지  체크할 수 있지 않을까?&lt;/em&gt;”&lt;/p&gt;

&lt;p&gt;구글 시트에 정리한 스키마와 BigQuery에 저장된 결과물을 Redash를 활용하여 비교했습니다.&lt;br /&gt;
검증 방식은 2가지 프로세스를 진행됩니다.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;플랫폼(iOS, Android) 별 이벤트 key 검토 &lt;br /&gt;→ 스키마와 다를 경우, 개발자에게 검토 요청
 &lt;img src=&quot;/images/2023-04-28/qa_developer.png&quot; alt=&quot;qa_developer&quot; /&gt;&lt;/li&gt;
  &lt;li&gt;플랫폼(iOS, Android) 별 이벤트 value 검토 &lt;br /&gt;→ 올바른 데이터가 적재되었는지 데이터 분석가가 검토
 &lt;img src=&quot;/images/2023-04-28/qa_da.png&quot; alt=&quot;qa_da&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;위 방식을 통해 수많은 이벤트들을 더 편하게 검증할 수 있었고, 시간도 아낄 수 있었습니다.&lt;/p&gt;

&lt;h1 id=&quot;개선-효과&quot;&gt;개선 효과&lt;/h1&gt;

&lt;p&gt;작업을 시작하기 전에 느꼈던 불편함은 아래의 방법으로 해결했습니다.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;어떻게 하면 분석을 더 잘할 수 있는 데이터를 모을 수 있을까요?
    &lt;ol&gt;
      &lt;li&gt;이벤트 버튼의 위치 (ex. object_section, object_position 등) 정보 저장&lt;/li&gt;
      &lt;li&gt;서버에 저장된 테이블과 연동할 수 있는 id 값 저장&lt;/li&gt;
      &lt;li&gt;scroll, 누락된 이벤트 잡기&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;어떻게 하면 모두가 이해하기 쉽게 문서를 만들 수 있을까요?
    &lt;ol&gt;
      &lt;li&gt;스크린별 분리하여 구글 시트에 저장&lt;/li&gt;
      &lt;li&gt;스크린별 해당하는 Figma 시안 저장 및 연동&lt;/li&gt;
      &lt;li&gt;스크린별 유저 시나리오 저장&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;어떻게 하면 특정 문제가 생겼을 때, 최대한 디버깅이 가능하도록 정보를 수집할 수 있을까요?
    &lt;ol&gt;
      &lt;li&gt;직관적인 이벤트명 설정&lt;/li&gt;
      &lt;li&gt;스크린명 ↔ firebase_screen_class를 비교하여 어느 point에서 오류가 생기는지 확인&lt;/li&gt;
      &lt;li&gt;Redash 검증 대시보드&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;추가적으로 문서를 생성할 때, 서비스의 확장성과 관리의 편리함도 고려했습니다.&lt;br /&gt;
하나의 문서로 이벤트 로그 설계를 관리할 수 있다는 점이 가장 좋았던 것 같습니다.&lt;/p&gt;

&lt;h1 id=&quot;회고&quot;&gt;회고&lt;/h1&gt;
&lt;p&gt;스포카에는 프로젝트가 끝나면 회고를 하는 문화가 있습니다.&lt;br /&gt;
프로젝트에 참여한 구성원들과 모여서 4L 방식으로 진행했습니다.&lt;br /&gt;
많은 의견이 있었지만, 그중 기억에 남는 한 가지씩을 소개하겠습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Liked&lt;/strong&gt; (좋았던 점)
    &lt;ul&gt;
      &lt;li&gt;참석자 V: 모두가 보기에 직관적인 앱 로그 시트, 드디어 숙원 사업 clear!!&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Learned&lt;/strong&gt; (배운 점)
    &lt;ul&gt;
      &lt;li&gt;참석자 A: Firebase의 제한점과 한계점을 배울 수 있었음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Lacked&lt;/strong&gt; (아쉬웠던 점)
    &lt;ul&gt;
      &lt;li&gt;참석자 M: 작업 중 변경된 내용이 잘 공유되지 않아서 아쉬웠음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Longed for&lt;/strong&gt; (앞으로 바라는 점)
    &lt;ul&gt;
      &lt;li&gt;참석자 E: 로그 변경 건의 태스크화&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-04-28/retrospect.png&quot; alt=&quot;retrospect&quot; /&gt;&lt;/p&gt;

&lt;p&gt;회고를 통해 프로젝트에 대해 고생한 모두 함께 격려했고,&lt;br /&gt;
부족한 점과 해결 방안을 고민하면서 이벤트 로그 작업이 나아갈 방향을 찾게 되었습니다.&lt;br /&gt;
(Update 시트는 회고를 통해서 새로 생성할 수 있었습니다)&lt;/p&gt;

&lt;h1 id=&quot;마무리&quot;&gt;마무리&lt;/h1&gt;
&lt;p&gt;우여곡절이 정말 많았던 프로젝트였지만,&lt;br /&gt;
훌륭한 동료분들과 함께 즐겁게 할 수 있어서 제 인생에서 가장 행복한 프로젝트였습니다.&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;FE 개발자 한 분이 작업이 끝나고 이렇게 말씀해 주셨습니다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;저는 토드님이 생각하시는 것 이상으로 이벤트 로그 작업이 즐거웠어요&lt;/p&gt;

&lt;/blockquote&gt;

&lt;p&gt;로그 체계를 구축하고 설계하는 과정은 처음이라 서툴고 두려웠지만&lt;br /&gt;
동료들과 함께한다면 어떤 일이든 해낼 수 있다는 생각을 하게 되었습니다.&lt;/p&gt;

&lt;p&gt;앞으로도 좋은 콘텐츠가 있다면 공유하겠습니다.&lt;br /&gt;
긴 글 읽어주셔서 감사합니다 🙇🏻‍♂️&lt;/p&gt;

&lt;h1 id=&quot;추천-자료&quot;&gt;추천 자료&lt;/h1&gt;
&lt;ul&gt;
  &lt;li&gt;모두의 요금제 “임광빈”님의 &lt;a href=&quot;https://learningspoons.com/course/detail/metrics_analysis/&quot;&gt;Data-driven Product를 위한 분석 설계 : 지표, 로그, A/B test&lt;/a&gt; 강의&lt;/li&gt;
  &lt;li&gt;쏘카, 타다 출신 “변성윤”님의 &lt;a href=&quot;https://www.inflearn.com/course/pm-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A6%AC%ED%84%B0%EB%9F%AC%EC%8B%9C&quot;&gt;PM을 위한 데이터 리터러시(프러덕트 데이터 분석)&lt;/a&gt; 강의와 &lt;a href=&quot;https://zzsza.github.io/data/2021/06/13/data-event-log-definition/&quot;&gt;블로그&lt;/a&gt; 자료&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    
    <item>
      <title>청구/수납 서비스 개발기</title>
      <link>https://spoqa.github.io/2023/02/24/bill-payment-development-story.html</link>
      <pubDate>Fri, 24 Feb 2023 00:00:00 +0000</pubDate>
      <author>남경호</author>
      <guid>/2023/02/24/bill-payment-development-story</guid>
      <description>&lt;p&gt;안녕하세요. 키친보드 제품팀의 백엔드 프로그래머 남경호입니다.&lt;/p&gt;

&lt;p&gt;엊그제 2023년을 축하했던 것 같은데 벌써 2월이 지나가고 있네요. 다들 느끼시겠지만, 시간이 참 빠른 것 같습니다.&lt;/p&gt;

&lt;p&gt;작년 키친보드는 많은 것을 이루었고 바꾸었습니다. 정비기간을 통해 &lt;a href=&quot;https://spoqa.github.io/2022/04/15/all-new-server.html&quot;&gt;서버 언어를 전환&lt;/a&gt;하였고요. 기존에 제공하던 정리 서비스가 아닌 &lt;a href=&quot;https://spoqa.github.io/2022/07/08/order-sheet-development-story.html&quot;&gt;주문 서비스&lt;/a&gt;를 새롭게 런칭하였습니다. 그리고 도도 카트에서 &lt;a href=&quot;https://kitchenboard.co.kr&quot;&gt;키친보드&lt;/a&gt;로 서비스명을 변경하면서 저희 서비스가 전달하는 가치를 사용자가 좀 더 명확하게 인지할 수 있도록 하였습니다.&lt;/p&gt;

&lt;p&gt;주문 서비스에 대한 시장성을 확인한 저희는 2022년 목표를 설정하여 이를 달성하기 위해 열심히 달렸고 마침내 목표했던 KPI를 달성하는 쾌거를 이루었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-02-24/2022-kpi-graph.png&quot; alt=&quot;2022-kpi-graph&quot; /&gt;&lt;/p&gt;

&lt;p&gt;키친보드는 주문 서비스의 성공적인 출시에 힘입어 고객에게 가치를 전달하기 위한 다음 스텝으로 청구/수납 서비스를 런칭하였습니다. 이 글에서는 청구/수납 서비스를 개발하면서 겪었던 여러 이야기들을 소개하고자 합니다.&lt;/p&gt;

&lt;h1 id=&quot;청구수납-서비스&quot;&gt;청구/수납 서비스?&lt;/h1&gt;

&lt;p&gt;여러분이 공과금을 납부하는 방식과 유사한 형태로 생각하면 이해하기 쉬울 것 같습니다. 매장은 식자재를 납품해주는 유통사에 음식을 조리하기 위한 식자재를 주문하게 됩니다. 주문마다 거래대금을 납부하는 형태가 아니라 외상과 같이 일정 기간의 거래대금을 한꺼번에, 유통사에 결제하는 형태를 가지는데요. 이때 유통사는 매장에 거래대금 납부를 요청하는 청구서를 발행하게 되고 매장은 이 청구서에 맞는 거래대금을 결제하는 방식을 청구와 수납이라고 합니다.&lt;/p&gt;

&lt;p&gt;키친보드는 요식업을 하는 매장과 해당 매장에 식자재를 납품해주는 유통사를 좀 더 세련되게 연결해주는 제품입니다. 주문/채팅 서비스가 매장이 유통사에 식자재를 주문하는 세련된 방법을 제공하였다면, 청구/수납 서비스는 유통사가 매장에 거래대금을 청구하고 매장이 청구된 금액을 수납하는 방식을 좀 더 세련되게 제공하고자 하는 것이 목표입니다.&lt;/p&gt;

&lt;h2 id=&quot;식자재-시장에서의-청구수납&quot;&gt;식자재 시장에서의 청구/수납&lt;/h2&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;&lt;img src=&quot;/images/2023-02-24/normal-bill-payment.png&quot; alt=&quot;normal-bill-payment&quot; /&gt;&lt;/p&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;즉, 언제든 청구 금액과 다른 금액을 매장에서 납부할 수 있는 것이 이곳 시장에서는 일반적인 형태라 볼 수 있습니다. (전기요금도 미납이 존재하지만 납부하지 않는 경우에만 해당하고 분할납부도 특정 기간 혹은 사유에 의해서 신청이 가능합니다.)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-02-24/kitchenboard-bill-payment.png&quot; alt=&quot;kitchenboard-bill-payment&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;목표&quot;&gt;목표&lt;/h2&gt;

&lt;p&gt;주문 서비스의 목표는 기존의 카카오톡(혹은 문자)으로 식자재를 주문하던 매장과 유통사의 경험을 최대한 해치지 않으면서 식자재 주문 방식을 개선하는 것이었습니다. 그래서 주문 서비스에 채팅 기능을 함께 탑재하는 것이 중요한 요소 중 하나였습니다. 이번 청구/수납 서비스의 목표 또한 유통사와 매장이 카카오톡(혹은 문자)을 통해서 이루어지던 청구/수납 경험을 최대한 해치지 않으면서 유통사에서는 좀 더 손쉽고 불편하지 않게 거래대금을 청구하고 매장에서는 편리하게 수납하고 수납내역을 확인할 수 있는 것들을 개선하고자 하였습니다.&lt;/p&gt;

&lt;h2 id=&quot;현실과-이상&quot;&gt;현실과 이상&lt;/h2&gt;

&lt;p&gt;저의 짧은 경험으로 제품을 만들 때 고객이 원했던 제품이 만들어지지 않았던 이유는 제품팀에서 생각했던 이상적인 모습과 실제 고객이 현실에서 느끼는 필요성에 대한 차이점 때문이라 생각합니다. 앞서 일반적인 청구/수납과 유통사와 매장의 청구/수납을 소개했던 이유도 크지 않을 수 있지만 제품의 기능을 만들 때 저희가 일반적으로 생각했던 청구/수납과 실제 유통사와 매장에서 이루어지고 있는 청구/수납에 차이가 있었기에 소개를 드린 것이었습니다. 이러한 부분들로 인해 제품팀에서는 제품을 개발하기 전에 요구사항을 정의하는 것에서부터 많은 시간을 투자하였습니다. 그럼, 대표적으로 어떤 것들이 있는지 한번 보겠습니다.&lt;/p&gt;

&lt;h3 id=&quot;청구-금액에-맞게-수납이-이루어지지-않는다&quot;&gt;청구 금액에 맞게 수납이 이루어지지 않는다.&lt;/h3&gt;

&lt;p&gt;만약 여러분이 전기요금 청구서에 적힌 금액에 맞지 않는 금액을 수납 계좌로 이체한 경우는 어떻게 될까요? 가상계좌로 이체하는 경우 과오납 체크를 하므로 이체가 되지 않습니다. 즉, 청구된 금액만큼 수납하여야 청구에 대한 수납이 완료되는 것입니다.&lt;/p&gt;

&lt;p&gt;하지만 앞서 유통사와 매장 간의 청구/수납에서 말씀드렸다시피 매장은 상황에 따라 유통사가 청구한 금액보다 적은 금액을 납부하기도 합니다. 그래서 가상계좌 발급을 통한 청구와 수납의 생명주기를 온전히 따르기에는 무리가 있었고 과오납 체크 또한 수행할 수 없는 이슈가 있었습니다.&lt;/p&gt;

&lt;h3 id=&quot;고객-전용-계좌를-사용해야-한다&quot;&gt;고객 전용 계좌를 사용해야 한다.&lt;/h3&gt;

&lt;p&gt;저희의 목표는 매장과 유통사의 기존 경험을 최대한 해치지 않으면서 식자재 주문 방식을 개선하는 것입니다. 앞서 청구 금액에 맞는 수납이 어렵다는 이유 말고도 기존에 청구와 수납의 경험을 해치지 않는 요소 중 하나가 바로 고객 전용 계좌를 두고 계좌이체를 하는 것입니다. 기존에는 아래와 같이 유통사가 보유하고 있는 계좌를 지정해두고 매장에 아래와 같이 입금요청을 보내는 방식으로 청구를 진행하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-02-24/bill-kakao-message.jpg&quot; alt=&quot;bill-kakao-message&quot; /&gt;&lt;/p&gt;

&lt;p&gt;매장에서는 유통사에 맞춰 계좌를 개설해놓고 이체를 할 수 있기에 이체 수수료에 대한 부담이 없고, 매번 계좌번호가 바뀌지 않기에 기억해두거나 최근 거래내역을 통해 이체를 할 수 있기에 편리함이 있습니다.&lt;/p&gt;

&lt;p&gt;하지만 청구할 때마다 새로운 가상계좌를 발급하여 수납을 요청한다면 청구한 금액과 일치하는 수납을 기대할 순 있지만 청구 금액보다 적은 금액의 수납을 허용하는 지금까지의 매장과 유통사의 청구/수납 프로세스를 갑작스럽게 변경하는 문제가 있기도 하고 가상계좌번호를 매번 새롭게 기억해서 이체해야 하는 번거로움 또한 발생하게 됩니다.&lt;/p&gt;

&lt;h3 id=&quot;미납금-추적이-힘들다&quot;&gt;미납금 추적이 힘들다.&lt;/h3&gt;

&lt;p&gt;키친보드는 유통사에서 사용하는 ERP를 대체하는 제품이 아닙니다. 그러다 보니 유통사가 ERP에서 이미 사용 중인 데이터를 키친보드의 데이터로 가져오는 것이 관건인데요. 아쉽게도 유통사에서 직접 키친보드에 미납금을 동기화해주지 않는다면 ERP의 미납금을 추적할 방법이 현재로선 딱히 존재하지 않습니다.&lt;/p&gt;

&lt;p&gt;이상적으로는 미납금 관리가 되어서 청구서에 미납금과 직전 거래금액이 합쳐진 금액이 청구 금액으로 표시되고 수납 시 미납 잔액과 연동되어 표시되면 좋겠다는 생각이 듭니다. 유통사에서 일일이 미납금 데이터 동기화를 해주면 좋겠지만 고객에게 번거로움을 줄 수 있고 사람이 직접 해야 하는 일이다 보니 누락이 생길 수 있습니다. 그래서 미납금을 어설프게 표시하여 고객에게 혼란을 주기보다는 미납금 관리를 과감하게 포기하고 청구 시 고객이 원하는 청구 금액을 입력할 수 있도록 자유도를 높이는 방법으로 우회하였습니다.&lt;/p&gt;

&lt;h3 id=&quot;청구-없이-수납할-수-있다&quot;&gt;청구 없이 수납할 수 있다.&lt;/h3&gt;

&lt;p&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;그러한 노력 끝에 지금 시점에 도출해낼 수 있는 최선이 선택하게 되었고 그 결과물이 지금 출시된 청구/수납 서비스입니다. 사실 이렇게 논의를 마치고 제품 개발을 진행하였음에도 불구하고 개발 중간중간에도 QA를 진행하고 있는 와중에서도 이상적인 부분에 대한 의견이 다시금 나오기도 하였습니다^^;; 하지만 저희 팀은 충분히 고민하고 조율한 끝에 내린 결론이었기에 새로운 의견이 나와도 목표 지점을 잃지 않고 원하는 시점에 계획했던 제품을 성공적으로 출시할 수 있게 되었습니다.&lt;/p&gt;

&lt;p&gt;저는 언제든 제품을 만들면서 현실과 이상 간의 괴리는 언제든 발생할 수 있다고 생각합니다. 다만, 이 괴리를 모두가 조정하고 합의한 다음 제품을 만들어 나가야 팀의 목표를 놓치지 않고 정체성을 가질 수 있다고 생각합니다.&lt;/p&gt;

&lt;h1 id=&quot;유저스토리&quot;&gt;유저스토리&lt;/h1&gt;

&lt;p&gt;앞선 논의를 통해 도출해낸 요구사항들을 토대로 아래와 같이 유저스토리들이 도출되었습니다. (관리기능과 주요하지 않은 스토리들은 제외하였으니 이 부분은 참고해주세요.)&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;유통사는 ERP 데이터를 이용하여 청구서를 일괄 생성할 수 있다.&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;/li&gt;
  &lt;li&gt;매장은 청구 금액을 납부한 경우 납부 완료 메시지를 수신할 수 있다.&lt;/li&gt;
  &lt;li&gt;매장은 유통사가 발송한 미납알림 메시지를 수신할 수 있다.&lt;/li&gt;
  &lt;li&gt;매장은 자신의 수납내역을 조회할 수 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;흐름도&quot;&gt;흐름도&lt;/h1&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;img src=&quot;/images/2023-02-24/normal-bill-payment-in-kitchenboard.png&quot; alt=&quot;normal-bill-payment-in-kitchenboard&quot; /&gt;&lt;/p&gt;

&lt;ol&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;/ol&gt;

&lt;h2 id=&quot;청구-금액보다-적은-금액을-납부하는-경우&quot;&gt;청구 금액보다 적은 금액을 납부하는 경우&lt;/h2&gt;

&lt;p&gt;다음으로는 유통사와 매장에서 흔하게 발생하는 경우인 청구 금액보다 적은 금액을 납부하는 경우를 다루어보겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-02-24/unpaid-bill-payment-in-kitchenboard.png&quot; alt=&quot;unpaid-bill-payment-in-kitchenboard&quot; /&gt;&lt;/p&gt;

&lt;ol&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;/ol&gt;

&lt;p&gt;아마 여기서 여러분은 위 흐름도에서 아래의 2가지 의문점을 가질 수 있을 것입니다. 바로 이 부분이 저희가 마주한 이상과 현실의 괴리 중 첫 번째입니다. 앞서 말씀드린 바와 같이 현재로선 완벽하게 청구의 종료 시점과 미납금을 관리하기 어렵다고 판단하였고 이에 대해 아래와 같이 타협점을 찾았습니다.&lt;/p&gt;

&lt;h3 id=&quot;처음-발행된-청구서의-청구-금액이-다-수납되지-않았는데-청구서가-만료되었다는-것을-어떻게-알-수-있는가&quot;&gt;처음 발행된 청구서의 청구 금액이 다 수납되지 않았는데 청구서가 만료되었다는 것을 어떻게 알 수 있는가?&lt;/h3&gt;

&lt;p&gt;매장이 정확히 청구한 금액을 수납한다는 것을 보장하기 어려우므로 청구서의 생명주기를 다음 청구서가 생성될 때까지로 정합니다. 그래서 다음 청구서가 발행되는 시점에 직전 청구서를 만료하는 방법으로 우회하였습니다. 그럼, 청구서와 수납과의 관계가 모호해지지 않는가? 라는 질문을 할 수 있을 텐데요. 물론 그럴 수 있습니다. 하지만 대부분의 경우 유통사에서는 거래대금을 매장에 요청하기 위해 청구서를 새롭게 발급할 것이고 자연스럽게 청구와 수납 간의 관계가 원하는 대로 맺어질 것이라 기대하고 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;다음-청구서의-청구-금액이-미납금--거래대금인데-미납금이-자동으로-계산되지-않고-직접-입력해야-하는가&quot;&gt;다음 청구서의 청구 금액이 미납금 + 거래대금인데 미납금이 자동으로 계산되지 않고 직접 입력해야 하는가?&lt;/h3&gt;

&lt;p&gt;앞서 말씀드렸다시피 키친보드는 유통사에서 사용하는 ERP를 대체하지 않습니다. 그래서 ERP에서 관리되는 미납금을 정확하게 추적하고 동기화하기가 어렵다는 이슈가 있습니다. 유통사에 미납금을 동기화해달라고 할 순 있지만 관리 포인트가 늘어나므로 고객의 경험을 해친다는 점과 유통사 담당자가 수기로 관리하기 때문에 누락 및 오입력과 같은 이슈가 있어 신뢰성 있는 미납금 관리가 현실적으로 어려울 수 있다고 판단했습니다. 돈을 다루는 부분에서는 모호한 데이터를 표시하기보다는 유통사가 원하는 청구 금액을 손쉽게 입력하여 매장에 전달할 수 있도록 하는 것에 집중하였습니다.&lt;/p&gt;

&lt;h2 id=&quot;청구-없이-수납하는-경우&quot;&gt;청구 없이 수납하는 경우&lt;/h2&gt;

&lt;p&gt;다음으로는 청구서가 발행되지 않았음에도 매장이 수납하는 경우를 다루어보겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-02-24/payment-without-bill.png&quot; alt=&quot;payment-without-bill&quot; /&gt;&lt;/p&gt;

&lt;ol&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;/ol&gt;

&lt;p&gt;일반적인 청구와 수납구조를 이해하고 계신다면 이상해 보일 것입니다. 왜냐하면 청구 없이 수납이 존재하기 때문입니다. 앞서 여러 이유로 인해 가상계좌가 아닌 고객 전용 계좌를 사용하게 되었다고 말씀드렸었는데요. 매장에 계좌번호가 한번 공유되고 나면 매장에서는 언제든지 미납금에 대해 수납을 할 수 있습니다. 심지어 키친보드에서는 미납금을 정확히 알기 어려우니 해당 수납금이 과납인지, 혹은 오납인지조차 알 수도 없습니다.&lt;/p&gt;

&lt;p&gt;그래서 저희는 과감하게 청구와 수납 간의 관계를 느슨하게 가져가 보기로 하였습니다. 청구서 없이도 매장은 언제든지 수납을 할 수 있고 만약 활성화된 청구서가 존재한다면 수납과 청구와의 관계를 선택적으로 맺어주는 형태로 개발하게 되었습니다.&lt;/p&gt;

&lt;h1 id=&quot;설계문서&quot;&gt;설계문서&lt;/h1&gt;

&lt;p&gt;위 유저스토리와 흐름도를 기반으로 저희 백엔드 챕터에서는 아래와 같이 설계문서를 작성하였습니다. 평소에도 개발하기 전에 설계문서를 작성해왔지만, 이번 프로젝트에서는 개발자끼리도 오해가 생기지 않도록 더욱더 꼼꼼하고 자세하게 적으려고 노력하였습니다. 이번 프로젝트에서는 백엔드에서 작업할 내용이 많고, 고민해야 할 것들이 많다 보니 유달리 문서량이 많았던 것 같습니다 ^^;;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-02-24/bill-payment-design.gif&quot; alt=&quot;bill-payment-design&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;message-throttling&quot;&gt;Message Throttling&lt;/h1&gt;

&lt;p&gt;개발기인데 개발적인 이야기가 빠진다면 섭섭하겠죠? 지금까지는 청구/수납 서비스에 대한 배경과 설계에 대한 이야기였다면 청구/수납기능을 개발하면서 겪었던 대표적인 이슈를 소개해보자 합니다.&lt;/p&gt;

&lt;p&gt;유통사와 매장은 기존에 카카오톡이나 문자를 통해서 주문도하고 소통도 하고 결제 대금 청구도 수행하였습니다. 그러다 보니 키친보드에서도 채팅 기능은 중요한 기능 중 하나인데요. 저희는 주요 알림 및 채팅 기능을 샌드버드를 이용하여 제공하고 있습니다. 주문하거나 접수 완료할 때도 채팅 알림을 통해서 수행하고 이번에 청구 및 수납을 할 때도 채팅 알림을 통해서 매장과 유통사에 알림을 전송합니다.&lt;/p&gt;

&lt;p&gt;이슈는 아래 유저스토리에서 다량의 메시지를 동시에 발송해야 하는 요구사항에서 발생하였습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;유통사는 ERP 데이터를 이용하여 청구서를 일괄 생성할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;유통사와 거래하는 여러 매장들에 일괄적으로 청구서를 발행한다면 키친보드 서버에서는 &lt;a href=&quot;https://sendbird.com/docs/chat/v3/platform-api/overview&quot;&gt;샌드버드의 Platform API&lt;/a&gt;를 통해 유통사와 매장이 들어가 있는 채팅방에 일괄적으로 청구서가 생성되었다는 메시지를 전달하게 됩니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-02-24/bill-message.png&quot; alt=&quot;bill-message&quot; /&gt;&lt;/p&gt;

&lt;p&gt;문제는 &lt;a href=&quot;https://sendbird.com/docs/chat/v3/platform-api/application/understanding-rate-limits/rate-limits#2-rate-limited-apis&quot;&gt;샌드버드의 Platform API에 제약조건&lt;/a&gt;이 있다는 것입니다. 채팅방에 메시지를 전송할 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/{channel_type}/{channel_url}/messages&lt;/code&gt; API를 사용하는데 초당 5건의 요청 제한이 걸려있습니다. (실제로 테스트해보면 일시적으로 요청하는 정도라면 초당 10개 정도까지는 받아주는 듯 합니다.)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-02-24/sendbird-platform-api-rate-limit.png&quot; alt=&quot;sendbird-platform-api-rate-limit&quot; /&gt;&lt;/p&gt;

&lt;p&gt;결국 유통사에서 다수 매장에 청구서를 발행하게 되면 샌드버드의 메시지 전송 제약에 의해 채팅 메시지를 실패할 수 있다는 우려가 제기되었습니다. 실제로 테스트 해보았을 때도 30개의 매장에, 동시에 청구서를 발행하는 경우 오류가 발생하는 것을 확인할 수 있었습니다.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Too many requests (scope: user, limit: 5;w=1, remaining: 0, retry_after: 0.16522979736328s, reset: 1.9652297496796s).&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;500910&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그럼, 위 문제점을 어떻게 해결할 수 있을까요?&lt;/p&gt;

&lt;h2 id=&quot;sendbird의-announcement-message-활용&quot;&gt;Sendbird의 Announcement Message 활용&lt;/h2&gt;

&lt;p&gt;Sendbird는 &lt;a href=&quot;https://sendbird.com/docs/chat/v3/platform-api/message/announcements/announcement-overview&quot;&gt;Announcement Message&lt;/a&gt;를 통해 다수의 사용자(최대 2만 명)에게 메시지를 전송할 수 있도록 기능을 제공해줍니다. 목적 자체가 다수의 사용자에게 메시지를 전달하는 것이기에 Platform API가 가진 사용자별로 초당 5개의 메시지 전송 제한을 두고 있지 않아 고려해봄 직하였습니다. 키친보드에서도 유통사의 공지사항을 전파하기 위한 용도로 이미 사용하고 있기도 해서 연동도 큰 문제는 되지 않았습니다.&lt;/p&gt;

&lt;p&gt;하지만 Announcement Message는 다수의 사용자가 동일한 메시지만 수신할 수 있습니다. 앞서 보여드린 청구 메시지를 보시면 아시겠지만, 매장이 수신하는 청구 메시지는 청구 금액과 계좌번호, 예금주가 매장마다 다르게 표시되어야 합니다. 그래서 아쉽게도 Announcement Message는 사용할 수 없다는 결론을 내렸습니다.&lt;/p&gt;

&lt;h2 id=&quot;message-queue의-delayed-message-활용&quot;&gt;Message Queue의 Delayed Message 활용&lt;/h2&gt;

&lt;p&gt;다음으로 제안되었던 방법은 Message Queue에서 제공해주는 &lt;a href=&quot;https://activemq.apache.org/delay-and-schedule-message-delivery&quot;&gt;Delayed Message&lt;/a&gt;를 활용하는 것입니다. ActiveMQ나 RabbitMQ에서는 Delayed Message 기능을 제공합니다. Delayed Message는 쉽게 말해서 MQ에서 메시지를 원하는 시간만큼 지연 발송해주는 기능입니다.&lt;/p&gt;

&lt;p&gt;Platform API가 초당 5개의 메시지 전송 제한이 있으므로, 0.2초당 하나의 메시지만 발송하도록 지연 발송한다면 문제없이 해결할 수 있으리라 생각했습니다. 물론 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Thread.sleep(200)&lt;/code&gt;과 같이 서버 내 코드로 지연발송을 사용할 수 있습니다. 하지만 서버 내 코드에서 지연발송을 구현한다면 아래와 같은 문제점이 발생합니다.&lt;/p&gt;

&lt;h3 id=&quot;비즈니스-코드가-외부-의존성을-의존한다&quot;&gt;비즈니스 코드가 외부 의존성을 의존한다.&lt;/h3&gt;

&lt;p&gt;일단 코드 수준에서 좋지 못한 디자인을 유발합니다. 도메인 코드는 외부 서비스에 대한 의존성을 최소화하는 것이 좋습니다. 그래서 외부 의존성인 Sendbird의 제약조건을 비즈니스 코드에 녹이는 것은 바람직하지 않다고 생각했습니다. 극단적으로 혹시나 채팅 서비스를 Sendbird가 아닌 다른 서비스로 교체한다고 가정해보겠습니다. 다른 서비스는 Sendbird와 다른 제약조건을 가질 수 있습니다. 그래서 외부 의존성의 변경이 도메인 코드의 변경을 유발하게 되는 좋지 않은 상황이 발생하게 됩니다.&lt;/p&gt;

&lt;h3 id=&quot;api의-성능저하를-유발한다&quot;&gt;API의 성능저하를 유발한다.&lt;/h3&gt;

&lt;p&gt;0.2초의 지연발송을 publisher 쪽에 구현한다면 메시지를 발송하는 Application Server의 API 성능은 최소 0.2초 이상의 응답시간을 가지게 됩니다. 이는 비동기 발송으로 풀어낼 수 있습니다만 애플리케이션 내 비동기 발송은 메시지 유실과 같은 또 다른 문제를 야기합니다.&lt;/p&gt;

&lt;h3 id=&quot;메시지-유실-가능성이-존재한다&quot;&gt;메시지 유실 가능성이 존재한다.&lt;/h3&gt;

&lt;p&gt;앞서 API의 성능을 올리기 위해 채팅 메시지 발송을 비동기로 처리할 수 있습니다. 서버는 별도의 스레드에서 메시지 발송을 대기하게 됩니다. 다만 예상치 못한 상황에서 서버가 다운되는 경우 사용자는 청구서 발송이 성공했다는 응답을 받았지만, 채팅 메시지를 받지 못한 상황이 발생할 수 있습니다. 서버가 &lt;a href=&quot;https://www.techtarget.com/whatis/definition/graceful-shutdown-and-hard-shutdown&quot;&gt;Graceful shutdown&lt;/a&gt;을 제공한다면 그나마 이러한 상황을 방지할 최소한의 안전장치를 마련했다고 할 순 있겠습니다. 하지만 프로그래밍에 있어서 0.2초는 참으로 긴 시간일 수 있기에 여전히 위험성을 가지고 있다고 생각했습니다.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;위와 같은 문제로 저희는 좀 더 안정적으로 지연 메시지를 사용하기 위한 방법으로 Message Queue를 사용하는 것이 좋다고 판단하였습니다. 하지만 Delayed Queue를 사용해도 앞서 말한 문제를 근본적으로 해결할 수 없었는데요. 그 이유는 바로 서버의 가용성과 성능향상을 위해 Scale-out 되어 있었기 때문입니다.&lt;/p&gt;

&lt;p&gt;좀 더 이해하기 쉽게 그림으로 표현해보겠습니다. 만약 여러 유통사가 청구서를 발송한다고 가정해보겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-02-24/delayed-message-problem.png&quot; alt=&quot;delayed-message-problem&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 그림에서 볼 수 있다시피 Message Queue Delayed Message는 서버에서 요청 시 얼마나 메시지를 지연할 것인지를 결정하기 때문에 Scale-out 된 서버에서 각각의 서버가 동시에 지연 메시지 발송요청을 하는 경우 Sendbird의 API 요청 제한을 초과하게 될 가능성이 존재했습니다.&lt;/p&gt;

&lt;h2 id=&quot;consumer-message-throttling-활용&quot;&gt;Consumer Message Throttling 활용&lt;/h2&gt;

&lt;p&gt;위 문제점들을 해결하기 위해 최종적으로 도출된 방법은 바로 메시지를 소비하는 Consumer에서 Message Throttling을 수행하는 것입니다. 즉 Messge Queue로 전달하는 메시지는 지연 발송하지 않고 즉시 전송하게 되고 메시지를 소비하는 쪽에서 Throttling을 걸어서 Sendbird의 API 제한조건에 맞도록 전송하도록 하는 것입니다.&lt;/p&gt;

&lt;p&gt;하지만 이 방법도, 만약 Consumer가 Scale-out 된다면 앞서 말한 Sendbird 의 API 요청 제한을 초과할 가능성은 여전히 남아있게 됩니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-02-24/cunsumer-problem.png&quot; alt=&quot;cunsumer-problem&quot; /&gt;&lt;/p&gt;

&lt;p&gt;저희는 이 부분을 해결하기 위해 라이브러리의 힘을 빌리기로 하였습니다. 바로 &lt;a href=&quot;https://github.com/bucket4j/bucket4j&quot;&gt;bucket4j&lt;/a&gt;입니다. buket4j는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Token_bucket&quot;&gt;Token bucket&lt;/a&gt;이라는 알고리즘을 기반으로 분산 환경에서도 원하는 속도제한을 수행할 수 있도록 기능을 제공해주는 라이브러리입니다. 즉, 여러 대의 Consumer가 존재하더라도 마치 하나의 Consumer가 지연발송 할 수 있도록 메시지 전송시간을 조절해줍니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2023-02-24/consumer-with-bucket.png&quot; alt=&quot;consumer-with-bucket&quot; /&gt;&lt;/p&gt;

&lt;p&gt;한편 메시지 발송하는 쪽에서 bucket4j를 이용하여 지연발송 할 수 있지 않는가에 대한 질문을 할 수 있습니다. 다만 발송하는 쪽 즉, Publisher에서 Message Throttling을 하는 경우 앞서 말한 동기 코드의 경우에 성능 문제, 비동기 코드의 경우 메시지 유실과 같은 문제를 동일하게 내포하고 있기에 Message Queue를 통해 전달보증을 꾀하였습니다. 만약 Consumer에서 메시지 소비를 대기하다가 모종의 이유로 서버가 다운되어 API 전송에 실패하는 경우 Message Queue는 재시도를 요청할 것이고 이때 메시지를 소비할 수 있는 서버가 해당 메시지를 소비하여 사용자에게 메시지를 반드시 전달해주게 될 것입니다.&lt;/p&gt;

&lt;p&gt;Message Throttling에 대한 간단한 &lt;a href=&quot;https://github.com/veluxer62/throttling-consumer-tutorial&quot;&gt;튜토리얼&lt;/a&gt;을 만들어두었으니 해당 코드를 참고해주세요.&lt;/p&gt;

&lt;h1 id=&quot;마무리&quot;&gt;마무리&lt;/h1&gt;

&lt;p&gt;지금까지 청구/수납 서비스를 개발하면서 고민하였던 부분과 개발적인 이슈들을 살펴보았습니다. 이번 글은 개발적인 내용보다는 일반적이지 않은 유통사 시장의 청구와 수납에 대한 이해와 제품의 설계이유를 설명하기 위한 내용이 대부분이어서 조금은 아쉬울 수 있다는 생각이 듭니다. 다만 저희 스포카에서 제품을 만들 때 고객을 위해 어떤 고민을 하고 더 나은 제품과 고객 경험을 제공하기 위해 어떠한 노력을 하고 있는지 여러분에게 소개해드릴 수 있게 되었다는 점에서 나름대로 의미를 부여하고 싶습니다.&lt;/p&gt;

&lt;p&gt;앞으로도 제품 개발에 대한 이야기를 많이 풀어보도록 하겠습니다. 긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>1년차 개발자의 훈수 두기</title>
      <link>https://spoqa.github.io/2022/09/01/first-year-developers-tips.html</link>
      <pubDate>Thu, 01 Sep 2022 00:00:00 +0000</pubDate>
      <author>이지민</author>
      <guid>/2022/09/01/first-year-developers-tips</guid>
      <description>&lt;p&gt;안녕하세요. 스포카 제품팀의 백엔드 프로그래머 이지민입니다.&lt;/p&gt;

&lt;p&gt;어느덧 스포카에 입사한 지, 개발자가 된 지 1년이 되었습니다!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/first-year-developers-tips/minions.gif&quot; alt=&quot;project&quot; /&gt;&lt;/p&gt;

&lt;p&gt;개발자 1년, 참 고생도 많이 하고 다사다난했던 것 같습니다. 그만큼 배운 점도 많기에 이 글에서는 1년 동안 스포카를 다니면서 신입 개발자로서 느낀 것들과 배운 점들, 그리고 질문받았던 내용들을 담아보려고 합니다.&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;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/first-year-developers-tips/wild.png&quot; alt=&quot;project&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;학교와-회사의-목적과-목표&quot;&gt;학교와 회사의 목적과 목표&lt;/h3&gt;
&lt;p&gt;학교의 목표는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;학생들을 가르치고 보호한다&lt;/code&gt; 에 있기 때문에 안전한 울타리가 저를 감싸고 보호해준다는 느낌이 들었습니다. 하지만, 회사의 목표는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;고객을 만족시키고 이익을 얻는다&lt;/code&gt; 에 있기 때문에 보호보다는 일을 잘할 수 있는 환경을 만들어주는 것이 회사의 역할이라고 생각합니다.&lt;/p&gt;

&lt;p&gt;이런 목표와 역할 차이 때문에 처음에는 매우 불안했던 것 같습니다. 회사에서는 이 신입이 회사에 이익을 가져다줄 수 있는지 평가하고 저는 그 평가 속에서 기대에 부응해야 했기 때문에 솔직하게 부담이 많이 되었습니다.&lt;/p&gt;

&lt;p&gt;그렇기 때문에 무리하게 일하기도 했고 많이 속상하기도 했습니다.
하지만 점차 어떻게 일하는지에 대해 배우고 학교와 회사의 목표 차이를 인지하며 많이 익숙해지니 학교처럼 회사도 점차 편안해졌습니다.
회사에 입사하기 전, 학교와 회사는 목표 자체가 다르다는 것을 인지하시고 그 차이에 너무 놀라지 않으셨으면 좋겠습니다.&lt;/p&gt;

&lt;p&gt;하지만 목적과 목표가 다름에도 불구하고 학교와 회사의 공통점이 있다면 바로 개인의 성장과 배움을 적극적으로 지원한다는 것입니다. 여전히 회사에서도 배워야 할 것들은 많으며 스포카에서는 이를 학습 할 수 있도록 적극적으로 지원해주고 있습니다. 덕분에 개인의 성장에도 큰 도움이 되는 것 같습니다.&lt;/p&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;하지만 회사에서는 서비스를 계속해서 확장해나가고 버그를 수정하고 유지 가능하도록 코드를 작성해야 합니다.
가장 중요한 이유는 혼자 하는 프로젝트가 아닌 다른 동료와 함께 일한다는 것입니다. 제가 코드를 복잡하게 짠다면 다른 동료가 같은 작업부분을 건드려야 할 때 다른 동료는 시간을 더 많이 사용하게 됩니다.
그렇게 된다면 제 작업 효율뿐만 아니라 함께 일하는 다른 동료의 작업 효율까지도 낮출 수 있는 일이 됩니다.
그렇기 때문에 더욱 책임감을 가지고 코드를 깔끔하게 유지하고 인터페이스를 잘 마련해주어야 한다고 생각합니다.&lt;/p&gt;

&lt;p&gt;이런 큰 책임감과 부담감 때문에 심리적으로는 힘들었지만 저를 부지런히 공부하게 만들었고 코드에 대한 고민도 많이 하게 만들어 주는 원동력으로 작용한 것 같습니다.
덕분에 처음의 저와 비교했을 때 많은 성장을 이뤄낸 것 같습니다.&lt;/p&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;위에서 설명한 회사와 학교의 차이점들 때문에 입사 후 첫 석 달 간은 매우 힘들었던 것 같습니다.
물론 개인차가 크겠지만 저는 소심하고 생각이 많은 INFP이기 때문에(?) 더욱 그랬던 것 같네요.&lt;/p&gt;

&lt;p&gt;마치 이상한 변호사 우영우에 나왔던 우영우 변호사가 다른 공간에 들어서기 전, 하나 둘 셋을 세 듯 신입 개발자인 저도 하나 둘 셋의 시간을 견디며 회사라는 다른 공간에 적응해 나갔습니다.
물론 개인의 3초가 다르듯 더 빠를 수도 있고 느릴 수 있겠지만 혹은 다른 공간 안에 또 다른 공간이 있을 수 있겠지만 신입 개발자분들이 조급함 없이 자신에 맞게 셋을 센다면 학교와 회사의 차이에도 어렵지 않게 적응할 수 있을 겁니다.
&lt;img src=&quot;/images/first-year-developers-tips/woo-young-woo.png&quot; alt=&quot;project&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;신입-개발자의-재택근무&quot;&gt;신입 개발자의 재택근무&lt;/h2&gt;
&lt;p&gt;첫 입사 당시, 스포카에서는 코로나로 인해 전원 재택근무를 하고 있었습니다.
입사 전에는 재택근무는 긍정의 이미지가 강했습니다. 출퇴근이 없다는 것, 집에서 근무를 할 수 있다는 것에 큰 기대를 했었습니다.&lt;/p&gt;

&lt;p&gt;하지만, 실제로 재택근무가 신입 개발자가 일하기 좋은 환경인가에 대해서는 어느 정도 단점이 존재함을 느꼈습니다.&lt;/p&gt;

&lt;p&gt;예민한 사항을 다루는 부분이라 조심스럽긴 하지만 재택근무를 하면서 느낀 장단점을 정리해보려고 합니다.
분명히 말씀드리지만, 개인차가 존재할 수 있으며 시니어 개발자 기준이 아닌 신입 개발자의 기준에서 정리한 것임을 한 번 더 인지해주시기를 바랍니다!&lt;/p&gt;

&lt;p&gt;제가 재택근무를 하면서 느낀 재택근무의 장단점입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/first-year-developers-tips/working-from-home.png&quot; alt=&quot;project&quot; /&gt;&lt;/p&gt;

&lt;p&gt;사실 장점은 많이들 파악하고 계실 거로 생각합니다.
신입 개발자도 장점을 그대로 가져가긴 하지만 제 개인적인 생각으로는 단점이 장점보다 더 크게 작용할 것입니다.
따라서 아래에서는 어떻게 보면 재택근무를 비판한다고 보일 수 있겠지만 시니어 개발자의 경우 재택근무가 효율을 크게 높여준다는 것에 매우 동의합니다.&lt;/p&gt;

&lt;p&gt;장점보다는 단점에 조금 더 치우쳐서 소개해드리겠습니다.&lt;/p&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;물론 결과만을 놓고 보는 것에 대해서 크게 잘못됨을 느끼지 않는 분들이 많겠지만 신입 개발자가 보여줄 수 있는 성실과 열정을 놓치게 됩니다.
결과만을 놓고 보았을 때 신입 개발자가 얼마나 큰 노력과 시간을 들였는지 파악하지 못한 채 성과를 측정하며, 성과만을 놓고 성실에 대한 여부를 판단할 수 있습니다.&lt;/p&gt;

&lt;p&gt;재택근무를 하지 않는다면 신입 개발자는 자신의 시행착오들을 모두 보여줄 수 있습니다.
이런 시행착오를 했으며, 얼마나 많은 시간을 쏟았는지 의자에서 고민하는 모습들을 성과 측정자의 눈에 보여주며 성실을 표현할 수 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;어색한-팀원들&quot;&gt;어색한 팀원들&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/first-year-developers-tips/awkward.png&quot; alt=&quot;project&quot; /&gt;
두 번째는 팀원들과 친해지기 어렵다는 것입니다.
재택근무를 하다보니 슬랙을 이용하는 경우가 많고 직접 만나 의사소통하는 경우가 줄어들다 보니 모든 팀원과 친해지기 어려운 것이 사실이었습니다. 
얼굴도 모르고 소통하다 보니 요청하는 것에 대한 망설임이 있었고 더 조심스러웠습니다.&lt;/p&gt;

&lt;p&gt;코로나 시기가 좀 잠잠해진 입사 후 몇 주가 지나서야 처음으로 일부 팀원들을 만나 소통할 수 있었고 팀장님께서 신입인 저와 팀원들이 친해질 기회들을 많이 제공해주셨습니다.
그때서나 팀원들과 많이 가까워진 느낌이 들었습니다.&lt;/p&gt;

&lt;h3 id=&quot;질문하기-어려운-환경&quot;&gt;질문하기 어려운 환경&lt;/h3&gt;
&lt;p&gt;세 번째는 많은 질문을 할 수 있는 환경이 아니었다는 것입니다. 개발을 하다 보면 여러 고민거리가 생깁니다.
회사에서는 툭 튀어나온 간단한 고민들을 산발적으로 시니어 개발자에게 공유하며 빠르게 결정하고 알아갈 수 있습니다.&lt;/p&gt;

&lt;p&gt;하지만 재택근무를 하게 되면 간단한 질문들을 혼자 고민하게 되고 비교적 시간을 더 사용하게 됩니다.
왜냐하면 재택근무 시에는 텍스트 형식으로 질문들을 많이 하게 되는데 텍스트로 질문할 때 더 많은 설명들이 필요하고 더 신중해지기 때문에 질문을 하는 데도 많은 정리가 필요합니다.
즉, 더 많이 신경을 써서 질문을 해야 한다고 느꼈습니다. 그 때문에 산발적인 질문이 크게 오가지 않는 것 같습니다.&lt;/p&gt;

&lt;h3 id=&quot;어려운-업무와-일상의-분리&quot;&gt;어려운 업무와 일상의 분리&lt;/h3&gt;
&lt;p&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;img src=&quot;/images/first-year-developers-tips/document.png&quot; alt=&quot;project&quot; /&gt;&lt;/p&gt;

&lt;p&gt;그렇다면, 어떨 때 문서화를 하면 좋은지와 문서화를 했을 때의 장점들을 나열해보겠습니다.&lt;/p&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;매일 기록하는 것이 귀찮은 일일 수도 있지만 쌓여가는 기록을 보면 회사의 기여 정도나 자신의 성장 정도를 체크해볼 수도 있어서 동기부여에도 좋은 역할을 합니다.&lt;/p&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;그리고 회의 진행 시 결론과 대화를 기록하며 회의 내용이 휘발되지 않도록 하는 것 또한 문서화를 함께하는 회의에서 중요한 요소인 것 같습니다.&lt;/p&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;히스토리를 더 잘 파악하기 위해서는 많은 문서가 필요하고 카테고라이징이 잘 되어 있으며,
심지어는 읽어야 하는 문서가 명확하다면 문서들의 효과를 더욱 많이 볼 수 있을 것 같습니다.&lt;/p&gt;

&lt;h3 id=&quot;질문-답변-기록&quot;&gt;질문 답변 기록&lt;/h3&gt;
&lt;p&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;/p&gt;

&lt;p&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;h3 id=&quot;잦은-질문&quot;&gt;잦은 질문&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/first-year-developers-tips/question.png&quot; alt=&quot;project&quot; /&gt;&lt;/p&gt;

&lt;p&gt;스포카의 시니어 개발자는 불쑥 저에게 어려운 질문들을 많이 해주셨습니다. “이거 알아요? 저거 알아요?” 이런 질문들이 저를 많이 배울 수 있게 해준 것 같습니다.
제가 모르는 개념들은 너무나도 많았고 모르는 개념에 대한 질문을 받으면 답변하기 위해 공부했던 것들이 많은 도움이 되었습니다.&lt;/p&gt;

&lt;p&gt;물론 질문들을 받으면 공부해야 한다는 생각에 마냥 좋지만은 않았지만, 새로 알게 되는 개념들이 많았기 때문에 저는 흥미롭고 많은 도움이 되었습니다.&lt;/p&gt;

&lt;p&gt;시니어 개발자가 해주는 간단한 질문들은 신입 개발자에게는 큰 공부가 될 것 같습니다&lt;/p&gt;

&lt;h3 id=&quot;페어-프로그래밍&quot;&gt;페어 프로그래밍&lt;/h3&gt;
&lt;p&gt;페어 프로그래밍은 특히 코드 convention에 대해서 잘 모를 때 convention에 대한 이해가 깊은 개발자와 하면 큰 도움이 됩니다.
하지만 페어 프로그래밍은 한 작업을 두 작업자가 함께하므로 효율이 더 높지 않다는 점과 시니어 개발자와 신입 개발자가 함께 페어 프로그래밍을 하면 상대적으로 신입 개발자가 얻는 지식은 많지만 시니어 개발자가 얻는 지식은 많지 않다는 단점이 있습니다.
그럼에도 불구하고 한 신입 개발자의 코드 적응에는 큰 도움이 됨을 확신하고 큰 변화를 가져다줄 수 있다고 생각합니다.
팀의 지금 당장의 효율보다는 이후에 효율을 생각했을 때 페어 프로그래밍은 분명 큰 도움이 될 것입니다.&lt;/p&gt;

&lt;p&gt;저는 TDD를 페어 프로그래밍으로 했는데 테스트 코드를 짜는 방법과 코드 convention 등 여러 개발 지식을 많이 얻었습니다. 페어 프로그래밍에 참여하는 모두가 힘들어하긴 했지만 좋은 경험이었고 지금도 가끔 페어 프로그래밍을 요청하기도 합니다.&lt;/p&gt;

&lt;h3 id=&quot;꼼꼼한-pr-리뷰&quot;&gt;꼼꼼한 PR 리뷰&lt;/h3&gt;
&lt;p&gt;제가 첫 입사 때를 생각해보면 저는 좋은 코드가 무엇인지 나쁜 코드가 무엇인지에 대한 인지가 깊지 않았습니다.
그렇기 때문에 이를 알아갈 방법이 필요했고 그것이 PR 리뷰였습니다.
&lt;img src=&quot;/images/first-year-developers-tips/pr-review.png&quot; alt=&quot;project&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 PR 리뷰처럼 어떤 의존성을 가지고 코드를 작성해야 하는 지와 코드의 가독성을 높이는 방법, 테스트코드를 더 명확하게 짜는 방법, SRP 등의 개발 원칙 등 꼼꼼히 리뷰해주시고 설명해주심과 동시에 코드의 질은 높아졌습니다.&lt;/p&gt;

&lt;p&gt;또한 PR 리뷰의 중요성과 PR 리뷰를 더 의미 있게 보는 방법에 대해서도 신입 개발자가 인지할 수 있도록 한다면 신입 개발자가 PR 리뷰를 받는 입장뿐만 아니라 PR 리뷰를 하는 입장에서도 더욱 책임감 있게 리뷰를 할 수 있게 될 것입니다.&lt;/p&gt;

&lt;h3 id=&quot;자신감을-올려주는-행위&quot;&gt;자신감을 올려주는 행위&lt;/h3&gt;
&lt;p&gt;마지막으로 자신감을 높여주는 행위입니다. 무슨 말인가 싶을지 모르겠지만 저는 제법 중요하다고 생각합니다.
신입 개발자는 사실 자신이 개발을 잘하지 못한다고 생각할 때가 많습니다. 또한 회사에 내가 필요한 사람인가를 항상 고민할 수 있습니다.
하지만 회사에 필요한 사람이 되는 순간 뿌듯함을 느끼고 더 열심히 일해야지 하는 의지를 얻는 것 같습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/first-year-developers-tips/letter.png&quot; alt=&quot;project&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 사진은 저희 회사의 연말 맞이 이벤트에서 제가 받은 선물에 쓰인 편지인데요.
저에게는 큰 위로와 감동을 주었습니다. 이것 말고도 요즘 더 성장한 것 같다는 말을 종종 들을 때 정말 큰 힘을 받았던 것 같아요.&lt;/p&gt;

&lt;p&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;/p&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;먼저, 혼자 고민한 것보다 질문을 했을 때 결론에 대한 퀄리티가 더 좋습니다. 시니어 개발자의 여부를 떠나서 혼자 고민하게 되면 한 가지 생각과 방안에 얽매이게 되고 결국엔 퀄리티의 한계가 있을 수 있습니다.
하지만 함께 고민하게 되면 더 넓은 시야로 해결 방안에 대해서 생각해볼 수 있었습니다.
또한 고민하던 것 외의 것들을 더 많이 배울 수 있습니다. 시니어 개발자의 문제해결 방법을 보며 해결 방법에 대해서 배우게 되고 문제를 해결하는 과정에서 새로운 개념에 대해서 알아야 할 때 그것들을 학습할 기회가 생기는 것도 큰 장점입니다.&lt;/p&gt;

&lt;p&gt;그리고 고민하는 시간을 줄이면 한 작업 당 효율이 높아지고, 질문을 함으로써 더욱 작업에 대한 적극성을 표현할 수 있습니다.
즉, 질문하면 오히려 작업을 더 잘하고 있다는 인식을 심어줄 수 있는 것 같습니다.&lt;/p&gt;

&lt;p&gt;그렇다면 질문을 할 때는 어떻게 해야 할까요?
먼저 문제 상황을 정확히 인지하는 과정이 필요합니다. 시니어 개발자도 현재 문제 상황을 정확히 인지하는 것이 아닐 것이기 때문에 문제 상황을 충분히 설명할 수 있도록 문제 상황에 대한 이해가 필요합니다.
두 번째로 질문을 정리하는 것입니다. 질문을 하기 전 머릿속에 있는 질문을 리스트업해서 효율적으로 질문하는 것이 필요합니다.
세 번째로 여쭤보기 전 질문에 대해서 먼저 생각해보는 것입니다. 그렇다면 질문의 질은 더 올라가고 고민한 티가 나는 질문을 하게 됩니다.&lt;/p&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;먼저, 결과물에 대한 기대를 어느정도 맞출 수 있습니다.
중간에 공유 없이 결과물만 바로 공유한다면 공유받는 이의 결과물에 대한 기대의 차이 때문에 더 큰 실망을 줄 수 있습니다.
그러므로 중간에 진행 사항을 공유해서 결과물이 어느 정도로 나오겠구나하고 예상이 되게끔 하여 결과물에 대한 기대감을 실제 결과물과 맞추는 것도 중요합니다.
또한 중간에 공유해서 피드백까지 받는다면 결과물의 퀄리티는 더 높아지겠죠.&lt;/p&gt;

&lt;p&gt;그리고 진행 상황을 공유하며 문제도 함께 공유한다면 더 빠르게 문제가 해결될 수 있고 작업 진행도에 대한 이유를 제시할 수 있습니다.
예를 들어 어떤 문제를 제시하며 이 문제 때문에 작업 진행도가 이 정도 되었음을 공유하면 함께 해결책을 찾아 더 빠르게 문제를 해결하거나 작업 진행도에 대해 납득을 시키는 데 도움이 될 것입니다.&lt;/p&gt;

&lt;h3 id=&quot;코드를-짜는-것에-대한-이유를-분명히-하라&quot;&gt;코드를 짜는 것에 대한 이유를 분명히 하라&lt;/h3&gt;
&lt;p&gt;신입 개발자는 왜 이런 코드를 짜는지, 왜 이런 설계가 되어야 하는지, 테스트코드가 왜 필요한지, 코드에서 사용하고 있는 패턴이 어떤 역할로 존재하는지 모르고 코드를 짜는 경우가 있을 수 있습니다.
기존의 코드를 보고 똑같이 따라 하려고 할 때 이런 경우가 많이 생기는데요.
시니어 개발자는 왜 이런 코드를 짜는지 이해하고 짜기를 바라고 있을 것입니다. 코드 한 줄에도 이유가 있어야 하고, 설계가 있다면 효율을 따져보아야 합니다.&lt;/p&gt;

&lt;p&gt;코드를 짤 때 한줄 한줄에 개선점을 찾고 이유를 찾다 보면 더 효율적인 코드가 보일 것입니다.
시니어 개발자는 코드 한 줄도 정성들여 짜는 것을 원합니다.&lt;/p&gt;

&lt;h2 id=&quot;마치며&quot;&gt;마치며&lt;/h2&gt;
&lt;p&gt;스포카에서 1년이라는 시간은 개발자로서 첫 단추를 끼우는 저에게 무척 소중한 시간이었습니다. 제가 잘 적응할 수 있도록 도와주신 저희 팀 여러분들, 특히 백엔드 챕터 여러분들께 감사드립니다.&lt;/p&gt;

&lt;p&gt;저는 팀에서 더 중요한 역할이 되기 위해서 더 많이 배우고 성장하며, 앞으로 스포카가 식자재를 주문하시는 점주분들께 더 좋은 서비스를 제공하기 위해서 노력하겠습니다.&lt;/p&gt;

&lt;p&gt;이 글이 어려움을 겪고 있는 신입 개발자에게는 큰 위로가, 신입 개발자를 잘 대응하고 싶은 시니어 개발자에게는 작은 가이드가 되었기를 바라는 마음으로 이만 글을 줄이겠습니다.&lt;/p&gt;

&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>스포카에서 Kotlin으로 JPA Entity를 정의하는 방법</title>
      <link>https://spoqa.github.io/2022/08/16/kotlin-jpa-entity.html</link>
      <pubDate>Tue, 16 Aug 2022 00:00:00 +0000</pubDate>
      <author>남경호</author>
      <guid>/2022/08/16/kotlin-jpa-entity</guid>
      <description>&lt;p&gt;안녕하세요. 키친보드 팀의 백엔드 프로그래머 남경호입니다.&lt;/p&gt;

&lt;p&gt;최근 Kotlin이 서버 언어로 주목받기 시작하면서 Kotlin + Spring으로 서버를 개발하는 케이스가 많아졌습니다. 그러면서 자연스레 Kotlin으로 JPA를 사용하는 사례 또한 많아졌는데요. 다만 Kotlin으로 JPA를 사용하다 보면, 정확하게는 Entity를 정의하다 보면 Kotlin의 언어적 특성과 잘 맞지 않는 부분을 많이 발견하게 됩니다. &lt;a href=&quot;https://en.wikipedia.org/wiki/Jakarta_Persistence&quot; target=&quot;\_blank&quot;&gt;JPA&lt;/a&gt; 는 Java Persistence API의 약자로 Java 진영의 ORM 표준을 말합니다. 그래서 데이터베이스를 매핑해주는 Entity를 정의할 때 Java를 기준으로 Entity를 정의하기 쉽게 만들어져 있기 때문입니다.&lt;/p&gt;

&lt;p&gt;이 글은 스포카에서 Kotlin으로 JPA Entity를 보다 Entity 답게 사용하기 위해 고민한 내용을 담은 것입니다. 그동안 Kotlin을 사용하면서 Java로 Entity를 정의했을 때의 차이와 최대한 Entity의 컨셉을 해치지 않으면서 어떻게 Kotlin답게 작성하면 좋을지 고민한 것들을 적어보려고 합니다.&lt;/p&gt;

&lt;h2 id=&quot;문제상황&quot;&gt;문제상황&lt;/h2&gt;

&lt;p&gt;Kotlin + JPA를 소개하는 블로그나 영상들을 보면 Entity를 정의한 부분에서 아쉬운 코드들을 많이 볼 수 있습니다. 물론 주제가 Entity를 좀 더 객체 지향적으로 정의하는 것이 아니라 Java보다 Kotlin으로 JPA를 사용했을 때 장점을 소개하는 것에 초점이 맞춰져 있다 보니 그러리라 생각이 듭니다. 하지만 저는 ORM에서 가장 중요한 것은 도메인 모델인 Entity라 생각합니다. 비즈니스 로직이 가장 풍부해야 하며 도메인이 가진 특징을 가장 잘 표현해줄 수 있어야 합니다. 그래서 Entity를 정의할 때 JPA의 규격을 위반하지 않으면서 Kotlin이 추구하는 개발 철학에 맞게 작성하면 좋다고 생각합니다.&lt;/p&gt;

&lt;p&gt;그럼 제가 생각하는 안티 패턴 사례를 좀 더 자세히 살펴보겠습니다.&lt;/p&gt;

&lt;h3 id=&quot;무분별한-mutable-property-사용&quot;&gt;무분별한 mutable property 사용&lt;/h3&gt;

&lt;p&gt;Kotlin은 불변(immutable)을 지향합니다. 변수(mutable)는 부작용이 많기 때문이지요. 앞서 말했다 시피 JPA는 java에 맞게 ORM을 사용하기 위한 API를 제공해줍니다. 그래서 기본적으로 mutable property에 초점이 맞춰져 있습니다. 사실 Entity가 가진 특성을 살펴보면 JPA가 java의 특성에 맞게 mutable property를 사용하는 것이 아니라 Entity의 특성에 따라 mutable property를 사용한 것이 맞는다는 생각이 듭니다. (&lt;a href=&quot;https://veluxer62.github.io/explanation/about-entity-and-value-object/#entity&quot; target=&quot;\_blank&quot;&gt;Entity에 대한 글&lt;/a&gt; 을 참고해 주세요)&lt;/p&gt;

&lt;p&gt;아무튼 그래서 Kotlin으로 Entity를 정의했음에도 불구하고 아래와 같은 코드를 쉽게 접할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@ID&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 코드의 문제점은 무엇일까요? 바로 캡슐화가 되어있지 않다는 것입니다. 그래서 Entity가 가진 상태를 그대로 노출하는 것은 차치하고 그 상태를 외부에서 아무런 제약 없이 변경할 수 있습니다. 심지어 Entity에서 바꾸지 말아야 할 식별자까지 바꿀 수 있도록 정의한 것은 정말 치명적입니다.&lt;/p&gt;

&lt;p&gt;다만 Kotlin에서 클래스가 가진 상태는 field가 아니라 property입니다. (&lt;a href=&quot;https://kotlinlang.org/docs/properties.html#declaring-properties&quot; target=&quot;\_blank&quot;&gt;Kotlin 공식 문서&lt;/a&gt; 를 참고해 주세요) 즉, 아래 Java 코드와 같이 field를 그대로 노출한 코드와 위 Kotlin 코드와는 차이가 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@ID&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Foo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;age&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;왜냐하면 Property는 Field를 외부에 직접 노출하지 않고 Setter와 Getter를 통해 노출하는 것이기 때문입니다. 만약 위 Java 코드에 Field를 그대로 노출하지 않고 Property로 노출하도록 하면 아래와 같이 적어볼 수 있겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Foo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@ID&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Foo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;age&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getNmae&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setAge&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;age&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getAge&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다시 주제로 돌아와서 저는 Field로 노출하든 Property로 노출하든 Entity가 가진 내부 상태의 변경을 직접 노출하도록 정의하는 것은 좋지 못하다고 생각합니다.&lt;/p&gt;

&lt;p&gt;내부 상태를 변경하는 것은 특정 행위를 통해 이루어집니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;foo.age = 3&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;foo.setAge(3)&lt;/code&gt;와 같은 코드는 Entity의 상태를 바꾸는 목적을 표현해주지 못합니다. 차라리 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;foo.age&lt;/code&gt;의 Setter는 외부에 노출하지 않고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;foo.getOld()&lt;/code&gt;와 같이 표현하는 게 더 나을 것입니다.&lt;/p&gt;

&lt;h3 id=&quot;data-class-활용&quot;&gt;Data class 활용&lt;/h3&gt;

&lt;p&gt;다음으로 가장 많이 발견되는 사례는 바로 Data class를 이용한 Entity를 정의하는 사례입니다. &lt;a href=&quot;https://kotlinlang.org/docs/data-classes.html&quot; target=&quot;\_blank&quot;&gt;Kotlin의 Data class&lt;/a&gt; 는 데이터를 전달하기 위한 용도로 사용하는 것을 목적으로 만들어진 클래스입니다. 데이터를 전달하는 구조체는 사용자가 전달한 데이터의 원본을 유지하는 것이 중요합니다. 그래서 저는 Data class를 사용할 때 꼭 필요한 경우가 아니라면 불변변수(immutable)를 사용합니다. 앞서 &lt;a href=&quot;https://veluxer62.github.io/explanation/about-entity-and-value-object/#entity&quot; target=&quot;\_blank&quot;&gt;Entity에 대한 글&lt;/a&gt; 을 보았다면 알 수 있겠지만 Entity는 식별자 외에는 생명주기 동안 상태가 변경될 수 있습니다. 그리고 Entity는 특정 생명주기를 가지고 비즈니스 요구사항을 수행하는 객체이므로 단순히 데이터를 전달하기 위한 용도로 사용하는 Data class와는 성격이 다르다고 볼 수 있습니다. 오히려 Value Object와 유사한 성격을 가진다고 볼 수 있습니다. (&lt;a href=&quot;https://veluxer62.github.io/explanation/about-entity-and-value-object/#value-object&quot; target=&quot;\_blank&quot;&gt;Value Object에 대한 글&lt;/a&gt;을 참고해 주세요)&lt;/p&gt;

&lt;p&gt;그러면 왜 사람들은 Data class를 사용할까요? 매개변수가 없는 생성자를 사용하지 못한다는 제약조건이 있음에도 말입니다. 아마 Data class가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copy&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equals&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hashCode&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toString&lt;/code&gt;을 기본으로 제공해주기 때문이지 않겠느냐고 조심스럽게 예상해봅니다. 아니라면 Entity를 단순히 DB 테이블의 상태를 전달해주는 객체로 본다는 것인데, 요즘 많은 블로그나 영상을 통해 기존에 사용하던 Entity가 도메인 모델이 아닌 Object Mapper로써의 역할만 하도록 정의하는 방식이 좋지 않다는 것을 배워가고 있기 때문에 Kotlin으로 JPA를 사용하고 있는 개발자라면 전자의 이유가 클 것이라는 게 제 생각입니다. 하지만 Kotlin Data class를 소개하는 문서에서 보면 아래와 같은 문구를 볼 수 있습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The compiler automatically derives the following members from all properties declared in the primary constructor&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;즉, 기본 생성자에 정의한 Property 들만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copy&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equals&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hashCode&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toString&lt;/code&gt;함수들에 활용된다는 것입니다. 그래서 아래와 같이 Data class를 정의하고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equals&lt;/code&gt;를 호출하면 예상치 못한 결과를 볼 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;person1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;John&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;person2&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;John&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;person1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;age&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;person2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;age&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;person1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;person2&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같은 상황을 피하기 위해서는 기본생성자에 모든 Property를 정의해야 합니다. 하지만 Entity를 생성할 때 모든 상태를 생성자 매개변수로 받는 것이 과연 좋은 디자인일까요? 저는 꼭 필요한 매개변수로 받고 필요한 상태는 Entity 내부에서 기본값으로 정의하도록 하는 게 좋다고 생각합니다.&lt;/p&gt;

&lt;p&gt;아래와 같이 주문 상태를 가진 주문 Entity를 정의한다고 가정해 보겠습니다. Data class로 정의한다면 아래와 같이 정의할 수 있을 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  
  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;만약 주문서가 최초 생성될 때 주문상태가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SUBMITTED&lt;/code&gt; 상태로 해야 한다면 어떻게 할 수 있을까요? 아마 아래와 같이 사용하는 코드에서 매개변수로 넣어주거나 기본값을 주려고 할 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  
  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderState&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SUBMITTED&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nc&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;orderAt&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SUBMITTED&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;하지만 만약에 사용자가 주문을 생성할 때 상태를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SUBMITTED&lt;/code&gt;가 아닌 다른 값으로 넣는다면 어떨까요? 위와 같은 방법으로는 이와 같은 사용을 막을 방법이 없을 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;orderAt&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CANCELED&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그래서 아래와 같이 생성자 매개변수로 두지 않고 Entity가 생성될 때 기본값을 가지도록 할 수 있습니다. 하지만 이렇게 정의한다면 앞서 보여준 사례처럼 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copy&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equals&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hashCode&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toString&lt;/code&gt; 함수들을 원하는 대로 활용할 수 없게 됩니다. 결국 Data class를 활용하는 것보다 일반 클래스를 사용하는 것이 더욱 자유로운 설계를 할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderState&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SUBMITTED&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nc&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;orderAt&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;한편 Entity의 동등성을 Data class의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equqls&lt;/code&gt;와 같이 모든 Property에 대한 비교를 통해 보장해야 할까요? 다시 &lt;a href=&quot;https://veluxer62.github.io/explanation/about-entity-and-value-object/#entity&quot; target=&quot;\_blank&quot;&gt;Entity에 대한 글&lt;/a&gt; 을 보면 Entity에 대한 동일성은 오로지 식별자를 통해서 이루어진다는 것을 알 수 있습니다. 즉 생명주기 동안 Person이라는 A Entity는 언제든지 이름 또는 나이가 바뀔 수 있습니다. 이름이나 나이가 바뀌었다고 해서 A Entity가 다른 Entity가 될 수 없는 것입니다.&lt;/p&gt;

&lt;p&gt;Kotlin에서 Java와 같이 equals를 따로 재정의하지 않으면 참조 비교를 통해 동일성을 확인합니다. 그래서 만약 프로그램에서 동일한 식별자를 가진 A Entity를 다른 코드에서 조회(좀 더 자세히 말하자면 영속화된 데이터를 메모리로 불러온 경우)하여 동일한 Entity인지 비교한다면 동일하다고 판단하지 않을 수 있습니다. 같은 식별자를 가진 Entity는 동일한 객체라고 판단해야 하므로 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equals&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hashCode&lt;/code&gt;를 재정의해주어야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;lateinit-사용&quot;&gt;lateinit 사용&lt;/h3&gt;

&lt;p&gt;다음은 연관관계 정의 시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lateinit&lt;/code&gt;을 사용하는 경우입니다. Kotlin에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lateinit&lt;/code&gt;을 통해 초기화를 뒤로 미룰 수 있는 코드를 작성할 수 있습니다. 주로 초기화에 비용이 많이 발생하는 경우 코드를 사용하는 시점까지 초기화를 미루어 꼭 필요한 경우에 초기화를 해 성능향상 및 자원 효율성에 도움을 주려는 용도로 사용합니다.&lt;/p&gt;

&lt;p&gt;Kotlin에서는 Java와 달리 초기화를 하지 않고 Property를 정의할 수 없습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;     &lt;span class=&quot;c1&quot;&gt;// compile error: Property must be initialized or be abstract&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// compile error: Property must be initialized or be abstract&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그래서 아래와 같이 생성자 매개변수로 전달받거나 기본값을 넣어줘야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;일반적으로 Kotlin으로 JPA Entity를 정의할 때 Column만 존재하는 경우 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lateinit&lt;/code&gt;을 사용하는 경우는 거의 없습니다. 위와 같이 생성자 매개변수를 활용하거나 기본값을 넣어주기 때문이죠. 문제는 연관관계를 정의할 때입니다. 이는 Java에서 JPA를 사용할 때 사용하던 패턴을 그대로 가져오다 보니 생긴 안 좋은 패턴이지 않나 생각됩니다. 사용사례를 한번 살펴보겠습니다.&lt;/p&gt;

&lt;p&gt;사용자가 게시판을 작성하는 기능을 만든다고 가정해보겠습니다. 하나의 사용자는 여러 게시판을 작성할 수 있고 게시판은 작성자 정보를 저장합니다. 이 기능을 구현하기 위해 Java로 Entity를 아래와 같이 정의해보겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@GeneratedValue&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writerId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;writerId&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writerId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;title&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;writerId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writerId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;생성자를 보면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;writerId&lt;/code&gt;는 초기화해주지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;writer&lt;/code&gt;는 초기화해 주지 않는 것을 볼 수 있습니다. 하지만 Java는 이렇게 사용해도 컴파일 시 오류가 발생하지 않습니다. 기본적으로 초기화해주지 않으면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt; 값을 가지기 때문이죠. Kotlin으로 위 Entity를 다시 정의해 보겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;writerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;writerId&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇듯 연관관계를 정의할 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lateinit&lt;/code&gt;을 활용하는 경우를 많이 볼 수 있습니다. 컴파일도 잘되기 때문에 아무 문제가 없어 보입니다. 심지어 아래와 같이 조회기능을 만들어 사용해보면 정상적으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt;가 조회됩니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@DataJpaTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;showSql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardRepositoryTest&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Autowired&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestEntityManager&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Autowired&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardRepository&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_get_writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;홍길동&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persistAndFlush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persistAndFlush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;detach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;detach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;board2Repository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getReferenceById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Assertions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;홍길동&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;SQL 로그를 봐도 우리가 원했던 대로 잘 조회하는 것을 볼 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Hibernate: 
    insert 
    into
        &quot;user&quot;
        (name, id) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        board
        (title, writer_id, id) 
    values
        (?, ?, ?)
Hibernate: 
    select
        board0_.id as id1_1_0_,
        board0_.title as title2_1_0_,
        board0_.writer_id as writer_i3_1_0_ 
    from
        board board0_
    where
        board0_.id=?
Hibernate: 
    select
        user0_.id as id1_5_0_,
        user0_.name as name2_5_0_ 
    from
        &quot;user&quot; user0_ 
    where
        user0_.id=?
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그렇다면 잘 동작하는데 무엇이 문제일까요? 문제는 영속화한 데이터를 조회할 때가 아니라 Entity를 생성한 직후 해당 Entity를 다룰 때 발생합니다. 예를 들어 저장을 위해 Entity를 생성하고 난 후 writer를 이용하여 특정한 기능을 수행하기 위해 아래와 같이 writer를 조회해 보겠습니다. 그럼 런타임 오류가 발생합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;홍길동&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// error: lateinit property writer has not been initialized&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;왜냐하면 영속화된 Board를 조회할 때는 JPA가 writer를 초기화해주었지만 이제 막 생성한 Entity는 JPA가 writer를 초기화해주지 않았기 때문에 위와 같은 오류가 발생하는 것입니다. 그래서 이처럼 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lateinit&lt;/code&gt;을 활용하는 것은 시스템이 단순할 땐 어찌어찌 사용할 순 있겠지만 어떻게 해서든 위와 같은 오류를 마주치게 되고 이 오류를 우회하기 위해 억지스러운 코드 구현들이 생겨나는 것입니다.&lt;/p&gt;

&lt;p&gt;그렇다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lateinit&lt;/code&gt;을 사용하지 않고 java와 같이 연관관계를 정의하는 다른 방법은 어떤 것이 있을까요? 간단합니다. nullable 타입으로 정의하는 것이죠. 조금만 생각해보면 이게 맞는 게 java의 변수는 초기화하지 않으면 기본값이 null입니다. 그러므로 초기화하지 않은 Property는 null로 보는 것이 맞는다고 봅니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;writerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;writerId&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같이 코드를 변경해도 잘 동작합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@DataJpaTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;showSql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardRepositoryTest&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Autowired&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestEntityManager&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Autowired&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardRepository&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_get_writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Given&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;홍길동&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persistAndFlush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persistAndFlush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;detach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;detach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// When&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;board2Repository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getReferenceById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// Then&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Assertions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;홍길동&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// writer는 nullable이기 때문에 `?`를 써줘야한다.&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;nullable하게 타입을 정의하면 사용하는 코드에서는 null이 있음을 가정하고 코드를 작성하기 때문에 적어도 예상치 못하게 초기화 오류를 만나지 않을 수는 있습니다.&lt;/p&gt;

&lt;p&gt;하지만 이 방법 또한 근본적인 해결책이라 보기 어렵습니다. 비즈니스 요구사항에 따라 다르겠지만 적어도 이 사례에서는 게시판이 생성되었는데 작성자가 없는 것이 말이 안 되기 때문이죠. ORM은 Entity 객체를 데이터베이스에 영속화를 손쉽게 해주기 위한 도구입니다. 그런데 이 ORM의 특성으로 인해 도메인의 제약조건을 위반하는 것이 과면 맞는 것일까요? 저는 아니라고 생각합니다. 그래서 가장 좋은 방법은 게시판을 생성할 때 사용자의 식별자를 넣어주는 것이 아니라 사용자 자체를 넣어주는 것이 맞는다고 생각됩니다. 그렇게 되면 아래와 같이 불필요한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;writerId&lt;/code&gt;와 같은 Property로 사용하지 않아도 됩니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;영속화 시 SQL 쿼리를 보면 이전과 동일하게 동작함을 볼 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;Hibernate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; 
    &lt;span class=&quot;n&quot;&gt;insert&lt;/span&gt; 
    &lt;span class=&quot;n&quot;&gt;into&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;user&quot;&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; 
    &lt;span class=&quot;nf&quot;&gt;values&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;(?,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt;
&lt;span class=&quot;nc&quot;&gt;Hibernate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; 
    &lt;span class=&quot;n&quot;&gt;insert&lt;/span&gt; 
    &lt;span class=&quot;n&quot;&gt;into&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;board&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; 
    &lt;span class=&quot;nf&quot;&gt;values&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;(?,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;여러 안티 패턴들이 있지만 제가 생각하기에 가장 많이 발견되는 대표적인 것들만 소개해 보았습니다. 그럼 이제 본론으로 돌아가서 스포카에서 Kotlin으로 JPA Entity를 정의할 때 사용하고 있는 여러 가지 팁을 소개해 보겠습니다. 팁을 소개하기 전에 우리가 개발할 때 흔하게 접할 수 있는 게시판 도메인을 예제로 두고 이야기를 풀어가면 좋을 것 같아서 먼저 도메인을 소개하고 팁을 소개하는 순서로 진행해 보겠습니다.&lt;/p&gt;

&lt;h2 id=&quot;도메인-정의&quot;&gt;도메인 정의&lt;/h2&gt;

&lt;p&gt;앞으로 설명하게 될 Entity를 보다 이해하기 쉽게 설명하기 위해서 우리가 개발을 시작할 때 흔하게 접하는 게시판 도메인을 가상으로 만들어 본다고 가정해 보겠습니다. 사용자는 API를 이용하여 게시판을 생성, 조회, 수정, 삭제할 수 있고 각 게시판에 댓글과 태그 정보를 추가할 수 있습니다. 자세한 유저 스토리는 아래와 같습니다.&lt;/p&gt;

&lt;h3 id=&quot;유저-스토리&quot;&gt;유저 스토리&lt;/h3&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;/li&gt;
  &lt;li&gt;사용자가 삭제되면 사용자가 작성한 게시글이 모두 삭제된다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;entity&quot;&gt;Entity&lt;/h3&gt;

&lt;p&gt;Entity는 아래와 같습니다. 총 3개의 Entity가 존재하며 사용자와 게시판 그리고 Root Aggregate라고 표현하긴 힘들지만, 단일로 생명주기를 가질 수 있는 Tag가 그 하나입니다. 지금 이 글을 읽고 독자라면 JPA에 대해서 기본적으로 알고 있다는 가정을 하고 있기 때문에 JPA Annotation 관련해서는 별도로 설명하지 않겠습니다. 다소 생략된 부분이 많기 때문에 데이터베이스 스키마가 바로 떠오르지 않는다면 아래 ERD를 참고해주세요.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@MappedSuperclass&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Persistable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;columnDefinition&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;uuid&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UlidCreator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getMonotonicUlid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toUuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Transient&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;_isNew&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;isNew&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_isNew&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HibernateProxy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getIdentifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getIdentifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;obj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Serializable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;obj&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HibernateProxy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;obj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hibernateLazyInitializer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;identifier&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;obj&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@PostPersist&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@PostLoad&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;_isNew&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`user`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToMany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ALL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;writer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;mutableBoards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableListOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutableBoards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;writeBoard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mutableBoards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uniqueConstraints&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UniqueConstraint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;tag_key_value_uk&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;columnNames&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;`key`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`value`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])])&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`key`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`value`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;information&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;createdAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Embedded&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;information&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;information&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@ManyToMany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PERSIST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MERGE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinTable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;board_tag_assoc&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;joinColumns&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;board_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)],&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;inverseJoinColumns&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;tag_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)],&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;mutableTags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toMutableSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutableTags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@ElementCollection&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@CollectionTable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;board_comment&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;mutableComments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableListOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;comments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutableComments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardUpdateData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;information&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;information&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;addTag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mutableTags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;removeTag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tagId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mutableTags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;removeIf&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tagId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;addComment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mutableComments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;writeBoard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Embeddable&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?,&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Embeddable&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;erd&quot;&gt;ERD&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/images/kotlin-jpa-entity/erd.png&quot; alt=&quot;erd&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;kotlin-entity-정의-팁&quot;&gt;Kotlin Entity 정의 팁&lt;/h2&gt;

&lt;p&gt;위 예제코드에 저희가 본격적으로 소개하고자 하는 Kotlin에서 JPA Entity를 정의할 때 사용하면 좋을 만한 팁들이 녹아 있습니다. 하나하나 살펴보겠습니다.&lt;/p&gt;

&lt;h3 id=&quot;allopen&quot;&gt;allopen&lt;/h3&gt;

&lt;p&gt;많은 블로그나 영상을 통해서 Kotlin으로 JPA를 사용할 때 Entity에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;allopen&lt;/code&gt;옵션과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;no-args constructor&lt;/code&gt;옵션을 주어야 한다는 것은 널리 알려져 있습니다. 그래서 간단하게 언급만 하고 넘어가겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://start.spring.io/&quot; target=&quot;\_blank&quot;&gt;Spring Initializr&lt;/a&gt; 를 통해 Kotlin으로 JPA 프로젝트를 설정해보았다면 알겠지만 아래 플러그인들이 추가된 것을 볼 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;kotlin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;plugin.spring&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;1.7.0&quot;&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;kotlin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;plugin.jpa&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;1.7.0&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Kotlin에서는 &lt;a href=&quot;https://kotlinlang.org/docs/all-open-plugin.html#spring-support&quot; target=&quot;\_blank&quot;&gt;Spring plugin&lt;/a&gt; 을 통해 스프링의 컴포넌트들을 사용하는 데 문제가 없도록 모든 클래스에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;open&lt;/code&gt; 키워드를 선언하지 않아도 되도록 &lt;a href=&quot;https://kotlinlang.org/docs/all-open-plugin.html&quot;&gt;All-open plugin&lt;/a&gt;을 포함하고 있습니다.&lt;/p&gt;

&lt;p&gt;그리고 &lt;a href=&quot;https://kotlinlang.org/docs/no-arg-plugin.html#jpa-support&quot; target=&quot;\_blank&quot;&gt;Jpa plugin&lt;/a&gt; 을 통해 JPA 관련 클래스들을 생성하는 데 문제가 없도록 생성자에 매개변수가 없는 &lt;a href=&quot;https://kotlinlang.org/docs/no-arg-plugin.html&quot; target=&quot;\_blank&quot;&gt;No-arg plugin&lt;/a&gt; 도 포함해줍니다.&lt;/p&gt;

&lt;p&gt;위 설정만으로 JPA를 사용하는 데 문제가 없어 보이지만 막상 Entity를 Decompile 해보면 여전히 final 키워드가 있는 것을 볼 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;com&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;example&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kotlinentitytutorial&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;com&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;example&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kotlinentitytutorial&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;


  &lt;span class=&quot;nd&quot;&gt;@Ljavax&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;persistence&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;()&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Ljavax&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;persistence&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Table&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;`user`&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// access flags 0x2&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Ljava&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@Ljavax&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;persistence&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Column&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@Lorg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jetbrains&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NotNull&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// invisible&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// access flags 0x11&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Ljava&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@Lorg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jetbrains&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NotNull&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// invisible&lt;/span&gt;
   &lt;span class=&quot;no&quot;&gt;L0&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;LINENUMBER&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;L0&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;ALOAD&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;GETFIELD&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;com&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;example&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kotlinentitytutorial&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Ljava&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;ARETURN&lt;/span&gt;
   &lt;span class=&quot;no&quot;&gt;L1&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;LOCALVARIABLE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Lcom&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;example&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kotlinentitytutorial&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;L0&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;L1&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;MAXSTACK&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;MAXLOCALS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#entity&quot; target=&quot;\_blank&quot;&gt;Hibernate의 사용자 가이드 문서&lt;/a&gt; 를 보면 Entity와 Entity가 가진 인스턴스 변수는 final이 아니어야 한다고 적혀있습니다. 그래서 가이드를 잘 지키는 착한 개발자(?)가 되기 위해서 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;allOpen&lt;/code&gt; 설정을 추가해주도록 합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;allOpen&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;annotation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;javax.persistence.Entity&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;annotation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;javax.persistence.MappedSuperclass&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;annotation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;javax.persistence.Embeddable&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같이 설정해주면 더 이상 final 키워드가 들어가 있지 않은 것을 볼 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;com&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;example&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kotlinentitytutorial&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;com&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;example&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kotlinentitytutorial&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;


  &lt;span class=&quot;nd&quot;&gt;@Ljavax&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;persistence&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;()&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Ljavax&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;persistence&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Table&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;`user`&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// access flags 0x2&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Ljava&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@Ljavax&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;persistence&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Column&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@Lorg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jetbrains&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NotNull&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// invisible&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// access flags 0x1&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Ljava&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nd&quot;&gt;@Lorg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jetbrains&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NotNull&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// invisible&lt;/span&gt;
   &lt;span class=&quot;no&quot;&gt;L0&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;LINENUMBER&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;L0&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;ALOAD&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;GETFIELD&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;com&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;example&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kotlinentitytutorial&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Ljava&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;ARETURN&lt;/span&gt;
   &lt;span class=&quot;no&quot;&gt;L1&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;LOCALVARIABLE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Lcom&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;example&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kotlinentitytutorial&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;L0&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;L1&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;MAXSTACK&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;MAXLOCALS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;primarykeyentity&quot;&gt;PrimaryKeyEntity&lt;/h3&gt;

&lt;p&gt;예제 Entity 코드를 보면 모든 Entity 들이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PrimaryKeyEntity&lt;/code&gt;를 상속받은 것을 볼 수 있습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PrimaryKeyEntity&lt;/code&gt;는 여러 가지 이유로 인해 추가되었는데, 그 이유는 아래와 같습니다.&lt;/p&gt;

&lt;h4 id=&quot;공통-primarykey-타입-정의&quot;&gt;공통 PrimaryKey 타입 정의&lt;/h4&gt;

&lt;p&gt;모든 Entity에는 Primary Key가 필요합니다. JPA에서 Primary Key는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Id&lt;/code&gt;로 정의되는데 보통 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Long&lt;/code&gt; 타입 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UUID&lt;/code&gt;타입으로 정의됩니다. 저는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Long&lt;/code&gt; 타입보다 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UUID&lt;/code&gt;타입으로 Primary Key를 정의하는 것을 선호하는 편입니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Long&lt;/code&gt; 타입의 Primary Key는 유일성을 보장하는 데 한계가 있기 때문입니다. Entity끼리도 키값이 중복될 수도 있고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UUID&lt;/code&gt;의 개수보다 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Long&lt;/code&gt; 타입의 개수가 현저하게 적기 때문에 최대 개수에 대해 걱정을 하지 않고 싶다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UUID&lt;/code&gt;는 좋은 선택이 될 것입니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UUID&lt;/code&gt;타입도 단점이 있습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Long&lt;/code&gt; 타입의 경우 순서를 가질 수 있기 때문에 정렬 시 성능적인 이점을 가져갈 수 있습니다. 그리고 데이터 크기도 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UUID&lt;/code&gt;에 비해 작기 때문에 저장용량 및 생성 비용을 적게 가져갈 수 있다는 장점도 있습니다. 하지만 일반적인 웹 애플리케이션을 개발하면서 그리고 클라우드 서비스를 사용하면서 저장용량 및 생성 비용을 최적화하는 것은 크게 중요하지 않을 수 있습니다. (이러한 성능이 아주아주 중요한 시스템이라면 ORM 사용 자체를 다시 고려하는 것이 어떨까 합니다) 그렇다면 나머지 단점이 정렬인데, 이 부분도 &lt;a href=&quot;https://github.com/ulid/spec&quot; target=&quot;\_blank&quot;&gt;ULID&lt;/a&gt; 를 사용한다면 해결할 수 있습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ULID&lt;/code&gt;는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UUID&lt;/code&gt;와 호환성을 가지면서 시간순으로 정렬할 수 있는 특징을 가지고 있습니다. 그래서 목록 조회 시 Primary Key를 기준으로 정렬을 수행할 수 있습니다.&lt;/p&gt;

&lt;p&gt;Java 또는 Kotlin으로 구현한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ULID&lt;/code&gt;라이브러리는 많이 있습니다. 그중에서 저는 &lt;a href=&quot;https://github.com/f4b6a3/ulid-creator&quot; target=&quot;\_blank&quot;&gt;ULID Creator&lt;/a&gt; 를 선호합니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UUID&lt;/code&gt;로 변환하는 함수를 제공하고 무엇보다 &lt;a href=&quot;https://github.com/f4b6a3/ulid-creator#monotonic-ulid&quot; target=&quot;\_blank&quot;&gt;Monotonic 함수&lt;/a&gt; 를 제공해주기 때문입니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ULID&lt;/code&gt;는 스펙상 밀리초까지만 시간순을 보장해줍니다. 하지만 코드를 작성하다 보면 반복문을 실행하면서 다수의 Entity를 생성하는 경우가 있습니다. 이런 경우 생성된 일반적인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ULID&lt;/code&gt;생성함수로는 나노초가 아닌 밀리초 단위까지만 사용하는 ULID 특성상 생성 순서를 보장해주지 않기 때문에 곤란한 상황이 생기곤 합니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Monotonic ULID&lt;/code&gt;는 이를 보완하기 위해 만약 동일한 밀리초가 있다면 다음에 생성되는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ULID&lt;/code&gt;의 밀리초를 1 증가시켜서 생성하도록 해줍니다.&lt;/p&gt;

&lt;p&gt;이렇듯 모든 Entity 들이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PrimaryKeyEntity&lt;/code&gt;를 상속받아 사용하도록 하면 공통된 Primary Key를 사용하도록 할 수 있습니다.&lt;/p&gt;

&lt;h4 id=&quot;공통-primary-key-생성-방식-결정&quot;&gt;공통 Primary Key 생성 방식 결정&lt;/h4&gt;

&lt;p&gt;JPA는 Primary Key를 자동으로 생성해주는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@GeneratedValue&lt;/code&gt;를 제공합니다. Entity를 생성하고 영속화 시 자동으로 Primary Key를 생성하도록 도와주는 것입니다. 편리한 기능이지만 저는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@GeneratedValue&lt;/code&gt; 사용을 좋아하지 않습니다. 그 이유는 다음과 같습니다.&lt;/p&gt;

&lt;h5 id=&quot;nullable-타입을-유도한다&quot;&gt;Nullable 타입을 유도한다&lt;/h5&gt;

&lt;p&gt;&lt;a href=&quot;https://spring.io/projects/spring-data-jpa&quot; target=&quot;\_blank&quot;&gt;Spring Data JPA&lt;/a&gt; 의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;save&lt;/code&gt;함수를 보면 아래와 같은 코드가 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;S&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;S&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;S&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

  &lt;span class=&quot;nc&quot;&gt;Assert&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;notNull&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Entity must not be null.&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entityInformation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isNew&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;em&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;em&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;merge&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entityInformation.isNew&lt;/code&gt;가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt;이면 즉, Entity가 새롭게 생성되었다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EntityManager.persist&lt;/code&gt; 함수를, 이미 존재하는 Entity라면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EntityManager.merge&lt;/code&gt;함수를 실행합니다. 그럼 새롭게 생성되었다는것을 어떻게 판단할까요? Entity의 신규 생성 여부는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JpaMetamodelEntityInformation&lt;/code&gt;를 거쳐 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AbstractEntityInformation.isNew&lt;/code&gt;를 통해 판단됩니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;isNew&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

  &lt;span class=&quot;no&quot;&gt;ID&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nc&quot;&gt;Class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idType&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getIdType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isPrimitive&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;instanceof&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Number&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Number&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;longValue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0L&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;IllegalArgumentException&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Unsupported primitive id type %s&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 코드를 보면 Entity의 ID가 원시 타입이 아니면 null 여부를 통해 새롭게 생성된 Entity인지 여부를 결정하는 것을 볼 수 있습니다. 즉, Primary Key가 nullable한 타입이라는 것입니다. 조금만 생각해보면 이상합니다. 데이터베이스는 Primary Key를 Not Null 값으로 강제합니다. 그리고 Entity 입장에서도 식별자가 Nullable하다는 것은 지극히 어색합니다.&lt;/p&gt;

&lt;h5 id=&quot;생성한-값과-영속화된-값이-다르다&quot;&gt;생성한 값과 영속화된 값이 다르다&lt;/h5&gt;

&lt;p&gt;그렇다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Long&lt;/code&gt; 타입은 어떨까요? 다시 위 코드를 보면 숫자 형인 경우 0인 값이면 새롭게 생성된 Entity인지 판단합니다. 그래서 참조 타입이 아닌 원시 타입인 경우 그나마 나은 게 적어도 Nullable하지는 않기 때문입니다. 하지만 이 또한 상당히 어색합니다. 왜냐하면 애플리케이션 내에서 생성된 Entity는 영속화하기 전까지는 모두 0인 ID를 가지기 때문입니다. 그렇다면 이 Entity 들은 모두 같은 Entity일까요? 아닙니다. 다르다고 봐야 합니다. 그럼 동일성을 ID가 0인 경우는 제외해야 할까요? 그것은 참으로 근시안적인 해결책이라 생각됩니다.&lt;/p&gt;

&lt;h5 id=&quot;채번을-유발한다&quot;&gt;채번을 유발한다&lt;/h5&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@GeneratedValue&lt;/code&gt;를 사용하는 방법은 다양합니다. (자세한 생성 방법은 &lt;a href=&quot;https://www.baeldung.com/hibernate-identifiers&quot; target=&quot;\_blank&quot;&gt;여기&lt;/a&gt; 를 참고해 주세요) &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;auto_increment&lt;/code&gt;나 시퀀스, 시퀀스 테이블을 활용하는 방법들은 모두 데이터베이스에 그 책임을 전가하고 부하를 유발합니다. 작은 규모의 애플리케이션에서는 괜찮을진 몰라도 대량의 트래픽을 다루기 위해 많은 서버를 두고 있는 애플리케이션에서는 이러한 채번 활동이 데이터베이스에 상당한 부담을 주게 됩니다.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;그렇다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@GeneratedValue&lt;/code&gt;를 사용하지 않고 Entity의 Primary Key를 다루는 방법은 무엇일까요? 간단합니다. Entity 생성 시 Primary Key도 함께 생성해주면 됩니다. 앞서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Long&lt;/code&gt; 타입보다 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ULID&lt;/code&gt;타입으로 Primary Key 타입을 두는 것을 선호한다고 말했었습니다. 그 이유가 여기에도 있는데요. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Long&lt;/code&gt; 타입은 랜덤한 값을 주기가 조금 모호합니다. 날짜 객체를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Long&lt;/code&gt; 타입으로 바꿔서 사용할 수는 있을 것 같기는 합니다. 하지만 1부터 시작하고 싶다면 이 방법도 좋은 대안이라 하긴 힘듭니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ULID&lt;/code&gt;는 어디서 생성하든 중복된 값이 생성될 확률이 거의 없다고 봐도 무방합니다. 그래서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PrimaryKeyEntity&lt;/code&gt;를 통해 Entity를 생성할 때 무조건 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ULID&lt;/code&gt;가 자동으로 생성하도록 강제하여 위에서 말하는 단점들을 회피할 수 있습니다.&lt;/p&gt;

&lt;h4 id=&quot;persistable-구현&quot;&gt;Persistable 구현&lt;/h4&gt;

&lt;p&gt;Primary Key를 영속화 전에 미리 생성해주는 방식으로 하면 주의할 점이 있습니다. 앞서 Primary Key가 참조변수 타입이면 null인 경우에만 신규 생성 Entity로 간주한다고 했었습니다. 그래서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ULID&lt;/code&gt;를 Entity를 생성할 때 함께 생성해서 영속화를 하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EntityManager.persist&lt;/code&gt; 함수가 아닌 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EntityManager.merge&lt;/code&gt; 함수가 호출됩니다. 그래서 아래와 같이 저장하기 전에 Primary Key를 가진 데이터가 존재하는지 여부를 확인하는 쿼리가 한번 실행됨을 볼 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;foo&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;fooRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;fooRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Hibernate: 
    select
        foo0_.id as id1_3_0_,
        foo0_.name as name2_3_0_ 
    from
        foo foo0_ 
    where
        foo0_.id=?
Hibernate: 
    insert 
    into
        foo
        (name, id) 
    values
        (?, ?)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;결과적으로는 새롭게 Entity가 생겨서 문제가 없지만 한번 조회한 후 저장하는 방법은 불필요한 쿼리를 실행시키기 때문에 좋아 보이진 않습니다. 다행히 Spring Data에서 이러한 문제를 해결하기 위한 방법으로 &lt;a href=&quot;https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Persistable.html&quot; target=&quot;\_blank&quot;&gt;Persistable&lt;/a&gt; 인터페이스를 제공해줍니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Persistable&lt;/code&gt;인터페이스는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getId&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;isNew&lt;/code&gt; 함수를 제공하는데 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;isNew&lt;/code&gt;함수는 위에서 언급한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entityInformation.isNew(entity)&lt;/code&gt;에서 활용됩니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Persistable&lt;/code&gt;인터페이스를 구현한 Entity를 영속화하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JpaPersistableEntityInformation.isNew&lt;/code&gt;함수가 호출되며 여기서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Persistable.isNew&lt;/code&gt;함수를 호출합니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;isNew&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isNew&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그럼 이제 위에서 예시로 들었던 Entity에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Persistable&lt;/code&gt;을 구현하도록 해보겠습니다. 테스트 코드를 실행하면 insert쿼리만 실행됨을 확인할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Persistable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;isNew&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Hibernate: 
    insert 
    into
        foo
        (name, id) 
    values
        (?, ?)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다만, 이렇게 설정하는 경우 이슈가 있습니다. 바로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete&lt;/code&gt;를 호출할 때 삭제 쿼리가 실행되지 않는 이슈입니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SimpleJpaRepository.delete&lt;/code&gt;함수를 살펴보겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@SuppressWarnings&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;unchecked&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

  &lt;span class=&quot;nc&quot;&gt;Assert&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;notNull&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Entity must not be null!&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entityInformation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isNew&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nc&quot;&gt;Class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;?&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProxyUtils&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getUserClass&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;no&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;em&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entityInformation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// if the entity to be deleted doesn&apos;t exist, delete is a NOOP&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;em&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;em&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;contains&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;em&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;merge&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;아주 익숙한 코드가 눈에 띕니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entityInformation.isNew&lt;/code&gt;를 호출해서 참이면 즉, 새롭게 생성된 Entity이면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EntityManager.remove&lt;/code&gt; 함수를 호출하지 않는 것입니다. 실제로도 그런지 테스트해보겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;foo&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;fooRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;fooRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;fooRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;fooRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Hibernate: 
    insert 
    into
        foo
        (name, id) 
    values
        (?, ?)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;insert 쿼리 외에 아무 쿼리도 보이지 않습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entityInformation.isNew&lt;/code&gt;가 true로 반환되기 때문에 그런 것입니다. 그렇다면 어떻게 해결할 수 있을까요? 곰곰이 생각해보면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;isNew&lt;/code&gt;가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt;인 경우는 이제 막 Entity를 생성해서 영속화를 하기 전까지입니다. 그렇다면 영속화를 한 이후와 Entity를 조회하였을 때는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;isNew&lt;/code&gt;가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;false&lt;/code&gt;가 되어야 합니다.&lt;/p&gt;

&lt;p&gt;이 부분을 해결하기 위해 JPA의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@PostPersist&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@PostLoad&lt;/code&gt;를 활용하기로 했습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@PostPersist&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@PostLoad&lt;/code&gt;는 JPA의 수명주기 이벤트에 대한 콜백 방법을 정의하는 것으로 각각 영속화 이후와 영속화한 데이터를 조회한 이후에 함수가 실행되도록 할 수 있습니다. 위 예시 코드에 추가해보겠습니다. isNew의 상태를 관리해야 하므로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;isNew&lt;/code&gt; Property를 추가하고 영속화는 하지 않도록 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Transient&lt;/code&gt;를 선언해주었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Persistable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Transient&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;_isNew&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;isNew&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_isNew&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@PostPersist&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@PostLoad&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;_isNew&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Hibernate: 
    insert 
    into
        foo
        (name, id) 
    values
        (?, ?)
Hibernate: 
    delete 
    from
        foo 
    where
        id=?
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;로그를 보면 delete 쿼리가 정상적으로 실행됨을 볼 수 있습니다.&lt;/p&gt;

&lt;p&gt;위와 같은 이슈들이 있을 수 있기 때문에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Persistable&lt;/code&gt;을 구현할 때 이 부분들을 꼭 염두에 두고 정의하면 좋겠습니다.&lt;/p&gt;

&lt;h4 id=&quot;공통-동일성-보장&quot;&gt;공통 동일성 보장&lt;/h4&gt;

&lt;p&gt;앞서 Entity는 같은 식별자를 가진다면 동일한 객체라고 판단해야 하므로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equals&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hashCode&lt;/code&gt;를 재정의해주어야 한다고 이야기했었습니다. 모든 Entity마다 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equals&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hashCode&lt;/code&gt;를 재정의해주는 것은 불편한 일입니다. 변경 사항이 발생했을 때 반복적으로 코드를 수정하는 행위도 좋은 코드라 하기 힘들 것입니다. 그래서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PrimaryKeyEntity&lt;/code&gt;에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equals&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hashCode&lt;/code&gt;를 재정의해두고 이를 공통으로 활용할 수 있도록 하면 좋습니다. 물론 이렇게 할 수 있는 이유는 모든 Entity는 식별자를 기준으로 동일함을 판단하기 때문입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@MappedSuperclass&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Persistable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;columnDefinition&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;uuid&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UlidCreator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getMonotonicUlid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toUuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;obj&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PrimaryKeyEntity&lt;/code&gt;를 이용하여 모든 동일성을 보장할 수 있으니 손쉽게 끝!! 이라고 외칠 수 있겠으나 아쉽게도 이번에도 이슈가 있습니다.&lt;/p&gt;

&lt;p&gt;이번에도 역시나 JPA의 메커니즘에 의해 발생하는 이슈인데, 상황을 한번 보겠습니다. 위에서 정의한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; Entity와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt; Entity를 영속화한 후 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt; Entity를 재조회하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; Entity가 기존에 추가한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; Entity와 일치하는지 보겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;홍길동&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boardInformation&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;내용1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;assertTrue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;expected: &amp;lt;true&amp;gt; but was: &amp;lt;false&amp;gt;
Expected :true
Actual   :false
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;성공할 것을 예상했지만, 결과는 실패합니다. 이유가 무엇일까요? 범인은 바로 &lt;a href=&quot;https://docs.jboss.org/hibernate/orm/current/javadocs/org/hibernate/proxy/package-summary.html&quot; target=&quot;\_blank&quot;&gt;Hibernate Proxy&lt;/a&gt; 때문입니다. JPA의 구현체인 Hibernate는 성능 최적화를 위해 연관관계 조회 시 꼭 필요할 때까지 조회 쿼리 호출을 지연시키는 지연 조회(Lazy Loading)를 지원합니다. 그래서 연관관계를 조회하는 쿼리가 실행되어 실제 Entity가 생성되기 전까지 Proxy 객체를 미리 생성해서 넣어두었다가 실제 조회가 되면 Entity를 Proxy와 교체하는 방식으로 동작합니다.&lt;/p&gt;

&lt;p&gt;다시 테스트 코드에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user == actual.writer&lt;/code&gt;가 true가 아니라 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;false&lt;/code&gt;로 반환되는지 equals를 다시 한번 살펴보겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;obj&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;첫 번째 조건문에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;이 아니므로 넘어가고, 두 번째 조건문이 아마 범인이지 않을까 생각됩니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this::class&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;other::class&lt;/code&gt;를 출력해보겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;// class com.example.kotlinentitytutorial.User&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// class com.example.kotlinentitytutorial.User$HibernateProxy$bl4KAli0&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// true&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;obj&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this::class&lt;/code&gt;는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; Entity임이 확인되지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;other::class&lt;/code&gt;는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User$HibernateProxy$bl4KAli0&lt;/code&gt; 인 것을 확인할 수 있습니다. 그래서 서로 다른 객체라 판단하는 것입니다. 즉, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;board.writer&lt;/code&gt;는 지연 로딩으로 인해 아직 쿼리가 실행되지 않았으므로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HibernateProxy&lt;/code&gt;객체로 존재하게 되는 것입니다.&lt;/p&gt;

&lt;p&gt;그럼 이 문제를 어떻게 해결하면 좋을까요? 첫 번째 떠오르는 방법은 즉시 조회(Eager Loading)를 사용하는 방법입니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt;가 조회될 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt;도 바로 조회 하도록 하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HibernateProxy&lt;/code&gt;로 Entity를 대체하지 않을 테니 말입니다. 실제로 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetchType&lt;/code&gt;을 변경하여 위 테스트를 실행하면 성공함을 볼 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;EAGER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;하지만 Entity 조회 시 반드시 연결된 연관관계를 사용해야 하는 상황이라면 이러한 해결책이 맞을 순 있으나 사용하지 않을 때도 쿼리를 무조건 실행하도록 모든 연관관계를 정의하면 성능적으로 아주 좋지 않을 것입니다. 데이터베이스에 부하를 주는 것은 물론이거니와 의도치 않은 N+1 문제를 야기할 수도 있습니다.&lt;/p&gt;

&lt;p&gt;그래서 해결할 수 있는 다른 방법은 바로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equals&lt;/code&gt;를 아래와 같이 다시 정의하는 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HibernateProxy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getIdentifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getIdentifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;obj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Serializable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;obj&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HibernateProxy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;obj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hibernateLazyInitializer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;identifier&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;obj&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;코드를 하나하나 살펴보면 클래스 타입을 체크하는 부분에서 HibernateProxy는 제외하도록 변경하였습니다. 그런 다음 식별자를 가져올 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HibernateProxy&lt;/code&gt; 객체이면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;obj.hibernateLazyInitializer.identifier&lt;/code&gt;를 통해 식별자를 가져오도록 하고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HibernateProxy&lt;/code&gt; 객체가 아니면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PrimaryKeyEntity.id&lt;/code&gt;를 가져오도록 하였습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;equals&lt;/code&gt; 코드를 변경한 후 위 테스트를 다시 실행해보면 지연 조회인 경우에도 정상적으로 동일함을 판단해줌을 확인할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;지금까지 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PrimaryKeyEntity&lt;/code&gt;를 활용하는 방법에 대해 알아보았습니다. 설명이 길어 다소 복잡할 수 있다는 생각이 듭니다. 그래서 간단하게 정리하자면 식별자의 타입과 생성 방법 그리고 동일성을 보장해주는 코드를 Entity 전체에 통일시키기 위한 용도로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PrimaryKeyEntity&lt;/code&gt;를 사용한다고 보면 됩니다. (여기서 네이밍은 크게 중요하지 않으니 원하는 이름으로 사용해주세요!)&lt;/p&gt;

&lt;h3 id=&quot;property-접근제어&quot;&gt;Property 접근제어&lt;/h3&gt;

&lt;p&gt;다음으로 가장 많이 고민한 부분이 바로 Entity의 Property에 대한 접근제어입니다. 앞서 문제상황에서 이야기한 바와 같이 Java 코드에서 사용하던 패턴과 Entity를 단순한 데이터베이스의 테이블 정보를 담은 데이터 구조체로만 생각함으로써 오는 좋지 못한 사용은 우리의 애플리케이션을 더욱 복잡하고 유지보수하기 어렵게 만듭니다. 그래서 Entity의 Property의 변경을 최소화하고 꼭 필요한 경우에 변경할 수 있도록 캡슐화를 해줄 필요가 있습니다. 아무리 Entity의 Property가 생명주기 동안에 변경 가능하다고 해도 말입니다. (나의 이름을 누군가가 마음대로 바꿀 수 있다면 좋을까요?)&lt;/p&gt;

&lt;p&gt;식별자의 생성과 조회는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PrimaryKeyEntity&lt;/code&gt;에게 위임했으니 식별자 관리에 대해서는 생략하겠습니다. 간단하게 짚고 넘어가자면 식별자는 Entity의 생명주기 동안 절대 바뀌지 않아야 하므로 불변변수로 선언합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;columnDefinition&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;uuid&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UlidCreator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getMonotonicUlid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toUuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그렇다면 다른 Property 들은 어떻게 선언하면 좋을까요? Entity의 Property 들은 생명주기 동안 변경 가능하다고 이야기했었습니다. 그래서 불변(immutable) Property가 아닌 변하는(muatable) Property로 선언하는 것이 이상적입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;하지만 저는 저의 이름을 누군가가 마음대로 변경하는 것은 원치 않습니다. 비록 Kotlin은 Property라 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setter&lt;/code&gt;를 재정의해줄 수 있지만 앞서 말했다시피 목적을 나타내지 않는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setter&lt;/code&gt;는 좋아할 수라 보기 어렵습니다. 그래서 외부에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt;을 변경하는 것을 막으면 좋겠습니다. 그러나 위 코드처럼 정의하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setter&lt;/code&gt;를 외부에서 사용하지 못하도록 막을 방법이 없습니다. 그래서 아래와 같은 방법으로 정의할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`user`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;protected set&lt;/code&gt;을 사용하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; Entity 자신 또는 상속받은 Entity에서만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt;을 변경할 권한이 생깁니다. 여기서 왜 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setter&lt;/code&gt;에 대한 접근 권한을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;private&lt;/code&gt;가 아니라 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;protected&lt;/code&gt;로 정의했는지 궁금할 것입니다. 앞서 우리는 Entity 클래스에 대해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;allOpen&lt;/code&gt; 설정을 해두었을 것입니다. open property의 경우 private setter는 허용되지 않기 때문에 아래와 같이 컴파일 에러가 발생합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/kotlin-jpa-entity/private-setter-compile-error.png&quot; alt=&quot;private-setter-compile-error&quot; /&gt;&lt;/p&gt;

&lt;p&gt;한편 Entity의 Property 중에 변경이 전혀 필요하지 않은 Property가 있을 수 있습니다. 가령 생성일시와 같은 Property는 한번 생성되고 나면 변경되지 않는 것이 이상적입니다. 그래서 아래와 같이 선언할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`user`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;createdAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다만 이 Property를 생성자 매개변수를 통해 초기화한다면 조금 불편함이 생깁니다. 생성자 매개변수에서 직접 선언할 수 있으니 생성자 매개변수로 위치를 이동하라고 IDEA에서 warning을 표시하는 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`user`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;createdAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;createdAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;createdAt&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/images/kotlin-jpa-entity/warning-constructor.png&quot; alt=&quot;warning-constructor&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이를 해결할 방법은 여러 가지가 있습니다. 모두 일장일단이 있으니 선호하는 방식을 채택하면 좋겠습니다.&lt;/p&gt;

&lt;h4 id=&quot;생성자-매개변수로-이동&quot;&gt;생성자 매개변수로 이동&lt;/h4&gt;

&lt;p&gt;IDEA가 가이드하는 대로 생성자 매개변수로 위치를 옮기는 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`user`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;createdAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;하지만 이렇게 되면 JPA의 Column이 선언된 Property 들이 생성자에도 존재하게 되고 클래스 내부에도 존재하게 됩니다. 개인적으로 일관성도 떨어져 보이고 가독성이 썩 좋진 않아 보입니다. 하지만 별도의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Suppress&lt;/code&gt;도 달아주지 않고 불변 Property를 사용할 수 있다는 장점이 있습니다.&lt;/p&gt;

&lt;h4 id=&quot;suppress-추가&quot;&gt;Suppress 추가&lt;/h4&gt;

&lt;p&gt;다음은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Suppress&lt;/code&gt;를 추가하는 방법입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`user`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;createdAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Suppress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;CanBePrimaryConstructorProperty&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;createdAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;createdAt&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Suppress&lt;/code&gt;를 추가해주면 더 이상 IDEA에서 warning을 표시하지 않습니다. Property 위에 선언하는 게 불편하다면 class나 file 레벨로 끌어 올릴 수도 있습니다. warning이 크리티컬한 위험성을 포함하는 것이 아니라면 이렇게 IDEA에 표시되지 않도록 처리하는 것도 합리적인 방법이 될 수 있습니다. 하지만 예기치 못한 상황이 생길 수 있으니 되도록 보수적으로 사용하는 것을 권장합니다. 자칫 무분별하게 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Suppress&lt;/code&gt;를 사용하는 버릇을 들이면 중요한 위험 알림도 무시할 수 있으니 말입니다.&lt;/p&gt;

&lt;h4 id=&quot;protected-set-설정&quot;&gt;protected set 설정&lt;/h4&gt;

&lt;p&gt;도매인 적으로는 생성일시는 생명 주기상 변하지 않아야 합니다. 하지만 Entity 입장에서는 모든 Property가 변경 가능하다고 봅니다. 그래서 Entity 내부에서만 변경할 수 있도록 선언해두고 외부에서는 변경이 가능하지 않도록 변경할 수를 제공하지 않는다면 특별히 문제가 되지 않을 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`user`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;createdAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;createdAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocalDateTime&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;createdAt&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Suppress&lt;/code&gt;를 추가하지 않고도 생성자 매개변수로 이용할 수 있습니다. 하지만 Entity 내부에서 변경 가능성을 열어두었기 때문에 위험성을 내포하고 있다는 단점이 있기는 합니다. (사실 불변(immutable) Property로 선언해도 개발자가 변경(mutable) Property로 바꾸는 것을 막지 못한다는 건 똑같다고 생각합니다)&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;지금까지 Property에 대한 접근제어에 대한 팁을 적어보았습니다. Entity의 캡슐화는 애플리케이션 개발할 시 상당히 중요한 포인트입니다. 상태의 변경은 부작용(Side-effect)을 유발합니다. 부작용을 최소화해서 우리가 만드는 제품의 복잡성을 최대한 줄이기 위해 항상 고민하면 좋겠습니다.&lt;/p&gt;

&lt;h3 id=&quot;nullable&quot;&gt;nullable&lt;/h3&gt;

&lt;p&gt;어찌 보면 간단한 팁이지만 놓치기 쉬운 부분입니다. 새롭게 애플리케이션을 만들 때보다 기존에 Java로 작성되어있는 코드를 Kotlin으로 전환할 때 더 높은 확률로 발생하는 이슈입니다. 바로 데이터베이스의 스키마와 Entity의 스키마가 불일치하는 경우인데, 타입이 다른 것은 대부분 JPA가 애플리케이션 빌드 시 잡아서 잘못 정의되었음을 알려줍니다. 하지만 nullable한 Column을 Entity에 non-nullable하게 선언한 경우 만약 Column에 값이 null이라면 런타임 시 오류가 발생합니다.&lt;/p&gt;

&lt;p&gt;Java로 Entity를 작성할 때는 모두 nullable한 Property이기 때문에 이를 염두에 두고 코드를 작성하거나 방어코드를 넣어두어서 오류를 쉽게 감지할 수 있도록 조치를 할 수 있습니다. (사실 실무를 하면서 이렇게 꼼꼼하게 방어코드를 작성하는 경우를 많이 보진 못했습니다 ^^;;) 하지만 Kotlin은 타입으로 non-nullable을 보장해 줄 수 있습니다. 그래서 데이터베이스의 Column은 nullable인데 Entity의 Property가 non-nullable로 선언되어 런타임 시 예기치 못한 오류가 발생하는 경우를 종종 보았습니다.&lt;/p&gt;

&lt;p&gt;개인적으로는 이를 방지하기 위해서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Column&lt;/code&gt;에 nullable 여부를 함께 적어주는 것을 선호합니다. &lt;a href=&quot;https://flywaydb.org/&quot; target=&quot;\_blank&quot;&gt;Flyway&lt;/a&gt; 등을 이용해서 DDL을 적어주었더라도 Entity를 정의할 때 생략할 수 있는 부분이 있더라도 최소한의 DDL 정보를 적어주면 좋다고 생각하는데 그중 하나가 바로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nullable&lt;/code&gt;속성입니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Column&lt;/code&gt;의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nullable&lt;/code&gt;속성은 기본이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt;이므로 nullable한 Property라면 생략해주어도 된다 생각합니다. 하지만 non-nullable한 Property라면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Column(nullable = false)&lt;/code&gt;과 같이 선언해주는 습관을 지니면 위 사례와 같이 예기치 않은 오류를 만날 가능성을 많이 줄일 수 있습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@JoinColumn&lt;/code&gt;에도 마찬가지고 연관관계에서도 마찬가지입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;연관관계&quot;&gt;연관관계&lt;/h3&gt;

&lt;p&gt;Java로 JPA Entity를 정의해보았던 개발자라면 Kotlin으로 Entity를 정의할 때 연관관계를 어떻게 정의하면 좋을지 고민을 많이 해보았겠다고 생각합니다. 아마 Java에서는 편하게 정의했던 것들을 Kotlin으로 정의할 때는 상당히 번거롭다고 느껴지기도 할 것입니다. 저는 오히려 잘못 사용되고 있던 패턴을 올바르게 사용하도록 바꾸어주었다고 생각합니다. 그럼 제가 생각하는 JPA에서 Entity의 연관관계를 좀 더 잘 사용하기 위한 팁들을 알아보겠습니다.&lt;/p&gt;

&lt;h4 id=&quot;연관관계는-entity-자체로-주입하자&quot;&gt;연관관계는 Entity 자체로 주입하자&lt;/h4&gt;

&lt;p&gt;앞서 안티 패턴을 소개하던 부분에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lateinit&lt;/code&gt;을 사용하는 사례를 소개했었습니다. 이러한 패턴이 발생하는 이유는 Entity 간에 연관관계를 맺어줄 때 객체적으로 접근한 것이 아니라 데이터베이스적으로 접근하기 때문이라 생각됩니다. 이게 무슨 말이냐고 하면 만약 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt; Entity가 생성될 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; Entity가 필요하다고 가정해보겠습니다. 객체적으로 생각하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt;는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; 정보를 가지고 있어야 하므로 생성자 매개변수로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt;를 받을 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;하지만 데이터베이스적으로 접근하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt; Entity는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt;를 가지고 있으므로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt;를 매개변수로 받고 연관관계 조회는 JPA가 알아서 해줄 것이니 따로 선언하려고 하지 않을 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;writerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writerId&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;writerId&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이러한 패턴이 나오는 또 다른 이유는 아마 다음과 같이 애플리케이션을 개발하기 때문일 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardCreationCommand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@RestController&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Controller&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boardService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@PostMapping&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/boards&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createBoard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;@RequestBody&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardCreationCommand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardDto&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;BoardDto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boardService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;


&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;readOnly&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardCreationCommand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;entity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;writerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;클라이언트에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt;를 생성하기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; Entity의 정보를 모두 보내는 것보다 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt;의 식별자만 전달하는 것이 효율적입니다. 그래서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt;의 식별자만 전달하게 되는데 서버에서는 그 정보를 그대로 이용해서 Entity를 생성하는 데 사용하기 때문입니다. 저는 이렇게 사용하는 패턴이 많은 문제를 가지고 있다고 생각합니다.&lt;/p&gt;

&lt;p&gt;첫 번째로 테스트하기에 용이하지 않습니다. Entity를 생성하고 연관관계를 조회할 때 JPA에 의존하게 되면 단위테스트는 애초에 불가능해집니다. 결국 통합테스트나 기능테스트로밖에 Entity가 가진 기능들을 테스트할 수밖에 없습니다. 왜냐하면 Entity가 온전하지 않고 외부 리소스(JPA)에 의존하는 코드이기 때문입니다.&lt;/p&gt;

&lt;p&gt;두 번째, 데이터베이스와 일치하지 않는 타입을 유발합니다. Entity를 생성자 매개변수로 전달하지 않고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lateinit&lt;/code&gt;을 사용하지 않으려면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nullable&lt;/code&gt;한 타입으로 선언해서 JPA가 연관관계를 불러와 주지 않는다면 null을 반환하도록 풀어야 합니다. 하지만 도메인적으로 게시글이 생성되었는데 작성자가 존재하지 않는 게 과연 옳은 구현일까요?&lt;/p&gt;

&lt;p&gt;세 번째, 위 문제점들을 해결하기 위해 불필요한 코드들이 양산됩니다. 조금 귀찮다고 올바르게 사용하지 않다 보면 이를 우회하기 위해 더 많은 불필요한 코드들이 양산됩니다.&lt;/p&gt;

&lt;p&gt;그러므로 애플리케이션을 개발할 때 위 코드를 아래처럼 바꿔서 사용하면 좋겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardCreationCommand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@RestController&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Controller&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boardService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@PostMapping&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/boards&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createBoard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;@RequestBody&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardCreationCommand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardDto&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;BoardDto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boardService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;


&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;readOnly&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;userRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardCreationCommand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getUserById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;writerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;entity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;양방향-관계인-경우에-양쪽다-entity를-주입해주자&quot;&gt;양방향 관계인 경우에 양쪽다 Entity를 주입해주자&lt;/h4&gt;

&lt;p&gt;&lt;a href=&quot;https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#associations-one-to-many-bidirectional&quot; target=&quot;\_blank&quot;&gt;Hibernate 가이드 문서&lt;/a&gt; 를 보면 양방향 관계 시 Entity 간의 관계가 형성될 때마다 두 Entity 모두 동기화를 시켜줘야 한다고 말하고 있습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Whenever a bidirectional association is formed, the application developer must make sure both sides are in-sync at all times.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;앞서 말한 객체적으로 접근하면 어찌 보면 당연한 이야기입니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt; Entity가 생성될 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; Entity가 매개변수로 사용된다고 한다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; Entity 입장에서도 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt;가 추가되는 것이니 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User.writeBoard&lt;/code&gt;함수가 호출되어야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 생략...&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;writeBoard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`user`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToMany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ALL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;writer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;mutableBoards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableListOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;writeBoard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mutableBoards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;외부에-노출하는-연관관계-collection은-immutable-collection을-노출하자&quot;&gt;외부에 노출하는 연관관계 Collection은 Immutable Collection을 노출하자&lt;/h4&gt;

&lt;p&gt;Kotlin은 두 가지 유형의 Collection 인터페이스를 제공해줍니다. Immutable Collection과 Mutable Collection인데 다이어그램을 보면 아래와 같습니다. (출처: &lt;a href=&quot;https://kotlinlang.org/docs/collections-overview.html#collection-types&quot; target=&quot;\_blank&quot;&gt;Kotlin 공식문서&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/Kotlin-jpa-entity/collections-diagram.png&quot; alt=&quot;collections-diagram&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Immutable Collection은 말 그대로 요소를 변경할 수 없도록 설계된 Collection이고 Mutable Collection은 Immutable Collection에 setter를 추가한 Collection이라 보면 됩니다.&lt;/p&gt;

&lt;p&gt;JPA에서 연관관계의 요소의 변경은 데이터베이스의 변경을 유발합니다. 그래서 아래와 같이 Property를 불변으로 선언하더라도 언제든지 외부에서 연관관계를 변경할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`user`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToMany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ALL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;writer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;mutableBoards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableListOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;


&lt;span class=&quot;nd&quot;&gt;@DataJpaTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;showSql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserRepositoryTest&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Autowired&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;userRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserRepository&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Autowired&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestEntityManager&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;홍길동&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;내용&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board2&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;내용&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;findUser&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board3&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;내용&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;findUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mutableBoards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;merge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;findUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Hibernate: 
    insert 
    into
        &quot;user&quot;
        (name, id) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        board
        (content, created_at, link, rank, title, &quot;writer_id&quot;, id) 
    values
        (?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        board
        (content, created_at, link, rank, title, &quot;writer_id&quot;, id) 
    values
        (?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        user0_.id as id1_4_0_,
        user0_.name as name2_4_0_ 
    from
        &quot;user&quot; user0_ 
    where
        user0_.id=?
Hibernate: 
    select
        board0_.id as id1_0_1_,
        board0_.content as content2_0_1_,
        board0_.created_at as created_3_0_1_,
        board0_.link as link4_0_1_,
        board0_.rank as rank5_0_1_,
        board0_.title as title6_0_1_,
        board0_.&quot;writer_id&quot; as writer_i7_0_1_,
        mutabletag1_.board_id as board_id1_2_3_,
        tag2_.id as tag_id2_2_3_,
        tag2_.id as id1_3_0_,
        tag2_.&quot;key&quot; as key2_3_0_,
        tag2_.&quot;value&quot; as value3_3_0_ 
    from
        board board0_ 
    left outer join
        board_tag_assoc mutabletag1_ 
            on board0_.id=mutabletag1_.board_id 
    left outer join
        tag tag2_ 
            on mutabletag1_.tag_id=tag2_.id 
    where
        board0_.id=?
Hibernate: 
    insert 
    into
        board
        (content, created_at, link, rank, title, &quot;writer_id&quot;, id) 
    values
        (?, ?, ?, ?, ?, ?, ?)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;로그를 보면 알 수 있다시피 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;add&lt;/code&gt; 함수를 통해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt;가 가진 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt;를 외부에서 자유롭게 추가할 수 있음을 볼 수 있습니다. 이러한 방식은 앞서 말한 Entity의 캡슐화를 위반하는 것이므로 부작용을 유발할 가능성이 높습니다. 그러면 위와 같은 현상이 발생하지 않도록 하기 위해서는 어떻게 코드를 작성하면 좋을까요? 연관관계를 변경하는 일은 Entity의 특성상 필요한 일이므로 MutableList로 선언하는 것이 좋다고 생각됩니다. 그럼 MutableList는 내부에서만 접근할 수 있도록 하고 ImmutableList를 외부에 노출하도록 하면 됩니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`user`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToMany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ALL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;writer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;mutableBoards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableListOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutableBoards&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;홍길동&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;내용&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board2&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;내용&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;findUser&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board3&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;내용&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;findUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// compile error : Unresolved reference: add&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다만 위와같은 방식에도 문제점은 존재합니다. 아래 코드를 보겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;홍길동&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;내용&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board2&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;내용&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;findUser&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boards&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;findUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boards&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board3&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;내용&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;findUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;writeBoard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// assert error : expected: &amp;lt;2&amp;gt; but was: &amp;lt;3&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;우리는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User.boards&lt;/code&gt;를 조회했을 때 보통 조회한 시점에 게시판 목록이 그대로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;boards&lt;/code&gt; 변수에 담겨 있을 것이라 기대합니다. 하지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User.writeBoard&lt;/code&gt;함수가 호출되고 난 후 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;boards&lt;/code&gt;변수에 담겨있는 게시판 목록의 수가 바뀐 것을 볼 수 있습니다. 지금은 간단한 코드이기 때문에 그 이유를 쉽게 찾을 수 있지만 여러 함수가 복잡하게 얽히고설켜 있으면 그 코드를 추적하는 것은 참 어려운 일입니다. 이러한 이유로 불변(Immutable)은 복잡함을 줄이고 코드를 단순화할 수 있다고 하는 것입니다. 자 그럼 이 문제를 해결할 수 있는 코드를 보겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;`user`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PrimaryKeyEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToMany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ALL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;writer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;mutableBoards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableListOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutableBoards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;writeBoard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mutableBoards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mutableBoards&lt;/code&gt;에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toList&lt;/code&gt;함수를 사용해서 Collection의 요소를 복제하여 반환하면 더 이상 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mutableBoards&lt;/code&gt;가 변경되어도 테스트 코드에 선언된 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;baords&lt;/code&gt;에 영향을 미치지 않게 됩니다. Entity의 Collection을 다룰 때 이 부분을 잊지 않도록 꼭 염두에 두길 바랍니다.&lt;/p&gt;

&lt;h4 id=&quot;단순하게-nm-관계를-맺어주고-싶다면-manytomany를-선언하자&quot;&gt;단순하게 N:M 관계를 맺어주고 싶다면 ManyToMany를 선언하자&lt;/h4&gt;

&lt;p&gt;JPA를 사용하는 많은 개발자가 ManyToMany를 선언하는 것을 꺼리는 경향을 볼 수 있습니다. 아마 많은 블로그나 영상에서 ManyToMany는 좋지 못하다고 이야기하므로 그러겠다고 생각됩니다. ManyToMany를 기피하는 이유는 아마 관계 테이블의 변경이 발생하였을 때 변경이 불편하기 때문이라 생각됩니다. 관계 정보에 단순하게 A Entity와 B Entity의 Primary Key만 존재하는 것이 아니라 다른 추가 정보가 들어갈 가능성이 있기 때문이라고 많은 글에서 이야기합니다. 하지만 저는 미래에 일어날지도 모를 가능성 때문에 굳이 좋은 연관관계 표현 방식을 포기해야 하는지 모르겠습니다. ManyToMany를 사용하면 불필요한 Assoc Entity를 선언하지 않아도 됩니다. Entity 입장에서는 N:M 관계인지 1:N 관계인지 중요하지 않습니다. 이것은 데이터베이스 관점에서 중요한 포인트입니다. 그래서 A Entity가 B Entity를 다수 가질 수 있고 B Entity가 A Entity를 다수 가질 수 있는 구조에서 중간에 Assoc Entity가 불필요한 것입니다.&lt;/p&gt;

&lt;p&gt;많은 글에서 말하는 Assoc Entity에 새로운 정보가 추가된다면 이것은 단순하게 도메인에 필요한 기능이 변경된 것입니다. 그렇다면 테스트 코드를 변경하고 이 요구사항을 충족하기 위해 기능을 변경하면서 Assoc Entity를 추가하고 관계를 ManyToMany에서 OneToMany, ManyToOne으로 바꿔주면 되는 것입니다.&lt;/p&gt;

&lt;p&gt;혹자는 데이터베이스를 변경하는 것은 어렵기 때문에 미리 변경 가능성을 열어두고 설계해야 한다고 말할 수도 있습니다. 맞는 말입니다. 그래서 관계 테이블이 변경될 가능성이 크다면 미리 변경 가능성을 열어두고 ManyToMany를 선언하지 않을 수도 있습니다. 하지만 그것은 비즈니스 요구사항을 충분히 뜯어보고 고민해본 후 내린 결정이어야 하리라 생각됩니다. 덮어놓고 ManyToMany는 좋지 않은 것이니 불필요한 Assoc Entity를 만들어 연관관계를 맺어줄 필요는 없다고 생각합니다.&lt;/p&gt;

&lt;p&gt;그래서 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt; Entity와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Tag&lt;/code&gt; Entity의 관계에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@ManyToMany&lt;/code&gt;를 선언해서 연관관계를 표현해 주었습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@JoinTable&lt;/code&gt;에 대해서는 JPA를 사용해본 분이라면 아실 테니 설명은 생략하겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@ManyToMany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PERSIST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MERGE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@JoinTable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;board_tag_assoc&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;joinColumns&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;board_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)],&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;inverseJoinColumns&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;tag_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)],&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;mutableTags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toMutableSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mutableTags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;여기서 추가로 팁을 주자면 앞서 정의한 도메인에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt;가 삭제될 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Tag&lt;/code&gt;는 삭제되지 않길 바라고 있습니다. 왜냐하면 서로 생명주기가 다르기 때문인데요. 그래서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CascadeType.REMOVE&lt;/code&gt;를 제외해야 합니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;board_tag_assoc&lt;/code&gt;이 삭제되어야 한다고 생각해서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CascadeType.REMOVE&lt;/code&gt;를 함께 추가해버리면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Tag&lt;/code&gt; Entity도 함께 삭제되니 주의할 필요가 있습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CascadeType.REMOVE&lt;/code&gt;가 없어도 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;board_tag_assoc&lt;/code&gt;은 삭제됩니다.&lt;/p&gt;

&lt;h4 id=&quot;중복된-관계를-추가하고-싶지-않다면-set을-활용하자&quot;&gt;중복된 관계를 추가하고 싶지 않다면 Set을 활용하자&lt;/h4&gt;

&lt;p&gt;위 코드를 자세히 보았다면 눈치챘겠지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Tag&lt;/code&gt;는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Set&lt;/code&gt;으로 정의된 것을 볼 수 있습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Tag&lt;/code&gt; Entity를 보면 알 수 있겠지만 동일한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Key&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Value&lt;/code&gt;는 저장될 수 없습니다. 즉, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt;에는 하나의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Key&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Value&lt;/code&gt; 세트만 입력될 수 있다는 것입니다. 만약 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;List&lt;/code&gt;로 Collection을 선언한다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;addTag&lt;/code&gt;함수에서 매번 아래와 같이 기존 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Tag&lt;/code&gt;에 새로 입력하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Tag&lt;/code&gt;가 존재하는지 여부를 체크해야 할 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;addTag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mutableTags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;contains&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mutableTags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;하지만 Set Collection을 이용하면 위와 같은 코드는 불필요합니다. 자료구조가 중복된 요소를 알아서 제거해주기 때문입니다. 이렇듯 연관관계를 표현할 때 반드시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;List&lt;/code&gt;만 사용할 필요는 없습니다. 적절한 자료구조를 선택해서 효율적으로 코드를 작성하면 좋겠습니다.&lt;/p&gt;

&lt;h3 id=&quot;value-object&quot;&gt;Value Object&lt;/h3&gt;

&lt;p&gt;Entity를 이야기할 때 Value Object를 빼고 이야기할 순 없습니다. &lt;a href=&quot;https://veluxer62.github.io/explanation/about-entity-and-value-object/#value-object&quot; target=&quot;\_blank&quot;&gt;Value Object에 관한 글&lt;/a&gt; 을 보면 Value Object는 식별자가 없이 속성값으로 식별하는 객체를 말합니다. 위에서 정의한 도메인에서도 두 가지 형태의 Value Object를 예로 들어봤습니다.&lt;/p&gt;

&lt;h4 id=&quot;embedded&quot;&gt;Embedded&lt;/h4&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Embedded&lt;/code&gt;는 Entity가 가진 Property 들의 공통된 특성을 하나의 객체로 묶어 표현해줄 때 사용할 수 있습니다. Entity와 Entity를 연결해주는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@OneToOne&lt;/code&gt; 관계로도 풀 수 있으나 이는 성격이 조금 다른 예라고 볼 수 있습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Embedded&lt;/code&gt;는 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Embedable&lt;/code&gt;이 선언된 클래스와 연결해서 사용할 수 있습니다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Kotlin&quot;&gt;@Embeddable
data class BoardInformation(
    @Column
    val link: String?,

    @Column(nullable = false)
    val rank: Int,
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;데이터베이스에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt; 테이블에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rank&lt;/code&gt; Column으로 정의되지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt; Entity에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BoardInformation&lt;/code&gt;이 가진 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rank&lt;/code&gt; Property로 표현되기 때문에 평탄하게 표현된 속성을 묶어주는 용도로 사용하기 좋습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Embedable&lt;/code&gt; 클래스는 Value Object이기 때문에 값이 중요한 객체입니다. 그래서 Kotlin의 Data class로 사용하기 아주 알맞은 객체이기도 합니다.&lt;/p&gt;

&lt;h4 id=&quot;elementcollection&quot;&gt;ElementCollection&lt;/h4&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Embedded&lt;/code&gt;가 단일 Value Object라면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ElementCollection&lt;/code&gt;은 복수의 Value Object를 표현해줄 때 사용합니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@OneToMany&lt;/code&gt;와 유사하지만 다른 점은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@OneToMany&lt;/code&gt;는 Entity와 Entity 간의 관계를 나타낼 때 사용하고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@ElementCollection&lt;/code&gt;은 Entity와 Value Object 간의 관계를 나타낼 때 사용합니다. 즉 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@ElementCollection&lt;/code&gt;에 정의된 Value Object는 Entity의 생명주기와 함께하며 식별자를 가지지 않는 것이 특징입니다. 왜냐하면 Value Object는 값이 곧 식별자이기 때문입니다.&lt;/p&gt;

&lt;p&gt;위에 정의한 도메인에서는 게시판의 댓글인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Comment&lt;/code&gt;를 Value Object로 표현해보았습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Comment&lt;/code&gt;를 Value Object로 봐도 되는지에 대해서는 논란의 여지가 있겠지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@ElementCollection&lt;/code&gt;을 보다 쉽게 소개하기 위한 용도로 정의한 것이니 실무에서 사용하는 것과 같이 깊이 생각은 하지 않았으면 좋겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Embeddable&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;writer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@ElementCollection&lt;/code&gt;의 또 하나 특징은 Collection의 요소가 변경될 때마다 아래와 같이 모든 데이터를 지웠다가 새롭게 등록한다는 것입니다. Value Object는 Entity의 하나의 Property에 불과하다는 점과 식별자가 따로 없다는 점에서 이러한 동작 방식은 어색하지 않습니다. 객체 관점에서는 문제가 없으나 데이터베이스 관점에서는 문제가 될 수 있으므로 이 부분을 염두에 두고 설계하면 좋겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@DataJpaTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;showSql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserRepositoryTest&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Autowired&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardRepository&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Autowired&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lateinit&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestEntityManager&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;홍길동&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;게시판&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;내용&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardInformation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addComment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;내용&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;persist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getReferenceById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addComment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;내용2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;merge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testEntityManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Hibernate: 
    insert 
    into
        &quot;user&quot;
        (name, id) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        board
        (content, created_at, link, rank, title, &quot;writer_id&quot;, id) 
    values
        (?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        board_comment
        (board_id, content, &quot;writer_id&quot;) 
    values
        (?, ?, ?)
Hibernate: 
    select
        board0_.id as id1_0_0_,
        board0_.content as content2_0_0_,
        board0_.created_at as created_3_0_0_,
        board0_.link as link4_0_0_,
        board0_.rank as rank5_0_0_,
        board0_.title as title6_0_0_,
        board0_.&quot;writer_id&quot; as writer_i7_0_0_ 
    from
        board board0_ 
    where
        board0_.id=?
Hibernate: 
    select
        mutablecom0_.board_id as board_id1_1_0_,
        mutablecom0_.content as content2_1_0_,
        mutablecom0_.&quot;writer_id&quot; as writer_i3_1_0_ 
    from
        board_comment mutablecom0_ 
    where
        mutablecom0_.board_id=?
Hibernate: 
    delete 
    from
        board_comment 
    where
        board_id=?
Hibernate: 
    insert 
    into
        board_comment
        (board_id, content, &quot;writer_id&quot;) 
    values
        (?, ?, ?)
Hibernate: 
    insert 
    into
        board_comment
        (board_id, content, &quot;writer_id&quot;) 
    values
        (?, ?, ?)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;cascade&quot;&gt;Cascade&lt;/h3&gt;

&lt;p&gt;JPA 뿐만아니라 많은 ORM에서 영속성 전이(Cascade)를 활용할 수 있도록 기능을 제공해줍니다. Cascade를 활용하면 비즈니스 코드를 상당히 줄일 수 있습니다. 물론 잘 활용하기 위해서는 도메인에 대한 깊은 이해가 필요한 것도 사실입니다.&lt;/p&gt;

&lt;p&gt;Entity를 정의하다 보면 하나의 Entity의 생명주기에 다른 Entity도 영향을 받아야 하는 경우가 종종 있습니다. 예제로 든 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;사용자가 삭제되면 해당 사용자가 작성한 모든 게시판이 삭제되어야 한다&lt;/code&gt;와 같이 말입니다. 만약 이 스토리를 만족시키기 위해 코드를 작성할 때 Cascade를 활용하지 않으면 어떤 코드가 작성될지 아래 서비스 코드를 보겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;userRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BoardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getReferenceById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deleteAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;userRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 코드와 같이 사용자를 삭제하기 위해서는 게시판이 삭제되어야 하므로 게시판을 먼저 삭제하는 코드가 필요합니다. 하지만 Cascade를 활용하면 위 코드를 훨씬 단순하게 바꿀 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;userRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;userRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deleteById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Entity를 새롭게 추가하는 경우에도 Cascade를 활용하면 좋습니다. 게시판에 태그를 추가하는 스토리를 구현해보겠습니다. Cascade가 없다면 아래와 같이 구현할 수 있을 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;addTag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TagCreationCommand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;tag&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tagRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findByKeyAndValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;orElseGet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;tagRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getReferenceById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addTag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;태그를 먼저 영속화한 다음에 게시판에 태그를 추가해주는 방식으로 해주어야 합니다. 하지만 Cascade를 활용하면 태그를 영속화해주는 코드가 필요 없습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;addTag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TagCreationCommand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Board&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;tag&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tagRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findByKeyAndValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;orElse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;board&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boardRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getReferenceById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addTag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;board&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;이렇듯 Cascade를 활용하면 많은 코드를 생략할 수 있습니다. 코드를 생략함으로써 명시적이지 않으니 좋지 않은 것이 아니냐는 생각이 들 수 있습니다. 하지만 저는 ORM을 사용하는 순간 명시적이지 않은 동작은 수없이 많기 때문에 그런 우려라면 ORM사용을 다시 검토해보아야 한다고 생각합니다. 오히려 도메인에 대한 충분한 이해를 바탕으로 ORM이 제공해주는 기능을 충분히 활용한다면 번거롭고 복잡한 코드를 작성하지 않고 간단한 코드로도 원하는 기능을 충분히 수행할 수 있을 것입니다.&lt;/p&gt;

&lt;h2 id=&quot;마치며&quot;&gt;마치며&lt;/h2&gt;

&lt;p&gt;지금까지 Kotlin을 이용하여 JPA Entity를 정의할 때 참고하면 좋을 만한 여러 가지 팁들을 소개해보았습니다. JPA 사용법에 대한 내용을 다루는 것이 아니라 다소 생략된 내용이 많이 있지만 Kotlin으로 Entity를 정의할 때 도움이 되었으면 하는 바람입니다. 사실 Entity를 정의하면서 이렇게까지 고민해야 하나 싶은 생각도 듭니다. 하지만 우리가 조금만 생각하고 Entity를 정의하면 애플리케이션의 복잡도를 상당히 낮출 수 있습니다. 억지스러운 구현을 위해 코드들 이리저리 우회하는 형태로 작성하지 않기 때문입니다.&lt;/p&gt;

&lt;p&gt;위 고민이 단순히 Entity를 이쁘게 작성하기 위한 팁이 아니라 우리의 제품을 위한 코드가 덜 복잡하고 더 안정적으로 기능이 동작하도록 작성되어서 사용자에게 제품의 가치를 보다 잘 전달할 수 있도록 하기 위한 노력임을 알아봐 주셨으면 합니다.&lt;/p&gt;

&lt;p&gt;위에서 예시로 보여준 전체 코드는 &lt;a href=&quot;https://github.com/veluxer62/kotlin-entity-tutorial&quot; target=&quot;\_blank&quot;&gt;Kotlin JPA Entity 예제코드&lt;/a&gt; 에 가면 볼 수 있습니다.&lt;/p&gt;

&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;

&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://kotlinlang.org/docs/home.html&quot; target=&quot;\_blank&quot;&gt;https://kotlinlang.org/docs/home.html&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html&quot; target=&quot;\_blank&quot;&gt;https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>우당탕탕 주문서 개발기</title>
      <link>https://spoqa.github.io/2022/07/08/order-sheet-development-story.html</link>
      <pubDate>Fri, 08 Jul 2022 00:00:00 +0000</pubDate>
      <author>남경호</author>
      <guid>/2022/07/08/order-sheet-development-story</guid>
      <description>&lt;p&gt;안녕하세요. 키친보드 팀의 백엔드 프로그래머 남경호입니다.&lt;/p&gt;

&lt;p&gt;최근 키친보드 팀에서는 정비기간을 마친 후 처음으로 제품 개발을 위한 프로젝트를 성황리에(?) 마쳤습니다. 약 7주간 진행했던 이 프로젝트는 키친보드가 제공하던 기존 기능과는 전혀 다른 성격의 기능을 제공하기 위한 프로젝트였습니다. 그러다 보니 개발할 내용도 많고 프로젝트 일정도 넉넉하지 않아 말도 많고 탈도 많았던 프로젝트였던 것 같습니다 ^^;;&lt;/p&gt;

&lt;p&gt;아무튼! 이 글에서는 이번 프로젝트에서 새롭게 개발했던 수많은 기능 중에 백엔드 챕터에서 가장 중요하게 생각하고 고민거리와 이슈도 많았던 주문서 도메인을 개발하면서 겪었던 이야기를 공유해드리고자 합니다.&lt;/p&gt;

&lt;h1 id=&quot;주문서-도메인&quot;&gt;주문서 도메인?&lt;/h1&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;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;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;h2 id=&quot;생명주기&quot;&gt;생명주기&lt;/h2&gt;

&lt;p&gt;위에서 정의된 유저 스토리를 기반으로 주문서의 생명주기를 다이어그램으로 표현하면 아래와 같습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/order-sheet-development-story/life-cycle-diagram.png&quot; alt=&quot;life-cycle-diagram&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;매장은 주문정보를 입력하여 거래처에 주문서를 접수합니다.&lt;/li&gt;
  &lt;li&gt;거래처/매장/관리자는 접수한 주문서에 수정할 항목이 있는지 확인합니다. 수정할 항목이 있다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3번&lt;/code&gt;으로 이동합니다. 수정할 항목이 없다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4번&lt;/code&gt;으로 이동합니다.&lt;/li&gt;
  &lt;li&gt;주문서를 수정합니다. 수정 후 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2번&lt;/code&gt;으로 이동합니다.&lt;/li&gt;
  &lt;li&gt;거래처는 접수된 주문서가 배송 가능한지 확인합니다. 배송이 가능하다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;5번&lt;/code&gt;으로 이동합니다. 배송이 불가능하다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;6번&lt;/code&gt;으로 이동합니다.&lt;/li&gt;
  &lt;li&gt;주문서를 마감합니다.&lt;/li&gt;
  &lt;li&gt;주문서를 취소합니다.&lt;/li&gt;
  &lt;li&gt;종료합니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;고민거리&quot;&gt;고민거리&lt;/h1&gt;

&lt;p&gt;주문서 도메인의 유저 스토리를 보며 개발할 것들을 정리하고 설계하면서 여러 가지 고민거리들이 있었습니다. 그래서 개발을 시작하기 전 함께 고민하고 결정한 것들을 이야기해 보겠습니다.&lt;/p&gt;

&lt;h2 id=&quot;여러-사용자가-동일한-주문서를-동시에-수정하면&quot;&gt;여러 사용자가 동일한 주문서를 동시에 수정하면?&lt;/h2&gt;

&lt;p&gt;유저 스토리를 처음 보았을 때 가장 우려되었던 부분이 주문서의 수정이 주문을 접수한 매장뿐만 아니라 거래처와 관리자 모두 주문서를 수정할 수 있다는 것이었습니다. 커머스 도메인을 다루어보신 분들이라면 아마 같은 걱정을 해보셨을 텐데요. 주문한 사용자는 자기도 모르게 주문서가 수정된다면 참 당황스러울 것이고 이는 곧 CS로 이어지게 됩니다. 거기다 주문을 수정하는 주체가 주문한 사용자 외에 다른 사용자도 가능하므로 동시에 주문서를 수정하게 된다면 의도치 않게 주문서 데이터가 변경될 위험성이 있습니다. 만약 동시에 주문데이터 수정이 발생해서 주문서 정보의 최종 상태가 의도치 않게 변경된다면 CS 처리도 상당히 어려워질 것이고 매번 개발자가 데이터 정합성을 맞춰줘야 하는 상황이 생길 것입니다. (아래부터는 이러한 이슈를 간단하게 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;동시성 이슈&lt;/code&gt;라고 부르겠습니다.)&lt;/p&gt;

&lt;p&gt;좀 더 이해가 쉽도록 다이어그램으로 설명해보겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/order-sheet-development-story/multi-thread-problem.png&quot; alt=&quot;multi-thread problem&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 그림과 같이 매장과 거래처가 동시에 수정을 요청하였다고 가정해보겠습니다. 매장과 거래처가 수정요청을 동시에 수행하였고 문제없이 처리되었지만, 매장이 수정한 정보는 온데간데없고 거래처가 수정한 정보만 주문서에 반영된 것을 보게 될 것입니다. 매장 입장에서는 당황스럽겠죠?&lt;/p&gt;

&lt;p&gt;그래서 저희가 주문서 도메인을 설계할 때 동시성 이슈 즉, 주문서의 수정이 동시에 수정하는 경우 수정이 되지 않도록 방지하여 만약 매장이 주문서를 수정하고자 할 때 거래처가 주문서를 수정한 경우 수정이 된 것을 매장이 알 수 있도록 조치하였습니다. 이러한 조치를 위해 어떤 방법을 채택하였을까요?&lt;/p&gt;

&lt;h3 id=&quot;데이터베이스의-비관적-잠금&quot;&gt;데이터베이스의 비관적 잠금&lt;/h3&gt;

&lt;p&gt;가장 먼저 제시되었던 의견은 바로 RDBMS의 비관적 잠금을 활용하는 것이었습니다. 비관적 잠금은 여러 사용자가 동시에 특정 레코드에 대한 수정요청이 들어오는 경우 데이터베이스의 잠금 기능을 이용하여 요청이 들어온 순서대로 수정사항을 처리하도록 하여서 원치 않게 데이터가 변경되지 않도록 방지하는 방법을 말합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/order-sheet-development-story/pessimistic-lock.png&quot; alt=&quot;pessimistic-lock&quot; /&gt;&lt;/p&gt;

&lt;p&gt;하지만 비관적 잠금은 일반적인 커머스에서 동시에 동일 상품에 대한 주문 시 재고차감과 같은 상황에서는 도움이 될진 몰라도 저희가 수행하고자 하는 주문서의 품목 수정에는 도움이 되지 않았습니다. 왜냐하면 매장이 주문서의 정보를 수정했는데 거래처가 이를 인지하지 못하고 주문서 정보를 덮어씌워 버리기 때문입니다.&lt;/p&gt;

&lt;h3 id=&quot;데이터베이스의-낙관적-잠금&quot;&gt;데이터베이스의 낙관적 잠금&lt;/h3&gt;

&lt;p&gt;비관적 잠금으로 인해 사용자가 인지하지 못한 상태로 주문서가 덮어씌워져 버리는 것이 문제라면 낙관적 잠금을 통해서 다음 사용자가 수정을 못 하도록 막으면 되지 않을까 하는 의견이 제시되었습니다. 낙관적 잠금은 데이터베이스에 잠금을 걸기보다 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;version&lt;/code&gt;과 같은 속성을 이용하여 동시에 동일한 레코드의 수정이 발생하지 않도록 방지하는 방법을 말합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/order-sheet-development-story/optimistic-lock.png&quot; alt=&quot;optimistic-lock&quot; /&gt;&lt;/p&gt;

&lt;p&gt;아쉽게도 이 방법도 저희 팀이 해결하고자 하는 문제를 해결하지 못하였습니다. 낙관적 잠금은 서버에 요청이 동시에 들어오는 경우에는 나중에 요청한 사용자가 주문서를 수정할 수 없기 때문에 원하는 바를 충족할 수 있습니다. 하지만 매장이 주문서를 보는 상황에서 주문서 수정을 요청하기 전에 거래처에서 주문서 수정을 완료 처리하게 되면 매장은 주문서를 거래처 수정요청이 완료된 이후에 요청한 후 완료 처리하였기 때문에 낙관적 잠금으로 인해 수정 요청이 거절되지 않고 원하는 주문서 정보로 수정할 수 있을 것입니다. 하지만 거래처에서 수정한 내용을 매장은 몰랐기 때문에 거래처의 수정내용은 비관적 잠금과 마찬가지로 덮어씌워지게 되는 꼴이 되는 것입니다.&lt;/p&gt;

&lt;h3 id=&quot;어플리케이션의-낙관적-잠금&quot;&gt;어플리케이션의 낙관적 잠금&lt;/h3&gt;

&lt;p&gt;결국, 데이터베이스의 잠금으로는 저희가 해결하고자 하는 여러 사용자가 같은 주문서를 수정했을 때 사용자가 인지하지 못한 상황에서 주문서의 내용을 덮어씌워 버리는 현상을 막을 방법이 마땅하지 않다고 생각했습니다. 그래서 데이터베이스의 잠금을 이용하는 것이 아닌 애플리케이션의 로직으로 낙관적 잠금과 유사하게 이 문제를 해결하고자 하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/order-sheet-development-story/application-lock.png&quot; alt=&quot;application-lock&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 다이어그램을 보면 동일한 주문서를 매장과 거래처가 동시에 조회하고 있는 상황에서 매장이 먼저 주문서를 수정 완료한 후 거래처가 주문서를 수정하면 오류가 발생함을 볼 수 있습니다. 즉, 사용자가 최초 조회한 화면과 서버의 데이터가 다른 상태라면 수정내용을 의도하지 않게 덮어씌우지 못하도록 막고 사용자가 다시 주문서를 확인한 후 수정할 수 있도록 의도한 것입니다.&lt;/p&gt;

&lt;p&gt;이는 주문서 수정 시도 시 사용자가 예상치 못하게 수정 요청 실패를 경험할 수 있다는 단점이 있지만 동일한 주문서를 조회하는 사용자가 많지 않기에 수정 요청 실패를 마주칠 확률이 낮고 사용자가 의도치 않게 주문서를 수정하는 행위에 대한 위험성이 더 크다고 판단해서 이와 같은 결정을 내리게 되었습니다.&lt;/p&gt;

&lt;h3 id=&quot;구현&quot;&gt;구현&lt;/h3&gt;

&lt;p&gt;설명은 거창하였지만 사실 구현은 단순합니다.&lt;/p&gt;

&lt;p&gt;먼저 단일 주문서 조회 시 내려주는 정보에서 현재 주문서의 버전 정보를 나타내는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt; 속성을 추가하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-graphql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;주문서&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;OrderSheetField&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ID&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;리비전&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;...생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Query&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문서를&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;조회합니다.&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;!):&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;OrderSheetField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;...생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그리고 수정요청 시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ID&lt;/code&gt;뿐만 아니라 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt; 정보도 함께 전송하도록 해서 주문서를 수정할 때 버전 정보를 함께 보내도록 하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-graphql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;주문서 수정 input&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UpdateOrderSheetInput&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문서&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;아이디&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문서&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;리비전&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;...생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Mutation&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;주문서를&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;수정합니다.&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updateOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UpdateOrderSheetInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;!):&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UpdateOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;...생략&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;도메인 서비스에서는 클라이언트에서 전송받은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ID&lt;/code&gt;를 이용하여 주문서정보를 데이터베이스에서 조회하고 주문서를 수정할 때 그 속성을 비교하여 만약 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt; 정보가 다르다면 예외를 발생시키고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt; 정보가 같다면 정상적으로 주문서를 수정할 수 있도록 하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;auditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetAuditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UpdateOrderSheetData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;auditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getByIdAndRevision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheetId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toDomainData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;auditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;userType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;auditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getByIdAndRevision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderSheetRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findByIdUsingPessimisticLock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;orElseThrow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ErrorMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NotFound&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ORDER_SHEET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ConcurrentModificationException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;주문서의 revision이 일치하지 않습니다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getByIdAndRevision&lt;/code&gt;함수를 보시면 알 수 있으시겠지만, ID를 이용하여 주문서를 조회할 때 비관적 잠금을 사용했습니다. 그 이유는 비관적 잠금을 하지 않는 경우 만약에 동시에 매장과 거래처가 주문 수정요청을 한다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;을 비교할 때 매장과 거래처의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;이 데이터베이스에 존재하는 주문서의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt; 값과 동일할 것이기 때문에 두 개의 요청 모두 수정 성공할 것이고 이는 앞에서 말한 주문서 정보가 원치 않게 수정되는 문제를 야기하게 될 것이기 때문입니다.&lt;/p&gt;

&lt;p&gt;위와 같은 고민을 한 결과 앱 사용자는 만약 수정 요청 시 보고 있던 화면과 데이터베이스의 내용이 달라 수정요청이 변경되면 아래와 같이 변경 사항이 있음을 인지하고 다시 조회하는 화면을 구성할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/order-sheet-development-story/optimistic-error.png&quot; alt=&quot;optimistic-error&quot; width=&quot;400&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;거래처가-관리하는-품목정보를-수정하면&quot;&gt;거래처가 관리하는 품목정보를 수정하면?&lt;/h2&gt;

&lt;p&gt;다음 고민은 주문서에 입력되는 품목정보를 어떻게 관리하는가였습니다. 거래처는 매장이 주문할 수 있는 품목정보를 가지고 있습니다. 그래서 매장은 이 품목들을 보고 오늘 거래처에 어떤 품목을 몇 개 주문할지 결정한 후 주문서를 제출하게 됩니다.&lt;/p&gt;

&lt;p&gt;여기에서 만약 거래처가 품목정보를 수정하게 되면 이전에 주문했던 주문서의 품목은 사용자에게 어떻게 보여야 할까요? 사용자가 원하는 것에 따라 다르겠지만 저희는 주문서가 주문을 위한 용도뿐만 아니라 주문 이력을 관리하는 용도로도 사용된다고 판단했기에 거래처에서 관리하는 품목정보를 변경되었다고 해서 한번 전송된 주문서의 품목정보는 변경되지 않아야 한다고 생각했습니다. 그래서 주문 시 선택하는 품목들은 주문서가 생성되는 시점에 주문서의 품목으로 정보들이 복사되어 저장하도록 구현하였습니다. 그렇게 하면 거래처가 관리하는 품목정보를 참조하고 있지 않기 때문에 거래처 품목정보가 수정되어도 주문서에 반영되지 않을 테니까요.&lt;/p&gt;

&lt;p&gt;하지만 여기서 또 하나의 고민거리가 추가됩니다. 바로 주문서의 수정 시 이미 주문한 품목정보를 사용자에게 보여줄 때인데요. 아래 이미지와 같이 주문서의 품목을 수정하려고 할 때 이미 주문한 품목정보들은 주문한 내용이 그대로 반영되어 보여주게 되어있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/order-sheet-development-story/edit-product.png&quot; alt=&quot;edit-product&quot; width=&quot;400&quot; /&gt;&lt;/p&gt;

&lt;p&gt;만약 여기에서 채원유통이라는 거래처가 관리하는 품목 중 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;미림(롯데)&lt;/code&gt;이라는 품목이 변경되어 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;미림(오뚜기)&lt;/code&gt;으로 바뀌었다고 가정해보겠습니다. 그러면 위 화면에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;미림(롯데) - 2통&lt;/code&gt;으로 표시되던 것이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;미림(오뚜기) - 2통&lt;/code&gt;으로 표시되어야 할까요? 사용자가 주문한 품목은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;미림(롯데)&lt;/code&gt;이지 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;미림(오뚜기)&lt;/code&gt;이 아니기 때문에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;미림(오뚜기) - 0통&lt;/code&gt;으로 표시되는 게 맞아 보입니다. 즉, 거래처 입장에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;미림(롯데)&lt;/code&gt; 품목이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;미림(오뚜기)&lt;/code&gt;으로 변경되었다고 생각할 수 있지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;미림(롯데)&lt;/code&gt; 품목은 삭제되었고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;미림(오뚜기)&lt;/code&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;blockquote&gt;
  &lt;p&gt;시스템은 주문서의 모든 변경 사항을 기록한다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;그래서 주문서의 변경 사항을 저장하기 위해 주문서 이력 Entity를 추가하기로 하였습니다. 여기서 고민은 주문서 상태를 어떻게 저장할 것 인가였습니다.&lt;/p&gt;

&lt;p&gt;처음에는 주문서와 동일하게 컬럼을 구성하고 필요한 추가정보를 가진 컬럼을 가진 형태로 만들어보려고 하였습니다. 데이터베이스를 이용하여 이력에 대한 통계를 낸다거나 검색하기에는 용이할 수 있을 테지만 그런 요구사항이 없다면 반드시 컬럼을 동일하게 맞출 필요는 없어 보였습니다. 그래서 저희는 하나의 컬럼에 JSON 형태로 주문서 정보를 저장하는 방법을 채택하였습니다.&lt;/p&gt;

&lt;p&gt;다행스럽게도 저희가 사용하는 PostgreSQL에서는 JSON과 JSONB 컬럼 타입이 있습니다. JSON과 JSONB 타입은 JSON 형태의 문자를 저장할 때 varchar 타입과 달리 JSON 포맷인지 유효성 체크를 해줍니다. 그래서 해당 컬럼에는 JSON 데이터가 저장되어있다는 것을 보장할 수 있습니다.&lt;/p&gt;

&lt;p&gt;JSON과 JSONB타입의 차이는 JSON은 입력된 값 그대로 문자열로 저장하지만, JSONB 타입은 바이너리 형태로 저장합니다. 그래서 JSONB가 JSON 타입에 비해 쓰기에 대한 부하가 컸지만, 조회 시에는 성능도 더 좋고 JSON 칼럼에 대해 인덱싱도 할 수 있고 operator를 통한 쿼리를 할 수 있기 때문에 유용합니다. (자세한 내용은 &lt;a href=&quot;https://www.postgresql.org/docs/14/datatype-json.html&quot;&gt;공식문서&lt;/a&gt;를 참고해주세요)&lt;/p&gt;

&lt;p&gt;저희는 그래서 주문서에 대한 스냅숏을 JSONB 컬럼 타입에 저장하도록 아래와 같이 주문서 이력 Entity를 구성하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetHistory&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;order_sheet_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;jsonb&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;snapshot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetSnapshot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Enumerated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;EnumType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;STRING&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;actionType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetHistoryActionType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Enumerated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;EnumType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;STRING&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;userType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetHistoryUserType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nullable&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;userId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BaseEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그런다음 주문서의 상태가 변경될 때마다 주문서 이력이 새롭게 추가되도록 하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToMany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;orderSheet&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PERSIST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MERGE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;_histories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheetHistory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableListOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;histories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheetHistory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_histories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sortedByDescending&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;createdAt&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;accept&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendorAccountId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_state&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CANCELED&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;IllegalStateException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;취소된 주문서는 마감할 수 없습니다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;_state&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ACCEPTED&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;_histories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;OrderSheetHistory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;actionType&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetHistoryActionType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ACCEPTED&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;userType&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetHistoryUserType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;VENDOR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderableVendorAccountId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;cancel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;managerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_state&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ACCEPTED&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;IllegalStateException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;마감된 주문서는 취소할 수 없습니다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;_state&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CANCELED&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;_histories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;OrderSheetHistory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;actionType&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetHistoryActionType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CANCELED&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;userType&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetHistoryUserType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;STORE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;managerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UpdateOrderSheetDomainData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;_requestedDeliveryDate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;requestedDeliveryDate&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;_additionalRequests&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;additionalRequests&lt;/span&gt;

        &lt;span class=&quot;nf&quot;&gt;replaceProducts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;_histories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;OrderSheetHistory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;actionType&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetHistoryActionType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UPDATE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;userType&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updateUserType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updateUserId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;_updatedAt&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OffsetDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;주문서의-도메인-이벤트-처리&quot;&gt;주문서의 도메인 이벤트 처리&lt;/h2&gt;

&lt;p&gt;주문서 도메인을 개발하면서 주문서의 생성과 상태 변경 또는 수정과 같이 주요 논리를 수행한 후 추가로 보조 논리를 수행해야 하는 요구사항이 있었습니다. 그래서 아래와 같이 처음에 작성하였는데요.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheetRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;chatClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ChatClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;slackMessageClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MessageClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;auditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetAuditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CreateOrderSheetData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 주문서 생성 (주요 논리)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;주문서&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;로직&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;savedOrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderSheetRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 주문 채팅 메시지 전송 (보조 논리)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;chatMessage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;메시지&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;로직&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;chatClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sendUserOrderMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chatMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 슬렉 알림 (보조 논리)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;slackMessage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;메시지&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;로직&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;slackMessageClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slackMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;savedOrderSheet&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같이 코드를 작성하는 경우 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OrderSheetService&lt;/code&gt;의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; 함수는 주요 논리 외에 보조 논리로 인해서 그 책임이 점점 커지는 문제가 발생하게 됩니다. 책임이 커지면 일단 먼저 &lt;a href=&quot;https://en.wikipedia.org/wiki/Single-responsibility_principle&quot;&gt;단일책임원칙(SRP)&lt;/a&gt;이 위배되는 것은 물론이고 가독성이 떨어지고 수정이 힘들어지니 유지보수성이 떨어질 것이며 의존성이 점점 추가되니 테스트하기에도 어려워지는 그런 거대한 공룡 함수가 탄생하게 되는 것입니다.&lt;/p&gt;

&lt;p&gt;그럼 위와 같은 문제를 해결하면서 주요 논리와 보조 논리들을 모두 수행할 방법은 무엇이 있을까요? 바로 이벤트를 활용하는 것입니다. 도메인이 주요 논리를 수행하고 난 후 도메인 이벤트를 발행하게 되고 그 이벤트를 구독하는 이벤트 처리기들이 이벤트를 수신받아서 각자가 가진 보조 논리를 처리하는 방법입니다. Spring에서는 이러한 구현을 더욱 손쉽게 할 수 있도록 &lt;a href=&quot;https://www.baeldung.com/spring-events&quot;&gt;Events 메커니즘&lt;/a&gt;을 제공해 줍니다. 그래서 아래와 같이 코드를 변경하여 구현하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheetRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;eventPublisher&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ApplicationEventPublisher&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;auditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetAuditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CreateOrderSheetData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;주문서&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;로직&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;savedOrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderSheetRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheetSubmittedEvent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;이벤트&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;로직&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;eventPublisher&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;publishEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheetSubmittedEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;savedOrderSheet&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackMessageEventHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;slackMessageClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MessageClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Async&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;readOnly&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@EventListener&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetSubmittedEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;메시지&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;로직&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;slackMessageClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetChatHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;chatClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ChatClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Async&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;readOnly&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@TransactionalEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheetSubmittedEvent&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetSubmittedEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;메시지&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;생성&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;로직&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;chatClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sendUserOrderMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같이 코드를 변경하게 되면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OrderSheetService&lt;/code&gt;는 더 이상 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SlackMessageClient&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ChatClient&lt;/code&gt;와 같이 보조 논리에 의해 필요한 의존성을 추가하지 않아도 됩니다. 그리고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; 함수는 이제 주요 논리인 주문서를 생성하는 논리와 이벤트를 발행하는 논리 외에는 다른 코드가 존재하지 않으므로 단순하고 간결한 코드를 유지할 수 있습니다. 또한 새로운 보조 논리가 생성된다면 새로운 이벤트 처리기를 추가해서 기능을 확장하면 되기 때문에 훨씬 더 유연하게 기능을 확장할 수 있다는 장점이 있습니다.&lt;/p&gt;

&lt;h1 id=&quot;이슈&quot;&gt;이슈&lt;/h1&gt;

&lt;p&gt;충분히 고민하고 설계했다고 생각하였고, 테스트 케이스도 꼼꼼하게 고민해서 테스트 코드를 작성하였다고 생각하였지만, 이슈가 발생하는 것은 어쩔 수 없나 봅니다.&lt;/p&gt;

&lt;p&gt;프로젝트 기간 중 발견되었던 이슈들과 그 이슈들을 어떻게 해결했는지 소개해 드리겠습니다.&lt;/p&gt;

&lt;h2 id=&quot;jpa의-1차-캐싱&quot;&gt;JPA의 1차 캐싱&lt;/h2&gt;

&lt;p&gt;QA 기간 중 아래와 같이 버그 이슈가 생성되었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/order-sheet-development-story/issue-1.png&quot; alt=&quot;issue-1&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위에서 우려했던 동시성 이슈였습니다. 분명 코드로 보았을 때 문제가 발생하지 않으리라 판단했기에 코드 리뷰에서도 별다른 이견 없이 코드가 병합되었었습니다. 하지만 우려했던 상황은 발생하였고 저희는 이에 대응하기 위해 이슈를 확인하기 시작하였고 그 원인은 JPA의 1차 캐싱이 원인이라는 것을 발견하게 되었습니다.&lt;/p&gt;

&lt;p&gt;JPA는 네트워크를 통해서 데이터베이스에 접근하는 비용을 최소한으로 하기 위해서 캐싱을 사용합니다. 1차 캐시는 이런 비용을 줄이기 위해서 하나의 트랜잭션 내에서 Entity를 조회하여 영속 컨텍스트에 로드하면 트랜잭션이 종료될 때까지 로드한 Entity를 재사용합니다. 데이터베이스의 격리 수준에서 Repeatable Read와 비슷한 메커니즘이라고 생각하시면 됩니다.&lt;/p&gt;

&lt;p&gt;다시 앞서 동시성 문제를 방지하기 위한 코드를 가져와 보겠습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;auditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetAuditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UpdateOrderSheetData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;auditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getByIdAndRevision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheetId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toDomainData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;auditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;userType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;auditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getByIdAndRevision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderSheetRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findByIdUsingPessimisticLock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;orElseThrow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ErrorMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NotFound&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ORDER_SHEET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ConcurrentModificationException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;주문서의 revision이 일치하지 않습니다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 코드만 보면 하나의 트랜잭션 내에서 한 번만 조회할 것이고 잠금을 통해서 조회하기 때문에 버그 티켓에서 적힌 현상이 발견되지 않으리라 생각됩니다. 하지만 문제는 서비스에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; 함수를 쓰기 이전에 발생하게 됩니다. 바로 DataFetcher 클래스(REST API라면 Controller 클래스로 이해하시면 됩니다.)에서 주문서의 수정 및 취소, 마감 처리하기 전에 자신의 주문서인지를 확인하는 논리 때문인데요. 이때 조회한 주문서가 1차 캐싱이 되면서 서비스에서 조회할 때는 새롭게 주문서를 조회하는 것이 아니라 캐시 된 주문서를 조회하면서 발생한 것이었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@DgsComponent&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetMutationDataFetcher&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheetFacade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetFacade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@DgsMutation&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Secured&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MANAGER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;cancelOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nd&quot;&gt;@InputArgument&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CancelOrderSheetInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DataFetcherResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CancelOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderSheetFacade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getOrderSheetById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(!&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;haveAccessToOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AccessDeniedException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ErrorMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NotFound&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ORDER_SHEET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CancelOrderSheetConverter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;orderSheetFacade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cancelOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;OrderSheetAuditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PrincipalProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;managerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;nc&quot;&gt;OrderSheetHistoryUserType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;STORE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;nc&quot;&gt;PrincipalProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;managerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@DgsMutation&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Secured&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OWNER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MANAGER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ORDERABLE_VENDOR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;updateOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nd&quot;&gt;@InputArgument&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UpdateOrderSheetInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DataFetcherResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UpdateOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderSheetFacade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getOrderSheetById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(!&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;haveAccessToOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AccessDeniedException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ErrorMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NotFound&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ORDER_SHEET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UpdateOrderSheetConverter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;orderSheetFacade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;updateOrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;OrderSheetAuditableData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;updateUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;updateUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;여기서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OrderSheetMutationDataFetcher&lt;/code&gt;는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Transactional&lt;/code&gt;을 사용하지 않는데 왜 1차 캐시가 적용되는가에 대한 의문이 드시는 부분이 있을 텐데요. 그 이유는 OSIV(Open Session In View)가 켜져 있기 때문입니다. (OSIV에 대한 자세한 내용은 &lt;a href=&quot;https://stackoverflow.com/questions/1103363/why-is-hibernate-open-session-in-view-considered-a-bad-practice#:~:text=Open%20Session%20In%20View%20takes,can%20trigger%20the%20Proxy%20initialization.&quot;&gt;여기&lt;/a&gt;를 참고해주세요) OSIV를 켜두면 HTTP 요청의 시작부터 끝까지 같은 영속성 컨텍스트를 유지합니다. 그래서 1차 캐시가 트랜잭션 범위 밖에 있는 조회도 캐싱 처리한 것이었습니다.&lt;/p&gt;

&lt;p&gt;API가 OSIV를 켜두는 것은 데이터베이스의 세션이 오래 유지되기 때문에 위험할 수 있습니다. 다만 OSIV를 활용하면 지연 로딩을 적극적으로 활용할 수 있고 현재 저희가 사용하고 있는 Graphql의 Field Resolver에서 지연 로딩을 활용하면 불필요한 쿼리를 호출하지 않도록 최적화할 수 있기 때문에 OSIV를 켜두는 선택을 하였습니다.&lt;/p&gt;

&lt;p&gt;그럼 이 이슈를 어떻게 해결할 수 있을까요? 저희는 2가지 안을 생각했습니다.&lt;/p&gt;

&lt;h3 id=&quot;dynamicupdate-이용&quot;&gt;DynamicUpdate 이용&lt;/h3&gt;

&lt;p&gt;JPA에서 Entity 변경을 저장할 때 기본적으로는 아래와 같이 변경 사항이 발생해도 모든 컬럼값에 대해 변경 요청 쿼리를 실행시킵니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PersonService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;personRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PersonRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;gettingOld&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;person&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;orElseThrow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;age&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;person&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=?&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;하지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@DynamicUpdate&lt;/code&gt;를 선언해주면 아래와 같이 변경 사항에 대한 수정 쿼리가 실행됩니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@DynamicUpdate&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nd&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PersonService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;personRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PersonRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;gettingOld&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;person&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;orElseThrow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;age&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;person&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=?&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이와같이 변경사항만 수정하는 쿼리가 실행된다면 위 버그 티켓에서 발생하는 현상인 주문서의 수정과 동시에 주문서를 마감하는 경우 주문서의 수정이 주문서의 마감에는 영향을 미치지 않도록 할 수 있습니다. 왜냐하면 주문서의 수정은 주문서의 주문상태의 변화는 일으키지 않고 주문서의 품목정보와 그 외 정보들만 수정하고 주문서의 마감은 주문서의 주문상태만 변경하기 때문입니다.&lt;/p&gt;

&lt;p&gt;하지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DynamicUpdate&lt;/code&gt;는 현재 가진 문제를 해결하는 것처럼 보일 순 있어도 근본적으로 동시에 수정요청에 대한 원치않는 값의 변경문제는 그대로 가지고 있기 때문에 채택하기 어려웠습니다. 그리고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DynamicUpdate&lt;/code&gt; 설정은 JPA가 Entity의 변경을 감지하고 저장할 때 변경사항을 계산해서 동적으로 쿼리를 생성해 주기때문에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DynamicUpdate&lt;/code&gt; 설정없이 Entity 수정 쿼리를 수행하는것보다 수행속도가 떨어진다는 문제점도 함께 가지고 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;1차-캐시를-사용하지-않는-방법&quot;&gt;1차 캐시를 사용하지 않는 방법&lt;/h3&gt;

&lt;p&gt;위 방법을 사용할 수 없다면 다음으로 시도해볼 수 있는 것은 1차 캐시를 사용하지 않는 방법입니다. 사실 Primary Key인 ID를 이용하여 레코드를 조회하는 것은 데이터베이스 입장에서는 비용이 큰 쿼리라고 보기 힘듭니다. 비록 어플리케이션 내에서 캐싱된 데이터를 조회하는 것이 성능적인 측면에서는 훨씬 이득이 많겠지만 지금과 같이 예외적인 상황에서는 오히려 캐시는 독이될 수 있습니다. (&lt;a href=&quot;https://medium.com/box-tech-blog/cache-is-the-root-of-all-evil-e64ebd7cbd3b&quot;&gt;캐시는 모든 악의 근원&lt;/a&gt;이라는 글을 한번 읽어보시면 좋을 것 같습니다.)&lt;/p&gt;

&lt;p&gt;그래서 저희는 주문서를 조회할때 필요한경우 1차 캐시를 사용하지 않도록 하는 기능을 추가하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Q&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AbstractJPAQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Q&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;AbstractJPAQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Q&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pessimisticWriteLocked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Q&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setLockMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LockModeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PESSIMISTIC_WRITE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setHint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;org.hibernate.cacheable&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// &amp;lt;----------------------- 여기&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setHint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;javax.persistence.lock.timeout&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;해당 함수를 Repository에서는 아래와 같이 사용합니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;QOrderSheetRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByIdUsingPessimisticLock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;QOrderSheetRepositoryImpl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;jpaQueryFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JPAQueryFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;QOrderSheetRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;QuerydslRepositorySupport&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;java&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findByIdUsingPessimisticLock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ofNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;jpaQueryFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;selectFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pessimisticWriteLocked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetchFirst&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다시 서비스 코드를 보면 서비스의 코드는 변경된 것이 없지만 더이상 JPA의 1차 캐시를 사용하지 않게 된 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getByIdAndRevision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderSheetRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findByIdUsingPessimisticLock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;orElseThrow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ErrorMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;NotFound&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ORDER_SHEET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ConcurrentModificationException&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;주문서의 revision이 일치하지 않습니다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;해당 코드를 적용 후 APM을 통해서 실제 쿼리가 어떻게 전송되는지 확인해보았습니다.&lt;/p&gt;

&lt;p&gt;이전에는 아래와 같이 한 번의 쿼리가 실행되었지만&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/order-sheet-development-story/as-is-query.png&quot; alt=&quot;as-is-query&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;order_sheet&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ordersheet0_&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ordersheet0_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;변경 코드를 적용하고 난 후 아래와 같이 2번의 쿼리가 실행되는 것을 볼 수 있습니다. 2번째에는 비관적 잠금을 사용하였기에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for update&lt;/code&gt;가 쿼리에 적혀 있는 것을 볼 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/order-sheet-development-story/to-be-query.png&quot; alt=&quot;to-be-query&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;order_sheet&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ordersheet0_&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ordersheet0_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=?&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;order_sheet&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ordersheet0_&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ordersheet0_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=?&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ordersheet0_&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;주문-품목의-버전관리&quot;&gt;주문 품목의 버전관리&lt;/h2&gt;

&lt;p&gt;최초 주문서의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;을 생성하는 방법은 주문서의 속성들과 주문서에 적힌 품목들의 속성들을 이용하여 생성하는 것이었습니다. 그래서 주문서의 속성값이 변경되면 주문서 조회 시 이전과는 다른 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt; 정보를 반환하도록 구성할 수 있었습니다. 하지만 동일한 내용으로 주문서 수정을 요청하면 주문서가 가진 상태들이 변경되지 않았기 때문에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt; 정보는 변경되지 않아야 함에도 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;이 변경되는 이슈를 발견하게 되었습니다. 원인을 파악해보니 바로 주문서가 가진 품목정보가 원인이었는데요.&lt;/p&gt;

&lt;p&gt;저희는 주문서에서 주문서는 Entity로 주문서가 가진 품목들을 Value Object로 생각하고 도메인 모델을 구성하였습니다. Entity와 Value Object의 차이를 간단하게 먼저 설명하고 이야기를 이어 나가겠습니다.&lt;/p&gt;

&lt;h3 id=&quot;entity&quot;&gt;Entity&lt;/h3&gt;

&lt;p&gt;우리말로 참조 객체라고 해석되기도 하며 식별성과 연속성을 가진 객체를 말합니다.&lt;/p&gt;

&lt;h4 id=&quot;식별성&quot;&gt;식별성&lt;/h4&gt;

&lt;p&gt;식별성을 가진 객체란 식별자를 가진 객체라고 할 수 있는데요. 사람을 예로 들면 주민등록 번호가 통장을 예로 들면 통장번호가 대표적인 식별자라고 할 수 있을 것입니다.&lt;/p&gt;

&lt;h4 id=&quot;연속성&quot;&gt;연속성&lt;/h4&gt;

&lt;p&gt;Entity는 자신의 생명주기 동안에 형태와 내용이 변경될 수 있습니다. 사람을 예로 들면 이름이나 성별(?), 출생지 등은 생애주기 동안에 언제든지 바뀔 수 있습니다. 하지만 주민등록 번호는 한번 생성되고 나면 변경되지 않습니다. 여기에서 이름이나 성별, 출생지 등이 변경되어도 동일한 사람임을 추적할 수 있는 성질이 연속성입니다.&lt;/p&gt;

&lt;h3 id=&quot;value-object&quot;&gt;Value Object&lt;/h3&gt;

&lt;p&gt;우리말로 번역하면 값 객체라고 부를 수 있을 것 같습니다. 개념적인 식별성이 없이 도메인의 서술적 측면만을 나타내는 객체를 이야기합니다. 이렇게 정의하면 너무 어려운 것 같아 &lt;a href=&quot;http://www.yes24.com/Product/Goods/91167539&quot;&gt;오브젝트 디자인 스타일 가이드&lt;/a&gt;에 적힌 글을 인용해 보겠습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;한 아이가 그림을 그릴 때 그 아이는 자기가 고른 펜의 색깔과 펜촉의 두께에 관심이 있을지도 모른다. 그러나 색과 모양이 같은 펜이 두 자루 있다면 아이는 아마 둘 중 어느 것을 사용하고 있는지 신경 쓰지 않을 것이다. 펜을 잃어버려서 펜 꾸러미에서 색깔이 같은 펜을 꺼내 바꿔놓더라도 아이는 펜이 바뀌었는지는 개의치 않고 계속해서 그림을 그릴 것이다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;위인용에서 볼 수 있듯이 Value Object는 객체의 식별성은 중요하지 않고 속성의 값이 중요합니다. 속성의 값이 동일하다면 동일한 객체로 인지하는 것입니다. 그래서 대부분의 경우 Value Object는 식별자가 필요하지 않습니다.&lt;/p&gt;

&lt;p&gt;다시 이야기로 돌아오면 주문서는 Entity로 주문서의 품목들은 Value Object로 판단했기 때문에 주문서의 품목은 ID는 중요하지 않고 그 값들이 중요합니다. 그래서 주문서를 수정할 때 품목정보의 변경을 더욱 단순하게 처리하기 위해 모든 품목을 지우고 수정요청이 들어온 값으로 새롭게 넣어주도록 로직을 작성하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToMany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;orderSheet&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ALL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orphanRemoval&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;_products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheetProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;arrayListOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheetProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;toList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;replaceProducts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dataList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ReplaceOrderSheetProductData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;_products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;products&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dataList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;OrderSheetProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;standard&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;standard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;unit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;orderableVendorProductId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderableVendorProductId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UpdateOrderSheetDomainData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;_requestedDeliveryDate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;requestedDeliveryDate&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;_additionalRequests&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;additionalRequests&lt;/span&gt;

        &lt;span class=&quot;nf&quot;&gt;replaceProducts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;_histories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;OrderSheetHistory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;orderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;actionType&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetHistoryActionType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;UPDATE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;userType&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updateUserType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updateUserId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;_updatedAt&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OffsetDateTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 로직을 구성하여도 문제없다고 판단한 이유는 주문서의 품목들은 ID가 중요하지 않고 값이 동일하다면 동일한 품목정보로 인식하기 때문에 수정 시마다 품목 정보를 모두 지우고 새롭게 생성하여 ID가 바뀌게 되더라도 문제가 없다고 판단했기 때문입니다. 만약 이렇게 구성하지 않고 기존 데이터와 새롭게 추가된 데이터를 비교해서 수정할 품목은 수정하고 삭제할 품목은 삭제하고 추가할 품목은 삭제하도록 코드를 작성한다면 위와 같이 간결한 코드는 작성하기 힘들 것입니다. 그렇다고 성능적으로 엄청나게 뛰어나게 이점을 가져가지도 않을 것이라고도 생각했습니다.&lt;/p&gt;

&lt;p&gt;다만 이로인해 이슈가 발생하였습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;을 생성할 때 주문서와 품목정보의 프로퍼티들을 이용한다고 말씀드렸었는데요.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requestedDeliveryDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;additionalRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderableVendorId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sumOf&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;


&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetProduct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;standard&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderableVendorProductId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 코드에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OrderSheetProduct&lt;/code&gt;의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;을 생성할 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; 프로퍼티가 활용되는 것을 볼 수 있습니다. 하지만 앞에서 말씀드렸다시피 주문서의 수정 시 품목정보는 모두 지운 후 수정요청을 받은 품목정보로 새롭게 다시 등록한다고 말씀드렸었습니다. 그래서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt;가 매 수정 시마다 변경되게 되므로 주문서의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;은 변경 사항이 없이 수정하더라도 변경되게 된 것입니다.&lt;/p&gt;

&lt;p&gt;그래서 변경 사항이 없이 수정하는 경우 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;을 생성할 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; 프로퍼티는 활용하지 않도록 변경함으로써 해당 이슈는 해결하게 됩니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetProduct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;standard&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderableVendorProductId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;hashcode&quot;&gt;hashCode&lt;/h2&gt;

&lt;p&gt;앞서 주문서의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;을 생성하는 코드를 보셔서 아시겠지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;의 정보를 생성할 때 hashCode를 사용하였습니다. Java에서 hashCode는 객체를 식별하는 하나의 정숫값으로 주로 HashSet, HashMap, HashTable 등에서 객체의 값에 대한 동등성 비교 시 많이 활용됩니다.&lt;/p&gt;

&lt;h3 id=&quot;hashcode를-사용한-이유&quot;&gt;hashCode를 사용한 이유&lt;/h3&gt;

&lt;p&gt;hashCode를 사용한 이유는 다음과 같습니다.&lt;/p&gt;

&lt;h4 id=&quot;고정된-크기&quot;&gt;고정된 크기&lt;/h4&gt;

&lt;p&gt;먼저 hashCode는 해시 알고리즘을 사용하게 깨문 임의 데이터를 고정된 크기의 데이터로 변환할 수 있기 때문에 주문서 정보의 양에는 상관없이 고정된 크기의 리비전 값을 반환할 수 있을 것이라 기대했습니다.&lt;/p&gt;

&lt;h4 id=&quot;내장-함수&quot;&gt;내장 함수&lt;/h4&gt;

&lt;p&gt;hashCode는 언어에서 내장된 함수이기 때문에 라이브러리 의존성은 Entity가 추가로 가지지 않아도 되어 복잡성을 줄일 수 있으리라 생각했습니다.&lt;/p&gt;

&lt;h4 id=&quot;동등성-보장&quot;&gt;동등성 보장&lt;/h4&gt;

&lt;p&gt;앞서 말한 바와 같이 hashCode는 HashSet, HashMap, HashTable 등에서 객체의 값에 대한 동등성 비교 시 활용되고 있습니다. 그렇기 때문에 같은 속성을 가진 주문서라면 동일함을 보장해 줄 것이라 기대했습니다.&lt;/p&gt;

&lt;p&gt;그래서 아래와 같이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;을 구현하였고 테스트 코드에서도 실제 QA 서버에서 배포하여 QA 진행 중에도 문제없이 잘 동작하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requestedDeliveryDate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;additionalRequests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderableVendorId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sumOf&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;revision&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;


&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheetProduct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;standard&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderableVendorProductId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;하지만 어느 날 프론트앤드 개발자분께서 개발 서버의 주문서의 수정을 수행할 수 없다는 이슈를 제기해 주셨습니다. 코드에 변경하지 않았기 때문에 QA에도 문제가 없어서 혹시 연동 시 문제가 발생하였는지 확인을 하던 중 동일한 주문서 ID로 조회하는데 조회 시마다 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt; 값이 바뀌어서 조회되는 것을 확인하게 되었습니다. 당시에 신기하다고 생각했던 건 2개의 값이 서로 번갈아 가면서 조회되는 것이었습니다. 원인을 찾던 중 문득 들었던 생각은 최근 개발 서버를 1대에서 2대로 올렸다는 것을 인지하게 되었습니다. 그래서 테스트를 위해 개발 서버를 다시 1대로 줄여보았고 이전과 같이 주문서를 정상적으로 수정할 수 있게 되었다는 것을 확인하게 되면서 결국 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt; 정보가 동일한 주문서임에도 불구하고 서버마다 다른 값을 반환하고 있다는 것을 증명할 수 있게 되었습니다.&lt;/p&gt;

&lt;h3 id=&quot;원인은-무엇이었을까요&quot;&gt;원인은 무엇이었을까요?&lt;/h3&gt;

&lt;p&gt;솔직히 말씀드리면 완전히 서버마다 hashCode가 다르게 반환됨을 확인할 수 있는 코드나 문서를 찾지는 못하였습니다. 다만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Object.hashCode&lt;/code&gt;에 대한 &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html&quot;&gt;Oracle 문서&lt;/a&gt;에서 어느 정도 힌트는 얻을 수 있었습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;하나의 애플리케이션(서버)이 실행되는 동안 해당 어플리케이션 내에서 hashCode를 호출하는 것은 동등성을 보장해 주지만 다른 애플리케이션(서버)에서 실행된 hashCode까지는 동등성을 보장해 주지 않는다는 것입니다. 즉, hashCode를 구현한 객체마다 다를 수 있지만, 최상위 객체인 Object에서 정해 놓은 바와 같이 어플리케이션 간에 hashCode에 대한 동등성은 보장해 주지 않는다는 의미로 해석할 수 있을 것 같습니다.&lt;/p&gt;

&lt;h3 id=&quot;그래서-어떻게-해결하였나요&quot;&gt;그래서 어떻게 해결하였나요?&lt;/h3&gt;

&lt;p&gt;hashCode를 사용하지 않는다면 다른 해시알고리즘을 사용할 수는 있을 것입니다. 하지만 아쉽게도 다른 해시 알고리즘은 문자열 형태를 띠는 값이 많아 Int 타입을 가진 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;의 스키마 유형을 바꾸는 것은 원치 않았습니다. 그래서 내부 변경에 대한 정보를 반환하면서 Int 타입을 반환할 수 있는 값이 무엇일까 고민하였고 다행히 주문서 도메인이 수정사항이 생길 때마다 이력을 기록한다는 것을 기억하게 되었고 주문상태가 변경되거나 주문서를 수정할 때마다 이력이 새롭게 등록될 것이기 때문에 주문서의 이력 개수를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;으로 사용하면 좋겠다고 생각하게 되었습니다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderSheet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToMany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;fetch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FetchType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LAZY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;orderSheet&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PERSIST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MERGE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;_histories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheetHistory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mutableListOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;histories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderSheetHistory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_histories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sortedByDescending&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;createdAt&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;revision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_histories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;마치며&quot;&gt;마치며&lt;/h1&gt;

&lt;p&gt;한편 이런 간단한 고민거리와 기초적인 지식의 부족으로 인해 이슈가 발생하는 부분에 대해서 말씀드리는 것이 부끄럽기도 합니다. 하지만 저희 팀이 이러한 고민과 이슈를 겪으면서 한층 더 발전했다는 것에 의미를 두고 싶고 혹시나 이 글을 읽으시는 분 중에 저희와 같은 고민을 하거나 이슈를 겪으셨다면 조금이나마 도움이 되었으면 하는 바람이 있습니다. 앞으로도 저희 스포카에서 개발하면서 겪었던 여러 경험을 공유하며 함께 발전해 나가기 위해 더욱더 노력하도록 하겠습니다.&lt;/p&gt;

&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>스포카에서 Jira를 활용하여 프로젝트를 수행하는 방법</title>
      <link>https://spoqa.github.io/2022/06/15/how-to-use-jira-in-spoqa.html</link>
      <pubDate>Wed, 15 Jun 2022 00:00:00 +0000</pubDate>
      <author>남경호</author>
      <guid>/2022/06/15/how-to-use-jira-in-spoqa</guid>
      <description>&lt;p&gt;안녕하세요. 키친보드 팀의 백엔드 프로그래머 남경호입니다.&lt;/p&gt;

&lt;p&gt;팀원들과 원활한 협업을 위해서 많은 회사에서 협업 도구를 사용하고 있는데요. 스포카에서는 협업 도구로 Atlassian의 &lt;a href=&quot;https://www.atlassian.com/software/jira&quot; target=&quot;\_blank&quot;&gt;Jira Software&lt;/a&gt;를 사용하고 있습니다. 이 글은 사내에 프로젝트를 진행할 때 좀 더 원활하게 Jira를 활용 할 수 있도록 가이드를 작성한 문서를 옮겨적은 것입니다. 협업 도구를 활용하는 방법은 회사다마 다르고 정답이 따로 있지는 않다고 생각합니다. 스포카에서는 이렇게 활용하고 있구나~ 라는 느낌으로 읽어주시면 감사하겠습니다.&lt;/p&gt;

&lt;h1 id=&quot;프로젝트-운영방식-선정-배경&quot;&gt;프로젝트 운영방식 선정 배경&lt;/h1&gt;

&lt;p&gt;도도카트는 &lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%95%A0%EC%9E%90%EC%9D%BC_%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4_%EA%B0%9C%EB%B0%9C&quot; target=&quot;\_blank&quot;&gt;애자일 소프트웨어 개발 방법론&lt;/a&gt;에서 스크럼과 칸반을 혼합한 형태로 제품을 개발하고 있습니다.&lt;/p&gt;

&lt;p&gt;스크럼은 스프린트라고 하는 단기 작업 블록을 통해서 프로젝트를 진행하며 스프린트 기간은 보통 2~4주 사이로 하고 있지만 도도카트팀은 2주를 채택하고 있습니다. 스크럼의 가장 큰 특징은 스프린트 기간내에 계획된 작업 외에 다른 작업들은 진행중인 스프린트에 포함시키지 않고 오로지 계획된 이슈들만 작업을 진행한다는 것입니다.&lt;/p&gt;

&lt;p&gt;칸반은 백로그에 필요한 작업들을 쌓아두고 작업자가 작업할 수 있을 때 우선순위가 높은 작업을 할당하여 작업을 진행하는 방법입니다. 칸반의 가장 큰 특징은 작업의 우선순위가 높은 새로운 작업이 발생하는 경우(버그, 급한 운영 이슈 등) 진행중이던 작업을 잠시 멈추고 우선순위가 높은 작업을 먼저 진행할 수 있다는 것입니다.&lt;/p&gt;

&lt;p&gt;도도카트가 스크럼과 칸반을 혼합한 이유는 스크럼과 칸반의 장점을 모두 가져가기 위해서 입니다. 앞서 스크럼의 큰 특징이 스프린트 내 계획되지 않은 작업은 추가하지 않는 것입니다. 이는 정확한 목표 수립과 정확한 공수측정을 위해서 인데요, 운영중인 서비스가 있어 계획되지 않은 작업이 자주 발생하거나 명확한 목표를 정하기 어려운 상황에서는 이상적으로 스크럼방식을 수행하기 어렵습니다. 그래서 칸반처럼 계획되지 않은 작업도 스프린트에 포함 시키면서 목표한 작업을 계획해서 스프린트를 운영하는 혼합된 방식을 채택하게 되었습니다.&lt;/p&gt;

&lt;h1 id=&quot;용어&quot;&gt;용어&lt;/h1&gt;

&lt;p&gt;글을 이어가기 전에 먼저 용어부터 정의하고 넘어가면 좀더 이해가 쉬울것 같아 사용되는 용어들을 먼저 정리해 보겠습니다.&lt;/p&gt;

&lt;h2 id=&quot;프로젝트project&quot;&gt;프로젝트(Project)&lt;/h2&gt;

&lt;p&gt;프로젝트는 말그대로 JIRA에서 프로젝트 단위로 이슈를 관리하는 공간입니다. 각 프로젝트마다 독립된 설정값을 따르며 이슈카드의 ID도 다른 형태로 생성됩니다. 스포카에서는 제품단위로 나뉘기도 하고 제품개발이 아닌 업무별로 나누어서 관리하기도 합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/project.png&quot; alt=&quot;project&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;보드board&quot;&gt;보드(Board)&lt;/h2&gt;

&lt;p&gt;프로젝트내에 여러개의 보드를 운영할 수 있습니다. JIRA에서는 스크럼과 칸반 2개의 보드 스타일을 제공합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/board.png&quot; alt=&quot;board&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;칸반&quot;&gt;칸반&lt;/h3&gt;

&lt;p&gt;칸반형태로 보드를 생성한다면 아래 그림과 같이 메뉴에 Kanban board가 나타나게 되며 칸반형태로 이슈를 관리할 수 있도록 합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/kanban.png&quot; alt=&quot;kanban&quot; /&gt;&lt;/p&gt;

&lt;p&gt;칸반보드를 보면 생성된 이슈들을 칸반보드 형태로 나타나 있는 것을 볼 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/kanban-board.png&quot; alt=&quot;kanban-board&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;스크럼&quot;&gt;스크럼&lt;/h3&gt;

&lt;p&gt;스크럼 형태로 보드를 생성한다면 아래 그림과 같이 메뉴에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Backlog&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Active sprints&lt;/code&gt;가 나타나게 됩니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/scrum.png&quot; alt=&quot;scrum&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;backlog&quot;&gt;Backlog&lt;/h4&gt;

&lt;p&gt;백로그는 보드 내 현재 생성되어 있는 모든 이슈카드 목록을 보여줍니다. 현재 진행중인 스프린트와 앞으로 진행할 스프린트 그리고 백로그들을 보여주기 때문에 다음 스프린트 계획 시 활용하면 좋습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/backlog.png&quot; alt=&quot;backlog&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;active-sprints&quot;&gt;Active sprints&lt;/h4&gt;

&lt;p&gt;Active sprints는 말그대로 현재 진행중인 스프린트내 이슈들만 볼 수 있는 화면입니다. 백로그와 달리 칸반형태로 표시하기 때문에 각 이슈의 진행상황을 파악하기에 용이합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/active-sprints.png&quot; alt=&quot;active-sprints&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;이슈issue&quot;&gt;이슈(Issue)&lt;/h2&gt;

&lt;p&gt;보드 내 생성된 작업들을 이슈라고 합니다. 이슈는 여러가지 타입이 있으며 도도카트에서는 크게 Epic, Story, Task, Bug, Sub-Task를 기본적으로 사용합니다.&lt;/p&gt;

&lt;h3 id=&quot;에픽epic&quot;&gt;에픽(Epic)&lt;/h3&gt;

&lt;p&gt;에픽이란 여러번의 스프린트를 거쳐 완료되는 정도의 작업량을 가진 업무를 말합니다. 에픽의 범위는 회사마다 이슈관리 도구마다 다르게 정의할 수 있지만 도도카트팀에서는 스토리들을 묶은 상위 개념의 기능을 의미합니다. 에픽에서는 기능에 대한 정의만하지 상세한 기술은 스토리를 통해 설명합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/epic.png&quot; alt=&quot;epic&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;스토리story&quot;&gt;스토리(Story)&lt;/h3&gt;

&lt;p&gt;스토리는 비지니스 가치를 제공하는 최소 단위의 요구사항을 말합니다. 보통 사용자 입장에서 필요한 내용을 일상적인 언어로 기술합니다. 하나의 에픽에 여러개의 스토리가 있을 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/story.png&quot; alt=&quot;story&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;태스크task&quot;&gt;태스크(Task)&lt;/h3&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;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/task.png&quot; alt=&quot;task&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;버그bug&quot;&gt;버그(Bug)&lt;/h3&gt;

&lt;p&gt;버그는 주로 QA 시 요구사항을 충족하지 못하는 경우나 운영 중 발생한 이슈를 리포팅하기 위한 용도로 사용됩니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/bug.png&quot; alt=&quot;bug&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;서브-태스크sub-task&quot;&gt;서브 태스크(Sub-task)&lt;/h3&gt;

&lt;p&gt;서브 태스크는 하나의 태스크를 여러개의 세부작업으로 나눌 필요가 있는 경우 태스크의 하위 작업의 용도로 사용합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/sub-task.png&quot; alt=&quot;sub-task&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;컴포넌트component&quot;&gt;컴포넌트(Component)&lt;/h3&gt;

&lt;p&gt;컴포넌트는 에픽 또는 이슈들을 묶어주는 단위로써 해당 이슈의 목표점 또는 특징을 나타냅니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/component.png&quot; alt=&quot;component&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;시나리오-1-스프린트-관리&quot;&gt;시나리오 1: 스프린트 관리&lt;/h1&gt;

&lt;p&gt;스프린트를 진행하는 시나리오를 기반으로 Jira의 프로젝트 관리 방법을 알아보겠습니다.&lt;/p&gt;

&lt;h2 id=&quot;스프린트-계획&quot;&gt;스프린트 계획&lt;/h2&gt;

&lt;p&gt;스프린트 계획은 스프린트를 진행하기 전에 진행할 다음 스프린트 작업을 계획하는 활동입니다. 스프린트를 생성하고 기획문서를 바탕으로 관련된 이슈들을 생성하거나 이전에 수행하지 못했던 이슈들을 다음 스프린트로 이동하는 등의 작업을 진행합니다.&lt;/p&gt;

&lt;h3 id=&quot;스프린트-생성&quot;&gt;스프린트 생성&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Backlog&lt;/code&gt;메뉴에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Create sprint&lt;/code&gt;버튼을 클릭하여 스프린트를 생성합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/create-sprint.png&quot; alt=&quot;create-sprint&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;스프린트-설정&quot;&gt;스프린트 설정&lt;/h3&gt;

&lt;p&gt;생성된 스프린트의 이름과 소요일정을 설정합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-sprint.png&quot; alt=&quot;set-sprint&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;에픽-생성&quot;&gt;에픽 생성&lt;/h3&gt;

&lt;p&gt;PO/PM은 기획문서를 기반으로 신규 또는 변경되는 기능에 대한 에픽을 생성합니다. (Confluence 문서에서 에픽을 링크해두면 이슈카드에서도 컨플루언스 문서 링크를 함께 확인할 수 있습니다.)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/create-epic.png&quot; alt=&quot;create-epic&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/create-epic-2.png&quot; alt=&quot;create-epic-2&quot; /&gt;&lt;/p&gt;

&lt;p&gt;에픽은 Jira 상단의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Create&lt;/code&gt;버튼이나 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Backlog&lt;/code&gt;화면 내 EPICS의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Create epic&lt;/code&gt;버튼을 통해 생성할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/create-epic-3.png&quot; alt=&quot;create-epic-3&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;에픽-설정&quot;&gt;에픽 설정&lt;/h3&gt;

&lt;p&gt;생성된 에픽의 상세화면으로 이동하여 다음과 같이 설정을 추가해줍니다.&lt;/p&gt;

&lt;h4 id=&quot;라벨-설정&quot;&gt;라벨 설정&lt;/h4&gt;

&lt;p&gt;각 이슈들은 프로젝트내에서는 공유됩니다. 그래서 별도의 설정을 하지 않으면 보드내 모든 이슈들이 보이는 문제가 있습니다. 그래서 특정 보드에 원하는 이슈들만 보이도록 하기 위해서 라벨을 설정합니다. 아래는 도도카트에서 현재(Sep 23, 2021기준) 운영중인 라벨 목록입니다. (라벨은 하나 이상 설정이 가능합니다.)&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;코어 스쿼드: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core-squad&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;거래처 스쿼드: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tf-vendor-list&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-epic-set-labels.png&quot; alt=&quot;set-epic-set-labels&quot; /&gt;&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;/images/how-to-use-jira-in-spoqa/set-epic-set-component.png&quot; alt=&quot;set-epic-set-component&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;담당자-할당&quot;&gt;담당자 할당&lt;/h4&gt;

&lt;p&gt;주로 에픽은 PO/PM이 관리하기에 담당자는 PO/PM으로 할당합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-epic-set-assignee.png&quot; alt=&quot;set-epic-set-assignee&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;스토리-생성&quot;&gt;스토리 생성&lt;/h3&gt;

&lt;p&gt;PO/PM은 기획문서를 기반으로 신규 또는 변경되는 기능에 대한 에픽의 하위 스토리들을 생성합니다. (컨플루언스 문서에서 스토리를 링크해두면 이슈카드에서도 컨플루언스 문서 링크를 함께 확인할 수 있습니다.)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/create-story.png&quot; alt=&quot;create-story&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/create-story-2.png&quot; alt=&quot;create-story-2&quot; /&gt;&lt;/p&gt;

&lt;p&gt;스토리는 Jira 상단의 Create버튼이나 에픽내 Create issue in epic버튼을 통해 생성합니다. (개인적으로는 에픽 내에서 생성하면 자동으로 에픽과 연결을 시켜주기 때문에 에픽 내에서 생성하는 것을 추천합니다.)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/create-story-3.png&quot; alt=&quot;create-story-3&quot; /&gt;&lt;/p&gt;

&lt;p&gt;만약 Jira 상단의 Create버튼을 통해 스토리를 생성한 경우 Eplic을 연결시켜주어야 합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/create-story-link-epic.png&quot; alt=&quot;create-story-link-epic&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;스토리-설정&quot;&gt;스토리 설정&lt;/h3&gt;

&lt;p&gt;생성된 스토리의 상세화면으로 이동하여 다음과 같이 설정을 추가해줍니다.&lt;/p&gt;

&lt;h4 id=&quot;라벨-설정-1&quot;&gt;라벨 설정&lt;/h4&gt;

&lt;p&gt;에픽에서 설정했던 방식과 동일하게 라벨을 설정해줍니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;코어 스쿼드: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core-squad&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;거래처 스쿼드: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tf-vendor-list&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;담당자-할당-1&quot;&gt;담당자 할당&lt;/h4&gt;

&lt;p&gt;스토리도 에픽과 마찬가지로 PO/PM이 관리하기에 담당자는 PO/PM으로 할당합니다.&lt;/p&gt;

&lt;h4 id=&quot;스프린트-설정-1&quot;&gt;스프린트 설정&lt;/h4&gt;

&lt;p&gt;스프린트 설정은 백로그 화면에서 백로그 이슈를 해당 스프린트로 드래그 앤 드랍으로 이동하거나 스토리 상세 페이지에서 스프린트를 설정하는 방법으로 설정할 수 있습니다.&lt;/p&gt;

&lt;p&gt;스프린트 설정을 하지 않으면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Active sprints&lt;/code&gt;메뉴에서 이슈를 확인할 수 없으니 수행해야할 스프린트에 맞도록 스프린트 설정을 반드시 해주셔야 합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-story-set-sprint.gif&quot; alt=&quot;set-story-set-sprint&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-story-set-sprint-2.png&quot; alt=&quot;set-story-set-sprint-2&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;태스크-생성&quot;&gt;태스크 생성&lt;/h3&gt;

&lt;p&gt;프로그래머/디자이너는 스토리를 기반으로 작업할 태스크를 생성합니다.&lt;/p&gt;

&lt;p&gt;태스크는 Jira 상단의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Create&lt;/code&gt;버튼이나 에픽내 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Create issue in epic&lt;/code&gt;버튼을 통해 생성합니다. (스토리와 마찬가지로 에픽 내에서 생성하는 것을 추천합니다.)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/create-task.png&quot; alt=&quot;create-task&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/create-task-2.png&quot; alt=&quot;create-task-2&quot; /&gt;&lt;/p&gt;

&lt;p&gt;만약 Jira 상단의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Create&lt;/code&gt;버튼을 통해 스토리를 생성한 경우 Eplic을 연결시켜주어야 합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/create-story-link-epic.png&quot; alt=&quot;create-task-link-epic&quot; /&gt;&lt;/p&gt;

&lt;p&gt;만약 에픽이 없는 태스크인 경우 별도로 에픽 링크를 설정하지 않고 생성합니다. 보통 에픽이 없는 태스트인 경우는 스프린트에 포함되지 않는 운영 이슈인 경우가 많습니다.&lt;/p&gt;

&lt;h3 id=&quot;태스크-설정&quot;&gt;태스크 설정&lt;/h3&gt;

&lt;p&gt;생성된 태스크의 상세화면으로 이동하여 다음과 같이 설정을 추가해줍니다.&lt;/p&gt;

&lt;h4 id=&quot;스토리-링크-설정&quot;&gt;스토리 링크 설정&lt;/h4&gt;

&lt;p&gt;생성된 태스크와 관련한 스토리를 링크합니다. (에픽이 없거나 관련된 스토리가 없는 태스크의 경우 링크하지 않습니다.)&lt;/p&gt;

&lt;p&gt;Jira에서는 스토리와 Task가 동일한 작업레벨을 가집니다. (서브 태스크가 있긴 하지만 서브 태스크를 이용하여 작업관리를 하는 경우 작업 Estimate에 대한 리포팅이 제대로 되지 않는 등 불편한 점들이 많아 태스크로 작업 관리를 하는 것을 추천합니다.)&lt;/p&gt;

&lt;p&gt;그래서 태스크가 특정 스토리와 관련이 있다는 것을 알 수 있도록 하기 위해서 링크를 걸어줍니다. 링크를 걸어주는 작업이 다소 번거로울 수 있지만 이슈 추적에 매우 도움이 되니 링크를 걸어주는 것을 추천합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-task-link-story.png&quot; alt=&quot;set-task-link-story&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;라벨-설정-2&quot;&gt;라벨 설정&lt;/h4&gt;

&lt;p&gt;에픽에서 설정했던 방식과 동일하게 라벨을 설정해줍니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;코어 스쿼드: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core-squad&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;거래처 스쿼드: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tf-vendor-list&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;담당자-할당-2&quot;&gt;담당자 할당&lt;/h4&gt;

&lt;p&gt;태스크의 담당자는 주로 해당 작업을 수행할 본인 또는 실무자를 할당합니다.&lt;/p&gt;

&lt;h4 id=&quot;스프린트-설정-2&quot;&gt;스프린트 설정&lt;/h4&gt;

&lt;p&gt;스프린트 설정은 백로그 화면에서 백로그 이슈를 해당 스프린트로 드래그 앤 드랍으로 이동하거나 스토리 상세 페이지에서 스프린트를 설정하는 방법으로 설정할 수 있습니다.&lt;/p&gt;

&lt;p&gt;스프린트 설정을 하지 않으면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Active sprints&lt;/code&gt;메뉴에서 이슈를 확인할 수 없으니 수행해야할 스프린트에 맞도록 스프린트 설정을 반드시 해주셔야 합니다. (스프린트 이슈가 아닌 운영 이슈의 경우 스프린트를 설정하지 않습니다.)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-story-set-sprint.gif&quot; alt=&quot;set-story-set-sprint&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-story-set-sprint-2.png&quot; alt=&quot;set-story-set-sprint-2&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;estimate-설정&quot;&gt;Estimate 설정&lt;/h4&gt;

&lt;p&gt;Original estimate를 설정하여 예상되는 작업 일정을 기입합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-estimate-time.png&quot; alt=&quot;set-estimate-time&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;작업 시간 설정과 관련해서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Original Estimate&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Story Point&lt;/code&gt; 중에 선택을 해볼 수 있을 것 같습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Original Estimate&lt;/code&gt;를 사용하면 작업 시간에 대해 명확하게 표현할 순 있으나 실제로 작업시간을 명확하게 예상하긴 어려운 부분이 있습니다. 하지만 스토리 포인트를 활용하면 작업에 대한 난이도 및 투입 공수를 어림잡아 예상할 수 있고 작업에 대한 범위를 가시적으로 파악할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;start-date와-due-date-설정&quot;&gt;Start date와 Due date 설정&lt;/h4&gt;

&lt;p&gt;실제 작업을 시작하고 종료하는 날을 입력합니다. 해당 설정은 사내에서 사용중인 WBS에 표시를 위한 설정이기도 합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-start-end-date.png&quot; alt=&quot;set-start-end-date&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/wbs.png&quot; alt=&quot;wbs&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;스프린트-진행&quot;&gt;스프린트 진행&lt;/h2&gt;

&lt;p&gt;계획된 스프린트를 수행하는 단계입니다. 만약 계획되지 않은 태스크가 추가되면 위의 생성규칙을 통해 생성하고 진행합니다.&lt;/p&gt;

&lt;h3 id=&quot;스프린트-시작&quot;&gt;스프린트 시작&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Backlog&lt;/code&gt;화면에서 시작할 스프린트의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Start sprint&lt;/code&gt;버튼을 클릭하여 스프린트를 시작합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/start-sprint.png&quot; alt=&quot;start-sprint&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;이슈-상태-변경&quot;&gt;이슈 상태 변경&lt;/h3&gt;

&lt;p&gt;이슈가 할당된 담당자들은 본인이 할당된 이슈의 진행여부를 파악하셔 이슈상태를 업데이트 합니다.&lt;/p&gt;

&lt;h4 id=&quot;에픽-상태-변경&quot;&gt;에픽 상태 변경&lt;/h4&gt;

&lt;p&gt;에픽은 하위 이슈들이 진행중으로 변경되면 진행중으로 상태를 변경합니다. 에픽은 여러 스프린트를 걸쳐 수행될 수 있기 때문에 관련된 이슈들이 모두 종료 상태가 되면 종료상태로 변경해 줍니다.&lt;/p&gt;

&lt;p&gt;에픽이 종료되었다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Backlog&lt;/code&gt;메뉴의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EPICS&lt;/code&gt;에서 해당 에픽을 아래 이미지와 같이 최종적으로 완료처리하여 에픽 목록에 더이상 표시가 되지 않도록 합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/done-epic.png&quot; alt=&quot;done-epic&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;스토리-상태-변경&quot;&gt;스토리 상태 변경&lt;/h4&gt;

&lt;p&gt;스토리는 관련된 태스크가 진행중으로 변경되면 진행중 상태로 변경합니다. 그리고 QA에 입고된다면 해결됨 상태로 변경하고 QA가 완료되고 작업이 종료된다면 종료상태로 변경합니다.&lt;/p&gt;

&lt;h4 id=&quot;태스크-상태-변경&quot;&gt;태스크 상태 변경&lt;/h4&gt;

&lt;p&gt;태스크는 작업자가 진행할 때 진행중으로 변경합니다. 프로그래머의 경우 Pull Request를 통한 코드리뷰 진행 시 리뷰 상태로 변경하고 코드가 병합이 된다면 종료상태로 변경합니다. 그 외 작업자들도 상황에 따라 리뷰 상태를 거쳐 종료상태로 변경하는 프로세스를 진행하여도 무방합니다.&lt;/p&gt;

&lt;p&gt;만약 리포터가 자기 자신 또는 작업 담당자가 아니라 다른 사람이라면 작업이 완료되고 해결됨 상태를 둔 후 리포터에게 완료여부를 요청할 수 있습니다. 이때 리포터는 작업완료를 확인 한 후 이슈를 종료할 수 있습니다. 해당 프로세스는 운영이슈에서 주로 사용됩니다.&lt;/p&gt;

&lt;h3 id=&quot;버그-이슈-처리&quot;&gt;버그 이슈 처리&lt;/h3&gt;

&lt;p&gt;QA 또는 운영 이슈 발생 시 버그 이슈가 생성될 수 있습니다. 버그 리포트가 생성되면 작업자는 위 태스크 생성방법을 통해서 태스크를 생성하고 버그 이슈를 링크해 줍니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/link-bug.png&quot; alt=&quot;link-bug&quot; /&gt;&lt;/p&gt;

&lt;p&gt;그리고 작업이 완료되면 태스크는 종료상태로 두고 버그 이슈는 해결된 상태로 변경합니다. (리포터에게 넛지를 주면 더 좋겠죠?)&lt;/p&gt;

&lt;p&gt;리포터는 해결됨이 확인되면 버그 이슈를 종료처리합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/bug-comment.png&quot; alt=&quot;bug-comment&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;스프린트-종료&quot;&gt;스프린트 종료&lt;/h2&gt;

&lt;p&gt;QA가 종료되고 작업된 코드를 배포하면서 진행중인 스프린트를 종료합니다. 종료 방법은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Active sprints&lt;/code&gt;메뉴에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Complete sprint&lt;/code&gt;버튼을 눌러 종료합니다. (만약 여러개의 스프린트가 진행중이라면 Select box에서 종료하고자 하는 스프린트를 선택한 후 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Complete sprint&lt;/code&gt;버튼을 누릅니다.)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/complete-sprint.png&quot; alt=&quot;complete-sprint&quot; /&gt;&lt;/p&gt;

&lt;p&gt;주의해야할 부분은 스프린트 종료 시 반드시 해당 스프린트 내 모든 이슈(서브 태스크 포함)는 종료 상태여야 합니다. 만약 종료되지 않은 이슈가 있다면 아래와 같이 경고 문구가 표시되고 종료되지 않습니다. 다만 종료되지 않은 이슈들은 다음 스프린트나 백로그로 이동시킬 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/complete-sprint-check-issue.png&quot; alt=&quot;complete-sprint-check-issue&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;리포트&quot;&gt;리포트&lt;/h2&gt;

&lt;p&gt;스프린트가 종료되고 난 후 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Reports&lt;/code&gt;메뉴를 통해 지난 스프린드 진행 결과를 분석할 수 있습니다. 이슈 생성 시 Estimate를 잘적어주고 카드의 상태관리를 제때 해주었다면 의미있는 보고서 결과를 얻을 수 있습니다. 모든 보고서를 다 이해하고 있지 않기 때문에 다루면 좋을 법한 보고서 들만 적어보겠습니다.&lt;/p&gt;

&lt;p&gt;아래에 보여주는 차트들은 동일한 스프린트를 다른 보고서 형태로 표시한 것입니다.&lt;/p&gt;

&lt;h3 id=&quot;burndown-chart&quot;&gt;Burndown Chart&lt;/h3&gt;

&lt;p&gt;스프린트 기간동안에 계획된 작업 잔여량이 시간이 지남에 따라 어떻게 변화하는지 보여주는 차트입니다. 목표 기간내에 작업이 완료되는 추세를 보여주기 때문에 계획된 작업에 대한 완료 여부를 예측하는 용도로 유용합니다.&lt;/p&gt;

&lt;p&gt;스프린트 기간 내 딱 정해진 작업만을 수행하는 이상적인 스크럼 환경에서 참고하면 유용한 보고서 입니다.&lt;/p&gt;

&lt;p&gt;도도카트에서는 칸반과 스크럼이 혼합된 형태이기 때문에 아래 번다운 차트에서 잔여량이 우하향만 하는 것이아니라 증가하는 추이를 함께 볼 수 있습니다. 그래서 도도카트 팀에서 참고하기에는 큰 인사이트를 얻기 힘들듯 합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/burndown-chart.png&quot; alt=&quot;burndown-chart&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;burnup-chart&quot;&gt;Burnup Chart&lt;/h3&gt;

&lt;p&gt;정해진 기간동안 새롭게 생성된 이슈들과 완료된 이슈들의 총량이 시간이 지남에 따라 어떻게 변화하는지 보여주는 차트입니다. 목표 기간내에 작업이 완료되는 추세를 볼 수 있다는 점에서는 번다운 차트와 비슷하지만 새롭게 생성되는 이슈들에 대한 추이도 함께 볼 수 있다는 점에서 번다운 차트와 조금 다른 인사이트를 제공합니다. 이 점으로 인해 진행중인 스프린트 기간내에 작업의 완료 여부를 예측함과 동시에 새롭게 생성되는 이슈의 추세를 봄으로써 스프린트 진행의 방해요소들을 파악할 수 있는 장점이 있습니다.&lt;/p&gt;

&lt;p&gt;도도카트와 같이 스크럼과 칸반을 혼합해서 프로젝트를 수행하는 경우 참고하면 좋을 보고서 입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/burnup-chart.png&quot; alt=&quot;burnup-chart&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;시나리오-2--운영-이슈-관리&quot;&gt;시나리오 2 : 운영 이슈 관리&lt;/h1&gt;

&lt;p&gt;운영 관리 관점에서 Jira의 이슈 관리 방법을 알아보겠습니다.&lt;/p&gt;

&lt;h2 id=&quot;운영-이슈-생성&quot;&gt;운영 이슈 생성&lt;/h2&gt;

&lt;p&gt;운영이슈는 운영 칸반에서 생성합니다. 별다른 생성공간이 없으므로 Jira 상단의 Create버튼을 통해 생성합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/create-task-in-kanban.png&quot; alt=&quot;create-task-in-kanban&quot; /&gt;&lt;/p&gt;

&lt;p&gt;생성 시 특별한 사유가 없으면 이슈 유형을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task&lt;/code&gt;로 설정합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-issue-type.png&quot; alt=&quot;set-issue-type&quot; /&gt;&lt;/p&gt;

&lt;p&gt;작업할 담당자를 할당해 준 후 라벨은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cart-request&lt;/code&gt;, 컴포넌트는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;운영 개선&lt;/code&gt;을 설정합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-lables-component.png&quot; alt=&quot;set-labels-component&quot; /&gt;&lt;/p&gt;

&lt;p&gt;요청 내용은 작업자가 작업내용을 쉽게 파악할 수 있도록 되도록 자세히 적어주시면 좋습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-task-content.png&quot; alt=&quot;set-task-content&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;운영-이슈-설정&quot;&gt;운영 이슈 설정&lt;/h2&gt;

&lt;h3 id=&quot;estimate-설정-1&quot;&gt;Estimate 설정&lt;/h3&gt;

&lt;p&gt;작업자는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Original estimate&lt;/code&gt;를 설정하여 예상되는 작업 일정을 기입합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-estimate-time.png&quot; alt=&quot;set-estimate-time&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;start-date와-due-date-설정-1&quot;&gt;Start date와 Due date 설정&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/set-start-end-date.png&quot; alt=&quot;set-start-end-date&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/how-to-use-jira-in-spoqa/wbs.png&quot; alt=&quot;wbs&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;상태-관리&quot;&gt;상태 관리&lt;/h2&gt;

&lt;p&gt;작업자는 할당된 작업을 진행할 때 작업을 진행중으로 변경합니다. 작업이 완료되면 해결됨 상태로 변경하고 리포터에게 작업완료를 알립니다. 리포터는 작업 완료여부를 확인한 후 해당 이슈를 종료처리합니다.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;지금까지 스포카에서 Jira를 활용하여 어떻게 제품을 개발해 나가는지 살펴보았습니다. 사실 이 글은 스포카가 정비기간을 가지기 이전까지 스크럼과 칸반을 혼합한 형태의 제품 개발 사이클에서 팀원들이 좀더 Jira를 잘 활용할 수 있도록 작성된 글이기 때문에 조금씩 사용방법 및 설정방법이 변한 상태입니다. 다만, 제품이 발전하면서 팀에서 프로젝트를 진행하는 방식도 끊임없이 변한다고 생각합니다. 그렇기에 앞으로도 스포카는 지속적인 피드백을 통해서 더 나은 제품을 서비스하기 위해서 더 개발 프로세스를 계속해서 발전시켜갈 예정입니다.&lt;/p&gt;

&lt;p&gt;모쪼록 이 글을 읽으시는 분들이 Jira활용에 조금이나마 도움이 되었길 바라며 글을 마무리 하겠습니다.&lt;/p&gt;

&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>테스트 자동화 구축 이야기</title>
      <link>https://spoqa.github.io/2022/05/24/test-automation-story.html</link>
      <pubDate>Tue, 24 May 2022 00:00:00 +0000</pubDate>
      <author>염주일</author>
      <guid>/2022/05/24/test-automation-story</guid>
      <description>&lt;p&gt;안녕하세요. 스포카 QA Leader 염주일입니다.&lt;/p&gt;

&lt;p&gt;스포카 QA 팀에서 오픈소스 자동화 도구인 Appium을 사용하여 Mobile App 테스트 자동화의 첫발을 내디뎠습니다.&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;ul&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;br /&gt;
일정 수준의 커버리지가 확보된, 일관되고 신뢰할 수 있는 테스트를 효율적으로 수행할 수 있는 이점이 있다고 볼 수 있겠습니다.&lt;/p&gt;

&lt;p&gt;이것만 보면 테스트 자동화를 하지 않을 이유가 없군요! &lt;br /&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;/ul&gt;

&lt;h3 id=&quot;뭐지&quot;&gt;뭐지?&lt;/h3&gt;

&lt;p&gt;테스트 자동화의 장점이 단점에 의해 일부 상쇄되는군요!&lt;/p&gt;

&lt;h3 id=&quot;왜-그럴까요&quot;&gt;왜 그럴까요?&lt;/h3&gt;

&lt;p&gt;장점을 기준으로 현실적인 이야기를 해 볼게요.&lt;br /&gt;
먼저 &lt;strong&gt;테스트 커버리지 확대&lt;/strong&gt;는 일부 맞는 말이긴 해요. 다만, 자동화만으로는 여러 가지 이유로 달성하기 힘들죠. 특히, 엣지 케이스와 일부 네거티브 테스트 케이스는 자동화로 구현하기가 상당히 어렵습니다. 비용도 많이 들고요.&lt;/p&gt;

&lt;p&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;/p&gt;

&lt;p&gt;이런 현상은 테스트 조건을 적절하게 변경하고 유지보수하지 않아 발생하는 것으로 볼 수 있을 것 같아요.&lt;/p&gt;

&lt;p&gt;일종의 살충제 패러독스를 겪게 되는 겁니다. 이런 이유로 “고정된 조건의 테스트 자동화는 테스팅이라고 볼 수 없다”고 이야기하시는 분들도 있습니다.&lt;/p&gt;

&lt;p&gt;하지만, 이 부분은 약간 논란의 여지가 있다고 생각됩니다. “사전 정의된 테스트 단계를 실행하고, 실제 결과를 예상된 결과와 비교하는 것” 자체로 범위를 규정하고 활용한다면 충분히 가치가 있다고 생각해요. &lt;a href=&quot;https://ko.wikipedia.org/wiki/%ED%9A%8C%EA%B7%80_%ED%85%8C%EC%8A%A4%ED%8A%B8&quot;&gt;리그레션 테스트&lt;/a&gt;가 이 범주에 속하죠.&lt;/p&gt;

&lt;p&gt;마지막으로 &lt;strong&gt;테스트 비용 절감&lt;/strong&gt; 부분은 테스트 자동화 적용 분야에 따라 다름이 있습니다만, 대체로 구축 및 유지보수 비용과 Trade off 된다고 보면 될 것 같아요.&lt;/p&gt;

&lt;h3 id=&quot;우리에게-필요할까요&quot;&gt;우리에게 필요할까요?&lt;/h3&gt;

&lt;p&gt;테스트 자동화의 활용 목적을 명확히 하고, 위의 장점을 최대화할 방법이 있다면 하지 않을 이유는 없죠! 특히, 적절한 범위의 리그레션 테스트에 활용한다면, 비단 테스트 효율을 높이는 것뿐만 아니라 우리가 지향하는 Agile 프로세스에도 많은 도움이 될 것으로 생각합니다.&lt;/p&gt;

&lt;p&gt;대규모 개발 또는 MVP 개발, 하물며 아주 작은 기능을 개발하여 그 산출물을 배포할 때까지의 사이클은 기본적으로 “기획 - 디자인 - 개발 - 테스트” 입니다. 전통적인 구조적 방법론처럼 보이지만 아무리 Agile이라고 해도 (병렬 수행으로 거의 동시에 진행될 수는 있지만) 프로젝트 내 활동을 기능적으로 나눈다면 이 범주를 벗어날 수는 없죠.&lt;/p&gt;

&lt;p&gt;여기서 기획-디자인-개발을 아무리 빠르게 진행한다고 해도 우리가 일정 수준의 품질을 유지하고, 그것에 대한 Confidence를 갖기 위해서는 테스트 기간을 줄이기 쉽지 않습니다. 특히, 추가되거나 변경에 의한 영향이 없는지 확인하는 절차를 제외 할 수 없습니다. 리그레션 테스트를 할 수 있는 최소한의 시간이 필요한 거죠.&lt;/p&gt;

&lt;p&gt;결국 이 리그레션 테스트때문에 아주 작은 기능을 추가하더라도 스프린트 또는 이터레이션의 기간은 일정 시간 이하로 떨어지지 않습니다.&lt;/p&gt;

&lt;p&gt;이것이 우리가 지향하는 Agility의 발목을 잡을 수 있다고 생각해요. 게다가 이 리그레션 테스트의 양은 제품이 배포되는 횟수에 따라 계속해서 늘어나는 경향이 있죠.&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;ol&gt;
  &lt;li&gt;리그레션 테스트에 활용하자.&lt;/li&gt;
  &lt;li&gt;그 범위는 구축과 유지보수에 부담되지 않는 수준으로 하자. (현재 테스트 케이스의 30% 수준)&lt;/li&gt;
  &lt;li&gt;All Pass 시나리오에 기반한 Positive 테스트 케이스를 자동화의 대상으로 추출해서 진행하자.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;이렇게 테스트 자동화의 목적과 범위를 결정 후 적당한 자동화 도구룰 선정하기로 했어요.&lt;/p&gt;

&lt;h2 id=&quot;어떤-도구를-사용할까&quot;&gt;어떤 도구를 사용할까?&lt;/h2&gt;

&lt;p&gt;도구를 선정하기 전에 우리가 어떠한 관점으로 테스트를 수행할지 정해야 합니다. Use Case Flow에 따른 UI 기능 테스트를 할 것인지, 아니면 명세표의 Life cycle에 따라 데이터의 변화 과정을 체크할 것인지, 그것도 아니면 둘 다 만족하는 테스트 환경을 구축할 것 인지를 정해야 하는거죠.&lt;/p&gt;

&lt;p&gt;먼저 우리는 활용 목적과 범위를 정했으니 그것을 기준으로 효율이 높은 쪽을 선택했습니다. Use Case에 따른 기능과 UI 변화를 체크하는 것이 상대적으로 효율이 높다고 판단했어요. 게다가 이쪽 부분이 사용자 접점이 가장 많은 부분이기도 하죠.&lt;/p&gt;

&lt;p&gt;이렇게 방향을 잡고 아래의 사항을 만족하는 UI 자동화 도구가 어떤 것이 있는지 조사했어요.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Android, iOS 모두 지원할 것&lt;/li&gt;
  &lt;li&gt;오픈소스이어야 할 것&lt;/li&gt;
  &lt;li&gt;스크립팅에 다양한 언어를 지원해야 할 것&lt;/li&gt;
  &lt;li&gt;다른 도구와의 통합이 가능할 것&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;아래 표와 같이 Appium이 오픈소스임에도 불구하고 많은 사항을 충족시켜주네요!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-05-24-test-automation-story/tools.png&quot; alt=&quot;tools&quot; /&gt;&lt;/p&gt;

&lt;p&gt;네, 테스트 자동화 도구로 Appium을 사용하기로 결정하고 환경 구축을 시작했습니다.&lt;/p&gt;

&lt;h2 id=&quot;appium-환경-구축하기&quot;&gt;Appium 환경 구축하기&lt;/h2&gt;

&lt;p&gt;Appium 환경 구축은 웹 상에 많은 가이드와 레퍼런스가 있음에도 불구하고 QA 엔지니어가 구축하기엔 여간 까다로운 일이 아닙니다. 특히 iOS 테스트 환경 구축은 더욱 그러하죠.&lt;/p&gt;

&lt;p&gt;Mac에서 Android, iOS 앱을 테스트 할 수 있는 환경을 만드는데 필요한 과정을 간단하게 나열하자면…&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[Android 환경]&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;디바이스 또는 에뮬레이터 연결을 위한 JDK와 Android Studio 설치&lt;/li&gt;
  &lt;li&gt;Appium Server 사용을 위한 Node.js 세팅&lt;/li&gt;
  &lt;li&gt;Appium Server 및 Inspector 세팅&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;비교적 간단하죠.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[iOS 환경]&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;WebDriverAgent 빌드를 위한 Xcode 설치&lt;/li&gt;
  &lt;li&gt;환경 구축에 필요한 라이브러리 설치를 위해 Homebrew 세팅&lt;/li&gt;
  &lt;li&gt;Appium Server 사용을 위한 Node.js 세팅&lt;/li&gt;
  &lt;li&gt;Appium Server 및 Inspector 세팅&lt;/li&gt;
  &lt;li&gt;iOS 디바이스와 통신하기 위한 libimobiledevice 라이브러리 설치&lt;/li&gt;
  &lt;li&gt;동적 라이브러리 관리 도구인 Carthage 설치&lt;/li&gt;
  &lt;li&gt;Command line으로 iOS 앱을 설치할 수 있게 도와주는 ios-deploy 세팅&lt;/li&gt;
  &lt;li&gt;디바이스를 제어하기 위한 WebDriverAgent 빌드&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;그밖에 세세하게 세팅해야 하는 부분이 있습니다. 복잡하네요.&lt;/p&gt;

&lt;p&gt;플랫폼 별 Appium 환경 구축에 대한 상세는 다른 기술 문서를 통해 이야기할게요.&lt;/p&gt;

&lt;h3 id=&quot;appium-동작-방식&quot;&gt;Appium 동작 방식&lt;/h3&gt;
&lt;p&gt;Appium이 어떻게 동작되는지 간단하게 살펴 보겠습니다.&lt;/p&gt;

&lt;figure&gt;
   &lt;img src=&quot;/images/2022-05-24-test-automation-story/appium-flow.png&quot; title=&quot;appium-flow&quot; /&gt;
   &lt;center&gt;&amp;lt;출처 : https://www.pentalog.com/blog/mobile-development/mobile-automation-with-robot-framework-and-appium&amp;gt;&lt;/center&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;br /&gt; &lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Appium Server는 Node.js를 사용하여 작성된 HTTP 서버입니다. Appium Client와 JSON Wire 프로토콜 및 HTTP 프로토콜을 사용하여 통신을 하죠. 또, Appium Server는 일종의 연결 Config인 DesiredCapabilites 정보를 사용하여 테스트 디바이스와 연결합니다.&lt;/p&gt;

&lt;p&gt;연결된 테스트 디바이스를 제어하는데에는 각 플랫폼 벤더에서 제공하거나 해당 플랫폼에 특화된 자동화 프레임워크를 이용합니다. 안드로이드 경우 UIAutomator2, iOS는 XCUITest 프레임워크를 사용하죠.&lt;/p&gt;

&lt;p&gt;Appium은 각 플랫폼의 테스트 프레임워크에 명령을 전달하고, UIAutomator2 또는 XCUITest는 해당 명령을 받아 테스트 디바이스를 컨트롤하죠.&lt;/p&gt;

&lt;p&gt;테스트가 완료되면 결과가 Appium 서버에 전달되고, 서버는 해당 log를 클라이언트에 전달하는 방식으로 테스트가 진행됩니다.&lt;/p&gt;

&lt;h3 id=&quot;appium-inspector-활용하기&quot;&gt;Appium Inspector 활용하기&lt;/h3&gt;

&lt;p&gt;Appium을 구성하는 것들 중 Server만큼 중요하고 유용한 친구가 있어요. 바로 Appium Inspector죠.&lt;/p&gt;

&lt;p&gt;Appium Inspector는 테스트하려는 앱 화면의 Element를 식별해주거나, 앱 화면을 DOM 구조로 파싱해주는 역할을 해요.&lt;/p&gt;

&lt;p&gt;테스트 자동화는 테스트 대상이 되는 객체에 대한 정보를 식별하는 것이 중요한데요. 이 Inspector가 그 역할을 해줍니다. 테스트 스크립트를 개발하는데 없어서 안되는 도구이죠. 그리고, 부가적으로 레코딩과 일부 동작의 스크립팅도 제공해주는 착한 친구입니다.&lt;/p&gt;

&lt;p&gt;Appium Inspector와의 연결은 테스트 디바이스 연결 방식과 같이 DesiredCapabilities 정보에 의해 이루어집니다. 이 컨피그 정보는 종류가 다양한데요. Appium 홈페이지에서 제공하는 Appium 가이드를 참고하여 컨피그를 설정하고 Appium Server를 띄운 후 실행하면 아래와 같은 화면이 출력됩니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-05-24-test-automation-story/appium-inspector.png&quot; alt=&quot;appium-inspector&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Capability 설정에 따라 테스트 디바이스에서 앱이 실행되고, 앱 화면이 미러링됩니다.&lt;/p&gt;

&lt;p&gt;앱 화면의 구조도 DOM Tree 형태로 노출되네요. 앱 화면이 미러링된 창의 특정 부분을 클릭하면 해당 Element의 정보를 알 수 있습니다.&lt;/p&gt;

&lt;p&gt;이 Inspector로 식별된 Element 정보는 앞서 이야기 했듯이 테스트 스크립트 개발에 필요한 중요하고 기본적인 정보입니다. 여기어 노출되는 ID, XPath 등의 정보로 Element 타게팅 방법을 적절하게 활용하여 테스트하려는 객체의 노출 여부를 확인하고, 클릭 후 변화된 화면을 식별하고, 정보를 입력하면서 테스트를 수행합니다.&lt;/p&gt;

&lt;h2 id=&quot;테스트-케이스-정제&quot;&gt;테스트 케이스 정제&lt;/h2&gt;

&lt;p&gt;지금까지 테스트 자동화 구축 전략을 세우고, 우리 입맛에 맞는 도구를 고른 후 환경 세팅까지 마쳤습니다. 이제 테스트 스크립트를 개발하는 일만 남았는데요. 그 전에 중요한 과정을 거쳐야 합니다.&lt;/p&gt;

&lt;p&gt;테스트 스크립트의 기반이 되는 테스트 케이스가 자동화하는데 적합하게 구성되어 있는지 분석할 필요가 있어요. 필요하다면 테스트 케이스를 수정/삭제/추가 해야 합니다. 그리고, 이 테스트 케이스 적합도 분석과 정제는 테스트 자동화의 유효성과 유지보수성을 결정하는 중요한 과정이기도 합니다.&lt;/p&gt;

&lt;p&gt;앞서 정한 활용 범위와 커버리지, 그리고 테스트 자동화 도구의 특성에 따라 테스트 케이스를 추출하고 정제했어요. 현재의 테스트 케이스에서 Positive 테스트 케이스를 추출하고, 다중 예상 결과에 대한 분리 작업, 동적인 객체 확인 제거 등등의 정제 과정을 거쳤습니다.&lt;/p&gt;

&lt;p&gt;다행히 현재 테스트 케이스는 카테고리를 명확하게 나누어 관리되고 있고, 사전조건, 절차, 예상결과가 명확한 상태이며, n:1구조를 가진 케이스는 없었기에 구조 변경까지는 필요 없었어요.&lt;/p&gt;

&lt;p&gt;테스트 케이스 적합도 분석과 정제를 통해 우리 목표치와 같이 전체 테스트 케이스 중 약 30% 케이스를 선정하고, 우선순위를 부여하여 1차와 2차 나누어 테스트 스크립트를 개발하기로 했습니다.&lt;/p&gt;

&lt;h2 id=&quot;테스트-스크립트-개발&quot;&gt;테스트 스크립트 개발&lt;/h2&gt;

&lt;p&gt;테스트 스크립트는 Python으로 구현하기로 했어요.&lt;/p&gt;

&lt;p&gt;안드로이드 앱 테스트 자동화를 먼저 진행하기로 했고, 1차로 전체 테스트 케이스의 약 10%의 커버리지를 목표로 진행했습니다. Sprint를 운영했는데, 정비 버전 통합 테스트 기간과 겹쳐 일정이 계획보다 약 2주 Delay 되었어요.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-05-24-test-automation-story/burndown.png&quot; alt=&quot;burndown&quot; /&gt;&lt;/p&gt;

&lt;p&gt;1차로 진행된 스프린트 번다운 입니다. 이상적이지는 않지만, 많은 프로젝트에서 볼 수 있는 그림이 되었어요.&lt;/p&gt;

&lt;p&gt;스프린트 중간 중간에 제품팀 개발자 분들께서 많은 도움을 주셨습니다. 바쁜 와중에 도움을 주신 제품팀 개발자 분들께 감사의 말씀 드립니다.&lt;/p&gt;

&lt;p&gt;현재까지 개발된 테스트 구조는 아래와 같아요.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-05-24-test-automation-story/test-struc.png&quot; alt=&quot;test-struc&quot; /&gt;&lt;/p&gt;

&lt;p&gt;간단하게 설명을 하자면, Python의 Unittest 라이브러리를 활용하여 Appium Client Test Set을 구성했어요.&lt;/p&gt;

&lt;p&gt;기본적인 구조는 AndroidTestRun에서 기능별 테스트 케이스를 Suite로 묶어 UnittestRunner를 통해 테스트가 수행될 수 있게 구성했습니다.&lt;/p&gt;

&lt;p&gt;홈, 더보기, 검색, KAMIS, 거래처 찾기 카테고리의 테스트 케이스 Set을  Unittest의 TestLoader로 로딩해서 실행하게 되면 아래와 같은 순서로 반복되는 구조에요.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;테스트 setUp() - ConfigSet 클래스의 testConfig() 호출&lt;/li&gt;
  &lt;li&gt;준비된 Testcase 실행&lt;/li&gt;
  &lt;li&gt;tearDown()으로 테스트 종료 및 자원 해제&lt;/li&gt;
  &lt;li&gt;AndroidTestRun에서 지정된 카테고리별로 1~4항목 반복&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;테스트 결과는 Appium Client 콘솔에 log 형태로 뿌려지기 때문에 결과 문서를 따로 추출할 수가 없습니다. 해서 HtmlTestRunner 라이브러리를 사용하여 Html로 결과를 뽑기로 했어요. 아래와 같이요.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-05-24-test-automation-story/result-html.png&quot; alt=&quot;result-html&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위의 결과서에서 Error나 Fail 항목 등은 View 버튼을 통해서 로그를 확인할 수 있습니다.&lt;/p&gt;

&lt;p&gt;현재는 Android App 테스트 케이스의 약 10%의 커버리지를 확보해 놓은 상태입니다만, 2022년 3분기까지 Android / iOS App 각각 테스트 케이스의 약 30% 커버리지를 확보할 계획이에요. Backoffice 역시 전체 테스트 케이스의 약 30% 커버리지를 목표로 Selenium을 활용하여 구축할 예정입니다.&lt;/p&gt;

&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;
&lt;p&gt;지금까지 구축과정에 대해 말씀드렸습니다. 앞서 말씀드린 그것처럼 테스트 자동화는 소프트웨어에 대한 다양한 테스트를 효율적으로 할 수 있는 만능 일꾼은 아닙니다. 필요한 부분에 필요한 만큼만 지원해주는 서포터라고 볼 수 있죠. 자동화가 꼭 필요한지도 살펴보아야 합니다. 어떤 목적으로 테스트 자동화를 활용할 것인지 명확해야 하고, 그 목적에 따라 전략을 잘 마련해야 유용하게 활용할 수 있다고 생각해요.&lt;/p&gt;

&lt;p&gt;우리는 테스트 자동화가 왜 필요한지와 어떻게 활용할 것인지, 자동화의 장점을 최대화할 방안이 무엇인지 고민을 통해 테스트 자동화를 구축하고 이제 한 걸음 뗐습니다.&lt;/p&gt;

&lt;p&gt;이 테스트 자동화가 우리 제품의 품질에 대한 Confidence를 확보하는 데 일조하고, 우리가 지향하는 프로세스에도 도움이 되는 아주 유용한 친구로 자리 잡는 것을 기대해 봅니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>도도카트 안드로이드 앱 개선 여정</title>
      <link>https://spoqa.github.io/2022/04/30/android-refactoring.html</link>
      <pubDate>Sat, 30 Apr 2022 00:00:00 +0000</pubDate>
      <author>황재우</author>
      <guid>/2022/04/30/android-refactoring</guid>
      <description>&lt;p&gt;안녕하세요. 스포카 제품팀의 안드로이드 개발자 황재우입니다.&lt;/p&gt;

&lt;p&gt;스포카는 최근 정비 기간을 통해서 개발팀 내에 많은 변화를 이루어냈습니다.&lt;br /&gt;
마찬가지로 안드로이드 챕터도 큰 변화를 가져왔는데요.&lt;br /&gt;
이 글은  키친보드 앱을 왜 개선해야 했는지, 무엇을 어떻게 진행했는지에 관해 이야기합니다.&lt;/p&gt;

&lt;h2 id=&quot;어떤-변화가-필요했을까요&quot;&gt;어떤 변화가 필요했을까요?&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/pray.gif&quot; alt=&quot;왜&quot; /&gt;&lt;/p&gt;

&lt;p&gt;키친보드 안드로이드 앱은 당시 복잡한 모습을 보여주고 있었습니다.&lt;/p&gt;

&lt;h3 id=&quot;이해하기-어려워진-프로젝트-구조&quot;&gt;이해하기 어려워진 프로젝트 구조&lt;/h3&gt;

&lt;p&gt;구조를 파악하기 어려웠습니다.&lt;br /&gt;
키친보드 안드로이드 앱은 그동안 2년간의 개발 기간을 거치면서 여러 가지 구조와 형태가 프로젝트 내에 섞였습니다. 몇몇 사용하지 않거나 중복된 코드가 존재하면서 이해하기 어렵게 됐습니다.&lt;/p&gt;

&lt;h3 id=&quot;라이브러리-버전-관리&quot;&gt;라이브러리 버전 관리&lt;/h3&gt;

&lt;p&gt;빌드 warning이 30~40개 정도 있었습니다.&lt;br /&gt;
대부분 Android Gradle Plugin에서 3rd party 라이브러리까지 오래된 버전들이 남아 있었습니다.&lt;/p&gt;

&lt;h3 id=&quot;파악하기-어려운-이슈&quot;&gt;파악하기 어려운 이슈&lt;/h3&gt;

&lt;p&gt;배포 시 맵핑 파일이 같이 올라가지 않아서, 발생한 이슈에서 호출 스택을 봐도 정확한 위치를 찾기 어려웠습니다. 또한 Firebase와 Sentry는 라이브러리 버전이 구 버전인 상태라서 가이드에 맞춰 다시 연동해야 했습니다.&lt;/p&gt;

&lt;h3 id=&quot;super-class&quot;&gt;Super Class&lt;/h3&gt;

&lt;p&gt;너무 많은 기능을 하고 있는 Super Class가 존재했습니다.&lt;br /&gt;
기능적인 면에서 분리되어야 했을 법한 로직들이 단일 클래스에 뭉쳐 있다는 것은 강력한 Coupling 이 프로젝트 내에 존재함을 뜻합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/cry.gif&quot; alt=&quot;cry&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;어떻게-준비했을까&quot;&gt;어떻게 준비했을까?&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/ready.gif&quot; alt=&quot;준비&quot; /&gt;&lt;/p&gt;

&lt;p&gt;키친보드 팀은 이전부터 정비기간이 필요할 것 같다는 공감대가 형성되어 논의 중이었습니다.&lt;br /&gt;
시기적으로 마침 안드로이드 앱 구조 개선에 대해 논의하면서 더불어 팀 내 정비 기간에 대한 논의가 본격적으로 시작됩니다. 결론적으로 저희는 정비 기간을 가지고 천하를 도모할 수 있게 되었죠!!&lt;br /&gt;
처음에는 리팩토링과 기능 개발을 병행하려던 생각이었으나 오로지 개선에 집중할 수 있게 되면서 작업에 유리한 환경이 조성됩니다.&lt;br /&gt;
이에 따라 최종 일정을 세우고 개선을 진행하기 위해서 내용을 한 번 더 정리했습니다.&lt;/p&gt;

&lt;h3 id=&quot;목표-설정&quot;&gt;목표 설정&lt;/h3&gt;

&lt;p&gt;일단 목표를 설정합니다. 왜 하는지 당위성도 포함됩니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/goal.png&quot; alt=&quot;goal&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;진행-방향과-작업-정의&quot;&gt;진행 방향과 작업 정의&lt;/h3&gt;

&lt;p&gt;모듈을 분리하고, 각 모듈은 클린 아키텍처를 표방합니다. 의존성 처리와 테스트 코드는 필요에 따라 들어갑니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/task_definition.png&quot; alt=&quot;def&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;마음가짐&quot;&gt;마음가짐&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/doyourbest.gif&quot; alt=&quot;do your best&quot; /&gt;&lt;/p&gt;

&lt;p&gt;가장 중요한 부분입니다. 작업 일정이 오래 지속되면 매우 힘들고 지치고 부담스럽거나, 더 잘하고 싶어서 욕심을 부리고 과다하게 일을 진행하게 됩니다. 그렇기에 내가 어떻게 하리라는 것을 정의하는 것은 매우 중요한 일입니다.&lt;/p&gt;

&lt;p&gt;이것은 두 가지 효과를 볼 수 있는데요.&lt;br /&gt;
첫째, 나 자신에게 거는 주문입니다.&lt;br /&gt;
무엇이 중요하고 무엇이 후순인지 명확히 하면, 의사 결정에 있어서 주저함이 없습니다. 그것이 코드이든 아키텍처이든 또는 그 외에 비 개발적인 것들도 포함해서요.&lt;br /&gt;
둘째, 내가 하는 작업의 그라운드 룰을 선포하는 의미가 있습니다.&lt;br /&gt;
나의 작업은 룰이 존재하고 그 룰안에서 이루어지리라는 것입니다라는 것을 대내외적으로 선포하는 것입니다. 이를 통해서 팀 내 구성원 모두에게 이 작업이 가지는 의미를 공표하는 효과를 가집니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/mindset.png&quot; alt=&quot;mindset&quot; /&gt;&lt;/p&gt;
&lt;figcaption align=&quot;center&quot;&gt;이렇게 마음 먹고 시작했다!&lt;/figcaption&gt;

&lt;h3 id=&quot;작업-사항과-일정-산출&quot;&gt;작업 사항과 일정 산출&lt;/h3&gt;

&lt;p&gt;작업 사항을 나열하고 일정을 산출합니다. 이때 주요한 작업과 기본 작업으로 분리된 상황이므로 주요 작업과 기본 작업에 대해서 일정을 적절하게 나눴습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/taskandschedule.png&quot; alt=&quot;task&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;어떻게-개선했을까&quot;&gt;어떻게 개선했을까?&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/how.gif&quot; alt=&quot;어떻게&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;빌드-구조-변경&quot;&gt;빌드 구조 변경&lt;/h3&gt;

&lt;p&gt;Kotlin DSL을 활용한 build.gradle.kts로 모두 전환했습니다.&lt;br /&gt;
&lt;img src=&quot;/images/2022-04-30-android-refactoring/result_build.png&quot; alt=&quot;gradle&quot; /&gt;
buildSrc를 활용해서 Gradle에서 라이브러리 의존성을 관리하고&lt;br /&gt;
&lt;img src=&quot;/images/2022-04-30-android-refactoring/result_buildsrc.png&quot; alt=&quot;buildsrc&quot; /&gt;
Gradle 구성 설정도 공용화했습니다.&lt;br /&gt;
&lt;img src=&quot;/images/2022-04-30-android-refactoring/result_depend.png&quot; alt=&quot;depend&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;모듈-분리&quot;&gt;모듈 분리&lt;/h3&gt;

&lt;p&gt;앱 모듈과 공통 모듈, 기능 모듈이 모여서 구성되는 형태로 바꿨습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/result_module.png&quot; alt=&quot;module&quot; /&gt;&lt;/p&gt;

&lt;p&gt;각 모듈은 대부분 UI를 포함한 기능 모듈이며, Clean Architecture를 표방하여 구조가 만들어졌습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/result_single_module.png&quot; alt=&quot;single_module&quot; /&gt;&lt;/p&gt;

&lt;p&gt;코드를 자세하게 다룰 수 없으나, Usecase와 Repository, DataSource로 이루어진 일련의 클래스들로 구성됩니다.&lt;br /&gt;
네 맞습니다. 안드로이드 개발자 사이트에서 안내하는 바로 그 앱 아키텍처를 기반으로 구현되었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/android_app_arch.png&quot; alt=&quot;app_arch&quot; /&gt;&lt;/p&gt;
&lt;figcaption align=&quot;center&quot;&gt;다이어그램은 쉬워 보인다...&lt;/figcaption&gt;

&lt;h3 id=&quot;이벤트-처리&quot;&gt;이벤트 처리&lt;/h3&gt;

&lt;p&gt;도메인과 데이터 영역에서 발생하는 이벤트는 ViewModel에서 UI 이벤트로 소비되도록 처리되었습니다. 따라서 도메인이나 데이터 영역에서 ViewModel을 통하지 않고 UI 영역을 넘어가지 않습니다.&lt;/p&gt;

&lt;h3 id=&quot;lifecycle-에-기반한-coroutine-과-flow&quot;&gt;lifecycle 에 기반한 Coroutine 과 Flow&lt;/h3&gt;

&lt;p&gt;기존에 있었던 Flow와 Coroutine을 기반으로 만들어진 비동기 처리 로직을 개선했습니다. 적절한 사용처와 Lifecycle에 맞춰서 Kotlin Coroutine을 처리했습니다.&lt;br /&gt;
Flow를 통한 서버 통신 처리는 단순한 비동기 데이터 수신 처리를 넘어서 Flow 확장 함수를 활용하여 예외 처리와 데이터 이벤트 처리를 하고 이를 연계 된 ViewModel에서 UI 이벤트로 소비될 수 있게 처리되었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/flow.gif&quot; alt=&quot;flow&quot; /&gt;&lt;/p&gt;
&lt;figcaption align=&quot;center&quot;&gt;물 흐르듯이 자연스럽게~&lt;/figcaption&gt;

&lt;h3 id=&quot;koin--hilt&quot;&gt;Koin → Hilt&lt;/h3&gt;

&lt;p&gt;Koin은 이해하기 쉽고 빠르게 적용할 수 있다는 큰 장점과 런타임에 오류와 오버헤드를 가지고 있다는 큰 단점이 존재합니다.&lt;br /&gt;
안드로이드 진영에서 Dagger를 대신해 선택되었던 라이브러리였지만 최근에 Hilt 가 나오면서 상황은 매우 달라졌습니다.&lt;br /&gt;
Hilt는 기존 Dagger를 활용해 안드로이드용으로 만들어진 의존성 주입 라이브러리입니다.&lt;br /&gt;
Koin과 달리 컴파일 타임에 오류가 검출되며, 적용하는 것이 어렵지 않습니다.&lt;br /&gt;
그래서 의존성 주입은 Hilt를 통해서 하기로 했습니다.&lt;br /&gt;
기존 의존성 처리를 걷어내고 Hilt로 새로 구성해 나갔습니다.&lt;/p&gt;

&lt;h2 id=&quot;아쉬움과-남은-과제&quot;&gt;아쉬움과 남은 과제&lt;/h2&gt;

&lt;p&gt;큰 구조적인 변화를 가져왔고, 분리된 모듈로서 형태를 갖췄음에도 아쉬움은 여전히 남습니다.&lt;br /&gt;
하고 싶었지만 못했던 것들은 이렇습니다.&lt;/p&gt;

&lt;h3 id=&quot;모델-분리&quot;&gt;모델 분리&lt;/h3&gt;

&lt;p&gt;키친보드 앱은 Graphql 을 사용하며 Graphql의 Data를 그대로 사용하고 있습니다. 이 의존성이 전 영역에 걸쳐서 뻗쳐 있어서 이걸 제거하고 VO와 DTO를 기반으로 하고 싶었습니다.&lt;/p&gt;

&lt;h3 id=&quot;사용자-액션-객체화&quot;&gt;사용자 액션 객체화&lt;/h3&gt;

&lt;p&gt;사용자가 발생시키는 이벤트에 대해 데이터 객체를 생성해서 처리하고 싶었습니다.&lt;/p&gt;

&lt;h3 id=&quot;비지니스-로직의-세분화&quot;&gt;비지니스 로직의 세분화&lt;/h3&gt;

&lt;p&gt;비즈니스 로직이 ViewModel에서 존재하지만 ViewModel 내에서도 너무 많은 코드가 들어갑니다. 단일 로직을 책임지고 결과를 줄 수 있는 객체로 변경하고 싶었습니다.&lt;/p&gt;

&lt;h3 id=&quot;부족한-테스트-코드&quot;&gt;부족한 테스트 코드&lt;/h3&gt;

&lt;p&gt;테스트 코드가 전체 모듈에 다 적용되지 못했고 주요 모듈에서만 적용되었습니다. 앱의 기능적인 완성도와 안정성을 위해서 테스트 코드를 좀 더 많이 작성하고 싶었습니다.&lt;/p&gt;

&lt;h2 id=&quot;마치면서&quot;&gt;마치면서&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/images/2022-04-30-android-refactoring/victory.gif&quot; alt=&quot;victory&quot; /&gt;&lt;/p&gt;
&lt;figcaption align=&quot;center&quot;&gt;드디어 끝냈다!&lt;/figcaption&gt;

&lt;p&gt;이번 개선 작업을 통해서 프로젝트 전반의 이해도를 높였고 앞으로 있을 여러 작업에 대한 만반의 준비를 했다고 자신합니다. 유지 보수와 관리가 용이한 구조로 바꾼 만큼 더 재밌게 일을 할 수 있게 되었습니다.&lt;/p&gt;

&lt;p&gt;무엇보다도 이번 개선 과정에서 많은 코드 리뷰와 배려, 신뢰를 보여준 동료들에게 깊은 감사를 드립니다.&lt;/p&gt;

&lt;p&gt;키친보드 앱은 이제 시작입니다! &lt;br /&gt;
저와 함께 재밌게 안드로이드 개발을 하실 분들의 많은 지원 부탁드립니다!&lt;/p&gt;

&lt;p&gt;지금까지 긴 글을 읽어주셔서 감사합니다.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>서버 언어 전환 이야기</title>
      <link>https://spoqa.github.io/2022/04/15/all-new-server.html</link>
      <pubDate>Fri, 15 Apr 2022 00:00:00 +0000</pubDate>
      <author>남경호</author>
      <guid>/2022/04/15/all-new-server</guid>
      <description>&lt;p&gt;안녕하세요. 키친보드 팀의 백엔드 프로그래머 남경호입니다.&lt;/p&gt;

&lt;p&gt;최근 &lt;a href=&quot;https://www.spoqa.com/&quot; target=&quot;\_blank&quot;&gt;스포카&lt;/a&gt;는 서울 본사 사무실 이전과 함께 &lt;a href=&quot;https://zdnet.co.kr/view/?no=20220127161642&quot; target=&quot;\_blank&quot;&gt;도도 포인트 서비스양도&lt;/a&gt; 등 많은 변화의 시간을 가졌습니다. 제가 몸담은 &lt;a href=&quot;https://kitchenboard.co.kr/&quot; target=&quot;\_blank&quot;&gt;키친보드&lt;/a&gt; 팀도 앞으로 더 나은 서비스를 제공하기 위한 정비 기간을 가졌었는데요, 정비 기간에 백엔드 챕터는 현재 운영되고 있는 키친보드 서비스의 서버에 사용된 언어를 변경하기로 하였습니다. 이 글에서는 이 기간 동안 왜, 그리고 어떻게 언어 전환을 이루었는지 이야기해보도록 하겠습니다.&lt;/p&gt;

&lt;h2 id=&quot;언어-전환이-필요합니다&quot;&gt;언어 전환이 필요합니다!&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://media.giphy.com/media/3osxYrlGDhnsYtl7vW/giphy.gif&quot; alt=&quot;언어전환&quot; /&gt;&lt;/p&gt;

&lt;p&gt;사실 제가 입사할 때 고민했던 요소 중 하나가 제 커리어 중 python을 이용한 개발 경험이 없었다는 것이었습니다. 하지만 많은 개발 서적이나 블로그에서 “언어는 도구에 불과하다”라는 말을 믿고 제품만 잘 만들면 될 거라는 생각과 함께 그 고민을 접어 두고 입사를 결정하게 되었습니다. 2021년 3월에 입사해서 정비 기간을 시작하는 11월까지 키친보드 서비스의 서버를 개발하면서 python의 문법도 배우고, 생태계도 알아가면서 미약하게나마(?) python과 프레임워크 들을 배워갔었고 또 더 나은 코드를 위한 노력도 함께 이어갔습니다. (&lt;a href=&quot;https://spoqa.github.io/2021/09/13/domain-driven-development-transition-story.html&quot; target=&quot;\_blank&quot;&gt;도메인 주도 개발 전환 이야기&lt;/a&gt;를 참고해주세요.)&lt;/p&gt;

&lt;p&gt;하지만 아래와 같은 이유로 서버 언어의 전환이 필요하다 판단되었고 CTO님과 팀장님 그리고 챕터 구성원분들과 함께 논의해본 결과 언어 전환을 진행하기로 결정하였습니다. (&lt;em&gt;아래에 적힌 이유는 지극히 저의 개인적인 생각과 팀이 처한 상황에 대한 내용입니다. 특정 언어의 좋고 나쁨을 이야기하는 것이 아니니 넓은 마음으로 이해해주셨으면 합니다 :)&lt;/em&gt;)&lt;/p&gt;

&lt;h3 id=&quot;팀-내-python과-그-프레임워크에-대한-높은-이해도를-가진-인력의-부재&quot;&gt;팀 내 python과 그 프레임워크에 대한 높은 이해도를 가진 인력의 부재&lt;/h3&gt;

&lt;p&gt;앞에서도 언급하였지만, 언어는 도구에 불과하다는 말에는 전적으로 동의합니다. 하지만 동일한 제품을 만드는 데 더 나은 품질과 더 빠른 속도로 개발할 수 있다면 그 도구를 선택하는 것이 팀을 위해서, 제품을 위해서 더 나은 선택이라 생각됩니다. 언어를 전환하는 것이 비록 적은 시간이 있어야 하는 것이 아니긴 하지만 팀원 모두가 더 전문성을 가진 도구를 사용하게 됨으로써 장기적으로 높은 생산성을 가지고 개발을 지속해 간다면 언어 전환 시 사용했던 시간을 충분히 상쇄시킬 수 있을 것이라 판단했습니다.&lt;/p&gt;

&lt;h3 id=&quot;보다-높은-유지보수성&quot;&gt;보다 높은 유지보수성&lt;/h3&gt;

&lt;p&gt;많은 개발자분이 알고 계시겠지만 python은 동적 프로그래밍 언어입니다. 컴파일 없이 개발할 수 있어 초기 프로토타입의 제품을 만들 때에는 빠르고 효율적으로 기능을 만들어 낼 수 있었을 것입니다. 하지만 기능이 점점 많아지고 기존 기능을 수정해야 할 필요성이 증가함에 따라 이 동적 타입은 우리의 발목을 잡기 시작했습니다. 비록 python도 타입 힌트를 줄 수 있지만 이것은 단지 코드 작성 시 도움을 주기 위한 기능이지 런타임에 해당 타입으로 동작함을 보장하지는 않습니다. (오히려 적혀진 타입과 다른 값이 들어가 한참을 디버깅했던 기억이 있네요) 또한 &lt;a href=&quot;https://en.wikipedia.org/wiki/Duck_typing&quot; target=&quot;\_blank&quot;&gt;덕 타이핑&lt;/a&gt;이라는 기술도 참 유연하고 편리한 기술이지만 기능의 수정이 필요할 때 인터페이스가 없기 때문에 구현 코드를 추적하여 수정하는 것이 쉽지 않다는 문제가 있습니다.&lt;/p&gt;

&lt;p&gt;반면 우리가 선택한 &lt;a href=&quot;https://kotlinlang.org/&quot; target=&quot;\_blank&quot;&gt;Kotlin&lt;/a&gt;은 타입 추론을 통해 동적 언어와 유사한 코드 스타일을 가져갈 수 있으며 좀 더 단순하면서 가독성 코드를 작성해 줄 수 있게 해주고, 정적 프로그래밍 언어이기에 타입을 보장해 주어 더 나은 유지보수성을 꾀할 수 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;보다-높은-비즈니스-집중도&quot;&gt;보다 높은 비즈니스 집중도&lt;/h3&gt;

&lt;p&gt;경량 프레임워크는 가볍고 개발자가 원하는 대로 커스터마이징하기 좋은 장점을 가지고 있습니다. 하지만 개발자의 능력에 따라 제품 품질의 편차가 크고 보편적인 개발 방법이 아닌 경우 새로운 개발 팀원이 합류하였을 때 회사의 프레임워크에 적응하는 시간을 많이 써야 한다는 단점도 가지고 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://spring.io/&quot; target=&quot;\_blank&quot;&gt;스프링 프레임워크&lt;/a&gt;는 우리가 개발하는 환경에서 필요한 대부분의 기능을 가져다 사용할 수 있는 풍부한 생태계를 제공하기 때문에 비즈니스 코드에만 집중할 수 있도록 합니다. 그리고 많은 레퍼런스와 보편적으로 사용되는 설정 등은 새로운 팀원이 합류하더라도 보다 짧은 시간 내에 회사 코드에 적응하는 장점도 가지고 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;넓은-인력풀&quot;&gt;넓은 인력풀&lt;/h3&gt;

&lt;p&gt;기술적인 부분에만 집중하면 좋겠지만 제품을 만들 때 혼자만으로 모든 것을 만들 수 없기 때문에 인력풀을 생각하지 않을 수 없습니다. 최근 python은 주목받는 언어로써 많은 개발자가 사용하고 좋아하는 언어이지만 한국에서는 여전히 웹 서버 개발 시 java와 Spring을 사용하는 개발자 비율이 높다고 생각합니다. (최근에는 Kotlin + Spring의 사용률도 점점 올라가고 있어 아주 반갑습니다) 감사하게도 &lt;a href=&quot;https://woowacourse.github.io/&quot; target=&quot;\_blank&quot;&gt;우아한 테크코스&lt;/a&gt;와 같이 좋은 개발 방법까지 함께 알려주는 교육기관이 늘어나면서 좀 더 우리가 원하는 인재를 더 많이 채용할 기회가 생기고 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;어떤-계획을-가지고-있나요&quot;&gt;어떤 계획을 가지고 있나요?&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/images/all-new-server/you-have-plan.gif&quot; alt=&quot;너는 계획이 다 있구나&quot; /&gt;&lt;/p&gt;

&lt;p&gt;사실 2년 정도 개발된 제품의 코드를 짧은 시간 내에 변경하겠다는 결정과 그 시간을 산정하는 것은 쉽지 않았습니다. 다행히 &lt;a href=&quot;https://spoqa.github.io/2021/09/13/domain-driven-development-transition-story.html&quot; target=&quot;\_blank&quot;&gt;도메인 주도 개발 전환 이야기&lt;/a&gt;에서 소개한 바와 같이 현재 제품에 사용되는 주요 도메인들의 기능목록이 정리되어 있었고, 기존 코드도 패키지 정리와 함께 도메인별로 코드들을 리팩토링을 해왔었습니다. (이쯤 되면 언어 전환을 위한 준비를 계속해왔다고 볼 수 있겠네요.)&lt;/p&gt;

&lt;p&gt;해당 데이터들을 기반으로 아래와 같이 전환할 기능 목록을 정리하고&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/all-new-server/graphql-api-list.png&quot; alt=&quot;Graphql API 목록&quot; /&gt;&lt;/p&gt;

&lt;p&gt;로드맵을 작성하였으며&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/all-new-server/roadmap.png&quot; alt=&quot;roadmap&quot; /&gt;&lt;/p&gt;

&lt;p&gt;프론트분들께 협조를 구하여 사용 중인 API들을 선별하고&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/all-new-server/survey-api.png&quot; alt=&quot;API 사용여부 조사&quot; /&gt;&lt;/p&gt;

&lt;p&gt;JIRA의 작업 항목들을 생성하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/all-new-server/task-list.png&quot; alt=&quot;Task 목록&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;어떻게-전환작업을-하였나요&quot;&gt;어떻게 전환작업을 하였나요?&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://media.giphy.com/media/26mfigxZrxXIrk7xm/giphy.gif&quot; alt=&quot;How&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;제약-조건&quot;&gt;제약 조건&lt;/h3&gt;

&lt;p&gt;먼저 전환 작업 시 부득이한 경우를 제외하고 절대 변경하지 않겠다고 다짐한 것이 바로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;API Schema&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Database&lt;/code&gt;였습니다.&lt;/p&gt;

&lt;p&gt;언어 전환을 하다 보면 많은 난관에 봉착하게 됩니다. 기존언어에서 사용된 표현 식이나 라이브러리 기능들이 전환하려고 하는 언어 또는 라이브러리가 지원하지 않아 다른 방식을 모색해야 한다든지, 나름대로 기존언어와 동일한 로직으로 구현한다고 노력하였지만 실제로는 다르게 동작한다는 것과 같은 이슈들이 생기게 됩니다. 이러한 문제들을 해결해가며 전환작업을 하는 것도 힘든데 기존 API의 Schema와 Database의 Column 또는 Table 들을 좀 더 나은 방식으로 변경하는 욕심까지 부린다면 전환 프로젝트는 영영 끝나지 않거나 약속된 기간을 지키지 못하여 작업 자체가 중단되는 상황이 발생하기도 합니다.&lt;/p&gt;

&lt;p&gt;저희는 이러한 위험성을 최소한으로 하고 싶었습니다. 그래서 프론트앤드에서 전혀 사용하지 않는 API를 제거하는 작업과 부득이하게 변경을 필요로 하는 작업 몇몇을 제외하고는 API Schema의 변경을 최소한으로 하였고 Database는 하나도 변경하지 않았습니다. 그러다 보니 오롯이 비즈니스 코드를 새로운 언어로 옮기는 데에만 집중할 수 있었습니다. 그리고 기존 통합 테스트 코드의 테스트 케이스를 그대로 가지고 올 수 있었기 때문에 테스트 케이스를 새롭게 고민하여야 하는 공수를 많이 줄일 수 있었습니다.&lt;/p&gt;

&lt;h3 id=&quot;테스트&quot;&gt;테스트&lt;/h3&gt;

&lt;p&gt;기존에 작성된 테스트와 유사하게 이번에도 도메인 레이어에서는 유닛테스트를 어플리케이션 레이어에서는 통합테스트를 작성하며 언어 전환 작업을 수행하였습니다. 도메인 레이어에서 유닛테스트를 수행한 이유는 도메인 레이어가 의존성이 적기 때문에 각 유즈케이스를 보다 쉽게 그리고 세세하게 다룰 수 있었기 때문에 엣지 케이스에 대한 테스트 케이스 작성이 보다 용이하다고 판단했기 때문입니다. 만약 어플리케이션 레이어에서 통합테스트로 도메인이 가진 모든 유즈케이스를 테스트 케이스로 표현하려고 한다면 중복되는 테스트도 엄청나게 많아질 것이고 의존하는 코드가 많기 때문에 코드가 장황해지면서 가독성이 상당히 나쁜 테스트 코드들이 작성될 것 같았습니다.&lt;/p&gt;

&lt;p&gt;이러한 테스트 작성 노력으로 인해 실제로 QA 입고 시까지 로컬이나 개발환경에서 서버를 거의 실행시켜보지 않고도 언어 전환을 완료할 수 있었습니다. 비록 몇몇 이슈가 발생하긴 하였지만 비즈니스 로직에 의한 버그가 아닌 실행환경의 차이로 인해 미처 발견하지 못했던 버그들이었고 이것 또한 손쉽게 원인을 파악하고 고칠 수 있었습니다.&lt;/p&gt;

&lt;h2 id=&quot;새로운-서버는-어떤-기술들이-적용되었나요&quot;&gt;새로운 서버는 어떤 기술들이 적용되었나요?&lt;/h2&gt;

&lt;p&gt;이번에 변경될 서버의 기술 스택을 적어보면 아래와 같습니다.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://kotlinlang.org/&quot; target=&quot;\_blank&quot;&gt;kotlin&lt;/a&gt;, &lt;a href=&quot;https://spring.io/projects/spring-boot&quot; target=&quot;\_blank&quot;&gt;Spring Boot&lt;/a&gt;, &lt;a href=&quot;https://gradle.org/&quot; target=&quot;\_blank&quot;&gt;Gradle&lt;/a&gt;, &lt;a href=&quot;https://netflix.github.io/dgs/&quot; target=&quot;\_blank&quot;&gt;DGS&lt;/a&gt;, &lt;a href=&quot;https://spring.io/projects/spring-data-jpa&quot; target=&quot;\_blank&quot;&gt;Spring Data JPA&lt;/a&gt;, &lt;a href=&quot;http://querydsl.com/&quot; target=&quot;\_blank&quot;&gt;QueryDSL&lt;/a&gt;, &lt;a href=&quot;https://spring.io/projects/spring-data-elasticsearch&quot; target=&quot;\_blank&quot;&gt;Spring Data Elasticsearch&lt;/a&gt;, &lt;a href=&quot;https://kotest.io/&quot; target=&quot;\_blank&quot;&gt;Kotest&lt;/a&gt;, &lt;a href=&quot;https://flywaydb.org/&quot; target=&quot;\_blank&quot;&gt;Flyway&lt;/a&gt;, &lt;a href=&quot;https://spring.io/projects/spring-security&quot; target=&quot;\_blank&quot;&gt;Spring Security&lt;/a&gt;, &lt;a href=&quot;https://ktlint.github.io/&quot; target=&quot;\_blank&quot;&gt;ktlint&lt;/a&gt;, &lt;a href=&quot;https://github.com/radarsh/gradle-test-logger-plugin&quot; target=&quot;\_blank&quot;&gt;Test Logger&lt;/a&gt;, &lt;a href=&quot;https://github.com/GoogleContainerTools/jib&quot; target=&quot;\_blank&quot;&gt;jib&lt;/a&gt;, &lt;a href=&quot;https://spring.io/projects/spring-cloud-openfeign&quot; target=&quot;\_blank&quot;&gt;OpenFeign&lt;/a&gt;, &lt;a href=&quot;https://spring.io/projects/spring-cloud-aws&quot; target=&quot;\_blank&quot;&gt;AWS&lt;/a&gt;, &lt;a href=&quot;https://docs.spring.io/spring-cloud-gcp/docs/current/reference/html/bigquery.html&quot; target=&quot;\_blank&quot;&gt;BigQuery&lt;/a&gt;, &lt;a href=&quot;https://github.com/f4b6a3/ulid-creator&quot; target=&quot;\_blank&quot;&gt;ULID&lt;/a&gt;, &lt;a href=&quot;https://github.com/MicroUtils/kotlin-logging&quot; target=&quot;\_blank&quot;&gt;Kotlin logging&lt;/a&gt;, &lt;a href=&quot;https://github.com/google/libphonenumber&quot; target=&quot;\_blank&quot;&gt;libphonenumber&lt;/a&gt;, &lt;a href=&quot;https://tika.apache.org/1.14/gettingstarted.html&quot; target=&quot;\_blank&quot;&gt;Tika&lt;/a&gt;, &lt;a href=&quot;https://github.com/sendgrid/sendgrid-java&quot; target=&quot;\_blank&quot;&gt;Sendgrid&lt;/a&gt;, &lt;a href=&quot;https://github.com/firebase/firebase-admin-java&quot; target=&quot;\_blank&quot;&gt;Firebase&lt;/a&gt;, &lt;a href=&quot;https://www.elastic.co/guide/en/observability/current/index.html&quot; target=&quot;\_blank&quot;&gt;Elastic APM&lt;/a&gt;, &lt;a href=&quot;https://poi.apache.org/&quot; target=&quot;\_blank&quot;&gt;POI&lt;/a&gt;, &lt;a href=&quot;https://mockk.io/&quot; target=&quot;\_blank&quot;&gt;mockk&lt;/a&gt;, &lt;a href=&quot;https://github.com/bluegroundltd/kfactory&quot; target=&quot;\_blank&quot;&gt;KFactory&lt;/a&gt;, &lt;a href=&quot;https://github.com/DiUS/java-faker&quot; target=&quot;\_blank&quot;&gt;Java Faker
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;적용된 기술들을 어떻게 사용하는지 모두 하나하나 말씀드리고 싶은 마음이 굴뚝같지만 본 글의 주제를 흐릴 수 있고 내용이 너무 방대해질 것 같으므로 별도로 소개하는 글을 적어보도록 하겠습니다. (이렇게 떡밥들만 던져놓았다가 어떻게 회수하려고…)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/all-new-server/paste-bait.jpeg&quot; alt=&quot;paste-bait&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;점진적으로-언어전환을-할-순-없나요&quot;&gt;점진적으로 언어전환을 할 순 없나요?&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://media.giphy.com/media/3orif8qr37qRdmcEYU/giphy.gif&quot; alt=&quot;Why&quot; /&gt;&lt;/p&gt;

&lt;p&gt;언어 전환 계획을 보면 전환작업을 모두 끝낸 후 한 번에 변경 사항을 배포하는 일정으로 계획되어 있습니다. 그럼, 여기서 왜 점진적 변경이 아닌 일괄적인 변경으로 큰 리스크를 짊어지나? 라는 질문을 할 수 있을 것 같습니다.&lt;/p&gt;

&lt;p&gt;저도 점진적 변경을 통해 리스크를 최소화하고 서비스 기능 개발과 언어 전환 작업을 함께하는 아주 이상적인 모습으로 이 작업을 수행하고 싶었습니다. 하지만 기존 서버를 운영하면서 언어 전환을 시도하다 레거시는 레거시대로 2배로 늘어나고 언어 전환은 새로운 기능을 쫓아가지 못해 결국 흐지부지되는 상황을 겪어본 터라 하려면 온전히 집중해서 언어 전환을 빠르게 마무리 짓는 게 나을 것이라 판단했습니다.&lt;/p&gt;

&lt;p&gt;또한 현재 키친보드 서비스는 Graphql을 사용하고 있는데요, Graphql의 특성상 단일 path를 사용합니다. 그러다 보니 &lt;a href=&quot;https://aws.amazon.com/ko/premiumsupport/knowledge-center/elb-achieve-path-based-routing-alb/&quot; target=&quot;\_blank&quot;&gt;Load balancer에서 경로기반 라우팅&lt;/a&gt;을 이용하여 앱의 배포 없이 점진적으로 서버를 교체하기에는 무리가 있어 보였습니다. (혹시 대안을 알고 있는 분이 계신다면 알려주시면 정말 감사하겠습니다!)&lt;/p&gt;

&lt;h2 id=&quot;이슈가-있습니다&quot;&gt;이슈가 있습니다.&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://media.giphy.com/media/t6008cWl3tAFADOCOw/giphy.gif&quot; alt=&quot;이슈&quot; /&gt;&lt;/p&gt;

&lt;p&gt;쉽지 않은 작업, 문제없이 순탄하게 진행되면 좋겠지만 세상만사 뜻대로 되지 않았습니다. 언어 전환을 하면서 어떤 이슈들이 있었는지 보겠습니다.&lt;/p&gt;

&lt;h3 id=&quot;도도-포인트-서비스-양도&quot;&gt;도도 포인트 서비스 양도&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://zdnet.co.kr/view/?no=20220127161642&quot; target=&quot;\_blank&quot;&gt;도도 포인트 서비스양도&lt;/a&gt; 소식은 내부 직원들에게 큰 이슈였습니다. 10년 동안 스포카를 있게 해준 서비스였기 때문에 많은 아쉬움과 함께 코드, 인프라 등 이관을 위한 많은 작업을 필요로 하였습니다. 이 부분은 전환 계획 시 고려되지 않았던 이슈였기 때문에 전환 작업 일정에 많은 영향을 미쳤습니다.&lt;/p&gt;

&lt;h3 id=&quot;jwt&quot;&gt;JWT&lt;/h3&gt;

&lt;p&gt;키친보드 서비스는 API를 사용하기 위한 인증 수단으로 &lt;a href=&quot;https://jwt.io/&quot; target=&quot;\_blank&quot;&gt;JWT&lt;/a&gt;를 사용합니다. 프레임워크를 변경하면서 이 JWT를 통한 인증 및 인가를 위한 코드를 새롭게 작성하게 되었고 이 과정에서 기존의 인증 방식에 대한 문제점을 발견하게 되었습니다. 이 이슈에 대한 내용을 적기에는 글의 내용이 너무 많아질 듯하여 추후 별도의 글로 해결 과정들을 소개해 드리도록 하겠습니다.&lt;/p&gt;

&lt;h3 id=&quot;jpa&quot;&gt;JPA&lt;/h3&gt;

&lt;p&gt;앞서 말씀드린 바와 같이, 저희 백엔드 챕터는 python으로 서버를 개발할 때에도 언어 전환을 할 때에도 항상 테스트 코드를 작성하고 있습니다. 유닛테스트와 통합테스트를 섞어서 사용함으로써 좀 더 안정성 높은 서비스를 만들어가기 위해 노력하였습니다. 하지만 QA 진행 중에 테스트 코드에서는 발견할 수 없었던 JPA와 관련한 이슈가 다수 발생했습니다. (&lt;a href=&quot;https://en.wikipedia.org/wiki/Jakarta_Persistence&quot; target=&quot;\_blank&quot;&gt;JPA&lt;/a&gt;는 Spring Framework를 이용한 개발 시 ORM을 사용할 때 JPA를 빼놓으면 서운할 정도로 요즘에는 많이 쓰는 라이브러리입니다)&lt;/p&gt;

&lt;p&gt;테스트 시 데이터 클리닝을 좀 더 편하게 하기 위해서 기능 테스트가 아닌 통합 테스트를 선택하였는데 이 실행 환경의 차이로 인해 QA 시 몇몇 이슈가 생긴 부분에 대해서는 아쉬움으로 남습니다. JPA로 인해 발생했던 이슈들도 별도의 글로 소개해 드리면서 해결 과정을 자세히 알려드리도록 하겠습니다.&lt;/p&gt;

&lt;h2 id=&quot;회고합시다&quot;&gt;회고합시다~&lt;/h2&gt;

&lt;p&gt;언어 전환 작업에 대해서 총 2번의 회고를 진행하였습니다. 위 로드맵에서 보셨다시피 언어 전환 작업은 크게 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;도메인&lt;/code&gt; 레이어에 대한 전환 작업과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;어플리케이션&lt;/code&gt; 레이어에 대한 전환작업으로 나누어서 진행하였는데요. 기간이 짧지 않은 만큼 중간에 쉬어가는 시간도 가질 겸 중간 점검의 목적으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;도메인&lt;/code&gt; 레이어 작업이 끝나고 난 후 회고를 한번 진행하고 언어 전환이 끝난 후에 전체 작업에 대한 회고를 한 번 더 하는 방식이었습니다.&lt;/p&gt;

&lt;p&gt;회고의 내용은 크게 작업의 퍼포먼스를 돌아보는 부분과 작업 기간 내 변경하고 싶은 점,&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/all-new-server/task-performance.png&quot; alt=&quot;task-performance&quot; /&gt;&lt;/p&gt;

&lt;p&gt;계속 이어가고 싶은 점, 시도해보고 싶은 점들을 이야기하는 회고 부분,&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/all-new-server/retrospective.png&quot; alt=&quot;retrospective&quot; /&gt;&lt;/p&gt;

&lt;p&gt;그리고 회고 시 나온 Action Item 들을 적어 다음 수행 시 개선할 부분을 적어보는 부분으로 나누어 진행했습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/all-new-server/action-itmes.png&quot; alt=&quot;action-items&quot; /&gt;&lt;/p&gt;

&lt;p&gt;저희는 다른 부분보다 특히 작업에 대한 퍼포먼스를 돌아보는 부분에 많은 이야기를 나누었는데요, 이때 &lt;a href=&quot;https://support.atlassian.com/jira-software-cloud/docs/view-and-understand-the-control-chart/&quot; target=&quot;\_blank&quot;&gt;JIRA의 Control Chart&lt;/a&gt;를 이용하여 이야기를 나누었던 것이 큰 도움이 되었습니다.&lt;/p&gt;

&lt;p&gt;Control Chart를 통해서 저희는 정해진 기간 동안 평균적으로 어느 정도 소요 시간을 가지고 작업을 처리해왔고 얼마나 효율적이고 생산성이 나아지는지, 나빠지는지를 파악할 수 있었습니다. 아래 사진은 저희가 언어 전환 기간 동안 수행했던 작업을 Control Chart로 표시한 것입니다. 해당 차트를 통해 언어 전환 작업을 진행하는 동안 점점 생산성이 높아지고 있었음을 알 수 있었고 중간에 어떤 이슈로 인해 작업에 영향을 미쳤는지 등을 파악하는 데 많은 도움이 되었습니다. (태스크를 생성하고 해당 태스크 하위로 서브 태스크들을 생성해서 작업하다 보니 분포도 표시에 조금 모순이 있습니다. 이 부분을 감안하여 데이터를 분석하였습니다.)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/all-new-server/cycle-time.png&quot; alt=&quot;cycle-time&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;마치며&quot;&gt;마치며&lt;/h2&gt;

&lt;p&gt;지금까지 저희가 언어 전환을 어떻게 계획하고 수행해 왔는지 이야기해보았습니다. 기술적인 이야기보다 비기술적인 이야기가 대부분이었는데요. 기술적인 부분은 앞서 예고 드린 바와 같이 앞으로 계속해서 구체적인 내용의 글로 찾아뵙도록 하겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/all-new-server/like.jpeg&quot; alt=&quot;like&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이번 작업을 통해서 언어 전환도 전환이지만 현재 우리 키친보드 서비스에 대한 이해도가 몇 배 이상 증가했다는 점만으로도 유종의 미를 거두었다고 생각합니다. 구현하면서 몰랐던 기능들을 아주 많이 알게 되었거든요. 그리고 이전에 하지 못했던 APM이라던지 불필요한 코드들을 제거하는 등의 많은 부분을 개선할 수 있게 되어서 작업자들의 만족도가 아주 높았다는 점도 성공적인 전환작업이었다고 말씀드릴 수 있는 부분이기도 합니다.&lt;/p&gt;

&lt;p&gt;한편, 전환 작업을 통해서 언어 변경을 통해 앞으로의 생산성이 높아질 것이란 기대감을 가지고 있습니다. 향후 많은 새로운 기능들이 기다리고 있는데요, 새로운 기능들을 개발해 가면서 이 전환 작업이 어떤 도움이 되었는지 돌아보는 시간을 가지는 것도 재미있을 것 같습니다.&lt;/p&gt;

&lt;p&gt;앞으로 발전해가는 키친보드 서비스, 많은 기대 바라며 긴 글 읽어 주셔서 감사하다는 말씀드리면서 이번 이야기는 마치도록 하겠습니다.&lt;/p&gt;

&lt;p&gt;감사합니다.&lt;/p&gt;
</description>
    </item>
    

  </channel>
</rss>
