이번 NEIS 시스템에서 성적 처리 오류로 인해 그 여파가 꽤 커지고 있군요.
교과부 관계자는 "책임 소재를 묻기 보다는 일단 문제를 해결하는 것이 급선무"라면서도 "개발자에게 원초적인 책임이 있지만, 교과부도 관리·감독에 대한 책임을 피하기는 어려울 것"이라고 말했다.
SW개발은 코딩에서부터 무수히 많은 에러가 발생할 수 있습니다. 그러한 에러를 보정하기 위해 무수히 많은 단계를 거치게 됩니다. unit 테스트, 통합 테스트, QA 및 QC, 등등... 그리고 NEIS정도의 규모라면 단순 개발 단위의 테스트뿐만 아니라 전문적인 인력을 동원해 감리 과정까지 거쳤겠죠. 그런데 뭐? 개발자에게 원초적인 책임? 말이 안나오는군요. 건물이 무너 졌는데, 애초 설계상의 문제점은 보지 못하고, 현장에서 작업한 이들에게만 그 책임을 전가하려고 하는군요.
"차세대 나이스 프로그램 개발 업체인 삼성SDS 프로그래머들의 실수가 있었다"고 한국교육학술정보원 관계자가 24일 밝혔다.
"나이스에서 성적과 관련된 프로그램은 삼성SDS의 프로그래머 7명이 맡아 제작했는데, 프로그램을 정밀하게 짜지 못했다"고 말했다.
엥? 삼성SDS에 웬 프로그래머? 삼성이 언제 설계&코딩 작업을 한 적이 있나요? 하청에 하청 주고 끝냈겠죠.
소수점 이하 자릿수 가운데 평가 결과와 상관없는 '1'이란 숫자가 느닷없이 표시되는 현상이 발생해 고교생 2만9000명의 내신석차가 틀렸다"고 말했다.
이건 또 무슨 개소리? 컴퓨터는 GIGO입니다. Garbage In Garbage Out이죠. 쓰레기 값이 들어 가면 쓰레기 처리가 된다는 말. 즉, 다시 말해서 올바른 입력이 되었다면 올바르게 나올 수 밖에 없다는 것입니다. "느닷"없다뇨? 기술적인 측면에서 정확한 원인을 파악해야죠.
통상 컴퓨터가 숫자를 처리하는 과정에선 이처럼 엉뚱한 수치(이른바 '쓰레기값')가 나오는 경우가 드물게 발생하는데 이를 잡아주는 작업을 실수로 빠뜨렸다는 것이다. 신 부장은 "쓰레기값은 소수점 이하 16개 자릿수 중 일정한 자리에서 발생한 것이 아니라 불규칙적으로 여러 자리에서 나왔다"고 했다.
이런 말이 안나오는군요. HW 고장이 아닌 이상, 컴퓨터는 거짓말을 하는 법이 없습니다. 쓰레기값이라뇨? 근본 원리를 모르기 때문에 이러한 무식한 변명이 가능한 거죠.
문제는 컴퓨터가 소수점 32번째 자리까지 인식을 하고 마지막 자리에는 임의의 숫자를 붙인다는 점. 이에 따라 프로그램 개발자는 통상 소수점 16번째 자리까지만 값을 인식하도록 인위적으로 계산 방식을 보정해야 한다. 이번 사태는 이 보정 과정을 빼먹으면서 일어난 것이다.
혹시나 했는데, 본 기사를 보고 확실히 알게 되었습니다. 역시나... 점수 처리에 부동 소수점을 사용하는군요. 허, 할 말이 없습니다. 정확한 수치의 표현을 해야 하는 곳에서 부동소수점을 이용하다니... 정확한 얘기를 해 보죠. 요즘에는 64bit CPU가 보급이 되었지만, 아직까지도 32bit CPU를 많이 사용합니다. 숫자를 표현하는 데 있어서 float와 같은 32bit형 부동소수점 타입을 사용한다는 말을 가지고 "소수점 32번째"라고 표현을 했네요, ㅎㅎㅎ. 기자도 참... 그냥 한번 웃어 주시고...
자, 지금까지는 언론 보도의 내용들이었고, 이제부터는 기술적인 얘기를 해 보고자 합니다.
외계 행성이 발견이 되었습니다. 이 행성에는 띠띠뿌라는 종족이 살고 있있고, 추후 지구인은 이 띠띠뿌라는 종족과 상업적인 거래를 하게 되었습니다. 이 띠띠뿌라는 종족은 손가락이 3개입니다. 지구인은 (손가락이 10개라서) 0~9까지의 숫자를 사용하는 반면에, 띠띠뿌 종족은 (손가락이 3개라서) 0, 1, 2 단 3개의 숫자만을 사용합니다. 상업적인 거래를 위해서 10진수 ~ 3진수 변환이 필요하게 되었습니다.
자, 이제 소수점으로 가 보죠. 띠띠부 종족 3명이 일을 해서 지구인으로부터 일당을 받았습니다. 그 일당을 3명이 나누어서 받습니다. 얼마씩 나눠 가지면 될까요?
지구인 입장에서는 1/3씩 나눈다고 생각을 하겠죠. 10진수로 표현을 하자면 0.3333.... 하지만 띠띠부 종족은 어떻게 생각할까요? 띠띠뿌 종족은 "0.1"씩 나눠 가지면 되는구나 하고 생각을 합니다. 즉, 띠띠뿌 종족이 0.1이라고 표현하는 것은 지구인이 0.3333..... 으로 표현하는 것과 일치하는 숫자입니다. 다시 말해서 1/3은 지구인은 정확하게 표현할 수가 없는 반면에 띠띠뿌는 정확하게 0.1로 표현을 할 수가 있습니다.
2진수와 10진수의 관계도 마찬가지입니다. 10진수에서 0.6과 같은 숫자는 2진수로 표현을 하면 정확한 표현을 할 수가 없습니다. 즉 10진수에서 0.6은 유한 소수이지만 똑같은 숫자가 2진수에서는 무한 소수(0.1101110111011101....b)가 된다는 것입니다. 이처럼 진법 변환에 있어서 정확한 표현을 하지 못할 수 있는 경우가 생기는 것은 당연한 것입니다. 컴퓨터를 생각할 때에는 컴퓨터의 입장에서 2진수로 생각을 해야 합니다.
10진수에서 2/3 가 0.66667 로 표현된 것을 가지고, "0.66667은 0.00001이라는 쓰레기가 추가된 값이다"라고 하면 안되는 겁니다. 근사치라고 얘기를 해야죠.
컴퓨터에서는 숫자를 표현하는데 있어서는 BCD(binary-coded decimal)방식이라는 게 있습니다. 지금은 거의 쓰지는 않지만 예전 프레임워크에서는 종종 볼 수가 있죠.
BCD 방식은 하나의 십진수를 표현하기 위해서 4개의 bit를 사용하는 방식입니다. 예를 들면 "64"를 표현하기 위해서는 "0110 0100"이라고 표기하는 겁니다. 이 방식(BCD 방식)은 정수뿐만 아니라, 부동소수점을 표현할 때에서도 초기에는 BCD 표기법이 채택이 되었었습니다. 왜냐? 인간에게 익숙하기 때문이었죠. 하지만 BCD는 bit의 낭비를 가져 오고 CPU 디자인이 복잡해 지는 단점들이 있죠. 그래서, 요즘에는 대개가 정수나 소수 모두 BCD를 사용하지 않고 순수 2진수 기반으로 처리를 하는 것이 일반화되었습니다.
즉 부동소수점에서 숫자의 표현은 1/2, 1/4, 1/8, ... 등의 조합으로 숫자를 표현을 한다는 것입니다. 자세한 사항은 여기에서 : Single precision floating-point format
쉽게 말해 보죠. 컴퓨터는 1/2(0.5), 1/4(0.25), 1/8(0.125), 1/16(0.0625) 과 같은 숫자들은 컴퓨터로 정확하게 표시를 할 수 있습니다. 0.75는 0.5와 0.25의 덧셈으도 쉽게 표현할 수 있습니다. 하지만 0.6과 같은 숫자는 정확히 표현할 수가 없습니다. 이는 십진수에서 1/5는 0.2로 정확히 표현할 수 있는 반면에 1/3은 0.3333... 으로 정확하게 표시를 하지 못하고 근사값(0.3333)으로밖에 표시를 하지 못하는 것과 동일한 원리입니다.
다시 말해서 인간은 10진수에 익숙해 있는 반면에 컴퓨터는 (정수 표현이든 소수 표현이든) 2진수 기반으로 처리를 하고 있으며 10진수로 쉽게 표현할 수 있는 숫자가 2진수로 표현하려면 정확하지 못할 수도 있다는 겁니다. 그 현상을 "쓰레기값이 나온다"라고 언급하는 것은 무식해서라고 밖에 볼 수 없습니다.
자, 위에서 말했듯이 컴퓨터는 0.25를 정확히 표현을 할 수가 있다고 했죠. 0.25를 만번 더해 보고 그에 대한 결과를 봅시다.
결과는 2500으로 정확하게 나옵니다.#include <iostream> const int COUNT = 10000; int main() { float a = 0; for (int i = 0; i < COUNT; i++) a = a + 0.25; std::cout << a << std::endl; }
자, 이제는 0.25가 아닌 0.6(컴퓨터가 정확히 표현하지 못하는 숫자)를 만번을 더해 보죠. 위의 코드에서 0.25를 0.6으로만 바꾸어 실행을 해 봅니다.
#include <iostream> const int COUNT = 10000; int main() { float a = 0; for (int i = 0; i < COUNT; i++) a = a + 0.6; std::cout << a << std::endl; }
결과는 6000.58이 나옵니다. 6000이라는 정확한 결과가 도출되지 않습니다.
이렇게 나오는 이유는 간단합니다. 십진수에서 2/3은 0.666666..(무한대) 이 되어야 하는데 자리수 표현의 한계가 있다 보니 0.66667 정도로만 표시를 하고 이 값을 가지고 연산을 하다 보면 엉뚱한 값이 나올 수 있는 것과 마찬가지입니다. 다시 말해서 십진수에서 유한 소수를 표현하지 못하는 한계를 알고 있다면 2진수에서도 똑같은 현상이 나타날 수가 있고, 인간에게 익숙한 십진수의 특정 숫자(예 0.6)가 컴퓨터로 표현을 하게 되면 정밀하지 못할 수 있다는 것은 상식적으로 알고 있어야 합니다.
그런데 기사를 보면 참, 웃음만 나오네요. 이것은 프로그래머의 실수가 아니고, 소수를 표현하는 데 있어서 100% 정밀하지 못한 타입을 사용했기 때문입니다. "보정을 해야 한다"라고 하는데요, 이건 보정한다고 해서 해결될 일이 아닙니다. 숫자의 범위가 커지면, 또 언제 발생할 지도 모르는 일이기 때문입니다. 그리고, 설령 보정을 했다 하더라도, 이러한 근본 원리를 모르는 상태에서 보정 후 결과가 제대로 나온 것을 case by case별로만 확인하고 넘어 간다면, 이번과 같은 실수가 또 발생하지 않을 거라는 보장을 할 수가 없습니다.
본 문제에 대한 해결 방안은 점수를 표현할 때 애시당초 부동수소점을 절대 사용하지 않는 것입니다. 성적처리를 하는데 정수로 처리를 하도록 전체 설계 방식을 바꾸거나, 굳이 소수점을 사용해야 한다면 BigFloat와 같이 무한대의 정밀도를 지원하는 클래스를 설계해서 사용을 해야 합니다. 이에 따른 DB처리 방식도 바꿔야 겠죠. 결국 시스템의 많은 부분을 바꾸어야 한다는 얘기. 상식적으로 이러한 일은 하루 이틀에 끝날 일도 아니고, 최소한 몇개월 작업을 해야 하는 건데, 뭐, 당장 수시접수 처리를 해야 하는 마당에 근본적인 해결 없이 땜빵으로 처리를 하겠죠. 결국 잠재적인 버그를 항상 가지고 가는 시스템을 계속해서 가지고 갈 수 밖에 없는 현실. 실수하면 또 프로그래머 잘못...
애시당초부터 이러한 작동(부동소수점 표현은 정확하지 못한다는 것을 인지하고 있어야 하는)을 알고 있더라면 설계 단계에서부터 이러한 점을 고려해서 설계를 했어야죠. 아니면 처음 설계 방식에는 정수만 썼다가, 나중에 (동점처리자들을 처리하기 위해서)소수점 처리 방식을 채택했던지 해서 발생한 일일 수도 있구요.
그런데 참 이상한게, 최소한의 전산 지식이 있는 사람이 프로젝트 진행 도중에 이러한 잠재 버그의 가능성을 강조했었다면 발생하지 않아도 되는 버그였는데, 왜 아무도 그러한 걸 얘기하지 않았을까요(혹은 못했을까요?) 뭐, 원인은 뻔합니다. 이러한 현상(부동소수점은 정확한 숫자 표현을 하지 못한다)을 프로젝트 참여자들 중에 한명도 모랐다는 건 말이 안되구요, 무리한 일정에 빨리 기능 구현을 해야 하는 압박감, 잠재적 버그라고 강조해서 얘기하다 보면 괜히 나서는 놈으로 찍히고 덤탱이 쓰게 되는 부담감 등이 작용을 해서였겠죠.
답답한 현실입니다.
또다시 프로그래머 탓하는 정부와 삼성SDS : http://bobbyryu.blogspot.com/2011/07/sds.html
곰곰히 생각을 해 봤는데, 왜 점수를 표시할 때 정수가 아닌 실수로 표현했을까요? 제 생각에는 동점자의 경우 당락을 결정짓기 위해서 다른 요인에 해당하는 내용을 소수로 표현을 했을 것으로 보입니다. 이러한 경우 SQL에서 "select* from ... order by score"라는 하나의 명령어도로 순위를 쉽게 정렬할 수 있는 장점이 있기 때문이죠.
결국 어느 누군가가 "점수 표현은 실수로 합시다"라고 했을 것이고, 그에 따라 DB Table Layout이 만들어 졌을 것이고, Native 코드에서도 호환이 되는 타입(float 혹은 double)이 사용되어졌겠죠.
이는 부동소수점의 성질을 알고 있으면 애시당초 이렇게 설계를 하지 않았겠죠. 본 사태의 근본적인 원인을 기술적으로 정확히 얘기를 하자면 "10진수(사람이 입력하는)와 2진수(컴퓨터가 내부적으로 표현하는)의 변환 과정에서 정확한 변환이 안되고 근사치로만 표현될 수 밖에 없다"가 되겠습니다. 이것을 언론에서 "쓰레기값"이라고 얘기를 하는 거구요.
"소수점 이하 자릿수 가운데 평가 결과와 상관없는 '1'이란 숫자가 느닷없이 표시되는 현상이 발생해 "라고 하는데, 참... 기본을 모르니 저런 소리가 나오죠. 우스울 뿐입니다. 1을 3으로 나누었다가 다시 3을 곱하니 0.99999가 나오는 것을 거지고, "에구머니나 1이 아니고 느닷없이 0.99999가 나오네"라고 하는 것과 다를 바가 없습니다.
이러한 엉뚱한 수치가 나올 수 밖에 없도록 구조적으로 만들어 놓고, 일일이 개발자들에게 노가다 처리를 하라고(10진수 아래 몇자리에서 잘라 버려라) 강요하는 것이 과연 올바른 행태일까요? 똥 싸는 놈과 똥 치우는 놈이 따로인 듯한 느낌이군요. 처음부터 똥을 안싸는게 제일 좋은 거죠(실수 사용하지 않고 정수를 사용).
아무쪼록 이번 사태가 좋은 방향으로 해결되었으면 하는 바램입니다.