<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>행복한 내 이야기</title>
    <link>https://yujinius45.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 17 Jun 2026 09:00:10 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>yujinius</managingEditor>
    <image>
      <title>행복한 내 이야기</title>
      <url>https://tistory1.daumcdn.net/tistory/6261351/attach/60ca8f6204994565adac736c00eb34dc</url>
      <link>https://yujinius45.tistory.com</link>
    </image>
    <item>
      <title>[Android/오르다 다이어리] 레거시 리팩토링 10 - Release 2 (v2.0.0) 완료 및 후기</title>
      <link>https://yujinius45.tistory.com/165</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;  [Android/오르다 다이어리] 3년 전 레거시 리팩토링, 기능 완성 &amp;amp; UI/UX 개선 - Release 2 (v2.0.0) 후기&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;지난 글:&lt;/b&gt;&lt;a href=&quot;https://yujinius45.tistory.com/164&quot;&gt;https://yujinius45.tistory.com/164&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740919990351&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android/오르다 다이어리] 레거시 리팩토링 09 - HomeFragment 애니메이션 적용 &amp;amp; UI 개선&quot; data-og-description=&quot;[Android/오르다 다이어리] 레거시 리팩토링 09 - HomeFragment 애니메이션 적용 &amp;amp; UI 개선&amp;nbsp;지난 글:https://yujinius45.tistory.com/163&amp;nbsp;이제 기능 미완성이었던 것도 완성되고 UI/UX 개선도 완료되어서 릴리즈를 &quot; data-og-host=&quot;yujinius45.tistory.com&quot; data-og-source-url=&quot;https://yujinius45.tistory.com/164&quot; data-og-url=&quot;https://yujinius45.tistory.com/164&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/de9aEx/hyYjAJqg5T/outydAdQQDizfqbQ4Ovfn1/img.gif?width=336&amp;amp;height=720&amp;amp;face=51_104_280_354,https://scrap.kakaocdn.net/dn/gKk1n/hyYjyES1Uz/EoedMKprkyFacEfv3LF2UK/img.gif?width=336&amp;amp;height=720&amp;amp;face=51_104_280_354,https://scrap.kakaocdn.net/dn/bb22HA/hyYmLiiTmE/opToPK535LksNB9JKCi2Gk/img.jpg?width=439&amp;amp;height=439&amp;amp;face=0_0_439_439&quot;&gt;&lt;a href=&quot;https://yujinius45.tistory.com/164&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yujinius45.tistory.com/164&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/de9aEx/hyYjAJqg5T/outydAdQQDizfqbQ4Ovfn1/img.gif?width=336&amp;amp;height=720&amp;amp;face=51_104_280_354,https://scrap.kakaocdn.net/dn/gKk1n/hyYjyES1Uz/EoedMKprkyFacEfv3LF2UK/img.gif?width=336&amp;amp;height=720&amp;amp;face=51_104_280_354,https://scrap.kakaocdn.net/dn/bb22HA/hyYmLiiTmE/opToPK535LksNB9JKCi2Gk/img.jpg?width=439&amp;amp;height=439&amp;amp;face=0_0_439_439');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 09 - HomeFragment 애니메이션 적용 &amp;amp; UI 개선&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 09 - HomeFragment 애니메이션 적용 &amp;amp; UI 개선&amp;nbsp;지난 글:https://yujinius45.tistory.com/163&amp;nbsp;이제 기능 미완성이었던 것도 완성되고 UI/UX 개선도 완료되어서 릴리즈를&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yujinius45.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  3년 전 레거시 코드 리팩토링, 그리고 마침내 기능 완성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오르다 다이어리를 처음 만들었을 때는 기능이 많았지만, &lt;b&gt;구조적으로 정리가 덜 되어 있었고, UI/UX에 대한 고민이 부족했던 상태&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음 앱을 만들었을 때는 &quot;기능만 구현되면 된다!&quot;라는 마인드로 개발했었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 3년이 지난 지금 다시 프로젝트를 리팩토링하면서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에 미처 고려하지 못했던 부분들이 많았다는 걸 깨달았고, 이를 하나하나 개선해 나갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 Release 2 (v2.0.0)에서는 &lt;b&gt;미완성 기능을 완성하고, UX를 개선하며, 코드 구조를 리팩토링&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트를 통해 많이 배웠던 점들을 정리해 보려고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  3년 전 코드에서 개선이 필요했던 점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v1.0.0에서는 기본적인 &lt;b&gt;일정 관리, 일기 작성, 알람, 지도 메모 기능&lt;/b&gt;이 포함되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 실제 사용하면서 &lt;b&gt;여러 문제점&lt;/b&gt;이 드러났다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ &lt;b&gt;기존 코드에서 개선이 필요했던 부분&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;UI/UX 문제&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 화면이 개별적인 &lt;code&gt;Activity&lt;/code&gt;로 구성되어 있어, 사용자가 &lt;b&gt;홈으로 돌아가서 버튼을 눌러야만 다른 기능으로 이동 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;화면 이동이 불편하고 비효율적&lt;/b&gt;이었으며, 처음 사용자가 홈 화면에서 어떻게 앱을 시작해야 하는지 명확하지 않았음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;홈 화면이 정적&lt;/b&gt; &amp;rarr; 처음 앱을 실행한 사용자는 &lt;b&gt;어떤 기능을 먼저 사용해야 할지 알기 어려움&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기능 미완성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MapMemo 기능이 완성되지 않음&lt;/b&gt; &amp;rarr; 앱을 종료하면 저장한 데이터가 사라지는 문제 발생&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DiaryWritingFragment&lt;/code&gt;의 &lt;b&gt;상태 유지 불가&lt;/b&gt; &amp;rarr; 화면 회전 시 입력한 내용이 사라짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 구조 및 보안 문제&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DBHelper가 MainActivity에 종속되어 있음&lt;/b&gt; &amp;rarr; 독립적인 테스트 및 유지보수가 어려움&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SQL Injection 취약점 존재&lt;/b&gt; &amp;rarr; 사용자의 입력값이 SQL 문과 함께 실행되어 보안 위험이 있었음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;직접 SQLite를 사용&lt;/b&gt; &amp;rarr; 관리가 어려워 유지보수성이 떨어짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  리팩토링을 하며 배운 점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트를 진행하면서 단순히 기능을 추가하는 것이 아니라, 어떻게 하면 유지보수하기 좋은 코드가 될까?를 고민하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 내 코드를 다시 보면서 개발을 할 때, &lt;b&gt;당장 기능이 동작하는 것보다, 이후 유지보수가 편하고 확장 가능한 코드를 작성하는 것이 중요하다는 것&lt;/b&gt;을 깨달았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1️⃣ 싱글톤 패턴을 적용하며 유지보수성이 높아진 사례&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  기존 코드 문제점:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 &lt;code&gt;DBHelper&lt;/code&gt;가 &lt;code&gt;MainActivity&lt;/code&gt;에서 static 변수로 선언되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;어떤 Fragment에서든 DBHelper를 사용하려면 MainActivity를 먼저 실행해야 했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;문제 발생 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class MainActivity extends AppCompatActivity {
    public static DBHelper mDBHelper;  // MainActivity에 종속
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 &lt;b&gt;DBHelper가 MainActivity에 강하게 결합&lt;/b&gt;되면서,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;다른 클래스에서 MainActivity 없이 DBHelper를 사용할 수 없음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MainActivity가 실행되지 않으면 DBHelper도 초기화되지 않음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 코드에서 독립적으로 DBHelper를 사용하기 어려움&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;해결 방법: 싱글톤 패턴 적용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public class DBHelper extends SQLiteOpenHelper {
    private static DBHelper instance;

    public static synchronized DBHelper getInstance(Context context) {
        if (instance == null) {
            instance = new DBHelper(context.getApplicationContext());
        }
        return instance;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  유지보수성이 어떻게 향상되었는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;DBHelper가 특정 Activity에 의존하지 않게 됨&lt;/b&gt; &amp;rarr; 어디서든 &lt;code&gt;DBHelper.getInstance(context)&lt;/code&gt;로 호출 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;테스트 코드에서 독립적으로 DBHelper를 사용할 수 있음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;메모리를 절약하고 중복 인스턴스 생성을 방지함&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2️⃣ MVVM 패턴 적용으로 개선&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;기존 코드 문제점:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Activity, &lt;b&gt;Fragment 등 UI에서 UI 상태를 직접 관리&lt;/b&gt;하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 화면들은 DB에서 데이터를 로드해와서 상태 유지를 하고 있었으나 일기 작성 화면 같은 경우 화면이 회전되면 데이터가 초기화되는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;문제 발생 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class DiaryWritingFragment extends Fragment {
    private EditText diaryContent;

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        diaryContent = view.findViewById(R.id.diaryContent);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &lt;b&gt;Fragment가 다시 생성될 때 데이터가 사라지는 문제 발생&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;해결 방법: ViewModel을 사용하여 데이터 상태 유지&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class DiaryViewModel extends ViewModel {
    private final MutableLiveData&amp;lt;String&amp;gt; diaryContent = new MutableLiveData&amp;lt;&amp;gt;();

    public LiveData&amp;lt;String&amp;gt; getDiaryContent() {
        return diaryContent;
    }

    public void setDiaryContent(String content) {
        diaryContent.setValue(content);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  어떻게 개선되었는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;화면 회전 시에도 데이터가 유지됨&lt;/b&gt; &amp;rarr; 사용성이 향상됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;UI 로직과 비즈니스 로직이 분리됨&lt;/b&gt; &amp;rarr; 이후 추가 기능을 넣는다면 ViewModel에 추가해주면 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;테스트 코드에서 ViewModel만 따로 테스트 가능 &amp;rarr; 추후 기능이 추가된다면 진행할 수 있음&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3️⃣ SQL Injection 방지 및 코드 관리가 쉬워진 사례&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;기존 코드 문제점:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 &lt;b&gt;사용자 입력값을 그대로 SQL 쿼리에 삽입&lt;/b&gt;하여 &lt;b&gt;SQL Injection 취약점이 존재&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;문제 발생 예시 (보안 취약)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;db.execSQL(&quot;INSERT INTO TODOLIST_TB (title, content) VALUES ('&quot; + title + &quot;', '&quot; + content + &quot;');&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 사용자가 &lt;code&gt;&quot;'); DROP TABLE TODOLIST_TB; --&quot;&lt;/code&gt;을 입력하면, &lt;b&gt;DB 테이블이 삭제될 수 있음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;해결 방법: Prepared Statement 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SQLiteDatabase db = getWritableDatabase();
String query = &quot;INSERT INTO TODOLIST_TB (title, content) VALUES (?, ?)&quot;;
SQLiteStatement stmt = db.compileStatement(query);
stmt.bindString(1, title);
stmt.bindString(2, content);
stmt.executeInsert();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  어떻게 개선되었는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;SQL Injection이 방어됨&lt;/b&gt; &amp;rarr; 보안 강화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;쿼리가 가독성 좋게 정리됨&lt;/b&gt; &amp;rarr; 이후 추가할 때는 좀 더 읽기 쉬워졌음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;테스트 코드에서 SQL이 정상 동작하는지 검증 가능&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  결론 및 마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 리팩토링을 통해, &lt;b&gt;앱을 단순히 기능만 구현하는 것이 아니라, 유지보수성이 높은 구조로 만드는 것이 중요하다는 것을 배웠다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히, &lt;b&gt;싱글톤 패턴 적용, MVVM 구조 도입, UI/UX 개선, 보안 강화 등의 작업을 진행하면서 성장할 수 있었다. (&lt;/b&gt;이후 더 작업을 진행한다면 RoomDB 적용을 고려해볼 것 같다. &lt;b&gt;)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 기능이 돌아가기만 하면 된다고 생각했지만, &lt;b&gt;이제는 코드의 확장성과 유지보수성까지 고민하게 되었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 더 좋은 코드와 UI/UX를 고민하면서 발전해 나가야겠다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9tIZR/btsMA8pBZ1V/M2tTSKy2Bnfc5K08kxi3vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9tIZR/btsMA8pBZ1V/M2tTSKy2Bnfc5K08kxi3vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9tIZR/btsMA8pBZ1V/M2tTSKy2Bnfc5K08kxi3vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9tIZR%2FbtsMA8pBZ1V%2FM2tTSKy2Bnfc5K08kxi3vk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;831&quot; height=&quot;670&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://github.com/yujin45/Team3_Orda_Diary/releases/tag/v2.0.0&quot;&gt;https://github.com/yujin45/Team3_Orda_Diary/releases/tag/v2.0.0&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(상세 변경 사항은 릴리즈 노트 참고)&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740920136768&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Release   Orda Diary Release 2 (v2.0.0) &amp;middot; yujin45/Team3_Orda_Diary&quot; data-og-description=&quot;What's Changed [Refactor] targetSdk 34 적용 및 권한 변경 대응 by @yujin45 in #1 [Refactor] Activity &amp;rarr; Fragment 전환 및 기능 개선 + DB 수정 버그 해결 by @yujin45 in #2 [Refactor] DiaryWritingFragment Configuration Chang...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/yujin45/Team3_Orda_Diary/releases/tag/v2.0.0&quot; data-og-url=&quot;https://github.com/yujin45/Team3_Orda_Diary/releases/tag/v2.0.0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cgt3Em/hyYmTN9EmG/0LjoqLBToHLXDdu5UQ72W1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/fAkcu/hyYm2K37qU/bzgkCII9lnXEoTykK4tmK0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/yujin45/Team3_Orda_Diary/releases/tag/v2.0.0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/yujin45/Team3_Orda_Diary/releases/tag/v2.0.0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cgt3Em/hyYmTN9EmG/0LjoqLBToHLXDdu5UQ72W1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/fAkcu/hyYm2K37qU/bzgkCII9lnXEoTykK4tmK0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Release   Orda Diary Release 2 (v2.0.0) &amp;middot; yujin45/Team3_Orda_Diary&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;What's Changed [Refactor] targetSdk 34 적용 및 권한 변경 대응 by @yujin45 in #1 [Refactor] Activity &amp;rarr; Fragment 전환 및 기능 개선 + DB 수정 버그 해결 by @yujin45 in #2 [Refactor] DiaryWritingFragment Configuration Chang...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/오르다 다이어리</category>
      <category>Android</category>
      <category>Github</category>
      <category>MVVM</category>
      <category>releasenote</category>
      <category>sql injection</category>
      <category>SQLite</category>
      <category>UI/UX</category>
      <category>레거시 코드</category>
      <category>리팩토링</category>
      <category>유지보수</category>
      <author>yujinius</author>
      <guid isPermaLink="true">https://yujinius45.tistory.com/165</guid>
      <comments>https://yujinius45.tistory.com/165#entry165comment</comments>
      <pubDate>Sun, 2 Mar 2025 21:57:11 +0900</pubDate>
    </item>
    <item>
      <title>[Android/오르다 다이어리] 레거시 리팩토링 09 - HomeFragment 애니메이션 적용 &amp;amp; UI 개선</title>
      <link>https://yujinius45.tistory.com/164</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;[Android/오르다 다이어리] 레거시 리팩토링 09 - HomeFragment 애니메이션 적용 &amp;amp; UI 개선&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지난 글&lt;/b&gt;:&lt;a href=&quot;https://yujinius45.tistory.com/163&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yujinius45.tistory.com/163&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기능 미완성이었던 것도 완성되고 UI/UX 개선도 완료되어서 릴리즈를 하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 전에 마지막으로 HomeFragment를 개선했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  HomeFragment UI 개선 및 애니메이션 적용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오르다 다이어리의 홈 화면(HomeFragment)에 &lt;b&gt;애니메이션을 적용하여 UX를 개선&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 화면에 정적인 UI 요소만 배치되어 있었는데, 처음 앱을 실행하는 사용자가 &lt;b&gt;어떤 동작을 해야 하는지 안내가 부족했다.&lt;/b&gt; 만약 처음 앱을 설치한 사람이었다면 Home 화면이 splash 화면이라고 오해했을 수도 있을 것 같아 개선이 필요함을 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▼ 아무런 안내가 없고 Home 화면이 정적이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;[오르다 다이어리] 05 Home Animation 적용 전.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/57SQ5/btsMAEh1f3p/kvocqgzL4bSppqe81lCpek/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/57SQ5/btsMAEh1f3p/kvocqgzL4bSppqe81lCpek/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/57SQ5/btsMAEh1f3p/kvocqgzL4bSppqe81lCpek/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/57SQ5/btsMAEh1f3p/kvocqgzL4bSppqe81lCpek/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;336&quot; height=&quot;720&quot; data-filename=&quot;[오르다 다이어리] 05 Home Animation 적용 전.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 아래와 같은 작업을 수행했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&quot;하단 버튼을 눌러 시작하세요&quot;&lt;/b&gt; 문구에 &lt;b&gt;fade in/out 애니메이션&lt;/b&gt; 추가하여 앱 사용 안내하기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타이틀, 서브타이틀, 메인 이미지&lt;/b&gt;에 &lt;b&gt;슬라이드 애니메이션&lt;/b&gt; 추가하여 동적 요소 추가하기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fragment 생명주기에 맞춰 애니메이션을 관리&lt;/b&gt;하여 &lt;b&gt;메모리 누수 방지하기&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  개선 전 문제점&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;홈 화면이 너무 정적&lt;/b&gt; &amp;rarr; 첫 화면에서 아무런 UI 변화가 없어 사용자가 &lt;b&gt;어떻게 시작해야 하는지 알기 어려움&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 안내 부족&lt;/b&gt; &amp;rarr; 앱을 실행하면 &lt;b&gt;아무런 인터랙션 없이&lt;/b&gt; 로고와 타이틀만 표시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UX 향상 필요&lt;/b&gt; &amp;rarr; &lt;b&gt;애니메이션을 활용하여 자연스럽게 UI 요소를 배치하고 안내&lt;/b&gt;할 필요가 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✨ 개선된 UI 설계&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;적용된 애니메이션&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;height: 139px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt; UI 요소 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;적용된 애니메이션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;메인 이미지 (&lt;code&gt;imageViewMain&lt;/code&gt;)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;위에서 아래로 &lt;b&gt;슬라이드 등장&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;타이틀 (&lt;code&gt;textViewTitle&lt;/code&gt;)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;위에서 아래로 &lt;b&gt;슬라이드 등장&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;서브타이틀 (&lt;code&gt;textViewSubTitle&lt;/code&gt;)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;위에서 아래로 &lt;b&gt;슬라이드 등장&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;안내 문구 (&lt;code&gt;textViewInfo&lt;/code&gt;)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;Fade In/Out 반복&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  애니메이션 적용 과정&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1️⃣ 애니메이션 XML 추가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애니메이션을 정의한 &lt;b&gt;XML 파일을 &lt;code&gt;res/anim/&lt;/code&gt; 디렉토리에 추가해서 사용했다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;슬라이드 애니메이션 (text_slide_up.xml)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;위에서 아래로 부드럽게 내려오는 애니메이션&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;translate xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:fromYDelta=&quot;-50%&quot;
    android:toYDelta=&quot;0&quot;
    android:duration=&quot;1000&quot;
    android:interpolator=&quot;@android:anim/decelerate_interpolator&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;Fade In/Out 애니메이션 (fade_in_out.xml)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;안내 문구가 부드럽게 깜빡이며 사용자에게 안내&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;set xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:interpolator=&quot;@android:anim/accelerate_decelerate_interpolator&quot;&amp;gt;

    &amp;lt;alpha
        android:duration=&quot;2000&quot;
        android:fromAlpha=&quot;0.0&quot;
        android:toAlpha=&quot;1.0&quot;
        android:repeatMode=&quot;reverse&quot;
        android:repeatCount=&quot;infinite&quot; /&amp;gt;
&amp;lt;/set&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2️⃣ HomeFragment UI 변경 (fragment_home.xml)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;TextView ID 및 UI 구조 개선&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&quot;하단 버튼을 눌러 시작하세요&quot;&lt;/b&gt; 안내 문구 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;TextView
    android:id=&quot;@+id/textViewInfo&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:fontFamily=&quot;@font/nanumleo_bold&quot;
    android:gravity=&quot;center&quot;
    android:text=&quot;@string/home_fragment_info&quot;
    android:textColor=&quot;@color/orda_is_your_diary&quot;
    android:textSize=&quot;16sp&quot;
    app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
    app:layout_constraintEnd_toEndOf=&quot;parent&quot;
    app:layout_constraintStart_toStartOf=&quot;parent&quot;
    app:layout_constraintTop_toBottomOf=&quot;@id/textViewSubTitle&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3️⃣ HomeFragment에서 애니메이션 적용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;ViewBinding을 활용하여 UI 요소를 바인딩하고, Fragment 생명주기에 맞춰 애니메이션을 관리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;&lt;code&gt;onResume()&lt;/code&gt;에서 애니메이션 시작 / &lt;code&gt;onPause()&lt;/code&gt;에서 정지하여 메모리 누수 방지&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package smu.team3_orda_diary.ui.home;

public class HomeFragment extends Fragment {

    private FragmentHomeBinding binding;
    private Animation fadeAnimation, slideAnimation;

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        binding = FragmentHomeBinding.inflate(inflater, container, false);
        return binding.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        // 애니메이션 로드
        fadeAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_in_out);
        slideAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.text_slide_up);
    }

    @Override
    public void onResume() {
        super.onResume();
        // 애니메이션 시작
        binding.imageViewMain.startAnimation(slideAnimation);
        binding.textViewTitle.startAnimation(slideAnimation);
        binding.textViewSubTitle.startAnimation(slideAnimation);
        binding.textViewInfo.startAnimation(fadeAnimation);
    }

    @Override
    public void onPause() {
        super.onPause();
        // 애니메이션 정지
        binding.imageViewMain.clearAnimation();
        binding.textViewTitle.clearAnimation();
        binding.textViewSubTitle.clearAnimation();
        binding.textViewInfo.clearAnimation();
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null; 
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  애니메이션 적용 후 실행 화면&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;[오르다 다이어리] 05 Home Animation 적용.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Iz8aN/btsMyT1OjSN/cb8xVz41Tt5aeErzDZaAjK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Iz8aN/btsMyT1OjSN/cb8xVz41Tt5aeErzDZaAjK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Iz8aN/btsMyT1OjSN/cb8xVz41Tt5aeErzDZaAjK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/Iz8aN/btsMyT1OjSN/cb8xVz41Tt5aeErzDZaAjK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;336&quot; height=&quot;720&quot; data-filename=&quot;[오르다 다이어리] 05 Home Animation 적용.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  결과 &amp;amp; 개선 효과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;홈 화면이 정적이지 않고 자연스러운 UI 변화 제공&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;&quot;하단 버튼을 눌러 시작하세요&quot; 안내 추가로 사용자 경험 개선&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;Fragment 생명주기에 맞춰 애니메이션 관리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;ViewBinding 적용으로 더 안전한 UI 참조 방식 적용&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  마무리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;홈 화면에서의 사용자 경험을 개선&lt;/b&gt;하고, &lt;b&gt;보다 직관적인 안내를 제공&lt;/b&gt;할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 홈 화면을 더 쉽게 이해할 수 있으며, 앱을 처음 실행하는 사용자가 &lt;b&gt;자연스럽게 다음 동작을 유도&lt;/b&gt;할 수 있다.&lt;/p&gt;</description>
      <category>Android/오르다 다이어리</category>
      <category>Android</category>
      <category>android animation</category>
      <category>Anim</category>
      <category>Animation</category>
      <category>fadeinout</category>
      <category>mobileui</category>
      <category>UI</category>
      <category>UIUX</category>
      <category>UX</category>
      <category>viewBinding</category>
      <author>yujinius</author>
      <guid isPermaLink="true">https://yujinius45.tistory.com/164</guid>
      <comments>https://yujinius45.tistory.com/164#entry164comment</comments>
      <pubDate>Sun, 2 Mar 2025 20:24:35 +0900</pubDate>
    </item>
    <item>
      <title>[Android/오르다 다이어리] 레거시 리팩토링 08 - SQL Injection 테스트 및 해결</title>
      <link>https://yujinius45.tistory.com/163</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 08 - SQL Injection 테스트 및 해결&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글: &lt;a href=&quot;https://yujinius45.tistory.com/162&quot;&gt;https://yujinius45.tistory.com/162&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740912073521&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android/오르다 다이어리] 레거시 리팩토링 07 - SQLite, MapMemo 기능 완성&quot; data-og-description=&quot;[Android/오르다 다이어리] 레거시 리팩토링 07 - SQLite, MapMemo 기능 완성지난 글: https://yujinius45.tistory.com/161&amp;nbsp;[Android/오르다 다이어리] 레거시 리팩토링 06 - DBHelper 싱글톤 패턴 적용[Android/오르다 다이&quot; data-og-host=&quot;yujinius45.tistory.com&quot; data-og-source-url=&quot;https://yujinius45.tistory.com/162&quot; data-og-url=&quot;https://yujinius45.tistory.com/162&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ccN03p/hyYmUlZnZe/twbXWkkpYgzNIM4HuYhXY1/img.gif?width=336&amp;amp;height=720&amp;amp;face=0_0_336_720,https://scrap.kakaocdn.net/dn/eN9Uf/hyYjvIdfI3/75Ib0xw3HWKp95ZaPeW4u1/img.gif?width=336&amp;amp;height=720&amp;amp;face=0_0_336_720,https://scrap.kakaocdn.net/dn/ThKDi/hyYjjA11HA/TtOxKFKIcoslIqrCLIr7V0/img.jpg?width=439&amp;amp;height=439&amp;amp;face=0_0_439_439&quot;&gt;&lt;a href=&quot;https://yujinius45.tistory.com/162&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yujinius45.tistory.com/162&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ccN03p/hyYmUlZnZe/twbXWkkpYgzNIM4HuYhXY1/img.gif?width=336&amp;amp;height=720&amp;amp;face=0_0_336_720,https://scrap.kakaocdn.net/dn/eN9Uf/hyYjvIdfI3/75Ib0xw3HWKp95ZaPeW4u1/img.gif?width=336&amp;amp;height=720&amp;amp;face=0_0_336_720,https://scrap.kakaocdn.net/dn/ThKDi/hyYjjA11HA/TtOxKFKIcoslIqrCLIr7V0/img.jpg?width=439&amp;amp;height=439&amp;amp;face=0_0_439_439');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 07 - SQLite, MapMemo 기능 완성&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 07 - SQLite, MapMemo 기능 완성지난 글: https://yujinius45.tistory.com/161&amp;nbsp;[Android/오르다 다이어리] 레거시 리팩토링 06 - DBHelper 싱글톤 패턴 적용[Android/오르다 다이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yujinius45.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 번 MapMemo 기능을 완성 작업을 진행하면서 DBHelper를 보다가 문득 SQL Injection이 가능한 코드라는 생각이 들었다. 그래서 이에 대해 시도해보고 Injection을 방어할 수 있게 해결해보고자 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h1&gt;  SQL Injection 테스트 및 해결 과정&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 DBHelper 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;code&gt;DBHelper&lt;/code&gt; 클래스의 &lt;code&gt;insertTodo&lt;/code&gt; 메서드는 아래와 같이 구현되어 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public void insertTodo(String _title, String _content, String _writeDate) {
    SQLiteDatabase db = getWritableDatabase();
    db.execSQL(&quot;INSERT INTO TODOLIST_TB (title, content, writeDate) VALUES ('&quot; + _title + &quot;','&quot; + _content + &quot;','&quot; + _writeDate + &quot;');&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 테스트 코드들을 작성하면서 SQL Injection이 실제로 가능한지 확인해보고 싶어졌다. Room DB와 같은 ORM을 사용하면 이러한 문제를 방지할 수 있지만, 현재 레거시 코드에서는 직접 SQL을 실행하기 때문에 보안 위험이 존재한다고 판단하여 이를 직접 테스트해보기로 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQL Injection 테스트 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 테스트 코드를 작성하여 SQL Injection의 가능성을 검증했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
public void testSQLInjectionUpdateTodo() {
    dbHelper.insertTodo(&quot;Todo A&quot;, &quot;Content A&quot;, &quot;2025-03-01&quot;);
    dbHelper.insertTodo(&quot;Todo B&quot;, &quot;Content B&quot;, &quot;2025-03-01&quot;);
    dbHelper.insertTodo(&quot;Todo C&quot;, &quot;Content C&quot;, &quot;2025-03-01&quot;);

    ArrayList&amp;lt;TodoItem&amp;gt; todoList = dbHelper.getTodoList(&quot;2025-03-01&quot;);
    assertEquals(3, todoList.size());

    int targetTodoId = todoList.get(0).getId();

    // SQL Injection 시도 - 모든 데이터를 업데이트하도록 WHERE 조건 변경
    String injectionTitle = &quot;Injected Title', writeDate='2025-03-02' WHERE id=id OR 1=1 --&quot;;

    dbHelper.updateTodo(targetTodoId, injectionTitle, &quot;Injected Content&quot;, &quot;2025-03-02&quot;, &quot;2025-03-01&quot;);

    ArrayList&amp;lt;TodoItem&amp;gt; manipulatedList = dbHelper.getTodoList(&quot;2025-03-02&quot;);

    // Injection이 성공했다면 모든 To-Do의 날짜가 변경됨 (원래 1개만 변경되어야 함)
    assertTrue(&quot;SQL Injection should have updated multiple rows unexpectedly!&quot;, manipulatedList.size() &amp;gt; 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3개의 값을 넣어두고 의도대로라면 1개만 변경되어야 하지만, &lt;code&gt;WHERE 1=1&lt;/code&gt;을 추가하는 Injection을 시도하여 &lt;b&gt;모든 행이 업데이트되는 결과가 발생&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 결과 SQL Injection이 성공하여 1개가 아니라 모든 행이 다 업데이트 되어 버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▼ SQL Injection 테스트 코드 통과 캡쳐&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1612&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UmmrY/btsMzu8hFe0/yKfb4RWHkECpPUlks3ykzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UmmrY/btsMzu8hFe0/yKfb4RWHkECpPUlks3ykzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UmmrY/btsMzu8hFe0/yKfb4RWHkECpPUlks3ykzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUmmrY%2FbtsMzu8hFe0%2FyKfb4RWHkECpPUlks3ykzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1612&quot; height=&quot;488&quot; data-origin-width=&quot;1612&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 앱에서 실행한 결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 앱에서도 동일한 Injection을 수행했을 때, &lt;b&gt;앱이 크래시&lt;/b&gt;하는 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▼ 할 일 추가 제목에 Injection 시도 &amp;rarr; 결과: 앱 크래시 종료&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2722&quot; data-origin-height=&quot;1764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eI2777/btsMyldkIon/Czrv0WcAvbCvPgyW4WR0i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eI2777/btsMyldkIon/Czrv0WcAvbCvPgyW4WR0i0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eI2777/btsMyldkIon/Czrv0WcAvbCvPgyW4WR0i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeI2777%2FbtsMyldkIon%2FCzrv0WcAvbCvPgyW4WR0i0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2722&quot; height=&quot;1764&quot; data-origin-width=&quot;2722&quot; data-origin-height=&quot;1764&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;b&gt;SQL 쿼리에 사용자 입력값을 직접 포함&lt;/b&gt;하면 발생하는 문제다. SQL Injection에 대해 이론적으로만 알고 있었는데, 직접 테스트를 통해 실제로 발생할 수 있는 보안 문제임을 확인할 수 있었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 잠깐, SQL Injection이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SQL Injection&lt;/b&gt;은 &lt;b&gt;사용자 입력값이 SQL 쿼리에 직접 삽입될 때 발생하는 보안 취약점&lt;/b&gt;으로, 악의적인 사용자가 SQL 문을 변조하여 &lt;b&gt;데이터베이스의 내용을 무단 조회, 수정, 삭제&lt;/b&gt;하는 공격 기법이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQL Injection의 위험성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 변조&lt;/b&gt;: &lt;code&gt;WHERE 1=1&lt;/code&gt;을 추가하여 모든 데이터를 변경할 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 유출&lt;/b&gt;: &lt;code&gt;UNION SELECT&lt;/code&gt; 등을 사용하여 테이블의 모든 데이터 조회 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 삭제&lt;/b&gt;: &lt;code&gt;DROP TABLE&lt;/code&gt; 명령을 삽입하여 테이블 삭제 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  SQL Injection 방지 방법 (Prepared Statement 사용)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLite를 사용하는 현 상황에서 SQL Injection을 방지하기 위해 &lt;b&gt;Prepared Statement&lt;/b&gt;를 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;Prepared Statement란?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Prepared Statement는 SQL 쿼리를 실행할 때 &lt;b&gt;미리 컴파일된 SQL 문&lt;/b&gt;을 사용하는 방식이다. 즉, 애플리케이션이 SQL 문을 실행할 때마다 전체 SQL 문을 다시 해석(파싱)하고 실행하는 것이 아니라, &lt;b&gt;미리 컴파일된 SQL 문을 재사용&lt;/b&gt;하는 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 &lt;b&gt;SQL Injection 공격을 방지하고, 성능을 최적화&lt;/b&gt;하는 데 도움을 준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;Prepared Statement의 동작 과정&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;SQL 템플릿을 미리 컴파일&lt;/b&gt;&amp;rarr; &lt;code&gt;?&lt;/code&gt;(바인딩 변수)를 포함한 SQL 문을 먼저 데이터베이스에 전달하여 컴파일함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 입력 값과 SQL을 분리&lt;/b&gt;&amp;rarr; &lt;code&gt;?&lt;/code&gt; 자리에 들어갈 실제 값을 나중에 바인딩(대입)하여 실행.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실제 값이 SQL과 함께 실행됨&lt;/b&gt;&amp;rarr; 입력 값은 데이터베이스 엔진에서 &lt;b&gt;문자열 값 그대로 처리&lt;/b&gt;되며, 실행 계획을 변경하지 않음.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;Prepared Statement 예제 (SQLite 사용)&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SQLiteDatabase db = getWritableDatabase();
String sql = &quot;INSERT INTO TODOLIST_TB (title, content, writeDate) VALUES (?, ?, ?)&quot;;

SQLiteStatement stmt = db.compileStatement(sql);
stmt.bindString(1, title);
stmt.bindString(2, content);
stmt.bindString(3, writeDate);
stmt.executeInsert();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 &lt;code&gt;?&lt;/code&gt;에 들어갈 값을 &lt;code&gt;bindString()&lt;/code&gt;으로 바인딩(대입)하고 실행하는 방식&lt;/li&gt;
&lt;li&gt;이렇게 하면 사용자가 입력한 값이 SQL 문 자체에 포함되지 않고 &lt;b&gt;독립적인 데이터 값으로 처리됨&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;? 바인딩이 Escape 처리를 한다는 의미&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL Injection의 핵심은 사용자의 입력값이 &lt;b&gt;SQL 문법과 결합&lt;/b&gt;되어 의도치 않은 실행을 유도하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 기존 코드에서는 다음과 같이 문자열을 직접 SQL 문에 삽입하고 있었다&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;String query = &quot;SELECT * FROM TODOLIST_TB WHERE title = '&quot; + userInput + &quot;'&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 &lt;code&gt;userInput&lt;/code&gt;에 만약 &lt;b&gt;&lt;code&gt;' OR 1=1 --&lt;/code&gt;&lt;/b&gt; 같은 값이 들어가면?&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT * FROM TODOLIST_TB WHERE title = '' OR 1=1 --'&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;1=1&lt;/code&gt;은 항상 참(True)이므로 &lt;b&gt;모든 데이터가 반환&lt;/b&gt;됨.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-&lt;/code&gt; 이후는 주석 처리되어 SQL 구문이 변조됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;Prepared Statement에서는 ? 바인딩을 사용하여 Escape 처리를 자동으로 수행한&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;String query = &quot;SELECT * FROM TODOLIST_TB WHERE title = ?&quot;;
Cursor cursor = db.rawQuery(query, new String[]{userInput});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;?&lt;/code&gt; 자리에 &lt;b&gt;입력값이 들어갈 때, 자동으로 Escape 처리&lt;/b&gt;되어 &lt;b&gt;문자열 그대로&lt;/b&gt; 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 &lt;code&gt;' OR 1=1 --&lt;/code&gt; 같은 값을 입력하더라도&lt;/li&gt;
&lt;li&gt;데이터베이스 엔진은 이를 &lt;b&gt;문자열 값 그대로 해석&lt;/b&gt;하고,&lt;/li&gt;
&lt;li&gt;SQL 문법과 결합되지 않도록 &lt;b&gt;안전하게 처리하게 되는 것이다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ &lt;b&gt;Escape 처리란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Escape 처리는 &lt;b&gt;특수 문자(&lt;code&gt;'&lt;/code&gt;, &lt;code&gt;&quot;&lt;/code&gt;, &lt;code&gt;;&lt;/code&gt;, &lt;code&gt;--&lt;/code&gt; 등)가 SQL 문법으로 해석되지 않도록 보호하는 방법이&lt;/b&gt;다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Prepared Statement에서 &lt;b&gt;자동으로 Escape 처리를 수행&lt;/b&gt;하므로,&lt;/li&gt;
&lt;li&gt;개발자가 따로 &lt;code&gt;replace(&quot;'&quot;, &quot;\\'&quot;)&lt;/code&gt; 같은 조작을 할 필요가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 돌아와서 수정해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;☠️&lt;/span&gt;&amp;nbsp; 기존의 취약한 코드 (❌ 위험한 방식)&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public void updateTodo(int _id, String _title, String _content, String _writeDate) {
    SQLiteDatabase db = getWritableDatabase();
    db.execSQL(&quot;UPDATE TODOLIST_TB SET title='&quot; + _title + &quot;', content='&quot; + _content + &quot;', writeDate='&quot; + _writeDate + &quot;' WHERE id=&quot; + _id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 입력값이 직접 SQL 문에 삽입됨 &amp;rarr; SQL Injection 위험&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;nbsp; 개선된 안전한 코드 (✅ Prepared Statement 사용)&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public void updateTodoSecure(int _id, String _title, String _content, String _writeDate) {
    SQLiteDatabase db = getWritableDatabase();
    String query = &quot;UPDATE TODOLIST_TB SET title = ?, content = ?, writeDate = ? WHERE id = ?&quot;;
    db.execSQL(query, new Object[]{_title, _content, _writeDate, _id});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ &lt;b&gt;&lt;code&gt;?&lt;/code&gt; 바인딩을 사용하면 SQL Injection을 방어할 수 있음&lt;/b&gt;&lt;br /&gt;✔ &lt;b&gt;입력값을 자동으로 Escape 처리하여 악의적인 SQL 코드 실행 방지&lt;/b&gt;&lt;br /&gt;✔ &lt;b&gt;SQL 문법이 깨지지 않아 앱이 크래시하는 문제도 해결&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt; &lt;/span&gt; 개선된 코드 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 방법으로 &lt;code&gt;DBHelper&lt;/code&gt;를 수정하고 다시 테스트 코드를 실행한 결과, &lt;b&gt;Injection이 차단되어 더 이상 여러 개의 행이 변경되지 않음을 확인&lt;/b&gt;할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▼ SQL Injection 테스트 코드가 통과되지 못하는 것 캡쳐 및 앱이 크래시 되지 않은 상태 캡쳐&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2694&quot; data-origin-height=&quot;2330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UIAdl/btsMzeYQSj3/r3NXVua5HfPyZfS0mYYcv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UIAdl/btsMzeYQSj3/r3NXVua5HfPyZfS0mYYcv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UIAdl/btsMzeYQSj3/r3NXVua5HfPyZfS0mYYcv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUIAdl%2FbtsMzeYQSj3%2Fr3NXVua5HfPyZfS0mYYcv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2694&quot; height=&quot;2330&quot; data-origin-width=&quot;2694&quot; data-origin-height=&quot;2330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;✅ Injection 시도 후에도 1개의 행만 업데이트됨
✅ 앱 크래시 문제 해결
✅ Injection 방어 성공&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;⭐&lt;/span&gt; &amp;nbsp; DBHelper에 Prepared Statement를 적용하기 전 테스트 코드를 작성해두자.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변경이 원래 기능에 영향이 미치지 않는지 확인용으로 테스트 코드를 작성해두고자 한다.&lt;/li&gt;
&lt;li&gt;또한, 위의 Injection 테스트 코드를 아래와 같이 변경하여 Injection 방어가 되었다면 테스트가 통과 되도록 변경했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt; // Injection이 성공했다면 모든 To-Do의 날짜가 변경됨 (원래 1개만 변경되어야 함)
        //assertTrue(&quot;SQL Injection should have updated multiple rows unexpectedly!&quot;, manipulatedList.size() &amp;gt; 1);
        // Injection 방어 코드 작성 후 False가 되어야 테스트 코드 통과
        assertFalse(&quot;SQL Injection should have updated multiple rows unexpectedly!&quot;, manipulatedList.size() &amp;gt; 1);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 코드 변경 전, 후로 동일하게 테스트 코드가 통과되는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세히 보면 Prepared Statement 적용 후 실행 시간이 살짝 증가한 것을 볼 수 있다. 이는 테스트 환경에서 Prepared Statement가 처음 실행되어 SQL 파싱, 컴파일, 실행 준비 과정으로 인해 증가한 것으로 볼 수 있다. 이후 실제 앱 사용에서 같은 SQL을 다시 실행할 때는 이미 준비된 SQL을 활용해서 파싱/컴파일 과정 없이 값만 바인딩해서 바로 실행되므로 시간이 절약될 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3066&quot; data-origin-height=&quot;1216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u0ddU/btsMAu7JASg/llxml2rpHzbXwTrpRhhxr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u0ddU/btsMAu7JASg/llxml2rpHzbXwTrpRhhxr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u0ddU/btsMAu7JASg/llxml2rpHzbXwTrpRhhxr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu0ddU%2FbtsMAu7JASg%2Fllxml2rpHzbXwTrpRhhxr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3066&quot; height=&quot;1216&quot; data-origin-width=&quot;3066&quot; data-origin-height=&quot;1216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;⭐&lt;/span&gt;&amp;nbsp; SQL Injection 테스트 및 해결 과정 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 테스트를 통해 레거시 코드에서 SQL Injection이 발생할 수 있음을 확인했고, 이를 방지하기 위해 &lt;b&gt;Prepared Statement를 사용한 안전한 SQL 실행 방식&lt;/b&gt;을 적용했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h1&gt;☺️ 후기&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SQL Injection을 직접 테스트하면서 배운 점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예전에는 이론적으로만 알던 SQL Injection이 실제로 앱에서 어떻게 발생하는지 경험하면서 더 확실히 이해할 수 있었다.&lt;/li&gt;
&lt;li&gt;Prepared Statement를 적용하고 나서 Injection이 방어되는 걸 직접 테스트로 확인하니 보안적인 개선이 체감되었다.&lt;/li&gt;
&lt;li&gt;앱 보안을 고려한 SQL 실행 방식이 얼마나 중요한지 다시 한번 깨달았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;향후 개선하고 싶은 부분&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQLite &amp;rarr; Room으로 마이그레이션 하는 것을 고려하고 있다.&lt;/li&gt;
&lt;li&gt;SQLite를 사용해도 충분히 구현 가능하지만 버전 관리 및 유지보수 편리성으로 Room으로 마이그레이션 하는 것이 좋을 것 같다는 생각을 했다. 추가 리팩토링이나 기능 추가가 된다면 그때 Room으로 변경해보는 것을 고려해봐야겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Android/오르다 다이어리</category>
      <category>Android</category>
      <category>compile</category>
      <category>Prepared Statement</category>
      <category>room</category>
      <category>SQL</category>
      <category>sql injection</category>
      <category>SQLite</category>
      <category>testcode</category>
      <category>레거시 리팩토링</category>
      <category>테스트 코드</category>
      <author>yujinius</author>
      <guid isPermaLink="true">https://yujinius45.tistory.com/163</guid>
      <comments>https://yujinius45.tistory.com/163#entry163comment</comments>
      <pubDate>Sun, 2 Mar 2025 19:46:54 +0900</pubDate>
    </item>
    <item>
      <title>[Android/오르다 다이어리] 레거시 리팩토링 07 - SQLite, MapMemo 기능 완성</title>
      <link>https://yujinius45.tistory.com/162</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 07 - SQLite, MapMemo 기능 완성&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글: &lt;a href=&quot;https://yujinius45.tistory.com/161&quot;&gt;https://yujinius45.tistory.com/161&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740911428013&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android/오르다 다이어리] 레거시 리팩토링 06 - DBHelper 싱글톤 패턴 적용&quot; data-og-description=&quot;[Android/오르다 다이어리] 레거시 리팩토링 06 - DBHelper 싱글톤 패턴 적용&amp;nbsp;지난글: https://yujinius45.tistory.com/160&amp;nbsp;[Android/오르다 다이어리] 레거시 리팩토링 05 - Configuration Change 대응 (ViewModel 적용)[Androi&quot; data-og-host=&quot;yujinius45.tistory.com&quot; data-og-source-url=&quot;https://yujinius45.tistory.com/161&quot; data-og-url=&quot;https://yujinius45.tistory.com/161&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/VoKS3/hyYjEZlneL/YTS4kKekZpMLZbyBHFtYZK/img.png?width=800&amp;amp;height=480&amp;amp;face=0_0_800_480,https://scrap.kakaocdn.net/dn/V5IaM/hyYmX34aBA/m8SmzkwskF07GQQNr0Ktuk/img.png?width=800&amp;amp;height=480&amp;amp;face=0_0_800_480,https://scrap.kakaocdn.net/dn/hlWBG/hyYm0sVYkg/XEKQG9Z8PjzKf0UCWWjb7k/img.png?width=2936&amp;amp;height=1300&amp;amp;face=0_0_2936_1300&quot;&gt;&lt;a href=&quot;https://yujinius45.tistory.com/161&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yujinius45.tistory.com/161&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/VoKS3/hyYjEZlneL/YTS4kKekZpMLZbyBHFtYZK/img.png?width=800&amp;amp;height=480&amp;amp;face=0_0_800_480,https://scrap.kakaocdn.net/dn/V5IaM/hyYmX34aBA/m8SmzkwskF07GQQNr0Ktuk/img.png?width=800&amp;amp;height=480&amp;amp;face=0_0_800_480,https://scrap.kakaocdn.net/dn/hlWBG/hyYm0sVYkg/XEKQG9Z8PjzKf0UCWWjb7k/img.png?width=2936&amp;amp;height=1300&amp;amp;face=0_0_2936_1300');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 06 - DBHelper 싱글톤 패턴 적용&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 06 - DBHelper 싱글톤 패턴 적용&amp;nbsp;지난글: https://yujinius45.tistory.com/160&amp;nbsp;[Android/오르다 다이어리] 레거시 리팩토링 05 - Configuration Change 대응 (ViewModel 적용)[Androi&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yujinius45.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지난 글을 통해 DBHelper를 싱글톤으로 변경하였고 이에 따라 테스트 코드 작성이 원활해졌다.&lt;/b&gt; DBHelper를 싱글톤으로 변경하면서 테스트 코드 작성이 가능해진 이유는 다음과 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  기존 구조의 문제 (MainActivity 의존)&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존에는 &lt;b&gt;MainActivity에서 static으로 선언된 &lt;code&gt;mDBHelper&lt;/code&gt;를 import해서 사용&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;즉, &lt;b&gt;DBHelper를 사용하려면 반드시 MainActivity가 실행되어야 하는 구조&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;b&gt;테스트 코드에서 DBHelper를 단독으로 사용하기 어려웠음.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ 싱글톤 패턴 적용 후 개선된 점&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;MainActivity 의존성 제거&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;MainActivity.mDBHelper&lt;/code&gt;를 참조하는 방식이 사라짐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DBHelper.getInstance(context)&lt;/code&gt;를 호출하면 어디서든 같은 인스턴스를 사용할 수 있음.&lt;br /&gt;&amp;rarr; &lt;b&gt;테스트 코드에서 MainActivity 없이 독립적으로 DBHelper를 사용할 수 있음!&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 환경에서도 context를 넘겨줄 수 있음&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드에서 &lt;code&gt;ApplicationProvider.getApplicationContext()&lt;/code&gt; 같은 방식으로 &lt;b&gt;독립적인 context를 제공 가능&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;따라서, &lt;code&gt;DBHelper.getInstance(context)&lt;/code&gt;를 호출해서 데이터베이스 테스트가 가능해짐.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  정리하면?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;이전에는 MainActivity 실행이 필요했지만, 이제는 테스트 코드에서 직접 &lt;code&gt;DBHelper.getInstance(context)&lt;/code&gt;를 호출할 수 있어서 독립적인 테스트 코드 작성이 가능해진 것이다!&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h1&gt;  오늘 진행 내용 미리보기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 오늘은 테스트 코드를 추가하고, MapMemo의 기능을 완성해줄 예정이다. MapMemo 같은 경우 DB에 저장되지 않아 메모가 날라가는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▼ MapMemo 데이터 유지X Configuration change 대응X&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;[오르다 다이어리] 04 map 기능 완성 전.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kEr1e/btsMyDrj288/5SYU9P6gFAXqmat3d1SMm0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kEr1e/btsMyDrj288/5SYU9P6gFAXqmat3d1SMm0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kEr1e/btsMyDrj288/5SYU9P6gFAXqmat3d1SMm0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/kEr1e/btsMyDrj288/5SYU9P6gFAXqmat3d1SMm0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;336&quot; height=&quot;720&quot; data-filename=&quot;[오르다 다이어리] 04 map 기능 완성 전.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업에서는 ⭐ &lt;b&gt;MapMemo를 저장할 Table을 추가하고, 데이터 저장/불러오는 코드를 작성한 후, 테스트를 먼저 진행한 뒤 로직을 추가&lt;/b&gt;하는 방식으로 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 SQLite 사용의 불편한 점도 함께 살펴보았다. 또한, SQL Injection을 직접 테스트 해보고 이를 방지하기 위해 Prepared Statement를 적용해보았다 . 이 부분은 다음 글에 작성해보겠다. 오늘은 MapMemo 기능 완성에 집중해보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  현재 방식: SQLite로 cursor 옮기며 직접 쿼리 실행&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 SQLite를 사용할 때, 직접 SQL 쿼리를 실행하고 &lt;code&gt;Cursor&lt;/code&gt;를 통해 데이터를 처리하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// SQLite 방식 - Cursor 사용 (불편한 점)
Cursor cursor = db.rawQuery(&quot;SELECT * FROM MAPMEMO_TB&quot;, null);
while (cursor.moveToNext()) {
    int id = cursor.getInt(cursor.getColumnIndexOrThrow(&quot;id&quot;));
    String title = cursor.getString(cursor.getColumnIndexOrThrow(&quot;title&quot;));
    String content = cursor.getString(cursor.getColumnIndexOrThrow(&quot;content&quot;));
    double latitude = cursor.getDouble(cursor.getColumnIndexOrThrow(&quot;latitude&quot;));
    double longitude = cursor.getDouble(cursor.getColumnIndexOrThrow(&quot;longitude&quot;));
}
cursor.close();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 코드가 길어지고 가독성이 떨어진다는 문제가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❓만약 Room이라면?&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// Room 방식 - DAO를 활용한 간결한 코드
@Query(&quot;SELECT * FROM mapmemo_tb&quot;)
fun getAllMapMemos(): List&amp;lt;MapMemoItem&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;차이점&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQLite는 Cursor를 사용하여 &lt;code&gt;getColumnIndex()&lt;/code&gt;로 일일이 값을 가져와야 함 (가독성이 안 좋음).&lt;/li&gt;
&lt;li&gt;Room에서는 SQL 없이 DAO 메서드를 호출하는 것만으로 데이터를 가져올 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h1&gt;✋ 잠깐! 혹시 cursor를 처음 보나요?&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그렇다면 아래를 읽어보면 좋다. 요즘은 RoomDB를 많이 써서 이렇게 cursor로 직접 db를 다루는 코드는 익숙하지 않을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; ️ SQLite란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLite는 &lt;code&gt;경량화된 관계형 데이터베이스(RDBMS)&lt;/code&gt;로, &lt;b&gt;앱 내에서 독립적으로 실행되는 데이터베이스이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 별도의 서버 없이 &lt;b&gt;파일 기반으로 동작&lt;/b&gt;하고, 안드로이드에서도 &lt;b&gt;기본 제공되는 내장 DB이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 없이 동작하는 &lt;b&gt;임베디드(Embedded) 데이터베이스&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 기반&lt;/b&gt;으로 동작 (앱 내부의 &lt;code&gt;.db&lt;/code&gt; 파일에 데이터 저장)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SQL 문법을 지원&lt;/b&gt; (SELECT, INSERT, UPDATE, DELETE 등)&lt;/li&gt;
&lt;li&gt;앱이 설치된 기기에서 &lt;b&gt;로컬 데이터 저장&lt;/b&gt; 용도로 사용됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;SQLite 사용 방법&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터베이스 &lt;b&gt;생성 및 테이블 정의&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;SQL 쿼리를 실행하여 &lt;b&gt;데이터 삽입, 조회, 수정, 삭제&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;조회된 데이터를 &lt;b&gt;Cursor&lt;/b&gt;를 통해 접근 및 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; ️ Cursor란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor는 &lt;b&gt;SQL 조회(SELECT) 결과를 가리키는 포인터이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;쿼리 실행 결과를 담고 있는 데이터셋을 가리키는 객체&lt;/b&gt;로, 데이터를 한 행씩 이동하면서 읽을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;Cursor의 역할&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;SELECT&lt;/code&gt; 쿼리를 실행하면, 결과 데이터들이 Cursor에 저장됨&lt;/li&gt;
&lt;li&gt;&lt;code&gt;moveToNext()&lt;/code&gt;를 호출하여 &lt;b&gt;한 행씩 이동&lt;/b&gt;하면서 데이터를 읽을 수 있음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getColumnIndex(&quot;컬럼명&quot;)&lt;/code&gt; 또는 &lt;code&gt;getColumnIndexOrThrow(&quot;컬럼명&quot;)&lt;/code&gt;을 사용하여 특정 컬럼의 데이터를 가져옴&lt;/li&gt;
&lt;li&gt;데이터 처리가 끝나면 &lt;code&gt;cursor.close()&lt;/code&gt;를 호출하여 &lt;b&gt;메모리 해제&lt;/b&gt;를 해줘야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  SQLite + Cursor를 이용한 데이터 조회 원리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLite에서 데이터를 조회하고 Cursor를 사용하여 데이터를 가져오는 기본적인 원리를 코드와 함께 설명하자면 다음과 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 1. 데이터베이스 테이블 생성&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;db.execSQL(&quot;CREATE TABLE IF NOT EXISTS USERS (&quot; +
           &quot;id INTEGER PRIMARY KEY AUTOINCREMENT, &quot; +
           &quot;name TEXT NOT NULL, &quot; +
           &quot;age INTEGER NOT NULL);&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;USERS&lt;/code&gt; 테이블을 만들고, &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;age&lt;/code&gt; 컬럼을 포함함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 2. 데이터 삽입 (INSERT)&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SQLiteDatabase db = getWritableDatabase();
db.execSQL(&quot;INSERT INTO USERS (name, age) VALUES ('Alice', 25)&quot;);
db.execSQL(&quot;INSERT INTO USERS (name, age) VALUES ('Bob', 30)&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;INSERT INTO USERS&lt;/code&gt;를 실행하여 데이터를 추가함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 3. 데이터 조회 (SELECT)와 Cursor 사용&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.rawQuery(&quot;SELECT * FROM USERS&quot;, null);

while (cursor.moveToNext()) {
    int id = cursor.getInt(cursor.getColumnIndexOrThrow(&quot;id&quot;));
    String name = cursor.getString(cursor.getColumnIndexOrThrow(&quot;name&quot;));
    int age = cursor.getInt(cursor.getColumnIndexOrThrow(&quot;age&quot;));

    System.out.println(&quot;ID: &quot; + id + &quot;, Name: &quot; + name + &quot;, Age: &quot; + age);
}
cursor.close();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;처리 과정&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;rawQuery(&quot;SELECT * FROM USERS&quot;, null)&lt;/code&gt;를 실행하면, Cursor 객체가 반환됨.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;moveToNext()&lt;/code&gt;를 호출하여 Cursor가 한 행씩 이동하면서 데이터를 읽음.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getColumnIndexOrThrow(&quot;컬럼명&quot;)&lt;/code&gt;을 사용하여 컬럼의 인덱스를 가져옴.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getInt()&lt;/code&gt;, &lt;code&gt;getString()&lt;/code&gt; 등의 메서드로 데이터를 읽음.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cursor.close()&lt;/code&gt;를 호출하여 &lt;b&gt;Cursor를 닫고 메모리 해제&lt;/b&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  Cursor를 사용하지 않으면?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Cursor를 사용하지 않는다면, 데이터 조회 후 &lt;b&gt;반환되는 행들을 직접 처리할 방법이 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;반복문을 사용하여 데이터를 한 줄씩 이동하면서 읽는 과정이 불가능&lt;/b&gt;해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Cursor는 SQLite의 데이터를 읽는 핵심적인 역할을 담당하는 객체!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  SQLite는 &lt;b&gt;앱 내부에서 독립적으로 실행되는 가벼운 RDBMS&lt;/b&gt;이며, SQL 문법을 그대로 사용해서 데이터를 저장하고 조회할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Cursor는 &lt;b&gt;SQL 쿼리 결과를 가리키는 포인터&lt;/b&gt;로, 데이터를 한 행씩 이동하면서 읽는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;code&gt;moveToNext()&lt;/code&gt;를 이용해 한 줄씩 데이터를 이동하면서 &lt;b&gt;컬럼 값을 가져오는 방식으로 처리&lt;/b&gt;한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &lt;/b&gt; 참고: Room&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Room은 SQLite의 기능을 추상화하여 보다 편리하게 데이터베이스 작업을 수행할 수 있도록 설계된 라이브러리이다.&lt;/li&gt;
&lt;li&gt;Room은 내부적으로 SQLite를 사용하지만, 개발자가 직접 SQL 쿼리를 작성하고 Cursor를 다루는 복잡성을 줄여준다. 대신, &lt;b&gt;Entity&lt;/b&gt;, &lt;b&gt;DAO(Data Access Object)&lt;/b&gt;, &lt;b&gt;Database&lt;/b&gt;의 세 가지 주요 구성 요소를 통해 데이터베이스 작업을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Room은 Nature Album에서 사용했었는데 확실히 지금 이렇게 cursor로 하는 것보다 헷갈리지 않고 편했었다. 그래도 오르다 다이어리는 우선 기존 방식 그대로 SQLite를 cursor를 통해 접근하는 기존 방식으로 일단 진행해보고자 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  MapMemoItem 추가&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Java 버전과 Kotlin 버전 비교&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Java 방식:&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class MapMemoItem {
    private int id;
    private String title;
    private String content;
    private double latitude;
    private double longitude;

  // getter, setter, constructor 필요
    public MapMemoItem(int id, String title, String content, double latitude, double longitude) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.latitude = latitude;
        this.longitude = longitude;
    }

    public int getId() {
        return id;
    }

    // 생략
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin으로 변경하면 &lt;b&gt;가독성이 좋아진다&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package smu.team3_orda_diary.model

data class MapMemoItem(
    val id: Int,
    val title: String,
    val content: String,
    val latitude: Double,
    val longitude: Double
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  getter, setter 없이 가독성이 좋아져서 Kotlin을 선택했다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  MapMemo 테이블 추가와 DB Upgrade&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블 수정이 있을 때마다 &lt;b&gt;DB 버전을 올려야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;기존 유저와 신규 유저 모두 정상 동작하도록 &lt;code&gt;CREATE&lt;/code&gt;와 &lt;code&gt;onUpgrade()&lt;/code&gt;에 추가해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class DBHelper extends SQLiteOpenHelper {
    public static final int DB_VERSION = 2;
    public static final String DB_NAME = &quot;Ordatest5.db&quot;;
...
    @Override
    public void onCreate(SQLiteDatabase db) {
    ...
        db.execSQL(&quot;CREATE TABLE IF NOT EXISTS MAPMEMO_TB (&quot; +
                &quot;id INTEGER PRIMARY KEY AUTOINCREMENT, &quot; +
                &quot;title TEXT NOT NULL, &quot; +
                &quot;content TEXT NOT NULL, &quot; +
                &quot;latitude REAL NOT NULL, &quot; +
                &quot;longitude REAL NOT NULL);&quot;);
    }
...
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion &amp;lt; 2) {
            db.execSQL(&quot;CREATE TABLE IF NOT EXISTS MAPMEMO_TB (&quot; +
                    &quot;id INTEGER PRIMARY KEY AUTOINCREMENT, &quot; +
                    &quot;title TEXT NOT NULL, &quot; +
                    &quot;content TEXT NOT NULL, &quot; +
                    &quot;latitude REAL NOT NULL, &quot; +
                    &quot;longitude REAL NOT NULL);&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  의문점: 테이블 수정 있을 때마다 onUpgrade()이 변경되어 복잡해지는가? &amp;rArr; yes&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지는 버전 1인 상태였지만 테이블 수정이 일어나면 버전을 올리고 onUpgrade()에 작업을 해줘야 한다. 위의 코드에서 볼 수 있듯이 버전 업되어 수정사항이 생길 수록 많은 코드가 작성될 것으로 보이지 않는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;맞다. 테이블 수정이 있을 때마다 &lt;code&gt;onUpgrade()&lt;/code&gt;가 점점 복잡해질 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  해결 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ &lt;b&gt;DB Migration 클래스를 따로 관리하기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public class DBMigration {
    public static void migrate(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion &amp;lt; 2) {
            db.execSQL(&quot;CREATE TABLE IF NOT EXISTS MAPMEMO_TB (&quot; +
                    &quot;id INTEGER PRIMARY KEY AUTOINCREMENT, &quot; +
                    &quot;title TEXT NOT NULL, &quot; +
                    &quot;content TEXT NOT NULL, &quot; +
                    &quot;latitude REAL NOT NULL, &quot; +
                    &quot;longitude REAL NOT NULL);&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;DBHelper&lt;/code&gt;에서는 아래처럼 &lt;code&gt;DBMigration.migrate()&lt;/code&gt; 호출만 하면 된다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    DBMigration.migrate(db, oldVersion, newVersion);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣ &lt;b&gt;Room Database 사용 고려&lt;/b&gt; (장기적으로 가장 효율적인 방법)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Room을 사용하면 &lt;b&gt;자동으로 버전 변경을 감지하고 Migration을 쉽게 관리&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Database(entities = {MapMemoItem.class}, version = 2)
public abstract class AppDatabase extends RoomDatabase {
    public abstract MapMemoDao mapMemoDao();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 고민과 결론&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재만 수정이 이루어졌고 이후에 또 추가 기능이 언제 들어올지는 모른다. 그래서 사실 class로 빼지 않아도 된다. 또한 Room DB도 지금 저 버전 업 하나 때문에 교체하기에는 애매하다.&lt;/li&gt;
&lt;li&gt;결론적으로 다음에 DB 변경이 필요하게 되면 Room으로 아예 Migration 하여 관리하기 쉽게 해야겠다.&lt;/li&gt;
&lt;li&gt;현재는 방법만 알아두고 일단 MapMemo 기능 완성부터 진행하고자 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;진행 결과&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MapMemo가 &lt;b&gt;이제 DB에 저장되도록 수정되었다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 코드 작성 후, SQLite 정상 동작 확인 완료했다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fragment 로직 적용:&lt;/b&gt; 저장 시 DB에 저장하고, 앱 재시작 시 DB에서 불러와 지도에 표시하도록 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과적으로, 이전에는 MapMemo가 유지되지 않았지만, 이제는 저장 후 다시 불러와도 유지되어 데이터 저장은 물론 Configuration change 대응도 된 것이다!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▼ MapMemo 데이터 유지O Configuration change 대응O&lt;/p&gt;
&lt;figure data-ke-type=&quot;image&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-ke-style=&quot;alignCenter&quot;&gt;&lt;span style=&quot;width: 85%;&quot; class=&quot;bar_progress&quot;&gt;&lt;/span&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;[오르다 다이어리] 04 map 기능 완성 후.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0ZaBc/btsMyB735b8/6keHWdmaqEKNhiZIgTszO0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0ZaBc/btsMyB735b8/6keHWdmaqEKNhiZIgTszO0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0ZaBc/btsMyB735b8/6keHWdmaqEKNhiZIgTszO0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b0ZaBc/btsMyB735b8/6keHWdmaqEKNhiZIgTszO0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;336&quot; height=&quot;720&quot; data-filename=&quot;[오르다 다이어리] 04 map 기능 완성 후.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/오르다 다이어리</category>
      <category>Android</category>
      <category>cursor</category>
      <category>db</category>
      <category>map</category>
      <category>room</category>
      <category>SQL</category>
      <category>SQLite</category>
      <category>레거시 코드</category>
      <category>리팩토링</category>
      <author>yujinius</author>
      <guid isPermaLink="true">https://yujinius45.tistory.com/162</guid>
      <comments>https://yujinius45.tistory.com/162#entry162comment</comments>
      <pubDate>Sun, 2 Mar 2025 19:39:57 +0900</pubDate>
    </item>
    <item>
      <title>[Android/오르다 다이어리] 레거시 리팩토링 06 - DBHelper 싱글톤 패턴 적용</title>
      <link>https://yujinius45.tistory.com/161</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; [Android/오르다 다이어리] 레거시 리팩토링 06 - DBHelper 싱글톤 패턴 적용&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난글: &lt;a href=&quot;https://yujinius45.tistory.com/160&quot;&gt;https://yujinius45.tistory.com/160&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740840326152&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android/오르다 다이어리] 레거시 리팩토링 05 - Configuration Change 대응 (ViewModel 적용)&quot; data-og-description=&quot;[Android/오르다 다이어리] 레거시 리팩토링 05 - Configuration Change 대응 (ViewModel 적용) 지난 글: https://yujinius45.tistory.com/159&amp;nbsp;[Android/오르다 다이어리] 레거시 리팩토링 04 - Fragment 마이그레이션[Android/오&quot; data-og-host=&quot;yujinius45.tistory.com&quot; data-og-source-url=&quot;https://yujinius45.tistory.com/160&quot; data-og-url=&quot;https://yujinius45.tistory.com/160&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Ne9C5/hyYmUe77DA/Du83L6kCkd9yh6aiDBA5yK/img.png?width=800&amp;amp;height=365&amp;amp;face=0_0_800_365,https://scrap.kakaocdn.net/dn/bfn9Pc/hyYmPdOxEA/HwIVOMLqwuIhsBd81CbtHk/img.png?width=800&amp;amp;height=365&amp;amp;face=0_0_800_365,https://scrap.kakaocdn.net/dn/ctpQBB/hyYmMOVzOY/Ycg4mVT9DChWSJ09k7Gexk/img.png?width=3638&amp;amp;height=1662&amp;amp;face=0_0_3638_1662&quot;&gt;&lt;a href=&quot;https://yujinius45.tistory.com/160&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yujinius45.tistory.com/160&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Ne9C5/hyYmUe77DA/Du83L6kCkd9yh6aiDBA5yK/img.png?width=800&amp;amp;height=365&amp;amp;face=0_0_800_365,https://scrap.kakaocdn.net/dn/bfn9Pc/hyYmPdOxEA/HwIVOMLqwuIhsBd81CbtHk/img.png?width=800&amp;amp;height=365&amp;amp;face=0_0_800_365,https://scrap.kakaocdn.net/dn/ctpQBB/hyYmMOVzOY/Ycg4mVT9DChWSJ09k7Gexk/img.png?width=3638&amp;amp;height=1662&amp;amp;face=0_0_3638_1662');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 05 - Configuration Change 대응 (ViewModel 적용)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 05 - Configuration Change 대응 (ViewModel 적용) 지난 글: https://yujinius45.tistory.com/159&amp;nbsp;[Android/오르다 다이어리] 레거시 리팩토링 04 - Fragment 마이그레이션[Android/오&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yujinius45.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  ViewModel 도입 과정에서 발견한 DBHelper 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서는 &lt;b&gt;Configuration Change 대응&lt;/b&gt;을 위해 ViewModel을 도입하고, LiveData와 DataBinding을 활용하여 &lt;b&gt;MVVM 패턴을 적용&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 &lt;b&gt;ViewModel에서 DBHelper를 주입하는 과정에서 예상치 못한 문제를 발견했다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로, &lt;b&gt;DBHelper가 여러 개 생성될 수 있다는 점&lt;/b&gt;이었다!  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;어? 우리는 DBHelper가 하나만 존재해야 한다고 생각했는데...?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVVM 패턴 적용을 통해 &lt;b&gt;UI와 비즈니스 로직을 분리&lt;/b&gt;하면서 오히려 &lt;b&gt;과거 우리가 의도했던 구조의 문제점이 드러난 것&lt;/b&gt;이었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  과거 우리 팀이 DBHelper를 설계했던 방식&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;⏳ 3년 전, 오르다 다이어리를 처음 만들던 시절&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오르다 다이어리 프로젝트를 처음 시작할 때, 우리 팀은 &lt;b&gt;DBHelper를 어떻게 관리할지 고민했다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 우리 팀의 &lt;b&gt;프로그래밍 사고방식&lt;/b&gt;은 이랬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;&quot;DBHelper는 어차피 SQLite에 접근해서 데이터를 저장/조회하는 용도인데, 굳이 여러 개 만들 필요 있을까?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;&quot;하나만 만들어서 모든 곳에서 공유해서 쓰면 되지 않을까?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;MainActivity에서 DBHelper를 &lt;code&gt;static&lt;/code&gt;으로 선언한 후, 모든 곳에서 &lt;code&gt;MainActivity.mDBHelper&lt;/code&gt;를 import해서 사용&lt;/b&gt;했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  당시 우리가 선택한 코드 (기존 방식)&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    private NavController navController;
    public static DBHelper mDBHelper;  // static으로 선언

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        mDBHelper = new DBHelper(this);  // MainActivity에서 직접 생성
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식을 선택한 이유는 단순했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;&quot;static으로 선언하면 앱 전역에서 같은 인스턴스를 공유할 수 있겠지?&quot;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 &lt;b&gt;우리가 원했던 대로 동작했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;한 번 생성된 DBHelper를 여러 Fragment에서 import하여 그대로 사용할 수 있었음.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만...!  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식이 &lt;b&gt;생각보다 많은 문제를 내포하고 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  ViewModel에 DBHelper를 주입하며 발견한 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서 MVVM 패턴을 적용하면서 &lt;b&gt;ViewModel에서 DBHelper를 주입&lt;/b&gt;하는 코드가 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 &lt;b&gt;이 과정에서 충격적인(?) 사실을 발견했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;DBHelper를 새로 생성해서 넣을 수 있었다!&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 우리가 &lt;b&gt;의도했던 &quot;DBHelper는 하나만 있어야 한다&quot;는 규칙이 깨질 가능성이 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이 방식의 &lt;b&gt;구체적인 문제점&lt;/b&gt;을 살펴보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;❌ 기존 방식의 문제점 (MainActivity에서 관리하는 방식)&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1️⃣ MainActivity에 대한 강한 의존성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 코드에서는 &lt;b&gt;DBHelper를 사용하려면 반드시 MainActivity를 import해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;MainActivity가 없으면 DBHelper를 독립적으로 사용할 수 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;예제 코드 (Fragment에서 DBHelper 사용)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class SomeFragment extends Fragment {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // MainActivity를 통해서만 DBHelper를 참조할 수 있음
        DBHelper dbHelper = MainActivity.mDBHelper;
        dbHelper.insertData(...);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  참고 구조 이미지 ▼&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2726&quot; data-origin-height=&quot;1126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cn9rYa/btsMAV4Nqyq/C2bYsloKJDCkvAq1bVOTR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cn9rYa/btsMAV4Nqyq/C2bYsloKJDCkvAq1bVOTR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cn9rYa/btsMAV4Nqyq/C2bYsloKJDCkvAq1bVOTR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcn9rYa%2FbtsMAV4Nqyq%2FC2bYsloKJDCkvAq1bVOTR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2726&quot; height=&quot;1126&quot; data-origin-width=&quot;2726&quot; data-origin-height=&quot;1126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오르다 다이어리 레거시 리팩토링 전 모든 화면은 Activity였고 MainActivity에서 DBHelper를 import해서 사용했다. 그리고 리팩토링하여 Fragment로 바뀐 지금도 DBHelper를 MainActivity에서 import해서 사용하고 있는 상태인 것이다. (이전 이야기는 이전 포스팅을 참고하길 바란다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;확장성을 고려하여 넓게 봤을 때의 문제점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;독립적인 모듈(예: ViewModel)에서 DBHelper를 사용하려면 아래 그림처럼 &lt;b&gt;MainActivity를 import해야 함&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1986&quot; data-origin-height=&quot;946&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l6Ef9/btsMz6eOB0f/Ffp1eItEfnUQEWoGpqnG5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l6Ef9/btsMz6eOB0f/Ffp1eItEfnUQEWoGpqnG5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l6Ef9/btsMz6eOB0f/Ffp1eItEfnUQEWoGpqnG5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl6Ef9%2FbtsMz6eOB0f%2FFfp1eItEfnUQEWoGpqnG5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1986&quot; height=&quot;946&quot; data-origin-width=&quot;1986&quot; data-origin-height=&quot;946&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;테스트 코드 작성이 어려움&lt;/b&gt; &amp;rarr; MainActivity가 실행되지 않으면 DBHelper를 사용할 수 없음.&lt;/li&gt;
&lt;li&gt;MainActivity가 종료되면 &lt;b&gt;DBHelper도 사라질 위험이 있음&lt;/b&gt;.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래의 그림처럼 mDBHelper를 가진 Activity가 destory된다면 이를 import 해서 쓰던 A, B, C는 오류가 발생할 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2010&quot; data-origin-height=&quot;982&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clf07z/btsMAGz5PxS/taCkfVDpA6KULaN8OZPdz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clf07z/btsMAGz5PxS/taCkfVDpA6KULaN8OZPdz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clf07z/btsMAGz5PxS/taCkfVDpA6KULaN8OZPdz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fclf07z%2FbtsMAGz5PxS%2FtaCkfVDpA6KULaN8OZPdz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2010&quot; height=&quot;982&quot; data-origin-width=&quot;2010&quot; data-origin-height=&quot;982&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2️⃣ DBHelper가 여러 개 생성될 가능성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DBHelper는 &lt;b&gt;오직 하나만 유지되어야 한다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 기존 방식에서는 &lt;b&gt;다른 사람이 DBHelper를 &lt;code&gt;new DBHelper(context)&lt;/code&gt;로 직접 생성할 수도 있는 구조&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;예제 코드 (ViewModel에서 DBHelper를 또 생성한 경우)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class DiaryViewModel extends ViewModel {
    private DBHelper dbHelper;

    public DiaryViewModel(Application application) {
        super(application);
        dbHelper = new DBHelper(application);  // 새로운 DBHelper 인스턴스 생성!
    }

    public void insertDiary(...) {
        dbHelper.insertData(...);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;문제점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 DBHelper를 써야 하는데, &lt;b&gt;새로운 인스턴스가 여러 개 생성될 가능성&lt;/b&gt;이 있다.&lt;/li&gt;
&lt;li&gt;메모리 낭비가 발생할 수 있음.&lt;/li&gt;
&lt;li&gt;동일한 DB에 여러 개의 객체가 접근하면 &lt;b&gt;데이터 동기화 문제가 발생할 가능성&lt;/b&gt;이 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;실제 발생한 상황&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래의 그림처럼 A, B는 Activity의 mDBHelper를 import하고 쓰고 있었는데, 어떤 사람이 mDBHelper를 C에서 새로 만들어서 DB에 접근한다고 생각하면 된다.&lt;/li&gt;
&lt;li&gt;그리고 실제로 우측과 같이 수정 도중에 발견했는데, 팀원이 작성한 코드 중 일부에서 DBHelper를 생성하여 사용하고 있는 것이 확인되었다&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2936&quot; data-origin-height=&quot;1300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIElEZ/btsMzWJ9tgF/DQ6DOsXc4MyuBBAemW4kG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIElEZ/btsMzWJ9tgF/DQ6DOsXc4MyuBBAemW4kG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIElEZ/btsMzWJ9tgF/DQ6DOsXc4MyuBBAemW4kG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIElEZ%2FbtsMzWJ9tgF%2FDQ6DOsXc4MyuBBAemW4kG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2936&quot; height=&quot;1300&quot; data-origin-width=&quot;2936&quot; data-origin-height=&quot;1300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ 해결 방법: DBHelper를 싱글톤 패턴으로 변경!&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 DBHelper를 &lt;code&gt;싱글톤 패턴(Singleton Pattern)&lt;/code&gt;으로 변경하여 &lt;b&gt;하나의 인스턴스만 유지하고, 어디서든 같은 DBHelper를 사용할 수 있도록 개선&lt;/b&gt;하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  잠깐, 싱글톤(Singleton) 패턴이란?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;하나의 클래스에 대해 오직 하나의 객체만 생성되도록 보장하는 디자인 패턴&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모든 코드에서 같은 인스턴스를 공유하며 사용&lt;/b&gt;하므로, &lt;b&gt;메모리 절약 &amp;amp; 데이터 일관성 유지&lt;/b&gt;가 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DBHelper 같은 객체는 여러 개 만들 필요 없이, 앱 전역에서 하나만 유지하면 되므로 싱글톤 패턴이 적합&lt;/b&gt;하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;수정 후 코드 (싱글톤 적용)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class DBHelper extends SQLiteOpenHelper {
    private static volatile DBHelper instance;  

    private DBHelper(Context context) {
        super(context.getApplicationContext(), DB_NAME, null, DB_VERSION);
    }

    public static DBHelper getInstance(Context context) {
        if (instance == null) {  // 첫 번째 체크 (동기화 불필요)
            synchronized (DBHelper.class) {
                if (instance == null) {  // 두 번째 체크 (동기화 블록 내 실행)
                    instance = new DBHelper(context);
                }
            }
        }
        return instance;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  왜 더블 체크 락킹(Double-Checked Locking)을 사용했을까?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  현재 오르다 다이어리는 멀티 스레드가 아닌데?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;synchronized&lt;/code&gt; 키워드를 메서드 전체에 적용하려 했지만, 현재 오르다 다이어리는 따로 멀티 스레드를 만들어서 처리하고 있는 작업이 없기 때문에 동기화 작업이 매번 필요하지 않다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public static synchronized DBHelper getInstance(Context context) {
    if (instance == null) {
        instance = new DBHelper(context);
    }
    return instance;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;code&gt;synchronized&lt;/code&gt; 키워드를 메서드 전체에 적용하면 &lt;b&gt;매번 getInstance()를 호출할 때마다 동기화가 발생하여 성능 저하 가능성이 있다고 판단했다&lt;/b&gt;.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  그렇지만 확장성을 고려한다면?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 추후 확장성을 고려했을 때 추가 스레드를 만들어 처리한다고 하면 synchronized가 필요하게 될 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  고민 해결!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 더블 체크 락킹을 통해 현재 상황에서 불필요한 synchronized 진입을 하지 않게 하면서 추가 스레드가 있을 때 동기화 처리는 되는 방식으로 구현했다. &lt;b&gt;더블 체크 락킹&lt;/b&gt;을 적용하여 불필요한 동기화를 최소화하고 성능을 최적화했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;더블 체크 락킹의 장점&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;이미 인스턴스가 있으면 동기화 블록에 진입하지 않고 바로 반환하여 성능 최적화&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;멀티스레드 환경에서도 인스턴스가 여러 개 생성되지 않도록 보장&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;volatile 키워드 사용으로 CPU 캐시 일관성 유지&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  잠깐, synchronized 키워드?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;synchronized&lt;/code&gt;는 &lt;b&gt;멀티스레드 환경에서 한 번에 하나의 스레드만 특정 코드 블록을 실행하도록 보장&lt;/b&gt;하는 키워드이다.&lt;/li&gt;
&lt;li&gt;즉, &lt;b&gt;공유 자원(예: DBHelper 인스턴스)에 여러 스레드가 동시에 접근하는 것을 막고, 데이터 정합성을 보장&lt;/b&gt;한다. JVM 내부적으로 모니터 락을 요청하는 등의 작업이 이루어진다. 스레드가 락을 얻고 반환한느 과정이 포함된다는 것이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;싱글톤 패턴에서 &lt;code&gt;synchronized&lt;/code&gt;를 사용하는 이유?&lt;/b&gt;&amp;rarr; 여러 개의 스레드가 동시에 &lt;code&gt;getInstance()&lt;/code&gt;를 호출할 경우, &lt;b&gt;여러 개의 객체가 생성되는 문제를 방지하기 위해&lt;/b&gt; 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  잠깐, volatile 키워드?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;volatile&lt;/code&gt; 키워드는 &lt;b&gt;CPU 캐시와 메인 메모리 간의 일관성을 보장하는 키워드&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;멀티스레드 환경에서 &lt;b&gt;한 스레드가 변수 값을 변경하면, 다른 스레드에서도 즉시 변경된 값을 볼 수 있도록 보장&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;싱글톤 패턴에서 인스턴스 변수를 &lt;code&gt;volatile&lt;/code&gt;로 선언하는 이유?&lt;/b&gt;&amp;rarr; &lt;b&gt;더블 체크 락킹(Double-Checked Locking)에서 CPU 캐시 최적화로 인해 발생하는 인스턴스 초기화 순서 문제를 방지하기 위함&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;싱글톤 적용 후 개선된 점&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;height: 181px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; width: 195px;&quot;&gt;&lt;b&gt; 문제 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 250px;&quot;&gt;기존 코드 (MainActivity에서 관리)&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 407px;&quot;&gt;개선 코드 (싱글톤 적용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px; width: 195px;&quot;&gt;&lt;b&gt;MainActivity 의존성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 250px;&quot;&gt;&lt;code&gt;MainActivity.mDBHelper&lt;/code&gt; 필요&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 407px;&quot;&gt;❌ &lt;code&gt;MainActivity&lt;/code&gt; 없이 &lt;code&gt;DBHelper.getInstance(context)&lt;/code&gt; 사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px; width: 195px;&quot;&gt;&lt;b&gt;여러 개의 인스턴스 생성 가능성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 250px;&quot;&gt;가능 (new DBHelper() 가능)&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 407px;&quot;&gt;❌ &lt;b&gt;하나의 인스턴스만 존재&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; width: 195px;&quot;&gt;&lt;b&gt;테스트 코드 작성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 250px;&quot;&gt;MainActivity 없으면 불가능&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 407px;&quot;&gt;✅ &lt;b&gt;MainActivity 없이 독립적으로 사용 가능&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px; width: 195px;&quot;&gt;&lt;b&gt;메모리 낭비&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 250px;&quot;&gt;여러 개의 DBHelper 인스턴스 생성 가능&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 407px;&quot;&gt;✅ &lt;b&gt;싱글톤 적용으로 메모리 절약&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  결론&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 코드에서는 &lt;b&gt;DBHelper를 MainActivity에서 관리하며 static으로 선언&lt;/b&gt;, 모든 곳에서 &lt;code&gt;MainActivity.mDBHelper&lt;/code&gt;를 사용해야 했다.&lt;/li&gt;
&lt;li&gt;이로 인해 &lt;b&gt;의존성이 강해지고&lt;/b&gt;, &lt;b&gt;여러 개의 DBHelper 인스턴스가 생성되는 등의 문제&lt;/b&gt;가 발생했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;싱글톤 패턴을 적용하여&lt;/b&gt; &lt;code&gt;DBHelper.getInstance(context)&lt;/code&gt; 방식으로 변경함으로써 &lt;b&gt;의존성을 줄이고, 인스턴스를 하나만 유지하도록 개선&lt;/b&gt;했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✍️ 현재 구조 도식화&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ8Lh8/btsMyGafK5e/w5EAnQ9sSJ9h4EKKKCPqx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ8Lh8/btsMyGafK5e/w5EAnQ9sSJ9h4EKKKCPqx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ8Lh8/btsMyGafK5e/w5EAnQ9sSJ9h4EKKKCPqx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ8Lh8%2FbtsMyGafK5e%2Fw5EAnQ9sSJ9h4EKKKCPqx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;596&quot; height=&quot;358&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;DBHelper를 싱글톤 패턴으로 변경하여 MainActivity 의존성을 제거하고, 어디서든 같은 인스턴스를 사용 가능하도록 개선&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;더블 체크 락킹을 적용하여 불필요한 동기화 호출을 최소화하고 성능을 최적화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;이제 DBHelper가 중복 생성되지 않으며, 모든 코드에서 &lt;code&gt;DBHelper.getInstance(context)&lt;/code&gt;를 통해 하나의 객체만 유지됨&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  참고: Kotlin에서 싱글톤 패턴 적용하는 방법&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 오르다 다이어리는 Java로 작성되어 리팩토링도 Java 코드 그대로 수정하고 있다. 최근 진행했던 Nature Album 프로젝트는 Kotlin 기반이었는데, Kotlin에서의 싱글톤 패턴 구현 방법도 간단히 적어두고자 한다. Kotlin에서는 object를 활용하면 기본적으로 안전한 싱글톤을 쉽게 구현할 수 있으며, 필요한 경우 companion object와 lazy 초기화를 활용하면 된다. companion object는 주로 java랑 호환을 중요시 한다면 사용하고, Kotlin에서는 object를 사용하는 것을 권장한다.&lt;/li&gt;
&lt;li&gt;object 키워드는 클래스를 인스턴스화하지 않고 즉시 싱글톤 객체를 생성한다. JVM 레벨에서 자동으로 스레드 안전(Thread-Safe)하게 관리되므로, synchronized 또는 volatile을 사용할 필요가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☺️ &lt;b&gt;후기&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3년 전, 우리는 DBHelper를 하나만 만들고 공유해서 쓰고 싶었다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그런데 시간이 지나고 보니, 우리가 했던 고민이 싱글톤 패턴과 맞닿아 있었던 것 같다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그리고 이번 리팩토링을 통해 왜 싱글톤 패턴이 필요한지, 어떻게 동작하는지를 확실하게 이해할 수 있었다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;싱글톤 패턴을 확실하게 알고 넘어갈 수 있는 리팩토링이었다.&lt;/li&gt;
&lt;li&gt;과거 3년 전 그때는 싱글톤 패턴도 몰랐는데 우리가 하나의 객체만 만들고 접근해서 사용하려고 했던 것이 싱글톤 패턴과 유사한 것 같아 기분이 묘하다. 그러면서 동시에 왜 싱글톤 패턴이 만들어졌고 이를 위해 멀티 스레드가 동시에 만들려고 했을 때의 처리 등도 고려해야 한다는 것을 확실하게 알게 되었다.&lt;/li&gt;
&lt;li&gt;Kotlin 기반 프로젝트를 진행할 때는 object를 사용하다 보니 Thread-Safe까지는 직접 고려할 필요가 없었는데 이번 기회에 고려할 수 있게 되어서 좋았다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Android/오르다 다이어리</category>
      <category>Android</category>
      <category>java</category>
      <category>kotlin</category>
      <category>Singletonpattern</category>
      <category>SQLite</category>
      <category>동기화</category>
      <category>레거시코드</category>
      <category>리팩토링</category>
      <category>멀티스레드</category>
      <category>싱글톤패턴</category>
      <author>yujinius</author>
      <guid isPermaLink="true">https://yujinius45.tistory.com/161</guid>
      <comments>https://yujinius45.tistory.com/161#entry161comment</comments>
      <pubDate>Sat, 1 Mar 2025 23:52:55 +0900</pubDate>
    </item>
    <item>
      <title>[Android/오르다 다이어리] 레거시 리팩토링 05 - Configuration Change 대응 (ViewModel 적용)</title>
      <link>https://yujinius45.tistory.com/160</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; [Android/오르다 다이어리] 레거시 리팩토링 05 - Configuration Change 대응 (ViewModel 적용) &lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글: &lt;a href=&quot;https://yujinius45.tistory.com/159&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yujinius45.tistory.com/159&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740822950013&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android/오르다 다이어리] 레거시 리팩토링 04 - Fragment 마이그레이션&quot; data-og-description=&quot;[Android/오르다 다이어리] 레거시 리팩토링 04 - Fragment 마이그레이션지난 글: https://yujinius45.tistory.com/158&amp;nbsp;[Android/오르다 다이어리] 레거시 리팩토링 03 - Navigation Component 적용[Android/오르다 다이어리]&quot; data-og-host=&quot;yujinius45.tistory.com&quot; data-og-source-url=&quot;https://yujinius45.tistory.com/159&quot; data-og-url=&quot;https://yujinius45.tistory.com/159&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cqYG3u/hyYjI1EE0e/H59OMQRUoEbVsanLbjWZ80/img.png?width=800&amp;amp;height=807&amp;amp;face=0_0_800_807,https://scrap.kakaocdn.net/dn/bBYVJG/hyYmYPhsea/AT2g0eK659ooVeYtKRX0EK/img.png?width=800&amp;amp;height=807&amp;amp;face=0_0_800_807&quot;&gt;&lt;a href=&quot;https://yujinius45.tistory.com/159&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yujinius45.tistory.com/159&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cqYG3u/hyYjI1EE0e/H59OMQRUoEbVsanLbjWZ80/img.png?width=800&amp;amp;height=807&amp;amp;face=0_0_800_807,https://scrap.kakaocdn.net/dn/bBYVJG/hyYmYPhsea/AT2g0eK659ooVeYtKRX0EK/img.png?width=800&amp;amp;height=807&amp;amp;face=0_0_800_807');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 04 - Fragment 마이그레이션&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 04 - Fragment 마이그레이션지난 글: https://yujinius45.tistory.com/158&amp;nbsp;[Android/오르다 다이어리] 레거시 리팩토링 03 - Navigation Component 적용[Android/오르다 다이어리]&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yujinius45.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난번에 &lt;b&gt;Activity에서 Fragment로 전환하는 마이그레이션 작업&lt;/b&gt;을 진행했다. 이를 통해 화면 이동이 불편했던 문제를 해결하고, 네비게이션 바를 추가하여 &lt;b&gt;더 나은 UX를 제공할 수 있었다&lt;/b&gt;. 하지만 이 과정에서 새로운 문제들이 발견되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;Fragment 마이그레이션 후 발견한 문제점(일기 작성 페이지 Configuration change 대응 문제 등)과 이를 해결하기 위해 ViewModel을 적용한 과정&lt;/b&gt;을 정리해보고자 한다.&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✨ Fragment 마이그레이션 후 느낀 점 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 리팩토링 작업을 통해 여러 가지 이슈를 발견했다. 그중에서도 가장 크게 불편함으로 느낀점은 &lt;b&gt;UI와 로직이 분리되지 않은 점&lt;/b&gt;과 &lt;b&gt;DB 관련 버그가 존재한 점&lt;/b&gt;이었다. 각각의 문제를 분석하고, 개선 방안을 찾아보았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. UI와 로직이 분리되지 않아 관련 수정에서 시간이 오래 걸림(유지보수성 떨어짐)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Activity 기반 코드에서는 &lt;b&gt;UI와 데이터 처리 로직이 뒤섞여 있었기 때문에 Fragment로 이동할 때 많은 코드 수정이 필요&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  UI를 Fragment로 옮기면서, 데이터 로직도 함께 이동해야 해서 &lt;b&gt;불필요한 코드 이동이 많았고, 시간이 오래 걸려 유지보수성이 낮다고 느낌&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  만약 UI와 로직을 분리했다면 &lt;b&gt;UI만 이동하면 되므로 변경 작업이 훨씬 간결했을 것&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  오르다 다이어리 같은 경우는 해당되지 않지만, 더 큰 규모의 프로젝트에서 여러 view를 재사용한다면 View와 로직이 분리되지 않으면 &lt;b&gt;재사용성이 떨어지고, 다른 곳에서 동일한 로직을 사용하고 싶을 때 중복 코드가 발생&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 이를 해결하기 위해 &lt;b&gt;ViewModel을 도입하여 UI와 로직을 분리&lt;/b&gt;하기로 결정했다. 현재는 &lt;b&gt;Configuration Change 대응이 필요한 일기 작성 페이지에만 ViewModel을 적용&lt;/b&gt;하고, 다른 페이지들은 기존 방식으로 상태가 유지되므로 그대로 두었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. DB 관련 SQL 쿼리 문제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 SQLite 코드에서 &lt;b&gt;UPDATE가 날짜(&lt;code&gt;writeDate&lt;/code&gt;)를 기준으로 이루어지는 문제&lt;/b&gt;가 있었다. 이로 인해 데이터가 꼬이는 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  해결책: &lt;b&gt;ID를 기준으로 업데이트하도록 수정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  하지만 &lt;b&gt;SQL 직접 작성 방식이 유지보수하기 어려웠음&lt;/b&gt; (최근에 진행한 프로젝트에서는 RoomDB를 사용했는데, 이를 다시 보니 가독성이 많이 떨어지는 것이 느껴짐)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ &lt;b&gt;SQLite 유지 vs RoomDB 마이그레이션 여부를 검토하기 위해 이후 테스트 코드 작성&lt;/b&gt;을 진행해볼지 고려중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다시 본론으로 돌아와서, 오늘 진행할 내용을 아래에 정리하고자 한다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난번 Fragment 전환 과정에서 일기장 작성 페이지의 Configuration Change 대응이 필요하다는 걸 알게 되었고, 이를 해결하기 위해 이번 리팩토링을 진행했다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ Configuration Change 문제 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오르다 다이어리&lt;/b&gt;는 Activity 기반일 때부터 &lt;b&gt;Configuration Change(화면 회전 등)에 어느 정도 대응이 되어 있었다.&lt;/b&gt; (과거의 우리 팀 기특하다!  )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;그 이유는?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Activity 구조에서는 &lt;b&gt;onCreate()가 실행될 때마다 DB에서 데이터를 다시 불러오는 방식&lt;/b&gt;을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 따라서 Fragment에서도 &lt;code&gt;onViewCreated()&lt;/code&gt;에서 &lt;b&gt;DB를 다시 로드하여 데이터가 대부분 유지됨&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만! 예외적인 경우가 있었는데, &lt;b&gt;일기 쓰기 페이지에서 이미지 상태가 유지되지 않는 문제&lt;/b&gt;가 발생했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  일기 쓰기 페이지에서 이미지 상태 유지 문제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▼ Configuration change 대응 안되는 모습 (사진 부분 참고)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;[오르다 다이어리] 03 Configuration change 대응 안됨.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/70Lmd/btsMz51cYst/PKKv61rhuII0RkMaVQs8Tk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/70Lmd/btsMz51cYst/PKKv61rhuII0RkMaVQs8Tk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/70Lmd/btsMz51cYst/PKKv61rhuII0RkMaVQs8Tk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/70Lmd/btsMz51cYst/PKKv61rhuII0RkMaVQs8Tk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;254&quot; height=&quot;544&quot; data-filename=&quot;[오르다 다이어리] 03 Configuration change 대응 안됨.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 일기 작성 도중 &lt;b&gt;이미지를 추가하고 화면을 회전, 다크 모드 변경 등을&lt;/b&gt; 하면 이미지가 사라짐&lt;/li&gt;
&lt;li&gt;기존 방식에서는 DB에서 다시 불러오기 때문에 유지됨, 하지만 &lt;b&gt;일기 작성 도중의 데이터(입력 내용, 이미지)는 DB에 저장되지 않음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;즉, &lt;b&gt;Configuration Change가 발생하면 Fragment가 재생성되면서 &lt;code&gt;imageUri&lt;/code&gt;가 초기화되는 문제가 발생&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 이를 해결하기 위해 &lt;b&gt;ViewModel을 도입하여 데이터 상태를 유지하는 방식으로 개선&lt;/b&gt;하기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Configuration Change 해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Configuration Change(예: 화면 회전, 다크 모드 변경 등) 발생 시 데이터를 유지하는 방법에는 &lt;b&gt;두 가지 방법&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  &lt;code&gt;onSaveInstanceState()&lt;/code&gt; 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✔️ 원리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;onSaveInstanceState()&lt;/code&gt;는 &lt;b&gt;Activity/Fragment가 소멸될 때 데이터를 Bundle에 저장&lt;/b&gt;하고, 이후 &lt;b&gt;Bundle을 통해 데이터를 복원하는 방식&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;간단한 UI 상태(텍스트, 숫자, Boolean 등)를 저장하는 용도로 사용됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❌ 단점&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/topic/libraries/architecture/saving-states#viewmodel&quot;&gt;https://developer.android.com/topic/libraries/architecture/saving-states#viewmodel&lt;/a&gt;&lt;br /&gt;Don't use saved instance state to store large amounts of data, such as bitmaps, nor complex data structures that require lengthy serialization or deserialization. Instead, store only primitive types and simple, small objects such as String.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data limitations : only for primitive types and simple, small objects such as String&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;크기 제한이 있어서 &lt;b&gt;Bitmap, Uri 같은 대용량 데이터 저장 불가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Read/write time: slow (requires serialization/deserialization)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가 많아질수록 &lt;b&gt;Bundle을 파싱하는 과정이 성능 저하를 일으킬 수 있음.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ &lt;b&gt;텍스트, 체크박스 상태 등 간단한 UI 상태 저장에는 적합하지만, 이미지 상태 유지에는 적절하지 않음. 물론 uri만 저장시키면 되지만 일기 저장 로직도 분리할 예정이기에 viewmodel 선택.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  ViewModel 활용 (선택한 방법)&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/topic/libraries/architecture/saving-states#viewmodel&quot;&gt;https://developer.android.com/topic/libraries/architecture/saving-states#viewmodel&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Use ViewModel to handle configuration changes&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel is ideal for storing and managing UI-related data while the user is actively using the application.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✔️ 원리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ViewModel은 Configuration Change 시에도 유지되는 객체&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Fragment가 소멸되더라도, &lt;b&gt;Activity가 살아 있는 동안 ViewModel은 유지됨&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;LiveData를 함께 사용하면 &lt;b&gt;자동으로 UI 업데이트 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ 장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Configuration Change 후에도 자동으로 데이터 유지됨&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fragment가 여러 번 생성되더라도 같은 ViewModel을 공유 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LiveData와 함께 사용하면 UI 자동 갱신 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ &lt;b&gt;이미지 상태 유지, UI와 로직 분리를 위해 ViewModel을 활용하기로 결정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☺️ 적용 결과 및 후기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Configuration Change 대응 강화&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면 회전, 다크 모드 변경 후에도 입력 데이터와 이미지 상태가 유지됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. UI와 비즈니스 로직 분리&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI는 Fragment에서 관리하고, 데이터 처리는 ViewModel에서 수행하여 코드 구조가 더 명확해짐&lt;/li&gt;
&lt;li&gt;아래 이미지와 같이 UI의 데이터를 가져와 유효값인지 체크하고 일기장 저장을 하던 로직을 ViewModel로 이전하여 UI는 저장 됐는지만 체크하고 토스트 메세지 출력.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3638&quot; data-origin-height=&quot;1662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W9TBA/btsMzOSUFiS/pwbksT2B16KfQPUY3BSZqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W9TBA/btsMzOSUFiS/pwbksT2B16KfQPUY3BSZqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W9TBA/btsMzOSUFiS/pwbksT2B16KfQPUY3BSZqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW9TBA%2FbtsMzOSUFiS%2FpwbksT2B16KfQPUY3BSZqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3638&quot; height=&quot;1662&quot; data-origin-width=&quot;3638&quot; data-origin-height=&quot;1662&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. DataBinding 및 LiveData 활용&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;DataBinding&lt;/code&gt;을 적용하여 UI와 ViewModel 데이터를 직접 연결&lt;/li&gt;
&lt;li&gt;데이터 변경 시 UI에서 &lt;code&gt;setText()&lt;/code&gt; 등을 명시적으로 호출할 필요 없어서 편리했음.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. MVVM 패턴 적용 후 개선된 점&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;setText() 등의 과정이 없어서 코드가 깔끔해짐.&lt;/li&gt;
&lt;li&gt;LiveData를 활용하여 UI가 자동으로 업데이트되도록 구성&lt;/li&gt;
&lt;li&gt;하지만 현재 작업에서는 ViewModel이 단순 데이터 저장소 역할만 하고 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;➡️ 현재는 &lt;/span&gt;&lt;b&gt;일기 작성 페이지에서만 ViewModel을 적용&lt;/b&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;했고, 다른 페이지들은 기존 방식으로도 상태가 유지되므로 그대로 두었다. 추후 더 복잡한 기능이 추가될 경우 ViewModel 활용도를 높일 계획이다.&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 다크 모드로 변경해도 작성하던 것 사라지지 않는다!!&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;▼ Configuration change 대응 되는 모습&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;[오르다 다이어리] 03 Configuration change 대응.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YHpBZ/btsMAkqoKOz/6oPeFZpKKN7aNLv6xkU5q0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YHpBZ/btsMAkqoKOz/6oPeFZpKKN7aNLv6xkU5q0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YHpBZ/btsMAkqoKOz/6oPeFZpKKN7aNLv6xkU5q0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/YHpBZ/btsMAkqoKOz/6oPeFZpKKN7aNLv6xkU5q0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;336&quot; height=&quot;720&quot; data-filename=&quot;[오르다 다이어리] 03 Configuration change 대응.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Android/오르다 다이어리</category>
      <category>Android</category>
      <category>Android 개발</category>
      <category>configuration change</category>
      <category>darkmode</category>
      <category>DataBinding</category>
      <category>MVVM</category>
      <category>ViewModel</category>
      <category>레거시 리팩토링</category>
      <author>yujinius</author>
      <guid isPermaLink="true">https://yujinius45.tistory.com/160</guid>
      <comments>https://yujinius45.tistory.com/160#entry160comment</comments>
      <pubDate>Sat, 1 Mar 2025 19:12:53 +0900</pubDate>
    </item>
    <item>
      <title>[Android/오르다 다이어리] 레거시 리팩토링 04 - Fragment 마이그레이션</title>
      <link>https://yujinius45.tistory.com/159</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 04 - Fragment 마이그레이션&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지난 글: &lt;a href=&quot;https://yujinius45.tistory.com/158&quot;&gt;https://yujinius45.tistory.com/158&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1740408166157&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android/오르다 다이어리] 레거시 리팩토링 03 - Navigation Component 적용&quot; data-og-description=&quot;[Android/오르다 다이어리] 레거시 리팩토링 03 - Navigation Component 적용이전 글: https://yujinius45.tistory.com/156&amp;nbsp;[Android/오르다 다이어리] 레거시 리팩토링 02 - targetSdk 34 적용 이슈와 해결보호되어 있는 글&quot; data-og-host=&quot;yujinius45.tistory.com&quot; data-og-source-url=&quot;https://yujinius45.tistory.com/158&quot; data-og-url=&quot;https://yujinius45.tistory.com/158&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bvgTms/hyYjjGGCLY/CUotkGjEDozorIdG7eS9Z0/img.png?width=800&amp;amp;height=807&amp;amp;face=0_0_800_807,https://scrap.kakaocdn.net/dn/jbcXD/hyYjyw1Awr/IoERTtqG3EtVLZmrwGLRD1/img.png?width=800&amp;amp;height=807&amp;amp;face=0_0_800_807&quot;&gt;&lt;a href=&quot;https://yujinius45.tistory.com/158&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yujinius45.tistory.com/158&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bvgTms/hyYjjGGCLY/CUotkGjEDozorIdG7eS9Z0/img.png?width=800&amp;amp;height=807&amp;amp;face=0_0_800_807,https://scrap.kakaocdn.net/dn/jbcXD/hyYjyw1Awr/IoERTtqG3EtVLZmrwGLRD1/img.png?width=800&amp;amp;height=807&amp;amp;face=0_0_800_807');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 03 - Navigation Component 적용&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 03 - Navigation Component 적용이전 글: https://yujinius45.tistory.com/156&amp;nbsp;[Android/오르다 다이어리] 레거시 리팩토링 02 - targetSdk 34 적용 이슈와 해결보호되어 있는 글&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yujinius45.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 번 오르다 다이어리 앱의 메뉴 이동 과정이 불편하여 Navigation을 도입해 사용자 경험을 향상시켰다. 그리고 이제 기존 Activity 중심 구조에서 Fragment로 전환하는 작업이 필요해지게 되었다. 오늘은 그 기나 긴 여정을 보내며 남겨둔 기록을 정리해두려고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 패키지 구조 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 Activity 중심 구조에서 Fragment 중심으로 전환하면서, &lt;b&gt;패키지 구조를 보다 체계적으로 정리했다.&lt;/b&gt;&lt;br /&gt;이전에는 모든 UI 관련 클래스가 한 패키지에 섞여 있었다. 이번에 다시 코드를 수정하다 보니, 파일을 찾아 가는 데에 불편함이 있어 &lt;b&gt;각 기능별로 폴더를 나누어 가독성을 높였다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 변경되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;1630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQiUt6/btsMtIGdAwW/BFtkMDnhppwHImK1V4rJTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQiUt6/btsMtIGdAwW/BFtkMDnhppwHImK1V4rJTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQiUt6/btsMtIGdAwW/BFtkMDnhppwHImK1V4rJTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQiUt6%2FbtsMtIGdAwW%2FBFtkMDnhppwHImK1V4rJTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;572&quot; height=&quot;571&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;1630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Activity에서 Fragment로 전환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난번 &lt;b&gt;Navigation Component를 적용&lt;/b&gt;했지만, 아직 모든 Activity를 Fragment로 전환하지 않은 상태였다. 이번 작업에서는 &lt;b&gt;실제 기능을 하는 모든 Activity를 Fragment로 전환하는 작업&lt;/b&gt;을 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정은 예상보다 시간이 오래 걸렸다. Fragment로 바꾸면서 &lt;b&gt;기존 Activity에서 쉽게 처리하던 기능들이 Fragment 환경에서는 다르게 동작&lt;/b&gt;하기 때문에 여러 이슈가 발생했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  Activity &amp;rarr; Fragment 전환 시 주의할 점&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Activity에서는 Intent를 사용하여 데이터를 전달하지만, Fragment에서는 Bundle 또는 Safe Args를 활용해야 한다. &amp;amp; Fragment에서 데이터를 다른 Fragment로 전달할 때 Safe Args를 사용할 수 있다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음에는 Activity 간 데이터 전달에서 &lt;code&gt;Intent&lt;/code&gt;를 사용했었다.&lt;/li&gt;
&lt;li&gt;Fragment에서는 &lt;code&gt;Intent&lt;/code&gt;를 사용할 수 없기 때문에 &lt;code&gt;Bundle&lt;/code&gt;을 활용해야 한다는 것을 알게 되었고, 따라서 &lt;code&gt;putSerializable()&lt;/code&gt;을 사용하여 데이터를 전달하는 방식으로 변경했었다.&lt;/li&gt;
&lt;li&gt;Bundle을 사용하고 null 체크를 확인해주는 작업을 해주었는데, Safe Args를 사용하면 아래와 같은 장점이 있다는 것을 알게 되었다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;타입 안전성 보장&lt;/b&gt; &amp;rarr; &lt;code&gt;OnePageDiary&lt;/code&gt;를 넘기기로 설정했으면, 다른 타입을 넘길 수 없음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컴파일 타임 오류 방지&lt;/b&gt; &amp;rarr; key값을 잘못 쓰거나 타입이 다르면 &lt;b&gt;컴파일 시점에 오류를 잡아줌&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;더 간결한 코드&lt;/b&gt; &amp;rarr; &lt;code&gt;Bundle&lt;/code&gt;을 따로 만들 필요가 없음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;b&gt;Safe Args의 장점&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;그래서 Navigation Component에서 제공하는 Safe Args 기능을 활용하기로 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fragment에서 getSupportFragmentManager()가 아니라 getChildFragmentManager()를 써야 할 때가 있다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fragment 내에서 다른 Fragment를 다룰 때는 &lt;code&gt;getChildFragmentManager()&lt;/code&gt;를 사용해야 했다.&lt;/li&gt;
&lt;li&gt;Fragment 내부에서 또 다른 Fragment(&lt;code&gt;SupportMapFragment&lt;/code&gt;)를 다룰 때, 기존의 &lt;code&gt;getSupportFragmentManager()&lt;/code&gt;를 사용하면 &lt;b&gt;런타임 오류가 발생&lt;/b&gt;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 데이터 전달 방식 변경 (Intent &amp;rarr; Bundle &amp;rarr; Safe Args 적용)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음에는 Fragment 간 데이터 전달을 위해 Bundle을 사용했지만, Navigation Component에서 Safe Args를 제공하는 것을 알게 되었다. Safe Args는 타입 안정성을 보장하고, key 값을 직접 관리할 필요가 없어 코드의 유지보수성이 더 높아지는 장점이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  Activity에서 데이터 전달 방식 (기존 방식)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 &lt;code&gt;Intent&lt;/code&gt;를 사용하여 데이터를 전달했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;기존 코드 (Activity 간 데이터 전달)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Intent intent = new Intent(myViewHolder.itemView.getContext(), DiarySelectedActivity.class);
intent.putExtra(&quot;selectedDiary&quot;, dataModels.get(position));
ContextCompat.startActivity(myViewHolder.itemView.getContext(), intent, null);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Activity에서 데이터 받기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Intent intent = getIntent();
selectedDiary = (OnePageDiary) intent.getSerializableExtra(&quot;selectedDiary&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ &lt;b&gt;문제점&lt;/b&gt;: Activity에서 Fragment로 전환할 때 &lt;code&gt;Intent&lt;/code&gt;를 사용할 수 없기 때문에, 새로운 데이터 전달 방식이 필요했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  Fragment에서 데이터 전달 방식 (Bundle 적용 후 Safe Args로 개선)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;Bundle&lt;/code&gt;을 사용하여 데이터를 전달했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Fragment 간 데이터 전달 (Bundle 방식)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Bundle bundle = new Bundle();
bundle.putSerializable(&quot;selectedDiary&quot;, diary);
Navigation.findNavController(v).navigate(R.id.action_diaryListFragment_to_diarySelectedFragment, bundle);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Fragment에서 데이터 받기 (Bundle 방식)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;if (getArguments() != null) {
    selectedDiary = (OnePageDiary) getArguments().getSerializable(&quot;selectedDiary&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ &lt;b&gt;문제점&lt;/b&gt;: Bundle을 사용하면 key-value 형식으로 데이터를 전달해야 하며, 타입 안전성이 보장되지 않는다. 또한 key 관리도 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  Safe Args 적용 (최종 개선)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Navigation Component에서는 &lt;b&gt;Safe Args&lt;/b&gt;를 제공하여 &lt;b&gt;타입 안전한 데이터 전달&lt;/b&gt;을 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Safe Args 적용 코드 (nav_graph.xml 수정)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;fragment
    android:id=&quot;@+id/diarySelectedFragment&quot;
    android:name=&quot;smu.team3_orda_diary.ui.diary.DiarySelectedFragment&quot;
    android:label=&quot;Diary Selected&quot;&amp;gt;
    &amp;lt;argument
        android:name=&quot;selectedDiary&quot;
        app:argType=&quot;smu.team3_orda_diary.model.OnePageDiary&quot; /&amp;gt;
&amp;lt;/fragment&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Fragment 간 데이터 전달 (Safe Args 적용)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;DiaryListFragmentDirections.ActionDiaryListFragmentToDiarySelectedFragment action =
        DiaryListFragmentDirections.actionDiaryListFragmentToDiarySelectedFragment(diary);
Navigation.findNavController(v).navigate(action);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Fragment에서 데이터 받기 (Safe Args 적용)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;DiarySelectedFragmentArgs args = DiarySelectedFragmentArgs.fromBundle(getArguments());
selectedDiary = args.getSelectedDiary();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ &lt;b&gt;Safe Args 적용 후 개선점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;타입 안전성 보장&lt;/b&gt; (컴파일 시점에서 타입 검증 가능)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Key 값 없이 직관적인 메서드 호출로 데이터 전달 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Nullable / Non-nullable 자동 처리&lt;/b&gt; &amp;rarr; &lt;code&gt;selectedDiary&lt;/code&gt;는 Non-nullable 타입이므로 null 체크가 필요 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ &lt;b&gt;따라서 &lt;code&gt;if (selectedDiary != null)&lt;/code&gt; 체크를 하지 않아도 경고가 발생하지 않음.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. XML 및 리소스 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 &lt;b&gt;하드코딩된 문자열을 &lt;code&gt;strings.xml&lt;/code&gt;로 이동시켰다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;UI 대응을 위해 &lt;b&gt;ConstraintLayout을 적용하여 다양한 화면 크기에 적절히 대응할 수 있도록 변경했다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Fragment 내에서 Fragment 사용 시 주의점 (&lt;code&gt;getChildFragmentManager()&lt;/code&gt; 활용) 관련 추가 사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fragment 내부에서 또 다른 Fragment(&lt;code&gt;SupportMapFragment&lt;/code&gt;)를 다룰 때, 기존의 &lt;code&gt;getSupportFragmentManager()&lt;/code&gt;를 사용하면 &lt;b&gt;런타임 오류가 발생&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  문제 상황&lt;/b&gt;&lt;br /&gt;기존에는 &lt;code&gt;getSupportFragmentManager()&lt;/code&gt;를 사용하여 &lt;code&gt;SupportMapFragment&lt;/code&gt;를 추가하려고 했으나, &lt;b&gt;Fragment 내부에서 Fragment를 추가할 경우 &lt;code&gt;getChildFragmentManager()&lt;/code&gt;를 사용해야 한다는 점을 간과했다.&lt;/b&gt; Activity 내에서 Map이 어떻게 사용되고 있는지를 먼저 체크하지 않은 탓이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;수정 전 (잘못된 코드)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SupportMapFragment mapFragment = new SupportMapFragment();
getSupportFragmentManager().beginTransaction()
        .replace(R.id.map_container, mapFragment)
        .commit(); // ❌ 오류 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;수정 후 (정상 작동 코드)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SupportMapFragment mapFragment = new SupportMapFragment();
getChildFragmentManager().beginTransaction()
        .replace(R.id.map_container, mapFragment)
        .commit(); // ✅ 정상 동작&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ &lt;b&gt;getChildFragmentManager()를 사용해야 하는 이유&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;getSupportFragmentManager()&lt;/code&gt;는 &lt;b&gt;Activity에 속한 Fragment 관리자를 가져오는 것&lt;/b&gt;이기 때문에 &lt;b&gt;Fragment 내부에서 새로운 Fragment를 추가하는 경우에는 사용하면 안 된다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getChildFragmentManager()&lt;/code&gt;를 사용하면 &lt;b&gt;현재 Fragment 내부에서 새로운 Fragment를 관리할 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 가장 어려웠던 작업: 일기장 관련 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fragment로 전환하는 과정에서 &lt;b&gt;가장 어려웠던 부분은 일기장 관련 화면&lt;/b&gt;이었다. 특히 포토피커(Photo Picker)의 동작 방식에 대한 오해가 큰 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;포토피커(Photo Picker)란?&lt;/b&gt;&lt;br /&gt;Android의 &lt;b&gt;Photo Picker API&lt;/b&gt;는 사용자가 갤러리에서 이미지를 선택할 수 있도록 돕는 기능이다.&lt;br /&gt;하지만, &lt;b&gt;선택된 이미지는 임시 URI로 제공되며, 개발자가 직접 이를 저장하거나 사용해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;잘못된 예상&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;처음에는 &lt;b&gt;Photo Picker에서 선택한 이미지가 자동으로 DB에 저장될 것이라고 착각&lt;/b&gt;했다. 기존 Media를 선택하고 저장하는 로직이 이미 있었기에 그 부분만 Photo Picker로 변경하면 저장될 것이라 생각해버린 것이었다. 하지만 &lt;b&gt;실제로는 앱에서 URI를 활용하여 별도로 저장해야 했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;해결 방법&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Photo Picker에서 선택한 이미지의 URI를 DB에 저장하는 방식으로 변경&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;URI가 만료되지 않도록 앱 내부 저장소에 이미지를 복사하여 보관 및 참조하여 해당 이미지 표시 (갤러리에서 삭제되더라도 앱 내부 저장소에 저장된 이미지가 유지되도록 함)&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. &lt;b&gt;할 일 목록(DB) 수정 로직 개선&lt;/b&gt; &lt;b&gt;(버그 수정)&lt;/b&gt;&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  문제 발생 원인&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할 일 목록 수정 기능을 테스트하는 과정에서, &lt;b&gt;같은 날짜에 여러 개의 할 일을 추가한 후 하나를 수정하면 모든 아이템이 동일하게 변경되는 버그를 발견&lt;/b&gt;했다.&lt;br /&gt;이는 기존 &lt;code&gt;updateTodo()&lt;/code&gt;가 &lt;code&gt;WHERE writeDate = '기존 Date'&lt;/code&gt;로 조건을 걸고 수정했기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  해결 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식에서는 &lt;code&gt;WHERE writeDate=?&lt;/code&gt;로 쿼리를 실행하여, 같은 날짜에 존재하는 모든 할 일이 동시에 변경되는 문제가 발생했다. 이를 해결하기 위해 각 할 일 항목이 고유한 ID(&lt;code&gt;id&lt;/code&gt;)를 가지도록 설계하고, &lt;code&gt;WHERE id=?&lt;/code&gt;로 변경하여 특정 아이템만 수정할 수 있도록 개선했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ID 기반 업데이트로 변경&lt;/b&gt; (&lt;code&gt;writeDate&lt;/code&gt; 대신 &lt;code&gt;id&lt;/code&gt; 사용)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존: &lt;code&gt;UPDATE TODOLIST_TB SET title=?, content=?, writeDate=? WHERE writeDate=?&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;변경: &lt;code&gt;UPDATE TODOLIST_TB SET title=?, content=?, writeDate=? WHERE id=?&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;b&gt;각 할 일 항목이 고유한 ID를 기준으로 업데이트되도록 수정&lt;/b&gt;하여, 같은 날짜에 여러 개의 할 일이 있을 경우에도 개별적으로 수정할 수 있게 되었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업을 통해 &lt;b&gt;Activity를 Fragment로 전환하고, Navigation Component와 Safe Args를 활용하는 구조로 변경&lt;/b&gt;했다. Activity에서 사용하던 데이터 전달 방식을 &lt;b&gt;Intent &amp;rarr; Bundle &amp;rarr; Safe Args&lt;/b&gt; 순으로 개선하며, &lt;b&gt;타입 안전성과 유지보수성을 높였다.&lt;/b&gt; 특히 Safe Args를 적용하면서 &lt;b&gt;null 체크가 필요 없는 구조로 변경&lt;/b&gt;되었으며, &lt;b&gt;Fragment 간 데이터 전달이 더욱 명확하고 안전하게 이루어질 수 있도록 개선했다.&lt;/b&gt; 또한, &lt;b&gt;할 일 목록의 데이터 수정 오류를 해결&lt;/b&gt;하여, 같은 날짜의 모든 할 일이 동시에 변경되는 문제를 방지했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, &lt;b&gt;알람 관련 &lt;code&gt;RingingAlarmActivity&lt;/code&gt;는 Fragment로 변경하지 않고 유지&lt;/b&gt;했다. 알람이 울릴 때는 사용자의 인터랙션이 흔들기로 제한되어 &lt;b&gt;독립적인 화면이 필요하며, Navigation Component와의 연관성이 낮아 별도의 Activity로 유지하는 것이 적절했기 때문&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 리팩토링을 통해 &lt;b&gt;코드 구조가 단순해지고, 유지보수성이 향상되었으며, UI/UX 측면에서도 개선된 사용자 경험을 제공&lt;/b&gt;할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▼ 변경된 구조&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4550&quot; data-origin-height=&quot;4590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mfr6P/btsMt9jcpGv/LyhQBkfi3hDRm1Sque854k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mfr6P/btsMt9jcpGv/LyhQBkfi3hDRm1Sque854k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mfr6P/btsMt9jcpGv/LyhQBkfi3hDRm1Sque854k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmfr6P%2FbtsMt9jcpGv%2FLyhQBkfi3hDRm1Sque854k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4550&quot; height=&quot;4590&quot; data-origin-width=&quot;4550&quot; data-origin-height=&quot;4590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 홈 화면을 들렸다가 다른 곳으로 이동하지 않아도 된다!&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;아래 영상과 같이 Navigation을 통해 메뉴를 빠르게 이동할 수 있어서 사용자 경험이 개선되었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;[오르다 다이어리] 02 Fragment 전환 완료.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kZVrX/btsMt6tehiI/hRnQz0ffxp57X3XFtFBSUk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kZVrX/btsMt6tehiI/hRnQz0ffxp57X3XFtFBSUk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kZVrX/btsMt6tehiI/hRnQz0ffxp57X3XFtFBSUk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/kZVrX/btsMt6tehiI/hRnQz0ffxp57X3XFtFBSUk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;336&quot; height=&quot;720&quot; data-filename=&quot;[오르다 다이어리] 02 Fragment 전환 완료.gif&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Android/오르다 다이어리</category>
      <category>activity</category>
      <category>Android</category>
      <category>bundle</category>
      <category>framgent</category>
      <category>Intent</category>
      <category>java</category>
      <category>navigation</category>
      <category>Safe Args</category>
      <category>XML</category>
      <category>리팩토링</category>
      <author>yujinius</author>
      <guid isPermaLink="true">https://yujinius45.tistory.com/159</guid>
      <comments>https://yujinius45.tistory.com/159#entry159comment</comments>
      <pubDate>Mon, 24 Feb 2025 23:56:01 +0900</pubDate>
    </item>
    <item>
      <title>[Android/오르다 다이어리] 레거시 리팩토링 03 - Navigation Component 적용</title>
      <link>https://yujinius45.tistory.com/158</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 03 - Navigation Component 적용&lt;/span&gt;&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글: &lt;a href=&quot;https://yujinius45.tistory.com/156&quot;&gt;https://yujinius45.tistory.com/156&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739978559419&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android/오르다 다이어리] 레거시 리팩토링 02 - targetSdk 34 적용 이슈와 해결&quot; data-og-description=&quot;보호되어 있는 글입니다. 내용을 보시려면 비밀번호를 입력하세요.&quot; data-og-host=&quot;yujinius45.tistory.com&quot; data-og-source-url=&quot;https://yujinius45.tistory.com/156&quot; data-og-url=&quot;https://yujinius45.tistory.com/156&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/m7F21/hyYfZPOFbT/GuWLoqTBk96AJThQVaP4zK/img.jpg?width=439&amp;amp;height=439&amp;amp;face=0_0_439_439&quot;&gt;&lt;a href=&quot;https://yujinius45.tistory.com/156&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yujinius45.tistory.com/156&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/m7F21/hyYfZPOFbT/GuWLoqTBk96AJThQVaP4zK/img.jpg?width=439&amp;amp;height=439&amp;amp;face=0_0_439_439');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 레거시 리팩토링 02 - targetSdk 34 적용 이슈와 해결&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;보호되어 있는 글입니다. 내용을 보시려면 비밀번호를 입력하세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yujinius45.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ SDK 업그레이드 관련 간단 정보 (이전 글 요약)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 &lt;b&gt;오르다 다이어리의 &lt;code&gt;targetSdkVersion&lt;/code&gt;을 업그레이드하면서 권한 설정을 수정&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 필요한 변경 사항을 직접 찾아 적용했는데, 최근 &lt;b&gt;Android Studio에서 제공하는 &lt;code&gt;SDK Upgrade Assistant&lt;/code&gt;라는 기능이 있다는 것을 알게 되었다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ SDK Upgrade Assistant란?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Android Studio의 도구 중 하나로, 버전 업그레이드 시 필요한 변경 사항을 자동으로 분석하고 가이드해준다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;targetSdkVersion&lt;/code&gt;을 변경하면 &lt;b&gt;API 변경점, 권한 정책 변경, 비호환 요소 등을 체크할 수 있어,&lt;/b&gt; 업그레이드 시 놓치기 쉬운 부분을 쉽게 보완할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결론:&lt;/b&gt; 다음 SDK 업그레이드 시에는 이 도구를 적극 활용하면 더 편리할 듯하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▼ 아래처럼 되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3186&quot; data-origin-height=&quot;2360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHob5o/btsMoYnNuNh/prbgFIJz8afnyqnm3Y8y0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHob5o/btsMoYnNuNh/prbgFIJz8afnyqnm3Y8y0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHob5o/btsMoYnNuNh/prbgFIJz8afnyqnm3Y8y0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHob5o%2FbtsMoYnNuNh%2FprbgFIJz8afnyqnm3Y8y0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;757&quot; height=&quot;561&quot; data-origin-width=&quot;3186&quot; data-origin-height=&quot;2360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;SDK 업그레이드는 이미 직접 적용한 상태이므로, 이번 작업에서는 기존 Activity 기반 네비게이션을 Fragment 기반으로 전환하는 과정에 집중한다&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오르다 다이어리&lt;/b&gt;의 레거시 코드를 리팩토링하면서 가장 먼저 해결해야 할 문제로 &lt;b&gt;화면 구성과 데이터 관리 방식 개선을 뽑았다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오르다 다이어리의 기존 화면 구조는 모든 기능이 개별 &lt;code&gt;Activity&lt;/code&gt;로 구성되어 있으며, 화면 간 이동은 &lt;code&gt;Intent&lt;/code&gt;를 통해 이루어졌다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 화면 이동이 직관적이지 않고, Activity 간 전환 비용이 발생하며, UI 상태 관리가 어렵다는 단점이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 &lt;b&gt;하나의 &lt;code&gt;MainActivity&lt;/code&gt;에서 여러 개의 &lt;code&gt;Fragment&lt;/code&gt;를 관리하는 구조&lt;/b&gt;로 변경하고, &lt;b&gt;&lt;code&gt;Navigation Component&lt;/code&gt;와 &lt;code&gt;BottomNavigationView&lt;/code&gt;를 적용하여 Fragment 간 전환을 간편하게 만들고자 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  기존 오르다 다이어리의 화면 구조와 문제점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  기존 화면 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4550&quot; data-origin-height=&quot;4590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tDGGw/btsMn76PROZ/xxDC2r6pQz0Ukm2ef0zoeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tDGGw/btsMn76PROZ/xxDC2r6pQz0Ukm2ef0zoeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tDGGw/btsMn76PROZ/xxDC2r6pQz0Ukm2ef0zoeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtDGGw%2FbtsMn76PROZ%2FxxDC2r6pQz0Ukm2ef0zoeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4550&quot; height=&quot;4590&quot; data-origin-width=&quot;4550&quot; data-origin-height=&quot;4590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  기존 구조의 문제점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;화면 이동의 불편함&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 사용자는 메인 화면에서 버튼을 눌러 특정 화면으로 이동할 수 있지만, 다른 화면으로 이동하려면 &lt;b&gt;항상 백 버튼을 눌러 MainActivity로 돌아와서 다시 다른 버튼을 눌러야 한다&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;즉, 일정 화면에서 일기 화면으로 바로 이동할 수 없고, &lt;b&gt;항상 MainActivity를 거쳐야 하는 구조&lt;/b&gt;다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;UI 상태 관리 부족&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;일부 화면의 UI 상태가 유지되지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Configuration Change(예: 화면 회전) 발생 시 &lt;b&gt;앱이 종료되거나, 데이터가 초기화되는 문제가 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;Activity 개수가 많아 성능 저하 우려&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;각 화면이 독립적인 Activity로 이루어져 있어, 화면 전환 시 메모리 사용량이 증가한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;사용자가 여러 화면을 빠르게 전환하면 &lt;b&gt;불필요한 Activity가 누적되어 앱 성능에 영향을 미칠 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;비효율적인 사용자 경험(UI/UX)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;한 화면에서 다른 화면으로 바로 이동하는 기능이 부족하여, 사용자가 불편함을 느낀다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;네비게이션 바와 같은 직관적인 전환 기능이 없어 &lt;b&gt;앱 사용 경험이 비효율적이다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  관련 영상&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;[오르다 다이어리] Activity로만 이동.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D0Xqz/btsMnOzIADf/mszgiMK5ecLCwSW6tLX5s1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D0Xqz/btsMnOzIADf/mszgiMK5ecLCwSW6tLX5s1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D0Xqz/btsMnOzIADf/mszgiMK5ecLCwSW6tLX5s1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/D0Xqz/btsMnOzIADf/mszgiMK5ecLCwSW6tLX5s1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;250&quot; height=&quot;556&quot; data-filename=&quot;[오르다 다이어리] Activity로만 이동.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1778&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  해결 방법: Single Activity + Multi Fragment 구조로 전환&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 구조의 문제를 해결하기 위해, &lt;b&gt;하단 네비게이션 바(Bottom Navigation Bar)를 도입하여 사용자들이 더욱 직관적으로 화면을 전환할 수 있도록 개선하기로 했다&lt;/b&gt;.&lt;br /&gt;이를 위해 &lt;b&gt;각 Activity를 Fragment로 변경하고, &lt;code&gt;Navigation Component&lt;/code&gt;를 활용하여 Fragment 전환을 관리하도록 구조를 변경하고자 한다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  리팩토링 후 새로운 화면 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4550&quot; data-origin-height=&quot;4590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/erd2Wo/btsMpLnJvnC/q0abDB3tgZNoKneWLvn7K0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/erd2Wo/btsMpLnJvnC/q0abDB3tgZNoKneWLvn7K0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/erd2Wo/btsMpLnJvnC/q0abDB3tgZNoKneWLvn7K0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ferd2Wo%2FbtsMpLnJvnC%2Fq0abDB3tgZNoKneWLvn7K0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4550&quot; height=&quot;4590&quot; data-origin-width=&quot;4550&quot; data-origin-height=&quot;4590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;하단 네비게이션 바 추가 &amp;rarr; 빠른 화면 전환 가능으로 사용자 경험 개선&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;Fragment 전환 방식 사용 &amp;rarr; Activity 생성 부담 감소&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;ViewModel을 활용한 UI 상태 유지 가능 (추후 적용 예정)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  Navigation Component 적용 과정&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1️⃣ &lt;code&gt;nav_graph.xml&lt;/code&gt; 생성 및 Fragment 등록&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Navigation Component&lt;/code&gt;를 사용하기 위해 &lt;b&gt;&lt;code&gt;nav_graph.xml&lt;/code&gt;을 생성하고, 기존 Activity를 Fragment로 등록했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;적용된 코드 (&lt;code&gt;nav_graph.xml&lt;/code&gt;)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;navigation xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    app:startDestination=&quot;@id/homeFragment&quot;&amp;gt;

    &amp;lt;fragment
        android:id=&quot;@+id/homeFragment&quot;
        android:name=&quot;smu.team3_orda_diary.HomeFragment&quot;
        android:label=&quot;Home&quot; /&amp;gt;

    &amp;lt;fragment
        android:id=&quot;@+id/todolistFragment&quot;
        android:name=&quot;smu.team3_orda_diary.ToDoListFragment&quot;
        android:label=&quot;Todolist&quot; /&amp;gt;

    &amp;lt;fragment
        android:id=&quot;@+id/mapMemoFragment&quot;
        android:name=&quot;smu.team3_orda_diary.MapMemoFragment&quot;
        android:label=&quot;Map Memo&quot; /&amp;gt;

    &amp;lt;fragment
        android:id=&quot;@+id/diaryListFragment&quot;
        android:name=&quot;smu.team3_orda_diary.DiaryListFragment&quot;
        android:label=&quot;Diary List&quot; /&amp;gt;

    &amp;lt;fragment
        android:id=&quot;@+id/alarmFragment&quot;
        android:name=&quot;smu.team3_orda_diary.AlarmFragment&quot;
        android:label=&quot;Alarm&quot; /&amp;gt;
&amp;lt;/navigation&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;code&gt;BottomNavigationView&lt;/code&gt;와 연결하여 클릭 시 자동으로 Fragment 이동이 가능하게 만들었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2️⃣ BottomNavigationView 추가 및 Navigation 연동&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 &lt;code&gt;Activity&lt;/code&gt; 간 이동 버튼을 제거하고, &lt;b&gt;&lt;code&gt;BottomNavigationView&lt;/code&gt;를 사용해 Fragment 전환을 자동화했다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메뉴 ID와 Fragment ID를 일치시키는 것이 중요하다.&lt;/b&gt; (초기에 ID 불일치 문제로 오류가 발생했다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  Navigation Component 설정 실수로 발생한 오류 해결기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 &lt;code&gt;BottomNavigationView&lt;/code&gt;와 &lt;code&gt;Navigation Component&lt;/code&gt;를 연동하는 과정에서, &lt;code&gt;bottom_nav_menu.xml&lt;/code&gt;의 메뉴 ID와 &lt;code&gt;nav_graph.xml&lt;/code&gt;의 Fragment ID가 일치하지 않아 &lt;code&gt;NavigationUI&lt;/code&gt;가 목적지를 찾지 못하는 오류가 발생했다. &lt;br /&gt;오류 메시지를 분석한 결과, &lt;code&gt;menu&lt;/code&gt;의 ID(&lt;code&gt;nav_home&lt;/code&gt;, &lt;code&gt;nav_schedule&lt;/code&gt; 등)와 &lt;code&gt;nav_graph.xml&lt;/code&gt;에서 해당 Fragment의 ID(&lt;code&gt;homeFragment&lt;/code&gt;, &lt;code&gt;todolistFragment&lt;/code&gt; 등)가 불일치한 것이 원인이었다. 이를 해결하기 위해 &lt;b&gt;모든 메뉴 ID를 Fragment ID와 동일하게 통일&lt;/b&gt;하였고, 이후 &lt;code&gt;NavigationUI&lt;/code&gt;가 정상적으로 목적지를 찾으며 오류 없이 &lt;code&gt;BottomNavigationView&lt;/code&gt;를 통한 Fragment 전환이 원활하게 이루어졌다. 오랜만에 xml으로 돌아오다 보니 이름 맞추는 것을 망각하고 있었다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1604&quot; data-origin-height=&quot;1708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEmrJQ/btsMpisE8Io/k8Gfnu6OM20zAnnaCDd8G1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEmrJQ/btsMpisE8Io/k8Gfnu6OM20zAnnaCDd8G1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEmrJQ/btsMpisE8Io/k8Gfnu6OM20zAnnaCDd8G1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEmrJQ%2FbtsMpisE8Io%2Fk8Gfnu6OM20zAnnaCDd8G1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;632&quot; height=&quot;1708&quot; data-origin-width=&quot;1604&quot; data-origin-height=&quot;1708&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;▲ 위의 캡쳐처럼 id를 동일하게 설정해야 하는 것! 잊지말자!!!!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;수정된 &lt;code&gt;bottom_nav_menu.xml&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;menu xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&amp;gt;
    &amp;lt;item
        android:id=&quot;@+id/homeFragment&quot;
        android:icon=&quot;@drawable/ic_home_24dp&quot;
        android:title=&quot;홈&quot; /&amp;gt;

    &amp;lt;item
        android:id=&quot;@+id/todolistFragment&quot;
        android:icon=&quot;@drawable/ic_today_24dp&quot;
        android:title=&quot;일정관리&quot; /&amp;gt;

    &amp;lt;item
        android:id=&quot;@+id/mapMemoFragment&quot;
        android:icon=&quot;@drawable/ic_location_on_24dp&quot;
        android:title=&quot;지도 메모&quot; /&amp;gt;

    &amp;lt;item
        android:id=&quot;@+id/diaryListFragment&quot;
        android:icon=&quot;@drawable/ic_feed_24dp&quot;
        android:title=&quot;일기장&quot; /&amp;gt;

    &amp;lt;item
        android:id=&quot;@+id/alarmFragment&quot;
        android:icon=&quot;@drawable/ic_alarm_24dp&quot;
        android:title=&quot;알람&quot; /&amp;gt;
&amp;lt;/menu&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;code&gt;Navigation Component&lt;/code&gt;와 &lt;code&gt;BottomNavigationView&lt;/code&gt;를 연동하여 Fragment 전환을 자동화했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3️⃣ MainActivity 수정 (&lt;code&gt;findNavController()&lt;/code&gt; 오류 해결)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에 &lt;code&gt;findNavController(this, R.id.fragmentContainer)&lt;/code&gt; 호출 시 &lt;b&gt;NavController가 설정되지 않아 &lt;code&gt;IllegalStateException&lt;/code&gt; 오류가 발생했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 &lt;b&gt;&lt;code&gt;NavHostFragment&lt;/code&gt;에서 직접 &lt;code&gt;NavController&lt;/code&gt;를 가져오는 방식으로 수정했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;수정된 &lt;code&gt;MainActivity.java&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
        .findFragmentById(R.id.fragmentContainer);
if (navHostFragment != null) {
    navController = navHostFragment.getNavController();
}
NavigationUI.setupWithNavController(binding.bottomNavigation, navController);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;&lt;code&gt;findNavController()&lt;/code&gt;를 안전하게 호출하도록 변경했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  현재 상태 &amp;amp; 다음 작업&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ &lt;b&gt;Fragment 전환이 정상적으로 동작하는 것까지 확인했다.&lt;/b&gt;  &lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  관련 영상&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;[오르다 다이어리] 01 Navigation 적용.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b56vzm/btsMoGOyrne/8DLAVNGMr66mEolz7dPRo0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b56vzm/btsMoGOyrne/8DLAVNGMr66mEolz7dPRo0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b56vzm/btsMoGOyrne/8DLAVNGMr66mEolz7dPRo0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b56vzm/btsMoGOyrne/8DLAVNGMr66mEolz7dPRo0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;277&quot; height=&quot;616&quot; data-filename=&quot;[오르다 다이어리] 01 Navigation 적용.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1778&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ 현재는 &lt;b&gt;빈 화면 전환만 되는 상태이며, 기존 &lt;code&gt;Activity&lt;/code&gt;의 기능을 각각 Fragment로 이동하는 작업이 필요하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ 이후 &lt;b&gt;각 Fragment에 기존 기능을 옮겨서 완전히 Fragment 기반으로 전환할 예정이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업을 통해 &lt;b&gt;&lt;code&gt;BottomNavigationView + Navigation Component&lt;/code&gt;를 활용하여 Fragment 기반 네비게이션 구조를 적용했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 &lt;code&gt;Activity&lt;/code&gt; 간 이동 방식에서 &lt;code&gt;Fragment&lt;/code&gt; 전환 방식으로 변경하면서 nav_graph 등을 활용해 코드가 깔끔해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  이후 작업에서는 기존 Activity의 UI와 기능을 Fragment로 이동하며, 필요에 따라 ViewModel을 활용한 데이터 관리도 적용할 계획이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;  다음 목표: 기존 Activity의 UI 및 기능을 각각 Fragment로 마이그레이션하기!&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/오르다 다이어리</category>
      <category>Android</category>
      <category>androiddevelopment</category>
      <category>BottomNavigationView</category>
      <category>legacyrefactoring</category>
      <category>navigationcomponent</category>
      <category>navigationgraph</category>
      <category>navigationui</category>
      <category>sdkupgradeassistant</category>
      <category>singleactivityarchitecture</category>
      <category>안드로이드리팩토링</category>
      <author>yujinius</author>
      <guid isPermaLink="true">https://yujinius45.tistory.com/158</guid>
      <comments>https://yujinius45.tistory.com/158#entry158comment</comments>
      <pubDate>Thu, 20 Feb 2025 00:39:22 +0900</pubDate>
    </item>
    <item>
      <title>[Android/오르다 다이어리] 레거시 리팩토링 02 - targetSdk 34 적용 이슈와 해결</title>
      <link>https://yujinius45.tistory.com/156</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;  오르다 다이어리 레거시 리팩토링 02&amp;nbsp;&lt;b&gt;- targetSdk 34 적용 이슈와 해결&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 &lt;b&gt;3년 전 개발한 레거시 프로젝트&lt;/b&gt;를 리팩토링하면서 targetSdkVersion을 &lt;b&gt;32에서 34로 변경&lt;/b&gt;해야 하는 상황에 직면했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HQs0r/btsMe1ygRaD/XjExu8kdHZUgMdkzNGMwhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HQs0r/btsMe1ygRaD/XjExu8kdHZUgMdkzNGMwhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HQs0r/btsMe1ygRaD/XjExu8kdHZUgMdkzNGMwhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHQs0r%2FbtsMe1ygRaD%2FXjExu8kdHZUgMdkzNGMwhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1052&quot; height=&quot;628&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;628&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;왜 Target SDK를 34로 올려야 했을까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 targetSdkVersion 32로 유지하면서 UI 상태 관리 및 아키텍처 리팩토링을 진행하려 했으나, 아래와 같은 고민이 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Google Play에 출시해보고 싶어질 수도 있고&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최신 Android 보안 정책을 반영해야 하며&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다른 라이브러리 사용할 때의 확장성을 고려해야 했기 때문&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;또한, 현재 사용자인 나의 핸드폰이 Anroid 14ㅇ이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 &lt;b&gt;최신 정책을 준수하는 것이 장기적으로 유지보수와 배포를 고려했을 때 더 나은 선택&lt;/b&gt;이라 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 &lt;b&gt;targetSdk를 34로 변경하자마자 Android Studio에서 다양한 권한 정책 변경과 보안 강화로 인해 일부 기능이 정상 동작하지 않음&lt;/b&gt;을 알리는 경고 메시지가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Configuration Change만 안되던 모든 기능들이 삐그덕 거리며 정상 동작하지 않기 시작했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오르다 다이어리는 내 프로젝트 중에서도 &lt;b&gt;가장 다양한 권한을 요청하는 앱&lt;/b&gt;이었기에, 이를 해결하기 위해 SDK 버전별 정책을 하나하나 살펴보는 과정이 필요했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;targetSdkVersion이란?&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;targetSdkVersion은 Android 시스템이 앱을 실행할 때 어떤 버전의 동작 방식을 따를지를 결정하는 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 Android 버전이 출시되면서, 보안과 성능 향상을 위한 변경 사항이 추가되며, targetSdkVersion이 낮으면 최신 기능을 사용할 수 없거나 새로운 정책이 적용되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, 최신 SDK 버전으로 올리면 새로운 보안 정책과 권한 변경 사항을 준수해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;a href=&quot;https://developer.android.com/distribute/best-practices/develop/target-sdk?hl=ko&quot;&gt;공식 문서: Target API Level&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;Target SDK 34 적용 시 발생한 문제들&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ &lt;b&gt;권한 요청 관련 변경 사항&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android 13(API 33) 및 14(API 34)에서는 기존의 READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE 권한이 동작하지 않고, 새로운 &lt;b&gt;미디어 접근 권한&lt;/b&gt;을 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE로 모든 갤러리에 접근 가능했지만,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Android 14(API 34)부터는 사용자가 선택한 사진만 접근할 수 있도록 Photo Picker 사용이 권장된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;API 33 이상에서는 Photo Picker를 사용하고, API 32 이하에서는 기존 방식 유지하도록 했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://support.google.com/googleplay/android-developer/answer/14115180?hl=ko&quot;&gt;https://support.google.com/googleplay/android-developer/answer/14115180?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;결정 사항:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE 대신 &lt;b&gt;Photo Picker 사용&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;API 32 이하는 기존 권한을 유지하도록 maxSdkVersion=&quot;32&quot; 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ &lt;b&gt;알람 설정 (AlarmManager) 이슈&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android 12(API 31) 이상에서는 &lt;b&gt;정확한 시간(Exact) 알람을 설정할 때 추가적인 권한이 필요&lt;/b&gt;하게 변경되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 권한 없이 AlarmManager.setExact()를 호출했으나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  SCHEDULE_EXACT_ALARM 권한을 추가해야 정상 작동.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;공식 문서 참고:&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://developer.android.com/about/versions/14/changes/schedule-exact-alarms?hl=ko&quot;&gt;AlarmManager API 변경 사항 (Android 개발 문서)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Android 12 이상에서는&lt;/b&gt; 정확한 알람을 설정하려면 SCHEDULE_EXACT_ALARM 또는 USE_EXACT_ALARM 권한이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, USE_EXACT_ALARM은 시스템 앱에서만 사용 가능하므로, 일반 앱에서는 SCHEDULE_EXACT_ALARM을 추가해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;결정 사항:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SCHEDULE_EXACT_ALARM 권한 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ &lt;b&gt;PendingIntent의 FLAG 변경&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android 12(API 31) 이상에서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PendingIntent를 생성할 때 FLAG_IMMUTABLE 또는 FLAG_MUTABLE을 반드시 명시해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 코드에서는 PendingIntent.FLAG_UPDATE_CURRENT 만 사용했으므로 &lt;b&gt;앱이 충돌하는 원인이 된다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;공식 문서 참고:&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://developer.android.com/reference/android/app/PendingIntent&quot;&gt;https://developer.android.com/reference/android/app/PendingIntent&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Android 12(API 31) 이상에서는&lt;/b&gt; 모든 PendingIntent가 FLAG_IMMUTABLE 또는 FLAG_MUTABLE을 포함해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않으면 앱이 충돌하거나 보안 문제로 인해 거부될 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다른 참고 블로그:&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://itstory1592.tistory.com/127&quot;&gt;https://itstory1592.tistory.com/127&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;결정 사항:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PendingIntent 생성 시 FLAG_IMMUTABLE 또는 FLAG_MUTABLE 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;Target SDK 34 적용을 위한 주요 변경 사항 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 사항 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Photo Picker 적용&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;READ_MEDIA_IMAGES, READ_MEDIA_VIDEO API 33+에서 요청하나, 사진 선택 1개라 권장되는 Photo Picker 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;알림 권한 POST_NOTIFICATIONS 추가&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;API 33+에서만 요청하도록 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Android 32 이하의 저장소 권한 유지&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;maxSdkVersion=&quot;32&quot;를 통해 API 32 이하에서만 기존 권한 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;불필요 권한 삭제 (READ_MEDIA_AUDIO)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;사용되지 않던 READ_MEDIA_AUDIO 권한 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ &lt;b&gt;마무리 및 느낀 점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 리팩토링을 하면서 &lt;b&gt;targetSdk를 높이는 과정에서 SDK 버전별 정책을 유지하면서 새로운 정책을 반영하는 과정의 중요성을 직접 경험&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실, 나는 아직 &lt;b&gt;서비스를 출시해 본 경험이 없었기 때문에 이런 버전 업과 정책 대응을 경험할 기회가 있을까 싶었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이번에 targetSdk를 올리는 과정에서 &lt;b&gt;버전별로 이전 기능을 유지하면서도 새로운 정책에 맞게 설정하는 과정이 필수적&lt;/b&gt;임을 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;구버전(Android 31 이하)에서는 기존의 권한 방식 유지&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;신버전(Android 33 이상)에서는 최신 보안 정책에 맞게 Photo Picker 등 새로운 API 적용&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정확한 알람 설정, PendingIntent 보안 정책 대응&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 것이 &lt;b&gt;버전별 대응 전략을 갖추는 것이 얼마나 중요한지를 깨닫게 한 경험&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직도 &lt;b&gt;리팩토링할 부분은 많지만&lt;/b&gt;, 이번 작업을 통해 &lt;b&gt;Android 버전별 정책을 꾸준히 살펴보는 것이 얼마나 중요한지 다시금 실감&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 이런 변화를 꾸준히 학습하고 적용해나가면서 더욱 안정적인 앱을 개발할 수 있도록 노력해야겠다!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;다음 목표&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;Navigation Component 적용 &amp;amp; Single Activity 구조로 전환&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;UI 상태 관리&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;결론: SDK 업데이트, 어렵지만 성장하는 기회였다!&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 targetSdk 34로 올리는 것이 &lt;b&gt;불필요한 복잡성을 초래하는 것 아닌가&lt;/b&gt; 고민했지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;장기적인 유지보수와 확장성을 위해 필수적인 과정&lt;/b&gt;이라는 점을 실감했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경험을 바탕으로 앞으로도 &lt;b&gt;Android 정책 변화를 지속적으로 모니터링하고, 유지보수 및 개선에 신경 써야겠다고 다짐&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/오르다 다이어리</category>
      <category>Android</category>
      <category>API</category>
      <category>legacycode</category>
      <category>refactoring</category>
      <category>sdk34</category>
      <category>targetSdk</category>
      <author>yujinius</author>
      <guid isPermaLink="true">https://yujinius45.tistory.com/156</guid>
      <comments>https://yujinius45.tistory.com/156#entry156comment</comments>
      <pubDate>Tue, 11 Feb 2025 20:57:04 +0900</pubDate>
    </item>
    <item>
      <title>[Android/오르다] 오르다 다이어리 레거시 리팩토링 01 - 현재 코드 분석</title>
      <link>https://yujinius45.tistory.com/155</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;  오르다&amp;nbsp;다이어리&amp;nbsp;레거시&amp;nbsp;리팩토링&amp;nbsp;01&amp;nbsp;-&amp;nbsp;현재&amp;nbsp;코드&amp;nbsp;분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://yujinius45.tistory.com/154&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yujinius45.tistory.com/154&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739255941274&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android/오르다 다이어리] 오르다 다이어리 리팩토링 - 계획 수립&quot; data-og-description=&quot;보호되어 있는 글입니다. 내용을 보시려면 비밀번호를 입력하세요.&quot; data-og-host=&quot;yujinius45.tistory.com&quot; data-og-source-url=&quot;https://yujinius45.tistory.com/154&quot; data-og-url=&quot;https://yujinius45.tistory.com/154&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/L3TmL/hyYfWw7vlL/jJcS6lHTskvsmR61yi4621/img.jpg?width=439&amp;amp;height=439&amp;amp;face=0_0_439_439&quot;&gt;&lt;a href=&quot;https://yujinius45.tistory.com/154&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yujinius45.tistory.com/154&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/L3TmL/hyYfWw7vlL/jJcS6lHTskvsmR61yi4621/img.jpg?width=439&amp;amp;height=439&amp;amp;face=0_0_439_439');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android/오르다 다이어리] 오르다 다이어리 리팩토링 - 계획 수립&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;보호되어 있는 글입니다. 내용을 보시려면 비밀번호를 입력하세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yujinius45.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▲ 이전 글에서는 &lt;b&gt;오르다 다이어리의 리팩토링 계획&lt;/b&gt;을 정리했다. 이번 글에서는 &lt;b&gt;현재 코드가 어떤 문제를 가지고 있으며, 이를 개선하기 위해 어떤 방향으로 리팩토링할 것인지 분석한 내용을 정리&lt;/b&gt;한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;오르다 다이어리 v1.0.0 주요 기능&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDoPlk/btsMez9zFVs/Wl681bEqBYCKDdetjsFWz0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDoPlk/btsMez9zFVs/Wl681bEqBYCKDdetjsFWz0/img.jpg&quot; data-alt=&quot;대략적으로 위와 같이 4가지의 기능이 존재한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDoPlk/btsMez9zFVs/Wl681bEqBYCKDdetjsFWz0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDoPlk%2FbtsMez9zFVs%2FWl681bEqBYCKDdetjsFWz0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대략적으로 위와 같이 4가지의 기능이 존재한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오르다 다이어리는 &lt;b&gt;일정 관리, 일기 작성, 알람, 지도 메모 기능을 제공하는 다기능 다이어리 앱&lt;/b&gt;이다. 사용자가 일상 기록을 보다 편리하게 관리할 수 있도록 설계되었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; ️ 일정 관리 (To-Do List)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캘린더에서 일정 확인 및 관리&lt;/li&gt;
&lt;li&gt;사용자가 입력한 일정 목록을 &lt;b&gt;내장 DB(SQLite)에 저장&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;일정 추가/수정/삭제 기능 제공&lt;/li&gt;
&lt;li&gt;RecyclerView를 활용한 일정 목록 UI 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  일기장 기능 (Diary)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사진 추가&lt;/b&gt;: 카메라 촬영 또는 갤러리에서 이미지 첨부&lt;/li&gt;
&lt;li&gt;&lt;b&gt;음성(STT) 지원&lt;/b&gt;: 음성 입력으로 빠르게 일기 작성 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;텍스트 입력&lt;/b&gt;: 키보드 타이핑을 통한 일기 작성 지원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내장 DB(SQLite) 저장&lt;/b&gt;: 영구적으로 데이터 저장 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;⏰ 알람 기능 (Alarm)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지정한 시간에 알람 설정 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;플래시 &amp;amp; 사운드 알람&lt;/b&gt;: 알람이 울릴 때 플래시와 음악이 함께 재생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가속도 센서를 활용한 알람 해제&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순한 버튼 클릭이 아닌 &lt;b&gt;핸드폰을 일정 강도로 흔들어야 알람이 해제&lt;/b&gt;됨 (가속도 센서 활용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; ️ 지도 기반 메모 기능 (Map Memo)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지도(Google Maps)를 활용한 특정 위치에 메모 추가 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  내장 DB(SQLite) 기반 데이터 저장&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;일정, 일기, 알람, 지도 메모 등 모든 데이터를 영구 저장&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;SQLiteOpenHelper를 활용하여 데이터 관리&lt;/li&gt;
&lt;li&gt;앱을 종료해도 데이터 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;  &lt;b&gt;현재 코드의 특징 및 분석&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;프로젝트 이야기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오르다 다이어리는 3년 전 개발된 &lt;b&gt;Java &amp;amp; XML 기반의 레거시 코드&lt;/b&gt;로, 현재의 &lt;b&gt;Android 개발 트렌드와는 다소 거리가 있는 구조&lt;/b&gt;를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시에는 &lt;b&gt;Android Jetpack이나 AAC(Android Architecture Components)와 같은 최신 기술을 적용하지 않고, Activity 중심의 화면 전환과 SQLite 직접 접근 방식&lt;/b&gt;을 사용했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; &lt;span&gt; &lt;/span&gt;&lt;span data-token-index=&quot;1&quot;&gt; 현재 코드에서 발견된 주요 사항&lt;/span&gt; &lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ &lt;b&gt;AAC 미적용 (Jetpack Architecture 미사용)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 프로젝트는 AAC(Android Architecture Components)를 전혀 활용하지 않고 있다.&lt;/li&gt;
&lt;li&gt;ViewModel, LiveData, RoomDB 등의 개념이 적용되지 않았으며, Activity에서 직접 데이터베이스에 접근하거나 UI 상태를 관리하고 있다.&lt;/li&gt;
&lt;li&gt;현재의 안드로이드 개발 방식에서는 &lt;b&gt;AAC를 활용한 구조적 개선이 필수적&lt;/b&gt;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ &lt;b&gt;모든 화면이 Activity 기반으로 구현됨&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 모든 주요 기능이 Activity 단위로 구현되어 있다.&lt;/li&gt;
&lt;li&gt;화면 이동 시 Intent를 통해 데이터를 주고받으며, &lt;b&gt;각 Activity가 독립적으로 동작하도록 설계되었지만, 그로 인해 화면 간 상태 유지가 어렵다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;예를 들어, 일정 관리 화면에서 일기 작성 화면으로 이동하려면 &lt;b&gt;뒤로 가기를 눌러 홈 화면을 거쳐야 하는 불편함&lt;/b&gt;이 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결 방향&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fragment를 활용하여 화면을 전환하고, Navigation Component를 적용하여 보다 자연스러운 화면 이동을 구현한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ &lt;b&gt;UI 상태 관리 문제 (Configuration Change 미대응)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면 회전(Configuration Change) 시 일부 화면의 UI 상태가 유지되지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확인된 문제점&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일정 화면(캘린더)에서 화면을 회전하면, 선택한 날짜가 초기화된다.&lt;/li&gt;
&lt;li&gt;지도 화면에서는 회전 시 저장된 메모가 사라진다.&lt;/li&gt;
&lt;li&gt;일기 작성 화면은 회전하면 입력 중이던 내용이 손실된다.&lt;/li&gt;
&lt;li&gt;반면, 일기 목록 화면에서는 데이터가 유지된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원인&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태 저장 로직이 대부분 적용되지 않았다.&lt;/li&gt;
&lt;li&gt;Activity 간 이동이 Intent를 통한 데이터 전달 방식이므로, 화면이 재생성되면서 데이터가 유지되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결 방향&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel과 LiveData를 적용하여 UI 상태를 유지하고, 회전 시에도 데이터가 보존되도록 개선한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4️⃣ &lt;b&gt;SQLite 직접 접근 방식 사용 (RoomDB 미적용)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 프로젝트는 SQLiteOpenHelper를 활용하여 &lt;b&gt;SQL 쿼리를 직접 작성하여 데이터베이스를 관리&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문제점&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL 쿼리가 직접 코드에 포함되어 있어 &lt;b&gt;가독성이 낮고, 유지보수가 어렵다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;데이터 접근 로직이 UI 코드와 섞여 있어 &lt;b&gt;관심사 분리가 되지 않은 구조&lt;/b&gt;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결 방향&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RoomDB를 적용하여 ORM(Object Relational Mapping) 방식으로 변경한다.&lt;/li&gt;
&lt;li&gt;DAO(Data Access Object) 패턴을 적용하여 데이터 관리 로직을 분리하고, Repository 패턴을 도입한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5️⃣ &lt;b&gt;ViewModel 및 MVVM 미적용&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 Activity에서 UI 로직과 데이터 로직이 섞여 있다.&lt;/li&gt;
&lt;li&gt;ViewModel을 활용하여 UI 데이터를 관리하지 않으며, &lt;b&gt;Activity가 종료되면 데이터가 모두 사라지는 구조&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결 방향&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel을 활용하여 UI 데이터를 관리하고, LiveData를 통해 UI와 데이터의 의존성을 낮춘다.&lt;/li&gt;
&lt;li&gt;MVVM 아키텍처를 적용하여 유지보수성을 높인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;리팩토링 방향 정리&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;문제점&lt;/td&gt;
&lt;td&gt;&amp;nbsp;현재 방식&lt;/td&gt;
&lt;td&gt;개선 방법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;AAC 미적용&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Jetpack 미사용&lt;/td&gt;
&lt;td&gt;AAC(ViewModel, LiveData, RoomDB) 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Activity 기반 구조&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;모든 화면을 Activity로 관리&lt;/td&gt;
&lt;td&gt;Fragment + Navigation Component 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;UI 상태 미유지&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DB에서 데이터를 불러와 바로 표시&lt;/td&gt;
&lt;td&gt;ViewModel, LiveData 활용하여 상태 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;DB 직접 접근&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;SQLiteOpenHelper 사용&lt;/td&gt;
&lt;td&gt;RoomDB + DAO 패턴 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;비효율적인 UI 로직&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Activity 내 UI 및 데이터 로직 혼재&lt;/td&gt;
&lt;td&gt;MVVM 패턴 적용하여 관심사 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ &lt;b&gt;다음 목표: UI 상태 관리 개선&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;b&gt;첫 번째 리팩토링 작업으로 UI 상태 관리를 개선&lt;/b&gt;하는 작업을 진행할 예정이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면 회전 및 Activity 재생성 시 데이터가 유지되도록 ViewModel과 LiveData를 적용한다.&lt;/li&gt;
&lt;li&gt;기존의 Intent 기반 화면 전환 구조를 Fragment로 변경하고, Navigation Component를 적용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다음 글에서는 UI 상태 유지 문제를 해결하기 위한 접근법과 구현 방안을 다룰 예정이다.&lt;/b&gt;  &lt;/p&gt;</description>
      <category>Android/오르다 다이어리</category>
      <category>activity</category>
      <category>Android</category>
      <category>SQLiteOpenHelper</category>
      <category>ui 상태 관리</category>
      <category>리팩토링</category>
      <author>yujinius</author>
      <guid isPermaLink="true">https://yujinius45.tistory.com/155</guid>
      <comments>https://yujinius45.tistory.com/155#entry155comment</comments>
      <pubDate>Tue, 11 Feb 2025 15:43:18 +0900</pubDate>
    </item>
  </channel>
</rss>