본문으로 건너뛰기

안동민 개발노트

본문 시작

접근 지정자

public·private·protected가 외부 접근과 상속 경계를 제한하는 방식을 익혀 클래스의 상태와 구현을 캡슐화합니다.

지난 장에서 publicprivate이라는 키워드를 클래스 정의 내에서 사용했는데, 이것이 바로 접근 지정자(Access Specifier)입니다.

이번 장에서는 C++ 클래스에서 멤버(변수와 함수)에 대한 접근 권한을 제어하는 세 가지 접근 지정자, 즉 public, private, 그리고 protected에 대해 심도 있게 다루겠습니다.

이들은 캡슐화(Encapsulation)라는 객체 지향 프로그래밍의 핵심 원칙을 구현하는 데 필수적인 요소입니다.

접근 지정자는 객체 주변에 호출 가능 경계를 만든다

public, protected, private는 멤버의 중요도가 아니라 “누가 직접 접근해도 되는가”를 나누는 설계 표지다.

  1. class Account
  2. public

    외부에 약속한 호출 입구

  3. protected

    파생 클래스 확장 지점

  4. private

    객체 내부 상태와 보조 구현


접근 지정자의 역할과 캡슐화

접근 지정자는 public, protected, private가 객체 외부 호출과 상속 경계를 어떻게 제한하는지 기준으로 정리합니다.

캡슐화는 private 상태를 public 문으로만 바꾸게 한다

접근 지정자는 값을 숨기기 위한 장식이 아니라, 객체 상태가 검증된 통로로만 변하도록 만드는 장치다.

  1. 외부 호출자
    객체를 사용한다

    외부 호출자 객체를 사용한다 deposit(1000) 처럼 공개된 동작만 호출한다.

  2. public method
    검증하고 의미를 붙인다

    public method 검증하고 의미를 붙인다 금액이 유효한지 확인하고, 상태 변경 규칙을 한곳에 모은다.

  3. private data
    직접 접근은 막는다

    private data 직접 접근은 막는다 balance 같은 내부 값은 클래스가 스스로 일관성을 지킨다.

접근 지정자는 클래스의 멤버(멤버 변수와 멤버 함수)가 클래스 외부에서 얼마나 접근 가능한지를 명시하는 키워드입니다.

이를 통해 클래스 사용자가 클래스의 내부 구현에 직접 접근하여 객체의 상태를 훼손하는 것을 막고, 오직 클래스가 의도한 방식으로만 객체와 상호작용하도록 강제합니다.

이러한 접근 제어는 캡슐화(Encapsulation)를 구현하는 핵심 요소입니다.

캡슐화는 데이터와 데이터를 처리하는 함수를 하나의 단위(클래스)로 묶고, 외부로부터의 직접적인 접근을 제한하여 정보 은닉(Information Hiding)을 달성하는 객체 지향의 주요 원칙입니다.

  • 정보 은닉의 장점
    • 데이터 보호: 객체의 내부 상태가 외부로부터 오염되거나 오용되는 것을 방지합니다.
    • 유지보수성 향상: 클래스의 내부 구현이 변경되어도 외부에 노출된 인터페이스(public 멤버)만 변경되지 않는다면, 클래스를 사용하는 외부 코드에 영향을 미치지 않습니다.
    • 코드 재사용성 증가: 잘 캡슐화된 클래스는 독립적으로 동작하며 다른 프로그램에서도 쉽게 재사용될 수 있습니다.
    • 디버깅 용이성: 문제가 발생했을 때, 문제가 발생한 범위를 좁히는 데 도움이 됩니다.
C++에는 세 가지 접근 지정자가 있습니다
public (공개)
private (비공개)
protected (보호된)

public 접근 지정자

public으로 선언된 멤버는 클래스 외부 어디에서든 접근할 수 있습니다.

이는 클래스가 외부와 소통하는 인터페이스(Interface) 역할을 합니다.

객체의 동작을 제어하기 위해 외부에서 호출할 수 있는 멤버 함수나, 객체의 상태를 나타내는 데 필요한 멤버 변수 등을 public으로 선언합니다.

특징
  • 클래스 객체를 통해 직접 접근 가능.
  • 클래스 내부, 상속받은 클래스 내부에서도 접근 가능.
public 접근 지정자 예시
#include <iostream>
#include <string>

class User {
public: // public 접근 지정자
    std::string username; // 외부에서 직접 접근 가능
    std::string email;    // 외부에서 직접 접근 가능

    void displayUserInfo() { // 외부에서 직접 호출 가능
        std::cout << "사용자 이름: " << username << ", 이메일: " << email << std::endl;
    }
};

