TEKHIT ANDROID SCHOOL

[TEKHIT] 오늘의 멘토링

빨주노초잠만보 2024. 2. 14. 00:30

멋쟁이 사자처럼에서 진행하는 안드로이드 앱 스쿨에서 멘토링을 받고 배운 지식들을 정리하는 글입니다. 

📕 질문

 갤러리 관련 권한을 요청할 때 아래 코드와 같이 요청합니다. PermissionManagement를 따로 만든 이유는 여러 화면에서 갤러리를 접근할 때마다 권한이 있는지 확인하기 때문에 액티비티나 프래그먼트 마다 권한을 검사하는 코드가 완전히 중복되어 최대한 중복되는 부분을 줄이고자 만들었습니다.

 

제가 할 수 있는 최대한으로 만들어봤는데 여기서 다른 더 좋은 방법이 있는지 알고 싶습니다.

class PermissionManagement(private val context: Context) {

    companion object{

        val REQUEST_IMAGE_PERMISSIONS =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
                arrayOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                arrayOf(Manifest.permission.READ_MEDIA_IMAGES)
            } else {
                arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
            }

        val REQUEST_LOCATION_PERMISSIONS = arrayOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION,
        )
    }

    fun isPermissionGranted(permission: String): Boolean {
        return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
    }
    fun isImagePermissionGranted(): Boolean = REQUEST_IMAGE_PERMISSIONS.any { isPermissionGranted(it) }

    fun isLocationPermissionGranted(): Boolean = REQUEST_LOCATION_PERMISSIONS.any { isPermissionGranted(it) }

    fun showPermissionSnackBar(view: View) {
        Snackbar.make(view, "권한이 거부 되었습니다. 설정(앱 정보)에서 권한을 확인해 주세요.",
            Snackbar.LENGTH_INDEFINITE
        ).setAction("확인"){
            //설정 화면으로 이동
            val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
            val packageName = context.packageName
            val uri = Uri.fromParts("package", packageName, null)
            intent.data = uri

            context.startActivity(intent)

        }.show()
    }
}

 

📖 이에 대해 멘토링에선 다음과 같은 답변을 얻을 수 있었습니다.

 현재 클래스는 REQUEST_IMAGE_PERMISSIONS 등의 상수를 사용하며 메소드만으로 이루어져 있고 프로퍼티가 없습니다. 이렇게 클래스의 인스턴스가 생성되었을 때 내부의 변동 없이 특정한 기능만을 하는 클래스는 싱글톤으로 만들면 객체 지향적 관점에서 더욱 분명한 코드가 될 수 있습니다.

                  Singleton Pattern : 클래스의 인스턴스를 생성할 때 단 하나의 인스턴스만 생성

코틀린에선 object Class를 통해 Singleton을 구현할 수 있습니다. 하지만 위 코드는 확장 함수를 통해서도 구현할 수 있기 때문에 코틀린에서 권장되는 패턴은 아닙니다. 확장 함수는 실제로 어떤 클래스의 실제 멤버는 아니지만 멤버인 것 마냥 호출할 수 있기 때문에 기존 클래스를 확장할 수 있어서 개방/폐쇄 디자인 원칙(Open Close Principle)을 지원할 수 있습니다. 이건 좀 더 공부를 해야 봐야겠네요 호호호🤔

 

위 클래스의 메소드를 보면 Context가 필요한 메소드들이 있습니다.  Object Class는 매개 변수를 가질 수 없기 때문에 각 메소드에 마다 매개 변수로 전달해줘야 합니다. 하지만 저는 이 메소드들을 Activity에서 사용하기 때문에 한 가지 문제점이 있었습니다. 

 

안드로이드에서 싱글톤, ViewModel처럼 Activity보다 오래 살아있을 수 있는 가능성이 있는 요소들은 내부 함수가 아직 종료되지 않은 상태에서 액티비티가 종료되는 경우가 발생할 수 있습니다. 이런 경우 Activity의 Context를 가진다면 Activity에 대한 참조를 메모리에 남겨두어 Memory Leak이 발생할 수  있습니다. 그래서 Acitivity의 생명 주기보다 오래 살아있거나 앱 전역에서 사용될 경우 applicationContext를 사용해야 합니다.

 

그래서 저는 이후 다른 클래스에서도 사용하기 위해  applicationContext를 제공하는 싱글톤 클래스를 만들었습니다.

class App: Application() {
    init {
        instance = this
    }

    companion object{
        private var instance: App? = null
        fun context(): Context {
            return instance?.applicationContext!!
        }
    }
}

 

