문서:스파게티 코드

역사 raw
대문 랜덤 문서 최근 토론
1. 설명2. 발생 원인3. 실제 사례4. 난독화


1. 설명

컴퓨터 프로그램의 흐름이 복잡하게 뒤엉킨 모습스파게티가 엉킨 모습에 비유한 표현이다.

스파게티 코드는 작동 자체는 제대로 하거나 하는 것 처럼 보인다. 하지만 해당 코드는 추후 유지보수가 매우 어려워진다. 코드의 작동 방식을 변경하거나 버그를 찾거나 개선하는 등 코드를 수정하는 모든 작업에 방해가 된다. OOP에서의 스파게티 코드는 따로 라자냐 코드라고도 하며 상속 계층이 너무 많을 때 발생한다. 물론 OOP에도 고전적인 스파게티 코드가 생길 수 있다.

이러한 이유로 스파게티 코드는 가독성이 많이 떨어진다는 평이 많다. 특히 이러한 코드는 프로그래밍 초보자들이 많이 작성한다. 심각한 경우 '수정할 바에야 다시 만드는 게 낫다'라는 농담을 하기도 한다.

비전공자를 위해 비유하자면,[1] 전구 1과 전구 2에 전원을 공급해야 할 때 효율적으로 공급한다면 전구 1에 우선 발전기 A와 연결된 전선을 꽂아넣고, 그 전구 1에서 별도의 전선을 하나 뽑아내서 전구 2에 꽂으면 되는것을, 발전기 B를 또 들여놓은 다음 전구 1은 발전기 A와 연결된 전선을 꽂아넣고 전구 2는 발전기 B와 연결된 전선을 꽂아넣는 셈이다. 일단 작동은 정상적으로 되기야 하지만 문제는 발전기 1개만 돌려도 되는걸 2개씩이나 돌리므로 설치 자체도 두배로 힘들고 또 두개의 발전기를 가동시키기 위해 연료 소모가 두배로 늘어나며, 무엇보다 전구를 교체하기 전 안전사고 예방을 위해 발전기를 꺼서 전기 공급을 차단해야 하는데, 설계를 한 당사자야 자신이 발전기를 두개를 썼다는걸 아니까 발전기 A와 B를 모두 꺼놓고 안전하게 작업할 수 있지만, 만약 아무것도 모르는 후임이 와서 전구를 교체하려 하면 발전기 A가 있다는 사실은 알아도 발전기 B가 있다는건 모르기 때문에 발전기 A만 끄고 전구를 교체하다가 아직 전기가 생생하게 살아 숨쉬는 발전기 B와 연결된 전구 2를 교체하다가 감전되는 식이다.

2. 발생 원인

