<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Boiledegg's Blog</title>
    <link>https://boiledeggishere.tistory.com/</link>
    <description>이모저모 개발 이야기</description>
    <language>ko</language>
    <pubDate>Sun, 12 Apr 2026 06:35:23 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>boiledeggishere</managingEditor>
    <image>
      <title>Boiledegg's Blog</title>
      <url>https://tistory1.daumcdn.net/tistory/7174671/attach/0ce8cc0f9f1d4a2b94ad9f1fad4c5b5a</url>
      <link>https://boiledeggishere.tistory.com</link>
    </image>
    <item>
      <title>[Android] WebSocket으로 실시간 채팅 구현하기 2 - STOMP, 그리고 KrossBow</title>
      <link>https://boiledeggishere.tistory.com/entry/Android-WebSocket%EC%9C%BC%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-2-STOMP-%EA%B7%B8%EB%A6%AC%EA%B3%A0-KrossBow</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://boiledeggishere.tistory.com/entry/Android-WebSocket%EC%9C%BC%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-WebSocket%EC%9D%B4%EB%9E%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;실시간 채팅 구현 1탄&lt;/a&gt; 이후 약 2달만에 2탄을 작성하네요. 그동안 제 채팅 구조에는 피바람이 불어서 채팅 구현기를 구상했을 때와 많이 바뀌었는데요.. 2탄에서는 기존 구현 방식과 더불어 변경 이유와 현재 구현 방식을 잘 융합해서 적어보도록 하겠습니다!&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;이 글의 내용에 대해 미리 스포를 하자면, STOMP 라이브러리인 Krossbow 사용과 Krossbow 사용에서 겪은 문제점과 Krossbow 없이 1탄에서 언급한 OkHttpWebSocket을 활용한 구현을 다룹니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 어떤 기능 때문에 꽤 고생을 했다고 했죠? 그 주인공은 바로 STOMP입니다.&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;웹소켓 사용도 처음 해보는데 STOMP라는 개념이 등장해서 약간 당황했고, API 테스트 프로그램인 POSTMAN으로도 확인하기가 어려워 꽤나 애를 먹었죠. 다행히도 서버팀이 제공해준 테스트용 HTML 페이지와 친절한 설명 덕분에 잘 적용할 수 있었습니다.&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;그러면 이 STOMP가 뭔지 그리고 안드로이드에선 어떻게 적용했는지 알아봅시다.&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;STOMP란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STOMP는 &lt;b&gt;SIMPLE TEXT ORIENTED MESSAGING PROTOCOL&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;STOMP는 웹소켓 위에서 동작합니다. STOMP 프로토콜만의 간결함과 유연성이 실시간 메시징을 지원하기에 적합하고, 클라이언트와 브로커 간의 메시징이 간소화됩니다.&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;STOMP는 &lt;b&gt;Publisher/Subscriber 구조&lt;/b&gt;로 이뤄져 있고 그 사이에서&amp;nbsp;&lt;b&gt;Broker가 메시지를 중계&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;발행자(Publisher)가 특정 토픽이나 큐에 메시지를 생성하고 발행합니다.&lt;/li&gt;
&lt;li&gt;메시지 브로커(Message Broker)가 발행된 메시지를 관리합니다. 메시지 라우팅, 분배, 필터링 등의 역할을 수행합니다.&lt;/li&gt;
&lt;li&gt;구독자(Subscriber)는 특정 주제나 큐에 구독할 수 있고, 브로커는 등록된 모든 구독자에게 해당 주제의 메시지들을 전달합니다.&lt;/li&gt;
&lt;/ul&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;STOMP 메시지 형식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STOMP는 텍스트 기반으로 만들어진 프로토콜이라 사람의 눈으로 봤을 때 매우 직관적인 프레임을 가지고 있습니다.&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;&lt;b&gt;CONNECT&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;/b&gt;STOMP 기반 연결을 수립하기 위한 연결 요청 프레임으로, 웹소켓을 연결한 후에 수행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;accept-version = STOMP 프로토콜 버전&lt;/li&gt;
&lt;li&gt;host = 호스트 이름 &lt;s&gt;(웹소켓 주소와 혼동하지 말자!)&lt;/s&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;CONNECT&lt;br /&gt;accept-version:1.1,1.2&lt;br /&gt;host:{호스트 주소}&lt;br /&gt;&lt;br /&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;b&gt;SUBSCRIBE&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 토픽의 메시지를 구독하기 위한 요청 프레임입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id = 구독 구분용 id&lt;/li&gt;
&lt;li&gt;destination = 구독할 채널의 주소&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;SUBSCRIBE&lt;br /&gt;id: 0&lt;br /&gt;destination:/topic/a&lt;br /&gt;&lt;br /&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;b&gt;SEND&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;destination = 메시지를 보낼  목적지(주소)&lt;/li&gt;
&lt;li&gt;content-type = 메시지 본문의 형식&lt;/li&gt;
&lt;li&gt;content-length = 메시지 본문의 길이&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;SEND&lt;br /&gt;destination:/queue/a&lt;br /&gt;content-type:application/json&lt;br /&gt;content-length:12&lt;br /&gt;&lt;br /&gt;hello world^@&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;b&gt;MESSAGE&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;subscription = 구독 요청을 보낼 때 추가한 id&lt;/li&gt;
&lt;li&gt;message-id = 서버가 부여한 메시지 고유 식별자&lt;/li&gt;
&lt;li&gt;destination = 메시지가 전송된 목적지 주소&lt;/li&gt;
&lt;li&gt;content-type = 메시지 본문의 형식&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;MESSAGE &lt;br /&gt;subscription:0 &lt;br /&gt;message-id:007 &lt;br /&gt;destination:/queue/a &lt;br /&gt;content-type:text/plain &lt;br /&gt;&lt;br /&gt;hello queue a^@&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;각 프레임마다 언급된 헤더 속성들 이외에도 여러 속성들이 있고, DISCONNECT, ERROR, BEGIN 등 더 많은 프레임이 있습니다. 궁금하시면 &lt;a href=&quot;https://stomp.github.io/stomp-specification-1.2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;STOMP 공식 문서&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Krossbow&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 알아본 STOMP 프로토콜을 직접 구현하려면 시간도 많이 들고 복잡하겠죠? 그래서 STOMP를 구현하는 많은 프로젝트에선 Krossbow 라이브러리를 사용한다고 합니다.&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;Krossbow는 웹소켓 위에서 STOMP 프로토콜을 사용할 수 있도록 도와주는 오픈소스 라이브러리&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;안드로이드에서 네트워크 기능을 구현하면 OkHttp나 Ktor 라이브러리를 사용하는데, Krossbow는 이 라이브러리들과 잘 융합되어 사용이 가능합니다. Kotlinx Serialization, Moshi 등의 직렬화 라이브러리도 지원해 기존 프로젝트 구조에 잘 넣을 수 있다고 합니다.&lt;s&gt;(만 저는 잘 사용하지 못했어요..ㅎㅎ)&lt;/s&gt;&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;Krossbow 다루기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;OkHttp와 Serialization을 활용해 REST API 통신 환경을 구축해둔 상태였기 때문에 이 라이브러리들을 활용해 구현했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 두 라이브러리에 대한 Krossbow 라이브러리 의존성을 추가해줬습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;implementation(&quot;org.hildan.krossbow:krossbow-websocket-okhttp:7.0.0&quot;) &lt;br /&gt;implementation(&quot;org.hildan.krossbow:krossbow-stomp-kxserialization:7.0.0&quot;)&lt;br /&gt;implementation(&quot;org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.6.3&quot;)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OkHttp는 웹소켓을 지원하는 WebSocket 클래스를 제공하는데, Krossbow는 이 클래스를 &lt;b&gt;OkHttpWebSocketClient&lt;/b&gt;를 활용해 Krossbow 전용 웹소켓 인터페이스로 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OkHttpWebSocketClient&lt;/b&gt;에는 &lt;b&gt;OkHttpClient &lt;/b&gt;인자를 통해 커스터마이징된 &lt;b&gt;OkHttpClient&lt;/b&gt;를 전달할 수 있다고 하네요&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761206395827&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val okHttpClient = OkHttpClient.Builder()
    .callTimeout(Duration.ofMinutes(1))
    .pingInterval(Duration.ofSeconds(10))
    .build()
val wsClient = OkHttpWebSocketClient(okHttpClient)
val stompClient = StompClient(wsClient)&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;Client 객체를 만들었다면, 메시지를 주고 받기 위한 세션을 열어야겠죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StompClient.connect()는 웹소켓에 연결하는 동시에 &lt;b&gt;CONNECT&lt;/b&gt; 프레임을 전송해 STOMP 프로토콜 기반 연결을 수립합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761207810085&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val stompClient = StompClient(OkHttpWebSocketClient())
val session = stompClient.connect(
   url = url, 	// 웹소켓 url
   host = host 	// STOMP 호스트 주소
)&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;세션 연결 이후엔 메시지 직렬화 설정을 해줘야 합니다. StompSession.withJsonConversions()을 호출하면 Kotlinx Serialization 기반 직렬화 기능을 갖춘 Session 객체가 반환됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761207223682&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Json 객체를 인자로 넘겨줄 수 있다.
val json = Json {
   ignoreUnknownKeys = true
   prettyPrint = true
}
val session = StompClient(OkHttpWebSocketClient()).connect(url)
val jsonStompSession: StompSessionWithKxSerialization = session.withJsonConversions(json)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CONNECT&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 이미 살펴봤듯이, StompClient.connect를 활용해 WebSocket 연결 후 곧바로 STOMP 연결을 수행할 수 있습니다.&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;SUBSCRIBE&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독의 경우, session의 subscribe()을 통해 서버에게 SUBSCRIBE 요청을 보낼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은, subscribe의 반환값이 &lt;b&gt;Flow&lt;/b&gt;라는 것입니다. Krossbow는 실시간으로 들어오는 메시지를 &lt;b&gt;코루틴 Flow&lt;/b&gt;를 활용해 스트림 형태로 제공합니다. 스트림을 통해 들어오는 메시지들은 Kotlinx Serialization에 의해 직렬화되는데, 이때 직렬화 기준을 &lt;b&gt;deserializer&lt;/b&gt; 인자를 통해 제공할 수 있습니다!&lt;/p&gt;
&lt;pre id=&quot;code_1761208555001&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// session: StompSessionWithKxSerialization
suspend fun &amp;lt;T : Any&amp;gt; getMessageStream(deserializer: KSerializer&amp;lt;T&amp;gt;): Flow&amp;lt;T&amp;gt; {
   val messageFlow: Flow&amp;lt;T&amp;gt; = session.subscribe(
      headers = StompSubscribeHeaders(
         destination, 
      ),
      customHeaders = mapOf(
   		// Map 형태로 다른 헤더 추가 가능
      )
      deserializer = deserializer,
   )
   return messageFlow
}&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;SEND&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 전송하기 위해선 session의 send() 함수를 사용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드를 통해 메시지를 보낼 땐 직접 메시지를 직렬화해야 합니다. 이렇게 직렬화한 메시지는 FrameBody로 래핑해 send 함수의 body 인자로 전달합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761209602853&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// session: StompSessionWithKxSerialization
suspend fun &amp;lt;T : Any&amp;gt;sendMessage(
   destination: String,
   request: T,
   serializer: KSerializer&amp;lt;T&amp;gt;
) {
   session?.send(
      headers = StompSendHeaders(
         destination = destination,
         configure = {
            contentType = &quot;application/json&quot;
         }
      ),
      body = FrameBody.Text(Json().encodeToString(serializer, encode))
   )
}&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;Disconnect, unsubscribe 등에 대한 메서드들과 맞춤 헤더 클래스들도 존재하여, 이게 Krossbow 사용의 전부입니다. 간단하죠?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Krossbow와 헤어진 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 저는 이 편한 라이브러리를 제대로 사용할 수 없었습니다. 이유는 3가지 정도로 꼽을 수 있을 것 같은데요&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;적은 레퍼런스와 불편한 디버깅&lt;/li&gt;
&lt;li&gt;구독 채널 수에 비례해 증가하는 메시지 스트림으로 인한 관리 복잡성 증가&lt;/li&gt;
&lt;li&gt;WebSocket Client를 사용할 때 한계점 존재&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 스트림이 활성화된 상태에서 웹소켓 연결이 끊겨 예외가 발생했을 때 앱 프로세스가 종료되버리는 오류의 원인을 파악하고자 애썼지만, 불분명한 스택트레이스와 적은 레퍼런스로 인해 해결에 난항을 겪었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 위 이슈를 해결하는 과정에서 웹소켓 클라이언트를 사용했을 때 Krossbow의 자동 재연결/재구독 기능이 지원되지 않는다는 이슈를 보고 말았어요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG58n8/dJMb8V0QRV0/5e6E05xce8gJE8DUp0kcQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG58n8/dJMb8V0QRV0/5e6E05xce8gJE8DUp0kcQ0/img.png&quot; data-alt=&quot;어뜩하라고...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG58n8/dJMb8V0QRV0/5e6E05xce8gJE8DUp0kcQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG58n8%2FdJMb8V0QRV0%2F5e6E05xce8gJE8DUp0kcQ0%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;845&quot; height=&quot;204&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;어뜩하라고...&lt;/figcaption&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;채팅 기능 구현 기간은 정해져 있는데, 오류가 해결될 기미가 보이지 않자 저는 과감히 Krossbow 라이브러리를 버리고 직접 STOMP 프레임을 구현하기로 마음먹게 되었습니다.&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;STOMP 프레임 구현&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STOMP에 대한 개념은 Krossbow를 적용하면서 충분히 잡아놨던 터라, 프로젝트 요구사항에 맞게 프레임을 구현하는건 크게 어렵지 않았습니다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 각 요청에 맞는 프레임 구조를 가진 클래스를 구현했습니다. 제 프로젝트에선 클라이언트용 프레임은 CONNECT, DISCONNECT, SEND, SUBSCRIBE 이렇게 4가지만 필요해 다음과 같이 구성했습니다. 내부적으로 포함되어야 하는 헤더들은 서버팀에게 문의해 파악했습니다. (구현하다가 막히면 반드시 팀원을 찾아가세요!)&lt;/p&gt;
&lt;pre id=&quot;code_1761211466674&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private const val END = &quot;\u0000&quot;
private fun headerLine(k: String, v: String) = &quot;$k:$v\n&quot;
private fun header(cmd: String, headers: Map&amp;lt;String, String&amp;gt;) =
    buildString {
        append(cmd).append('\n')
        headers.forEach { (k, v) -&amp;gt; append(headerLine(k, v)) }
        append('\n')
    }
    
class ConnectBuilder {
    var host: String = &quot;/&quot;
    var acceptVersion: List&amp;lt;String&amp;gt; = listOf(&quot;1.1&quot;, &quot;1.0&quot;)
    var heartBeat: Pair&amp;lt;Int, Int&amp;gt;? = null
    val headers = mutableMapOf&amp;lt;String, String&amp;gt;()

    fun build(): String {
        require(host.isNotBlank())
        val h = linkedMapOf(
            &quot;host&quot; to host,
            &quot;accept-version&quot; to acceptVersion.joinToString(&quot;,&quot;),
        ).apply {
            heartBeat?.let { put(&quot;heart-beat&quot;, &quot;${it.first},${it.second}&quot;) }
            putAll(headers)
        }

        return header(&quot;CONNECT&quot;, h) + END
    }
}

class SubscribeBuilder {
    var destination: String = &quot;&quot;
    var id: String = &quot;&quot;
    var receipt: Boolean = false
    val headers = mutableMapOf&amp;lt;String, String&amp;gt;()

    fun build(): String {
        require(destination.isNotBlank()) { &quot;destination은 필수로 입력해야 합니다.&quot; }
        val subId = id.ifBlank { &quot;sub-${System.nanoTime()}&quot; }
        val h = linkedMapOf(
            &quot;id&quot; to subId,
            &quot;destination&quot; to destination
        ).apply {
            if (receipt) put(&quot;receipt&quot;, &quot;rcpt-$subId&quot;)
            putAll(headers)
        }
        return header(&quot;SUBSCRIBE&quot;, h) + END
    }
}

class SendBuilder {
    var destination: String = &quot;&quot;
    var contentType: String? = null
    var body: String? = null
    val headers = mutableMapOf&amp;lt;String, String&amp;gt;()

    fun build(): String {
        require(destination.isNotBlank())
        val bytes = body?.toByteArray(Charsets.UTF_8)
        val h = linkedMapOf(&quot;destination&quot; to destination).apply {
            contentType?.let { put(&quot;content-type&quot;, it) }
            bytes?.let { put(&quot;content-length&quot;, it.size.toString()) }
            putAll(headers)
        }
        return header(&quot;SEND&quot;, h) + (body ?: &quot;&quot;) + END
    }
}

class DisconnectBuilder {
    val headers = mutableMapOf&amp;lt;String, String&amp;gt;()
    fun build(): String = header(&quot;DISCONNECT&quot;, headers) + END
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;b&gt;@DslMarker&lt;/b&gt;는 해당 어노테이션이 붙은 리시버들이 중복으로 사용되지 못하게 방지하기 위해 사용했습니다. 만약 중복된다면 컴파일 에러가 발생하게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빌더 패턴&lt;/b&gt;을 활용해 StompDsl의 함수를 호출함과 동시에 프레임 클래스가 생성되고 함수 내부에서 클래스들의 속성을 설정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1761211765651&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@DslMarker
annotation class StompMarker

@StompMarker
interface StompDsl {
    fun connect(block: ConnectBuilder.() -&amp;gt; Unit)
    fun subscribe(block: SubscribeBuilder.() -&amp;gt; Unit)
    fun send(block: SendBuilder.() -&amp;gt; Unit)
    fun disconnect(block: DisconnectBuilder.() -&amp;gt; Unit = {})
}

class StompDslImpl : StompDsl {
    private val frames = mutableListOf&amp;lt;String&amp;gt;()
    override fun connect(block: ConnectBuilder.() -&amp;gt; Unit) {
        frames += ConnectBuilder().apply(block).build()
    }

    override fun subscribe(block: SubscribeBuilder.() -&amp;gt; Unit) {
        frames += SubscribeBuilder().apply(block).build()
    }

    override fun send(block: SendBuilder.() -&amp;gt; Unit) {
        frames += SendBuilder().apply(block).build()
    }

    override fun disconnect(block: DisconnectBuilder.() -&amp;gt; Unit) {
        frames += DisconnectBuilder().apply(block).build()
    }

