<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Talent</title>
        <link>https://velog.io/</link>
        <description>기초 튼튼 개발자</description>
        <lastBuildDate>Thu, 27 Mar 2025 16:40:17 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Talent</title>
            <url>https://velog.velcdn.com/images/moony_-/profile/1e51ed75-827f-4d4e-8154-eb6131a32c73/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Talent. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/moony_-" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Super.init(version=6) 발표 후기]]></title>
            <link>https://velog.io/@moony_-/Super.initversion6-%EB%B0%9C%ED%91%9C-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@moony_-/Super.initversion6-%EB%B0%9C%ED%91%9C-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Thu, 27 Mar 2025 16:40:17 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<blockquote>
<p><strong>&quot;문휘님, Super.init이라고 주니어 컨퍼런스가 있어요. 발표 도전해보세요&quot;</strong></p>
</blockquote>
<p><a href="https://gdg.community.dev/gdg-korea-android/">GDG</a>에서 여는 <a href="https://gdg.community.dev/events/details/google-gdg-korea-android-presents-devfest-android-in-korea-2024/">DevFest</a>에 <strong>우연히</strong> 갔다가, <strong>우연히</strong> 운영진 회식 자리에 참석하게 되고, 또 <strong>우연히</strong> 네이버 부스트캠프 멘토님을 만나 들었던 이야기이다.
(운명인가)</p>
<p><strong>네이버 부스트캠프</strong>에서 발표를 여러번 하고 난 후 발표에 대해 자신감이 붙고 있었는데, 저 이야기를 들으니 하고싶은 마음이 들었다.</p>
<br/>


<blockquote>
<p><strong>&quot;부스트 캠프에서 컨퍼런스로 확장할 수 있는 기회다.&quot;</strong></p>
</blockquote>
<p>다른 개발자의 앞에 나서서 공유할 수 있는 기회는 많지 않다. 그래서 네이버 부스트캠프에서도 열심히 발표를 했었는데, 이 것을 더 확장할 수 있는 기회였다.</p>
<p>이후 연사자 모집 공고가 떴을 때, 내 프로젝트에 발표 내용이 있는지 뒤지기 시작했다.</p>
<br/>


<br/>


<h1 id="준비">준비</h1>
<blockquote>
<p>사람들에게 도움이 될 만한, 뻔하지 않은 주제를 발표하자.</p>
</blockquote>
<p>내가 갔었던 안드로이드 컨퍼런스에는 <code>Modularization</code>, <code>Clean Architecture</code>의 발표가 항상 있었던 것 같다. 아주 좋은 주제이기도 하고 각자 조금씩은 다른 생각을 가지고 있는거 같아서 매번 흥미롭게 들었다.</p>
<p>이전에 부스트 캠프 내부에서 모듈화와 아키텍쳐 관련 발표를 했기 때문에 이것을 좀 더 고쳐서 발표하려했지만, 난 아직 주니어이기 때문에 <strong>구조와 같이 중요하고 되돌리기 힘든</strong> 것을 잘못 알렸다간 민폐가 될 것 같았다.</p>
<p>결국 이것을 엎고 다른 주제로 발표하게 되었는데, 바로바로</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/1618d11f-aed6-43b3-8cd1-a66b40767cf3/image.png" alt=""></p>
<p>이것을 발표하게 된 계기가 있었는데,</p>
<h3 id="게임-제작-경험을-android에도-적용할-수-있었다">게임 제작 경험을 Android에도 적용할 수 있었다.</h3>
<p>Unity로 게임 제작을 할 때, 수학으로 별의 별것을 다 직접 구현해야 한다.</p>
<p>게임은 일반적으로 정해진 UI나 가이드가 없고, 각자의 요구사항이 천차만별이다. 무슨 UI 요구사항이 있을지 알 수 없기 때문에 어떤 것이든 구현할 수 있도록 준비해야 한다.</p>
<p>그 중에 중요한 것은 수학이다. 캐릭터의 이동, 물리적인 충돌 등 게임에 필요한 요구사항들은 대부분 <strong>움직임</strong>에 초점이 맞춰져 있는데, 이 때 여러 수학 계산이 들어간다.</p>
<p>난 Android도 다르지 않다고 생각한다. 권장 가이드라인이 있지만, 항상 따를 수 없고 벗어난 UI가 있을 수 있다.</p>
<p>그리고 그런 요구사항이 왔을 때, <strong>&quot;싫어요. 바꿔주세요.&quot;</strong> 보다는 구현할 수 있는 사람이 되고 싶었다.</p>
<p>그래서 여러 요구사항들을 게임 만들듯이 해결하곤 했는데, 이런 경험을 공유하면 재미있을 것 같다고 생각했다.</p>
<br/>

<blockquote>
<p><strong>&quot;특이하지만 유용한 내용이었으면 좋겠다.&quot;</strong></p>
</blockquote>
<p>많이 나오는 주제의 발표는 나보다 더욱 잘하는 사람이 있을 것이기에, 내가 해본 경험중에서 가장 특이하고 유용한 &quot;수학으로 UI 요구사항 만족시키기&quot; 주제로 발표를 하였다.</p>
<br/>

<br/>


<h1 id="발표">발표</h1>
<p>장소는 <a href="https://story.baemin.com/3168/">우아한 테크살롱</a>에서 진행되었다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/61c0e28e-5c01-4f9e-9f3f-1ec9c8d51b62/image.png" alt=""></p>
<p>이전에 GDG DevFesta에 참여한 적이 있어 익숙한 장소였다.
(여기서 발표를 듣기만 했는데 내가 할 줄이야...)</p>
<p>내가 발표할 때는 워크샵 때문에 사람들이 빠져나갔지만, 그럼에도 굉장히 많은 인원이 남아계셔서 긴장했었다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/fdd750c5-6dc1-45ca-9da0-2cd97aa28ddd/image.jpeg" alt=""></p>
<p>나의 주제가 수학이라, 반응이 별로 안좋았던 부분들이 있었다... ㅎㅎ</p>
<p>그 중에 표정이 안좋았던 파트는 역시 <strong>제 2코사인 법칙</strong>과 <strong>외적</strong>부분이 아니었을까...? ㅎㅎ</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/moony_-/post/47d8d35d-8c40-4a12-93e8-e26b0e90db47/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/moony_-/post/a4c6a874-9fc0-4ae0-bdfe-7dfcbb4d1572/image.png" alt=""></th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/moony_-/post/e2dbc342-a7cf-411b-931a-6eb37e96fb06/image.gif" alt=""></td>
<td><img src="https://velog.velcdn.com/images/moony_-/post/5fa78687-30ab-472c-b67d-f5fcf26df7dc/image.gif" alt=""></td>
</tr>
</tbody></table>
<br/>

<blockquote>
<p><strong>&quot;여러 발표를 들었지만, 이런 발표는 처음인 것 같아요&quot;</strong></p>
</blockquote>
<p>옆에서 진행해주시는 오거나이저분이 말씀해주셨던 말이다.
특이하고 유용한 발표를 하는게 목표였던 만큼, 가장 뿌듯했던 말이었다!</p>
<br/>

<br/>

<h1 id="느낀점">느낀점</h1>
<p><img src="https://velog.velcdn.com/images/moony_-/post/8174a56b-f56d-4765-9162-635e7b7e18e6/image.jpeg" alt=""></p>
<p>가장 좋았던 것은 역시 <strong>여러 개발자들과 만남을 가지는 것</strong>이었다.</p>
<p>발표가 모두 끝나고 나서 소통 시간을 가졌는데, 처음 만나는 개발자분들과 각자의 회사 사정 이야기도 들을 수 있었고, 여러 개발 방식, 요즘 트렌드도 들을 수 있었다.</p>
<p>개발 대화만 해도 즐거운데, 유용한 트렌드도 잘 알 수 있으니 역시 커뮤니케이션을 하는 것이 개발자로서 가장 빨리 성장하는 방법이라는 것을 다시 한번 느낄 수 있는 시간이었다.</p>
<br/>

<blockquote>
<p><strong>&quot;이후에도 계속 발표를 해보자&quot;</strong></p>
</blockquote>
<p>이번에는 운좋게 제안해주셔서 했지만, 다음에는 하지 않으려고 했었다.
나는 내가 작성한 코드도, 내가 공부했던 내용들도 모두 불신하는 사람이기 때문이다...</p>
<p>아직 주니어이기도 하고, 공부할 것도 많은데 사람들 앞에서 잘못된 내용을 발표할까봐 두려움이 앞서기도 한다.</p>
<p>그러나 생각을 좀 고쳐먹었다.</p>
<br/>

<blockquote>
<p><strong>&quot;틀린 내용도 사람들에게 공개해야 틀리다는 것을 알 수 있다.&quot;
&quot;공개의 두려움보단 발전을 위하는 마음으로&quot;</strong></p>
</blockquote>
<p>회사라는 작은 공간에서도 누군가에게 내가 코드를 작성한 이유, 기술을 사용한 이유를 공유하지 않으면 틀렸는지 맞았는지 알 수 없다.</p>
<p>그래서 코드 리뷰도 하고, 같이 회의도 하면서 안정적으로 서비스를 만들어나가는 것이다.</p>
<p>이것을 조금 확장한다 생각한다면, 결국 <strong>발표도 나의 코드를 리뷰받을 수 있는 환경</strong>이 되는 것이다.</p>
<p>이제 계속 좋은 내용을 발표하는 개발자가 되어야겠다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin - Flow Back Pressure]]></title>
            <link>https://velog.io/@moony_-/Kotlin-Flow-Back-Pressure</link>
            <guid>https://velog.io/@moony_-/Kotlin-Flow-Back-Pressure</guid>
            <pubDate>Thu, 20 Feb 2025 17:53:04 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<blockquote>
<p><strong>A: &quot;Flow에서 back pressure현상이 일어났을 때 어떻게 처리해야하나요?&quot;</strong>
<strong>나: &quot;...그게 뭐에요...?&quot;</strong></p>
</blockquote>
<p>어디선가 이런 질문을 받았는데, back pressure라는 키워드를 들어 본 적도 없어서 대답하지 못했다.</p>
<p>질문 해주셨던 분이 대략적으로 설명을 해 주셔서 그나마 나의 생각을 말했는데...</p>
<p>긴장해서 잘 기억은 안나지만, <strong>&quot;SharedFlow에 버퍼 있으니 버퍼를 이용해서 오래된 것들을 버리면 되지 않을까요...?&quot;</strong>라는 애매한 대답을 한 것 같다. <del>(근데 또 대충 맞았던 것도 신기하다.)</del></p>
<p>Back Pressure에 대한 트러블 경험이 없어서 오히려 아무 생각 없이 개발했다는 생각이 든다.</p>
<p>이번 글에서는 <strong>데이터의 발행 속도가 소비 속도보다 빠를 때 생기는 문제</strong>인 <strong>Back Pressure</strong>에 대해 알아보자!</p>
<br/>

<br/>

<h2 id="back-pressure">Back Pressure</h2>
<p><code>Back Pressure</code>는 데이터의 소비 속도가 공급 속도보다 느릴 때 일어나는 현상이다. 데이터가 소비되지 않고 계속 <strong>뒤에서 쌓이게 되면</strong> 데이터가 처리되지 않아 <strong>OutOfMemory</strong>가 일어날 수 있다.</p>
<p>아래의 <strong>예시 코드</strong>를 보자.</p>
<pre><code class="language-kotlin">val backPressureFlow = flow {
    while(true) {
        delay(100L) // 100ms 간격으로 방출
        emit(&quot;emit data&quot;)
    }
}

launch {
    backPressureFlow.collect {
        delay(1000L)// 1000ms 간격으로 소비
        println(it)
    }
}</code></pre>
<br/>

<h4 id="데이터의-발행">데이터의 발행</h4>
<p>먼저 데이터의 발행을 담당하는 <code>backPressureFlow</code>는 100ms 주기로 무한히 <code>String</code>데이터를 발행하고 있다.</p>
<p><strong>Android App</strong>으로 예시로 들자면 앱을 켜놓는 동안에 주기적으로 데이터를 방출하거나 갱신시키는 정도로 생각할 수 있다.</p>
<p>이는 평소에도 잘 일어날 수 있는 시나리오인 <strong>&quot;앱 오래 켜두기&quot;</strong>로 볼 수 있다.</p>
<br/>

<h4 id="데이터의-소비">데이터의 소비</h4>
<p>데이터를 소비하는 쪽은 <code>collect</code>로 소비를 하고 있다.</p>
<p>그리고 소비의 주기는 <strong>1000ms</strong>간격이다. 1초에 한 번씩 처리를 하고 있는 셈이다.
이는 <strong>데이터의 발행</strong>보다 10배는 느린 상황이다.</p>
<p><strong>Android App</strong>을 예시로 보자면, 데이터를 통해 화면을 그리는 행위에 해당할 수 있다.
보통 데이터를 생성해 발행하는 것 보다 데이터로 화면을 그리는 행위가 더 오래걸릴 것으로 예상한다.</p>
<br/>

<br/>

<h2 id="문제점">문제점</h2>
<p>문제점은 <strong>데이터의 발행</strong>보다 <strong>데이터의 소비</strong>가 10배는 느리다는 것이다.</p>
<p>데이터를 발행하는 쪽에서는 0.1초 마다 데이터를 발행하지만, 소비하는 쪽은 1초에 하나씩 소비하기 때문에 
<strong>1초 간격으로 9개의 데이터가 쌓이게 된다.</strong> </p>
<p>이런 상황에서 <strong>앱을 오래 켜두게 되면</strong> 1초에 9개씩 계속 데이터가 쌓이게 때문에 언젠가 <strong>OOM(OutOfMemory) Exception</strong>으로 앱이 비정상 종료될 수 있다.</p>
<br/>

<br/>

<h2 id="해결-방법">해결 방법</h2>
<blockquote>
<p>이 문제의 해결 방법은 매우 가까이에 있었다. 그리고 다들 한번씩은 사용해보았을 것이다.</p>
</blockquote>
<p>여기서 키워드는 <strong>다량의 데이터</strong>를 잘못 소비하면 <strong>OOM</strong>이 발생한다는 점인데, 이와 똑같은 문제를 해결하기 위해 사용하는 방법이 있다. 바로 <strong>파일을 읽어올 때 쓰는 방식</strong>이다.</p>
<br/>


<h3 id="파일-읽기">파일 읽기?</h3>
<p>파일을 읽어올 때, 한번에 읽어올 수 있다. 아래처럼 <code>readText()</code>를 사용하면 파일의 텍스트를 한번에 읽어오게 된다.</p>
<pre><code class="language-kotlin">fun main() {
    val file = File(&quot;file/path&quot;)
    println(file.readText())
}</code></pre>
<br/>


<p>하지만 이는 파일의 크기가 작을 때만 가능하다. 만약 한번에 읽어올 파일의 크기가 크다면 메모리 영역을 가득 채울 것이고, <strong>OOM</strong>을 발생시켜 프로세스가 종료될 것이다.</p>
<p>파일의 크기가 얼마나 될 지는 보통 알 수 없기 때문에 한번에 불러오지 않고, <code>Buffer</code>를 두어 그 크기 만큼 나눠 읽는 경우가 일반적이다.</p>
<pre><code class="language-kotlin">fun main() {
    val file = File(&quot;file/path&quot;)
    file.bufferedReader().use { reader -&gt;
        reader.forEachLine {
            println(it)
        }
    }
}</code></pre>
<br/>


<p>만약 읽어올 단위를 정하고 싶다면 <code>buffueredReader()</code>에 값을 정하면 된다.</p>
<pre><code class="language-kotlin">fun main() {
    val file = File(&quot;file/path&quot;)
    file.bufferedReader(bufferSize = 512).use { reader -&gt;
        reader.forEachLine {
            println(it)
        }
    }
}</code></pre>
<br/>

<p>위의 방식에 <code>buffer</code>를 넘는 데이터에 대해 처리하는 적절한 정책까지 정해진다면, <strong>Flow</strong>에도 적용시킬 수 있을 것이다!</p>
<br/>

<br/>

<h3 id="buffer를-통한-back-pressure-해결">Buffer를 통한 Back Pressure 해결</h3>
<p><strong>Flow</strong>에도 <code>buffer()</code>라는 Extension이 존재한다. 그래서 <strong>얼마만큼의 데이터만을 다룰 것인지 결정</strong>할 수 있다.</p>
<pre><code class="language-kotlin">val backPressureFlow = flow {
    while (true) { //매우 많은 양의 데이터 방출
        delay(100L) // 100ms 간격으로 방출
        emit(&quot;emit data&quot;)
    }
}

//...중략

launch {
    backPressureFlow.buffer(capacity = 100).collect { //최대 100개만 처리하자.
        delay(1000L)// 1000ms 간격으로 소비
        println(it)
    }
}</code></pre>
<br/>

<p><code>backPressureFlow</code>에 <code>buffer</code>라는 Extension을 달아주고, <code>capacity</code>를 100으로 설정하였다.</p>
<p>이렇게 하면 버퍼 값인 100개 만큼의 데이터만 다루게 된다.</p>
<br/>

<br/>

<p>만약 버퍼 값이 가득 찬 상태에서 데이터가 발행되면 어떻게 될까?</p>
<p>이럴 때 사용할 수 있는 정책이 3가지가 있는데, 코드를 직접 살펴보면 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/cd99522b-8eca-4916-805f-c385eaeb17df/image.png" alt=""></p>
<ul>
<li><strong>SUSPEND</strong>: buffer의 값이 가득 찬 상태에서 새로운 값이 들어올 경우, 발행을 잠시 중단(SUSPEND)한다.</li>
<li><strong>DROP_OLDEST</strong>: buffer의 값이 가득 찬 상태에서 새로운 값이 들어올 경우, 가장 오래된 값을 버리고, 새로운 값을 buffer에 저장한다.</li>
<li><strong>DROP_LATEST</strong>: buffer의 값이 가득 찬 상태에서 새로운 값이 들어올 경우, 가장 최신 값을 버리고, 새로운 값을 buffer에 저장한다.</li>
</ul>
<br/>


<p>위 세 개가 buffer가 가득찼을 경우 설정할 수 있는 정책이다.</p>
<p>만약 최신 값을 반영하고 싶다면 <code>DROP_OLDEST</code>를 아래와 같이 설정하면 된다...!!</p>
<pre><code class="language-kotlin">val backPressureFlow = flow {
    while (true) { //매우 많은 양의 데이터 방출
        delay(100L) // 100ms 간격으로 방출
        emit(&quot;emit data&quot;)
    }
}

//...중략

launch {
//오래된 값 버리기
    backPressureFlow.buffer(capacity = 100, onBufferOverflow = BufferOverflow.DROP_OLDEST) 
        .collect {
            delay(1000L)// 1000ms 간격으로 소비
            println(it)
        }
}</code></pre>
<blockquote>
<p>어디서 많이 보지 않았나...?</p>
</blockquote>
<p>그렇다.
SharedFlow에서도 위와 같이 버퍼와 버퍼를 비우는 정책을 설정할 수 있다...!</p>
<pre><code class="language-kotlin">val sharedFlow = MutableSharedFlow&lt;Int&gt;(extraBufferCapacity = 100, onBufferOverflow = BufferOverflow.DROP_OLDEST)</code></pre>
<p><del>(내가 완전히 틀린 대답을 한 것이 아니었다.)</del></p>
<br/>


<p>이렇게 버퍼를 두고 값을 버리거나 발행을 중지하면서 <strong>Back Pressure</strong>를 예방할 수 있다.</p>
<br/>


<br/>


<h3 id="collectlatest를-통한-back-pressure-해결">collectLatest를 통한 Back Pressure 해결</h3>
<p><code>collect</code>는 값을 소비하고 lambda 안에 있는 코드를 반드시 실행시킨다.
이는 실행하는 코드가 매우 느릴 경우(앞선 코드의 <code>delay</code>) <strong>Back Pressure</strong>를 발생시키는 원인이 된다.</p>
<p><code>collectLatest</code>는 새로운 값이 발행되었을 때, 이전 작업을 취소시키고 현재 방출된 값으로 다시 코드를 실행시킨다.</p>
<pre><code class="language-kotlin">val backPressureFlow = flow {
    while (true) { //매우 많은 양의 데이터 방출
        delay(100L) // 100ms 간격으로 방출
        emit(&quot;emit data&quot;)
    }
}

//...중략

launch {
    backPressureFlow.collectLatest { //새로운 값이 들어오면 이전 작업을 취소시킨다.
        delay(1000L)// 1000ms 간격으로 소비
        println(it)
    }
}</code></pre>
<br/>


<p>매우 간단하게 <strong>Back Pressure</strong>를 해결할 수 있다!</p>
<br/>

<br/>


<h3 id="collectlatest-vs-collect--buffer">collectLatest VS collect + buffer</h3>
<p><strong>Back Pressure</strong>를 해결하는 두 가지 방법을 보았는데
두 코드를 비교해 보면 <code>collectLatest</code>가 가장 간단하게 보인다. 이것만 쓰면 될까?</p>
<blockquote>
<p>상황에 따라 다르다고 할 수 있다.</p>
</blockquote>
<p><code>collectLatest</code>는 이전 작업을 취소시키기 때문에 곤란한 경우가 있을 수 있다.</p>
<p>&quot;데이터가 들어온 이상 무조건 실행&quot;해야 하고 <strong>Back Pressure</strong>를 예방하려면 <code>buffer</code> 방법을, &quot;취소되어도 괜찮은 코드&quot;라면 <code>collectLatest</code>가 좋은 것 같다.</p>
<ul>
<li>데이터가 들어올 경우 무조건 실행: <code>buffer</code></li>
<li>취소되어도 괜찮은 실행: <code>collectLatest</code></li>
</ul>
<br/>

