34
2014/10/16 09:22 http://blog.naver.com/knix008/220152186137 [ 관리하기 쉬운 코드를 만드는 원리] [ 관리하기 쉬운 코드를 만드는 원리] C언어 C언어 프로그래밍의 가장 기본적인 원칙은 "의존성의 제거(Decoupling) 의존성의 제거(Decoupling)와 높은 응집성(Cohesion) 응집성(Cohesion)"으로 요약된다. 의존성은 어쩔 수 없이 발생할 수 밖에 없지만 가능하다면 줄여야 한다. 응집성은 코드의 조각들(모듈단위, 파일단위, 함수나 클래스 단위에서도)이 주어진 역할만을 충실하게 수행하도록 뭉쳐져야 한다. 의존성이 줄어든 코드는 의존적인 것들이 변경되더 라도 최소한의 영향만을 받게된다. 따라서, 의존성을 줄이면 확장이나 변경이 쉬워진다. 응집성이 높은 코드는 외부에 대 한 의존성이 줄어들게 되며, 잘 정의된 역할(Role)만을 수행하기에 변경에 대한 영향을 외부로 전달하지 않고 내부에서 해 결한다. 이 두가지 원칙이 코딩의 기본원리다. 객체지향 언어를 사용하는 사람은 한번쯤은 "SOLID"라는 원칙을 들어봤을 것이다. 즉, 객체지향 코딩에서는 SOLID원칙 에 충실하게 코딩을 해야지만, 앞에서 이야기한 의존성이 줄어들고 응집성이 높은 코드를 만들어낼 수 있다. SOLID원칙은 SOLID원칙은 "SRP(Single Responsibility Principle),OCP(Open Close Principle),LSP(Liskov Substitution Principle),ISP(Interface "SRP(Single Responsibility Principle),OCP(Open Close Principle),LSP(Liskov Substitution Principle),ISP(Interface Segregation Principle),DIP(Dependency Inversion Principle)" Segregation Principle),DIP(Dependency Inversion Principle)"을 의미한다. 이런 원칙들이 객체지향 언어에서만 유용 한 것은 아니며, C언어와 같은 절차지향 언어에서도 의존성을 줄이고, 응집성이 높은 코드를 만드는데 도움을 줄 수 있다. SRP(Single Responsibility Principle) SRP(Single Responsibility Principle)은 한가지 책임만을 가지도록 코딩하라는 말이다. 여기서 말하는 한가지 책임이란 추상적인 수준에서 한 가지 개념을 구현하는 코드를 말한다. 책임이란 결국 역할에 대해서 주어지는 활동의 범위를 말하 며, 그 범위를 표현할 수 있는 하나의 추상적인 개념을 찾을 수 있으면 된다. 예를 들어, 운영체제를 보게될 때, 큰 개념에 서는 하드웨어 자원을 관리하는 소프트웨어라는 것으로 표현되며, 다시 그 개념을 나누면 CPU 스케줄링, 메모리 관리, 파 일 시스템, 네트워크로 나누어진다. 각각이 하나의 추상적인 개념을 구현하고 있으며, 다시 하위 개념으로 쪼갤 수 있다. 이때 각각의 개념은 자신이 해야할 활동의 범위가 있다는 것을 알 수 있다. 즉, 그 범위의 역할만 충실한 코드를 만들면 된 다. C언어에서의 가장 작은 단위(Unit)로 가져갈 수 있는 것이 함수라고 할 때, 하나의 함수는 하나의 개념을 구현해야 한다. 함수들이 모인 파일은 한가지 개념을 구현하기 위한 함수들로 구성되어야 하며, 파일들을 묶은 폴더(디렉토리)들은 한가 지 개념을 구현하기 위해서 필요한 파일들로 묶여야 한다. 다시 이런 폴더들이 모여서 하나의 서브시스템을 구현하고, 서 브시스템들이 모여서 시스템을 구현하게 된다. 묶이는 과정에서 개념적으로 생략(혹은, 지나친 비약)이 발생한다면, 더 세 분화할 것이 남았다는 이야기다. 따라서, 이때는 좀 더 추가적으로 정의할 개념들이 있는지를 분석해야 한다. OCP(Open Close Principle) OCP(Open Close Principle)은 확장에 대해서는 열려(Open)있고, 변경에 대해서는 닫혀(Close)있어야 한다는 원칙이 다. 이 원칙은 지속적인 변경이 발생하는 소프트웨어의 특성에 대해서 어떻게 대응할 수 있을 것인가에 대한 해결책이다. 1 · [ 소프트웨어 개발자 이야기 ]

C Language II

Embed Size (px)

Citation preview

Page 1: C Language II

2014/10/16 09:22 http://blog.naver.com/knix008/220152186137

[ 관리하기 쉬운 코드를 만드는 원리][ 관리하기 쉬운 코드를 만드는 원리]

C언어C언어

프로그래밍의 가장 기본적인 원칙은 "의존성의 제거(Decoupling)의존성의 제거(Decoupling)와 높은 응집성(Cohesion)응집성(Cohesion)"으로 요약된다. 의존성은

어쩔 수 없이 발생할 수 밖에 없지만 가능하다면 줄여야 한다. 응집성은 코드의 조각들(모듈단위, 파일단위, 함수나 클래스

단위에서도)이 주어진 역할만을 충실하게 수행하도록 뭉쳐져야 한다. 의존성이 줄어든 코드는 의존적인 것들이 변경되더

라도 최소한의 영향만을 받게된다. 따라서, 의존성을 줄이면 확장이나 변경이 쉬워진다. 응집성이 높은 코드는 외부에 대

한 의존성이 줄어들게 되며, 잘 정의된 역할(Role)만을 수행하기에 변경에 대한 영향을 외부로 전달하지 않고 내부에서 해

결한다. 이 두가지 원칙이 코딩의 기본원리다.

객체지향 언어를 사용하는 사람은 한번쯤은 "SOLID"라는 원칙을 들어봤을 것이다. 즉, 객체지향 코딩에서는 SOLID원칙

에 충실하게 코딩을 해야지만, 앞에서 이야기한 의존성이 줄어들고 응집성이 높은 코드를 만들어낼 수 있다. SOLID원칙은SOLID원칙은

"SRP(Single Responsibility Principle),OCP(Open Close Principle),LSP(Liskov Substitution Principle),ISP(Interface"SRP(Single Responsibility Principle),OCP(Open Close Principle),LSP(Liskov Substitution Principle),ISP(Interface

Segregation Principle),DIP(Dependency Inversion Principle)"Segregation Principle),DIP(Dependency Inversion Principle)"을 의미한다. 이런 원칙들이 객체지향 언어에서만 유용

한 것은 아니며, C언어와 같은 절차지향 언어에서도 의존성을 줄이고, 응집성이 높은 코드를 만드는데 도움을 줄 수 있다.

SRP(Single Responsibility Principle)SRP(Single Responsibility Principle)은 한가지 책임만을 가지도록 코딩하라는 말이다. 여기서 말하는 한가지 책임이란

추상적인 수준에서 한 가지 개념을 구현하는 코드를 말한다. 책임이란 결국 역할에 대해서 주어지는 활동의 범위를 말하

며, 그 범위를 표현할 수 있는 하나의 추상적인 개념을 찾을 수 있으면 된다. 예를 들어, 운영체제를 보게될 때, 큰 개념에

서는 하드웨어 자원을 관리하는 소프트웨어라는 것으로 표현되며, 다시 그 개념을 나누면 CPU 스케줄링, 메모리 관리, 파

일 시스템, 네트워크로 나누어진다. 각각이 하나의 추상적인 개념을 구현하고 있으며, 다시 하위 개념으로 쪼갤 수 있다.

이때 각각의 개념은 자신이 해야할 활동의 범위가 있다는 것을 알 수 있다. 즉, 그 범위의 역할만 충실한 코드를 만들면 된

다.

C언어에서의 가장 작은 단위(Unit)로 가져갈 수 있는 것이 함수라고 할 때, 하나의 함수는 하나의 개념을 구현해야 한다.

함수들이 모인 파일은 한가지 개념을 구현하기 위한 함수들로 구성되어야 하며, 파일들을 묶은 폴더(디렉토리)들은 한가

지 개념을 구현하기 위해서 필요한 파일들로 묶여야 한다. 다시 이런 폴더들이 모여서 하나의 서브시스템을 구현하고, 서

브시스템들이 모여서 시스템을 구현하게 된다. 묶이는 과정에서 개념적으로 생략(혹은, 지나친 비약)이 발생한다면, 더 세

분화할 것이 남았다는 이야기다. 따라서, 이때는 좀 더 추가적으로 정의할 개념들이 있는지를 분석해야 한다.

OCP(Open Close Principle)OCP(Open Close Principle)은 확장에 대해서는 열려(Open)있고, 변경에 대해서는 닫혀(Close)있어야 한다는 원칙이

다. 이 원칙은 지속적인 변경이 발생하는 소프트웨어의 특성에 대해서 어떻게 대응할 수 있을 것인가에 대한 해결책이다.

1 · [ 소프트웨어 개발자 이야기 ]

Page 2: C Language II

즉, 새로운 기능을 구현하기는 쉬우면서도, 변경의 영향이 부수효과(Side Effect)가 없도록 해야한다는 말이다. 이러한 코

드를 만들기 위한 핵심은 추상화(Abstraction)에 있다. C++언어와 같은 객체지향 언어에서는 ADT(Abstract Data

Type)을 제공하며, 확장을 위해서 상속과 같은 기법을 사용할 수 있다. 또한, 상속된 클래스도 부모 클래서가 제공하는 인

터페이스를 제공하기에, 부모 클래스에 접근하듯이 상속된 자식 클래스를 접근할 수 있도록 하고 있다.

C언어에서도 추상화를 통해서 이와 유사한 기능을 제공할 수 있다. 즉, 자료구조에 대한 상세한 내용은 가리고, 단순히 자

료구조의 형(Type)에 대한 정보만을 제공한 후에, 이 자료구조를 접근하는 함수들을 제공하는 방법이다. 호출하는 코드에

서는 상세한 자료구조에 대한 정보를 알 수 없기에 반드시 함수를 통해서 접근해야 한다는 단점이 있지만, 내부적인 자료

구조의 변경이나 함수 코드의 수정이 호출하는 코드에 대한 영향을 최소화 시킬 수 있는 방법이다. 호출하는 코드와 호출

을 받는 코드는 서로 모르면 모를 수록 더 좋은 코드가 만들어질 가능성이 높다. 너무 많은 정보를 공개하는 것은 코드들의

의존성을 높이는 일이기 때문이다.

LSP(Liskov Substitution Principle)LSP(Liskov Substitution Principle)은 대체(Replacement)에 대한 것이다. 즉, 프로그램에서 필요한 객체(Object)를 하

