인터페이스는 "이 객체는 반드시 이런 기능을 가져야 해"라는 약속입니다.
디자인 패턴의 핵심은 구체적인 클래스가 아닌 인터페이스에 의존하는 것입니다.
// ❌ 나쁜 예: 구체적인 클래스에 의존
class KakaoNotifier {
send(msg: string) { console.log(`카카오: ${msg}`) }
}
class App {
notifier = new KakaoNotifier() // ← KakaoNotifier에 묶여버림!
}
// ✅ 좋은 예: 인터페이스에 의존
interface Notifier {
send(message: string): void // 이렇게 동작해야 해!
}
class KakaoNotifier implements Notifier {
send(message: string) { console.log(`카카오: ${message}`) }
}
class EmailNotifier implements Notifier {
send(message: string) { console.log(`이메일: ${message}`) }
}
class App {
// Notifier 인터페이스를 사용 → 어떤 알림이든 교체 가능!
constructor(private notifier: Notifier) {}
notify(msg: string) { this.notifier.send(msg) }
}
const app1 = new App(new KakaoNotifier())
const app2 = new App(new EmailNotifier()) // 코드 변경 없이 교체!
🔑 핵심: 인터페이스는 어떻게 구현할지는 모르고, 무엇을 할 수 있는지만 정의합니다. Strategy, Observer 패턴에서 핵심적으로 사용됩니다.
추상 클래스는 직접 인스턴스를 만들 수 없는 클래스입니다.
공통 기능은 구현하고, 자식 클래스마다 다른 부분은 abstract로 남겨둡니다.
// Template Method 패턴에서 사용!
abstract class Report {
// 공통 흐름 (변하지 않음)
generate(): void {
this.fetchData()
this.format() // ← 각 자식이 다르게 구현
this.print()
}
private fetchData() { console.log("데이터 불러오는 중...") }
private print() { console.log("출력 완료") }
// 자식클래스에서 반드시 구현해야 하는 부분
abstract format(): void
}
class PdfReport extends Report {
format() { console.log("PDF 형식으로 변환") }
}
class ExcelReport extends Report {
format() { console.log("엑셀 형식으로 변환") }
}
const report = new PdfReport()
report.generate()
// 데이터 불러오는 중... → PDF 형식으로 변환 → 출력 완료
🔑 핵심: 인터페이스는 구현이 없고, 추상 클래스는 공통 구현을 가질 수 있습니다. Template Method 패턴의 핵심입니다.
제네릭은 "어떤 타입이든 안전하게 쓸 수 있는 코드"를 만드는 방법입니다.
<T>를 타입의 변수라고 생각하면 쉽습니다.
// T는 나중에 정해지는 타입 (마치 변수처럼)
class Stack<T> {
private items: T[] = []
push(item: T): void {
this.items.push(item)
}
pop(): T | undefined {
return this.items.pop()
}
}
// T = number로 결정됨
const numStack = new Stack<number>()
numStack.push(42)
numStack.push("hello") // ❌ 타입 에러! number가 아님
// T = string으로 결정됨
const strStack = new Stack<string>()
strStack.push("안녕!") // ✅ OK
// Factory 패턴 등에서 제네릭이 자주 사용됩니다
function create<T>(cls: new() => T): T {
return new cls()
}
🔑 핵심: any는 타입 안전성이 없고, Generic은 타입 안전성을 유지하면서 재사용 가능한 코드를 만듭니다.
클래스 내부 데이터를 어디서 접근할 수 있는지 제어합니다. 캡슐화(Encapsulation)의 핵심입니다.
class BankAccount {
public owner: string // 어디서든 접근 가능
private balance: number // 클래스 내부에서만!
protected id: string // 클래스 + 자식 클래스까지
readonly createdAt: Date // 읽기 전용 (변경 불가)
constructor(owner: string, balance: number) {
this.owner = owner
this.balance = balance
this.createdAt = new Date()
this.id = Math.random().toString()
}
// 잔액은 메서드를 통해서만 변경 가능 (안전!)
deposit(amount: number) {
if (amount > 0) this.balance += amount
}
getBalance(): number { return this.balance }
}
const acc = new BankAccount("홍길동", 1000)
acc.owner // ✅ OK
acc.balance // ❌ 에러: private
acc.deposit(500) // ✅ OK
acc.getBalance() // ✅ 1500
// 생성자 단축 문법 (더 깔끔!)
class User {
constructor(
public name: string, // 자동으로 this.name 생성
private age: number
) {}
}
🔑 핵심: Singleton 패턴에서 private constructor를 사용해 외부에서 new를 못 하게 막습니다!
|는 "이것 또는 저것", &는 "이것 이면서 저것"입니다.
// 유니온 타입 (OR): string 또는 number
type ID = string | number
const userId: ID = "U001" // ✅
const postId: ID = 42 // ✅
// 좁히기 (Narrowing)
function printId(id: ID) {
if (typeof id === "string") {
console.log(id.toUpperCase()) // string임이 확실
} else {
console.log(id.toFixed(2)) // number임이 확실
}
}
// 교차 타입 (AND): 두 타입을 합침
interface Serializable { serialize(): string }
interface Printable { print(): void }
type Document = Serializable & Printable
class Report implements Serializable, Printable {
serialize() { return JSON.stringify(this) }
print() { console.log(this.serialize()) }
}
readonly는 값을 변경 불가로 만들고, ?는 없어도 되는 옵션 속성입니다.
interface Config {
readonly apiUrl: string // 반드시 있어야 하고, 변경 불가
timeout?: number // 있어도 되고 없어도 됨
retries?: number
}
const config: Config = {
apiUrl: "https://api.example.com"
// timeout, retries는 생략 가능!
}
config.apiUrl = "..." // ❌ 에러: readonly라 변경 불가
// Prototype 패턴에서 객체 복사 시 readonly 주의 필요
type Immutable<T> = { readonly [K in keyof T]: T[K] }
// 모든 속성을 readonly로!
// optional을 활용하는 Builder 패턴
interface UserOptions {
name: string
email?: string
role?: 'admin' | 'user' | 'guest'
}
function createUser(options: UserOptions) {
return {
name: options.name,
email: options.email ?? 'no-reply@example.com',
role: options.role ?? 'user' // null 병합 연산자
}
}
디자인 패턴의 핵심은 다형성(Polymorphism) — 같은 인터페이스로 다른 객체를 다루는 것입니다.
// extends: 클래스를 상속 (부모의 기능을 물려받음)
// implements: 인터페이스를 구현 (약속을 지킴)
interface Animal {
speak(): string
}
abstract class Pet implements Animal {
constructor(public name: string) {}
abstract speak(): string // 자식이 구현
greeting() { return `나는 ${this.name}` }
}
class Dog extends Pet {
speak() { return "멍멍!" }
}
class Cat extends Pet {
speak() { return "야옹~" }
}
// 다형성: 같은 Animal 타입으로 다른 동물을 다룸
const pets: Animal[] = [new Dog("바둑이"), new Cat("나비")]
pets.forEach(pet => console.log(pet.speak()))
// 멍멍! / 야옹~
// → 이게 바로 디자인 패턴의 핵심!
// 새로운 동물을 추가해도 기존 코드를 수정할 필요가 없습니다 (OCP)
🔑 핵심: 이 개념이 Strategy, Observer, Command 등 거의 모든 디자인 패턴의 기반입니다!