보통 계획없이 바로 코딩을 시작하거나 요구사항이 원래 기획한 범위를 크게 벗어날 때 혹은 프로그래머알고리즘 구현 실력이 지나치게 떨어질 때 주로 출현한다. 발적화도 덤으로 나타나는 경우가 많지만, 스파게티 소스 코드라고 모두 실제 작동에서 비효율적인 것은 아니다.

  • 함수/메소드가 둘 이상의 작업을 한다.
    하나의 함수/메소드에서는 논리적으로 구분되는 가장 작은 단위의 작업을 처리하는 게 이상적이다. 이 규칙을 어겼을 때 로직이 꼬이면서 스파게티 코드가 발생한다. 하나의 함수/메소드의 코드의 길이가 1000줄이 넘는다면 거의 대부분 이런 경우다. 또 다른 기준으로, 로컬 변수의 특정한 그룹이 함수의 일부 영역에서만 변경되고 나머지 영역에서는 단지 읽기만 할 때 그 변수를 마지막으로 조작한 지점까지를 하나의 작업 단위로 정의하고 코드를 분리할 수도 있다. 이러한 면에서 혹자는 함수의 매개변수에 boolean[3]를 받으면 안 된다고 한다.

  • 논리적으로 하나의 작업 단위를 억지로 둘 이상의 함수/메소드로 쪼개는 경우

  • 여러 함수/메소드들이 지나치게 서로서로 의존하면서 얽혀있는것.
    특히 전혀 상관없는 클래스들의 메소드 (인터페이스도 아니고 상속한것도 아닌...)들이 뒤죽박죽 얽혀있으면 그냥 답이 없다. 이럴 경우 메소드 하나를 조금 손 보면 프로그램 전체가 오작동하기 십상이기 때문에 디버깅은 물론이고 기능 확장 같은게 악몽이된다. 귀찮아서 다시 짜지 않고 계속 방치할 경우 언제 터질지 모르는 시한 폭탄이 된다. 거대한 메소드의 가독성을 높이기 위해서 특정 부분들을 작은 메소드들로 쪼개서 코딩하는건 좋지만 너무 마구 엉키지 않게 조심해야 한다.

  • if-else 문의 계층이 지나치게 얽혀 있다.
    여러 조건을 동시에 비교하는 경우에는 진리표패턴 매칭을 대신 사용해야 한다. 순서도를 그렸을 때 마름모(조건분기 기호)가 나무뿌리처럼 얽혀 있는 코드는 훌륭한 스파게티 코드다. 개별 변수 a, b, c 를 if-else 로 하나씩 비교하는 대신 if a and (not b) and c then 'do something' 하는 식으로 모든 조건을 한꺼번에 검사하면서 내려가라는 얘기다. 혹은 별도의 '평가 함수'를 작성하는 방법도 있다. if(eval_function(a,b,c, goal) == true) then 'do someting' 하는 식이다.

    C++에서 진리표를 구현하는 방식은 여러 가지인데 그 중에서 비트 마스크 방식의 진리표는 다음과 같다. int 사용시 32개의 조건을, long 사용시 최대 64개의 TRUE/FALSE 조건을 고속으로 검사할 수 있다.
     #include <stdio.h>
    #include <stdbool.h>
     
     int main(void) {
       bool a = true;
       bool b = false;
       bool c = true;
     
       int condition = ((int)a << 2) | ((int)b << 1) | ((int)c);
     
       printf("%d\n", condition); // 4+1 = 5
     
       if(condition == 0x0005) {
         printf("[a, b, c] == [T ,F, T]\n");
       }
     }
     
    


  • 콜백 함수
    콜백 계층이 지나치게 들어가 코드가 대각선으로 쓰여지는 경우 문제가 된다. 보통 3~5단계를 초과하는 콜백 체인은 스파게티 코드로 간주할 수 있다. 콜백 많기로 둘째가라면 서러울 Node.js가 가장 대표적인 예시. 아예 node.js의 단점을 콜백 지옥이라고 표현할 정도.[5] 콜백 함수 문서 참조.
    fs.readdir(source, function(err, files) {
      if (err) {
        console.log('Error finding files: ' + err)
      } else {
        files.forEach(function(filename, fileIndex) {
          console.log(filename)
          gm(source + filename).size(function(err, values) {
            if (err) {
              console.log('Error identifying file size: ' + err)
            } else {
              console.log(filename + ' : ' + values)
              aspect = (values.width / values.height)
              widths.forEach(function(width, widthIndex) {
                height = Math.round(width / aspect)
                console.log('resizing ' + filename + 'to ' + height + 'x' + height)
                 this.resize(width, height).write(destination + 'w' + width + '_' + filename, function(err) {
                 if (err) console.log('Error writing file: ' + err)
                 })
               }.bind(this))
             }
          })
        })
      }
    });
    


  • 다중 for 루프
    행렬을 처리할 때에는 다중 for루프가 하나의 논리적 처리 단위이므로 다중 for루프 자체는 스파게티 코드가 아니다. 문제는 트리 자료구조 등을 순회할 때 다중 for루프를 통해 처리하는 경우다. 트리 자료구조(대표적으로 XML/JSON)를 처리할 때에는 과감히 재귀함수를 사용해 줄 필요가 있다. 특히 다중 for 루프에서 break와 continue를 마구 남발하면 GOTO문을 사용한 것보다 코드의 흐름을 추적하기 더욱 더 어려워진다[7]. 재귀함수가 스택 깊이를 뚫을 수 있다거나 함수 호출 오버헤드가 있어서 쓸 수 없는 경우가 있어서 다중 for문을 사용해야 하는 경우도 있으나 이런 경우는 흔한 경우는 아니다.

  • 포인터 남용
    포인터는 양날의 검과 같다. 분명 강력하고 유용한 도구이지만 잘못 사용하면 그야말로 재앙이 된다. 값 복사의 오버헤드가 도저히 용납이 안 될 정도로 성능하락을 보이는 게 아니면 포인터 전달보다는 값 복사를 사용하는 편이 좋고, 그게 어려울 때에도 포인터를 '상수'로 간주해 한 번 할당한 값을 변경하지 않는 게 좋고, 그것도 여의치 않으면 최소한 포인터를 조작하는 로직을 코드의 한 군데에 모두 몰아넣어야 한다.

  • GOTO 남용
    항목 참조.

  • 잘못된 프로그램 최적화 방법 남용
    쓸데없는 루프 언롤링, 과도한 인트린식 사용, 매크로 남용 등이다. 컴파일러의 최적화 엔진을 불신하는 꼰대 프로그래머들의 경향이기도 한데 이들이 만든 스파게티 코드는 컴파일러조차 해석에 애를 먹어서 최적화 엔진이 최적화를 포기하기 때문에 저성능, 불안정, 낮은 보안성을 모두 갖춘 쓰레기가 만들어진다.

  • 용도에 맞지 않는 프로그래밍 언어의 사용
    그래픽 프로세싱용 언어로 데이터베이스 로직을 구현하거나 논리 프로그래밍 언어로 기계제어를 시도하는 경우 등. 원래 프로그래밍 언어가 상정한 활용 영역을 벗어나서 코딩하려니 라이브러리 지원도, 문법 지원도 받지 못하고 차라리 어셈블리어가 읽기 쉬워질 지경의 코드를 만들게 된다. 차라리 C++은 만능 언어에 가까우니 그렇다쳐도 PHP로 인공지능 로직을 구현하려고 시도하는 건 도대체...[9]

  • 코딩 스타일이 통일되지 못함
    특히 다소 마이너한 주제의 무보수 오픈소스 프로젝트에서 흔히 일어날 수 있는 일이다. 주제의 매니악함과 보수가 없음으로 인해 유입이 적고 엄격한 기여 규칙을 적용하기 어려워지기 때문이다. 또한 개발기간이 오래된 경우 과거의 잔재들이 남아있을 수 있는데, 이 경우 과거의 잔재가 버그의 온상이 된다. 특히 어느정도 C언어 스타일로 코딩할 수 있는 C++이 이런 경우가 많다.