위의 타입으로 프로그램의 정확성을 해치지 않고 교체가 가능하여야 한다는 원칙이다. 객체지향 언어의 경우 클래스를 정

의하고, 이를 상속한 클래스들이 부모의 역할을 대신처리할 수 있다는 것으로, 객체를 접근하는 쪽에서는 접근되는 객체의

타입만을 알고 있을 뿐이다. 실제 어떤 객체를 접근하는지는 알지 못하기에, 상속받은 객체들은 어떤 것이라도 부모의 역

할을 대신할 수 있게 되는 것이다. 이것은 실행 중에 프로그램의 동작을 변경할 수 있는 요소가 되기도 하지만, 호출하는

코드의 변경없이 새로운 환경에 적용할 수 있는 방법으로 사용될 수 있다.

C언어에서는 이와 비슷한 예로 다양한 파일 시스템을 하나의 함수로 접근하는 경우를 들 수 있다. 즉, 하위에서 실제로 구

현하는 방법은 다르지만, 상위의 코드는 일관된 인터페이스를 사용해서 각각의 전혀 다른 파일 시스템을 사용할 수 있다.

변경은 하위의 코드에서만 일어나게 되며, 상위 코드의 변경없이 새로운 파일 시스템을 지원할 수 있다. 이를 위해서는 C

의 "struct"와 "*(pointer)"를 이용해서, 동적으로 변경가능한 코드를 구현하는 방법이 있다. 포인터는 주소를 가르키기에

변수의 주소나 함수의 주소에 상관없이 사용할 수 있으며, 이를 잘 이용하면 동일한 인터페이스를 가지지만 새로운 기능을

추가하는데 유용하다.

ISP(Interface Segregation Principle)ISP(Interface Segregation Principle)은 필요한 인터페이스만 관심을 두어야 한다는 것이다. 즉, 호출하는 측에서 필요로

하는 인터페이스들을 나누어서 관리하라는 말이다. 상호 관련성이 없는 인터페이스들을 묶어서 두면, 인터페이스를 구현

하는 측에서는(호출되는 코드에서는) 복잡도가 증가(코딩 량이 증가)해서 구현하기 어려워진다. 관련성이 없는 인터페이

스들을 따로 묶어두면, 계층이 더 생길지는 모르지만 구현하기가 쉬워지며, 역할의 구분과 계층의 구분이 나타나게 된다.

그리고, 인터페이스 자체도 역할이 명확해지기 때문에, 더 이해하기 쉬운 코드가 나온다.

C언어에서 이것을 적용하기 위해서는 인터페이스에 해당하는 함수에 대한 정의가 더 명확하게 드러나도록 해야한다. 그

리고, 그런 함수들을 묶어서 관리해줄 수 있는 방법이 필요하다. 헤더 파일을 이용해서 관련된 함수들을 목적에 맞게 구분

해주고, 각각의 헤더 파일 하나당 구현 파일 하나를 가지는 것도 좋다. 즉, 호출하는 측에서는 자신이 필요한 부분만 정확

2 · [ 소프트웨어 개발자 이야기 ]

Page 3: C Language II

히 가져다 사용할 수 있으면 된다. 헤더 파일로 묶어 주는 원칙은 공통된 자료구조를 다루는 단위가 될 수 있다. 하지만, 자

료구조 자체가 너무크다면 자료구조내의 관련성을 분석한 후에 자료구조 자체를 나누고, 관련된 함수를 분리된 구현 파일

로 나눌 수 있다.

DIP(Dependency Inversion Principle)DIP(Dependency Inversion Principle)은 일종의 "의존성 제거(Decoupling)" 방법이다. 즉, 구체적인 것에 의존하지 말

고, 추상적인 것에 의존하라는 뜻이다. 호출하는 코드는 호출받는 코드의 세밀한 부분(Detail)에 의존하지 말아야 하며, 호

출받는 코드도 제공된 인터페이스를 충분히 만족시킬 수 있도록 구현되어야 한다는 뜻이다. 호출하는 코드는 자신이 의존

하는 대상을 인터페이스를 정의하고, 이렇게 정의된 인터페이스를 호출받는 코드가 구현하게된다. 따라서, 기존의 호출하

는 코드가 호출받는 코드에 의존성을 가지는 대신에, 호출받는 코드가 호출하는 코드의 인터페이스에 대해서 의존성을 가

지도록 구현된다는 점에서 "의존성 역전(Dependency Inversion)"이라고 볼 수 있다.

C언어에서 DIP를 구현하기 위해서는 의존성이 생기는 부분에 대해서 인터페이스를 정의한 후, 이를 새로운 계층으로 표현

하는 것이 좋다. 그리고, 이를 구현하는 코드에서 정의된 인터페이스를 채워주도록 한다. 새로운 계층으로 표현된 인터페

이스가 이미 존재하는 코드와 호환되지 않을 경우에는 호환을 위한 추가적인 코드가 더 필요할 수 있다. 호출하는 코드에

서는 자신이 필요한 인터페이스에만 의존성을 가지게 되며, 인터페이스 자체는 호출되는 코드에서 구현하기에 인터페이

스를 기준으로 두개의 코드가 계층적으로 나누어질 수 있다. 또한, 기존의 이미 있는 코드를 이용해서도 구현할 수 있는 분

리된 계층이 존재하기에, 코드의 재사용 가능성도 높아지게 된다. 소프트웨어에서 복잡성을 해결하는 방법은 이처럼 계층

적인 구조를 가지는 것이다.

소프트웨어 아키텍처(Software Architecture)소프트웨어 아키텍처(Software Architecture)

소프트웨어의 개발을 건축에 비유하는 경우가 많은데, 건물을 지을 때 필요한 설계도와 모델링 등도 소프트웨어를 개발하

는데 있어선 필수적인 요소다. 즉, 미리 다양한 생각들을 구성해서 이를 구현에서 기준으로 활용한다. 만들어야 할 시스템

에 대한 명세(Specification)가 명확하게 주어지는 경우는 드물기에 대략적인 명세를 가지고 설계를 시작하게 되며, 구현

과 설계를 반복하는 과정에서 더 명확한 명세가 만들어진다. 설계는 구현을 위한 기준을 제공하기에 무턱대고 코딩부터 시

작하면 나중에 수정되어야 할 부분들이 늘어나게 되며, 그런 것들이 쌓이게되면 수정하는 비용이 새로 짜는 것 보다 더 많

이 들어갈 수도 있다. 따라서, 생각을 정리해서 코딩을 어떻게 할 것인가를 정해야 한다.

설계는 시스템을 구성하는 요소를 정하는 일부터 시작한다. 그리고, 그렇게 정해진 요소들간의 관계를 설정한다. 이렇게

만들어진 설계서를 "소프트웨어 아키텍처(Software Architecture)"라고 부르며, 이런 업무를 주로 담당하는 사람을 "아

키텍트(Architect)"라고 부른다. 시스템을 구성하는 요소들은 시스템이 구현해야 할 기능들에 의해서 좌우되며, 요소들간

의 관계는 비기능적인 것들이 결정한다. 따라서, 기능적인 것과 비기능적인 명세를 확보하는 것이 중요한 일이며, 결과적

으로 사용자를 만족시킬 수 있는 소프트웨어를 만드는 가장 핵심적인 활동이라고 할 수 있다.

3 · [ 소프트웨어 개발자 이야기 ]

Page 4: C Language II

2014/10/15 23:12 http://blog.naver.com/knix008/220151953694

[ 매크로의 활용 - 02 ][ 매크로의 활용 - 02 ]

C언어C언어

매크로를 사용할 때는 주의해야 한다. 잘 못된 대치(Replacement)로 인해서 발생하는 오류들은 찾기 어려우며, 매크로

정의가 사용되는 곳을 제한하기도 어렵다. 즉, 파일 전체적으로 영향을 줄 수 있기 때문이다. 만약, 주요 헤더 파일에 정의

된 매크로라면, 그 영향력이 더 커진다. 원칙적으로 전역 자료 구조를 가져가는 것은 옳지 않은 일이다. 매크로로 길게 코

드를 작성할 일이 있다면, 차라리 함수로 선언해서 사용하는 것이 좋다. "inline" 함수의 경우가 대표적인 예이다. 매크로

간단한 식을 만들때도 "inline"함수를 활용한다. 매크로로 변수를 선언하는 변경되지 않는 상수 변수를 사용할 수 있다(예

로, cont int PI = 3.141592).

#include <stdbool.h>

#include "stack.h"

inlne bool isEmpty( STACK stack )

{

if ( stack->top == stack->bottom )

{

return true;

}

return false;

}

위의 함수는 간단한 stack을 구현한 것이다. stack의 top과 bottom이 같을 때, stack이 비었다고 가정했다. 항상

bottom은 고정상태로 두고, top을 움직인다고 보았다. 이 함수를 이용할 때는 단지 일반적인 함수와 같이 사용하면 된다.

매크로와 다른 점은 함수가 컴파일러의 의해서 매크로처럼 대치해서 코드에 들어갈 것인지, 아니면 일반적으로 함수로 호

출이 될 것인지가 결정된다는 점이다. 따라서, 매크로 보다 읽기 쉬운 코드를 만들 수 있으며, 잘 못될 확률도 줄어든다. 컴

파일러가 문법적인 검사를 다 할 것이기에 단순한 대치와는 다르다.

4 · [ 소프트웨어 개발자 이야기 ]

Page 5: C Language II

2014/10/15 15:42 http://blog.naver.com/knix008/220151501843

[ 좋은 코드의 특성 ][ 좋은 코드의 특성 ]

C언어C언어

기초를 배우는 사람들은 어떤 코드가 좋은 코드인지를 반드시 알아야 한다. 기초가 튼튼해야 나중에 무너지는 경우가 생기

지 않는다. 소프트웨어 개발에서 말하는 "무너짐"은 실제로는 발생하지 않는다. 다만 비용과 개발기간, 그리고 품질에 문

제가 발생할 뿐이다. 그리고, 결과적으로는 그런 소프트웨어가 시장에서 살아남기는 힘들다. 좋은 코드는 사실 그렇지 못

한 코드보다 오래 살아남는다. 그래서, 생각보다 유지보수(Maintenance)비용도 더 들기도 한다. 하지만, 그런 비용을 다

합한다 해도 좋은 코드가 벌어들이는 이득보다는 많지 않을 것이다. 코드 문제로 인해서 발생하는 비용은 생각보다 훨씬

많다. 왜냐하면, 수정(기능추가 혹은 버그 해결)에 필요한 소프트웨어 개발자가 신규 개발과제에 투입되지 못하는 비용까

지 합쳐야 하며, 대응이 늦어짐에 따라 고객들이 더 이상 신용하지 않게 될 것이기 때문이다.

