5편. 클래스에 타입 적용하기: 클래스 정의와 인터페이스 구현
📚 목차
1. 클래스에 타입 적용: 클래스 정의와 구현
2. 접근 제어자: 캡슐화의 핵심 (public, private, protected)
3. 클래스 속성을 안전하게 다루는 방법: getter와 setter
4. 상속과 추상 클래스: 코드 재사용성과 구조 설계

📂 [GitHub 예시 코드 보러가기] (https://github.com/cericube/nodejs-tutorials) /TypeScript
1. 클래스에 타입 적용: 클래스 정의와 구현
TypeScript에서는 클래스(Class)를 통해 객체를 만들 수 있으며, 이때 클래스의 속성(Property)과 생성자(Constructor), 그리고 메서드(Method)에 타입을 명확히 지정함으로써 정확하고 안정적인 코드를 작성할 수 있습니다.
또한, 클래스는 인터페이스(Interface)를 구현함으로써 일관된 구조를 보장할 수 있습니다.
🔷 클래스 선언 및 속성 타입 지정
클래스를 정의할 때는 각 속성의 타입을 명확히 지정하는 것이 매우 중요합니다.
또한 생성자에서는 매개변수 타입도 정확히 작성해야 합니다.
TypeScript는 클래스 속성에 잘못된 타입의 값을 할당할 경우 컴파일 타임에 오류를 알려주어 예기치 않은 런타임 오류를 방지할 수 있습니다.
class Book {
// 1) 속성에 타입 지정
title: string;
author: string;
pages: number;
isPublished: boolean;
// 2) 생성자 매개변수에도 타입 지정
constructor(title: string, author: string, pages: number, isPublished: boolean) {
this.title = title;
this.author = author;
this.pages = pages;
this.isPublished = isPublished;
}
// 3) 반환 타입이 있는 메서드
getSummary(): string {
return `${this.title} - ${this.author} (${this.pages}쪽) / 출간 여부: ${
this.isPublished ? '출간됨' : '미출간'
}`;
}
// 4) 반환 타입이 void인 메서드
publish(): void {
this.isPublished = true;
console.log(`"${this.title}"가 출간 상태로 변경되었습니다.`);
}
}
// ✅ 사용 예시
const book1 = new Book('타입스크립트 완벽 가이드', '홍길동', 350, false);
console.log(book1.getSummary()); // 메서드 호출
book1.publish(); // 상태 변경
console.log(book1.getSummary());
// 🚨 잘못된 타입 할당 예시 (컴파일 에러)
// book1.pages = "많이"; // Type 'string' is not assignable to type 'number'.
🔷 클래스와 인터페이스 결합 (implements)
클래스는 인터페이스(Interface)를 구현(implements)할 수 있습니다. 이는 "이 클래스는 해당 인터페이스에서 정의한 모든 속성과 메서드를 반드시 갖추겠다"는 약속입니다. 이는 구조적 일관성을 확보하는 데 매우 중요합니다.
// 이동 가능한 객체
interface Drivable {
drive(distance: number): void;
}
// 충전 가능한 객체
interface Chargeable {
charge(amount: number): void;
}
// Drivable + Chargeable 을 동시에 구현하는 전기차
class ElectricCar implements Drivable, Chargeable {
private battery: number = 100; // 배터리 잔량 (%)
private odometer: number = 0; // 총 주행 거리 (km)
drive(distance: number): void {
if (this.battery <= 0) {
console.log('배터리가 없어 운행할 수 없습니다.');
return;
}
this.odometer += distance;
this.battery -= distance * 0.5; // 단순 계산: 1km 주행에 0.5% 소모
if (this.battery < 0) this.battery = 0;
console.log(
`${distance}km 주행 완료 (총 주행 거리: ${this.odometer}km, 배터리: ${this.battery.toFixed(
1,
)}%)`,
);
}
charge(amount: number): void {
this.battery += amount;
if (this.battery > 100) this.battery = 100;
console.log(`충전 완료: 현재 배터리 ${this.battery.toFixed(1)}%`);
}
}
// ✅ 사용 예시
const tesla = new ElectricCar();
tesla.drive(50);
tesla.drive(80);
tesla.charge(30);
tesla.drive(40);
// 🚨 인터페이스 규약 위반 예 (컴파일 에러 예시)
// class Bike implements Drivable {
// // drive를 구현하지 않으면 오류
// }
2. 접근 제어자: 캡슐화의 핵심 (public, private, protected)
객체 지향 프로그래밍(OOP)의 중요한 개념 중 하나는 캡슐화(Encapsulation)입니다.
캡슐화는 클래스의 내부 상태(데이터)를 외부에서 직접 접근하지 못하도록 제한하고, 정해진 메서드를 통해서만 접근하거나 변경할 수 있도록 함으로써 코드의 안정성과 유지보수성을 높입니다.
TypeScript는 이러한 캡슐화를 구현하기 위해 다음과 같은 세 가지 접근 제어자(Access Modifiers)를 제공합니다.
참고: 접근 제어자는 **속성(필드)**과 메서드 모두에 사용할 수 있습니다.
| 제어자 | 설명 | 접근 가능 범위 |
| public | 어디에서든 접근 가능 (기본값) | 클래스 내부, 자식 클래스, 외부 코드 |
| private | 오직 해당 클래스 내부에서만 접근 가능 | 클래스 내부에서만 |
| protected | 해당 클래스 및 자식 클래스 내부에서 접근 가능 | 클래스 내부, 상속받은 클래스 내부 |
🔷 private 사용 예시: 외부 접근 제한하기
class BankAccount {
public ownerName: string; // 어디서든 접근 가능
protected accountNumber: string; // 자식 클래스에서 접근 가능
private balance: number; // 외부에서 직접 접근 불가
constructor(ownerName: string, accountNumber: string, initialBalance: number) {
this.ownerName = ownerName;
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
// private 필드에 접근하기 위한 public 메서드
public getBalance(): number {
return this.balance;
}
public deposit(amount: number): void {
if (amount <= 0) {
console.log('입금액은 0보다 커야 합니다.');
return;
}
this.balance += amount;
console.log(
`${amount.toLocaleString()}원 입금 완료. 현재 잔액: ${this.balance.toLocaleString()}원`,
);
}
public withdraw(amount: number): void {
if (amount <= 0) {
console.log('출금액은 0보다 커야 합니다.');
return;
}
if (amount > this.balance) {
console.log('잔액이 부족합니다.');
return;
}
this.balance -= amount;
console.log(
`${amount.toLocaleString()}원 출금 완료. 현재 잔액: ${this.balance.toLocaleString()}원`,
);
}
}
const account = new BankAccount('김철수', '123-456-7890', 1_000_000);
console.log(account.ownerName); // ✅ public: 접근 가능
// console.log(account.accountNumber); // ❌ protected: 외부에서 접근 불가 (컴파일 에러)
// console.log(account.balance); // ❌ private: 외부에서 접근 불가 (컴파일 에러)
account.deposit(500_000);
account.withdraw(200_000);
console.log('잔액 조회:', account.getBalance());
salary 속성은 외부에서 직접 접근할 수 없지만, getSalaryInfo() 메서드를 통해 안전하게 정보를 조회할 수 있습니다.
이는 정보 은닉과 캡슐화의 전형적인 예시입니다.
🔷 protected 사용 예시: 상속 관계에서 접근 허용
class SafeBankAccount extends BankAccount {
// 부모의 protected accountNumber에 접근 가능
public printMaskedAccount(): void {
// 계좌번호 일부만 보여주기 (마스킹 처리)
const masked = this.accountNumber.replace(/.(?=.{4})/g, '*');
console.log(`계좌번호(마스킹): ${masked}`);
}
}
const safeAccount = new SafeBankAccount('이영희', '987-654-3210', 3_000_000);
safeAccount.printMaskedAccount();
// console.log(safeAccount.accountNumber); // ❌ 여전히 외부에서 직접 접근은 불가
protected는 외부에서는 접근할 수 없지만, 자식 클래스에서는 사용할 수 있는 중간 수준의 보호 방식입니다.
🔷 생성자 매개변수 속성 지정 (Shorthand Property Declaration)
TypeScript는 생성자의 매개변수에 접근 제어자를 붙이면, 해당 매개변수를 자동으로 클래스의 속성으로 선언 및 초기화해 줍니다.
class Product {
// name: public, price: private, category: public(기본값)
constructor(
public name: string,
private price: number,
public category: string = '일반',
) {}
public getPriceWithTax(): number {
// private price는 클래스 내부에서 자유롭게 사용 가능
const taxRate = 0.1; // 10%
return this.price * (1 + taxRate);
}
public getLabel(): string {
return `[${this.category}] ${this.name}`;
}
}
const product = new Product('게이밍 마우스', 50_000, '전자기기');
console.log(product.name); // ✅ public
console.log(product.getLabel());
console.log(product.getPriceWithTax());
// console.log(product.price); // ❌ private이라 직접 접근 불가
3. 클래스 속성을 안전하게 다루는 방법: getter와 setter
TypeScript에서는 클래스의 속성을 간접적으로 읽고 수정할 수 있도록 getter와 setter를 제공합니다.
| 구분 | 설명 |
| get | 속성처럼 읽기 전용 접근을 제공하는 메서드 |
| set | 속성처럼 쓰기(수정) 접근을 제공하는 메서드 |
| 내부 변수 | 주로 _name처럼 언더스코어(_)를 붙여 getter/setter가 감싸는 실제 데이터로 구분 |
| 유효성 검사 | set 안에서 조건을 걸어 잘못된 값을 방지할 수 있음 |
get, set은 실제로는 함수지만, 호출 시에는 함수처럼 보이지 않고 속성처럼 사용됩니다.
✔️ 기본 예제: 이름(name) 속성 관리
class UserName {
private _firstName: string;
private _lastName: string;
constructor(firstName: string, lastName: string) {
this._firstName = firstName;
this._lastName = lastName;
}
// 읽기용 getter: `user.fullName` 형식으로 사용
public get fullName(): string {
return `${this._firstName} ${this._lastName}`;
}
// 쓰기용 setter: "이름 성" 형식의 문자열을 받아 내부를 분리
public set fullName(value: string) {
const parts = value.trim().split(' ');
if (parts.length !== 2) {
throw new Error("fullName은 '이름 성' 형식으로 입력해야 합니다.");
}
this._firstName = parts[0];
this._lastName = parts[1];
}
}
// ✅ 사용 예시
const userName = new UserName('Jimin', 'Park');
console.log(userName.fullName); // getter → "Jimin Park"
userName.fullName = 'Minji Kim'; // setter 호출
console.log(userName.fullName); // "Minji Kim"
// userName.fullName = "잘못된형식"; // ❌ 오류 발생 (예외 던짐)
✔️ 응용 예시: 숫자 범위를 제한하는 setter
class Temperature {
private _celsius: number = 0;
public get celsius(): number {
return this._celsius;
}
public set celsius(value: number) {
if (value < -273.15) {
throw new Error("절대영도 이하의 온도는 설정할 수 없습니다.");
}
this._celsius = value;
}
}
const temp = new Temperature();
temp.celsius = 25; // ✅ setter 사용
console.log(temp.celsius); // ✅ getter 사용 → 출력: 25
// temp.celsius = -300; // ❌ 오류: 절대영도 이하의 온도는 설정 불가
4. 상속과 추상 클래스: 코드 재사용성과 구조 설계
🔷 상속 (Inheritance): 기존 기능을 재사용하기
상속은 기존 클래스의 속성과 메서드를 자식 클래스가 물려받아 재사용하는 기능입니다.
TypeScript에서는 extends 키워드를 사용하여 상속을 구현합니다.
이로 인해 중복 코드를 줄이고, 클래스 간의 관계를 명확히 설계할 수 있습니다.
// 기본 Employee 클래스를 상속하는 Developer, Manager 예제
class EmployeeBase {
constructor(
public name: string,
protected baseSalary: number,
) {}
// 공통 동작
work(): void {
console.log(`${this.name}이(가) 회사에서 일하고 있습니다.`);
}
// 공통 급여 계산 메서드
getMonthlySalary(): number {
return this.baseSalary;
}
}
class Developer extends EmployeeBase {
constructor(
name: string,
baseSalary: number,
public mainLanguage: string,
) {
super(name, baseSalary);
}
// 메서드 오버라이딩
work(): void {
console.log(`${this.name}이(가) ${this.mainLanguage}로 기능을 개발하고 있습니다.`);
}
}
class Manager extends EmployeeBase {
constructor(
name: string,
baseSalary: number,
private teamSize: number,
) {
super(name, baseSalary);
}
// 메서드 오버라이딩
work(): void {
console.log(`${this.name}이(가) ${this.teamSize}명의 팀을 관리하고 있습니다.`);
}
// 추가 규칙: 팀원의 수에 따라 관리 수당
getMonthlySalary(): number {
const bonus = this.teamSize * 100_000;
return this.baseSalary + bonus;
}
}
// 다형성(polymorphism) 예시
const employees: EmployeeBase[] = [
new Developer('개발자A', 4_000_000, 'TypeScript'),
new Manager('매니저B', 5_000_000, 5),
];
for (const e of employees) {
e.work(); // 각 클래스에 맞는 구현이 호출됨
console.log('월 급여:', e.getMonthlySalary().toLocaleString(), '원');
}
🔷 추상 클래스 (Abstract Class): 공통 구조를 설계하기 위한 틀
추상 클래스는 그 자체로는 인스턴스를 만들 수 없고, 오직 상속을 통해 구체적인 클래스를 만드는 용도로만 사용됩니다.
▸ abstract 키워드를 사용합니다.
▸추상 클래스는 추상 메서드 (abstract method)를 가질 수 있습니다.
추상 메서드는 선언부만 있고 구현부(내용)가 없는 메서드로, 이 클래스를 상속받는 모든 자식 클래스가 반드시 구현해야 합니다.
이는 개발자들에게 일종의 규칙과 틀을 제공하여 구조적인 설계를 강제합니다.
// 추상 클래스 Shape와 이를 상속하는 Circle, Rectangle, Triangle 예제
abstract class ShapeBase {
abstract name: string;
// 각 도형이 넓이를 계산하는 방식은 다르므로 추상 메서드로 정의
abstract getArea(): number;
// 공통 동작: 정보 출력
display(): void {
console.log(`도형: ${this.name}, 넓이: ${this.getArea().toFixed(2)}`);
}
}
class Circle2 extends ShapeBase {
name: string = '원';
constructor(private radius: number) {
super();
}
getArea(): number {
return Math.PI * this.radius * this.radius;
}
}
class Rectangle2 extends ShapeBase {
name: string = '직사각형';
constructor(
private width: number,
private height: number,
) {
super();
}
getArea(): number {
return this.width * this.height;
}
}
class Triangle2 extends ShapeBase {
name: string = '삼각형';
constructor(
private base: number,
private height: number,
) {
super();
}
getArea(): number {
return (this.base * this.height) / 2;
}
}
// const s = new ShapeBase(); // ❌ 추상 클래스는 직접 인스턴스화 불가
// 다형성: ShapeBase 타입 배열로 한 번에 처리
const shapes: ShapeBase[] = [new Circle2(5), new Rectangle2(4, 6), new Triangle2(10, 3)];
for (const shape of shapes) {
shape.display(); // 실제 인스턴스 타입에 따라 각자의 getArea()가 호출됨
}
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.