왜 C++과 C는 클래스 개념을 다르게 구현하거나 모사하는가?

by gg582 · 2026-06-25 06:07:45 · 86 views

글에 앞서서

기본적으로, 꽤 많은 프로그래머들은 이미 C를 할 줄 안다. 너무나 당연한 언어로의 지위는 이제 Java에게 넘겨줬지만, 특히 지방 학군의 대학은 저수준에 여전히 많은 시간을 쏟는다. 하지만 많은 경우 C는 기계에 가까우니 절차대로 통파일이나 써 내려간다고 생각한다. 이는 디바이스 드라이버나 간단한 전력 관리 모듈에서 때때로 등장하지만 C언어로 작성된 모던 프로그램을 대표하지는 못한다. 그러나, C++ 흉내를 내는 데에서 그치는 것은 아니다. 여전히 이해를 돕기 위해 C++ 교재 내용을 빌려와야 하지만, C++ 교재의 대표적 발췌 코드는 이러하다.

#include <iostream>

using namespace std;

class Car {
public:
    int num;
    double gas;
    void show();
};

void Car::show() {
    cout << "차량 번호는 " << num << "입니다.\n";
    cout << "연료량은 " << gas << "입니다.\n";
}

// 이후 아래와 같이 사용한다.

int main() {
    Car *pCar;
    pCar = new Car();
    pCar->num = 1234;
    pCar->gas = 20.5;
    pCar->show();
    
    delete pCar;
}

이것은 구조적으로 명료하며 프로그래머의 구현법이 편차 없이 정리될 수 있게 하는 "산업성 코딩에 잘 맞는다." 아무나 차출해서 OOP를 시켜도 그다지 문제되지 않게 하는 것에는 명수이다.

그러나, 언어의 사양은 외울 것이 늘어나고 프로그램의 스타일은 조직의 거시적 목표와 상이할 수 있다. 이러한 상황에서 역설적으로 수많은 기업들은 오히려 더 통제적인 언어를 고른 후 조직의 체질을 바꾸는 선택을 하였으며, 21세기 전반을 C++/Java가 가져가게 되었다. 마치 이 시대의 중반을 뺏겠다고 선언하는 Rust 역시 프로그래머의 체질을 바꾸려 드니, 유행은 거꾸로 조직을 바꾸는 운명을 따르게 되었다.

C언어의 경우 그 사상과 퇴출 논의를 떼놓고 볼 수가 없다. 기본적으로는 패인에 대한 편파 중계이지만 말이다.

첫째로, 모범적 의사 OOP 비슷하게 흉내 내 보자.

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

#define CAR_INIT_VAL -1

struct car {
    int num;
    double gas;
    void(*show)(struct car*);
};

void show(struct car* c) {
    printf("차량 번호는 %d입니다.\n", c->num);
    printf("연료량은 %lf입니다.\n", c->gas);
}

void init(struct car **c) {
    if(*c == NULL) *c = malloc(sizeof(struct car));
    (*c)->num  = CAR_INIT_VAL;
    (*c)->gas  = CAR_INIT_VAL;
    (*c)->show = show;
}

int main() {
    struct car *c = NULL;
    init(&c);
    c->num = 1234;
    c->gas = 20.5;
    c->show(c);

    if(c != NULL) free(c);
    return 0;
}

이중 포인터의 참조가 생겼고, 메모리 해제가 명시적인 대신 위험하여, 당장은 불필요한 Null check도 넣게 된다. 보통 교과서들에서는 main에서 직접 값을 밀어 넣는 패턴을 여전히 살리는데, 이것이 어떻게 가독성과 유지보수에 영향을 미치는지는 이어지는 단락들에서 언급하겠다. 그러나, 적어도 키워드가 중복될지언정 c->show(c);의 경우 확실히 show 함수는 구조체 변수 c를 위해 사용되고 있음이 가시화되며, 외부 문맥에서 실수로 함수를 쓸 일이 적다.

둘째는 전통적인 스타일이다. 아마 많은 사람들은 이것에 익숙할지도 모른다.

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

#define CAR_INIT_VAL 1

struct car {
    int num;
    double gas;
};

void car_show(struct car c) {
    printf("차량 번호는 %d입니다.\n", c.num);
    printf("연료량은 %lf입니다.\n", c.gas);
}

void car_init(struct car *c) {
    c->num = CAR_INIT_VAL;
    c->gas = CAR_INIT_VAL;
}

int main() {
    struct car c;
    car_init(&c);
    c.num = 1234;
    c.gas = 20.5;
    car_show(c);
}