01. 읽기 쉽다.01. 읽기 쉽다.

; 가장 중요한 특징으로 다른 사람이 읽기 쉽게 작성된 코드가 좋은 코드이다. C언어를 위해서 필요한 것이 아니라, 모든

코드는 반드시 읽기 편해야 한다. 읽기 어려운 코드를 짜는 것은 버그 수정이나 신기능 추가와 같은 유지보수에 치명적인

영향을 주게되기 때문이다. 나보다는 남을 위해서 일하는 것이 결국 자신을 위한 것이다. 함수의 이름이나 변수의 이름(함

수내의 Local변수의 이름도 포함하고, 반복문에 사용되는 임시 변수들도 이름을 가지는게 좋다. 예외적으로 i, j, k등과 같

은 배열을 접근하기 위한 임시변수는 이미 많이 사용되기에 이름을 주지 않을 수 있다.)

02. 개념적으로 분리되어 있다.02. 개념적으로 분리되어 있다.

; 모든 코드 조각(Unit)들이 하나의 개념을 구현한다. 여러가지 일을 한번에 하지 않으며, 추상적으로 한가지 개념만 구현

한다. 개념적으로 한 가지 일만한다는 의미는 함수가 하는 일을 하나로 정의할 수 있다는 말이되며, 그 정의를 이용해서 함

수의 이름을 정한다. 특히, C언어에서는 함수를 가장 작은 단위로 보기에 함수의 이름을 정하는 것은 중요한 일이다. 함수

의 계층을 따라 내려갈 경우, 상위 함수가 가지는 개념의 한 수준아래의 이름을 하위 함수가 가져야 한다.

03. 변경에 대한 대비가 되어 있다.03. 변경에 대한 대비가 되어 있다.

; 변경할 수 있는 부분들을 미리 설계에 반영해서 구현되어 있다. 변경이 생기더라도 지역적(Local)인 영향만 준다. 변경에

대한 대비는 주로 인터페이스의 정의로부터 시작한다. 좋은 함수를 만드는 것은 항상 이런 변경에 대비한 한가지 방법이

다. 설계의 마지막도 그러한 인터페이스에 대한 정의다. 인터페이스의 정의가 완료되고, 이를 이용해서 구현할 내용이 명

확해지면 코딩할 수 있는 준비가 완료된 것이다.

04. 주석이 적다.04. 주석이 적다.

5 · [ 소프트웨어 개발자 이야기 ]

Page 6: C Language II

; 주석을 많이 다는 경우는 대부분 코드를 설명하기 위한 것이지만, 코드 자체가 쉽게 이해가 된다면 주석은 별 필요없다.

주석이 필요한 경우는 선택에 대한 이유(Why?)를 알려줄 경우다. 실무자들은 코딩을 할 때 한 라인당 하나의 주석을 달라

고 할 정도로 많이 주석을 넣지만, 코드를 쉽게 만들어 이해가 되는데도, 그 코드를 다시 설명하는 주석을 많이 사용한다.

이것도 일종의 반복이며, 반복은 없는게 좋다.

05. 중복이 적다.05. 중복이 적다.

; 중복된 코드들이 많은 경우는 정리되지 않고 급하게 개발된 코드인 경우가 많다. 중복은 최소화 되어야 하며, 가능하다면

없어야 한다. 코드의 중복도 있지만, 논리의 중복도 확인해야 한다. 특히, "switch()"문과 같은 경우, 각각의 경우에 대해

서 반복적으로 코드가 있는 경우가 많이 생기며, 또한, 여러곳에서 사용할 코드를 복사해서 붙여넣는(Copy & Paste) 경우

에도 발생한다. 이런 것들은 코드가 변경될 경우에 일일이 찾아가면 수정해야하는 어려움이 있다. 반복되는 코드는 함수로

만들어 사용하는 것이 좋다.

06. 마치 한 사람이 개발한 것처럼 보인다.06. 마치 한 사람이 개발한 것처럼 보인다.

; 여러 사람이 개발에 참여했지만, 개발 결과물에 대해서 관리를 잘 했다는 의미다. 즉, 규칙(Rule)에 기반해서 코딩이 진

행되었으며, 코드들이 전부 리뷰(Review)가 되었을 확률이 높다. 이러한 규칙을 코딩 룰(Coding Rule, Coding

Convention, Coding Standard)이라고 부르며, 보통의 경우 회사 차원보다는 팀 차원이나 과제 차원에서 관리된다. 과

제에 참여하는 모든 개발자들은 그 내용을 잘 숙지하고 있어야 하며, 일관되게 지속적으로 사용하는 것이 중요하다.

07. 자동화된 테스트가 존재한다.07. 자동화된 테스트가 존재한다.

; 자동화된 테스트가 없다면 변경이 발생할 경우 매번 오랜시간을 테스트로 낭비하게 된다. 자동화된 테스트는 필수이다.

그리고, 그 결과로 만들어지는 테스트 범위(Coverage)는 얼마나 코드가 테스트 되었는지를 알려주는 척도로 사용할 수

있다.(100% Coverage라고 해도 버그는 존재한다.)

08. 코드속에 모듈간의 상하관계가 명확하다.08. 코드속에 모듈간의 상하관계가 명확하다.

; 상식적으로 복잡한 문제는 내부에 계층적인 구조를 가질 가능성이 높다. 소프트웨어는 복잡한 문제를 어떻게 다루냐에

따라 품질이 달라질 수 있으며, 계층적(Layering)으로 문제를 해결해간다. 이때, 상위 계층은 하위 계층에 의존적이며, 하

위 계층은 상위 계층에 의존적이지 않다(즉, 호출이 없다.).

09. 함수들의 인수 갯수가 적다.09. 함수들의 인수 갯수가 적다.

; 한 덩어리로 개발되지 않은 코드들은 당연히 다른 함수(혹은, 메쏘드:Method)의 호출을 가진다. 그런 함수들의 정의가

한 가지 역할에 충실하게 되어 있다면, 인자들의 갯수는 적을 것이다.

10. 모든 함수의 호출 뒤에는 반드시 오류를 검증하는 코드가 있다.10. 모든 함수의 호출 뒤에는 반드시 오류를 검증하는 코드가 있다.

; 함수들은 실패할 가능성이 항상 존재한다. 따라서, 반드시 호출 결과를 검증하는 코드를 동반한다. 완전한 코드가 없듯

이, 자신이 만든 함수를 남들이 사용하더라도 최소한 사용법과 복귀 값에 대한 설명은 필요하다.

6 · [ 소프트웨어 개발자 이야기 ]

Page 7: C Language II

11. 매직 넘버(Magic Number)가 없다.11. 매직 넘버(Magic Number)가 없다.

; 아마도 코드를 읽을 때 가장 어려운 부분이 매직 넘버일 것이다. 왜 그 수가 나왔는지, 그리고 어떤 의미인지에 대해서 전

혀 아무런 답이 없는 코드들이 많다. 그런 수를 만날 때면 대부분 그냥 그러려니하고 넘어가게 되지만, 결국에는 다시 그

부분으로 가서 해석하는데 시간을 다 보내고 만다. 매직 넘버를 줄이는 방법은 "enum"과 같은 것을 사용하거나, 상수 변

수(const variable)를 정의해서 사용하도록 한다.

12. 함수의 내부에는 넘겨받은 인수에 대한 적합한 범위를 검증한다.12. 함수의 내부에는 넘겨받은 인수에 대한 적합한 범위를 검증한다.

; 방어적인 코드를 할 경우, 함수가 넘겨받는 인수가 적합한지를 검사해야 한다. 누가 어떻게 사용할 지 모르는 코드이기

때문이다. 특히, 배포와 디버그를 할 경우를 대비해서, 이런 코드들을 따로 묶어서 조건부 컴파일을 시킬 수도 있다(즉, 검

사가 완료되어 더 이상 필요가 없을 경우에는 제거한다). 혹은, 그냥 코드에 남겨서 잘 못된 호출이 발생한 원인을 남기고

프로그램을 종료 시킬 수도 있다.

이외에도 다양한 것들이 있을 수 있지만, 일단은 이 정도로 만족하다. 중요한 것은 어떤 규칙을 가지고 끝없이 코드를 개선

하는 것이다. 그리고, 그 규칙이라는 것들이 일반적으로 받아들여지는 좋은 코드를 만드는 방법들이어야 하는 것은 당연하

다. 실무에서 만나는 코드들은 위와 같지 않으며, 이해하기도 쉽지 않다. 특정 도메인(Domain)에 대한 지식을 요구하는

경우도 많으며, 그런 것들이 생략된 코드에서 찾는것은 오랜 경험을 요구한다. 따라서, 우리가 해야할 일은 그런 시간을 단

축시켜줄 수 있는 방법을 고안하고 코딩에 적용하는 것이다.

7 · [ 소프트웨어 개발자 이야기 ]

Page 8: C Language II

2014/10/15 12:51 http://blog.naver.com/knix008/220151327080

[ 변수의 크기(Size)에 민감한 코드는 어떻게? ][ 변수의 크기(Size)에 민감한 코드는 어떻게? ]

C언어C언어

정수를 표현하는 "int"는 CPU의 아키텍처(Architecture)에 의존적이다. 즉, 32bit을 지원하는 CPU는 32bit크기를 가지

며, 64bit CPU에서는 64bit을 가진다. 만약 다양한 CPU에서 동작해야 하는 코드를 만든다면, 변수의 크기에 민감한 값들

은 따로 타입을 정의해서 관리하는 것이 일반적이다. 즉, 각각의 사용하고자 하는 타입의 크기를 CPU별로 설정할 수 있도

록 헤더 파이렝 정의를 하고, 이를 CPU에 맞게 포팅(Porting)해서 사용한다. 예를 들어, 32bit 양의 정수만을 가지는 변수

에는 "uint32_t"와 같이 "#define"으로 선언하고, 해당하는 CPU에 적당한 값으로 정하도록 한다.

#ifndef __TYPE_H__

#define __TYPE_H__

#define uint32_t unsigned int /* 32bit CPU에서 양의 정수만을 사용하겠다. */

#define uint8_t unsigned char /* 8bit 양의 정수만을 사용하겠다. */

...

#endif

이렇게 정의하고나서 모든 변수의 타입을 이것에 맞춰서 정의한다. 구현 파일에서는 "type.h"를 "#include" 시켜주기만

하면 된다. 이렇게 모든 변수의 크기가 특정한 크기를 가져야할 필요가 있는 코드들은 하드웨어에 의존적인 코드인 경우가

많다. 그리고, 실시간 운영체제(Real-Time Operating System)의 경우에는 자료형에 대해서 특히 민감한 부분들이 있기

때문에, 이런 식으로 코딩을 해 주어야 한다.

