본문 바로가기

클래스와 객체의 개념

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

지금까지는 데이터는 변수에, 그 데이터를 다루는 코드는 함수에 따로 두었습니다. 자동차의 모델, 색상, 속도를 변수 세 개로 두고 가속하는 함수를 따로 만들면, 프로그램이 커질수록 어떤 함수가 어떤 변수를 바꾸는지 따라가기 어려워집니다. 객체 지향 프로그래밍(object-oriented programming)은 함께 다루어야 하는 데이터와 함수를 하나로 묶은 객체를 프로그램의 단위로 삼습니다.

이 장에서는 C++의 객체 지향 기초를 다룹니다. 이 글은 객체를 만드는 틀인 클래스(class)와, 클래스로 만든 실체인 객체(object)의 관계, 그리고 객체의 데이터를 보호하는 캡슐화(encapsulation)를 소개합니다. 클래스를 정의하는 자세한 문법은 다음 글부터 차례로 봅니다.

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

C++ · class · object tree

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

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

공통 Car 타입과 독립된 두 객체원desktop과같이공통classCar에서두인스턴스로분기하는타입관계3노드2간선이다.myCarspeed30/yourCarspeed70을구분하고두객체사이에상태전파나시간간선은없다.현재myCar/yourCar의모델과색을보존한다.class Carmodel · color · speedaccelerate · brakedisplayInfomyCarSonata · Whitespeed30yourCarK5 · Blackspeed70타입 관계이며 실행 순서 아님두 객체의 상태는 독립
Car 타입과 독립된 두 객체 상태Car는 공통 멤버와 동작을 정의하며 myCar와 yourCar의 비정적 데이터 저장 공간은 독립적이다.class Car · 공통 타입 정의데이터: model · color · speed동작: accelerate · brake · displayInfomyCarmodel="Sonata" · color="White"speed=30 · 객체별 상태yourCarmodel="K5" · color="Black"speed=70 · 객체별 상태
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은 스레드별 한 사본입니다.

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

상태와 동작

자동차는 모델, 색상, 현재 속도 같은 상태를 가지고, 가속하거나 멈추는 동작을 합니다. 객체 지향에서는 이런 상태를 데이터로, 동작을 그 데이터를 바꾸는 함수로 표현하고, 둘을 한 단위로 묶습니다. 이렇게 묶으면 속도를 바꾸는 방법이 가속과 감속 함수로 정해지므로, 속도가 음수가 되는 것 같은 잘못된 상태를 막을 곳도 한 군데로 모입니다.

객체 지향의 특징으로는 흔히 네 가지를 듭니다. 데이터와 함수를 묶고 내부를 감추는 캡슐화, 필요한 기능만 드러내는 추상화(abstraction), 기존 클래스를 물려받아 새 클래스를 만드는 상속(inheritance), 같은 호출이 객체에 따라 다르게 동작하는 다형성(polymorphism)입니다. 이 장에서는 캡슐화를 중심으로 다루고, 상속과 다형성은 10장에서 봅니다.

클래스

클래스는 객체가 가질 데이터와 함수를 정의한 타입입니다. 클래스 안의 변수를 멤버 변수(member variable) 또는 데이터 멤버라고 하고, 함수를 멤버 함수(member function)라고 합니다.

car.cpp
#include <iostream>
#include <string>

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 " << model << ", color " << color
                  << ", speed " << speed << '\n';
    }
};

클래스는 class 키워드와 이름으로 시작하고, 중괄호 안에 멤버를 적은 뒤 세미콜론으로 끝납니다. public:은 그 아래의 멤버를 클래스 밖에서도 쓸 수 있게 하는 접근 지정자(access specifier)이며, 접근 지정자에서 자세히 다룹니다. 멤버 함수 안에서는 speed처럼 같은 클래스의 멤버 변수를 이름만으로 쓸 수 있습니다.

클래스를 정의한 것만으로는 model이나 speed를 담을 공간이 생기지 않습니다. 클래스는 int처럼 하나의 타입이며, 그 타입으로 객체를 만들어야 멤버 변수의 공간이 생깁니다.

객체

객체는 클래스를 타입으로 해서 만든 실체이며, 클래스의 인스턴스(instance)라고도 합니다. 객체를 만들면 그 객체만의 멤버 변수 공간이 생기고, 멤버에는 점 연산자 .로 접근합니다.

