접근 제어자
클래스는 데이터(속성)와 그 데이터를 다루는 로직(메서드)을 하나로 묶어 객체를 생성하는 틀입니다. 이때 객체 지향 프로그래밍의 중요한 원칙 중 하나가 바로 캡슐화(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
(보호된)
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
의 차이점을 명확히 이해하고, 각 멤버의 성격에 맞게 적절한 접근 제어자를 사용하는 것은 견고하고 유지보수하기 쉬운 클래스를 설계하는 데 매우 중요합니다.