"진짜 CS 마스터 해야지..."
"아 다음엔 진짜 날 잡아서 CS 공부 빡세게 한다..."
매번 이렇게 다짐하지만 막상 CS 공부하는게 쉽지 않다.
안드로이드 개발 공부할 것도 너무 많은데...

(가입해야 하나...)
CS가 실제 개발에서 중요할까?
난 안드로이드 개발하면서 알고리즘, OS는 조금 필요하다는 것을 느낀다.
RecyclerView에 표현할 리스트를 다룰 때는 삽입 정렬, 해쉬를 고려하는 경우가 많고,
아주 가끔 "앱이 언제 꺼질까?"를 생각할 때, OS 책에서 배운 프로세스와 메모리 구조를 떠올리긴 한다.
이 상황 외에는 CS지식이 필요한 적은 없었던 것 같다.
특히 공룡책과 같은 수많은 페이지의 전공 서적을 볼 때면, "이정도 까지 필요하나?"라는 생각이 많이 든다.
또 안드로이드에선 네트워크 지식이 필요한 경우가 거의 없어 매번 했갈리고 까먹고는 한다.
그럼에도 많은 기업들은 CS 지식을 물어보곤 한다.
사실 나도 CS를 공부하고 나서 까먹을 뿐이지, 다시 공부하다 보면 내가 작성한 코드들에는 CS지식이 기반이 되는 것들이 많다는 것을 느낀다.
그 만큼 작은 부분 곳곳에 CS지식이 들어가곤 한다.
그래서 이번 기회에 다시 한번 왜 우리는 CS를 공부해야 하는가에 대해 알아보자!!
"기초가 탄탄해진다."
"최적화할 수 있다."
이런 뻔한 것들 말고, 나의 경험을 토대로!
가게에 재료가 없거나, 가게 자리가 없거나, 영업일이 아니라면 "예약 불가합니다."를, 그 반대라면 "예약 시도중..."라는 문구를 띄워 주세요.
위의 요구사항을 구현하려 한다.
가장 먼저 떠오르는 것은 위의 말을 그대로 코드로 옮겨 적는 것이다. 가장 빠르고 쉽게 떠올릴 수 있는 방법이다.
if(!isStoreOpen && !isIngredientExist && !isTableExist) {
println("예약이 불가능합니다.")
}else{
println("예약 시도중...")
}
위의 코드는 문제가 없지만 단점이 있는데, !(NOT)이 3번이나 들어가 읽기 불편하다는 것이다.
!(NOT)은 정말 좋은 도구이지만, 변수의 앞에 적어야 한다.
그렇기 때문에 변수를 읽고 눈을 다시 앞으로 되돌려야 한다.
(NOT이 한국식으로 뒤에 왔다면 더 읽기 편했을 것이다.)
읽는 과정을 보자면 아래와 같다.
!(NOT)이 앞에 있으니 다시 앞으로 눈길을 옮긴다. -> isStoreOpen을 반대로 이해한다. -> 다음 변수를 읽기 위해 isStoreOpen을 뛰어 넘어 다음 변수를 읽는다.코드의 조건을 이해하려면 위의 과정을 3번 반복한 후, AND까지 시켜야 한다. 이는 컴퓨터도 마찬가지(NOT 연산을 3번 함)여서 성능에 조금 영향을 미친다.
CS를 공부했다면 이를 해소시킬 수 있는 방법들이 몇 가지 있다.
드모르간의 법칙을 이용하면 !(NOT)을 한 개로 줄일 수 있다.
//드모르간의 법칙으로 바꾸기!
if (!(isStoreOpen || isIngredientExist || isTableExist)) {
println("예약이 불가능합니다.")
} else {
println("예약 시도중...")
}
공통으로 들어가는 !(NOT)을 앞으로 뺀 후, 괄호로 묶고 모든 AND를 OR로 바꾸면 된다.
!(NOT)이 3개에서 하나로 줄었기 때문에 읽는 과정이 줄어들고 더욱 빠르게 조건을 파악할 수 있을 것이다.
내가 가장 애용하는 방식이다.
else는 기본적으로 !(NOT)의 의미를 내포하고 있다.
그렇기 때문에 중괄호 안의 코드 순서를 바꾼 후, NOT을 추가하거나 빼는 방법으로 코드를 더욱 이해하기 쉽게 바꿀 수 있다.
위의 드모르간의 법칙을 사용한 코드에서 더욱 줄일 수 있게 되는 것이다!
if(isStoreOpen || isIngredientExist || isTableExist){
println("예약 시도중...")
} else{
println("예약이 불가능합니다.")
}
(예시 조건이 좀 이상하지만 넘어가자...)
if, else의 중괄호에 넣는 결과 코드를 뒤집으면 NOT을 하나 더 줄일 수 있다.
처음 코드와 비교하면 읽기 너무너무 편리해 졌다.
만약 다른 곳에서 계산을 끝마쳤는데, 다시 구분해서 출력을 해야 하는 상황이 있을 수 있다. 아래와 같이 이미 계산을 끝마친 후, 다른 곳에서 사용한다고 가정해 보자.
fun canReserve(): Boolean {
if (isStoreOpen || isIngredientExist || isTableExist) {
return true
} else {
return false
}
}
fun printMessage() {
if (canReserve()) {
println("예약이 불가능합니다.")
} else {
println("예약 시도중...")
}
}
이렇게 되면 또 한번 canReserve의 결과 값을 if에 넣어 판단해야 한다.
하지만 하나의 객체를 선언한 후, 상속받는 여러 구현체를 이용한다면 불필요한 if를 줄일 수 있다.
interface PrintReserveUseCase{
fun print()
}
class CanReserveUseCase:PrintReserveUseCase{
override fun print() {
println("예약 시도중...")
}
}
class CanNotReserveUseCase:PrintReserveUseCase{
override fun print() {
println("예약이 불가능합니다.")
}
}
fun canReserve(): PrintReserveUseCase {
if (isStoreOpen || isIngredientExist || isTableExist) {
return CanReserveUseCase()
} else {
return CanNotReserveUseCase()
}
}
fun printMessage() {
canReserve().print()
}
if를 사용하지 않고 추상화된 객체를 이용, print 함수를 호출하게 되어 더욱 편리한 코드가 될 수 있다.
이렇게 객체지향의 원리를 이용한 코드를 작성하면 추후에 여러 조건들이 추가되었을 때 더욱 빛을 발할 것이다.
만약 극한으로 성능을 생각해야 하는 로봇, 임베디드 분야라면 비트 마스크를 통해 코드의 양을 줄이고 성능을 올릴 수 있다.
주의할 점은 "프로그래머 끼리 약속"되어있어야 한다는 점이다.
1의 자리 수는 조건 A, 2의 자리 수는 조건 B, 4의 자리 수는 조건 C를 나타낸다.
위의 가정대로 프로그래머 끼리 약속했다면, 동시에 만족하는 수를 빠르게 찾을 수 있다.
(maskValue and 0b101)!=0(maskValue and 0b110)!=0(maskValue and 0b011)!=0"월, 수, 금 중에 한 요일이라도 영업하는 가게를 필터링해서 보여주세요"
실제 내가 앱을 개발하면서 받았던 요구사항인데, 비트마스크를 통해 해결한 경험이 있다.
먼저 월화수목금토일을 자리수에 맞게 비트로 변환한다.
예시를 몇가지 두자면 아래처럼 요일이 비트로 바뀌게 된다.
이제 원래 조건인 월수금을 비트로 바꾼 후 논리곱 AND를 하면 조건에 맞는 가게를 쉽게 찾을 수 있다.
data class Store(
//...생략
val dayOfWeekMask: Int
//...생략
)
fun filterStore(storeList: List<Store>) {
//월, 수, 금 비트 마스크
val condition = 0b1010100
//월, 수, 금 중에 하나라도 없으면 제거
storeList.filterNot { (it.dayOfWeekMask and condition) == 0 }
}
이렇게 하면 여러 조건 중에 동시에 만족할 수 있는 조건들을 빠르게 찾을 수 있다.
이를 이용하여, 위의 가게 예시를 구현한다면 아래와 같이 할 수 있을 것이다.
//condition이 가게 상태의 비트마스크라면...
if (condition != 0) {
println("예약 시도중...")
} else {
println("예약이 불가능합니다.")
}
약속이 미리 되어있기 때문에, 코드를 파악하는데 문제가 되지 않고 더욱 짧아졌다.
또한 단 한번의 비교연산으로 조건을 탐색할 수 있다.
이처럼 CS는 코드의 성능을 높이고, 간결하고 읽기 쉽게 만들어준다.
예전 네이버 신입 공채에서는 "트렌드에 휘말리지 않는 탄탄한 기본기"가 중요하다고 말했다.
트랜드, 정말 많이 바뀐다.
Android의 Jetpack Compose, iOS의 Swift UI처럼 명령형 UI에서 선언형 UI로 많이 바뀌고 있고, 이에 따라 MVVM에서 MVI를 고려해야 하는 경우도 생긴다.
더 큰 FrameWork 관점에서는 Flutter, ReactNative의 파이가 점점 커져 "다시 공부해야 하나"하는 마음도 든다.
개발자들도 이런데, 개발자가 아닌 사람들이 개발자를 구할 때는 더욱 곤란할 것이다.
"앱을 만들어 사업하려고 해요. 어떤 개발자를 구해서 앱을 만들어야 할까요?"
지인 중에 사업을 하고 계신 분이 있는데, 이런 질문을 받은 적이 있다.
어떻게 대답해야 할까 잠시 고민을 하다가 대답했다.
"어떤 앱을 만드시는데요? 비용은 얼마나 가지고 있죠?"
앱을 만드는 방법은 Android, iOS 네이티브 이외에도 생각보다 많다.
비용과 개발 속도를 생각한다면 Flutter, ReactNative도 아주 좋은 선택지가 될 수 있고, 심지어 Unity, Unreal로 게임 뿐 아니라 네이티브 못지 않은 상용 앱을 만들 수 있다.
(크로스 플랫폼 처럼 하나의 코드로 Android, iOS 둘다 빌드 까지 가능하다.)
또한 Kotlin MultiPlatform으로도 만들 수 있으며
이미 지원 종료되었지만 저 질문을 받았을 때는 Xamarin으로도 앱을 만들 수 있었다.
이렇게 수많은 프레임워크 중에서 좋은 대답을 하려면 각각의 플랫폼을 사용해본 경험과 장, 단점을 알아야 한다.
이런 경험과 장단점을 자세히, 모두 알고 있는 개발자는 얼마나 될까...
(하나 공부하기도 벅차다)
하지만 모두 경험 하지 않아도, 판단할 수 있는 방법이 있다.
공통점. CS 지식이다.
위 플랫폼들의 공통점은 사용자에게 보여주는 그래픽이 있다는 것이다.
그럼 판단할 수 있는 기준이 한 가지 생기게 되는데, 어떤 그래픽을 사용자에게 보여 줘서 서비스를 사용하게 만들지를 들으면 된다.
3D 그래픽을 사용하는 앱이라면 Unity, Unreal을 추천하겠다.
Android, Flutter에서 사용하는 Skia Engine은 기본적으로 2D 그래픽 엔진이어서 3D를 구현하기 쉽지 않다.
Unity, Unreal은 3D로 개발하기 쉽고, 빌드 설정에서 OpenGL, DirectX로 설정하여 플랫폼에 최적화된 그래픽 라이브러리를 직접 설정할 수 있다.
또 설정을 통해 똑같은 코드로 Android, iOS로 빌드 가능해서 여러 플랫폼을 지원하기 쉽다.
이렇다면 두 가지중에 선택해서 말할 것 같다.
물리적인 계산이 들어가는 엔터테인먼트가 있다면 Unreal, Unity를 추천한다. 3D 뿐만 아니라 2D도 쉽게 지원할 수 있고, 물리 계산이 편하다.
물리적인 계산이 없고, 상용앱이라면 Flutter, KMP를 추천한다.
플랫폼에 매칭되는 UI Component로 변환시키는 ReactNative에 반해 Flutter는 Skia Engine, KMP는 각각의 그래픽 엔진을 사용하여 픽셀을 직접 그리기 때문에 성능이 좋다.
그럼 ReactNative를 추천하겠다.
JavaScript의 개발자 풀은 굉장히 많고, 또 뛰어난 개발자들이 많다.
그리고 ReactNative로 빠르게 좋은 앱을 만들 수 있다. 이는 개발비용 측에서 좋은 효율을 보여줄 것이다.
그럼 Flutter를 추천한다.
Skia Engine으로 픽셀 하나하나 직접 그리기 때문에 화려한 애니메이션을 성능 좋게 구현할 수 있다.
Android, iOS 개발자를 추천하면 된다.
위 처럼 기본적인 CS지식이 바탕이 된다면 여러 기술을 판단할 수 있게 된다.
개발자에게는 어떻게 도움이 될까?
요즘 공고를 보면, Flutter, React Native 개발자를 찾는 공고를 많이 볼 수 있다.
그만큼 멀티플랫폼 는 굉장히 많은 발전을 하고 있으며 사용중인 개발자들, 그리고 예시 코드들이 많이 늘어나고 있다.
만약 시간이 지난 후.. 갑자기 구글에서 이렇게 발표를 한다면...?
"Android의 모든 기능은 이제 Flutter로 구현할 수 있게 되었습니다. 따라서 Android는 이제 지원하지 않겠습니다."
난 노숙자가 되지 않으려면, 어떤 개발자로 전향해야 할 지 판단해야 할 것이다...
이 때 CS 지식이 어느 프레임워크를 새로 공부할 지 기준점이 될 수 있다.
일례로 iOS는 13 버전부터 OpenGL이 아닌 자체 그래픽 라이브러리 Metal을 사용하여 화면을 그리게 되었다.
(Android에는 Vulkan이라는 친구가 생겼다.)
Flutter를 개발하는 개발자들은 OpenGL로 된 하나의 코드를 이용해서 여러 플랫폼을 지원하고 있었는데, iOS만 Metal로 개발해야 하는 상황이 온것이다.
이는 Google이 Flutter를 개발하는데 비용이 증가했다는 것으로 연결되며, Flutter나 ReactNative나 개발 유지비용이 똑같아(혹은 Flutter가 더 많아짐) 조금 불안해졌다는 것이다.
(Google은 끄떡 없지만 혹...시 모르니까...)
물론 프레임워크를 바꿔야 하는 상황은 잘 일어나지 않는다.
하지만 이보다 작은 단위인 라이브러리, API를 바꾸는 상황은 평소에도 자주 일어난다.
극단적인 상황이 아니더라도, 잘 사용했던 라이브러리가 Deprecated 되는 일은 많이 경험해 보았을 것이다.
기존에 잘 사용했던 라이브러리가 치명적인 결함으로 갑자기 Deprecated 되었다면 새로운 라이브러리를 찾아야 한다.
(안 그러면 서비스가 망한다)
이 때는 여러 검증되지 않은 라이브러리가 있어서 다른 개발자들도 뭘 사용해야 할 지 모르는 상황이 올 것이다.
이 때 CS 지식이 라이브러리를 선택하는데 도움을 줄 수 있다.
여러 조건을 토대로 서칭해야 하는 라이브러리를 새로 찾아야 한다면 조건을 하나하나 비교하여 서칭하는 라이브러리와 비트마스크를 AND시켜 서칭하는 라이브러리의 성능은 다를 것이며, 이를 판단하기 위해서는 알고리즘을 열심히 공부해야 할 것이다.
어떤 기술이 어떤 장단점을 가지고 있는 지 판단하는 근거는 CS 지식에서 나온다.
현실적인 이유인데, 회사에서 다른 개발자가 되길 원하는 경우가 꽤 많은 것 같다.
"원래 안드로이드 개발자가 아니었는데, 회사에서 원해서 안드로이드 개발자가 되었어요."
GDG DevFest 안드로이드 개발자 컨퍼런스에서 20년차가 넘은 개발자분께 들었던 이야기이다.
원래는 C/C++ 개발자였는데, 회사에서 앱을 만들기를 원해 전향하게 되었고, 처음에는 당황스럽고 부담스러웠는데 지금은 만족한다고 하셨다.
또 내가 자주 보는 개발자 유튜버는 iOS 개발자로 취업했다가 백엔드 개발자가 되었다고 한다.
나 또한 회사에서 이런 이야기를 들은 적이 있다.
"문휘님, 혹시 플러터 가능하세요? 저희 한번 플러터 해볼까요?"
처음에는 달갑지 않았다.
안드로이드 개발자인데 커리어가 꼬이게 되지 않을까 걱정도 많이 했었다.
하지만 회사에서 하라면 해야지... 수 개월동안 플러터를 개발했는데...
"Flutter는 신이야"
너무 재밌었다.
처음에는 어색했던 선언형 UI도 생산성에서 놀랐고, Web, iOS, Android, 심지어는 데스크톱 앱까지 한 코드로 빌드되는 것에 신기함을 느꼈다.
또 Web에서는 기본인 반응형 UI로 앱을 개발하면 UI가 깨지지 않고 모든 환경에서 일관된 UI를 보여주었다.
그리고 언젠가는 내가 Android 개발자가 아닌 Flutter 개발자가 될 지도 모르겠다는 생각이 들었다.
Flutter에서도 CS는 중요했다.
Flutter를 개발할 때 화면이 바뀌지 않는 문제가 있었는데, 이는 Widget의 Key가 바뀌지 않을 때 일어난다고 하였다.
그래서 Key를 데이터에 따라 수동으로 바꾸는 코드를 작성하면서, Flutter가 화면을 그리는 방식을 공부했다.
"Flutter는 화면을 그릴 때 Widget Tree를 DFS로 순회하며 Widget의 Key가 바뀐 것만 다시 그립니다."
DFS는 깊이 우선 탐색이다.
그렇기 때문에 자신의 형제 노드를 먼저 탐색하는 것이 아닌 자신의 자식 노드를 먼저 탐색한다.
Flutter 뿐만 아니라 Android에서도 DFS를 사용하는데, 그 이유는 부모의 크기가 정해 진 후, 그 크기 안에서 자식의 크기를 정해야 하기 때문이다.
BFS를 통해 A Widget보다 B Widget이 먼저 검사되어 크기가 정해졌다면, C Widget의 크기를 정할 때 다시 A Widget의 크기를 가져오는 등, 불편한 부분이 있을 것이다.
DFS는 이런 과정이 자식부터 되기 때문에 크기 계산이 자연스럽다.
또한 DFS 순서로 그리게 되면 먼저 그려진 A, C Widget이 나중에 그려지는 B Widget에 의해 자연스럽게 가려지게 된다.
내가 만약 CS 알고리즘을 공부하지 않았다면 위의 말을 이해하지 못했을 것이다.
조금 딴길로 샜는데,
회사는 다른 기술로 개발할 때 또 새로운 직원을 뽑는 것 보다 기존 직원을 활용해서 서비스를 만들고 싶어한다. 그리고 CS가 탄탄하면 기술 스택 전환이 빠르다.
-> 회사는 기본기가 탄탄한 개발자를 원한다!
회사는 새로운 직원을 뽑는 것 보다 기존 직원의 연봉을 높여 다른 기술을 개발하는 것이 시간적 측면에서도, 비용적 측면에서도 효율적이다.
연차가 높은 개발자분들의 생생한 전환 경험담을 들은 만큼, 나도 CS공부로 탄탄히 준비해놓아야 한다.
글을 쓰다 보니 너무 알고리즘에만 치우친 느낌이 든다.
내가 백엔드 개발자가 아니라서 네트워크 부분이 부족한 것도 있고, 다른 개발자분들은 공감하지 못할 것도 많을 것 같다.
그럼에도 최대한 나의 경험을 토대로 작성하려고 노력했다. 내가 더 많은 경험을 쌓게 되면 다른 상황에서 CS 지식을 이용한 문제 해결 경험을 적고 싶다.
문휘님이당