소프트웨어 디자인 패턴을 이해하고 적용하는 것은 소프트웨어 개발에서 매우 중요한 일입니다. 이번 포스트에서는 '팩토리 패턴'과 '빌더 패턴'을 중점적으로 다루어 보겠습니다. 이 패턴들을 이해하면 코드의 구조를 더 명확하고 관리하기 쉽게 만들 수 있습니다. 자, 그럼 시작해 볼까요?
디자인 패턴은 소프트웨어 디자인에서 흔히 발생하는 문제를 해결하기 위한 미리 만들어진 청사진입니다.
패턴은 특정 코드가 아니라 문제를 해결하기 위한 일반적인 개념입니다.
프로그래머로 일하면서 하나의 패턴도 모른 채 일할 수도 있고, 자신도 모르게 패턴을 사용할 수도 있습니다. 그렇다면 패턴을 배우는 이유는 무엇일까요?
팩토리 패턴은 객체 생성 로직을 별도의 팩토리 클래스로 분리하는 디자인 패턴입니다. 이를 통해 코드의 유연성과 재사용성을 크게 향상시킬 수 있습니다.
먼저, CPU라는 인터페이스를 정의해 보겠습니다. 이 인터페이스에는 process
라는 추상 메서드가 포함되어 있습니다.
from abc import ABC, abstractmethod
class CPU(ABC):
@abstractmethod
def process(self, tasks: list[int]) -> list[list[int]]:
pass
CPU를 구현하는 두 가지 클래스, SingleCoreCPU와 DoubleCoreCPU를 만들어 보겠습니다.
class SingleCoreCPU(CPU):
def process(self, tasks: list[int]) -> list[list[int]]:
return [tasks]
class DoubleCoreCPU(CPU):
def process(self, tasks: list[int]) -> list[list[int]]:
return [tasks[::2], tasks[1::2]]
이제 팩토리 클래스를 만들어서 CPU 객체를 생성해 보겠습니다.
class CPUFactory:
@staticmethod
def make_cpu(type: str) -> CPU:
if type == "single":
return SingleCoreCPU()
elif type == "dual":
return DoubleCoreCPU()
else:
raise ValueError(f"CPU type {type} not supported")
CPUFactory를 사용하여 CPU 객체를 생성할 수 있습니다. 예를 들어, SingleCoreCPU를 생성하려면 다음과 같이 작성합니다.
cpu = CPUFactory.make_cpu("single")
print(cpu.process([1, 2, 3, 4])) # 결과: [[1, 2, 3, 4]]
추상 팩토리 패턴은 관련된 객체들의 집합을 생성하는 인터페이스를 정의하는 패턴입니다. 팩토리 패턴의 확장형이라 할 수 있습니다.
RAM과 ROM이라는 추상 클래스를 정의해 보겠습니다.
from abc import ABC, abstractmethod
from pydantic import BaseModel
class Memory(BaseModel, ABC):
data: list[int]
@property
def size(self) -> int:
return len(self.data)
@abstractmethod
def read(self, idx: int) -> int:
pass
@abstractmethod
def write(self, idx: int, value: int):
pass
이 추상 클래스를 상속받아 RAM과 ROM 클래스를 구현합니다.
class Ram(Memory):
def read(self, idx: int) -> int:
return self.data[idx]
def write(self, idx: int, value: int):
self.data[idx] = value
class Rom(Memory):
def read(self, idx: int) -> int:
return self.data[idx]
def write(self, idx: int, value: int):
raise ValueError("ROM is read-only")
RAM (Random Access Memory) 과 ROM (Read-Only Memory)의 가장 큰 차이는 RAM은 메모리에 write 작업이 가능하지만 ROM은 write작업이 불가능하다는 점입니다.
RAM과 ROM을 생성하는 팩토리 클래스를 만들어 보겠습니다.
class MemoryFactory(ABC):
@staticmethod
@abstractmethod
def make_memory(size: int = 0, data: list[int] = []) -> Memory:
pass
class RamFactory(MemoryFactory):
@staticmethod
def make_memory(size: int = 0, data: list[int] = []) -> Ram:
if data:
raise ValueError("RAM cannot be initialized with data")
return Ram(data=[0] * size)
class RomFactory(MemoryFactory):
@staticmethod
def make_memory(size: int = 0, data: list[int] = []) -> Rom:
if size > 0:
raise ValueError("ROM cannot be initialized with size")
return Rom(data=data)
빌더 패턴은 복잡한 객체를 단계별로 생성하는 패턴입니다. 이를 통해 객체 생성 과정을 명확하고 유연하게 관리할 수 있습니다.
먼저 Computer라는 추상 클래스를 정의해 봅시다. 이 클래스는 CPU, RAM, ROM을 포함하며, bootstrap
이라는 추상 메서드를 가지고 있습니다.
from abc import ABC, abstractmethod
from pydantic import BaseModel, ConfigDict
class Computer(BaseModel, ABC):
model_config = ConfigDict(arbitrary_types_allowed=True)
cpu: CPU
ram: Ram
rom: Rom
@abstractmethod
def bootstrap(self) -> dict[str, list[int] | list[list[int]]]:
pass
Laptop과 Desktop이라는 두 가지 구체적인 Computer 클래스를 구현합니다.
class Laptop(Computer):
def bootstrap(self) -> dict[str, list[int] | list[list[int]]]:
state = {}
state["cpu_processed"] = self.cpu.process(tasks=[1, 2, 3, 4])
state["ram_data"] = self.ram.data
state["rom_data"] = self.rom.data
return state
class Desktop(Computer):
def bootstrap(self) -> dict[str, list[int] | list[list[int]]]:
state = {}
state["cpu_processed"] = self.cpu.process(tasks=[1, 2, 3, 4, 5, 6, 7, 8])
state["ram_data"] = self.ram.data
state["rom_data"] = self.rom.data
return state
마지막으로 ComputerBuilder 클래스를 만들어서 Laptop과 Desktop를 생성해 보겠습니다.
이때 Laptop의 경우는 SingleCPU에 RAM size 8, ROM data [1,2,3,4] Desktop 경우에는 DualCPU에 RAM size 16, ROM data [1,2,3,4,5,6,7,8]로 하겠습니다.
class ComputerBuilder:
@staticmethod
def build_computer(type: str) -> Computer:
if type == "laptop":
cpu = CPUFactory.make_cpu("single")
ram = RamFactory.make_memory(size=8)
rom = RomFactory.make_memory(data=[1, 2, 3, 4])
return Laptop(cpu=cpu, ram=ram, rom=rom)
elif type == "desktop":
cpu = CPUFactory.make_cpu("dual")
ram = RamFactory.make_memory(size=16)
rom = RomFactory.make_memory(data=[1, 2, 3, 4, 5, 6, 7, 8])
return Desktop(cpu=cpu, ram=ram, rom=rom)
else:
raise ValueError(f"Computer type {type} not supported")
이제 ComputerBuilder를 사용하여 컴퓨터 객체를 생성하고 부트스트랩을 해보겠습니다.
computer = ComputerBuilder.build_computer(type="laptop")
state = computer.bootstrap()
print(state)
# 결과: {'cpu_processed': [[1, 2, 3, 4]], 'ram_data': [0, 0, 0, 0, 0, 0, 0, 0], 'rom_data': [1, 2, 3, 4]}
코드가 제대로 동작하는지 확인하려면 테스트가 필수적입니다. pytest를 사용하여 위의 클래스를 테스트해 볼 수 있습니다. 다음 코드를 통해 다양한 경우를 테스트할 수 있습니다.
import pytest
from computer import ComputerBuilder
from memory import RamFactory, RomFactory
from cpu import CPUFactory
@pytest.mark.computer
class TestComputer:
def test_laptop(self):
computer = ComputerBuilder.build_computer(type="laptop")
state = computer.bootstrap()
assert state["cpu_processed"] == [[1, 2, 3, 4]]
assert state["ram_data"] == [0] * 8
assert state["rom_data"] == [1, 2, 3, 4]
def test_desktop(self):
computer = ComputerBuilder.build_computer(type="desktop")
state = computer.bootstrap()
assert state["cpu_processed"] == [[1, 3, 5, 7], [2, 4, 6, 8]]
assert state["ram_data"] == [0] * 16
assert state["rom_data"] == [1, 2, 3, 4, 5, 6, 7, 8]
def test_invalid_computer(self):
with pytest.raises(ValueError):
ComputerBuilder.build_computer(type="invalid")
@pytest.mark.memory
class TestMemory:
def test_ram(self):
memory = RamFactory.make_memory(size=8)
assert memory.size == 8
assert memory.read(0) == 0
assert memory.read(7) == 0
memory.write(0, 1)
assert memory.read(0) == 1
memory.write(7, 2)
assert memory.read(7) == 2
def test_rom(self):
memory = RomFactory.make_memory(data=[1, 2, 3, 4])
assert memory.size == 4
assert memory.read(0) == 1
assert memory.read(3) == 4
with pytest.raises(ValueError):
memory.write(0, 1)
with pytest.raises(ValueError):
memory.write(3, 4)
def test_invalid_memory(self):
with pytest.raises(ValueError):
RamFactory.make_memory(data=[1, 2, 3, 4])
with pytest.raises(ValueError):
RomFactory.make_memory(size=8)
@pytest.mark.cpu
class TestCPU:
def test_single_core(self):
cpu = CPUFactory.make_cpu(type="single")
assert cpu.process([1, 2, 3, 4]) == [[1, 2, 3, 4]]
def test_dual_core(self):
cpu = CPUFactory.make_cpu(type="dual")
assert cpu.process([1, 2, 3, 4]) == [[1, 3], [2, 4]]
def test_invalid_cpu(self):
with pytest.raises(ValueError):
CPUFactory.make_cpu(type="invalid")
테스트를 실행하면 우리가 만든 코드가 예상대로 동작하는지 확인할 수 있습니다.
이번 포스트에서는 팩토리 패턴과 빌더 패턴을 사용하여 CPU, RAM, ROM으로 구성된 컴퓨터 객체를 만들어보았습니다. 이러한 디자인 패턴을 사용하면 전체 코드의 구조가 명확해지고, 유지 보수와 확장이 용이해집니다. 디자인 패턴을 더 깊이 이해하려면 다양한 예제를 통해 계속해서 학습하는 것이 중요합니다.
🔥 해당 포스팅은 Dev.POST 도움을 받아 작성되었습니다.