<br/>


<h3 id="의문점-collectlatest는-그럼-hot-flow처럼-동작하나">의문점: collectLatest는 그럼 Hot Flow처럼 동작하나?</h3>
<blockquote>
<p>답은 <strong>X</strong> 이다.</p>
</blockquote>
<p>이전 작업은 취소시키고 <strong>&quot;항상 최신 값만 반영&quot;</strong>한다는 특성때문에 자칫 오해하기 쉽지만, <code>collectLatest</code>를 쓴다고 <strong>Hot Flow</strong>가 되는 것은 아니다.
(하지만 매우 유사해지긴 한다.)</p>
<p><strong>Cold Flow</strong>에서 <code>collectLatest</code>를 사용하는 것과 <strong>Hot Flow</strong>에서 <code>collectLatest</code>를 사용하는 것의 차이점들을 정리해 보면 아래와 같다.</p>
<ul>
<li>새로운 구독자가 생겼을 때 <strong>Cold Flow</strong>는 첫 번째 값 부터 <strong>취소되어가며</strong> 처리, <strong>Hot Flow</strong>는 최신 값만 처리한다.</li>
<li>구독자가 없다면 <strong>Cold Flow</strong>는 값을 방출하지 않기 때문에 <strong>어차피 취소될 데이터</strong>가 쌓이지만, <strong>Hot Flow</strong>는 최신 값만 유지하기 때문에 <strong>취소될 데이터</strong>가 없다.</li>
</ul>
<br/>

<br/>


<h2 id="마치며">마치며</h2>
<blockquote>
<p>Android에서는 UI 업데이트에 <code>collectLatest</code>를 사용할 것을 권장하고 있다.</p>
</blockquote>
<p>UI를 업데이트 할 때에 성능을 위해 이전 값을 취소시키는 <code>collectLatest</code>가 적절한 것 같다.</p>
<p>하지만 꼭 실행시켜야 하는 UI 업데이트 작업이 있거나, 무한히 방출되는 값에 의해 계속 취소되는 상황이라면 <code>collect</code>와 <code>buffer</code>, <code>conflate</code>와 같이 쓰는 방법으로<code>collectLatest</code>를 대체할 수 있을 것이다.</p>
<p>이제 두 가지를 잘 알았으니 적절한 곳에 사용해보아야 겠다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CS 다시 공부하기 - CS는  왜 필요할까?]]></title>
            <link>https://velog.io/@moony_-/CS%EB%8A%94-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@moony_-/CS%EB%8A%94-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Tue, 11 Feb 2025 09:10:47 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<blockquote>
<p><strong>&quot;진짜 CS 마스터 해야지...&quot;</strong>
<strong>&quot;아 다음엔 진짜 날 잡아서 CS 공부 빡세게 한다...&quot;</strong></p>
</blockquote>
<p>매번 이렇게 다짐하지만 막상 CS 공부하는게 쉽지 않다.</p>
<p>안드로이드 개발 공부할 것도 너무 많은데...
<img src="https://velog.velcdn.com/images/moony_-/post/74908f39-22e9-4134-928b-2d6a7a261035/image.jpeg" alt="">
(가입해야 하나...)</p>
<blockquote>
<p>CS가 실제 개발에서 중요할까?</p>
</blockquote>
<p>난 안드로이드 개발하면서 알고리즘, OS는 조금 필요하다는 것을 느낀다.</p>
<p>RecyclerView에 표현할 리스트를 다룰 때는 삽입 정렬, 해쉬를 고려하는 경우가 많고,</p>
<p>아주 가끔 <strong>&quot;앱이 언제 꺼질까?&quot;</strong>를 생각할 때, OS 책에서 배운 프로세스와 메모리 구조를 떠올리긴 한다.</p>
<br/>

<h3 id="하지만">하지만..</h3>
<p>이 상황 외에는 CS지식이 필요한 적은 없었던 것 같다.</p>
<p>특히 공룡책과 같은 수많은 페이지의 전공 서적을 볼 때면, &quot;이정도 까지 필요하나?&quot;라는 생각이 많이 든다.</p>
<p>또 안드로이드에선 네트워크 지식이 필요한 경우가 거의 없어 매번 했갈리고 까먹고는 한다.</p>
<blockquote>
<p>그럼에도 많은 기업들은 CS 지식을 물어보곤 한다.</p>
</blockquote>
<br/>


<p>사실 나도 CS를 공부하고 나서 까먹을 뿐이지, 다시 공부하다 보면 내가 작성한 코드들에는 CS지식이 기반이 되는 것들이 많다는 것을 느낀다.</p>
<p>그 만큼 작은 부분 곳곳에 CS지식이 들어가곤 한다.</p>
<p>그래서 이번 기회에 다시 한번 <strong>왜 우리는 CS를 공부해야 하는가</strong>에 대해 알아보자!!</p>
<blockquote>
<p><strong>&quot;기초가 탄탄해진다.&quot;</strong>
<strong>&quot;최적화할 수 있다.&quot;</strong>
<br/>이런 뻔한 것들 말고, <strong>나의 경험</strong>을 토대로!</p>
</blockquote>
<br/>

<br/>

<h2 id="짧고-효율적인-코드를-작성할-수-있다">짧고 효율적인 코드를 작성할 수 있다.</h2>
<blockquote>
<p>가게에 재료가 없거나, 가게 자리가 없거나, 영업일이 아니라면 <strong>&quot;예약 불가합니다.&quot;</strong>를, 그 반대라면 <strong>&quot;예약 시도중...&quot;</strong>라는 문구를 띄워 주세요.</p>
</blockquote>
<p>위의 요구사항을 구현하려 한다.</p>
<p>가장 먼저 떠오르는 것은 위의 말을 그대로 코드로 옮겨 적는 것이다. 가장 빠르고 쉽게 떠올릴 수 있는 방법이다.</p>
<pre><code class="language-kotlin">if(!isStoreOpen &amp;&amp; !isIngredientExist &amp;&amp; !isTableExist) {
    println(&quot;예약이 불가능합니다.&quot;)
}else{
    println(&quot;예약 시도중...&quot;)
}</code></pre>
<p>위의 코드는 문제가 없지만 단점이 있는데, <code>!(NOT)</code>이 3번이나 들어가 읽기 불편하다는 것이다.</p>
<p><code>!(NOT)</code>은 정말 좋은 도구이지만, 변수의 앞에 적어야 한다.
그렇기 때문에 변수를 읽고 눈을 다시 앞으로 되돌려야 한다.
<del>(NOT이 한국식으로 뒤에 왔다면 더 읽기 편했을 것이다.)</del></p>
<p>읽는 과정을 보자면 아래와 같다.</p>
<ul>
<li>isStoreOpen의 의미를 이해한다. -&gt; <code>!(NOT)</code>이 <strong>앞에 있으니 다시 앞으로 눈길을 옮긴다.</strong> -&gt; isStoreOpen을 반대로 이해한다. -&gt; 다음 변수를 읽기 위해 isStoreOpen을 뛰어 넘어 다음 변수를 읽는다.</li>
</ul>
<p>코드의 조건을 이해하려면 위의 과정을 3번 반복한 후, AND까지 시켜야 한다. 이는 컴퓨터도 마찬가지(NOT 연산을 3번 함)여서 성능에 조금 영향을 미친다.</p>
<br/>

<p><strong>CS</strong>를 공부했다면 이를 해소시킬 수 있는 방법들이 몇 가지 있다.</p>
<br/>


<h3 id="드모르간-법칙-이용하기">드모르간 법칙 이용하기</h3>
<p><strong>드모르간의 법칙</strong>을 이용하면 <code>!(NOT)</code>을 한 개로 줄일 수 있다.</p>
<pre><code class="language-kotlin">//드모르간의 법칙으로 바꾸기!
if (!(isStoreOpen || isIngredientExist || isTableExist)) {
    println(&quot;예약이 불가능합니다.&quot;)
} else {
    println(&quot;예약 시도중...&quot;)
}</code></pre>
<p>공통으로 들어가는 <code>!(NOT)</code>을 앞으로 뺀 후, 괄호로 묶고 모든 <code>AND</code>를 <code>OR</code>로 바꾸면 된다.</p>
<p><code>!(NOT)</code>이 3개에서 하나로 줄었기 때문에 읽는 과정이 줄어들고 더욱 빠르게 조건을 파악할 수 있을 것이다.</p>
<br/>


<h3 id="else의-원리를-이용하기">else의 원리를 이용하기</h3>
<p>내가 가장 <strong>애용하는 방식</strong>이다.</p>
<p><code>else</code>는 기본적으로 <code>!(NOT)</code>의 의미를 내포하고 있다.</p>
<p>그렇기 때문에 중괄호 안의 코드 순서를 바꾼 후, <strong>NOT을 추가하거나 빼는 방법</strong>으로 코드를 더욱 이해하기 쉽게 바꿀 수 있다.</p>
<p>위의 <strong>드모르간의 법칙</strong>을 사용한 코드에서 더욱 줄일 수 있게 되는 것이다!</p>
<pre><code class="language-kotlin">if(isStoreOpen || isIngredientExist || isTableExist){
    println(&quot;예약 시도중...&quot;)
} else{
    println(&quot;예약이 불가능합니다.&quot;)
}</code></pre>
<p><del>(예시 조건이 좀 이상하지만 넘어가자...)</del></p>
<p><strong>if, else</strong>의 중괄호에 넣는 결과 코드를 뒤집으면 <strong>NOT을 하나 더 줄일 수 있다.</strong>
처음 코드와 비교하면 읽기 너무너무 편리해 졌다.</p>
<br/>


<h3 id="객체지향을-이용하기">객체지향을 이용하기</h3>
<p>만약 다른 곳에서 계산을 끝마쳤는데, 다시 구분해서 출력을 해야 하는 상황이 있을 수 있다. 아래와 같이 이미 계산을 끝마친 후, 다른 곳에서 사용한다고 가정해 보자.</p>
<pre><code class="language-kotlin">fun canReserve(): Boolean {
    if (isStoreOpen || isIngredientExist || isTableExist) {
        return true
    } else {
        return false
    }
}

fun printMessage() {
    if (canReserve()) {
        println(&quot;예약이 불가능합니다.&quot;)
    } else {
        println(&quot;예약 시도중...&quot;)
    }
}</code></pre>
<p>이렇게 되면 또 한번 <code>canReserve</code>의 결과 값을 <strong>if</strong>에 넣어 판단해야 한다.</p>
<p>하지만 하나의 객체를 선언한 후, 상속받는 여러 구현체를 이용한다면 불필요한 if를 줄일 수 있다.</p>
<h4 id="객체-선언">객체 선언</h4>
<pre><code class="language-kotlin">interface PrintReserveUseCase{
    fun print()
}

class CanReserveUseCase:PrintReserveUseCase{
    override fun print() {
        println(&quot;예약 시도중...&quot;)
    }
}

class CanNotReserveUseCase:PrintReserveUseCase{
    override fun print() {
        println(&quot;예약이 불가능합니다.&quot;)
    }
}</code></pre>
<h4 id="객체-사용">객체 사용</h4>
<pre><code class="language-kotlin">fun canReserve(): PrintReserveUseCase {
    if (isStoreOpen || isIngredientExist || isTableExist) {
        return CanReserveUseCase()
    } else {
        return CanNotReserveUseCase()
    }
}

fun printMessage() {
    canReserve().print()
}</code></pre>
<p><strong>if</strong>를 사용하지 않고 <strong>추상화</strong>된 객체를 이용, <code>print</code> 함수를 호출하게 되어 더욱 편리한 코드가 될 수 있다.</p>
<p>이렇게 <strong>객체지향</strong>의 원리를 이용한 코드를 작성하면 추후에 여러 조건들이 추가되었을 때 더욱 빛을 발할 것이다.</p>
<br/>


<h3 id="극한의-성능-추구하기">극한의 성능 추구하기</h3>
<p>만약 극한으로 성능을 생각해야 하는 로봇, 임베디드 분야라면 <strong>비트 마스크</strong>를 통해 코드의 양을 줄이고 성능을 올릴 수 있다.</p>
<p>주의할 점은 <strong>&quot;프로그래머 끼리 약속&quot;</strong>되어있어야 한다는 점이다.</p>
<blockquote>
<p>1의 자리 수는 조건 <strong>A</strong>, 2의 자리 수는 조건 <strong>B</strong>, 4의 자리 수는 조건 <strong>C</strong>를 나타낸다.</p>
</blockquote>
<p>위의 가정대로 프로그래머 끼리 약속했다면, 동시에 만족하는 수를 빠르게 찾을 수 있다.</p>
<ul>
<li><strong>a, c만 만족하는 경우를 찾는다.</strong>: <code>(maskValue and 0b101)!=0</code></li>
<li><strong>b, c만 만족하는 경우를 찾는다.</strong>: <code>(maskValue and 0b110)!=0</code></li>
<li><strong>a, b만 만족하는 경우를 찾는다.</strong>: <code>(maskValue and 0b011)!=0</code></li>
</ul>
<br/>

<blockquote>
<p><strong>&quot;월, 수, 금 중에 한 요일이라도 영업하는 가게를 필터링해서 보여주세요&quot;</strong></p>
</blockquote>
<p>실제 내가 앱을 개발하면서 받았던 요구사항인데, <strong>비트마스크</strong>를 통해 해결한 경험이 있다.</p>
<br/>

<p>먼저 <code>월화수목금토일</code>을 자리수에 맞게 비트로 변환한다.</p>
<p>예시를 몇가지 두자면 아래처럼 요일이 비트로 바뀌게 된다.</p>
<ul>
<li><strong>&quot;월수금&quot; -&gt; 1010100</strong></li>
<li><strong>&quot;화수목일&quot; -&gt; 0111001</strong></li>
<li><strong>&quot;월화목금토&quot; -&gt; 1101110</strong></li>
</ul>
<p>이제 원래 조건인 월수금을 <strong>비트로 바꾼 후</strong> 논리곱 <code>AND</code>를 하면 조건에 맞는 가게를 쉽게 찾을 수 있다.</p>
<pre><code class="language-kotlin">data class Store(
    //...생략
    val dayOfWeekMask: Int
    //...생략
)

fun filterStore(storeList: List&lt;Store&gt;) {
    //월, 수, 금 비트 마스크
    val condition = 0b1010100

    //월, 수, 금 중에 하나라도 없으면 제거
    storeList.filterNot { (it.dayOfWeekMask and condition) == 0 }
}</code></pre>
<p>이렇게 하면 여러 조건 중에 동시에 만족할 수 있는 조건들을 빠르게 찾을 수 있다.</p>
<p>이를 이용하여, 위의 가게 예시를 구현한다면 아래와 같이 할 수 있을 것이다.</p>
<pre><code class="language-kotlin">//condition이 가게 상태의 비트마스크라면...
if (condition != 0) {
    println(&quot;예약 시도중...&quot;)
} else {
    println(&quot;예약이 불가능합니다.&quot;)
}</code></pre>
<p>약속이 미리 되어있기 때문에, 코드를 파악하는데 문제가 되지 않고 더욱 짧아졌다.</p>
<p>또한 <strong>단 한번의 비교연산으로 조건을 탐색할 수 있다.</strong></p>
<br/>

<p> 이처럼 CS는 코드의 성능을 높이고, 간결하고 읽기 쉽게 만들어준다.</p>
<br/>

<br/>

<h2 id="새로운-기술을-판단할-수-있게-된다">새로운 기술을 판단할 수 있게 된다.</h2>
<blockquote>
<p>예전 네이버 신입 공채에서는 <strong>&quot;트렌드에 휘말리지 않는 탄탄한 기본기&quot;</strong>가 중요하다고 말했다.</p>
</blockquote>
<p>트랜드, 정말 많이 바뀐다.</p>
<p><strong>Android</strong>의 <code>Jetpack Compose</code>, <strong>iOS</strong>의 <code>Swift UI</code>처럼 <strong>명령형 UI</strong>에서 <strong>선언형 UI</strong>로 많이 바뀌고 있고, 이에 따라 <strong>MVVM</strong>에서 <strong>MVI</strong>를 고려해야 하는 경우도 생긴다.</p>
<p>더 큰 <code>FrameWork</code> 관점에서는 <code>Flutter</code>, <code>ReactNative</code>의 파이가 점점 커져 &quot;다시 공부해야 하나&quot;하는 마음도 든다.</p>
<p>개발자들도 이런데, 개발자가 아닌 사람들이 개발자를 구할 때는 더욱 곤란할 것이다.</p>
<br/>


<blockquote>
<p><strong>&quot;앱을 만들어 사업하려고 해요. 어떤 개발자를 구해서 앱을 만들어야 할까요?&quot;</strong></p>
</blockquote>
<p>지인 중에 사업을 하고 계신 분이 있는데, 이런 질문을 받은 적이 있다.</p>
<p>어떻게 대답해야 할까 잠시 고민을 하다가 대답했다.</p>
<br/>


<blockquote>
<p><strong>&quot;어떤 앱을 만드시는데요? 비용은 얼마나 가지고 있죠?&quot;</strong></p>
</blockquote>
<p>앱을 만드는 방법은 <code>Android</code>, <code>iOS</code> 네이티브 이외에도 생각보다 많다.</p>
<p>비용과 개발 속도를 생각한다면 <code>Flutter</code>, <code>ReactNative</code>도 아주 좋은 선택지가 될 수 있고, 심지어 <code>Unity</code>, <code>Unreal</code>로 게임 뿐 아니라 네이티브 못지 않은 <strong>상용 앱</strong>을 만들 수 있다.
(크로스 플랫폼 처럼 하나의 코드로 <code>Android</code>, <code>iOS</code> 둘다 빌드 까지 가능하다.)</p>
<p>또한 <code>Kotlin MultiPlatform</code>으로도 만들 수 있으며
이미 지원 종료되었지만 저 질문을 받았을 때는 <code>Xamarin</code>으로도 앱을 만들 수 있었다.</p>
<br/>

<p>이렇게 수많은 프레임워크 중에서 좋은 대답을 하려면 각각의 플랫폼을 사용해본 경험과 장, 단점을 알아야 한다.
이런 경험과 장단점을 자세히, 모두 알고 있는 개발자는 얼마나 될까...
<del>(하나 공부하기도 벅차다)</del></p>
<p>하지만 모두 경험 하지 않아도, 판단할 수 있는 방법이 있다.</p>
<p>공통점. <strong>CS 지식</strong>이다.</p>
<br/>


<blockquote>
<p>위 플랫폼들의 공통점은 <strong>사용자에게 보여주는 그래픽</strong>이 있다는 것이다.</p>
</blockquote>
<p>그럼 판단할 수 있는 기준이 한 가지 생기게 되는데, <strong>어떤 그래픽을 사용자에게 보여 줘서 서비스를 사용하게 만들지</strong>를 들으면 된다.</p>
<br/>


<h3 id="3d로-보여줘야-하는-앱이에요">&quot;3D로 보여줘야 하는 앱이에요.&quot;</h3>
<p>3D 그래픽을 사용하는 앱이라면 <code>Unity</code>, <code>Unreal</code>을 추천하겠다. </p>
<p><code>Android</code>, <code>Flutter</code>에서 사용하는 <code>Skia Engine</code>은 기본적으로 2D 그래픽 엔진이어서 3D를 구현하기 쉽지 않다.</p>
<p><code>Unity</code>, <code>Unreal</code>은 3D로 개발하기 쉽고, 빌드 설정에서 <code>OpenGL</code>, <code>DirectX</code>로 설정하여 플랫폼에 최적화된 그래픽 라이브러리를 직접 설정할 수 있다.
또 설정을 통해 똑같은 코드로 <code>Android</code>, <code>iOS</code>로 빌드 가능해서 여러 플랫폼을 지원하기 쉽다.</p>
<br/>


<h3 id="2d인데-화려한-애니메이션이-들어가요">&quot;2D인데 화려한 애니메이션이 들어가요.&quot;</h3>
<p>이렇다면 두 가지중에 선택해서 말할 것 같다.</p>
<ul>
<li><p>물리적인 계산이 들어가는 <strong>엔터테인먼트</strong>가 있다면 <code>Unreal</code>, <code>Unity</code>를 추천한다. 3D 뿐만 아니라 2D도 쉽게 지원할 수 있고, 물리 계산이 편하다.</p>
</li>
<li><p>물리적인 계산이 없고, 상용앱이라면 <code>Flutter</code>, <code>KMP</code>를 추천한다.
  플랫폼에 매칭되는 UI Component로 변환시키는 <code>ReactNative</code>에 반해 <code>Flutter</code>는 <code>Skia Engine</code>, <code>KMP</code>는 각각의 그래픽 엔진을 사용하여 픽셀을 직접 그리기 때문에 성능이 좋다.</p>
</li>
</ul>
<br/>


<h3 id="상용-앱인데-화려한-애니메이션은-없고-숙련된-개발자를-구하고-싶어요">&quot;상용 앱인데 화려한 애니메이션은 없고, 숙련된 개발자를 구하고 싶어요.&quot;</h3>
<p>그럼 <code>ReactNative</code>를 추천하겠다.</p>
<p><code>JavaScript</code>의 개발자 풀은 굉장히 많고, 또 뛰어난 개발자들이 많다.
그리고 <code>ReactNative</code>로 빠르게 좋은 앱을 만들 수 있다. 이는 개발비용 측에서 좋은 효율을 보여줄 것이다.</p>
<h3 id="상용-앱인데-화려한-애니메이션이-들어가요">&quot;상용 앱인데, 화려한 애니메이션이 들어가요.&quot;</h3>
<p>그럼 <code>Flutter</code>를 추천한다.</p>
<p><code>Skia Engine</code>으로 픽셀 하나하나 직접 그리기 때문에 화려한 애니메이션을 성능 좋게 구현할 수 있다.</p>
<br/>


<h3 id="네이티브-기술을-사용하는-앱을-만들어야-해요">&quot;네이티브 기술을 사용하는 앱을 만들어야 해요&quot;</h3>
<p><code>Android</code>, <code>iOS</code> 개발자를 추천하면 된다.</p>
<br/>


<p>위 처럼 기본적인 CS지식이 바탕이 된다면 여러 기술을 판단할 수 있게 된다.</p>
<blockquote>
<p>개발자에게는 어떻게 도움이 될까?</p>
</blockquote>
<p>요즘 공고를 보면, <code>Flutter</code>, <code>React Native</code> 개발자를 찾는 공고를 많이 볼 수 있다.</p>
<p>그만큼 멀티플랫폼 는 굉장히 많은 발전을 하고 있으며 사용중인 개발자들, 그리고 예시 코드들이 많이 늘어나고 있다.</p>
<p>만약 시간이 지난 후.. 갑자기 구글에서 이렇게 발표를 한다면...?</p>
<blockquote>
<p><strong><em>&quot;Android의 모든 기능은 이제 Flutter로 구현할 수 있게 되었습니다. 따라서 Android는 이제 지원하지 않겠습니다.&quot;</em></strong></p>
</blockquote>
<p>난 노숙자가 되지 않으려면, 어떤 개발자로 전향해야 할 지 판단해야 할 것이다...</p>
<p><strong>이 때 CS 지식이 어느 프레임워크를 새로 공부할 지 기준점이 될 수 있다.</strong></p>
<br/>


<blockquote>
<p>일례로 <strong>iOS</strong>는 13 버전부터 <strong>OpenGL</strong>이 아닌 자체 그래픽 라이브러리 <strong>Metal</strong>을 사용하여 화면을 그리게 되었다.
(Android에는 <strong>Vulkan</strong>이라는 친구가 생겼다.)
<br/><strong>Flutter</strong>를 개발하는 개발자들은 <strong>OpenGL</strong>로 된 <strong>하나의 코드</strong>를 이용해서 여러 플랫폼을 지원하고 있었는데, <strong>iOS</strong>만 <strong>Metal</strong>로 개발해야 하는 상황이 온것이다.
<br/>이는 <strong>Google</strong>이 <strong>Flutter</strong>를 개발하는데 비용이 증가했다는 것으로 연결되며, <strong>Flutter</strong>나  <strong>ReactNative</strong>나 개발 유지비용이 똑같아(혹은 Flutter가 더 많아짐) 조금 불안해졌다는 것이다.<br/>
<del>(Google은 끄떡 없지만 혹...시 모르니까...)</del></p>
</blockquote>
<p>물론 프레임워크를 바꿔야 하는 상황은 잘 일어나지 않는다.</p>
<p>하지만 이보다 작은 단위인 <strong>라이브러리</strong>, <strong>API</strong>를 바꾸는 상황은 평소에도 자주 일어난다.</p>
<br/>


<blockquote>
<p>극단적인 상황이 아니더라도, 잘 사용했던 라이브러리가 <strong>Deprecated</strong> 되는 일은 많이 경험해 보았을 것이다.</p>
</blockquote>
<p>기존에 잘 사용했던 라이브러리가 치명적인 결함으로 갑자기 <strong>Deprecated</strong> 되었다면 새로운 라이브러리를 찾아야 한다.
<del>(안 그러면 서비스가 망한다)</del></p>
<p>이 때는 여러 검증되지 않은 라이브러리가 있어서 다른 개발자들도 뭘 사용해야 할 지 모르는 상황이 올 것이다.</p>
<p>이 때 <strong>CS 지식</strong>이 라이브러리를 선택하는데 도움을 줄 수 있다.</p>
<br/>

<p>여러 조건을 토대로 서칭해야 하는 라이브러리를 새로 찾아야 한다면 조건을 <strong>하나하나 비교하여 서칭</strong>하는 라이브러리와 <strong>비트마스크</strong>를 <strong>AND</strong>시켜 서칭하는 라이브러리의 성능은 다를 것이며, 이를 판단하기 위해서는 <strong>알고리즘</strong>을 열심히 공부해야 할 것이다.</p>
<blockquote>
<p>어떤 기술이 어떤 장단점을 가지고 있는 지 판단하는 근거는 <strong>CS 지식</strong>에서 나온다.</p>
</blockquote>
<br/>


<h2 id="회사에서-원한다">회사에서 원한다.</h2>
<p>현실적인 이유인데, 회사에서 다른 개발자가 되길 원하는 경우가 꽤 많은 것 같다.</p>
<blockquote>
<p><strong>&quot;원래 안드로이드 개발자가 아니었는데, 회사에서 원해서 안드로이드 개발자가 되었어요.&quot;</strong></p>
</blockquote>
<p><strong><a href="https://gdg.community.dev/events/details/google-gdg-korea-android-presents-devfest-android-in-korea-2024/">GDG DevFest</a></strong> 안드로이드 개발자 컨퍼런스에서 <strong>20년차가 넘은 개발자</strong>분께 들었던 이야기이다.</p>
<p>원래는 <code>C/C++</code> 개발자였는데, 회사에서 앱을 만들기를 원해 전향하게 되었고, 처음에는 당황스럽고 부담스러웠는데 지금은 만족한다고 하셨다.</p>
<p>또 내가 자주 보는 <strong>개발자 유튜버</strong>는 iOS 개발자로 취업했다가 백엔드 개발자가 되었다고 한다.</p>
<p>나 또한 회사에서 이런 이야기를 들은 적이 있다.</p>
<br/>


<blockquote>
<p><strong>&quot;문휘님, 혹시 플러터 가능하세요? 저희 한번 플러터 해볼까요?&quot;</strong></p>
</blockquote>
<p>처음에는 달갑지 않았다.
안드로이드 개발자인데 커리어가 꼬이게 되지 않을까 걱정도 많이 했었다.</p>
<p>하지만 회사에서 하라면 해야지... 수 개월동안 플러터를 개발했는데...</p>
<br/>


<blockquote>
<p><strong>&quot;Flutter는 신이야&quot;</strong></p>
</blockquote>
<p>너무 재밌었다.</p>
<p>처음에는 어색했던 <strong>선언형 UI</strong>도 생산성에서 놀랐고, <strong>Web, iOS, Android,</strong> 심지어는 <strong>데스크톱 앱</strong>까지 한 코드로 빌드되는 것에 신기함을 느꼈다.</p>
<p>또 Web에서는 기본인 <strong>반응형 UI</strong>로 앱을 개발하면 UI가 깨지지 않고 모든 환경에서 일관된 UI를 보여주었다.</p>
<p>그리고 언젠가는 내가 <strong>Android</strong> 개발자가 아닌 <strong>Flutter</strong> 개발자가 될 지도 모르겠다는 생각이 들었다.</p>
<br/>


<blockquote>
<p><strong>Flutter에서도 CS는 중요했다.</strong></p>
</blockquote>
<p>Flutter를 개발할 때 화면이 바뀌지 않는 문제가 있었는데, 이는 Widget의 Key가 바뀌지 않을 때 일어난다고 하였다.</p>
<p>그래서 Key를 데이터에 따라 수동으로 바꾸는 코드를 작성하면서, Flutter가 화면을 그리는 방식을 공부했다.</p>
<br/>


<blockquote>
<p><strong>&quot;Flutter는 화면을 그릴 때 Widget Tree를 DFS로 순회하며 Widget의 Key가 바뀐 것만 다시 그립니다.&quot;</strong></p>
</blockquote>
<p><strong>DFS</strong>는 <strong>깊이 우선 탐색</strong>이다.
그렇기 때문에 <strong>자신의 형제 노드</strong>를 먼저 탐색하는 것이 아닌 <strong>자신의 자식 노드</strong>를 먼저 탐색한다.</p>
<p><strong>Flutter</strong> 뿐만 아니라 <strong>Android</strong>에서도 <strong>DFS</strong>를 사용하는데, 그 이유는 부모의 크기가 정해 진 후, 그 크기 안에서 자식의 크기를 정해야 하기 때문이다.</p>
<br/>


<img src="https://velog.velcdn.com/images/moony_-/post/a01c8d7b-b082-4017-8c74-09323d6dcae6/image.png" width="1000" height="1000"/>

<br/>



<p><strong>BFS</strong>를 통해 <strong>A Widget</strong>보다 <strong>B Widget</strong>이 먼저 검사되어 크기가 정해졌다면, <strong>C Widget</strong>의 크기를 정할 때 다시 <strong>A Widget</strong>의 크기를 가져오는 등, 불편한 부분이 있을 것이다.</p>
<p><strong>DFS</strong>는 이런 과정이 자식부터 되기 때문에 크기 계산이 자연스럽다.</p>
<p>또한 <strong>DFS</strong> 순서로 그리게 되면 먼저 그려진 <strong>A, C Widget</strong>이 나중에 그려지는 <strong>B Widget</strong>에 의해 자연스럽게 가려지게 된다.</p>
<p>내가 만약 <strong>CS</strong> 알고리즘을 공부하지 않았다면 위의 말을 이해하지 못했을 것이다.</p>
<br/>

<blockquote>
<p>조금 딴길로 샜는데,
회사는 다른 기술로 개발할 때 또 <strong>새로운 직원을</strong> 뽑는 것 보다 <strong>기존 직원</strong>을 활용해서 서비스를 만들고 싶어한다. 그리고 CS가 탄탄하면 기술 스택 전환이 빠르다.
<br/>
-&gt; <strong>회사는 기본기가 탄탄한 개발자를 원한다!</strong></p>
</blockquote>
<p>회사는 새로운 직원을 뽑는 것 보다 기존 직원의 연봉을 높여 다른 기술을 개발하는 것이 <strong>시간적 측면</strong>에서도, <strong>비용적 측면에서도</strong> 효율적이다.</p>
<p>연차가 높은 개발자분들의 생생한 <strong>전환 경험담</strong>을 들은 만큼, 나도 <strong>CS공부로 탄탄히</strong> 준비해놓아야 한다.</p>
<br/>

<h2 id="마치며">마치며...</h2>
<p>글을 쓰다 보니 너무 <strong>알고리즘</strong>에만 치우친 느낌이 든다.</p>
<p>내가 백엔드 개발자가 아니라서 네트워크 부분이 부족한 것도 있고, 다른 개발자분들은 공감하지 못할 것도 많을 것 같다.</p>
<p>그럼에도 최대한 <strong>나의 경험을 토대로</strong> 작성하려고 노력했다. 내가 더 많은 경험을 쌓게 되면 다른 상황에서 CS 지식을 이용한 문제 해결 경험을 적고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android Music 앱 만들기 (3) - ExoPlayer 커스텀하기]]></title>
            <link>https://velog.io/@moony_-/Android-Music-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-3-ExoPlayer-%EC%BB%A4%EC%8A%A4%ED%85%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@moony_-/Android-Music-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-3-ExoPlayer-%EC%BB%A4%EC%8A%A4%ED%85%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 06 Feb 2025 19:32:02 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>이전에는 <code>MediaLibraryService</code>를 이용하여 간단하게 음악을 실행해보았다.
이제 음악 플레이어를 만들기 위해 <code>ExoPlayer</code>를 커스텀 해야 한다.</p>
<p><code>ExoPlayer</code>는 <code>Player</code>라는 인터페이스를 상속 받아 구현하는 구현체이다.
그러니 사실상 <code>Player</code>에 기본적인 기능들은 다 들어가 있다.</p>
<p>그래서 똑같이 <code>Player</code>를 상속 받는 <code>MediaController</code>만으로 <code>ExoPlayer</code>의 기능들을 모두 사용할 수 있는 것이다.</p>
<p>그러니 <strong>&quot;ExoPlayer 커스텀하기&quot;</strong>보다는 <strong>&quot;Player 커스텀하기&quot;</strong>가 좀 더 맞는 것 같다.</p>
<p>이제 <code>Player</code>를 커스텀 해 보자.</p>
<h3 id="interface-작성">Interface 작성</h3>
<p>내가 만들고 싶은 기능을 먼저 Interface로 작성하고,  그 다음 상속받아 구현체를 작성하는 편이다.
SOLID 원칙 중에 <code>의존 역전 원칙</code>을 잘 지키는 방법이기도 하다.</p>
<p>그러니 일단 필요한 것들을 생각 없이 작성해 보자.</p>
<p>나는 아래와 같은 필드들이 필요하다고 생각해서 작성하였다.</p>
<pre><code class="language-kotlin">interface MediaPlayer {
    val isPlayingFlow: StateFlow&lt;Boolean&gt; //현재 음악 플레이 중인지
    val currentPositionFlow: Flow&lt;Long&gt; //현재 음악의 몇초를 플레이했는지
    val durationFlow: StateFlow&lt;Long&gt; //지금 음악의 총 길이는 몇 ms인지
    val currentMusicFlow: StateFlow&lt;Music?&gt; //현재 음악은 무엇인지
    val musicCountFlow: StateFlow&lt;Int&gt;//총 음악 갯수는 무엇인지
    val errorFlow: SharedFlow&lt;PlayerError&gt;//ExoPlayer 에러가 무엇인지
    val currentMusicIndexFlow: StateFlow&lt;Int&gt;//현재 음악의 index가 무엇인지
    val repeatModeFlow: StateFlow&lt;RepeatMode&gt;//현재 반복모드는 무엇인지
    val isShuffleFlow: StateFlow&lt;Boolean&gt;//셔플 모드인지
    fun play()//재생
    fun pause()//멈춤
    fun next()//다음
    fun previous()//이전
    fun seekTo(positionMillis: Long)//음악 재생바 넘김
    fun seekMusicByIndex(index: Int)//음악을 넘김
    fun addMusic(music: Music)//음악 추가
    fun addMusic(index: Int, music: Music)//음악 index에 추가
    fun addMusics(musics: List&lt;Music&gt;)//음악 여러개 추가
    fun addMusics(index: Int, musics: List&lt;Music&gt;)// 음악 여러개 index에 추가
    fun getMusic(index: Int): Music?//음악 가져오기
    fun setRepeatMode(repeatMode: RepeatMode)//반복모드 설정
    fun setShuffle(isShuffle: Boolean)// 셔플 모드 설정
}</code></pre>
<p>어떻게 구현할 지, 이게 구현 가능할 지 모르지만 서비스에 필요한 기능들을 모아 작성하였다.
<del>(다들 ExoPlayer로 뮤직 앱 잘만 만들고 있으니 아마 될것이다.)</del></p>
<p><strong>고수준 모듈</strong>이 완성되었으니, 이것을 상속받아 구현하는 <strong>저수준 모듈</strong>을 만들어 보자.</p>
<h3 id="playerlistener">Player.Listener</h3>
<p><code>Player</code>에는 여러가지 이벤트에 대해 CallBack을 받을 수 있도록 <code>Player.Listener</code>를 제공해 준다. </p>
<p><a href="https://developer.android.com/media/media3/exoplayer/listening-to-player-events">안드로이드 공식 문서</a></p>
<p>이 곳에서 보면 여러 Callback을 제공해주는 것을 볼 수 있는데, 우리의 Interface인 <code>MediaPlayer</code>에 필요한 Callback만 설명해보고자 한다.</p>
<br/>

<br/>


<h4 id="onmediaitemtransition">onMediaItemTransition</h4>
<pre><code class="language-kotlin">override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
    super.onMediaItemTransition(mediaItem, reason)
}</code></pre>
<p>미디어 아이템이 바뀔 때 호출된다. <code>mediaItem</code>이라는 파라미터가 있어서 어떤 음악을 보고있는지를 가져올 수 있다.</p>
<p>이 곳에는 현재 음악이 무엇인지를 나태내는 <code>currentMusicFlow</code>, 현재 음악이 재생목록의 몇번째인지 Index를 나타내는 <code>currentMusicIndexFlow</code>를 구현할 수 있을 것 같다.</p>
<br/>

<h4 id="onisplayingchanged">onIsPlayingChanged</h4>
<pre><code class="language-kotlin">override fun onIsPlayingChanged(isPlaying: Boolean) {
    super.onIsPlayingChanged(isPlaying)
}</code></pre>
<p>미디어가 재생되거나 정지되었을 때 호출된다. <code>isPlaying</code>으로 재생인지, 정지인지 알 수 있으니 <code>isPlayingFlow</code>에 emit하기 좋은 장소이다.</p>
<p>또한 음악 <code>스트리밍</code>이기 때문에 실행해보기 전까진 이 음악이 총 몇초인지 알지 못한다.(이것때문에 꽤 고생을 했다...)
그렇기에 현재 음악이 총 몇초인지 나타내는 <code>durationFlow</code>도 이곳에 넣기 좋을 것 같다.</p>
<br/>

<h4 id="onplayererror">onPlayerError</h4>
<pre><code class="language-kotlin">override fun onPlayerError(error: PlaybackException) {
    super.onPlayerError(error)
}</code></pre>
<p><code>Player</code>에 Error가 났을 때 호출된다. 어떤 에러인지 알 수 있어서 <code>errorFlow</code>를 갱신하기 좋은 장소이다.</p>
<br/>

<h4 id="onrepeatmodechanged">onRepeatModeChanged</h4>
<pre><code class="language-kotlin">override fun onRepeatModeChanged(repeatMode: Int) {
    super.onRepeatModeChanged(repeatMode)
}</code></pre>
<p>반복 모드가 바뀔 때 호출된다. 이름에서 알 수 있듯이 <code>repeatModeFlow</code>를 호출하기에 적합한 곳이다.
<code>repeatMode</code>가 Int로 반환되는데, 적절히 Enum으로 변환해주자.</p>
<br/>

<h4 id="onshufflemodeenabledchanged">onShuffleModeEnabledChanged</h4>
<pre><code class="language-kotlin">override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
    super.onShuffleModeEnabledChanged(shuffleModeEnabled)
}</code></pre>
<p>셔플 모드가 바뀔 때 호출된다. <code>isShuffleFlow</code>를 갱신하기 좋은 장소이다.</p>
<br/>

<br/>

<p>나는 위의 Callback만 필요하기 때문에 다른 것들은 사용하지 않았지만, <code>Player.Listener</code>의 안에는 이외에도 굉장히 많은 Callback이 내부에 준비되어 있기 때문에 입맛대로 골라서 사용하면 될 것 같다.</p>
<h3 id="mediaplayerimpl-만들기">MediaPlayerImpl 만들기</h3>
<p>필요한 Callback을 모두 알았으나, 아직 <code>currentPositoinFlow</code>를 구현하지 않는다.</p>
<p>아쉽게도 현재 재생 시간이 바뀔 때 호출되는 Callback은 없었다.
대체 이유가 무엇일까 생각했는데, ExoPlayer Github에서 비슷한 이슈를 찾을 수 있었다.</p>
<p><a href="https://github.com/google/ExoPlayer/issues/3980#issuecomment-373310137">ExoPlayer Github</a></p>
<p>위의 링크로 들어가 보면 성능상의 문제가 있어서 제공하지 않는다는 것을 알 수 있다.</p>
<p>그리고 검색을 해 보면 보통 Handler를 사용하는 경우가 많은 것 같다.</p>
<p><a href="https://stackoverflow.com/questions/62969648/how-to-get-current-duration-of-video-in-exo-player">StackOverFlow 답변</a></p>
<blockquote>
<p>우린 Flow를 사용중인데,(물론 변환할 수 있지만) 아쉽지 않은가...!!</p>
</blockquote>
<p>그래서 다른 코드를 참고하지 않고 직접 만들어 보기로 하였다.</p>
<h4 id="실시간-재생-시간-가져오기">실시간 재생 시간 가져오기</h4>
<p>성능 이슈가 있다는 것을 고려하여 재생 시간을 가져오는 조건은 아래와 같았다.</p>
<ol>
<li>불필요한 호출을 막기 위해 <strong>플레이중일 때만</strong> <code>currentPosition</code>을 가져와야 한다.</li>
<li>음악이 바뀌면 0으로 초기화 해 준다.</li>
<li><code>delay</code> 를 통해 주기를 완화시키고, Coroutine, While문을 적절히 취소시켜야 한다.</li>
</ol>
<p>이미 만들 수 있는 <code>currentMusicFlow</code>와 <code>isPlayingFlow</code>를 이용한다면 쉽게 조건을 만족시킬 수 있다.</p>
<ul>
<li>음악이 바뀔 때 → <code>currentMusicFlow</code></li>
<li>플레이 중일 때 → <code>isPlayingFlow</code></li>
</ul>
<p>그럼 이 두개를 <code>combine</code>하여 조건을 만족할 때만 Flow를 흘려주면 된다!</p>
<pre><code class="language-kotlin">@OptIn(ExperimentalCoroutinesApi::class)
override val currentPositionFlow = currentMusicFlow
    .combine(isPlayingFlow) { currentMusic, isPlaying -&gt;
        channelFlow {
            while (isPlaying &amp;&amp; coroutineContext.isActive) {
                delay(250L)
                val position = withContext(Dispatchers.Main) { player.currentPosition }
                send(position)
            }
        }
    }.flattenMerge().flowOn(Dispatchers.IO) //빠르게 변경되면 이전 flow가 취소되지 않은 상태에서 두 flow가 병렬로 실행될 수 있음.
