커맨드 패턴: 요청을 객체로 캡슐화하는 디자인 패턴
실행 취소와 재실행을 지원하는 유연한 시스템 설계를 위한 커맨드 패턴 구현
팩토리 메서드 패턴(Factory Method Pattern)은 객체 생성을 처리하는 인터페이스를 정의하되, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브클래스가 내리도록 하는 생성 디자인 패턴입니다. 이를 통해 객체 생성 로직을 캡슐화하고, 클라이언트 코드와 구체적인 클래스 구현을 분리할 수 있습니다.
이 글에서는 팩토리 메서드 패턴의 구조, 구현 방법, 실전 사례, 그리고 다른 패턴과의 차이점을 상세히 분석합니다.
클라이언트 코드에서 직접 구체 클래스를 인스턴스화하면 여러 문제가 발생합니다.
문제 상황:
class OrderProcessor {
fun processOrder(orderType: String, items: List<Item>) {
val order = when (orderType) {
"ONLINE" -> OnlineOrder(items)
"OFFLINE" -> OfflineOrder(items)
"SUBSCRIPTION" -> SubscriptionOrder(items)
else -> throw IllegalArgumentException("Unknown order type")
}
order.validate()
order.calculate()
order.process()
}
}문제점:
// 추상 팩토리 클래스
abstract class OrderFactory {
abstract fun createOrder(items: List<Item>): Order
fun process(items: List<Item>) {
val order = createOrder(items)
order.validate()
order.calculate()
order.process()
}
}
// 구체적 팩토리들
class OnlineOrderFactory : OrderFactory() {
override fun createOrder(items: List<Item>): Order {
return OnlineOrder(items)
}
}
class OfflineOrderFactory : OrderFactory() {
override fun createOrder(items: List<Item>): Order {
return OfflineOrder(items)
}
}
// 클라이언트 코드
fun main() {
val factory: OrderFactory = OnlineOrderFactory()
factory.process(items) // 구체 클래스 모름
}개선점:
┌────────────────┐
│ Creator │
├────────────────┤
│ + factoryMethod│◄────────────┐
│ + operation() │ │
└────────────────┘ │
▲ │ creates
│ │
┌────────┴────────┐ │
│ │ │
┌───────────────┐ ┌───────────────┐ │
│ ConcreteA │ │ ConcreteB │ │
├───────────────┤ ├───────────────┤ │
│ factoryMethod │ │ factoryMethod │ │
│ returns A │ │ returns B │ │
└───────────────┘ └───────────────┘ │
│ │ │
└────────┬────────┘ │
│ │
▼ │
┌────────────────┐ │
│ Product │◄─────────────┘
├────────────────┤
│ + operation() │
└────────────────┘
▲
│
┌────────┴────────┐
│ │
┌───────────────┐ ┌───────────────┐
│ ProductA │ │ ProductB │
├───────────────┤ ├───────────────┤
│ + operation() │ │ + operation() │
└───────────────┘ └───────────────┘
1. Creator (추상 팩토리)
factoryMethod(): 서브클래스에서 구현할 추상 메서드operation(): 팩토리 메서드를 사용하는 템플릿 메서드2. ConcreteCreator (구체적 팩토리)
factoryMethod() 오버라이드하여 특정 Product 반환3. Product (추상 제품)
4. ConcreteProduct (구체적 제품)
여러 결제 제공업체(Nice, Toss, KakaoPay)와 결제 방식(온라인/오프라인)을 지원하는 시스템을 설계합니다.
// 추상 제품
abstract class Payment {
abstract val description: String
abstract val method: PaymentMethod
open fun paramValidation(): Boolean {
println("Validating parameters for ${description}")
return true
}
open fun transfer(): Boolean {
println("Processing transfer for ${description}")
return true
}
open fun sendMessage(): Boolean {
println("Sending message for ${description}")
return true
}
open fun refund(amount: Double): Boolean {
println("Refunding $amount for ${description}")
return true
}
}
enum class PaymentMethod {
ONLINE, OFFLINE
}
// 구체적 제품들
class NiceOnlinePayment : Payment() {
override val description = "Nice Online Payment"
override val method = PaymentMethod.ONLINE
override fun transfer(): Boolean {
println("Nice: Processing online credit card transaction")
// Nice 특화 온라인 결제 로직
return super.transfer()
}
override fun sendMessage(): Boolean {
println("Nice: Sending SMS notification")
return super.sendMessage()
}
}
class NiceOfflinePayment : Payment() {
override val description = "Nice Offline Payment"
override val method = PaymentMethod.OFFLINE
override fun transfer(): Boolean {
println("Nice: Processing offline card terminal transaction")
return super.transfer()
}
override fun sendMessage(): Boolean {
println("Nice: Sending both SMS and email notification")
return super.sendMessage()
}
}
class TossOnlinePayment : Payment() {
override val description = "Toss Online Payment"
override val method = PaymentMethod.ONLINE
override fun paramValidation(): Boolean {
println("Toss: Validating with enhanced security")
return super.paramValidation()
}
override fun transfer(): Boolean {
println("Toss: Fast online transfer with Toss API")
return super.transfer()
}
override fun sendMessage(): Boolean {
println("Toss: Push notification via Toss app")
return super.sendMessage()
}
}
class TossOfflinePayment : Payment() {
override val description = "Toss Offline Payment"
override val method = PaymentMethod.OFFLINE
override fun transfer(): Boolean {
println("Toss: Offline payment with QR code")
return super.transfer()
}
// Toss 오프라인은 메시지를 보내지 않음
override fun sendMessage(): Boolean {
return true
}
}
class KakaoOnlinePayment : Payment() {
override val description = "KakaoPay Online Payment"
override val method = PaymentMethod.ONLINE
override fun transfer(): Boolean {
println("KakaoPay: Processing via Kakao platform")
return super.transfer()
}
override fun sendMessage(): Boolean {
println("KakaoPay: Sending KakaoTalk message")
return super.sendMessage()
}
}
class KakaoOfflinePayment : Payment() {
override val description = "KakaoPay Offline Payment"
override val method = PaymentMethod.OFFLINE
override fun transfer(): Boolean {
println("KakaoPay: Offline barcode payment")
return super.transfer()
}
override fun sendMessage(): Boolean {
println("KakaoPay: KakaoTalk notification")
return super.sendMessage()
}
}// 추상 팩토리
abstract class PaymentFactory {
abstract fun createPayment(method: PaymentMethod): Payment
// 템플릿 메서드: 전체 결제 프로세스
fun process(method: PaymentMethod, amount: Double): Boolean {
val payment = createPayment(method)
if (!payment.paramValidation()) {
println("Parameter validation failed")
return false
}
if (!payment.transfer()) {
println("Transfer failed, initiating refund")
payment.refund(amount)
return false
}
payment.sendMessage()
return true
}
}
// 구체적 팩토리들
class NicePaymentFactory : PaymentFactory() {
override fun createPayment(method: PaymentMethod): Payment {
return when (method) {
PaymentMethod.ONLINE -> NiceOnlinePayment()
PaymentMethod.OFFLINE -> NiceOfflinePayment()
}
}
}
class TossPaymentFactory : PaymentFactory() {
override fun createPayment(method: PaymentMethod): Payment {
return when (method) {
PaymentMethod.ONLINE -> TossOnlinePayment()
PaymentMethod.OFFLINE -> TossOfflinePayment()
}
}
}
class KakaoPaymentFactory : PaymentFactory() {
override fun createPayment(method: PaymentMethod): Payment {
return when (method) {
PaymentMethod.ONLINE -> KakaoOnlinePayment()
PaymentMethod.OFFLINE -> KakaoOfflinePayment()
}
}
}fun main() {
val amount = 50000.0
println("=== Nice 결제 ===")
val niceFactory = NicePaymentFactory()
niceFactory.process(PaymentMethod.ONLINE, amount)
println()
println("=== Toss 결제 ===")
val tossFactory = TossPaymentFactory()
tossFactory.process(PaymentMethod.OFFLINE, amount)
println()
println("=== KakaoPay 결제 ===")
val kakaoFactory = KakaoPaymentFactory()
kakaoFactory.process(PaymentMethod.ONLINE, amount)
}출력:
=== Nice 결제 ===
Validating parameters for Nice Online Payment
Nice: Processing online credit card transaction
Processing transfer for Nice Online Payment
Nice: Sending SMS notification
Sending message for Nice Online Payment
=== Toss 결제 ===
Toss: Validating with enhanced security
Validating parameters for Toss Offline Payment
Toss: Offline payment with QR code
Processing transfer for Toss Offline Payment
=== KakaoPay 결제 ===
Validating parameters for KakaoPay Online Payment
KakaoPay: Processing via Kakao platform
Processing transfer for KakaoPay Online Payment
KakaoPay: Sending KakaoTalk message
Sending message for KakaoPay Online Payment
다양한 형식(PDF, DOCX, HTML, Markdown)의 문서를 생성하는 시스템을 구현합니다.
// Product 인터페이스
interface Document {
content: string;
format: string;
render(): string;
save(path: string): void;
export(): Buffer;
}
// 추상 Creator
abstract class DocumentCreator {
abstract createDocument(content: string): Document;
// 템플릿 메서드
generateDocument(content: string, path: string): void {
const doc = this.createDocument(content);
console.log(`Rendering ${doc.format} document...`);
const rendered = doc.render();
console.log(`Saving to ${path}...`);
doc.save(path);
console.log(`Document saved successfully`);
}
}
// Concrete Products
class PDFDocument implements Document {
content: string;
format = 'PDF';
constructor(content: string) {
this.content = content;
}
render(): string {
// PDF 렌더링 로직 (실제로는 라이브러리 사용)
return `%PDF-1.4\n${this.content}\n%%EOF`;
}
save(path: string): void {
console.log(`Writing PDF to ${path}`);
// fs.writeFileSync(path, this.export());
}
export(): Buffer {
return Buffer.from(this.render());
}
}
class DOCXDocument implements Document {
content: string;
format = 'DOCX';
constructor(content: string) {
this.content = content;
}
render(): string {
// DOCX XML 구조 생성
return `<?xml version="1.0"?>
<document>
<body>
<p>${this.content}</p>
</body>
</document>`;
}
save(path: string): void {
console.log(`Writing DOCX to ${path}`);
// 실제로는 docx 라이브러리 사용
}
export(): Buffer {
return Buffer.from(this.render());
}
}
class HTMLDocument implements Document {
content: string;
format = 'HTML';
constructor(content: string) {
this.content = content;
}
render(): string {
return `<!DOCTYPE html>
<html>
<head><title>Document</title></head>
<body>
<div>${this.content}</div>
</body>
</html>`;
}
save(path: string): void {
console.log(`Writing HTML to ${path}`);
}
export(): Buffer {
return Buffer.from(this.render());
}
}
class MarkdownDocument implements Document {
content: string;
format = 'Markdown';
constructor(content: string) {
this.content = content;
}
render(): string {
// Markdown은 원본 그대로
return this.content;
}
save(path: string): void {
console.log(`Writing Markdown to ${path}`);
}
export(): Buffer {
return Buffer.from(this.render());
}
}
// Concrete Creators
class PDFDocumentCreator extends DocumentCreator {
createDocument(content: string): Document {
return new PDFDocument(content);
}
}
class DOCXDocumentCreator extends DocumentCreator {
createDocument(content: string): Document {
return new DOCXDocument(content);
}
}
class HTMLDocumentCreator extends DocumentCreator {
createDocument(content: string): Document {
return new HTMLDocument(content);
}
}
class MarkdownDocumentCreator extends DocumentCreator {
createDocument(content: string): Document {
return new MarkdownDocument(content);
}
}
// 클라이언트 코드
function main() {
const content = "# Hello World\n\nThis is a test document.";
const creators: Record<string, DocumentCreator> = {
pdf: new PDFDocumentCreator(),
docx: new DOCXDocumentCreator(),
html: new HTMLDocumentCreator(),
md: new MarkdownDocumentCreator()
};
// 사용자 선택에 따라 동적으로 문서 생성
const format = 'pdf';
const creator = creators[format];
if (creator) {
creator.generateDocument(content, `output.${format}`);
}
}| 특징 | 팩토리 메서드 | 추상 팩토리 |
|---|---|---|
| 목적 | 하나의 제품 생성 | 관련된 제품 군 생성 |
| 메서드 수 | 1개 (factoryMethod) | 여러 개 (제품별 메서드) |
| 확장 방법 | 서브클래스 추가 | 새 팩토리 클래스 추가 |
| 복잡도 | 낮음 | 높음 |
| 사용 시점 | 단일 제품 변형 | 제품 패밀리 |
팩토리 메서드 예시:
abstract class ButtonFactory {
abstract fun createButton(): Button
}
class WindowsButtonFactory : ButtonFactory() {
override fun createButton() = WindowsButton()
}추상 팩토리 예시:
interface GUIFactory {
fun createButton(): Button
fun createCheckbox(): Checkbox
fun createTextbox(): Textbox
}
class WindowsGUIFactory : GUIFactory {
override fun createButton() = WindowsButton()
override fun createCheckbox() = WindowsCheckbox()
override fun createTextbox() = WindowsTextbox()
}| 특징 | 팩토리 메서드 | 심플 팩토리 |
|---|---|---|
| 확장성 | 높음 (OCP 준수) | 낮음 (조건문 수정 필요) |
| 상속 사용 | O | X |
| 복잡도 | 높음 | 낮음 |
| 유연성 | 서브클래스로 확장 | 정적 메서드로 제한 |
심플 팩토리 예시:
// 확장 시 기존 코드 수정 필요
class PaymentFactory {
companion object {
fun create(type: String): Payment {
return when (type) {
"nice" -> NicePayment()
"toss" -> TossPayment()
else -> throw IllegalArgumentException()
}
}
}
}| 특징 | 팩토리 메서드 | 빌더 |
|---|---|---|
| 목적 | 객체 타입 선택 | 객체 구성 |
| 매개변수 | 적음 | 많음 (복잡한 객체) |
| 생성 방식 | 한 번에 | 단계적 |
| 사용 시점 | 다형성 필요 | 복잡한 초기화 |
// Product
interface Logger {
fun log(level: LogLevel, message: String)
}
enum class LogLevel {
DEBUG, INFO, WARN, ERROR
}
class ConsoleLogger : Logger {
override fun log(level: LogLevel, message: String) {
println("[$level] $message")
}
}
class FileLogger(private val filePath: String) : Logger {
override fun log(level: LogLevel, message: String) {
// File(filePath).appendText("[$level] $message\n")
println("Writing to $filePath: [$level] $message")
}
}
class RemoteLogger(private val endpoint: String) : Logger {
override fun log(level: LogLevel, message: String) {
// HTTP POST to endpoint
println("Sending to $endpoint: [$level] $message")
}
}
// Creator
abstract class LoggerFactory {
abstract fun createLogger(): Logger
fun log(level: LogLevel, message: String) {
val logger = createLogger()
logger.log(level, message)
}
}
class ConsoleLoggerFactory : LoggerFactory() {
override fun createLogger() = ConsoleLogger()
}
class FileLoggerFactory(private val filePath: String) : LoggerFactory() {
override fun createLogger() = FileLogger(filePath)
}
class RemoteLoggerFactory(private val endpoint: String) : LoggerFactory() {
override fun createLogger() = RemoteLogger(endpoint)
}
// 환경에 따라 로거 선택
fun getLoggerFactory(): LoggerFactory {
return when (System.getenv("ENV")) {
"production" -> RemoteLoggerFactory("https://logs.example.com")
"development" -> ConsoleLoggerFactory()
else -> FileLoggerFactory("/tmp/app.log")
}
}
fun main() {
val logger = getLoggerFactory()
logger.log(LogLevel.INFO, "Application started")
logger.log(LogLevel.ERROR, "Connection failed")
}// Product
interface DatabaseConnection {
connect(): void;
disconnect(): void;
executeQuery(query: string): any[];
}
class MySQLConnection implements DatabaseConnection {
private connectionString: string;
constructor(connectionString: string) {
this.connectionString = connectionString;
}
connect(): void {
console.log(`Connecting to MySQL: ${this.connectionString}`);
}
disconnect(): void {
console.log('Disconnecting from MySQL');
}
executeQuery(query: string): any[] {
console.log(`MySQL executing: ${query}`);
return [];
}
}
class PostgreSQLConnection implements DatabaseConnection {
private connectionString: string;
constructor(connectionString: string) {
this.connectionString = connectionString;
}
connect(): void {
console.log(`Connecting to PostgreSQL: ${this.connectionString}`);
}
disconnect(): void {
console.log('Disconnecting from PostgreSQL');
}
executeQuery(query: string): any[] {
console.log(`PostgreSQL executing: ${query}`);
return [];
}
}
class MongoDBConnection implements DatabaseConnection {
private connectionString: string;
constructor(connectionString: string) {
this.connectionString = connectionString;
}
connect(): void {
console.log(`Connecting to MongoDB: ${this.connectionString}`);
}
disconnect(): void {
console.log('Disconnecting from MongoDB');
}
executeQuery(query: string): any[] {
console.log(`MongoDB executing: ${query}`);
return [];
}
}
// Creator
abstract class DatabaseFactory {
protected connectionString: string;
constructor(connectionString: string) {
this.connectionString = connectionString;
}
abstract createConnection(): DatabaseConnection;
query(query: string): any[] {
const connection = this.createConnection();
connection.connect();
const results = connection.executeQuery(query);
connection.disconnect();
return results;
}
}
class MySQLFactory extends DatabaseFactory {
createConnection(): DatabaseConnection {
return new MySQLConnection(this.connectionString);
}
}
class PostgreSQLFactory extends DatabaseFactory {
createConnection(): DatabaseConnection {
return new PostgreSQLConnection(this.connectionString);
}
}
class MongoDBFactory extends DatabaseFactory {
createConnection(): DatabaseConnection {
return new MongoDBConnection(this.connectionString);
}
}
// 설정 기반 팩토리 선택
function getDatabaseFactory(config: { type: string; connectionString: string }): DatabaseFactory {
switch (config.type) {
case 'mysql':
return new MySQLFactory(config.connectionString);
case 'postgresql':
return new PostgreSQLFactory(config.connectionString);
case 'mongodb':
return new MongoDBFactory(config.connectionString);
default:
throw new Error(`Unsupported database type: ${config.type}`);
}
}
// 사용
const dbFactory = getDatabaseFactory({
type: 'postgresql',
connectionString: 'postgresql://localhost:5432/mydb'
});
dbFactory.query('SELECT * FROM users');// Product
interface Dialog {
fun render()
fun onClick(handler: () -> Unit)
}
class WindowsDialog : Dialog {
override fun render() {
println("Rendering Windows-style dialog with sharp corners")
}
override fun onClick(handler: () -> Unit) {
println("Windows dialog clicked")
handler()
}
}
class MacOSDialog : Dialog {
override fun render() {
println("Rendering macOS-style dialog with rounded corners")
}
override fun onClick(handler: () -> Unit) {
println("macOS dialog clicked")
handler()
}
}
class LinuxDialog : Dialog {
override fun render() {
println("Rendering Linux GTK dialog")
}
override fun onClick(handler: () -> Unit) {
println("Linux dialog clicked")
handler()
}
}
// Creator
abstract class DialogFactory {
abstract fun createDialog(): Dialog
fun showDialog(message: String, onConfirm: () -> Unit) {
val dialog = createDialog()
dialog.render()
println("Dialog message: $message")
dialog.onClick(onConfirm)
}
}
class WindowsDialogFactory : DialogFactory() {
override fun createDialog() = WindowsDialog()
}
class MacOSDialogFactory : DialogFactory() {
override fun createDialog() = MacOSDialog()
}
class LinuxDialogFactory : DialogFactory() {
override fun createDialog() = LinuxDialog()
}
// OS 감지 후 팩토리 선택
fun getDialogFactory(): DialogFactory {
val osName = System.getProperty("os.name").lowercase()
return when {
osName.contains("windows") -> WindowsDialogFactory()
osName.contains("mac") -> MacOSDialogFactory()
osName.contains("linux") -> LinuxDialogFactory()
else -> WindowsDialogFactory() // 기본값
}
}
fun main() {
val dialogFactory = getDialogFactory()
dialogFactory.showDialog("Do you want to save changes?") {
println("Changes saved!")
}
}[주의] 잘못된 예:
// 단순한 객체 생성에 불필요한 팩토리 계층
abstract class StringFactory {
abstract fun createString(value: String): String
}
class UpperCaseStringFactory : StringFactory() {
override fun createString(value: String) = value.uppercase()
}
// 이 경우 확장 함수가 더 적절
fun String.toUpperCaseString() = this.uppercase()[주의] 잘못된 예:
class PaymentFactory : PaymentFactoryBase() {
override fun createPayment(method: PaymentMethod): Payment {
// 팩토리 메서드 안에 분기문 → 잘못된 사용
return when (method) {
PaymentMethod.ONLINE -> OnlinePayment()
PaymentMethod.OFFLINE -> OfflinePayment()
}
}
}[권장] 개선된 예:
// 각 결제 방식마다 별도 팩토리
class OnlinePaymentFactory : PaymentFactoryBase() {
override fun createPayment(method: PaymentMethod) = OnlinePayment()
}
class OfflinePaymentFactory : PaymentFactoryBase() {
override fun createPayment(method: PaymentMethod) = OfflinePayment()
}[주의] 잘못된 예:
// Product A가 Product B에 의존
class ProductA(private val productB: ProductB) {
// ...
}
class FactoryA : Factory() {
override fun createProduct(): ProductA {
// 다른 팩토리에 의존
val productB = FactoryB().createProduct()
return ProductA(productB)
}
}[권장] 개선 방법:
이 경우 추상 팩토리 패턴으로 전환하거나 의존성 주입 사용
// 상태 없는 팩토리는 싱글톤으로 재사용
object NicePaymentFactory : PaymentFactory() {
override fun createPayment(method: PaymentMethod): Payment {
return when (method) {
PaymentMethod.ONLINE -> NiceOnlinePayment()
PaymentMethod.OFFLINE -> NiceOfflinePayment()
}
}
}
// 사용
val payment = NicePaymentFactory.createPayment(PaymentMethod.ONLINE)class PooledPaymentFactory : PaymentFactory() {
private val pool = mutableMapOf<PaymentMethod, MutableList<Payment>>()
override fun createPayment(method: PaymentMethod): Payment {
val available = pool[method]?.removeFirstOrNull()
return available ?: createNewPayment(method)
}
private fun createNewPayment(method: PaymentMethod): Payment {
return when (method) {
PaymentMethod.ONLINE -> NiceOnlinePayment()
PaymentMethod.OFFLINE -> NiceOfflinePayment()
}
}
fun releasePayment(payment: Payment) {
pool.getOrPut(payment.method) { mutableListOf() }.add(payment)
}
}abstract class LazyPaymentFactory {
private var cachedPayment: Payment? = null
protected abstract fun createPaymentInstance(method: PaymentMethod): Payment
fun createPayment(method: PaymentMethod): Payment {
if (cachedPayment == null || cachedPayment!!.method != method) {
cachedPayment = createPaymentInstance(method)
}
return cachedPayment!!
}
}모두 "예"라면 팩토리 메서드 패턴이 적합합니다.
팩토리 메서드 패턴은 객체 생성을 캡슐화하고 확장 가능한 코드를 작성하는 강력한 방법입니다.
핵심 장점:
적용 시기:
팩토리 메서드 패턴을 적절히 활용하면 변경에 유연하고 테스트하기 쉬운 코드를 작성할 수 있습니다.
실행 취소와 재실행을 지원하는 유연한 시스템 설계를 위한 커맨드 패턴 구현
Double-Checked Locking과 초기화 지연을 활용한 안전하고 효율적인 싱글톤 구현 가이드
객체의 행위를 동적으로 변경하고 코드 중복을 제거하는 전략 패턴 설계와 구현 가이드