본문으로 건너뛰기

안동민 개발노트

본문 시작

클래스와 객체의 개념

현실의 상태와 행동을 클래스와 객체로 모델링하는 원리를 이해하고 캡슐화 중심의 객체 지향 사고를 익힙니다.

이번 장부터는 C++의 핵심 특징 중 하나인 객체 지향 프로그래밍(Object-Oriented Programming, OOP)을 다룹니다.

객체 지향 프로그래밍은 현실 세계의 사물이나 개념을 컴퓨터 프로그램 안으로 가져와 모델링하는 방식입니다.

이를 통해 복잡한 시스템을 더 효과적으로 설계하고 관리할 수 있게 됩니다.

객체 지향 프로그래밍의 가장 기본적인 구성 요소가 바로 클래스(Class)객체(Object)입니다.

Car 클래스 정의가 공통 타입 계약을 제공하고, 그 정의로 생성된 myCar와 yourCar가 서로 다른 비정적 데이터 멤버 저장 공간을 가지는 클래스와 객체 관계

C++ · class · object tree

클래스는 공통 타입을 정의하고, 객체는 각자의 상태를 저장한다

class Car는 멤버의 이름·형식·동작을 정의하는 타입 계약입니다. 객체를 만들면 비정적 데이터 멤버의 저장 공간은 객체마다 따로 생기고, 멤버 함수 호출은 그 대상 객체의 상태에 작용합니다.

Car 타입 정의와 두 객체의 독립 상태 Car 클래스 정의 하나에서 myCar와 yourCar가 생성된다. 두 객체는 같은 멤버 이름과 함수 인터페이스를 따르지만 model, color, speed 같은 비정적 데이터 멤버의 값과 저장 공간은 서로 독립적이다. TYPE CONTRACT class Car 타입 정의 non-static data · model · color · speed functions · accelerate() · brake() · displayInfo() OBJECT STORAGE myCar model="Sonata" · color="White" speed=30 · independent state OBJECT STORAGE yourCar model="K5" · color="Black" speed=70 · independent state
class Car 하나의 타입 정의가 공통 인터페이스를 정합니다.
myCar

Sonata · White · speed 30

myCar의 비정적 데이터 멤버를 위한 독립 저장 공간입니다.

yourCar

K5 · Black · speed 70

yourCar의 비정적 데이터 멤버를 위한 별도 저장 공간입니다.

per object

비정적 데이터 멤버는 객체마다 저장된다

myCar.speed를 바꿔도 yourCar.speed는 바뀌지 않습니다. 어느 인스턴스를 대상으로 호출했는지가 상태 변경의 기준입니다.

class level

함수 코드와 공유 static 상태는 객체별 복사본이 아니다

멤버 함수의 정의는 공통 동작이며 호출할 때 대상 객체를 사용합니다. 정의된 non-thread_local static 데이터 멤버는 모든 객체가 공유하는 한 사본입니다. 일반 non-inline 멤버는 클래스 밖 정의가 필요하고, C++17 inline static은 클래스 안 선언이 정의를 겸합니다. thread_local static은 스레드별 한 사본입니다.

클래스 정의는 객체의 모양과 사용할 수 있는 동작을 정합니다. 실제 인스턴스마다 생기는 것은 비정적 데이터 멤버의 저장 공간이며, 동일한 함수 인터페이스를 통해 각 객체의 상태를 독립적으로 다룹니다.


객체 지향 프로그래밍 (OOP)이란?

객체 지향 프로그래밍은 프로그램을 만들 때 기능보다는 객체를 중심으로 생각하는 프로그래밍 패러다임입니다.

현실 세계의 모든 것은 속성(상태)과 행동(기능)을 가지고 있는 객체로 볼 수 있습니다.

예시
  • 자동차 객체
    • 속성: 색상, 모델, 속도, 연료량, 문 개수 등
    • 행동: 가속한다, 감속한다, 정지한다, 방향을 바꾼다, 시동을 켠다 등
  • 사람 객체
    • 속성: 이름, 나이, 키, 몸무게 등
    • 행동: 걷는다, 말한다, 먹는다, 잠잔다 등

