개발자의 적절한 공부법?(딥다이브(Deep Dive)?, XML과 Compose?)
무작정 딥다이브하는 경우가 있진 않겠지만 굳이 모든 걸 딥다이브해가면서 공부할 필요가 있을까? 그건 아니다.
그럼 무엇을 공부해야할까? 일단 시작이 중요하다. 뭔가 만들고 따라 해보고 나서 필요하면 딥다이브하는 것도 매우 좋다. 무작정 안쪽 코드 파악해서 얻어봐야 그걸 안 보고도 말할 수 있는 사람도 있다. 그럼 뭐가 중요한 것일까?
안드로이드 개발자 로드맵이라는 GDE skydoves의 번역글을 참고해도 좋다.
처음 안드로이드개발을 시작한다고 하면 무엇을 공부하는 게 좋을까? 그냥 나열해 보면
- 안드로이드
- 안드로이드 관련한 아키텍처, 안드로이드 Activity의 이해 등등
- Java
- Kotlin
- 비동기 처리(Thread, coroutines, RxJava, Flow 등)
- 운영체제
- 네트워킹
- 데이터베이스
- 알고리즘
- 등등...
이중에 안드로이드를 하기 위한 최소한을 구분한다면
- Kotlin
- 안드로이드
- 비동기 처리(Corouintes)
젯브레인과 구글에서 제공하는 자료들도 많이 있다.(언어 장벽만 없다면)
- Kotlin을 처음부터 학습할 수 있는 example을 제공한다. : https://play.kotlinlang.org/byExample/overview
- Android을 순차 학습할 수 있는 자료 :
이 외에도 수많은 학습법이 있다. 너무 많은 자료가 있으니 무엇을 봐야 할지를 모를 뿐이다.
그럴 땐 그냥 최신의 안드로이드 책을 사는 것도 방법이다. 그 책에 나온 내용을 그냥 무작정 따라 해보고 읽어보고 필요한 걸 추가로 학습하는 형태.
학습의 범위?
활용법을 알고, 원리도 알고, 딥다이브도 해본다. 다 좋다. 사람마다 학습의 방법이 다르기 때문에 어떤 이는 코드를 먼저보는 걸 좋아하고, 어떤 이는 수많은 글을 먼저 보는걸 좋아한다. 본인만의 학습 방법부터 찾는 걸 해야 한다.
필자는 보통 이런 식으로 이어간다.
- 일단 코드 사용법을 습득해서 만들어본다.
- 그다음 코드에 들어간 문서를 한번 살펴본다.
- 블로그나 GPT를 활용해 물어본다.
근데 보통 돌아가면 1번에서 끝난다. 그렇다면 다시 공부를 한다면? 2번과 3번만 진행해도 충분하다고 생각한다. 여기서 딥다이브는 없다. 동작 원리를 이해하는 것이 아니라 우린 제공받은 라이브러리를 활용하는 것이다.
안드로이드에서 가장 중요한 건 무엇일까? 당연하지만 Activity lifecycle이다. onCrate, onStart, onResume이 각각 어느 시점에 동작하는지가 중요하다. 저게 어디에서 왜 불리는지가 중요할까? 알고 싶다면 딥다이브해볼 수 있다. 하지만 앱을 만듬에 있어서 중요하지 않다.
이미지 로더의 동작을 알고 이미지로더 라이브러릴 활용할까? 아니다. Glide, Coil 등에서 제공하는 이미지 로드가 중요한 것이지 그 내부가 중요한 것은 아니다.
그럼 어느 시점에 딥다이브를 해야 할까?
딥다이브?
보통의 개발자는 이슈를 경험한 이후에 딥다이브를 선택한다. 따라가다 아 이건 라이브러리 문제이군! 을 파악할 필요가 있을 때이다. 하지만 디버깅해도 문제를 찾지 못할 경우도 있다. 눈에 보이는 UI 만 문제가 생길 때가 있기 때문이다. 그럼 내부 코드를 딥다이브해서 얻을 수 있는 건? 이슈를 해결하진 못한다. 어차피 라이브러리니깐 보고를 할 수 있다. 그리고 그게 수정되길 기다릴 수 있다. 대신 버전을 낮추는 선택을 해볼 수 있는 것이다.
예를 들어보자. 내가 이미지 로더를 만들 것이다. 그럼 단순하게 생각해 보자. 그냥 네트워크를 통해 이미지를 불러와서 화면에 뿌려주면 될 것이다. 그럼 여기서 알아야 할 부분은?
- 스레드를 사용해 이미지의 byte를 받아온다.
- bitmap으로 바꿔서 화면에 노출한다.
이 정도 생각했다면 성공한 것이다. 그럼 다른 이미지 라이브러리는 어떻게 되어있을까? 궁금하지 않을까? 이럴 때 딥다이브해보면서 다른 라이브러리의 동작도 파악해 보는 것이다. 그냥 무작정 뭔지도 모르는데 파보는 것이 아니다.
그렇다면 이미지 로더를 만들 이유가 없다면? 그럼 굳이 파보지 않는다. 그걸 파보았을 때의 얻을 수 있는 것이 있을까? 당장은 없다. 그냥 나의 호기심을 해소할 수 있다. 그걸로 만족한다면 충분하다. 그리고 거기서 얻은 키워드를 가지고 나중에 써먹을 수 있다면 최고다.
개인적으로 딥다이브해서 얻었던 포인트를 가지고 다음에 사용하거나, 말할 수 있을 때가 좋았다. 단순히 사용하는 것이 아닌
참고로 이미지 로더의 동작은 아래와 같다.
- 이미지를 읽어오는 순서는?
- 네트워크 활용이 가장 늦은 이유는 로컬에 있다면 그걸 활용하는 게 가장 좋은 선택지이기 때문이다.(첫 시도시에는 아래와 같을 수 있다(참고용))
- 이미지를 디스크 캐시에서부터 읽어본다)
- 메모리에서 읽는다
- 다음 네트워크를 통해 읽어온다.
- 이미지를 메모리에서 내릴 때는?
- 가장 최근에 사용한 이미지는 지속 사용 가능성이 있으니 메모리 최상단에 올 수 있도록 우선순위를 높인다.
- 메모리 우선순위가 떨어지면 다음에 활용할 수 있도록 디스크에 저장한다.
근데 어디서 많이 본 것 같지 않은가? Java의 GC와 유사한 것 같지 않은가? GC도 유사한 방식이다. 알고리즘은 당연히 다르겠고, 알고리즘에 따라 처리도 다르겠지만 기본형태는 유사하다. 이렇게 유추하는 것도 방법이다. 이는 딥다이브해서 얻는 지식이 아니다.
유사한 걸 찾는 즐거움부터 느껴보면 더 많은걸 더 빠르게 파악할 수 있다. 반대로 사용하기 너무 어려웠던걸 간단하게 바꾼 라이브러리들도 있다. SQLite의 쿼리문을 직접 적고, 버전 관리도 복잡하던걸 Room으로 자동화했다. Dagger의 사용이 너무 복잡했는데 Hilt를 사용해 많은 공수를 줄였다. Room을 더 잘 쓰고 싶다고 SQLite의 사용법을 알아야 하는 건 아니다. Hilt가 왜 이렇게 동작하는지 Dagger를 이해하면 도움은 될 수 있지만 주입이란 개념이 우선이다.
GDE 안성용님이 작성한 Dagger hilt 내부코드 분석: Advanced
GDE Pluu 님이 작성한 Fragment Lifecycle과 LiveData
이와 같이 Hilt 내부 코드 분석을 해본걸 흔적으로 남겨둘수도 있고, 필요에 의해 동작을 분석하고 해결법을 제시하는 글도 남겨볼 수 있다.
정말 필요한 개념이고, 호기심이 가득하다면 사용법부터 익히고 다음으로 넘어가도 충분하다.
XML과 Compose 중에 난 무엇을 공부해야 할까?
최근 가장 많이 보이는 공부법 중에 하나로 보이는데?
컴포즈는 필수이니 꼭 학습해야 한다!?
필수일 수도 있고 아닐 수도 있다. 당신이 선택한 회사가 둘 중 뭘 쓸지 어떻게 알 수 있을까? 사실 모른다. 눈에 보이는 건 컴포즈일 것 같아 보일 수 있지만(레이아웃 보기로 padding 영역을 확인할 수 있다.(padding 영역이 분홍색으로 보인다.)) 당신의 안드로이드 경력보다 그 서비스의 개발 시기가 더 길 수 있다. 그 말은 레거시 가득한 코드가 많을 수 있단 소리다.
- XML과 컴포즈 둘다 사용하는 회사도(전환 중)
- 컴포즈만 사용하는 회사도
- 아직 XML만 쓰는 회사도
이력서에 컴포즈 밖에 안 해봤어요라면 서류 탈락을 맛 볼 수 있다. XML로 코드 짜고 있는데 컴포즈만 할 줄 아는 사람을 뽑을 것인가?는 선택지가 좁아진다. 결국 둘 다 공부해야 한다. 아직은 둘다 학습하고 알고있어야 한다.
이현우님이 작성한 Efficient Layout(View Based) in Android
알아야 할 부분의 차이와 공통점은?
XML과 Compose에서 얼마나 학습해야 할 부분의 차이가 발생하는지 한번 살펴보자. 대표적인 리스트를 뿌려주는 RecyclerView와 Compose LazyColumn을 각각 학습한다면 어떤 부분을 알아야 할지 나열해 보았다.
RecyclerView vs LazyColumn
RecyclerView로 화면에 N개의 리스트를 만든다면?
- XML을 다룰 수 있어야 한다.(컴포즈로도 할 순 있지만 재사용의 장점을 가진 리사이클러뷰와 함께 사용 시 본래의 성능을 내진 않는다)
- 데이터의 흐름을 이해하고, 화면의 데이터를 처리할 수 있어야 한다.
- RecyclerView.Adapter 또는 ListAdapter를 다룰 수 있어야 한다.
- ListAdapter에서는 DiffUtil이 필수로 사용하도록 해뒀을 뿐 RecyclerView.Adapter와 동일하다.
- onCreateViewHolder, onBindViewHolder의 동작을 이해해야 한다.
- N개의 리스트를 이해하려면 ViewType도 이해해야 한다.
- ViewType을 적절하게 활용하면 여러 개의 ViewHolder 타입을 가질 수 있다.
- sealed interface을 활용해 다양한 타입의 아이템을 구성하고, 하나의 리스트로 처리한다.
- 또는 ConcatAdapter를 활용하는 것도 방법
- ViewBinding 또는 DataBinding 활용을 이해해야 한다.
- DiffUtil을 이해하려면 equals/hashCode를 이해해야 한다.
- RecyclerView가 재사용된다는데 왜 재사용하고, Data를 어떻게 관리해야 하는지 이해해야 한다.
- 보통 RecyclerView를 사용하면서 좋아요 같은 리스트의 이벤트를 처리에 어려움을 겪는데, 데이터 흐름을 잘 파악해야 이해가 쉽다.
- 리스트에 좋아요를 하더라도, item에는 반영되어있지 않다면 스크롤 업/다운 시 문제가 생긴다.(당연하다)
- ViewHolder에서 데이터의 업데이트를 어떻게 할 것인지?
- DiffUtil 사용 시 리스트가 바뀌지 않는 경우가 있는 데에 대한 대응
코드로 RecyclerView 구현부를 적어보면
class SampleAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun getItemViewType(position: Int): Int {
return getItem(position).viewType
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
when (viewType) {
VIEW_TYPE_IMAGE -> TextViewHolder(parent)
else -> ImageViewHolder(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (getItemViewType(position)) {
VIEW_TYPE_TEXT -> (holder as? TextViewHolder)?.bindView(getItem(position))
VIEW_TYPE_IMAGE -> (holder as? ImageViewHolder)?.bindView(getItem(position))
}
}
companion object {
const val VIEW_TYPE_TEXT = 1
const val VIEW_TYPE_IMAGE = 2
}
}
RecyclerView.Adapter 만을 구현한 부분이다. 하지만 이외에도 구현해야 할 코드는 상당히 많다.
- Data를 주입해 주는 부분
- View와 상호작용을 처리하기 위한 부분(ViewHolder)
Compose LazyColumn, LazyRow을 사용한 N개의 리스틀 만든다면
- LazyColumn, LazyRow를 그냥 화면에 구현한다.
- sealed interface을 활용해 다양한 타입의 아이템을 구성하고, 하나의 리스트로 처리한다.
- when 문으로 화면에 하나씩 처리한다.
Compose로 오면서 알아야 할 부분이 확 줄어들고, 코드도 상당히 짧아진다.
@Composable
fun List(items: ImmutableList<Item>) {
LazyColumn {
items(items) { item ->
when (item) {
is Item.TextItem -> {
// view
}
is Item.ImageItem -> {
// view
}
}
}
}
}
훨씬 간단하게 코드를 작성할 수 있다. Compose는 사실 아무것도 하지 않아도 아래와 같이 동작한다.
- 이전 데이터와 새로운 데이터가 같은지 다른지를 개별로 체크하고, View를 갱신한다.
- 여기서 중요한 건 데이터의 관리이다.
- 상태관리가 중요한데, remember {}를 활용한 데이터의 관리가 중요하다.
- 결국 상태관리를 하지 않더라도 화면의 갱신은 일어나고, 화면의 갱신이 일어날 때 영향을 줄이기 위한 추가적인 학습은 필요하다.
공통점 - equals/hashCode
equals/hashCode의 활용은 Java Object의 조건에 해당한다.
원문 그대로
- It is reflexive: for any non-null reference value x, x.equals(x) should return true.
- It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
- It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
- It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
- For any non-null reference value x, x.equals(null) should return false.
kotlin의 data class를 사용하면 당연히 자동으로 해준다. 하지만 RecyclerView와 corotuines flow를 활용함에 있어서도 중요하지만 컴포즈에선 더 중요한 역할을 하는 부분에 해당한다.
코드 양이 줄어들었지만, 내부에서는 서로 비교하는 방식 중 하나이다.
정리하면?
중요한 건 딥다이브도 아니고 완전 최신 기술도 아니다.
딥다이브도 중요하지 않고, 완전 최신 기술만 알아야 하는 것도 아니다. 사실 동작 원리를 이해하면 이는 어느 정도 다 보충할 수 있다. 그리고 공부할 범위는 더 넓어지고 더 쉽게 학습할 수 있다.
개인적으론 RxJava/Flow를 이해하는데 어렵지 않게 느꼈는데, 어려운 건 사용법이 너무 많다는 것이지, 결국 그 사용법이 너무 많으니 어려운 것이다. 결국 받아들이는 데는 어렵지 않았지만 사용은 어렵다.
이 둘의 공통점은 옵서버 패턴과 Stream이라는 것이다. 옵서버패턴은 누구나 다 알 수 있고, 이미 다 알고 있는 개념이다. 이 둘을 합쳐서 어려운가? 그건 아니다. 쉽게 이해하면 물이 흐르는데 물줄기를 다양하게 바꿀 수 있느냐이다. 그걸 하는데 필요한 함수가 너무 많으니 어떤 게 더 좋은 선택인지가 어려운 것이다.(결국 어렵단 이야기다)
하지만 좀 더 빠르게 파악하는 건 가능하고, 빠르게 받아들일 수 있는 것은 패턴인 것이다. 패턴을 이해하면 쉽게 접근할 수 있다.
딥다이브가 아니라 문서를 잘 보아도 알 수 있다. 그냥 궁금해서 더 파보는 것이지 그걸 메인으로 삼아서 딥다이브만 하는 건 의미 없다고 생각한다.
기본과 내가 학습한 내용이 어떻게 이어지는지를 잘 파악하면 더 쉬운 개발을 하면서 즐거울 수 있다. 시간 역시 줄일 수 있다.
그러려면 가장 쉬운 쓰는 법부터 아는 게 중요하다고 생각한다.
자바는 모두 객체이다. Activity 조차 객체로 움직인다.