<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>어플 공부 기록장✍ </title>
    <link>https://soung-appdeveloper.tistory.com/</link>
    <description>안녕하세요, 저는 어플리케이션을 만드는 과정에 관심이 많습니다 </description>
    <language>ko</language>
    <pubDate>Sat, 4 Jul 2026 00:35:52 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>청둥이 </managingEditor>
    <image>
      <title>어플 공부 기록장✍ </title>
      <url>https://tistory1.daumcdn.net/tistory/6940148/attach/5314d4402c274399b0563b0dcb66f3e6</url>
      <link>https://soung-appdeveloper.tistory.com</link>
    </image>
    <item>
      <title>SOLID - ISP (인터페이스 분리 원칙)</title>
      <link>https://soung-appdeveloper.tistory.com/174</link>
      <description>&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;face&quot; data-emoticon-name=&quot;071&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/071.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/071.png&quot; width=&quot;80&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요~ 오늘은 SOLID 원칙 중 네 번째, &lt;b&gt;인터페이스 분리 원칙&lt;/b&gt;에 대해 알아보겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인터페이스 분리 원칙&lt;/b&gt;(Interface Segregation Principle)은 클라이언트 자신이 사용하는 기능 단위까지 인터페이스를 작게 분리하는 원칙입니다. 작게 분리하는 이유는, &lt;u&gt;클라이언트가 자신이 사용하지 않는 메서드에 의존하도록 강제하지 않기 위함&lt;/u&gt;인데요. 인터페이스를 적절히 단위로 분리하면 특정 클라이언트가 불필요한 메서드를 구현하는 상황을 방지할 수 있습니다. 이는 결과적으로 클라이언트 코드의 결합도를 낮추고, 시스템의 유연성을 증가시키며 변경 시 영향 범위를 최소화하는 데 도움을 줍니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 코틀린 위반/준수 예제&lt;/h2&gt;
&lt;pre id=&quot;code_1765596540166&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ISP 위반
interface Worker {
    fun work()
    fun eat()
}

class Human: Worker {
    override fun work() { println(&quot;Human can work&quot;) }
    override fun eat() { println(&quot;Human can eat&quot;) }
}

