icon
9장 : 객체 지향 프로그래밍 기초

클래스와 객체의 개념

C++의 핵심이자 가장 강력한 특징인 객체 지향 프로그래밍(Object-Oriented Programming, OOP) 의 세계로 들어설 차례입니다.

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

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

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


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

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

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

예시

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

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

OOP의 주요 특징 (간략히)

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

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


클래스 (Class)의 개념

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

클래스 자체는 객체가 아니며, 메모리에 직접 공간을 차지하지도 않습니다.

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

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

  1. 멤버 변수 (Member Variables / Attributes / Data Members): 객체가 가질 속성(상태)을 정의합니다. 일반 변수와 동일하게 데이터 타입을 가집니다.
  2. 멤버 함수 (Member Functions / Methods / Behaviors): 객체가 수행할 수 있는 행동(기능)을 정의합니다. 일반 함수와 유사하지만, 클래스 내부에 선언됩니다.
클래스 선언 형식
class 클래스이름 {
public: // 접근 지정자 (Public Access Specifier)
    // 멤버 변수 (속성)
    타입 변수1;
    타입 변수2;

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

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

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

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

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

    // 멤버 함수 (행동)
    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; // 연료량은 외부에 직접 노출하지 않음
}; // 클래스 선언 끝에는 반드시 세미콜론!

객체 (Object)의 개념

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

즉, 클래스의 정의에 따라 생성된 구체적인 개별 사물입니다. 붕어빵 틀(클래스)로 만들어진 각각의 붕어빵이 객체입니다.

객체는 클래스에 정의된 멤버 변수들을 각각의 고유한 값으로 가질 수 있으며, 멤버 함수들을 호출하여 자신의 상태를 변경하거나 특정 작업을 수행할 수 있습니다.

객체 생성 형식
클래스이름 객체이름; // 스택에 객체 생성
클래스이름* 포인터객체 = new 클래스이름(); // 힙에 동적으로 객체 생성 (포인터로 접근)
Car 클래스로 객체 생성 및 사용
#include <iostream>
#include <string>

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

    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 클래스로 myCar 객체 생성 (스택에 생성)
    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)와 접근 지정자

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

또한, 클래스 외부에서 데이터에 직접 접근하는 것을 막고, 멤버 함수를 통해서만 데이터를 조작하도록 하여 정보 은닉(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;
        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 함수를 통해서만 안전하게 변경되도록 제어할 수 있습니다.

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