[android] findViewById의 문제점, 그리고 View Binding

Juyeong·2022년 7월 3일
3

Android

목록 보기
2/2
post-thumbnail

1. findViewById

1) 메소드 동작 방식

지금까지 앱을 만들며 가장 많이 사용해왔던 방식입니다. xml 파일에서 정의했던 layout Id를 통해 View를 참조하는 가징 기본적인 방식입니다.

val myName = findViewById<TextView>(R.id.my_name)
val myAge = findViewById<TextView>(R.id.my_age)

위와 같이 findViewById 메소드를 통해 Activity, Fragment 등 View를 필요로하는 곳에서 자유롭게 활용할 수 있는데요.해당 메서드는 아래와 같이 동작함을 확인할 수 있었습니다.

@Nullable
public final <T extends View> T findViewById(@IdRes int id) {
    if (id == NO_ID) {
        return null;
    }
    return findViewTraversal(id);
}

findViewById 메서드가 호출되면 View Type에 해당되는 findByViewById 메서드를 호출합니다. id가 없다면 null을 반환하고, 그렇지 않다면 findViewTraversal 메소드를 호출합니다.

protected <T extends View> T findViewTraversal(@IdRes int id) {
    if (id == mID) {
        return (T) this;
    }
    return null;
}

findViewTraversal 메소드를 통해 최종적으로 id값의 일치를 확인하여 null을 반환하는 것을 확인할 수 있습니다. 여기서 생각해볼 점은 Activity의 RootView를 불러오는 방식입니다.

setContentView(R.layout.main_activity, null)
val rootView = findViewById(R.id.rootView)

만약 RootView를 findViewById 메소드를 통해 RootView 객체를 담는 방식을 사용한다면 rootView의 id값이 일치하는 지에 대한 확인만 이루어지고, rootView의 하위 뷰에 대한 정보를 얻을 수 없습니다. 하지만 이런 생각과는 달리 실제로는 rootView의 하위 뷰를 잘 받아와서 사용할 수 있습니다.

그 이유는, rootView는 View를 상속받은 ViewGroup이며, ViewGroup은 findViewTraversal 메소드를 오버라이드하고 있기 때문입니다.

if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
      v = v.findViewById(id);
            
      if (v != null) {
          return (T) v;
      }
 }

위의 코드는 ViewGroup의 findViewTraversal 메소드 내부 로직 중 일부로, ViewGroup의 하위 뷰들에 대해 다시 findViewById 메소드를 호출하는 방식입니다. 위와 같은 동작방식을 시각화하면 아래와 같습니다.

2) findViewById 단점

1️⃣ 성능 측면에서 무거움
Traversal은 순회를 뜻합니다. 위의 코드와 의미에서 유추해볼 수 있듯이, ViewGroup에 할당되어있는 모든 View에 대해 순회하며 id값을 확인하게 됩니다.

즉, ViewGroup 내부에서 탐색해야 할 View의 양이 많아지면 많아질수록 Activity를 실행될 때 성능적으로 저하될 것입니다. RootView를 참조하는 경우 외에 개별적으로 하위 View를 참조하는 경우에도 성능이 좋지 않을 뿐더러 코드가 지저분해질 수 있습니다.

2️⃣ Null-Safety에 위배됨
findViewTraversal 메소드에서 확인할 수 있듯이, 유효하지 않은 View Id로 인해 null값이 반환될 수 있습니다. 이는 실제로 코드를 작성하는 과정에서 경고를 해주지 않기 때문에 치명적인 오류를 야기할 수 있는데요.

가령, FirstActivity와 SecondActivity가 있다고 가정하겠습니다. 각 Activity xml 내에 first_text, second_text id로 TextView가 있다고 해봅시다.

class FirstActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_first)
         val firstText = findViewById<TextView>(R.id.second_text)
    }
}

위 코드와 같이 FirstActivity xml 내에 second_text id에 해당하는 View가 없음에도 해당 Id를 참조하여 빌드할 수 있습니다. 즉, 클래스에 있는 필드의 유형이 xml 파일에서 참조하는 뷰와 일치하지 않는 경우에도 컴파일 시간에 에러가 발생하지 않습니다.

