클래스와 객체의 개념
현실의 상태와 행동을 클래스와 객체로 모델링하는 원리를 이해하고 캡슐화 중심의 객체 지향 사고를 익힙니다.
이번 장부터는 C++의 핵심 특징 중 하나인 객체 지향 프로그래밍(Object-Oriented Programming, OOP)을 다룹니다.
객체 지향 프로그래밍은 현실 세계의 사물이나 개념을 컴퓨터 프로그램 안으로 가져와 모델링하는 방식입니다.
이를 통해 복잡한 시스템을 더 효과적으로 설계하고 관리할 수 있게 됩니다.
객체 지향 프로그래밍의 가장 기본적인 구성 요소가 바로 클래스(Class)와 객체(Object)입니다.
C++ · class · object tree
클래스는 공통 타입을 정의하고, 객체는 각자의 상태를 저장한다
class Car는 멤버의 이름·형식·동작을 정의하는 타입 계약입니다. 객체를 만들면 비정적 데이터 멤버의 저장 공간은 객체마다 따로 생기고, 멤버 함수 호출은 그 대상 객체의 상태에 작용합니다.
Sonata · White · speed 30
myCar의 비정적 데이터 멤버를 위한 독립 저장 공간입니다.
K5 · Black · speed 70
yourCar의 비정적 데이터 멤버를 위한 별도 저장 공간입니다.
비정적 데이터 멤버는 객체마다 저장된다
myCar.speed를 바꿔도 yourCar.speed는 바뀌지 않습니다. 어느 인스턴스를 대상으로 호출했는지가 상태 변경의 기준입니다.
함수 코드와 공유 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: 상속 관계에서 사용됩니다. (나중에 학습)
#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 쌍을 직접 관리하지 않습니다.
#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;
}위 예시에서 myCar와 yourCar는 모두 Car 클래스의 객체입니다.
이들은 동일한 model, color, speed라는 멤버 변수를 가지고 있지만, 각 객체는 자신만의 고유한 model, color, speed 값을 가집니다.
또한, 동일한 accelerate(), brake(), displayInfo() 인터페이스를 호출하되, 각 호출은 대상 객체의 비정적 상태를 조작하거나 출력합니다.
캡슐화 (Encapsulation)와 접근 지정자
클래스와 객체의 개념은 객체 경계, 접근 권한, 생성·소멸 책임을 기준으로 확인합니다.
C++ · encapsulation · checked flowchart
외부 요청은 공개 API와 검증을 통과한 뒤에만 내부 상태를 바꾼다
일반 외부 코드는 private age를 직접 쓸 수 없습니다. 이 예제에서는 공개된 setAge()가 값의 범위를 검사하고, 유효할 때만 객체 내부 상태를 변경합니다.
-
s.setAge(newAge)호출일반 외부 코드는 공개된 멤버 함수를 통해 변경을 요청합니다.
-
0 <= newAge && newAge <= 120검사범위 검사는 접근 지정자가 아니라
setAge()의 실행 로직입니다.
private age 변경
클래스 멤버 함수는 자신의 객체에 속한 비공개 상태에 접근하여 age = newAge를 수행합니다.
이전 값 유지
오류를 알리고 대입하지 않으므로 객체 상태는 바뀌지 않습니다.
s.age = -5는 컴파일되지 않는다
일반 외부 코드의 직접 쓰기는 private 접근 제어에 의해 차단됩니다.
private는 접근 가능한 코드를 제한한다
클래스의 멤버와 허용된 friend는 접근할 수 있지만, 일반 외부 호출자는 비공개 멤버를 직접 읽거나 쓸 수 없습니다.
유효성 검사는 공개 함수의 실행 규칙이다
private만으로 값의 범위가 보장되지는 않습니다. setAge()가 검사와 변경을 한 경로에 모아 불변식을 지킵니다.
- 공개 API를 통한 진행
- 검증을 통과한 상태 변경
- 접근 또는 값이 거부됨
캡슐화는 비공개 데이터를 숨기는 데서 끝나지 않습니다. 외부가 사용할 공개 약속을 작게 만들고, 그 경로에서 검증한 뒤에만 상태를 바꾸어 객체의 일관성을 유지합니다.
OOP의 중요한 원칙 중 하나인 캡슐화(Encapsulation)는 데이터(멤버 변수)와 그 데이터를 조작하는 코드(멤버 함수)를 하나의 단위(클래스)로 묶는 것을 의미합니다.
또한, 보호할 데이터를 private으로 지정해 일반 외부 코드의 직접 접근을 막고, 필요한 public 멤버 함수를 통해 데이터를 조작하게 함으로써 정보 은닉(Information Hiding)을 달성할 수 있습니다.
private접근 지정자는 캡슐화를 구현하는 핵심 도구입니다. 예를 들어,Car클래스에서fuelLevel을private으로 선언하면 일반 외부 코드에서myCar.fuelLevel = 100;와 같이 직접 접근할 수 없습니다.- 데이터를
private으로 만들고, 해당 데이터에 접근하거나 변경하는public멤버 함수(이들을 보통 게터(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 함수를 통해서만 안전하게 변경되도록 제어할 수 있습니다.
객체 지향 설계는 클래스 내부 상태를 감추는 것에서 끝나지 않고, 외부에 어떤 약속을 공개할지 결정하는 과정입니다.
이것이 바로 객체 지향 설계의 강점 중 하나입니다.
클래스를 설계할 때는 멤버를 많이 넣는 것보다, 어떤 값은 숨기고 어떤 함수만 약속으로 공개할지 판단하는 기준이 중요합니다.