본문으로 건너뛰기

안동민 개발노트

본문 시작

접근 지정자

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

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

이번 장에서는 C++ 클래스 멤버를 어느 문맥에서 이름 붙여 사용할 수 있는지 제어하는 세 가지 접근 지정자, 즉 public, private, 그리고 protected를 다룹니다.

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

접근 검사는 프로그램이 올바른 형식인지 판단하는 컴파일 시점 언어 규칙입니다. 메모리를 암호화하거나 실행 중 공격을 차단하는 보안 경계는 아닙니다.

public, protected, private 멤버를 선언 클래스, 파생 클래스, 외부 코드에서 이름 붙일 수 있는지와 각 지정자의 설계 역할을 비교하는 표

C++ · access control · semantic table

접근 위치와 지정자를 함께 봐야 허용 범위가 보인다

public은 외부 계약, protected는 상속 확장 지점, private은 내부 구현입니다. 접근 검사는 소스를 컴파일할 때 적용되는 언어 규칙이며 실행 중 보안 경계는 아닙니다.

접근 지정자 × 이름을 사용하는 코드 위치와 설계 역할
지정자 선언 클래스 문맥 파생 클래스 문맥 그 밖의 외부 코드 주요 설계 역할
public 허용 · 선언 클래스도 자신의 공개 멤버를 사용합니다. 허용 · 접근 제어 관점에서 그대로 사용할 수 있습니다. 허용 · 외부 사용자가 의존하는 공개 API가 됩니다. 안정적으로 제공할 공개 계약과 검증 진입점입니다.
protected 허용 · 선언 클래스의 멤버와 명시된 friend가 사용합니다. 조건부 허용 · 비정적 멤버는 현재 파생 타입 또는 더 파생된 타입의 객체를 통해 접근해야 합니다. 임의의 Base& 객체를 통한 접근은 허용되지 않습니다. 차단 · 일반 객체 사용자의 공개 API가 아닙니다. 파생 타입과 결합되는 의도적인 상속 확장 지점입니다.
private 허용 · 선언 클래스의 멤버와 그 클래스가 명시한 friend가 사용합니다. 차단 · 파생됐다는 이유만으로는 접근할 수 없습니다. 기반 클래스가 별도로 허가한 friend 권한은 예외입니다. 차단 · 외부에서 직접 이름 붙일 수 없습니다. 표현 세부, 보조 동작, 외부가 직접 바꾸지 않을 상태입니다.
PUBLIC

외부에 약속한 계약

선언 클래스, 파생 클래스, 외부 코드에서 접근 제어상 허용됩니다. 공개 메서드는 검증을 모으는 진입점이 되어 불변식을 지키는 데 도움을 주지만, 구현의 정확성까지 보장하지는 않습니다.

PROTECTED

상속 계층의 확장 지점

선언 클래스의 멤버와 friend, 파생 클래스의 멤버와 friend에게 허용되고 외부에서는 차단됩니다. 파생 문맥의 비정적 멤버는 현재 파생 타입 또는 더 파생된 타입의 객체를 통해 접근해야 하며, 임의의 Base& 객체는 사용할 수 없습니다.

PRIVATE

선언 클래스의 내부 구현

선언 클래스의 멤버와 그 클래스가 명시한 friend만 접근합니다. 파생 클래스와 외부 코드는 기본적으로 차단되며, 기반 클래스가 별도로 부여한 friend 권한만 예외입니다.

접근 제어 위반은 컴파일 시 해당 구성을 ill-formed로 만듭니다. 메모리 보호나 권한 인증을 대신하지 않으며, 공개 진입점도 올바르게 구현될 때에만 불변식 유지에 도움이 됩니다.


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

접근 지정자는 public, protected, private가 객체 외부 호출과 상속 경계를 어떻게 제한하는지 기준으로 정리합니다. 각 지정자는 다음 지정자가 나오거나 클래스 정의가 끝날 때까지 뒤따르는 멤버 선언에 적용됩니다.

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

이를 통해 클래스 사용자가 내부 구현에 직접 의존하는 경로를 줄이고, 클래스가 제공하는 공개 동작을 통해 객체와 상호작용하도록 돕습니다.

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

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

  • 정보 은닉의 장점
    • 직접 변경 경로 축소: 객체의 내부 상태를 아무 호출 지점에서나 바꾸지 못하게 합니다.
    • 유지보수성 향상: 공개 계약을 유지하고 외부에서 관찰되는 동작이나 필요한 ABI 호환성을 깨지 않는 범위에서 내부 구현을 바꾸면, 외부 코드의 수정 범위를 줄일 수 있습니다.
    • 코드 재사용성 증가: 잘 캡슐화된 클래스는 독립적으로 동작하며 다른 프로그램에서도 쉽게 재사용될 수 있습니다.
    • 디버깅 용이성: 문제가 발생했을 때, 문제가 발생한 범위를 좁히는 데 도움이 됩니다.
C++에는 세 가지 접근 지정자가 있습니다
public (공개)
private (비공개)
protected (보호된)

public 접근 지정자

public으로 선언된 멤버는 접근 제어 관점에서는 어떤 문맥에서도 이름 붙일 수 있습니다. 물론 이름 탐색, 타입, 객체 수명 같은 다른 C++ 규칙은 여전히 만족해야 합니다.

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

외부 사용자가 의존해도 되는 멤버 함수와 타입을 public 계약으로 둡니다. 멤버 변수도 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;
}

이 예제의 usernameemail은 공개 접근 문법을 보여주기 위해 직접 노출했습니다. 값 사이의 규칙을 유지해야 하는 클래스라면 상태를 private으로 두고 의미 있는 공개 동작을 제공하는 편이 낫습니다.


