<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>FOREST, FOR REST</title>
    <link>https://aerimforest.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 17 Apr 2026 07:55:01 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>yenim</managingEditor>
    <image>
      <title>FOREST, FOR REST</title>
      <url>https://tistory1.daumcdn.net/tistory/3120024/attach/8f8efcdc40da40fb8c654f52bbba2421</url>
      <link>https://aerimforest.tistory.com</link>
    </image>
    <item>
      <title>[Jetpack Compose] Lazy List, 대용량 아이템 리스트 처리하기</title>
      <link>https://aerimforest.tistory.com/305</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 리스트에 수백 수천개의 아이템을 표시하기 위해서는 반드시 성능을 고려해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 JetPack Compose에서는 UI 버벅거림을 피하고 효율성을 개선하기 위해 필요에 따라 아이템을 동적으로 구성할 수 있게 해주는 &lt;code&gt;Lazy List&lt;/code&gt; 라는 컴포넌트를 제공하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lazy List의 종류로는 크게 &lt;code&gt;LazyColumn&lt;/code&gt;, &lt;code&gt;LazyRow&lt;/code&gt;, &lt;code&gt;LazyGrid&lt;/code&gt;가 있는데 하나씩 살펴봅시다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  1. LazyColumn&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LazyColumn&lt;/code&gt;은 화면에 보이는 요소들만 실시간으로 렌더링하는 세로 방향의 스크롤 리스트입니다.&lt;br /&gt;전통적인 Android 개발에서의 &lt;code&gt;RecyclerView&lt;/code&gt;와 유사한 역할을 하지만, 훨씬 직관적이고 적은 코드로 구현할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;일반적인 Column은 리스트에 1,000개의 아이템이 있다면 화면에 보이든 안 보이든 1,000개를 한꺼번에 메모리에 올립니다. 이는 성능 저하와 앱 크래시의 원인이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;반면 LazyColumn은 Lazy라는 이름에서 알 수 있듯이 현재 화면에 보이는 아이템만 랜더링 하고 화면 밖의 항목을 재활용하여 대규모 아이템을 효율적으로 렌더링 하도록 설계되었습니다. 덕분에 수만 개의 아이템이 있는 리스트도 부드럽게 보여줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt; 1-1. &lt;/span&gt;LazyColumn 구조&lt;/h3&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;@OptIn(ExperimentalFoundationApi::class) // for stickyHeader
@Composable
fun AdvancedLazyColumnExample() {
    // 1. 샘플 데이터 (성씨별로 그룹화된 리스트)
    val groupedData = mapOf(
        &quot;A&quot; to listOf(&quot;Apple&quot;, &quot;Avocade&quot;, &quot;Almond&quot;),
        &quot;B&quot; to listOf(&quot;Banana&quot;, &quot;Blueberry&quot;),
        &quot;C&quot; to listOf(&quot;Cherry&quot;, &quot;Coconut&quot;)
    )

    LazyColumn(
        // 2. contentPadding: 리스트 전체 테두리에 16dp 여백
        contentPadding = PaddingValues(16.dp),
        // 3. verticalArrangement: 아이템 간의 간격을 12dp로 설정
        verticalArrangement = Arrangement.spacedBy(12.dp),
        modifier = Modifier.fillMaxSize()
    ) {
        groupedData.forEach { (initial, names) -&amp;gt;
            // 4. Sticky Header: 스크롤 시 상단에 고정되는 헤더
            stickyHeader {
                Text(
                    text = initial,
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.LightGray)
                        .padding(8.dp),
                    style = MaterialTheme.typography.h6
                )
            }

            // 5. items: 리스트 내의 실제 데이터 항목들
            items(names) { name -&amp;gt;
                Card(
                    elevation = 4.dp,
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text(
                        text = name,
                        modifier = Modifier.padding(24.dp)
                    )
                }
            }
        }
    }
}&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;items(List): 데이터 리스트를 받아 각 항목을 순회하며 UI를 생성합니다.&lt;/li&gt;
&lt;li&gt;contentPadding: 리스트 전체 테두리에 여백을 줍니다. (스크롤 시 아이템이 잘리지 않게 함)&lt;/li&gt;
&lt;li&gt;verticalArrangement: 아이템 간의 간격(Arrangement.spacedBy(8.dp))을 조절합니다.&lt;/li&gt;
&lt;li&gt;Sticky Header: 특정 아이템을 상단에 고정시킬 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt; 2. &lt;/span&gt;LazyRow&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LazyColumn과 형제 관계인 컴포저블로, 화면에 보이는 요소들만 실시간으로 렌더링하는 가로(Horizontal) 방향의 스크롤 리스트입니다.&lt;br /&gt;사용법은 LazyColumn과 거의 동일하지만, 축의 방향이 세로에서 가로로 바뀌었다고 이해하시면 됩니다.&lt;br /&gt;LazyColumn 안에 여러 개의 LazyRow를 넣으면, 우리가 흔히 보는 앱(ex. 유튜브, 넷플릭스)의 메인 화면 같은 복합 레이아웃을 아주 쉽게 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt; 2-2. &lt;/span&gt;LazyRow 구성 요소&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun FruitLazyRow() {
    val fruits = listOf(&quot;Banana&quot;, &quot;Blueberry&quot;, &quot;Blackberry&quot;, &quot;Cherry&quot;, &quot;Coconut&quot;, &quot;Cranberry&quot;)

    LazyRow(
        // 가로 간격 12dp
        horizontalArrangement = Arrangement.spacedBy(12.dp),
        // 상하 가운데 정렬
        verticalAlignment = Alignment.CenterVertically,
        // 시작과 끝에 16dp 여백
        contentPadding = PaddingValues(horizontal = 16.dp),
        modifier = Modifier.fillMaxWidth().height(100.dp)
    ) {
        items(fruits) { fruit -&amp;gt;
            FruitCard(name = fruit)
        }
    }
}