car_objects.cpp
int main() {
    Car myCar;
    myCar.model = "Sonata";
    myCar.color = "White";
    myCar.displayInfo();

    myCar.accelerate(50);
    myCar.displayInfo();
    myCar.brake(20);
    myCar.displayInfo();

    Car yourCar;
    yourCar.model = "K5";
    yourCar.color = "Black";
    yourCar.accelerate(70);
    yourCar.displayInfo();

    myCar.displayInfo();
}

앞의 Car 정의 아래에 이 main을 이어 붙여 실행하면 다음과 같이 출력됩니다. 앞의 세 줄은 myCar 하나의 속도가 바뀌어 가는 과정이고, 넷째 줄은 새로 만든 yourCar, 마지막 줄은 다시 myCar의 상태입니다.

model Sonata, color White, speed 0
model Sonata, color White, speed 50
model Sonata, color White, speed 30
model K5, color Black, speed 70
model Sonata, color White, speed 30

myCar와 yourCar는 같은 Car 클래스로 만들었지만, 위 그림처럼 멤버 변수를 각자 따로 가집니다. yourCar.accelerate(70)은 yourCar의 speed만 바꾸므로, 마지막 줄에서 myCar의 속도는 여전히 30입니다. 멤버 함수의 코드는 객체마다 복사되지 않고 하나뿐이며, 호출할 때 점 앞의 객체를 대상으로 동작합니다.

speed는 클래스 안에서 = 0으로 기본값을 주었으므로 객체를 만들면 0에서 시작합니다. 객체도 다른 변수처럼 지역 객체는 블록이 끝나면 사라지고, 실행 중에 만들어야 한다면 동적 메모리 할당에서 본 std::make_unique<Car>()로 만들어 std::unique_ptr에 소유를 맡깁니다.

캡슐화

Car의 brake는 속도가 음수가 되지 않게 막지만, speed가 public이므로 myCar.speed = -10;처럼 밖에서 직접 대입하면 이 검사를 건너뜁니다. 캡슐화는 데이터를 private으로 감추고, 데이터를 바꾸는 길을 검사가 들어 있는 public 멤버 함수로만 열어 두는 것입니다. 이렇게 내부를 감추는 것을 정보 은닉(information hiding)이라고도 합니다.

student.cpp
#include <iostream>
#include <string>

class Student {
private:
    std::string name;
    int age = 0;

public:
    Student(std::string n, int a) : name(n) {
        setAge(a);
    }

    std::string getName() { return name; }
    int getAge() { return age; }

    void setAge(int newAge) {
        if (newAge >= 0 && newAge <= 120) {
            age = newAge;
        } else {
            std::cerr << "invalid age: " << newAge << '\n';
        }
    }
};

int main() {
    Student s1("Kim", 20);
    std::cout << s1.getName() << ' ' << s1.getAge() << '\n';

    // s1.age = -5; // 컴파일 오류: age는 private
    s1.setAge(-5);
    std::cout << s1.getAge() << '\n';

    s1.setAge(22);
    std::cout << s1.getAge() << '\n';
}
Kim 20
20
22

setAge(-5)는 범위 검사에서 거부되어 표준 오류에 invalid age: -5를 출력하고, 나이는 20 그대로입니다. Student(std::string n, int a) : name(n)처럼 클래스 이름과 같은 함수는 객체를 만들 때 실행되는 생성자(constructor)이며, 생성자와 소멸자에서 자세히 다룹니다. 이 생성자도 setAge를 거쳐 나이를 정하므로, 처음부터 잘못된 나이로 객체를 만들 수 없습니다.

공개 API를 통한 private 상태 변경

public setAge의 범위 검사 성공 때만 age가 갱신되고 거절 때 이전 값이 유지된다. private 직접 쓰기 오류는 런타임 분기로 포함하지 않는다.

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

C++ · encapsulation · checked flowchart

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

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

