๐๏ธ 14. ํด๋์ค โ ํ๋กํ ํ์ ์ ์งํ, ๊ทธ๋ฆฌ๊ณ ํ๋์ OOP์ ์ค๋ฌด ํ์ฉ
๐ ๊ฐ์
ES6+ ํด๋์ค์ ๋ชจ๋ ๊ธฐ๋ฅ(private ํ๋, static, getter/setter), ๋ฏน์ค์ธ ํจํด, TypeScript์์ ์ฐ๊ฒฐ๊ณ ๋ฆฌ๋ฅผ ์ค๋ฌด ์ค์ฌ์ผ๋ก ์ ๋ณตํฉ๋๋ค.
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
class์ ๋ชจ๋ ๋ฌธ๋ฒ(private ํ๋, static, getter/setter, ์์)์ ์ค๋ฌด์ ์ ์ฉํ๋ค.- ๋ฏน์ค์ธ(Mixin) ํจํด์ผ๋ก ๋ค์ค ์์ ์์ด ๊ธฐ๋ฅ์ ์กฐํฉํ๋ค.
- TypeScript์
interface,abstract class์ JS class์ ๊ด๊ณ๋ฅผ ์ดํดํ๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค 1. ์ ์์์ผ ํ๋๊ฐ?
- ๐๏ธ 2. ํด๋์ค ๊ธฐ๋ณธ ๋ฌธ๋ฒ ์์ ์ ๋ณต
- ๐งฌ 3. ์์๊ณผ super
- ๐งฉ 4. ๋ฏน์ค์ธ ํจํด
- ๐ท 5. TypeScript์์ ์ฐ๊ฒฐ๊ณ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 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; // truestatic ๋ฉ์๋์ ํ๋
๊ฐ๋ณ ์ธ์คํด์ค๊ฐ ์๋๋ผ ํด๋์ค ๊ทธ ์์ฒด์ ์ํ๋ ๋๊ตฌ๋ค์ ๋๋ค. ๊ณต์ฉ ์นดํ๋ก๊ทธ๋ ํฉํ ๋ฆฌ ํจ์๋ฅผ ๋ง๋ค ๋ ๋งค์ฐ ์ ์ฉํฉ๋๋ค.
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"); // truegetter / 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์ผ๋ก ์์คํ ๋ฐ์ดํฐ๋ฅผ ๋ณดํธํ๊ณ , ๋ฏน์ค์ธ์ผ๋ก ํ์ํ ๋ฅ๋ ฅ์ ์์ ๋กญ๊ฒ ์กฐ๋ฆฝํ๋ฉฐ, ์์์ ๊ผญ ํ์ํ ๋งํผ๋ง ์งํ๋กญ๊ฒ ์ฌ์ฉํ์ธ์."
๋๋์ด ๋ง์ง๋ง ์ฑํฐ ํ๋ ๋จ์๋ค. ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ๋ ์ฑ๋ฅ. ์ด๊ฒ๋ ๋ฐฐ์๋๋ฉด ๋์ค์ ์ฑ๋ฅ ์ต์ ํํ ๋ ์ง์ง ๋์์ด ๋ ๊ฒ ๊ฐ๋ค. ์ค๋์ ์ผ์ฐ ์์ผ์ง. ๋ด์ผ ์์นจ์ ๊ฐ์ดํ ๋จธ๋ฆฌ๋ก ๋ง์ง๋ง ์ฑํฐ ๋ฌ๋ฆฌ์!