변수를 선언할 때, 흔히 메모리를 더 적게 쓰는 방법을 생각해서 변수에 최적의 크기를 가지는 타입으로 정의하는 경우가

많다. 하지만, 이렇게 해서 얻는 효과가 크지 않을 경우가 많다. 예를 들어, 32bit CPU에서 함수의 인수를 32bit 보다 작

게 선언할 경우, 어차피 내부적으로는 32bit 스택과 같은 것을 이용하거나, 혹은 레지스터를 이용해서 전달되기에 32bit

을 다 사용한다고 볼 수 있다. 또한, 복귀 값(Return Value)로 32bit 보다 작은 값을 주는 경우, 32bit 크기로 변환하는 과

정이 내부적으로 추가될 수 있다. 이럴 경우에도 역시 그냥 32bit 복귀 값을 사용하는 편이 더 빠른 코드를 만드는데 도움

을 줄 수 있다.

위에서와 같이 특정 크기를 가지는 변수들을 명시할 경우에는 다른 기본 자료형들도 다 정의해서 사용하는게 좋다. 그리

고, 일관되게 모든 코드에서 다 같이 적용하는 것이 좋다. 번거롭기는 하지만 나중에 코드를 다른 CPU 아키텍처에 적용할

경우에는 컴파일만 하면 실행될 가능성이 높기 때문이다.

8 · [ 소프트웨어 개발자 이야기 ]

Page 9: C Language II

정수를 나타내는 "int"에 대해서 좀더 자세히 보도록 하자. "int"는 음의 정수, 0, 양의 정수를 다 포함하며, CPU 아키텍처

에 따라 그 크기가 달라질 수 있다. 그리고, "if()"와 같이 사용될 경우에 표현하는 값이 어떤가에 따라 달리 해석될 수 있

다.

#include <stdio.h>

#include <stdlib.h>

int main(void)

{

unsigned int bigger = 8;

int smaller = -9;

puts("This is Integer Comparison Test Program!!!");

if ((bigger + smaller) < 4 )

{

puts("This is TRUE!!!\n");

printf("The value : %d\n", bigger + smaller);

}

else

{

puts("This is FALSE!!!\n");

printf("The value : %d\n", bigger + smaller);

}

return EXIT_SUCCESS;

}

코드를 보면, "8 + ( -9 )"를 계산해서 "4"보다 작은지를 이용해서 값을 화면에 출력한다. 실제로 계산 결과는 "-1"로

"4"보다 작은 값이지만, 화면에 출력되는 값은 "This is FALSE!!!"를 볼 수 있을 것이다. 이유는 "if()"와 같은 조건절에서

는 부호가 있는 값들이 부호가 없는 값으로 인식되어 "-9"가 "(0xFF - 9)"로 해석되기 때문이다. 따라서, "if()와 같은 조건

절에서 정수 비교시에는 부호가 있는 정수와 부호가 없는 정수를 혼용해서 사용해서는 안된다.

9 · [ 소프트웨어 개발자 이야기 ]

Page 10: C Language II

정수의 크기에 민감한 자료형을 정의하기 위해서는 "stdint.h"를 이용하는 것이 좋다. 이 헤더 파일에는 각종 정수의 크기

및 부호여부에 따라 사용할 수 있는 위한 자료형이 제공되고 있다. "stdint.h"는 "C99" 표준에서 채택되었으니, 사용하는

컴파일러가 지원하는지 확인할 필요가 있다. "GCC(GNU C/C++ Compiler)"를 사용하는 경우에는 이미 제공되고 있다.

10 · [ 소프트웨어 개발자 이야기 ]

Page 11: C Language II

2014/10/14 11:49 http://blog.naver.com/knix008/220150228538

[ 하드웨어가 없으면 코딩할 수 없다? ][ 하드웨어가 없으면 코딩할 수 없다? ]

C언어C언어

보통 하드웨어가 준비되지 않으면, 실제 하드웨어에서 동작해야하는 소프트웨어를 만든데 어려움을 겪는 경우가 많다. 대

부분의 경우 사용하려는 칩셋(Chipset)으로 만들어진 평가보드(Evaluation Board)를 구매해서 일단 소프트웨어 개발을

진행하게 된다. 요즘 같은 경우에는 대부분의 하드웨어 기능이 하나의 칩에 다 들어가 있는 경우가 많지만, 특정 주변장치

를 사용하기 위한 인터페이스 역할을 하는 칩들은 따로 구매해서 사용하기도 한다. 예를 들어, 네트워크 디바이스같은 것

은 하나의 주요 칩에 들어가지 않고 따로 구매해서 사용한다. 그리고, 특정 장치와의 인터페이스를 위해서 메모리 주소 맵

핑(Memory Mapped I/O)을 하는 경우도 있는데, 이럴 경우에는 하드웨어와의 직접적인 통신을 위해서 메모리 공간에

대한 접근을 시도하게 된다. 즉, 하드웨어와의 직접적인 통신을 담당하는 소프트웨어의 경우에는 타겟(Target)이 되는 하

드웨어가 없는 상황에서는 소프트웨어의 개발이 어렵다.

그렇다고, 그냥 하드웨어의 개발이 끝나기를 기다리는 것은 과제를 지연시킬 가능성이 많기에, 대부분의 경우에는 하드웨

어 인터페이스를 분리하는 방법으로 소프트웨어 개발을 시도하게 된다. 즉, 하드웨어의 스펙이 있다면, 이것을 기준으로

하드웨어에서 제공해야하는 기능과 소프트웨어에서 제공해야할 데이터를 분석한다. 하드웨어 스펙을 구할 수 없다면, 해

당하는 하드웨어와 유사한 기능을 하는 하드웨어를 찾아서 제공되는 기능과 제공해야할 데이터를 보고, 하드웨어에 접근

하는 API(Application Programming Interface)를 먼저 정의하게 된다.

원칙은 앞에서 이미 이야기했지만, 하드웨어를 추상화 시키는데 있다. 즉, 하드웨어에 대한 직접적인 접근대신에 그에 해

당하는 인터페이스를 정의하고, 이를 기반으로 소프트웨어를 만드는 것이다. 따라서, 실제 하드웨어가 나오게 되면 정의한

인터페이스를 구현하는 일을 해주어야 한다. 하드웨어가 준비되기 전에는 마치 실제 하드웨어와 같이 동작하는 가상의 환

경을 소프트웨어로 구현해서 사용한다. 여기서 중요한 점은 직접적인 하드웨어가 맵핑된 주소를 접근하는 코드를 만들어

서는 안된다는 것이다. 이것을 해결하는 방법은 마찬가지로 하드웨어가 맵핑된 주소로 접근하는 것들을 인터페이스로 만

들어야 한다.

좀더 나아가서 하드웨어에 정확한 값을 읽고 쓰는지를 확인하기 위해서는 하드웨어를 접근하는 코드의 외부에서 하드웨어

의 주소를 지정하는 방식을 사용하는 것이 좋다. 즉, 하드웨어 인터페이스를 가로채서 원하는 값을 읽고 쓰는지를 확인하

기 위해서다. 이것은 일종의 "의존성 삽입(Dependency Injection)의 기법으로, 객체지향 언어에서 객체의 생성을 특정

객체내부에서 하기보다는 외부에서 삽입해 주는 방법을 응용한 것이다. 당연히 삽입되는 객체나 혹은 하드웨어에 의존적

인 주소 값들은 외부에서 정의한 객체나 변수의 주소등이 될 것이다. 따라서, 외부에서는 해당 객체나 변수를 읽어서 원하

는 동작을 하는지를 물을 수 있으며, 원하는 값을 적어줄 수도 있게 된다.

11 · [ 소프트웨어 개발자 이야기 ]

Page 12: C Language II

#include <stdio.h>

#include <stdlib.h>

#include <stdbool.h>

int hardwareStatusRegister;

bool initializeHardware( int &statusRegisterAddress )

{

*statusRegisterAddress = 0xFFFF0000; /* Intialize Hardware with 0xFFFF0000 */

if ( *statusRegisterAddress != 0xFFFF0000 )

{

return FALSE;

}

return TRUE;

}

int main()

{

initializeHardware( &hardwareStatusRegister );

return EXIT_SUCCESS;

}

위의 코드는 특정 주소에 값을 저장하고, 그것을 읽어서 쓴 값과 읽은 값이 일치하는지를 확인해서 제대로 하드웨어가 초

기화 되었는지를 확인한다(실제로는 하드웨어의 레지스터들은 읽기 전용이나 쓰기 전용, 혹은 읽기 쓰기 가능등으로 나눠

질 수 있으며, 읽기가 불가능한 경우도 있다.). 여기서는 하드웨어의 상태 레지스터의 주소를 초기화시에 설정해서(Inject

시켜서), 나중에 다른 곳에서 해당 변수를 읽을 수 있도록 만든 경우다.

12 · [ 소프트웨어 개발자 이야기 ]

Page 13: C Language II

2014/10/14 11:21 http://blog.naver.com/knix008/220150199749

[ 매크로(Macro)의 활용 ][ 매크로(Macro)의 활용 ]

C언어C언어

C언어에서 매크로는 일종의 대체(Replacement)로 활용되고 있다. 즉, 논리적으로 한 덩어리의 코드를 묶어서 한 번에 처

리할 수 있도록 만들어준다. 그 중에서도 특히 수식이나 자료구조의 정의, 함수등을 대체할 수 있는 수단으로 사용될 수 있

다. 일반적인 수식이나 자료구조의 대체는 프로그래머가 사용할 수 있는 어휘를 늘리는 역할을 하며, 함수등의 대체는 다

형성(Polymorphism)을 제공하기 위해서 사용된다고 볼 수 있다.

#ifdef __DEBUG__

#define DEBUG_PRINT( fmt, args... ) printf( fmt, ##args )

#else

#define DEBUG_PRINT( fmt, args... ) /* Nothing */

#endif

위와 같은 코드는 개발중인 소프트웨어에서 printf()를 활용해서 디버깅 메시지를 출력할 때, 흔히 사용할 수 있는 방법이

다. 즉, 디버깅 목적으로 컴파일 할 경우에는 메시지를 출력하지만, 만약 배포(Release)를 목적으로 할 경우에는 아무런

메시지도 출력하지 않도록 만든다. 왜 이것이 중요하냐면 흔히 개발자들은 배포시에도 메시지를 출력하는 코드를 만들어

서, 시스템이 이상 동작을 할 경우를 대비한 로그(Log)를 만들려고 하기 때문이다. 그리고, 위와 같은 매크로를 정의하지

않고, printf()와 같은 출력함수를 바로 사용해서 디버깅 목적의 코드와 배포 목적의 코드를 구분하지 않는 경우가 흔하기

때문이다.

#include <stdio.h>

#include <stdlib.h>

#define __DEBUG__ 1

