Linux Programmer

시험에만 보이는 C언어, 시스템 프로그래밍 기법 본문

컴퓨터 관련/C언어

시험에만 보이는 C언어, 시스템 프로그래밍 기법

sunyzero 2013. 3. 18. 19:18

실무에서 쓰이지 않지만 시험에서만 출제되는 C언어 시험 문제의 세계를 고발(?)하고자 한다

고발이라고 하면 좀 자극적이긴 하지만 사실 특별한 내용은 아니다. 대부분 C언어 하시는 분들은 예전부터 생각해온 것일지도...

 

사실 요새 몇몇 기관의 시험문제를 출제, 감수 하면서 현실과 동떨어진 시험의 세계를 느낀 후 적는 글이다. 어떤 시험을 출제하느냐고 묻지는 말았으면 한다. 보안서약상 구체적인 기관이나 정보를 이야기할 수는 없다.

시험 문제 출제를 할 때면 출제를 요구하는 기관에서 과목, 출제 목표, 세부사항 등을 일일히 정해준다. 따라서 누가 문제를 내든지 비슷한 형태가 나올 수 밖에 없다.

예를 들면 다음과 같은 요구 사항이 있다고 치자.

 

 과목  대분류  소분류  세부 사항/출제 목표
 C언어  예약어  연산자  연산자 우선 순위
 C언어  문자열  입력방법  scanf 함수군
 시스템  시그널  시그널 처리기  signal 함수

 

1. 연산자 우선순위는 실무에선 필요없다.

앞서 첫번째 요구 사항인 연산자 우선 순위를 테스트한다고 가정하자. 출제자는 어쩔 수 없이 말도 안되는 복합적인 연산을 한 행에서 실행하도록 코딩해야만 한다. 예를 들면 아래 코드와 같이 복합 연산 후 최종값을 물어본다든지 해야 한다.

  int x = 4, y = 50, z = 100;   int sum = --x*2 + (y>>1) / 3 * 5 + (z & 0x10); 

 

위에서 sum의 값은 무엇일까? 대부분은 맞추겠지만 좀 짜증이 날 것이다. 

그런데 실무에서 저딴 식으로 짜놓으면 선임에게 혼쭐이 난다. 실무에서 모든 연산은 소괄호로 묶어서 명확하게 작성하도록 한다. 더군다나 저렇게 한 행에 복잡한 수식을 만들기보다는 간단한 수식으로 여러 행으로 풀어서 작성해야만 한다. 만일 소괄호를 쓰지 않고 한 행에 저렇게 복잡하게 수식을 넣어버리면 나중에 큰 사고가 터질 가능성이 있다.(수정을 하거나, 컴파일러를 교체할 때 연산자 우선순위 오류를 일으키는 경우가 있기 때문이다.)

 

위의 예에서는 x, y, z가 한 번만 등장하지만 2번 이상 등장하는 경우, 예를 들어 --x와 x가 섞여서 나오는 경우에는 해석 순서는 undefined이므로 어떤 일이 발생할 지 모른다.(C언어는 sequence point가 중요하기 때문에 한 행에 여러 sequence가 섞여 있으면 안된다.)[1]

 

undefined behaviour란 정의되지 않은 행위, 달리 말해 implementation에 따라 다른 결과가 나올 수도 있는 행위므로 심각한 오류를 발생 시킬 확률이 높아진다. 실제로 위 행동에 대해 표준안에는 다음과 같이 쓰여있다.

 

Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be read only to determine the value to be stored. - ISO:IEC 9899:1999 C99 standard. p67

 

수치 연산 오류는 시스템의 신뢰도에 심각한 영향을 주기 때문에 절대로 한 행에 연산자 우선순위를 감안한 프로그래밍이나 sequence point가 엉망인 코드를 작성하면 안된다. 개인적인 생각으로 연산자 우선 순위 같은 문제는 나오지 말아야만 한다. 오히려 교수님이나 강사분들이 강의할 때 저런 식으로 프로그래밍 하지 말라고 가르쳐야 한다.

 

 

2. 묵시적 타입 캐스팅(implicit type casting)

C언어에서는 묵시적으로 이뤄지는 타입 캐스팅이 있다. 예를 들어 정수형을 나눌 때 피연산값이 실수형이면 알아서 타입캐스팅이 일어나는 것이다. 문제는 이런 묵시적인 타입 캐스팅은 실무에선 문제를 발생시킬 수 있어서 명시적인 타입 캐스팅(explicit type casting)으로 바꾼다는 점이다.

 

만일 정수형을 꼭 써야 한다면 나눗셈에서 반올림이나 반내림 때문에 문제가 발생할 수 있다. 그래서 미리 100이든 1000이든 곱해서 자리 올림을 하든지, 아니면 실수형으로 바꿔서 계산하기도 한다.

결국 타입 캐스팅이나 나눗셈 문제를 꼬는 것은 그다지 좋은 문제는 아니다. 시험에서 좋지 않은 버릇을 들여서 실무에 투입된 직원은 언젠가는 사고 칠 수 있는 폭탄이 되기도 한다.

 

 