위 코드의 firstTextnull 값을 반환할 것입니다.

이 외에도 사용하고자 하는 모든 View에 대해 find 메소드를 호출해야하는 번거로움이 있고, 사용 편의성이 매우 떨어지기 때문에 많은 개발자들이 findViewById를 지양하고 있습니다. 그렇다면 어떤 방식으로 View를 참조해야할까요?

2. View binding

findViewById의 대체로써 View Binding을 사용할 수 있습니다. view binding을 사용하고자 하면 build.gradle에서 다음과 같이 설정하면 됩니다.

android {
        ...
        viewBinding {
            enabled = true
        }
    }
    

위와 같이 설정했다면 이제 프로젝트의 xml 레이아웃 파일에 대해 binding 클래스가 생성됩니다. 여기서 binding 클래스는 RootView의 하위 View와 Id를 가지고 있는 모든 View들의 참조가 포함됩니다.

binding 클래스는 자동적으로 xml파일의 이름을 카멜 표기법으로 변환하고 뒤에 'Binding'을 붙여 생성합니다. 예를 들어 activity_main.xml 의 경우, ActivityMainBinding 이라는 이름으로 binding 클래스가 형성됩니다.

1) Activity에서의 View binding

이제 Activity에서 View binding을 정의하고 예제를 통해 확인해보겠습니다.

    private lateinit var binding: ResultProfileBinding

    override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        binding = ResultProfileBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
    }
    

위와 같이 Binding 클래스를 선언해주고, onCreate에서 초기화해줍니다. 여기서 xml에 id가 user_name 이라는 textView가 있다고 가정해보겠습니다.

val userName = findViewById<TextView>(R.id.user_name)
userName.text = "dejavu"

기존에 findViewById 방식을 사용하여 textView에 해당하는 id를 참조하고 textView를 변경하면 위와 같습니다. 다음은 binding 방식을 적용한 코드입니다.

binding.userName.text = "dejavu"

훨씬 더 코드가 간결하고 깔끔해진 것을 확인할 수 있습니다. 지금은 비록 하나의 TextView에 대한 예제이지만 프로젝트 규모가 매우 크고 한 Activity 내에 수십개의 View가 있을 것을 생각하면 훨씬 더 효율적인 방식일 것입니다.

2) Fragment에서의 View binding

다음으로 Activity가 아닌 Fragment에서 View binding을 사용해보겠습니다.

  private var _binding: MainFragmentBinding? = null

    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = MainFragmentBinding.inflate(inflater, container, false)
        val view = binding.root
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
    

위 코드를 보면 Activity와는 달리 onDestoryView() 메소드 내에서 binding 변수를 null로 만들어주는 것을 확인할 수 있습니다. 위와 같은 방식은 안드로이드 공식 문서에서 제시하는 방식인데요. 공식 문서에는 다음과 같은 내용도 확인할 수 있습니다.

"Fragments outlives their views."

fragment 생명주기에 의하면 fragment내의 view보다 fragment가 더 오래 지속된다는 의미입니다. 즉 fragment view가 종료되었음에도, fragment는 계속 살아있기 때문에 메모리 누수가 발생할 위험이 있습니다. 이러한 이유 때문에 onDestoryView() 메소드 내에서 binding을 해제해주는 것입니다.

이번 포스팅을 통해 findViewById 대신 View Binding을 사용하여야 하는 이유에 대해 알아보았습니다. 다음 포스팅에서는 View Binding과 Data Binding의 차이에 대해 알아보겠습니다.

📑 참고자료
안드로이드 액티비티의 View 정보 구하기
[내 맘대로 정리한 안드로이드] findViewById의 사용을 최소화해야 하는 이유와 대체 방법(ViewBinding)
Android) Fragment에서 View Binding 문제점, 제대로 사용하기

profile
ios / Android developer 💻

0개의 댓글