Unknownpgr

개발자가 마주치는 진짜 문제들[2]

2021-09-16 01:20:31 | Korean

원래 '개발자가 마주치는 진짜 문제들' 은 시리즈물이 아니었습니다. 그런데 저와 비슷한 상황을 겪으신 모 선배님의 한을 풀어드리고자(?) 그 썰을 제 블로그에 올립니다.

아래 글은 제가 작성한 것이 아니며, 선배님의 글을 그대로 옮긴 것임을 밝힙니다.

모 기업 후기

<2021 봄 4월 말~8월>

다운로드

인턴십은 정말 좋은 경험이다.” -에이브러햄 링컨-

지난가을 안양의 한 회사에서 아르바이트를 했다. (아르바이트지만 건강보험과 연금이 들어가서 인턴과 다를 게 없다) 그러다 대전 한국전자통신연구원 ETRI인턴에 합격하여 일을 그만두었다. 대전에서 돌아와서 4월 즈음에 안양의 회사에서 러브콜을 받고 남은 휴학 기간을 회사에서 보내게 되었다. 지난가을에도 그랬다시피 다음날부터 출근했다. 넉달 반 동안 많은 일이 있었지만 인상깊었던 일부터 얘기하겠다. 실은 영양가 있는 이야기보다는 썰에 가깝다. (직장 선임 뚝배기 깨려다 참은 썰 푼다)

1. 시작

그 회사는 장비에 들어가는 프로그램을 만들었다. 장비에는 보드 3~5개 정도 들어간다. 보드마다의 통신은 매번 다르지만, 이번 여름에 한 프로젝트들은 모두 USB통신을 사용했다. 메인보드는 win10을 설치한 임베디드 pc이다. (내 컴퓨터는 win7이었다) C# winform으로 GUI를 만들었다. 프로그램은 어떤 사람이 짰는데, 그 사람은 이직한 지 꽤 되었다고 한다. 다행이다. 회사에 남아 있었다면 참지 못하고 뒤통수를 쳤을지도 모른다. 프로젝트 코드는 매우 엉망이었다. 이유는 간단하다. 한 번 짠 프로젝트를 다른 프로젝트에 여러 번 돌려쓴 것이고, 코드 개발자가 코드관리를 전혀 하지 않았기 때문이다. 모든 코드들이 한 디렉터리 내에 있었으며 개중에는 전혀 사용하지 않는 파일도 있었다. (로그인 기능도 없으면서 login.cs 같은 파일이 있었다.) 게다가 UI 창 밖에 있는 GUI 컨트롤들도 있었는데, 예를 들어 창 사이즈가 400x600이라고 하면 어떤 컨트롤의 좌표는 (600, 700)이었다. 이것들도 전에 쓰던 것들인데 필요 없어져서 감춘 것이다. Global.cs라는 파일 내에 static class인 Global이 있는데 여기에는 온갖 flag들, 데이터를 담는 배열들, 디버깅의 잔재 등이 남아 있다. 10년동안 설거지 안 하고 같은 밥그릇에 먹은 것이다.

2. 충격과 공포의 Thread

태초에 스레드가 3개 있었다. 하나는 데이터를 읽고 처리하는 스레드로 잘 돌아갔다. 다른 하나는 커맨드 파싱 스레드로 돌아는 가지만 통신을 하지 않아 작동여부는 불확실 했지만 프로그램은 돌아갔다. 나머지 하나는 서버 스레드로 TCP/IP의 서버를 여는 스레드인데 놀랍게도 메인스레드가 품고 있었다. 모든 스레드는 무한루프를 가지고 있었다. 충격적인 사실을 확인하고 동작하는 함수를 확인하였다.

doThread(){
     ... //서버 동작 초기화
    while(true){
        try{
        	...
        }catch{
            doThread();
        }
    }    
}

