[전문가를 위한 C] Chapter 7. 합성과 집합

YUSHIN KIM·2026년 9월 3일

전문가를 위한 C

목록 보기
7/13

Chapter 7. 합성과 집합

7.1 클래스 간의 관계

두 클래스 사이에 존재할 수 있는 관계는 크게 to-have, to-be 두 가지로 분류될 수 있다. to-have 관계에는 합성(composition), 집합(aggregation)이 있고, to-be 관계는 상속(inheritance)이라 한다.


7.2 객체 대 클래스

공개 속성을 갖는 Person 클래스를 C, C++에서 정의한다면 다음과 같은 형태이다.

코드 박스 7-1 C에서의 Person 속성 구조체

typedef struct {
	char name[32];
    char surname[32];
    unsigned int age;
} person_t;

코드 박스 702 C++에서의 Person 클래스의 클래스

class Person {
public:
	std::string name;
    std::string family;
    uint32_t age;
}

클래스 기반 객체 생성 접근법에서 객체를 생성할 때는 우선 메모리가 할당되어 속성 값에 대한 자리 표시자(placeholder)가 된다. 이후 속성 값이 초기화된다. 이를 종합적으로 생성(construction)이라 하며, 생성 단계를 수행하도록 지정된 함수는 생성자(constructor)라고 한다.

객체 사용이 끝나면 할당된 리소스를 해제해야 하며, 해제(destruction) 작업을 수행하도록 지정된 함수를 소멸자(destructor)라고 한다.

클래스는 객체에 대한 청사진으로서 컴파일 시점까지만 존재하고, 실행 시점에는 메모리에 인스턴스화된 객체가 존재한다.


7.3 합성

합성(composition)이란 한 객체가 다른 객체를 포함하거나 소유하는 관계, 반대로 생각하면 한 객체가 다른 객체로 구성되는 관계이다. 합성 관계에서 컨테이너에 포함된 객체의 수명은 컨테이너 객체의 수명에 바인딩된다.

코드 박스 703 [예제 7-1] Car 클래스의 공용 인터페이스

#ifndef EXTREME_C_EXAMPLES_CHAPTER_7_1_CAR_H
#define EXTREME_C_EXAMPLES_CHAPTER_7_1_CAR_H

struct car_t;

// memory allocator
struct car_t* car_new();

// constructor
void car_constructor(struct car_t*);

// destructor
void car_destructor(struct car_t*);

// behavior function
void car_start(struct car_t*);
void car_stop(struct car_t*);
double car_get_engine_temperature(struct car_t*);

#endif

car_t와 같이 속성 구조체의 필드를 정의하지 않고 선언만 하는 방식을 전방 선언(forward declaration)이라 한다. 이는 불완전한 형식(incomplete type)으로 간주된다.

코드 박스 7-4 [예제 7-1] Engine 클래스의 공용 인터페이스

#ifndef EXTREME_C_EXAMPLES_CHAPTER_7_1_ENGINE_H
#define EXTREME_C_EXAMPLES_CHAPTER_7_1_ENGINE_H

struct engine_t;

// memory allocator
struct engine_t* engine_new();

// constructor
void engine_constructor(struct engine_t*);

// destructor
void engine_destructor(struct engine_t*);

// behavior function
void engine_turn_on(struct engine_t*);
void engine_turn_off(struct engine_t*);
double engine_get_temperature(struct engine_t*);

#endif

코드 박스 7-5 [예제 7-1] Car 클래스에 대한 정의

#include <stdlib.h>

// Car can only serve as a public interface for Engine
#include "7_1_engine.h"

typedef struct {
    // composition relationship
    struct engine_t* engine;
} car_t;

car_t* car_new() {
    return (car_t*) malloc(sizeof(car_t));
}

void car_constructor(car_t* car) {
    // allocate memory for an engine object
    car->engine = engine_new();

    // construct the engine object
    engine_constructor(car->engine);
}

void car_destructor(car_t* car) {
    // destruct the engine object
    engine_destructor(car->engine);

    // free the memory space allocated for the engine object
    free(car->engine);
}

void car_start(car_t* car) {
    engine_turn_on(car->engine);
}

void car_stop(car_t* car) {
    engine_turn_off(car->engine);
}

double car_get_engine_temperature(car_t* car) {
    return engine_get_temperature(car->engine);
}

car_t 속성 구조체에 정의된 struct engine_t* 타입의 새로운 속성으로 인해 합성 관계가 발생한다. 비록 struct engine_t* 타입은 소스 파일에서는 여전히 불완전한 형식으로 간주되지만, 런타임에 완전한 형식을 지시할 수 있다.

