๐Ÿ›๏ธ 14. ํด๋ž˜์Šค โ€” ํ”„๋กœํ† ํƒ€์ž…์˜ ์ง„ํ™”, ๊ทธ๋ฆฌ๊ณ  ํ˜„๋Œ€์  OOP์˜ ์‹ค๋ฌด ํ™œ์šฉ

2026๋…„ 3์›” 8์ผ ์ˆ˜์ •๋จ

๐Ÿ“‹ ๊ฐœ์š”

ES6+ ํด๋ž˜์Šค์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ(private ํ•„๋“œ, static, getter/setter), ๋ฏน์Šค์ธ ํŒจํ„ด, TypeScript์™€์˜ ์—ฐ๊ฒฐ๊ณ ๋ฆฌ๋ฅผ ์‹ค๋ฌด ์ค‘์‹ฌ์œผ๋กœ ์ •๋ณตํ•ฉ๋‹ˆ๋‹ค.

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • class์˜ ๋ชจ๋“  ๋ฌธ๋ฒ•(private ํ•„๋“œ, static, getter/setter, ์ƒ์†)์„ ์‹ค๋ฌด์— ์ ์šฉํ•œ๋‹ค.
  • ๋ฏน์Šค์ธ(Mixin) ํŒจํ„ด์œผ๋กœ ๋‹ค์ค‘ ์ƒ์† ์—†์ด ๊ธฐ๋Šฅ์„ ์กฐํ•ฉํ•œ๋‹ค.
  • TypeScript์˜ interface, abstract class์™€ JS class์˜ ๊ด€๊ณ„๋ฅผ ์ดํ•ดํ•œ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 20๋ถ„(์ „์ฒด) / ํ•ต์‹ฌ ํŒŒํŠธ๋งŒ: 12๋ถ„

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
[ํด๋ž˜์Šค ๋ฌธ๋ฒ• ์ „์ฒด] โ†’ [private/#] โ†’ [์ƒ์†/super] โ†’ [๋ฏน์Šค์ธ] โ†’ [TypeScript ์—ฐ๊ฒฐ]

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • #privateField๋กœ ์™„๋ฒฝํ•œ ์บก์Аํ™”๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค.
  • ๋ฏน์Šค์ธ ํŒจํ„ด์œผ๋กœ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ธฐ๋Šฅ์„ ์กฐํ•ฉํ•œ๋‹ค.
  • TypeScript abstract class์™€ JS class์˜ ์ฐจ์ด๋ฅผ ์„ค๋ช…ํ•œ๋‹ค.

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

๐Ÿฃ ์˜์ฒ : "์˜ํ˜ธ ๋‹˜, ํŒ€์—์„œ class๋ฅผ ์จ์„œ ์„œ๋น„์Šค ๋ ˆ์ด์–ด๋ฅผ ๊ตฌ์กฐํ™”ํ•˜์ž๋Š” ์ด์•ผ๊ธฐ๊ฐ€ ๋‚˜์™”๋Š”๋ฐ... ํด๋ž˜์Šค ์ƒ์†์ด ๊นŠ์–ด์ง€๋ฉด ๊ด€๋ฆฌ๊ฐ€ ์–ด๋ ต๋‹ค๋Š” ๋ง๋„ ์žˆ๊ณ , ๋ฏน์Šค์ธ์ด๋ผ๋Š” ํŒจํ„ด๋„ ์žˆ๋‹ค๋Š”๋ฐ ์ž˜ ๋ชจ๋ฅด๊ฒ ์–ด์š”. ๊ทธ๋ฆฌ๊ณ  TypeScript์—์„œ abstract class๋ž‘ interface๋Š” ๋ญ๊ฐ€ ๋‹ค๋ฅธ ๊ฑด์ง€๋„ ํ—ท๊ฐˆ๋ ค์š”."

๐Ÿฆ ์˜ํ˜ธ: "ํด๋ž˜์Šค ์ƒ์†์€ 'ํ•  ์ˆ˜ ์žˆ์œผ๋ฉด' 2๋‹จ๊ณ„๊นŒ์ง€๊ฐ€ ํ•œ๊ณ„์•ผ. 3๋‹จ๊ณ„ ์ด์ƒ์ด ๋˜๋ฉด ๋ณ€๊ฒฝ ์˜ํ–ฅ ๋ฒ”์œ„๊ฐ€ ํญ๋ฐœ์ ์œผ๋กœ ์ปค์ ธ. ๋ฏน์Šค์ธ์€ '์ƒ์† ์—†์ด ๊ธฐ๋Šฅ์„ ๋นŒ๋ ค์˜ค๋Š”' ๋ฐฉ๋ฒ•์ด์•ผ. ์˜ค๋Š˜ ํด๋ž˜์Šค ์ „์ฒด๋ฅผ ์งš์–ด๋ณด๋ฉด์„œ, ํ”„๋กœํ† ํƒ€์ž… ์ฑ•ํ„ฐ์—์„œ ๋ฐฐ์šด ๋‚ด๋ถ€ ๊ตฌ์กฐ๋„ ๋‹ค์‹œ ์—ฐ๊ฒฐํ•ด๋ณด์ž."


๐Ÿค” 1. ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€?

์˜ํ˜ธ ๋ฆฌ๋“œ ๋‹˜์ด "์„œ๋น„์Šค ๋ ˆ์ด์–ด๋ฅผ class๋กœ ์งœ์ž"๊ณ  ํ–ˆ์„ ๋•Œ, ์˜์ฒ ์ด๋Š” ์ฒ˜์Œ์— "๊ทธ๋ƒฅ ํ•จ์ˆ˜๋กœ ์งœ๋ฉด ์•ˆ ๋˜๋‚˜์š”?"๋ผ๊ณ  ํ–ˆ๋‹ค. ์˜ํ˜ธ ๋‹˜์˜ ๋‹ต: "ํ•จ์ˆ˜๋กœ ์งœ๋ฉด ๊ณตํ†ต ๋กœ์ง โ€” HTTP, ์—๋Ÿฌ ๋กœ๊น…, ์บ์‹ฑ โ€” ์„ ์–ด๋””์— ๋‘๋Š”์ง€๊ฐ€ ๋ถˆ๋ช…ํ™•ํ•ด์ ธ. ํด๋ž˜์Šค๋Š” ์ƒํƒœ์™€ ํ–‰๋™์„ ํ•จ๊ป˜ ๋ฌถ๋Š” ๊ทธ๋ฆ‡์ด์•ผ." ํด๋ž˜์Šค๋Š” JS ์ฝ”๋“œ ์กฐ์งํ™”์˜ ํ•ต์‹ฌ์ด๋‹ค:

  • ์„œ๋น„์Šค ๋ ˆ์ด์–ด: UserService, PostService, AuthService
  • ์—๋Ÿฌ ๊ณ„์ธต: ์•ž์„œ ๋งŒ๋“  AppError extends Error
  • ์ƒํƒœ ๊ด€๋ฆฌ: Redux ์—†์ด ์บก์Аํ™”๋œ ์ƒํƒœ๋ฅผ ๊ฐ€์ง„ ํด๋ž˜์Šค
  • React: class ์ปดํฌ๋„ŒํŠธ ๋ ˆ๊ฑฐ์‹œ ์ดํ•ด, ErrorBoundary ๊ตฌํ˜„

๐Ÿ—๏ธ 2. ํด๋ž˜์Šค ๊ธฐ๋ณธ ๋ฌธ๋ฒ• ์™„์ „ ์ •๋ณต

constructor์™€ ์ธ์Šคํ„ด์Šค ํ•„๋“œ

๊ฐ์ฒด๊ฐ€ ํƒœ์–ด๋‚  ๋•Œ ํ•„์š”ํ•œ ์žฌ๋ฃŒ๋ฅผ ์ค€๋น„ํ•˜๊ณ , ๊ธฐ๋ณธ ์ƒํƒœ๋ฅผ ์ •์˜ํ•˜๋Š” ๊ฐ€์žฅ ๊ธฐ์ดˆ์ ์ธ ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค.

class Post {
  // ํด๋ž˜์Šค ํ•„๋“œ ์„ ์–ธ (ES2022)
  viewCount = 0;        // ํผ๋ธ”๋ฆญ ์ธ์Šคํ„ด์Šค ํ•„๋“œ
  likeCount = 0;
  comments = [];
 
  constructor(id, title, authorId) {
    this.id = id;
    this.title = title;
    this.authorId = authorId;
    this.createdAt = new Date();
  }
 
  // ํ”„๋กœํ† ํƒ€์ž… ๋ฉ”์„œ๋“œ (๋ชจ๋“  ์ธ์Šคํ„ด์Šค๊ฐ€ ๊ณต์œ )
  addView() {
    this.viewCount++;
    return this;  // ๋ฉ”์„œ๋“œ ์ฒด์ด๋‹์„ ์œ„ํ•ด this ๋ฐ˜ํ™˜
  }
 
  addLike() {
    this.likeCount++;
    return this;
  }
 
  // toString ์˜ค๋ฒ„๋ผ์ด๋“œ
  toString() {
    return `[Post #${this.id}] ${this.title} (์กฐํšŒ: ${this.viewCount})`;
  }
}
 
const post = new Post(1, "ํด๋กœ์ € ์™„์ „์ •๋ณต", 42);
post.addView().addView().addLike(); // ์ฒด์ด๋‹
console.log(post.toString()); // "[Post #1] ํด๋กœ์ € ์™„์ „์ •๋ณต (์กฐํšŒ: 2)"

Private ํ•„๋“œ (#)

์˜์ฒ ์ด๊ฐ€ PostManager ๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ์— ์ง์ ‘ ์ ‘๊ทผํ•˜๋ ค๋‹ค ์—๋Ÿฌ๋ฅผ ๋งŒ๋‚ฌ์„ ๋•Œ, ์˜ํ˜ธ ๋ฆฌ๋“œ ๋‹˜์ด ํ•ด์ค€ ์กฐ์–ธ์ž…๋‹ˆ๋‹ค. "์ด ์ฃผ๋จธ๋‹ˆ๋Š” ํด๋ž˜์Šค ์•ˆ์—์„œ๋งŒ ์“ธ ์ˆ˜ ์žˆ๋Š” ๋น„๋ฐ€ ๊ณต๊ฐ„์ด์—์š”. ๋ฐ–์—์„œ๋Š” ์ ˆ๋Œ€ ์—ด์–ด๋ณผ ์ˆ˜ ์—†์ฃ ."

// ES2022 private ํ•„๋“œ โ€” ์ง„์ •ํ•œ ์บก์Аํ™”
class PostManager {
  // # ๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•„๋“œ๋Š” ํด๋ž˜์Šค ์™ธ๋ถ€์—์„œ ์ ‘๊ทผ ๋ถˆ๊ฐ€
  #posts = new Map();
  #maxPosts;
  #adminIds = new Set();
 
  constructor(maxPosts = 1000) {
    this.#maxPosts = maxPosts;
  }
 
  addPost(post) {
    if (this.#posts.size >= this.#maxPosts) {
      throw new Error(`์ตœ๋Œ€ ๊ฒŒ์‹œ๊ธ€ ์ˆ˜(${this.#maxPosts})๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค.`);
    }
    this.#posts.set(post.id, post);
    return this;
  }
 
  // private ๋ฉ”์„œ๋“œ
  #validatePost(post) {
    if (!post.title?.trim()) throw new Error("์ œ๋ชฉ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.");
    return true;
  }
 
  getPost(id) {
    return this.#posts.get(id) ?? null;
  }
 
  get size() {
    return this.#posts.size; // getter๋กœ๋งŒ ๋…ธ์ถœ
  }
}
 
const manager = new PostManager(100);
manager.addPost({ id: 1, title: "ํด๋กœ์ €" });
manager.size; // 1
 
// ์™ธ๋ถ€์—์„œ ์ ‘๊ทผ ๋ถˆ๊ฐ€
manager.#posts;      // SyntaxError: Private field '#posts' must be declared
manager.#maxPosts;   // SyntaxError
manager.#adminIds;   // SyntaxError
 
// in ์—ฐ์‚ฐ์ž๋กœ private ํ•„๋“œ ์กด์žฌ ํ™•์ธ (ES2022)
#posts in manager; // true

static ๋ฉ”์„œ๋“œ์™€ ํ•„๋“œ

๊ฐœ๋ณ„ ์ธ์Šคํ„ด์Šค๊ฐ€ ์•„๋‹ˆ๋ผ ํด๋ž˜์Šค ๊ทธ ์ž์ฒด์— ์†ํ•˜๋Š” ๋„๊ตฌ๋“ค์ž…๋‹ˆ๋‹ค. ๊ณต์šฉ ์นดํƒˆ๋กœ๊ทธ๋‚˜ ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค ๋•Œ ๋งค์šฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

class User {
  // static ํ•„๋“œ โ€” ํด๋ž˜์Šค ์ž์ฒด์— ์†ํ•จ (์ธ์Šคํ„ด์Šค ๊ณต์œ  ์•ˆ ํ•จ)
  static #count = 0;
  static MAX_LOGIN_ATTEMPTS = 5;
 
  constructor(name, email) {
    this.name = name;
    this.email = email;
    this.id = ++User.#count; // static ํ•„๋“œ ์ ‘๊ทผ
  }
 
  // static ๋ฉ”์„œ๋“œ โ€” ์ธ์Šคํ„ด์Šค ์—†์ด ํด๋ž˜์Šค๋กœ ์ง์ ‘ ํ˜ธ์ถœ
  static getCount() {
    return User.#count;
  }
 
  static fromJSON(jsonString) {
    const { name, email } = JSON.parse(jsonString);
    return new User(name, email); // ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ
  }
 
  static validate(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }
}
 
// ์‚ฌ์šฉ
const u1 = new User("์˜์ฒ ", "yc@example.com");
const u2 = new User("์˜ํ˜ธ", "yh@example.com");
User.getCount(); // 2
 
// ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ โ€” JSON์—์„œ ๊ฐ์ฒด ์ƒ์„ฑ
const u3 = User.fromJSON('{"name":"์˜์ˆ˜","email":"ys@example.com"}');
 
// ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
User.validate("invalid-email"); // false
User.validate("yc@example.com"); // true

getter / setter

๋‹จ์ˆœํžˆ ๊ฐ’์„ ์ฝ๊ณ  ์“ฐ๋Š” ๊ฒƒ์„ ๋„˜์–ด, ๊ทธ ๊ณผ์ •์— '๋ฌธ์ง€๊ธฐ'๋ฅผ ์„ธ์šฐ๋Š” ์ž‘์—…์ž…๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ค๋Š” ๊ฒƒ์„ ๋ง‰๊ณ , ํ•„์š”ํ•œ ๊ณ„์‚ฐ์„ ์†์„ฑ์ฒ˜๋Ÿผ ๋…ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

class Post {
  #_title = "";
  #_content = "";
  #isPublished = false;
  #publishedAt = null;
 
  constructor(title, content) {
    this.title = title;   // setter ํ˜ธ์ถœ
    this.content = content;
  }
 
  // getter โ€” ๊ณ„์‚ฐ๋œ ๊ฐ’์„ ์†์„ฑ์ฒ˜๋Ÿผ ๋…ธ์ถœ
  get wordCount() {
    return this.#_content.split(/\s+/).filter(Boolean).length;
  }
 
  get readingTime() {
    return Math.ceil(this.wordCount / 200); // ๋ถ„๋‹น 200๋‹จ์–ด ๊ธฐ์ค€
  }
 
  // getter + setter โ€” ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํฌํ•จ
  get title() {
    return this.#_title;
  }
 
  set title(value) {
    const trimmed = value?.trim();
    if (!trimmed) throw new Error("์ œ๋ชฉ์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
    if (trimmed.length > 100) throw new Error("์ œ๋ชฉ์€ 100์ž ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.");
    this.#_title = trimmed;
  }
 
  get isPublished() {
    return this.#isPublished;
  }
 
  // setter ์—†์Œ โ€” publish() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด์„œ๋งŒ ๋ณ€๊ฒฝ (๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ณดํ˜ธ)
  publish() {
    if (!this.#_title || !this.#_content) {
      throw new Error("์ œ๋ชฉ๊ณผ ๋‚ด์šฉ์ด ์žˆ์–ด์•ผ ๋ฐœํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.");
    }
    this.#isPublished = true;
    this.#publishedAt = new Date();
  }
}
 
const post = new Post("ํด๋กœ์ € ์™„์ „์ •๋ณต", "๋‚ด์šฉ์ด ๋งค์šฐ ๊ธธ๋‹ค...");
post.readingTime; // ๊ณ„์‚ฐ๋œ ๊ฐ’์„ ์†์„ฑ์ฒ˜๋Ÿผ ์ ‘๊ทผ
post.title = ""; // Error: ์ œ๋ชฉ์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
post.publish();
post.isPublished; // true

๐Ÿงฌ 3. ์ƒ์†๊ณผ super

์˜ํ˜ธ ๋ฆฌ๋“œ ๋‹˜์ด ๋Š˜ ๊ฐ•์กฐํ•˜๋Š” "์ƒ์†์€ 2๋‹จ๊ณ„๊นŒ์ง€๋งŒ"์ด๋ผ๋Š” ๊ทœ์น™์„ ๊ธฐ์–ตํ•˜๋ฉฐ, ์–ด๋–ป๊ฒŒ ๋ถ€๋ชจ์˜ ์œ ์‚ฐ์„ ์ง€ํ˜œ๋กญ๊ฒŒ ๋ฌผ๋ ค๋ฐ›๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

class BaseService {
  #baseUrl;
  #defaultHeaders;
 
  constructor(baseUrl) {
    this.#baseUrl = baseUrl;
    this.#defaultHeaders = { "Content-Type": "application/json" };
  }
 
  async request(method, endpoint, data = null) {
    const response = await fetch(`${this.#baseUrl}${endpoint}`, {
      method,
      headers: this.#defaultHeaders,
      body: data ? JSON.stringify(data) : null,
    });
 
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
 
    return response.json();
  }
 
  async get(endpoint) { return this.request("GET", endpoint); }
  async post(endpoint, data) { return this.request("POST", endpoint, data); }
  async put(endpoint, data) { return this.request("PUT", endpoint, data); }
  async delete(endpoint) { return this.request("DELETE", endpoint); }
}
 
class PostService extends BaseService {
  constructor() {
    super("https://api.youngsu.com"); // ๋ถ€๋ชจ constructor ํ˜ธ์ถœ (ํ•„์ˆ˜!)
  }
 
  async getPosts(page = 1) {
    return super.get(`/posts?page=${page}`); // ๋ถ€๋ชจ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ
  }
 
  async createPost(data) {
    return super.post("/posts", data);
  }
 
  async getByAuthor(authorId) {
    return super.get(`/users/${authorId}/posts`);
  }
}
 
class AdminPostService extends PostService {
  async deletePost(postId) {
    return super.delete(`/posts/${postId}`);
  }
 
  async bulkDelete(postIds) {
    return super.post("/posts/bulk-delete", { ids: postIds });
  }
}
 
// ์‚ฌ์šฉ
const postService = new PostService();
const posts = await postService.getPosts(1);

์ƒ์† ๊นŠ์ด ์›์น™: BaseService โ†’ PostService โ†’ AdminPostService โ€” 3๋‹จ๊ณ„. ์ด ์ด์ƒ์€ ๊ด€๋ฆฌ๊ฐ€ ์–ด๋ ค์›Œ์ง„๋‹ค. 4๋‹จ๊ณ„ ์ด์ƒ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ๋ฏน์Šค์ธ์ด๋‚˜ ์ปดํฌ์ง€์…˜์„ ๊ฒ€ํ† ํ•˜๋ผ.


๐Ÿงฉ 4. ๋ฏน์Šค์ธ ํŒจํ„ด

์˜์ˆ˜ PM๋‹˜์ด "๊ณตํ†ต ๊ธฐ๋Šฅ์„ ์—ฌ๋Ÿฌ ํด๋ž˜์Šค์— ๋„ฃ์–ด์•ผ ํ•˜๋Š”๋ฐ, ์ƒ์†๋งŒ์œผ๋กœ๋Š” ๋ถ€๋ชจ๊ฐ€ ๋„ˆ๋ฌด ๋น„๋Œ€ํ•ด์งˆ ๊ฒƒ ๊ฐ™์•„์š”"๋ผ๊ณ  ๊ฑฑ์ •ํ•  ๋•Œ, ์˜ํ˜ธ ๋ฆฌ๋“œ ๋‹˜์ด ์ œ์‹œํ•œ ํ•ด๋ฒ•์ž…๋‹ˆ๋‹ค. ํ•„์š”ํ•œ '๋Šฅ๋ ฅ'๋งŒ ์™์™ ๊ณจ๋ผ ์กฐ๋ฆฝํ•˜๋Š” ํŒจํ„ด์ด์ฃ .

// ์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ โ€” ๋‹ค์–‘ํ•œ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๊ณตํ†ต ๊ธฐ๋Šฅ์„ ์กฐํ•ฉ
 
// ํƒ€์ž„์Šคํƒฌํ”„ ๋ฏน์Šค์ธ
const Timestamped = (Base) =>
  class extends Base {
    createdAt = new Date();
    updatedAt = new Date();
 
    touch() {
      this.updatedAt = new Date();
      return this;
    }
  };
 
// ์†Œํ”„ํŠธ ๋”œ๋ฆฌํŠธ ๋ฏน์Šค์ธ
const SoftDeletable = (Base) =>
  class extends Base {
    #deletedAt = null;
 
    get isDeleted() {
      return this.#deletedAt !== null;
    }
 
    softDelete() {
      this.#deletedAt = new Date();
      return this;
    }
 
    restore() {
      this.#deletedAt = null;
      return this;
    }
  };
 
// ์ง๋ ฌํ™” ๋ฏน์Šค์ธ
const Serializable = (Base) =>
  class extends Base {
    toJSON() {
      return JSON.parse(JSON.stringify(this));
    }
 
    static fromJSON(json) {
      return Object.assign(new this(), JSON.parse(json));
    }
  };
 
// ๊ธฐ๋ฐ˜ ํด๋ž˜์Šค
class Entity {
  constructor(id) {
    this.id = id;
  }
}
 
// ๋ฏน์Šค์ธ ์กฐํ•ฉ โ€” Post๋Š” ํƒ€์ž„์Šคํƒฌํ”„, ์†Œํ”„ํŠธ ๋”œ๋ฆฌํŠธ, ์ง๋ ฌํ™” ๊ธฐ๋Šฅ์„ ๋ชจ๋‘ ๊ฐ€์ง
class Post extends Serializable(SoftDeletable(Timestamped(Entity))) {
  constructor(id, title, authorId) {
    super(id);
    this.title = title;
    this.authorId = authorId;
  }
}
 
const post = new Post(1, "ํด๋กœ์ € ์™„์ „์ •๋ณต", 42);
post.createdAt;     // Date (Timestamped)
post.isDeleted;     // false (SoftDeletable)
post.softDelete();  // ์†Œํ”„ํŠธ ๋”œ๋ฆฌํŠธ (SoftDeletable)
post.touch();       // updatedAt ๊ฐฑ์‹  (Timestamped)
post.toJSON();      // JSON ์ง๋ ฌํ™” (Serializable)

๐Ÿ”ท 5. TypeScript์™€์˜ ์—ฐ๊ฒฐ๊ณ ๋ฆฌ

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํด๋ž˜์Šค๊ฐ€ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์˜ ์˜ท์„ ์ž…์—ˆ์„ ๋•Œ ์–ด๋–ค ์ฐจ์ด๊ฐ€ ์ƒ๊ธฐ๋Š”์ง€, abstract์™€ interface๋ฅผ ์–ด๋–ป๊ฒŒ ๊ตฌ๋ถ„ํ•ด์„œ ์“ฐ๋Š”์ง€ ์ •๋ฆฌํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

// TypeScript์—์„œ์˜ ํด๋ž˜์Šค ํ™œ์šฉ
 
// interface โ€” ๊ตฌ์กฐ(๋ชจ์–‘)๋งŒ ์ •์˜, ๋Ÿฐํƒ€์ž„์— ์กด์žฌ ์•ˆ ํ•จ
interface Postable {
  id: number;
  title: string;
  publish(): void;
}
 
// abstract class โ€” ์ผ๋ถ€ ๊ตฌํ˜„ ํฌํ•จ ๊ฐ€๋Šฅ, new ๋ถˆ๊ฐ€, ๋Ÿฐํƒ€์ž„์— ์กด์žฌ
abstract class BasePost {
  abstract validate(): boolean; // ์„œ๋ธŒํด๋ž˜์Šค์—์„œ ๋ฐ˜๋“œ์‹œ ๊ตฌํ˜„
 
  publish() {
    if (!this.validate()) throw new Error("์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ");
    console.log("๋ฐœํ–‰!");
  }
}
 
// ๊ตฌ์ฒด ํด๋ž˜์Šค
class BlogPost extends BasePost implements Postable {
  constructor(
    public id: number,
    public title: string,
    private content: string
  ) {
    super();
  }
 
  validate(): boolean {
    return this.title.length > 0 && this.content.length > 0;
  }
}
 
// interface vs abstract class ์„ ํƒ ๊ธฐ์ค€:
// interface: ๊ตฌ์กฐ๋งŒ ๊ฐ•์ œํ•  ๋•Œ, ๋‹ค์ค‘ ๊ตฌํ˜„ ํ—ˆ์šฉ
// abstract class: ๊ณตํ†ต ๊ตฌํ˜„(๋ฉ”์„œ๋“œ ๋ฐ”๋””)์„ ์ œ๊ณตํ•˜๋ฉด์„œ ์ผ๋ถ€๋Š” ์„œ๋ธŒํด๋ž˜์Šค์— ๊ฐ•์ œํ•  ๋•Œ

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. #privateField์™€ ํด๋กœ์ €๋กœ ๋งŒ๋“  private ๋ณ€์ˆ˜์˜ ์ฐจ์ด๋Š”?

โœ… ์ •๋‹ต: #privateField๋Š” ํด๋ž˜์Šค ๋ฌธ๋ฒ• ์ˆ˜์ค€์—์„œ ์—„๊ฒฉํ•˜๊ฒŒ ๊ฐ•์ œ๋˜์–ด ์ ‘๊ทผ ์ž์ฒด๊ฐ€ SyntaxError. ํด๋กœ์ € ๋ฐฉ์‹์€ ๊ด€ํ–‰์  ์€๋‹‰์œผ๋กœ WeakMap ๋“ฑ์„ ํ†ตํ•ด ์šฐํšŒ ๊ฐ€๋Šฅ.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

// ํด๋กœ์ € ๋ฐฉ์‹ โ€” ๊ด€ํ–‰์  private
const _data = new WeakMap();
class OldWay {
  constructor() { _data.set(this, { secret: 42 }); }
  getSecret() { return _data.get(this).secret; }
}
// ์šฐํšŒ ๊ฐ€๋Šฅ: _data.get(instance).secret
 
// # ๋ฐฉ์‹ โ€” ์–ธ์–ด ์ˆ˜์ค€ ๊ฐ•์ œ
class NewWay {
  #secret = 42;
  getSecret() { return this.#secret; }
}
// ์šฐํšŒ ๋ถˆ๊ฐ€: instance.#secret โ†’ SyntaxError (๋Ÿฐํƒ€์ž„๋„ ์•„๋‹Œ ํŒŒ์‹ฑ ๋‹จ๊ณ„์—์„œ ์ฐจ๋‹จ)
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "# ๋ฐฉ์‹์€ ๋ฌธ๋ฒ• ์ˆ˜์ค€ ์ž๋ฌผ์‡ , ํด๋กœ์ € ๋ฐฉ์‹์€ ๊ด€ํ–‰ ์ˆ˜์ค€ ์ž๋ฌผ์‡ . ๋ณด์•ˆ์ด ์ค‘์š”ํ•˜๋‹ค๋ฉด #์„ ์จ๋ผ."

Q2. ๋ฏน์Šค์ธ ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ์™€ ์ƒ์†๊ณผ์˜ ์ฐจ์ด๋Š”?

โœ… ์ •๋‹ต: JS๋Š” ๋‹จ์ผ ์ƒ์†๋งŒ ์ง€์›ํ•˜์ง€๋งŒ, ๋ฏน์Šค์ธ์€ ์—ฌ๋Ÿฌ ๊ธฐ๋Šฅ์„ ์กฐํ•ฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๋˜ํ•œ "is-a" ๊ด€๊ณ„๊ฐ€ ์•„๋‹Œ "has-a" ๊ด€๊ณ„๋ฅผ ํ‘œํ˜„ํ•  ๋•Œ ๋ฏน์Šค์ธ์ด ๋” ์ž์—ฐ์Šค๋Ÿฝ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • ์ƒ์†: Post extends Article โ€” "Post๋Š” Article์ด๋‹ค" (is-a ๊ด€๊ณ„)
  • ๋ฏน์Šค์ธ: Timestamped(SoftDeletable(Post)) โ€” "Post๋Š” ํƒ€์ž„์Šคํƒฌํ”„ ๊ธฐ๋Šฅ๊ณผ ์†Œํ”„ํŠธ ๋”œ๋ฆฌํŠธ ๊ธฐ๋Šฅ์„ ๊ฐ€์ง„๋‹ค" (has-a)
  • ๋ฏน์Šค์ธ์˜ ์žฅ์ : ์ˆ˜ํ‰์  ๊ธฐ๋Šฅ ์กฐํ•ฉ, ํŠน์ • ๊ธฐ๋Šฅ๋งŒ ์„ ํƒ์ ์œผ๋กœ ์ถ”๊ฐ€
  • ๋ฏน์Šค์ธ์˜ ๋‹จ์ : ๋ฉ”์„œ๋“œ ์ด๋ฆ„ ์ถฉ๋Œ ๊ฐ€๋Šฅ, ์กฐํ•ฉ์ด ๋งŽ์•„์ง€๋ฉด ํƒ€์ž… ์ถ”๋ก  ๋ณต์žก
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "์ƒ์†์ด '๋ˆ„๊ฐ€ ๋ถ€๋ชจ๋ƒ'๋ฅผ ๋ฌป๋Š”๋‹ค๋ฉด, ๋ฏน์Šค์ธ์€ '์–ด๋–ค ๋Šฅ๋ ฅ์ด ํ•„์š”ํ•˜๋ƒ'๋ฅผ ๋ฌป๋Š”๋‹ค."

Q3. ์˜์ฒ ์ด์˜ ํ…Œ์ŠคํŠธ ํƒ€์ž„ โ€” ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„

์˜ํ˜ธ ๋ฆฌ๋“œ๊ฐ€ ๋งํ•œ๋‹ค: "์˜์ฒ  ๋‹˜, UserService์™€ PostService๊ฐ€ ๋‘˜ ๋‹ค HTTP ์š”์ฒญ, ์—๋Ÿฌ ๋กœ๊น…, ์บ์‹ฑ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•œ๋ฐ, ๊ฐ๊ฐ ์ค‘๋ณต์œผ๋กœ ๊ตฌํ˜„ํ•˜๋ฉด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ํž˜๋“ค์–ด์š”. ์–ด๋–ป๊ฒŒ ๊ตฌ์กฐํ™”ํ• ๊นŒ์š”?"

โœ… ์ •๋‹ต: BaseService ์ถ”์ƒ ํด๋ž˜์Šค์— ๊ณตํ†ต ๊ธฐ๋Šฅ์„ ๋‘๊ณ , ๊ฐ ์„œ๋น„์Šค๊ฐ€ ์ƒ์†. ๋˜๋Š” HTTP/์บ์‹ฑ์„ ๋ฏน์Šค์ธ์œผ๋กœ ๋ถ„๋ฆฌํ•ด ์กฐํ•ฉ.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

// ๋ฐฉ๋ฒ• 1: ์ƒ์† (๊ณตํ†ต ๊ธฐ๋Šฅ์ด ๊ธด๋ฐ€ํžˆ ์—ฐ๊ฒฐ๋  ๋•Œ)
class BaseService {
  async request(method, url, data) { /* HTTP ์ฒ˜๋ฆฌ */ }
  log(message) { /* ๋กœ๊น… */ }
}
class UserService extends BaseService { /* ์œ ์ € ์ „์šฉ */ }
class PostService extends BaseService { /* ๊ฒŒ์‹œ๊ธ€ ์ „์šฉ */ }
 
// ๋ฐฉ๋ฒ• 2: ๋ฏน์Šค์ธ (๊ธฐ๋Šฅ์ด ๋…๋ฆฝ์ ์ผ ๋•Œ, ๋” ์œ ์—ฐ)
const Cacheable = (Base) => class extends Base {
  #cache = new Map();
  async cachedRequest(key, fn) {
    if (this.#cache.has(key)) return this.#cache.get(key);
    const result = await fn();
    this.#cache.set(key, result);
    return result;
  }
};
 
const Loggable = (Base) => class extends Base {
  log(level, message, context) { /* ๋กœ๊น… */ }
};
 
class UserService extends Cacheable(Loggable(BaseService)) { }
class PostService extends Cacheable(Loggable(BaseService)) { }
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "์ค‘๋ณต ์ฝ”๋“œ๋Š” ํ•ญ์ƒ ๊ณตํ†ต ์กฐ์ƒ์œผ๋กœ. ๋…๋ฆฝ์ ์ธ ๊ธฐ๋Šฅ์€ ๋ฏน์Šค์ธ์œผ๋กœ ํ•ฉ์„ฑํ•ด๋ผ."

๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

์˜ค๋Š˜์€ ํด๋ž˜์Šค ์ฑ•ํ„ฐ์˜€๋Š”๋ฐ, ํ”„๋กœํ† ํƒ€์ž… ์ฑ•ํ„ฐ์—์„œ ๋ฐฐ์šด ๊ฒŒ ์—ฌ๊ธฐ์„œ ๋‹ค ์—ฐ๊ฒฐ๋๋‹ค. class๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ prototype์— ๋ฉ”์„œ๋“œ๋ฅผ ์˜ฌ๋ฆฐ๋‹ค๋Š” ๊ฑธ ์•Œ๊ณ  ์žˆ์œผ๋‹ˆ๊นŒ static์ด ์™œ ์ธ์Šคํ„ด์Šค์—์„œ ์•ˆ ๋ณด์ด๋Š”์ง€, getter๊ฐ€ ์™œ prototype์— ์˜ฌ๋ผ๊ฐ€๋Š”์ง€ ์ด์ œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ดํ•ด๋œ๋‹ค.

๋ฏน์Šค์ธ ํŒจํ„ด์€ ์ง„์งœ ์‹ ์„ ํ–ˆ๋‹ค. "์ƒ์† ์—†์ด ๊ธฐ๋Šฅ์„ ๋นŒ๋ ค์˜จ๋‹ค"๋Š” ๊ฐœ๋…์ด ์ฒ˜์Œ์—๋Š” ์–ด์ƒ‰ํ–ˆ๋Š”๋ฐ, Timestamped(SoftDeletable(Entity))์ฒ˜๋Ÿผ ์กฐํ•ฉํ•˜๋Š” ๊ฒŒ ์ƒ๊ฐ๋ณด๋‹ค ๊ฐ•๋ ฅํ•˜๋‹ค๋Š” ๊ฑธ ์ฝ”๋“œ๋กœ ๋ณด๋‹ˆ๊นŒ ๋‚ฉ๋“์ด ๋๋‹ค.

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "ํด๋ž˜์Šค๋Š” ํ”„๋กœํ† ํƒ€์ž…์ด๋ผ๋Š” ๊ฒฌ๊ณ ํ•œ ์„ค๊ณ„๋„ ์œ„์— ์ž…ํ˜€์ง„ ์•„๋ฆ„๋‹ต๊ณ  ์‹ค์šฉ์ ์ธ ์˜ท์ž…๋‹ˆ๋‹ค. #private์œผ๋กœ ์†Œ์ค‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณดํ˜ธํ•˜๊ณ , ๋ฏน์Šค์ธ์œผ๋กœ ํ•„์š”ํ•œ ๋Šฅ๋ ฅ์„ ์ž์œ ๋กญ๊ฒŒ ์กฐ๋ฆฝํ•˜๋ฉฐ, ์ƒ์†์€ ๊ผญ ํ•„์š”ํ•œ ๋งŒํผ๋งŒ ์ง€ํ˜œ๋กญ๊ฒŒ ์‚ฌ์šฉํ•˜์„ธ์š”."

๋“œ๋””์–ด ๋งˆ์ง€๋ง‰ ์ฑ•ํ„ฐ ํ•˜๋‚˜ ๋‚จ์•˜๋‹ค. ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๋ž‘ ์„ฑ๋Šฅ. ์ด๊ฒƒ๋„ ๋ฐฐ์›Œ๋‘๋ฉด ๋‚˜์ค‘์— ์„ฑ๋Šฅ ์ตœ์ ํ™”ํ•  ๋•Œ ์ง„์งœ ๋„์›€์ด ๋  ๊ฒƒ ๊ฐ™๋‹ค. ์˜ค๋Š˜์€ ์ผ์ฐ ์ž์•ผ์ง€. ๋‚ด์ผ ์•„์นจ์— ๊ฐœ์šดํ•œ ๋จธ๋ฆฌ๋กœ ๋งˆ์ง€๋ง‰ ์ฑ•ํ„ฐ ๋‹ฌ๋ฆฌ์ž!


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