무려 서버 동작에서 예외가 발생하면 서버 동작 초기화를 위해서 재귀함수를 호출한다. 재귀함수가 while 루프 안에 있는게 더 충격이었다. while 루프 안 에는 break문이 없어서 어디 있으나 도긴개긴이지만 생각이란 걸 전혀 안 하는 건가 싶었다. 그리고 데이터 처리 스레드를 제외하고는 모두 mainWindow.cs (main window form)파일에 있어서 파일이 2000줄 가까이 되었던 것으로 기억한다. 스레드 관련 함수들을 모아 새로운 클래스에 만들어 주고, 스레드에서만 접근하는 변수들을 클래스의 필드에 할당해 주었다.

3. 쉬지 않아도 쉬는 것처럼

C#에서 일정 시간 지연을 주는 방법은 Thread.Sleep(1000)Task.Delay(1000).Wait()이 대표적이다. 둘의 성격이 다르지만 일단 스레드가 그동안 일하지 않는 것은 비슷하다. 문제는 Task.Delay(1000).Wait()인데, Task.Delay()의 반환형은 Task이다. Wait()을 호출하여 딜레이 작업을 실행해야 한다. 놀랍게도 모든 코드 곳곳에 Task.Delay(1)이 들어있었다. 동작도 안 하는거 왜 넣었을까. 통신코드 근처에서 자주 발견할 수 있었다.

set_XXX();
Task.Delay(1);
set_YYY();
Task.Delay(1);
...

이상한 점

일단 코드는 잘(?) 동작했기 때문에 딜레이 없이도 기계가 작동하는거라 생각하고 딜레이들을 모두 지워버렸는데, 주임님한테 혼났다. 그자리에 딜레이가 있으면 마음대로 삭제하지 말고 그냥 Thread.Sleep(1)로 교체해 놓으면 되는 거라고 한다. 맞는 말이긴 한데, 동작도 안 하는 함수 여기저기 써 놓은 사람한테 듣자니 좀 억울하지 않은가. 그냥 딜레이를 통신코드에 합쳐버렸다.

4. 토끼뜀 20번 실시

위의 딜레이와 연결되는 내용이다. 가끔 코드에 아래와 같은 부분이 있었다.

set_XXX();
flag = read_XXX();
int cnt=0;
while(true){
	if(flag) break;
	else{
		cnt++;
		set_XXX();
		if(cnt>=20) break;
	}
}

원래는 더 복잡했지만 나의 방어기제로 인해 충격적인 기억이 지워져 완전히 똑같이 쓰지 못했다. 대충 요약하면 XXX라는 값을 세팅했는데, 그 값이 제대로 세팅되지 않았다면 세팅 명령을 더 보내라는 것이다. 언뜻 심오해 보이지만 들여다 보면 flag변수를 read_XXX()함수로 한 번 초기화하고 이후에는 write하지도 않는데 while문에서 if문 체크를 하는 등 웃긴 점이 한두가지가 아니다. 위 코드는 아래와 같다.

set_XXX();
flag = read_XXX();
if(flag)
	for(int i=0; i<20; i++)
		set_XXX();

주임님께 이런 복잡한 코드와, 그 코드가 실은 두 번째 코드와 같다는 것을 말씀드렸다. 이런 괴코드가 존재하는 이유를 물어보았다. 그 이유는 장비가 세팅 명령을 잘 못 받는거 같아서 여러번 보내주면 잘 받지 않을까 해서 쓴거라고 한다. 명령과 명령 사이에 딜레이를 주어서 장비가 여유롭게 받을 수 있게 했는데도 불구하고 잘 동작하지 않아 쓴 것이라고 한다. 위에서 언급했다시피 딜레이는 Wait()함수를 사용하지 않아서 동작하지 않은 것이다. 백번 양보해서 딜레이는 몰라서 그랬다고 하자. 첫번째 코드처럼 복잡한 코딩은... 시켜도 못 하겠다. ㅋㅋㅋ 사람이기에 모르는 건 언제나 존재하기 마련이다. 그런데 석사졸이 고졸인 나보다 도큐먼트를 안 본 다는건 문제가 있는 것 같다.

5. USB 통신