    fun build(): String = frames.joinToString(&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;프레임 변환 로직과 수신한 메시지를 전처리하는 로직을 캡슐화한 클래스&lt;/b&gt;와 그 클래스의 &lt;b&gt;함수들을 호출하는 두 개의 함수&lt;/b&gt;를 구현했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1761212153464&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class StompFrameBuilder {
    fun build(block: StompDsl.() -&amp;gt; Unit): String =
        StompDslImpl().apply(block).build()

    fun decodeMessage(frame: String): String {
        val parts = frame.split(&quot;\n\n&quot;, limit = 2)
        val bodyWithNull = parts.getOrNull(1) ?: return &quot;&quot;
        return bodyWithNull.trimEnd('\u0000', '\r', '\n')
    }
}

fun stompFrame(block: StompDsl.() -&amp;gt; Unit): String =
    StompFrameBuilder().build(block)

fun decodeMessage(frame: String): String =
    StompFrameBuilder().decodeMessage(frame)&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;/p&gt;
&lt;pre id=&quot;code_1761212712029&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val frame = stompFrame {
   connect {
      host = &quot;/&quot;
   }
}

val stompFrame = stompFrame { 
   disconnect() 
}

val frame = stompFrame {
   subscribe {
      destination = &quot;/topic/chat1&quot;
   }
}

val sendFrame: String = stompFrame {
   send {
      destination = &quot;/pub/send&quot;
      contentType = &quot;application/json&quot;
      body = &quot;hello world&quot;
   }
}&lt;/code&gt;&lt;/pre&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;OkHttp WebSocket&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Krossbow에서 지원하던 모든 편의성을 버리고 저는 OkHttp의 WebSocket 객체를 사용했습니다. 물론 재연결, 재구독 등 직접 신경써야할 것이 늘어났지만, 오히려 직접 처리해야 되기 때문에 처음 채팅을 구현하는 입장에선 불명확한 오류를 방지할 수 있어서 더 편했습니다.&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; 관리 덕분에 더 편하게 채팅 환경을 구축할 수 있었던 것 같아요. 예를 들면 onOpen이 호출되면 하트비트를 시작하고, 연결이 오류로 인해 종료되면 재연결을 시도하는 등 더 유연하게 상황에 대처할 수 있었습니다.&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;또한 Krossbow는 구독 채널마다 Flow를 생성해서 저는 이걸 하나의 Flow로 다시 결합해서 발행했었는데, OkHttp만 활용하게 되면서 onMessage를 통해 들어오는 메시지를 단일 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;프로젝트 레포지토리&lt;/b&gt; 참고해주세요!&lt;/p&gt;
&lt;figure id=&quot;og_1761213464391&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Napzak-Android/data/remote/src/main/java/com/napzak/market/remote/socket/StompWebSocketClient.kt at develop &amp;middot; napzakmarket/Napz&quot; data-og-description=&quot;납작 업~ 납작 업~. Contribute to napzakmarket/Napzak-Android development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/napzakmarket/Napzak-Android/blob/develop/data/remote/src/main/java/com/napzak/market/remote/socket/StompWebSocketClient.kt&quot; data-og-url=&quot;https://github.com/napzakmarket/Napzak-Android/blob/develop/data/remote/src/main/java/com/napzak/market/remote/socket/StompWebSocketClient.kt&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Egjbn/hyZMd5durP/rbhiRVzLzp2WTvbgsypq0K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/mE8kf/hyZLbAODfj/cPxqpoVkCp7w10aXZ907a1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/napzakmarket/Napzak-Android/blob/develop/data/remote/src/main/java/com/napzak/market/remote/socket/StompWebSocketClient.kt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/napzakmarket/Napzak-Android/blob/develop/data/remote/src/main/java/com/napzak/market/remote/socket/StompWebSocketClient.kt&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Egjbn/hyZMd5durP/rbhiRVzLzp2WTvbgsypq0K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/mE8kf/hyZLbAODfj/cPxqpoVkCp7w10aXZ907a1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Napzak-Android/data/remote/src/main/java/com/napzak/market/remote/socket/StompWebSocketClient.kt at develop &amp;middot; napzakmarket/Napz&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;납작 업~ 납작 업~. Contribute to napzakmarket/Napzak-Android development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 Krossbow와 커스텀 Stomp 프레임에 대한 포스트를 마치겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Krossbow는 꼭 다른 클라이언트 라이브러리를 사용하지 않더라도 자체적으로 웹소켓을 지원한다고 해요! 이 경우 자동 재연결 및 채널 재구독을 지원해 개발자가 신경 쓸 부분이 조금은 줄어들지 않을까 싶네요:)&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;혹시라도 직접 STOMP 프레임을 구현하시려는 분들에게 조금이나마 도움이 되길 바랍니다! 가능하다면 라이브러리 쓰세요 &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1761213792292&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;https://stomp.github.io/stomp-specification-1.2.html&quot; data-og-description=&quot;STOMP Protocol Specification, Version 1.2 Abstract STOMP is a simple interoperable protocol designed for asynchronous message passing between clients via mediating servers. It defines a text based wire-format for messages passed between these clients and s&quot; data-og-host=&quot;stomp.github.io&quot; data-og-source-url=&quot;https://stomp.github.io/stomp-specification-1.2.html&quot; data-og-url=&quot;https://stomp.github.io/stomp-specification-1.2.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://stomp.github.io/stomp-specification-1.2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stomp.github.io/stomp-specification-1.2.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;https://stomp.github.io/stomp-specification-1.2.html&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;STOMP Protocol Specification, Version 1.2 Abstract STOMP is a simple interoperable protocol designed for asynchronous message passing between clients via mediating servers. It defines a text based wire-format for messages passed between these clients and s&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stomp.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1761213818553&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - joffrey-bion/krossbow: A Kotlin multiplatform coroutine-based STOMP client over websockets, with built-in conversions.&quot; data-og-description=&quot;A Kotlin multiplatform coroutine-based STOMP client over websockets, with built-in conversions. - joffrey-bion/krossbow&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/joffrey-bion/krossbow&quot; data-og-url=&quot;https://github.com/joffrey-bion/krossbow&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/xBBEI/hyZL60WekS/QE3HyLkEhpUj9sK5zuqk90/img.png?width=1200&amp;amp;height=600&amp;amp;face=986_118_1057_196,https://scrap.kakaocdn.net/dn/DRAGG/hyZMaPNcGE/wYOc7qNuOKPLQOci1tbqKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=986_118_1057_196&quot;&gt;&lt;a href=&quot;https://github.com/joffrey-bion/krossbow&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/joffrey-bion/krossbow&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/xBBEI/hyZL60WekS/QE3HyLkEhpUj9sK5zuqk90/img.png?width=1200&amp;amp;height=600&amp;amp;face=986_118_1057_196,https://scrap.kakaocdn.net/dn/DRAGG/hyZMaPNcGE/wYOc7qNuOKPLQOci1tbqKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=986_118_1057_196');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - joffrey-bion/krossbow: A Kotlin multiplatform coroutine-based STOMP client over websockets, with built-in conversions.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A Kotlin multiplatform coroutine-based STOMP client over websockets, with built-in conversions. - joffrey-bion/krossbow&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Implementation</category>
      <category>Android</category>
      <category>Krossbow</category>
      <category>stomp</category>
      <category>websocket</category>
      <category>안드로이드</category>
      <category>웹소켓</category>
      <category>채팅</category>
      <author>boiledeggishere</author>
      <guid isPermaLink="true">https://boiledeggishere.tistory.com/22</guid>
      <comments>https://boiledeggishere.tistory.com/entry/Android-WebSocket%EC%9C%BC%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-2-STOMP-%EA%B7%B8%EB%A6%AC%EA%B3%A0-KrossBow#entry22comment</comments>
      <pubDate>Fri, 24 Oct 2025 16:44:31 +0900</pubDate>
    </item>
    <item>
      <title>[Android] WebSocket으로 실시간 채팅 구현하기 1 - WebSocket이란?</title>
      <link>https://boiledeggishere.tistory.com/entry/Android-WebSocket%EC%9C%BC%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-WebSocket%EC%9D%B4%EB%9E%80</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span&gt;최근에 &quot;납작마켓&quot;이라는 프로젝트에서 웹소켓을 활용한 채팅 기능을 구현했습니다. 이전엔 Retrofit을 활용한 REST 통신만 다뤄봤기에 안해 많이 해멜 것이라고 예상했는데, 예상대로 시행착오도 많았고 완성까지 꽤 오랜 시간이 걸렸습니다.&lt;br /&gt;&lt;br /&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span&gt;많은 고민과 시간을 투자한 기능이기에, 추후에 다시 웹소켓 기반 기능을 구현하게 되거나 웹소켓의 기능을 확장하는 상황에 대비해, 이 시점에서 생생한 구현 경험을 기록으로 남겨보고자 합니다. 한 번에 다 풀어내기엔 양이 많아서, 여러 편에 걸쳐 차근차근 정리할 예정입니다.&lt;br /&gt;&lt;/span&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: 'Nanum Gothic';&quot;&gt;이번 글 1편에서는 본격적인 구현 설명에 앞서, 웹소켓이 어떤 방식으로 동작하며 서버와 클라이언트를 연결해주는지, 그 개념을 먼저 간단히 정리해보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;&lt;span&gt;HTTP와 실시간 통신&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;일반적인 http 프로토콜은 클라이언트가 요청을 보내면 서버가 요청에 대한 응답을 보내주는 방식으로 정보를 교환하도록 설계되어 있습니다. 클라이언트가 요청을 보내지 않는다면 서버는 클라이언트에게 어떤 정보도 송신할 수 없는 것이죠. 이처럼 양쪽에서 데이터를 동시에 보내는 것이 아닌, 한쪽에서 요청을 보내면 다른 쪽에선 요청에 대한 응답을 보내는 통신 방식을 반이중 통신이라고 한답니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;http 프로토콜을 활용해서 실시간 통신을 구현하려면 3가지 방식 중 하나를 사용한다고 합니다:&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Polling&lt;br /&gt;&lt;/b&gt;클라이언트가 서버에게 지속적으로 요청을 보내 응답을 받는 방식입니다. 하지만 지속적인 연결 요청은 서버에 부담을 주게 됩니다. Http 연결을 핸드세이킹이라는 과정을 거치는데, 매 요청마다 핸드세이킹 과정이 수행되는 것도 굉장히 큰 부담이 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Long-Polling&lt;br /&gt;&lt;/b&gt;클라이언트가 요청을 보낸 뒤 서버가 응답할 때까지 대기하다가 응답을 받으면 연결을 끊는 방식입니다. 응답 대기 시간이 길면 연결을 유지하는 클라이언트의 수가 많아져 서버 리소스가 고갈될 위험이 있고, 응답 대기 시간이 짧으면 polling과 동일해집니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Streaming&lt;br /&gt;&lt;/b&gt;서버가 클라이언트의 요청에 대한 응답을 보내도 연결을 해제하지 않고 필요한 메시지를 반복해서 보낼 수 있는 구조입니다. 하지만 클라이언트가 요청을 보낼 때마다 새로운 HTTP 연결을 생성합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&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: 'Nanum Gothic';&quot;&gt;앞서 설명한 모든 방식은 채팅을 위한 진정한 양방향 통신을 지원하지 않죠. 이를 보완하기 위해 등장한게 바로 WebSocket입니다!&lt;/span&gt;&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;WebSocket이란?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;웹소켓은 양방향 통신을 지원하는 프로토콜입니다. 클라이언트가 서버에 접속하는 작업에는 HTTP 프로토콜을 이용하고, 그 이후 통신은 WebSocket 프로토콜을 사용합니다. 연결을 유지한 상태에서 WebSocket만의 경량화된 프레임을 활용해 클라이언트와 서버 간 양방향 데이터 송수신이 가능해집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;웹소켓이 어떻게 연결되는지도 알아야 제대로 적용이 가능하겠죠?&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;TCP 연결 수립&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;클라이언트와 서버 간 TCP 연결이 시도됩니다. TCP 연결을 일반적으로 세 방향 핸드세이크를 통해 이뤄집니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;WebSocket 핸드셰이크&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;클라이언트는 HTTP 프로토콜을 사용하여 WebSocket으로 프로토콜 업그레이드를 요청합니다. 서버에서는 패킷 내용을 읽고 업그레이드 승인 여부에 대한 응답을 보내게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;WebSocket 연결 완료&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;핸드셰이크가 성공적으로 완료되면, TCP 위에서 WebSocket 프로토콜을 사용하는 양방향(Full Duplex) 통신 채널이 열리고, 이후부터는 실시간 데이터 송수신이 가능합니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brxIKH/btsPGvQcy25/WsWb9s9INMNB7OAdF6rzgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brxIKH/btsPGvQcy25/WsWb9s9INMNB7OAdF6rzgk/img.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;190&quot; data-is-animation=&quot;false&quot; width=&quot;379&quot; height=&quot;144&quot; data-widthpercent=&quot;35.59&quot; data-filename=&quot;blob&quot; style=&quot;width: 35.1793%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brxIKH/btsPGvQcy25/WsWb9s9INMNB7OAdF6rzgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrxIKH%2FbtsPGvQcy25%2FWsWb9s9INMNB7OAdF6rzgk%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;500&quot; height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eulxmm/btsPHhqFpkb/uK19Bygz9ontUxaqWKkyi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eulxmm/btsPHhqFpkb/uK19Bygz9ontUxaqWKkyi1/img.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;84&quot; data-is-animation=&quot;false&quot; width=&quot;417&quot; height=&quot;88&quot; data-widthpercent=&quot;64.41&quot; data-filename=&quot;blob&quot; style=&quot;width: 63.6579%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eulxmm/btsPHhqFpkb/uK19Bygz9ontUxaqWKkyi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feulxmm%2FbtsPHhqFpkb%2FuK19Bygz9ontUxaqWKkyi1%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;400&quot; height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;좌측이 요청으로 Upgrade 헤더가 포함된 것을 볼 수 있음. 우측이 응답이며, 101 코드는 웹소켓 프로토콜 전환이 승인되었음을 알려줌.&lt;/figcaption&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;안드로이드에서 웹소켓&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;안드로이드 플랫폼에선 다양한 서드파티 라이브러리를 활용해 WebSocket 기반 양방향 통신을 구현할 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;대표적인 라이브러리는 OkHttp, Ktor가 있으며, 저는 OkHttp를 활용해 채팅 기능을 구현했습니다. 앞으로의 글들에서도 OkHttpWebSocket를 기반으로 구현한 내용을 소개할 예정입니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;OkHttp을 채택한 이유는 Retrofit을 활용해 REST API를 호출하는 구조가 마련되어 있었기 때문에, 굳이 새로운 프레임워크를 도입해서 프로젝트 규모를 키울 필요가 없다고 생각해서 입니다. &lt;s&gt;(거창한 이유는 아니죠..ㅎㅎ)&lt;/s&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;OkHttp로 웹소켓 객체를 마련하는 방법은 꽤 간단합니다. 거두절미하고 바로 코드를 보도록 하죠.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1754325552234&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Request 객체를 활용해 웹소켓의 엔드포인트를 설정합니다.
val request = Request.Builder().url(&quot;url&quot;).build()

// OkHttpClient로 WebSocket 연결을 위한 Http 요청을 보냄과 동시에 소켓을 통해 들어오는 메시지들에 대한 리스너를 설정합니다.
 val client = OkHttpClient().newWebSocket(request, object: WebSocketListener() {
    // 소켓이 연결되었을 경우
    override fun onOpen(webSocket: WebSocket, response: Response) { ... }

    // 바이트 형식의 메시지를 수신한 경우
    override fun onMessage(webSocket: WebSocket, bytes: ByteString) { ... }

    // 텍스트 형식의 메시지를 수신한 경우
    override fun onMessage(webSocket: WebSocket, text: String) { ... }
    
    // 상대방(서버)가 더이상 보낼 메시지가 없음을 알려준 경우
    override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { ... }
    
    // 서로가 더이상 보낼 메시지가 없음을 서로에게 알리고, 소켓 연결이 성공적으로 해제된 경우
    override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { ... }
    
    // 연결이 오류로 인해 비정상적으로 해제된 경우
    override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { ... }
    })&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;span style=&quot;font-family: 'Nanum Gothic';&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: 'Nanum Gothic';&quot;&gt;웹소켓을 활용해 채팅을 구현하는게 어려웠다고 느낀 이유는 웹소켓 연결 자체가 복잡해서가 아니였습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;오히려 웹소켓 연결을 비동기적으로 안정적으로 관리하고, 실시간 통신의 특성상 발생할 수 있는 예측 불가능한 오류에 대비하며,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;수신된 메시지를 Flow로 관리하고, 이를 프로젝트 아키텍처에 맞게 UI까지 자연스럽게 전달하는 과정에서 고민할 지점이 많았기 때문에 어렵게 다가왔습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그래도 위 방식대로 구현했다면 참고할 레퍼런스도 꽤 있고, 리스너가 상황 별 메서드를 제공하기 때문에 더 수월하게 구현했을 것 같네요.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;저는 앞서 언급한 여러 고민들 외에도 한 가지 기능 때문에 굉장히 고생한 바가 있는데, 이건 다음 글에서 설명하도록 하겠습니다!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Android/Implementation</category>
      <category>Android</category>
      <category>websocket</category>
      <category>납작마켓</category>
      <category>안드로이드</category>
      <category>웹소켓</category>
      <category>채팅</category>
      <author>boiledeggishere</author>
      <guid isPermaLink="true">https://boiledeggishere.tistory.com/21</guid>
      <comments>https://boiledeggishere.tistory.com/entry/Android-WebSocket%EC%9C%BC%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-WebSocket%EC%9D%B4%EB%9E%80#entry21comment</comments>
      <pubDate>Tue, 5 Aug 2025 02:23:15 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의 사실과 오해] 7. 함께 모으기</title>
      <link>https://boiledeggishere.tistory.com/entry/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%9D%98-%EC%82%AC%EC%8B%A4%EA%B3%BC-%EC%98%A4%ED%95%B4-7-%ED%95%A8%EA%BB%98-%EB%AA%A8%EC%9C%BC</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드와 모델을 밀접하게 연관시키는 것은 코드에 의미를 부여하고 모델을 적절하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 에릭 에반스&lt;/p&gt;
&lt;/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;&lt;b&gt;개념 관점 (Conceptual Perspective)&lt;/b&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;/li&gt;
&lt;li&gt;&lt;b&gt;명세 관점 (Specification Perspective)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자의 관점으로, 객체가 협력을 위해 &amp;lsquo;무엇&amp;rsquo;을 할 수 있는가에 집중한다.&lt;/li&gt;
&lt;li&gt;인터페이스와 구현을 분리하고, 인터페이스를 설계하여 객체의 책임을 명확하게 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구현 관점 (Implementation Perspective)&lt;/b&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;lsquo;어떻게&amp;rsquo; 수행할 것인가에 초점을 맞춘다.&lt;/li&gt;
&lt;li&gt;인터페이스를 구현하는 데 필요한 속성과 메서드를 클래스에 추가한다.&lt;/li&gt;
&lt;/ul&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;협력에 참여하기 위해 객체가 수신해야 하는 메시지를 결정한다.&lt;/li&gt;
&lt;li&gt;메시지가 모여 객체의 인터페이스를 구성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;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;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;상황 &lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;☕&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;1.&amp;nbsp; 카페에 손님이 들어온다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&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;2. 손님은 메뉴판을 훑어보고 마시고 싶은 커피를 주문한다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;3. 손님의 주문을 받은 바리스타는 커피를 제조한다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발에 들어가기 앞서, 현실 세계의 구성 요소들을 객체들의 타입과 관계로 추상화하여 시스템에 대해 이해할 필요가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메뉴판&lt;/b&gt;은 다양한 메뉴 항목들을 포함한다. &lt;b&gt;메뉴 항목&lt;/b&gt;은 커피 이름과 가격을 포함한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;손님&lt;/b&gt;은 메뉴판을 보고 바리스타에게 원하는 커피를 주문한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;바리스타&lt;/b&gt;는 주문받은 메뉴에 따라 적절한 커피를 제조한다.&lt;/li&gt;
&lt;li&gt;최종적으로 &lt;b&gt;커피&lt;/b&gt;가 제조된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rArr; 객체지향적 관점에서 커피 전문점이라는 도메인은 &lt;b&gt;메뉴판&lt;/b&gt;, &lt;b&gt;메뉴 항목&lt;/b&gt;, &lt;b&gt;손님,&lt;/b&gt; &lt;b&gt;바리스타&lt;/b&gt;, &lt;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;table style=&quot;border-collapse: collapse; width: 63.9552%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.6816%;&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.5652%;&quot;&gt;&lt;b&gt;관계&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.6816%;&quot;&gt;손님은 메뉴판을 알아야 메뉴를 선택할 수 있다.&lt;/td&gt;
&lt;td style=&quot;width: 26.5652%;&quot;&gt;손님 &amp;harr; 메뉴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.6816%;&quot;&gt;손님은 바리스타에게 주문한다.&lt;/td&gt;
&lt;td style=&quot;width: 26.5652%;&quot;&gt;손님 &amp;harr; 바리스타&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.6816%;&quot;&gt;바리스타는 주문받은 커피를 제조한다.&lt;/td&gt;
&lt;td style=&quot;width: 26.5652%;&quot;&gt;바리스타 &amp;harr; 커피&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;동일하게 행동할 수 있는 객체들은 동일한 타입의 인스턴스&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;lsquo;손님 타입&amp;rsquo;의 인스턴스&lt;/li&gt;
&lt;li&gt;메뉴판 객체는 &amp;lsquo;메뉴판 타입&amp;rsquo;의 인스턴스&lt;/li&gt;
&lt;li&gt;아메리카노, 라떼 등의 객체는 &amp;lsquo;커피 타입&amp;rsquo;의 인스턴스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;타입들간의 관계&lt;/b&gt;도 다음과 같이 표현할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 메뉴판에는 여러 개의 메뉴 항목이 존재한다. 메뉴 항목이 메뉴판에 포함되어 있다고 볼 수 있다. 이 관계를 &lt;b&gt;포함(containment)&lt;/b&gt; 관계 또는 &lt;b&gt;합성(composition)&lt;/b&gt; 관계라고 한다.&lt;/li&gt;
&lt;li&gt;손님은 메뉴판을 알고 있어야 한다. 그러나 메뉴판은 손님의 일부가 아니다. 포함 관계가 아닌, 서로 알고 있는 관계를 &lt;b&gt;연관(association)&lt;/b&gt; 관계라고 한다.&lt;/li&gt;
&lt;li&gt;손님과 바리스타, 바리스타와 커피 또한 연관 관계로 표현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 도메인과 관련된 객체들을 타입과 관계를 이용해 추상화하고 단순화한 모델이 바로 &lt;b&gt;도메인 모델&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✏️&amp;nbsp;설계하고 구현하기&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;커피 주문을 위한 협력 찾기&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리마인드&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;br /&gt;▪️ 훌륭한 협력 설계는 훌륭한 객체를 낳는다.&lt;br /&gt;▪️&amp;nbsp;협력을 설계할 때는 메시지를 먼저 선택하고 메시지를 수신하기에 적절한 객체를 선택해야 한다.&lt;br /&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;전체 시스템의 첫 메시지는 &amp;lsquo;커피를 주문하라&amp;rsquo;다. 이 메시지는 주문할 메뉴 이름을 포함해야 할 것이다.&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;예를 들어 커피를 주문하는 것은 손님의 역할이다. 즉, &quot;커피를 주문하라&quot;라는 메시지를 처리할 객체는 손님 타입의 인스턴스다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;객체가 메시지를 처리하는 도중에 스스로 할 수 없는 일이 있는지 파악한다. 만약 있다면 다른 객체에게 이를 요청해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;손님은 메뉴 항목을 알지 못한다. 손님은 선택한 메뉴를 누군가가 제공해 줄 것을 요청해야 한다.&lt;/li&gt;
&lt;li&gt;&amp;lsquo;메뉴 항목을 찾아라&amp;rsquo;라는 새로운 메시지를 도출할 수 있다. 이 메시지는 메뉴 이름을 포함하여 전송되어야 하며 응답으로 메뉴 항목을 반환해야 할 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;3번에서 파생된 메시지를 수신할 객체를 선택해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메뉴 항목을 찾을 책임은 메뉴 항목을 알고 있는 객체에게 할당하는 것이 가장 적절하다.&lt;/li&gt;
&lt;li&gt;메뉴판 객체가 바로 적임자다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 단계를 거듭하다 보면 다음과 같은 메시지 구조를 띄게 될 것이다.&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;&lt;b&gt;메시지&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;인자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;반환&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;송신자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;수신자&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커피를 주문하라&lt;/td&gt;
&lt;td&gt;커피 이름&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;손님&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메뉴 항목을 찾아라&lt;/td&gt;
&lt;td&gt;메뉴 이름&lt;/td&gt;
&lt;td&gt;메뉴 항목&lt;/td&gt;
&lt;td&gt;손님&lt;/td&gt;
&lt;td&gt;메뉴판&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커피를 제조하라&lt;/td&gt;
&lt;td&gt;메뉴 항목&lt;/td&gt;
&lt;td&gt;커피&lt;/td&gt;
&lt;td&gt;손님&lt;/td&gt;
&lt;td&gt;바리스타&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커피를 생성하라&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;바리스타&lt;/td&gt;
&lt;td&gt;커피&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지, 인자, 반환값들을 파악하여 얻어낸 것은 객체들의 인터페이스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 객체의 수신 가능한 메시지는 객체의 인터페이스를 구성하고, 인터페이스 안에는 메시지에 해당하는 오퍼레이션이 들어가게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어의 구현은 동적인 객체가 아닌 정적인 타입을 이용해 구현된다. 따라서 객체들을 포괄하는 타입을 정의한 후 식별된 오퍼레이션을 타입의 인터페이스에 추가해야 한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class Customer {
   fun order(String menuName) {}
}

class MenuItem {
}

class Menu {
   fun MenuItem choose(String name) {}
}&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;구현하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스의 인터페이스를 식별했으면 오퍼레이션을 수행하는 방법을 메서드로 구현해야 한다. Customer의 협력인 order를 코드로 옮겨보도록 하자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Customer는 menuName을 수신한다.&lt;/li&gt;
&lt;li&gt;Menu에게 menuName에 해당하는 MenuItem을 찾아달라고 요청한다.&lt;/li&gt;
&lt;li&gt;Menu로부터 MenuItem을 받아 Barista에게 전달한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Customer에서 Menu와 Barista 객체에 접근하기 위해 해당 객체들에 대한 참조를 얻어야 한다. 이 예제에선 인자로 받아와 참조 문제를 해결한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class Customer {
   fun order(menuName: String, menu: Menu, barista: Barista) {
      val menuItem: MenuItem = menu.choose(menuName)
      val coffee: Coffee = barista.makeCoffee(menuItem)
				
      /*
       * 기타 로직
       */
   }
}&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;위와 동일한 방법으로 Menu, MenuItem, Barista, Coffee에 대한 메서드와 속성을 구현하면 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  NOTE&lt;br /&gt;&lt;br /&gt;구현 도중에 인터페이스가 변경되는 것은 흔한 일이다. 설계 작업은 스케치일 뿐이기 때문에 설계에 많은 시간을 쏟기 보단 빨리 코드를 구현하여 설계가 구현이 가능한지 판단해야 한다.&lt;br /&gt;&lt;br /&gt;그리고 객체의 인터페이스를 정하는 단계에선 속성의 구현을 고려하지 않는 것이 좋다고 한다. 책임을 결정하고 책임에 필요한 속성을 결정해야 인터페이스와 구현을 깔끔하게 분리할 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3️⃣&amp;nbsp;코드와 세 가지 관점&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;코드는 세 가지 관점을 모두 제공해야 한다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방식대로 시스템을 구현한 코드는 개념 관점, 명세 관점, 구현 관점을 두루 포함한다. 세 관점을 모두 포함하면서 각 관점에 대응되는 요소를 명확하고 깔끔하게 드러내는 것이 훌륭한 객체지향 개발자의 지향점이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;개념 관점&lt;/b&gt;
&lt;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;예를 들어, 커피를 제조하는 과정을 변경해야 한다면 커피를 제조하는 책임이 있는 Barista 클래스의 오퍼레이션을 수정하면 된다.&lt;/li&gt;
&lt;li&gt;도메인 모델을 은유한 구현 덕분에 코드를 변경해야 하는 부분을 빠르게 파악할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;명세 관점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스의 public 메서드는 공용 인터페이스를 드러내어 외부에서 접근할 수 있도록 한다.&lt;/li&gt;
&lt;li&gt;인터페이스는 수정하기 어렵다. 최대한 변화에 안정적인 인터페이스를 만들기 위해선 구현과 관련된 세부 사항이 인터페이스를 통해 드러나지 않아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구현 관점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스의 메서드와 속성는 공용 인터페이스의 일부가 아니다. 따라서 메서드의 구현과 속성의 변경을 외부에 영향을 미쳐서는 안된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 메시지를 수신할 객체를 도메인 개념 안에서 선택하는 것은 도메인에 대한 지식을 기반으로 코드의 구조와 의미를 쉽게 유추할 수 있게 해준다. 이는 시스템 유지보수성에 큰 영향을 미친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스와 구현을 분리하는 이유 중 하나는 변경이 발생했을 때 쉽게 대응할 수 있기 때문이라는 것을 명심하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;인터페이스와 구현을 분리하라&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명세 관점과 구현 관점을 혼동해선 안된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명세 관점은 클래스의 안정적인 측면(도메인 모델 등)을 드러내야 한다.&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;/ul&gt;</description>
      <category>Development/서적</category>
      <category>객체지향</category>
      <category>객체지향의사실과오해</category>
      <author>boiledeggishere</author>
      <guid isPermaLink="true">https://boiledeggishere.tistory.com/20</guid>
      <comments>https://boiledeggishere.tistory.com/entry/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%9D%98-%EC%82%AC%EC%8B%A4%EA%B3%BC-%EC%98%A4%ED%95%B4-7-%ED%95%A8%EA%BB%98-%EB%AA%A8%EC%9C%BC#entry20comment</comments>
      <pubDate>Sat, 28 Jun 2025 00:01:01 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의 사실과 오해] 6. 객체 지도</title>
      <link>https://boiledeggishere.tistory.com/entry/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%9D%98-%EC%82%AC%EC%8B%A4%EA%B3%BC-%EC%98%A4%ED%95%B4-6-%EA%B0%9D%EC%B2%B4-%EC%A7%80%EB%8F%84</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;유일하게 변하지 않는 것은 모든 것이 변한다는 사실뿐이다.&lt;br /&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;&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;&lt;i&gt;범용적&lt;/i&gt;&lt;/b&gt;이고,&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;재사용성&lt;/b&gt;&lt;/i&gt;이 높으며,&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;변경에 안정적&lt;/b&gt;&lt;/i&gt;인 모델을 만들 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 특징이 중요한 이유는 &lt;u&gt;&lt;b&gt;사람들의 요구사항이 계속 변하기 때문이다&lt;/b&gt;&lt;/u&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;기능 중심과 구조 중심의 설계를 길을 찾는 경우에 빗대어 설명해보자면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기능 중심: A에서 B로 가기 위한 경로를 방향을 비롯하여 랜드마크들을 사용하여 상세하게 설명한다.&lt;/li&gt;
&lt;li&gt;구조 중심: 지도 상에서 A에서 B로 연결된 경로를 따라 이동한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로를 설명하는 경우, A와 B 사이에 경로만 포함하기 때문에 재사용이 제한된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 반대로, 구조 중심으로 설계된 지도를 사용하게 되면 B에 도착한 후 다시 C로 이동하고 싶을 때 지도를 펼쳐서 B부터 C까지의 경로를 잇고 이동하면 된다. 재사용성이 높다는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt; &lt;/span&gt;&amp;nbsp;기능 설계 대 구조 설계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠르게 변화하는 사용자 요구사항에 맞춰 새로운 기능을 빠르고 안정적으로 추가할 수 있어야 좋은 소프트웨어라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해서는 깔끔하고 단순하며 유지보수하기 쉬운 설계가 갖춰져야 한다.&lt;/p&gt;
&lt;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;&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;&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;시스템 기능을 책임으로 변환한 후 분할된 책임을 적잘한 객체에게 분배하여, 기능이 변경되더라도 객체 간의 구조는 그대로 유지할 수 있도록 한다. 이를 통해&amp;nbsp;&lt;/span&gt;변경에 유연한 소프트웨어를 만들 수 있도록 유도한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt; &lt;/span&gt; 객체 지향의 안정적인 재료, 구조&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자나 이해관계자들이 도메인에 관해 생각하는 개념과 개념들 간의 관계로 표현한 것을 &lt;b&gt;구조&lt;/b&gt;라고 한다.&lt;/span&gt;&lt;/blockquote&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 모델은 소프트웨어 영역 내의 개념과 개념의 관계, 규칙이나 제약 등을 깊게 추상화한 것으로, &lt;b&gt;&lt;span style=&quot;background-color: #fbf3db;&quot; data-token-index=&quot;1&quot;&gt;소프트웨어 개발과 관련된 이해관계자들이 도메인에 대해 생각하는 관점&lt;/span&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;b&gt;도메인&lt;/b&gt;: 사용자가 프로그램을 사용하는 대상 분야&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모델&lt;/b&gt;: 지식을 선택적으로 단순화하고 의식적으로 구조화한 형태&lt;/li&gt;
&lt;li&gt;&lt;b&gt;도메인 모델: &lt;/b&gt;프로그램을 사용하는 대상 영역에 관한 지식을 선택적으로 단순화하고 의식적으로 구조화한 형태&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 모델을 이해관계자들이 도메인을 바라보는 멘탈 모델이라고 정의하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;멘탈 모델&lt;/b&gt;이란 &lt;b&gt;&lt;span style=&quot;background-color: #fbf3db;&quot; data-token-index=&quot;0&quot;&gt;사람들이 자기 자신, 다른 사람, 환경, 자신이 상호작용하는 사물들에 대해 갖는 모형&lt;/span&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; 사용자가 제품/시스템에 대해 가지고 있는 개념&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디자인 모델:&lt;/b&gt; 설계자가 제품/시스템에 대해 가지고 있는 개념&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시스템 이미지:&lt;/b&gt; 최종 제품&lt;/li&gt;
&lt;/ol&gt;
&lt;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;&lt;b&gt;표현적 차이&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향을 사용하면 도메인 모델의 세 가지 측면을 모두 유사한 형태로 유지하며 소프트웨어를 설계할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실생활 내 사물을 시스템 안의 객체로 재창조할 수 있다.&lt;/li&gt;
&lt;li&gt;동적인 객체가 가진 복잡성을 정적인 타입을 이용해 단순화할 수 있다.&lt;/li&gt;
&lt;li&gt;클래스라는 도구를 이용해 타입을 코드 안으로 옮길 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향의 이러한 특징을 &lt;b&gt;연결완전성&lt;/b&gt; 또는 &lt;b&gt;표현적 차이&lt;/b&gt;라고 한다.&lt;/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;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;&lt;b&gt;불안정한 기능을 담는 안정적인 도메인 모델&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;사용자 관점을 반영하는 것이 중요한 이유는 사용자는 도메인의 본질적인 측면을 가장 잘 이해하고 있기 때문이다.&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 모델에 포함된 개념과 규칙은 비교적 변경될 확률이 적기 때문에 사용자 모델을 기반으로 설계하고 구현하면 변경에 쉽게 대처할 수 있을 가능성이 커진다. 이것은 &lt;b&gt;&lt;span style=&quot;background-color: #fbf3db;&quot; data-token-index=&quot;1&quot;&gt;도메인 모델이 기능을 담을 수 있는 안정적인 구조를 제공할 수 있음을 의미한다. &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt; &lt;/span&gt; &lt;b&gt;객체 지향의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;불안정한 재료, 기능&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자의 목표를 만족시키기 위해 책임을 수행하는 시스템의 행위를 &lt;b&gt;기능&lt;/b&gt;이라고 한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&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;&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;lsquo;상호작용&amp;rsquo; 관점에서 시스템을 바라봐야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;유즈케이스란,&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자의 목표를 달성하기 위해 사용자와 시스템 간에 이뤄지는 상호작용의 흐름을 텍스트로 정리한 것이다.&lt;/li&gt;
&lt;li&gt;시스템의 이해관계자들 중에서 일차 액터라 불리는 행위자의 요청에 대한 시스템의 응답으로서, 다양한 조건하에 있는 시스템의 행위를 서술한다.&lt;/li&gt;
&lt;li&gt;일차 액터는 하나의 목표를 달성하기 위해 시스템과 상호작용을 시작하는 이해관계자를 의미한다. 시스템과 연동하는 외부 시스템 역시 일차 액터의 범주에 포함된다.&lt;/li&gt;
&lt;li&gt;특별한 요청과 관계되는 조건에 따라 서로 다른 일련의 행위 혹은 시나리오가 전개될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;유스케이스 예시)&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;유스케이스명&lt;/b&gt;:&amp;nbsp;중도 해지 이자액을 계산한다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;일차 액터:&lt;/b&gt;&amp;nbsp;예금주&lt;br /&gt;&lt;br /&gt;&lt;b&gt;주요 성공 시나리오:&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp;1. 예금주가 정기예금 계좌를 선택한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp;2. 시스템은 정기예금 계좌 정보를 보여준다.&lt;br /&gt;&amp;nbsp; &amp;nbsp;3. 예금주가 금일 기준으로 예금을 해지할 경우 지금받을 수 있는 계산을 요청한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp;4. 시스템은 중도 해지 시 지급받을 수 있는 이자를 계산한 후 결과를 사용자에게 제공한다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;확장:&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp;3a. 사용자는 해지 일자를 다른 일자로 입력할 수 있다.&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;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;다.&lt;/li&gt;
&lt;li&gt;유스케이스는 &lt;b&gt;여러 시나리오의 집합&lt;/b&gt;이다. 시나리오란 유스케이스를 통해 시스템을 사용하는 하나의 경로다.&lt;/li&gt;
&lt;li&gt;유스케이스는 단순히 기능의 목록을 나열한 것이 아니라, &lt;b&gt;연관된 기능을 묶고 시스템의 기능에 대해 의사소통할 수 있는 문맥&lt;/b&gt;을 얻을 수 있는 이야기를 제공한다.&lt;/li&gt;
&lt;li&gt;유스케이스는 &lt;b&gt;사용자 인터페이스와 관련된 세부 정보를 포함하지 말아야 한다&lt;/b&gt;. 이런 유스케이스 형식을 본질적인 유스케이스라고 한다.&lt;/li&gt;
&lt;li&gt;유스케이스는 &lt;b&gt;내부 설계와 관련된 정보를 포함하지 않는다&lt;/b&gt;.&lt;/li&gt;
&lt;/ol&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;유스케이스는 &lt;b&gt;객체지향과는 무관&lt;/b&gt;한, 단지 기능적 요구사항을 사용자의 목표라는 문맥을 중심으로 묶기 위한 &lt;b&gt;정리 기법&lt;/b&gt;임을 명심하자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt; &lt;/span&gt;&amp;nbsp;재료 합치기: 기능과 구조의 통합&lt;/b&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;/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;&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;이렇게 구현된 객체들의 협력을 통해 시스템의 기능이 수행된다.&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;background-color: #fbf3db;&quot; data-token-index=&quot;1&quot;&gt;책임-주도 설계 방법&lt;/span&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;객체지향의 가장 큰 장점은 도메인 모델링에서 정의한 개념들이 그대로 코드에 녹아들 수 있다는 것이다. 도메인에서 정의한 &amp;lsquo;책임&amp;rsquo;과 &amp;lsquo;객체&amp;rsquo;는 클래스와 메서드로 자연스럽게 전환될 수 있는 특징을&amp;nbsp; &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;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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이자 계산 기능 구현 예제&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 유스케이스에 명시된 두 문자과 관련된 기능을 소프트웨어 기능으로 구현해보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 예금주가 금일 기준으로 예금을 해지할 경우 지급받을 수 있는 이자 계산을 요청한다. &lt;br /&gt;2. 시스템은 중도 해지 시 지급받을 수 있는 이자를 계산한 후 결과를 사용자에게 제공한다. &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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 사진은 정기예금의 도메인 모델이다 이를 기반으로 책임을 분할하고 객체들에게 할당하여 협력하는 객체들의 공동체를 형성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG4RYm/btsOEhMP8Ye/bxqyOH2rP8gXw6gWrlPNE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG4RYm/btsOEhMP8Ye/bxqyOH2rP8gXw6gWrlPNE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG4RYm/btsOEhMP8Ye/bxqyOH2rP8gXw6gWrlPNE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG4RYm%2FbtsOEhMP8Ye%2FbxqyOH2rP8gXw6gWrlPNE1%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;479&quot; height=&quot;187&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;799&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유스케이스를 기반으로 도출해낸 각 객체의 책임이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정기예금: 해지 일자를 전달받아 이자를 계산한다. 해지 일자가 약정 기간에 포함되는지 확인하여, 포함될 경우 계좌에게 이자 계산을 요청한다.&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이자를 계산하는 방식이 변하는 예제&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;위 기능에 이자에 대해서도 이자를 지급하는 복리 이자 방식을 추가해보도록 하겠다.&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;해결방안을 먼저 설명하자면, STRATEGY 디자인 패턴을 활용하여 이자 규칙을 일반화하여 다양한 문맥에서 교체가 가능하게끔 구현하고, 실제 비즈니스 정책은 캡슐화한다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt; Strategy 패턴 &lt;/b&gt;&lt;br /&gt;Strategy 디자인 패턴은 행동 패턴에 속하는 디자인 패턴으로, 알고리즘을 객체로 캡슐화하여 동적으로 교체할 수 있게 해준다. 이 패턴은 하나의 작업을 여러 알고리즘으로 구현할 수 있을 때, 그 알고리즘을 동적으로 선택할 수 있게 만든다. &lt;br /&gt;&lt;br /&gt;컨텍스트(Context) 클래스가 있고, 이 클래스는 다양한 전략(Strategy) 객체를 사용할 수 있다. &lt;br /&gt;전략 객체는 알고리즘을 캡슐화하고, 컨텍스트는 이 알고리즘을 동적으로 변경할 수 있다. &lt;br /&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;InterestRate라는 추상 클래스 생성한다.&lt;/li&gt;
&lt;li&gt;단리 이자를 계산하는 SimpleInterest와 복리 이자를 계산하는 CompoundInterest를 생성하고 InterestRate를 상속받게 한다.&lt;/li&gt;
&lt;li&gt;이자 계산이 필요한 곳에서 수신하는 객체의 타입에 따라 이자를 계산하는 방식이 달리지게 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 구현하면 예금 기능을 처음 구현할 때의 클래스 구조가 그대로 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 도메인 모델을 기반으로 시스템의 기능을 대응시켰기 때문이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Development/서적</category>
      <author>boiledeggishere</author>
      <guid isPermaLink="true">https://boiledeggishere.tistory.com/19</guid>
      <comments>https://boiledeggishere.tistory.com/entry/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%9D%98-%EC%82%AC%EC%8B%A4%EA%B3%BC-%EC%98%A4%ED%95%B4-6-%EA%B0%9D%EC%B2%B4-%EC%A7%80%EB%8F%84#entry19comment</comments>
      <pubDate>Tue, 17 Jun 2025 17:09:01 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의 사실과 오해] 5. 책임과 메시지</title>
      <link>https://boiledeggishere.tistory.com/entry/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%9D%98-%EC%82%AC%EC%8B%A4%EA%B3%BC-%EC%98%A4%ED%95%B4-5-%EC%B1%85%EC%9E%84%EA%B3%BC-%EB%A9%94%EC%8B%9C%EC%A7%80</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;의도는 &amp;ldquo;메시징&amp;rdquo;이다. 휼륭하고 성장 가능한 시스템을 만들기 위한 핵심은 모듈 내부의 속성과 행동이 어떤가보다는 모듈이 어떻게 소통하는가에 달려있다.&lt;/b&gt;&lt;br /&gt;- 엘런 케이&lt;/blockquote&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;/b&gt;자율적인 책임&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;설계의 품질을 좌우하는 책임&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향 공동체를 구성하는 기본 단위는 &amp;lsquo;&lt;b&gt;자율적&lt;/b&gt;&amp;rsquo;인 객체다.&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;: 자기 스스로의 원칙에 따라 어떤 일을 하거나 자신을 통제해서 절제하는 성질이나 특성&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;b&gt;다른 객체로부터 요청을 수신&lt;/b&gt;해야 하고,&lt;b&gt;수신한&lt;/b&gt; &lt;b&gt;요청을 처리하기 위해 수행하는 행동을 책임&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적절한 책임을 객체에 할당하여 자율적인 객체를 만들고, 자율적인 객체들이 모여 유연하고 단순한 협력을 만들어낼 수 있다.&lt;/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;u&gt;&lt;i&gt;&lt;b&gt;자신의 의지에 따라 증언할 수 있는 자유&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/h4&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;4장에서 다뤘던 앨리스 이야기에서 왕이 모자장수에게 증언하라고 요구하는 장면을 복기해보자.&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;Case 1&lt;/b&gt;: 추상적인 요청&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재판 과정에서 왕은 모자장수에게 &amp;lsquo;&lt;b&gt;증언하라&lt;/b&gt;&amp;rsquo;라는 요청을 보내고 모자장수는 요청을 적절하게 처리한 후 응답한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 보낸 왕은 모자장수가 요청을 처리하는 방법보다는 요청을 처리하는 책임을 완수하는지에 더 관심이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 모자장수는 왕이 만족할 만한 수준의 증언을 제공한다면 증언 방식이나 증언에 필요한 자료를 스스로의 판단에 따라 자유롭게 선택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Case 2&lt;/b&gt;: 구체적인 요청&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 왕이 모자장수에게 &amp;lsquo;&lt;b&gt;목격한 장면을 떠올려라&lt;/b&gt;&amp;rsquo;, &amp;lsquo;&lt;b&gt;시간 순서대로 재구성하라&lt;/b&gt;&amp;rsquo;, &amp;lsquo;&lt;b&gt;간결하게 표현해라&lt;/b&gt;&amp;rsquo;라는 요청을 보낸다고 가정하자.&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;즉, 지나치게 &lt;b&gt;상세한 수준의 책임&lt;/b&gt;은 객체의 &lt;b&gt;자유를 크게 훼손&lt;/b&gt;하고 &lt;b&gt;외부에 의존하는 상황&lt;/b&gt;을 만들어버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;너무 추상적인 책임&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상적인 책임은 협력의 재사용성과 유연성을 높인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, &lt;b&gt;협력에 참여하는 의도가 명확하게 표현되는 수준에서 추상적&lt;/b&gt;이어야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;lsquo;&lt;b&gt;증언하라&lt;/b&gt;&amp;rsquo;는 재판이라는 협력에서 특정 사건에 대한 얘기를 하라는, 문맥에 맞는 요청이다.&lt;br /&gt;&amp;lsquo;&lt;b&gt;말하라&lt;/b&gt;&amp;rsquo;는 특정 사건에 대해 얘기하라는 건지, 그냥 잡담하자는 건지 요청이 명확하지 않다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;&amp;rsquo;어떻게&amp;rsquo;가 아니라 &amp;lsquo;무엇을&amp;rsquo;&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자율적인 책임은 객체가 &lt;b&gt;&amp;lsquo;어떻게(how)&amp;rsquo;&lt;/b&gt;해야 하는가가 아니라 &lt;b&gt;&amp;lsquo;무엇(what)&amp;rsquo;&lt;/b&gt;을 해야 하는가를 설명한다. &lt;br /&gt;&lt;b&gt;&amp;lsquo;어떻게&amp;rsquo;&lt;/b&gt;가 책임 수준에서 결정되면 객체의 자율성을 크게 제한된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;lsquo;증언하라&amp;rsquo;는 무엇을 해야 하는지 나타내지만 어떻게 해야 하는지에 대해서는 전혀 언급하지 않는다.&lt;/blockquote&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;u&gt;&lt;i&gt;&lt;b&gt;책임을 자극하는 메세지&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/h4&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;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt; &lt;span&gt; &lt;/span&gt;&lt;/b&gt;&lt;/b&gt;메시지와 메서드&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;메시지&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 전송은 객체가 다른 객체에 접근할 수 있는 유일한 방법이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;왕은 모자장수에서 &amp;lsquo;증언하라&amp;rsquo;라는 메시지를 전송하여 모자장수와 협력한다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메세지 이름&lt;/b&gt;: 메시지를 가리키는 단어나 문자열 (예: 증언하라)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시지 인자&lt;/b&gt;: 메시지를 통해 제공될 수 있는 추가적인 정보 (예: 특정한 장소와 시간을 알기 위해 포함하는 &amp;lsquo;언제&amp;rsquo;와 &amp;lsquo;어디서&amp;rsquo;라는 정보)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 전송은 수신자와 메시지의 조합이고, 결국 &lt;b&gt;메시지 전송은 수신자, 메시지 이름, 인자의 조합&lt;/b&gt;이 된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 클래스 예시