외부 직접 접근 거부와 공개 함수의 두 실행 결과원desktop의6노드5관계간선을보존한다.일반외부코드의직접age대입은컴파일오류경로이며공개setAge호출은범위검사로진행한다.예는age저장/아니요는이전age유지.직접쓰기를실행중조건으로검사하는뜻이아니다.범위는이Student예제의정책이다.일반 외부 코드Student 사용직접 age 쓰기private · 컴파일 오류공개 setAge 호출newAge 변경 요청0..120 안인가?함수의 실행 검사이전 값 유지아니요 · 대입 안 함age = newAge예 · 검증된 값 저장아니요예직접 쓰기는 실행 분기가 아님private만으로 범위 검사는 안 됨
setAge의 검증 경로와 직접 접근 차단일반 외부 직접 대입은 컴파일 오류이며 공개 setAge 호출은 범위 검사 후 참이면 대입하고 거짓이면 이전 값을 유지한다.일반 외부 코드Student 사용s.age = -5private · 컴파일 오류s.setAge(newAge)공개 API 호출0..120 범위인가?0 <= newAge && newAge <= 120아니오 · 대입하지 않음이전 age 유지예 · age = newAge검증된 값 저장
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()가 검사와 변경을 한 경로에 모아 불변식을 지킵니다.

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

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명원래 가로 그림의 6개 노드와 5개 관계 간선을 보존한다. 일반 외부 코드의 직접 age 대입은 컴파일 오류 경로이며 공개 setAge 호출은 범위 검사로 진행한다. 예 분기는 age 저장/아니요 분기는 이전 age 유지로 이어진다. 직접 쓰기를 실행 중 조건으로 검사한다는 뜻이 아니다. 범위는 이 Student 예제의 정책이다.
노드·연결선 라벨아니요 · 대입하지 않음
예의 값과 변화
관찰·상황값·보충 정보변화·이유
입력 검사

public setAge가 요청값을 범위와 대조한다.

  • 경계: setAge
  • age: 이전 값
  • 외부 요청이 private 값을 즉시 덮어쓰지 않는다.
조건부 대입

허용 범위 검사 결과에 따라 갱신 여부를 결정한다.

  • 검사 true: age를 요청값으로 갱신
  • 검사 false: age 이전 값 유지
  • 실패한 검증은 기존 객체 상태를 보존한다.

공개 API를 통한 private 상태 변경

public setAge의 범위 검사 성공 때만 age가 갱신되고 거절 때 이전 값이 유지된다. private 직접 쓰기 오류는 런타임 분기로 포함하지 않는다.

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

C++ · encapsulation · checked flowchart

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

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

외부 직접 접근 거부와 공개 함수의 두 실행 결과원desktop의6노드5관계간선을보존한다.일반외부코드의직접age대입은컴파일오류경로이며공개setAge호출은범위검사로진행한다.예는age저장/아니요는이전age유지.직접쓰기를실행중조건으로검사하는뜻이아니다.범위는이Student예제의정책이다.일반 외부 코드Student 사용직접 age 쓰기private · 컴파일 오류공개 setAge 호출newAge 변경 요청0..120 안인가?함수의 실행 검사이전 값 유지아니요 · 대입 안 함age = newAge예 · 검증된 값 저장아니요예직접 쓰기는 실행 분기가 아님private만으로 범위 검사는 안 됨
setAge의 검증 경로와 직접 접근 차단일반 외부 직접 대입은 컴파일 오류이며 공개 setAge 호출은 범위 검사 후 참이면 대입하고 거짓이면 이전 값을 유지한다.일반 외부 코드Student 사용s.age = -5private · 컴파일 오류s.setAge(newAge)공개 API 호출0..120 범위인가?0 <= newAge && newAge <= 120아니오 · 대입하지 않음이전 age 유지예 · age = newAge검증된 값 저장
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()가 검사와 변경을 한 경로에 모아 불변식을 지킵니다.

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

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명원래 가로 그림의 6개 노드와 5개 관계 간선을 보존한다. 일반 외부 코드의 직접 age 대입은 컴파일 오류 경로이며 공개 setAge 호출은 범위 검사로 진행한다. 예 분기는 age 저장/아니요 분기는 이전 age 유지로 이어진다. 직접 쓰기를 실행 중 조건으로 검사한다는 뜻이 아니다. 범위는 이 Student 예제의 정책이다.
노드·연결선 라벨아니요 · 대입하지 않음
예의 값과 변화
관찰·상황값·보충 정보변화·이유
입력 검사