3. 비비꼬인 포인터 문제

C언어 시험에 종종 등장하는 복잡한 포인터 문제도 실무에선 쓰이지 않는다. 심하게는 3~4중 포인터를 만들어서 값을 물어보는 경우도 있는데, 3~4중 포인터가 실무에서 쓰이던가? 개념을 잡기 위함이라고 변명하는 사람도 있지만, 쓰지도 않는 방법으로 가르치는 것은 좋은 교습방법이 아니라고 생각된다.

 

이상하게 꼬였네... 스크류 포인터
이상하게 꼬였네... 포인터

 

실무에서 2중 포인터 이상을 쓰는 일은 극히 드물다. 2중 포인터도 reentrant 함수나 thread 함수에서나 주로 등장하지 그 외에는 거의 등장하지 않는다.

 

또한 2중 포인터 이상을 사용하는 경우라 하더라도 포인터 변수를 typedef 해서 연산자를 줄이는 경우가 많다. 실제로 GNU의 glib 라이브러리가 이런 방식으로 작성되어있다.

 

 

* 왜 복잡한 포인터 연산을 쓰지 않는가?

= 가장 큰 이유는 복잡한 포인터 연산은 현대적인 CPU와 컴파일러의 최적화(optimization)에 방해물이 되기 때문이다. 포인터 연산이 복잡해지면 CPU와 컴파일러가 각 변수의 의존성(dependancy)을 판단하기 어려워진다. 그 결과 비순차 실행(out of order)이나 분기 예측(branch prediction)을 하기 어려워지고, 코드의 성능이 떨어지게 된다.[각주:1]

 

포인터 연산이 꼭 필요한 경우라면 restrict pointer를 사용할 수 있는지 생각해보자. 
restrict pointer를 쓸 수 있는 조건이라면 포인터를 사용하더라도 최적화에 도움을 줄 수 있다.

 

결국 복잡한 포인터 연산을 스크류바처럼 빙글빙글 꼬은 복잡한 포인터 연산을 사용하면 멋지게 코딩 한 것처럼 보이겠지만, 그건 하수(下手)들이나 쓰는 기술이다. 중급 이상의 고수들 눈에는 쓸데없는 짓으로 보일 뿐이다. 포인터 정복이니 하면서 포인터 연산에 대한 환상이 있다면 버리는 것이 좋다. 포인터는 주로 call by reference를 흉내내는 용도가 중요하니 그 쪽만 확실하게 익혀두는 것을 추천한다.

 

* 주의!!

포인터를 사용하여 call by reference를 구현할 수 있다는 것이지 C언어 스펙에서 reference type을 지원한다는 의미는 아니다.

엄밀하게 C언어는 언어 자체에서 reference type을 지원하지 않는다. C언어는 오직 call by value만 지원한다. C언어의 창시자인 데니스 리치가 직접 이야기 한 것이니 리치의 책을 보면 알 수 있다. 즉 C언어에 call by reference를 이야기 한다면 데니스 리치의 책도 안 읽었음을 인증하는 꼴이 된다. 따라서 정확하게 표현하려면 C는 call by reference를 흉내낼 수 있는 것이 맞는 표현이다.

 

 

 

4. scanf 함수

scanf는 입력을 받는 대표적인 함수이다. C언어에서는 누구든지 배우는 기능이지만 사실 실무에서는 쓰이지 않는다. 왜냐하면 scanf는 개행 문자나 공백 처리에 문제가 있기 때문이다. 그래서 실무에서는 fgets나 fread를 통해서 문자열, 버퍼를 받아 파싱하는 구조를 더 많이 사용한다.

 

최근에는 fgets보다 발전된 새로운 getline이나 getdelim 함수를 더 많이 사용한다.(C언어: fscanf를 대체하는 getline 함수)

 

그럼에도 불구하고 모든 시험에 빠지지 않고 등장하는 것이 scanf 함수다. 개인적으로 이 함수는 이제 가볍게 배우고, 새로운 getline, getdelim 함수를 열심히 배워야 한다고 생각된다.

 

 

5. signal 함수 (POSIX 한정)

유닉스, 리눅스 계열에서 시그널 처리기를 설치하는 함수로 signal()을 쓰는 책이나 시험이 많다. 문제는 signal() 함수는 신뢰성에 결함이 있어 표준안에서도 sigaction()으로 대체하라고 쓰여 있다.  더군다나 이건 십수년전부터 나온 이야기다. 그럼에도 불구하고 아직도 signal()로 쓰여진 책들을 보면 좀 암담하다. 

 

SUS 유닉스 표준안에서 signal 함수 부분을 보면 다음과 같이 나와있다.[각주:2]

 

APPLICATION USAGE
The sigaction() function provides a more comprehensive and reliable mechanism for controlling signals; new applications should use sigaction() rather than signal().
- The Open Group Base Specifications Issue 7. IEEE Std 1003.1-2008.

 