//그래서 flattenMerge로 병합함.</code></pre>
<h4 id="주의-main이-아닌-thread에서-exoplayer를-실행시키면-안된다">주의! Main이 아닌 Thread에서 ExoPlayer를 실행시키면 안된다.</h4>
<p>위의 코드에서 <code>flowOn(Dispatchers.IO)</code>를 했는데, 내부에서는 <code>withContext(Dispatchers.Main)</code>으로 player의 값을 가져왔다.</p>
<p>만약 Main이 아닌 다른 Thread에서 <code>ExoPlayer</code>를 호출한다면 아래와 같은 Exception이 뜬다.
<img src="https://velog.velcdn.com/images/moony_-/post/0d3e452a-8c0f-439b-bf07-d434ebe01c50/image.png" alt=""></p>
<p>이제 완성된 코드는 아래와 같다!</p>
<h3 id="완성된-mediaplayerimple">완성된 MediaPlayerImple</h3>
<pre><code class="language-kotlin">class MediaPlayerImpl @Inject constructor(
    private val player: Player
) : MediaPlayer {
    override val isPlayingFlow = MutableStateFlow(false)
    override val currentMusicFlow = MutableStateFlow&lt;Music?&gt;(null)

    @OptIn(ExperimentalCoroutinesApi::class)
    override val currentPositionFlow = currentMusicFlow
        .combine(isPlayingFlow) { currentMusic, isPlaying -&gt;
            channelFlow {
                while (isPlaying &amp;&amp; coroutineContext.isActive) {
                    delay(250L)
                    val position = withContext(Dispatchers.Main) { player.currentPosition }
                    send(position)
                }
            }
        }.flattenMerge().flowOn(Dispatchers.IO) //빠르게 변경되면 이전 flow가 취소되지 않은 상태에서 두 flow가 병렬로 실행될 수 있음.
    //그래서 flattenMerge로 병합함.

    override val durationFlow = MutableStateFlow(0L)
    override val errorFlow = MutableSharedFlow&lt;PlayerError&gt;(
        replay = 1,
        extraBufferCapacity = 1,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )

    override val currentMusicIndexFlow = MutableStateFlow(player.currentMediaItemIndex)
    override val musicCountFlow = MutableStateFlow(0)
    override val repeatModeFlow = MutableStateFlow(RepeatMode.NONE)
    override val isShuffleFlow = MutableStateFlow(false)

    init {
        player.addListener(object : Player.Listener {

            override fun onEvents(player: Player, events: Player.Events) {
                super.onEvents(player, events)
            }

            override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
                super.onMediaItemTransition(mediaItem, reason)
                currentMusicFlow.value = mediaItem?.toMusic()
                currentMusicIndexFlow.value = player.currentMediaItemIndex
            }


            override fun onIsPlayingChanged(isPlaying: Boolean) {
                super.onIsPlayingChanged(isPlaying)
                isPlayingFlow.value = isPlaying
                durationFlow.value = player.duration
            }

            override fun onPlayerError(error: PlaybackException) {
                super.onPlayerError(error)
                errorFlow.tryEmit(error.toPlayerError())
            }

            override fun onPlaybackStateChanged(playbackState: Int) {
                super.onPlaybackStateChanged(playbackState)
            }

            override fun onRepeatModeChanged(repeatMode: Int) {
                super.onRepeatModeChanged(repeatMode)
                repeatModeFlow.value = getRepeatModeFromMedia(repeatMode)
            }

            override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
                super.onShuffleModeEnabledChanged(shuffleModeEnabled)
                isShuffleFlow.value = shuffleModeEnabled
            }

        })
    }

    override fun play() {
        if (player.playbackState == Player.STATE_IDLE)
            player.prepare()
        player.play()
    }

    override fun pause() {
        player.pause()
    }

    override fun next() {
        if (player.currentMediaItemIndex == player.mediaItemCount - 1) {
            player.seekTo(0, 0L)
            return
        }
        player.seekToNext()
    }

    override fun previous() {
        if (player.currentMediaItemIndex == 0) {
            player.seekTo(player.mediaItemCount - 1, 0L)
            return
        }
        player.seekToPrevious()
    }

    override fun seekTo(positionMillis: Long) {
        player.seekTo(positionMillis)
    }

    override fun seekMusicByIndex(index: Int) {
        player.seekTo(index, 0L)
    }

    override fun addMusic(music: Music) {
        player.addMediaItem(music.toMediaItem())
        updateMusicCount()
    }

    override fun addMusic(index: Int, music: Music) {
        player.addMediaItem(index, music.toMediaItem())
        updateMusicCount()

    }

    override fun addMusics(musics: List&lt;Music&gt;) {
        player.addMediaItems(musics.map { it.toMediaItem() })
        updateMusicCount()

    }

    override fun addMusics(index: Int, musics: List&lt;Music&gt;) {
        player.addMediaItems(index, musics.map { it.toMediaItem() })
        updateMusicCount()
    }

    override fun getMusic(index: Int): Music? {
        if (player.mediaItemCount &lt;= index) return null
        return player.getMediaItemAt(index).toMusic()
    }

    override fun setRepeatMode(repeatMode: RepeatMode) {
        player.repeatMode = repeatMode.toMediaRepeatModeInt()
    }

    override fun setShuffle(isShuffle: Boolean) {
        player.shuffleModeEnabled = isShuffle
    }

    private fun updateMusicCount() {
        musicCountFlow.value = player.mediaItemCount
    }

    private fun getRepeatModeFromMedia(repeatModeInt: Int) = when (repeatModeInt) {
        Player.REPEAT_MODE_ALL -&gt; RepeatMode.ALL
        Player.REPEAT_MODE_ONE -&gt; RepeatMode.ONE
        Player.REPEAT_MODE_OFF -&gt; RepeatMode.NONE
        else -&gt; {
            RepeatMode.NONE
        }
    }


}</code></pre>
<p>많이 길어졌지만, 아주 좋다..!!</p>
<h2 id="마치며">마치며</h2>
<p>이번에는 <code>ExoPlayer</code>를 Jetpack Compose에서 쉽게 사용하기 위해 Flow로 변환하는 과정을 거쳤다.</p>
<p><code>ExoPlayer</code> 내부도 뜯어 보고, 특히 <strong>실시간으로 재생 시간 데이터 받아오기</strong>에 많은 고민을 쏟은 시간이었다.</p>
<p>다음 포스팅에는 기존 구조를 Multi Module에 맞는 구조로 바꾸어 좀 더 쉽게 사용하도록 할 예정이다.</p>
<p><strong>많은 기대 부탁!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android Music 앱 만들기 (2) - MediaLibraryService]]></title>
            <link>https://velog.io/@moony_-/Android-Music-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-MediaLibraryService</link>
            <guid>https://velog.io/@moony_-/Android-Music-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-MediaLibraryService</guid>
            <pubDate>Thu, 09 Jan 2025 16:42:34 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>이전에는 Android Music 앱을 만들 때 필요한 클래스, 타입들과 기본적인 구조에 대해 살펴보았다.
이번에는 살펴본 내용으로 직접 <code>MediaLibraryService</code> 만들어 보자.</p>
<h3 id="medialibraryservice">MediaLibraryService</h3>
<p><code>MediaLibraryService</code>는 <code>MediaSessionService</code>를 상속받고 있으며 abstract class이다.
<img src="https://velog.velcdn.com/images/moony_-/post/5ab1d1d8-a805-4442-8a5c-1e7ed7b0cfa7/image.png" alt=""></p>
<p>그리고 한 메소드를 override 해야 하는데 <code>onGetSession</code>이다.</p>
<p>원래 <code>MediaSessionService</code>에 있는 abstract 함수인데 <code>MediaLibraryService</code>에서는 override되어 MediaLibrarySession을 반환하도록 되어 있다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/eb7d5e55-eece-4529-83aa-e507b3d7f257/image.png" alt=""></p>
<p>MediaLibrarySession은 MediaSession을 상속받고 있기 때문에 abstract 함수를 override를 할 수 있었던 것이고, MediaSession에 있는 음악 조절 기능들도 모두 사용할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/e76a1f3b-b3d8-4ff1-a6c7-7bbcf1950b16/image.png" alt=""></p>
<p>그럼 MediaLibrarySession을 상속받아 구현해보자.</p>
<pre><code class="language-kotlin">class PlayerService : MediaLibraryService() {

    private var mediaLibrarySession: MediaLibrarySession? = null
    private val mediaLibrarySessionCallback = object : MediaLibrarySession.Callback {    }

    override fun onCreate() {
        super.onCreate()
        mediaLibrarySession = MediaLibrarySession.Builder(
            this,
            ExoPlayer.Builder(this).build(),
            mediaLibrarySessionCallback
        ).build()
    }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) =
        mediaLibrarySession

    override fun onTaskRemoved(rootIntent: Intent?) {
        super.onTaskRemoved(rootIntent)
        val player = mediaLibrarySession?.player!!
        if (!player.playWhenReady
            || player.mediaItemCount == 0
            || player.playbackState == Player.STATE_ENDED) {
            // Stop the service if not playing, continue playing in the background
            // otherwise.
            stopSelf()
        }
    }

    override fun onDestroy() {
        mediaLibrarySession?.run {
            player.release()
            release()
            mediaLibrarySession = null
        }
        super.onDestroy()

    }



}</code></pre>
<h3 id="medialibrarysession">MediaLibrarySession</h3>
<p>미디어를 조작하거나 탐색하기 위한 정보가 들어있다.
생성하려면 Builder로 생성해야 하며, 필요한 파라미터는 <code>Context</code>, <code>Player</code>, <code>MediaLibrarySession.Callback</code>이다.</p>
<p>특히 <code>MediaLibrarySession.Callback</code>이 다른 점인데, 이 곳에 있는 <code>onSearch</code>같은 함수를 override하여 탐색을 구현할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/bc741b6e-2bf2-4e29-ae8d-22a8bfdce8c6/image.png" alt=""></p>
<h3 id="ongetsession">onGetSession</h3>
<p><code>MediaSessionService</code>와 같이 필수로 override 해야하는 abstract function이다.
다만 이전과는 다르게 MediaSession이 아닌 MediaLibrarySession을 반환해야 한다.</p>
<h3 id="ontaskremoved">onTaskRemoved</h3>
<p>앱이 Task에 지워졌을 때 어떻게 할 것인지 결정할 수 있는 함수이다.
우리는 App이 종료되었을 때도 음악은 계속 재생되어야 하기 때문에 유지시켜 주어야 한다.</p>
<p><a href="https://developer.android.com/media/media3/session/background-playback">공식 문서</a>에서는 이 조건을 위와 같이 했는데 뜻은 이러하다.</p>
<ul>
<li><code>player.playWhenReady</code>: <code>getPlaybackState() == STATE_READ</code>일 때 음악을 재생할 것인지 아닌지 결정하는 변수이다. 음악 재생이 준비되었을 때 재생할 것인지를 true, false로 결정할 수 있다.</li>
<li><code>player.mediaItemCount</code>: 현재 플레이리스트에 있는 미디어 아이템 갯수를 반환한다. 위의 코드에서는 갯수가 0일 때 정지하는 것으로 조건을 설정하였다.</li>
<li><code>player.playbackState</code>: 현재 플레이어의 상태를 나타낸다. 조건에서는 STATE_END일 때 정지한다.</li>
</ul>
<p><code>STATE</code>에는 아래의 4가지 상태가 있다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/c5e9dcaa-7dd7-464a-9996-7a67052093d2/image.png" alt=""></p>
<p><a href="https://developer.android.com/media/media3/session/serve-content">MediaLibraryService 공식 문서</a>에서는 onTaskRemoved를 override하지 않지만, <a href="https://developer.android.com/media/media3/session/background-playback">MediaSessionService 공식 문서</a>에서는 onTaskRemoved를 위와 같이 override한다.</p>
<p><code>MediaLibraryService</code>로 되면서 뭔가 변화되었기 때문인가 생각했지만 변화된 것은 딱히 없었다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/104ff0fa-9a7c-4762-8ad7-9746be9e1709/image.png" alt=""></p>
<p>그래서 <code>MediaSessionService</code>문서와 같이 보면서 작성해야 한다.</p>
<h3 id="state_end일-때-종료">STATE_END일 때 종료??</h3>
<p>위의 코드에서 보면 <code>player.playbackState == Player.STATE_ENDED</code>의 조건으로 되어 있다.
하지만 Player.STATE_ENDED는 <strong>한 곡이 끝나도 호출</strong>되기 때문에 한곡 재생이나 반복 재생으로 세팅되어도 꺼지게 된다.</p>
<p>마침 <code>player</code>에 <code>repeatMode</code>를 받을 수 있고, <code>REPEAT_MODE_OFF</code>라는 것도 있어서
공식 문서와는 조건을 다르게 주었다.</p>
<pre><code class="language-kotlin">override fun onTaskRemoved(rootIntent: Intent?) {
    val player = mediaLibrarySession?.player!!
    if (!player.playWhenReady
        || player.mediaItemCount == 0
        || (player.playbackState == Player.STATE_ENDED
                &amp;&amp; player.repeatMode == Player.REPEAT_MODE_OFF) //곡이 끝났고, 반복 재생 모드가 아닐 때
    ) {
        stopSelf()
    }
}</code></pre>
<br/>

<br/>

<h3 id="activity-연결">Activity 연결</h3>
<p>Media3가 나름 최신 라이브러리이니, 나도 Compose로 UI를 만들어 Activity에 연결해 보려고 한다.</p>
<p>MediaLibraryService가 완성되었으니 이제 연결해보자.</p>
<br/>

<p>MediaLibraryService는 MediaController, MediaBrowser 둘 다 만들 수 있다.</p>
<p>그러니 <code>MediaController</code>와 <code>MediaBrowser</code>를 Compose 여러곳에서 사용할 수 있도록 CompositionLocal을 만들자.</p>
<pre><code class="language-kotlin">val LocalMediaController = compositionLocalOf&lt;MediaController?&gt; { error(&quot;No MediaSession provided&quot;) }

val LocalMediaBrowser = compositionLocalOf&lt;MediaBrowser?&gt; { error(&quot;No LocalMediaBrowser provided&quot;) }
</code></pre>
<p>그다음 private 필드를 activity에 만들어 놓자.
compose에서 CompositionLocal을 사용해야 하니, 이 변수는 mutableState로 만든다.</p>
<pre><code class="language-kotlin">private var mediaController by mutableStateOf&lt;MediaController?&gt;(null)
private var mediaBrowser by mutableStateOf&lt;MediaBrowser?&gt;(null)</code></pre>
<p><code>MediaController</code>와 <code>MediaBrowser</code>는 Future로 받아오게 된다.
만약 Future로 받아오는 중에 앱이 중단되거나 사용자가 끄면 받아오는 것을 중단시켜야 한다.
그래서 onStop에서 중단할 수 있도록 Future도 private Field로 만들어주자.</p>
<pre><code class="language-kotlin">private lateinit var mediaControllerFuture: ListenableFuture&lt;MediaController&gt;
private lateinit var mediaBrowserFuture: ListenableFuture&lt;MediaBrowser&gt;</code></pre>
<p>이제 이 변수들을 초기화 하는 코드를 작성해 보자.</p>
<pre><code class="language-kotlin">//MediaService 토큰 받기
val sessionToken = SessionToken(this, ComponentName(this, MediaService::class.java))

//토큰으로 MediaController의 future 생성
mediaControllerFuture = MediaController.Builder(this, sessionToken)
    .buildAsync()

//토큰으로 MediaBrowser의 future 생성
mediaBrowserFuture = MediaBrowser.Builder(this, sessionToken)
    .buildAsync()

//MediaController가 준비되면 저장
mediaControllerFuture.addListener({
    mediaController = mediaControllerFuture.get()
}, MoreExecutors.directExecutor())
//MediaBrowser가 준비되면 저장

mediaBrowserFuture.addListener({
    mediaBrowser = mediaBrowserFuture.get()
}, MoreExecutors.directExecutor())</code></pre>
<p>이제 이것을 <code>CompositionLocalProvider</code>에 넣어주면 된다.</p>
<pre><code class="language-kotlin">CompositionLocalProvider(
    LocalMediaController.provides(mediaController),
    LocalMediaBrowser.provides(mediaBrowser)
) {
    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -&gt;
        MusicScreen(modifier = Modifier.padding(innerPadding))
    }
}</code></pre>
<p>마지막으로 앱이 stop 될 때, <code>MediaController</code>와 <code>MediaBrowser</code>를 해제시켜주고, 앞서 말했듯 Future들도 해제시켜 준다</p>
<pre><code class="language-kotlin">override fun onStop() {
    super.onStop()
    mediaController?.release()
    mediaBrowser?.release()
    MediaController.releaseFuture(mediaControllerFuture)
    MediaBrowser.releaseFuture(mediaBrowserFuture)
}</code></pre>
<p>위의 내용은 안드로이드 <a href="https://developer.android.com/media/media3/session/connect-to-media-app?_gl=1*iqsfec*_up*MQ..*_ga*MTc0MTkzMDY4MC4xNzM2MzM3NjQ3*_ga_6HH9YJMN9M*MTczNjMzNzY0Ny4xLjAuMTczNjMzNzY0Ny4wLjAuMTA1NjUyMjExNg..#create-controller">MediaController 공식 문서</a>에서 찾을 수 있다.</p>
<h3 id="activity전체-코드">Activity전체 코드</h3>
<pre><code class="language-kotlin">class MainActivity : ComponentActivity() {

    private var mediaController by mutableStateOf&lt;MediaController?&gt;(null)
    private var mediaBrowser by mutableStateOf&lt;MediaBrowser?&gt;(null)

    private lateinit var mediaControllerFuture: ListenableFuture&lt;MediaController&gt;
    private lateinit var mediaBrowserFuture: ListenableFuture&lt;MediaBrowser&gt;

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //MediaService 토큰 받기
        val sessionToken = SessionToken(this, ComponentName(this, MediaService::class.java))

        //토큰으로 MediaController의 future 생성
        mediaControllerFuture = MediaController.Builder(this, sessionToken)
            .buildAsync()

        //토큰으로 MediaBrowser의 future 생성
        mediaBrowserFuture = MediaBrowser.Builder(this, sessionToken)
            .buildAsync()

        //MediaController가 준비되면 저장
        mediaControllerFuture.addListener({
            mediaController = mediaControllerFuture.get()
        }, MoreExecutors.directExecutor())

        //MediaBrowser가 준비되면 저장
        mediaBrowserFuture.addListener({
            mediaBrowser = mediaBrowserFuture.get()
        }, MoreExecutors.directExecutor())

        enableEdgeToEdge()
        setContent {
            OffLineMusicTheme {
                CompositionLocalProvider(
                    LocalMediaController.provides(mediaController),
                    LocalMediaBrowser.provides(mediaBrowser)
                ) {
                    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -&gt;
                        MusicScreen(modifier = Modifier.padding(innerPadding))
                    }
                }
            }
        }
    }

    override fun onStop() {
        super.onStop()
        mediaController?.release()
        mediaBrowser?.release()
        MediaController.releaseFuture(mediaControllerFuture)
        MediaBrowser.releaseFuture(mediaBrowserFuture)
    }
}
</code></pre>
<p>이제 준비는 끝났다. <code>MusicScreen</code>이라는 곳에 <code>LocalMediaController.current</code>를 통해 MediaController를 받아오고 실행시켜 보자.</p>
<pre><code class="language-kotlin">@Composable
fun MusicScreen(modifier: Modifier = Modifier) {
    val localMediaController = LocalMediaController.current
    val localMediaBrowser = LocalMediaBrowser.current
    localMediaController?.addMediaItem(
        MediaItem.Builder().setMediaId(&quot;1&quot;)
            .setUri(&quot;https://storage.googleapis.com/exoplayer-test-media-0/play.mp3&quot;).build()
    )
    localMediaController?.play()
}
</code></pre>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/moony_-/post/057deecb-db07-4c06-bc26-03c0b1461943/image.gif" alt=""></p>
<p>앱을 꺼도 백그라운드에서 잘 동작하는 모습을 볼 수 있다.</p>
<h3 id="목록">목록</h3>
<p><a href="https://velog.io/@moony_-/Android-Music-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-%EC%82%AC%EC%A0%84-%EC%A7%80%EC%8B%9D-%EB%B0%8F-%EA%B5%AC%EC%A1%B0">Android Music 앱 만들기 (1) - 사전지식 및 구조</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android Music 앱 만들기 (1) - 사전 지식 및 구조]]></title>
            <link>https://velog.io/@moony_-/Android-Music-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-%EC%82%AC%EC%A0%84-%EC%A7%80%EC%8B%9D-%EB%B0%8F-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@moony_-/Android-Music-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-%EC%82%AC%EC%A0%84-%EC%A7%80%EC%8B%9D-%EB%B0%8F-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Wed, 08 Jan 2025 12:43:11 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>나는 음악을 정말 좋아한다. 장르를 가리지 않고 그 날의 기분이나 분위기에 맞는 음악을 버스, 지하철에서 듣는다.</p>