합성 관계에서 포함되는 객체의 포인터는 비공개로 은닉되어야 한다. 또한, 포함하는 객체가 포함되는 객체의 메모리 생명주기를 관리해야 한다.

car_get_engine_temperature 함수는 엔진의 temperature 속성에 대한 접근을 허용하는 비공개 구현이다. 자동차에 대한 비공개 구현이 엔진의 공용 인터페이스를 사용한다. 정보 은닉 규칙에 따라, 대부분의 경우 상이한 자료형의 두 객체는 상대의 세부 구현에 대해 몰라야 한다.

코드 박스 7-6 [예제 7-1] Engine 클래스에 대한 정의

#include <stdlib.h>

typedef enum {
    ON,
    OFF
} state_t;

typedef struct {
    state_t state;
    double temperature;
} engine_t;

// memory allocator
engine_t* engine_new() {
    return (engine_t*) malloc(sizeof(engine_t));
}

// constructor
void engine_constructor(engine_t* engine) {
    engine->state = OFF;
    engine->temperature = 15;
}

// destructor
void engine_destructor(engine_t* engine) {
    // no need to do anything
}

// behavior function
void engine_turn_on(engine_t* engine) {
    if (engine->state == ON) {
        return;
    }
    engine->state = ON;
    engine->temperature = 75;
}

void engine_turn_off(engine_t* engine) {
    if (engine->state == OFF) {
        return;
    }
    engine->state = OFF;
    engine->temperature = 15;
}

double engine_get_temperature(engine_t* engine) {
    return engine->temperature;
}

engine 객체는 합성 관계에서 engine 객체를 포함할 외부 객체에 대해 모른다.

코드 박스 7-7 [예제 7-1]의 main 함수

#include <stdio.h>
#include <stdlib.h>

#include "7_1_car.h"

int main(int argc, char** argv) {

    // allocate memory for a car object
    struct car_t* car = car_new();

    // construct the car object
    car_constructor(car);

    printf("Engine temperature before starting the car object: %f\n",
        car_get_engine_temperature(car));
    car_start(car);
    printf("Engine temperature after starting the car object: %f\n",
        car_get_engine_temperature(car));
    car_stop(car);
    printf("Engine temperature after stopping the car object: %f\n",
        car_get_engine_temperature(car));
    
    // destruct the car object
    car_destructor(car);

    // free the memory space allocated for the car object
    free(car);
    return 0;
}

메인 소스 파일은 컨테이너 객체인 car의 공용 인터페이스에만 의존한다. 그러므로 링크 시 메인 소스 파일은 car 객체의 비공개 구현만 필요하다. 하지만 car 객체에 대한 비공개 구현은 engine 인터페이스의 공용 인터페이스에 의존한다. 그러므로 이에 대한 링크 단계에서 engine 객체에 대한 비공개 구현을 제공해야 한다.

셀 박스 7-1 [예제 7-1]의 컴파일, 링크, 실행

$ gcc -c 7_1_engine.c -o engine.o
$ gcc -c 7_1_car.c -o car.o
$ gcc -c 7_1_main.c -o main.o
$ gcc engine.o car.o main.o -o 7_1.out
$ ./7_1.out
Engine temperature before starting the car object: 15.000000
Engine temperature after starting the car object: 75.000000
Engine temperature after stopping the car object: 15.000000

7.4 집합

집합(aggregation)은 컨테이너에 포함된 객체의 수명이 컨테이너 객체의 수명과 독립적이다. 컨테이너에 포함된 객체는 컨테이너 객체보다도 먼저 생성될 수 있다.

코드 박스 7-8 [예제 7-2] Gun 클래스의 공용 인터페이스

#ifndef EXTREME_C_EXAMPLES_CHAPTER_7_2_GUN_H
#define EXTREME_C_EXAMPLES_CHAPTER_7_2_GUN_H

typedef int bool_t;

// forward declaration
struct gun_t;

// memory allocator
struct gun_t* gun_new();

// constructor
void gun_constructor(struct gun_t*, int);

// destructor
void gun_destructor(struct gun_t*);

// behavior function
bool_t gun_has_bullets(struct gun_t*);
void gun_trigger(struct gun_t*);
void gun_refill(struct gun_t*);

#endif

코드 박스 7-9 [예제 7-2] Player 클래스의 공용 인터페이스

#ifndef EXTREME_C_EXAMPLES_CHAPTER_7_2_PLAYER_H
#define EXTREME_C_EXAMPLES_CHAPTER_7_2_PLAYER_H

// forward declaration
struct player_t;
struct gun_t;