signal을 보고 다시 SUS의 sigaction 매뉴얼 페이지를 보면 왜 signal을 쓰면 안되는지 자세하게 적혀있다. 여기에 적으면 너무 길어지니 나중에 다시 포스팅하도록 하겠다.

하지만 결론만 말하자면 signal() 함수 대신에 sigaction()을 써야만 한다는 것이다.

 

 

6. 그 외의 짜투리.

위에 5가지를 적었는데, 이 외에도 시험과 실무간에 갭이 있는 기법들이 있다. 예를 들면 다음과 같은 것들은 시험에서만 보이고 실무에서는 뒤의 것으로 대체되어 쓰인다.

  • 재귀함수 - 쓰면 혼남. 많이 혼남.(돈이 왔다갔다 하는 곳에서는 싫어하지만, 다른 파트는 좀 다를수도 있음)
  • fflush(stdin) - 이건 비표준. 쓰면 안되는데 좋지 않은 C책에 많이 등장함.
  • fork-exec 기법 - posix_spawn로 대체
  • vfork 기법 - fork로 대체
  • strcpy, strcat - stpcpy, stpncpy로 대체
    (guarantee null terminated string = strlcpy, but non-standard)[각주:3]
  • select 멀티플렉싱 - poll, epoll로 대체
  • gethostbyname, getservbyname, inet_addr, inet_aton - getaddrinfo로 대체
  • gethostbyaddr, getservbyport, inet_ntoa - getnameinfo로 대체
  • 무한 루프 재시도 - queue, 타이머 모델로 대체
  • memcpy - 되도록이면 memmove로 대체

 

fork-exec, vfork, strcpy 함수군, select/pselect, gethostbyname ... 등은 구식 함수(removed functions)이며 기능에 문제가 있는 경우도 있다. 따라서 현재 실무에서는 사용 빈도가 줄어드는 함수이다. 몇몇 함수는 표준에서도 사용을 지양하라는 기능도 있다.

 

 


 

PS.

얼마전 C언어 국내책을 추천해달라고 해서 서점 간김에 책을 이리저리 살펴봤다.
결론은 국내서적 중에 C언어를 제대로 설명하는 책은 없었다. 

쉽게 설명하는 책은 내용이 틀렸고, 내용이 올바른 책은 너무 어렵게 설명되어있고...(그나마 올바르게 설명된 책은 외국번역서... -_-) 판매순위 1위에 랭크된 영광스런 책도 내용은 그닥이었다. 국제 C언어 표준을 안보시고 책을 쓰시나?

 

더보기
사실 예전에 C언어를 강의 할 때 제일 힘든 것이 교재 선정이었다. 몇몇 책은 표준을 쌩 무시하고 설명하고 있고, 어떤 책은 이론 자체를 잘못 설명하고 있고, 심지어 어떤 책은 위의 포인터 문제처럼 성능을 떨어뜨릴 수 있는 나쁜 코딩 기법을 가르친다. 그리고 C99가 나온지가 14년이 지났는데... 아직도 설명이 없는 것은 이해가 안 간다.

또한 구조체를 설명하면서 XDR을 무시하고 구조체의 크기를 packed 된 상태로 그려 놓는 경우도 있었다.


국내에서는 데니스 리치 정도는 아니여도 대가로 불리는 분이 책 한번 써주면 좋을텐데... 정작 실력이 좋으신 분들은 책을 안쓰고 이상한 책들만 난립하는 것 같아서 참 안타깝다.

 

2015년 기준으로 가장 쓸만한 C언어 기초 책은 KNK, C primer plus이며, 이에 대한 내용을 포스팅 해두었다.

C언어 공부법과 책 추천 - http://sunyzero.tistory.com/225

 


PS2.

앞으로 시험 출제 총괄이 된다면, C99의 기법 정도는 꼭 포함시킬테다.(C11은 아직 영글지 않은 기술이고, 지원하는 컴파일러도 적어서 스킵) 시대가 어느 때인데 아직도 ANSI C에 맞춰서 내는지... 답답할 때가 많다. 
 
시험이란 현재 사용하는 기술과 이론, 국제표준에 부합해야 한다. 그렇지 않으면 시험 자체가 아무런 의미가 없다고 생각된다.

 

 

* 히스토리

2015-03-28 C언어 공부법과 책 추천 링크 추가

2014-02-21 PS2 추가

2013-08-07 PS 추가

2013-04-15 strlcpy 코멘트 추가

2013-03-18 작성

 

 

* 레퍼런스

 

[1] Sequence point, https://en.wikipedia.org/wiki/Sequence_point

 

 

 

  1. 김민장. (2010). 프로그래머가 몰랐던 멀티코어 CPU 이야기. 서울, 한빛미디어. [본문으로]
  2. Single UNIX Spec., http://www.opengroup.org/standards/platform [본문으로]
  3. C String handling, http://en.wikipedia.org/wiki/C_string_handling [본문으로]
반응형
Comments