viewBinding (lazy, lateinit)

jericho·2024년 1월 3일

Android

목록 보기
2/15

viewBinding은 findViewById를 대체한다.
뷰바인딩은 fvbi와 비교해서 세 가지 장점이 있다.

  1. null safety
    다른 액티비티의 뷰를 잘못 가져와서 널포인터로 터지는 문제가 없다.
  2. type safety
    타입을 잘못지정하여 터지는 문제가 없다.
  3. 일일이 아이디를 가져와 변수 등록할 필요가 없다.

뷰바인딩은 각 액티비티의 레이아웃 xml에 대해 바인딩 클래스를 생성하고, ID가 있는 모든 뷰의 직접 참조가 포함되어, 사용할 때에 binding 변수에서 ID에 접근해 사용하는데, 여기에 타입 정보까지 있으므로, 잘못된 아이디를 접근하거나 다른 액티비티에 있는 아이디를 잘못 접근하여 널이 발생할 위험도 없고, 타입을 잘못 지정할 위험도 없는데다, 뷰 각각의 변수 등록 과정도 없는 것이다.

다만 이렇게 작동하려면 파일 이름이 제대로 포맷에 맞게 관리가 되어야만 작동하는 시스템인 것 같다. kt 파일과 xml 파일이 이름 규칙이 틀리거나, 혹은 다르게 사용할 수밖에 없는 경우가 있다면 어떻게 될지 모르겠다.

뷰바인딩 사용법은 아래와 같다.

// build.gradle.kts (Module :app)
viewBinding { enable = true }

// MainActivity
private lateinit var binding: ActivityMainBinding

// super.onCreate(savedInstanceState) 아래에
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

// binding 사용 예
binding.btn1.setOnClickListener {
    binding.tv1.text = "바인딩이 잘 되었네요~~"
}

fvbi 사용할 때는 by lazy로 쓰는 것을 선호했다. lateinit 하면 사용하기 전에 초기화 해주거나, onCreate에서 모두 초기화해주어야 하는데, 등록할 변수가 많아지면 코드 라인을 많이 잡아먹고 누락 등의 문제도 있어서 by lazy로 코드 파편화도 막고 코드 라인도 줄이는 것이 낫다는 판단이다.

반면 뷰바인딩은 setContentView에 R.layout.activity_ooo 로 주어지는 int값 layoutResID 대신, binding.root로 바탕이 되는 constraintLayout 등을 View 매개변수의 인자로 전달해야 한다. 그래서 초기화 위치가 onCreate으로 고정되어, binding에 ActivityOooBinding.inflate(layoutInflater)을 넣고 setContentView(binding.root)를 한다.

lateinit var를 써야하는 이유라고 생각하며 정리했는데, 다시 생각해보니 by lazy 하고 binding 초기화 없이 setContentView(binding.root) 해도 되긴 한다. 실제로 이 방법도 많이 쓰이는 것 같다.

하지만 fvbi와 달리 by lazy가 아닌 lateinit을 써도 초기화 위치가 고정적이고 한 줄만 추가된다. 게다가 Fragment에서 뷰바인딩을 적용할 때에는 onCreateView에서 매개변수로 넘겨받아야 하기 때문에 by lazy를 적용할 수 없다는 것 같다. 그러니 사용 방법의 통일 측면에서 lateinit을 사용하는 것이 좋을 것 같다.

이외에, by lazy는 lateinit보다 성능상 미세하게 불리하지 않을까 하는 생각이다. by lazy는 변수에 접근할 때마다 초기화가 되었는지를 체크해야 할 것이고, lateinit은 초기화 여부를 체크하지 않을 것이다. 그렇다면 매우 빈번하게 접근해야 할 변수의 경우에는 성능차이가 나타날 수도 있지 않을까.

이에 대해 튜터님께 질문을 드렸더니, 상관 없다고 하셨다. 그보다는 메모리 접근을 나중에 하는 것이 핵심이라는 것이다. 확실히 겨우 분기 한 번짜리를 고민할 필요는 없을 것 같고, 이거나 저거나 지연초기화가 핵심이다.


240105 추가
Lazy는 기본적으로 동기화 되어있다. 초기화가 프로퍼티를 사용하는 곳과 항상 같은 스레드에서 일어남이 확실하다면 LazyThreadSafetyMode.NONE 하여 관련된 오버헤드를 없앨 수 있다고 한다.

이걸 보면 확실히 by lazy는 단순히 변수에 접근하는 것 외의 오버헤드가 있다는 것이다.
(그리고 단순히 변수가 아니라, Lazy라는 애가 따로 있어서 얘가 변수를 들고 있는 것 같다.)

By default, the evaluation of lazy properties is synchronized.
...
If you're sure that the initialization will always happen in the same thread as the one where you use the property, you can use LazyThreadSafetyMode.NONE. It doesn't incur any thread-safety guarantees and related overhead.

반면 lateinit은 변수 사용 시 null 체크만 하고 가져오는 모양이다.

@NotNull
public final Area getArea() {
   Area var10000 = this.area;
   if (var10000 == null) {
      Intrinsics.throwUninitializedPropertyAccessException("area");
   }

   return var10000;
}

 

by lazy의 오버헤드가 미미할 거라고 생각은 하지만, lateinit에 비하면 미세하게나마 느릴 것 같다.


240121 추가
팀프로젝트를 진행하다보니, 코드 파편화가 없이 만드는 쪽이 훨씬 안전하다고 생각이 된다. lateinit var로 만들어놓고 반드시 onCreate에서 초기화를 해주어야 하는 것보다, val by lazy로 같은 위치에 초기화 코드를 작성해두는 것이 혹시라도 모를 초기화 누락을 없앨 수 있고, 나중에 코드를 읽게 되더라도 해당 값의 초기화 값이 어떻게 되는지 찾기 위해 문서를 헤집어봐야 하는 일이 없다. lazy를 쓰는 것에 성능상 문제가 전혀 없을 것 같기에, 개발 편의성과 나중에 다시 볼 때 혹은 타인의 코드를 볼 때를 위해 lazy를 쓰는 것이 답이라고 생각이 된다.

그리고 추가적으로, lateinit은 어쨌거나 변수를 만들고 초기화만 나중에 하는 것인 반면, lazy는 해당 변수에 접근하기 전에는 변수를 만드는 계산 자체를 미뤄두고 있다는 점이 있다. (물론 뷰바인딩은 생성과 함께 호출되므로 해당 없긴 하다)

0개의 댓글