#if ( __DEBUG__ == 1 ) /* #ifdef __DEBUG__ , #if defined( __DEBUG__ ) */

#define PRINT_WARNING( fmt, args... ) printf( fmt, ##args )

#define PRINT_ERROR( fmt, args... ) printf( fmt, ##args )

#define PRINT_DEBUG( fmt, args... ) printf( fmt, ##args )

#define PRINT_LOG( fmt, args... ) printf( fmt, ##args )

#else

13 · [ 소프트웨어 개발자 이야기 ]

Page 14: C Language II

#define PRINT_WARNING( fmt, args... )

#define PRINT_ERROR( fmt, args... )

#define PRINT_DEBUG( fmt, args... )

#define PRINT_LOG( fmt, args... )

#endif

int main(void)

{

PRINT_DEBUG("This is a Test Message!!!\n");

return EXIT_SUCCESS;

}

디버깅을 목적으로 한다면, 디버깅 목적을 세분화 해야한다. 크게 디버그(Debug), 경고(Warning), 오류(Error), 로그

(Log)로 나누어서 각각에 맞는 매크로를 정의해서 사용하는 것이 좋다. 그리고, 추가적으로 개별 모듈별로 메시지 출력을

끄고 켤 수 있는 매크로를 따로 가져가는 것이 좋다. 왜냐하면, 디버그시나 기타 발생하는 메시지를 자신이 개발한 모듈에

한정적으로만 적용하고, 다른 사람이 개발중인 모듈에서 발생하는 메시지와 섞이지 않도록 만드는게 훨씬 효율적이기 때

문이다.

#define __MODULE_PRINT__ 0

#define __MODULE_SEND__ 0

#define __MODULE_MYMODULE__ 1

...

위와 같이 정의한 후에, 각각의 모듈별로 디버그 매크로를 정의한 파일을 만들어서, 앞에서 정의한 모듈별 메시지의 끄고

켜는 것을 정의한 헤더 파일을 include하면 될 것이다. 모듈별로 자신만의 매크로를 일관되게 사용할 수 있다면, 실제 실

행에서 발생하는 디버깅 메시지의 양도 작으며 찾기도 편하다. 그리고, 수준(Level)별로 메시지의 출력을 정하면, 정말 원

하는 메시지만 골라서 볼 수 있다. 이런 것들은 주로 과제의 시작 초기 코딩에서 개발자들간의 협의로 정하고, 과제 완료까

지 일관되게 적용하는 것이 좋다.

14 · [ 소프트웨어 개발자 이야기 ]

Page 15: C Language II

2014/10/13 13:45 http://blog.naver.com/knix008/220149253262

[ "float"의 사용에 대한 주의 ][ "float"의 사용에 대한 주의 ]

C언어C언어

"float"는 부동 소수점을 나타내기 위해서 사용하는 자료형이다. 다양한 유용성이 있지만, 가장 중요한 것은 유효수자의

범위와 관련 된 것은 기억해야 한다. 그리고, ARM과 같은 CPU에서 코드의 크기가 문제가 될 경우에는 조심해서 사용해

야 한다. 즉, ARM CPU Core에는 부동 소수점을 지원하는 것이 추가적인 소프트웨어 라이브러리(Library)를 통해서 가능

한 경우가 있다. 예전의 경험으로 생각해 보면, 운영체제 코드에서는 부동 소수점을 사용하는 경우가 없다. 운영체제의 경

우 연산 능력보다는 주로 하드웨어에 직접적인 접근을 요구하는 코드가 많으며, 이런 하드웨어에 직접적으로 접근하는 코

드에서는 부동 소수점 연산이 필요하지 않기 때문이다. 대부분의 경우 양의 정수 값을 사용한다. 특히, 디바이스 드라이버

를 구현하는 경우에는 레지스터(Register)에 대한 읽기 쓰기 값은 양의 정수다.

15 · [ 소프트웨어 개발자 이야기 ]

Page 16: C Language II

2014/10/13 13:20 http://blog.naver.com/knix008/220149229736

[ 추상화 ][ 추상화 ]

C언어C언어

추상화(Abstraction)는 인간이 가장 잘 하는 것 중에 한 가지다. 흔히 특정 사람을 부를 때 "이름"을 사용한다. 그 사람 자

체가 누군지를 묘사하는 것이 아니라, 그 사람을 대표하는 "이름"을 부르는 것이다. 마찬가지로 코드에서도 특정한 일을

수행하는 함수를 가리킬 때, 함수의 이름을 사용한다. 그 함수 내부에서 일어나는 것들을 대표할 수 있는 이름을 붙이는 것

은 당연한 일이다. 즉, 구체적인 것을 모르고도 우리는 그 함수를 불러서 사용할 수 있게되는 것이다. 그 함수가 사용하는

자료구조가 어떤 형태인지는 모르지만, "그 함수를 호출해서 우리가 얻을 수 있는 결과는 어떤 것이다."라는 것을 가정할

수 있기 때문이다.

호출하는 코드를 클라이언트(Client)라고하고, 호출받는 코드를 서버(Server)라고 할 때, 클라이언트와 서버간에는 모종

의 약속이 있게 된다. 즉, 서버에서 제공되는 서비스를 사용하기 위해서 클라이언트가 어떤 행동들을 해주어야 한다는 것

이며, 그러한 서비스를 충족시킬 의무는 서버측에 있다는 것이다. 일종의 계약(Contract)이라고도 볼 수 있는데, 우리가

함수를 만들 때도 이러한 계약관계를 명확히 해 주어야 한다. 즉, 클라이언트 코드는 함수의 내부는 모르지만, 함수가 제공

하는 인터페이스(Interface)를 호출함으로써, 원하는 결과를 얻을 수 있어야 한다는 점이다. 따라서, 클라이언트 코드는

인터페이스에 대해서 의존적이게 되며, 서버는 인터페이스만 지키면 내부적인 구조는 변경이 가능하다는 것이다.

추상화의 관점에서 C언어는 파일 범위에서의 한계(Limit)를 사용한다고 볼 수 있다. 즉, 자료구조를 구현 파일에 담아두

고, 이를 같은 파일에 있는 함수들이 이용할 수 있다. 물론, 헤더 파일에 선언해서 다른 파일에 있는 함수들도 사용할 수 있

지만, 헤더파일은 될 수 있으면 최소한의 정보만 공개하는 형태로 만들어져야 한다. 만약, 내부적인 자료구조를 알 필요가

있는 다른 파일에 정의된 함수가 있다면, 오히려 그 함수를 나누어서 자료구조에 접근하는 함수를 자료구조가 정의된 파일

로 옮겨주는 것이 좋다. 즉, 의존성을 분리하는 것이다. 이렇게 해서 얻는 장점은 코드가 변경이 용이한 구조가 된다는 점

이다. 단점은 함수를 통해서만 자료구조에 접근하기에 접근 경로가 길어질 수 있다는 점이다. 하지만, 얻는 장점에 비해서

단점은 극히 미미하기에(혹은, 거의 없기에), 추상화를 기본으로 한 코딩이 더 효과적이라고 볼 수 있다.

예를 들어보도록 하자. Stack과 Queue라는 예는 항상 코드에서 많이 등장한다. Stack은 먼저 들어가면 나중에 나오는 자

료구조를 표현할 때 많이 사용되며, Queue는 먼저들어가면 먼저 나오는 자료구조에 많이 사용된다. "stack.h"와

"queue.h"를 아래와 같이 정의할 수 있다.

#ifndef _STACK_H_

#define _STACK_H_

16 · [ 소프트웨어 개발자 이야기 ]

Page 17: C Language II

typedef struct stackStructure *STACK;

STACK createStack( void );

Bool pushStack( STACK stack, x );

int popStack( STACK stack );

#endif

코드에서 먼저 사용할 자료구조를 선언해 주는 것을 볼 수 있다(stackStructure). 그리고, 이를 사용하기 위해서, 생성하

는 함수와 push 및 pop을 담당하는 함수를 선언했다. push와 pop함수를 위해서, 생성시에 자료구조에 대한 포인터를 얻

고, 이를 함수의 인수(Parameter)로 넘겨주었다. 이 함수들을 호출하는 코드들은 "stack.h"파일을 "#inlcude"시켜주어

야 하겠지만, "stackStructure"가 어떻게 구현되었는지를 알 필요는 없다.

17 · [ 소프트웨어 개발자 이야기 ]

Page 18: C Language II

2014/10/13 12:53 http://blog.naver.com/knix008/220149205815

[ 할당 문과 비교문 ][ 할당 문과 비교문 ]

C언어C언어

할당 문은 변수에 특정 값을 저장하는 목적으로 사용한다. 비교문은 두 개의 변수나 상수 값을 비교할 경우에 사용한다. C

언어에서는 할당 문에는 "="를, 비교문에는 "=="를 사용해서 혼동을 주는 경우가 흔하다. 예를 들어, "x=y"와 "x==y"는

서로 다른 의미를 가지기에 실수로 잘 못 사용할 경우에는 찾아내기 어려운 문제가 되곤한다.

if( x = y )

{

printf("The value of x and y is same!!!\n");

}

else

{

printf("The value of x and y is not same!!!\n");

}

위의 예제는 x와 y의 값이 같은 지를 확인하기 위해서 사용했지만, 실제로는 x의 값에 y를 넣어 0이 아닌 값이라면 항상

"The value of x and y is same!!!"이라는 출력 결과를 얻게 된다. 즉, "=="를 사용해야 하는데, "="를 사용해서 생겨나

는 문제다. 버그를 수정하기 위해서 논리적인 것보다는 좋은 시력이 더 필요한 경우다. 좀 더 명쾌하게 의미를 전달하기 위

해서는 "if( x = y )"를 차라리 함수의 형태로 만들수도 있다.

BOOL isEqual( x, y )

{

if( x == y ) return TRUE;

return FALSE;

}

if ( isEqual( x, y ) )

{

printf("The value of x and y is same!!!\n");

}

else

18 · [ 소프트웨어 개발자 이야기 ]

Page 19: C Language II

{

printf("The value of x and y is not same!!!\n");

}

물론, 이 경우에는 지나치게 추상화 시킨 경향이 있기는 하지만, 어쨌든 직접적으로 보기에는 문제가 생길 여지가 줄어든

다. 즉, 한번에 코드를 다 읽지 않고, 두 부분으로 나누어서 이해해야 할 부분을 줄여주고 있기 때문이다. 이런 식으로 복잡

한 조건(Condition)에 대한 처리를 분리된 함수로 만들수 있다.

19 · [ 소프트웨어 개발자 이야기 ]

Page 20: C Language II

2014/10/13 10:03 http://blog.naver.com/knix008/220149046383

[ 코드 리뷰에 대한 단상 ][ 코드 리뷰에 대한 단상 ]

C언어C언어