3. 실제 사례

웹 브라우저넷스케이프가 이 상태였다. 1990년대 후반부터 2000년대 전반까지 벌어진 웹 브라우저 전쟁에서 넷스케이프는 코드 정리 이런 것 없이 급하게 기능을 추가했고, 그 바람에 코드가 점점 꼬여가며 헬게이트를 열었다. 모질라 재단이 이 소스 코드로 브라우저를 만들려고 했다가 포기하고 2년 동안 아예 백지에서 출발해 게코 엔진을 만든 것은 유명한 일화다. 이 엔진으로 나온 것이 모질라 파이어폭스다.

게임 리그 오브 레전드의 캐릭터 렝가는 캐릭터의 스킬셋을 구성하는 코드가 심하게 꼬여 있어서 몇 년째 끊임없이 버그를 수정하는데도 계속해서 버그가 나오고 있다. 2020년 기준으로 발견된 버그만 해도 44개에 달한다. 그 중 게임 플레이에 영향을 미치지 않는 버그는 단 2개로, 나머지 버그들은 죄다 인게임에 치명적인 영향을 줬던 버그들이다.

소수 인원이 개발하는 인디 게임은 코드를 다듬을 인원이 부족하다 보니 코드가 스파게티인 경우가 많다. 마인크래프트 자바에디션도 스파게티 코드로 악명이 높다.[10] 개발자였던 마르쿠스 페르손의 프로그래밍 역량이 떨어지다 보니 그냥 어떻게든 돌아가기만 하면 된다는 마인드로 주석 처리도 부실하게 이리저리 꼬인 구조였다고 한다.[11] 버전업을 할 때마다 코드를 대폭 갈아치워서 이전 버전의 모드가 호환이 안 될 정도. 일례로 유저들이 벽에 붙인 횃불은 빛이 나는데 왜 손에 들린 횃불에서는 빛이 나지 않는지에 대해 묻자 노치는 코드상 불가능하다는 입장을 보였으나 머지않아 모드 팀들이 아무것도 아니란듯이 손에 들린 채로 빛나는 횃불을 구현하면서 노치의 역량 문제라는 것을 까발려버렸다.

그래서 마이크로소프트가 모장을 인수하고 C++로 다시 개발한 베드락 에디션은 아예 소스 코드를 완전히 갈아엎어 리버스 엔지니어링에 가깝게 새로 만드는 수준의 최적화를 진행했는데, 나온 결과물은 자바 에디션과 비교를 거부하는 최적화를 보여준다. 인수 이전의 마인크래프트는 너무 무거워서 자바에디션의 모든 기능을 당시 포켓에디션이던 베드락 에디션으로 이식할 수 없었지만, 인수 이후 최적화를 거친 베드락 에디션은 아예 모든 기능을 멀티플랫폼으로 구현 가능할 정도로 최적화가 진행되어 양 플랫폼을 통합할 수 있게 되었다. 이 과정에서 그나마 다행인 건 마인크래프트의 구현 난이도가 중견 프로그래머라면 조금만 시간을 들여서 비슷하게 만들 수 있을 정도로 쉬웠기 때문에 마이크로소프트가 단기간 내에 최적화를 진행할 수 있었다는 점이다.

