<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dyno-jun.log</title>
        <link>https://velog.io/</link>
        <description>사람들에게 긍정적 에너지와 즐거움을 주는 개발자</description>
        <lastBuildDate>Thu, 22 May 2025 12:54:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dyno-jun.log</title>
            <url>https://velog.velcdn.com/images/dyno-jun/profile/6d9c2fde-931d-46fd-9865-17e14002ea26/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dyno-jun.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dyno-jun" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Coding Agent] 바이브코딩 - Copilot Pro+ ]]></title>
            <link>https://velog.io/@dyno-jun/Coding-Agent-Copilot</link>
            <guid>https://velog.io/@dyno-jun/Coding-Agent-Copilot</guid>
            <pubDate>Thu, 22 May 2025 12:54:24 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-server-starter-copilot">spring-server-starter-copilot</h1>
<h2 id="일단-결제">일단 결제</h2>
<ul>
<li>36.48 USD =&gt; <strong>50,371원</strong> 비싸다...
<img src="https://velog.velcdn.com/images/dyno-jun/post/259049d1-e330-4a45-aa87-7a1ca09fe702/image.png" alt=""></li>
</ul>
<h2 id="문제점">문제점</h2>
<ul>
<li>매번 반복되는 작업이 개발 속도를 저해한다.</li>
<li>프로젝트 시작 시 가장 많은 시간을 잡아먹는 건 설정(config) 이다.</li>
<li>비즈니스 로직 중심의 코드 리뷰에 집중하고 싶다.</li>
<li>도메인만 정의하면 나머지 구현체들은 자동으로 생겼으면 좋겠다.</li>
</ul>
<h2 id="도전-과제">도전 과제</h2>
<ul>
<li>템플릿 코드를 만들어 반복 작업 제거</li>
<li>Copilot Agent를 활용해 기본 구현체 자동 생성</li>
<li>정적 분석, 린팅, 코드 포맷팅, 리뷰 자동화 → 코드리뷰는 비즈니스 로직에만 집중</li>
<li>DDD 기반 개발 → 도메인 모델에 집중할 수 있는 구조 설계</li>
</ul>
<h2 id="copilot-agent-활용">copilot agent 활용!!</h2>
<h4 id="1-이슈-등록">1. 이슈 등록</h4>
<p><img src="https://velog.velcdn.com/images/dyno-jun/post/0bf3714e-5d51-4c8c-822d-b573f0819f9e/image.png" alt=""></p>
<h4 id="2-코파일럿-이슈-할당">2. 코파일럿 이슈 할당</h4>
<p><img src="https://velog.velcdn.com/images/dyno-jun/post/492a4cec-3b2b-49c4-9a0b-f0f92536ffc8/image.png" alt=""></p>
<h4 id="3-agent-작업-시작-pr-만들고-다음-동작-진행">3. agent 작업 시작 PR 만들고 다음 동작 진행</h4>
<p><img src="https://velog.velcdn.com/images/dyno-jun/post/3d3e1833-8545-41c3-8b96-20da1cd27b0a/image.png" alt=""></p>
<h4 id="4-이슈-관련-코드-분석-하고-todo-작성">4. 이슈 관련 코드 분석 하고 TODO 작성</h4>
<p><img src="https://velog.velcdn.com/images/dyno-jun/post/e572904f-bc84-48ca-b864-ce00e9b329d0/image.png" alt=""></p>
<h4 id="5-구현-끝나면-다시-정리해서-작성해줌">5. 구현 끝나면 다시 정리해서 작성해줌</h4>
<p><img src="https://velog.velcdn.com/images/dyno-jun/post/5996bcbd-d389-4ed8-a07c-fd7aa43fc244/image.png" alt=""></p>
<h4 id="6-코드-확인">6. 코드 확인</h4>
<p><img src="https://velog.velcdn.com/images/dyno-jun/post/6754ae0f-e1c4-4b13-a796-e3ee81658ffe/image.png" alt=""></p>
<h4 id="7-리뷰로-나머지-함수-추가해줘-하니-반영해줌">7. 리뷰로 나머지 함수 추가해줘 하니 반영해줌</h4>
<ul>
<li>리뷰 작성
<img src="https://velog.velcdn.com/images/dyno-jun/post/fe0cab75-9791-4d31-b27b-1204a06bedf0/image.png" alt=""></li>
<li>반영 코드
<img src="https://velog.velcdn.com/images/dyno-jun/post/93ebd973-7629-4809-8081-09bfbfe44fcf/image.png" alt=""></li>
</ul>
<h4 id="8-코드-분석-툴-실행">8. 코드 분석 툴 실행</h4>
<ul>
<li>spotless</li>
<li>sonarqubecloud 정적 분석 도구</li>
<li>JaCoCo 테스트 커버리지</li>
</ul>
<blockquote>
<p>코드 체크가 안된다 좀더 확인 필요
코드가 없어 전체 테스트 커버리지가 낮음 개선 필요</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dyno-jun/post/6b3a85aa-cc72-4cd1-83aa-e41589806239/image.png" alt=""></p>
<h4 id="9-sematic-활용한-changelog-tag-release-자동-생성">9. sematic 활용한 CHANGELOG, TAG, RELEASE 자동 생성</h4>
<ul>
<li>sematic-release 활용</li>
<li>CHANGELOG.md, tag, release, gradle version 까지 한큐 관리</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dyno-jun/post/4e434408-1590-4e86-857a-882cc6b5c13a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dyno-jun/post/58ed4fae-bbb4-42be-919e-0a6fd0543705/image.png" alt=""></p>
<h4 id="10-최종-코드-병합---cd-연동-예정">10. 최종 코드 병합 -&gt; CD 연동 예정</h4>
<blockquote>
<p>업데이트 예정</p>
</blockquote>
<h2 id="이슈">이슈</h2>
<ul>
<li>.class 파일이 생겨있었음. 바이너리인데 이게 왜 생기는지 모르겠음. 개선해주세요 :)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dyno-jun/post/107caba9-0d97-4603-9fa4-91b936bbad83/image.png" alt=""></p>
<h2 id="느낀점">느낀점</h2>
<blockquote>
<p>반복 작업을 줄이고, AI 시대에 맞는 개발자로 성장하기</p>
</blockquote>
<p>요즘 개발자로서 더 효율적으로 일하는 방법이 뭘까 자주 고민하게 된다.
AI 에이전트들이 점점 많아지는 걸 보면, 단순 구현은 이제 의미 없어질지도 모르겠다.
살아남으려면 반복작업은 줄이고, 생산성을 끌어올려야 한다.
그래서 작은 프로젝트부터 실험해보는 중이다.
아직은 초반이지만, 하나씩 적용해보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] 10년전 내코드를 보며 ]]></title>
            <link>https://velog.io/@dyno-jun/%ED%9A%8C%EA%B3%A0-10%EB%85%84%EC%A0%84-%EB%82%B4%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%B3%B4%EB%A9%B0</link>
            <guid>https://velog.io/@dyno-jun/%ED%9A%8C%EA%B3%A0-10%EB%85%84%EC%A0%84-%EB%82%B4%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%B3%B4%EB%A9%B0</guid>
            <pubDate>Thu, 08 May 2025 06:09:09 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dyno-jun/post/0b69ebaa-cbff-4864-af8b-e72ef4f3b8ed/image.png" alt=""></p>
