들어가며...
모든 언어가 마찬가지이겠지만 특정 어플리케이션 (라이브러리, 프로그램, 모듈 등)을 개발하기 전에 어떻게 개발을 진행할 것인지 설계를 하게 됩니다. 개발을 좋아하는 저같은 경우에도 이 설계단계가 매우 중요함을 알면서도 개발보다는 상대적으로 지루하고 귀찮은 작업이라 등한 시 하게 되거나 우선 기능 구현 후 추후에 프로그램 구조를 잡아가는 과정을 거칠 때도 있습니다.
이는 매우 안좋은 습관입니다. 로직의 변경은 해당 함수나 (또는 클래스) 특정 부분만을 수정하면 그만이지만 프로그램의 구조를 바꾸게 되면 소스 전반에 걸친 수정 작업이 이루어지기 때문에 수정 후 모든 기능을 다시 테스트 해야 하는 일이 발생할 수도 있습니다.
이번 글에서는 설계라는 큰 범위의 얘기까지는 아니지만 C언어로 개발을 할 때 폴더나 소스코드의 구조를 어떻게 가져가는 것이 좋을 지에 대해 살펴보도록 하겠습니다. 물론 이것이 옳다라는 정답은 없습니다. 다만 이런 의견도 있다라는 정도로 이해하시고 좋은 점이 있다면 본인의 것으로 습득하시면 좋을 듯 합니다.
- Table of Contents
- 나쁜 프로그램의 예
- 프로그램 구조의 예
나쁜 프로그램의 예
앞서도 언급하였 듯이 좋은 프로그램 구조의 정답은 없지만 나쁜 프로그램의 예는 있습니다. 아래 예시 중 본인에 해당하는 것은 없는 지 살펴보시고 되도록 이면 아래와 같은 형태로의 개발은 지양하시는 게 좋습니다.
● 하나의 소스파일에 너무 많은 걸 구현한 코드
뒤돌아 보면 대학교 시절 프로그램 과제를 할 때 저 또한 저질렀던 실수 중 하나입니다. (요즘 대학 수업에서는 이러지 않겠지만요) C언어는 main() 함수에서 시작한다는 것은 다들 아시리라 생각됩니다. 이 main함수에 모든 기능을 구현해본 적은 없으셨나요? 이정도는 아니더라도 main함수가 포함된 소스파일에 이런 저런 함수를 모두 구현하는 경우가 있습니다. 또는 나름 기능별로 소스파일은 분리했지만 해당 소스파일의 코드길이가 너무 너무 긴 경우가 있습니다.
이런 것은 모두 지양하는게 좋습니다. 지금 개발은 본인이 하지만 항상 다른 누군가가 내 코드를 볼 일이 생긴다는 것을 염두해 두셔야 합니다.
● 소스파일명, 함수명, 변수명에 규칙이 없는 코드
설계단에서 반드시 정의하고 시작하면 좋은 것인데 특히 여러사람이 같이 하는 프로젝트라면 더더욱 필요한 일입니다. 즉, 소스파일명, 함수명, 변수명에 대한 규칙을 정의하는 일입니다.
//< 통일성 없는 소스파일명
timeUtils.c
common_utils.c
//< 통일성 없는 함수명
int plus(int a, int b);
int cal_minus(int x, int y);
//< 통일성 없는 변수명
void main() {
int my_value = 0;
int yourValue = 0;
}
위 예제를 보시고 어떤 말을 하려는지 알아차리시겠나요?
가장 기본적으로는 소스파일명, 함수명, 변수명의 이름을 지을 때 Camel Case, Snake Case 중 어느 것으로 할 것인지 정하신 후 해당 방식으로 개발을 진행하여 전체적으로 통일성을 유지하는 것이 좋습니다.
추가적으로 C언어에는 Java의 패키지 개념이 없기 때문에 파일명이나 함수명등에는 해당 프로젝트의 특성을 표현할 수 있는 prefix를 공통으로 달아주는 것도 좋습니다.
//< U2ful C Program 이라는 프로젝일 경우 약어로 ucp를 사용
//< 파일명
ucp_utils.c
ucp_main.c
//< 함수명
int ucp_plus(int a, int b);
int ucp_minus(int a, int b);
● 소스파일만 있는 코드
함수의 구현부에 함수의 프로토타입까지 정의하는 경우가 종종 있습니다. Feasibility로 간단하게 동작 확인을 위한 코드 구현 시에는 이러한 방법이 빠르게 구현할 수 있으므로 나쁘지 않겠지만 제대로 된 프로젝트의 개발을 진행할 때는 되도록이면 소스파일과 헤더파일을 구분하여 구조 있게 개발을 진행하는 것이 좋습니다.
#include <stdio.h>
//< 함수의 프로토타입을 동일한 파일에 선언
int plus(int a, int b);
void main() {
int add = plus(10, 2);
printf("plus is %d\n", add);
}
int plus(int a, int b) {
return (a + b);
}
✳︎ 생각해보기
C언어에는 public, private, protect와 같이 외부에서 참조 가능 여부를 지정할 수 있는 예약어가 없습니다. 따라서, 모든 함수의 프로토타입을 헤더파일에 선언하여 사용하는 것이 반드시 좋은 방법이 아닐 수도 있습니다.
Java언어와 같이 외부로 노출하고자 하는 함수는 헤더파일에 정의하고, 외부에 노출하지 하지 않고 Java의 private 함수처럼 사용하고자 할 경우에는 위의 예처럼 ".c"파일에 함수의 프로토타입을 선언하여 사용하는 방법도 나쁘지 않을 것입니다.
● 재활용이 불가능한 코드
해당 프로젝트에서만 사용되는 로직(보통 이러한 경우의 로직을 Policy(정책)라고 합니다.)은 다른 프로젝트에서 재활용될 일이 없겠지만 개발을 하다보면 특별한 함수들은 다른 프로젝트에서도 사용할 일이 생기곤 합니다.
이러한 것을 염두해 두지 않고 구현을 한다면 매번 해당 함수를 가져와 붙여넣기를 하거나 같은 기능의 함수를 또 구현하게 됩니다. 이러한 것을 방지하기 위해 공통으로 사용될 법한 것들은 하나의 라이브러리 처럼 별도의 파일로 분리하여 구현하는 것이 좋습니다.
위와 비슷한 예로 비슷한 기능을 수행하는 두 개 이상의 함수가 있는데 몇가지 로직을 제외하고 나머지 부분들은 모두 동일한 로직의 함수가 있을 수 있습니다. 개발 일정이 촉박하거나 초심자의 경우 동일한 로직을 함수마다 구현하는 경우가 있으나 이 또한 고민을 좀 하여 별도의 함수로 분리해 내면 코드 중복을 방지하고 좀 더 간결한 코드를 작성할 수 있습니다.
● 로직이 중복되는 경우
#include <stdio.h>
int plusA(int a, int b);
int plusB(int a);
int main(void) {
printf("plusA = %d\n", plusA(5, 10));
printf("plusB = %d\n", plusB(10));
}
int plusA(int a, int b) {
return (a + b);
}
int plusB(int a) {
int b = 10;
return (a + b);
}
위의 예를 보면 plusA와 plusB는 동일하게 더하기를 수행하는 함수입니다. 다만 plusB의 경우는 하나의 숫자만을 입력하여 무조건 10을 더한다는 차이점이 있습니다.
너무 간단한 예라 위의 코드는 수정할 이유가 없지만 더하기 로직이 실제로는 좀 더 긴 코드의 로직이라고 가정한다면 위의 방식은 공통되는 로직을 함수 plusA, plusB에 중복되게 구현한 결과가 됩니다. 이를 좀 더 구조적으로 표현한다면 아래와 같은 형태가 좋을 듯 합니다.
● 동일한 로직을 분리
#include <stdio.h>
int plusA(int a, int b);
int plusB(int a);
int plusImpl(int a, int b);
int main(void) {
printf("plusA = %d\n", plusA(5, 10));
printf("plusB = %d\n", plusB(10));
}
int plusA(int a, int b) {
return plusImpl(a, b);
}
int plusB(int a) {
int b = 10;
return plusImpl(a, b);
}
//< 공통 로직을 하나의 함수로 분리
int plusImpl(int a, int b) {
return (a + b);
}
프로그램의 구조 예
좋은 프로그램 구조의 예는 위에 열거한 나쁜 예에서 반대로 구현하면 될 것입니다. 이번 장에서는 폴더구조, 소스파일 및 헤더파일의 템플릿 을 예로 들면서 좋은 프로그램 구조에 대해 살펴보도록 하겠습니다.
● 폴더구조
C언어에는 패키지 개념이 없기 때문에 소스를 기능별로 그룹화하려면 폴더로 분리하는 방법밖에 없습니다. 따라서, 폴더로 그룹을 지어 소스를 분리하는 방법을 사용해야 하는데 방법은 천차만별입니다. 보기좋게 분리하는 것도 좋지만 나중에 컴파일을 하려면 너무 분리하는 것이 복잡할 수 있기 때문에 전체 소스의 양이 많지 않은 프로젝트일 경우에는 다음과 같은 방법도 나쁘지 않을 듯 합니다.
● 헤더파일 템플릿
/**
PROJECT : Sample project
FILENAME : header_template.h
VERSION : 1.0
DATE : 2020-05-XX
*/
#ifndef _HEADER_TEMPLATE_H_
#define _HEADER_TEMPLATE_H_
#ifdef __cplusplus
extern "C" {
#endif
//=========================================================================//
// MODULES USED
//=========================================================================//
#include <stdio.h>
#include <string.h>
//=========================================================================//
// DEFINITIONS AND MACROS
//=========================================================================//
#define MAX_BLA_BLA_NUM (10)
//=========================================================================//
// TYPEDEFS AND STRUCTURES
//=========================================================================//
typedef struct _ST_TIME {
unsigned int year;
unsigned int month;
unsigned int day;
unsigned int hour;
unsigned int minute;
unsigned int second;
} ST_TIME;
//=========================================================================//
//
// EXTERN FUNCTIONS
//
//=========================================================================//
int ups_plus (int a, int b);
int ups_minus (int a, int b);
int ups_multiply (int a, int b);
int ups_divide (int a, int b);
#ifdef __cplusplus
}
#endif
#endif/*_HEADER_TEMPLATE_H_*/
헤더파일에는 기본적으로 외부에서 사용하고 알아야 하는 정보에 대해서 노출시키는 것이 원칙입니다. 기본적으로 include할 외부 헤더파일, #define으로 정의할 상수, 외부에서 알아야 할 구조체 및 함수의 프로토타입 정도를 헤더파일에서 정의한다고 생각하면 될 듯 합니다.
● 소스파일 템플릿
/**
PROJECT : Sample project
FILENAME : header_template.c
VERSION : 1.0
DATE : 2020-05-XX
*/
//=========================================================================//
// MODULES USED
//=========================================================================//
#include "header_template.h"
//=========================================================================//
// LOCAL FUNCTION PROTOTYPE
//=========================================================================//
int ups_plus_impl(int a, int b);
//=========================================================================//
// EXTERN FUNTION
//=========================================================================//
/**
desc : 더하기 연산 수행
param a : 더하기 연산 수행을 위한 인자
param b : 더하기 연산 수행을 위한 인자
return
: 더하기 연산 결과 값 반환
*/
int ups_plus(int a, int b) {
return ups_plus_impl(a, b);
}
/////////////////////////////////////////////////////////////////////////////
//=========================================================================//
// LOCAL FUNTION
//=========================================================================//
/**
desc : 더하기 연산 수행
param a : 더하기 연산 수행을 위한 인자
param b : 더하기 연산 수행을 위한 인자
return
: 더하기 연산 결과 값 반환
*/
int ups_plus_impl(int a, int b) {
return (a + b);
}
자신의 헤더파일에 정의된 함수들을 구현합니다. 만약 해당 기능 내에서만 사용되는 내부 함수가 있을 경우에는 소스파일에 프로토타입과 해당 함수를 구현하는 것도 하나의 방법입니다.
마무리...
지금까지 C언어 개발 시 필요한 프로젝트의 구조에 대해서 알아봤습니다. 이보다 더 좋은 방법은 분명히 있을 것이나 보편적으로 사용되는 방법에 대해서 살펴봤습니다.
간단하다면 간단하다할 수 있는 이러한 구조는 반드시 본인이 고민하셔서 정리 및 정의한 후 개발을 시작하는 습관은 매우 중요합니다. 다른 사람이 볼 때, 구현한 후 오랜 시간이 흐른 후 본인의 코드를 다시 들여다 볼일이 있을 때, 추후 코드를 변경, 추가 할 경우에 도움이 많이 될 것입니다.
U2ful은 ♥입니다. @U2ful Corp.
'Programming > C언어 초급' 카테고리의 다른 글
C언어 초급) 02.변수 : 02. 입출력 (0) | 2020.05.25 |
---|---|
C언어 초급) 02.변수 : 01. 변수의 정의 (0) | 2020.05.22 |
C언어 초급) 01.들어가며 : 03. C언어의 구성요소 (0) | 2019.10.02 |
C언어 초급) 01.들어가며 : 02. "Hello U2ful" (0) | 2019.10.01 |
C언어 초급) 01.들어가며 : 01. 개발환경 구축하기 (0) | 2019.09.30 |