OOP는 이러한 객체들을 프로그램의 기본 단위로 삼아, 객체들이 서로 상호작용하면서 전체 프로그램이 동작하도록 설계합니다.

OOP의 주요 특징 (간략히)
  • 캡슐화 (Encapsulation): 데이터와 데이터를 다루는 함수를 하나의 단위로 묶는 것. (이번 장에서 학습)
  • 추상화 (Abstraction): 복잡한 내부 구현은 숨기고, 필요한 기능만 외부에 노출하는 것.
  • 상속 (Inheritance): 기존 클래스의 속성과 기능을 물려받아 새로운 클래스를 만드는 것.
  • 다형성 (Polymorphism): 동일한 이름의 기능이 상황에 따라 다르게 동작하는 것.

이러한 특징들은 OOP를 통해 재사용성, 유지보수성, 확장성이 높은 코드를 작성할 수 있게 해줍니다.


클래스 (Class)의 개념

클래스는 객체를 생성하기 위한 설계도 또는 틀(blueprint)입니다.

클래스 정의 자체는 객체 인스턴스가 아니며, 정의만 작성했다고 비정적 데이터 멤버를 위한 객체별 저장 공간이 생기지는 않습니다.

실제 객체를 생성할 때 각 객체의 비정적 데이터 멤버를 위한 저장 공간이 마련됩니다. 정의된 non-thread_local static 데이터 멤버는 모든 객체가 공유하는 한 사본입니다. 일반적인 non-inline static 데이터 멤버는 클래스 안의 선언과 별도로 클래스 밖 정의가 필요하지만, C++17의 inline static은 클래스 안 선언이 정의를 겸합니다. thread_local static 데이터 멤버는 객체별이 아니라 스레드별로 한 사본이 생깁니다.

마치 붕어빵을 만들기 위한 붕어빵 틀이나, 건물을 짓기 위한 설계도와 같습니다.

클래스는 다음 두 가지 주요 구성 요소로 이루어집니다.

멤버 변수 (Member Variables / Attributes / Data Members): 객체가 가질 속성(상태)을 정의합니다.

일반 변수와 동일하게 데이터 타입을 가집니다.

멤버 함수 (Member Functions / Methods / Behaviors): 객체가 수행할 수 있는 행동(기능)을 정의합니다.

일반 함수와 유사하지만, 클래스 내부에 선언됩니다.

클래스 선언 형식
class 클래스이름 {
public: // 접근 지정자 (Public Access Specifier)
    // 멤버 변수 (속성)
    타입 변수1;
    타입 변수2;

    // 멤버 함수 (행동)
    반환타입 함수1(매개변수);
    반환타입 함수2(매개변수);

private: // 접근 지정자 (Private Access Specifier)
    // 클래스 내부에서만 접근 가능한 멤버
    타입 비밀변수;
};
  • class 키워드로 시작합니다.
  • 클래스 이름은 일반적으로 대문자로 시작하는 파스칼 표기법(PascalCase)을 사용합니다.
  • 중괄호 {} 안에 멤버 변수와 멤버 함수를 정의합니다.
  • 클래스 선언 끝에는 반드시 세미콜론 ;을 붙여야 합니다.

접근 지정자 (Access Specifiers): 클래스의 멤버(변수, 함수)는 외부에서 접근할 수 있는 권한을 지정할 수 있습니다.

  • public: 클래스 외부 어디에서든 접근 가능합니다. 객체를 통해 이 멤버들을 사용할 수 있습니다.
  • private: 해당 클래스의 멤버와 friend로 허용된 코드에서 접근할 수 있습니다. 일반적인 외부 코드는 직접 접근할 수 없습니다. (정보 은닉, 캡슐화의 핵심)
  • protected: 상속 관계에서 사용됩니다. (나중에 학습)