@Composable
fun FruitCard(name: String) {
    Surface(
        shape = RoundedCornerShape(16.dp),
        color = Color(0xFFE3F2FD),
        modifier = Modifier.padding(vertical = 8.dp)
    ) {
        Text(
            text = name,
            modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
            style = MaterialTheme.typography.body1
        )
    }
}&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;horizontalArrangement: 아이템 사이의 가로 간격을 조절합니다. (Arrangement.spacedBy(8.dp))&lt;/li&gt;
&lt;li&gt;verticalAlignment: 아이템들을 위, 아래, 혹은 가운데 중 어디에 정렬할지 결정합니다. (Alignment.CenterVertically)&lt;/li&gt;
&lt;li&gt;contentPadding: 리스트의 시작(왼쪽)과 끝(오른쪽)에 여백을 주어, 스크롤 시 아이템이 화면 끝에 딱 붙지 않게 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt; 3. &lt;/span&gt;LazyGrid&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LazyColumn과 LazyRow가 한 방향으로만 아이템을 나열한다면, &lt;code&gt;LazyGrid&lt;/code&gt;는 격자 모양(바둑판 모양)으로 아이템을 배치할 때 사용합니다. 갤러리 앱이나 쇼핑몰의 상품 목록 화면을 상상하시면 됩니다.&lt;br /&gt;Jetpack Compose에서는 주로 &lt;code&gt;LazyVerticalGrid&lt;/code&gt;(세로 스크롤)를 사용하며, 드물게 가로로 스크롤되는 &lt;code&gt;LazyHorizontalGrid&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt; 3-1. &lt;/span&gt;LazyGrid 구성 요소&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun FruitGrid() {
    val fruits = listOf(&quot;Banana&quot;, &quot;Blueberry&quot;, &quot;Blackberry&quot;, &quot;Cherry&quot;, &quot;Coconut&quot;, &quot;Cranberry&quot;, &quot;Cantaloupe&quot;)

    LazyVerticalGrid(
        // 1. 열 개수를 2개로 고정
        columns = GridCells.Fixed(2),
        // 2. 상하/좌우 간격 설정
        verticalArrangement = Arrangement.spacedBy(10.dp),
        horizontalArrangement = Arrangement.spacedBy(10.dp),
        contentPadding = PaddingValues(16.dp),
        modifier = Modifier.fillMaxSize()
    ) {
        items(fruits) { fruit -&amp;gt;
            Box(
                modifier = Modifier
                    .aspectRatio(1f) // 정사각형 모양 유지
                    .clip(RoundedCornerShape(12.dp))
                    .background(Color(0xFFFFF9C4)),
                contentAlignment = Alignment.Center
            ) {
                Text(text = fruit, fontWeight = FontWeight.Bold)
            }
        }
    }
}&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;columns: 열을 어떻게 나눌지 결정하는 방식으로, 아래 두가지 방식을 제공합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fixed: 열의 개수를 고정합니다. (예: 무조건 3줄)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;columns = GridCells.Fixed(3)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Adaptive: 아이템의 최소 폭을 설정하면, 화면 크기에 맞춰 열 개수가 자동으로 조절됩니다. (태블릿이나 가로 모드 대응에 유리)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;columns = GridCells.Adaptive(minSize = 128.dp)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;verticalArrangement: 행(Row) 사이의 간격&lt;/li&gt;
&lt;li&gt;horizontalArrangement: 열(Column) 사이의 간격&lt;/li&gt;
&lt;li&gt;Span: 특정 아이템만 혼자서 2칸 이상을 차지하게 만들 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;키(Keys)를 사용한 성능 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 위의 과일 리스트 예제에서 0번에 있던 Banana를 삭제한다고 가정해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 상태는 0번(Banana) &amp;gt; 1번(Cherry) &amp;gt; 2번(Apple) 순서입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Banana를 삭제해봅시다. 이제 1번이었던 Cherry가 0번이 되고, 2번이었던 Apple이 1번이 됩니다.&lt;br /&gt;이때 컴포즈는 &quot;어? 0번 데이터가 Banana에서 Cherry로 바뀌었네? 0번 UI를 아예 새로 그려야겠다!&quot;라고 판단하고, 이 과정에서 불필요한 Recomposition이 발생하게 되지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 0번 Banana에 체크박스를 체크해뒀다면, 그 '체크됨' 상태는 0번 자리와 연결되어 있는 값이기에 Banana는 사라졌음에도 그 자리에 새로 들어온 Cherry가 체크되어 있는 황당한 일이 벌어질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 아이템마다 고유한 키 값을 부여하면 상황은 달라집니다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;items(
    items = fruitList,
    key = { fruit -&amp;gt; fruit.id } // 각 과일의 고유 ID를 키로 지정
) { fruit -&amp;gt;
    FruitItem(fruit)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트 재정렬 또는 아이템 삭제 시 Banana가 사라져도 컴포즈는 &quot;0번이 바뀌었네?&quot;가 아니라, &quot;ID 101번(Banana)이 사라졌고, ID 102번(Cherry)은 위치만 위로 옮겨졌구나!&quot;라고 정확히 이해합니다.&lt;br /&gt;또한, 위치가 바뀐 아이템들은 Recomposition 없이 위치만 이동시키기 때문에 성능을 최적화 할 수 있고, 아이템의 순서가 바뀌더라도 내 데이터 상태가 유지됩니다.&lt;br /&gt;⚠️ 주의사항: 키 값은 리스트 내에서 절대 중복되면 안 됩니다. 중복될 경우 앱이 크래시 날 수 있으니 보통 DB의 ID 값이나 고유한 문자열을 사용합니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Manifest Interview Android&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Android</category>
      <category>compose</category>
      <category>Kotlin</category>
      <category>LazyColumn</category>
      <author>yenim</author>
      <guid isPermaLink="true">https://aerimforest.tistory.com/305</guid>
      <comments>https://aerimforest.tistory.com/305#entry305comment</comments>
      <pubDate>Sun, 1 Feb 2026 17:43:26 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 안드로이드에서 예외(Exceptions) 추적하기</title>
      <link>https://aerimforest.tistory.com/304</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 앱 개발 과정에서 예외 추적은 매우 중요한 작업입니다. 예외를 제대로 기록하고 분석해야 문제를 재현하고, 근본 원인을 찾아내어 더 안정적인 앱을 만들 수 있습니다. 이번 글에서는 안드로이드에서 예외를 추적하는 다양한 방법을 정리해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Logcat을 이용한 예외 로깅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 방법은 &lt;code&gt;Logcat&lt;/code&gt;을 활용하는 것입니다.&lt;br /&gt;예외가 발생하면 시스템은 예외 유형, 메시지, 발생 코드 줄, 스택 트레이스를 Logcat에 출력합니다.&lt;br /&gt;Logcat에서 &lt;code&gt;E/AndroidRuntime&lt;/code&gt; 키워드로 필터링하면 예외 로그만 집중적으로 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. try-catch로 예외 처리 및 로깅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 크래시로 이어지지 않도록 방지하려면 &lt;code&gt;try-catch&lt;/code&gt; 블록을 적절히 활용해야 합니다.&lt;br /&gt;예외 발생 시 로그를 남기면 문제 분석이 훨씬 쉬워집니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;try {
    val result = performRiskyOperation()
} catch (e: Exception) {
    Log.e(&quot;Error&quot;, &quot;Exception occurred: ${e.message}&quot;, e)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 예외 메시지와 스택 트레이스가 Logcat에 출력되어 추적이 용이합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 전역 예외 핸들러 (Global Exception Handler)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 전체에서 발생하는 처리되지 않은 예외(Uncaught Exception) 를 포착하려면&lt;br /&gt;&lt;code&gt;Thread.setDefaultUncaughtExceptionHandler&lt;/code&gt;를 설정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()

        Thread.setDefaultUncaughtExceptionHandler { thread, exception -&amp;gt;
            Log.e(&quot;GlobalHandler&quot;,
                &quot;Uncaught exception in thread ${thread.name}: ${exception.message}&quot;, exception)

            // Crashlytics 같은 외부 서비스로 전송 가능
            // FirebaseCrashlytics.getInstance().recordException(exception)

            // 기존 기본 핸들러 호출 (선택)
            defaultHandler?.uncaughtException(thread, exception)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 QA나 디버깅 환경에서 특히 유용합니다.&lt;br /&gt;예외를 중앙 집중식으로 관리하고, 필요하다면 서드파티 서비스(Crashlytics 등) 로 전송할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Firebase Crashlytics 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로덕션 환경에서 가장 널리 쓰이는 방법은 &lt;code&gt;Firebase Crashlytics&lt;/code&gt;입니다.&lt;br /&gt;Crashlytics는 처리되지 않은 예외를 자동으로 기록하고, 스택 트레이스, 기기 상태, 사용자 세션 정보까지 포함한 보고서를 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;try {
    val data = fetchData()
} catch (e: IOException) {
    FirebaseCrashlytics.getInstance().recordException(e)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Crashlytics를 사용하면 크래시 발생 비율, 발생 조건, 사용자 영향 범위를 한눈에 파악할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 브레이크포인트(Breakpoints)를 활용한 디버깅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 중이라면 Android Studio의 디버깅 기능이 강력한 도구가 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 특정 지점에 브레이크포인트 설정&lt;/li&gt;
&lt;li&gt;실행 중 앱 상태(변수 값, 호출 스택, 객체 상태) 실시간 확인&lt;/li&gt;
&lt;li&gt;예외 발생 직전의 흐름 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 개발 단계에서 원인을 좁혀가는 과정에 매우 유용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 버그 리포트(Bug Report) 캡처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서 예외 상황을 재현하기 어렵다면, 버그 리포트를 캡처하는 것도 좋은 방법입니다.&lt;br /&gt;ADB 또는 기기 자체에서 버그 리포트를 생성하면, 로그와 시스템 상태를 함께 얻을 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자 옵션 &amp;rarr; 버그 신고 기능 활용&lt;/li&gt;
&lt;li&gt;에뮬레이터 확장 컨트롤에서 버그 리포트 저장&lt;/li&gt;
&lt;li&gt;ADB 명령어&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;adb bugreport /path/to/save/bugreport.zip&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 ZIP 파일에는 logcat, dumpsys, dumpstate 등 진단에 필요한 정보가 포함됩니다.&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;안드로이드에서 예외 추적을 위한 방법은 다양합니다. 이 도구들을 적절히 조합하면 개발 단계부터 프로덕션 운영까지 포괄적인 예외 관리 체계를 구축할 수 있습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Manifest Interview Android&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Android</category>
      <author>yenim</author>
      <guid isPermaLink="true">https://aerimforest.tistory.com/304</guid>
      <comments>https://aerimforest.tistory.com/304#entry304comment</comments>
      <pubDate>Wed, 17 Sep 2025 12:27:52 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 안드로이드의 Looper, Handler, HandlerThread 이해하기</title>
      <link>https://aerimforest.tistory.com/303</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 멀티스레딩을 다룰 때 자주 등장하는 세 가지 컴포넌트가 있습니다. 바로 Looper, Handler, HandlerThread입니다. 이들은 스레드를 관리하고, 메시지/작업을 큐에 넣어 순차적으로 처리하며, UI 스레드와의 통신을 가능하게 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Looper&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Looper&lt;/code&gt;는 안드로이드의 스레딩 모델에서 메시지 큐를 모니터링하고 처리하는 핵심 컴포넌트입니다.&lt;br /&gt;스레드를 살아있게 유지하며, 큐에 들어온 메시지나 작업을 순차적으로 꺼내 처리합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 역할&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지 큐를 지속적으로 모니터링하고 메시지나 작업을 적절한 핸들러로 전달&lt;/li&gt;
&lt;li&gt;스레드가 종료되지 않고 계속해서 작업을 기다릴 수 있도록 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메인(UI) 스레드에는 기본적으로 Looper가 붙어 있음&lt;/li&gt;
&lt;li&gt;워커 스레드의 경우 Looper.prepare() &amp;rarr; Looper.loop()를 통해 직접 초기화 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val thread = Thread {
    Looper.prepare() // Looper 준비
    val handler = Handler(Looper.myLooper()!!) // 현재 스레드의 Looper로 Handler 생성
    Looper.loop() // 메시지 루프 시작
}
thread.start()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;Handler&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Handler&lt;/code&gt;는 Looper와 연결되어 메시지나 Runnable을 큐에 넣고, 해당 스레드에서 실행되도록 도와줍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 역할&lt;/h4&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;한 스레드에서 다른 스레드로 작업이나 메시지 전달 he(ex. 백그라운드 스레드에서 UI 업데이트 하기)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성 시점의 Looper와 연결됨&lt;/li&gt;
&lt;li&gt;post {} 로 Runnable 실행, sendMessage() 로 메시지 전달 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;val handler = Handler(Looper.getMainLooper()) // 메인 스레드 Handler 생성

handler.post {
    // 메인 스레드(UI)에서 실행
    textView.text = &quot;Updated from background thread&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;HandlerThread&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HandlerThread&lt;/code&gt;는 내장된 Looper를 가진 특별한 Thread입니다. 별도의 설정 없이 Looper가 자동으로 붙어 있기 때문에, 메시지 큐 기반의 워커 스레드를 간단히 만들 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 역할&lt;/h4&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;Handler와 함께 사용해 순차적인 작업 처리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;start() 호출 시 자동으로 Looper가 준비됨&lt;/li&gt;
&lt;li&gt;Handler(handlerThread.looper) 형태로 Handler 생성 가능&lt;/li&gt;
&lt;li&gt;종료 시 quit() 또는 quitSafely() 호출 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val handlerThread = HandlerThread(&quot;WorkerThread&quot;)
handlerThread.start() // 스레드 시작

val workerHandler = Handler(handlerThread.looper)

workerHandler.post {
    // 백그라운드에서 실행되는 작업
    Thread.sleep(1000)
    Log.d(&quot;HandlerThread&quot;, &quot;Task completed&quot;)
}

// 종료 (큐에 남은 메시지까지 처리 후 안전하게 종료)
handlerThread.quitSafely()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;차이점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Looper &amp;rarr; 메시지 루프 유지 (스레드를 계속 살아있게 함)&lt;/li&gt;
&lt;li&gt;Handler &amp;rarr; 메시지와 Runnable을 큐에 넣고 처리 (스레드 간 통신 담당)&lt;/li&gt;
&lt;li&gt;HandlerThread &amp;rarr; 자동으로 Looper가 붙은 워커 스레드 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;  &lt;/span&gt;언제 어떤 걸 쓸까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Looper: 메인 스레드 유지 또는 직접 구현한 워커 스레드에서 메시지 루프가 필요할 때&lt;/li&gt;
&lt;li&gt;Handler: 스레드 간 통신, 백그라운드에서 UI 업데이트 등 작업 전달이 필요할 때&lt;/li&gt;
&lt;li&gt;HandlerThread: 네트워크 요청, DB 처리 등 별도의 전용 스레드가 필요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드의 스레드 관리 구조를 이해하는 것은 효율적인 비동기 처리의 핵심입니다.&lt;br /&gt;Looper는 메시지 루프를 유지하고, Handler는 메시지를 보내고 처리하며, HandlerThread는 이를 쉽게 활용할 수 있는 워커 스레드를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 가지를 적절히 활용하면 UI는 부드럽게 유지하면서도 무거운 작업을 안정적으로 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  참고&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Manifest Interview Android&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Android</category>
      <category>android</category>
      <author>yenim</author>
      <guid isPermaLink="true">https://aerimforest.tistory.com/303</guid>
      <comments>https://aerimforest.tistory.com/303#entry303comment</comments>
      <pubDate>Wed, 17 Sep 2025 12:15:02 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 런타임 권한(runtime permissions) 처리</title>
      <link>https://aerimforest.tistory.com/302</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt; &amp;zwj;  런타임 권한(Runtime Permissions)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 6.0(API 23) 이상에서는 런타임 권한(Runtime Permission) 또는 위험 권한(Dangerous Permission)이라는 개념이 도입되었습니다.&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; &amp;zwj; &lt;span&gt;&amp;nbsp;&lt;/span&gt;런타임 권한이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런타임 권한은 앱이 제한된 데이터에 접근하거나, 시스템 및 다른 앱에 영향을 줄 수 있는 동작을 수행할 때 &lt;u&gt;반드시&lt;/u&gt; 요청해야 하는 권한입니다.&lt;br /&gt;설치 시 자동으로 부여되지 않으며, 앱 실행 중 실제로 필요할 때 팝업 다이얼로그로 요청됩니다.&lt;br /&gt;즉, 개발자는 권한이 이미 부여되어 있다고 가정해서는 안되며, &lt;u&gt;기능 실행 전마다 권한을 확인하고 필요 시 요청&lt;/u&gt;해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-14 오전 12.42.28.png&quot; data-origin-width=&quot;566&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0Q7HX/btsQvOWVHuC/U3BolEBkcdDyP208jnaBcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0Q7HX/btsQvOWVHuC/U3BolEBkcdDyP208jnaBcK/img.png&quot; data-alt=&quot;앱에서 런타임 권한을 요청 시 표시되는 시스템 권한 메시지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0Q7HX/btsQvOWVHuC/U3BolEBkcdDyP208jnaBcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0Q7HX%2FbtsQvOWVHuC%2FU3BolEBkcdDyP208jnaBcK%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;310&quot; height=&quot;243&quot; data-filename=&quot;스크린샷 2025-09-14 오전 12.42.28.png&quot; data-origin-width=&quot;566&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;앱에서 런타임 권한을 요청 시 표시되는 시스템 권한 메시지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &amp;zwj; &lt;span&gt;&amp;nbsp;&lt;/span&gt;위험 권한 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런타임 권한 대부분은 민감한 사용자 데이터와 관련이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;위치 정보 (Location)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;연락처 (Contacts)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마이크 (Microphone)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;카메라 (Camera)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 특히 마이크와 카메라는 사생활과 직접적으로 연결된 매우 민감한 정보에 접근할 수 있기 때문에, 안드로이드는 앱이 해당 권한을 요청할 때 사용자에게 권한이 필요한 이유를 설명할 수 있도록 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &amp;zwj; &lt;span&gt;&amp;nbsp;&lt;/span&gt;시스템 보호 수준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드는 런타임 권한을 dangerous(위험)보호 수준으로 분류합니다.&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;Normal&lt;/code&gt; 권한 &amp;rarr; 자동 부여 (예: 인터넷 접근)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dangerous&lt;/code&gt; 권한 &amp;rarr; 런타임에 사용자 동의 필요 (예: 위치, 카메라 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &amp;zwj; &lt;span&gt;&amp;nbsp;&lt;/span&gt;권한 처리 가이드&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 권한 선언 및 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한을 요청하기 전에 &lt;code&gt;AndroidManifest.xml&lt;/code&gt;에 해당 권한을 선언해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;uses-permission android:name=&quot;android.permission.CAMERA&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런타임 시에는 사용자가 실제로 해당 기능을 사용할 때만 권한을 요청하는 것이 바람직하며 요청 전에는 반드시 &lt;code&gt;ContextCompat.checkSelfPermission()&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;허용됨&lt;/b&gt; &amp;rarr; 기능 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;허용되지 않음&lt;/b&gt; &amp;rarr; 권한 요청 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1757778097830&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 권한 확인 및 요청
fun checkAndRequestPermission() {
    when {
        // 이미 권한 있음
        ContextCompat.checkSelfPermission(
            this, Manifest.permission.CAMERA
        ) == PackageManager.PERMISSION_GRANTED -&amp;gt; {
            startCamera()
        }

        // 권한이 필요한 이유 설명
        shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -&amp;gt; {
            showPermissionExplanationDialog {
                requestPermissionLauncher.launch(Manifest.permission.CAMERA)
            }
        }

        // 권한 요청
        else -&amp;gt; {
            requestPermissionLauncher.launch(Manifest.permission.CAMERA)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 권한 요청하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 권장하는 최신 방식은 &lt;code&gt;ActivityResultLauncher&lt;/code&gt; API 를 활용하는 것입니다.&lt;br /&gt;시스템이 자동으로 권한 다이얼로그를 띄워주고, 사용자는 허용/거부를 선택할 수 있습니다. 결과는 콜백으로 간단히 처리할 수 있어 코드가 깔끔해집니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// 권한 요청 런처 등록
private val requestPermissionLauncher =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -&amp;gt;
        if (isGranted) {
            // 권한 허용됨 &amp;rarr; 기능 실행
        } else {
            // 권한 거부됨 &amp;rarr; 사용자 안내
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 권한 요청 근거 제공하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경우에 따라 시스템은 &lt;code&gt;shouldShowRequestPermissionRationale()&lt;/code&gt;을 true로 반환할 수 있습니다. 이때는 권한 요청 전, 사용자에게 해당 기능에 권한이 왜 필요한지 UI로 설명해야 합니다. 이렇게 하면 사용자가 권한의 필요성을 이해하여 권한 허용 가능성을 높일 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 권한 거부 처리하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 권한을 여러 번 거부하면, 안드로이드는 이를 영구 거부로 간주합니다. 이 경우 앱은 더 이상 권한 요청 다이얼로그를 띄울 수 없으며, 앱은 사용자에게 기능 제한을 명확히 안내하고 필요한 경우 시스템 설정 화면으로 이동하도록 유도해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/th9fr/dJMb9V7vJtv/OEYVI7klcpKGrl518h9cwK/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/th9fr/dJMb9V7vJtv/OEYVI7klcpKGrl518h9cwK/tfile.svg&quot; data-alt=&quot;Android에서 권한을 사용하는 대략적인 워크플로&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/th9fr/dJMb9V7vJtv/OEYVI7klcpKGrl518h9cwK/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fth9fr%2FdJMb9V7vJtv%2FOEYVI7klcpKGrl518h9cwK%2Ftfile.svg&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;300&quot; height=&quot;130&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Android에서 권한을 사용하는 대략적인 워크플로&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[앱 실행]
    │
    ▼
[권한 선언 확인 (Manifest)]
    │
    ▼
[권한 확인]
 ┌───────────────┬────────────────────────┐
 │               │                        │
 ▼               ▼                        ▼
이미 허용됨   설명 필요(true)           권한 없음
(바로 실행)   &amp;rarr; 근거 표시 후 요청       &amp;rarr; 권한 요청
 │
 ▼
사용자 응답
 ┌───────────────┬────────────────────────┐
 │               │                        │
 ▼               ▼                        ▼
허용됨        거부됨                  영구 거부됨
기능 실행    &amp;rarr; 안내 메시지           &amp;rarr; 설정 화면 유도&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  참고&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Manifest Interview Android&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/guide/topics/permissions/overview?hl=ko#runtime&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/guide/topics/permissions/overview?hl=ko#runtime&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Android</category>
      <category>android</category>
      <category>permissions</category>
      <author>yenim</author>
      <guid isPermaLink="true">https://aerimforest.tistory.com/302</guid>
      <comments>https://aerimforest.tistory.com/302#entry302comment</comments>
      <pubDate>Sun, 14 Sep 2025 00:37:06 +0900</pubDate>
    </item>
    <item>
      <title>[Android] SparseArray에 대하여</title>
      <link>https://aerimforest.tistory.com/301</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SparseArray&lt;/code&gt;는 정수(int)를 객체(Object)에 매핑하는 안드로이드에 최적화된 자료구조입니다. 일반적인 배열과 달리 인덱스가 연속적일 필요가 없어 중간에 값이 비어 있는(gap) 형태도 허용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스는 &lt;code&gt;HashMap&amp;lt;Integer, Object&amp;gt;&lt;/code&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; &amp;zwj;  SparseArray의 주요 특징&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;br /&gt;키-값 매핑을 위해 HashTable을 사용하는 HashMap과 달리 SparseArray는 오토박싱(int &amp;rarr; Integer 변환)이 필요 없습니다. 각 엔트리를 별도의 객체로 포장하지 않고, 내부 배열에 직접 저장하기 때문에 훨씬 적은 메모리를 소비합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능&lt;/b&gt;&lt;br /&gt;매우 큰 데이터 셋의 경우 HashMap만큼 빠르지는 않지만, SparseArray는 메모리 최적화 덕분에 중간 크기의 데이터 셋에서 더 나은 성능을 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Null 키 값 사용 불가&lt;/b&gt;&lt;br /&gt;SparseArray는 키 값으로 기본 정수를 사용하므로 키 값에 Null 사용을 허용하지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;삭제 최적화&lt;/b&gt;&lt;br /&gt;&lt;code&gt;SparseArray&lt;/code&gt;는 항목을 삭제할 때 즉시 배열을 정리하지 않습니다. 삭제된 항목은 &quot;삭제됨&quot; 표시만 남기고 자리를 유지하며, 같은 키가 다시 들어올 경우 그 자리를 재활용하게 됩니다. 이후 배열이 커져야 하거나, 크기/값 조회가 필요한 경우 한 번에 가비지 컬렉션(compaction)으로 배열을 정리하게 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &amp;zwj; &lt;span&gt;&amp;nbsp;&lt;/span&gt;동작 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 배열에 데이터를 저장하며, 키를 찾을 때는 이진 탐색(binary search)을 사용합니다. 데이터가 많거나 자주 삽입/삭제가 일어나는 경우에는 적합하지 않습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조회(lookup): &lt;code&gt;HashMap&lt;/code&gt;보다 느림 (이진 탐색 필요)&lt;/li&gt;
&lt;li&gt;추가/삭제: 배열에 삽입/삭제 작업이 필요하므로 비용이 큼&lt;br /&gt;다만, 수백 개 정도의 항목까지는 성능 차이가 50% 미만이라 HashMap 대신 SparseArray을 사용해도 괜찮습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &amp;zwj; &lt;span&gt;&amp;nbsp;&lt;/span&gt;SparseArray의 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SparseArray의 사용법은 간단하며, HashMap과 유사한 형태로 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import android.util.SparseArray

val sparseArray = SparseArray&amp;lt;String&amp;gt;()  

// 값 추가
sparseArray.put(1, &quot;One&quot;)
sparseArray.put(2, &quot;Two&quot;)

// 요소 접근
val value = sparseArray[1] // &quot;One&quot;

// 요소 제거
sparseArray.remove(2)

// 요소 순회
for (i in 0 until sparseArray.size()) {
    // 주어진 인덱스 위치의 키를 반환(인덱스를 오름차순으로 증가시키면 키도 오름차순으로 반환됨)
    val key = sparseArray.keyAt(i)
    // 해당 인덱스 키에 연결된 값을 반환(키 순서에 따라 값도 정렬된 순서로 반환됨)
    val value = sparseArray.valueAt(i)
    println(&quot;Key: $key, Value: $value&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &amp;zwj; &lt;span&gt;&amp;nbsp;&lt;/span&gt;SparseArray의 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SparseArray는 메모리 효율적이나 모든 사용 사례에 항상 적합한 것은 아닙니다. 위에서 한번 언급했듯이 SparseArray의 요소 접근은 키 조회를 위해 이진 탐색(binary search)을 사용하기 때문에 매우 큰 데이터 셋의 경우 HasgMap보다 느립니다. 또한, 키가 정수로만 제한되어 다른 유형의 키가 필요한 사용 사례에는 적합하지 않습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Manifest Interview Android&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/reference/android/util/SparseArray&quot;&gt;https://developer.android.com/reference/android/util/SparseArray&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Android</category>
      <category>android</category>
      <category>HashMap</category>
      <category>sparsearray</category>
      <author>yenim</author>
      <guid isPermaLink="true">https://aerimforest.tistory.com/301</guid>
      <comments>https://aerimforest.tistory.com/301#entry301comment</comments>
      <pubDate>Sun, 14 Sep 2025 00:08:54 +0900</pubDate>
    </item>
    <item>
      <title>[Android] ActivityManager란?</title>
      <link>https://aerimforest.tistory.com/300</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;  ActivityManager&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ActivityManager는 안드로이드에서 액티비티, 서비스, 그리고 이를 포함하는 프로세스에 대한 정보를 제공하고, 필요할 경우 이들과 상호작용할 수 있게 해주는 클래스입니다.&lt;br /&gt;이 클래스의 많은 기능은 특수한 상황이나 디버깅, 테스트 목적으로 만들어졌기 때문에 일반적인 앱 개발에서는 ActivityManager를 직접 다룰 일은 거의 없으나, 일부 메서드는 실무에서도 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;ActivityManager의 주요 기능&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;태스크 및 Activity 정보&lt;br /&gt;ActivityManager은 실행 중인 태스크, Activity 및 해당 스택에 대한 세부 정보를 추적할 수 있습니다. 이는 개발자가 앱 동작 및 시스템 리소스 사용량을 모니터링하는 데 도움이 됩니다.&lt;/li&gt;
&lt;li&gt;메모리 관리&lt;br /&gt;앱이 메모리 소비 및 시스템 전체 메모리 상태를 포함하여 시스템 전체의 메모리 사용량에 대한 정보를 제공합니다. 개발자는 이를 사용하여 앱 성능을 최적화하고 메모리 부족 상태를 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;앱 프로세스 관리&lt;br /&gt;ActivityManager은 실행 중인 앱 프로세스 및 Service에 대한 세부 정보를 쿼리할 수 있습니다. 개발자는 이 정보를 사용하여 앱 상태를 감지하거나 프로세스 수준의 변화에 응답할 수 있습니다.&lt;/li&gt;
&lt;li&gt;디버깅 및 진단&lt;br /&gt;힙 덤프 생성 또는 앱 프로파일링과 같이 디버깅을 위한 도구를 제공하여 성능 병목 현상이나 메모리 누수를 식별하는 데 도움이 될 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&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;getRunningAppProcesses()&lt;/code&gt;: 디바이스에서 실행 중인 애플리케이션 프로세스 목록을 반환합니다. 이를 통해 앱이 현재 실행 중인지를 판단할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getMemoryInfo(ActivityManager.MemoryInfo memoryInfo)&lt;/code&gt;: 사용 가능한 메모리, 임계 메모리, 기기가 메모리 부족 상태인지 여부 등 시스템에 대한 자세한 메모리 정보를 검색합니다. 이는 메로리 부족 상태 동안 앱 동작을 최적화하는 데 유용합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;killBackgroundProcesses(String packageName)&lt;/code&gt;: 시스템 리소스를 확보하기 위해 지정된 앱의 백그라운드 프로세스를 종료합니다. 이는 리소스 집약적인 앱을 테스트하거나 관리하는 데 유용합니다.&lt;/li&gt;
&lt;li&gt;isLowRamDevice(): 현재 기기가 메모리가 제한된 환경인지 확인하여 앱이 저메모리 기기에 대한 리소스 사용량을 최적화하는 데 도움을 줍니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;appNotResponding(String message)&lt;/code&gt;: 테스트 목적으로 ANR(App Not Responding) 이벤트를 시뮬레이션합니다. 디버깅 중에 앱이 ANR 상황에서 어떻게 동작하거나 응답하는지 이해하는 데 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clearApplicationUserData()&lt;/code&gt;: 파일, 데이터베이스 및 SharedPreferences을 포함하여 애플리케이션과 관련된 모든 사용자별 데이터를 지웁니다. 공장 초기화나 앱을 기본 상태로 재설정하는 경우에 종종 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;Manifest Android Interview&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Android</category>
      <category>ActivityManager</category>
      <category>android</category>
      <author>yenim</author>
      <guid isPermaLink="true">https://aerimforest.tistory.com/300</guid>
      <comments>https://aerimforest.tistory.com/300#entry300comment</comments>
      <pubDate>Sat, 13 Sep 2025 23:33:10 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 화면 회전과 같은 구성 변경이 발생할 때 Activity에 생기는 변화</title>
      <link>https://aerimforest.tistory.com/298</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 구성 변경(ex. 화면 회전, 테마 변경, 글꼴 크기 조정 또는 언어 업데이트)이 발생하면 시스템은 새 구성을 적용하기 위해 현재 Activity를 종료하고 다시 실행하게 됩니다. 이러한 동작은 앱의 리소스가 변경된 구성을 새롭게 반영하고 앱이 다시 로드되도록 보장합니다.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Activity 종료 및 재시작&lt;br /&gt;구성 변경이 발생하면 Activity가 종료된 다음 재시작됩니다.&lt;br /&gt;1) 시스템은 현재 실행 중인 Activity의 onPause() &amp;gt; onStop() &amp;gt; onDestroy() 메서드를 순차적으로 호출&lt;br /&gt;2) 구성 변경이 완료되면 Activity는 재시작되고, onCreate() 메서드가 호출됨&lt;/li&gt;
&lt;li&gt;리소스 재로드&lt;br /&gt;시스템은 새 구성에 따라 리소스(ex. 레이아웃, Drawable, 문자열)를 다시 로드하여 앱이 화면 방향, 테마 또는 언어와 같은 변경 사항이 반영될 수 있도록 합니다.&lt;/li&gt;
&lt;li&gt;데이터 손실 방지&lt;br /&gt;개발자는 재생성 중 데이터 손실을 방지하기 위해 &lt;code&gt;onSaveInstanceState()&lt;/code&gt; 또는 &lt;code&gt;onRestoreInstanceState()&lt;/code&gt; 메서드를 사용하거나 viewModel을 활용하여 인스턴스 상태를 저장하고 복원할 수 있습니다.&lt;br /&gt;예를 들어, 화면 회전이 발생하는 경우 액티비티의 라이프 사이클은 아래와 같이 호출됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;onPause() &amp;rarr; onSaveInstanceState() &amp;rarr; onStop() &amp;rarr; onDestory() &amp;rarr; onCreate() &amp;rarr; onStart() &amp;rarr; onResume()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 보존이 필요한 정보는 아래와 같이 &lt;code&gt;onSaveInstanceState()&lt;/code&gt;에서 outState 번들에 저장한 후, &lt;code&gt;onCreate()&lt;/code&gt;의 파라미터인 savedInstanceState 번들에서 꺼내어 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putString(&quot;user_input&quot;, editText.text.toString())
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val restoredInput = savedInstanceState?.getString(&quot;user_input&quot;)
    editText.setText(restoredInput)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 fragment에서도 onSaveInstanceState을 이용하여 기존 데이터를 유지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&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;br /&gt;화면 방향을 세로/가로로 변경하여 새 크기게 맞게 레이아웃을 다시 로드합니다.&lt;/li&gt;
&lt;li&gt;다크/라이트 테마 변경&lt;br /&gt;사용자가 다크 모드와 라이트 모드 간에 전환하면 앱은 테마별 리소스(ex. color, styles)를 다시 로드합니다.&lt;/li&gt;
&lt;li&gt;글꼴 크기 변경&lt;br /&gt;기기의 글꼴 크기 설정 조정은 새 배율을 반영하도록 텍스트 리소스를 다시 로드합니다.&lt;/li&gt;
&lt;li&gt;언어 변경&lt;br /&gt;시스템 언어 업데이트는 현지화된 리소스(ex. 다른 언어의 문자열)를 다시 로드합니다.&lt;/li&gt;
&lt;li&gt;앱 디스플레이 크기 변경&lt;br /&gt;기기를 접거나 펴는 등의 디스플레이 크기 변경이 발생하면 리소스를 다시 로드합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;액티비티 재생성 피하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템은 새 기기의 구성과 일치하는 리소스로 애플리케이션을 자동 새로고침하여 애플리케이션이 새 구성에 맞게 조정되도록 지원합니다. 하지만 앱이 특정 구성 변경 중에 리소스를 업데이트하지 않아도 되거나, 성능 제한이 있는 경우 개발자가 구성 변경을 직접 처리한다고 선언하여 시스템이 활동을 다시 시작하지 못하도록 할 수 있습니다.&lt;br /&gt;Activity를 다시 시작하지 않고 구성 변경을 처리하려면 AndroidManifest.xml에서 &lt;code&gt;android:ConfigChanges&lt;/code&gt; 속성을 추가하면 됩니다. 이 방식은 변경 사항을 개발자가 수동적으로 처리하는 형태로 책임을 개발자에게 위임합니다.&lt;br /&gt;아래의 매니페스트 코드는 화면 방향과 키보드 사용 가능 여부가 변경될 때 MyActivity의 Activity 재생성을 사용 중지합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;activity
    android:name=&quot;.MyActivity&quot;
    android:configChanges=&quot;orientation|screenSize|screenLayout|keyboardHidden&quot;
    android:label=&quot;@string/app_name&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 시스템은 Activity를 소멸시키고 다시 생성하지 않습니다. 대신 &lt;code&gt;onConfigurationCHanged()&lt;/code&gt; 메서드가 호출되어 개발자가 변경 사항을 수동으로 처리할 수 있게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        // 화면이 가로로 변경된 경우 로직 처리
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
        // 화면이 세로로 변경된 경우 로직 처리
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 구성 변경사항은 항상 액티비티가 재생성 되도록 강제하기 때문에 재생성을 피할 수 없습니다.(ex. DynamicColors)&lt;br /&gt;또한, 재성성을 막는 경우 언어나 테마, 레이아웃 전환 등의 리소스가 자동 적용되지 않고 전부 수동으로 처리해야 하기 때문에 유지보수가 어려워지게 됩니다. 이러한 이유에서 구글은 View 시스템 기반 앱에서는 Activity 재생성 막는 것을 최후의 수단으로만 사용하도록 권고하고 있습니다.&lt;br /&gt;따라서 특별한 이유(ex. 비디오 플레이어처럼 재생 중단되면 안 되는 경우)가 아니라면 기본 동작(재생성)을 그대로 두는 것이 권장되는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;  &lt;/span&gt;참고&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Manifest Android Interview&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/guide/topics/resources/runtime-changes&quot;&gt;https://developer.android.com/guide/topics/resources/runtime-changes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Android</category>
      <category>activity</category>
      <category>android</category>
      <author>yenim</author>
      <guid isPermaLink="true">https://aerimforest.tistory.com/298</guid>
      <comments>https://aerimforest.tistory.com/298#entry298comment</comments>
      <pubDate>Sun, 31 Aug 2025 15:12:19 +0900</pubDate>
    </item>
    <item>
      <title>lateinit var</title>
      <link>https://aerimforest.tistory.com/293</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Lateinit&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 내에서 변수만 Nullable로 미리 선언하고 초기화를 나중에 해야 하는 경우 lateinit 키워드를 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1716113671104&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var tmp: String? = null
tmp?.plus(&quot;1&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 null 값으로 변수를 선언할 수 있지만, 이 경우 tmp 변수를 사용하고자 하는 모든 곳에 Safe Call(?.)을 붙여줘야해서 가독성이 저하될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 lateinit을 사용하여 변수를 선언하면 Safe Call을 사용할 필요가 없어 가독성이 높아진다.&lt;/p&gt;
&lt;pre id=&quot;code_1716113696768&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;lateinit var tmp: String
tmp.plus(&quot;1&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;lateinit 특징&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. var로 선언된 클래스의 프로퍼티에만 사용 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. null값은 허용되지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 기본 자료형(int, Long, Double, Float 등) 사용 불가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 초기화 하지 않고 변수 접근 시, &lt;b&gt;NullPointerException 발생하여 앱 강제 종료 발생(매우 주의!)&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;lateinit NullPointerException 방지&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lateinit 타입의 변수 사용시 NullPointerException으로 앱이 강제 종료되는 것을 막기 위해, 초기화전에 해당 변수에 접근하게 되는 경우가 있다면(ex. onPause, onStop 등) 반드시 초기화 여부를 체크한 뒤에 해당 변수에 접근해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1716113758468&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;lateinit var tmp: String

if(this::tmp.isInitialized) {
   // tmp 변수 접근  
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Programming Languages/Kotlin</category>
      <category>Kotlin</category>
      <category>lateinit</category>
      <author>yenim</author>
      <guid isPermaLink="true">https://aerimforest.tistory.com/293</guid>
      <comments>https://aerimforest.tistory.com/293#entry293comment</comments>
      <pubDate>Sun, 19 May 2024 19:18:21 +0900</pubDate>
    </item>
    <item>
      <title>Git 여러 커밋 하나로 합치기</title>
      <link>https://aerimforest.tistory.com/291</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜치를 따서 작업을 한 후, master 브랜치로 합치는 과정에서 그냥 머지를 하게되면 내 브랜치에서 작업했던 여러 커밋들도 master 히스토리에 그대로 찍히기 때문에 지저분해질 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 내 브랜치의 커밋 기록을 하나로 합친 후 master로 머지한다면 커밋 히스토리를 좀 더 간결하게 관리할 수 있다.&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;1. 하나의 커밋으로 합칠 최근 N개의 커밋 선택하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 3개의 커밋을 하나로 합치고 싶다면 아래와 같이 명령어를 입력하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1692450453779&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git rebase -i HEAD~3&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번의 명령어를 입력하면 최근 3개의 커밋 기록이 vim 모드로 뜨는데, 여기서 i를 눌러 insert 모드로 진입한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 가장 위에 뜨는 커밋을 제외한 나머지 커밋들의 제일 앞에 있는 pick을 s로 변경한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 됐다면 esc를 누르고 :wq를 입력하여 파일을 저장 후 종료한다.&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;3. 새로운 커밋 메세지 작성하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N개의 커밋을 하나로 합칠때 커밋 메세지를 새로 작성할 수 있는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 커밋 메세지의 가장 앞에는 #을 붙여서 주석처리해주고,&lt;/p&gt;
&lt;pre id=&quot;code_1692450992744&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# This is combination of N commits.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에 새로운 커밋 메세지를 작성해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 원격 저장소에 수정 사항 반영하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 원격 저장소에 아래 명령어로 수정 사항을 반영해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1692451052286&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git push -f origin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끄읕.&lt;/p&gt;</description>
      <category>Git</category>
      <category>git</category>
      <author>yenim</author>
      <guid isPermaLink="true">https://aerimforest.tistory.com/291</guid>
      <comments>https://aerimforest.tistory.com/291#entry291comment</comments>
      <pubDate>Sat, 19 Aug 2023 22:18:16 +0900</pubDate>
    </item>
    <item>
      <title>Android Studio Git 계정 변경하기</title>
      <link>https://aerimforest.tistory.com/290</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1. 현재 로그인된 계정 확인(생략 가능)&lt;/p&gt;
&lt;pre id=&quot;code_1692441407547&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git config user.name
git config user.email&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;/p&gt;
&lt;pre id=&quot;code_1692441455819&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git config --global user.name 변경할 계정
git config --global user.email 변경할 이메일&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Git</category>
      <category>AndroidStudio</category>
      <category>git</category>
      <category>안드로이드스튜디오</category>
      <author>yenim</author>
      <guid isPermaLink="true">https://aerimforest.tistory.com/290</guid>
      <comments>https://aerimforest.tistory.com/290#entry290comment</comments>
      <pubDate>Sat, 19 Aug 2023 19:38:30 +0900</pubDate>
    </item>
  </channel>
</rss>