Yandere Simulator도 매우 심각한 스파게티 코드로 이루어져 있다고 한다.

온라인 게임 던전 앤 파이터스파게티 코드로 악명높은 게임중 하나인데 개발사 네오플도 도대체 이걸 어떻게 건드릴지 감도 안 온다고 우는 소리를 할정도로 코드가 꼬일대로 꼬여버려서 규모있는 패치 직후 패치 내용과는 전혀 상관 없는곳에서 버그가 발생하는게 일상이며, 게임 플레이에 치명적이지 않으면서 해결불가능한 버그는 손을 놔버리는 경우가 타 게임에 비해 많은 편이다. 어떠한 버그는 도저히 해결 할 수가 없어서 우회하는 방법을 만들어두었다.[12] 이렇게 수습이 불가능할정도로 코드가 꼬여있기 때문에 유명한 게임이라면 한번씩 등장하는 프리서버조차 만들어지지 않고 있다가 대만 서버가 서비스 종료후 유출된 데이터를 기반으로 간신히 만들어졌다. 이 말인즉슨 이미 있는 서버 데이터를 베이스로 하지 않으면 프리서버를 만들지 못할 정도로 코드 꼬임이 심각하단 이야기. 리버스 엔지니어링조차 불가능해서 코드를 완전히 이해하지 못한 상태로 서버를 연 셈이니 프리서버의 업데이트와 유지보수도 험난해 보이는 건 안 봐도 비디오. 한 전문가가 던파 코드를 열어보고 이렇게 코드가 꼬여있는데 도대체 어떻게 게임이 돌아가는지 모르겠다고 감탄할 정도로 베베 꼬였다. 게다가 더미 데이터의 양이 너무 많은것도 한 몫하고 있다. 던파는 특이하게도 게임을 설치할때 실행에 필요한 이미지와 사운드 파일을 npk 확장자로 압축하고 음악 파일은 ogg 확장자로 설정해 컴퓨터에 설치하는 식인데 당장 음악 폴더로 가보면 이제는 사용을 안하는 이벤트 용으로 만든 음악도 그대로 남아있다. 이러니 더 꼬이는지도...

마비노기플레이오네 엔진도 스파게티 코드급으로 발적화가 심해서 고퀄의 그래픽카드 등의 부품으로 된 고사양 환경에서만 플레이 할 수 있다;

패러독스 인터랙티브가 직접 만든 게임들[13]의 코딩이 스파게티인 건 매우 유명하다. 엔진은 어떻게든 개발해서 여러 코어를 사용할 수 있도록 만들고는, 정작 코딩은 코어 하나만을 죽어라 갈궈대는 데다가, 뭐 하나 최적화하려고 하면 다른 부분이 삑사리나서 짧은 기간동안 순식간에 1.*.0에서 1.*.4까지도 패치가 나오곤 한다. 거기다 렉 먹는 주요 원인을 역설사식 땜빵으로 해결하기도 하니[예시], 업데이트 내겠다고 해놓고서는 뜬금없이 휴가란 휴가는 칼같이 가는 개발진들을 향해 칼을 가는 유저들이 많은 건 당연지사.

코딩은 아니지만 4호선 꽈배기굴도 탄생 경위는 완벽하게 스파게티 코드와 똑같다. 여러 가지 혼란 속에 궁여지책으로 나왔는데 25년이 넘게 지난 지금에 와서 고치자니 애물단지가 되어버린 것.

4. 난독화

일부러 스파게티 코드로 만드는 경우. 주로 리버싱을 방해하거나 읽어보는 사람에게 장난치기 위해 만든다.
주로 소스 코드를 인터프리터가 그대로 실행하는 스크립트 언어에서 많이 쓰며, C이나 Java 같은 관리 언어[15]의 경우 리버싱 툴(디컴파일러)을 사용하면 소스코드를 원본 그대로(!) 뽑을 수도 있기 때문에 난독화 툴이 어느정도 필요하다.

그러나 프로그램 코드 자체는 컴파일러와 인터프리터가 최적화해버리기 때문에 원본 소스코드 그 자체가 유출되지 않는 이상은 복잡하게 바꿔도 별 의미가 없는 경우가 많다. 어차피 리버싱할 최종 결과물은 논리식이 간략화되어 있을 수밖에 없기 때문.