Car 클래스 선언
#include <iostream>
#include <string> // std::string을 위해

// Car 클래스 선언
class Car {
public: // 공용으로 접근 가능한 멤버들
    // 멤버 변수 (속성)
    std::string model;
    std::string color;
    int speed = 0;

    // 멤버 함수 (행동)
    void accelerate(int amount) { // 속도를 amount만큼 증가시키는 함수
        speed += amount;
        // speed = speed + amount; 와 동일
    }

    void brake(int amount) { // 속도를 amount만큼 감소시키는 함수
        speed -= amount;
        if (speed < 0) { // 속도가 음수가 되지 않도록 방지
            speed = 0;
        }
    }

    void displayInfo() { // 자동차 정보를 출력하는 함수
        // 멤버 함수는 클래스 내의 다른 멤버 변수에 직접 접근 가능
        std::cout << "모델: " << model << ", 색상: " << color << ", 현재 속도: " << speed << "km/h" << std::endl;
    }

private: // 비공개 멤버 (외부에서 직접 접근 불가)
    // 이 멤버는 Car 클래스 내부에서만 접근 가능
    int fuelLevel = 0; // 연료량은 외부에 직접 노출하지 않음
}; // 클래스 선언 끝에는 반드시 세미콜론!

객체 (Object)의 개념

객체는 클래스라는 설계도를 바탕으로 메모리에 실제로 생성된 실체(instance)입니다.

즉, 클래스의 정의에 따라 생성된 구체적인 개별 사물입니다.

붕어빵 틀(클래스)로 만들어진 각각의 붕어빵이 객체입니다.

객체는 클래스에 정의된 비정적 데이터 멤버를 각각의 고유한 값으로 가질 수 있으며, 클래스에 정의된 멤버 함수를 호출하여 자신의 상태를 변경하거나 특정 작업을 수행할 수 있습니다. 비정적 멤버 함수의 코드가 객체마다 별도로 복제된다는 뜻은 아닙니다.

객체 생성 형식
#include <memory>

class Car {};

int main() {
    Car myCar; // 블록 범위 지역 객체: automatic storage duration
    auto ownedCar = std::make_unique<Car>(); // 동적 객체를 unique_ptr가 RAII로 소유
}

myCar의 수명은 블록을 벗어날 때 자동으로 끝납니다. ownedCar가 소유한 동적 객체도 std::unique_ptr의 소멸과 함께 해제되므로, 소유권을 나타내지 못하는 raw new/delete 쌍을 직접 관리하지 않습니다.

Car 클래스로 객체 생성 및 사용
#include <iostream>
#include <string>

// Car 클래스 선언 (위 예시와 동일)
class Car {
public:
    std::string model;
    std::string color;
    int speed = 0;

    void accelerate(int amount) {
        speed += amount;
    }
    void brake(int amount) {
        speed -= amount;
        if (speed < 0) {
            speed = 0;
        }
    }
    void displayInfo() {
        std::cout << "모델: " << model << ", 색상: " << color << ", 현재 속도: " << speed << "km/h" << std::endl;
    }
    // private: int fuelLevel; // 간단한 예시를 위해 생략
};

int main() {
    // 1. 블록 범위의 Car 지역 객체 생성 (automatic storage duration)
    Car myCar; // 객체 생성

    // 2. 객체의 멤버 변수에 접근 (점(.) 연산자 사용)
    myCar.model = "Sonata";
    myCar.color = "White";
    myCar.speed = 0; // 초기 속도 설정

    // 3. 객체의 멤버 함수 호출 (점(.) 연산자 사용)
    myCar.displayInfo(); // 출력: 모델: Sonata, 색상: White, 현재 속도: 0km/h

    myCar.accelerate(50); // 속도 50 증가
    myCar.displayInfo(); // 출력: 모델: Sonata, 색상: White, 현재 속도: 50km/h

    myCar.brake(20); // 속도 20 감소
    myCar.displayInfo(); // 출력: 모델: Sonata, 색상: White, 현재 속도: 30km/h

    // 4. 또 다른 Car 객체 생성
    Car yourCar;
    yourCar.model = "K5";
    yourCar.color = "Black";
    yourCar.speed = 0;
    yourCar.accelerate(70);
    yourCar.displayInfo(); // 출력: 모델: K5, 색상: Black, 현재 속도: 70km/h

    // 각 객체는 자신만의 멤버 변수 값을 가집니다.
    myCar.displayInfo(); // 출력: 모델: Sonata, 색상: White, 현재 속도: 30km/h (여전히 myCar의 속도)

    return 0;
}