int main() {
    User user1;

    // public 멤버 변수에 직접 접근하여 값 할당
    user1.username = "Alice";
    user1.email = "alice@example.com";

    // public 멤버 함수 호출
    user1.displayUserInfo(); // 출력: 사용자 이름: Alice, 이메일: alice@example.com

    // 외부에서 public 멤버에 직접 접근 가능
    std::cout << "직접 접근: " << user1.username << std::endl;

    return 0;
}

private 접근 지정자

private으로 선언된 멤버는 오직 해당 클래스의 멤버 함수 내에서만 접근할 수 있습니다.

클래스 외부에서는 직접 접근할 수 없습니다.

이는 객체의 내부 상태를 보호하고 캡슐화를 강화하는 데 사용됩니다.

특징
  • 클래스 외부에서 직접 접근 불가능.
  • 클래스 내부의 멤버 함수에서는 접근 가능.
  • 상속받은 클래스에서도 직접 접근 불가능.
private 접근 지정자 예시
#include <iostream>
#include <string>

class BankAccount {
private: // private 접근 지정자
    std::string accountNumber; // 계좌번호 (외부에서 직접 접근 불가)
    double balance;            // 잔액 (외부에서 직접 접근 불가)

public:
    // 생성자
    BankAccount(std::string accNum, double initialBalance) {
        accountNumber = accNum;
        if (initialBalance >= 0) {
            balance = initialBalance;
        } else {
            balance = 0;
            std::cerr << "초기 잔액은 음수가 될 수 없습니다. 0으로 설정합니다." << std::endl;
        }
    }

    // public 멤버 함수 (private 멤버에 접근하여 작업 수행)
    void deposit(double amount) { // 입금
        if (amount > 0) {
            balance += amount;
            std::cout << amount << "원 입금되었습니다. 현재 잔액: " << balance << std::endl;
        } else {
            std::cout << "유효하지 않은 입금액입니다." << std::endl;
        }
    }

    void withdraw(double amount) { // 출금
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            std::cout << amount << "원 출금되었습니다. 현재 잔액: " << balance << std::endl;
        } else {
            std::cout << "출금 실패: 잔액이 부족하거나 유효하지 않은 금액입니다." << std::endl;
        }
    }

    // 잔액을 확인하는 게터 함수 (private 멤버의 값을 외부에 안전하게 노출)
    double getBalance() {
        return balance;
    }

    std::string getAccountNumber() { // 계좌번호 게터
        return accountNumber;
    }
};

int main() {
    BankAccount myAccount("123-456-7890", 1000.0);

    // myAccount.balance = -500; // 컴파일 오류! balance는 private이므로 직접 접근 불가

    std::cout << "초기 잔액: " << myAccount.getBalance() << std::endl; // 게터를 통해 잔액 확인

    myAccount.deposit(500.0);   // public 함수를 통해 입금
    myAccount.withdraw(200.0);  // public 함수를 통해 출금
    myAccount.withdraw(2000.0); // 잔액 부족으로 출금 실패

    std::cout << "최종 잔액: " << myAccount.getBalance() << std::endl;

    return 0;
}

위 예시에서 accountNumberbalanceprivate으로 선언되어 외부에서 직접 접근하거나 변경할 수 없습니다.

대신 deposit(), withdraw(), getBalance() 같은 public 멤버 함수를 통해 balance에 간접 접근하고 조작할 수 있습니다.

이 방식은 유효성 검사(예: 음수 잔액 방지, 유효하지 않은 입출금액)를 강제해 데이터 무결성을 보장하는 데 매우 효과적입니다.


protected 접근 지정자

protected로 선언된 멤버는 privatepublic의 중간 정도의 접근 권한을 가집니다.

특징
  • 해당 클래스의 멤버 함수 내에서 접근 가능. (private과 동일)
  • 해당 클래스를 상속받은 자식(파생) 클래스의 멤버 함수 내에서 접근 가능. (private과 다름)
  • 클래스 외부에서는 직접 접근 불가능. (private과 동일)

protected는 주로 상속 관계에서 부모 클래스의 일부 멤버를 자식 클래스가 접근하고 싶지만, 외부에는 노출하고 싶지 않을 때 사용됩니다.

상속에 대한 자세한 내용은 이후에 다룰 것이므로, 지금은 개념만 이해해 두시면 됩니다.

protected 접근 지정자 예시
class Animal {
protected: // protected 멤버
    std::string species; // 종은 자식 클래스가 접근 가능
    int age;             // 나이도 자식 클래스가 접근 가능

public:
    Animal(std::string s, int a) : species(s), age(a) {}
    void displaySpecies() {
        std::cout << "종: " << species << std::endl;
    }
};