코드 리뷰(Code Review)를 하라는 윗 사람의 지시에 대해서 고민해 본 적이 있을까? 윗 사람이 지시는 하지만, 아랫 사람

은 그 지시에 대한 최선의 해답을 찾지않고 일단은 모면할 수 있는 이유를 찾게된다. 이유는 단순하다. 코드 리뷰를 왜 하

는지 이해하지 못하고, 어떻게 하는지도 알지 못하기 때문이다. 그리고, 더 중요한 것은 그런 시간이 있으면 코딩에 더 집

중하는 것이 좋다고 생각한다. 하지만, 코드 리뷰도 일종의 피드백(Feedback)이라는 것을 잊고 하는 소리다. 문제는 발생

한 순간과 가장 근접한 시간에 수정하는 것이 좋다. 문제가 발생한 이후에 시간이 흘러버리면, 문제의 파급효과는 더 커지

고 더 많은 비용을 들여야 수정할 수 있다.

코드 리뷰의 목적은 "코드의 가독성(Readability)" 높이는 것이다. 즉, 코드가 제대로 일을 하고 있는지를 쉽게 파악하고,

구조적인 문제가 없는지를 보기 위한 것이다. 이와 같은 활동을 통해서 소프트웨어 개발자들 간에 기술의 상향 평준화를

이룰 수 있으며, 전체적인 시스템의 이해를 높여 더 좋은 품질을 가지는 소프트웨어를 만들기 위함이다. 따라서, 코드 리뷰

는 문제의 근원인 코드를 직접보고, 코드에 대한 공통된 해석을 모든 소프트웨어 개발자들이 공유하도록 해야한다. 시간이

부족하다고 빼먹고 지나가면, 나중에 문제가 발생했을 때는 코드를 이해하는 활동과 더불어 버그를 찾는 시간까지 같이 소

비해야 하기에, 코드 리뷰는 시간을 낭비하는 일이 아니라 시간을 더 효과적으로 사용할 수 있도록 하는 일이다.

1. 코딩 룰(Coding Rule)을 준수하는지 확인한다.

2. 코드의 가독성(Readability)이 낮은 부분을 확인한다.

3. 코드가 주어진 일을 충실히 수행하는지를 확인한다.

4. 코드가 주어진 일 이외에 수행하는 일이 없는지를 확인한다.

5. 코드가 적절한 테스트가 있는지를 확인한다.

위와 같은 5가지 코르 리뷰는 체크 리스트는 소프트웨어 개발자들이 코드 리뷰시에 활용해 볼 수 있는 것들이다. 여러 개

발자가 소프트웨어 개발에 참여하더라도 마치 한 사람이 코딩을 한 것 처럼 보이도록 만드는 것이 좋다. 이렇게 보이기 위

해서는 팀원들이 공유하는 개발 문화를 가지는 것이 필수다. 그리고, 코드 리뷰에 앞서 팀원들이 공유하는 코딩 룰을 간단

하게라도 유지할 필요가 있다. 대부분의 소프트웨어 과제는 시작전에 이런 부분들에 대해서 잘 정리한다. 각종 문서의 양

식(Format)을 정리하고, 코딩에 대한 룰도 만들어서 팀원들이 익숙하게 사용할 수 있도록 템플릿(Template)도 제공한다.

물론, 이런 것들을 만드는데 사소한 의견 충돌이 있을 수는 있으나, 한번 만들어진 규칙에 대해서는 모든 팀원들이 다 지켜

야 한다.

20 · [ 소프트웨어 개발자 이야기 ]

Page 21: C Language II

코드 리뷰는 일방적인 지시가 아니다. 물론 경험의 차이는 있지만, 그런 경험을 서로간에 공유하는 것이지 숙제를 검사받

는 자리는 아니다. 즉, 코드 리뷰에서는 사람에 대한 평가가 아니라, 코드에 대한 평가를 해야한다. 이를 통해서 팀원들간

의 공유되는 지식이 늘어나게 되며, 좋은 코드가 무엇인가에 대한 서로간의 의견을 나누는 것이다. 코드 리뷰는 팀 전체 개

발자들의 요청을 받는 것이 좋으며, 최소한 2명 이상이 리뷰를 완료한 후에 코드를 저장소에 "Check-In"할 수 있도록 해

야한다. 팀 전체 개발자가 5에서 9명 사이라고 한다면, 리뷰를 요청한 팀원에 대해서 2명을 고정적으로 할당할 수도 있을

것이다. 직급이나 경력은 일단 리뷰자 할당에서는 무시해야 한다. 왜냐면, 직급이 높은 사람에게 리뷰 요청이 몰리거나, 혹

은 일방적인 명령하달이 안되도록 하는 것이 중요하기 때문이다. 현실적으로 경험을 무시할 수는 없기에 최소한의 업무 부

담을 역량이 뛰어난 사람에게 지어주는 것이 좋다.

21 · [ 소프트웨어 개발자 이야기 ]

Page 22: C Language II

2014/10/09 14:14 http://blog.naver.com/knix008/220145621781

[ C언어 정리중인 자료 Upload ][ C언어 정리중인 자료 Upload ]

C언어C언어

첨부파일 참고.

22 · [ 소프트웨어 개발자 이야기 ]

Page 23: C Language II

2014/10/08 11:23 http://blog.naver.com/knix008/220144525368

[ "Hello, World!!!"를 다시 보다. - 04 ][ "Hello, World!!!"를 다시 보다. - 04 ]

C언어C언어

만들어진 c_main.c 파일과 printHelloWorld.c파일는 각각 분리되어 재 사용될 수 있다. c_main.c파일은 Test 용도로 재

활용이 가능하고, printHelloWorld.c파일은 다른 프로그램에서 파일 자체를 이용할 수 있다. 하지만, 여기에도 추가할 부

분이 존재한다. 즉, C와 C++ 코드를 같이 사용하기 위해서는 C 파일에 추가적으로 해줘야 할 일이 있다. 즉, C로 정의된

함수를 C++에서 찾을 수 있도록 만들어주는 것이다. 이를 위해서는 "extern "C""를 사용한다. "extern"은 외부에서 정의

된 것을 찾으라는 뜻이고, ""C""는 그것이 C언어로 표현된 것이라는 뜻이다. C++ 컴파일러는 이 문장을 만나면, 자신이

가지고 있는 기본적인 기능보다는 "C"언어의 규칙을 따르도록 코드를 생성한다.

#ifdef __cplusplus

extern "C" {

#endif

int printHelloWorld();

#ifdef __cplusplus

}

#endif

위와 같이 해주면, printHelloWorld()함수를 이제는 C++에서도 활용할 수 있다. 따라서, C++에서 C코드를 재사용하기

위해서는 Header파일에 위와 같이 정의해 주는 것이 필요하다. 수정하지 않았을 경우에는 C++ 컴파일러는 해당 함수를

찾을 수 없다는 링크(Link)오류를 발생시킬 것이다. 실무 개발에서는 C언어 및 C++언어를 섞어서 사용할 수 있으며, 이때

는(C++언어 컴파일러를 사용하는 경우에는) 이런 과정이 반드시 필요하다.

한 가지 프로그램에서는 일반적으로 하나의 언어를 사용해서 구현하는 것이 일반적이지만, 이미 만들어진 라이브러리

(Library)나 제 삼자(3rd Party)가 제공하는 바이너리(Binary)를 사용할 경우에는 개발 환경에 맞춰서 재사용할 코드를 변

경해 주어야 한다. 이를 위해서 Header파일이나, 혹은 컴파일 옵션(Option)을 변경할 수 있도록 코드에 삽입하기도 한

다.

23 · [ 소프트웨어 개발자 이야기 ]

Page 24: C Language II

2014/10/07 13:36 http://blog.naver.com/knix008/220143598549

[ 포인터(Pointer)에 대해서 ][ 포인터(Pointer)에 대해서 ]

C언어C언어

C언어를 배우는데 있어서 가장 큰 장애물은 많은 사람들이 지적했듯이 포인터(Pointer)의 사용에 관련되어 있다. 포인터

는 주소(Address)를 적어두는 명패으로 수시로 값을 변경할 수 있다(물론, 상수 포인터로 선언된 경우에는 변경이 불가능

하며, 선언 시에 초기화도 해주어야 한다.). 즉, 그 명패가 가리키는 주소로 가야지 원하는 것을 찾을 수 있다는 뜻이다. 포

인터는 특정 메모리를 가리키는 역할을 하기에, 포인터의 값을 즉각적으로 사용하지 않고, 그 값을 다시 이용해서 원하는

주소를 찾게 된다. 찾아간 주소에는 데이터가 들어있을 수도 있고, 코드가 들어갈 수도 있다. 즉, 포인터는 자신이 가리키

는 값에 대해서는 사용자가 쓸 수 있도록 형(Type)을 지정해 주어야 한다는 말이다. 어떤 내용을 가르키는지를 알아야지

나중에 컴파일러가 실제 동작하는 코드를 만들때 정확한 형을 검사할 수 있기 때문이다.

포인터는 다양한 유용성이 있지만, C언어가 가장 적당한 분야인 하드웨어를 제어하는 코딩에서 가장 잘 사용될 수 있다.

즉, 특정 주소(대부분의 경우, 논리적인 주소보다는 물리적인 주소를 사용한다.)에 대해서 데이터를 읽거나 쓸 경우에 포

인터를 사용할 수 있다. 예를 들어, 0xFFFF0000이라는 주소에 데이터를 쓰는 것을 아래와 같이 할 수 있다.

const int *memoryBuffer = 0xFFFF0000;

int status = 0;

* memoryBuffer = 0x0100;

status = *memoryBuffer;

const라고 선언한 이유는 memoryBuffer가 포인팅(Pointing)하는 주소를 더 이상 변경하지 않고 고정하겠다는 뜻이며,

"status"는 memoryBuffer 포인터가 가리키는 곳의 주소에서 데이터를 읽어들인다. 장치의 경우에는 특정 메모리 주소에

자신들을 제어할 수 있는 레지스터(Register)들을 메핑(Mapping)시켜서 사용하는 경우가 많은데, 이때 포인터를 이용해

서 제어하기를 원하는 장치에 직접적으로 접근할 수 있다. 한가지 덧붙이고 싶은 것은 하드웨어에 대한 접근은 특정 순서

대로 해야할 경우가 많은데, 이때는 컴파일러가 제공하는 최적화(Optimization) 옵션으로 인해서 접근 순서가 바뀔 수 있

다. 이를 막기 위해서는 "volatile"이라는 것을 변수의 선언 앞에 둘 수 있다.

24 · [ 소프트웨어 개발자 이야기 ]

Page 25: C Language II

2014/10/07 13:31 http://blog.naver.com/knix008/220143593610

[ 연산자의 우선 순위를 걱정하나? ][ 연산자의 우선 순위를 걱정하나? ]