위 예시에서 myCaryourCar는 모두 Car 클래스의 객체입니다.

이들은 동일한 model, color, speed라는 멤버 변수를 가지고 있지만, 각 객체는 자신만의 고유한 model, color, speed 값을 가집니다.

또한, 동일한 accelerate(), brake(), displayInfo() 인터페이스를 호출하되, 각 호출은 대상 객체의 비정적 상태를 조작하거나 출력합니다.


캡슐화 (Encapsulation)와 접근 지정자

클래스와 객체의 개념은 객체 경계, 접근 권한, 생성·소멸 책임을 기준으로 확인합니다.

일반 외부 코드가 public setAge를 호출하면 범위 검증 뒤에만 private age가 바뀌고, private 멤버 직접 쓰기는 컴파일 단계에서 차단되는 캡슐화 흐름도

C++ · encapsulation · checked flowchart

외부 요청은 공개 API와 검증을 통과한 뒤에만 내부 상태를 바꾼다

일반 외부 코드는 private age를 직접 쓸 수 없습니다. 이 예제에서는 공개된 setAge()가 값의 범위를 검사하고, 유효할 때만 객체 내부 상태를 변경합니다.

public API를 통한 private 상태 변경 흐름 외부 코드가 setAge를 호출하면 0부터 120까지의 범위를 검사한다. 유효한 값만 private age에 저장되고 잘못된 값은 이전 상태를 유지한다. 외부에서 age에 직접 대입하려는 코드는 접근 제어로 컴파일되지 않는다. DIRECT YES NO 일반 외부 코드 Student user s.age = -5 private · compile error PUBLIC API s.setAge(newAge) 나이 범위가 유효한가? 0 <= newAge && newAge <= 120 요청 거부 previous age remains unchanged PRIVATE STATE age = newAge 일관된 Student 상태 validated age
public API 공개 메서드가 상태 변경의 공식 경로입니다.
  1. s.setAge(newAge) 호출

    일반 외부 코드는 공개된 멤버 함수를 통해 변경을 요청합니다.

  2. 0 <= newAge && newAge <= 120 검사

    범위 검사는 접근 지정자가 아니라 setAge()의 실행 로직입니다.

YES · valid

private age 변경

클래스 멤버 함수는 자신의 객체에 속한 비공개 상태에 접근하여 age = newAge를 수행합니다.

NO · invalid

이전 값 유지

오류를 알리고 대입하지 않으므로 객체 상태는 바뀌지 않습니다.

blocked

s.age = -5는 컴파일되지 않는다

일반 외부 코드의 직접 쓰기는 private 접근 제어에 의해 차단됩니다.

access control

private는 접근 가능한 코드를 제한한다

클래스의 멤버와 허용된 friend는 접근할 수 있지만, 일반 외부 호출자는 비공개 멤버를 직접 읽거나 쓸 수 없습니다.

domain rule

유효성 검사는 공개 함수의 실행 규칙이다

private만으로 값의 범위가 보장되지는 않습니다. setAge()가 검사와 변경을 한 경로에 모아 불변식을 지킵니다.

  • 공개 API를 통한 진행
  • 검증을 통과한 상태 변경
  • 접근 또는 값이 거부됨