<p>그리고 우연히 모 회사에서 뮤직 스트리밍 앱 개발자를 뽑는다는 공고를 보고 음악 앱을 만드는 방법에 대해 생각했었다.</p>
<blockquote>
<p>백그라운드에서 돌아갈 수 있게 Service를 만들고, 음악을 재생하면 되겠지?</p>
</blockquote>
<p>그리고 자료를 찾아 봤는데, 생각보다 자료가 많이 나와있지 않았다.
그나마 영어 자료는 많지만 한국어 자료들은 오래된 것들이 대부분이고 최근 것들은 부족했었다.</p>
<p>그래서 내가 한번 Compose로 간단한 음악 스트리밍 앱을 만들어 보기로 했다.</p>
<h2 id="구조-생각해보기">구조 생각해보기</h2>
<p>클린 아키텍쳐, MVVM을 이야기 하는 것이 아니다.</p>
<p>백그라운드에서도 음악을 재생하려면 Service에 그 기능이 있어야겠다고 생각했다.
그리고 앱이 ForeGround에 있다면 보여줘야 할 Activity와 Service에서 재생되고 있는 음악이 따로 있어야 할텐데,</p>
<p>두 컴포넌트에서 어떻게 통신해야 할까?</p>
<p>다행히 안드로이드에서는 아래와 같은 구조로 짜라고 가이드를 준다.
<em>(역시 친절한 Android...)</em></p>
<p><a href="https://developer.android.com/media/media3?_gl=1*1g31ye3*_up*MQ..*_ga*MTI1Nzg5NDE4OC4xNzM2Mjc1NDY4*_ga_6HH9YJMN9M*MTczNjI3NTQ2Ny4xLjAuMTczNjI3NTQ2Ny4wLjAuNTIxMjE5Njcw">Jetpack Media3 공식 문서</a><img src="https://velog.velcdn.com/images/moony_-/post/d8bd8a1c-1f0d-410b-b296-019311aec6d8/image.png" alt=""></p>
<p>처음 보는 단어들이 많이 나온다.
문서에는 저 단어들을 정리한 표를 보여주는데 그 표를 간단히 정리하자면 다음과 같다.</p>
<ul>
<li><strong>Player</strong>는 음악을 재생하는 역할을 담당한다. interface이며 ExoPlayer가 이를 구현하는 구현체이다. 보통 ExoPlayer를 사용한다.</li>
<li><strong>MediaSession</strong>은 Player와 상호작용하여 재생, 정지와 같은 명령을 전달한다. 명령을 주는 객체는 뒤에 설명할 MediaController이다.</li>
<li><strong>MediaSessionService</strong>는 MediaSession을 사용하는 전용 Service이다. 기본적인 MediaSession관련 기능들을 가지고 있다.</li>
<li><strong>MediaController</strong>는 <strong>MediaSession</strong>에게 명령을 전달하는 역할을 한다.
특이하게 Player를 상속 받아 구현하고 있으며, 미디어를 재생하는 주체가 아님에도 마치 재생하는 것처럼 조작할 수 있다.</li>
</ul>
<p>크게 본다면 재생하는 주체인 <strong>ExoPlayer</strong>, 중간다리 역할을 하는 <strong>MediaSession</strong>, 그리고 재생하는 주체가 아님에도 마치 재생하는 주체처럼 컨트롤 할 수 있게 하는 <strong>MediaController</strong>가 있다.</p>
<h3 id="medialibraryservice-mediabrowser">MediaLibraryService, MediaBrowser?</h3>
<p>/로 구분지어 놓은 <strong>MediaLibraryService</strong>와 <strong>MediaBrowser</strong>가 눈에 띈다.</p>
<p>MediaLibraryService는 MediaSessionService를 상속받고 있다.</p>
<p>공식 문서에서는 아래와 같이 설명하는데</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/ff376609-2c05-4cb0-955c-4fb11d43ea3c/image.png" alt=""></p>
<p>MediaLibraryService는 MediaSessionService와 비슷하며 content library를 제공할 수 있게 한다는 것이다.</p>
<p>그리고 이 제공한 것들을 브라우징 할 수 있는 녀석이 MediaBrowser이다.</p>
<p>멜론이나 라인 뮤직과 같은 앱 들은 빅스비, 시리로도 실행이 가능한데 이처럼 다른 앱에서 접근하여 음악 목록들을 탐색할 수 있게 하려면 MediaLibraryService 사용해야 한다!(혹은 아래 설명할 MediaBrowserService)</p>
<p>자세한 내용은 아래 공식 문서에서 확인할 수 있다.</p>
<p><a href="https://developer.android.com/media/media3/session/serve-content?hl=en">MediaLibraryService 공식 문서</a></p>
<h3 id="mediabrowserservice">MediaBrowserService?</h3>
<p>찾아보니 옛날 문서에는 MediaBrowserService라는 것이 있었다.
이름 그대로 미디어를 브라우징(찾는)역할을 할 수 있는 서비스라는 것인데...</p>
<p><a href="https://developer.android.com/media/legacy#audio_app">옛날 공식 문서</a></p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/17016c53-c2d4-480c-b7af-794babcbee5b/image.png" alt=""></p>
<p>앨범에 있는 트랙, 혹은 오디오북 처럼 음악을 탐색할 때 사용되는 Service라는 것이다.</p>
<p>두 Service를 비교하는 글이 없어서 어떤 것이 달라졌는지 찾기 힘들었지만, 제미나이의 답변은 아래와 같았다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/43301a73-54a3-49bb-9f6c-c7a76d795740/image.png" alt=""></p>
<p>보면 <code>MediaLibraryService</code>는 외부 클라이언트가 Service에서 음악을 탐색할 수 있지만, <code>MediaBrowserService</code>는 외부 클라이언트가 Service에서 음악을 탐색할 수 없는 것 같다.
(믿을만 한가...?)</p>
<p>정리하면</p>
<ul>
<li><code>MediaLibraryService</code>는 미디어 라이브러리를 외부에서 탐색할 수 있도록 <strong>제공하는 역할</strong></li>
<li><code>MediaBrowserService</code>는 미디어 라이브러리를 내부에서 <strong>탐색</strong>하는 역할</li>
</ul>
<p><code>MediaLibraryService</code>와 함께 나오는 키워드들이 Android Auto랑 Google Assistance이니, 아마 Media3로 넘어오면서 이런 기능 연계가 중요해진게 아닐까? </p>
<h3 id="other-app-system-media-controls">Other App? System Media Controls?</h3>
<p>이것들은 뭐길래 내 Service에 접근하는 것일까?
우리가 음악 앱을 보통 사용하면 이어폰 버튼으로 음악을 재생하거나 멈추는 행동을 많이 한다.</p>
<p>당연히 외부에서 음악을 조절할 수 있어야 하는 것이다...!</p>
<p>그리고 플레이스토어에서 깔 수 있는 <strong>음악 꺼주는 앱</strong>들도 있다. 이런 앱들이 음악을 꺼줘야 한다.</p>
<h3 id="기술-결정">기술 결정</h3>
<p>대부분의 앱 들은 빅스비, 시리로 실행 가능하다. 또한 여러 상황에 맞는 앱을 구현하려면 역시 <code>MediaLibraryService</code>로 구현하는 것이 알맞다고 생각했다.</p>
<p>그럼 자연스럽게 <code>MediaBrowser</code>로 탐색하여 UI에 제공. 그리고 이 기능을 이용하여 Activity와 Compose를 구현하면 되겠다!</p>
<h3 id="마치며">마치며</h3>
<p>Android의 4대 컴포넌트인 Activity, Service, Content Provider, BroadCast Reciever는 면접 단골 질문이다.
그래서 단어들은 다들 익숙하지만, 막상 사용한 것은 Activity밖에 없었다.</p>
<p>이번 기회에 Service를 사용하는 방법을 잘 알 수 있을 것 같다.</p>
<p>그래서 다음 번에는 Service를 이용하여 음악을 재생하는 방법을 알아 보자!</p>
<h3 id="목록">목록</h3>
<p><a href="https://velog.io/@moony_-/Android-Music-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-MediaLibraryService">Android Music 앱 만들기 (2) - MediaLibraryService</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android - Retrofit 없이 Http 통신하기]]></title>
            <link>https://velog.io/@moony_-/Android-Retrofit-%EC%97%86%EC%9D%B4-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@moony_-/Android-Retrofit-%EC%97%86%EC%9D%B4-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 27 Dec 2024 20:24:23 GMT</pubDate>
            <description><![CDATA[<h2 id="retrofit">Retrofit?</h2>
<p>Retrofit이란 Android에서 Http통신을 쉽게 할 수 있도록 만들어진 Library이다.
어노테이션과 함께 Interface를 작성하면, 그 내용을 토대로 객체를 만들어 반환해 준다.
만약, GitHub에서 이슈를 가져오는 코드를 짠 다면, 아래와 같이 짤 수 있다.</p>
<pre><code class="language-kotlin">internal interface IssueApiRetrofit {
    @GET(&quot;issues&quot;)
    suspend fun getIssue(
        @Query(&quot;page&quot;) page: Int,
        @Query(&quot;per_page&quot;) perPage: Int = 100,
    ): Response&lt;List&lt;IssueDto&gt;&gt;
}</code></pre>
<p>GET, POST, UPDATE, DELETE 어노테이션으로 API 통신 함수를 만들어 주고
error code, body, errorBody를 손 쉽게 가져올 수 있는 Response와 Call 객체를 제공해 준다.</p>
<br/>

<p>이렇게 손쉽게 만들 수 있는 Retrofit.
하지만 <strong>&quot;Retrofit을 왜 사용하는가?&quot;</strong> 에 대한 대답을 온전히 할 수 없을 것 같다.
대답을 하려면 Retrofit이 없는 환경을 먼저 알아야 하는게 좋다고 생각했다.</p>
<p>따라서 나는 Retrofit 없이 HttpConnection과 JsonParser를 이용하여 Http통신을 해보려 한다.</p>
<h2 id="httpconnection-만들기">HttpConnection 만들기</h2>
<p>먼저 HttpConnection을 만드려면 URL을 만들어야 한다.
그럼 URL의 구조에 대해 알아야 하는데, <a href="https://ko.wikipedia.org/wiki/URL">URL의 구조</a>를 위키에서는 아래와 같이 표현하고 있다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/e22833c5-918a-418e-85cf-ca99293741ad/image.png" alt=""></p>
<p>이 구조를 보고 자주 사용하는 패턴으로 바꾼다면</p>
<blockquote>
<p><font color="grey"><em>https(또는 http)://authority/path?query</em></font></p>
</blockquote>
<p>이 정도로 볼 수 있다.</p>
<p>그럼 Retrofit과 비교했을 때, 위의 URL은 어떻게 대응될까?</p>
<br/>


<br/>


<h3 id="https와-authority">https와 authority</h3>
<p>먼저 <code>https(또는 http)://authority</code>는 Retrofit 객체를 만들 때 들어가는 baseUrl로 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/37241b9b-9606-42d4-bc9f-d6efc6d45d5e/image.png" alt=""></p>
<p>이 때 BaseUrl에는 통신 규약(https, http)와 authority가 들어간다.</p>
<br/>

<br/>


<h3 id="path">Path</h3>
<p>그 다음 <code>path</code>는 GET, POST, UPDATE, DELETE에 들어가는 값이나 </p>
<pre><code class="language-kotlin">@GET(&quot;issues&quot;) //Path 값이 들어간다.
suspend fun getIssue(
    @Query(&quot;page&quot;) page: Int,
    @Query(&quot;per_page&quot;) perPage: Int = 100,
): Response&lt;List&lt;IssueDto&gt;&gt;</code></pre>
<p><code>@Path</code> 어노테이션으로 대응될 수 있다.</p>
<pre><code class="language-kotlin">@PATCH(&quot;issues/{issue_number}&quot;)
suspend fun updateIssue(
    @Path(&quot;issue_number&quot;) issueNumber: Long,
    @Body issueBody: IssueBody
): Response&lt;IssueDto&gt;</code></pre>
<h3 id="query">Query</h3>
<p>마지막으로 쿼리는 <code>@Query</code>라는 어노테이션으로 대체될 수 있다.</p>
<pre><code class="language-kotlin">@GET(&quot;issues&quot;)
suspend fun getIssue(
    @Query(&quot;page&quot;) page: Int, //query 대체
    @Query(&quot;per_page&quot;) perPage: Int = 100,
): Response&lt;List&lt;IssueDto&gt;&gt;</code></pre>
<p>즉, <strong>GitHub야, Page가 1인, issue를 줘.</strong> 라고 해야 한다면</p>
<blockquote>
<p><code>https://github.com/issue?page=1</code></p>
</blockquote>
<p>이렇게 URL을 보내면 된다.</p>
<p>이제 GET, POST와 Header를 붙여 요청하는 함수를 만들어 보자.</p>
<h3 id="httpconnection함수-작성">HttpConnection함수 작성</h3>
<p>위의 내용을 토대로 BaseUrl, Path, Qeury를 받는 함수를 만들자.</p>
<pre><code class="language-kotlin">
const val GITHUB_ACCEPT_KEY = &quot;Accept&quot;
const val GITHUB_ACCEPT_VALUE = &quot;application/vnd.github+json&quot;

const val GITHUB_AUTH_KEY = &quot;Authorization&quot;
const val GITHUB_AUTH_VALUE_BEARER = &quot;Bearer ${BuildConfig.TOKEN}&quot;

const val GITHUB_API_VERSION_KEY = &quot;X-GitHub-Api-Version&quot;
const val GITHUB_API_VERSION_VALUE = &quot;2022-11-28&quot;

private suspend fun getConnection(
    method: String,
    baseUrl: String,
    endPoint: String,
    vararg queries: String
): HttpURLConnection {

    //url 생성
    val urlString = baseUrl + endPoint

    //query 생성
    val queryString = if (queries.isEmpty()) &quot;&quot; else &quot;?${queries.joinToString(&quot;&amp;&quot;)}&quot;

    //URL을 만들고 Connection 생성
    val url = URL(urlString + queryString)
    val conn = withContext(Dispatchers.IO) {
        url.openConnection()
    } as HttpURLConnection

    //GET, POST, UPDATE, DELETE
    conn.requestMethod = method

    //TIME_OUT 제한 설정
    conn.connectTimeout = TIME_OUT_MILLIS
    conn.readTimeout = TIME_OUT_MILLIS

    //Header 붙이기
    conn.setRequestProperty(Header.GITHUB_AUTH_KEY, Header.GITHUB_AUTH_VALUE_BEARER)
    conn.setRequestProperty(Header.GITHUB_ACCEPT_KEY, Header.GITHUB_ACCEPT_VALUE)
    conn.setRequestProperty(Header.GITHUB_API_VERSION_KEY, Header.GITHUB_API_VERSION_VALUE)
    return conn
}</code></pre>
<p>이제 에러를 핸들링 할 함수를 만들고</p>
<pre><code class="language-kotlin">private suspend fun &lt;T : Any&gt; handleResponse(
    connSupplier: suspend () -&gt; HttpURLConnection,
    supplier: (String) -&gt; T
): ApiResult&lt;T&gt; {
    return try {
        val conn = connSupplier()
        if (conn.responseCode == HttpURLConnection.HTTP_OK) {
            val jsonString = getResponseString(conn)
            conn.disconnect()
            val info = supplier(jsonString)
            ApiResult.Succeed(conn.responseCode, info)
        } else {
            ApiResult.Error(conn.responseCode, conn.responseMessage)
        }
    } catch (e: Exception) {
        e.printStackTrace()
        ApiResult.Error(InternalErrorCode.PARSING_FAIL.code, e.message)
    }
}</code></pre>
<p>아래와 같이 호출해 주면 된다.</p>
<pre><code class="language-kotlin">override suspend fun getIssue(page: Int): ApiResult&lt;List&lt;IssueDto&gt;&gt; {
    return handleResponse({
        getConnection(
            &quot;GET&quot;,
            END_POINT_ISSUES,
            &quot;page=$page&quot;
        )
    }) { jsonString -&gt;
        //json parsing하기
    }
}</code></pre>
<h2 id="json-직접-파싱하기">Json 직접 파싱하기.</h2>
<p>Json을 파싱하려면 string을 JsonObject로 만든 후, 필드의 이름을 string으로 넘겨주면 가져올 수 있다.</p>
<p>만약 array형태로 되어있다면, JsonArray를 사용하면 된다.</p>
<pre><code class="language-kotlin">fun parseIssueDto(jsonObject: JSONObject): IssueDto {

    //label
    val labels = parseLabelDtoList(jsonObject.getJSONArray(&quot;labels&quot;))
    val assignees = parseUserDtoList(jsonObject.getJSONArray(&quot;assignees&quot;))
    val url = jsonObject.getString(&quot;url&quot;)
    val repository_url = jsonObject.getString(&quot;repository_url&quot;)
    val comments_url = jsonObject.getString(&quot;comments_url&quot;)
    val events_url = jsonObject.getString(&quot;events_url&quot;)
    val html_url = jsonObject.getString(&quot;html_url&quot;)
    val id = jsonObject.getLong(&quot;id&quot;)
    val node_id = jsonObject.getString(&quot;node_id&quot;)
    val number = jsonObject.getLong(&quot;number&quot;)
    val title = jsonObject.getString(&quot;title&quot;)
    val user = parseUserDto(jsonObject.getJSONObject(&quot;user&quot;))
    val state = jsonObject.getString(&quot;state&quot;)
    val locked = jsonObject.getBoolean(&quot;locked&quot;)
    val body = jsonObject.getString(&quot;body&quot;)
    val mileStoneDto = jsonObject.optJSONObject(&quot;milestone&quot;)?.let { parseMileStoneDto(it) }
    val created_at = jsonObject.getString(&quot;created_at&quot;)
    val closed_at = jsonObject.getString(&quot;closed_at&quot;)
    val updated_at = jsonObject.getString(&quot;updated_at&quot;)

    return IssueDto(
        url = url,
        repositoryUrl = repository_url,
        commentsUrl = comments_url,
        eventsUrl = events_url,
        htmlUrl = html_url,
        id = id,
        nodeId = node_id,
        number = number,
        title = title,
        user = user,
        labels = labels,
        state = state,
        locked = locked,
        assignees = assignees,
        body = body,
        milestone = mileStoneDto,
        createdAt = created_at,
        closedAt = closed_at,
        updatedAt = updated_at,
    )

}</code></pre>
<p>이렇게 하면 Retrofit 없이 Http통신을 할 수 있다.</p>
<h2 id="느낀점">느낀점</h2>
<p>이런 노가다를 하며... 느낀점이 있다.</p>
<h3 id="1-실수할-여지가-많다">1. 실수할 여지가 많다.</h3>
<p>GET, POST, UPDATE, DELETE 메소드를 String으로 넘겨야 하고,
또한 Query도 String으로 넘겨 주어야 한다.</p>
<p>String으로 넘겼을 때 단점은 다들 알다시피 <code>오타</code>가 날 수 있다는 것이다.</p>
<p>따라서 오타가 났는지, 아닌지 조심하며 작성해야 한다.</p>
<p>또한 Query를 넘길 때, Query가 없다면 물음표(?)를 빼줘야 하고, Query가 있을 때는 넣어야 한다는 귀찮음도 있었다.</p>
<h3 id="2-json-parsing이-어렵다">2. Json Parsing이 어렵다.</h3>
<p>이건 HttpConnection으로 데이터를 가져온 후, Kotlin Serialization이나 Gson으로 대체하여 Parsing할 수 있지만, 이번에 Retrofit 사용 안하는 김에 똑같이 사용하지 않고 JsonObject로 해 보았다.</p>
<p>이 때의 단점은 앞선 오타의 단점과, 하나하나 JsonObject로 접근해 String, Int등으로 가져와야 하는 번거로움이 있었다. 실수할 여지도 많았으며, 필드가 많으면 잘못 가져올 수도 있다는 단점도 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] 프로그래머스(2022카카오 신입 공채) 파괴되지 않은 건물(구간 합)]]></title>
            <link>https://velog.io/@moony_-/Python-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A42022%EC%B9%B4%EC%B9%B4%EC%98%A4-%EC%8B%A0%EC%9E%85-%EA%B3%B5%EC%B1%84-%ED%8C%8C%EA%B4%B4%EB%90%98%EC%A7%80-%EC%95%8A%EC%9D%80-%EA%B1%B4%EB%AC%BC%EA%B5%AC%EA%B0%84-%ED%95%A9</link>
            <guid>https://velog.io/@moony_-/Python-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A42022%EC%B9%B4%EC%B9%B4%EC%98%A4-%EC%8B%A0%EC%9E%85-%EA%B3%B5%EC%B1%84-%ED%8C%8C%EA%B4%B4%EB%90%98%EC%A7%80-%EC%95%8A%EC%9D%80-%EA%B1%B4%EB%AC%BC%EA%B5%AC%EA%B0%84-%ED%95%A9</guid>
            <pubDate>Sat, 21 Dec 2024 15:22:29 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<blockquote>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/92344">파괴되지 않은 건물</a></p>
</blockquote>
<p>풀이</p>
<p>정확성 테스트를 통과하는 것은 어렵지 않다. 그저 반복문을 통하여 Skill에 나오는 범위 만큼 더하거나 빼주면 쉽게 풀 수 있다.</p>
<p>하지만 효율성 테스트는 통과하지 못하는데, 어떻게 하면 통과하는 지, 통과하는 이유는 무엇 인지를 기록할 것이다.</p>
<p>아이디어는 &quot;명령어 들을 한번에 하나 씩 처리하는 것이 아니라, 한번에 모아서 처리할 수는 없을까?&quot;이다.</p>
<p>먼저 정확성 테스트만 통과하는 코드를 보자.</p>
<pre><code class="language-python">def solution(board, skill):
    answer = 0
    for i in skill:
        t,r1,c1,r2,c2,d=i
        if t==1:
            d=-d
        for i in range(r1,r2+1):
            for j in range(c1,c2+1):
                board[i][j]+=d
    for i in range(len(board)):
        for j in range(len(board[0])):
            if board[i][j]&gt;0:
                answer+=1
    return answer</code></pre>
