프로그래밍 입문서의 문제점

모바일앱, 웹서비스 등을 스스로 만들어보려는 사람이 늘면서 코딩을 배우는 사람들도 늘고 있습니다. 페이스북 그룹 “생활 코딩”에 가입자가 4만명 이상 있고, 여러 온오프라인 행사들을 개최하는 것을 보면 코딩을 배우고자 하는 열기가 느껴지기도 합니다.

하지만 입문자들에게 코딩을 가르치는 방식을 보면 우려가 되는 부분이 있습니다. 많은 분들이 우려하는 전산 전공 지식이 아닌 “코딩”만 가르친다는 문제 제기를 하려는 것은 아닙니다. 코딩만 놓고 봐도 나쁜 습관을 가르치고 있는 것이 문제입니다.

가장 큰 문제는 변수를 값을 담을 수 있는 용기로, 변수 값의 변경(mutation)을 계산 방법으로 설명하는 것입니다. 다음의 생활코딩 JavaScript 강의 변수 편에서 인용한 변수에 대한 설명입니다.

변수(Variable)는 (문자나 숫자 같은) 값을 담는 컨테이너로 값을 유지할 필요가 있을 때 사용한다. 여기에 담겨진 값은 다른 값으로 바꿀 수 있다.

이 설명이 기술적으로 틀렸다는 뜻은 아닙니다. 컴퓨터 프로그램은 결국 메모리를 읽고 쓰면서 계산을 수행하기 때문입니다. 문제는 입문자들에게 프로그래밍을 “풀고자 하는 문제에 대한 답을 컴퓨터가 알아들을 수 있게 메모리 읽기/쓰기로 표현하라”고 가르치는 것입니다. 반복문에 대한 설명을 보면 이런 경향이 명확해 집니다.

다음은 생활 코드 JavaScript 강의 반복문 편에서 인용한 “coding everybody”라는 문장을 10번 출력하는 코드입니다.

