안동민 개발노트 아이콘

안동민 개발노트

4장 : 클래스와 인터페이스

접근 제어자

클래스는 데이터(속성)와 로직(메서드)을 묶어 객체를 생성하는 틀입니다.

여기서 중요한 객체 지향 원칙이 캡슐화(Encapsulation)입니다. 캡슐화는 내부 구현을 숨기고, 외부에 꼭 필요한 부분만 선택적으로 노출하는 방식입니다.

이 원칙을 지키면 코드 응집도가 높아지고, 외부에서 잘못된 방식으로 상태를 바꾸는 일을 줄여 안정성을 확보할 수 있습니다.

타입스크립트는 클래스의 속성과 메서드에 대한 접근 수준을 제어하기 위해 세 가지 접근 제어자(Access Modifiers)를 제공합니다. 이들은 클래스 멤버가 클래스 외부, 자식 클래스, 또는 오직 클래스 내부에서만 접근 가능하도록 설정하는 역할을 합니다.


public (공개)

아래 다이어그램은 이 절의 핵심 흐름을 역할과 상태 전환 중심으로 정리한 것입니다.

public은 가장 기본적인 접근 제어자입니다. public으로 선언된 속성이나 메서드는 클래스 외부의 어디에서든 자유롭게 접근할 수 있습니다. 자바스크립트 클래스의 기본 동작과 동일하며, 타입스크립트에서 접근 제어자를 명시하지 않으면 해당 멤버는 자동으로 public으로 간주됩니다.

class Product {
  public name: string; // 'public' 명시
  price: number;      // 접근 제어자를 명시하지 않으면 기본적으로 'public'

  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }

  public getDetails(): string { // 'public' 명시
    return `제품명: ${this.name}, 가격: ${this.price}원`;
  }
}

const laptop = new Product("노트북", 1200000);

console.log(laptop.name);      // '노트북' - public 속성에 외부에서 직접 접근
console.log(laptop.price);     // '1200000' - public 속성에 외부에서 직접 접근
console.log(laptop.getDetails()); // '제품명: 노트북, 가격: 1200000원' - public 메서드 호출

// public 멤버는 외부에서 자유롭게 변경 가능
laptop.price = 1150000;
console.log(laptop.price); // 1150000

public 멤버는 객체의 인터페이스, 즉 외부에서 객체를 어떻게 사용해야 하는지를 정의하는 데 사용됩니다.


private (비공개)

private은 클래스 멤버가 오직 해당 클래스 내부에서만 접근 가능하도록 제한합니다. 클래스 외부에서는 물론, 심지어 이 클래스를 상속받은 자식 클래스에서도 private 멤버에 직접 접근할 수 없습니다. 이는 객체의 특정 상태나 로직이 외부에서 직접 조작되어서는 안 될 때, 또는 내부 구현의 세부 사항을 감추고 싶을 때 사용됩니다.

class BankAccount {
  private _balance: number; // 계좌 잔액은 private으로 선언하여 외부에서 직접 변경 불가

  constructor(initialBalance: number) {
    if (initialBalance < 0) {
      throw new Error("초기 잔액은 0보다 작을 수 없습니다.");
    }
    this._balance = initialBalance;
  }

  // public 메서드를 통해 private 속성에 간접적으로 접근
  public getBalance(): number {
    return this._balance;
  }

  public deposit(amount: number): void {
    if (amount > 0) {
      this._balance += amount;
      console.log(`${amount}원 입금. 현재 잔액: ${this._balance}원`);
    } else {
      console.log("0원 이상 입금 가능합니다.");
    }
  }

  public withdraw(amount: number): void {
    if (amount > 0 && this._balance >= amount) {
      this._balance -= amount;
      console.log(`${amount}원 출금. 현재 잔액: ${this._balance}원`);
    } else {
      console.log("잔액이 부족하거나 유효하지 않은 금액입니다.");
    }
  }
}

const myAccount = new BankAccount(10000);

// console.log(myAccount._balance); // Error: '_balance' 속성은 private이며 'BankAccount' 클래스 내에서만 접근할 수 있습니다.

myAccount.deposit(5000);   // 5000원 입금. 현재 잔액: 15000원
myAccount.withdraw(2000);  // 2000원 출금. 현재 잔액: 13000원
console.log(myAccount.getBalance()); // 13000 - public 메서드를 통해 안전하게 잔액 확인

myAccount.withdraw(20000); // 잔액이 부족하거나 유효하지 않은 금액입니다. (내부 로직에 의해 제어)

private 멤버는 클래스의 내부 비밀을 보호하는 역할을 합니다. 외부에서는 public 메서드(예: deposit, withdraw)를 통해서만 _balance에 접근하거나 변경할 수 있도록 하여, 데이터의 무결성을 유지할 수 있습니다.