<p>요즘 진행하고 있는 리팩토링 작업 중, 우연히 10년 전 내가 작성한 코드를 다시 보게 되었다. 단순히 기능을 개선하는 과정이었지만, 그 코드들을 보며 자연스럽게 지난 시간들을 돌아보게 되었고, 이 글을 통해 짧은 회고를 남기고자 한다.</p>
<h3 id="빠르게-만들고-빠르게-고치던-시절">빠르게 만들고, 빠르게 고치던 시절</h3>
<p>초기 몇 년간은 정말 정신없이 코드를 썼다. 사용자의 피드백을 빠르게 반영하고, 신규 기능을 추가하고, 기존 기능을 수정하며 “빠르게 만들고, 빠르게 고치는” 것이 가장 중요한 가치였다. 코드의 품질보다는 변화에 대응하는 속도가 더 중요했고, 실제로 그 방식은 제품이 성장하는 데 있어 꽤 유효했다.</p>
<p>하지만 시간이 지나며, 제품이 점차 안정화되고 시장도 변화하면서 우선순위가 조금씩 바뀌기 시작했다. 기능의 추가보다는 안정성과 유지보수가 중요해졌고, 점점 더 변화보다 ‘지속 가능성’이 중요한 가치로 떠올랐다.</p>
<h3 id="기술-부채의-대가">기술 부채의 대가</h3>
<p>이번 리팩토링 작업의 출발점은 단순한 신규 기능 추가였다. 그런데 막상 들어가 보니 너무 많은 이슈가 얽혀 있었다. 구형 안드로이드 버전, 더 이상 지원되지 않는 라이브러리들, 예상치 못한 CS 이슈들까지…</p>
<p>작은 기능 하나 추가하는 데 과하게 많은 시간과 에너지를 소모하게 된 이유는, 그동안 누적되어 온 기술 부채 때문이었다. 다행히 집중적으로 모듈을 최신화하고 레거시 라이브러리를 교체하며 정리해나갈 수 있었지만, 이번 경험을 통해 몇 가지 깊이 느낀 점이 있다.</p>
<h3 id="소프트웨어도-집처럼-늙는다">소프트웨어도 ‘집’처럼 늙는다</h3>
<p>처음엔 새롭고 반짝이던 코드도, 시간이 지나면 점점 낡고 손보지 않으면 안 되는 상태가 된다. 업데이트를 미루고, 문서를 생략하고, 테스트 없이 돌아가는 것만 확인한 채 배포했던 코드들은 결국 유지보수의 큰 장애물이 되었다.</p>
<p>‘잘 동작하니까 굳이 손대지 말자’는 마음이 쌓이면, 결국 어떤 기능 하나를 추가하기 위해서도 전체 구조를 다시 이해하고 하나하나 추적해야만 하는 상황이 된다. 문서화되지 않은 코드, 테스트 없이 돌아가는 함수, 구조화되지 않은 설계는 결국 지금의 나에게 많은 시간을 요구하게 된다.</p>
<h3 id="유지보수-단계에-접어들며-깨달은-것들">유지보수 단계에 접어들며 깨달은 것들</h3>
<p>이번 리팩토링 과정에서 가장 크게 깨달은 점은, 소프트웨어의 라이프사이클에는 리팩토링과 문서화, 테스트 코드, 자동화가 반드시 포함되어야 한다는 것이다.</p>
<p>초기에는 빠른 개발이 중요하다. 하지만 프로젝트가 어느 시점에 이르면 유지보수라는 또 다른 국면에 진입하게 된다. 이때부터는 다음과 같은 요소들이 핵심이 된다.</p>
<ul>
<li>문서화: 코드의 흐름과 의도를 명확히 기록함으로써, 시간이 지나도 낯선 코드가 되지 않도록 한다.</li>
<li>테스트 코드: 시스템의 안정성을 담보하고, 리팩토링이나 기능 추가 시 문제를 조기에 감지할 수 있게 한다.</li>
<li>모듈화: 작은 단위로 기능을 책임지고, 다른 영역에 영향을 주지 않는 범위 내에서 유연하게 수정할 수 있도록 한다.</li>
<li>자동화: 반복되는 작업은 자동화하여, 시간 낭비를 줄이고 실수를 예방한다.</li>
</ul>
<p>이런 기본적인 요소들이 갖춰져 있으면, 시간이 지나도 프로젝트는 여전히 유의미하게 진화해나갈 수 있다.</p>
<h3 id="결론-지속-가능한-개발을-위한-태도">결론: 지속 가능한 개발을 위한 태도</h3>
<p>소프트웨어는 그저 동작만 잘한다고 끝나는 게 아니다. 시간이 지나도 관리 가능한 시스템으로 남아야 진짜 살아있는 코드라고 할 수 있다.</p>
<p>빠르게 만들고 피드백을 반영하는 민첩함도 물론 중요하다. 하지만 그 과정 속에서도 최소한의 구조화, 문서화, 테스트가 병행되어야 한다. 그리고 어느 시점부터는 의도적으로 리팩토링과 정리를 우선순위에 두어야 한다.</p>
<p>이번 경험을 통해 나는 소프트웨어가 가지는 ‘시간의 무게’를 실감했고, 앞으로의 개발에서 이 점을 더 깊이 새기고 싶다. 그리고 지금도 내가 만들어가고 있는 코드들이 언젠가 다시 내게 돌아올 때, 조금 더 반가운 얼굴로 마주할 수 있기를 바란다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Data 수집] SMS, MMS,  RCS, Notificaiton - Android 편]]></title>
            <link>https://velog.io/@dyno-jun/Data-%EC%88%98%EC%A7%91-SMS-MMS-RCS-Notificaiton-Android-%ED%8E%B8</link>
            <guid>https://velog.io/@dyno-jun/Data-%EC%88%98%EC%A7%91-SMS-MMS-RCS-Notificaiton-Android-%ED%8E%B8</guid>
            <pubDate>Wed, 07 May 2025 06:51:07 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p><img src="https://velog.velcdn.com/images/dyno-jun/post/59cd3b27-a3ac-4da3-af83-9d0fc739dd4d/image.png" alt=""></p>