<p>이 코드는 하나의 명령이 떨어질 때 마다, 그 명령의 사각형 범위 만큼 반복문을 통하여 board를 갱신하게 된다.</p>
<p>입출력 예 1번에 비유하면</p>
<p>이 상태에서 r1=0, c1=0, r2=3, c2=4 이니 (0,0)부터 (3,4)까지 4의 피해를 입힌다. 그럼 밑의 그림처럼 된다.</p>
<p>그럼 이번 명령을 처리하기 위해 컴퓨터가 계산한 양은 (r2-r1+1)<em>(c2-c1+1)=4</em>5, 20이 된다.</p>
<p>다음 명령을 수행하면 (2,0) 부터 (2,3) 까지 2의 피해를 입히게 되어 밑의 그림처럼 바뀌게 된다.</p>
<p>그럼 이번의 컴퓨터가 계산한 양은 (2-2+1)*(3-0+1)=4가 된다.</p>
<p>그렇다면 가장 많이 계산하는 상황은 무엇일까?</p>
<p>매 명령 마다 표의 모든 요소를 변화 시키는 것이다. 모든 명령이 [1,0,0,3,4,-1] 인 경우이다.</p>
<p>표의 요소는 5*4=20개 이니 명령어 마다 20개의 숫자를 계산할 것이다.</p>
<p>board의 가로의 길이를 n, 세로의 길이를 m, 명령어의 갯수를 c라고 한다면 컴퓨터가 처리해야 할 계산의 횟수는 n<em>m</em>c가 된다. O(n<em>m</em>c)</p>
<p>이제 이것을 개선해 보자.</p>
<p>먼저 간단하게 생각하기 위해 위의 board를 1차원 배열로 생각해 보자. 그럼 밑의 표처럼 된다.</p>
<p>여기서 구간 0~3까지 -1을 하는 명령이 떨어졌다 생각해 보자. 전의 방식은 위의 표에 직접 -1씩 했을 것이다.</p>
<p>하지만 이렇게 하지 말고 각 칸의 변화 만을 저장하는 표를 하나 더 만들어 따로 저장해 보자. 그럼 밑의 그림처럼 될 것이다.</p>
<p>변화를 저장하는 표에 효율성을 통과하지 못하는 코드와 똑같이 4번의 연산이 들어갔다.</p>
<p>이 연산을 줄이는 방법을 찾아야 하는데, 이 방법은 명령에 의한 숫자 변화를 처음과 끝+1에 표시만 하는 방법이다.</p>
<p>이렇게 하면 연산은 시작 점에 -1, 끝 점의 다음 점에 +1, 총 두 번의 연산을 수행하게 된다.
이 표식을 나중에 풀 때는 이 변화를 저장하는 표를 처음부터 끝 까지 순회하며 누적으로 더해 주면 원래의 표가 나온다.</p>
<p>arr[i]=arr[i]+arr[i-1]</p>
<p>우리는 위의 표시만 하는 방법으로 모든 명령을 처리 할 것이다.
그럼 1차원 배열로 예시(Test Case)를 하나 만들어 보자.
맨 처음 board가 [5,5,5,5,5] 이고, 명령어가 [[1,0,0,0,3,1],[1,0,1,0,4,1],[2,0,2,0,4,1],[1,0,1,0,3,3]] 이라고 가정해 보자.</p>
<p>그럼 우리는 표시만 하는 방법으로 변화 표를 계산하면 밑의 그림처럼 된다.</p>
<p>1번째 명령(0 부터 3 까지 -1, 0번 index에 -1, 3+1번 index에 +1):</p>
<p>2번째 명령(1 부터 4까지 -1, 1번 index에 -1, 4+1번 index에 +1, 하지만 5번 index이면 표의 범위를 벗어나므로 생략.):</p>
<p>3번째 명령(회복, 2번에 +1 맨 끝은 표의 범위를 벗어나므로 생략.):</p>
<p>4번째 명령(1번 index에 -3, 3+1번 index에 +3):</p>
<p>자 이렇게 표식 만을 모두 남겨 놓았다. 이 표식을 남겨 놓은 표를 누적으로 풀게 되면 밑의 표  처럼 되는데,</p>
<p>이 표가 바로 모든 칸의 변화 점이 된다.(의심이 된다면 직접 계산하여 비교해 보면 된다.)</p>
<p>위의 방법은</p>
<p>한 명령어가 얼마나 많은 칸의 요소들을 변화 시키는 명령이든, 단 두 번의 표식만 남기고 넘기기 때문에 한 명령어 당 계산 횟수는 2회가 된다.</p>
<p>또한 마지막에 누적으로 원래의 변화를 계산할 때는 표의 길이 만큼 순회하기 때문에 표의 길이 만큼의 계산 횟수가 들어가게 된다.</p>
<p>따라서 명령의 횟수를 c, 표의 길이를 n이라 할 때, 표식을 남겨 놓은 표를 풀 때 까지의 계산 횟수는 O(c<em>2)(명령어 처리 및 표식)+O(n)(표식이 된 표를 풀기)=O(c</em>2+n)이 된다.</p>
<p>이제 원래의 표에 변화들을 적용 시키면 또 표의 길이 만큼 순회하며 계산이 들어갈 것이다. 따라서 표의 길이 n만큼 계산이 들어간다. O(n)</p>
<p>최종적으로 컴퓨터가 계산한 횟수는 O(c<em>2)+O(n)+O(n) 이므로 O(c</em>2+2n)이 된다.</p>
<p>이는 위의 Test Case의 상황 말고도, 매 명령이 모든 표의 요소를 변화 시키는 명령이라고 해도 매 명령 마다 두 번만 수행하므로 O(2c+2n)이 된다.</p>
<p>정확성만 통과하는 방식은 명령어 마다 모든 요소를 변화 시키므로 O(c*n)이 되는데, c를 10, n을 20으로 가정 하였을 때 정확성 방식은 200, 개선된 방식은 20+40=60이 된다.</p>
<p>아주 훠어어얼씬 빠른 코드가 되는 것이다.(이는 계산 식에 상수가 들어있기 때문이다.)</p>
<p>만약 c와 n이 더욱 큰 숫자라면 이 차이는 더욱 많이 될 것이다.</p>
<p>지금 까지 개선한 방식을 2차원 배열로 확장하면 이 문제에 대입할 수 있다.</p>
<p>1차원 배열에서 변화를 저장하는 표의 arr[i]는 arr[0]부터 arr[i]까지의 합으로 볼 수 있다.</p>
<p>2차원 배열에서 변화를 저장하는 표의 arr[i][j]는 arr[0][0] 부터 arr[i][j]까지의 사각형이 그리는 구간의 누적 합이 된다.</p>
<p> (예시, arr[3][3]은 arr[0][0]과 arr[3][3]이 그리는 사각형의 합.)</p>
<p>그럼 표식은 어떻게 할까?</p>
<p>시작 점 (i,j)(왼쪽 위 모서리)와 끝 점 (k,l)(오른쪽 아래 모서리), 변화 시킬 숫자 d가 주어졌다고 가정했을 때, arr[i][j]에 +d, arr[i][l+1]에 -d arr[k][j+1]에 -d, arr[k+1][l+1]에 +d를 해주면 된다.</p>
<p>(예시, (1,1)과 (3,3), 변화는 -1)</p>
<p>(표가 굉장히 더럽혀 졌다... 요점은 arr[?][?]는 시작점[i][j] 부터 자신의 위치 arr[?][?]까지의 모든 칸의 누적 합이라는 것이다. 누적 했을 때, 원래 의도했던 구간에 모두 -1 할 수 있도록 표식을 해주는 것이 포인트 이다.)</p>
<p>이러한 방식을 문제에 대입하면 효율성 테스트 까지 모두 통과한다.</p>
<p>코드:</p>
<pre><code class="language-python">def solution(board, skill):
    answer = 0
    change=[[0 for _ in range(len(board[0])+1)]for _ in range(len(board)+1)]
    for i in skill:
        t,r1,c1,r2,c2,d=i
        if t==1:
            d=-d
        change[r1][c1]+=d
        change[r1][c2+1]-=d
        change[r2+1][c1]-=d
        change[r2+1][c2+1]+=d
    for i in range(1,len(change[0])):
        change[0][i]+=change[0][i-1]
    for i in range(1,len(change)):
        change[i][0]+=change[i-1][0]

    for i in range(1,len(change)):
        for j in range(1,len(change[0])):
            change[i][j]=change[i-1][j]+change[i][j-1]+change[i][j]-change[i-1][j-1]


    for i in range(len(board)):
        for j in range(len(board[0])):
            board[i][j]+=change[i][j]
            if board[i][j]&gt;0:
                answer+=1
    return answer</code></pre>
<p>결과:</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/a8928683-1c5c-4bc8-9839-3e90ebfc8d12/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[객체 지향과 디자인 패턴] SOLID 다섯 가지 설계 원칙]]></title>
            <link>https://velog.io/@moony_-/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EA%B3%BC-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-SOLID-%EB%8B%A4%EC%84%AF-%EA%B0%80%EC%A7%80-%EC%84%A4%EA%B3%84-%EC%9B%90%EC%B9%99</link>
            <guid>https://velog.io/@moony_-/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EA%B3%BC-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-SOLID-%EB%8B%A4%EC%84%AF-%EA%B0%80%EC%A7%80-%EC%84%A4%EA%B3%84-%EC%9B%90%EC%B9%99</guid>
            <pubDate>Wed, 13 Apr 2022 16:39:17 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<hr>
<p>스파게티 코드를 작성하지 않으려면 프로그램을 설계하는 능력이 중요하다.</p>
<p>체계적이고 확장성이 좋으며 변경에 민감하지 않아야 나중에 유연하게 고칠 수 있는 프로그램이 된다고 한다.
<br/></p>
<p>하지만 처음부터 좋은 설계를 하는 것은 쉽지 않다. 실패했던 경험이 쌓여야 어느 기능을 분리 시키고, 합쳐야 하는지 직감적으로 알 수 있기 때문이다.</p>
<blockquote>
<p>마치 데이터베이스 수업 시간 때 들었던 관계형 데이터베이스 설계가 생각난다.</p>
</blockquote>
<br/>

<p>하지만 감각이나 센스가 중요한 데이터 베이스에도 1차 정규형, 2차 정규형과 같이 &quot;이럴 때는 이렇게 해야 한다.&quot;라는 명확한 기준을 제시해 주는 방법이 존재한다.</p>
<br/>

<p>객체지향에도 위와 같은 기틀인 SOLID 5 가자 설계 원칙이 있다.</p>
<p>SOLID 원칙은 객체 지향 설계를 할 때에 명확한 기준을 제공하는데, 이런 규칙은 경험이 없어도 좋은 프로그램 설계를 할 수 있도록 도와준다.</p>
<ul>
<li><p><strong>단일 책임 원칙</strong> (Single responsibility principle; SRP)</p>
</li>
<li><p><strong>개방-폐쇄 원칙</strong> (Open-closed principle; OCP)</p>
</li>
<li><p><strong>리스코프 치환 원칙</strong> (Liskov substitution principle; LSP)</p>
</li>
<li><p><strong>인터페이스 분리 원칙</strong> (Interface segregation principle; ISP)</p>
</li>
<li><p><strong>의존 역전 원칙</strong> (Dependency inversion principle; DIP)</p>
</li>
</ul>
<br/>

<p>이번에는 이 다섯 가지 원칙들을 공부한 것을 정리해 보자</p>
<br/>

<br/>

<h2 id="1-단일-책임-원칙-single-responsibility-principle-srp">1. 단일 책임 원칙 (Single responsibility principle; SRP)</h2>
<br/>
첫 번째는단일 책임 원칙이다.

<br/>

<blockquote>
<p>&quot;클래스는 단 한개의 책임을 가져야 한다.&quot; </p>
</blockquote>
<p>단일 책임 원칙은 다섯 가지 원칙 중에서 가장 중요한 원칙인데, 객체 지향의 기본은 하나의 책임을 객체에게 할당하는 데에 있기 때문이다.
따라서  이 원칙이 지켜지지 않을 경우 다른 원칙을 지켜도 무용지물이 될 수 있다.</p>
<br/>

<p>단일 책임의 원칙을 지키는지, 지키지 않는 지를 구분하려면 지키지 않았을 때에 일어나는 현상에 대해 알아야 합니다.</p>
<br/>

<p>만약 하나의 클래스가 하나의 임무를 담당하지 않고 여러 가지의 임무를 담당한다면 어떻게 될까?</p>
<br/>

<p>먼저 실수할 만한 상황을 가정하여 의도적으로 하나의 클래스에 두 임무를 부여해 보자.</p>
<br/>

<blockquote>
<p>&quot;HTML 프로토콜을 이용하여 데이터를 불러와 화면에 보여주자.&quot;</p>
</blockquote>
<br/>

<pre><code class="language-java">public class DataViewer {

    public void display(){
        String data=getHtmlData();
        updateUi(data);
    }
    public String getHtmlData(){

        HttpClient client=new HttpClient();
        client.connect();
        String string=client.getResponese();
        return string;

    }
    private void updateUi(String data){
        ui.changeData(data);
    }
}
</code></pre>
<br/>

<p>데이터를 화면에 보여주기 위해서는 먼저 데이터를 불러와야 한다.</p>
<p>데이터를 html으로 받아오는 행동이 선행되어야 하기 때문에 무심코 DataViewer 클래스에 데이터를 가져오는 메소드를 작성하는 실수를 할 수 있다. 아직 까진 문제가 되지 않는다.</p>
<blockquote>
<p>하지만 이러한 상황에서 http 통신을 socket통신으로 바꾸게 된다면, 또 이 여파로 데이터 타입 또한 byte로 바뀐다면 어떻게 될까?</p>
</blockquote>
<br/>

<p>클래스 내부에는 display와 getHtmlData가 String 타입으로 묶여 있다. 또한 display와 updateUi가 String으로 묶여 있다.</p>
<p>따라서 모든 데이터 타입을 바꿔 주어야 한다.</p>
<br/>

<pre><code class="language-java">public class DataViewer {

    public void display(){
        byte[] data=getHtmlData(); //변경 1: 바이트 타입으로 변경
        updateUi(data);
    }
    public byte[] getHtmlData(){

        SocketClient client=new SocketClient(); //변경 2: 소켓으로 변경
        client.connect(server,port);
        byte[] bytes=client.getResponese();//변경 3: 통신 방식 변경의 여파로 byte 타입으로 변경.
        return bytes;

    }

    private void updateUi(byte[] data){//변경 4: 매개변수 타입 변경.
        ui.changeData(data);
    }
}</code></pre>
<br/>

<p>통신 방식 하나만 달라져도 데이터 타입이 바뀌며 그 여파로 많은 메소드들도 바꾸어야 한다.
더 복잡한 구조로 되어 있다면 바꾸어야 할 코드들이 훨씬 많을 것이다.</p>
<p>따라서 서로 영향을 받지 않아야 할 책임들이 영향을 받게 된다.</p>
<p>이러한 문제점을 고치려면 서로 다른 책임을 다른 클래스에 작성하여 분리하고, 양쪽 모두 데이터의 변화에 유연하게 데이터를 적절한 타입으로 추상화 한다면 해결할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/5e1e1443-7eba-496e-a85e-d8b28cc06578/image.png" alt=""></p>
<p>이렇듯 서로 다른 책임이 다른 클래스로 분리 되어 있다면 변경이 일어났을 때 서로 영향을 주지 않게 된다.</p>
<p>하지만 경험이 부족한 프로그래머는 단일 책임 원칙을 처음부터 잘 지키기 어렵다. 이러한 문제점을 잘 파악할 수 있는 방법은 무엇일까?</p>
<blockquote>
<p>메소드를 실행시키는 것이 누구인지 파악하면 됩니다.</p>
</blockquote>
<p>클래스의 사용자들이 서로 다른 하나의 메소드만을 사용한다면 다른 책임에 속할 가능성이 높다. 따라서 분리를 고려해 볼 대상이 된다.</p>
<br/>

<br/>

<h2 id="2-개방-폐쇄-원칙-open-closed-principle-ocp">2. 개방-폐쇄 원칙 (Open-closed principle; OCP)</h2>
<br/>
두 번째는 개방 폐쇄 원칙 이다.

<p>개방 폐쇄 원칙의 설명은 다음과 같은데,</p>
<blockquote>
<p>&quot;확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.&quot;</p>
</blockquote>
<p>좀 더 쉽게 풀이하자면</p>
<blockquote>
<p>기능을 변경하거나 확장할 수 있다.</p>
</blockquote>
<blockquote>
<p>그 기능을 사용하는 코드는 수정하지 않는다.</p>
</blockquote>
<p>말이 조금 어렵지만, 우리는 이미 Interface를 사용하는 이유 에서 이러한 원칙을 해본 적이 있다.</p>
<br/>

<p><a href="https://velog.io/@moony_-/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EA%B3%BC-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%A5%BC-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0">인터페이스를 사용하는 이유</a></p>
<br/>

<p><img src="https://velog.velcdn.com/images/moony_-/post/2a8a3afa-e66a-4121-9ff5-2f8b6fda8116/image.png" alt=""></p>
<p>여기서 DataProvider라는 interface를 상속 받은 American과 Chinese는 서로 다른 기능을 하고, 서로 다른 타입이지만 DataProvider라는 같은 타입으로 묶을 수 있다.</p>
<p>따라서 Main에서 사용하는 메소드가 getData()인 것은 바뀌지 않는다</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/632b90e9-0c0a-4904-b533-5786e1c99753/image.png" alt=""></p>
<p>또한 DataProvider를 상속 받아 다른 기능인 Japanese를 확장할 수 있으며, 마찬가지로 Main에서 사용하는 메소드가 getData()인 것은 바뀌지 않는다.</p>
<p>따라서 확장에는 열려 있고, 변경에는 닫혀 있는 상태라고 할 수 있다.</p>
<p>개방 폐쇄 원칙을 지키는 또 다른 방법은 상속을 이용하는 방법이 있다.</p>
<br/>


<blockquote>
<p>&quot;파일을 보내는 기능을 작성해 보자&quot;</p>
</blockquote>
<br/>

<br/>

<p>파일을 보내는 클래스를 미리 작성했다고 가정해 보자.</p>
<pre><code class="language-java">public class DataSender {
    private Data data;
    public DataSender(Data data){
        this.data=data;

    }

    protected void sendData(){
        //data sending
    }

}</code></pre>
<br/>

<p>위의sendData 메소드는 protected로, 확장 가능성이 있다고 명시 되어 있다. </p>
<p>따라서 DataSender 클래스를 상속 받는 클래스는 sendData를 이용하여 기능을 확장할 수 있다.</p>
<p>따라서 압축을 하여 보내는 기능이 필요하다고 느낄 때, 우리는 밑과 같이 클래스를 작성할 수 있다.</p>
<pre><code class="language-java">public class ZippedDataSender extends DataSender{

    public ZippedDataSender(Data data) {
        super(data);
    }

    @Override
    protected void sendData(){
        //zip data
        super.sendData();

    }
}
</code></pre>
<br/>

<p>위의 클래스 DataSender의 입장에서 살펴 보면 확장에는 열려 있고
(protected void sendData is Overrode by ZippedDataSender) </p>
<p>변경에는 닫혀 있는(DataSender를 사용하는 클래스는 변경하지 않아도 된다.) 상태가 된다.</p>
<p>또한 데이터를 압축한 후 보내기는 데이터 보내기이다.이기 때문에 IS-A도 성립하게 된다.</p>
<br/>

<br/>

<h2 id="3-리스코프-치환-원칙-liskov-substitution-principle-lsp">3. 리스코프 치환 원칙 (Liskov substitution principle; LSP)</h2>
<br/>

<p>상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.</p>
<p>리스코프 치환 원칙은 개방 폐쇄 원칙을 받쳐 주는 다형성에 관한 원칙을 제공한다.</p>
<p>다형성이란?</p>
<blockquote>
<p>한 객체가 다양한 형태를 가질 수 있는 성질을 의미한다.</p>
</blockquote>
<p>전 시간에 해 보았던 인터페이스를 상속한 객체가 인터페이스 타입으로 인스턴스 생성이 가능한 추상화나, 부모 클래스의 타입으로 자식 클래스를 생성 가능하게 하는 상속 등이 다형성에 속한다.(또한 오버로딩 등 많은 다형성이 있습니다.)</p>
<p>따라서 상위 타입을 하위 타입으로 치환하는 리스코프 치환 원칙은 상속의 다형성에 관한 것이고, 개방 폐쇄 원칙과 관련하여 이를 보완해 주는 원칙이다.</p>
<br/>

<p>개방 폐쇄 원칙에서 추상화 또는 상속으로 여러 타입을 가질 수 있게 된 객체는 반드시 상위 타입의 객체를 가지며, 마찬가지로 상위 타입의 객체는 하위 타입의 객체를 가지게 된다.</p>
<p>이러한 상황에 상위 타입의 코드로 이루어진 메소드에서 <strong>하위 타입으로 대신해도 메소드에는 아무 문제가 없어야 한다</strong>는 의미다.</p>
<br/>


<pre><code class="language-java">public class SuperClass {
    public void someMethod(){
        System.out.println(&quot;it is SuperClass&quot;);
    }
}</code></pre>
<br/>

