Skip to content

00_05.ModulesAndInfoHiding

jdeokkim edited this page Apr 26, 2022 · 5 revisions

모듈과 인터페이스

'다중 소스 파일' 페이지에서 우리는 프로그램을 설계할 때 여러 개의 소스 파일로 나누어서 작성하는 것이 바람직하다는 것을 배웠다. 이제 프로그램을 어떻게 여러 개의 소스 파일로 나눌 것인지를 생각해보자. 우리가 프로그램을 여러 개의 소스 파일로 나누었을 때, 우리는 "프로그램이 여러 개의 모듈 (module)로 이루어져 있다"라고 한다. 여기서 모듈은 프로그램의 다른 부분이 자유롭게 사용할 수 있는 기능 (함수)의 집합을 의미하는 단어로, 모듈의 모든 기능이 정의 (definition)되어 있는 소스 파일을 모듈의 구현부 (implementation)라고 한다.

우리가 프로그램의 다른 부분에서 모듈을 사용하려면, 그 모듈이 어떤 기능을 제공하는지를 알아야 한다. 이때, 모듈의 구현부 없이 모듈이 제공하는 모든 기능을 나타내는 개념을 우리는 인터페이스 (interface)라고 한다. C언어에서 보통 인터페이스라고 하면, 모듈이 제공하는 함수의 원형 (prototype)이 선언 (declaration)된 헤더 파일을 의미한다.

모듈과 인터페이스라는 개념을 이해하기 위해, 예시를 하나 살펴보자. 아래 내용은 GNU/Linux 운영 체제를 위해 구현된 C 표준 라이브러리 (GNU C 라이브러리)<stdio.h> 파일이다.

GNU C 라이브러리의 <stdio.h> 파일

그러면 이제 한번 생각해보자. 우리가 지금까지 printf() 함수를 쓸 때, printf() 함수가 어떻게 구현되어 있는지 생각해본 적이 있을까? 아마 아무 생각 없이 #include <stdio.h>를 소스 파일의 맨 윗줄에 적고, "printf() 함수는 문자열을 형식에 맞추어 출력해주는 함수구나"라는 것만을 떠올리며 printf() 함수를 사용해왔을 것이다. 바로 이 <stdio.h> 헤더 파일이 뭐다? printf(), scanf() 등의 함수가 구현된 코드 없이, 함수의 원형과 이 함수가 무슨 일을 하는지만을 제공하는 인터페이스다!

그렇다면 printf() 함수가 구현된 부분은 도대체 어디에 있을까? 바로 printf.c라는 소스 파일에 있다.

GNU C 라이브러리의 printf.c 파일

이제 프로그램을 왜 여러 개의 모듈로 나누어야 하는지를 알아보자.

  1. 추상화 (abstraction): 우리가 모듈을 사용할 때는 모듈이 어떻게 구현되어 있는지를 알 필요가 없이, 그냥 우리가 필요한 함수의 원형을 찾고 그 함수를 그대로 가져다쓰면 되는데 (printf() 함수를 생각해보자!), 이러한 특성을 추상화라고 한다.

  2. 재사용성 (reusability): 모듈을 설계할 때는 주로 비슷한 기능을 수행하는 함수끼리 같은 모듈에 모아놓기 때문에, 우리가 사용하는 모듈을 다른 프로그램을 만들 때도 소스 파일과 헤더 파일만 복사해서 그대로 사용할 수 있는데, 이러한 특성을 재사용성이라고 한다.

  3. 유지 보수성 (maintainability): 프로그램을 여러 개의 모듈로 나누면, '다중 소스 파일' 페이지에서 배웠던 것처럼 버그를 수정하거나 프로그램에 새로운 기능을 추가하기 쉬우며, 컴파일 시간도 단축시킬 수 있다.

응집도와 결합도

모듈과 인터페이스가 무엇인지 알았으니, 프로그램을 어떻게 여러 개의 모듈로 나눠야 하는지를 알아보자. 프로그램을 여러 개의 모듈로 나눌 때에는 반드시 각 모듈이 아래 조건을 모두 만족해야 한다.

  1. 높은 응집성 (high cohesion): 모듈은 반드시 비슷한 기능을 수행하는 함수만으로 이루어져야 한다. '다중 소스 파일' 페이지의 게임을 예시로 들면, collision.c 모듈에는 캐릭터와 벽 사이의 충돌을 처리하는 함수만이 있어야 하고, menu.c 모듈에는 게임 메뉴와 관련된 함수만 있어야 한다.

  2. 낮은 결합도 (low coupling): 모듈은 반드시 프로그램의 다른 모듈에 최대한 의지하지 않고 어디서나 독립적으로 사용할 수 있어야 한다. 이게 무슨 뜻이냐면, 우리가 만든 menu.c 모듈로 퍼즐 게임이나 레이싱 게임을 만들 때 menu.h 파일과 menu.c 파일만 복사해서 게임 메뉴와 관련된 기능을 그대로 사용할 수 있어야 한다는 뜻이다.

