어릴 때 친구들끼리 서로 놀리기 위해 가사를 바꿔서 생일 축하 노래를 부르곤 했었다.
"왜 태어났니~~ 왜 태어났니~~ 얼굴도 못생긴 게 왜 태어났니~~~"
철없던 그 시절 노래로 얼굴 평가를 하며친구들끼리 놀리곤 했었는데, 가만히 곱씹어 보면 가사에 상당히 철학적인 질문이 있다. 우리는 왜 태어났는가? 기술은 왜 태어나는가?
인류의 기술은 불편함을 개선하고 편의를 제공하고 효율을 높이기 위해 지속적으로 발전해 왔다. 가령, 세탁기를 예로 들어보자. 손빨래가 너무 힘들고 시간도 오래 걸리니까 이런 불편함을 해결하기 위해 탄생한 것이 세탁기다. 배나 기차보다 더 빨리 먼 곳을 가고 싶어서 발명된 것이 비행기다. 대부분의 기술은 탄생하게 된 배경이나 이유가 있다.
프로그래밍 세계에서도 마찬가지다. 많은 IT 기술이나 개념들이 누군가가 (혹은 단체가) 무언가를 해결하거나 개선하기 위해 연구하고 정립한 끝에 태어난다. 대충 아래와 같은 과정일 것이다.
문제가 있네? →
어떻게 해결하지? →
이렇게 해결해 볼까? →
오 괜찮은데? 제대로 만들어보자. →
캬~! 완성됐어! →
다른 사람들에게도 이걸 쓰도록 알려야지! 분명 도움이 될 거야
이처럼 새로운 기술이나 개념이 만들어지는 과정의 출발점에는 어떤 이유나 배경이 있다. 반면, 새로운 기술을 접하거나 지식을 습득하는 과정은 어떠한가? 보통은 아래와 같은 과정일 것이다.
오호~ 새로운 개념이 나오네? →
열심히 공부해야지~! 외울 것도 많네! 하하핫ㅎㅎ →
오~! 이제 좀 뭔가 알 것 같아!
나 또한 이러한 과정으로 새로운 지식을 얻곤 했었는데 한 가지 문제가 있었다. 이렇게 새로운 개념을 배웠다가도 내가 너무 잘 까먹게 된다는 것이다!! 기억력이 좋은 사람이야 이런 어려움이 없겠지만, 나 같은 경우에는 기억력이 좋은 편이 아니어서 방금 전에 공부했던 개념도 하루만 지나도 희미해질 때가 많았다. 나에게 필요한 것은 오래가는 건전지 기억력이었다.
어떻게 하면 좀 더 오래 잘 기억할 수 있을까?
이 고민을 하다가 여기까지 왔다. 앞에서 언급했던 것처럼, 새로운 기술이나 개념이 탄생할 때는 합당한 이유가 있다. 이전의 나는 기술의 출생의 비밀(?)에는 관심 없이 기술의 개념 그 자체만을 이해하고 기억하려 했다면, 좀 더 오래 좀 더 선명히 기억하기 위해 이유와 배경에 대해서도 관심을 가지기 시작했다.
예를 들어 설명해 보자면,,
Process와 Thread가 있다. 이 두 개의 차이를 학부 때는 그냥 외웠던 것 같다.
"하나의 Process는 여러 개의 thread들을 가질 수 있고, thread는 process 보다 좀 더 가벼워.
그리고 thread들 끼리는 같은 메모리 공간을 공유하지."
대충 이런 식으로 말이다. 자~ 그럼, OX 퀴즈를 맞혀보자.
"임의의 thread가 같은 process의 다른 thread와 공유하는 메모리 영역은 stack이다."
'아,, 헷갈리는데? 분명히 외웠는데 어따 날려먹은 거지..?' 그렇다, 잘 기억이 나지 않을 수 있다. 실제로 나는 학부 시절 운영체제 중간고사에서 위와 유사한 문제를 만났고 틀렸었다.. 그 당시의 나는 이런 종류의 지식은 외워야 한다고 생각했었고 그러다 보니 시간이 흐르면서 잘 기억이 나지 않았던 것이다.
지금은 이 부분은 잘 기억하는데, 왜냐하면 '이유'에 대해서 나름대로 찾아보고 고민하면서 최대한 이해하려고 노력했기 때문이다. 위의 예를 지금은 어떻게 이해하고 있는지 혼잣말 형식으로 설명해 보겠다. '어떤 방식으로' 이해하기에 기억에 오래 남는지에 집중하며 읽어주길 바란다.
"Process의 메모리 영역은 크게 code, data, stack, heap으로 나뉘지. 이 네 가지도 굳이 외울 필요 없이 이해하면 돼. 나름의 이유가 있으니까. 우선 code 영역은, 당연히 프로그램이 코드로 작성됐으니까 프로그램을 실행하려면 코드가 메모리에 상주해야 할 테고, data 영역은 프로그램이 실행돼서 종료될 때까지 전역(global)적으로 혹은 정적(static)으로 필요한 변수들을 모아 둔 곳이겠지. stack은 자료 구조에서 배운 그 stack과 유사하잖아. 먼저 들어온 게 나중에 나가는 게 stack이니까, 함수를 호출하고 그 함수 안에서 또 다른 함수를 호출하고 이런 식으로 함수들이 호출될 때마다 매개변수(parameter)나 지역(local) 변수 등을 stack에 차곡차곡 쌓아서 관리하는 영역이 당연히 필요하겠지. 그렇다면 heap은? 여기서 함정 카드가 발동하지. stack 메모리는 자료 구조의 그 stack의 개념을 사용하는데, heap은 자료 구조의 그 heap을 사용하는 게 아니더라고. 어쨌든 heap이 사전적으로 '더미'라는 뜻이 있으니까 new나 malloc으로 다이나믹하게 생성된 데이터들이 더미로 모여있는 공간으로 이해하면 될 거야.
그러면,, 어디 보자. thread가 탄생하게 된 이유는, process는 너무 무겁고, process 끼리는 메모리 공유도 안돼서 서로 데이터를 주고받으려면 추가적인 방법이 필요하고, context switching도 오래 걸리다 보니 process보다 조금 더 가볍게 사용하기 위해 탄생한 개념이 thread잖아. 그래서 하나의 process는 여러 개의 thread를 가질 수 있고 thread들끼리의 context switching은 더 가볍고, 이 thread들은 자신이 소속된 process의 메모리 영역을 공유하기 때문에 thread들끼리도 바로바로 데이터를 주고받는 게 쉬워졌지.
자 그럼, 왜 stack 영역만 thread마다 만들어지는 걸까? 나름 그 이유를 찾아도 보고 생각해 봤는데, 아무래도 설계의 복잡성 때문이지 않을까 싶었어. 기본적으로 메모리 stack은 stack이라는 이름의 자료구조를 사용해서 함수의 매개변수와 지역 변수를 관리하는 방법을 단순화시켰지. 가령 함수 A에서 지역변수 int num = 10을 만들고 그 num을 함수 B의 int target이라는 매개변수로 넘겨준다면 메모리 stack에는 num = 10이 가장 아래에 있고 target = 10이 그 위에 쌓이게 될 거야. 그래서 함수 B의 호출이 끝나면 stack에서 targret = 10은 pop이 되어 사라지게 되겠지. 이런 식으로 메모리 stack에선 함수들의 호출에 따른 변수 관리를 단순하게 처리하는데, 만약 thread들 사이에서 메모리 stack을 공유해서 사용하면, 어떤 변수가 어느 thread 소속인지도 챙겨야 하고.. 단순했던 관리 구조가 매우 복잡해질 거야. simple is best라는 말도 있으니까. 관리의 단순함을 유지하기 위해 stack은 thread마다 따로따로 할당한다고 이해하자!"
이런 식으로 배경과 이유에 대해 스스로 질문해 보고 답도 찾아가면서 이해하려고 노력하니, 단순히 기억만 했을 때에 비해서 훨씬 오래 기억에 남게 되는 것을 경험했다. 여기서 팁은 100% 정확한 이유를 찾으려고 많은 시간을 쓰진 않는다는 점이다. 주객이 전도되면 안 되기 때문이다. 원래 배우기로 목적했던 내용을 더 잘 이해하고 더 오래 기억하기 위해 그 이유와 배경을 찾아보는 것이므로, 100%의 정확성을 추구하기보다는 혼자만의 추측을 하더라도 나름 말이 되는 이유가 생각나서 이를 지렛대 삼아 목적했던 내용을 더 잘 이해하고 기억할 수 있다면 그걸로도 충분하다.
두 가지 간략한 예를 통해 감을 잡아보자.
인덱스(Index)
데이터베이스에는 인덱스라는 것이 존재한다. 인덱스는 왜 태어나게 된 것일까? 탐색 성능을 향상시키기 위해서다. 예를 들어 사용자의 택배 송장 번호를 통해 현재 배송 상태를 조회하는 기능을 개발한다고 해보자. 하루에도 수없이 많은 물건이 배송되기 때문에 데이터베이스에 저장되는 송장 번호의 개수는 어마어마하고 무시무시하게 많을 것이고 앞으로도 계속 증가할 것이다. 이런 상황에서 사용자가 송장번호를 입력했을 때, 데이터베이스에서 일치되는 송장번호가 있는지 하나하나 비교한다면.. 현빈횽아가 묻는다. 이게 최선입니까? 확실해요?
그래서 인덱스가 등장했다. 탐색 속도를 비약적으로 증가시키기 위한 목적으로 말이다. 세상 이치가 그렇듯 얻는 것이 있다면 잃는 것도 있는 법! 인덱스를 통해 탐색 속도 증가를 얻은 대신에, 인덱스를 저장하기 위한 추가적인 디스크 공간이 필요하며 데이터베이스에 데이터를 생성하거나 삭제할 때마다 그 데이터의 인덱스 또한 생성/삭제가 이뤄지기 때문에 약간의 시간적인 추가 비용 또한 발생한다. 그럼에도 탐색 성능의 비약적인 개선은 이런 손해를 감수하고도 훨씬 개이득이다. 왜냐하면 일단 사용자 입장에서 읽어오는 속도가 빨라서 좋고, 심지어 대부분의 서비스의 경향을 보면 데이터베이스에서 읽는 것이 쓰는 것보다 훨씬 많기 때문이다.
B-Tree
데이터베이스에서 인덱스를 만들기 위해 사용되는 자료구조는 B-Tree이다. 기술 면접 때도 많이 물어보는 질문이라서 이 글을 읽고 있는 여러분 중에는 이미 알고 있는 분들도 있을 것이다. 그런데 인덱스를 만들기 위해 하필 왜 B-Tree를 사용할까? 왜?라는 질문을 가지고 계속 파다 보면, B-Tree는 균형 트리(balanced Tree)로서 데이터가 정렬된 상태를 유지하며 생성/삭제/탐색 속도가 항상 O(logN)을 보장하기 때문임을 알게 된다.
개발자로 잘 성장하기 위해 조금 더 집요하게 파기로 결심했다. 그렇다면 탐색 속도가 O(1)인 Hash table은 왜 인덱스의 자료 구조로 사용될 수 없는 것일까? 왜냐하면 Hash table은 범위 탐색을 할 수 없기 때문이다. 가령 21년 11월 1일부터 11월 31일 한 달 동안 수원시로 배송된 택배를 Hash Table로 검색하긴 매우 난해하다.
그렇다면 다른 질문을 해보자. Balanced Tree는 B-Tree 말고도 AVL tree, Red-Black tree가 있으며 이들도 생성/삭제/탐색 속도는 항상 O(logN)을 보장한다. 그런데 왜 이들은 인덱스로 사용하지 않을까? B-Tree와는 다르게 나머지 두 Tree는 노드 하나에 데이터 하나만 가질 수 있다. 그에 비해 B-Tree는 하나의 노드에 데이터를 하나 이상 가질 수 있으며 이는 탐색할 때 속도가 다른 두 Tree 보다 빠름을 의미한다. 같은 O(logN)이어도 탐색해야 할 노드 개수가 적기 때문이다.
왜?를 고민하면서 배우다 보면 기억에 오래 남는다는 장점 말고도 좋은 장점이 한 가지 더 있다. 기술이나 개념에 대한 이해도가 깊어지기 때문에 적절하게 사용할 수 있게 된다는 것이다! 외우기만 하면 죽은 지식이 될 수도 있는데, 배경과 이유를 이해하면 실무에서도 적절히 사용할 수 있는 살아있는 지식이 된다.
예를 들어 인덱스가 DB 탐색 속도에 영향을 준다는 것을 알게 됐고, 동시에 생성/삭제 시 일정 부분의 손해는 감수한다는 것 또한 이해했다면, 우선 서비스에서 호출할 쿼리들의 where 절에 맞춰서 적절한 인덱스를 걸어주게 된다. 이는 서비스의 성능과 사용자의 경험에 영향을 끼치게 된다. 그렇다고 인덱스를 이것저것 닥치는 대로 걸어주는 것 또한 마냥 좋은 것은 아니라는 것도 인지하고 있기 때문에, 여러 가지를 고려하게 되는데 가령 하나의 인덱스로 두 가지의 쿼리를 처리할 수는 없는지도 고민하게 되고, 불필요한 인덱스가 걸린 것은 없는지도 고민하게 되고, 넓게는 DB 구조까지도 생각해 보는 시간들을 가지게 된다.
다른 예를 들어보자. 자바라는 객체지향 언어를 배웠다고 하자. 객체지향의 탄생 이유를 이해하면 어떻게 개발하는 것이 객체지향답게 개발하는 것인지 고민하며 개발하게 되고, 종국에는 객체지향언어의 특징을 십분 발휘하여 유지/보수가 용이하고 확장이 쉬운 형태로 개발할 수 있게 된다.
지금의 우리는 누군가가 연구하고 개발한 기술을 배우고 사용하지만.. 혹시 또 모른다.
언젠가 우리 중 누군가는 새로운 기술을 만드는 당사자가 될 지도?
p.s.
새로운 기술이나 개념이 탄생하는 데는 나름의 이유가 있듯, 우리 각자가 태어난 것에도 이유가 있음을 믿는다.
그 이유를 찾을 수 있길 응원한다.
그래야 우리 삶도 더욱 의미 있게 빛날 수 있으니까.