이번 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를 만번 더해 보고 그에 대한 결과를 봅시다.

#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;
}
결과는 2500으로 정확하게 나옵니다.


자, 이제는 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처리 방식도 바꿔야 겠죠. 결국 시스템의 많은 부분을 바꾸어야 한다는 얘기. 상식적으로 이러한 일은 하루 이틀에 끝날 일도 아니고, 최소한 몇개월 작업을 해야 하는 건데, 뭐, 당장 수시접수 처리를 해야 하는 마당에 근본적인 해결 없이 땜빵으로 처리를 하겠죠. 결국 잠재적인 버그를 항상 가지고 가는 시스템을 계속해서 가지고 갈 수 밖에 없는 현실. 실수하면 또 프로그래머 잘못...


애시당초부터 이러한 작동(부동소수점 표현은 정확하지 못한다는 것을 인지하고 있어야 하는)을 알고 있더라면 설계 단계에서부터 이러한 점을 고려해서 설계를 했어야죠. 아니면 처음 설계 방식에는 정수만 썼다가, 나중에 (동점처리자들을 처리하기 위해서)소수점 처리 방식을 채택했던지 해서 발생한 일일 수도 있구요.


그런데 참 이상한게, 최소한의 전산 지식이 있는 사람이 프로젝트 진행 도중에 이러한 잠재 버그의 가능성을 강조했었다면 발생하지 않아도 되는 버그였는데, 왜 아무도 그러한 걸 얘기하지 않았을까요(혹은 못했을까요?) 뭐, 원인은 뻔합니다. 이러한 현상(부동소수점은 정확한 숫자 표현을 하지 못한다)을 프로젝트 참여자들 중에 한명도 모랐다는 건 말이 안되구요, 무리한 일정에 빨리 기능 구현을 해야 하는 압박감, 잠재적 버그라고 강조해서 얘기하다 보면 괜히 나서는 놈으로 찍히고 덤탱이 쓰게 되는 부담감 등이 작용을 해서였겠죠.


답답한 현실입니다.