본문 바로가기
안드로이드/기술

[android] Context를 가지는 객체를 singleton으로 관리하기

by 코드 이야기 2023. 5. 22.
728x90

 

아래의 코드들을 실행시키기 위한 MainActivity.kt와 activity_main.xml

더보기
class MainActivity : AppCompatActivity() {

    private val settingSwitch: SwitchCompat by lazy { findViewById(R.id.notification_setting_switch) }
    private val settingTv: TextView by lazy { findViewById(R.id.textView) }

    private val alarmSetting: NotificationSetting by lazy { NotificationSetting.getInstance(this) }

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

        settingSwitch.setOnCheckedChangeListener { compoundButton, checked ->
            alarmSetting.isEnable = checked
            settingTv.text = alarmSetting.isEnable.toString()
        }
    }
}
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.SwitchCompat
        android:id="@+id/notification_setting_switch"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="알림 설정"
        app:layout_constraintBottom_toTopOf="@+id/textView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/notification_setting_switch" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

싱글톤: 하나의 클래스가 오직 하나의 객체 인스턴스만 가지는 것

 

가정1 : 불필요한 객체 생성

  • 알람에 대한 설정값을 관리하는 SharedPreferences가 있습니다.
  • 이 SharedPreferences는 한 곳에서만 사용되는 것이 아니기 때문에 재사용성을 위해 SharedPreferences를 관리하는 클래스를 만들고 싶습니다.
  • 아래와 같이 간단하게 구현할 수 있습니다.
class NotificationSetting(private val context: Context) {
    private val prefs: SharedPreferences =
        context.getSharedPreferences(SETTING_PREFERENCES_KEY, MODE_PRIVATE)

    var isEnable: Boolean
        get() = prefs.getBoolean(NOTIFICATION_KEY, false)
        set(value) = prefs.edit().putBoolean(NOTIFICATION_KEY, value).apply()

    companion object {
        private const val SETTING_PREFERENCES_KEY = "setting_preferences_key"
        private const val NOTIFICATION_KEY = "notification_key"
    }
}

하지만, 여러개가 필요하지 않은 객체임에도 여러개가 생성될 수 있습니다.

 

 

가정2 : 메모리 누수

  • 아래와 같이 간단하게 싱글톤으로 변경할 수 있습니다.
class NotificationSetting private constructor(private val context: Context) {
    private val prefs: SharedPreferences =
        context.getSharedPreferences(SETTING_PREFERENCES_KEY, MODE_PRIVATE)

    var isEnable: Boolean
        get() = prefs.getBoolean(NOTIFICATION_KEY, false)
        set(value) = prefs.edit().putBoolean(NOTIFICATION_KEY, value).apply()

    companion object {
        private const val SETTING_PREFERENCES_KEY = "setting_preferences_key"
        private const val NOTIFICATION_KEY = "notification_key"

        private var instance: NotificationSetting? = null

        fun getInstance(context: Context): NotificationSetting {
            return instance ?: synchronized(this) {
                instance ?: NotificationSetting(context).also {
                    instance = it
                }
            }
        }
    }
}

하지만, 외부에서 들어온 context(Activity)를 더이상 사용하지 않더라도 해당 싱글톤 객체에서 참조하고있기 때문에 메모리 누수가 발생합니다.

 

해결1 : context를 프로퍼티로 가지지 않는다.

  • 프로퍼티로 가지지 않는다면, 생성자로 받은 context를 참조할 필요가 없어지고, 메모리 누수도 없습니다!
class NotificationSetting private constructor(context: Context) {
    private val prefs: SharedPreferences =
        context.getSharedPreferences(SETTING_PREFERENCES_KEY, MODE_PRIVATE)

    var isEnable: Boolean
        get() = prefs.getBoolean(NOTIFICATION_KEY, false)
        set(value) = prefs.edit().putBoolean(NOTIFICATION_KEY, value).apply()

    companion object {
        private const val SETTING_PREFERENCES_KEY = "setting_preferences_key"
        private const val NOTIFICATION_KEY = "notification_key"

        private var instance: NotificationSetting? = null

        fun getInstance(context: Context): NotificationSetting {
            return instance ?: synchronized(this) {
                instance ?: NotificationSetting(context).also {
                    instance = it
                }
            }
        }
    }
}

하지만, 근본적인 해결 방법이라고는 할 수 없겠죠…

 

 

해결2 : application context를 사용한다.

  • context를 주입받는 싱글톤 객체가 메모리 누수 문제가 발생하는 이유는 프로그램 실행 중 사라질 수 있는 Context(ex. ActivityContext)를 참조할 수 있기 때문입니다.
  • 그렇다면 최상위에 위치한 application context를 참조하면 됩니다.
class NotificationSetting private constructor(
    context: Context
) {
    private val prefs: SharedPreferences by lazy { context.getSharedPreferences(SETTING_PREFERENCES_KEY, MODE_PRIVATE) }

    var isEnable: Boolean
        get() = prefs.getBoolean(NOTIFICATION_KEY, false)
        set(value) = prefs.edit().putBoolean(NOTIFICATION_KEY, value).apply()

    companion object {
        private const val SETTING_PREFERENCES_KEY = "setting_preferences_key"
        private const val NOTIFICATION_KEY = "notification_key"

        private var instance: NotificationSetting? = null

        fun getInstance(context: Context): NotificationSetting {
            return instance ?: synchronized(this) {
                instance ?: NotificationSetting(context.applicationContext).also {
                    instance = it
                }
            }
        }
    }
}

 

 

 

 

 

 


참고

『안드로이드 프로그래밍 Next Step』, 255p~258p

728x90

댓글