class Person {
   ...
   void testify(String where, String when) { ... }  
}

// 메시지 전송 예시 [**수신자.메시지이름(인자)**]
Person.verify(&quot;Seoul&quot;, &quot;Today&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;메시지를 수신한 객체는 자신이 해당 메시지를 처리할 수 있는지 확인한다. 메시지를 처리할 수 있다는 것은 곧 해당 메시지에 해당하는 행동을 수행해야 할 책임이 있다는 것을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;객체가 수신할 수 있는 메시지의 모양이 객체가 수행할 책임의 모양을 결정한다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;모자장수는 &amp;lsquo;증인석에 입장하라&amp;rsquo;와 &amp;lsquo;증언하라&amp;rsquo;라는 두 가지 메시지를 받고, 이는 곧 두 메시지에 대한 적절한 행동을 할 책임이 있다는 것을 의미한다.&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;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;: 객체가 제공하는 메시지가 속한 부분으로, 외부의 다른 객체가 볼 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;메서드&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/h4&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;모자장수가 &amp;lsquo;증언하라&amp;rsquo;라는 메시지를 수신했을 때 내부적으로 선택하는 메시지 처리 방법이 메서드다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향 프로그래밍 언어에서는 클래스에 포함된 함수나 프로시저를 통해 메서드가 구현된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체에서 어떤 메시지를 전송하면 메시지에 대응되는 특정 메서드가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지는 &amp;lsquo;&lt;b&gt;어떻게&lt;/b&gt;&amp;rsquo; 수행될 것인지는 명시하지 않고, &amp;lsquo;&lt;b&gt;무엇&lt;/b&gt;&amp;rsquo;이 실행되기를 바라는지만 명시한다. 어떤 메서드가 선택되고 실행될지는 전적으로 수신자의 결정에 좌우된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향 언어의 경우, 메시지를 수신한 객체가 실행시간에 메서드를 선택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 절차지향에는 컴파일 시간에 프로시저 호출에 대한 실행코드를 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;다형성&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/h4&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;다형성을 &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;모자장수, 요리사, 앨리스 모두 &amp;ldquo;증인&amp;rdquo;이라는 역할로 &amp;ldquo;증언하라&amp;rdquo;라는 메시지를 처리할 수 있지만, 본인만의 방법을 통해 자유롭게 증언할 수 있다.&lt;/li&gt;
&lt;li&gt;증언을 듣는 왕의 입장에서는 &amp;ldquo;증언을 들었다&amp;rdquo;라는 결과가 매번 동일하게 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;송신자의 관점에선 동일한 오퍼레이션을 메시지 수신자들이 서로 다른 방식으로 처리하더라도 이 객체들은 동일한 책임을 수행하는 것이다. 즉, &lt;b&gt;송진자의 관점에서 다형적인 수신자들을 구별할 필요가 없으며 자신의 요청을 수행할 책임을 지닌다는 점에서 모두 동일하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다형성은 송신자는 동일한 역할을 수행하는 다양한 타입의 객체와 협력할 수 있는 &lt;b&gt;대체가능성&lt;/b&gt;을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대체 가능성을 이용해 설계를 유연하고 재사용 가능하게 만들 수 있으며, 송신자가 수신자의 종류를 모르더라도 메시지를 전송할 수 있다. 즉, &lt;b&gt;다형성은 수신자의 종류를 캡슐화하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;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;rArr; 인터페이스를 만들어서 대체 가능성을 보장하고 메시지 위주의 협력이 가능토록 구현해야 한다는 것을 의미함&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;u&gt;&lt;i&gt;&lt;b&gt;유연하고 확장 가능하고 재사용성이 높은 협력의 의미&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;송신자가 수신자에 대해 매우 적은 정보만 알고 있어도 상호 협력이 가능하다는 사실은 설계의 품질에 큰 영향을 미친다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;협력이 유연해진다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;송신자는 수신자에 대한 어떤 가정도 하지 않기 때문에 수신자를 다른 타입의 객체로 대체하더라도 송신자를 알지 못한다.&lt;/li&gt;
&lt;li&gt;송신자는 대체 여부를 알지 못하기 때문에 파급효과 없이 유연한 협력 변경이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;협력이 수행되는 방식을 확장할 수 있다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유연한 수신자 대체가 가능하기 때문에 협력의 세부적인 수행 방식을 쉽게 수정할 수 있다.&lt;/li&gt;
&lt;li&gt;협력에서 기존 객체를 빼고 새로운 유형의 객체를 끼워 맞추면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;협력이 수행되는 방식을 재사용할 수 있다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다양한 객체들이 수신자의 자리를 대체할 수 있기 때문에 다양한 문맥에서 협력을 재사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;송신자와 수신자를 약하게 연결하는 메시지&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지는 송신자와 수신자 사이의 결합도를 낮춘다. 이는 설계의 유연성, 확장성, 재사용성을 높인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;송신자는 수신자의 타입과 상관없이 메시지를 처리할 수 있는지에 대한 여부만 알아도 충분하다.&lt;/p&gt;
&lt;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;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;b&gt; &lt;span&gt;&lt;span&gt; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;b&gt;메시지를 따라라&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;b&gt;객체지향의 핵심, 메시지&lt;/b&gt;&lt;/u&gt;&lt;/h4&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클래스는 동적인 객체들의 특성과 행위를 정적인 텍스트로 표현하기 위한 도구일 뿐&lt;/b&gt;이다. 객체들의 속성과 행위를 잘 식별하는 것이 더욱 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 클래스를 중심에 두고 설계를 진행하기 보단, &lt;b&gt;협력하는 객체들의 관점에서 시스템을 바라보고&lt;/b&gt; 클래스에 담길 객체들의 공통적인 행위와 속성을 포착해야 한다.&lt;/p&gt;
&lt;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;객체 자체에 초점을 맞출 경우 &lt;b&gt;협력이라는 문맥을 배제한 채 객체 내부의 데이터 구조를 먼저 생각한 후 데이터 조작에 필요한 오퍼레이션을 나중에 고려하게 된다&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;/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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;책임 주도 설계&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;책임-주도 설계는 책임을 완수하기 위해 협력하는 객체들을 이용해 시스템을 설계하는 방법이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책임-주도 설계의 기본 아이디어는 &lt;b&gt;객체들 간에 주고받는 메시지를 기반으로 적절한 역할, 책임, 협력을 발견하는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;u&gt;&lt;b&gt;What/Who 사이클&lt;/b&gt;&lt;/u&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 사이의 협력 관계를 설계하기 위해서는 먼저&lt;b&gt; &amp;lsquo;어떤 행위(what)&amp;rsquo;를&lt;/b&gt; 수행할 것인지를 결정한 후에 &lt;b&gt;&amp;lsquo;누가(Who)&amp;rsquo;&lt;/b&gt; 그 행위를 수행할 것인지를 결정해야 한다. 협력 속에서 객체의 책임을 결정하는 것은 &lt;b&gt;메시지&lt;/b&gt;다. 즉, &lt;b&gt;행위에 해당하는 것이 메시지&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 행위를 결정하는 것은 객체 자체의 속성이 아니다. 반대로 행위를 먼저 식별한 후에 행위를 수행할 객체를 찾는다. 즉, &lt;b&gt;협력이라는 문맥&lt;/b&gt; 안에서 필요한 메시지를 먼저 결정한 후에 메시지를 수신하게에 적합한 객체가 선택되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;u&gt;&lt;b&gt;묻지 말고 시켜라&lt;/b&gt;&lt;/u&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;What/Who 사이클에선 어떤 메시지가 필요한지를 먼저 고민하고 어떤 객체가 필요한지 고민한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 결정하는 시점에선 어떤 객체가 메시지를 수신할 것이지 정해지지 않기 때문에 당연히 메시지 송신자는 메시지를 수신할 객체의 내부 상태를 볼 수 없다. 따라서 메시지 중심의 설계는 &lt;b&gt;메시지 수신자의 캡슐화를 증진&lt;/b&gt;시킨다. 또한 &lt;b&gt;송신자와 수신자가 느슨하게 결합&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 메시지를 먼저 결정하는 방식을 따르게 되면 송신자는 수신자가 어떤 객체인지는 모르지만 자신이 전송한 메시지를 잘 처리할 것이라고 믿고 메시지를 전송할 수 밖에 없다. 이런 스타일의 협력 패턴을 &lt;b&gt;&amp;lsquo;묻지 말고 시켜라&amp;rsquo;&lt;/b&gt; 스타일 또는 &lt;b&gt;&amp;lsquo;데메테르 법칙&amp;rsquo;&lt;/b&gt;이라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스타일은 객체지향 애플리케이션이 자율적인 객체들의 공동체라는 사실을 강조한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체는 다른 객체의 결정에 간섭하지 않아야 한다.&lt;/li&gt;
&lt;li&gt;모든 객체는 자신의 상태를 기반으로 자율적으로 결정을 내려야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;묻지 말고 시켜라 스타일과 What/Who 사이클은 객체 인터페이스의 크기를 감소시키고 외부에서 해당 객체에게 의존해야 하는 부분을 감소시킨다. 결과적으로 송신자와 수신자 간의 결합도가 낮아지게 된다.&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;i&gt;&lt;u&gt;메시지를 믿어라&lt;/u&gt;&lt;/i&gt;&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;송신자 입장에선 객체가 메시지를 수신할 수 있다면 종류가 무엇이든 상관없다. 중요한 것은 메시지의 의미를 이해아고 송신자의 의도대로 요청을 처리할 수 있는지 여부다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유연하고 재사용 가능한 설계를 위해서 객체의 구체적인 타입과 무관하게 전송된 메시지를 이해할 수 있는 객체들을 서로 연결하고 상호 협력이 가능하도록 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 믿으면 자율적인 책임은 저절로 따라오게 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;  객체 인터페이스&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스란 어떤 두 사물이 마주치는 경계 지점에서 서로 상호작용할 수 있게 이어주는 방법이나 장치를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 인터페이스는 세 가지 특징을 지닌다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;인터페이스의 사용법을 익히기만 하면 내부 구조나 동작 방식을 몰라도 쉽게 대상을 조작하거나 의사를 전달할 수 있다.&lt;br /&gt;&lt;/b&gt;운전자는 자동차의 내부 구성과 동작 원리를 몰라도 자동차를 운전하는 데 아무런 문제가 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인터페이스 자체는 변경하지 않고 단순히 내부 구성이나 작동 방식만을 변경하는 것은 인터페이스 사용자에게 어떤 영향도 미치지 않는다.&lt;br /&gt;&lt;/b&gt;자동차의 내부 부품이 변경되어도 자동차를 운전하는 방법은 변하지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대상이 변경되더라도 동일한 인터페이스를 제공하면 아무런 문제 없이 상호작용할 수 있다.&lt;br /&gt;&lt;/b&gt;모든 자동차는 동일한 인터페이스를 제공한다. 따라서 하나의 자동차를 운전할 수 있다면 다른 자동차도 운전할 수 있다.&lt;/li&gt;
&lt;/ol&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;i&gt;&lt;u&gt;&lt;b&gt;메시지가 인터페이스를 결정한다.&lt;/b&gt;&lt;/u&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 인터페이스는 객체가 &lt;b&gt;수신할 수 있는 메시지의 목록으로 구성&lt;/b&gt;되며 객체가 &lt;b&gt;어떤 메시지를 수신할 수 있는지가 인터페이스의 모양을 결정&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;u&gt;&lt;b&gt;공용 인터페이스&lt;/b&gt;&lt;/u&gt;&lt;/i&gt;&lt;/h4&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;&lt;b&gt;객체가 협력에 참여하기 위해 수행하는 메시지가 객체의 공용 인터페이스의 모양을 암시한다&lt;/b&gt;.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;왕과 모자장수 간 협력은 증언하라 라는 메시지를 전송하는 것이다. 따라서 왕과 모자 장수 사이에는 &amp;lsquo;증언하라&amp;rsquo;라는 메시지를 전송하고 수신할 수 있는 인터페이스가 존재한다.&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;먼저 메시지를 결정하고 이 메시지를 수행할 객체를 나중에 결정하는 책임-주도 설계 방식의 What/Who 사이클에서 메시지가 수신자의 인터페이스를 결정하게 된다.&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;u&gt;&lt;b&gt;책임, 메시지, 그리고 인터페이스&lt;/b&gt;&lt;/u&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;협력와 자율성&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;협력에 참여하는 객체의 책임은 자율적이어야 한다.&lt;/li&gt;
&lt;li&gt;자율성이란 자신의 의지와 판단력을 기반으로 객체 스스로 책임을 수행하는 방법을 결정하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시지&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지 전송이란 객체가 다른 객체에게 요청을 전송하는 메커니즘이다.&lt;/li&gt;
&lt;li&gt;객체의 인터페이스는 수신 가능한 메시지의 목록이다.&lt;/li&gt;
&lt;li&gt;메시지가 인터페이스를 자극하여 객체가 책임을 수행하도록 한다..&lt;/li&gt;
&lt;li&gt;객체는 메시지를 수신하면 책임을 수행하기 위해 적절한 메서드를 수행한다.&lt;/li&gt;
&lt;li&gt;메시지와 메서드의 구분은 객체를 외부와 내부라는 두 영역으로 분리한다. 그리고 다형성을 통해 다양한 타입의 객체를 수용할 수 있는 유연성을 부과한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인터페이스&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체가 다른 객체와 협력하기 위한 접점이 인터페이스.&lt;/li&gt;
&lt;li&gt;객체는 다른 객체로부터 메시지를 받으면 책임을 수행.&lt;/li&gt;
&lt;li&gt;객체가 어떤 메시지를 수신할 수 있는가? &amp;rarr; 객체가 어떤 책임을 수행할 수 있는가? &amp;rarr; 어떤 인터페이스를 가지는가?&lt;/li&gt;
&lt;/ul&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;  인터페이스와 구현의 분리&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;u&gt;&lt;b&gt;객체 관점에서 생각하는 방법&lt;/b&gt;&lt;/u&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 맷 와이스펠드라는 분이 객체지향적인 사고 방식에 대한 이해를 돕기 위해 제안한 세 가지 원칙이다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;좀 더 추상적인 인터페이스&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지나치게 상세한 수준의 메시지는 자율성을 저해하기 때문에, 좀 더 추상적인 수준의 메시지를 수신할 수 있는 인터페이스를 마련하여 수신자의 자율성을 보장해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최소 인터페이스&lt;/b&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;/li&gt;
&lt;li&gt;&lt;b&gt;인터페이스와 구현 간에 차이가 있다는 점을 인식&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;u&gt;&lt;b&gt;구현&lt;/b&gt;&lt;/u&gt;&lt;/i&gt;&lt;/h4&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;객체는 행동을 가진다. 여기서 행동이란 수신한 메시지를 처리하는 방버을 의미한다. 이를 메서드라고 하며, 메서드 또한 내부에 감춰져야 함으로 구현 부분에 해당한다.&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;i&gt;&lt;u&gt;&lt;b&gt;인터페이스와 구현의 분리 원칙&lt;/b&gt;&lt;/u&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훌륭한 객체는 구현을 모른 채 인터페이스에 대한 정보만으로 쉽게 상호작용이 가능해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 객체를 설계할 때 공용 인터페이스와 구현을 명확하게 분리해서 고려해야 한다. 이를 인터페이스와 구현의 분리 원칙이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어는 향상 변경된다. 수많은 객체들의 상호작용으로 이뤄진 객체지향 시스템에서 하나의 객체를 수정했을 때 어떤 객체가 영향을 받는지 판단하는 것은 매우 어려운 일이다. 따라서 변경해도 외부에 영향을 미치는 부분과 안 미치는 부분을 구분짓는 작업이 바로 인터페이스와 구현 분리 원칙이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에 노출되어 변경이 이뤄졌을 때 외부에 영향을 미치는 부분이 공용 인터페이스, 내부에 감춰져 수정이 이뤄져도 외부에 영향을 끼치지 않는 부분을 구현으로 구분한다. 따라서 메시지 목록을 공용 인터페이스에 두고 구현을 내부에 감춰 객체의 자율성을 보장할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;u&gt;&lt;b&gt;캡슐화&lt;/b&gt;&lt;/u&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 자율성을 보존하기 위해 구현을 내부로 감추는 것을 캡슐화하고 한다. 캡슐화를 정보 은닉이라고 부르기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캡슐화는 두 가지 관점에서 사용된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;상태와 행위의 캡슐화&lt;/b&gt;
&lt;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;/li&gt;
&lt;li&gt;&lt;b&gt;사적인 비밀의 캡슐화&lt;/b&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;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향은 내부와 외부를 명확하게 구분하는 객체들로 구성된 협력 공동체다. 명확하게 구분된 설계 덕분에 과거 개발 방법보다 더욱 유연하고 재사용 가능하다고 소문난 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;  책임의 자율성이 협력의 품질을 결정한다.&lt;/b&gt;&lt;/h3&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;br /&gt;&lt;/b&gt;자율적인 책임은 세부적인 사항들을 무시하고 의도를 드러내는 하나의 문장으로 추상화함으로써 협력을 단순하게 만든다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자율적인 책임은 객체의 외부와 내부를 명확하게 분리한다. &lt;br /&gt;&lt;/b&gt;객체는 수신한 메시지를 처리할 방식을 자율적으로 선택할 수 있다. &lt;br /&gt;이는 송신자와 수신자 사이에서 이뤄지는 협력을 수신자를 바라보는 송신자의 관점과 수신자가 책임을 수행할 방법을 표현하는 내부 관점으로 분리한다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;수신자 객체는 협력에 참여하기 위해 외부에 노출하는 공용 인터페이스와 책임을 수행하는 방법인 구현을 명확하게 나눈다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;캡슐화를 통해 인터페이스와 구현을 분리한다.&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;책임이 자율적인 경우 책임을 수행하는 내부적인 방법을 변경하더라도 외부에 영향을 미치지 않는다. &lt;br /&gt;&lt;/b&gt;책임이 자율적일수록 변경에 의해 수정돼야 하는 범위가 좁하지고 명확해진다.&lt;b&gt;&lt;br /&gt;&lt;/b&gt;변경의 파급효과가 객체 내부로 캡슐화되기 때문에 외부와 내부 간의 결합도가 낮아진다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자율적인 책임은 협력의 대상을 다양하게 선택할 수 있는 유연성을 제공한다.&lt;br /&gt;&lt;/b&gt;책임이 자율적일수록 다양한 문맥에서 재활용될 수 있다. 설계가 유연해지고 유연성이 높아진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;객체가 수행하는 책임들이 자율적일수록 객체의 역할을 이해하기 쉬워진다.&lt;br /&gt;&lt;/b&gt;책임들이 자율적이면 자율적일수록 객체의 존재 이유를 명확하게 표현할 수 있다. 객체는 동일한 목적을 달성하는 강하게 연관된 책임으로 구성되기 때문이다.&lt;b&gt;&lt;br /&gt;&lt;/b&gt;책임이 자율적일수록 객체의 응집도를 높은 상태를 유지하기가 쉬워진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;자율적인 책임의 효능!&lt;/b&gt;&lt;br /&gt;- 적잘하게 추상화된다. &lt;br /&gt;- 응집도가 높아진다. &lt;br /&gt;- 결합도가 낮아진다. &lt;br /&gt;- 캡슐화가 증진된다. &lt;br /&gt;- 인터페이스와 구현 간 분리가 명확해진다. &lt;br /&gt;- 설계가 유연해진다. &lt;br /&gt;- 설계의 재사용성이 향상된다.&lt;/blockquote&gt;</description>
      <category>Development/서적</category>
      <category>객체지향의사실과오해</category>
      <author>boiledeggishere</author>
      <guid isPermaLink="true">https://boiledeggishere.tistory.com/17</guid>
      <comments>https://boiledeggishere.tistory.com/entry/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%9D%98-%EC%82%AC%EC%8B%A4%EA%B3%BC-%EC%98%A4%ED%95%B4-5-%EC%B1%85%EC%9E%84%EA%B3%BC-%EB%A9%94%EC%8B%9C%EC%A7%80#entry17comment</comments>
      <pubDate>Sun, 1 Jun 2025 18:28:17 +0900</pubDate>
    </item>
    <item>
      <title>[Jetpack Compose] 안정성 어노테이션에 대하여...</title>
      <link>https://boiledeggishere.tistory.com/entry/Jetpack-Compose-%EC%95%88%EC%A0%95%EC%84%B1-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;https://getstream.io/blog/jetpack-compose-stability/&lt;br /&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;b&gt;@Stable&lt;/b&gt;, &lt;b&gt;@Immutable&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;그러다 최근에 진행 중인 프로젝트에서 팀원이 안정성 어노테이션을 사용하는 것을 보았고, 이제는 제대로 짚고 넘어가야겠다 싶어 안정성과 어노테이션에 대해 알아보았다.&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;이 글에선 안정성 어노테이션만을 다루고자 했지만, 온전히 이해하기 위해선 Compose 안정성에 대한 이해가 바탕에 있어야 하기에 때문에 안정성에 관한 설명도 포함하게 되었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&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;Jetpack Compose Phases&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Jetpack Compose가 함수를 화면에 렌더링하는 과정을 살펴보자.&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;Jetpack Compose에서는 여러 Composable 함수들을 화면에 렌더링하기 위해 &lt;b&gt;Composition&lt;/b&gt;, &lt;b&gt;Layout&lt;/b&gt;, &lt;b&gt;Drawing&lt;/b&gt; 세 단계를 거치게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1358&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyVAfU/btsN5Odls2t/0ucokGXklHau0m8ehM9Ir1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyVAfU/btsN5Odls2t/0ucokGXklHau0m8ehM9Ir1/img.png&quot; data-alt=&quot;출처: https://getstream.io/blog/jetpack-compose-stability/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyVAfU/btsN5Odls2t/0ucokGXklHau0m8ehM9Ir1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdyVAfU%2FbtsN5Odls2t%2F0ucokGXklHau0m8ehM9Ir1%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;631&quot; height=&quot;97&quot; data-origin-width=&quot;1358&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://getstream.io/blog/jetpack-compose-stability/&lt;/figcaption&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;/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;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Composable 함수의 설명을 생성하고 여러 메모리 슬롯을 할당한다.&lt;/li&gt;
&lt;li&gt;생성된 슬롯들은 각 Composable 함수를 메모이즈(memoize)한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레이아웃 (Layout)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Composable 트리 내에서 각 Composable 노드의 위치를 설정한다.&lt;/li&gt;
&lt;li&gt;각 노드의 측정 및 적절한 배치를 포함하며, UI 내에서 모든 요소가 정확하게 배치되도록 보장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그리기 (Drawing)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Composable 노드들을 사용자 상호작용이 가능하도록 시각적 표현을 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 렌더링이 끝났다는 것은 Drawing 단계가 완료되었다는 것을 의미한다. &lt;br /&gt;만약 같은 뷰에 새로운 업데이트 사항을 적용해야 한다면 &lt;b&gt;Composition&lt;/b&gt; 단계부터 다시 실행해야 하는데, 이 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;업데이트 과정을 &lt;b&gt;&lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;1&quot;&gt;Recomposition&lt;/span&gt;&lt;/b&gt; &lt;b&gt;(재구성)&lt;/b&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jetpack Compose에서 &lt;span style=&quot;color: #000000;&quot;&gt;&lt;u&gt;성능을 최적화한다는 것은 불필요한 recomposition을 줄여 오버헤드를 최소화하고 UI 성능을 높이는 것&lt;/u&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Compose와 안정성&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Composable 함수의 &lt;b&gt;매개변수 안정성&lt;/b&gt;은 recomposition을 유발하는 중요한 요소라고 한다.&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;Compose 컴파일러는 함수의 매개변수를 &lt;b&gt;안정(Stable)&lt;/b&gt;과 &lt;b&gt;불안정(Unstable)&lt;/b&gt; 두 가지의 상태로 분류하는데, &lt;br /&gt;이 분류는 Compose 런타임에 의해 Composable 함수가 recomposition을 거쳐야 하는지를 결정하는데 사용한다.&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; &amp;nbsp; &lt;b&gt;Stable&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose 컴파일러는 다음 유형들을 안정적인 상태로 분류한다:&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;기본 유형 (boolean, byte, char, short, int, float 등)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문자열&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;람다 표현식으로 표현된 함수 유형 (람다에서 unstable한 값을 캡쳐하는 경우 잠재적 unstable로 분류됨)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모든 public property가 불변이고 stable한 속성을 가진 클래스&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@Stable, @Immutable과 같은 안정성 어노테이스이 표기된 클래스&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Stable&lt;/b&gt;한 클래스에 대한 예시를 들자면,&lt;/p&gt;
&lt;pre id=&quot;code_1747803793555&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class User(
   val id: Int,
   val name: 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;위 클래스는 안정적으로 간주되는 기본 유형 타입과 불변인 value로 정의된 public property만을 가지므로 Compose 컴파일러에 의해 안정적인 것으로 간주된다.&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; &amp;nbsp; &lt;b&gt;Unstable&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose 컴파일러는 다음 유형들을 불안정한 상태로 분류한다:&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;Kotlin의 collections에서 제공하는 List, Map 등을 포함한 모든 인터페이스&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Any 타입과 같은 추상 클래스&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하나 이상의 가변적이거나 본질적으로 불안정한 public property를 포함하는 클래스&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스나 추상클래스의 경우 컴파일 시 구현을 예측할 수 없기 때문에 불안정한 것으로 간주된다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 List를 매개변수로 받을 경우, 실제로 List가 전달될지 MutableList가 전달될지 예측이 불가능하다.&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;Unstable한 클래스에 대한 예시를 들자면,&lt;/p&gt;
&lt;pre id=&quot;code_1747803874505&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class User(
  val id: Int,
  var name: 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;위 클래스는 가변적인 variable 프로퍼티가 존재하기 때문에 불안정한 클래스로 분류된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;안정성 추론은 모든 property의 전체적인 안정성을 평가함으로써 결정되기 때문에, &lt;br /&gt;단 하나의 가변 property만으로도 클래스 전체를 불안정하게 만들 수 있다고 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;스마트 Recomposition&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스마트 Recomposition&lt;/b&gt;은 &lt;span style=&quot;color: #000000;&quot;&gt;Compose 컴파일러가 제공하는 안정성 정보를 활용&lt;/span&gt;하여 &lt;span style=&quot;color: #000000;&quot;&gt;불필요한 recomposition을 선택적으로 건너뛰어&lt;/span&gt; Compose의 성능을 향상시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스마트 Recomposition&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; 매개변수가 안정적이고 그 값이 변경되지 않은 경우 recomposition을 건너뛴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동등성 검사:&lt;/b&gt; 위 상황처럼 equals() 함수 등을 통한 동등성 비교는 매개변수가 안정적인 타입일 때만 수행한다. 새로운 입력값이 주어지면 항상 해당 타입의 equals() 메서드를 사용하여 이전 값과 비교한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스마트 Recomposition이 지원되고 있지만, 개발자들은 클래스들을 안정적으로 만들고 recomposition을 최대한 줄이는 방법을 철저히 이해하고 있어야 한다.&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;Composable 함수 추론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 설명에서 Compose 컴파일러가 제공하는 안정성 정보를 통해 스마트 Recomposition을 수행한다고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정성 정보는 Composable 함수들을 &lt;b&gt;Restartable, Skippable, Moveable, Replaceable&lt;/b&gt; 등 여러 그룹을 분류한 정보를 포함한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 분류 개념을 알고 있으면 recomposition을 더 잘 이해할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  재실행 가능한 (Restartable)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 Composable 함수들은 입력의 변화를 감지하면 새로운 입력을 반영하기 위해 재호출된다. 따라서 대부분의 함수는 기본적으로 &lt;b&gt;restartable&lt;/b&gt;로 간주된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 입력이나 상태가 변경될 때마다 Compose 런타임이 Composable 함수에 대해 recomposition을 발생시킬 수 있다는 것을 의미한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  생략 가능한 (Skippable)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스마트 Recomposition의 원칙에서 살펴봤듯이, Composable 함수는 적절한 조건 하에서 recomposition을 건너뛸 수 있다. &lt;b&gt;Skippable&lt;/b&gt;한 Composable 함수는 특정 상황에 따라 recomposition을 건너뛸 수 있는 함수를 의미하는 것이다.&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;  Compose Compiler Metrics&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&quot;Compose Compiler Plugin&quot;&lt;/b&gt;&lt;/span&gt; 이라는 것을 제공한다. &lt;br /&gt;이 플러그인은 Compose 코드의 개선점을 파악하고 코드의 동작을 이해할 수 있는 상세한 보고서와 메트릭을 제공한다.&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;메트릭을 생성하고 싶은 build.gradle파일에 컴파일러 옵션을 추가하기만 하면 보고서와 메트릭을 생성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1747804052975&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;subprojects {
  tasks.withType&amp;lt;org.jetbrains.kotlin.gradle.tasks.KotlinCompile&amp;gt;().all {
    kotlinOptions.freeCompilerArgs += listOf(
      &quot;-P&quot;,
      &quot;plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=&quot; +
        project.buildDir.absolutePath + &quot;/compose_metrics&quot;
    )
    kotlinOptions.freeCompilerArgs += listOf(
      &quot;-P&quot;,
      &quot;plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=&quot; +
        project.buildDir.absolutePath + &quot;/compose_metrics&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;안드로이드 스튜디오에서 프로젝트를 동기화하고 빌드한 후,&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;b&gt;&amp;nbsp;/build/compose_metrics&lt;/b&gt;&lt;/span&gt;&amp;nbsp;디렉터리에 생성된 세 개의 파일(&lt;b&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;module.json,&amp;nbsp;composable.txt,&amp;nbsp;classes.txt&lt;/span&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;1. Top Level Metrics (module.json)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈에 포함된, 추적 가능한 Composable 함수의 특성을 수치화된 데이터로 나타낸다.&lt;/p&gt;
&lt;pre id=&quot;code_1747804105125&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
 &quot;skippableComposables&quot;: 36,
 &quot;restartableComposables&quot;: 41,
 &quot;readonlyComposables&quot;: 6,
 &quot;totalComposables&quot;: 60,
 &quot;restartGroups&quot;: 41,
 &quot;totalGroups&quot;: 82,
 &quot;staticArguments&quot;: 25,
  &quot;certainArguments&quot;: 138,
  &quot;knownStableArguments&quot;: 377,
  &quot;knownUnstableArguments&quot;: 25,
  &quot;unknownStableArguments&quot;: 24,
  ..
}&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;2. Composable Signature (composables.txt)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈 내 모든 함수를 상세하게 분석하여 각 매개변수에 대한 구체적인 정보를 제공한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수를 restartable, skippable, read-only 등으로 분류한다.&lt;/li&gt;
&lt;li&gt;각 매개변수가 stable인지 unstable인지 분류한다.&lt;/li&gt;
&lt;li&gt;각 매개변수의 표현식이 정적인지 동적인지 표시한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정보들을 사용하여 Composable 함수가 Skippable 한지 여부를 분석하고 어떤 매개변수가 불안정한지 식별한다.&lt;/p&gt;
&lt;pre id=&quot;code_1747804157188&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;restartable skippable scheme(&quot;[androidx.compose.ui.UiComposable]&quot;) fun Avatar(
  stable modifier: Modifier? = @static Companion
  stable imageUrl: String? = @static null
  stable initials: String? = @static null
  stable shape: Shape? = @dynamic VideoTheme.&amp;lt;get-shapes&amp;gt;($composer, 0b0110).circle
  ...
)&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. &lt;b&gt;Classes (classes.txt)&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;매개변수들을 stable, unstable, runtime으로 분류한다. runtime은 안정성이 다른 종속성에 의존하는 것을 의미하며, 런타임 시 결정됨을 의미한다.(예: 타입 매개변수나 외부 모듈의 타입)&lt;/p&gt;
&lt;pre id=&quot;code_1747804183322&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stable class StreamShapes {
  stable val circle: Shape
  stable val square: Shape
  stable val button: Shape
  stable val input: Shape
  ...
)&lt;/code&gt;&lt;/pre&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;Strong Skipping Mode&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose 컴파일러 버전 1.5.4에 소개된 &lt;b&gt;Strong Skipping Mode&lt;/b&gt;는 Composable 함수가 unstable한 매개변수를 포함하더라도 skippable한 함수로 분류될 수 있도록 지원한다.&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;Strong Skipping Mode에서는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;모든 restartable한 Composable 함수를 skippable한 함수&lt;/b&gt;로 취급&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unrestartable한 함수는 영향을 받지 않고 리컴포지션을 건너뛸 수 없다.&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;recomposition을 수행할 땐 unstable한 파라미터들은 참조 동일성을 가지고 비교하지만, 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;함수를 Strong Skipping mode에서 제외하여 건너뛸 수 없고 재실행이 가능한 함수로 만들고자 한다면 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;@NonSkippableComposable&lt;/b&gt;&lt;/span&gt;&amp;nbsp;어노테이션을 사용하면 된다. 이 어노테이션은 함수가 안정성과 상관없이 항상 리컴포지션이 수행될 것을 보장한다.&lt;/p&gt;
&lt;pre id=&quot;code_1747804230788&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
@OptIn(InternalComposeApi::class)
@NonSkippableComposable
fun CompositionLocalProvider(value: ProvidedValue&amp;lt;*&amp;gt;, content: @Composable () -&amp;gt; Unit) {
    currentComposer.startProvider(value)
    content()
    currentComposer.endProvider()
}&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;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;s&gt;먼 길을 돌아왔다.&lt;/s&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;Compose 컴파일러가 안정적인 상태로 분류하는 유형 중에 다음과 같은 유형이 있었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;@Stable, @Immutable과 같은 안정성 어노테이스이 표기된 클래스&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;각각의 어노테이션의 의미를 알고 알맞게 사용하면 불필요한 recomposition을 줄이고 결과적으로 애플리케이션 성능을 높일 수 있다.&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;@Immutable&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Immutable&lt;/b&gt; 어노테이션은 Compose 컴파일러에게 클래스의 모든 public property가 초기 생성 후 변경되지 않는다는 강력한 보증을 제공한다. 즉, 컴파일러는 어노테이션을 읽고 해당 클래스를 불변으로 간주한다는 것이다.&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;@Immutable&lt;/b&gt; 어노테이션의 불변 보증은 val보다 더 엄격하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;val은 MutableList로 초기화된 List와 같은 가변 데이터 구조를 허용하기 때문이다.&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;@Immutable&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;모든 public property에 val 키워드를 사용해야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커스텀 setter를 생성하지 않고 public property의 가변성을 지원하지 않아야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모든 public property의 타입이 본질적으로 불변으로 간주돼야 한다. &lt;br /&gt;만약 Kotlin Collection을 사용한다면, 클래스가 생성된 후 해당 Collection이 절대 수정되지 않아야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Immutable&lt;/b&gt; 어노테이션을 통해 불필요한 recomposition을 건너뛰어 애플리케이션 성능을 향상시킬 수 있지만, 잘못 사용하는 경우 recomposition을 의도하지 않게 건너뛰게 되어 업데이트를 원해도 이뤄지지 않을 수 있다.&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;@Stable&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Stable&lt;/b&gt; 어노테이션은 @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;&amp;ldquo;&lt;b&gt;Stable&lt;/b&gt;&amp;rdquo;이라는 용어는 동일한 입력에 대해 항상 동일한 결과를 반환하여 예측 가능한 동작을 보장한다는 의미를 담고 있으며, &lt;b&gt;@Stable&lt;/b&gt; 어노테이션이 붙은 클래스나 함수는 타입 변경이 가능하다. 따라서 &lt;b&gt;@Stable&lt;/b&gt; 어노테이션은 public property는 불변하지만 클래스 자체는 안정적으로 간주될 수 없는 경우에 사용하면 된다.&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;Compose에서 상태를 관리할 때 주로 사용하는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;State와 MutableState에 @Stable 어노테이션이 붙어있다&lt;/b&gt;&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;State의 경우, value라는 불변 속성만 노출하지만 기본 값은 MutableState에 의해 생성되며 setter 함수를 통해 수정될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MutableState 내 value 속성이 가변적이여도 getter와 setter 함수를 통해 동일한 입력에 대해 동일한 결과를 얻어낼 수 있다. 따라서 State와 MutableState에는 @Stable 어노테이션이 붙을 수 있는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1747804341656&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Stable
interface State&amp;lt;out T&amp;gt; {
    val value: T
}

@Stable
interface MutableState&amp;lt;T&amp;gt; : State&amp;lt;T&amp;gt; {
    override var value: T
    operator fun component1(): T
    operator fun component2(): (T) -&amp;gt; Unit
}&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;@Immutable vs @Stable&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;&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;@Immutable&lt;/b&gt;은 클래스의 모든 public property가 불변이고 객체가 생성된 후 상태가 변경될 수 없음을 의미한다.&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;@Immutable&lt;/b&gt;은 비즈니스 모델에 주로 사용된다고 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1747804383724&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Immutable
data class User(
    public val id: String,
    public val nickname: String,
    public val profileImages: List&amp;lt;String&amp;gt;,
)&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;@Stable&lt;/b&gt;은 가변성이 있는 객체에 적용될 수 있으며, 동일한 입력에 대해 일관된 결과를 생성하는 객체여야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Stable&lt;/b&gt;은 내부적으로 변경 가능한 상태를 가질 수 있는 인터페이스에 주로 사용된다. UiState처럼 상태 값을 관리하는 클래스가 그 예시다.&lt;/p&gt;
&lt;pre id=&quot;code_1747804422941&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Stable
interface UiState&amp;lt;T : Result&amp;lt;T&amp;gt;&amp;gt; {
    val value: T?
    val exception: Throwable?
		
    val hasSuccess: Boolean
        get() = exception == null
}&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;@NonRestartableComposable&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@NonRestartableComposable&lt;/b&gt; 어노테이션은 Composable 함수의 매개변수가 변경되더라도 recomposition이 일어나지 않고 값만 업데이트되도록 지시하는 어노테이션이다. 이를 통해 내부 상태와 진행 중인 사이드 이펙트를 유지할 수 있다.&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;@NonRestartableComposable&lt;/b&gt;이 사용된 대표적인 예는 &lt;b&gt;LaunchedEffect&lt;/b&gt;다.&lt;/p&gt;
&lt;pre id=&quot;code_1747804448074&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -&amp;gt; Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1) { LaunchedEffectImpl(applyContext, block) }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;@NonRestartableComposable&lt;/b&gt;을 사용하여 &lt;b&gt;remember&lt;/b&gt; 객체가 재생성되는 것을 방지한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;key1&lt;/b&gt; 값이 갱신되면 &lt;b&gt;remember&lt;/b&gt; 내부에 있는 &lt;b&gt;LauncedEffectImpl&lt;/b&gt;만 재실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;서두에서도 언급했지만 더 자세한 내용을 알고 싶다면 아래 글을 참고하자.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1747805929297&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Jetpack Compose 성능 최적화를 위한 Stability 이해하기&quot; data-og-description=&quot;\_원문은 Optimize App Performance By Mastering Stability in Jetpack Compose(https://getstream.io/blog/jetpack-compose-stability/이 포스트의 내용을 심층적으로 다루는&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@skydoves/compose-stability&quot; data-og-url=&quot;https://velog.io/@skydoves/compose-stability&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/TWHed/hyYYx4uHxk/WIxfViU2Bw52OIALHYWPi0/img.png?width=1600&amp;amp;height=836&amp;amp;face=0_0_1600_836,https://scrap.kakaocdn.net/dn/Bx7Qp/hyYVerXFxn/KKC4ypk3tJ8J2fmk6Q7zJk/img.png?width=1600&amp;amp;height=836&amp;amp;face=0_0_1600_836,https://scrap.kakaocdn.net/dn/dv2BcK/hyYTmRv1t0/dgm1mT0KojMuKMix4Dm1Z1/img.png?width=1600&amp;amp;height=836&amp;amp;face=0_0_1600_836&quot;&gt;&lt;a href=&quot;https://velog.io/@skydoves/compose-stability&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@skydoves/compose-stability&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/TWHed/hyYYx4uHxk/WIxfViU2Bw52OIALHYWPi0/img.png?width=1600&amp;amp;height=836&amp;amp;face=0_0_1600_836,https://scrap.kakaocdn.net/dn/Bx7Qp/hyYVerXFxn/KKC4ypk3tJ8J2fmk6Q7zJk/img.png?width=1600&amp;amp;height=836&amp;amp;face=0_0_1600_836,https://scrap.kakaocdn.net/dn/dv2BcK/hyYTmRv1t0/dgm1mT0KojMuKMix4Dm1Z1/img.png?width=1600&amp;amp;height=836&amp;amp;face=0_0_1600_836');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Jetpack Compose 성능 최적화를 위한 Stability 이해하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;\_원문은 Optimize App Performance By Mastering Stability in Jetpack Compose(https://getstream.io/blog/jetpack-compose-stability/이 포스트의 내용을 심층적으로 다루는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1747805907379&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Optimize App Performance By Mastering Stability in Jetpack Compose&quot; data-og-description=&quot;Learn comprehensive knowledge of stability in Jetpack Compose and improve your Android app performance with them.&quot; data-og-host=&quot;getstream.io&quot; data-og-source-url=&quot;https://getstream.io/blog/jetpack-compose-stability/&quot; data-og-url=&quot;https://getstream.io/blog/jetpack-compose-stability/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pEWKr/hyYW065QR7/cnFZonoQKAecI0NrfZSCv1/img.jpg?width=2400&amp;amp;height=1254&amp;amp;face=0_0_2400_1254,https://scrap.kakaocdn.net/dn/ctDIY7/hyYU6U0dEz/no5z5CCjC9gwtmS211tKxK/img.jpg?width=2400&amp;amp;height=1254&amp;amp;face=0_0_2400_1254,https://scrap.kakaocdn.net/dn/zx9bY/hyYYu0ZgsQ/s00VqM8SnKYWxciZEPzWF0/img.jpg?width=2400&amp;amp;height=1254&amp;amp;face=0_0_2400_1254&quot;&gt;&lt;a href=&quot;https://getstream.io/blog/jetpack-compose-stability/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://getstream.io/blog/jetpack-compose-stability/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pEWKr/hyYW065QR7/cnFZonoQKAecI0NrfZSCv1/img.jpg?width=2400&amp;amp;height=1254&amp;amp;face=0_0_2400_1254,https://scrap.kakaocdn.net/dn/ctDIY7/hyYU6U0dEz/no5z5CCjC9gwtmS211tKxK/img.jpg?width=2400&amp;amp;height=1254&amp;amp;face=0_0_2400_1254,https://scrap.kakaocdn.net/dn/zx9bY/hyYYu0ZgsQ/s00VqM8SnKYWxciZEPzWF0/img.jpg?width=2400&amp;amp;height=1254&amp;amp;face=0_0_2400_1254');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Optimize App Performance By Mastering Stability in Jetpack Compose&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn comprehensive knowledge of stability in Jetpack Compose and improve your Android app performance with them.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;getstream.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Jetpack Compose</category>
      <category>Jetpack Compose</category>
      <category>안정성</category>
      <author>boiledeggishere</author>
      <guid isPermaLink="true">https://boiledeggishere.tistory.com/16</guid>
      <comments>https://boiledeggishere.tistory.com/entry/Jetpack-Compose-%EC%95%88%EC%A0%95%EC%84%B1-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC#entry16comment</comments>
      <pubDate>Wed, 21 May 2025 14:40:59 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의 사실과 오해] 4. 역할, 책임, 협력</title>
      <link>https://boiledeggishere.tistory.com/entry/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%9D%98-%EC%82%AC%EC%8B%A4%EA%B3%BC-%EC%98%A4%ED%95%B4-4-%EC%97%AD%ED%95%A0-%EC%B1%85%EC%9E%84-%ED%98%91%EB%A0%A5</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;우리 모두를 합친 것보다 더 현명한 사람은 없다.&lt;/b&gt; &lt;br /&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;이번 장은 1982년에 독일의 베르너 귀스 교수가 실시한 &amp;ldquo;최후통첩 게임&amp;rdquo; 실험에 대한 이야기로 시작한다.&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;lsquo;제안자&amp;rsquo;와 &amp;lsquo;응답자&amp;rsquo; 무리로 나눈다. 제안자에게는 일정한 금액이 주어지며, 제안자는 받은 금액의 일부를 응답자에게 나눠줘야 한다. 이 때 제안자가 응답자에게 제시하는 금액의 비율은 직접 정하면 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답자는 제안자의 제안을 수용하거나 거부할 수 있다. 만약 응답자가 제안을 승낙하면 제안자와 응답자 모두 제시한 비율에 따라 금액을 나누어 가질 수 있다. 하지만 응답자가 제안을 거부하게 되면 둘 다 한 푼도 건질 수 없게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 게임의 룰만 들었을 땐 제안자는 최소 금액을 응답자에게 제안하는 이기심을 보일 것이며, 응답자는 돈을 받을 수 있기 때문에 응할 것이라고 추측할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실험 결과, 금액의 40%를 제안하는 제안자 수가 많았다. 그리고 금액의 20%를 제안받은 응답자 대부분이 거절했다고 한다. 즉, 인간은 어떤 본질적인 특성을 지닌 것이 아니라 개인이 처해 있는 문맥에 따라 행동 방식을 결정한다는 것을 알 수 있는 실험이었던 것이다. 그리고 여기서 &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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span&gt; &lt;/span&gt;&amp;nbsp;협력&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;요청하고 응답하며 협력하는 사람들&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;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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;재판 속의 협력&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;이상한 나라의 앨리스&amp;rdquo;에선 파이를 훔친 하트 잭에 대한 공판이 이뤄지는 이야기가 있다. 이 상황에선 재판관인 왕, 왕의 신하인 토끼, 그리고 증인 신분의 모자 장수가 등장한다.&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;1204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YPB4b/btsNRKoS012/J3OhL7RvVTDCW62WZeK0k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YPB4b/btsNRKoS012/J3OhL7RvVTDCW62WZeK0k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YPB4b/btsNRKoS012/J3OhL7RvVTDCW62WZeK0k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYPB4b%2FbtsNRKoS012%2FJ3OhL7RvVTDCW62WZeK0k0%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;467&quot; height=&quot;297&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;1204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화살표의 방향은 요청자에서 응답자로 요청이 향하는 것을 시각화한 것이다. 예를 들어 왕이 토끼에서 목격자를 불러오라고 요청하는 구조인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 등장인물이 다른 인물로부터 &lt;b&gt;요청을 받을 수 있는 이유는 그 요청에 대해 적절한 방식으로 응답하는 데 필요한 지식과 행동 방식을 가지고 있기 때문&lt;/b&gt;이다. 요청과 응답은 협력에 참여하는 객체가 수행할 책임을 정의한다는 것을 알 수 있는 대목이다.&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span&gt;  &lt;/span&gt;책임&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;객체지향에서는 어떤 객체가 어떤 요청에 대해 응답할 수 있거나, 적절한 행동을 할 의무가 있는 경우 해당 객체가 책임을 가진다라고 한다.&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #333333; text-align: left;&quot;&gt;어떤 객체에 요청을 보낸다 라는 것은&amp;nbsp;해당 객체가 요청을 처리할 책임이 있음을 암시한다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;ldquo;크레이그 라만 왈: 객체지향 개발에서 가장 중요한 능력은 책임 능숙하게 소프트웨어 객체에 할당하는 것&amp;rdquo;&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;책임의 분류&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;책임은 객체에 의해 정의되는 응집도 있는 행위의 집합으로, 객체가 알아야 하는 정보와 객체가 수행할 수 있는 행위에 대해 개략적으로 서술한 문장이다.&lt;/span&gt;&lt;/b&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;&lt;u&gt;&lt;b&gt;하는 것 (doing)&lt;/b&gt;&lt;/u&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;/li&gt;
&lt;li&gt;&lt;u&gt;&lt;b&gt;아는 것 (knowing)&lt;/b&gt;&lt;/u&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;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재판 이야기에 이 범주를 적용해보면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;왕&lt;/b&gt;은 토끼에게 목격자를 불러오도록 요청하고 증인에게 증언을 하라고 요청한다. &lt;b&gt;다른 객체들의 활동을 제어&lt;/b&gt;하고 조율하고 있음으로 왕은 &lt;b&gt;하는 것&lt;/b&gt;과 관련된 책임을 수행하고 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;토끼&lt;/b&gt;는 모자 장수가 목격자임을 알고 있고, 모자 장수가 증인석에 입장할 것을 요청한다. &lt;b&gt;관련된 객체에 대해 알고 다른 객체의 활동을 제어함&lt;/b&gt;으로 &lt;b&gt;하는 것과 아는 것&lt;/b&gt;의 두 가지 종류의 책임을 모두 수행하고 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모자 장수&lt;/b&gt;는 스스로 증인석에 입장하고 증언해야 할 책임이 있다. **&amp;ldquo;객체를 생성하거나 계산을 하는 등의 스스로 하는 것&amp;rdquo;**과 &lt;b&gt;&amp;ldquo;자신을 유도하거나 계산할 수 있는 것에 관해 아는 것&lt;/b&gt;&amp;rdquo; 두 가지 책임을 수행한다. 즉, &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;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;/h4&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;b&gt;메시지 전송&lt;/b&gt;: 객체가 다른 객체에게 책임을 수행하도록 요청을 보내는 행위&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시지&lt;/b&gt;: 두 객체 간의 협력이 이뤄지도록 하는 매개체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;송신자&lt;/b&gt;: 요청을 보내는 객체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수신자&lt;/b&gt;: 요청을 받아서 처리하는 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 모자 장수의 책임은 증언을 하는 것이고, 왕으로부터 &amp;ldquo;증언하라&amp;rdquo;는 메시지를 수신했을 때 그 책임을 수행하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;책임과 메시지의 수준은 같지 않다. 책임을 결정한 후 실제로 협력을 정제하면서 메시지로 변환할 때 하나의 책임이 여러 메시지로 분할된다고 한다.&lt;/b&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;책임을 수행하기 위해서는 다른 객체와 협력해야 한다. 따라서 &amp;ldquo;메시지로 분할된다&amp;rdquo;는 것은 외부 객체와 협력이 늘어나는 것을 의미한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 요청을 받았을 때 다양한 객체들의 함수들을 호출하곤 하는데, 이것이 메시지를 보내는 행동이며 책임이 메시지로 분할되는 것으로 볼 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span&gt; &lt;/span&gt;&amp;nbsp;&lt;/b&gt;역할&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;책임의 집합이 의미하는 것&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재판 이야기에서 등장인물들은 역할을 가지고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왕 = 재판관&lt;/li&gt;
&lt;li&gt;토끼 = 신하&lt;/li&gt;
&lt;li&gt;모자 장수 = 증인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 역할을 부여하는 이유는 &lt;b&gt;역할이 재사용이 가능하고 유연한 객체지향 설계를 돕기 때문&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재판 이야기에서 등장인물들을 바꿨는데 재판 과정이 동일하다면 협력을 그대로 유지하는 것이 효율적일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등장인물에 따라서 바꾼다면 매번 객체를 교체해야 하는 번거로움이 있겠지만 역할을 사용하면 그 번거로움이 싹 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;역할이 답이다.&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;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;역할을 대체할 수 있는 객체는 동일한 메시지를 이해할 수 있는 객체로 한정된다는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 왕비가 재판관의 책임을 수행할 수 없다면 왕을 대신할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 역할을 수행하는 객체들이 동일한 메시지를 수신할 수 있기 때문에 동일한 책임을 수행할 수 있다는 것은 객체지향과 역할에 대한 아주 중요한 개념이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;협력의 추상화&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할을 사용함으로써 하나의 협력 안에 여러 종류의 객체가 참여할 수 있도록 협력의 추상화가 가능해진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설계자가 다뤄야 하는 협력의 개수를 줄일 수 있다.&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;b&gt;대체 가능성&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;객체가 역할을 대체 가능하기 위해서는 협력 안에서 역할이 수행하는 모든 책임을 동일하게 수행할 수 있어야 한다.&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하자면 역할의 대체 가능성은 행위 호환성을 의미라고, 행위 호환성은 동일한 책임의 수행을 의미한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span&gt; &lt;/span&gt;&amp;nbsp;객체의 모양을 결정하는 협력&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;흔한 오류&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;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;&lt;b&gt;협력을 따라 흐르는 객체의 책임&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;p data-ke-size=&quot;size16&quot;&gt;객체에게 책임이 할당되면 책임은 객체가 외부에 제공하게 될 행동을 의미한다. 객체의 행동을 결정한 후에 그 행동을 수행하는데 필요한 데이터를 고민한다. 필요한 데이터와 행동이 어느 정도 결정되고 나면 클래스의 구현 방법을 결정하면 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span&gt; &lt;/span&gt;&amp;nbsp;객체지향 설계 기법&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 책임-주도 설계&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;책임-주도 설계에서는 시스템의 기능, 책임을 객체의 책임으로 변환하고, 각 객체가 책임을 수행하는 중에 필요한 정보나 서비스를 제공해줄 협력자를 찾아 해당 협력자에게 책임을 할당하는 순차적인 방식으로 객체드르이 협력 공동체를 구축한다.&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;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;b&gt;설계 요약&lt;br /&gt;&lt;/b&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다&lt;/li&gt;
&lt;li&gt;시스템 책임을 더 작은 책임으로 분할한다.&lt;/li&gt;
&lt;li&gt;분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.&lt;/li&gt;
&lt;li&gt;객체가 책임을 수행하는 중에 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.&lt;/li&gt;
&lt;li&gt;해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&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;2. 디자인 패턴&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;일반적으로 디자인 패턴은 반복적으로 발생하는 문제와 그 문제에 대한 해법의 쌍으로 정의된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해결하려고 하는 문제가 무엇인지를 명확하게 서술&lt;/li&gt;
&lt;li&gt;패턴을 적용할 수 있는 상황과 적용할 수 없는 상황을 설명&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정한 상황에 적용 가능한 디자인 패턴을 잘 알고 있다면 책임-주도 설계의 절차를 순차적으로 따르지 않고도 객체들의 역할과 책임, 협력 관계를 빠르게 손쉽게 포착할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 테스트-주도 개발&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;테스트-주도 개발의 기본 흐름은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실패하는 테스트를 작성한다.&lt;/li&gt;
&lt;li&gt;테스트를 통과하는 가장 간단한 코드를 작성한다.&lt;/li&gt;
&lt;li&gt;리팩터링을 통해 중복을 제거한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 작동하는 깔끔한 코드를 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트-주도 개발은 테스트를 작성하는 것이 아니라 책임을 수행할 객체 또는 클라이언트가 기대하는 객체의 역할이 메시지를 수신할 때 어떤 결과를 반환하고 그 과정에서 어떤 객체와 협력할 것이지에 대한 기대를 코드의 형태로 작성하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트-주도 개발은 책임-주도 설계의 기본 개념과 다양한 원칙과 프랙티스, 패턴을 종합적으로 이해하고 좋은 설계에 대한 감각과 경험을 길러야만 적용할 수 있는 설계 기법이다.&lt;/p&gt;</description>
      <category>Development/서적</category>
      <category>객사오</category>
      <category>객체지향의사실과오해</category>
      <author>boiledeggishere</author>
      <guid isPermaLink="true">https://boiledeggishere.tistory.com/15</guid>
      <comments>https://boiledeggishere.tistory.com/entry/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%9D%98-%EC%82%AC%EC%8B%A4%EA%B3%BC-%EC%98%A4%ED%95%B4-4-%EC%97%AD%ED%95%A0-%EC%B1%85%EC%9E%84-%ED%98%91%EB%A0%A5#entry15comment</comments>
      <pubDate>Sun, 11 May 2025 19:16:48 +0900</pubDate>
    </item>
    <item>
      <title>[Jetpack Compose] derivedStateOf에 대하여...</title>
      <link>https://boiledeggishere.tistory.com/entry/Jetpack-Compose-derivedStateOf%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Medium]&amp;nbsp;Jetpack&amp;nbsp;Compose&amp;nbsp;-&amp;nbsp;When&amp;nbsp;shoul&amp;nbsp;I&amp;nbsp;use&amp;nbsp;derivedStateOf?&lt;/a&gt;&lt;br /&gt;해당 포스트를 내 마음대로 해석한 글이다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;derivedStateOf란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;derivedStateOf&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;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;derivedStateOf&lt;/b&gt;는 상태값이 UI보다 더 자주 업데이트돼야 하는 경우에 사용해야 한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 입력이 바뀌어 &lt;b&gt;derivedStateOf&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;예시를 하나 보자. 아이디를 입력하는 TextField와 아이디가 유효하면 활성화되는 버튼이 있는 화면이라고 가정하자. 다음 변수들을 각각의 상태를 관리하기 위한 변수들이다.&lt;/p&gt;
&lt;pre id=&quot;code_1745829477323&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var id by remember { mutableStateOf(&quot;&quot;) }
val buttonEnabled = checkIdValidity(id)&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;id&lt;/b&gt;에 빈 문자열이 할당되고, &lt;b&gt;buttonEnabled&lt;/b&gt;는 false가 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;id&lt;/b&gt;에 한 글자를 입력할 때마다 id의 상태가 변경되고, &lt;b&gt;buttonEnabled&lt;/b&gt;는 변경된 id를 받아 유효성을 검사하고 값을 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 경우, 글자가 입력될 때마다 &lt;b&gt;buttonEnabled&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;글자가 입력되는 것&quot;이 아니라 &quot;아이디가 유효한지&quot;에 초점을 맞춰야 한다. 이렇게 하면 &lt;b&gt;buttonEnabled&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;derivedStateOf&lt;/b&gt;를 사용한 코드다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745829769210&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var id by remember { mutableStateOf(&quot;&quot;) }
val buttonEnabled = remember {
  derivedStateOf { checkIdValidity(id) }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 동작 방식은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;id&lt;/b&gt;에 빈 문자열이 할당되고, &lt;b&gt;buttonEnabled&lt;/b&gt;는 false가 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;id&lt;/b&gt;에 한 글자를 입력하면 id의 상태가 변경되고, &lt;b&gt;buttonEnabled&lt;/b&gt;는 변경된 &lt;b&gt;id&lt;/b&gt;를 받아 유효성을 검사한다. 이 때, 기존의 &lt;b&gt;buttonEnabled&lt;/b&gt; 값과 &lt;b&gt;checkIdValidity&lt;/b&gt;가 새로 반환하는 값이 같다면 &lt;b&gt;buttonEnabled&lt;/b&gt;는 기존 값을 유지한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;buttonEnabled&lt;/b&gt;의 기존 상태와 새로운 상태가 동일하다면 &lt;b&gt;isIdValid&lt;/b&gt;는 상태가 변경되지 않고, &lt;b&gt;button&lt;/b&gt;에는 상태가 제공되지 않아 불필요한 리컴포지션을 방지할 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;실제론 컴포즈에서 스마트 리컴포지션을 지원하기 때문에 값이 변하지 않으면 리컴포지션이 일어나지 않는다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;그러나 상태 자체가 자주 변하거나 의미 없는 변화까지 리컴포지션을 일으킬 위험이 있는 경우, &lt;br /&gt;derivedStateOf를 사용해 실제로 의미 있는 변경만 UI에 반영하는 최적화를 적용할 수 있다. &lt;br /&gt;&lt;br /&gt;자주 사용되는 경우는 다음과 같다고 한다:&lt;br /&gt;&amp;nbsp; &amp;nbsp;- 스크롤이 임계치를 넘어가는지 &amp;rArr; scrollPosition &amp;gt; 0 &lt;br /&gt;&amp;nbsp; &amp;nbsp;- 목록의 크기가 임계치보다 큰지 &amp;rArr; items &amp;gt; 0 &lt;br /&gt;&amp;nbsp; &amp;nbsp;- 상태의 유효성 검사 &amp;rArr; variable.isValid()&lt;/blockquote&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;derivedStateOf는 remember 되어야 하는가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;derivedStateOf&lt;/b&gt;가 컴포저블 내부에서 쓰인다면 &lt;b&gt;remember&lt;/b&gt;와 같은 캐싱 블록으로 감싸져야 한다. &lt;b&gt;derivedStateOf&lt;/b&gt;는 &lt;b&gt;mutableStateOf&lt;/b&gt;와 같은 다른 객체들처럼 리컴포지션을 견디고 값을 보존해야 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 &lt;b&gt;remember&lt;/b&gt;로 감싸지지 않는다면 매 리컴포지션마다 변수를 초기화하게 되는 비효율적인 상황이 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1745830038210&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val state by remember { 
	derivedStateOf{ ... }
}&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;b&gt;  &lt;/b&gt;derivedStateOf와 remember(key) 간의 차이점은 무엇인가?&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;&lt;b&gt;derivedStateOf&lt;/b&gt;의 사용법을 예시로 이해해보자. &lt;b&gt;LazyColumn&lt;/b&gt;에서 스크롤이 돼야 활성화가 되는 버튼이 있다고 가정하자.&lt;/p&gt;
&lt;pre id=&quot;code_1745830089795&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val isEnabled = lazyListState.firstVisibleItemIndex &amp;gt; 0&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;firstVisibleItemIndex&lt;/b&gt;의 값은 증가하게 될 것이고, 리컴포지션이 계속 일어나며 &lt;b&gt;isEnabled&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;firstVisibleItemIndex&lt;/b&gt;가 바뀌는 것이 아니라 &lt;b&gt;firstVisibleItemIndex&lt;/b&gt;가 0보다 큰지다. &lt;b&gt;firstVisibleItemIndex&lt;/b&gt;라는 입력이 바뀌는 횟수와 &lt;b&gt;isEnabled&lt;/b&gt;에 값이 할당되는 횟수에 차이가 나고, 이 때 &lt;b&gt;derivedStateOf&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;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;remember(key)&lt;/b&gt;는 일반적으로 파라미터를 동반하는 비싼 연산을 캐싱&lt;/span&gt;하기 위해 사용한다. &lt;b&gt;key&lt;/b&gt;값이 바뀌어야만 내부 연산이 실행되는 구조로, 입력과 출력의 수가 동일하게 유지되는 구조다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;derivedStateOf&lt;/b&gt;와 &lt;b&gt;remember(key)&lt;/b&gt;의 주요 차이점은 입력과 출력의 변화 간 차이가 존재한다는 점이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;derivedStateOf&lt;/b&gt;는 입력의 변화 수보다 출력의 변화 수가 작거나 같다. &lt;b&gt;remember(key)&lt;/b&gt;는 입력과 출력의 변화의 수가 같다.&amp;nbsp;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;b&gt;  &lt;/b&gt;remember(key)와 derivedStateOf를 같이 사용할 일이 있을까?&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1745830233727&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun ScrollToTopButton(lazyListState: LazyListState, threshold: Int) {
  // There is a bug here
  val isEnabled by remember {
    derivedStateOf { lazyListState.firstVisibleItemIndex &amp;gt; threshold }
  }
  
  Button(onClick = { }, enabled = isEnabled) {
    Text(&quot;Scroll to top&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;위 경우에 대해 생각해보자. &lt;b&gt;remember&lt;/b&gt; 블록은 &lt;b&gt;ScrollToTopButton&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;threshold&lt;/b&gt; 값이 바뀌게 된다면 어떻게 될까? 대소관계의 기준이 변경되며 &lt;b&gt;derivedStateOf&lt;/b&gt; 내부에서 반환되는 값이 달라져야 하지만, &lt;b&gt;remember&lt;/b&gt;는 최초 실행의 &lt;b&gt;snapshot&lt;/b&gt;만 보유하고 있기 때문에 초기 &lt;b&gt;threshold&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;ScrollToTopButton&lt;/b&gt;이 몇번 리컴포즈된던, 입력이 몇번 바뀌던 상관없이, 키 없는 &lt;b&gt;remember&lt;/b&gt;는 초기값을 계속 유지하며 &lt;b&gt;derivedStateOf&lt;/b&gt;에 새로운 값이 들어갈 수 없도록 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EC45P/btsNBz2nNMj/tL8j3ji2YoFoIFlermLnbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EC45P/btsNBz2nNMj/tL8j3ji2YoFoIFlermLnbK/img.png&quot; data-alt=&quot;컴포저블 내부의 초기 상태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EC45P/btsNBz2nNMj/tL8j3ji2YoFoIFlermLnbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEC45P%2FbtsNBz2nNMj%2FtL8j3ji2YoFoIFlermLnbK%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;487&quot; height=&quot;141&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;201&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컴포저블 내부의 초기 상태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dr5iiZ/btsNDrB71U7/JWqedOUMIPcUpdZN4IwrAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dr5iiZ/btsNDrB71U7/JWqedOUMIPcUpdZN4IwrAK/img.png&quot; data-alt=&quot;리컴포즈가 되어 threshold값이 바뀌어도, remember는 초기 threshold값을 기억하고 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dr5iiZ/btsNDrB71U7/JWqedOUMIPcUpdZN4IwrAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdr5iiZ%2FbtsNDrB71U7%2FJWqedOUMIPcUpdZN4IwrAK%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;483&quot; height=&quot;141&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;리컴포즈가 되어 threshold값이 바뀌어도, remember는 초기 threshold값을 기억하고 있다.&lt;/figcaption&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 &lt;b&gt;remember(key)&lt;/b&gt;와 &lt;b&gt;derivedStateOf&lt;/b&gt;를 같이 쓸 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키에 &lt;b&gt;threshold&lt;/b&gt;를 주게 되면, &lt;b&gt;threshold&lt;/b&gt;가 바뀔 때마다 &lt;b&gt;snapshot&lt;/b&gt;을 찍게 되고 이 값을 기반으로 &lt;b&gt;derivedStateOf&lt;/b&gt;의 상태를 초기화할 수 있게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1745830442313&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val isEnabled by remember(threshold) {
	derivedStateOf { lazyListState.firstVisibleItemIndex &amp;gt; threshold }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m6Ghb/btsNCfPVu1L/O6c5zd6TbOKhZb9tpkk0uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m6Ghb/btsNCfPVu1L/O6c5zd6TbOKhZb9tpkk0uK/img.png&quot; data-alt=&quot;key를 통해 remember 내부에서 사용되는 threshold 값에 변화를 줄 수 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m6Ghb/btsNCfPVu1L/O6c5zd6TbOKhZb9tpkk0uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm6Ghb%2FbtsNCfPVu1L%2FO6c5zd6TbOKhZb9tpkk0uK%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;420&quot; height=&quot;211&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;key를 통해 remember 내부에서 사용되는 threshold 값에 변화를 줄 수 있다.&lt;/figcaption&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  여러 상태들을 결합하기 위해 derivedStateOf를 사용할 일이 있을까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 상태를 가지고 하나의 결과를 만드려고 할 때, 하나가 바뀌면 리컴포지션이 일어나도록 유도하고 싶을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1745830521615&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var firstName by remember { mutableStateOf(&quot;&quot;) }
var lastName by remember { mutableStateOf(&quot;&quot;) }

// This derivedStateOf is redundant
val fullName = remember { derivedStateOf { &quot;$firstName $lastName&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;위와 같은 경우엔 &lt;b&gt;derivedStateOf&lt;/b&gt;는 별다른 일을 하고 있지 않을 뿐더러 오히려 하나의 오버헤드만 발생시키고 있다. 또한 위 코드는 동기적인 상태 업데이트임으로 상태를 감지하여 업데이트할 필요도 없고, Compose의 상태 스냅샷 시스템이 비동기 업데이트를 관리하는 방법은 따로 있다. 따라서 저 경우엔 굳이 상태를 관리할 필요가 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1745830560381&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val fullName = &quot;$firstName $lastName&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실제 리컴포지션 비교: remember(key) vs derivedStateOf&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Medium 글의 댓글 중 &lt;b&gt;remember(key)&lt;/b&gt;와 &lt;b&gt;derivedStateOf&lt;/b&gt;의 성능과 기능의 차이가 이해가 되지 않는다며 깃허브 코드를 올려둔 것을 보았다. 대충 해석해보자면 두 방법 모두 리컴포지션의 수가 동일하다고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;249&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JcWu1/btsND6jWKPd/JMHoZbFokYJYdCaPOsHAs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JcWu1/btsND6jWKPd/JMHoZbFokYJYdCaPOsHAs1/img.png&quot; data-alt=&quot;안드레이씨의 궁금증&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JcWu1/btsND6jWKPd/JMHoZbFokYJYdCaPOsHAs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJcWu1%2FbtsND6jWKPd%2FJMHoZbFokYJYdCaPOsHAs1%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;702&quot; height=&quot;249&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;249&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안드레이씨의 궁금증&lt;/figcaption&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ATest 컴포저블에서 frequentlyChangedState라는 정수형 변수를 0.2초마다 1씩 증가시킨다.&lt;/li&gt;
&lt;li&gt;valueToDisplay 변수는 frequentlyChangedState를 2로 나눈 값을 저장한다. 이 값은 &lt;b&gt;remember(key)&lt;/b&gt;로 관리한다.&lt;/li&gt;
&lt;li&gt;valueToDisplay를 ATestInner의 파라미터로 전달한다.&lt;/li&gt;
&lt;li&gt;ATestInner에서 Text에 파라미터를 다시 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;remember(key)를 사용하는 이 코드를 실행시켰을 때 ATest와 ATestInner의 리컴포지션이 각각 몇번씩 발생하는지 알아본 결과 다음과 같았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPCW6L/btsNCce4X6K/llbtulhwCNJKqX1y3x3570/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPCW6L/btsNCce4X6K/llbtulhwCNJKqX1y3x3570/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPCW6L/btsNCce4X6K/llbtulhwCNJKqX1y3x3570/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPCW6L%2FbtsNCce4X6K%2FllbtulhwCNJKqX1y3x3570%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;530&quot; height=&quot;74&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;114&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;frequenlyChangedState가 변경됨으로 인해 remember 블록의 연산이 실행되어 valueToDisplay가 초기화된다. 이로 인해 ATest는 10번 리컴포즈되는 것이고, ATestInner도 10번 재실행되지만 5번은 이전 파라미터와 같은 값의 파라미터를 넘겨받기 때문에 컴포즈 시스템에 의해 스킵된다.&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;remember(key)&lt;/b&gt;를 &lt;b&gt;derivedStateOf&lt;/b&gt;로 바꾸고 실행시켜본 결과다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bW8Fmc/btsNEnlqhKW/hKUjywbEhTKvaLHMx67AY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bW8Fmc/btsNEnlqhKW/hKUjywbEhTKvaLHMx67AY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bW8Fmc/btsNEnlqhKW/hKUjywbEhTKvaLHMx67AY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbW8Fmc%2FbtsNEnlqhKW%2FhKUjywbEhTKvaLHMx67AY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;534&quot; height=&quot;77&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;frequenlyChangedState가 변경되어도 2로 나눈 결과가 바뀌지 않는 이상 valueToDisplay는 초기화되지 않는다. 따라서 모든 컴포저블들이 스킵없이 딱 5번 리컴포즈되는 것을 볼 수 있다.&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;/h4&gt;
&lt;figure id=&quot;og_1745831135368&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Jetpack Compose &amp;mdash; When should I use derivedStateOf?&quot; data-og-description=&quot;derivedStateOf &amp;mdash; a really common question we see is where and when is the correct place to use this API?&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b&quot; data-og-url=&quot;https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cY5Kg1/hyYMQJXEBb/YIRFxKREiABeT0wEsW7mb0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cY5Kg1/hyYMQJXEBb/YIRFxKREiABeT0wEsW7mb0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Jetpack Compose &amp;mdash; When should I use derivedStateOf?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;derivedStateOf &amp;mdash; a really common question we see is where and when is the correct place to use this API?&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1745831131647&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Compose Study] derivedStateOf&quot; data-og-description=&quot;val result = remember(state1, state2) { calculation(state1, state2) }val result1 = remember { derivedStateOf { calculation(state1, state2) } }derivedStateOf* 특정 상태가 계산되거나 다른 상태 개체에서 파생되는 경우 derivedStateOf를 &quot; data-og-host=&quot;hbj0209.tistory.com&quot; data-og-source-url=&quot;https://hbj0209.tistory.com/264&quot; data-og-url=&quot;https://hbj0209.tistory.com/264&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bq5Zl4/hyYMbgGt32/lhPggDIJpbYY09zjtoxO3K/img.gif?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/cuR33V/hyYId7E2jd/2pEKpEgW6EffgV33fGN9Vk/img.gif?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bdsjTa/hyYM5mOunt/VKZChiDSAGyNVv9qbGMAuk/img.jpg?width=2560&amp;amp;height=522&amp;amp;face=0_0_2560_522&quot;&gt;&lt;a href=&quot;https://hbj0209.tistory.com/264&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hbj0209.tistory.com/264&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bq5Zl4/hyYMbgGt32/lhPggDIJpbYY09zjtoxO3K/img.gif?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/cuR33V/hyYId7E2jd/2pEKpEgW6EffgV33fGN9Vk/img.gif?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bdsjTa/hyYM5mOunt/VKZChiDSAGyNVv9qbGMAuk/img.jpg?width=2560&amp;amp;height=522&amp;amp;face=0_0_2560_522');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Compose Study] derivedStateOf&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;val result = remember(state1, state2) { calculation(state1, state2) }val result1 = remember { derivedStateOf { calculation(state1, state2) } }derivedStateOf* 특정 상태가 계산되거나 다른 상태 개체에서 파생되는 경우 derivedStateOf를&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hbj0209.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Jetpack Compose</category>
      <category>Jetpack Compose</category>
      <category>상태관리</category>
      <author>boiledeggishere</author>
      <guid isPermaLink="true">https://boiledeggishere.tistory.com/14</guid>
      <comments>https://boiledeggishere.tistory.com/entry/Jetpack-Compose-derivedStateOf%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC#entry14comment</comments>
      <pubDate>Mon, 28 Apr 2025 18:14:10 +0900</pubDate>
    </item>
    <item>
      <title>[Jetpack Compose] CompositionLocal에 대하여...</title>
      <link>https://boiledeggishere.tistory.com/entry/Jetpack-Compose-CompositionLocal%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;CompositionLocal이란?&lt;/b&gt;&lt;br /&gt;Composable 트리의 최상단에서 상태를 관리하기 위해 소개된 개념&lt;/blockquote&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;상태 호이스팅 (State Hoisting)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포저블 함수는 트리 구조로 조직된다. 먼저 등장하는 컴포저블이 상위, 그 다음이 하위가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태를 트리의 가능한 가장 상위 노드에 선언하여 관리하는 것이 상태 호이스팅이며, 컴포즈에서 권장되는 패턴이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;특정 상태가 여러 컴포넌트에서 필요한 경우, 그 모든 컴포넌트들의 접점이 되는 상위 컴포넌트에서 관리함으로써 재사용성을 높임&lt;/li&gt;
&lt;li&gt;여러 컴포넌트들이 하나의 상태를 참조함으로써, UI의 일관성을 유지할 수 있음&lt;/li&gt;
&lt;li&gt;상태 변경 로직을 한 곳에 집중시킬 수 있음. 이를 통해 가독성을 향상시키고, 상태 변경에 따른 부수 효과(Side-Effects)도 관리하기 쉬워짐&lt;/li&gt;
&lt;li&gt;하위 컴포넌트에서 상태를 읽기 전용으로 사용하고, 필요에 따라 상태 업데이트 함수를 제공하여 상태를 변경하도록 제어할 수 있음&lt;/li&gt;
&lt;/ol&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;이러한 구조적 문제를 해결하고 상태를 효율적으로 전달하기 위해 CompositionLocal이 활용된다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CompositionLocal&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mX46a/btsNr3vRq3P/SR4KSveqiC1tzGzBOZhFK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mX46a/btsNr3vRq3P/SR4KSveqiC1tzGzBOZhFK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mX46a/btsNr3vRq3P/SR4KSveqiC1tzGzBOZhFK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmX46a%2FbtsNr3vRq3P%2FSR4KSveqiC1tzGzBOZhFK1%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;450&quot; height=&quot;251&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;790&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;CompositionLocal은 컴포저블 트리 상위에서 선언된 상태를 하위에서 접근할 수 있는 방법을 제공한다. 상위에서 지정된 CompositionLocal은 하위 컴포저블들에서 인자로 받지 않고도 접근이 가능하다.&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;/h4&gt;
&lt;pre id=&quot;code_1745332495652&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// StaticProvidableCompositionLocal
val localStaticComposition = staticCompositionLocalOf {
    DEFAULT_VALUE
}

// DynamicProvidableCompositionLocal
val localDynamicComposition = compositionLocalOf {
    DEFAULT_VALUE
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;static의 경우, 값이 변경되면 관련 컴포저블들과 그 하위 컴포저블 모두가 재구성된다.&lt;br /&gt;즉, 데이터 변경으로 인해 상당히 많은 재구성이 발생하게 된다.&lt;/li&gt;
&lt;li&gt;dynamic의 경우, 값이 변경되면 해당 데이터를 직접 사용하는 컴포저블 함수만 재구성된다.&lt;br /&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;CompositionLocalProvider의 value는 하위 컴포저블에 전달될 값을 의미한다. content는 하위 컴포저블을 의미한다. 마지막 파라미터로 선언되어있어 트레일링 람다로 구현할 수 있다.&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;예시처럼 기본값이 정의된 CompositionLocal 객체를 생성하고, provides를 사용하여 원하는 값을 전달하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1745332595927&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun CompositionLocalProvider(
    vararg values: ProvidedValue&amp;lt;*&amp;gt;, 
    content: @Composable () -&amp;gt; Unit
)

// 예시
val ColorCompositionLocal = staticCompositionLocalOf {
    Color.Blue // 기본값을 정의
}
@Composable
fun Composable1() {
    ...
    CompositionLocalProvider(ColorCompositionLocal.provides(Color.Red)) {
        Composable4()
    }
}&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하위 컴포저블에선 컴포저블 객체의 current를 통해 값을 읽을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시에선 ColorCompositionLocal.current를 통해 빨간색 값을 받아올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun Composable4() {
    Text(
        modifier = Modifier.background(color = ColorCompositionLocal.current), // 빨간색
        text = &quot;Composable4&quot;
    )
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 CompositionLocal들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용되는 방법을 보면 알겠지만 컴포저블 내부에서 흔히 사용되는 &lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;1&quot;&gt;LocalContext.current, LocalDensity.current&lt;/span&gt;들도 다 CompositionLocal을 통해 전달되는 것들이다.&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;Activity의 setContent 내부 로직에서 ProvideAndroidCompositionLocals가 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setContent 블록 내부에 들어가는 컴포저블 함수가 ProvideAndroidCompositionLocals에 content 파라미터로 넘겨지게 되며, 결과적으로 컴포저블 내에서 compose 플랫폼의 여러 전역 변수에 접근할 수 있게 된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package androidx.compose.ui.platform
...

/*선언 방식*/
val LocalConfiguration = compositionLocalOf&amp;lt;Configuration&amp;gt; {
    noLocalProvidedFor(&quot;LocalConfiguration&quot;)
}

val LocalContext = staticCompositionLocalOf&amp;lt;Context&amp;gt; {
    noLocalProvidedFor(&quot;LocalContext&quot;)
}

internal val LocalImageVectorCache = staticCompositionLocalOf&amp;lt;ImageVectorCache&amp;gt; {
    noLocalProvidedFor(&quot;LocalImageVectorCache&quot;)
}

val LocalLifecycleOwner = staticCompositionLocalOf&amp;lt;LifecycleOwner&amp;gt; {
    noLocalProvidedFor(&quot;LocalLifecycleOwner&quot;)
} 
...

/*사용 방식*/
@Composable
@OptIn(ExperimentalComposeUiApi::class)
internal fun ProvideAndroidCompositionLocals(
    owner: AndroidComposeView,
    content: @Composable () -&amp;gt; Unit
) {
    ...
    
    CompositionLocalProvider(
        LocalConfiguration provides configuration,
        LocalContext provides context,
        LocalLifecycleOwner provides viewTreeOwners.lifecycleOwner,
        LocalSavedStateRegistryOwner provides viewTreeOwners.savedStateRegistryOwner,
        LocalSaveableStateRegistry provides saveableStateRegistry,
        LocalView provides owner.view,
        LocalImageVectorCache provides imageVectorCache
    ) {
        ProvideCommonCompositionLocals(
            owner = owner,
            uriHandler = uriHandler,
            content = content
        )
    }
    ...
}

 
&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;이 파일에서 static과 dynamic이 사용되는 방식을 이해할 수 있는데,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;static의 경우, 액티비티의 Context나 Lifecycle처럼 값이 바뀔 일이 거의 없는 요소에 대한 CompositionLocal을 선언하기 위해 사용된다.&lt;/li&gt;
&lt;li&gt;dynamic의 경우, 화면의 Configuration처럼 값이 바뀔 가능성이 있는 요소에 대한 CompositionLocal을 선언하기 위해 사용된다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Android/Jetpack Compose</category>
      <category>Android</category>
      <category>Jetpack Compose</category>
      <author>boiledeggishere</author>
      <guid isPermaLink="true">https://boiledeggishere.tistory.com/13</guid>
      <comments>https://boiledeggishere.tistory.com/entry/Jetpack-Compose-CompositionLocal%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90#entry13comment</comments>
      <pubDate>Tue, 22 Apr 2025 23:55:35 +0900</pubDate>
    </item>
    <item>
      <title>[트슛] 모듈 Extension 오류: Extension of type 'LibraryExtension` does not exist</title>
      <link>https://boiledeggishere.tistory.com/entry/%ED%8A%B8%EC%8A%9B-%EB%AA%A8%EB%93%88-Extension-%EC%98%A4%EB%A5%98-Extension-of-type-LibraryExtension-does-not-exist</link>
      <description>&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;Compose와 Kotlin에 관련된 플러그인을 마련한 후 Application 모듈에 해당 플러그인들을 적용하려는 순간 다음과 같은 오류가 발생했다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1745331184823&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//오류 중 일부 발췌
Caused by: org.gradle.api.internal.plugins.PluginApplicationException: Failed to apply plugin class 'com.boiled.calendar.buildlogic.primitive.ComposePlugin'
...

Caused by: org.gradle.api.UnknownDomainObjectException: Extension of type 'LibraryExtension' does not exist. Currently registered extension types: [ExtraPropertiesExtension, LibrariesForLibs, VersionCatalogsExtension, BasePluginExtension, DefaultArtifactPublicationSet, SourceSetContainer, ReportingExtension, JavaToolchainService, JavaPluginExtension, BaseAppModuleExtension, ApplicationAndroidComponentsExtension, NamedDomainObjectContainer&amp;lt;BaseVariantOutput&amp;gt;, KotlinAndroidProjectExtension, KotlinTestsRegistry, ComposeCompilerGradlePluginExtension]
	at org.gradle.internal.extensibility.ExtensionsStorage.getHolderByType(ExtensionsStorage.java:89)
	at org.gradle.internal.extensibility.ExtensionsStorage.getByType(ExtensionsStorage.java:74)
	at org.gradle.internal.extensibility.DefaultConvention.getByType(DefaultConvention.java:172)
	at com.boiled.calendar.buildlogic.primitive.ComposePlugin.apply(ComposePlugin.kt:54)
...&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 오류 내용 중에 ComposePlugin에 `LibraryExtension`이 존재하지 않는다는 오류 내용을 볼 수 있다. 따라서 해당 오류가 발생하는 코드 파일로 이동해봤다.&lt;/p&gt;
&lt;pre id=&quot;code_1745331405053&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//ComposePlugin.kt
class ComposePlugin : Plugin&amp;lt;Project&amp;gt; {
    override fun apply(target: Project) {
        with(target) {
            with(plugins) {
                apply(&quot;org.jetbrains.kotlin.plugin.compose&quot;)
            }

            extensions.getByType&amp;lt;ComposeCompilerGradlePluginExtension&amp;gt;().apply {
                val isRelease = System.getenv(&quot;BUILD_TYPE&quot;) == &quot;release&quot;

                enableStrongSkippingMode.set(true)
                includeSourceInformation.set(!isRelease)
                includeTraceMarkers.set(!isRelease)
            }

            extensions.getByType&amp;lt;LibraryExtension&amp;gt;().apply {
                buildFeatures.compose = true
            }
            ...&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;문제가 되는 코드는 바로 LibraryExtension 타입의 extension이 선언된 부분이다. 컴파일러에서는 LibraryExtension이 존재하지 않는다고 오류를 발생시켰지만, IDE는 오류를 파악하지 못했다. 즉 문법 상으론 문제가 없다는 것이다.&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;추측해봤을 땐, 빌드에서 애플리케이션 모듈에 ComposePlugin을 추가하려는 시도를 했는데, LibraryExtension이라는 라이브러리 전용 Extension을 사용하려고 하여 문제가 발생한 것으로 보인다. 즉, 애플리케이션 모듈에선 ApplicationExtension과 같은 Extension이 따로 존재하지 않나 생각이 들었다.&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;찾아보니 실제로 ApplicationExtension과 LibraryExtension이 따로 존재했다.&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;ApplicationExtension&lt;/b&gt;은 직접 실행 가능한 Android 애플리케이션 모듈의 빌드 설정을 담당하는 확장 클래스&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LibraryExtension&lt;/b&gt;은 다른 프로젝트에서 재사용할 수 있는 라이브러리의 빌드 설정을 담당하는 확장 클래스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 위 오류는 &lt;b&gt;Phone&amp;amp;Tablet&lt;/b&gt; 모듈에서 &lt;b&gt;LibraryExtension&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;LibraryExtension&lt;/b&gt;인지 &lt;b&gt;ApplicationExtension&lt;/b&gt;인지 확인을 거쳐서 최종적으로 하나의 Extension만 반환하는 변수를 생성하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1745331621291&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal val Project.applicationExtension: CommonExtension&amp;lt;*, *, *, *, *, *&amp;gt;
    get() = extensions.getByType&amp;lt;ApplicationExtension&amp;gt;()

internal val Project.libraryExtension: CommonExtension&amp;lt;*, *, *, *, *, *&amp;gt;
    get() = extensions.getByType&amp;lt;LibraryExtension&amp;gt;()

internal val Project.androidExtension: CommonExtension&amp;lt;*, *, *, *, *, *&amp;gt;
    get() = runCatching { libraryExtension }
        .recoverCatching { applicationExtension }
        .onFailure { println(&quot;Could not find Library or Application extension&quot;) }
        .getOrThrow()&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;이후 라이브러리 확장 클래스를 불러오던 기존 코드를 지우고, 위에 만든 androidExtension을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;androidExtension.apply {
	buildFeatures.compose = true        
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cku8cQ/btsNvguwwdc/owxpWGrWqefZ7OaIEWmQjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cku8cQ/btsNvguwwdc/owxpWGrWqefZ7OaIEWmQjK/img.png&quot; data-alt=&quot;고친 후에 성공적으로 빌드된 것을 확인할 수 있었다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cku8cQ/btsNvguwwdc/owxpWGrWqefZ7OaIEWmQjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcku8cQ%2FbtsNvguwwdc%2FowxpWGrWqefZ7OaIEWmQjK%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;384&quot; height=&quot;130&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;고친 후에 성공적으로 빌드된 것을 확인할 수 있었다.&lt;/figcaption&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;&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;그리고 예전에 빌드로직을 작성할 때도 느낀 것이지만 모듈 타입에 따라 설정이 달라 빌드로직을 구현하는 것이 꽤 귀찮고 까다로운 것 같다. 에러가 발생해도 그 이유를 찾기가 쉽지 않기도 하다. 하지만 깔끔한 코드와 높은 유지보수성을 확보하기 위해서 구현해두는 것이 좋지 않을까 싶다!&lt;/p&gt;</description>
      <category>Android/Troubleshooting</category>
      <author>boiledeggishere</author>
      <guid isPermaLink="true">https://boiledeggishere.tistory.com/12</guid>
      <comments>https://boiledeggishere.tistory.com/entry/%ED%8A%B8%EC%8A%9B-%EB%AA%A8%EB%93%88-Extension-%EC%98%A4%EB%A5%98-Extension-of-type-LibraryExtension-does-not-exist#entry12comment</comments>
      <pubDate>Tue, 22 Apr 2025 23:29:45 +0900</pubDate>
    </item>
  </channel>
</rss>