var i = 0;
// 종료조건으로 i의 값이 10보다 작다면 true, 같거나 크다면 false가 된다.
while(i < 10){
    // 반복이 실행될 때마다 coding everybody 
이 출력된다. 
 줄바꿈을 의미하는 HTML 태그
    document.write('coding everybody 
');
    // i의 값이 1씩 증가한다.
    i++
}

요구사항은 그저 같은 문장을 10번 반복해서 출력하는 것인데, 변수 i를 선언하고 while 루프를 돌면서 i를 하나씩 증가시키고 i가 10보다 작은지 판단해서 루프를 빠져나오는 코드를 예로 보이고 있습니다.

이미 이런 스타일에 프로그래밍에 익숙해지신 분들은 입문자들에게 이런 스타일로 프로그래밍을 가르치는 게 얼마나 저수준(기계에 가까운) 사고를 요구하는지 이해하기 힘드실 수도 있습니다. 위 코드를 다시 읽어 봅시다.

  • 변수 i가 가르키는 메모리 공간이 있고 이 공간에 값 0을 초기화합니다.
  • while 루프에서 변수 i가 가르키는 메모리 공간의 값을 읽어 이 값이 10보다 크면 루프를 빠져 나가고 아니면 다음 코드를 수행합니다.
  • 문자열을 출력하고, 변수 i가 가르키는 메모리 공간의 값을 읽어 1을 더한 다음에 이 값을 다시 변수 i가 가르키는 메모리 공간에 씁니다.
  • 다시 while 루프를 반복합니다.

변수 그 자체가 값이 아니라 메모리 공간에 대한 포인터 개념으로 설명을 하고 있고, 실제로 이 메모리 공간의 값을 변경해서 계산을 수행하고 다음 수행될 코드를 결정하기 때문에 얼핏 간단해 보이는 이 코드는 사실상 어셈블리에 가까운 코드인 걸 알 수 있습니다.

어떤 문자열을 10번 출력하는 프로그램은 다음과 같이 요구사항 그대로 말로 옮긴 코드가 되어야 합니다.

_.times(10, function () {
  document.write('coding everybody');
});

저는 입문자에게 변수라는 개념을 가르칠 때 단순히 이름 붙이기(바인딩)로 가르치는 것이 적절하다고 생각합니다. 예를 들어, 아래 코드에서 x는 10, y는 20, z는 10+20입니다. x는 10이라는 값을 담고 있는 메모리 공간, y는 20이라는 값을 담고 있는 메모리 공간, zx 메모리 공간이 담고 있는 값과 y 메모리 공간이 담고 있는 값의 합을 계산하여 그 결과를 담고 있는 메모리 공간으로 생각하는 것과 복잡도 면에서 큰 차이가 있습니다.

var x = 10;
var y = 20;
var z = x + y;

물론 어떤 프로그램이 값의 변경(mutation)이 없이 유용한 일을 할 수는 없습니다. 하지만 mutation은 프로그램이 복잡해지는 가장 큰 이유이고, 프로그래밍에 입문할 때부터 mutation이나 side effect는 꼭 필요한 경우에만 조심스럽게 하도록 가르치는 것이 필요하다고 생각합니다.

케이팝스타 같은 오디션 프로그램을 보면 심사위원들이 가장 큰 문제로 지적하는 것이 “나쁜 습관”입니다. 노래 연습을 열심히 한만큼 나쁜 습관은 더 고치기가 어렵고, 그래서 나이 어린 참가자들에 비해 성인 참가자들의 발전 가능성을 더 낮게 봅니다. 프로그래밍도 마찬가집니다. 사람은 튜링 머신이 아닌데, 모든 문제를 메모리 읽고 쓰는 문제로 보는 습관은 복잡한 소프트웨어를 작성할 때 가장 큰 걸림돌이 됩니다. 그리고 이런 사고 방식은 오래될수록 고치기 어렵습니다. 그래서 처음 프로그래밍에 입문할 때 어떻게 배우느냐가 중요합니다.

문제는 알고리즘만이 아니다.

오늘 임백준님이 ZDNET에 기고하신 문제는 알고리즘이다 글이 페이스북에 많이 회자되었습니다. 프로그래머가 문제 해결 능력을 함양해야 한다는 글의 취지에는 동의하지만, “알고리즘=문제 해결 방법”이라는 대유법에는 동의하기가 어렵습니다.

문제 해결에 알고리즘이나 알고리즘적인 사고 방식이 도움이 되는 것은 사실이지만, 문제 해결 능력이란 알고리즘보다 훨씬 더 큰 능력입니다. 임백준님이 인용한 알고리즘의 정의는 “수학적인 문제를 해결하거나 컴퓨터 프로세스를 완결하기 위해서 차례로 뒤따르는 단계의 집합”입니다. 이 정의 어디에도 실제 소프트웨어 개발에 필요한 요구사항 분석이나 설계, 테스트, 유지 보수 등의 이슈가 언급이 안 되고 있습니다.

게임 개발을 예로 들어 봅시다. 10-20명 정도의 팀을 꾸려 1년간 모바일 게임을 개발하는 상황을 가정해 봅시다. 새로 시작한 프로젝트인 만큼 신나서 다들 열심히 코딩을 하지만, 시간이 지날수록 코드는 복잡해지고 출시 때쯤 되면 간단하 버그 하나 못 잡아 2-3일씩 시간을 쓰거나 간단한 수정 사항 하나 반영 못해서 회사와 싸우는 경우가 비일비재합니다. 알고리즘적인 사고 방식만 있었으면 이런 일이 일어나지 않았을까요?

문제 해결에 필요한 여러 능력 중 알고리즘만 유독 강조되는 또 다른 이유는 구글을 필두로 미국 IT 회사들이 면접 때 알고리즘 문제를 많이 내기 때문인 것으로 보입니다. 하지만 이게 실제 업무가 아니라 면접이라는 점을 감안해야 합니다. 지원자의 문제 해결 능력을 확인하고 싶은데, 짧은 면접 시간 동안 그나마 가장 효과적인 방법이 알고리즘적인 사고를 보는 것이지, “알고리즘=문제 해결 능력”이라고 생각하는 게 아닙니다.

CMU 교수인 Jeannette M. Wing이 CACM에 게재한 글 중 Computational Thinking이란 글이 있습니다. Wing 교수가 정의한 Computational Thinking(CT)은 열린 문제에 대한 정답을 찾는 과정을 일반화하는 프로세스입니다. 정답이 정해져 있는 것이 아니라 여러 변수를 고려해서 다양한 답을 낼 수 있고, 컴퓨터뿐만 아니라 사람을 이용해도 됩니다.

CT는 알고리즘뿐만 아니라 분해(decomposition), 데이타 표현(data representation), 일반화(generalization), 모델링(modeling) 등 컴퓨터 과학 혹은 소프트웨어 엔지니어링에서 문제 해결에 사용하는 다양한 기법들을 총동원하여 문제를 푸는 과정을 말합니다. 알고리즘은 CT에서 요구하는 여러 기술 중에 하나일 뿐입니다.

코딩을 “특정 플랫폼, 특정 언어, 특정 API”라고 좁은 의미로 정의하고 문제 해결 능력인 알고리즘과 대비해서 설명하는 것도 문제가 있습니다. 프레임 자체을 2분법적으로 짜고 알고리즘과 코딩을 대비해서, 알고리즘은 문제 해결 능력이고, 코딩은 단순 기술이라는 식으로 설명을 하면 알고리즘과 코딩 어느 쪽에도 속하지 않지만, 좋은 코드를 작성하기 위해 필요한 수많은 지식이 갈 곳이 없기 때문입니다.

또 단순히 알고리즘이 문제라고 설명하면 수많은 한국 IT 회사들이 겪고 있는 문제를 잘못 진단하게 됩니다. 미국 IT 회사가 알고리즘적인 사고만 보고 사람을 뽑아도 되는 이유는 웬만한 IT 회사들이 이미 좋은 개발 문화를 가지고 있고, 누가 들어오든 문제 해결 능력이 있고 사람만 똑똑하면 코딩을 잘할 수 있는 환경을 만들어 놓았기 때문입니다. 반대로 한국 IT 회사는 좋은 코드를 작성하기 위한 기본적인 개발 문화도 환경도 없는 경우가 없기 때문에 알고리즘적인 사고만 뛰어난 개발자를 뽑으면 슈퍼개발자가 되어 회사를 망칠 확률이 더 높습니다.

코딩은 학교에서 못 가르치는 게 맞지만, 그렇다고 학원에서 쉽게 배울 수 있는 것도 절대 아닙니다. 코딩은 혼자 배우는 게 아닙니다. 프로그래밍 언어를 배우는 과정도 마찬가지입니다. 문법(syntax)이나 의미(semantic)야 학원을 다니든 독학을 하든 쉽게 배울 수 있지만, 해당 언어 커뮤니티가 사용하는 용법(idiom)을 익히는데 시간과 노력을 투자해야만 합니다. 또한 혼자 세상과 소통하며 오픈소스 개발자 할 것 아닌 이상, 결국 코딩은 회사에서 동료들에게 배우는 겁니다. 바꿔 말해, 누군가가 코딩을 못 하는 이유는 주변 동료들이 코딩을 못 하기 때문입니다.

알고리즘만 문제가 아니라 알고리즘도 코딩도 그외 모든 것도 다 문제입니다.

코딩 교육

요즘 초중고 학생, 일반인들을 위한 코딩 교육에 대한 관심이 많은데, 이 글에서는 전산 혹은 컴퓨터공학 전공자에 대한 코딩 교육을 이야기해 보려고 합니다.

우리나라의 코딩 교육은 영어 교육과 닮은 면이 있습니다. 비슷한 경제 수준의 다른 나라들에 비해 교육열이 낮거나 쓰는 시간과 비용이 적은 편이 아닌데도 결과가 좋지 않다는 점입니다. 10년 이상 영어 공부를 해도 간단한 영어 문장도 말하지 못하고, 대학 4년을 컴퓨터공학과에서 공부했는데도 간단한 스마트폰 어플, 웹 서비스도 못 만드는 현실이 그렇습니다.

여기에는 여러 이유가 있겠지만 저는 가장 큰 문제 중 하나로 읽기 교육의 부재를 꼽고 싶습니다. 여기서 읽기는 인문 교양 서적이나 문학 서적의 독서 부족를 이야기하는 것이 아니라 코드 읽기를 말합니다. C++, JavaScript, C# 등 프로그래밍 언어만 하나 달랑 배운 후에 곧바로 코드를 작성하는 자체가 어불성설입니다. 이건 국어 문법만 달랑 배우고 글쓰는 일을 업으로 삼는 것과 마찬가지입니다. 글이야 쓸 수 있겠지만, 절대로 좋은 글을 쓸 수는 없습니다.

현업에 와도 좋은 코드를 읽을 기회는 드뭅니다. 선배들도 비슷한 시행 착오를 거쳐서 겨우 돌아만 가는 코드를 짜놓은 경우가 많고, 이 코드를 유지보수하면서 코드 품질에 대한 기준을 잡기 때문에 5년 10년을 개발해도 실력이 늘기가 어려운 악순화에 빠지게 됩니다. 대부분의 개발자는 좋은 코드를 한 번도 보지 못하고 비슷한 수준의 작업만 무한히 반복하고 있습니다.

게임 개발자는 이런 상황에 노출되기가 더 쉽습니다. 게임의 특성상 기존 코드를 활용하기 보다 매번 코드를 새로 작성하는 경우가 많습니다. 오픈소스로 공부하고 싶어도 게임의 일부 라이브러리가 아닌, 게임 코드 자체가 공개된 경우는 잘 없습니다. 결국 대부분의 게임 개발자는 내가 작성한 코드, 우리팀이 작성한 코드외에 다른 사람의 코드를 읽어본 경험이 거의 없게 됩니다.

해결책은 결국 좋은 코드를 많이 읽는 것밖에 없습니다. 다행히 점점 더 많은 회사들이 오픈소스로 코드를 공개하고 있고, GitHub나 여러 오픈소스 커뮤니티 사이트를 통해 쉽게 소스코드를 내려받을 수 있습니다. 많은 분들이 오픈소스 프로젝트에 실제 참여하는 것을 권장하지만, 저는 최소한 잘 작성된 코드를 읽는 것만으로 실력 성장에 많은 도움이 된다고 생각합니다. 좋은 작가가 되기 위해 고전을 읽는 것과 마찬가지입니다.

문제는 남아 있습니다. 사실 읽을 책이 없어서 독서를 안 하는 게 아니라 독서하는 습관이 없기 때문에 독서를 할 줄 모르는 게 문제인 것처럼, 인터넷에 좋은 오픈소스 프로젝트가 널렸어도 소스 코드를 읽고 공부하는 방법을 모르는 개발자에게는 그림의 떡일 수밖에 없습니다.

이 문제에 대한 답도 고전 읽기에서 찾아야 한다고 생각합니다. 가장 좋은 방법은 스터디 모임을 만드는 것입니다. 관심 있는 분야가 맞는 사람끼리 코드 읽는 모임을 만들어서 각자 코드를 파악하고 모르는 부분을 서로 묻는 자리를 만든 것도 좋은 방법입니다. 고전이든 코드든 읽을 때는 이해한 것 같지만, 막상 다른 사람에게 설명하려고 하면 막히는데, 이는 단순히 익숙해진 것뿐이지 이해한 것이 아니기 때문입니다.

사실 오픈소스는 공부하기 쉬운 편은 아닙니다. 겉으로는 외부 참여자를 독려한다고 하지만, 실제 중요 프로젝트들은 대부분은 해외 대기업들이 주도하고 있고, 오픈소스에 참여할 수 있는 실력 있는 개발자들의 수도 한정되어 있습니다. 따라서 외부 개발자의 참여를 독려하기 위한 자료나 문서를 제공하는 경우도 드뭅니다. 다행히 The Architecture of Open Source Applications와 같이 여러 오픈소스 아키텍처에 대해 소개하는 책이 나오기도 했습니다.

저는 우리나라에서 개최되는 개발자 컨퍼런스에 대해서는 부정적입니다. 세미나 혹은 컨퍼런스라고 부르기에 민망할 정도로 그저 외국의 최신 기술을 소개하는 수준이고, 해당 기술의 개발자도 아니면서 최신 기술만 다루려고 하다보니 내용도 수박 겉핥기에 그치고 있습니다. 이런 컨퍼런스는 개발자들이 뭔가를 배우고 싶다는 갈증을 순간적으로만 해소시켜주지 개발 역량을 실제로 개선해주지 못합니다.

우리나라가 다른 기술의 수준에 비해 유독 소프트웨어 역량만 떨어지는 것이 우연히 아니라고 생각합니다. 글을 읽고, 비판적으로 생각하고, 쓰는 훈련이 부족한 나라는 소프트웨어를 제대로 만들 수 있는 기초 역량도 부족할 수밖에 없습니다. 같은 맥락에서 서두에 언급한 초중고 코딩 교육이라는 것도, 이런 근본적인 문제를 해결하는 방법을 찾는 데 집중해야 합니다. 12년을 국영수를 가르쳐도 못 하는 논리적 사고가 똑같은 방식의 코딩 교육을 통해 갑자기 생길까요?

컴퓨터공학과 나와도 코딩 ‘쩔쩔’

오늘 페이스북을 보니 컴퓨터공학과 나와도 코딩 ‘쩔쩔’ 기사에 대한 이야기가 많던데, 이 블로그 주제가 코딩 스쿨인 만큼 코딩 교육에 대한 이야기를 해볼까 합니다.

사실 기사 자체는 새로운 내용도 없습니다. 컴퓨터공학을 전공한 대학 졸업생들이 현업에 투입될 만큼 코딩 실력이 없고, 원인은 정부 정책이나 교육에 있다는 겁니다. 제가 놀란 것은 기사가 아니라 댓글의 반응입니다. 이 기사에 대한 주된 비판은 컴퓨터공학과는 단순 기능에 불과한 “코딩”이 아니라 알고리즘, 데이터 구조, 운영체제, 네트워크, 소프트웨어 엔지니어링 등 컴퓨터 과학과 이론을 습득하는 곳이기 때문에 컴퓨터공학과 졸업생이 6개월 정도 코딩만 배운 학원생들에 비해 코딩을 못할 수도 있다는 겁니다.

그럴 듯한 변명이지만 현실과는 다릅니다. 댓글에 따르면 컴퓨터공학과 졸업생들이 4년 내내 컴퓨터 이론을 습득하느라 너무 바쁜 나머지 코딩에 쓸 시간이 없었다는 이야기인데, 그렇다면 다소 코딩은 미숙해도 이론은 컴퓨터학원 출신과는 비교할 수 없는 수준으로 잘 알고 있어야 할 겁니다. 하지만 제가 지난 7년간 (상위권 대학) 컴퓨터공학과 학생들을 면접해 본 결과는 오히려 정반대입니다. 그나마 API 문서보고 앱이나 웹페이지 정도는 만들어 본 경험은 있어도, 컴퓨터 이론을 제대로 공부한 학생은 손에 꼽을 정도로 적었습니다. 일례로, 분명히 알고리즘 수업을 들었는데도, Big-O 표기법이 무슨 뜻인지, binary tree 검색 시에 time complexity가 얼마인지도 모르는 친구들이 적지 않습니다. OS의 가상 메모리가 어떻게 구현되고, 이게 C/C++의 malloc()new와 어떤 관계가 있는지 설명할 수 있는 학생은 손에 꼽을 정도로 적었습니다.

많이 양보해서 이론 공부하느라 코딩할 시간이 없었다는 게 사실이라고 쳐도 여전히 문제입니다. 컴퓨터공학은 이론과 실습의 균형이 중요합니다. 내가 이론으로 배운 것들이라고 해도, 실제로 만들어보지 않고서는 제대로 이해하기가 불가능하기 때문입니다. 알고리즘과 데이터 구조 수업을 들으면서 여러 알고리즘 실제로 구현해보고, 운영체제 수업을 들으면서 간단한 운영체제를 직접 만들어보고, 프로그래밍 언어나 컴파일러 수업을 들으면서 간단한 프로그래밍 언어를 직접 구현해 보지 않고서나 해당 과목을 이수했다고 말하기가 어렵습니다. 수업을 듣고 당장 한 학기만 지나도 아마 대부분의 내용이 기억에서 사라질 겁니다.

또한 대학이든 회사든 “코딩”을 단순 기능으로 생각해서 교육하지 않는 것이 문제입니다. 대학 졸업생은 물론이고 10년 이상된 개발자들 중에서도 코딩을 제대로 하는 사람이 별로 없는 이유는, 어떻게 하면 코딩을 잘하는지에 대해 고민하지 않을 뿐만 아니라 소프트웨어 아키텍처나 비지니스 로직, 머싱 러닝 알고리즘 등 그럴 듯한 말에 비해 상대적으로 저급한 일로 취급하기 때문입니다. 코딩을 글쓰기에 많이 비유하는데, 이런 상황은 “노인과 바다”와 같은 대작을 머리 속에 그리고 있는데 글쓰기 실력은 초딩 수준인거나 마찬가지입니다.

코딩 잘 못하는 소프트웨어 대가 같은 건 없습니다. 1년 이상 자기 손으로 코드 한 번 안 짜본 사람이 와서 소프트웨어 아키텍처나 프로세스에 대해 이야기하면 바로 무시하시기 바랍니다. 이론도 마찬가지입니다. 해당 분야에 특허를 내고 논문을 쓴다고 소프트웨어를 만들 줄 아는 게 아닙니다. 변수 이름 하나 수준에서 많은 고민을 하고 좋은 코드를 작성하기 위해 오랜 시간 고민하고 실제로 코드를 작성해야 대학에서 배운 이론이라는 것도 쓸모가 있습니다.

대학에서 이론 안 가르치고 코딩 가르친다고 문제 해결되는 것도 아니고, 지금처럼 컴퓨터공학과는 고급 지식을 배우는 곳이니 코딩 따위는 못할 수도 있다고 당당하게 이야기하는 것도 해결책이 아닙니다. 지금 컴퓨터공학과, 소프트웨어 엔지니어의 문제점은 그저 절대적인 학습량, 절대적인 코딩량이 낮은데 있습니다. 대학에서는 이론도 지금보다 더 많이 가르쳐야 하고, 실습 과제도 더 많이 내서 컴퓨터공학과 졸업하는 것 자체가 자부심이 될 정도로 기준을 높여야 합니다.

대학이 졸업생들의 실력을 보증하는 방법은 이미 의대에서 찾을 수 있습니다. 의대는 사람의 생명을 다루는 직업인 만큼 이론과 실습 모두 일정 수준 이상 도달하지 않으면 유급도 시키고, 의사자격증도 안 주는 방식으로 일종의 품질 보증을 하고 있습니다. 컴퓨터공학과처럼 취업 잘 되는 학과라고 사람 많이 뽑아서 졸업장 팔아 장사하는 모델이 되면 졸업생의 품질을 보증하지 못하는 건 어찌 보면 당연한 것 아닐까요?

코딩을 잘해야 하는 이유

이 블로그는 게임 개발자들이 코딩을 좀 더 잘할 수 있는 방법을 주제로 다루고 있습니다. 객체지향 프로그래밍, 함수형 프로그래밍 등 프로그래밍 언어에 관한 이야기도 있고, 디자인 패턴, 코드 스멜 및 리팩토링, 테스팅 등 소프트웨어 엔지니어링 프랙티스에 관한 이야기도 있습니다.

이런 이야기를 쓰는 이유는 제가 경험한 게임 개발은 다른 분야에 비해서 프로그래밍 실력이나 개발 방법론 등 여러 면에서 상당히 뒤떨어져 있었기 때문입니다. 예를 들어, 게임 개발팀이 코드 리뷰를 전혀 하지 않고 있다는 사실을 알았을 때 충격이었고, 어떤 PD는 코드 리뷰가 안 그래도 바쁜 게임 개발에 마이너스 요인이라고 말하는 것을 듣고 더 충격이었습니다.

개발자들이 해보지도 않고 “저 포도는 시어서 못 먹을거야”라고 말하는 것 아닙니다. 어떤 개발 방법론 등이 좋다는 얘기를 듣고 어설프게 1-2번 시도를 해보다가 초기 시행 착오 단계만 거치고 이 방법이 게임 개발에는 안 맞다고 결론낸 경우가 더 많았습니다. 코드 리뷰만 해도 코드 리뷰를 통해 무엇을 얻을 것인지 많이 고민하고 경험하는 일이 필요한데, 코드 리뷰 경험 없는 개발자들에게 모든 코드는 상호 리뷰해야 한다는 원칙만 가지고 강제하면 리뷰를 위한 리뷰에 그치기 때문입니다.

가장 큰 문제는 게임 개발이라는 특수성을 지나치게 강조하는데 있습니다. 게임 개발은 일반적인 소프트웨어 개발과 다르기 때문에 이미 다른 소프트웨어에서는 당연하게 여기는 기본적인 프랙티스조차 게임 개발과는 맞지 않다고 결론 내리고 비효율적인 방법을 고집하는 경우가 꽤 있습니다. 특히, 게임의 특성상 기획이 자주 바뀌기 때문에 코드를 처음부터 잘 만들 필요가 없다고 말합니다. 하지만 개발 방법론 자체가 변화하는 요구사항에 쉽게 대응하려고 나왔다는 사실을 생각하면 이 말이 설득력이 없다는 사실을 쉽게 알 수 있습니다.

물론 코딩 실력이 게임 개발에 있어서 가장 중요한 요소이면 이런 이야기를 할 필요도 없습니다. 자연스럽게 코딩을 잘 하는 팀이 성공하고, 그렇지 못한 팀은 도태될 것이기 때문입니다. 하지만 게임 개발이라는 게 종합적인 예술이고, 성공/실패의 차이가 워낙 큰 흥행 산업이다 보니 코딩 하나만 가지고 성공/실패의 인과 관계를 따지기가 어렵다는 것이 문제입니다.

사실 게임 개발사 입장에서 코딩을 잘하는 게 중요하지 않을 수 있다고 생각합니다. 어차피 출시도 못 하는 게임이 즐비한 판에 오랜 기간 유지보수를 잘하기 위한 코드를 준비한다는 게 이치에 맞지 않고, 게임이 한 번 성공하면 어느 정도의 비효율성을 상쇄하고도 남을 만큼 큰 레버리지가 나오기 때문입니다. 개발이 200% 정도 비효율적이라고 해도, 게임이 성공하면 그 정도는 개발자 많이 충원해서 해결하면 된다는 결론이 나오는 것이죠.

하지만 개발자 입장에서 코딩을 잘하는 건 중요합니다. 코드의 품질이 곧 삶의 품질이기 때문입니다. 아무 것도 아닌 버그 하나 잡으려고 며칠 밤을 꼬박 새거나, 계속해서 바뀌는 요구사항을 땜질로만 대응해서 기능 하나 만들면 다른 기능 2-3개가 안 되는 상황이 되면 개발자만 괴롭습니다. 어쨌거나 시간은 정해져 있고, 소중한 시간에 뭔가 유의미한 것을 만들지, 땜질만 할 것인지는 개발자의 코딩 실력이 결정하기 때문입니다.

회사나 PD가 대충 돌아가게만 짜라, 코드 리뷰하지 마라, 테스트하지 마라고 이야기하는 건 내 삶의 품질을 희생해서 개발 일정을 맞추라는 말입니다. 근데 이게 역설적으로 게임 개발이 일정을 못 맞추는 가장 큰 이유입니다. 코드 품질을 희생하는 건 은행에서 빚내는 것과 마찬가지라서 언제가는 갚아야 하고, 미룰수록 더 커지기 때문입니다. 빚내는 사람과 갚는 사람이 따로 있는 경우도 많습니다. 엉망으로 짠 코드를 나중에 유지보수하는 사람은 남이 빌린 빚을 대신 갚아주는 노예나 마찬가지인 셈입니다.

정리하면, 개발자에게 복지란 코드의 품질입니다. 그리고 코딩의 잘해야 하는 이유는 그게 개발자에겐 삶의 질이기 때문입니다.

성능 최적화를 위해 꼭 알아야 할 숫자들

모바일 게임은 핸드폰에서 돌릴 수 있는 가장 무거운 소프트웨어 중 하나이기 때문에 출시 전 성능 최적화가 반드시 필요합니다. Peter Norvig이 쓴 Teach Yourself Programming in Ten Years을 보면 성능 최적화를 위해 개발자가 반드시 숙지하고 있어야 할 숫자들이 나옵니다.

  • execute typical instruction: 1/1,000,000,000 sec = 1 nanosec
  • fetch from L1 cache memory: 0.5 nanosec
  • branch misprediction: 5 nanosec
  • fetch from L2 cache memory: 7 nanosec
  • Mutex lock/unlock: 25 nanosec
  • fetch from main memory: 100 nanosec
  • send 2K bytes over 1Gbps network: 20,000 nanosec
  • read 1MB sequentially from memory: 250,000 nanosec
  • fetch from new disk location (seek): 8,000,000 nanosec
  • read 1MB sequentially from disk: 20,000,000 nanosec
  • send packet US to Europe and back: 150 milliseconds = 150,000,000 nanosec

최적화에서 위 숫자가 중요한 이유는 각 최적화마다 기대되는 효과가 천차만별이기 때문입니다. 예를 들어, 실제로는 네트워크 병목이 문제인 게임에서 뮤텍스 락을 최적화해서 기대되는 효과는 무척 낮을 수밖에 없습니다. 위 표를 보면 미국에서 유럽으로 패킷을 보내고 받을 때 걸리는 지연 시간(latency)는 뮤텍스 락을 잡고 푸는데 걸리는 시간보다 무려 6,000,000배나 깁니다.

하드웨어 성능은 매년 발전하기 때문에 위 표의 숫자를 기계적으로 다 외울 필요는 없습니다. 다만, 각 연산에 걸리는 시간의 차이를 상대적으로는 기억할 필요가 있습니다. L1 캐시(cache)에서 데이터를 가져오는 데 걸리는 시간은 0.5 nanosec이고, 캐시 미스(cache miss)가 나서 메인 메모리에서 데이터를 가져오게 되면 100 nonosec이 걸립니다. 캐시 미스가 나면 페널티로 200배 가까운 시간이 더 걸리는 셈입니다.

그런데 저 표를 외우기가 쉽지 않습니다. 보통 사람은 nanosec이라는 시간에 대해 전혀 감이 없습니다. 1초도 눈 깜짝할 사이기 때문에 1초의 1/1,000이나 1/1,000,000이나 1/1,000,000,000이나 다 그냥 짧은 시간처럼 느껴지기 때문입니다. 최적화에서는 어차피 상대적인 시간이 중요하기 때문에 위 표를 외우는 좀 더 쉬운 방법은 1 nanosec을 1 sec로 생각하고 우리가 일상적으로 쓰는 단위로 환산해 보는 것입니다.

  • execute typical instruction: 1/1,000,000,000 sec = 1 초
  • fetch from L1 cache memory: 0.5 초
  • branch misprediction: 5 초
  • fetch from L2 cache memory: 7 초
  • Mutex lock/unlock: 0.5 분
  • fetch from main memory: 1.5 분
  • send 2K bytes over 1Gbps network: 5.5 시간
  • read 1MB sequentially from memory: 3 일
  • fetch from new disk location (seek): 13 주
  • read 1MB sequentially from disk: 6.5 달
  • send packet US to Europe and back: 5 년

이렇게 놓고 보면 nanosec으로 보는 것에 비해 시간의 상대적이 길이가 한 눈에 들어옵니다. 뮤텍스 락을 잡고 푸는데 걸리는 시간은 0.5분이고, 미국에서 유럽으로 패킷을 보내고 받는데 걸리는 시간은 무려 5년입니다. 비유를 하자면, 네트워크가 병목인 소프트웨어에서 뮤텍스 락 잡는 시간을 줄이는 최적화를 하는 것은, 유럽 미국 왕복 여행을 하는데 비행기 시간 줄일 생각은 안 하고 아침에 세수하는 시간 줄이는 수준의 일을 하는 것입니다.

최적화 작업을 수행하기 전에는 최적화의 오래된 격언을 항상 상기하시기 바랍니다.

Premature optimization is the root of all evil (섣부른 최적화는 모든 악의 근원이다.)

Unity가 직면한 기술적 문제들 2

Unity가 직면한 기술적 문제들을 생각보다 많은 분들이 보시고 피드백을 주셔서 관련하여 제 의견을 한 번 더 정리합니다.

일단 오해의 소지를 줄이기 위해 한 가지 말씀드리면, 이 글은 제가 지난 1년 2개월 Unity를 이용한 모바일 게임 개발에 참여하며 개발자로서 느낀 기술적인 문제점을 정리한 것입니다. 모바일 게임 엔진의 향후 전망 같이 거창한 이야기는 절대 아니고, Unity 사용자가 작성한 피드백 정도로 이해하시면 됩니다. 또한 게임 엔진으로서 Unity 전체에 대한 종합적인 평가가 아니라 .NET 런타임 지원에 국한한 기술적인 문제만 제기한 것이므로 이 부분도 감안해서 읽으시길 권해 드립니다.

글을 올린 후에 많이 받은 피드백 중에 하나가 C#은 유니티 게임 엔진에서 단순히 스크립팅 언어로 사용될 뿐인데, 최신 .NET 런타임을 완벽히 지원하는 것이 중요하느냐라는 질문입니다.

이 피드백에 대해 제 생각을 말씀드리기 위해서는 일단 용어 정의부터 명확하게 해야할 것 같습니다. 프로그래밍 언어는 크게 프로그래밍 언어 자체와 런타임으로 나뉩니다. 런타임은 표준 라이브러리라고도 불리는데, 프로그래밍 언어와 불가분의 관계에 있어서 보통은 언어와 런타임을 통칭하여 프로그래밍 언어로 부릅니다. 예를 들어, C 언어는 C 언어와 C 표준 라이브러리고 구성이 되고, 자바는 자바 언어와 JDK로, C#은 C#과 .NET 런타임으로 구성이 되는 것이죠.

우리가 스크립팅 언어로 흔히 사용하는 언어로 JavaScript와 Lua가 있는데, 이들 언어의 공통점은 런타임이라고 부를 수 있는 요소가 굉장히 작다는 점입니다. 바꿔 말하면, 프로그래밍 단독으로는 거의 할 수 있는 일이 없습니다. 웹브라우저에서 DOM에 접근할 수 있다거나, node.js에서 HTTP 서버를 띄울 수 있는 이유는 JavaScript가 이런 기능을 제공하기 때문이 아니라, 웹브라우저나 node.js가 제공하는 기능을 JavaScript라는 스크립팅 언어를 통해 접근하는 할 수 있기 때문입니다. 이런 언어들은 언어 런타임이 굉장히 작기 때문에 이식성도 높고, 임베딩도 쉽게 할 수 있다는 장점이 있습니다.

이런 제약이 싫다면 Python이나 Ruby를 스크립트 언어로 사용할 수 있습니다. 이들 언어는 JavaScript나 Lua와 달리 상당히 방대한 양의 언어 런타임(표준 라이브러리)을 제공하고 있습니다. 예를 들어, Python은 로컬 파일을 읽는 기능이나, 쓰레드 생성, 네트워크 통신 등을 별도의 통합 작업 없이도 언어 런타임이 기본으로 지원합니다. 이런 기능은 필요에 따라 상당히 편리하게 이용될 수도 있으나, 반대로 임베더 입장에서는 열어줘서는 안 되는 기능을 의도치 않게 열게 될 수도 있습니다. 일례로, 만약 웹브라우저가 파이썬을 스크립팅 언어로 제공했다면 갑자기 웹브라우저의 기능과 상관 없이 Python의 표준 라이브러리를 사용하여 로컬 파일을 읽어가거나 네트워크 통신을 하는 일이 가능해집니다.

따라서 언어 런타임에 많은 기능이 포함된 언어를 스크립팅 언어로 사용하기 위해서는 어떤 API를 열어줄지 엄밀하게 고민해서 API를 부분적으로 잘라내거나, 기능에 접근할 수 없게 막는 장치가 필요하게 됩니다. 사내에서만 사용하는 스크립팅 언어라면 내부적으로 합의만 하면 되지만, Unity처럼 SDK 형태로 나가는 경우에는 상황이 달라집니다. 공개된 모든 API는 사용될 가능성이 있기 때문입니다.

Unity는 .NET을 스크립팅 언어로 선택했습니다. 명시적으로는 C#, UnityScript, Boo 등 구체적인 프로그래밍 언어를 지원한다고 하지만, 이들 언어 모두 .NET 기반이고 F#과 같이 지원이 명시되지 않은 언어도 Unity에서 동작에 문제가 없으므로 Unity의 스크립팅 언어는 사실상 .NET이라고 봐야 합니다. 그리고 .NET을 스크립팅 언어로 선택한 것은 축복이면서 동시에 저주가 되었다고 생각합니다.

Unity가 초기 모바일 게임 시장에 빠르게 진압할 수 있었던 이유에는 게임 엔진의 우수함이나 에디터의 편리함도 있겠지만, .NET 플랫폼을 활용한 크로스플랫폼 개발 환경이라는 요소를 무시할 수 없다고 생각합니다. C#이라는 편리한 개발 언어, .NET이 제공하는 수많은 API는 런타임 없이 간단한 스크립팅 언어만 제공하는 엔진에 비해 높은 생산성을 제공했기 때문입니다. 또한 .NET에 존재하는 수많은 써드파티 라이브러리도 별다른 노력없이 확보할 수 있었습니다.

하지만 Unity의 .NET 기술 지원이 뒤처지면서 초기의 이런 장점들은 점점 빚이 됩니다. .NET 4 이후로는 사실상 별도의 섬처럼 생태계가 고립되기 시작하였고, 더 이상 .NET 라이브러리를 Unity에서 그대로 사용할 수 없어 포팅 작업을 하거나 별도의 라이브러리를 제작해야 하는 상태가 되었습니다. 또한 LINQ, async/await 등 C#과 .NET의 최신 기능을 제공하지 않으면서 그 간극은 점점 더 멀어지고 있는 상태입니다.

일례로, 이전 글에서 이야기한 것처럼 LINQ를 지원하는 것도 아니고 지원하지 않는 것도 아닌 채로 방치하는 것은 심각한 문제입니다. .NET을 스크립팅 언어로 채택했으면, 아에 LINQ를 사용할 수 없게 API를 잘라냈거나 제대로 지원을 했어야 합니다. 이런 상황은 껍데기는 예쁘게 포장했는데 막상 포장을 뜯어보니 정작 내용물은 여기 저기 상해 있는 과일 상자와 다를 바가 없습니다.

쓰레드를 사용하지 말라고 이야기하는 것도 무책임한 말입니다. .NET을 스크립팅 언어로 채택했으면 쓰레드를 지원하거나 쓰레드 지원이 Unity 게임 엔진 사용에 문제가 된다면 아에 쓰레드 API를 제외했어야 합니다. LINQ와 마찬가지로 100% 지원하는 것도 아니고, 그렇다고 안 되는 것도 아닌 상태로 출시를 하면 당연히 .NET에 익숙한 개발자들이 쓰레드를 사용하게 되고 여러 가지 문제점을 겪으며 고생할 수밖에 없습니다.

물론 게임 엔진이라는 게 내부적으로 태스크(task) 혹은 잡(job)을 관리하기 위한 스케쥴러가 있고, 병렬적으로 동작하므로 단순히 게임 로직만 작성하는 스크립팅 언어에서 굳이 쓰레드를 사용하거나 복잡한 일을 해서는 안 된다고 조언할 수는 있습니다. 하지만 어린 아이는 다칠 위험이 없는 안전한 방에서 놀게 해야지, 온갖 위험한 물건들이 즐비한 방에서 놀게 하고선 위험한 물건은 만지지 말라고 조언해서는 안 된다고 생각합니다.

또한 async/await 키워드로 대표되는 C#의 비동기 프로그래밍은 단순히 멀티쓰레드 프로그래밍과는 개념이 다릅니다. 오히려 Unity가 코루틴을 이용해 어설프게 해결하고 있는 동시성(concurrency) 혹은 멀티태스킹(multitasking)을 쉽게 작성하기 위해 고안된 방법에 가깝습니다. 게임이라는 게 로직만 작성하더라도 동시성을 표현하지 않을 수 없는데, 이런 동시성을 표현하는 가장 효과적인 방법이 async를 이용한 비동기 프로그래밍이기 때문입니다. 참고로 대표적인 싱글 쓰레드 모델의 프로그래밍 언어인 JavaScript 또한 ES7에서 C#의 async와 유사한 async 함수 도입을 논의하고 있습니다.

Unity는 어쨌거나 .NET을 채택했으므로 이왕이면 발전하고 있는 .NET 기술의 혜택도 누리는 게 맞는데, 이러지도 저러지도 못하고 구버전 .NET에 갖혀 있는 상태가 안타깝다는 생각이 듭니다. 마이크로소프트나 Xamarin과 협상이 잘 되어서 돌파구를 찾거나 IL2CPP 개발에 더 박차를 가해서 당면한 문제를 풀기를 기대해 봅니다.