참고: 자바스크립트의 최신 문법에는 #을 사용하는 프라이빗 필드(Private Fields)가 있습니다. 타입스크립트의 private 키워드는 컴파일 시점 타입 검사로 접근을 제한하지만, 자바스크립트로 변환된 뒤에는 접근 가능한 형태로 남을 수 있습니다(물론 접근하지 않는 것이 권장됩니다). 반면 # 프라이빗 필드는 런타임에서도 완전히 프라이빗하게 동작합니다. 일반적으로 타입스크립트에서는 private 키워드를 주로 사용하고, 런타임에서의 완전한 캡슐화가 필요할 때 # 프라이빗 필드를 고려할 수 있습니다.


protected (보호된)

protectedprivate보다는 덜 제한적이고 public보다는 더 제한적인 접근 제어자입니다. protected로 선언된 속성이나 메서드는 해당 클래스 내부이 클래스를 상속받은 자식 클래스 내부에서만 접근할 수 있습니다. 클래스 외부에서는 접근이 불가능합니다.

이는 부모 클래스에서 정의된 특정 로직이나 데이터가 자식 클래스에서 재활용되거나 확장되어야 하지만, 외부에는 노출되지 않아야 할 때 유용합니다.

class Vehicle {
  protected speed: number; // protected 속성
  constructor(initialSpeed: number) {
    this.speed = initialSpeed;
  }

  protected accelerate(amount: number): void { // protected 메서드
    this.speed += amount;
    console.log(`속도 증가: ${this.speed}`);
  }

  public getCurrentSpeed(): number {
    return this.speed; // public 메서드를 통해 간접 접근
  }
}

class Car extends Vehicle {
  private gear: number = 1;

  constructor(initialSpeed: number, initialGear: number) {
    super(initialSpeed); // 부모 클래스(Vehicle)의 생성자 호출
    this.gear = initialGear;
  }

  public drive(): void {
    console.log(`현재 기어: ${this.gear}`);
    this.accelerate(10); // 자식 클래스에서 부모의 protected 메서드 접근 가능
    console.log(`차량이 ${this.speed}km/h로 주행 중입니다.`); // 자식 클래스에서 부모의 protected 속성 접근 가능
  }

  // 외부에서는 접근 불가
  // public setProtectedSpeed(newSpeed: number) {
  //   this.speed = newSpeed; // Error: Property 'speed' is protected...
  // }
}

const myCar = new Car(60, 3);
myCar.drive(); // 현재 기어: 3, 속도 증가: 70, 차량이 70km/h로 주행 중입니다.
console.log(myCar.getCurrentSpeed()); // 70

// console.log(myCar.speed); // Error: 'speed' 속성은 protected이며 'Vehicle' 클래스 및 해당 하위 클래스 내에서만 접근할 수 있습니다.
// myCar.accelerate(20); // Error: 'accelerate' 속성은 protected이며...

protected 멤버는 상속 계층 구조 내에서 데이터를 공유하고 메서드를 호출할 수 있도록 하여, 서브클래싱(Subclassing)을 통한 기능 확장을 용이하게 합니다.


생성자의 매개변수에 접근 제어자 사용 (복습 및 강조)

이전 절에서 잠시 언급했지만, 생성자의 매개변수에 접근 제어자를 붙여 속성을 선언하고 초기화하는 것을 간결하게 처리할 수 있습니다. 이는 클래스 선언에서 자주 사용되는 패턴입니다.

class Student {
  // public, private, protected 또는 readonly를 매개변수 앞에 붙이면
  // 해당 매개변수는 자동으로 클래스의 해당 접근 제어자를 가진 속성으로 선언되고 초기화됩니다.
  constructor(
    public studentId: string, // public 속성으로 선언
    private _studentName: string, // private 속성으로 선언
    protected grade: number, // protected 속성으로 선언
    readonly entryYear: number // readonly + public 속성으로 선언 (읽기 전용)
  ) {
    // 별도로 this.studentId = studentId; 등을 작성할 필요가 없습니다.
  }

  public getInfo(): string {
    return `ID: ${this.studentId}, 이름: ${this._studentName}, 학년: ${this.grade}`;
  }

  // private _studentName에 대한 getter
  get studentName(): string {
    return this._studentName;
  }
}

const s1 = new Student("S001", "김민수", 3, 2022);
console.log(s1.studentId); // S001 (public)
console.log(s1.getInfo()); // ID: S001, 이름: 김민수, 학년: 3

// console.log(s1._studentName); // Error: private
// console.log(s1.grade); // Error: protected

s1.studentId = "S002"; // public이므로 변경 가능
// s1.entryYear = 2023; // Error: readonly이므로 변경 불가

이 문법은 특히 DTO(Data Transfer Object)나 간단한 모델 클래스를 정의할 때 코드의 양을 크게 줄여줍니다.


접근 제어자는 타입스크립트에서 객체 지향의 캡슐화를 구현하는 핵심적인 도구입니다. public, private, protected의 차이점을 명확히 이해하고, 각 멤버의 성격에 맞게 적절한 접근 제어자를 사용하는 것은 견고하고 유지보수하기 쉬운 클래스를 설계하는 데 매우 중요합니다.