모듈의 종류

  1. 데이터 풀 (data pool): 데이터 풀은 프로그램에서 사용되는 변수 (variable)나 상수 (constant)만이 정의되어 있는 모듈을 의미하며, <float.h><limits.h> 등의 헤더 파일이 여기에 해당된다.

GNU C 라이브러리의 <limits.h> 파일

  1. 라이브러리 (library): 라이브러리는 서로 관련이 있는 함수가 모여있는 모듈을 의미하며, <string.h>와 문자열 관련 함수가 구현된 여러 소스 파일이 여기에 해당한다. 이해하기 쉽게 예시를 들면, 라이브러리를 사용하는 우리 같은 프로그래머를 '도서관 이용자'라고 하고, 라이브러리는 말 그대로 '도서관', 모듈은 철학, 문화, 예술 등의 '도서 분류 섹션', 인터페이스는 '책의 제목', 마지막으로 구현부는 '책 내용'이라고 생각해보자. 한 문장으로 풀어 설명해보면 도서관 이용자 (프로그래머)가 도서관 (라이브러리)에 들어가, 자신이 읽고 싶은 책의 도서 분류 섹션 (모듈)에서 책 제목 (인터페이스)과 표지의 설명을 읽고 그 책을 대출하는 것으로 생각하면 된다. 이때 도서관 이용자는 책의 내용 (구현부)을 따로 확인하지 않는다.

GNU C 라이브러리의 <string.h> 파일

  1. 추상 자료형 (abstract data type, ADT): 추상 자료형은 내부 구조를 알 수 없는 자료형을 의미하며, 주로 구조체로 구현된다. 사용자는 인터페이스 (헤더 파일)을 통해 이 추상 자료형으로 무슨 작업을 할 수 있는지는 알 수 있지만, 추상 자료형이 실제로 어떻게 생겼는지는 알 수 없다. 아래 예시를 보면 알 수 있듯이, 헤더 파일에는 Stack의 구조체 정의가 없기 때문에, 이 모듈을 사용하는 사람이 함부로 Stack의 내부 변수를 수정할 수 없다.

Stack 추상 자료형의 인터페이스인 stack.h 파일

그렇다면 Stack 구조체는 어디서 정의되는 것일까? 바로 stack.c라는 소스 파일에서 정의된다.

/* 스택을 나타내는 구조체. */
struct Stack {
    unsigned char *arr; // 스택의 원소가 저장될 배열.
    size_t elem_size;   // 스택에 저장할 각 원소의 크기.
    size_t capacity;    // 스택에 저장할 수 있는 원소의 최대 개수.
    size_t length;      // 스택에 저장된 원소의 현재 개수.
};

/* 각 원소의 크기가 `elem_size`인 스택을 생성한다. */
Stack *stack_create(size_t elem_size) {
    Stack *result = calloc(1, sizeof(Stack));
    
    result->capacity = 8;
    result->elem_size = elem_size;

    result->arr = calloc(result->capacity, result->elem_size);
    
    return result;
}

/* ... */

정보 은닉

stack.hstack.c 파일에서 보았듯이, 모듈을 설계할 때는 모듈의 사용자가 함부로 접근하지 못하도록 일부 정보를 숨기는 것이 필수적인데, 이것을 정보 은닉 (information hiding)이라고 한다. 그렇다면 정보 은닉이라는 개념이 도대체 왜 중요한 것일까? 그냥 stack.h 헤더 파일에 구조체의 정의를 넣고 구조체의 멤버 변수에 직접 접근하면 안되는 것일까?

  1. 보안성 (security): Stack 구조체의 정의를 그냥 stack.h 헤더 파일에 넣어두면, 모듈의 사용자는 Stack의 모든 멤버 변수 (arr, elem_size, capacity, length)에 자유롭게 접근할 수 있게 된다. 이게 무슨 뜻이냐면, 우리가 만든 Stack 관련 함수를 사용하지 않고 arrelem_size를 우리의 의도와는 관계없이 마음대로 수정하고, 프로그램 오류를 발생시킬 수 있다는 뜻이다. 만약 Stack 구조체의 정의가 소스 파일에만 있다면, 모듈의 사용자는 우리가 직접 만들고 테스트한 함수만을 통해 멤버 변수의 값에 접근할 수 있게 되고, 프로그램 오류가 발생할 가능성이 매우 낮아지게 된다.

  2. 유연성 (flexibility): Stack 구조체의 정의가 소스 파일에만 있으면, 우리가 나중에 Stack 구조체의 멤버 변수의 이름이나 자료형을 변경할 일이 있을 때 인터페이스 (헤더 파일) 부분은 수정하지 않고 소스 파일만 수정하면 된다.


흠... 뭔가 이렇게 보니까... 객체 지향 프로그래밍 언어를 공부하는 것 같기도 하고...

Clone this wiki locally