bonggyulim 님의 블로그
디자인패턴이란 본문
프로그램을 만들다 보면 비슷한 구조의 문제를 반복해서 만나게 된다.
객체를 어떻게 생성할지, 기능을 어떻게 유연하게 바꿀지, 객체들 사이의 의존성을 어떻게 줄일지 같은 문제들이다.
이런 반복되는 설계 문제에 대해 많이 사용되는 해법을 정리한 것이 디자인패턴(Design Pattern) 이다.
디자인패턴은 정답 코드가 아니라, 유지보수하기 좋은 구조를 만들기 위한 설계 방법이라고 볼 수 있다.
디자인패턴에는 어떤 것들이 있을까?
디자인패턴은 생성패턴, 구조패턴, 행위패턴 3가지 종류로 나눠서 본다.
이번 글에서는 Python 예시로 아래 중요 패턴 5가지를 정리해보겠다.
- Singleton: 객체를 하나만 생성해서 공유
- Factory Method: 객체 생성 책임을 분리
- Strategy: 알고리즘/정책을 교체 가능하게 설계
- Observer: 상태 변화를 여러 객체에 알림
- Adapter: 인터페이스가 다른 객체를 연결
1. Singleton 패턴
Singleton 패턴은 인스턴스를 하나만 생성하도록 제한하는 패턴이다.
즉, 어떤 객체가 프로그램 전체에서 하나만 존재해야 할 때 사용한다.
# =========================
# 1. Singleton Pattern
# =========================
# 목적:
# - 인스턴스를 하나만 만들고 계속 재사용
# - 설정값, 로그 관리자 같은 전역 공용 객체에 사용
class Config:
_instance = None # 클래스 변수: 만들어진 인스턴스를 저장
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls) # 아직 객체가 없으면 한 번만 생성
cls._instance.settings = {} # 이미 객체가 있으면 기존 객체를 그대로 반환
return cls._instance
config1 = Config()
config2 = Config()
config1.settings["theme"] = "dark"
print(config1.settings) # {'theme': 'dark'}
print(config2.settings) # {'theme': 'dark'}
2. Factory Method 패턴
Factory Method 패턴은 객체 생성 책임을 별도의 메서드나 클래스에 맡기는 패턴이다.
객체를 직접 생성하지 않고 “필요한 객체를 대신 만들어주는 공장”을 두는 방식이다.
# =========================
# 2. Factory Method Pattern
# =========================
# 목적:
# - 객체 생성 책임을 따로 분리
# - 어떤 객체를 만들지 결정하는 로직을 한 곳에 모음
class EmailSender:
def send(self, message):
print(f"이메일 전송: {message}")
class SmsSender:
def send(self, message):
print(f"SMS 전송: {message}")
class NotificationFactory:
@staticmethod
def create(channel):
# 생성 로직을 여기서 관리
if channel == "email":
return EmailSender()
elif channel == "sms":
return SmsSender()
else:
raise ValueError("지원하지 않는 채널")
sender = NotificationFactory.create("email")
sender.send("안녕하세요")
Factory를 사용하면 객체를 어떻게 생성할지를 숨길 수 있다.
- 클라이언트는 무엇이 필요한지만 말한다
- 실제 생성은 Factory가 담당한다
3. Strategy 패턴
Strategy 패턴은 같은 목적을 수행하는 여러 알고리즘을 각각 분리하고, 필요에 따라 교체해서 사용하는 패턴이다.
기능은 같지만 방식이 여러 개일 때 유용하다.
# =========================
# 3. Strategy Pattern
# =========================
# 목적:
# - 같은 기능을 여러 방식으로 처리
# - 상황에 따라 알고리즘/정책을 갈아끼움
class NormalDiscount:
def apply(self, price):
return price
class MemberDiscount:
def apply(self, price):
return price * 0.9
class VipDiscount:
def apply(self, price):
return price * 0.8
class PaymentService:
def __init__(self, discount_strategy):
# 어떤 할인 정책을 쓸지 외부에서 주입받음
self.discount_strategy = discount_strategy
def calculate_price(self, price):
return self.discount_strategy.apply(price)
service1 = PaymentService(NormalDiscount())
service2 = PaymentService(MemberDiscount())
service3 = PaymentService(VipDiscount())
print(service1.calculate_price(10000)) # 10000
print(service2.calculate_price(10000)) # 9000.0
print(service3.calculate_price(10000)) # 8000.0
할인 정책이 일반, 회원, VIP로 나뉜다고 했을때 if-elif로 계속 처리하면 코드가 길어지고 수정도 어려워진다.
Strategy 패턴은 할인 정책별로 클래스를 따로 만들고, 필요한 정책을 주입받아 사용하게 만든다.
PPE Guard에서의 예시
PPE를 분석한다는 목적은 같지만, 입력 방식에 따라 처리 전략이 달랐다.
- 비디오는 세그먼트 단위 처리
- 웹캠은 실시간 이벤트 전송
둘 다 “PPE 탐지 + OCR + 결과 저장”이라는 큰 목적은 같지만, 내부 동작은 다르다.
4. Observer 패턴
Observer 패턴은 어떤 객체의 상태가 바뀌었을 때, 그 변화를 여러 객체에게 자동으로 알려주는 패턴이다.
# =========================
# 4. Observer Pattern
# =========================
# 목적:
# - 한 객체의 변화가 생기면 여러 객체에게 자동 알림
# - 구독 / 알림 구조
class EmailObserver:
def update(self, message):
print(f"[이메일 알림] {message}")
class SmsObserver:
def update(self, message):
print(f"[SMS 알림] {message}")
class NewsPublisher:
def __init__(self):
self.observers = [] # 구독자 목록
def subscribe(self, observer):
self.observers.append(observer)
def notify(self, message):
# 상태 변화가 생기면 모든 구독자에게 알림
for observer in self.observers:
observer.update(message)
publisher = NewsPublisher() # 인스턴스 생성
publisher.subscribe(EmailObserver()) # 구독자 추가
publisher.subscribe(SmsObserver())
publisher.notify("새 뉴스가 등록되었습니다.")
NewsPublisher는 구독자 목록을 가지고 있고, 새 소식이 생기면 모든 구독자에게 알림을 보낸다.
이 구조의 장점은 발행자(Publisher)가 구독자의 구체적인 내부 동작을 몰라도 된다는 점이다.
5. Adapter 패턴
Adapter 패턴은 서로 다른 인터페이스를 가진 객체들을 연결해주는 패턴이다.
기존 코드가 기대하는 방식과 실제 외부 라이브러리의 방식이 다를 때 중간에서 맞춰준다.
# =========================
# 5. Adapter Pattern
# =========================
# 목적:
# - 인터페이스가 다른 기존 클래스를 현재 코드에 맞게 연결
# - 기존 코드 수정 없이 재사용 가능
class OldPrinter:
def print_text(self, text):
print(f"기존 프린터 출력: {text}")
class PrinterAdapter:
def __init__(self, old_printer):
self.old_printer = old_printer
def print(self, message):
# 현재 코드가 기대하는 print()를
# 기존 클래스의 print_text()로 연결
self.old_printer.print_text(message)
def client_code(printer):
# 여기서는 print() 메서드가 있다고 가정
printer.print("Hello Adapter")
old_printer = OldPrinter()
adapter = PrinterAdapter(old_printer)
client_code(adapter)
client_code()는 print() 메서드를 기대한다. 그런데 기존 프린터는 print_text()만 가지고 있다.
이때 Adapter가 중간에서 메서드 이름과 사용 방식을 맞춰준다. 그래서 기존 클래스를 수정하지 않고도 재사용할 수 있다.
PPE Guard에서의 예시
YOLO나 EasyOCR는 원래 자기들 방식의 API가 있지만 서비스 계층에서는 그런 외부 API를 직접 알 필요가 없다.
나중에 OCR 엔진을 바꾸더라도
서비스 로직 전체를 뜯어고치지 않고 Adapter 구현체만 바꾸면 된다.
결론
결국 디자인패턴은 단순히 개념을 외우기 위한 것이 아니라, 실제 프로젝트에서 기능을 분리하고 변경에 유연하게 대응하기 위한 설계 방식 이다.
특히 기능 단위로 개발해야 하는 프로젝트에서는 하나의 큰 로직에 모든 기능을 넣기보다,
역할을 나누고, 교체 가능한 구조로 만들고, 외부 의존성을 분리하는 방식이 더 효과적이다.
이런 구조는 기능 추가와 수정이 쉬울 뿐 아니라, 팀원들이 기능별로 나누어 개발한 뒤 다시 통합하는 과정에서도 유지보수성과 협업 효율을 높여준다.
결국 좋은 설계는 패턴 이름을 많이 아는 것이 아니라,
현재 프로젝트에 맞는 구조를 선택해 기능 단위 개발이 가능하고 확장 가능한 코드로 만드는 것이라고 생각한다.
'CS & Fundamentals' 카테고리의 다른 글
| I/O 바운드와 CPU 바운드 (0) | 2026.04.06 |
|---|---|
| 앱에서 API 요청부터 응답까지의 통신 과정 정리 (0) | 2026.03.12 |
| SOLID와 클린 아키텍처 (0) | 2026.03.11 |