public setAge가 요청값을 범위와 대조한다.

  • 경계: setAge
  • age: 이전 값
  • 외부 요청이 private 값을 즉시 덮어쓰지 않는다.
조건부 대입

허용 범위 검사 결과에 따라 갱신 여부를 결정한다.

  • 검사 true: age를 요청값으로 갱신
  • 검사 false: age 이전 값 유지
  • 실패한 검증은 기존 객체 상태를 보존한다.

위 그림처럼 밖의 코드가 age를 직접 바꾸려는 시도는 실행하기 전에 컴파일 오류로 막히고, setAge를 통한 요청만 실행 중에 범위 검사를 거칩니다. private은 직접 접근을 막아 줄 뿐 값의 범위를 검사하지는 않으므로, 검사는 공개한 함수 안에 따로 적어야 합니다. 0부터 120까지라는 범위도 이 예제가 정한 규칙입니다.

이처럼 값을 읽는 함수를 게터(getter), 바꾸는 함수를 세터(setter)라고 부르기도 합니다. 하지만 모든 멤버에 게터와 세터를 기계적으로 붙이면 public 변수와 다를 바가 없으므로, 객체가 밖에 무엇을 약속할지부터 정합니다. Car라면 속도를 아무 값으로나 정하는 세터보다, 지금처럼 accelerate와 brake만 공개하는 편이 객체의 규칙을 지키기 쉽습니다.

정리

  • 클래스는 데이터인 멤버 변수와 동작인 멤버 함수를 묶은 타입이며, 정의만으로는 멤버 변수의 공간이 생기지 않습니다.
  • 객체는 클래스로 만든 실체이며, 객체마다 자기만의 멤버 변수를 가지고 멤버에는 .로 접근합니다.
  • 멤버 함수의 코드는 하나이고, 호출할 때 점 앞의 객체를 대상으로 동작합니다.
  • 캡슐화는 데이터를 private으로 감추고, 검사가 들어 있는 public 멤버 함수로만 바꾸게 하는 것입니다.
  • private은 직접 접근을 컴파일할 때 막을 뿐이므로, 값의 범위 검사는 공개 함수 안에 따로 둡니다.

연습 문제

1. 두 객체의 속도

car_objects.cpp의 끝에 myCar.brake(50); yourCar.accelerate(5);를 추가하면 두 객체의 속도는 각각 얼마인가요?

해설 보기

myCar는 30에서 50을 빼 -20이 되지만, brake가 음수를 0으로 고치므로 0입니다. yourCar는 70에 5를 더해 75입니다. 같은 클래스의 함수라도 점 앞의 객체에만 작용하므로, 두 객체의 속도는 서로 영향을 주지 않습니다.

2. 생성자에서 거부된 나이

Student s2("Lee", -5);를 만든 뒤 s2.setAge(121);과 s2.setAge(120);을 차례로 호출하면, 각 시점의 s2의 나이는 얼마인가요?

해설 보기

age는 기본값 0으로 시작하고, 생성자의 setAge(-5)가 거부되므로 만든 직후는 0입니다. setAge(121)도 범위 밖이어서 0을 유지하고, setAge(120)은 허용되어 120이 됩니다. s1은 별도의 객체이므로 이 호출들과 상관이 없습니다.

3. 공개된 멤버 변수

car_objects.cpp에서 myCar.speed = -10;은 컴파일되나요? brake의 검사를 언제나 지키게 하려면 Car를 어떻게 고치면 되나요?

해설 보기

speed가 public이므로 컴파일되고, 속도가 -10이 됩니다. brake의 검사는 brake를 거칠 때만 적용되기 때문입니다. speed를 private으로 옮기고 읽기용 getSpeed()만 공개하면 속도를 바꾸는 길이 accelerate와 brake로 한정됩니다. 다만 지금의 accelerate는 accelerate(-50)처럼 음수를 받으면 속도를 음수로 만들 수 있으므로, amount가 음수이면 무시하는 검사를 accelerate에도 넣어야 속도가 음수가 될 수 없습니다.

다음 글 클래스 정의와 객체 생성에서는 클래스를 헤더와 소스 파일로 나누어 정의하는 방법과, 객체를 만들고 초기화하는 여러 방법을 다룹니다.