<p>오늘은 데이터 수집을 위해 안드로이드에서 제공하는 SMS, MMS, RCS, Notification의 개념을 정리하고,
각 메시지 타입의 데이터 수집 방법, 실제 필드 파싱, 그리고 처리 과정에서 발생한 문제와 해결 방안을 공유하고자 합니다.</p>
<p>메시지를 수신하고 데이터를 추출하는 작업은 겉보기에는 단순해 보일 수 있지만,
실제로는 다양한 이슈와 시행착오가 존재했습니다.
이 글은 그런 과정 하나하나를 기록하기 위한 첫걸음입니다.</p>
<p>저는 앞으로 데이터 수집 → 변형 → 적재 → 분석 → AI에 이르는 과정을 직접 다루며,
데이터 엔지니어로서의 여정을 기술 블로그를 통해 정리해나가고자 합니다.</p>
<h2 id="메시지-타입-및-notification-정의">메시지 타입 및 Notification 정의</h2>
<table>
<thead>
<tr>
<th>유형</th>
<th>설명</th>
<th>주요 특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>SMS</strong><br>(Short Message Service)</td>
<td>160자 이하의 단문 텍스트 메시지</td>
<td>- 인터넷 없이 통신망으로 전송<br>- 단일 텍스트로 구조 단순<br>- <code>BroadcastReceiver</code>로 수신 가능 <br>- <code>context.getContentResolver()</code>로 과거 데이터 조회</td>
</tr>
<tr>
<td><strong>MMS</strong><br>(Multimedia Messaging Service)</td>
<td>이미지, 영상, 오디오 등 멀티미디어 메시지</td>
<td>- 멀티파트 메시지 구조<br>- 첨부파일 포함 가능<br>- <code>Broadcast</code> 수신 불가 → <code>ContentProvider</code>에서 폴링 필요</td>
</tr>
<tr>
<td><strong>RCS</strong><br>(Rich Communication Services)</td>
<td>인터넷 기반 고급 메시징 프로토콜</td>
<td>- 읽음 확인, 타이핑 표시 등 채팅 기능<br>- 고화질 미디어 전송<br>- 통신사/구글 메시지 앱 기반<br></td>
</tr>
<tr>
<td><strong>Notification</strong><br>(알림 시스템)</td>
<td>앱에서 발생한 이벤트를 사용자에게 알리는 UI 요소</td>
<td>- 모든 앱에서 사용 가능<br>- <code>NotificationListenerService</code>로 수신<br>- 제목/본문 등 UI 기반 정보 추출<br>- 앱마다 포맷 상이하여 파싱 필요</td>
</tr>
</tbody></table>
<h3 id="데이터-추출">데이터 추출</h3>
<p>공통</p>
<pre><code class="language-kotlin">data class Sms(
    val id: String,
    val message: String,
    val sender: String,
    val displaySender: String,
    val date: String,
    val type: SmsType
)

enum class SmsType {
    SMS, MMS, RCS, Notification
}

object Utils {
    fun getConvertedDate(timestampMillis: Long): String {
        val date = Date(timestampMillis)
        val sdf = SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;, Locale.getDefault())

        return sdf.format(date)
    }

    fun generateHashId(input: String): String {
        val bytes = MessageDigest.getInstance(&quot;SHA-256&quot;)
        .digest(input.toByteArray(Charsets.UTF_8))

        return bytes.joinToString(&quot;&quot;) { &quot;%02x&quot;.format(it) }
    }
}</code></pre>
<h3 id="sms">SMS</h3>
<h4 id="실시간-수신">실시간 수신</h4>
<p>Receiver</p>
<pre><code class="language-kotlin">// BroadcastReceiver를 상속하여 SMS 수신 이벤트를 감지하는 클래스
class SmsCatcher : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        // 브로드캐스트로부터 전달된 데이터(Bundle)를 가져옵니다
        val bundle = intent.extras ?: return

        // SMS 메시지는 &quot;pdus&quot;라는 이름의 배열로 전달됩니다
        val pdus = bundle.get(&quot;pdus&quot;) as? Array&lt;*&gt; ?: return

        // Android 6.0(M) 이상에서는 format이 필요합니다
        val format = bundle.getString(&quot;format&quot;)

        // 메시지 본문, 발신자, 표시용 발신자, 타임스탬프 초기화
        var messageBody = &quot;&quot;
        var sender = &quot;&quot;
        var displaySender = &quot;&quot;
        var timestamp = 0L

        // pdus 배열을 순회하면서 SmsMessage 객체로 변환 및 정보 추출
        pdus.mapNotNull { it as? ByteArray }           // ByteArray로 캐스팅
            .map { pdu -&gt;
                if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.M) {
                    SmsMessage.createFromPdu(pdu, format)
                } else {
                    SmsMessage.createFromPdu(pdu)
                }
            }
            .forEach { sms -&gt;
                // 다중 메시지를 하나로 이어붙이기
                messageBody += sms.messageBody.orEmpty()

                // 발신자 번호 추출
                sender = sms.originatingAddress.orEmpty()
                displaySender = sms.displayOriginatingAddress.orEmpty()

                // 타임스탬프는 첫 번째 SMS 기준으로만 설정
                if (timestamp == 0L) timestamp = sms.timestampMillis
            }

        // 최종 SMS 객체 생성 (사용자 정의 모델에 맞게 매핑)
        val sms = SMS(
            id = Utils.generateHashId(messageBody + sender + date),
            msg = messageBody,
            sender = sender,
            displaySender = displaySender,
            date = Utils.getConvertedDate(timestamp),
            type = Constants.SmsType.SMS_TYPE_SMS
        )

        // 디버깅 로그 출력 (실제로는 저장, 파싱 등 처리)
        Log.d(&quot;SMS_SIMPLE&quot;, sms.toString())
    }
}</code></pre>
<p>Reciever 등록</p>
<pre><code class="language-Kotlin">&lt;uses-permission android:name=&quot;android.permission.RECEIVE_SMS&quot; /&gt;
&lt;uses-permission android:name=&quot;android.permission.READ_SMS&quot; /&gt;

&lt;receiver android:name=&quot;.SmsCatcher&quot; android:exported=&quot;true&quot;&gt;
    &lt;intent-filter&gt;
        &lt;action android:name=&quot;android.provider.Telephony.SMS_RECEIVED&quot; /&gt;
    &lt;/intent-filter&gt;
&lt;/receiver&gt;</code></pre>
<h4 id="과거-데이터-조회">과거 데이터 조회</h4>
<pre><code class="language-kotlin">import android.content.Context
import android.net.Uri
import android.provider.Telephony
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.*