// memory allocator
struct player_t* player_new();

// constructor
void player_constructor(struct player_t*, const char*);

// destructor
void player_destrutor(struct player_t*);

// behavior function
void player_pickup_gun(struct player_t*, struct gun_t*);
void player_shoot(struct player_t*);
void player_drop_gun(struct player_t*);

#endif

Player 클래스의 행위 함수가 gun_t 형 인수를 갖기 때문에 이 헤더 파일에서도 전방 선언이 필요하다.

코드 박스 7-10 [예제 7-2] Player 클래스에 대한 정의

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#include "7_2_gun.h"

// attribute structure
typedef struct {
    char* name;
    struct gun_t* gun;
} player_t;

// memory allocator
player_t* player_new() {
    return (player_t*) malloc(sizeof(player_t));
}

// constructor
void player_constructor(player_t* player, const char* name) {
    player->name = (char*) malloc((strlen(name) + 1) * sizeof(char));
    strcpy(player->name, name);
    // set the aggregation pointer as NULL if it cannot be set in the constructor
    player->gun = NULL;
}

// destructor
void player_destructor(player_t* player) {
    free(player->name);
}

// behavior function
void player_pickup_gun(player_t* player, struct gun_t* gun) {
    // from this line, the aggregation relationship begins
    player->gun = gun;
}

void player_shoot(player_t* player) {
    // check if the player picked up a gun
    if (player->gun) {
        gun_trigger(player->gun);
    } else {
        printf("There's no gun!");
        exit(1);
    }
}

void player_drop_gun(player_t* player) {
    // from thie line, the aggregation relationship ends
    // the gun object cannot be destructed as it is not owned by the player object
    player->gun = NULL;
}

집합 포인터가 생성 시 설정되어야 한다면 의무적 집합(mandatory aggregation), NULL로 남아있을 수 있다면 선택적 집합(optional aggregation)이라 한다. 선택적 집합 포인터는 생성자 실행 시, 집합 관계 종료 시 반드시 NULL로 설정해야 한다.

코드 박스 7-11 [예제 7-2] Gun 클래스에 대한 정의

#include <stdlib.h>

typedef int bool_t;

// attribute structure
typedef struct {
    int bullets;
} gun_t;

// memory allocator
gun_t* gun_new() {
    return (gun_t*) malloc(sizeof(gun_t));
}

// constructor
void gun_constructor(gun_t* gun, int initial_bullets) {
    gun->bullets = 0;
    if (initial_bullets > 0) {
        gun->bullets = initial_bullets;
    }
}

// destructor
void gun_destructor(gun_t* gun) {
    // no need to do anything
}

// behavior function
bool_t gun_has_bullets(gun_t* gun) {
    return (gun->bullets > 0);
}

void gun_trigger(gun_t* gun) {
    --gun->bullets;
}

void gun_refill(gun_t* gun) {
    gun->bullets = 7;
}

코드 박스 7-12 [예제 7-2]의 main 함수

#include <stdio.h>
#include <stdlib.h>

#include "7_2_player.h"
#include "7_2_gun.h"

int main(int argc, char** argv) {
    // create and construct a gun object
    struct gun_t* gun = gun_new();
    gun_constructor(gun, 3);

    // create and construct a player object
    struct player_t* player = player_new();
    player_constructor(player, "Billy");

    // start the aggregate relationship
    player_pickup_gun(player, gun);

    // shoot until bullets run out
    while (gun_has_bullets(gun)) {
        player_shoot(player);
    }

    // reload the gun
    gun_refill(gun);

    // shoot until the bullets run out
    while (gun_has_bullets(gun)) {
        plyaer_shoot(player);
    }

    // end the aggregate relationship
    player_drop_gun(player);

    // destruct and free the player object
    player_destructor(player);
    free(player);

    // destruct and free the gun object
    gun_destructor(gun);
    free(gun);

    return 0;
}

gun, player 객체는 서로 독립적이기 때문에 main 함수가 두 객체의 생명주기를 관리한다.

셀 박스 7-2 [예제 7-2]의 컴파일, 링크, 실행

$ gcc -c 7_2_gun.c -o gun.o
$ gcc -c 7_2_player.c -o player.o
$ gcc -c 7_2_main.c -o main.o
$ gcc gun.o player.o main.o -o 7_2.out
$ ./7_2.out

합성 관계는 두 객체가 영구적으로 결합되어 있지만, 집합 관계는 기본적으로 분리되어 있으며 일시적으로 결합된다. 소유(possession) 관점에서 집합 관계는 합성 관계보다 약한 관계이다.

profile
안녕하세요

0개의 댓글