C언어C언어

솔직히 연산자의 우선 순위는 크게 기억할 필요가 없다. 왜냐면 "()"와 같은 것을 충분히 사용해서 구분해 주면, 오류가 날

가능성이 줄어들기 때문이다. 하지만, 기본적인 우선 순위정도는 기억할 필요가 있으며, 일반적인 사칙 연산외에 조합되는

연산들에 대해서는 "()"를 충분히 사용해서 우선 순위를 지정하도록 한다. 가장 쉽게 틀리는 부분은 "*"를 사용해서 포인

터(pointer)로 이용하는 부분인데, 이때도 포인터라는 것을 명시적으로 나타내주면 문제될 것은 없다. 생각을 하게 만들지

않는 코딩이 최선이다. 읽는 것에 막힘이 없다면 좋은 코딩인 것이다.

연산자들과 "()"를 같이 섞어서 사용할 때 가장 틀리기 쉬운 곳은 매크로(Macro)를 정의하는 경우다. 매크로는 말 그대로

1:1로 대치(Replace)가 발생하기에, 제대로 "()"를 사용하지 않을 경우에는 엉뚱한 값을 만들어 낸다. 그리고, 매크로는

부수효과(Side Effect)을 가질 가능성이 높기에 복잡한 매크로를 사용할 경우에는 차라리 함수(Inline함수와 같은)를 만들

어서 사용하는 것이, 버그도 줄이고 코드의 이해력도 높일 수 있는 방법이다. 기본적인 연산자의 운선 순위에 대해서 충분

히 이해했다면, 이를 응용한 코딩은 어떻게 하는지 몇가지 살펴보는 것이 좋을 것이다.

25 · [ 소프트웨어 개발자 이야기 ]

Page 26: C Language II

2014/10/07 13:22 http://blog.naver.com/knix008/220143585670

[ "Hello, World!!!"를 다시 보다. - 03 ][ "Hello, World!!!"를 다시 보다. - 03 ]

C언어C언어

printHelloWorld()함수는 내부에서 "printf()"함수를 호출하고 있다. 만약 printf()함수를 사용해서 화면(Console)상에

글자를 쓰는 것이 아니라, 로그(Log)를 저장하는 목적이라고 한다면 어떨까? 즉, 단순히 printf()함수를 호출하는 것 보다

는 사용자의 설정에 맞춰서 출력 방향을 변경해 줄 수 있어야 한다. 이를 위해서는 "printf()"함수 자체에 대한 추상화를 고

려하게 되는데, 이때 사용할 수 있는 방법도 역시 인터페이스를 정의하는 것이다.

void (*PRINT)(const char* message);

이렇게 형식을 정하고, 화면으로의 출력과 로그의 출력에 대해서 각각 "printMessageOnConsole()",

"printLogMessageOnFile()"과 같은 함수를 같은 형식으로 선언하면 된다. 각각의 함수는 화면에 대한 출력과 파일에 대

한 출력으로 구분해서 구현한다. 그리고, 각각의 함수는 "print"라는 범주에 속하기 때문에 같은 파일내에 구현될 수 있다.

나중에 더 기능이 세분화 되면, 이것도 분리된 파일로 나눠주면 된다.

26 · [ 소프트웨어 개발자 이야기 ]

Page 27: C Language II

2014/10/07 11:29 http://blog.naver.com/knix008/220143488456

[ Contents ][ Contents ]

C언어C언어

1. 도입

2. Programming 기초

A. Design

B. Implementation

C. Process

3. Coding Rule

A. Embedded System Coding Rule

B. General Rule

4. Code Review

A. Tools

B. How to Review Code

5. Unit Test

A. Why?

B. What We Need to do Unit Test

6. Test Driven Development

A. Software Bug

7. Refactoring

8. 맺음말

27 · [ 소프트웨어 개발자 이야기 ]

Page 28: C Language II

2014/10/07 10:10 http://blog.naver.com/knix008/220143411181

[ "Hello, World!!!"를 다시 보다. - 02 ][ "Hello, World!!!"를 다시 보다. - 02 ]

C언어C언어

"Hello, World!!!" 프로그램을 보면, main()함수와 printHelloWorld()함수간의 논리적인 일관성이 부족하다. 즉,

main()함수가 하는 일과 printHelloWorld()가 하는 일은 호출관계가 있지만, main()함수와 같은 파일에 있기에는 파일

이름(printHelloWorld.c)과 어울리지 않는다. 따라서, printHelloWorld()함수를 main.c(혹은, main()함수를 분리해서

다른 파일로)에서 분리해서 새로운 파일로 만드는 것이 옳다. 분리된 파일을 "printHelloWorld.c"라고 하자. 그리고,

main()함수가 정의된 파일은 "c_main.c"라고 하자. c_main.c파일에 남은 main()함수는 아래와 같다.

#include<stdio.h>

/* Start Point */

int main( int argc, char** argv )

{

return printHelloWorld();

}

이전보다 좀더 깔끔하게 정리된 것을 볼 수 있다. 즉, c_main.c파일에는 main()함수 이외에는 정의가 없는 것을 볼 수 있

다. 대부분의 소프트웨어 개발자들은 c_main.c파일이 프로그램의 시작을 정의한 파일이라는 것을 인식할 것이고, 잠시

훑어본 후에는 다시 열어보는 일이 없을 것이다.한가지 빠진 부분은 printHelloWorld()함수를 main()함수가 찾을 수 없

다는 점이다. 즉, 어디에도 정의가 나오지 않는다. 따라서, printHelloWorld()함수를 찾을 수 있도록 도움을 주어야 한다.

먼저해야 할 일은 선언과 구현을 분리하는 일이다.즉, 인터페이스(Interface)와 구현(Implementation)을 각각 다른 파일

로 만들어서, 구현을 인터페이스 너머로 감춰주는 일이다. 이렇게 하는 이유는 인터페이스의 일관성을 유지하면서, 구현을

확장 혹은 변경을 하기 위함이다. 즉, 구체적인 것에 의지하면 변경이 어려워지거나 혹은, 변경의 영향으로 버그가 유발될

가능성이 높기 때문이다.위의 경우에는 Header파일을 통해서 인터페이스와 구현이 분리될 수 있다.

선언과 정의를 분리하는 것은 중요한 개념이다. 객체지향 프로그램 언어에서 제공하는 캡슐화(Encapsulation)를 이용한

숨김(Information Hiding)을 구현하는 이유와도 동일하다. 즉, 구체적인 것을 감춤으로 해서 변경의 자유도를 높이는 것

이다. 변경의 자유도가 높다는 말은 호출하는 코드와 호출되는 코드간에 연결고리가 약해져서, 서로 상대방을 알지 못하는

상황에서 인터페이스만 일치시키면 변경이 자유롭다는 것이다. 물론, 인터페이스가 제공해야할 기능은 반드시 호출되는

코드에서 제공되어야 한다. 그렇지 않다면, 인터페이스라는 계약을 위반한 것이기 때문이다.

28 · [ 소프트웨어 개발자 이야기 ]

Page 29: C Language II

Header파일이 하는 역할은 간단하다. 즉, 각종 필요한 선언(Declaration)들을 넣어서 필요한 모듈(Module)에서 참조하

도록 만든는데 있다. 어떤 자료형이나 함수의 선언들을 미리 가지고 있어서, 그것을 필요한 부분에서 그 선언들을 참고해

서 코딩할 수 있도록 도움을 주는 것이다. 따라서, Header파일에는 가능한 최소한의 정보가 포함되어야 한다. 많은 정보

가 있을수록 좋다고 생각할 수도 있겠지만, 필요한 만큼의 정보를 외부에 제공하는 것이 핵심이다. 그리고, 정의는 가능한

구현파일에 숨기고, 인터페이스만 외부에 제공되어야 한다. 핵심 자료구조와 같은 것들도 될 수 있으면 외부에 제공되지

않아야 한다. 단순한 형(Type)정도의 정보만 제공되면 충분한다. 여기서는 printHelloWorld()함수를 위한 header파일을

보도록 하자. 파일의 이름은 "printHelloWorld.h"라고 하자.

29 · [ 소프트웨어 개발자 이야기 ]

Page 30: C Language II

2014/10/06 14:38 http://blog.naver.com/knix008/220142607239

[ 함수(Function), 프로그래밍의 최소단위 ][ 함수(Function), 프로그래밍의 최소단위 ]

C언어C언어

C언어에서는 가장 작은 단위(Unit)의 프로그램으로 하나의 함수(Function)을 사용한다. C++과 같은 객체지향 언어에서

는 가장 작은 단위로 클래스(Class)를 가진다. 따라서, 하나의 함수를 잘 만드는 것은 집을 짓는데 비유하면, 하나의 좋은

벽돌을 만드는 것과 같다고 볼 수 있다. 좋은 재료를 가지고 있어야 좋은 건축물을 지을 수 있듯이(물론, 좋은 설계도 중요

하다), C언어에서는 좋은 함수를 만드는데 노력을 집중해야 한다. 그럼 좋은 함수란 무엇일까? 좋은 함수가 가져야하는 특

징은 아래와 같이 요약해서 볼 수 있다.

01. 함수는 짧을 수록 이해하기 쉽다.

02. 테스트를 위해서는 출력이 있는 것이 좋다. 즉, 실패했을 때와 성공했을 때를 구분할 수 있어야 한다.

03. 될 수 있으면 다른 함수나 모듈에 의존하지 않아야 한다. 즉, 다른 함수나 모듈을 호출하는 것이 적을 수록 좋다.

04. 함수의 이름은 함수가 하는 일을 정확히 설명할 수 있어야 한다.

05. 함수는 한가지 일만 해야 한다.

06. 함수는 입력 인수(Parameter)가 없을수록 좋다. 가장 좋은 것은 아예 인수가 없는 함수이다.

07. 함수는 입력 인수가 많이 필요할 경우, 구조체(Structure)를 이용해서 인수를 묶어서 관리하는 것이 좋다.

08. 함수의 내부에서 사용하는 자료구조는 외부로 될 수 있으면 보여주지 않아아 햔다.

09. 함수의 외부로 자료구조를 보여주어야 할 경우에는, 자료구조를 생성하는 함수를 추가로 구현해서 내부구조를 숨긴

다.향후에는 이렇게 생성된 자료구조를 필요한 함수들의 인수로 사용한다.

09. 함수의 입력 인수들은 값의 유효성 범위가 검증될 수 있어야 한다.

10. 각각의 함수마다 테스트 할 수 있는 프로그램을 충분히 만든다.

11. 함수를 종료하는 조건은 한 곳만 있는 것이 이해하기 쉽다.

12. 각 함수들은 중복된 코드가 없어야 한다. 있을 경우에는 이를 뽑아내서 다른 함수로 만든다.

