팩토리 함수는 일반 함수일까?
“팩토리 함수는 그냥 일반 함수 아닌가요?”라는 질문을 받은 적 있으신가요? 팩토리 함수를 설명할 때 “객체를 생성하는 일반 함수“라고 표현하는 걸 자주 보셨을 겁니다. 그렇다면 팩토리 함수와 일반 함수는 같은 건가요? 아니면 다른 건가요?
이 글에서는 이 질문에 명확하게 답하고, 팩토리 함수의 진짜 정체를 알아보겠습니다.
먼저, 기초부터 이해하기
질문에 답하기 전에, 용어부터 정확히 짚어봐야 합니다. “일반 함수”라는 표현 자체가 문맥에 따라 다르게 해석될 수 있기 때문입니다.
“일반 함수”는 무엇을 의미하는가?
JavaScript에서 “일반 함수”라는 말은 주로 두 가지 의미로 사용됩니다:
1. 문법적 의미: “function 키워드로 선언된 함수”
클래스(class)나 화살표 함수(arrow function)가 아닌, 전통적인 function 키워드로 정의된 함수를 말합니다.
// ✅ 일반 함수 (Regular Function)
function greet(name) {
return `Hello, ${name}`;
}
// ❌ 화살표 함수 (Arrow Function)
const greet = (name) => `Hello, ${name}`;
// ❌ 클래스 메서드 (Class Method)
class Greeter {
greet(name) {
return `Hello, ${name}`;
}
}
2. 역할적 의미: “특별한 패턴 없이 작성된 함수”
특정 디자인 패턴이나 용도를 따르지 않는, 그냥 평범한 함수를 말합니다.
// ✅ 일반 함수: 값을 계산해서 반환
function add(a, b) {
return a + b;
}
// ✅ 일반 함수: 데이터를 가공해서 반환
function formatDate(date) {
return date.toLocaleDateString();
}
// ❌ 팩토리 함수: 객체를 생성하는 특정 패턴
function createUser(name, age) {
return { name, age };
}
두 번째 의미를 이해하는 게 핵심입니다. “일반 함수”는 역할에 대한 설명이 아니라, 평범함을 의미합니다.
팩토리 함수는 일반 함수인가?
이제 답할 수 있습니다:
답변: 관점에 따라 다릅니다
문법적 관점: ✅ 맞습니다
팩토리 함수는 function 키워드로 선언됩니다. new 키워드가 필요한 생성자 함수(Constructor Function)도 아니고, class 문법도 아닙니다.
// 팩토리 함수: function 키워드 사용
function createCounter() {
let count = 0;
return {
increment: () => ++count,
getCount: () => count
};
}
// 생성자 함수: function 키워드 사용하지만 new 필요
function Counter() {
this.count = 0;
this.increment = function() {
this.count++;
};
}
// 클래스: class 문법
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
}
}
문법적으로는 팩토리 함수와 생성자 함수 모두 function 키워드로 작성되므로 “일반 함수”라고 부를 수 있습니다.
역할적 관점: ❌ 아닙니다
팩토리 함수는 “객체를 생성하는” 특정 패턴을 가진 함수입니다. 일반 함수처럼 단순히 값을 계산하거나 데이터를 가공하는 게 아니라, 객체를 만들어 반환하는 역할을 합니다.
// ❌ 일반 함수: 값을 계산
function multiply(a, b) {
return a * b;
}
// ✅ 팩토리 함수: 객체를 생성
function createCalculator() {
return {
multiply: (a, b) => a * b,
divide: (a, b) => a / b
};
}
일반 함수는 “무엇이든 할 수 있는 함수”이고, 팩토리 함수는 “객체를 만드는 함수”입니다. 즉, 팩토리 함수는 일반 함수의 특수한 사용 사례라고 볼 수 있습니다.
왜 “일반 함수”라는 표현을 사용할까?
팩토리 함수를 “일반 함수”라고 부르는 이유가 궁금하셨다면, 이 섹션에서 답을 찾을 수 있습니다. 생성자 함수, 클래스와 비교하면 팩토리 함수가 왜 “평범하다”고 여겨지는지 이해하게 될 겁니다.
이유 1: 생성자 함수와 비교하기 위해
생성자 함수는 new 키워드와 함께 사용되며, this 바인딩이라는 특별한 메커니즘이 필요합니다. 반면 팩토리 함수는 그런 특별한 문법 없이 그냥 호출하면 됩니다.
// 생성자 함수: new 필수, this 필수
function User(name) {
this.name = name; // this를 사용해야 함
}
const user1 = new User('Alice'); // new 필수
// 팩토리 함수: new 불필요, this 불필요
function createUser(name) {
return { name }; // 그냥 객체 반환
}
const user2 = createUser('Bob'); // 그냥 호출
생성자 함수는 “특별한 규칙”이 있지만, 팩토리 함수는 “평범하게” 호출합니다. 이런 의미에서 “일반 함수”라고 표현하는 것입니다.
이유 2: 클래스와 비교하기 위해
ES6에서 class 문법이 도입되면서, 객체를 생성하는 방법이 다양해졌습니다. 클래스는 별도의 문법(class, constructor, new)을 사용하지만, 팩토리 함수는 기존 함수 문법 그대로 사용합니다.
// 클래스: 특별한 문법
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
}
}
const counter1 = new Counter(); // new 필요
// 팩토리 함수: 일반 함수 문법
function createCounter() {
let count = 0;
return {
increment: () => ++count,
getCount: () => count
};
}
const counter2 = createCounter(); // new 불필요
클래스는 “새로운 문법”이지만, 팩토리 함수는 “기존 함수 문법”을 활용합니다. 그래서 “일반 함수”라고 부르는 것입니다.
이유 3: “평범하다”는 의미를 강조하기 위해
팩토리 함수는 겉보기에 아무런 특별한 점이 없습니다. 그냥 함수를 호출하고, 객체를 받는 것뿐입니다.
// 팩토리 함수: 평범하게 보임
function createPoint(x, y) {
return { x, y };
}
const point = createPoint(10, 20); // 평범한 함수 호출
console.log(point); // { x: 10, y: 20 }
이 코드를 보면 “특별한 패턴”이라기보다는 그냥 “객체를 반환하는 함수”로 보입니다. 이런 단순함을 강조하기 위해 “일반 함수”라고 표현합니다.
정확한 표현은 무엇일까?
그렇다면 팩토리 함수를 설명할 때 어떻게 표현하는 게 가장 정확할까요?
✅ 권장하는 표현
- “객체를 생성해서 반환하는 함수”
- 역할을 명확히 설명
- “new 없이 객체를 생성하는 함수”
- 생성자 함수와의 차이 강조
- “팩토리 패턴을 따르는 함수”
- 디자인 패턴 관점에서 설명
⚠️ 주의해야 할 표현
- “일반 함수” (모호함)
- 문법적으로는 맞지만, 역할적으로는 부정확
- “그냥 함수” (오해 유발)
- 특별한 역할이 없는 것처럼 들림
실전에서는 어떻게 구분할까?
이론을 배웠으니, 이제 실제 코드에서 어떻게 구분하는지 보겠습니다.
예제 1: 일반 함수 vs 팩토리 함수
// ❌ 일반 함수: 값을 계산해서 반환
function calculateTotal(price, quantity) {
return price * quantity;
}
const total = calculateTotal(1000, 3);
console.log(total); // 3000
// ✅ 팩토리 함수: 객체를 생성해서 반환
function createProduct(name, price) {
return {
name,
price,
getTotal(quantity) {
return price * quantity;
}
};
}
const product = createProduct('노트북', 1000000);
console.log(product.getTotal(2)); // 2000000
차이점:
calculateTotal: 숫자를 반환 (일반 함수)createProduct: 객체를 반환 (팩토리 함수)
예제 2: 유틸리티 함수 vs 팩토리 함수
// ❌ 유틸리티 함수: 데이터를 가공
function formatCurrency(amount) {
return `₩${amount.toLocaleString()}`;
}
console.log(formatCurrency(10000)); // "₩10,000"
// ✅ 팩토리 함수: 포맷터 객체 생성
function createCurrencyFormatter(locale, currency) {
return {
format(amount) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(amount);
}
};
}
const krwFormatter = createCurrencyFormatter('ko-KR', 'KRW');
console.log(krwFormatter.format(10000)); // "₩10,000"
차이점:
formatCurrency: 문자열을 반환 (유틸리티 함수)createCurrencyFormatter: 포맷터 객체를 반환 (팩토리 함수)
예제 3: 상태가 없는 함수 vs 팩토리 함수
// ❌ 상태 없는 함수: 매번 새로 계산
function getRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
console.log(getRandomNumber(1, 100)); // 42
console.log(getRandomNumber(1, 100)); // 78
// ✅ 팩토리 함수: 상태를 가진 객체 생성
function createRandomNumberGenerator(min, max) {
let lastValue = null;
return {
generate() {
lastValue = Math.floor(Math.random() * (max - min + 1)) + min;
return lastValue;
},
getLast() {
return lastValue;
}
};
}
const rng = createRandomNumberGenerator(1, 100);
console.log(rng.generate()); // 42
console.log(rng.getLast()); // 42 (마지막 값 유지)
차이점:
getRandomNumber: 상태 없이 값만 반환 (일반 함수)createRandomNumberGenerator: 상태를 캡슐화한 객체 반환 (팩토리 함수)
팩토리 함수의 특별한 점
팩토리 함수가 “일반 함수의 특수한 사용”이라면, 정확히 무엇이 특별한 걸까요?
1. 객체 생성이 목적
팩토리 함수는 항상 객체를 반환합니다. 숫자, 문자열, 불린 같은 원시 타입을 반환하지 않습니다.
// ❌ 팩토리 함수가 아님: 원시 타입 반환
function double(n) {
return n * 2;
}
// ✅ 팩토리 함수: 객체 반환
function createDoubler(initialValue) {
return {
value: initialValue,
double() {
this.value *= 2;
return this.value;
}
};
}
2. 클로저로 프라이빗 상태 관리
팩토리 함수는 클로저를 활용해 진짜 프라이빗 변수를 만들 수 있습니다.
// ✅ 팩토리 함수: 진짜 프라이빗 변수
function createBankAccount(initialBalance) {
let balance = initialBalance; // 외부에서 절대 접근 불가
return {
deposit(amount) {
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) {
throw new Error('잔액 부족');
}
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(10000);
console.log(account.balance); // undefined (접근 불가)
console.log(account.getBalance()); // 10000 (getter로만 접근)
클래스를 사용하면 # 문법(Private Fields)이 필요하지만, 팩토리 함수는 클로저로 자동으로 프라이빗 변수를 만듭니다.
3. this 바인딩 문제 없음
생성자 함수나 클래스는 this 바인딩 때문에 골치 아프지만, 팩토리 함수는 클로저로 변수를 캡처하기 때문에 this 문제가 없습니다.
// ❌ 클래스: this 바인딩 문제
class Timer {
constructor() {
this.seconds = 0;
}
start() {
// ❌ this가 window를 가리킴
setInterval(function() {
this.seconds++; // 작동 안 함
console.log(this.seconds);
}, 1000);
// ✅ bind 필요
setInterval(function() {
this.seconds++;
}.bind(this), 1000);
// ✅ 또는 화살표 함수
setInterval(() => {
this.seconds++;
}, 1000);
}
}
// ✅ 팩토리 함수: this 없이 클로저 사용
function createTimer() {
let seconds = 0; // 클로저로 자동 캡처
return {
start() {
// ✅ bind 없이 바로 사용
setInterval(function() {
seconds++;
console.log(seconds);
}, 1000);
}
};
}
4. new 없이 객체 생성
생성자 함수는 new 키워드가 필수지만, 팩토리 함수는 그냥 호출하면 됩니다.
// ❌ 생성자 함수: new 필수
function User(name) {
this.name = name;
}
const user1 = User('Alice'); // ❌ undefined
const user2 = new User('Bob'); // ✅ { name: 'Bob' }
// ✅ 팩토리 함수: new 불필요
function createUser(name) {
return { name };
}
const user3 = createUser('Charlie'); // ✅ { name: 'Charlie' }
함정과 주의사항
팩토리 함수는 단순해 보이지만, 잘못 사용하면 문제가 생길 수 있습니다.
함정 1: 메모리 사용량
팩토리 함수는 인스턴스마다 메서드를 새로 생성하기 때문에, 클래스보다 메모리를 더 많이 사용할 수 있습니다.
// ❌ 팩토리 함수: 메서드가 인스턴스마다 생성됨
function createUser(name) {
return {
name,
greet() { // 인스턴스마다 새로 생성
return `Hello, ${name}`;
}
};
}
const user1 = createUser('Alice');
const user2 = createUser('Bob');
console.log(user1.greet === user2.greet); // false (다른 함수)
// ✅ 클래스: 메서드가 프로토타입에 한 번만 생성됨
class User {
constructor(name) {
this.name = name;
}
greet() { // 프로토타입에 한 번만 생성
return `Hello, ${this.name}`;
}
}
const user3 = new User('Charlie');
const user4 = new User('Dave');
console.log(user3.greet === user4.greet); // true (같은 함수)
실무 영향:
- 인스턴스가 수십 개 이하: 거의 없음
- 인스턴스가 수백~수천 개: 메모리 증가 가능성
해결책:
- 공유 메서드를 외부 함수로 분리
// ✅ 메서드 재사용
function greet(name) {
return `Hello, ${name}`;
}
function createUser(name) {
return {
name,
greet: () => greet(name) // 함수 재사용
};
}
함정 2: 타입 체크 어려움
팩토리 함수로 생성된 객체는 instanceof로 타입을 확인할 수 없습니다.
// ❌ 팩토리 함수: instanceof 사용 불가
function createUser(name) {
return { name };
}
const user = createUser('Alice');
console.log(user instanceof User); // ❌ 에러 (User는 생성자가 아님)
// ✅ 클래스: instanceof 사용 가능
class User {
constructor(name) {
this.name = name;
}
}
const user2 = new User('Bob');
console.log(user2 instanceof User); // ✅ true
해결책:
- 타입 체크가 필요하면 속성으로 구분
function createUser(name) {
return {
_type: 'User', // 타입 식별자
name
};
}
function isUser(obj) {
return obj && obj._type === 'User';
}
const user = createUser('Alice');
console.log(isUser(user)); // true
함정 3: 상속 어려움
팩토리 함수는 클래스의 extends 같은 상속 메커니즘이 없습니다.
// ❌ 상속 불가능
function createAnimal(name) {
return {
name,
speak() {
return `${name} makes a sound`;
}
};
}
function createDog(name) {
// createAnimal의 메서드를 어떻게 재사용?
}
// ✅ 컴포지션으로 해결
function createDog(name) {
const animal = createAnimal(name); // 기본 기능 재사용
return {
...animal, // 기존 메서드 복사
speak() { // 메서드 오버라이드
return `${name} barks`;
},
fetch() { // 새 메서드 추가
return `${name} fetches the ball`;
}
};
}
const dog = createDog('Buddy');
console.log(dog.speak()); // "Buddy barks"
console.log(dog.fetch()); // "Buddy fetches the ball"
그래서, 언제 사용해야 할까?
팩토리 함수를 언제 사용하고, 언제 일반 함수나 클래스를 사용해야 할까요?
✅ 팩토리 함수를 사용하세요
- 프라이빗 상태가 필요할 때
function createCounter() { let count = 0; // 진짜 프라이빗 return { increment: () => ++count, getCount: () => count }; } - this 바인딩 문제를 피하고 싶을 때
function createButton() { let clicks = 0; return { onClick() { clicks++; // this 없이 접근 console.log(clicks); } }; } - 의존성 주입이 필요할 때
function createLogger(dependencies = {}) { const { logLevel = 'info' } = dependencies; return { log(message) { if (logLevel === 'info') { console.log(message); } } }; }
✅ 일반 함수를 사용하세요
- 값을 계산할 때
function add(a, b) { return a + b; } - 데이터를 변환할 때
function toUpperCase(str) { return str.toUpperCase(); } - 부수 효과가 목적일 때
function logMessage(message) { console.log(message); }
✅ 클래스를 사용하세요
- 상속이 필요할 때
class Animal { speak() {} } class Dog extends Animal { speak() { return 'Woof'; } } - 타입 체크가 필요할 때
class User {} const user = new User(); console.log(user instanceof User); // true - 대량의 인스턴스를 생성할 때 (메모리 효율)
정리하며
팩토리 함수는 “일반 함수”일까요?
문법적으로는 맞습니다. function 키워드로 작성되고, 특별한 문법이 없습니다.
역할적으로는 틀렸습니다. 객체를 생성하는 특정 패턴을 따르는 함수입니다.
팩토리 함수를 설명할 때 “일반 함수”라고 표현하는 이유는:
- 생성자 함수처럼
new가 필요 없음 - 클래스처럼 특별한 문법이 없음
- 그냥 평범하게 호출하면 됨
하지만 더 정확한 표현은:
- “객체를 생성해서 반환하는 함수”
- “new 없이 객체를 생성하는 함수”
- “팩토리 패턴을 따르는 함수”
팩토리 함수는 일반 함수의 특수한 사용 사례입니다. 일반 함수처럼 호출하지만, 객체를 만드는 특별한 역할을 합니다.
댓글