소프트웨어 개발에서 반복적으로 나타나는 문제들을 해결하기 위한 검증된 설계 템플릿들을 체계적으로 정리해보겠습니다. 각 패턴은 특정 상황에서 코드의 유지보수성, 확장성, 재사용성을 크게 향상시킵니다.
🔧 Strategy Pattern (전략 패턴)
목적: 알고리즘군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만드는 패턴
핵심 개념: 런타임에 객체의 행동을 변경할 수 있도록 알고리즘을 분리하여 독립적으로 변경 가능하게 합니다.
구현 예시 – 버튼 타입별 실행 방식:
pythonfrom abc import ABC, abstractmethod
class ButtonStrategy(ABC):
@abstractmethod
def execute(self, config):
pass
class WebButtonStrategy(ButtonStrategy):
def execute(self, config):
return f"웹 브라우저로 {config['url']} 열기"
class ProgramButtonStrategy(ButtonStrategy):
def execute(self, config):
return f"프로그램 {config['path']} 실행"
class FileDialogButtonStrategy(ButtonStrategy):
def execute(self, config):
return f"파일 다이얼로그 {config['filter']}로 열기"
class Button:
def __init__(self, strategy: ButtonStrategy):
self.strategy = strategy
def click(self, config):
return self.strategy.execute(config)
장점: 조건문 없이 깔끔하게 다양한 실행 방식을 처리할 수 있으며, 새로운 버튼 타입 추가가 용이합니다.
🏭 Factory Pattern (팩토리 패턴)
목적: 객체 생성 로직을 별도 클래스로 분리하여 객체 생성을 캡슐화하는 패턴
핵심 개념: 클라이언트는 구체적인 클래스를 몰라도 인터페이스를 통해 객체를 생성할 수 있습니다.
구현 예시 – JSON 기반 동적 UI 생성:
pythonimport json
from typing import Dict, Any
class UIElement:
def __init__(self, config: Dict[str, Any]):
self.config = config
class Button(UIElement):
def render(self):
return f"Button: {self.config['text']}"
class Label(UIElement):
def render(self):
return f"Label: {self.config['text']}"
class UIFactory:
_creators = {
'button': Button,
'label': Label
}
@classmethod
def create_element(cls, element_type: str, config: Dict[str, Any]):
creator = cls._creators.get(element_type)
if not creator:
raise ValueError(f"Unknown element type: {element_type}")
return creator(config)
@classmethod
def create_from_json(cls, json_config: str):
config = json.loads(json_config)
elements = []
for item in config['elements']:
element = cls.create_element(item['type'], item)
elements.append(element)
return elements
장점: JSON 설정만 변경하면 UI 구성을 완전히 바꿀 수 있어 유연성이 극대화됩니다.
📋 Template Method Pattern (템플릿 메서드 패턴)
목적: 알고리즘의 구조를 정의하고 하위 클래스에서 특정 단계를 재정의할 수 있게 하는 패턴
핵심 개념: 전체적인 흐름은 상위 클래스에서 제어하고, 세부 구현만 하위 클래스에서 담당합니다.
구현 예시 – 앱 초기화 프로세스:
pythonfrom abc import ABC, abstractmethod
class ApplicationInitializer(ABC):
def initialize(self):
"""템플릿 메서드: 초기화 순서를 정의"""
self.load_configuration()
self.apply_theme()
self.create_ui()
self.setup_event_handlers()
@abstractmethod
def load_configuration(self):
pass
@abstractmethod
def apply_theme(self):
pass
@abstractmethod
def create_ui(self):
pass
def setup_event_handlers(self):
"""공통 이벤트 핸들러 설정"""
print("기본 이벤트 핸들러 설정 완료")
class LauncherInitializer(ApplicationInitializer):
def load_configuration(self):
print("JSON 설정 파일 로드")
def apply_theme(self):
print("다크/라이트 테마 적용")
def create_ui(self):
print("동적 버튼 UI 생성")
장점: 일관된 초기화 순서를 보장하면서도 각 단계의 구체적인 구현은 유연하게 변경할 수 있습니다.
👀 Observer Pattern (옵저버 패턴)
목적: 객체 간의 일대다 의존성을 정의하여 한 객체의 상태 변화를 여러 객체에 자동으로 통지하는 패턴
핵심 개념: Subject(주제)의 상태가 변경되면 등록된 모든 Observer(관찰자)에게 자동으로 알림이 전송됩니다.
구현 예시 – WebView2 상태 모니터링:
pythonfrom typing import List, Protocol
class Observer(Protocol):
def update(self, event_type: str, data: dict) -> None:
pass
class WebViewSubject:
def __init__(self):
self._observers: List[Observer] = []
self._navigation_state = "idle"
def attach(self, observer: Observer):
self._observers.append(observer)
def detach(self, observer: Observer):
self._observers.remove(observer)
def notify(self, event_type: str, data: dict):
for observer in self._observers:
observer.update(event_type, data)
def navigate_to(self, url: str):
self._navigation_state = "navigating"
self.notify("navigation_started", {"url": url})
# 네비게이션 완료 시뮬레이션
self._navigation_state = "completed"
self.notify("navigation_completed", {"url": url})
class StatusBarObserver:
def update(self, event_type: str, data: dict):
if event_type == "navigation_started":
print(f"상태바: {data['url']} 로딩 중...")
elif event_type == "navigation_completed":
print(f"상태바: {data['url']} 로딩 완료")
class ProgressBarObserver:
def update(self, event_type: str, data: dict):
if event_type == "navigation_started":
print("프로그레스바: 로딩 애니메이션 시작")
elif event_type == "navigation_completed":
print("프로그레스바: 로딩 완료, 숨김 처리")
장점: UI 컴포넌트들이 WebView 상태에 자동으로 반응하여 사용자 경험이 향상됩니다.
📦 Command Pattern (커맨드 패턴)
목적: 요청을 객체로 캡슐화하여 요청을 매개변수화하고 큐에 저장하거나 로깅, 되돌리기 등을 지원하는 패턴
핵심 개념: 행동(Action)을 객체로 만들어 실행, 취소, 로깅, 큐잉 등이 가능해집니다.
구현 예시 – 버튼 액션 캡슐화:
pythonfrom abc import ABC, abstractmethod
import subprocess
import webbrowser
class ItemAction(ABC):
@abstractmethod
def execute(self) -> bool:
pass
@abstractmethod
def can_undo(self) -> bool:
pass
@abstractmethod
def undo(self) -> bool:
pass
class WebAction(ItemAction):
def __init__(self, url: str):
self.url = url
def execute(self) -> bool:
try:
webbrowser.open(self.url)
return True
except Exception as e:
print(f"웹 열기 실패: {e}")
return False
def can_undo(self) -> bool:
return False # 웹 페이지는 되돌릴 수 없음
def undo(self) -> bool:
return False
class ProgramAction(ItemAction):
def __init__(self, path: str, args: list = None):
self.path = path
self.args = args or []
self.process = None
def execute(self) -> bool:
try:
self.process = subprocess.Popen([self.path] + self.args)
return True
except Exception as e:
print(f"프로그램 실행 실패: {e}")
return False
def can_undo(self) -> bool:
return self.process and self.process.poll() is None
def undo(self) -> bool:
if self.can_undo():
self.process.terminate()
return True
return False
class ActionInvoker:
def __init__(self):
self.history = []
def execute_action(self, action: ItemAction):
if action.execute():
self.history.append(action)
def undo_last_action(self):
if self.history:
action = self.history.pop()
if action.can_undo():
action.undo()
장점: 각 버튼의 기능을 독립적인 객체로 관리하여 실행 이력 추적, 되돌리기 등의 고급 기능을 쉽게 구현할 수 있습니다.
💉 Dependency Injection (의존성 주입)
목적: 객체의 의존성을 외부에서 주입하여 결합도를 낮추고 테스트 용이성을 높이는 패턴
핵심 개념: 객체가 직접 의존성을 생성하지 않고 外부에서 주입받아 사용합니다.
구현 예시 – 런처 의존성 관리:
pythonfrom typing import Protocol
class ConfigLoader(Protocol):
def load_config(self) -> dict:
pass
class ThemeManager(Protocol):
def apply_theme(self, theme_name: str) -> None:
pass
class UIRenderer(Protocol):
def render(self, config: dict) -> None:
pass
class JSONConfigLoader:
def __init__(self, file_path: str):
self.file_path = file_path
def load_config(self) -> dict:
import json
with open(self.file_path, 'r', encoding='utf-8') as f:
return json.load(f)
class DarkThemeManager:
def apply_theme(self, theme_name: str) -> None:
print(f"다크 테마 '{theme_name}' 적용")
class ModernUIRenderer:
def render(self, config: dict) -> None:
print(f"모던 UI 렌더링: {len(config.get('buttons', []))}개 버튼")
class Launcher:
def __init__(self,
config_loader: ConfigLoader,
theme_manager: ThemeManager,
ui_renderer: UIRenderer):
self.config_loader = config_loader
self.theme_manager = theme_manager
self.ui_renderer = ui_renderer
def start(self):
config = self.config_loader.load_config()
self.theme_manager.apply_theme(config.get('theme', 'default'))
self.ui_renderer.render(config)
# 의존성 주입을 통한 런처 생성
def create_launcher():
config_loader = JSONConfigLoader('config.json')
theme_manager = DarkThemeManager()
ui_renderer = ModernUIRenderer()
return Launcher(config_loader, theme_manager, ui_renderer)
장점: 각 컴포넌트를 독립적으로 테스트할 수 있고, 런타임에 다른 구현체로 쉽게 교체할 수 있습니다.
🎨 Builder Pattern (빌더 패턴)
목적: 복잡한 객체의 생성 과정을 단계별로 나누어 동일한 생성 절차에서 서로 다른 표현 결과를 만드는 패턴
핵심 개념: 객체 생성의 복잡함을 숨기고 직관적인 인터페이스로 단계별 구성을 가능하게 합니다.
구현 예시 – 복잡한 설정 객체 구성:
pythonfrom dataclasses import dataclass
from typing import List, Optional
@dataclass
class ButtonConfig:
text: str
action_type: str
action_data: dict
style: dict = None
@dataclass
class ThemeConfig:
name: str
colors: dict
fonts: dict
@dataclass
class LauncherConfig:
title: str
buttons: List[ButtonConfig]
theme: ThemeConfig
window_size: tuple
auto_start: bool = False
class LauncherConfigBuilder:
def __init__(self):
self.reset()
def reset(self):
self._title = "기본 런처"
self._buttons = []
self._theme = None
self._window_size = (800, 600)
self._auto_start = False
return self
def set_title(self, title: str):
self._title = title
return self
def add_button(self, text: str, action_type: str, action_data: dict, style: dict = None):
button = ButtonConfig(text, action_type, action_data, style)
self._buttons.append(button)
return self
def set_theme(self, name: str, colors: dict, fonts: dict):
self._theme = ThemeConfig(name, colors, fonts)
return self
def set_window_size(self, width: int, height: int):
self._window_size = (width, height)
return self
def set_auto_start(self, auto_start: bool):
self._auto_start = auto_start
return self
def build(self) -> LauncherConfig:
if not self._theme:
self._theme = ThemeConfig("default", {"bg": "#ffffff"}, {"main": "Arial"})
return LauncherConfig(
title=self._title,
buttons=self._buttons,
theme=self._theme,
window_size=self._window_size,
auto_start=self._auto_start
)
# 사용 예시
config = (LauncherConfigBuilder()
.set_title("개발자 도구 런처")
.add_button("VS Code", "program", {"path": "code.exe"})
.add_button("GitHub", "web", {"url": "https://github.com"})
.set_theme("dark", {"bg": "#2d2d2d", "fg": "#ffffff"}, {"main": "Consolas"})
.set_window_size(1200, 800)
.set_auto_start(True)
.build())
장점: 복잡한 설정 객체를 직관적이고 읽기 쉬운 방식으로 생성할 수 있으며, 필수/선택 매개변수를 명확히 구분할 수 있습니다.
🔄 State Pattern (상태 패턴)
목적: 객체의 내부 상태가 바뀜에 따라 객체의 행동을 바꿀 수 있게 하는 패턴
핵심 개념: 상태별 행동을 별도 클래스로 분리하여 상태 전환을 명확하게 관리합니다.
구현 예시 – 테마별 상태 관리:
pythonfrom abc import ABC, abstractmethod
class ThemeState(ABC):
@abstractmethod
def get_colors(self) -> dict:
pass
@abstractmethod
def get_layout_style(self) -> dict:
pass
@abstractmethod
def apply_to_component(self, component):
pass
class LightThemeState(ThemeState):
def get_colors(self) -> dict:
return {
"background": "#ffffff",
"foreground": "#000000",
"accent": "#0078d4",
"border": "#e1e1e1"
}
def get_layout_style(self) -> dict:
return {
"padding": 10,
"margin": 5,
"border_width": 1,
"shadow": True
}
def apply_to_component(self, component):
colors = self.get_colors()
component.set_background(colors["background"])
component.set_foreground(colors["foreground"])
class DarkThemeState(ThemeState):
def get_colors(self) -> dict:
return {
"background": "#2d2d2d",
"foreground": "#ffffff",
"accent": "#00b4ff",
"border": "#404040"
}
def get_layout_style(self) -> dict:
return {
"padding": 12,
"margin": 6,
"border_width": 0,
"shadow": False
}
def apply_to_component(self, component):
colors = self.get_colors()
component.set_background(colors["background"])
component.set_foreground(colors["foreground"])
class HighContrastThemeState(ThemeState):
def get_colors(self) -> dict:
return {
"background": "#000000",
"foreground": "#ffffff",
"accent": "#ffff00",
"border": "#ffffff"
}
def get_layout_style(self) -> dict:
return {
"padding": 15,
"margin": 8,
"border_width": 2,
"shadow": False
}
def apply_to_component(self, component):
colors = self.get_colors()
component.set_background(colors["background"])
component.set_foreground(colors["foreground"])
class ThemeManager:
def __init__(self):
self._current_state = LightThemeState()
self._states = {
"light": LightThemeState(),
"dark": DarkThemeState(),
"high_contrast": HighContrastThemeState()
}
def set_theme(self, theme_name: str):
if theme_name in self._states:
self._current_state = self._states[theme_name]
self._apply_current_theme()
def _apply_current_theme(self):
# 모든 UI 컴포넌트에 현재 테마 적용
colors = self._current_state.get_colors()
layout = self._current_state.get_layout_style()
print(f"테마 적용: {colors}, 레이아웃: {layout}")
def get_current_colors(self) -> dict:
return self._current_state.get_colors()
def get_current_layout(self) -> dict:
return self._current_state.get_layout_style()
장점: 새로운 테마 추가가 쉽고, 각 테마의 복잡한 설정을 독립적으로 관리할 수 있습니다.
🎭 Adapter Pattern (어댑터 패턴)
목적: 호환되지 않는 인터페이스를 가진 클래스들이 함께 작동할 수 있도록 기존 클래스의 인터페이스를 다른 인터페이스로 변환하는 패턴
핵심 개념: 기존 코드를 수정하지 않고도 새로운 인터페이스에 맞게 동작하도록 중간 변환 계층을 제공합니다.
구현 예시 – 하드코딩 버튼을 JSON 설정으로 변환:
pythonfrom typing import List, Dict, Any
# 기존 하드코딩된 버튼 클래스들
class LegacyWebButton:
def __init__(self, url: str, title: str):
self.url = url
self.title = title
def open_url(self):
print(f"웹 브라우저로 {self.url} 열기")
class LegacyProgramButton:
def __init__(self, exe_path: str, name: str):
self.exe_path = exe_path
self.name = name
def launch_program(self):
print(f"프로그램 {self.exe_path} 실행")
# 새로운 JSON 기반 인터페이스
class JsonButton:
def __init__(self, config: Dict[str, Any]):
self.config = config
def execute(self):
action_type = self.config.get('action_type')
if action_type == 'web':
print(f"웹: {self.config['url']} 열기")
elif action_type == 'program':
print(f"프로그램: {self.config['path']} 실행")
# 어댑터 클래스들
class LegacyWebButtonAdapter(JsonButton):
def __init__(self, legacy_button: LegacyWebButton):
self.legacy_button = legacy_button
# 레거시 버튼을 JSON 형태로 변환
config = {
'text': legacy_button.title,
'action_type': 'web',
'url': legacy_button.url
}
super().__init__(config)
def execute(self):
# 레거시 메서드 호출
self.legacy_button.open_url()
class LegacyProgramButtonAdapter(JsonButton):
def __init__(self, legacy_button: LegacyProgramButton):
self.legacy_button = legacy_button
config = {
'text': legacy_button.name,
'action_type': 'program',
'path': legacy_button.exe_path
}
super().__init__(config)
def execute(self):
self.legacy_button.launch_program()
# 마이그레이션 도우미
class ButtonMigrationAdapter:
def __init__(self):
self.buttons: List[JsonButton] = []
def add_legacy_web_button(self, url: str, title: str):
legacy_button = LegacyWebButton(url, title)
adapter = LegacyWebButtonAdapter(legacy_button)
self.buttons.append(adapter)
def add_legacy_program_button(self, exe_path: str, name: str):
legacy_button = LegacyProgramButton(exe_path, name)
adapter = LegacyProgramButtonAdapter(legacy_button)
self.buttons.append(adapter)
def add_json_button(self, config: Dict[str, Any]):
json_button = JsonButton(config)
self.buttons.append(json_button)
def execute_all(self):
for button in self.buttons:
button.execute()
# 사용 예시
migration = ButtonMigrationAdapter()
# 기존 하드코딩 버튼들을 어댑터로 감싸서 추가
migration.add_legacy_web_button("https://google.com", "구글")
migration.add_legacy_program_button("notepad.exe", "메모장")
# 새로운 JSON 기반 버튼도 함께 사용
migration.add_json_button({
'text': 'Visual Studio Code',
'action_type': 'program',
'path': 'code.exe'
})
migration.execute_all() # 모든 버튼이 통일된 방식으로 실행됨
장점: 기존 코드를 전면 수정하지 않고도 점진적으로 새로운 아키텍처로 마이그레이션할 수 있습니다.
📝 Configuration Pattern (설정 패턴)
목적: 애플리케이션의 동작을 런타임에 외부 설정으로 완전히 변경할 수 있게 하는 패턴
핵심 개념: 하드코딩된 값들을 외부 설정 파일로 분리하여 코드 수정 없이 애플리케이션 동작을 변경할 수 있습니다.
구현 예시 – 완전한 런타임 설정 변경:
pythonimport json
import os
from typing import Dict, Any, Optional
from dataclasses import dataclass, asdict
from pathlib import Path
@dataclass
class WindowConfig:
width: int = 800
height: int = 600
resizable: bool = True
always_on_top: bool = False
@dataclass
class ThemeConfig:
name: str = "default"
background_color: str = "#ffffff"
text_color: str = "#000000"
accent_color: str = "#0078d4"
@dataclass
class ButtonConfig:
text: str
action_type: str # web, program, file_dialog
action_data: Dict[str, Any]
position: tuple = (0, 0)
size: tuple = (100, 30)
@dataclass
class ApplicationConfig:
app_title: str = "동적 런처"
window: WindowConfig = None
theme: ThemeConfig = None
buttons: list = None
auto_save_config: bool = True
config_file_path: str = "config.json"
def __post_init__(self):
if self.window is None:
self.window = WindowConfig()
if self.theme is None:
self.theme = ThemeConfig()
if self.buttons is None:
self.buttons = []
class ConfigurationManager:
def __init__(self, config_path: str = "config.json"):
self.config_path = Path(config_path)
self.config: Optional[ApplicationConfig] = None
self._watchers = [] # 설정 변경 감시자들
def load_config(self) -> ApplicationConfig:
"""JSON 파일에서 설정 로드"""
if self.config_path.exists():
try:
with open(self.config_path, 'r', encoding='utf-8') as f:
data = json.load(f)
self.config = self._dict_to_config(data)
except Exception as e:
print(f"설정 로드 실패: {e}")
self.config = ApplicationConfig()
else:
self.config = ApplicationConfig()
self.save_config() # 기본 설정 파일 생성
return self.config
def save_config(self):
"""현재 설정을 JSON 파일로 저장"""
if self.config:
try:
with open(self.config_path, 'w', encoding='utf-8') as f:
data = self._config_to_dict(self.config)
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"설정 저장 실패: {e}")
def _dict_to_config(self, data: Dict[str, Any]) -> ApplicationConfig:
"""딕셔너리를 ApplicationConfig 객체로 변환"""
window_data = data.get('window', {})
theme_data = data.get('theme', {})
window = WindowConfig(**window_data)
theme = ThemeConfig(**theme_data)
buttons = []
for btn_data in data.get('buttons', []):
button = ButtonConfig(**btn_data)
buttons.append(button)
config = ApplicationConfig(
app_title=data.get('app_title', '동적 런처'),
window=window,
theme=theme,
buttons=buttons,
auto_save_config=data.get('auto_save_config', True),
config_file_path=data.get('config_file_path', 'config.json')
)
return config
def _config_to_dict(self, config: ApplicationConfig) -> Dict[str, Any]:
"""ApplicationConfig 객체를 딕셔너리로 변환"""
return {
'app_title': config.app_title,
'window': asdict(config.window),
'theme': asdict(config.theme),
'buttons': [asdict(btn) for btn in config.buttons],
'auto_save_config': config.auto_save_config,
'config_file_path': config.config_file_path
}
def update_config(self, **kwargs):
"""설정 업데이트"""
if self.config:
for key, value in kwargs.items():
if hasattr(self.config, key):
setattr(self.config, key, value)
if self.config.auto_save_config:
self.save_config()
self._notify_watchers()
def add_button(self, text: str, action_type: str, action_data: Dict[str, Any],
position: tuple = (0, 0), size: tuple = (100, 30)):
"""새 버튼 추가"""
if self.config:
button = ButtonConfig(text, action_type, action_data, position, size)
self.config.buttons.append(button)
if self.config.auto_save_config:
self.save_config()
self._notify_watchers()
def remove_button(self, index: int):
"""버튼 제거"""
if self.config and 0 <= index < len(self.config.buttons):
del self.config.buttons[index]
if self.config.auto_save_config:
self.save_config()
self._notify_watchers()
def watch_changes(self, callback):
"""설정 변경 감시자 등록"""
self._watchers.append(callback)
def _notify_watchers(self):
"""모든 감시자에게 변경 알림"""
for watcher in self._watchers:
watcher(self.config)
# 사용 예시
def on_config_changed(config: ApplicationConfig):
print(f"설정 변경됨: {config.app_title}")
print(f"버튼 개수: {len(config.buttons)}")
# 설정 관리자 초기화
config_manager = ConfigurationManager("launcher_config.json")
config_manager.watch_changes(on_config_changed)
# 설정 로드
app_config = config_manager.load_config()
# 런타임에 버튼 추가
config_manager.add_button(
"GitHub Desktop",
"program",
{"path": "C:/Users/User/AppData/Local/GitHubDesktop/GitHubDesktop.exe"},
(10, 10),
(150, 40)
)
# 테마 변경
config_manager.update_config(theme=ThemeConfig(
name="dark",
background_color="#2d2d2d",
text_color="#ffffff",
accent_color="#00b4ff"
))
장점: JSON 파일만 수정하면 애플리케이션의 모든 동작을 변경할 수 있어 개발자가 아닌 사용자도 커스터마이징이 가능합니다.
패턴들의 상호작용과 조합
이러한 패턴들은 독립적으로 사용될 수도 있지만, 실제 프로젝트에서는 여러 패턴을 조합하여 더욱 강력하고 유연한 아키텍처를 만들 수 있습니다:
일반적인 조합 예시:
- Factory + Strategy: 설정에 따라 다른 전략 객체를 생성
- Observer + Command: 명령 실행 상태를 여러 UI 컴포넌트에 알림
- Builder + Dependency Injection: 복잡한 의존성 그래프를 단계별로 구성
- Configuration + Template Method: 설정에 따라 초기화 과정의 각 단계를 다르게 구현
이러한 디자인 패턴들을 적절히 활용하면 코드의 가독성, 유지보수성, 확장성을 크게 향상시킬 수 있으며, 특히 사용자 요구사항이 자주 변경되는 프로젝트에서 그 진가를 발휘합니다.
답글 남기기