iBetter Books
수정

Ch 02. 제네릭 인터페이스와 클래스

함수뿐 아니라 인터페이스와 클래스에도 타입 매개변수를 붙일 수 있습니다. 데이터를 담는 컨테이너나 공통 패턴을 가진 도구들을 재사용 가능하게 만드는 데 필수적인 기법입니다.

제네릭 인터페이스

// 파일: src/generics/generic-interface.ts// 제네릭 응답 래퍼 — API 응답에 자주 쓰는 패턴interface ApiResponse<T> {  data: T;  status: number;  message: string;  timestamp: Date;}interface User {  id: number;  name: string;  email: string;}interface Product {  id: number;  title: string;  price: number;}// 동일한 ApiResponse 인터페이스로 다양한 응답 표현const userResponse: ApiResponse<User> = {  data: { id: 1, name: "Alice", email: "[email protected]" },  status: 200,  message: "OK",  timestamp: new Date(),};const productResponse: ApiResponse<Product[]> = {  data: [    { id: 1, title: "TypeScript 책", price: 30000 },    { id: 2, title: "JavaScript 책", price: 25000 },  ],  status: 200,  message: "OK",  timestamp: new Date(),};

제네릭 인터페이스로 페이지네이션 표현하기

// 파일: src/generics/pagination.tsinterface Page<T> {  items: T[];  total: number;  page: number;  pageSize: number;  hasNext: boolean;  hasPrev: boolean;}interface Cursor<T> {  items: T[];  nextCursor: string | null;  prevCursor: string | null;}// 페이지네이션을 처리하는 제네릭 함수function paginate<T>(items: T[], page: number, pageSize: number): Page<T> {  const start = (page - 1) * pageSize;  const end = start + pageSize;  const paginatedItems = items.slice(start, end);  return {    items: paginatedItems,    total: items.length,    page,    pageSize,    hasNext: end < items.length,    hasPrev: page > 1,  };}

Repository 패턴 — 제네릭 클래스

제네릭 클래스는 데이터 접근 계층을 추상화할 때 특히 유용합니다.

// 파일: src/generics/repository.tsinterface Entity {  id: number;}class Repository<T extends Entity> {  private items: Map<number, T> = new Map();  private nextId = 1;  create(data: Omit<T, "id">): T {    const id = this.nextId++;    const item = { ...data, id } as T;    this.items.set(id, item);    return item;  }  findById(id: number): T | undefined {    return this.items.get(id);  }  findAll(): T[] {    return Array.from(this.items.values());  }  update(id: number, data: Partial<Omit<T, "id">>): T | undefined {    const existing = this.items.get(id);    if (!existing) return undefined;    const updated = { ...existing, ...data } as T;    this.items.set(id, updated);    return updated;  }  delete(id: number): boolean {    return this.items.delete(id);  }  count(): number {    return this.items.size;  }}// 사용interface User extends Entity {  name: string;  email: string;}interface Post extends Entity {  title: string;  content: string;  authorId: number;}const userRepo = new Repository<User>();const postRepo = new Repository<Post>();const alice = userRepo.create({ name: "Alice", email: "[email protected]" });const bob = userRepo.create({ name: "Bob", email: "[email protected]" });const post = postRepo.create({  title: "TypeScript 제네릭",  content: "제네릭은 강력합니다.",  authorId: alice.id,});console.log(userRepo.findById(1)); // { id: 1, name: "Alice", ... }console.log(postRepo.findAll());   // [{ id: 1, title: "TypeScript 제네릭", ... }]

제네릭 클래스의 상속

// 파일: src/generics/generic-inheritance.tsclass TimestampedRepository<T extends Entity> extends Repository<T> {  private timestamps: Map<number, { created: Date; updated: Date }> = new Map();  create(data: Omit<T, "id">): T {    const item = super.create(data);    this.timestamps.set(item.id, {      created: new Date(),      updated: new Date(),    });    return item;  }  update(id: number, data: Partial<Omit<T, "id">>): T | undefined {    const item = super.update(id, data);    if (item) {      const ts = this.timestamps.get(id);      if (ts) {        ts.updated = new Date();      }    }    return item;  }  getTimestamps(id: number) {    return this.timestamps.get(id);  }}

제네릭으로 Stack 구현하기

// 파일: src/generics/stack.tsclass Stack<T> {  private items: T[] = [];  push(item: T): void {    this.items.push(item);  }  pop(): T | undefined {    return this.items.pop();  }  peek(): T | undefined {    return this.items[this.items.length - 1];  }  isEmpty(): boolean {    return this.items.length === 0;  }  get size(): number {    return this.items.length;  }  toArray(): T[] {    return [...this.items];  }}// 숫자 스택const numStack = new Stack<number>();numStack.push(1);numStack.push(2);numStack.push(3);console.log(numStack.pop()); // 3// 문자열 스택const strStack = new Stack<string>();strStack.push("first");strStack.push("second");console.log(strStack.peek()); // "second"

제네릭 인터페이스로 전략 패턴 구현하기

// 파일: src/generics/strategy.tsinterface Sorter<T> {  sort(items: T[]): T[];  compare(a: T, b: T): number;}class NumberSorter implements Sorter<number> {  sort(items: number[]): number[] {    return [...items].sort(this.compare);  }  compare(a: number, b: number): number {    return a - b;  }}class StringSorter implements Sorter<string> {  sort(items: string[]): string[] {    return [...items].sort(this.compare);  }  compare(a: string, b: string): number {    return a.localeCompare(b);  }}function sortAndFirst<T>(items: T[], sorter: Sorter<T>): T | undefined {  const sorted = sorter.sort(items);  return sorted[0];}const smallest = sortAndFirst([3, 1, 4, 1, 5], new NumberSorter()); // 1const first = sortAndFirst(["banana", "apple", "cherry"], new StringSorter()); // "apple"

제네릭 인터페이스와 클래스는 단순한 타입 재사용을 넘어서 아키텍처 수준의 추상화를 가능하게 합니다. Repository, Container, Strategy 같은 패턴을 제네릭으로 구현하면 타입 안전성을 유지하면서 여러 도메인에서 같은 구조를 재사용할 수 있습니다.