TypeScript의 객체지향 프로그래밍
OOP 캡슐화 (Encapsulation) 상속 (Inheritance) 추상화
프로퍼티, 생성자 (Constructor), 메서드로 구성됩니다
class User {
// 프로퍼티 선언 (타입 필수!)
name: string;
email: string;
age: number;
// 생성자: 클래스로 객체를 만들 때 자동으로 호출되는 함수
constructor(name: string, email: string, age: number) {
this.name = name;
this.email = email;
this.age = age;
}
// 메서드: 클래스 안에 정의된 함수
greet(): string {
return `안녕하세요, ${this.name}입니다!`;
}
}
// new 키워드로 인스턴스(실제 객체) 생성
const user = new User("홍길동", "hong@example.com", 30);
console.log(user.greet()); // "안녕하세요, 홍길동입니다!"
| 제어자 | 클래스 내부 | 자식 클래스 | 외부 |
|---|---|---|---|
| public | O | O | O |
| protected | O | O | X |
| private | O | X | X |
class BankAccount {
public owner: string; // 어디서든 접근 가능 (기본값)
protected balance: number; // 클래스와 자식에서만
private pin: string; // 이 클래스 내부에서만
constructor(owner: string, balance: number, pin: string) {
this.owner = owner;
this.balance = balance;
this.pin = pin;
}
}
class BankAccount {
public owner: string; // 공개: 어디서든 접근 가능
protected balance: number; // 보호: 이 클래스와 자식 클래스에서만
private pin: string; // 비공개: 이 클래스 내부에서만
constructor(owner: string, balance: number, pin: string) {
this.owner = owner;
this.balance = balance;
this.pin = pin;
}
// private 메서드: 클래스 내부에서만 호출 가능
private verifyPin(inputPin: string): boolean {
return this.pin === inputPin;
}
// public 메서드: 외부에서 호출 가능
public withdraw(amount: number, inputPin: string): boolean {
if (!this.verifyPin(inputPin)) return false; // 내부에서 private 호출
if (this.balance < amount) return false;
this.balance -= amount;
return true;
}
}
// 상속: 부모 클래스의 기능을 물려받음
class SavingsAccount extends BankAccount {
getBalance(): number {
return this.balance; // OK: protected는 자식에서 접근 가능
// return this.pin; // Error: private은 자식에서도 접근 불가!
}
}
const account = new BankAccount("홍길동", 10000, "1234");
account.owner; // OK: public이므로 외부에서 접근 가능
// account.balance; // Error: protected - 외부 접근 불가
// account.pin; // Error: private - 외부 접근 불가
readonly 프로퍼티생성 시에만 값을 할당하고, 이후 변경 불가 (읽기 전용)
class Config {
readonly apiUrl: string;
readonly maxRetries: number;
readonly createdAt: Date;
constructor(apiUrl: string, maxRetries: number) {
// 생성자에서만 할당 가능
this.apiUrl = apiUrl;
this.maxRetries = maxRetries;
this.createdAt = new Date();
}
// updateUrl(url: string) {
// this.apiUrl = url; // Error! readonly 변경 불가
// }
}
const config = new Config("https://api.example.com", 3);
console.log(config.apiUrl); // OK: 읽기는 가능
// config.apiUrl = "new-url"; // Error: readonly!
const는 변수에, readonly는 프로퍼티에 사용합니다.
생성자 매개변수에 접근 제한자를 붙이면 자동으로 프로퍼티 선언 + 할당!
class User {
public name: string;
private email: string;
readonly id: number;
constructor(
name: string,
email: string,
id: number
) {
this.name = name;
this.email = email;
this.id = id;
}
}
class User {
constructor(
public name: string,
private email: string,
readonly id: number
) {
// 자동으로 선언 + 할당!
// 본문이 비어있어도 OK
}
}
프로퍼티 접근을 제어하고 유효성 검증 로직을 추가
class Temperature {
private _celsius: number;
constructor(celsius: number) {
this._celsius = celsius;
}
// Getter: 프로퍼티처럼 읽기
get fahrenheit(): number {
return this._celsius * 9 / 5 + 32;
}
// Setter: 프로퍼티처럼 쓰기 + 유효성 검증
set celsius(value: number) {
if (value < -273.15) {
throw new Error("절대영도 이하는 불가능합니다!");
}
this._celsius = value;
}
get celsius(): number {
return this._celsius;
}
}
const temp = new Temperature(100);
console.log(temp.fahrenheit); // 212 (마치 프로퍼티처럼!)
temp.celsius = 0; // setter 호출
// temp.celsius = -300; // Error: 절대영도 이하!
인스턴스가 아닌 클래스 자체에 속하는 멤버
class MathUtils {
static readonly PI = 3.14159265;
static circleArea(radius: number): number {
return MathUtils.PI * radius ** 2;
}
static clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
}
// new 없이 클래스 이름으로 직접 호출!
console.log(MathUtils.PI); // 3.14159265
console.log(MathUtils.circleArea(5)); // 78.539...
console.log(MathUtils.clamp(15, 0, 10)); // 10
// 실전: 싱글톤 패턴 (프로그램 전체에서 딱 하나만 존재하는 객체)
class Database {
private static instance: Database; // 유일한 인스턴스 저장
// private 생성자: 외부에서 new로 생성 못하게 막음
private constructor(private connectionString: string) {}
// 유일한 인스턴스를 반환하는 정적 메서드
static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database("mongodb://localhost:27017");
}
return Database.instance;
}
}
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true (항상 같은 인스턴스!)
직접 인스턴스화할 수 없고, 반드시 상속해서 구현 (Implements)해야 하는 클래스
abstract class Shape {
constructor(public color: string) {}
// 추상 메서드: 구현 없이 시그니처만 정의
abstract getArea(): number;
abstract getPerimeter(): number;
// 일반 메서드: 공통 로직 제공
describe(): string {
return `${this.color} 도형 (면적: ${this.getArea().toFixed(2)})`;
}
}
// const shape = new Shape("red"); // Error! 추상 클래스 인스턴스화 불가
class Circle extends Shape {
constructor(color: string, public radius: number) {
super(color);
}
// 반드시 구현해야 함!
getArea(): number {
return Math.PI * this.radius ** 2;
}
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(color: string, public width: number, public height: number) {
super(color);
}
getArea(): number { return this.width * this.height; }
getPerimeter(): number { return 2 * (this.width + this.height); }
}
const circle = new Circle("파랑", 5);
console.log(circle.describe()); // "파랑 도형 (면적: 78.54)"
| 특성 | 추상 클래스 | 인터페이스 |
|---|---|---|
| 구현 코드 포함 | 가능 (일반 메서드) | 불가능 |
| 다중 상속/구현 | 단일 상속만 | 다중 구현 가능 |
| 생성자 | 가질 수 있음 | 없음 |
| 접근 제한자 | 사용 가능 | public만 |
| 런타임 (Runtime, 실행 시점) 존재 | JavaScript로 컴파일 (Compile, 코드 변환)됨 | 컴파일 시 제거됨 |
| 사용 시기 | 공통 구현 + 틀 제공 | 계약(규격) 정의 |
공통 로직이 있으면 추상 클래스, 순수하게 형태만 정의하면 인터페이스
implements)클래스가 특정 인터페이스의 계약을 이행하도록 강제합니다. "이 클래스는 반드시 이 기능들을 가지고 있어야 한다"는 약속입니다.
interface Serializable {
serialize(): string;
deserialize(data: string): void;
}
interface Loggable {
log(message: string): void;
}
// 다중 인터페이스 구현 가능!
class UserProfile implements Serializable, Loggable {
constructor(
public name: string,
public email: string
) {}
serialize(): string {
return JSON.stringify({ name: this.name, email: this.email });
}
deserialize(data: string): void {
const parsed = JSON.parse(data);
this.name = parsed.name;
this.email = parsed.email;
}
log(message: string): void {
console.log(`[UserProfile] ${message}`);
}
}
// 인터페이스 타입으로 사용 가능
function saveToFile(item: Serializable): void {
const data = item.serialize();
// 파일에 저장...
}
implements는 타입 체크만 합니다. 인터페이스의 메서드를 구현하지 않으면 컴파일 에러!
함수 표현식처럼 클래스도 표현식으로 사용 가능
// 이름 있는 클래스 표현식
const Animal = class AnimalClass {
constructor(public name: string) {}
speak(): string {
return `${this.name}이(가) 소리를 냅니다.`;
}
};
const dog = new Animal("멍멍이");
// new AnimalClass("..."); // Error: 외부에서 클래스 이름 접근 불가
// 실전: 팩토리 패턴에서 유용
function createLogger(prefix: string) {
return class {
log(message: string) {
console.log(`[${prefix}] ${message}`);
}
};
}
const AppLogger = createLogger("APP");
const DbLogger = createLogger("DB");
const logger = new AppLogger();
logger.log("서버 시작됨"); // [APP] 서버 시작됨
this를 타입으로 사용하면 메서드 체이닝에 유용
class QueryBuilder {
private conditions: string[] = [];
private orderByField?: string;
where(condition: string): this {
this.conditions.push(condition);
return this; // this 반환으로 체이닝
}
orderBy(field: string): this {
this.orderByField = field;
return this;
}
build(): string {
let query = "SELECT * FROM table";
if (this.conditions.length > 0) {
query += " WHERE " + this.conditions.join(" AND ");
}
if (this.orderByField) {
query += " ORDER BY " + this.orderByField;
}
return query;
}
}
// 자식 클래스에서도 체이닝이 유지됨!
class AdvancedQueryBuilder extends QueryBuilder {
private limitCount?: number;
limit(count: number): this {
this.limitCount = count;
return this;
}
}
const query = new AdvancedQueryBuilder()
.where("age > 18") // this = AdvancedQueryBuilder
.where("active = true") // this = AdvancedQueryBuilder
.orderBy("name") // this = AdvancedQueryBuilder
.limit(10) // this = AdvancedQueryBuilder
.build();
다중 상속 (Multiple Inheritance)의 대안 - 여러 클래스의 기능을 조합
// 믹스인을 위한 생성자 타입
type Constructor<T = {}> = new (...args: any[]) => T;
// 타임스탬프 믹스인
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
updatedAt = new Date();
touch() {
this.updatedAt = new Date();
}
};
}
// 소프트 삭제 믹스인
function SoftDeletable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
isDeleted = false;
deletedAt?: Date;
softDelete() {
this.isDeleted = true;
this.deletedAt = new Date();
}
};
}
// 기본 클래스
class User {
constructor(public name: string, public email: string) {}
}
// 믹스인 조합!
const EnhancedUser = SoftDeletable(Timestamped(User));
const user = new EnhancedUser("홍길동", "hong@mail.com");
user.touch(); // Timestamped 기능
user.softDelete(); // SoftDeletable 기능
console.log(user.createdAt, user.isDeleted); // 모든 기능 사용 가능
type EventHandler<T = any> = (data: T) => void;
abstract class EventEmitter {
private handlers: Map<string, EventHandler[]> = new Map();
on(event: string, handler: EventHandler): void {
const existing = this.handlers.get(event) || [];
this.handlers.set(event, [...existing, handler]);
}
protected emit(event: string, data?: any): void {
const handlers = this.handlers.get(event) || [];
handlers.forEach(handler => handler(data));
}
off(event: string, handler: EventHandler): void {
const existing = this.handlers.get(event) || [];
this.handlers.set(event, existing.filter(h => h !== handler));
}
}
class ShoppingCart extends EventEmitter {
private items: Array<{ name: string; price: number }> = [];
addItem(name: string, price: number): void {
this.items.push({ name, price });
this.emit("itemAdded", { name, price });
this.emit("totalChanged", this.getTotal());
}
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
const cart = new ShoppingCart();
cart.on("itemAdded", (item) => console.log(`추가됨: ${item.name}`));
cart.on("totalChanged", (total) => console.log(`합계: ${total}원`));
cart.addItem("TypeScript 책", 35000);
public / protected / private로 캡슐화