캡슐화는 비공개 데이터를 숨기는 데서 끝나지 않습니다. 외부가 사용할 공개 약속을 작게 만들고, 그 경로에서 검증한 뒤에만 상태를 바꾸어 객체의 일관성을 유지합니다.

OOP의 중요한 원칙 중 하나인 캡슐화(Encapsulation)는 데이터(멤버 변수)와 그 데이터를 조작하는 코드(멤버 함수)를 하나의 단위(클래스)로 묶는 것을 의미합니다.

또한, 보호할 데이터를 private으로 지정해 일반 외부 코드의 직접 접근을 막고, 필요한 public 멤버 함수를 통해 데이터를 조작하게 함으로써 정보 은닉(Information Hiding)을 달성할 수 있습니다.

  • private 접근 지정자는 캡슐화를 구현하는 핵심 도구입니다. 예를 들어, Car 클래스에서 fuelLevelprivate으로 선언하면 일반 외부 코드에서 myCar.fuelLevel = 100;와 같이 직접 접근할 수 없습니다.
  • 데이터를 private으로 만들고, 해당 데이터에 접근하거나 변경하는 public 멤버 함수(이들을 보통 게터(Getter)세터(Setter)라고 부릅니다)를 제공하는 것이 일반적인 캡슐화 방법입니다. 이를 통해 데이터의 무결성을 보호하고, 객체의 상태가 예측 불가능하게 변하는 것을 방지할 수 있습니다.
캡슐화 적용 (Getter/Setter)
#include <iostream>
#include <string>

class Student {
private: // private 멤버 변수
    std::string name;
    int age;

public: // public 멤버 함수 (게터/세터)
    // 생성자 (객체 생성 시 초기화하는 특별한 함수)
    Student(std::string n, int a) : name(n), age(0) {
        setAge(a); // age 설정 시 유효성 검사 수행
    }

    // name에 대한 게터
    std::string getName() {
        return name;
    }

    // age에 대한 게터
    int getAge() {
        return age;
    }

    // age에 대한 세터 (유효성 검사 포함)
    void setAge(int newAge) {
        if (newAge >= 0 && newAge <= 120) { // 나이 유효성 검사
            age = newAge;
        } else {
            std::cerr << "유효하지 않은 나이: " << newAge << std::endl;
            // 적절한 오류 처리 (예: 기본값 설정, 예외 발생)
        }
    }

    void displayStudentInfo() {
        std::cout << "이름: " << name << ", 나이: " << age << std::endl;
    }
};

int main() {
    Student s1("김철수", 20); // 객체 생성 및 초기화
    s1.displayStudentInfo(); // 출력: 이름: 김철수, 나이: 20

    // s1.age = -5; // 컴파일 오류! age는 private이므로 직접 접근 불가
    s1.setAge(-5); // 세터를 통해 나이 변경 시도 (유효성 검사로 인해 오류 메시지 출력)
    s1.displayStudentInfo(); // 나이는 변경되지 않고 20 유지

    s1.setAge(22); // 유효한 나이로 변경
    s1.displayStudentInfo(); // 출력: 이름: 김철수, 나이: 22

    std::cout << "s1의 이름: " << s1.getName() << std::endl; // 게터를 통해 이름 접근
    std::cout << "s1의 나이: " << s1.getAge() << std::endl;   // 게터를 통해 나이 접근

    return 0;
}

캡슐화를 통해 age 멤버 변수에 잘못된 값이 직접 할당되는 것을 막고, setAge 함수를 통해서만 안전하게 변경되도록 제어할 수 있습니다.

객체 지향 설계는 클래스 내부 상태를 감추는 것에서 끝나지 않고, 외부에 어떤 약속을 공개할지 결정하는 과정입니다.

이것이 바로 객체 지향 설계의 강점 중 하나입니다.

클래스를 설계할 때는 멤버를 많이 넣는 것보다, 어떤 값은 숨기고 어떤 함수만 약속으로 공개할지 판단하는 기준이 중요합니다.