JSFuck은 난독화가 어디까지 갈 수 있는지를 보여주는 예이다.

  • 매크로를 이용해 흔한 표현도 뒤바꾼다
    #define a if(
    #define e else
    #define E return
    
    // 예시
    main()
    {
        a 3 == 3 ) puts("1234");
        e puts("5678");
        E 0;
    }
    

  • .net obfuscator 같은 난독화 툴을 쓴다. 이런 프로그램은 변수, 상수, 함수의 이름을 컴퓨터만 알아볼 수 있게 마구 섞어버리고, 보기에만 더 복잡한 같은 일을 하는 다른 논리식으로 바꿔친다. RTTI(RunTime Type Information) 같은 기술 구현의 문제 때문에 원래 실행시간에는 필요 없는 자료구조나 변수 크기 같은 정보도 프로그램에 포함되는데, 이 부분을 중점적으로 복잡하게 꼬아 사람이 읽기 힘들게 한다. 프로그램에 따라서는 소스코드의 순서나 include 관계도 등도 손보는 경우가 있다.


[1] 전기의 회로 설계 작업도 스파게티 코드와 마찬가지로 엉망으로 만들어도 일단은 작동은 시킬 수 있지만 나중에 유지보수할때 애로사항이 꽃피게 되는 경우가 잦다. 이 경우는 무자격자가 현장박치기 방식으로 전기 공사 방법을 배운 탓에(원칙적으로는 자격증 소지자만 써야 하지만 실전에선 팀장, 선임만 자격증 소지자로 뽑고 이하 인물들은 무자격자를 앉혀놓는 경우가 많다) 주먹구구식으로 회로를 설계해서 생기는 문제. 같은 '설계'라는 공통점 때문에 프로그래밍과 같은 애로사항이 존재하는 것이다. 다만 프로그래밍은 잘못되어도 스트레스나 받을 뿐 안전하고 수정할 기회가 언제든 있지만 전기는 잘못되면 작업자가 요단강을 건널 수 있기에 자격증을 더 따진다.[2] 참, 거짓을 저장하는 변수[3] 참, 거짓을 저장하는 변수[4] 그래서 보통 Promise나 async/await를 사용하여 해결한다.[5] 그래서 보통 Promise나 async/await를 사용하여 해결한다.[6] 몇 안되는 GOTO문 적절한 사용 예시가 다중 for 루프 탈출이다.[7] 몇 안되는 GOTO문 적절한 사용 예시가 다중 for 루프 탈출이다.[8] 물론 PHP가 7.x로 올라가서 속도가 어마무시하게 빨라졌기에 불가능한 건 아니다.[9] 물론 PHP가 7.x로 올라가서 속도가 어마무시하게 빨라졌기에 불가능한 건 아니다.[10] 얼마나 심각하냐면 대규모인 1.16 버전 업데이트에 대비하기 위해 1.15 컨텐츠 일부를 희생할 정도다.[11] 그 예시로 몹의 AI 같은 경우 버그로 인해 무한 루프가 자주 일어났는데, 노치는 원인을 찾아 해결하는 대신 그냥 루프한 횟수를 세서 일정 수가 넘어가면 AI를 초기화했다. 그래서 몹들이 가끔 아무 이유 없이 멈춰서곤 했다.[12] 대표적인 사례가 던파 최고 컨텐츠 중 하나인 '핀드워'에서 발생하는 로딩버그인데 파티매칭 중에 특정 유저만 게임 로딩이 안되어서 해당 던전에 진입 못하고 팅겨져 나가버리는 현상이다. 나머지 파티원으로는 클리어가 불가능하게 돼서 결국 퇴각하는데 이러면 진행에 매우 큰 어려움이 발생한다. 네오플은 근본적인 원인은 해결하지 못하고 결국 로딩버그 발생시 파티원 전원이 입장 전 상황으로 롤백되게 만들어서 퇴각하는 상황을 막았다.[13] 클라우제비츠 엔진을 사용중이라는 공통점을 지녔다.[예시] 스텔라리스의 노예 시장 시스템은 원래 여러 POP들이 시장 위에 올라와야 하지만 현재는 노예 시장 창이 텅 비어있는 걸 알 수 있는데, 알고보니 역설사가 재깍재깍 사가도록 해서 렉을 줄이는 대신 노예 시장 시스템을 유저가 쓸 수 없도록 만들어버리는 황당한 패치를 적용하면서 이렇게 됐다는 것.[15] 실행 파일을 기계어가 아닌 중간 언어(전자는 IL, 후자는 JVM 바이트 코드)로 컴파일하는 프로그래밍 언어. 크로스플랫폼을 위해 만들어졌지만 후술하는 것처럼 악용되기도 한다.