실은 USB통신만 쓰고 자려고 했는데 USB를 쓰려고 하니 관련된 작은 것들이 자꾸 생각나서 그것들을 먼저 썼다. 장비에는 3개의 USB가 붙어있다. 임베디드 PC와 3개의 보드가 연결되어 있다. 프로젝트 코드에 다음과 같이 구현되어 있었다. (XXXX와 YYYY, ZZZZ는 나의 신상보호를 위해 블러한 것이다.)

각 파일에는 파일명과 동명의 static class들이 있었으며, static class에는 static한 메서드들이 죽 늘어져 있었다. 그리고 각 보드에 사용하는 명령들이 함수 하나하나에 구현되어 있었다. 예를들어 어떤 보드에 온도, 습도, 풍속 등등의 측정기능이 있다고 가정한다면 그 기능마다 메서드가 하나씩 존재했다.

public static int read_Temp() {...};
public static float read_Humidity() {...};
public static int read_wind() {...};

당연하게도 보드에 명령을 보내는 메서드의 내용은 아래와 같이 획일화 되어 있었다.

byte[] cmd = new byte[4];
cmd[0] = 0x40; // R/W
cmd[1] = 0x44; // 명령어의 코드
usb.write(cmd, ...);
usb.read(...);

나라면 복붙하는 것도 팔아파서 enum을 사용했을 텐데... 옛날에는 이런식으로 명령어 코드를 함수로 래핑하는 게 편리했나 싶다. (명령어와 관련한 문제점도 있다.)

또한 USB 객체역시 static class로 선언하여 3개의 USB클래스가 있었다.

USB 3개는 기본적으로 USB device와 연결하고, 데이터를 읽고 쓰는 함수들은 라이브러리의 함수들을 조합해서 사용하다보니 토씨하나 안 틀리고 같았다. 다른 부분은 USB 포트 넘버가 전부였다. usbCCC에서는 다른 USB와 주고받는 데이터가 다르다보니 새로운 메서드를 추가하였지만 기본적인 틀은 같았다. 이렇게 커맨드클래스와 usb클래스가 나뉘어 있어서 파일만 7개를 차지했다. 위에서 언급했다시피 모든 파일이 같은 디렉터리에 있었다.

이번 프로젝트에서 USB파트가 가장 개과천선한 부분이기에 조금 자랑스럽다. 일단 USB 커맨드들을 enum으로 정리해야겠다고 생각했다. 그리고 USB 파트도 정리하였는데, USB라는 abstract class를 하나 만들었다. 기본적으로 USB device를 연결하는 기능을 구현하였다. 그리고 통신에 필요한 cmd 함수를 abstract로 선언하였다. 각 보드마다 기능이 다르니 USB=보드=특정기능 이런 식으로 유추해 볼 때, USB객체 자체가 특정 기능을 갖는다고 추상화 해 볼 수 있었다. 그래서 추상 클래스인 USB를 상속받은 usbAAA, usbBBB, usbCCC라는 클래스를 만들고, 그 클래스 안에 enum과 cmd함수를 구현하였다. (cmd함수는 이전에 set_XXXread_XXX함수와 구조가 같고, 명령어의 코드 인자로 입력받는다.) USB코드에서는 중복이었던 코드들을 삭제하여서 분량이 많이 줄었다. 굳이 3개의 파일에 별도로 존재할 필요가 없어서 하나의 파일에 합쳐주었다. cmd함수가 set_XXX, read_XXX 함수들을 대체하게 되었으므로 commandXXXX.cs의 파일도 필요없었다. commandXXXX.cs의 데이터 변환하는 함수들은 Convert.cs라는 파일에 모두 옮겨 주었다. 7개의 파일이 하나의 파일로 줄어들었다. 어떤 루틴에서 어떤 USB통신에 자주 접근하는지, 어떤 명령을 사용하지 않는지 등을 쉽게 파악할 수 있게 되었다. 이전에는 절대절대절대 못했던 것이다.

옛날 옛적에는 코드 줄 수에 비례하여 월급을 주었다고 한다. 그럼 내 월급은 마이너스인가? 크크크

6. 다음 편 예고

해당 '다음 편'들이 제 블로그에 기고될지는... 아무도 모릅니다.


- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -