<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>오늘도 더 나은 코드를 작성하였습니까?</title>
    <link>https://hik-coding.tistory.com/</link>
    <description>당신은 스스로 개발 공부를 한적이 있습니까?</description>
    <language>ko</language>
    <pubDate>Tue, 21 Jul 2026 20:32:20 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>hik14</managingEditor>
    <image>
      <title>오늘도 더 나은 코드를 작성하였습니까?</title>
      <url>https://tistory1.daumcdn.net/tistory/4094934/attach/2f15009a8753417fb907452693acdf14</url>
      <link>https://hik-coding.tistory.com</link>
    </image>
    <item>
      <title>State Pattern(feat. kotlin)</title>
      <link>https://hik-coding.tistory.com/376</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정의&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;State Pattern은 객체의 내부 상태에 따라 행동이 바뀌어야 할 때 사용하는 행동 디자인 패턴입니다. 객체가 마치 자신의 클래스를 바꾼 것처럼 보이게 만드는 게 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제 사용하는지?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;when이나 if-else로 상태를 분기하는 코드가 여러 메서드에 흩어져 있을 때가 가장 명확한 신호입니다. 예를 들어 결제 화면에서 PENDING, PROCESSING, SUCCESS, FAILED 상태마다 버튼 클릭, 화면 이탈, 재시도 동작이 다르다면 &amp;mdash; 각 메서드마다 when (state) 분기가 반복됩니다. 상태가 하나 추가될 때마다 모든 메서드를 수정해야 하니 OCP(개방-폐쇄 원칙)를 위반하게 되고, 수정범위가 많아진다. &quot;그냥 enum class XXXStatus로 충분하지 않나?&quot; 싶을 수 있는데, 명확한 차이가 존재한다.&lt;b&gt; enum + when 분기는 상태 값만 표현하지만, State 패턴은 상태 + 그 상태의 행동을 함께 캡슐화합니다&lt;/b&gt;. 각 상태마다 허용/금지되는 행동이 많고, 전이 규칙이 복잡할수록 State 패턴의 가치가 커집니다. 반대로 상태가 단순한 라벨 역할만 한다면 enum이 더 적절합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트레이드오프&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점: 상태별 행동이 응집됨, 새 상태 추가가 OCP를 따름, 거대한 when 블록 제거&lt;/li&gt;
&lt;li&gt;단점: 클래스가 늘어남, 상태 전이의 흐름을 따라가려면 여러 파일을 봐야 함 (특히 상태가 4~5개 이상일 때)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클래스 다이어그램&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-21 오후 1.02.46.png&quot; data-origin-width=&quot;1902&quot; data-origin-height=&quot;1760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEoMfp/dJMcaak3h00/iW5epNjnLPZkOfsKmmFKK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEoMfp/dJMcaak3h00/iW5epNjnLPZkOfsKmmFKK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEoMfp/dJMcaak3h00/iW5epNjnLPZkOfsKmmFKK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEoMfp%2FdJMcaak3h00%2FiW5epNjnLPZkOfsKmmFKK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;673&quot; height=&quot;623&quot; data-filename=&quot;스크린샷 2026-05-21 오후 1.02.46.png&quot; data-origin-width=&quot;1902&quot; data-origin-height=&quot;1760&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Context&lt;/b&gt;: 현재 상태를 들고 있는 객체. 외부에서 보는 인터페이스.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;State (인터페이스)&lt;/b&gt;: 상태별로 달라지는 행동을 정의 및 캡슐화.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ConcreteState&lt;/b&gt;: 각 상태의 구체적인 행동 구현. 필요하면 다음 상태로의 전이도 담당.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779347263519&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ============================================================
// State 인터페이스
// ============================================================
sealed interface PaymentState {
    fun pay(context: PaymentContext)
    fun approve(context: PaymentContext)
    fun fail(context: PaymentContext, reason: String)
    fun retry(context: PaymentContext)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779347570706&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.example

// ============================================================
// ConcreteStates
// ============================================================

// 결제 대기
object IdleState : PaymentState {
    override fun pay(context: PaymentContext) {
        println(&quot;[${context.orderId}] 결제 요청 시작&quot;)
        context.transitionTo(ProcessingState)
    }
    override fun approve(context: PaymentContext) = reject(&quot;결제 시작 전입니다&quot;)
    override fun fail(context: PaymentContext, reason: String) = reject(&quot;결제 시작 전입니다&quot;)
    override fun retry(context: PaymentContext) = reject(&quot;재시도할 결제가 없습니다&quot;)
}

// 결제 처리 중 (PG사 응답 대기)
object ProcessingState : PaymentState {
    override fun pay(context: PaymentContext) = reject(&quot;이미 처리 중입니다 (중복 요청 차단)&quot;)

    override fun approve(context: PaymentContext) {
        println(&quot;[${context.orderId}] PG 승인 완료&quot;)
        context.transitionTo(SuccessState)
    }

    override fun fail(context: PaymentContext, reason: String) {
        println(&quot;[${context.orderId}] 결제 실패: $reason&quot;)
        context.transitionTo(FailedState)
    }

    override fun retry(context: PaymentContext) = reject(&quot;처리 중에는 재시도할 수 없습니다&quot;)
}

// 결제 실패
object FailedState : PaymentState {
    override fun pay(context: PaymentContext) = reject(&quot;실패 상태에서는 retry()로 재시도하세요&quot;)
    override fun approve(context: PaymentContext) = reject(&quot;실패한 결제는 승인할 수 없습니다&quot;)
    override fun fail(context: PaymentContext, reason: String) = reject(&quot;이미 실패 상태입니다&quot;)