최종 완성된 코드입니다. 확장 함수를 사용한 방법도 연구해 봐야겠네용 

object PermissionManagement {
    private fun getContext(): Context = App.context()

    val REQUEST_IMAGE_PERMISSIONS =
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            arrayOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            arrayOf(Manifest.permission.READ_MEDIA_IMAGES)
        } else {
            arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
        }

    val REQUEST_LOCATION_PERMISSIONS = arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION,
    )

    fun isPermissionGranted(permission: String): Boolean {
        return ContextCompat.checkSelfPermission(getContext(), permission) == PackageManager.PERMISSION_GRANTED
    }
    fun isImagePermissionGranted(): Boolean =
        REQUEST_IMAGE_PERMISSIONS.any { isPermissionGranted(it) }

    fun isLocationPermissionGranted(): Boolean =
        REQUEST_LOCATION_PERMISSIONS.any { isPermissionGranted(it) }

    fun showPermissionSnackBar(view: View) {
        Snackbar.make(view, "권한이 거부 되었습니다. 설정(앱 정보)에서 권한을 확인해 주세요.",
            Snackbar.LENGTH_SHORT
        ).setAction("확인"){
            //설정 화면으로 이동
            val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
            val packageName = getContext().packageName
            val uri = Uri.fromParts("package", packageName, null)
            intent.data = uri

            getContext().startActivity(intent)

        }.show()
    }
}

🚨 requireContext의 비밀

코틀린의 !! 연산자는 NullpointerException을 발생시킵니다. 이는 NullpointerException 은 어플리케이션을 크래쉬 시킨다는 문제도 있지만 가장 큰 문제점은 코틀린의 장점은 널에 유연한 대처가 가능하다는 점인데 이 의도에 어긋납니다.

 

그럼 이게 requireContext( ) 와 무슨 상관일까요? requireContext( ) 의 내부 구현을 한번 살펴 보겠습니다.

내부 구현을 보면 context가 null 일 때 IllegalStateException을 일으키며 앱을 크래쉬 시킵니다. !! 연산자와 비슷한 동작을 하는거죠 ! 멘토님께 이 내용을 듣고 정말 놀랐습니다. Android Studio에서 대놓고 requireContext( ) 를 써서 에러를 고치라는 문구를 보여주니까 당연히 이걸 쓰는게 맞겠거니 해서 써왔는데 !!!! 심지어 프로젝트의 Fragment의 컨텍스트가 필요한 거의 모든 부분에 requireContext( )를 사용했는데 !!! 물론 requireContext를 수행했을 때 에러 날 일이 거의 없겠지만 굉장히 놀라웠습니다

🙇‍♂️ 후기

정말 멘토링을 할 때마다 많은 것들을 배웁니다. 너무 소중한 배움이었습니다. 이러니 멘토링을 안 할 수가 있나~ 한 가지 아쉬웠던 점은 항상 멘토링을 하는 분들만 하는 게 주제 넘지만 좀 안타..깝다... ? 라고 느꼈습니다. 이렇게 정말 아니 진짜 진짜 진짜 엄청난걸 배워갈 수 있는데 이 좋은 기회들을 놓치고 계신 게 너무 아깝다는 생각이 들었습니다. 아뇨 ? 하지마세요 제가 다할 거예요

 

그리고 제가 지금껏 열심히 프로젝트를 계속 했기 때문에 코루틴이든 StateFlow든 러닝 커브가 조금 있는 기술도 깊게 물어볼 수 있었던 거 같고 또 이해를 할 수 있었던 것 같습니다. 뿐만 아니라 아키텍처적으로도 계속 관심을 가지고 한 게 멘토님께서 아키텍처적 관점으로 하시는 말씀들을 다 이해할 수 있었고 무엇보다 그런 아키텍처에 관한 얘기들이 너무 재미있더라고요. 아키텍처를 살짝 찍먹 해본 결과 이건 몇 번 외우고 공부한다고 되는 게 아닌 거 같아서 이런 중간중간 알아가는 지식들이 쌓이고 쌓여 나중에 아키텍처에 대한 공부를 하게 됐을 때 큰 도움이 될 것 같습니다. 

 

흔히 하는 말들 중에 준비된 자가 기회를 얻는 말이 있는데 이럴 때 쓰는 말이 아닌가 싶네요. 앞으로도 멘토님들 많이 많이 괴롭혀야겠습니다. 앱 스쿨 과정이 이제 약 4개월 남았는데 뭔가 벌써 짧게 느껴지는 기분이라 하루 하루 열심히 달려보려 합니다 퐈이팅!!🔥