이곳에서 본 것들이 좋은 함수를 만들기 위한 모든 방법은 아니다. 개발자 마다 자신의 경험이 틀리기 때문에, 다양한 팁

(Tip)들이 있을 수 있다 하지만, 대체적으로 위에서 본 것들은 기본적으로 좋은 코드를 만들기 위해서 가져야 할 것들이기

에 기억하는 것이 좋을 것이다. 각각에 대해서 좀더 자세히 살펴보도록 하자.

30 · [ 소프트웨어 개발자 이야기 ]

Page 31: C Language II

2014/10/06 13:07 http://blog.naver.com/knix008/220142516684

[ "Hello, World!!!"를 다시 보다. ][ "Hello, World!!!"를 다시 보다. ]

C언어C언어

"Hello, World!!!"는 C나 C++언어를 보면 가장 먼저 제시되는 예제 프로그램이다. "Main"으로 시작해서 간단히 "Hello,

World!!!"라는 문장을 화면에 출력한다. 아래의 코드를 보도록 하자. 일단 아래의 코드를 HelloWorld.c라는 파일에 저장

했다고 가정하자.

#include<stdio.h>

int main(int argc, char** argv)

{

printf("Hello, World!!!\n");

return 0;

}

간단한 코드다. 하지만, 초보자에게는 대단히 어려울 수 있는 코드다. 왜냐면 모르는 언어를 처음 배울때, 단어의 뜻과 문

법에 압도될 수 있기 때문이다. 이 코드는 단순히 C코드가 어떻게 동작하는지를 알려주기 위해서 작성된 것이지만, 처음보

는 사용자는 이해하기 어려울 수 있다는 사실을 간과 하고 있다. 그리고, 모든 일반적인 언어(일본어, 중국어, 영어 등등)을

배울때 가장 쉬운 방법은 그냥 들은 것을 따라해서 반복하는 것이라는 것을 내포하고 있다.

따라서, 이해하기 쉬운 코드를 짜야하는 것과 더불어 습관적으로 좋은 코드를 짜는 방법을 반복해서 익혀야 한다. 개선할

점은 먼저 파일의 이름부터 시작한다. "HelloWorld.c"라는 파일이름 보다는 내부의 코드가 무슨 일을 하는지를 정확히 기

술하는 이름을 주는 것이 좋다. "PrintHelloWorld.c"는 어떨까? 일단 파일을 열지 않아도 무슨 일을 하는지는 알 수 있다.

급한 경우에는 파일 이름만 보고도 쉽게 원하는 일을 하는 코드를 찾을 수 있고, 파일 검색등도 이용할 수 있다. 두 번째는

"main()"함수가 무엇인지를 알려주는 것이 좋다(물론, 이 경우는 아무것도 모르는 신참 개발자는 대상으로 한다.).

"main()"함수 위에 간단히 "/* Start Point */와 같은 Comment를 적어준다(가장 좋은 코드는 Comment가 필요없이

코드가 즉각 이해되는 것이다.).

"printf()"함수는 시스템 호출(System Call)을 동반한다. 그리고, "main()"함수의 수준에서 호출하기에는 저 수준 함수에

속한다. 즉, 추상화 수준이 "main()"함수보다 한단계만 아래에 있다고 볼 수 없다. 따라서, "int printHelloWorld()"라는

함수로 분리시켜준다. 이때는 "main()"함수가 하는 일이 저 수준 함수의 호출보다는 더 효과적으로 이해가 될 것이다. 그

31 · [ 소프트웨어 개발자 이야기 ]

Page 32: C Language II

리고, "return"문은 "printHelloWorld()"함수의 호출을 돌려주는(Return)하는 것을 바꿀 수 있다. 따라서, 다시 적은 코

드는 아래와 같다.

#include<stdio.h>

int printHelloWorld()

{

printf("Hello, World!!!\n");

return 0;

}

/* Start Point */

int main(int argc, char** argv)

{

return printHelloWorld();

}

조금 개선되었다고 보이나? 사실 초보자가 이런 모든 과정을 볼 이유는 없다. 여기서 단지 몇 가지만 보여주고 싶기 때문

에 적어본 것이다. 첫 번째는 코드로 이해가 어려울 경우에만 코멘트로 설명한다는 점이다. 두 번째는 함수의 추상화 수준

은 자신이 하는 일을 한번에 설명할 수있는 이름을 가져야 한다는 점과 함수의 내부 코드들은 이름보다 한 단계 아래의 추

상화 수준을 가져야 한다는 점이다. 세번째는 함수의 이름은 이해하기 쉬워야 하며, 하나의 함수는 추상화 수준에서 하나

의 일만해야 한다는 것이다(이것을 Single Responsibility Principle"이라고 한다.). 짧은 함수가 이해하기 쉽다. 모든 함수

는 복귀값(Return Value)을 가지는 것이 좋다는 것과 인자(Parameter)가 없는 함수가 최고라는 것이다.

32 · [ 소프트웨어 개발자 이야기 ]

Page 33: C Language II

2014/10/06 11:50 http://blog.naver.com/knix008/220142451854

[ 도입(Introduction) ][ 도입(Introduction) ]

C언어C언어

시중에는 C언어에 대한 다양한 종류의 책들이 많이 나와있는 상황이다. 그리고, 많은 프로그래머들이 그런 책을 읽고 공부

한 후에, 프로그래밍을 한다. 다양한 책들의 예제는 신참 프로그래머에게는 따라하기 쉬운 코드들로 이루어져 있고, 아무

런 부담감 없이 "Copy-and-Paste"를 한다. 이런 습관이 자주들다보면 결국 상업적으로 코딩해야할 경우에도 책에서 나온

예제를 그냥 사용하게 되고, 부지불식간에 익힌 습성이 몸에 베인다. 하지만, 상업적으로 유용한 코드를 짜는 것은 다른 세

상이며 그런 코딩을 가르치는 곳도 없다. 오로지 자신의 "시행착오"와 오랜 경험 및 공부를 통해서만 배울 수 있는 지식들

은 쉽게 얻지 못하는 값비싼 것들이다. 이제는 이런 부분에 대해서 충분히 이야기 할 필요가 있다고 본다. 왜냐하면, 우린

일상에서의 실패들이 모두 이런 것들에 기인하고 있다는 사실을 잘 알기 때문이다.

상업적인 코드와 "아마추어적인 코드"의 차이점은 뭘까? 단순히 생각한다면, 상업적인 코드는 외부 고객(External

Customer)뿐 아니라, "내부 고객(Internal Customer)"의 욕구를 만족시켜줄 수 있는 코드이며, 내부 고객으로는 다른 소

프트웨어 개발자와 상품기획, 관리자, 테스터등 다양하다. 다른 소프트웨어 개발자는 코드가 읽기 편하고 잘 구조화

(Structured)되어 있어서 전체적으로 이해하기 쉬워야 한다는 것을 강조한다. 상품기획은 추가적인 기능 구현을 꾸준히

요구하기에 확장이 쉬운 코드를 만들기를 기대하며, 관리자는 저비용으로 짧은 기간동안에 원하는 소프트웨어를 만들어

내기를 소프트웨어 개발자에게 요구한다. 테스터도 마찬가지로 자신이 테스트 해야할 코드가 테스트가 쉽게되어 있고, 자

동화되어 있는 것을 원할 것이다. 가장 중요한 것은 어쨌든 나 이외의 다른 소프트웨어 개발자가 편한 마음으로 쉽게 고칠

수 있는 코드를 만들어야 한다. 이것이 궁극적인 상업적으로 좋은 코드라 할 수 있겠다. 물론, 상업적으로 만들어진 코드들

이 이런 모든 것들을 만족시킨다고 볼 수는 없다. 하지만, 추구해야할 목표는 반드시 "이해하기 쉬운"코드여야 한다는 점

이다.

그런 코드를 만드는 것은 기초적인 문법만을 가지고 되는 것은 아니다. 즉, 많은 시행착으로를 겪고, 그 속에서 개선된 생

각을 반영해야 한다. 하지만, 그렇다고 남들이 다 겪은 문제를 다시 겪을 필요는 없다. 이미 충분한 경험은 다양한 형태로

축적되어 있고, 이제는 그것을 초보 개발자들이 이해할 수 있는 형태로 주어지면 되는 것이다. 다른 공학 분야와 마찬가지

로 지식은 항상 기초적인 토대를 바탕으로 쌓아 올라가는 방향으로 발전한다. 기초적인 지식이란 실무에서 겪는 문제들에

대한 해답들이며, 그런 해답들 위에서 이론적인 토대가 마련된다. 그리고, 다시 그런 이론적인 바탕에서 실무적인 개선이

이루어지며, 그렇게 쌓여진 것들이 다시 다른 형태로 사용자에게 다가갈 수 있는 제품 형태로 만들어진다. 어쨌든 기초가

가장 중요한 것임은 부인할 수 없다. 기초가 없으면 다시 허물고 쌓아올리게 되는 일이 잦아지게 되며, 다시 새로운 형태의

기준이 제시에 맞게 시행착오를 경험할 것이다.

33 · [ 소프트웨어 개발자 이야기 ]

Page 34: C Language II

시중에 있는 다양한 책들은 아직 시행착오를 겪었던 것을 충분히 반영하고 있지 못하며, 이런 지식들은 소위 발하는

"Guru"에게서만 듣거나 혹은 그들이 쓴 책을 공부해서 얻어지고 있다. 하지만, 몸에 밴 습관은 쉽게 고쳐지지 않으며, 잘

못된 지식은 현실에서 다시 다음 세대로 아무런 근거없이 전달된다. 왜냐하면, 그렇게 배운 사람들이 이미 중요한 직책에

서 중요한 일을 하고 있을 것이기 때문이다. 그렇다고, 그들이 옳다는 것은 아니다.(또한, 그렇다고 그들이 그릇되었다고

이야기 하지도 않겠다.) 자신들이 경험한 것을 잘 정리하고 개선할 시간이 없었다고 생각되기 때문이다. 사실 우리는 빨리

가기 위해서 천천히 근본적인 것들에 대한 것을 익힐 수 있는 시간을 가지지 못하는 것이 실무의 현실이다. 왜 이런 현실이

주어졌는지 근본 이유를 찾아올라가면 사실 새로운 것은 없다. 즉, "코딩은 열심히하고 많이 하지만, 제대로 하지 않는

다."가 정답이다. 따라서, 이젠 제대로 하는 코딩에 대해서 이야기 할 때라고 생각하며, 그런 부족한 부분을 메워줄 수 있는

것들을 이곳에서 찾아보기를 기대한다.

- 좋은 코드와 그렇지 못한 코드, 작은 차이가 큰 변화를 만든다. -

34 · [ 소프트웨어 개발자 이야기 ]