class Dog : public Animal { // Dog 클래스가 Animal 클래스를 상속받음
public:
    Dog(std::string name, int age) : Animal("개", age) { // 부모 클래스의 생성자 호출
        // 자식 클래스인 Dog의 멤버 함수에서 protected 멤버인 age에 접근 가능
        std::cout << "새로운 강아지 객체 생성! 나이: " << this->age << std::endl;
    }

    void bark() {
        std::cout << "멍멍!" << std::endl;
    }
};

int main() {
    Dog myDog("바둑이", 3);
    myDog.displaySpecies(); // Animal의 public 함수 호출
    myDog.bark();

    // myDog.age = 4; // 컴파일 오류: age는 protected이므로 외부에서 직접 접근 불가
    return 0;
}

세 접근 지정자는 "무엇을 선언했는가"보다 "어디에서 접근하는가"를 기준으로 보면 더 빠르게 구분할 수 있습니다.

같은 멤버도 호출 위치에 따라 허용 여부가 달라진다

protected는 “외부 공개”가 아니라 “상속받은 클래스 내부에서만 열리는 통로”다.

  1. 허용 자기 멤버 함수

    직접 사용한다.

  2. 허용 상속용 멤버도 내부에서

    접근 가능하다.

  3. 허용 상태

    구현을 직접 관리한다.

  4. 허용 상속된 공개 API

    호출한다.

  5. 허용 부모

    의도한 확장 지점만 사용한다.

  6. 차단 부모 내부 상태

    직접 만질 수 없다.

  7. 허용 객체 사용자

    호출하는 계약이다.

  8. 차단 상속 설계용 통로라 외부에

    보이지 않는다.

  9. 차단 객체의 내부 구현

    숨긴다.

class Animal {
protected:
  int age;
public:
  void grow();
};
class Dog : public Animal {
  void bark() {

age
++;  // 허용
  }
};

int main() {
  Dog dog;
  dog.
age
++; // 차단
}

기본 접근 지정자

클래스에서 명시적으로 접근 지정자를 선언하지 않으면, 기본적으로 모든 멤버는 private 으로 간주됩니다. (struct 키워드로 클래스를 선언하면 기본적으로 public입니다.)

classstruct는 기본 접근 지정자가 다르므로, 첫 번째 접근 지정자를 쓰기 전의 멤버가 어디에 노출되는지 먼저 확인해야 합니다.

class는 닫힌 상태로, struct는 열린 상태로 시작한다

접근 지정자를 쓰기 전의 멤버는 class와 struct에서 서로 다른 기본 공개 범위를 가진다.

  1. class

    상태 보호 목적이면 기본값만으로도 먼저 닫힌다.

  2. struct

    단순 데이터 묶음이면 기본값이 공개 쪽이라 자연스럽다.

  3. 명시 우선

    중요한 경계는 public, protected, private를 직접 적어 의도를 남긴다.

class MyClass {
  int privateVar;   // private

public:
  int publicVar;    // public
};
struct MyStruct {
  int publicVar;    // public

private:
  int privateVar;   // private
};
기본 접근 지정자 예시
class MyClass { // class 키워드 사용
    int privateVar; // 명시하지 않으면 private
public:
    int publicVar;
};

struct MyStruct { // struct 키워드 사용
    int publicVar; // 명시하지 않으면 public
private:
    int privateVar;
};

일반적으로 C++에서는 객체 지향적인 설계를 위해 class 키워드를 사용하여 클래스를 정의하고, 멤버 변수는 private 또는 protected로 선언하여 캡슐화를 강화합니다.

접근 지정자는 문법을 나누는 표식이 아니라 객체 상태가 바깥으로 새어 나가는 경로를 제한하는 설계 장치입니다.

접근 지정자는 객체 상태가 새는 길을 막는다

좋은 클래스는 외부가 내부 필드를 외우게 하지 않고, 검증된 public 동작만 의존하게 만든다.

  1. public

    나쁜 노출: 필드가 public public balance 외부 코드가 account.balance = -100; 처럼 값을 직접 바꾼다. 검증 지점이 흩어지고 객체 불변식이 깨진다.

  2. private

    좋은 경계: 상태는 private, 동작은 public private balance public deposit() , withdraw() 모든 변경이 메서드의 검증 규칙을 지난다.

  3. 설계 질문:

    “이 멤버를 바꾸는 책임이 외부 사용자에게 있는가, 객체 자신에게 있는가?” 답이 객체 자신이면 private으로 닫고 public 메서드로 통제한다.