Programming Design Pattern
프로그래밍 디자인 패턴은 소프트웨어를 개발할 때 발생하는 문제를 해결하는 것에 재사용 가능한 방식을 뜻한다.
즉, 많은 프로그래머들이 공통적으로 겪는 문제들에 대한 솔루션이다.
면접에서 자주 나오는 질문이기도 하고, 대표적인 패턴은 어떻게 작동하는지, 장단점이 뭔지 미리 알아둬야 실제로 프로젝트를 개발할 때 문제가 발생할 확률을 줄여준다.
너무 디자인 패턴을 지키려고 강박관념을 가질 필요는 없겠지만, 그렇다고 아무 생각도 없이 개발하는 건 더 문제가 되므로 꼭 개념에 대해 이해하고 있자.
Factory Pattern
이름만 보고 감이 온다면 센스쟁이라고 칭해도 된다.
말 그대로 공장과 같이 객체를 생성하기 위한 패턴이다.
객체를 생성하는 과정을 숨기고, 추상화된 기능으로 제공하여 객체 간 결합도를 낮추고 유지보수를 용이하게 하기 위한 패턴이다.
사용 방식에 따라
단순 팩토리 패턴, 팩토리 메서드 패턴, 추상 팩토리 패턴으로 나뉜다.
단순 팩토리 패턴의 예시를 보자.
#include <iostream>
#include <memory>
class Product {
public:
virtual void use() = 0;
virtual ~Product() = default;
};
class ConcreteProductA : public Product {
public:
void use() override {
std::cout << "Using ConcreteProductA" << std::endl;
}
};
class ConcreteProductB : public Product {
public:
void use() override {
std::cout << "Using ConcreteProductB" << std::endl;
}
};
class SimpleFactory {
public:
std::unique_ptr<Product> createProduct(const std::string& type) {
if (type == "A") {
return std::make_unique<ConcreteProductA>();
} else if (type == "B") {
return std::make_unique<ConcreteProductB>();
}
return nullptr;
}
};
int main() {
SimpleFactory factory;
auto productA = factory.createProduct("A");
auto productB = factory.createProduct("B");
productA->use(); // Using ConcreteProductA
productB->use(); // Using ConcreteProductB
return 0;
}
main 코드에서 ConcreteProductA와 B의 객체 생성 과정이 명확하게 드러나나?
아니다.
추상화된 기능인 createProduct라는 함수를 사용하여 그 과정을 숨기고 분리하고 있다.
A와 B를 생성하는 것을 다른 여러 곳에서도 사용할 수 있다.
또한 객체 생성이 또 필요하다 하더라도 해당 공장을 사용하던 코드를 수정하지 않고 공장에 생산 시설만 추가하면 처리할 수 있으므로 확장성이 전보다 좋아진다.
다음으로 팩토리 메서드 패턴의 예시를 보자.
#include <iostream>
#include <memory>
// Product 인터페이스
class Product {
public:
virtual void use() = 0;
virtual ~Product() = default;
};
// ConcreteProductA 클래스
class ConcreteProductA : public Product {
public:
void use() override {
std::cout << "Using ConcreteProductA" << std::endl;
}
};
// ConcreteProductB 클래스
class ConcreteProductB : public Product {
public:
void use() override {
std::cout << "Using ConcreteProductB" << std::endl;
}
};
// Creator 클래스 (팩토리 메서드 패턴)
class Creator {
public:
virtual std::unique_ptr<Product> factoryMethod() const = 0;
virtual ~Creator() = default;
void operation() const {
std::unique_ptr<Product> product = factoryMethod();
product->use();
}
};
// ConcreteCreatorA 클래스
class ConcreteCreatorA : public Creator {
public:
std::unique_ptr<Product> factoryMethod() const override {
return std::make_unique<ConcreteProductA>();
}
};
// ConcreteCreatorB 클래스
class ConcreteCreatorB : public Creator {
public:
std::unique_ptr<Product> factoryMethod() const override {
return std::make_unique<ConcreteProductB>();
}
};
int main() {
std::unique_ptr<Creator> creatorA = std::make_unique<ConcreteCreatorA>();
std::unique_ptr<Creator> creatorB = std::make_unique<ConcreteCreatorB>();
creatorA->operation(); // Using ConcreteProductA
creatorB->operation(); // Using ConcreteProductB
return 0;
}
이건 이제 기존 단순 팩토리 패턴에서 여러 종류의 객체를 한번에 생산하던 공장에서 종류마다 생산 공장을 따로 두는 패턴이다.
A를 생성하는 공장 따로, B를 생성하는 공장 따로 만들어서 필요한 객체만 생성하도록 한다.
다음으로 추상 팩토리 패턴을 보자
#include <iostream>
#include <memory>
// AbstractProductA 인터페이스
class AbstractProductA {
public:
virtual void use() = 0;
virtual ~AbstractProductA() = default;
};
// AbstractProductB 인터페이스
class AbstractProductB {
public:
virtual void eat() = 0;
virtual ~AbstractProductB() = default;
};
// ConcreteProductA1 클래스
class ConcreteProductA1 : public AbstractProductA {
public:
void use() override {
std::cout << "Using ConcreteProductA1" << std::endl;
}
};
// ConcreteProductA2 클래스
class ConcreteProductA2 : public AbstractProductA {
public:
void use() override {
std::cout << "Using ConcreteProductA2" << std::endl;
}
};
// ConcreteProductB1 클래스
class ConcreteProductB1 : public AbstractProductB {
public:
void eat() override {
std::cout << "Eating ConcreteProductB1" << std::endl;
}
};
// ConcreteProductB2 클래스
class ConcreteProductB2 : public AbstractProductB {
public:
void eat() override {
std::cout << "Eating ConcreteProductB2" << std::endl;
}
};
// AbstractFactory 인터페이스
class AbstractFactory {
public:
virtual std::unique_ptr<AbstractProductA> createProductA() const = 0;
virtual std::unique_ptr<AbstractProductB> createProductB() const = 0;
virtual ~AbstractFactory() = default;
};
// ConcreteFactory1 클래스
class ConcreteFactory1 : public AbstractFactory {
public:
std::unique_ptr<AbstractProductA> createProductA() const override {
return std::make_unique<ConcreteProductA1>();
}
std::unique_ptr<AbstractProductB> createProductB() const override {
return std::make_unique<ConcreteProductB1>();
}
};
// ConcreteFactory2 클래스
class ConcreteFactory2 : public AbstractFactory {
public:
std::unique_ptr<AbstractProductA> createProductA() const override {
return std::make_unique<ConcreteProductA2>();
}
std::unique_ptr<AbstractProductB> createProductB() const override {
return std::make_unique<ConcreteProductB2>();
}
};
int main() {
std::unique_ptr<AbstractFactory> factory1 = std::make_unique<ConcreteFactory1>();
std::unique_ptr<AbstractFactory> factory2 = std::make_unique<ConcreteFactory2>();
auto productA1 = factory1->createProductA();
auto productB1 = factory1->createProductB();
auto productA2 = factory2->createProductA();
auto productB2 = factory2->createProductB();
productA1->use(); // Using ConcreteProductA1
productB1->eat(); // Eating ConcreteProductB1
productA2->use(); // Using ConcreteProductA2
productB2->eat(); // Eating ConcreteProductB2
return 0;
}
이건 이제 각 객체들을 하나의 "군"으로 묶어서 생산할 수 있도록 하는 것이다.
예를 들어서, 식품 관련 공장을 만들 때 "유제품 공장", "육류 공장"으로 나눠서 유제품 공장에서는 우유와 요거트를, 육류 공장에서는 소시지와 베이컨을 생산하도록 하는 것이다.
특히나 이런 하나의 군에 속하는 객체를 동시에 한 번에 생산하기 좋은 패턴이다.
팩토리 메서드 패턴과 추상 팩토리 패턴은 유사하지만 그 목적이 조금 다르다.
팩토리 메서드 패턴은 객체 생성을 위한 인터페이스를 정의하고, 서브클래스가 객체 생성의 구체적인 방법을 결정하도록 해서 단일 제품에 대한 생산 라인을 확보하는 것이 목표이다.
추상 팩토리 패턴은 관련된 객체들의 군(family)을 생성하기 위한 인터페이스를 제공하여 여러 제품군을 생성하는 것을 목표로 한다.
간단한 예시를 들어보자.
첫번째 상황 - 궁수와 총잡이를 구현할 때
궁수와 총잡이 캐릭터를 구현하자.
두 직업의 특징은 모든 공격과 스킬에 "투사체"가 존재한다는 것이다.
궁수는 항상 "화살"이 필요하고, 총잡이는 항상 "총알"이 필요하다.
이럴 때는 어떤 패턴을 사용하는 것이 옳을까?
바로 "팩토리 메서드 패턴"이다.
궁수는 총알 생성에는 관심 없다. 화살만 생성하면 되므로 화살 공장이 필요하다.
총잡이는 화살 생성에는 관심 없다. 총알만 생성하면 되므로 총알 공장이 필요하다.
이렇듯 각각의 객체를 따로 단일 객체만 생성해야 할 때 팩토리 메서드 패턴을 사용한다.
두번째 상황 - 언데드 출몰 지역을 구현할 때
RPG 게임에서 언데드 출몰 지역을 구현하자.
해당 구역에서는 일반 동물이나 고블린, 오크와 같은 몬스터는 등장하지 않는다.
오직 좀비, 구울, 강시와 같은 "언데드" 몬스터만 등장한다.
이런 상황이면 어떤 패턴을 사용해야 할까?
바로 "추상 팩토리 패턴"이다.
좀비와 구울, 강시를 언데드라는 제품군으로 묶어서 한번에 생산하는 공장을 만들자.
해당 공장에서 각 몬스터를 랜덤하게 생성해서 배치하게 하면 언데드 출몰 지역을 구현하기 수월할 것이다.
이렇듯 하나의 군을 생성할 때 추상 팩토리 패턴을 사용한다.
굳이 이 두 개념을 완전히 분리하지 말고, 그냥 상황에 맞춰서 '아, 생산 공장을 만들어야겠다'라는 생각 아래 구현을 하다보면 알아서 저 두 개념이 적용되어 코드로 나타날 것이다.
'CS > 면접질문 시리즈' 카테고리의 다른 글
클라이언트 개발자 면접 질문 시리즈(1-1) - 스레드와 프로세스의 차이 (0) | 2025.03.13 |
---|---|
C# - String Builder에 대해 알아보자 (0) | 2024.07.16 |
객체지향 5원칙 - SOLID 원칙, 추상 클래스 vs 인터페이스(C#, C++) (0) | 2024.07.14 |
클라이언트 기술 면접 질문 - 항상 나오는 질문, 디퍼드 vs 포워드?(2) 디퍼드 렌더링이 항상 옳은 선택인가? (0) | 2024.05.31 |
클라이언트 기술 면접 질문 - 항상 나오는 질문, 디퍼드 vs 포워드?(1) 일단 뭔지 알아보자 (0) | 2024.05.29 |