접근 제어자
클래스는 데이터(속성)와 로직(메서드)을 묶어 객체를 생성하는 틀입니다.
여기서 중요한 객체 지향 원칙이 캡슐화(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); // 1150000public 멤버는 객체의 인터페이스, 즉 외부에서 객체를 어떻게 사용해야 하는지를 정의하는 데 사용됩니다.
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 (보호된)
protected는 private보다는 덜 제한적이고 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의 차이점을 명확히 이해하고, 각 멤버의 성격에 맞게 적절한 접근 제어자를 사용하는 것은 견고하고 유지보수하기 쉬운 클래스를 설계하는 데 매우 중요합니다.