만능 코드를 만들지 마라
여러 개발자들과 협업하면서 좋은 개발자도 많이 만났고, 나쁜 개발자도 많이 만났다. 그들과 일하며 좋은 코드도 많이 봤고, 나쁜 코드는 더 많이 봤다. 많은 책들이 "좋은 코드 작성하는 방법"에 대해 이야기하지만 지켜지지 않은 경우가 대부분이었다.
어떻게 하면 좋은 코드를 작성하는 방법을 잘 지킬 수 있을까? 쉽다. 나쁜 코드를 작성하지 않으면 된다. 나는 오늘 좋은 코드보다는 나쁜 코드에 대해 이야기를 해보려고 한다.
그렇다면 어떤 코드가 "나쁜 코드"일까?
개발할 때 가장 많은 시간이 드는 것은 코드를 작성하는게 아니라, 다른 사람이 만들어둔 코드를 읽고 이해하는 것이다. 나는 한 줄을 이해하기 위해서 여러 줄을 읽을 필요가 있는 코드가 바로 읽기 어렵고 이해하기 어려운 코드라고 생각한다. 나는 이런 코드를 "나쁜 코드"라고 생각한다. "나쁜 코드"를 만드는 개발자는 같이 일하기 싫은 개발자 0순위다.
"나쁜 코드"는 매우 위험하다. 내 개발 시간을 늘어지게 만들 뿐만 아니라 유지보수도 어렵고, 나중에는 알 수 없는 버그가 생긴다면 100% 확률로 이 코드 때문에 발생한 문제다. 지금부터 나쁜 코드의 대표적인 유형 중 "만능 코드"에 대해 이야기를 해보려고 한다.
만능 코드란?
하나의 함수와 하나의 클래스는 하나의 기능만을 담당해야 한다. 하지만 개발자들이 가장 흔히 저지르는 실수가 바로 이 규칙을 어기는 것이다. 대표적인 예시가 바로 "만능 코드"다. 이 만능 코드에 대해 알아보기 위해 나쁜 게임 개발자 "철철수"에 대해 이야기를 해보려고 한다.
우리들의 나쁜 게임 개발자 철철수 씨는 오늘도 FPS 게임을 만드는 중이다. 그가 오늘 만들어야 하는 것은 여러 유저가 모두 접속을 완료하면 게임을 시작하고, 조건이 충족되면 게임을 종료하는 기능이다. 그는 먼저 게임의 상태를 저장하는 변수를 만들었다.
enum class GameProgressStep
{
NONE,
STANBY,
READY,
START,
END,
};
//중략
GameProgressStep currentStep;
맨 처음 초기화 상태는 NONE, 유저의 접속을 기다리는 상태는 STANBY, 모든 유저가 접속했으면 READY, 게임을 시작하면 START, 게임이 종료되면 END 상태가 된다. 여기서 철철수 개발자는 생각했다.
'좋아. 현재 상태를 저장했으니, 현재 상태에 따라 다음 스탭으로 넘어가는 함수를 만들면 되겠지?'
그가 이렇게 해서 만든 함수는 아래와 같다.
void DoNextStep()
{
switch(currentStep)
{
case NONE:
StandByGame();
break;
case STANBY:
ReadyToGame();
break;
case READY:
StartToGame();
break;
case START:
EndToGame();
break;
case END:
Quit();
break;
}
}
실제로는 이 코드보다 더 나쁜 코드였지만 설명을 위해 간략하게 표현했다. 이 코드를 작성한 철철수 개발자는 미래에 동료 개발자들에게 욕을 먹게 된다. 왜 욕을 먹었을까? 바로 하나의 함수는 하나의 기능만을 담당해야 한다는 규칙을 어겼기 때문이다. "다음 스탭으로 넘어가는 하나의 기능이잖아?" 라고 생각할 수 있다. 하지만 이런 생각은 비개발자들의 정리되지 않고 구체적이지 않은 두루뭉실한 표현일 뿐이다. 우리는 개발자로서 사고해야 한다. "다음 스탭으로 넘어가는" 기능이 구체적으로 무엇을 말하고 있는가를 생각해보자. swtich 문 안에 있는 5가지 함수가 전부 "다음 스탭으로 넘어가는" 기능이다.
왜 이런 코드를 작성하면 안될까? 읽기 어렵기 때문이다. 먼저 배경지식 없이 DoNextStep의 호출부를 보도록 하겠다.
...
TimerHandle = GetTimerManager().SetTimer(DoNextStep, 1.0f);
이해가 되는가? 과연 1초 뒤 5개의 함수 중 어떤 함수가 실행이 될까? 이해가 된다면 그건 거짓말이거나 당신이 DoNextStep 함수를 만든 철철수 개발자라는 뜻이다.
당신이 이 코드를 이해하기 위해서는 1초 뒤 currentStep의 상태가 무엇인지 파악해야 한다. 만약 게임 내내 상태가 NONE이었다가, 0.9초 뒤 갑자기 STANBY가 된다면? 당신은 왜 StanByGame 대신에 ReadyToGame이 호출되는지 파악해야 한다. 타이머 핸들러 바로 위에 로그를 찍어봐도 정상적으로 currentStep이 NONE이라고 나올테고, 당신은 "왜 안되는거지?"를 육성으로 뱉으며 스트레스를 받을 것이다.
어찌저찌 파악을 했다고 끝이 아니다. 위 코드가 한 곳에서만 사용하는 것도 아니기 때문에 DoNextStep이 사용된 모든 곳에서 방금 했던 작업을 함수가 사용된 횟수만큼 반복해야 한다. 그것 뿐이랴? 당신은 이제 프로젝트의 모든 곳에서 currentStep 에 값을 넣어주는 부분을 찾아 정상적으로 값이 들어갔는지, 만약 멀티 쓰레드 환경이라면 호출 타이밍이 왜 어긋나는지를 점검해야 할 것이고 엄청난 시간을 잡아먹을 것이다.
그렇다면 어떻게?
쉽다. 저 함수 대신에 그냥 switch 문 안에 있는 함수 5개를 적절한 곳에서 사용하면 될 일이다.
(제발 부탁이니) 만능 코드는 만들지 마라.
모두 철철수 개발자처럼 되는게 아니라 좋은 코드를 만들지는 못해도 나쁜 코드는 만들지 않는 안철수 개발자가 되자!