이 경우 앞보다 오히려 포인터 참조 깊이가 낮고, main 안에서 흐름을 추적할 수 있어 아두이노, ESP32 등의 기판에 실습 코드를 작성하는 경우 상당히 도움되며, 작은 코드 베이스의 단순 센서 개발에서는 특정한 우위를 점한다. 그러나, 이러한 코드가 수천 줄-수만 줄 규모로 불어나기 시작하면 개발자는 비대한 함수의 몸체를 하나하나 추적하다가 다시 main으로 돌아와야 하는 상황에 빠진다. 또한 지금은 단순히 자동차만 다루기 때문에 car_show가 읽기 좋으나, 코드 베이스에서 흔한 '매우 비슷한데 다른 타입'들이 등장하면 공식 문서만 무한히 뒤지다가 미궁으로 빠지게 되기도 한다. main에서 멤버에 직접 값을 밀어 넣는 행위가 타입 파편화와 만났을 때 통제력을 잃는 이유가 여기에 있다.

셋째는 오히려 첫째보다도 추상화 강도가 심해진 형태이다. 오픈 소스를 읽어 보면 이러한 형태가 흔하게 보인다.

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

#define CAR_INIT_VAL -1

struct car {
    int num;
    double gas;
    void(*show)(struct car*);
    void(*setval)(struct car **c, int num, double gas);
};

void init(struct car **c);
void setval(struct car **c, int num, double gas);
void show(struct car *c);

void CAR_INIT(struct car **c) {
    if(*c == NULL) *c = malloc(sizeof(struct car));
    (*c)->num  = CAR_INIT_VAL;
    (*c)->gas  = CAR_INIT_VAL;
    (*c)->show = show;
    (*c)->setval = setval;
}

void setval(struct car **c, int num, double gas) {
    (*c)->num = num;
    (*c)->gas = gas;
}

void show(struct car* c) {
    printf("차량 번호는 %d입니다.\n", c->num);
    printf("연료량은 %lf입니다.\n", c->gas);
}

void CAR_FREE(struct car **c) {
    if((*c) != NULL) {
        free(*c);
        *c = NULL;
    }
}

int main() {
    struct car *c = NULL;
    CAR_INIT(&c);
    c->setval(&c, 1234, 20.5);
    c->show(c);
    CAR_FREE(&c);

    return 0;
}

여기서 주목할 점은, CAR_INITCAR_FREE처럼 대문자로 강조해 구획을 만들어 넣는 부분, 그리고 값의 할당조차 구조체의 요소로 초기화하는 기법이다. 앞선 코드들의 직접 대입 패턴이 야기하는 문맥적 불안정을 함수 포인터 인터페이스 안으로 격리하여 해소하는 방식이다.

C++에서의 사고는, 자동차 클래스 Car안의 부속을 선언하고 조립한다는 개념이다. 그러나 C의 의사 OOP 스타일은, 실상은 객체의 생성과 소멸 시점을 가시화하고, 프로그래머가 만든 자료를 보다 편리하게 다루겠다는 실리주의 정도에서 그친다.

이렇게 된 데에는 C언어의 특성이 한몫한다고 본다. 보통 아직도 C++로 교체하지 못한 분야는, 문맥과 흐름을 따라 가면서 전자 장비들이 순서대로 작동해야 하는 분야들이며, 이러한 경우 실행 순서와 가공 맥락을 구체적으로 알 수 있는 편이 OOP보다 유리하다. 물론, 최근에는 C의 그러한 강점을 챙기면서도 OOP나 모던 프로그래밍 개념에서 상당히 영감받은 Rust와 같은 언어가 있다(필자는 이 언어를 꽤 복고적인 측면이 있다고 생각한다). 그러나, 언어의 생존, 그리고 레거시의 생존을 위해서는 어설프게 OOP를 도입하느니 기존의 문맥을 더 견고하게 하기 위한 아토믹, 제네릭 등만 가끔 추가해 주는 방향의 '패치'가 효과적이라고 C언어 표준에 대한 논의가 흘러가는 것으로 보인다. 데니스 리치가 스트링 타입 등 다양한 패러다임의 변화를 추적하자고 제안한 것이, 오히려 다른 위원들에 의해 반려된 것 역시 이러한 맥락이라고 본다.

따라서, C++을 이해할 때처럼 '숙련된 여공의 손놀림'의 연장선으로 정해진 재봉틀을 꿰맨다는 접근법으로 C를 대하면 어렵다. 오히려, C는 여전히 소수 민족에서 쓰이는 원시적인 동물 뼈 대바늘에 가깝다. C에서는 맥락, 그리고 형상의 흐름이 중요하다.

Back

Comments

No comments yet.