팩토리 메소드 패턴(Factory Method Pattern)은 객체 생성을 캡슐화하여, 하위클래스에서객체의 생성 방식을 결정하도록 하는 디자인 패턴입니다. 즉, 객체를 직접 생성하는 것이 아니라, 객체 생성을 담당하는 메소드를 통해 객체를 반환하도록 합니다. 이 패턴을 활용하면 클라이언트 코드가 특정 클래스의 인스턴스를 직접 생성하지 않고, 추상화된 인터페이스를 통해 객체를 생성할 수 있습니다.
팩토리 패턴과 팩토리 메서드 패턴은 어떻게 다를까?
둘 다 객체 생성을 캡슐화하는 디자인 패턴이지만, 그 방식과 의도에서 차이가 있습니다.
팩토리 패턴
객체 생성을 직접 수행하는 것이 아니라, 객체 생성을 담당하는 별도의 클래스(팩토리 클래스)를 두고, 이를 통해 객체를 생성하는 방식입니다. 즉, 객체 생성 로직을 클라이언트 코드에서 분리하여 관리하는 것입니다. 주로 단순한 객체 생성을 중앙 집중화할 때 유용합니다.
User라는 엔티티와 DTO를 만들어보겠습니다.
❌ 팩토리 패턴이 없을 경우 (변경 범위가 넓음)
// 1. UserDto 클래스 (데이터 구조를 정의하는 DTO)
class UserDto {
id: number;
name: string;
email: string;
age?: number;
constructor(id: number, name: string, email: string, age?: number) {
this.id = id;
this.name = name;
this.email = email;
this.age = age ?? 0; // 기본값 설정
}
validate(): boolean {
if (!this.name || !this.email) {
throw new Error("이름과 이메일은 필수 입력 값입니다.");
}
return true;
}
}
// 2. User 클래스 (엔티티)
class User {
id: number;
name: string;
email: string;
age?: number;
constructor(id: number, name: string, email: string, age?: number) {
this.id = id;
this.name = name;
this.email = email;
this.age = age ?? 0; // 기본값 설정
}
getProfile(): string {
return `이름: ${this.name}, 이메일: ${this.email}, 나이: ${this.age ?? "미입력"}`;
}
}
// 3. UserService (User 관련 비즈니스 로직 관리)
class UserService {
signIn(dto: UserDto): User {
console.log("UserService를 통해 User 로그인");
return new User(dto.id, dto.name, dto.email, dto.age);
}
signUp(dto: UserDto): User {
console.log("UserService를 통해 User 회원가입");
return new User(dto.id, dto.name, dto.email, dto.age);
}
}
만약 UserDto에 gender 프로퍼티가 추가된다면, UserDto, User, 그리고 UserService의 signIn, signUp 메서드를 포함하여 UserDto에 의존하는 모든 코드에서 수정이 필요하게 됩니다.
✅ 팩토리 패턴을 적용한 경우 (변경 범위 최소화)
// 1. UserDto 클래스 (데이터 구조를 정의하는 DTO)
class UserDto {
id: number;
name: string;
email: string;
age?: number;
constructor(id: number, name: string, email: string, age?: number) {
this.id = id;
this.name = name;
this.email = email;
this.age = age ?? 0; // 기본값 설정
}
validate(): boolean {
if (!this.name || !this.email) {
throw new Error("이름과 이메일은 필수 입력 값입니다.");
}
return true;
}
}
// 2. User 클래스 (도메인 객체)
class User {
id: number;
name: string;
email: string;
age?: number;
constructor(id: number, name: string, email: string, age?: number) {
this.id = id;
this.name = name;
this.email = email;
this.age = age ?? 0; // 기본값 설정
}
getProfile(): string {
return `이름: ${this.name}, 이메일: ${this.email}, 나이: ${this.age ?? "미입력"}`;
}
}
// 3. UserFactory (팩토리 클래스 - User 객체 생성 책임) - 캡슐화
class UserFactory {
static createUser(dto: UserDto): User {
dto.validate(); // ✅ 데이터 검증 실행
return new User(dto.id, dto.name, dto.email, dto.age);
}
}
// 4. UserService (User 관련 비즈니스 로직 관리)
class UserService {
signIn(dto: UserDto): User {
console.log("✅ UserService: 로그인 요청 처리");
return UserFactory.createUser(dto); // 🔥 팩토리 사용하여 객체 생성
}
signUp(dto: UserDto): User {
console.log("✅ UserService: 회원가입 요청 처리");
return UserFactory.createUser(dto); // 🔥 팩토리 사용하여 객체 생성
}
}
만약 UserDto에 gender라는 프로퍼티가 추가된다면, 팩토리를 활용하여 UserService 코드 수정 없이도 객체 생성 로직을 변경할 수 있습니다.
🔄 생성 방식이 바뀐 후 (팩토리 패턴 적용)
// 1. UserDto 클래스 (데이터 구조를 정의하는 DTO)
class UserDto {
id: number;
name: string;
email: string;
age?: number;
gender: string;
constructor(id: number, name: string, email: string, age?: number, gender: string) {
this.id = id;
this.name = name;
this.email = email;
this.age = age ?? 0; // 기본값 설정
this.gender = gender;
}
validate(): boolean {
if (!this.name || !this.email) {
throw new Error("이름과 이메일은 필수 입력 값입니다.");
}
return true;
}
}
// 2. User 클래스 (도메인 객체)
class User {
id: number;
name: string;
email: string;
age?: number;
gender: string;
constructor(id: number, name: string, email: string, age?: number, gender: string) {
this.id = id;
this.name = name;
this.email = email;
this.age = age ?? 0; // 기본값 설정
this.gender = gender;
}
getProfile(): string {
return `이름: ${this.name}, 이메일: ${this.email}, 나이: ${this.age ?? "미입력"}, 성별: ${this.gender}`;
}
}
// 3. UserFactory (팩토리 클래스 - User 객체 생성 책임) - 캡슐화
class UserFactory {
static createUser(dto: UserDto): User {
dto.validate(); // ✅ 데이터 검증 실행
return new User(dto.id, dto.name, dto.email, dto.age, dto.gender);
}
}
// 4. UserService (User 관련 비즈니스 로직 관리)
class UserService {
signIn(dto: UserDto): User {
console.log("✅ UserService: 로그인 요청 처리");
return UserFactory.createUser(dto); // 🔥 팩토리 사용하여 객체 생성
}
signUp(dto: UserDto): User {
console.log("✅ UserService: 회원가입 요청 처리");
return UserFactory.createUser(dto); // 🔥 팩토리 사용하여 객체 생성
}
}
User 엔티티에 gender 속성을 추가해보면 UserService 코드엔 영향을 끼치지 않게 됩니다.
이제 User 엔티티 말고, 소셜 로그인 인터페이스를 통해 다양한 제 3자 로그인을 구현해보겠습니다. 하나의 객체 생성이 아니라 다양한 객체 생성이 필요하겠죠? 팩토리 패턴을 활용해서 SocialLoginFactory를 구현해보겠습니다. 어떤 문제가 있는지 같이 확인해봐요!
❌ 모든 로그인 객체를 관리하는 SocialLoginFactory 구현
// 1. SocialLogin 인터페이스 (추상화)
interface SocialLogin {
login(): void;
}
// 2. 구체적인 로그인 클래스 (Kakao, Naver, Google, Apple)
class KakaoLogin implements SocialLogin {
login(): void {
console.log("카카오 로그인 실행");
}
}
class NaverLogin implements SocialLogin {
login(): void {
console.log("네이버 로그인 실행");
}
}
class GoogleLogin implements SocialLogin {
login(): void {
console.log("구글 로그인 실행");
}
}
class AppleLogin implements SocialLogin {
login(): void {
console.log("애플 로그인 실행");
}
}
// 3. 팩토리 클래스 (모든 로그인 객체를 관리) - OCP(개방폐쇄) 위반, SRP(단일책임) 위반
class SocialLoginFactory {
static createLogin(type: string): SocialLogin {
switch (type) {
case "kakao":
return new KakaoLogin();
case "naver":
return new NaverLogin();
case "google":
return new GoogleLogin();
case "apple":
return new AppleLogin();
default:
throw new Error("지원하지 않는 로그인 타입입니다.");
}
}
}
// 4. 사용 예시
const kakaoLogin = SocialLoginFactory.createLogin("kakao");
const googleLogin = SocialLoginFactory.createLogin("google");
kakaoLogin.login(); // "카카오 로그인 실행"
googleLogin.login(); // "구글 로그인 실행"
googleLogin.login(); // 구글 로그인 실행
현재 SocialLoginFactory 클래스는 타입에 따라 KakaoLogin 객체 생성을, 때론 다른 객체 생성을 합니다. 그래서 OOP의 단일책임원칙에 어긋나고 있습니다. 뿐만 아니라 깃허브 로그인이 추가된다면, 기존 코드에 case 문을 추가해서 생성해야하는 개방-폐쇄 원칙을 위배하고 있습니다.
추가 소셜로그인 기능이 생긴다면 기존 코드를 수정해야하는 문제점이 생깁니다. 이때 팩토리 메서드 패턴을 활용할 수 있겠습니다.
팩토리 메서드 패턴
팩토리 메서드는 객체 생성을 위한 인터페이스(추상 메서드)를 정의하고, 하위 클래스가 이를 구현하여 객체를 생성하도록 하는 방식입니다. 즉, 객체 생성을 하위 클래스에서 결정하도록 유도하여, 개방-폐쇄 원칙(OCP)을 따르게 합니다. 이렇게 되면 확장 가능성이 높아 새로운 객체 타입을 추가하기 쉽습니다.
소셜 로그인 인터페이스와 소셜 로그인 팩토리 추상클래스를 통해 다양한 제 3자 로그인을 구현해보겠습니다.
✅ 추상 클래스 SocialLoginFactory
// 1. SocialLogin 인터페이스 (추상화)
interface SocialLogin {
login(): void;
}
// 2. 구체적인 로그인 클래스 (Kakao, Naver, Google, Apple)
class KakaoLogin implements SocialLogin {
login(): void {
console.log("카카오 로그인 실행");
}
}
class NaverLogin implements SocialLogin {
login(): void {
console.log("네이버 로그인 실행");
}
}
class GoogleLogin implements SocialLogin {
login(): void {
console.log("구글 로그인 실행");
}
}
class AppleLogin implements SocialLogin {
login(): void {
console.log("애플 로그인 실행");
}
}
// 3. 팩토리 메서드를 가진 추상 팩토리 클래스
abstract class SocialLoginFactory {
abstract createLogin(): SocialLogin;
}
// 4. 개별 팩토리 클래스 (각 로그인 방식별로 별도 팩토리 구현)
class KakaoLoginFactory extends SocialLoginFactory {
createLogin(): SocialLogin {
return new KakaoLogin();
}
}
class NaverLoginFactory extends SocialLoginFactory {
createLogin(): SocialLogin {
return new NaverLogin();
}
}
class GoogleLoginFactory extends SocialLoginFactory {
createLogin(): SocialLogin {
return new GoogleLogin();
}
}
class AppleLoginFactory extends SocialLoginFactory {
createLogin(): SocialLogin {
return new AppleLogin();
}
}
// 5. 사용 예시
const kakaoFactory = new KakaoLoginFactory();
const googleFactory = new GoogleLoginFactory();
const kakaoLogin = kakaoFactory.createLogin(); // SocialLogin
const googleLogin = googleFactory.createLogin(); // SocialLogin
kakaoLogin.login(); // "카카오 로그인 실행"
googleLogin.login(); // "구글 로그인 실행"
만약 깃허브 로그인이 추가된다면, SocialLoginFactory를 확장하여 GithubLoginFactory 클래스만 추가해주면 됩니다.
구분
팩토리 (Factory)
팩토리 메서드 (Factory Method)
객체 생성 책임
하나의 팩토리 클래스가 생성 책임을 가짐
하위클래스에서 객체 생성 로직을 결정
유연성
새로운 객체 유형 추가 시 기존 팩토리 클래스 수정 필요
새로운 하위클래스를 추가하여 확장 가능 (OCP 준수)
설계 방식
단순한 정적 메서드 또는 별도의 팩토리 클래스를 사용
상속을 활용해 객체 생성 방식을 변경
사용 예
단순한 객체 생성을 중앙 집중화할 때
다양한 객체 생성을 하위클래스에 위임할 때
주요 특징
객체 생성 로직을 캡슐화 → 객체 생성을 직접 하지 않고, 팩토리 메소드를 통해 생성
유연한 확장성 → 새로운 객체 타입이 추가되더라도 기존 코드 수정 없이 확장 가능
결합도 낮추기 → 클라이언트 코드가 구체적인 클래스를 몰라도 객체를 생성할 수 있도록 유도
언제 활용할 수 있을까?
싱글톤 패턴에 비해 팩토리 메서드 패턴을 언제 활용할 수 있을지 이해하는 것이 어려웠습니다. 하지만 비슷한 객체를 반복적으로(공장처럼) 생성해야 할 경우에 팩토리 메서드 패턴 사용을 고려해본다고 생각하니 이해가 쉬워졌습니다. 또 개발자가 컴파일 단계에서 어떤 객체를 생성해야할 지 모르고, 런타임 단계에서 동적으로 객체를 생성해야 할 때도 사용할 수 있습니다.
플랫폼(Windows, MacOS 등) 환경에 따라 다른 버튼을 생성해하는 경우
OS마다 UI 컴포넌트가 다르기 때문에 Button을 직접 생성하면 OS별 분기 처리가 필요합니다. 이 경우 팩토리 메소드 패턴을 활용하면 OS별 버튼을 쉽게 추가할 수 있습니다. 자바스크립트의 class를 활용하여 버튼 인터페이스를 만들어보겠습니다.
// 1. 버튼 인터페이스 정의
abstract class Button {
abstract render(): void;
}
// 2. 각 OS에 맞는 버튼 클래스 구현
class WindowsButton extends Button {
render(): void {
console.log("Windows 스타일의 버튼 렌더링");
}
}
class MacOSButton extends Button {
render(): void {
console.log("MacOS 스타일의 버튼 렌더링");
}
}
// 3. 팩토리 메소드가 있는 렌더러 클래스
abstract class Renderer {
abstract createButton(): Button;
render(): void {
const button = this.createButton();
button.render();
}
}
// 4. OS에 맞는 팩토리 구현
class WindowsRenderer extends Renderer {
createButton(): Button {
return new WindowsButton();
}
}
class MacOSRenderer extends Renderer {
createButton(): Button {
return new MacOSButton();
}
}
// 5. 클라이언트 코드
function application(OS: string): void {
let renderer: Renderer;
if (OS === "Windows") {
renderer = new WindowsRenderer();
} else if (OS === "MacOS") {
renderer = new MacOSRenderer();
} else {
throw new Error("지원되지 않는 OS");
}
renderer.render();
}
// 사용 예시
application("Windows"); // Windows 스타일의 버튼 렌더링
application("MacOS"); // MacOS 스타일의 버튼 렌더링
OS에 따라 적절한 버튼을 생성하여 렌더링할 수 있게 되었습니다. 새로운 OS 타입이 추가될 경우, 기존 코드를 수정할 필요 없이 새로운 클래스만 추가하면 됩니다.
다양한 데이터베이스(MySQL, PostgreSQL 등)를 지원해하는 경우
어떤 애플리케이션이 다양한 데이터베이스를 지원해야 할 때, 팩토리 메소드 패턴을 사용하면 코드의 변경 없이 쉽게 확장할 수 있습니다.
// 1. 데이터베이스 인터페이스 정의
abstract class Database {
abstract connect(): void;
}
// 2. 특정 데이터베이스 연결 클래스 구현
class MySQLDatabase extends Database {
connect(): void {
console.log("MySQL 데이터베이스에 연결됨");
}
}
class PostgreSQLDatabase extends Database {
connect(): void {
console.log("PostgreSQL 데이터베이스에 연결됨");
}
}
// 3. 팩토리 메소드 패턴 적용
abstract class DatabaseFactory {
abstract createDatabase(): Database;
}
class MySQLDatabaseFactory extends DatabaseFactory {
createDatabase(): Database {
return new MySQLDatabase();
}
}
class PostgreSQLDatabaseFactory extends DatabaseFactory {
createDatabase(): Database {
return new PostgreSQLDatabase();
}
}
// 4. 클라이언트 코드
function getDatabaseConnection(type: string): void {
let factory: DatabaseFactory;
if (type === "MySQL") {
factory = new MySQLDatabaseFactory();
} else if (type === "PostgreSQL") {
factory = new PostgreSQLDatabaseFactory();
} else {
throw new Error("지원되지 않는 데이터베이스 타입");
}
const database = factory.createDatabase();
database.connect();
}
// 사용 예시
getDatabaseConnection("MySQL"); // MySQL 데이터베이스에 연결됨
getDatabaseConnection("PostgreSQL"); // PostgreSQL 데이터베이스에 연결됨
새로운 데이터베이스 유형을 추가하려면, 새로운 DatabaseFactory와 Database 클래스를 만들기만 하면 됩니다. 클라이언트 코드(getDatabaseConnection)는 데이터베이스 연결 방식이 변경되어도 영향을 받지 않게 됩니다.
다양한 소셜로그인(Kakao, Naver 등)을 지원해야하는 경우
// 1. 소셜 로그인 인터페이스 정의
abstract class SocialLogin {
abstract authenticate(): void;
}
// 2. 각 소셜 로그인 클래스 구현
class KakaoLogin extends SocialLogin {
authenticate(): void {
console.log("카카오 로그인 성공");
}
}
class NaverLogin extends SocialLogin {
authenticate(): void {
console.log("네이버 로그인 성공");
}
}
class GoogleLogin extends SocialLogin {
authenticate(): void {
console.log("구글 로그인 성공");
}
}
// 3. 팩토리 메소드 패턴 적용
abstract class SocialLoginFactory {
abstract createLogin(): SocialLogin;
}
class KakaoLoginFactory extends SocialLoginFactory {
createLogin(): SocialLogin {
return new KakaoLogin();
}
}
class NaverLoginFactory extends SocialLoginFactory {
createLogin(): SocialLogin {
return new NaverLogin();
}
}
class GoogleLoginFactory extends SocialLoginFactory {
createLogin(): SocialLogin {
return new GoogleLogin();
}
}
// 4. 클라이언트 코드
function socialLogin(type: string): void {
let factory: SocialLoginFactory;
if (type === "Kakao") {
factory = new KakaoLoginFactory();
} else if (type === "Naver") {
factory = new NaverLoginFactory();
} else if (type === "Google") {
factory = new GoogleLoginFactory();
} else {
throw new Error("지원되지 않는 소셜 로그인 타입");
}
const login = factory.createLogin();
login.authenticate();
}
// 사용 예시
socialLogin("Kakao"); // 카카오 로그인 성공
socialLogin("Naver"); // 네이버 로그인 성공
socialLogin("Google"); // 구글 로그인 성공
팩토리 메서드 패턴과 추상 팩토리 패턴은 어떻게 다를까?
팩토리 메소드 패턴은 종종 추상 팩토리 패턴(Abstract Factory Pattern)과 함께 사용됩니다.
🏭 OS별 버튼 생성 - 팩토리 메서드 패턴
// 1. 버튼 인터페이스 정의
abstract class Button {
abstract render(): void;
}
// 2. 각 OS에 맞는 버튼 클래스 구현
class WindowsButton extends Button {
render(): void {
console.log("Windows 스타일의 버튼 렌더링");
}
}
class MacOSButton extends Button {
render(): void {
console.log("MacOS 스타일의 버튼 렌더링");
}
}
// 3. 팩토리 메소드가 있는 렌더러 클래스
abstract class Renderer {
abstract createButton(): Button;
render(): void {
const button = this.createButton();
button.render();
}
}
// 4. OS에 맞는 팩토리 구현
class WindowsRenderer extends Renderer {
createButton(): Button {
return new WindowsButton();
}
}
class MacOSRenderer extends Renderer {
createButton(): Button {
return new MacOSButton();
}
}
// 5. 등록 기반 팩토리 구현 (OCP 적용)
class RendererFactory {
private static registry: Map<string, new () => Renderer> = new Map();
static register(OS: string, rendererClass: new () => Renderer): void {
this.registry.set(OS, rendererClass);
}
static createRenderer(OS: string): Renderer {
const RendererClass = this.registry.get(OS);
if (!RendererClass) {
throw new Error(`지원되지 않는 OS: ${OS}`);
}
return new RendererClass();
}
}
// 6. OS 렌더러 등록
RendererFactory.register("Windows", WindowsRenderer);
RendererFactory.register("MacOS", MacOSRenderer);
// 7. 클라이언트 코드
function application(OS: string): void {
const renderer = RendererFactory.createRenderer(OS);
renderer.render();
}
// 사용 예시
application("Windows"); // Windows 스타일의 버튼 렌더링
application("MacOS"); // MacOS 스타일의 버튼 렌더링
🏢 OS별 UI 컴포넌트(Button & Checkbox) 생성 - 추상 팩토리 패턴
하나의 팩토리에서 서로 연관된 객체 여러 개를 생성할 수 있습니다. UI 요소 같은 제품군(Product Family) 관리 가능합니다. 새로운 UI 시스템이 추가될 경우, 새로운 UIFactory를 만들 수 있습니다. (확장성 우수)
// 1. 공통 인터페이스 정의
abstract class Button {
abstract render(): void;
}
abstract class Checkbox {
abstract render(): void;
}
// 2. 구체적인 제품 클래스 (Windows UI)
class WindowsButton extends Button {
render(): void {
console.log("Windows 스타일 버튼 렌더링");
}
}
class WindowsCheckbox extends Checkbox {
render(): void {
console.log("Windows 스타일 체크박스 렌더링");
}
}
// 3. 구체적인 제품 클래스 (MacOS UI)
class MacOSButton extends Button {
render(): void {
console.log("MacOS 스타일 버튼 렌더링");
}
}
class MacOSCheckbox extends Checkbox {
render(): void {
console.log("MacOS 스타일 체크박스 렌더링");
}
}
// 4. 추상 팩토리 (서로 연관된 제품을 한 번에 생성)
abstract class UIFactory {
abstract createButton(): Button;
abstract createCheckbox(): Checkbox;
}
// 5. 구체적인 팩토리 클래스 (Windows용 UI)
class WindowsUIFactory extends UIFactory {
createButton(): Button {
return new WindowsButton();
}
createCheckbox(): Checkbox {
return new WindowsCheckbox();
}
}
// 6. 구체적인 팩토리 클래스 (MacOS용 UI)
class MacOSUIFactory extends UIFactory {
createButton(): Button {
return new MacOSButton();
}
createCheckbox(): Checkbox {
return new MacOSCheckbox();
}
}
// 7. 클라이언트 코드
function createUI(factory: UIFactory): void {
const button = factory.createButton();
const checkbox = factory.createCheckbox();
button.render();
checkbox.render();
}
// 사용 예시
const factory: UIFactory = new MacOSUIFactory();
createUI(factory);
// "MacOS 스타일 버튼 렌더링"
// "MacOS 스타일 체크박스 렌더링"
팩토리 메소드를 추상 클래스로 정의하여 각 팩토리마다 독립적인 생성 방식 유지할 수 있습니다. 클라이언트 코드가 특정 팩토리 인스턴스를 선택하여 사용할 수 있도록 유도합니다.
팩토리 메소드 패턴은 객체 생성 로직을 캡슐화하고, 유연한 확장을 가능하게 합니다. 다양한 프레임워크와 라이브러리에서 널리 사용되므로, 실제 프로젝트에서 적절히 활용하면 보다 유지보수성이 높은 코드를 작성할 수 있습니다. 여러분은 어떤 사례에서 팩토리 메소드 패턴을 활용해 보셨나요? 😊