<p>위 클래스를 상속받는 subClass를 생성해 보자.</p>
<pre><code class="language-java">public class SubClass extends SuperClass{
    public void childMethod(){
        System.out.println(&quot;it is SubClass&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class Main {
    public static void main(String[] args){

        printing(new SuperClass());

        //인스턴스를 변경해도 printing 메소드는 정상적으로 작동함.
        printing(new SubClass());


    }

    public static void printing(SuperClass instance){

        //이 메소드는 무조건 it is SuperClass를 출력해야 한다.
        instance.someMethod();

    }

}</code></pre>
<br/>

<p>위의 코드에서 printing 메소드는 SuperClass 타입을 매개변수로 받는다. 또한 printing 메소드의 임무는 무조건 it is SuperClass를 출력해야 한다.</p>
<p>SubClass는 자체적으로 childMethod라는 메소드도 가지고 있지만, 
SuperClass의 someMethod라는 메소드를 상속 받아 가지고 있기 때문에 부모 타입으로 생성 되어도 someMethod라는 메소드를 실행하는데 아무런 문제가 없다!</p>
<p>따라서 위는 리스코프 치환 원칙을 지키는 코드라고 할 수 있다.</p>
<p>결과:
<img src="https://velog.velcdn.com/images/moony_-/post/2d811b03-f9e9-4e44-a64f-8c8b208111ac/image.png" alt=""></p>
<p>하지만 위의 상속의 예에는 맞지만 리스코프 치환 원칙을 어기는 상황이 있다.</p>
<p>대표적인 예로 <a href="https://en.wikipedia.org/wiki/Circle%E2%80%93ellipse_problem">원-타원 문제(Circle-ellipse problem)</a> 가 있다.</p>
<p>이는 리스코프 치환 원칙의 계약에 관한 내용을 잘 설명해 준다.</p>
<br/>

<details>
  <summary>원-타원 문제?</summary>
    <div markdown="1">

<p>x,y평면 상에 a만큼의 반지름으로 원을 그리는 공식은 x2+y2=a2 이다. 피타고라스의 정리를 이용하여 a만큼의 빗변(반지름)을 가지기 위해선 √(x2+y2)=a가 되어야 하기 때문이다.</p>
<p>타원은 &quot;두 점으로부터 거리의 합이 일정한 점들의 모임&quot;이다.
위의 원과 비슷한 이유로 x,y평면 상에 세로축 2a, 가로축 2b만큼의 타원을 그리려면 피타고라스의 정리를 사용해 양 변을 정리하여 x2/a2+y2/b2=1의 식을 이용하여 그릴 수 있다.</p>
<p>만약 타원의 두 점의 위치가 같다면, 두 점으로부터 거리의 합이 일정한 점들을 모으면 원이 된다. 따라서 타원은 원이 아니지만, 원은 타원이 된다. 즉 타원이 좀 더 넓은 범위에 속한다.</p>
<br/>

<p>이는 &quot;원은 타원이다.&quot;라는 것이 성립하기 때문에 IS-A법칙이 맞게 된다. 따라서 우리는 타원의 클래스를 만들고, 그를 상속 시킨 원의 클래스를 만들 수 있게 된다.</p>
<p>먼저 타원 클래스를 만들어 보겠다.</p>
<pre><code class="language-java">public class Ellipse {
    private int verticalAxis;
    private int horizontalAxis;

    public void setVerticalAxis(int verticalAxis){
        this.verticalAxis=verticalAxis;
    }

    public void setHorizontalAxis(int horizontalAxis){
        this.horizontalAxis=horizontalAxis;
    }

    public int getVerticalAxis(){
        return verticalAxis;
    }

    public int getHorizontalAxis(){
        return horizontalAxis;
    }
}
</code></pre>
<br/>

<p>이렇게 장축과 단축을 따로 설정하여 타원을 만드는 클래스를 작성하였다.</p>
<p>원은 타원과 다르게 지름 하나만 설정할 수 있다. 따라서 장축과 단축을 따로 설정할 필요가 없다.</p>
<br/>

<p>하지만 setVerticalAxis, setHorizontalAxis 둘다 사용해야 하기 때문에 둘 중 하나만 메소드를 실행시켜도 두 변수 verticalAxis HorizontalAxis를 같은 값으로 초기화하도록 메소드를 override 해보자.</p>
<pre><code class="language-java">public class Circle extends Ellipse{

    @Override
    public void setVerticalAxis(int verticalAxis){
        super.setVerticalAxis(verticalAxis);
        super.setHorizontalAxis(verticalAxis);

    }

    @Override
    public void setHorizontalAxis(int horizontalAxis){
        super.setVerticalAxis(horizontalAxis);
        super.setHorizontalAxis(horizontalAxis);
    }

}</code></pre>
<br/>

<p>이제 이 두 클래스가 리스코프 치환 원칙을 지키는지 알아 보자.</p>
<br/>

<p>타원은 세로축과 가로축이 다른 값을 가질 수 있다. 따라서 우리가 원하는 축만 길이를 늘릴 수 있다.</p>
<p>이렇기 때문에 우리는 마음껏 세로축과 가로축을 세팅할 수 있다.
가로로 긴 타원을 만드는데, 가로축은 세로축의 두 배로 만들어 찌그러진 타원을 만들고 싶다고 가정해 보자.</p>
<p>그렇다면 세로축을 가져와 가로축을 세로축의 두 배로 변경할 수 있다.</p>
<pre><code class="language-java">public static void setLongEllipse(Ellipse ellipse){
    int vertical=ellipse.getVerticalAxis();
    ellipse.setHorizontalAxis(vertical*2);

}</code></pre>
<br/>

<p>여기에 리스코프 치환 원칙을 적용시켜 매개변수에 circle이 들어 온다면 어떻게 될까?</p>
<p>가로축과 세로축이 같은 Circle은 위와 같이 가로축을 늘린다면 세로축도 같은 크기로 늘기 때문에 영원히 가로축이 세로축의 두 배가 되는 일은 일어나지 않는다.</p>
<p>따라서 예상하지 못한 곳에서 버그가 발생할 수 있다.</p>
<br/>

<p>물론 instanceof로 인스턴스가 circle인지, ellipse인지 확인 후 예외를 처리할 수 있지만, 반대로 instanceof를 사용한다는 것은 리스코프 치환 원칙을 어긴다는 증거가 된다.</p>
<p>따라서 원과 타원은 상속 관계가 될 수 없고 따로 클래스를 작성하여야 한다.</p>
<br/>

<p>이런 일이 발생하는 이유는 무엇일까?</p>
<br/>

<blockquote>
<p>ellipse의 setVerticalAxis 메소드는 이름 그대로 &quot;세로축을 늘린다.&quot;라는 행동을 수행하는 메소드다.</p>
</blockquote>
<p>따라서 우리는 메소드의 이름으로 &quot;세로 축 만을 늘리는 메소드&quot;라고 약속을 하고 있는 것이다.</p>
<p>하지만 circle 클래스에서 우리는 그 약속을 깨고 setVerticalAxis안에 setHorizontalAxis까지 수행하게 만들었습니다. 그러기에 리스코프 치환 원칙에 위배된다.</p>
</div>
</details></h3>



<br/>


<p>이번에는 확장에 관하여 한 가지 예를 들어 보겠다.</p>
<br/>

<blockquote>
<p>&quot;우리 가게에 쿠폰 할인 이벤트를 하려고 한다. 하지만 몇몇 품목들은 할인에서 제외하도록 하려 한다. 어떻게 해야 할까?&quot; </p>
</blockquote>
<br/>

<p>먼저 물건을 팔기 위해선 &quot;물건&quot;이란 클래스가 필요하다.</p>
<p>그럼 먼저 물건을 만들어 보자.</p>
<pre><code class="language-java">public class Item {
    private int price;
    public Item(int price){
        this.price=price;

    }
    public void setPrice(int price){
        this.price=price;
    }
    public int getPrice(){
        return price;
    }

}</code></pre>
<br/>

<p>이제 쿠폰을 만들어서 아이템을 할인해 보자.</p>
<pre><code class="language-java">public class Coupon {
    private double discountRate;

    public Coupon(double discountRate){
        this.discountRate=discountRate;   
    }

    public double discount(Item item){
        return item.getPrice()*discountRate;
    }

}</code></pre>
<br/>

<p>쿠폰을 가지고 있으면 discount 메소드를 사용하여 아이템의 가격을 할인하는 클래스를 만들었다.</p>
<br/>

<p>자 이제 할인이 되지 않는 아이템을 만들어 보자.
할인이 되지 않는 아이템도 아이템이니(IS-A) 아이템을 상속하여 클래스를 만들어 보자.</p>
<pre><code class="language-java">public class SpecificItem extends Item{

    public SpecificItem(int price) {
        super(price);
    }
}</code></pre>
<br/>

<p>만약 여기서 Coupon의 discount에 Item 타입 대신에 SpecificItem이 들어 간다면 어떻게 될까?</p>
<p>SpecificItem은 할인이 가능할 것이다. 이러한 문제를 막기 위해서 우리는 간단히 생각해서 instanceof를 사용하는 방법을 떠올릴 것이다.</p>
<pre><code class="language-java">public class Coupon {
    private double discountRate;

    public Coupon(double discountRate){
        this.discountRate=discountRate;
    }

    public double discount(Item item){
        if(item instanceof SpecificItem)
            return item.getPrice();
        return item.getPrice()*discountRate;
    }

}</code></pre>
<br/>

<p>하지만 instanceof를 사용하면 리스코프 치환 원칙을 어기게 된다.
상위 또는 하위 타입으로 바꿔도 정상 동작하지 않는다는 증거이기 때문이다.</p>
<p>또한 개방 폐쇄 원칙에 따라 Item은 확장에는 열려 있고 수정에는 닫혀 있어야 하지만, 이렇게 된 다면 Item 클래스의 하위 타입이 새로 생길 때 마다 기존에 있던 discount 코드를 계속 수정해야 할 지도 모른다.</p>
<p>이는 두 번째 원칙인 개방 폐쇄 원칙을 지키기 어렵게 만들게 된다. </p>
<br/>

<p>우리는 어떻게 해결해야 할까?</p>
<p>위의 Item 클래스는 추상화가 덜 된 케이스이다.</p>
<p>할인되지 않는 상품이 추가되었다는 것은 &quot;정해진 기간만 할인하는 상품&quot;, &quot;출시 된 지 한 달 이내는 할인이 불가능한 상품&quot; 등 여러 요구가 후에 생길 확률이 높다는 것이다.</p>
<p>따라서 Item 클래스에 할인이 되는지, 되지 않는 지를 결정하는 코드를 넣어 더욱 &quot;확장에는 열려있고, 변경에는 닫힌 코드&quot;로 만들어야 한다.</p>
<pre><code class="language-java">public class Item {
    private int price;
    public Item(int price){
        this.price=price;

    }
    public void setPrice(int price){
        this.price=price;
    }
    public int getPrice(){
        return price;
    }

    //요구에 맞춰 확장
    public boolean isDiscountAvailable(){
        return true;
    }

}</code></pre>
<pre><code class="language-java">public class SpecificItem extends Item{

    public SpecificItem(int price) {
        super(price);
    }
    @Override
    public boolean isDiscountAvailable(){
        return false;
    }
}</code></pre>
<br/>

<p>따라서 우리는 이후에 들어올 여러 할인 요구 사항도 맞춰서 편하게 확장할 수 있는 코드를 완성하였다.</p>
<pre><code class="language-java">public class Coupon {
    private double discountRate;

    public Coupon(double discountRate){
        this.discountRate=discountRate;
    }

    public double discount(Item item){
        if(item.isDiscountAvailable())
            return item.getPrice()*discountRate;
        return item.getPrice();
    }

}</code></pre>
<p>이처럼 리스코프 치환 원칙은 개방 폐쇄 원칙을 좀 더 잘 파악할 수 있게 해 주는 기준을 제공한다는 것을 배웠다.</p>
<br/>

<br/>

<h2 id="4-인터페이스-분리-원칙-interface-segregation-principle-isp">4. 인터페이스 분리 원칙 (Interface segregation principle; ISP)</h2>
<br/>

<p>밑은 인터페이스 분리 원칙의 정의이다.</p>
<blockquote>
<p>클라이언트는 자신이 사용하는 메소드에만 의존해야 한다.</p>
</blockquote>
<p>조금 말이 어렵다. 위키 백과에서는 어떻게 정의 되어있을까?</p>
<blockquote>
<p>&quot;인터페이스 분리 원칙은 클라이언트가 자신이 사용하지 않는 메소드에 의존하지 않아야 한다.&quot;</p>
</blockquote>
<br/>

<p>이렇게 말을 조금 바꾸어 본다면 이 원칙을 정의한 개발자가 무슨 상황에 처해서 곤란했는지 알 수 있다.</p>
<p>사용하지도 않는 메소드 때문에 코드를 바꾸거나 귀찮은 일을 경험했을 것이다.</p>
<p>이런 일이 어떤 상황에서 일어날까?</p>
<p>한 상황을 가정해 봅시다.</p>
<blockquote>
<p>&quot;음식점을 찾는 어플을 만드려고 한다. 음식점 찾기, 음식점 목록에서 음식점 삭제하기, 음식점 목록에 음식점 추가하기 이 4 가지의 기능을 만드려고 한다. 어떻게 해야 할까?&quot;</p>
</blockquote>
<br/>

<p>이번 상황에서 만큼은 Java가 아닌 C++ 언어로 생각해 보자.</p>
<p>왜냐하면 C++에서 인터페이스 분리 원칙을 지켜야 하는 이유가 더욱 드러나기 때문이다.</p>
<p>위의 기능을 가지는 모듈, 또는 클래스를 restaurant service라고 해보자.</p>
<p>C++언어는 보통 헤더 파일과 cpp파일로 나누어 모듈을 개발한다.</p>
<p>헤더 파일에는 인터페이스처럼 메소드와 필드의 정의를, cpp파일에는 정의된 메소드와 필드를 구현하고 있다.</p>
<p>따라서 cpp 파일은 import처럼 #include 키워드를 이용하여 RestaurantService.h 헤더 파일을 가져와 구현한다.</p>
<br/>


<p>구현이 다 되었다면, 이 기능을 사용하는 클래스(클라이언트)는 마찬가지로 #include를 사용하여 cpp 파일이 아닌 RestaurantService.h 파일을 가져와 사용하게 된다.</p>
<p>우리는 이 원칙을 지키지 않을 때, 어떠한 일이 벌어지는지 알아보기 위하여 일부러 &quot;사용하지 않는 메소드를 의존&quot;하게 만들어 보자.</p>
<br/>

<p>RestaurantService.h에 우리가 구현해야 하는 음식점 찾기, 음식점 목록에서 음식점 삭제하기, 음식점 목록에 음식점 추가하기 기능 모두를 구현해 보자.</p>
<p>그럼 RestaurantService.h를 의존하는 파일들의 관계는 밑의 사진처럼 된다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/ec0f133b-23a7-460a-b15d-25e74648f810/image.png" alt=""></p>
<p>자 이제 실행을 해 보자.</p>
<p>C++은 컴파일과 링크를 직접 해주는 언어다.</p>
<p>따라서 밑과 같은 절차를 따르는 데,</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/215b8abe-e299-4f1c-81e2-c312180f3efb/image.png" alt=""></p>
<p>하나의 파일을 컴파일 하여 기계어로 번역한다.</p>
<p>기계어로 번역한 파일은 object(목적) 파일이라고 부른다.</p>
<p>이렇게 하나 하나 번역된 파일들을 한 군데로 모아 exe로 만들어 주는 것을 링크라고 한다.</p>
<blockquote>
<p>링크는 원래 동적 링크와 정적 링크로 나뉘는데, 깊은 내용이니 여기에서는 정적 링크라 하자.</p>
</blockquote>
<br/>

<p>이렇게 링크롤 하고 나면 exe로 만들어져 파일을 실행할 수 있게 된다.</p>
<p>하지만 모든 설계 원칙이 그랬듯이, 문제는 프로그램의 변화에서 일어난다.</p>
<br/>

<p>만약 음식점 삭제에서 요구 사항이 발생해 이를 바꾸자고 한다.</p>
<p>따라서 RestaurantService.h에 새로운 메소드를 작성하고 음식점 삭제 UI파일만 컴파일, 링크를 해주었다.</p>
<p>하지만 RestaurantService를 참조하는 파일은 음식점 삭제 UI뿐이 아니다.</p>
<p>따라서 RestaurantService를 참조하는 음식점 찾기 UI, 음식점 목록 보여주기 UI, 음식점 목록에 음식점 추가하기 UI 까지 모두 컴파일과 링크를 해 주어야 한다.</p>
<br/>

<p>변경 점은 단 하나, 음식점 삭제하기 UI만 요구 사항이 들어 왔으나 이것과 상관 없는 파일들 까지 영향이 갔다.</p>
<p>SOLID원칙이 싫어한다고 계속 외쳐 대던 클래스간의 연결성 떨어트리기, 변경의 영향을 최소화 하기가 깨진 것이다!</p>
<p>이러한 일들을 해결하기 위해선 밑의 그림처럼 하나의 기능에 하나의 헤더 파일을 작성해야 한다. 그렇다면 다른 파일들을 컴파일 할 필요가 없어진다.</p>
<br/>

<blockquote>
<p>&quot;난 C++을 사용할 일이 없는데, 이런 규칙을 따를 필요가 있나?&quot;</p>
</blockquote>
<p>실제로 이런 규칙은 Java에서는 재 컴파일을 하는 상황이 발생하지 않는다. 우리 유능한 JVM이 알아서 해 주기 때문이다. <del><em>(JVM 만세)</em></del></p>
<p>하지만 이러한 규칙을 잘 지키면 Java에서도 장점은 분명히 있는데,
단일 책임 원칙을 지키기 쉬워 지기 때문이다.</p>
<p>단일 책임이 잘 지켜지지 않을 경우 한 기능의 변화가 다른 기능에게 영향을 미치기 쉽다는 것은 위에서 잘 봐왔다.</p>
<p>따라서 위의 단일 책임 원칙의 상황에서 기능 별로 인터페이스를 분리한다면</p>
<p>그 인터페이스를 상속 받은 클래스(클라이언트)는 서로 영향을 미치지 않는 클래스로 분리하게 된다.</p>
<br/>

<p>이 이외에도 인터페이스 분리 원칙의 장점은 단일 책임 원칙이 잘 지켜 짐으로서 인터페이스의 재사용성이 늘어난다는 것이다.</p>
<p>인터페이스가 다른 기능 없이 한 기능에 집중하여 세부적으로 나누어져 있다면 이 인터페이스를 다시 사용할 가능성이 높아진다.</p>
<p>이 인터페이스가 재사용성이 높아 짐에 따라 이 것을 상속 받은 콘트리트 클래스 또한 재사용성이 높아지는 효과를 볼 수 있다.</p>
<br/>

<p>코틀린 언어로 개발했음에도 불구하고, 음식점 클래스를 여러 곳에서 사용하고 나면 음식점 클래스의 자료형을 바꿀 때마다 클래스를 사용하는 클래스들도 다 바꿔야 했던 경험이 많았다.</p>
<p><del><em>(사실 자료형 추상화도 캡슐화도 안된 케이스..)</em></del></p>
<p>따라서 자바나 코틀린, 다른 객체 지향 언어들도 이 원칙을 지키면 단점보단 장점이 훨씬 많을 것이다.</p>
<br/>

<br/>

<h2 id="5-의존-역전-원칙-dependency-inversion-principle-dip">5. 의존 역전 원칙 (Dependency inversion principle; DIP)</h2>
<br/>

<p>마지막은 의존 역전 원칙이다.</p>
<p>의존 역전 원칙의 정의는 다음과 같다.</p>
<blockquote>
<p>&quot;고수준 모듈은 저수준 모듈의 구현에 의존해선 안된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.&quot;</p>
</blockquote>
<br/>

<p>고수준은 무엇이고, 저수준은 무엇일까?</p>
<blockquote>
<p>프로그래밍 언어는 고급 언어와 저급 언어로 나뉜다.</p>
</blockquote>
<p>프로그래밍을 처음 배울 때, 고급 언어와 저급 언어라는 것을 배운다.</p>
<p>고급 언어는 C, C++, Python과 같이 인간에게 친숙한 언어,
저급 언어는 어셈블리, 기계어와 같이 기계에게 친숙한 언어이다.</p>
<br/>

<p>고급 언어는 저급 언어에 비해 좀 더 인간에 가까운(추상적인) 사고로 작성해도 되는 언이이다.</p>
<p>이와 비슷하게 이해하면 되는데,
고수준 모듈이란 의미가 담겨있는 추상적인 모듈, 저수준 모듈은 구현에 가까운 모듈이다.</p>
<p>즉, 자세한 구현이 담긴 저수준 모듈은 의미가 담긴 추상적인 모듈을 개념을 의존해야 한다는 것이다.</p>
<blockquote>
<p>우리가 팔을 움직일 때, &quot;근육을 사용하여 얼마만큼 수축하라!&quot; 라고 말하지 않는다.
그냥 팔을 움직인다고 생각하면 저수준인 근육은 고수준의 명령(뇌)을 따라(의존해서) 움직인다!</p>
</blockquote>
<br/>

<p>여러 상황 중에 한 가지 예시를 들어 보자.</p>
<blockquote>
<p>&quot;저장된 데이터를 변경하는 프로그램을 작성해 보자.&quot;</p>
</blockquote>
<br/>

<p>위의 예시는 많이 추상화된 예시이다.</p>
<p>데이터를 변경하는 행동에는 &quot;데이터 불러오기&quot;,&quot;데이터 변경하기&quot;,&quot;데이터 저장하기&quot;라는 구체적인 행동들이 따라오기 때문이다.</p>
<p>따라서 &quot;데이터를 변경하는 프로그램&quot;은 고수준,</p>
<p>불러오기, 변경하기 저장하기는 저수준이라고 볼 수 있다.</p>
<p>그럼 이 상태에서 저 원칙을 일부러 위반하여 무슨 현상이 일어나는 지 알아보자.</p>
<br/>

<p>저장된 데이터를 변경하려면 먼저 데이터를 불러와야 한다.</p>
<p>따라서 Local에서 데이터를 불러오는 기능을 하는 클래스 DataReader를 작성하겠다.</p>
<pre><code class="language-java">public class DataReader {
    public int getData(){
        int data=0;
        //some data reading logic
        return data;
    }
}</code></pre>
<br/>

<p>그 다음 데이터를 변경하는 기능을 하는 클래스 DataChanger를 작성하자.</p>
<pre><code class="language-java">public class DataChanger {
    public int dataChange(int data){
        return data+5;
    }
}</code></pre>
<br/>

<p>이제 데이터를 저장하는 클래스 DataStorage를 작성하자.</p>
<pre><code class="language-java">public class DataStorage {
    public void setData(int data){
        //some data saving logic
    }
}</code></pre>
<br/>

<p>우리는 구체적인 저수준 기능 3 가지를 만들었다.</p>
<p>이제 고수준인 데이터를 변경하는 프로그램을 위의 3가지 모듈을 활용하여 만들어 보자.</p>
<pre><code class="language-java">public class JavaPractice {
    public static void main(String[] args) {


        //create instance
        DataReader reader=new DataReader();
        DataStorage storage=new DataStorage();
        DataChanger changer=new DataChanger();

        //flow logic
        int data=reader.getData();
        data=changer.dataChange(data);
        storage.setData(data);


    }
}</code></pre>
<br/>

<p>먼저 각 기능을 하는 클래스의 인스턴스들을 생성하였다.</p>
<p>그 다음 읽기 -&gt; 변경 -&gt; 저장의 흐름에 맞게 flow logic을 작상하였다.</p>
<p>항상 그렇듯, 기획의 변경이 생겨 난다.</p>
<blockquote>
<p>&quot;데이터를 읽어 오는 방법을 로컬이 아니라 서버에서 받아오게 변경해야 할 것 같아요.&quot;</p>
</blockquote>
<p>위와 같은 요구 사항이 들어 온다면 어떤 코드들이 변경 될까?</p>
<br/>

<p>일단 DataReader 클래스가 변경될 것이다.</p>
<p>새로운 클래스 ServerDataLeader를 만들던지, 혹은 DataReader를 변경하여 서버에서도 데이터를 받아올 수 있게 말이다.</p>
<p>또한 흐름을 담당하는 클라이언트 클래스(main)도 변경되게 될 것이다.</p>
<p>DataReader의 인스턴스를 생성하고, 그 인스턴스의 메소드도 사용했기 때문이다.</p>
<br/>

<p>각 클래스의 의미는 이러하다.</p>
<ul>
<li><p><strong>main - 데이터를 읽어와 변경하고 저장하는 흐름을 제어한다.</strong></p>
</li>
<li><p><strong>DataReader - 요구 사항에 맞는 데이터를 저장소에서 불러와 Return한다.</strong></p>
</li>
<li><p><strong>DataChanger - 데이터를 받고, 그 데이터를 변경한 후에 데이터를 Return 한다.</strong></p>
</li>
<li><p><strong>DataStorage - 데이터를 받고, 그 데이터를 요구 사항에 맞는 저장소에 저장한다.</strong></p>
</li>
</ul>
<br/>

<p>우리는 데이터를 불러오는 기능 하나만을 변경했지만, 변경되는 것은 DataReader 하나만이 아닌 main까지 변경하게 되었다.</p>
<p>흐름을 제어하는 고수준 모듈인 main은 데이터를 불러오는 기능과는 상관이 없는데, 왜 변경되는 걸까?</p>
<p>이는 추상적인 고수준 모듈인 main이 구체적인 저수준 모듈인 DataReader를 &quot;의존&quot;했기 때문이다.</p>
<br/>

<p>main에서는 Local에서 데이터를 불러오는 DataReader의 콘크리트 클래스 인스턴스를 직접 생성하고, 또 그 인스턴스의 메소드까지 직접 사용하였다.</p>
<p>따라서 main의 구현은 DataReader에 의존(DataReader 클래스의 구현에 좌지우지 된다.)한다고 볼 수 있다.</p>
<p>그렇기 때문에 DataReader가 변경이 된다면 main도 같이 변경되어야 한다.</p>
<br/>

<p>이러한 문제점을 어떻게 해결해야 할까?</p>
<p>답은 또 추상화다.</p>
<p>위에서 말했다시피, 한 객체를 추상할 수록 고수준에 가깝다.</p>
<p>따라서 &quot;데이터를 로컬에서 읽어온다.&quot;를 &quot;데이터를 읽어온다.&quot;로 추상화 해야 한다.</p>
<br/>

<p>추상화를 하기 위해 먼저 &quot;데이터를 읽어 온다.&quot;라는 인터페이스를 작성하자.</p>
<pre><code class="language-java">public interface DataReader {
    public int getData();
}</code></pre>
<br/>

<p>이번에는 새로 들어온 요구 사항인 &quot;서버에서 데이터를 읽어온다.&quot;를 의미하는 클래스 ServerDataReader를 작성해보자.</p>
<p>마찬가지로 DataReader를 상속한다.</p>
<pre><code class="language-java">public class ServerDataReader implements DataReader{
    @Override
    public int getData() {
        int data=0;
        //some data reading logic
        return 0;
    }
}</code></pre>
<br/>

<p>이제 &quot;Data Reader 인스턴스를 생성한다.&quot;라는 의미를 가진 팩토리 클래스 DataReaderFactory를 작성하자.</p>
<pre><code class="language-java">public class DataReaderFactory {
    static public DataReader getDataReader(){
        return new ServerDataReader();
    }
}</code></pre>
<br/>

<p>이제 이렇게 추상화 된 클래스를 main에 접목시키겠다.</p>
<p>이 인터페이스를 상속하는 클래스는 모두 interface 타입으로 생성할 수 있다.</p>
<p>또한 interface에 정의되어 있는 getData 메소드를 상속받은 클래스 모두 공통으로 가지고 있다.</p>
<p>따라서 콘크리트 클래스로 생성하던 방식에서 interface 타입으로 생성하는 방식으로 바꾼다면 같은 타입임에도 불구하고 다른 저수준 기능을 하는 코드를 작성할 수 있다.</p>
<p>그리고 같은 타입임에도 다른 인스턴스를 할당해 주는 역할을 해 주는 클래스인 DataReaderFactory를 따로 만들어 준 것이다.</p>
<br/>

<p>이제 main은 이렇게 바뀔 것이다.</p>
<pre><code class="language-java">public class JavaPractice {
    public static void main(String[] args) {

        //change to interface type
        DataReader reader= DataReaderFactory.getDataReader();


        DataStorage storage=new DataStorage();
        DataChanger changer=new DataChanger();

        //flow logic
        int data=reader.getData();
        data=changer.dataChange(data);
        storage.setData(data);


    }
}</code></pre>
<br/>

<p>아직 의존 중인 DattaStorage와 DataChanger도 위와 같은 방식으로 추상화 하여 바꿔 준다면 외부의 요구사항에 의해 바뀌지 않아도 되는 main이 완성될 것 이다.</p>
<p>만약 로컬과 서버 모두에서 상황에 따라 데이터를 받아와야 하는 상황이 생겼다면, 이제 &quot;로컬에서 데이터를 받아온다.&quot;라는 의미를 가진 클래스를 하나 새로 만들고, &quot;DataReader를 생성한다.&quot;라는 의미를 가진 DataReaderFactory를 수정한다면 main은 자신이 해야 할 일인 흐름제어만 담당하게 된다.</p>
<br/>

<blockquote>
<p>&quot;코드의 양이 늘어 더 복잡해 진 것 아니야?&quot;</p>
</blockquote>
<p>이렇게 말할 수도 있겠다.</p>
<p>하지만 매우 많은 코드들이 있는 큰 프로젝트에서 이 정도의 양이 늘어난 것은 별로 크게 복잡해진 것이 아니다.</p>
<p>오히려 코드의 양이 늘어난 만큼 수정이 용이해진다.</p>
<p>추상화를 하여 의존성을 떨어트리면 한 객체당 하나의 책임을 가지게 되고, 이는 단일 책임 원칙을 지키기 쉬워진다.</p>
<p>또한 하나의 책임(기능)을 변경할 일이 생기면 그 책임이 변경됨에 따라 나올 여파를 신경 쓰지 않고 변경할 수 있게 되기 때문에 수정해야 할 코드를 찾기가 쉬워 진다.</p>
<br/>

<p>위의 예시를 현실적으로 바라본다면,</p>
<p>Data Reading 기능을 변경할 때 산더미 많큼 쌓여있는 코드들과 클래스들 중에 이름이 일맥상통하는 ServerDataReader 클래스와 DataReaderFactory를 변경하는 것이</p>
<p>ServerDataReader 클래스와 이 클래스를 사용하는, 어디 있는지 모를 클래스들을 하나 하나 다 살펴보는 방법이 훨씬 어려울 것이다.</p>
<p>따라서 코드의 양이 많으면 많을 수록(큰 프로젝트일 수록) 이 방법은 더욱 빛이 날 것이다.</p>
<br/>

<br/>

<h2 id="끝">끝</h2>
<p>이렇게 다섯 가지 설계 원칙을 알아 보았다!</p>
<p>가장 긴 글이 되었는데, 꼼꼼히 읽어보고 다시 공부하고 해 보자...</p>
<br/>

<blockquote>
<p>오류가 있다면 열심히 지적해 주세요!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[객체 지향과 디자인 패턴] 인터페이스를 작성하는 이유]]></title>
            <link>https://velog.io/@moony_-/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EA%B3%BC-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%A5%BC-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@moony_-/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EA%B3%BC-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%A5%BC-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Wed, 13 Apr 2022 16:36:47 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<br/>

<p>저는 평소에 인터페이스를 자주 사용하지 않습니다. 보통 작은 프로젝트를 만들기 때문에 제가 생각한 대로, 설계한 대로 만들면 콘크리트 클래스만 만들어도 문제가 없기 때문입니다.</p>
<p>또한 기획의 일부가 바뀌게 되어도 별로 큰 타격을 입지 않는데, 클래스를 사용한 위치, 사용하는 이유 등등 명확히 알고 있기 때문이죠.</p>
<br/>

<p>하지만 여러 사람이 하나의 프로젝트를 만들 때는 이야기가 다른 것 같습니다. &quot;이렇게 만듭시다.&quot;라고 모두에게 말하면 모두 똑같이 이해하고 만들기 힘들죠!</p>
<p>혹은 변경 점이 생겼을 때 남이 만든 코드의 변경된 클래스 사용처를 알 수 있을까? 그리고 그것을 잘 고칠 수 있을까? 이런 의문들도 함께 남습니다.</p>
<p>위에서 얘기한 문제점 두 가지는 협업 때의 규약의 부재와 변경의 유연성 부재 라고 생각합니다. 우리는 이 것을 인터페이스로 해결할 수 있죠!</p>
<br/>

<p>해결하기에 앞서 먼저 한 상황을 가정해 봅시다.</p>
<blockquote>
<p>&quot;우리는 서버로부터, 혹은 로컬에서 데이터를 가져오는 시스템을 만들라는 명령을 받았다. 우리 팀에는 미국인과 일본인 동료가 있다. 두 분은 매우 뛰어난 베테랑 프로그래머이지만, 난 불행하게도 일본어, 영어 모두 못한다. 이들에게 각각 로컬 데이터 받아오기, 서버 데이터 받아오기를 시키려고 한다. 어떻게 해야 할까?&quot;</p>
</blockquote>
<br/>

<br/>

<h3 id="규약의-부재">규약의 부재</h3>
<br/>
인터페이스는 내용은 없고 형식만 있는 형태입니다. 그렇기에 다른 클래스로 implement를 하여 내용을 작성해야 완성되죠. 일단 미국인 동료에게 시킬 데이터베이스에서 데이터 가져오기를 interface로 작성해 봅시다.

<pre><code class="language-java">public interface DataProviderWithDataBase {
    abstract int getDataFromDataBase();
}</code></pre>
<br/>

<p>자 이제 이 interface를 미국인 동료의 클래스에 implement시켜 봅시다. implement는 한글로 &quot;시행하다&quot; 즉, 무언가를 시행하라고 명령하는 것이기에 인터페이스는 implement를 한 클래스에게 자신에게 정의된 메소드, 필드 껍데기의 구현을 강제하게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/cdbc9cb6-6a9a-4dba-b201-8b3d3cf08542/image.png" alt=""></p>
<p>(implement를 해 놓고 구현을 하지 않으면 이렇게 에러 메시지를 띄운다.)</p>
<br/>

<p>이렇게 강제하게 되면 DataProviderWithDataBase 안에 있는 getDataFromDataBase 메소드를 구현해야(재정의 해야) 에러 메시지가 없어 집니다. Implement methods를 누르거나, 혹은 Alt+Shift+Enter를 눌러 메소드를 재정의 해 봅시다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/bc1d100d-88c4-4dd9-8221-d000c5e8ed82/image.png" alt=""></p>
<p>자 이제 에러는 사라졌으며, interface 안에 있던 구현해야 할 메소드가 보입니다. 베테랑 프로그래머인 미국인 동료는 자신이 해야 할 일을 메소드의 이름을 통해 단번에 알아차렸고, 그는 구현을 시작할 것입니다.</p>
<br/>

<p>마찬가지로, 일본인 동료도 똑같이 interface를 만들어 줍시다.</p>
<pre><code class="language-java">public interface DataProviderWithServer {
    abstract int getDataFromServer();

}</code></pre>
<br/>



<p>자. 이제 그도 일을 시작할 것입니다.</p>
<br/>

<br/>

<h3 id="유연성의-부재">유연성의 부재</h3>
<br/>

<p>자 모두 해결한 것 같습니다. 일단 이들이 구현한 클래스로 데이터를 받아 와 봅시다!</p>
<pre><code class="language-java">public int getData(Boolean isServer){
    if (isServer){
        American american=new American();
        return american.getDataFromDataBase();
    }
    else{
        Japanese japanese=new Japanese();
        return japanese.getDataFromServer();
    }
}
</code></pre>
<br/>

<p>두 부분을 구분하기 위해 Boolean자료형을 이용하여 구현하였습니다. 하지만 무언가 찜찜 합니다.</p>
<p>마침 위에서 기획을 바꾼다고 지시가 내려왔습니다. 다른 곳에서도 데이터를 받아와야 한다고 하는데, 이번에는 중국인 동료가 있습니다.. 또 인터페이스를 작성해야 할까요..?</p>
<p>곰곰히 생각해 보면 사실 이 세 부분에서 데이터를 받아 오는 것은 공통점이 존재합니다. 바로 &quot;데이터를 받아온다&quot; 라는 것으로 묶을 수 있다는 것이죠! 이 개념적인 것을 interface로 뽑아 봅시다.</p>
<pre><code class="language-java">public interface DataProvider {
    abstract int getData();
}</code></pre>
<br/>



<p>각각 DataProviderWithDataBase, DataProviderWithServer로 나누어 졌던 것을 DataProvider로 한 곳으로 모았습니다. 그리고 getData라는 추상 메소드로 통합하였죠. 또한 중국인 동료에게 줄 추가될 인터페이스도 데이터를 가져 온다는 것으로 구현을 할 수 있기 때문에 일맥상통합니다.</p>
<p>자 이제 세 동료 모두에게 이 interface를 implement 시켜 구현하도록 부탁해 봅시다.</p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/8af18852-2714-4a70-a6aa-43d42b22cf27/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/b00180e7-f9ee-45ec-ab0c-8c43b640ee33/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/moony_-/post/34a05889-06c2-41c8-92a4-43d484873187/image.png" alt=""></p>
<p>자, 다들 자신의 임무에 알맞게 구현을 해왔습니다.</p>
<br/>

<p>먼저 우리는 각각 다른 interface로 각각 다른 class를 구현하였고, 각각의 class로 instance를 생성하여 method를 실행시켜 return 하였는데요.</p>
<p>하지만 이번엔 하나의 interface로 통합 시킨 후 각각의 class를 구현하였습니다. 제가 자주 까먹는 것이 있는데, 굳이 자신의 class 타입으로 instance를 생성하지 않아도 된다는 겁니다!</p>
<br/>

<p>implement 혹은 extend를 한 class는 상위 class, 혹은 interface의 타입으로 instance를 생성할 수 있다는 것이죠. 여기서 interface의 장점이 나옵니다.</p>
<p>즉, 위의 DataProvider로 구현한 class 세 개는 DataProvider의 타입으로 통합할 수 있습니다.</p>
<pre><code class="language-java">public int getData(int flag){

    DataProvider provider = null;
    if(flag==Flag.GET_FROM_SERVER){
        provider=new American();
    }
    else if(flag==Flag.GET_FROM_OTHER_SERVER){
        provider=new Chinese();
    }
    else if(flag==Flag.GET_FROM_DATABASE){
        provider=new Japanese();
    }

    //변하지 않는 곳.
    if(provider!=null){
        return provider.getData();
    }
    else{
        throw new NullPointerException();
    }

}
</code></pre>
<pre><code class="language-java">public class Flag {
    GET_FROM_SERVER, GET_FROM_OTHER_SERVER, GET_FROM_DATABASE
}
</code></pre>
<br/>

<p>3개의 선택지로 바뀌었으니 Boolean으로 구분 하던 것을 int로 바꾸어 주었고, 구분을 쉽게 하기 위해 Flag라는 클래스를 새로 만들어 그 안에 flag를 넣어 주었습니다.</p>
<p>또한 기존에 있던 함수에도 변화가 생겼는데, 데이터를 제공하는 클래스의 타입이 interface인 DataProvider로 변경 되었습니다.</p>
<p>미국인, 일본인, 중국인 동료 각각 메소드의 내용을 따로 작성하였어도, 메소드의 이름이 모두 같기 때문에 interface 타입으로 메소드를 실행 시켜도 아무 문제가 없습니다.</p>
<p>또한 할당한 객체에 따라 같은 타입이어도 다른 행동을 하는 메소드를 실행시킬 수 있게 됩니다!</p>
<p>그리고 변하지 않는 부분이 생겼기 때문에 새로운 방법으로 데이터를 가져오는 로직이 생겨도, 바꾸는 코드는 줄어들게 되었죠!</p>
<br/>

<p>좀 더 다듬는 방법은 팩토리 메소드 패턴을 이용하여 DataProvider의 생성을 따로 작성할 수 있습니다. 밑과 같이 하면 클래스 간의 연결성이 더욱 떨어집니다.</p>
<br/>

<pre><code class="language-java">
public class DataProviderFactory {
    private static DataProviderFactory instance;
    private DataProviderFactory(){

    }
    public static DataProviderFactory getInstance(){
        //싱글톤 패턴 적용
        if(instance==null){
            instance=new DataProviderFactory();
        }
        return instance;
    }

    public DataProvider getDataProvider(Flag flag){
        DataProvider provider=null;
        if(flag==Flag.GET_FROM_SERVER){
            provider= new Japanese();
        }
        else if (flag==Flag.GET_FROM_DATABASE){
            provider= new Chinese();
        }
        else if(flag==Flag.GET_FROM_OTHER_SERVER){
            provider= new American();
        }
        return provider;


    }

}
</code></pre>
<p>위의 코드의 양 만으로 보아선 간단해 지지 않았습니다. 코드의 양이 더욱 늘었죠. 하지만 더욱 많은 상황을 떠올려 보면 장점이 더욱 부각 됩니다.</p>
<p>만약 DataProvider가 여러 곳에서 참조해야 하는 상황이 생겼다면?</p>
<p>이 전의 구현에서는 객체 타입이 서로 다르기 때문에 참조하는 곳 마다 if, else를 달아 주어야 될 것이고, 새롭게 데이터를 받아오는 로직이 생기면 또 if를 달아 주어 코드의 양이 기하급수적으로 늘어날 것 입니다.</p>
<p>따라서 DataProvider 하나로 통합하면, 각각 다른 경로로 데이터를 받아오는 메소드를 단 하나의 타입으로 모두 수행할 수 있게 되죠!</p>
<p>이 뿐만 아니라, 이 인터페이스 타입을 사용하는 측에서도 이익이 있습니다.</p>
<p>interface가 먼저 구현이 되지 않아도, interface 타입을 사용하는 쪽에서 자신의 로직을 테스트 할 수 있게 됩니다. 하드 코딩을 통해서 임시로 interface를 구현하는 것이죠.</p>
<pre><code class="language-java">public int process(){
    MockDataProvider mock=new MockDataProvider();
    //test logic
    int data= mock.getData();
    data-=1000;
    return data;
}


class MockDataProvider implements DataProvider{

    @Override
    public int getData() {
        //hard coding
        int itIsTestData=99999;
        return itIsTestData
    }
}</code></pre>
<p>이렇게 모두 생산성에 이익을 보며 같이 작업할 수 있게 됩니다.</p>
<p>이번에는 interface의 사용처에 대해서 알아보았습니다. 큰 프로젝트 경험은 없지만, 이렇게 간접적으로 문제를 해결해 나가며 다음에도 작성해 보겠습니다.</p>
<p>틀린 부분이 있다면 적극 반영하겠습니다. 감사합니다!</p>
<p>(이 포스팅은 책 &quot;객체 지향과 디자인 패턴&quot;을 읽고 재구성한 글 입니다.)</p>
]]></description>
        </item>
    </channel>
</rss>