class Robot: Worker {
    override fun work() { println(&quot;Robot can work&quot;) }
    override fun eat() { throw UnsupportedOperationException(&quot;Robot can't eat&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;코틀린 예제&lt;/b&gt;를 살펴보겠습니다. 위 코드에서 Worker 인터페이스는 work()와 eat() 이라는 두 개의 메소드를 정의하고 있습니다. 실제로 사람은 일을 하고 밥도 먹으니 이 인터페이스를 구현하는 데 아무런 문제가 없습니다. 하지만 로봇은 어떨까요? 로봇은 일은 할 수 있지만 음식을 먹지는 못합니다. 이 상황에서 Robot 클래스는 자신에게 필요 없는 eat() 메서드를 억지로 상속받게 되고, 해당 기능을 수행할 수 없어 예외를 던지고 있습니다. &lt;u&gt;특정 클라이언트(Robot)가 자신이 사용하지 않는 메서드에 불필요하게 의존&lt;/u&gt;하고 있기 때문에 이는 ISP 원칙을 위반합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765596843348&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ISP 준수
interface Workable {
    fun work()
}

interface Eatable {
    fun eat()
}

class Human: Workable, Eatable {
    override fun work() { println(&quot;Human can work&quot;) }
    override fun eat() { println(&quot;Human can eat&quot;) }
}

class Robot: Workable {
    override fun work() { println(&quot;Robot can work&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;앞서 발생했던 문제를 해결하기 위해, Worker 인터페이스를 Workable과 Eatable 이라는 &lt;u&gt;두 개의 인터페이스로 분리&lt;/u&gt;했습니다. 이때, Human 클래스는 두 인터페이스를 모두 구현하면 됩니다. Robot 클래스는 이제 본연의 기능인 Workable만 구현하여, 더 이상 예외를 던지는 코드를 작성할 필요가 없습니다. 클라이언트는 자신이 사용하지 않는 메서드에 더이상 의존하지 않아 ISP 원칙을 준수할 수 있게 되었습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. 안드로이드 위반/준수 예제&lt;/h2&gt;
&lt;pre id=&quot;code_1765597100724&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ISP 위반
interface ItemTouchListener {
    fun onClick()
    fun onLongClick()
    fun onSwipe()
}

class HomeFragment: ItemTouchListener {
    override fun onClick() { // 클릭 기능 처리 }
    override fun onLongClick() { // 사용 X }
    override fun onSwipe() { // 사용 X }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 &lt;b&gt;안드로이드에서 흔히 발생하는 사례&lt;/b&gt;를 살펴보겠습니다. 여기 클릭, 롱 클릭, 스와이프 이벤트를 모두 처리하는 ItemTouchListener 인터페이스가 있습니다. 만약 HomeFragment 에서 단순 클릭 이벤트만 필요하다면 어떻게 될까요? 위 코드에서 사용하지 않는 onLongClick()과 onSwipe() 메소드를 억지로 구현하여 의도적으로 비워둬야 합니다. 이런 설계는 &lt;u&gt;인터페이스에 작은 변경이 생겨도 해당 기능을 쓰지 않는 모든 클래스에 영향을 주어 유지보수를 더 어렵게 만듭니다&lt;/u&gt;.&lt;/p&gt;
&lt;pre id=&quot;code_1765597324696&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ISP 준수
interface Clickable {
    fun onClick()
}

interface LongClickable {
    fun onLongClick()
}

interface Swipeable {
    fun onSwipe()
}

class HomeFragment: Clickable {
    override fun onClick() { // 클릭 처리 }
}&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;기존 인터페이스를 Clickable, LongClickable, Swipeable로 각각 세분화했습니다.&lt;span&gt; &lt;/span&gt;이렇게 기능을 독립적인 인터페이스로 분리하면 명확하게 의도를 전달할 수 있고, 더 이상 사용하지 않는 기능을 위해 빈 본문을 만들거나 예외 처리를 하지 않게 되어 코드 가독성이 좋아집니다. 나중에 스와이프 기능이 필요한 새로운 화면이 생기면 Swipeable 인터페이스만 구현하면 되기에, 재사용성 또한 좋아집니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 클린 아키텍처와의 연관성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스 분리 원칙을 설계해서 작성하면, 클린 아키텍처의 각 레이어는 자신이 필요한 기능만을 제공하는 인터페이스에 의존하게 됩니다. 이는 코드 변경의 범위를 줄이고 유지보수를 쉽게 만들어줍니다. 또한, 인터페이스 분리 원칙은 한개의 클래스가 하나의 책임만을 갖는 단일 책임 원칙과 유사한&lt;span style=&quot;color: #333333; text-align: start;&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;color: #333333; text-align: start;&quot;&gt;오늘은&lt;span&gt; 인터페이스 분리 &lt;/span&gt;&lt;/span&gt;원칙에 대해 살펴보았습니다. 다음 시간에는 SOLID 원칙의 마지막 다섯 번째 원칙, 의존성 역전 원칙에 대해 알아보겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Clean Architecture</category>
      <category>ISP</category>
      <category>solid</category>
      <category>SRP와 비슷한 맥락</category>
      <category>유지보수</category>
      <category>인터페이스 분리 원칙</category>
      <category>클린 아키텍처</category>
      <author>청둥이 </author>
      <guid isPermaLink="true">https://soung-appdeveloper.tistory.com/174</guid>
      <comments>https://soung-appdeveloper.tistory.com/174#entry174comment</comments>
      <pubDate>Sun, 11 Jan 2026 18:51:03 +0900</pubDate>
    </item>
    <item>
      <title>SOLID - LSP (리스코프 치환 원칙)</title>
      <link>https://soung-appdeveloper.tistory.com/173</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;025&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/025.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/025.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요~ 오늘은 SOLID 원칙 중 세 번째, &lt;b&gt;LSP 원칙&lt;/b&gt;에 대해 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 정의와 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리스코프 치환 원칙&lt;/b&gt;(Liskov Substitution Principle)은 리스코프 교수님이 주장한 원칙으로, &lt;b&gt;하위 클래스는 언제나 상위 클래스를 대체할 수 있어야 한다&lt;/b&gt;는 것을 의미합니다. 좀 더 풀어서 설명하자면, &lt;b&gt;하위 클래스는 상위 클래스가 제공하는 모든 기능을 동일하게 수행해야 하며&lt;/b&gt;, &lt;b&gt;상위 클래스의 동작을 변경하거나 제한해서는 안된다&lt;/b&gt;는 것으로, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;안정적이고 예측 가능&lt;/span&gt;&lt;/span&gt;한 코드를 설계하는 데 중요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 코틀린 위반/준수 예제&lt;/h2&gt;
&lt;pre id=&quot;code_1764826065510&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LSP 원칙 위반
open class Bird {
    open fun fly() {
        println(&quot;Bird can fly&quot;)
    }
}

class Sparrow: Bird() {
    override fun fly() {
        println(&quot;Sparrow can fly&quot;)
    }
}

class Penguin: Bird() {
    override fun fly() {
        throw UnsupportedOperationException(&quot;Penguin can't fly&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;코틀린 언어 예시를 살펴보겠습니다. 참새 하위 클래스는 Bird 상위 클래스가 제공하는 비행 기능을 올바르게 수행하지만, 펭귄 하위 클래스는 해당 기능을 수행하지 못하고 예외를 던지고 있습니다. &lt;b&gt;일부 하위 클래스가 상위 클래스의 기능을 동일하게 수행하지 못하고, 동작을 변경하거나 제한하기 때문에 LSP 원칙을 위배&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;510&quot; data-origin-height=&quot;397&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nOpIW/dJMcafSDsD3/xtlIXylvuRFQh3ZvqlKOG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nOpIW/dJMcafSDsD3/xtlIXylvuRFQh3ZvqlKOG0/img.png&quot; data-alt=&quot;LSP 원칙을 위반한 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nOpIW/dJMcafSDsD3/xtlIXylvuRFQh3ZvqlKOG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnOpIW%2FdJMcafSDsD3%2FxtlIXylvuRFQh3ZvqlKOG0%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;337&quot; height=&quot;262&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;397&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;LSP 원칙을 위반한 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764826420197&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LSP 원칙 준수
open class Bird

open class FlyingBird: Bird() {
    open fun fly() {
        println(&quot;I can fly&quot;)
    }
}

class Penguin: Bird() {
    fun swim() {
        println(&quot;Penguin can swim&quot;)
    }
}

class Sparrow: FlyingBird() {
    override fun fly() {
        println(&quot;Sparrow can fly&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;LSP 위반을 해결하기 위해 &lt;b&gt;상위 클래스&lt;/b&gt;를&lt;b&gt; 새, 날 수 있는 새 2가지로 분리&lt;/b&gt;하였습니다. 이렇게 하면 펭귄이나 타조와 같이 날지 못하는 경우에는 Bird 클래스를 상속받으면 되고, 참새나 비둘기와 같이 날 수 있는 경우는 FlyingBird 클래스를 상속받으면 됩니다. 이제 상위 클래스를 위반하는 하위 클래스가 존재하지 않게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1117&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjqnpT/dJMcaihwf5K/INJRakLKSfrkeZ8WyiVhM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjqnpT/dJMcaihwf5K/INJRakLKSfrkeZ8WyiVhM1/img.png&quot; data-alt=&quot;LSP 원칙을 준수한 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjqnpT/dJMcaihwf5K/INJRakLKSfrkeZ8WyiVhM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjqnpT%2FdJMcaihwf5K%2FINJRakLKSfrkeZ8WyiVhM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;536&quot; height=&quot;258&quot; data-origin-width=&quot;1117&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;LSP 원칙을 준수한 경우&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. 안드로이드 위반/준수 예제&lt;/h2&gt;
&lt;pre id=&quot;code_1765173093538&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LSP 위반
open class BaseViewHolder(view: View): RecyclerView.ViewHolder(view) {
    open fun bind(data: String) { // String 데이터 바인딩 }
}

class ImageViewHolder(view: View): BaseViewHolder(view) {
    override fun bind(data: String) {
        throw UnsupportedOperationException(&quot;ImageViewHolder는 String 데이터를 바인딩 할 수 없습니다.&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;상위 클래스가 제공하는 기능을 수행하지 못하고 예외를 발생시키는 하위 클래스&lt;/b&gt;는 LSP 원칙 을 위반합니다. 이를 어떻게 해결할 수 있을까요?&lt;/p&gt;
&lt;pre id=&quot;code_1765173311031&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LSP 준수
open class BaseViewHolder(view: View): RecyclerView.ViewHolder(view) {
    open fun bind(data: T) { // 모든 데이터 바인딩 }
}

class ImageViewHolder(view: View): BaseViewHolder(view) {
    override fun bind(data: Image) {
        showUI(data) // 이미지 데이터 바인딩
    }
}

class TextViewHolder(view: View): BaseViewHolder(view) {
    override fun bind(data: String) {
        showUI(data) // 텍스트 데이터 바인딩
    }
}&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;상위 클래스의 데이터 타입을 제네릭 T로 변경&lt;/b&gt;하면 범위를 한정하지 않게 됩니다. 모든 하위 클래스에서 예외없이 상위 클래스의 제공 메소드를 수행할 수 있게 되면서 LSP 원칙을 준수하게 되었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765171915742&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LSP를 준수한 경우
class CustomMyButton: Button {
    override fun setOnClickListener(l: OnClickListener?) {
        super.setOnClickListener(l)
        showUI()
    }
}

// LSP를 위반한 경우 1
class DisabledOneButton: Button {
    override fun setOnClickListener(l: OnClickListener?) {
        super.setOnClickListener(null)
    }
}

// LSP를 위반한 경우 2
class DisabledTwoButton: Button {
    override fun setOnClickListener(l: OnClickListener?) {
        // super.setOnClickListener(l)
    }
}&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;하위 클래스인 Button은 &lt;b&gt;상위 클래스인 View가 정의한 기본적인 동작을 예상대로 수행&lt;/b&gt;해야 합니다. 그렇게 해야 클라이언트는 View 객체를 다룰 때, 그것이 Button이든 ImageView든 관계없이 View의 공통 동작을 믿고 로직을 작성할 수 있습니다. 하지만, 위의 예시에서는 오버라이드 메소드에서 super.on 부모 클래스의 구현을 호출하지 않게 되면서 LSP 원칙을 위반합니다. &lt;b&gt;메소드 내의super.on 호출&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;4. LSP와 클린 아키텍처의 연관성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LSP 원칙에서 강조하는 상위 클래스와 하위 클래스의 관계를 보면, &lt;b&gt;추상체&lt;/b&gt;와 &lt;b&gt;구현체&lt;/b&gt;를 떠올릴 수 있는데요. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;클린 아키텍처와 무슨 연관성&lt;/b&gt;이 있을까요?&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 52px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 24.4186%; text-align: center;&quot;&gt;&lt;b&gt;추상체 레이어&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.7675%; text-align: center;&quot;&gt;&lt;b&gt;추상체&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 26.6279%; text-align: center;&quot;&gt;&lt;b&gt;구현체 레이어&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.9535%; text-align: center;&quot;&gt;&lt;b&gt;구현체&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 14px;&quot;&gt;
&lt;td style=&quot;width: 24.4186%; height: 14px; text-align: center;&quot;&gt;Domain&lt;/td&gt;
&lt;td style=&quot;width: 24.7675%; height: 14px; text-align: center;&quot;&gt;UseCase Interface&lt;/td&gt;
&lt;td style=&quot;width: 26.6279%; height: 14px; text-align: center;&quot;&gt;Domain&lt;/td&gt;
&lt;td style=&quot;width: 23.9535%; height: 14px; text-align: center;&quot;&gt;UseCase Impl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 24.4186%; height: 17px; text-align: center;&quot;&gt;Domain&lt;/td&gt;
&lt;td style=&quot;width: 24.7675%; height: 17px; text-align: center;&quot;&gt;Repository Interface&lt;/td&gt;
&lt;td style=&quot;width: 26.6279%; height: 17px; text-align: center;&quot;&gt;Data&lt;/td&gt;
&lt;td style=&quot;width: 23.9535%; height: 17px; text-align: center;&quot;&gt;Repository Impl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.4186%; text-align: center;&quot;&gt;Data&lt;/td&gt;
&lt;td style=&quot;width: 24.7675%; text-align: center;&quot;&gt;DataSource Interface&lt;/td&gt;
&lt;td style=&quot;width: 26.6279%; text-align: center;&quot;&gt;Remote, Local&lt;/td&gt;
&lt;td style=&quot;width: 23.9535%; text-align: center;&quot;&gt;DataSoure Impl&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;UseCase, Repository, DataSource&lt;/b&gt; 3개의 요소에 대해 추상체와 구현체가 존재합니다. 모든 구현체(서브 클래스)는 추상체(상위 클래스)가 제공하는 기능을 일관되게 준수해야 &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;h2 data-ke-size=&quot;size26&quot;&gt;5. OCP와 LSP의 차이와 관계성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에도 인터페이스, 추상메소드를 언급하여 아마 저번 시간에 다룬 개방 폐쇄 원칙과 헷갈릴 수 있습니다. 인터페이스, 추상 클래스를 여전히 다루고 있지만 &lt;b&gt;개방 폐쇄원칙&lt;/b&gt;과 &lt;b&gt;리스코프 치환 원칙&lt;/b&gt;은 이를 &lt;b&gt;서로 다른 관점&lt;/b&gt;에서 보고 있습니다.&lt;/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;691&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DHCS4/dJMcagxftsG/RP0mWfiPB9rD62F7Hiitn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DHCS4/dJMcagxftsG/RP0mWfiPB9rD62F7Hiitn0/img.png&quot; data-alt=&quot;LSP&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DHCS4/dJMcagxftsG/RP0mWfiPB9rD62F7Hiitn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDHCS4%2FdJMcagxftsG%2FRP0mWfiPB9rD62F7Hiitn0%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;402&quot; height=&quot;340&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;691&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;LSP&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;먼저, 리스코프 치환 원칙(LSP)는 &lt;b&gt;추상체(상위 클래스)를 위반하는 구현체(하위 클래스)는 없어야 한다&lt;/b&gt;는 점에서 추상체, 구현체의 &lt;b&gt;치환 가능한 관계&lt;/b&gt;에 집중합니다. 예시로, 클린 아키텍처에서 추상체는 DataSource 인터페이스, 구현체는 RemoteDataSource, LocalDataSoure가 있습니다. 기반 타입인 DataSource 인터페이스에서 유저를 가져오는 메소드 getUser()를 제공한다면, 서브 타입인 Remote와 Local은 &lt;b&gt;해당 메소드를 위반하지 않고 동일한 방식으로 동작&lt;/b&gt;하도록 구현해야합니다. 그렇게해야 클라이언트는 &lt;b&gt;데이터 소스가 변경되더라도&lt;/b&gt; 로컬 DB에서 가져오든 서버에서 가져오든 &lt;b&gt;결과값이 같다고 기대&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;651&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmoIKM/dJMcaaX7ahK/GAFE24lU6jL09bZ1oRq0c1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmoIKM/dJMcaaX7ahK/GAFE24lU6jL09bZ1oRq0c1/img.png&quot; data-alt=&quot;OCP&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmoIKM/dJMcaaX7ahK/GAFE24lU6jL09bZ1oRq0c1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmoIKM%2FdJMcaaX7ahK%2FGAFE24lU6jL09bZ1oRq0c1%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;346&quot; height=&quot;343&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;646&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OCP&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;반면, 개방폐쇄 원칙(OCP)은 &lt;b&gt;추상체를 이용해서 기존 코드의 수정을 막고 확장할 수 있다&lt;/b&gt;는 점에 의미를 두며, &lt;b&gt;구현체에 대해 신경쓰지 않습니다&lt;/b&gt;.&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 예를 들어, &lt;/span&gt;Repository는 DataSource 인터페이스의 메서드만 호출할 뿐 어떤 구체적인 데이터 소스를 사용하는지 모릅니다.&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;b&gt;LSP는 OCP를 달성하기 위한 필수 조건&lt;/b&gt;입니다. Repository가 데이터 소스의 변경과 상관없이 닫혀 있으려면(OCP), RemoteDataSource에서 LocalDataSource로 문제없이 치환되어야 합니다(LSP). 만약 치환된 서브타입이 기반 타입인 DataSource 인터페이스의 메소드 기능을 위반한다면(LSP), Repository 또한 관심사 분리를 하지 못하고 변경되어야 합니다(OCP).&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;</description>
      <category>Clean Architecture</category>
      <category>Android 예제</category>
      <category>datasource</category>
      <category>impl</category>
      <category>Interface</category>
      <category>Kotlin 예제</category>
      <category>lsp</category>
      <category>repository</category>
      <category>solid</category>
      <category>Usecase</category>
      <category>리스코프 치환 원칙</category>
      <author>청둥이 </author>
      <guid isPermaLink="true">https://soung-appdeveloper.tistory.com/173</guid>
      <comments>https://soung-appdeveloper.tistory.com/173#entry173comment</comments>
      <pubDate>Sun, 14 Dec 2025 14:10:59 +0900</pubDate>
    </item>
    <item>
      <title>SOLID - OCP (개방 폐쇄 원칙)</title>
      <link>https://soung-appdeveloper.tistory.com/172</link>
      <description>&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;niniz&quot; data-emoticon-name=&quot;002&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/002.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/002.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요~ 오랜만이네요. 오늘은 SOLID 원칙 중 두 번째인, &lt;b&gt;OCP 원칙&lt;/b&gt;에 대해 알아보겠습니다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. OCP 원칙이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OCP 원칙은 Open-Closed Principle 의 약어로, 개방 폐쇄 원칙을 의미합니다. 여기서 개방과 폐쇄는 무엇을 가리킬까요? 기능 &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;h2 data-ke-size=&quot;size26&quot;&gt;2. OCP 원칙 위반/준수 예제 (코틀린)&lt;/h2&gt;
&lt;pre id=&quot;code_1764226445840&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// OCP 위반
class Notifier {
    fun send(type: String, message: String) {
        when(type) {
            &quot;Email&quot; -&amp;gt; sendEmail(message)
            &quot;SMS&quot; -&amp;gt; sendSms(message)
            &quot;Slack&quot; -&amp;gt; sendSlack(message)
            // 새로운 알림 수단 추가
        }
    }
}&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;위의 예제에서는, 새로운 알림 수단 추가 시 send 메소드 내에&lt;b&gt; 변경&lt;/b&gt;이 일어납니다. 이는, &lt;b&gt;새로운 기능 추가 시&lt;/b&gt; &lt;b&gt;기존 코드를 변경&lt;/b&gt;하기에 OCP 원칙을 &lt;b&gt;위반&lt;/b&gt;합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764310110948&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// OCP 준수
interface Notification {
    fun send()
}

class EmailNotifier: Notification {
    override fun send() { // 이메일 알림 로직 }
}

class SnsNotifier: Notification {
    override fun send() { // SNS 알림 로직 }
}

class Notifier {
    fun send(notification: Notification) {
        notification.send()
    }
}&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;OCP 원칙을 준수하기 위해 알림 로직을 &lt;b&gt;인터페이스로 추상화&lt;/b&gt;했습니다. 또한, 각각의 알림 기능을 클래스로 분리하여 &lt;b&gt;인터페이스를 상속받고 추상체를 구현&lt;/b&gt;하여 구체적인 알림 로직을 작성합니다.&amp;nbsp;이후 Notifier 클래스에서 send 메소드를 작성할 때 &lt;b&gt;인자로 인터페이스&lt;/b&gt;를 받고, &lt;b&gt;메소드를 호출&lt;/b&gt;하는 식으로 &lt;b&gt;코드의 변경을 최소화&lt;/b&gt;할 수 있습니다. 이렇게 작성하게 되면, 슬랙과 같은 새로운 알림 기능이 생길때 Notifier 클래스의 send 메소드를 수정하지 않고, SlackNotifier 라는 새로운 클래스를 만듦으로써 기능을 확장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. OCP 원칙 위반/준수 예제 (안드로이드)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린 예제를 보았으니 &lt;b&gt;안드로이드&lt;/b&gt;에서는 어떻게 적용하는지 이해해봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1764227386637&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// OCP 위반
class ItemAdapter: RecyclerView.Adapter&amp;lt;RecyclerView.ViewHolder&amp;gt;() {
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when(holder) {
            is UserViewHolder -&amp;gt; holder.bind(userData[posiiton])
            is ChatBotViewHolder -&amp;gt; holder.bind(chatbotData[position])
            // 새로운 뷰홀더 추가
        }
    }
}&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;RecyclerView 의 어댑터를 예제로 가져왔습니다. 여기서 새로운 뷰홀더가 추가가 되면 when(holder) 내에서 새로운 코드를 추가하면서 기존 onBindViewHolder 메소드에 코드 &lt;b&gt;수정&lt;/b&gt;이 일어납니다. 이는 &lt;b&gt;OCP 원칙을 위반&lt;/b&gt;한다고 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764310232971&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// OCP 준수
abstract class BaseViewHolder&amp;lt;T&amp;gt;(itemView: View): RecyclerView.ViewHolder(itemView) {
    abstract fun bind(data: T)
}

class UserViewHolder(itemView: View): BaseViewHolder&amp;lt;User&amp;gt;(itemView) {
    override fun bind(data: String) { // 유저 데이터 바인딩 }
}

class ChatbotViewHolder(itemView: View): BaseViewHolder&amp;lt;ChatBot&amp;gt;(itemView) {
    override fun bind(data: String) { // 챗봇 데이터 바인딩 }
}

class ItemAdapter&amp;lt;T&amp;gt;(private val items: List&amp;lt;T&amp;gt;): RecyclerView.Adapter&amp;lt;BaseViewHolder&amp;lt;T&amp;gt;&amp;gt;() {
    override fun onBindViewHolder(holder: BaseViewHolder&amp;lt;T&amp;gt;, position: Int) {
        holder.bind(items[position])
    }
}&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;OCP 원칙을 준수하기 위해 인터페이스와 유사한 역할을 하는 BaseViewHolder 라는 &lt;b&gt;추상 메소드&lt;/b&gt;를 선언했습니다. 데이터 타입을 &lt;b&gt;제네릭 T&lt;/b&gt;로 두어, 어떤 데이터 타입이 들어와도 처리가 가능하게 했습니다. 다른 뷰홀더는 해당 &lt;b&gt;추상 메소드를 상속받아 구현&lt;/b&gt;하게 됩니다. 이렇게 설계하면 onBindViewHolder 내부에서는 매개변수로 holder 타입을 abstract class 로 받을 수 있으며, &lt;b&gt;내부 코드 수정 없이 bind 메소드만 호출&lt;/b&gt;하면 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 클린 아키텍처, 도메인 레이어의 유즈 케이스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;앞의 예제를 보면 OCP 원칙을 준수하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;인터페이스(interface)&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;추상 클래스(abstract class)&lt;/b&gt;가 쓰인 것을 볼 수 있습니다. 인터페이스와 추상 클래스를 클린 아키텍처와 연결지어 생각해보면&amp;nbsp;&lt;b&gt;&lt;u&gt;변경되지 않는 코어한 핵심 레이어,&lt;/u&gt; 도메인 레이어의 유즈케이스&lt;/b&gt;를 떠올릴 수 있을 것입니다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;새로운 기능 추가 시 기존 코드의 수정을 &quot;최소화&quot;하는 의미에서, 도메인 레이어의 유즈케이스(Use Case)는 OCP 원칙과 아주 잘 맞아떨어집니다. 유즈케이스를 OCP 원칙을 잘 준수하여 설계하면 &lt;b&gt;기존 코드의 변경 없이도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;새로운 요구사항에 대응&lt;/b&gt;할 수 있습니다. 반대로, 도메인 레이어의 유즈 케이스를 설계하지 않거나 OCP 원칙을 지키지 않는다면, 새로운 기능을 추가할 때 매번 기존의 코드를 연쇄적으로 수정해야하는 &lt;b&gt;불필요한 비용이 발생&lt;/b&gt;하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마무리 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;실제 서비스 수준의 프로젝트 규모를 생각한다면, 새로운 기능을 추가할 때 기존의 코드의 수정을 최소화하고, 유연하게 확장하는 방식으로 설계하는 것은 유지보수 비용 측면에서 정말 중요합니다. 이를 위해서는 &lt;b&gt;인터페이스&lt;/b&gt;와 &lt;b&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;다음 시간에는 SOLID 의 세 번째 원칙, 리스코프 치환 원칙에 대해 알아보겠습니다! 12월도 화이팅하시길 바래요!&lt;/p&gt;</description>
      <category>Clean Architecture</category>
      <category>abstract class</category>
      <category>Interface</category>
      <category>OCP</category>
      <category>개방 폐쇄 원칙</category>
      <category>도메인</category>
      <category>안드로이드 예제</category>
      <category>유즈케이스</category>
      <category>추상체</category>
      <category>추상체 구현</category>
      <category>코틀린 예제</category>
      <author>청둥이 </author>
      <guid isPermaLink="true">https://soung-appdeveloper.tistory.com/172</guid>
      <comments>https://soung-appdeveloper.tistory.com/172#entry172comment</comments>
      <pubDate>Sun, 30 Nov 2025 12:41:53 +0900</pubDate>
    </item>
    <item>
      <title>SOLID - SRP (단일 책임 원칙)</title>
      <link>https://soung-appdeveloper.tistory.com/170</link>
      <description>&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;001&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/001.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/001.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;오늘은 &lt;/span&gt;&lt;b&gt;SOLID 원칙&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;이라는 따끈따끈한(?) 새로운 주제를 가져왔습니다! 한번 알아볼까요?&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. SOLID 원칙이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;취업 준비할 때 면접 단계에서 자주 나오는 단골 질문&lt;/u&gt; 중에 하나죠. 저도 관련 &lt;u&gt;인턴 공고 조건&lt;/u&gt;에서도 &lt;b&gt;SOLID 원칙에 대한 이해&lt;/b&gt;를 자격 요건으로 두는 경우를 가끔씩 보았던 것 같습니다. SOLID 원칙은 &lt;b&gt;객체 지향 프로그래밍에서 견고하고 유지보수 가능한 프로젝트를 설계하기 위한 원칙&lt;/b&gt;입니다. &lt;b&gt;SRP, OCP, LSP, ISP, DIP&lt;/b&gt; 이렇게 5가지 원칙의 앞글자를 따서 만들었습니다. 클린 아키텍처의 기반이 되며, 모듈화 계층화를 통해 코드의 유연성과 확장성을 높일 수 있습니다.&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;2. SRP 란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOLID의 첫 번째 원칙인 &lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;단일 책임 원칙(&lt;/span&gt;Single Responsibility Principle)&lt;/b&gt;입니다. 말 그대로 &lt;b&gt;여러 개의 책임이 아니라 단일 책임만을 갖는 것&lt;/b&gt;인데요. 안드로이드에서 SRP는 클래스가 하나의 책임만 가지며, 오직 하나의 이유로만 변경되는 것을 의미합니다. 이를 통해 복잡성을 줄일 수 있고 여러 책임을 가질 때 발생할 수 있는 변경의 부담을 줄일 수 있습니다. 클래스를 단일 책임으로 제한함으로써, 코드의 가독성과 유지보수성 또한 향상시킬 수 있습니다. 한 가지 목적에 집중된 클래스는 이해하기 쉽고, 변경이 필요할 때 변경의 영향을 최소화할 수 있습니다. SRP 원칙을 준수하지 않아서 안드로이드 앱의 구성요소들이 점점 무거워지면, 화면 전환 시 속도가 느려지거나 메모리 누수 및 뷰 상태 저장 문제 등이 발생할 수 있습니다.&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;3. 예제 코드 (코틀린)&lt;/h3&gt;
&lt;pre id=&quot;code_1759674088630&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CinemaManager {
    fun inspectTheater(category: String) {
        // 전반적인 영화관 점검
    }
    fun assistCustomer(user: User) {
        // 손님 응대
    }
    fun prepareFood(food: String) {
        // 팝콘, 핫도그 등 음식 제조 및 준비
    }
    fun cleanFacility(place: 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;예제 코드를 보겠습니다. 영화관 매니저 클래스가 여러 책임을 갖는 상황입니다. 매니저가 슈퍼맨이 아니라면 혼자서 이렇게 책임을 많이 갖는건 여러 측면에서 좋지 않을 것 같네요. 단일 책임 원칙(SRP)을 위반한 상태입니다. 이런 경우에 어떻게 해결하는게 좋을까요?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1759674261900&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CinemaManager {
    fun inspectTheater(category: String) {
        // 전반적인 영화관 점검
    }
}

class CinemaStaffOne {
    fun prepareFood(food: String) {
        // 팝콘, 핫도그 등 음식 제조 및 준비
    }
}

class CinemaStaffTwo {
    fun assistCustomer(user: User) {
        // 손님 응대
    }
}

class CinemaStaffThree {
    fun cleanFacility(place: 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;과중한 업무를 맡은 영화관 매니저가 3명의 알바생을 뽑고 각각에게 역할을 하나씩 부여함으로써 문제를 해결할 수 있었습니다. 단일 책임 원칙(SRP)을 준수하게 되었습니다. 실제 안드로이드에 적용한 예시도 살펴보겠습니다.&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;4. 예제 코드 (안드로이드)&lt;/h3&gt;
&lt;pre id=&quot;code_1759676385501&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainActivity: AppCompatActivity() {
    override fun onCreate() {
        super.onCreate()
        fetchData()
    }
    
    fun fetchData() {
        // 1. 네트워크 요청
        val data = apiService.getData()
        // 2. 데이터 처리(가공)
        val processedData = processData(data)
        // 3. UI 업데이트
        updateUI(processedData)
    }
}&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;데이터를 가져와서 UI 에 바인딩하는 예시가 있습니다. 이때 네트워크 요청, 데이터 가공처리, UI 바인딩 세가지가 필요합니다. 현재 코드에서는 &lt;u&gt;해당 3가지 역할을 UI 레이어에서 전부 처리하는 상황&lt;/u&gt;입니다. 한가지 역할이 아닌 여러 개의 책임을 갖기에 &lt;b&gt;SRP를 위반&lt;/b&gt;한 상황이죠. 여기서 &lt;b&gt;3개의 기능 중 단 한개라도 변경되면 fetchData 메소드가 항상 변경되어야 합니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 영화관 예시에서 음식 조리에 이상이 있거나, 청소가 제대로 안되어 있거나, 손님이 매표소에서 발권이 어려운 경우 항상 매니저를 불러야하는 상황으로 비유할 수 있습니다. 이러한 문제를 해결하기 위해 한번 &lt;b&gt;책임을 분리&lt;/b&gt;시켜봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1759676962697&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 네트워크 요청
class DataRepository(private val apiService: ApiService) {
    fun fetchData(): Model {
        return apiService.getData()
    }
}

// 데이터 처리 및 관리
class MainViewModel(private val dataRepository: DataRepository): ViewModel() {
    private val _data = MutableLiveData&amp;lt;Model&amp;gt;()
    val data: LiveData&amp;lt;Model&amp;gt; get() = _data
    
    private fun loadData() {
        _data.value = dataRepository.fetchData()
    }
    
}

// UI 업데이트
class MainActivity: AppCompatActivity() {
    private val viewModel: MainViewmodel by viewModels()
    
    override fun onCreate() {
        super.onCreate()
        viewModel.data.observe(this) { data -&amp;gt;
            updateUI(data)
        }
        viewModel.loadData()
    }
}&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;세 가지의 역할을 단일 책임 원칙(SRP)을 준수하며 분리해보았습니다. 위의 코드를 보면 &lt;b&gt;네트워크 요청은 레포지토리(repository)&lt;/b&gt;에서, &lt;b&gt;데이터 가공 및 관리는 뷰모델(viewModel)&lt;/b&gt;에서,&lt;b&gt; UI 업데이트&lt;/b&gt;는 액티비티,프래그먼트와 같은 &lt;b&gt;안드로이드 컴포넌트&lt;/b&gt;에서 합니다. 이렇게 기존에 UI 에서 부담했던 3가지 역할을 뷰모델, 레포지토리 각각에게 1개씩 책임을 주면서 해결할 수 있었습니다. 영화관으로 비유하자면, 매니저가 알바생을 고용하여 각각의 역할을 부여한 상황입니다.&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;6. 클린 아키텍처와의 연관성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOLID 원칙 중 단일 책임 원칙(SRP)이 &lt;b&gt;클린 아키텍처와 무슨 관련&lt;/b&gt;이 있을까요? 클린 아키텍처에서 &lt;b&gt;데이터, 도메인, 프레젠테이션 같은 각각의 계층&lt;/b&gt;은 &lt;b&gt;자신의 책임&lt;/b&gt;만을 가져야 하며, 이를 통해 모듈 간 결합도를 낮추고 유지보수를 용이하게 할 수 있습니다. 예를 들어, 도메인 레이어는 비즈니스 로직만 담당하고, 데이터 레이어는 데이터 처리를 담당하며, 프레젠테이션 레이어는 UI 와 사용자 상호작용을 담당합니다. 각 계층을 SOLID 원칙의 단일 책임 원칙(SRP)에 맞게 분리함으로써 &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;SOLID 원칙 중 단일 책임 원칙&lt;/b&gt;에 대해 알아보았습니다! 다음 시간에는 두번째 원칙인 개방 폐쇄 원칙(OCP)에 대해 알아보겠습니다! 즐거운 추석 되세요  &lt;/p&gt;</description>
      <category>Clean Architecture</category>
      <category>solid</category>
      <category>SRP</category>
      <category>단일 책임 원칙</category>
      <category>클린 아키텍처</category>
      <author>청둥이 </author>
      <guid isPermaLink="true">https://soung-appdeveloper.tistory.com/170</guid>
      <comments>https://soung-appdeveloper.tistory.com/170#entry170comment</comments>
      <pubDate>Sun, 5 Oct 2025 23:07:20 +0900</pubDate>
    </item>
    <item>
      <title>사용자 수면 시간 추적하기</title>
      <link>https://soung-appdeveloper.tistory.com/168</link>
      <description>&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;niniz&quot; data-emoticon-name=&quot;002&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/002.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/002.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&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;h2 data-ke-size=&quot;size26&quot;&gt;1. 사용자가 잠든 시각 추적하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 정보로부터 사용자가 잠든 시간을 추적해 볼 수 있습니다. 하나씩 살펴보겠습니다!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-1. 매니저 받아오기/사용 권한 받아오기&lt;/h4&gt;
&lt;pre id=&quot;code_1756711249375&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val usageStatsManager = getSystemService(USAGE_STATS_SERVICE) as UsageStatsManager&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;안드로이드에서 getSystemService() 호출할 때, 어떤 종류의 시스템 서비스 객체를 얻고 싶은지 &lt;b&gt;문자열 상수&lt;/b&gt;로 지정해줘야 합니다. USAGE_STATS_SERVICE 상수를 통해 앱 사용 통계 관련 서비스를 요청합니다. 이때, UsageStatsManager는 안드로이드에서 &lt;b&gt;앱 사용 기록, 화면 사용 시간 등 통계 데이터를 제공하는 클래스&lt;/b&gt;입니다. 특정 기간 동안 어떤 앱이 얼마나 사용됐는지 조회하거나, &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;앱 사용 빈도나 최근 사용 시간을 확인하여 &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;앱 사용 패턴을 분석할 수 있게 됩니다.&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 비교적 특별한 권한을 앱이 가지려면 사용자로부터 그 권한을 요청하여 받아와야합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1756712527315&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;uses-permission
        android:name=&quot;android.permission.PACKAGE_USAGE_STATS&quot;
        tools:ignore=&quot;ProtectedPermissions&quot; /&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;먼저&lt;b&gt; manifest 에 요청할 권한을 등록&lt;/b&gt;해야 합니다. 다른 권한 요청과 달리 tools:ignore 이 붙기에 일반 접근 권한이 아닌 &lt;b&gt;특별 접근&lt;/b&gt; &lt;b&gt;권한&lt;/b&gt;임을 알 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1756712374748&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (!checkUsageAccessPermission(context)) {
    val dialog = AlertDialog.Builder(context).apply {
        setTitle(&quot;권한 요청 다이얼로그&quot;)
        setMessage(&quot;사용자의 수면시간 정보를 제공하기 위해선 사용 정보 접근 권한이 필요합니다. 설정에 들어가서 권한을 허용해주세요.&quot;)
        setPositiveButton(&quot;설정 이동&quot;) { _, _ -&amp;gt;
            val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
            startActivity(intent)
        }
        setCancelable(false)
        create()
    }
    dialog.show()
}

// 앱 사용 시간 권한 활성 여부를 확인하는 함수
private fun checkUsageAccessPermission(context: Context): Boolean {
    val appOpsManager = context.getSystemService(APP_OPS_SERVICE) as AppOpsManager
    val mode = if (Build.VERSION.SDK_INT &amp;gt;= Build.VERSION_CODES.Q) {
        // Android 10 이상
        appOpsManager.unsafeCheckOpNoThrow(
            AppOpsManager.OPSTR_GET_USAGE_STATS,
            android.os.Process.myUid(),
            context.packageName
        )
    } else {
        // Android 10 미만
        appOpsManager.checkOpNoThrow(
            AppOpsManager.OPSTR_GET_USAGE_STATS,
            android.os.Process.myUid(),
            context.packageName
        )
    }
    return mode == AppOpsManager.MODE_ALLOWED
}&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;먼저 권한이 있는지 확인합니다. 이때 if else 문을 통해 &lt;b&gt;안드로이드 버전 호환성&lt;/b&gt;을 관리합니다. 안드로이드 10 이상에서 사용되는 unsafeCheckOpNoThrow 메소드가 안드로이드 10 미만보다 좀 더 &lt;b&gt;보안성&lt;/b&gt;이 좋다고 합니다. 확인하고 싶은 접근 권한, 앱 고유 UID, 앱 패키지 이름을 인자로 지정하면 관련 상태(모드)를 알아낼 수 있는데, MODE_ALLOWED 와 비교해서 권한이 있는 지 불리언 값을 반환합니다.&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;다이얼로그 UI&lt;/b&gt;를 통해 사용자가 직접 설정에서 권한을 킬 수 있도록 유도합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-2. 잠든 시간대 지정하기&lt;/h4&gt;
&lt;pre id=&quot;code_1756711286077&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val sleepCalendar = Calendar.getInstance().apply {
    // 오늘 날짜
    set(Calendar.HOUR_OF_DAY, 4)
    set(Calendar.MINUTE, 0)
    set(Calendar.SECOND, 0)
    set(Calendar.MILLISECOND, 0)
}
val sleepTrackingEndTime = sleepCalendar.timeInMillis // 04:00
sleepCalendar.add(Calendar.HOUR_OF_DAY, -8) // 어제 날짜
val sleepTrackingStartTime = sleepCalendar.timeInMillis // 20:00&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 권한을 통해 받아온&lt;b&gt; 앱 사용 통계 데이터를 필요한 만큼 추출&lt;/b&gt;해야 합니다. 잠든 시간을 추적하려면 사용자가 어떤 시간대에 잠이 들었는지 기간 정보가 필요합니다. 저는 &lt;u&gt;오후 8시부터 새벽 4시 사이에는 잠들 것이라 가정&lt;/u&gt;했고, 시간 변수를 millisecond 단위로 얻기 위해 Calendar 객체를 사용했습니다. 이 방법의 단점은 오후 8시 이전에 잠이 들거나 새벽 4시 이후에 잠이 드는 경우는 잠든 시간을 제대로 측정할 수 없습니다  (&lt;s&gt;이러한 단점을 극복할 수 있는 더 나은 방법이 있다면 댓글로 적어주세요&lt;/s&gt;)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-3. 이벤트 받아오기(SCREEN_NON_INTERACTIVE)&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;val sleepEvent = UsageEvents.Event() 

val sleepUsageEvents = usageStatsManager.queryEvents( 
    sleepTrackingStartTime,
    sleepTrackingEndTime
) 

while (sleepUsageEvents.hasNextEvent()) {
    sleepUsageEvents.getNextEvent(sleepEvent) // 다음 이벤트를 변수에 저장
    if(sleepEvent.eventType == UsageEvents.Event.SCREEN_NON_INTERACTIVE) {
        lastUsageTimeBeforeSleep = sleepEvent.timeStamp 
    } else if (sleepEvent.eventType == UsageEvents.Event.ACTIVITY_STOPPED &amp;amp;&amp;amp; sleepEvent.packageName != &quot;com.sec.android.app.launcher&quot;) { 
        lastUsedApp = sleepEvent.packageName
    }
}

Log.e(&quot;NIGHT&quot;, &quot;마지막 사용 앱: $lastUsedApp, 화면 끈 시각: ${Date(lastUsageTimeBeforeSleep)}&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;&lt;u&gt;어제 오후 8시부터 오늘 새벽 4시 사이에 일어난 앱 사용 통계 정보를 받아올 수 있습니다&lt;/u&gt;&lt;/b&gt;. 여기서, 발생한 정보 단위를 '&lt;b&gt;이벤트&lt;/b&gt;' 라고 부릅니다. 오후 8시부터 발생한 이벤트를 이제 하나씩 꺼내오는데, UsageEvents.Event 객체에 이벤트를 담습니다. getNextEvent() 메소드로 이벤트를 꺼낼 때마다 이 객체에 계속해서 덮어씌웁니다. sleepEvent 객체가 가진 이벤트 정보가 만약 &lt;b&gt;SCREEN_NON_INTERACTIVE&lt;/b&gt; 라면 사용자가 잠들었을 가능성이 있는 시점으로 보게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #080808; text-align: start;&quot;&gt;이때, &lt;b&gt;SCREEN_NON_INTERACTIVE&lt;/b&gt;&lt;span&gt; 는&lt;b&gt; 화면이 꺼진 시점에 대한 로그&lt;/b&gt;를 나타냅니다. 이후&lt;/span&gt;&lt;/span&gt; lastUsageTimeBeforeSleep 변수에다가 해당 시점을 timeStamp 로 기록 및 업데이트합니다. while 문을 통해 남아있는 이벤트가 없을 때까지 확인하기 때문에, 해당 변수에는 가장 마지막으로 핸드폰을 사용하고 화면을 끈 시점이 남아있을 것입니다.&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;span style=&quot;background-color: #ffffff; color: #080808; text-align: start;&quot;&gt;SCREEN_NON_INTERACTIVE&lt;span&gt; 말고도 &lt;b&gt;ACTIVITY_STOPPED&lt;/b&gt; 라는 이벤트를 통해 &lt;b&gt;언제 앱을 껐는지&lt;/b&gt; 로그를 알아낼 수 있습니다. 이벤트 유형이 ACTIVITY_STOPPED 이고, 해당 앱이 기본 화면인 런처 앱(com.sec.android.app.launcher)이 아니라는 조건하에서 마지막으로 사용한 앱이 무엇인지도 알 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 사용자가 일어난 시각 추적하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 진행하고 있는 프로젝트 같은 경우에는 &lt;b&gt;사용자가 일어난 시각에 대한 기준&lt;/b&gt;을 다음과 같은&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt; 플로우&lt;/b&gt;로 설정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mQVCw/btsQfAjkkUk/HuxnvN2Ir8RBVkvGarEAg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mQVCw/btsQfAjkkUk/HuxnvN2Ir8RBVkvGarEAg1/img.png&quot; data-alt=&quot;사용자가 일어난 시점 기준&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mQVCw/btsQfAjkkUk/HuxnvN2Ir8RBVkvGarEAg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmQVCw%2FbtsQfAjkkUk%2FHuxnvN2Ir8RBVkvGarEAg1%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;593&quot; height=&quot;373&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;502&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;u&gt;사용자가 일어난 시각을 핸드폰을 처음 켠 시각으로 볼 수도 있고, 기상 알람 미션을 끝낸 시각으로 볼 수도 있기 때문&lt;/u&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2-1. 기상 시간대 지정하기&lt;/h4&gt;
&lt;pre id=&quot;code_1756716853618&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val wakeUpCalendar = Calendar.getInstance().apply {
    set(Calendar.HOUR_OF_DAY, 5)
    set(Calendar.MINUTE, 0)
    set(Calendar.SECOND, 0)
    set(Calendar.MILLISECOND, 0)
}
val wakeTrackingStartTime = wakeUpCalendar.timeInMillis // 05:00
wakeUpCalendar.add(Calendar.HOUR_OF_DAY, 5)
val wakeTrackingEndTime = wakeUpCalendar.timeInMillis // 10:00&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;UsageStats 매니저를 받아오는 과정은 이미 진행했기에 또 다시 하지 않아도 됩니다. 먼저, 사용자가 &lt;b&gt;기상했을 것 같은 시간대&lt;/b&gt;를 지정합니다. 저는 &lt;u&gt;오전 5시에서 오전 10시&lt;/u&gt;로 잡았고, 구체적인 시간 계산을 위해 Calendar 객체를 이용했습니다. 아까와 마찬가지로, 오전 5시 전에 일어나거나 오전 10시 이후에 일어나는 경우는 &lt;u&gt;정확한 측정이 어렵다&lt;/u&gt;는 단점이 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2-2. 이벤트 받아오기(SCREEN_INTERACTIVE)&lt;/h4&gt;
&lt;pre id=&quot;code_1756716924271&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private var firstUsageTimeAfterWake: Long? = null
private var firstUsedApp: String? = null

val wakeEvent = UsageEvents.Event()
val wakeUsageEvents = usageStatsManager.queryEvents(
    wakeTrackingStartTime,
    wakeTrackingEndTime
) 

while (wakeUsageEvents.hasNextEvent()) {
    wakeUsageEvents.getNextEvent(wakeEvent)
    if (wakeEvent.eventType == UsageEvents.Event.SCREEN_INTERACTIVE) {
        if (firstUsageTimeAfterWake == null) {
            firstUsageTimeAfterWake = wakeEvent.timeStamp  // 기상 후 처음 핸드폰을 킨 시간 추적
            Log.e(&quot;MORNING&quot;, &quot;SCREEN_INTERACTIVE: ${Date(firstUsageTimeAfterWake ?: 0L)}&quot;)
        }
    } else if (wakeEvent.eventType == UsageEvents.Event.ACTIVITY_RESUMED &amp;amp;&amp;amp; wakeEvent.packageName != &quot;com.sec.android.app.launcher&quot;) {
        if (firstUsedApp == null) {
            firstUsedApp = wakeEvent.packageName // 기상 후 처음 사용한 앱 추적
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠든 시간을 추적할 때는 관련 이벤트 중 마지막 시점을 확인한 것과 달리, 기상 시간은 &lt;b&gt;가장 첫 번째 이벤트를 파악&lt;/b&gt;하는 것이 중요합니다. 먼저, queryEvents 메소드를 통해 &lt;u&gt;금일 오전 5시부터 오전 10시 사이의 이벤트&lt;/u&gt;를 wakeUsageEvents 변수에 저장합니다. 이후 저장된 이벤트를 하나씩 꺼내오면서 wakeEvent에 담습니다. 해당 변수가 가진 이벤트가 사용자가 핸드폰 화면을 킬 때 발생하는 &lt;b&gt;SCREEN_INTERACTIVE&lt;/b&gt; 이벤트이고, 이전에 저장된 변수 값이 없다면 해당 시간대(timeStamp)를 기상 시간으로 판단합니다. 이후에 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;SCREEN_INTERACTIVE&lt;span&gt; 이벤트가 발생해도 &lt;u&gt;이미 firstUsageTimeAfterWake 변수 값이 null 이 아니기 때문에 값이 덮어씌워지지 않습니다&lt;/u&gt;.&amp;nbsp;&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;b&gt;ACTIVITY_RESUMED&lt;/b&gt; 이고, 핸드폰 기본 화면인 런처 앱이 아니라면 &lt;b&gt;처음 들어간 앱&lt;/b&gt;을 &lt;b&gt;추적&lt;/b&gt;할 수 있습니다. 이제 사용자의 취침 시각과 기상 시각을 아주 정확하진 않지만 시스템에 저장된 로그 기록을 통해 얼추(?) 구할 수 있게 되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. 시연 영상&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screen_Recording_20250901_191133_LifeMaster-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P9NDR/btsQfARgCrx/xuHkKwYaLK7DSKYwHpYdXk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P9NDR/btsQfARgCrx/xuHkKwYaLK7DSKYwHpYdXk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P9NDR/btsQfARgCrx/xuHkKwYaLK7DSKYwHpYdXk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/P9NDR/btsQfARgCrx/xuHkKwYaLK7DSKYwHpYdXk/img.gif&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;291&quot; height=&quot;647&quot; data-filename=&quot;Screen_Recording_20250901_191133_LifeMaster-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1778&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 추가 생각/정리&lt;/h2&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;b&gt;핸드폰 내부 시스템 데이터&lt;/b&gt;' 입니다. &lt;u&gt;내부 시스템에서는 용도에 따라 쓸만한 데이터가 정말 많습니다&lt;/u&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;u&gt;사용자의 수면 시간을 아주 정확하게 구하려면 사용자가 핸드폰과 인터랙션이 끝난 이후에도 측정을 해야하는데요&lt;/u&gt;. 사용자 인근의 소리를 앱에서 백그라운드 서비스로 녹음까지는 할 수 있지만, 그 소리를 통해 잠이 들었는지를 판단하기 위해선 별도의 API, AI 등이 필요할 것 같다는 생각이 듭니다. 이에 대한 &lt;b&gt;&lt;u&gt;아이디어나 생각을 댓글로 남겨주시면 추후 관련 기능을 개선하는데 많은 도움이 될 것 같습니다 &lt;/u&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;오늘은 사용자의 수면 시각을 측정하는 기능을 시스템 OS 데이터를 기반으로 알아보았습니다! 9월도 화이팅하세요!  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android</category>
      <category>events</category>
      <category>usagestatsmanager</category>
      <category>기상 시간</category>
      <category>사용자 수면 시간 측정</category>
      <category>시스템 os 내부 데이터</category>
      <category>취침 시간</category>
      <author>청둥이 </author>
      <guid isPermaLink="true">https://soung-appdeveloper.tistory.com/168</guid>
      <comments>https://soung-appdeveloper.tistory.com/168#entry168comment</comments>
      <pubDate>Sun, 31 Aug 2025 21:15:24 +0900</pubDate>
    </item>
    <item>
      <title>UI 레이어란?</title>
      <link>https://soung-appdeveloper.tistory.com/166</link>
      <description>&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;001&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/001.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/001.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요~ 오늘은 &lt;b&gt;클린 아키텍처의 마지막 레이어, UI 레이어&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;1168&quot; data-origin-height=&quot;591&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vcRG0/btsP3asmqay/9n1gp8qiTd9OJI6WKLm61K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vcRG0/btsP3asmqay/9n1gp8qiTd9OJI6WKLm61K/img.png&quot; data-alt=&quot;feat. 어플을 사용하는 유저가 왕이다  &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vcRG0/btsP3asmqay/9n1gp8qiTd9OJI6WKLm61K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvcRG0%2FbtsP3asmqay%2F9n1gp8qiTd9OJI6WKLm61K%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;1168&quot; height=&quot;591&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;591&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;feat. 어플을 사용하는 유저가 왕이다  &lt;/figcaption&gt;
&lt;/figure&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;1. UI 레이어란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 레이어는 &lt;b&gt;User Interface&lt;/b&gt; 레이어의 준말입니다. Presentation 레이어와 상호작용하는데, viewmodel을 통해 데이터를 받아와 화면 UI에 표시하거나 사용자의 액션을 viewModel로 전달하는 &lt;b&gt;인터페이스 역할&lt;/b&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;2. XML vs Compose&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;xml과 compose 둘 다 &lt;b&gt;'UI를 구현한다'&lt;/b&gt;는 공통점이 있는데요. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;기존 레거시 뷰로 xml 을 많이 사용했었지만 선언형 UI인 compose도 많은 장점이 있습니다. &lt;u&gt;상태를 mutablestate 로 지정하여 변경되었을 때 자동으로 recomposition을 해주는 점&lt;/u&gt;에서 상태 관리가 더욱 용이합니다. &lt;b&gt;프리뷰(Preview) 기능&lt;/b&gt;으로 따로 앱을 빌드하지 않고도 UI의 변경을 곧바로 확인할 수 있습니다. 또한 &lt;b&gt;애니메이션&lt;/b&gt; 기능을 기존 xml에서는 복잡하게 구현해야 했는데 &lt;u&gt;compose는 xml보다 비교적 쉽고 간단하게 구현&lt;/u&gt;할 수 있습니다. &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;xml과 compose 는 &lt;b&gt;호환성&lt;/b&gt;을 가지고 있기 때문에, 기존 xml 에다가 compose 뷰를 넣거나 compose 에서 xml 뷰를 넣는 혼합 UI를 구성할 수 있습니다. &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이때 중요한 점은 기존 xml 코드에서 compose로 변환하는 과정을 거쳐도&lt;b&gt; Presentation 레이어의 뷰모델 코드가 변경되서는 안됩니다&lt;/b&gt;. 왜냐하면 안쪽인 Presentation 레이어는 바깥쪽 UI 레이어에 의존하지 않기 때문입니다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&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;color: #333333; text-align: start;&quot;&gt;3. xml 예제 코드&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1756043194485&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;layout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&amp;gt;
    &amp;lt;data&amp;gt;
        &amp;lt;variable
            name=&quot;viewModel&quot;
            type=&quot;com.example.app.UserViewModel&quot;/&amp;gt;
    &amp;lt;/data&amp;gt;
    &amp;lt;LinearLayout
        android:orientation=&quot;vertical&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;&amp;gt;
        &amp;lt;TextView
            android:text=&quot;@{viewModel.user.name}&quot;/&amp;gt;
    &amp;lt;/LinearLayout&amp;gt;
&amp;lt;/layout&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;&lt;b&gt;데이터 바인딩&lt;/b&gt;을 사용하기 위해 &amp;lt;layout&amp;gt; 태그를 가장 바깥쪽에 감싸줍니다. 해당 태그는 데이터와 UI를 연결하는 역할을 합니다. &amp;lt;data&amp;gt; 태그는 xml 레이아웃에서 사용할 객체를 정의합니다. name 속성을 통해 viewmodel 객체에 접근할 수 있습니다. TextView의 text에서는 데이터 바인딩 표현식인 @{ } 을 통해 viewmodel 객체의 값에 접근합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756043743985&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserActivity: AppCompatActivity() {
    private lateinit var binding: ActivityUserBinding
    private val viewModel: UserViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_user)
        binding.viewModel = viewModel
        binding.lifecycleOwner = this
        
        val userId = intent.getStringExtra(&quot;USER_ID&quot;)
        viewModel.loadUserProfile(userId)
    }
}&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;여기서는 DataBindingUtil 을 통해 데이터 바인딩을 사용해 binding 객체과 layout 파일을 연결해줍니다. binding.viewModel 에서는 아까 &amp;lt;data&amp;gt; 태그로 선언한 viewmodel 변수에 실제 viewmodel 을 주입합니다. binding.lifecycleOwner = this 는 livedata 를 xml에서 관찰하기 위해 반드시 지정해야 합니다. 해당 설정을 하지 않으면 text 에서의 @{viewModel.user.name} 값은 초기에 한 번만 값이 들어가고, 그 이후에 변경은 UI에 반영되지 않습니다. viewModel.loadUserProfile(userId) 에서는 UI 레이어에서 Presentation 레이어의 뷰모델을 호출하여 값을 전달하는 것을 확인할 수 있습니다.&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;color: #333333; text-align: start;&quot;&gt;4. compose 예제 코드&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1756045095554&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Composable
fun UserInfo(viewModel: UserViewModel) {
    val user by viewModel.user.collectAsStateWithLifecycle()
    if(user == null) {
        Text(text = &quot;Loading&quot;)
    } else {
        Text(text = &quot;user: ${user.name}&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presentation 레이어의 뷰모델로부터 user 데이터를 받아와 UI 레이어의 컴포저블로 노출하는 것을 확인할 수 있습니다. 여기서 viewModel의 user는 플로우(flow)를 state로 변환해주고 Lifecycle을 통해 화면이 보일 때만 안전하게 데이터를 수집(collect)합니다. Text 컴포저블로 값의 상태에 따라 UI를 다르게 표시하는걸 볼 수 있습니다.&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;5. 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 레이어는 Presentation 레이어의 뷰모델의 데이터를 화면에 표시하는 역할을 할 뿐 &lt;u&gt;도메인 레이어의 비즈니스 로직을 알지 못합니다&lt;/u&gt;. 지금까지 배운 레이어를 &lt;b&gt;플로우로 정리&lt;/b&gt;하면 사용자의 이벤트가 UI 레이어를 통해 Presentation 레이어의 뷰모델로 전파되고, 이후 도메인 레이어의 usecase 에 도달합니다. Data 레이어의 Repository를 거쳐 Remote, Local 레이어의 DataSource 를 통해 관련된 데이터를 가져와 다시 반대 방향으로 UI에 다시 액션으로 돌려주는 과정을 거칩니다.&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;u&gt;Domain 레이어, Data 레이어, Remote 레이어, Local 레이어, Presentation 레이어, UI 레이어&lt;/u&gt;에 대해서 자세히 살펴보았습니다. &lt;b&gt;클린 아키텍처를 통해 복잡하고 고도화된 앱의 구조 설계에 실제 적용&lt;/b&gt;해보면 좋을 것 같습니다!  &lt;/p&gt;</description>
      <category>Clean Architecture</category>
      <category>activity</category>
      <category>Clean Architecture</category>
      <category>compose</category>
      <category>fragment</category>
      <category>ui layer</category>
      <category>User Interface</category>
      <author>청둥이 </author>
      <guid isPermaLink="true">https://soung-appdeveloper.tistory.com/166</guid>
      <comments>https://soung-appdeveloper.tistory.com/166#entry166comment</comments>
      <pubDate>Sun, 24 Aug 2025 22:00:41 +0900</pubDate>
    </item>
    <item>
      <title>Presentation 레이어란?</title>
      <link>https://soung-appdeveloper.tistory.com/163</link>
      <description>&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;025&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/025.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/025.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&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; Presentation 레이어&lt;/b&gt;에 대해서 알아보겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;573&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qRqcH/btsPUHJ3PVF/XEiR3tGAvvtNvTec1yP9D1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qRqcH/btsPUHJ3PVF/XEiR3tGAvvtNvTec1yP9D1/img.png&quot; data-alt=&quot;출처: https://github.com/bufferapp/clean-architecture-components-boilerplate?tab=readme-ov-file#Architecture (뷰모델만 그림)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qRqcH/btsPUHJ3PVF/XEiR3tGAvvtNvTec1yP9D1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqRqcH%2FbtsPUHJ3PVF%2FXEiR3tGAvvtNvTec1yP9D1%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;1234&quot; height=&quot;573&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;573&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://github.com/bufferapp/clean-architecture-components-boilerplate?tab=readme-ov-file#Architecture (뷰모델만 그림)&lt;/figcaption&gt;
&lt;/figure&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;1. Presentation layer의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프레젠테이션 레이어&lt;/b&gt;는 UI 레이어와 도메인 레이어 사이에 존재하는데요. 도메인 레이어의 UseCase를 사용하여 데이터를 가져와 UI 레이어로 전달하거나, 반대로 UI의 사용자 액션(버튼 클릭 등)을 처리해서 도메인 레이어에 전달하는 &lt;b&gt;중간다리 역할&lt;/b&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;2. Presentation layer의 뷰모델&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레젠테이션 레이어에는 Model과 Model Mapper도 있지만, &lt;b&gt;뷰모델(ViewModel)&lt;/b&gt; 역할이 큰데요. 주로 &lt;b&gt;MVVM 패턴의 뷰모델&lt;/b&gt;이 사용됩니다. 먼저 관련 코드를 한번 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1755438778061&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserViewModel(): ViewModel(
    private val getUserUseCase: GetUserUseCase
) {
    private val _user = MutableLiveData&amp;lt;User&amp;gt;()
    val user: LiveData&amp;lt;User&amp;gt; get() = _user
    
    fun getUser(userId: String) {
        viewModelScope.launch {
            _user.value = getUserUseCase.invoke(userId)
        }
    }
}&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;뷰모델은 UI의 데이터를 상태로 관리하기 위해 위와 같이 &lt;b&gt;LiveData&lt;/b&gt;나 &lt;b&gt;StateFlow&lt;/b&gt;를 사용합니다. LiveData는 &lt;b&gt;안드로이드 플랫폼&lt;/b&gt;에 의존하고, StateFlow는 &lt;b&gt;코틀린&lt;/b&gt;에 의존합니다. 근데, Presentation 레이어는 최대한 &lt;u&gt;안드로이드 플랫폼에 의존하지 않는 것을 목표&lt;/u&gt;로 하기 때문에 기존에 LiveData를 사용한다면 StateFlow로 바꾸는 것이 좋습니다. 또한, 데이터를 가져오기위해 도메인 레이어의 비즈니스 로직인 &lt;b&gt;UseCase&lt;/b&gt;를 호출하는데요. UseCase로부터 얻어온 데이터를&amp;nbsp; UI 레이어에 전달하여 사용자의 액션을 처리합니다. 이때 데이터가 없는 경우 에러를 전달합니다. 반대로, UI 레이어의 사용자 액션을 받아와 처리한 후 UseCase에 전달하기도 합니다.&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;3. 클린 아키텍처의 뷰모델 vs AAC ViewModel&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린 아키텍처의 뷰모델은 안드로이드 &lt;b&gt;앱 아키텍처에 기반한 뷰모델과 다른 개념&lt;/b&gt;입니다. 왜냐하면, &lt;b&gt;클린 아키텍처의 뷰모델&lt;/b&gt;은&lt;u&gt; 안드로이드 플랫폼을 의존하지 않기 때문&lt;/u&gt;입니다. import android.* 와 같은 android 패키지를 사용해서는 안되며, R.id. 또는 R.layout. 처럼 android에 특화된 R도 사용할 수 없습니다. 아까 상태 관리에서 안드로이드에 의존하는 LiveData가 아닌 StateFlow 사용을 권장하는 것도 같은 맥락입니다.&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;u&gt;생명주기나 상태관리를 안드로이드 뷰모델이 더 잘 관리해주기 때문&lt;/u&gt;입니다. 예를 들어, 화면 회전 등으로 다시 UI가 그려지는 상황에서도 뷰모델 덕분에 데이터 상태가 초기화되지 않고 그대로 유지됩니다. 또한, 뷰모델의 생명주기는 뷰(액티비티, 프래그먼트)의 생명주기보다 더 오래 유지되는 특성이 있습니다.&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;따라서, 안드로이드 뷰 모델을 사용해야 하기에 AAC 관련한 import android.arch.xxx 는 필요합니다(이때, arch는 architecture을 의미합니다). 하지만 그 외의 다른 안드로이드 패키지는 사용할 수 없습니다.&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;4. 단방향 데이터 흐름(UDF)&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Presentaion 레이어의 뷰모델은 Domain 레이어의 데이터를 UI 에 전달하거나 사용자 액션을 뷰모델에서 처리해서 다시 UseCase에 전달하는 단방향 흐름을 갖습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 테스트 코드 작성 예시&lt;/h3&gt;
&lt;pre id=&quot;code_1755440987820&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserViewModelTest {
    private lateinit var viewModel: UserViewModel
    private val getUserUseCase = mock(GetUserUseCase::class.java)
    
    @Before
    fun setup() {
        viewModel = UserViewModel(getUserUseCase)
    }
    
    @Test
    fun 'test mothod'() {
        val user = User(&quot;1&quot;, &quot;홍길동&quot;, &quot;대한민국&quot;)
        'when'(getUserUseCase(&quot;1&quot;)).thenReturn(user)
        viewModel.loadUser(&quot;1&quot;)
        assertEquals(viewModel.user.value.address, &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;위와 같이 &lt;b&gt;JUnit&lt;/b&gt;와 &lt;b&gt;Mockito&lt;/b&gt;를 사용하여 ViewModel &lt;b&gt;단위 테스트&lt;/b&gt;를 할 수 있습니다. 실제 UseCase 대신 mock을 주입하여 UserViewModel.loadUser() 메소드가 GetUserUseCase 의 반환값을 LiveData에 올바르게 반영하는지 확인합니다.&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;Presentation 레이어의 뷰모델&lt;/b&gt;에 대해 집중적으로 알아보았습니다. 다음에는 Presentation 레이어에 사용자의 액션을 전달하는 &lt;b&gt;UI 레이어&lt;/b&gt;에 대해서 알아보겠습니다!&lt;/p&gt;</description>
      <category>Clean Architecture</category>
      <category>AAC ViewModel</category>
      <category>Clean Architecture</category>
      <category>Livedata</category>
      <category>MVVM</category>
      <category>presentation layer</category>
      <category>StateFlow</category>
      <category>udf</category>
      <category>Usecase</category>
      <category>ViewModel</category>
      <category>상태 관리</category>
      <author>청둥이 </author>
      <guid isPermaLink="true">https://soung-appdeveloper.tistory.com/163</guid>
      <comments>https://soung-appdeveloper.tistory.com/163#entry163comment</comments>
      <pubDate>Sun, 17 Aug 2025 21:46:48 +0900</pubDate>
    </item>
    <item>
      <title>Local 레이어란?</title>
      <link>https://soung-appdeveloper.tistory.com/161</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요~ 저번주에는 데이터 레이어를 의존하는 원격 레이어에 대해서 알아봤었는데요. 오늘은 데이터 레이어를 의존하는 또 다른 레이어인 &lt;b&gt;로컬 레이어&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;1032&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7Yt74/btsPOHbhDwM/rIFtDqrl4cYbouO3EkIdAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7Yt74/btsPOHbhDwM/rIFtDqrl4cYbouO3EkIdAk/img.png&quot; data-alt=&quot;출처: https://github.com/bufferapp/clean-architecture-components-boilerplate?tab=readme-ov-file#Architecture&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7Yt74/btsPOHbhDwM/rIFtDqrl4cYbouO3EkIdAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7Yt74%2FbtsPOHbhDwM%2FrIFtDqrl4cYbouO3EkIdAk%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;1032&quot; height=&quot;492&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;492&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://github.com/bufferapp/clean-architecture-components-boilerplate?tab=readme-ov-file#Architecture&lt;/figcaption&gt;
&lt;/figure&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;1. 로컬 레이어 정의와 구성요소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 레이어는 &lt;b&gt;로컬 데이터를 저장하고 읽어오는 계층&lt;/b&gt;으로 DB, SharedPreferences, 파일 시스템으로 관리합니다. &lt;b&gt;로컬 레이어의 구성요소&lt;/b&gt;로는 DataSource Implementation, Database, Local model, Local Model Mapper가 있습니다. 하나씩 살펴보겠습니다.&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;2. DataSource Implementation&lt;/h3&gt;
&lt;pre id=&quot;code_1754830535600&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserLocalDataSourceImpl(
    private val userDao: UserDao,
    private val userMapper: UserMapper
): UserLocalDataSource {
    override suspend fun getUserById(userId: Int): UserEntity? {
        val localUser = userDao.getUserById(userId)
        return userMapper.mapLocalToEntity
    }
    override suspend fun saveUser(user: UserEntity) {
        val localUser = userMapper.mapEntityToLocal(user)
        userDao.insertUser(local)
    }
}&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;LocalDataSourceImpl&lt;/b&gt;은 데이터 레이어의 &lt;u&gt;LocalDataSoure 인터페이스를 구현&lt;/u&gt;합니다. 첫번째 메소드에서는 userId를 통해 유저데이터를 가져오는 로직이 필요합니다.이때, &lt;u&gt;로컬 데이터베이스인 &lt;b&gt;DAO&lt;/b&gt;를 의존&lt;/u&gt;하여 관련 데이터를 가져옵니다. 가져온 데이터는 Local 레이어에서 쓸 수 있는 모델이기 때문에 이를 다시 데이터 레이어의 모델인 엔티티로 변환하기 위해서 &lt;u&gt;&lt;b&gt;mapper&lt;/b&gt;를 의존&lt;/u&gt;합니다. 두번째 메소드에서는 데이터 레이어의 모델을 로컬 레이어에 저장합니다. 이때, 서로 다른 모델간의 변환을 위해 mapper를 사용하고 마찬가지로 로컬 데이터베이스에 저장합니다.&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;3. Database (= DAO)&lt;/h3&gt;
&lt;pre id=&quot;code_1754831243423&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Dao
interface userDao: BaseDao&amp;lt;UserLocal&amp;gt; {
    @Query(&quot;SELECT * FROM {RoomConstant.Table.USER} WHERE id = :userId&quot;)
    suspend fun getUser(userId: Int): UserLocal?
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: UserLocal)
}&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;DAO 데이터베이스&lt;/b&gt;에서는 &lt;b&gt;Room의 SQL문&lt;/b&gt;을 통해 &lt;u&gt;사용자의 정보에 대한 조회, 추가, 업데이트&lt;/u&gt; 등의 기능을 합니다. 이때 조회 기능을 하는 getUser 메소드에서 리턴값인 유저 정보는 &lt;u&gt;물음표(?)를 통해 null 처리&lt;/u&gt;를 해야합니다. 왜냐하면 &lt;u&gt;로컬 데이터는 '언제든 데이터가 삭제될 수 있다'는 가정으로 설계&lt;/u&gt;해야하기 때문입니다. 또한 데이터 추가를 하는 @Insert 어노테이션 옆의 onConflict는 기존 데이터가 존재할때 덮어쓴다는 것을 의미합니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Mapper&lt;/h3&gt;
&lt;pre id=&quot;code_1754831568318&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal class UserMapper {
    fun mapLocalToEntity(userLocal: UserLocal): UserEntity {
        return UserEntity(
            id = userLocal.id,
            name = userLocal.name,
            address = userLoal.address
        )
    }
    
    fun mapEntityToLocal(userEntity: UserEntity): UserLocal {
        return UserLocal(
            id = userEntity.id,
            name = userEntity.name,
            address = userEntity.address
        )
    }
}&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;매퍼(Mapper)&lt;/b&gt;는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;u&gt;로컬 레이어의 모델과 데이터 레이어의 모델 사이를 변환&lt;/u&gt;해주는 역할을 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 추가 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째로, 데이터베이스에서 데이터를 가져오는 과정을 UI 스레드인 메인 스레드에서 실행하게 되면 화면이 멈추는 &lt;b&gt;ANR(Application Not Responding) 오류&lt;/b&gt;가 생길 수 있습니다. 따라서 &lt;u&gt;코루틴 등을 이용하여 다른 스레드에서 실행하는 비동기 처리&lt;/u&gt;가 필요합니다. 두번째로, 데이터저장소 중 &lt;b&gt;Room&lt;/b&gt;을 사용할 지 아니면 &lt;b&gt;SharedPreference&lt;/b&gt; 사용할 지에 대한 고민 과정이 필요합니다. &lt;u&gt;Room의 경우 객체와 같은 비교적 무거운 데이터&lt;/u&gt;를, &lt;u&gt;SharedPreference는 토큰과 같은 String primitive type을 저장&lt;/u&gt;할 수 있는 &lt;u&gt;가벼운 데이터&lt;/u&gt;가 적합합니다. 물론 객체를 Gson, Moshi&lt;b&gt;를&lt;/b&gt; 이용해 String으로 직렬화하여 Room이 아닌 SharedPreference에 저장할 수 있는데 &lt;u&gt;상황에 따라 잘 파악&lt;/u&gt;하는게 좋습니다. 세번째로, 데이터베이스의 &lt;b&gt;DTO 필드 구조가 변경&lt;/b&gt;될 때 적절히 대응해야합니다. &lt;b&gt;기본값&lt;/b&gt;을 추가하거나, &lt;b&gt;nullable 필드&lt;/b&gt;로 선언하는 방법이 있습니다. 마지막으로, 로컬 레이어와 원격 레이어는 각각 데이터 레이어에 의존할 뿐 &lt;u&gt;서로를 의존하지 않습니다&lt;/u&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;Presentation 레이어&lt;/b&gt;에 대해서 알아보겠습니다. 도움이 되셨다면 좋겠습니다 이번주 화이팅하세요!  &lt;/p&gt;</description>
      <category>Clean Architecture</category>
      <category>Clean Architecture</category>
      <category>dao</category>
      <category>local layer</category>
      <category>LocalDataSourceImpl</category>
      <category>Mapper</category>
      <category>Model</category>
      <category>room</category>
      <category>SharedPreference</category>
      <author>청둥이 </author>
      <guid isPermaLink="true">https://soung-appdeveloper.tistory.com/161</guid>
      <comments>https://soung-appdeveloper.tistory.com/161#entry161comment</comments>
      <pubDate>Sun, 10 Aug 2025 20:40:53 +0900</pubDate>
    </item>
    <item>
      <title>Remote 레이어란?</title>
      <link>https://soung-appdeveloper.tistory.com/159</link>
      <description>&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;niniz&quot; data-emoticon-name=&quot;002&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/002.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/002.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&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;오늘은 데이터 레이어(data layer)에 의존하는 &lt;b&gt;원격 레이어(remote layer)&lt;/b&gt;에 대해 알아보려고 합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1257&quot; data-origin-height=&quot;541&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wH45E/btsPGtK11cS/5NcBVzLbUwQpCwvRksg5Q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wH45E/btsPGtK11cS/5NcBVzLbUwQpCwvRksg5Q1/img.png&quot; data-alt=&quot;출처: https://github.com/bufferapp/clean-architecture-components-boilerplate?tab=readme-ov-file#Architecture + 직접 그림&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wH45E/btsPGtK11cS/5NcBVzLbUwQpCwvRksg5Q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwH45E%2FbtsPGtK11cS%2F5NcBVzLbUwQpCwvRksg5Q1%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;1257&quot; height=&quot;541&quot; data-origin-width=&quot;1257&quot; data-origin-height=&quot;541&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://github.com/bufferapp/clean-architecture-components-boilerplate?tab=readme-ov-file#Architecture + 직접 그림&lt;/figcaption&gt;
&lt;/figure&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;1. 원격 레이어란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원격 레이어(remote layer)&lt;/b&gt;는 원격 서버와 통신하여 데이터를 주고받는 계층입니다. 데이터를 요청하거나, 요청에 대한 응답 처리 및 네트워크 오류 처리를 하게 됩니다. 이때 Retrofit이나 OkHttp같은 네트워크 라이브러리를 사용하여 API 호출을 관리하게 됩니다. 구성요소는 DataSourceImplementation, API Service, Model Mapper, API Factory, Model 이 있습니다. 중요한 구성요소 위주로 하나씩 살펴보겠습니다.&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;2. 원격 레이어의 구성요소&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. RemoteDataSourceImpl&lt;/h4&gt;
&lt;pre id=&quot;code_1754235253886&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal class UserRemoteDataSourceImpl(
    private val apiService: ApiService,
    private val userMapper: UserMapper
): UserRemoteDataSource {
    override fun getUserById(userId: String): UserEntity {
        val remoteUser = apiService.getUserById(userId)
        return userMapper.mapResponseToEntity(remoteUser)
    }
}&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; RemoteDataSourceImpl&lt;/b&gt;은 데이터 레이어에 정의되어 있는 DataSource Interface를 구현합니다. 인터페이스 내부에 정의되어 있는 메소드는 userId를 가지고 데이터 레이어의 모델(Entity)를 반환하는 함수입니다. 데이터를 받아오기 위해 필요한 첫번째 의존성은 &lt;b&gt;API Service&lt;/b&gt;입니다. API Service를 통해 원격 레이어의 모델(Response)을 받아오고, 이를 다시 데이터 레이어에 전달하기 위해 &lt;b&gt;Model Mapper&lt;/b&gt;을 사용합니다. 즉, 원격 레이어의 모델(Reponse)를 데이터 레이어의 모델(Entity)로 매핑합니다.&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;2-2. API Service&lt;/h4&gt;
&lt;pre id=&quot;code_1754236564125&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface ApiService {
    @GET(&quot;users/{userId}&quot;)
    suspend fun getUserById(@Path(&quot;userId&quot;) userId: String): UserResponse
}&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;ApiService&lt;/b&gt;는 인터페이스 형식으로 Retrofit 등을 정의합니다. 이때 suspend 지연 함수를 통해 코루틴을 사용하는 것을 알 수 있습니다. Retrofit의 API Factory에서 실제 구현사항을 만들어줍니다.&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;2-3. Model Mapper&lt;/h4&gt;
&lt;pre id=&quot;code_1754236884528&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal class UserMapper {
    // remote layer &amp;rarr; data layer
    fun mapResponseToEntity(userResponse: UserResponse): UserEntity {
        return UserEntity(
            id = userResponse.id,
            name = userResponse.name,
            address = userResponse.address
        )
    }
    // data layer &amp;rarr; remote layer
    fun mapEntityToResponse(userEntity: UserEntity): UserResponse {
        return UserResponse(
            id = userEntity.id,
            name = userEntity.name,
            address = userEntity.address
        )
    }
}&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;Model Mapper&lt;/b&gt;는 &lt;u&gt;원격 레이어와 데이터 레이어의 모델을 서로 변환해주는 역할&lt;/u&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;3. 테스트 코드 작성&lt;/h3&gt;
&lt;pre id=&quot;code_1754237234305&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserApiTest {
    @Test
    fun 'API로 유저 정보 가져오기'() {
        val mockWebServer = MockWebServer().apply {
            enqueue(MockResponse().setBody{&quot;...&quot;)
        }
        
        val apiService = Retrofit.Builder()
            .baseUrl(mockWebServer.url(&quot;...&quot;)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
            
        runBlocking {
            val userResponse = apiService.getUserById(&quot;1&quot;)
            assertEquals(&quot;비교 대상&quot;, userResponse.name)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같이 &lt;b&gt;테스트 코드&lt;/b&gt;를 작성하여 &lt;u&gt;API가 제대로 작동하는 지 점검&lt;/u&gt;할 수 있습니다. runBlocking 코루틴 빌더를 이용하여 코드 블록 수행이 완료될 때까지 다음 코드의 수행을 막는 것을 알 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. @SerializedName&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@SerializedName&lt;/b&gt;은 &lt;u&gt;API 서버 통신 등으로 응답값을 받아오는 상황에서 필드명을 재정의하는데 도움을 주는 어노테이션&lt;/u&gt;입니다. 다음과 같이 데이터 레이어의 모델(엔티티)가 정의되어있다고 가정합시다.&lt;/p&gt;
&lt;pre id=&quot;code_1754237709785&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// data layer model
data class UserEntity(
    val id: String,
    val name: String,
    val createdAt: LocalDateTime
)&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;pre id=&quot;code_1754237830250&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// case1) remote layer의 model 필드 그대로 사용
data class UserResponse(
    @SerializedName(&quot;id&quot;)
    val id: String,
    @SerializedName(&quot;name&quot;)
    val name: String,
    @SerializedName(&quot;time_stamp&quot;)
    val timeStamp: LocalDateTime
)

// case2) data layer의 model 필드에 맞게 변경
data class UserResponse(
    @SerializedName(&quot;id&quot;)
    val id: String,
    @SerializedName(&quot;name&quot;)
    val name: String,
    @SerializedName(&quot;time_stamp&quot;)
    val createdAt: LocalDateTime
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫 번째&lt;/b&gt;는, &lt;u&gt;원격 레이어에서 응답값으로 내려오는 필드명을 그대로 사용&lt;/u&gt;하는 경우입니다. &lt;b&gt;두 번째&lt;/b&gt;는, &lt;u&gt;데이터 레이어에 정의된 모델의 필드명을 반영하여 변경하는 경우&lt;/u&gt;입니다. 프로젝트 내에서 일관성만 유지하면 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;두 방법 중 &lt;b&gt;어떤 방식을 취하든&lt;/b&gt;&lt;span&gt;&lt;b&gt; 괜찮습니다&lt;/b&gt;. 다만, 두 가지 방법을 한 프로젝트 내에서 같이 쓰는 것은 좋지 않습니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 추가 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫 번째&lt;/b&gt;로, 원격 레이어(remote layer)는 이론적으로는 안드로이드 플랫폼 의존성 없이 사용 가능한 것으로 설명하지만, 실제 프로젝트에서는 그렇지 못한 경우가 많습니다. 즉, 코틀린 모듈로만 만들지 않고 &lt;u&gt;안드로이드 모듈에도 의존&lt;/u&gt;하게 됩니다. &lt;b&gt;두 번째&lt;/b&gt;로, &lt;u&gt;원격 통신을 위한 인증 토큰을 잘 관리해야 합니다&lt;/u&gt;. 인증 토큰에 해당하는 쿠키 및 헤더를 &lt;b&gt;Interceptor&lt;/b&gt;를 통해 관리할 수 있습니다. &lt;b&gt;세 번째&lt;/b&gt;로, &lt;u&gt;HTTP 상태 코드에 맞는 에러처리 및 메세지 관리&lt;/u&gt;를 해야합니다. 요청이 실패했을 때 한번만 다시 재요청할 것인지, 아니면 성공할 때까지 재요청할 것인지에 대한 retry를 설계해야합니다. &lt;b&gt;마지막&lt;/b&gt;으로, &lt;u&gt;외부 API 키가 노출되지 않게 관리&lt;/u&gt;해야하며 데&lt;u&gt;이터를 암호화하는 과정을 거치는&amp;nbsp;것&lt;/u&gt;이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 데이터 레이어를 의존하는&lt;b&gt; 원격 레이어(remote layer)의 특징&lt;/b&gt;과 &lt;b&gt;구성요소&lt;/b&gt; 중 RemoteDataSourceImpl, API Service, Model Mapper 에 대해 알아보았습니다. 다음에는 데이터 레이어를 의존하는 또 다른 레이어인 &lt;b&gt;로컬 레이어(local layer)&lt;/b&gt;에 대해 알아보겠습니다. 이번주도 화이팅하세요!  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Clean Architecture</category>
      <category>Clean Architecture</category>
      <category>Model</category>
      <category>model mapper</category>
      <category>OkHttpClient</category>
      <category>remote layer</category>
      <category>RemoteDataSourceImpl</category>
      <category>response</category>
      <category>retrofit</category>
      <author>청둥이 </author>
      <guid isPermaLink="true">https://soung-appdeveloper.tistory.com/159</guid>
      <comments>https://soung-appdeveloper.tistory.com/159#entry159comment</comments>
      <pubDate>Sun, 3 Aug 2025 20:06:11 +0900</pubDate>
    </item>
    <item>
      <title>Data 레이어란?</title>
      <link>https://soung-appdeveloper.tistory.com/158</link>
      <description>&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;face&quot; data-emoticon-name=&quot;036&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/036.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/036.png&quot; width=&quot;80&quot; /&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요  오늘 UMC x 구름톤 유니브에서 주관한 &lt;b&gt;컨퍼런스&lt;/b&gt;에 참여하기 위해 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;b&gt;공덕역 창업허브센터&lt;/b&gt;에 다녀왔어&lt;/span&gt;요. 연사님들로부터 30분씩 총 아홉 분의 강연을 들었는데요. &lt;b&gt;SDK, Coil 라이브러리&lt;/b&gt; 외에도 &quot;여러분의 아키텍처 안녕하신가요?&quot; 라는 주제로 &lt;b&gt;밥아저씨의 클린 아키텍처&lt;/b&gt;를 강연해주시더라고요. &lt;u&gt;레이어의 개수보다 의존 관계 및 방향이 중요하다는 점, 엔티티와 유즈케이스를 외부 관심사로부터 분리해야 한다는 점&lt;/u&gt;을 말씀하셨습니다. 또한 안드로이드는 &lt;b&gt;compose, hilt/koin(DI), flow, coroutine 등 정형화된 구조를 딥다이브하는게 좋다&lt;/b&gt;고 하셨는데요. 공부하는 데에 있어 &lt;u&gt;좋은 동기부여가 될 수 있었던 유익한 시간&lt;/u&gt;이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBiHml/btsPBfUTxz9/ryIJhHi3o0nAy0stXKkyH1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBiHml/btsPBfUTxz9/ryIJhHi3o0nAy0stXKkyH1/img.jpg&quot; data-alt=&quot;프론트엔드 컨퍼런스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBiHml/btsPBfUTxz9/ryIJhHi3o0nAy0stXKkyH1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBiHml%2FbtsPBfUTxz9%2FryIJhHi3o0nAy0stXKkyH1%2Fimg.jpg&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;316&quot; height=&quot;421&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프론트엔드 컨퍼런스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;오늘은 저번에 이어 클린 아키텍처의 도메인 레이어를 의존하는 &lt;b&gt;데이터 레이어(data layer)&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;397&quot; data-origin-height=&quot;609&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n1M39/btsPDb4o90R/D1tqdRpyRy23aaxES9fllK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n1M39/btsPDb4o90R/D1tqdRpyRy23aaxES9fllK/img.png&quot; data-alt=&quot;출처: https://github.com/bufferapp/clean-architecture-components-boilerplate?tab=readme-ov-file#Architecture&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n1M39/btsPDb4o90R/D1tqdRpyRy23aaxES9fllK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn1M39%2FbtsPDb4o90R%2FD1tqdRpyRy23aaxES9fllK%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;397&quot; height=&quot;609&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;609&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://github.com/bufferapp/clean-architecture-components-boilerplate?tab=readme-ov-file#Architecture&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;1. 데이터 레이어의 역할&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 레이어(Data layer)&lt;/b&gt;는 저번에 살펴보았던 &lt;b&gt;도메인 레이어(Domain layer) 에 필요한 데이터를 제공&lt;/b&gt;하는 역할을 합니다. 도메인 레이어를 의존하긴 하지만, 비즈니스 로직(도메인)은 관심사가 아니며 오직&lt;b&gt; 데이터 전달&lt;/b&gt;에만 집중을 하는데요. 이때, 데이터 연결을 위해 &lt;b&gt;레포지토리 패턴(Repository Pattern)&lt;/b&gt;을 사용합니다. 여기서는 &lt;u&gt;데이터를 저장하고, 수정하거나 삭제하는 등 CRUD 작업을 수행&lt;/u&gt;합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;2. 데이터 레이어의 구성요소&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; 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;977&quot; data-origin-height=&quot;819&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZJmol/btsPB30n6bD/Jn5iE9kil3xHCDpZGE4ih1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZJmol/btsPB30n6bD/Jn5iE9kil3xHCDpZGE4ih1/img.png&quot; data-alt=&quot;데이터 레이어(출처: 직접 그림)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZJmol/btsPB30n6bD/Jn5iE9kil3xHCDpZGE4ih1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZJmol%2FbtsPB30n6bD%2FJn5iE9kil3xHCDpZGE4ih1%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;734&quot; height=&quot;615&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;819&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;b&gt;Repository Implementation&lt;/b&gt;가 있습니다. 도메인 레이어에 정의된 Repository Interface 를 구현하고, DataSource(Local, Remote)를 의존하여 도메인에 필요한 데이터를 제공합니다. 두 번째는 &lt;b&gt;DataSource Interface&lt;/b&gt; 로 LocalDataSource, RemoteDataSource 두 종류로 구성됩니다. 세 번째로 &lt;b&gt;Model&lt;/b&gt;은 데이터 레이어에 정의된 DTO로 &lt;b&gt;엔티티(Entity)&lt;/b&gt;라고도 불립니다. 마지막으로 &lt;b&gt;Model Mapper&lt;/b&gt;는 도메인 레이어와 데이터 레이어의 모델을 형변환하는 역할을 합니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. RepositoryImpl (= Repository Implementation)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RepositoryImpl 을 구현할 때는 두 가지 케이스가 있는데 하나씩 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1753684883001&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// case1) RemoteDataSource 의존하는 경우
class UserRepositoryImpl(
    private val userRemoteDataSource: UserRemoteDataSource, // interface
    private val userMapper: UserMapper
): UserRepository {
    override fun getUserById(userId: String): User {
        val remoteUser = userRemoteDataSource.getUserById(userId) // entity
        return userMapper.mapEntityToDomain(remoteUser)
    }
}&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;DataSource 중 RemoteDataSource 만 의존하는 경우&lt;/b&gt;입니다. 여기서 RepositoryImpl 은 도메인에 정의된 Repository Interface 를 구현합니다. 오버라이딩 함수를 통해 얻어지는 userId 값을 가지고 User 라는 도메인 레이어의 모델(Model) 을 반환해야하는 상황입니다. 데이터를 얻기 위해 RemoteDataSource 인터페이스를 의존하게 되고, 내부의 메소드에 userId 을 전달하면서 원하는 값을 얻어옵니다. 하지만 이때 값은 데이터 레이어에서만 사용할 수 있는 모델(entity)이기에, Model Mapper 또한 의존하여 도메인이 원하는 모델로 바꿔서 반환합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1753685638957&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// case2) RemoteDataSource, LocalDataSource 둘 다 의존하는 경우
class UserRepositoryImpl(
    private val userLocalDataSource: UserLocalDataSource,
    private val userRemoteDataSource: UserRemoteDataSource
    private val userMapper: UserMapper
): UserRepository {
    override fun getUserById(userId: String): User {
        val localUser = userLocalDataSource.getUserById(userId) // entity
        if(localUser != null) return userMapper.mapEntityToDomain(localUser)
        // localUser가 null인 경우 (=기존에 저장된 유저 정보가 없는 경우)
        val remoteUser = userRemoteDataSource.getUserById(userId) // entity
        userLocalDataSource.saveUser(remoteUser) // 캐싱(caching) 작업
        return userMapper.mapEntityToDomain(remoteUser)
    }
}&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;DataSource 중 LocalDataSource, RemoteDataSource 둘 다 의존하는 경우&lt;/b&gt;입니다. 먼저, LocalDataSource로부터 기존에 유저 정보가 저장되어있는지 확인합니다. 이미 있다면, 캐싱된 유저 정보를 mapper 를 통해 형변환하여 도메인에 전달합니다. 만약 없다면 RemoteDataSource 에서 유저 정보를 가져오게 됩니다. 이후 LocalDataSource를 통해 내부에 캐싱(caching)을 하고나서 도메인에 마찬가지로 형 변환하여 전달하게 됩니다.&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;데이터를 가져올 때 단순히 Remote에서 가져오는 것&lt;/b&gt;입니다. 이때는 &lt;u&gt;Local의 캐싱을 사용하지 않기 때문에, 가져올 때까지 원형 프로그래스바와 같은 UI로 사용자에게 지연(delay)을 알려야합니다&lt;/u&gt;. &lt;b&gt;두 번째 케이스&lt;/b&gt;는 &lt;b&gt;사용자에게 먼저 캐싱된 Local을 먼저 보여주고나서 Remote을 통해 최신의 정보로 동기화&lt;/b&gt;하는 것인데요. 첫 번째 경우와 달리, &lt;u&gt;프로그래스바를 띄워줘야 하는 지연 상황은 발생하지 않지만, 두 가지 상황을 고려&lt;/u&gt;해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;만약 local과 remote의 &lt;b&gt;데이터 필드 값&lt;/b&gt;이 다르다면 어떻게 처리해야 할까?&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;만약 local과 remote의 &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;h3 data-ke-size=&quot;size23&quot;&gt;2-2. DataSource Interface&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataSource Interface 는 &lt;b&gt;RemoteDataSource, LocalDataSource&lt;/b&gt; 두 가지로 구성됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1753686359698&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface UserRemoteDataSource {
    fun getUserById(userId: String): UserEntity
}

interface UserLocalDataSource {
    fun saveUser(user: UserEntity)
    fun getUserById(userId: String): UserEntity?
}&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;Remote 와 다르게 Local 같은 경우에는 &lt;u&gt;기존에 저장된 정보가 없을 수 있기 때문&lt;/u&gt;에(정보 삭제 및 처음 시작할 경우) 물음표(?) 를 통해 nullable 처리를 해야합니다. 이때 반환값은 엔티티(entity)로 데이터 레이어의 모델입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. Model Mapper&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;b&gt;서로 다른 레이어의 모델을 형변환&lt;/b&gt;해주는 &lt;b&gt;model mapper&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1753687021154&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;internal class UserMapper {
    // data layer &amp;rarr; domain layer
    fun mapEntityToDomain(userEntity: UserEntiy): User {
        return User(
            id = userEntity.id,
            name = userEntity.name,
            address = userEntity.address
        )
    }
    
    // domain layer &amp;rarr; data layer
    fun mapDomainToEntity(user: User): UserEntity {
        return UserEntity(
            id = user.id,
            name = user.name,
            address = user.address
        )
    }
}&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;model mapper&lt;/b&gt;는 데이터 레이어의 모델(entity)을 도메인 모델로 바꿔주거나, 그 반대의 역할을 수행합니다. &lt;u&gt;&lt;b&gt;근데 왜 model mapper 가 도메인 레이어와 데이터 레이어 중 왜 꼭 데이터 레이어에 존재해야 하는걸까요?&lt;/b&gt;&lt;/u&gt; 의존성 방향을 보면 데이터 레이어가 도메인 레이어를 바라봅니다. 이때 데이터 레이어는 엔티티와 도메인 레이어의 모델 둘 다 알지만, &lt;u&gt;도메인 레이어는 엔티티 모델을 알지 못합니다&lt;/u&gt;. 따라서, &lt;u&gt;두 모델을 알고 있는 데이터 레이어에 model maper 을 두는 것&lt;/u&gt;입니다. 추가적으로 알아볼 부분은 클래스 앞의 접근 범위 제한자입니다. internal 은 같은 모듈 내에서만 접근할 수 있게 허용하는데요. 클린아키텍처의 각각의 계층이 모듈로 분리된다는 점을 고려하면 이해할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 추가적인 부분&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로, 도메인에 전달할 데이터를 위해 &lt;b&gt;remoteDataSource&lt;/b&gt;를 사용할 경우 &lt;b&gt;서버 오류, 매핑 오류&lt;/b&gt; 등이 발생할 수 있습니다. 이때, &lt;u&gt;데이터에 대한 Loading, Success, Error 상태를 관리&lt;/u&gt;하여 여러 상황에 적절하게 대처해야합니다. 두 번째로, 지난번에 도메인 레이어에서 언급한 유즈케이스처럼, &lt;b&gt;레포지토리 또한 단일 책임 원칙(SRP)을 준수&lt;/b&gt;해야 합니다. 여러 개의 기능을 하나의 레포지토리 안에 설계해서는 안 되며, UserRepository, OrderRepository, PaymentRepository 처럼 &lt;u&gt;각각 하나의 책임만 갖도록 분리&lt;/u&gt;해야 합니다. 세 번째로, 데이터를 가져오는 부분이다보니 &lt;b&gt;반응형 프로그래밍&lt;/b&gt;이 필요한데요. 이를 위해 &lt;u&gt;RxJava나 순수 코틀린 라이브러리인 Flow를 사용하여 비동기 데이터 스트림을 처리&lt;/u&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;데이터 레이어의 역할&lt;/b&gt;과 구성요소인 &lt;b&gt;RepositoryImpl, DataSource Interface, Model Mapper&lt;/b&gt;, 그리고 그 외의 &lt;b&gt;추가적인 부분&lt;/b&gt;에 대해 알아보았습니다. 다음에는 DataSource와 관련하여 &lt;b&gt;Remote 데이터 레이어&lt;/b&gt;에 대해 알아보겠습니다. 읽어주셔서 감사합니다!&lt;/p&gt;</description>
      <category>Clean Architecture</category>
      <category>Clean Architecture</category>
      <category>data layer</category>
      <category>datasource interface</category>
      <category>model mapper</category>
      <category>repositoryImpl</category>
      <category>SRP</category>
      <category>컨퍼런스</category>
      <author>청둥이 </author>
      <guid isPermaLink="true">https://soung-appdeveloper.tistory.com/158</guid>
      <comments>https://soung-appdeveloper.tistory.com/158#entry158comment</comments>
      <pubDate>Sun, 27 Jul 2025 19:40:26 +0900</pubDate>
    </item>
  </channel>
</rss>