private 접근 지정자

private으로 선언된 멤버는 선언한 클래스의 멤버와 그 클래스가 명시한 friend만 이름 붙일 수 있습니다.

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

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

특징
  • 클래스 외부에서 직접 접근 불가능.
  • 선언 클래스의 멤버와 명시적으로 허가한 friend에서는 접근 가능.
  • 파생 클래스라는 이유만으로는 직접 접근할 수 없음.
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() const {
        return balance;
    }

    std::string getAccountNumber() const { // 계좌번호 게터
        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로 선언된 멤버는 외부 API가 아니라 상속 계층을 위한 확장 지점입니다.

특징
  • 선언 클래스의 멤버와 friend에서 접근 가능.
  • 파생 클래스의 멤버와 friend에서도 접근 가능.
  • 그 밖의 외부 코드에서는 직접 접근 불가능.

다만 파생 클래스 문맥에서 비정적 protected 멤버에 접근할 때는 객체 표현식에도 제한이 있습니다. 현재 파생 클래스 타입이나 그보다 더 파생된 타입의 객체를 통해서는 접근할 수 있지만, 임의의 기반 클래스 객체를 통해서는 접근할 수 없습니다. 예를 들어 Dog의 멤버 안에서 Dog& other의 보호 멤버는 사용할 수 있지만 Animal& base의 같은 멤버는 사용할 수 없습니다.

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

protected 접근 지정자 예시
#include <iostream>
#include <string>

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 클래스를 상속받음
private:
    std::string name;

public:
    Dog(const std::string& dogName, int dogAge)
        : Animal("개", dogAge), name(dogName) {}

    void copyAgeFrom(const Dog& other) {
        age = other.age; // 허용: other의 정적 타입이 Dog
    }

    void compareWith(const Animal& base) {
        // age = base.age; // 컴파일 오류: 임의의 Animal 객체를 통한 접근
        (void)base;
    }

    void display() const {
        std::cout << name << " / " << species << " / " << age << std::endl;
    }
};

int main() {
    Dog myDog("바둑이", 3);
    Dog olderDog("누렁이", 5);
    myDog.copyAgeFrom(olderDog);
    myDog.displaySpecies(); // Animal의 public 함수 호출
    myDog.display();

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

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


기본 접근 지정자

class 정의에서 접근 지정자를 생략한 멤버는 기본적으로 private이고, struct 정의에서는 기본적으로 public입니다.

첫 번째 접근 지정자 이전의 멤버만 이 기본값의 영향을 받습니다. public:, protected:, private:를 명시한 뒤의 의미는 classstruct에서 같습니다.

class와 struct에서 첫 접근 지정자 이전 멤버의 기본 접근과 명시적인 public, protected, private 지정자의 동일한 동작을 비교하는 표

C++ · class key · semantic table

멤버 기본 접근은 class가 private, struct가 public이다

차이는 첫 접근 지정자 이전의 시작값입니다. 명시한 public:, protected:, private:의 의미와 적용 범위는 두 클래스 키에서 같습니다.

멤버 선언에서 class와 struct의 기본 접근 비교
비교 항목 class struct
첫 지정자 이전 private가 기본입니다. public이 기본입니다.
명시한 지정자 public:, protected:, private:를 모두 같은 의미로 사용할 수 있습니다. public:, protected:, private:를 모두 같은 의미로 사용할 수 있습니다.
적용 범위 지정자 뒤부터 다음 지정자 또는 클래스 정의 끝까지 적용됩니다. 지정자 뒤부터 다음 지정자 또는 클래스 정의 끝까지 적용됩니다.
흔한 관례 불변식을 관리하는 추상화에 자주 쓰지만 언어가 강제하지 않습니다. 공개 데이터 묶음에 자주 쓰지만 언어가 강제하지 않습니다.
CLASS

첫 멤버는 기본적으로 private

첫 지정자 이전만 닫힌 상태로 시작합니다. 이후에는 명시한 public:, protected:, private:가 다음 지정자 또는 정의 끝까지 적용됩니다. 불변식을 관리하는 추상화에 자주 쓰지만 관례일 뿐입니다.

STRUCT

첫 멤버는 기본적으로 public

첫 지정자 이전만 열린 상태로 시작합니다. 명시한 세 접근 지정자의 의미와 적용 범위는 class와 같습니다. 공개 데이터 묶음에 자주 쓰지만 관례일 뿐입니다.

이 표는 멤버 기본 접근을 비교합니다. 기반 클래스 접근 지정자를 생략한 상속 목록에서는 파생 클래스를 class로 정의하면 private, struct로 정의하면 public이 기본이지만 이는 별도 규칙입니다.

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

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

int main() {
    MyClass classValue;
    MyStruct structValue;
    classValue.publicVar = 1;
    structValue.publicVar = 2;
    return classValue.publicVar + structValue.publicVar == 3 ? 0 : 1;
}

관례상 불변식을 가진 추상화에는 class, 단순한 공개 데이터 묶음에는 struct를 자주 사용하지만 이는 언어 규칙이 아닙니다. 어느 키워드를 쓰든 중요한 경계에는 접근 지정자를 명시할 수 있습니다.

상속 목록에서 기반 클래스 접근을 생략하면 파생 클래스를 class로 정의한 경우 private, struct로 정의한 경우 public이 기본이지만, 이는 여기서 비교한 멤버 기본 접근과 별도의 규칙입니다.

접근 지정자는 문법을 나누는 표식이 아니라 외부 계약, 상속 확장 지점, 내부 구현의 경계를 표현하는 설계 장치입니다.