fun getRecentSmsList(context: Context): List&lt;Sms&gt; {
    val results = mutableListOf&lt;Sms&gt;()

    // 안드로이드의 SMS inbox URI
    val uri = Uri.parse(&quot;content://sms/inbox&quot;)

    // 현재 시간 (밀리초 기준)
    val endTime = System.currentTimeMillis()

    // 3개월 전의 타임스탬프 계산
    val startTime = Calendar.getInstance().apply {
        add(Calendar.MONTH, -3)
    }.timeInMillis

    // 날짜 조건 설정: 최근 3개월간 SMS만 조회
    val where = &quot;date BETWEEN ? AND ?&quot;
    val whereArgs = arrayOf(startTime.toString(), endTime.toString())

    // 날짜 기준 오름차순 정렬
    val sortOrder = &quot;date ASC&quot;

    // ContentResolver로 쿼리 실행
    val cursor = context.contentResolver.query(uri, null, where, whereArgs, sortOrder)

    cursor?.use {
        // 필요한 컬럼 인덱스 정의
        val bodyIdx = it.getColumnIndexOrThrow(&quot;body&quot;)
        val addressIdx = it.getColumnIndexOrThrow(&quot;address&quot;)
        val dateIdx = it.getColumnIndexOrThrow(&quot;date&quot;)

        // 커서를 순회하며 SMS 항목 추출
        while (it.moveToNext()) {
            val body = it.getString(bodyIdx)               // 메시지 본문
            val address = it.getString(addressIdx)         // 발신자 번호
            val timestamp = it.getLong(dateIdx)            // 수신 일시 (ms)
            val dateString = Utils.getConvertedDate(timestamp)   // 사람이 읽을 수 있는 날짜 포맷으로 변환

            // 메시지 본문 + 발신자 + 날짜 조합으로 고유 ID 생성
            val hashId = Utils.generateHashId(body + address + dateString)

            // SMS 데이터 클래스 인스턴스 생성
            val sms = Sms(
                id = hashId,
                message = body,
                sender = address,
                displaySender = address,
                date = dateString,
                type = SmsType.SMS
            )

            results.add(sms)
        }
    }

    return results
}</code></pre>
<h3 id="mms">MMS</h3>
<h4 id="실시간-수신-1">실시간 수신</h4>
<pre><code class="language-kotlin">class MmsCatcher : BroadcastReceiver() {

    companion object {
        const val MMS_ACTION = &quot;android.provider.Telephony.WAP_PUSH_RECEIVED&quot;
    }

    private var mmsHelper: MmsHelper? = null

    override fun onReceive(context: Context, intent: Intent) {
        // MMS는 수신 후 inbox에 저장되기까지 딜레이가 있어 지연 처리
        Handler(Looper.getMainLooper()).postDelayed({
            try {
                // MMSHelper를 통해 최근 수신된 MMS 데이터를 가져옵니다
                val helper = mmsHelper ?: MmsHelper(context).also { mmsHelper = it }
                val mms = helper.getMms()

                // 추출한 MMS 로그 출력 (또는 리스트에 저장 등으로 확장 가능)
                Log.d(&quot;MMS_CATCHER&quot;, &quot;MMS received: $mms&quot;)

            } catch (e: Exception) {
                Log.e(&quot;MMS_CATCHER&quot;, &quot;Error reading MMS&quot;, e)
            }
        }, 3000) // inbox에 완전히 저장되기 위한 지연 (필요 시 조절 가능)
    }
}

class MmsHelper(private val context: Context) {

    /**
     * 가장 최근 수신된 MMS를 읽어와서 SMS 형태로 반환합니다.
     * 내부 예외 발생 시 null 반환.
     */
    fun getMms(): Sms? = try {
        val cursor = context.contentResolver.query(
            Uri.parse(&quot;content://mms/inbox&quot;),
            null,
            &quot;msg_box = 1&quot;,
            null,
            &quot;_id DESC LIMIT 1&quot;
        ) ?: return null

        cursor.use {
            if (!it.moveToFirst()) return null

            val id = it.getInt(it.getColumnIndexOrThrow(&quot;_id&quot;))
            val timestamp = it.getLong(it.getColumnIndexOrThrow(&quot;date&quot;)) * 1000
            val sender = getMmsAddress(id)
            val body = parseMmsBody(id.toString()).take(490)

            Sms(
                id = generateHashId(body + sender + timestamp.toString()),
                message = body,
                sender = sender,
                displaySender = sender,
                date = Utils.getConvertedDate(timestamp),
                type = SmsType.MMS
            )
        }
    } catch (e: Exception) {
        null
    }

    /** 발신자 번호를 content://mms/{id}/addr 에서 추출 */
    private fun getMmsAddress(messageId: Int): String {
        val uri = Uri.parse(&quot;content://mms/$messageId/addr&quot;)
        val cursor = context.contentResolver.query(uri, arrayOf(&quot;address&quot;), &quot;msg_id=$messageId&quot;, null, null)
            ?: return &quot;&quot;

        cursor.use {
            while (it.moveToNext()) {
                val raw = it.getString(it.getColumnIndexOrThrow(&quot;address&quot;))
                if (!raw.isNullOrBlank()) {
                    return raw.replace(&quot;-&quot;, &quot;&quot;)
                }
            }
        }
        return &quot;&quot;
    }

    /** MMS 본문 파트에서 텍스트 추출 */
    private fun parseMmsBody(messageId: String): String {
        val uri = Uri.parse(&quot;content://mms/part&quot;)
        val cursor = context.contentResolver.query(uri, null, null, null, null) ?: return &quot;&quot;

        cursor.use {
            while (it.moveToNext()) {
                val mid = it.getString(it.getColumnIndexOrThrow(&quot;mid&quot;))
                if (mid != messageId) continue

                val type = it.getString(it.getColumnIndexOrThrow(&quot;ct&quot;))
                if (type != &quot;text/plain&quot;) continue

                val data = it.getString(it.getColumnIndexOrThrow(&quot;_data&quot;))
                return if (!data.isNullOrBlank()) {
                    readMmsText(it.getString(it.getColumnIndexOrThrow(&quot;_id&quot;)))
                } else {
                    it.getString(it.getColumnIndexOrThrow(&quot;text&quot;)).orEmpty()
                }
            }
        }
        return &quot;&quot;
    }

    /** _data가 있는 경우, InputStream을 통해 텍스트 추출 */
    private fun readMmsText(partId: String): String {
        val uri = Uri.parse(&quot;content://mms/part/$partId&quot;)
        val builder = StringBuilder()

        context.contentResolver.openInputStream(uri)?.use { input -&gt;
            BufferedReader(InputStreamReader(input, Charsets.UTF_8)).use { reader -&gt;
                var line = reader.readLine()
                while (line != null) {
                    builder.append(line)
                    line = reader.readLine()
                }
            }
        }

        return builder.toString()
    }
}
</code></pre>
<h4 id="과거-데이터-조회-1">과거 데이터 조회</h4>
<blockquote>
<p>MMS는 content://mms/inbox를 통해 과거 데이터를 조회할 수 있지만, 쿼리 성능이 매우 느리고, 기기/OS 버전에 따라 저장 구조도 다릅니다.
따라서 MMS가 필수 데이터가 아닌 경우, 과거 MMS 조회는 스킵하거나 제한된 범위로 필터링하는 것을 권장합니다.</p>
</blockquote>
<pre><code class="language-kotlin">fun getRecentMmsAsSmsList(cnt: Int): List&lt;Sms&gt; {
    val smsList = mutableListOf&lt;Sms&gt;()
    var cursor: Cursor? = null

    try {
        val uri = Uri.parse(&quot;content://mms/inbox&quot;)
        val sortOrder = &quot;date ASC LIMIT $cnt&quot;

        cursor = context.contentResolver.query(uri, null, &quot;msg_box=1&quot;, null, sortOrder)

        cursor?.use {
            val idIndex = it.getColumnIndexOrThrow(&quot;_id&quot;)
            val dateIndex = it.getColumnIndexOrThrow(&quot;date&quot;)

            while (it.moveToNext()) {
                val id = it.getInt(idIndex)
                val timestamp = it.getLong(dateIndex) * 1000
                val dateString = Utils.getConvertedDate(timestamp)
                val sender = mmsHelper.getMmsAddress(id)
                val body = mmsHelper.parseMmsBody(id.toString()).take(490)

                val hashId = Utils.generateHashId(body + sender + dateString)

                val sms = Sms(
                    id = hashId,
                    message = body,
                    sender = sender,
                    displaySender = sender,
                    date = dateString,
                    type = SmsType.MMS
                )

                smsList.add(sms)
            }
        }
    } catch (e: Exception) {
        Log.e(&quot;MMS_PARSE&quot;, &quot;Error parsing MMS to Sms&quot;, e)
    } finally {
        cursor?.close()
    }

    return smsList
}</code></pre>
<h3 id="rcs">RCS</h3>
<h4 id="실시간-수신-2">실시간 수신</h4>
<pre><code class="language-kotlin">class RcsCatchReceiver : BroadcastReceiver() {

    private var rcsService: RcsService? = null

    override fun onReceive(context: Context?, intent: Intent?) {
        // context 또는 intent가 null이면 바로 종료
        if (context == null || intent == null) return
        if (intent.action != RCS_RECEIVED_ACTION) return

        try {
            // msg_id 는 RCS 메시지를 식별하는 고유 키
            val msgId = intent.extras?.get(&quot;msg_id&quot;)?.toString() ?: return

            // RcsService 초기화 후 메시지 조회 및 처리
            val service = rcsService ?: RcsService(context).also { rcsService = it }
            val sms = service.queryRcs(msgId)

            sms?.let {
                Log.d(&quot;RCS_CATCH&quot;, &quot;RCS message received: $it&quot;)
                // 이후 처리 (파싱, 저장 등) 필요 시 확장 가능
            }

        } catch (e: Exception) {
            Log.e(&quot;RCS_CATCH&quot;, &quot;Error handling RCS message&quot;, e)
        }
    }

    companion object {
        const val RCS_RECEIVED_ACTION = &quot;com.services.rcs.MESSAGE_RECEIVED&quot;
    }
}</code></pre>
<h4 id="과거-데이터-조회-2">과거 데이터 조회</h4>
<pre><code class="language-kotlin">
    /**
     * RCS 전체 메시지를 조건(where) 기반으로 조회
     */
    fun getRcsList(where: String? = null, cnt: Int = Int.MAX_VALUE): List&lt;SMS&gt; {
        val results = mutableListOf&lt;SMS&gt;()
        val uri = &quot;content://im/rcs_read_im&quot;.toUri()
        val sort = &quot;date ASC LIMIT $cnt&quot;

        context.contentResolver.query(uri, null, where, null, sort)?.use { cursor -&gt;
            while (cursor.moveToNext()) {
                RcsParser.parseRcs(cursor)?.let { results.add(it) }
            }
        }

        return results
    }</code></pre>
<h3 id="notification">Notification</h3>
<h4 id="실시간-수신-3">실시간 수신</h4>
<pre><code class="language-kotlin">import android.app.Notification
import android.content.Intent
import android.provider.Telephony
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import android.util.Log
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.*

class NotificationCatchService : NotificationListenerService() {

    override fun onNotificationPosted(sbn: StatusBarNotification?) {
        sbn ?: return
        val notification = sbn.notification ?: return
        val extras = notification.extras ?: return

        val title = extras.getCharSequence(Notification.EXTRA_TITLE)?.toString().orEmpty()
        val content = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString().orEmpty()
        val bigText = extras.getCharSequence(Notification.EXTRA_BIG_TEXT)?.toString().orEmpty()
        val packageName = sbn.packageName.orEmpty()
        val timestamp = notification.`when`
        val dateString = Utils.getConvertedDate(timestamp)

        // Sms 객체로 직접 변환
        val sms = Sms(
            id = Utils.generateHashId(title + content + dateString),
            message = if (bigText.isNotEmpty()) bigText else content,
            sender = packageName,
            displaySender = title,
            date = dateString,
            type = SmsType.Notification
        )

        Log.d(&quot;NOTI_CATCH&quot;, &quot;Parsed SMS: $sms&quot;)
    }


    override fun onNotificationRemoved(sbn: StatusBarNotification?) {
        // 필요시 구현
    }

    override fun onListenerConnected() {
        super.onListenerConnected()
        Log.d(&quot;NOTI_CATCH&quot;, &quot;Listener connected&quot;)
    }

    override fun onListenerDisconnected() {
        super.onListenerDisconnected()
        Log.d(&quot;NOTI_CATCH&quot;, &quot;Listener disconnected&quot;)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        return START_STICKY
    }
}</code></pre>
<h4 id="과거-데이터-조회-3">과거 데이터 조회</h4>
<p><em><strong>불가능</strong></em></p>
<h3 id="데이터-처리-문제">데이터 처리 문제</h3>
<p><strong>문제 1: 문자와 알림의 중복 수신 문제</strong></p>
<p>현상:
하나의 문자 수신 이벤트가 실제 문자(SMS_RECEIVED)와 동시에 알림(Notification)으로도 발생 →
동일한 메시지가 두 번 수신되어 파싱/저장이 중복됨</p>
<p>원인:
안드로이드 시스템이 문자 수신 시, 알림 채널을 통해 별도의 Notification을 발송하기 때문</p>
<p>해결 방법:</p>
<ul>
<li>알림에서 수신된 메시지의 패키지명이 기본 SMS 앱인지 확인하고, 해당 앱에서 발생한 알림은 필터링</li>
<li>hashId를 통한 중복 처리</li>
</ul>
<hr>
<p><strong>문제 2: 같은 결제, 다른 출처로 중복 수신되는 문제</strong></p>
<p>현상:
예를 들어 신한은행, 삼성페이 등 하나의 결제에 대해
문자 + 금융앱 알림 + 삼성페이 알림 등 3중 알림/문자 수신 발생 →
동일 금액이 여러 번 인식되는 문제</p>
<p>원인:
다양한 채널이 하나의 결제에 대해 각각 독립적으로 알림/문자를 발송함</p>
<p>해결 방법:</p>
<ul>
<li>금액 + 날짜 + 발신처 기반의 고유 해시 ID 생성 후, 중복 제거</li>
<li>수신되는 시간의 3분 정도 여유를 두어 금액, 키워드 비교 진행</li>
<li>키워드의 경우 중복 데이터로 판단될 경우 키워드가 긴경우로 replace 진행</li>
</ul>
<hr>
<p><strong>문제 3: 알림이 동일 데이터인데도 중복 수신되는 문제</strong></p>
<p>현상:
같은 알림 내용이 반복 수신됨. StatusBarNotification.postTime이 다르게 설정되어 있어
내용은 동일하지만 중복으로 인식되지 않음</p>
<p>원인:
알림 시스템이 앱 상태, 갱신 주기 등에 따라 동일한 알림을 다시 push함
(postTime이 매번 다르게 들어옴)</p>
<p>해결 방법:</p>
<ul>
<li>title + content + packageName + message 기반의 중복 로직 체크</li>
<li>postTime 대신 실제 내용 기준 비교</li>
</ul>
<hr>
<p><strong>문제 4: 알림 내 필드별 데이터 위치가 다름</strong></p>
<p>현상:
금액, 키워드 등이 알림의 title, text, bigText, subText 등 다양한 필드에 분산되어 있어 파싱 어려움</p>
<p>해결 방법:</p>
<ul>
<li>파서에서 모든 필드를 한 메시지로 concat 후 패턴 매칭 또는,
field → 역할 기준으로 분리하여 다중 파싱 규칙 적용</li>
</ul>
<hr>
<h3 id="결론">결론</h3>
<p>안드로이드는 SMS, MMS, RCS, Notification을 통해 개인에게 전달되는 다양한 메시지를 수신하고 저장할 수 있는 강력한 수단을 제공합니다.
이러한 메시지를 자동으로 수집하고 정제하여 내 삶의 결제/소비/이벤트 흐름을 데이터화할 수 있습니다.</p>
<p>저는 이 과정을 통해 수집 → 변형 → 적재 → 전처리 → 분석 → 개인화된 AI 모델 생성까지 이어지는
엔드 투 엔드 데이터 파이프라인을 직접 구축하고, 그 여정을 블로그를 통해 기록하고자 합니다.</p>
<p>작은 수신 메시지 하나가,
나만의 AI model 을 누구나 만들수 있도록 한발 한발 나아가 보겠습니다. </p>
<p>감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[일상 회고] - 가계부 CS 대응]]></title>
            <link>https://velog.io/@dyno-jun/%EC%9D%BC%EC%83%81-%ED%9A%8C%EA%B3%A0-%EA%B0%80%EA%B3%84%EB%B6%80-CS-%EB%8C%80%EC%9D%91</link>
            <guid>https://velog.io/@dyno-jun/%EC%9D%BC%EC%83%81-%ED%9A%8C%EA%B3%A0-%EA%B0%80%EA%B3%84%EB%B6%80-CS-%EB%8C%80%EC%9D%91</guid>
            <pubDate>Fri, 02 May 2025 07:18:35 GMT</pubDate>
            <description><![CDATA[<h3 id="주요-이슈">주요 이슈</h3>
<ul>
<li>서버 데이터 + 로컬 데이터 간 중복 처리 이슈</li>
<li>반복 지출 관련 중복 데이터 적재</li>
<li>알림, 문자간 중복 로직 처리 적용되지 않음.</li>
</ul>
<h3 id="문제-및-해결">문제 및 해결</h3>
<ul>
<li>서버에서 내려받은 데이터와 문자에서 파싱된 정보간의 중복 발생<blockquote>
<p>금액, 날짜, 키워드 형태로 키로 만들어 체크해서 진행 </p>
</blockquote>
</li>
<li>반복 지출의 경우 알림이 여러번 수신되는 케이스 발생<blockquote>
<p>일정 시간동안 반복 수신되는 정보는 내역에 적재되지 않도록 처리</p>
</blockquote>
</li>
<li>하나의 결제에 여려경로의 input이 존재함. 가령 알림 수신, RCS 수신 등 같은 내역이지만 둘다 넣으면 금액이 두배로 증가하는 현상 발생<blockquote>
<p>이를 해결하기 위해 특정시간, 금액이 같은 경우 중복 로직을 반영</p>
</blockquote>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dyno-jun/post/6d7c4489-8b85-47e1-9d91-5fd5d5c2eff9/image.png" alt=""></p>
<h3 id="오늘의-생각">오늘의 생각</h3>
<p>이번 일을 통해 크게 반성하게 되었다.
“작은 문제 하나 고치자고, 왜 이렇게 오래 걸리지?” 라는 생각 뒤에는
<strong>“작은 걸 오래 방치해서 더 복잡해졌다”</strong>는 구조적 이유가 숨어 있었다.</p>
<p>당장 급하지 않다는 이유로 버전 업데이트, 테스트 코드 작성, 의존성 정리는 계속 뒤로 밀려왔다.
하지만 이번에 느낀 건, 결국 이 모든 것들이 작은 문제를 작게 고칠 수 있는 조건이라는 점이다.</p>
<h3 id="앞으로의-다짐">앞으로의 다짐</h3>
<pre><code>•    작은 이슈라도 바로 확인하고 정리하기
•    레거시 테스트 코드 작성은 매주 작은 단위로 진행
•    버전 및 의존성 업데이트는 주간 단위로 체크하며 ‘기술 부채’를 줄여나가기</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[리팩토링 원칙]]></title>
            <link>https://velog.io/@dyno-jun/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%9B%90%EC%B9%99</link>
            <guid>https://velog.io/@dyno-jun/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%9B%90%EC%B9%99</guid>
            <pubDate>Sun, 01 Dec 2024 01:40:52 GMT</pubDate>
            <description><![CDATA[<h3 id="1-리팩토링이란">1. 리팩토링이란?</h3>
<blockquote>
<p>정의: 코드의 외부 동작은 유지하면서 내부 구조를 개선하는 과정.</p>
</blockquote>
<ul>
<li>가독성과 유지보수성을 높이고,</li>
<li>코드의 복잡도를 줄이며,</li>
<li>새로운 기능 추가와 버그 수정을 더 쉽게 만듦.</li>
</ul>
<h3 id="2-리팩토링의-중요성">2. 리팩토링의 중요성</h3>
<ul>
<li>코드가 점점 복잡해지고 품질이 낮아지는 것을 방지.</li>
<li>유지보수 비용 절감 및 팀 내 협업 강화.</li>
<li>새로운 요구사항에 유연하게 대응 가능.</li>
<li>기술 부채를 줄이고 프로젝트의 장기적인 건강 유지.<h3 id="3-리팩토링의-장점">3. 리팩토링의 장점</h3>
</li>
<li>가독성 개선:
코드의 명확성과 이해도가 높아짐.</li>
<li>변경 용이성 향상:
새로운 기능 추가나 변경 작업이 쉬워짐.</li>
<li>버그 수정 용이:
명확한 코드가 문제를 쉽게 찾고 해결할 수 있게 함.</li>
<li>재사용성 증가:
잘 구조화된 코드는 다른 프로젝트나 기능에서도 쉽게 활용 가능.<h3 id="4-리팩토링의-신호-code-smells">4. 리팩토링의 신호 (Code Smells)</h3>
</li>
<li>중복 코드: 비슷하거나 동일한 코드가 여러 곳에 반복.</li>
<li>긴 함수: 너무 많은 작업을 처리하는 함수.</li>
<li>큰 클래스: 책임이 분산된 클래스.</li>
<li>긴 매개변수 목록: 과도한 인자를 받는 메서드.</li>
<li>데이터 클럼프: 항상 함께 다니는 데이터 덩어리.<h3 id="5-리팩토링의-과정">5. 리팩토링의 과정</h3>
</li>
<li>테스트 작성:
리팩토링 전후의 동작이 동일한지 확인.</li>
<li>작은 단계로 진행:
코드를 조금씩 수정하며 테스트를 통과시킴.</li>
<li>변화 점검:
리팩토링이 기존 코드의 기능에 영향을 주지 않는지 검증.<h3 id="6-리팩토링과-성능">6. 리팩토링과 성능</h3>
</li>
<li>리팩토링 자체는 성능 최적화를 목적으로 하지 않음.</li>
<li>성능 저하는 우려할 필요 없음. 성능 이슈는 리팩토링 후, 필요 시 별도로 최적화.<h3 id="7-리팩토링과-팀-문화">7. 리팩토링과 팀 문화</h3>
</li>
<li>팀 내 협업 강화:
코드 리뷰와 정기적인 리팩토링을 통해 기술 부채를 줄임.</li>
<li>리팩토링 주기화:
정기적으로 리팩토링을 수행하며, 코드 품질을 유지.<h3 id="핵심-요약">핵심 요약</h3>
리팩토링은 개발의 필수 과정으로 코드의 가치를 유지하고 향상시킴.
Code Smells를 발견하면 즉각 리팩토링을 시작할 신호.
테스트 기반으로 리팩토링을 안전하게 수행하며, 성능 문제는 별도로 다룸.</li>
</ul>
<h3 id="회고">회고</h3>
<p>오늘은 리팩토링 원칙을 정리하면서, 실제 개발 환경에서의 다양한 상황을 떠올리게 되었다. 혼자 개발하거나, 여러 명이 협업하는 상황에서 종종 코드의 의도를 제대로 이해하지 못한 채 변경 작업을 진행하게 되고, 이는 장애로 이어지는 경우가 많다. 이런 상황이 반복되면 &quot;차라리 새로 개발하는 게 낫겠다&quot;는 결론에 도달하기도 한다.</p>
<p>또한, 리팩토링이 필요하다고 느끼더라도 막상 어디서부터 시작해야 할지 몰라 막막함을 느낄 때가 많다. 그러나 ChatGPT를 활용해 코드 분석을 진행하고, 분석된 결과를 바탕으로 테스트 코드를 작성하여 기존 코드의 안정성을 확보한 상태에서 리팩토링을 진행한다면, 보다 빠르고 안정적으로 코드 품질을 개선할 수 있을 것이라는 확신이 들었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리팩토링 1장]]></title>
            <link>https://velog.io/@dyno-jun/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-1</link>
            <guid>https://velog.io/@dyno-jun/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-1</guid>
            <pubDate>Sat, 30 Nov 2024 12:50:44 GMT</pubDate>
            <description><![CDATA[<h1 id="리팩토링-기법-refactoring-techniques">리팩토링 기법 (Refactoring Techniques)</h1>
<p>코드를 더 깔끔하고 유지보수하기 쉽게 만드는 과정이 바로 리팩토링입니다. 소프트웨어 개발에서 리팩토링은 필수적인 작업인데요, 그 과정에서 사용되는 다양한 기법들이 있습니다. 이번 글에서는 그중 몇 가지 대표적인 리팩토링 기법에 대해 알아보겠습니다.</p>
<h2 id="1-함수-추출하기-extract-method">1. 함수 추출하기 (Extract Method)</h2>
<p>설명: 코드의 특정 부분을 별도의 함수로 추출하여 코드의 가독성을 높이고 재사용성을 증가시키는 기법입니다.</p>
<p>적용 이유: 코드가 길어지고 복잡해질 때, 특정 기능을 수행하는 코드 블록을 새로운 함수로 만들어주면 더 이해하기 쉬워지고 테스트하기도 용이해집니다.</p>
<p>예시: 반복적으로 사용되는 로직을 독립적인 함수로 분리하여 코드의 중복을 줄이는 경우. 예를 들어, 여러 곳에서 동일한 데이터 검증 로직을 수행한다면 이를 별도의 함수로 만들어 재사용할 수 있습니다.</p>
<p><strong>단계 1: 원래 코드</strong></p>
<pre><code class="language-java">public class BankTransfer {
  private String sender;
  private String receiver;
  private double amount;

  public BankTransfer(String sender, String receiver, double amount) {
    this.sender = sender;
    this.receiver = receiver;
    this.amount = amount;
  }

  public String processTransfer() {
    double fee = 0;
    if (amount &gt; 10000) {
      fee = amount * 0.02;
    } else if (amount &gt; 5000) {
      fee = amount * 0.01;
    }
    double totalAmount = amount + fee;
    return &quot;Transferring &quot; + totalAmount + &quot; from &quot; + sender + &quot; to &quot; + receiver;
  }
}</code></pre>
<p><strong>단계 2: 함수 추출하기 적용</strong></p>
<pre><code class="language-java">public class BankTransfer {
  private String sender;
  private String receiver;
  private double amount;

  public BankTransfer(String sender, String receiver, double amount) {
    this.sender = sender;
    this.receiver = receiver;
    this.amount = amount;
  }

  public String processTransfer() {
    double totalAmount = amount + calculateFee();
    return &quot;Transferring &quot; + totalAmount + &quot; from &quot; + sender + &quot; to &quot; + receiver;
  }

  private double calculateFee() {
    if (amount &gt; 10000) {
      return amount * 0.02;
    } else if (amount &gt; 5000) {
      return amount * 0.01;
    }
    return 0;
  }
}</code></pre>
<p>위의 예시에서 calculateFee()라는 메서드를 별도로 추출하여 코드를 더 이해하기 쉽고 테스트하기 용이하도록 만들었습니다.</p>
<h2 id="2-변수-인라인하기-inline-variable">2. 변수 인라인하기 (Inline Variable)</h2>
<p>설명: 의미가 없거나 코드의 가독성을 떨어뜨리는 임시 변수를 없애고, 그 값을 직접 사용하도록 바꾸는 기법입니다.</p>
<p>적용 이유: 변수가 코드 이해를 도와주는 역할을 하지 않거나 그저 값만 전달하는 경우, 해당 변수를 없애고 직관적으로 값 자체를 사용하면 코드의 단순성을 높일 수 있습니다.</p>
<p><strong>단계 1: 함수 추출 전 코드</strong></p>
<pre><code class="language-java">public String processTransfer() {
  double fee = calculateFee();
  double totalAmount = amount + fee;
  return &quot;Transferring &quot; + totalAmount + &quot; from &quot; + sender + &quot; to &quot; + receiver;
}</code></pre>
<p><strong>단계 2: 변수 인라인하기 적용</strong></p>
<pre><code class="language-java">public String processTransfer() {
  double totalAmount = amount + calculateFee();
  return &quot;Transferring &quot; + totalAmount + &quot; from &quot; + sender + &quot; to &quot; + receiver;
}</code></pre>
<p>변수 fee가 코드 이해에 별로 도움이 되지 않기 때문에 이를 제거하고 직접 사용하는 방식으로 개선하였습니다.</p>
<h2 id="3-문장-슬라이스-하기-split-phase">3. 문장 슬라이스 하기 (Split Phase)</h2>
<p>설명: 하나의 함수나 메서드가 여러 단계의 작업을 수행할 때, 각 단계를 별도의 함수로 분리하는 기법입니다.</p>
<p>적용 이유: 각 단계를 별도로 관리하고 테스트할 수 있어 유지보수성이 향상되며, 단계마다 의미가 명확해져 가독성이 높아집니다.</p>
<p><strong>단계 1: 원래 코드</strong></p>
<pre><code class="language-java">public String processTransfer() {
  double totalAmount = amount + calculateFee();
  return &quot;Transferring &quot; + totalAmount + &quot; from &quot; + sender + &quot; to &quot; + receiver;
}</code></pre>
<p><strong>단계 2: 문장 슬라이스 하기 적용</strong></p>
<pre><code class="language-java">public String processTransfer() {
  double totalAmount = calculateTotalAmount();
  return generateTransferMessage(totalAmount);
}

private double calculateTotalAmount() {
  return amount + calculateFee();
}

private String generateTransferMessage(double totalAmount) {
  return &quot;Transferring &quot; + totalAmount + &quot; from &quot; + sender + &quot; to &quot; + receiver;
}</code></pre>
<p>각 단계를 별도의 함수로 분리하여 로직의 흐름을 더 명확하게 만들었습니다.</p>
<h2 id="4-조건부-로직을-다형성으로-바꾸기-replace-conditional-with-polymorphism">4. 조건부 로직을 다형성으로 바꾸기 (Replace Conditional with Polymorphism)</h2>
<p>설명: 조건문으로 구현된 로직을 다형성을 사용하여 각 조건에 따라 서로 다른 클래스를 만들어 처리하는 기법입니다.</p>
<p>적용 이유: 조건문이 많아지면 코드가 복잡해지고 읽기 어려워집니다. 이 문제를 다형성을 활용해 클래스 구조로 해결하면 각 클래스가 독립적으로 조건을 처리하게 되어 코드가 더 직관적이고 확장하기 쉬워집니다.</p>
<p><strong>단계 1: 조건부 로직 사용 코드</strong></p>
<pre><code class="language-java">private double calculateFee() {
  if (amount &gt; 10000) {
    return amount * 0.02;
  } else if (amount &gt; 5000) {
    return amount * 0.01;
  }
  return 0;
}</code></pre>
<p><strong>단계 2: 다형성 적용</strong></p>
<pre><code class="language-java">public interface FeeCalculator {
  double calculate(double amount);
}

public class HighAmountFeeCalculator implements FeeCalculator {
  public double calculate(double amount) {
    return amount * 0.02;
  }
}

public class MediumAmountFeeCalculator implements FeeCalculator {
  public double calculate(double amount) {
    return amount * 0.01;
  }
}

public class NoFeeCalculator implements FeeCalculator {
  public double calculate(double amount) {
    return 0;
  }
}

private FeeCalculator getFeeCalculator() {
  if (amount &gt; 10000) {
    return new HighAmountFeeCalculator();
  } else if (amount &gt; 5000) {
    return new MediumAmountFeeCalculator();
  }
  return new NoFeeCalculator();
}

private double calculateFee() {
  FeeCalculator calculator = getFeeCalculator();
  return calculator.calculate(amount);
}</code></pre>
<p>조건부 로직을 각 조건에 맞는 클래스로 대체하여 코드의 복잡성을 줄였습니다.</p>
<h2 id="5-함수-옮기기-move-method">5. 함수 옮기기 (Move Method)</h2>
<p>설명: 특정 클래스에 속해 있는 함수가 다른 클래스에 더 잘 맞는 경우, 그 함수를 적절한 클래스나 모듈로 옮기는 기법입니다.</p>
<p>적용 이유: 데이터와 로직이 서로 잘 맞아야 코드가 더 직관적이고 유지보수하기 쉬워집니다.</p>
<p><strong>단계 1: 원래 코드</strong></p>
<p>calculateFee() 함수가 BankTransfer 클래스에 존재하지만, 이는 수수료 계산 클래스에서 더 적절합니다.</p>
<p><strong>단계 2: 함수 옮기기 적용</strong></p>
<p>calculateFee() 함수를 각 수수료 계산 클래스 (HighAmountFeeCalculator, MediumAmountFeeCalculator, NoFeeCalculator)로 옮겨 응집도를 높였습니다.</p>
<h2 id="6-단계-쪼개기-split-phase">6. 단계 쪼개기 (Split Phase)</h2>
<p>설명: 복잡한 처리를 단계별로 나누어 각각의 단계가 명확히 드러나도록 하는 기법입니다.</p>
<p>적용 이유: 여러 단계를 처리하는 코드가 있을 때 이를 명확히 분리하면 각 단계가 독립적이고 테스트하기 쉬워지며, 로직의 흐름을 쉽게 파악할 수 있습니다.</p>
<p><strong>단계 1: 원래 코드</strong></p>
<pre><code class="language-java">public String processTransfer() {
  double totalAmount = calculateTotalAmount();
  return generateTransferMessage(totalAmount);
}</code></pre>
<p><strong>단계 2: 단계 쪼개기 적용</strong></p>
<pre><code class="language-java">public void executeTransfer() {
  double totalAmount = calculateTotalAmount();
  String message = generateTransferMessage(totalAmount);
  printTransferMessage(message);
}

private void printTransferMessage(String message) {
  System.out.println(message);
}</code></pre>
<p>각 단계가 명확히 드러나도록 함수를 분리하여 로직의 가독성을 높였습니다.</p>
<h2 id="정리">정리</h2>
<p>GPT를 활용해서 코드를 작성하다보니 생산성이 많이 높아졌습니다. 하지만 생성된 코드를 그냥 쓰다보면 결국 리팩토링의 필요성을 느끼게 되었고 이를 해결하기 위해 리팩토링 기법을 숙지하면서 GPT의 생산성과 코드의 가독성을 함께 지켜나가며 좋은 서비스를 만들어 나가고자 합니다.</p>
]]></description>
        </item>
    </channel>
</rss>