    override fun retry(context: PaymentContext) {
        println(&quot;[${context.orderId}] 재시도 시작&quot;)
        context.transitionTo(ProcessingState)
    }
}

// 결제 성공 (종료 상태)
object SuccessState : PaymentState {
    override fun pay(context: PaymentContext)                   = reject(&quot;이미 완료된 결제입니다&quot;)
    override fun approve(context: PaymentContext)               = reject(&quot;이미 승인된 결제입니다&quot;)
    override fun fail(context: PaymentContext, reason: String)  = reject(&quot;이미 완료된 결제입니다&quot;)
    override fun retry(context: PaymentContext)                 = reject(&quot;완료된 결제는 재시도할 수 없습니다&quot;)
}

private fun reject(msg: String) {
    println(&quot;  ↳ 거부: $msg&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779347631038&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ============================================================
// Context
// ============================================================
class PaymentContext(val orderId: String) {

    var state: PaymentState = IdleState
        private set

    fun transitionTo(next: PaymentState) {
        println(&quot;[$orderId] ${state::class.simpleName} &amp;rarr; ${next::class.simpleName}&quot;)
        state = next
    }

    // 외부에서 호출하는 행동 &amp;mdash; 모두 현재 state에 위임
    fun pay() = state.pay(this)
    fun approve() = state.approve(this)
    fun fail(reason: String) = state.fail(this, reason)
    fun retry() = state.retry(this)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779347928103&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    // 시나리오 1: 정상 흐름
    val payment1 = PaymentContext(&quot;ORDER-001&quot;)
    payment1.pay()       // Idle &amp;rarr; Processing
    payment1.pay()       // 거부: 이미 처리 중 (중복 차단)
    payment1.approve()   // Processing &amp;rarr; Success
    payment1.retry()     // 거부: 완료된 결제

    println()

    // 시나리오 2: 실패 후 재시도
    val payment2 = PaymentContext(&quot;ORDER-002&quot;)
    payment2.pay()                      // Idle &amp;rarr; Processing
    payment2.fail(&quot;카드 한도 초과&quot;)      // Processing &amp;rarr; Failed
    payment2.retry()                    // Failed &amp;rarr; Processing
    payment2.approve()                  // Processing &amp;rarr; Success
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-21 오후 4.14.31.png&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIjnHU/dJMcah5BDSm/FumKisqSSMx5cFTGADnCIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIjnHU/dJMcah5BDSm/FumKisqSSMx5cFTGADnCIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIjnHU/dJMcah5BDSm/FumKisqSSMx5cFTGADnCIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIjnHU%2FdJMcah5BDSm%2FFumKisqSSMx5cFTGADnCIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1212&quot; height=&quot;954&quot; data-filename=&quot;스크린샷 2026-05-21 오후 4.14.31.png&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중복 결제 방지가 됨&lt;/b&gt;: ProcessingState.pay()가 reject로 처리하기 때문에, UI에서 사용자가 결제 버튼을 두 번 눌러도 두 번째 요청은 자연스럽게 차단됩니다. Context 쪽에 if (isProcessing) return 같은 플래그 체크를 흩어둘 필요가 없어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전이 규칙이 한 곳에 모임&lt;/b&gt;: &quot;어떤 상태에서 어떤 액션이 허용되는가&quot;가 각 State 객체의 메서드 구현으로 표현됩니다. 새 상태(예: CancelledState, RefundedState)를 추가할 때 기존 코드를 거의 건드리지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;sealed interface + object&lt;/b&gt; 조합: 상태가 내부 데이터를 갖지 않으므로 object(싱글톤)로 충분합니다. 만약 실패 사유나 트랜잭션 ID 같은 상태별 데이터가 필요하다면 object 대신 data class로 바꾸고, sealed interface가 컴파일러의 when exhaustive 체크까지 받쳐줍니다.&lt;/p&gt;</description>
      <category>디자인패턴</category>
      <author>hik14</author>
      <guid isPermaLink="true">https://hik-coding.tistory.com/376</guid>
      <comments>https://hik-coding.tistory.com/376#entry376comment</comments>
      <pubDate>Thu, 21 May 2026 16:21:32 +0900</pubDate>
    </item>
    <item>
      <title>안드로이드 개발에서 사용하는 캐싱 전략2</title>
      <link>https://hik-coding.tistory.com/375</link>
      <description>&lt;div&gt;
&lt;div&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Room Offline Cache&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Room을 SSOT(Single Source of Truth)로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;항상 Room에서만 읽고&lt;/b&gt;, 네트워크는 Room을 업데이트하는 용도로만 씁니다.&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-19 오후 4.10.49.png&quot; data-origin-width=&quot;2476&quot; data-origin-height=&quot;1302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vN7YH/dJMcadWlBDS/sjJA150F0Rd2MoHOfUQhak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vN7YH/dJMcadWlBDS/sjJA150F0Rd2MoHOfUQhak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vN7YH/dJMcadWlBDS/sjJA150F0Rd2MoHOfUQhak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvN7YH%2FdJMcadWlBDS%2FsjJA150F0Rd2MoHOfUQhak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2476&quot; height=&quot;1302&quot; data-filename=&quot;스크린샷 2026-05-19 오후 4.10.49.png&quot; data-origin-width=&quot;2476&quot; data-origin-height=&quot;1302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오프라인 자동 지원&lt;/li&gt;
&lt;li&gt;로딩/에러와 데이터 표시 분리 가능&lt;/li&gt;
&lt;li&gt;네트워크 실패해도 캐시된 화면 유지&lt;/li&gt;
&lt;li&gt;화면 회전, 다른 화면에서 돌아와도 즉시 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779175304695&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity(tableName = &quot;orders&quot;)
data class OrderEntity(
    @PrimaryKey val id: String,
    val amount: Long,
    val status: String,
    val createdAt: Long,

    // --- 캐시 메타데이터 ---
    val cachedAt: Long,           // 캐시 시각
    val etag: String? = null,     // 서버 ETag (HTTP 304 활용)
    val isStale: Boolean = false  // 강제 새로고침 필요 플래그
)

@Dao
interface OrderDao {
    @Query(&quot;SELECT * FROM orders ORDER BY createdAt DESC&quot;)
    fun observeAll(): Flow&amp;lt;List&amp;lt;OrderEntity&amp;gt;&amp;gt;

    @Query(&quot;SELECT * FROM orders WHERE id = :id&quot;)
    fun observeById(id: String): Flow&amp;lt;OrderEntity?&amp;gt;

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsertAll(orders: List&amp;lt;OrderEntity&amp;gt;)

    @Query(&quot;SELECT MAX(cachedAt) FROM orders&quot;)
    suspend fun lastCachedAt(): Long?

    @Query(&quot;DELETE FROM orders WHERE cachedAt &amp;lt; :threshold&quot;)
    suspend fun deleteOlderThan(threshold: Long)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;여러가지 패턴들&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패턴 A: Cache-First (캐시 우선)&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;캐시가 신선하면 그냥 보여주고, 오래됐을 때만 새로 가져온다&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 순서&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;UI가 데이터를 요청한다&lt;/li&gt;
&lt;li&gt;Room의 캐시를 확인한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐시의 cachedAt(저장 시각)을 본다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;현재 시각과 비교해서 TTL(예: 5분) 이내면 &amp;rarr; &lt;b&gt;캐시 그대로 반환, 끝&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;TTL이 지났으면 &amp;rarr; 네트워크 호출 &amp;rarr; Room 갱신 &amp;rarr; 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠름 (대부분의 경우 네트워크 안 탐)&lt;/li&gt;
&lt;li&gt;데이터가 5분 정도 오래돼도 괜찮은 화면에 적합&lt;/li&gt;
&lt;li&gt;첫 진입 시에도 캐시가 있으면 네트워크 호출 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적합한 화면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품 카테고리 목록 (자주 안 바뀜)&lt;/li&gt;
&lt;li&gt;사용자 프로필&lt;/li&gt;
&lt;li&gt;공지사항&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부적합한 화면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간 잔액, 재고&lt;/li&gt;
&lt;li&gt;결제 직전 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-test-render-count=&quot;1&quot;&gt;
&lt;div&gt;
&lt;div data-is-streaming=&quot;false&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패턴 B: NetworkBoundResource (캐시 + 네트워크 동시 노출)&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;캐시를 먼저 보여주고, 네트워크 결과는 로딩 상태와 함께 따로 알려준다&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 순서&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;UI가 데이터를 요청한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;즉시 캐시를 Loading(데이터 있음) 상태로 내보낸다&lt;/b&gt; &amp;rarr; 사용자는 일단 화면을 본다&lt;/li&gt;
&lt;li&gt;동시에 캐시가 신선한지 판단한다(shouldFetch)&lt;/li&gt;
&lt;li&gt;신선하지 않으면 네트워크 호출
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성공: Room에 저장 &amp;rarr; Flow가 자동으로 Success로 전환&lt;/li&gt;
&lt;li&gt;실패: Error(이전 캐시 데이터 포함)로 내보냄 &amp;rarr; 사용자는 여전히 화면을 봄&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;신선하면 그대로 Success 유지&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI가 &lt;b&gt;로딩/성공/에러 + 데이터&lt;/b&gt;를 한 번에 받는다&lt;/li&gt;
&lt;li&gt;로딩 인디케이터를 띄우면서도 이전 데이터를 계속 보여줄 수 있음 (Pull-to-refresh UX)&lt;/li&gt;
&lt;li&gt;에러가 나도 캐시는 살아있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적합한 화면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;피드, 타임라인&lt;/li&gt;
&lt;li&gt;새로고침 가능한 리스트 화면&lt;/li&gt;
&lt;li&gt;검색 결과&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;핵심 가치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;로딩 중에 화면이 비어 보이는 문제&quot;를 해결합니다. 캐시가 있으면 그걸로 즉시 채우고, 진짜 데이터는 뒤에서 채워옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패턴 C: Cache-Then-Network / Stale-While-Revalidate (SWR)&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;오래된 캐시라도 즉시 보여주고, 뒤에서 조용히 새로 가져온다&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 순서&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;UI가 데이터를 요청한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐시를 즉시 내보낸다 (신선도 따지지 않음)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;동시에 백그라운드에서 네트워크 호출&lt;/li&gt;
&lt;li&gt;네트워크 성공 &amp;rarr; Room 갱신 &amp;rarr; Flow가 자동으로 새 데이터로 업데이트&lt;/li&gt;
&lt;li&gt;네트워크 실패 &amp;rarr; &lt;b&gt;조용히 무시&lt;/b&gt; (이미 캐시는 보여주고 있으니까)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 빠른 첫 화면 표시&lt;/li&gt;
&lt;li&gt;사용자 입장에서는 &quot;즉시 떴다가, 잠시 후 살짝 갱신되는&quot; 경험&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 에러를 UI에 노출하지 않음 (좋은 점이자 나쁜 점)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 41.6279%; height: 91px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 14.6512%; text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 12.7907%; text-align: center;&quot;&gt;패턴 B&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 13.9535%; text-align: center;&quot;&gt;패턴 C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 14.6512%; text-align: center;&quot;&gt;캐시 신선도 체크&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 12.7907%; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 13.9535%; text-align: center;&quot;&gt;X (무조건 갱신)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 14.6512%; text-align: center;&quot;&gt;로딩 상태 노출&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 12.7907%; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 13.9535%; text-align: center;&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 14.6512%; text-align: center;&quot;&gt;에러 노출&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 12.7907%; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 13.9535%; text-align: center;&quot;&gt;X (조용히 실패)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적합한 화면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;홈 피드, 대시보드&lt;/li&gt;
&lt;li&gt;이미지 썸네일 리스트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자주 보지만 정확도가 절대적이지 않은 화면&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부적합한 화면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 &quot;새로고침했는데 안 됐다&quot;는 걸 알아야 하는 화면&lt;/li&gt;
&lt;li&gt;결제, 금액 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패턴 D: Network-First (네트워크 우선, 결제 도메인 권장)&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;무조건 서버에서 최신 데이터를 받는다. 캐시는 fallback도 안 한다&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 순서&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;UI가 데이터를 요청한다 (예: 결제 진행 직전 주문 정보)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무조건 네트워크 호출&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;성공 &amp;rarr; Room 갱신 &amp;rarr; 반환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실패 &amp;rarr; 캐시로 폴백하지 않고 그냥 에러&lt;/b&gt; (&quot;네트워크 필요&quot; 화면 표시)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 캐시로 fallback하면 안 되는가&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결제 화면에서 상품 가격이 캐시 시점(예: 5분 전)에는 10,000원이었는데, 지금은 12,000원으로 바뀌었다면?&lt;/li&gt;
&lt;li&gt;캐시를 보여주고 결제 진행하면 &amp;rarr; &lt;b&gt;사용자가 잘못된 금액을 승인하게 됨&lt;/b&gt; &amp;rarr; 분쟁/환불 사유&lt;/li&gt;
&lt;li&gt;재고도 마찬가지:&amp;nbsp; 캐시에는 &quot;재고 있음&quot;이었지만 지금은 품절일 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 느린 패턴 (항상 네트워크 대기)&lt;/li&gt;
&lt;li&gt;가장 정확한 패턴&lt;/li&gt;
&lt;li&gt;오프라인이면 진행 불가 (의도된 동작)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적합한 시나리오&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결제 직전 최종 확인 화면&lt;/li&gt;
&lt;li&gt;잔액 조회&lt;/li&gt;
&lt;li&gt;한도 확인&lt;/li&gt;
&lt;li&gt;주문 생성 직전&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&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;A. Cache-First&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;B. NetworkBoundResource&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;C. SWR (Cache-Then-Network)&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;D. Network-First&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;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴 선택 의사결정 순서&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이 데이터가 stale이면 사용자에게 실질적 피해(금전적/거래 분쟁)가 발생하는가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Yes &amp;rarr; 패턴 D (Network-First)&lt;/li&gt;
&lt;li&gt;No &amp;rarr; 다음 단계로&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용자가 새로고침 결과(성공/실패)를 명확히 알아야 하는가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Yes &amp;rarr; 패턴 B (NetworkBoundResource)&lt;/li&gt;
&lt;li&gt;No &amp;rarr; 다음 단계로&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;첫 화면을 즉시 보여주는 게 가장 중요한가? 약간 오래된 데이터도 OK인가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Yes &amp;rarr; 패턴 C (SWR)&lt;/li&gt;
&lt;li&gt;No &amp;rarr; 패턴 A (Cache-First)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;결제 앱에서의 조합 예시&lt;/h4&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 143px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;화면&lt;/td&gt;
&lt;td&gt;패턴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;홈 (최근 거래, 배너)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;C - SWR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;거래 내역 리스트&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;B - NetworkBoundResource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;거래 상세 보기&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;A - Cache-First (5분 TTL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;잔액 표시&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;D - Network-First&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;결제 진행 화면&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;D - Network-First&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;상품 카테고리&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;A - Cache-First (1시간 TTL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;사용자 프로필&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;A - Cache-First&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 패턴은 &quot;어느 게 좋다&quot;가 아니라 &lt;b&gt;화면별 요구사항에 따라 다르게 적용&lt;/b&gt;하는 것입니다. 핵심은 &lt;b&gt;두 가지 축&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;속도&lt;/b&gt; (캐시 즉시 노출) vs &lt;b&gt;정확성&lt;/b&gt; (서버 우선)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 노출&lt;/b&gt; (사용자에게 알림) vs &lt;b&gt;에러 숨김&lt;/b&gt; (조용히 처리)&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-state=&quot;closed&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-chat-input-container=&quot;true&quot;&gt;
&lt;div aria-hidden=&quot;true&quot;&gt;
&lt;div data-state=&quot;closed&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;캐시 만료 정책 (Cache Expiration Policy)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시된 데이터를 &lt;b&gt;언제 &quot;더 이상 못 믿겠다&quot;고 판단할지&lt;/b&gt; 정하는 규칙입니다. 만료 정책 없이 캐시만 두면 영원히 오래된 데이터를 보여주게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. TTL (Time-To-Live) &amp;mdash; 시간 기반 만료&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;저장한 지 N분 지나면 만료&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 흔하고 단순한 정책입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div data-alert-band-wrapper=&quot;true&quot;&gt;동작&lt;/div&gt;
&lt;div data-alert-band-wrapper=&quot;true&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-alert-band-wrapper=&quot;true&quot;&gt;12:00&amp;nbsp;&amp;nbsp;캐시&amp;nbsp;저장&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;&amp;nbsp;expiresAt&amp;nbsp;=&amp;nbsp;12:05&lt;br /&gt;12:03&amp;nbsp;&amp;nbsp;요청&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;&amp;nbsp;아직&amp;nbsp;12:05&amp;nbsp;안&amp;nbsp;됨&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;&amp;nbsp;캐시&amp;nbsp;사용&lt;br /&gt;12:06&amp;nbsp;&amp;nbsp;요청&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;&amp;nbsp;12:05&amp;nbsp;지났음&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;&amp;nbsp;만료,&amp;nbsp;새로&amp;nbsp;가져옴&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL 길이 선택 기준&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;데이터 종류권장&lt;/td&gt;
&lt;td&gt;TTL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;거의 안 바뀌는 메타데이터 (카테고리, 약관)&lt;/td&gt;
&lt;td&gt;1시간 ~ 1일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가끔 바뀌는 정보 (프로필, 설정)&lt;/td&gt;
&lt;td&gt;5~30분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;자주 바뀌는 정보 (피드, 알림)&lt;/td&gt;
&lt;td&gt;30초 ~ 5분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실시간 데이터 (잔액, 재고)&lt;/td&gt;
&lt;td&gt;TTL 쓰지 않음 (Network-First)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. TTI (Time-To-Idle) &amp;mdash; 마지막 접근 기준&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;자주 보는 데이터는 살려두고, 안 보는 데이터만 비움&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;동작&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;12:00 캐시 저장 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt;12:03 읽음 &amp;rarr; 만료 카운터 리셋 &lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt;12:08 읽음 &amp;rarr; 만료 카운터 리셋&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt;(10분지남)&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt;12:18 10분간 안 읽힘 &amp;rarr; 만료&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적합한 데이터&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 캐시(LruCache, Caffeine)&lt;/li&gt;
&lt;li&gt;자주 보는 항목은 계속 살려두고 싶을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 74px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;TTL&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;TTI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;기준&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;저장 시각&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;마지막 접근 시각&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;자주 접근하면?&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;그래도 만료됨&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;계속 살아있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;신선도 보장&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;강함&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;약함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. ETag / Last-Modified &amp;mdash; 서버 검증 기반&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;TTL 지나면 서버에 '바뀐 거 있어?'라고 물어봄&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;캐시&amp;nbsp;만료&amp;nbsp;(TTL&amp;nbsp;지남)&lt;br /&gt;2.&amp;nbsp;서버에&amp;nbsp;요청&amp;nbsp;보낼&amp;nbsp;때&amp;nbsp;헤더&amp;nbsp;추가:&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;If-None-Match:&amp;nbsp;&quot;abc123&quot;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;larr;&amp;nbsp;내가&amp;nbsp;가진&amp;nbsp;ETag&lt;br /&gt;3.&amp;nbsp;서버&amp;nbsp;응답:&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;304&amp;nbsp;Not&amp;nbsp;Modified&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;&amp;nbsp;본문&amp;nbsp;없음,&amp;nbsp;&quot;안&amp;nbsp;바뀜&quot;&amp;nbsp;&amp;rarr;&amp;nbsp;캐시&amp;nbsp;그대로&amp;nbsp;사용&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;200&amp;nbsp;OK&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;&amp;nbsp;새&amp;nbsp;본문&amp;nbsp;+&amp;nbsp;새&amp;nbsp;ETag&amp;nbsp;&amp;rarr;&amp;nbsp;캐시&amp;nbsp;갱신&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안 바뀐 데이터는 본문 다운로드 안 함 &amp;rarr; &lt;b&gt;트래픽 절약&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;TTL보다 정확함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적합한 데이터&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큰 데이터 (이미지, 상세 정보)&lt;/li&gt;
&lt;li&gt;변경 빈도가 낮지만 정확성도 필요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 이벤트 기반 무효화 &amp;mdash; 능동적 만료 (안드로이드에서&amp;nbsp; 개발자가 가장 많이 신경써야 하는 부분)&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;특정 사건이 일어나면 캐시를 즉시 stale로 표시&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 아니라 &lt;b&gt;사건&lt;/b&gt;으로 만료시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리거 예시&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 133px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;이벤트&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;무효화 대상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;사용자 로그아웃&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;사용자 관련 캐시 전부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;주문 생성 완료&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;주문 리스트 캐시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;결제 성공&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;잔액, 거래 내역 캐시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;푸시 알림 수신&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;알림 관련 캐시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Pull-to-refresh&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;해당 화면 캐시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;앱이 백그라운드 &amp;rarr; 포그라운드&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;홈 화면 캐시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 방식 두 가지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A. 즉시 삭제&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;이벤트 발생 &amp;rarr; dao.deleteAll() &amp;rarr; 다음 요청 시 새로 가져옴&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;B. Stale 플래그만 세움&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;이벤트&amp;nbsp;발생&amp;nbsp;&amp;rarr;&amp;nbsp;dao.markAllStale()&amp;nbsp;(isStale=true)&lt;br /&gt;다음&amp;nbsp;요청&amp;nbsp;시&amp;nbsp;isStale&amp;nbsp;보고&amp;nbsp;새로&amp;nbsp;가져옴&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*B 방식은 캐시를 즉시 비우지 않아 백그라운드 갱신 동안 이전 데이터를 보여줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. Stale-While-Revalidate (SWR)&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;만료됐어도 일단 보여주고, 뒤에서 새로 가져온다&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엄밀히는 만료 정책이라기보다 &lt;b&gt;만료된 데이터의 활용 정책&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시&amp;nbsp;상태:&amp;nbsp;stale&amp;nbsp;(TTL&amp;nbsp;지남)&lt;br /&gt;요청 시&lt;br /&gt;&amp;nbsp;&amp;nbsp;1.&amp;nbsp;일단&amp;nbsp;stale&amp;nbsp;데이터를&amp;nbsp;즉시&amp;nbsp;반환&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;&amp;nbsp;사용자는&amp;nbsp;화면을&amp;nbsp;본다&lt;br /&gt;&amp;nbsp;&amp;nbsp;2.&amp;nbsp;동시에&amp;nbsp;백그라운드에서&amp;nbsp;fetch&lt;br /&gt;&amp;nbsp;&amp;nbsp;3.&amp;nbsp;fetch&amp;nbsp;결과로&amp;nbsp;캐시&amp;nbsp;갱신&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;&amp;nbsp;Flow를&amp;nbsp;통해&amp;nbsp;UI&amp;nbsp;자동&amp;nbsp;업데이트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적합한 데이터&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 화면 속도가 중요한 경우&lt;/li&gt;
&lt;li&gt;약간 오래돼도 큰 문제 없는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;캐시 정책별 빈도&amp;middot;중요도&amp;middot;구현 주체 매트릭스&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;정책 코드 등장 빈도.&amp;nbsp; 사용자 체감 중요도빠지면 생기는 일구현 주체
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 133px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;정책&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;코드 등장 빈도&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;사용자 체감 중요도&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;빠지면 생기는 일&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;구현 주체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;LRU&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;매우 높음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;낮음 (자동)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;메모리 부족, OOM&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;라이브러리 (Coil/Glide, OkHttp, LruCache, Caffeine)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;TTL&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;매우 높음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;중간&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;데이터가 영원히 안 갱신됨&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;반반 (HTTP는 서버+OkHttp / Room 캐시는 개발자)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;이벤트 기반&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;높음&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;매우 높음&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;즉각적인 버그, 클레임&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;개발자 (도메인 로직이라 자동화 불가)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;ETag&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;중간&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;낮음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;트래픽 증가&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;반반 (서버가 헤더 주면 OkHttp/Ktor가 자동 처리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;SWR&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;중간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;중간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;첫 화면 느림&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;개발자 (Repository에서 흐름 직접 설계)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>hik14</author>
      <guid isPermaLink="true">https://hik-coding.tistory.com/375</guid>
      <comments>https://hik-coding.tistory.com/375#entry375comment</comments>
      <pubDate>Tue, 19 May 2026 17:26:52 +0900</pubDate>
    </item>
    <item>
      <title>안드로이드 개발에서 사용하는 캐싱 전략1</title>
      <link>https://hik-coding.tistory.com/374</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Cache란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시(Cache)는 자주 사용되는 데이터를 빠르게 접근할 수 있는 임시 저장 공간에 보관하는 기술입니다. 원본 데이터 소스(디스크, 네트워크, DB 등)보다 접근 속도가 빠른 곳(메모리 등)에 데이터를 두어&lt;b&gt; 반복적인 요청의 응답 시간을 단축&lt;/b&gt;시킵니다. Android에서는 LruCache(메모리 캐시), OkHttp Cache(네트워크 응답 캐시), Room 등을 활용해 성능과 사용자 경험을 개선합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;in-memory cache&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In-memory cache는 앱 프로세스의 RAM에 데이터를 보관하는 방식으로, 접근 속도가 매우 빠르지만 앱 종료나 OS의 메모리 회수 시 사라집니다. 용도에 따라 적절한 도구를 선택하는 것이 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 이미지 캐싱 (Coil / Glide)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이미지는 크기가 크고 디코딩 비용이 비싸기 때문에 전용 라이브러리를 쓰는 것이 표준입니다. 두 라이브러리 모두 &lt;b&gt;메모리 캐시 + 디스크 캐시 2단계&lt;/b&gt;를 자동으로 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Coil&lt;/p&gt;
&lt;pre id=&quot;code_1779157337953&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기본 사용 - 캐시 자동 처리
AsyncImage(model = url, contentDescription = null)

// 전역 설정
val imageLoader = ImageLoader.Builder(context)
    .memoryCache {
        MemoryCache.Builder(context)
            .maxSizePercent(0.25)  // 가용 메모리의 25%
            .build()
    }
    .build()

// 캐시 정책 개별 제어
imageView.load(url) {
    memoryCachePolicy(CachePolicy.ENABLED)
    diskCachePolicy(CachePolicy.ENABLED)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Glide&lt;/p&gt;
&lt;pre id=&quot;code_1779157423044&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Glide.with(context)
    .load(url)
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .skipMemoryCache(false)
    .into(imageView)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Coroutine / Flow 기반 패턴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리 없이 직접 캐시 레이어를 만들 때 자주 쓰는 패턴. API 호출시에 사용한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779158883692&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserRepository {
    private val _users = MutableStateFlow&amp;lt;Map&amp;lt;String, User&amp;gt;&amp;gt;(emptyMap())
    val users: StateFlow&amp;lt;Map&amp;lt;String, User&amp;gt;&amp;gt; = _users.asStateFlow()

    suspend fun getUser(id: String): User {
        return _users.value[id] ?: api.fetchUser(id).also { user -&amp;gt;
            _users.update { it + (id to user) }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Paging3 - cachedIn&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;cachedIn(scope)의 scope가 캐시의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;생명주기&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;를 결정한다. 일반적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;viewModelScope 캐싱한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Configuration Change 시 처음부터 다시 로드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 100번째 아이템까지 스크롤한 상태에서 &lt;b&gt;화면을 회전할때 &lt;/b&gt;cachedIn 없다면&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Activity가 재생성됨&lt;/li&gt;
&lt;li&gt;Fragment/Activity/Compose가 다시 collectLatest로 구독&lt;/li&gt;
&lt;li&gt;cold flow이므로 &lt;b&gt;PagingSource가 처음부터 다시 실행&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;1페이지부터 다시 로드, 사용자는 맨 위로 돌아감&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cachedIn(viewModelScope)로 사용&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Activity가 재생성됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ViewModel은 살아있고, 캐시된 PagingData도 메모리에 있음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;새 구독자는 이미 로드된 100개 데이터를 그대로 받음&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1779165238624&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ProductViewModel(repo: ProductRepository) : ViewModel() {
    val products: Flow&amp;lt;PagingData&amp;lt;Product&amp;gt;&amp;gt; = Pager(
        config = PagingConfig(pageSize = 20),
        pagingSourceFactory = { repo.getProductPagingSource() }
    ).flow
        .cachedIn(viewModelScope)  // &amp;larr; 핵심
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 구독자가 있으면 크래시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 Flow&amp;lt;PagingData&amp;gt;를 두 곳에서 구독하면 Paging3는 &lt;b&gt;런타임 예외&lt;/b&gt;를 던집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;cachedIn은 내부적으로 SharedFlow 형태로 변환해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;여러 구독자가 같은 데이터를 공유&lt;/b&gt;할 수 있게 만들어줍니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1779165551251&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ⚠️ cachedIn 없이
viewModel.products.collectLatest { adapter1.submitData(it) }
viewModel.products.collectLatest { adapter2.submitData(it) }  //   크래시&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;viewModelScope에 캐싱했기 때문에 &lt;b&gt;ViewModel이 살아있는 동안 PagingData가 메모리에 유지&lt;/b&gt;됩니다. ViewModel이 onCleared()로 사라지면 캐시도 함께 정리되어 메모리 누수가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;DataStore(Sharedpreferences) cache&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataStore는 &lt;b&gt;SharedPreferences의 후속&lt;/b&gt;으로, 작은 키-값 데이터를 &lt;b&gt;비동기로 안전하게 저장&lt;/b&gt;하기 위한 라이브러리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시라고 부르긴 하지만 실제로는 &lt;b&gt;영속 저장소&lt;/b&gt;이며, 그 특성을 이해해야 어떤 데이터를 둘지 결정할 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 61.0465%; height: 127px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: left;&quot;&gt;특성&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: left;&quot;&gt;내용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;저장 방식&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;파일 기반 (Preferences는 XML 유사, Proto는 바이너리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;접근&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Flow 기반 비동기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;트랜잭션&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;O (원자적 업데이트 보장)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 13px;&quot;&gt;
&lt;td style=&quot;height: 13px;&quot;&gt;크기 한계&lt;/td&gt;
&lt;td style=&quot;height: 13px;&quot;&gt;&lt;b&gt;전체 파일을 메모리에 로드&lt;/b&gt; &amp;mdash; 작아야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;쿼리&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;불가 (전체 읽기만)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: left;&quot;&gt;동시성&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: left;&quot;&gt;Coroutine&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 환경설정&lt;/p&gt;
&lt;pre id=&quot;code_1779171553403&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val Context.settingsDataStore by preferencesDataStore(&quot;settings&quot;)

object SettingsKeys {
    val DARK_MODE = booleanPreferencesKey(&quot;dark_mode&quot;)
    val LANGUAGE = stringPreferencesKey(&quot;language&quot;)
    val FONT_SCALE = floatPreferencesKey(&quot;font_scale&quot;)
    val NOTIFICATION_ENABLED = booleanPreferencesKey(&quot;notification_enabled&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다크모드, 언어, 폰트 크기, 알림 on/off&lt;/li&gt;
&lt;li&gt;화면별 UI 옵션상태 (탭 선택, 정렬 순서, 필터 옵션)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증/세션 메타데이터&lt;/p&gt;
&lt;pre id=&quot;code_1779171719916&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;object AuthKeys {
    val ACCESS_TOKEN = stringPreferencesKey(&quot;access_token&quot;)
    val REFRESH_TOKEN = stringPreferencesKey(&quot;refresh_token&quot;)
    val TOKEN_EXPIRES_AT = longPreferencesKey(&quot;token_expires_at&quot;)
    val USER_ID = stringPreferencesKey(&quot;user_id&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰, 토큰 만료 시간, 로그인 상태&lt;/li&gt;
&lt;li&gt;&lt;b&gt;토큰은 EncryptedSharedPreferences 또는 Android Keystore로 암호화 저장 권장&lt;/b&gt;.다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 상태 플래그&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;온보딩 완료 여부 (최초 앱실행)&lt;/li&gt;
&lt;li&gt;튜토리얼 표시 여부&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&quot;이 메시지 다시 보지 않기&quot; 플래그&lt;/li&gt;
&lt;li&gt;마지막 앱 버전 (마이그레이션 트리거용)&lt;/li&gt;
&lt;li&gt;첫 실행 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;</description>
      <author>hik14</author>
      <guid isPermaLink="true">https://hik-coding.tistory.com/374</guid>
      <comments>https://hik-coding.tistory.com/374#entry374comment</comments>
      <pubDate>Tue, 19 May 2026 16:22:27 +0900</pubDate>
    </item>
    <item>
      <title>개발자가 알아야 하는 숫자1 - Latency Numbers (Android 개발)</title>
      <link>https://hik-coding.tistory.com/373</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;전력망 60Hz에서 시작&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미국/한국 전력은 60Hz, 유럽은 50Hz&lt;/li&gt;
&lt;li&gt;1930년대 아날로그 TV가 화면 새로 고침을 전력 주파수에 맞춤&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 60Hz =&amp;gt; 1초에 60번 진동&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인간 시각과 우연히 맞음&lt;/h4&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 109px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;프레임률&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;인식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;24 fps&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;영화 (모션 블러로 보정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;30 fps&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;가끔 끊김&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;60 fps&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;대부분 부드럽다고 인식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;120 fps+&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;한계효용 급감&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인간 시각 처리 한계가 60~80Hz 근처라, 60Hz가 &quot;부드러움의 임계선&quot;과 자연스럽게 맞았음.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;1개의 프레임이 렌더링 되는 시간&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1초 = 1,000 ms 이고 프레임은 1개의 화면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;1,000&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;ms&lt;/span&gt; / 60 frames&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;=&amp;gt; 16.666... ms&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;안드로이드의 16.6ms &amp;mdash; Project Butter (2012)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 16.6ms가 &lt;b&gt;공식적인 성능 목표&lt;/b&gt;가 된 건 &lt;b&gt;2012년 Google I/O의 &quot;Project Butter&quot;&lt;/b&gt; 발표부터입니다. Android 4.1 Jelly Bean. 이전 Android는 프레임률이 들쭉날쭉했는데, Project Butter가 도입한 핵심 개념이:&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) VSync 동기화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면 새로 고침(60Hz)에 맞춰 &lt;b&gt;정확히 16.6ms마다 한 프레임씩&lt;/b&gt; 그리도록 강제. 이전엔 GPU가 빨리 그리면 빨리 보내고, 늦으면 늦게 보내서 tearing(찢김) 발생했음.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) Triple Buffering&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU, GPU, 디스플레이가 동시에 일할 수 있게 버퍼 3개를 돌림. 이걸로 16.6ms 예산을 빈틈없이 활용.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(3) Choreographer&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// Android 내부에서 16.6ms 타이밍을 관리하는 클래스
Choreographer.getInstance().postFrameCallback { frameTimeNanos -&amp;gt;
    // 매 프레임 호출되는 콜백
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Choreographer가 16.6ms마다 신호를 보내고, 그 안에 모든 그리기 작업이 끝나야 한 프레임이 정상적으로 송출&lt;/b&gt;됩니다. 못 끝내면 그 프레임은 버려지고(=프레임 드롭), =&amp;gt; 사용자는 끊김으로 인식.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이제는 60fps만이 표준이 아님&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 폰들은 더 빠른 디스플레이를 사용&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.7907%;&quot;&gt;주사율&lt;/td&gt;
&lt;td style=&quot;width: 21.5116%;&quot;&gt;한 프레임(렌더링 시간)&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;사용 기기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.7907%;&quot;&gt;60Hz&lt;/td&gt;
&lt;td style=&quot;width: 21.5116%;&quot;&gt;16.6 ms&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;일반 폰, 대부분의 디스플레이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.7907%;&quot;&gt;90Hz&lt;/td&gt;
&lt;td style=&quot;width: 21.5116%;&quot;&gt;11.1 ms&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;OnePlus 7 Pro 이후 일부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.7907%;&quot;&gt;&lt;b&gt;120Hz&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.5116%;&quot;&gt;&lt;b&gt;8.3 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;iPhone Pro, Galaxy S 시리즈, ProMotion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.7907%;&quot;&gt;144Hz&lt;/td&gt;
&lt;td style=&quot;width: 21.5116%;&quot;&gt;6.9 ms&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;게이밍 폰&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;120Hz가 점점 표준이 되고 있어서 예산이 절반으로 줄어드는 추세&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 코드가 60Hz 폰에선 부드럽고 120Hz 폰에선 끊겨 보일 수 있음&lt;/li&gt;
&lt;li&gt;고급 폰일수록 성능 요구사항이 더 빡빡해짐&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;출발점: 60fps = 한 프레임 16.6ms&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 UI가 부드럽게 보이려면 &lt;b&gt;메인 스레드가 16.6ms마다 한 프레임씩 그려내야&lt;/b&gt; 합니다. 이 시간을 넘기면 사용자에겐 즉시 jank(끊김)로 보일수 있다. (120fps 기기는 더 빡빡해서 8.3ms)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-18 오후 5.05.57.png&quot; data-origin-width=&quot;1372&quot; data-origin-height=&quot;1238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uP1tz/dJMcaarPhpN/d0cLtugZHCIrWIZjpIFDfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uP1tz/dJMcaarPhpN/d0cLtugZHCIrWIZjpIFDfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uP1tz/dJMcaarPhpN/d0cLtugZHCIrWIZjpIFDfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuP1tz%2FdJMcaarPhpN%2Fd0cLtugZHCIrWIZjpIFDfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1372&quot; height=&quot;1238&quot; data-filename=&quot;스크린샷 2026-05-18 오후 5.05.57.png&quot; data-origin-width=&quot;1372&quot; data-origin-height=&quot;1238&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시스템이 7~14ms를 먼저 가져갑니다.&lt;/b&gt; 복잡한 레이아웃은 더 많은 시간을 소요할수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 스레드 최적화는 두 가지 방법.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;시스템 시간을 줄인다&lt;/b&gt; &amp;rarr; 레이아웃 단순화, 뷰 계층 평탄화, ConstraintLayout 활용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;우리 코드 시간을 줄인다&lt;/b&gt; &amp;rarr; 비동기로 빼기, 데이터 미리 준비, 캐싱&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;실무에서 중요한것은 무엇일까?&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 209px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;작업&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;1번 비용&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;예산(3~8ms) 안 가능 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;메모리 접근 (HashMap get)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;100 ns&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;30,000~80,000번&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;StateFlow value 읽기&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;~200 ns&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;15,000~40,000번&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Room 단순 쿼리 (suspend)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;~50-100 &amp;mu;s&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;30~160번&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Room JOIN/@Relation&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;~300 &amp;mu;s&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;약 10~25번&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;SharedPreferences apply()&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;~수십 &amp;mu;s (메모리만)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;수백 번&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;SharedPreferences commit()&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;10~50 ms&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;0번&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DataStore 읽기 (캐시 hit, Flow collect)&lt;/td&gt;
&lt;td&gt;~수십 &amp;mu;s&lt;/td&gt;
&lt;td&gt;수백 번(DataStore는 한번 읽으면 메모리에 캐싱)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DataStore 읽기 (첫 호출, 디스크에서 로드)&lt;/td&gt;
&lt;td&gt;~5-20 ms&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;b&gt;0~1번 (첫 호출만 )&lt;/b&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DataStore 쓰기 (edit { })&lt;/td&gt;
&lt;td&gt;~5-20 ms&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;b&gt;0번&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;이미지 디코딩 (큰 이미지)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;10~100 ms&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;0번&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;큰 JSON 파싱&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;5~50 ms&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;0번&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;서버 API 호출&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;50ms+&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;0번&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;디스크 동기 I/O (큰 파일)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;수십 ms&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;0번&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&quot;0번&quot;으로 표시된 작업은 메인 스레드 금지&lt;/b&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1. 서버 API 호출&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표에서 가능 횟수가 &lt;b&gt;0번인 작업&lt;/b&gt;들이 메인 스레드 절대 금지 목록입니다. Coroutine 또는 다른 Thread로 작업한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1번 비용이 우리 예산(3~8ms)을 이미 넘기거나 거의 다 소모해버려서, 한 번만 실행해도 프레임이 깨집니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779094499713&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//   NetworkOnMainThreadException
val response = okHttpClient.newCall(request).execute()

// ✅ 코루틴 + Dispatchers.IO
viewModelScope.launch {
    val response = withContext(Dispatchers.IO) {
        okHttpClient.newCall(request).execute()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 이미지 디코딩 (10~100ms) &amp;rarr; 우리 예산의 1~30배&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Coil 또는 Glide 와 같은 라이브러리 사용하여 비동기 처리. 절대로 메인 스레드에서 처리하면 안됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View든 Compose든 라이브러리를 사용하여 안전하게 처리한다.&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1779094862849&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//   직접 디코딩 - 큰 이미지면 100ms+
val bitmap = BitmapFactory.decodeFile(path)
imageView.setImageBitmap(bitmap)

// ✅ Coil
AsyncImage(
    model = file,
    contentDescription = null
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 큰 JSON 파싱 (5~50ms) &amp;rarr; 우리 예산의 1~10배&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1779094977378&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ✅ Retrofit + suspend (자동으로 IO에서 실행)
@GET(&quot;/posts&quot;)
suspend fun getPosts(): List&amp;lt;Post&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. SharedPreferences.commit() (10~50ms) &amp;rarr; 우리 예산의 1~10배&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;//   메인 스레드 블로킹
prefs.edit().putString(&quot;key&quot;, value).commit()

// ✅ 비동기 디스크 쓰기 (메모리만 반영, 디스크는 백그라운드)
prefs.edit().putString(&quot;key&quot;, value).apply()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*commit()은 디스크 동기 쓰기라 10~50ms 걸리지만, apply()는 메모리만 건드리니까 수십&amp;mu;s(예산 안).&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. DataStore 쓰기 (5~20ms) &amp;rarr; 우리 예산을 거의 다 먹음&lt;/h4&gt;
&lt;pre id=&quot;code_1779096767059&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//   DataStore는 컴파일 단에서 막혀있어서 메인 호출 자체가 불가
// suspend 함수만 제공
viewModelScope.launch {
    dataStore.edit { it[KEY] = value }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataStore는 안전성을 위해 매 쓰기마다 디스크 sync 보장하니까 5~20ms =&amp;gt; suspend로만 노출돼서 강제로 비동기&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&quot;30번 이상 가능&quot;한 작업은 메인 스레드 OK&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표에서 가능 횟수가 &lt;b&gt;수백~수만 번&lt;/b&gt;인 작업들은 무조건 비동기로 뺄 필요 없어요. &lt;b&gt;모든 걸 코루틴으로 감싸는 게 능사가 아닙니다.&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 115px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;작업&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;가능 횟수&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;메인 스레드 판단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;StateFlow value 읽기&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;15,000~40,000번&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;HashMap/List get&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;30,000~80,000번&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;SharedPreferences apply()&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;수백 번&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;DataStore 읽기 (캐시 hit)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;수백 번&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;✅ (다만 API는 suspend)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Room 단순 쿼리&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;30~160번&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;⚠️ 1~2번은 OK, 반복은 위험&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ViewModel에서 StateFlow 값 읽어서 UI에 반영하는 정도는 메인 스레드 동기 처리로 충분합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 String 포맷팅, 작은 List map/filter도 마찬가지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Room 단순 쿼리는 애매한 영역. 1번이면 50&amp;mu;s라 예산에 여유 있지만, 반복문에서 호출하면 금세 한도 넘어요. 그래서 &lt;b&gt;Room은 무조건 suspend로 선언해서 컴파일러가 IO Dispatcher로 빼주게 하는 게 표준입니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RecyclerView 바인딩&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onBindViewHolder는 &lt;b&gt;한 프레임 안에 여러 번 호출&lt;/b&gt;됩니다. 그래서 표의 &quot;3~8ms 예산&quot;을 다시 &lt;b&gt;바인딩 한 번당 1~2ms&lt;/b&gt;로 더 쪼개서 봐야 해요.&lt;/p&gt;
&lt;pre id=&quot;code_1779097172433&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val item = items[position]
    
    //   동기 DB 조회 (50~100&amp;mu;s &amp;times; 반복 = 위험)
    val detail = db.getDetail(item.id)
    
    //   이미지 메인 스레드 디코딩 (10~100ms = 즉사)
    holder.image.setImageBitmap(BitmapFactory.decodeFile(item.path))
    
    //   매번 SimpleDateFormat 생성 (각 ~수십 &amp;mu;s &amp;times; N개)
    holder.date.text = SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;).format(item.date)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779097194573&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private val dateFormatter = SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;)  // 재사용

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val item = items[position]  // 메모리 접근만 (100ns)
    
    holder.image.load(item.path)        // Coil이 백그라운드 처리
    holder.date.text = dateFormatter.format(item.date)  // 포맷터 재사용
    holder.title.text = item.title       // 단순 할당
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;콜드 스타트 &amp;mdash; 16.6ms의 100~300배 예산&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 시작은 1.5~2초 안에 첫 프레임이 그려져야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.5초면 약 90배, 2초면 약 120배, 5초(ANR 경고선)면 약 300배&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표의 작업들이 누적되면, 한두 개는 OK지만 여러 개 쌓이면 1초 넘기는 경우가 많기 때문에 조심해야된다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1779097324234&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//   합치면 500ms+, 사용자는 빈 화면만 봄
class MyApp : Application() {
    override fun onCreate() {
        initFirebase()      // 100ms
        initAnalytics()     // 50ms
        initImageCache()    // 200ms (디스크 캐시 로드)
        initDatabase()      // 100ms (DB 파일 열기 ~ Room이라면 표의 &quot;디스크 동기 I/O&quot; 항목)
        initNetworking()    // 50ms
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;해결: &lt;b&gt;Jetpack App Startup&lt;/b&gt;으로 지연 초기화, Hilt @Singleton lazy 활용. &lt;b&gt;&quot;진짜 시작 시 필요한 것&quot;과 &quot;나중에 필요한 것&quot;을 분리&lt;/b&gt;.&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;//   빈 화면 50~500ms (표에서 &quot;서버 API 호출 50ms+&quot;)
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        viewModel.posts.observe(this) { /* API 응답 기다림 */ }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Room 캐시(50~100&amp;mu;s)에서 즉시 보여주고, API는 백그라운드.&lt;/b&gt; 표 기준으로 보면 &lt;b&gt;API 50ms 대신 Room 100&amp;mu;s로 500배 빠르게&lt;/b&gt; 첫 화면 표시.&lt;/p&gt;
&lt;pre id=&quot;code_1779097579804&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Repository
fun observePosts(): Flow&amp;lt;List&amp;lt;Post&amp;gt;&amp;gt; = flow {
    emitAll(postDao.observeAll())  // 즉시 emit (&amp;mu;s 단위)
}.onStart {
    refreshFromServer()  // 백그라운드 (ms 단위, 사용자는 모름)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;한 줄 요약&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메인 스레드 = 16.6ms 예산 = 우리 코드 3~8ms = 표의 &quot;수십 번 이상&quot; 작업만 자유롭게.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표 기준 0번 작업은 무조건 코루틴으로:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 &amp;rarr; suspend + Retrofit&lt;/li&gt;
&lt;li&gt;Room &amp;rarr; suspend 또는 Flow&lt;/li&gt;
&lt;li&gt;DataStore &amp;rarr; suspend (API 자체가 강제)&lt;/li&gt;
&lt;li&gt;이미지 디코딩 &amp;rarr; Coil/Glide&lt;/li&gt;
&lt;li&gt;JSON 파싱 &amp;rarr; Retrofit이 IO에서&lt;/li&gt;
&lt;li&gt;SharedPreferences &amp;rarr; commit() 금지, apply() 사용&lt;/li&gt;
&lt;li&gt;RecyclerView 바인딩 &amp;rarr; 메모리 접근/단순 할당만&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>hik14</author>
      <guid isPermaLink="true">https://hik-coding.tistory.com/373</guid>
      <comments>https://hik-coding.tistory.com/373#entry373comment</comments>
      <pubDate>Mon, 18 May 2026 18:46:32 +0900</pubDate>
    </item>
    <item>
      <title>개발자가 알아야 하는 숫자1 - Latency Numbers</title>
      <link>https://hik-coding.tistory.com/372</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-18 오전 10.51.18.png&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T1oHr/dJMcai4mZZ5/DmKVSW9K4KIhAvTSFhFun1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T1oHr/dJMcai4mZZ5/DmKVSW9K4KIhAvTSFhFun1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T1oHr/dJMcai4mZZ5/DmKVSW9K4KIhAvTSFhFun1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT1oHr%2FdJMcai4mZZ5%2FDmKVSW9K4KIhAvTSFhFun1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1844&quot; height=&quot;1038&quot; data-filename=&quot;스크린샷 2026-05-18 오전 10.51.18.png&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;1038&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 419px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;작업&lt;/td&gt;
&lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;1993 (Norvig)&lt;/td&gt;
&lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;2009 (Jeff Dean)&lt;/td&gt;
&lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;2012 (SSD 시대)&lt;/td&gt;
&lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;2024 (NVMe 시대)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;L1 캐시 참조&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;10 ns&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;0.5 ns&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;0.5 ns&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;0.5 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;분기 예측 실패&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;5 ns&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;5 ns&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;3 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;L2 캐시 참조&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;7 ns&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;7 ns&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;4 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;Mutex lock/unlock&lt;/td&gt;
&lt;td style=&quot;height: 35px; text-align: center;&quot;&gt;100 ns&lt;/td&gt;
&lt;td style=&quot;height: 35px; text-align: center;&quot;&gt;25 ns&lt;/td&gt;
&lt;td style=&quot;height: 35px; text-align: center;&quot;&gt;25 ns&lt;/td&gt;
&lt;td style=&quot;height: 35px; text-align: center;&quot;&gt;17 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;메인 메모리 참조&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;200 ns&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;100 ns&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;100 ns&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;80 ns&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;&lt;b&gt;메모리 1MB 순차 읽기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 35px;&quot;&gt;&lt;b&gt;5,000,000 ns&lt;/b&gt;&lt;br /&gt;&lt;b&gt;(5,000 &amp;mu;s)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 35px;&quot;&gt;&lt;b&gt;250,000 ns&lt;/b&gt;&lt;br /&gt;&lt;b&gt;(250 &amp;mu;s)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 35px;&quot;&gt;&lt;b&gt;250,000 ns&lt;/b&gt;&lt;br /&gt;&lt;b&gt;(250 &amp;mu;s)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 35px;&quot;&gt;&lt;b&gt;100,000 ns&lt;/b&gt;&lt;br /&gt;&lt;b&gt;(100 &amp;mu;s)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;1KB Zip 압축&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;3,000 ns (3 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;3,000 ns (3 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;2,000 ns (2 &amp;mu;s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;1Gbps로 1KB 전송&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;10,000 ns (10 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;10,000 ns (10 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;1,000 ns (1 &amp;mu;s, 10Gbps)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;height: 36px;&quot;&gt;&lt;b&gt;SSD 랜덤 읽기 (SATA)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 36px; text-align: center;&quot;&gt;&lt;b&gt;없음&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 36px; text-align: center;&quot;&gt;&lt;b&gt;없음&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 36px; text-align: center;&quot;&gt;&lt;b&gt;150,000 ns &lt;/b&gt;&lt;br /&gt;&lt;b&gt;(150 &amp;mu;s)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 36px; text-align: center;&quot;&gt;&lt;b&gt;100,000 ns &lt;/b&gt;&lt;br /&gt;&lt;b&gt;(100 &amp;mu;s)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;height: 36px;&quot;&gt;&lt;b&gt;SSD 랜덤 읽기 (NVMe)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 36px; text-align: center;&quot;&gt;&lt;b&gt;없음&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 36px; text-align: center;&quot;&gt;&lt;b&gt;없음&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 36px; text-align: center;&quot;&gt;&lt;b&gt;없음&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 36px; text-align: center;&quot;&gt;&lt;b&gt;20,000 ns &lt;/b&gt;&lt;br /&gt;&lt;b&gt;(20 &amp;mu;s)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;SSD 1MB 순차 읽기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;&lt;b&gt;없음&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;&lt;b&gt;없음&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;&lt;b&gt;1,000,000 ns&lt;/b&gt;&lt;br /&gt;&lt;b&gt;(1,000 &amp;mu;s)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;&lt;b&gt;50,000 ns&lt;/b&gt;&lt;br /&gt;&lt;b&gt;(50 &amp;mu;s, NVMe)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;같은 DtatCenter내 RTT&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;500,000 ns &lt;br /&gt;(500 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;500,000 ns &lt;br /&gt;(500 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;500,000 ns&lt;br /&gt;(500 &amp;mu;s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;HDD seek&lt;/td&gt;
&lt;td style=&quot;height: 35px; text-align: center;&quot;&gt;10,000,000 ns &lt;br /&gt;(10,000 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 35px; text-align: center;&quot;&gt;10,000,000 ns &lt;br /&gt;(10,000 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 35px; text-align: center;&quot;&gt;10,000,000 ns &lt;br /&gt;(10,000 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 35px; text-align: center;&quot;&gt;10,000,000 ns &lt;br /&gt;(10,000 &amp;mu;s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;HDD 1MB 순차 읽기&lt;/td&gt;
&lt;td style=&quot;height: 38px; text-align: center;&quot;&gt;30,000,000 ns &lt;br /&gt;(30,000 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 38px; text-align: center;&quot;&gt;20,000,000 ns &lt;br /&gt;(20,000 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 38px; text-align: center;&quot;&gt;20,000,000 ns &lt;br /&gt;(20,000 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 38px; text-align: center;&quot;&gt;20,000,000 ns &lt;br /&gt;(20,000 &amp;mu;s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;한국 &amp;harr; 미국 RTT&lt;/td&gt;
&lt;td style=&quot;height: 35px; text-align: center;&quot;&gt;200,000,000 ns &lt;br /&gt;(200,000 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 35px; text-align: center;&quot;&gt;150,000,000 ns &lt;br /&gt;(150,000 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 35px; text-align: center;&quot;&gt;150,000,000 ns &lt;br /&gt;(150,000 &amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;height: 35px; text-align: center;&quot;&gt;150,000,000 ns &lt;br /&gt;(150,000 &amp;mu;s)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;중요하게 봐야할 것들&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 관통하는&lt;span&gt; &lt;/span&gt;통찰물리&lt;span&gt; &lt;/span&gt;한계&lt;span&gt; &lt;/span&gt;기반&lt;span&gt; &lt;/span&gt;숫자는&lt;span&gt; &lt;/span&gt;변하지&lt;span&gt; &lt;/span&gt;않는다&lt;span&gt; (&lt;/span&gt;빛의&lt;span&gt; &lt;/span&gt;속도&lt;span&gt;, HDD &lt;/span&gt;회전&lt;span&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 기술&lt;span&gt; &lt;/span&gt;기반&lt;span&gt; &lt;/span&gt;숫자만&lt;span&gt; &lt;/span&gt;격변한다&lt;span&gt; (SSD &lt;/span&gt;등장으로&lt;span&gt; &lt;/span&gt;디스크&lt;span&gt; 500&lt;/span&gt;배&lt;span&gt; &lt;/span&gt;빨라짐&lt;span&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 병목은&lt;span&gt; &lt;/span&gt;사라지지&lt;span&gt; &lt;/span&gt;않고&lt;span&gt; &lt;/span&gt;옮겨간다&lt;span&gt; (&lt;/span&gt;디스크에서&lt;span&gt; &lt;/span&gt;네트워크와&lt;span&gt; &lt;/span&gt;메모리로&lt;span&gt;)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 정확한&lt;span&gt; &lt;/span&gt;수치보다&lt;span&gt; &lt;/span&gt;자릿수&lt;span&gt;(order of magnitude) &lt;/span&gt;감각이&lt;span&gt; &lt;/span&gt;중요하다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 표는&lt;span&gt; &lt;/span&gt;항상&lt;span&gt; &lt;/span&gt;만든&lt;span&gt; &lt;/span&gt;시점의&lt;span&gt; &lt;/span&gt;스냅샷&lt;span&gt; &amp;mdash; &lt;/span&gt;시간이&lt;span&gt; &lt;/span&gt;지나면&lt;span&gt; &lt;/span&gt;검증이&lt;span&gt; &lt;/span&gt;필요하다'&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;자릿수 감각 정리 및 무엇이 중요할까?&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 59.5349%; height: 279px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;계층&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;단위&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;비유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 24.4186%; text-align: center;&quot;&gt;L1 캐시&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 34.0698%; text-align: center;&quot;&gt;sub-ns&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 41.3954%; text-align: center;&quot;&gt;손에 들고 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 24.4186%; text-align: center;&quot;&gt;&lt;b&gt;메인 메모리&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 34.0698%; text-align: center;&quot;&gt;&lt;b&gt;100 ns&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 41.3954%; text-align: center;&quot;&gt;&lt;b&gt;책상 위&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 24.4186%; text-align: center;&quot;&gt;&lt;b&gt;NVMe SSD&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 34.0698%; text-align: center;&quot;&gt;&lt;b&gt;10~100 &amp;mu;s&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 41.3954%; text-align: center;&quot;&gt;&lt;b&gt;같은 방 책장&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 24.4186%; text-align: center;&quot;&gt;&lt;b&gt;SATA SSD&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 34.0698%; text-align: center;&quot;&gt;&lt;b&gt;100 &amp;mu;s ~ 1 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 41.3954%; text-align: center;&quot;&gt;&lt;b&gt;옆방&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 24.4186%; text-align: center;&quot;&gt;HDD&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 34.0698%; text-align: center;&quot;&gt;10 ms&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 41.3954%; text-align: center;&quot;&gt;도서관 가서 찾기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 24.4186%; text-align: center;&quot;&gt;같은 DataCenter 네트워크&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 34.0698%; text-align: center;&quot;&gt;500 &amp;mu;s&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 41.3954%; text-align: center;&quot;&gt;옆 사무실 전화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 24.4186%; text-align: center;&quot;&gt;대륙 간 RTT&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 34.0698%; text-align: center;&quot;&gt;150 ms&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 41.3954%; text-align: center;&quot;&gt;해외로 편지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메모리 &amp;rarr; NVMe SSD는 약 1,000배 차이&lt;/b&gt;입니다. 옛날엔 메모리 &amp;rarr; HDD가 10만 배였으니,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;격차가 100배 줄어든 것&lt;/b&gt;.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;RAM이 곧 디스크&quot;라는 말이 어느 정도 농담이 아닌 시대.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Android 개발 관점에서 보면, Room에 로컬캐싱하는 이유.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모바일 NVMe(UFS 3.0~4.0)도 랜덤 읽기가 수십&amp;mu;s 수준이라, 네트워크 한 번 다녀오는 것(수십 ms)보다&lt;span&gt;&amp;nbsp;&lt;/span&gt;1000배 빠르다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;*&quot;Room은 디바이스 내부 SQLite를 호출하니까 수 ms 수준(전형적으로 1~10ms, 단순 쿼리는 그보다 빠름)에 끝납니다.&quot;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이게 &quot;로컬 캐싱이 압도적으로 유리한&quot; 물리적 근거입니다.&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100.349%; height: 229px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;text-align: center; width: 18.0446%; height: 19px;&quot;&gt;작업&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 18.3617%; height: 19px;&quot;&gt;시간&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 36.9684%; height: 19px;&quot;&gt;의미&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 13.0031%; height: 19px;&quot;&gt;1초 기준 환산&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 16.4617%; height: 19px;&quot;&gt;체감&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 95px;&quot;&gt;
&lt;td style=&quot;text-align: left; width: 18.0446%; height: 95px;&quot;&gt;&lt;b&gt;메인 메모리 접근&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 18.3617%; height: 95px;&quot;&gt;100 ns&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 36.9684%; height: 95px;&quot;&gt;가장 빠른 &quot;데이터 가져오기&quot;의 기준점. 모든 비교의 출발점.&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 13.0031%; height: 95px;&quot;&gt;&lt;b&gt;1초&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 16.4617%; height: 95px;&quot;&gt;손가락 한 번 튕기기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;text-align: left; width: 18.0446%; height: 19px;&quot;&gt;&lt;b&gt;NVMe SSD 랜덤 읽기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 18.3617%; height: 19px;&quot;&gt;20,000 ns &lt;br /&gt;(20&amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 36.9684%; height: 19px;&quot;&gt;로컬 저장소의 현실. 모바일 UFS도 비슷한 수준.&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 13.0031%; height: 19px;&quot;&gt;&lt;b&gt;3분 20초&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 16.4617%; height: 19px;&quot;&gt;라면 끓이기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 39px;&quot;&gt;
&lt;td style=&quot;text-align: left; width: 18.0446%; height: 39px;&quot;&gt;&lt;b&gt;같은 DC 내 RTT&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 18.3617%; height: 39px;&quot;&gt;500,000 ns &lt;br /&gt;(500&amp;mu;s)&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 36.9684%; height: 39px;&quot;&gt;마이크로서비스, Redis, DB 같은 백엔드 통신의 기준선.&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 13.0031%; height: 39px;&quot;&gt;&lt;b&gt;1시간 23분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 16.4617%; height: 39px;&quot;&gt;영화 한 편&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;text-align: left; width: 18.0446%; height: 19px;&quot;&gt;&lt;b&gt;한 프레임 (60fps)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 18.3617%; height: 19px;&quot;&gt;16,666,666 ns &lt;br /&gt;(16.6ms)&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 36.9684%; height: 19px;&quot;&gt;&lt;b&gt;&lt;span&gt;UI가 안 끊기게 만들 시간 예산.&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 13.0031%; height: 19px;&quot;&gt;&lt;b&gt;1일 22시간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 16.4617%; height: 19px;&quot;&gt;주말&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;text-align: left; width: 18.0446%; height: 19px;&quot;&gt;&lt;b&gt;모바일 &amp;harr; 서버 RTT&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 18.3617%; height: 19px;&quot;&gt;50,000,000 ns&lt;br /&gt;(50ms)&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 36.9684%; height: 19px;&quot;&gt;&lt;b&gt;앱 &amp;rarr; 서버 호출의 현실. 무선이 추가되는 순간 자릿수가 바뀜.&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 13.0031%; height: 19px;&quot;&gt;&lt;b&gt;5일 19시간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 16.4617%; height: 19px;&quot;&gt;휴가 일주일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;text-align: left; width: 18.0446%; height: 19px;&quot;&gt;&lt;b&gt;대륙 간 RTT&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 18.3617%; height: 19px;&quot;&gt;150,000,000 ns&lt;br /&gt;(150ms)&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 36.9684%; height: 19px;&quot;&gt;&lt;b&gt;글로벌 서비스 설계의 물리적 한계.&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 13.0031%; height: 19px;&quot;&gt;&lt;b&gt;17일 8시간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 16.4617%; height: 19px;&quot;&gt;보름 넘게&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무에서 이게 왜 중요한가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 마이크로서비스 호출 한 번 = 메모리 접근 5,000번&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 서비스 A가 서비스 B를 HTTP로 호출하면, 같은 DC 안이라도 최소 500&amp;mu;s는 깔립니다. 메모리에서 읽는 것보다 5,000배-6000배 느리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &quot;필요 없는 마이크로서비스 분리는 성능 재앙&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. N+1 쿼리 문제 (서버, 로컬DB)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째, 1번의 쿼리 100개의 게시글 가져오기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째, 100번의 쿼리 100개의 게시글의 각 작성자 가져오기.&lt;/p&gt;
&lt;pre id=&quot;code_1779082660758&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1번째 쿼리: 게시글 100개 가져오기
val posts = postDao.getAllPosts() // SELECT * FROM Post LIMIT 100

// 그 다음 작성자 정보를 가져온다
posts.forEach { post -&amp;gt;
    // 2번째, 3번째, ..., 101번째 쿼리: 매번 작성자 조회
    val author = userDao.getUserById(post.userId) // SELECT * FROM User WHERE id = ?
    println(&quot;${post.title} by ${author.name}&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOIN 쿼리가 조금 무거운 작업이라도 한 번이면 끝날 일.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;서버(백앤드)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;환경:&lt;/b&gt; 앱 서버 &amp;harr; DB 서버가 같은 데이터센터에 있고, 둘 사이는 내부망 네트워크로 통신.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기준 시간:&lt;/b&gt; DB 쿼리 1번 &amp;asymp; 0.5 ms (DataCenter 내 RTT 500&amp;mu;s + 쿼리 처리 약간)&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 57px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 18.3721%;&quot;&gt;&lt;b&gt;방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 31.1628%;&quot;&gt;총 쿼리 수&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 19.5349%;&quot;&gt;&lt;b&gt;시간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 30.814%;&quot;&gt;체감&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 18.3721%;&quot;&gt;&lt;b&gt;N+1번 쿼리&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 31.1628%;&quot;&gt;1 + 100 = 101번&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 19.5349%;&quot;&gt;&lt;b&gt;약 50 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 30.814%;&quot;&gt;느리다고 인식 시작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 18.3721%;&quot;&gt;&lt;b&gt;JOIN 1번&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 31.1628%;&quot;&gt;1번&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 19.5349%;&quot;&gt;&lt;b&gt;약 0.7 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 30.814%;&quot;&gt;즉각적&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;약 70배 더 걸림.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;안드로이드 로컬 (Room)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;환경:&lt;/b&gt; 앱 안에서 SQLite를 직접 호출. 네트워크 없음, 디바이스 내부 파일 접근만.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기준 시간:&lt;/b&gt; Room 단순 쿼리 1번 &amp;asymp; 50~100 &amp;mu;s (네트워크 없음, 디스크 접근 + 소프트웨어 스택)&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 57px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 18.9535%; height: 19px;&quot;&gt;&lt;b&gt;&lt;b&gt;방식&lt;/b&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.9302%; height: 19px;&quot;&gt;총 쿼리 수&lt;/td&gt;
&lt;td style=&quot;width: 18.8372%; height: 19px;&quot;&gt;&lt;b&gt;&lt;b&gt;시간&lt;/b&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.0465%; height: 19px;&quot;&gt;체감&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 18.9535%; height: 19px;&quot;&gt;&lt;b&gt;N+1&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.9302%; height: 19px;&quot;&gt;1 + 100 = 101번&lt;/td&gt;
&lt;td style=&quot;width: 18.8372%; height: 19px;&quot;&gt;&lt;b&gt;약 5-10 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.0465%; height: 19px;&quot;&gt;위험 (프레임의 절반 이상)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 18.9535%; height: 19px;&quot;&gt;&lt;b&gt;JOIN(@Relation) 1번&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.9302%; height: 19px;&quot;&gt;1번&lt;/td&gt;
&lt;td style=&quot;width: 18.8372%; height: 19px;&quot;&gt;&lt;b&gt;약 &lt;b&gt;0.3 ms&lt;/b&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.0465%; height: 19px;&quot;&gt;여유 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;약 20~30배 더 걸림.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 캐싱 판단의 기준선&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 데이터를 Redis에 캐싱할 가치가 있나?&quot;의 답이 여기서 나옵니다. Redis도 결국 네트워크(같은 DC RTT)호출이라 500&amp;mu;s 소요된다 그래서 **단순 메모리 캐시(앱 프로세스 내 HashMap)**가 Redis보다 압도적으로 빠릅니다. 다만 여러 서버가 공유해야 한다면 Redis로 가야한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 모바일 앱(android native)에서는?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android에서 서버 API 한 번 호출하면 보통 &lt;b&gt;50ms 이상&lt;/b&gt; 깔립니다. (무선 구간 + 인터넷 구간 + 서버 처리가 다 포함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Room은 디바이스 내부 SQLite를 호출하니까 &lt;b&gt;수 ms 수준&lt;/b&gt;(전형적으로 1~10ms, 단순 쿼리는 그보다 빠름)에 끝납니다. 무선도, 인터넷도, 서버 부하도 없이 그냥 UFS에서 파일 읽고 객체로 매핑하면 끝이니까요. &lt;b&gt;같은 데이터를 가져오는 데 자릿수가 한 단계 다른 비용이 들어갑니다. &lt;/b&gt;이게 Single Source of Truth 패턴(네트워크는 백그라운드에서 Room에 동기화, UI는 Room만 본다)이 모던 안드로이드 아키텍처의 표준이 된 정량적 근거입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>hik14</author>
      <guid isPermaLink="true">https://hik-coding.tistory.com/372</guid>
      <comments>https://hik-coding.tistory.com/372#entry372comment</comments>
      <pubDate>Mon, 18 May 2026 16:10:52 +0900</pubDate>
    </item>
    <item>
      <title>도메인 주도 설계 (Domain Driven Design - DDD)의 이벤트 스토밍 개념정리2</title>
      <link>https://hik-coding.tistory.com/371</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 이벤트 스토밍을 하는가&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 도메인 전문가 및 기획, 개발자, QA의 언어 간극을 좁히기 위해&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 이유입니다. 보통 PM/기획자/현업 담당자가 들고 오는 요구사항 문서는 &quot;사용자가 결제를 하면 주문이 처리된다&quot; 같은 추상적인 문장이에요. 개발자는 이걸 받아서 머릿속으로 &quot;그럼 이건 어떻게 분기하지?&quot;, &quot;실패하면?&quot;, &quot;동시 요청이면?&quot; 같은 질문을 혼자 만들어내며 구현합니다. 결과적으로 &lt;b&gt;개발자가 비즈니스를 추측하는 코드&lt;/b&gt;가 만들어지고, 나중에 &quot;이거 그렇게 동작하면 안 되는데?&quot; 하고 충돌이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 스토밍은 이 사람들을 한 방에 모아놓고, 벽에 포스트잇을 같이 붙이면서 &quot;결제 요청됨 다음에 뭐가 발생해요?&quot;, &quot;그때 PG사가 응답을 안 주면요?&quot; 같은 질문을 &lt;b&gt;그 자리에서 즉시&lt;/b&gt; 해결합니다. 추측 대신 합의.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 숨겨진 복잡성을 미리 드러내기 위해&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요구사항 문서에는 &quot;결제 처리&quot; 한 줄로 끝나지만, 막상 이벤트 스토밍을 하면 빨간 Hotspot이 10개씩 붙습니다.&lt;/b&gt; &quot;중복 결제 어떻게 막죠?&quot;, &quot;타임아웃 처리는?&quot;, &quot;PG사 장애 시 재시도는?&quot; &amp;mdash; 이 질문들이 &lt;b&gt;개발 시작 전에&lt;/b&gt; 발견되는 게 핵심이에요. 코드를 다 짠 다음에 발견되면 비용이 100배가 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 시스템 경계(Bounded Context)를 식별하기 위해&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벽에 포스트잇을 다 붙이고 나면, 자연스럽게 &quot;여기는 결제 영역&quot;, &quot;여기는 주문 영역&quot;, &quot;여기는 배송 영역&quot; 처럼 응집된 덩어리가 보입니다. 이 덩어리들이 바로 &lt;b&gt;Bounded Context&lt;/b&gt;이고, 마이크로서비스 분리나 모듈 경계의 근거가 됩니다. 추측이 아니라 도메인 자체의 모양을 따라가는 거죠.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 빠르고 저렴하다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UML이나 시퀀스 다이어그램을 그리려면 도구 띄우고 박스 위치 잡고 정렬하느라 시간이 가는데, 이벤트 스토밍은 &lt;b&gt;포스트잇과 벽만 있으면&lt;/b&gt; 됩니다(요즘은 Miro/FigJam). 반나절~하루 안에 한 도메인이 정리되고, 틀렸으면 떼고 다시 붙이면 끝입니다. &lt;b&gt;진입 장벽이 낮아서 비개발자도 참여할 수 있어요.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과물은 무엇인가&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 도메인 이벤트 카탈로그&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 직접적인 산출물입니다. 그 도메인에서 &lt;b&gt;실제로 발생하는 모든 사건의 목록&lt;/b&gt;이에요. &quot;결제 요청됨&quot;, &quot;PG사 응답 수신됨&quot;, &quot;결제 완료됨&quot;, &quot;주문 확정됨&quot;... 이 목록은 그대로 코드의 sealed class나 이벤트 타입 정의로 옮겨갑니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;kotlin&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;sealed class PaymentEvent {
    data class PaymentRequested(val orderId: String, val amount: Long) : PaymentEvent()
    data class PgResponseReceived(val result: PgResult) : PaymentEvent()
    data class PaymentCompleted(val transactionId: String) : PaymentEvent()
    data class PaymentFailed(val reason: String) : PaymentEvent()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Aggregate 식별과 책임 분배&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 Command를 받아서 이 Event를 발생시키는 책임자가 누구인가&quot;가 명확해집니다. 결제 Aggregate, 주문 Aggregate, 배송 Aggregate처럼요. 이게 그대로 도메인 모델의 클래스 구조가 됩니다. Android 코드에서는 Repository 또는 UseCase의 경계와 연결된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Bounded Context 경계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 워크샵에서는 색깔이나 영역으로 &quot;여기서부터 여기까지는 결제 컨텍스트&quot;, &quot;이 너머는 주문 컨텍스트&quot; 식으로 묶입니다. 이 경계가:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈 분리 기준 (:feature-payment, :feature-order)&lt;/li&gt;
&lt;li&gt;팀 분담 기준&lt;/li&gt;
&lt;li&gt;API/이벤트 계약의 경계&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Hotspot 목록 (미결정 사항 리스트)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빨간 포스트잇들이 그대로 &lt;b&gt;백로그/이슈 트래커&lt;/b&gt;로 옮겨갑니다. &quot;PG사 장애 시 재시도 정책 결정 필요&quot;, &quot;중복 결제 방지 메커니즘 설계 필요&quot; &amp;mdash; 이게 다음 스프린트의 기술 부채 또는 설계 과제가 되죠. 워크샵에서 답이 안 나온 게 부끄러운 게 아니라, &lt;b&gt;답이 없는 곳을 명시적으로 찍어둔 게&lt;/b&gt; 가치입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. Read Model 명세 (UI 요구사항)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Read Model 포스트잇은 그대로 화면 명세가 됩니다. &quot;결제 화면에는 이런 정보가 필요하다&quot;, &quot;결제 결과 화면에는 저런 정보가 필요하다&quot; &amp;mdash; 이건 UI 디자인과 ViewModel의 StateFlow 구조에 직접 반영돼요.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. 유비쿼터스 언어 (Ubiquitous Language)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크샵에서 합의된 단어들 &amp;mdash; &quot;결제 요청&quot;인가 &quot;결제 시도&quot;인가, &quot;주문 확정&quot;인가 &quot;주문 완료&quot;인가 &amp;mdash; 이게 코드, API, 문서, 회의에서 &lt;b&gt;하나의 통일된 단어&lt;/b&gt;로 정착합니다. 더 이상 &quot;기획자는 '결제 시도'라 부르고 백엔드는 'paymentAttempt'이라 부르고 앱은 'PayRequest'라 부르는&quot; 카오스가 없어진다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 스토밍은 &lt;b&gt;&quot;사람들 간의 이해 차이&quot;라는 가장 비싼 비용을 미리 지불해서, 나중에 발생할 더 큰 비용을 막는 활동&lt;/b&gt;이에요. 산출물은 멋진 다이어그램 한 장이 아니라:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 구조의 뼈대 (Event, Aggregate, Context)&lt;/li&gt;
&lt;li&gt;미결정 사항의 명시적 목록 (Hotspot)&lt;/li&gt;
&lt;li&gt;모두가 같은 단어를 쓰게 만든 사전 (유비쿼터스 언어)&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>hik14</author>
      <guid isPermaLink="true">https://hik-coding.tistory.com/371</guid>
      <comments>https://hik-coding.tistory.com/371#entry371comment</comments>
      <pubDate>Mon, 18 May 2026 00:55:52 +0900</pubDate>
    </item>
    <item>
      <title>도메인 주도 설계 (Domain Driven Design - DDD)의 이벤트 스토밍 개념정리1</title>
      <link>https://hik-coding.tistory.com/370</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이벤트 스토밍 (Event Storming)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 스토밍은 Alberto Brandolini가 2013년에 고안한 워크숍 기반의&lt;b&gt; 도메인 모델링 기법&lt;/b&gt;입니다. 복잡한 비즈니스 도메인을 빠르게 탐색하고 이해하기 위해 &lt;b&gt;개발자, 도메인 전문가, 기획자, QA 등이 제품에 생산에 참여하는 사람들이 한자리에 모여 포스트잇으로 시스템의 흐름을 시각화하는 협업 방법론&lt;/b&gt;이죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 아이디어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 시스템을 &quot;&lt;b&gt;도메인에서 일어나는 사건(Event)들의 흐름&lt;/b&gt;&quot;으로 바라보는 것이 핵심입니다. 데이터베이스 테이블이나 클래스 다이어그램부터 시작하는 전통적인 방식과 달리, &quot;무슨 일이 일어나는가?&quot;라는 질문에서 출발합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 구성 요소 (색상별 포스트잇)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크숍에서는 보통 색깔별로 의미를 구분합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;  Domain Event (주황색)&lt;/b&gt;: &quot;과거형&quot;으로 표현되는 사건. 예: &quot;주문이 생성됨&quot;, &quot;결제가 완료됨&quot;, &quot;PG사 응답이 수신됨&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;  Command (파란색)&lt;/b&gt;: 이벤트를 발생시키는 행동/명령. 예: &quot;주문 생성하기&quot;, &quot;결제 요청하기&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;  Actor (노란색)&lt;/b&gt;: 명령을 수행하는 주체 (사용자, 외부 시스템)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;  Aggregate (연보라색)&lt;/b&gt;: 명령을 받아 이벤트를 발생시키는 도메인 객체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;  Hotspot/Issue (빨간색)&lt;/b&gt;: 의문점, 충돌 지점, 결정이 필요한 사항&lt;/li&gt;
&lt;li&gt;&lt;b&gt;  Policy (초록색)&lt;/b&gt;: &quot;이벤트가 발생하면 &amp;rarr; 자동으로 명령 실행&quot;의 규칙&lt;/li&gt;
&lt;li&gt;&lt;b&gt;  Read Model (연한 노랑)&lt;/b&gt;: 사용자가 의사결정을 위해 보는 화면/데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 보통 이렇게 연결 Actor &amp;rarr; Command &amp;rarr; Aggregate &amp;rarr; Domain Event &amp;rarr;&amp;nbsp; Policy &amp;rarr; (다음 Command)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-17 오후 6.33.14.png&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kw4Ez/dJMcaak0z4b/qcW5f9NlvHkLMrTsbNkaR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kw4Ez/dJMcaak0z4b/qcW5f9NlvHkLMrTsbNkaR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kw4Ez/dJMcaak0z4b/qcW5f9NlvHkLMrTsbNkaR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkw4Ez%2FdJMcaak0z4b%2FqcW5f9NlvHkLMrTsbNkaR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;639&quot; height=&quot;247&quot; data-filename=&quot;스크린샷 2026-05-17 오후 6.33.14.png&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;235&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-17 오후 6.34.31.png&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qpeuK/dJMcaiwzORH/RJCw4KXjhB0u4RaHOVo5Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qpeuK/dJMcaiwzORH/RJCw4KXjhB0u4RaHOVo5Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qpeuK/dJMcaiwzORH/RJCw4KXjhB0u4RaHOVo5Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqpeuK%2FdJMcaiwzORH%2FRJCw4KXjhB0u4RaHOVo5Wk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;645&quot; height=&quot;260&quot; data-filename=&quot;스크린샷 2026-05-17 오후 6.34.31.png&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;257&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1 사이클 안에서는&lt;/b&gt; 사용자가 의도(Command)를 보내고, Aggregate가 비즈니스 규칙을 확인한 뒤 사실(Event)을 발생시킵니다. 여기까지가 한 단위&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;연속된 사이클은 Policy가 다리 역할&lt;/b&gt;을 합니다. &quot;이런 Event가 발생하면 &amp;rarr; 자동으로 저런 Command를 실행한다&quot;는 규칙이 Policy이고, 이게 사이클과 사이클을 이어준다. 사용자가 매번 명령을 내리지 않아도 시스템이 자동으로 다음 단계를 진행할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;HOTSPOT이 추가된 이벤트 스토밍&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff; text-align: start;&quot;&gt;Hotspot이 추가된 이벤트 스토밍&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-17 오후 6.58.27.png&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbAt8t/dJMcaffBSSU/TJobIw9vtiXlWVkwXYKVyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbAt8t/dJMcaffBSSU/TJobIw9vtiXlWVkwXYKVyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbAt8t/dJMcaffBSSU/TJobIw9vtiXlWVkwXYKVyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbAt8t%2FdJMcaffBSSU%2FTJobIw9vtiXlWVkwXYKVyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;701&quot; height=&quot;364&quot; data-filename=&quot;스크린샷 2026-05-17 오후 6.58.27.png&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. &quot;중복 결제 방지는?&quot; (Command 위)&lt;/b&gt;&lt;br /&gt;사용자가 결제 버튼을 빠르게 두 번 누르거나 네트워크 재전송으로 같은 Command가 두 번 들어왔을 때 어떻게 막을 것인지 &amp;mdash; Idempotency Key 같은 결정이 필요한 지점이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. &quot;미결제 상태 DB 저장? in-memory?&quot; (Event 옆)&lt;/b&gt;&lt;br /&gt;이건 실제로 고민하셨던 주제와 직접 닿아있는 의문이죠. &quot;결제 요청됨&quot; 상태를 Room에 영속화할지, Flow로만 흘려보낼지의 결정 포인트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. &quot;PG사 장애 시 재시도 정책?&quot; (Policy 위)&lt;/b&gt;&lt;br /&gt;Policy 자체가 실패할 수 있는 외부 호출을 트리거하기 때문에, 그 실패를 어떻게 다룰지가 핫스팟이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. &quot;타임아웃 발생 시 처리 방법?&quot; (Aggregate-Event 사이)&lt;/b&gt;&lt;br /&gt;Aggregate가 PG사 응답을 기다리는 동안 시간이 초과되면 결제 상태를 어떻게 둘지 &amp;mdash; &quot;결제 실패됨&quot; Event를 만들지, &quot;결제 보류됨&quot;으로 둘지의 결정.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;*Hotspot의 진짜 가치는 워크샵 끝에 빨간 포스트잇이 어디 몰려있는지 보는 것. &lt;/b&gt;&lt;b&gt;빨간 게 많이 붙은 영역은 도메인 전문가도 명확한 답을 못 가진 곳, 즉 시스템에서 가장 위험하고 가장 깊이 설계해야 할 부분 위 그림에서도 Policy~Cycle 2 구간에 핫스팟이 몰려있다. 외부 시스템(PG사)과의 통신이 가장 불확실성이 높다는 뜻이고, 이게 실제 결제 도메인의 본질적인 어려움을 반영합니다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;READ MODEL이 추가된 이벤트 스토밍&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기 모델(Read Model)은 사용자가 어떤 행동(커맨드)을 하기 전에 &lt;b data-index-in-node=&quot;68&quot; data-path-to-node=&quot;1&quot;&gt;의사결정을 내릴 수 있도록 돕는 정보의 덩어리&lt;/b&gt;를 의미합니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size23&quot;&gt;단순한 '화면 디자인'과의 차이점&lt;/h3&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;이벤트 스토밍에서 이를 단순한 UI 스케치라고 부르지 않고 굳이 '읽기 모델'이라는 용어를 쓰는 이유는 기술적인 데이터 구조(CQRS 패턴)와 연결되기 때문입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,0&quot;&gt;명령(Command)을 위한 데이터:&lt;/b&gt; 데이터를 저장하고 수정할 때 필요한 데이터 구조 (예: 정규화된 DB 테이블)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0&quot;&gt;조회(Query)를 위한 데이터:&lt;/b&gt; 사용자가 화면에서 오직 '조회'만 하기 위해 여러 테이블을 조합하거나 미리 가공해 둔 데이터 구조 (예: 역정규화된 조회용 데이터)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;즉, 이벤트 스토밍에서의 읽기 모델은 &lt;b&gt;&quot;사용자가 커맨드를 유발하기 위해 시스템으로부터 요구하는 최적화된 데이터 뷰(View)&quot;라고 이해하시면 설계 관점에서 가장 완벽합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-17 오후 7.11.39.png&quot; data-origin-width=&quot;687&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/leFNP/dJMb990K0sf/28kgAp3G1yvfaKQkiNC8yK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/leFNP/dJMb990K0sf/28kgAp3G1yvfaKQkiNC8yK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/leFNP/dJMb990K0sf/28kgAp3G1yvfaKQkiNC8yK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FleFNP%2FdJMb990K0sf%2F28kgAp3G1yvfaKQkiNC8yK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;687&quot; height=&quot;441&quot; data-filename=&quot;스크린샷 2026-05-17 오후 7.11.39.png&quot; data-origin-width=&quot;687&quot; data-origin-height=&quot;441&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android 개발 관점에서 Read Model은 &lt;b&gt;UI State&lt;/b&gt; 와 거의 1:1 매핑됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결제 화면 &amp;rarr; PaymentScreenState (StateFlow로 관찰)&lt;/li&gt;
&lt;li&gt;결제 결과 화면 &amp;rarr; PaymentResultState&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 여기서 흥미로운 점은, Event를 어디서 가져와서 Read Model을 만들 것인가가 바로 &lt;b&gt;in-memory vs DB-first&lt;/b&gt; 결정과 직결된다. Read Model을 Room에서 쿼리해서 만들면 DB-first SSOT 패턴이 되고, 메모리에 보관된 Event Stream에서 직접 파생시키면 in-memory CQRS가 되는 거죠. 이벤트 스토밍에서 Read Model 포스트잇 옆에 빨간 Hotspot을 붙여 &quot;이 Read Model은 어디서 파생되어야 하는가?&quot;를 표시해두면, 워크샵에서 바로 그 결정을 논의할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>hik14</author>
      <guid isPermaLink="true">https://hik-coding.tistory.com/370</guid>
      <comments>https://hik-coding.tistory.com/370#entry370comment</comments>
      <pubDate>Sun, 17 May 2026 19:21:05 +0900</pubDate>
    </item>
    <item>
      <title>도메인 주도 설계 (Domain Driven Design - DDD)의 기초 개념정리2</title>
      <link>https://hik-coding.tistory.com/369</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;전술적 설계 (Tactical Design)&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-12 오전 2.37.18.png&quot; data-origin-width=&quot;209&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yFMWH/dJMcafzOygb/9eo1fhMlYlck8Xr4PKHsI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yFMWH/dJMcafzOygb/9eo1fhMlYlck8Xr4PKHsI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yFMWH/dJMcafzOygb/9eo1fhMlYlck8Xr4PKHsI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyFMWH%2FdJMcafzOygb%2F9eo1fhMlYlck8Xr4PKHsI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;209&quot; height=&quot;547&quot; data-filename=&quot;스크린샷 2026-05-12 오전 2.37.18.png&quot; data-origin-width=&quot;209&quot; data-origin-height=&quot;547&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. Entity (엔티티)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 고유한 식별자(ID)로 구별되는 객체&lt;/b&gt;입니다. 속성이 바뀌어도 같은 것으로 취급됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 특징:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ID가 같으면 같은 객체로 간주&lt;/li&gt;
&lt;li&gt;시간에 따라 상태가 변함 (가변)&lt;/li&gt;
&lt;li&gt;생명주기를 가짐 (생성 &amp;rarr; 변경 &amp;rarr; 삭제)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1778521117786&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class User(
    val id: UserId,           // 식별자
    var name: String,          // 바뀔 수 있는 속성
    var email: Email
) {
    override fun equals(other: Any?): Boolean {
        if (other !is User) return false
        return this.id == other.id   // ID로만 동등성 판단
    }
    
    override fun hashCode(): Int = id.hashCode()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. Value Object (값 객체)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 식별자 없이 값 자체로 동등성이 판단되는 불변 객체&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 특징:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 속성이 같으면 같은 객체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;불변(Immutable)&lt;/b&gt; &amp;mdash; 한번 만들면 못 바꿈, 바꾸려면 새로 생성&lt;/li&gt;
&lt;li&gt;ID 없음&lt;/li&gt;
&lt;li&gt;생명주기 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1778521201761&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class Money(
    val amount: BigDecimal,
    val currency: Currency
) {
    operator fun plus(other: Money): Money {
        require(currency == other.currency) { &quot;통화가 달라요&quot; }
        return Money(amount + other.amount, currency)  // 새 객체 반환
    }
}

data class Address(
    val zipCode: String,
    val street: String,
    val city: String
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;*&lt;/b&gt;도메인 모델링에서는 가능한 한 Value Object를 많이 쓰는 게 좋다. 원시 타입(String, Int)을 그대로 쓰지 말고 의미를 담은 Value Object로 감싸서 의미를 전달한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778521669178&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 나쁨 - 그냥 String
fun sendEmail(email: String)

// 좋음 - Email Value Object  
@JvmInline value class Email(val value: String) {
    init { require(value.contains(&quot;@&quot;)) }
}
fun sendEmail(email: Email)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Aggregate (애그리거트)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 함께 변경되어야 하는 엔티티&amp;middot;값 객체의 묶음&lt;/b&gt;이며, 트랜잭션과 일관성의 단위입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 특징:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부에서는 &lt;b&gt;Aggregate Root&lt;/b&gt;를 통해서만 접근&lt;/li&gt;
&lt;li&gt;1개의Root Entity를 가짐&lt;/li&gt;
&lt;li&gt;내부 객체에 직접 접근 금지&lt;/li&gt;
&lt;li&gt;하나의 트랜잭션에서 변경되는 단위&lt;/li&gt;
&lt;li&gt;불변식(invariant)을 보장하는 책임&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1778522607786&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Aggregate Root
class Order(
    val id: OrderId,
    private val items: MutableList&amp;lt;OrderItem&amp;gt; = mutableListOf(),  // 내부 엔티티
    private var status: OrderStatus = OrderStatus.DRAFT,
    val shippingAddress: Address                                   // 값 객체
) {
    // 외부는 Order를 통해서만 OrderItem 조작 가능
    fun addItem(product: Product, quantity: Int) {
        require(status == OrderStatus.DRAFT) { &quot;확정된 주문은 수정 불가&quot; }
        require(items.size &amp;lt; 100) { &quot;한 주문에 100개 초과 불가&quot; }
        items.add(OrderItem(product.id, quantity, product.price))
    }
    
    fun totalAmount(): Money = items
        .map { it.subtotal() }
        .reduce { acc, money -&amp;gt; acc + money }
    
    fun confirm() {
        require(items.isNotEmpty()) { &quot;빈 주문은 확정 불가&quot; }
        status = OrderStatus.CONFIRMED
    }
    
    // 외부에 노출할 땐 읽기 전용
    fun items(): List&amp;lt;OrderItem&amp;gt; = items.toList()
}

// Aggregate 내부 엔티티 (외부에서 직접 생성/접근 X)
class OrderItem(
    val productId: ProductId,
    var quantity: Int,
    val unitPrice: Money
) {
    fun subtotal(): Money = unitPrice * quantity
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;*Aggregate 설계 원칙&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;작게 유지&lt;/b&gt;: 큰 애그리거트는 동시성&amp;middot;성능 문제를 일으킴&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ID로 다른 애그리거트 참조&lt;/b&gt;: Order가 User 객체를 직접 들고 있지 말고 userId: UserId로 참조&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한 트랜잭션엔 하나의 애그리거트만 변경&lt;/b&gt;: 여러 애그리거트를 한 번에 바꿔야 한다면 도메인 이벤트로 비동기 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. Repository (리포지토리)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 애그리거트를 저장하고 조회하는 추상화&lt;/b&gt;입니다. 도메인 입장에서는 컬렉션처럼 보이고, 실제 DB 구현은 인프라 계층에 숨깁니다.&lt;/p&gt;
&lt;pre id=&quot;code_1778523303487&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 도메인 계층 - 인터페이스만 정의
interface OrderRepository {
    fun findById(id: OrderId): Order?
    fun save(order: Order)
    fun findByUserId(userId: UserId): List&amp;lt;Order&amp;gt;
}

// 인프라 계층 - 실제 구현 (Room, Retrofit 등)
class OrderRepositoryImpl(
    private val orderDao: OrderDao,
    private val mapper: OrderMapper
) : OrderRepository {
    override fun findById(id: OrderId): Order? {
        return orderDao.findById(id.value)?.let { mapper.toDomain(it) }
    }
    
    override fun save(order: Order) {
        orderDao.insertOrUpdate(mapper.toEntity(order))
    }
    
    override fun findByUserId(userId: UserId): List&amp;lt;Order&amp;gt; {
        return orderDao.findByUserId(userId.value).map { mapper.toDomain(it) }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요 원칙&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;애그리거트 단위로만 조작&lt;/b&gt;: OrderItemRepository 같은 건 만들지 않음. Order를 통해 접근&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인터페이스는 도메인 계층, 구현체는 인프라 계층&lt;/b&gt;: 의존성 역전(DIP)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컬렉션처럼 설계&lt;/b&gt;: findAll(), save(), remove() 같은 메서드. SQL 냄새 풍기지 않게&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드의 Repository 패턴과 비슷하지만 DDD의 Repository는 &lt;b&gt;반드시 애그리거트만 다룬다&lt;/b&gt;는 점이 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 Repository는 보통 DTO나 단순 모델까지 다 다루죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5. Domain Service (도메인 서비스)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 특정 엔티티나 값 객체에 자연스럽게 속하지 않는 도메인 로직&lt;/b&gt;을 담는 곳입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언제 쓰나?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 애그리거트를 협력시켜야 할 때&lt;/li&gt;
&lt;li&gt;어느 한 객체에 책임을 두기 어색할 때&lt;/li&gt;
&lt;li&gt;외부 시스템과 협력해야 하는 도메인 로직&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1778523503552&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 두 계좌 사이 송금 - Account 하나에 두기 어색함
class TransferService(
    private val accountRepository: AccountRepository
) {
    fun transfer(fromId: AccountId, toId: AccountId, amount: Money) {
        val from = accountRepository.findById(fromId) ?: error(&quot;계좌 없음&quot;)
        val to = accountRepository.findById(toId) ?: error(&quot;계좌 없음&quot;)
        
        from.withdraw(amount)   // 도메인 로직은 여전히 Account 안에
        to.deposit(amount)
        
        accountRepository.save(from)
        accountRepository.save(to)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&lt;/b&gt;: 도메인 서비스로 도망치는 게 너무 쉬워서 남발하면 &lt;b&gt;빈혈 도메인 모델(Anemic Domain Model)&lt;/b&gt; 이 됩니다. 엔티티는 데이터만 들고 서비스가 모든 로직을 갖는 안티패턴이에요. 먼저 엔티티/값 객체에 책임을 두려고 시도하고, 정말 어색할 때만 도메인 서비스로 빼야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;6. Domain Event (도메인 이벤트)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 도메인에서 의미 있는 일이 일어났음을 알리는 객체&lt;/b&gt;입니다. 과거형으로 이름 짓습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1778523692998&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sealed class DomainEvent {
    abstract val occurredAt: Instant
}

data class OrderPlaced(
    val orderId: OrderId,
    val userId: UserId,
    val totalAmount: Money,
    override val occurredAt: Instant = Instant.now()
) : DomainEvent()

data class PaymentCompleted(
    val orderId: OrderId,
    val paymentId: PaymentId,
    override val occurredAt: Instant = Instant.now()
) : DomainEvent()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 쓰나?&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;애그리거트 간 결합도 낮춤&lt;/b&gt;: Order가 Payment를 직접 호출하지 않고, &quot;주문됨&quot; 이벤트만 발행. 결제 모듈이 이벤트를 구독해서 알아서 처리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨텍스트 간 통합&lt;/b&gt;: 주문 컨텍스트의 이벤트를 배송&amp;middot;알림 컨텍스트가 구독.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이벤트 소싱(Event Sourcing)의 기반&lt;/b&gt;: 상태가 아닌 이벤트의 흐름으로 시스템을 구성.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1778523738231&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Order(/* ... */) {
    private val events = mutableListOf&amp;lt;DomainEvent&amp;gt;()
    
    fun confirm() {
        require(items.isNotEmpty())
        status = OrderStatus.CONFIRMED
        events.add(OrderPlaced(id, userId, totalAmount()))  // 이벤트 발생
    }
    
    fun pullEvents(): List&amp;lt;DomainEvent&amp;gt; {
        val pulled = events.toList()
        events.clear()
        return pulled
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드로 비유하면 LiveData/Flow로 상태 변화를 알리는 것과 발상이 비슷해요. 다만 도메인 이벤트는 &lt;b&gt;비즈니스 사실의 기록&lt;/b&gt;이라는 점에서 의미가 더 무겁습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;7. Factory (팩토리)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 복잡한 애그리거트 생성 로직을 캡슐화&lt;/b&gt;하는 객체입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언제 쓰나?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성 자체에 복잡한 규칙이 있을 때&lt;/li&gt;
&lt;li&gt;여러 객체를 조합해야 할 때&lt;/li&gt;
&lt;li&gt;다양한 형태로 생성될 수 있을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1778523883855&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class OrderFactory(
    private val pricingPolicy: PricingPolicy,
    private val discountService: DiscountService
) {
    fun createFromCart(cart: Cart, user: User): Order {
        require(cart.items.isNotEmpty()) { &quot;빈 장바구니로 주문 불가&quot; }
        
        val orderItems = cart.items.map { cartItem -&amp;gt;
            val price = pricingPolicy.calculate(cartItem.product, user)
            val discounted = discountService.applyDiscount(price, user)
            OrderItem(cartItem.product.id, cartItem.quantity, discounted)
        }
        
        return Order(
            id = OrderId.generate(),
            userId = user.id,
            items = orderItems.toMutableList(),
            shippingAddress = user.defaultAddress
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;팁&lt;/b&gt;: 생성 로직이 단순하면 굳이 Factory를 만들지 말고 생성자나 정적 팩토리 메서드로 충분합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1778523946567&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Order private constructor(...) {
    companion object {
        fun create(userId: UserId, items: List&amp;lt;OrderItem&amp;gt;): Order {
            require(items.isNotEmpty())
            return Order(OrderId.generate(), userId, items.toMutableList())
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;DDD 개념안드로이드 일반적 위치&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Entity, Value Object&lt;/td&gt;
&lt;td&gt;domain/model/ 안의 클래스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate&lt;/td&gt;
&lt;td&gt;동일하게 domain/model/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repository (인터페이스)&lt;/td&gt;
&lt;td&gt;domain/repository/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repository (구현체)&lt;/td&gt;
&lt;td&gt;data/repository/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain Service&lt;/td&gt;
&lt;td&gt;domain/service/ 또는 UseCase로 흡수되기도 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain Event&lt;/td&gt;
&lt;td&gt;Flow/Channel로 발행하거나 EventBus 활용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Factory&lt;/td&gt;
&lt;td&gt;domain/factory/ 또는 companion object&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <author>hik14</author>
      <guid isPermaLink="true">https://hik-coding.tistory.com/369</guid>
      <comments>https://hik-coding.tistory.com/369#entry369comment</comments>
      <pubDate>Tue, 12 May 2026 03:27:41 +0900</pubDate>
    </item>
    <item>
      <title>도메인 주도 설계 (Domain Driven Design - DDD)의 기초 개념정리1.</title>
      <link>https://hik-coding.tistory.com/368</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정의&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 주도 설계(DDD)는 소프트웨어를 그것이 다루는 업무 분야(도메인)에 맞춰 모델링하는 설계 방식입니다. 특정 기술이나 원칙이 아니라 사고하는 방법에 가까우며, 복잡한 업무 분야를 다루는 프로젝트에서 핵심을 놓치지 않도록 돕는 접근입니다. &lt;b&gt;핵심은 도메인 전문가와 개발자가 같은 용어(유비쿼터스 언어)로 소통해 이해의 차이에서 오는 오해를 없애고, 비즈니스 문제를 제대로 풀어내는 소프트웨어를 만드는 것입니다.&lt;/b&gt; 코드가 곧 비즈니스 지도가 되게 함으로써, 요구사항 변화에 유연하게 대응하고 소프트웨어의 유지보수성을 극대화하는 설계 철학입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-11 오전 12.41.33.png&quot; data-origin-width=&quot;749&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bH8FEO/dJMcageo00O/WA2AJUKntxMb8fIoaQ92Vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bH8FEO/dJMcageo00O/WA2AJUKntxMb8fIoaQ92Vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bH8FEO/dJMcageo00O/WA2AJUKntxMb8fIoaQ92Vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbH8FEO%2FdJMcageo00O%2FWA2AJUKntxMb8fIoaQ92Vk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;749&quot; height=&quot;440&quot; data-filename=&quot;스크린샷 2026-05-11 오전 12.41.33.png&quot; data-origin-width=&quot;749&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;전략적 설계 (Strategic Design)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-11 오전 1.35.15.png&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdMiMC/dJMcafT6oOT/2GSKJXx5sUkQEgjmVq0mMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdMiMC/dJMcafT6oOT/2GSKJXx5sUkQEgjmVq0mMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdMiMC/dJMcafT6oOT/2GSKJXx5sUkQEgjmVq0mMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdMiMC%2FdJMcafT6oOT%2F2GSKJXx5sUkQEgjmVq0mMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;740&quot; height=&quot;722&quot; data-filename=&quot;스크린샷 2026-05-11 오전 1.35.15.png&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;722&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Problem Space (문제 공간)&lt;/b&gt;&lt;/h4&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 자체를 이해하는 영역입니다. &quot;우리가 어떤 문제를 해결하려는 거지?&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 도메인(Domain)과 서브도메인(Subdomain)&lt;/h4&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;도메인&lt;/b&gt;은 비즈니스 영역 전체를 뜻합니다. 쿠팡이라면 &quot;이커머스&quot; 전체가 도메인이다.. 하지만 도메인 전체를 한 번에 다루기엔 너무 크기 때문에 &lt;b&gt;서브 도메인&lt;/b&gt;으로 쪼갠다. 쿠팡 도메인이라면 상품 카탈로그, 주문, 결제, 배송, 리뷰, 회원, 검색, 추천 등으로 나눌 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브 도메인의 분류&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Core Subdomain (핵심 서브도메인)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비즈니스의 경쟁력이 나오는 영역. 회사가 살아남는 이유 그 자체.&lt;/li&gt;
&lt;li&gt;가장 똑똑한 개발자, 가장 많은 리소스, 가장 정교한 모델링이 들어가야 합니다.&lt;/li&gt;
&lt;li&gt;절대 외주 주거나 패키지 사서 쓰면 안 되는 영역.&lt;/li&gt;
&lt;li&gt;예: 쿠팡의 &quot;물류&amp;middot;배송 최적화&quot;, 토스의 &quot;송금&amp;middot;인증&quot;, 넷플릭스의 &quot;추천 알고리즘&quot;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Supporting Subdomain (지원 서브도메인)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핵심을 받쳐주지만, 그 자체로 차별화 포인트는 아닌 영역.&lt;/li&gt;
&lt;li&gt;우리 비즈니스에 맞게 자체 개발은 해야 하지만, Core만큼 공들일 필요는 없음.&lt;/li&gt;
&lt;li&gt;단순한 CRUD에 가까울 수 있고, 주니어가 맡아도 되는 영역.&lt;/li&gt;
&lt;li&gt;예: 쿠팡의 &quot;리뷰 시스템&quot;, &quot;쿠폰 관리&quot;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Generic Subdomain (일반 서브도메인)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어느 회사나 똑같이 필요한 범용 영역.&lt;/li&gt;
&lt;li&gt;직접 만들지 말고 외부 솔루션&amp;middot;SaaS&amp;middot;오픈소스로 해결하는 게 합리적.&lt;/li&gt;
&lt;li&gt;직접 만들면 시간 낭비.&lt;/li&gt;
&lt;li&gt;예: 인증/로그인 &amp;rarr; Auth0&amp;middot;Firebase Auth, 결제 게이트웨이 &amp;rarr; 토스페이먼츠, 이메일 발송 &amp;rarr; SendGrid.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 분류가 중요한 이유는 &lt;/span&gt;&lt;b&gt;&quot;어디에 자원을 집중할지&quot;&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 정하는 의사결정 도구이기 때문입니다. Core에는 베스트 인력을, Generic에는 SaaS를. 안드로이드 앱으로 비유하면 비즈니스 핵심 화면은 직접 정성껏 만들고, 로그인은 Firebase Auth 붙이고, 푸시는 FCM 쓰는 것&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Domain Experts (도메인 전문가)&lt;/h4&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 비즈니스를 가장 잘 아는 사람. 개발자가 아닌 현업 담당자를 의미합니다. 보험 도메인이면 보험 설계사, 물류면 창고 관리자, 의료면 의사. 이들의 머릿속에 있는 암묵지를 끌어내는 게 도메인 분석의 출발점.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Knowledge Discovery (지식 수집)&lt;/h4&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 전문가의 머릿속에 있는 비즈니스 지식을 발굴하는 활동. 인터뷰, 워크샵, 도메인 문서 분석, 실제 업무 관찰 등으로 이루어진다. 개발자가 일방적으로 듣는 게 아니라, 질문하고 모델로 그려보고 다시 검증받는 협력 과정.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Event Storming (이벤트 스토밍)&lt;/h4&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Knowledge Discovery의 대표적 기법. 큰 벽에 포스트잇을 붙여가며 도메인을 시각화하는 워크샵.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이벤트스토밍 과정에서 자연스럽게 바운디드 컨텍스트의 경계가 드러납니다. 이벤트의 흐름이 끊기는 지점, 용어가 달라지는 지점이 컨텍스트 경계가 된다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. Communication &amp;amp; Ubiquitous Language (소통과 유비쿼터스 언어)&lt;/h4&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;도메인 전문가와 개발자가 같은 단어로 대화하기로 합의한 언어. &lt;/span&gt;&lt;b&gt;번역하지 않는 것&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 핵심입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;흔한 안티패턴:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현업: &quot;고객이 장바구니에 담은 상품&quot;&lt;/li&gt;
&lt;li&gt;개발자: &quot;User의 Cart 엔티티 안에 있는 ProductItem&quot;&lt;/li&gt;
&lt;li&gt;회의에서는 매번 머릿속으로 번역 &amp;rarr; 오해와 버그의 원천&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유비쿼터스 언어를 쓰면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회의에서도, 코드에서도, 문서에서도 모두 똑같이 &quot;장바구니&quot;와 &quot;상품&quot;을 사용&lt;/li&gt;
&lt;li&gt;클래스명 Cart, 메서드명 addProduct() &amp;mdash; 코드가 곧 도메인 언어&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kotlin으로 보면: class Cart { fun addProduct(product: Product) } 이런 식으로 비즈니스 용어가 그대로 코드에 드러남&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*주의할 점은 유비쿼터스 언어가 &lt;b&gt;컨텍스트별로 다르다&lt;/b&gt;는 것. 주문 컨텍스트의 &quot;상품&quot;과 배송 컨텍스트의 &quot;상품&quot;은 다른 모델일 수 있습니다. 그래서 다음에 나오는 바운디드 컨텍스트가 필요해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Solution Space (해결 공간)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 어떻게 시스템 구조로 풀어낼지를 다루는 영역&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Bounded Context (바운디드 컨텍스트)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하나의 모델과 하나의 유비쿼터스 언어가 일관되게 통하는 경계&lt;/b&gt;입니다. DDD의 가장 중요한 개념&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 필요한가? &lt;u&gt;시스템이 커지면 같은 단어가 다른 의미를 갖게 됩니다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 &amp;mdash; 이커머스에서 &quot;상품(Product)&quot;이라는 단어:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;카탈로그 컨텍스트&lt;/b&gt;: 이름, 설명, 카테고리, 이미지, 가격 &amp;mdash; &quot;고객에게 보여주는 상품 정보&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재고 컨텍스트&lt;/b&gt;: SKU, 창고 위치, 수량 &amp;mdash; &quot;물리적으로 관리되는 단위&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주문 컨텍스트&lt;/b&gt;: 상품 ID, 주문 시점 가격, 수량 &amp;mdash; &quot;주문에 담긴 스냅샷&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배송 컨텍스트&lt;/b&gt;: 무게, 부피, 포장 단위 &amp;mdash; &quot;운반 가능한 형태&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 &quot;상품&quot;인데 컨텍스트마다 모델이 완전히 다르다. 이걸 하나의 거대한 Product 클래스로 합치려고 하면 필드가 50개 넘는 괴물이 된다. 그래서 컨텍스트별로 경계를 긋고 각자의 모델을 갖게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;*서브도메인과의 차이:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서브도메인 = Problem Space, 비즈니스 영역의 구분&lt;/li&gt;
&lt;li&gt;바운디드 컨텍스트 = Solution Space, 모델/코드의 경계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상적으로는 1:1이지만 현실에서는 어긋나는 경우도 많습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Context Map (컨텍스트 맵)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 바운디드 컨텍스트가 서로 어떤 관계로 연결되는지 그린 지도. 단순한 시스템 구조도가 아니라 &lt;b&gt;팀 간 관계와 협력 방식까지 표현&lt;/b&gt;하는 게 특징입니다. (콘웨이의 법칙 =&amp;gt; &quot;소프트웨어 구조는 팀의 조직도와 똑같이 만들어진다&quot;)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 컨텍스트 간 통합 패턴&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 컨텍스트가 만났을 때 어떻게 연결할지에 대한 카탈로그입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Partnership (파트너십)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 팀이 운명 공동체. 함께 성공하거나 함께 실패.&lt;/li&gt;
&lt;li&gt;양쪽이 긴밀하게 협력해서 인터페이스를 같이 진화시킴.&lt;/li&gt;
&lt;li&gt;예: 주문팀과 결제팀이 새 결제 방식을 같이 만들 때.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Shared Kernel (공유 커널)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 컨텍스트가 모델의 일부를 공유.&lt;/li&gt;
&lt;li&gt;공유된 부분은 양쪽 팀의 합의 없이 절대 변경 불가.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;강결합이라 신중하게 써야 함&lt;/b&gt;. 보통 핵심 도메인 객체나 공유 라이브러리.&lt;/li&gt;
&lt;li&gt;예: &quot;Money&quot;, &quot;Address&quot; 같은 값 객체.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Customer-Supplier (고객-공급자)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한쪽이 공급자(Upstream), 다른 쪽이 고객(Downstream).&lt;/li&gt;
&lt;li&gt;고객이 요구사항을 내면 공급자가 일정에 반영해줌. 협상 가능한 관계.&lt;/li&gt;
&lt;li&gt;예: 주문(고객)이 결제(공급자)에게 &quot;이런 응답 필드가 필요해요&quot; 요청 &amp;rarr; 결제팀이 다음 스프린트에 반영.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Conformist (순응자)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다운스트림이 업스트림 모델을 그대로 따라감. 협상력 없음.&lt;/li&gt;
&lt;li&gt;외부 시스템이나 사내 다른 팀이 우리 요청을 안 받아줄 때.&lt;/li&gt;
&lt;li&gt;예: 외부 PG사 API 모델을 그대로 우리 도메인에 받아들이는 경우.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Anti-Corruption Layer / ACL (부패 방지 계층)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 모델이 우리 도메인을 오염시키지 못하도록 &lt;b&gt;번역 계층&lt;/b&gt;을 둠.&lt;/li&gt;
&lt;li&gt;외부의 이상한 모델&amp;middot;용어가 우리 코드에 스며드는 걸 막음.&lt;/li&gt;
&lt;li&gt;DDD에서 매우 자주 쓰이는 패턴.&lt;/li&gt;
&lt;li&gt;예: 레거시 시스템과 통신할 때, 레거시의 USR_INFO_TBL_DTO를 우리 도메인의 Customer로 변환해주는 어댑터 레이어.&lt;/li&gt;
&lt;li&gt;안드로이드로 비유하면 Repository 패턴에서 외부 API 응답 DTO를 도메인 모델로 매핑하는 매퍼 클래스가 ACL의 역할에 가까워요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Open Host Service (개방 호스트 서비스)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 다운스트림이 사용할 수 있도록 잘 정의된 프로토콜&amp;middot;API를 공개.&lt;/li&gt;
&lt;li&gt;공급자가 일관된 API를 제공해서 각 고객별로 커스텀하지 않음.&lt;/li&gt;
&lt;li&gt;예: 사내 공통 인증 서비스, 결제 게이트웨이.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Published Language (공표된 언어)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Open Host Service와 짝을 이룸. 모두가 따르는 표준 데이터 형식.&lt;/li&gt;
&lt;li&gt;예: REST API의 JSON 스키마, ProtoBuf 정의, 산업 표준(HL7, FHIR 등).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Separate Ways (각자의 길)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통합하지 말고 그냥 분리해서 가자.&lt;/li&gt;
&lt;li&gt;통합 비용이 가치보다 클 때의 의식적 선택.&lt;/li&gt;
&lt;li&gt;예: 영업팀과 인사팀 시스템처럼 사실상 접점이 없는 경우.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Big Ball of Mud (거대한 진흙 공)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;경계가 없는 혼돈 상태. 의도된 패턴이 아니라 &lt;b&gt;현재 상태를 직시&lt;/b&gt;하는 라벨.&lt;/li&gt;
&lt;li&gt;컨텍스트 맵에 굳이 표시하는 이유: &quot;이 영역은 진흙공이니 ACL 두고 격리하자&quot; 같은 결정을 위해.&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>hik14</author>
      <guid isPermaLink="true">https://hik-coding.tistory.com/368</guid>
      <comments>https://hik-coding.tistory.com/368#entry368comment</comments>
      <pubDate>Mon, 11 May 2026 01:45:00 +0900</pubDate>
    </item>
    <item>
      <title>Compose UI 성능 최적화 하기</title>
      <link>https://hik-coding.tistory.com/367</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;참조영상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=ahXLwg2JYpc&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=ahXLwg2JYpc&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.&amp;nbsp; 언제 성능을 최적화 할 것인가?&amp;nbsp; 성급한 최적화를 하지 말라!&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose는 이미 기본적으로 꽤 효율적이기 때문에, 앱을 만들고 실제로 성능 문제가 관찰될 때 최적화하라고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 최적화는 &lt;b data-index-in-node=&quot;15&quot; data-path-to-node=&quot;13&quot;&gt;측정 &amp;rarr; 분석 &amp;rarr; 개선 &amp;rarr; 재검증&lt;/b&gt;의 반복이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;가장 적은 코딩 노력을 들여 사용자에게 가장 부드러운 경험을 제공하는 지점&quot;을 찾는 것이 진정한 의미의 최적화입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size20&quot;&gt;조기 최적화를 경계하라 (Avoid Premature Optimization)&lt;/h4&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&quot;조기 최적화는 만악의 근원&quot;이라는 유명한 격언처럼, 성능 문제가 확인되지 않은 상태에서 복잡한 최적화 기법을 적용하는 것은 피해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;유지보수성 우선:&lt;/b&gt; 과도한 최적화는 코드를 읽기 어렵게 만들고 버그를 유발합니다. 먼저 깨끗하고 동작하는 코드를 작성한 뒤, &lt;span style=&quot;color: #ee2323;&quot;&gt;병목 지점이 발견되었을 때만&lt;/span&gt; 정밀 타격을 가해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;Tools, not Rules:&lt;/b&gt; &quot;모든 리스트에는 반드시 이 기법을 써야 한다&quot;는 규칙보다는, 현재 상황에서 도구(Layout Inspector, Profiler 등)가 가리키는 문제를 해결하는 데 집중하세요&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;1&quot; data-ke-size=&quot;size20&quot;&gt;추측하지 말고 측정하라 (Measure, Don't Guess)&lt;/h4&gt;
&lt;p data-path-to-node=&quot;2&quot; data-ke-size=&quot;size16&quot;&gt;최적화에서 가장 위험한 것은 &quot;이 코드가 느릴 것 같다&quot;는 직감에 의존하는 것&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;3&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,0,0&quot;&gt;벤치마킹 도구 활용:&lt;/b&gt; Android에서는 Macrobenchmark를 통해 실제 사용자가 느끼는 앱 시작 시간이나 스크롤 성능(Jank)을 수치화&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,1,0&quot;&gt;비용 확인:&lt;/b&gt; 최적화도 시간이라는 비용이 듭니다. 측정을 통해 성능 저하가 미미한 수준이라면, &lt;b&gt;코드의 가독성을 희생하며 최적화하지 않는 것이 더 나은 전략일 수 있습니다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*성능 테스트는 반드시 R8이 활성화된 릴리스 모드에서 진행해야 정확한 결과를 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.&amp;nbsp; 상태 읽기 지연 (Defer Reading State)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;* Compose UI 렌더링 단계 이해하기&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-15 오후 10.59.30.png&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rbmok/dJMcaiwbJmE/3IYoPNT972AGGjD6tVYJtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rbmok/dJMcaiwbJmE/3IYoPNT972AGGjD6tVYJtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rbmok/dJMcaiwbJmE/3IYoPNT972AGGjD6tVYJtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frbmok%2FdJMcaiwbJmE%2F3IYoPNT972AGGjD6tVYJtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;867&quot; height=&quot;90&quot; data-filename=&quot;스크린샷 2026-04-15 오후 10.59.30.png&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;90&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Compose는 한 프레임을 그리기 위해 다음 단계를 &lt;b&gt;단방향(UDF)&lt;/b&gt; 으로 거칩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;컴포지션 (Composition)&lt;/b&gt; &amp;mdash; 무엇을 그릴지 결정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Composable 함수를 실행해서 UI &lt;b&gt;트리(레이아웃 노드)를 생성&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레이아웃 (Layout)&lt;/b&gt; &amp;mdash; 어디에 배치할지 결정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;측정(measure) &amp;rarr; 자체 크기 결정 &amp;rarr; 자식 배치(placement) 순서로 트리를 한 번만 순회 (선형 시간)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그리기 (Drawing)&lt;/b&gt; &amp;mdash; 어떻게 렌더링할지 결정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트리를 위&amp;rarr;아래로 순회하며 캔버스에 픽셀을 그림&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;상태 읽기(state read) 추적&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Compose는 &lt;b&gt;어느 단계에서 어떤 state를 읽었는지&lt;/b&gt;를 자동으로 추적해서, 그 state가 변경되면 &lt;b&gt;필요한 단계만&lt;/b&gt; 재실행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777472887236&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 컴포지션에서 state 읽음 &amp;rarr; 매 스크롤마다 리컴포지션
Modifier.offset(
    with(LocalDensity.current) { 
        (listState.firstVisibleItemScrollOffset / 2).toDp() 
    }
)

// 레이아웃 단계에서 state 읽음 &amp;rarr; 리컴포지션 스킵
Modifier.offset {
    IntOffset(0, listState.firstVisibleItemScrollOffset / 2)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.&amp;nbsp; &lt;span&gt;&amp;nbsp;&lt;/span&gt;Stability(안정성) &amp;mdash; Compose가 스킵을 결정하는 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리컴포지션을 건너뛸지 말지를 결정하는 가장 중요한 개념입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose는 컴포저블의 파라미터가 &lt;b&gt;stable(안정적)&lt;/b&gt; 이라고 판단되면, 입력값이 같을 때 해당 컴포저블을 &lt;b&gt;스킵&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Stable의 조건&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 두 인스턴스의 equals() 결과가 항상 일관됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- public property가 변하면 컴포지션에 알림 (예: MutableState)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 모든 public property도 stable&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자동으로 stable한 타입&lt;/b&gt;: 원시 타입(Int, Boolean 등), String, &lt;b&gt;함수 타입(람다),&lt;/b&gt; MutableState 등.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Unstable한 대표 케이스&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;var 프로퍼티를 가진 클래스&lt;/li&gt;
&lt;li&gt;List, Set, Map 같은 인터페이스 (구현체가 mutable일 수 있어서 컴파일러가 보장 못함)&lt;/li&gt;
&lt;li&gt;다른 모듈의 클래스 (컴파일러가 검증 못함)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*Data Class로 val(값)으로 원시타입을 이용하여 상태를 정의한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777473723417&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class UserUiState(
    val id: Int,
    val name: String,
    val isLoggedIn: Boolean,
    val score: Double
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. Stability를 다루는 방법&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Stable / @Immutable 어노테이션: 개발자가 컴파일러에게 &quot;이건 안정적이다&quot;라고 알려주는 약속.&lt;/li&gt;
&lt;li&gt;data class라도 unstable일 수 있음: 내부에 List&amp;lt;T&amp;gt; 같은 unstable 타입이 있으면 전체가 unstable.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1777473788648&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class TodoUiState(
    val title: String,
    val items: List&amp;lt;String&amp;gt;   // val이지만 List는 unstable!
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결책&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin의 List, Set, Map 대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Kotlinx Immutable Collections&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(ImmutableList, PersistentList)를 사용&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는 wrapper 클래스를 만들어 @Immutable 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션 1: Kotlinx Immutable Collections 사용 (권장)&lt;/p&gt;
&lt;pre id=&quot;code_1777473810183&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlinx.collections.immutable.ImmutableList

data class TodoUiState(
    val title: String,
    val items: ImmutableList&amp;lt;String&amp;gt;   // ✅ stable
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션 2: @Immutable 어노테이션으로 약속&lt;/p&gt;
&lt;pre id=&quot;code_1777473847617&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Immutable
data class TodoUiState(
    val title: String,
    val items: List&amp;lt;String&amp;gt;   // 개발자가 &quot;절대 변경 안 한다&quot;고 약속
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Compose Compiler Metrics&lt;/b&gt;로 어떤 클래스/함수가 unstable로 판정됐는지 리포트 생성 가능. 컴파일러 옵션으로 활성화하면 어떤 컴포저블이 스킵되지 못하는지 정확히 진단할 수 있습니다.&lt;/p&gt;</description>
      <category>Compose</category>
      <author>hik14</author>
      <guid isPermaLink="true">https://hik-coding.tistory.com/367</guid>
      <comments>https://hik-coding.tistory.com/367#entry367comment</comments>
      <pubDate>Wed, 29 Apr 2026 23:44:45 +0900</pubDate>
    </item>
  </channel>
</rss>